You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Logarithmic scales round-trip through double, so a precise storage type silently loses its precision #235
Every logarithmic scale is generic over its storage type, so Decibels<PreciseNumber> and PH<decimal> compile and read as precise. The conversions to and from their linear counterparts are not: they convert to double, call Math.Log* / Math.Pow, and convert back.
All eight generated scales are affected: Cents, Decibels, DirectionalityIndex, PH, Semitones, SoundIntensityLevel, SoundPowerLevel, SoundPressureLevel.
Why it matters now
This is a documented decision, not an oversight — CLAUDE.md says the logarithmic scales and the hand-written audio types still compute through double. It is worth revisiting because the premise changed twice since:
So a consumer of the Precise alias package gets exact foot-to-inch conversion and a 50-digit vector length, and — with no diagnostic and no difference in how the type is spelled — roughly 16 significant digits for anything that passes through a decibel or a pH. The alias package is precisely the context in which someone has declared they care about the error budget.
The same applies to decimal, which StorageConversionTests<decimal> otherwise holds to 1e-25.
Scope
Math.Log and Math.Pow over an arbitrary INumber<T> are a real piece of work, not a mechanical change, which is presumably why the original decision went the way it did. Options, roughly in increasing order of cost:
Document it at the call site. Cheapest, and stops it being a surprise: an XML <remarks> on each generated scale saying the conversion is computed in double whatever T is, so it shows in IntelliSense rather than only in CLAUDE.md.
Route through the widest available path. Use decimal where T is decimal, keeping double otherwise. Narrow benefit, small change.
Compute the log and the exponential in T's own arithmetic, the way StorageMath.Sqrt already does for roots — seed from the double result and refine. StorageMath is the obvious home, and Sqrt is the precedent for the shape.
Option 1 is worth doing regardless of whether 3 ever happens.
Verification
Whatever the fix, StorageConversionTests<T> is the natural place to pin it: it already runs the same conversions over double and decimal and takes one derived line per storage type, and a PreciseNumber derived class was added in #232.
Category: Improvement Priority: Medium Area:Semantics.SourceGenerators — LogarithmicScalesGenerator, eight generated scales
Medium: a documented decision rather than an oversight, but the premise it rested on has changed twice. #226 made unit factors and metric magnitudes exact per storage type, and #232 shipped ktsu.Semantics.Quantities.Precise, which binds every quantity in a project to PreciseNumber through a global-using alias. So a consumer of the Precise package now gets exact conversions and a 50-digit vector length, and roughly 16 significant digits for anything touching a decibel or a pH — with no diagnostic and no difference in how the type is spelled. The alias package is by definition the context where someone has declared they care about the error budget, which is what makes this worth revisiting rather than re-affirming.
Medium rather than High because it is silent precision loss in a bounded set of eight scales, not incorrect results, and decimal users are in the same position today.
Suggested assignment: none specific.
Related:ktsu-dev/PreciseNumber#81 is the same defect one layer down — Pow's non-integer path computes through Math.Log/Math.Exp on double and returns ~15 digits from a 50-digit type. Worth noting that option 3 here (compute log and exp in T's own arithmetic) substantially overlaps that work: if PreciseNumber grows real Exp/Log, the expensive part of option 3 for the most demanding storage type is already paid for. Whoever considers option 3 should check #81's status first — sequencing them the other way means writing it twice.
Open PR covering this: none.
Suggested next step: land option 1 now, independently of whether option 3 ever happens — an XML <remarks> on each generated scale saying the conversion computes in double whatever T is puts it in IntelliSense instead of only in CLAUDE.md, which is where a Precise consumer would actually encounter it. It is a generator change of a few lines and it converts a silent surprise into a stated contract.
Option 2 (decimal where T is decimal) is worth pricing but looks like the weakest of the three: narrow benefit, and it leaves PreciseNumber — the case that motivated the issue — exactly where it is. StorageConversionTests<T> is the right place to pin whatever lands, as the issue says, since it already runs these conversions across storage types and gained a PreciseNumber derived class in #232.
What happens
Every logarithmic scale is generic over its storage type, so
Decibels<PreciseNumber>andPH<decimal>compile and read as precise. The conversions to and from their linear counterparts are not: they convert todouble, callMath.Log*/Math.Pow, and convert back.Semantics.SourceGenerators/Generators/LogarithmicScalesGenerator.cs:151:and
:174:Which lands in committed generated source —
Semantics.Quantities/Generated/…/LogarithmicScalesGenerator/Decibels.g.cs:39-40:and
:49-50for the way back:All eight generated scales are affected:
Cents,Decibels,DirectionalityIndex,PH,Semitones,SoundIntensityLevel,SoundPowerLevel,SoundPressureLevel.Why it matters now
This is a documented decision, not an oversight — CLAUDE.md says the logarithmic scales and the hand-written audio types still compute through
double. It is worth revisiting because the premise changed twice since:Length()/Distance()compute roots in the storage type's own arithmetic.ktsu.Semantics.Quantities.Precise, which binds every quantity in a project toktsu.PreciseNumber.PreciseNumberthrough a global-using alias.So a consumer of the Precise alias package gets exact foot-to-inch conversion and a 50-digit vector length, and — with no diagnostic and no difference in how the type is spelled — roughly 16 significant digits for anything that passes through a decibel or a pH. The alias package is precisely the context in which someone has declared they care about the error budget.
The same applies to
decimal, whichStorageConversionTests<decimal>otherwise holds to1e-25.Scope
Math.LogandMath.Powover an arbitraryINumber<T>are a real piece of work, not a mechanical change, which is presumably why the original decision went the way it did. Options, roughly in increasing order of cost:<remarks>on each generated scale saying the conversion is computed indoublewhateverTis, so it shows in IntelliSense rather than only in CLAUDE.md.decimalwhereTisdecimal, keepingdoubleotherwise. Narrow benefit, small change.T's own arithmetic, the wayStorageMath.Sqrtalready does for roots — seed from thedoubleresult and refine.StorageMathis the obvious home, andSqrtis the precedent for the shape.Option 1 is worth doing regardless of whether 3 ever happens.
Verification
Whatever the fix,
StorageConversionTests<T>is the natural place to pin it: it already runs the same conversions overdoubleanddecimaland takes one derived line per storage type, and aPreciseNumberderived class was added in #232.