Skip to content

Implement IHyperbolicFunctions<PreciseNumber> [minor] - #94

Merged
matt-edmondson merged 3 commits into
mainfrom
claude/precisenumber-78-hyperbolic-functions
Sep 23, 2026
Merged

matt-edmondson merged 3 commits into
mainfrom
claude/precisenumber-78-hyperbolic-functions

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #78

IHyperbolicFunctions was the one piece of #78 still outstanding — the issue called it "cheap once exp/log land", and #80, #81 and #82 have all since closed. Sinh, Cosh, Tanh, Asinh, Acosh and Atanh, each with the (value, int significantDigits) overload the rest of the file establishes, none routing through double. That closes the set the issue opened: PreciseNumber now satisfies every generic-math interface it listed as missing.

The part worth reviewing

These are the exponentials and logarithms under other names, so almost nothing here is new machinery — one series, and otherwise Exp, ExpM1, LogP1 and Sqrt. The interesting question was which of the six need their textbook form rearranged, and the answer is narrower than floating-point habit suggests.

Add, subtract and multiply are exact on this type. So a difference of two nearly equal values loses nothing by itself. Digits are lost only by cancelling against a value that Exp, Log, Sqrt or Divide has already rounded to a working width.

Three fail that test, and are rearranged:

textbook form why it loses
sinh (e^x - e^-x)/2 both exponentials arrive rounded, then cancel to something of order x
asinh ln(x + √(x² + 1)) the root arrives rounded
atanh ½ ln((1 + x)/(1 - x)) the quotient arrives rounded

So sinh sums its own series below one half, and both inverses subtract their one analytically and hand the remainder to LogP1.

Three pass it as written, and are not rearranged for precision. cosh sums two positive terms and has nothing to cancel at all. tanh is still written -t/(2 + t) with t = expm1(-2x) — but for range, not precision: the negative exponent decays instead of growing, so it saturates to ±1 where e^2x would overflow. acosh still factors the difference of squares — but to keep a 2n-digit intermediate out of the root, not to save digits.

I flag this because I originally wrote it up the other way round, claiming all five non-cosh forms were precision fixes. Reverting each one to its textbook form to check disproved two of them, and the code comments, the tests and CLAUDE.md now say so explicitly rather than carrying a tidier story that isn't true.

Confirmed the tests depend on the change

Substituted the textbook form back in, one function at a time, and re-ran. 8 of 26 fail, and the three near-zero assertions fail outright rather than drifting in the last place — at 1e-30 the textbook forms return about thirty correct digits from a fifty-digit type:

TestSinhKeepsItsDigitsNearZero
  expected 1e-30, got 0.0000000000000000000000000000009999999999999999999999999999995
TestAsinhKeepsItsDigitsNearZero    — same shape
TestAtanhKeepsItsDigitsNearZero    — same shape
TestTanhSaturatesRatherThanOverflowing — OverflowException at 1e15

The other two near-zero tests — TestTanhKeepsItsDigitsNearZero and TestAcoshKeepsItsDigitsJustAboveOne — passed on the textbook forms. They're kept as ordinary regression assertions, and both their doc comments and the class remark now say they don't police those two functions, so nobody later mistakes them for a guard they aren't.

Two tests I had to correct rather than satisfy

Both were my assertions over-reaching, not implementation defects, and both are now documented at the test:

  • Acosh(Cosh(x)) at x = 1e-30. cosh is flat at zero: cosh(1e-30) - 1 = 5e-61, below any working precision short of 61 digits. Once cosh has rounded to exactly one, no acosh can recover the argument. The round trip now skips the sub-0.25 entries.
  • cosh²x - sinh²x = 1 at x = 100. Both are ~1.34e43, so their squares are ~1.8e86 and a difference of one first appears at the 87th significant digit. At 50 digits the identity is correctly zero. Now checked at 140.

Tests

Full suite green: 376 total, 0 failed (350 before, 26 added). Release build clean, zero warnings, all four TFMs.

Reference digits were computed at 80–120 digits in Python's decimal — an independent implementation — from each function's closed form (asinh 1 = ln(1 + √2), acosh 2 = ln(2 + √3), atanh ½ = ½ ln 3), and rounded half-away-from-zero to match ReduceSignificance rather than truncated.

Also covered: cosh² - sinh² = 1 and tanh = sinh/cosh across a sweep straddling the series boundary, the three round trips, oddness and evenness, exactness at zero, domain rejections (Acosh below one, Atanh at and beyond ±1), precision rejection below one digit, and agreement with Math.Sinh/Cosh/Tanh/Asinh/Acosh/Atanh to 15 digits as the cheap regression net.

Also

HyperbolicBenchmarks on the repo's Digits axis (8/30/200) with allocation reported, per the issue's acceptance. Dry-run to confirm every operand is in domain; not measured for real here, since a shared CI container isn't a number worth recording. CLAUDE.md gains the design-pattern entry and the test-structure line.

Not covered

Sinh's series boundary is pinned by the identity holding on both sides of it, not by a test asserting which path ran — so a change that moved or removed DirectSeriesLimit would be caught only if it moved far enough to cost digits at 1e-30.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TQLCwRG4ViVx3uEQ23aVMW


Generated by Claude Code

matt-edmondson and others added 3 commits September 22, 2026 13:38
Completes the generic-math interface set tracked by #78. Sinh, Cosh, Tanh,
Asinh, Acosh and Atanh, each with the (value, int significantDigits) overload
the rest of the file establishes, and none of them routing through double.

These are the exponentials and logarithms under other names, so they add no
transcendental machinery of their own beyond one series. Which of them needs
its textbook form rearranged is narrower than floating-point habit suggests,
and that was measured rather than assumed: add, subtract and multiply are
exact here, 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.

Three fail that test and are rearranged. sinh sums its own series below one
half instead of taking (e^x - e^-x)/2; asinh and atanh subtract their one
analytically and hand the remainder to LogP1. Substituting the textbook form
back in returns about thirty correct digits from a fifty-digit type at 1e-30,
so the three near-zero tests fail outright on them rather than drifting.

Three pass it as written. cosh sums two positive terms and has nothing to
cancel. tanh is still -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, which is what keeps Tanh(1e15)
from throwing. acosh still factors the difference of squares, to keep a
2n-digit intermediate out of the root. Both are documented as such rather
than as precision fixes, and the two tests that look like they police them
say plainly that they do not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQLCwRG4ViVx3uEQ23aVMW
MSTEST0037, on the new hyperbolic tests. Assert.IsTrue(a >= b) reports only
"expected true, got false" when it fails; the comparison asserts report both
operands. The repo already uses them in the root, constant and value-type
tests, so this was the odd one out rather than a new convention.

Sonar flagged one of the three sites. Fixing only that one would have left
the same pattern inconsistent within one file, so all three move: the
tolerance check in AssertAgreesTo, the strict-range check on Tanh(20), and
the precision check the analyzer named.

Argument order on these is (bound, value), which is easy to invert into an
assertion that passes for the wrong reason, so each was re-checked by
inducing a failure it had to catch: comparing Sinh against Math.Cosh, asking
Tanh(20) to be below zero, and demanding 500 more digits than Sinh returns.
All three failed as they should.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQLCwRG4ViVx3uEQ23aVMW
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PreciseNumber implements INumber&lt;T&gt; and nothing else, so there is no sqrt, exp, log or trig

2 participants