From 4fb420f7fda254a4c9d2b7cf82d0eadbbb12ff07 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sun, 11 Oct 2026 04:02:04 +0000 Subject: [PATCH] A power of x beside two blocks in one power of x is split over the blocks first Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 17 ++++++ .../Integration/IndefiniteIntegralSolver.cs | 59 +++++++++++++++++-- ...rOfXBesideBlocksInAPowerOfXIntegralTest.cs | 53 +++++++++++++++++ 3 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/APowerOfXBesideBlocksInAPowerOfXIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 05ed4545d..70bf20862 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -277,6 +277,23 @@ takes 6 s for 265,499 now ([#718](https://github.com/asc-community/AngouriMath/i | `"(d + f*x + g*x^2 + h*x^3 + k*x^4 + m*x^5 + n*x^8 + p*x^11)/(a + b*x^2 + c*x^4)^3".ToEntity().Integrate("x")` | `integral(...)` | 77,389 characters | | `"csc(c + d*x)^2/(a - b*sin(c + d*x)^4)^3".ToEntity().Integrate("x")` | `integral(...)` | 157,787 characters | +### A power of x beside two blocks in one power of x is split over the blocks first + +**Shorter answers, and sooner.** `1/(x^7 (a + b x^3)(c + d x^3))` came to 265,499 characters on the unreleased +master: the Hermite reduction read `x^7` as a repeated factor, and what it left went to the split at the power of x, +whose rest over both blocks had coefficients pages long. The blocks are split in `u = x^3` first now, with the power +of x kept outside, and each piece is a power of x over one block: 351 characters. The other `1/(x^m (a + b x^n)(c + d x^n))` +in Rubi's 1.1.3.4 come out shorter as well, and those that took two seconds take milliseconds +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/(x*(a + b*x^3)*(c + d*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 185 characters | +| `"1/(x^4*(a + b*x^3)*(c + d*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 259 characters | +| `"1/(x^7*(a + b*x^3)*(c + d*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 351 characters | +| `"1/(x^8*(a + b*x^3)*(c + d*x^3))".ToEntity().Integrate("x")` | `integral(...)` | 1,217 characters | +| `"1/(x^6*(a + b*x^4)*(c + d*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 795 characters | + ### One linear twice is not two linear powers **Answers that had no value.** `(a c + b c x)^(-3 - 2 p) (f + g x) (a^2 + 2 a b x + b^2 x^2)^p` was answered diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 881634610..9c9e4843b 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -1057,6 +1057,14 @@ is var (multiple, leftover) ?? Integration.ComputeIndefiniteIntegral(numerator / overTheConjugates, x, integrateByParts)) is { } overConjugates) return overConjugates; + // A power of x beside two blocks or more in one power of x, `1/(x^7 (a + b x^3)(c + d x^3))`: + // split in `u = x^3` over the blocks, each fraction then a power of x over one block. + // Before the Hermite reduction, which reads the power of x as a repeated factor: what it + // leaves went to the split at the power of x, whose rest over both blocks had coefficients + // pages long, and the answer was 265,499 characters. + if (IntegrateOverBlocksInAPowerOfX(numerator, denominator, x, integrateByParts, besideAPowerOfX: true) is { } besideBlocksInAPower) + return besideBlocksInAPower; + // A denominator with a written repeated factor takes the Hermite reduction first: // the rational part of the answer in one linear solve, and what is left is a proper // fraction over a squarefree denominator for the splits below. `(1 + x^2)/(x (1 + x^3)^2)` @@ -1622,7 +1630,8 @@ static Entity Binomial(int n, int k) /// n >= 2, linear or quadratic in it, with a symbol among them: split in /// u = x^n by , /// one residue of the numerator's powers modulo n at a time, each fraction then - /// over one block in x. + /// over one block in x. With , a power of x beside two + /// blocks or more as well, and only then. /// /// /// 1/((a + b x^3)(c + d x^3)) was declined: over the rationals it does not factor, @@ -1630,11 +1639,18 @@ static Entity Binomial(int n, int k) /// u = x^3 it is 1/((a + b u)(c + d u)), two linear factors. A numerator /// term x^r M(x^n) keeps its x^r outside the split, which is exact since /// the blocks are polynomials in x^n alone. + /// + /// A power of x beside the blocks stays outside the split: 1/(x^7 (a + b x^3)(c + d x^3)) + /// is (b/(x^7 (a + b x^3)) - d/(x^7 (c + d x^3)))/(b c - a d), a power of x over one + /// block twice. Split at the power of x first, the rest over both blocks had coefficients + /// that were pages of symbols, and the answer came to 265,499 characters. + /// /// https://github.com/asc-community/AngouriMath/issues/718 /// - private static Entity? IntegrateOverBlocksInAPowerOfX(Entity numerator, Entity denominator, Entity.Variable x, bool integrateByParts, bool oneBlockAtItsRoots = false) + private static Entity? IntegrateOverBlocksInAPowerOfX(Entity numerator, Entity denominator, Entity.Variable x, bool integrateByParts, bool oneBlockAtItsRoots = false, bool besideAPowerOfX = false) { var blocks = new List<(Dictionary Polynomial, int Power)>(); + var powerOfX = 0; Entity constant = Number.Integer.One; foreach (var factor in Mulf.LinearChildren(denominator)) { @@ -1646,6 +1662,13 @@ static Entity Binomial(int n, int k) var (@base, power) = factor is Powf(var b, Number.Integer e) && e.EInteger.Sign > 0 && e.EInteger.CanFitInInt32() ? (b, e.EInteger.ToInt32Unchecked()) : (factor, 1); + if (@base == x) + { + if (!besideAPowerOfX) + return null; + powerOfX += power; + continue; + } if (!TreeAnalyzer.TryGetPolynomial(@base, x, out var polynomial) || polynomial.Count < 2) return null; foreach (var term in polynomial) @@ -1660,6 +1683,8 @@ static Entity Binomial(int n, int k) } if (blocks.Count == 0 || !(numerator + denominator).Vars.Any(symbol => symbol != x)) return null; + if (besideAPowerOfX && (powerOfX == 0 || blocks.Count < 2 || powerOfX > MaximumPowerOfXOverABlock)) + return null; // n, the greatest common divisor of every power the blocks hold. var n = EInteger.Zero; foreach (var (polynomial, _) in blocks) @@ -1678,6 +1703,8 @@ static Entity Binomial(int n, int k) foreach (var term in above) if (term.Key.Sign < 0 || !term.Key.CanFitInInt32() || term.Value.ContainsNode(x)) return null; + if (besideAPowerOfX && above.Values.Any(coefficient => coefficient is not (Number or Variable))) + return null; var u = Entity.Variable.CreateUnique(numerator + denominator, "u"); Entity InU(Dictionary polynomial) @@ -1735,11 +1762,33 @@ Entity InU(Dictionary polynomial) } if (!Functions.PartialFractions.TrySplitOverWrittenFactors(inU, below, u, out var decomposition)) return null; - var outside = residue.Key == 0 ? (Entity)Number.Integer.One : residue.Key == 1 ? x : MathS.Pow(x, residue.Key); - foreach (var fraction in Sumf.LinearChildren(decomposition)) + // The power of x below stays outside the split, so each fraction is a power of x + // over one block, which the split at a power of x beside a block reads. + var outside = (residue.Key - powerOfX) switch { + 0 => (Entity)Number.Integer.One, + 1 => x, + var power => MathS.Pow(x, power), + }; + // The split comes as a sum over a constant. Beside a power of x it is taken term by + // term: whole, it is the power of x over both blocks again, for the split at that + // power, whose coefficients run to `a^54 b^6 c^46 d^14`. + var (terms, scale) = besideAPowerOfX && decomposition is Divf(var sum, var divisor) && !divisor.ContainsNode(u) + ? (sum, divisor) + : (decomposition, (Entity)Number.Integer.One); + foreach (var term in Sumf.LinearChildren(terms)) + { + if (term == Number.Integer.Zero) + continue; + var fraction = scale == Number.Integer.One ? term : term / scale; var inX = outside * fraction.Substitute(u, MathS.Pow(x, step)); - if (Integration.ComputeIndefiniteIntegral(inX, x, integrateByParts) is not { } integrated) + // Beside a power of x, each piece is a part of the question asked, over one block + // fewer, and answered at its level: the split at the power of x that reads it + // answers only the question asked or one below. + var integrated = besideAPowerOfX + ? Integration.ComputeAsTheSameQuestion(inX, x, integrateByParts) + : Integration.ComputeIndefiniteIntegral(inX, x, integrateByParts); + if (integrated is null) return null; total += integrated; } diff --git a/Sources/Tests/UnitTests/Calculus/APowerOfXBesideBlocksInAPowerOfXIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/APowerOfXBesideBlocksInAPowerOfXIntegralTest.cs new file mode 100644 index 000000000..58f4228a9 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/APowerOfXBesideBlocksInAPowerOfXIntegralTest.cs @@ -0,0 +1,53 @@ +// +// 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 power of x beside two blocks in one power of x is split over the blocks in + /// u = x^n with the power of x kept outside: 1/(x^7 (a + b x^3)(c + d x^3)) is a + /// power of x over one block twice. Rubi's 1.1.3.4. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class APowerOfXBesideBlocksInAPowerOfXIntegralTest + { + [Theory] + [InlineData("1/(x*(a + b*x^3)*(c + d*x^3))")] + [InlineData("1/(x^4*(a + b*x^3)*(c + d*x^3))")] + [InlineData("1/(x^7*(a + b*x^3)*(c + d*x^3))")] + [InlineData("1/(x^6*(a + b*x^4)*(c + d*x^4))")] + public void DifferentiatesBack(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pinned(Entity e) => e.Substitute("a", 2).Substitute("b", 3).Substitute("c", 5).Substitute("d", 7); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { 0.35, 0.6, 0.85, 1.4 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart)), + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + + /// + /// Split at the power of x first, the rest over both blocks had coefficients pages long, + /// and the answer was 265,499 characters; over the blocks first it is 351. + /// + [Fact] + public void TheAnswerIsTheSizeOfItsPieces() + => Assert.True("1/(x^7*(a + b*x^3)*(c + d*x^3))".ToEntity().Integrate("x").Stringize().Length < 2000); + } +}