From b46c2bd5c3fe1a0fa86954744bd683d2dcb7fb5c Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 3 Oct 2026 11:55:55 +0000 Subject: [PATCH] A polynomial with symbols in it that is a binomial or an even quartic in a shifted variable is integrated in it 1/(c^2 x^3 + 3 b c x^2 + 3 b^2 x + 3 a b) was declined, although its denominator is ((c x + b)^3 + 3 a b c - b^3)/c and 1/(a + (b + c x)^3) is answered at once: nothing factors a polynomial with a symbol among its coefficients. In partial fractions, before the Hermite reduction, a power of one polynomial of the third to the sixth degree with a symbol in it, written out, is read in y = x + s with s = a_(n-1)/(n a_n), which takes its term of degree n - 1 away; where that leaves a binomial, or a quartic even in y -- a + 8 x - 8 x^2 + 4 x^3 - x^4 is a + 3 - 2 y^2 - y^4 in y = x - 1 -- the integral is taken in y. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 14 ++++ .../Integration/IndefiniteIntegralSolver.cs | 84 +++++++++++++++++++ .../Calculus/ShiftedPolynomialIntegralTest.cs | 61 ++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ShiftedPolynomialIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 891e0ae34..e2fa95a3f 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -232,6 +232,20 @@ Rubi's `x^m (a + b x^n)^p` and `(a + b x^n)^p (c + d x^n)^q` | `"1/(2+3/x^2)^3".Integrate("x")`, `1/(a + b/x^3)` | left unevaluated | an antiderivative | | `"(a+b*x^n)*(c+d*x^n)^3".Integrate("x")` | left unevaluated | written out, eight powers of `x` integrated: `a c^3 x + ... + b d^3 x^(4 n + 1)/(4 n + 1)` | +### A polynomial with symbols in it that is a binomial or an even quartic in a shifted variable is integrated in it + +**Answers where there were none.** `1/(c^2 x^3 + 3 b c x^2 + 3 b^2 x + 3 a b)` was left unevaluated, +although its denominator is `((c x + b)^3 + 3 a b c - b^3)/c`, which the rule for a binomial reads at +once: nothing factors a polynomial with a symbol among its coefficients. A polynomial below the bar +of the third to the sixth degree, with a symbol in it, that written in `y = x + s` with +`s = a_(n-1)/(n a_n)` is a binomial, or a quartic even in `y`, is integrated in `y`. Rubi's 1.3.1 +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/(3*a*b + 3*b^2*x + 3*b*c*x^2 + c^2*x^3)".ToEntity().Integrate("x")`, and its square | `integral(...)` | two logarithms and an arctangent in `x + b/c` | +| `"x/(a + 8*x - 8*x^2 + 4*x^3 - x^4)".ToEntity().Integrate("x")`, and `1` over it | `integral(...)` | the antiderivative in `x - 1` | + ### The hyperbolic functions have antiderivatives, and so does anything rational in `e^(k x)` An integrand rational in `e^(k x)` becomes a rational function of one variable under diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 402d52850..5f0b13db6 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -745,6 +745,17 @@ is var (multiple, leftover) ?? Integration.ComputeIndefiniteIntegral(numerator / respelled, x, integrateByParts)) is { } overPolynomials) return overPolynomials; + // A polynomial below the bar that is a binomial, or a quartic even in its variable, once + // written in `y = x + s` with `s = a_(n-1)/(n a_n)`: `c^2 x^3 + 3 b c x^2 + 3 b^2 x + 3 a b` + // is `((c x + b)^3 + 3 a b c - b^3)/c`, and `a + 8 x - 8 x^2 + 4 x^3 - x^4` is + // `a + 3 - 2 y^2 - y^4` in `y = x - 1`. Nothing above factors a polynomial with a symbol + // in it, and the rules for a binomial and for an even quartic read it in y at once. + // Once: in y the term that s takes away is gone. + if (InTheVariableThatDepressesIt(numerator, denominator, x) is var (inY, y, shift) + && (SolveByPartialFractions(inY, y, integrateByParts) + ?? Integration.ComputeIndefiniteIntegral(inY, y, integrateByParts)) is { } inTheShiftedVariable) + return inTheShiftedVariable.Substitute(y, (x + shift).InnerSimplified); + // 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)` @@ -850,6 +861,79 @@ is var (multiple, leftover) return null; } + /// + /// over written in y = x + s, + /// where the denominator is a constant times a power of one polynomial in x of the third to + /// the sixth degree, written as a sum of its monomials, that in y is a binomial or, of the + /// fourth degree, even; s = a_(n-1)/(n a_n) is what takes its term of degree + /// n - 1 away. Null otherwise, and where that term is not there to take away. + /// + private static (Entity InY, Entity.Variable Y, Entity Shift)? InTheVariableThatDepressesIt(Entity numerator, Entity denominator, Entity.Variable x) + { + Entity? polynomial = null; + var power = 0; + Entity constant = Number.Integer.One; + foreach (var factor in Mulf.LinearChildren(denominator)) + { + if (!factor.ContainsNode(x)) + { + constant *= factor; + continue; + } + if (polynomial is not null) + return null; + (polynomial, power) = factor is Powf(var raised, Number.Integer { EInteger.Sign: > 0 } exponent) && exponent.EInteger.CanFitInInt32() + ? (raised, exponent.EInteger.ToInt32Unchecked()) : (factor, 1); + } + // With a symbol among its coefficients, since a polynomial over the rationals is split + // over its factors above; and written out, since one written in a linear, + // `a + (b + c x)^3`, is read as it stands. + if (polynomial is null || !polynomial.Vars.Any(symbol => symbol != x) + || polynomial.Nodes.Any(node => node is Powf(Sumf or Minusf, _) && node.ContainsNode(x)) + || !TreeAnalyzer.TryGetPolynomial(polynomial, x, out var terms) + || terms.Any(term => term.Key.Sign < 0 || !term.Key.CanFitInInt32() || term.Value.ContainsNode(x))) + return null; + if (numerator.ContainsNode(x) + && (!TreeAnalyzer.TryGetPolynomial(numerator, x, out var above) || above.Any(term => term.Key.Sign < 0 || term.Value.ContainsNode(x)))) + return null; + var degree = terms.Keys.Max()!.ToInt32Unchecked(); + if (degree < 3 || degree > 6) + return null; + Entity Coefficient(int k) => terms.TryGetValue(EInteger.FromInt32(k), out var coefficient) ? coefficient : Number.Integer.Zero; + if (VanishesIdentically(Coefficient(degree)) || VanishesIdentically(Coefficient(degree - 1))) + return null; + var shift = Functions.PartialFractions.InLowestTermsOverTheSymbols(Coefficient(degree - 1) / (degree * Coefficient(degree))); + // P(y - s) has at y^i the sum over m >= i of a_m C(m, i) (-s)^(m - i). + var inY = new Entity[degree + 1]; + for (var i = 0; i <= degree; i++) + { + Entity sum = Number.Integer.Zero; + var choose = EInteger.One; + for (var m = i; m <= degree; m++) + { + if (m > i) + choose = choose.Multiply(EInteger.FromInt32(m)).Divide(EInteger.FromInt32(m - i)); + sum += Coefficient(m) * Number.Integer.Create(choose) * (m == i ? Number.Integer.One : MathS.Pow(-shift, m - i)); + } + inY[i] = Functions.PartialFractions.InLowestTermsOverTheSymbols(sum); + } + var vanishing = inY.Select(VanishesIdentically).ToArray(); + var aBinomial = Enumerable.Range(1, degree - 1).All(i => vanishing[i]); + var anEvenQuartic = degree == 4 && vanishing[1] && vanishing[3]; + if (!aBinomial && !anEvenQuartic) + return null; + var y = Variable.CreateUnique(numerator + denominator, "y_shift"); + Entity depressed = Number.Integer.Zero; + for (var i = degree; i >= 0; i--) + if (!vanishing[i]) + { + var monomial = i == 0 ? inY[i] : inY[i] * (i == 1 ? y : MathS.Pow(y, i)); + depressed = depressed == Number.Integer.Zero ? monomial : depressed + monomial; + } + var below = power == 1 ? depressed : MathS.Pow(depressed, power); + return (numerator.Substitute(x, y - shift) / (constant == Number.Integer.One ? below : constant * below), y, shift); + } + /// /// A polynomial over a power of a linear, P(x)/(a + b x)^k with k >= 2, /// under t = a + b x: P((t - a)/b) t^(-k) / b is a sum of powers of diff --git a/Sources/Tests/UnitTests/Calculus/ShiftedPolynomialIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ShiftedPolynomialIntegralTest.cs new file mode 100644 index 000000000..7c98be466 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ShiftedPolynomialIntegralTest.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 +{ + /// + /// A polynomial below the bar with a symbol among its coefficients that is a binomial, or a + /// quartic even in its variable, once written in y = x + s with + /// s = a_(n-1)/(n a_n): Rubi's 1.3.1. Nothing factors a polynomial with a symbol in it, + /// and in y the rules for a binomial and for an even quartic read it at once. + /// #718 + /// + /// + /// Checked by differentiating back with the symbols pinned after the integral is taken. + /// + [Trait("Area", "Calculus")] + public sealed class ShiftedPolynomialIntegralTest + { + private static readonly double[] Points = { 0.3, 0.6, 0.9 }; + + private static void DifferentiatesBack(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pin(Entity e) => e.Substitute("a", 0.7).Substitute("b", 1.3).Substitute("c", 2.1); + var derivative = Pin(integral.Substitute("C", 0).Differentiate("x")); + var original = Pin(integrand.ToEntity()); + foreach (var point in Points) + { + var expected = original.Substitute("x", point).EvalNumerical().RealPart.EDecimal.ToDouble(); + var actual = derivative.Substitute("x", point).EvalNumerical().RealPart.EDecimal.ToDouble(); + Assert.True(Math.Abs(expected - actual) < 1e-8 * Math.Max(1, Math.Abs(expected)), + $"d/dx of the antiderivative of {integrand} is {actual} at x = {point}, where the integrand is {expected}"); + } + } + + /// + /// c^2 x^3 + 3 b c x^2 + 3 b^2 x + 3 a b is ((c x + b)^3 + 3 a b c - b^3)/c. + /// + [Theory] + [InlineData("1/(3*a*b + 3*b^2*x + 3*b*c*x^2 + c^2*x^3)")] + [InlineData("1/(3*a*b + 3*b^2*x + 3*b*c*x^2 + c^2*x^3)^2")] + public void ACubicThatIsABinomialInALinear(string integrand) => DifferentiatesBack(integrand); + + /// + /// a + 8 x - 8 x^2 + 4 x^3 - x^4 is a + 3 - 2 y^2 - y^4 in y = x - 1. + /// + [Theory] + [InlineData("x/(a + 8*x - 8*x^2 + 4*x^3 - x^4)")] + [InlineData("1/(a + 8*x - 8*x^2 + 4*x^3 - x^4)")] + public void AQuarticThatIsEvenInALinear(string integrand) => DifferentiatesBack(integrand); + } +}