diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 4cf77a562..66785bb64 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -1058,6 +1058,25 @@ secant too now, and the integrand is `sgn(cos(x)) (1 + u^2)^(3/2)/(a + b u)^2` i | `"sec(x)^7/(a + b*tan(x))^3".ToEntity().Integrate("x")` | `integral(...)` | 1,253 characters | | `"sec(x)/(a + b*cot(x))".ToEntity().Integrate("x")` | `integral(...)` | 571 characters | +### A sum with the root of a quadratic below the bar is cleared by its conjugate + +**Answers where there were none.** `1/(a c + b c x^2 + d sqrt(a + b x^2))` was declined: Euler's substitution +writes `sqrt(b)` into every coefficient for a symbolic leading coefficient, and the rational integrator could not +finish. `(A + B sqrt(Q))/(C + D sqrt(Q))` is `(A + B sqrt(Q)) (C - D sqrt(Q))/(C^2 - D^2 Q)`, a rational function and +one beside the root, and each is answered +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +The symbolic answers are large because each piece's answer is piecewise in the signs of the constants, and adding +two of them multiplies their cases. + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"1/(a*c + b*c*x^2 + d*sqrt(a + b*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 3,464 characters | +| `"x^2/(a*c + b*c*x^2 + d*sqrt(a + b*x^2))".ToEntity().Integrate("x")` | `integral(...)` | 1,605 characters | +| `"1/(x^2*(a*c + b*c*x^2 + d*sqrt(a + b*x^2)))".ToEntity().Integrate("x")` | `integral(...)` | 5,052 characters | +| `"1/(c*sqrt(a + b*x^2) + d)".ToEntity().Integrate("x")` | `integral(...)` | 2,888 characters | +| `"x*sqrt(2 - x^2)/(x - sqrt(2 - x^2))".ToEntity().Integrate("x")` | `integral(...)` | 195 characters | + ### A quadratic with rational roots takes Euler's third substitution at one of them **Answers where there were none.** `1/(x + sqrt(3 - 2x - x^2))` was declined. Its leading coefficient is negative and diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 12ba5197e..217d26362 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -18955,6 +18955,82 @@ node is Powf(var @base, Number.Rational r) && byRadicand.TryGetValue(@base, out return (rewritten, ratios); } + /// + /// A rational function of x and one square root of a quadratic Q, with the + /// root in a sum below the bar, written over a denominator free of it: + /// (A + B sqrt(Q))/(C + D sqrt(Q)) is (A + B sqrt(Q)) (C - D sqrt(Q))/(C^2 - D^2 Q), + /// a rational function and one beside the root, each asked of the chain. + /// + /// + /// Rubi's 1/(a c + b c x^2 + d sqrt(a + b x^2)): Euler's substitution takes the + /// generic first substitution for a symbolic leading coefficient, with sqrt(b) in + /// every coefficient, and was declined. Over the conjugate it is + /// (c Q - d sqrt(Q))/(c^2 Q^2 - d^2 Q), and both pieces are answered. It runs after + /// every rule that answers the numeric spellings. Only a sum: a root alone below the bar, + /// C = 0, is the root moved above it, which the chain writes back, and D = 0 + /// has nothing to clear. + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveByTheConjugateOfASumWithARootOfAQuadraticBelowTheBar(Entity expr, Entity.Variable x, bool integrateByParts) + { + Entity? radicand = null; + foreach (var node in expr.Nodes) + if (node is Powf(var @base, Number.Rational r) && r is not Number.Integer && @base.ContainsNode(x)) + { + if (!r.ERational.Denominator.Equals(EInteger.FromInt32(2)) || !r.ERational.Numerator.CanFitInInt32() + || radicand is not null && radicand != @base) + return null; + radicand = @base; + } + if (radicand is null || !TreeAnalyzer.TryGetPolyQuadratic(radicand, x, out var a, out _, out _) || VanishesIdentically(a)) + return null; + var w = Variable.CreateUnique(expr, "w_root"); + var inW = expr.Replace(node => node is Powf(var @base, Number.Rational r) && r is not Number.Integer && @base == radicand + ? MathS.Pow(w, Number.Integer.Create(r.ERational.Numerator)) + : node); + var (top, bottom) = Functions.SingleQuotient.Of(Functions.SingleQuotient.Combine(inW)); + if (!bottom.ContainsNode(w) || InTheRoot(top) is not var (above, aboveBeside) || InTheRoot(bottom) is not var (below, belowBeside) + || VanishesIdentically(below) || VanishesIdentically(belowBeside)) + return null; + var over = (below * below - belowBeside * belowBeside * radicand).InnerSimplified; + if (VanishesIdentically(over)) + return null; + var rational = (above * below - aboveBeside * belowBeside * radicand).InnerSimplified; + var beside = (aboveBeside * below - above * belowBeside).InnerSimplified; + Entity answer = Number.Integer.Zero; + if (!VanishesIdentically(rational)) + { + if (Integration.ComputeIndefiniteIntegral(rational / over, x, integrateByParts) is not { } first) + return null; + answer += first; + } + if (!VanishesIdentically(beside)) + { + if (Integration.ComputeIndefiniteIntegral(beside * MathS.Sqrt(radicand) / over, x, integrateByParts) is not { } second) + return null; + answer += second; + } + return answer; + + // A polynomial in w as A + B w, with w^2 = Q. + (Entity Whole, Entity Beside)? InTheRoot(Entity side) + { + if (!TreeAnalyzer.TryGetPolynomial(side.Expand().InnerSimplified, w, out var monomials) || monomials.Keys.Any(k => k.Sign < 0)) + return null; + Entity whole = Number.Integer.Zero; + Entity besideTheRoot = Number.Integer.Zero; + foreach (var pair in monomials) + { + // Q^0 written as a power is `1 provided not Q = 0`, a condition the pieces would carry. + var k = pair.Key.ToInt32Checked(); + var term = k / 2 == 0 ? pair.Value : pair.Value * MathS.Pow(radicand, k / 2); + if (k % 2 == 0) whole += term; + else besideTheRoot += term; + } + return (whole.InnerSimplified, besideTheRoot.InnerSimplified); + } + } + /// /// 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 924f34825..6ad575041 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -1197,6 +1197,9 @@ private static Entity Normalized(Entity expr, Entity.Variable x) => // `A - B y` whose A/B agrees with a branch of y at the poles, found by an ansatz // over Padé approximants. if ((answer = SquareRootLogarithmAnsatz.Integrate(expr, x)) is { }) return answer; + // And a sum with the root below the bar, over its conjugate: after every rule above, + // which answer the numeric spellings, `1/(a c + b c x^2 + d sqrt(a + b x^2))` among them. + if ((answer = IndefiniteIntegralSolver.SolveByTheConjugateOfASumWithARootOfAQuadraticBelowTheBar(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/TheConjugateOfASumWithARootBelowTheBarIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/TheConjugateOfASumWithARootBelowTheBarIntegralTest.cs new file mode 100644 index 000000000..91ad68d91 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/TheConjugateOfASumWithARootBelowTheBarIntegralTest.cs @@ -0,0 +1,78 @@ +// +// 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 sum with the square root of a quadratic below the bar, cleared by its conjugate. + /// #718 + /// + /// + /// Rubi's 1/(a c + b c x^2 + d sqrt(a + b x^2)) and its x^2 and 1/x^2 + /// neighbours had no antiderivative: Euler's substitution declines a symbolic leading + /// coefficient. Each answer is differentiated back with the constants pinned on both signs of + /// b and of a c^2 - d^2, at points where the root is real. + /// + [Trait("Area", "Calculus")] + public sealed class TheConjugateOfASumWithARootBelowTheBarIntegralTest + { + 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)); + 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("1/(a*c+b*c*x^2+d*sqrt(a+b*x^2))")] + [InlineData("x^2/(a*c+b*c*x^2+d*sqrt(a+b*x^2))")] + [InlineData("1/(x^2*(a*c+b*c*x^2+d*sqrt(a+b*x^2)))")] + [InlineData("1/(c*sqrt(a+b*x^2)+d)")] + public void SymbolicConstants(string integrand) + { + var points = new[] { 0.3, 0.7, 1.1, 1.3 }; + DifferentiatesBack(integrand, points, ("a", 2), ("b", 3), ("c", 1.25), ("d", 1)); + DifferentiatesBack(integrand, points, ("a", 2), ("b", 3), ("c", 0.5), ("d", 3)); + DifferentiatesBack(integrand, points, ("a", 2), ("b", -1), ("c", 1), ("d", 0.5)); + DifferentiatesBack(integrand, points, ("a", 2), ("b", 3), ("c", 1), ("d", -0.5)); + } + + [Fact] + public void ARootBesideXOverTheirDifference() + => DifferentiatesBack("x*sqrt(2-x^2)/(x-sqrt(2-x^2))", new[] { -0.5, 0.3, 1.2, 1.3 }); + } +}