Skip to main content

hypercurve/
bezier_parameter.rs

1//! Exact carriers for Bezier split parameters.
2//!
3//! Bezier arrangements eventually need split points whose parameters are not
4//! represented by the scalar `Real` API yet. This module gives those parameters
5//! a first-class exact carrier instead of forcing an approximate collapse: an
6//! exact parameter is either a represented [`Real`] or an algebraic root
7//! described by a power-basis polynomial and an isolating interval in `[0, 1]`.
8//! That is the representation boundary Yap prescribes for exact geometric
9//! computation: construct exact objects first, then branch only through exact
10//! predicates or explicit uncertainty; see Yap, "Towards Exact Geometric
11//! Computation," *Computational Geometry* 7(1-2), 3-23 (1997).
12//!
13//! The root-count validation below uses Sturm sequences. The sign-variation
14//! theorem used here is the classical one from Sturm, "Memoire sur la
15//! resolution des equations numeriques," *Bulletin des Sciences de Ferussac*
16//! 11 (1829). Hypercurve intentionally stores the validated interval with the
17//! parameter so later Bezier boolean and offset APIs can carry a certificate
18//! rather than re-solving the root from scratch.
19//! Linear defining polynomials are additionally recoverable as represented
20//! [`Real`] values when the exact quotient is certified to be the singleton
21//! root. That is the first narrow "true algebraic root materialization" bridge:
22//! it keeps Yap's construction/decision separation, but it avoids retaining an
23//! algebraic wrapper when the exact root already lives in the scalar tower.
24
25use std::cmp::Ordering;
26
27use hyperreal::{Real, RealSign};
28
29use crate::classify::{compare_reals, in_closed_unit_interval, is_zero, real_sign};
30use crate::{
31    BezierMonotoneSpan, Classification, CurveError, CurvePolicy, CurveResult, UncertaintyReason,
32};
33
34/// Power-basis polynomial used to define an algebraic Bezier parameter.
35///
36/// Coefficients are stored from low to high degree, so `coefficients()[0]` is
37/// the constant term. Constructors trim certified trailing zero coefficients
38/// and reject the structurally zero polynomial. Unknown leading-zero status is
39/// reported as [`Classification::Uncertain`] so a topology caller cannot
40/// silently choose the wrong degree.
41#[derive(Clone, Debug, PartialEq)]
42pub struct BezierParameterPolynomial {
43    coefficients: Vec<Real>,
44}
45
46/// Closed isolating interval for a Bezier parameter root.
47///
48/// The interval is always certified to lie inside `[0, 1]` and to satisfy
49/// `start <= end`. `BezierAlgebraicParameter2` additionally requires the
50/// defining polynomial to have no endpoint root and exactly one distinct root
51/// in this interval under Sturm validation.
52#[derive(Clone, Debug, PartialEq)]
53pub struct BezierParameterInterval {
54    start: Real,
55    end: Real,
56}
57
58/// Algebraic Bezier parameter represented by a polynomial and isolating interval.
59///
60/// This is the minimum certificate needed by native Bezier boolean/offset
61/// materialization: consumers can retain the exact defining equation, carry
62/// the bracket through API boundaries, and ask for ordering only when interval
63/// separation proves it. The `root_count` is stored explicitly so downstream
64/// code can assert that the object was validated as a singleton isolator.
65#[derive(Clone, Debug, PartialEq)]
66pub struct BezierAlgebraicParameter2 {
67    polynomial: BezierParameterPolynomial,
68    interval: BezierParameterInterval,
69    root_count: usize,
70}
71
72/// Exact Bezier parameter carrier.
73#[allow(clippy::large_enum_variant)]
74#[derive(Clone, Debug, PartialEq)]
75pub enum BezierParameter2 {
76    /// A parameter represented directly by `Real`.
77    Exact(Real),
78    /// A parameter represented as one isolated algebraic root.
79    Algebraic(BezierAlgebraicParameter2),
80}
81
82impl BezierParameterPolynomial {
83    /// Constructs a nonzero power-basis polynomial.
84    pub fn try_new_power_basis(
85        coefficients: Vec<Real>,
86        policy: &CurvePolicy,
87    ) -> CurveResult<Classification<Self>> {
88        match normalize_coefficients(coefficients, policy)? {
89            Classification::Decided(Some(coefficients)) => {
90                Ok(Classification::Decided(Self { coefficients }))
91            }
92            Classification::Decided(None) => Err(CurveError::InvalidBezierPolynomial),
93            Classification::Uncertain(reason) => Ok(Classification::Uncertain(reason)),
94        }
95    }
96
97    /// Returns coefficients in low-to-high power-basis order.
98    pub fn coefficients(&self) -> &[Real] {
99        &self.coefficients
100    }
101
102    /// Returns the certified degree.
103    pub fn degree(&self) -> usize {
104        self.coefficients.len() - 1
105    }
106
107    /// Evaluates the polynomial at `parameter` using Horner's rule.
108    pub fn evaluate(&self, parameter: &Real) -> Real {
109        evaluate_coefficients(&self.coefficients, parameter)
110    }
111
112    /// Counts distinct roots in `interval` using a Sturm sequence.
113    ///
114    /// The interval endpoints must not themselves be roots. Endpoint roots are
115    /// legitimate split parameters, but they should be represented with
116    /// [`BezierParameter2::Exact`] or isolated by a narrower interval. This
117    /// avoids half-open endpoint conventions leaking into arrangement code.
118    pub fn root_count_in_interval(
119        &self,
120        interval: &BezierParameterInterval,
121        policy: &CurvePolicy,
122    ) -> CurveResult<Classification<usize>> {
123        let start_value = self.evaluate(interval.start());
124        let end_value = self.evaluate(interval.end());
125        match (
126            real_sign(&start_value, policy),
127            real_sign(&end_value, policy),
128        ) {
129            (Some(RealSign::Zero), _) | (_, Some(RealSign::Zero)) => {
130                return Err(CurveError::InvalidBezierAlgebraicParameter);
131            }
132            (Some(_), Some(_)) => {}
133            _ => return Ok(Classification::Uncertain(UncertaintyReason::RealSign)),
134        }
135
136        let sequence = match sturm_sequence(&self.coefficients, policy)? {
137            Classification::Decided(sequence) => sequence,
138            Classification::Uncertain(reason) => return Ok(Classification::Uncertain(reason)),
139        };
140        let start_variations = sign_variations_at(&sequence, interval.start(), policy)?;
141        let end_variations = sign_variations_at(&sequence, interval.end(), policy)?;
142        match (start_variations, end_variations) {
143            (Classification::Decided(start), Classification::Decided(end)) => {
144                Ok(Classification::Decided(start.saturating_sub(end)))
145            }
146            (Classification::Uncertain(reason), _) | (_, Classification::Uncertain(reason)) => {
147                Ok(Classification::Uncertain(reason))
148            }
149        }
150    }
151}
152
153impl BezierParameterInterval {
154    /// Constructs a closed interval in Bezier parameter space.
155    pub fn try_new(
156        start: Real,
157        end: Real,
158        policy: &CurvePolicy,
159    ) -> CurveResult<Classification<Self>> {
160        let in_start = in_closed_unit_interval(&start, policy);
161        let in_end = in_closed_unit_interval(&end, policy);
162        match (in_start, in_end) {
163            (Some(false), _) | (_, Some(false)) => return Err(CurveError::InvalidBezierParameter),
164            (Some(true), Some(true)) => {}
165            _ => return Ok(Classification::Uncertain(UncertaintyReason::Ordering)),
166        }
167
168        match compare_reals(&start, &end, policy) {
169            Some(Ordering::Greater) => Err(CurveError::InvalidBezierRange),
170            Some(_) => Ok(Classification::Decided(Self { start, end })),
171            None => Ok(Classification::Uncertain(UncertaintyReason::Ordering)),
172        }
173    }
174
175    /// Converts an existing monotone span into a validated parameter interval.
176    pub fn from_monotone_span(
177        span: &BezierMonotoneSpan,
178        policy: &CurvePolicy,
179    ) -> CurveResult<Classification<Self>> {
180        Self::try_new(span.start().clone(), span.end().clone(), policy)
181    }
182
183    /// Returns the interval start.
184    pub const fn start(&self) -> &Real {
185        &self.start
186    }
187
188    /// Returns the interval end.
189    pub const fn end(&self) -> &Real {
190        &self.end
191    }
192}
193
194impl BezierAlgebraicParameter2 {
195    /// Validates a singleton algebraic Bezier parameter isolator.
196    pub fn try_isolate(
197        polynomial: BezierParameterPolynomial,
198        interval: BezierParameterInterval,
199        policy: &CurvePolicy,
200    ) -> CurveResult<Classification<Self>> {
201        let count = match polynomial.root_count_in_interval(&interval, policy)? {
202            Classification::Decided(count) => count,
203            Classification::Uncertain(reason) => return Ok(Classification::Uncertain(reason)),
204        };
205        if count != 1 {
206            return Err(CurveError::InvalidBezierAlgebraicParameter);
207        }
208
209        Ok(Classification::Decided(Self {
210            polynomial,
211            interval,
212            root_count: count,
213        }))
214    }
215
216    /// Returns the defining polynomial.
217    pub const fn polynomial(&self) -> &BezierParameterPolynomial {
218        &self.polynomial
219    }
220
221    /// Returns the certified isolating interval.
222    pub const fn interval(&self) -> &BezierParameterInterval {
223        &self.interval
224    }
225
226    /// Returns the certified distinct-root count for the interval.
227    pub const fn root_count(&self) -> usize {
228        self.root_count
229    }
230
231    /// Returns the represented root when this isolator is a certified linear equation.
232    ///
233    /// For a defining polynomial `c0 + c1 t`, the root is `-c0/c1`.  The
234    /// quotient is accepted only when it is certified to lie in `[0, 1]`, lie
235    /// inside the stored isolating interval, and evaluate the defining
236    /// polynomial to zero.  Higher-degree polynomials return `None` even when
237    /// they happen to have a rational root, because extracting those roots
238    /// needs a separate exact factorization/resultant step rather than an
239    /// opportunistic approximation.  This is the small exact materialization
240    /// bridge allowed by Yap's construction/decision model; see Yap, "Towards
241    /// Exact Geometric Computation," *Computational Geometry* 7(1-2), 3-23
242    /// (1997).
243    pub fn represented_linear_root(
244        &self,
245        policy: &CurvePolicy,
246    ) -> CurveResult<Classification<Option<Real>>> {
247        if self.polynomial.degree() != 1 {
248            return Ok(Classification::Decided(None));
249        }
250
251        let constant = &self.polynomial.coefficients()[0];
252        let slope = &self.polynomial.coefficients()[1];
253        if is_zero(slope, policy) != Some(false) {
254            return Ok(Classification::Uncertain(UncertaintyReason::RealSign));
255        }
256        let root = ((Real::zero() - constant) / slope.clone())?;
257        match in_closed_unit_interval(&root, policy) {
258            Some(true) => {}
259            Some(false) => return Ok(Classification::Decided(None)),
260            None => return Ok(Classification::Uncertain(UncertaintyReason::Ordering)),
261        }
262        match (
263            compare_reals(self.interval.start(), &root, policy),
264            compare_reals(&root, self.interval.end(), policy),
265        ) {
266            (Some(Ordering::Greater), _) | (_, Some(Ordering::Greater)) => {
267                return Ok(Classification::Decided(None));
268            }
269            (Some(_), Some(_)) => {}
270            _ => return Ok(Classification::Uncertain(UncertaintyReason::Ordering)),
271        }
272        match real_sign(&self.polynomial.evaluate(&root), policy) {
273            Some(RealSign::Zero) => Ok(Classification::Decided(Some(root))),
274            Some(_) => Ok(Classification::Uncertain(UncertaintyReason::RealSign)),
275            None => Ok(Classification::Uncertain(UncertaintyReason::RealSign)),
276        }
277    }
278}
279
280impl BezierParameter2 {
281    /// Constructs a represented exact Bezier parameter.
282    pub fn exact(value: Real, policy: &CurvePolicy) -> CurveResult<Classification<Self>> {
283        match in_closed_unit_interval(&value, policy) {
284            Some(true) => Ok(Classification::Decided(Self::Exact(value))),
285            Some(false) => Err(CurveError::InvalidBezierParameter),
286            None => Ok(Classification::Uncertain(UncertaintyReason::Ordering)),
287        }
288    }
289
290    /// Wraps a validated algebraic Bezier parameter.
291    pub const fn algebraic(value: BezierAlgebraicParameter2) -> Self {
292        Self::Algebraic(value)
293    }
294
295    /// Returns the exact value when represented directly.
296    pub const fn as_exact(&self) -> Option<&Real> {
297        match self {
298            Self::Exact(value) => Some(value),
299            Self::Algebraic(_) => None,
300        }
301    }
302
303    /// Returns true for a directly represented exact parameter.
304    pub const fn is_exact(&self) -> bool {
305        matches!(self, Self::Exact(_))
306    }
307
308    /// Promotes a linearly defined algebraic parameter to a represented exact value.
309    ///
310    /// Nonlinear algebraic parameters are returned unchanged. Linear parameters
311    /// are promoted only through [`BezierAlgebraicParameter2::represented_linear_root`],
312    /// so callers get native materialization exactly when the stored algebraic
313    /// certificate proves a scalar root already representable by [`Real`].
314    pub fn promote_represented_linear_root(
315        self,
316        policy: &CurvePolicy,
317    ) -> CurveResult<Classification<Self>> {
318        match self {
319            Self::Exact(_) => Ok(Classification::Decided(self)),
320            Self::Algebraic(parameter) => match parameter.represented_linear_root(policy)? {
321                Classification::Decided(Some(root)) => {
322                    Ok(Classification::Decided(Self::Exact(root)))
323                }
324                Classification::Decided(None) => {
325                    Ok(Classification::Decided(Self::Algebraic(parameter)))
326                }
327                Classification::Uncertain(reason) => Ok(Classification::Uncertain(reason)),
328            },
329        }
330    }
331
332    /// Returns the known enclosing interval.
333    pub fn known_interval(
334        &self,
335        policy: &CurvePolicy,
336    ) -> CurveResult<Classification<BezierParameterInterval>> {
337        match self {
338            Self::Exact(value) => {
339                BezierParameterInterval::try_new(value.clone(), value.clone(), policy)
340            }
341            Self::Algebraic(value) => Ok(Classification::Decided(value.interval().clone())),
342        }
343    }
344
345    /// Compares parameters when exact values or disjoint isolating intervals prove the order.
346    pub fn cmp_by_interval(
347        &self,
348        other: &Self,
349        policy: &CurvePolicy,
350    ) -> CurveResult<Classification<Ordering>> {
351        if let (Self::Exact(left), Self::Exact(right)) = (self, other) {
352            return Ok(compare_reals(left, right, policy)
353                .map(Classification::Decided)
354                .unwrap_or(Classification::Uncertain(UncertaintyReason::Ordering)));
355        }
356
357        let left = match self.known_interval(policy)? {
358            Classification::Decided(interval) => interval,
359            Classification::Uncertain(reason) => return Ok(Classification::Uncertain(reason)),
360        };
361        let right = match other.known_interval(policy)? {
362            Classification::Decided(interval) => interval,
363            Classification::Uncertain(reason) => return Ok(Classification::Uncertain(reason)),
364        };
365
366        if compare_reals(left.end(), right.start(), policy) == Some(Ordering::Less) {
367            return Ok(Classification::Decided(Ordering::Less));
368        }
369        if compare_reals(right.end(), left.start(), policy) == Some(Ordering::Less) {
370            return Ok(Classification::Decided(Ordering::Greater));
371        }
372
373        Ok(Classification::Uncertain(UncertaintyReason::Ordering))
374    }
375}
376
377fn sturm_sequence(
378    coefficients: &[Real],
379    policy: &CurvePolicy,
380) -> CurveResult<Classification<Vec<Vec<Real>>>> {
381    let p0 = coefficients.to_vec();
382    let p1 = derivative_coefficients(coefficients);
383    let p1 = match normalize_coefficients(p1, policy)? {
384        Classification::Decided(Some(coefficients)) => coefficients,
385        Classification::Decided(None) => return Ok(Classification::Decided(vec![p0])),
386        Classification::Uncertain(reason) => return Ok(Classification::Uncertain(reason)),
387    };
388
389    let mut sequence = vec![p0, p1];
390    while sequence.len() < 64 {
391        let last = sequence[sequence.len() - 1].clone();
392        let previous = sequence[sequence.len() - 2].clone();
393        let remainder = match polynomial_remainder(previous, last, policy)? {
394            Classification::Decided(Some(remainder)) => remainder,
395            Classification::Decided(None) => break,
396            Classification::Uncertain(reason) => return Ok(Classification::Uncertain(reason)),
397        };
398        sequence.push(negate_coefficients(remainder));
399    }
400
401    Ok(Classification::Decided(sequence))
402}
403
404fn sign_variations_at(
405    sequence: &[Vec<Real>],
406    parameter: &Real,
407    policy: &CurvePolicy,
408) -> CurveResult<Classification<usize>> {
409    let mut previous = None;
410    let mut variations = 0_usize;
411
412    for polynomial in sequence {
413        let value = evaluate_coefficients(polynomial, parameter);
414        let sign = match real_sign(&value, policy) {
415            Some(RealSign::Zero) => continue,
416            Some(sign) => sign,
417            None => return Ok(Classification::Uncertain(UncertaintyReason::RealSign)),
418        };
419        if let Some(previous) = previous
420            && previous != sign
421        {
422            variations += 1;
423        }
424        previous = Some(sign);
425    }
426
427    Ok(Classification::Decided(variations))
428}
429
430fn polynomial_remainder(
431    mut remainder: Vec<Real>,
432    divisor: Vec<Real>,
433    policy: &CurvePolicy,
434) -> CurveResult<Classification<Option<Vec<Real>>>> {
435    let divisor = match normalize_coefficients(divisor, policy)? {
436        Classification::Decided(Some(coefficients)) => coefficients,
437        Classification::Decided(None) => {
438            return Ok(Classification::Uncertain(UncertaintyReason::Unsupported));
439        }
440        Classification::Uncertain(reason) => return Ok(Classification::Uncertain(reason)),
441    };
442
443    loop {
444        remainder = match normalize_coefficients(remainder, policy)? {
445            Classification::Decided(Some(coefficients)) => coefficients,
446            Classification::Decided(None) => return Ok(Classification::Decided(None)),
447            Classification::Uncertain(reason) => return Ok(Classification::Uncertain(reason)),
448        };
449        if remainder.len() < divisor.len() {
450            return Ok(Classification::Decided(Some(remainder)));
451        }
452
453        let shift = remainder.len() - divisor.len();
454        let factor = (remainder[remainder.len() - 1].clone() / divisor[divisor.len() - 1].clone())?;
455        for (index, divisor_coefficient) in divisor.iter().enumerate() {
456            let product = &factor * divisor_coefficient;
457            remainder[shift + index] = &remainder[shift + index] - &product;
458        }
459    }
460}
461
462fn normalize_coefficients(
463    mut coefficients: Vec<Real>,
464    policy: &CurvePolicy,
465) -> CurveResult<Classification<Option<Vec<Real>>>> {
466    while let Some(last) = coefficients.last() {
467        match is_zero(last, policy) {
468            Some(true) => {
469                coefficients.pop();
470            }
471            Some(false) => return Ok(Classification::Decided(Some(coefficients))),
472            None => return Ok(Classification::Uncertain(UncertaintyReason::RealSign)),
473        }
474    }
475
476    Ok(Classification::Decided(None))
477}
478
479fn derivative_coefficients(coefficients: &[Real]) -> Vec<Real> {
480    let mut derivative = Vec::with_capacity(coefficients.len().saturating_sub(1));
481    for (degree, coefficient) in coefficients.iter().enumerate().skip(1) {
482        let scale = Real::from(degree as i64);
483        derivative.push(coefficient * &scale);
484    }
485    derivative
486}
487
488fn evaluate_coefficients(coefficients: &[Real], parameter: &Real) -> Real {
489    coefficients
490        .iter()
491        .rev()
492        .fold(Real::zero(), |accumulator, coefficient| {
493            (&accumulator * parameter) + coefficient
494        })
495}
496
497fn negate_coefficients(coefficients: Vec<Real>) -> Vec<Real> {
498    coefficients
499        .into_iter()
500        .map(|coefficient| Real::zero() - coefficient)
501        .collect()
502}