using System.Globalization; using AIStudio.Chat; using AngouriMath; using PeterO.Numbers; namespace AIStudio.Tools.Mathematics; /// /// Evaluates an expression without variables to a number, exactly where it can, and states how many /// of its digits are certain. /// /// /// A rational value stays exact, so 547 / 57576 comes back as that fraction. Every other value is /// evaluated twice, at ten and at twenty digits beyond those asked for, and only the digits both agree /// on are reported: AngouriMath rounds at every step, and a compound expression such as pi^4/15 loses /// its last digits at the precision asked for, see AngouriMathBehaviorTests.

/// A definite integral is integrated numerically, never by AngouriMath: its antiderivative may jump /// between the bounds, and Evaled turns the constants inside an integral it cannot take into decimals. /// Its value then carries the digits of a double at most. The two evaluations shift every integral /// by its estimated error, down in one and up in the other, so the digits they agree on are those the /// integrals vouch for, too.

/// A tiny value, such as one computed with the Planck constant, gets as many more digits as it has /// leading zeros, see EvaluateAt.

/// Call it inside a computation of MathComputationService. ///
public static class NumericEvaluator { private const int GUARD_DIGITS = 10; private const int MAX_SHOWN_LENGTH = 120; /// /// The most digits an evaluation computes with. A value which needs more, because it is smaller /// than about 1e-900, loses its digits. /// private const int MAX_PRECISION = 1_000; /// /// The precision of a bound of an integral, which ends up as a double. /// private const int BOUND_PRECISION = 30; /// /// Evaluates an expression to a number. /// /// The expression, read by MathExpressionReader. /// How many significant digits are asked for. /// The cancellation token of the computation. /// The value with its certain digits. /// The expression has no numeric value, or cannot be integrated. public static NumericValue Evaluate(Entity expression, int digits, CancellationToken token) { RefuseWhatIsNoNumber(expression); MathGuards.CheckExactSize(expression); var integrals = new List(); var issues = new List(); var withPlaceholders = expression.Replace(node => node is Entity.Integralf integral ? Integrate(integral, integrals, issues, token) : node); var exact = integrals.Count == 0 ? ExactValueOf(expression) : null; if (exact is not null && IsZero(exact) && MayBeTiny(expression, digits)) exact = null; if (exact is not null) return new(exact, DecimalOf(exact, digits + GUARD_DIGITS), digits, false, null, issues); var substituted = Substituted(withPlaceholders, integrals, 0); if (!substituted.EvaluableNumerical) throw new MathComputationException($"'{Show(expression)}' has no value the tool can compute as a single number, as for a sum without a closed form, an infinite sum, or a matrix."); var central = EvaluateAt(substituted, digits + GUARD_DIGITS); if (IsNaN(central)) throw new MathComputationException($"'{Show(expression)}' has no value: it evaluates to NaN, as for a division by zero, a pole, or the factorial of a negative integer."); if (IsInfinite(central)) { issues.Add("The value is infinite."); return new(null, central, 0, integrals.Count > 0, null, issues); } // // Where AngouriMath rounds to decimal places, a value far below 1 keeps only some of its digits, and a // tiny one none at all. The precision therefore grows by the leading zeros of the value, and a value of // zero is evaluated again with twice the digits, until MAX_PRECISION. More digits cannot change an // integral's value of zero, which comes from the quadrature: // var shift = 0; var maxShift = MAX_PRECISION - digits - 2 * GUARD_DIGITS; while (true) { int wantedShift; if (!IsZero(central)) wantedShift = LeadingZerosOf(central); else if (integrals.Count == 0) wantedShift = 2 * shift + digits + GUARD_DIGITS; else break; wantedShift = Math.Min(wantedShift, maxShift); if (wantedShift <= shift + GUARD_DIGITS) break; token.ThrowIfCancellationRequested(); shift = wantedShift; central = EvaluateAt(substituted, digits + GUARD_DIGITS + shift); } var low = integrals.Count == 0 ? central : EvaluateAt(Substituted(withPlaceholders, integrals, -1), digits + GUARD_DIGITS + shift); var high = EvaluateAt(Substituted(withPlaceholders, integrals, 1), digits + 2 * GUARD_DIGITS + shift); var certainDigits = MathFormatting.CountMatchingDigits(low, high, digits); double? estimatedError = integrals.Count == 0 ? null : MathFormatting.Distance(low, high) / 2; if (IsZero(central) && integrals.Count == 0) { certainDigits = 0; issues.Add(string.Create(CultureInfo.InvariantCulture, $"The value evaluates to 0 with {digits + GUARD_DIGITS + shift} digits, but the tool cannot show that it is exactly 0: a value too small for that many digits comes out as 0, too.")); } else if (certainDigits == 0) issues.Add(string.Create(CultureInfo.InvariantCulture, $"No digit of the value is certain: it lies within about {MathFormatting.FormatDecimal(MathFormatting.Distance(low, high), 2)} of the decimal.")); else if (certainDigits < digits) issues.Add(string.Create(CultureInfo.InvariantCulture, $"Only {certainDigits} of the {digits} significant digits asked for are certain, so the decimal has {certainDigits}.")); return new(null, central, certainDigits, integrals.Count > 0, estimatedError, issues); } private static void RefuseWhatIsNoNumber(Entity expression) { if (expression is Entity.Statement) throw new MathComputationException($"'{Show(expression)}' is a statement, such as a comparison, and has no numeric value. An equation with one '=' is checked by comparing its sides."); if (expression is Entity.Set) throw new MathComputationException($"'{Show(expression)}' is a set, and has no numeric value."); var variables = MathVariables.FreeIn(expression).Select(variable => variable.Name).ToList(); if (variables.Count > 0) throw new MathComputationException($"'{Show(expression)}' contains the variable{(variables.Count == 1 ? string.Empty : "s")} {string.Join(", ", variables)}, so it has no numeric value. compute_symbolically works with variables."); } /// /// The exact value of an expression when it is rational, or complex with rational parts. /// /// The expression. /// The exact value, or null for an expression without one. public static Entity.Number.Complex? ExactValueOf(Entity expression) => expression.InnerSimplified switch { Entity.Number.Rational rational => rational, Entity.Number.Complex { RealPart: Entity.Number.Rational, ImaginaryPart: Entity.Number.Rational } complex => complex, _ => null, }; /// /// Integrates a definite integral numerically, and puts a variable of its own in its place. /// private static Entity Integrate(Entity.Integralf integral, List integrals, List issues, CancellationToken token) { if (integral.Range is not { } range) throw new MathComputationException($"'{Show(integral)}' has no bounds, so it has no numeric value. compute_symbolically integrates without bounds."); var (from, to) = range; if (integral.Var is not Entity.Variable variable) throw new MathComputationException($"'{Show(integral)}' does not integrate over a variable."); if (MathVariables.FreeIn(integral.Expression).Any(free => free != variable) || MathVariables.HasAny(from) || MathVariables.HasAny(to)) throw new MathComputationException($"'{Show(integral)}' depends on the variable of an outer integral. Nested integrals are not integrated numerically."); var lower = BoundOf(from, integral); var upper = BoundOf(to, integral); Func integrand; try { integrand = integral.Expression.Compile(variable); } catch (Exception exception) when (exception is not OperationCanceledException) { throw new MathComputationException($"The integrand of '{Show(integral)}' cannot be evaluated numerically: {exception.Message.Shorten(MAX_SHOWN_LENGTH)}"); } var result = DoubleExponentialQuadrature.Integrate(integrand, lower, upper, token); switch (result.Status) { case QuadratureStatus.NO_VALUE: throw new MathComputationException(string.Create(CultureInfo.InvariantCulture, $"The integrand of '{Show(integral)}' has no finite value at {variable.Name} = {MathFormatting.FormatDecimal(result.FailurePoint ?? double.NaN, 6)}, inside the interval of integration.")); case QuadratureStatus.NOT_CONVERGED: throw new MathComputationException($"The numerical integration of '{Show(integral)}' did not settle. The integral may diverge, converge only conditionally such as sin(x)/x over [0, +oo), or have a singularity inside the interval, which a split at the singularity moves to a bound."); } // The expression has no free variables at this point, so no name can clash with one of them: var placeholder = MathS.Var($"Q_{integrals.Count + 1}"); integrals.Add(new(placeholder, result.Value, Math.Max(result.EstimatedError, Math.Abs(result.Value) * Math.ScaleB(1.0, -52)))); issues.Add(string.Create(CultureInfo.InvariantCulture, $"'{Show(integral)}' was integrated numerically, with an estimated error of {MathFormatting.FormatDecimal(result.EstimatedError, 2)}.")); return placeholder; } private static double BoundOf(Entity bound, Entity.Integralf integral) { var value = EvaluateAt(bound, BOUND_PRECISION); if (!value.ImaginaryPart.EDecimal.IsZero) throw new MathComputationException($"A bound of '{Show(integral)}' is not a real number."); var number = value.RealPart.EDecimal; if (number.IsNaN()) throw new MathComputationException($"A bound of '{Show(integral)}' has no value."); if (number.IsInfinity()) return number.IsNegative ? double.NegativeInfinity : double.PositiveInfinity; return number.ToDouble(); } /// /// The expression with every integral replaced by its value, shifted by its estimated error in the given direction. /// private static Entity Substituted(Entity expression, IReadOnlyList integrals, int direction) { // Downcasting would turn the value of a tiny integral into 0, see EvaluateAt: using var withoutDowncasting = MathS.Settings.DowncastingEnabled.Set(false); foreach (var integral in integrals) expression = expression.Substitute(integral.Placeholder, Entity.Number.Real.Create(EDecimal.FromDouble(integral.Value + direction * integral.Error))); return expression; } /// /// Evaluates an expression without variables to a number, with at least the given number of significant digits. /// /// /// By default, AngouriMath downcasts: it turns a number which looks like an integer at the precision into that /// integer, and so a tiny value such as 1.380649e-23 * 300 into 0, see AngouriMathBehaviorTests. This evaluation /// does without. Then AngouriMath rounds the quotient of two integers to as many decimal places as the precision /// has digits, so 21/10^50 vanishes at 30 digits. The precision therefore grows by the leading zeros of the /// smallest fraction in the expression, up to MAX_PRECISION. /// /// The expression, without variables. /// How many significant digits to compute with, at least. /// The value, which may be NaN or infinite. public static Entity.Number.Complex EvaluateAt(Entity expression, int precision) { precision = Math.Min(precision + LeadingZerosOfFractions(expression), MAX_PRECISION); using var context = MathS.Settings.DecimalPrecisionContext.Set(EContext.ForPrecision(precision)); using var withoutDowncasting = MathS.Settings.DowncastingEnabled.Set(false); return expression.EvalNumerical(); } /// /// Whether an exact 0 may stand for a tiny value instead. /// /// /// AngouriMath simplifies a value below about 1e-1000 to 0, e.g., pi/10^1500, see AngouriMathBehaviorTests. /// A fraction that small, such as 1e-2000, leaves the 0 in doubt. Otherwise, a true 0, as for sin(pi), /// evaluates to rounding noise which changes with the precision, but a tiny value, as for exp(-3000), keeps /// its digits. A quotient such as pi/10^1500 evaluates to 0 as well, since AngouriMath rounds it to decimal /// places; its 0 stands. /// private static bool MayBeTiny(Entity expression, int digits) { if (LeadingZerosOfFractions(expression) > MAX_PRECISION - digits - 2 * GUARD_DIGITS) return true; var value = EvaluateAt(expression, digits + GUARD_DIGITS); var check = EvaluateAt(expression, digits + 2 * GUARD_DIGITS); return !IsZero(value) && MathFormatting.CountMatchingDigits(value, check, GUARD_DIGITS) >= GUARD_DIGITS / 2; } /// /// How many zeros the smallest fraction of an expression has after its decimal point, e.g., 49 for 21/10^50. /// /// /// The fractions are those the expression writes, such as 1e-200, and those its simplification computes, such as /// 21/10^50 from 3/10^50 * 7. /// private static int LeadingZerosOfFractions(Entity expression) => expression.Nodes .Concat(expression.InnerSimplified.Nodes) .OfType() .SelectMany(number => new[] { number.RealPart, number.ImaginaryPart }) .OfType() .Select(fraction => fraction.ERational.Denominator.ToString().Length - fraction.ERational.Numerator.Abs().ToString().Length) .Append(0) .Max(); /// /// How many zeros a number has after its decimal point, e.g., 18 for 3.97e-19, and 0 for a number of at least 0.1. /// private static int LeadingZerosOf(Entity.Number.Complex number) { var real = number.RealPart.EDecimal; var imaginary = number.ImaginaryPart.EDecimal; var largest = real.Abs().CompareTo(imaginary.Abs()) >= 0 ? real : imaginary; if (largest.IsZero || largest.IsNaN() || largest.IsInfinity()) return 0; var exponentOfFirstDigit = largest.Exponent.ToInt64Checked() + largest.UnsignedMantissa.ToString().Length - 1; return (int)Math.Clamp(-exponentOfFirstDigit - 1, 0, MAX_PRECISION); } private static bool IsZero(Entity.Number.Complex number) => number.RealPart.EDecimal.IsZero && number.ImaginaryPart.EDecimal.IsZero; /// /// An exact value as a decimal with the given number of significant digits. /// /// /// AngouriMath converts a fraction to a fixed number of decimal places, so 1/10^200 would become 0, /// see AngouriMathBehaviorTests. /// private static Entity.Number.Complex DecimalOf(Entity.Number.Complex exact, int precision) { var context = EContext.ForPrecision(precision); using var withoutDowncasting = MathS.Settings.DowncastingEnabled.Set(false); return Entity.Number.Complex.Create(Entity.Number.Real.Create(DecimalOf(exact.RealPart, context)), Entity.Number.Real.Create(DecimalOf(exact.ImaginaryPart, context))); } private static EDecimal DecimalOf(Entity.Number.Real part, EContext context) => part is Entity.Number.Rational fraction ? fraction.ERational.ToEDecimal(context) : part.EDecimal; private static bool IsNaN(Entity.Number.Complex number) => number.RealPart.EDecimal.IsNaN() || number.ImaginaryPart.EDecimal.IsNaN(); private static bool IsInfinite(Entity.Number.Complex number) => number.RealPart.EDecimal.IsInfinity() || number.ImaginaryPart.EDecimal.IsInfinity(); private static string Show(Entity expression) => expression.ToString().Shorten(MAX_SHOWN_LENGTH); /// /// A definite integral, integrated numerically, and the variable which stands for it. /// private sealed record NumericIntegral(Entity.Variable Placeholder, double Value, double Error); }