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
56 changes: 56 additions & 0 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ read first.
| loud | the target frameworks | `net7.0;netstandard2.0` | `netstandard2.0;net8.0;net10.0` |
| **silent** | `abs(x) = c` for a negative `c` | a set of non-solutions | the empty set |
| loud | implicit `List<Entity>` to `Entity` | made a `FiniteSet`, and made three `params` overloads uncallable | removed |
| **silent** | a `MathS.Settings` scope across an `await`, or inside a task | lost, or somebody else's | follows the call |

---

Expand Down Expand Up @@ -149,6 +150,61 @@ Entity set = new Entity[] { 1, 2, 3 }; // the array conversion is unchange
The `Entity[]` conversion is kept: an array argument binds to the `params` overload in its normal
form by an identity conversion, which beats the alternatives outright, so it never produced the
ambiguity.
### A settings scope belongs to the call, not to the thread

`MathS.Settings` values were held in `[ThreadStatic]` fields — fourteen of them. A scope
therefore stopped at the first `await`:

```csharp
using var _ = MathS.Settings.MaxExpansionTermCount.Set(1);
Console.WriteLine(MathS.Settings.MaxExpansionTermCount.Value); // 1
await Task.Delay(20).ConfigureAwait(false);
Console.WriteLine(MathS.Settings.MaxExpansionTermCount.Value); // was 2000, the default
```

The continuation resumed on a pool thread that had never seen the scope, so the setting
silently read its default. The same mechanism ran the other way too: a thread returned to
the pool still carried whatever scope was left on it, so the *next* caller to borrow that
thread could compute under a precision or codomain it never asked for. Neither shows up as
an error; both change the answer.

The values now live in an `AsyncLocal`, which is what the cancellation token in
`MathS.Multithreading` already used.

| | was | is |
|---|---|---|
| a scope across an `await` | lost | kept |
| a scope inside `Task.Run` started under it | not inherited | **inherited** |
| a scope opened in a task, seen by a sibling | no | no |
| a scope opened in a task, seen after it ends | no | no |
| a thread reused by the pool | could carry a stale scope | cannot |

**What breaks.** The second row is the one to read. Work started inside a scope now runs
under it:

```csharp
using var _ = MathS.Settings.Codomain.Set(Domain.Real);
await Task.Run(() => expr.Solve("x")); // now solves over R; used to solve over C
```

That is what the code says, and almost always what was meant — but if you parallelised
inside a scope and relied on the child *not* seeing it, it does now. Move the scope inside
the callback to keep the old behaviour.

**Cost.** Measured over 20 000 000 reads and 2 000 000 scopes:

| | was | is |
|---|---|---|
| read `PrecisionErrorZeroRange.Value` | 7.3–8.0 ns | **1.2 ns** |
| read `DowncastingEnabled.Value` | 0.79 ns | 0.96 ns |
| `Set()` + `Dispose()` | 388–395 ns, 32 B | **46 ns**, 112 B |
| a `Simplify` workload | 5.6–5.8 s | 5.5–5.6 s, +1.5 % allocated |

Reads got faster rather than slower: the old getter re-tested a `[ThreadStatic]` field for
null on every access, and that is dearer than the async-local lookup that replaced it.
Opening a scope got much cheaper because it no longer mints a `Guid` to identify itself,
though it allocates more, since assigning an `AsyncLocal` copies the flow's value map. Reads
outnumber scope openings by orders of magnitude in any real workload.

### `Minusf`'s two operands exchanged names

