From b4b5252dc3df5ae604420c7b01b114a0fb8b9dc1 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 22 Oct 2026 03:14:32 +0000 Subject: [PATCH] Write out the factors the half tangent shares between the parts it homogenised apart Rubi's 6.7.1 sech(x)^2/(a + c sech(x) + b tanh(x)) ran past the corpus's budget; master answered it in 1,383,216 characters after twenty-two seconds. SolveARationalFunctionOfTheHyperbolicTangent homogenises each maximal rational function of e^x apart, which keeps the written factors, and leaves a factor two parts share inside the expanded polynomial of one: under u = tanh(x/2) the integrand was (1 + u^2)^2 over (1 + u^2)^2 times a quartic holding 1 + u^2 once more, and the rational integrator wrote that quartic's partial fractions with symbols. The factors the homogenising makes, 1 + u^2, 1 - u^2, 1 - u, 1 + u and u, are now divided out of every written factor of the denominator as often as they go exactly and written as powers of their own, then out of the numerator: the integrand is 1/((1 + u^2) Q(u)) for a quadratic Q, 2,931 characters in 0.6 s. Only where one of them then divides the numerator too: written out with nothing cancelled, 6.3.1's (c + d x)^3/(a + a tanh(e + f x))^3 ran past the budget it was answered in. And 1 - u^2 before its two linears, so that an integrand even in u stays written so: split into them, 6.1.7's sinh(u)^8/(a - b sinh(u)^4)^2 took 5.4 GB where it takes 0.5. A polynomial gcd over the whole quotient was tried first; it cancels the same factor, but expands the denominator, and the rational integrator declined the quartic it left. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 15 ++++ .../Algebra/Polynomials/PartialFractions.cs | 81 +++++++++++++++++++ .../Integration/IndefiniteIntegralSolver.cs | 9 +++ ...lfTangentsFactorsWrittenOutIntegralTest.cs | 50 ++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/TheHalfTangentsFactorsWrittenOutIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 531381a24..88eeae61c 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -678,6 +678,21 @@ was declined. Each of its terms alone is not elementary, and the sum is the deri |---|---|---| | `"e^(a+b*x)*sinh(c+d*x)^(3/2)-3/4*e^(a+b*x)*d^2/((b^2-9/4*d^2)*sqrt(sinh(c+d*x)))".ToEntity().Integrate("x")` | `integral(...)` | 249 characters | +### Under the half tangent, a factor the parts share is written out + +**Answers where there were none, shorter than master's.** Rubi's 6.7.1 `sech(x)^2/(a + c sech(x) + b tanh(x))` ran past +the corpus's budget. Master answered it in 1,383,216 characters after twenty-two seconds. Under `u = tanh(x/2)` the parts +of the integrand are homogenised apart, and a factor they share stayed hidden inside the expanded quartic of one of them: +`(1 + u^2)^2` over `(1 + u^2)^2` times a quartic that holds `1 + u^2` once more. The factors `1 + u^2`, `1 - u^2`, `1 - u`, `1 + u` +and `u`, which the homogenising makes, are now divided out of the written factors of the denominator and out of the +numerator, and the integrand is `1/((1 + u^2) Q(u))` for a quadratic Q +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sech(x)^2/(a+c*sech(x)+b*tanh(x))".ToEntity().Integrate("x")` | `integral(...)` | 2,931 characters, where before this change master gave 1,383,216 | +| `"csch(x)^2/(a+c*csch(x)+b*coth(x))".ToEntity().Integrate("x")` | `integral(...)` | 1,766 characters, where before this change master gave 1,690 | + ### A function of the hyperbolic tangent alone is integrated under `u = tanh` **Answers where there were none.** `tanh(x)/sqrt(a + b tanh(x)^2 + c tanh(x)^4)` was declined, and so were 35 more of diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs index a920387b4..6243fcad4 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs @@ -882,6 +882,87 @@ private static Entity WithThePrimitiveDenominator(Entity quotient) return primitive.IsConstant ? scaledTop : scaledTop / primitive.ToEntity(variables); } + /// + /// over , a product of powers as + /// written, with each of divided out of every written factor of + /// the denominator as often as it goes exactly and written as a power of its own, and then + /// out of the numerator as often as it goes there too. Null where no candidate divides a + /// factor it is not already, where none then divides the numerator, or where a side does not + /// read as a polynomial over the rationals. + /// + /// + /// A substitution homogenises the parts of an integrand apart, and a factor they share is + /// then hidden inside the expanded quartic of one of them: under u = tanh(x/2), + /// sech(x)^2/(a + c sech(x) + b tanh(x)) is (1 + u^2)^2 over (1 + u^2)^2 + /// times a quartic that holds 1 + u^2 once more. Written out, it is 1/((1 + u^2) Q(u)) + /// with Q a quadratic, which the rational rules read. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? WithTheCandidateFactorsWrittenOut(Entity numerator, Entity denominator, IReadOnlyList candidates) + { + var variables = numerator.Vars.Concat(denominator.Vars).Concat(candidates.SelectMany(c => c.Vars)).Distinct() + .OrderBy(v => v.Name, System.StringComparer.Ordinal).ToArray(); + if (variables.Length == 0 || variables.Length > MultivariatePolynomial.MaxVariables) + return null; + var indices = new Dictionary(variables.Length); + for (var i = 0; i < variables.Length; i++) + indices[variables[i]] = i; + var parsed = new List(); + foreach (var candidate in candidates) + if (MultivariatePolynomial.TryParse(candidate, indices) is { IsConstant: false } read) + parsed.Add(read); + else + return null; + var times = new int[parsed.Count]; + var changed = false; + Entity rest = Integer.One; + foreach (var written in Mulf.LinearChildren(denominator)) + { + var (@base, power) = written is Powf(var repeated, Integer { EInteger.Sign: > 0 } raised) && raised.EInteger.CanFitInInt32() + ? (repeated, raised.EInteger.ToInt32Unchecked()) : (written, 1); + if (!@base.Vars.Any() || MultivariatePolynomial.TryParse(@base, indices) is not { IsConstant: false } factor) + { + rest *= written; + continue; + } + var divided = false; + for (var k = 0; k < parsed.Count; k++) + while (factor.DivideExact(parsed[k]) is { } quotient) + { + // A factor that is the candidate itself, to a power, is already written out. + divided |= !quotient.IsConstant; + factor = quotient; + times[k] += power; + } + changed |= divided; + var remaining = factor.ToEntity(variables); + if (remaining != Integer.One) + rest *= power == 1 ? remaining : MathS.Pow(remaining, power); + } + if (!changed) + return null; + if (MultivariatePolynomial.TryParse(numerator, indices) is not { } top) + return null; + // Only where the numerator shares one of them: written out with nothing cancelled, the + // tangent's `(c + d x)^3/(a + a tanh(e + f x))^3` ran past the budget it was answered in. + var cancelled = false; + for (var k = 0; k < parsed.Count; k++) + while (times[k] > 0 && top.DivideExact(parsed[k]) is { } quotient) + { + top = quotient; + times[k]--; + cancelled = true; + } + if (!cancelled) + return null; + Entity below = rest; + for (var k = 0; k < parsed.Count; k++) + if (times[k] > 0) + below *= times[k] == 1 ? candidates[k] : MathS.Pow(candidates[k], times[k]); + var above = top.ToEntity(variables); + return below == Integer.One ? above : above / below; + } + /// /// over , a product of /// powers as written, with every written factor that divides the numerator exactly diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 91b4b1ad6..817c6afcb 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -34467,6 +34467,15 @@ private static bool IsTheZeroPolynomial(Entity expr) && Mulf.LinearChildren(below).All(factor => !factor.ContainsNode(t) || factor == t || factor is Powf(var b, Number.Integer) && b == t))) continue; var rationalInT = WithThePowersOfOnePlusMinusTCollected(inT / (1 - MathS.Sqr(t))); + // With the factors the parts were homogenised over written out of the rest: `sech(x)^2` + // and `a + c sech(x) + b tanh(x)` are each over `1 + t^2`, and the one in the quartic + // below was hidden, where the rational integrator wrote 1.38 million characters. + // `1 - t^2` before its two linears, so that an integrand even in t is written so: split + // into them, `sinh(u)^8/(a - b sinh(u)^4)^2` took ten times the memory. + if (Functions.SingleQuotient.Of(rationalInT) is var (aboveT, belowT) && belowT.ContainsNode(t) + && Functions.PartialFractions.WithTheCandidateFactorsWrittenOut(aboveT, belowT, + new Entity[] { 1 + MathS.Sqr(t), 1 - MathS.Sqr(t), 1 - t, 1 + t, t }) is { } writtenOut) + rationalInT = writtenOut; if (rationalInT.ContainsNode(square) || rationalInT.ContainsNode(vt) || rationalInT.Nodes.Any(node => node == MathS.NaN) || Integration.ComputeAsAQuestionOfItsOwn(rationalInT, t, integrateByParts) is not { } evenResult) return null; diff --git a/Sources/Tests/UnitTests/Calculus/TheHalfTangentsFactorsWrittenOutIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/TheHalfTangentsFactorsWrittenOutIntegralTest.cs new file mode 100644 index 000000000..b0463c78d --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/TheHalfTangentsFactorsWrittenOutIntegralTest.cs @@ -0,0 +1,50 @@ +// +// 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 +{ + /// + /// Under u = tanh(x/2) the parts of an integrand are homogenised apart, and a factor they + /// share was left hidden inside the expanded quartic of one of them: Rubi's 6.7.1 + /// sech(x)^2/(a + c sech(x) + b tanh(x)) is (1 + u^2)^2 over (1 + u^2)^2 times a + /// quartic holding 1 + u^2 once more, and the rational integrator wrote 1.38 million + /// characters in twenty-two seconds, past the corpus's budget. Written out, it is + /// 1/((1 + u^2) Q(u)) for a quadratic Q. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class TheHalfTangentsFactorsWrittenOutIntegralTest + { + [Theory] + // Rubi 6.7.1:910. + [InlineData("sech(x)^2/(a + c*sech(x) + b*tanh(x))")] + [InlineData("csch(x)^2/(a + c*csch(x) + b*coth(x))")] + public void IsAnsweredShortly(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 5000, $"{integrand} came back in {text.Length} characters"); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + foreach (var at in new[] { -2.2, -0.9, 0.3, 1.4, 2.1 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + Assert.False(got.IsNaN, $"d/dx of the antiderivative of {integrand} has no value at x = {at}, where the integrand is {want}"); + 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}"); + } + } + } +}