1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
29use smallvec::SmallVec;
30
31pub type BasisValues = SmallVec<[f64; 8]>;
33
34pub type DerivativeRows = SmallVec<[BasisValues; 4]>;
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct KnotVector {
41 knots: Vec<f64>,
42 degree: usize,
43}
44
45impl KnotVector {
46 pub fn new(knots: Vec<f64>, degree: usize) -> OgeomResult<Self> {
56 if degree == 0 {
57 ogeom_bail!(Construction, "degree must be at least 1");
58 }
59 let minimum = 2 * (degree + 1);
62 if knots.len() < minimum {
63 ogeom_bail!(
64 Construction,
65 "degree {degree} needs at least {minimum} knots, got {}",
66 knots.len()
67 );
68 }
69 if !knots.iter().all(|k| k.is_finite()) {
70 ogeom_bail!(Construction, "knot vector contains a non-finite value");
71 }
72 if knots.windows(2).any(|w| w[1] < w[0]) {
73 ogeom_bail!(Construction, "knot vector is not non-decreasing");
74 }
75
76 let this = Self { knots, degree };
77 if this.domain_start() >= this.domain_end() {
78 ogeom_bail!(Construction, "knot vector spans an empty domain");
79 }
80
81 let (first, last) = (this.degree, this.knots.len() - this.degree - 1);
83 let mut index = first;
84 while index < last {
85 let value = this.knots[index];
86 let mut count = 0;
87 while index < last && this.knots[index] == value {
88 count += 1;
89 index += 1;
90 }
91 if value > this.domain_start() && value < this.domain_end() && count > this.degree {
94 ogeom_bail!(
95 Construction,
96 "interior knot {value} has multiplicity {count}, above degree {}",
97 this.degree
98 );
99 }
100 }
101 Ok(this)
102 }
103
104 pub fn clamped_uniform(degree: usize, control_points: usize) -> OgeomResult<Self> {
114 if control_points < degree + 1 {
115 ogeom_bail!(
116 Construction,
117 "degree {degree} needs at least {} control points, got {control_points}",
118 degree + 1
119 );
120 }
121 let interior = control_points - degree - 1;
122 let mut knots = Vec::with_capacity(control_points + degree + 1);
123 knots.extend(core::iter::repeat_n(0.0, degree + 1));
124 for i in 1..=interior {
125 #[allow(clippy::cast_precision_loss)]
126 knots.push(i as f64 / (interior + 1) as f64);
127 }
128 knots.extend(core::iter::repeat_n(1.0, degree + 1));
129 Self::new(knots, degree)
130 }
131
132 pub fn averaged(degree: usize, parameters: &[f64]) -> OgeomResult<Self> {
143 if parameters.len() < degree + 1 {
144 ogeom_bail!(
145 Construction,
146 "degree {degree} needs at least {} parameters",
147 degree + 1
148 );
149 }
150 if parameters.windows(2).any(|w| w[1] <= w[0]) {
151 ogeom_bail!(Construction, "parameters must be strictly increasing");
152 }
153 let n = parameters.len();
154 let mut knots = Vec::with_capacity(n + degree + 1);
155 knots.extend(core::iter::repeat_n(parameters[0], degree + 1));
156 #[allow(clippy::cast_precision_loss)]
157 for j in 1..n - degree {
158 let mean: f64 = parameters[j..j + degree].iter().sum::<f64>() / degree as f64;
159 knots.push(mean);
160 }
161 knots.extend(core::iter::repeat_n(parameters[n - 1], degree + 1));
162 Self::new(knots, degree)
163 }
164
165 #[must_use]
167 pub const fn degree(&self) -> usize {
168 self.degree
169 }
170
171 #[must_use]
173 pub fn knots(&self) -> &[f64] {
174 &self.knots
175 }
176
177 #[must_use]
179 pub const fn control_point_count(&self) -> usize {
180 self.knots.len() - self.degree - 1
181 }
182
183 #[must_use]
185 pub fn domain_start(&self) -> f64 {
186 self.knots[self.degree]
187 }
188
189 #[must_use]
191 pub fn domain_end(&self) -> f64 {
192 self.knots[self.knots.len() - self.degree - 1]
193 }
194
195 #[must_use]
197 pub fn domain(&self) -> (f64, f64) {
198 (self.domain_start(), self.domain_end())
199 }
200
201 #[must_use]
204 pub fn is_clamped(&self) -> bool {
205 let last = self.knots.len() - 1;
206 self.knots[..=self.degree]
207 .iter()
208 .all(|k| *k == self.knots[0])
209 && self.knots[last - self.degree..]
210 .iter()
211 .all(|k| *k == self.knots[last])
212 }
213
214 #[must_use]
218 pub fn multiplicity_at(&self, index: usize) -> usize {
219 let Some(&value) = self.knots.get(index) else {
220 return 0;
221 };
222 self.knots.iter().filter(|k| **k == value).count()
223 }
224
225 #[must_use]
227 pub fn multiplicity_of(&self, value: f64) -> usize {
228 self.knots.iter().filter(|k| **k == value).count()
229 }
230
231 #[must_use]
233 pub fn distinct(&self) -> Vec<(f64, usize)> {
234 let mut out: Vec<(f64, usize)> = Vec::new();
235 for &k in &self.knots {
236 match out.last_mut() {
237 Some((value, count)) if *value == k => *count += 1,
238 _ => out.push((k, 1)),
239 }
240 }
241 out
242 }
243
244 #[must_use]
246 pub fn contains(&self, u: f64, tol: Tolerances) -> bool {
247 let (start, end) = self.domain();
248 u >= start - tol.parametric() && u <= end + tol.parametric()
249 }
250
251 pub fn span(&self, u: f64, tol: Tolerances) -> OgeomResult<usize> {
262 let (start, end) = self.domain();
263 if !u.is_finite() || u < start - tol.parametric() || u > end + tol.parametric() {
264 ogeom_bail!(Domain, "parameter {u} outside knot domain [{start}, {end}]");
265 }
266 Ok(self.span_unchecked(u))
267 }
268
269 #[must_use]
272 pub fn span_unchecked(&self, u: f64) -> usize {
273 let last = self.control_point_count() - 1;
274 if u >= self.knots[last + 1] {
277 return last;
278 }
279 if u <= self.knots[self.degree] {
280 return self.degree;
281 }
282 let mut low = self.degree;
283 let mut high = last + 1;
284 while high - low > 1 {
285 let mid = usize::midpoint(low, high);
286 if u < self.knots[mid] {
287 high = mid;
288 } else {
289 low = mid;
290 }
291 }
292 low
293 }
294
295 #[must_use]
305 pub fn basis(&self, span: usize, u: f64) -> BasisValues {
306 let p = self.degree;
307 let mut n = BasisValues::with_capacity(p + 1);
308 n.push(1.0);
309 let mut left = BasisValues::with_capacity(p + 1);
310 let mut right = BasisValues::with_capacity(p + 1);
311 left.push(0.0);
312 right.push(0.0);
313
314 for j in 1..=p {
315 left.push(u - self.knots[span + 1 - j]);
316 right.push(self.knots[span + j] - u);
317 let mut saved = 0.0;
318 n.push(0.0);
319 for r in 0..j {
320 let denominator = right[r + 1] + left[j - r];
324 let temp = n[r] / denominator;
325 n[r] = saved + right[r + 1] * temp;
326 saved = left[j - r] * temp;
327 }
328 n[j] = saved;
329 }
330 n
331 }
332
333 #[must_use]
339 pub fn basis_derivatives(&self, span: usize, u: f64, n: usize) -> DerivativeRows {
340 let p = self.degree;
341 let order = n.min(p);
342
343 let mut ndu: SmallVec<[BasisValues; 8]> =
349 core::iter::repeat_with(|| BasisValues::from_elem(0.0, p + 1))
350 .take(p + 1)
351 .collect();
352 ndu[0][0] = 1.0;
353 let mut left = BasisValues::from_elem(0.0, p + 1);
354 let mut right = BasisValues::from_elem(0.0, p + 1);
355
356 for j in 1..=p {
357 left[j] = u - self.knots[span + 1 - j];
358 right[j] = self.knots[span + j] - u;
359 let mut saved = 0.0;
360 for r in 0..j {
361 ndu[j][r] = right[r + 1] + left[j - r];
362 let temp = ndu[r][j - 1] / ndu[j][r];
363 ndu[r][j] = saved + right[r + 1] * temp;
364 saved = left[j - r] * temp;
365 }
366 ndu[j][j] = saved;
367 }
368
369 let mut derivatives: DerivativeRows =
370 core::iter::repeat_with(|| BasisValues::from_elem(0.0, p + 1))
371 .take(n + 1)
372 .collect();
373 for (j, slot) in derivatives[0].iter_mut().enumerate() {
374 *slot = ndu[j][p];
375 }
376 let mut a = [
381 BasisValues::from_elem(0.0, p + 1),
382 BasisValues::from_elem(0.0, p + 1),
383 ];
384 for r in 0..=p {
385 let (mut s1, mut s2) = (0_usize, 1_usize);
386 a[0][0] = 1.0;
387 for k in 1..=order {
388 let mut d = 0.0;
389 let rk = r as isize - k as isize;
390 let pk = p - k;
391 if r >= k {
392 a[s2][0] = a[s1][0] / ndu[pk + 1][rk as usize];
393 d = a[s2][0] * ndu[rk as usize][pk];
394 }
395 let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
396 let j2 = if r as isize - 1 <= pk as isize {
397 k - 1
398 } else {
399 p - r
400 };
401 for j in j1..=j2 {
402 let index = (rk + j as isize) as usize;
403 a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
404 d += a[s2][j] * ndu[index][pk];
405 }
406 if r <= pk {
407 a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
408 d += a[s2][k] * ndu[r][pk];
409 }
410 derivatives[k][r] = d;
411 core::mem::swap(&mut s1, &mut s2);
412 }
413 }
414
415 let mut factor = p;
417 for (k, row) in derivatives.iter_mut().enumerate().take(order + 1).skip(1) {
418 #[allow(clippy::cast_precision_loss)]
419 let scale = factor as f64;
420 for value in row.iter_mut() {
421 *value *= scale;
422 }
423 factor = factor.saturating_mul(p.saturating_sub(k));
424 }
425 derivatives
426 }
427
428 pub fn with_knot_inserted(&self, value: f64, count: usize) -> OgeomResult<Self> {
438 let mut knots = self.knots.clone();
439 let position = knots.partition_point(|k| *k <= value);
440 for _ in 0..count {
441 knots.insert(position, value);
442 }
443 Self::new(knots, self.degree)
444 }
445
446 pub fn reparameterized(&self, start: f64, end: f64) -> OgeomResult<Self> {
453 if !start.is_finite() || !end.is_finite() || end <= start {
454 ogeom_bail!(Construction, "target range [{start}, {end}] is empty");
455 }
456 let (a, b) = self.domain();
457 let scale = (end - start) / (b - a);
458 let mut knots: Vec<f64> = self
462 .knots
463 .iter()
464 .map(|k| (k - a).mul_add(scale, start))
465 .collect();
466 for k in &mut knots {
467 if *k <= start {
468 *k = start;
469 } else if *k >= end {
470 *k = end;
471 }
472 }
473 let last = knots.len() - 1;
474 for i in 0..self.knots.len() {
475 if self.knots[i] == a {
476 knots[i] = start;
477 }
478 if self.knots[last - i] == b {
479 knots[last - i] = end;
480 }
481 }
482 Self::new(knots, self.degree)
483 }
484
485 #[must_use]
497 pub fn reversed(&self) -> Self {
498 let (a, b) = self.domain();
499 let sum = a + b;
500 let mut knots: Vec<f64> = self.knots.iter().rev().map(|k| sum - k).collect();
501 let last = knots.len() - 1;
503 for i in 0..knots.len() {
504 if knots[i] <= a {
505 knots[i] = a;
506 }
507 if knots[last - i] >= b {
508 knots[last - i] = b;
509 }
510 }
511 Self {
512 knots,
513 degree: self.degree,
514 }
515 }
516}
517
518#[cfg(test)]
519#[allow(clippy::unwrap_used)]
520mod tests {
521 use super::*;
522 use approx::assert_relative_eq;
523
524 const T: Tolerances = Tolerances::millimetres();
525
526 fn cubic() -> KnotVector {
527 KnotVector::new(
529 vec![0.0, 0.0, 0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0, 1.0, 1.0],
530 3,
531 )
532 .unwrap()
533 }
534
535 #[test]
536 fn malformed_vectors_are_refused() {
537 assert!(KnotVector::new(vec![0.0, 1.0], 3).is_err(), "too short");
538 assert!(
539 KnotVector::new(vec![0.0, 0.0, 1.0, 0.5, 1.0, 1.0], 2).is_err(),
540 "not non-decreasing"
541 );
542 assert!(
543 KnotVector::new(vec![0.0, 0.0, f64::NAN, 1.0, 1.0, 1.0], 2).is_err(),
544 "non-finite"
545 );
546 assert!(KnotVector::new(vec![0.0; 8], 3).is_err(), "empty domain");
547 assert!(KnotVector::new(vec![], 0).is_err(), "degree zero");
548 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());
550 assert!(KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0], 2).is_ok());
552 }
553
554 #[test]
555 fn clamped_uniform_has_the_expected_shape() {
556 let k = KnotVector::clamped_uniform(3, 7).unwrap();
557 assert_eq!(k.knots().len(), 11);
558 assert_eq!(k.control_point_count(), 7);
559 assert_eq!(k.domain(), (0.0, 1.0));
560 assert!(k.is_clamped());
561 assert_eq!(k.multiplicity_of(0.0), 4);
562 assert_eq!(k.multiplicity_of(1.0), 4);
563 assert_relative_eq!(k.knots()[4], 1.0 / 4.0);
564
565 let b = KnotVector::clamped_uniform(3, 4).unwrap();
567 assert_eq!(b.knots(), &[0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0]);
568 assert!(KnotVector::clamped_uniform(3, 3).is_err());
569 }
570
571 #[test]
572 fn span_lookup_handles_both_ends_of_the_domain() {
573 let k = cubic();
574 assert_eq!(k.span(0.0, T).unwrap(), 3, "domain start");
575 assert_eq!(k.span(0.1, T).unwrap(), 3);
576 assert_eq!(k.span(0.25, T).unwrap(), 4, "on a knot, span to its right");
577 assert_eq!(k.span(0.3, T).unwrap(), 4);
578 assert_eq!(k.span(0.9, T).unwrap(), 6);
579 assert_eq!(k.span(1.0, T).unwrap(), 6);
581 assert!(k.span(-0.1, T).is_err());
582 assert!(k.span(1.1, T).is_err());
583 assert!(k.span(f64::NAN, T).is_err());
584 }
585
586 #[test]
587 fn basis_functions_form_a_partition_of_unity() {
588 let k = cubic();
589 for i in 0..=200 {
590 let u = f64::from(i) / 200.0;
591 let span = k.span(u, T).unwrap();
592 let n = k.basis(span, u);
593 assert_eq!(n.len(), 4);
594 let sum: f64 = n.iter().sum();
595 assert_relative_eq!(sum, 1.0, epsilon = 1e-14);
596 assert!(n.iter().all(|v| *v >= -1e-15), "basis must be non-negative");
597 }
598 }
599
600 #[test]
601 fn basis_matches_the_bernstein_polynomials_for_a_bezier() {
602 let k = KnotVector::clamped_uniform(3, 4).unwrap();
604 for i in 0..=20 {
605 let u = f64::from(i) / 20.0;
606 let span = k.span(u, T).unwrap();
607 let n = k.basis(span, u);
608 let v = 1.0 - u;
609 let expected = [v * v * v, 3.0 * u * v * v, 3.0 * u * u * v, u * u * u];
610 for (got, want) in n.iter().zip(expected) {
611 assert_relative_eq!(*got, want, epsilon = 1e-14);
612 }
613 }
614 }
615
616 #[test]
617 fn basis_is_an_interpolant_at_a_clamped_end() {
618 let k = cubic();
619 let n = k.basis(k.span(0.0, T).unwrap(), 0.0);
620 assert_relative_eq!(n[0], 1.0, epsilon = 1e-15);
621 assert!(n[1..].iter().all(|v| v.abs() < 1e-15));
622
623 let n = k.basis(k.span(1.0, T).unwrap(), 1.0);
624 assert_relative_eq!(n[3], 1.0, epsilon = 1e-15);
625 assert!(n[..3].iter().all(|v| v.abs() < 1e-15));
626 }
627
628 #[test]
629 fn basis_derivatives_agree_with_finite_differences() {
630 let k = cubic();
631 let h = 1e-6;
632 for i in 1..20 {
633 let u = f64::from(i) / 20.0;
634 let span = k.span(u, T).unwrap();
635 let d = k.basis_derivatives(span, u, 2);
636
637 let plain = k.basis(span, u);
639 for (a, b) in d[0].iter().zip(plain.iter()) {
640 assert_relative_eq!(a, b, epsilon = 1e-14);
641 }
642
643 let ahead = k.basis(span, u + h);
646 let behind = k.basis(span, u - h);
647 for j in 0..=k.degree() {
648 let numeric = (ahead[j] - behind[j]) / (2.0 * h);
649 assert_relative_eq!(d[1][j], numeric, epsilon = 1e-5);
650 }
651 }
652 }
653
654 #[test]
655 fn basis_derivatives_sum_to_zero() {
656 let k = cubic();
659 for i in 0..=50 {
660 let u = f64::from(i) / 50.0;
661 let span = k.span(u, T).unwrap();
662 let d = k.basis_derivatives(span, u, 3);
663 assert_relative_eq!(d[0].iter().sum::<f64>(), 1.0, epsilon = 1e-13);
664 for (order, row) in d.iter().enumerate().skip(1) {
665 let sum: f64 = row.iter().sum();
666 assert!(sum.abs() < 1e-8, "order {order} sums to {sum}");
667 }
668 }
669 }
670
671 #[test]
672 fn derivatives_above_the_degree_are_zero() {
673 let k = cubic();
674 let span = k.span(0.4, T).unwrap();
675 let d = k.basis_derivatives(span, 0.4, 5);
676 assert_eq!(d.len(), 6);
677 for (order, row) in d.iter().enumerate().skip(k.degree() + 1) {
678 assert!(row.iter().all(|v| *v == 0.0), "order {order} is not zero");
679 }
680 }
681
682 #[test]
683 fn multiplicity_and_distinct_knots() {
684 let k = cubic();
685 assert_eq!(k.multiplicity_of(0.0), 4);
686 assert_eq!(k.multiplicity_of(0.5), 1);
687 assert_eq!(k.multiplicity_of(0.6), 0);
688 assert_eq!(k.multiplicity_at(0), 4);
689 assert_eq!(
690 k.distinct(),
691 vec![(0.0, 4), (0.25, 1), (0.5, 1), (0.75, 1), (1.0, 4)]
692 );
693 }
694
695 #[test]
696 fn knot_insertion_raises_multiplicity() {
697 let k = cubic();
698 let inserted = k.with_knot_inserted(0.5, 2).unwrap();
699 assert_eq!(inserted.multiplicity_of(0.5), 3);
700 assert_eq!(inserted.knots().len(), k.knots().len() + 2);
701 assert_eq!(inserted.domain(), k.domain());
702 assert!(k.with_knot_inserted(0.5, 3).is_err());
704 }
705
706 #[test]
707 fn reparameterization_preserves_clamping_exactly() {
708 let k = cubic();
709 let r = k.reparameterized(-2.0, 6.0).unwrap();
710 assert_eq!(r.domain(), (-2.0, 6.0));
711 assert!(r.is_clamped(), "the repeated end knots must stay identical");
712 assert_eq!(r.multiplicity_of(-2.0), 4);
713 assert_eq!(r.multiplicity_of(6.0), 4);
714 assert_relative_eq!(r.knots()[4], 0.0, epsilon = 1e-12);
716 assert!(k.reparameterized(1.0, 1.0).is_err());
717 assert!(k.reparameterized(1.0, f64::NAN).is_err());
718 }
719
720 #[test]
721 fn reversal_mirrors_the_spacing_and_keeps_the_domain() {
722 let k = KnotVector::new(vec![0.0, 0.0, 0.0, 0.1, 0.8, 1.0, 1.0, 1.0], 2).unwrap();
724 let r = k.reversed();
725 assert_eq!(r.domain(), k.domain());
726 assert!(r.is_clamped());
727 assert_relative_eq!(r.knots()[3], 0.2, epsilon = 1e-15);
728 assert_relative_eq!(r.knots()[4], 0.9, epsilon = 1e-15);
729 for (got, want) in r.reversed().knots().iter().zip(k.knots()) {
732 assert_relative_eq!(got, want, epsilon = 1e-15);
733 }
734 let multiplicities: Vec<usize> = r.distinct().iter().map(|(_, m)| *m).collect();
738 let original: Vec<usize> = k.distinct().iter().map(|(_, m)| *m).collect();
739 assert_eq!(multiplicities, original);
740 }
741
742 #[test]
743 fn averaged_knots_follow_the_parameters() {
744 let params = [0.0, 0.1, 0.4, 0.9, 1.0];
745 let k = KnotVector::averaged(3, ¶ms).unwrap();
746 assert_eq!(k.control_point_count(), 5);
747 assert_eq!(k.domain(), (0.0, 1.0));
748 assert!(k.is_clamped());
749 assert_relative_eq!(k.knots()[4], (0.1 + 0.4 + 0.9) / 3.0, epsilon = 1e-15);
751 assert!(KnotVector::averaged(3, &[0.0, 1.0]).is_err());
752 assert!(
753 KnotVector::averaged(2, &[0.0, 0.5, 0.5, 1.0]).is_err(),
754 "not increasing"
755 );
756 }
757
758 #[test]
759 fn basis_at_a_repeated_interior_knot_is_still_a_partition_of_unity() {
760 let k = KnotVector::new(vec![0.0, 0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 1.0], 2).unwrap();
763 for u in [0.0_f64, 0.25, 0.499_999, 0.5, 0.500_001, 0.75, 1.0] {
764 let span = k.span(u, T).unwrap();
765 let n = k.basis(span, u);
766 assert_relative_eq!(n.iter().sum::<f64>(), 1.0, epsilon = 1e-14);
767 assert!(n.iter().all(|v| v.is_finite()), "non-finite basis at {u}");
768 }
769 }
770}