Skip to main content

geop_core_math/scalars/
mod.rs

1//! The kernel's number: an interval `[lo, hi]` of `f64`, every operation
2//! rounded outward, so that a value encloses every real it could be and
3//! comparisons are three-valued (`could_be_equal`, `definitely_greater`).
4//! [`Number`] is what a residual of the constraint solvers is written in:
5//! a [`Scalar`], or a [`crate::dual::Dual`] of one carrying derivatives.
6
7mod number;
8
9use core::fmt::Display;
10
11pub use number::Number;
12
13use crate::geop_error::{GeopError, GeopResult};
14
15// ── IEEE 754 outward-rounding helpers ─────────────────────────────────────────
16//
17// The next number either way, zero included: from zero that is the
18// smallest subnormal, as from any other value it is one unit in the last
19// place. Zero once rounded to `MIN_POSITIVE`, `2.2e-308`, so every value
20// computed near zero carried that much, while values a subnormal apart
21// (a cap's side at `u = MIN_POSITIVE / 20`) were sharp: a point between
22// them could not be told on or off the side.
23
24#[inline]
25pub(crate) fn next_up(x: f64) -> f64 {
26    if x.is_nan() {
27        return x;
28    }
29    if x == f64::INFINITY {
30        return f64::INFINITY;
31    }
32    if x == 0.0 {
33        return f64::from_bits(1);
34    }
35    let bits = x.to_bits();
36    let bits = if x > 0.0 { bits + 1 } else { bits - 1 };
37    f64::from_bits(bits)
38}
39
40#[inline]
41pub(crate) fn next_down(x: f64) -> f64 {
42    if x.is_nan() {
43        return x;
44    }
45    if x == f64::NEG_INFINITY {
46        return f64::NEG_INFINITY;
47    }
48    if x == 0.0 {
49        return -f64::from_bits(1);
50    }
51    let bits = x.to_bits();
52    let bits = if x < 0.0 { bits + 1 } else { bits - 1 };
53    f64::from_bits(bits)
54}
55
56// ── Interval trigonometry ─────────────────────────────────────────────────────
57
58/// True iff `[lo, hi]` contains some `target + k * period` for an integer `k`
59/// — i.e. whether the interval passes through one of `sin`/`cos`'s
60/// extrema, which sampling only the endpoints could miss entirely.
61#[inline]
62fn contains_periodic(lo: f64, hi: f64, target: f64, period: f64) -> bool {
63    let k = ((lo - target) / period).ceil();
64    target + k * period <= hi
65}
66
67/// Outward-rounded enclosure of `sin`/`cos([lo, hi])`, correct even though
68/// `f64::sin`/`f64::cos` are not guaranteed correctly rounded: the true
69/// extrema at the given `max_at`/`min_at` phases are recognized structurally
70/// (`could_be_periodic`) rather than hunted for numerically, and the sampled
71/// endpoint values are widened outward by a further ULP against roundoff in
72/// the libm call itself.
73fn interval_trig(lo: f64, hi: f64, f: impl Fn(f64) -> f64, max_at: f64, min_at: f64) -> (f64, f64) {
74    use std::f64::consts::TAU;
75    if !lo.is_finite() || !hi.is_finite() || hi - lo >= TAU {
76        return (-1.0, 1.0);
77    }
78    let (a, b) = (f(lo), f(hi));
79    let mut out_lo = next_down(a.min(b));
80    let mut out_hi = next_up(a.max(b));
81    if contains_periodic(lo, hi, max_at, TAU) {
82        out_hi = 1.0;
83    }
84    if contains_periodic(lo, hi, min_at, TAU) {
85        out_lo = -1.0;
86    }
87    (out_lo.max(-1.0), out_hi.min(1.0))
88}
89
90// ── Type ──────────────────────────────────────────────────────────────────────
91
92/// A number as the kernel computes with it: an interval `[lo, hi]` of
93/// `f64` with outward-rounded arithmetic. The default is zero, `[0, 0]`.
94/// A plain value, borrowing nothing: what is worked out in one can be kept
95/// as any other value — a drawing's views, in the session editing it.
96#[derive(Copy, Clone, Debug, Default, PartialEq)]
97pub struct Scalar {
98    pub lo: f64,
99    pub hi: f64,
100}
101
102impl Display for Scalar {
103    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
104        write!(f, "{:.3}", self.to_f64())
105    }
106}
107
108#[allow(
109    clippy::should_implement_trait,
110    reason = "the arithmetic is fallible (`div`) and spelled as `Number` spells it, so a residual reads the same over either"
111)]
112impl Scalar {
113    pub const ZERO: Self = Scalar { lo: 0.0, hi: 0.0 };
114    pub const ONE: Self = Scalar { lo: 1.0, hi: 1.0 };
115    pub const TWO: Self = Scalar { lo: 2.0, hi: 2.0 };
116
117    // π ≈ 3.141592653589793   (next_down/up computed at compile time as literals)
118    pub const PI: Self = Scalar {
119        lo: std::f64::consts::PI,   // rounded value is a lower bound for π
120        hi: 3.1415926535897936_f64, // next_up(π)
121    };
122    /// Saturation sentinel — set on overflow.
123    pub const INFINITY: Self = Scalar {
124        lo: f64::INFINITY,
125        hi: f64::INFINITY,
126    };
127    /// The "entire" interval `(-inf, inf)` — the top element of the interval
128    /// lattice. `could_be_equal`/`could_be_greater`/`could_be_less` against
129    /// it are always `true`, and it never satisfies `definitely_*`. Used to
130    /// represent a value or a whole curve/surface whose position is not yet
131    /// known — an unsharp placeholder that automatically passes any
132    /// overlap/equality check made against it.
133    pub const ENTIRE: Self = Scalar {
134        lo: f64::NEG_INFINITY,
135        hi: f64::INFINITY,
136    };
137
138    // ── Construction ─────────────────────────────────────────────────────────
139
140    /// Create a proper interval. Panics in debug if lo > hi.
141    #[inline]
142    pub fn new(lo: f64, hi: f64) -> Self {
143        debug_assert!(lo <= hi, "Scalar::new: lo ({lo}) > hi ({hi})");
144        Scalar { lo, hi }
145    }
146
147    /// Degenerate (point) interval.
148    #[inline]
149    pub fn degenerate(v: f64) -> Self {
150        Scalar { lo: v, hi: v }
151    }
152
153    #[inline]
154    pub fn from_i64(v: i64) -> Self {
155        Scalar::degenerate(v as f64)
156    }
157
158    #[inline]
159    pub fn from_f64(v: f64) -> Self {
160        Scalar::degenerate(v)
161    }
162
163    #[inline]
164    pub fn from_ratio(num: i64, den: i64) -> GeopResult<Self> {
165        if den == 0 {
166            return Err(GeopError::new("Scalar::from_ratio: denominator is zero"));
167        }
168        let exact = num as f64 / den as f64;
169        Ok(Scalar::new(next_down(exact), next_up(exact)))
170    }
171
172    /// Approximate f64 midpoint, `(lo + hi) / 2`. Used only for
173    /// rendering/debugging.
174    #[inline]
175    pub fn to_f64(self) -> f64 {
176        (self.lo + self.hi) / 2.0
177    }
178
179    // ── Arithmetic ───────────────────────────────────────────────────────────
180
181    #[inline]
182    pub fn add(self, other: Self) -> Self {
183        Scalar::new(next_down(self.lo + other.lo), next_up(self.hi + other.hi))
184    }
185
186    #[inline]
187    pub fn sub(self, other: Self) -> Self {
188        Scalar::new(next_down(self.lo - other.hi), next_up(self.hi - other.lo))
189    }
190
191    #[inline]
192    pub fn mul(self, other: Self) -> Self {
193        let products = [
194            self.lo * other.lo,
195            self.lo * other.hi,
196            self.hi * other.lo,
197            self.hi * other.hi,
198        ];
199        let lo = products.iter().cloned().fold(f64::INFINITY, f64::min);
200        let hi = products.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
201        Scalar::new(next_down(lo), next_up(hi))
202    }
203
204    #[inline]
205    pub fn neg(self) -> Self {
206        Scalar::new(-self.hi, -self.lo)
207    }
208
209    /// Fails where the divisor could be zero.
210    #[inline]
211    pub fn div(self, other: Self) -> GeopResult<Self> {
212        if other.lo <= 0.0 && other.hi >= 0.0 {
213            return Err(GeopError::new(
214                "Scalar::div: divisor interval contains zero",
215            ));
216        }
217        // [lo,hi] * [1/hi', 1/lo']
218        let inv_lo = next_down(1.0 / other.hi);
219        let inv_hi = next_up(1.0 / other.lo);
220        let inv = Scalar::new(inv_lo, inv_hi);
221        Ok(self.mul(inv))
222    }
223
224    // ── Real-valued operations ───────────────────────────────────────────────
225
226    #[inline]
227    pub fn abs(self) -> Self {
228        if self.lo >= 0.0 {
229            self
230        } else if self.hi <= 0.0 {
231            Scalar::new(-self.hi, -self.lo)
232        } else {
233            Scalar::new(0.0, self.lo.abs().max(self.hi.abs()))
234        }
235    }
236
237    #[inline]
238    pub fn sqrt(self) -> GeopResult<Self> {
239        if self.hi < 0.0 {
240            return Err(GeopError::new(
241                "Scalar::sqrt: interval is definitely negative",
242            ));
243        }
244        let lo_clamped = if self.lo < 0.0 { 0.0 } else { self.lo };
245        Ok(Scalar::new(
246            next_down(lo_clamped.sqrt()),
247            next_up(self.hi.sqrt()),
248        ))
249    }
250
251    /// Outward-rounded enclosure of `sin` over the whole interval
252    /// (radians). Total — never fails, even for [`Scalar::ENTIRE`] or an
253    /// [`Scalar::INFINITY`]-adjacent value, which just widen to `[-1, 1]`.
254    #[inline]
255    pub fn sin(self) -> Self {
256        use std::f64::consts::FRAC_PI_2;
257        let (lo, hi) = interval_trig(self.lo, self.hi, f64::sin, FRAC_PI_2, -FRAC_PI_2);
258        Scalar::new(lo, hi)
259    }
260
261    /// Outward-rounded enclosure of `cos` (see [`Scalar::sin`]).
262    #[inline]
263    pub fn cos(self) -> Self {
264        use std::f64::consts::PI;
265        let (lo, hi) = interval_trig(self.lo, self.hi, f64::cos, 0.0, PI);
266        Scalar::new(lo, hi)
267    }
268
269    /// Outward-rounded enclosure of `acos` over the interval, in `[0, π]`:
270    /// the angle whose cosine it is. The part of the interval beyond
271    /// `[-1, 1]` is left out; an interval with nothing in `[-1, 1]` fails.
272    #[inline]
273    pub fn acos(self) -> GeopResult<Self> {
274        if self.hi < -1.0 || self.lo > 1.0 {
275            return Err(GeopError::new(format!(
276                "Scalar::acos: {self:?} lies outside [-1, 1]"
277            )));
278        }
279        // Decreasing: the upper bound gives the smaller angle. Widened by
280        // an ULP against roundoff in the libm call, like `sin`/`cos`.
281        let (lo, hi) = (self.lo.max(-1.0), self.hi.min(1.0));
282        Ok(Scalar::new(
283            next_down(hi.acos()).max(0.0),
284            next_up(lo.acos()).min(next_up(std::f64::consts::PI)),
285        ))
286    }
287
288    /// The angle `self`, given in degrees, in radians.
289    pub fn degrees_to_radians(self) -> GeopResult<Self> {
290        self.mul(Self::PI).div(Self::from_i64(180))
291    }
292
293    /// An enclosure of the angle of the point `(x, self)` from the `x` axis,
294    /// in radians, in `(-pi, pi]`: of `atan2(y, x)` for every `y` in `self`
295    /// and `x` in `x`. All of `[-pi, pi]` where that is not one range of
296    /// angles: the box `(x, self)` holds the origin, or reaches across the
297    /// negative `x` axis, where the angle jumps.
298    ///
299    /// Over a box clear of the origin the angle is least and greatest at
300    /// corners. Those are taken in `f64` — free choices of where to put the
301    /// bounds — and then each is proven, not trusted: a bound `b` holds when
302    /// every point of the box lies on the far side of the ray at angle `b`,
303    /// `cos(b) y - sin(b) x` definitely of the right sign, evaluated in the
304    /// scalar's own outward-rounded arithmetic. Where rounding leaves that
305    /// undecided the bound moves outward, by steps that double, until it
306    /// holds.
307    pub fn atan2(self, x: Self) -> Self {
308        let y = self;
309        let whole = Self::PI.neg().union(Self::PI);
310        // On the negative `x` axis itself the angle is `pi`, just below it
311        // near `-pi`: a box reaching below it from there jumps too.
312        if (y.could_be_equal(Self::ZERO) && x.could_be_equal(Self::ZERO))
313            || (x.could_be_less(Self::ZERO)
314                && y.could_be_less(Self::ZERO)
315                && !y.definitely_less(Self::ZERO))
316        {
317            return whole;
318        }
319        // `+ 0.0` turns a zero's sign positive: on the negative `x` axis
320        // the angle is `pi`, never `-pi`.
321        let ys = [y.lower().to_f64() + 0.0, y.upper().to_f64() + 0.0];
322        let xs = [x.lower().to_f64(), x.upper().to_f64()];
323        let corners = ys.iter().flat_map(|&a| xs.iter().map(move |&b| a.atan2(b)));
324        let (low, high) = corners.fold((f64::INFINITY, f64::NEG_INFINITY), |(l, h), a| {
325            (l.min(a), h.max(a))
326        });
327        // Whether every point of the box is at an angle `>= b` (`sign = 1`)
328        // or `<= b` (`sign = -1`).
329        let holds = |b: f64, sign: f64| {
330            let b = Self::from_f64(b);
331            let side = b.cos().mul(y).sub(b.sin().mul(x)).mul(Self::from_f64(sign));
332            !side.could_be_less(Self::ZERO)
333        };
334        let prove = |start: f64, sign: f64, limit: f64| -> Option<f64> {
335            let mut b = start;
336            let mut step = f64::EPSILON * start.abs().max(1.0);
337            for _ in 0..64 {
338                if holds(b, sign) {
339                    return Some(b);
340                }
341                b -= sign * step;
342                step *= 2.0;
343                if (b - limit) * sign < 0.0 {
344                    return None;
345                }
346            }
347            None
348        };
349        let pi = std::f64::consts::PI;
350        match (prove(low, 1.0, -pi - 1.0), prove(high, -1.0, pi + 1.0)) {
351            (Some(lo), Some(hi)) => Self::from_f64(lo)
352                .union(Self::from_f64(hi))
353                .intersect(whole),
354            _ => whole,
355        }
356    }
357
358    // ── Three-valued comparisons ─────────────────────────────────────────────
359
360    #[inline]
361    pub fn could_be_equal(self, other: Self) -> bool {
362        self.lo <= other.hi && other.lo <= self.hi
363    }
364
365    #[inline]
366    pub fn definitely_not_equal(self, other: Self) -> bool {
367        self.hi < other.lo || self.lo > other.hi
368    }
369
370    #[inline]
371    pub fn could_be_greater(self, other: Self) -> bool {
372        self.hi > other.lo
373    }
374
375    #[inline]
376    pub fn definitely_greater(self, other: Self) -> bool {
377        self.lo > other.hi
378    }
379
380    #[inline]
381    pub fn could_be_less(self, other: Self) -> bool {
382        self.lo < other.hi
383    }
384
385    #[inline]
386    pub fn definitely_less(self, other: Self) -> bool {
387        self.hi < other.lo
388    }
389
390    /// Whether it is definitely zero — no value it could be is less or more
391    /// than zero: an exact zero, such as the entry of a column a row has
392    /// nothing in.
393    pub fn is_zero(self) -> bool {
394        !self.could_be_less(Self::ZERO) && !self.could_be_greater(Self::ZERO)
395    }
396
397    /// The sign it definitely has: `Some(true)` if it is definitely
398    /// positive, `Some(false)` if definitely negative, `None` where it could
399    /// be zero.
400    pub fn definite_sign(self) -> Option<bool> {
401        if self.definitely_greater(Self::ZERO) {
402            Some(true)
403        } else if self.definitely_less(Self::ZERO) {
404            Some(false)
405        } else {
406            None
407        }
408    }
409
410    // ── Finiteness ───────────────────────────────────────────────────────────
411
412    #[inline]
413    pub fn is_infinite(self) -> bool {
414        self.lo == f64::NEG_INFINITY || self.hi == f64::INFINITY
415    }
416
417    #[inline]
418    pub fn is_finite(self) -> bool {
419        self.lo.is_finite() && self.hi.is_finite()
420    }
421
422    // ── Set-valued helpers ───────────────────────────────────────────────────
423
424    #[inline]
425    pub fn midpoint(self) -> Self {
426        let m = (self.lo + self.hi) / 2.0;
427        Scalar::degenerate(m)
428    }
429
430    /// True iff this value carries no width — it's a single, exactly-known
431    /// point, not a genuine range of possibility.
432    #[inline]
433    pub fn is_sharp(self) -> bool {
434        self.lo == self.hi
435    }
436
437    /// How much possibility this enclosure carries: `hi - lo`, as a **sharp,
438    /// non-negative** value. Zero exactly when [`Scalar::is_sharp`].
439    ///
440    /// This is how much a computed quantity is *not* known. Being sharp
441    /// itself is what makes it usable as a threshold — comparing an uncertain
442    /// width against an uncertain bound could never be decided three-valuedly
443    /// (see `validation::numerical_accuracy`).
444    #[inline]
445    pub fn width(self) -> Self {
446        // Non-negative and sharp, so it can serve as a decidable threshold.
447        let w = (self.hi - self.lo).max(0.0);
448        Scalar::new(w, w)
449    }
450
451    /// The sharp lower endpoint of this enclosure. Every value `self` could
452    /// be is `>= lower()` and `<= upper()`, so these are the *outer* bounds
453    /// to cut at when a search restricts a domain to an enclosure of its
454    /// answer: a cut there never loses a solution (unlike
455    /// [`Scalar::sharpen`], which would cut through the enclosure).
456    #[inline]
457    pub fn lower(self) -> Self {
458        Self::degenerate(self.lo)
459    }
460
461    /// The sharp upper endpoint (see [`Scalar::lower`]).
462    #[inline]
463    pub fn upper(self) -> Self {
464        Self::degenerate(self.hi)
465    }
466
467    /// Collapse to a single representative point (currently the midpoint,
468    /// like [`Scalar::midpoint`], but named for its distinct *purpose*: use
469    /// this only when you are free to pick *any* value within `self` and
470    /// don't need to preserve which one — e.g. choosing where to place a
471    /// new knot when subdividing a curve at an arbitrary interior point.
472    /// **Never** use this to compress a value that represents a genuinely
473    /// uncertain physical quantity (a search's converged bound, a measured
474    /// position) — that would silently discard real uncertainty rather than
475    /// making an arbitrary, harmless choice.
476    ///
477    /// Exists to break a specific class of interval blowup: repeatedly
478    /// re-deriving a split point as `(t0 + t1) / 2` from an already-widened
479    /// domain propagates and compounds that width forever, even though nothing
480    /// downstream actually cares *which* interior point was chosen — only
481    /// that some valid one was. Sharpening throws that unneeded width away
482    /// at the source instead of letting every later `alpha = (t - e) / (s - e)`
483    /// division amplify it further.
484    pub fn sharpen(self) -> Self {
485        self.midpoint()
486    }
487
488    /// The point a fraction `f` of the way from `lo` to `hi`, sharp: a
489    /// sample position, a free choice (see [`Scalar::sharpen`]) — not an
490    /// enclosure of the point there, which [`Scalar::interpolate`] is.
491    pub fn fraction(lo: Self, hi: Self, f: Self) -> Self {
492        lo.add(hi.sub(lo).mul(f)).sharpen()
493    }
494
495    /// Point a fraction `alpha` of the way from `a` to `b`: `a` at
496    /// `alpha=0`, `b` at `alpha=1`.
497    ///
498    /// Deliberately `a.add(alpha.mul(b.sub(a)))`, *not* the equally-valid
499    /// `a.mul(Scalar::ONE.sub(alpha)).add(b.mul(alpha))` — both give the
500    /// same exact real result, but the latter computes `alpha` and
501    /// `1-alpha` as two *decorrelated* intervals before ever relating `a`
502    /// and `b`, so interval arithmetic can't recognize when they cancel.
503    /// This form computes `b.sub(a)` first: when `a` and `b` are honestly
504    /// the same value (e.g. a weight that should stay exactly `1.0` across
505    /// many subdivisions), that subtraction is exactly `0` regardless of
506    /// `alpha`'s own width, and the whole expression collapses to exactly
507    /// `a` instead of needlessly widening with every call.
508    pub fn interpolate(a: Self, b: Self, alpha: Self) -> Self {
509        // Two algebraically identical forms with *opposite* numerical
510        // strengths, so this evaluates both and keeps their intersection —
511        // both are honest enclosures of the same exact value, so the
512        // narrower parts of each are jointly valid, and no magic tolerance
513        // is involved in preferring them.
514        //
515        // - `a + alpha*(b - a)` mentions `a` twice (so `a`'s own width is
516        //   counted twice, decorrelated) but computes `b - a` first: when
517        //   `a` and `b` are honestly equal — a weight that should stay
518        //   exactly `1.0` across many subdivisions, say — that difference
519        //   is exactly zero and the whole thing collapses to exactly `a`,
520        //   no matter how wide `alpha` is.
521        // - `(1 - alpha)*a + alpha*b` mentions each of `a`/`b` once, so
522        //   wide control points don't get double-counted, but it splits
523        //   `alpha` into two decorrelated factors and so can't see the
524        //   `a == b` cancellation at all.
525        //
526        // Neither dominates: the first is what a repeatedly-split curve's
527        // weights need, the second is what a surface patch with genuinely
528        // wide control points needs.
529        //
530        // Yet neither collapses as the first promises: `b - a` rounds
531        // outward even where it is exactly zero, and `a` plus that is `a`
532        // widened by a bit. So a point between two equal enclosures, no
533        // further than either end, is taken as it is: whatever two values
534        // they stand for, every point between them lies in that enclosure
535        // too. A polynomial patch cut in pieces keeps its weights exactly
536        // one, and stays a polynomial (see `Model::mass_properties`).
537        if a.is_exactly(b) && alpha.lo >= 0.0 && alpha.hi <= 1.0 {
538            return a;
539        }
540        let via_delta = a.add(alpha.mul(b.sub(a)));
541        let via_weights = Self::ONE.sub(alpha).mul(a).add(alpha.mul(b));
542        via_delta.intersect(via_weights)
543    }
544
545    /// The largest value contained in *both* `self` and `other` — the dual
546    /// of [`Scalar::union`]. Callers must only intersect two enclosures of
547    /// the same underlying exact value (as [`Scalar::interpolate`] does);
548    /// given that, the result is still an honest enclosure, just a tighter
549    /// one. Returns the narrower input if the two somehow don't overlap,
550    /// rather than fabricating an empty/inverted interval.
551    #[inline]
552    pub fn intersect(self, other: Self) -> Self {
553        let lo = self.lo.max(other.lo);
554        let hi = self.hi.min(other.hi);
555        if lo <= hi {
556            Scalar::new(lo, hi)
557        } else if self.hi - self.lo <= other.hi - other.lo {
558            self
559        } else {
560            other
561        }
562    }
563
564    /// The smallest value definitely containing both `self` and `other` —
565    /// the scalar-level analog of `Set::union`.
566    #[inline]
567    pub fn union(self, other: Self) -> Self {
568        Self::new(self.lo.min(other.lo), self.hi.max(other.hi))
569    }
570
571    /// True iff `self` is contained in `other` as sets: `other.lo <= self.lo`
572    /// and `self.hi <= other.hi`. This is the rigorous existence/uniqueness
573    /// test a Krawczyk-style contraction relies on (`K(X) ⊆ X`) — distinct
574    /// from [`Scalar::could_be_equal`], which only asks whether the two
575    /// enclosures *overlap*.
576    #[inline]
577    pub fn is_subset_of(self, other: Self) -> bool {
578        other.lo <= self.lo && self.hi <= other.hi
579    }
580
581    /// Whether it is exactly `other`, as a set: both the same single value,
582    /// or both the same range. A weight that is exactly one is skipped
583    /// rather than multiplied by, since the product would only be widened
584    /// by rounding.
585    pub fn is_exactly(self, other: Self) -> bool {
586        self.is_subset_of(other) && other.is_subset_of(self)
587    }
588
589    /// The larger of the two: an enclosure of `max(a, b)` for every `a` and
590    /// `b` the two could be — sharp where both are.
591    pub fn max(self, other: Self) -> Self {
592        let pick = |a: Self, b: Self| if b.definitely_greater(a) { b } else { a };
593        pick(self.lower(), other.lower()).union(pick(self.upper(), other.upper()))
594    }
595
596    /// The smaller of the two (see [`Scalar::max`]).
597    pub fn min(self, other: Self) -> Self {
598        self.neg().max(other.neg()).neg()
599    }
600
601    /// Clamped into `[lo, hi]`: moved to a bound only where it is
602    /// definitely beyond it, else left as it is.
603    pub fn clamped(self, lo: Self, hi: Self) -> Self {
604        if self.definitely_less(lo) {
605            lo
606        } else if self.definitely_greater(hi) {
607            hi
608        } else {
609            self
610        }
611    }
612}
613
614// ── Serialization ─────────────────────────────────────────────────────────────
615
616/// `#[serde(with = "geop_core_math::scalars::as_f64")]`: a scalar as a plain
617/// number — its midpoint written, a sharp scalar read — the way a
618/// [`crate::vector::Vector`] serializes, for what travels to and from a
619/// viewer.
620pub mod as_f64 {
621    use super::Scalar;
622    use serde::{Deserialize, Deserializer, Serializer};
623
624    pub fn serialize<Ser: Serializer>(x: &Scalar, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
625        serializer.serialize_f64(x.to_f64())
626    }
627
628    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Scalar, D::Error> {
629        f64::deserialize(deserializer).map(Scalar::from_f64)
630    }
631
632    /// `#[serde(with = "geop_core_math::scalars::as_f64::vec")]`: a list of
633    /// scalars, each as [`super::as_f64`] has it.
634    pub mod vec {
635        use super::super::Scalar;
636        use serde::{Deserialize, Deserializer, Serializer, ser::SerializeSeq};
637
638        pub fn serialize<Ser: Serializer>(
639            xs: &[Scalar],
640            serializer: Ser,
641        ) -> Result<Ser::Ok, Ser::Error> {
642            let mut seq = serializer.serialize_seq(Some(xs.len()))?;
643            for x in xs {
644                seq.serialize_element(&x.to_f64())?;
645            }
646            seq.end()
647        }
648
649        pub fn deserialize<'de, D: Deserializer<'de>>(
650            deserializer: D,
651        ) -> Result<Vec<Scalar>, D::Error> {
652            Vec::<f64>::deserialize(deserializer)
653                .map(|xs| xs.into_iter().map(Scalar::from_f64).collect())
654        }
655    }
656
657    /// `#[serde(with = "geop_core_math::scalars::as_f64::option")]`: a
658    /// scalar that may be missing, as [`super::as_f64`] has it, or `null`.
659    pub mod option {
660        use super::super::Scalar;
661        use serde::{Deserialize, Deserializer, Serializer};
662
663        pub fn serialize<Ser: Serializer>(
664            x: &Option<Scalar>,
665            serializer: Ser,
666        ) -> Result<Ser::Ok, Ser::Error> {
667            match x {
668                Some(x) => serializer.serialize_some(&x.to_f64()),
669                None => serializer.serialize_none(),
670            }
671        }
672
673        pub fn deserialize<'de, D: Deserializer<'de>>(
674            deserializer: D,
675        ) -> Result<Option<Scalar>, D::Error> {
676            Option::<f64>::deserialize(deserializer).map(|x| x.map(Scalar::from_f64))
677        }
678    }
679}
680
681// ── Tests ─────────────────────────────────────────────────────────────────────
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    fn pt(v: f64) -> Scalar {
688        Scalar::degenerate(v)
689    }
690
691    fn iv(lo: f64, hi: f64) -> Scalar {
692        Scalar::new(lo, hi)
693    }
694
695    #[test]
696    fn arithmetic_add() {
697        let r = pt(1.0).add(pt(2.0));
698        assert!(r.could_be_equal(pt(3.0)));
699    }
700
701    #[test]
702    fn arithmetic_sub() {
703        let r = pt(5.0).sub(pt(3.0));
704        assert!(r.could_be_equal(pt(2.0)));
705    }
706
707    #[test]
708    fn arithmetic_mul() {
709        let r = pt(3.0).mul(pt(4.0));
710        assert!(r.could_be_equal(pt(12.0)));
711    }
712
713    #[test]
714    fn arithmetic_div() {
715        let r = pt(10.0).div(pt(2.0)).unwrap();
716        assert!(r.could_be_equal(pt(5.0)));
717    }
718
719    #[test]
720    fn arithmetic_sqrt_4() {
721        let r = pt(4.0).sqrt().unwrap();
722        assert!(r.could_be_equal(pt(2.0)));
723    }
724
725    #[test]
726    fn arithmetic_sqrt_9() {
727        let r = pt(9.0).sqrt().unwrap();
728        assert!(r.could_be_equal(pt(3.0)));
729    }
730
731    #[test]
732    fn arithmetic_abs() {
733        assert!(pt(-3.0).abs().could_be_equal(pt(3.0)));
734    }
735
736    #[test]
737    fn arithmetic_neg() {
738        assert!(pt(-5.0).neg().could_be_equal(pt(5.0)));
739    }
740
741    #[test]
742    fn sin_at_a_point() {
743        assert!(pt(0.0).sin().could_be_equal(pt(0.0)));
744        assert!(
745            pt(std::f64::consts::FRAC_PI_2)
746                .sin()
747                .could_be_equal(pt(1.0))
748        );
749    }
750
751    #[test]
752    fn cos_at_a_point() {
753        assert!(pt(0.0).cos().could_be_equal(pt(1.0)));
754    }
755
756    #[test]
757    fn sin_over_a_peak_reaches_exactly_one() {
758        // [0, pi] straddles the sin maximum at pi/2; the lower bound stays
759        // outward-rounded (sin(0) = 0 exactly, but sin(pi) is a hair above
760        // zero in f64, and the enclosure must not round that away).
761        let r = iv(0.0, std::f64::consts::PI).sin();
762        assert!(r.hi == 1.0 && r.lo >= -1e-15);
763    }
764
765    #[test]
766    fn cos_over_a_full_turn_is_entire_range() {
767        let r = iv(0.0, std::f64::consts::TAU).cos();
768        assert!(r.lo == -1.0 && r.hi == 1.0);
769    }
770
771    #[test]
772    fn sin_narrow_interval_encloses_true_value() {
773        // A narrow enclosure of 0.5 radians; sin(0.5) should lie in the result.
774        let r = iv(0.5 - 1e-9, 0.5 + 1e-9).sin();
775        let truth = 0.5_f64.sin();
776        assert!(r.lo <= truth && truth <= r.hi);
777    }
778
779    #[test]
780    fn err_div_by_zero() {
781        assert!(pt(1.0).div(Scalar::ZERO).is_err());
782    }
783
784    #[test]
785    fn err_sqrt_negative() {
786        assert!(pt(-1.0).sqrt().is_err());
787    }
788
789    #[test]
790    fn sqrt_straddles_zero_ok() {
791        // Interval [-1, 4] straddles zero — sqrt should succeed and contain 2
792        let r = iv(-1.0, 4.0).sqrt().unwrap();
793        assert!(r.could_be_equal(pt(2.0)));
794    }
795
796    #[test]
797    fn acos_encloses_the_angle() {
798        let r = iv(-0.5, 0.5).acos().unwrap();
799        assert!(r.could_be_equal(pt(std::f64::consts::FRAC_PI_2)));
800        assert!(r.lo <= std::f64::consts::FRAC_PI_3 && r.hi >= 2.0 * std::f64::consts::FRAC_PI_3);
801        assert!(pt(-1.0).acos().unwrap().could_be_equal(Scalar::PI));
802        assert!(pt(1.5).acos().is_err());
803    }
804
805    #[test]
806    fn overflow_infinity() {
807        let big = pt(f64::MAX);
808        let result = big.mul(big);
809        assert!(result.is_infinite());
810    }
811
812    #[test]
813    fn cmp_definitely_greater() {
814        assert!(pt(3.0).definitely_greater(pt(2.0)));
815    }
816
817    #[test]
818    fn cmp_could_be_less_false() {
819        assert!(!pt(3.0).definitely_less(pt(2.0)));
820    }
821
822    #[test]
823    fn cmp_overlapping_intervals_could_be_equal() {
824        assert!(iv(1.0, 3.0).could_be_equal(iv(2.0, 4.0)));
825    }
826
827    #[test]
828    fn cmp_disjoint_intervals_definitely_not_equal() {
829        assert!(iv(1.0, 2.0).definitely_not_equal(iv(3.0, 4.0)));
830    }
831
832    #[test]
833    fn entire_could_be_equal_point() {
834        assert!(Scalar::ENTIRE.could_be_equal(pt(42.0)));
835        assert!(pt(-1e300).could_be_equal(Scalar::ENTIRE));
836    }
837
838    #[test]
839    fn entire_could_be_equal_interval() {
840        assert!(Scalar::ENTIRE.could_be_equal(iv(-5.0, 5.0)));
841    }
842
843    #[test]
844    fn entire_could_be_greater_and_less() {
845        assert!(Scalar::ENTIRE.could_be_greater(pt(1e300)));
846        assert!(Scalar::ENTIRE.could_be_less(pt(-1e300)));
847    }
848
849    #[test]
850    fn entire_never_definitely() {
851        assert!(!Scalar::ENTIRE.definitely_not_equal(pt(0.0)));
852        assert!(!Scalar::ENTIRE.definitely_greater(pt(1e300)));
853        assert!(!Scalar::ENTIRE.definitely_less(pt(-1e300)));
854    }
855
856    #[test]
857    fn fp_enclosure_01_plus_02() {
858        // 0.1 + 0.2 is famously not exactly 0.3; the interval should still enclose the true sum
859        let a = iv(next_down(0.1), next_up(0.1));
860        let b = iv(next_down(0.2), next_up(0.2));
861        let c = iv(next_down(0.3), next_up(0.3));
862        let sum = a.add(b);
863        assert!(sum.could_be_equal(c));
864    }
865
866    /// `atan2` encloses the true angle tightly in every quadrant, on the
867    /// axes, and right at the jump on the negative `x` axis.
868    #[test]
869    fn atan2_encloses_the_angle() {
870        let f = Scalar::from_f64;
871        for degrees in (-179..=180).step_by(7).chain([0, 90, 180, -90, 45]) {
872            let a = (degrees as f64).to_radians();
873            let angle = f(a.sin()).atan2(f(a.cos()));
874            assert!(angle.could_be_equal(f(a)), "{degrees}: {angle:?}");
875            assert!(angle.width().to_f64() < 1e-8, "{degrees}: {angle:?}");
876        }
877        // pi itself, enclosed.
878        let pi = Scalar::ZERO.atan2(f(-1.0));
879        assert!(pi.could_be_equal(Scalar::PI), "{pi:?}");
880        assert!(pi.width().to_f64() < 1e-8, "{pi:?}");
881        // A box across the jump, or round the origin: all angles.
882        let across = f(-1e-3).union(f(1e-3)).atan2(f(-1.0));
883        assert!(across.could_be_equal(Scalar::PI) && across.could_be_equal(Scalar::PI.neg()));
884        let round = f(-1.0).union(f(1.0)).atan2(f(-1.0).union(f(1.0)));
885        assert!(round.could_be_equal(Scalar::PI.neg()) && round.could_be_equal(Scalar::PI));
886        // A wide box clear of the origin: its corners' angles, enclosed.
887        let wide = f(1.0).union(f(2.0)).atan2(f(1.0).union(f(3.0)));
888        assert!(wide.could_be_equal(f(1.0f64.atan2(3.0))));
889        assert!(wide.could_be_equal(f(2.0f64.atan2(1.0))));
890        assert!(!wide.could_be_equal(f(0.3)) && !wide.could_be_equal(f(1.11)));
891    }
892}