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