1use super::*;
2
3pub struct SplineScratch {
5 pub(crate) inner: internal::BsplineScratch,
6}
7
8impl SplineScratch {
9 pub fn new(degree: usize) -> Self {
10 Self {
11 inner: internal::BsplineScratch::new(degree),
12 }
13 }
14}
15
16pub fn evaluate_bspline_basis_scalar(
20 x: f64,
21 knot_vector: ArrayView1<f64>,
22 degree: usize,
23 out: &mut [f64],
24 scratch: &mut SplineScratch,
25) -> Result<(), BasisError> {
26 validate_knots_for_degree(knot_vector, degree)?;
27
28 let num_basis = knot_vector.len() - degree - 1;
29 if out.len() != num_basis {
30 return Err(BasisError::InvalidKnotVector(format!(
31 "Output buffer length {} does not match number of basis functions {}",
32 out.len(),
33 num_basis
34 )));
35 }
36
37 internal::evaluate_splines_at_point_into(x, degree, knot_vector, out, &mut scratch.inner);
38
39 Ok(())
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct PeriodicBSplineBasisSpec {
51 pub degree: usize,
53 pub num_basis: usize,
55 pub period: f64,
57 pub origin: f64,
59 pub penalty_order: usize,
62}
63
64impl PeriodicBSplineBasisSpec {
65 pub fn new(
68 degree: usize,
69 num_basis: usize,
70 period: f64,
71 origin: f64,
72 penalty_order: usize,
73 ) -> Self {
74 Self {
75 degree,
76 num_basis,
77 period,
78 origin,
79 penalty_order,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct PeriodicSplineCurve {
92 pub spec: PeriodicBSplineBasisSpec,
93 pub coefficients: Array2<f64>,
94}
95
96impl PeriodicSplineCurve {
97 pub fn ambient_dim(&self) -> usize {
99 self.coefficients.ncols()
100 }
101
102 pub fn evaluate(&self, u: ArrayView1<'_, f64>) -> Result<Array2<f64>, BasisError> {
105 if self.coefficients.nrows() != self.spec.num_basis {
106 crate::bail_dim_basis!(
107 "curve coefficient rows ({}) must equal periodic basis size ({})",
108 self.coefficients.nrows(),
109 self.spec.num_basis
110 );
111 }
112 let basis = build_periodic_bspline_basis_1d(u, &self.spec)?;
113 Ok(basis.dot(&self.coefficients))
114 }
115
116}
117
118pub(crate) fn validate_periodic_bspline_spec(
119 spec: &PeriodicBSplineBasisSpec,
120) -> Result<(), BasisError> {
121 if spec.degree < 1 {
122 return Err(BasisError::InvalidDegree(spec.degree));
123 }
124 if spec.num_basis < spec.degree + 1 {
125 crate::bail_invalid_basis!(
126 "periodic B-spline basis requires num_basis >= degree + 1 (got num_basis={}, degree={})",
127 spec.num_basis,
128 spec.degree
129 );
130 }
131 if !spec.period.is_finite() || spec.period <= 0.0 {
132 crate::bail_invalid_basis!(
133 "periodic B-spline period must be finite and positive, got {}",
134 spec.period
135 );
136 }
137 if !spec.origin.is_finite() {
138 crate::bail_invalid_basis!(
139 "periodic B-spline origin must be finite, got {}",
140 spec.origin
141 );
142 }
143 if spec.penalty_order == 0 || spec.penalty_order >= spec.num_basis {
144 return Err(BasisError::InvalidPenaltyOrder {
145 order: spec.penalty_order,
146 num_basis: spec.num_basis,
147 });
148 }
149 if spec.penalty_order > spec.degree {
150 return Err(BasisError::InsufficientDegreeForDerivative {
151 degree: spec.degree,
152 derivative_order: spec.penalty_order,
153 minimum_degree: spec.penalty_order,
154 });
155 }
156 Ok(())
157}
158
159#[inline]
160pub(crate) fn wrap_periodic_phase(u: f64, origin: f64, period: f64) -> f64 {
161 let wrapped = (u - origin).rem_euclid(period);
162 if wrapped >= period { 0.0 } else { wrapped }
165}
166
167pub(crate) fn cardinal_bspline_value(x: f64, degree: usize) -> f64 {
168 if degree == 0 {
169 return if (0.0..1.0).contains(&x) { 1.0 } else { 0.0 };
170 }
171 if x <= 0.0 || x >= (degree + 1) as f64 {
172 return 0.0;
173 }
174 let p = degree as f64;
175 (x / p) * cardinal_bspline_value(x, degree - 1)
176 + (((degree + 1) as f64 - x) / p) * cardinal_bspline_value(x - 1.0, degree - 1)
177}
178
179pub(crate) fn fill_periodic_bspline_unnormalized_value_row(
180 u: f64,
181 origin: f64,
182 period: f64,
183 degree: usize,
184 row: &mut [f64],
185) -> f64 {
186 let m = row.len();
187 let m_f = m as f64;
188 let h = period / m_f;
189 let t = wrap_periodic_phase(u, origin, period) / h;
190 let mut rowsum = 0.0_f64;
191 for (col, value_slot) in row.iter_mut().enumerate() {
192 let base = t - col as f64;
193 let k_min = ((-base) / m_f).floor() as isize - 1;
194 let k_max = (((degree + 1) as f64 - base) / m_f).ceil() as isize + 1;
195 let mut value = 0.0_f64;
196 for k in k_min..=k_max {
197 value += cardinal_bspline_value(base + (k as f64) * m_f, degree);
198 }
199 *value_slot = value;
200 rowsum += value;
201 }
202 rowsum
203}
204
205pub(crate) fn fill_periodic_bspline_unnormalized_derivative_row(
206 u: f64,
207 origin: f64,
208 period: f64,
209 degree: usize,
210 row: &mut [f64],
211) -> f64 {
212 let m = row.len();
213 let m_f = m as f64;
214 let h = period / m_f;
215 let tau = wrap_periodic_phase(u, origin, period) / h;
216 let mut rowsum_derivative = 0.0_f64;
217 for (col, value_slot) in row.iter_mut().enumerate() {
218 let base = tau - col as f64;
219 let k_min = ((-base) / m_f).floor() as isize - 1;
220 let k_max = (((degree + 1) as f64 - base) / m_f).ceil() as isize + 1;
221 let mut value = 0.0_f64;
222 for k in k_min..=k_max {
223 let x_arg = base + (k as f64) * m_f;
224 value += cardinal_bspline_value(x_arg, degree - 1)
225 - cardinal_bspline_value(x_arg - 1.0, degree - 1);
226 }
227 let derivative = value / h;
228 *value_slot = derivative;
229 rowsum_derivative += derivative;
230 }
231 rowsum_derivative
232}
233
234pub fn build_periodic_bspline_basis_1d(
242 u: ArrayView1<'_, f64>,
243 spec: &PeriodicBSplineBasisSpec,
244) -> Result<Array2<f64>, BasisError> {
245 validate_periodic_bspline_spec(spec)?;
246 if u.iter().any(|v| !v.is_finite()) {
247 crate::bail_invalid_basis!("periodic B-spline inputs must all be finite");
248 }
249
250 let n = u.len();
251 let m = spec.num_basis;
252 let mut out = Array2::<f64>::zeros((n, m));
253 let mut value_row = vec![0.0_f64; m];
254 for (row_idx, &ui) in u.iter().enumerate() {
255 let rowsum = fill_periodic_bspline_unnormalized_value_row(
256 ui,
257 spec.origin,
258 spec.period,
259 spec.degree,
260 &mut value_row,
261 );
262 if !rowsum.is_finite() || rowsum <= 0.0 {
263 crate::bail_invalid_basis!(
264 "periodic B-spline row has non-positive rowsum at row {row_idx}: {rowsum}"
265 );
266 }
267 for col in 0..m {
268 out[[row_idx, col]] = value_row[col] / rowsum;
269 }
270 }
271 Ok(out)
272}
273
274pub fn create_ispline_derivative_dense(
283 data: ArrayView1<'_, f64>,
284 knot_vector: &Array1<f64>,
285 degree: usize,
286 derivative_order: usize,
287) -> Result<Array2<f64>, BasisError> {
288 if derivative_order == 0 {
289 let (basis_arc, _) = create_basis::<Dense>(
291 data,
292 KnotSource::Provided(knot_vector.view()),
293 degree,
294 BasisOptions::i_spline(),
295 )?;
296 return Ok(basis_arc.as_ref().clone());
297 }
298 let bs_degree = degree
299 .checked_add(1)
300 .ok_or_else(|| BasisError::InvalidInput("I-spline degree overflow".to_string()))?;
301 if derivative_order > bs_degree {
302 let num_bspline_basis = knot_vector.len().saturating_sub(bs_degree + 1);
304 let num_ispline_basis = num_bspline_basis.saturating_sub(1);
305 return Ok(Array2::zeros((data.len(), num_ispline_basis)));
306 }
307 let num_bspline_cols = knot_vector.len().saturating_sub(bs_degree + 1);
308 let db = match derivative_order {
309 1 => {
310 let (db_arc, _) = create_basis::<Dense>(
311 data,
312 KnotSource::Provided(knot_vector.view()),
313 bs_degree,
314 BasisOptions::first_derivative(),
315 )?;
316 db_arc.as_ref().clone()
317 }
318 2 => {
319 let (db_arc, _) = create_basis::<Dense>(
320 data,
321 KnotSource::Provided(knot_vector.view()),
322 bs_degree,
323 BasisOptions::second_derivative(),
324 )?;
325 db_arc.as_ref().clone()
326 }
327 3 => {
328 let mut db = Array2::<f64>::zeros((data.len(), num_bspline_cols));
329 for (row_idx, &x) in data.iter().enumerate() {
330 let row = db.slice_mut(s![row_idx, ..]).into_slice().ok_or_else(|| {
331 BasisError::InvalidInput(
332 "I-spline derivative row is not contiguous".to_string(),
333 )
334 })?;
335 evaluate_bsplinethird_derivative_scalar(x, knot_vector.view(), bs_degree, row)?;
336 }
337 db
338 }
339 4 => {
340 let mut db = Array2::<f64>::zeros((data.len(), num_bspline_cols));
341 for (row_idx, &x) in data.iter().enumerate() {
342 let row = db.slice_mut(s![row_idx, ..]).into_slice().ok_or_else(|| {
343 BasisError::InvalidInput(
344 "I-spline derivative row is not contiguous".to_string(),
345 )
346 })?;
347 evaluate_bspline_fourth_derivative_scalar(x, knot_vector.view(), bs_degree, row)?;
348 }
349 db
350 }
351 other => {
352 crate::bail_invalid_basis!(
353 "I-spline derivative supports orders 1..=4; got order={other}"
354 );
355 }
356 };
357 let num_ispline_cols = num_bspline_cols.saturating_sub(1);
358 if num_ispline_cols == 0 {
359 return Ok(Array2::zeros((data.len(), 0)));
360 }
361 let left = knot_vector[bs_degree];
394 let right = knot_vector[num_bspline_cols];
395 let interval_is_usable = left.is_finite() && right.is_finite() && left < right;
396
397 let mut out = Array2::<f64>::zeros((data.len(), num_ispline_cols));
400 for i in 0..data.len() {
401 if interval_is_usable && !(data[i] >= left && data[i] <= right) {
409 continue;
410 }
411 let mut running = 0.0_f64;
412 for j in (1..num_bspline_cols).rev() {
413 let term = db[[i, j]];
414 if term.is_finite() {
415 running += term;
416 }
417 out[[i, j - 1]] = running;
418 }
419 }
420 Ok(out)
421}
422
423pub fn evaluate_bspline_derivative_scalar(
435 x: f64,
436 knot_vector: ArrayView1<f64>,
437 degree: usize,
438 out: &mut [f64],
439) -> Result<(), BasisError> {
440 if degree < 1 {
441 return Err(BasisError::InvalidDegree(degree));
442 }
443 let num_basis_lower = knot_vector.len().saturating_sub(degree);
444 let mut lower_basis = vec![0.0; num_basis_lower];
445 let mut lower_scratch = internal::BsplineScratch::new(degree.saturating_sub(1));
446 evaluate_bspline_derivative_scalar_into(
447 x,
448 knot_vector,
449 degree,
450 out,
451 &mut lower_basis,
452 &mut lower_scratch,
453 )
454}
455
456pub fn evaluate_bspline_derivative_scalar_into(
460 x: f64,
461 knot_vector: ArrayView1<f64>,
462 degree: usize,
463 out: &mut [f64],
464 lower_basis: &mut [f64],
465 lower_scratch: &mut internal::BsplineScratch,
466) -> Result<(), BasisError> {
467 validate_knots_for_degree(knot_vector, degree)?;
468
469 let num_basis = knot_vector.len() - degree - 1;
470 if out.len() != num_basis {
471 return Err(BasisError::InvalidKnotVector(format!(
472 "Output buffer length {} does not match number of basis functions {}",
473 out.len(),
474 num_basis
475 )));
476 }
477
478 let num_basis_lower = knot_vector.len() - degree;
479 if lower_basis.len() < num_basis_lower {
480 return Err(BasisError::InvalidKnotVector(format!(
481 "lower_basis buffer too small: {} < {}",
482 lower_basis.len(),
483 num_basis_lower
484 )));
485 }
486
487 for v in lower_basis.iter_mut().take(num_basis_lower) {
489 *v = 0.0;
490 }
491
492 if open_knot_derivative_exterior_is_zero(x, knot_vector, degree) {
505 out.fill(0.0);
506 return Ok(());
507 }
508 let x_clamped = clamp_eval_point_to_modeling_interval(x, knot_vector, degree);
509 let x_eval = one_sided_derivative_eval_point(x_clamped, knot_vector, degree);
510
511 internal::evaluate_splines_at_point_full_support_into(
513 x_eval,
514 degree - 1,
515 knot_vector,
516 &mut lower_basis[..num_basis_lower],
517 lower_scratch,
518 );
519
520 let k = degree as f64;
522 for i in 0..num_basis {
523 let denom_left = knot_vector[i + degree] - knot_vector[i];
524 let denom_right = knot_vector[i + degree + 1] - knot_vector[i + 1];
525
526 let left_term = if !knot_span_is_degenerate(denom_left) && i < num_basis_lower {
527 lower_basis[i] / denom_left
528 } else {
529 0.0
530 };
531
532 let right_term = if !knot_span_is_degenerate(denom_right) && (i + 1) < num_basis_lower {
533 lower_basis[i + 1] / denom_right
534 } else {
535 0.0
536 };
537
538 out[i] = k * (left_term - right_term);
539 }
540
541 Ok(())
542}
543
544fn mspline_scales(knot_vector: ArrayView1<f64>, degree: usize, num_basis: usize) -> Vec<f64> {
550 let order = (degree + 1) as f64;
551 (0..num_basis)
552 .map(|i| order / (knot_vector[i + degree + 1] - knot_vector[i]))
553 .collect()
554}
555
556pub(crate) fn create_mspline_dense(
557 data: ArrayView1<f64>,
558 knot_vector: ArrayView1<f64>,
559 degree: usize,
560) -> Result<Array2<f64>, BasisError> {
561 validate_knots_for_degree(knot_vector, degree)?;
562 validate_mspline_normalization_spans(knot_vector, degree)?;
563 let num_basis = knot_vector.len() - degree - 1;
564 let mut out = Array2::<f64>::zeros((data.len(), num_basis));
565 let mut scratch = internal::BsplineScratch::new(degree);
566 let support = degree + 1;
567 let mut local = vec![0.0; support];
568 let left = knot_vector[degree];
569 let right = knot_vector[num_basis];
570 let scales = mspline_scales(knot_vector, degree, num_basis);
571
572 for (row_i, &x) in data.iter().enumerate() {
573 if x < left || x > right {
574 continue;
575 }
576 let start = internal::evaluate_splines_sparse_into(
577 x,
578 degree,
579 knot_vector,
580 &mut local,
581 &mut scratch,
582 );
583 for (offset, &b) in local.iter().enumerate() {
584 let j = start + offset;
585 if j < num_basis {
586 out[[row_i, j]] = b * scales[j];
587 }
588 }
589 }
590 Ok(out)
591}
592
593pub(crate) fn create_mspline_sparse(
594 data: ArrayView1<f64>,
595 knot_vector: ArrayView1<f64>,
596 degree: usize,
597) -> Result<SparseColMat<usize, f64>, BasisError> {
598 validate_knots_for_degree(knot_vector, degree)?;
599 validate_mspline_normalization_spans(knot_vector, degree)?;
600 let nrows = data.len();
601 let ncols = knot_vector.len() - degree - 1;
602 let mut scratch = internal::BsplineScratch::new(degree);
603 let support = degree + 1;
604 let mut local = vec![0.0; support];
605 let left = knot_vector[degree];
606 let right = knot_vector[ncols];
607 let scales = mspline_scales(knot_vector, degree, ncols);
608
609 let mut triplets: Vec<Triplet<usize, usize, f64>> =
610 Vec::with_capacity(nrows.saturating_mul(support));
611 for (row_i, &x) in data.iter().enumerate() {
612 if x < left || x > right {
613 continue;
614 }
615 let start = internal::evaluate_splines_sparse_into(
616 x,
617 degree,
618 knot_vector,
619 &mut local,
620 &mut scratch,
621 );
622 for (offset, &b) in local.iter().enumerate() {
623 let col = start + offset;
624 if col >= ncols {
625 continue;
626 }
627 let v = b * scales[col];
628 if v.abs() > 0.0 {
629 triplets.push(Triplet::new(row_i, col, v));
630 }
631 }
632 }
633
634 SparseColMat::try_new_from_triplets(nrows, ncols, &triplets)
635 .map_err(|e| BasisError::SparseCreation(format!("{e:?}")))
636}
637
638pub(crate) fn validate_mspline_normalization_spans(
639 knot_vector: ArrayView1<f64>,
640 degree: usize,
641) -> Result<(), BasisError> {
642 let num_basis = knot_vector.len().saturating_sub(degree + 1);
643 for i in 0..num_basis {
644 let span = knot_vector[i + degree + 1] - knot_vector[i];
645 if span <= 0.0 {
646 crate::bail_invalid_basis!(
647 "invalid M-spline normalization span at i={i}: t[i+degree+1]-t[i]={span:.3e} must be > 0"
648 );
649 }
650 }
651 Ok(())
652}
653
654pub(crate) fn create_ispline_dense(
655 data: ArrayView1<f64>,
656 knot_vector: ArrayView1<f64>,
657 degree: usize,
658) -> Result<Array2<f64>, BasisError> {
659 let bs_degree = degree
660 .checked_add(1)
661 .ok_or_else(|| BasisError::InvalidInput("I-spline degree overflow".to_string()))?;
662 validate_knots_for_degree(knot_vector, bs_degree)?;
663 let num_bspline_basis = knot_vector.len() - bs_degree - 1;
664 let num_ispline_basis = num_bspline_basis.saturating_sub(1);
665 let mut out = Array2::<f64>::zeros((data.len(), num_ispline_basis));
666 let mut scratch = internal::BsplineScratch::new(bs_degree);
667 let support = bs_degree + 1;
668 let mut local = vec![0.0; support];
669 let left = knot_vector[bs_degree];
670 let right = knot_vector[num_bspline_basis];
671
672 let mut left_local = vec![0.0_f64; support];
674 let mut left_scratch = internal::BsplineScratch::new(bs_degree);
675 let mut left_offsets = vec![0.0_f64; num_bspline_basis];
676 internal::cumulative_bspline_offsets_into(
677 left,
678 bs_degree,
679 knot_vector,
680 &mut left_local,
681 &mut left_scratch,
682 &mut left_offsets,
683 );
684
685 for (row_i, &x) in data.iter().enumerate() {
698 if x < left {
699 continue;
701 }
702 if x >= right {
703 for j in 1..num_bspline_basis {
704 let value = 1.0 - left_offsets[j];
705 out[[row_i, j - 1]] = if value.abs() <= 1e-15 { 0.0 } else { value };
706 }
707 continue;
708 }
709 let start = internal::evaluate_splines_sparse_into(
710 x,
711 bs_degree,
712 knot_vector,
713 &mut local,
714 &mut scratch,
715 );
716 let total = local.iter().copied().sum::<f64>();
717 let lead_end = start.min(num_bspline_basis);
718 if lead_end > 1 {
719 out.slice_mut(s![row_i, 0..(lead_end - 1)]).fill(total);
720 }
721 let mut running = 0.0f64;
722 for offset in (0..support).rev() {
723 let j = start + offset;
724 if j >= num_bspline_basis {
725 continue;
726 }
727 running += local[offset];
728 if j > 0 {
729 let value = running - left_offsets[j];
730 out[[row_i, j - 1]] = if value.abs() <= 1e-15 { 0.0 } else { value };
731 }
732 }
733 }
734 Ok(out)
735}
736
737#[derive(Default)]
748pub struct BsplineDerivativeWorkspace {
749 pub(crate) chain: Vec<Vec<f64>>,
752 pub(crate) lower_basis: Vec<f64>,
754 pub(crate) lower_scratch: internal::BsplineScratch,
756}
757
758impl BsplineDerivativeWorkspace {
759 #[inline]
761 pub fn new() -> Self {
762 Self::default()
763 }
764
765 #[inline]
768 pub(crate) fn chain_buffer(&mut self, depth: usize, len: usize) -> &mut [f64] {
769 if self.chain.len() <= depth {
770 self.chain.resize_with(depth + 1, Vec::new);
771 }
772 let buf = &mut self.chain[depth];
773 if buf.len() != len {
774 buf.resize(len, 0.0);
775 }
776 for v in buf.iter_mut() {
777 *v = 0.0;
778 }
779 buf
780 }
781}
782
783pub(crate) fn evaluate_bspline_derivative_recurrence_into(
801 derivative_order: usize,
802 x: f64,
803 knot_vector: ArrayView1<f64>,
804 degree: usize,
805 out: &mut [f64],
806 workspace: &mut BsplineDerivativeWorkspace,
807 depth: usize,
808) -> Result<(), BasisError> {
809 if degree < derivative_order {
810 return Err(BasisError::InsufficientDegreeForDerivative {
811 degree,
812 derivative_order,
813 minimum_degree: derivative_order,
814 });
815 }
816 if depth == 0
831 && (open_knot_derivative_exterior_is_zero(x, knot_vector, degree)
832 || linear_extension_higher_derivative_is_zero(x, knot_vector, degree, derivative_order))
833 {
834 out.fill(0.0);
835 return Ok(());
836 }
837 let x = if depth == 0 {
838 clamp_eval_point_to_modeling_interval(x, knot_vector, degree)
839 } else {
840 x
841 };
842
843 if derivative_order <= 1 {
846 let num_basis_lower = knot_vector.len().saturating_sub(degree);
847 if workspace.lower_basis.len() < num_basis_lower {
848 workspace.lower_basis.resize(num_basis_lower, 0.0);
849 }
850 return evaluate_bspline_derivative_scalar_into(
851 x,
852 knot_vector,
853 degree,
854 out,
855 &mut workspace.lower_basis,
856 &mut workspace.lower_scratch,
857 );
858 }
859
860 validate_knots_for_degree(knot_vector, degree)?;
861
862 let num_basis = knot_vector.len() - degree - 1;
863 if out.len() != num_basis {
864 return Err(BasisError::InvalidKnotVector(format!(
865 "Output buffer length {} does not match number of basis functions {}",
866 out.len(),
867 num_basis
868 )));
869 }
870 let num_basis_lower = knot_vector.len() - degree;
874
875 workspace.chain_buffer(depth, num_basis_lower);
879 let mut lower = std::mem::take(&mut workspace.chain[depth]);
880
881 let recurse = evaluate_bspline_derivative_recurrence_into(
882 derivative_order - 1,
883 x,
884 knot_vector,
885 degree - 1,
886 &mut lower,
887 workspace,
888 depth + 1,
889 );
890 workspace.chain[depth] = lower;
891 recurse?;
892
893 let lower = &workspace.chain[depth];
894 let k = degree as f64;
895 for i in 0..num_basis {
896 let denom1 = knot_vector[i + degree] - knot_vector[i];
897 let denom2 = knot_vector[i + degree + 1] - knot_vector[i + 1];
898 let term1 = if !knot_span_is_degenerate(denom1) {
899 k * lower[i] / denom1
900 } else {
901 0.0
902 };
903 let term2 = if !knot_span_is_degenerate(denom2) {
904 k * lower[i + 1] / denom2
905 } else {
906 0.0
907 };
908 out[i] = term1 - term2;
909 }
910
911 Ok(())
912}
913
914pub fn evaluate_bsplinethird_derivative_scalar(
923 x: f64,
924 knot_vector: ArrayView1<f64>,
925 degree: usize,
926 out: &mut [f64],
927) -> Result<(), BasisError> {
928 let mut workspace = BsplineDerivativeWorkspace::new();
929 evaluate_bspline_derivative_recurrence_into(3, x, knot_vector, degree, out, &mut workspace, 0)
930}
931
932pub fn evaluate_bspline_fourth_derivative_scalar(
941 x: f64,
942 knot_vector: ArrayView1<f64>,
943 degree: usize,
944 out: &mut [f64],
945) -> Result<(), BasisError> {
946 let mut workspace = BsplineDerivativeWorkspace::new();
947 evaluate_bspline_derivative_recurrence_into(4, x, knot_vector, degree, out, &mut workspace, 0)
948}
949
950#[cfg(test)]
968mod ispline_exterior_derivative_2695_tests {
969 use super::*;
970
971 fn clamped_knots() -> Array1<f64> {
974 Array1::from_vec(vec![
975 -3.0, -3.0, -3.0, -3.0, -1.5, 0.0, 1.5, 3.0, 3.0, 3.0, 3.0,
976 ])
977 }
978
979 const DEGREE: usize = 2;
981
982 fn value_row(x: f64) -> Vec<f64> {
983 let knots = clamped_knots();
984 let data = Array1::from_vec(vec![x]);
985 create_ispline_dense(data.view(), knots.view(), DEGREE)
986 .expect("i-spline value")
987 .row(0)
988 .to_vec()
989 }
990
991 fn derivative_row(x: f64, order: usize) -> Vec<f64> {
992 let knots = clamped_knots();
993 let data = Array1::from_vec(vec![x]);
994 create_ispline_derivative_dense(data.view(), &knots, DEGREE, order)
995 .expect("i-spline derivative")
996 .row(0)
997 .to_vec()
998 }
999
1000 #[test]
1003 fn the_ispline_value_is_constant_outside_the_modelling_interval() {
1004 for (a, b) in [(-4.0, -8.0), (4.0, 9.0)] {
1005 let left = value_row(a);
1006 let right = value_row(b);
1007 assert_eq!(
1008 left.len(),
1009 right.len(),
1010 "the basis width must not depend on the evaluation point"
1011 );
1012 for (j, (lo, hi)) in left.iter().zip(right.iter()).enumerate() {
1013 assert_eq!(
1014 lo.to_bits(),
1015 hi.to_bits(),
1016 "I_{j}({a}) = {lo} but I_{j}({b}) = {hi}; the I-spline value is \
1017 documented as saturating outside the knot domain"
1018 );
1019 }
1020 }
1021 }
1022
1023 #[test]
1027 fn the_ispline_derivative_matches_a_finite_difference_inside_the_interval() {
1028 let x = 0.4_f64;
1029 let h = 1.0e-5;
1030 let plus = value_row(x + h);
1031 let minus = value_row(x - h);
1032 let analytic = derivative_row(x, 1);
1033 let mut any_nonzero = false;
1034 for (j, value) in analytic.iter().enumerate() {
1035 let fd = (plus[j] - minus[j]) / (2.0 * h);
1036 assert!(
1037 (fd - value).abs() <= 1.0e-6 * (1.0 + value.abs()),
1038 "interior column {j}: analytic I'_{j}({x}) = {value:.9e} but the central \
1039 difference of the value is {fd:.9e}"
1040 );
1041 any_nonzero |= value.abs() > 1.0e-6;
1042 }
1043 assert!(
1044 any_nonzero,
1045 "the interior control must exercise a non-zero derivative"
1046 );
1047 }
1048
1049 #[test]
1051 fn the_ispline_derivative_is_zero_where_its_value_saturates() {
1052 for x in [-4.0_f64, -3.5, 3.5, 4.0, 12.0] {
1053 for order in 1..=4 {
1054 for (j, value) in derivative_row(x, order).iter().enumerate() {
1055 assert_eq!(
1056 *value, 0.0,
1057 "order-{order} I-spline derivative at x={x} (outside the knot domain \
1058 [-3, 3], where the value is constant) reports {value:.9e} in column \
1059 {j}; a constant function has zero derivative"
1060 );
1061 }
1062 }
1063 }
1064 }
1065
1066 #[test]
1071 fn the_monotone_warp_multiplier_is_one_where_the_warp_is_flat() {
1072 let beta_w = [0.30_f64, 0.40, 0.50, 0.60, 0.70, 0.80];
1073 for x in [-5.0_f64, 5.0] {
1074 let d1 = derivative_row(x, 1);
1075 assert_eq!(
1076 d1.len(),
1077 beta_w.len(),
1078 "fixture coefficient width must match the basis"
1079 );
1080 let m1: f64 = 1.0 + d1.iter().zip(beta_w.iter()).map(|(b, c)| b * c).sum::<f64>();
1081 assert_eq!(
1082 m1, 1.0,
1083 "at x={x} the warp value is constant, so its multiplier must be exactly 1"
1084 );
1085 }
1086 }
1087}