From d51fcb19390b97c71356980038e342734712f882 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 26 Sep 2026 20:50:35 +0000 Subject: [PATCH] A whole power of a perfect square in a power of the variable is written as a power of its root 1/(a^2 + 2 a b x^2 + b^2 x^4)^2 was left unevaluated. The quartic is b^2 (x^2 + a/b)^2, so a whole power of it is (b^2)^n (x^2 + a/b)^(2n), exactly and with no sign, and the rational integrator reads a power of x^2 + a/b where it declined the quartic. At the question asked, for a square written out as a sum with a symbol in it and x below the bar: not in a polynomial, and not where the square is already written as a power, which the hyperbolic tangent's integrand for Rubi 6.5.7's coth(x)^2/(a + b sech(x)^2)^2 holds. Rubi's 1.2.2.x and 1.2.3.2 perfect squares: 388 to 409 of 574, no row lost. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 18 +++++++ .../Integration/IndefiniteIntegralSolver.cs | 47 +++++++++++++------ .../RootOfAPerfectSquareIntegralTest.cs | 34 ++++++++++++++ 3 files changed, 85 insertions(+), 14 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index d3d2ae92b..1d4a3c545 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -1483,6 +1483,24 @@ Rubi's 1.2.1.4, all 632 problems that count: 250 to 409, no row lost, 61 timeout new timeouts have a radicand of four symbols beside a power of `x`, and went from a quick decline to one past the budget. +### A whole power of a perfect square in a power of the variable is written as a power of its root + +`1/(a^2 + 2 a b x^2 + b^2 x^4)^2` was left unevaluated. The quartic is `b^2 (x^2 + a/b)^2`, so a +whole power of it is `(b^2)^n (x^2 + a/b)^(2n)`, exactly and with no sign, and the rational +integrator reads a power of `x^2 + a/b` where it declined the quartic. It is written so at the +question asked, for a square written out as a sum with a symbol in it and `x` below the bar. It +is not written so in a polynomial, or where the square is already written as a power. Answers +that came out in the expanded square before now come out in `x^2 + a/b`. + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/(a^2+2*a*b*x^2+b^2*x^4)^2".Integrate("x")` | left unevaluated | the antiderivative, in `x^2 + a/b` | +| `"x/(a^2+2*a*b*x^2+b^2*x^4)^3".Integrate("x")` | a piecewise on `b^2 = 0`, in `2 b^2 x^2 + 2 a b` | `-(x^2 + a/b)^(-5)/(10 (b^2)^3)` | +| `"x^2*(a^2+2*a*b*x^3+b^2*x^6)^(-2)".Integrate("x")` | a piecewise on `b^2 = 0`, in `2 b^2 x^3 + 2 a b` | `-(b^2)^(-2) b^3/(9 (a + b x^3)^3)` | + +Rubi's 1.2.2.2, 1.2.2.4, 1.2.2.7 and 1.2.3.2, the 574 problems that count with a perfect square +written with symbols: 388 to 409, no row lost. + ### `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 9d4df0c68..b19c03a23 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -10602,12 +10602,28 @@ private static bool IsAPerfectSquareDiscriminant(Entity a, Entity b, Entity c) // `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; + // A whole power too, at the top, of a square with a symbol in it, and in an integrand + // with x below the bar: `(a^2 + 2 a b x^2 + b^2 x^4)^(-2)` is `(b^2)^(-2) (x^2 + a/b)^(-4)`, + // exact and with no sign, and the rational integrator reads a power of `x^2 + a/b` + // where it declined the quartic. Not in a polynomial, which is answered as one already; + // and read off the whole integrand, since `1/P^2` holds `P^2` with a positive exponent. + // And only of a square written out as a sum: `((a + b - b u^2)^2)^(-1)`, what the + // hyperbolic tangent makes of `coth(x)^2/(a + b sech(x)^2)^2`, is a square written as + // one, and taken apart into `(u^2 - a/b - 1)^(-2)` it went from under a second to past + // a minute. + bool? xBelowTheBar = null; + bool XIsBelowTheBar() + => xBelowTheBar ??= Functions.SingleQuotient.Of(Functions.SingleQuotient.Combine(expr)).Denominator.ContainsNode(x); bool MayBeARootOfASquare(Entity node, Entity.Variable x) - => node is Powf(var radicand, Number.Rational r) && r.ERational.Denominator.Equals(EInteger.FromInt32(2)) - && (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; + => node is Powf(var radicand, Number.Rational r) + && (r.ERational.Denominator.Equals(EInteger.FromInt32(2)) + && (atTheTop || r.ERational.Equals(ERational.Create(1, 2)) + || radicand is Powf(var @base, var degree) && @base == x && degree == Number.Integer.Create(2)) + && radicand.Complexity <= (atTheTop ? 40 : 12) + || atTheTop && r is Number.Integer && r != Number.Integer.Zero && r != Number.Integer.One + && radicand is Sumf or Minusf + && radicand.Complexity <= 40 && radicand.Vars.Any(symbol => symbol != x) && XIsBelowTheBar()) + && radicand.ContainsNode(x) && radicand.Nodes.Count(inner => inner == x) <= 2; if (!expr.Nodes.Any(node => MayBeARootOfASquare(node, x))) return null; var changed = false; @@ -10631,6 +10647,11 @@ bool MayBeARootOfASquare(Entity node, Entity.Variable x) if (monomials.Keys.Any(k => !k.IsZero && !k.Equals(half) && !k.Equals(degree)) || !half.Equals(EInteger.One) && !monomials.ContainsKey(EInteger.Zero)) return node; + // A whole power of a square in x itself is a rational function the splits answer + // already, in the form they give; in a power of x, `x^2 + a/b`, they did not. + var whole = r is Number.Integer; + if (whole && half.Equals(EInteger.One)) + return node; var a = monomials[degree]; var b = monomials.TryGetValue(half, out var b1) ? b1 : Number.Integer.Zero; var c = monomials.TryGetValue(EInteger.Zero, out var c0) ? c0 : Number.Integer.Zero; @@ -10640,16 +10661,14 @@ bool MayBeARootOfASquare(Entity node, Entity.Variable x) // `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)) + // A whole power needs no sign of anything: (a (w + h)^2)^n is a^n (w + h)^(2n). + if (!whole && a.Evaled is not Number.Real { IsPositive: true }) { - leading = a; + if (!IsPositiveForARealParameter(a, x, out var leadingAssumed, assumed)) + return node; assumed = leadingAssumed; } - else - return node; + var leading = a; 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; @@ -10660,14 +10679,14 @@ bool MayBeARootOfASquare(Entity node, Entity.Variable x) if (h.Vars.Any()) h = Functions.PartialFractions.Bare(h.Simplify()); var linear = h == Number.Integer.Zero ? w : (w + h).InnerSimplified; - var twoR = Number.Integer.Create(r.ERational.Numerator); + var twoR = Number.Integer.Create(whole ? r.ERational.Numerator * EInteger.FromInt32(2) : r.ERational.Numerator); Entity power = twoR == Number.Integer.One ? linear : MathS.Pow(linear, twoR); // 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. // None where it is plainly one: an even power of x plus a positive number. - if (!(half.IsEven && h.Evaled is Number.Real { IsPositive: true })) + if (!whole && !(half.IsEven && h.Evaled is Number.Real { IsPositive: true })) signs = signs * MathS.Signum(linear); changed = true; return leading == Number.Integer.One ? power : MathS.Pow(leading, r) * power; diff --git a/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs index 834bd354e..d02bca095 100644 --- a/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/RootOfAPerfectSquareIntegralTest.cs @@ -128,6 +128,40 @@ Entity Pin(Entity e) => e.Substitute("a", -0.9).Substitute("b", 1.7).Substitute( Assert.True(compared >= 5, $"only {compared} points were comparable for {integrand}"); } + /// + /// A whole power of the same square: (a^2 + 2 a b x^2 + b^2 x^4)^(-2) is + /// (b^2)^(-2) (x^2 + a/b)^(-4), exactly and with no sign, a power of a quadratic in + /// x^2 the rational integrator reads. Pinned with a/b positive, so that the + /// square has no real zero among the points. + /// + [Theory] + [InlineData("1/(a^2 + 2*a*b*x^2 + b^2*x^4)^2")] + [InlineData("x/(a^2 + 2*a*b*x^2 + b^2*x^4)^3")] + [InlineData("x^2*(a^2 + 2*a*b*x^3 + b^2*x^6)^(-2)")] + public void AWholePowerOfASquareInAPowerOfTheVariable(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); + 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() {