From c852aab36b16728850848c100adc9c29b022234b Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 20 Oct 2026 13:14:36 +0000 Subject: [PATCH] Pair the half-odd powers of two linears whose product is an arsinh radicand `(d + i c d x)^(3/2) (f - i c f x)^(5/2)` beside `(a + b arsinh(c x))^n` is `K^3 (f - i c f x) M^(3/2)`, with M the radicand arsinh holds and K constant: a polynomial beside the root, which takes half a second. Paired the other way, as `K^5 M^(5/2)/(d + i c d x)`, it ran past the budget. Both factors above the bar, the smaller power is the one paired, so that the whole power left is above the bar too. With the first factor above and the second below under a power of -3/2 or less, the whole power is written out as a sum of monomials, each over the root: `sqrt(f - i c f x)/(d + i c d x)^(3/2)` is `K^(-3) (f - i c f x)^2/M^(3/2)`, which the rules for a power of a quadratic did not read as that product. Rubi's 7.1.4, twelve rows. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 18 +++++ .../Integration/IndefiniteIntegralSolver.cs | 71 +++++++++++++++++++ ...arsOverARadicandAsMonomialsIntegralTest.cs | 61 ++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/APairOfLinearsOverARadicandAsMonomialsIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 706174fdb..c2f6fee8c 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -6216,6 +6216,24 @@ with `K = sqrt(L1) sqrt(L2)/sqrt(M)` in front of the answer, constant wherever i Rubi's 7.1.4 and 7.1.5, all 376 problems that count: 308 to 345, no row lost, 56 timeouts to 27. +### Half-odd powers of the two linears whose product is the radicand `arsinh` holds are paired to leave a polynomial + +**Answers where there were none.** `(a + b arsinh(c x)) sqrt(f - i c f x)/(d + i c d x)^(3/2)`, +`(d + i c d x)^(3/2) (f - i c f x)^(5/2) (a + b arsinh(c x))` and ten more of Rubi's 7.1.4 were declined. With both +linears above the bar the smaller power is now the one paired into the radicand `M = 1 + c^2 x^2`, so that the whole +power left is above the bar too: `(d + i c d x)^(3/2) (f - i c f x)^(5/2)` is `K^3 (f - i c f x) M^(3/2)`, a polynomial +beside the root. With the second below the bar under a power of `-3/2` or less, the whole power of the first is +written out as monomials, each over the root, and each `x^k (a + b arsinh(c x))/M^(3/2)` is answered alone. Over +`sqrt(M)`, and with both linears below the bar, the pairing is as before +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"(a+b*asinh(c*x))*sqrt(f-i*c*f*x)/(d+i*c*d*x)^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | 1,001 characters | +| `"(f-i*c*f*x)^(5/2)*(a+b*asinh(c*x))/(d+i*c*d*x)^(5/2)".ToEntity().Integrate("x")` | `integral(...)` | 7,148 characters | +| `"(d+i*c*d*x)^(3/2)*(f-i*c*f*x)^(5/2)*(a+b*asinh(c*x))".ToEntity().Integrate("x")` | `integral(...)` | 3,624 characters | +| `"(d+i*c*d*x)^(3/2)*(f-i*c*f*x)^(5/2)*(a+b*asinh(c*x))^2".ToEntity().Integrate("x")` | `integral(...)` | 7,625 characters | + ### The radicand an inverse sine or cosine holds is read as one `(d + c d x)^(1/2) (f - c f x)^(3/2) (a + b arcsin(c x))` ran out of time. `arcsin(u)` holds a diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index b64fa6967..d8c92eefa 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -31898,6 +31898,26 @@ Piecewise piecewise when piecewise.Cases.Any() => piecewise.Cases.First().Expres /// rows this way; `(d + i c d x)^(5/2) sqrt(f - i c f x) (a + b arsinh(c x))` ran past /// seventy seconds where the same thing over `1 + c^2 x^2` takes two thirds of one. /// + /// + /// + /// Both factors above the bar, `q` is the smaller power, so that the whole power left is + /// above the bar too: `(d + i c d x)^(3/2) (f - i c f x)^(5/2)` is `K^3 (f - i c f x) M^(3/2)`, + /// a polynomial beside the root. Paired the other way it was `K^5 M^(5/2)/(d + i c d x)`, + /// which ran past the budget, as did the same with `(a + b arsinh(c x))^2`; the polynomial + /// takes half a second. + /// + /// + /// The first factor above the bar and the second below it under a power of `-3/2` or + /// less, the whole power `L1^n` is written out as a sum of monomials, each over the root: + /// `sqrt(f - i c f x)/(d + i c d x)^(3/2)` is `K^(-3) (f - i c f x)^2/M^(3/2)`, and as that + /// product the rules for a power of a quadratic do not read it -- ten of Rubi's 7.1.4 + /// rows, from `(f - i c f x)^2` to `(f - i c f x)^5` over `M^(3/2)` and `M^(5/2)`, ran past + /// the budget -- while each `x^k (a + b arsinh(c x))/M^(3/2)` is answered in a tenth of a + /// second. Over `sqrt(M)` the product is answered as it is, and is left so; and both + /// factors below the bar are left as they are paired. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + /// private static bool TryWriteAPairOfLinearsOverARadicand(Entity expr, Entity.Variable x, List references, Entity.Variable multiple, out Entity rewritten, out Entity? root) { @@ -31928,6 +31948,11 @@ private static bool TryWriteAPairOfLinearsOverARadicand(Entity expr, Entity.Vari var twice = (2 * half).Evaled; if (half is not Number.Integer && twice is not Number.Integer) continue; + // Both above the bar, the smaller power is the one paired, so that the whole + // power left over is above it too. See the remarks. + var whole = (Number.Integer)(p - q).Evaled; + if (half is not Number.Integer && !half.IsNegative && whole.IsNegative) + continue; var product = (first * second).Expand(); foreach (var candidate in references) { @@ -31944,6 +31969,14 @@ private static bool TryWriteAPairOfLinearsOverARadicand(Entity expr, Entity.Vari together = MathS.Pow(multiple, twice) * MathS.Pow(candidate, half); root = MathS.Sqrt(first) * MathS.Sqrt(second) / MathS.Sqrt(candidate); } + // The first factor above the bar and the root of the radicand 3/2 or more + // below it: a sum of monomials over that root. See the remarks. + if (half is not Number.Integer && half < Number.Rational.Create(-1, 1) && p.Evaled is Number.Real { IsPositive: true } + && AsMonomialsOverARoot(first, whole, x, factors.Where((_, k) => k != i && k != j), candidate, (Number.Rational)(-half)) is { } monomials) + { + rewritten = MathS.Pow(multiple, twice) * monomials; + return true; + } Entity result = MathS.Pow(first, (p - q).InnerSimplified) * together; for (var k = 0; k < factors.Count; k++) if (k != i && k != j) @@ -31961,6 +31994,44 @@ private static bool TryWriteAPairOfLinearsOverARadicand(Entity expr, Entity.Vari Powf(var @base, var exponent) => (@base, exponent), _ => (factor, Number.Integer.One), }; + + // `L^n g/M^r` as `sum_k a_k x^k g/M^r`, each term spelled as a quotient, which is how + // the rules for a power of a quadratic read it; null where L^n is no polynomial. + static Entity? AsMonomialsOverARoot(Entity linear, Number.Integer power, Entity.Variable x, + IEnumerable<(Entity Base, Entity Exponent)> rest, Entity radicand, Number.Rational rootPower) + { + if (!TreeAnalyzer.TryGetPolynomial(MathS.Pow(linear, power), x, out var terms) + || terms.Keys.Any(key => key.Sign < 0) || terms.Values.Any(coefficient => coefficient.ContainsNode(x))) + return null; + // The other factors, a negative power of one below the bar with the radicand's root. + Entity? beside = null; + Entity under = MathS.Pow(radicand, rootPower); + foreach (var (@base, exponent) in rest) + { + if (exponent.Evaled is Number.Real { IsNegative: true } negative) + { + var positive = (-negative).Evaled; + under = under * (positive == Number.Integer.One ? @base : MathS.Pow(@base, positive)); + continue; + } + var factor = exponent == Number.Integer.One ? @base : MathS.Pow(@base, exponent); + beside = beside is null ? factor : beside * factor; + } + Entity? sum = null; + foreach (var pair in terms.OrderBy(term => term.Key)) + { + var degree = pair.Key; + var written = pair.Value.InnerSimplified; + if (VanishesIdentically(written)) + continue; + Entity term = degree.IsZero ? written : written * (degree.Equals(EInteger.One) ? x : MathS.Pow(x, Number.Integer.Create(degree))); + if (beside is not null) + term = term * beside; + term = term / under; + sum = sum is null ? term : sum + term; + } + return sum; + } } /// Set while asks its product. diff --git a/Sources/Tests/UnitTests/Calculus/APairOfLinearsOverARadicandAsMonomialsIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/APairOfLinearsOverARadicandAsMonomialsIntegralTest.cs new file mode 100644 index 000000000..d0d4a1934 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/APairOfLinearsOverARadicandAsMonomialsIntegralTest.cs @@ -0,0 +1,61 @@ +// +// 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 +{ + /// + /// Two linears whose product is a multiple of the radicand of arsinh(c x), + /// (d + i c d x)(f - i c f x) = d f (1 + c^2 x^2), under powers a whole number apart: + /// with the root of the radicand 3/2 or more below the bar the whole power above it is + /// written out as monomials over the root, and with both above the bar the smaller power is + /// paired. Each ran past the budget. Rubi's 7.1.4. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class APairOfLinearsOverARadicandAsMonomialsIntegralTest + { + [Theory] + [InlineData("(a+b*asinh(c*x))*sqrt(f-i*c*f*x)/(d+i*c*d*x)^(3/2)")] + [InlineData("(f-i*c*f*x)^(3/2)*(a+b*asinh(c*x))/(d+i*c*d*x)^(3/2)")] + [InlineData("(f-i*c*f*x)^(3/2)*(a+b*asinh(c*x))/(d+i*c*d*x)^(5/2)")] + [InlineData("(f-i*c*f*x)^(5/2)*(a+b*asinh(c*x))/(d+i*c*d*x)^(3/2)")] + [InlineData("(f-i*c*f*x)^(5/2)*(a+b*asinh(c*x))/(d+i*c*d*x)^(5/2)")] + [InlineData("(d+i*c*d*x)^(5/2)*(a+b*asinh(c*x))/(f-i*c*f*x)^(3/2)")] + [InlineData("(d+i*c*d*x)^(3/2)*(a+b*asinh(c*x))/(f-i*c*f*x)^(3/2)")] + [InlineData("(a+b*asinh(c*x))*sqrt(d+i*c*d*x)/(f-i*c*f*x)^(3/2)")] + [InlineData("(d+i*c*d*x)^(5/2)*(a+b*asinh(c*x))/(f-i*c*f*x)^(5/2)")] + [InlineData("(d+i*c*d*x)^(3/2)*(a+b*asinh(c*x))/(f-i*c*f*x)^(5/2)")] + [InlineData("(d+i*c*d*x)^(3/2)*(f-i*c*f*x)^(5/2)*(a+b*asinh(c*x))")] + [InlineData("(d+i*c*d*x)^(3/2)*(f-i*c*f*x)^(5/2)*(a+b*asinh(c*x))^2")] + public void AnsweredAndDifferentiatesBack(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Entity Pinned(Entity e) => e.Substitute("a", 0.9).Substitute("b", 1.7).Substitute("c", 0.6).Substitute("d", -1.3).Substitute("f", 0.8); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -1.1, -0.4, 0.3, 0.7, 1.2, 2.1 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (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 could be compared for {integrand}"); + } + } +}