1use super::*;
2
3pub(crate) fn as_implicit(op: &dyn HyperOperator) -> Option<&ImplicitHyperOperator> {
4 op.as_any().downcast_ref::<ImplicitHyperOperator>()
5}
6
7pub(crate) fn as_composite(op: &dyn HyperOperator) -> Option<&CompositeHyperOperator> {
8 op.as_any().downcast_ref::<CompositeHyperOperator>()
9}
10
11pub(crate) fn as_weighted(op: &dyn HyperOperator) -> Option<&WeightedHyperOperator> {
12 op.as_any().downcast_ref::<WeightedHyperOperator>()
13}
14
15pub(crate) trait DriftDerivTraceExt {
16 fn trace_logdet(&self, hop: &dyn HessianFactorization) -> f64;
17
18 fn trace_logdet_hessian_cross(&self, rhs: &Self, hop: &dyn HessianFactorization) -> f64;
19}
20
21impl DriftDerivTraceExt for DriftDerivResult {
22 fn trace_logdet(&self, hop: &dyn HessianFactorization) -> f64 {
23 match self {
24 Self::Dense(matrix) => hop.trace_logdet_gradient(matrix),
25 Self::Operator(operator) => hop.trace_logdet_operator(operator.as_ref()),
26 }
27 }
28
29 fn trace_logdet_hessian_cross(&self, rhs: &Self, hop: &dyn HessianFactorization) -> f64 {
30 match (self, rhs) {
31 (Self::Dense(left), Self::Dense(right)) => hop.trace_logdet_hessian_cross(left, right),
32 (Self::Dense(left), Self::Operator(right)) => {
33 hop.trace_logdet_hessian_cross_matrix_operator(left, right.as_ref())
34 }
35 (Self::Operator(left), Self::Dense(right)) => {
36 hop.trace_logdet_hessian_cross_matrix_operator(right, left.as_ref())
37 }
38 (Self::Operator(left), Self::Operator(right)) => {
39 hop.trace_logdet_hessian_cross_operator(left.as_ref(), right.as_ref())
40 }
41 }
42 }
43}
44
45#[derive(Clone)]
46pub struct CompositeHyperOperator {
47 pub dense: Option<Array2<f64>>,
48 pub operators: Vec<Arc<dyn HyperOperator>>,
49 pub dim_hint: usize,
50}
51
52pub(crate) fn composite_trace_implicit_batched(
60 operators: &[Arc<dyn HyperOperator>],
61 factor: &Array2<f64>,
62 cache: Option<&ProjectedFactorCache>,
63) -> f64 {
64 let mut trace = 0.0;
65 let mut group_starts: Vec<Vec<usize>> = Vec::new();
66 let mut handled = vec![false; operators.len()];
67
68 for (i, op) in operators.iter().enumerate() {
69 if handled[i] {
70 continue;
71 }
72 let Some(impl_i) = as_implicit(op.as_ref()) else {
73 continue;
74 };
75 let mut group = vec![i];
76 handled[i] = true;
77 for j in (i + 1)..operators.len() {
78 if handled[j] {
79 continue;
80 }
81 if let Some(impl_j) = as_implicit(operators[j].as_ref())
82 && Arc::ptr_eq(&impl_i.implicit_deriv, &impl_j.implicit_deriv)
83 && Arc::ptr_eq(&impl_i.x_design, &impl_j.x_design)
84 && Arc::ptr_eq(impl_i.w_diag.as_arc(), impl_j.w_diag.as_arc())
85 && impl_i.p == impl_j.p
86 {
87 group.push(j);
88 handled[j] = true;
89 }
90 }
91 group_starts.push(group);
92 }
93
94 for group in &group_starts {
95 if group.len() >= 2 {
96 let lead = as_implicit(operators[group[0]].as_ref())
97 .expect("a group leader is by construction an index that matched as_implicit");
98 let xf = match cache {
99 Some(c) => lead.cached_xf(factor, c),
100 None => Arc::new(lead.compute_xf(factor)),
101 };
102 let axes: Vec<(usize, &Array2<f64>, Option<&Array1<f64>>)> = group
103 .iter()
104 .map(|&k| {
105 let op = as_implicit(operators[k].as_ref()).expect(
106 "a group member is by construction an index that matched as_implicit",
107 );
108 (op.axis, &op.s_psi, op.c_x_psi_beta.as_deref())
109 })
110 .collect();
111 let values = lead.trace_projected_factor_all_axes_with_xf(factor, xf.view(), &axes);
112 trace += values.iter().sum::<f64>();
113 } else {
114 let op = &operators[group[0]];
115 trace += match cache {
116 Some(c) => op.trace_projected_factor_cached(factor, c),
117 None => op.trace_projected_factor(factor),
118 };
119 }
120 }
121
122 for (i, op) in operators.iter().enumerate() {
123 if handled[i] {
124 continue;
125 }
126 trace += match cache {
127 Some(c) => op.trace_projected_factor_cached(factor, c),
128 None => op.trace_projected_factor(factor),
129 };
130 }
131
132 trace
133}
134
135pub(crate) fn trace_projected_factors_batched(
140 operators: &[Arc<dyn HyperOperator>],
141 factor: &Array2<f64>,
142 cache: &ProjectedFactorCache,
143) -> Vec<f64> {
144 let mut out = vec![0.0; operators.len()];
145 let mut handled = vec![false; operators.len()];
146
147 for i in 0..operators.len() {
148 if handled[i] {
149 continue;
150 }
151 let Some(impl_i) = as_implicit(operators[i].as_ref()) else {
152 out[i] = operators[i].trace_projected_factor_cached(factor, cache);
153 handled[i] = true;
154 continue;
155 };
156
157 let mut group = vec![i];
158 handled[i] = true;
159 for j in (i + 1)..operators.len() {
160 if handled[j] {
161 continue;
162 }
163 if let Some(impl_j) = as_implicit(operators[j].as_ref())
164 && Arc::ptr_eq(&impl_i.implicit_deriv, &impl_j.implicit_deriv)
165 && Arc::ptr_eq(&impl_i.x_design, &impl_j.x_design)
166 && Arc::ptr_eq(impl_i.w_diag.as_arc(), impl_j.w_diag.as_arc())
167 && impl_i.p == impl_j.p
168 {
169 group.push(j);
170 handled[j] = true;
171 }
172 }
173
174 if group.len() >= 2 {
175 let xf = impl_i.cached_xf(factor, cache);
176 let axes: Vec<(usize, &Array2<f64>, Option<&Array1<f64>>)> = group
177 .iter()
178 .map(|&idx| {
179 let op = as_implicit(operators[idx].as_ref()).expect(
180 "a group member is by construction an index that matched as_implicit",
181 );
182 (op.axis, &op.s_psi, op.c_x_psi_beta.as_deref())
183 })
184 .collect();
185 let values = impl_i.trace_projected_factor_all_axes_with_xf(factor, xf.view(), &axes);
186 for (&idx, value) in group.iter().zip(values) {
187 out[idx] = value;
188 }
189 } else {
190 out[i] = operators[i].trace_projected_factor_cached(factor, cache);
191 }
192 }
193
194 out
195}
196
197pub(crate) fn collect_projected_trace_terms<'a>(
198 out_idx: usize,
199 weight: f64,
200 op: &'a dyn HyperOperator,
201 factor: &Array2<f64>,
202 dense_acc: &mut [f64],
203 terms: &mut Vec<(usize, f64, &'a dyn HyperOperator)>,
204) {
205 if weight == 0.0 {
206 return;
207 }
208 if let Some(composite) = as_composite(op) {
209 if let Some(dense) = composite.dense.as_ref() {
210 dense_acc[out_idx] += weight * dense_trace_projected_factor(dense, factor);
211 }
212 for inner in &composite.operators {
213 collect_projected_trace_terms(
214 out_idx,
215 weight,
216 inner.as_ref(),
217 factor,
218 dense_acc,
219 terms,
220 );
221 }
222 } else if let Some(weighted) = as_weighted(op) {
223 for (term_weight, inner) in &weighted.terms {
224 collect_projected_trace_terms(
225 out_idx,
226 weight * *term_weight,
227 inner.as_ref(),
228 factor,
229 dense_acc,
230 terms,
231 );
232 }
233 } else {
234 terms.push((out_idx, weight, op));
235 }
236}
237
238pub(crate) fn collect_projected_matrix_terms<'a>(
239 out_idx: usize,
240 weight: f64,
241 op: &'a dyn HyperOperator,
242 factor: &Array2<f64>,
243 dense_acc: &mut [Array2<f64>],
244 terms: &mut Vec<(usize, f64, &'a dyn HyperOperator)>,
245) {
246 if weight == 0.0 {
247 return;
248 }
249 if let Some(composite) = as_composite(op) {
250 if let Some(dense) = composite.dense.as_ref() {
251 dense_acc[out_idx].scaled_add(weight, &dense_projected_matrix(dense, factor));
252 }
253 for inner in &composite.operators {
254 collect_projected_matrix_terms(
255 out_idx,
256 weight,
257 inner.as_ref(),
258 factor,
259 dense_acc,
260 terms,
261 );
262 }
263 } else if let Some(weighted) = as_weighted(op) {
264 for (term_weight, inner) in &weighted.terms {
265 collect_projected_matrix_terms(
266 out_idx,
267 weight * *term_weight,
268 inner.as_ref(),
269 factor,
270 dense_acc,
271 terms,
272 );
273 }
274 } else {
275 terms.push((out_idx, weight, op));
276 }
277}
278
279pub(crate) fn trace_projected_operator_terms_batched(
280 n_out: usize,
281 terms: &[(usize, f64, &dyn HyperOperator)],
282 factor: &Array2<f64>,
283 cache: &ProjectedFactorCache,
284) -> Vec<f64> {
285 let mut out = vec![0.0_f64; n_out];
286 let mut handled = vec![false; terms.len()];
287
288 for i in 0..terms.len() {
289 if handled[i] {
290 continue;
291 }
292 let Some(impl_i) = as_implicit(terms[i].2) else {
293 continue;
294 };
295 let mut group = vec![i];
296 handled[i] = true;
297 for j in (i + 1)..terms.len() {
298 if handled[j] {
299 continue;
300 }
301 if let Some(impl_j) = as_implicit(terms[j].2)
302 && Arc::ptr_eq(&impl_i.implicit_deriv, &impl_j.implicit_deriv)
303 && Arc::ptr_eq(&impl_i.x_design, &impl_j.x_design)
304 && Arc::ptr_eq(impl_i.w_diag.as_arc(), impl_j.w_diag.as_arc())
305 && impl_i.p == impl_j.p
306 {
307 group.push(j);
308 handled[j] = true;
309 }
310 }
311
312 let lead = impl_i;
316 let xf = lead.cached_xf(factor, cache);
317 let axes: Vec<(usize, &Array2<f64>, Option<&Array1<f64>>)> = group
318 .iter()
319 .map(|&term_idx| {
320 let op = as_implicit(terms[term_idx].2)
321 .expect("a group member is by construction an index that matched as_implicit");
322 (op.axis, &op.s_psi, op.c_x_psi_beta.as_deref())
323 })
324 .collect();
325 let values = lead.trace_projected_factor_all_axes_with_xf(factor, xf.view(), &axes);
326 for (&term_idx, value) in group.iter().zip(values.iter()) {
327 let (out_idx, weight, _) = terms[term_idx];
328 out[out_idx] += weight * *value;
329 }
330 }
331
332 for (i, (out_idx, weight, op)) in terms.iter().enumerate() {
333 if handled[i] {
334 continue;
335 }
336 out[*out_idx] += *weight * op.trace_projected_factor_cached(factor, cache);
337 }
338
339 out
340}
341
342pub(crate) fn projected_operator_terms_batched(
343 n_out: usize,
344 terms: &[(usize, f64, &dyn HyperOperator)],
345 factor: &Array2<f64>,
346 cache: &ProjectedFactorCache,
347) -> Vec<Array2<f64>> {
348 let rank = factor.ncols();
349 let mut out: Vec<Array2<f64>> = (0..n_out)
350 .map(|_| Array2::<f64>::zeros((rank, rank)))
351 .collect();
352 for (out_idx, weight, op) in terms.iter() {
353 let projected = op.projected_matrix_cached(factor, cache);
354 out[*out_idx].scaled_add(*weight, &projected);
355 }
356 out
357}
358
359pub(crate) fn project_hyper_operators_batched(
360 n_out: usize,
361 terms: &[(usize, f64, &dyn HyperOperator)],
362 factor: &Array2<f64>,
363 cache: &ProjectedFactorCache,
364) -> Vec<Array2<f64>> {
365 projected_operator_terms_batched(n_out, terms, factor, cache)
366}
367
368pub(crate) fn trace_logdet_drifts_projected_factor_batched(
369 drifts: &[DriftDerivResult],
370 factor: &Array2<f64>,
371 cache: &ProjectedFactorCache,
372) -> Vec<f64> {
373 let mut out = vec![0.0_f64; drifts.len()];
374 let mut terms: Vec<(usize, f64, &dyn HyperOperator)> = Vec::new();
375 for (idx, drift) in drifts.iter().enumerate() {
376 match drift {
377 DriftDerivResult::Dense(matrix) => {
378 out[idx] += dense_trace_projected_factor(matrix, factor);
379 }
380 DriftDerivResult::Operator(op) => {
381 collect_projected_trace_terms(idx, 1.0, op.as_ref(), factor, &mut out, &mut terms);
382 }
383 }
384 }
385 let batched = trace_projected_operator_terms_batched(drifts.len(), &terms, factor, cache);
386 for (dst, value) in out.iter_mut().zip(batched) {
387 *dst += value;
388 }
389 out
390}
391
392pub(crate) fn dense_spectral_trace_logdet_drifts_batched(
393 ds: &DenseSpectralOperator,
394 drifts: &[DriftDerivResult],
395) -> Vec<f64> {
396 trace_logdet_drifts_projected_factor_batched(drifts, &ds.g_factor, &ds.projected_factor_cache)
397}
398
399pub(crate) fn penalty_subspace_trace_factor(kernel: &PenaltySubspaceTrace) -> Array2<f64> {
400 let (evals, evecs) = kernel
401 .h_proj_inverse
402 .eigh(faer::Side::Lower)
403 .expect("PenaltySubspaceTrace kernel factor eigendecomposition failed");
404 let r = evals.len();
405 let mut root = evecs.clone();
422 for col in 0..r {
423 let scale = evals[col].max(0.0).sqrt();
424 for row in 0..r {
425 root[[row, col]] *= scale;
426 }
427 }
428 gam_linalg::faer_ndarray::fast_ab(&kernel.u_s, &root)
429}
430
431pub(crate) fn penalty_subspace_trace_drifts_batched(
432 kernel: &PenaltySubspaceTrace,
433 drifts: &[DriftDerivResult],
434) -> Vec<f64> {
435 let factor = penalty_subspace_trace_factor(kernel);
436 let cache = ProjectedFactorCache::default();
437 trace_logdet_drifts_projected_factor_batched(drifts, &factor, &cache)
438}
439
440pub(crate) fn penalty_subspace_reduce_drifts_batched(
441 kernel: &PenaltySubspaceTrace,
442 drifts: &[DriftDerivResult],
443) -> Vec<Array2<f64>> {
444 drifts
445 .iter()
446 .map(|drift| match drift {
447 DriftDerivResult::Dense(matrix) => kernel.reduce(matrix),
448 DriftDerivResult::Operator(op) => kernel.reduce_operator(op.as_ref()),
458 })
459 .collect()
460}
461
462pub(crate) fn dense_spectral_trace_logdet_operators_batched(
463 ds: &DenseSpectralOperator,
464 operators: &[Arc<dyn HyperOperator>],
465) -> Vec<f64> {
466 if operators.is_empty() {
467 return Vec::new();
468 }
469 if log::log_enabled!(log::Level::Info) {
470 let start = std::time::Instant::now();
471 let out =
472 trace_projected_factors_batched(operators, &ds.g_factor, &ds.projected_factor_cache);
473 let implicit_count = operators.iter().filter(|op| op.is_implicit()).count();
474 dense_spectral_stage_log(
475 &format!(
476 "DenseSpectralOperator::trace_logdet_operators_batched dim={} rank={} ops={} implicit_ops={}",
477 ds.n_dim,
478 ds.g_factor.ncols(),
479 operators.len(),
480 implicit_count,
481 ),
482 start.elapsed().as_secs_f64(),
483 );
484 out
485 } else {
486 trace_projected_factors_batched(operators, &ds.g_factor, &ds.projected_factor_cache)
487 }
488}
489
490impl HyperOperator for CompositeHyperOperator {
491 fn as_any(&self) -> &(dyn std::any::Any + 'static) {
492 self
493 }
494
495 fn dim(&self) -> usize {
496 self.dim_hint
497 }
498
499 fn mul_vec(&self, v: &Array1<f64>) -> Array1<f64> {
500 let mut out = Array1::<f64>::zeros(v.len());
501 self.mul_vec_into(v.view(), out.view_mut());
502 out
503 }
504
505 fn mul_vec_view(&self, v: ArrayView1<'_, f64>) -> Array1<f64> {
506 let mut out = Array1::<f64>::zeros(v.len());
507 self.mul_vec_into(v, out.view_mut());
508 out
509 }
510
511 fn mul_vec_into(&self, v: ArrayView1<'_, f64>, mut out: ArrayViewMut1<'_, f64>) {
512 if self.dense.is_none() && self.operators.len() == 1 {
513 self.operators[0].mul_vec_into(v, out);
514 return;
515 }
516
517 out.fill(0.0);
518 if let Some(dense) = self.dense.as_ref() {
519 dense::matvec_into(dense, v, out.view_mut());
520 }
521 for op in &self.operators {
522 op.scaled_add_mul_vec(v, 1.0, out.view_mut());
523 }
524 }
525
526 fn mul_basis_columns_into(&self, start: usize, mut out: ArrayViewMut2<'_, f64>) {
527 if self.dense.is_none() && self.operators.len() == 1 {
528 self.operators[0].mul_basis_columns_into(start, out);
529 return;
530 }
531
532 out.fill(0.0);
533 let cols = out.ncols();
534 let end = start + cols;
535 if let Some(dense) = self.dense.as_ref() {
536 out += &dense.slice(ndarray::s![.., start..end]);
537 }
538 let mut work = Array2::<f64>::zeros((out.nrows(), cols));
539 for op in &self.operators {
540 op.mul_basis_columns_into(start, work.view_mut());
541 out += &work;
542 }
543 }
544
545 fn scaled_add_mul_vec(
546 &self,
547 v: ArrayView1<'_, f64>,
548 scale: f64,
549 mut out: ArrayViewMut1<'_, f64>,
550 ) {
551 if scale == 0.0 {
552 return;
553 }
554 if self.dense.is_none() && self.operators.len() == 1 {
555 self.operators[0].scaled_add_mul_vec(v, scale, out);
556 return;
557 }
558
559 if let Some(dense) = self.dense.as_ref() {
560 dense::matvec_scaled_add_into(dense, v, scale, out.view_mut());
561 }
562 for op in &self.operators {
563 op.scaled_add_mul_vec(v, scale, out.view_mut());
564 }
565 }
566
567 fn mul_mat(&self, factor: &Array2<f64>) -> Array2<f64> {
572 if self.dense.is_none() && self.operators.len() == 1 {
573 return self.operators[0].mul_mat(factor);
574 }
575 let p = factor.nrows();
576 let k = factor.ncols();
577 let mut out = Array2::<f64>::zeros((p, k));
578 if let Some(dense) = self.dense.as_ref() {
579 out += &dense.dot(factor);
580 }
581 for op in &self.operators {
582 out += &op.mul_mat(factor);
583 }
584 out
585 }
586
587 fn trace_projected_factor(&self, factor: &Array2<f64>) -> f64 {
588 if self.dense.is_none() && self.operators.len() == 1 {
589 return self.operators[0].trace_projected_factor(factor);
590 }
591
592 let mut trace = 0.0;
593 if let Some(dense) = self.dense.as_ref() {
594 let dense_factor = dense.dot(factor);
595 trace += factor
596 .iter()
597 .zip(dense_factor.iter())
598 .map(|(&f, &bf)| f * bf)
599 .sum::<f64>();
600 }
601 trace += composite_trace_implicit_batched(&self.operators, factor, None);
602 trace
603 }
604
605 fn trace_projected_factor_cached(
606 &self,
607 factor: &Array2<f64>,
608 cache: &ProjectedFactorCache,
609 ) -> f64 {
610 if self.dense.is_none() && self.operators.len() == 1 {
611 return self.operators[0].trace_projected_factor_cached(factor, cache);
612 }
613
614 let mut trace = 0.0;
615 if let Some(dense) = self.dense.as_ref() {
616 let dense_factor = dense.dot(factor);
617 trace += factor
618 .iter()
619 .zip(dense_factor.iter())
620 .map(|(&f, &bf)| f * bf)
621 .sum::<f64>();
622 }
623 trace += composite_trace_implicit_batched(&self.operators, factor, Some(cache));
624 trace
625 }
626
627 fn projected_matrix(&self, factor: &Array2<f64>) -> Array2<f64> {
628 if self.dense.is_none() && self.operators.len() == 1 {
629 return self.operators[0].projected_matrix(factor);
630 }
631
632 let rank = factor.ncols();
633 let mut projected = Array2::<f64>::zeros((rank, rank));
634 if let Some(dense) = self.dense.as_ref() {
635 let mf = gam_linalg::faer_ndarray::fast_ab(dense, factor);
636 projected += &gam_linalg::faer_ndarray::fast_atb(factor, &mf);
637 }
638 for op in &self.operators {
639 projected += &op.projected_matrix(factor);
640 }
641 projected
642 }
643
644 fn projected_matrix_cached(
645 &self,
646 factor: &Array2<f64>,
647 cache: &ProjectedFactorCache,
648 ) -> Array2<f64> {
649 if self.dense.is_none() && self.operators.len() == 1 {
650 return self.operators[0].projected_matrix_cached(factor, cache);
651 }
652
653 let rank = factor.ncols();
654 let mut projected = Array2::<f64>::zeros((rank, rank));
655 if let Some(dense) = self.dense.as_ref() {
656 let mf = gam_linalg::faer_ndarray::fast_ab(dense, factor);
657 projected += &gam_linalg::faer_ndarray::fast_atb(factor, &mf);
658 }
659 for op in &self.operators {
660 projected += &op.projected_matrix_cached(factor, cache);
661 }
662 projected
663 }
664
665 fn bilinear(&self, v: &Array1<f64>, u: &Array1<f64>) -> f64 {
666 let mut total = 0.0;
667 if let Some(dense) = self.dense.as_ref() {
668 total += dense::bilinear(dense, v.view(), u.view());
669 }
670 for op in &self.operators {
671 total += op.bilinear(v, u);
672 }
673 total
674 }
675
676 fn bilinear_view(&self, v: ArrayView1<'_, f64>, u: ArrayView1<'_, f64>) -> f64 {
677 let mut total = 0.0;
678 if let Some(dense) = self.dense.as_ref() {
679 total += dense::bilinear(dense, v, u);
680 }
681 for op in &self.operators {
682 total += op.bilinear_view(v, u);
683 }
684 total
685 }
686
687 fn to_dense(&self) -> Array2<f64> {
688 let mut out = self
689 .dense
690 .clone()
691 .unwrap_or_else(|| Array2::<f64>::zeros((self.dim_hint, self.dim_hint)));
692 for op in &self.operators {
693 out += &op.to_dense();
694 }
695 out
696 }
697
698 fn is_implicit(&self) -> bool {
699 self.operators.iter().any(|op| op.is_implicit())
700 }
701}
702
703mod implicit_matvec_scratch {
718 use std::cell::RefCell;
719
720 pub(super) struct Scratch {
721 pub x_v: Vec<f64>,
722 pub n_work: Vec<f64>,
723 pub p_work: Vec<f64>,
724 }
725
726 impl Scratch {
727 pub(crate) const fn new() -> Self {
728 Self {
729 x_v: Vec::new(),
730 n_work: Vec::new(),
731 p_work: Vec::new(),
732 }
733 }
734 }
735
736 thread_local! {
737 static SCRATCH: RefCell<Scratch> = const { RefCell::new(Scratch::new()) };
738 }
739
740 pub(super) fn with<R>(f: impl FnOnce(&mut Scratch) -> R) -> R {
741 SCRATCH.with(|cell| f(&mut cell.borrow_mut()))
742 }
743}
744
745pub struct ImplicitHyperOperator {
746 pub implicit_deriv: std::sync::Arc<gam_terms::basis::ImplicitDesignPsiDerivative>,
748 pub axis: usize,
750 pub(crate) x_design: std::sync::Arc<DesignMatrix>,
752 pub(crate) w_diag: gam_linalg::matrix::SignedWeightsArc,
758 pub s_psi: Array2<f64>,
760 pub(crate) p: usize,
762 pub c_x_psi_beta: Option<std::sync::Arc<Array1<f64>>>,
769}
770
771impl HyperOperator for ImplicitHyperOperator {
772 fn dim(&self) -> usize {
773 self.p
774 }
775
776 fn mul_vec(&self, v: &Array1<f64>) -> Array1<f64> {
777 let mut out = Array1::<f64>::zeros(self.p);
783 self.mul_vec_into(v.view(), out.view_mut());
784 out
785 }
786
787 fn mul_vec_view(&self, v: ArrayView1<'_, f64>) -> Array1<f64> {
788 let mut out = Array1::<f64>::zeros(self.p);
789 self.mul_vec_into(v, out.view_mut());
790 out
791 }
792
793 fn mul_vec_into(&self, v: ArrayView1<'_, f64>, out: ArrayViewMut1<'_, f64>) {
794 assert_eq!(v.len(), self.p);
795 let n_obs = self.w_diag.len();
796 implicit_matvec_scratch::with(|s| {
800 s.x_v.clear();
801 s.x_v.resize(n_obs, 0.0);
802 s.n_work.clear();
803 s.n_work.resize(n_obs, 0.0);
804 s.p_work.clear();
805 s.p_work.resize(self.p, 0.0);
806 let mut x_v_view = ndarray::ArrayViewMut1::from(s.x_v.as_mut_slice());
807 let n_work_view = ndarray::ArrayViewMut1::from(s.n_work.as_mut_slice());
808 let p_work_view = ndarray::ArrayViewMut1::from(s.p_work.as_mut_slice());
809 self.x_design.apply_view_into(v, x_v_view.view_mut());
810 self.matvec_with_shared_xz_into(x_v_view.view(), v, out, n_work_view, p_work_view);
811 });
812 }
813
814 fn mul_basis_columns_into(&self, start: usize, mut out: ArrayViewMut2<'_, f64>) {
815 let cols = out.ncols();
816 assert!(start + cols <= self.p);
817
818 let n_obs = self.w_diag.len();
819 let mut basis = Array1::<f64>::zeros(self.p);
820 let mut x_col = Array1::<f64>::zeros(n_obs);
821 let mut dx_col = Array1::<f64>::zeros(n_obs);
822 let mut weighted = Array1::<f64>::zeros(n_obs);
823 let mut term = Array1::<f64>::zeros(self.p);
824
825 for local_col in 0..cols {
826 let global_col = start + local_col;
827 let mut out_col = out.column_mut(local_col);
828 out_col.assign(&self.s_psi.column(global_col));
829
830 self.x_design.column_into(global_col, x_col.view_mut());
831 Zip::from(weighted.view_mut())
832 .and(self.w_diag.view())
833 .and(x_col.view())
834 .par_for_each(|dst, &w, &x| *dst = w * x);
835 term.assign(
836 &self
837 .implicit_deriv
838 .transpose_mul(self.axis, &weighted.view())
839 .expect("radial scalar evaluation failed during implicit hyper transpose_mul"),
840 );
841 out_col += &term;
842
843 basis[global_col] = 1.0;
844 dx_col.assign(
845 &self
846 .implicit_deriv
847 .forward_mul(self.axis, &basis.view())
848 .expect("radial scalar evaluation failed during implicit hyper forward_mul"),
849 );
850 basis[global_col] = 0.0;
851
852 Zip::from(weighted.view_mut())
853 .and(self.w_diag.view())
854 .and(dx_col.view())
855 .par_for_each(|dst, &w, &dx| *dst = w * dx);
856 self.x_design
857 .transpose_apply_view_into(weighted.view(), term.view_mut());
858 out_col += &term;
859
860 self.accumulate_c_correction_xt_into(
862 x_col.view(),
863 weighted.view_mut(),
864 term.view_mut(),
865 out_col,
866 );
867 }
868 }
869
870 fn bilinear(&self, v: &Array1<f64>, u: &Array1<f64>) -> f64 {
871 self.bilinear_view(v.view(), u.view())
872 }
873
874 fn bilinear_view(&self, v: ArrayView1<'_, f64>, u: ArrayView1<'_, f64>) -> f64 {
875 assert_eq!(v.len(), self.p);
876 assert_eq!(u.len(), self.p);
877
878 let x_v = self.x_design.apply_view(v);
879 let x_u = self.x_design.apply_view(u);
880 let dx_v = self
881 .implicit_deriv
882 .forward_mul(self.axis, &v)
883 .expect("radial scalar evaluation failed during implicit hyper forward_mul");
884 let dx_u = self
885 .implicit_deriv
886 .forward_mul(self.axis, &u)
887 .expect("radial scalar evaluation failed during implicit hyper forward_mul");
888
889 let w = &*self.w_diag;
890 let mut design = 0.0;
891 for i in 0..w.len() {
892 design += dx_v[i] * w[i] * x_u[i];
893 design += dx_u[i] * w[i] * x_v[i];
894 }
895
896 design += self.c_correction_bilinear(&x_v, &x_u);
897
898 let penalty = dense::bilinear(&self.s_psi, v, u);
899
900 design + penalty
901 }
902
903 fn is_implicit(&self) -> bool {
904 true
905 }
906
907 fn as_any(&self) -> &(dyn std::any::Any + 'static) {
908 self
909 }
910
911 fn trace_projected_factor(&self, factor: &Array2<f64>) -> f64 {
947 assert_eq!(factor.nrows(), self.p);
948 let n_obs = self.w_diag.len();
949 let rank = factor.ncols();
950 if rank == 0 || n_obs == 0 {
951 return 0.0;
952 }
953 let xf = self.compute_xf(factor);
954 self.trace_projected_factor_with_xf(factor, xf.view())
955 }
956
957 fn trace_projected_factor_cached(
968 &self,
969 factor: &Array2<f64>,
970 cache: &ProjectedFactorCache,
971 ) -> f64 {
972 assert_eq!(factor.nrows(), self.p);
973 let n_obs = self.w_diag.len();
974 let rank = factor.ncols();
975 if rank == 0 || n_obs == 0 {
976 return 0.0;
977 }
978 let xf = self.cached_xf(factor, cache);
979 self.trace_projected_factor_with_xf(factor, xf.view())
980 }
981}
982
983pub(crate) use gam_runtime::resource::byte_balanced_row_chunk;
987
988impl ImplicitHyperOperator {
989 pub(crate) fn compute_xf(&self, factor: &Array2<f64>) -> Array2<f64> {
997 let n_obs = self.w_diag.len();
998 let rank = factor.ncols();
999 let mut xf = Array2::<f64>::zeros((n_obs, rank));
1000 let chunk_rows = byte_balanced_row_chunk(self.p + rank, n_obs);
1001 let mut start = 0usize;
1002 while start < n_obs {
1003 let end = (start + chunk_rows).min(n_obs);
1004 let rows = self
1005 .x_design
1006 .try_row_chunk(start..end)
1007 .unwrap_or_else(|err| {
1014 reml_contract_panic(format!(
1016 "ImplicitHyperOperator::compute_xf row chunk failed: {err}"
1017 ))
1018 });
1019 let block = gam_linalg::faer_ndarray::fast_ab(&rows, factor);
1020 xf.slice_mut(ndarray::s![start..end, ..]).assign(&block);
1021 start = end;
1022 }
1023 xf
1024 }
1025
1026 pub(crate) fn cached_xf(
1033 &self,
1034 factor: &Array2<f64>,
1035 cache: &ProjectedFactorCache,
1036 ) -> Arc<Array2<f64>> {
1037 let design_id = Arc::as_ptr(&self.x_design) as usize;
1038 let key = ProjectedFactorKey::from_factor_view(design_id, factor.view());
1039 cache.get_or_insert_with(key, || self.compute_xf(factor))
1040 }
1041
1042 pub(crate) fn trace_projected_factor_with_xf(
1049 &self,
1050 factor: &Array2<f64>,
1051 xf: ArrayView2<'_, f64>,
1052 ) -> f64 {
1053 let rank = factor.ncols();
1054 let n_obs = self.w_diag.len();
1055 assert_eq!(xf.dim(), (n_obs, rank));
1056
1057 let u_knot = self.implicit_deriv.unproject_matrix(&factor.view());
1059
1060 let chunk_rows = byte_balanced_row_chunk(self.p + rank, n_obs);
1063
1064 let w = self.w_diag.as_ref();
1065 let c_opt = self.c_x_psi_beta.as_ref().map(|arc| arc.as_ref());
1066 let mut design_total = 0.0_f64;
1067 let mut correction_total = 0.0_f64;
1068 let mut start = 0usize;
1069 while start < n_obs {
1070 let end = (start + chunk_rows).min(n_obs);
1071 let chunk_n = end - start;
1072
1073 let xf_chunk = xf.slice(ndarray::s![start..end, ..]);
1075
1076 let kd_chunk = self
1079 .implicit_deriv
1080 .row_chunk_first_raw(self.axis, start..end)
1081 .expect("radial scalar evaluation failed during implicit hyper forward_mul_matrix");
1082 let dxf_chunk = gam_linalg::faer_ndarray::fast_ab(&kd_chunk, &u_knot);
1083
1084 for i_local in 0..chunk_n {
1086 let i = start + i_local;
1087 let w_i = w[i];
1088 let dxf_row = dxf_chunk.row(i_local);
1089 let xf_row = xf_chunk.row(i_local);
1090 for k in 0..rank {
1091 design_total += dxf_row[k] * w_i * xf_row[k];
1092 }
1093 if let Some(c) = c_opt {
1094 let c_i = c[i];
1095 for k in 0..rank {
1096 let v = xf_row[k];
1097 correction_total += c_i * v * v;
1098 }
1099 }
1100 }
1101 start = end;
1102 }
1103
1104 let s_f = self.s_psi.dot(factor);
1106 let penalty: f64 = factor.iter().zip(s_f.iter()).map(|(&f, &s)| f * s).sum();
1107
1108 2.0 * design_total + correction_total + penalty
1109 }
1110
1111 pub(crate) fn trace_projected_factor_all_axes_with_xf(
1116 &self,
1117 factor: &Array2<f64>,
1118 xf: ArrayView2<'_, f64>,
1119 axes: &[(usize, &Array2<f64>, Option<&Array1<f64>>)],
1120 ) -> Vec<f64> {
1121 let rank = factor.ncols();
1122 let n_obs = self.w_diag.len();
1123 assert_eq!(xf.dim(), (n_obs, rank));
1124
1125 let u_knot = self.implicit_deriv.unproject_matrix(&factor.view());
1126
1127 let chunk_rows = byte_balanced_row_chunk(self.p + rank, n_obs.max(1));
1128
1129 let w = self.w_diag.as_ref();
1130 let mut design_totals = vec![0.0_f64; axes.len()];
1131 let mut correction_totals = vec![0.0_f64; axes.len()];
1132
1133 let mut start = 0usize;
1134 while start < n_obs {
1135 let end = (start + chunk_rows).min(n_obs);
1136 let chunk_n = end - start;
1137 let xf_chunk = xf.slice(ndarray::s![start..end, ..]);
1138
1139 for (axis_idx, (axis, _s_psi, c_opt_axis)) in axes.iter().enumerate() {
1140 let kd_chunk = self
1141 .implicit_deriv
1142 .row_chunk_first_raw(*axis, start..end)
1143 .expect(
1144 "radial scalar evaluation failed during \
1145 trace_projected_factor_all_axes_with_xf",
1146 );
1147 let dxf_chunk = gam_linalg::faer_ndarray::fast_ab(&kd_chunk, &u_knot);
1148
1149 for i_local in 0..chunk_n {
1150 let i = start + i_local;
1151 let w_i = w[i];
1152 let dxf_row = dxf_chunk.row(i_local);
1153 let xf_row = xf_chunk.row(i_local);
1154 for k in 0..rank {
1155 design_totals[axis_idx] += dxf_row[k] * w_i * xf_row[k];
1156 }
1157 if let Some(c) = c_opt_axis {
1158 let c_i = c[i];
1159 for k in 0..rank {
1160 let v = xf_row[k];
1161 correction_totals[axis_idx] += c_i * v * v;
1162 }
1163 }
1164 }
1165 }
1166 start = end;
1167 }
1168
1169 axes.iter()
1170 .enumerate()
1171 .map(|(idx, (_axis, s_psi, _c_opt_axis))| {
1172 let s_f = s_psi.dot(factor);
1173 let penalty: f64 = factor.iter().zip(s_f.iter()).map(|(&f, &s)| f * s).sum();
1174 2.0 * design_totals[idx] + correction_totals[idx] + penalty
1175 })
1176 .collect()
1177 }
1178
1179 pub(crate) fn accumulate_c_correction_xt_into(
1180 &self,
1181 x_col: ArrayView1<'_, f64>,
1182 mut n_work: ArrayViewMut1<'_, f64>,
1183 mut p_work: ArrayViewMut1<'_, f64>,
1184 mut out_col: ArrayViewMut1<'_, f64>,
1185 ) {
1186 let Some(c_x_psi_beta) = self.c_x_psi_beta.as_ref() else {
1187 return;
1188 };
1189 let c = c_x_psi_beta.as_ref();
1190 assert_eq!(x_col.len(), c.len());
1191 assert_eq!(n_work.len(), c.len());
1192 assert_eq!(p_work.len(), self.p);
1193
1194 for i in 0..c.len() {
1195 n_work[i] = c[i] * x_col[i];
1196 }
1197 self.x_design
1198 .transpose_apply_view_into(n_work.view(), p_work.view_mut());
1199 out_col += &p_work;
1200 }
1201
1202 pub(crate) fn c_correction_bilinear(&self, x_v: &Array1<f64>, x_u: &Array1<f64>) -> f64 {
1203 let Some(c_x_psi_beta) = self.c_x_psi_beta.as_ref() else {
1204 return 0.0;
1205 };
1206 x_v.iter()
1207 .zip(x_u.iter())
1208 .zip(c_x_psi_beta.iter())
1209 .map(|((&xv, &xu), &c)| xv * c * xu)
1210 .sum()
1211 }
1212
1213 pub fn bilinear_with_shared_x(
1238 &self,
1239 x_vec: &Array1<f64>,
1240 y_vec: &Array1<f64>,
1241 z: &Array1<f64>,
1242 u: &Array1<f64>,
1243 ) -> f64 {
1244 let dx_z = self
1246 .implicit_deriv
1247 .forward_mul(self.axis, &z.view())
1248 .expect("radial scalar evaluation failed during implicit hyper forward_mul");
1249 let dx_u = self
1250 .implicit_deriv
1251 .forward_mul(self.axis, &u.view())
1252 .expect("radial scalar evaluation failed during implicit hyper forward_mul");
1253
1254 let mut design = 0.0f64;
1255 let w = &*self.w_diag;
1256 for i in 0..x_vec.len() {
1257 let wi = w[i];
1258 design += dx_z[i] * wi * y_vec[i];
1259 design += dx_u[i] * wi * x_vec[i];
1260 }
1261
1262 if let Some(c_x_psi_beta) = self.c_x_psi_beta.as_ref() {
1266 let c = c_x_psi_beta.as_ref();
1267 for i in 0..x_vec.len() {
1268 design += y_vec[i] * c[i] * x_vec[i];
1269 }
1270 }
1271
1272 let penalty = dense::bilinear(&self.s_psi, z.view(), u.view());
1274
1275 design + penalty
1276 }
1277
1278 pub fn matvec_with_shared_xz_into(
1288 &self,
1289 x_vec: ArrayView1<'_, f64>,
1290 z: ArrayView1<'_, f64>,
1291 mut out: ArrayViewMut1<'_, f64>,
1292 mut n_work: ArrayViewMut1<'_, f64>,
1293 mut p_work: ArrayViewMut1<'_, f64>,
1294 ) {
1295 assert_eq!(z.len(), self.p);
1296 assert_eq!(out.len(), self.p);
1297 assert_eq!(n_work.len(), self.w_diag.len());
1298 assert_eq!(p_work.len(), self.p);
1299
1300 let w = &*self.w_diag;
1301 for i in 0..w.len() {
1302 n_work[i] = w[i] * x_vec[i];
1303 }
1304 let term1 = self
1305 .implicit_deriv
1306 .transpose_mul(self.axis, &n_work.view())
1307 .expect("radial scalar evaluation failed during implicit hyper transpose_mul");
1308 out.assign(&term1);
1309
1310 let dx_z = self
1311 .implicit_deriv
1312 .forward_mul(self.axis, &z)
1313 .expect("radial scalar evaluation failed during implicit hyper forward_mul");
1314 for i in 0..w.len() {
1315 n_work[i] = w[i] * dx_z[i];
1316 }
1317 self.x_design
1318 .transpose_apply_view_into(n_work.view(), p_work.view_mut());
1319 out += &p_work;
1320
1321 dense::matvec_into(&self.s_psi, z, p_work.view_mut());
1322 out += &p_work;
1323
1324 if let Some(c_x_psi_beta) = self.c_x_psi_beta.as_ref() {
1326 let c = c_x_psi_beta.as_ref();
1327 for i in 0..w.len() {
1328 n_work[i] = c[i] * x_vec[i];
1329 }
1330 self.x_design
1331 .transpose_apply_view_into(n_work.view(), p_work.view_mut());
1332 out += &p_work;
1333 }
1334 }
1335}
1336
1337pub struct SparseDirectionalHyperOperator {
1344 pub(crate) x_tau: super::super::HyperDesignDerivative,
1346 pub(crate) x_design: DesignMatrix,
1348 pub(crate) w_diag: gam_linalg::matrix::SignedWeightsArc,
1353 pub(crate) s_tau: Array2<f64>,
1355 pub(crate) c_x_tau_beta: Option<Array1<f64>>,
1357 pub(crate) firth_hphi_tau_partial: Option<Array2<f64>>,
1359 pub(crate) p: usize,
1361}
1362
1363impl HyperOperator for SparseDirectionalHyperOperator {
1364 fn dim(&self) -> usize {
1365 self.p
1366 }
1367
1368 fn mul_vec(&self, v: &Array1<f64>) -> Array1<f64> {
1369 assert_eq!(v.len(), self.p);
1370
1371 let x_v = self.x_design.matrixvectormultiply(v);
1373
1374 let w_x_v = &*self.w_diag * &x_v;
1376 let term1 = self
1377 .x_tau
1378 .transpose_mul_original(&w_x_v)
1379 .expect("SparseDirectionalHyperOperator transpose product should be shape-consistent");
1380
1381 let x_tau_v = self
1383 .x_tau
1384 .forward_mul_original(v)
1385 .expect("SparseDirectionalHyperOperator forward product should be shape-consistent");
1386 let w_x_tau_v = &*self.w_diag * &x_tau_v;
1387 let term2 = self.x_design.transpose_vector_multiply(&w_x_tau_v);
1388
1389 let term3 = self.s_tau.dot(v);
1391
1392 let mut out = term1 + term2 + term3;
1393
1394 if let Some(c_x_tau_beta) = self.c_x_tau_beta.as_ref() {
1396 let weighted = c_x_tau_beta * &x_v;
1397 out += &self.x_design.transpose_vector_multiply(&weighted);
1398 }
1399
1400 if let Some(hphi_tau_partial) = self.firth_hphi_tau_partial.as_ref() {
1402 out -= &hphi_tau_partial.dot(v);
1403 }
1404
1405 out
1406 }
1407
1408 fn is_implicit(&self) -> bool {
1409 false
1410 }
1411 fn as_any(&self) -> &(dyn std::any::Any + 'static) {
1412 self
1413 }
1414}
1415
1416pub struct GlmCurvatureCorrectionOperator {
1439 pub(crate) x_design: DesignMatrix,
1441 pub(crate) neg_c_xv: Array1<f64>,
1443 pub(crate) p: usize,
1445}
1446
1447impl HyperOperator for GlmCurvatureCorrectionOperator {
1448 fn dim(&self) -> usize {
1449 self.p
1450 }
1451
1452 fn mul_vec(&self, v: &Array1<f64>) -> Array1<f64> {
1453 assert_eq!(v.len(), self.p);
1454 let x_v = self.x_design.matrixvectormultiply(v);
1455 let weighted = &self.neg_c_xv * &x_v;
1456 self.x_design.transpose_vector_multiply(&weighted)
1457 }
1458
1459 fn as_any(&self) -> &(dyn std::any::Any + 'static) {
1460 self
1461 }
1462
1463 fn is_implicit(&self) -> bool {
1464 false
1465 }
1466}
1467
1468