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}"); + } + } +}