Expand Down
3 changes: 3 additions & 0 deletions Sources/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed
[Tests/UnitTests/Common/ListArgumentOverloadTest.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

[Tests/UnitTests/Core/Multithreading/SettingsAcrossAsync.cs]
file_header_template=\nCopyright (c) 2019-2026 Angouri.\nAngouriMath is licensed under MIT.\nDetails: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.\nWebsite: https://am.angouri.org.\n

# A new file in a directory whose other files predate it, so the section is on the file
# rather than the folder.
[AngouriMath/Core/Entity/Continuous/Entity.Continuous.{Floors,Rounding}.Classes.cs]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,14 @@ public static class Diagnostic
/// Explicit output for ToString, that is, no signs or parentheses will be omitted. Useful
/// for debugging and diagnostic.
/// </summary>
public static Setting<bool> OutputExplicit => outputExplicit ??= false;
[ThreadStatic] private static Setting<bool>? outputExplicit;
public static Setting<bool> OutputExplicit { get; } = false;

/// <summary>
/// Set a predicate on the state of <see cref="Entity"/> so that once
/// the predicate turns true in method <see cref="Entity.Simplify"/>,
/// an exception <see cref="DiagnosticCatchException"/> is thrown.
/// </summary>
public static Setting<Func<Entity, bool>> CatchOnSimplify => catchOnSimplify ??= (Func<Entity, bool>)(a => false);
[ThreadStatic] private static Setting<Func<Entity, bool>>? catchOnSimplify;
public static Setting<Func<Entity, bool>> CatchOnSimplify { get; } = (Func<Entity, bool>)(a => false);

/// <summary>
/// Will only occur in debug mode,
Expand Down
39 changes: 12 additions & 27 deletions Sources/AngouriMath/Convenience/MathS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5347,8 +5347,7 @@ public static partial class Settings
/// Exception, but we still can parse a ^ 2 + 2 * x + a * b + 2 * (g + e) ^ 3
/// </code>
/// </example>
public static Setting<bool> ExplicitParsingOnly => explicitParsingOnly ??= false;
[ThreadStatic] private static Setting<bool>? explicitParsingOnly;
public static Setting<bool> ExplicitParsingOnly { get; } = false;

/// <summary>
/// That is how we perform newton solving when no analytical solution was found
Expand Down Expand Up @@ -5400,8 +5399,7 @@ public sealed record NewtonSetting
/// AngouriMath.Entity+Number+Real
/// </code>
/// </example>
public static Setting<bool> DowncastingEnabled => downcastingEnabled ??= true;
[ThreadStatic] private static Setting<bool>? downcastingEnabled;
public static Setting<bool> DowncastingEnabled { get; } = true;

/// <summary>
/// Amount of iterations allowed for attempting to cast to a rational
Expand Down Expand Up @@ -5448,8 +5446,7 @@ public sealed record NewtonSetting
/// 5/7
/// </code>
/// </example>
public static Setting<int> FloatToRationalIterCount => floatToRationalIterCount ??= 15;
[ThreadStatic] private static Setting<int>? floatToRationalIterCount;
public static Setting<int> FloatToRationalIterCount { get; } = 15;

/// <summary>
/// If a numerator or denominator is too large, it's suspended to better keep the real number instead of casting
Expand Down Expand Up @@ -5503,24 +5500,20 @@ public sealed record NewtonSetting
/// 100/169137
/// </code>
/// </example>
public static Setting<EInteger> MaxAbsNumeratorOrDenominatorValue =>
maxAbsNumeratorOrDenominatorValue ??= EInteger.FromInt32(100000000);
[ThreadStatic] private static Setting<EInteger>? maxAbsNumeratorOrDenominatorValue;
public static Setting<EInteger> MaxAbsNumeratorOrDenominatorValue { get; } = EInteger.FromInt32(100000000);

/// <summary>
/// Sets threshold for comparison
/// For example, if you don't need precision higher than 6 digits after .,
/// you can set it to 1.0e-6 so 1.0000000 == 0.9999999
/// </summary>
public static Setting<EDecimal> PrecisionErrorCommon => precisionErrorCommon ??= EDecimal.Create(1, -6);
[ThreadStatic] private static Setting<EDecimal>? precisionErrorCommon;
public static Setting<EDecimal> PrecisionErrorCommon { get; } = EDecimal.Create(1, -6);


/// <summary>
/// Numbers whose absolute value is less than PrecisionErrorZeroRange are considered zeros
/// </summary>
public static Setting<EDecimal> PrecisionErrorZeroRange => precisionErrorZeroRange ??= EDecimal.Create(1, -16);
[ThreadStatic] private static Setting<EDecimal>? precisionErrorZeroRange;
public static Setting<EDecimal> PrecisionErrorZeroRange { get; } = EDecimal.Create(1, -16);

/// <summary>
/// The tolerance within which downcasting rounds a number onto a nearby integer.
Expand Down Expand Up @@ -5576,8 +5569,7 @@ internal static EDecimal DowncastingTolerance
/// { } // nothing was found for 5-degree polynomial without numeric solution
/// </code>
/// </example>
public static Setting<bool> AllowNewton => allowNewton ??= true;
[ThreadStatic] private static Setting<bool>? allowNewton;
public static Setting<bool> AllowNewton { get; } = true;

/// <summary>
/// Criteria for simplifier so you could control which expressions are considered easier by you.
Expand Down Expand Up @@ -5649,8 +5641,7 @@ internal static EDecimal DowncastingTolerance
/// By default criteria it cannot simplify it further, however, the custom one
/// it simplified from 2 to 1.
/// </example>
public static Setting<Func<Entity, double>> ComplexityCriteria =>
complexityCriteria ??= new Func<Entity, double>(expr =>
public static Setting<Func<Entity, double>> ComplexityCriteria { get; } = new Func<Entity, double>(expr =>
{
// Those are of the 2nd power to avoid problems with floating numbers
const double TinyWeight = 0.5;
Expand Down Expand Up @@ -5688,7 +5679,6 @@ internal static EDecimal DowncastingTolerance
} + Weight; // Number of nodes
return DefaultCriteria(expr);
});
[ThreadStatic] private static Setting<Func<Entity, double>>? complexityCriteria;

/// <summary>
/// Settings for the Newton-Raphson's root-search method
Expand All @@ -5703,8 +5693,7 @@ internal static EDecimal DowncastingTolerance
/// ...
/// </code>
/// </summary>
public static Setting<NewtonSetting> NewtonSolver => newtonSolver ??= new NewtonSetting();
[ThreadStatic] private static Setting<NewtonSetting>? newtonSolver;
public static Setting<NewtonSetting> NewtonSolver { get; } = new NewtonSetting();

/// <summary>
/// The maximum number of linear children of an expression in polynomial solver
Expand Down Expand Up @@ -5736,15 +5725,12 @@ internal static EDecimal DowncastingTolerance
/// </item>
/// </list>
/// </summary>
public static Setting<long> MaxExpansionTermCount => maxExpansionTermCount ??= 2_000;
[ThreadStatic] private static Setting<long>? maxExpansionTermCount;
public static Setting<long> MaxExpansionTermCount { get; } = 2_000;

/// <summary>
/// Settings for <see cref="EDecimal"/> precisions of <a href="https://github.com/peteroupc/Numbers">PeterO.Numbers</a>
/// </summary>
public static Setting<EContext> DecimalPrecisionContext =>
decimalPrecisionContext ??= new EContext(100, ERounding.HalfUp, -100, 1000, false);
[ThreadStatic] private static Setting<EContext>? decimalPrecisionContext;
public static Setting<EContext> DecimalPrecisionContext { get; } = new EContext(100, ERounding.HalfUp, -100, 1000, false);

/// <summary>
/// Whether functions are being read as real-valued or complex-valued. It is a
Expand Down Expand Up @@ -5777,8 +5763,7 @@ internal static EDecimal DowncastingTolerance
/// prints the unevaluated limit, where the default prints <c>-oo</c>.
/// </example>
/// </remarks>
public static Setting<Domain> Codomain => codomain ??= Domain.Complex;
[ThreadStatic] private static Setting<Domain>? codomain;
public static Setting<Domain> Codomain { get; } = Domain.Complex;
}

/// <summary>Returns an <see cref="Entity"/> in polynomial order if possible</summary>
Expand Down
Loading
Loading