diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index bb79325a8..a89f8e5df 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -1232,6 +1232,30 @@ and reads nothing here ([#718](https://github.com/asc-community/AngouriMath/issu
| `"1/(x^2*sqrt(1-(a+b*x)^2))".Integrate("x")` | left unevaluated | the antiderivative |
| `"acos(a+b*x)/x^4".Integrate("x")` | left unevaluated | the antiderivative |
+### A perfect square written with symbols is read as one
+
+`(A + B x)(d + e x)/(a^2 + 2 a b x + b^2 x^2)^(5/2)` was left as written: the radicand is
+`(a + b x)^2`, and the power is `|a + b x|^5`, but three conditions between the rule and the
+rewrite each said no. The discriminant `4 a^2 b^2 - 4 a^2 b^2` is a zero
+`InnerSimplified` does not collect, so the square read as an ordinary quadratic. The leading
+coefficient `b^2` is not a *number*, though it is positive for a real parameter, which the answer
+now says as `provided b^2 > 0`. And the rule's own bound on how large a radicand it will read --
+there so that it costs nothing at every level of the descent -- is lifted at the top, where any
+half-odd power of a square is the power of the modulus; below it the square root only, since a
+sign written for a substitution's variable is a factor the rest of the search has to carry.
+
+The sign goes in front of the integral rather than into the integrand, which is what every rule
+here that writes one does: left inside, a rule below differentiated it, and
+`sqrt(a^2 + 2 a b x + b^2 x^2) sqrt(c + e x + d x^2)` threw
+`CannotEvalException: derivative(sgn(...))` rather than answering
+([#718](https://github.com/asc-community/AngouriMath/issues/718)).
+
+| Input | Was (2.5.0) | Now |
+|---|---|---|
+| `"(A+B*x)*(d+pe*x)/(a^2+2*a*b*x+b^2*x^2)^(5/2)".Integrate("x")` | left unevaluated | the antiderivative, `provided b^2 > 0` |
+| `"sqrt(a^2+2*a*b*x+b^2*x^2)*sqrt(c+pe*x+d*x^2)".Integrate("x")` | left unevaluated | the antiderivative |
+| `"(a^2+2*a*b*x+b^2*x^2)^(5/2)".Integrate("x")` | the antiderivative, through the substitution search | `sgn(a + b x) (a + b x)^6 |b|^5/(6 b)` up to the form |
+
### `binomial(n, k)` is a function
**Addition, not silent.** The binomial coefficient is a node, `Entity.Binomialf`, spelled
diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
index dae7a7793..ec8ebcba7 100644
--- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
+++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs
@@ -10197,6 +10197,20 @@ Entity Without(Entity side)
return answer;
}
+ ///
+ /// Whether b^2 - 4ac is zero, which makes a x^2 + b x + c a perfect square.
+ /// Symbolically as well as numerically: for a^2 + 2 a b x + b^2 x^2 it is
+ /// 4 a^2 b^2 - 4 a^2 b^2, which does not
+ /// collect, so a guard that asks only what it evaluates to reads the square as an
+ /// ordinary quadratic.
+ ///
+ private static bool IsAPerfectSquareDiscriminant(Entity a, Entity b, Entity c)
+ {
+ var discriminant = (b * b - Number.Integer.Create(4) * a * c).InnerSimplified;
+ return discriminant.Evaled is Number.Complex { IsZero: true }
+ || discriminant.Vars.Any() && Functions.PartialFractions.Bare(discriminant.Simplify()).Evaled is Number.Complex { IsZero: true };
+ }
+
///
/// A square root of a perfect square in is the modulus:
/// sqrt(x^2) is |x|, which for a real x is sgn(x) x, and
@@ -10225,14 +10239,23 @@ Entity Without(Entity side)
{
// Asked of every integrand at every depth, so the radicand is read as a polynomial
// only where it is small enough to be a written square: x^2, or a x^2 + b x + c.
- static bool MayBeARootOfASquare(Entity node, Entity.Variable x)
+ // At the top, any half-odd power of a perfect square is the power of the modulus;
+ // below it the square root only, since a sign written for a substitution's variable
+ // is a factor the rest of the search carries -- `(1 + 2u + u^2)^(5/2)` under
+ // `u = e^(2x)` cost twenty seconds that way. A bare `x^2` is the exception at any
+ // depth, nothing else reading `(x^2)^(3/2)`.
+ var atTheTop = Integration.AnsweringTheQuestionAsked;
+ bool MayBeARootOfASquare(Entity node, Entity.Variable x)
=> node is Powf(var radicand, Number.Rational r) && r.ERational.Denominator.Equals(EInteger.FromInt32(2))
- && (r.ERational.Equals(ERational.Create(1, 2)) || radicand is Powf(var @base, var degree) && @base == x && degree == Number.Integer.Create(2))
- && radicand.ContainsNode(x) && radicand.Complexity <= 12
+ && (atTheTop || r.ERational.Equals(ERational.Create(1, 2))
+ || radicand is Powf(var @base, var degree) && @base == x && degree == Number.Integer.Create(2))
+ && radicand.ContainsNode(x) && radicand.Complexity <= (atTheTop ? 40 : 12)
&& radicand.Nodes.Count(inner => inner == x) <= 2;
if (!expr.Nodes.Any(node => MayBeARootOfASquare(node, x)))
return null;
var changed = false;
+ Entity assumed = Entity.Boolean.True;
+ Entity signs = Number.Integer.One;
var written = expr.Replace(node =>
{
if (!MayBeARootOfASquare(node, x))
@@ -10247,8 +10270,23 @@ static bool MayBeARootOfASquare(Entity node, Entity.Variable x)
var a = monomials.TryGetValue(EInteger.FromInt32(2), out var a2) ? a2 : Number.Integer.Zero;
var b = monomials.TryGetValue(EInteger.One, out var b1) ? b1 : Number.Integer.Zero;
var c = monomials.TryGetValue(EInteger.Zero, out var c0) ? c0 : Number.Integer.Zero;
- if (a.Evaled is not Number.Real { IsPositive: true } leading || b.Evaled is not Number.Real || c.Evaled is not Number.Real
- || (b * b - Number.Integer.Create(4) * a * c).InnerSimplified.Evaled is not Number.Complex { IsZero: true })
+ // A positive leading coefficient, since `sqrt(a (x + h)^2)` is `sqrt(a) |x + h|`:
+ // a positive number, or one positive for a real parameter -- `b^2` is, and
+ // `a^2 + 2 a b x + b^2 x^2` is the square Rubi writes -- whose condition travels
+ // with the answer. The other two coefficients are real or symbolic, not a
+ // number off the line, for the same reason.
+ Entity leading;
+ if (a.Evaled is Number.Real { IsPositive: true })
+ leading = a;
+ else if (IsPositiveForARealParameter(a, x, out var leadingAssumed, assumed))
+ {
+ leading = a;
+ assumed = leadingAssumed;
+ }
+ else
+ return node;
+ if (b.Evaled is Number.Complex and not Number.Real || c.Evaled is Number.Complex and not Number.Real
+ || !IsAPerfectSquareDiscriminant(a, b, c))
return node;
// a (x + h)^2 with h = b/(2a): (a (x + h)^2)^r is a^r |x + h|^(2r), and with 2r odd
// (a rational is held in lowest terms) that is a^r sgn(x + h) (x + h)^(2r).
@@ -10256,11 +10294,19 @@ static bool MayBeARootOfASquare(Entity node, Entity.Variable x)
var linear = h == Number.Integer.Zero ? x : (x + h).InnerSimplified;
var twoR = Number.Integer.Create(r.ERational.Numerator);
Entity power = twoR == Number.Integer.One ? linear : MathS.Pow(linear, twoR);
- Entity signed = MathS.Signum(linear) * power;
+ // The sign in front of the integral rather than inside it: it is constant
+ // between the linear factor's zeros, and a rule below that differentiates the
+ // integrand cannot evaluate `derivative(sgn(...))` -- which is the exception
+ // `sqrt(a^2 + 2abx + b^2x^2) sqrt(c + ex + dx^2)` threw with it left in place.
+ signs = signs * MathS.Signum(linear);
changed = true;
- return leading == Number.Integer.One ? signed : MathS.Pow(leading, r) * signed;
+ return leading == Number.Integer.One ? power : MathS.Pow(leading, r) * power;
});
- return changed ? Integration.ComputeAsTheSameQuestion(written, x, integrateByParts) : null;
+ if (!changed || Integration.ComputeAsTheSameQuestion(written, x, integrateByParts) is not { } answer)
+ return null;
+ if (signs != Number.Integer.One)
+ answer = signs * answer;
+ return assumed == Entity.Boolean.True ? answer : answer.Provided(assumed);
}
///
diff --git a/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs
index 9a2f8cdb1..b664da26a 100644
--- a/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs
+++ b/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs
@@ -54,6 +54,44 @@ private static void DifferentiatesBack(string integrand)
[InlineData("sqrt(sinh(x)^2)")]
public void ARootOfAPerfectSquareIsTheModulus(string integrand) => DifferentiatesBack(integrand);
+ ///
+ /// A square written with symbols: a^2 + 2 a b x + b^2 x^2 is (a + b x)^2,
+ /// whose discriminant 4a^2b^2 - 4a^2b^2 does
+ /// not collect, and whose leading coefficient b^2 is not a number -- it is
+ /// positive for a real parameter, which is what the answer's provided b^2 > 0
+ /// says. At the top the power may be any half-odd one, since the sign written there is
+ /// not a factor some substitution below has to carry. Rubi's 1.2.1.9.
+ /// #718
+ ///
+ [Theory]
+ [InlineData("(a^2 + 2*a*b*x + b^2*x^2)^(5/2)")]
+ [InlineData("(A + B*x)*(d + pe*x)/(a^2 + 2*a*b*x + b^2*x^2)^(5/2)")]
+ [InlineData("1/(a^2 + 2*a*b*x + b^2*x^2)^(3/2)")]
+ public void ASquareWrittenWithSymbols(string integrand)
+ {
+ var integral = integrand.ToEntity().Integrate("x").Substitute("C", 0);
+ Assert.DoesNotContain("integral(", integral.Stringize());
+ Assert.DoesNotContain("NaN", integral.Stringize());
+ Entity Pin(Entity e) => e.Substitute("a", 0.9).Substitute("b", 1.7).Substitute("A", 0.7)
+ .Substitute("B", 1.3).Substitute("d", 0.4).Substitute("pe", 1.1);
+ var derivative = Pin(integral).Differentiate("x");
+ var original = Pin(integrand.ToEntity());
+ var compared = 0;
+ foreach (var at in Points)
+ {
+ var got = derivative.Substitute("x", at).EvalNumerical();
+ var want = original.Substitute("x", at).EvalNumerical();
+ if (got.IsNaN || want.IsNaN)
+ continue;
+ compared++;
+ var difference = Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart);
+ var scale = Math.Max(1.0, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart));
+ Assert.True(difference / scale < 1e-9,
+ $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}");
+ }
+ Assert.True(compared >= 5, $"only {compared} points were comparable for {integrand}");
+ }
+
[Fact]
public void TheSignIsTheSignOfTheLinearFactor()
{