Skip to content

Make StorageMath public, and add Cbrt, RootN and Hypot - #247

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/public-storage-math-239
Sep 17, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/public-storage-math-239

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #239

StorageMath.Sqrt is the piece of this library that System.Numerics does not offer: a root at the precision of an arbitrary INumber<T>, with no IRootFunctions<T> constraint — which is the whole trick, since decimal could not have satisfied that constraint anyway. It was internal, and the generated quantities were its only callers, so an application computing a norm the generator does not emit had to reimplement it.

What changed

The class and Sqrt are public, with what they guarantee written into the XML docs rather than left to be discovered, since this is the point at which the shape freezes:

  • MaximumIterations = 256, and the ArithmeticException on a root that does not settle
  • the double round trip for the binary floating point and integer primitives — Sqrt<double> is Math.Sqrt, not a refinement of it
  • the floor for integer types
  • OverflowException rather than NaN for a negative value on a type that has no NaN

IsRoundedThroughDouble, Seed and TryRootThroughDouble stay private, as the issue asks. This takes option 1 from the issue's "one thing to agree before merging" — a public static class with static methods, the smallest diff and the shape that is already there. Option 3, an IStorageMath<T> a caller could substitute, is deliberately not taken here: it is worth having only once there is a higher-precision root to inject (ktsu-dev/PreciseNumber#80), and it can be added later over these methods without them becoming a second way to do the same thing. Say the word if you would rather have it now, before this ships.

Cbrt, RootN and Hypot ship alongside, on the same seeding and the same loop, since they are what a caller asks for next:

  • RootN refines by x = (((n - 1) * x) + (value / x^(n - 1))) / n; a value outside the range of double is scaled by powers of 2^n into [1, 2^n) for its seed, the same trick Sqrt plays with powers of four.
  • Cbrt and RootN take the double route only for the binary floating point primitives, and refine every integer type in integer arithmetic. So their floor is exact rather than whatever Math.Pow happened to round to — Cbrt(27) == 3, not 2.
  • Every power is taken checked, because a narrow type can hold a value whose estimate^(n-1) it cannot, and an estimate that wrapped would send the steps somewhere arbitrary. A degree so high that the type cannot hold 2^n is answered before any of that: no value the type holds has a root of two or more.
  • Hypot computes a fractional type as larger * Sqrt(1 + (smaller / larger)²), so a pair whose squares leave the range of the type still has its hypotenuse — Hypot(1e20m, 1e20m) answers where 1e20m * 1e20m throws. An integer type squares and sums directly, since the ratio of two integers is not a ratio.

Exp, Log and trig stay out, as the issue scopes them: they need a series and belong to the storage type.

Sqrt itself is untouched — visibility and documentation only — so every existing quantity result is unchanged.

Tests

StorageMathTests grows 13 test methods, and the class is now 43 tests:

  • TheRootsArePublicAndTheirWorkingsAreNot asserts the surface in both directions — the four methods public, the nine private helpers not. Making the class internal again fails it; the rest of the new tests do not compile at all without the change.
  • the primitives are asserted against Math.Cbrt and double.Hypot directly, so a change to their results fails here
  • decimal is asserted at its own precision (cubing the refined root lands nearer 2 than cubing the double one), and on exact cubes including negative ones
  • BigInteger well beyond the range of double: Cbrt(10^300), RootN(10^500, 5), and the neighbours either side that pin the floor
  • the narrow-type edges: RootN(100, 20), RootN(int.MaxValue, 40), and a degree below one throwing

Verification

  • dotnet build over the solution: clean, no warnings (this repo builds warnings as errors, across net8.0net10.0)
  • full suite: 1274 tests, 0 failed (8 skipped, all Windows-only path tests)
  • reverting StorageMath.cs and rebuilding fails the new tests, as above

CLAUDE.md, docs/physics-generator.md and the package README record the new surface.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A9Ke7x6EeGyKHfPHncbNrP


Generated by Claude Code

StorageMath.Sqrt is the piece of this library that System.Numerics does
not offer: a root at the precision of an arbitrary INumber<T>, with no
IRootFunctions<T> constraint, which decimal could not have satisfied
anyway. It was internal, so an application doing its own vector math over
quantities — a norm the generator does not emit — had to reimplement it.

The class and Sqrt are now public, with what they guarantee written down
rather than left to be discovered: the 256-step cap and the
ArithmeticException on a root that does not settle, the double round trip
for the primitives, the floor for integers, and OverflowException rather
than NaN for a negative value on a type with no NaN. The seeding, the
double round trip and the Newton loop stay private.

Cbrt, RootN and Hypot ship alongside, on the same seeding and the same
loop, since they are what a caller asks for next. Sqrt itself is
untouched, so every existing result is unchanged. Cbrt and RootN take the
double route only for the binary floating point primitives and refine
every integer type in integer arithmetic, so their floor is exact rather
than whatever Math.Pow rounded to; each power is taken checked, so a
narrow type reports the overflow instead of wrapping into an estimate
that goes nowhere. Hypot computes a fractional type from the ratio of its
legs, so a pair whose squares leave the type still has its hypotenuse.

StorageMathTests pins both halves of the surface — the four methods
public, the workings not — so the reason this is public does not erode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9Ke7x6EeGyKHfPHncbNrP
RootByNewton carried both loops in one method, switching on whether the
type's division floors, which Sonar reads as cognitive complexity 19
against a limit of 15 — and it is right that the two have little to say
to each other. RootByDescent stops on the first step that does not
descend and answers with the estimate before it; RootBySettling stops
when the estimate stops changing, or takes the smaller of a pair it
alternates between. Each now says only its own rule.

No behaviour changes: same seed, same step, same checked powers, and the
suite still runs 1274 tests green.

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

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 4e7217e into main Sep 17, 2026
13 checks passed
@matt-edmondson
matt-edmondson deleted the claude/public-storage-math-239 branch September 17, 2026 00:06
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.

StorageMath.Sqrt&lt;T&gt; is internal, so nothing outside the assembly can take a root at the storage type's precision

2 participants