namespace AIStudio.Tools.Mathematics;
///
/// Integrates a function numerically by double exponential quadrature, also over infinite intervals.
///
///
/// A substitution moves the points of a trapezoidal sum doubly exponentially close to the bounds of
/// the interval, or out to infinity: tanh-sinh on [a, b], exp-sinh on [a, oo) and (-oo, b], and
/// sinh-sinh on the whole line. Integrands which are analytic inside the interval converge to about
/// fourteen digits within a few hundred points, singularities at the bounds included, such as ln(x) or
/// 1/sqrt(x) at 0. The points never lie on a bound, where an integrand such as x^3/(e^x - 1) has no
/// value.
/// Each level halves the step, and the difference between two levels estimates the error. The terms
/// left out at the ends add to it whenever the sum had to stop before they became negligible: at the
/// largest parameter, at a bound which a double cannot tell apart from its neighbors, or where the
/// integrand has no value. That is what keeps an integral like sin(x)/x over [0, oo), which the
/// substitution cannot settle, from converging by chance. A singularity at a bound other than 0 is
/// reached only to about seven digits, since a double near such a bound carries few digits of its
/// distance to it; the estimated error says so.
/// The integrand is a double function, compiled by AngouriMath, so the result carries at most the
/// digits of a double.
///
public static class DoubleExponentialQuadrature
{
///
/// The finest step is 2^-MAX_LEVEL.
///
private const int MAX_LEVEL = 10;
///
/// The coarsest step which may end the integration: a coarser sum can match the next one by chance.
///
private const int MIN_CONVERGED_LEVEL = 3;
///
/// Two levels which differ by less than this, relative to the integral, end the refinement.
///
private const double RELATIVE_TOLERANCE = 1e-13;
///
/// The largest estimated relative error of a converged result: six digits at least.
///
private const double ACCEPTABLE_RELATIVE_ERROR = 1e-6;
///
/// A term this much smaller than the sum so far no longer counts.
///
private const double NEGLIGIBLE = 1e-18;
///
/// How many negligible terms in a row end one side of a sum.
///
private const int NEGLIGIBLE_IN_A_ROW = 3;
///
/// The largest parameter on a finite interval: there, the points lie within 1e-37 of the bounds.
///
private const double FINITE_MAX_PARAMETER = 4.0;
///
/// The largest parameter towards infinity: there, the points lie beyond 1e50.
///
private const double INFINITE_MAX_PARAMETER = 5.0;
///
/// How much more the terms left out at an end may add up to than estimated. The last terms before
/// a bound are also the least accurate ones: 1 - x carries few digits for an x close to 1, which a
/// singularity such as 1/sqrt(1 - x) magnifies.
///
private const double TAIL_SAFETY_FACTOR = 10;
private const int EVALUATIONS_PER_CANCELLATION_CHECK = 256;
private const double HALF_PI = Math.PI / 2;
///
/// Integrates a function over an interval.
///
/// The function, which returns NaN or an infinity where it has no value.
/// The lower bound, which may be negative infinity.
/// The upper bound, which may be positive infinity; below the lower bound, the integral changes its sign.
/// The cancellation token.
/// The result, with its estimated error and how the integration ended.
public static QuadratureResult Integrate(Func integrand, double lower, double upper, CancellationToken token = default)
{
if (double.IsNaN(lower) || double.IsNaN(upper))
throw new ArgumentException("A bound of the integral is not a number.");
if (lower.Equals(upper))
return new(0, 0, QuadratureStatus.CONVERGED, null, 0);
if (lower > upper)
{
var reversed = Integrate(integrand, upper, lower, token);
return reversed with { Value = -reversed.Value };
}
var interval = new Interval(lower, upper);
var evaluations = 0;
var previous = double.NaN;
LevelSum sum = default;
for (var level = 0; level <= MAX_LEVEL; level++)
{
sum = Sum(integrand, interval, Math.ScaleB(1.0, -level), ref evaluations, token);
if (sum.NoValueAt is { } point)
return new(double.NaN, double.PositiveInfinity, QuadratureStatus.NO_VALUE, point, evaluations);
var difference = Math.Abs(sum.Integral - previous);
var scale = Math.Max(Math.Abs(sum.Integral), sum.AbsoluteIntegral);
var settled = level >= MIN_CONVERGED_LEVEL && difference <= RELATIVE_TOLERANCE * scale;
if (settled || level == MAX_LEVEL)
return Result(sum, difference + sum.TailError, scale, evaluations);
previous = sum.Integral;
}
return Result(sum, double.PositiveInfinity, 0, evaluations);
}
private static QuadratureResult Result(LevelSum sum, double error, double scale, int evaluations)
{
if (!double.IsFinite(sum.Integral) || double.IsNaN(error))
return new(sum.Integral, double.PositiveInfinity, QuadratureStatus.NOT_CONVERGED, null, evaluations);
var status = error <= ACCEPTABLE_RELATIVE_ERROR * scale ? QuadratureStatus.CONVERGED : QuadratureStatus.NOT_CONVERGED;
return new(sum.Integral, error, status, null, evaluations);
}
///
/// The trapezoidal sum of one level, from the middle of the parameter range outwards on both sides.
///
private static LevelSum Sum(Func integrand, Interval interval, double step, ref int evaluations, CancellationToken token)
{
var center = interval.NodeAt(0);
var centerValue = integrand(center.X);
evaluations++;
if (!double.IsFinite(centerValue))
return new(0, 0, 0, center.X);
var sum = center.Weight * centerValue;
var absoluteSum = Math.Abs(sum);
var tail = 0.0;
foreach (var direction in (ReadOnlySpan)[1, -1])
{
// Unknown before the first term of this side, so a missing value there is never negligible:
var lastTerm = double.PositiveInfinity;
var lastParameter = 0.0;
var negligibleInARow = 0;
for (var index = 1; ; index++)
{
var parameter = direction * index * step;
if (Math.Abs(parameter) > interval.MaxParameter)
{
tail += Tail(lastTerm, lastParameter, step);
break;
}
var node = interval.NodeAt(parameter);
if (node.AtBound)
{
tail += Tail(lastTerm, lastParameter, step);
break;
}
if (++evaluations % EVALUATIONS_PER_CANCELLATION_CHECK == 0)
token.ThrowIfCancellationRequested();
var value = integrand(node.X);
if (!double.IsFinite(value))
{
// Near a bound, an integrand such as x^3/(e^x - 1) loses its value to rounding, where its terms no longer count:
if (Math.Abs(lastTerm) <= NEGLIGIBLE * Math.Abs(sum))
{
tail += Tail(lastTerm, lastParameter, step);
break;
}
return new(0, 0, 0, node.X);
}
var term = node.Weight * value;
sum += term;
absoluteSum += Math.Abs(term);
lastTerm = term;
lastParameter = parameter;
if (Math.Abs(term) <= NEGLIGIBLE * Math.Abs(sum) && sum != 0)
{
if (++negligibleInARow >= NEGLIGIBLE_IN_A_ROW)
break;
}
else
negligibleInARow = 0;
}
}
return new(step * sum, step * tail, step * absoluteSum, null);
}
///
/// What the terms beyond the last one of a side may add up to, before the sum is multiplied by the
/// step: as much as the last term, or the integral of terms which decay from it at the pace of the
/// substitution, whichever is more, times TAIL_SAFETY_FACTOR.
///
private static double Tail(double lastTerm, double lastParameter, double step) => TAIL_SAFETY_FACTOR * Math.Abs(lastTerm) * Math.Max(1, 1 / (HALF_PI * Math.Cosh(lastParameter) * step));
///
/// The sum of one level: the integral, the estimated error of what its sides left out, and the
/// integral of the magnitude, which scales the tolerance of an integral close to zero.
///
private readonly record struct LevelSum(double Integral, double TailError, double AbsoluteIntegral, double? NoValueAt);
///
/// A point of the sum and its weight; at a bound when a double cannot tell the point from the bound.
///
private readonly record struct QuadratureNode(double X, double Weight, bool AtBound);
///
/// The substitution which fits the bounds.
///
private readonly record struct Interval(double Lower, double Upper)
{
public double MaxParameter => double.IsFinite(this.Lower) && double.IsFinite(this.Upper) ? FINITE_MAX_PARAMETER : INFINITE_MAX_PARAMETER;
public QuadratureNode NodeAt(double parameter)
{
var inner = HALF_PI * Math.Sinh(parameter);
var derivative = HALF_PI * Math.Cosh(parameter);
if (double.IsFinite(this.Lower) && double.IsFinite(this.Upper))
{
// tanh-sinh, with the distance to the nearer bound computed directly: 1 - tanh(u) = 2 / (e^(2u) + 1).
var halfWidth = (this.Upper - this.Lower) / 2;
var distance = halfWidth * 2 / (Math.Exp(2 * Math.Abs(inner)) + 1);
var x = parameter >= 0 ? this.Upper - distance : this.Lower + distance;
var secant = 2 / (Math.Exp(inner) + Math.Exp(-inner));
var atBound = distance == 0 || x.Equals(parameter >= 0 ? this.Upper : this.Lower);
return new(x, halfWidth * derivative * secant * secant, atBound);
}
if (double.IsFinite(this.Lower))
{
// exp-sinh towards positive infinity:
var offset = Math.Exp(inner);
var x = this.Lower + offset;
return new(x, derivative * offset, !double.IsFinite(x) || x.Equals(this.Lower));
}
if (double.IsFinite(this.Upper))
{
// exp-sinh towards negative infinity, mirrored:
var offset = Math.Exp(inner);
var x = this.Upper - offset;
return new(x, derivative * offset, !double.IsFinite(x) || x.Equals(this.Upper));
}
// sinh-sinh over the whole line:
var point = Math.Sinh(inner);
return new(point, derivative * Math.Cosh(inner), !double.IsFinite(point));
}
}
}