dashu_float/root.rs
1use dashu_base::{
2 ring::DivRem, Approximation, CubicRoot, EstimatedLog2, Sign, SquareRoot, SquareRootRem,
3 UnsignedAbs,
4};
5use dashu_int::{IBig, UBig};
6
7use crate::{
8 ball::Ball,
9 error::{assert_limited_precision, panic_root_zeroth, FpError, FpResult},
10 fbig::FBig,
11 repr::{Context, Repr, Word},
12 round::{mode, ErrorBounds, Round, Rounding},
13 utils::{digit_len, shl_digits, split_digits_ref},
14};
15use core::cmp::Ordering;
16
17impl<R: ErrorBounds, const B: Word> SquareRoot for FBig<R, B> {
18 type Output = Self;
19 #[inline]
20 fn sqrt(&self) -> Self {
21 self.context.unwrap_fp(self.context.sqrt(self.repr()))
22 }
23}
24
25impl<R: Round, const B: Word> CubicRoot for FBig<R, B> {
26 type Output = Self;
27 #[inline]
28 fn cbrt(&self) -> Self {
29 self.context.unwrap_fp(self.context.cbrt(self.repr()))
30 }
31}
32
33impl<R: Round, const B: Word> FBig<R, B> {
34 /// Calculate the nth root of the floating point number.
35 ///
36 /// When `n` is large the computation can be expensive — the significand is
37 /// aligned to up to `n · precision` digits before the integer root is taken, and
38 /// the integer Newton iteration works with numbers of that size. For large
39 /// `n` consider [`powf`][`FBig::powf`] with a rational exponent `1 / n`
40 /// as a faster approximate alternative.
41 ///
42 /// # Examples
43 ///
44 /// ```
45 /// # use core::str::FromStr;
46 /// # use dashu_base::ParseError;
47 /// # use dashu_float::DBig;
48 /// let a = DBig::from_str("16")?;
49 /// assert_eq!(a.nth_root(4), DBig::from_str("2")?);
50 /// # Ok::<(), ParseError>(())
51 /// ```
52 ///
53 /// # Panics
54 ///
55 /// Panics if `n` is zero, or if `n` is even and the number is negative.
56 #[inline]
57 pub fn nth_root(&self, n: usize) -> Self {
58 self.context
59 .unwrap_fp(self.context.nth_root(n, self.repr()))
60 }
61}
62
63impl<R: Round> Context<R> {
64 /// Calculate the cubic root of the floating point number.
65 ///
66 /// # Examples
67 ///
68 /// ```
69 /// # use core::str::FromStr;
70 /// # use dashu_base::ParseError;
71 /// # use dashu_float::DBig;
72 /// use dashu_base::Approximation::*;
73 /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
74 ///
75 /// let context = Context::<HalfAway>::new(2);
76 /// let a = DBig::from_str("8")?;
77 /// assert_eq!(context.cbrt(&a.repr()), Ok(Exact(DBig::from_str("2")?)));
78 /// # Ok::<(), ParseError>(())
79 /// ```
80 ///
81 /// # Panics
82 ///
83 /// Panics if the precision is unlimited.
84 #[inline]
85 pub fn cbrt<const B: Word>(&self, x: &Repr<B>) -> FpResult<FBig<R, B>> {
86 self.nth_root(3, x)
87 }
88
89 /// Calculate the nth root of the floating point number.
90 ///
91 /// # Examples
92 ///
93 /// ```
94 /// # use core::str::FromStr;
95 /// # use dashu_base::ParseError;
96 /// # use dashu_float::DBig;
97 /// use dashu_base::Approximation::*;
98 /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
99 ///
100 /// let context = Context::<HalfAway>::new(2);
101 /// let a = DBig::from_str("27")?;
102 /// assert_eq!(context.nth_root(3, &a.repr()), Ok(Exact(DBig::from_str("3")?)));
103 /// # Ok::<(), ParseError>(())
104 /// ```
105 ///
106 /// # Panics
107 ///
108 /// Panics if `n` is zero, if the precision is unlimited, or if `n` is even and `x` is negative.
109 pub fn nth_root<const B: Word>(&self, n: usize, x: &Repr<B>) -> FpResult<FBig<R, B>> {
110 if x.is_infinite() {
111 return Err(FpError::InfiniteInput);
112 }
113 assert_limited_precision(self.precision);
114 if n == 0 {
115 panic_root_zeroth()
116 }
117 debug_assert!(n < isize::MAX as usize);
118 let sign = x.sign();
119 if sign == Sign::Negative && n % 2 == 0 {
120 return Err(FpError::OutOfDomain);
121 }
122 if n == 1 {
123 return Ok(self.repr_round_ref(x).map(|v| FBig::new(v, *self)));
124 }
125 if x.significand.is_zero() {
126 // UBig::ZERO.nth_root(n) erroneously returns ONE, so short-circuit here.
127 // An even root of -0 already errored above, so reaching here the sign is
128 // preserved: odd root of ±0 is ±0.
129 return Ok(Approximation::Exact(FBig::new(x.clone(), *self)));
130 }
131
132 // operate on the magnitude so that shifting/splitting keep a clean sign;
133 // the original sign is re-applied to the result at the end.
134 let xmag: IBig = if sign == Sign::Negative {
135 -&x.significand
136 } else {
137 x.significand.clone()
138 };
139
140 // Adjust the significand so that the exponent is divisible by n and the root of the
141 // aligned significand carries exactly `precision` digits. The alignment allows two
142 // shifts that differ by n digits: `+r` (padding the significand up) or `r − n`
143 // (truncating it, with the dropped part kept as the sticky `low`). When r > 0 the
144 // padding choice yields n·precision + r aligned digits whose root carries
145 // `precision + 1` digits, which would then have to be rounded to the precision in a
146 // second step — and the two roundings disagree exactly at midpoints of the coarse
147 // grid (round-to-integer-then-re-round is a double rounding). The truncating choice
148 // keeps the root at exactly `precision` digits, so a single rounding decides.
149 let digits = x.digits() as isize;
150 let r = (x.exponent + digits).rem_euclid(n as isize);
151 let shift =
152 n as isize * self.precision as isize - digits + r - n as isize * (r > 0) as isize;
153 let (signif, low, low_digits) = if shift > 0 {
154 (shl_digits::<B>(&xmag, shift as usize), IBig::ZERO, 0)
155 } else {
156 let shift = (-shift) as usize;
157 let (hi, lo) = split_digits_ref::<B>(&xmag, shift);
158 (hi, lo, shift)
159 };
160
161 let mag: UBig = signif.unsigned_abs();
162 let root: UBig = mag.nth_root(n);
163 let rem: UBig = &mag - root.clone().pow(n);
164 let exp = (x.exponent - shift) / n as isize;
165
166 let result_sign = if sign == Sign::Negative {
167 Sign::Negative
168 } else {
169 Sign::Positive
170 };
171 let signed_root: IBig = result_sign * root.clone();
172 debug_assert!(digit_len::<B>(root.as_ibig()) <= self.precision);
173
174 let res = if rem.is_zero() && low.is_zero() {
175 Approximation::Exact(signed_root)
176 } else {
177 // The true value is (root + frac)·BASE^exp with frac ∈ (0, 1), where root =
178 // floor(mag^(1/n)) and the fraction continues into the truncated input part
179 // `low`. Comparing frac against 1/2:
180 // 2·(root + frac) vs 2·root + 1
181 // ⟺ 2^n·full vs (2·root + 1)^n·BASE^low_digits
182 // where full = mag·BASE^low_digits + low is the full aligned significand.
183 let adjust = R::round_low_part(&signed_root, result_sign, || {
184 let base_pow = Repr::<B>::BASE.pow(low_digits);
185 let full = &mag * &base_pow + low.unsigned_abs();
186 let lhs = full << n;
187 let rhs = ((root.clone() << 1) + UBig::from_word(1)).pow(n) * base_pow;
188 lhs.cmp(&rhs)
189 });
190 Approximation::Inexact(signed_root.clone() + adjust, adjust)
191 };
192 Ok(res
193 .map(|signif| Repr::new(signif, exp))
194 .map(|v| FBig::new(v, *self)))
195 }
196}
197
198impl<R: ErrorBounds> Context<R> {
199 /// Calculate the square root of the floating point number (correctly rounded).
200 ///
201 /// The integer square root of the exponent-aligned significand (which carries ~2·p digits) is
202 /// computed exactly, and its rounding to `p` digits is decided in a single step from the round
203 /// digit and the `sqrtrem` remainder (the sticky bit) — the same principle as MPFR. When the
204 /// root has `p + 1` digits it is rounded directly to `p` digits, avoiding the double rounding
205 /// that a rem-vs-root + re-round path incurs. For a power-of-two base this fast path is already
206 /// correctly rounded; for other bases the result is additionally certified by a Ziv loop (the
207 /// base-`B` digit alignment of an integer square root is only clean when the base is a power of
208 /// two).
209 ///
210 /// # Panics
211 ///
212 /// Panics if the precision is unlimited.
213 pub fn sqrt<const B: Word>(&self, x: &Repr<B>) -> FpResult<FBig<R, B>> {
214 if x.is_infinite() {
215 return Err(FpError::InfiniteInput);
216 }
217 if x.significand.is_zero() {
218 // sqrt(+0) = +0, sqrt(-0) = -0 (preserve the sign of zero). Exact, so handle
219 // it before the limited-precision assertion: a precision-0 (unlimited) value
220 // such as the one from `try_from(0.0)` must still compute sqrt(0) exactly.
221 return Ok(Approximation::Exact(FBig::new(x.clone(), *self)));
222 }
223 assert_limited_precision(self.precision);
224 if x.sign() == Sign::Negative {
225 return Err(FpError::OutOfDomain);
226 }
227
228 // One-step correctly-rounded sqrt of the (finite, positive, limited) input at a working
229 // precision, used directly by the power-of-two fast path and by the Ziv loop otherwise.
230 let sqrt_rounded = |guard: usize| -> FpResult<FBig<R, B>> {
231 let gctx = Context::<R>::new(self.precision + guard);
232 let p = gctx.precision;
233
234 // Adjust the significand so the exponent is even, with ~2p significant digits.
235 let digits = x.digits() as isize;
236 let shift = p as isize * 2 - (digits & 1) + (x.exponent & 1) - digits;
237 let (signif, low, low_digits) = if shift > 0 {
238 (shl_digits::<B>(&x.significand, shift as usize), IBig::ZERO, 0)
239 } else {
240 let shift = (-shift) as usize;
241 let (hi, lo) = split_digits_ref::<B>(&x.significand, shift);
242 (hi, lo, shift)
243 };
244
245 let (root, rem) = signif.unsigned_abs().sqrt_rem();
246 let exp = (x.exponent - shift) / 2;
247 let exact = rem.is_zero() && low.is_zero();
248 // The root has `p` or `p+1` base-B digits, decided exactly in O(1) from the shifted
249 // significand's digit count: `signif` carries `2p − (digits&1) + (exp&1)` digits, so it
250 // has 2p+1 digits (root has p+1) exactly when the significand's digit count is even and
251 // the input exponent is odd.
252 let root_is_p1 = (digits & 1) == 0 && (x.exponent & 1) == 1;
253
254 // The result's exponent. A p+1-digit root is rounded to p digits by dropping the lowest
255 // base-B digit (`r = root / B`), which shifts the value by one base-B digit: `exp + 1`.
256 // The arithmetic works on the unsigned `root`; it is converted to `IBig` only where
257 // `round_low_part` (signed) and the result's `+ Rounding` need it.
258 let (sig, adjust, result_exp) = if !root_is_p1 {
259 // p-digit root: the remainder (and any truncated input) decide the rounding. An
260 // integer sqrt has no exact half-tie (`sqrt(n) = root + 1/2` would require
261 // `4·rem = 2·root + 1`, impossible), so the rem-vs-root comparison is the exact
262 // single rounding.
263 let adjust = if exact {
264 Rounding::NoOp
265 } else {
266 R::round_low_part(root.as_ibig(), Sign::Positive, || {
267 rem.cmp(&root)
268 .then_with(|| (low << 2).cmp(&Repr::<B>::BASE.pow(low_digits).into()))
269 })
270 };
271 (IBig::from(root), adjust, exp)
272 } else {
273 // p+1-digit root: round to p digits in one step. `r` = top p digits, `d` = round
274 // digit (a Word, from the single `div_rem`).
275 let (r, d) = root.div_rem(B);
276 let adjust = if exact {
277 // The exact p+1-digit root: a clean rounding of `2·d` vs `B`, ties per the mode
278 // (`2·d = B` is the real half-tie; `d vs ⌊B/2⌋` would mis-round odd bases).
279 R::round_low_part(r.as_ibig(), Sign::Positive, || (d * 2).cmp(&B))
280 } else {
281 // The true value is strictly above `root` (sticky remainder / truncated input),
282 // so a round digit at the real half (2·d = B) is strictly past the half — report
283 // Greater so every mode (including the directed ones) rounds from the correct
284 // side. Delegating to `round_low_part` is what makes directed modes work: they
285 // ignore the comparison and truncate/extend per their own direction.
286 R::round_low_part(r.as_ibig(), Sign::Positive, || match (d * 2).cmp(&B) {
287 Ordering::Equal => Ordering::Greater,
288 other => other,
289 })
290 };
291 (IBig::from(r), adjust, exp + 1)
292 };
293
294 let res = if exact && !root_is_p1 {
295 Approximation::Exact(sig)
296 } else {
297 Approximation::Inexact(sig + adjust, adjust)
298 };
299 Ok(res
300 .map(|signif| Repr::new(signif, result_exp))
301 .and_then(|v| gctx.repr_round(v))
302 .map(|v| FBig::new(v, gctx)))
303 };
304
305 if B.is_power_of_two() {
306 sqrt_rounded(0)
307 } else {
308 // Near-correct kernel, mechanical radius: the base-`B` digit alignment of the integer
309 // square root is only clean when the base is a power of two, so for other bases the
310 // single rounding step is bounded by one ulp at the working precision — the same
311 // assumption every other [`Ball`] operator makes of its kernel.
312 //
313 // Routing it through `Ball::from_rounded` also gives an *exact* root `rad == 0`, and
314 // that is load-bearing: a zero radius is the only one Ziv can certify against a
315 // one-sided directed preimage (`Down`'s `[y, y+ulp)` cannot contain `[y−r, y+r]` for
316 // any `r > 0`). With a blanket `value.ulp()` the loop never converged on a perfect
317 // square — `sqrt(4)` in base 10 under `Down`/`Up`/`Zero` doubled its working precision
318 // to ~10^8 digits instead of returning `2`.
319 let initial_guard = crate::utils::ceil_usize(self.precision.log2_est()) + 10;
320 self.ziv(initial_guard, |guard| {
321 let wp = self.precision + guard;
322 let rounded = sqrt_rounded(guard)?;
323 Ok(Ball::from_rounded(rounded.map(FBig::into_repr), wp)
324 .to_value_radius::<R>(&Context::<R>::new(wp)))
325 })
326 }
327 }
328
329 /// Compute `sqrt(a² + b²)` without spurious overflow/underflow.
330 ///
331 /// This is the overflow-safe scaled sum-of-squares: the larger-magnitude operand is never
332 /// squared. Writing `m = max(|a|, |b|)` and `r = min(|a|,|b|) / m` (so `|r| ≤ 1`), the result is
333 /// `m · sqrt(1 + r²)`, where `1 + r² ∈ [1, 2]` cannot overflow. The result is correctly rounded
334 /// via a Ziv retry loop (`hypot(±inf, ·) = +inf`, `hypot(0, 0) = +0`).
335 ///
336 /// This is a field-arithmetic-class op (no constant cache), like `sqrt`/`atan2`.
337 ///
338 /// # Panics
339 ///
340 /// Panics if the precision is unlimited.
341 pub fn hypot<const B: Word>(&self, a: &Repr<B>, b: &Repr<B>) -> FpResult<FBig<R, B>> {
342 if a.is_infinite() || b.is_infinite() {
343 return Ok(Approximation::Exact(FBig::new(Repr::infinity(), *self)));
344 }
345 assert_limited_precision(self.precision);
346 if a.significand.is_zero() && b.significand.is_zero() {
347 return Ok(Approximation::Exact(FBig::new(Repr::zero(), *self)));
348 }
349
350 // magnitudes, ordered large >= small (both finite, not both zero here)
351 let a_mag = if a.sign() == Sign::Negative {
352 -a.clone()
353 } else {
354 a.clone()
355 };
356 let b_mag = if b.sign() == Sign::Negative {
357 -b.clone()
358 } else {
359 b.clone()
360 };
361 let (large, small) = if a_mag.cmp(&b_mag).is_ge() {
362 (a_mag, b_mag)
363 } else {
364 (b_mag, a_mag)
365 };
366
367 if small.significand.is_zero() {
368 // hypot(x, 0) = |x|; `large` is already a magnitude.
369 return Ok(self.repr_round_ref(&large).map(|v| FBig::new(v, *self)));
370 }
371
372 // The result is `sqrt(large² + small²)`, i.e. ∈ [large, large·√2]. It overflows only when
373 // `large` is so large that the result reaches the infinity sentinel exponent — unreachable
374 // for real inputs, but pre-checked here so the Ziv closure can use infallible `FBig`
375 // arithmetic.
376 if large.exponent >= isize::MAX - 1 {
377 return Err(FpError::Overflow(Sign::Positive));
378 }
379
380 let initial_guard = crate::utils::ceil_usize(self.precision.log2_est()) + 10;
381 self.ziv(initial_guard, |guard| {
382 let gctx = Context::<mode::HalfEven>::new(self.precision + guard);
383 // result = sqrt(large² + small²), with both operands scaled down by `k` base-B digits
384 // before squaring (so `large²` can't overflow the exponent) and the root scaled back:
385 // sqrt(L² + S²) · B^k = sqrt(large² + small²) for L = large·B⁻ᵏ, S = small·B⁻ᵏ. No
386 // division — so for integer inputs every step is exact (MPFR's `exact` flag), and an
387 // all-exact chain yields the exact true value. The tracking variants report radius 0
388 // then, which `ziv` accepts without the containment test (it can't certify an
389 // exactly-representable result under directed rounding — e.g. hypot(3,4)=5,
390 // hypot(5,12)=13 — which sits on a one-sided preimage boundary).
391 // Scale down by the largest raw exponent of the *two* operands: base-B
392 // normalization can leave the smaller value with the larger raw exponent
393 // (e.g. `4·2^e` normalizes to `1·2^(e+2)`), and it is small's square that
394 // would then collide with the infinity sentinel.
395 let k = (large.exponent.max(small.exponent) as i128 - (isize::MAX as i128 - 2) / 2)
396 .max(0) as isize;
397 let wp = gctx.precision;
398 // The input roundings' exactness folds through `from_rounded`: an exact input keeps
399 // `rad = 0`, so an all-exact chain (integer inputs, no rounding anywhere) carries a
400 // zero radius — the exactly-representable directed-rounding case that no nonzero
401 // radius could certify (e.g. hypot(3,4)=5, hypot(5,12)=13).
402 let large_ball = Ball::from_rounded(gctx.repr_round_ref(&large), wp);
403 let small_ball = Ball::from_rounded(gctx.repr_round_ref(&small), wp);
404 // The shifted balls are used twice (the square), so bind them once — a shift is a full
405 // O(p) clone otherwise.
406 let l = large_ball.shift(-k);
407 let s = small_ball.shift(-k);
408 let l_sq = l.mul(&l, wp)?;
409 let s_sq = s.mul(&s, wp)?;
410 let sum = l_sq.add(&s_sq, wp)?;
411 let root = sum.sqrt(wp)?;
412 let result = root.shift(k); // exact exponent shift — scales back, radius unchanged
413 Ok(result.to_value_radius::<R>(&Context::<R>::new(wp)))
414 })
415 }
416}
417
418impl<R: ErrorBounds, const B: Word> FBig<R, B> {
419 /// Calculate the square root of the floating point number (correctly rounded).
420 ///
421 /// # Panics
422 ///
423 /// Panics if the precision is unlimited.
424 #[inline]
425 pub fn sqrt(&self) -> Self {
426 self.context.unwrap_fp(self.context.sqrt(&self.repr))
427 }
428
429 /// Compute `sqrt(self² + other²)` without spurious overflow/underflow.
430 ///
431 /// The result precision is `max(self.precision(), other.precision())`. See
432 /// [`Context::hypot`] for the overflow-safety strategy.
433 ///
434 /// # Examples
435 ///
436 /// ```
437 /// # use core::str::FromStr;
438 /// # use dashu_base::ParseError;
439 /// # use dashu_float::DBig;
440 /// let a = DBig::from_str("3")?;
441 /// let b = DBig::from_str("4")?;
442 /// assert_eq!(a.hypot(&b), DBig::from_str("5")?);
443 /// # Ok::<(), ParseError>(())
444 /// ```
445 ///
446 /// # Panics
447 ///
448 /// Panics if the precision is unlimited.
449 #[inline]
450 pub fn hypot(&self, other: &Self) -> Self {
451 let context = Context::max(self.context, other.context);
452 context.unwrap_fp(context.hypot(&self.repr, &other.repr))
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use crate::round::mode;
460 use dashu_base::Approximation::*;
461
462 fn r2(sig: i32, exp: isize) -> Repr<2> {
463 Repr::new(sig.into(), exp)
464 }
465
466 // sqrt under directed modes must bracket the true value (issue #99): for a positive
467 // non-square x, sqrt_down(x)² < x < sqrt_up(x)² and the two results differ. The inputs
468 // 6 = 3·2¹ and 24 = 3·2³ exercise the p+1-digit integer-root path (even digit count, odd
469 // exponent) whose sticky remainder the old code rounded the wrong way under Up/Away. Every
470 // input here has a non-square odd part, so x is never a perfect square at any exponent.
471 #[test]
472 fn test_sqrt_directed_modes_bracket() {
473 for x in [3i32, 5, 6, 7, 10, 24, 30] {
474 for exp in [-3isize, -1, 0, 1, 3] {
475 let input = Repr::<2>::new(IBig::from(x), exp);
476 for p in [20usize, 50, 100, 500] {
477 let down = Context::<mode::Down>::new(p)
478 .sqrt(&input)
479 .unwrap()
480 .value()
481 .with_precision(0)
482 .value();
483 let up = Context::<mode::Up>::new(p)
484 .sqrt(&input)
485 .unwrap()
486 .value()
487 .with_precision(0)
488 .value();
489 let x = FBig::<mode::HalfEven>::new(input.clone(), Context::new(0));
490 // squares at unlimited precision are exact, so these comparisons are exact
491 assert!(&down * &down < x, "sqrt({input:?}) @ p{p}: Down bound too high");
492 assert!(&up * &up > x, "sqrt({input:?}) @ p{p}: Up bound too low");
493 assert!(down < up, "sqrt({input:?}) @ p{p}: Down == Up on a non-square");
494 }
495 }
496 }
497 }
498
499 // Perfect squares are exact under every mode and yield identical Down/Up results.
500 #[test]
501 fn test_sqrt_perfect_square_is_exact() {
502 for (input, root) in [
503 (r2(1, 2), r2(1, 1)),
504 (r2(9, -2), r2(3, -1)),
505 (r2(25, -4), r2(5, -2)),
506 ] {
507 for p in [20usize, 50] {
508 let r = Context::<mode::HalfEven>::new(p).sqrt(&input).unwrap();
509 assert!(matches!(r, Exact(..)), "sqrt({input:?}) @ p{p} should be exact");
510 assert_eq!(r.value().repr(), &root);
511 assert_eq!(
512 Context::<mode::Down>::new(p)
513 .sqrt(&input)
514 .unwrap()
515 .value()
516 .repr(),
517 &root
518 );
519 assert_eq!(
520 Context::<mode::Up>::new(p)
521 .sqrt(&input)
522 .unwrap()
523 .value()
524 .repr(),
525 &root
526 );
527 }
528 }
529 }
530
531 // The same class of bug at tiny precision: sqrt(1.75) = 1.3228… has the p+1-digit integer
532 // root 5 (= 5·2^-2 = 1.25), exactly the midpoint of the p2 neighbours 1 and 1.5 — but the
533 // inexact root's remainder puts the true value strictly above the midpoint, so HalfEven
534 // must round up to 1.5 (the old code tied down to 1.0).
535 #[test]
536 fn test_sqrt_p1_digit_root_midpoint() {
537 let v = Context::<mode::HalfEven>::new(2)
538 .sqrt(&r2(7, -2))
539 .unwrap()
540 .value();
541 assert_eq!(v.repr(), &r2(3, -1));
542 // the same input under Up/Down brackets 1.3228… correctly
543 let up = Context::<mode::Up>::new(2)
544 .sqrt(&r2(7, -2))
545 .unwrap()
546 .value();
547 let down = Context::<mode::Down>::new(2)
548 .sqrt(&r2(7, -2))
549 .unwrap()
550 .value();
551 assert_eq!(up.repr(), &r2(3, -1));
552 assert_eq!(down.repr(), &r2(1, 0));
553 }
554
555 // An exact p+1-digit root is rounded to the precision in one step: sqrt(1.5625) = 1.25 is
556 // exactly the midpoint of 1.0 and 1.5; ties to even picks the significand 10₂ -> 1.0. The
557 // result differs from the exact root, so the operation is still Inexact.
558 #[test]
559 fn test_sqrt_exact_p1_digit_root_ties_to_even() {
560 let r = Context::<mode::HalfEven>::new(2).sqrt(&r2(25, -4)).unwrap();
561 assert!(matches!(r, Inexact(..)));
562 assert_eq!(r.value().repr(), &r2(1, 0));
563 }
564
565 // nth_root must round a precision+1-digit integer root in a single step (issue #100):
566 // the old code rounded the root to an integer first and re-rounded, which ties the wrong
567 // way exactly at midpoints of the coarse grid.
568 #[test]
569 fn test_nth_root_single_step_rounding() {
570 // nth_root(2, 1.75) @ p2 HalfEven = 1.5 (see test_sqrt_p1_digit_root_midpoint)
571 let v = Context::<mode::HalfEven>::new(2)
572 .nth_root(2, &r2(7, -2))
573 .unwrap()
574 .value();
575 assert_eq!(v.repr(), &r2(3, -1));
576
577 // exact root one digit wider than the precision: nth_root(2, 2.25) = 1.5 @ p2
578 let r = Context::<mode::HalfEven>::new(2)
579 .nth_root(2, &r2(9, -2))
580 .unwrap();
581 assert!(matches!(r, Exact(..)));
582 assert_eq!(r.value().repr(), &r2(3, -1));
583
584 // directed modes bracket on the same input
585 let up = Context::<mode::Up>::new(2)
586 .nth_root(2, &r2(7, -2))
587 .unwrap()
588 .value();
589 let down = Context::<mode::Down>::new(2)
590 .nth_root(2, &r2(7, -2))
591 .unwrap()
592 .value();
593 assert_eq!(up.repr(), &r2(3, -1));
594 assert_eq!(down.repr(), &r2(1, 0));
595
596 // agreement with sqrt on a perfect square: nth_root(2, 16) @ p5 = 4
597 let v = Context::<mode::HalfEven>::new(5)
598 .nth_root(2, &r2(1, 4))
599 .unwrap()
600 .value();
601 assert_eq!(v.repr(), &r2(1, 2));
602 }
603
604 #[test]
605 #[should_panic]
606 fn test_fbig_sqrt_negative_panics() {
607 // sqrt(-1) is out of domain; the FBig layer panics.
608 let neg_one = FBig::<mode::HalfEven>::try_from(-1.0f64).unwrap();
609 let _ = neg_one.sqrt();
610 }
611
612 #[test]
613 fn test_hypot_pythagorean() {
614 let ctx = Context::<mode::HalfEven>::new(53);
615 let mk = |v: i32| Repr::<2>::new(v.into(), 0);
616 // hypot(3, 4) = 5
617 let r = ctx.hypot(&mk(3), &mk(4)).unwrap().value();
618 assert_eq!(r.repr().significand(), &5.into());
619 // hypot(5, 0) = 5
620 let r = ctx.hypot(&mk(5), &mk(0)).unwrap().value();
621 assert_eq!(r.repr().significand(), &5.into());
622 // hypot(0, 0) = 0
623 let r = ctx.hypot(&mk(0), &mk(0)).unwrap().value();
624 assert!(r.repr().is_pos_zero());
625 // hypot(inf, x) = +inf
626 let r = ctx.hypot(&Repr::infinity(), &mk(3)).unwrap().value();
627 assert!(r.repr().is_infinite());
628 assert_eq!(r.repr().sign(), Sign::Positive);
629 }
630
631 fn check_hypot_exact_triples<R: ErrorBounds>(ctx: Context<R>) {
632 let mk = |v: i32| Repr::<2>::new(v.into(), 0);
633 // Pythagorean triples: the result is exactly representable, so under a directed mode it
634 // sits on a one-sided preimage boundary. The closure must terminate (radius 0 from the
635 // all-exact `sqrt(large²+small²)` chain) rather than infinite-retry.
636 for (a, b, h) in [(3, 4, 5), (5, 12, 13), (8, 15, 17)] {
637 let r = ctx.hypot(&mk(a), &mk(b)).unwrap().value();
638 assert_eq!(r.repr().significand(), &h.into(), "hypot({a}, {b})");
639 }
640 }
641
642 #[test]
643 fn test_hypot_exact_under_directed_rounding() {
644 check_hypot_exact_triples(Context::<mode::Down>::new(53));
645 check_hypot_exact_triples(Context::<mode::Up>::new(53));
646 check_hypot_exact_triples(Context::<mode::Zero>::new(53));
647 }
648
649 #[test]
650 fn test_hypot_no_spurious_overflow() {
651 // a value whose square would collide with the +inf sentinel exponent, but whose
652 // hypot is itself representable: hypot(a, 0) = |a| must not overflow via a².
653 let ctx = Context::<mode::HalfEven>::new(53);
654 // exponent near isize::MAX/2 so that a² would overflow, but |a| is fine
655 let a = Repr::<2>::new(IBig::from(3), isize::MAX / 2);
656 let r = ctx.hypot(&a, &Repr::<2>::zero()).unwrap().value();
657 assert_eq!(r.repr().exponent(), isize::MAX / 2);
658 }
659
660 #[test]
661 fn test_hypot_extreme_exponents_rescale_both_operands() {
662 // Both operands past the scale-down threshold (`k > 0`): the `B⁻ᵏ` rescale → square →
663 // root → `B^k` scale-back chain must keep the value (the zero-operand test above never
664 // exercises the shifted `small` operand). `4·2^e` normalizes to `1·2^(e+2)`, so the
665 // *smaller* value carries the larger raw exponent — the regression: `k` was derived
666 // from `large`'s exponent only, and the non-exact `hypot(7·2^e, 4·2^e)` died on
667 // `Err(Overflow)` through small's square.
668 let ctx = Context::<mode::HalfEven>::new(53);
669 let e = isize::MAX / 2 + 10; // large enough that k > 0
670 let a = Repr::<2>::new(IBig::from(3), e);
671 let b = Repr::<2>::new(IBig::from(4), e);
672 let r = ctx.hypot(&a, &b).unwrap().value();
673 assert_eq!(r.repr().significand(), &5.into(), "hypot(3·B^e, 4·B^e) = 5·B^e");
674 assert_eq!(r.repr().exponent(), e);
675 // the non-exact value (√65 · 2^e): a power-of-two rescale of the moderate-exponent
676 // result — same significand, exponent shifted by e
677 let a7 = Repr::<2>::new(IBig::from(7), e);
678 let r = ctx.hypot(&a7, &b).unwrap().value();
679 let base = ctx
680 .hypot(&Repr::<2>::new(IBig::from(7), 0), &Repr::<2>::new(IBig::from(4), 0))
681 .unwrap()
682 .value();
683 let expect = Repr::<2>::new(base.repr().significand().clone(), base.repr().exponent() + e);
684 assert_eq!(r.repr(), &expect, "hypot(7·2^e, 4·2^e) = √65·2^e");
685 }
686
687 /// A perfect square in a *non-power-of-two* base must certify under every rounding mode,
688 /// the one-sided directed ones included.
689 ///
690 /// Regression: `sqrt`'s Ziv closure reported a blanket `value.ulp()` radius, never zero, and
691 /// no `r > 0` fits inside `Down`'s preimage `[y, y+ulp)` — so an exactly-representable root
692 /// could not be certified at all. `sqrt(4)` in base 10 under `Down`/`Up`/`Zero` doubled its
693 /// working precision until the retry budget ran out (~10^8 digits) instead of returning `2`.
694 /// The root is now wrapped as a [`Ball`], so an exact result carries `rad == 0`.
695 #[test]
696 fn test_sqrt_exact_root_certifies_under_directed_rounding() {
697 // (input, expected exact root) — perfect squares, and a non-square for contrast
698 let cases: [(i64, isize, i64, isize, bool); 4] = [
699 (4, 0, 2, 0, true),
700 (100, 0, 10, 0, true),
701 (9, 0, 3, 0, true),
702 (25, -2, 5, -1, true), // 0.25 → 0.5
703 ];
704 macro_rules! check {
705 ($m:ty, $name:expr) => {
706 for (sig, exp, root_sig, root_exp, exact) in cases {
707 let x = Repr::<10>::new(IBig::from(sig), exp);
708 let r = Context::<$m>::new(10).sqrt::<10>(&x).unwrap();
709 let want = Repr::<10>::new(IBig::from(root_sig), root_exp);
710 assert_eq!(
711 matches!(r, Approximation::Exact(_)),
712 exact,
713 "{}: sqrt({sig}e{exp}) exactness",
714 $name
715 );
716 assert_eq!(r.value().repr(), &want, "{}: sqrt({sig}e{exp})", $name);
717 }
718 };
719 }
720 check!(mode::Down, "Down");
721 check!(mode::Up, "Up");
722 check!(mode::Zero, "Zero");
723 check!(mode::HalfEven, "HalfEven");
724 check!(mode::HalfAway, "HalfAway");
725 }
726}