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 CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,15 @@ dotnet run -c Release --project PreciseNumber.Benchmarks -- --filter '*' --job s
- Roots (`PreciseNumber/PreciseNumber.Roots.cs`, satisfying `IRootFunctions<PreciseNumber>`) follow `Divide`'s precision rule and do not route through `double`. Each scales the significand by a power of ten until the degree divides the exponent, then takes an integer Newton root of the significand, so an exact root stops on the exact answer rather than on a tolerance and no seed has to survive a value outside `double`'s range
- Exponentials, logarithms and powers (`PreciseNumber/PreciseNumber.Exponentials.cs`, satisfying `IExponentialFunctions`, `ILogarithmicFunctions` and `IPowerFunctions`) follow the same precision rule and do not route through `double` either. `ln(m · 10^k)` is `ln m + k · ln 10` against the stored `Ln10`, with the mantissa centred on `[1/√10, √10)` and fed to the atanh series; `exp(v)` factors out `10^round(v / ln 10)` as an exponent shift and halves what is left before a Taylor sum. Nothing here is a free-standing decision: `Exp10`/`Log10` must not route through the natural log, because the exponent is the whole answer for a power of ten, and the `…M1`/`…P1` variants must not be computed as `Exp(x) - 1`/`Log(1 + x)`, because that cancels away the precision near zero they exist to keep
- A fractional `Pow` is `exp(y · ln x)`, carried wider by the integer digits of `y · ln x` because `Exp`'s range reduction consumes them. The integer path stays exponentiation by squaring and is exact; tests pin that exactness rather than a tolerance
- Hyperbolics (`PreciseNumber/PreciseNumber.Hyperbolics.cs`, satisfying `IHyperbolicFunctions`) are the exponentials and logarithms under other names and add no transcendental machinery of their own. Which of them needs its textbook form rearranged is narrower than floating-point habit suggests, and the reason is worth keeping straight: addition, subtraction and multiplication here are *exact*, so cancelling two nearly equal values costs nothing by itself. Digits are lost only by cancelling against something `Exp`, `Log`, `Sqrt` or `Divide` has **already** rounded to a working width. That is why `sinh` sums its own series below one half instead of taking `(e^x - e^-x)/2`, and why `asinh` and `atanh` subtract their one analytically and go through `LogP1` — each of those three otherwise returns about thirty correct digits from a fifty-digit type at `1e-30`, and the tests fail outright on them rather than drifting in the last place. `cosh`, `tanh` and `acosh` do **not** need it: `cosh` sums two positive terms, and the other two cancel only against operands nothing has rounded yet. `tanh` is still written as `-t/(2 + t)` with `t = expm1(-2x)`, but for range rather than precision — the negative exponent decays instead of growing, so it saturates to `±1` where `e^2x` would overflow — and `acosh` still factors the difference of squares, to keep a `2n`-digit intermediate out of the root. Don't simplify the first three back, and don't defend the last two on precision grounds
- The `sanitize` constructor parameter controls whether trailing zeros are removed (default: true)
- Constants (`Zero`, `One`, `Pi`, `E`, `Tau`) are pre-computed static instances
- As a value type it can't be null or inherited. Don't add null checks for `PreciseNumber` parameters, and don't reintroduce `protected` members
- Conversions to integer types go through `BigInteger`, so range checks, clamping, and wrapping follow its conventions. Conversions to `double`, `float`, `Half`, and `decimal` render normalized scientific notation (`d.ddd…E±n`) and parse it, because the runtime parsers round correctly, with Clinger's fast path for small values. Keep one digit before the point. The .NET 7 and 8 parsers clamp an exponent above 1000 and still offset it by every digit ahead of the point, so a long significand rendered as an integer parses as zero there. Conversions from `double`, `float`, and `Half` use the shortest text that round-trips (`"R"`). NaN and infinity coming in follow `BigInteger` too

### Test Structure

Tests use MSTest. `PreciseNumber.Test/PreciseNumberTests.cs` covers arithmetic, parsing, and formatting, `PreciseNumberConversionTests.cs` covers generic math conversion in every mode, `PreciseNumberRootTests.cs` pins the roots against published digits and against squaring back, `PreciseNumberExponentialTests.cs` does the same for the exponentials and logarithms and additionally pins the cases a `double` fallback cannot reach — fifty published digits of a fractional power, and `ExpM1`/`LogP1` of `1e-30` not collapsing to zero — and `PreciseNumberValueTypeTests.cs` pins `default` as zero and asserts that small-value addition, subtraction, multiplication, and comparison allocate nothing. The test project targets only .NET 10.0 while the main library multi-targets net7.0, net8.0, net9.0, and net10.0.
Tests use MSTest. `PreciseNumber.Test/PreciseNumberTests.cs` covers arithmetic, parsing, and formatting, `PreciseNumberConversionTests.cs` covers generic math conversion in every mode, `PreciseNumberRootTests.cs` pins the roots against published digits and against squaring back, `PreciseNumberExponentialTests.cs` does the same for the exponentials and logarithms and additionally pins the cases a `double` fallback cannot reach — fifty published digits of a fractional power, and `ExpM1`/`LogP1` of `1e-30` not collapsing to zero — `PreciseNumberHyperbolicTests.cs` pins the hyperbolics against published digits and against `cosh²x - sinh²x = 1`, and separates the small-argument assertions that actually discriminate (`sinh`, `asinh`, `atanh`) from the two that read like they do and don't (`tanh`, `acosh`) — the class remark records which is which, so the distinction survives the next person to read it — and `PreciseNumberValueTypeTests.cs` pins `default` as zero and asserts that small-value addition, subtraction, multiplication, and comparison allocate nothing. The test project targets only .NET 10.0 while the main library multi-targets net7.0, net8.0, net9.0, and net10.0.

