From 2a372b975e1b3e4d26846659ec9b732ac5ceafa7 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 22 Oct 2026 13:44:11 +0000 Subject: [PATCH 1/2] Compare numerically to half the evaluation's digits, where the comparison was exact AreEqualNumerically evaluated both sides at its check points and compared the values with !=. Two equal expressions computed by different routes differ in their last digits, so the derivative of x ln(x)'s antiderivative, 2 x (-1/4 + ln(x)/2) + 1/x * 2/4 * x^2, read as unequal to x ln(x) at -100 and 100. Two finite values now agree when they differ by no more than half the evaluation's digits, relative to the larger of them or to one; a value that is not finite agrees only with itself, and a NaN is still passed over. A difference of 1e-40 is still a difference. The limit solver's two-sided check goes through the same comparison. The MCP server keeps its own comparison, at positive reals and with an answer for "could not evaluate", and its comment no longer says the library's is exact; its README lists the workarounds that remain. Fixes #1674. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 18 ++++++++ .../Functions/ExpressionNumerical.Equality.cs | 21 ++++++++- Sources/MCP/AngouriMath.Mcp/Numeric.cs | 15 +++---- Sources/MCP/README.md | 3 +- .../AreEqualNumericallyToThePrecisionTest.cs | 45 +++++++++++++++++++ 5 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 Sources/Tests/UnitTests/Core/AreEqualNumericallyToThePrecisionTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index d50c7c089..e78716ca7 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -40,6 +40,24 @@ A not-equal written `<>` is read as `not (a = b)`, and printed that way. --- +## Unreleased — since 2.6.0 + +### Numerical equality is to half the evaluation's digits + +**Silent: wrong answers.** `MathS.UnsafeAndInternal.AreEqualNumerically` evaluated both sides at its check points and +compared the values exactly, so two equal expressions computed by different routes, which differ in their last digits, +read as unequal. Two values now agree when they differ by no more than half the evaluation's digits, relative to the +larger of them or to one: a difference of `1e-40` is still a difference at the default hundred digits. The limit +solver's check that a limit agrees from both sides goes through the same comparison +([#1674](https://github.com/asc-community/AngouriMath/issues/1674)). + +| Input | Was (2.6.0) | Now | +|---|---|---| +| `AreEqualNumerically("2 x (-1/4 + ln(x)/2) + 1/x * 2/4 * x^2", "x ln(x)")` | `false` | `true` | +| `AreEqualNumerically` of `x ln(x)` and its antiderivative's derivative | `false` | `true` | + +--- + ## 2.6.0 — since 2.5.0 ### A high half-odd power of a quadratic beside a power of another is reduced in exact rationals diff --git a/Sources/AngouriMath/Functions/ExpressionNumerical.Equality.cs b/Sources/AngouriMath/Functions/ExpressionNumerical.Equality.cs index 3c5e3898e..763faf99f 100644 --- a/Sources/AngouriMath/Functions/ExpressionNumerical.Equality.cs +++ b/Sources/AngouriMath/Functions/ExpressionNumerical.Equality.cs @@ -6,6 +6,7 @@ // using AngouriMath.Extensions; +using PeterO.Numbers; using static AngouriMath.Entity; namespace AngouriMath.Functions @@ -47,13 +48,31 @@ static int TrueRemainder(int left, int right) var evaled2 = expr2Subs.EvalNumerical(); // TODO: should we consider NaN = {} to be true? - if (evaled1 != evaled2 && evaled1 != MathS.NaN && evaled2 != MathS.NaN) + if (evaled1 != evaled2 && evaled1 != MathS.NaN && evaled2 != MathS.NaN && !AgreeToHalfThePrecision(evaled1, evaled2)) return false; } return true; } + /// + /// Whether two values differ by no more than half the evaluation's digits, relative to the larger + /// of them or to one. Two routes to one value differ in their last digits: the derivative of + /// x ln(x)'s antiderivative, 2 x (-1/4 + ln(x)/2) + 1/x * 2/4 * x^2, is x ln(x), + /// and compared exactly at -100 and 100 the two read as unequal. A value that is not finite agrees + /// only with itself. + /// https://github.com/asc-community/AngouriMath/issues/1674 + /// + private static bool AgreeToHalfThePrecision(Number first, Number second) + { + if (first is not Number.Complex a || second is not Number.Complex b || !a.IsFinite || !b.IsFinite) + return false; + var digits = MathS.Settings.DecimalPrecisionContext.Value.Precision; + var tolerance = EDecimal.FromString("1E-" + digits.Divide(2)); + var scale = EDecimal.Max(EDecimal.One, EDecimal.Max(a.Abs().EDecimal, b.Abs().EDecimal)); + return ((Number.Complex)(a - b)).Abs().EDecimal.CompareTo(scale.Multiply(tolerance)) <= 0; + } + /// /// Checks if two expressions are equivalent if /// does not give the diff --git a/Sources/MCP/AngouriMath.Mcp/Numeric.cs b/Sources/MCP/AngouriMath.Mcp/Numeric.cs index 7964bf784..e2bbc0727 100644 --- a/Sources/MCP/AngouriMath.Mcp/Numeric.cs +++ b/Sources/MCP/AngouriMath.Mcp/Numeric.cs @@ -13,16 +13,11 @@ namespace AngouriMath.Mcp; /// /// Tolerance-based numeric equality. /// -/// Why not just use MathS.UnsafeAndInternal.AreEqualNumerically? Because despite the name it -/// compares with EXACT equality — the implementation is `if (evaled1 != evaled2 ...) return -/// false` over arbitrary-precision decimals, with no tolerance at all. Any expression -/// involving a transcendental therefore fails: ln(x) computed via two different but -/// mathematically identical routes disagrees in the last digit and the check reports the -/// expressions as different. -/// -/// That is not a hypothetical. `∫ x*ln(x)` returns the correct antiderivative, which -/// differentiates back to the correct integrand, and AreEqualNumerically still says false. -/// So this compares with a relative tolerance instead. +/// Why not MathS.UnsafeAndInternal.AreEqualNumerically? It compares to half the evaluation's +/// digits since #1674, but at its own check points, negative and complex among them, where a +/// correct antiderivative with ln(x) or abs(x) in it can disagree with its integrand on a branch, +/// and it answers only true or false. This samples positive reals and answers null where no +/// point can be evaluated, which is not evidence either way. /// https://github.com/asc-community/AngouriMath/issues/1674 /// internal static class Numeric diff --git a/Sources/MCP/README.md b/Sources/MCP/README.md index 2a037db77..0e942c7f1 100644 --- a/Sources/MCP/README.md +++ b/Sources/MCP/README.md @@ -151,9 +151,8 @@ existing tool, then a prompt, and add a tool last. A tool's description is a rou documentation: say when to call it, and where a model would wrongly trust itself, say so. This is an adapter, not a second computer algebra system. Mathematics belongs in the library, where -the terminal and the notebooks get it too, and four workarounds here wait for it: +the terminal and the notebooks get it too, and three workarounds here wait for it: eigenvalues by the characteristic polynomial ([#1676](https://github.com/asc-community/AngouriMath/issues/1676)), -a numeric comparison with a tolerance ([#1674](https://github.com/asc-community/AngouriMath/issues/1674)), `a = b` rewritten to `a - b` for a system ([#1673](https://github.com/asc-community/AngouriMath/issues/1673)), and the numeric definite integral run twice to find its good digits ([#1675](https://github.com/asc-community/AngouriMath/issues/1675)). diff --git a/Sources/Tests/UnitTests/Core/AreEqualNumericallyToThePrecisionTest.cs b/Sources/Tests/UnitTests/Core/AreEqualNumericallyToThePrecisionTest.cs new file mode 100644 index 000000000..5563a8569 --- /dev/null +++ b/Sources/Tests/UnitTests/Core/AreEqualNumericallyToThePrecisionTest.cs @@ -0,0 +1,45 @@ +// +// 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 AngouriMath.Extensions; +using Xunit; + +namespace AngouriMath.Tests.Core +{ + /// + /// compares the values at + /// its check points to half the evaluation's digits, where it compared them exactly: two routes to + /// one value differ in their last digits, and the derivative of x ln(x)'s antiderivative read + /// as unequal to x ln(x) at -100 and 100. + /// #1674 + /// + [Trait("Area", "Core")] + public sealed class AreEqualNumericallyToThePrecisionTest + { + [Theory] + [InlineData("2 * x * (-1/4 + ln(x)/2) + 1/x * 2/4 * x^2", "x * ln(x)")] + [InlineData("sin(x)^2 + cos(x)^2", "1")] + [InlineData("e^(2 * ln(x))", "x^2")] + public void TwoRoutesToOneValueAreEqual(string one, string another) + => Assert.True(MathS.UnsafeAndInternal.AreEqualNumerically(one.ToEntity(), another.ToEntity())); + + [Fact] + public void AnAntiderivativeDifferentiatedBackIsItsIntegrand() + { + var integrand = "x * ln(x)".ToEntity(); + var back = integrand.Integrate("x").Substitute("C", 0).Differentiate("x"); + Assert.True(MathS.UnsafeAndInternal.AreEqualNumerically(back, integrand)); + } + + [Theory] + // Past the tolerance, half of a hundred digits: still unequal. + [InlineData("x", "x + 1/10^20")] + [InlineData("x^2", "x^2 + 1/10^40")] + public void ADifferenceAboveThePrecisionStillCounts(string one, string another) + => Assert.False(MathS.UnsafeAndInternal.AreEqualNumerically(one.ToEntity(), another.ToEntity())); + } +} From c28e7f07ac50b199f806b1d97fdb77b98e2bbcb5 Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 22 Oct 2026 14:31:01 +0000 Subject: [PATCH 2/2] Say where the MCP server's workarounds are written, not which they are The README listed the workarounds waiting for the library by issue, and every PR that fixed one edited the list, so the PRs fixing #1673-#1676 conflicted with each other one merge at a time. Each workaround links its issue in a comment beside it, which is where it is removed. Part of #1674. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- Sources/MCP/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/MCP/README.md b/Sources/MCP/README.md index 4c6e44dae..3da4fb593 100644 --- a/Sources/MCP/README.md +++ b/Sources/MCP/README.md @@ -151,7 +151,5 @@ existing tool, then a prompt, and add a tool last. A tool's description is a rou documentation: say when to call it, and where a model would wrongly trust itself, say so. This is an adapter, not a second computer algebra system. Mathematics belongs in the library, where -the terminal and the notebooks get it too, and two workarounds here wait for it: -eigenvalues by the characteristic polynomial ([#1676](https://github.com/asc-community/AngouriMath/issues/1676)), -and the numeric definite integral run twice to find its good digits -([#1675](https://github.com/asc-community/AngouriMath/issues/1675)). +the terminal and the notebooks get it too. Where the server has to work around the library, the +workaround links the library's issue in a comment beside it, and the fix for that issue removes it.