From a803baf0039a0206b3618dbb66857aa7ae7cf898 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 21 Oct 2026 22:47:02 +0000 Subject: [PATCH] Do not simplify a substitution's quotient that still varies with x, which no rewriting can take out The substitution search simplified every candidate's quotient with symbols in it before refusing the ones that kept an x, and some of those simplifications ran past any budget: tan(c + d x)^(11/2)/(a + b tan(c + d x))^3 spent 31 s on one candidate under u = t^(3/2). Where the quotient in u and x, the two read as independent, takes two values at two points, a rewriting that keeps its value cannot take the x out; the simplification is not tried there, nor in the step that writes the integrand as a polynomial in the candidate, nor in the pass in the complement, and the scaling of the variable reads at two points first whether the integrand is homogeneous. A quotient that does not vary is simplified as before, and so is one whose candidate has a symbolic factor and may leave a power of itself. Kept, the quotient's sums in x alone have their like terms collected, and a constant condition is read past: under u = t^2, what Rubi's 1/((d + e x) (a + b (d + e x)^2 + c (d + e x)^4)^3) leaves in t = d + e x is (d + t - d) 2 t (a + b u + c u^2)^3 below the bar, whose t^2 is the candidate, and read with the d in it, it was refused and 1.2.3.2:854 was lost in the first chain. Kept, the quotient also has a condition over the whole of it taken off, as the simplification takes its own, and where an x is still left that may be a power of the candidate, it is simplified after all and read again. Without either, the first composite of the open branches lost eight rows master solves: the shifted linear's d^0 is 1 provided not d = 0 inner-simplified, and under u = t^3 Rubi's 1/((c + d x) (a + b (c + d x)^3)) left 1/(3 u (a + b u)) provided not d = 0, whose integral in u was declined; and beside u = x^x, e^(x^x) x^(2 x) (1 + ln(x)) kept an x/x the inner simplification leaves standing, and the u^2 in it was not seen. That second step is taken only where the quotient by the power is read at two points and is one value, and not beside a special function of x: read for every kept candidate beside erf(b x), Rubi's x^5 erf(b x)^2 took 4.85 GB where it takes 1.63. Chain against master f7f2a7a8: pocket of 571 rows, 517 -> 524 solved, 0 wrong; families 3 and 6's samples 222 -> 223 and 471 -> 472, the others unchanged; of the 16 rows that moved, alone, master solved 5 and this solves 11, every one of master's among them, and 1.1.2.4:1197 and 3.3:246 in 4.7 s and 0.2 s where master took 19.6 s and 15.8 s. 6,048 rows compared for allocation: two grew past 1.3x, tanh(x)/sqrt(a + b cosh(x)^n) and coth(c + d x)^3/(a + b sech(c + d x))^(3/2), +0.75 and +0.44 GB, each answered in another spelling (777 characters where master gives 589, and 9,095 where it gives 8,290); 2.5.0 answers neither. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 20 +++ .../Integration/IndefiniteIntegralSolver.cs | 170 +++++++++++++++++- ...sTheVariableIsNotSimplifiedIntegralTest.cs | 111 ++++++++++++ 3 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/ASubstitutionThatKeepsTheVariableIsNotSimplifiedIntegralTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 7b8cb4a18..f272ff529 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -2278,6 +2278,26 @@ takes the branches of that respelling | `"sec(x)*sqrt(cos(x) - sin(x))/sin(x)^(3/2)".ToEntity().Integrate("x")` | `integral(...)` | 74 characters | | `"1/(cos(x)^(3/2)*sqrt(3*cos(x) + sin(x)))".ToEntity().Integrate("x")` | `integral(...)` | `2 * sqrt(tan(x) + 3)` | +### A substitution's quotient that still varies with x is not simplified + +**Answers where there were none.** `tan(c + d x)^(11/2)/(a + b tan(c + d x))^3` ran out of time, and six more of +Rubi's logarithmic, trigonometric and hyperbolic integrands did on the unreleased master. The time went in simplifications +deep in the substitution search, of a candidate's quotient with symbols in it, where that quotient in `u` and `x`, +the two read as independent, still varies with `x`: a rewriting that keeps its value cannot take that `x` out. Two +values at two positive points now say so, and the simplification is not tried; the same in the step that writes the +integrand as a polynomial in the candidate, in the pass in the complement, and in the scaling of the variable, which +reads at two points whether the integrand is homogeneous first. A quotient that does not vary is simplified as before, +and so is one whose candidate has a symbolic factor free of `x` and may leave a power of itself, whose simplification is +what carries the substitution's own condition, `provided not k = 0` beside `F^(k sqrt(1 - a x)/sqrt(1 + a x))` +([#718](https://github.com/asc-community/AngouriMath/issues/718)). + +| Input | Was (2.5.0) | Now | +|---|---|---| +| `"tan(c+d*x)^(11/2)/(a+b*tan(c+d*x))^3".ToEntity().Integrate("x")` | no answer in twenty seconds | 4,909 characters | +| `"tan(c+d*x)^4/(a+i*a*tan(c+d*x))^(4/3)".ToEntity().Integrate("x")` | `integral(...)` | 3,713 characters | +| `"1/((c+d*sin(k+f*x))*sqrt(g*sin(k+f*x))*sqrt(a+a*sin(k+f*x)))".ToEntity().Integrate("x")` | `integral(...)` | 1,473 characters | +| `"x^(7/2)/((a+b*x^2)^2*(c+d*x^2)^3)".ToEntity().Integrate("x")` | `integral(...)` | 2,685 characters | + ### An even rational function with symbols in it is not searched for a substitution **Answers where there were none.** `(d + e x^2 + f x^4)/(x^4 (a + b x^2 + c x^4)^2)` ran past a minute: the diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index 5a953b156..6c6c575e1 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -33818,6 +33818,16 @@ static System.Numerics.Complex ToComplex(Number.Complex number) // dx = c dt. var t = Variable.CreateUnique(expr, "u_scale"); + // Homogeneous or not, read at two points first: the scaled integrand over itself at + // c = 1 is c^k wherever it is c^k h(t), the same at every t for a positive c, and + // where it is not the same the simplifications below cannot make it so. Each of + // them is a simplification of the whole integrand with the symbol in it, and the + // third alone, of the quotient, ran past thirty seconds on the rational function the + // imaginary tangent's substitution makes of Rubi's `tan(c + d x)^4/(a + i a tan(c + d x))^(4/3)` + // before it was declined. https://github.com/asc-community/AngouriMath/issues/718 + var scaledAsWritten = expr.Substitute(x, scale * t) * scale; + if (VariesWith((scaledAsWritten / scaledAsWritten.Substitute(scale, Number.Integer.One)).Substitute(scale, Number.Rational.Create(137, 100)), t)) + return null; // With the imaginary unit in the integrand the simplifier's search beside the symbol // does not end within any budget, and inner simplification separates what a whole // power of the scale does: `((1 + i a x)/(1 - i a x))^(-5/4)/x^2` under its radical's @@ -42196,6 +42206,83 @@ void Add(Entity root, Entity times) } } + /// + /// Whether takes two clearly different values at two positive + /// points of , every other symbol pinned to the same number at both: + /// then it depends on as a function of all its symbols, and no + /// rewriting that keeps its value writes it without . False wherever + /// a value cannot be had, and wherever the two agree. + /// + private static bool VariesWith(Entity expr, Entity.Variable x) => TakesTwoValues(expr, x) == true; + + /// + /// , with where a value cannot be had at + /// either point: false is a decision that the two agree. + /// + private static bool? TakesTwoValues(Entity expr, Entity.Variable x) + { + var values = new[] { "1.37", "0.61", "2.23", "1.91", "0.83", "1.13" }; + var pinned = expr; + var index = 0; + foreach (var symbol in expr.Vars.Distinct().ToList()) + if (symbol != x) + pinned = pinned.Substitute(symbol, Number.Real.Create(EDecimal.FromString(values[index++ % values.Length]))); + Number.Complex? At(string point) + => pinned.Substitute(x, Number.Real.Create(EDecimal.FromString(point))).Evaled is Number.Complex { IsNaN: false } number && number.IsFinite + ? number : null; + if (At("0.47") is not { } first || At("1.53") is not { } second) + return null; + var scale = System.Math.Max(1.0, System.Math.Max(((Number.Real)first.Abs()).EDecimal.ToDouble(), ((Number.Real)second.Abs()).EDecimal.ToDouble())); + return ((Number.Real)(first - second).Abs()).EDecimal.ToDouble() > 1e-6 * scale; + } + + /// + /// Whether is a factor free of with a + /// symbol in it times a function of , and what it leaves of + /// in may be a constant times its power + /// from -2 to 2: the quotient by one of those powers does not vary with , + /// read off two points, or cannot be read there. That is what + /// takes from the simplified quotient, where the + /// factor stands on both sides of the bar and its cancelling is the condition that it is not + /// zero, under which the candidate varies at all. Never beside a linear candidate, which that + /// rewriting does not read. + /// + private static bool APowerOfACandidateWithASymbolicFactorMayBeLeftIn(Entity integrandInU, Entity candidate, Entity.Variable x) + { + if (IsALinearCandidate(candidate, x)) + return false; + var (above, below) = Functions.SingleQuotient.Of(candidate); + return Mulf.LinearChildren(above).Concat(Mulf.LinearChildren(below)).Any(factor => !factor.ContainsNode(x) && factor.Vars.Any()) + && APowerOfTheCandidateMayBeLeftIn(integrandInU, candidate, x); + } + + /// Whether is written as a polynomial of degree at most one in . + private static bool IsALinearCandidate(Entity candidate, Entity.Variable x) + => IsWrittenAsAPolynomial(candidate, x) && TreeAnalyzer.TryGetPolynomial(candidate, x, out var asAPolynomial) + && asAPolynomial.Keys.Max()!.CompareTo(EInteger.One) <= 0; + + /// + /// Whether what leaves of may be a + /// constant times a power of from -2 to 2, the powers + /// reads: the quotient by one of them does not + /// vary with , read off two points, or cannot be read there. + /// + private static bool APowerOfTheCandidateMayBeLeftIn(Entity integrandInU, Entity candidate, Entity.Variable x) + => APowerOfTheCandidateIsLeftIn(integrandInU, candidate, x, unreadIsLeft: true); + + /// + /// Whether the quotient of by a power of + /// from -2 to 2 takes one value at two points: or may, where + /// , for a quotient that cannot be read there. + /// + private static bool APowerOfTheCandidateIsLeftIn(Entity integrandInU, Entity candidate, Entity.Variable x, bool unreadIsLeft) + { + foreach (var power in new[] { -1, 1, -2, 2 }) + if (TakesTwoValues(integrandInU / MathS.Pow(candidate, power), x) is var takes && (takes is false || unreadIsLeft && takes is null)) + return true; + return false; + } + /// /// Whether over takes two values at /// two points where takes one -- every other symbol pinned -- @@ -42591,6 +42678,10 @@ private static bool VariesWithTheVariableWhereTheCandidateIsHeld(Entity expr, Va /// private static Entity? WithTheCandidatesPowerLeftInX(Entity integrandInU, Entity u, Variable uSub, Entity.Variable x) { + // A condition on the constants, `provided not e = 0` from a change of scale, is read + // past and kept: as one factor it held both x and the candidate's variable. + if (integrandInU is Providedf(var provided, var condition) && !condition.ContainsNode(x) && !condition.ContainsNode(uSub)) + return WithTheCandidatesPowerLeftInX(provided, u, uSub, x) is { } read ? read.Provided(condition) : null; // Not for a linear candidate: what is left of x beside `u = f x` is always a power of u, // and taking the change of scale here asks the same question again in u, ahead of the // rules that answer it. `(c + d x)^2/(a + i a tan(e + f x))^3` is answered in 33 s, @@ -45968,13 +46059,19 @@ private static bool HoldsTheImaginaryUnit(Entity expr) /// private static Entity SimplifiedWithoutTheImaginaryUnit(Entity written, Entity integrand) { - var simplified = written.Simplify(1); - if (simplified is Providedf(var inner, _)) simplified = inner; // TODO: singularities ignored but not handled properly + var simplified = WithoutItsCondition(written.Simplify(1)); return HoldsTheImaginaryUnit(simplified) && !HoldsTheImaginaryUnit(written) && !HoldsTheImaginaryUnit(integrand) ? written : simplified; } + /// + /// without a condition written over the whole of it, which the + /// integral in a candidate's variable does not read. + /// + private static Entity WithoutItsCondition(Entity integrandInU) + => integrandInU is Providedf(var inner, _) ? inner : integrandInU; // TODO: singularities ignored but not handled properly + /// /// Attempts to solve an integral using u-substitution. /// Looks for patterns where f(g(x)) * g'(x) can be integrated as F(g(x)). @@ -46254,6 +46351,10 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) var complemented = WithTheComplementInEvenPowers(firstPass[u], u, uSub); if (complemented == firstPass[u]) continue; // nothing the first pass did not see + // Nothing after the simplification here reads the candidate, so an x the quotient + // varies with, which the simplification cannot take out, refuses it at once. + if (VariesWith(complemented, x)) + continue; integrandInU = SimplifiedWithoutTheImaginaryUnit(complemented, expr); } else @@ -46324,7 +46425,40 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) && !inTermsOfU.Nodes.Any(node => IsASpecialFunction(node) && node.ContainsNode(x)) && VariesWhereASpecialFunctionIsHeld(inTermsOfU, uSub, x)) continue; - integrandInU = !inTermsOfU.ContainsNode(x) + // An x left that the quotient in u and x, the two read as independent, varies + // with: the simplification, the cancelling and the gathering below keep the + // value, and none of them can take that x out, so none is tried. Each was a + // simplification of a rational function with symbols in it to be told so, and + // one alone can run past any budget: the remainder the division leaves of + // `t^(11/2)/((a/b + t)^3 (1 + t^2))`, which the tangent makes of Rubi's + // `tan(c + d x)^(11/2)/(a + b tan(c + d x))^3`, took thirty-one seconds to be + // simplified under `u = t^(3/2)` and forty-four for the four powers of t before + // `u = sqrt(t)` answered it. The rewritings after them, which read u = g(x), + // still see the quotient. Not under a sine or a cosine, whose quotient the pass + // in the complement reads simplified. + // And not where the candidate has a symbolic factor free of x and what is left + // in x may be a constant times a power of it, which the rewriting below reads off + // the simplified quotient: under `u = k sqrt(1 - a x)/sqrt(1 + a x)` Rubi's + // `F^u/(1 - a^2 x^2)` over du/dx is `-F^u/(a u)`, and read off the simplified + // quotient it keeps a `k/k` whose cancelling is the answer's `provided not k = 0`, + // where the candidate varies at all. Read off the quotient as written, the answer + // came back without it. Beside a candidate with no such factor the rewriting + // reads either spelling alike, and the simplification is still not tried. + // https://github.com/asc-community/AngouriMath/issues/718 + var keepsTheVariable = u is not (Sinf or Cosf) && inTermsOfU.ContainsNode(x) && VariesWith(inTermsOfU, x) + && !APowerOfACandidateWithASymbolicFactorMayBeLeftIn(inTermsOfU, u, x); + // Kept, its sums in x alone have their like terms collected, which the inner + // simplification leaves standing: under `u = t^2`, what Rubi's + // `1/((d + e x) (a + b (d + e x)^2 + c (d + e x)^4)^3)` leaves in `t = d + e x` is + // `1/((d + t - d) 2 t (a + b u + c u^2)^3)`, whose `t^2` is the candidate, and read + // with the `d` in it, it was refused as a power of nothing. And a condition on it is + // taken off, as the simplification below takes its own: the shifted linear writes + // `c + d^0 (t - c)`, inner-simplified to `1 provided not d = 0`, and under `u = t^3` + // Rubi's `1/((c + d x) (a + b (c + d x)^3))` left `1/(3 u (a + b u)) provided not d = 0`, + // whose integral in u was declined. + integrandInU = keepsTheVariable + ? WithoutItsCondition(inTermsOfU.InnerSimplified).Replace(node => node is Sumf or Minusf && node.ContainsNode(x) && !node.ContainsNode(uSub) ? Collected(node) : node) + : !inTermsOfU.ContainsNode(x) && TreeAnalyzer.TryGetPolyLinear(u, x, out var slope, out var offset) && !slope.ContainsNode(x) && !offset.ContainsNode(x) && IsAFunctionOfTheTangentAlone(inTermsOfU, uSub) && !HoldsTheImaginaryUnit(inTermsOfU) ? Functions.PartialFractions.Bare(inTermsOfU.InnerSimplified) @@ -46339,7 +46473,7 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) // A factor written on both sides of the bar cancelled, where x survived: // the one-level simplification leaves `u/((a w + b)^2 p u)` as it is, and // the candidate was refused for the u it did not cancel. - if (integrandInU.ContainsNode(x)) + if (!keepsTheVariable && integrandInU.ContainsNode(x)) { var (top, bottom) = Functions.SingleQuotient.Of(Functions.SingleQuotient.Combine(integrandInU)); var cancelled = CancelCommonFactors(top, bottom); @@ -46368,7 +46502,7 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) // from: done for every candidate that left an x, it gathered the exponentials of // `1/((c + d x)^3 (a + a tanh(e + f x)))` and simplified them for every one, and // a decline in a third of a second became a timeout. - if (integrandInU.ContainsNode(x) && IsASpecialFunction(u)) + if (!keepsTheVariable && integrandInU.ContainsNode(x) && IsASpecialFunction(u)) { var (above, below) = Functions.SingleQuotient.Of(Functions.SingleQuotient.Combine(integrandInU)); var spread = above; @@ -46399,7 +46533,14 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) // linears on either side of a nested division. var (aboveTheBar, belowTheBar) = Functions.SingleQuotient.Of(Functions.SingleQuotient.Combine(integrandInU)); var inTheCandidate = WithPolynomialsInTheCandidate(aboveTheBar, u, uSub, x) / WithPolynomialsInTheCandidate(belowTheBar, u, uSub, x); - if (inTheCandidate.ContainsNode(x) == false || inTheCandidate != aboveTheBar / belowTheBar) + // And not simplified where what is left still varies with x: `t` below the + // bar beside `u = 1 - t^2` is no polynomial in it, and the simplification of + // `u^(9/2)/(2 t (c u + d (2 - u))^2 (2 - u))`, which the half-angle tangent + // leaves of Rubi's `1/((a + a sec(e + f x))^(5/2) (c + d sec(e + f x))^2)`, + // did not return in five minutes. + if (inTheCandidate.ContainsNode(x) && VariesWith(inTheCandidate, x) && !APowerOfACandidateWithASymbolicFactorMayBeLeftIn(inTheCandidate, u, x)) + integrandInU = WithoutItsCondition(inTheCandidate.InnerSimplified); + else if (inTheCandidate.ContainsNode(x) == false || inTheCandidate != aboveTheBar / belowTheBar) { integrandInU = SimplifiedWithoutTheImaginaryUnit(inTheCandidate, expr); } @@ -46412,6 +46553,23 @@ private static bool IsAFunctionOfTheTangentAlone(Entity expr, Entity.Variable u) // https://github.com/asc-community/AngouriMath/issues/718 if (integrandInU.ContainsNode(x) && WithTheCandidatesPowerLeftInX(integrandInU, u, uSub, x) is { } inPowersOfTheCandidate) integrandInU = inPowersOfTheCandidate; + // Kept, and an x still left that is a power of the candidate, which the kept + // spelling did not let the rewriting read: simplified after all, as it was before + // the gate, and read again. Beside `u = x^x` the kept quotient of Rubi's + // `e^(x^x) x^(2 x) (1 + ln(x))` is `e^u x^(2 x) (1 + ln(x))/(u (x/x + ln(x)))`, + // whose `x/x` the inner simplification leaves standing, and the `u^2` in it was + // not seen. Only where the quotient by the power is read and is one value, and not + // beside a special function of x, whose value in decimals is a series: read four + // times for every candidate kept, `erf(b x)` took Rubi's `x^5 erf(b x)^2` to 4.85 GB + // where it takes 1.63. + if (keepsTheVariable && integrandInU.ContainsNode(x) && !IsALinearCandidate(u, x) + && !inTermsOfU.Nodes.Any(node => IsASpecialFunction(node) && node.ContainsNode(x)) + && APowerOfTheCandidateIsLeftIn(inTermsOfU, u, x, unreadIsLeft: false)) + { + integrandInU = SimplifiedWithoutTheImaginaryUnit(inTermsOfU, expr); + if (integrandInU.ContainsNode(x) && WithTheCandidatesPowerLeftInX(integrandInU, u, uSub, x) is { } readAgain) + integrandInU = readAgain; + } if (u is Sinf or Cosf && integrandInU.ContainsNode(x)) firstPass[u] = integrandInU; } diff --git a/Sources/Tests/UnitTests/Calculus/ASubstitutionThatKeepsTheVariableIsNotSimplifiedIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/ASubstitutionThatKeepsTheVariableIsNotSimplifiedIntegralTest.cs new file mode 100644 index 000000000..ebaffca27 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/ASubstitutionThatKeepsTheVariableIsNotSimplifiedIntegralTest.cs @@ -0,0 +1,111 @@ +// +// 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 System.Threading; +using System.Threading.Tasks; +using AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Calculus +{ + /// + /// Rubi's rows that ran past the corpus's budget beneath the polynomial term, each of them in + /// one simplification of a rational function with symbols in it: a substitution candidate's + /// quotient that still varied with x, or a scaled integrand that was not homogeneous. Neither + /// can lose its x by a rewriting that keeps the value, and the search now reads that off two + /// points instead. tan(c + d x)^(11/2)/(a + b tan(c + d x))^3 took thirty-five seconds + /// and the other two did not return in two minutes. + /// #718 + /// + [Trait("Area", "Calculus")] + public sealed class ASubstitutionThatKeepsTheVariableIsNotSimplifiedIntegralTest + { + private static Entity IntegrateWithinTheGuard(string integrand) + { + Entity? answer = null; + // A thread of its own, so that the guard is not spent waiting behind other tests' work, + // and a wall clock that only a search which does not end can reach. + var task = Task.Factory.StartNew(() => answer = integrand.ToEntity().Integrate("x"), + CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); + Assert.True(task.Wait(LimitTermination.Guard), $"{integrand} did not finish within {LimitTermination.Guard}"); + return answer!; + } + + [Theory] + // Rubi 4.3.2.1:690, under the tangent and then the root of it. + [InlineData("tan(c + d*x)^(11/2)/(a + b*tan(c + d*x))^3", new[] { 0.1, 0.3, 0.5, 0.7, 0.9, 1.1 })] + // Rubi 4.3.2.1:349, in the imaginary sum, which the scaling read twice before declining. + [InlineData("tan(c + d*x)^4/(a + i*a*tan(c + d*x))^(4/3)", new[] { -0.9, -0.4, 0.2, 0.6, 1.1, 1.7 })] + // Rubi 4.1.2.3:77, under the half angle, where the sine is positive. + [InlineData("1/((c + d*sin(k + f*x))*sqrt(g*sin(k + f*x))*sqrt(a + a*sin(k + f*x)))", new[] { 0.1, 0.3, 0.5, 0.7, 0.9, 1.1 })] + // Rubi 1.2.3.2:854, in the shifted linear and then its square: what is left there is the + // square written with the shift in it, which is the candidate once its terms are collected. + [InlineData("1/((d + g*x)*(a + b*(d + g*x)^2 + c*(d + g*x)^4)^3)", new[] { 0.1, 0.3, 0.5, 0.7, 0.9, 1.1 })] + // Rubi 1.1.3.2:3483, in the shifted linear and then its cube: the shift's `d^0` is + // `1 provided not d = 0` inner-simplified, and the condition comes off what is kept. + [InlineData("1/((c + d*x)*(a + b*(c + d*x)^3))", new[] { 0.1, 0.3, 0.5, 0.7, 0.9, 1.1 })] + // Rubi 2.3:805, under `u = x^x`, whose kept quotient holds an `x/x` the inner simplification + // leaves: what is left is a power of the candidate, and so it is simplified after all. + [InlineData("e^(x^x)*x^(2*x)*(1 + ln(x))", new[] { 0.1, 0.3, 0.5, 0.7, 0.9, 1.1 })] + public void IsAnswered(string integrand, double[] points) + { + var integral = IntegrateWithinTheGuard(integrand); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Entity Pinned(Entity e) => e.Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 0.4) + .Substitute("d", 0.9).Substitute("f", 0.8).Substitute("g", 1.7).Substitute("k", 0.2); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in points) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.IsNaN || got.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}"); + } + + /// + /// Rubi 2.3's F^(k sqrt(1 - a x)/sqrt(1 + a x))/(1 - a^2 x^2), under the exponent: what + /// it leaves in x is a power of the candidate, read off the simplified quotient, and so that + /// one is still simplified. The answer, -Ei(ln(F) u)/a, has no value at k = 0, + /// where the candidate is constant and the integrand is 1/(1 - a^2 x^2), and it says so. + /// + [Fact] + public void KeepsTheConditionWhereTheCandidateVaries() + { + const string integrand = "F^(k*sqrt(1 - a*x)/sqrt(1 + a*x))/(1 - a^2*x^2)"; + var integral = IntegrateWithinTheGuard(integrand); + var text = integral.Stringize(); + Assert.DoesNotContain("integral(", text); + Assert.Contains("provided not k = 0", text); + Entity Pinned(Entity e) => e.Substitute("F", 1.7).Substitute("k", 0.6).Substitute("a", 1.3); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var at in new[] { -0.6, -0.4, -0.2, 0.1, 0.3, 0.5 }) + { + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.IsNaN || got.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}"); + } + } +}