From 130e46b68a1a3806f3e84def648c9c4524e528c4 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 23 Sep 2026 06:35:55 +0000 Subject: [PATCH] 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, though the radicand is `(a + b x)^2` and the power is `|a + b x|^5`. Three conditions said no, and each is a different kind of mistake: The discriminant `4 a^2 b^2 - 4 a^2 b^2` is a zero `InnerSimplified` does not collect, so a guard asking what it evaluates to read the square as an ordinary quadratic. The leading coefficient `b^2` is not a number, though it is positive for a real parameter -- `IsPositiveForARealParameter` is the library's own answer to that, and the condition `b^2 > 0` travels with the answer as it does elsewhere. And the rule's bound on how large a radicand it reads, which is there so that it costs nothing at every level of the descent, is lifted at the top only: there any half-odd power of a square is the power of the modulus, while below it the square root alone is safe, a sign written for a substitution's variable being a factor the rest of the search has to carry. The sign goes in front of the integral rather than into the integrand. Left inside, a rule below differentiates it: `sqrt(a^2 + 2abx + b^2x^2) sqrt(c + ex + dx^2)` threw `CannotEvalException: derivative(sgn(...))`, which the corpus found and no probe had. Family 1 of the Rubi suite: 156 -> 158 of 228, 0 wrong, 0 error; family 6 377/417 and the 1774-problem suite 1707 unchanged. Suite 12665 passed; allocation gate passed on all 19 gated benchmarks. Part of #718. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 24 +++++++ .../Integration/IndefiniteIntegralSolver.cs | 62 ++++++++++++++++--- .../RootOfAPerfectSquareIntegralTest.cs | 38 ++++++++++++ 3 files changed, 116 insertions(+), 8 deletions(-) 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() {