From c95bf74d3afc64cc68235f4cc427b55a5dea90d2 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 13 Oct 2026 08:11:43 +0000 Subject: [PATCH 1/2] Roots of multiples of the radicand are one root beside the Euler sum, and a radicand over a constant is read Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 81 ++++++++++++++++++- ...earAndARootAsEulersVariableIntegralTest.cs | 17 +++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 9dbcf742e..97454fe35 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -18851,6 +18851,76 @@ private static bool VanishesIdentically(Entity expr) return Functions.PartialFractions.Bare(inner.Simplify()).Evaled is Number.Complex { IsZero: true }; } + /// + /// as a x^2 + b x + c, read through a constant below the + /// bar as well: Rubi writes (a f^2 + e x (2 d + e x))/f^2 for + /// a + 2 d e x/f^2 + e^2 x^2/f^2, and the first was not read as a quadratic. + /// + private static bool TryReadAQuadraticOverAConstant(Entity radicand, Entity.Variable x, + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out Entity? a, + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out Entity? b, + [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out Entity? c) + { + if (TreeAnalyzer.TryGetPolyQuadratic(radicand, x, out a, out b, out c)) + return true; + if (radicand is Divf(var over, var constant) && !constant.ContainsNode(x) + && TreeAnalyzer.TryGetPolyQuadratic(over, x, out var overA, out var overB, out var overC)) + { + (a, b, c) = ((overA / constant).InnerSimplified, (overB / constant).InnerSimplified, (overC / constant).InnerSimplified); + return true; + } + return false; + } + + /// + /// with every square root of a constant multiple of one quadratic + /// Q written as a named constant times sqrt(Q), and the names' values; null + /// where there is one radicand, or radicands that are not multiples of one. + /// + /// + /// sqrt(g Q) (d + e x + f sqrt(Q))^n: the ratio sqrt(g Q)/sqrt(Q) is constant + /// wherever both roots are defined, whatever the sign of g, so it comes out of the + /// integral named and goes back in after. The radicand kept is the one a power of a sum + /// holds, the substitution's own. Rubi's 1.3.2. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + private static (Entity Rewritten, List<(Entity.Variable Name, Entity Value)> Ratios)? WithTheRootsOfMultiplesAsOne(Entity expr, Entity.Variable x) + { + static bool IsARoot(Entity node, Entity.Variable x) + => node is Powf(var @base, Number.Rational r) && r is not Number.Integer + && r.ERational.Denominator.Equals(EInteger.FromInt32(2)) && @base.ContainsNode(x); + var radicands = expr.Nodes.Where(node => IsARoot(node, x)).Select(node => ((Powf)node).Base).Distinct().ToList(); + if (radicands.Count < 2) + return null; + var kept = expr.Nodes + .OfType() + .Where(power => !power.Exponent.ContainsNode(x) && !IsARoot(power, x) && power.Base.Nodes.Any(inner => IsARoot(inner, x))) + .SelectMany(power => power.Base.Nodes.Where(inner => IsARoot(inner, x)).Select(inner => ((Powf)inner).Base)) + .FirstOrDefault() ?? radicands[0]; + if (!TryReadAQuadraticOverAConstant(kept, x, out var a, out var b, out var c) || VanishesIdentically(a)) + return null; + var ratios = new List<(Entity.Variable, Entity)>(); + var byRadicand = new Dictionary(); + foreach (var other in radicands) + { + if (other == kept) + continue; + if (!TryReadAQuadraticOverAConstant(other, x, out var oa, out var ob, out var oc)) + return null; + var factor = (oa / a).InnerSimplified; + if (factor.ContainsNode(x) || !VanishesIdentically(ob - factor * b) || !VanishesIdentically(oc - factor * c)) + return null; + var name = Variable.CreateUnique(expr + ratios.Aggregate((Entity)Number.Integer.Zero, (sum, pair) => sum + pair.Item1), "k_ratio"); + ratios.Add((name, MathS.Sqrt(other) / MathS.Sqrt(kept))); + byRadicand[other] = name; + } + var rewritten = expr.Replace(node => + node is Powf(var @base, Number.Rational r) && byRadicand.TryGetValue(@base, out var ratio) && IsARoot(node, x) + ? MathS.Pow(ratio, Number.Integer.Create(r.ERational.Numerator)) * MathS.Pow(kept, r) + : node); + return (rewritten, ratios); + } + /// /// A rational function of x and one square root of a quadratic in x, /// rationalised by an Euler substitution and handed to the rational integrator. @@ -18902,6 +18972,15 @@ private static bool VanishesIdentically(Entity expr) /// internal static Entity? SolveByEulerSubstitution(Entity expr, Entity.Variable x, bool onlyBySumItWrites = false) { + // Roots of constant multiples of one quadratic are one root: see the method below. + if (WithTheRootsOfMultiplesAsOne(expr, x) is var (asOne, ratios)) + { + if (SolveByEulerSubstitution(asOne, x, onlyBySumItWrites) is not { } inTheRatios) + return null; + foreach (var (name, value) in ratios) + inTheRatios = inTheRatios.Substitute(name, value); + return inTheRatios; + } // One square root of a quadratic in x, and otherwise a rational function of x -- // or, besides those, powers of `x + sqrt(Q)` and `sqrt(Q) - x` to any exponent, @@ -18942,7 +19021,7 @@ private static bool VanishesIdentically(Entity expr) else if (radicand != innerBase) return null; } if (radicand is null - || !TreeAnalyzer.TryGetPolyQuadratic(radicand, x, out var a, out var b, out var c) + || !TryReadAQuadraticOverAConstant(radicand, x, out var a, out var b, out var c) || a.Evaled is Number.Complex { IsZero: true }) return null; // A missing linear term is the trigonometric substitution's and answered more shortly there. diff --git a/Sources/Tests/UnitTests/Calculus/SumOfALinearAndARootAsEulersVariableIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/SumOfALinearAndARootAsEulersVariableIntegralTest.cs index 842afc071..823f4a8f2 100644 --- a/Sources/Tests/UnitTests/Calculus/SumOfALinearAndARootAsEulersVariableIntegralTest.cs +++ b/Sources/Tests/UnitTests/Calculus/SumOfALinearAndARootAsEulersVariableIntegralTest.cs @@ -35,12 +35,25 @@ public sealed class SumOfALinearAndARootAsEulersVariableIntegralTest // And beside powers of the radicand, whole and not, before the split expands them. [InlineData("(a + 2*d*k*x/f^2 + k^2*x^2/f^2)^2*(d + k*x + f*sqrt(a + 2*d*k*x/f^2 + k^2*x^2/f^2))^n")] [InlineData("(a + 2*d*k*x/f^2 + k^2*x^2/f^2)^(3/2)*(d + k*x + f*sqrt(a + 2*d*k*x/f^2 + k^2*x^2/f^2))^n")] - public void IsIntegratedInTheSum(string integrand) + public void IsIntegratedInTheSum(string integrand) => DifferentiatesBack(integrand, 2.5); + + // The radicand written over `f^2` as Rubi writes it, and the root of a constant multiple + // of it beside the sum: `sqrt(g Q)` is `sqrt(Q)` times a ratio constant wherever both are + // defined, named while the question is asked, and so for either sign of `g`. + [Theory] + [InlineData("(d + k*x + f*sqrt((a*f^2 + k*x*(2*d + k*x))/f^2))^n", 2.5)] + [InlineData("(d + k*x + f*sqrt((a*f^2 + k*x*(2*d + k*x))/f^2))^n/((a*f^2 + k*x*(2*d + k*x))/f^2)^(1/2)", 2.5)] + [InlineData("(a*g + 2*d*k*g*x/f^2 + k^2*g*x^2/f^2)^(1/2)*(d + k*x + f*sqrt(a + 2*d*k*x/f^2 + k^2*x^2/f^2))^n", 2.5)] + [InlineData("(a*g + 2*d*k*g*x/f^2 + k^2*g*x^2/f^2)^(1/2)*(d + k*x + f*sqrt(a + 2*d*k*x/f^2 + k^2*x^2/f^2))^n", -2.5)] + [InlineData("(d + k*x + f*sqrt((a*f^2 + k*x*(2*d + k*x))/f^2))^n/((a*f^2*g + k*g*x*(2*d + k*x))/f^2)^(1/2)", -2.5)] + public void WrittenOverItsDenominatorOrBesideTheRootOfAMultiple(string integrand, double g) => DifferentiatesBack(integrand, g); + + private static void DifferentiatesBack(string integrand, double g) { var integral = integrand.ToEntity().Integrate("x"); Assert.DoesNotContain("integral(", integral.Stringize()); Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.6).Substitute("d", 0.7) - .Substitute("f", 1.1).Substitute("k", 0.9).Substitute("n", 0.37); + .Substitute("f", 1.1).Substitute("k", 0.9).Substitute("n", 0.37).Substitute("g", g); var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); var original = Pinned(integrand.ToEntity()); foreach (var at in new[] { -2.5, -1.2, 0.3, 0.8, 1.4 }) From 4031b92c5c905878bdc626c0451d17b62c9aa9e4 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 13 Oct 2026 09:14:56 +0000 Subject: [PATCH 2/2] A root of a multiple of the radicand beside Euler's sum in BREAKING-CHANGES Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 27016b206..80e69aa15 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -1058,6 +1058,21 @@ secant too now, and the integrand is `sgn(cos(x)) (1 + u^2)^(3/2)/(a + b u)^2` i | `"sec(x)^7/(a + b*tan(x))^3".ToEntity().Integrate("x")` | `integral(...)` | 1,253 characters | | `"sec(x)/(a + b*cot(x))".ToEntity().Integrate("x")` | `integral(...)` | 571 characters | +### A root of a multiple of the radicand beside Euler's sum is the same root + +**Answers where there were none.** `sqrt(g Q) (d + e x + f sqrt(Q))^n` was declined, with the root of `g Q` below the +bar, and so was the sum with its quadratic written over a constant, `sqrt((a f^2 + e x (2 d + e x))/f^2)`, as Rubi's +1.3.2 writes it. The substitution read one radicand, and a quadratic only as a polynomial. `sqrt(g Q)/sqrt(Q)` is +constant wherever both roots are defined, for either sign of `g`, so it is named while the question is asked and put +back after; and a quadratic over a constant is read through the constant +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"(d + e*x + f*sqrt((a*f^2 + e*x*(2*d + e*x))/f^2))^n".ToEntity().Integrate("x")` | `integral(...)` | 227 characters | +| `"(a*g + 2*d*e*g*x/f^2 + e^2*g*x^2/f^2)^(1/2)*(d + e*x + f*sqrt(a + 2*d*e*x/f^2 + e^2*x^2/f^2))^n".ToEntity().Integrate("x")` | `integral(...)` | 539 characters | +| `"(d + e*x + f*sqrt(a + 2*d*e*x/f^2 + e^2*x^2/f^2))^n/(a*g + 2*d*e*g*x/f^2 + e^2*g*x^2/f^2)^(1/2)".ToEntity().Integrate("x")` | `integral(...)` | 253 characters | + ### A whole power of the radicand under Euler's substitution is read as one **Answers where there were none, and none where they were wrong.** Euler's substitution writes the