Skip to content

Add Ziv loop for guaranteed rounding#92

Open
cmpute wants to merge 16 commits into
masterfrom
ziv
Open

Add Ziv loop for guaranteed rounding#92
cmpute wants to merge 16 commits into
masterfrom
ziv

Conversation

@cmpute

@cmpute cmpute commented Jul 19, 2026

Copy link
Copy Markdown
Owner

No description provided.

Jacob Zhong and others added 16 commits July 18, 2026 23:37
Migrate the remaining real transcendentals to guaranteed-correct
rounding via the Ziv retry loop (`Context::ziv`/`ziv_pair`):

- Trig (sin, cos, sin_cos, tan, asin, acos, atan, atan2): extract
  near-correct `_compute` series cores and wrap them in Ziv. The
  argument reduction folds a `|k|·ulp(π/2)` reduction-error term into
  the radius so the containment test stays sound for huge `|x|`.
- Hyperbolic (sinh, cosh, sinh_cosh, tanh, asinh, acosh, atanh):
  composition-based, treating the Ziv-correct `exp_m1`/`ln_1p` as
  black boxes; the unreachable exp-overflow case is hoisted via a
  shared `exp_overflows` probe (closures can't return `Err`).
- `hypot`: composition-based (sqrt attenuates), small-constant radius.

`sin_cos`/`sinh_cosh` certify both halves via the new `ziv_pair`
driver. `powf` is left near-correct — its `exp(y·ln x)` amplification
makes the data-dependent radius converge poorly for large results, so
a dedicated treatment is deferred.

Bound propagation: the migrated `Context`/`FBig`/`CachedFBig` methods
move to `R: ErrorBounds`; dashu-cmplx's complex `sqrt`/`norm`/`abs`/
`arg` follow (they route through `hypot`/`atan2`).

Tests: 20 exact-oracle soundness tests (`f(x)@p == f(x)@2p re-rounded`)
covering every migrated function; existing trig/hyper/exp suites pass.
Docs: float CHANGELOG, V1-ROADMAP, and bilingual compliance/faq updated.

Co-Authored-By: Claude <noreply@anthropic.com>
…to powi

powf with a non-integer exponent is now correctly rounded via the Ziv loop.
The fix for the previously-deferred amplification problem: the error radius is
result.ulp()*(|y*ln x|+1)*(B+8) taken at the *working* precision, so it shrinks
as B^{-guard} and the containment test converges. A radius computed at unlimited
precision is constant across retries and never settles for a value near a
rounding boundary, which is why the earlier attempt infinite-retried.

An integer-valued exponent now delegates to powi (binary exponentiation), gated
on Repr::is_int. This also admits a negative base (sign fixed by the exponent's
parity) -- the old OutOfDomain TODO. That path is near-correct (<=1 ulp),
matching powi. The exp overflow case is hoisted out of the Ziv closure.

Also: Repr::is_int is now const (pure exponent/sentinel check).

Co-Authored-By: Claude <noreply@anthropic.com>
acos(1) = pi/2 - asin(1) cancels to exactly 0, but the composition carries a
positive radius. Under a directed rounding mode (Down/Zero/Up) the preimage of 0
is one-sided ([0, ulp)), so the Ziv containment test can never certify it -- any
positive radius dips the interval below 0 -- and it infinite-retried. This was
the signed_zero.rs full-file hang: test_asin_acos_unit_under_down hung on
acos(1) under Down.

Short-circuit x = 1 to the exact 0 before the Ziv loop, matching asin's existing
|x| = 1 special case. (asin(+-1) = +-pi/2 and acos(-1) = pi are nonzero, so they
were already fine.)

Co-Authored-By: Claude <noreply@anthropic.com>
A scan of every transcendental at its exact-representable special inputs under
Down/Up/Zero turned up two more instances of the acos(1) hang (a transcendental
whose true value is exactly representable carries a positive radius that can't be
certified against the value's one-sided preimage, so Ziv infinite-retries):

- asin(0) = 0: asin lacked the x=0 short-circuit that asinh/atanh/sin/cos/... all
  have, so it computed atan(0)=0 with a positive radius and infinite-retried.
  Now short-circuits to +-0 (asin is odd), matching the rest of the family.
- acos(-1) = pi: short-circuited for symmetry with acos(1).

The scan confirmed no other transcendentals hang (sqrt of perfect squares,
exp(0), cos(0), cosh(0), powf(x,0), nth_root of exact roots, etc. are all already
special-cased or detect exact results). Remaining: hypot of an exact Pythagorean
triple (e.g. hypot(3,4)=5) -- same containment class but no clean single special
case, plus a dashu-int NTT crash hit during the retry; tracked separately.

Co-Authored-By: Claude <noreply@anthropic.com>
add_signed_sqr_conv and add_signed_mul_conv assumed a non-zero input
(debug_assert!(la_bits > 0)); an all-zero slice panicked in debug and indexed
out of bounds in release. sqrt_rem of a perfect square with many trailing zero
words squares the estimate's all-zero low half to verify the remainder, so this
was reachable -- and was the crash behind dashu-float's hypot(3,4) under directed
rounding (the Ziv retry drove sqrt to a precision where the scaled significand
hit the NTT path).

An all-zero operand now returns early: the product is zero, so c += sign*a*b (or
a^2) is a no-op. This matches the existing zero-guard in the chunked-multiply
closure. Regression test: sqrt_rem of (1<<560000)^2.

Co-Authored-By: Claude <noreply@anthropic.com>
The float changelog's hypot note said the underlying dashu-int NTT crash was
"tracked separately"; that crash is now fixed (a482d7e), so update the note to
reflect that hypot(exact) still infinite-retries on the containment issue, but
no longer crashes.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the ratio form (large * sqrt(1 + (small/large)^2)) with the scaled
direct form sqrt(large^2 + small^2), tracking each step's Exact/Inexact flag
(MPFR's `exact` flag in hypot.c). When the whole chain is exact -- which holds
for every Pythagorean-triple input -- the closure reports radius 0, which ziv
accepts without the containment test. This is the only way to terminate an
exactly-representable result under directed rounding, where the value sits on a
one-sided preimage boundary.

The ratio form broke this for triples like hypot(5,12)=13: 5/12 is a
non-terminating binary fraction, so div returned Inexact even though the final
result 13 is exact. The direct form has no division, so sqr/add/sqrt are all
exact for integer inputs -- mirroring how MPFR avoids a division precisely so
its exact flag works for every triple.

Both operands are scaled down by k base-B digits before squaring (k chosen so
large^2 can't overflow the exponent) and the root scaled back, exactly MPFR's
sh. Regression tests: hypot of (3,4)/(5,12)/(8,15) under Down/Up/Zero all
return the exact integer.

Co-Authored-By: Claude <noreply@anthropic.com>
- tan: drop the hoisted pole check. It re-evaluated the sin/cos series a
  second time on top of the first Ziv attempt (~2x per call), and guarded a
  condition that can't occur: near a pole the value is large but finite, and
  dashu's wide exponent range holds it (sign carried by the arithmetic,
  s/-|c| is negative); the reduced argument cancels to exactly zero only
  when the input equals the work-precision pi/2, which the trig guard
  inflation rules out. The closure's significand-zero retry guard handles
  that unreachable case. Matches MPFR's structure (no pole special-case).
- Dedup helpers: Context::base_guard_digits (ceil(log_B(precision))) across
  the 12 transcendental Ziv guard sites; series_radius (ulp*(4*terms+12))
  across the sin/cos/sin_cos/atan/ln series cores; merge the two textually
  identical CachedFBig forwarding macros.

Co-Authored-By: Claude <noreply@anthropic.com>
Centralize the unlimited-precision check in Context::guard (the guard-digit
recipe is an inherently limited-precision technique, so guard rejects
precision 0 rather than silently making a finite 0+GUARD context). All
transcendentals that build a guard context via guard -- exp, log, abs, sqrt,
and complex sin/cos/tan/sin_cos -- now panic on an unlimited context as
documented (previously they silently computed at the fixed guard precision
and marked the truncated value Exact). asin/acos/atan and powf build their
work context directly (Context::new(p+GUARD), bypassing guard) and so assert
explicitly. The exact special-value shortcuts still bypass the check.

Arithmetic switches from guard to a new Context::work_context that uses the
exact self.float() (precision 0) at unlimited: mul/sqr/norm are now exact
there (were guard-rounded, a latent bug); div/inv panic via dashu-float's
div (a quotient isn't exactly representable).

The check is kept private to dashu-cmplx (a pub(crate) helper) rather than
re-exported from dashu-float.

Co-Authored-By: Claude <noreply@anthropic.com>
Wrap dashu-cmplx's complex transcendentals (exp, log, powf, sin/cos/tan/
sin_cos, asin/acos/atan, sqrt) in a Ziv retry loop that certifies *both*
the real and imaginary parts, matching dashu-float's real transcendentals.

The driver lives in a new complex/src/ziv.rs, reimplemented in dashu-cmplx
rather than reusing dashu-float's pub(crate) ziv/ziv_pair (invisible across
crates). Two complex-specific choices vs float's driver:
- const-generic part count N (2 for one complex result, 4 for sin_cos, which
  shares one sin_cos/sinh_cosh evaluation across sin and cos);
- a *fallible* closure |guard| -> Result<[((v,e)); N], FpError>, so overflow/
  domain propagate via ? with no hoisted per-function probe and no closure
  unwraps. The per-part contained test runs on FBig at FloatCtxt::new(0)
  (public-API exact arithmetic).

Each transcendental reports a provable per-part radius (result.ulp() * C,
plus an amplification term where composition magnifies error: log's ln|z|
near |z|=1, tan's real part for large |Im z|, powf's result magnitude).
abs delegates straight to the (already correctly-rounded) real hypot,
dropping a double-rounding re-round. The dead Context::guard() is removed;
the driver asserts limited precision, keeping the panic-at-unlimited contract.

Known limitation: tan for large |Im z| and asin/acos/atan right at their
singularities lose accuracy (formula cancellation); the well-conditioned
regime is guaranteed-correctly rounded. tan's cancellation is fixed by the
double-angle formula in a follow-up.

Self-oracle tests tightened 2-4 -> 1 ULP; added tan/powf/abs oracles, a
retry-count test, and an overflow-propagation test.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace tan's sin(z)/cos(z) (computed through the complex div, which
catastrophically cancels in the real part for large |Im z| — sin and cos
are both ~cosh(y)-scale, so sin·conj(cos) cancels from ~cosh²y down to
O(1)) with the double-angle identity

  tan(x+iy) = (sin 2x + i sinh 2y) / (cos 2x + cosh 2y).

The denominator cos 2x + cosh 2y is a sum of a bounded term and one >= 1,
so it never cancels; the result is accurate for all finite |Im z|, and a
plain result.ulp() * C radius is sound (the B^{1-pw} workaround and the
large-|Im z| caveat are both gone). The only small-denominator points are
the real-axis poles (y = 0, x = pi/2 + k pi), where the large value is
genuine.

tan now operates on the float parts (FloatCtxt) directly, like exp/log/
sqrt, rather than driving whole-CBig sin_cos + div. Broadened the
tan_self_oracle to a moderate-large |Im z| strategy (up to ~40) and added
a tan_large_imaginary_is_near_i test. Docs/changelog updated to drop tan
from the limitation list.

Co-Authored-By: Claude <noreply@anthropic.com>
The LAST_ZIV_RETRIES thread_local! (used by the retry-count tests) needs
std — thread_local! isn't in scope under --no-default-features, so both
float/src/ziv.rs and complex/src/ziv.rs failed to compile in the no_std CI
job (cargo test --no-default-features --features rand).

Gate the counter, its loop uses, and the counter-reading tests behind
feature = "std" (#[cfg(all(test, feature = "std"))]). The Ziv driver itself
is now no_std-clean; the retry-count tests still run under std as before.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

1 participant