From 9873d7bbd232f4832b49d9e7ba9190c5d89c5056 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Fri, 11 Sep 2026 18:52:26 +0000 Subject: [PATCH] Euler's substitution, closed: a rational function of x and one square root of a quadratic, rationalised and finished by the rational integrator `sqrt(2 - x - x^2)/x^2`, `1/((4 + x^2) sqrt(1 + 4x^2))` and `x sqrt(2 r x - x^2)` had no antiderivative. The trigonometric substitution answers `x^m sqrt(a + b x^2)^k` and nothing wider, and nothing at all read a rational function of `x` and one such root -- which is also what the nested radicals of Bondarenko's suite become under `u = sqrt(1 + x)`, and what by parts against an inverse function leaves behind. ## The three substitutions For `Q = a x^2 + b x + c`: a > 0: sqrt(Q) = t - sqrt(a) x x = (t^2 - c)/(2 sqrt(a) t + b) c > 0: sqrt(Q) = x t + sqrt(c) x = (2 sqrt(c) t - b)/(a - t^2) c = 0: sqrt(Q) = x t x = b/(t^2 - a) Each makes `x`, `sqrt(Q)` and `dx/dt` rational in `t`, so the integrand becomes a rational function of `t`; whichever applies with its radical a rational number is taken first, then a real one, then the generic case for a symbol. The rational function goes to the rational integrator **directly** -- long division, the splits, the Hermite reduction, the binomial rule -- and never back into the chain. ## Why this one is not the Euler rule that was abandoned An earlier Euler rule (#1265) rewrote and handed on, and its cost was in two places: a `Simplify` it ran before it could tell whether it applied, and the open search below it once it had. This one decides by reading the tree -- every node holding `x` is a sum, product, quotient, whole power, `x` itself or the one root -- and declines in microseconds on anything else; and it lands on a closed path. Three things were measured on the way to that: - **Placed before the general substitution** it answered that rule's sub-integrals and let a doomed search run: `ln(x^2 + sqrt(1 - x^2))` went from a 19 ms decline to thirty seconds. It sits after the trigonometric and binomial rules, late. - **Scoped to the question asked** it declined nine integrands that reach it one level down -- the nested radicals, the by-parts remainders -- each closed here. It is not scoped, and the corpus says that costs nothing: no timeouts, no losses. - **Bounded at degree eight** in `t`. At nine the splits' factorisation is five seconds per attempt with nothing to show, which is what the same `ln(x^2 + sqrt(1 - x^2))` cost at the late position; Timofeev's `(1 + x^4)/((1 + x + x^2) sqrt(2 + x + x^2))` is a degree-nine case and is the one answer the bound gives up. ## Two defects it exposed, both fixed `1/(1 + sqrt(1 - x^2))` **threw**: the rewrite left a factor that collapses to the constant `-2`, a rational reader divided by its zero leading coefficient and read a NaN, and `Rational.Create` on a NaN throws. Each side of the rational function is now rebuilt from its coefficients -- the denominator factor by factor, since with `sqrt(c)` irrational only the split over written factors reads it -- and the biquadratic split declines a coefficient that is not finite rather than throwing on it. `1/((1 + x)^2 sqrt(2) sqrt(x^2 - i))` **answered `NaN`**: `-i` is a number and not a real one, and the rule took it for a symbol. A coefficient that is a number has to be real, and an answer holding a NaN is declined -- not answering is legitimate, answering NaN is not. The cancellation of common factors, shared with the trigonometric-argument rewrite, now folds nested whole powers, so `((2t + 1)^2)^2` against `(2t + 1)^4` is one factor; a substitution that squares what it built leaves the first, and a degree of twelve that was really eight was being declined. ## Measured Every answer differentiated back, with parameters pinned. Rubi corpus, 463-problem sample, against #1288's branch it was cut from, both timed on the same evening: #1288 325/463, 0 wrong, 0 timeouts, 80 s with this 334/463, 0 wrong, 0 timeouts, 90 s Nine more and nothing lost: `sqrt(2 - x - x^2)/x^2` (Apostol), `1/((4 + x^2) sqrt(1 + 4x^2))` and `x sqrt(2 r x - x^2)` (Timofeev), `sqrt(x + sqrt(1 + x))/x^2` and `tanh(x)/sqrt(e^x + e^(2x))` (Bondarenko), and Charlwood's `tan(x)/sqrt(1 + tan(x)^4)`, `sqrt(1 + tan(x)^4) tan(x)`, `atan(x)/(x^2 sqrt(1 - x^2))` and `x atan(x)/sqrt(1 - x^2)`. Welz's `sqrt(x^2 - 1)/(x - i)^2` now answers and is complex at every real sample point, which the harness reports as unverifiable. The sum of per-problem time over the sample is 79 s to 88 s: three seconds of it is Apostol's answer, whose coefficients are unfolded sums of `sqrt(2)` terms that every later simplification pays for, and four is a decline that reaches the degree bound only after by parts. The five test suites are green. Part of #718. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- Sources/.editorconfig | 3 + .../Algebra/Polynomials/PartialFractions.cs | 4 + .../Integration/IndefiniteIntegralSolver.cs | 234 +++++++++++++++++- .../Integration/Integration.Definition.cs | 10 + .../Calculus/EulerSubstitutionTest.cs | 140 +++++++++++ 5 files changed, 387 insertions(+), 4 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/EulerSubstitutionTest.cs diff --git a/Sources/.editorconfig b/Sources/.editorconfig index 28042a0be..33e444c15 100644 --- a/Sources/.editorconfig +++ b/Sources/.editorconfig @@ -402,3 +402,6 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed [Tests/UnitTests/Calculus/ABareInverseFunctionByPartsTest.cs] file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n + +[Tests/UnitTests/Calculus/EulerSubstitutionTest.cs] +file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n diff --git a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs index 1c47c3d94..fef096f97 100644 --- a/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs +++ b/Sources/AngouriMath/Functions/Algebra/Polynomials/PartialFractions.cs @@ -270,6 +270,10 @@ internal static bool TrySplitBiquadraticOverTheReals( if (q.IsZero) return false; + // A coefficient that is not finite is not a coefficient: a rewrite upstream that + // divided zero by zero reads as a rational NaN, and `Rational.Create` on it throws. + if (!p.IsFinite || !q.IsFinite) + return false; var discriminant = p.Multiply(p).Subtract(q.Multiply(ERational.FromInt32(4))); if (discriminant.IsZero) return false; diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 1271a329c..d19baa621 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -661,7 +661,14 @@ or Entity.Arctanf or Entity.Arccotanf && next is not null && rest is not null && IsRationalIn(rest, x)) remaining = next * rest; - var remainingIntegral = Integration.ComputeIndefiniteIntegral(remaining, x, partsOnTheRemainder); + // A remainder that is a rational function of x and one root of a quadratic goes + // to Euler's substitution directly, before the chain: `ln(x^2 + sqrt(1 - x^2))` + // against 1 leaves exactly that, and the chain spent five seconds of + // substitutions and reductions on it before Euler answered it in half of one. + // From the top only: a nested step's remainder answered this way let a doomed + // search above it carry on, thirty seconds where the gate leaves it at one. + var remainingIntegral = (Integration.AnsweringTheQuestionAsked ? SolveByEulerSubstitution(remaining, x) : null) + ?? Integration.ComputeIndefiniteIntegral(remaining, x, partsOnTheRemainder); if (remainingIntegral is null) return null; return v * integralOfU - remainingIntegral; @@ -2977,9 +2984,15 @@ static Dictionary Powers(Entity side) var powers = new Dictionary(); foreach (var factor in Mulf.LinearChildren(side)) { - var (@base, power) = factor is Powf(var b, Number.Integer e) && e.EInteger.CanFitInInt32() - ? (b, e.EInteger.ToInt32Unchecked()) - : (factor, 1); + // Nested whole powers folded, so that `((2t + 1)^2)^2` and `(2t + 1)^4` are + // one factor: a substitution that squares what it built leaves the first. + var @base = factor; + var power = 1; + while (@base is Powf(var inner, Number.Integer e) && e.EInteger.CanFitInInt32()) + { + power *= e.EInteger.ToInt32Unchecked(); + @base = inner; + } powers[@base] = powers.TryGetValue(@base, out var already) ? already + power : power; } return powers; @@ -3382,6 +3395,219 @@ private static Entity SquarefreePartAsWritten(Entity denominator, Entity.Variabl /// private const int MaximumAnsatzDegree = 12; + /// + /// 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. + /// + /// + /// + /// The trigonometric substitution beside this answers x^m sqrt(a + b x^2)^k and + /// nothing wider; sqrt(2 - x - x^2)/x^2, 1/((4 + x^2) sqrt(1 + 4x^2)) and + /// x sqrt(2 r x - x^2) had no antiderivative, and neither did any of the + /// integrands that other rules reduce to a rational function of x and one such + /// root -- the nested radicals of Bondarenko's suite under u = sqrt(1 + x), the + /// by-parts remainders of Charlwood's inverse functions. + /// + /// + /// Euler's three substitutions, for Q = a x^2 + b x + c: + /// + /// + /// a > 0: sqrt(Q) = t - sqrt(a) x, so x = (t^2 - c)/(2 sqrt(a) t + b). + /// c > 0: sqrt(Q) = x t + sqrt(c), so x = (2 sqrt(c) t - b)/(a - t^2). + /// c = 0: sqrt(Q) = x t, so x = b/(t^2 - a) -- the third substitution at + /// the root the quadratic has at zero. + /// + /// + /// Each makes x, sqrt(Q) and dx/dt rational in t, so the + /// integrand becomes a rational function of t; whichever applies with its radical + /// a rational number is taken first, and a symbol in a or c takes the first + /// in the generic case. The result goes to the rational integrator directly -- + /// long division, the splits, the Hermite reduction, the binomial rule -- and not back + /// into the chain: an earlier Euler rule rewrote and handed on, and its cost was in the + /// open search below it and in a Simplify it ran before it could tell whether it + /// applied at all (https://github.com/asc-community/AngouriMath/issues/1265). This one + /// decides by reading the tree, in microseconds, and finishes in one closed step or not + /// at all. + /// + /// + /// Bounded in the degree of the rational function it produces, since a degree the + /// splits cannot factor is declined by them at a cost that grows with it. Asked, not + /// volunteered, like every rule that lands on a search of any size. + /// + /// https://github.com/asc-community/AngouriMath/issues/718 + /// + internal static Entity? SolveByEulerSubstitution(Entity expr, Entity.Variable x) + { + + // One square root of a quadratic in x, and otherwise a rational function of x. + Entity? radicand = null; + foreach (var node in expr.Nodes) + { + if (!node.ContainsNode(x)) + continue; + switch (node) + { + case Variable or Sumf or Minusf or Mulf or Divf: + break; + case Powf(_, Number.Integer): + break; + case Powf(var radicalBase, Number.Rational half) when half.ERational.Denominator.Equals(EInteger.FromInt32(2)): + if (radicand is null) + radicand = radicalBase; + else if (radicand != radicalBase) + return null; + break; + default: + return null; + } + } + if (radicand is null + || !TreeAnalyzer.TryGetPolyQuadratic(radicand, x, out var a, out var b, out var c) + || a.Evaled is Number.Complex { IsZero: true }) + return null; + // A missing linear term is the trigonometric substitution's and answered more shortly there. + if (b.Evaled is Number.Complex { IsZero: true } && expr.Nodes.Count(n => n == radicand) == 1 + && TryReadAPowerTimesARadicalQuadratic(expr, x)) + return null; + + var t = Variable.CreateUnique(expr, "t_euler"); + Entity xInT, rootInT; + Entity backSubstitution; + // A coefficient that is a number has to be a real one: `sqrt(x^2 - i)` is not a + // radical any of the three substitutions is about, and taking `-i` for a symbol + // produced `NaN` for an answer. + if (a.Evaled is Number.Complex and not Number.Real || b.Evaled is Number.Complex and not Number.Real + || c.Evaled is Number.Complex and not Number.Real) + return null; + var aValue = a.Evaled as Number.Real; + var cValue = c.Evaled as Number.Real; + Entity sqrtA = MathS.Sqrt(a).InnerSimplified; + Entity sqrtC = MathS.Sqrt(c).InnerSimplified; + var aPositive = aValue is { IsPositive: true }; + var cPositive = cValue is { IsPositive: true }; + var aRational = aPositive && sqrtA.Evaled is Number.Rational; + var cRational = cPositive && sqrtC.Evaled is Number.Rational; + // Whichever applies with its radical a rational number first; then a real one; then + // the generic case for a symbol. + int which; + if (cValue is { IsZero: true }) which = 3; + else if (aRational) which = 1; + else if (cRational) which = 2; + else if (aPositive) which = 1; + else if (cPositive) which = 2; + else if (aValue is null) which = 1; + else if (cValue is null) which = 2; + else return null; // a < 0 and c < 0 with no root at zero: the radical is nowhere real + + switch (which) + { + case 3: + // sqrt(Q) = x t: x = b/(t^2 - a) + xInT = b / (MathS.Sqr(t) - a); + rootInT = xInT * t; + backSubstitution = MathS.Pow(radicand, Number.Rational.Create(1, 2)) / x; + break; + case 1: + // sqrt(Q) = t - sqrt(a) x: x = (t^2 - c)/(2 sqrt(a) t + b) + xInT = (MathS.Sqr(t) - c) / (2 * sqrtA * t + b); + rootInT = t - sqrtA * xInT; + backSubstitution = MathS.Pow(radicand, Number.Rational.Create(1, 2)) + sqrtA * x; + break; + default: + // sqrt(Q) = x t + sqrt(c): x = (2 sqrt(c) t - b)/(a - t^2) + xInT = (2 * sqrtC * t - b) / (a - MathS.Sqr(t)); + rootInT = xInT * t + sqrtC; + backSubstitution = (MathS.Pow(radicand, Number.Rational.Create(1, 2)) - sqrtC) / x; + break; + } + + var dxdt = xInT.Differentiate(t); + var rewritten = expr.Replace(node => + node is Powf(var @base, Number.Rational half) && @base == radicand + ? MathS.Pow(rootInT, Number.Integer.Create(half.ERational.Numerator)) + : node) + .Substitute(x, xInT) * dxdt; + var (numerator, denominator) = Functions.SingleQuotient.Of(rewritten.InnerSimplified); + var cancelled = CancelCommonFactors(numerator, denominator); + (numerator, denominator) = Functions.SingleQuotient.Of(cancelled); + // Each side rebuilt as a polynomial from its coefficients, so that a factor which has + // collapsed to a constant -- `-1 - t^2 + 2 t t - 1 - t^2` is `-2` -- is one before + // the rational readers see it: one of them divided by that factor's zero leading + // coefficient and read a NaN. Every coefficient must be finite for the same reason. + if (!TreeAnalyzer.TryGetPolynomial(numerator, t, out var above) || !TreeAnalyzer.TryGetPolynomial(denominator, t, out var below)) + return null; + var degree = System.Math.Max(above.Count == 0 ? 0 : (int)above.Keys.Max()!.ToInt32Checked(), + below.Count == 0 ? 0 : (int)below.Keys.Max()!.ToInt32Checked()); + if (degree > MaximumEulerDegree) + return null; + Entity? Rebuilt(Dictionary read) + { + Entity? built = null; + foreach (var pair in read.OrderBy(pair => pair.Key)) + { + var coefficient = pair.Value.InnerSimplified; + if (coefficient.Evaled is Number.Complex { IsFinite: false }) + return null; + if (coefficient.Evaled is Number.Complex { IsZero: true }) + continue; + var k = pair.Key.ToInt32Checked(); + Entity term = k == 0 ? coefficient : coefficient * (k == 1 ? t : MathS.Pow(t, k)); + built = built is null ? term : built + term; + } + return built ?? Number.Integer.Zero; + } + // The denominator keeps its written factors -- with `sqrt(c)` irrational only the + // split over written factors reads it -- and each factor is rebuilt on its own. + Entity? cleanDenominator = null; + foreach (var factor in Mulf.LinearChildren(denominator)) + { + var (@base, power) = factor is Powf(var b0, Number.Integer e0) ? (b0, e0) : (factor, Number.Integer.One); + if (!TreeAnalyzer.TryGetPolynomial(@base, t, out var baseRead) || Rebuilt(baseRead) is not { } cleanBase) + return null; + if (cleanBase == Number.Integer.Zero) + return null; + Entity cleanFactor = power == Number.Integer.One ? cleanBase : MathS.Pow(cleanBase, power); + cleanDenominator = cleanDenominator is null ? cleanFactor : cleanDenominator * cleanFactor; + } + if (Rebuilt(above) is not { } cleanNumerator || cleanDenominator is null) + return null; + var rational = cleanNumerator / cleanDenominator; + + var inT = SolveByPartialFractions(rational, t, integrateByParts: false) + ?? IntegralPatterns.TryStandardIntegrals(rational, t); + if (inT is null) + return null; + var answer = inT.Substitute(t, backSubstitution).InnerSimplified; + // Not answering is legitimate; answering NaN is not. + return answer.Nodes.Any(n => n is Number.Complex { IsNaN: true }) ? null : answer; + } + + /// + /// The largest degree, in t, of the rational function + /// hands to the rational integrator. + /// + private const int MaximumEulerDegree = 8; + + /// + /// Whether is x^m sqrt(a + b x^2)^k up to a constant -- + /// the shape the trigonometric substitution answers. + /// + private static bool TryReadAPowerTimesARadicalQuadratic(Entity expr, Entity.Variable x) + { + foreach (var (factor, _) in FactorsOfTheIntegrand(expr)) + { + if (!factor.ContainsNode(x)) + continue; + if (factor == x || factor is Powf(var b1, Number.Integer) && b1 == x) + continue; + if (factor is Powf(var b2, Number.Rational) && TreeAnalyzer.TryGetPolyQuadratic(b2, x, out _, out var linear, out _) + && linear.Evaled is Number.Complex { IsZero: true }) + continue; + return false; + } + return true; + } + /// /// A product of sines and cosines of different arguments, rewritten as a sum by the /// product-to-sum identities and then integrated term by term. diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 73f233319..58e5831f6 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -503,6 +503,16 @@ private static Entity Normalized(Entity expr) => // `a + b x^n`. After the quadratic, which is the case `n = 2` and answers it through // the trigonometric substitution -- a shorter answer than a root of a root. if ((answer = IndefiniteIntegralSolver.SolveABinomialDifferential(expr, x)) is { }) return answer; + // And a rational function of x and one square root of a quadratic, rationalised by + // Euler's substitution and finished by the rational integrator directly. After the + // trigonometric substitution and the binomial differential, which answer their own + // shapes more shortly; this is for everything else with one such root in it. Late + // and not scoped, and both were measured: placed before the general substitution it + // answered that rule's sub-integrals and let a doomed search run for thirty seconds + // (the #1265 failure), and scoped it declined nine integrands that reach it one + // level down -- the nested radicals under `u = sqrt(1 + x)`, the remainders of by + // parts -- each of which is closed here. + if ((answer = IndefiniteIntegralSolver.SolveByEulerSubstitution(expr, x)) 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/EulerSubstitutionTest.cs b/Sources/Tests/UnitTests/Calculus/EulerSubstitutionTest.cs new file mode 100644 index 000000000..271dfeead --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/EulerSubstitutionTest.cs @@ -0,0 +1,140 @@ +// +// 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 x and one square root of a quadratic, rationalised by + /// an Euler substitution and finished by the rational integrator. + /// #718 + /// + /// + /// + /// The trigonometric substitution answers x^m sqrt(a + b x^2)^k and nothing wider. + /// sqrt(2 - x - x^2)/x^2, 1/((4 + x^2) sqrt(1 + 4x^2)) and + /// x sqrt(2 r x - x^2) had no antiderivative, and neither did anything another rule + /// reduces to a rational function of x and one such root. Each of Euler's three + /// substitutions makes x, the root and dx rational in t; the rational + /// function goes to the rational integrator directly, and the answer comes back through + /// t = sqrt(Q) + sqrt(a) x or its kin. + /// + /// + /// Every answer is differentiated back at points where the root is real. The one input that + /// used to throw — a complex constant under the root — and the one that used to answer + /// NaN are pinned as declined. + /// + /// + [Trait("Area", "Calculus")] + public sealed class EulerSubstitutionTest + { + 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}"); + } + + /// + /// The first substitution, a > 0: Timofeev's 1/((4 + x^2) sqrt(1 + 4x^2)) + /// and its neighbours. + /// + [Theory] + [InlineData("1/((4 + x^2)*sqrt(1 + 4*x^2))", new[] { 0.3, 0.9, 1.7, 2.6 })] + [InlineData("sqrt(1 + x^2)/(1 + x)", new[] { 0.3, 0.9, 1.7, 2.6 })] + [InlineData("1/(x*sqrt(x^2 + x + 1))", new[] { 0.3, 0.9, 1.7, 2.6 })] + [InlineData("x/(1 + sqrt(1 + x^2))", new[] { 0.3, 0.9, 1.7, 2.6 })] + [InlineData("1/((x - 1)*sqrt(x^2 - 2*x + 5))", new[] { 1.3, 1.9, 2.7, 3.6 })] + public void ThePositiveLeadingCoefficient(string integrand, double[] points) => DifferentiatesBack(integrand, points); + + /// + /// The second, c > 0: Apostol's sqrt(2 - x - x^2)/x^2, and the input that + /// once made a rational reader divide by zero. + /// + [Theory] + [InlineData("sqrt(2 - x - x^2)/x^2", new[] { 0.2, 0.4, 0.6, 0.8 })] + [InlineData("1/(1 + sqrt(1 - x^2))", new[] { 0.2, 0.4, 0.6, 0.8 })] + public void ThePositiveConstant(string integrand, double[] points) => DifferentiatesBack(integrand, points); + + /// + /// The third, at the root the quadratic has at zero: Timofeev's x sqrt(2 r x - x^2), + /// with r a symbol. + /// + [Theory] + [InlineData("x*sqrt(2*r*x - x^2)", new[] { 0.3, 0.9, 1.7, 2.6 })] + [InlineData("sqrt(x^2 + 2*x)/x", new[] { 0.3, 0.9, 1.7, 2.6 })] + public void TheRootAtZero(string integrand, double[] points) => DifferentiatesBack(integrand, points, ("r", 1.7)); + + /// + /// Reached one level down, which is why the rule is not scoped: a nested radical under + /// u = sqrt(1 + x), and the remainders of by parts against an inverse function. + /// + [Theory] + [InlineData("sqrt(x + sqrt(1 + x))/x^2", new[] { 0.4, 0.9, 1.7, 2.6 })] + [InlineData("x*atan(x)/sqrt(1 - x^2)", new[] { 0.2, 0.4, 0.6, 0.8 })] + [InlineData("atan(x)/(x^2*sqrt(1 - x^2))", new[] { 0.2, 0.4, 0.6, 0.8 })] + [InlineData("tanh(x)/sqrt(exp(x) + exp(2*x))", new[] { 0.3, 0.9, 1.7, 2.6 })] + public void ReachedOneLevelDown(string integrand, double[] points) => DifferentiatesBack(integrand, points); + + /// + /// A complex constant under the root is none of the three substitutions', and taking + /// -i for a symbol once answered NaN; a rational function of the root + /// whose Euler form is past the degree the splits can factor is declined too, and not + /// slowly. + /// + [Theory] + [InlineData("1/((1 + x)^2*sqrt(2)*sqrt(-i + x^2))")] + [InlineData("ln(x^2 + sqrt(1 - x^2))")] + public void DeclinedRatherThanWrongOrSlow(string integrand) + { + var watch = System.Diagnostics.Stopwatch.StartNew(); + var integral = integrand.ToEntity().Integrate("x"); + Assert.True(watch.Elapsed < TimeSpan.FromSeconds(20), $"took {watch.Elapsed}"); + Assert.DoesNotContain("NaN", integral.Stringize()); + } + + /// + /// The shapes the rule leaves to the rules that answer them more shortly keep their + /// answers: a power of the variable times a root with no linear term, and a root of + /// something linear. + /// + [Theory] + [InlineData("x^3/sqrt(1 + x^2)", "-((1 / sqrt(1 + x ^ 2)) ^ (-3) / (-3) + -1 / (1 / sqrt(1 + x ^ 2)) / (-1)) + C")] + [InlineData("sqrt(1 + 2*x)", "(1 + 2 * x) ^ (3/2) / (3/2) / 2 + C")] + public void TheNeighboursKeepTheirForms(string integrand, string expected) + => Assert.Equal(expected, integrand.ToEntity().Integrate("x").Stringize()); + } +}