From 69d556a0562368d9824abdcfa9a8cba1cb0e553d Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 3 Oct 2026 03:09:18 +0000 Subject: [PATCH 1/2] The integrand the root's substitution leaves has its common factor cancelled as written, not simplified A rational function of x^n beside (c + d x^n)^(k - 1/n) is a rational function of u = x/(c + d x^n)^(1/n), and the integrand in u was simplified before it was integrated. What that found is the factor the substitution leaves on both sides of the bar, and with symbols in it the search grew with the power of the binomial: 1/((a + b x^2)^(5/2)(1 + x^2)) took 5 s, the 7/2 power past 30 s. Each factor written alike above and below is cancelled power for power now, and the rest is left as it is written, which the rules after it read better than an expansion: (a + b x^4)^(7/4)/(c + d x^4) is answered in a second. Part of https://github.com/asc-community/AngouriMath/issues/718 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 17 +++++ .../Integration/IndefiniteIntegralSolver.cs | 55 ++++++++++++++- .../Calculus/DividedByTheRootIntegralTest.cs | 69 +++++++++++++++++++ 3 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/DividedByTheRootIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index d036a8063..e741df210 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -540,6 +540,23 @@ taken apart into pieces each closed the same way | `"(2 + x)/((2 + 4*x - 3*x^2)*(1 + 3*x + 2*x^2)^(3/2))".ToEntity().Integrate("x")` | `integral(...)` | an antiderivative | | `"1/((x^2 + 1)*sqrt(x^2 + x + 1))".ToEntity().Integrate("x")` | `integral(...)` | a logarithm and an arctangent | +### A rational function of `x^n` beside a power of a symbolic binomial is integrated without a search + +**Answers where there were none.** A rational function of `x^n` beside `(c + d x^n)^(k - 1/n)` is a +rational function of `u = x/(c + d x^n)^(1/n)`, and the integrand in `u` was simplified before it +was integrated. With symbols in it the search for a simpler form grew with the power of the +binomial: `1/((a + b x^2)^(5/2) (1 + x^2))` took 5 s on the unreleased master, the `7/2` power ran +past 30 s, and both were declined on 2.5.0. What the search found was the factor the substitution +leaves on both sides of the bar, which is cancelled as it is written now, the other factors left as +they are written ([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/((a + b*x^2)^(5/2)*(1 + x^2))".ToEntity().Integrate("x")` | `integral(...)` | powers of `x/sqrt(a + b x^2)`, and an arctangent or a logarithm by the sign of `a - b`, in 0.6 s | +| `"1/((a + b*x^2)^(7/2)*(1 + x^2))".ToEntity().Integrate("x")` | `integral(...)` | the same, in 0.7 s | +| `"1/((a + b*x^2)^(9/2)*(1 + x^2))".ToEntity().Integrate("x")` | `integral(...)` | the same, in 1 s | +| `"x^2/((a + b*x^2)^(7/2)*(c + d*x^2))".ToEntity().Integrate("x")` | `integral(...)` | the same by the sign of `c (a d - b c)`, in 1 s | + ### A power of a multiple of a quadratic's derivative beside a power of the quadratic is a binomial **Improvement, not silent.** `(b d + 2 c d x)^m (a + b x + c x^2)^p`, with a power that is not whole, diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 261c1f821..6af87d155 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -3741,10 +3741,16 @@ node is Powf(var xAgain, Number.Integer power) && xAgain == x && power.EInteger. var uToTheN = MathS.Pow(u, n); var oneMinusDUToTheN = 1 - d * uToTheN; var binomialInU = c / oneMinusDUToTheN; - var integrand = Functions.SingleQuotient.Combine( + // The factor the substitution leaves on both sides cancelled as it is written, not simplified: + // `(1 - u^4)/((1 + u^4)(1 - u^4))` for Timofeev's above, which the rational integrator does + // not cancel, and the search for a simpler form that did grows with the power of the binomial + // while the symbols are in it: `1/((a + b x^2)^(7/2) (1 + x^2))` ran past 90 s on it. The + // other factors stay as they are written, which the rules after this read better than + // their expansion. + var integrand = WithTheFactorsWrittenOnBothSidesCancelled(Functions.SingleQuotient.Combine( inXToTheN.Substitute(xToTheN, c * uToTheN / oneMinusDUToTheN) * (k.IsZero ? Number.Integer.One : MathS.Pow(binomialInU, Number.Integer.Create(k))) - / oneMinusDUToTheN).Simplify(); + / oneMinusDUToTheN).InnerSimplified); if (integrand is Providedf(var inner, _)) integrand = inner; if (integrand.ContainsNode(x) || integrand.Nodes.Any(node => node == MathS.NaN)) @@ -3755,6 +3761,51 @@ node is Powf(var xAgain, Number.Integer power) && xAgain == x && power.EInteger. return answer.Nodes.Any(node => node == MathS.NaN) ? null : answer; } + /// + /// with each factor written alike above and below the bar cancelled, + /// power for power, and the rest as written; unchanged where none is. + /// + private static Entity WithTheFactorsWrittenOnBothSidesCancelled(Entity quotient) + { + var (top, bottom) = Functions.SingleQuotient.Of(quotient); + static List<(Entity Base, EInteger Power)> Factors(Entity side) + { + var factors = new List<(Entity Base, EInteger Power)>(); + foreach (var factor in Mulf.LinearChildren(side)) + { + var (@base, power) = factor is Powf(var raised, Number.Integer exponent) && exponent.EInteger.Sign > 0 + ? (raised, exponent.EInteger) : (factor, EInteger.One); + var at = factors.FindIndex(written => written.Base == @base); + if (at < 0) + factors.Add((@base, power)); + else + factors[at] = (@base, factors[at].Power.Add(power)); + } + return factors; + } + var above = Factors(top); + var below = Factors(bottom); + var cancelled = false; + for (var i = 0; i < above.Count; i++) + { + if (above[i].Base is Number) + continue; + var j = below.FindIndex(written => written.Base == above[i].Base); + if (j < 0) + continue; + var common = EInteger.Min(above[i].Power, below[j].Power); + above[i] = (above[i].Base, above[i].Power.Subtract(common)); + below[j] = (below[j].Base, below[j].Power.Subtract(common)); + cancelled = true; + } + if (!cancelled) + return quotient; + static Entity Product(List<(Entity Base, EInteger Power)> factors) + => factors.Where(written => !written.Power.IsZero).Aggregate((Entity)Number.Integer.One, + (product, written) => product * (written.Power.Equals(EInteger.One) ? written.Base : MathS.Pow(written.Base, Number.Integer.Create(written.Power)))); + return (Product(above) / Product(below)).InnerSimplified; + } + /// /// Reads as c + d x^n with c and d free of /// . diff --git a/Sources/Tests/UnitTests/Calculus/DividedByTheRootIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/DividedByTheRootIntegralTest.cs new file mode 100644 index 000000000..6c95653a8 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/DividedByTheRootIntegralTest.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using System; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// A rational function of x^n times (c + d x^n)^(k - 1/n), which + /// u = x/(c + d x^n)^(1/n) makes a rational function of u. + /// #718 + /// + /// + /// Checked by differentiating back at points where the integrand is real, with the symbols + /// pinned after the integral is taken. + /// + [Trait("Area", "Calculus")] + public sealed class DividedByTheRootIntegralTest + { + private static void DifferentiatesBack(string integrand, double[] points, params (string, double)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + var derivative = integral.Substitute("C", 0).Differentiate("x"); + Entity original = integrand.ToEntity(); + foreach (var (name, value) in pins) + { + derivative = derivative.Substitute(name, value); + original = original.Substitute(name, value); + } + 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 >= 3, $"only {compared} points could be compared for {integrand}"); + } + + /// Timofeev's, which is 1/(1 + u^4) under the substitution. + [Fact] + public void TimofeevsQuarticBesideAFourthRoot() + => DifferentiatesBack("1/((1 + x^4)*(2 + x^4)^(1/4))", new[] { -1.3, -0.4, 0.3, 0.9, 1.7 }); + + /// + /// A higher power of the root with symbols in it, beside another quadratic: the rational + /// function of u grows with the power, and the larger ones ran past 90 s. + /// + [Theory] + [InlineData("1/((a + b*x^2)^(5/2)*(1 + x^2))")] + [InlineData("1/((a + b*x^2)^(7/2)*(1 + x^2))")] + [InlineData("1/((a + b*x^2)^(9/2)*(1 + x^2))")] + public void AHalfOddPowerOfASymbolicQuadratic(string integrand) + => DifferentiatesBack(integrand, new[] { -1.3, -0.4, 0.3, 0.9, 1.7 }, ("a", 1.3), ("b", 0.7)); + } +} From 10118dc5ec452b55db95f3fd580fa55baae6a928 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 3 Oct 2026 10:50:15 +0000 Subject: [PATCH 2/2] Each base of the root-divided integrand is its polynomial in u The substitution writes a base twice over where the radicand is a binomial in a half-angle tangent: under a secant's, 1 - (-u^2) - u^2, which is 1 and which nothing after it collects, so the integrand went on with a degree-eight numerator that is not there. sqrt(a + a sec(e + f x))/(c - c sec(e + f x))^4 ran past a minute; it is 1.2 s with each base written as its polynomial in u before the factors written alike on both sides are cancelled. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 6af87d155..8ec1d94d0 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -3744,10 +3744,11 @@ node is Powf(var xAgain, Number.Integer power) && xAgain == x && power.EInteger. // The factor the substitution leaves on both sides cancelled as it is written, not simplified: // `(1 - u^4)/((1 + u^4)(1 - u^4))` for Timofeev's above, which the rational integrator does // not cancel, and the search for a simpler form that did grows with the power of the binomial - // while the symbols are in it: `1/((a + b x^2)^(7/2) (1 + x^2))` ran past 90 s on it. The - // other factors stay as they are written, which the rules after this read better than - // their expansion. - var integrand = WithTheFactorsWrittenOnBothSidesCancelled(Functions.SingleQuotient.Combine( + // while the symbols are in it: `1/((a + b x^2)^(7/2) (1 + x^2))` ran past 90 s on it. Each + // base is its polynomial in u first, which collects what the substitution writes twice -- + // `1 - (-u^2) - u^2` is 1 under a secant's half angle -- and the factors stay as they are + // written, which the rules after this read better than their product expanded. + var integrand = WithTheFactorsWrittenOnBothSidesCancelled(u, Functions.SingleQuotient.Combine( inXToTheN.Substitute(xToTheN, c * uToTheN / oneMinusDUToTheN) * (k.IsZero ? Number.Integer.One : MathS.Pow(binomialInU, Number.Integer.Create(k))) / oneMinusDUToTheN).InnerSimplified); @@ -3762,19 +3763,38 @@ node is Powf(var xAgain, Number.Integer power) && xAgain == x && power.EInteger. } /// - /// with each factor written alike above and below the bar cancelled, - /// power for power, and the rest as written; unchanged where none is. + /// with each base that is a sum written as its polynomial in + /// , and each factor then written alike above and below the bar + /// cancelled, power for power; the factors are kept as factors. /// - private static Entity WithTheFactorsWrittenOnBothSidesCancelled(Entity quotient) + private static Entity WithTheFactorsWrittenOnBothSidesCancelled(Entity.Variable u, Entity quotient) { var (top, bottom) = Functions.SingleQuotient.Of(quotient); - static List<(Entity Base, EInteger Power)> Factors(Entity side) + Entity AsItsPolynomial(Entity @base) + { + if (@base is not (Sumf or Minusf) || !@base.ContainsNode(u) + || !TreeAnalyzer.TryGetPolynomial(@base, u, out var terms) + || terms.Any(term => term.Key.Sign < 0 || term.Value.ContainsNode(u))) + return @base; + Entity written = Number.Integer.Zero; + foreach (var term in terms.OrderBy(term => term.Key)) + { + var coefficient = term.Value.InnerSimplified; + if (Functions.PartialFractions.IsZeroAsAValue(coefficient)) + continue; + var monomial = term.Key.IsZero ? coefficient : coefficient * MathS.Pow(u, Number.Integer.Create(term.Key)); + written = written == Number.Integer.Zero ? monomial : written + monomial; + } + return written.InnerSimplified; + } + List<(Entity Base, EInteger Power)> Factors(Entity side) { var factors = new List<(Entity Base, EInteger Power)>(); foreach (var factor in Mulf.LinearChildren(side)) { var (@base, power) = factor is Powf(var raised, Number.Integer exponent) && exponent.EInteger.Sign > 0 ? (raised, exponent.EInteger) : (factor, EInteger.One); + @base = AsItsPolynomial(@base); var at = factors.FindIndex(written => written.Base == @base); if (at < 0) factors.Add((@base, power)); @@ -3785,7 +3805,6 @@ private static Entity WithTheFactorsWrittenOnBothSidesCancelled(Entity quotient) } var above = Factors(top); var below = Factors(bottom); - var cancelled = false; for (var i = 0; i < above.Count; i++) { if (above[i].Base is Number) @@ -3796,10 +3815,7 @@ private static Entity WithTheFactorsWrittenOnBothSidesCancelled(Entity quotient) var common = EInteger.Min(above[i].Power, below[j].Power); above[i] = (above[i].Base, above[i].Power.Subtract(common)); below[j] = (below[j].Base, below[j].Power.Subtract(common)); - cancelled = true; } - if (!cancelled) - return quotient; static Entity Product(List<(Entity Base, EInteger Power)> factors) => factors.Where(written => !written.Power.IsZero).Aggregate((Entity)Number.Integer.One, (product, written) => product * (written.Power.Equals(EInteger.One) ? written.Base : MathS.Pow(written.Base, Number.Integer.Create(written.Power))));