From 467acf5b892bbdf1556c744aa877f374cc0446a0 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 16 Oct 2026 23:21:02 +0000 Subject: [PATCH 1/2] Integrate a rational function over an inverse tangent's quadratic beside it by partial fractions Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 95 +++++++++++++++++++ .../Integration/Integration.Definition.cs | 3 + ...rTheQuadraticOfAnArctangentIntegralTest.cs | 59 ++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ARationalFunctionOverTheQuadraticOfAnArctangentIntegralTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index ded68afa2..22801fa49 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -26154,6 +26154,101 @@ when two.Equals(EInteger.FromInt32(2)) && e1 == MathS.e && e2 == MathS.e return Functions.PartialFractions.DerivativeDiffersWhereReal(answer, expr, x) ? null : answer; } + /// + /// A rational function over the quadratic an inverse tangent's derivative is written over, + /// times a function of that inverse tangent, x^4 atan(a x)/(c + a^2 c x^2), with the + /// rational function split into partial fractions over its written factors and each piece + /// beside the function its own question: atan(a x)/(1 + a^2 x^2) is + /// atan(a x)^2/(2a), and the rest is a power of x beside the inverse tangent. + /// + /// + /// The rules before it timed out on Rubi's 5.3.4 x^4 atan(a x)/(c + a^2 c x^2) and + /// atan(a x)/(x^4 (c + a^2 c x^2)), and declined x^2 atan(a x)/(c + a^2 c x^2). + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveARationalFunctionOverTheQuadraticOfAnArctangentBesideIt(Entity expr, Entity.Variable x) + { + if (!Integration.AnsweringTheQuestionAsked) + return null; + static IEnumerable<(Entity Factor, bool Below)> Factors(Entity e, bool below) => e switch + { + Mulf(var l, var r) => Factors(l, below).Concat(Factors(r, below)), + Divf(var l, var r) => Factors(l, below).Concat(Factors(r, !below)), + _ => new[] { (e, below) } + }; + Entity? function = null; + Entity above = Number.Integer.One, below = Number.Integer.One; + foreach (var (factor, isBelow) in Factors(expr, false)) + { + if (factor.Nodes.Any(node => node is Arctanf && node.ContainsNode(x))) + { + if (function is not null || isBelow) + return null; + function = factor; + } + else if (isBelow) + below *= factor; + else + above *= factor; + } + if (function is null || !below.ContainsNode(x)) + return null; + var arctangents = function.Nodes.Where(node => node is Arctanf && node.ContainsNode(x)).Distinct().ToList(); + if (arctangents.Count != 1 || arctangents[0] is not Arctanf(var argument) + || !TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out var intercept) + || !Functions.PartialFractions.IsZeroAsAValue(intercept)) + return null; + // The rest is a rational function in x, and one written factor below the bar is a + // multiple of 1 + (a x)^2. + var quadratic = 1 + MathS.Sqr(slope) * MathS.Sqr(x); + var (numerator, denominator) = Functions.SingleQuotient.Of(above / below); + if (!IsARationalFunction(numerator / denominator, x) + || !Mulf.LinearChildren(denominator).Any(part => part.ContainsNode(x) + && part.Substitute(x, Number.Integer.Zero).InnerSimplified is var atZero + && !Functions.PartialFractions.IsZeroAsAValue(atZero) + && Functions.PartialFractions.IsZeroAsAValue((part - atZero * quadratic).InnerSimplified))) + return null; + // An improper one divided first: `x^2/(c + a^2 c x^2)` is `1/(a^2 c) - 1/(a^2 (c + a^2 c x^2))`. + var pieces = new List(); + if (!IsProperByDegree(numerator, denominator, x)) + { + if (TreeAnalyzer.PolynomialLongDivision(numerator, denominator, genericCase: true, inTermsOf: x) is not var (quotient, properPart)) + return null; + pieces.Add(quotient); + // The generic case, as the integrator answers it: the remainder carries the + // condition that the leading coefficient is not zero. + properPart = properPart.Replace(node => node is Providedf(var expression, _) ? expression : node).InnerSimplified; + (numerator, denominator) = Functions.SingleQuotient.Of(properPart); + if (Functions.PartialFractions.IsZeroAsAValue(numerator)) + numerator = Number.Integer.Zero; + } + Entity decomposition; + if (numerator == Number.Integer.Zero) + decomposition = Number.Integer.Zero; + else if (!Functions.PartialFractions.TrySplitOverWrittenFactors(numerator, denominator, x, out var split)) + { + if (pieces.Count == 0) + return null; + decomposition = numerator / denominator; + } + else + decomposition = split; + // The split comes back as one sum over a factor free of x, `(0 + 1/(c x^2) + ...)/1`. + var (terms, divisor) = decomposition is Divf(var sum, var free) && !free.ContainsNode(x) ? (sum, free) : (decomposition, (Entity)Number.Integer.One); + pieces.AddRange(Sumf.LinearChildren(terms).Where(term => !Functions.PartialFractions.IsZeroAsAValue(term)).Select(term => term / divisor)); + if (pieces.Count < 2) + return null; + Entity answer = Number.Integer.Zero; + foreach (var piece in pieces) + { + if (Integration.ComputeAsAQuestionOfItsOwn((piece * function).InnerSimplified, x, integrateByParts: true) is not { } integrated) + return null; + answer += integrated; + } + answer = answer.InnerSimplified; + return answer.Nodes.Any(node => node == MathS.NaN) ? null : answer; + } + /// /// A rational function of a sine, or of a cosine, over a quadratic in it with a symbol /// among the coefficients: sin(x)/(a + b sin(x) + c sin(x)^2), and the same with diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 39247aeed..2025a5978 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -670,6 +670,9 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // A power of a sine, cosine or hyperbolic one over a binomial in a power of it, over the // binomial's roots, before the rules that spent the budget on `sinh(y)^4/(a + b sinh(y)^3)`. if ((answer = IndefiniteIntegralSolver.SolveAPowerOverABinomialInAPowerOfASineOverItsRoots(expr, x)) is { }) return answer; + // A rational function over the quadratic of an inverse tangent beside it, split, before the + // rules that timed out on `x^4 atan(a x)/(c + a^2 c x^2)`. + if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionOverTheQuadraticOfAnArctangentBesideIt(expr, x)) is { }) return answer; // A rational function times the logarithm of a nested product of powers of linears, by parts, // its logarithm's derivative read off as written: `ln(e (f (a + b x)^p (c + d x)^q)^r)`. if ((answer = IndefiniteIntegralSolver.SolveARationalFunctionTimesALogarithmOfPowersOfLinears(expr, x, integrateByParts)) is { }) return answer; diff --git a/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverTheQuadraticOfAnArctangentIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverTheQuadraticOfAnArctangentIntegralTest.cs new file mode 100644 index 000000000..ce2b5d2a2 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ARationalFunctionOverTheQuadraticOfAnArctangentIntegralTest.cs @@ -0,0 +1,59 @@ +// +// 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 over the quadratic an inverse tangent's derivative is written over, beside + /// that inverse tangent, split into partial fractions: Rubi's 5.3.4 x^4 atan(a x)/(c + a^2 c x^2) + /// and atan(a x)/(x^4 (c + a^2 c x^2)), which timed out. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ARationalFunctionOverTheQuadraticOfAnArctangentIntegralTest + { + private static void DifferentiatesBack(string integrand, params (string, double)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + + var pinned = integral.Substitute("C", 0); + Entity original = integrand.ToEntity(); + foreach (var (name, value) in pins) + { + pinned = pinned.Substitute(name, value); + original = original.Substitute(name, value); + } + var derivative = pinned.Differentiate("x"); + foreach (var at in new[] { -2.3, -0.7, 0.4, 1.9 }) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + var difference = Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart); + var scale = Math.Max(1.0, Math.Abs((double)want.RealPart) + Math.Abs((double)want.ImaginaryPart)); + Assert.True(difference / scale < 1e-8, + $"d/dx of the antiderivative of {integrand} is {got} at x = {at}, where the integrand is {want}"); + } + } + + // The quadratic's factor is q, not Rubi's c: the helper takes C, and c is too close. + [Theory] + [InlineData("x^4*atan(a*x)/(q+a^2*q*x^2)")] + [InlineData("x^2*atan(a*x)/(q+a^2*q*x^2)")] + [InlineData("atan(a*x)/(x^2*(q+a^2*q*x^2))")] + [InlineData("atan(a*x)/(x^4*(q+a^2*q*x^2))")] + public void IsAnsweredAndDifferentiatesBack(string integrand) + { + DifferentiatesBack(integrand, ("a", 1.3), ("q", 0.7)); + DifferentiatesBack(integrand, ("a", -0.4), ("q", -2)); + } + } +} From 5a1949ca5d35dc1ff47460a881c34eee8fb48440 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Sat, 17 Oct 2026 10:19:23 +0000 Subject: [PATCH 2/2] Record the rational functions over an inverse tangent's quadratic in BREAKING-CHANGES Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 441e6c0df..52046c744 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -310,6 +310,20 @@ 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 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. +The rational factor is now divided down and split over `c + a^2 c x^2` and the powers of x, and each part beside the +inverse tangent is integrated on its own +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"x^4*atan(a*x)/(c+a^2*c*x^2)".ToEntity().Integrate("x")` | `integral(...)` | 233 characters | +| `"x^2*atan(a*x)/(c+a^2*c*x^2)".ToEntity().Integrate("x")` | `integral(...)` | 126 characters | +| `"atan(a*x)/(x^2*(c+a^2*c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 612 characters | +| `"atan(a*x)/(x^4*(c+a^2*c*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 894 characters | + ### A piecewise case whose condition is undefined is passed over **A different answer.** A case whose predicate evaluates to NaN -- an order comparison of a number