Skip to content

Commit e043a06

Browse files
Integrate numerically by Gauss-Legendre, and give the digits it knows (#2063)
DefiniteIntegral was the mean of steps + 1 equally weighted samples, its bounds among them: a first-order rule, off in the third digit on x^2 over [0, 1] at the default 100 steps and printed to a hundred digits, and NaN for a convergent integral with a singular bound, sin(x)/x and ln(x) over [0, 1] among them. It is now the five-point Gauss-Legendre rule on steps panels, which never evaluates a bound and is exact on each panel for degree nine, run on the panels, twice as many and four times as many. Never evaluating a bound, the rule gives a divergent integral a finite value -- 1/x over [0, 1] came to about 10 -- so where the differences between the three do not shrink above the rounding, the integral is NaN. Where they shrink, the rest of their geometric series is added and is the error: for x^-0.9 the last difference is a fourteenth of it, and the value without it was 7 where it is 10. The rounding is measured against the integral of the modulus, so x^3 over [-1, 1] is 0 and not NaN. The value is rounded to the place of the error, at most fifteen digits, with the downcasting off so that -0.9999 does not print as -9999/10000. A singularity inside the segment is not seen, and the docs say so. The MCP server ran the old rule at 2000 and 4000 steps and reported the digits the two agreed on, and its self-test documented the first-order rule, which no longer reproduces. It now asks once and reports the digits of the value it gets, with half a unit in the last place for the error; its caveat says what the rule does at a bound, and its README lists the workarounds that remain. Fixes #1675. Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent e230dd7 commit e043a06

7 files changed

Lines changed: 221 additions & 89 deletions

File tree

‎BREAKING-CHANGES.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,26 @@ A not-equal written `<>` is read as `not (a = b)`, and printed that way.
4242

4343
## Unreleased — since 2.6.0
4444

45+
### A numerical definite integral is Gauss–Legendre, with the digits it knows
46+
47+
**Silent: wrong answers, and values where there was NaN.** `Entity.DefiniteIntegral(x, a, b, steps)` was the mean of
48+
`steps + 1` equally weighted samples, its bounds among them: a first-order rule, whose value on `x^2` over `[0, 1]` was
49+
off in the third digit at the default 100 steps and printed to a hundred digits. A convergent integral with a singular
50+
bound had no value. It is now the five-point Gauss–Legendre rule on `steps` panels, which never evaluates a bound, run
51+
on the panels, twice as many and four times as many. Where the differences between the three do not shrink, the
52+
integral diverges at a bound and is `NaN`. Otherwise the rest of their geometric series is added and taken as the
53+
error, and the value is rounded to its place, at most fifteen digits, a double's. A singularity inside the segment is
54+
not seen, and a symmetric divergent one comes to its principal value
55+
([#1675](https://github.com/asc-community/AngouriMath/issues/1675)).
56+
57+
| Input | Was (2.6.0) | Now |
58+
|---|---|---|
59+
| `"x^2".ToEntity().DefiniteIntegral("x", 0, 1)` | `0.334999999999999938…`, a hundred digits | `0.333333333333333` |
60+
| `"e^x".ToEntity().DefiniteIntegral("x", 0, 2)` | `6.409068937660210827…` | `6.38905609893066` |
61+
| `"sin(x)/x".ToEntity().DefiniteIntegral("x", 0, 1)` | `NaN` | `0.946083070367182` |
62+
| `"ln(x)".ToEntity().DefiniteIntegral("x", 0, 1)` | `NaN` | `-1.00000` |
63+
| `"1/x".ToEntity().DefiniteIntegral("x", 0, 1)` | `NaN` | `NaN` |
64+
4565
### An equality in an equation or a system is read as a difference equal to zero
4666

4767
**Silent: wrong answers.** `MathS.SolveEquation`, `Entity.SolveEquation` and `MathS.Equations` read each equation as an

‎Sources/AngouriMath/Functions/Continuous/Integration/Integration.Definition.cs‎

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,12 @@ private static Entity AtTheBound(Entity antiderivative, Variable x, Entity bound
139139
/// <param name="x">The variable to integrate over.</param>
140140
/// <param name="from">The lower bound.</param>
141141
/// <param name="to">The upper bound.</param>
142-
/// <param name="stepCount">How many steps to take; more is more accurate and slower.</param>
142+
/// <param name="stepCount">
143+
/// How many panels to cut the segment into, each integrated by the five-point Gauss–Legendre rule; more
144+
/// is more accurate and slower. Neither bound is evaluated, so an integrable singularity at one is
145+
/// allowed, and one that diverges is NaN. The value has the digits the rule agrees on as the panels are
146+
/// doubled, at most fifteen, a double's. A singularity inside the segment is not seen.
147+
/// </param>
143148
/// <returns>The value of the integral.</returns>
144149
/// <remarks>
145150
/// This is the instance method the obsolete <c>MathS.Compute.DefiniteIntegral</c>
@@ -156,7 +161,12 @@ public Number.Complex DefiniteIntegral(Variable x, Number.Complex from, Number.C
156161
/// <param name="x">The variable to integrate over.</param>
157162
/// <param name="from">The lower bound.</param>
158163
/// <param name="to">The upper bound.</param>
159-
/// <param name="stepCount">How many steps to take; more is more accurate and slower.</param>
164+
/// <param name="stepCount">
165+
/// How many panels to cut the segment into, each integrated by the five-point Gauss–Legendre rule; more
166+
/// is more accurate and slower. Neither bound is evaluated, so an integrable singularity at one is
167+
/// allowed, and one that diverges is NaN. The value has the digits the rule agrees on as the panels are
168+
/// doubled, at most fifteen, a double's. A singularity inside the segment is not seen.
169+
/// </param>
160170
/// <returns>The value of the integral.</returns>
161171
public Number.Complex DefiniteIntegral(Variable x, EDecimal from, EDecimal to, int stepCount = 100)
162172
=> Integration.IntegrateNumerically(this, x,
@@ -1825,26 +1835,106 @@ internal static bool ASumOverRootsWouldAnswer
18251835
return null;
18261836
}
18271837
/// <summary>
1828-
/// Returns the approximate numeric value of a definite integral of a function. Only works for one-variable functions.
1829-
/// Accuracy is limited to the number specified (default is 100).
1830-
/// See more at <see cref="Entity.DefiniteIntegral(Entity.Variable, EDecimal, EDecimal, int)"/>
1838+
/// The integral of a function of one variable along the segment from <paramref name="from"/> to
1839+
/// <paramref name="to"/>, by the five-point Gauss–Legendre rule on each of
1840+
/// <paramref name="panels"/> equal panels, in double precision. See
1841+
/// <see cref="Entity.DefiniteIntegral(Entity.Variable, EDecimal, EDecimal, int)"/>.
18311842
/// </summary>
1843+
/// <remarks>
1844+
/// It was the mean of <c>panels + 1</c> equally weighted samples, the endpoints among them: a first-order
1845+
/// rule, whose error on <c>x^2</c> over <c>[0, 1]</c> halved with each doubling, 1.67e-4 at 1000 steps,
1846+
/// and printed to a hundred digits. And a convergent integral with a singular endpoint had no value:
1847+
/// <c>sin(x)/x</c> and <c>ln(x)</c> over <c>[0, 1]</c> were NaN. Gauss–Legendre never evaluates an
1848+
/// endpoint, and the five-point rule is exact on each panel for a polynomial of degree nine. It is run on
1849+
/// the panels, twice as many and four times as many: where the differences do not shrink, the integral
1850+
/// diverges at a bound and is NaN, and otherwise the rest of their geometric series is added and is the
1851+
/// error, and the value is rounded to its place, fifteen digits at most. A singularity inside the
1852+
/// segment is not seen, and a symmetric divergent one, as in 1/x over [-1, 1], comes to its principal
1853+
/// value.
1854+
/// https://github.com/asc-community/AngouriMath/issues/1675
1855+
/// </remarks>
18321856
/// <param name="expr">Expression to integrate</param>
18331857
/// <param name="x">Variable to integrate over</param>
18341858
/// <param name="from">The complex lower bound for integrating</param>
18351859
/// <param name="to">The complex upper bound for integrating</param>
1836-
/// <param name="accuracy">Accuracy (for now, number of iterations)</param>
1837-
internal static Complex IntegrateNumerically(Entity expr, Entity.Variable x, Complex from, Complex to, int accuracy = 100)
1860+
/// <param name="panels">How many panels the segment is cut into, five evaluations each</param>
1861+
internal static Complex IntegrateNumerically(Entity expr, Entity.Variable x, Complex from, Complex to, int panels = 100)
18381862
{
1839-
System.Numerics.Complex res = 0;
1840-
var cfunc = expr.Compile(x);
1841-
for (int i = 0; i <= accuracy; i++)
1863+
var function = expr.Compile(x);
1864+
var start = from.ToNumerics();
1865+
var length = to.ToNumerics() - start;
1866+
panels = System.Math.Max(1, panels);
1867+
// The integral on so many panels, and the integral of the modulus, which the rounding is measured
1868+
// against: an integral that comes to zero, x^3 over [-1, 1], is a sum of terms that do not.
1869+
(System.Numerics.Complex Value, double OfTheModulus) OnPanels(int count)
18421870
{
1843-
var share = ((EDecimal)i) / accuracy;
1844-
var tmp = Complex.Create(from.RealPart.EDecimal * share + to.RealPart.EDecimal * (1 - share), from.ImaginaryPart.EDecimal * share + to.ImaginaryPart.EDecimal * (1 - share));
1845-
res += cfunc.Substitute(tmp.ToNumerics());
1871+
System.Numerics.Complex sum = 0;
1872+
var ofTheModulus = 0.0;
1873+
for (var i = 0; i < count; i++)
1874+
{
1875+
var centre = start + length * ((i + 0.5) / count);
1876+
for (var k = 0; k < GaussLegendreNodes.Length; k++)
1877+
{
1878+
var term = GaussLegendreWeights[k] * function.Substitute(centre + length * (GaussLegendreNodes[k] / (2.0 * count)));
1879+
sum += term;
1880+
ofTheModulus += System.Numerics.Complex.Abs(term);
1881+
}
1882+
}
1883+
var width = System.Numerics.Complex.Abs(length) / (2.0 * count);
1884+
return (sum * length / (2.0 * count), ofTheModulus * width);
18461885
}
1847-
return res.ToNumber() / (accuracy + 1) * (to - from);
1886+
// On the panels, twice as many and four times as many. Never evaluating a bound, the rule gives a
1887+
// divergent integral a finite value: 1/x over [0, 1] came to about 10. Where it converges the
1888+
// differences between the three shrink, by r = 2^-(1 - p) for an endpoint singularity like x^-p,
1889+
// p < 1; for 1/x they stay at ln(2), and they grow for anything worse. So differences that do not
1890+
// shrink, above the rounding, are NaN; and where they shrink, the rest of the geometric series,
1891+
// d r/(1 - r) for the last difference d, is both the correction and the error: for x^-0.9 the last
1892+
// difference is a fourteenth of the error, and the value without it was 7 where it is 10.
1893+
var (coarse, _) = OnPanels(panels);
1894+
var (middle, _) = OnPanels(2 * panels);
1895+
var (fine, ofTheModulus) = OnPanels(4 * panels);
1896+
if (double.IsNaN(fine.Real) || double.IsNaN(fine.Imaginary) || double.IsInfinity(fine.Real) || double.IsInfinity(fine.Imaginary))
1897+
return fine.ToNumber();
1898+
var rounding = 1e-14 * System.Math.Max(ofTheModulus, System.Numerics.Complex.Abs(fine));
1899+
if (rounding == 0)
1900+
return Integer.Zero;
1901+
var first = System.Numerics.Complex.Abs(middle - coarse);
1902+
var last = System.Numerics.Complex.Abs(fine - middle);
1903+
if (last > rounding && last >= 0.99 * first)
1904+
return Real.NaN;
1905+
var value = fine;
1906+
var error = System.Math.Max(last, rounding);
1907+
if (last > rounding && first > rounding)
1908+
{
1909+
var ratio = last / first;
1910+
var tail = (fine - middle) * (ratio / (1 - ratio));
1911+
value = fine + tail;
1912+
error = System.Math.Max(System.Numerics.Complex.Abs(tail), rounding);
1913+
}
1914+
// Then rounded to the place of that error, and to no more digits than a double holds: the value
1915+
// was the double's exact binary expansion, fifty digits of which most were not known.
1916+
var magnitude = System.Numerics.Complex.Abs(value);
1917+
var quantum = (int)System.Math.Floor(System.Math.Log10(error));
1918+
if (magnitude > 0)
1919+
quantum = System.Math.Max(quantum, (int)System.Math.Floor(System.Math.Log10(magnitude)) - MostDigitsOfADouble + 1);
1920+
// A part that rounds to zero is zero, which prints as 0 and not as -0E-15; and a value with no
1921+
// imaginary part is real.
1922+
EDecimal Rounded(double part)
1923+
=> EDecimal.FromDouble(part).Quantize(quantum, ERounding.HalfEven) is var rounded && rounded.IsZero ? EDecimal.Zero : rounded;
1924+
var (realPart, imaginaryPart) = (Rounded(value.Real), Rounded(value.Imaginary));
1925+
using var _ = MathS.Settings.DowncastingEnabled.Set(false);
1926+
return imaginaryPart.IsZero ? Real.Create(realPart) : Complex.Create(realPart, imaginaryPart);
18481927
}
1928+
1929+
private const int MostDigitsOfADouble = 15;
1930+
1931+
// The five-point Gauss–Legendre rule on [-1, 1]: the roots of the fifth Legendre polynomial,
1932+
// 0 and ±sqrt(5 ∓ 2 sqrt(10/7))/3, and their weights.
1933+
[ConstantField]
1934+
private static readonly double[] GaussLegendreNodes =
1935+
{ 0.0, -0.5384693101056831, 0.5384693101056831, -0.9061798459386640, 0.9061798459386640 };
1936+
[ConstantField]
1937+
private static readonly double[] GaussLegendreWeights =
1938+
{ 0.5688888888888889, 0.4786286704993665, 0.4786286704993665, 0.2369268850561891, 0.2369268850561891 };
18491939
}
18501940
}

‎Sources/MCP/AngouriMath.Mcp/Numeric.cs‎

Lines changed: 19 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -174,64 +174,36 @@ public static Entity Strip(Entity e) =>
174174
/// <summary>A quadrature result: the value, and how far it can be trusted.</summary>
175175
public readonly record struct Quadrature(double Value, double ErrorEstimate, int Digits);
176176

177-
// Step counts for the two runs. AngouriMath's rule is first order — measured, by
178-
// doubling: the error of `∫[0,1] e^(x^2)` halves at every doubling of the step count
179-
// rather than falling by four — so the gap between a run and its double IS the error of
180-
// the coarser one, to a constant. Two runs at these counts cost about a third of a
181-
// second together, which is inside the request budget; going further buys one digit per
182-
// quadrupling and is not worth the wall clock.
183-
private const int CoarseSteps = 2_000;
184-
private const int FineSteps = 4_000;
177+
// Panels for the library's rule: five evaluations each, on these, twice as many and four times
178+
// as many, which is seven thousand evaluations of a compiled function.
179+
private const int Panels = 200;
185180

186181
/// <summary>
187-
/// Numeric definite integration via `Entity.DefiniteIntegral`, run TWICE so that the
188-
/// answer arrives with an error estimate rather than as a hundred digits of false
189-
/// precision. https://github.com/asc-community/AngouriMath/issues/1675
182+
/// Numeric definite integration via `Entity.DefiniteIntegral`, with the digits it reports. The
183+
/// library's rule is five-point Gauss-Legendre, run on its panels, twice as many and four times
184+
/// as many, and its value is rounded to the place of the error that leaves, so the digits of the
185+
/// value it returns are the digits it knows: `∫[0,1] ln(x)` is -1.000000, six of them.
186+
/// https://github.com/asc-community/AngouriMath/issues/1675
190187
///
191-
/// That precision is the whole reason this is not a one-line call. The library returns
192-
/// an arbitrary-precision decimal whatever the step count, so `∫[0,1] e^(x^2)` prints as
193-
/// 1.4666222366253148581140206684127892598 when the true value is 1.46265174590718 — a
194-
/// figure that is wrong in its third decimal and reads as converged to thirty. Reporting
195-
/// only the digits two runs agree on is exactly the job this server exists to do.
196-
///
197-
/// Returns null when the integrand cannot be sampled: the rule evaluates at the
198-
/// endpoints, so a singularity at either — `∫[0,1] sin(x)/x`, `∫[0,1] ln(x)`, both of
199-
/// which converge — comes back NaN and must be declined rather than reported.
188+
/// Returns null where there is nothing to report: a NaN, which is how the library answers an
189+
/// integral that diverges at a bound, or a complex value for a real integrand over a real
190+
/// interval, which means the rule left the domain.
200191
/// </summary>
201192
public static Quadrature? TryDefiniteIntegral(Entity integrand, Variable variable,
202193
double from, double to)
203-
{
204-
var coarse = Sample(integrand, variable, from, to, CoarseSteps);
205-
var fine = Sample(integrand, variable, from, to, FineSteps);
206-
if (coarse is not { } c || fine is not { } f) return null;
207-
208-
var error = Math.Abs(f - c);
209-
210-
// Digits justified by the error, relative to the magnitude of the answer: an error
211-
// of 2e-4 on a value near 1.46 pins three decimals, and the fourth is noise. One
212-
// digit is dropped on purpose — the estimate is itself an estimate.
213-
var scale = Math.Max(Math.Abs(f), double.Epsilon);
214-
var digits = error <= 0
215-
? 15
216-
: (int)Math.Floor(-Math.Log10(error / scale)) - 1;
217-
digits = Math.Clamp(digits, 0, 15);
218-
219-
return new Quadrature(f, error, digits);
220-
}
221-
222-
private static double? Sample(Entity integrand, Variable variable,
223-
double from, double to, int steps)
224194
{
225195
try
226196
{
227-
var value = integrand.DefiniteIntegral(variable, from, to, steps);
228-
var real = value.RealPart.EDecimal.ToDouble();
197+
var value = integrand.DefiniteIntegral(variable, from, to, Panels);
198+
var real = value.RealPart.EDecimal;
229199
var imaginary = value.ImaginaryPart.EDecimal.ToDouble();
230-
if (double.IsNaN(real) || double.IsInfinity(real)) return null;
231-
// A real integrand integrated over a real interval that comes back complex means
232-
// the rule wandered off the domain; that is not an answer to report.
200+
if (real.IsNaN() || real.IsInfinity()) return null;
233201
if (double.IsNaN(imaginary) || Math.Abs(imaginary) > 1e-12) return null;
234-
return real;
202+
// A value the library rounded to zero has no significant digit to count, and was
203+
// rounded at the fifteenth of the integral of the modulus at most.
204+
var digits = real.IsZero ? 15 : Math.Min(15, real.Mantissa.Abs().ToString().Length);
205+
var error = 0.5 * Math.Pow(10, real.Exponent.ToInt32Checked());
206+
return new Quadrature(real.ToDouble(), error, digits);
235207
}
236208
catch
237209
{

‎Sources/MCP/AngouriMath.Mcp/SelfTest.cs‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,6 @@ private static readonly (string Name, Func<bool> StillBroken, string Documented)
7575
[
7676
("Simplify(sqrt(x^2))", () => Value("sqrt(x^2)") != "abs(x)",
7777
"left as written rather than reduced to abs(x)"),
78-
("DefiniteIntegral is a first-order rule", () =>
79-
{
80-
// Halving the step count roughly doubles the error iff the rule is first order.
81-
// https://github.com/asc-community/AngouriMath/issues/1675
82-
// Numeric.TryDefiniteIntegral's step counts, its error estimate and the caveat
83-
// am_integrate prints all assume that; a rule of higher order would make the
84-
// reported error bound far too pessimistic and the wording wrong.
85-
var e = "e^(x^2)".ToEntity();
86-
var v = MathS.Var("x");
87-
var coarse = e.DefiniteIntegral(v, 0, 1, 500).RealPart.EDecimal.ToDouble();
88-
var fine = e.DefiniteIntegral(v, 0, 1, 1000).RealPart.EDecimal.ToDouble();
89-
var finer = e.DefiniteIntegral(v, 0, 1, 2000).RealPart.EDecimal.ToDouble();
90-
var ratio = Math.Abs(fine - coarse) / Math.Abs(finer - fine);
91-
return ratio is > 1.7 and < 2.3;
92-
}, "error halves per doubling, so ~4 digits at 4000 steps; Simpson would give more"),
9378
];
9479

9580
/// <summary>Runs every check, writes what it found, and returns the exit code: 1 when an identity fails.</summary>

0 commit comments

Comments
 (0)