axiolid_exact/
tower.rs

1//! Nested square roots: values in a tower of adjoined radicals.
2//!
3//! [`crate::Root2`] holds one square root over plain numbers. Arc
4//! constructions need more: a hit on a circle is `a + b*sqrt(D)`, and a
5//! distance or a second construction from that hit takes another square
6//! root of an expression that already contains `sqrt(D)`. This module
7//! represents such values exactly.
8//!
9//! # Representation
10//!
11//! A [`Tower`] is a list of radicands `r_1, ..., r_k`, each built only
12//! from the radicals before it. A [`Nested`] value at level `k` is
13//! `a + b*sqrt(r_k)` with `a` and `b` at level `k - 1`, stored flat as a
14//! coefficient vector of length `2^k` (low half `a`, high half `b`).
15//!
16//! # Why no canonical form is needed
17//!
18//! Radicals need not be independent: `sqrt(8)` and `sqrt(2)` may both be
19//! adjoined, and then `sqrt(8) - 2*sqrt(2)` has non-zero coefficients but
20//! value zero. That is fine. Both the product rule
21//! `(a + b√r)(c + d√r) = (ac + bd·r) + (ad + bc)√r` and the sign rule below
22//! are identities about real numbers, true whatever the coefficients are.
23//! So signs are exact, including exact zeros, without ever reducing to a
24//! basis, which is what keeps this module small.
25//!
26//! # Sign
27//!
28//! `sign(a + b*sqrt(r))`, recursively by level: if `a` and `b*sqrt(r)`
29//! agree in sign (or one is zero) that is the answer; otherwise it is
30//! `sign(a) * sign(a^2 - b^2*r)`, a value one level down. Level 0 asks the
31//! arithmetic `T` directly, so the same code runs as the interval filter
32//! and as the exact fallback (see [`crate::certify()`]).
33//!
34//! # Cost
35//!
36//! Each level squares once, so the polynomial degree in the inputs, and
37//! with it exact-tier mantissa length, doubles per level; a product costs
38//! five products one level down. Depth is capped at [`MAX_DEPTH`].
39
40use axiolid_guarantees::Sign;
41
42use crate::arith::{sign_product, Arith};
43use crate::certify::ExactError;
44
45/// Most radicals one tower may hold. A product at depth `k` costs `5^k`
46/// base products, and exact mantissas grow `2^k`-fold in degree.
47pub const MAX_DEPTH: usize = 6;
48
49/// A value in a [`Tower`]: `2^level` coefficients over `T`.
50///
51/// Only meaningful together with the tower that made it.
52#[derive(Debug, Clone, PartialEq)]
53pub struct Nested<T> {
54    coeffs: Vec<T>,
55}
56
57impl<T> Nested<T> {
58    /// How many radicals this value can involve.
59    #[must_use]
60    pub fn level(&self) -> usize {
61        self.coeffs.len().trailing_zeros() as usize
62    }
63
64    /// The coefficients, low half first at every level.
65    #[must_use]
66    pub fn coeffs(&self) -> &[T] {
67        &self.coeffs
68    }
69}
70
71/// The radicals adjoined so far.
72#[derive(Debug, Clone, Default)]
73pub struct Tower<T> {
74    /// Radicand `i` (0-based), lifted to exactly level `i`.
75    radicands: Vec<Nested<T>>,
76}
77
78impl<T: Arith> Tower<T> {
79    /// An empty tower: values are plain `T`.
80    #[must_use]
81    pub fn new() -> Self {
82        Self {
83            radicands: Vec::new(),
84        }
85    }
86
87    /// Number of radicals adjoined.
88    #[must_use]
89    pub fn depth(&self) -> usize {
90        self.radicands.len()
91    }
92
93    /// A plain value.
94    #[must_use]
95    pub fn value(&self, value: T) -> Nested<T> {
96        Nested {
97            coeffs: vec![value],
98        }
99    }
100
101    /// The value of a finite `f64` (see [`Arith::from_f64`]).
102    #[must_use]
103    pub fn from_f64(&self, value: f64) -> Nested<T> {
104        self.value(T::from_f64(value))
105    }
106
107    /// Adjoin `sqrt(radicand)` and return it as a value.
108    ///
109    /// The radicand must not be negative. That is not checked here, since
110    /// the interval tier may be unable to tell; [`Tower::sign`] of any
111    /// value involving a negative radicand returns `None`, which the exact
112    /// tier reports as [`ExactError::Undefined`].
113    ///
114    /// # Errors
115    ///
116    /// [`ExactError::TooDeep`] beyond [`MAX_DEPTH`] radicals.
117    ///
118    /// # Panics
119    ///
120    /// If `radicand` came from a different tower (its level exceeds this
121    /// tower's depth).
122    pub fn sqrt(&mut self, radicand: &Nested<T>) -> Result<Nested<T>, ExactError> {
123        let level = self.depth();
124        if level >= MAX_DEPTH {
125            return Err(ExactError::TooDeep);
126        }
127        self.own(radicand);
128        self.radicands.push(self.lift(radicand, level));
129        let mut coeffs = vec![T::from_f64(0.0); 1 << (level + 1)];
130        coeffs[1 << level] = T::from_f64(1.0);
131        Ok(Nested { coeffs })
132    }
133
134    /// `x + y`.
135    #[must_use]
136    pub fn add(&self, x: &Nested<T>, y: &Nested<T>) -> Nested<T> {
137        self.zip(x, y, T::add)
138    }
139
140    /// `x - y`.
141    #[must_use]
142    pub fn sub(&self, x: &Nested<T>, y: &Nested<T>) -> Nested<T> {
143        self.zip(x, y, T::sub)
144    }
145
146    /// `-x`.
147    #[must_use]
148    pub fn neg(&self, x: &Nested<T>) -> Nested<T> {
149        self.own(x);
150        Nested {
151            coeffs: x.coeffs.iter().map(T::neg).collect(),
152        }
153    }
154
155    /// `x * y`.
156    #[must_use]
157    pub fn mul(&self, x: &Nested<T>, y: &Nested<T>) -> Nested<T> {
158        self.own(x);
159        self.own(y);
160        let level = x.level().max(y.level());
161        let (x, y) = (self.lift(x, level), self.lift(y, level));
162        Nested {
163            coeffs: self.mul_at(level, &x.coeffs, &y.coeffs),
164        }
165    }
166
167    /// The sign of `x`, or `None` when `T` cannot decide or a radicand
168    /// involved is negative.
169    ///
170    /// Approximate arithmetics first evaluate `x` numerically through
171    /// [`Arith::sqrt_enclosure`]; that decides whenever `x` is visibly away
172    /// from zero. Otherwise, and always for exact arithmetics, signs are
173    /// decided by case analysis on the coefficients.
174    #[must_use]
175    pub fn sign(&self, x: &Nested<T>) -> Option<Sign> {
176        self.own(x);
177        // An interval sign is sound whenever it is decided. A numeric
178        // Zero can only come from [0, 0], i.e. from coefficients that are
179        // literally zero, where Zero is also the exact answer.
180        if let Some(sign) = self
181            .numeric(x.level(), &x.coeffs)
182            .and_then(|value| value.sign())
183        {
184            return Some(sign);
185        }
186        self.sign_at(x.level(), &x.coeffs)
187    }
188
189    /// Numerical value of a coefficient vector, for arithmetics that can
190    /// enclose square roots. `None` for exact arithmetics, or when some
191    /// radicand might be negative.
192    fn numeric(&self, level: usize, x: &[T]) -> Option<T> {
193        if level == 0 {
194            return Some(x[0].clone());
195        }
196        let half = 1 << (level - 1);
197        let (a, b) = x.split_at(half);
198        let root = self
199            .numeric(level - 1, &self.radicands[level - 1].coeffs)?
200            .sqrt_enclosure()?;
201        let a = self.numeric(level - 1, a)?;
202        let b = self.numeric(level - 1, b)?;
203        Some(a.add(&b.mul(&root)))
204    }
205
206    /// The sign of `x - y`.
207    #[must_use]
208    pub fn cmp(&self, x: &Nested<T>, y: &Nested<T>) -> Option<Sign> {
209        self.sign(&self.sub(x, y))
210    }
211
212    fn own(&self, x: &Nested<T>) {
213        assert!(
214            x.level() <= self.depth(),
215            "a Nested value is only meaningful in the tower that made it"
216        );
217    }
218
219    /// `x` padded with zero coefficients to `level`.
220    fn lift(&self, x: &Nested<T>, level: usize) -> Nested<T> {
221        let mut coeffs = x.coeffs.clone();
222        coeffs.resize(1 << level, T::from_f64(0.0));
223        Nested { coeffs }
224    }
225
226    fn zip(&self, x: &Nested<T>, y: &Nested<T>, op: impl Fn(&T, &T) -> T) -> Nested<T> {
227        self.own(x);
228        self.own(y);
229        let level = x.level().max(y.level());
230        let (x, y) = (self.lift(x, level), self.lift(y, level));
231        Nested {
232            coeffs: x
233                .coeffs
234                .iter()
235                .zip(&y.coeffs)
236                .map(|(a, b)| op(a, b))
237                .collect(),
238        }
239    }
240
241    /// Product of two coefficient vectors of length `2^level`.
242    fn mul_at(&self, level: usize, x: &[T], y: &[T]) -> Vec<T> {
243        if level == 0 {
244            return vec![x[0].mul(&y[0])];
245        }
246        let half = 1 << (level - 1);
247        let (a, b) = x.split_at(half);
248        let (c, d) = y.split_at(half);
249        let radicand = &self.radicands[level - 1].coeffs;
250        let ac = self.mul_at(level - 1, a, c);
251        let bd = self.mul_at(level - 1, b, d);
252        let bdr = self.mul_at(level - 1, &bd, radicand);
253        let ad = self.mul_at(level - 1, a, d);
254        let bc = self.mul_at(level - 1, b, c);
255        let mut out: Vec<T> = ac.iter().zip(&bdr).map(|(p, q)| p.add(q)).collect();
256        out.extend(ad.iter().zip(&bc).map(|(p, q)| p.add(q)));
257        out
258    }
259
260    fn sign_at(&self, level: usize, x: &[T]) -> Option<Sign> {
261        if level == 0 {
262            return x[0].sign();
263        }
264        let half = 1 << (level - 1);
265        let (a, b) = x.split_at(half);
266        let radicand = &self.radicands[level - 1].coeffs;
267        let sr = self.sign_at(level - 1, radicand)?;
268        if sr == Sign::Negative {
269            return None;
270        }
271        let sa = self.sign_at(level - 1, a)?;
272        // b*sqrt(r) has the sign of b, unless r is zero.
273        let sb = if sr == Sign::Zero {
274            Sign::Zero
275        } else {
276            self.sign_at(level - 1, b)?
277        };
278        if sb == Sign::Zero {
279            return Some(sa);
280        }
281        if sa == Sign::Zero || sa == sb {
282            return Some(sb);
283        }
284        let a2 = self.mul_at(level - 1, a, a);
285        let b2 = self.mul_at(level - 1, b, b);
286        let b2r = self.mul_at(level - 1, &b2, radicand);
287        let diff: Vec<T> = a2.iter().zip(&b2r).map(|(p, q)| p.sub(q)).collect();
288        let dominance = self.sign_at(level - 1, &diff)?;
289        Some(sign_product(sa, dominance))
290    }
291}