From 0d8619190e7f1c81568bf2ea3a2ceefbf09e52cd Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 8 Oct 2026 21:17:10 +0000 Subject: [PATCH] A quadratic with rational roots counts as two linears before the substitution search The substitution search declines a rational function over symbolic linear factors and a quadratic, which the partial fractions split at once; the half-angle tangent's 1 - u^2 is two linear factors written together and is counted so. Part of #718. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 13 +++++ .../Integration/IndefiniteIntegralSolver.cs | 23 +++++++++ ...nctionOverOneMinusTheSquareIntegralTest.cs | 50 +++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/AHalfAngleRationalFunctionOverOneMinusTheSquareIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index db2c34985..409cf8bd6 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -208,6 +208,19 @@ after it |---|---|---| | `"(a*c + b*c*x)^(-3-2*p)*(f + g*x)*(a^2 + 2*a*b*x + b^2*x^2)^p".ToEntity().Integrate("x")` | `integral(...)`; an answer with no value on the unreleased master | the antiderivative | +### A quadratic with rational roots counts as two linears before the substitution search + +**Answers where there were none.** `sec(x)^2 sin(x)/(a + b sin(x))^3` ran past thirty seconds: under the +half-angle tangent it is a rational function over `(1 - u^2)^2 (a u^2 + 2 b u + a)^3`, and the substitution +search spent eighty seconds on it before the partial fractions, which split it at once, were asked. The search +already declines a rational function over symbolic linear factors and a quadratic; `1 - u^2` is two linear +factors written together, and is counted so ([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"sec(x)^2*sin(x)/(a + b*sin(x))^3".ToEntity().Integrate("x")` | `integral(...)`; past thirty seconds on the unreleased master | 5,017 characters | +| `"tan(x)^2/(a + b*sin(x))^3".ToEntity().Integrate("x")` | `integral(...)`; past thirty seconds on the unreleased master | 4,816 characters | + ### A sine of a double argument beside the argument is written as a product **Answers where there were none.** `csc(a + b x)^3 sin(2a + 2b x)^7` was declined: the rule that writes multiples diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index aca16ef3c..fcbd8c337 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -21277,6 +21277,12 @@ private static bool IsAProductOfSymbolicLinearFactors(Entity denominator, Entity return false; if (read.Keys.Max()!.Equals(EInteger.One)) linears++; + // A quadratic with rational coefficients and rational roots is two linear factors + // written together: the half-angle tangent's `1 - u^2` beside `a u^2 + 2 b u + a` is + // `(1 - u)(1 + u)` to the split, and the search spent eighty seconds declining Rubi's + // `sec(x)^2 sin(x)/(a + b sin(x))^3` before it was asked. + else if (read.Keys.Max()!.Equals(EInteger.FromInt32(2)) && HasRationalRoots(read)) + linears += 2; else if (read.Keys.Max()!.Equals(EInteger.FromInt32(2))) quadratics++; else @@ -21286,6 +21292,23 @@ private static bool IsAProductOfSymbolicLinearFactors(Entity denominator, Entity return (linears >= 1 || !aLinearAmongThem) && linears + quadratics >= 2 && symbolic; } + /// + /// Whether a quadratic read as its coefficients has rational coefficients and a + /// discriminant that is the square of a rational. + /// + private static bool HasRationalRoots(Dictionary quadratic) + { + Number.Rational? Coefficient(int power) + => quadratic.TryGetValue(EInteger.FromInt32(power), out var c) ? c.Evaled as Number.Rational : Number.Integer.Zero; + if (Coefficient(2) is not { } a || Coefficient(1) is not { } b || Coefficient(0) is not { } c) + return false; + var discriminant = (b * b - 4 * a * c).Evaled; + if (discriminant is not Number.Rational d || d.ERational.IsNegative) + return false; + var root = MathS.Sqrt(d).Evaled; + return root is Number.Rational; + } + /// /// Whether a written factor of with a symbol in it stands /// to a whole power of two or more: where the Hermite reduction's one solve takes those diff --git a/Sources/Tests/UnitTests/Calculus/AHalfAngleRationalFunctionOverOneMinusTheSquareIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/AHalfAngleRationalFunctionOverOneMinusTheSquareIntegralTest.cs new file mode 100644 index 000000000..4e3f9a19d --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/AHalfAngleRationalFunctionOverOneMinusTheSquareIntegralTest.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 +{ + /// + /// A rational function of the sine over a power of a + b sin(x) with the cosine below the bar: under + /// the half-angle tangent, (1 - u^2) beside a u^2 + 2 b u + a, split by its partial fractions + /// rather than searched for a substitution. Rubi's 4.1.1.2, 4.1.1.3 and 4.1.2.2. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class AHalfAngleRationalFunctionOverOneMinusTheSquareIntegralTest + { + [Theory] + [InlineData("sec(x)^2*sin(x)/(a + b*sin(x))^3")] + [InlineData("tan(x)^2/(a + b*sin(x))^3")] + public void ByItsPartialFractions(string integrand) + { + var integral = integrand.ToEntity().Integrate("x"); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.True(text.Length < 20000, $"{text.Length} characters of answer for {integrand}"); + Entity Pinned(Entity e) => e.Substitute("a", 2.3).Substitute("b", 0.7); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -1.2, -0.7, 0.3, 0.8, 1.3, 2.9 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.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}"); + } + } +}