ruint/root.rs
1#![cfg(feature = "std")]
2
3use crate::Uint;
4use core::cmp::{min, Ordering};
5
6impl<const BITS: usize, const LIMBS: usize> Uint<BITS, LIMBS> {
7 /// Computes the floor of the `degree`-th root of the number.
8 ///
9 /// $$
10 /// \floor{\sqrt[\mathtt{degree}]{\mathtt{self}}}
11 /// $$
12 ///
13 /// # Panics
14 ///
15 /// Panics if `degree` is zero.
16 ///
17 /// # Examples
18 ///
19 /// ```
20 /// # use ruint::{Uint, uint, aliases::*};
21 /// # uint!{
22 /// assert_eq!(0_U64.root(2), 0_U64);
23 /// assert_eq!(1_U64.root(63), 1_U64);
24 /// assert_eq!(0x0032da8b0f88575d_U63.root(64), 1_U63);
25 /// assert_eq!(0x1756800000000000_U63.root(34), 3_U63);
26 /// # }
27 /// ```
28 #[inline]
29 #[must_use]
30 pub fn root(self, degree: usize) -> Self {
31 assert!(degree > 0, "degree must be greater than zero");
32
33 // Handle zero case (including BITS == 0).
34 if self.is_zero() {
35 return Self::ZERO;
36 }
37
38 // Handle case where `degree > Self::BITS`.
39 if degree >= Self::BITS {
40 return Self::from(1);
41 }
42
43 // Handle case where `degree == 1`.
44 if degree == 1 {
45 return self;
46 }
47
48 // Create a first guess.
49 // Root should be less than the value, so approx_pow2 should always succeed.
50 #[allow(clippy::cast_precision_loss)] // Approximation is good enough.
51 #[allow(clippy::cast_sign_loss)] // Result should be positive.
52 let mut result = Self::approx_pow2(self.approx_log2() / degree as f64).unwrap();
53
54 let deg_m1 = Self::from(degree - 1);
55
56 // Iterate using Newton's method
57 // See <https://en.wikipedia.org/wiki/Integer_square_root#Algorithm_using_Newton's_method>
58 // See <https://gmplib.org/manual/Nth-Root-Algorithm>
59 let mut decreasing = false;
60 loop {
61 // OPT: This could benefit from single-limb multiplication
62 // and division.
63 //
64 // OPT: The division can be turned into bit-shifts when the degree is a power of
65 // two.
66 let division = result
67 .checked_pow(deg_m1)
68 .map_or(Self::ZERO, |power| self / power);
69 let iter = (division + deg_m1 * result) / Self::from(degree);
70 match (decreasing, iter.cmp(&result)) {
71 // Stop when we hit fix point or stop decreasing.
72 (_, Ordering::Equal) | (true, Ordering::Greater) => break result,
73
74 // When `degree` is high and the initial guess is less than or equal to the
75 // (small) true result, it takes a long time to converge. Example:
76 // 0x215f07147d573ef203e1f268ab1516d3f294619db820c5dfd0b334e4d06320b7_U256.
77 // root(196) takes 5918 iterations to converge from the initial guess of `2`.
78 // to the final result of `2`. This is because after the first iteration
79 // it jumps to `1533576856264507`. To fix this we cap the increase at `2x`.
80 // Once `result` exceeds the true result, it will converge downwards.
81 (false, Ordering::Greater) => result = min(iter, result.saturating_shl(1)),
82
83 // Converging downwards.
84 (_, Ordering::Less) => {
85 decreasing = true;
86 result = iter;
87 }
88 }
89 }
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::{const_for, nlimbs};
97 use proptest::proptest;
98
99 #[test]
100 #[allow(clippy::absurd_extreme_comparisons)] // From macro.
101 fn test_root() {
102 const_for!(BITS in SIZES if (BITS > 3) {
103 const LIMBS: usize = nlimbs(BITS);
104 type U = Uint<BITS, LIMBS>;
105 proptest!(|(value: U, degree in 1_usize..=5)| {
106 let root = value.root(degree);
107 let lower = root.pow(U::from(degree));
108 assert!(value >= lower);
109 let upper = root
110 .checked_add(U::from(1))
111 .and_then(|n| n.checked_pow(U::from(degree)));
112 if let Some(upper) = upper {
113 assert!(value < upper);
114 }
115 });
116 });
117 }
118
119 #[test]
120 #[allow(clippy::absurd_extreme_comparisons)] // From macro.
121 #[allow(clippy::reversed_empty_ranges)] // From macro.
122 fn test_root_large() {
123 const_for!(BITS in SIZES if (BITS > 3) {
124 const LIMBS: usize = nlimbs(BITS);
125 type U = Uint<BITS, LIMBS>;
126 proptest!(|(value: U, degree in 1_usize..=BITS)| {
127 let root = value.root(degree);
128 let lower = root.pow(U::from(degree));
129 assert!(value >= lower);
130 let upper = root
131 .checked_add(U::from(1))
132 .and_then(|n| n.checked_pow(U::from(degree)));
133 if let Some(upper) = upper {
134 assert!(value < upper);
135 }
136 });
137 });
138 }
139}