### Benchmarks

Expand Down
105 changes: 105 additions & 0 deletions PreciseNumber.Benchmarks/HyperbolicBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.PreciseNumber.Benchmarks;

using BenchmarkDotNet.Attributes;

/// <summary>
/// Measures the hyperbolic functions and their inverses.
/// </summary>
/// <remarks>
/// None of these carries a series of its own except <c>Sinh</c> below one half, so read them against
/// <see cref="ExponentialBenchmarks"/> rather than against each other in isolation: each should cost
/// about what the exponential or logarithm underneath it costs, plus the arithmetic named below. A
/// row that is several times its underlying transcendental is doing work it should not be.
/// <para>
/// <see cref="SinhOfASmallValue"/> against <see cref="SinhOfAValue"/> is the pair worth watching.
/// The small case sums its own series and the larger one takes an exponential and a reciprocal, so
/// the two are different algorithms rather than the same one at different arguments. The series
/// should win at every point on the <c>Digits</c> axis — it exists for precision rather than speed,
/// but it would be worth knowing if it cost more than the path it replaces.
/// </para>
/// <para>
/// <see cref="CoshOfAValue"/> is one exponential, one reciprocal and one halving, which makes it the
/// floor for this file. <see cref="TanhOfASaturatedValue"/> is the opposite end: it returns
/// <c>±1</c> without taking an exponential at all, so it should be near free and should not move
/// with the <c>Digits</c> axis. Anything else there means the saturation test is not firing.
/// </para>
/// </remarks>
[MemoryDiagnoser]
public class HyperbolicBenchmarks
{
private PreciseNumber value = PreciseNumber.Zero;
private PreciseNumber smallValue = PreciseNumber.Zero;
private PreciseNumber unitInterval = PreciseNumber.Zero;
private PreciseNumber aboveOne = PreciseNumber.Zero;
private PreciseNumber saturated = PreciseNumber.Zero;

/// <summary>
/// Gets or sets the number of significant digits in the operand, and so in the answer.
/// </summary>
[Params(8, 30, 200)]
public int Digits { get; set; }

/// <summary>
/// Prepares the operands.
/// </summary>
[GlobalSetup]
public void Setup()
{
// A little over one, so Sinh takes the exponential path rather than the series.
value = Operands.Number(Digits, -(Digits - 1));

// Well under one half, so Sinh sums its series instead.
smallValue = Operands.Number(Digits, -(Digits + 1), offset: 13);

// In (0, 1), the domain of Atanh.
unitInterval = Operands.Number(Digits, -Digits, offset: 29);

// Above one, the domain of Acosh.
aboveOne = PreciseNumber.One + Operands.Number(Digits, -(Digits - 1), offset: 5);

// Far enough out that Tanh returns without taking an exponential.
saturated = Operands.Number(Digits, -(Digits - 4), offset: 17);
}

/// <summary>Takes the hyperbolic sine, by way of an exponential and a reciprocal.</summary>
/// <returns>The result.</returns>
[Benchmark(Baseline = true)]
public PreciseNumber SinhOfAValue() => PreciseNumber.Sinh(value);

/// <summary>Takes the hyperbolic sine of a small value, which sums its own series.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber SinhOfASmallValue() => PreciseNumber.Sinh(smallValue);

/// <summary>Takes the hyperbolic cosine, which is the cheapest thing here.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber CoshOfAValue() => PreciseNumber.Cosh(value);

/// <summary>Takes the hyperbolic tangent, which is one exponential and a division.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber TanhOfAValue() => PreciseNumber.Tanh(value);

/// <summary>Takes the hyperbolic tangent of a value past saturation, which takes no exponential.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber TanhOfASaturatedValue() => PreciseNumber.Tanh(saturated);

/// <summary>Takes the inverse hyperbolic sine, which is a root and a logarithm.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber AsinhOfAValue() => PreciseNumber.Asinh(value);

/// <summary>Takes the inverse hyperbolic cosine, which is a root and a logarithm.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber AcoshOfAValue() => PreciseNumber.Acosh(aboveOne);

/// <summary>Takes the inverse hyperbolic tangent, which is a division and a logarithm.</summary>
/// <returns>The result.</returns>
[Benchmark]
public PreciseNumber AtanhOfAValue() => PreciseNumber.Atanh(unitInterval);
}
Loading
Loading