Files
Just.PreciseMath/README.md
T
just 5797bf4884
.NET Test / .NET tests (push) Successful in 3m55s
added sanity checks
2026-09-18 14:10:24 +04:00

436 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Just.PreciseMath
Extended-precision floating-point arithmetic for .NET using double-double
representations: a high/low pair of `double` values. The goal is to retain more
precision than a single `double` while using a fixed-size representation,
rather than arbitrary-precision arithmetic.
> **Work in progress.** The public API is incomplete and may change. Numerical
> contracts are covered by regression tests, not an exhaustive accuracy
> certification. The library is not ready for production use.
## DoubleDouble core
`DoubleDouble` stores a normalized high/low pair. Use `new DoubleDouble(value)`
for a single `double`, or `DoubleDouble.FromComponents(high, low)` for arbitrary
components. The factory normalizes finite sums and canonicalizes NaN/infinity
with a positive-zero low component. The two-component constructor is internal
and performs no normalization or validation; it is reserved for trusted,
already-normalized results. Mathematical constants use precomputed high/low pairs
checked against independently computed high-precision values; accessing them does
not perform double-double arithmetic or allocate on the heap.
- Arithmetic: unary `+`/`-`, binary `+`, `-`, `*`, `/`, and both operand orders with
a `double`. Addition retains residuals under cancellation; multiplication uses
fused multiply-add; division uses residual corrections. Mixed `double` operators
use specialized scalar paths rather than promoting the scalar to `DoubleDouble`.
Division retains the complete first remainder and applies two quotient corrections,
normalizing before the final correction. Mixed addition and subtraction also retain
intermediate sum residuals through final normalization. Mixed addition, subtraction,
and division produce the same component bits as their `DoubleDouble` operations
with the scalar represented as a zero-low pair.
- Exponent boundaries: bounded `BigInteger` calculations avoid intermediate
overflow and underflow on the exceptional finite path. Ordinary arithmetic uses
floating-point transforms without allocations, although sparse division correction
products can also reach a boundary path. The stored value remains two
doubles; this is not an arbitrary-precision API.
- Comparisons use both components. `Equals` treats NaNs as equal and signed zeros
as equal for collections. `CompareTo` orders NaN before other values. Numerical
equality and relational operators treat NaN as unordered, like `double`.
- Signed zero is preserved by single-value construction and unary negation.
`FromComponents` with a zero low input preserves the high zero's
sign. Exact cancellation of nonzero values yields positive zero. Arithmetic
special values follow binary64 rules.
Arithmetic is approximate double-double arithmetic, **not a promise of correctly
rounded 106-bit results**. The deterministic rational-oracle tests check a
conservative error bound of `2^-100` relative plus one minimum binary64 subnormal,
with exact component checks for selected representable cases. Near underflow,
extended precision necessarily decreases; overflow produces infinity. Performance
of the allocating exponent-boundary path is not covered by the basic benchmarks.
Named value constants include `Zero`, `NegativeZero`, `One`, `NegativeOne`, `NaN`,
`PositiveInfinity`, `NegativeInfinity`, and `Epsilon`. `Epsilon` is the smallest
positive representable value, `2^-1074` (the same as `double.Epsilon`), **not** a
relative-error tolerance or machine epsilon. These constants have a positive-zero
low component; `NegativeZero` preserves the high sign bit while comparing and
hashing equal to `Zero`.
## Predefined mathematical constants
All constants below are static `DoubleDouble` properties. Each stores the nearest
binary64 high component followed by the nearest binary64 residual, rather than
calculating a ratio, root, or logarithm on access. Names use PascalCase, including
`Pi`, `E`, and `Ln2`.
`DoubleDouble` implements `IFloatingPointConstants<DoubleDouble>` for generic
access to `E`, `Pi`, and `Tau`; this does not imply support for the full
`IFloatingPointIeee754<DoubleDouble>` interface.
| Group | Properties and values |
|---|---|
| Circle and common angles | `Pi` (π), `Tau` (2π), `PiOver2`, `PiOver3`, `PiOver4`, `PiOver6` |
| Angular conversion | `DegToRad` (π/180), `RadToDeg` (180/π), `InvPi` (1/π), `InvTau` (1/(2π), radians to turns) |
| Exponential and logarithmic | `E`, `InvE` (1/e), `Ln2` (ln 2), `Ln10` (ln 10) |
| Log-base conversion | `Log2E` (1/ln 2), `Log10E` (1/ln 10), `Log2Of10` (ln 10/ln 2), `Log10Of2` (ln 2/ln 10) |
| Roots and geometry | `Sqrt2`, `Sqrt3`, `Sqrt5`, `InvSqrt2`, `InvSqrt3`, `GoldenRatio` ((1+√5)/2) |
| Gaussian and error-function factors | `SqrtPi`, `InvSqrtPi`, `TwoInvSqrtPi` (2/√π), `SqrtTau` (√(2π)), `InvSqrtTau` (1/√(2π)) |
Multiply by conversion factors instead of recomputing them:
```csharp
using Just.PreciseMath;
DoubleDouble degrees = new(180.0);
DoubleDouble radians = degrees * DoubleDouble.DegToRad;
DoubleDouble convertedDegrees = radians * DoubleDouble.RadToDeg;
DoubleDouble quarterTurn = DoubleDouble.PiOver2;
```
The factors avoid deriving constants at runtime; the multiplication itself remains
approximate DD arithmetic, so conversions are not guaranteed exact round trips.
The list is mathematical and dimensionless, not a table of unit-dependent physical
constants. The natural logarithm function is provided separately by `DDMath.Log`.
## Mathematical functions
Root functions are implemented on `DoubleDouble` in `DoubleDouble.RootFunctions.cs`:
`Sqrt`, `Cbrt`, `Hypot`, and `RootN` complete `IRootFunctions<DoubleDouble>`.
The additional `InvSqrt` helper and three-argument `Hypot` overload live alongside
them. Exponential functions live in `DoubleDouble.ExponentialFunctions.cs`:
`Exp`, `Exp2`, `Exp10`, `ExpM1`, `Exp2M1`, and `Exp10M1` complete
`IExponentialFunctions<DoubleDouble>`, including cancellation-safe overrides of
the interface's default minus-one methods. `DDMath` exposes thin forwarding
wrappers for roots and exponentials; logarithm, power, and reciprocal kernels
remain in `DDMath`.
The `DDMath` static class provides:
- `Abs(DoubleDouble)`: preserves both components, maps either signed zero to
positive zero and either infinity to positive infinity, and returns canonical NaN.
It shares the existing `DoubleDouble.Abs` implementation.
- `Reciprocal(DoubleDouble)`: returns exactly the same high and low component bits
as `1.0 / value`. It specializes scalar/DD division for a numerator of one,
omitting redundant numerator checks and sharing the finite scalar-numerator kernel,
including both residual corrections and normalization. Signed zeros map to signed infinities,
signed infinities to signed zeros, and NaN to canonical NaN. The allocating exact boundary
path handles extreme exponents; finite overflow produces signed infinity.
No speedup over scalar/DD division has been measured.
- `Sqrt(DoubleDouble)`: uses power-of-two scaling and an FMA-based Newton correction
to retain extended precision, including for subnormal inputs, without squaring
an unscaled estimate near the exponent limits. Signed zero and positive infinity
are preserved; negative nonzero inputs and NaN return canonical NaN.
- `InvSqrt(DoubleDouble)`: computes the reciprocal square root with power-of-two
scaling and a compensated Newton step, avoiding double-double division and its
allocating boundary paths. The estimate's squared-product residual is retained
with FMA. Signed zeros map to correspondingly signed infinities; positive infinity
maps to positive zero; negative nonzero inputs and NaN return canonical NaN.
This is a dedicated algorithm, not a claim of measured speedup over `1.0 / Sqrt(x)`.
- `Cbrt(DoubleDouble)`: real cube root using a scaled binary64 seed and compensated
Newton corrections. Signed zeros and infinities are preserved; NaN is canonical.
An exponent-tracked low-component correction avoids losing representable sparse
residuals during scaling.
- `Hypot(DoubleDouble, DoubleDouble)`: computes `sqrt(x²+y²)` with bounded scaled
squares and complete-expansion rescaling at exponent boundaries. Widely separated
operands use a small correction without squaring an underflow-prone ratio.
Results are nonnegative, including positive zero. Either infinity takes precedence
over NaN; otherwise NaN propagates. At the upper boundary, exact squared-input
comparisons determine overflow; an overflowing approximation of a finite root
is clamped to the largest finite component pair. Overflow produces positive infinity.
- `Hypot(DoubleDouble, DoubleDouble, DoubleDouble)`: the 3D Euclidean norm
`sqrt(x²+y²+z²)`, with the same special-value and approximate-accuracy contracts.
Coordinates are ordered by magnitude before evaluating scaled squares, so signs
and permutations give identical component bits. Widely separated coordinates
contribute combined corrections without prematurely underflowing their squares.
A zero coordinate reduces to the two-argument overload; exact overflow checks
include all three original inputs. This overload is a convenience API, not an
additional member of `IRootFunctions`.
- `RootN(DoubleDouble, int)`: real nth root for positive degrees, reciprocal root
for negative degrees, supporting the complete `int` range. Degree zero always
returns NaN; negative nonzero inputs require an odd degree. Odd degrees retain
the input sign, while even degrees map either signed zero to positive zero for
positive degrees and positive infinity for negative degrees. Negative degrees
exchange zero and infinity. Unlike `Sqrt(-0)`, `RootN(-0, 2)` is positive zero.
Degrees ±1, ±2 and ±3 reuse identity/reciprocal and existing root kernels; other
degrees refine a binary64 seed using integer powers with separately tracked
exponents, then incorporate the input low without premature underflow.
- `Pow(DoubleDouble, int)`: exponentiation by squaring with a separately tracked
binary exponent. Supports the full `int` domain, including `int.MinValue`, and
reciprocates a bounded significand before final scaling for negative exponents.
Any value to power zero is one (including NaN and signed zero). Other NaNs
propagate; zero/infinity signs follow integer-power parity and exponent sign.
- `Pow(DoubleDouble, double)` and `Pow(DoubleDouble, DoubleDouble)`: arbitrary
real powers, retaining the base's low component and, for the DD overload, the
exponent's low component. For finite exponents, negative finite bases require integers;
integrality and odd/even parity use the complete exponent, even above `2^53`.
Integer-valued exponents within `int` range reuse the integer implementation.
Other finite cases share `Log`'s range-scaled finite kernel, followed by `Exp`.
Near-one bases retain tiny low components before multiplication by large powers.
- `Exp(DoubleDouble)`: binary range reduction with three components of `ln(2)`,
followed by a [12/12] Padé approximation and power-of-two scaling. Both input
components affect range boundaries; representable subnormals are retained.
Either zero maps to one, negative infinity to positive zero, positive infinity
to positive infinity, and NaN to canonical NaN.
- `Exp2(DoubleDouble)`: reduces the argument in base two before evaluating a
bounded natural exponential. Integer powers from -1074 through 1023 are exact.
An exponent-tracked correction preserves sparse lows that would otherwise round
prematurely before final scaling. The exact half-minimum-subnormal threshold
uses both input components. Special-value behavior is the same as `Exp`.
- `Exp10(DoubleDouble)`: subtracts three split `log10(2)` products before converting
the bounded remainder to a natural exponent, avoiding large-argument amplification
of a rounded `ln(10)` product. Special-value behavior is the same as `Exp`.
- `ExpM1(DoubleDouble)`, `Exp2M1(DoubleDouble)`, and `Exp10M1(DoubleDouble)`:
compute the corresponding exponential minus one, using a direct series near
zero to avoid cancellation. Signed zeros are preserved, negative infinity maps
to negative one, positive infinity to positive infinity, and NaN to canonical NaN.
Their tested relative error is measured against the minus-one result itself,
not against the exponential before subtraction.
- `Log(DoubleDouble)`: natural logarithm using binary range reduction and a
centered atanh series. The bounded mantissa avoids denominator overflow for
large inputs; a separate near-one path retains even minimum-subnormal low
components. Either zero maps to negative infinity, one to positive zero,
positive infinity to itself, and negative nonzero inputs or NaN to canonical NaN.
```csharp
using Just.PreciseMath;
DoubleDouble root = DoubleDouble.Sqrt(new DoubleDouble(2.0));
DoubleDouble inverseRoot = DoubleDouble.InvSqrt(new DoubleDouble(2.0));
DoubleDouble cubeRoot = DoubleDouble.Cbrt(new DoubleDouble(-8.0));
DoubleDouble distance = DoubleDouble.Hypot(new DoubleDouble(3.0), new DoubleDouble(4.0));
DoubleDouble norm3D = DoubleDouble.Hypot(new DoubleDouble(2.0), new DoubleDouble(3.0), new DoubleDouble(6.0));
DoubleDouble fifthRoot = DoubleDouble.RootN(new DoubleDouble(2.0), 5);
DoubleDouble reciprocal = DDMath.Reciprocal(new DoubleDouble(3.0));
DoubleDouble magnitude = DDMath.Abs(-root);
DoubleDouble smallPower = DDMath.Pow(new DoubleDouble(2.0), -1024);
DoubleDouble exponential = DoubleDouble.Exp(new DoubleDouble(1.0));
DoubleDouble binaryPower = DoubleDouble.Exp2(new DoubleDouble(-1000.0));
DoubleDouble decimalPower = DoubleDouble.Exp10(new DoubleDouble(0.5));
DoubleDouble tinyChange = DoubleDouble.ExpM1(new DoubleDouble(1e-30));
DoubleDouble logarithm = DDMath.Log(new DoubleDouble(10.0));
DoubleDouble fractionalPower = DDMath.Pow(new DoubleDouble(2.0), 0.5);
DoubleDouble preciseExponent = DoubleDouble.FromComponents(0.5, 1e-30);
DoubleDouble precisePower = DDMath.Pow(new DoubleDouble(2.0), preciseExponent);
```
Reciprocal tests check bitwise equivalence with `1.0 / value` and independently
check exact rational error against `2^-100` relative plus one minimum binary64
subnormal. They sample every binary64 exponent, both signs, dense and sparse lows,
binade neighbors, and special values; selected powers of two and sparse corrections
also have exact component checks. This preserves division's approximate-accuracy
contract, not a guarantee of correctly rounded results.
Square-root and inverse-square-root tests compare the exact component sum against
a `2^-100` relative error bound using integer inequalities. They include samples at every binary64
exponent, boundary neighbors, both signs of the low component, and exact binary
squares/powers of four. Square-root tests also bracket exact root-rounding midpoints;
both suites exercise half-ulp low-component normalization boundaries.
This is a tested approximate-accuracy contract, not exhaustive coverage
of all component pairs or a guarantee of correctly rounded results.
Cube-root and nth-root tests check `2^-100` relative error for finite nonzero
inputs with degree magnitude at least two. Small degrees use exact integer-power
inequalities; large degrees use independently generated decimal references checked
at 450 and 650 digits, including `int.MinValue` and `int.MaxValue`.
`RootN(x, -1)` retains the reciprocal contract, including its subnormal error floor.
Hypotenuse tests use exact squared inequalities with `2^-100` relative error plus
one minimum binary64 subnormal. Tests also check exact binary cases, sparse lows,
special-value signs, facade component-bit agreement, and constrained generic dispatch.
The 2D and 3D hypotenuse overflow tests bracket the exact threshold, including scaled
55/48/73 and 1/2/2/3 Pythagorean cases at the midpoint. Three-coordinate tests include
all signs and permutations of boundary cases and sparse corrections, including
corrections that only become representable when combined. Finite components remain approximate,
not universally correctly rounded, including near underflow.
Logarithm tests compare exact component sums with independently generated
120-digit decimal references, requiring agreement at 450 and 650 digits. They
check `2^-100` relative error plus one minimum binary64 subnormal, with an explicit
reference-rounding allowance. Powers of two cover every finite binary64 exponent;
additional tests cover extreme magnitudes, near-one cancellation, sparse lows of
either sign, and range-reduction transitions. This is sampled approximate accuracy,
not a universal error proof or a correct-rounding guarantee.
Exponential-family tests use independent high-precision decimal references evaluated at
two precisions, with exact integer comparisons of the stored component sum. They
check `2^-100` relative error plus one minimum binary64 subnormal, with a separately
bounded reference-rounding allowance. The base-two/base-ten and minus-one fixtures
are checked at 450/650 digits, including adjacent lows at true range boundaries,
series transitions, and tiny inputs. Additional component checks pin sparse
base-two corrections; constrained generic calls and facade calls match direct
calls bit-for-bit. Integer-power tests use exact rational
references and high-precision fixtures for large exponents; their tested absolute
error bound is `|exact result| * (|exponent| + 1) * 2^-100 + 2^-1074`, not uniform
relative accuracy independent of the exponent. These functions are approximate;
precision decreases near underflow and approximation can affect results extremely
close to a rounding boundary. Final scaling uses the existing allocating exact
boundary machinery where necessary. No performance measurements are claimed.
For real powers, `x^±0 = 1` and `1^y = 1`, including NaN in the other operand;
other NaNs propagate. Infinite exponents compare the complete `|x|` with one,
with `(-1)^±∞ = 1`. Zero and infinite bases yield a negative sign only for odd
integer exponents; negative powers exchange zero and infinity. Finite negative
bases with non-integer finite exponents return canonical NaN.
Real-power reference tests check `2^-90` relative error plus one minimum binary64
subnormal for the logarithm/Exp path, with an explicit reference-rounding allowance.
Integer dispatch retains the exponent-dependent bound above. These are tested
approximate-accuracy contracts, not universal error proofs or correct-rounding
guarantees. Reference inputs are exact component sums; logarithms/exponentials
are independently evaluated at 450 and 650 decimal digits.
**Known real-power limitations:** extremely close to the range boundaries, the
logarithm/Exp path can return infinity for a mathematically finite result, or a
minimum subnormal where zero is expected. The latter can also break monotonicity
across integer-exponent dispatch. These issues remain unresolved; passing the
sampled error bounds does not guarantee correct range decisions for every input.
## Conversions and formatting
- Explicit conversions support `double`, `float`, `int`, `long`, and `decimal`
in both directions. Integer inputs are exact. Decimal inputs use their exact
coefficient and scale to compute the high component and its residual.
- Integer casts truncate the complete expansion toward zero and throw
`OverflowException` for nonfinite or out-of-range results. `IConvertible`
integer conversions instead round to nearest, ties to even, with range checks.
- Binary32 output rounds the complete expansion directly, including low-component
decisions at midpoints. Decimal output rounds to the greatest fitting scale up
to 28; nonfinite values and magnitudes above `decimal.MaxValue` throw.
- `IConvertible` reports `TypeCode.Object`, supports conversion to itself, and
treats only numerical zero as false. Char, DateTime, and enum conversions are
unsupported and throw `InvalidCastException`.
- `ToString` formats the exact component sum, supports culture-sensitive
`G`/`g`, `E`/`e`, and `F`/`f`, and rounds ties to even. Precision is bounded to
0999; other standard and custom formats throw `FormatException`. Default
`G32` is **not** shortest-round-trip formatting. NaN, infinities, and signed
zero are supported without converting through decimal.
- `TryFormat(Span<char>, ...)` implements `ISpanFormattable` with the same formats.
It currently allocates via `ToString`; insufficient space returns `false`, writes
zero characters, and leaves the destination unchanged.
`DoubleDouble` implements `ISignedNumber<DoubleDouble>`, including the inherited
`INumberBase` contracts: binary radix, classification, absolute value, magnitude
selection, increment/decrement, and generic numeric conversions. Integer/parity
tests and magnitude comparisons retain both components. Magnitude ties prefer
positive values for maximum and negative values for minimum, including signed zero;
the `Number` variants prefer a number over NaN.
`CreateChecked`, `CreateSaturating`, and `CreateTruncating` support built-in numeric
types and `BigInteger`. Floating overflow produces signed infinity in all modes.
Finite integer output truncates the exact sum, then throws on overflow, clamps,
or retains the low destination-width bits, respectively. Decimal nonchecked output
clamps out-of-range values and maps NaN to zero. These policies are distinct from
the existing casts and `IConvertible` conversions above.
Conversions, parsing, and formatting use allocating `BigInteger` intermediates
where needed to preserve precision; no additional dependency is required.
## Parsing
`Parse` and `TryParse` accept strings and `ReadOnlySpan<char>` and implement
`IParsable<DoubleDouble>` / `ISpanParsable<DoubleDouble>`. This initial parser
preserves high/low precision rather than parsing through `double` or `decimal`.
It converts an exact decimal coefficient/exponent into rounded high and residual
components, then normalizes the pair. It does not promise universally correctly
rounded 106-bit results or a general `ToString` round trip.
```csharp
using System.Globalization;
using Just.PreciseMath;
DoubleDouble value = DoubleDouble.Parse("9007199254740993", CultureInfo.InvariantCulture);
// value.High == 9007199254740992.0; value.Low == 1.0
bool success = DoubleDouble.TryParse("1.25e-2".AsSpan(), CultureInfo.InvariantCulture,
out DoubleDouble parsed);
```
- Provider-only finite grammar: optional sign, ASCII decimal digits with an optional decimal
separator, and optional `e`/`E` exponent with sign and digits. At least one
mantissa digit is required; `.5` and `1.` are accepted with invariant culture.
Surrounding whitespace is allowed; internal whitespace is not.
- Signs and the decimal separator come from the supplied culture; a null or
omitted provider uses the current culture. Culture-specific NaN and infinity
symbols are recognized case-insensitively. The additional alias `inf` accepts an
optional culture-specific sign (`inf`, `+inf`, `-inf` with invariant culture).
Exact custom special symbols take precedence over the alias. Special values accept
surrounding whitespace and signs even with `NumberStyles.None`; ordinary finite
numbers still obey the supplied style flags. Signed zero is preserved.
- Provider-only overloads reject grouping, currency, and parentheses. Explicit
`NumberStyles` overloads support decimal flags through `NumberStyles.Any`, including
grouping, currency, parentheses, and trailing signs; group sizes are not validated.
Hexadecimal, binary, and undefined style flags throw `ArgumentException`, including
in `TryParse`. Hexadecimal notation and programming-language digit separators
remain unsupported.
- Input is limited to **2048 characters**, including surrounding whitespace.
Huge exponents are bounded before constructing powers of ten. Well-formed
overflow succeeds with signed infinity; underflow rounds to a subnormal or
signed zero. A second rounding just below the overflow midpoint stays finite.
- `Parse` throws `ArgumentNullException` for a null string and `FormatException`
for invalid, unsupported, or oversized input. `TryParse` returns `false` and
positive `Zero` for those inputs.
## Deferred scope
Natural `DDMath.Log`, the complete exponential family on `DoubleDouble`, and all
three `DDMath.Pow` overloads are implemented.
Logarithms in other bases, generic-math interfaces beyond `ISignedNumber`,
`IFloatingPointConstants`, `IRootFunctions`, and `IExponentialFunctions`, additional text formats/general
round-trip formatting, and non-arithmetic performance benchmarks remain deferred.
Replacing allocating arithmetic boundary fallbacks is also deferred; the current
`BigInteger` paths remain in place. That optimization does not require removing
`BigInteger` from conversions, parsing, formatting, or independent test oracles.
## Build and test
Requires the .NET 10 SDK in the `10.0.1xx` feature band, as selected by `global.json`.
Run from the repository root:
```sh
dotnet restore Just.PreciseMath.slnx --locked-mode
dotnet build Just.PreciseMath.slnx -c Release --no-restore
dotnet test --solution Just.PreciseMath.slnx -c Release --no-build --minimum-expected-tests 1
dotnet format Just.PreciseMath.slnx --verify-no-changes --no-restore
```
Test results and coverage reports are available in the `test-results` artifact
on CI workflow runs.
## Benchmarks
BenchmarkDotNet measures arithmetic throughput, dependent-chain latency, and allocations,
including comparisons of `DoubleDouble`, `decimal`, and `double`, mixed scalar operations,
and exponent-boundary paths. These are performance measurements, not accuracy tests.
After the Release build above, run from the repository root:
```sh
# Discover benchmark methods without running them.
dotnet run --project 2-benchmarks/Just.PreciseMath.Benchmarks -c Release --no-build -- --list flat
# Smoke test; Dry timings are not performance measurements.
dotnet run --project 2-benchmarks/Just.PreciseMath.Benchmarks -c Release --no-build -- --job Dry --filter '*'
# Full measurement run.
dotnet run --project 2-benchmarks/Just.PreciseMath.Benchmarks -c Release --no-build -- --filter '*'
```
Reports are written under the ignored `BenchmarkDotNet.Artifacts/` directory.
Replace `'*'` with a benchmark-name pattern to select a subset; use `--artifacts <path>`
to keep runs separate. Run measurements on an idle machine and inspect BenchmarkDotNet warnings.
## Project structure
- `0-source/Just.PreciseMath/`: library implementation.
- `1-tests/Just.PreciseMath.Tests/`: unit tests.
- `2-benchmarks/Just.PreciseMath.Benchmarks/`: arithmetic benchmarks against `decimal` and `double`.
## Contributing
Follow `.editorconfig` and include regression tests with numerical changes.
Explain the algorithm's assumptions, the source of reference values, and any
error tolerances. Include updated `packages.lock.json` files with dependency changes.
## License
Licensed under the [MIT License](LICENSE).