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
3 changes: 2 additions & 1 deletion Sources/AngouriMath/Convenience/MathS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5912,7 +5912,8 @@ internal static EDecimal DowncastingTolerance
/// <summary>
/// Settings for <see cref="EDecimal"/> precisions of <a href="https://github.com/peteroupc/Numbers">PeterO.Numbers</a>
/// </summary>
public static Setting<EContext> DecimalPrecisionContext { get; } = new EContext(100, ERounding.HalfUp, -100, 1000, false);
public static Setting<EContext> DecimalPrecisionContext { get; } =
new(new EContext(100, ERounding.HalfUp, -100, 1000, false)) { AdvancesEvaluationEpoch = true };

/// <summary>
/// Whether functions are being read as real-valued or complex-valued. It is a
Expand Down
27 changes: 27 additions & 0 deletions Sources/AngouriMath/Convenience/SettingClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,23 @@ internal static bool StillHolds(object?[] snapshot)
}
}

/// <summary>
/// A count of the changes to what a numerical evaluation depends on -- the precision
/// context, set or its scope ended, on any thread. An <see cref="Entity"/> records the
/// epoch its <see cref="Entity.Evaled"/> 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
/// </summary>
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);
}

/// <summary>
/// This class for configuring some internal mechanisms from outside
/// </summary>
Expand Down Expand Up @@ -146,6 +163,12 @@ internal Setting(T defaultValue)
SettingsState.Register(this);
}

/// <summary>
/// Whether a change of this setting advances the <see cref="EvaluationEpoch"/>: the
/// precision context does, since every cached evaluation depends on it.
/// </summary>
internal bool AdvancesEvaluationEpoch { get; init; }

/// <summary>
/// The frame this setting currently reads from, as the identity of its state. Released
/// in order — which <c>using</c> guarantees — a scope restores the very frame that was
Expand Down Expand Up @@ -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);
}

Expand All @@ -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;
Expand Down
19 changes: 11 additions & 8 deletions Sources/AngouriMath/Core/Entity/Omni/Entity.Variable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,18 @@ public void Deconstruct(out string name)
protected override Entity[] InitDirectChildren() => Array.Empty<Entity>();

/// <summary>
/// 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 <c>pi</c> or <c>e</c> 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 <c>pi</c> or <c>e</c> by name.
/// </summary>
[ConstantField] internal static readonly IReadOnlyDictionary<string, Complex> ConstantList =
new Dictionary<string, Complex>
[ConstantField] internal static readonly IReadOnlyDictionary<string, Func<Complex>> ConstantList =
new Dictionary<string, Func<Complex>>
{
{ nameof(pi), MathS.DecimalConst.pi },
{ nameof(e), MathS.DecimalConst.e }
{ nameof(pi), static () => MathS.DecimalConst.pi },
{ nameof(e), static () => MathS.DecimalConst.e }
};

/// <summary>Each constant as the name a writer types, which is the form a binder can take.</summary>
Expand Down Expand Up @@ -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.
/// </remarks>
internal Number.Complex Value => ConstantList[Name];
internal Number.Complex Value => ConstantList[Name]();

/// <summary>
/// Euler's number as the base of <c>ln</c> and of <c>exp</c>. Equal to the written
Expand Down
24 changes: 24 additions & 0 deletions Sources/AngouriMath/Docs/WhatsNew/version_performance_control.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,25 @@ private Entity InnerSimplifyWithCheck(bool isExact)
/// AngouriMath.Entity+Boolean
/// </code>
/// </example>
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<Entity> evaled;
private int evaledEpoch;

/// <summary>
/// This is the result of naive simplifications, but not creating imprecise <see cref="Real"/> values unlike <see cref="Evaled"/>. In other
Expand Down
44 changes: 44 additions & 0 deletions Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// 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
/// </summary>
[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()));
}

/// <summary>
/// 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
/// </summary>
[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());
}
}
}
Loading