From 430998df8e19e18a13c14855e5f2b86e223d7786 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 13 Oct 2026 21:09:55 +0000 Subject: [PATCH 1/2] Split a root of a product beside a root of one of its factors (P Q)^r is K P^r Q^r, with K constant wherever it is defined, and the powers of P gather. Rubi 1.2.2.3:282-293. Part of #718 Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 70 ++++++++++++++++++ .../Integration/Integration.Definition.cs | 3 + ...ctSplitBesideARootOfAFactorIntegralTest.cs | 72 +++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 Sources/Tests/UnitTests/Calculus/ARootOfAProductSplitBesideARootOfAFactorIntegralTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index c9d3c9d81..dfca3fe23 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -19120,6 +19120,76 @@ Entity TakeOut(Entity node) /// Set while asks its question. [System.ThreadStatic] private static bool takingTheWholePowersOut; + /// + /// A root of a product beside a root of one of its factors, the product split: + /// (P Q)^r is K P^r Q^r, where K = (P Q)^r/(P^r Q^r) is constant wherever + /// it is defined, for the same reason as in + /// . The powers of P + /// then gather: (a + b x^2)^(5/2)/sqrt(a^2 - b^2 x^4) is + /// K (a + b x^2)^2/sqrt(a - b x^2). + /// + /// + /// Rubi's 1.2.2.3:282-293 were declined: nothing reads a radicand as a product of another + /// radicand and a cofactor. Only factors of the integrand are read, and at the question + /// asked only. https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveBySplittingARootOfAProductBesideARootOfAFactor(Entity expr, Entity.Variable x, bool integrateByParts) + { + if (!Integration.AnsweringTheQuestionAsked || splittingARootOfAProduct) + return null; + var roots = new List<(Entity Node, Entity Base, Number.Rational Exponent)>(); + Gather(expr); + if (roots.Count < 2) + return null; + foreach (var product in roots) + foreach (var factor in roots) + { + if (ReferenceEquals(product.Node, factor.Node) || product.Base == factor.Base + || ExactlyDivided(product.Base, factor.Base, x) is not { } cofactor || !cofactor.ContainsNode(x)) + continue; + var multiple = Variable.CreateUnique(expr, "k_split"); + var written = MathS.Pow(factor.Base, product.Exponent) * MathS.Pow(cofactor, product.Exponent); + var constant = product.Node / written; + var rewritten = expr.Replace(node => ReferenceEquals(node, product.Node) ? multiple * written : node).InnerSimplified; + Entity? answer; + splittingARootOfAProduct = true; + try + { + answer = Integration.ComputeAsTheSameQuestion(rewritten, x, integrateByParts); + } + finally + { + splittingARootOfAProduct = false; + } + if (answer is null) + return null; + answer = answer.Substitute(multiple, constant); + return answer.ContainsNode(multiple) ? null : answer; + } + return null; + + void Gather(Entity node) + { + switch (node) + { + case Mulf(var left, var right): + Gather(left); + Gather(right); + break; + case Divf(var above, var below): + Gather(above); + Gather(below); + break; + case Powf(var @base, Number.Rational exponent) when exponent is not Number.Integer && @base.ContainsNode(x): + roots.Add((node, @base, exponent)); + break; + } + } + } + + /// Set while asks its question. + [System.ThreadStatic] private static bool splittingARootOfAProduct; + /// /// A rational function of x and one square root of a quadratic in x, /// rationalised by an Euler substitution and handed to the rational integrator. diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index f7b5b5a8b..b4c2725ba 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -1203,6 +1203,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // And a power of a product with a factor under a whole power, that power taken out in // front of a constant: `(c sin(a + b x)^3)^(1/3)` is `K sin(a + b x)`. if ((answer = IndefiniteIntegralSolver.SolveByTakingTheWholePowersOutOfAPowerOfAProduct(expr, x, integrateByParts)) is { }) return answer; + // And a root of a product beside a root of one of its factors, the product split: + // `(a + b x^2)^(5/2)/sqrt(a^2 - b^2 x^4)` is `K (a + b x^2)^2/sqrt(a - b x^2)`. + if ((answer = IndefiniteIntegralSolver.SolveBySplittingARootOfAProductBesideARootOfAFactor(expr, x, integrateByParts)) is { }) return answer; // Product-to-sum among the rewrites rather than before them, because a product of // trigonometric functions of *equal* arguments is a power and wants a different tool; // this only fires where the arguments differ, which is exactly what every substitution diff --git a/Sources/Tests/UnitTests/Calculus/ARootOfAProductSplitBesideARootOfAFactorIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ARootOfAProductSplitBesideARootOfAFactorIntegralTest.cs new file mode 100644 index 000000000..76afdb560 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ARootOfAProductSplitBesideARootOfAFactorIntegralTest.cs @@ -0,0 +1,72 @@ +// +// 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 root of a product beside a root of one of its factors, the product split in front of a + /// constant: (a + b x^2)^(5/2)/sqrt(a^2 - b^2 x^4) is K (a + b x^2)^2/sqrt(a - b x^2). + /// #718 + /// + /// + /// Rubi's 1.2.2.3:282-293 were declined. Each answer is differentiated back with b of + /// either sign, so that each factor of the radicand is negative somewhere it is sampled. + /// + [Trait("Area", "Calculus")] + public sealed class ARootOfAProductSplitBesideARootOfAFactorIntegralTest + { + private static void DifferentiatesBack(string integrand, double[] points, params (string, double)[] pins) + { + var integral = integrand.ToEntity().Integrate("x"); + Assert.DoesNotContain("integral(", integral.Stringize()); + Assert.DoesNotContain("NaN", integral.Stringize()); + + var derivative = integral.Substitute("C", 0).Differentiate("x"); + Entity original = integrand.ToEntity(); + foreach (var (name, value) in pins) + { + derivative = derivative.Substitute(name, value); + original = original.Substitute(name, value); + } + + var compared = 0; + foreach (var at in points) + { + var got = derivative.Substitute("x", at).EvalNumerical(); + var want = original.Substitute("x", at).EvalNumerical(); + if (got.IsNaN || want.IsNaN) + continue; + compared++; + 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}"); + } + Assert.True(compared >= 3, + $"only {compared} of {points.Length} points were comparable for {integrand}"); + } + + [Theory] + [InlineData("(a+b*x^2)^(5/2)/sqrt(a^2-b^2*x^4)")] + [InlineData("(a+b*x^2)^(1/2)/sqrt(a^2-b^2*x^4)")] + [InlineData("1/((a+b*x^2)^(3/2)*sqrt(a^2-b^2*x^4))")] + [InlineData("(a-b*x^2)^(3/2)/sqrt(a^2-b^2*x^4)")] + [InlineData("1/((a-b*x^2)^(5/2)*sqrt(a^2-b^2*x^4))")] + public void ARootOfADifferenceOfSquaresBesideARootOfAFactor(string integrand) + { + var points = new[] { -1.3, -0.4, 0.3, 0.7, 1.6 }; + DifferentiatesBack(integrand, points, ("a", 2), ("b", 3)); + DifferentiatesBack(integrand, points, ("a", 2), ("b", -3)); + } + } +} From 04d720c9d29119b67851beca900fa852a02bef3a Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Tue, 13 Oct 2026 22:23:20 +0000 Subject: [PATCH 2/2] Record the split roots' new answers in BREAKING-CHANGES.md Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 4f6eb215f..277e44c3b 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -607,6 +607,23 @@ left ([#718](https://github.com/asc-community/AngouriMath/issues/718)). | `"1/(a + b*cot(x) + c*csc(x))".ToEntity().Integrate("x")` | `integral(...)` | 2,738 characters | | `"1/(a + c*sec(2*x + 1) + b*tan(2*x + 1))".ToEntity().Integrate("x")` | `integral(...)`; past 290 s on the unreleased master | 2,890 characters | +### A root of a product beside a root of one of its factors is split + +**Answers where there were none.** `(a + b x^2)^(5/2)/sqrt(a^2 - b^2 x^4)` was declined: nothing read a radicand as +another radicand times a cofactor. `(P Q)^r` is `K P^r Q^r` with `K = (P Q)^r/(P^r Q^r)`, constant wherever it is +defined, and the powers of `P` then gather into `K (a + b x^2)^2/sqrt(a - b x^2)` +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +The answers are as large as the split integrands' answers, which are piecewise in the signs of `a` and `b`. + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"(a + b*x^2)^(5/2)/sqrt(a^2 - b^2*x^4)".ToEntity().Integrate("x")` | `integral(...)` | 2,937 characters | +| `"(a + b*x^2)^(1/2)/sqrt(a^2 - b^2*x^4)".ToEntity().Integrate("x")` | `integral(...)` | 562 characters | +| `"1/((a + b*x^2)^(3/2)*sqrt(a^2 - b^2*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 4,191 characters | +| `"(a - b*x^2)^(3/2)/sqrt(a^2 - b^2*x^4)".ToEntity().Integrate("x")` | `integral(...)` | 6,543 characters | +| `"1/((a - b*x^2)^(1/2)*sqrt(a^2 - b^2*x^4))".ToEntity().Integrate("x")` | `integral(...)` | 877 characters | + ### A secant or a cosecant beside a power of a sum of them is read as a power of the cosine or the sine **Answers where there were none.** `sec(x) sqrt(sec(x) + tan(x))` was declined, with the rest of Rubi's 4.7.7