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`. A
292        // span of no length (the end knot repeated, unclamped) is passed
293        // over for the one before it, and at the start for the one after.
294        if u >= self.knots[last + 1] {
295            let mut span = last;
296            while span > self.degree && self.knots[span] >= self.knots[span + 1] {
297                span -= 1;
298            }
299            return span;
300        }
301        if u <= self.knots[self.degree] {
302            let mut span = self.degree;
303            while span < last && self.knots[span] >= self.knots[span + 1] {
304                span += 1;
305            }
306            return span;
307        }
308        let mut low = self.degree;
309        let mut high = last + 1;
310        while high - low > 1 {
311            let mid = usize::midpoint(low, high);
312            if u < self.knots[mid] {
313                high = mid;
314            } else {
315                low = mid;
316            }
317        }
318        low
319    }
320
321    /// The `degree + 1` non-zero basis functions at `u`.
322    ///
323    /// Entry `i` is the value of basis function `span - degree + i`. They are
324    /// non-negative and sum to exactly one up to rounding: the partition of
325    /// unity, which is what makes a B-spline curve lie in the convex hull of its
326    /// control points.
327    ///
328    /// Cox-de Boor, in the triangular form that avoids evaluating the zero
329    /// functions and never divides by a zero knot difference.
330    #[must_use]
331    pub fn basis(&self, span: usize, u: f64) -> BasisValues {
332        let p = self.degree;
333        let mut n = BasisValues::with_capacity(p + 1);
334        n.push(1.0);
335        let mut left = BasisValues::with_capacity(p + 1);
336        let mut right = BasisValues::with_capacity(p + 1);
337        left.push(0.0);
338        right.push(0.0);
339
340        for j in 1..=p {
341            left.push(u - self.knots[span + 1 - j]);
342            right.push(self.knots[span + j] - u);
343            let mut saved = 0.0;
344            n.push(0.0);
345            for r in 0..j {
346                // `right[r + 1] + left[j - r]` is the width of the union of two
347                // adjacent supports, which is positive whenever the basis
348                // function is, so this cannot divide by zero for a valid vector.
349                let denominator = right[r + 1] + left[j - r];
350                let temp = n[r] / denominator;
351                n[r] = saved + right[r + 1] * temp;
352                saved = left[j - r] * temp;
353            }
354            n[j] = saved;
355        }
356        n
357    }
358
359    /// The non-zero basis functions and their derivatives up to order `n`.
360    ///
361    /// `result[k][i]` is the `k`th derivative of basis function
362    /// `span - degree + i`. Orders above the degree are identically zero and
363    /// are returned as such rather than as noise.
364    #[must_use]
365    pub fn basis_derivatives(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
366        if self.degree < SMALL_ORDER {
367            return self.basis_derivatives_small(span, u, n);
368        }
369        self.basis_derivatives_any(span, u, n)
370    }
371
372    /// [`Self::basis_derivatives`] for degree below [`SMALL_ORDER`], every
373    /// scratch table a fixed array: the same recurrence, the same
374    /// arithmetic in the same order, so the same bits, without building
375    /// nested vectors on each of the millions of calls a fit or a march
376    /// makes.
377    fn basis_derivatives_small(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
378        const M: usize = SMALL_ORDER;
379        let p = self.degree;
380        let order = n.min(p);
381        let mut ndu = [[0.0_f64; M]; M];
382        ndu[0][0] = 1.0;
383        let mut left = [0.0_f64; M];
384        let mut right = [0.0_f64; M];
385        for j in 1..=p {
386            left[j] = u - self.knots[span + 1 - j];
387            right[j] = self.knots[span + j] - u;
388            let mut saved = 0.0;
389            for r in 0..j {
390                ndu[j][r] = right[r + 1] + left[j - r];
391                let temp = ndu[r][j - 1] / ndu[j][r];
392                ndu[r][j] = saved + right[r + 1] * temp;
393                saved = left[j - r] * temp;
394            }
395            ndu[j][j] = saved;
396        }
397
398        let mut rows = [[0.0_f64; M]; M];
399        for (j, slot) in rows[0].iter_mut().enumerate().take(p + 1) {
400            *slot = ndu[j][p];
401        }
402        let mut a = [[0.0_f64; M]; 2];
403        for r in 0..=p {
404            let (mut s1, mut s2) = (0_usize, 1_usize);
405            a[0][0] = 1.0;
406            for k in 1..=order {
407                let mut d = 0.0;
408                let rk = r as isize - k as isize;
409                let pk = p - k;
410                if r >= k {
411                    a[s2][0] = a[s1][0] / ndu[pk + 1][rk as usize];
412                    d = a[s2][0] * ndu[rk as usize][pk];
413                }
414                let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
415                let j2 = if r as isize - 1 <= pk as isize {
416                    k - 1
417                } else {
418                    p - r
419                };
420                for j in j1..=j2 {
421                    let index = (rk + j as isize) as usize;
422                    a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
423                    d += a[s2][j] * ndu[index][pk];
424                }
425                if r <= pk {
426                    a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
427                    d += a[s2][k] * ndu[r][pk];
428                }
429                rows[k][r] = d;
430                core::mem::swap(&mut s1, &mut s2);
431            }
432        }
433        let mut factor = p;
434        for (k, row) in rows.iter_mut().enumerate().take(order + 1).skip(1) {
435            #[allow(clippy::cast_precision_loss)]
436            let scale = factor as f64;
437            for value in row.iter_mut().take(p + 1) {
438                *value *= scale;
439            }
440            factor = factor.saturating_mul(p.saturating_sub(k));
441        }
442        (0..=n)
443            .map(|k| {
444                if k < M {
445                    BasisValues::from_slice(&rows[k][..=p])
446                } else {
447                    BasisValues::from_elem(0.0, p + 1)
448                }
449            })
450            .collect()
451    }
452
453    fn basis_derivatives_any(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
454        let p = self.degree;
455        let order = n.min(p);
456
457        // `ndu` holds the basis values and the knot differences from the
458        // triangular recurrence. Both halves are needed to build derivatives.
459        // Every scratch row lives inline for the degrees the kernel actually
460        // meets: this is the innermost loop of every spline evaluation, and
461        // a heap row per call there is the kernel's largest allocation source.
462        let mut ndu: SmallVec<[BasisValues; 8]> =
463            core::iter::repeat_with(|| BasisValues::from_elem(0.0, p + 1))
464                .take(p + 1)
465                .collect();
466        ndu[0][0] = 1.0;
467        let mut left = BasisValues::from_elem(0.0, p + 1);
468        let mut right = BasisValues::from_elem(0.0, p + 1);
469
470        for j in 1..=p {
471            left[j] = u - self.knots[span + 1 - j];
472            right[j] = self.knots[span + j] - u;
473            let mut saved = 0.0;
474            for r in 0..j {
475                ndu[j][r] = right[r + 1] + left[j - r];
476                let temp = ndu[r][j - 1] / ndu[j][r];
477                ndu[r][j] = saved + right[r + 1] * temp;
478                saved = left[j - r] * temp;
479            }
480            ndu[j][j] = saved;
481        }
482
483        let mut derivatives: DerivativeRows =
484            core::iter::repeat_with(|| BasisValues::from_elem(0.0, p + 1))
485                .take(n + 1)
486                .collect();
487        for (j, slot) in derivatives[0].iter_mut().enumerate() {
488            *slot = ndu[j][p];
489        }
490        // The rows above `order` stay zero: a derivative past the degree of a
491        // piecewise polynomial is identically zero, not merely small.
492
493        // Two alternating rows of coefficients, per the standard algorithm.
494        let mut a = [
495            BasisValues::from_elem(0.0, p + 1),
496            BasisValues::from_elem(0.0, p + 1),
497        ];
498        for r in 0..=p {
499            let (mut s1, mut s2) = (0_usize, 1_usize);
500            a[0][0] = 1.0;
501            for k in 1..=order {
502                let mut d = 0.0;
503                let rk = r as isize - k as isize;
504                let pk = p - k;
505                if r >= k {
506                    a[s2][0] = a[s1][0] / ndu[pk + 1][rk as usize];
507                    d = a[s2][0] * ndu[rk as usize][pk];
508                }
509                let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
510                let j2 = if r as isize - 1 <= pk as isize {
511                    k - 1
512                } else {
513                    p - r
514                };
515                for j in j1..=j2 {
516                    let index = (rk + j as isize) as usize;
517                    a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
518                    d += a[s2][j] * ndu[index][pk];
519                }
520                if r <= pk {
521                    a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
522                    d += a[s2][k] * ndu[r][pk];
523                }
524                derivatives[k][r] = d;
525                core::mem::swap(&mut s1, &mut s2);
526            }
527        }
528
529        // Multiply through by the falling factorial the recurrence omits.
530        let mut factor = p;
531        for (k, row) in derivatives.iter_mut().enumerate().take(order + 1).skip(1) {
532            #[allow(clippy::cast_precision_loss)]
533            let scale = factor as f64;
534            for value in row.iter_mut() {
535                *value *= scale;
536            }
537            factor = factor.saturating_mul(p.saturating_sub(k));
538        }
539        derivatives
540    }
541
542    /// Insert `value` into the sequence, `count` times.
543    ///
544    /// Only the knots change; adjusting control points to keep the shape is
545    /// [`crate::bspline::insert_knot`].
546    ///
547    /// # Errors
548    ///
549    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the result
550    /// would push a knot's multiplicity above the degree.
551    pub fn with_knot_inserted(&self, value: f64, count: usize) -> OgeomResult<Self> {
552        let mut knots = self.knots.clone();
553        let position = knots.partition_point(|k| *k <= value);
554        for _ in 0..count {
555            knots.insert(position, value);
556        }
557        Self::new(knots, self.degree)
558    }
559
560    /// This vector with its domain mapped onto `[start, end]`.
561    ///
562    /// # Errors
563    ///
564    /// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the target
565    /// range is empty or non-finite.
566    pub fn reparameterized(&self, start: f64, end: f64) -> OgeomResult<Self> {
567        if !start.is_finite() || !end.is_finite() || end <= start {
568            ogeom_bail!(Construction, "target range [{start}, {end}] is empty");
569        }
570        let (a, b) = self.domain();
571        let scale = (end - start) / (b - a);
572        // Map, then overwrite the knots at the domain's ends with the exact
573        // endpoints: the arithmetic would otherwise give each copy a slightly
574        // different value and silently destroy the clamping. Knots outside
575        // the domain (an unclamped or periodic vector's) map like any other,
576        // each held on its own side of the ends it lies beyond.
577        let knots: Vec<f64> = self
578            .knots
579            .iter()
580            .map(|&k| {
581                let mapped = (k - a).mul_add(scale, start);
582                if k == a {
583                    start
584                } else if k == b {
585                    end
586                } else if k < a {
587                    mapped.min(start)
588                } else if k > b {
589                    mapped.max(end)
590                } else {
591                    mapped.clamp(start, end)
592                }
593            })
594            .collect();
595        Self::new(knots, self.degree)
596    }
597
598    /// This vector with the parameter direction reversed.
599    ///
600    /// The domain is preserved and the sequence of interior spacings is
601    /// mirrored. Reversing a curve reverses its knots and its control points
602    /// together.
603    ///
604    /// Multiplicity is preserved *exactly* (equal knots map through the same
605    /// arithmetic and so stay equal), which is what continuity depends on. The
606    /// interior knot *values* are not bit-exactly restored by reversing twice,
607    /// since `a + b - k` is not an exact involution in floating point. They
608    /// return to within one ulp.
609    #[must_use]
610    pub fn reversed(&self) -> Self {
611        let (a, b) = self.domain();
612        let sum = a + b;
613        // Same reasoning as `reparameterized`: the domain's ends exchange
614        // exactly, and every other knot mirrors, inside the domain or out.
615        let knots: Vec<f64> = self
616            .knots
617            .iter()
618            .rev()
619            .map(|&k| {
620                if k == a {
621                    b
622                } else if k == b {
623                    a
624                } else if k < a {
625                    (sum - k).max(b)
626                } else if k > b {
627                    (sum - k).min(a)
628                } else {
629                    (sum - k).clamp(a, b)
630                }
631            })
632            .collect();
633        Self {
634            knots,
635            degree: self.degree,
636        }
637    }
638}
639
640#[cfg(test)]
641#[allow(clippy::unwrap_used)]
642mod tests {
643    use super::*;
644    use approx::assert_relative_eq;
645
646    const T: Tolerances = Tolerances::millimetres();
647
648    fn cubic() -> KnotVector {
649        // Degree 3, 7 control points, two interior knots.
650        KnotVector::new(
651            vec![0.0, 0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0, 1.0],
652            3,
653        )
654        .unwrap()
655    }
656
657    #[test]
658    fn malformed_vectors_are_refused() {
659        assert!(KnotVector::new(vec![0.0, 1.0], 3).is_err(), "too short");
660        assert!(
661            KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], 3).is_err(),
662            "a start repeated degree + 2 times"
663        );
664        assert!(
665            KnotVector::new(vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0], 3).is_err(),
666            "an end repeated degree + 2 times"
667        );
668        assert!(
669            KnotVector::new(vec![0.0, 0.0, 1.0, 0.5, 1.0, 1.0], 2).is_err(),
670            "not non-decreasing"
671        );
672        assert!(
673            KnotVector::new(vec![0.0, 0.0, f64::NAN, 1.0, 1.0, 1.0], 2).is_err(),
674            "non-finite"
675        );
676        assert!(KnotVector::new(vec![0.0; 8], 3).is_err(), "empty domain");
677        assert!(KnotVector::new(vec![], 0).is_err(), "degree zero");
678        // Interior multiplicity above the degree disconnects the curve.
679        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());
680        // At the degree it is merely a corner, which is legitimate.
681        assert!(KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0], 2).is_ok());
682    }
683
684    #[test]
685    fn clamped_uniform_has_the_expected_shape() {
686        let k = KnotVector::clamped_uniform(3, 7).unwrap();
687        assert_eq!(k.knots().len(), 11);
688        assert_eq!(k.control_point_count(), 7);
689        assert_eq!(k.domain(), (0.0, 1.0));
690        assert!(k.is_clamped());
691        assert_eq!(k.multiplicity_of(0.0), 4);
692        assert_eq!(k.multiplicity_of(1.0), 4);
693        assert_relative_eq!(k.knots()[4], 1.0 / 4.0);
694
695        // A Bezier: no interior knots at all.
696        let b = KnotVector::clamped_uniform(3, 4).unwrap();
697        assert_eq!(b.knots(), &[0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]);
698        assert!(KnotVector::clamped_uniform(3, 3).is_err());
699    }
700
701    #[test]
702    fn span_lookup_handles_both_ends_of_the_domain() {
703        let k = cubic();
704        assert_eq!(k.span(0.0, T).unwrap(), 3, "domain start");
705        assert_eq!(k.span(0.1, T).unwrap(), 3);
706        assert_eq!(k.span(0.25, T).unwrap(), 4, "on a knot, span to its right");
707        assert_eq!(k.span(0.3, T).unwrap(), 4);
708        assert_eq!(k.span(0.9, T).unwrap(), 6);
709        // The domain end must resolve to the last span, not one past it.
710        assert_eq!(k.span(1.0, T).unwrap(), 6);
711        assert!(k.span(-0.1, T).is_err());
712        assert!(k.span(1.1, T).is_err());
713        assert!(k.span(f64::NAN, T).is_err());
714    }
715
716    #[test]
717    fn basis_functions_form_a_partition_of_unity() {
718        let k = cubic();
719        for i in 0..=200 {
720            let u = f64::from(i) / 200.0;
721            let span = k.span(u, T).unwrap();
722            let n = k.basis(span, u);
723            assert_eq!(n.len(), 4);
724            let sum: f64 = n.iter().sum();
725            assert_relative_eq!(sum, 1.0, epsilon = 1e-14);
726            assert!(n.iter().all(|v| *v >= -1e-15), "basis must be non-negative");
727        }
728    }
729
730    #[test]
731    fn basis_matches_the_bernstein_polynomials_for_a_bezier() {
732        // With no interior knots the B-spline basis is exactly Bernstein.
733        let k = KnotVector::clamped_uniform(3, 4).unwrap();
734        for i in 0..=20 {
735            let u = f64::from(i) / 20.0;
736            let span = k.span(u, T).unwrap();
737            let n = k.basis(span, u);
738            let v = 1.0 - u;
739            let expected = [v * v * v, 3.0 * u * v * v, 3.0 * u * u * v, u * u * u];
740            for (got, want) in n.iter().zip(expected) {
741                assert_relative_eq!(*got, want, epsilon = 1e-14);
742            }
743        }
744    }
745
746    #[test]
747    fn basis_is_an_interpolant_at_a_clamped_end() {
748        let k = cubic();
749        let n = k.basis(k.span(0.0, T).unwrap(), 0.0);
750        assert_relative_eq!(n[0], 1.0, epsilon = 1e-15);
751        assert!(n[1..].iter().all(|v| v.abs() < 1e-15));
752
753        let n = k.basis(k.span(1.0, T).unwrap(), 1.0);
754        assert_relative_eq!(n[3], 1.0, epsilon = 1e-15);
755        assert!(n[..3].iter().all(|v| v.abs() < 1e-15));
756    }
757
758    #[test]
759    fn basis_derivatives_agree_with_finite_differences() {
760        let k = cubic();
761        let h = 1e-6;
762        for i in 1..20 {
763            let u = f64::from(i) / 20.0;
764            let span = k.span(u, T).unwrap();
765            let d = k.basis_derivatives(span, u, 2);
766
767            // Zeroth order must reproduce the plain basis.
768            let plain = k.basis(span, u);
769            for (a, b) in d[0].iter().zip(plain.iter()) {
770                assert_relative_eq!(a, b, epsilon = 1e-14);
771            }
772
773            // First order against a central difference, evaluated in the same
774            // span so the basis indices line up.
775            let ahead = k.basis(span, u + h);
776            let behind = k.basis(span, u - h);
777            for j in 0..=k.degree() {
778                let numeric = (ahead[j] - behind[j]) / (2.0 * h);
779                assert_relative_eq!(d[1][j], numeric, epsilon = 1e-5);
780            }
781        }
782    }
783
784    #[test]
785    fn basis_derivatives_sum_to_zero() {
786        // The basis sums to one everywhere, so every derivative of that sum is
787        // identically zero: a strong check on the whole recurrence.
788        let k = cubic();
789        for i in 0..=50 {
790            let u = f64::from(i) / 50.0;
791            let span = k.span(u, T).unwrap();
792            let d = k.basis_derivatives(span, u, 3);
793            assert_relative_eq!(d[0].iter().sum::<f64>(), 1.0, epsilon = 1e-13);
794            for (order, row) in d.iter().enumerate().skip(1) {
795                let sum: f64 = row.iter().sum();
796                assert!(sum.abs() < 1e-8, "order {order} sums to {sum}");
797            }
798        }
799    }
800
801    #[test]
802    fn derivatives_above_the_degree_are_zero() {
803        let k = cubic();
804        let span = k.span(0.4, T).unwrap();
805        let d = k.basis_derivatives(span, 0.4, 5);
806        assert_eq!(d.len(), 6);
807        for (order, row) in d.iter().enumerate().skip(k.degree() + 1) {
808            assert!(row.iter().all(|v| *v == 0.0), "order {order} is not zero");
809        }
810    }
811
812    #[test]
813    fn multiplicity_and_distinct_knots() {
814        let k = cubic();
815        assert_eq!(k.multiplicity_of(0.0), 4);
816        assert_eq!(k.multiplicity_of(0.5), 1);
817        assert_eq!(k.multiplicity_of(0.6), 0);
818        assert_eq!(k.multiplicity_at(0), 4);
819        assert_eq!(
820            k.distinct(),
821            vec![(0.0, 4), (0.25, 1), (0.5, 1), (0.75, 1), (1.0, 4)]
822        );
823    }
824
825    #[test]
826    fn knot_insertion_raises_multiplicity() {
827        let k = cubic();
828        let inserted = k.with_knot_inserted(0.5, 2).unwrap();
829        assert_eq!(inserted.multiplicity_of(0.5), 3);
830        assert_eq!(inserted.knots().len(), k.knots().len() + 2);
831        assert_eq!(inserted.domain(), k.domain());
832        // Beyond the degree it would disconnect the curve.
833        assert!(k.with_knot_inserted(0.5, 3).is_err());
834    }
835
836    #[test]
837    fn an_unclamped_vector_reverses_and_rescales_without_changing_its_basis() {
838        // A uniform cubic over [3, 4] with its outer knots beyond the domain.
839        let kv = KnotVector::new((0..8).map(f64::from).collect(), 3).unwrap();
840        let at = |kv: &KnotVector, u: f64| kv.basis(kv.span(u, T).unwrap(), u);
841        let rescaled = kv.reparameterized(0.0, 1.0).unwrap();
842        let reversed = kv.reversed();
843        for (u, s) in [(3.1, 0.1), (3.5, 0.5), (3.9, 0.9)] {
844            let original = at(&kv, u);
845            for (x, y) in original.iter().zip(at(&rescaled, s)) {
846                assert_relative_eq!(*x, y, epsilon = 1e-12);
847            }
848            for (x, y) in original.iter().zip(at(&reversed, 7.0 - u).iter().rev()) {
849                assert_relative_eq!(*x, *y, epsilon = 1e-12);
850            }
851        }
852    }
853
854    #[test]
855    fn reparameterization_preserves_clamping_exactly() {
856        let k = cubic();
857        let r = k.reparameterized(-2.0, 6.0).unwrap();
858        assert_eq!(r.domain(), (-2.0, 6.0));
859        assert!(r.is_clamped(), "the repeated end knots must stay identical");
860        assert_eq!(r.multiplicity_of(-2.0), 4);
861        assert_eq!(r.multiplicity_of(6.0), 4);
862        // Interior knots map proportionally.
863        assert_relative_eq!(r.knots()[4], 0.0, epsilon = 1e-12);
864        assert!(k.reparameterized(1.0, 1.0).is_err());
865        assert!(k.reparameterized(1.0, f64::NAN).is_err());
866    }
867
868    #[test]
869    fn reversal_mirrors_the_spacing_and_keeps_the_domain() {
870        // Deliberately uneven interior spacing, so a mirror is observable.
871        let k = KnotVector::new(vec![0.0, 0.0, 0.0, 0.1, 0.8, 1.0, 1.0, 1.0], 2).unwrap();
872        let r = k.reversed();
873        assert_eq!(r.domain(), k.domain());
874        assert!(r.is_clamped());
875        assert_relative_eq!(r.knots()[3], 0.2, epsilon = 1e-15);
876        assert_relative_eq!(r.knots()[4], 0.9, epsilon = 1e-15);
877        // Reversing twice restores the vector to within rounding. Not exactly:
878        // `a + b - k` is not an exact involution in floating point.
879        for (got, want) in r.reversed().knots().iter().zip(k.knots()) {
880            assert_relative_eq!(got, want, epsilon = 1e-15);
881        }
882        // What must hold exactly is multiplicity, since continuity depends on
883        // it: two knots that were equal stay equal through any number of
884        // reversals.
885        let multiplicities: Vec<usize> = r.distinct().iter().map(|(_, m)| *m).collect();
886        let original: Vec<usize> = k.distinct().iter().map(|(_, m)| *m).collect();
887        assert_eq!(multiplicities, original);
888    }
889
890    #[test]
891    fn averaged_knots_follow_the_parameters() {
892        let params = [0.0, 0.1, 0.4, 0.9, 1.0];
893        let k = KnotVector::averaged(3, &params).unwrap();
894        assert_eq!(k.control_point_count(), 5);
895        assert_eq!(k.domain(), (0.0, 1.0));
896        assert!(k.is_clamped());
897        // One interior knot, the mean of parameters 1..4.
898        assert_relative_eq!(k.knots()[4], (0.1 + 0.4 + 0.9) / 3.0, epsilon = 1e-15);
899        assert!(KnotVector::averaged(3, &[0.0, 1.0]).is_err());
900        assert!(
901            KnotVector::averaged(2, &[0.0, 0.5, 0.5, 1.0]).is_err(),
902            "not increasing"
903        );
904    }
905
906    #[test]
907    fn basis_at_a_repeated_interior_knot_is_still_a_partition_of_unity() {
908        // Multiplicity equal to the degree: a corner, where the recurrence has
909        // the most opportunity to divide by something vanishing.
910        let k = KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0], 2).unwrap();
911        for u in [0.0_f64, 0.25, 0.499_999, 0.5, 0.500_001, 0.75, 1.0] {
912            let span = k.span(u, T).unwrap();
913            let n = k.basis(span, u);
914            assert_relative_eq!(n.iter().sum::<f64>(), 1.0, epsilon = 1e-14);
915            assert!(n.iter().all(|v| v.is_finite()), "non-finite basis at {u}");
916        }
917    }
918
919    /// The fixed-array path answers bit for bit what the general one does,
920    /// at every degree it serves and every derivative order asked.
921    #[test]
922    fn the_small_basis_path_is_the_general_one_to_the_bit() {
923        for degree in 1..SMALL_ORDER {
924            let count = degree + 5;
925            let knots = KnotVector::clamped_uniform(degree, count).unwrap();
926            let (lo, hi) = knots.domain();
927            for step in 0..=40 {
928                let u = lo + (hi - lo) * f64::from(step) / 40.0;
929                let span = knots.span_unchecked(u);
930                for n in 0..=degree + 1 {
931                    let fast = knots.basis_derivatives_small(span, u, n);
932                    let slow = knots.basis_derivatives_any(span, u, n);
933                    assert_eq!(fast.len(), slow.len());
934                    for (a, b) in fast.iter().zip(&slow) {
935                        let bits =
936                            |r: &BasisValues| r.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
937                        assert_eq!(bits(a), bits(b), "degree {degree} u {u} n {n}");
938                    }
939                }
940            }
941        }
942    }
943}