From ac188e95515f603e7f23bab45b6034857e561ba8 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Wed, 21 Oct 2026 04:07:33 +0000 Subject: [PATCH] Answer the borderline rows inside the budget by four guards on slow declines - The pair rule SolveAPairOfHyperbolicPowersTwoApart is asked for sums just before the first sum split, which spent 9-12 s declining each elliptic term before the pair rule answered at its later position. - Exact pre-checks read before the expensive numerator in five readers: a whole power >= 2 of something in x below the bar, a root below the bar, an arctangent present. - SolveByCombiningRadicals counts the root inside the logarithm of an inverse hyperbolic function as no root, where it had rewritten ln(1/u + sqrt(1 + 1/u^2)) into a spelling nothing reads. - The substitution search's repeat test covers any function of one trigonometric function of a linear, a quadratic mirrored across its vertex, and even powers of a quotient of linears; its two values are compared in doubles, and nothing is refused where a double cannot hold them, and it is skipped past a power of x of 64 -- an exact subtraction of two 1,900-digit rationals had hung 1.1.4.2:487. Rubi's 7.6.2:23 goes from 34.8 s and 39.5 GB to 0.6 s, 6.6.1:24-26 and 6.1.1:123-125 from about 23 s to 0.1 s, and 7.2.4:599, 3.2.1:292, 4.7.7:117, 1.2.1.4:1120, 1.2.3.4:111 and 4.1.2.1:659 to 5-11 s. Every answer is unchanged, character for character, across 710 rows; over 300 rows of the 2-12 s band CPU falls 18.6% and allocation 19.0%. Part of #233. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Integration/IndefiniteIntegralSolver.cs | 224 +++++++++++++++--- .../Integration/Integration.Definition.cs | 8 + ...inesAheadOfTheAnsweringRuleIntegralTest.cs | 82 +++++++ 3 files changed, 281 insertions(+), 33 deletions(-) create mode 100644 Sources/Tests/UnitTests/Calculus/SlowDeclinesAheadOfTheAnsweringRuleIntegralTest.cs diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs index ebbefb911..646a8f7f5 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/IndefiniteIntegralSolver.cs @@ -5069,6 +5069,19 @@ bool IsAnExponential(Entity factor) return null; } + /// + /// Whether a factor of is a whole power past the first of + /// something in : what a rule for a power of a quadratic below the + /// bar needs, asked before the numerator is read. Under a substitution the numerator can + /// be a sum of exponentials brought over one bar, thousands of terms, and three such + /// rules expanded it in turn to be told it was not a polynomial -- six seconds each of + /// the inverse hyperbolic substitution for `x^4 (d + e x^2)^3 (a + b arcosh(c x))`, whose + /// denominator holds no x at all. https://github.com/asc-community/AngouriMath/issues/233 + /// + private static bool HasAWholePowerPastTheFirstOf(Entity denominator, Entity.Variable x) + => Mulf.LinearChildren(denominator).Any(factor => + factor is Powf(var @base, Number.Integer { EInteger: var whole }) && whole.CompareTo(EInteger.FromInt32(2)) >= 0 && @base.ContainsNode(x)); + /// /// Whether is built from by the four /// operations and rational powers alone: no function of anywhere. @@ -7788,9 +7801,10 @@ private static bool HasASquareRootOf(Entity expr, Entity.Variable x) internal static Entity? SolveXOverACubicBinomialBesideTheRootOfAnother(Entity expr, Entity.Variable x, bool integrateByParts) { var (numerator, denominator) = Functions.SingleQuotient.Of(expr); - if (!TreeAnalyzer.TryGetPolynomial(numerator, x, out var above) || above.Count != 1 - || !above.TryGetValue(EInteger.One, out var multiple) || multiple.ContainsNode(x)) - return null; + // The root below the bar read before the numerator, which is the expensive half: under + // a substitution a sum of exponentials over a common denominator has no root below it + // and a numerator of thousands of terms, and expanding that to be told it is not x took + // seconds of the inverse hyperbolic substitution for `x^4 (d + e x^2)^3 (a + b arcosh(c x))`. Entity? radicand = null; Entity below = Number.Integer.One; foreach (var factor in Mulf.LinearChildren(denominator)) @@ -7802,7 +7816,12 @@ private static bool HasASquareRootOf(Entity expr, Entity.Variable x) } else below = below == Number.Integer.One ? factor : below * factor; - if (radicand is null || ACubicBinomial(radicand) is not var (c, d) || ACubicBinomial(below) is not var (a, b)) + if (radicand is null) + return null; + if (!TreeAnalyzer.TryGetPolynomial(numerator, x, out var above) || above.Count != 1 + || !above.TryGetValue(EInteger.One, out var multiple) || multiple.ContainsNode(x)) + return null; + if (ACubicBinomial(radicand) is not var (c, d) || ACubicBinomial(below) is not var (a, b)) return null; static bool Zero(Entity condition) => Functions.PartialFractions.IsZeroAsAValue(condition); if (Zero(a) || Zero(b) || Zero(c) || Zero(d) || Zero(b * c - a * d)) @@ -23678,7 +23697,8 @@ Entity TakeOut(Entity node) if (!Integration.AnsweringTheQuestionAsked) return null; var (numerator, denominator) = Functions.SingleQuotient.Of(expr); - if (!TreeAnalyzer.TryGetPolynomial(numerator, x, out var top) || top.Keys.Any(k => k.Sign < 0 || !k.CanFitInInt32())) + if (!HasAWholePowerPastTheFirstOf(denominator, x) + || !TreeAnalyzer.TryGetPolynomial(numerator, x, out var top) || top.Keys.Any(k => k.Sign < 0 || !k.CanFitInInt32())) return null; Entity? quadratic = null; var power = 0; @@ -24595,6 +24615,8 @@ Piecewise piecewise when piecewise.Cases.Any() => piecewise.Cases.First().Expres if (!Integration.AnsweringTheQuestionAsked) return null; var (numerator, denominator) = Functions.SingleQuotient.Of(expr); + if (!HasAWholePowerPastTheFirstOf(denominator, x)) + return null; Entity? quadratic = null; var power = 0; Entity? linear = null; @@ -24718,6 +24740,8 @@ Piecewise piecewise when piecewise.Cases.Any() => piecewise.Cases.First().Expres internal static Entity? SolveAPowerOfALinearBesideTheDerivativeOfAQuadraticOverItsPower(Entity expr, Entity.Variable x, bool integrateByParts) { var (numerator, denominator) = Functions.SingleQuotient.Of(expr); + if (!HasAWholePowerPastTheFirstOf(denominator, x)) + return null; Entity? quadratic = null; var power = 0; Entity? linear = null; @@ -32798,11 +32822,19 @@ private static (int, int) SignOfTheTwoRoots(Entity node, List radicands) // sub-integrand of the chain. // A root of a quotient counts as the two roots it splits into -- a sum with a // quotient in it, `1/cos(x)^4 - 1`, being a quotient once combined. - if (expr.Nodes.Sum(node => node is Powf(var @base, Number.Rational half) - && half is not Number.Integer && half.ERational.Denominator.Equals(EInteger.FromInt32(2)) - && @base.ContainsNode(x) - ? (@base is Divf || Functions.SingleQuotient.Of(AsOneQuotient(@base)).Denominator != Number.Integer.One ? 2 : 1) - : 0) < 2) + // The root inside the logarithm an inverse hyperbolic function is written as counts as + // none: that logarithm is the inverse hyperbolic substitution's, which reads it as + // written, and combined, `ln(1/u + sqrt(1 + 1/u^2))` is `ln(1/u + sqrt(u^4 + u^2)/u^2)`, + // which nothing reads -- `u^7` beside it, from `x^3 arcsch(sqrt(x))`, was eighteen + // seconds of search to decline before the substitution answered it in a tenth of one. + // https://github.com/asc-community/AngouriMath/issues/233 + if (RootsToCombine(expr) < 2) + return null; + if (expr.Nodes.Any(node => node is Logf(_, var inside) && HasARadicalOf(inside, x)) + && RootsToCombine(expr.Replace(node => node is Logf(var logarithmBase, var antilogarithm) && logarithmBase == MathS.e + && antilogarithm.ContainsNode(x) && ReadAnInverseHyperbolic(antilogarithm, x) is not null + ? Number.Integer.One + : node)) < 2) return null; var rewritten = CombineRadicalsIn(expr, x); // The rewriting is exact and the rewritten integrand is the question asked in @@ -32832,6 +32864,13 @@ private static (int, int) SignOfTheTwoRoots(Entity node, List radicands) && !result.Nodes.Any(node => node == MathS.NaN)) return sign * result; return null; + + int RootsToCombine(Entity written) + => written.Nodes.Sum(node => node is Powf(var @base, Number.Rational half) + && half is not Number.Integer && half.ERational.Denominator.Equals(EInteger.FromInt32(2)) + && @base.ContainsNode(x) + ? (@base is Divf || Functions.SingleQuotient.Of(AsOneQuotient(@base)).Denominator != Number.Integer.One ? 2 : 1) + : 0); } /// @@ -36255,6 +36294,11 @@ Piecewise piecewise when piecewise.Cases.Any() => piecewise.Cases.First().Expres { if (!Integration.AnsweringTheQuestionAsked) return null; + // The arctangent the numerator needs is the integrand's: asked first, since the + // quotient below brings a sum of exponentials over one bar, every term's + // denominator multiplied into every other. + if (!expr.Nodes.Any(node => node is Arctanf && node.ContainsNode(x))) + return null; var (numerator, denominator) = Functions.SingleQuotient.Of(expr); Arctanf? arctangent = null; Entity? rate = null; @@ -37174,29 +37218,23 @@ void Add(Entity root, Entity times) /// /// Whether over takes two values at - /// two points where , a sine, cosine, tangent, cotangent, - /// secant or cosecant of a linear, takes one -- every other symbol pinned -- which no - /// function of the candidate does. False for any other candidate, and wherever a value - /// cannot be had or the candidate itself does not repeat there. + /// two points where takes one -- every other symbol pinned -- + /// which no function of the candidate does. False for a candidate + /// finds no such pair for, and wherever + /// a value cannot be had or the candidate itself does not repeat there. /// private static bool TheQuotientDisagreesWhereTheCandidateRepeats(Entity expr, Entity derivative, Entity candidate, Entity.Variable x) { - Entity? argument = candidate switch - { - Sinf(var a) => a, Cosf(var a) => a, Tanf(var a) => a, - Cotanf(var a) => a, Secantf(var a) => a, Cosecantf(var a) => a, - _ => null, - }; - if (argument is null || !TreeAnalyzer.TryGetPolyLinear(argument, x, out var slope, out var intercept) - || slope.Evaled is Number.Complex { IsZero: true }) + // Past a trigonometric function itself, only where the quotient is cheap to evaluate: a + // special function's value is a series, and beside `x^5 erf(b x)^2` the even powers of x + // the search offers were four hundred evaluations that refused nothing, a quarter more + // of the integrand's allocation. + if (candidate is not (Sinf or Cosf or Tanf or Cotanf or Secantf or Cosecantf) + && (expr.Nodes.Any(IsASpecialFunction) || PowerOfXAsWritten(expr, x) > LargestPowerEvaluatedForARepeat)) return false; Entity first = Number.Real.Create(EDecimal.FromString("0.37")); - Entity second = candidate switch - { - Sinf or Cosecantf => (MathS.pi - 2 * intercept) / slope - first, - Cosf or Secantf => -2 * intercept / slope - first, - _ => first + MathS.pi / slope, - }; + if (TheOtherPointWhereTheCandidateRepeats(candidate, x, first) is not { } second) + return false; var pinned = new Dictionary(); var values = new[] { "1.37", "0.61", "2.23", "1.91", "0.83", "1.13" }; var index = 0; @@ -37210,17 +37248,137 @@ private static bool TheQuotientDisagreesWhereTheCandidateRepeats(Entity expr, En value = value.Substitute(pair.Key, pair.Value); return value.Evaled is Number.Complex { IsNaN: false } number && number.IsFinite ? number : null; } - static bool Close(Number.Complex left, Number.Complex right, double tolerance) + // Compared in doubles, and not compared where a double does not hold the value. At a + // rational point the values are exact rationals, and for `x^24 (a x + b x^38)^12` under + // `u = x^24` each has two thousand digits: their difference, taken as numbers, ran for + // minutes, where the doubles say in a millisecond that they are opposite. + static bool? Close(Number.Complex left, Number.Complex right, double tolerance) { - var scale = System.Math.Max(1.0, ((Number.Real)left.Abs()).EDecimal.ToDouble()); - return ((Number.Real)(left - right).Abs()).EDecimal.ToDouble() <= tolerance * scale; + var (leftReal, leftImaginary) = (left.RealPart.EDecimal.ToDouble(), left.ImaginaryPart.EDecimal.ToDouble()); + var (rightReal, rightImaginary) = (right.RealPart.EDecimal.ToDouble(), right.ImaginaryPart.EDecimal.ToDouble()); + var scale = System.Math.Max(1.0, System.Math.Sqrt(leftReal * leftReal + leftImaginary * leftImaginary)); + var difference = System.Math.Sqrt((leftReal - rightReal) * (leftReal - rightReal) + (leftImaginary - rightImaginary) * (leftImaginary - rightImaginary)); + return !double.IsNaN(scale) && !double.IsInfinity(scale) && !double.IsNaN(difference) && !double.IsInfinity(difference) + ? difference <= tolerance * scale : null; } if (At(candidate, first) is not { } candidateFirst || At(candidate, second) is not { } candidateSecond - || !Close(candidateFirst, candidateSecond, 1e-9)) + || Close(candidateFirst, candidateSecond, 1e-9) is not true) return false; var quotient = expr / derivative; return At(quotient, first) is { } quotientFirst && At(quotient, second) is { } quotientSecond - && !Close(quotientFirst, quotientSecond, 1e-6); + && Close(quotientFirst, quotientSecond, 1e-6) is false; + } + + /// + /// The power of x past which does + /// not evaluate for a candidate other than a trigonometric function. The values at the two + /// points are exact, and their digits grow with the power: `x^36 (a + b x^37)^12` offers + /// `u = x^36`, and evaluating its quotient at the two points, for every expansion the search + /// asks, took the row from 68 ms to 296. + /// + private const long LargestPowerEvaluatedForARepeat = 64; + + /// + /// The largest power is raised to in as written: + /// the powers of a product added, of a sum the largest, a whole power multiplying its base's, + /// on either side of the bar; capped, so that it cannot overflow. + /// + private static long PowerOfXAsWritten(Entity expr, Entity.Variable x) + { + const long cap = 1L << 40; + long Of(Entity node) => System.Math.Min(cap, node switch + { + _ when !node.ContainsNode(x) => 0, + Variable => 1, + Powf(var @base, Number.Integer { EInteger: var power }) => Of(@base) * (power.Abs().CompareTo(EInteger.FromInt64(cap)) > 0 ? cap : power.Abs().ToInt64Checked()), + Mulf(var left, var right) => Of(left) + Of(right), + Divf(var left, var right) => Of(left) + Of(right), + _ => node.DirectChildren.Max(Of), + }); + return Of(expr); + } + + /// + /// A point other than where takes the + /// value it takes at , or null. A function of one sine, cosine, + /// tangent, cotangent, secant or cosecant of a linear, with nowhere + /// else, repeats where that function does: a sine at pi - theta, a cosine at + /// -theta, a tangent a period on. A quadratic repeats across its vertex, and a + /// constant times an even power of a quotient of linears, or of one linear, where the + /// quotient takes the opposite value. + /// + /// + /// The trigonometric function alone was read, and a multiple of one was not: + /// sin(x) tan(6x) written in the sine and cosine of x offers 2 cos(x), and + /// simplifying the integrand over its derivative to refuse it was eight seconds. The + /// quotient by the derivative of e ((c + d x)/(a + b x))^2 holds the quotient itself, + /// which the square does not determine -- nine seconds of Rubi's 3.2.1 + /// 1/((a g + b g x)^2 (A + B ln(e (c + d x)^2/(a + b x)^2))^2) -- and that over the + /// derivative of a + b x + c x^2 holds the linears of + /// (a + b x + c x^2)^(5/2)/((d + e x)(f + g x)), five more. + /// https://github.com/asc-community/AngouriMath/issues/233 + /// + private static Entity? TheOtherPointWhereTheCandidateRepeats(Entity candidate, Entity.Variable x, Entity first) + { + var trigonometric = candidate.Nodes + .Where(node => node is Sinf or Cosf or Tanf or Cotanf or Secantf or Cosecantf && node.ContainsNode(x)).Distinct().ToList(); + if (trigonometric.Count == 1) + { + var function = trigonometric[0]; + if (candidate.Replace(node => node == function ? Number.Integer.One : node).ContainsNode(x) + || !TreeAnalyzer.TryGetPolyLinear(function.DirectChildren.First(), x, out var slope, out var intercept) + || slope.Evaled is Number.Complex { IsZero: true }) + return null; + return function switch + { + Sinf or Cosecantf => (MathS.pi - 2 * intercept) / slope - first, + Cosf or Secantf => -2 * intercept / slope - first, + _ => first + MathS.pi / slope, + }; + } + if (trigonometric.Count > 1 || !IsARationalFunction(candidate, x)) + return null; + if (candidate is Sumf or Minusf && TreeAnalyzer.TryGetPolyQuadratic(candidate, x, out var leading, out var linear, out _) + && leading.Evaled is not Number.Complex { IsZero: true }) + return -linear / leading - first; + // k ((p + q x)/(r + s x))^n for an even n, or k (p + q x)^n: the quotient R takes -R(first) at + // -(p + R(first) r)/(q + R(first) s). + Entity? above = null, below = null; + EInteger? power = null; + foreach (var (factor, underneath) in FactorsOfTheIntegrand(candidate)) + { + if (!factor.ContainsNode(x)) + continue; + if (factor is not Powf(var @base, Number.Integer { EInteger: var raised }) || raised.IsZero) + return null; + var signed = underneath ? raised.Negate() : raised; + if (!signed.Abs().Remainder(EInteger.FromInt32(2)).IsZero || power is { } known && !known.Equals(signed.Negate())) + return null; + if (signed.Sign > 0 && above is null) + above = @base; + else if (signed.Sign < 0 && below is null) + below = @base; + else + return null; + power = signed; + } + if (power is null) + return null; + Entity p = Number.Integer.One, q = Number.Integer.Zero, r = Number.Integer.One, s = Number.Integer.Zero; + if (above is not null) + { + if (!TreeAnalyzer.TryGetPolyLinear(above, x, out var aboveSlope, out var aboveIntercept) || aboveSlope is null || aboveIntercept is null) + return null; + (q, p) = (aboveSlope, aboveIntercept); + } + if (below is not null) + { + if (!TreeAnalyzer.TryGetPolyLinear(below, x, out var belowSlope, out var belowIntercept) || belowSlope is null || belowIntercept is null) + return null; + (s, r) = (belowSlope, belowIntercept); + } + var atFirst = (above ?? Number.Integer.One).Substitute(x, first) / (below ?? Number.Integer.One).Substitute(x, first); + return -(p + atFirst * r) / (q + atFirst * s); } /// diff --git a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs index 84948298f..ac3141bc9 100644 --- a/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs +++ b/Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs @@ -1040,6 +1040,14 @@ private static Entity WithItsPiecewiseSumsCombined(Entity expr) // elementary: Welz's `x/((4 - x^3) sqrt(1 - x^3))`. if ((answer = IndefiniteIntegralSolver.SolveXOverACubicBinomialBesideTheRootOfAnother(expr, x, integrateByParts)) is { }) return answer; if ((answer = IndefiniteIntegralSolver.SolveByReducingThePolynomialOverALinearFactor(expr, x, integrateByParts)) is { }) return answer; + // Hyperbolic powers two apart whose coefficients kill the reduction's residual, asked + // again below, are a sum's before this split too: split, each elliptic term went + // through the chain alone, and `x/csch(x)^(3/2) + x sqrt(csch(x))/3` was declined + // here in ten seconds before the rule answered it in a tenth of one. Fractional + // powers only, which no term of the split answers apart. + if (expr is Entity.Sumf or Entity.Minusf + && (answer = IndefiniteIntegralSolver.SolveAPairOfHyperbolicPowersTwoApart(expr, x, integrateByParts)) is { }) + return answer; if (expr is Entity.Sumf or Entity.Minusf && (answer = IndefiniteIntegralSolver.SolveBySplittingSum(expr, x, integrateByParts)) is { }) return answer; diff --git a/Sources/Tests/UnitTests/Calculus/SlowDeclinesAheadOfTheAnsweringRuleIntegralTest.cs b/Sources/Tests/UnitTests/Calculus/SlowDeclinesAheadOfTheAnsweringRuleIntegralTest.cs new file mode 100644 index 000000000..bcd31fd85 --- /dev/null +++ b/Sources/Tests/UnitTests/Calculus/SlowDeclinesAheadOfTheAnsweringRuleIntegralTest.cs @@ -0,0 +1,82 @@ +// +// 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 +{ + /// + /// Integrands that were answered, but only after a rule ahead of the one that answers had + /// spent twenty to thirty seconds declining: a sum split into elliptic terms before the pair + /// rule was asked, the radical of an inverse hyperbolic function combined into a spelling + /// nothing reads, a numerator of thousands of terms expanded by three readers of quadratics + /// that wanted a power below the bar it did not have, and substitution candidates simplified + /// over their derivatives where two points already showed that no function of them was the + /// integrand. Rubi's 6.1.1, 6.6.1, 7.2.4, 7.6.2, 4.7.7 and 3.2.1, which the corpus counted + /// as solved only because its worker let a case run past its budget, and which timed out under + /// load. What is asserted is that each comes back, answered, with a wall far above the run. + /// #233 + /// + [Trait("Area", "Calculus")] + public sealed class SlowDeclinesAheadOfTheAnsweringRuleIntegralTest + { + [Theory] + [InlineData("x/csch(x)^(3/2) + 1/3*x*sqrt(csch(x))", 0.3)] + [InlineData("x/csch(x)^(5/2) + 3/5*x/sqrt(csch(x))", 0.3)] + [InlineData("x/csch(x)^(7/2) - 5/21*x*sqrt(csch(x))", 0.3)] + [InlineData("x^3*acsch(sqrt(x))", 0.3)] + [InlineData("x^4*(d + k*x^2)^3*(a + b*acosh(c*x))", 0.8)] + [InlineData("sin(x)*tan(6*x)", 0.05)] + [InlineData("1/((a*g + b*g*x)^2*(A + B*ln(k*(c + d*x)^2/(a + b*x)^2))^2)", 0.3)] + // Polynomials of high degree, whose quotient over the derivative of `x^24` or `x^36` takes + // values of two thousand digits at the two points the repeat test compares: Rubi's + // 1.1.4.2, under a second each, which with that test widened did not return in minutes. + [InlineData("x^24*(a*x + b*x^38)^12", 0.3)] + [InlineData("x^36*(a + b*x^37)^12", 0.3)] + public void IsAnswered(string integrand, double from) + { + var integral = Run(() => integrand.ToEntity().Integrate("x")); + Assert.DoesNotContain("integral(", integral.Stringize()); + Entity Pinned(Entity e) => e + .Substitute("a", 1.3).Substitute("b", 0.7).Substitute("c", 1.4).Substitute("d", 0.6) + .Substitute("g", 0.9).Substitute("k", 1.7).Substitute("A", 0.5).Substitute("B", 1.1); + var derivative = Pinned(integral.Substitute("C", 0)).Differentiate("x"); + var original = Pinned(integrand.ToEntity()); + var compared = 0; + foreach (var step in new[] { 0.0, 0.11, 0.23, 0.37, 0.52, 0.71, 0.93 }) + { + var at = from + step; + var want = original.Substitute("x", at).EvalNumerical(); + var got = derivative.Substitute("x", at).EvalNumerical(); + if (want.IsNaN) + continue; + compared++; + Assert.True(Math.Abs((double)(got - want).RealPart) + Math.Abs((double)(got - want).ImaginaryPart) + < 1e-7 * 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}"); + } + + /// + /// On a thread of its own, so that a test host whose pool is busy cannot hold it back, and + /// with a wall far above the run, so that the bound is reached only by a computation that + /// does not finish. + /// + private static Entity Run(Func work) + { + var task = Task.Factory.StartNew(work, CancellationToken.None, + TaskCreationOptions.LongRunning, TaskScheduler.Default); + Assert.True(task.Wait(TimeSpan.FromMinutes(3)), "did not return in three minutes"); + return task.Result; + } + } +}