1use super::*;
2
3pub struct SplineScratch {
5 pub(crate) inner: internal::BsplineScratch,
6 pub(crate) local: Vec<f64>,
7 pub(crate) left_inner: internal::BsplineScratch,
8 pub(crate) left_local: Vec<f64>,
9 pub(crate) left_offsets: Vec<f64>,
10}
11
12impl SplineScratch {
13 pub fn new(degree: usize) -> Self {
14 Self {
15 inner: internal::BsplineScratch::new(degree),
16 local: Vec::new(),
17 left_inner: internal::BsplineScratch::new(degree),
18 left_local: Vec::new(),
19 left_offsets: Vec::new(),
20 }
21 }
22}
23
24pub fn evaluate_bspline_basis_scalar(
28 x: f64,
29 knot_vector: ArrayView1<f64>,
30 degree: usize,
31 out: &mut [f64],
32 scratch: &mut SplineScratch,
33) -> Result<(), BasisError> {
34 validate_knots_for_degree(knot_vector, degree)?;
35
36 let num_basis = knot_vector.len() - degree - 1;
37 if out.len() != num_basis {
38 return Err(BasisError::InvalidKnotVector(format!(
39 "Output buffer length {} does not match number of basis functions {}",
40 out.len(),
41 num_basis
42 )));
43 }
44
45 internal::evaluate_splines_at_point_into(x, degree, knot_vector, out, &mut scratch.inner);
46
47 Ok(())
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct PeriodicBSplineBasisSpec {
59 pub degree: usize,
61 pub num_basis: usize,
63 pub period: f64,
65 pub origin: f64,
67 pub penalty_order: usize,
70}
71
72impl PeriodicBSplineBasisSpec {
73 pub fn new(
76 degree: usize,
77 num_basis: usize,
78 period: f64,
79 origin: f64,
80 penalty_order: usize,
81 ) -> Self {
82 Self {
83 degree,
84 num_basis,
85 period,
86 origin,
87 penalty_order,
88 }
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct PeriodicSplineCurve {
100 pub spec: PeriodicBSplineBasisSpec,
101 pub coefficients: Array2<f64>,
102}
103
104impl PeriodicSplineCurve {
105 pub fn ambient_dim(&self) -> usize {
107 self.coefficients.ncols()
108 }
109
110 pub fn evaluate(&self, u: ArrayView1<'_, f64>) -> Result<Array2<f64>, BasisError> {
113 if self.coefficients.nrows() != self.spec.num_basis {
114 crate::bail_dim_basis!(
115 "curve coefficient rows ({}) must equal periodic basis size ({})",
116 self.coefficients.nrows(),
117 self.spec.num_basis
118 );
119 }
120 let basis = build_periodic_bspline_basis_1d(u, &self.spec)?;
121 Ok(basis.dot(&self.coefficients))
122 }
123
124 pub fn evaluate_derivative(&self, u: ArrayView1<'_, f64>) -> Result<Array2<f64>, BasisError> {
127 if self.coefficients.nrows() != self.spec.num_basis {
128 crate::bail_dim_basis!(
129 "curve coefficient rows ({}) must equal periodic basis size ({})",
130 self.coefficients.nrows(),
131 self.spec.num_basis
132 );
133 }
134 let t = u.to_owned().insert_axis(Axis(1));
135 let derivative = periodic_bspline_first_derivative_nd(
136 t.view(),
137 (self.spec.origin, self.spec.origin + self.spec.period),
138 self.spec.degree,
139 self.spec.num_basis,
140 )?
141 .index_axis(Axis(2), 0)
142 .to_owned();
143 Ok(derivative.dot(&self.coefficients))
144 }
145}
146
147pub(crate) fn validate_periodic_bspline_spec(
148 spec: &PeriodicBSplineBasisSpec,
149) -> Result<(), BasisError> {
150 if spec.degree < 1 {
151 return Err(BasisError::InvalidDegree(spec.degree));
152 }
153 if spec.num_basis < spec.degree + 1 {
154 crate::bail_invalid_basis!(
155 "periodic B-spline basis requires num_basis >= degree + 1 (got num_basis={}, degree={})",
156 spec.num_basis,
157 spec.degree
158 );
159 }
160 if !spec.period.is_finite() || spec.period <= 0.0 {
161 crate::bail_invalid_basis!(
162 "periodic B-spline period must be finite and positive, got {}",
163 spec.period
164 );
165 }
166 if !spec.origin.is_finite() {
167 crate::bail_invalid_basis!(
168 "periodic B-spline origin must be finite, got {}",
169 spec.origin
170 );
171 }
172 if spec.penalty_order == 0 || spec.penalty_order >= spec.num_basis {
173 return Err(BasisError::InvalidPenaltyOrder {
174 order: spec.penalty_order,
175 num_basis: spec.num_basis,
176 });
177 }
178 if spec.penalty_order > spec.degree {
179 return Err(BasisError::InsufficientDegreeForDerivative {
180 degree: spec.degree,
181 derivative_order: spec.penalty_order,
182 minimum_degree: spec.penalty_order,
183 });
184 }
185 Ok(())
186}
187
188#[inline]
189pub(crate) fn wrap_periodic_phase(u: f64, origin: f64, period: f64) -> f64 {
190 let wrapped = (u - origin).rem_euclid(period);
191 if wrapped >= period { 0.0 } else { wrapped }
194}
195
196pub(crate) fn cardinal_bspline_value(x: f64, degree: usize) -> f64 {
197 if degree == 0 {
198 return if (0.0..1.0).contains(&x) { 1.0 } else { 0.0 };
199 }
200 if x <= 0.0 || x >= (degree + 1) as f64 {
201 return 0.0;
202 }
203 let p = degree as f64;
204 (x / p) * cardinal_bspline_value(x, degree - 1)
205 + (((degree + 1) as f64 - x) / p) * cardinal_bspline_value(x - 1.0, degree - 1)
206}
207
208pub(crate) fn fill_periodic_bspline_unnormalized_value_row(
209 u: f64,
210 origin: f64,
211 period: f64,
212 degree: usize,
213 row: &mut [f64],
214) -> f64 {
215 let m = row.len();
216 let m_f = m as f64;
217 let h = period / m_f;
218 let t = wrap_periodic_phase(u, origin, period) / h;
219 let mut rowsum = 0.0_f64;
220 for (col, value_slot) in row.iter_mut().enumerate() {
221 let base = t - col as f64;
222 let k_min = ((-base) / m_f).floor() as isize - 1;
223 let k_max = (((degree + 1) as f64 - base) / m_f).ceil() as isize + 1;
224 let mut value = 0.0_f64;
225 for k in k_min..=k_max {
226 value += cardinal_bspline_value(base + (k as f64) * m_f, degree);
227 }
228 *value_slot = value;
229 rowsum += value;
230 }
231 rowsum
232}
233
234pub(crate) fn fill_periodic_bspline_unnormalized_derivative_row(
235 u: f64,
236 origin: f64,
237 period: f64,
238 degree: usize,
239 row: &mut [f64],
240) -> f64 {
241 let m = row.len();
242 let m_f = m as f64;
243 let h = period / m_f;
244 let tau = wrap_periodic_phase(u, origin, period) / h;
245 let mut rowsum_derivative = 0.0_f64;
246 for (col, value_slot) in row.iter_mut().enumerate() {
247 let base = tau - col as f64;
248 let k_min = ((-base) / m_f).floor() as isize - 1;
249 let k_max = (((degree + 1) as f64 - base) / m_f).ceil() as isize + 1;
250 let mut value = 0.0_f64;
251 for k in k_min..=k_max {
252 let x_arg = base + (k as f64) * m_f;
253 value += cardinal_bspline_value(x_arg, degree - 1)
254 - cardinal_bspline_value(x_arg - 1.0, degree - 1);
255 }
256 let derivative = value / h;
257 *value_slot = derivative;
258 rowsum_derivative += derivative;
259 }
260 rowsum_derivative
261}
262
263pub fn build_periodic_bspline_basis_1d(
271 u: ArrayView1<'_, f64>,
272 spec: &PeriodicBSplineBasisSpec,
273) -> Result<Array2<f64>, BasisError> {
274 validate_periodic_bspline_spec(spec)?;
275 if u.iter().any(|v| !v.is_finite()) {
276 crate::bail_invalid_basis!("periodic B-spline inputs must all be finite");
277 }
278
279 let n = u.len();
280 let m = spec.num_basis;
281 let mut out = Array2::<f64>::zeros((n, m));
282 let mut value_row = vec![0.0_f64; m];
283 for (row_idx, &ui) in u.iter().enumerate() {
284 let rowsum = fill_periodic_bspline_unnormalized_value_row(
285 ui,
286 spec.origin,
287 spec.period,
288 spec.degree,
289 &mut value_row,
290 );
291 if !rowsum.is_finite() || rowsum <= 0.0 {
292 crate::bail_invalid_basis!(
293 "periodic B-spline row has non-positive rowsum at row {row_idx}: {rowsum}"
294 );
295 }
296 for col in 0..m {
297 out[[row_idx, col]] = value_row[col] / rowsum;
298 }
299 }
300 Ok(out)
301}
302
303fn distinct_periodic_phase_count(u: ArrayView1<'_, f64>, origin: f64, period: f64) -> usize {
304 let mut phases = u
305 .iter()
306 .map(|&value| wrap_periodic_phase(value, origin, period))
307 .collect::<Vec<_>>();
308 phases.sort_by(f64::total_cmp);
309 let tol = 1.0e-12 * period.abs().max(1.0);
310 let mut count = 0usize;
311 let mut previous: Option<f64> = None;
312 for phase in phases {
313 if previous
314 .map(|prev| (phase - prev).abs() <= tol)
315 .unwrap_or(false)
316 {
317 continue;
318 }
319 count += 1;
320 previous = Some(phase);
321 }
322 count
323}
324
325pub(crate) fn solve_spd_cholesky(
326 a: Array2<f64>,
327 b: &Array2<f64>,
328) -> Result<Array2<f64>, BasisError> {
329 let n = a.nrows();
330 if a.ncols() != n || b.nrows() != n {
331 crate::bail_dim_basis!(
332 "normal-equation solve shape mismatch: A is {}x{}, B is {}x{}",
333 a.nrows(),
334 a.ncols(),
335 b.nrows(),
336 b.ncols()
337 );
338 }
339 let mut jitter = 0.0_f64;
340 for attempt in 0..8 {
341 let mut l = a.clone();
342 if jitter > 0.0 {
343 for i in 0..n {
344 l[[i, i]] += jitter;
345 }
346 }
347 let mut ok = true;
348 for i in 0..n {
349 for j in 0..=i {
350 let mut sum = l[[i, j]];
351 for k in 0..j {
352 sum -= l[[i, k]] * l[[j, k]];
353 }
354 if i == j {
355 if sum <= 0.0 || !sum.is_finite() {
356 ok = false;
357 break;
358 }
359 l[[i, j]] = sum.sqrt();
360 } else {
361 l[[i, j]] = sum / l[[j, j]];
362 }
363 }
364 if !ok {
365 break;
366 }
367 for j in (i + 1)..n {
368 l[[i, j]] = 0.0;
369 }
370 }
371 if ok {
372 let mut y = Array2::<f64>::zeros(b.raw_dim());
373 for i in 0..n {
374 for rhs in 0..b.ncols() {
375 let mut sum = b[[i, rhs]];
376 for k in 0..i {
377 sum -= l[[i, k]] * y[[k, rhs]];
378 }
379 y[[i, rhs]] = sum / l[[i, i]];
380 }
381 }
382 let mut x = Array2::<f64>::zeros(b.raw_dim());
383 for i_rev in 0..n {
384 let i = n - 1 - i_rev;
385 for rhs in 0..b.ncols() {
386 let mut sum = y[[i, rhs]];
387 for k in (i + 1)..n {
388 sum -= l[[k, i]] * x[[k, rhs]];
389 }
390 x[[i, rhs]] = sum / l[[i, i]];
391 }
392 }
393 return Ok(x);
394 }
395 let diag_scale = (0..n)
396 .map(|i| a[[i, i]].abs())
397 .fold(0.0_f64, f64::max)
398 .max(1.0);
399 jitter = if attempt == 0 {
400 1e-12 * diag_scale
401 } else {
402 jitter * 10.0
403 };
404 }
405 Err(BasisError::InvalidInput(
406 "periodic spline normal equations were not positive definite even after jitter".to_string(),
407 ))
408}
409
410pub fn fit_periodic_bspline_curve(
418 u: ArrayView1<'_, f64>,
419 y: ArrayView2<'_, f64>,
420 spec: &PeriodicBSplineBasisSpec,
421 smoothing_lambda: f64,
422) -> Result<PeriodicSplineCurve, BasisError> {
423 validate_periodic_bspline_spec(spec)?;
424 if y.nrows() != u.len() {
425 crate::bail_dim_basis!(
426 "periodic curve fit requires y rows ({}) to match u length ({})",
427 y.nrows(),
428 u.len()
429 );
430 }
431 if y.ncols() == 0 {
432 crate::bail_invalid_basis!(
433 "periodic curve fit requires at least one ambient output column"
434 );
435 }
436 if !smoothing_lambda.is_finite() || smoothing_lambda < 0.0 {
437 crate::bail_invalid_basis!(
438 "smoothing_lambda must be finite and nonnegative, got {smoothing_lambda}"
439 );
440 }
441 if y.iter().any(|v| !v.is_finite()) {
442 crate::bail_invalid_basis!("periodic curve outputs must all be finite");
443 }
444 let distinct_phases = distinct_periodic_phase_count(u, spec.origin, spec.period);
445 if distinct_phases < spec.num_basis {
446 crate::bail_invalid_basis!(
447 "periodic curve fit needs at least {} distinct wrapped sample positions for {} basis functions; got {}",
448 spec.num_basis,
449 spec.num_basis,
450 distinct_phases
451 );
452 }
453
454 let basis = build_periodic_bspline_basis_1d(u, spec)?;
455 let mut lhs = basis.t().dot(&basis);
456 if smoothing_lambda > 0.0 {
457 let penalty = cyclic_bspline_derivative_penalty_matrix(
458 spec.degree,
459 spec.num_basis,
460 spec.period,
461 spec.penalty_order,
462 )?;
463 lhs = lhs + smoothing_lambda * penalty;
464 }
465 let rhs = basis.t().dot(&y);
466 let coefficients = solve_spd_cholesky(lhs, &rhs)?;
467 Ok(PeriodicSplineCurve {
468 spec: spec.clone(),
469 coefficients,
470 })
471}
472
473pub fn evaluate_mspline_scalar(
480 x: f64,
481 knot_vector: ArrayView1<f64>,
482 degree: usize,
483 out: &mut [f64],
484 scratch: &mut SplineScratch,
485) -> Result<(), BasisError> {
486 validate_knots_for_degree(knot_vector, degree)?;
487 validate_mspline_normalization_spans(knot_vector, degree)?;
488 let num_basis = knot_vector.len() - degree - 1;
489 if out.len() != num_basis {
490 crate::bail_dim_basis!(
491 "M-spline output buffer length {} does not match basis size {}",
492 out.len(),
493 num_basis
494 );
495 }
496
497 let left = knot_vector[degree];
498 let right = knot_vector[num_basis];
499 if x < left || x > right {
500 out.fill(0.0);
501 return Ok(());
502 }
503
504 out.fill(0.0);
507 if scratch.local.len() < degree + 1 {
508 scratch.local.resize(degree + 1, 0.0);
509 }
510 let local = &mut scratch.local[..degree + 1];
511 local.fill(0.0);
512 let start =
513 internal::evaluate_splines_sparse_into(x, degree, knot_vector, local, &mut scratch.inner);
514 let order = (degree + 1) as f64;
515 for (offset, &b) in local.iter().enumerate() {
516 let i = start + offset;
517 if i >= num_basis {
518 continue;
519 }
520 let span = knot_vector[i + degree + 1] - knot_vector[i];
521 out[i] = b * (order / span);
522 }
523 Ok(())
524}
525
526pub fn evaluate_ispline_scalarwith_scratch(
535 x: f64,
536 knot_vector: ArrayView1<f64>,
537 degree: usize,
538 out: &mut [f64],
539 scratch: &mut SplineScratch,
540) -> Result<(), BasisError> {
541 let bs_degree = degree
542 .checked_add(1)
543 .ok_or_else(|| BasisError::InvalidInput("I-spline degree overflow".to_string()))?;
544 validate_knots_for_degree(knot_vector, bs_degree)?;
545 let num_bspline_basis = knot_vector.len() - bs_degree - 1;
546 let num_ispline_basis = num_bspline_basis.saturating_sub(1);
547 if out.len() != num_ispline_basis {
548 crate::bail_dim_basis!(
549 "I-spline output buffer length {} does not match basis size {}",
550 out.len(),
551 num_ispline_basis
552 );
553 }
554
555 let left = knot_vector[bs_degree];
557 let right = knot_vector[num_bspline_basis];
558 let support = bs_degree + 1;
559 if x < left {
560 out.fill(0.0);
561 return Ok(());
562 }
563 if x >= right {
564 if scratch.left_local.len() < support {
565 scratch.left_local.resize(support, 0.0);
566 }
567 if scratch.left_offsets.len() < num_bspline_basis {
568 scratch.left_offsets.resize(num_bspline_basis, 0.0);
569 }
570 scratch.left_offsets[..num_bspline_basis].fill(0.0);
571 let left_local = &mut scratch.left_local[..support];
572 left_local.fill(0.0);
573 scratch.left_inner.ensure_degree(bs_degree);
574 let left_offsets = &mut scratch.left_offsets[..num_bspline_basis];
575 internal::cumulative_bspline_offsets_into(
576 left,
577 bs_degree,
578 knot_vector,
579 left_local,
580 &mut scratch.left_inner,
581 left_offsets,
582 );
583 for j in 1..num_bspline_basis {
584 let value = 1.0 - left_offsets[j];
585 out[j - 1] = if value.abs() <= 1e-15 { 0.0 } else { value };
586 }
587 return Ok(());
588 }
589
590 out.fill(0.0);
596 if scratch.local.len() < support {
597 scratch.local.resize(support, 0.0);
598 }
599 scratch.local[..support].fill(0.0);
600 scratch.inner.ensure_degree(bs_degree);
601 let local = &mut scratch.local[..support];
602 let start = internal::evaluate_splines_sparse_into(
603 x,
604 bs_degree,
605 knot_vector,
606 local,
607 &mut scratch.inner,
608 );
609
610 let total = local.iter().copied().sum::<f64>();
611 let lead_end = start.min(num_bspline_basis);
612 if lead_end > 1 {
613 out[..(lead_end - 1)].fill(total);
614 }
615
616 let mut running = 0.0f64;
617 for offset in (0..support).rev() {
618 let j = start + offset;
619 if j >= num_bspline_basis {
620 continue;
621 }
622 running += local[offset];
623 if j > 0 {
624 out[j - 1] = running;
625 }
626 }
627
628 if scratch.left_local.len() < support {
630 scratch.left_local.resize(support, 0.0);
631 }
632 if scratch.left_offsets.len() < num_bspline_basis {
633 scratch.left_offsets.resize(num_bspline_basis, 0.0);
634 }
635 scratch.left_offsets[..num_bspline_basis].fill(0.0);
636 let left_local = &mut scratch.left_local[..support];
637 left_local.fill(0.0);
638 scratch.left_inner.ensure_degree(bs_degree);
639 let left_offsets = &mut scratch.left_offsets[..num_bspline_basis];
640 internal::cumulative_bspline_offsets_into(
641 left,
642 bs_degree,
643 knot_vector,
644 left_local,
645 &mut scratch.left_inner,
646 left_offsets,
647 );
648 for j in 1..num_bspline_basis {
649 let out_idx = j - 1;
650 out[out_idx] -= left_offsets[j];
651 if out[out_idx].abs() <= 1e-15 {
652 out[out_idx] = 0.0;
653 }
654 }
655 Ok(())
656}
657
658pub fn create_ispline_derivative_dense(
667 data: ArrayView1<'_, f64>,
668 knot_vector: &Array1<f64>,
669 degree: usize,
670 derivative_order: usize,
671) -> Result<Array2<f64>, BasisError> {
672 if derivative_order == 0 {
673 let (basis_arc, _) = create_basis::<Dense>(
675 data,
676 KnotSource::Provided(knot_vector.view()),
677 degree,
678 BasisOptions::i_spline(),
679 )?;
680 return Ok(basis_arc.as_ref().clone());
681 }
682 let bs_degree = degree
683 .checked_add(1)
684 .ok_or_else(|| BasisError::InvalidInput("I-spline degree overflow".to_string()))?;
685 if derivative_order > bs_degree {
686 let num_bspline_basis = knot_vector.len().saturating_sub(bs_degree + 1);
688 let num_ispline_basis = num_bspline_basis.saturating_sub(1);
689 return Ok(Array2::zeros((data.len(), num_ispline_basis)));
690 }
691 let num_bspline_cols = knot_vector.len().saturating_sub(bs_degree + 1);
692 let db = match derivative_order {
693 1 => {
694 let (db_arc, _) = create_basis::<Dense>(
695 data,
696 KnotSource::Provided(knot_vector.view()),
697 bs_degree,
698 BasisOptions::first_derivative(),
699 )?;
700 db_arc.as_ref().clone()
701 }
702 2 => {
703 let (db_arc, _) = create_basis::<Dense>(
704 data,
705 KnotSource::Provided(knot_vector.view()),
706 bs_degree,
707 BasisOptions::second_derivative(),
708 )?;
709 db_arc.as_ref().clone()
710 }
711 3 => {
712 let mut db = Array2::<f64>::zeros((data.len(), num_bspline_cols));
713 for (row_idx, &x) in data.iter().enumerate() {
714 let row = db.slice_mut(s![row_idx, ..]).into_slice().ok_or_else(|| {
715 BasisError::InvalidInput(
716 "I-spline derivative row is not contiguous".to_string(),
717 )
718 })?;
719 evaluate_bsplinethird_derivative_scalar(x, knot_vector.view(), bs_degree, row)?;
720 }
721 db
722 }
723 4 => {
724 let mut db = Array2::<f64>::zeros((data.len(), num_bspline_cols));
725 for (row_idx, &x) in data.iter().enumerate() {
726 let row = db.slice_mut(s![row_idx, ..]).into_slice().ok_or_else(|| {
727 BasisError::InvalidInput(
728 "I-spline derivative row is not contiguous".to_string(),
729 )
730 })?;
731 evaluate_bspline_fourth_derivative_scalar(x, knot_vector.view(), bs_degree, row)?;
732 }
733 db
734 }
735 other => {
736 crate::bail_invalid_basis!(
737 "I-spline derivative supports orders 1..=4; got order={other}"
738 );
739 }
740 };
741 let num_ispline_cols = num_bspline_cols.saturating_sub(1);
742 if num_ispline_cols == 0 {
743 return Ok(Array2::zeros((data.len(), 0)));
744 }
745 let mut out = Array2::<f64>::zeros((data.len(), num_ispline_cols));
748 for i in 0..data.len() {
749 let mut running = 0.0_f64;
750 for j in (1..num_bspline_cols).rev() {
751 let term = db[[i, j]];
752 if term.is_finite() {
753 running += term;
754 }
755 out[[i, j - 1]] = running;
756 }
757 }
758 Ok(out)
759}
760
761pub fn evaluate_ispline_scalar(
762 x: f64,
763 knot_vector: ArrayView1<f64>,
764 degree: usize,
765 out: &mut [f64],
766) -> Result<(), BasisError> {
767 let bs_degree = degree
768 .checked_add(1)
769 .ok_or_else(|| BasisError::InvalidInput("I-spline degree overflow".to_string()))?;
770 let mut scratch = SplineScratch::new(bs_degree);
771 evaluate_ispline_scalarwith_scratch(x, knot_vector, degree, out, &mut scratch)
772}
773
774pub fn evaluate_bspline_derivative_scalar(
786 x: f64,
787 knot_vector: ArrayView1<f64>,
788 degree: usize,
789 out: &mut [f64],
790) -> Result<(), BasisError> {
791 if degree < 1 {
792 return Err(BasisError::InvalidDegree(degree));
793 }
794 let num_basis_lower = knot_vector.len().saturating_sub(degree);
795 let mut lower_basis = vec![0.0; num_basis_lower];
796 let mut lower_scratch = internal::BsplineScratch::new(degree.saturating_sub(1));
797 evaluate_bspline_derivative_scalar_into(
798 x,
799 knot_vector,
800 degree,
801 out,
802 &mut lower_basis,
803 &mut lower_scratch,
804 )
805}
806
807pub fn evaluate_bspline_derivative_scalar_into(
811 x: f64,
812 knot_vector: ArrayView1<f64>,
813 degree: usize,
814 out: &mut [f64],
815 lower_basis: &mut [f64],
816 lower_scratch: &mut internal::BsplineScratch,
817) -> Result<(), BasisError> {
818 validate_knots_for_degree(knot_vector, degree)?;
819
820 let num_basis = knot_vector.len() - degree - 1;
821 if out.len() != num_basis {
822 return Err(BasisError::InvalidKnotVector(format!(
823 "Output buffer length {} does not match number of basis functions {}",
824 out.len(),
825 num_basis
826 )));
827 }
828
829 let num_basis_lower = knot_vector.len() - degree;
830 if lower_basis.len() < num_basis_lower {
831 return Err(BasisError::InvalidKnotVector(format!(
832 "lower_basis buffer too small: {} < {}",
833 lower_basis.len(),
834 num_basis_lower
835 )));
836 }
837
838 for v in lower_basis.iter_mut().take(num_basis_lower) {
840 *v = 0.0;
841 }
842
843 if open_knot_derivative_exterior_is_zero(x, knot_vector, degree) {
856 out.fill(0.0);
857 return Ok(());
858 }
859 let x_clamped = clamp_eval_point_to_modeling_interval(x, knot_vector, degree);
860 let x_eval = one_sided_derivative_eval_point(x_clamped, knot_vector, degree);
861
862 internal::evaluate_splines_at_point_full_support_into(
864 x_eval,
865 degree - 1,
866 knot_vector,
867 &mut lower_basis[..num_basis_lower],
868 lower_scratch,
869 );
870
871 let k = degree as f64;
873 for i in 0..num_basis {
874 let denom_left = knot_vector[i + degree] - knot_vector[i];
875 let denom_right = knot_vector[i + degree + 1] - knot_vector[i + 1];
876
877 let left_term = if !knot_span_is_degenerate(denom_left) && i < num_basis_lower {
878 lower_basis[i] / denom_left
879 } else {
880 0.0
881 };
882
883 let right_term = if !knot_span_is_degenerate(denom_right) && (i + 1) < num_basis_lower {
884 lower_basis[i + 1] / denom_right
885 } else {
886 0.0
887 };
888
889 out[i] = k * (left_term - right_term);
890 }
891
892 Ok(())
893}
894
895fn mspline_scales(knot_vector: ArrayView1<f64>, degree: usize, num_basis: usize) -> Vec<f64> {
901 let order = (degree + 1) as f64;
902 (0..num_basis)
903 .map(|i| order / (knot_vector[i + degree + 1] - knot_vector[i]))
904 .collect()
905}
906
907pub(crate) fn create_mspline_dense(
908 data: ArrayView1<f64>,
909 knot_vector: ArrayView1<f64>,
910 degree: usize,
911) -> Result<Array2<f64>, BasisError> {
912 validate_knots_for_degree(knot_vector, degree)?;
913 validate_mspline_normalization_spans(knot_vector, degree)?;
914 let num_basis = knot_vector.len() - degree - 1;
915 let mut out = Array2::<f64>::zeros((data.len(), num_basis));
916 let mut scratch = internal::BsplineScratch::new(degree);
917 let support = degree + 1;
918 let mut local = vec![0.0; support];
919 let left = knot_vector[degree];
920 let right = knot_vector[num_basis];
921 let scales = mspline_scales(knot_vector, degree, num_basis);
922
923 for (row_i, &x) in data.iter().enumerate() {
924 if x < left || x > right {
925 continue;
926 }
927 let start = internal::evaluate_splines_sparse_into(
928 x,
929 degree,
930 knot_vector,
931 &mut local,
932 &mut scratch,
933 );
934 for (offset, &b) in local.iter().enumerate() {
935 let j = start + offset;
936 if j < num_basis {
937 out[[row_i, j]] = b * scales[j];
938 }
939 }
940 }
941 Ok(out)
942}
943
944pub(crate) fn create_mspline_sparse(
945 data: ArrayView1<f64>,
946 knot_vector: ArrayView1<f64>,
947 degree: usize,
948) -> Result<SparseColMat<usize, f64>, BasisError> {
949 validate_knots_for_degree(knot_vector, degree)?;
950 validate_mspline_normalization_spans(knot_vector, degree)?;
951 let nrows = data.len();
952 let ncols = knot_vector.len() - degree - 1;
953 let mut scratch = internal::BsplineScratch::new(degree);
954 let support = degree + 1;
955 let mut local = vec![0.0; support];
956 let left = knot_vector[degree];
957 let right = knot_vector[ncols];
958 let scales = mspline_scales(knot_vector, degree, ncols);
959
960 let mut triplets: Vec<Triplet<usize, usize, f64>> =
961 Vec::with_capacity(nrows.saturating_mul(support));
962 for (row_i, &x) in data.iter().enumerate() {
963 if x < left || x > right {
964 continue;
965 }
966 let start = internal::evaluate_splines_sparse_into(
967 x,
968 degree,
969 knot_vector,
970 &mut local,
971 &mut scratch,
972 );
973 for (offset, &b) in local.iter().enumerate() {
974 let col = start + offset;
975 if col >= ncols {
976 continue;
977 }
978 let v = b * scales[col];
979 if v.abs() > 0.0 {
980 triplets.push(Triplet::new(row_i, col, v));
981 }
982 }
983 }
984
985 SparseColMat::try_new_from_triplets(nrows, ncols, &triplets)
986 .map_err(|e| BasisError::SparseCreation(format!("{e:?}")))
987}
988
989pub(crate) fn validate_mspline_normalization_spans(
990 knot_vector: ArrayView1<f64>,
991 degree: usize,
992) -> Result<(), BasisError> {
993 let num_basis = knot_vector.len().saturating_sub(degree + 1);
994 for i in 0..num_basis {
995 let span = knot_vector[i + degree + 1] - knot_vector[i];
996 if span <= 0.0 {
997 crate::bail_invalid_basis!(
998 "invalid M-spline normalization span at i={i}: t[i+degree+1]-t[i]={span:.3e} must be > 0"
999 );
1000 }
1001 }
1002 Ok(())
1003}
1004
1005pub(crate) fn create_ispline_dense(
1006 data: ArrayView1<f64>,
1007 knot_vector: ArrayView1<f64>,
1008 degree: usize,
1009) -> Result<Array2<f64>, BasisError> {
1010 let bs_degree = degree
1011 .checked_add(1)
1012 .ok_or_else(|| BasisError::InvalidInput("I-spline degree overflow".to_string()))?;
1013 validate_knots_for_degree(knot_vector, bs_degree)?;
1014 let num_bspline_basis = knot_vector.len() - bs_degree - 1;
1015 let num_ispline_basis = num_bspline_basis.saturating_sub(1);
1016 let mut out = Array2::<f64>::zeros((data.len(), num_ispline_basis));
1017 let mut scratch = internal::BsplineScratch::new(bs_degree);
1018 let support = bs_degree + 1;
1019 let mut local = vec![0.0; support];
1020 let left = knot_vector[bs_degree];
1021 let right = knot_vector[num_bspline_basis];
1022
1023 let mut left_local = vec![0.0_f64; support];
1025 let mut left_scratch = internal::BsplineScratch::new(bs_degree);
1026 let mut left_offsets = vec![0.0_f64; num_bspline_basis];
1027 internal::cumulative_bspline_offsets_into(
1028 left,
1029 bs_degree,
1030 knot_vector,
1031 &mut left_local,
1032 &mut left_scratch,
1033 &mut left_offsets,
1034 );
1035
1036 for (row_i, &x) in data.iter().enumerate() {
1049 if x < left {
1050 continue;
1052 }
1053 if x >= right {
1054 for j in 1..num_bspline_basis {
1055 let value = 1.0 - left_offsets[j];
1056 out[[row_i, j - 1]] = if value.abs() <= 1e-15 { 0.0 } else { value };
1057 }
1058 continue;
1059 }
1060 let start = internal::evaluate_splines_sparse_into(
1061 x,
1062 bs_degree,
1063 knot_vector,
1064 &mut local,
1065 &mut scratch,
1066 );
1067 let total = local.iter().copied().sum::<f64>();
1068 let lead_end = start.min(num_bspline_basis);
1069 if lead_end > 1 {
1070 out.slice_mut(s![row_i, 0..(lead_end - 1)]).fill(total);
1071 }
1072 let mut running = 0.0f64;
1073 for offset in (0..support).rev() {
1074 let j = start + offset;
1075 if j >= num_bspline_basis {
1076 continue;
1077 }
1078 running += local[offset];
1079 if j > 0 {
1080 let value = running - left_offsets[j];
1081 out[[row_i, j - 1]] = if value.abs() <= 1e-15 { 0.0 } else { value };
1082 }
1083 }
1084 }
1085 Ok(out)
1086}
1087
1088#[derive(Default)]
1099pub struct BsplineDerivativeWorkspace {
1100 pub(crate) chain: Vec<Vec<f64>>,
1103 pub(crate) lower_basis: Vec<f64>,
1105 pub(crate) lower_scratch: internal::BsplineScratch,
1107}
1108
1109impl BsplineDerivativeWorkspace {
1110 #[inline]
1112 pub fn new() -> Self {
1113 Self::default()
1114 }
1115
1116 #[inline]
1119 pub(crate) fn chain_buffer(&mut self, depth: usize, len: usize) -> &mut [f64] {
1120 if self.chain.len() <= depth {
1121 self.chain.resize_with(depth + 1, Vec::new);
1122 }
1123 let buf = &mut self.chain[depth];
1124 if buf.len() != len {
1125 buf.resize(len, 0.0);
1126 }
1127 for v in buf.iter_mut() {
1128 *v = 0.0;
1129 }
1130 buf
1131 }
1132}
1133
1134pub(crate) fn evaluate_bspline_derivative_recurrence_into(
1152 derivative_order: usize,
1153 x: f64,
1154 knot_vector: ArrayView1<f64>,
1155 degree: usize,
1156 out: &mut [f64],
1157 workspace: &mut BsplineDerivativeWorkspace,
1158 depth: usize,
1159) -> Result<(), BasisError> {
1160 if degree < derivative_order {
1161 return Err(BasisError::InsufficientDegreeForDerivative {
1162 degree,
1163 derivative_order,
1164 minimum_degree: derivative_order,
1165 });
1166 }
1167 if depth == 0
1182 && (open_knot_derivative_exterior_is_zero(x, knot_vector, degree)
1183 || linear_extension_higher_derivative_is_zero(x, knot_vector, degree, derivative_order))
1184 {
1185 out.fill(0.0);
1186 return Ok(());
1187 }
1188 let x = if depth == 0 {
1189 clamp_eval_point_to_modeling_interval(x, knot_vector, degree)
1190 } else {
1191 x
1192 };
1193
1194 if derivative_order <= 1 {
1197 let num_basis_lower = knot_vector.len().saturating_sub(degree);
1198 if workspace.lower_basis.len() < num_basis_lower {
1199 workspace.lower_basis.resize(num_basis_lower, 0.0);
1200 }
1201 return evaluate_bspline_derivative_scalar_into(
1202 x,
1203 knot_vector,
1204 degree,
1205 out,
1206 &mut workspace.lower_basis,
1207 &mut workspace.lower_scratch,
1208 );
1209 }
1210
1211 validate_knots_for_degree(knot_vector, degree)?;
1212
1213 let num_basis = knot_vector.len() - degree - 1;
1214 if out.len() != num_basis {
1215 return Err(BasisError::InvalidKnotVector(format!(
1216 "Output buffer length {} does not match number of basis functions {}",
1217 out.len(),
1218 num_basis
1219 )));
1220 }
1221 let num_basis_lower = knot_vector.len() - degree;
1225
1226 workspace.chain_buffer(depth, num_basis_lower);
1230 let mut lower = std::mem::take(&mut workspace.chain[depth]);
1231
1232 let recurse = evaluate_bspline_derivative_recurrence_into(
1233 derivative_order - 1,
1234 x,
1235 knot_vector,
1236 degree - 1,
1237 &mut lower,
1238 workspace,
1239 depth + 1,
1240 );
1241 workspace.chain[depth] = lower;
1242 recurse?;
1243
1244 let lower = &workspace.chain[depth];
1245 let k = degree as f64;
1246 for i in 0..num_basis {
1247 let denom1 = knot_vector[i + degree] - knot_vector[i];
1248 let denom2 = knot_vector[i + degree + 1] - knot_vector[i + 1];
1249 let term1 = if !knot_span_is_degenerate(denom1) {
1250 k * lower[i] / denom1
1251 } else {
1252 0.0
1253 };
1254 let term2 = if !knot_span_is_degenerate(denom2) {
1255 k * lower[i + 1] / denom2
1256 } else {
1257 0.0
1258 };
1259 out[i] = term1 - term2;
1260 }
1261
1262 Ok(())
1263}
1264
1265pub fn evaluate_bsplinesecond_derivative_scalar(
1274 x: f64,
1275 knot_vector: ArrayView1<f64>,
1276 degree: usize,
1277 out: &mut [f64],
1278) -> Result<(), BasisError> {
1279 let mut workspace = BsplineDerivativeWorkspace::new();
1280 evaluate_bspline_derivative_recurrence_into(2, x, knot_vector, degree, out, &mut workspace, 0)
1281}
1282
1283pub fn evaluate_bsplinethird_derivative_scalar(
1292 x: f64,
1293 knot_vector: ArrayView1<f64>,
1294 degree: usize,
1295 out: &mut [f64],
1296) -> Result<(), BasisError> {
1297 let mut workspace = BsplineDerivativeWorkspace::new();
1298 evaluate_bspline_derivative_recurrence_into(3, x, knot_vector, degree, out, &mut workspace, 0)
1299}
1300
1301pub fn evaluate_bspline_fourth_derivative_scalar(
1310 x: f64,
1311 knot_vector: ArrayView1<f64>,
1312 degree: usize,
1313 out: &mut [f64],
1314) -> Result<(), BasisError> {
1315 let mut workspace = BsplineDerivativeWorkspace::new();
1316 evaluate_bspline_derivative_recurrence_into(4, x, knot_vector, degree, out, &mut workspace, 0)
1317}