Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 20 additions & 1 deletion Sources/AngouriMath/Functions/ExpressionNumerical.Equality.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

using AngouriMath.Extensions;
using PeterO.Numbers;
using static AngouriMath.Entity;

namespace AngouriMath.Functions
Expand Down Expand Up @@ -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;
}

/// <summary>
/// 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
/// <c>x ln(x)</c>'s antiderivative, <c>2 x (-1/4 + ln(x)/2) + 1/x * 2/4 * x^2</c>, is <c>x ln(x)</c>,
/// 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
/// </summary>
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;
}

/// <summary>
/// Checks if two expressions are equivalent if
/// <see cref="Entity.Simplify"/> does not give the
Expand Down
15 changes: 5 additions & 10 deletions Sources/MCP/AngouriMath.Mcp/Numeric.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,11 @@ namespace AngouriMath.Mcp;
/// <summary>
/// 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
/// </summary>
internal static class Numeric
Expand Down
7 changes: 2 additions & 5 deletions Sources/MCP/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// <see cref="MathS.UnsafeAndInternal.AreEqualNumerically(Entity, Entity)"/> 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 <c>x ln(x)</c>'s antiderivative read
/// as unequal to <c>x ln(x)</c> at -100 and 100.
/// <a href="https://github.com/asc-community/AngouriMath/issues/1674">#1674</a>
/// </summary>
[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()));
}
}
Loading