diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md
index 9f1a36a05..5875d2c1c 100644
--- a/BREAKING-CHANGES.md
+++ b/BREAKING-CHANGES.md
@@ -58,6 +58,20 @@ as `a - b = 0`, as a statement's own solve reads it: `"x + 1 = 3".Solve("x")` wa
| `MathS.Equations("x + y = 3", "x - y = 1").Solve("x", "y")` | `null` | `[[2, 1]]` |
| `MathS.Equations("x + y = 3", "x - y = 1").ToString()` | `x + y = 3 = 0` and `x - y = 1 = 0` | `x + y - 3 = 0` and `x - y - 1 = 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
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 cfd5afc61..3da4fb593 100644
--- a/Sources/MCP/README.md
+++ b/Sources/MCP/README.md
@@ -151,8 +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 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)),
-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.
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()));
+ }
+}