Skip to main content

ogeom_math/
knots.rs

1//! Knot vectors and B-spline basis functions.
2//!
3//! The basis is the foundation of every free-form curve and surface in the
4//! kernel. Everything else (de Boor evaluation, knot insertion, degree
5//! elevation, Bézier decomposition) is built on the functions here.
6//!
7//! # Representation
8//!
9//! A [`KnotVector`] stores the *flat* non-decreasing sequence, with repeated
10//! knots written out. That is what every algorithm wants, and deriving it from
11//! a distinct-knots-plus-multiplicities form on each call would cost an
12//! allocation in the hottest loop in the crate.
13//!
14//! Repeated knots must be bit-identical, and every operation here preserves
15//! that: knot insertion copies the inserted value rather than recomputing it.
16//! Multiplicity is therefore an exact question, not a tolerance one, which
17//! matters because multiplicity determines continuity: a knot of multiplicity
18//! `p` in a degree-`p` curve is a corner, and "nearly a corner" is not a thing.
19//!
20//! # Conventions
21//!
22//! For degree `p` and `n` control points the flat vector has `n + p + 1`
23//! entries. A *clamped* vector repeats its first and last knots `p + 1` times,
24//! so the curve passes through its first and last control points. That is the
25//! usual form for a bounded curve and the one [`KnotVector::clamped_uniform`]
26//! produces.
27
28use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
29use smallvec::SmallVec;
30
31/// Basis values for one span, sized to avoid allocating for typical degrees.
32pub type BasisValues = SmallVec<[f64; 8]>;
33
34/// One row of basis values per derivative order, inline up to the jet
35/// orders the kernel asks for.
36pub type DerivativeRows = SmallVec<[BasisValues; 4]>;
37
38/// Degrees below this take the fixed-array basis path.
39const SMALL_ORDER: usize = 8;
40
41/// A non-decreasing knot sequence with an associated degree.
42#[derive(Debug, Clone, PartialEq)]
43pub struct KnotVector {
44    knots: Vec<f64>,
45    degree: usize,
46}
47
48impl KnotVector {
49    /// A knot vector from a flat non-decreasing sequence.
50    ///
51    /// # Errors
52    ///
53    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the
54    /// sequence is too short for the degree, is not non-decreasing, contains a
55    /// non-finite value, or has an interior knot of multiplicity greater than
56    /// the degree, which would disconnect the curve rather than merely make it
57    /// sharp.
58    pub fn new(knots: Vec<f64>, degree: usize) -> OgeomResult<Self> {
59        if degree == 0 {
60            ogeom_bail!(Construction, "degree must be at least 1");
61        }
62        // n + p + 1 entries for n control points, and n >= p + 1 for the basis
63        // to be well defined over a non-empty domain.
64        let minimum = 2 * (degree + 1);
65        if knots.len() < minimum {
66            ogeom_bail!(
67                Construction,
68                "degree {degree} needs at least {minimum} knots, got {}",
69                knots.len()
70            );
71        }
72        if !knots.iter().all(|k| k.is_finite()) {
73            ogeom_bail!(Construction, "knot vector contains a non-finite value");
74        }
75        if knots.windows(2).any(|w| w[1] < w[0]) {
76            ogeom_bail!(Construction, "knot vector is not non-decreasing");
77        }
78
79        let this = Self { knots, degree };
80        if this.domain_start() >= this.domain_end() {
81            ogeom_bail!(Construction, "knot vector spans an empty domain");
82        }
83
84        // A domain end repeated more than `degree + 1` times leaves a basis
85        // function over an empty span, which evaluates to nothing there.
86        for end in [this.domain_start(), this.domain_end()] {
87            let count = this.multiplicity_of(end);
88            if count > this.degree + 1 {
89                ogeom_bail!(
90                    Construction,
91                    "end knot {end} has multiplicity {count}, above degree + 1 = {}",
92                    this.degree + 1
93                );
94            }
95        }
96
97        // Interior multiplicity above the degree splits the curve in two.
98        let (first, last) = (this.degree, this.knots.len() - this.degree - 1);
99        let mut index = first;
100        while index < last {
101            let value = this.knots[index];
102            let mut count = 0;
103            while index < last && this.knots[index] == value {
104                count += 1;
105                index += 1;
106            }
107            // The two clamp knots at either end of the domain are allowed their
108            // full multiplicity. Only strictly interior ones are constrained.
109            if value > this.domain_start() && value < this.domain_end() && count > this.degree {
110                ogeom_bail!(
111                    Construction,
112                    "interior knot {value} has multiplicity {count}, above degree {}",
113                    this.degree
114                );
115            }
116        }
117        Ok(this)
118    }
119
120    /// A clamped uniform knot vector for `control_points` control points.
121    ///
122    /// The domain is `[0, 1]`, the ends are clamped, and the interior knots are
123    /// evenly spaced.
124    ///
125    /// # Errors
126    ///
127    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if there are
128    /// too few control points for the degree.
129    pub fn clamped_uniform(degree: usize, control_points: usize) -> OgeomResult<Self> {
130        if control_points < degree + 1 {
131            ogeom_bail!(
132                Construction,
133                "degree {degree} needs at least {} control points, got {control_points}",
134                degree + 1
135            );
136        }
137        let interior = control_points - degree - 1;
138        let mut knots = Vec::with_capacity(control_points + degree + 1);
139        knots.extend(core::iter::repeat_n(0.0, degree + 1));
140        for i in 1..=interior {
141            #[allow(clippy::cast_precision_loss)]
142            knots.push(i as f64 / (interior + 1) as f64);
143        }
144        knots.extend(core::iter::repeat_n(1.0, degree + 1));
145        Self::new(knots, degree)
146    }
147
148    /// A clamped knot vector from parameter values, for interpolation.
149    ///
150    /// Uses the averaging rule, which places interior knots so that the
151    /// resulting interpolation system is well conditioned. A uniform vector
152    /// over unevenly spaced parameters gives a nearly singular one.
153    ///
154    /// # Errors
155    ///
156    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if there are
157    /// too few parameters, or they are not strictly increasing.
158    pub fn averaged(degree: usize, parameters: &[f64]) -> OgeomResult<Self> {
159        if parameters.len() < degree + 1 {
160            ogeom_bail!(
161                Construction,
162                "degree {degree} needs at least {} parameters",
163                degree + 1
164            );
165        }
166        if parameters.windows(2).any(|w| w[1] <= w[0]) {
167            ogeom_bail!(Construction, "parameters must be strictly increasing");
168        }
169        let n = parameters.len();
170        let mut knots = Vec::with_capacity(n + degree + 1);
171        knots.extend(core::iter::repeat_n(parameters[0], degree + 1));
172        #[allow(clippy::cast_precision_loss)]
173        for j in 1..n - degree {
174            let mean: f64 = parameters[j..j + degree].iter().sum::<f64>() / degree as f64;
175            knots.push(mean);
176        }
177        knots.extend(core::iter::repeat_n(parameters[n - 1], degree + 1));
178        Self::new(knots, degree)
179    }
180
181    /// The degree.
182    #[must_use]
183    pub const fn degree(&self) -> usize {
184        self.degree
185    }
186
187    /// The flat knot sequence.
188    #[must_use]
189    pub fn knots(&self) -> &[f64] {
190        &self.knots
191    }
192
193    /// The number of control points this vector describes.
194    #[must_use]
195    pub const fn control_point_count(&self) -> usize {
196        self.knots.len() - self.degree - 1
197    }
198
199    /// The first parameter of the usable domain.
200    #[must_use]
201    pub fn domain_start(&self) -> f64 {
202        self.knots[self.degree]
203    }
204
205    /// The last parameter of the usable domain.
206    #[must_use]
207    pub fn domain_end(&self) -> f64 {
208        self.knots[self.knots.len() - self.degree - 1]
209    }
210
211    /// The usable domain.
212    #[must_use]
213    pub fn domain(&self) -> (f64, f64) {
214        (self.domain_start(), self.domain_end())
215    }
216
217    /// Whether the ends are clamped, so the curve meets its first and last
218    /// control points.
219    #[must_use]
220    pub fn is_clamped(&self) -> bool {
221        let last = self.knots.len() - 1;
222        self.knots[..=self.degree]
223            .iter()
224            .all(|k| *k == self.knots[0])
225            && self.knots[last - self.degree..]
226                .iter()
227                .all(|k| *k == self.knots[last])
228    }
229
230    /// The multiplicity of the knot value at `index`.
231    ///
232    /// Exact: repeated knots are bit-identical by construction.
233    #[must_use]
234    pub fn multiplicity_at(&self, index: usize) -> usize {
235        let Some(&value) = self.knots.get(index) else {
236            return 0;
237        };
238        self.knots.iter().filter(|k| **k == value).count()
239    }
240
241    /// The multiplicity of `value`, or zero if it is not a knot.
242    #[must_use]
243    pub fn multiplicity_of(&self, value: f64) -> usize {
244        self.knots.iter().filter(|k| **k == value).count()
245    }
246
247    /// The distinct knot values with their multiplicities, in order.
248    #[must_use]
249    pub fn distinct(&self) -> Vec<(f64, usize)> {
250        let mut out: Vec<(f64, usize)> = Vec::new();
251        for &k in &self.knots {
252            match out.last_mut() {
253                Some((value, count)) if *value == k => *count += 1,
254                _ => out.push((k, 1)),
255            }
256        }
257        out
258    }
259
260    /// Whether `u` lies in the usable domain, within `tol.parametric()`.
261    #[must_use]
262    pub fn contains(&self, u: f64, tol: Tolerances) -> bool {
263        let (start, end) = self.domain();
264        u >= start - tol.parametric() && u <= end + tol.parametric()
265    }
266
267    /// The index of the knot span containing `u`.
268    ///
269    /// Returns `i` with `knots[i] <= u < knots[i+1]`, clamped so that the end of
270    /// the domain resolves to the last non-empty span rather than falling off
271    /// it. Binary search, so cost is logarithmic in the knot count.
272    ///
273    /// # Errors
274    ///
275    /// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) if `u` is outside the
276    /// domain by more than `tol.parametric()`.
277    pub fn span(&self, u: f64, tol: Tolerances) -> OgeomResult<usize> {
278        let (start, end) = self.domain();
279        if !u.is_finite() || u < start - tol.parametric() || u > end + tol.parametric() {
280            ogeom_bail!(Domain, "parameter {u} outside knot domain [{start}, {end}]");
281        }
282        Ok(self.span_unchecked(u))
283    }
284
285    /// The knot span containing `u`, clamping out-of-range values into the
286    /// domain rather than reporting them.
287    #[must_use]
288    pub fn span_unchecked(&self, u: f64) -> usize {
289        let last = self.control_point_count() - 1;
290        // The end of the domain belongs to the last span; without this it would
291        // land one past it, since the search looks for `knots[i] <= u`.
292        if u >= self.knots[last + 1] {
293            return last;
294        }
295        if u <= self.knots[self.degree] {
296            return self.degree;
297        }
298        let mut low = self.degree;
299        let mut high = last + 1;
300        while high - low > 1 {
301            let mid = usize::midpoint(low, high);
302            if u < self.knots[mid] {
303                high = mid;
304            } else {
305                low = mid;
306            }
307        }
308        low
309    }
310
311    /// The `degree + 1` non-zero basis functions at `u`.
312    ///
313    /// Entry `i` is the value of basis function `span - degree + i`. They are
314    /// non-negative and sum to exactly one up to rounding: the partition of
315    /// unity, which is what makes a B-spline curve lie in the convex hull of its
316    /// control points.
317    ///
318    /// Cox-de Boor, in the triangular form that avoids evaluating the zero
319    /// functions and never divides by a zero knot difference.
320    #[must_use]
321    pub fn basis(&self, span: usize, u: f64) -> BasisValues {
322        let p = self.degree;
323        let mut n = BasisValues::with_capacity(p + 1);
324        n.push(1.0);
325        let mut left = BasisValues::with_capacity(p + 1);
326        let mut right = BasisValues::with_capacity(p + 1);
327        left.push(0.0);
328        right.push(0.0);
329
330        for j in 1..=p {
331            left.push(u - self.knots[span + 1 - j]);
332            right.push(self.knots[span + j] - u);
333            let mut saved = 0.0;
334            n.push(0.0);
335            for r in 0..j {
336                // `right[r + 1] + left[j - r]` is the width of the union of two
337                // adjacent supports, which is positive whenever the basis
338                // function is, so this cannot divide by zero for a valid vector.
339                let denominator = right[r + 1] + left[j - r];
340                let temp = n[r] / denominator;
341                n[r] = saved + right[r + 1] * temp;
342                saved = left[j - r] * temp;
343            }
344            n[j] = saved;
345        }
346        n
347    }
348
349    /// The non-zero basis functions and their derivatives up to order `n`.
350    ///
351    /// `result[k][i]` is the `k`th derivative of basis function
352    /// `span - degree + i`. Orders above the degree are identically zero and
353    /// are returned as such rather than as noise.
354    #[must_use]
355    pub fn basis_derivatives(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
356        if self.degree < SMALL_ORDER {
357            return self.basis_derivatives_small(span, u, n);
358        }
359        self.basis_derivatives_any(span, u, n)
360    }
361
362    /// [`Self::basis_derivatives`] for degree below [`SMALL_ORDER`], every
363    /// scratch table a fixed array: the same recurrence, the same
364    /// arithmetic in the same order, so the same bits, without building
365    /// nested vectors on each of the millions of calls a fit or a march
366    /// makes.
367    fn basis_derivatives_small(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
368        const M: usize = SMALL_ORDER;
369        let p = self.degree;
370        let order = n.min(p);
371        let mut ndu = [[0.0_f64; M]; M];
372        ndu[0][0] = 1.0;
373        let mut left = [0.0_f64; M];
374        let mut right = [0.0_f64; M];
375        for j in 1..=p {
376            left[j] = u - self.knots[span + 1 - j];
377            right[j] = self.knots[span + j] - u;
378            let mut saved = 0.0;
379            for r in 0..j {
380                ndu[j][r] = right[r + 1] + left[j - r];
381                let temp = ndu[r][j - 1] / ndu[j][r];
382                ndu[r][j] = saved + right[r + 1] * temp;
383                saved = left[j - r] * temp;
384            }
385            ndu[j][j] = saved;
386        }
387
388        let mut rows = [[0.0_f64; M]; M];
389        for (j, slot) in rows[0].iter_mut().enumerate().take(p + 1) {
390            *slot = ndu[j][p];
391        }
392        let mut a = [[0.0_f64; M]; 2];
393        for r in 0..=p {
394            let (mut s1, mut s2) = (0_usize, 1_usize);
395            a[0][0] = 1.0;
396            for k in 1..=order {
397                let mut d = 0.0;
398                let rk = r as isize - k as isize;
399                let pk = p - k;
400                if r >= k {
401                    a[s2][0] = a[s1][0] / ndu[pk + 1][rk as usize];
402                    d = a[s2][0] * ndu[rk as usize][pk];
403                }
404                let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
405                let j2 = if r as isize - 1 <= pk as isize {
406                    k - 1
407                } else {
408                    p - r
409                };
410                for j in j1..=j2 {
411                    let index = (rk + j as isize) as usize;
412                    a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
413                    d += a[s2][j] * ndu[index][pk];
414                }
415                if r <= pk {
416                    a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
417                    d += a[s2][k] * ndu[r][pk];
418                }
419                rows[k][r] = d;
420                core::mem::swap(&mut s1, &mut s2);
421            }
422        }
423        let mut factor = p;
424        for (k, row) in rows.iter_mut().enumerate().take(order + 1).skip(1) {
425            #[allow(clippy::cast_precision_loss)]
426            let scale = factor as f64;
427            for value in row.iter_mut().take(p + 1) {
428                *value *= scale;
429            }
430            factor = factor.saturating_mul(p.saturating_sub(k));
431        }
432        (0..=n)
433            .map(|k| {
434                if k < M {
435                    BasisValues::from_slice(&rows[k][..=p])
436                } else {
437                    BasisValues::from_elem(0.0, p + 1)
438                }
439            })
440            .collect()
441    }
442
443    fn basis_derivatives_any(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
444        let p = self.degree;
445        let order = n.min(p);
446
447        // `ndu` holds the basis values and the knot differences from the
448        // triangular recurrence. Both halves are needed to build derivatives.
449        // Every scratch row lives inline for the degrees the kernel actually
450        // meets: this is the innermost loop of every spline evaluation, and
451        // a heap row per call there is the kernel's largest allocation source.
452        let mut ndu: SmallVec<[BasisValues; 8]> =
453            core::iter::repeat_with(|| BasisValues::from_elem(0.0, p + 1))
454                .take(p + 1)
455                .collect();
456        ndu[0][0] = 1.0;
457        let mut left = BasisValues::from_elem(0.0, p + 1);
458        let mut right = BasisValues::from_elem(0.0, p + 1);
459
460        for j in 1..=p {
461            left[j] = u - self.knots[span + 1 - j];
462            right[j] = self.knots[span + j] - u;
463            let mut saved = 0.0;
464            for r in 0..j {
465                ndu[j][r] = right[r + 1] + left[j - r];
466                let temp = ndu[r][j - 1] / ndu[j][r];
467                ndu[r][j] = saved + right[r + 1] * temp;
468                saved = left[j - r] * temp;
469            }
470            ndu[j][j] = saved;
471        }
472
473        let mut derivatives: DerivativeRows =
474            core::iter::repeat_with(|| BasisValues::from_elem(0.0, p + 1))
475                .take(n + 1)
476                .collect();
477        for (j, slot) in derivatives[0].iter_mut().enumerate() {
478            *slot = ndu[j][p];
479        }
480        // The rows above `order` stay zero: a derivative past the degree of a
481        // piecewise polynomial is identically zero, not merely small.
482
483        // Two alternating rows of coefficients, per the standard algorithm.
484        let mut a = [
485            BasisValues::from_elem(0.0, p + 1),
486            BasisValues::from_elem(0.0, p + 1),
487        ];
488        for r in 0..=p {
489            let (mut s1, mut s2) = (0_usize, 1_usize);
490            a[0][0] = 1.0;
491            for k in 1..=order {
492                let mut d = 0.0;
493                let rk = r as isize - k as isize;
494                let pk = p - k;
495                if r >= k {
496                    a[s2][0] = a[s1][0] / ndu[pk + 1][rk as usize];
497                    d = a[s2][0] * ndu[rk as usize][pk];
498                }
499                let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
500                let j2 = if r as isize - 1 <= pk as isize {
501                    k - 1
502                } else {
503                    p - r
504                };
505                for j in j1..=j2 {
506                    let index = (rk + j as isize) as usize;
507                    a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
508                    d += a[s2][j] * ndu[index][pk];
509                }
510                if r <= pk {
511                    a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
512                    d += a[s2][k] * ndu[r][pk];
513                }
514                derivatives[k][r] = d;
515                core::mem::swap(&mut s1, &mut s2);
516            }
517        }
518
519        // Multiply through by the falling factorial the recurrence omits.
520        let mut factor = p;
521        for (k, row) in derivatives.iter_mut().enumerate().take(order + 1).skip(1) {
522            #[allow(clippy::cast_precision_loss)]
523            let scale = factor as f64;
524            for value in row.iter_mut() {
525                *value *= scale;
526            }
527            factor = factor.saturating_mul(p.saturating_sub(k));
528        }
529        derivatives
530    }
531
532    /// Insert `value` into the sequence, `count` times.
533    ///
534    /// Only the knots change; adjusting control points to keep the shape is
535    /// [`crate::bspline::insert_knot`].
536    ///
537    /// # Errors
538    ///
539    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the result
540    /// would push a knot's multiplicity above the degree.
541    pub fn with_knot_inserted(&self, value: f64, count: usize) -> OgeomResult<Self> {
542        let mut knots = self.knots.clone();
543        let position = knots.partition_point(|k| *k <= value);
544        for _ in 0..count {
545            knots.insert(position, value);
546        }
547        Self::new(knots, self.degree)
548    }
549
550    /// This vector with its domain mapped onto `[start, end]`.
551    ///
552    /// # Errors
553    ///
554    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the target
555    /// range is empty or non-finite.
556    pub fn reparameterized(&self, start: f64, end: f64) -> OgeomResult<Self> {
557        if !start.is_finite() || !end.is_finite() || end <= start {
558            ogeom_bail!(Construction, "target range [{start}, {end}] is empty");
559        }
560        let (a, b) = self.domain();
561        let scale = (end - start) / (b - a);
562        // Map, then overwrite the knots at the domain's ends with the exact
563        // endpoints: the arithmetic would otherwise give each copy a slightly
564        // different value and silently destroy the clamping. Knots outside
565        // the domain (an unclamped or periodic vector's) map like any other,
566        // each held on its own side of the ends it lies beyond.
567        let knots: Vec<f64> = self
568            .knots
569            .iter()
570            .map(|&k| {
571                let mapped = (k - a).mul_add(scale, start);
572                if k == a {
573                    start
574                } else if k == b {
575                    end
576                } else if k < a {
577                    mapped.min(start)
578                } else if k > b {
579                    mapped.max(end)
580                } else {
581                    mapped.clamp(start, end)
582                }
583            })
584            .collect();
585        Self::new(knots, self.degree)
586    }
587
588    /// This vector with the parameter direction reversed.
589    ///
590    /// The domain is preserved and the sequence of interior spacings is
591    /// mirrored. Reversing a curve reverses its knots and its control points
592    /// together.
593    ///
594    /// Multiplicity is preserved *exactly* (equal knots map through the same
595    /// arithmetic and so stay equal), which is what continuity depends on. The
596    /// interior knot *values* are not bit-exactly restored by reversing twice,
597    /// since `a + b - k` is not an exact involution in floating point. They
598    /// return to within one ulp.
599    #[must_use]
600    pub fn reversed(&self) -> Self {
601        let (a, b) = self.domain();
602        let sum = a + b;
603        // Same reasoning as `reparameterized`: the domain's ends exchange
604        // exactly, and every other knot mirrors, inside the domain or out.
605        let knots: Vec<f64> = self
606            .knots
607            .iter()
608            .rev()
609            .map(|&k| {
610                if k == a {
611                    b
612                } else if k == b {
613                    a
614                } else if k < a {
615                    (sum - k).max(b)
616                } else if k > b {
617                    (sum - k).min(a)
618                } else {
619                    (sum - k).clamp(a, b)
620                }
621            })
622            .collect();
623        Self {
624            knots,
625            degree: self.degree,
626        }
627    }
628}
629
630#[cfg(test)]
631#[allow(clippy::unwrap_used)]
632mod tests {
633    use super::*;
634    use approx::assert_relative_eq;
635
636    const T: Tolerances = Tolerances::millimetres();
637
638    fn cubic() -> KnotVector {
639        // Degree 3, 7 control points, two interior knots.
640        KnotVector::new(
641            vec![0.0, 0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0, 1.0],
642            3,
643        )
644        .unwrap()
645    }
646
647    #[test]
648    fn malformed_vectors_are_refused() {
649        assert!(KnotVector::new(vec![0.0, 1.0], 3).is_err(), "too short");
650        assert!(
651            KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], 3).is_err(),
652            "a start repeated degree + 2 times"
653        );
654        assert!(
655            KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0], 3).is_err(),
656            "an end repeated degree + 2 times"
657        );
658        assert!(
659            KnotVector::new(vec![0.0, 0.0, 1.0, 0.5, 1.0, 1.0], 2).is_err(),
660            "not non-decreasing"
661        );
662        assert!(
663            KnotVector::new(vec![0.0, 0.0, f64::NAN, 1.0, 1.0, 1.0], 2).is_err(),
664            "non-finite"
665        );
666        assert!(KnotVector::new(vec![0.0; 8], 3).is_err(), "empty domain");
667        assert!(KnotVector::new(vec![], 0).is_err(), "degree zero");
668        // Interior multiplicity above the degree disconnects the curve.
669        assert!(KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0], 2).is_err());
670        // At the degree it is merely a corner, which is legitimate.
671        assert!(KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0], 2).is_ok());
672    }
673
674    #[test]
675    fn clamped_uniform_has_the_expected_shape() {
676        let k = KnotVector::clamped_uniform(3, 7).unwrap();
677        assert_eq!(k.knots().len(), 11);
678        assert_eq!(k.control_point_count(), 7);
679        assert_eq!(k.domain(), (0.0, 1.0));
680        assert!(k.is_clamped());
681        assert_eq!(k.multiplicity_of(0.0), 4);
682        assert_eq!(k.multiplicity_of(1.0), 4);
683        assert_relative_eq!(k.knots()[4], 1.0 / 4.0);
684
685        // A Bezier: no interior knots at all.
686        let b = KnotVector::clamped_uniform(3, 4).unwrap();
687        assert_eq!(b.knots(), &[0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]);
688        assert!(KnotVector::clamped_uniform(3, 3).is_err());
689    }
690
691    #[test]
692    fn span_lookup_handles_both_ends_of_the_domain() {
693        let k = cubic();
694        assert_eq!(k.span(0.0, T).unwrap(), 3, "domain start");
695        assert_eq!(k.span(0.1, T).unwrap(), 3);
696        assert_eq!(k.span(0.25, T).unwrap(), 4, "on a knot, span to its right");
697        assert_eq!(k.span(0.3, T).unwrap(), 4);
698        assert_eq!(k.span(0.9, T).unwrap(), 6);
699        // The domain end must resolve to the last span, not one past it.
700        assert_eq!(k.span(1.0, T).unwrap(), 6);
701        assert!(k.span(-0.1, T).is_err());
702        assert!(k.span(1.1, T).is_err());
703        assert!(k.span(f64::NAN, T).is_err());
704    }
705
706    #[test]
707    fn basis_functions_form_a_partition_of_unity() {
708        let k = cubic();
709        for i in 0..=200 {
710            let u = f64::from(i) / 200.0;
711            let span = k.span(u, T).unwrap();
712            let n = k.basis(span, u);
713            assert_eq!(n.len(), 4);
714            let sum: f64 = n.iter().sum();
715            assert_relative_eq!(sum, 1.0, epsilon = 1e-14);
716            assert!(n.iter().all(|v| *v >= -1e-15), "basis must be non-negative");
717        }
718    }
719
720    #[test]
721    fn basis_matches_the_bernstein_polynomials_for_a_bezier() {
722        // With no interior knots the B-spline basis is exactly Bernstein.
723        let k = KnotVector::clamped_uniform(3, 4).unwrap();
724        for i in 0..=20 {
725            let u = f64::from(i) / 20.0;
726            let span = k.span(u, T).unwrap();
727            let n = k.basis(span, u);
728            let v = 1.0 - u;
729            let expected = [v * v * v, 3.0 * u * v * v, 3.0 * u * u * v, u * u * u];
730            for (got, want) in n.iter().zip(expected) {
731                assert_relative_eq!(*got, want, epsilon = 1e-14);
732            }
733        }
734    }
735
736    #[test]
737    fn basis_is_an_interpolant_at_a_clamped_end() {
738        let k = cubic();
739        let n = k.basis(k.span(0.0, T).unwrap(), 0.0);
740        assert_relative_eq!(n[0], 1.0, epsilon = 1e-15);
741        assert!(n[1..].iter().all(|v| v.abs() < 1e-15));
742
743        let n = k.basis(k.span(1.0, T).unwrap(), 1.0);
744        assert_relative_eq!(n[3], 1.0, epsilon = 1e-15);
745        assert!(n[..3].iter().all(|v| v.abs() < 1e-15));
746    }
747
748    #[test]
749    fn basis_derivatives_agree_with_finite_differences() {
750        let k = cubic();
751        let h = 1e-6;
752        for i in 1..20 {
753            let u = f64::from(i) / 20.0;
754            let span = k.span(u, T).unwrap();
755            let d = k.basis_derivatives(span, u, 2);
756
757            // Zeroth order must reproduce the plain basis.
758            let plain = k.basis(span, u);
759            for (a, b) in d[0].iter().zip(plain.iter()) {
760                assert_relative_eq!(a, b, epsilon = 1e-14);
761            }
762
763            // First order against a central difference, evaluated in the same
764            // span so the basis indices line up.
765            let ahead = k.basis(span, u + h);
766            let behind = k.basis(span, u - h);
767            for j in 0..=k.degree() {
768                let numeric = (ahead[j] - behind[j]) / (2.0 * h);
769                assert_relative_eq!(d[1][j], numeric, epsilon = 1e-5);
770            }
771        }
772    }
773
774    #[test]
775    fn basis_derivatives_sum_to_zero() {
776        // The basis sums to one everywhere, so every derivative of that sum is
777        // identically zero: a strong check on the whole recurrence.
778        let k = cubic();
779        for i in 0..=50 {
780            let u = f64::from(i) / 50.0;
781            let span = k.span(u, T).unwrap();
782            let d = k.basis_derivatives(span, u, 3);
783            assert_relative_eq!(d[0].iter().sum::<f64>(), 1.0, epsilon = 1e-13);
784            for (order, row) in d.iter().enumerate().skip(1) {
785                let sum: f64 = row.iter().sum();
786                assert!(sum.abs() < 1e-8, "order {order} sums to {sum}");
787            }
788        }
789    }
790
791    #[test]
792    fn derivatives_above_the_degree_are_zero() {
793        let k = cubic();
794        let span = k.span(0.4, T).unwrap();
795        let d = k.basis_derivatives(span, 0.4, 5);
796        assert_eq!(d.len(), 6);
797        for (order, row) in d.iter().enumerate().skip(k.degree() + 1) {
798            assert!(row.iter().all(|v| *v == 0.0), "order {order} is not zero");
799        }
800    }
801
802    #[test]
803    fn multiplicity_and_distinct_knots() {
804        let k = cubic();
805        assert_eq!(k.multiplicity_of(0.0), 4);
806        assert_eq!(k.multiplicity_of(0.5), 1);
807        assert_eq!(k.multiplicity_of(0.6), 0);
808        assert_eq!(k.multiplicity_at(0), 4);
809        assert_eq!(
810            k.distinct(),
811            vec![(0.0, 4), (0.25, 1), (0.5, 1), (0.75, 1), (1.0, 4)]
812        );
813    }
814
815    #[test]
816    fn knot_insertion_raises_multiplicity() {
817        let k = cubic();
818        let inserted = k.with_knot_inserted(0.5, 2).unwrap();
819        assert_eq!(inserted.multiplicity_of(0.5), 3);
820        assert_eq!(inserted.knots().len(), k.knots().len() + 2);
821        assert_eq!(inserted.domain(), k.domain());
822        // Beyond the degree it would disconnect the curve.
823        assert!(k.with_knot_inserted(0.5, 3).is_err());
824    }
825
826    #[test]
827    fn an_unclamped_vector_reverses_and_rescales_without_changing_its_basis() {
828        // A uniform cubic over [3, 4] with its outer knots beyond the domain.
829        let kv = KnotVector::new((0..8).map(f64::from).collect(), 3).unwrap();
830        let at = |kv: &KnotVector, u: f64| kv.basis(kv.span(u, T).unwrap(), u);
831        let rescaled = kv.reparameterized(0.0, 1.0).unwrap();
832        let reversed = kv.reversed();
833        for (u, s) in [(3.1, 0.1), (3.5, 0.5), (3.9, 0.9)] {
834            let original = at(&kv, u);
835            for (x, y) in original.iter().zip(at(&rescaled, s)) {
836                assert_relative_eq!(*x, y, epsilon = 1e-12);
837            }
838            for (x, y) in original.iter().zip(at(&reversed, 7.0 - u).iter().rev()) {
839                assert_relative_eq!(*x, *y, epsilon = 1e-12);
840            }
841        }
842    }
843
844    #[test]
845    fn reparameterization_preserves_clamping_exactly() {
846        let k = cubic();
847        let r = k.reparameterized(-2.0, 6.0).unwrap();
848        assert_eq!(r.domain(), (-2.0, 6.0));
849        assert!(r.is_clamped(), "the repeated end knots must stay identical");
850        assert_eq!(r.multiplicity_of(-2.0), 4);
851        assert_eq!(r.multiplicity_of(6.0), 4);
852        // Interior knots map proportionally.
853        assert_relative_eq!(r.knots()[4], 0.0, epsilon = 1e-12);
854        assert!(k.reparameterized(1.0, 1.0).is_err());
855        assert!(k.reparameterized(1.0, f64::NAN).is_err());
856    }
857
858    #[test]
859    fn reversal_mirrors_the_spacing_and_keeps_the_domain() {
860        // Deliberately uneven interior spacing, so a mirror is observable.
861        let k = KnotVector::new(vec![0.0, 0.0, 0.0, 0.1, 0.8, 1.0, 1.0, 1.0], 2).unwrap();
862        let r = k.reversed();
863        assert_eq!(r.domain(), k.domain());
864        assert!(r.is_clamped());
865        assert_relative_eq!(r.knots()[3], 0.2, epsilon = 1e-15);
866        assert_relative_eq!(r.knots()[4], 0.9, epsilon = 1e-15);
867        // Reversing twice restores the vector to within rounding. Not exactly:
868        // `a + b - k` is not an exact involution in floating point.
869        for (got, want) in r.reversed().knots().iter().zip(k.knots()) {
870            assert_relative_eq!(got, want, epsilon = 1e-15);
871        }
872        // What must hold exactly is multiplicity, since continuity depends on
873        // it: two knots that were equal stay equal through any number of
874        // reversals.
875        let multiplicities: Vec<usize> = r.distinct().iter().map(|(_, m)| *m).collect();
876        let original: Vec<usize> = k.distinct().iter().map(|(_, m)| *m).collect();
877        assert_eq!(multiplicities, original);
878    }
879
880    #[test]
881    fn averaged_knots_follow_the_parameters() {
882        let params = [0.0, 0.1, 0.4, 0.9, 1.0];
883        let k = KnotVector::averaged(3, &params).unwrap();
884        assert_eq!(k.control_point_count(), 5);
885        assert_eq!(k.domain(), (0.0, 1.0));
886        assert!(k.is_clamped());
887        // One interior knot, the mean of parameters 1..4.
888        assert_relative_eq!(k.knots()[4], (0.1 + 0.4 + 0.9) / 3.0, epsilon = 1e-15);
889        assert!(KnotVector::averaged(3, &[0.0, 1.0]).is_err());
890        assert!(
891            KnotVector::averaged(2, &[0.0, 0.5, 0.5, 1.0]).is_err(),
892            "not increasing"
893        );
894    }
895
896    #[test]
897    fn basis_at_a_repeated_interior_knot_is_still_a_partition_of_unity() {
898        // Multiplicity equal to the degree: a corner, where the recurrence has
899        // the most opportunity to divide by something vanishing.
900        let k = KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0], 2).unwrap();
901        for u in [0.0_f64, 0.25, 0.499_999, 0.5, 0.500_001, 0.75, 1.0] {
902            let span = k.span(u, T).unwrap();
903            let n = k.basis(span, u);
904            assert_relative_eq!(n.iter().sum::<f64>(), 1.0, epsilon = 1e-14);
905            assert!(n.iter().all(|v| v.is_finite()), "non-finite basis at {u}");
906        }
907    }
908
909    /// The fixed-array path answers bit for bit what the general one does,
910    /// at every degree it serves and every derivative order asked.
911    #[test]
912    fn the_small_basis_path_is_the_general_one_to_the_bit() {
913        for degree in 1..SMALL_ORDER {
914            let count = degree + 5;
915            let knots = KnotVector::clamped_uniform(degree, count).unwrap();
916            let (lo, hi) = knots.domain();
917            for step in 0..=40 {
918                let u = lo + (hi - lo) * f64::from(step) / 40.0;
919                let span = knots.span_unchecked(u);
920                for n in 0..=degree + 1 {
921                    let fast = knots.basis_derivatives_small(span, u, n);
922                    let slow = knots.basis_derivatives_any(span, u, n);
923                    assert_eq!(fast.len(), slow.len());
924                    for (a, b) in fast.iter().zip(&slow) {
925                        let bits =
926                            |r: &BasisValues| r.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
927                        assert_eq!(bits(a), bits(b), "degree {degree} u {u} n {n}");
928                    }
929                }
930            }
931        }
932    }
933}