1use super::*;
2
3#[derive(Debug, Clone)]
19pub struct FixedRowSpaceProjector {
20 range_basis: Array2<f64>,
21 constraint_coordinates: Array2<f64>,
29}
30
31impl FixedRowSpaceProjector {
32 pub fn from_constraint_block(constraint: ArrayView2<'_, f64>) -> Result<Self, BasisError> {
33 let (n, q) = constraint.dim();
34 if constraint.iter().any(|value| !value.is_finite()) {
35 return Err(BasisError::InvalidInput(
36 "fixed row-space projector received a non-finite constraint block".to_string(),
37 ));
38 }
39 if q == 0 {
40 return Ok(Self {
41 range_basis: Array2::zeros((n, 0)),
42 constraint_coordinates: Array2::zeros((0, 0)),
43 });
44 }
45
46 let mut normalized = constraint.to_owned();
47 let mut column_norms = vec![0.0_f64; q];
48 for column in 0..q {
49 let norm = normalized
50 .column(column)
51 .dot(&normalized.column(column))
52 .sqrt();
53 column_norms[column] = norm;
54 if norm > 0.0 && norm.is_finite() {
55 normalized
56 .column_mut(column)
57 .mapv_inplace(|value| value / norm);
58 }
59 }
60 let (left, singular, right_t) =
61 gam_linalg::faer_ndarray::FaerSvd::svd(&normalized, true, true)
62 .map_err(BasisError::LinalgError)?;
63 let leading = singular.first().copied().unwrap_or(0.0);
64 let cutoff =
65 default_rrqr_rank_alpha() * f64::EPSILON * n.max(q).max(1) as f64 * leading.max(1.0);
66 let rank = singular.iter().filter(|&&value| value > cutoff).count();
67 let left = left.ok_or_else(|| {
68 BasisError::InvalidInput(
69 "fixed row-space projector SVD did not return its requested left frame".to_string(),
70 )
71 })?;
72 if left.nrows() != n || left.ncols() < rank {
73 return Err(BasisError::InvalidInput(format!(
74 "fixed row-space projector SVD returned a {}x{} left frame for an {n}x{q} constraint block of rank {rank}",
75 left.nrows(),
76 left.ncols(),
77 )));
78 }
79 let right_t = right_t.ok_or_else(|| {
80 BasisError::InvalidInput(
81 "fixed row-space projector SVD did not return its requested right frame"
82 .to_string(),
83 )
84 })?;
85 if right_t.nrows() < rank || right_t.ncols() != q {
86 return Err(BasisError::InvalidInput(format!(
87 "fixed row-space projector SVD returned a {}x{} right frame for an {n}x{q} constraint block of rank {rank}",
88 right_t.nrows(),
89 right_t.ncols(),
90 )));
91 }
92 let mut constraint_coordinates = Array2::<f64>::zeros((q, rank));
93 for constraint_column in 0..q {
94 let norm = column_norms[constraint_column];
95 if !(norm > 0.0 && norm.is_finite()) {
96 continue;
97 }
98 for range_column in 0..rank {
99 constraint_coordinates[[constraint_column, range_column]] =
100 right_t[[range_column, constraint_column]] / (norm * singular[range_column]);
101 }
102 }
103 Ok(Self {
104 range_basis: left.slice(s![.., 0..rank]).to_owned(),
105 constraint_coordinates,
106 })
107 }
108
109 pub fn nrows(&self) -> usize {
110 self.range_basis.nrows()
111 }
112
113 pub fn rank(&self) -> usize {
114 self.range_basis.ncols()
115 }
116
117 fn project_vector_owned(&self, mut values: Array1<f64>) -> Array1<f64> {
118 assert_eq!(values.len(), self.nrows());
119 if self.rank() > 0 {
120 let coordinates = self.range_basis.t().dot(&values);
121 values -= &self.range_basis.dot(&coordinates);
122 }
123 values
124 }
125
126 pub fn project_matrix_in_place(&self, values: &mut Array2<f64>) -> Result<(), BasisError> {
127 if values.nrows() != self.nrows() {
128 crate::bail_dim_basis!(
129 "fixed row-space projector has {} rows but the design jet has {}",
130 self.nrows(),
131 values.nrows()
132 );
133 }
134 if self.rank() > 0 {
135 let coordinates = fast_atb(&self.range_basis, values);
136 *values -= &fast_ab(&self.range_basis, &coordinates);
137 }
138 Ok(())
139 }
140
141 pub fn project_design(
153 &self,
154 design: DesignMatrix,
155 context: &str,
156 ) -> Result<(DesignMatrix, Array2<f64>), BasisError> {
157 use gam_linalg::matrix::{BlockDesignOperator, DesignBlock};
158
159 if design.nrows() != self.nrows() {
160 crate::bail_dim_basis!(
161 "fixed row-space projector has {} rows but value design '{context}' has {}",
162 self.nrows(),
163 design.nrows()
164 );
165 }
166 let p = design.ncols();
167 let rank = self.rank();
168 if rank == 0 {
169 return Ok((
170 design,
171 Array2::zeros((self.constraint_coordinates.nrows(), p)),
172 ));
173 }
174
175 let mut range_cross = Array2::<f64>::zeros((rank, p));
176 const CHUNK: usize = 1024;
177 for start in (0..design.nrows()).step_by(CHUNK) {
178 let end = (start + CHUNK).min(design.nrows());
179 let design_chunk = design
180 .try_row_chunk(start..end)
181 .map_err(|error| BasisError::InvalidInput(error.to_string()))?;
182 range_cross += &fast_atb(&self.range_basis.slice(s![start..end, ..]), &design_chunk);
183 }
184 let row_space_correction = fast_ab(&self.constraint_coordinates, &range_cross);
185
186 let design_block = match design {
187 DesignMatrix::Dense(inner) => DesignBlock::Dense(inner),
188 DesignMatrix::Sparse(inner) => DesignBlock::Sparse(inner),
189 };
190 let stacked = BlockDesignOperator::new(vec![
191 design_block,
192 DesignBlock::Dense(gam_linalg::matrix::DenseDesignMatrix::from(
193 self.range_basis.clone(),
194 )),
195 ])
196 .map_err(BasisError::InvalidInput)?;
197 let mut transform = Array2::<f64>::zeros((p + rank, p));
198 for column in 0..p {
199 transform[[column, column]] = 1.0;
200 }
201 for range_column in 0..rank {
202 for column in 0..p {
203 transform[[p + range_column, column]] = -range_cross[[range_column, column]];
204 }
205 }
206 let projected = CoefficientTransformOperator::new(
207 gam_linalg::matrix::DenseDesignMatrix::from(Arc::new(stacked)),
208 transform,
209 )
210 .map_err(|error| {
211 BasisError::InvalidInput(format!(
212 "fixed row-space projection failed for value design '{context}': {error}"
213 ))
214 })?;
215 Ok((
216 DesignMatrix::Dense(gam_linalg::matrix::DenseDesignMatrix::from(Arc::new(
217 projected,
218 ))),
219 row_space_correction,
220 ))
221 }
222
223 fn project_matrix_owned(&self, mut values: Array2<f64>) -> Array2<f64> {
224 self.project_matrix_in_place(&mut values)
225 .expect("installed fixed row-space projector has the operator's row count");
226 values
227 }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
231enum ProjectedJetKey {
232 FirstRaw(usize),
233 SecondDiagonal(usize),
234 SecondCross(usize, usize),
235}
236
237#[derive(Debug)]
238struct ImplicitRowProjection {
239 projector: FixedRowSpaceProjector,
240 corrections: std::sync::Mutex<HashMap<ProjectedJetKey, Arc<Array2<f64>>>>,
241}
242
243impl ImplicitRowProjection {
244 fn new(projector: FixedRowSpaceProjector) -> Self {
245 Self {
246 projector,
247 corrections: std::sync::Mutex::new(HashMap::new()),
248 }
249 }
250}
251
252#[derive(Debug, Clone)]
285pub struct ImplicitDesignPsiDerivative {
286 logarithmic_correction: Option<Arc<DuchonLogarithmicPsiCorrection>>,
287 pub(crate) phi_values: Array1<f64>,
290
291 pub(crate) axis_components: Array2<f64>,
295
296 pub(crate) q_values: Array1<f64>,
299
300 pub(crate) t_values: Array1<f64>,
303
304 pub(crate) streaming: Option<StreamingRadialState>,
307
308 pub(crate) ident_transform: Option<Array2<f64>>,
315
316 pub(crate) full_ident_transform: Option<Array2<f64>>,
323
324 pub(crate) n: usize,
326
327 pub(crate) n_knots: usize,
329
330 pub(crate) n_poly: usize,
333
334 pub(crate) n_axes: usize,
336
337 pub(crate) psi_scale_share: f64,
339
340 pub(crate) chart_scale: f64,
346
347 pub(crate) chart_first: Vec<f64>,
350
351 pub(crate) chart_second: Array2<f64>,
355
356 row_projection: Option<Arc<ImplicitRowProjection>>,
360
361 pub(crate) axis_combinations: Option<Vec<Vec<(usize, f64)>>>,
364}
365
366include!("duchon_logarithmic_psi.rs");
367
368pub struct LatentCoordDesignDerivative {
376 pub(crate) provider: Arc<dyn LocalDesignJacobianProvider>,
377}
378
379#[derive(Debug, Clone)]
380pub(crate) struct RadialLatentCoordLocalDesignJacobian {
381 pub(crate) latent: Arc<crate::latent::LatentCoordValues>,
382 pub(crate) centers: Arc<Array2<f64>>,
384 pub(crate) input_scale: crate::IsotropicScale,
392 pub(crate) radial_kind: RadialScalarKind,
393 pub(crate) ident_transform: Option<Array2<f64>>,
394 pub(crate) full_ident_transform: Option<Array2<f64>>,
395 pub(crate) n_poly: usize,
396 pub(crate) polynomial_order: Option<DuchonNullspaceOrder>,
397 pub(crate) chart_scale: f64,
404}
405
406#[derive(Debug, Clone)]
407pub(crate) struct JetLatentCoordLocalDesignJacobian {
408 pub(crate) latent: Arc<crate::latent::LatentCoordValues>,
409 pub(crate) jet: Arc<Array3<f64>>,
410 pub(crate) ident_transform: Option<Array2<f64>>,
411}
412
413impl std::fmt::Debug for LatentCoordDesignDerivative {
414 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
415 f.debug_struct("LatentCoordDesignDerivative")
416 .field("n_data", &self.n_data())
417 .field("latent_dim", &self.latent_dim())
418 .field("n_axes", &self.n_axes())
419 .field("p_out", &self.p_out())
420 .field("provider", &self.provider)
421 .finish()
422 }
423}
424
425impl Clone for LatentCoordDesignDerivative {
426 fn clone(&self) -> Self {
427 Self {
428 provider: Arc::clone(&self.provider),
429 }
430 }
431}
432
433impl RadialLatentCoordLocalDesignJacobian {
434 pub(crate) fn p_constrained(&self) -> usize {
435 self.ident_transform
436 .as_ref()
437 .map_or(self.centers.nrows(), Array2::ncols)
438 }
439
440 pub(crate) fn p_after_pad(&self) -> usize {
441 self.p_constrained() + self.n_poly
442 }
443
444 pub(crate) fn p_out(&self) -> usize {
445 self.full_ident_transform
446 .as_ref()
447 .map_or(self.p_after_pad(), Array2::ncols)
448 }
449}
450
451impl JetLatentCoordLocalDesignJacobian {
452 pub(crate) fn p_out(&self) -> usize {
453 self.ident_transform
454 .as_ref()
455 .map_or(self.jet.shape()[1], Array2::ncols)
456 }
457}
458
459pub trait LocalDesignJacobianProvider: Send + Sync + std::fmt::Debug {
474 fn n_data(&self) -> usize;
476
477 fn latent_dim(&self) -> usize;
479
480 fn n_axes(&self) -> usize;
482
483 fn p_out(&self) -> usize;
485
486 fn local_design_jacobian_row(&self, row: usize, axis: usize)
489 -> Result<Array1<f64>, BasisError>;
490
491 fn row_axis(&self, flat_axis: usize) -> (usize, usize) {
494 let d = self.latent_dim();
495 (flat_axis / d, flat_axis % d)
496 }
497
498 fn forward_mul_axis(
500 &self,
501 flat_axis: usize,
502 u: &ArrayView1<'_, f64>,
503 ) -> Result<Array1<f64>, BasisError> {
504 assert!(
505 flat_axis < self.n_axes(),
506 "latent-coordinate derivative flat axis out of bounds in forward_mul_axis: flat_axis={flat_axis}, n_axes={}",
507 self.n_axes()
508 );
509 let (row, axis) = self.row_axis(flat_axis);
510 let local_jacobian = self.local_design_jacobian_row(row, axis)?;
511 assert_eq!(
512 u.len(),
513 local_jacobian.len(),
514 "latent-coordinate derivative coefficient length mismatch in forward_mul_axis"
515 );
516 let value = local_jacobian.dot(u);
517 let mut out = Array1::<f64>::zeros(self.n_data());
518 out[row] = value;
519 Ok(out)
520 }
521
522 fn transpose_mul_axis(
524 &self,
525 flat_axis: usize,
526 v: &ArrayView1<'_, f64>,
527 ) -> Result<Array1<f64>, BasisError> {
528 assert!(
529 flat_axis < self.n_axes(),
530 "latent-coordinate derivative flat axis out of bounds in transpose_mul_axis: flat_axis={flat_axis}, n_axes={}",
531 self.n_axes()
532 );
533 assert_eq!(
534 v.len(),
535 self.n_data(),
536 "latent-coordinate derivative row-adjoint length mismatch in transpose_mul_axis"
537 );
538 let (row, axis) = self.row_axis(flat_axis);
539 let scale = v[row];
540 Ok(self
541 .local_design_jacobian_row(row, axis)?
542 .mapv(|value| scale * value))
543 }
544
545 fn materialize_axis(&self, flat_axis: usize) -> Result<Array2<f64>, BasisError> {
548 assert!(
549 flat_axis < self.n_axes(),
550 "latent-coordinate derivative flat axis out of bounds in materialize_axis: flat_axis={flat_axis}, n_axes={}",
551 self.n_axes()
552 );
553 let (row, axis) = self.row_axis(flat_axis);
554 let projected = self.local_design_jacobian_row(row, axis)?;
555 let mut out = Array2::<f64>::zeros((self.n_data(), projected.len()));
556 out.row_mut(row).assign(&projected);
557 Ok(out)
558 }
559}
560
561pub(crate) const IMPLICIT_MATVEC_CHUNK_SIZE: usize = 1000;
564
565pub(crate) const IMPLICIT_MATVEC_PAR_THRESHOLD: usize = 10_000;
567
568pub(crate) const THIN_PLATE_PENALTY_PSI_TILE_ROWS: usize = 32;
571
572impl LatentCoordDesignDerivative {
573 pub(crate) fn from_local_design_jacobian_provider(
574 provider: Arc<dyn LocalDesignJacobianProvider>,
575 ) -> Self {
576 Self { provider }
577 }
578
579 pub fn new_matern(
585 latent: Arc<crate::latent::LatentCoordValues>,
586 centers: Arc<Array2<f64>>,
587 input_scale: crate::IsotropicScale,
588 length_scale: crate::OriginalUnits,
589 nu: MaternNu,
590 include_intercept: bool,
591 ident_transform: Option<Array2<f64>>,
592 ) -> Result<Self, BasisError> {
593 if latent.latent_dim() != centers.ncols() {
594 crate::bail_dim_basis!(
595 "LatentCoordDesignDerivative Matérn dimension mismatch: latent d={} centers d={}",
596 latent.latent_dim(),
597 centers.ncols()
598 );
599 }
600 let length_scale = input_scale
601 .to_standardized_units(length_scale)
602 .standardized_value();
603 Ok(Self::from_local_design_jacobian_provider(Arc::new(
604 RadialLatentCoordLocalDesignJacobian {
605 latent,
606 centers,
607 input_scale,
608 radial_kind: RadialScalarKind::Matern { length_scale, nu },
609 ident_transform,
610 full_ident_transform: None,
611 n_poly: usize::from(include_intercept),
612 polynomial_order: None,
613 chart_scale: 1.0,
614 },
615 )))
616 }
617
618 pub fn new_duchon(
621 latent: Arc<crate::latent::LatentCoordValues>,
622 centers: Arc<Array2<f64>>,
623 input_scale: crate::IsotropicScale,
624 length_scale: Option<crate::OriginalUnits>,
625 power: f64,
626 nullspace_order: DuchonNullspaceOrder,
627 radial_reparam: Option<&Array2<f64>>,
628 full_ident_transform: Option<Array2<f64>>,
629 ) -> Result<Self, BasisError> {
630 if latent.latent_dim() != centers.ncols() {
631 crate::bail_dim_basis!(
632 "LatentCoordDesignDerivative Duchon dimension mismatch: latent d={} centers d={}",
633 latent.latent_dim(),
634 centers.ncols()
635 );
636 }
637 let effective_order = duchon_effective_nullspace_order(centers.view(), nullspace_order);
638 let p_order = duchon_p_from_nullspace_order(effective_order);
639 let s_order = power.max(0.0).round() as usize;
640 let length_scale =
645 length_scale.map(|ell| input_scale.to_standardized_units(ell).standardized_value());
646 let (radial_kind, chart_scale) = if let Some(length_scale) = length_scale {
651 let coeffs = duchon_partial_fraction_coeffs(
652 p_order,
653 s_order,
654 duchon_inverse_length_scale(length_scale, "implicit ψ-derivative Duchon kernel")?,
655 );
656 let chart_scale = duchon_kernel_chart(
657 centers.view(),
658 Some(length_scale),
659 p_order,
660 s_order,
661 centers.ncols(),
662 None,
663 Some(&coeffs),
664 None,
665 )
666 .amplification;
667 (
668 RadialScalarKind::Duchon {
669 length_scale,
670 p_order,
671 s_order,
672 dim: centers.ncols(),
673 coeffs,
674 },
675 chart_scale,
676 )
677 } else {
678 let pure_poly_coeff = PolyharmonicBlockCoeff::new(
679 pure_duchon_block_order(p_order, power),
680 centers.ncols(),
681 );
682 let chart_scale = duchon_kernel_chart(
683 centers.view(),
684 None,
685 p_order,
686 s_order,
687 centers.ncols(),
688 None,
689 None,
690 Some(&pure_poly_coeff),
691 )
692 .amplification;
693 (
694 RadialScalarKind::PureDuchon {
695 block_order: pure_duchon_block_order(p_order, power).max(1.0) as usize,
696 p_order,
697 s_order,
698 dim: centers.ncols(),
699 },
700 chart_scale,
701 )
702 };
703 let mut workspace = BasisWorkspace::default();
704 let mut ident_transform =
705 kernel_constraint_nullspace(centers.view(), effective_order, &mut workspace.cache)?;
706 if let Some(v) = radial_reparam {
715 if v.nrows() != ident_transform.ncols() {
716 crate::bail_dim_basis!(
717 "LatentCoordDesignDerivative Duchon radial chart mismatch: Z has {} columns, V has {} rows",
718 ident_transform.ncols(),
719 v.nrows()
720 );
721 }
722 ident_transform = ident_transform.dot(v);
723 }
724 let n_poly = polynomial_block_from_order(centers.view(), effective_order).ncols();
725 Ok(Self::from_local_design_jacobian_provider(Arc::new(
726 RadialLatentCoordLocalDesignJacobian {
727 latent,
728 centers,
729 input_scale,
730 radial_kind,
731 ident_transform: Some(ident_transform),
732 full_ident_transform,
733 n_poly,
734 polynomial_order: Some(effective_order),
735 chart_scale,
736 },
737 )))
738 }
739
740 pub fn new_sphere(
741 latent: Arc<crate::latent::LatentCoordValues>,
742 centers: Arc<Array2<f64>>,
743 penalty_order: usize,
744 ident_transform: Option<Array2<f64>>,
745 ) -> Result<Self, BasisError> {
746 if latent.latent_dim() != centers.ncols() {
747 crate::bail_dim_basis!(
748 "LatentCoordDesignDerivative sphere dimension mismatch: latent d={} centers d={}",
749 latent.latent_dim(),
750 centers.ncols()
751 );
752 }
753 let raw_jet = sphere_first_derivative_nd(
754 latent.as_matrix().view(),
755 centers.view(),
756 penalty_order,
757 true,
758 )?;
759 let jet = latent.design_gradient_wrt_t_dispatch(
760 crate::latent::InputLocationDerivative::Jet(raw_jet.view()),
761 )?;
762 Self::from_jet(latent, jet, ident_transform)
763 }
764
765 pub fn new_periodic_bspline(
766 latent: Arc<crate::latent::LatentCoordValues>,
767 data_range: (f64, f64),
768 degree: usize,
769 num_basis: usize,
770 ident_transform: Option<Array2<f64>>,
771 ) -> Result<Self, BasisError> {
772 let raw_jet = periodic_bspline_first_derivative_nd(
773 latent.as_matrix().view(),
774 data_range,
775 degree,
776 num_basis,
777 )?;
778 let jet = latent.design_gradient_wrt_t_dispatch(
779 crate::latent::InputLocationDerivative::Jet(raw_jet.view()),
780 )?;
781 Self::from_jet(latent, jet, ident_transform)
782 }
783
784 pub fn new_tensor_bspline(
785 latent: Arc<crate::latent::LatentCoordValues>,
786 knots_per_axis: Vec<Array1<f64>>,
787 degrees: Vec<usize>,
788 ident_transform: Option<Array2<f64>>,
789 ) -> Result<Self, BasisError> {
790 let knot_views = knots_per_axis
791 .iter()
792 .map(|knots| knots.view())
793 .collect::<Vec<_>>();
794 let raw_jet =
795 bspline_tensor_first_derivative(latent.as_matrix().view(), &knot_views, °rees)?;
796 let jet = latent.design_gradient_wrt_t_dispatch(
797 crate::latent::InputLocationDerivative::Jet(raw_jet.view()),
798 )?;
799 Self::from_jet(latent, jet, ident_transform)
800 }
801
802 pub fn new_pca(
803 latent: Arc<crate::latent::LatentCoordValues>,
804 basis_matrix: Arc<Array2<f64>>,
805 ) -> Result<Self, BasisError> {
806 if latent.latent_dim() != basis_matrix.nrows() {
807 crate::bail_dim_basis!(
808 "LatentCoordDesignDerivative Pca dimension mismatch: latent d={} basis rows={}",
809 latent.latent_dim(),
810 basis_matrix.nrows()
811 );
812 }
813 let mut jet =
814 Array3::<f64>::zeros((latent.n_obs(), basis_matrix.ncols(), basis_matrix.nrows()));
815 for row in 0..latent.n_obs() {
816 for axis in 0..basis_matrix.nrows() {
817 for col in 0..basis_matrix.ncols() {
818 jet[[row, col, axis]] = basis_matrix[[axis, col]];
819 }
820 }
821 }
822 Self::from_jet(latent, jet, None)
823 }
824
825 pub fn from_jet(
826 latent: Arc<crate::latent::LatentCoordValues>,
827 jet: Array3<f64>,
828 ident_transform: Option<Array2<f64>>,
829 ) -> Result<Self, BasisError> {
830 if jet.shape()[0] != latent.n_obs() || jet.shape()[2] != latent.latent_dim() {
831 crate::bail_dim_basis!(
832 "LatentCoordDesignDerivative jet shape {:?} does not match latent shape ({}, {}, {})",
833 jet.shape(),
834 latent.n_obs(),
835 jet.shape()[1],
836 latent.latent_dim()
837 );
838 }
839 if let Some(z) = ident_transform.as_ref()
840 && z.nrows() != jet.shape()[1]
841 {
842 crate::bail_dim_basis!(
843 "LatentCoordDesignDerivative identifiability transform has {} rows but derivative jet has {} basis columns",
844 z.nrows(),
845 jet.shape()[1]
846 );
847 }
848 Ok(Self::from_local_design_jacobian_provider(Arc::new(
849 JetLatentCoordLocalDesignJacobian {
850 latent,
851 jet: Arc::new(jet),
852 ident_transform,
853 },
854 )))
855 }
856
857 pub(crate) fn n_data(&self) -> usize {
858 self.provider.n_data()
859 }
860
861 pub(crate) fn latent_dim(&self) -> usize {
862 self.provider.latent_dim()
863 }
864
865 pub fn n_axes(&self) -> usize {
866 self.provider.n_axes()
867 }
868
869 pub fn p_out(&self) -> usize {
870 self.provider.p_out()
871 }
872}
873
874impl RadialLatentCoordLocalDesignJacobian {
875 pub(crate) fn project_and_pad(
876 &self,
877 raw_knot: &Array1<f64>,
878 raw_poly: &Array1<f64>,
879 ) -> Result<Array1<f64>, BasisError> {
880 let constrained = match &self.ident_transform {
881 Some(z) => z.t().dot(raw_knot),
882 None => raw_knot.clone(),
883 };
884 let mut padded = Array1::<f64>::zeros(constrained.len() + self.n_poly);
885 padded
886 .slice_mut(s![..constrained.len()])
887 .assign(&constrained);
888 if self.n_poly > 0 {
889 padded.slice_mut(s![constrained.len()..]).assign(raw_poly);
890 }
891 Ok(match &self.full_ident_transform {
892 Some(zf) => zf.t().dot(&padded),
893 None => padded,
894 })
895 }
896
897 pub(crate) fn kernel_axis_scalar(
898 &self,
899 row: usize,
900 center: usize,
901 axis: usize,
902 ) -> Result<f64, BasisError> {
903 let t_row = self.latent.row(row);
907 let reciprocal = self.input_scale.reciprocal();
908 let mut r2 = 0.0_f64;
909 for a in 0..self.latent.latent_dim() {
910 let delta = t_row[a] * reciprocal - self.centers[[center, a]];
911 r2 += delta * delta;
912 }
913 let r = r2.sqrt();
914 if r == 0.0 {
915 if self.radial_kind.is_smooth_at_collision() {
921 return Ok(0.0);
922 }
923 return Err(BasisError::DegenerateAtCollision {
924 kernel: "RadialScalarKind (design axis)",
925 dim: self.latent.latent_dim(),
926 m: 0.0,
927 message: "radial scalar q = φ'/r has no finite limit at r = 0; \
928 the design row axis component is undefined",
929 });
930 }
931 let (_, q, _) = self.radial_kind.eval_design_triplet(r)?;
932 Ok(self.chart_scale
937 * q
938 * (t_row[axis] * reciprocal - self.centers[[center, axis]])
939 * reciprocal)
940 }
941
942 pub(crate) fn polynomial_axis_values(&self, row: usize, axis: usize) -> Array1<f64> {
943 let Some(order) = self.polynomial_order else {
944 return Array1::<f64>::zeros(self.n_poly);
945 };
946 let max_degree = match order {
947 DuchonNullspaceOrder::Zero => 0usize,
948 DuchonNullspaceOrder::Linear => 1usize,
949 DuchonNullspaceOrder::Degree(k) => k,
950 };
951 let t_row = self.latent.row(row);
956 let reciprocal = self.input_scale.reciprocal();
957 let exponents = monomial_exponents(self.latent.latent_dim(), max_degree);
958 let mut out = Array1::<f64>::zeros(exponents.len());
959 for (col, alpha) in exponents.iter().enumerate() {
960 let a_axis = alpha[axis];
961 if a_axis == 0 {
962 continue;
963 }
964 let mut value = a_axis as f64 * reciprocal;
965 for a in 0..self.latent.latent_dim() {
966 let exp_a = if a == axis { a_axis - 1 } else { alpha[a] };
967 if exp_a != 0 {
968 value *= (t_row[a] * reciprocal).powi(exp_a as i32);
969 }
970 }
971 out[col] = value;
972 }
973 out
974 }
975}
976
977impl JetLatentCoordLocalDesignJacobian {
978 pub(crate) fn project_jet(&self, raw_knot: &Array1<f64>) -> Result<Array1<f64>, BasisError> {
979 Ok(match &self.ident_transform {
980 Some(z) => z.t().dot(raw_knot),
981 None => raw_knot.clone(),
982 })
983 }
984}
985
986impl LocalDesignJacobianProvider for LatentCoordDesignDerivative {
987 fn n_data(&self) -> usize {
988 self.provider.n_data()
989 }
990
991 fn latent_dim(&self) -> usize {
992 self.provider.latent_dim()
993 }
994
995 fn n_axes(&self) -> usize {
996 self.provider.n_axes()
997 }
998
999 fn p_out(&self) -> usize {
1000 self.provider.p_out()
1001 }
1002
1003 fn local_design_jacobian_row(
1004 &self,
1005 row: usize,
1006 axis: usize,
1007 ) -> Result<Array1<f64>, BasisError> {
1008 self.provider.local_design_jacobian_row(row, axis)
1009 }
1010}
1011
1012impl LocalDesignJacobianProvider for RadialLatentCoordLocalDesignJacobian {
1013 fn n_data(&self) -> usize {
1014 self.latent.n_obs()
1015 }
1016
1017 fn latent_dim(&self) -> usize {
1018 self.latent.latent_dim()
1019 }
1020
1021 fn n_axes(&self) -> usize {
1022 self.latent.len()
1023 }
1024
1025 fn p_out(&self) -> usize {
1026 Self::p_out(self)
1027 }
1028
1029 fn local_design_jacobian_row(
1030 &self,
1031 row: usize,
1032 axis: usize,
1033 ) -> Result<Array1<f64>, BasisError> {
1034 let mut raw_knot = Array1::<f64>::zeros(self.centers.nrows());
1035 for center in 0..self.centers.nrows() {
1036 raw_knot[center] = self.kernel_axis_scalar(row, center, axis)?;
1037 }
1038 let raw_poly = self.polynomial_axis_values(row, axis);
1039 self.project_and_pad(&raw_knot, &raw_poly)
1040 }
1041}
1042
1043impl LocalDesignJacobianProvider for JetLatentCoordLocalDesignJacobian {
1044 fn n_data(&self) -> usize {
1045 self.latent.n_obs()
1046 }
1047
1048 fn latent_dim(&self) -> usize {
1049 self.latent.latent_dim()
1050 }
1051
1052 fn n_axes(&self) -> usize {
1053 self.latent.len()
1054 }
1055
1056 fn p_out(&self) -> usize {
1057 Self::p_out(self)
1058 }
1059
1060 fn local_design_jacobian_row(
1061 &self,
1062 row: usize,
1063 axis: usize,
1064 ) -> Result<Array1<f64>, BasisError> {
1065 let mut raw_knot = Array1::<f64>::zeros(self.jet.shape()[1]);
1066 for basis_col in 0..self.jet.shape()[1] {
1067 raw_knot[basis_col] = self.jet[[row, basis_col, axis]];
1068 }
1069 self.project_jet(&raw_knot)
1070 }
1071}
1072
1073impl ImplicitDesignPsiDerivative {
1074 pub fn new(
1087 phi_values: Array1<f64>,
1088 q_values: Array1<f64>,
1089 t_values: Array1<f64>,
1090 axis_components: Array2<f64>,
1091 ident_transform: Option<Array2<f64>>,
1092 full_ident_transform: Option<Array2<f64>>,
1093 n: usize,
1094 n_knots: usize,
1095 n_poly: usize,
1096 n_axes: usize,
1097 ) -> Self {
1098 assert_eq!(
1099 phi_values.len(),
1100 n * n_knots,
1101 "implicit psi derivative phi length mismatch: expected n*n_knots={}*{}={}, got {}",
1102 n,
1103 n_knots,
1104 n * n_knots,
1105 phi_values.len()
1106 );
1107 assert_eq!(
1108 q_values.len(),
1109 n * n_knots,
1110 "implicit psi derivative q length mismatch: expected n*n_knots={}*{}={}, got {}",
1111 n,
1112 n_knots,
1113 n * n_knots,
1114 q_values.len()
1115 );
1116 assert_eq!(
1117 t_values.len(),
1118 n * n_knots,
1119 "implicit psi derivative t length mismatch: expected n*n_knots={}*{}={}, got {}",
1120 n,
1121 n_knots,
1122 n * n_knots,
1123 t_values.len()
1124 );
1125 assert_eq!(
1126 axis_components.nrows(),
1127 n * n_knots,
1128 "implicit psi derivative axis-component row mismatch: expected n*n_knots={}*{}={}, got {}",
1129 n,
1130 n_knots,
1131 n * n_knots,
1132 axis_components.nrows()
1133 );
1134 assert_eq!(
1135 axis_components.ncols(),
1136 n_axes,
1137 "implicit psi derivative axis-component column mismatch: expected n_axes={n_axes}, got {}",
1138 axis_components.ncols()
1139 );
1140 Self {
1141 phi_values,
1142 axis_components,
1143 q_values,
1144 t_values,
1145 streaming: None,
1146 ident_transform,
1147 full_ident_transform,
1148 n,
1149 n_knots,
1150 n_poly,
1151 n_axes,
1152 psi_scale_share: 0.0,
1153 chart_scale: 1.0,
1154 chart_first: Vec::new(),
1155 chart_second: Array2::<f64>::zeros((0, 0)),
1156 row_projection: None,
1157 axis_combinations: None,
1158 logarithmic_correction: None,
1159 }
1160 }
1161
1162 pub(crate) fn with_psi_scale_share(mut self, psi_scale_share: f64) -> Self {
1163 self.psi_scale_share = psi_scale_share;
1164 self
1165 }
1166
1167 pub(crate) fn with_kernel_chart(
1180 mut self,
1181 scale: f64,
1182 first: Vec<f64>,
1183 second: Array2<f64>,
1184 ) -> Self {
1185 let raw_axes = self.n_axes;
1186 assert!(
1187 scale.is_finite() && scale > 0.0,
1188 "kernel chart scale must be a positive finite number, got {scale}"
1189 );
1190 assert_eq!(
1191 first.len(),
1192 raw_axes,
1193 "kernel chart first log-jet must have one entry per raw axis"
1194 );
1195 assert_eq!(
1196 second.dim(),
1197 (raw_axes, raw_axes),
1198 "kernel chart second log-jet must be raw-axes square"
1199 );
1200 self.chart_scale = scale;
1201 self.chart_first = first;
1202 self.chart_second = second;
1203 self
1204 }
1205
1206 #[inline]
1210 pub(crate) fn effective_share(&self, axis: usize) -> f64 {
1211 let raw_share =
1212 |raw: usize| self.psi_scale_share + self.chart_first.get(raw).copied().unwrap_or(0.0);
1213 match self.axis_combinations.as_ref() {
1214 Some(_) => self
1215 .transformed_axis_combination(axis)
1216 .iter()
1217 .map(|(raw, coeff)| coeff * raw_share(*raw))
1218 .sum(),
1219 None => raw_share(axis),
1220 }
1221 }
1222
1223 #[inline]
1226 pub(crate) fn chart_lambda(&self, axis_a: usize, axis_b: usize) -> f64 {
1227 if self.chart_second.is_empty() {
1228 return 0.0;
1229 }
1230 match self.axis_combinations.as_ref() {
1231 Some(_) => {
1232 let combo_a = self.transformed_axis_combination(axis_a);
1233 let combo_b = self.transformed_axis_combination(axis_b);
1234 let mut total = 0.0;
1235 for (raw_a, coeff_a) in combo_a {
1236 for (raw_b, coeff_b) in combo_b {
1237 total += coeff_a * coeff_b * self.chart_second[[*raw_a, *raw_b]];
1238 }
1239 }
1240 total
1241 }
1242 None => self.chart_second[[axis_a, axis_b]],
1243 }
1244 }
1245
1246 pub fn new_streaming(
1255 data: Arc<Array2<f64>>,
1256 centers: Arc<Array2<f64>>,
1257 eta: Vec<f64>,
1258 radial_kind: RadialScalarKind,
1259 ident_transform: Option<Array2<f64>>,
1260 full_ident_transform: Option<Array2<f64>>,
1261 n_poly: usize,
1262 ) -> Self {
1263 let n = data.nrows();
1264 let n_knots = centers.nrows();
1265 let n_axes = data.ncols();
1266 let psi_scale_share = radial_kind.raw_psi_isotropic_share();
1267 assert_eq!(eta.len(), n_axes);
1268 assert_eq!(
1269 centers.ncols(),
1270 n_axes,
1271 "streaming radial centers have {} columns but data/eta have {n_axes}",
1272 centers.ncols()
1273 );
1274 let metric_weights: Arc<[f64]> = Arc::from(centered_aniso_metric_weights(&eta));
1275 Self {
1276 phi_values: Array1::<f64>::zeros(0),
1278 axis_components: Array2::<f64>::zeros((0, 0)),
1279 q_values: Array1::<f64>::zeros(0),
1280 t_values: Array1::<f64>::zeros(0),
1281 streaming: Some(StreamingRadialState {
1282 data,
1283 centers,
1284 axis_mode: StreamingAxisMode::PerAxis { metric_weights },
1285 radial_kind,
1286 triplet_cache: Arc::new(std::sync::OnceLock::new()),
1287 }),
1288 ident_transform,
1289 full_ident_transform,
1290 n,
1291 n_knots,
1292 n_poly,
1293 n_axes,
1294 psi_scale_share,
1295 chart_scale: 1.0,
1296 chart_first: Vec::new(),
1297 chart_second: Array2::<f64>::zeros((0, 0)),
1298 row_projection: None,
1299 axis_combinations: None,
1300 logarithmic_correction: None,
1301 }
1302 }
1303
1304 pub(crate) fn new_streaming_scalar(
1308 data: Arc<Array2<f64>>,
1309 centers: Arc<Array2<f64>>,
1310 eta: Vec<f64>,
1311 radial_kind: RadialScalarKind,
1312 ident_transform: Option<Array2<f64>>,
1313 full_ident_transform: Option<Array2<f64>>,
1314 n_poly: usize,
1315 ) -> Self {
1316 let n = data.nrows();
1317 let n_knots = centers.nrows();
1318 let dim = data.ncols();
1319 assert_eq!(eta.len(), dim);
1320 assert_eq!(
1321 centers.ncols(),
1322 dim,
1323 "streaming scalar radial centers have {} columns but data/eta have {dim}",
1324 centers.ncols()
1325 );
1326 let metric_weights: Arc<[f64]> = Arc::from(centered_aniso_metric_weights(&eta));
1327 Self {
1328 phi_values: Array1::<f64>::zeros(0),
1329 axis_components: Array2::<f64>::zeros((0, 0)),
1330 q_values: Array1::<f64>::zeros(0),
1331 t_values: Array1::<f64>::zeros(0),
1332 streaming: Some(StreamingRadialState {
1333 data,
1334 centers,
1335 axis_mode: StreamingAxisMode::ScalarTotal { metric_weights },
1336 radial_kind,
1337 triplet_cache: Arc::new(std::sync::OnceLock::new()),
1338 }),
1339 ident_transform,
1340 full_ident_transform,
1341 n,
1342 n_knots,
1343 n_poly,
1344 n_axes: 1,
1345 psi_scale_share: 0.0,
1346 chart_scale: 1.0,
1347 chart_first: Vec::new(),
1348 chart_second: Array2::<f64>::zeros((0, 0)),
1349 row_projection: None,
1350 axis_combinations: None,
1351 logarithmic_correction: None,
1352 }
1353 }
1354
1355 #[inline]
1357 pub(crate) fn is_streaming(&self) -> bool {
1358 self.streaming.is_some()
1359 }
1360
1361 pub fn n_data(&self) -> usize {
1363 self.n
1364 }
1365
1366 pub fn n_axes(&self) -> usize {
1368 self.axis_combinations
1369 .as_ref()
1370 .map_or(self.n_axes, Vec::len)
1371 }
1372
1373 pub fn is_duchon_family(&self) -> bool {
1374 self.streaming.as_ref().is_some_and(|state| {
1375 matches!(
1376 state.radial_kind,
1377 RadialScalarKind::Duchon { .. } | RadialScalarKind::PureDuchon { .. }
1378 )
1379 }) || self.psi_scale_share != 0.0
1380 }
1381
1382 pub(crate) fn enforces_dense_materialization_budget(&self) -> bool {
1396 if self
1397 .streaming
1398 .as_ref()
1399 .is_some_and(|state| state.radial_kind.enforces_dense_materialization_budget())
1400 {
1401 return true;
1402 }
1403 self.psi_scale_share != 0.0
1409 }
1410
1411 pub fn p_out(&self) -> usize {
1413 if let Some(ref zf) = self.full_ident_transform {
1414 zf.ncols()
1415 } else {
1416 self.p_after_pad()
1417 }
1418 }
1419
1420 pub fn append_full_transform(mut self, transform: &Array2<f64>) -> Result<Self, BasisError> {
1421 if self.row_projection.is_some() {
1422 return Err(BasisError::InvalidInput(
1423 "implicit psi coefficient transforms must be composed before the fixed row-space projector is installed"
1424 .to_string(),
1425 ));
1426 }
1427 if transform.nrows() != self.p_out() {
1428 crate::bail_dim_basis!(
1429 "implicit psi derivative transform has {} rows but operator has {} output columns",
1430 transform.nrows(),
1431 self.p_out()
1432 );
1433 }
1434 self.full_ident_transform = Some(match self.full_ident_transform.take() {
1435 Some(existing) => fast_ab(&existing, transform),
1436 None => transform.clone(),
1437 });
1438 Ok(self)
1439 }
1440
1441 pub fn with_fixed_row_space_projection(
1449 mut self,
1450 projector: FixedRowSpaceProjector,
1451 ) -> Result<Self, BasisError> {
1452 if projector.nrows() != self.n {
1453 crate::bail_dim_basis!(
1454 "fixed row-space projector has {} rows but the implicit psi operator has {}",
1455 projector.nrows(),
1456 self.n
1457 );
1458 }
1459 if projector.rank() > 0 {
1460 self.row_projection = Some(Arc::new(ImplicitRowProjection::new(projector)));
1461 }
1462 Ok(self)
1463 }
1464
1465 fn projected_jet_correction(
1466 &self,
1467 key: ProjectedJetKey,
1468 ) -> Result<Option<Arc<Array2<f64>>>, BasisError> {
1469 let Some(row_projection) = self.row_projection.as_ref() else {
1470 return Ok(None);
1471 };
1472 let key = match key {
1473 ProjectedJetKey::SecondCross(left, right) if left > right => {
1474 ProjectedJetKey::SecondCross(right, left)
1475 }
1476 key => key,
1477 };
1478 if let Some(cached) = row_projection
1479 .corrections
1480 .lock()
1481 .unwrap_or_else(|poison| poison.into_inner())
1482 .get(&key)
1483 .cloned()
1484 {
1485 return Ok(Some(cached));
1486 }
1487
1488 let width = match key {
1489 ProjectedJetKey::FirstRaw(_) => self.n_knots,
1490 ProjectedJetKey::SecondDiagonal(_) | ProjectedJetKey::SecondCross(_, _) => self.p_out(),
1491 };
1492 let mut correction = Array2::<f64>::zeros((row_projection.projector.rank(), width));
1493 for basis_column in 0..row_projection.projector.rank() {
1494 let row_direction = row_projection.projector.range_basis.column(basis_column);
1495 let mut values = match key {
1496 ProjectedJetKey::FirstRaw(axis) => {
1497 self.transpose_mul_first_raw_unprojected(axis, &row_direction)?
1498 }
1499 ProjectedJetKey::SecondDiagonal(axis) => {
1500 self.transpose_mul_second_diag_unprojected(axis, &row_direction)?
1501 }
1502 ProjectedJetKey::SecondCross(left, right) => {
1503 self.transpose_mul_second_cross_unprojected(left, right, &row_direction)?
1504 }
1505 };
1506 if self.logarithmic_correction.is_some() {
1507 for start in (0..self.n).step_by(IMPLICIT_MATVEC_CHUNK_SIZE) {
1508 let end = (start + IMPLICIT_MATVEC_CHUNK_SIZE).min(self.n);
1509 let mut polynomial = Array2::<f64>::zeros((end - start, width));
1510 self.add_logarithmic_correction(key, start..end, &mut polynomial);
1511 values += &polynomial
1512 .t()
1513 .dot(&row_direction.slice(ndarray::s![start..end]));
1514 }
1515 }
1516 correction.row_mut(basis_column).assign(&values);
1517 }
1518 let correction = Arc::new(correction);
1519 let correction = row_projection
1520 .corrections
1521 .lock()
1522 .unwrap_or_else(|poison| poison.into_inner())
1523 .entry(key)
1524 .or_insert_with(|| Arc::clone(&correction))
1525 .clone();
1526 Ok(Some(correction))
1527 }
1528
1529 fn subtract_projected_row_chunk_correction(
1530 &self,
1531 key: ProjectedJetKey,
1532 rows: std::ops::Range<usize>,
1533 chunk: &mut Array2<f64>,
1534 ) -> Result<(), BasisError> {
1535 let Some(row_projection) = self.row_projection.as_ref() else {
1536 return Ok(());
1537 };
1538 let Some(correction) = self.projected_jet_correction(key)? else {
1539 return Ok(());
1540 };
1541 let removed = fast_ab(
1542 &row_projection.projector.range_basis.slice(s![rows, ..]),
1543 correction.as_ref(),
1544 );
1545 *chunk -= &removed;
1546 Ok(())
1547 }
1548
1549 pub(crate) fn p_after_pad(&self) -> usize {
1551 let p_constrained = self.p_constrained();
1552 p_constrained + self.n_poly
1553 }
1554
1555 pub(crate) fn p_constrained(&self) -> usize {
1557 match &self.ident_transform {
1558 Some(z) => z.ncols(),
1559 None => self.n_knots,
1560 }
1561 }
1562
1563 pub(crate) fn accumulate_knot_vector<F>(&self, v: &ArrayView1<f64>, per_pair: F) -> Array1<f64>
1569 where
1570 F: Fn(usize) -> f64 + Send + Sync,
1571 {
1572 let n = self.n;
1573 let k = self.n_knots;
1574
1575 if n >= IMPLICIT_MATVEC_PAR_THRESHOLD {
1576 let n_chunks = n.div_ceil(IMPLICIT_MATVEC_CHUNK_SIZE);
1578 let partial_sums: Vec<Array1<f64>> = (0..n_chunks)
1579 .into_par_iter()
1580 .map(|chunk_idx| {
1581 let start = chunk_idx * IMPLICIT_MATVEC_CHUNK_SIZE;
1582 let end = (start + IMPLICIT_MATVEC_CHUNK_SIZE).min(n);
1583 let mut local = Array1::<f64>::zeros(k);
1584 for i in start..end {
1585 let vi = v[i];
1586 if vi == 0.0 {
1587 continue;
1588 }
1589 let base = i * k;
1590 for j in 0..k {
1591 local[j] += vi * per_pair(base + j);
1592 }
1593 }
1594 local
1595 })
1596 .collect();
1597 let mut total = Array1::<f64>::zeros(k);
1598 for p in partial_sums {
1599 total += &p;
1600 }
1601 total
1602 } else {
1603 let mut total = Array1::<f64>::zeros(k);
1605 for i in 0..n {
1606 let vi = v[i];
1607 if vi == 0.0 {
1608 continue;
1609 }
1610 let base = i * k;
1611 for j in 0..k {
1612 total[j] += vi * per_pair(base + j);
1613 }
1614 }
1615 total
1616 }
1617 }
1618
1619 pub(crate) fn streaming_accumulate_knot_vector<G>(
1621 &self,
1622 v: &ArrayView1<f64>,
1623 deriv_fn: G,
1624 ) -> Result<Array1<f64>, BasisError>
1625 where
1626 G: Fn(f64, f64, f64, &[f64]) -> f64 + Send + Sync,
1627 {
1628 let Some(st) = self.streaming.as_ref() else {
1629 return Err(BasisError::InvalidInput(
1630 "streaming_accumulate_knot_vector needs the streaming radial state, but this implicit \
1631 ψ-derivative operator was built without one"
1632 .to_string(),
1633 ));
1634 };
1635 let (n, k, dim) = (self.n, self.n_knots, self.n_axes);
1636 if n >= IMPLICIT_MATVEC_PAR_THRESHOLD {
1637 let err_flag = std::sync::atomic::AtomicBool::new(false);
1638 let nc = n.div_ceil(IMPLICIT_MATVEC_CHUNK_SIZE);
1639 let ps: Vec<Array1<f64>> = (0..nc)
1640 .into_par_iter()
1641 .map(|ci| {
1642 let s = ci * IMPLICIT_MATVEC_CHUNK_SIZE;
1643 let e = (s + IMPLICIT_MATVEC_CHUNK_SIZE).min(n);
1644 let mut loc = Array1::<f64>::zeros(k);
1645 let mut sb = vec![0.0; dim];
1646 for i in s..e {
1647 let vi = v[i];
1648 if vi == 0.0 {
1649 continue;
1650 }
1651 for j in 0..k {
1652 match st.compute_pair(i, j, &mut sb) {
1653 Ok((phi, q, t)) => {
1654 loc[j] += vi * deriv_fn(phi, q, t, &sb);
1655 }
1656 Err(_) => {
1657 err_flag.store(true, std::sync::atomic::Ordering::Relaxed);
1658 return loc;
1659 }
1660 }
1661 }
1662 }
1663 loc
1664 })
1665 .collect();
1666 if err_flag.load(std::sync::atomic::Ordering::Relaxed) {
1667 crate::bail_invalid_basis!(
1668 "radial scalar evaluation failed during streaming accumulate_knot_vector"
1669 .into(),
1670 );
1671 }
1672 let mut tot = Array1::<f64>::zeros(k);
1673 for p in ps {
1674 tot += &p;
1675 }
1676 Ok(tot)
1677 } else {
1678 let mut tot = Array1::<f64>::zeros(k);
1679 let mut sb = vec![0.0; dim];
1680 for i in 0..n {
1681 let vi = v[i];
1682 if vi == 0.0 {
1683 continue;
1684 }
1685 for j in 0..k {
1686 let (phi, q, t) = st.compute_pair(i,j,&mut sb).map_err(|e| BasisError::InvalidInput(
1687 format!("radial scalar evaluation failed during streaming accumulate_knot_vector: {e}"),
1688 ))?;
1689 tot[j] += vi * deriv_fn(phi, q, t, &sb);
1690 }
1691 }
1692 Ok(tot)
1693 }
1694 }
1695 pub(crate) fn streaming_forward_mul<G>(
1697 &self,
1698 u_knot: &Array1<f64>,
1699 deriv_fn: G,
1700 ) -> Result<Array1<f64>, BasisError>
1701 where
1702 G: Fn(f64, f64, f64, &[f64]) -> f64 + Send + Sync,
1703 {
1704 let Some(st) = self.streaming.as_ref() else {
1705 return Err(BasisError::InvalidInput(
1706 "streaming_forward_mul needs the streaming radial state, but this implicit \
1707 ψ-derivative operator was built without one"
1708 .to_string(),
1709 ));
1710 };
1711 let (n, k, dim) = (self.n, self.n_knots, self.n_axes);
1712 if n >= IMPLICIT_MATVEC_PAR_THRESHOLD {
1713 let err_flag = std::sync::atomic::AtomicBool::new(false);
1714 let nc = n.div_ceil(IMPLICIT_MATVEC_CHUNK_SIZE);
1715 let cr: Vec<(usize, Vec<f64>)> = (0..nc)
1716 .into_par_iter()
1717 .map(|ci| {
1718 let s = ci * IMPLICIT_MATVEC_CHUNK_SIZE;
1719 let e = (s + IMPLICIT_MATVEC_CHUNK_SIZE).min(n);
1720 let mut loc = vec![0.0; e - s];
1721 let mut sb = vec![0.0; dim];
1722 for i in s..e {
1723 let mut val = 0.0;
1724 for j in 0..k {
1725 match st.compute_pair(i, j, &mut sb) {
1726 Ok((phi, q, t)) => {
1727 val += deriv_fn(phi, q, t, &sb) * u_knot[j];
1728 }
1729 Err(_) => {
1730 err_flag.store(true, std::sync::atomic::Ordering::Relaxed);
1731 break;
1732 }
1733 }
1734 }
1735 loc[i - s] = val;
1736 }
1737 (s, loc)
1738 })
1739 .collect();
1740 if err_flag.load(std::sync::atomic::Ordering::Relaxed) {
1741 crate::bail_invalid_basis!(
1742 "radial scalar evaluation failed during streaming forward_mul".into(),
1743 );
1744 }
1745 let mut res = Array1::<f64>::zeros(n);
1746 for (s, vs) in cr {
1747 for (o, &v) in vs.iter().enumerate() {
1748 res[s + o] = v;
1749 }
1750 }
1751 Ok(res)
1752 } else {
1753 let mut res = Array1::<f64>::zeros(n);
1754 let mut sb = vec![0.0; dim];
1755 for i in 0..n {
1756 let mut val = 0.0;
1757 for j in 0..k {
1758 let (phi, q, t) = st.compute_pair(i, j, &mut sb).map_err(|e| {
1759 BasisError::InvalidInput(format!(
1760 "radial scalar evaluation failed during streaming forward_mul: {e}"
1761 ))
1762 })?;
1763 val += deriv_fn(phi, q, t, &sb) * u_knot[j];
1764 }
1765 res[i] = val;
1766 }
1767 Ok(res)
1768 }
1769 }
1770 pub(crate) fn streaming_materialize<G>(&self, deriv_fn: G) -> Result<Array2<f64>, BasisError>
1772 where
1773 G: Fn(f64, f64, f64, &[f64]) -> f64 + Send + Sync,
1774 {
1775 let Some(st) = self.streaming.as_ref() else {
1776 return Err(BasisError::InvalidInput(
1777 "streaming_materialize needs the streaming radial state, but this implicit \
1778 ψ-derivative operator was built without one"
1779 .to_string(),
1780 ));
1781 };
1782 let (n, k, dim) = (self.n, self.n_knots, self.n_axes);
1783 let mut raw = Array2::<f64>::zeros((n, k));
1784 let cs = IMPLICIT_MATVEC_CHUNK_SIZE;
1785 let nc = n.div_ceil(cs);
1786 let err_flag = std::sync::atomic::AtomicBool::new(false);
1787 {
1788 let rp = SendPtr(raw.as_mut_ptr());
1789 let ef = &err_flag;
1790 (0..nc).into_par_iter().for_each(move |ci| {
1791 let s = ci * cs;
1792 let e = (s + cs).min(n);
1793 let mut sb = vec![0.0; dim];
1794 for i in s..e {
1795 for j in 0..k {
1796 match st.compute_pair(i, j, &mut sb) {
1797 Ok((phi, q, t)) => unsafe {
1801 *rp.add(i * k + j) = deriv_fn(phi, q, t, &sb);
1802 },
1803 Err(_) => {
1804 ef.store(true, std::sync::atomic::Ordering::Relaxed);
1805 return;
1806 }
1807 }
1808 }
1809 }
1810 });
1811 }
1812 if err_flag.load(std::sync::atomic::Ordering::Relaxed) {
1813 crate::bail_invalid_basis!(
1814 "radial scalar evaluation failed during streaming materialize".into(),
1815 );
1816 }
1817 Ok(self.project_matrix(raw))
1818 }
1819
1820 pub(crate) fn project_and_pad(&self, raw_knot_vec: &Array1<f64>) -> Array1<f64> {
1823 let constrained = match &self.ident_transform {
1825 Some(z) => z.t().dot(raw_knot_vec),
1826 None => raw_knot_vec.clone(),
1827 };
1828
1829 let p_padded = constrained.len() + self.n_poly;
1831 let mut padded = Array1::<f64>::zeros(p_padded);
1832 padded
1833 .slice_mut(s![..constrained.len()])
1834 .assign(&constrained);
1835
1836 match &self.full_ident_transform {
1838 Some(zf) => zf.t().dot(&padded),
1839 None => padded,
1840 }
1841 }
1842
1843 pub(crate) fn unproject(&self, u: &ArrayView1<f64>) -> Array1<f64> {
1846 let after_full = match &self.full_ident_transform {
1848 Some(zf) => zf.dot(u),
1849 None => u.to_owned(),
1850 };
1851
1852 let p_constrained = self.p_constrained();
1854 let smooth_part = after_full.slice(s![..p_constrained]);
1855
1856 match &self.ident_transform {
1858 Some(z) => z.dot(&smooth_part),
1859 None => smooth_part.to_owned(),
1860 }
1861 }
1862
1863 pub fn unproject_matrix(&self, u: &ArrayView2<f64>) -> Array2<f64> {
1869 assert_eq!(u.nrows(), self.p_out());
1870 let after_full = match &self.full_ident_transform {
1872 Some(zf) => fast_ab(zf, u),
1873 None => u.to_owned(),
1874 };
1875 let p_constrained = self.p_constrained();
1877 let smooth_part = after_full.slice(s![..p_constrained, ..]);
1878 match &self.ident_transform {
1880 Some(z) => fast_ab(z, &smooth_part),
1881 None => smooth_part.to_owned(),
1882 }
1883 }
1884
1885 pub fn transpose_mul(
1899 &self,
1900 axis: usize,
1901 v: &ArrayView1<f64>,
1902 ) -> Result<Array1<f64>, BasisError> {
1903 if self.logarithmic_correction.is_some() {
1904 return Ok(self.project_and_pad(
1905 &self.logarithmic_transpose(ProjectedJetKey::FirstRaw(axis), v)?,
1906 ));
1907 }
1908 if let Some(row_projection) = self.row_projection.as_ref() {
1909 let projected = row_projection.projector.project_vector_owned(v.to_owned());
1910 return self.transpose_mul_unprojected(axis, &projected.view());
1911 }
1912 self.transpose_mul_unprojected(axis, v)
1913 }
1914
1915 fn transpose_mul_unprojected(
1916 &self,
1917 axis: usize,
1918 v: &ArrayView1<f64>,
1919 ) -> Result<Array1<f64>, BasisError> {
1920 let raw = self.transpose_mul_first_raw_unprojected(axis, v)?;
1921 Ok(self.project_and_pad(&raw))
1922 }
1923
1924 fn transpose_mul_first_raw_unprojected(
1925 &self,
1926 axis: usize,
1927 v: &ArrayView1<f64>,
1928 ) -> Result<Array1<f64>, BasisError> {
1929 assert!(
1930 axis < self.n_axes(),
1931 "implicit psi first transpose axis out of bounds: axis={axis}, n_axes={}",
1932 self.n_axes()
1933 );
1934 assert_eq!(
1935 v.len(),
1936 self.n,
1937 "implicit psi first transpose row-adjoint length mismatch"
1938 );
1939 if self.axis_combinations.is_some() {
1940 let combo = self.transformed_axis_combination(axis);
1941 if self.is_streaming() {
1942 let scale = self.chart_scale;
1943 let g = self.effective_share(axis);
1944 let raw = self.streaming_accumulate_knot_vector(v, |phi, q, _, sb| {
1945 let s_combo = combo
1946 .iter()
1947 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
1948 .sum();
1949 Self::first_kernel_value(scale, phi, q, s_combo, g)
1950 })?;
1951 return Ok(raw);
1952 }
1953 let scale = self.chart_scale;
1954 let g = self.effective_share(axis);
1955 let raw = self.accumulate_knot_vector(v, |idx| {
1956 let s_combo = self.transformed_combo_axis_value_materialized(idx, combo);
1957 Self::first_kernel_value(
1958 scale,
1959 self.phi_values[idx],
1960 self.q_values[idx],
1961 s_combo,
1962 g,
1963 )
1964 });
1965 return Ok(raw);
1966 }
1967 if self.is_streaming() {
1968 let scale = self.chart_scale;
1969 let g = self.effective_share(axis);
1970 let raw = self.streaming_accumulate_knot_vector(v, |phi, q, _, sb| {
1971 Self::first_kernel_value(scale, phi, q, sb[axis], g)
1972 })?;
1973 return Ok(raw);
1974 }
1975 let scale = self.chart_scale;
1976 let g = self.effective_share(axis);
1977 let af = &self.axis_components;
1978 let pv = &self.phi_values;
1979 let qv = &self.q_values;
1980 let raw = self.accumulate_knot_vector(v, |idx| {
1981 Self::first_kernel_value(scale, pv[idx], qv[idx], af[[idx, axis]], g)
1982 });
1983 Ok(raw)
1984 }
1985
1986 pub fn forward_mul(&self, axis: usize, u: &ArrayView1<f64>) -> Result<Array1<f64>, BasisError> {
1994 if self.logarithmic_correction.is_some() {
1995 return self
1996 .logarithmic_forward(ProjectedJetKey::FirstRaw(axis), &self.unproject(u).view());
1997 }
1998 let values = self.forward_mul_unprojected(axis, u)?;
1999 Ok(match self.row_projection.as_ref() {
2000 Some(row_projection) => row_projection.projector.project_vector_owned(values),
2001 None => values,
2002 })
2003 }
2004
2005 fn forward_mul_unprojected(
2006 &self,
2007 axis: usize,
2008 u: &ArrayView1<f64>,
2009 ) -> Result<Array1<f64>, BasisError> {
2010 assert!(
2011 axis < self.n_axes(),
2012 "implicit psi first forward axis out of bounds: axis={axis}, n_axes={}",
2013 self.n_axes()
2014 );
2015 assert_eq!(
2016 u.len(),
2017 self.p_out(),
2018 "implicit psi first forward coefficient length mismatch"
2019 );
2020 let u_knot = self.unproject(u);
2021 if self.axis_combinations.is_some() {
2022 let combo = self.transformed_axis_combination(axis);
2023 if self.is_streaming() {
2024 let scale = self.chart_scale;
2025 let g = self.effective_share(axis);
2026 return self.streaming_forward_mul(&u_knot, |phi, q, _, sb| {
2027 let s_combo = combo
2028 .iter()
2029 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
2030 .sum();
2031 Self::first_kernel_value(scale, phi, q, s_combo, g)
2032 });
2033 }
2034 let n = self.n;
2035 let k = self.n_knots;
2036 let scale = self.chart_scale;
2037 let g = self.effective_share(axis);
2038 if n >= IMPLICIT_MATVEC_PAR_THRESHOLD {
2039 let mut result = Array1::<f64>::zeros(n);
2040 let n_chunks = n.div_ceil(IMPLICIT_MATVEC_CHUNK_SIZE);
2041 let chunk_results: Vec<(usize, Vec<f64>)> = (0..n_chunks)
2042 .into_par_iter()
2043 .map(|chunk_idx| {
2044 let start = chunk_idx * IMPLICIT_MATVEC_CHUNK_SIZE;
2045 let end = (start + IMPLICIT_MATVEC_CHUNK_SIZE).min(n);
2046 let mut local = vec![0.0; end - start];
2047 for i in start..end {
2048 let base = i * k;
2049 let mut val = 0.0;
2050 for j in 0..k {
2051 let idx = base + j;
2052 let s_combo =
2053 self.transformed_combo_axis_value_materialized(idx, combo);
2054 val += Self::first_kernel_value(
2055 scale,
2056 self.phi_values[idx],
2057 self.q_values[idx],
2058 s_combo,
2059 g,
2060 ) * u_knot[j];
2061 }
2062 local[i - start] = val;
2063 }
2064 (start, local)
2065 })
2066 .collect();
2067 for (start, vals) in chunk_results {
2068 for (offset, &v) in vals.iter().enumerate() {
2069 result[start + offset] = v;
2070 }
2071 }
2072 return Ok(result);
2073 }
2074 let mut result = Array1::<f64>::zeros(n);
2075 for i in 0..n {
2076 let base = i * k;
2077 let mut val = 0.0;
2078 for j in 0..k {
2079 let idx = base + j;
2080 let s_combo = self.transformed_combo_axis_value_materialized(idx, combo);
2081 val += Self::first_kernel_value(
2082 scale,
2083 self.phi_values[idx],
2084 self.q_values[idx],
2085 s_combo,
2086 g,
2087 ) * u_knot[j];
2088 }
2089 result[i] = val;
2090 }
2091 return Ok(result);
2092 }
2093 if self.is_streaming() {
2094 let scale = self.chart_scale;
2095 let g = self.effective_share(axis);
2096 return self.streaming_forward_mul(&u_knot, |phi, q, _, sb| {
2097 Self::first_kernel_value(scale, phi, q, sb[axis], g)
2098 });
2099 }
2100 let n = self.n;
2101 let k = self.n_knots;
2102 let scale = self.chart_scale;
2103 let g = self.effective_share(axis);
2104 let af = &self.axis_components;
2105 let pv = &self.phi_values;
2106 let qv = &self.q_values;
2107
2108 if n >= IMPLICIT_MATVEC_PAR_THRESHOLD {
2109 let mut result = Array1::<f64>::zeros(n);
2110 let n_chunks = n.div_ceil(IMPLICIT_MATVEC_CHUNK_SIZE);
2112 let chunk_results: Vec<(usize, Vec<f64>)> = (0..n_chunks)
2113 .into_par_iter()
2114 .map(|chunk_idx| {
2115 let start = chunk_idx * IMPLICIT_MATVEC_CHUNK_SIZE;
2116 let end = (start + IMPLICIT_MATVEC_CHUNK_SIZE).min(n);
2117 let mut local = vec![0.0; end - start];
2118 for i in start..end {
2119 let base = i * k;
2120 let mut val = 0.0;
2121 for j in 0..k {
2122 val += Self::first_kernel_value(
2123 scale,
2124 pv[base + j],
2125 qv[base + j],
2126 af[[base + j, axis]],
2127 g,
2128 ) * u_knot[j];
2129 }
2130 local[i - start] = val;
2131 }
2132 (start, local)
2133 })
2134 .collect();
2135 for (start, vals) in chunk_results {
2136 for (offset, &v) in vals.iter().enumerate() {
2137 result[start + offset] = v;
2138 }
2139 }
2140 Ok(result)
2141 } else {
2142 let mut result = Array1::<f64>::zeros(n);
2143 for i in 0..n {
2144 let base = i * k;
2145 let mut val = 0.0;
2146 for j in 0..k {
2147 val += Self::first_kernel_value(
2148 scale,
2149 pv[base + j],
2150 qv[base + j],
2151 af[[base + j, axis]],
2152 g,
2153 ) * u_knot[j];
2154 }
2155 result[i] = val;
2156 }
2157 Ok(result)
2158 }
2159 }
2160
2161 pub fn transpose_mul_second_diag(
2166 &self,
2167 axis: usize,
2168 v: &ArrayView1<f64>,
2169 ) -> Result<Array1<f64>, BasisError> {
2170 if self.logarithmic_correction.is_some() {
2171 return self.logarithmic_transpose(ProjectedJetKey::SecondDiagonal(axis), v);
2172 }
2173 if let Some(row_projection) = self.row_projection.as_ref() {
2174 let projected = row_projection.projector.project_vector_owned(v.to_owned());
2175 return self.transpose_mul_second_diag_unprojected(axis, &projected.view());
2176 }
2177 self.transpose_mul_second_diag_unprojected(axis, v)
2178 }
2179
2180 fn transpose_mul_second_diag_unprojected(
2181 &self,
2182 axis: usize,
2183 v: &ArrayView1<f64>,
2184 ) -> Result<Array1<f64>, BasisError> {
2185 assert!(
2186 axis < self.n_axes(),
2187 "implicit psi second diagonal transpose axis out of bounds: axis={axis}, n_axes={}",
2188 self.n_axes()
2189 );
2190 assert_eq!(
2191 v.len(),
2192 self.n,
2193 "implicit psi second diagonal transpose row-adjoint length mismatch"
2194 );
2195 if self.axis_combinations.is_some() {
2196 let combo = self.transformed_axis_combination(axis);
2197 if self.is_streaming() {
2198 let scale = self.chart_scale;
2199 let g = self.effective_share(axis);
2200 let lam = self.chart_lambda(axis, axis);
2201 let raw = self.streaming_accumulate_knot_vector(v, |phi, q, t, sb| {
2202 let s_combo = combo
2203 .iter()
2204 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
2205 .sum();
2206 let overlap_s = Self::transformed_combo_overlap_streaming(combo, combo, sb);
2207 Self::second_kernel_value(
2208 scale, phi, q, t, s_combo, s_combo, overlap_s, g, g, lam,
2209 )
2210 })?;
2211 return Ok(self.project_and_pad(&raw));
2212 }
2213 let scale = self.chart_scale;
2214 let g = self.effective_share(axis);
2215 let lam = self.chart_lambda(axis, axis);
2216 let raw = self.accumulate_knot_vector(v, |idx| {
2217 let s_combo = self.transformed_combo_axis_value_materialized(idx, combo);
2218 let overlap_s = self.transformed_combo_overlap_materialized(idx, combo, combo);
2219 Self::second_kernel_value(
2220 scale,
2221 self.phi_values[idx],
2222 self.q_values[idx],
2223 self.t_values[idx],
2224 s_combo,
2225 s_combo,
2226 overlap_s,
2227 g,
2228 g,
2229 lam,
2230 )
2231 });
2232 return Ok(self.project_and_pad(&raw));
2233 }
2234 if self.is_streaming() {
2235 let scale = self.chart_scale;
2236 let g = self.effective_share(axis);
2237 let lam = self.chart_lambda(axis, axis);
2238 let raw = self.streaming_accumulate_knot_vector(v, |phi, q, t, sb| {
2239 let s = sb[axis];
2240 Self::second_kernel_value(scale, phi, q, t, s, s, s, g, g, lam)
2241 })?;
2242 return Ok(self.project_and_pad(&raw));
2243 }
2244 let scale = self.chart_scale;
2245 let g = self.effective_share(axis);
2246 let lam = self.chart_lambda(axis, axis);
2247 let af = &self.axis_components;
2248 let pv = &self.phi_values;
2249 let qv = &self.q_values;
2250 let tv = &self.t_values;
2251 let raw = self.accumulate_knot_vector(v, |idx| {
2252 let s = af[[idx, axis]];
2253 Self::second_kernel_value(scale, pv[idx], qv[idx], tv[idx], s, s, s, g, g, lam)
2254 });
2255 Ok(self.project_and_pad(&raw))
2256 }
2257
2258 pub fn transpose_mul_second_cross(
2260 &self,
2261 axis_d: usize,
2262 axis_e: usize,
2263 v: &ArrayView1<f64>,
2264 ) -> Result<Array1<f64>, BasisError> {
2265 if self.logarithmic_correction.is_some() {
2266 return self.logarithmic_transpose(ProjectedJetKey::SecondCross(axis_d, axis_e), v);
2267 }
2268 if let Some(row_projection) = self.row_projection.as_ref() {
2269 let projected = row_projection.projector.project_vector_owned(v.to_owned());
2270 return self.transpose_mul_second_cross_unprojected(axis_d, axis_e, &projected.view());
2271 }
2272 self.transpose_mul_second_cross_unprojected(axis_d, axis_e, v)
2273 }
2274
2275 fn transpose_mul_second_cross_unprojected(
2276 &self,
2277 axis_d: usize,
2278 axis_e: usize,
2279 v: &ArrayView1<f64>,
2280 ) -> Result<Array1<f64>, BasisError> {
2281 assert!(
2282 axis_d < self.n_axes(),
2283 "implicit psi second cross transpose first axis out of bounds: axis_d={axis_d}, n_axes={}",
2284 self.n_axes()
2285 );
2286 assert!(
2287 axis_e < self.n_axes(),
2288 "implicit psi second cross transpose second axis out of bounds: axis_e={axis_e}, n_axes={}",
2289 self.n_axes()
2290 );
2291 assert_ne!(
2292 axis_d, axis_e,
2293 "implicit psi second cross transpose requires distinct axes: axis_d={axis_d}, axis_e={axis_e}"
2294 );
2295 assert_eq!(
2296 v.len(),
2297 self.n,
2298 "implicit psi second cross transpose row-adjoint length mismatch"
2299 );
2300 if self.axis_combinations.is_some() {
2301 let combo_d = self.transformed_axis_combination(axis_d);
2302 let combo_e = self.transformed_axis_combination(axis_e);
2303 if self.is_streaming() {
2304 let scale = self.chart_scale;
2305 let g_d = self.effective_share(axis_d);
2306 let g_e = self.effective_share(axis_e);
2307 let lam = self.chart_lambda(axis_d, axis_e);
2308 let raw = self.streaming_accumulate_knot_vector(v, |phi, q, t, sb| {
2309 let s_d = combo_d
2310 .iter()
2311 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
2312 .sum();
2313 let s_e = combo_e
2314 .iter()
2315 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
2316 .sum();
2317 let overlap_s = Self::transformed_combo_overlap_streaming(combo_d, combo_e, sb);
2318 Self::second_kernel_value(scale, phi, q, t, s_d, s_e, overlap_s, g_d, g_e, lam)
2319 })?;
2320 return Ok(self.project_and_pad(&raw));
2321 }
2322 let scale = self.chart_scale;
2323 let g_d = self.effective_share(axis_d);
2324 let g_e = self.effective_share(axis_e);
2325 let lam = self.chart_lambda(axis_d, axis_e);
2326 let raw = self.accumulate_knot_vector(v, |idx| {
2327 let s_d = self.transformed_combo_axis_value_materialized(idx, combo_d);
2328 let s_e = self.transformed_combo_axis_value_materialized(idx, combo_e);
2329 let overlap_s = self.transformed_combo_overlap_materialized(idx, combo_d, combo_e);
2330 Self::second_kernel_value(
2331 scale,
2332 self.phi_values[idx],
2333 self.q_values[idx],
2334 self.t_values[idx],
2335 s_d,
2336 s_e,
2337 overlap_s,
2338 g_d,
2339 g_e,
2340 lam,
2341 )
2342 });
2343 return Ok(self.project_and_pad(&raw));
2344 }
2345 if self.is_streaming() {
2346 let scale = self.chart_scale;
2347 let g_d = self.effective_share(axis_d);
2348 let g_e = self.effective_share(axis_e);
2349 let lam = self.chart_lambda(axis_d, axis_e);
2350 let raw = self.streaming_accumulate_knot_vector(v, |phi, q, t, sb| {
2351 Self::second_kernel_value(
2352 scale, phi, q, t, sb[axis_d], sb[axis_e], 0.0, g_d, g_e, lam,
2353 )
2354 })?;
2355 return Ok(self.project_and_pad(&raw));
2356 }
2357 let scale = self.chart_scale;
2358 let g_d = self.effective_share(axis_d);
2359 let g_e = self.effective_share(axis_e);
2360 let lam = self.chart_lambda(axis_d, axis_e);
2361 let af = &self.axis_components;
2362 let pv = &self.phi_values;
2363 let qv = &self.q_values;
2364 let tv = &self.t_values;
2365 let raw = self.accumulate_knot_vector(v, |idx| {
2366 Self::second_kernel_value(
2367 scale,
2368 pv[idx],
2369 qv[idx],
2370 tv[idx],
2371 af[[idx, axis_d]],
2372 af[[idx, axis_e]],
2373 0.0,
2374 g_d,
2375 g_e,
2376 lam,
2377 )
2378 });
2379 Ok(self.project_and_pad(&raw))
2380 }
2381
2382 pub fn forward_mul_second_diag(
2384 &self,
2385 axis: usize,
2386 u: &ArrayView1<f64>,
2387 ) -> Result<Array1<f64>, BasisError> {
2388 if self.logarithmic_correction.is_some() {
2389 return self.logarithmic_forward(ProjectedJetKey::SecondDiagonal(axis), u);
2390 }
2391 let values = self.forward_mul_second_diag_unprojected(axis, u)?;
2392 Ok(match self.row_projection.as_ref() {
2393 Some(row_projection) => row_projection.projector.project_vector_owned(values),
2394 None => values,
2395 })
2396 }
2397
2398 fn forward_mul_second_diag_unprojected(
2399 &self,
2400 axis: usize,
2401 u: &ArrayView1<f64>,
2402 ) -> Result<Array1<f64>, BasisError> {
2403 assert!(
2404 axis < self.n_axes(),
2405 "implicit psi second diagonal forward axis out of bounds: axis={axis}, n_axes={}",
2406 self.n_axes()
2407 );
2408 assert_eq!(
2409 u.len(),
2410 self.p_out(),
2411 "implicit psi second diagonal forward coefficient length mismatch"
2412 );
2413 let u_knot = self.unproject(u);
2414 if self.axis_combinations.is_some() {
2415 let combo = self.transformed_axis_combination(axis);
2416 if self.is_streaming() {
2417 let scale = self.chart_scale;
2418 let g = self.effective_share(axis);
2419 let lam = self.chart_lambda(axis, axis);
2420 return self.streaming_forward_mul(&u_knot, |phi, q, t, sb| {
2421 let s_combo = combo
2422 .iter()
2423 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
2424 .sum();
2425 let overlap_s = Self::transformed_combo_overlap_streaming(combo, combo, sb);
2426 Self::second_kernel_value(
2427 scale, phi, q, t, s_combo, s_combo, overlap_s, g, g, lam,
2428 )
2429 });
2430 }
2431 let n = self.n;
2432 let k = self.n_knots;
2433 let scale = self.chart_scale;
2434 let g = self.effective_share(axis);
2435 let lam = self.chart_lambda(axis, axis);
2436 let compute_row = |i: usize| -> f64 {
2437 let base = i * k;
2438 let mut val = 0.0;
2439 for j in 0..k {
2440 let idx = base + j;
2441 let s_combo = self.transformed_combo_axis_value_materialized(idx, combo);
2442 let overlap_s = self.transformed_combo_overlap_materialized(idx, combo, combo);
2443 val += Self::second_kernel_value(
2444 scale,
2445 self.phi_values[idx],
2446 self.q_values[idx],
2447 self.t_values[idx],
2448 s_combo,
2449 s_combo,
2450 overlap_s,
2451 g,
2452 g,
2453 lam,
2454 ) * u_knot[j];
2455 }
2456 val
2457 };
2458 if n >= IMPLICIT_MATVEC_PAR_THRESHOLD {
2459 let n_chunks = n.div_ceil(IMPLICIT_MATVEC_CHUNK_SIZE);
2460 let mut result = Array1::<f64>::zeros(n);
2461 let chunk_results: Vec<(usize, Vec<f64>)> = (0..n_chunks)
2462 .into_par_iter()
2463 .map(|chunk_idx| {
2464 let start = chunk_idx * IMPLICIT_MATVEC_CHUNK_SIZE;
2465 let end = (start + IMPLICIT_MATVEC_CHUNK_SIZE).min(n);
2466 let local: Vec<f64> = (start..end).map(compute_row).collect();
2467 (start, local)
2468 })
2469 .collect();
2470 for (start, vals) in chunk_results {
2471 for (offset, &value) in vals.iter().enumerate() {
2472 result[start + offset] = value;
2473 }
2474 }
2475 return Ok(result);
2476 }
2477 return Ok(Array1::from_vec((0..n).map(compute_row).collect()));
2478 }
2479 if self.is_streaming() {
2480 let scale = self.chart_scale;
2481 let g = self.effective_share(axis);
2482 let lam = self.chart_lambda(axis, axis);
2483 return self.streaming_forward_mul(&u_knot, |phi, q, t, sb| {
2484 let s = sb[axis];
2485 Self::second_kernel_value(scale, phi, q, t, s, s, s, g, g, lam)
2486 });
2487 }
2488 let n = self.n;
2489 let k = self.n_knots;
2490 let scale = self.chart_scale;
2491 let g = self.effective_share(axis);
2492 let lam = self.chart_lambda(axis, axis);
2493 let af = &self.axis_components;
2494 let pv = &self.phi_values;
2495 let qv = &self.q_values;
2496 let tv = &self.t_values;
2497 let compute_row = |i: usize| -> f64 {
2498 let base = i * k;
2499 let mut val = 0.0;
2500 for j in 0..k {
2501 let s = af[[base + j, axis]];
2502 val += Self::second_kernel_value(
2503 scale,
2504 pv[base + j],
2505 qv[base + j],
2506 tv[base + j],
2507 s,
2508 s,
2509 s,
2510 g,
2511 g,
2512 lam,
2513 ) * u_knot[j];
2514 }
2515 val
2516 };
2517
2518 if n >= IMPLICIT_MATVEC_PAR_THRESHOLD {
2519 let n_chunks = n.div_ceil(IMPLICIT_MATVEC_CHUNK_SIZE);
2520 let mut result = Array1::<f64>::zeros(n);
2521 let chunk_results: Vec<(usize, Vec<f64>)> = (0..n_chunks)
2522 .into_par_iter()
2523 .map(|chunk_idx| {
2524 let start = chunk_idx * IMPLICIT_MATVEC_CHUNK_SIZE;
2525 let end = (start + IMPLICIT_MATVEC_CHUNK_SIZE).min(n);
2526 let local: Vec<f64> = (start..end).map(compute_row).collect();
2527 (start, local)
2528 })
2529 .collect();
2530 for (start, vals) in chunk_results {
2531 for (offset, &value) in vals.iter().enumerate() {
2532 result[start + offset] = value;
2533 }
2534 }
2535 Ok(result)
2536 } else {
2537 Ok(Array1::from_vec((0..n).map(compute_row).collect()))
2538 }
2539 }
2540
2541 pub fn forward_mul_second_cross(
2543 &self,
2544 axis_d: usize,
2545 axis_e: usize,
2546 u: &ArrayView1<f64>,
2547 ) -> Result<Array1<f64>, BasisError> {
2548 if self.logarithmic_correction.is_some() {
2549 return self.logarithmic_forward(ProjectedJetKey::SecondCross(axis_d, axis_e), u);
2550 }
2551 let values = self.forward_mul_second_cross_unprojected(axis_d, axis_e, u)?;
2552 Ok(match self.row_projection.as_ref() {
2553 Some(row_projection) => row_projection.projector.project_vector_owned(values),
2554 None => values,
2555 })
2556 }
2557
2558 fn forward_mul_second_cross_unprojected(
2559 &self,
2560 axis_d: usize,
2561 axis_e: usize,
2562 u: &ArrayView1<f64>,
2563 ) -> Result<Array1<f64>, BasisError> {
2564 assert!(
2565 axis_d < self.n_axes(),
2566 "implicit psi second cross forward first axis out of bounds: axis_d={axis_d}, n_axes={}",
2567 self.n_axes()
2568 );
2569 assert!(
2570 axis_e < self.n_axes(),
2571 "implicit psi second cross forward second axis out of bounds: axis_e={axis_e}, n_axes={}",
2572 self.n_axes()
2573 );
2574 assert_ne!(
2575 axis_d, axis_e,
2576 "implicit psi second cross forward requires distinct axes: axis_d={axis_d}, axis_e={axis_e}"
2577 );
2578 assert_eq!(
2579 u.len(),
2580 self.p_out(),
2581 "implicit psi second cross forward coefficient length mismatch"
2582 );
2583 let u_knot = self.unproject(u);
2584 if self.axis_combinations.is_some() {
2585 let combo_d = self.transformed_axis_combination(axis_d);
2586 let combo_e = self.transformed_axis_combination(axis_e);
2587 if self.is_streaming() {
2588 let scale = self.chart_scale;
2589 let g_d = self.effective_share(axis_d);
2590 let g_e = self.effective_share(axis_e);
2591 let lam = self.chart_lambda(axis_d, axis_e);
2592 return self.streaming_forward_mul(&u_knot, |phi, q, t, sb| {
2593 let s_d = combo_d
2594 .iter()
2595 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
2596 .sum();
2597 let s_e = combo_e
2598 .iter()
2599 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
2600 .sum();
2601 let overlap_s = Self::transformed_combo_overlap_streaming(combo_d, combo_e, sb);
2602 Self::second_kernel_value(scale, phi, q, t, s_d, s_e, overlap_s, g_d, g_e, lam)
2603 });
2604 }
2605 let n = self.n;
2606 let k = self.n_knots;
2607 let scale = self.chart_scale;
2608 let g_d = self.effective_share(axis_d);
2609 let g_e = self.effective_share(axis_e);
2610 let lam = self.chart_lambda(axis_d, axis_e);
2611 let compute_row = |i: usize| -> f64 {
2612 let base = i * k;
2613 let mut val = 0.0;
2614 for j in 0..k {
2615 let idx = base + j;
2616 let s_d = self.transformed_combo_axis_value_materialized(idx, combo_d);
2617 let s_e = self.transformed_combo_axis_value_materialized(idx, combo_e);
2618 let overlap_s =
2619 self.transformed_combo_overlap_materialized(idx, combo_d, combo_e);
2620 val += Self::second_kernel_value(
2621 scale,
2622 self.phi_values[idx],
2623 self.q_values[idx],
2624 self.t_values[idx],
2625 s_d,
2626 s_e,
2627 overlap_s,
2628 g_d,
2629 g_e,
2630 lam,
2631 ) * u_knot[j];
2632 }
2633 val
2634 };
2635 if n >= IMPLICIT_MATVEC_PAR_THRESHOLD {
2636 let n_chunks = n.div_ceil(IMPLICIT_MATVEC_CHUNK_SIZE);
2637 let mut result = Array1::<f64>::zeros(n);
2638 let chunk_results: Vec<(usize, Vec<f64>)> = (0..n_chunks)
2639 .into_par_iter()
2640 .map(|chunk_idx| {
2641 let start = chunk_idx * IMPLICIT_MATVEC_CHUNK_SIZE;
2642 let end = (start + IMPLICIT_MATVEC_CHUNK_SIZE).min(n);
2643 let local: Vec<f64> = (start..end).map(compute_row).collect();
2644 (start, local)
2645 })
2646 .collect();
2647 for (start, vals) in chunk_results {
2648 for (offset, &value) in vals.iter().enumerate() {
2649 result[start + offset] = value;
2650 }
2651 }
2652 return Ok(result);
2653 }
2654 return Ok(Array1::from_vec((0..n).map(compute_row).collect()));
2655 }
2656 if self.is_streaming() {
2657 let scale = self.chart_scale;
2658 let g_d = self.effective_share(axis_d);
2659 let g_e = self.effective_share(axis_e);
2660 let lam = self.chart_lambda(axis_d, axis_e);
2661 return self.streaming_forward_mul(&u_knot, |phi, q, t, sb| {
2662 Self::second_kernel_value(
2663 scale, phi, q, t, sb[axis_d], sb[axis_e], 0.0, g_d, g_e, lam,
2664 )
2665 });
2666 }
2667 let n = self.n;
2668 let k = self.n_knots;
2669 let scale = self.chart_scale;
2670 let g_d = self.effective_share(axis_d);
2671 let g_e = self.effective_share(axis_e);
2672 let lam = self.chart_lambda(axis_d, axis_e);
2673 let af = &self.axis_components;
2674 let pv = &self.phi_values;
2675 let qv = &self.q_values;
2676 let tv = &self.t_values;
2677 let compute_row = |i: usize| -> f64 {
2678 let base = i * k;
2679 let mut val = 0.0;
2680 for j in 0..k {
2681 val += Self::second_kernel_value(
2682 scale,
2683 pv[base + j],
2684 qv[base + j],
2685 tv[base + j],
2686 af[[base + j, axis_d]],
2687 af[[base + j, axis_e]],
2688 0.0,
2689 g_d,
2690 g_e,
2691 lam,
2692 ) * u_knot[j];
2693 }
2694 val
2695 };
2696
2697 if n >= IMPLICIT_MATVEC_PAR_THRESHOLD {
2698 let n_chunks = n.div_ceil(IMPLICIT_MATVEC_CHUNK_SIZE);
2699 let mut result = Array1::<f64>::zeros(n);
2700 let chunk_results: Vec<(usize, Vec<f64>)> = (0..n_chunks)
2701 .into_par_iter()
2702 .map(|chunk_idx| {
2703 let start = chunk_idx * IMPLICIT_MATVEC_CHUNK_SIZE;
2704 let end = (start + IMPLICIT_MATVEC_CHUNK_SIZE).min(n);
2705 let local: Vec<f64> = (start..end).map(compute_row).collect();
2706 (start, local)
2707 })
2708 .collect();
2709 for (start, vals) in chunk_results {
2710 for (offset, &value) in vals.iter().enumerate() {
2711 result[start + offset] = value;
2712 }
2713 }
2714 Ok(result)
2715 } else {
2716 Ok(Array1::from_vec((0..n).map(compute_row).collect()))
2717 }
2718 }
2719
2720 pub fn materialize_first(&self, axis: usize) -> Result<Array2<f64>, BasisError> {
2727 if self.logarithmic_correction.is_some() {
2728 if self.enforces_dense_materialization_budget() {
2729 assert_no_dense_derivative_materialization(self.n, self.p_out(), self.n_axes());
2730 }
2731 return self.row_chunk_first(axis, 0..self.n);
2732 }
2733 assert!(
2734 axis < self.n_axes(),
2735 "implicit psi first materialization axis out of bounds: axis={axis}, n_axes={}",
2736 self.n_axes()
2737 );
2738 if self.enforces_dense_materialization_budget() {
2739 assert_no_dense_derivative_materialization(self.n, self.p_out(), self.n_axes());
2740 }
2741 if self.axis_combinations.is_some() {
2742 let combo = self.transformed_axis_combination(axis);
2743 if self.is_streaming() {
2744 let scale = self.chart_scale;
2745 let g = self.effective_share(axis);
2746 return self.streaming_materialize(|phi, q, _, sb| {
2747 let s_combo = combo
2748 .iter()
2749 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
2750 .sum();
2751 Self::first_kernel_value(scale, phi, q, s_combo, g)
2752 });
2753 }
2754 let n = self.n;
2755 let k = self.n_knots;
2756 let scale = self.chart_scale;
2757 let g = self.effective_share(axis);
2758 let mut raw = Array2::<f64>::zeros((n, k));
2759 for i in 0..n {
2760 let base = i * k;
2761 for j in 0..k {
2762 let idx = base + j;
2763 let s_combo = self.transformed_combo_axis_value_materialized(idx, combo);
2764 raw[[i, j]] = Self::first_kernel_value(
2765 scale,
2766 self.phi_values[idx],
2767 self.q_values[idx],
2768 s_combo,
2769 g,
2770 );
2771 }
2772 }
2773 return Ok(self.project_matrix(raw));
2774 }
2775 if self.is_streaming() {
2776 let scale = self.chart_scale;
2777 let g = self.effective_share(axis);
2778 return self.streaming_materialize(|phi, q, _, sb| {
2779 Self::first_kernel_value(scale, phi, q, sb[axis], g)
2780 });
2781 }
2782 let n = self.n;
2783 let k = self.n_knots;
2784 let scale = self.chart_scale;
2785 let g = self.effective_share(axis);
2786 let mut raw = Array2::<f64>::zeros((n, k));
2787 for i in 0..n {
2788 let base = i * k;
2789 for j in 0..k {
2790 raw[[i, j]] = Self::first_kernel_value(
2791 scale,
2792 self.phi_values[base + j],
2793 self.q_values[base + j],
2794 self.axis_components[[base + j, axis]],
2795 g,
2796 );
2797 }
2798 }
2799 Ok(self.project_matrix(raw))
2800 }
2801
2802 pub fn materialize_second_diag(&self, axis: usize) -> Result<Array2<f64>, BasisError> {
2804 if self.logarithmic_correction.is_some() {
2805 if self.enforces_dense_materialization_budget() {
2806 assert_no_dense_derivative_materialization(self.n, self.p_out(), self.n_axes());
2807 }
2808 return self.row_chunk_second_diag(axis, 0..self.n);
2809 }
2810 assert!(
2811 axis < self.n_axes(),
2812 "implicit psi second diagonal materialization axis out of bounds: axis={axis}, n_axes={}",
2813 self.n_axes()
2814 );
2815 if self.enforces_dense_materialization_budget() {
2816 assert_no_dense_derivative_materialization(self.n, self.p_out(), self.n_axes());
2817 }
2818 if self.axis_combinations.is_some() {
2819 let combo = self.transformed_axis_combination(axis);
2820 if self.is_streaming() {
2821 let scale = self.chart_scale;
2822 let g = self.effective_share(axis);
2823 let lam = self.chart_lambda(axis, axis);
2824 return self.streaming_materialize(|phi, q, t, sb| {
2825 let s_combo = combo
2826 .iter()
2827 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
2828 .sum();
2829 let overlap_s = Self::transformed_combo_overlap_streaming(combo, combo, sb);
2830 Self::second_kernel_value(
2831 scale, phi, q, t, s_combo, s_combo, overlap_s, g, g, lam,
2832 )
2833 });
2834 }
2835 let n = self.n;
2836 let k = self.n_knots;
2837 let scale = self.chart_scale;
2838 let g = self.effective_share(axis);
2839 let lam = self.chart_lambda(axis, axis);
2840 let mut raw = Array2::<f64>::zeros((n, k));
2841 for i in 0..n {
2842 let base = i * k;
2843 for j in 0..k {
2844 let idx = base + j;
2845 let s_combo = self.transformed_combo_axis_value_materialized(idx, combo);
2846 let overlap_s = self.transformed_combo_overlap_materialized(idx, combo, combo);
2847 raw[[i, j]] = Self::second_kernel_value(
2848 scale,
2849 self.phi_values[idx],
2850 self.q_values[idx],
2851 self.t_values[idx],
2852 s_combo,
2853 s_combo,
2854 overlap_s,
2855 g,
2856 g,
2857 lam,
2858 );
2859 }
2860 }
2861 return Ok(self.project_matrix(raw));
2862 }
2863 if self.is_streaming() {
2864 let scale = self.chart_scale;
2865 let g = self.effective_share(axis);
2866 let lam = self.chart_lambda(axis, axis);
2867 return self.streaming_materialize(|phi, q, t, sb| {
2868 let s = sb[axis];
2869 Self::second_kernel_value(scale, phi, q, t, s, s, s, g, g, lam)
2870 });
2871 }
2872 let n = self.n;
2873 let k = self.n_knots;
2874 let scale = self.chart_scale;
2875 let g = self.effective_share(axis);
2876 let lam = self.chart_lambda(axis, axis);
2877 let mut raw = Array2::<f64>::zeros((n, k));
2878 for i in 0..n {
2879 let base = i * k;
2880 for j in 0..k {
2881 let s = self.axis_components[[base + j, axis]];
2882 raw[[i, j]] = Self::second_kernel_value(
2883 scale,
2884 self.phi_values[base + j],
2885 self.q_values[base + j],
2886 self.t_values[base + j],
2887 s,
2888 s,
2889 s,
2890 g,
2891 g,
2892 lam,
2893 );
2894 }
2895 }
2896 Ok(self.project_matrix(raw))
2897 }
2898
2899 pub fn materialize_second_cross(
2903 &self,
2904 axis_d: usize,
2905 axis_e: usize,
2906 ) -> Result<Array2<f64>, BasisError> {
2907 if self.logarithmic_correction.is_some() {
2908 if self.enforces_dense_materialization_budget() {
2909 assert_no_dense_derivative_materialization(self.n, self.p_out(), self.n_axes());
2910 }
2911 return self.row_chunk_second_cross(axis_d, axis_e, 0..self.n);
2912 }
2913 assert!(
2914 axis_d < self.n_axes(),
2915 "implicit psi second cross materialization first axis out of bounds: axis_d={axis_d}, n_axes={}",
2916 self.n_axes()
2917 );
2918 assert!(
2919 axis_e < self.n_axes(),
2920 "implicit psi second cross materialization second axis out of bounds: axis_e={axis_e}, n_axes={}",
2921 self.n_axes()
2922 );
2923 assert_ne!(
2924 axis_d, axis_e,
2925 "implicit psi second cross materialization requires distinct axes: axis_d={axis_d}, axis_e={axis_e}"
2926 );
2927 if self.enforces_dense_materialization_budget() {
2928 assert_no_dense_derivative_materialization(self.n, self.p_out(), self.n_axes());
2929 }
2930 if self.axis_combinations.is_some() {
2931 let combo_d = self.transformed_axis_combination(axis_d);
2932 let combo_e = self.transformed_axis_combination(axis_e);
2933 if self.is_streaming() {
2934 let scale = self.chart_scale;
2935 let g_d = self.effective_share(axis_d);
2936 let g_e = self.effective_share(axis_e);
2937 let lam = self.chart_lambda(axis_d, axis_e);
2938 return self.streaming_materialize(|phi, q, t, sb| {
2939 let s_d = combo_d
2940 .iter()
2941 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
2942 .sum();
2943 let s_e = combo_e
2944 .iter()
2945 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
2946 .sum();
2947 let overlap_s = Self::transformed_combo_overlap_streaming(combo_d, combo_e, sb);
2948 Self::second_kernel_value(scale, phi, q, t, s_d, s_e, overlap_s, g_d, g_e, lam)
2949 });
2950 }
2951 let n = self.n;
2952 let k = self.n_knots;
2953 let scale = self.chart_scale;
2954 let g_d = self.effective_share(axis_d);
2955 let g_e = self.effective_share(axis_e);
2956 let lam = self.chart_lambda(axis_d, axis_e);
2957 let mut raw = Array2::<f64>::zeros((n, k));
2958 for i in 0..n {
2959 let base = i * k;
2960 for j in 0..k {
2961 let idx = base + j;
2962 let s_d = self.transformed_combo_axis_value_materialized(idx, combo_d);
2963 let s_e = self.transformed_combo_axis_value_materialized(idx, combo_e);
2964 let overlap_s =
2965 self.transformed_combo_overlap_materialized(idx, combo_d, combo_e);
2966 raw[[i, j]] = Self::second_kernel_value(
2967 scale,
2968 self.phi_values[idx],
2969 self.q_values[idx],
2970 self.t_values[idx],
2971 s_d,
2972 s_e,
2973 overlap_s,
2974 g_d,
2975 g_e,
2976 lam,
2977 );
2978 }
2979 }
2980 return Ok(self.project_matrix(raw));
2981 }
2982 if self.is_streaming() {
2983 let scale = self.chart_scale;
2984 let g_d = self.effective_share(axis_d);
2985 let g_e = self.effective_share(axis_e);
2986 let lam = self.chart_lambda(axis_d, axis_e);
2987 return self.streaming_materialize(|phi, q, t, sb| {
2988 Self::second_kernel_value(
2989 scale, phi, q, t, sb[axis_d], sb[axis_e], 0.0, g_d, g_e, lam,
2990 )
2991 });
2992 }
2993 let n = self.n;
2994 let k = self.n_knots;
2995 let scale = self.chart_scale;
2996 let g_d = self.effective_share(axis_d);
2997 let g_e = self.effective_share(axis_e);
2998 let lam = self.chart_lambda(axis_d, axis_e);
2999 let mut raw = Array2::<f64>::zeros((n, k));
3000 for i in 0..n {
3001 let base = i * k;
3002 for j in 0..k {
3003 raw[[i, j]] = Self::second_kernel_value(
3004 scale,
3005 self.phi_values[base + j],
3006 self.q_values[base + j],
3007 self.t_values[base + j],
3008 self.axis_components[[base + j, axis_d]],
3009 self.axis_components[[base + j, axis_e]],
3010 0.0,
3011 g_d,
3012 g_e,
3013 lam,
3014 );
3015 }
3016 }
3017 Ok(self.project_matrix(raw))
3018 }
3019
3020 pub(crate) fn project_matrix(&self, raw: Array2<f64>) -> Array2<f64> {
3023 let constrained = match &self.ident_transform {
3025 Some(z) => fast_ab(&raw, z),
3026 None => raw,
3027 };
3028
3029 let padded = if self.n_poly > 0 {
3031 let cols = constrained.ncols();
3032 let mut out = Array2::<f64>::zeros((self.n, cols + self.n_poly));
3033 out.slice_mut(s![.., ..cols]).assign(&constrained);
3034 out
3035 } else {
3036 constrained
3037 };
3038
3039 let projected = match &self.full_ident_transform {
3041 Some(zf) => fast_ab(&padded, zf),
3042 None => padded,
3043 };
3044 match self.row_projection.as_ref() {
3045 Some(row_projection) => row_projection.projector.project_matrix_owned(projected),
3046 None => projected,
3047 }
3048 }
3049
3050 pub(crate) fn project_matrix_rows(&self, raw: Array2<f64>) -> Array2<f64> {
3051 let nrows = raw.nrows();
3052 let constrained = match &self.ident_transform {
3053 Some(z) => fast_ab(&raw, z),
3054 None => raw,
3055 };
3056 let padded = if self.n_poly > 0 {
3057 let cols = constrained.ncols();
3058 let mut out = Array2::<f64>::zeros((nrows, cols + self.n_poly));
3059 out.slice_mut(s![.., ..cols]).assign(&constrained);
3060 out
3061 } else {
3062 constrained
3063 };
3064 match &self.full_ident_transform {
3065 Some(zf) => fast_ab(&padded, zf),
3066 None => padded,
3067 }
3068 }
3069
3070 pub(crate) fn row_chunk_with_kernel<G>(
3071 &self,
3072 rows: std::ops::Range<usize>,
3073 deriv_fn: G,
3074 ) -> Result<Array2<f64>, BasisError>
3075 where
3076 G: Fn(f64, f64, f64, &[f64], usize) -> f64,
3077 {
3078 let raw = self.row_chunk_with_kernel_raw(rows, deriv_fn)?;
3079 Ok(self.project_matrix_rows(raw))
3080 }
3081
3082 pub(crate) fn row_chunk_with_kernel_raw<G>(
3088 &self,
3089 rows: std::ops::Range<usize>,
3090 deriv_fn: G,
3091 ) -> Result<Array2<f64>, BasisError>
3092 where
3093 G: Fn(f64, f64, f64, &[f64], usize) -> f64,
3094 {
3095 let mut raw = Array2::<f64>::zeros((rows.end - rows.start, self.n_knots));
3096 if let Some(st) = self.streaming.as_ref() {
3097 let mut sb = vec![0.0; self.n_axes];
3098 if let Some(cache) = st.ensure_triplet_cache() {
3099 for (local, i) in rows.enumerate() {
3100 let base = i * self.n_knots;
3101 for j in 0..self.n_knots {
3102 let idx = base + j;
3103 st.fill_s_buf(i, j, &mut sb);
3104 raw[[local, j]] =
3105 deriv_fn(cache.phi[idx], cache.q[idx], cache.t[idx], &sb, idx);
3106 }
3107 }
3108 } else {
3109 for (local, i) in rows.enumerate() {
3110 for j in 0..self.n_knots {
3111 let (phi, q, t) = st.compute_pair(i, j, &mut sb)?;
3112 raw[[local, j]] = deriv_fn(phi, q, t, &sb, i * self.n_knots + j);
3113 }
3114 }
3115 }
3116 } else {
3117 for (local, i) in rows.enumerate() {
3118 let base = i * self.n_knots;
3119 for j in 0..self.n_knots {
3120 let idx = base + j;
3121 raw[[local, j]] = deriv_fn(
3122 self.phi_values[idx],
3123 self.q_values[idx],
3124 self.t_values[idx],
3125 &[],
3126 idx,
3127 );
3128 }
3129 }
3130 }
3131 Ok(raw)
3132 }
3133
3134 pub fn row_chunk_first(
3135 &self,
3136 axis: usize,
3137 rows: std::ops::Range<usize>,
3138 ) -> Result<Array2<f64>, BasisError> {
3139 let raw = self.row_chunk_first_raw(axis, rows)?;
3140 Ok(self.project_matrix_rows(raw))
3141 }
3142
3143 pub fn row_chunk_first_raw(
3150 &self,
3151 axis: usize,
3152 rows: std::ops::Range<usize>,
3153 ) -> Result<Array2<f64>, BasisError> {
3154 assert!(
3155 axis < self.n_axes(),
3156 "implicit psi first raw row chunk axis out of bounds: axis={axis}, n_axes={}",
3157 self.n_axes()
3158 );
3159 let scale = self.chart_scale;
3160 let g = self.effective_share(axis);
3161 let mut raw = if self.axis_combinations.is_some() {
3162 let combo = self.transformed_axis_combination(axis);
3163 self.row_chunk_with_kernel_raw(rows.clone(), |phi, q, _, sb, idx| {
3164 let s_combo = if sb.is_empty() {
3165 self.transformed_combo_axis_value_materialized(idx, combo)
3166 } else {
3167 combo
3168 .iter()
3169 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
3170 .sum()
3171 };
3172 Self::first_kernel_value(scale, phi, q, s_combo, g)
3173 })?
3174 } else {
3175 self.row_chunk_with_kernel_raw(rows.clone(), |phi, q, _, sb, idx| {
3176 let s = if sb.is_empty() {
3177 self.axis_components[[idx, axis]]
3178 } else {
3179 sb[axis]
3180 };
3181 Self::first_kernel_value(scale, phi, q, s, g)
3182 })?
3183 };
3184 self.add_logarithmic_correction(ProjectedJetKey::FirstRaw(axis), rows.clone(), &mut raw);
3185 self.subtract_projected_row_chunk_correction(
3186 ProjectedJetKey::FirstRaw(axis),
3187 rows,
3188 &mut raw,
3189 )?;
3190 Ok(raw)
3191 }
3192
3193 pub fn row_chunk_second_diag(
3194 &self,
3195 axis: usize,
3196 rows: std::ops::Range<usize>,
3197 ) -> Result<Array2<f64>, BasisError> {
3198 assert!(
3199 axis < self.n_axes(),
3200 "implicit psi second diagonal row chunk axis out of bounds: axis={axis}, n_axes={}",
3201 self.n_axes()
3202 );
3203 let scale = self.chart_scale;
3204 let g = self.effective_share(axis);
3205 let lam = self.chart_lambda(axis, axis);
3206 let mut chunk = if self.axis_combinations.is_some() {
3207 let combo = self.transformed_axis_combination(axis);
3208 self.row_chunk_with_kernel(rows.clone(), |phi, q, t, sb, idx| {
3209 let s_combo = if sb.is_empty() {
3210 self.transformed_combo_axis_value_materialized(idx, combo)
3211 } else {
3212 combo
3213 .iter()
3214 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
3215 .sum()
3216 };
3217 let overlap = if sb.is_empty() {
3218 self.transformed_combo_overlap_materialized(idx, combo, combo)
3219 } else {
3220 Self::transformed_combo_overlap_streaming(combo, combo, sb)
3221 };
3222 Self::second_kernel_value(scale, phi, q, t, s_combo, s_combo, overlap, g, g, lam)
3223 })?
3224 } else {
3225 self.row_chunk_with_kernel(rows.clone(), |phi, q, t, sb, idx| {
3226 let s = if sb.is_empty() {
3227 self.axis_components[[idx, axis]]
3228 } else {
3229 sb[axis]
3230 };
3231 Self::second_kernel_value(scale, phi, q, t, s, s, s, g, g, lam)
3232 })?
3233 };
3234 self.add_logarithmic_correction(
3235 ProjectedJetKey::SecondDiagonal(axis),
3236 rows.clone(),
3237 &mut chunk,
3238 );
3239 self.subtract_projected_row_chunk_correction(
3240 ProjectedJetKey::SecondDiagonal(axis),
3241 rows,
3242 &mut chunk,
3243 )?;
3244 Ok(chunk)
3245 }
3246
3247 pub fn row_chunk_second_cross(
3248 &self,
3249 axis_d: usize,
3250 axis_e: usize,
3251 rows: std::ops::Range<usize>,
3252 ) -> Result<Array2<f64>, BasisError> {
3253 assert!(
3254 axis_d < self.n_axes(),
3255 "implicit psi second cross row chunk first axis out of bounds: axis_d={axis_d}, n_axes={}",
3256 self.n_axes()
3257 );
3258 assert!(
3259 axis_e < self.n_axes(),
3260 "implicit psi second cross row chunk second axis out of bounds: axis_e={axis_e}, n_axes={}",
3261 self.n_axes()
3262 );
3263 assert_ne!(
3264 axis_d, axis_e,
3265 "implicit psi second cross row chunk requires distinct axes: axis_d={axis_d}, axis_e={axis_e}"
3266 );
3267 let scale = self.chart_scale;
3268 let g_d = self.effective_share(axis_d);
3269 let g_e = self.effective_share(axis_e);
3270 let lam = self.chart_lambda(axis_d, axis_e);
3271 let mut chunk = if self.axis_combinations.is_some() {
3272 let combo_d = self.transformed_axis_combination(axis_d);
3273 let combo_e = self.transformed_axis_combination(axis_e);
3274 self.row_chunk_with_kernel(rows.clone(), |phi, q, t, sb, idx| {
3275 let s_d = if sb.is_empty() {
3276 self.transformed_combo_axis_value_materialized(idx, combo_d)
3277 } else {
3278 combo_d
3279 .iter()
3280 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
3281 .sum()
3282 };
3283 let s_e = if sb.is_empty() {
3284 self.transformed_combo_axis_value_materialized(idx, combo_e)
3285 } else {
3286 combo_e
3287 .iter()
3288 .map(|(raw_axis, coeff)| coeff * sb[*raw_axis])
3289 .sum()
3290 };
3291 let overlap = if sb.is_empty() {
3292 self.transformed_combo_overlap_materialized(idx, combo_d, combo_e)
3293 } else {
3294 Self::transformed_combo_overlap_streaming(combo_d, combo_e, sb)
3295 };
3296 Self::second_kernel_value(scale, phi, q, t, s_d, s_e, overlap, g_d, g_e, lam)
3297 })?
3298 } else {
3299 self.row_chunk_with_kernel(rows.clone(), |phi, q, t, sb, idx| {
3300 let sd = if sb.is_empty() {
3301 self.axis_components[[idx, axis_d]]
3302 } else {
3303 sb[axis_d]
3304 };
3305 let se = if sb.is_empty() {
3306 self.axis_components[[idx, axis_e]]
3307 } else {
3308 sb[axis_e]
3309 };
3310 Self::second_kernel_value(scale, phi, q, t, sd, se, 0.0, g_d, g_e, lam)
3311 })?
3312 };
3313 self.add_logarithmic_correction(
3314 ProjectedJetKey::SecondCross(axis_d, axis_e),
3315 rows.clone(),
3316 &mut chunk,
3317 );
3318 self.subtract_projected_row_chunk_correction(
3319 ProjectedJetKey::SecondCross(axis_d, axis_e),
3320 rows,
3321 &mut chunk,
3322 )?;
3323 Ok(chunk)
3324 }
3325
3326 pub fn row_vector_first_into(
3334 &self,
3335 axis: usize,
3336 row: usize,
3337 mut out: ArrayViewMut1<'_, f64>,
3338 ) -> Result<(), BasisError> {
3339 assert!(
3340 row < self.n,
3341 "implicit psi row-vector request out of bounds: row={row}, n={}",
3342 self.n
3343 );
3344 assert_eq!(
3345 out.len(),
3346 self.p_out(),
3347 "implicit psi row-vector output length mismatch"
3348 );
3349 let chunk = self.row_chunk_first(axis, row..row + 1)?;
3350 out.assign(&chunk.row(0));
3351 Ok(())
3352 }
3353
3354 pub(crate) fn transformed_axis_combination(&self, axis: usize) -> &[(usize, f64)] {
3355 self.axis_combinations
3356 .as_ref()
3357 .expect("transformed axis combinations")
3358 .get(axis)
3359 .map(Vec::as_slice)
3360 .expect("transformed axis index")
3361 }
3362
3363 #[inline]
3364 pub(crate) fn transformed_combo_axis_value_materialized(
3365 &self,
3366 idx: usize,
3367 combo: &[(usize, f64)],
3368 ) -> f64 {
3369 combo
3370 .iter()
3371 .map(|(raw_axis, coeff)| coeff * self.axis_components[[idx, *raw_axis]])
3372 .sum()
3373 }
3374
3375 #[inline]
3376 pub(crate) fn transformed_combo_overlap_streaming(
3377 combo_left: &[(usize, f64)],
3378 combo_right: &[(usize, f64)],
3379 sb: &[f64],
3380 ) -> f64 {
3381 let mut overlap = 0.0;
3382 for &(left_axis, left_coeff) in combo_left {
3383 for &(right_axis, right_coeff) in combo_right {
3384 if left_axis == right_axis {
3385 overlap += left_coeff * right_coeff * sb[left_axis];
3386 }
3387 }
3388 }
3389 overlap
3390 }
3391
3392 #[inline]
3393 pub(crate) fn transformed_combo_overlap_materialized(
3394 &self,
3395 idx: usize,
3396 combo_left: &[(usize, f64)],
3397 combo_right: &[(usize, f64)],
3398 ) -> f64 {
3399 let mut overlap = 0.0;
3400 for &(left_axis, left_coeff) in combo_left {
3401 for &(right_axis, right_coeff) in combo_right {
3402 if left_axis == right_axis {
3403 overlap += left_coeff * right_coeff * self.axis_components[[idx, left_axis]];
3404 }
3405 }
3406 }
3407 overlap
3408 }
3409
3410 #[inline]
3416 pub(crate) fn first_kernel_value(scale: f64, phi: f64, q: f64, s: f64, g: f64) -> f64 {
3417 if s == ALGEBRAIC_PER_AXIS_COMPONENT {
3418 scale * (q + g * phi)
3419 } else {
3420 scale * (q * s + g * phi)
3421 }
3422 }
3423
3424 #[inline]
3431 pub(crate) fn second_kernel_value(
3432 scale: f64,
3433 phi: f64,
3434 q: f64,
3435 t: f64,
3436 s_a: f64,
3437 s_b: f64,
3438 overlap: f64,
3439 g_a: f64,
3440 g_b: f64,
3441 lam: f64,
3442 ) -> f64 {
3443 if s_a == ALGEBRAIC_PER_AXIS_COMPONENT && s_b == ALGEBRAIC_PER_AXIS_COMPONENT {
3444 scale * (t + (g_a + g_b) * q + (g_a * g_b + lam) * phi)
3445 } else {
3446 scale
3447 * (t * s_a * s_b
3448 + 2.0 * q * overlap
3449 + q * (g_b * s_a + g_a * s_b)
3450 + (g_a * g_b + lam) * phi)
3451 }
3452 }
3453}
3454
3455#[derive(Clone, Copy, Debug)]
3461pub struct DesignKernelChart {
3462 pub scale: f64,
3463 pub reference_pair: Option<(usize, usize)>,
3464}
3465
3466impl DesignKernelChart {
3467 pub const IDENTITY: Self = Self {
3468 scale: 1.0,
3469 reference_pair: None,
3470 };
3471}
3472
3473#[derive(Clone, Debug)]
3476pub(crate) struct DesignChartJets {
3477 pub(crate) scale: f64,
3478 pub(crate) first: Vec<f64>,
3479 pub(crate) second: Array2<f64>,
3480}
3481
3482pub(crate) fn design_chart_jets(
3498 chart: DesignKernelChart,
3499 centers: ArrayView2<'_, f64>,
3500 eta: Option<&[f64]>,
3501 radial_kind: &RadialScalarKind,
3502 per_axis: bool,
3503 share_c: f64,
3504) -> Result<Option<DesignChartJets>, BasisError> {
3505 if chart.scale == 1.0 {
3506 return Ok(None);
3507 }
3508 let Some((i, j)) = chart.reference_pair else {
3509 return Err(BasisError::InvalidInput(format!(
3510 "design kernel chart is amplified (scale={}) but names no reference center pair",
3511 chart.scale
3512 )));
3513 };
3514 let dim = centers.ncols();
3515 let metric =
3516 centered_aniso_metric_weights(&eta.map(<[f64]>::to_vec).unwrap_or_else(|| vec![0.0; dim]));
3517 let mut components = vec![0.0_f64; dim];
3518 for a in 0..dim {
3519 let h = centers[[i, a]] - centers[[j, a]];
3520 components[a] = metric[a] * h * h;
3521 }
3522 let r2: f64 = components.iter().sum();
3523 let r = r2.sqrt();
3524 let (phi, q, t, scalar_component) = if per_axis {
3525 let (phi, q, t) = radial_kind.eval_design_triplet(r)?;
3526 (phi, q, t, r2)
3527 } else {
3528 radial_kind.eval_scalar_total_psi_carriers(r)?
3529 };
3530 if !(phi.is_finite() && phi != 0.0) {
3531 return Err(BasisError::InvalidInput(format!(
3532 "design kernel chart reference pair ({i}, {j}) at r={r:.6e} has kernel value {phi:e}; \
3533 the chart's log-derivative is undefined there"
3534 )));
3535 }
3536 let s_axes: Vec<f64> = if per_axis {
3537 components
3538 } else {
3539 vec![scalar_component]
3540 };
3541 let n_axes = s_axes.len();
3542 let (log_value, log_radial) = if per_axis {
3543 DuchonLogarithmicPsiCorrection::coefficients(radial_kind)
3544 .map(|coefficients| DuchonLogarithmicPsiCorrection::evaluate(&coefficients, r2))
3545 .unwrap_or((0.0, 0.0))
3546 } else {
3547 (0.0, 0.0)
3548 };
3549 let mut first = vec![0.0_f64; n_axes];
3550 for (a, &s_a) in s_axes.iter().enumerate() {
3551 let k_a = ImplicitDesignPsiDerivative::first_kernel_value(1.0, phi, q, s_a, share_c)
3552 + log_value / dim as f64;
3553 first[a] = -k_a / phi;
3554 }
3555 let mut second = Array2::<f64>::zeros((n_axes, n_axes));
3556 for (a, &s_a) in s_axes.iter().enumerate() {
3557 for (b, &s_b) in s_axes.iter().enumerate() {
3558 let overlap = if a == b { s_a } else { 0.0 };
3559 let k_ab = ImplicitDesignPsiDerivative::second_kernel_value(
3560 1.0, phi, q, t, s_a, s_b, overlap, share_c, share_c, 0.0,
3561 ) + (log_radial * (s_a + s_b) + 2.0 * share_c * log_value) / dim as f64;
3562 second[[a, b]] = -k_ab / phi + first[a] * first[b];
3563 }
3564 }
3565 Ok(Some(DesignChartJets {
3566 scale: chart.scale,
3567 first,
3568 second,
3569 }))
3570}
3571
3572fn install_design_chart(
3573 op: ImplicitDesignPsiDerivative,
3574 jets: &Option<DesignChartJets>,
3575) -> ImplicitDesignPsiDerivative {
3576 match jets {
3577 Some(jets) => op.with_kernel_chart(jets.scale, jets.first.clone(), jets.second.clone()),
3578 None => op,
3579 }
3580}
3581
3582pub(crate) fn build_aniso_design_psi_derivatives_shared(
3583 data: ArrayView2<'_, f64>,
3584 centers: ArrayView2<'_, f64>,
3585 eta: &[f64],
3586 p_final: usize,
3587 ident_transform: Option<Array2<f64>>,
3588 full_ident_transform: Option<Array2<f64>>,
3589 n_poly: usize,
3590 radial_kind: RadialScalarKind,
3591 chart: DesignKernelChart,
3592) -> Result<AnisoBasisPsiDerivatives, BasisError> {
3593 let n = data.nrows();
3594 let k = centers.nrows();
3595 let dim = data.ncols();
3596 if eta.len() != dim {
3597 crate::bail_dim_basis!(
3598 "aniso design derivatives: eta.len()={} != data dimension {dim}",
3599 eta.len()
3600 );
3601 }
3602 let chart_jets = design_chart_jets(
3603 chart,
3604 centers,
3605 Some(eta),
3606 &radial_kind,
3607 true,
3608 radial_kind.raw_psi_isotropic_share(),
3609 )?;
3610
3611 let logarithmic_correction =
3612 DuchonLogarithmicPsiCorrection::new(data, centers, eta, &radial_kind);
3613 let policy = gam_runtime::resource::ResourcePolicy::default_library();
3614 let force_operator = radial_kind.is_duchon_family();
3615 let dense_derivatives_exceed_budget =
3616 should_use_implicit_operators_with_policy(n, p_final, dim, &policy);
3617 let operator_only = force_operator || dense_derivatives_exceed_budget;
3618 let cache_radial_components = should_cache_implicit_radial_components(n, k, dim, &policy);
3619 if operator_only && !cache_radial_components {
3639 let op = ImplicitDesignPsiDerivative::new_streaming(
3640 shared_owned_data_matrix_from_view(data),
3641 shared_owned_centers_matrix_from_view(centers),
3642 eta.to_vec(),
3643 radial_kind,
3644 ident_transform,
3645 full_ident_transform,
3646 n_poly,
3647 );
3648 let op = install_design_chart(op, &chart_jets)
3649 .with_logarithmic_correction(logarithmic_correction);
3650 return Ok(AnisoBasisPsiDerivatives {
3651 design_first: Vec::new(),
3652 design_second_diag: Vec::new(),
3653 design_second_cross: Vec::new(),
3654 design_second_cross_pairs: Vec::new(),
3655 penalties_first: vec![Vec::new(); dim],
3656 penalties_second_diag: vec![Vec::new(); dim],
3657 penalties_cross_pairs: Vec::new(),
3658 penalties_cross_provider: None,
3659 implicit_operator: Some(op),
3660 });
3661 }
3662
3663 let nk = n.checked_mul(k).ok_or_else(|| {
3668 BasisError::InvalidInput("aniso radial cache has too many data-center pairs".to_string())
3669 })?;
3670 if nk.checked_mul(dim).is_none() {
3671 crate::bail_invalid_basis!("aniso radial cache axis component storage is too large");
3672 }
3673 let mut phi_values = Array1::<f64>::zeros(nk);
3674 let mut q_values = Array1::<f64>::zeros(nk);
3675 let mut t_values = Array1::<f64>::zeros(nk);
3676 let mut axis_components = Array2::<f64>::zeros((nk, dim));
3677
3678 let psi_scale_share = radial_kind.raw_psi_isotropic_share();
3679
3680 let cs = IMPLICIT_MATVEC_CHUNK_SIZE;
3681 let nc = n.div_ceil(cs);
3682 let first_err: std::sync::Mutex<Option<BasisError>> = std::sync::Mutex::new(None);
3691 let aniso_scales = aniso_axis_scales(eta);
3702 let profile = if nk >= RADIAL_PROFILE_MIN_PAIRS {
3703 let mut r_lo = f64::INFINITY;
3704 let mut r_hi = 0.0_f64;
3705 let mut drb = vec![0.0; dim];
3706 let mut cb = vec![0.0; dim];
3707 for i in 0..n {
3708 for a in 0..dim {
3709 drb[a] = data[[i, a]];
3710 }
3711 for j in 0..k {
3712 for a in 0..dim {
3713 cb[a] = centers[[j, a]];
3714 }
3715 let r = aniso_distance_with_scales(&drb, &cb, &aniso_scales);
3716 if r > 0.0 {
3717 r_lo = r_lo.min(r);
3718 r_hi = r_hi.max(r);
3719 }
3720 }
3721 }
3722 if r_lo.is_finite() && r_hi > r_lo {
3723 radial_profile::RadialProfile::build(&radial_kind, r_lo, r_hi)
3724 } else {
3725 None
3726 }
3727 } else {
3728 None
3729 };
3730 {
3731 let pp = SendPtr(phi_values.as_mut_ptr());
3732 let qp = SendPtr(q_values.as_mut_ptr());
3733 let tp = SendPtr(t_values.as_mut_ptr());
3734 let ap = SendPtr(axis_components.as_mut_ptr());
3735 let ferr = &first_err;
3736 let profile_ref = profile.as_ref();
3737 let aniso_scales_ref = &aniso_scales;
3738 (0..nc).into_par_iter().for_each(move |ci| {
3739 let start = ci * cs;
3740 let end = start.saturating_add(cs).min(n);
3741 let mut drb = vec![0.0; dim];
3742 let mut cb = vec![0.0; dim];
3743 let mut sv = vec![0.0; dim];
3744 for i in start..end {
3745 for a in 0..dim {
3746 drb[a] = data[[i, a]];
3747 }
3748 for j in 0..k {
3749 for a in 0..dim {
3750 cb[a] = centers[[j, a]];
3751 }
3752 let r = aniso_distance_and_components_with_scales(
3753 &drb,
3754 &cb,
3755 aniso_scales_ref,
3756 &mut sv,
3757 );
3758 let collision = if r == 0.0 {
3759 Some(radial_kind.eval_per_axis_psi_carriers(r))
3760 } else {
3761 None
3762 };
3763 let triplet = match collision {
3764 Some(result) => result,
3765 None => match profile_ref {
3766 Some(profile) => profile
3767 .eval_or_exact(&radial_kind, r)
3768 .map(|(phi, q, t)| (phi, q, t, false)),
3769 None => radial_kind
3770 .eval_design_triplet(r)
3771 .map(|(phi, q, t)| (phi, q, t, false)),
3772 },
3773 };
3774 let (phi, q, t, marked) = match triplet {
3775 Ok(p) => p,
3776 Err(e) => {
3777 let mut slot = ferr.lock().unwrap_or_else(|p| p.into_inner());
3778 if slot.is_none() {
3779 *slot = Some(e);
3780 }
3781 return;
3782 }
3783 };
3784 if marked {
3785 sv.fill(ALGEBRAIC_PER_AXIS_COMPONENT);
3786 }
3787 let flat = i * k + j;
3788 unsafe {
3792 *pp.add(flat) = phi;
3793 *qp.add(flat) = q;
3794 *tp.add(flat) = t;
3795 for a in 0..dim {
3796 *ap.add(flat * dim + a) = sv[a];
3797 }
3798 }
3799 }
3800 }
3801 });
3802 }
3803 if let Some(cause) = first_err.into_inner().unwrap_or_else(|p| p.into_inner()) {
3804 return Err(BasisError::InvalidInput(format!(
3805 "radial scalar evaluation failed during aniso derivative construction \
3806 (eta={eta:?}): {cause}"
3807 )));
3808 }
3809
3810 let op = ImplicitDesignPsiDerivative::new(
3811 phi_values,
3812 q_values,
3813 t_values,
3814 axis_components,
3815 ident_transform,
3816 full_ident_transform,
3817 n,
3818 k,
3819 n_poly,
3820 dim,
3821 )
3822 .with_psi_scale_share(psi_scale_share)
3823 .with_logarithmic_correction(logarithmic_correction);
3824 let op = install_design_chart(op, &chart_jets);
3825
3826 if operator_only {
3836 return Ok(AnisoBasisPsiDerivatives {
3837 design_first: Vec::new(),
3838 design_second_diag: Vec::new(),
3839 design_second_cross: Vec::new(),
3840 design_second_cross_pairs: Vec::new(),
3841 penalties_first: vec![Vec::new(); dim],
3842 penalties_second_diag: vec![Vec::new(); dim],
3843 penalties_cross_pairs: Vec::new(),
3844 penalties_cross_provider: None,
3845 implicit_operator: Some(op),
3846 });
3847 }
3848
3849 let design_first = (0..dim)
3850 .map(|a| op.materialize_first(a))
3851 .collect::<Result<Vec<_>, _>>()?;
3852 let design_second_diag = (0..dim)
3853 .map(|a| op.materialize_second_diag(a))
3854 .collect::<Result<Vec<_>, _>>()?;
3855
3856 Ok(AnisoBasisPsiDerivatives {
3857 design_first,
3858 design_second_diag,
3859 design_second_cross: Vec::new(),
3860 design_second_cross_pairs: Vec::new(),
3861 penalties_first: vec![Vec::new(); dim],
3862 penalties_second_diag: vec![Vec::new(); dim],
3863 penalties_cross_pairs: Vec::new(),
3864 penalties_cross_provider: None,
3865 implicit_operator: Some(op),
3866 })
3867}
3868
3869#[derive(Debug, Clone)]
3870pub(crate) struct ScalarDesignPsiDerivatives {
3871 pub(crate) design_first: Array2<f64>,
3872 pub(crate) design_second_diag: Array2<f64>,
3873 pub(crate) implicit_operator: Option<ImplicitDesignPsiDerivative>,
3874}
3875
3876pub(crate) fn build_scalar_design_psi_derivatives_shared(
3877 data: ArrayView2<'_, f64>,
3878 centers: ArrayView2<'_, f64>,
3879 fixed_eta: Option<&[f64]>,
3880 p_final: usize,
3881 ident_transform: Option<Array2<f64>>,
3882 full_ident_transform: Option<Array2<f64>>,
3883 n_poly: usize,
3884 radial_kind: RadialScalarKind,
3885 psi_scale_share: f64,
3886 chart: DesignKernelChart,
3887) -> Result<ScalarDesignPsiDerivatives, BasisError> {
3888 let n = data.nrows();
3889 let k = centers.nrows();
3890 let dim = data.ncols();
3891 if let Some(eta) = fixed_eta
3892 && eta.len() != dim
3893 {
3894 crate::bail_dim_basis!(
3895 "scalar design derivatives: eta.len()={} != data dimension {dim}",
3896 eta.len()
3897 );
3898 }
3899 let chart_jets = design_chart_jets(
3900 chart,
3901 centers,
3902 fixed_eta,
3903 &radial_kind,
3904 false,
3905 psi_scale_share,
3906 )?;
3907
3908 let policy = gam_runtime::resource::ResourcePolicy::default_library();
3909 let force_operator = radial_kind.is_duchon_family();
3910 let dense_derivatives_exceed_budget =
3911 should_use_implicit_operators_with_policy(n, p_final, 1, &policy);
3912 let operator_only = force_operator || dense_derivatives_exceed_budget;
3913 let cache_radial_components = should_cache_implicit_radial_components(n, k, 1, &policy);
3914 if operator_only && !cache_radial_components {
3915 let metric_eta = fixed_eta
3916 .map(|eta| eta.to_vec())
3917 .unwrap_or_else(|| vec![0.0; dim]);
3918 let op = ImplicitDesignPsiDerivative::new_streaming_scalar(
3919 shared_owned_data_matrix_from_view(data),
3920 shared_owned_centers_matrix_from_view(centers),
3921 metric_eta,
3922 radial_kind,
3923 ident_transform,
3924 full_ident_transform,
3925 n_poly,
3926 )
3927 .with_psi_scale_share(psi_scale_share);
3928 let op = install_design_chart(op, &chart_jets);
3929 return Ok(ScalarDesignPsiDerivatives {
3930 design_first: Array2::<f64>::zeros((0, 0)),
3931 design_second_diag: Array2::<f64>::zeros((0, 0)),
3932 implicit_operator: Some(op),
3933 });
3934 }
3935
3936 let nk = n.checked_mul(k).ok_or_else(|| {
3937 BasisError::InvalidInput("scalar radial cache has too many data-center pairs".to_string())
3938 })?;
3939 let mut phi_values = Array1::<f64>::zeros(nk);
3940 let mut q_values = Array1::<f64>::zeros(nk);
3941 let mut t_values = Array1::<f64>::zeros(nk);
3942 let mut axis_components = Array2::<f64>::zeros((nk, 1));
3943
3944 let cs = IMPLICIT_MATVEC_CHUNK_SIZE;
3945 let nc = n.div_ceil(cs);
3946 let first_err: std::sync::Mutex<Option<BasisError>> = std::sync::Mutex::new(None);
3947 let fixed_scales: Option<Vec<f64>> = fixed_eta.map(aniso_axis_scales);
3951 let pair_r = |i: usize, j: usize, drb: &mut [f64], cb: &mut [f64]| -> f64 {
3952 if let Some(scales) = fixed_scales.as_deref() {
3953 for a in 0..dim {
3954 drb[a] = data[[i, a]];
3955 cb[a] = centers[[j, a]];
3956 }
3957 aniso_distance_with_scales(drb, cb, scales)
3958 } else {
3959 stable_euclidean_norm((0..dim).map(|a| data[[i, a]] - centers[[j, a]]))
3960 }
3961 };
3962 let profile = if nk >= RADIAL_PROFILE_MIN_PAIRS {
3963 let mut r_lo = f64::INFINITY;
3964 let mut r_hi = 0.0_f64;
3965 let mut drb = vec![0.0; dim];
3966 let mut cb = vec![0.0; dim];
3967 for i in 0..n {
3968 for j in 0..k {
3969 let r = pair_r(i, j, &mut drb, &mut cb);
3970 if r > 0.0 {
3971 r_lo = r_lo.min(r);
3972 r_hi = r_hi.max(r);
3973 }
3974 }
3975 }
3976 if r_lo.is_finite() && r_hi > r_lo {
3977 radial_profile::RadialProfile::build(&radial_kind, r_lo, r_hi)
3978 } else {
3979 None
3980 }
3981 } else {
3982 None
3983 };
3984 {
3985 let pp = SendPtr(phi_values.as_mut_ptr());
3986 let qp = SendPtr(q_values.as_mut_ptr());
3987 let tp = SendPtr(t_values.as_mut_ptr());
3988 let ap = SendPtr(axis_components.as_mut_ptr());
3989 let ferr = &first_err;
3990 let profile_ref = profile.as_ref();
3991 let exact_scalar_carrier = matches!(
3992 radial_kind,
3993 RadialScalarKind::Duchon {
3994 p_order,
3995 s_order,
3996 dim,
3997 ..
3998 } if !duchon_hybrid_stable_integral_applies(p_order, s_order, dim)
3999 );
4000 (0..nc).into_par_iter().for_each(move |ci| {
4001 let start = ci * cs;
4002 let end = start.saturating_add(cs).min(n);
4003 let mut data_row_buf = vec![0.0; dim];
4004 let mut center_buf = vec![0.0; dim];
4005 let mut component_buf = vec![0.0; dim];
4006 for i in start..end {
4007 for a in 0..dim {
4008 data_row_buf[a] = data[[i, a]];
4009 }
4010 for j in 0..k {
4011 let (r, scalar_component) = if let Some(scales) = fixed_scales.as_deref() {
4012 for a in 0..dim {
4013 center_buf[a] = centers[[j, a]];
4014 }
4015 let r = aniso_distance_and_components_with_scales(
4016 &data_row_buf,
4017 ¢er_buf,
4018 scales,
4019 &mut component_buf,
4020 );
4021 (r, component_buf.iter().sum::<f64>())
4022 } else {
4023 let r =
4024 stable_euclidean_norm((0..dim).map(|a| data[[i, a]] - centers[[j, a]]));
4025 (r, r * r)
4026 };
4027 let carrier_triplet = if exact_scalar_carrier {
4028 radial_kind.eval_scalar_total_psi_carriers(r)
4029 } else {
4030 let triplet = match profile_ref {
4031 Some(profile) => profile.eval_or_exact(&radial_kind, r),
4032 None => radial_kind.eval_design_triplet(r),
4033 };
4034 triplet.map(|(phi, q, t)| (phi, q, t, scalar_component))
4035 };
4036 let (phi, q, t, carrier_component) = match carrier_triplet {
4037 Ok(p) => p,
4038 Err(e) => {
4039 let mut slot = ferr.lock().unwrap_or_else(|p| p.into_inner());
4040 if slot.is_none() {
4041 *slot = Some(e);
4042 }
4043 return;
4044 }
4045 };
4046 let flat = i * k + j;
4047 unsafe {
4051 *pp.add(flat) = phi;
4052 *qp.add(flat) = q;
4053 *tp.add(flat) = t;
4054 *ap.add(flat) = carrier_component;
4055 }
4056 }
4057 }
4058 });
4059 }
4060 if let Some(cause) = first_err.into_inner().unwrap_or_else(|p| p.into_inner()) {
4061 return Err(BasisError::InvalidInput(format!(
4062 "radial scalar evaluation failed during scalar derivative construction: {cause}"
4063 )));
4064 }
4065
4066 let op = ImplicitDesignPsiDerivative::new(
4067 phi_values,
4068 q_values,
4069 t_values,
4070 axis_components,
4071 ident_transform,
4072 full_ident_transform,
4073 n,
4074 k,
4075 n_poly,
4076 1,
4077 )
4078 .with_psi_scale_share(psi_scale_share);
4079 let op = install_design_chart(op, &chart_jets);
4080
4081 if operator_only {
4082 return Ok(ScalarDesignPsiDerivatives {
4083 design_first: Array2::<f64>::zeros((0, 0)),
4084 design_second_diag: Array2::<f64>::zeros((0, 0)),
4085 implicit_operator: Some(op),
4086 });
4087 }
4088
4089 Ok(ScalarDesignPsiDerivatives {
4090 design_first: op.materialize_first(0)?,
4091 design_second_diag: op.materialize_second_diag(0)?,
4092 implicit_operator: Some(op),
4093 })
4094}
4095
4096#[cfg(test)]
4097mod fixed_row_space_value_tests {
4098 use super::*;
4099
4100 fn frobenius(matrix: &Array2<f64>) -> f64 {
4101 matrix.iter().map(|value| value * value).sum::<f64>().sqrt()
4102 }
4103
4104 #[test]
4105 fn projected_value_design_exports_raw_constraint_correction() {
4106 let constraint = Array2::from_shape_vec(
4111 (5, 3),
4112 vec![
4113 1.0, -2.0, 7.0, 1.0, -1.0, 7.0, 1.0, 0.0, 7.0, 1.0, 1.0, 7.0, 1.0, 2.0, 7.0,
4114 ],
4115 )
4116 .expect("constraint shape");
4117 let value = Array2::from_shape_vec(
4118 (5, 2),
4119 vec![0.3, -1.0, 2.0, 0.5, -0.7, 3.0, 1.4, -0.2, 4.0, 1.1],
4120 )
4121 .expect("value shape");
4122 let projector =
4123 FixedRowSpaceProjector::from_constraint_block(constraint.view()).expect("projector");
4124 assert_eq!(projector.rank(), 2);
4125
4126 let mut expected = value.clone();
4127 projector
4128 .project_matrix_in_place(&mut expected)
4129 .expect("dense projection");
4130 let (lazy, correction) = projector
4131 .project_design(DesignMatrix::from(value.clone()), "unit value")
4132 .expect("lazy projection");
4133 let actual = lazy.to_dense();
4134 let reconstructed = &value - &constraint.dot(&correction);
4135 let scale = frobenius(&expected).max(1.0);
4136 assert!(
4137 frobenius(&(&actual - &expected)) / scale < 1.0e-12,
4138 "lazy value projection must equal the dense projector"
4139 );
4140 assert!(
4141 frobenius(&(&reconstructed - &expected)) / scale < 1.0e-12,
4142 "raw constraint correction must replay the same projected value"
4143 );
4144 assert!(
4145 frobenius(&constraint.t().dot(&actual))
4146 / (frobenius(&constraint) * frobenius(&actual)).max(1.0e-300)
4147 < 1.0e-12,
4148 "projected value must be collection-orthogonal"
4149 );
4150 }
4151}