From 340236a0e0ed14dbed38e0ad20c87ca995a64136 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 21 Oct 2026 22:53:05 +0000 Subject: [PATCH] Split a rational function over a binomial of degree three or four beside other factors TrySplitOverWrittenFactors read linear and quadratic factors only, and the tangent's 1/((a + b u^3) (1 + u^2)), from Rubi's 4.3.7 1/(a + b tan(c + d x)^3), and the root of the sine's 2 u/((a + b u) (1 + u^4)), from 6.1.7's sech(c + d x)/(a + b sqrt(sinh(c + d x))), were declined for want of a split. The system reads any degree, and the decomposition is checked at sampled points before it is returned; it now admits one binomial of degree three or four written once, and the rules for a polynomial over a binomial answer the pieces. Not beside a power of the variable alone, nor beside a second such binomial: the cube root's substitution makes Rubi's 1.1.3.4 x^m (a + b x^3)^(1/3)/(c + d x^3) rational functions over u^3 (c + (a d - b c) u^3) and over (b u^3 - 1) (c + (a d - b c) u^3) with a^9 c^36 d^9 and a^17 b^16 c^43 in their numerators, and split, nine of them came back two to nine times longer -- (a + b x^3)^(1/3)/(x^5 (c + d x^3)) in 12,377 characters where the rules for the cube root write 1,964 -- and three were wrong in the corpus's judgement. Fenced, those nine are master's answers to the character. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 17 ++++ .../Algebra/Polynomials/PartialFractions.cs | 21 ++++- ...BinomialOfDegreeThreeOrFourIntegralTest.cs | 86 +++++++++++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAWrittenBinomialOfDegreeThreeOrFourIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 7b8cb4a18..23af66a16 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -614,6 +614,23 @@ carried the discriminant's being real in its condition, and held nowhere when it | `"piecewise(2 * x provided not x = 0, 3 * x ^ 2 provided i - 1 < 0)".ToEntity().DomainCondition` | `false` | `not x = 0` | | `"1/(sqrt(a + i*a*x)*(c + d*x)^(3/2)*(a - i*a*x))".ToEntity().Integrate("x")` | `integral(...)`; on the unreleased master, an answer with no value at any point | 3,267 characters, defined | +### A rational function over a binomial of degree three or four beside other factors is split over them + +**Answers where there were none.** Rubi's 4.3.7 `1/(a + b tan(c + d x)^3)` and its square, and 6.1.7 +`sech(c + d x)/(a + b sqrt(sinh(c + d x)))` and its square, were declined. Under the tangent and under the root of the +sine they are rational functions over `(a + b u^3) (1 + u^2)` and `(a + b u) (1 + u^4)`, and the split over the +factors as written read linear and quadratic ones only. It now reads one binomial of degree three or four, written +once, beside factors of degree one or two, and the rules for a polynomial over a binomial answer each piece; not +beside a power of the variable alone, which those rules read term by term, nor beside a second such binomial +([#233](https://github.com/asc-community/AngouriMath/issues/233)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/(a+b*tan(c+d*x)^3)".ToEntity().Integrate("x")` | `integral(...)` | 1,853 characters | +| `"1/(a+b*tan(c+d*x)^3)^2".ToEntity().Integrate("x")` | `integral(...)` | 3,026 characters | +| `"sech(c+d*x)/(a+b*sqrt(sinh(c+d*x)))".ToEntity().Integrate("x")` | `integral(...)` | 3,325 characters | +| `"sech(c+d*x)/(a+b*sqrt(sinh(c+d*x)))^2".ToEntity().Integrate("x")` | `integral(...)` | 4,614 characters | + ### A rational function over the quadratic of an inverse tangent beside it is split **Answers where there were none.** `x^4 atan(a x)/(c + a^2 c x^2)` ran out of time, and so did three more of Rubi's 5.3.4. diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs index 1ff2f3dc2..a920387b4 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs @@ -438,6 +438,8 @@ internal static bool TrySplitOverWrittenFactors( // coefficients free of x; a factor free of x is a constant and comes out in front. var factors = new List<(Entity Factor, int Degree)>(); Entity constant = Integer.One; + var binomialsPastTheSecondDegree = 0; + var aPowerOfXAlone = false; foreach (var part in Mulf.LinearChildren(denominator)) { if (!part.ContainsNode(x)) @@ -464,8 +466,16 @@ internal static bool TrySplitOverWrittenFactors( if (!TreeAnalyzer.TryGetPolynomial(toRead, x, out var read) || read.Count == 0) return false; var degree = read.Keys.Max()!; - if (!degree.CanFitInInt32() || degree.ToInt32Unchecked() is not (1 or 2)) + // And a binomial of degree three or four written once, `a + b x^3`: the system below + // reads any degree, and the rules for a polynomial over a binomial answer each + // piece -- `1/((a + b u^3) (1 + u^2))`, which the tangent makes of Rubi's + // `1/(a + b tan(c + d x)^3)`, was declined for want of a split. + var aBinomialOnce = read.Count == 2 && multiplicity == 1 && degree.CanFitInInt32() && degree.ToInt32Unchecked() is 3 or 4; + if (!degree.CanFitInInt32() || degree.ToInt32Unchecked() is not (1 or 2) && !aBinomialOnce) return false; + if (aBinomialOnce) + binomialsPastTheSecondDegree++; + aPowerOfXAlone |= read.Count == 1; // A repeated quadratic has a rule to land on only with a symbol somewhere, // the split by residues below; over the rationals the system would hand it a // cubic over the square, which no rule reads. @@ -476,6 +486,15 @@ internal static bool TrySplitOverWrittenFactors( return false; factors.Add((part.InnerSimplified, degree.ToInt32Unchecked() * multiplicity)); } + // Not beside a power of x alone, which is the rules' for `x^m` over a binomial, term by + // term: the cube root's substitution makes Rubi's 1.1.3.4 `(a + b x^3)^(1/3)/(x^5 (c + d x^3))` + // a polynomial over `u^3 (c + (a d - b c) u^3)` with `a^9 c^36 d^9` in it, and split over + // the two, it came back in 12,377 characters where those rules write 1,964. Nor beside a + // second such binomial: `x^7 (a + b x^3)^(1/3)/(c + d x^3)` is one over `(b u^3 - 1)` and + // `(c + (a d - b c) u^3)`, whose system with symbols in it came back with `a^17 b^16 c^43` + // and a wrong answer, where the rules for the cube root answer it in 2,861 characters. + if (binomialsPastTheSecondDegree > 1 || binomialsPastTheSecondDegree == 1 && aPowerOfXAlone) + return false; // Two linear factors with one root are one factor: `(a + b x)(a + x b)^2` is // written so by the rules that make a factor monic and gather its powers, and // as two distinct factors the decomposition has no answer -- its coefficients diff --git a/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAWrittenBinomialOfDegreeThreeOrFourIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAWrittenBinomialOfDegreeThreeOrFourIntegralTest.cs new file mode 100644 index 000000000..b11b0cdea --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverAWrittenBinomialOfDegreeThreeOrFourIntegralTest.cs @@ -0,0 +1,86 @@ +// +// 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 with symbols in it over a product written with a binomial of degree three + /// or four in it, split over the factors as written by undetermined coefficients: each piece is a + /// polynomial over a binomial, or over a linear or a quadratic, which the rules for those answer. + /// The split read only linear and quadratic factors, and `1/((a + b u^3) (1 + u^2))`, which the + /// tangent makes of Rubi's 4.3.7 `1/(a + b tan(c + d x)^3)`, was declined. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class ARationalFunctionOverAWrittenBinomialOfDegreeThreeOrFourIntegralTest + { + [Theory] + [InlineData("1/((a+b*x^3)*(1+x^2))")] + [InlineData("1/((a+b*x^4)*(c+x^2))")] + [InlineData("1/(a+b*tan(c+d*x)^3)")] + [InlineData("1/(a+b*tan(c+d*x)^3)^2")] + public void DifferentiatesBack(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4).Substitute("d", 0.9); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -1.3, -0.6, -0.2, 0.3, 0.8, 1.4 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.IsNaN || got.IsNaN) + continue; + compared++; + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)), + $"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}"); + } + + /// + /// Not split beside a power of the variable alone, nor beside a second such binomial: the cube + /// root's substitution makes Rubi's 1.1.3.4 rows rational functions over u^3 (c + k u^3) + /// and over (b u^3 - 1)(c + k u^3), and split, they came back in 12,377 and 27,590 + /// characters with c^36 and c^43 in them, the second wrong, where the rules for + /// the cube root write 1,964 and 2,861. + /// + [Theory] + [InlineData("(a+b*x^3)^(1/3)/(x^5*(c+d*x^3))")] + [InlineData("x^7*(a+b*x^3)^(1/3)/(c+d*x^3)")] + public void IsNotSplitBesideAPowerOfXOrASecondBinomial(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 5000, $"the antiderivative of {integrand} is {text.Length} characters"); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4).Substitute("d", 0.9); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { 0.3, 0.6, 0.9, 1.2, 1.5, 1.8 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.IsNaN || got.IsNaN) + continue; + compared++; + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-9 * Math.Max(1, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)), + $"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}"); + } + } +}