1use super::*;
2
3#[inline]
11pub(crate) fn knot_span_is_degenerate(span: f64) -> bool {
12 span == 0.0
13}
14
15pub(crate) const DEFAULT_STREAMING_CHUNK_ROWS: usize = 2048;
20pub struct Dense;
22
23pub struct Sparse;
25
26pub trait BasisOutput {
28 type Output;
29}
30
31impl BasisOutput for Dense {
32 type Output = Arc<Array2<f64>>;
33}
34
35impl BasisOutput for Sparse {
36 type Output = SparseColMat<usize, f64>;
37}
38
39pub fn create_basis<O: BasisOutputFormat>(
55 data: ArrayView1<f64>,
56 knot_source: KnotSource<'_>,
57 degree: usize,
58 options: BasisOptions,
59) -> Result<(O::Output, Array1<f64>), BasisError> {
60 if degree < 1 {
61 return Err(BasisError::InvalidDegree(degree));
62 }
63
64 if options.basis_family != BasisFamily::BSpline && options.derivative_order != 0 {
65 crate::bail_invalid_basis!("derivatives are only supported for BasisFamily::BSpline");
66 }
67
68 let eval_kind = match options.derivative_order {
69 0 => BasisEvalKind::Basis,
70 1 => BasisEvalKind::FirstDerivative,
71 2 => BasisEvalKind::SecondDerivative,
72 n => {
73 crate::bail_invalid_basis!(
74 "unsupported derivative order {n}; only 0, 1, 2 are supported"
75 );
76 }
77 };
78
79 let knot_degree = match options.basis_family {
80 BasisFamily::BSpline | BasisFamily::MSpline => degree,
81 BasisFamily::ISpline => degree
82 .checked_add(1)
83 .ok_or_else(|| BasisError::InvalidInput("I-spline degree overflow".to_string()))?,
84 };
85
86 let knotvec: Array1<f64> = match knot_source {
87 KnotSource::Provided(view) => view.to_owned(),
88 KnotSource::Generate {
89 data_range,
90 num_internal_knots,
91 } => {
92 if data_range.0 > data_range.1 {
93 return Err(BasisError::InvalidRange(data_range.0, data_range.1));
94 }
95 if data_range.0 == data_range.1 {
96 return Err(BasisError::DegenerateRange(num_internal_knots));
97 }
98 internal::generate_full_knot_vector(data_range, num_internal_knots, knot_degree)?
99 }
100 };
101 validate_knots_for_degree(knotvec.view(), knot_degree)?;
102 validate_knot_spans_nondegenerate(knotvec.view(), knot_degree)?;
103
104 match options.basis_family {
105 BasisFamily::BSpline => O::build_basis(data, degree, eval_kind, knotvec),
106 BasisFamily::MSpline => {
107 if O::LAYOUT.is_sparse() {
108 let sparse = create_mspline_sparse(data, knotvec.view(), degree)?;
109 Ok((O::from_sparse(sparse)?, knotvec))
110 } else {
111 let dense = create_mspline_dense(data, knotvec.view(), degree)?;
112 Ok((O::from_dense(dense)?, knotvec))
113 }
114 }
115 BasisFamily::ISpline => {
116 if O::LAYOUT.is_sparse() {
117 crate::bail_invalid_basis!(
118 "BasisFamily::ISpline does not support sparse output; use Dense"
119 );
120 }
121 let dense = create_ispline_dense(data, knotvec.view(), degree)?;
122 Ok((O::from_dense(dense)?, knotvec))
123 }
124 }
125}
126
127pub fn apply_linear_extension_from_first_derivative(
134 z_raw: ArrayView1<f64>,
135 z_clamped: ArrayView1<f64>,
136 knot_vector: ArrayView1<f64>,
137 degree: usize,
138 basisvalues: &mut Array2<f64>,
139) -> Result<(), BasisError> {
140 if z_raw.len() != z_clamped.len() {
141 crate::bail_dim_basis!("z_raw and z_clamped must have equal length");
142 }
143 if basisvalues.nrows() != z_raw.len() {
144 crate::bail_dim_basis!("basis row count must match z length");
145 }
146
147 let mut needs_ext = false;
148 for i in 0..z_raw.len() {
149 if z_raw[i] != z_clamped[i] {
150 needs_ext = true;
151 break;
152 }
153 }
154 if !needs_ext {
155 return Ok(());
156 }
157
158 let (b_prime_arc, _) = create_basis::<Dense>(
159 z_clamped,
160 KnotSource::Provided(knot_vector),
161 degree,
162 BasisOptions::first_derivative(),
163 )?;
164 let b_prime = b_prime_arc.as_ref();
165 if b_prime.nrows() != basisvalues.nrows() || b_prime.ncols() != basisvalues.ncols() {
166 crate::bail_dim_basis!("basis derivative shape mismatch");
167 }
168
169 for i in 0..z_raw.len() {
170 let dz = z_raw[i] - z_clamped[i];
171 if dz == 0.0 {
172 continue;
173 }
174 for j in 0..basisvalues.ncols() {
175 basisvalues[[i, j]] += dz * b_prime[[i, j]];
176 }
177 }
178 Ok(())
179}
180
181#[derive(Debug, Clone, Copy, Eq, PartialEq)]
185pub enum BasisStorageLayout {
186 Dense,
187 Sparse,
188}
189
190impl BasisStorageLayout {
191 #[inline]
192 pub const fn is_sparse(self) -> bool {
193 matches!(self, Self::Sparse)
194 }
195}
196
197pub trait BasisOutputFormat {
200 type Output;
201 const LAYOUT: BasisStorageLayout;
202
203 fn build_basis(
204 data: ArrayView1<f64>,
205 degree: usize,
206 eval_kind: BasisEvalKind,
207 knotvec: Array1<f64>,
208 ) -> Result<(Self::Output, Array1<f64>), BasisError>;
209
210 fn from_dense(dense: Array2<f64>) -> Result<Self::Output, BasisError>;
211 fn from_sparse(sparse: SparseColMat<usize, f64>) -> Result<Self::Output, BasisError>;
212}
213
214impl BasisOutputFormat for Dense {
215 type Output = Arc<Array2<f64>>;
216 const LAYOUT: BasisStorageLayout = BasisStorageLayout::Dense;
217
218 fn build_basis(
219 data: ArrayView1<f64>,
220 degree: usize,
221 eval_kind: BasisEvalKind,
222 knotvec: Array1<f64>,
223 ) -> Result<(Self::Output, Array1<f64>), BasisError> {
224 let knotview = knotvec.view();
225
226 let num_basis_functions = knotview.len().saturating_sub(degree + 1);
227 let basis_matrix = if should_use_sparse_basis(num_basis_functions, degree, 1) {
228 let left = knotview[degree];
229 let right = knotview[num_basis_functions];
230 let data_clamped = data.mapv(|x| x.clamp(left, right));
231 let sparse = generate_basis_internal::<SparseStorage>(
232 data_clamped.view(),
233 knotview,
234 degree,
235 eval_kind,
236 )?;
237 let mut dense = Array2::<f64>::zeros((sparse.nrows(), sparse.ncols()));
238 let (symbolic, values) = sparse.parts();
239 let col_ptr = symbolic.col_ptr();
240 let row_idx = symbolic.row_idx();
241 for col in 0..sparse.ncols() {
242 let start = col_ptr[col];
243 let end = col_ptr[col + 1];
244 for idx in start..end {
245 dense[[row_idx[idx], col]] += values[idx];
246 }
247 }
248 apply_dense_bspline_extrapolation(data, knotview, degree, eval_kind, &mut dense)?;
249 dense
250 } else {
251 generate_basis_internal::<DenseStorage>(data.view(), knotview, degree, eval_kind)?
252 };
253
254 Ok((Arc::new(basis_matrix), knotvec))
255 }
256
257 fn from_dense(dense: Array2<f64>) -> Result<Self::Output, BasisError> {
258 Ok(Arc::new(dense))
259 }
260
261 fn from_sparse(sparse: SparseColMat<usize, f64>) -> Result<Self::Output, BasisError> {
262 let mut dense = Array2::<f64>::zeros((sparse.nrows(), sparse.ncols()));
263 let (symbolic, values) = sparse.parts();
264 let col_ptr = symbolic.col_ptr();
265 let row_idx = symbolic.row_idx();
266 for col in 0..sparse.ncols() {
267 let start = col_ptr[col];
268 let end = col_ptr[col + 1];
269 for idx in start..end {
270 dense[[row_idx[idx], col]] += values[idx];
271 }
272 }
273 Ok(Arc::new(dense))
274 }
275}
276
277pub(crate) fn apply_dense_bspline_extrapolation(
278 data: ArrayView1<f64>,
279 knotview: ArrayView1<f64>,
280 degree: usize,
281 eval_kind: BasisEvalKind,
282 basis_matrix: &mut Array2<f64>,
283) -> Result<(), BasisError> {
284 let num_basis_functions = basis_matrix.ncols();
285 if num_basis_functions == 0 {
286 return Ok(());
287 }
288
289 let left = knotview[degree];
290 let right = knotview[num_basis_functions];
291 if !(left.is_finite() && right.is_finite() && left < right) {
292 return Ok(());
293 }
294
295 if !has_clamped_bspline_boundaries(knotview, degree) {
305 if matches!(
306 eval_kind,
307 BasisEvalKind::FirstDerivative | BasisEvalKind::SecondDerivative
308 ) {
309 for (i, &x) in data.iter().enumerate() {
310 if x < left || x > right {
311 basis_matrix.row_mut(i).fill(0.0);
312 }
313 }
314 }
315 return Ok(());
316 }
317
318 if matches!(eval_kind, BasisEvalKind::FirstDerivative) {
319 let num_basis_lower = knotview.len().saturating_sub(degree);
320 let mut lower_basis = vec![0.0; num_basis_lower];
321 let mut lower_scratch = internal::BsplineScratch::new(degree.saturating_sub(1));
322 for (i, &x) in data.iter().enumerate() {
323 if x >= left && x <= right {
324 continue;
325 }
326 let x_c = x.clamp(left, right);
327 let mut row = basis_matrix.row_mut(i);
328 let row_slice = row
329 .as_slice_mut()
330 .expect("basis matrix rows should be contiguous");
331 evaluate_bspline_derivative_scalar_into(
332 x_c,
333 knotview,
334 degree,
335 row_slice,
336 &mut lower_basis,
337 &mut lower_scratch,
338 )?;
339 }
340 }
341
342 if matches!(eval_kind, BasisEvalKind::SecondDerivative) {
343 for (i, &x) in data.iter().enumerate() {
344 if x < left || x > right {
345 basis_matrix.row_mut(i).fill(0.0);
346 }
347 }
348 }
349
350 if matches!(eval_kind, BasisEvalKind::Basis) {
351 let z_clamped = data.mapv(|x| x.clamp(left, right));
352 apply_linear_extension_from_first_derivative(
353 data,
354 z_clamped.view(),
355 knotview,
356 degree,
357 basis_matrix,
358 )?;
359 }
360
361 Ok(())
362}
363
364#[inline]
365pub(crate) fn has_clamped_bspline_boundaries(knotview: ArrayView1<f64>, degree: usize) -> bool {
366 let clamp_count = degree + 1;
367 if knotview.len() < 2 * clamp_count {
368 return false;
369 }
370 let left = knotview[0];
371 let right = knotview[knotview.len() - 1];
372 let left_clamped = knotview.iter().take(clamp_count).all(|&k| k == left);
373 let right_clamped = knotview.iter().rev().take(clamp_count).all(|&k| k == right);
374 left_clamped && right_clamped
375}
376
377#[inline]
385pub(crate) fn clamp_eval_point_to_modeling_interval(
386 x: f64,
387 knotview: ArrayView1<f64>,
388 degree: usize,
389) -> f64 {
390 let num_basis = knotview.len().saturating_sub(degree + 1);
391 if num_basis == 0 {
392 return x;
393 }
394 let left = knotview[degree];
395 let right = knotview[num_basis];
396 if !left.is_finite() || !right.is_finite() || left >= right {
397 return x;
398 }
399 x.clamp(left, right)
400}
401
402#[inline]
427pub(crate) fn open_knot_derivative_exterior_is_zero(
428 x: f64,
429 knotview: ArrayView1<f64>,
430 degree: usize,
431) -> bool {
432 let num_basis = knotview.len().saturating_sub(degree + 1);
433 if num_basis == 0 {
434 return false;
435 }
436 let left = knotview[degree];
437 let right = knotview[num_basis];
438 if !(left.is_finite() && right.is_finite() && left < right) {
439 return false;
440 }
441 (x < left || x > right) && !has_clamped_bspline_boundaries(knotview, degree)
442}
443
444#[inline]
464pub(crate) fn linear_extension_higher_derivative_is_zero(
465 x: f64,
466 knotview: ArrayView1<f64>,
467 degree: usize,
468 derivative_order: usize,
469) -> bool {
470 if derivative_order < 2 {
471 return false;
472 }
473 let num_basis = knotview.len().saturating_sub(degree + 1);
474 if num_basis == 0 {
475 return false;
476 }
477 let left = knotview[degree];
478 let right = knotview[num_basis];
479 if !(left.is_finite() && right.is_finite() && left < right) {
480 return false;
481 }
482 x < left || x > right
483}
484
485#[inline]
486pub(crate) fn one_sided_derivative_eval_point(
487 x: f64,
488 knotview: ArrayView1<f64>,
489 degree: usize,
490) -> f64 {
491 let num_basis = knotview.len().saturating_sub(degree + 1);
492 if num_basis == 0 {
493 return x;
494 }
495 let left = knotview[degree];
496 let right = knotview[num_basis];
497 if !left.is_finite() || !right.is_finite() || left >= right {
498 return x;
499 }
500 if x == left {
501 let next = left.next_up();
502 if next < right {
503 next
504 } else {
505 left + 0.5 * (right - left)
506 }
507 } else if x == right {
508 let prev = right.next_down();
509 if prev > left {
510 prev
511 } else {
512 left + 0.5 * (right - left)
513 }
514 } else {
515 x
516 }
517}
518
519impl BasisOutputFormat for Sparse {
520 type Output = SparseColMat<usize, f64>;
521 const LAYOUT: BasisStorageLayout = BasisStorageLayout::Sparse;
522
523 fn build_basis(
524 data: ArrayView1<f64>,
525 degree: usize,
526 eval_kind: BasisEvalKind,
527 knotvec: Array1<f64>,
528 ) -> Result<(Self::Output, Array1<f64>), BasisError> {
529 let knotview = knotvec.view();
530 let sparse =
531 generate_basis_internal::<SparseStorage>(data.view(), knotview, degree, eval_kind)?;
532 Ok((sparse, knotvec))
533 }
534
535 fn from_dense(dense: Array2<f64>) -> Result<Self::Output, BasisError> {
536 let (nrows, ncols) = dense.dim();
537 let mut triplets: Vec<Triplet<usize, usize, f64>> = Vec::new();
538 triplets.reserve(nrows.saturating_mul(ncols / 8));
539 for i in 0..nrows {
540 for j in 0..ncols {
541 let v = dense[[i, j]];
542 if v.abs() > 0.0 {
543 triplets.push(Triplet::new(i, j, v));
544 }
545 }
546 }
547 SparseColMat::try_new_from_triplets(nrows, ncols, &triplets)
548 .map_err(|e| BasisError::SparseCreation(format!("{e:?}")))
549 }
550
551 fn from_sparse(sparse: SparseColMat<usize, f64>) -> Result<Self::Output, BasisError> {
552 Ok(sparse)
553 }
554}
555
556pub(crate) fn validate_knots_for_degree(
557 knot_vector: ArrayView1<f64>,
558 degree: usize,
559) -> Result<(), BasisError> {
560 if degree < 1 {
561 return Err(BasisError::InvalidDegree(degree));
562 }
563
564 let required_knots = 2 * (degree + 1);
565 if knot_vector.len() < required_knots {
566 return Err(BasisError::InsufficientKnotsForDegree {
567 degree,
568 required: required_knots,
569 provided: knot_vector.len(),
570 });
571 }
572
573 if knot_vector.iter().any(|&k| !k.is_finite()) {
574 return Err(BasisError::InvalidKnotVector(
575 "knot vector contains non-finite (NaN or Infinity) values".to_string(),
576 ));
577 }
578
579 if knot_vector.len() >= 2 {
580 for i in 0..(knot_vector.len() - 1) {
581 if knot_vector[i] > knot_vector[i + 1] {
582 return Err(BasisError::InvalidKnotVector(
583 "knot vector is not non-decreasing".to_string(),
584 ));
585 }
586 }
587 }
588
589 Ok(())
590}
591
592pub(crate) fn validate_knot_spans_nondegenerate(
602 knot_vector: ArrayView1<f64>,
603 degree: usize,
604) -> Result<(), BasisError> {
605 if knot_vector.len() <= degree + 1 {
606 return Ok(());
607 }
608 let num_basis = knot_vector.len() - degree - 1;
609 for i in 0..num_basis {
610 let span = knot_vector[i + degree + 1] - knot_vector[i];
611 if span <= 0.0 {
612 return Err(BasisError::InvalidKnotVector(format!(
613 "basis function {i} has zero support: t[i+degree+1]-t[i]={span:.3e} must be > 0"
614 )));
615 }
616 }
617 Ok(())
618}
619
620#[derive(Clone, Copy, Debug)]
621pub enum BasisEvalKind {
622 Basis,
623 FirstDerivative,
624 SecondDerivative,
625}
626
627pub(crate) struct BasisEvalScratch {
628 pub(crate) basis: internal::BsplineScratch,
629 pub(crate) lower_basis: Vec<f64>,
630 pub(crate) lower_scratch: internal::BsplineScratch,
631 pub(crate) derivative_workspace: BsplineDerivativeWorkspace,
632}
633
634impl BasisEvalScratch {
635 pub(crate) fn new(degree: usize) -> Self {
636 let lower_degree = degree.saturating_sub(1);
637 Self {
638 basis: internal::BsplineScratch::new(degree),
639 lower_basis: vec![0.0; lower_degree + 1],
640 lower_scratch: internal::BsplineScratch::new(lower_degree),
641 derivative_workspace: BsplineDerivativeWorkspace::new(),
642 }
643 }
644}
645
646#[inline]
647pub(crate) fn copy_full_row_to_sparse_window(full: &[f64], values: &mut [f64]) -> usize {
648 values.fill(0.0);
649 let Some(start_col) = full.iter().position(|&v| v != 0.0) else {
650 return 0;
651 };
652 for (offset, value_slot) in values.iter_mut().enumerate() {
653 if let Some(&v) = full.get(start_col + offset) {
654 *value_slot = v;
655 }
656 }
657 start_col
658}
659
660pub(crate) fn evaluate_splines_derivative_sparse_intowith_lower(
661 x: f64,
662 degree: usize,
663 knotview: ArrayView1<f64>,
664 values: &mut [f64],
665 lowervalues: &mut [f64],
666 lower_scratch: &mut internal::BsplineScratch,
667) -> usize {
668 let num_basis = knotview.len().saturating_sub(degree + 1);
669 if degree == 0 {
670 values.fill(0.0);
671 return 0;
672 }
673
674 let num_basis_lower = knotview.len().saturating_sub(degree);
675 if lowervalues.len() < num_basis_lower {
676 values.fill(0.0);
677 return 0;
678 }
679 lowervalues[..num_basis_lower].fill(0.0);
680
681 if open_knot_derivative_exterior_is_zero(x, knotview, degree) {
694 values.fill(0.0);
695 return 0;
696 }
697 let x_clamped = clamp_eval_point_to_modeling_interval(x, knotview, degree);
705 let x_eval = one_sided_derivative_eval_point(x_clamped, knotview, degree);
706 internal::evaluate_splines_at_point_full_support_into(
707 x_eval,
708 degree - 1,
709 knotview,
710 &mut lowervalues[..num_basis_lower],
711 lower_scratch,
712 );
713
714 let mut full_derivative = vec![0.0; num_basis];
715 for i in 0..num_basis {
716 let denom_left = knotview[i + degree] - knotview[i];
717 let denom_right = knotview[i + degree + 1] - knotview[i + 1];
718 let left_term = if !knot_span_is_degenerate(denom_left) {
719 lowervalues[i] / denom_left
720 } else {
721 0.0
722 };
723 let right_term = if !knot_span_is_degenerate(denom_right) {
724 lowervalues[i + 1] / denom_right
725 } else {
726 0.0
727 };
728 let value = (degree as f64) * (left_term - right_term);
729 full_derivative[i] = value;
730 }
731
732 copy_full_row_to_sparse_window(&full_derivative, values)
733}
734
735#[inline]
736pub(crate) fn evaluate_splines_derivative_sparse_into(
737 x: f64,
738 degree: usize,
739 knotview: ArrayView1<f64>,
740 values: &mut [f64],
741 scratch: &mut BasisEvalScratch,
742) -> usize {
743 let num_basis_lower = knotview.len().saturating_sub(degree);
744 if scratch.lower_basis.len() != num_basis_lower {
745 scratch.lower_basis.resize(num_basis_lower, 0.0);
746 scratch
747 .lower_scratch
748 .ensure_degree(degree.saturating_sub(1));
749 }
750 evaluate_splines_derivative_sparse_intowith_lower(
751 x,
752 degree,
753 knotview,
754 values,
755 &mut scratch.lower_basis,
756 &mut scratch.lower_scratch,
757 )
758}
759
760pub(crate) fn evaluate_splinessecond_derivative_sparse_into(
761 x: f64,
762 degree: usize,
763 knotview: ArrayView1<f64>,
764 values: &mut [f64],
765 scratch: &mut BasisEvalScratch,
766) -> usize {
767 let num_basis = knotview.len().saturating_sub(degree + 1);
768 if degree < 2 {
769 values.fill(0.0);
770 return 0;
771 }
772
773 if scratch.lower_basis.len() != num_basis {
774 scratch.lower_basis.resize(num_basis, 0.0);
775 }
776 evaluate_bspline_derivative_recurrence_into(
777 2,
778 x,
779 knotview,
780 degree,
781 &mut scratch.lower_basis,
782 &mut scratch.derivative_workspace,
783 0,
784 )
785 .expect("validated B-spline second-derivative inputs");
786
787 copy_full_row_to_sparse_window(&scratch.lower_basis, values)
788}
789
790#[inline]
791pub(crate) fn evaluate_splines_sparsewith_kind(
792 x: f64,
793 degree: usize,
794 knotview: ArrayView1<f64>,
795 eval_kind: BasisEvalKind,
796 values: &mut [f64],
797 scratch: &mut BasisEvalScratch,
798) -> usize {
799 match eval_kind {
800 BasisEvalKind::Basis => {
801 internal::evaluate_splines_sparse_into(x, degree, knotview, values, &mut scratch.basis)
802 }
803 BasisEvalKind::FirstDerivative => {
804 evaluate_splines_derivative_sparse_into(x, degree, knotview, values, scratch)
805 }
806 BasisEvalKind::SecondDerivative => {
807 evaluate_splinessecond_derivative_sparse_into(x, degree, knotview, values, scratch)
808 }
809 }
810}
811
812#[inline]
813pub(crate) fn evaluate_bsplinerow_entries<F>(
814 x: f64,
815 degree: usize,
816 knotview: ArrayView1<f64>,
817 eval_kind: BasisEvalKind,
818 num_basis_functions: usize,
819 scratch: &mut BasisEvalScratch,
820 values: &mut [f64],
821 mut write_entry: F,
822) where
823 F: FnMut(usize, f64),
824{
825 let start_col =
826 evaluate_splines_sparsewith_kind(x, degree, knotview, eval_kind, values, scratch);
827 for (offset, &v) in values.iter().enumerate() {
828 if v == 0.0 {
829 continue;
830 }
831 let col_j = start_col + offset;
832 if col_j < num_basis_functions {
833 write_entry(col_j, v);
834 }
835 }
836}
837
838pub(crate) trait BasisStorage {
839 type Output;
840
841 fn build(
842 data: ArrayView1<f64>,
843 knotview: ArrayView1<f64>,
844 degree: usize,
845 eval_kind: BasisEvalKind,
846 num_basis_functions: usize,
847 support: usize,
848 use_parallel: bool,
849 ) -> Result<Self::Output, BasisError>;
850}
851
852pub(crate) struct DenseStorage;
853
854impl BasisStorage for DenseStorage {
855 type Output = Array2<f64>;
856
857 fn build(
858 data: ArrayView1<f64>,
859 knotview: ArrayView1<f64>,
860 degree: usize,
861 eval_kind: BasisEvalKind,
862 num_basis_functions: usize,
863 support: usize,
864 use_parallel: bool,
865 ) -> Result<Self::Output, BasisError> {
866 let mut basis_matrix = Array2::zeros((data.len(), num_basis_functions));
867
868 if let (true, Some(data_slice)) = (use_parallel, data.as_slice()) {
869 basis_matrix
870 .axis_iter_mut(Axis(0))
871 .into_par_iter()
872 .zip(data_slice.par_iter().copied())
873 .for_each_init(
874 || (BasisEvalScratch::new(degree), vec![0.0; support]),
875 |(scratch, values), (mut row, x)| {
876 let row_slice = row
877 .as_slice_mut()
878 .expect("basis matrix rows should be contiguous");
879 evaluate_bsplinerow_entries(
880 x,
881 degree,
882 knotview,
883 eval_kind,
884 num_basis_functions,
885 scratch,
886 values,
887 |col_j, v| row_slice[col_j] = v,
888 );
889 },
890 );
891 } else {
892 let mut scratch = BasisEvalScratch::new(degree);
893 let mut values = vec![0.0; support];
894 for (mut row, &x) in basis_matrix.axis_iter_mut(Axis(0)).zip(data.iter()) {
895 let row_slice = row
896 .as_slice_mut()
897 .expect("basis matrix rows should be contiguous");
898 evaluate_bsplinerow_entries(
899 x,
900 degree,
901 knotview,
902 eval_kind,
903 num_basis_functions,
904 &mut scratch,
905 &mut values,
906 |col_j, v| row_slice[col_j] = v,
907 );
908 }
909 }
910
911 apply_dense_bspline_extrapolation(data, knotview, degree, eval_kind, &mut basis_matrix)?;
912
913 Ok(basis_matrix)
914 }
915}
916
917pub(crate) struct SparseStorage;
918
919impl BasisStorage for SparseStorage {
920 type Output = SparseColMat<usize, f64>;
921
922 fn build(
923 data: ArrayView1<f64>,
924 knotview: ArrayView1<f64>,
925 degree: usize,
926 eval_kind: BasisEvalKind,
927 num_basis_functions: usize,
928 support: usize,
929 use_parallel: bool,
930 ) -> Result<Self::Output, BasisError> {
931 let nrows = data.len();
932 let left = knotview[degree];
933 let right = knotview[num_basis_functions];
934 let needs_extrapolation = has_clamped_bspline_boundaries(knotview, degree)
935 && data.iter().any(|&x| x < left || x > right);
936 if needs_extrapolation {
937 let dense = DenseStorage::build(
938 data,
939 knotview,
940 degree,
941 eval_kind,
942 num_basis_functions,
943 support,
944 use_parallel,
945 )?;
946 return Sparse::from_dense(dense);
947 }
948
949 let triplets: Vec<Triplet<usize, usize, f64>> =
950 if let (true, Some(data_slice)) = (use_parallel, data.as_slice()) {
951 const CHUNK_SIZE: usize = 1024;
952 let triplet_chunks: Vec<Vec<Triplet<usize, usize, f64>>> = data_slice
953 .par_chunks(CHUNK_SIZE)
954 .enumerate()
955 .map_init(
956 || (BasisEvalScratch::new(degree), vec![0.0; support]),
957 |(scratch, values), (chunk_idx, chunk)| {
958 let baserow = chunk_idx * CHUNK_SIZE;
959 let mut local = Vec::with_capacity(chunk.len().saturating_mul(support));
960 for (i, &x) in chunk.iter().enumerate() {
961 let row_i = baserow + i;
962 evaluate_bsplinerow_entries(
963 x,
964 degree,
965 knotview,
966 eval_kind,
967 num_basis_functions,
968 scratch,
969 values,
970 |col_j, v| local.push(Triplet::new(row_i, col_j, v)),
971 );
972 }
973 local
974 },
975 )
976 .collect();
977
978 let mut flattened = Vec::with_capacity(nrows.saturating_mul(support));
979 for mut chunk in triplet_chunks {
980 flattened.append(&mut chunk);
981 }
982 flattened
983 } else {
984 let mut scratch = BasisEvalScratch::new(degree);
985 let mut values = vec![0.0; support];
986 let mut triplets = Vec::with_capacity(nrows.saturating_mul(support));
987
988 for (row_i, &x) in data.iter().enumerate() {
989 evaluate_bsplinerow_entries(
990 x,
991 degree,
992 knotview,
993 eval_kind,
994 num_basis_functions,
995 &mut scratch,
996 &mut values,
997 |col_j, v| triplets.push(Triplet::new(row_i, col_j, v)),
998 );
999 }
1000
1001 triplets
1002 };
1003
1004 SparseColMat::try_new_from_triplets(nrows, num_basis_functions, &triplets)
1005 .map_err(|err| BasisError::SparseCreation(format!("{err:?}")))
1006 }
1007}
1008
1009pub(crate) fn generate_basis_internal<S: BasisStorage>(
1010 data: ArrayView1<f64>,
1011 knotview: ArrayView1<f64>,
1012 degree: usize,
1013 eval_kind: BasisEvalKind,
1014) -> Result<S::Output, BasisError> {
1015 let num_basis_functions = knotview.len().saturating_sub(degree + 1);
1016 let support = degree + 1;
1017 let par_threshold = match degree {
1022 0 | 1 => 512,
1023 2 | 3 => 128,
1024 _ => 64,
1025 };
1026 let use_parallel = data.len() >= par_threshold && data.as_slice().is_some();
1027 S::build(
1028 data,
1029 knotview,
1030 degree,
1031 eval_kind,
1032 num_basis_functions,
1033 support,
1034 use_parallel,
1035 )
1036}
1037
1038pub fn should_use_sparse_basis(num_basis_cols: usize, degree: usize, dim: usize) -> bool {
1040 if num_basis_cols == 0 {
1041 return false;
1042 }
1043
1044 let support_perrow = (degree + 1).saturating_pow(dim as u32) as f64;
1045 let density = support_perrow / num_basis_cols as f64;
1046
1047 density < 0.20 && num_basis_cols > 32
1048}
1049
1050pub fn create_difference_penalty_matrix(
1069 num_coefficients: usize,
1070 order: usize,
1071 coefficient_abscissae: Option<ArrayView1<f64>>,
1072) -> Result<Array2<f64>, BasisError> {
1073 if order == 0 || order >= num_coefficients {
1074 return Err(BasisError::InvalidPenaltyOrder {
1075 order,
1076 num_basis: num_coefficients,
1077 });
1078 }
1079
1080 if let Some(g) = coefficient_abscissae
1081 && g.len() != num_coefficients
1082 {
1083 crate::bail_dim_basis!(
1084 "coefficient abscissae length {} does not match coefficient count {}",
1085 g.len(),
1086 num_coefficients
1087 );
1088 }
1089
1090 let mut d = Array2::<f64>::eye(num_coefficients);
1092
1093 for o in 1..=order {
1096 d = &d.slice(s![1.., ..]) - &d.slice(s![..-1, ..]);
1098
1099 if let Some(g) = coefficient_abscissae {
1110 let nrows = d.nrows();
1111 let mut log_span_sum = 0.0_f64;
1112 for i in 0..nrows {
1113 let span = g[i + o] - g[i];
1114 if span == 0.0 {
1115 return Err(BasisError::InvalidKnotVector(format!(
1116 "singular divided-difference span at order {o}, row {i}: coefficient coordinates g[{}]={:.6e} and g[{i}]={:.6e} coincide",
1117 i + o,
1118 g[i + o],
1119 g[i]
1120 )));
1121 }
1122 if !span.is_finite() || span < 0.0 {
1123 return Err(BasisError::InvalidKnotVector(format!(
1124 "divided-difference coordinates must be finite and strictly increasing at order {o}, row {i}: g[{}]={:.6e}, g[{i}]={:.6e}",
1125 i + o,
1126 g[i + o],
1127 g[i]
1128 )));
1129 }
1130 log_span_sum += span.abs().ln();
1131 }
1132 let ref_span = (log_span_sum / nrows as f64).exp();
1135 for i in 0..nrows {
1136 let span = (g[i + o] - g[i]) / ref_span;
1137 let mut row = d.row_mut(i);
1138 row /= span;
1139 }
1140 }
1141 }
1142
1143 let s = fast_ata(&d);
1145 Ok(s)
1146}
1147
1148pub(crate) fn bspline_raw_column_count(
1149 knots: &Array1<f64>,
1150 degree: usize,
1151 periodic: Option<(f64, f64, usize)>,
1152) -> Result<usize, String> {
1153 if let Some((_, _, num_basis)) = periodic {
1154 if num_basis <= degree {
1155 return Err(format!(
1156 "streaming cyclic B-spline basis requires more basis functions ({num_basis}) than degree ({degree})"
1157 ));
1158 }
1159 return Ok(num_basis);
1160 }
1161 knots
1162 .len()
1163 .checked_sub(degree + 1)
1164 .filter(|&p| p > 0)
1165 .ok_or_else(|| {
1166 format!(
1167 "streaming B-spline knots length {} is too short for degree {}",
1168 knots.len(),
1169 degree
1170 )
1171 })
1172}
1173
1174pub(crate) fn bspline_raw_row_chunk(
1175 data: ArrayView1<'_, f64>,
1176 knots: ArrayView1<'_, f64>,
1177 degree: usize,
1178 periodic: Option<(f64, f64, usize)>,
1179 start: usize,
1180 end: usize,
1181) -> Result<Array2<f64>, BasisError> {
1182 if start > end || end > data.len() {
1183 crate::bail_dim_basis!(
1184 "B-spline row chunk [{start}, {end}) is out of bounds for {} rows",
1185 data.len()
1186 );
1187 }
1188 let chunk = data.slice(s![start..end]);
1189 if let Some((domain_start, period, num_basis)) = periodic {
1190 if period <= 0.0 {
1191 crate::bail_invalid_basis!("periodic B-spline period must be positive, got {period}");
1192 }
1193 let (anchor, _) = crate::basis::cyclic_knot_anchor(domain_start, period, num_basis);
1198 let wrapped = chunk.mapv(|x| wrap_to_period(x, anchor, period));
1199 let (extended, _) = create_basis::<Dense>(
1200 wrapped.view(),
1201 KnotSource::Provided(knots),
1202 degree,
1203 BasisOptions::value(),
1204 )?;
1205 let mut cyclic = Array2::<f64>::zeros((chunk.len(), num_basis));
1206 for i in 0..extended.nrows() {
1207 for j in 0..extended.ncols() {
1208 cyclic[[i, j % num_basis]] += extended[[i, j]];
1209 }
1210 }
1211 Ok(cyclic)
1212 } else {
1213 let (basis, _) = create_basis::<Dense>(
1214 chunk,
1215 KnotSource::Provided(knots),
1216 degree,
1217 BasisOptions::value(),
1218 )?;
1219 Ok((*basis).clone())
1220 }
1221}