diff --git a/Sources/AngouriMath/Convenience/MathS.cs b/Sources/AngouriMath/Convenience/MathS.cs index 335458006..64ba04656 100644 --- a/Sources/AngouriMath/Convenience/MathS.cs +++ b/Sources/AngouriMath/Convenience/MathS.cs @@ -5912,7 +5912,8 @@ internal static EDecimal DowncastingTolerance /// /// Settings for precisions of PeterO.Numbers /// - public static Setting DecimalPrecisionContext { get; } = new EContext(100, ERounding.HalfUp, -100, 1000, false); + public static Setting DecimalPrecisionContext { get; } = + new(new EContext(100, ERounding.HalfUp, -100, 1000, false)) { AdvancesEvaluationEpoch = true }; /// /// Whether functions are being read as real-valued or complex-valued. It is a diff --git a/Sources/AngouriMath/Convenience/SettingClass.cs b/Sources/AngouriMath/Convenience/SettingClass.cs index a6ee5a1ca..62a1942b1 100644 --- a/Sources/AngouriMath/Convenience/SettingClass.cs +++ b/Sources/AngouriMath/Convenience/SettingClass.cs @@ -92,6 +92,23 @@ internal static bool StillHolds(object?[] snapshot) } } + /// + /// A count of the changes to what a numerical evaluation depends on -- the precision + /// context, set or its scope ended, on any thread. An records the + /// epoch its was computed at and recomputes when it has moved, + /// since a hundred digits of pi cached at one precision are the wrong answer at another. + /// One counter for every thread: a change on another thread costs a recomputation and + /// never a stale value. https://github.com/asc-community/AngouriMath/issues/1367 + /// + internal static class EvaluationEpoch + { + [ConcurrentField] private static int current; + + internal static int Current => Volatile.Read(ref current); + + internal static void Advance() => Interlocked.Increment(ref current); + } + /// /// This class for configuring some internal mechanisms from outside /// @@ -146,6 +163,12 @@ internal Setting(T defaultValue) SettingsState.Register(this); } + /// + /// Whether a change of this setting advances the : the + /// precision context does, since every cached evaluation depends on it. + /// + internal bool AdvancesEvaluationEpoch { get; init; } + /// /// The frame this setting currently reads from, as the identity of its state. Released /// in order — which using guarantees — a scope restores the very frame that was @@ -174,6 +197,8 @@ public AutoBackRollableTemporarySettingUnit Set(T value) { var id = Interlocked.Increment(ref lastId); frames.Value = new Frame(id, value, frames.Value); + if (AdvancesEvaluationEpoch) + EvaluationEpoch.Advance(); return new AutoBackRollableTemporarySettingUnit(this, id); } @@ -190,6 +215,8 @@ private void Remove(long id) var top = frames.Value; if (top is null) return; + if (AdvancesEvaluationEpoch) + EvaluationEpoch.Advance(); if (top.Id == id) { frames.Value = top.Next; diff --git a/Sources/AngouriMath/Core/Entity/Omni/Entity.Variable.cs b/Sources/AngouriMath/Core/Entity/Omni/Entity.Variable.cs index bef9f34dc..9a1c9916c 100644 --- a/Sources/AngouriMath/Core/Entity/Omni/Entity.Variable.cs +++ b/Sources/AngouriMath/Core/Entity/Omni/Entity.Variable.cs @@ -45,15 +45,18 @@ public void Deconstruct(out string name) protected override Entity[] InitDirectChildren() => Array.Empty(); /// - /// Which names the language reads as mathematical constants, and what each is worth. - /// This is the whole registry: a name is a constant exactly when it is a key here, - /// and nothing below asks about pi or e by name. + /// Which names the language reads as mathematical constants, and what each is worth + /// -- asked at the moment of evaluation, so that the digits are the current + /// precision's and not those of whichever context was set when this class was + /// initialised (https://github.com/asc-community/AngouriMath/issues/1367). This + /// is the whole registry: a name is a constant exactly when it is a key here, and + /// nothing below asks about pi or e by name. /// - [ConstantField] internal static readonly IReadOnlyDictionary ConstantList = - new Dictionary + [ConstantField] internal static readonly IReadOnlyDictionary> ConstantList = + new Dictionary> { - { nameof(pi), MathS.DecimalConst.pi }, - { nameof(e), MathS.DecimalConst.e } + { nameof(pi), static () => MathS.DecimalConst.pi }, + { nameof(e), static () => MathS.DecimalConst.e } }; /// Each constant as the name a writer types, which is the form a binder can take. @@ -201,7 +204,7 @@ private Constant(string name) : base(name) { } /// A computed property on purpose: a record compares its instance fields, and the /// identity of a constant is its name and its role, not a hundred digits of it. /// - internal Number.Complex Value => ConstantList[Name]; + internal Number.Complex Value => ConstantList[Name](); /// /// Euler's number as the base of ln and of exp. Equal to the written diff --git a/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md b/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md index 9dd586011..cdd9690c8 100644 --- a/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md +++ b/Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md @@ -256,6 +256,30 @@ cent — its measured run-to-run spread on mean time is up to 51.8%. A move of 8 outside that band by a wide margin and agrees in sign and rough size with the allocation column beside it. The small rows from the same run are still not worth reading, and are not quoted. +## The 2075th: a cached evaluation follows the precision context + +A correctness fix ([#1367](https://github.com/asc-community/AngouriMath/issues/1367)) with a +cost, recorded here because the cost is on every row: `pi` and `e` were worth the digits of +whichever precision was set when their registry was built, and an entity's cached `Evaled` +survived a change of `DecimalPrecisionContext`, so `pi` at three hundred digits after an +evaluation at a hundred came back with a hundred, and `e^x` came back correct to a hundred of +its three hundred. The constant's value is now looked up at the moment of evaluation, and the +precision setting advances an evaluation epoch on every `Set` and every scope's end, which an +entity records beside its `Evaled` and compares on each read -- one `int` on every entity and +one static read per evaluation. + +| benchmark | 2074th | 2075th | allocation | time | +|---|--:|--:|--:|--:| +| `SolveMediumHard` | 1,367,804 | 1,381,260 | +1.0% | | +| `SimplifyHard` | 179,367,888 | 180,917,280 | +0.9% | | +| `Derivate` | 53,111 | 53,527 | +0.8% | | +| `EvalPolynomialFresh` | 12,328 | 12,424 | +0.8% | | +| every other entry | | | within 0.7% | within the noise | + +Bytes allocated per call, same machine, both columns by the gate in one session: the eight +bytes an entity grew by, times the entities each row builds, all inside the gate's band, so +the baseline stands. + ## The 2074th: the logarithm and the arctangent reduced by a table, the cosine from the sine, the square root an integer one After the 2072nd the remaining factor to mpmath was the term count: eighty artanh terms for a diff --git a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs index e70c48d0f..1127fe05f 100644 --- a/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs +++ b/Sources/AngouriMath/Functions/Evaluation/Evaluation.Definition.cs @@ -180,8 +180,25 @@ private Entity InnerSimplifyWithCheck(bool isExact) /// AngouriMath.Entity+Boolean /// /// - public Entity Evaled => evaled.GetValue(static @this => @this.InnerSimplifyWithCheck(false), this); + public Entity Evaled + { + get + { + // Cached for the precision it was computed under: a hundred digits of pi are + // the wrong answer at five hundred, and an expression held across a change of + // MathS.Settings.DecimalPrecisionContext used to answer from the first one. + // https://github.com/asc-community/AngouriMath/issues/1367 + var epoch = Convenience.EvaluationEpoch.Current; + if (evaledEpoch != epoch) + { + evaled = default; + evaledEpoch = epoch; + } + return evaled.GetValue(static @this => @this.InnerSimplifyWithCheck(false), this); + } + } private LazyPropertyA evaled; + private int evaledEpoch; /// /// This is the result of naive simplifications, but not creating imprecise values unlike . In other diff --git a/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs b/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs index 973d28b1f..cdf93aebe 100644 --- a/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs +++ b/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs @@ -232,5 +232,49 @@ public void TheCheapDecisionReadsTwoThousandDigits() // that could not cover the width would refuse none. Assert.True(readable > 150, $"{readable} of 200 refused cheaply"); } + + private static readonly EContext threeHundredDigits = new(300, ERounding.HalfUp, -5000, 5000, false); + + private static int DigitsOf(Entity number) => number.ToString().TrimStart('-').Replace(".", "").Length; + + /// + /// A constant is worth the current precision's digits, whichever precision it was + /// first evaluated at -- pi at a hundred digits, then three hundred, then a hundred + /// again; and the same object of the same expression, held across a change of the + /// precision, is evaluated afresh at the new one rather than answered from its cache. + /// https://github.com/asc-community/AngouriMath/issues/1367 + /// + [Fact] + public void ConstantsAndCachedEvaluationsFollowThePrecision() + { + var pi = MathS.pi; + var held = MathS.Sin(1) + MathS.pi; + Assert.Equal(100, DigitsOf(pi.EvalNumerical())); + Assert.Equal(100, DigitsOf(held.EvalNumerical())); + using (MathS.Settings.DecimalPrecisionContext.Set(threeHundredDigits)) + { + Assert.Equal(300, DigitsOf(pi.EvalNumerical())); + Assert.Equal(300, DigitsOf(held.EvalNumerical())); + Assert.Equal(300, DigitsOf(MathS.FromString("pi").EvalNumerical())); + Assert.StartsWith("3.14159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493038196442881097566593344612847564823378678316527120190914564856692346034861045432664821339360726024914127", pi.EvalNumerical().ToString()); + } + Assert.Equal(100, DigitsOf(pi.EvalNumerical())); + Assert.Equal(100, DigitsOf(held.EvalNumerical())); + } + + /// + /// e to a power at three hundred digits, after the same expression's shape was + /// evaluated at a hundred: it used to come back correct to a hundred, since the base + /// was the hundred-digit e and no longer the exponential's own constant. + /// https://github.com/asc-community/AngouriMath/issues/1367 + /// + [Fact] + public void EToAPowerAtAHigherPrecisionAfterALowerOne() + { + var x = Real.Create(EDecimal.FromString("0.37000000000001234")); + Assert.Equal(100, DigitsOf(MathS.Pow(MathS.e, x).EvalNumerical())); + using var _ = MathS.Settings.DecimalPrecisionContext.Set(threeHundredDigits); + Assert.StartsWith("1.4477346146633423266298972810533128989549659677348909712480333771187271973782808968904998312256362611570708175918866347464490548332052196352156710435710332682587883217473960307622422354384764392652386915303661697273096380952633917033052762392317525332683262721013424903315670646133049105751036966", MathS.Pow(MathS.e, x).EvalNumerical().ToString()); + } } }