From 310f07f39e53787bff2bcb1d4aebd6aa6db830bb Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 22 Oct 2026 14:40:07 +0000 Subject: [PATCH 1/2] Read a computed value as a rational only within half the working digits Real.Create made a value the integer it was near within half the working digits, and the small rational it was near within a fixed 1e-16, whatever the precision. At two thousand digits ln(e^a + sqrt(2) e^(a/2) + 1) was exactly a, where it is a + 1.2e-67, so the derivative of a right antiderivative of 1/sqrt(a + i a sinh(c + d x)) at x = 227283/1250, built on the difference of two such logarithms, lost its imaginary part, and the corpus harness read the row as wrong. The continued fraction now stops within half the working digits, as the integer test does. A number from a float, a double or a decimal carries that type's digits, not the working precision's, and keeps the 1e-16: (Entity)(1m / 3) is still 1/3, and the LaTeX tests writing a cube root as x^(1m/3) still print one. The public FindRational keeps its tolerance too. The derivative is still wrong at 300 digits and NaN at 50 to 200: values below half the working digits are zero, products of nonzero values among them, and those are the next change. Part of #1944. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- BREAKING-CHANGES.md | 22 ++++++ .../Convenience/AngouriMathExtensions.cs | 8 +- Sources/AngouriMath/Convenience/MathS.cs | 4 +- .../Entity.Continuous.Complex.Definition.cs | 25 ++++-- .../Entity.Continuous.Number.Definition.cs | 8 +- .../Entity.Continuous.Rational.Definition.cs | 19 +++-- .../Entity.Continuous.Real.Definition.cs | 27 +++++-- .../Core/Entity/Entity.ImplicitOperators.cs | 8 +- .../Core/AValueKeepsTheDigitsItHasTest.cs | 79 +++++++++++++++++++ .../Core/HighPrecisionFunctionsTest.cs | 4 +- Sources/Tests/UnitTests/Core/Numeric.cs | 11 ++- 11 files changed, 178 insertions(+), 37 deletions(-) create mode 100644 Sources/Tests/UnitTests/Core/AValueKeepsTheDigitsItHasTest.cs diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index d50c7c089..97e2f1d03 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -40,6 +40,28 @@ A not-equal written `<>` is read as `not (a = b)`, and printed that way. --- +## Unreleased — since 2.6.0 + +### A computed value is a rational only within half the working digits + +**Silent: wrong answers.** A number an evaluation produced became the small rational it was near within a fixed +`1e-16`, whatever the precision, while it became the integer it was near only within half the working digits. So at two +thousand digits `ln(e^a + sqrt(2) e^(a/2) + 1)` was exactly `a`, where it is `a + 1.2e-67`, and the derivative of a right +antiderivative of `1/sqrt(a + i a sinh(c + d x))` at a large argument lost its imaginary part, so that a check by +evaluation read the antiderivative as wrong. A value is now that rational only within half the working digits, as for an +integer. A number from a `double`, a `float` or a `decimal` carries its type's digits, not the working precision's, and +is rounded as before: `(Entity)(1m / 3)` is still `1/3` ([#1944](https://github.com/asc-community/AngouriMath/issues/1944)). + +| Input, at the default hundred digits unless stated | Was (2.6.0) | Now | +|---|---|---| +| `"1/3 + 1e-30".EvalNumerical()` | `1/3` | `0.33333333333333333333333333333433…` | +| `"0.333333333333333333333".EvalNumerical()` | `1/3` | `0.333333333333333333333` | +| `"1.000000000000000000001".EvalNumerical()` | `1` | `1.000000000000000000001` | +| The derivative of the antiderivative of `1/sqrt(a + i*a*sinh(c + d*x))` at `a, c, d, x = 21/10, 19/10, 17/10, 227283/1250`, at 2000 digits | `2.0185464…E-68` | `2.0185464…E-68 - 2.0185464…E-68i` | +| `"1/3 + 1e-60".EvalNumerical()`, `"sin(pi)".EvalNumerical()`, `(Entity)(1.0 / 3)` | `1/3`, `0`, `1/3` | `1/3`, `0`, `1/3` | + +--- + ## 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/Convenience/AngouriMathExtensions.cs b/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs index dee989172..bff929ebf 100644 --- a/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs +++ b/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs @@ -619,7 +619,7 @@ public static Set Solve(this string expr, Variable var) /// 0.100000001490116119384765625 /// /// - public static Real ToNumber(this float value) => Real.Create(EDecimal.FromSingle(value)); + public static Real ToNumber(this float value) => Real.FromMachineNumber(EDecimal.FromSingle(value)); /// /// Converts an into an AM's understandable @@ -639,7 +639,7 @@ public static Set Solve(this string expr, Variable var) /// 1/2 /// /// - public static Real ToNumber(this double value) => Real.Create(EDecimal.FromDouble(value)); + public static Real ToNumber(this double value) => Real.FromMachineNumber(EDecimal.FromDouble(value)); /// /// Converts an into an AM's understandable @@ -659,7 +659,7 @@ public static Set Solve(this string expr, Variable var) /// 0.1000000000000000055511151231257827021181583404541015625 /// /// - public static Real ToNumber(this decimal value) => Real.Create(EDecimal.FromDecimal(value)); + public static Real ToNumber(this decimal value) => Real.FromMachineNumber(EDecimal.FromDecimal(value)); /// /// Converts PeterO's into an AM's understandable @@ -695,7 +695,7 @@ public static Set Solve(this string expr, Variable var) /// /// public static Complex ToNumber(this NumericsComplex complex) - => Complex.Create(EDecimal.FromDouble(complex.Real), EDecimal.FromDouble(complex.Imaginary)); + => Complex.FromMachineNumbers(EDecimal.FromDouble(complex.Real), EDecimal.FromDouble(complex.Imaginary)); /// /// Converts an into an AM's understandable diff --git a/Sources/AngouriMath/Convenience/MathS.cs b/Sources/AngouriMath/Convenience/MathS.cs index 02812987f..b4fe23014 100644 --- a/Sources/AngouriMath/Convenience/MathS.cs +++ b/Sources/AngouriMath/Convenience/MathS.cs @@ -4583,7 +4583,7 @@ public static class Numbers /// /// public static Complex Create(NumericsComplex value) => - Create(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); + Complex.FromMachineNumbers(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); /// Creates an instance of from a /// A value of type (signed 64-bit integer) @@ -4916,7 +4916,7 @@ public static Rational CreateRational(EInteger numerator, EInteger denominator) /// EDecimal i9 = ((Entity.Number.Real)FromString("1 + 5").EvalNumerical()).EDecimal; /// /// - public static Real Create(double value) => Real.Create(EDecimal.FromDouble(value)); + public static Real Create(double value) => Real.FromMachineNumber(EDecimal.FromDouble(value)); /// /// Creates an instance of from two s diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Complex.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Complex.Definition.cs index ed16435d0..1413254ec 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Complex.Definition.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Complex.Definition.cs @@ -80,18 +80,27 @@ public static Complex Create(Real real, Real imaginary) => /// /// Creates an instance of Complex /// - public static Complex Create(EDecimal real, EDecimal imaginary) + public static Complex Create(EDecimal real, EDecimal imaginary) => Create(real, imaginary, fromMachineNumbers: false); + + /// + /// A number from a pair of s, each part rounded as + /// rounds it. + /// + internal static Complex FromMachineNumbers(EDecimal real, EDecimal imaginary) => Create(real, imaginary, fromMachineNumbers: true); + + private static Complex Create(EDecimal real, EDecimal imaginary, bool fromMachineNumbers) { if (!MathS.Settings.DowncastingEnabled) return new Complex(real, imaginary); + Real Part(EDecimal part) => fromMachineNumbers ? Real.FromMachineNumber(part) : Real.Create(part); if (imaginary.IsZero) - return Real.Create(real); + return Part(real); if (real.IsNaN() || imaginary.IsNaN()) return Real.NaN; if (imaginary.IsFinite && imaginary.Abs().LessThan(MathS.Settings.DowncastingTolerance)) - return Real.Create(real); + return Part(real); else - return new Complex(Real.Create(real), Real.Create(imaginary)); + return new Complex(Part(real), Part(imaginary)); } /// @@ -293,7 +302,7 @@ public static implicit operator Complex(EDecimal value) /// as the value that literal actually holds and not as one tenth. /// public static implicit operator Complex(float value) - => Create(EDecimal.FromSingle(value), 0); + => FromMachineNumbers(EDecimal.FromSingle(value), EDecimal.Zero); /// /// The value as a with no imaginary part, read as the @@ -301,7 +310,7 @@ public static implicit operator Complex(float value) /// written as. /// public static implicit operator Complex(double value) - => Create(EDecimal.FromDouble(value), 0); + => FromMachineNumbers(EDecimal.FromDouble(value), EDecimal.Zero); /// /// The value as a with no imaginary part. A @@ -309,14 +318,14 @@ public static implicit operator Complex(double value) /// that were written. /// public static implicit operator Complex(decimal value) - => Create(EDecimal.FromDecimal(value), 0); + => FromMachineNumbers(EDecimal.FromDecimal(value), EDecimal.Zero); /// /// The .NET complex number as this one, through its two /// parts and their precision. /// public static implicit operator Complex(System.Numerics.Complex value) => - Create(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); + FromMachineNumbers(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); /// The pair read as real and imaginary parts. public static implicit operator Complex((int re, int im) v) => Create(v.re, v.im); diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs index c7d1a8c7b..adab9d7b3 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs @@ -89,26 +89,26 @@ public abstract partial record Number : ContinuousNode /// The value as a , read as the binary it /// is — so 0.1f becomes the value that literal holds, not one tenth. /// - public static implicit operator Number(float value) => Real.Create(EDecimal.FromSingle(value)); + public static implicit operator Number(float value) => Real.FromMachineNumber(EDecimal.FromSingle(value)); /// /// The value as a , read as the binary it /// is rather than as the decimal it was written as. /// - public static implicit operator Number(double value) => Real.Create(EDecimal.FromDouble(value)); + public static implicit operator Number(double value) => Real.FromMachineNumber(EDecimal.FromDouble(value)); /// /// The value as a . A is decimal /// already, so this one keeps the digits that were written. /// - public static implicit operator Number(decimal value) => Real.Create(EDecimal.FromDecimal(value)); + public static implicit operator Number(decimal value) => Real.FromMachineNumber(EDecimal.FromDecimal(value)); /// /// The .NET complex number as a , through its two /// parts and their precision. /// public static implicit operator Number(System.Numerics.Complex value) - => Complex.Create(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); + => Complex.FromMachineNumbers(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); /// The big integer as an , exactly and at any size. /// /// Read as the two's-complement bytes it is, which is what diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rational.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rational.Definition.cs index 727ba11d4..aaaf8c0c4 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rational.Definition.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rational.Definition.cs @@ -162,6 +162,13 @@ public static Rational Create(ERational value) /// if found, otherwise. /// public static Rational? FindRational(EDecimal num, int iterCount = int.MinValue) + => FindRationalWithin(num, MathS.Settings.PrecisionErrorZeroRange, iterCount); + + /// + /// , accepting a level of the continued fraction whose + /// remainder is below its integer part times . + /// + internal static Rational? FindRationalWithin(EDecimal num, EDecimal tolerance, int iterCount = int.MinValue) { if (iterCount is int.MinValue) iterCount = MathS.Settings.FloatToRationalIterCount; @@ -179,11 +186,11 @@ public static Rational Create(ERational value) // the exact one. if (!MayBeASmallRational(num, iterCount)) return null; - return FindRationalExactly(num, iterCount); + return FindRationalExactly(num, iterCount, tolerance); } /// The continued fraction in the decimal's own arithmetic, level by level. - internal static Rational? FindRationalExactly(EDecimal num, int iterCount) + internal static Rational? FindRationalExactly(EDecimal num, int iterCount, EDecimal tolerance) { if (iterCount <= 0) return null; @@ -200,12 +207,12 @@ public static Rational Create(ERational value) // 1e-20 came to parse as 0. Below the top, intPart is at least 1 (rest // was inverted to get here), so the test stays as permissive as it was. if (rest.Abs().LessThan(EDecimal.FromEInteger(intPart) - .Multiply(MathS.Settings.PrecisionErrorZeroRange, MathS.Settings.DecimalPrecisionContext))) + .Multiply(tolerance, MathS.Settings.DecimalPrecisionContext))) return Integer.Create(sign * intPart); else { var inv = CtxDivide(EDecimal.One, rest); - var rat = FindRationalExactly(inv, iterCount - 1); + var rat = FindRationalExactly(inv, iterCount - 1, tolerance); if (rat is null) return null; return new Rational((intPart * sign + sign / rat.ERational).InLowestTerms()); @@ -223,8 +230,8 @@ public static Rational Create(ERational value) /// /// /// The exact search accepts a level whose remainder is below the integer part - /// times , ten to the minus - /// sixteen. In a double the remainder at a level carries an error of the value's + /// times its tolerance, at most , + /// ten to the minus sixteen, unless a caller has set it higher. In a double the remainder at a level carries an error of the value's /// representation error, two to the minus fifty-two, times the square of that /// level's convergent denominator; the tolerance here is the exact one widened by /// that bound, so a value the exact search would accept is never refused, and diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs index 822c44781..30b3e9b95 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs @@ -58,7 +58,24 @@ private protected Real() : base(null, null) { } /// Creates an instance of Real /// (one can do it by implicit conversation) /// - public static Real Create(EDecimal value) + /// + /// A value is the small rational it is near within half the working digits, as it is the integer it + /// is near. Within a fixed 1e-16, ln(e^a + sqrt(2) e^(a/2) + 1) was exactly a at two + /// thousand digits, where it is a + 1.2e-67, and a derivative built on the difference of two + /// such logarithms lost its imaginary part. + /// https://github.com/asc-community/AngouriMath/issues/1944 + /// + public static Real Create(EDecimal value) => CreateWithRationalTolerance(value, MathS.Settings.DowncastingTolerance); + + /// + /// A number from a , a or a , which carries + /// that type's digits and not the working precision's: it is the small rational it is near within + /// , ten to the minus sixteen, so that 1m/3 + /// is 1/3. https://github.com/asc-community/AngouriMath/issues/1944 + /// + internal static Real FromMachineNumber(EDecimal value) => CreateWithRationalTolerance(value, MathS.Settings.PrecisionErrorZeroRange); + + private static Real CreateWithRationalTolerance(EDecimal value, EDecimal rationalTolerance) { if (!MathS.Settings.DowncastingEnabled) return new Real(value); @@ -79,7 +96,7 @@ public static Real Create(EDecimal value) if (intRest.GreaterThan(1 - tolerance)) return Integer.Create(intPart.Increment()); - var attempt = Rational.FindRational(value); + var attempt = Rational.FindRationalWithin(value, rationalTolerance); if (attempt is null || attempt.ERational.Numerator.Abs() > MathS.Settings.MaxAbsNumeratorOrDenominatorValue || attempt.ERational.Denominator.Abs() > MathS.Settings.MaxAbsNumeratorOrDenominatorValue) @@ -279,19 +296,19 @@ internal static bool TryParse(string s, /// The value as a , read as the binary /// it is — so 0.1f becomes the value that literal holds, not one tenth. /// - public static implicit operator Real(float value) => Create(EDecimal.FromSingle(value)); + public static implicit operator Real(float value) => FromMachineNumber(EDecimal.FromSingle(value)); /// /// The value as a , read as the binary /// it is rather than as the decimal it was written as. /// - public static implicit operator Real(double value) => Create(EDecimal.FromDouble(value)); + public static implicit operator Real(double value) => FromMachineNumber(EDecimal.FromDouble(value)); /// /// The value as a . A is decimal /// already, so this one keeps the digits that were written. /// - public static implicit operator Real(decimal value) => Create(EDecimal.FromDecimal(value)); + public static implicit operator Real(decimal value) => FromMachineNumber(EDecimal.FromDecimal(value)); } } } diff --git a/Sources/AngouriMath/Core/Entity/Entity.ImplicitOperators.cs b/Sources/AngouriMath/Core/Entity/Entity.ImplicitOperators.cs index d0c44e4b6..b9de52b31 100644 --- a/Sources/AngouriMath/Core/Entity/Entity.ImplicitOperators.cs +++ b/Sources/AngouriMath/Core/Entity/Entity.ImplicitOperators.cs @@ -58,26 +58,26 @@ partial record Entity /// and not one tenth. Write 0.1m, or a , for the exact /// number. /// - public static implicit operator Entity(float value) => Number.Real.Create(EDecimal.FromSingle(value)); + public static implicit operator Entity(float value) => Number.Real.FromMachineNumber(EDecimal.FromSingle(value)); /// /// The value as a literal, read as the binary /// it is rather than as the decimal it was written as. /// - public static implicit operator Entity(double value) => Number.Real.Create(EDecimal.FromDouble(value)); + public static implicit operator Entity(double value) => Number.Real.FromMachineNumber(EDecimal.FromDouble(value)); /// /// The value as a literal. A is /// decimal already, so this one keeps the digits that were written. /// - public static implicit operator Entity(decimal value) => Number.Real.Create(EDecimal.FromDecimal(value)); + public static implicit operator Entity(decimal value) => Number.Real.FromMachineNumber(EDecimal.FromDecimal(value)); /// /// The .NET complex number as a literal, through its two /// parts and their precision. /// public static implicit operator Entity(System.Numerics.Complex value) => - Number.Complex.Create(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); + Number.Complex.FromMachineNumbers(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); /// The big integer as an literal, exactly. public static implicit operator Entity(System.Numerics.BigInteger bigInt) diff --git a/Sources/Tests/UnitTests/Core/AValueKeepsTheDigitsItHasTest.cs b/Sources/Tests/UnitTests/Core/AValueKeepsTheDigitsItHasTest.cs new file mode 100644 index 000000000..0bd187928 --- /dev/null +++ b/Sources/Tests/UnitTests/Core/AValueKeepsTheDigitsItHasTest.cs @@ -0,0 +1,79 @@ +// +// 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 PeterO.Numbers; +using Xunit; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Tests.Core +{ + /// + /// A numerical evaluation rounds a value onto a small rational only within half the working digits, as it + /// rounds onto an integer, where it rounded within 1e-16 whatever the precision: at two thousand digits the + /// derivative of a right antiderivative of 1/sqrt(a + i a sinh(c + d x)) lost its imaginary part that + /// way. The residuals of exact cancellation are still exact, and so are numbers from a double or a decimal. + /// The reference is mpmath's at 120 digits. + /// #1944 + /// + [Trait("Area", "Core")] + public sealed class AValueKeepsTheDigitsItHasTest + { + private static void AssertClose(string reference, EDecimal actual) + { + var expected = EDecimal.FromString(reference); + var error = actual.Subtract(expected, EContext.ForPrecision(200)).Abs().Divide(expected.Abs(), EContext.ForPrecision(10)); + Assert.True(error.LessThan(EDecimal.FromString("1E-40")), $"{actual} where it is {expected}"); + } + + [Theory] + [InlineData("1/3 + 1e-30")] + [InlineData("0.333333333333333333333")] + public void AValueNearARationalIsNotThatRational(string expression) + => Assert.IsType(expression.EvalNumerical()); + + [Fact] + public void AMachineNumberIsTheRationalItIsNearToItsOwnDigits() + { + // A double or a decimal carries its type's digits, not the working precision's, and is read as + // before: within 1e-16 of a small rational, it is that rational. + Assert.Equal((Entity)Rational.Create(1, 3), (Entity)(1m / 3)); + Assert.Equal((Entity)Rational.Create(1, 3), (Entity)(1.0 / 3)); + Assert.Equal((Entity)Rational.Create(1, 4), (Entity)0.25); + } + + [Theory] + [InlineData("sin(pi)", "0")] + [InlineData("cos(pi/2)", "0")] + [InlineData("e^(i*pi) + 1", "0")] + [InlineData("sin(100*pi)", "0")] + [InlineData("sqrt(2)*sqrt(3) - sqrt(6)", "0")] + [InlineData("sin(pi/6)", "1/2")] + [InlineData("sqrt(2)^2/3", "2/3")] + [InlineData("gamma(1/2)^2/pi", "1")] + public void AResidualOfExactArithmeticIsStillExact(string expression, string exact) + => Assert.Equal(exact.ToEntity(), expression.EvalNumerical()); + + [Fact] + public void WithinHalfTheDigitsAValueIsTheRationalItIsNear() + => Assert.Equal(Rational.Create(1, 3), "1/3 + 1e-60".EvalNumerical()); + + [Fact] + public void TheDerivativeOfAnAntiderivativeAtALargeHyperbolicArgumentIsTheIntegrand() + { + const int digits = 2000; + var integrand = "1/sqrt(a + i*a*sinh(c + d*x))".ToEntity(); + var derivative = integrand.Integrate("x").Differentiate("x"); + Entity Pin(Entity e) => e.Substitute("a", Rational.Create(21, 10)).Substitute("c", Rational.Create(19, 10)) + .Substitute("d", Rational.Create(17, 10)).Substitute("x", Rational.Create(227283, 1250)); + using var _ = MathS.Settings.DecimalPrecisionContext.Set(new EContext(digits, ERounding.HalfEven, -100000, 100000, false)); + var value = Assert.IsType(Pin(derivative).EvalNumerical()); + AssertClose("2.01854640568606010538100888350636282143172892E-68", value.RealPart.EDecimal); + AssertClose("-2.01854640568606010538100888350636282143172892E-68", value.ImaginaryPart.EDecimal); + } + } +} diff --git a/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs b/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs index 5f1a7efad..71328d9bc 100644 --- a/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs +++ b/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs @@ -153,7 +153,7 @@ public void TheCheapDecisionAgreesWithTheExactSearch() var checkedCount = 0; void Check(EDecimal value) { - var exact = Rational.FindRationalExactly(value, iterCount); + var exact = Rational.FindRationalExactly(value, iterCount, MathS.Settings.PrecisionErrorZeroRange); var cheap = Rational.MayBeASmallRational(value, iterCount); var found = Rational.FindRational(value); checkedCount++; @@ -216,7 +216,7 @@ public void TheCheapDecisionReadsTwoThousandDigits() var denominator = 1 + random.Next(100_000_000); var numerator = random.Next(100_000_000); var value = EDecimal.FromInt32(numerator).Divide(EDecimal.FromInt32(denominator), context); - var exact = Rational.FindRationalExactly(value, iterCount); + var exact = Rational.FindRationalExactly(value, iterCount, MathS.Settings.PrecisionErrorZeroRange); if (exact is not null) Assert.True(Rational.MayBeASmallRational(value, iterCount)); Assert.Equal(exact, Rational.FindRational(value)); diff --git a/Sources/Tests/UnitTests/Core/Numeric.cs b/Sources/Tests/UnitTests/Core/Numeric.cs index 7710e0097..fd165c123 100644 --- a/Sources/Tests/UnitTests/Core/Numeric.cs +++ b/Sources/Tests/UnitTests/Core/Numeric.cs @@ -433,8 +433,10 @@ public void AComplexDivisionWithALargeDivisor(string ar, string ai, string br, s /// /// The modulus is made under the downcasting setting in force. It turned the downcasting on /// for itself, so with it off |75 + 316.22776601683796i|, which is 325 + 2.6e-17, - /// came back as 325. With it on, 325 is the setting's own rounding. + /// came back as 325. With it on, the setting rounds a value to half the working digits, which + /// 2.6e-17 is far above, so it is the same value; a modulus that is 325 to the working digits is 325. /// #1615 + /// #1944 /// [Fact] public void AModulusIsMadeUnderTheDowncastingInForce() @@ -449,7 +451,12 @@ public void AModulusIsMadeUnderTheDowncastingInForce() Assert.True(evaluated.RealPart.EDecimal.Subtract(expected).Abs().CompareTo(EDecimal.Create(1, -90)) < 0, $"{evaluated}"); } using (MathS.Settings.DowncastingEnabled.Set(true)) - Assert.Equal(Integer.Create(325), Complex.Create(EDecimal.FromInt32(75), EDecimal.FromString("316.22776601683796")).Abs()); + { + var modulus = Complex.Create(EDecimal.FromInt32(75), EDecimal.FromString("316.22776601683796")).Abs(); + Assert.True(modulus.EDecimal.Subtract(expected).Abs().CompareTo(EDecimal.Create(1, -90)) < 0, $"{modulus}"); + var rootOf100000 = EDecimal.FromInt32(100000).Sqrt(MathS.Settings.DecimalPrecisionContext); + Assert.Equal(Integer.Create(325), Complex.Create(EDecimal.FromInt32(75), rootOf100000).Abs()); + } } } } From 0362dd76c41c1878f1bdfeec4ffe7523308c87cf Mon Sep 17 00:00:00 2001 From: Rafael Vuijk Date: Thu, 22 Oct 2026 15:49:34 +0000 Subject: [PATCH 2/2] Make the rational tolerance only past the cheap check Passed as an argument, the half-digit tolerance was made on every Real.Create, where it had been made only past the check that almost every value stops at: EvalPolynomialFresh allocated 5.0% more and the kernel gate failed. The rational search now takes the integer test's tolerance, already made there. Part of #1944. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_012sonx8iAspMiwRwokT1Ura --- .../Continuous/Entity.Continuous.Real.Definition.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs index 30b3e9b95..ba6d381cb 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs @@ -65,7 +65,7 @@ private protected Real() : base(null, null) { } /// such logarithms lost its imaginary part. /// https://github.com/asc-community/AngouriMath/issues/1944 /// - public static Real Create(EDecimal value) => CreateWithRationalTolerance(value, MathS.Settings.DowncastingTolerance); + public static Real Create(EDecimal value) => Create(value, fromMachineNumber: false); /// /// A number from a , a or a , which carries @@ -73,9 +73,11 @@ private protected Real() : base(null, null) { } /// , ten to the minus sixteen, so that 1m/3 /// is 1/3. https://github.com/asc-community/AngouriMath/issues/1944 /// - internal static Real FromMachineNumber(EDecimal value) => CreateWithRationalTolerance(value, MathS.Settings.PrecisionErrorZeroRange); + internal static Real FromMachineNumber(EDecimal value) => Create(value, fromMachineNumber: true); - private static Real CreateWithRationalTolerance(EDecimal value, EDecimal rationalTolerance) + // The tolerance is made only past the cheap check below, which almost every value stops at: made at + // the call, it was 580 more bytes on each evaluation of a cubic at a hundred digits. + private static Real Create(EDecimal value, bool fromMachineNumber) { if (!MathS.Settings.DowncastingEnabled) return new Real(value); @@ -96,7 +98,7 @@ private static Real CreateWithRationalTolerance(EDecimal value, EDecimal rationa if (intRest.GreaterThan(1 - tolerance)) return Integer.Create(intPart.Increment()); - var attempt = Rational.FindRationalWithin(value, rationalTolerance); + var attempt = Rational.FindRationalWithin(value, fromMachineNumber ? MathS.Settings.PrecisionErrorZeroRange : tolerance); if (attempt is null || attempt.ERational.Numerator.Abs() > MathS.Settings.MaxAbsNumeratorOrDenominatorValue || attempt.ERational.Denominator.Abs() > MathS.Settings.MaxAbsNumeratorOrDenominatorValue)