1use crate::faer_ndarray::{
2 CrossprodAccum, CrossprodStructure, FaerArrayView, array2_to_matmut,
3 effective_global_parallelism, fast_ab, fast_atb, fast_atv, fast_atv_into, fast_av,
4 fast_av_into, fast_xt_diag_x, stream_weighted_crossprod_into,
5};
6use crate::types::RidgePolicy;
7use faer::Accum;
8use faer::linalg::matmul::matmul;
9use faer::sparse::{SparseColMat, SparseRowMat, Triplet};
10use gam_runtime::resource::{
11 Governed, MaterializationPolicy, MatrixMaterializationError, MemoryGovernor, MemoryReservation,
12 ResourcePolicy, dense_f64_bytes, rows_for_target_bytes,
13};
14use ndarray::{
15 Array1, Array2, ArrayView1, ArrayView2, ArrayViewMut1, ArrayViewMut2, Axis, ShapeBuilder, s,
16};
17use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
18use std::borrow::Cow;
19use std::collections::BTreeMap;
20use std::ops::Deref;
21use std::ops::Range;
22use std::sync::{Arc, OnceLock};
23
24const MATRIX_FREE_PCG_MIN_P: usize = 2048;
25const MATRIX_FREE_PCG_REL_TOL: f64 = 1e-8;
26const MATRIX_FREE_PCG_MAX_ITER: usize = 2000;
31const CHUNKED_DENSE_MATERIALIZATION_BYTES: usize =
34 gam_runtime::resource::LIBRARY_ROW_CHUNK_TARGET_BYTES;
35const OPERATOR_ROW_CHUNK_SIZE: usize = 256;
36const DENSE_ROW_PARALLEL_MIN_NP: u64 = 200_000;
40const WEIGHTED_CROSSPROD_PARALLEL_MIN_FLOPS: u64 = 500_000;
41const SPARSE_ROW_PARALLEL_MIN_FLOPS: u64 = 100_000;
42const TENSOR_GEMM_MAX_INTERMEDIATE_BYTES: usize = 128 * 1024 * 1024; pub use crate::utils::PcgSolveInfo;
47
48mod sparse_hessian;
49pub use sparse_hessian::{SparseHessianAccumulator, SparseHessianSymbolic};
50
51mod weights;
52pub use weights::{FiniteSignedWeightsView, PsdWeightsView, SignedWeightsArc, SignedWeightsView};
53
54#[derive(Debug, Clone)]
59pub enum MatrixError {
60 DimensionMismatch { reason: String },
63 DensificationRefused { reason: String },
67}
68
69crate::impl_reason_error_boilerplate! {
70 MatrixError {
71 DimensionMismatch,
72 DensificationRefused,
73 }
74}
75
76#[inline]
77fn dense_materialization_chunk_rows(nrows: usize, ncols: usize) -> usize {
78 rows_for_target_bytes(CHUNKED_DENSE_MATERIALIZATION_BYTES, ncols)
79 .max(1)
80 .min(nrows.max(1))
81}
82
83fn dense_operator_to_dense_by_chunks<O: DenseDesignOperator + ?Sized>(
84 op: &O,
85) -> Result<Array2<f64>, MatrixMaterializationError> {
86 let n = op.nrows();
87 let p = op.ncols();
88 let chunk_rows = dense_materialization_chunk_rows(n, p);
89 let mut out = Array2::<f64>::zeros((n, p));
90 for start in (0..n).step_by(chunk_rows) {
91 let end = (start + chunk_rows).min(n);
92 let slice = out.slice_mut(s![start..end, ..]);
93 op.row_chunk_into(start..end, slice)?;
94 }
95 Ok(out)
96}
97
98fn governed_dense_operator_to_dense_by_chunks<O: DenseDesignOperator + ?Sized>(
101 op: &O,
102 policy: &MaterializationPolicy,
103 context: &'static str,
104) -> Result<Governed<Array2<f64>>, MatrixMaterializationError> {
105 let effective_policy =
106 merge_operator_materialization_policies(Some(policy.clone()), op.materialization_policy())
107 .expect("caller policy is always present");
108 if !effective_policy.allow_operator_materialization {
109 crate::governed_capture::record_governed_decision(
110 context,
111 op.nrows(),
112 op.ncols(),
113 None,
114 crate::governed_capture::GovernedArm::Ineligible,
115 );
116 return Err(MatrixMaterializationError::Forbidden {
117 context,
118 mode: gam_runtime::resource::DerivativeStorageMode::AnalyticOperatorRequired,
119 });
120 }
121 let bytes = dense_f64_bytes(op.nrows(), op.ncols()).unwrap_or(usize::MAX);
122 if bytes > effective_policy.max_single_dense_bytes {
123 crate::governed_capture::record_governed_decision(
124 context,
125 op.nrows(),
126 op.ncols(),
127 Some(bytes),
128 crate::governed_capture::GovernedArm::Ineligible,
129 );
130 return Err(MatrixMaterializationError::TooLarge {
131 context,
132 nrows: op.nrows(),
133 ncols: op.ncols(),
134 bytes,
135 limit_bytes: effective_policy.max_single_dense_bytes,
136 });
137 }
138 let reservation =
139 match MemoryGovernor::global().try_reserve_dense_f64(op.nrows(), op.ncols(), context) {
140 Ok(reservation) => reservation,
141 Err(err) => {
142 crate::governed_capture::record_governed_decision(
143 context,
144 op.nrows(),
145 op.ncols(),
146 Some(bytes),
147 crate::governed_capture::GovernedArm::Refused,
148 );
149 return Err(err.into());
150 }
151 };
152 crate::governed_capture::record_governed_decision(
153 context,
154 op.nrows(),
155 op.ncols(),
156 Some(bytes),
157 crate::governed_capture::GovernedArm::Admitted,
158 );
159 dense_operator_to_dense_by_chunks(op).map(|matrix| reservation.bind(matrix))
160}
161
162pub fn checked_dense_nbytes(nrows: usize, ncols: usize, context: &str) -> Result<usize, String> {
163 nrows
164 .checked_mul(ncols)
165 .and_then(|cells| cells.checked_mul(std::mem::size_of::<f64>()))
166 .ok_or_else(|| {
167 MatrixError::DimensionMismatch {
168 reason: format!("{context}: dense size overflow for {nrows}x{ncols}"),
169 }
170 .into()
171 })
172}
173
174pub fn panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
175 context: &str,
176 n: usize,
177 p: usize,
178 policy: &ResourcePolicy,
179) -> Result<(), String> {
180 if matches!(
186 policy.derivative_storage_mode,
187 gam_runtime::resource::DerivativeStorageMode::AnalyticOperatorRequired
188 ) {
189 return Err(MatrixError::DensificationRefused {
190 reason: format!(
191 "{context}: refusing to densify operator-backed design {n}x{p} under \
192 AnalyticOperatorRequired policy; provide an operator-form path"
193 ),
194 }
195 .into());
196 }
197 let dense_bytes = checked_dense_nbytes(n, p, context)?;
198 let limit = policy.max_single_materialization_bytes;
199 if dense_bytes > limit {
200 let gib = dense_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
220 let limit_gib = limit as f64 / (1024.0 * 1024.0 * 1024.0);
221 return Err(MatrixError::DensificationRefused {
222 reason: format!(
223 "{context}: refusing to densify operator-backed design {n}x{p} \
224 (~{gib:.2} GiB, {dense_bytes} bytes) because it exceeds the single-materialization \
225 cap (~{limit_gib:.2} GiB, {limit} bytes); use matrix-free or chunked code. \
226 A cap at or near zero means the process could not size a memory budget — \
227 check the governor's detected availability rather than this allocation"
228 ),
229 }
230 .into());
231 }
232 Ok(())
233}
234
235fn merge_operator_materialization_policies(
236 left: Option<MaterializationPolicy>,
237 right: Option<MaterializationPolicy>,
238) -> Option<MaterializationPolicy> {
239 match (left, right) {
240 (None, policy) | (policy, None) => policy,
241 (Some(left), Some(right)) => Some(MaterializationPolicy {
242 max_single_dense_bytes: left
243 .max_single_dense_bytes
244 .min(right.max_single_dense_bytes),
245 max_cached_dense_bytes: left
246 .max_cached_dense_bytes
247 .min(right.max_cached_dense_bytes),
248 row_chunk_target_bytes: left
249 .row_chunk_target_bytes
250 .min(right.row_chunk_target_bytes),
251 allow_operator_materialization: left.allow_operator_materialization
252 && right.allow_operator_materialization,
253 allow_diagnostic_materialization: left.allow_diagnostic_materialization
254 && right.allow_diagnostic_materialization,
255 }),
256 }
257}
258
259fn enforce_operator_materialization_policy(
260 op: &dyn DenseDesignOperator,
261 context: &str,
262) -> Result<(), String> {
263 let Some(policy) = op.materialization_policy() else {
264 return Ok(());
265 };
266 if !policy.allow_operator_materialization {
267 return Err(MatrixError::DensificationRefused {
268 reason: format!(
269 "{context}: refusing to densify {}x{} operator-backed design because its \
270 construction policy requires streamed storage",
271 op.nrows(),
272 op.ncols(),
273 ),
274 }
275 .into());
276 }
277 let bytes = checked_dense_nbytes(op.nrows(), op.ncols(), context)?;
278 if bytes > policy.max_single_dense_bytes {
279 return Err(MatrixError::DensificationRefused {
280 reason: format!(
281 "{context}: refusing to densify {}x{} operator-backed design ({bytes} bytes); \
282 its construction-policy limit is {} bytes",
283 op.nrows(),
284 op.ncols(),
285 policy.max_single_dense_bytes,
286 ),
287 }
288 .into());
289 }
290 Ok(())
291}
292
293#[inline]
298fn certify_signed_weights<'a>(
299 context: &str,
300 weights: &'a Array1<f64>,
301 expected_len: usize,
302) -> Result<FiniteSignedWeightsView<'a>, String> {
303 if weights.len() != expected_len {
304 return Err(MatrixError::DimensionMismatch {
305 reason: format!(
306 "{context} weight length mismatch: weights={}, nrows={expected_len}",
307 weights.len()
308 ),
309 }
310 .into());
311 }
312 FiniteSignedWeightsView::try_from_array(weights)
313 .map_err(|reason| format!("{context}: {reason}"))
314}
315
316fn weighted_crossprod_dense(
317 left: &Array2<f64>,
318 weights: &Array1<f64>,
319 right: &Array2<f64>,
320) -> Result<Array2<f64>, String> {
321 if left.nrows() != weights.len() || right.nrows() != weights.len() {
322 return Err(MatrixError::DimensionMismatch {
323 reason: format!(
324 "weighted_crossprod_dense row mismatch: left={}, weights={}, right={}",
325 left.nrows(),
326 weights.len(),
327 right.nrows()
328 ),
329 }
330 .into());
331 }
332 certify_signed_weights("weighted_crossprod_dense", weights, left.nrows())?;
333 Ok(weighted_crossprod_dense_view(left, weights.view(), right))
334}
335
336fn weighted_crossprod_dense_view(
337 left: &Array2<f64>,
338 weights: ArrayView1<'_, f64>,
339 right: &Array2<f64>,
340) -> Array2<f64> {
341 let n = weights.len();
342 let p_left = left.ncols();
343 let p_right = right.ncols();
344 let work = (n as u64)
345 .saturating_mul(p_left as u64)
346 .saturating_mul(p_right as u64);
347 if rayon::current_num_threads() <= 1 || work < WEIGHTED_CROSSPROD_PARALLEL_MIN_FLOPS {
348 return weighted_crossprod_dense_rows(left, weights, right, 0..n);
349 }
350
351 let min_parallel_work = WEIGHTED_CROSSPROD_PARALLEL_MIN_FLOPS.min(usize::MAX as u64) as usize;
352 let Some(chunk_rows) = crate::parallel::row_reduction_chunk_rows(
353 n,
354 p_left.saturating_mul(p_right),
355 p_left.saturating_mul(p_right),
356 min_parallel_work,
357 ) else {
358 return weighted_crossprod_dense_rows(left, weights, right, 0..n);
359 };
360 let starts: Vec<usize> = (0..n).step_by(chunk_rows).collect();
361 let partials: Vec<Array2<f64>> = starts
362 .into_par_iter()
363 .map(|start| {
364 weighted_crossprod_dense_rows(left, weights, right, start..(start + chunk_rows).min(n))
365 })
366 .collect();
367 let mut out = Array2::<f64>::zeros((p_left, p_right));
368 for partial in &partials {
369 out += partial;
370 }
371 out
372}
373
374fn weighted_crossprod_dense_rows(
375 left: &Array2<f64>,
376 weights: ArrayView1<'_, f64>,
377 right: &Array2<f64>,
378 rows: Range<usize>,
379) -> Array2<f64> {
380 let p_left = left.ncols();
388 let p_right = right.ncols();
389 let mut out = Array2::<f64>::zeros((p_left, p_right));
390 if left.is_standard_layout()
391 && right.is_standard_layout()
392 && let (Some(lx), Some(rx), Some(w)) =
393 (left.as_slice(), right.as_slice(), weights.as_slice())
394 {
395 let out_slice = out.as_slice_mut().expect("zeros are contiguous");
396 for i in rows {
397 let wi = w[i];
398 if wi == 0.0 {
399 continue;
400 }
401 let l_row = &lx[i * p_left..i * p_left + p_left];
402 let r_row = &rx[i * p_right..i * p_right + p_right];
403 for a in 0..p_left {
404 let scaled = wi * l_row[a];
405 if scaled == 0.0 {
406 continue;
407 }
408 let out_row = &mut out_slice[a * p_right..a * p_right + p_right];
409 for b in 0..p_right {
410 out_row[b] += scaled * r_row[b];
411 }
412 }
413 }
414 return out;
415 }
416 for i in rows {
417 let wi = weights[i];
418 if wi == 0.0 {
419 continue;
420 }
421 for a in 0..p_left {
422 let scaled = wi * left[[i, a]];
423 if scaled == 0.0 {
424 continue;
425 }
426 for b in 0..p_right {
427 out[[a, b]] += scaled * right[[i, b]];
428 }
429 }
430 }
431 out
432}
433
434pub struct DenseRightProductView<'a> {
435 base: &'a Array2<f64>,
436 first: Option<&'a Array2<f64>>,
437 second: Option<&'a Array2<f64>>,
438}
439
440impl<'a> DenseRightProductView<'a> {
441 pub fn new(base: &'a Array2<f64>) -> Self {
442 Self {
443 base,
444 first: None,
445 second: None,
446 }
447 }
448
449 pub fn with_factor(mut self, factor: &'a Array2<f64>) -> Self {
450 if self.first.is_none() {
451 self.first = Some(factor);
452 } else if self.second.is_none() {
453 self.second = Some(factor);
454 } else {
455 std::panic::panic_any("DenseRightProductView supports at most two right factors");
461 }
462 self
463 }
464
465 pub fn with_optional_factor(self, factor: Option<&'a Array2<f64>>) -> Self {
466 match factor {
467 Some(factor) => self.with_factor(factor),
468 None => self,
469 }
470 }
471
472 pub fn materialize(&self) -> Array2<f64> {
473 let mut out = self.base.clone();
474 if let Some(factor) = self.first {
475 out = fast_ab(&out, factor);
476 }
477 if let Some(factor) = self.second {
478 out = fast_ab(&out, factor);
479 }
480 out
481 }
482
483 fn transformed_ncols(&self) -> usize {
484 if let Some(factor) = self.second {
485 factor.ncols()
486 } else if let Some(factor) = self.first {
487 factor.ncols()
488 } else {
489 self.base.ncols()
490 }
491 }
492}
493
494pub struct EmbeddedColumnBlock<'a> {
495 local: &'a Array2<f64>,
496 global_range: Range<usize>,
497 total_cols: usize,
498}
499
500impl<'a> EmbeddedColumnBlock<'a> {
501 pub fn new(local: &'a Array2<f64>, global_range: Range<usize>, total_cols: usize) -> Self {
502 Self {
503 local,
504 global_range,
505 total_cols,
506 }
507 }
508
509 pub fn materialize(&self) -> Array2<f64> {
510 if self.local.nrows() == 0 {
511 return Array2::<f64>::zeros((0, self.total_cols));
512 }
513 assert_eq!(
514 self.local.ncols(),
515 self.global_range.len(),
516 "embedded column block width mismatch"
517 );
518 let mut out = Array2::<f64>::zeros((self.local.nrows(), self.total_cols));
519 out.slice_mut(ndarray::s![.., self.global_range.clone()])
520 .assign(self.local);
521 out
522 }
523}
524
525pub struct EmbeddedSquareBlock<'a> {
526 local: &'a Array2<f64>,
527 global_range: Range<usize>,
528 total_dim: usize,
529}
530
531impl<'a> EmbeddedSquareBlock<'a> {
532 pub fn new(local: &'a Array2<f64>, global_range: Range<usize>, total_dim: usize) -> Self {
533 Self {
534 local,
535 global_range,
536 total_dim,
537 }
538 }
539
540 pub fn materialize(&self) -> Array2<f64> {
541 let mut out = Array2::<f64>::zeros((self.total_dim, self.total_dim));
542 out.slice_mut(ndarray::s![
543 self.global_range.clone(),
544 self.global_range.clone()
545 ])
546 .assign(self.local);
547 out
548 }
549}
550
551struct PenalizedWeightedNormalOperator<'a, O: LinearOperator + ?Sized> {
552 operator: &'a O,
553 weights: &'a Array1<f64>,
554 finite_weights: FiniteSignedWeightsView<'a>,
555 penalty: Option<&'a Array2<f64>>,
556 ridge: f64,
557}
558
559impl<'a, O: LinearOperator + ?Sized> PenalizedWeightedNormalOperator<'a, O> {
560 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
561 self.operator
562 .apply_weighted_normal(self.finite_weights, vector, self.penalty, self.ridge)
563 }
564
565 fn jacobi_preconditioner(&self) -> Result<Array1<f64>, String> {
566 let mut diag = self.operator.diag_gram(self.weights)?;
567 if let Some(pen) = self.penalty {
568 for i in 0..diag.len() {
569 diag[i] += pen[[i, i]];
570 }
571 }
572 if self.ridge > 0.0 {
573 for i in 0..diag.len() {
574 diag[i] += self.ridge;
575 }
576 }
577 Ok(diag)
578 }
579}
580
581#[inline]
582fn dense_diag_gram_view(matrix: &Array2<f64>, weights: ArrayView1<'_, f64>) -> Array1<f64> {
583 let p = matrix.ncols();
587 let n = matrix.nrows();
588 let large = (n as u64) * (p as u64) >= DENSE_ROW_PARALLEL_MIN_NP;
589 let parallel = large && rayon::current_thread_index().is_none();
590 if matrix.is_standard_layout()
593 && let (Some(x), Some(w)) = (matrix.as_slice(), weights.as_slice())
594 {
595 if parallel {
596 return crate::pairwise_reduce::par_deterministic_block_fold(
600 n,
601 |range: core::ops::Range<usize>| {
602 let mut acc = vec![0.0_f64; p];
603 for i in range {
604 let wi = w[i];
605 if wi != 0.0 {
606 let row = &x[i * p..i * p + p];
607 for j in 0..p {
608 let xij = row[j];
609 acc[j] += wi * xij * xij;
610 }
611 }
612 }
613 acc
614 },
615 |mut a, b| {
616 for (av, bv) in a.iter_mut().zip(b) {
617 *av += bv;
618 }
619 a
620 },
621 )
622 .unwrap_or_else(|| vec![0.0_f64; p])
623 .into();
624 }
625 let mut diag = Array1::<f64>::zeros(p);
626 let diag_slice = diag.as_slice_mut().expect("zeros are contiguous");
627 for i in 0..n {
628 let wi = w[i];
629 if wi == 0.0 {
630 continue;
631 }
632 let row = &x[i * p..i * p + p];
633 for j in 0..p {
634 let xij = row[j];
635 diag_slice[j] += wi * xij * xij;
636 }
637 }
638 return diag;
639 }
640 let mut diag = Array1::<f64>::zeros(p);
641 for i in 0..n {
642 let wi = weights[i];
643 if wi == 0.0 {
644 continue;
645 }
646 for j in 0..p {
647 let xij = matrix[[i, j]];
648 diag[j] += wi * xij * xij;
649 }
650 }
651 diag
652}
653
654fn sparse_csr_weighted_xtwx(
655 row_ptr: &[usize],
656 col_idx: &[usize],
657 vals: &[f64],
658 n: usize,
659 p: usize,
660 weights: ArrayView1<'_, f64>,
661) -> Array2<f64> {
662 let nnz = vals.len() as u64;
663 let avg = nnz.checked_div(n.max(1) as u64).unwrap_or(0);
664 let work = (n as u64).saturating_mul(avg.saturating_mul(avg));
665 if rayon::current_num_threads() <= 1 || work < SPARSE_ROW_PARALLEL_MIN_FLOPS {
666 return sparse_csr_weighted_xtwx_rows(row_ptr, col_idx, vals, p, weights, 0..n);
667 }
668
669 let min_parallel_work = SPARSE_ROW_PARALLEL_MIN_FLOPS.min(usize::MAX as u64) as usize;
670 let Some(chunk_rows) = crate::parallel::row_reduction_chunk_rows(
671 n,
672 avg.min(usize::MAX as u64) as usize,
673 p.saturating_mul(p),
674 min_parallel_work,
675 ) else {
676 return sparse_csr_weighted_xtwx_rows(row_ptr, col_idx, vals, p, weights, 0..n);
677 };
678 let starts: Vec<usize> = (0..n).step_by(chunk_rows).collect();
679 let partials: Vec<Array2<f64>> = starts
680 .into_par_iter()
681 .map(|start| {
682 sparse_csr_weighted_xtwx_rows(
683 row_ptr,
684 col_idx,
685 vals,
686 p,
687 weights,
688 start..(start + chunk_rows).min(n),
689 )
690 })
691 .collect();
692 let mut xtwx = Array2::<f64>::zeros((p, p));
693 for partial in &partials {
694 xtwx += partial;
695 }
696 xtwx
697}
698
699fn sparse_csr_weighted_xtwx_rows(
700 row_ptr: &[usize],
701 col_idx: &[usize],
702 vals: &[f64],
703 p: usize,
704 weights: ArrayView1<'_, f64>,
705 rows: Range<usize>,
706) -> Array2<f64> {
707 let mut xtwx = Array2::<f64>::zeros((p, p));
714 for i in rows {
715 let wi = weights[i];
716 if wi == 0.0 {
717 continue;
718 }
719 let start = row_ptr[i];
720 let end = row_ptr[i + 1];
721 for a_ptr in start..end {
722 let a = col_idx[a_ptr];
723 let wxa = wi * vals[a_ptr];
724 for b_ptr in a_ptr..end {
725 let b = col_idx[b_ptr];
726 let v = wxa * vals[b_ptr];
727 xtwx[[a, b]] += v;
728 if a != b {
729 xtwx[[b, a]] += v;
730 }
731 }
732 }
733 }
734 xtwx
735}
736
737pub fn streaming_sparse_csc_xt_diag_x(
738 col_ptr: &[usize],
739 row_idx: &[usize],
740 vals: &[f64],
741 n: usize,
742 p: usize,
743 weights: ArrayView1<'_, f64>,
744 out: &mut Array2<f64>,
745) {
746 if n == 0 || p == 0 {
747 return;
748 }
749
750 let chunk_rows = dense_materialization_chunk_rows(n, p);
751 let par = effective_global_parallelism();
752 let mut x_chunk = Array2::<f64>::zeros((chunk_rows, p).f());
753 let mut wx_chunk = Array2::<f64>::zeros((chunk_rows, p).f());
754
755 {
756 let mut out_view = array2_to_matmut(out);
757
758 for start in (0..n).step_by(chunk_rows) {
759 let rows = (n - start).min(chunk_rows);
760 {
761 let mut x_slice = x_chunk.slice_mut(s![0..rows, ..]);
762 let mut wx_slice = wx_chunk.slice_mut(s![0..rows, ..]);
763 x_slice.fill(0.0);
764 wx_slice.fill(0.0);
765 let end = start + rows;
766 for col in 0..p {
767 let col_start = col_ptr[col];
768 let col_end = col_ptr[col + 1];
769 let rows_for_col = &row_idx[col_start..col_end];
770 let local_start = rows_for_col.partition_point(|&row| row < start);
771 let local_end = rows_for_col.partition_point(|&row| row < end);
772 for local_ptr in local_start..local_end {
773 let ptr = col_start + local_ptr;
774 let row = row_idx[ptr];
775 let local = row - start;
776 let wi = weights[row];
777 let value = vals[ptr];
778 x_slice[[local, col]] += value;
779 wx_slice[[local, col]] += wi * value;
780 }
781 }
782 }
783 let x_slice = x_chunk.slice(s![0..rows, ..]);
784 let wx_slice = wx_chunk.slice(s![0..rows, ..]);
785 let x_view = FaerArrayView::new(&x_slice);
786 let wx_view = FaerArrayView::new(&wx_slice);
787 matmul(
788 out_view.as_mut(),
789 Accum::Add,
790 x_view.as_ref().transpose(),
791 wx_view.as_ref(),
792 1.0,
793 par,
794 );
795 }
796 }
797}
798
799fn sparse_csr_diag_gram(
800 row_ptr: &[usize],
801 col_idx: &[usize],
802 vals: &[f64],
803 n: usize,
804 p: usize,
805 weights: ArrayView1<'_, f64>,
806) -> Array1<f64> {
807 let work = vals.len() as u64;
808 if rayon::current_num_threads() <= 1 || work < SPARSE_ROW_PARALLEL_MIN_FLOPS {
809 return sparse_csr_diag_gram_rows(row_ptr, col_idx, vals, p, weights, 0..n);
810 }
811 let min_parallel_work = SPARSE_ROW_PARALLEL_MIN_FLOPS.min(usize::MAX as u64) as usize;
812 let Some(chunk_rows) = crate::parallel::row_reduction_chunk_rows(n, 1, p, min_parallel_work)
813 else {
814 return sparse_csr_diag_gram_rows(row_ptr, col_idx, vals, p, weights, 0..n);
815 };
816 let starts: Vec<usize> = (0..n).step_by(chunk_rows).collect();
817 let partials: Vec<Array1<f64>> = starts
818 .into_par_iter()
819 .map(|start| {
820 sparse_csr_diag_gram_rows(
821 row_ptr,
822 col_idx,
823 vals,
824 p,
825 weights,
826 start..(start + chunk_rows).min(n),
827 )
828 })
829 .collect();
830 let mut diag = Array1::<f64>::zeros(p);
831 for partial in &partials {
832 diag += partial;
833 }
834 diag
835}
836
837fn sparse_csr_diag_gram_rows(
838 row_ptr: &[usize],
839 col_idx: &[usize],
840 vals: &[f64],
841 p: usize,
842 weights: ArrayView1<'_, f64>,
843 rows: Range<usize>,
844) -> Array1<f64> {
845 let mut diag = Array1::<f64>::zeros(p);
850 for i in rows {
851 let wi = weights[i];
852 if wi == 0.0 {
853 continue;
854 }
855 for idx in row_ptr[i]..row_ptr[i + 1] {
856 let j = col_idx[idx];
857 let xij = vals[idx];
858 diag[j] += wi * xij * xij;
859 }
860 }
861 diag
862}
863
864#[inline]
865fn dense_transpose_weighted_response(
866 matrix: &Array2<f64>,
867 weights: &Array1<f64>,
868 y: &Array1<f64>,
869 row_scale: Option<&Array1<f64>>,
870) -> Array1<f64> {
871 let p = matrix.ncols();
876 let n = matrix.nrows();
877 let mut out = Array1::<f64>::zeros(p);
878 if matrix.is_standard_layout()
879 && let (Some(x), Some(w), Some(yslice)) =
880 (matrix.as_slice(), weights.as_slice(), y.as_slice())
881 {
882 let scale_slice = row_scale.and_then(|s| s.as_slice());
883 let out_slice = out.as_slice_mut().expect("zeros are contiguous");
884 for i in 0..n {
885 let mut scaled = yslice[i] * w[i];
886 if let Some(s) = scale_slice {
887 scaled *= s[i];
888 } else if let Some(scale) = row_scale {
889 scaled *= scale[i];
890 }
891 if scaled == 0.0 {
892 continue;
893 }
894 let row = &x[i * p..i * p + p];
895 for j in 0..p {
896 out_slice[j] += row[j] * scaled;
897 }
898 }
899 return out;
900 }
901 for i in 0..n {
902 let mut scaled = y[i] * weights[i];
903 if let Some(scale) = row_scale {
904 scaled *= scale[i];
905 }
906 if scaled == 0.0 {
907 continue;
908 }
909 for j in 0..p {
910 out[j] += matrix[[i, j]] * scaled;
911 }
912 }
913 out
914}
915
916#[inline]
917fn dense_transpose_weighted_response_view(
918 matrix: &Array2<f64>,
919 weights: ArrayView1<'_, f64>,
920 y: ArrayView1<'_, f64>,
921) -> Array1<f64> {
922 let p = matrix.ncols();
925 let n = matrix.nrows();
926 let mut out = Array1::<f64>::zeros(p);
927 if matrix.is_standard_layout()
928 && let (Some(x), Some(w), Some(yslice)) =
929 (matrix.as_slice(), weights.as_slice(), y.as_slice())
930 {
931 let out_slice = out.as_slice_mut().expect("zeros are contiguous");
932 for i in 0..n {
933 let scaled = yslice[i] * w[i];
934 if scaled == 0.0 {
935 continue;
936 }
937 let row = &x[i * p..i * p + p];
938 for j in 0..p {
939 out_slice[j] += row[j] * scaled;
940 }
941 }
942 return out;
943 }
944 for i in 0..n {
945 let scaled = y[i] * weights[i];
946 if scaled == 0.0 {
947 continue;
948 }
949 for j in 0..p {
950 out[j] += matrix[[i, j]] * scaled;
951 }
952 }
953 out
954}
955
956#[derive(Clone)]
957pub struct SparseDesignMatrix {
958 matrix: SparseColMat<usize, f64>,
959 dense_cache: Arc<OnceLock<(Arc<Array2<f64>>, MemoryReservation)>>,
962 csr_cache: Arc<OnceLock<Arc<SparseRowMat<usize, f64>>>>,
963 hessian_pattern_cache: Arc<OnceLock<Arc<SparseHessianSymbolic>>>,
967}
968
969impl SparseDesignMatrix {
970 pub fn new(matrix: SparseColMat<usize, f64>) -> Self {
971 Self {
972 matrix,
973 dense_cache: Arc::new(OnceLock::new()),
974 csr_cache: Arc::new(OnceLock::new()),
975 hessian_pattern_cache: Arc::new(OnceLock::new()),
976 }
977 }
978
979 pub fn hessian_accumulator_template(&self) -> Option<SparseHessianAccumulator> {
991 if let Some(sym) = self.hessian_pattern_cache.get() {
992 return Some(SparseHessianAccumulator::from_symbolic(Arc::clone(sym)));
993 }
994 let csr = self.to_csr_arc()?;
995 let sym = self
998 .hessian_pattern_cache
999 .get_or_init(|| SparseHessianAccumulator::build_symbolic(&[&csr], self.matrix.ncols()));
1000 Some(SparseHessianAccumulator::from_symbolic(Arc::clone(sym)))
1001 }
1002
1003 fn dense_nbytes(&self) -> Result<usize, String> {
1004 self.matrix
1005 .nrows()
1006 .checked_mul(self.matrix.ncols())
1007 .and_then(|cells| cells.checked_mul(std::mem::size_of::<f64>()))
1008 .ok_or_else(|| {
1009 format!(
1010 "dense size overflow for sparse design {}x{}",
1011 self.matrix.nrows(),
1012 self.matrix.ncols()
1013 )
1014 })
1015 }
1016
1017 fn materialize_dense_arc(&self) -> Arc<Array2<f64>> {
1018 let mut out = Array2::<f64>::zeros((self.matrix.nrows(), self.matrix.ncols()));
1019 let (symbolic, values) = self.matrix.parts();
1020 let col_ptr = symbolic.col_ptr();
1021 let row_idx = symbolic.row_idx();
1022 for col in 0..self.matrix.ncols() {
1023 let start = col_ptr[col];
1024 let end = col_ptr[col + 1];
1025 for idx in start..end {
1026 out[[row_idx[idx], col]] += values[idx];
1027 }
1028 }
1029 Arc::new(out)
1030 }
1031
1032 pub fn try_to_dense_arc(&self, context: &str) -> Result<Arc<Array2<f64>>, String> {
1033 if let Some((cached, _)) = self.dense_cache.get() {
1034 return Ok(cached.clone());
1035 }
1036 let dense_bytes = self.dense_nbytes()?;
1037 let governor = MemoryGovernor::global();
1038 if dense_bytes > governor.single_materialization_cap_bytes() {
1039 let gib = dense_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
1040 return Err(MatrixError::DensificationRefused {
1041 reason: format!(
1042 "{context}: refusing to densify sparse design {}x{} (~{gib:.2} GiB, over the process memory budget); use sparse or matrix-free code",
1043 self.matrix.nrows(),
1044 self.matrix.ncols(),
1045 ),
1046 }
1047 .into());
1048 }
1049 let reservation = governor.try_reserve(dense_bytes, context).map_err(|err| {
1057 String::from(MatrixError::DensificationRefused {
1058 reason: format!(
1059 "{context}: refusing to densify sparse design {}x{}: {err}",
1060 self.matrix.nrows(),
1061 self.matrix.ncols(),
1062 ),
1063 })
1064 })?;
1065 Ok(self
1066 .dense_cache
1067 .get_or_init(|| (self.materialize_dense_arc(), reservation))
1068 .0
1069 .clone())
1070 }
1071
1072 fn cached_dense_owner(context: &str) -> MemoryReservation {
1077 MemoryGovernor::global()
1078 .try_reserve(0, context)
1079 .expect("zero-byte reservation cannot exceed any budget")
1080 }
1081
1082 pub fn try_to_dense_governed(
1097 &self,
1098 context: &str,
1099 ) -> Result<Governed<Arc<Array2<f64>>>, String> {
1100 let governor = MemoryGovernor::global();
1101 let (nrows, ncols) = (self.matrix.nrows(), self.matrix.ncols());
1102 if let Some((cached, _)) = self.dense_cache.get() {
1103 crate::governed_capture::record_governed_decision(
1104 context,
1105 nrows,
1106 ncols,
1107 Some(0),
1108 crate::governed_capture::GovernedArm::CacheHit,
1109 );
1110 return Ok(Self::cached_dense_owner(context).bind(cached.clone()));
1111 }
1112 let dense_bytes = match self.dense_nbytes() {
1113 Ok(bytes) => bytes,
1114 Err(err) => {
1115 crate::governed_capture::record_governed_decision(
1116 context,
1117 nrows,
1118 ncols,
1119 None,
1120 crate::governed_capture::GovernedArm::Ineligible,
1121 );
1122 return Err(err);
1123 }
1124 };
1125 let reservation = match governor.try_reserve(dense_bytes, context) {
1126 Ok(reservation) => reservation,
1127 Err(err) => {
1128 crate::governed_capture::record_governed_decision(
1132 context,
1133 nrows,
1134 ncols,
1135 Some(dense_bytes),
1136 crate::governed_capture::GovernedArm::Refused,
1137 );
1138 return Err(String::from(MatrixError::DensificationRefused {
1139 reason: format!(
1140 "{context}: refusing to densify sparse design {nrows}x{ncols}: {err}"
1141 ),
1142 }));
1143 }
1144 };
1145 crate::governed_capture::record_governed_decision(
1146 context,
1147 nrows,
1148 ncols,
1149 Some(dense_bytes),
1150 crate::governed_capture::GovernedArm::Admitted,
1151 );
1152 let cached = self
1157 .dense_cache
1158 .get_or_init(|| (self.materialize_dense_arc(), reservation))
1159 .0
1160 .clone();
1161 Ok(Self::cached_dense_owner(context).bind(cached))
1162 }
1163
1164 pub fn to_dense_arc(&self) -> Arc<Array2<f64>> {
1165 self.try_to_dense_arc("SparseDesignMatrix::to_dense_arc")
1166 .unwrap_or_else(|msg| {
1167 let bt = std::backtrace::Backtrace::force_capture();
1168 std::panic::panic_any(format!("{msg}\nbacktrace:\n{bt}"))
1175 })
1176 }
1177
1178 pub fn to_csr_arc(&self) -> Option<Arc<SparseRowMat<usize, f64>>> {
1179 if let Some(cached) = self.csr_cache.get() {
1180 return Some(cached.clone());
1181 }
1182 let csr = self.matrix.as_ref().to_row_major().ok()?;
1183 let arc = Arc::new(csr);
1184 if self.csr_cache.set(arc.clone()).is_err() {
1185 return self.csr_cache.get().cloned();
1189 }
1190 Some(arc)
1191 }
1192
1193 fn row_chunk_into(
1194 &self,
1195 rows: Range<usize>,
1196 mut out: ArrayViewMut2<'_, f64>,
1197 ) -> Result<(), MatrixMaterializationError> {
1198 if out.nrows() != rows.end - rows.start || out.ncols() != self.ncols() {
1199 return Err(MatrixMaterializationError::MissingRowChunk {
1200 context: "SparseDesignMatrix::row_chunk_into shape mismatch",
1201 });
1202 }
1203 out.fill(0.0);
1204 let csr = self
1205 .to_csr_arc()
1206 .ok_or(MatrixMaterializationError::MissingRowChunk {
1207 context: "SparseDesignMatrix::row_chunk_into: failed to obtain CSR view",
1208 })?;
1209 let symbolic = csr.symbolic();
1210 let row_ptr = symbolic.row_ptr();
1211 let col_idx = symbolic.col_idx();
1212 let values = csr.val();
1213 for (local_row, row) in rows.enumerate() {
1214 for ptr in row_ptr[row]..row_ptr[row + 1] {
1215 out[[local_row, col_idx[ptr]]] = values[ptr];
1216 }
1217 }
1218 Ok(())
1219 }
1220}
1221
1222impl Deref for SparseDesignMatrix {
1223 type Target = SparseColMat<usize, f64>;
1224 fn deref(&self) -> &Self::Target {
1225 &self.matrix
1226 }
1227}
1228
1229impl AsRef<SparseColMat<usize, f64>> for SparseDesignMatrix {
1230 fn as_ref(&self) -> &SparseColMat<usize, f64> {
1231 &self.matrix
1232 }
1233}
1234
1235pub trait DenseDesignOperator: LinearOperator + Send + Sync {
1243 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
1244 let n = self.nrows();
1246 if weights.len() != n || y.len() != n {
1247 return Err(format!(
1248 "DenseDesignOperator::compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
1249 weights.len(),
1250 y.len(),
1251 n
1252 ));
1253 }
1254 certify_signed_weights("DenseDesignOperator::compute_xtwy", weights, n)?;
1255 let mut wy = Array1::<f64>::zeros(n);
1258 ndarray::Zip::from(&mut wy)
1259 .and(weights)
1260 .and(y)
1261 .par_for_each(|o, &w, &yi| *o = w * yi);
1262 Ok(self.apply_transpose(&wy))
1263 }
1264
1265 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
1266 if middle.nrows() != self.ncols() || middle.ncols() != self.ncols() {
1269 return Err(format!(
1270 "DenseDesignOperator::quadratic_form_diag dimension mismatch: {}x{} vs expected {}x{}",
1271 middle.nrows(),
1272 middle.ncols(),
1273 self.ncols(),
1274 self.ncols()
1275 ));
1276 }
1277 let n = self.nrows();
1278 let mut out = Array1::<f64>::zeros(n);
1279 let chunk_size = (CHUNKED_DENSE_MATERIALIZATION_BYTES / (self.ncols().max(1) * 8 * 2))
1284 .max(16)
1285 .min(n.max(1));
1286 let mut start = 0;
1287 while start < n {
1288 let end = (start + chunk_size).min(n);
1289 let x_chunk = self.try_row_chunk(start..end).map_err(|e| e.to_string())?;
1290 let xm_chunk = fast_ab(&x_chunk, middle);
1291 let mut chunk_out = out.slice_mut(ndarray::s![start..end]);
1292 ndarray::Zip::from(&mut chunk_out)
1293 .and(x_chunk.rows())
1294 .and(xm_chunk.rows())
1295 .par_for_each(|o, xr, xmr| *o = xr.dot(&xmr).max(0.0));
1298 start = end;
1299 }
1300 Ok(out)
1301 }
1302
1303 fn row_chunk_into(
1306 &self,
1307 rows: Range<usize>,
1308 out: ArrayViewMut2<'_, f64>,
1309 ) -> Result<(), MatrixMaterializationError>;
1310
1311 fn try_row_chunk(&self, rows: Range<usize>) -> Result<Array2<f64>, MatrixMaterializationError> {
1314 let mut out = Array2::<f64>::zeros((rows.end - rows.start, self.ncols()));
1315 self.row_chunk_into(rows, out.view_mut())?;
1316 Ok(out)
1317 }
1318
1319 fn as_dense_ref(&self) -> Option<&Array2<f64>> {
1321 None
1322 }
1323
1324 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
1329 None
1330 }
1331
1332 fn apply_columns(&self, cols: &[usize]) -> Array2<f64> {
1340 let n = self.nrows();
1341 let p = self.ncols();
1342 let mut out = Array2::<f64>::zeros((n, cols.len()));
1343 let mut e = Array1::<f64>::zeros(p);
1344 for (k, &j) in cols.iter().enumerate() {
1345 assert!(
1346 j < p,
1347 "DenseDesignOperator::apply_columns: column index {j} out of bounds (ncols={p})"
1348 );
1349 e[j] = 1.0;
1350 let col = self.apply(&e);
1351 e[j] = 0.0;
1352 out.column_mut(k).assign(&col);
1353 }
1354 out
1355 }
1356
1357 fn to_dense(&self) -> Array2<f64>;
1361
1362 fn estimated_dense_bytes(&self) -> usize {
1363 self.nrows()
1364 .saturating_mul(self.ncols())
1365 .saturating_mul(std::mem::size_of::<f64>())
1366 }
1367
1368 fn try_to_dense_with_policy(
1369 &self,
1370 policy: &MaterializationPolicy,
1371 context: &'static str,
1372 ) -> Result<Arc<Array2<f64>>, MatrixMaterializationError> {
1373 let effective_policy = merge_operator_materialization_policies(
1374 Some(policy.clone()),
1375 self.materialization_policy(),
1376 )
1377 .expect("caller policy is always present");
1378 let bytes = self.estimated_dense_bytes();
1379 if !effective_policy.allow_operator_materialization {
1380 return Err(MatrixMaterializationError::Forbidden {
1381 context,
1382 mode: gam_runtime::resource::DerivativeStorageMode::AnalyticOperatorRequired,
1383 });
1384 }
1385 if bytes > effective_policy.max_single_dense_bytes {
1386 return Err(MatrixMaterializationError::TooLarge {
1387 context,
1388 nrows: self.nrows(),
1389 ncols: self.ncols(),
1390 bytes,
1391 limit_bytes: effective_policy.max_single_dense_bytes,
1392 });
1393 }
1394 dense_operator_to_dense_by_chunks(self).map(Arc::new)
1395 }
1396
1397 fn try_to_dense_governed_with_policy(
1401 &self,
1402 policy: &MaterializationPolicy,
1403 context: &'static str,
1404 ) -> Result<Governed<Array2<f64>>, MatrixMaterializationError> {
1405 governed_dense_operator_to_dense_by_chunks(self, policy, context)
1406 }
1407
1408 fn to_dense_arc(&self) -> Arc<Array2<f64>> {
1415 Arc::new(
1416 dense_operator_to_dense_by_chunks(self)
1417 .expect("DenseDesignOperator::to_dense_arc: row-chunk materialization failed"),
1418 )
1419 }
1420}
1421
1422#[derive(Clone)]
1434pub struct LazyDense {
1435 op: Arc<dyn DenseDesignOperator>,
1436 dense_memo: Arc<OnceLock<Governed<Arc<Array2<f64>>>>>,
1437}
1438
1439impl LazyDense {
1440 fn new(op: Arc<dyn DenseDesignOperator>) -> Self {
1441 Self {
1442 op,
1443 dense_memo: Arc::new(OnceLock::new()),
1444 }
1445 }
1446
1447 fn operator_arc_identity(&self) -> usize {
1448 Arc::as_ptr(&self.op) as *const () as usize
1449 }
1450
1451 fn try_governed_dense_arc(&self, context: &str) -> Result<Arc<Array2<f64>>, String> {
1457 enforce_operator_materialization_policy(self.op.as_ref(), context)?;
1458 if let Some(governed) = self.dense_memo.get() {
1459 return Ok(Arc::clone(governed.as_ref()));
1460 }
1461 let reservation = MemoryGovernor::global()
1462 .try_reserve_dense_f64(self.op.nrows(), self.op.ncols(), context)
1463 .map_err(|err| {
1464 format!(
1465 "{context}: refusing to densify {}x{} operator-backed design: {err}",
1466 self.op.nrows(),
1467 self.op.ncols(),
1468 )
1469 })?;
1470 let dense = dense_operator_to_dense_by_chunks(self.op.as_ref()).map_err(|err| {
1471 format!(
1472 "{context}: failed to materialize {}x{} operator-backed design via row chunks: {err}",
1473 self.op.nrows(),
1474 self.op.ncols(),
1475 )
1476 })?;
1477 Ok(Arc::clone(
1480 self.dense_memo
1481 .get_or_init(|| reservation.bind(Arc::new(dense)))
1482 .as_ref(),
1483 ))
1484 }
1485}
1486
1487impl std::ops::Deref for LazyDense {
1488 type Target = Arc<dyn DenseDesignOperator>;
1489
1490 fn deref(&self) -> &Self::Target {
1491 &self.op
1492 }
1493}
1494
1495#[derive(Clone)]
1496pub enum DenseDesignMatrix {
1497 Materialized(Arc<Array2<f64>>),
1498 Lazy(LazyDense),
1499}
1500
1501impl std::fmt::Debug for DenseDesignMatrix {
1502 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1503 match self {
1504 Self::Materialized(matrix) => {
1505 write!(
1506 f,
1507 "DenseDesignMatrix::Materialized({}x{})",
1508 matrix.nrows(),
1509 matrix.ncols()
1510 )
1511 }
1512 Self::Lazy(op) => write!(f, "DenseDesignMatrix::Lazy({}x{})", op.nrows(), op.ncols()),
1513 }
1514 }
1515}
1516
1517impl From<Arc<Array2<f64>>> for DenseDesignMatrix {
1518 fn from(value: Arc<Array2<f64>>) -> Self {
1519 Self::Materialized(value)
1520 }
1521}
1522
1523impl From<Array2<f64>> for DenseDesignMatrix {
1524 fn from(value: Array2<f64>) -> Self {
1525 Self::Materialized(Arc::new(value))
1526 }
1527}
1528
1529impl<T> From<Arc<T>> for DenseDesignMatrix
1530where
1531 T: DenseDesignOperator + 'static,
1532{
1533 fn from(value: Arc<T>) -> Self {
1534 Self::Lazy(LazyDense::new(value))
1535 }
1536}
1537
1538impl DenseDesignMatrix {
1539 pub fn cache_identity(&self) -> usize {
1548 match self {
1549 Self::Materialized(matrix) => Arc::as_ptr(matrix) as *const () as usize,
1550 Self::Lazy(lazy) => lazy.operator_arc_identity(),
1551 }
1552 }
1553
1554 pub fn nrows(&self) -> usize {
1555 match self {
1556 Self::Materialized(matrix) => matrix.nrows(),
1557 Self::Lazy(op) => op.nrows(),
1558 }
1559 }
1560
1561 pub fn ncols(&self) -> usize {
1562 match self {
1563 Self::Materialized(matrix) => matrix.ncols(),
1564 Self::Lazy(op) => op.ncols(),
1565 }
1566 }
1567
1568 pub fn as_dense_ref(&self) -> Option<&Array2<f64>> {
1569 match self {
1570 Self::Materialized(matrix) => Some(matrix.as_ref()),
1571 Self::Lazy(lazy) => lazy
1572 .dense_memo
1573 .get()
1574 .map(|governed| governed.as_ref().as_ref())
1575 .or_else(|| lazy.op.as_dense_ref()),
1576 }
1577 }
1578
1579 pub const fn is_materialized_dense(&self) -> bool {
1580 matches!(self, Self::Materialized(_))
1581 }
1582
1583 pub const fn is_operator_backed(&self) -> bool {
1584 matches!(self, Self::Lazy(_))
1585 }
1586
1587 pub fn to_dense(&self) -> Array2<f64> {
1588 match self {
1589 Self::Materialized(matrix) => matrix.as_ref().clone(),
1590 Self::Lazy(lazy) => {
1591 let policy = ResourcePolicy::default_library();
1592 panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
1593 "DenseDesignMatrix::to_dense",
1594 lazy.nrows(),
1595 lazy.ncols(),
1596 &policy,
1597 )
1598 .unwrap_or_else(|reason| std::panic::panic_any(reason));
1599 enforce_operator_materialization_policy(
1600 lazy.op.as_ref(),
1601 "DenseDesignMatrix::to_dense",
1602 )
1603 .unwrap_or_else(|reason| std::panic::panic_any(reason));
1604 if let Some(governed) = lazy.dense_memo.get() {
1605 return governed.as_ref().as_ref().clone();
1608 }
1609 let construction_charge = MemoryGovernor::global()
1615 .try_reserve_dense_f64(
1616 lazy.nrows(),
1617 lazy.ncols(),
1618 "DenseDesignMatrix::to_dense",
1619 )
1620 .unwrap_or_else(|err| std::panic::panic_any(err.to_string()));
1622 let dense =
1623 dense_operator_to_dense_by_chunks(lazy.op.as_ref()).unwrap_or_else(|err| {
1624 std::panic::panic_any(format!(
1625 "DenseDesignMatrix::to_dense: failed to materialize {}x{} \
1626 operator-backed design via row chunks: {err}",
1627 lazy.nrows(),
1628 lazy.ncols(),
1629 ))
1630 });
1631 drop(construction_charge);
1634 dense
1635 }
1636 }
1637 }
1638
1639 pub fn to_dense_arc(&self) -> Arc<Array2<f64>> {
1640 match self {
1641 Self::Materialized(matrix) => Arc::clone(matrix),
1642 Self::Lazy(lazy) => {
1643 let policy = ResourcePolicy::default_library();
1644 panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
1645 "DenseDesignMatrix::to_dense_arc",
1646 lazy.nrows(),
1647 lazy.ncols(),
1648 &policy,
1649 )
1650 .unwrap_or_else(|reason| std::panic::panic_any(reason));
1651 lazy.try_governed_dense_arc("DenseDesignMatrix::to_dense_arc")
1652 .unwrap_or_else(|msg| std::panic::panic_any(msg))
1654 }
1655 }
1656 }
1657
1658 pub fn try_to_dense_arc(&self, context: &str) -> Result<Arc<Array2<f64>>, String> {
1659 let policy = ResourcePolicy::default_library();
1671 self.try_to_dense_arc_with_policy(context, &policy)
1672 }
1673
1674 pub fn try_to_dense_arc_with_policy(
1686 &self,
1687 context: &str,
1688 policy: &ResourcePolicy,
1689 ) -> Result<Arc<Array2<f64>>, String> {
1690 match self {
1691 Self::Materialized(matrix) => Ok(Arc::clone(matrix)),
1692 Self::Lazy(lazy) => {
1693 panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
1694 context,
1695 lazy.nrows(),
1696 lazy.ncols(),
1697 policy,
1698 )?;
1699 lazy.try_governed_dense_arc(context)
1700 }
1701 }
1702 }
1703
1704 pub fn try_row_chunk(
1705 &self,
1706 rows: Range<usize>,
1707 ) -> Result<Array2<f64>, MatrixMaterializationError> {
1708 match self {
1709 Self::Materialized(matrix) => Ok(matrix.slice(s![rows, ..]).to_owned()),
1710 Self::Lazy(op) => op.try_row_chunk(rows),
1711 }
1712 }
1713
1714 pub fn row_chunk_into(
1715 &self,
1716 rows: Range<usize>,
1717 out: ArrayViewMut2<'_, f64>,
1718 ) -> Result<(), MatrixMaterializationError> {
1719 match self {
1720 Self::Materialized(matrix) => {
1721 let mut out = out;
1722 out.assign(&matrix.slice(s![rows, ..]));
1723 Ok(())
1724 }
1725 Self::Lazy(op) => op.row_chunk_into(rows, out),
1726 }
1727 }
1728}
1729
1730impl LinearOperator for DenseDesignMatrix {
1731 fn nrows(&self) -> usize {
1732 DenseDesignMatrix::nrows(self)
1733 }
1734
1735 fn ncols(&self) -> usize {
1736 DenseDesignMatrix::ncols(self)
1737 }
1738
1739 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
1740 match self {
1741 Self::Materialized(matrix) => fast_av(matrix, vector),
1742 Self::Lazy(op) => op.apply(vector),
1743 }
1744 }
1745
1746 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
1747 match self {
1748 Self::Materialized(matrix) => fast_atv(matrix, vector),
1749 Self::Lazy(op) => op.apply_transpose(vector),
1750 }
1751 }
1752
1753 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
1754 certify_signed_weights("DenseDesignMatrix::diag_xtw_x", weights, self.nrows())?;
1755 match self {
1756 Self::Materialized(matrix) => {
1757 let mut xtwx = Array2::<f64>::zeros((matrix.ncols(), matrix.ncols()));
1758 stream_weighted_crossprod_into(
1759 matrix,
1760 weights,
1761 &mut xtwx,
1762 CrossprodStructure::Full,
1763 CrossprodAccum::Replace,
1764 effective_global_parallelism(),
1765 );
1766 Ok(xtwx)
1767 }
1768 Self::Lazy(op) => op.diag_xtw_x(weights),
1769 }
1770 }
1771
1772 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
1773 certify_signed_weights("DenseDesignMatrix::diag_gram", weights, self.nrows())?;
1776 match self {
1777 Self::Materialized(matrix) => {
1778 let n = matrix.nrows();
1779 let p = matrix.ncols();
1780 if (n as u64) * (p as u64) < DENSE_ROW_PARALLEL_MIN_NP {
1781 let mut diag = Array1::<f64>::zeros(p);
1782 for i in 0..n {
1783 let wi = weights[i];
1784 if wi == 0.0 {
1785 continue;
1786 }
1787 for j in 0..p {
1788 let xij = matrix[[i, j]];
1789 diag[j] += wi * xij * xij;
1790 }
1791 }
1792 return Ok(diag);
1793 }
1794 let diag = crate::pairwise_reduce::par_deterministic_block_fold(
1797 n,
1798 |range: core::ops::Range<usize>| {
1799 let mut acc = Array1::<f64>::zeros(p);
1800 for i in range {
1801 let wi = weights[i];
1802 if wi != 0.0 {
1803 for j in 0..p {
1804 let xij = matrix[[i, j]];
1805 acc[j] += wi * xij * xij;
1806 }
1807 }
1808 }
1809 acc
1810 },
1811 |mut a, b| {
1812 a += &b;
1813 a
1814 },
1815 )
1816 .unwrap_or_else(|| Array1::<f64>::zeros(p));
1817 Ok(diag)
1818 }
1819 Self::Lazy(op) => op.diag_gram(weights),
1820 }
1821 }
1822
1823 fn apply_weighted_normal(
1824 &self,
1825 weights: FiniteSignedWeightsView<'_>,
1826 vector: &Array1<f64>,
1827 penalty: Option<&Array2<f64>>,
1828 ridge: f64,
1829 ) -> Array1<f64> {
1830 assert_eq!(
1831 weights.len(),
1832 self.nrows(),
1833 "DenseDesignMatrix::apply_weighted_normal weight length mismatch"
1834 );
1835 assert_eq!(
1836 vector.len(),
1837 self.ncols(),
1838 "DenseDesignMatrix::apply_weighted_normal vector length mismatch"
1839 );
1840 let weights_view = weights.view();
1845 match self {
1846 Self::Materialized(matrix) => {
1847 let n = matrix.nrows();
1848 let p = matrix.ncols();
1849 let mut out = if (n as u64) * (p as u64) < DENSE_ROW_PARALLEL_MIN_NP {
1850 let mut out = Array1::<f64>::zeros(p);
1851 for i in 0..n {
1852 let wi = weights_view[i];
1853 if wi == 0.0 {
1854 continue;
1855 }
1856 let mut row_dot = 0.0_f64;
1857 for j in 0..p {
1858 row_dot += matrix[[i, j]] * vector[j];
1859 }
1860 if row_dot == 0.0 {
1861 continue;
1862 }
1863 let scaled = wi * row_dot;
1864 for j in 0..p {
1865 out[j] += scaled * matrix[[i, j]];
1866 }
1867 }
1868 out
1869 } else {
1870 crate::pairwise_reduce::par_deterministic_block_fold(
1873 n,
1874 |range: core::ops::Range<usize>| {
1875 let mut acc = Array1::<f64>::zeros(p);
1876 for i in range {
1877 let wi = weights_view[i];
1878 if wi != 0.0 {
1879 let mut row_dot = 0.0_f64;
1880 for j in 0..p {
1881 row_dot += matrix[[i, j]] * vector[j];
1882 }
1883 if row_dot != 0.0 {
1884 let scaled = wi * row_dot;
1885 for j in 0..p {
1886 acc[j] += scaled * matrix[[i, j]];
1887 }
1888 }
1889 }
1890 }
1891 acc
1892 },
1893 |mut a, b| {
1894 a += &b;
1895 a
1896 },
1897 )
1898 .unwrap_or_else(|| Array1::<f64>::zeros(p))
1899 };
1900 if let Some(pen) = penalty {
1901 out += &fast_av(pen, vector);
1902 }
1903 if ridge > 0.0 {
1904 for j in 0..p {
1905 out[j] += ridge * vector[j];
1906 }
1907 }
1908 out
1909 }
1910 Self::Lazy(op) => op.apply_weighted_normal(weights, vector, penalty, ridge),
1911 }
1912 }
1913
1914 fn uses_matrix_free_pcg(&self) -> bool {
1915 match self {
1916 Self::Materialized(_) => true,
1917 Self::Lazy(op) => op.uses_matrix_free_pcg(),
1918 }
1919 }
1920}
1921
1922impl DenseDesignOperator for DenseDesignMatrix {
1923 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
1924 if y.len() != self.nrows() {
1925 return Err(format!(
1926 "DenseDesignMatrix::compute_xtwy response length mismatch: y={}, nrows={}",
1927 y.len(),
1928 self.nrows()
1929 ));
1930 }
1931 certify_signed_weights("DenseDesignMatrix::compute_xtwy", weights, self.nrows())?;
1932 match self {
1933 Self::Materialized(matrix) => {
1934 Ok(dense_transpose_weighted_response(matrix, weights, y, None))
1935 }
1936 Self::Lazy(op) => op.compute_xtwy(weights, y),
1937 }
1938 }
1939
1940 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
1941 match self {
1942 Self::Materialized(matrix) => {
1943 if middle.nrows() != matrix.ncols() || middle.ncols() != matrix.ncols() {
1944 return Err(format!(
1945 "quadratic_form_diag dimension mismatch: matrix is {}x{}, expected {}x{}",
1946 middle.nrows(),
1947 middle.ncols(),
1948 matrix.ncols(),
1949 matrix.ncols()
1950 ));
1951 }
1952 let xc = fast_ab(matrix, middle);
1953 let n = matrix.nrows();
1954 let p = matrix.ncols();
1955 let mut out = Array1::<f64>::zeros(n);
1956 if matrix.is_standard_layout()
1957 && xc.is_standard_layout()
1958 && let (Some(m_all), Some(xc_all), Some(out_slice)) =
1959 (matrix.as_slice(), xc.as_slice(), out.as_slice_mut())
1960 {
1961 use rayon::iter::{IndexedParallelIterator, ParallelIterator};
1966 use rayon::slice::ParallelSliceMut;
1967 out_slice
1968 .par_chunks_mut(1)
1969 .enumerate()
1970 .for_each(|(i, slot)| {
1971 let off = i * p;
1972 let m_row = &m_all[off..off + p];
1973 let xc_row = &xc_all[off..off + p];
1974 let mut acc = 0.0_f64;
1975 for j in 0..p {
1976 acc += m_row[j] * xc_row[j];
1977 }
1978 slot[0] = acc.max(0.0);
1981 });
1982 } else {
1983 for i in 0..n {
1984 out[i] = matrix.row(i).dot(&xc.row(i)).max(0.0);
1987 }
1988 }
1989 Ok(out)
1990 }
1991 Self::Lazy(op) => op.quadratic_form_diag(middle),
1992 }
1993 }
1994
1995 fn as_dense_ref(&self) -> Option<&Array2<f64>> {
1996 DenseDesignMatrix::as_dense_ref(self)
1997 }
1998
1999 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
2000 match self {
2001 Self::Materialized(_) => None,
2002 Self::Lazy(lazy) => lazy.op.materialization_policy(),
2003 }
2004 }
2005
2006 fn row_chunk_into(
2007 &self,
2008 rows: Range<usize>,
2009 mut out: ArrayViewMut2<'_, f64>,
2010 ) -> Result<(), MatrixMaterializationError> {
2011 if out.nrows() != rows.end - rows.start || out.ncols() != self.ncols() {
2012 return Err(MatrixMaterializationError::MissingRowChunk {
2013 context: "DenseDesignMatrix::row_chunk_into shape mismatch",
2014 });
2015 }
2016 match self {
2017 Self::Materialized(matrix) => {
2018 out.assign(&matrix.slice(s![rows, ..]));
2019 Ok(())
2020 }
2021 Self::Lazy(op) => op.row_chunk_into(rows, out),
2022 }
2023 }
2024
2025 fn to_dense(&self) -> Array2<f64> {
2026 DenseDesignMatrix::to_dense(self)
2027 }
2028
2029 fn to_dense_arc(&self) -> Arc<Array2<f64>> {
2030 DenseDesignMatrix::to_dense_arc(self)
2031 }
2032}
2033
2034pub struct ReparamOperator {
2049 x_original: DesignMatrix,
2050 qs: Arc<Array2<f64>>,
2051 n: usize,
2052 p: usize,
2053}
2054
2055impl ReparamOperator {
2056 pub fn new(x_original: DesignMatrix, qs: Arc<Array2<f64>>) -> Self {
2057 let n = x_original.nrows();
2058 let p = qs.ncols();
2059 assert_eq!(
2060 x_original.ncols(),
2061 qs.nrows(),
2062 "ReparamOperator: X cols ({}) must match Qs rows ({})",
2063 x_original.ncols(),
2064 qs.nrows()
2065 );
2066 Self {
2067 x_original,
2068 qs,
2069 n,
2070 p,
2071 }
2072 }
2073
2074 pub fn x_original(&self) -> &DesignMatrix {
2076 &self.x_original
2077 }
2078
2079 pub fn qs(&self) -> &Array2<f64> {
2081 &self.qs
2082 }
2083}
2084
2085impl LinearOperator for ReparamOperator {
2086 fn nrows(&self) -> usize {
2087 self.n
2088 }
2089
2090 fn ncols(&self) -> usize {
2091 self.p
2092 }
2093
2094 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
2095 let qv = self.qs.dot(vector);
2097 self.x_original.apply(&qv)
2098 }
2099
2100 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
2101 let xtv = self.x_original.apply_transpose(vector);
2103 fast_atv(&self.qs, &xtv)
2104 }
2105
2106 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
2107 let xtwx = self.x_original.diag_xtw_x(weights)?;
2110 let tmp = fast_atb(&self.qs, &xtwx);
2111 Ok(fast_ab(&tmp, &self.qs))
2112 }
2113
2114 fn apply_weighted_normal(
2115 &self,
2116 weights: FiniteSignedWeightsView<'_>,
2117 vector: &Array1<f64>,
2118 penalty: Option<&Array2<f64>>,
2119 ridge: f64,
2120 ) -> Array1<f64> {
2121 assert_eq!(
2122 weights.len(),
2123 self.x_original.nrows(),
2124 "ReparamOperator::apply_weighted_normal weight length mismatch"
2125 );
2126 assert_eq!(
2127 vector.len(),
2128 self.qs.ncols(),
2129 "ReparamOperator::apply_weighted_normal vector length mismatch"
2130 );
2131 let weights = weights.view();
2134 let qv = self.qs.dot(vector);
2135 let xqv = self.x_original.apply(&qv);
2136 let mut wxqv = xqv;
2137 for i in 0..wxqv.len() {
2138 wxqv[i] *= weights[i];
2139 }
2140 let xtw = self.x_original.apply_transpose(&wxqv);
2141 let mut out = fast_atv(&self.qs, &xtw);
2142 if let Some(pen) = penalty {
2143 out += &fast_av(pen, vector);
2144 }
2145 if ridge > 0.0 {
2146 out.scaled_add(ridge, vector);
2148 }
2149 out
2150 }
2151}
2152
2153impl DenseDesignOperator for ReparamOperator {
2154 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
2155 let xtwy = self.x_original.compute_xtwy(weights, y)?;
2157 Ok(fast_atv(&self.qs, &xtwy))
2158 }
2159
2160 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
2161 let qm = fast_ab(&self.qs, middle);
2164 let m_orig = fast_ab(&qm, &self.qs.t().to_owned());
2165 self.x_original.quadratic_form_diag(&m_orig)
2166 }
2167
2168 fn to_dense(&self) -> Array2<f64> {
2169 match &self.x_original {
2170 DesignMatrix::Dense(x) => fast_ab(x.to_dense_arc().as_ref(), &self.qs),
2171 _ => {
2172 let x_dense = self.x_original.to_dense();
2173 fast_ab(&x_dense, &self.qs)
2174 }
2175 }
2176 }
2177
2178 fn to_dense_arc(&self) -> Arc<Array2<f64>> {
2179 Arc::new(self.to_dense())
2180 }
2181
2182 fn as_dense_ref(&self) -> Option<&Array2<f64>> {
2183 None
2184 }
2185
2186 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
2187 self.x_original.materialization_policy()
2188 }
2189
2190 fn apply_columns(&self, cols: &[usize]) -> Array2<f64> {
2191 let qs_cols = self.qs.select(Axis(1), cols);
2194 match &self.x_original {
2195 DesignMatrix::Dense(x) => match x.as_dense_ref() {
2196 Some(x_dense) => fast_ab(x_dense, &qs_cols),
2197 None => {
2198 let n = self.n;
2199 let mut out = Array2::<f64>::zeros((n, cols.len()));
2200 for k in 0..cols.len() {
2201 let col = qs_cols.column(k).to_owned();
2202 let xc = self.x_original.apply(&col);
2203 out.column_mut(k).assign(&xc);
2204 }
2205 out
2206 }
2207 },
2208 DesignMatrix::Sparse(_) => {
2209 let n = self.n;
2211 let mut out = Array2::<f64>::zeros((n, cols.len()));
2212 for k in 0..cols.len() {
2213 let col = qs_cols.column(k).to_owned();
2214 let xc = self.x_original.apply(&col);
2215 out.column_mut(k).assign(&xc);
2216 }
2217 out
2218 }
2219 }
2220 }
2221
2222 fn row_chunk_into(
2223 &self,
2224 rows: Range<usize>,
2225 mut out: ArrayViewMut2<'_, f64>,
2226 ) -> Result<(), MatrixMaterializationError> {
2227 if out.nrows() != rows.end - rows.start || out.ncols() != self.p {
2228 return Err(MatrixMaterializationError::MissingRowChunk {
2229 context: "ReparamOperator::row_chunk_into shape mismatch",
2230 });
2231 }
2232 match &self.x_original {
2233 DesignMatrix::Dense(x) => {
2234 let chunk = x.try_row_chunk(rows)?;
2235 out.assign(&fast_ab(&chunk, &self.qs));
2236 }
2237 DesignMatrix::Sparse(sdm) => {
2238 let csr = sdm
2240 .to_csr_arc()
2241 .ok_or(MatrixMaterializationError::MissingRowChunk {
2242 context: "ReparamOperator::row_chunk_into: failed to obtain CSR view",
2243 })?;
2244 let sym = csr.symbolic();
2245 let row_ptr = sym.row_ptr();
2246 let col_idx = sym.col_idx();
2247 let vals = csr.val();
2248 let chunk_rows = rows.end - rows.start;
2249 let p_inner = sdm.ncols();
2250 let mut chunk = Array2::<f64>::zeros((chunk_rows, p_inner));
2251 for (local, global) in (rows.start..rows.end).enumerate() {
2252 for ptr in row_ptr[global]..row_ptr[global + 1] {
2253 chunk[[local, col_idx[ptr]]] = vals[ptr];
2254 }
2255 }
2256 out.assign(&fast_ab(&chunk, &self.qs));
2257 }
2258 }
2259 Ok(())
2260 }
2261}
2262
2263#[derive(Clone)]
2273pub struct RandomEffectOperator {
2274 pub group_ids: Vec<Option<usize>>,
2278 pub n: usize,
2280 pub num_groups: usize,
2282}
2283
2284impl RandomEffectOperator {
2285 pub fn new(group_ids: Vec<Option<usize>>, num_groups: usize) -> Self {
2286 let n = group_ids.len();
2287 Self {
2288 group_ids,
2289 n,
2290 num_groups,
2291 }
2292 }
2293
2294 pub fn weighted_cross_with_dense(
2300 &self,
2301 dense: &Array2<f64>,
2302 weights: &Array1<f64>,
2303 ) -> Result<Array2<f64>, String> {
2304 if dense.nrows() != self.n {
2305 return Err(format!(
2306 "RandomEffectOperator::weighted_cross_with_dense row mismatch: dense={}, nrows={}",
2307 dense.nrows(),
2308 self.n
2309 ));
2310 }
2311 certify_signed_weights(
2312 "RandomEffectOperator::weighted_cross_with_dense",
2313 weights,
2314 self.n,
2315 )?;
2316 let p_dense = dense.ncols();
2317 let mut cross = Array2::<f64>::zeros((p_dense, self.num_groups));
2318 for i in 0..self.n {
2319 if let Some(g) = self.group_ids[i] {
2320 let wi = weights[i];
2321 if wi == 0.0 {
2322 continue;
2323 }
2324 for j in 0..p_dense {
2325 cross[[j, g]] += wi * dense[[i, j]];
2326 }
2327 }
2328 }
2329 Ok(cross)
2330 }
2331
2332 pub fn weighted_cross_with_re(
2336 &self,
2337 other: &RandomEffectOperator,
2338 weights: &Array1<f64>,
2339 ) -> Result<Array2<f64>, String> {
2340 if other.n != self.n {
2341 return Err(format!(
2342 "RandomEffectOperator::weighted_cross_with_re row mismatch: other={}, nrows={}",
2343 other.n, self.n
2344 ));
2345 }
2346 certify_signed_weights(
2347 "RandomEffectOperator::weighted_cross_with_re",
2348 weights,
2349 self.n,
2350 )?;
2351 let mut cross = Array2::<f64>::zeros((self.num_groups, other.num_groups));
2352 for i in 0..self.n {
2353 if let (Some(a), Some(b)) = (self.group_ids[i], other.group_ids[i]) {
2354 let wi = weights[i];
2355 if wi != 0.0 {
2356 cross[[a, b]] += wi;
2357 }
2358 }
2359 }
2360 Ok(cross)
2361 }
2362}
2363
2364impl LinearOperator for RandomEffectOperator {
2365 fn nrows(&self) -> usize {
2366 self.n
2367 }
2368
2369 fn ncols(&self) -> usize {
2370 self.num_groups
2371 }
2372
2373 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
2375 use rayon::prelude::*;
2376 let out: Vec<f64> = self
2377 .group_ids
2378 .par_iter()
2379 .map(|g| g.map(|g| vector[g]).unwrap_or(0.0))
2380 .collect();
2381 Array1::from(out)
2382 }
2383
2384 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
2386 let mut out = Array1::<f64>::zeros(self.num_groups);
2387 for i in 0..self.n {
2388 if let Some(g) = self.group_ids[i] {
2389 out[g] += vector[i];
2390 }
2391 }
2392 out
2393 }
2394
2395 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
2397 certify_signed_weights("RandomEffectOperator::diag_xtw_x", weights, self.n)?;
2398 let q = self.num_groups;
2399 let mut xtwx = Array2::<f64>::zeros((q, q));
2400 for i in 0..self.n {
2401 if let Some(g) = self.group_ids[i] {
2402 xtwx[[g, g]] += weights[i];
2403 }
2404 }
2405 Ok(xtwx)
2406 }
2407
2408 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
2410 certify_signed_weights("RandomEffectOperator::diag_gram", weights, self.n)?;
2411 let mut diag = Array1::<f64>::zeros(self.num_groups);
2412 for i in 0..self.n {
2413 if let Some(g) = self.group_ids[i] {
2414 diag[g] += weights[i];
2415 }
2416 }
2417 Ok(diag)
2418 }
2419
2420 fn apply_weighted_normal(
2422 &self,
2423 weights: FiniteSignedWeightsView<'_>,
2424 vector: &Array1<f64>,
2425 penalty: Option<&Array2<f64>>,
2426 ridge: f64,
2427 ) -> Array1<f64> {
2428 assert_eq!(
2429 weights.len(),
2430 self.n,
2431 "RandomEffectOperator::apply_weighted_normal weight length mismatch"
2432 );
2433 assert_eq!(
2434 vector.len(),
2435 self.num_groups,
2436 "RandomEffectOperator::apply_weighted_normal vector length mismatch"
2437 );
2438 let weights = weights.view();
2442 let mut group_wacc = Array1::<f64>::zeros(self.num_groups);
2443 for i in 0..self.n {
2444 if let Some(g) = self.group_ids[i] {
2445 group_wacc[g] += weights[i];
2446 }
2447 }
2448 let mut out = Array1::<f64>::zeros(self.num_groups);
2449 for g in 0..self.num_groups {
2450 out[g] = group_wacc[g] * vector[g];
2451 }
2452 if let Some(pen) = penalty {
2453 out += &pen.dot(vector);
2454 }
2455 if ridge > 0.0 {
2456 for g in 0..self.num_groups {
2457 out[g] += ridge * vector[g];
2458 }
2459 }
2460 out
2461 }
2462
2463 fn uses_matrix_free_pcg(&self) -> bool {
2464 true
2465 }
2466}
2467
2468impl DenseDesignOperator for RandomEffectOperator {
2469 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
2470 if weights.len() != self.n || y.len() != self.n {
2471 return Err(format!(
2472 "RandomEffectOperator::compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
2473 weights.len(),
2474 y.len(),
2475 self.n
2476 ));
2477 }
2478 certify_signed_weights("RandomEffectOperator::compute_xtwy", weights, self.n)?;
2479 let mut out = Array1::<f64>::zeros(self.num_groups);
2480 for i in 0..self.n {
2481 if let Some(g) = self.group_ids[i] {
2482 let wi = weights[i];
2483 out[g] += wi * y[i];
2484 }
2485 }
2486 Ok(out)
2487 }
2488
2489 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
2491 use rayon::prelude::*;
2492 let out: Vec<f64> = self
2493 .group_ids
2494 .par_iter()
2495 .map(|g| g.map(|g| middle[[g, g]].max(0.0)).unwrap_or(0.0))
2496 .collect();
2497 Ok(Array1::from(out))
2498 }
2499
2500 fn row_chunk_into(
2501 &self,
2502 rows: Range<usize>,
2503 mut out: ArrayViewMut2<'_, f64>,
2504 ) -> Result<(), MatrixMaterializationError> {
2505 if out.nrows() != rows.end - rows.start || out.ncols() != self.num_groups {
2506 return Err(MatrixMaterializationError::MissingRowChunk {
2507 context: "RandomEffectOperator::row_chunk_into shape mismatch",
2508 });
2509 }
2510 out.fill(0.0);
2511 for (local, global) in rows.enumerate() {
2512 if let Some(g) = self.group_ids[global] {
2513 out[[local, g]] = 1.0;
2514 }
2515 }
2516 Ok(())
2517 }
2518
2519 fn to_dense(&self) -> Array2<f64> {
2521 let mut out = Array2::<f64>::zeros((self.n, self.num_groups));
2522 ndarray::Zip::indexed(out.rows_mut()).par_for_each(|i, mut row| {
2523 if let Some(g) = self.group_ids[i] {
2524 row[g] = 1.0;
2525 }
2526 });
2527 out
2528 }
2529}
2530
2531#[derive(Clone)]
2537pub enum DesignBlock {
2538 Dense(DenseDesignMatrix),
2539 Sparse(SparseDesignMatrix),
2540 RandomEffect(Arc<RandomEffectOperator>),
2541 Intercept(usize),
2543}
2544
2545impl DesignBlock {
2546 pub fn nrows(&self) -> usize {
2547 match self {
2548 Self::Dense(d) => d.nrows(),
2549 Self::Sparse(s) => s.nrows(),
2550 Self::RandomEffect(op) => op.nrows(),
2551 Self::Intercept(n) => *n,
2552 }
2553 }
2554
2555 pub fn ncols(&self) -> usize {
2556 match self {
2557 Self::Dense(d) => d.ncols(),
2558 Self::Sparse(s) => s.ncols(),
2559 Self::RandomEffect(op) => op.ncols(),
2560 Self::Intercept(_) => 1,
2561 }
2562 }
2563
2564 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
2565 match self {
2566 Self::Dense(design) => design.materialization_policy(),
2567 Self::Sparse(_) | Self::RandomEffect(_) | Self::Intercept(_) => None,
2568 }
2569 }
2570
2571 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
2572 match self {
2573 Self::Dense(d) => d.apply(vector),
2574 Self::Sparse(s) => DesignMatrix::Sparse(s.clone()).apply(vector),
2575 Self::RandomEffect(op) => op.apply(vector),
2576 Self::Intercept(n) => Array1::from_elem(*n, vector[0]),
2577 }
2578 }
2579
2580 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
2581 match self {
2582 Self::Dense(d) => d.apply_transpose(vector),
2583 Self::Sparse(s) => DesignMatrix::Sparse(s.clone()).apply_transpose(vector),
2584 Self::RandomEffect(op) => op.apply_transpose(vector),
2585 Self::Intercept(_) => {
2586 let sum: f64 = vector.iter().sum();
2587 Array1::from_vec(vec![sum])
2588 }
2589 }
2590 }
2591
2592 fn try_row_chunk(&self, rows: Range<usize>) -> Result<Array2<f64>, MatrixMaterializationError> {
2593 match self {
2594 Self::Dense(d) => d.try_row_chunk(rows),
2595 Self::Sparse(s) => DesignMatrix::Sparse(s.clone()).try_row_chunk(rows),
2596 Self::RandomEffect(op) => op.try_row_chunk(rows),
2597 Self::Intercept(_) => Ok(Array2::ones((rows.end - rows.start, 1))),
2598 }
2599 }
2600
2601 fn row_chunk_into(
2602 &self,
2603 rows: Range<usize>,
2604 mut out: ArrayViewMut2<'_, f64>,
2605 ) -> Result<(), MatrixMaterializationError> {
2606 if out.nrows() != rows.end - rows.start || out.ncols() != self.ncols() {
2607 return Err(MatrixMaterializationError::MissingRowChunk {
2608 context: "DesignBlock::row_chunk_into shape mismatch",
2609 });
2610 }
2611 match self {
2612 Self::Dense(d) => d.row_chunk_into(rows, out),
2613 Self::Sparse(s) => s.row_chunk_into(rows, out),
2614 Self::RandomEffect(op) => op.row_chunk_into(rows, out),
2615 Self::Intercept(_) => {
2616 out.fill(1.0);
2617 Ok(())
2618 }
2619 }
2620 }
2621
2622 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
2623 certify_signed_weights("DesignBlock::diag_xtw_x", weights, self.nrows())?;
2624 match self {
2625 Self::Dense(d) => d.diag_xtw_x(weights),
2626 Self::Sparse(s) => DesignMatrix::Sparse(s.clone()).diag_xtw_x(weights),
2627 Self::RandomEffect(op) => op.diag_xtw_x(weights),
2628 Self::Intercept(_) => {
2629 let sum: f64 = weights.iter().sum();
2636 Ok(Array2::from_elem((1, 1), sum))
2637 }
2638 }
2639 }
2640
2641 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
2642 certify_signed_weights("DesignBlock::diag_gram", weights, self.nrows())?;
2643 match self {
2644 Self::Dense(d) => d.diag_gram(weights),
2645 Self::Sparse(s) => DesignMatrix::Sparse(s.clone()).diag_gram(weights),
2646 Self::RandomEffect(op) => op.diag_gram(weights),
2647 Self::Intercept(_) => {
2648 let sum: f64 = weights.iter().sum();
2652 Ok(Array1::from_vec(vec![sum]))
2653 }
2654 }
2655 }
2656
2657 fn to_dense(&self) -> Array2<f64> {
2659 match self {
2660 Self::Dense(d) => d.to_dense(),
2661 Self::Sparse(s) => s.to_dense_arc().as_ref().clone(),
2662 Self::RandomEffect(op) => op.to_dense(),
2663 Self::Intercept(n) => Array2::ones((*n, 1)),
2664 }
2665 }
2666}
2667
2668#[derive(Clone)]
2675pub struct BlockDesignOperator {
2676 pub blocks: Vec<DesignBlock>,
2677 pub col_offsets: Vec<usize>,
2679 pub total_cols: usize,
2680 pub n: usize,
2681}
2682
2683impl BlockDesignOperator {
2684 pub fn new(blocks: Vec<DesignBlock>) -> Result<Self, String> {
2685 if blocks.is_empty() {
2686 return Err("BlockDesignOperator: need at least one block".to_string());
2687 }
2688 let n = blocks[0].nrows();
2689 for (i, b) in blocks.iter().enumerate() {
2690 if b.nrows() != n {
2691 return Err(format!(
2692 "BlockDesignOperator: block {i} has {} rows, expected {n}",
2693 b.nrows()
2694 ));
2695 }
2696 }
2697 let mut col_offsets = Vec::with_capacity(blocks.len() + 1);
2698 col_offsets.push(0);
2699 let mut total_cols = 0usize;
2702 for b in &blocks {
2703 total_cols += b.ncols();
2704 col_offsets.push(total_cols);
2705 }
2706 Ok(Self {
2707 blocks,
2708 col_offsets,
2709 total_cols,
2710 n,
2711 })
2712 }
2713
2714 fn weighted_cross_chunked(
2715 &self,
2716 left: &DesignBlock,
2717 right: &DesignBlock,
2718 weights: &Array1<f64>,
2719 ) -> Result<Array2<f64>, String> {
2720 let pi = left.ncols();
2721 let pj = right.ncols();
2722 let mut cross = Array2::<f64>::zeros((pi, pj));
2723 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2724 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2725 let left_chunk = left.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2726 let right_chunk = right.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2727 for local in 0..(end - start) {
2728 let wi = weights[start + local];
2736 if wi == 0.0 {
2737 continue;
2738 }
2739 for a in 0..pi {
2740 let scaled = wi * left_chunk[[local, a]];
2741 if scaled == 0.0 {
2742 continue;
2743 }
2744 for b in 0..pj {
2745 cross[[a, b]] += scaled * right_chunk[[local, b]];
2746 }
2747 }
2748 }
2749 }
2750 Ok(cross)
2751 }
2752
2753 fn quadratic_form_diag_cross_chunked(
2754 &self,
2755 block_a: &DesignBlock,
2756 block_b: &DesignBlock,
2757 m_ab: &Array2<f64>,
2758 ) -> Result<Array1<f64>, String> {
2759 let mut out = Array1::<f64>::zeros(self.n);
2760 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2761 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2762 let a_chunk = block_a
2763 .try_row_chunk(start..end)
2764 .map_err(|e| e.to_string())?;
2765 let b_chunk = block_b
2766 .try_row_chunk(start..end)
2767 .map_err(|e| e.to_string())?;
2768 let a_m = fast_ab(&a_chunk, m_ab);
2769 for local in 0..(end - start) {
2770 out[start + local] = a_m.row(local).dot(&b_chunk.row(local));
2771 }
2772 }
2773 Ok(out)
2774 }
2775
2776 fn cross_block(
2778 &self,
2779 i: usize,
2780 j: usize,
2781 weights: &Array1<f64>,
2782 ) -> Result<Array2<f64>, String> {
2783 match (&self.blocks[i], &self.blocks[j]) {
2784 (DesignBlock::Dense(d_i), DesignBlock::Dense(d_j)) => {
2786 if let (Some(xi), Some(xj)) = (d_i.as_dense_ref(), d_j.as_dense_ref()) {
2787 weighted_crossprod_dense(xi, weights, xj)
2788 } else {
2789 self.weighted_cross_chunked(&self.blocks[i], &self.blocks[j], weights)
2790 }
2791 }
2792 (DesignBlock::Dense(_), DesignBlock::Sparse(_))
2793 | (DesignBlock::Sparse(_), DesignBlock::Dense(_))
2794 | (DesignBlock::Sparse(_), DesignBlock::Sparse(_))
2795 | (DesignBlock::Sparse(_), DesignBlock::RandomEffect(_))
2796 | (DesignBlock::RandomEffect(_), DesignBlock::Sparse(_)) => {
2797 self.weighted_cross_chunked(&self.blocks[i], &self.blocks[j], weights)
2798 }
2799
2800 (DesignBlock::Dense(d), DesignBlock::RandomEffect(re)) => {
2802 if let Some(dense) = d.as_dense_ref() {
2803 re.weighted_cross_with_dense(dense, weights)
2804 } else {
2805 self.weighted_cross_chunked(&self.blocks[i], &self.blocks[j], weights)
2806 }
2807 }
2808 (DesignBlock::RandomEffect(re), DesignBlock::Dense(d)) => {
2809 if let Some(dense) = d.as_dense_ref() {
2810 let cross_t = re.weighted_cross_with_dense(dense, weights)?;
2811 Ok(cross_t.t().to_owned())
2812 } else {
2813 self.weighted_cross_chunked(&self.blocks[i], &self.blocks[j], weights)
2814 }
2815 }
2816
2817 (DesignBlock::RandomEffect(re_a), DesignBlock::RandomEffect(re_b)) => {
2819 re_a.weighted_cross_with_re(re_b, weights)
2820 }
2821
2822 (DesignBlock::Intercept(_), other) => {
2825 let pj = other.ncols();
2828 let mut cross = Array2::<f64>::zeros((1, pj));
2829 let row = other.apply_transpose(weights);
2830 cross.row_mut(0).assign(&row);
2831 Ok(cross)
2832 }
2833 (other, DesignBlock::Intercept(_)) => {
2834 let pi = other.ncols();
2835 let mut cross = Array2::<f64>::zeros((pi, 1));
2836 let col = other.apply_transpose(weights);
2837 cross.column_mut(0).assign(&col);
2838 Ok(cross)
2839 }
2840 }
2841 }
2842
2843 fn quadratic_form_diag_block(
2845 &self,
2846 block: &DesignBlock,
2847 m_kk: &Array2<f64>,
2848 ) -> Result<Array1<f64>, String> {
2849 match block {
2850 DesignBlock::Dense(d) => {
2851 if let Some(dense) = d.as_dense_ref() {
2852 let xm = fast_ab(dense, m_kk);
2853 let mut out = Array1::<f64>::zeros(self.n);
2854 ndarray::Zip::from(&mut out)
2855 .and(dense.rows())
2856 .and(xm.rows())
2857 .par_for_each(|o, dr, xmr| *o = dr.dot(&xmr));
2858 Ok(out)
2859 } else {
2860 d.quadratic_form_diag(m_kk)
2861 }
2862 }
2863 DesignBlock::Sparse(s) => {
2864 let sparse = DesignMatrix::Sparse(s.clone());
2865 sparse.quadratic_form_diag(m_kk)
2866 }
2867 DesignBlock::RandomEffect(re) => {
2868 use rayon::prelude::*;
2869 let out: Vec<f64> = re
2870 .group_ids
2871 .par_iter()
2872 .map(|g| g.map(|g| m_kk[[g, g]]).unwrap_or(0.0))
2873 .collect();
2874 Ok(Array1::from(out))
2875 }
2876 DesignBlock::Intercept(_) => {
2877 Ok(Array1::from_elem(self.n, m_kk[[0, 0]]))
2879 }
2880 }
2881 }
2882
2883 fn quadratic_form_diag_cross(
2885 &self,
2886 block_a: &DesignBlock,
2887 block_b: &DesignBlock,
2888 m_ab: &Array2<f64>,
2889 ) -> Result<Array1<f64>, String> {
2890 match (block_a, block_b) {
2891 (DesignBlock::Dense(da), DesignBlock::Dense(db)) => {
2892 if let (Some(da), Some(db)) = (da.as_dense_ref(), db.as_dense_ref()) {
2893 let da_m = fast_ab(da, m_ab);
2894 let mut out = Array1::<f64>::zeros(self.n);
2895 ndarray::Zip::from(&mut out)
2896 .and(da_m.rows())
2897 .and(db.rows())
2898 .par_for_each(|o, ar, br| *o = ar.dot(&br));
2899 Ok(out)
2900 } else {
2901 self.quadratic_form_diag_cross_chunked(block_a, block_b, m_ab)
2902 }
2903 }
2904 (DesignBlock::Dense(_), DesignBlock::Sparse(_))
2905 | (DesignBlock::Sparse(_), DesignBlock::Dense(_))
2906 | (DesignBlock::Sparse(_), DesignBlock::Sparse(_))
2907 | (DesignBlock::Sparse(_), DesignBlock::RandomEffect(_))
2908 | (DesignBlock::RandomEffect(_), DesignBlock::Sparse(_)) => {
2909 self.quadratic_form_diag_cross_chunked(block_a, block_b, m_ab)
2910 }
2911 (DesignBlock::Dense(d), DesignBlock::RandomEffect(re)) => {
2912 let mut out = Array1::<f64>::zeros(self.n);
2913 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2914 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2915 let chunk = d.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2916 for local in 0..chunk.nrows() {
2917 let i = start + local;
2918 if let Some(g) = re.group_ids[i] {
2919 let mut val = 0.0;
2920 for j in 0..chunk.ncols() {
2921 val += chunk[[local, j]] * m_ab[[j, g]];
2922 }
2923 out[i] = val;
2924 }
2925 }
2926 }
2927 Ok(out)
2928 }
2929 (DesignBlock::RandomEffect(re), DesignBlock::Dense(d)) => {
2930 let mut out = Array1::<f64>::zeros(self.n);
2931 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2932 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2933 let chunk = d.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2934 for local in 0..chunk.nrows() {
2935 let i = start + local;
2936 if let Some(g) = re.group_ids[i] {
2937 let mut val = 0.0;
2938 for j in 0..chunk.ncols() {
2939 val += m_ab[[g, j]] * chunk[[local, j]];
2940 }
2941 out[i] = val;
2942 }
2943 }
2944 }
2945 Ok(out)
2946 }
2947 (DesignBlock::RandomEffect(re_a), DesignBlock::RandomEffect(re_b)) => {
2948 use rayon::prelude::*;
2949 let out: Vec<f64> = re_a
2950 .group_ids
2951 .par_iter()
2952 .zip(re_b.group_ids.par_iter())
2953 .map(|(ga, gb)| match (ga, gb) {
2954 (Some(ga), Some(gb)) => m_ab[[*ga, *gb]],
2955 _ => 0.0,
2956 })
2957 .collect();
2958 Ok(Array1::from(out))
2959 }
2960
2961 (DesignBlock::Intercept(_), other) => {
2963 let m_row = m_ab.row(0);
2964 let mut out = Array1::<f64>::zeros(self.n);
2965 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2966 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2967 let chunk = other.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2968 for local in 0..(end - start) {
2969 out[start + local] = chunk.row(local).dot(&m_row);
2970 }
2971 }
2972 Ok(out)
2973 }
2974 (other, DesignBlock::Intercept(_)) => {
2975 let m_col = m_ab.column(0);
2976 let mut out = Array1::<f64>::zeros(self.n);
2977 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2978 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2979 let chunk = other.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2980 for local in 0..(end - start) {
2981 out[start + local] = chunk.row(local).dot(&m_col);
2982 }
2983 }
2984 Ok(out)
2985 }
2986 }
2987 }
2988}
2989
2990impl LinearOperator for BlockDesignOperator {
2991 fn nrows(&self) -> usize {
2992 self.n
2993 }
2994
2995 fn ncols(&self) -> usize {
2996 self.total_cols
2997 }
2998
2999 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
3000 let mut out = Array1::<f64>::zeros(self.n);
3001 for (idx, block) in self.blocks.iter().enumerate() {
3002 let start = self.col_offsets[idx];
3003 let end = self.col_offsets[idx + 1];
3004 let slice = vector.slice(s![start..end]).to_owned();
3005 let contribution = block.apply(&slice);
3006 out += &contribution;
3007 }
3008 out
3009 }
3010
3011 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
3012 let mut out = Array1::<f64>::zeros(self.total_cols);
3013 for (idx, block) in self.blocks.iter().enumerate() {
3014 let start = self.col_offsets[idx];
3015 let end = self.col_offsets[idx + 1];
3016 let transposed = block.apply_transpose(vector);
3017 out.slice_mut(s![start..end]).assign(&transposed);
3018 }
3019 out
3020 }
3021
3022 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
3023 certify_signed_weights("BlockDesignOperator::diag_xtw_x", weights, self.n)?;
3024 let p = self.total_cols;
3025 let mut result = Array2::<f64>::zeros((p, p));
3026
3027 for (idx, block) in self.blocks.iter().enumerate() {
3029 let start = self.col_offsets[idx];
3030 let end = self.col_offsets[idx + 1];
3031 let block_xtwx = block.diag_xtw_x(weights)?;
3032 result
3033 .slice_mut(s![start..end, start..end])
3034 .assign(&block_xtwx);
3035 }
3036
3037 let cross_left_blocks = self.blocks.len().saturating_sub(1);
3057 let weighted_dense: Vec<Option<Array2<f64>>> = self
3058 .blocks
3059 .iter()
3060 .take(cross_left_blocks)
3061 .map(|block| match block {
3062 DesignBlock::Dense(d) => d.as_dense_ref().map(|x| {
3063 x * &weights.view().insert_axis(Axis(1))
3066 }),
3067 _ => None,
3068 })
3069 .collect();
3070
3071 for i in 0..cross_left_blocks {
3072 for j in (i + 1)..self.blocks.len() {
3073 let cross = match (&weighted_dense[i], &self.blocks[j]) {
3074 (Some(wx_i), DesignBlock::Dense(d_j)) => match d_j.as_dense_ref() {
3077 Some(x_j) => fast_atb(wx_i, x_j),
3078 None => self.cross_block(i, j, weights)?,
3079 },
3080 _ => self.cross_block(i, j, weights)?,
3081 };
3082 let si = self.col_offsets[i];
3083 let ei = self.col_offsets[i + 1];
3084 let sj = self.col_offsets[j];
3085 let ej = self.col_offsets[j + 1];
3086 result.slice_mut(s![si..ei, sj..ej]).assign(&cross);
3087 result.slice_mut(s![sj..ej, si..ei]).assign(&cross.t());
3088 }
3089 }
3090
3091 Ok(result)
3092 }
3093
3094 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
3095 certify_signed_weights("BlockDesignOperator::diag_gram", weights, self.n)?;
3096 let mut out = Array1::<f64>::zeros(self.total_cols);
3097 for (idx, block) in self.blocks.iter().enumerate() {
3098 let start = self.col_offsets[idx];
3099 let end = self.col_offsets[idx + 1];
3100 let block_diag = block.diag_gram(weights)?;
3101 out.slice_mut(s![start..end]).assign(&block_diag);
3102 }
3103 Ok(out)
3104 }
3105
3106 fn apply_weighted_normal(
3107 &self,
3108 weights: FiniteSignedWeightsView<'_>,
3109 vector: &Array1<f64>,
3110 penalty: Option<&Array2<f64>>,
3111 ridge: f64,
3112 ) -> Array1<f64> {
3113 assert_eq!(
3114 weights.len(),
3115 self.n,
3116 "BlockDesignOperator::apply_weighted_normal weight length mismatch"
3117 );
3118 assert_eq!(
3119 vector.len(),
3120 self.total_cols,
3121 "BlockDesignOperator::apply_weighted_normal vector length mismatch"
3122 );
3123 let weights = weights.view();
3125 let xv = self.apply(vector);
3126 let mut weighted = xv;
3127 for i in 0..weighted.len() {
3128 weighted[i] *= weights[i];
3129 }
3130 let mut out = self.apply_transpose(&weighted);
3131 if let Some(pen) = penalty {
3132 out += &fast_av(pen, vector);
3133 }
3134 if ridge > 0.0 {
3135 out.scaled_add(ridge, vector);
3137 }
3138 out
3139 }
3140
3141 fn uses_matrix_free_pcg(&self) -> bool {
3142 self.blocks
3144 .iter()
3145 .any(|b| matches!(b, DesignBlock::RandomEffect(_) | DesignBlock::Intercept(_)))
3146 }
3147}
3148
3149impl DenseDesignOperator for BlockDesignOperator {
3150 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
3151 self.blocks.iter().fold(None, |policy, block| {
3152 merge_operator_materialization_policies(policy, block.materialization_policy())
3153 })
3154 }
3155
3156 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
3157 if weights.len() != self.n || y.len() != self.n {
3158 return Err(format!(
3159 "BlockDesignOperator::compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
3160 weights.len(),
3161 y.len(),
3162 self.n
3163 ));
3164 }
3165 certify_signed_weights("BlockDesignOperator::compute_xtwy", weights, self.n)?;
3166 let mut wy = Array1::<f64>::zeros(self.n);
3167 ndarray::Zip::from(&mut wy)
3168 .and(weights)
3169 .and(y)
3170 .par_for_each(|o, &w, &yi| *o = w * yi);
3171 Ok(self.apply_transpose(&wy))
3172 }
3173
3174 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
3175 let mut out = Array1::<f64>::zeros(self.n);
3178 let nb = self.blocks.len();
3179
3180 for k in 0..nb {
3182 let sk = self.col_offsets[k];
3183 let ek = self.col_offsets[k + 1];
3184 let m_kk = middle.slice(s![sk..ek, sk..ek]).to_owned();
3185 let block_diag = self.quadratic_form_diag_block(&self.blocks[k], &m_kk)?;
3186 out += &block_diag;
3187 }
3188
3189 for a in 0..nb {
3191 for b in (a + 1)..nb {
3192 let sa = self.col_offsets[a];
3193 let ea = self.col_offsets[a + 1];
3194 let sb = self.col_offsets[b];
3195 let eb = self.col_offsets[b + 1];
3196 let m_ab = middle.slice(s![sa..ea, sb..eb]);
3197
3198 let cross_diag = self.quadratic_form_diag_cross(
3199 &self.blocks[a],
3200 &self.blocks[b],
3201 &m_ab.to_owned(),
3202 )?;
3203 for i in 0..self.n {
3204 out[i] += 2.0 * cross_diag[i];
3205 }
3206 }
3207 }
3208
3209 for v in out.iter_mut() {
3211 *v = v.max(0.0);
3212 }
3213 Ok(out)
3214 }
3215
3216 fn row_chunk_into(
3217 &self,
3218 rows: Range<usize>,
3219 mut out: ArrayViewMut2<'_, f64>,
3220 ) -> Result<(), MatrixMaterializationError> {
3221 if out.nrows() != rows.end - rows.start || out.ncols() != self.total_cols {
3222 return Err(MatrixMaterializationError::MissingRowChunk {
3223 context: "BlockDesignOperator::row_chunk_into shape mismatch",
3224 });
3225 }
3226 for (idx, block) in self.blocks.iter().enumerate() {
3227 let cs = self.col_offsets[idx];
3228 let ce = self.col_offsets[idx + 1];
3229 block.row_chunk_into(rows.clone(), out.slice_mut(s![.., cs..ce]))?;
3230 }
3231 Ok(())
3232 }
3233
3234 fn to_dense(&self) -> Array2<f64> {
3235 let mut out = Array2::<f64>::zeros((self.n, self.total_cols));
3236 for (idx, block) in self.blocks.iter().enumerate() {
3237 let start = self.col_offsets[idx];
3238 let end = self.col_offsets[idx + 1];
3239 let dense_block = block.to_dense();
3240 out.slice_mut(s![.., start..end]).assign(&dense_block);
3241 }
3242 out
3243 }
3244}
3245
3246#[derive(Clone)]
3260pub struct MultiChannelOperator {
3261 pub channels: Vec<DesignMatrix>,
3263 pub n_per_channel: usize,
3265 pub p: usize,
3267}
3268
3269impl MultiChannelOperator {
3270 pub fn new(channels: Vec<DesignMatrix>) -> Result<Self, String> {
3271 if channels.is_empty() {
3272 return Err("MultiChannelOperator: need at least one channel".to_string());
3273 }
3274 let n = channels[0].nrows();
3275 let p = channels[0].ncols();
3276 for (i, ch) in channels.iter().enumerate() {
3277 if ch.nrows() != n {
3278 return Err(format!(
3279 "MultiChannelOperator: channel {i} has {} rows, expected {n}",
3280 ch.nrows()
3281 ));
3282 }
3283 if ch.ncols() != p {
3284 return Err(format!(
3285 "MultiChannelOperator: channel {i} has {} cols, expected {p}",
3286 ch.ncols()
3287 ));
3288 }
3289 }
3290 Ok(Self {
3291 channels,
3292 n_per_channel: n,
3293 p,
3294 })
3295 }
3296}
3297
3298impl LinearOperator for MultiChannelOperator {
3299 fn nrows(&self) -> usize {
3300 self.n_per_channel * self.channels.len()
3301 }
3302
3303 fn ncols(&self) -> usize {
3304 self.p
3305 }
3306
3307 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
3308 let total = self.nrows();
3309 let mut out = Array1::<f64>::zeros(total);
3310 let n = self.n_per_channel;
3311 for (i, ch) in self.channels.iter().enumerate() {
3312 let ch_result = ch.matrixvectormultiply(vector);
3313 out.slice_mut(s![i * n..(i + 1) * n]).assign(&ch_result);
3314 }
3315 out
3316 }
3317
3318 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
3319 let n = self.n_per_channel;
3320 let mut out = Array1::<f64>::zeros(self.p);
3321 for (i, ch) in self.channels.iter().enumerate() {
3322 out += &ch.apply_transpose_view(vector.slice(s![i * n..(i + 1) * n]));
3323 }
3324 out
3325 }
3326
3327 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
3328 let n = self.n_per_channel;
3329 certify_signed_weights("MultiChannelOperator::diag_xtw_x", weights, self.nrows())?;
3330 let mut xtwx = Array2::<f64>::zeros((self.p, self.p));
3331 for (i, ch) in self.channels.iter().enumerate() {
3332 let channel_weights = weights.slice(s![i * n..(i + 1) * n]).to_owned();
3333 let ch_xtwx = ch.diag_xtw_x(&channel_weights)?;
3334 xtwx += &ch_xtwx;
3335 }
3336 Ok(xtwx)
3337 }
3338
3339 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
3340 let n = self.n_per_channel;
3341 certify_signed_weights("MultiChannelOperator::diag_gram", weights, self.nrows())?;
3342 let mut diag = Array1::<f64>::zeros(self.p);
3343 for (i, ch) in self.channels.iter().enumerate() {
3344 diag += &ch.diag_gram_view(weights.slice(s![i * n..(i + 1) * n]))?;
3345 }
3346 Ok(diag)
3347 }
3348
3349 fn uses_matrix_free_pcg(&self) -> bool {
3350 true
3351 }
3352}
3353
3354impl DenseDesignOperator for MultiChannelOperator {
3355 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
3356 self.channels.iter().fold(None, |policy, channel| {
3357 merge_operator_materialization_policies(policy, channel.materialization_policy())
3358 })
3359 }
3360
3361 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
3362 let n = self.n_per_channel;
3363 let total = self.nrows();
3364 if weights.len() != total || y.len() != total {
3365 return Err(format!(
3366 "MultiChannelOperator::compute_xtwy: weights={}, y={}, nrows={}",
3367 weights.len(),
3368 y.len(),
3369 total
3370 ));
3371 }
3372 certify_signed_weights("MultiChannelOperator::compute_xtwy", weights, total)?;
3373 let mut out = Array1::<f64>::zeros(self.p);
3374 for (i, ch) in self.channels.iter().enumerate() {
3375 out += &ch.compute_xtwy_view(
3376 weights.slice(s![i * n..(i + 1) * n]),
3377 y.slice(s![i * n..(i + 1) * n]),
3378 )?;
3379 }
3380 Ok(out)
3381 }
3382
3383 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
3384 let n = self.n_per_channel;
3385 let mut out = Array1::<f64>::zeros(self.nrows());
3386 for (i, ch) in self.channels.iter().enumerate() {
3387 let ch_diag = ch.quadratic_form_diag(middle)?;
3388 out.slice_mut(s![i * n..(i + 1) * n]).assign(&ch_diag);
3389 }
3390 Ok(out)
3391 }
3392
3393 fn to_dense(&self) -> Array2<f64> {
3394 let total = self.nrows();
3395 let n = self.n_per_channel;
3396 let mut out = Array2::<f64>::zeros((total, self.p));
3397 for (i, ch) in self.channels.iter().enumerate() {
3398 let dense = ch.to_dense();
3399 out.slice_mut(s![i * n..(i + 1) * n, ..]).assign(&dense);
3400 }
3401 out
3402 }
3403
3404 fn row_chunk_into(
3405 &self,
3406 rows: Range<usize>,
3407 mut out: ArrayViewMut2<'_, f64>,
3408 ) -> Result<(), MatrixMaterializationError> {
3409 if out.nrows() != rows.end - rows.start || out.ncols() != self.p {
3410 return Err(MatrixMaterializationError::MissingRowChunk {
3411 context: "MultiChannelOperator::row_chunk_into shape mismatch",
3412 });
3413 }
3414 let n = self.n_per_channel;
3415 let mut local = 0usize;
3416 let mut global = rows.start;
3417 while global < rows.end {
3418 let ch_idx = global / n;
3419 let ch_local_start = global % n;
3420 let ch_local_end = ((ch_idx + 1) * n).min(rows.end) - ch_idx * n;
3421 let segment_len = ch_local_end - ch_local_start;
3422 self.channels[ch_idx].row_chunk_into(
3423 ch_local_start..ch_local_end,
3424 out.slice_mut(s![local..local + segment_len, ..]),
3425 )?;
3426 local += segment_len;
3427 global += segment_len;
3428 }
3429 Ok(())
3430 }
3431}
3432
3433mod kronecker;
3435pub use kronecker::*;
3436
3437pub struct CoefficientTransformOperator {
3444 inner: DenseDesignMatrix,
3445 transform: Arc<Array2<f64>>,
3446 n: usize,
3447 p_out: usize,
3448 materialized: OnceLock<Option<Arc<Array2<f64>>>>,
3452}
3453
3454impl CoefficientTransformOperator {
3455 const MATERIALIZE_MAX_BYTES: usize = 1024 * 1024 * 1024;
3458
3459 pub fn new(inner: DenseDesignMatrix, transform: Array2<f64>) -> Result<Self, String> {
3460 let p_inner = inner.ncols();
3461 if transform.nrows() != p_inner {
3462 return Err(format!(
3463 "CoefficientTransformOperator: inner has {} cols but transform has {} rows",
3464 p_inner,
3465 transform.nrows(),
3466 ));
3467 }
3468 let n = inner.nrows();
3469 let p_out = transform.ncols();
3470 Ok(Self {
3471 inner,
3472 transform: Arc::new(transform),
3473 n,
3474 p_out,
3475 materialized: OnceLock::new(),
3476 })
3477 }
3478
3479 fn materialized_combined(&self) -> Option<&Array2<f64>> {
3484 if let Some(slot) = self.materialized.get() {
3485 return slot.as_ref().map(|a| a.as_ref());
3486 }
3487 if self.inner.is_operator_backed() {
3488 if self.materialized.set(None).is_err() {
3489 return self
3490 .materialized
3491 .get()
3492 .and_then(|opt| opt.as_ref().map(|a| a.as_ref()));
3493 }
3494 return None;
3495 }
3496 let bytes = self
3497 .n
3498 .checked_mul(self.p_out)
3499 .and_then(|cells| cells.checked_mul(std::mem::size_of::<f64>()));
3500 let computed = match bytes {
3501 Some(b) if b <= Self::MATERIALIZE_MAX_BYTES => self
3502 .inner
3503 .as_dense_ref()
3504 .map(|x| Arc::new(fast_ab(x, &self.transform))),
3505 _ => None,
3506 };
3507 if self.materialized.set(computed).is_err() {
3508 return self
3509 .materialized
3510 .get()
3511 .and_then(|opt| opt.as_ref().map(|a| a.as_ref()));
3512 }
3513 self.materialized
3514 .get()
3515 .and_then(|opt| opt.as_ref().map(|a| a.as_ref()))
3516 }
3517}
3518
3519impl LinearOperator for CoefficientTransformOperator {
3520 fn nrows(&self) -> usize {
3521 self.n
3522 }
3523 fn ncols(&self) -> usize {
3524 self.p_out
3525 }
3526 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
3527 if let Some(combined) = self.materialized_combined() {
3528 return fast_av(combined, vector);
3529 }
3530 let tv = fast_av(&self.transform, vector);
3531 self.inner.apply(&tv)
3532 }
3533 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
3534 if let Some(combined) = self.materialized_combined() {
3535 return fast_atv(combined, vector);
3536 }
3537 let xtv = self.inner.apply_transpose(vector);
3538 fast_atv(&self.transform, &xtv)
3539 }
3540 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
3541 certify_signed_weights("CoefficientTransformOperator::diag_xtw_x", weights, self.n)?;
3542 if let Some(combined) = self.materialized_combined() {
3543 let mut xtwx = Array2::<f64>::zeros((self.p_out, self.p_out));
3544 stream_weighted_crossprod_into(
3545 combined,
3546 weights,
3547 &mut xtwx,
3548 CrossprodStructure::Full,
3549 CrossprodAccum::Replace,
3550 effective_global_parallelism(),
3551 );
3552 return Ok(xtwx);
3553 }
3554 let inner_xtwx = self.inner.diag_xtw_x(weights)?;
3555 let tmp = fast_ab(&self.transform.t().to_owned(), &inner_xtwx);
3557 Ok(fast_ab(&tmp, &self.transform))
3558 }
3559}
3560
3561impl DenseDesignOperator for CoefficientTransformOperator {
3562 fn as_dense_ref(&self) -> Option<&Array2<f64>> {
3570 self.materialized_combined()
3571 }
3572
3573 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
3574 self.inner.materialization_policy()
3575 }
3576
3577 fn to_dense(&self) -> Array2<f64> {
3578 if let Some(combined) = self.materialized_combined() {
3579 return combined.clone();
3580 }
3581 let x = self.inner.to_dense();
3582 fast_ab(&x, &self.transform)
3583 }
3584 fn row_chunk_into(
3585 &self,
3586 rows: Range<usize>,
3587 mut out: ArrayViewMut2<'_, f64>,
3588 ) -> Result<(), MatrixMaterializationError> {
3589 if out.nrows() != rows.end - rows.start || out.ncols() != self.p_out {
3590 return Err(MatrixMaterializationError::MissingRowChunk {
3591 context: "CoefficientTransformOperator::row_chunk_into shape mismatch",
3592 });
3593 }
3594 if let Some(combined) = self.materialized_combined() {
3595 out.assign(&combined.slice(s![rows, ..]));
3596 return Ok(());
3597 }
3598 let chunk = self.inner.try_row_chunk(rows)?;
3599 out.assign(&fast_ab(&chunk, &self.transform));
3600 Ok(())
3601 }
3602}
3603
3604pub struct ConditionedDesign {
3633 inner: DesignMatrix,
3634 columns: Vec<(usize, f64, f64)>,
3636}
3637
3638impl ConditionedDesign {
3639 pub fn new(inner: DesignMatrix, columns: Vec<(usize, f64, f64)>) -> Self {
3640 Self { inner, columns }
3641 }
3642}
3643
3644impl LinearOperator for ConditionedDesign {
3645 fn nrows(&self) -> usize {
3646 self.inner.nrows()
3647 }
3648
3649 fn ncols(&self) -> usize {
3650 self.inner.ncols()
3651 }
3652
3653 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
3655 let mut scaled = vector.clone();
3656 let mut shift = 0.0;
3657 for &(j, mean, scale) in &self.columns {
3658 scaled[j] /= scale;
3659 shift += mean * scaled[j];
3660 }
3661 let mut result = self.inner.apply(&scaled);
3662 if shift != 0.0 {
3663 result.mapv_inplace(|v| v - shift);
3664 }
3665 result
3666 }
3667
3668 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
3670 let mut result = self.inner.apply_transpose(vector);
3671 let sum_u: f64 = vector.iter().sum();
3672 for &(j, mean, scale) in &self.columns {
3673 result[j] = (result[j] - mean * sum_u) / scale;
3674 }
3675 result
3676 }
3677
3678 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
3680 certify_signed_weights("ConditionedDesign::diag_xtw_x", weights, self.nrows())?;
3681 let mut base = self.inner.diag_xtw_x(weights)?;
3682 if self.columns.is_empty() {
3683 return Ok(base);
3684 }
3685 let p = base.ncols();
3686 let sum_w: f64 = weights.sum();
3687 let cw = self.inner.apply_transpose(weights);
3688
3689 let mut a = vec![1.0_f64; p];
3691 let mut d = vec![0.0_f64; p];
3692 for &(j, mean, scale) in &self.columns {
3693 a[j] = 1.0 / scale;
3694 d[j] = mean / scale;
3695 }
3696
3697 for i in 0..p {
3699 for j in i..p {
3700 let val = a[i] * base[[i, j]] * a[j] - a[i] * cw[i] * d[j] - d[i] * cw[j] * a[j]
3701 + sum_w * d[i] * d[j];
3702 base[[i, j]] = val;
3703 base[[j, i]] = val;
3704 }
3705 }
3706 Ok(base)
3707 }
3708
3709 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
3711 certify_signed_weights("ConditionedDesign::diag_gram", weights, self.nrows())?;
3712 let mut result = self.inner.diag_gram(weights)?;
3713 if self.columns.is_empty() {
3714 return Ok(result);
3715 }
3716 let sum_w: f64 = weights.sum();
3717 let cw = self.inner.apply_transpose(weights);
3718 for &(j, mean, scale) in &self.columns {
3719 let a_j = 1.0 / scale;
3720 let d_j = mean / scale;
3721 result[j] = a_j * a_j * result[j] - 2.0 * a_j * cw[j] * d_j + sum_w * d_j * d_j;
3722 }
3723 Ok(result)
3724 }
3725
3726 fn uses_matrix_free_pcg(&self) -> bool {
3727 match &self.inner {
3728 DesignMatrix::Dense(_) => true,
3729 DesignMatrix::Sparse(_) => false,
3730 }
3731 }
3732}
3733
3734impl DenseDesignOperator for ConditionedDesign {
3735 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
3736 self.inner.materialization_policy()
3737 }
3738
3739 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
3741 if y.len() != self.nrows() {
3742 return Err(format!(
3743 "ConditionedDesign::compute_xtwy response length mismatch: y={}, nrows={}",
3744 y.len(),
3745 self.nrows()
3746 ));
3747 }
3748 certify_signed_weights("ConditionedDesign::compute_xtwy", weights, self.nrows())?;
3749 let mut result = self.inner.compute_xtwy(weights, y)?;
3750 if self.columns.is_empty() {
3751 return Ok(result);
3752 }
3753 let sum_wy: f64 = weights.iter().zip(y.iter()).map(|(&w, &yi)| w * yi).sum();
3754 for &(j, mean, scale) in &self.columns {
3755 result[j] = (result[j] - mean * sum_wy) / scale;
3756 }
3757 Ok(result)
3758 }
3759
3760 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
3762 if self.columns.is_empty() {
3763 return self.inner.quadratic_form_diag(middle);
3764 }
3765 let p = self.ncols();
3766 let mut d = Array1::zeros(p);
3767 for &(j, mean, scale) in &self.columns {
3768 d[j] = mean / scale;
3769 }
3770
3771 let mut ama = middle.clone();
3773 for &(j, _, scale) in &self.columns {
3774 for k in 0..p {
3775 ama[[j, k]] /= scale;
3776 ama[[k, j]] /= scale;
3777 }
3778 }
3779
3780 let md = middle.dot(&d);
3782 let mut amd = md;
3783 for &(j, _, scale) in &self.columns {
3784 amd[j] /= scale;
3785 }
3786
3787 let dtmd: f64 = d.dot(&middle.dot(&d));
3788
3789 let mut result = self.inner.quadratic_form_diag(&ama)?;
3790 let x_amd = self.inner.apply(&amd);
3791 for i in 0..result.len() {
3792 result[i] = (result[i] - 2.0 * x_amd[i] + dtmd).max(0.0);
3793 }
3794 Ok(result)
3795 }
3796
3797 fn row_chunk_into(
3798 &self,
3799 rows: Range<usize>,
3800 mut out: ArrayViewMut2<'_, f64>,
3801 ) -> Result<(), MatrixMaterializationError> {
3802 if out.nrows() != rows.end - rows.start || out.ncols() != self.ncols() {
3803 return Err(MatrixMaterializationError::MissingRowChunk {
3804 context: "ConditionedDesign::row_chunk_into shape mismatch",
3805 });
3806 }
3807 let mut chunk = self.inner.try_row_chunk(rows)?;
3808 for &(j, mean, scale) in &self.columns {
3809 chunk.column_mut(j).mapv_inplace(|v| (v - mean) / scale);
3810 }
3811 out.assign(&chunk);
3812 Ok(())
3813 }
3814
3815 fn to_dense(&self) -> Array2<f64> {
3816 let mut dense = self.inner.to_dense();
3817 for &(j, mean, scale) in &self.columns {
3818 dense.column_mut(j).mapv_inplace(|v| (v - mean) / scale);
3819 }
3820 dense
3821 }
3822}
3823
3824#[derive(Clone)]
3834pub enum DesignMatrix {
3835 Dense(DenseDesignMatrix),
3836 Sparse(SparseDesignMatrix),
3837}
3838
3839impl std::fmt::Debug for DesignMatrix {
3840 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3841 match self {
3842 Self::Dense(m) => write!(f, "DesignMatrix::Dense({}x{})", m.nrows(), m.ncols()),
3843 Self::Sparse(s) => write!(f, "DesignMatrix::Sparse({}x{})", s.nrows(), s.ncols()),
3844 }
3845 }
3846}
3847
3848mod symmetric;
3850pub use symmetric::*;
3851pub trait FactorizedSystem: Send + Sync {
3853 fn solve(&self, rhs: &Array1<f64>) -> Result<Array1<f64>, String>;
3855
3856 fn solvemulti(&self, rhs: &Array2<f64>) -> Result<Array2<f64>, String>;
3858
3859 fn logdet(&self) -> f64;
3861}
3862
3863pub trait LinearOperator {
3864 fn nrows(&self) -> usize;
3865 fn ncols(&self) -> usize;
3866 fn apply(&self, vector: &Array1<f64>) -> Array1<f64>;
3867 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64>;
3868 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String>;
3869
3870 fn xt_diag_x_signed_op(
3876 &self,
3877 weights: FiniteSignedWeightsView<'_>,
3878 ) -> Result<Array2<f64>, String> {
3879 self.diag_xtw_x(&weights.view().to_owned())
3880 }
3881
3882 fn xt_diag_x_psd_op(&self, weights: PsdWeightsView<'_>) -> Result<SymmetricMatrix, String> {
3887 FiniteSignedWeightsView::try_new(weights.view())
3888 .map_err(|reason| format!("LinearOperator::xt_diag_x_psd_op: {reason}"))?;
3889 let xtwx = self.diag_xtw_x(&weights.view().to_owned())?;
3890 Ok(SymmetricMatrix::Dense(xtwx))
3891 }
3892
3893 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
3894 let xtwx = self.diag_xtw_x(weights)?;
3895 Ok(Array1::from_iter((0..self.ncols()).map(|j| xtwx[[j, j]])))
3896 }
3897 fn apply_weighted_normal(
3898 &self,
3899 weights: FiniteSignedWeightsView<'_>,
3900 vector: &Array1<f64>,
3901 penalty: Option<&Array2<f64>>,
3902 ridge: f64,
3903 ) -> Array1<f64> {
3904 assert_eq!(
3905 weights.len(),
3906 self.nrows(),
3907 "apply_weighted_normal weight length mismatch"
3908 );
3909 assert_eq!(
3910 vector.len(),
3911 self.ncols(),
3912 "apply_weighted_normal vector length mismatch"
3913 );
3914 let weights = weights.view();
3915 let xv = self.apply(vector);
3916 let mut weighted_xv = xv;
3917 for i in 0..weighted_xv.len() {
3918 weighted_xv[i] *= weights[i];
3919 }
3920 let mut out = self.apply_transpose(&weighted_xv);
3921 if let Some(pen) = penalty {
3922 out += &fast_av(pen, vector);
3923 }
3924 if ridge > 0.0 {
3925 out.scaled_add(ridge, vector);
3927 }
3928 out
3929 }
3930 fn uses_matrix_free_pcg(&self) -> bool {
3931 false
3932 }
3933 fn solve_system_matrix_free_pcg_try(
3934 &self,
3935 weights: &Array1<f64>,
3936 rhs: &Array1<f64>,
3937 penalty: Option<&Array2<f64>>,
3938 baseridge: f64,
3939 ) -> Result<Array1<f64>, String> {
3940 self.solve_system_matrix_free_pcg_with_info_try(weights, rhs, penalty, baseridge)
3941 .map(|(solution, _)| solution)
3942 }
3943 fn solve_system_matrix_free_pcg_with_info_try(
3944 &self,
3945 weights: &Array1<f64>,
3946 rhs: &Array1<f64>,
3947 penalty: Option<&Array2<f64>>,
3948 baseridge: f64,
3949 ) -> Result<(Array1<f64>, PcgSolveInfo), String> {
3950 if rhs.len() != self.ncols() {
3951 return Err(format!(
3952 "solve_system_matrix_free_pcg rhs dimension mismatch: rhs length {} != ncols {}",
3953 rhs.len(),
3954 self.ncols()
3955 ));
3956 }
3957 if !self.uses_matrix_free_pcg() {
3958 return Err("matrix-free PCG is only enabled for eligible operator types".to_string());
3959 }
3960 if let Some(pen) = penalty
3961 && (pen.nrows() != self.ncols() || pen.ncols() != self.ncols())
3962 {
3963 return Err(format!(
3964 "solve_system_matrix_free_pcg penalty shape mismatch: got {}x{}, expected {}x{}",
3965 pen.nrows(),
3966 pen.ncols(),
3967 self.ncols(),
3968 self.ncols()
3969 ));
3970 }
3971 let p = self.ncols();
3972 let finite_weights = certify_signed_weights(
3973 "solve_system_matrix_free_pcg_with_info_try",
3974 weights,
3975 self.nrows(),
3976 )?;
3977 if !(baseridge.is_finite() && baseridge >= 0.0) {
3978 return Err(format!(
3979 "matrix-free PCG ridge must be finite and non-negative, got {baseridge:?}"
3980 ));
3981 }
3982 let normal_op = PenalizedWeightedNormalOperator {
3983 operator: self,
3984 weights,
3985 finite_weights,
3986 penalty,
3987 ridge: baseridge,
3988 };
3989 let preconditioner = normal_op.jacobi_preconditioner()?;
3990 let attempt_started = std::time::Instant::now();
3991 let (solution, info) = crate::utils::solve_spd_pcg_with_info(
3992 |v| normal_op.apply(v),
3993 rhs,
3994 &preconditioner,
3995 MATRIX_FREE_PCG_REL_TOL,
3996 MATRIX_FREE_PCG_MAX_ITER.max(4 * p),
3997 )
3998 .ok_or_else(|| {
3999 format!("matrix-free PCG broke down for explicitly requested ridge {baseridge:.3e}")
4000 })?;
4001 if !solution.iter().all(|value| value.is_finite()) {
4002 return Err("matrix-free PCG produced a non-finite solution".to_string());
4003 }
4004 log::debug!(
4005 "[matrix-free PCG] solved: p={p} ridge={baseridge:.3e} iters={} converged={} rel_resid={:.3e} elapsed={:.3}s",
4006 info.iterations,
4007 info.converged,
4008 info.relative_residual_norm,
4009 attempt_started.elapsed().as_secs_f64(),
4010 );
4011 Ok((solution, info))
4012 }
4013 fn factorize_system(
4014 &self,
4015 weights: &Array1<f64>,
4016 penalty: Option<&Array2<f64>>,
4017 ) -> Result<Box<dyn FactorizedSystem>, String> {
4018 let mut system = self.diag_xtw_x(weights)?;
4019 if let Some(pen) = penalty {
4020 if pen.nrows() != system.nrows() || pen.ncols() != system.ncols() {
4021 return Err(format!(
4022 "factorize_system penalty shape mismatch: got {}x{}, expected {}x{}",
4023 pen.nrows(),
4024 pen.ncols(),
4025 system.nrows(),
4026 system.ncols()
4027 ));
4028 }
4029 system += pen;
4030 }
4031 let factor = crate::utils::StableSolver::new()
4032 .factorize(&system)
4033 .map_err(|e| format!("factorize_system failed: {e:?}"))?;
4034 Ok(Box::new(factor))
4035 }
4036 fn solve_system(
4037 &self,
4038 weights: &Array1<f64>,
4039 rhs: &Array1<f64>,
4040 penalty: Option<&Array2<f64>>,
4041 ) -> Result<Array1<f64>, String> {
4042 self.solve_systemwith_policy(weights, rhs, penalty, 0.0, RidgePolicy::solver_only())
4043 }
4044 fn solve_systemwith_policy(
4045 &self,
4046 weights: &Array1<f64>,
4047 rhs: &Array1<f64>,
4048 penalty: Option<&Array2<f64>>,
4049 ridge_floor: f64,
4050 ridge_policy: RidgePolicy,
4051 ) -> Result<Array1<f64>, String> {
4052 if rhs.len() != self.ncols() {
4053 return Err(format!(
4054 "solve_systemwith_policy rhs dimension mismatch: rhs length {} != ncols {}",
4055 rhs.len(),
4056 self.ncols()
4057 ));
4058 }
4059 if !(ridge_floor.is_finite() && ridge_floor >= 0.0) {
4060 return Err(format!(
4061 "solve_systemwith_policy ridge floor must be finite and non-negative, got {ridge_floor:?}"
4062 ));
4063 }
4064 let ridge = ridge_floor;
4065 if self.uses_matrix_free_pcg() && self.ncols() >= MATRIX_FREE_PCG_MIN_P {
4069 return self.solve_system_matrix_free_pcg_try(weights, rhs, penalty, ridge);
4070 }
4071 let mut system = self.diag_xtw_x(weights)?;
4072 if let Some(pen) = penalty {
4073 if pen.nrows() != system.nrows() || pen.ncols() != system.ncols() {
4074 return Err(format!(
4075 "solve_systemwith_policy penalty shape mismatch: got {}x{}, expected {}x{}",
4076 pen.nrows(),
4077 pen.ncols(),
4078 system.nrows(),
4079 system.ncols()
4080 ));
4081 }
4082 system += pen;
4083 }
4084 if ridge > 0.0 {
4085 for diagonal in 0..system.nrows() {
4086 system[[diagonal, diagonal]] += ridge;
4087 }
4088 }
4089 let factor = crate::utils::StableSolver::new()
4090 .factorize(&system)
4091 .map_err(|error| {
4092 format!(
4093 "solve_systemwith_policy ({ridge_policy:?}) exact factorization failed at ridge {ridge:.3e}: {error:?}"
4094 )
4095 })?;
4096 let mut solution = rhs.clone();
4097 let mut solution_matrix = crate::faer_ndarray::array1_to_col_matmut(&mut solution);
4098 factor.solve_in_place(solution_matrix.as_mut());
4099 if solution.iter().all(|value| value.is_finite()) {
4100 Ok(solution)
4101 } else {
4102 Err("solve_systemwith_policy produced a non-finite solution".to_string())
4103 }
4104 }
4105}
4106
4107impl LinearOperator for DesignMatrix {
4108 fn uses_matrix_free_pcg(&self) -> bool {
4109 match self {
4110 Self::Dense(matrix) => matrix.uses_matrix_free_pcg(),
4111 Self::Sparse(_) => false,
4112 }
4113 }
4114
4115 fn nrows(&self) -> usize {
4116 match self {
4117 Self::Dense(matrix) => matrix.nrows(),
4118 Self::Sparse(matrix) => matrix.nrows(),
4119 }
4120 }
4121
4122 fn ncols(&self) -> usize {
4123 match self {
4124 Self::Dense(matrix) => matrix.ncols(),
4125 Self::Sparse(matrix) => matrix.ncols(),
4126 }
4127 }
4128
4129 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
4130 match self {
4131 Self::Dense(matrix) => matrix.apply(vector),
4132 Self::Sparse(matrix) => {
4133 let mut output = Array1::<f64>::zeros(matrix.nrows());
4134 let (symbolic, values) = matrix.parts();
4135 let col_ptr = symbolic.col_ptr();
4136 let row_idx = symbolic.row_idx();
4137 for col in 0..matrix.ncols() {
4138 let start = col_ptr[col];
4139 let end = col_ptr[col + 1];
4140 let x = vector[col];
4141 for idx in start..end {
4142 let row = row_idx[idx];
4143 output[row] += values[idx] * x;
4144 }
4145 }
4146 output
4147 }
4148 }
4149 }
4150
4151 fn apply_weighted_normal(
4152 &self,
4153 weights: FiniteSignedWeightsView<'_>,
4154 vector: &Array1<f64>,
4155 penalty: Option<&Array2<f64>>,
4156 ridge: f64,
4157 ) -> Array1<f64> {
4158 assert_eq!(
4159 weights.len(),
4160 self.nrows(),
4161 "DesignMatrix::apply_weighted_normal weight length mismatch"
4162 );
4163 assert_eq!(
4164 vector.len(),
4165 self.ncols(),
4166 "DesignMatrix::apply_weighted_normal vector length mismatch"
4167 );
4168 let weights_view = weights.view();
4169 match self {
4170 Self::Dense(matrix) => matrix.apply_weighted_normal(weights, vector, penalty, ridge),
4171 Self::Sparse(_) => {
4172 let sparse = self
4173 .as_sparse()
4174 .expect("DesignMatrix::Sparse must expose sparse view");
4175 let mut out = if let Some(csr) = sparse.to_csr_arc() {
4176 let sym = csr.symbolic();
4177 let row_ptr = sym.row_ptr();
4178 let col_idx = sym.col_idx();
4179 let vals = csr.val();
4180 let mut fused = Array1::<f64>::zeros(self.ncols());
4181 for i in 0..self.nrows() {
4182 let wi = weights_view[i];
4183 if wi == 0.0 {
4184 continue;
4185 }
4186 let start = row_ptr[i];
4187 let end = row_ptr[i + 1];
4188 let mut row_dot = 0.0_f64;
4189 for ptr in start..end {
4190 row_dot += vals[ptr] * vector[col_idx[ptr]];
4191 }
4192 if row_dot == 0.0 {
4193 continue;
4194 }
4195 let scaled = wi * row_dot;
4196 for ptr in start..end {
4197 fused[col_idx[ptr]] += vals[ptr] * scaled;
4198 }
4199 }
4200 fused
4201 } else {
4202 let xv = self.apply(vector);
4203 let mut weighted_xv = xv;
4204 for i in 0..weighted_xv.len() {
4205 weighted_xv[i] *= weights_view[i];
4206 }
4207 self.apply_transpose(&weighted_xv)
4208 };
4209 if let Some(pen) = penalty {
4210 out += &fast_av(pen, vector);
4211 }
4212 if ridge > 0.0 {
4213 for j in 0..out.len() {
4214 out[j] += ridge * vector[j];
4215 }
4216 }
4217 out
4218 }
4219 }
4220 }
4221
4222 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
4223 match self {
4224 Self::Dense(matrix) => matrix.apply_transpose(vector),
4225 Self::Sparse(matrix) => {
4226 let mut output = Array1::<f64>::zeros(matrix.ncols());
4227 let (symbolic, values) = matrix.parts();
4228 let col_ptr = symbolic.col_ptr();
4229 let row_idx = symbolic.row_idx();
4230 for col in 0..matrix.ncols() {
4231 let mut acc = 0.0;
4232 let start = col_ptr[col];
4233 let end = col_ptr[col + 1];
4234 for idx in start..end {
4235 let row = row_idx[idx];
4236 acc += values[idx] * vector[row];
4237 }
4238 output[col] = acc;
4239 }
4240 output
4241 }
4242 }
4243 }
4244
4245 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
4246 certify_signed_weights("DesignMatrix::diag_xtw_x", weights, self.nrows())?;
4247 let p = self.ncols();
4248 match self {
4249 Self::Dense(x) => x.diag_xtw_x(weights),
4250 Self::Sparse(xs) => {
4251 let n = self.nrows();
4274 let nnz_x = xs.as_ref().val().len();
4275 let avg_nnz_row = if n > 0 { nnz_x / n } else { p };
4276 let dense_regime = 4 * avg_nnz_row >= p;
4277 if dense_regime {
4278 let mut xtwx = Array2::<f64>::zeros((p, p));
4279 if let Ok(xd) =
4283 xs.try_to_dense_governed("DesignMatrix::diag_xtw_x dense sparse route")
4284 {
4285 stream_weighted_crossprod_into(
4286 &**xd,
4287 weights,
4288 &mut xtwx,
4289 CrossprodStructure::Full,
4290 CrossprodAccum::Replace,
4291 effective_global_parallelism(),
4292 );
4293 } else {
4294 let (symbolic, values) = xs.parts();
4295 streaming_sparse_csc_xt_diag_x(
4296 symbolic.col_ptr(),
4297 symbolic.row_idx(),
4298 values,
4299 n,
4300 p,
4301 weights.view(),
4302 &mut xtwx,
4303 );
4304 }
4305 return Ok(xtwx);
4306 }
4307 let csr = xs
4308 .to_csr_arc()
4309 .ok_or_else(|| "failed to obtain CSR view in xt_diag_x".to_string())?;
4310 let sym = csr.symbolic();
4311 Ok(sparse_csr_weighted_xtwx(
4312 sym.row_ptr(),
4313 sym.col_idx(),
4314 csr.val(),
4315 n,
4316 p,
4317 weights.view(),
4318 ))
4319 }
4320 }
4321 }
4322
4323 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
4324 certify_signed_weights("DesignMatrix::diag_gram", weights, self.nrows())?;
4325 let p = self.ncols();
4326 match self {
4327 Self::Dense(x) => x.diag_gram(weights),
4328 Self::Sparse(xs) => {
4329 let csr = xs
4330 .to_csr_arc()
4331 .ok_or_else(|| "failed to obtain CSR view in diag_gram".to_string())?;
4332 let sym = csr.symbolic();
4333 Ok(sparse_csr_diag_gram(
4334 sym.row_ptr(),
4335 sym.col_idx(),
4336 csr.val(),
4337 self.nrows(),
4338 p,
4339 weights.view(),
4340 ))
4341 }
4342 }
4343 }
4344
4345 fn factorize_system(
4346 &self,
4347 weights: &Array1<f64>,
4348 penalty: Option<&Array2<f64>>,
4349 ) -> Result<Box<dyn FactorizedSystem>, String> {
4350 if weights.len() != self.nrows() {
4351 return Err(format!(
4352 "factorize_system dimension mismatch: weights length {} != nrows {}",
4353 weights.len(),
4354 self.nrows()
4355 ));
4356 }
4357 match self {
4358 Self::Dense(_) => self.factorize_system_dense(weights, penalty),
4359 Self::Sparse(matrix) => {
4360 let system = assemble_sparseweighted_gram_system(matrix, weights, penalty)?;
4361 let factor = crate::sparse_exact::factorize_sparse_spd(&system)
4362 .map_err(|e| format!("factorize_system failed: {e:?}"))?;
4363 Ok(Box::new(factor))
4364 }
4365 }
4366 }
4367}
4368
4369impl DenseDesignOperator for DesignMatrix {
4370 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
4371 match self {
4372 Self::Dense(design) => design.materialization_policy(),
4373 Self::Sparse(_) => None,
4374 }
4375 }
4376
4377 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
4378 if weights.len() != self.nrows() || y.len() != self.nrows() {
4379 return Err(format!(
4380 "compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
4381 weights.len(),
4382 y.len(),
4383 self.nrows()
4384 ));
4385 }
4386 certify_signed_weights("DesignMatrix::compute_xtwy", weights, self.nrows())?;
4387 match self {
4388 Self::Dense(x) => x.compute_xtwy(weights, y),
4389 Self::Sparse(xs) => {
4390 let csr = xs
4391 .as_ref()
4392 .to_row_major()
4393 .map_err(|_| "failed to obtain CSR view in compute_xtwy".to_string())?;
4394 let sym = csr.symbolic();
4395 let row_ptr = sym.row_ptr();
4396 let col_idx = sym.col_idx();
4397 let vals = csr.val();
4398 let mut out = Array1::<f64>::zeros(xs.ncols());
4399 for i in 0..xs.nrows() {
4400 let scaled = weights[i] * y[i];
4401 if scaled == 0.0 {
4402 continue;
4403 }
4404 for idx in row_ptr[i]..row_ptr[i + 1] {
4405 out[col_idx[idx]] += vals[idx] * scaled;
4406 }
4407 }
4408 Ok(out)
4409 }
4410 }
4411 }
4412
4413 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
4414 if middle.nrows() != self.ncols() || middle.ncols() != self.ncols() {
4415 return Err(format!(
4416 "quadratic_form_diag dimension mismatch: matrix is {}x{}, expected {}x{}",
4417 middle.nrows(),
4418 middle.ncols(),
4419 self.ncols(),
4420 self.ncols()
4421 ));
4422 }
4423
4424 match self {
4425 Self::Dense(xd) => xd.quadratic_form_diag(middle),
4426 Self::Sparse(xs) => {
4427 let csr = xs
4428 .to_csr_arc()
4429 .ok_or_else(|| "quadratic_form_diag: failed to obtain CSR view".to_string())?;
4430 let sym = csr.symbolic();
4431 let row_ptr = sym.row_ptr();
4432 let col_idx = sym.col_idx();
4433 let vals = csr.val();
4434 let mut out = Array1::<f64>::zeros(self.nrows());
4435 for i in 0..xs.nrows() {
4436 let start = row_ptr[i];
4437 let end = row_ptr[i + 1];
4438 let mut acc = 0.0_f64;
4439 for a in start..end {
4440 let j = col_idx[a];
4441 let xij = vals[a];
4442 for b in start..end {
4443 let k = col_idx[b];
4444 let xik = vals[b];
4445 acc += xij * middle[[j, k]] * xik;
4446 }
4447 }
4448 out[i] = acc.max(0.0);
4449 }
4450 Ok(out)
4451 }
4452 }
4453 }
4454
4455 fn row_chunk_into(
4456 &self,
4457 rows: Range<usize>,
4458 out: ArrayViewMut2<'_, f64>,
4459 ) -> Result<(), MatrixMaterializationError> {
4460 if out.nrows() != rows.end - rows.start || out.ncols() != self.ncols() {
4461 return Err(MatrixMaterializationError::MissingRowChunk {
4462 context: "DesignMatrix::row_chunk_into shape mismatch",
4463 });
4464 }
4465 match self {
4466 Self::Dense(matrix) => matrix.row_chunk_into(rows, out),
4467 Self::Sparse(matrix) => matrix.row_chunk_into(rows, out),
4468 }
4469 }
4470
4471 fn to_dense(&self) -> Array2<f64> {
4472 DesignMatrix::to_dense(self)
4473 }
4474}
4475
4476impl LinearOperator for DenseRightProductView<'_> {
4477 fn nrows(&self) -> usize {
4478 self.base.nrows()
4479 }
4480
4481 fn ncols(&self) -> usize {
4482 self.transformed_ncols()
4483 }
4484
4485 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
4486 let rhs;
4487 let v = match (self.second, self.first) {
4488 (None, None) => vector,
4489 (Some(s), None) => {
4490 rhs = fast_av(s, vector);
4491 &rhs
4492 }
4493 (None, Some(f)) => {
4494 rhs = fast_av(f, vector);
4495 &rhs
4496 }
4497 (Some(s), Some(f)) => {
4498 let tmp = fast_av(s, vector);
4499 rhs = fast_av(f, &tmp);
4500 &rhs
4501 }
4502 };
4503 fast_av(self.base, v)
4504 }
4505
4506 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
4507 let mut out = fast_atv(self.base, vector);
4508 if let Some(factor) = self.first {
4509 out = fast_atv(factor, &out);
4510 }
4511 if let Some(factor) = self.second {
4512 out = fast_atv(factor, &out);
4513 }
4514 out
4515 }
4516
4517 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
4518 if weights.len() != self.nrows() {
4519 return Err(format!(
4520 "xt_diag_x dimension mismatch: weights length {} != nrows {}",
4521 weights.len(),
4522 self.nrows()
4523 ));
4524 }
4525 certify_signed_weights("DenseRightProductView::diag_xtw_x", weights, self.nrows())?;
4526 let mut gram = fast_xt_diag_x(self.base, weights);
4527 if let Some(factor) = self.first {
4528 gram = fast_ab(&fast_atb(factor, &gram), factor);
4529 }
4530 if let Some(factor) = self.second {
4531 gram = fast_ab(&fast_atb(factor, &gram), factor);
4532 }
4533 Ok(gram)
4534 }
4535
4536 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
4537 Ok(self.diag_xtw_x(weights)?.diag().to_owned())
4538 }
4539}
4540
4541impl DenseRightProductView<'_> {
4542 pub fn compute_xtwy(
4543 &self,
4544 weights: &Array1<f64>,
4545 y: &Array1<f64>,
4546 ) -> Result<Array1<f64>, String> {
4547 if weights.len() != self.nrows() || y.len() != self.nrows() {
4548 return Err(format!(
4549 "compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
4550 weights.len(),
4551 y.len(),
4552 self.nrows()
4553 ));
4554 }
4555 certify_signed_weights("DenseRightProductView::compute_xtwy", weights, self.nrows())?;
4556 let weighted_xty = dense_transpose_weighted_response(self.base, weights, y, None);
4557 let mut out = weighted_xty;
4558 if let Some(factor) = self.first {
4559 out = fast_atv(factor, &out);
4560 }
4561 if let Some(factor) = self.second {
4562 out = fast_atv(factor, &out);
4563 }
4564 Ok(out)
4565 }
4566
4567 pub fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
4568 let dense = self.materialize();
4569 DesignMatrix::Dense(DenseDesignMatrix::from(dense)).quadratic_form_diag(middle)
4570 }
4571}
4572
4573impl LinearOperator for EmbeddedColumnBlock<'_> {
4574 fn nrows(&self) -> usize {
4575 self.local.nrows()
4576 }
4577
4578 fn ncols(&self) -> usize {
4579 self.total_cols
4580 }
4581
4582 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
4583 fast_av(
4584 self.local,
4585 &vector.slice(ndarray::s![self.global_range.clone()]),
4586 )
4587 }
4588
4589 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
4590 let mut out = Array1::<f64>::zeros(self.total_cols);
4591 out.slice_mut(ndarray::s![self.global_range.clone()])
4592 .assign(&fast_atv(self.local, vector));
4593 out
4594 }
4595
4596 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
4597 if weights.len() != self.nrows() {
4598 return Err(format!(
4599 "xt_diag_x dimension mismatch: weights length {} != nrows {}",
4600 weights.len(),
4601 self.nrows()
4602 ));
4603 }
4604 certify_signed_weights("EmbeddedColumnBlock::diag_xtw_x", weights, self.nrows())?;
4605 let mut out = Array2::<f64>::zeros((self.total_cols, self.total_cols));
4606 let local = fast_xt_diag_x(self.local, weights);
4607 out.slice_mut(ndarray::s![
4608 self.global_range.clone(),
4609 self.global_range.clone()
4610 ])
4611 .assign(&local);
4612 Ok(out)
4613 }
4614
4615 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
4616 let mut out = Array1::<f64>::zeros(self.total_cols);
4617 let local =
4618 DesignMatrix::Dense(DenseDesignMatrix::from(self.local.clone())).diag_gram(weights)?;
4619 out.slice_mut(ndarray::s![self.global_range.clone()])
4620 .assign(&local);
4621 Ok(out)
4622 }
4623}
4624
4625impl EmbeddedColumnBlock<'_> {
4626 pub fn compute_xtwy(
4627 &self,
4628 weights: &Array1<f64>,
4629 y: &Array1<f64>,
4630 ) -> Result<Array1<f64>, String> {
4631 if weights.len() != self.nrows() || y.len() != self.nrows() {
4632 return Err(format!(
4633 "compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
4634 weights.len(),
4635 y.len(),
4636 self.nrows()
4637 ));
4638 }
4639 certify_signed_weights("EmbeddedColumnBlock::compute_xtwy", weights, self.nrows())?;
4640 let local = dense_transpose_weighted_response(self.local, weights, y, None);
4641 let mut out = Array1::<f64>::zeros(self.total_cols);
4642 out.slice_mut(ndarray::s![self.global_range.clone()])
4643 .assign(&local);
4644 Ok(out)
4645 }
4646
4647 pub fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
4648 let middle_local = middle
4649 .slice(ndarray::s![
4650 self.global_range.clone(),
4651 self.global_range.clone()
4652 ])
4653 .to_owned();
4654 DesignMatrix::Dense(DenseDesignMatrix::from(self.local.clone()))
4655 .quadratic_form_diag(&middle_local)
4656 }
4657}
4658
4659impl DesignMatrix {
4660 fn factorize_system_dense(
4661 &self,
4662 weights: &Array1<f64>,
4663 penalty: Option<&Array2<f64>>,
4664 ) -> Result<Box<dyn FactorizedSystem>, String> {
4665 let mut system = self.diag_xtw_x(weights)?;
4666 if let Some(pen) = penalty {
4667 if pen.nrows() != system.nrows() || pen.ncols() != system.ncols() {
4668 return Err(format!(
4669 "factorize_system penalty shape mismatch: got {}x{}, expected {}x{}",
4670 pen.nrows(),
4671 pen.ncols(),
4672 system.nrows(),
4673 system.ncols()
4674 ));
4675 }
4676 system += pen;
4677 }
4678 let factor = crate::utils::StableSolver::new()
4679 .factorize(&system)
4680 .map_err(|e| format!("factorize_system failed: {e:?}"))?;
4681 Ok(Box::new(factor))
4682 }
4683}
4684
4685fn assemble_sparseweighted_gram_system(
4686 matrix: &SparseDesignMatrix,
4687 weights: &Array1<f64>,
4688 penalty: Option<&Array2<f64>>,
4689) -> Result<SparseColMat<usize, f64>, String> {
4690 certify_signed_weights(
4691 "assemble_sparseweighted_gram_system",
4692 weights,
4693 matrix.nrows(),
4694 )?;
4695 let csr = matrix
4696 .to_csr_arc()
4697 .ok_or_else(|| "failed to obtain CSR view in factorize_system".to_string())?;
4698 let sym = csr.symbolic();
4699 let row_ptr = sym.row_ptr();
4700 let col_idx = sym.col_idx();
4701 let vals = csr.val();
4702 let p = matrix.ncols();
4703 let mut upper = BTreeMap::<(usize, usize), f64>::new();
4704
4705 for i in 0..csr.nrows() {
4706 let wi = weights[i];
4707 if wi == 0.0 {
4708 continue;
4709 }
4710 let start = row_ptr[i];
4711 let end = row_ptr[i + 1];
4712 for a_ptr in start..end {
4713 let a = col_idx[a_ptr];
4714 let xa = vals[a_ptr];
4715 for b_ptr in a_ptr..end {
4716 let b = col_idx[b_ptr];
4717 let xb = vals[b_ptr];
4718 let key = if a <= b { (a, b) } else { (b, a) };
4719 *upper.entry(key).or_insert(0.0) += wi * xa * xb;
4720 }
4721 }
4722 }
4723
4724 if let Some(pen) = penalty {
4725 if pen.nrows() != p || pen.ncols() != p {
4726 return Err(format!(
4727 "factorize_system penalty shape mismatch: got {}x{}, expected {}x{}",
4728 pen.nrows(),
4729 pen.ncols(),
4730 p,
4731 p
4732 ));
4733 }
4734 for i in 0..p {
4735 for j in i..p {
4736 let value = pen[[i, j]];
4737 if value != 0.0 {
4738 *upper.entry((i, j)).or_insert(0.0) += value;
4739 }
4740 }
4741 }
4742 }
4743
4744 let mut triplets = Vec::with_capacity(upper.len());
4745 for ((row, col), value) in upper {
4746 if value != 0.0 {
4747 triplets.push(Triplet::new(row, col, value));
4748 }
4749 }
4750 SparseColMat::try_new_from_triplets(p, p, &triplets)
4751 .map_err(|_| "failed to build sparse penalized system".to_string())
4752}
4753
4754impl DesignMatrix {
4755 pub fn hstack(blocks: Vec<DesignMatrix>) -> Result<Self, String> {
4761 if blocks.is_empty() {
4762 return Err("DesignMatrix::hstack requires at least one block".to_string());
4763 }
4764 if blocks.len() == 1 {
4765 return Ok(blocks.into_iter().next().expect("non-empty block list"));
4766 }
4767 let operator =
4768 BlockDesignOperator::new(blocks.into_iter().map(DesignBlock::from).collect())?;
4769 Ok(Self::Dense(DenseDesignMatrix::from(Arc::new(operator))))
4770 }
4771
4772 pub fn nrows(&self) -> usize {
4773 <Self as LinearOperator>::nrows(self)
4774 }
4775
4776 pub fn ncols(&self) -> usize {
4777 <Self as LinearOperator>::ncols(self)
4778 }
4779
4780 pub fn try_row_chunk(
4786 &self,
4787 rows: Range<usize>,
4788 ) -> Result<Array2<f64>, MatrixMaterializationError> {
4789 match self {
4790 Self::Dense(matrix) => matrix.try_row_chunk(rows),
4791 Self::Sparse(matrix) => {
4792 let csr =
4793 matrix
4794 .to_csr_arc()
4795 .ok_or(MatrixMaterializationError::MissingRowChunk {
4796 context: "DesignMatrix::try_row_chunk: failed to obtain CSR view",
4797 })?;
4798 let sym = csr.symbolic();
4799 let row_ptr = sym.row_ptr();
4800 let col_idx = sym.col_idx();
4801 let vals = csr.val();
4802 let chunk_rows = rows.end - rows.start;
4803 let ncols = self.ncols();
4804 let mut out = Array2::<f64>::zeros((chunk_rows, ncols));
4805 for (local_row, row) in rows.enumerate() {
4806 for ptr in row_ptr[row]..row_ptr[row + 1] {
4807 out[[local_row, col_idx[ptr]]] = vals[ptr];
4808 }
4809 }
4810 Ok(out)
4811 }
4812 }
4813 }
4814
4815 pub fn row_chunk_into(
4821 &self,
4822 rows: Range<usize>,
4823 out: ArrayViewMut2<'_, f64>,
4824 ) -> Result<(), MatrixMaterializationError> {
4825 <Self as DenseDesignOperator>::row_chunk_into(self, rows, out)
4826 }
4827
4828 pub fn try_to_dense_governed(
4834 &self,
4835 context: &'static str,
4836 ) -> Result<Governed<Array2<f64>>, MatrixMaterializationError> {
4837 self.try_to_dense_governed_with_policy(
4838 &ResourcePolicy::default_library().material_policy(),
4839 context,
4840 )
4841 }
4842
4843 pub fn try_to_dense_governed_with_policy(
4846 &self,
4847 policy: &MaterializationPolicy,
4848 context: &'static str,
4849 ) -> Result<Governed<Array2<f64>>, MatrixMaterializationError> {
4850 governed_dense_operator_to_dense_by_chunks(self, policy, context)
4851 }
4852
4853 pub fn try_to_dense_by_chunks(&self, context: &str) -> Result<Array2<f64>, String> {
4854 let n = self.nrows();
4855 let p = self.ncols();
4856 let chunk_rows = dense_materialization_chunk_rows(n, p);
4857 let mut out = Array2::<f64>::zeros((n, p));
4858 for start in (0..n).step_by(chunk_rows) {
4859 let end = (start + chunk_rows).min(n);
4860 let slice = out.slice_mut(s![start..end, ..]);
4861 self.row_chunk_into(start..end, slice)
4862 .map_err(|err| format!("{context}: failed to materialize row chunk: {err}"))?;
4863 }
4864 Ok(out)
4865 }
4866
4867 pub fn try_to_dense_by_chunks_budgeted(
4873 &self,
4874 context: &str,
4875 max_bytes: usize,
4876 ) -> Result<Array2<f64>, String> {
4877 let n = self.nrows();
4878 let p = self.ncols();
4879 let dense_bytes = checked_dense_nbytes(n, p, context)?;
4880 if dense_bytes > max_bytes {
4881 let gib = dense_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
4882 let cap_gib = max_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
4883 return Err(MatrixError::DensificationRefused {
4884 reason: format!(
4885 "{context}: refusing to densify {n}x{p} (~{gib:.2} GiB, cap ~{cap_gib:.2} GiB)"
4886 ),
4887 }
4888 .into());
4889 }
4890 self.try_to_dense_by_chunks(context)
4891 }
4892
4893 pub fn dot_row(&self, row: usize, beta: &Array1<f64>) -> f64 {
4896 self.dot_row_view(row, beta.view())
4897 }
4898
4899 pub fn dot_row_view(&self, row: usize, beta: ArrayView1<'_, f64>) -> f64 {
4900 assert_eq!(
4901 beta.len(),
4902 self.ncols(),
4903 "DesignMatrix::dot_row_view length mismatch: beta={}, ncols={}",
4904 beta.len(),
4905 self.ncols()
4906 );
4907 match self {
4908 Self::Dense(matrix) => {
4909 if let Some(dense) = matrix.as_dense_ref() {
4910 dense.row(row).dot(&beta)
4911 } else {
4912 matrix
4913 .try_row_chunk(row..row + 1)
4914 .expect("DesignMatrix::dot_row_view: try_row_chunk must succeed")
4915 .row(0)
4916 .dot(&beta)
4917 }
4918 }
4919 Self::Sparse(matrix) => {
4920 let csr = matrix
4928 .to_csr_arc()
4929 .expect("DesignMatrix::dot_row: failed to obtain CSR view");
4930 let sym = csr.symbolic();
4931 let row_ptr = sym.row_ptr();
4932 let col_idx = sym.col_idx();
4933 let vals = csr.val();
4934 let mut out = 0.0;
4935 for ptr in row_ptr[row]..row_ptr[row + 1] {
4936 out += vals[ptr] * beta[col_idx[ptr]];
4937 }
4938 out
4939 }
4940 }
4941 }
4942
4943 pub fn axpy_row_into(
4945 &self,
4946 row: usize,
4947 alpha: f64,
4948 out: &mut ArrayViewMut1<'_, f64>,
4949 ) -> Result<(), String> {
4950 self.axpy_row_into_impl(row, alpha, out, false, "axpy_row_into")
4951 }
4952
4953 pub fn squared_axpy_row_into(
4956 &self,
4957 row: usize,
4958 alpha: f64,
4959 out: &mut ArrayViewMut1<'_, f64>,
4960 ) -> Result<(), String> {
4961 self.axpy_row_into_impl(row, alpha, out, true, "squared_axpy_row_into")
4962 }
4963
4964 #[inline]
4971 fn axpy_row_into_impl(
4972 &self,
4973 row: usize,
4974 alpha: f64,
4975 out: &mut ArrayViewMut1<'_, f64>,
4976 square: bool,
4977 method: &str,
4978 ) -> Result<(), String> {
4979 if out.len() != self.ncols() {
4980 return Err(format!(
4981 "DesignMatrix::{method} length mismatch: out={}, ncols={}",
4982 out.len(),
4983 self.ncols()
4984 ));
4985 }
4986 if alpha == 0.0 {
4987 return Ok(());
4988 }
4989 let scale = |value: f64| {
4991 if square {
4992 alpha * value * value
4993 } else {
4994 alpha * value
4995 }
4996 };
4997 match self {
4998 Self::Dense(matrix) => {
4999 if let Some(dense) = matrix.as_dense_ref() {
5000 for (dst, &value) in out.iter_mut().zip(dense.row(row).iter()) {
5001 *dst += scale(value);
5002 }
5003 } else {
5004 let chunk = matrix
5005 .try_row_chunk(row..row + 1)
5006 .map_err(|e| format!("DesignMatrix::{method}: {e}"))?;
5007 for (dst, &value) in out.iter_mut().zip(chunk.row(0).iter()) {
5008 *dst += scale(value);
5009 }
5010 }
5011 }
5012 Self::Sparse(matrix) => {
5013 let csr = matrix
5018 .to_csr_arc()
5019 .ok_or_else(|| format!("DesignMatrix::{method}: failed to obtain CSR view"))?;
5020 let sym = csr.symbolic();
5021 let row_ptr = sym.row_ptr();
5022 let col_idx = sym.col_idx();
5023 let vals = csr.val();
5024 for ptr in row_ptr[row]..row_ptr[row + 1] {
5025 out[col_idx[ptr]] += scale(vals[ptr]);
5026 }
5027 }
5028 }
5029 Ok(())
5030 }
5031
5032 pub fn crossdiag_axpy_row_into(
5038 &self,
5039 row: usize,
5040 other: &DesignMatrix,
5041 alpha: f64,
5042 out: &mut ArrayViewMut1<'_, f64>,
5043 ) -> Result<(), String> {
5044 assert_eq!(self.ncols(), other.ncols());
5045 assert_eq!(out.len(), self.ncols());
5046 if alpha == 0.0 {
5047 return Ok(());
5048 }
5049 match (self, other) {
5050 (Self::Dense(lhs), Self::Dense(rhs)) => {
5051 let lhs_chunk;
5052 let rhs_chunk;
5053 let x = if let Some(lhs_dense) = lhs.as_dense_ref() {
5054 lhs_dense.row(row)
5055 } else {
5056 lhs_chunk = lhs
5057 .try_row_chunk(row..row + 1)
5058 .map_err(|e| format!("crossdiag_axpy_row_into lhs: {e}"))?;
5059 lhs_chunk.row(0)
5060 };
5061 let y = if let Some(rhs_dense) = rhs.as_dense_ref() {
5062 rhs_dense.row(row)
5063 } else {
5064 rhs_chunk = rhs
5065 .try_row_chunk(row..row + 1)
5066 .map_err(|e| format!("crossdiag_axpy_row_into rhs: {e}"))?;
5067 rhs_chunk.row(0)
5068 };
5069 for (dst, (&xi, &yi)) in out.iter_mut().zip(x.iter().zip(y.iter())) {
5070 *dst += alpha * xi * yi;
5071 }
5072 }
5073 (Self::Sparse(lhs), Self::Sparse(rhs)) => {
5074 let lhs_csr = lhs.to_csr_arc().ok_or_else(|| {
5080 "crossdiag_axpy_row_into: failed to obtain lhs CSR view".to_string()
5081 })?;
5082 let rhs_csr = rhs.to_csr_arc().ok_or_else(|| {
5083 "crossdiag_axpy_row_into: failed to obtain rhs CSR view".to_string()
5084 })?;
5085 let lhs_sym = lhs_csr.symbolic();
5086 let rhs_sym = rhs_csr.symbolic();
5087 let lhs_rp = lhs_sym.row_ptr();
5088 let rhs_rp = rhs_sym.row_ptr();
5089 let lhs_ci = lhs_sym.col_idx();
5090 let rhs_ci = rhs_sym.col_idx();
5091 let lhs_v = lhs_csr.val();
5092 let rhs_v = rhs_csr.val();
5093 let mut li = lhs_rp[row];
5095 let mut ri = rhs_rp[row];
5096 let l_end = lhs_rp[row + 1];
5097 let r_end = rhs_rp[row + 1];
5098 while li < l_end && ri < r_end {
5099 let lc = lhs_ci[li];
5100 let rc = rhs_ci[ri];
5101 if lc == rc {
5102 out[lc] += alpha * lhs_v[li] * rhs_v[ri];
5103 li += 1;
5104 ri += 1;
5105 } else if lc < rc {
5106 li += 1;
5107 } else {
5108 ri += 1;
5109 }
5110 }
5111 }
5112 _ => {
5113 let (sparse_mat, dense_mat) = match (self, other) {
5115 (Self::Sparse(s), Self::Dense(d)) => (s, d),
5116 (Self::Dense(d), Self::Sparse(s)) => (s, d),
5117 _ => {
5120 return Err(
5121 "crossdiag_axpy_row_into: mixed-arm dispatch reached non-mixed pair"
5122 .to_string(),
5123 );
5124 }
5125 };
5126 let csr = sparse_mat.to_csr_arc().ok_or_else(|| {
5130 "crossdiag_axpy_row_into: failed to obtain CSR view".to_string()
5131 })?;
5132 let sym = csr.symbolic();
5133 let row_ptr = sym.row_ptr();
5134 let col_idx = sym.col_idx();
5135 let vals = csr.val();
5136 let dense_chunk;
5137 let dense_row = if let Some(dense_ref) = dense_mat.as_dense_ref() {
5138 dense_ref.row(row)
5139 } else {
5140 dense_chunk = dense_mat
5141 .try_row_chunk(row..row + 1)
5142 .map_err(|e| format!("crossdiag_axpy_row_into dense chunk: {e}"))?;
5143 dense_chunk.row(0)
5144 };
5145 for ptr in row_ptr[row]..row_ptr[row + 1] {
5146 let c = col_idx[ptr];
5147 out[c] += alpha * vals[ptr] * dense_row[c];
5148 }
5149 }
5150 }
5151 Ok(())
5152 }
5153
5154 pub fn syr_row_into(
5156 &self,
5157 row: usize,
5158 alpha: f64,
5159 target: &mut Array2<f64>,
5160 ) -> Result<(), String> {
5161 self.syr_row_into_view(row, alpha, target.view_mut())
5162 }
5163
5164 pub fn syr_row_into_view(
5167 &self,
5168 row: usize,
5169 alpha: f64,
5170 mut target: ArrayViewMut2<'_, f64>,
5171 ) -> Result<(), String> {
5172 if target.nrows() != self.ncols() || target.ncols() != self.ncols() {
5173 return Err(format!(
5174 "DesignMatrix::syr_row_into shape mismatch: target={}x{}, ncols={}",
5175 target.nrows(),
5176 target.ncols(),
5177 self.ncols()
5178 ));
5179 }
5180 if alpha == 0.0 {
5181 return Ok(());
5182 }
5183 match self {
5184 Self::Dense(matrix) => {
5185 if let Some(dense) = matrix.as_dense_ref() {
5186 let x = dense.row(row);
5187 for i in 0..x.len() {
5188 let xi = x[i];
5189 if xi == 0.0 {
5190 continue;
5191 }
5192 for j in 0..x.len() {
5193 target[[i, j]] += alpha * xi * x[j];
5194 }
5195 }
5196 } else {
5197 let chunk = matrix
5198 .try_row_chunk(row..row + 1)
5199 .map_err(|e| format!("DesignMatrix::syr_row_into: {e}"))?;
5200 let x = chunk.row(0);
5201 for i in 0..x.len() {
5202 let xi = x[i];
5203 if xi == 0.0 {
5204 continue;
5205 }
5206 for j in 0..x.len() {
5207 target[[i, j]] += alpha * xi * x[j];
5208 }
5209 }
5210 }
5211 }
5212 Self::Sparse(matrix) => {
5213 let csr = matrix.to_csr_arc().ok_or_else(|| {
5218 "DesignMatrix::syr_row_into: failed to obtain CSR view".to_string()
5219 })?;
5220 let sym = csr.symbolic();
5221 let row_ptr = sym.row_ptr();
5222 let col_idx = sym.col_idx();
5223 let vals = csr.val();
5224 for ptr_i in row_ptr[row]..row_ptr[row + 1] {
5225 let i = col_idx[ptr_i];
5226 let xi = vals[ptr_i];
5227 for ptr_j in row_ptr[row]..row_ptr[row + 1] {
5228 let j = col_idx[ptr_j];
5229 target[[i, j]] += alpha * xi * vals[ptr_j];
5230 }
5231 }
5232 }
5233 }
5234 Ok(())
5235 }
5236
5237 pub fn row_outer_into(
5242 &self,
5243 row: usize,
5244 other: &DesignMatrix,
5245 alpha: f64,
5246 target: &mut Array2<f64>,
5247 ) -> Result<(), String> {
5248 self.row_outer_into_view(row, other, alpha, target.view_mut())
5249 }
5250
5251 pub fn row_outer_into_view(
5254 &self,
5255 row: usize,
5256 other: &DesignMatrix,
5257 alpha: f64,
5258 mut target: ArrayViewMut2<'_, f64>,
5259 ) -> Result<(), String> {
5260 if target.nrows() != self.ncols() || target.ncols() != other.ncols() {
5261 return Err(format!(
5262 "DesignMatrix::row_outer_into shape mismatch: target={}x{}, lhs={}, rhs={}",
5263 target.nrows(),
5264 target.ncols(),
5265 self.ncols(),
5266 other.ncols()
5267 ));
5268 }
5269 if alpha == 0.0 {
5270 return Ok(());
5271 }
5272 match (self, other) {
5273 (Self::Dense(lhs), Self::Dense(rhs)) => {
5274 let lhs_chunk;
5275 let rhs_chunk;
5276 let x = if let Some(lhs_dense) = lhs.as_dense_ref() {
5277 lhs_dense.row(row)
5278 } else {
5279 lhs_chunk = lhs
5280 .try_row_chunk(row..row + 1)
5281 .map_err(|e| format!("row_outer_into_view lhs: {e}"))?;
5282 lhs_chunk.row(0)
5283 };
5284 let y = if let Some(rhs_dense) = rhs.as_dense_ref() {
5285 rhs_dense.row(row)
5286 } else {
5287 rhs_chunk = rhs
5288 .try_row_chunk(row..row + 1)
5289 .map_err(|e| format!("row_outer_into_view rhs: {e}"))?;
5290 rhs_chunk.row(0)
5291 };
5292 for i in 0..x.len() {
5293 let xi = x[i];
5294 if xi == 0.0 {
5295 continue;
5296 }
5297 for j in 0..y.len() {
5298 target[[i, j]] += alpha * xi * y[j];
5299 }
5300 }
5301 }
5302 (Self::Sparse(lhs), Self::Sparse(rhs)) => {
5303 let lhs_csr = lhs
5308 .to_csr_arc()
5309 .ok_or_else(|| "row_outer_into: failed to obtain lhs CSR view".to_string())?;
5310 let rhs_csr = rhs
5312 .to_csr_arc()
5313 .ok_or_else(|| "row_outer_into: failed to obtain rhs CSR view".to_string())?;
5314 let lhs_sym = lhs_csr.symbolic();
5315 let rhs_sym = rhs_csr.symbolic();
5316 let lhs_rp = lhs_sym.row_ptr();
5317 let rhs_rp = rhs_sym.row_ptr();
5318 let lhs_ci = lhs_sym.col_idx();
5319 let rhs_ci = rhs_sym.col_idx();
5320 let lhs_v = lhs_csr.val();
5321 let rhs_v = rhs_csr.val();
5322 for pi in lhs_rp[row]..lhs_rp[row + 1] {
5323 let i = lhs_ci[pi];
5324 let xi = lhs_v[pi];
5325 for pj in rhs_rp[row]..rhs_rp[row + 1] {
5326 let j = rhs_ci[pj];
5327 target[[i, j]] += alpha * xi * rhs_v[pj];
5328 }
5329 }
5330 }
5331 _ => {
5332 let x = self
5334 .try_row_chunk(row..row + 1)
5335 .map_err(|e| format!("row_outer_into_view lhs: {e}"))?;
5336 let x_row = x.row(0);
5337 let y = other
5338 .try_row_chunk(row..row + 1)
5339 .map_err(|e| format!("row_outer_into_view rhs: {e}"))?;
5340 let y_row = y.row(0);
5341 for i in 0..x_row.len() {
5342 let xi = x_row[i];
5343 if xi == 0.0 {
5344 continue;
5345 }
5346 for j in 0..y_row.len() {
5347 target[[i, j]] += alpha * xi * y_row[j];
5348 }
5349 }
5350 }
5351 }
5352 Ok(())
5353 }
5354
5355 pub fn apply_view_into(&self, vector: ArrayView1<'_, f64>, mut output: ArrayViewMut1<'_, f64>) {
5361 assert_eq!(self.ncols(), vector.len());
5362 assert_eq!(self.nrows(), output.len());
5363 match self {
5364 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => {
5365 crate::dense::matvec_into(matrix.as_ref(), vector, output);
5366 }
5367 Self::Dense(DenseDesignMatrix::Lazy(operator)) => {
5368 output.assign(&operator.apply(&vector.to_owned()));
5369 }
5370 Self::Sparse(matrix) => {
5371 output.fill(0.0);
5372 let (symbolic, values) = matrix.parts();
5373 let col_ptr = symbolic.col_ptr();
5374 let row_idx = symbolic.row_idx();
5375 for col in 0..matrix.ncols() {
5376 let x = vector[col];
5377 if x == 0.0 {
5378 continue;
5379 }
5380 for idx in col_ptr[col]..col_ptr[col + 1] {
5381 output[row_idx[idx]] += values[idx] * x;
5382 }
5383 }
5384 }
5385 }
5386 }
5387
5388 pub fn apply_view(&self, vector: ArrayView1<'_, f64>) -> Array1<f64> {
5390 let mut output = Array1::<f64>::zeros(self.nrows());
5391 self.apply_view_into(vector, output.view_mut());
5392 output
5393 }
5394
5395 pub fn transpose_apply_view_into(
5397 &self,
5398 vector: ArrayView1<'_, f64>,
5399 mut output: ArrayViewMut1<'_, f64>,
5400 ) {
5401 assert_eq!(self.nrows(), vector.len());
5402 assert_eq!(self.ncols(), output.len());
5403 match self {
5404 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => {
5405 crate::dense::transpose_matvec_into(matrix.as_ref(), vector, output);
5406 }
5407 Self::Dense(DenseDesignMatrix::Lazy(operator)) => {
5408 output.assign(&operator.apply_transpose(&vector.to_owned()));
5409 }
5410 Self::Sparse(matrix) => {
5411 let (symbolic, values) = matrix.parts();
5412 let col_ptr = symbolic.col_ptr();
5413 let row_idx = symbolic.row_idx();
5414 for col in 0..matrix.ncols() {
5415 let mut value = 0.0;
5416 for idx in col_ptr[col]..col_ptr[col + 1] {
5417 value += values[idx] * vector[row_idx[idx]];
5418 }
5419 output[col] = value;
5420 }
5421 }
5422 }
5423 }
5424
5425 pub fn column_into(&self, col: usize, mut output: ArrayViewMut1<'_, f64>) {
5427 assert!(col < self.ncols());
5428 assert_eq!(self.nrows(), output.len());
5429 match self {
5430 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => {
5431 output.assign(&matrix.column(col));
5432 }
5433 Self::Dense(DenseDesignMatrix::Lazy(operator)) => {
5434 let mut basis = Array1::<f64>::zeros(operator.ncols());
5435 basis[col] = 1.0;
5436 output.assign(&operator.apply(&basis));
5437 }
5438 Self::Sparse(matrix) => {
5439 output.fill(0.0);
5440 let (symbolic, values) = matrix.parts();
5441 let col_ptr = symbolic.col_ptr();
5442 let row_idx = symbolic.row_idx();
5443 for idx in col_ptr[col]..col_ptr[col + 1] {
5444 output[row_idx[idx]] += values[idx];
5445 }
5446 }
5447 }
5448 }
5449
5450 #[inline]
5462 pub fn get(&self, i: usize, j: usize) -> f64 {
5463 match self {
5464 Self::Dense(matrix) => match matrix.as_dense_ref() {
5465 Some(dense) => dense[[i, j]],
5466 None => {
5471 let mut e_j = Array1::<f64>::zeros(matrix.ncols());
5472 e_j[j] = 1.0;
5473 matrix.apply(&e_j)[i]
5474 }
5475 },
5476 Self::Sparse(sp) => {
5477 let dense = sp
5485 .try_to_dense_arc("DesignMatrix::get")
5486 .unwrap_or_else(|msg| std::panic::panic_any(msg));
5487 dense[[i, j]]
5488 }
5489 }
5490 }
5491
5492 pub fn extract_column(&self, j: usize) -> Array1<f64> {
5498 let mut column = Array1::zeros(self.nrows());
5499 self.column_into(j, column.view_mut());
5500 column
5501 }
5502
5503 pub fn extract_columns(&self, cols: &[usize]) -> Array2<f64> {
5510 match self {
5511 Self::Dense(m) => match m {
5512 DenseDesignMatrix::Materialized(mat) => mat.select(Axis(1), cols),
5513 DenseDesignMatrix::Lazy(op) => op.apply_columns(cols),
5514 },
5515 Self::Sparse(sp) => {
5516 let n = sp.nrows();
5517 let mut out = Array2::<f64>::zeros((n, cols.len()));
5518 let (symbolic, values) = sp.parts();
5519 let col_ptr = symbolic.col_ptr();
5520 let row_idx = symbolic.row_idx();
5521 for (k, &j) in cols.iter().enumerate() {
5522 let start = col_ptr[j];
5523 let end = col_ptr[j + 1];
5524 let mut out_col = out.column_mut(k);
5525 for idx in start..end {
5526 out_col[row_idx[idx]] += values[idx];
5527 }
5528 }
5529 out
5530 }
5531 }
5532 }
5533
5534 pub fn as_dense_ref(&self) -> Option<&Array2<f64>> {
5536 match self {
5537 Self::Dense(matrix) => matrix.as_dense_ref(),
5538 Self::Sparse(_) => None,
5539 }
5540 }
5541
5542 pub const fn is_materialized_dense(&self) -> bool {
5543 matches!(self, Self::Dense(DenseDesignMatrix::Materialized(_)))
5544 }
5545
5546 pub const fn is_operator_backed(&self) -> bool {
5547 match self {
5548 Self::Dense(matrix) => matrix.is_operator_backed(),
5549 Self::Sparse(_) => false,
5550 }
5551 }
5552
5553 pub const fn is_sparse(&self) -> bool {
5559 matches!(self, Self::Sparse(_))
5560 }
5561
5562 pub fn as_dense_cow(&self) -> Cow<'_, Array2<f64>> {
5568 match self {
5569 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => Cow::Borrowed(matrix.as_ref()),
5570 Self::Dense(DenseDesignMatrix::Lazy(op)) => match op.as_dense_ref() {
5571 Some(dense) => Cow::Borrowed(dense),
5572 None => std::panic::panic_any(format!(
5579 "DesignMatrix::as_dense_cow called on operator-backed design ({}x{}); use row chunks or matrix-vector products",
5580 op.nrows(),
5581 op.ncols()
5582 )),
5583 },
5584 Self::Sparse(matrix) => Cow::Owned(
5585 matrix
5586 .try_to_dense_arc("DesignMatrix::as_dense_cow")
5587 .unwrap_or_else(|msg| std::panic::panic_any(msg))
5593 .as_ref()
5594 .clone(),
5595 ),
5596 }
5597 }
5598
5599 pub fn to_dense_cow(&self) -> Cow<'_, Array2<f64>> {
5608 match self {
5609 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => Cow::Borrowed(matrix.as_ref()),
5610 Self::Dense(DenseDesignMatrix::Lazy(lazy)) => {
5611 if let Some(dense) = lazy.as_dense_ref() {
5612 Cow::Borrowed(dense)
5613 } else {
5614 let policy = ResourcePolicy::default_library();
5615 panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
5616 "DesignMatrix::to_dense_cow",
5617 lazy.nrows(),
5618 lazy.ncols(),
5619 &policy,
5620 )
5621 .unwrap_or_else(|reason| std::panic::panic_any(reason));
5622 lazy.try_governed_dense_arc("DesignMatrix::to_dense_cow")
5628 .unwrap_or_else(|msg| std::panic::panic_any(msg));
5630 Cow::Borrowed(
5631 lazy.dense_memo
5632 .get()
5633 .expect("memo initialized by try_governed_dense_arc just above")
5634 .as_ref()
5635 .as_ref(),
5636 )
5637 }
5638 }
5639 Self::Sparse(matrix) => Cow::Owned(
5640 matrix
5641 .try_to_dense_arc("DesignMatrix::to_dense_cow")
5642 .unwrap_or_else(|msg| std::panic::panic_any(msg))
5648 .as_ref()
5649 .clone(),
5650 ),
5651 }
5652 }
5653
5654 pub fn to_dense(&self) -> Array2<f64> {
5665 match self {
5666 Self::Dense(matrix) => matrix.to_dense(),
5667 Self::Sparse(matrix) => matrix
5668 .try_to_dense_arc("DesignMatrix::to_dense")
5669 .unwrap_or_else(|msg| std::panic::panic_any(msg))
5671 .as_ref()
5672 .clone(),
5673 }
5674 }
5675
5676 pub fn to_dense_arc(&self) -> Arc<Array2<f64>> {
5678 match self {
5679 Self::Dense(matrix) => matrix.to_dense_arc(),
5680 Self::Sparse(matrix) => matrix
5681 .try_to_dense_arc("DesignMatrix::to_dense_arc")
5682 .unwrap_or_else(|msg| std::panic::panic_any(msg)),
5684 }
5685 }
5686
5687 pub fn try_to_dense_arc(&self, context: &str) -> Result<Arc<Array2<f64>>, String> {
5688 match self {
5689 Self::Dense(matrix) => matrix.try_to_dense_arc(context),
5690 Self::Sparse(matrix) => matrix.try_to_dense_arc(context),
5691 }
5692 }
5693
5694 pub fn try_to_dense_arc_with_policy(
5697 &self,
5698 context: &str,
5699 policy: &ResourcePolicy,
5700 ) -> Result<Arc<Array2<f64>>, String> {
5701 match self {
5702 Self::Dense(matrix) => matrix.try_to_dense_arc_with_policy(context, policy),
5703 Self::Sparse(matrix) => matrix.try_to_dense_arc(context),
5704 }
5705 }
5706
5707 pub fn to_csr_cache(&self) -> Option<SparseRowMat<usize, f64>> {
5708 match self {
5709 Self::Dense(_) => None,
5710 Self::Sparse(matrix) => matrix.to_csr_arc().map(|arc| (*arc).clone()),
5711 }
5712 }
5713
5714 pub fn as_sparse(&self) -> Option<&SparseDesignMatrix> {
5715 match self {
5716 Self::Sparse(matrix) => Some(matrix),
5717 Self::Dense(_) => None,
5718 }
5719 }
5720
5721 pub fn as_dense(&self) -> Option<&Array2<f64>> {
5722 match self {
5723 Self::Dense(matrix) => matrix.as_dense_ref(),
5724 Self::Sparse(_) => None,
5725 }
5726 }
5727
5728 fn apply_transpose_view(&self, vector: ArrayView1<'_, f64>) -> Array1<f64> {
5729 match self {
5730 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => fast_atv(matrix, &vector),
5731 Self::Dense(DenseDesignMatrix::Lazy(op)) => op.apply_transpose(&vector.to_owned()),
5732 Self::Sparse(matrix) => {
5733 let mut output = Array1::<f64>::zeros(matrix.ncols());
5734 let (symbolic, values) = matrix.parts();
5735 let col_ptr = symbolic.col_ptr();
5736 let row_idx = symbolic.row_idx();
5737 for col in 0..matrix.ncols() {
5738 let mut acc = 0.0;
5739 let start = col_ptr[col];
5740 let end = col_ptr[col + 1];
5741 for idx in start..end {
5742 acc += values[idx] * vector[row_idx[idx]];
5743 }
5744 output[col] = acc;
5745 }
5746 output
5747 }
5748 }
5749 }
5750
5751 fn diag_gram_view(&self, weights: ArrayView1<'_, f64>) -> Result<Array1<f64>, String> {
5752 if weights.len() != self.nrows() {
5753 return Err(format!(
5754 "diag_gram dimension mismatch: weights length {} != nrows {}",
5755 weights.len(),
5756 self.nrows()
5757 ));
5758 }
5759 FiniteSignedWeightsView::try_new(weights)
5760 .map_err(|reason| format!("DesignMatrix::diag_gram_view: {reason}"))?;
5761 match self {
5762 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => {
5763 Ok(dense_diag_gram_view(matrix, weights))
5764 }
5765 Self::Dense(DenseDesignMatrix::Lazy(op)) => op.diag_gram(&weights.to_owned()),
5766 Self::Sparse(xs) => {
5767 let p = xs.ncols();
5768 let csr = xs
5769 .to_csr_arc()
5770 .ok_or_else(|| "failed to obtain CSR view in diag_gram".to_string())?;
5771 let sym = csr.symbolic();
5772 Ok(sparse_csr_diag_gram(
5773 sym.row_ptr(),
5774 sym.col_idx(),
5775 csr.val(),
5776 xs.nrows(),
5777 p,
5778 weights,
5779 ))
5780 }
5781 }
5782 }
5783
5784 fn compute_xtwy_view(
5785 &self,
5786 weights: ArrayView1<'_, f64>,
5787 y: ArrayView1<'_, f64>,
5788 ) -> Result<Array1<f64>, String> {
5789 if weights.len() != self.nrows() || y.len() != self.nrows() {
5790 return Err(format!(
5791 "compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
5792 weights.len(),
5793 y.len(),
5794 self.nrows()
5795 ));
5796 }
5797 FiniteSignedWeightsView::try_new(weights)
5798 .map_err(|reason| format!("DesignMatrix::compute_xtwy_view: {reason}"))?;
5799 match self {
5800 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => {
5801 Ok(dense_transpose_weighted_response_view(matrix, weights, y))
5802 }
5803 Self::Dense(DenseDesignMatrix::Lazy(op)) => {
5804 op.compute_xtwy(&weights.to_owned(), &y.to_owned())
5805 }
5806 Self::Sparse(xs) => {
5807 let csr = xs
5808 .as_ref()
5809 .to_row_major()
5810 .map_err(|_| "failed to obtain CSR view in compute_xtwy".to_string())?;
5811 let sym = csr.symbolic();
5812 let row_ptr = sym.row_ptr();
5813 let col_idx = sym.col_idx();
5814 let vals = csr.val();
5815 let mut out = Array1::<f64>::zeros(xs.ncols());
5816 for i in 0..xs.nrows() {
5817 let scaled = weights[i] * y[i];
5818 if scaled == 0.0 {
5819 continue;
5820 }
5821 for idx in row_ptr[i]..row_ptr[i + 1] {
5822 out[col_idx[idx]] += vals[idx] * scaled;
5823 }
5824 }
5825 Ok(out)
5826 }
5827 }
5828 }
5829
5830 pub fn dot(&self, vector: &Array1<f64>) -> Array1<f64> {
5831 <Self as LinearOperator>::apply(self, vector)
5832 }
5833
5834 pub fn matrixvectormultiply(&self, vector: &Array1<f64>) -> Array1<f64> {
5835 <Self as LinearOperator>::apply(self, vector)
5836 }
5837
5838 pub fn transpose_vector_multiply(&self, vector: &Array1<f64>) -> Array1<f64> {
5839 <Self as LinearOperator>::apply_transpose(self, vector)
5840 }
5841
5842 pub fn compute_xtwy(
5843 &self,
5844 weights: &Array1<f64>,
5845 y: &Array1<f64>,
5846 ) -> Result<Array1<f64>, String> {
5847 <Self as DenseDesignOperator>::compute_xtwy(self, weights, y)
5848 }
5849
5850 pub fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
5851 <Self as LinearOperator>::diag_gram(self, weights)
5852 }
5853
5854 pub fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
5855 <Self as DenseDesignOperator>::quadratic_form_diag(self, middle)
5856 }
5857
5858 pub fn apply_weighted_normal(
5859 &self,
5860 weights: &Array1<f64>,
5861 vector: &Array1<f64>,
5862 penalty: Option<&Array2<f64>>,
5863 ridge: f64,
5864 ) -> Result<Array1<f64>, String> {
5865 let finite =
5866 certify_signed_weights("DesignMatrix::apply_weighted_normal", weights, self.nrows())?;
5867 if vector.len() != self.ncols() {
5868 return Err(format!(
5869 "DesignMatrix::apply_weighted_normal vector length mismatch: vector={}, ncols={}",
5870 vector.len(),
5871 self.ncols()
5872 ));
5873 }
5874 Ok(<Self as LinearOperator>::apply_weighted_normal(
5875 self, finite, vector, penalty, ridge,
5876 ))
5877 }
5878
5879 pub fn solve_system(
5880 &self,
5881 weights: &Array1<f64>,
5882 rhs: &Array1<f64>,
5883 penalty: Option<&Array2<f64>>,
5884 ) -> Result<Array1<f64>, String> {
5885 <Self as LinearOperator>::solve_system(self, weights, rhs, penalty)
5886 }
5887
5888 pub fn solve_systemwith_policy(
5889 &self,
5890 weights: &Array1<f64>,
5891 rhs: &Array1<f64>,
5892 penalty: Option<&Array2<f64>>,
5893 ridge_floor: f64,
5894 ridge_policy: RidgePolicy,
5895 ) -> Result<Array1<f64>, String> {
5896 <Self as LinearOperator>::solve_systemwith_policy(
5897 self,
5898 weights,
5899 rhs,
5900 penalty,
5901 ridge_floor,
5902 ridge_policy,
5903 )
5904 }
5905
5906 pub fn solve_system_matrix_free_pcg(
5907 &self,
5908 weights: &Array1<f64>,
5909 rhs: &Array1<f64>,
5910 penalty: Option<&Array2<f64>>,
5911 ridge_floor: f64,
5912 ) -> Result<Array1<f64>, String> {
5913 <Self as LinearOperator>::solve_system_matrix_free_pcg_try(
5914 self,
5915 weights,
5916 rhs,
5917 penalty,
5918 ridge_floor,
5919 )
5920 }
5921
5922 pub fn solve_system_matrix_free_pcg_with_info(
5923 &self,
5924 weights: &Array1<f64>,
5925 rhs: &Array1<f64>,
5926 penalty: Option<&Array2<f64>>,
5927 ridge_floor: f64,
5928 ) -> Result<(Array1<f64>, PcgSolveInfo), String> {
5929 <Self as LinearOperator>::solve_system_matrix_free_pcg_with_info_try(
5930 self,
5931 weights,
5932 rhs,
5933 penalty,
5934 ridge_floor,
5935 )
5936 }
5937
5938 pub fn should_use_matrix_free_pcg(&self) -> bool {
5939 <Self as LinearOperator>::uses_matrix_free_pcg(self)
5940 && self.ncols() >= MATRIX_FREE_PCG_MIN_P
5941 }
5942
5943 pub fn factorize_system(
5944 &self,
5945 weights: &Array1<f64>,
5946 penalty: Option<&Array2<f64>>,
5947 ) -> Result<Box<dyn FactorizedSystem>, String> {
5948 <Self as LinearOperator>::factorize_system(self, weights, penalty)
5949 }
5950}
5951
5952impl<'a> From<ArrayView2<'a, f64>> for DesignMatrix {
5953 fn from(value: ArrayView2<'a, f64>) -> Self {
5954 Self::Dense(DenseDesignMatrix::from(value.to_owned()))
5955 }
5956}
5957
5958impl From<Array2<f64>> for DesignMatrix {
5959 fn from(value: Array2<f64>) -> Self {
5960 Self::Dense(DenseDesignMatrix::from(value))
5961 }
5962}
5963
5964impl From<Arc<Array2<f64>>> for DesignMatrix {
5965 fn from(value: Arc<Array2<f64>>) -> Self {
5966 Self::Dense(DenseDesignMatrix::from(value))
5967 }
5968}
5969
5970impl From<&Array2<f64>> for DesignMatrix {
5971 fn from(value: &Array2<f64>) -> Self {
5972 Self::Dense(DenseDesignMatrix::from(value.clone()))
5973 }
5974}
5975
5976impl From<DenseDesignMatrix> for DesignMatrix {
5977 fn from(value: DenseDesignMatrix) -> Self {
5978 Self::Dense(value)
5979 }
5980}
5981
5982impl From<SparseColMat<usize, f64>> for DesignMatrix {
5983 fn from(value: SparseColMat<usize, f64>) -> Self {
5984 Self::Sparse(SparseDesignMatrix::new(value))
5985 }
5986}
5987
5988impl From<&SparseColMat<usize, f64>> for DesignMatrix {
5989 fn from(value: &SparseColMat<usize, f64>) -> Self {
5990 Self::Sparse(SparseDesignMatrix::new(value.clone()))
5991 }
5992}
5993
5994impl From<&DesignMatrix> for DesignMatrix {
5995 fn from(value: &DesignMatrix) -> Self {
5996 value.clone()
5997 }
5998}
5999
6000impl From<DesignMatrix> for DesignBlock {
6001 fn from(value: DesignMatrix) -> Self {
6002 match value {
6003 DesignMatrix::Dense(matrix) => Self::Dense(matrix),
6004 DesignMatrix::Sparse(matrix) => Self::Sparse(matrix),
6005 }
6006 }
6007}
6008
6009impl From<&DesignMatrix> for DesignBlock {
6010 fn from(value: &DesignMatrix) -> Self {
6011 match value {
6012 DesignMatrix::Dense(matrix) => Self::Dense(matrix.clone()),
6013 DesignMatrix::Sparse(matrix) => Self::Sparse(matrix.clone()),
6014 }
6015 }
6016}
6017
6018#[cfg(test)]
6019mod tests {
6020 use super::{
6021 BlockDesignOperator, CoefficientTransformOperator, ConditionedDesign, DenseDesignMatrix,
6022 DenseDesignOperator, DesignBlock, DesignMatrix, EmbeddedColumnBlock,
6023 FiniteSignedWeightsView, MultiChannelOperator, PsdWeightsView, RandomEffectOperator,
6024 ReparamOperator, RowwiseKroneckerOperator, SparseDesignMatrix,
6025 dense_operator_to_dense_by_chunks, dense_transpose_weighted_response, fast_atv, fast_av,
6026 streaming_sparse_csc_xt_diag_x, weighted_crossprod_dense_view, xt_diag_x_symmetric,
6027 };
6028 use crate::matrix::LinearOperator;
6029 use crate::test_support::no_densify_design;
6030 use crate::types::RidgePolicy;
6031 use crate::utils::{PcgSolveInfo, StableSolver};
6032 use faer::sparse::{SparseColMat, SymbolicSparseColMat, Triplet};
6033 use gam_runtime::resource::{
6034 MaterializationPolicy, MatrixMaterializationError, MemoryGovernor, ResourcePolicy,
6035 };
6036 use ndarray::{Array1, Array2, ArrayViewMut2, Axis, array, s};
6037 use std::ops::Range;
6038 use std::sync::Arc;
6039 use std::sync::atomic::{AtomicUsize, Ordering};
6040
6041 struct ChunkOnlyOperator {
6042 n: usize,
6043 p: usize,
6044 row_chunk_calls: AtomicUsize,
6045 materialization_policy: Option<MaterializationPolicy>,
6046 }
6047
6048 impl ChunkOnlyOperator {
6049 fn value(&self, i: usize, j: usize) -> f64 {
6050 ((i % 251) as f64) * 0.25 - ((j % 127) as f64) * 0.5 + ((i + j) % 7) as f64
6051 }
6052 }
6053
6054 impl LinearOperator for ChunkOnlyOperator {
6055 fn nrows(&self) -> usize {
6056 self.n
6057 }
6058
6059 fn ncols(&self) -> usize {
6060 self.p
6061 }
6062
6063 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
6064 let mut out = Array1::<f64>::zeros(self.n);
6065 for i in 0..self.n {
6066 let mut acc = 0.0;
6067 for j in 0..self.p {
6068 acc += self.value(i, j) * vector[j];
6069 }
6070 out[i] = acc;
6071 }
6072 out
6073 }
6074
6075 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
6076 let mut out = Array1::<f64>::zeros(self.p);
6077 for i in 0..self.n {
6078 for j in 0..self.p {
6079 out[j] += self.value(i, j) * vector[i];
6080 }
6081 }
6082 out
6083 }
6084
6085 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
6086 let dense = dense_operator_to_dense_by_chunks(self).map_err(|err| err.to_string())?;
6087 let psd = PsdWeightsView::try_new(weights.view())?;
6088 Ok(weighted_crossprod_dense_view(&dense, psd.view(), &dense))
6089 }
6090 }
6091
6092 impl DenseDesignOperator for ChunkOnlyOperator {
6093 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
6094 self.materialization_policy.clone()
6095 }
6096
6097 fn row_chunk_into(
6098 &self,
6099 rows: Range<usize>,
6100 mut out: ArrayViewMut2<'_, f64>,
6101 ) -> Result<(), MatrixMaterializationError> {
6102 self.row_chunk_calls.fetch_add(1, Ordering::SeqCst);
6103 if out.nrows() != rows.end - rows.start || out.ncols() != self.p {
6104 return Err(MatrixMaterializationError::MissingRowChunk {
6105 context: "ChunkOnlyOperator::row_chunk_into shape mismatch",
6106 });
6107 }
6108 for (local, row) in rows.enumerate() {
6109 for col in 0..self.p {
6110 out[[local, col]] = self.value(row, col);
6111 }
6112 }
6113 Ok(())
6114 }
6115
6116 fn to_dense(&self) -> Array2<f64> {
6117 panic!("ChunkOnlyOperator::to_dense fallback must not be used")
6119 }
6120 }
6121
6122 struct DirectFillOnlyOperator {
6123 values: Array2<f64>,
6124 row_chunk_calls: AtomicUsize,
6125 }
6126
6127 impl LinearOperator for DirectFillOnlyOperator {
6128 fn nrows(&self) -> usize {
6129 self.values.nrows()
6130 }
6131
6132 fn ncols(&self) -> usize {
6133 self.values.ncols()
6134 }
6135
6136 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
6137 self.values.dot(vector)
6138 }
6139
6140 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
6141 self.values.t().dot(vector)
6142 }
6143
6144 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
6145 let mut out = Array2::<f64>::zeros((self.ncols(), self.ncols()));
6146 for row in 0..self.nrows() {
6147 for left in 0..self.ncols() {
6148 for right in 0..self.ncols() {
6149 out[[left, right]] +=
6150 weights[row] * self.values[[row, left]] * self.values[[row, right]];
6151 }
6152 }
6153 }
6154 Ok(out)
6155 }
6156 }
6157
6158 impl DenseDesignOperator for DirectFillOnlyOperator {
6159 fn row_chunk_into(
6160 &self,
6161 rows: Range<usize>,
6162 mut out: ArrayViewMut2<'_, f64>,
6163 ) -> Result<(), MatrixMaterializationError> {
6164 self.row_chunk_calls.fetch_add(1, Ordering::SeqCst);
6165 if rows.end > self.nrows()
6166 || out.nrows() != rows.end - rows.start
6167 || out.ncols() != self.ncols()
6168 {
6169 return Err(MatrixMaterializationError::MissingRowChunk {
6170 context: "DirectFillOnlyOperator::row_chunk_into shape mismatch",
6171 });
6172 }
6173 out.assign(&self.values.slice(s![rows, ..]));
6174 Ok(())
6175 }
6176
6177 fn try_row_chunk(
6178 &self,
6179 rows: Range<usize>,
6180 ) -> Result<Array2<f64>, MatrixMaterializationError> {
6181 panic!(
6182 "DirectFillOnlyOperator owned row chunk {}..{} is forbidden",
6183 rows.start, rows.end
6184 )
6185 }
6186
6187 fn to_dense(&self) -> Array2<f64> {
6188 panic!("DirectFillOnlyOperator dense materialization is forbidden")
6189 }
6190 }
6191
6192 fn exact_weighted_penalized_solve(
6193 design: &Array2<f64>,
6194 weights: &Array1<f64>,
6195 rhs: &Array1<f64>,
6196 penalty: &Array2<f64>,
6197 ridge: f64,
6198 ) -> Array1<f64> {
6199 let mut h = design
6200 .t()
6201 .dot(&(design * &weights.view().insert_axis(Axis(1))));
6202 h += penalty;
6203 if ridge > 0.0 {
6204 for i in 0..h.nrows() {
6205 h[[i, i]] += ridge;
6206 }
6207 }
6208 let factor = StableSolver::new()
6209 .factorize(&h)
6210 .expect("exact reference factorization");
6211 let mut solution = rhs.clone();
6212 let mut solution_matrix = crate::faer_ndarray::array1_to_col_matmut(&mut solution);
6213 factor.solve_in_place(solution_matrix.as_mut());
6214 assert!(solution.iter().all(|value| value.is_finite()));
6215 solution
6216 }
6217
6218 #[test]
6219 fn fast_av_matches_ndarray_dot() {
6220 let x = array![[1.0, 2.0, -1.0], [0.5, -3.0, 4.0], [2.0, 0.0, 1.5]];
6221 let v = array![0.25, -1.0, 2.0];
6222 let expected = x.dot(&v);
6223 let got = fast_av(&x, &v);
6224 for i in 0..expected.len() {
6225 assert!((expected[i] - got[i]).abs() < 1e-12);
6226 }
6227 }
6228
6229 #[test]
6230 fn fast_atv_matches_ndarray_dot() {
6231 let x = array![[1.0, 2.0, -1.0], [0.5, -3.0, 4.0], [2.0, 0.0, 1.5]];
6232 let v = array![0.25, -1.0, 2.0];
6233 let expected = x.t().dot(&v);
6234 let got = fast_atv(&x, &v);
6235 for i in 0..expected.len() {
6236 assert!((expected[i] - got[i]).abs() < 1e-12);
6237 }
6238 }
6239
6240 #[test]
6241 fn sparse_to_dense_accumulates_duplicate_entries() {
6242 let ledger = ledger_read_guard();
6247 assert_eq!(*ledger, (), "ledger read guard is held for this test");
6248 let symbolic = SymbolicSparseColMat::new_unsorted_checked(
6251 3,
6252 2,
6253 vec![0_usize, 2, 3],
6254 None,
6255 vec![1_usize, 1, 0],
6256 );
6257 let sparse = SparseColMat::new(symbolic, vec![2.0_f64, 3.5, -1.0]);
6258 let design = DesignMatrix::from(sparse);
6259 let dense = design.to_dense_arc();
6260
6261 assert!((dense[[1, 0]] - 5.5).abs() < 1e-12);
6262 assert!((dense[[0, 1]] + 1.0).abs() < 1e-12);
6263
6264 let v = array![4.0, -2.0];
6265 let y_sparse = design.matrixvectormultiply(&v);
6266 let y_dense = dense.dot(&v);
6267 for i in 0..y_sparse.len() {
6268 assert!((y_sparse[i] - y_dense[i]).abs() < 1e-12);
6269 }
6270 }
6271
6272 #[test]
6273 fn sparse_column_extractors_accumulate_duplicate_entries() {
6274 let ledger = ledger_read_guard();
6276 assert_eq!(*ledger, (), "ledger read guard is held for this test");
6277 let symbolic = SymbolicSparseColMat::new_unsorted_checked(
6282 3,
6283 2,
6284 vec![0_usize, 2, 3],
6285 None,
6286 vec![1_usize, 1, 0],
6287 );
6288 let sparse = SparseColMat::new(symbolic, vec![2.0_f64, 3.5, -1.0]);
6289 let design = DesignMatrix::from(sparse);
6290 let dense = design.to_dense();
6291
6292 let mut col0 = Array1::<f64>::zeros(3);
6293 design.column_into(0, col0.view_mut());
6294 assert!((col0[1] - 5.5).abs() < 1e-12);
6295 for i in 0..3 {
6296 assert!((col0[i] - dense[[i, 0]]).abs() < 1e-12);
6297 }
6298
6299 let block = design.extract_columns(&[0, 1]);
6300 assert!((block[[1, 0]] - 5.5).abs() < 1e-12);
6301 assert!((block[[0, 1]] + 1.0).abs() < 1e-12);
6302 for i in 0..3 {
6303 for (k, &j) in [0usize, 1].iter().enumerate() {
6304 assert!((block[[i, k]] - dense[[i, j]]).abs() < 1e-12);
6305 }
6306 }
6307 }
6308
6309 #[test]
6310 fn huge_sparse_densification_is_rejected_before_allocation() {
6311 let sparse = SparseColMat::try_new_from_triplets(1usize << 44, 4, &[])
6315 .expect("empty sparse matrix should build");
6316 let design = SparseDesignMatrix::new(sparse);
6317 let err = design
6318 .try_to_dense_arc("matrix test")
6319 .expect_err("huge sparse densification should be rejected");
6320 assert!(err.contains("refusing to densify sparse design"));
6321 }
6322
6323 static LEDGER_PRESSURE: std::sync::RwLock<()> = std::sync::RwLock::new(());
6335
6336 fn ledger_read_guard() -> std::sync::RwLockReadGuard<'static, ()> {
6338 LEDGER_PRESSURE
6339 .read()
6340 .unwrap_or_else(|poisoned| poisoned.into_inner())
6341 }
6342
6343 fn ledger_write_guard() -> std::sync::RwLockWriteGuard<'static, ()> {
6345 LEDGER_PRESSURE
6346 .write()
6347 .unwrap_or_else(|poisoned| poisoned.into_inner())
6348 }
6349
6350 #[test]
6355 fn sparse_densification_reserves_ledger_and_full_ledger_streams() {
6356 let ledger = ledger_write_guard();
6359 assert_eq!(*ledger, (), "ledger pressure guard is held for this test");
6360 let triplets = [
6361 Triplet::new(0, 0, 1.0),
6362 Triplet::new(0, 1, -2.0),
6363 Triplet::new(1, 0, 0.5),
6364 Triplet::new(1, 1, 3.0),
6365 Triplet::new(2, 0, -1.5),
6366 Triplet::new(2, 1, 0.25),
6367 ];
6368 let sparse = SparseColMat::try_new_from_triplets(3, 2, &triplets).expect("sparse");
6369 let governor = MemoryGovernor::global();
6370
6371 let design = SparseDesignMatrix::new(sparse.clone());
6372 let before = governor.reserved_bytes();
6373 let footprint = 3 * 2 * std::mem::size_of::<f64>();
6374 let governed = design
6375 .try_to_dense_governed("governed sparse test")
6376 .expect("small governed densification succeeds");
6377 assert_eq!(
6378 governor.reserved_bytes(),
6379 before + footprint,
6380 "governed densification must charge its dense footprint"
6381 );
6382 assert_eq!(governed.dim(), (3, 2));
6383
6384 crate::governed_capture::begin_governed_decision_capture();
6390 let second = design
6391 .try_to_dense_governed("governed sparse test, second owner")
6392 .expect("a memoized design admits again");
6393 let arms: Vec<_> = crate::governed_capture::take_governed_decision_capture()
6394 .expect("capture was started")
6395 .into_iter()
6396 .map(|decision| decision.arm)
6397 .collect();
6398 assert_eq!(
6399 arms,
6400 vec![crate::governed_capture::GovernedArm::CacheHit],
6401 "a design that has already been densified must report a cache hit, \
6402 not a second admission"
6403 );
6404 assert!(
6405 std::ptr::eq(&**governed, &**second),
6406 "both governed owners must share the design's one dense image"
6407 );
6408 drop(governed);
6409 drop(second);
6410 assert_eq!(
6411 governor.reserved_bytes(),
6412 before + footprint,
6413 "the memo holds the charge, not the owner: dropping owners must not \
6414 release bytes that still back a live cached buffer"
6415 );
6416
6417 {
6420 let scoped = SparseDesignMatrix::new(sparse.clone());
6421 let owner = scoped
6422 .try_to_dense_governed("governed sparse scoped owner")
6423 .expect("small governed densification succeeds");
6424 assert_eq!(
6425 governor.reserved_bytes(),
6426 before + 2 * footprint,
6427 "a second design must charge its own dense footprint"
6428 );
6429 drop(owner);
6430 }
6431 assert_eq!(
6432 governor.reserved_bytes(),
6433 before + footprint,
6434 "dropping the design must release the memo's charge"
6435 );
6436
6437 let dense = design.to_dense_arc();
6445
6446 let filler = governor
6450 .try_reserve(governor.remaining_bytes(), "test ledger filler")
6451 .expect("filling the remaining budget succeeds");
6452 let pressured = SparseDesignMatrix::new(sparse.clone());
6453 assert!(
6454 pressured
6455 .try_to_dense_governed("governed sparse test under pressure")
6456 .is_err(),
6457 "a full ledger must refuse governed densification"
6458 );
6459 let weights = array![1.0, -2.0, 0.5];
6460 let gram = xt_diag_x_symmetric(&DesignMatrix::from(sparse.clone()), &weights)
6461 .expect("streaming fallback under a full ledger");
6462 let mut expected = Array2::<f64>::zeros((2, 2));
6463 for row in 0..3 {
6464 for a in 0..2 {
6465 for b in 0..2 {
6466 expected[[a, b]] += weights[row] * dense[[row, a]] * dense[[row, b]];
6467 }
6468 }
6469 }
6470 let got = gram.as_dense().expect("dense symmetric result");
6471 for a in 0..2 {
6472 for b in 0..2 {
6473 assert!(
6474 (got[[a, b]] - expected[[a, b]]).abs() < 1e-12,
6475 "streaming fallback Gram mismatch at ({a}, {b})"
6476 );
6477 }
6478 }
6479 drop(filler);
6480 }
6481
6482 #[test]
6483 fn streaming_sparse_csc_xt_diag_x_matches_dense_signed_weights() {
6484 let sparse = SparseColMat::try_new_from_triplets(
6485 4,
6486 3,
6487 &[
6488 Triplet::new(0, 0, 1.0),
6489 Triplet::new(1, 0, 2.0),
6490 Triplet::new(2, 0, -1.0),
6491 Triplet::new(0, 1, 0.5),
6492 Triplet::new(1, 1, -3.0),
6493 Triplet::new(3, 1, 4.0),
6494 Triplet::new(0, 2, 2.0),
6495 Triplet::new(2, 2, 1.5),
6496 Triplet::new(3, 2, -0.25),
6497 ],
6498 )
6499 .expect("sparse matrix");
6500 let design = SparseDesignMatrix::new(sparse.clone());
6501 let ledger = ledger_read_guard();
6503 assert_eq!(*ledger, (), "ledger read guard is held for this test");
6504 let dense = design.to_dense_arc();
6505 let weights = array![1.0, -2.0, 0.5, -1.5];
6506 let (symbolic, values) = sparse.parts();
6507 let mut got = Array2::<f64>::zeros((3, 3));
6508 streaming_sparse_csc_xt_diag_x(
6509 symbolic.col_ptr(),
6510 symbolic.row_idx(),
6511 values,
6512 4,
6513 3,
6514 weights.view(),
6515 &mut got,
6516 );
6517
6518 let mut expected = Array2::<f64>::zeros((3, 3));
6519 for row in 0..4 {
6520 for a in 0..3 {
6521 for b in 0..3 {
6522 expected[[a, b]] += weights[row] * dense[[row, a]] * dense[[row, b]];
6523 }
6524 }
6525 }
6526 let max_diff = (&got - &expected)
6527 .iter()
6528 .map(|v| v.abs())
6529 .fold(0.0_f64, f64::max);
6530 assert!(
6531 max_diff < 1e-12,
6532 "streamed sparse weighted Gram mismatch: max_diff={max_diff}"
6533 );
6534 }
6535
6536 #[test]
6537 fn block_design_row_chunk_into_fills_mixed_blocks_without_owned_child_chunks() {
6538 let eager = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]];
6539 let lazy = Arc::new(DirectFillOnlyOperator {
6540 values: array![[10.0, 11.0], [12.0, 13.0], [14.0, 15.0], [16.0, 17.0]],
6541 row_chunk_calls: AtomicUsize::new(0),
6542 });
6543 let sparse = SparseColMat::try_new_from_triplets(
6544 4,
6545 3,
6546 &[
6547 Triplet::new(0, 0, 20.0),
6548 Triplet::new(1, 1, 21.0),
6549 Triplet::new(2, 2, 22.0),
6550 Triplet::new(3, 0, 23.0),
6551 ],
6552 )
6553 .expect("sparse block");
6554 let random_effect = Arc::new(RandomEffectOperator::new(
6555 vec![Some(0), None, Some(1), Some(0)],
6556 2,
6557 ));
6558 let op = BlockDesignOperator::new(vec![
6559 DesignBlock::Dense(DenseDesignMatrix::from(eager)),
6560 DesignBlock::Dense(DenseDesignMatrix::from(Arc::clone(&lazy))),
6561 DesignBlock::Sparse(SparseDesignMatrix::new(sparse)),
6562 DesignBlock::RandomEffect(random_effect),
6563 DesignBlock::Intercept(4),
6564 ])
6565 .expect("mixed block design");
6566
6567 let mut got = Array2::<f64>::from_elem((3, 10), f64::NAN);
6568 op.row_chunk_into(1..4, got.view_mut())
6569 .expect("mixed block direct row fill");
6570
6571 assert_eq!(
6572 got,
6573 array![
6574 [3.0, 4.0, 12.0, 13.0, 0.0, 21.0, 0.0, 0.0, 0.0, 1.0],
6575 [5.0, 6.0, 14.0, 15.0, 0.0, 0.0, 22.0, 0.0, 1.0, 1.0],
6576 [7.0, 8.0, 16.0, 17.0, 23.0, 0.0, 0.0, 1.0, 0.0, 1.0],
6577 ]
6578 );
6579 assert_eq!(lazy.row_chunk_calls.load(Ordering::SeqCst), 1);
6580 }
6581
6582 #[test]
6583 fn multi_channel_row_chunk_into_crosses_boundary_without_owned_channel_chunks() {
6584 let first = Arc::new(DirectFillOnlyOperator {
6585 values: array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]],
6586 row_chunk_calls: AtomicUsize::new(0),
6587 });
6588 let second = Arc::new(DirectFillOnlyOperator {
6589 values: array![[10.0, 20.0], [30.0, 40.0], [50.0, 60.0]],
6590 row_chunk_calls: AtomicUsize::new(0),
6591 });
6592 let op = MultiChannelOperator::new(vec![
6593 DesignMatrix::Dense(DenseDesignMatrix::from(Arc::clone(&first))),
6594 DesignMatrix::Dense(DenseDesignMatrix::from(Arc::clone(&second))),
6595 ])
6596 .expect("direct-fill multi-channel operator");
6597
6598 let mut got = Array2::<f64>::from_elem((3, 2), f64::NAN);
6599 op.row_chunk_into(2..5, got.view_mut())
6600 .expect("cross-channel direct row fill");
6601
6602 assert_eq!(got, array![[5.0, 6.0], [10.0, 20.0], [30.0, 40.0]]);
6603 assert_eq!(first.row_chunk_calls.load(Ordering::SeqCst), 1);
6604 assert_eq!(second.row_chunk_calls.load(Ordering::SeqCst), 1);
6605 }
6606
6607 #[test]
6608 fn multi_channel_operator_view_paths_match_stacked_dense_reference() {
6609 let dense_channel = array![[1.0, 2.0], [0.5, -1.0], [3.0, 0.25]];
6610 let sparse_dense = array![[0.0, 1.5], [2.0, 0.0], [-1.0, 0.75]];
6611 let sparse = SparseColMat::try_new_from_triplets(
6612 3,
6613 2,
6614 &[
6615 Triplet::new(1, 0, 2.0),
6616 Triplet::new(2, 0, -1.0),
6617 Triplet::new(0, 1, 1.5),
6618 Triplet::new(2, 1, 0.75),
6619 ],
6620 )
6621 .expect("sparse channel");
6622 let op = MultiChannelOperator::new(vec![
6623 DesignMatrix::Dense(DenseDesignMatrix::from(dense_channel.clone())),
6624 DesignMatrix::from(sparse),
6625 ])
6626 .expect("multi-channel operator");
6627 let mut stacked = Array2::<f64>::zeros((6, 2));
6628 stacked.slice_mut(s![0..3, ..]).assign(&dense_channel);
6629 stacked.slice_mut(s![3..6, ..]).assign(&sparse_dense);
6630
6631 let beta = array![0.25, -0.4];
6632 let expected_apply = stacked.dot(&beta);
6633 let got_apply = op.apply(&beta);
6634 for i in 0..expected_apply.len() {
6635 assert!((expected_apply[i] - got_apply[i]).abs() < 1e-12);
6636 }
6637
6638 let probe = array![0.5, -1.0, 0.25, 1.5, -0.75, 0.2];
6639 let expected_transpose = stacked.t().dot(&probe);
6640 let got_transpose = op.apply_transpose(&probe);
6641 for i in 0..expected_transpose.len() {
6642 assert!((expected_transpose[i] - got_transpose[i]).abs() < 1e-12);
6643 }
6644
6645 let weights = array![1.0, -0.5, 0.75, 2.0, -0.25, 1.5];
6646 let weighted = stacked.clone() * weights.view().insert_axis(Axis(1));
6647 let expected_xtwx = stacked.t().dot(&weighted);
6648 let got_xtwx = op.diag_xtw_x(&weights).expect("multi-channel xtwx");
6649 for i in 0..expected_xtwx.nrows() {
6650 for j in 0..expected_xtwx.ncols() {
6651 assert!((expected_xtwx[[i, j]] - got_xtwx[[i, j]]).abs() < 1e-12);
6652 }
6653 }
6654
6655 let expected_diag = Array1::from_iter((0..2).map(|j| expected_xtwx[[j, j]]));
6656 let got_diag = op.diag_gram(&weights).expect("multi-channel diag gram");
6657 for i in 0..expected_diag.len() {
6658 assert!((expected_diag[i] - got_diag[i]).abs() < 1e-12);
6659 }
6660
6661 let y = array![1.0, 0.5, -0.25, 2.0, -1.0, 0.75];
6662 let expected_xtwy = stacked.t().dot(&(&weights * &y));
6663 let got_xtwy = op.compute_xtwy(&weights, &y).expect("multi-channel xtwy");
6664 for i in 0..expected_xtwy.len() {
6665 assert!((expected_xtwy[i] - got_xtwy[i]).abs() < 1e-12);
6666 }
6667 }
6668
6669 #[test]
6670 fn random_effect_weighted_operators_preserve_signed_curvature() {
6671 let op = RandomEffectOperator::new(vec![Some(0), Some(1), Some(0), None, Some(1)], 2);
6672 let weights = array![2.0, -3.0, 0.5, -7.0, 1.25];
6673 let expected_diag = array![2.5, -1.75];
6674
6675 let gram = op.diag_xtw_x(&weights).expect("signed random-effect Gram");
6676 assert_eq!(gram, Array2::from_diag(&expected_diag));
6677 assert_eq!(
6678 op.diag_gram(&weights)
6679 .expect("fixture design and weight vector share n rows"),
6680 expected_diag
6681 );
6682
6683 let dense = array![[1.0, 2.0], [3.0, -1.0], [4.0, 0.5], [9.0, 9.0], [-2.0, 3.0]];
6684 let cross = op
6685 .weighted_cross_with_dense(&dense, &weights)
6686 .expect("signed dense/random-effect cross product");
6687 let re_dense = op.to_dense();
6688 let expected_cross = dense
6689 .t()
6690 .dot(&(&re_dense * &weights.view().insert_axis(Axis(1))));
6691 assert_eq!(cross, expected_cross);
6692
6693 let beta = array![4.0, -2.0];
6694 let finite = FiniteSignedWeightsView::try_from_array(&weights)
6695 .expect("fixture weights are finite and signed-representable");
6696 let normal = op.apply_weighted_normal(finite, &beta, None, 0.0);
6697 assert_eq!(normal, &expected_diag * &beta);
6698
6699 let y = array![1.0, 2.0, -4.0, 100.0, 0.5];
6700 let got_xtwy = op
6701 .compute_xtwy(&weights, &y)
6702 .expect("fixture design and weight vector share n rows");
6703 let expected_xtwy = re_dense.t().dot(&(&weights * &y));
6704 assert_eq!(got_xtwy, expected_xtwy);
6705 }
6706
6707 #[test]
6708 fn conditioned_design_signed_gram_and_response_match_materialized_reference() {
6709 let raw = array![[1.0, 5.0], [2.0, -1.0], [-3.0, 2.0], [4.0, 7.0]];
6710 let conditioned = ConditionedDesign::new(
6711 DesignMatrix::Dense(DenseDesignMatrix::from(raw)),
6712 vec![(1, 2.0, 3.0)],
6713 );
6714 let dense = conditioned.to_dense();
6715 let weights = array![2.0, -4.0, 0.5, -1.5];
6716 let weighted = &dense * &weights.view().insert_axis(Axis(1));
6717 let expected_gram = dense.t().dot(&weighted);
6718 let got_gram = conditioned
6719 .diag_xtw_x(&weights)
6720 .expect("fixture design and weight vector share n rows");
6721 assert!(
6722 (&got_gram - &expected_gram)
6723 .iter()
6724 .all(|value| value.abs() < 1e-12)
6725 );
6726 let got_diag = conditioned
6727 .diag_gram(&weights)
6728 .expect("fixture design and weight vector share n rows");
6729 assert!(
6730 (&got_diag - &expected_gram.diag())
6731 .iter()
6732 .all(|value| value.abs() < 1e-12)
6733 );
6734
6735 let y = array![0.5, -2.0, 3.0, 1.25];
6736 let expected_xtwy = dense.t().dot(&(&weights * &y));
6737 let got_xtwy = conditioned
6738 .compute_xtwy(&weights, &y)
6739 .expect("fixture design and weight vector share n rows");
6740 assert!(
6741 (&got_xtwy - &expected_xtwy)
6742 .iter()
6743 .all(|value| value.abs() < 1e-12)
6744 );
6745 }
6746
6747 #[test]
6748 fn weighted_operator_certification_reports_smallest_nonfinite_row() {
6749 let channel = DesignMatrix::Dense(DenseDesignMatrix::from(array![[1.0], [2.0], [3.0]]));
6750 let op = MultiChannelOperator::new(vec![channel])
6751 .expect("a single channel is a valid multi-channel operator");
6752 let bad = array![1.0, f64::NAN, f64::INFINITY];
6753
6754 for err in [
6755 op.diag_xtw_x(&bad).unwrap_err(),
6756 op.diag_gram(&bad).unwrap_err(),
6757 op.compute_xtwy(&bad, &array![1.0, 1.0, 1.0]).unwrap_err(),
6758 ] {
6759 assert!(err.contains("row 1"), "unexpected diagnostic: {err}");
6760 }
6761 }
6762
6763 #[test]
6770 fn block_design_fused_dense_cross_matches_stacked_reference_xtwx() {
6771 let b0 = array![
6772 [1.0, 2.0],
6773 [0.5, -1.0],
6774 [3.0, 0.25],
6775 [-2.0, 1.5],
6776 [0.75, -0.5],
6777 ];
6778 let b1 = array![
6779 [-1.0, 0.5, 2.0],
6780 [1.5, -0.25, 0.0],
6781 [0.0, 1.0, -1.5],
6782 [2.0, 0.5, 1.0],
6783 [-0.5, -1.0, 0.25],
6784 ];
6785 let b2 = array![[0.5], [-1.0], [2.0], [0.25], [-0.75]];
6786
6787 let mut stacked = Array2::<f64>::zeros((5, 6));
6788 stacked.slice_mut(s![.., 0..2]).assign(&b0);
6789 stacked.slice_mut(s![.., 2..5]).assign(&b1);
6790 stacked.slice_mut(s![.., 5..6]).assign(&b2);
6791
6792 let blocks = vec![
6793 DesignBlock::Dense(DenseDesignMatrix::from(b0)),
6794 DesignBlock::Dense(DenseDesignMatrix::from(b1)),
6795 DesignBlock::Dense(DenseDesignMatrix::from(b2)),
6796 ];
6797 let op = BlockDesignOperator::new(blocks).expect("block design");
6798
6799 let weights = array![1.5, -0.5, 2.0, -1.0, 0.75];
6801 let weighted = stacked.clone() * weights.view().insert_axis(Axis(1));
6802 let expected = stacked.t().dot(&weighted);
6803
6804 let got = op.diag_xtw_x(&weights).expect("block fused xtwx");
6805 assert_eq!(got.dim(), (6, 6));
6806 let max_diff = (&got - &expected)
6807 .iter()
6808 .map(|v| v.abs())
6809 .fold(0.0_f64, f64::max);
6810 assert!(
6811 max_diff < 1e-10,
6812 "fused block Dense×Dense Gram mismatch: max_diff={max_diff}"
6813 );
6814 }
6815
6816 #[test]
6817 fn block_design_intercept_cross_and_diag_are_sign_honest() {
6818 let x = array![[2.0], [5.0], [-1.0], [3.0]];
6825 let mut stacked = Array2::<f64>::zeros((4, 2));
6826 stacked.column_mut(0).fill(1.0);
6827 stacked.slice_mut(s![.., 1..2]).assign(&x);
6828
6829 let blocks = vec![
6830 DesignBlock::Intercept(4),
6831 DesignBlock::Dense(DenseDesignMatrix::from(x)),
6832 ];
6833 let op = BlockDesignOperator::new(blocks).expect("block design");
6834
6835 let weights = array![3.0, -1.0, 2.0, -0.5];
6836 let weighted = stacked.clone() * weights.view().insert_axis(Axis(1));
6837 let expected = stacked.t().dot(&weighted);
6838
6839 let got = op.diag_xtw_x(&weights).expect("block fused xtwx");
6840 assert_eq!(got.dim(), (2, 2));
6841 let max_diff = (&got - &expected)
6842 .iter()
6843 .map(|v| v.abs())
6844 .fold(0.0_f64, f64::max);
6845 assert!(
6846 max_diff < 1e-10,
6847 "intercept-block Gram mismatch: got={got:?} expected={expected:?} max_diff={max_diff}"
6848 );
6849
6850 let intercept_block = &op.blocks[0];
6852 let diag = intercept_block
6853 .diag_xtw_x(&weights)
6854 .expect("intercept diag_xtw_x");
6855 assert!((diag[[0, 0]] - weights.sum()).abs() < 1e-12);
6856 let gram = intercept_block
6857 .diag_gram(&weights)
6858 .expect("intercept diag_gram");
6859 assert!((gram[0] - weights.sum()).abs() < 1e-12);
6860 }
6861
6862 #[test]
6863 #[should_panic(expected = "ReparamOperator: X cols (2) must match Qs rows (3)")]
6864 fn reparam_operator_rejects_incompatible_transform_shape() {
6865 let x = array![[1.0, 2.0], [0.5, -1.0]];
6866 let qs = Arc::new(Array2::<f64>::zeros((3, 1)));
6867 ReparamOperator::new(DesignMatrix::Dense(DenseDesignMatrix::from(x)), qs);
6868 }
6869
6870 #[test]
6882 fn coefficient_transform_operator_exposes_cached_dense_to_block_dispatch() {
6883 let inner = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
6884 let transform = array![[0.5, -1.0, 2.0], [1.0, 0.0, -0.5]];
6885 let expected = inner.dot(&transform);
6886
6887 let op =
6888 CoefficientTransformOperator::new(DenseDesignMatrix::from(inner), transform.clone())
6889 .expect("coefficient transform operator");
6890 let dense_design = DenseDesignMatrix::from(Arc::new(op));
6891
6892 let probe = Array1::from_elem(3, 1.0);
6897 let warmed = dense_design.apply_transpose(&probe);
6898 assert_eq!(warmed.len(), expected.ncols());
6899
6900 let dense_ref = dense_design
6901 .as_dense_ref()
6902 .expect("DenseDesignMatrix::as_dense_ref must reach the cached X·T");
6903 assert_eq!(dense_ref.dim(), expected.dim());
6904 for ((r, c), v) in expected.indexed_iter() {
6905 assert!((dense_ref[[r, c]] - v).abs() < 1e-12);
6906 }
6907 }
6908
6909 #[test]
6910 fn coefficient_transform_operator_preserves_lazy_inner_storage() {
6911 let inner_values = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
6912 let transform = array![[0.5, -1.0], [1.0, 0.25]];
6913 let expected = inner_values.dot(&transform);
6914 let DesignMatrix::Dense(inner) = no_densify_design(inner_values) else {
6915 panic!("no-densify fixture must be dense-operator-backed");
6916 };
6917 let op = CoefficientTransformOperator::new(inner, transform)
6918 .expect("coefficient transform operator");
6919 let dense_design = DenseDesignMatrix::from(Arc::new(op));
6920
6921 let probe = Array1::from_elem(2, 1.0);
6922 let got = dense_design.apply(&probe);
6923 let want = expected.dot(&probe);
6924 for (got_i, want_i) in got.iter().zip(want.iter()) {
6925 assert!((got_i - want_i).abs() < 1e-12);
6926 }
6927 assert!(
6928 dense_design.as_dense_ref().is_none(),
6929 "coefficient transform must not materialize an operator-backed inner design"
6930 );
6931 }
6932
6933 #[test]
6934 fn design_matrix_hstack_preserves_lazy_blocks() {
6935 let left_dense = array![[1.0, 2.0], [3.0, 4.0]];
6936 let right_dense = array![[5.0], [6.0]];
6937 let left = no_densify_design(left_dense.clone());
6938 let right = no_densify_design(right_dense.clone());
6939 let stacked = DesignMatrix::hstack(vec![left, right]).expect("stacked design");
6940
6941 assert!(stacked.as_dense_ref().is_none());
6942 assert!(!stacked.is_materialized_dense());
6943 assert!(stacked.is_operator_backed());
6944 assert_eq!(stacked.nrows(), 2);
6945 assert_eq!(stacked.ncols(), 3);
6946
6947 let beta = array![0.25, -0.5, 2.0];
6948 let expected = array![9.25, 10.75];
6949 let got = stacked.dot(&beta);
6950 for i in 0..expected.len() {
6951 assert!((got[i] - expected[i]).abs() < 1e-12);
6952 }
6953
6954 let chunk = stacked
6955 .try_row_chunk(0..2)
6956 .expect("stacked.try_row_chunk must succeed");
6957 assert_eq!(chunk, array![[1.0, 2.0, 5.0], [3.0, 4.0, 6.0]]);
6958 }
6959
6960 #[test]
6961 #[should_panic(expected = "DesignMatrix::as_dense_cow called on operator-backed design")]
6962 fn design_matrix_as_dense_cow_rejects_operator_backed_designs() {
6963 let design = no_densify_design(array![[1.0, 2.0], [3.0, 4.0]]);
6964 design.as_dense_cow();
6965 }
6966
6967 #[test]
6968 fn sparse_factorized_solve_matches_dense_operator_solve() {
6969 let triplets = vec![
6970 Triplet::new(0usize, 0usize, 1.0),
6971 Triplet::new(1, 0, 2.0),
6972 Triplet::new(1, 1, -1.0),
6973 Triplet::new(2, 1, 3.0),
6974 Triplet::new(2, 2, 0.5),
6975 ];
6976 let sparse = SparseColMat::try_new_from_triplets(3, 3, &triplets)
6977 .expect("sparse design should build");
6978 let sparse_design = DesignMatrix::from(sparse);
6979 let ledger = ledger_read_guard();
6981 assert_eq!(*ledger, (), "ledger read guard is held for this test");
6982 let dense_design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(
6983 sparse_design.to_dense(),
6984 ));
6985 let weights = array![1.5, 0.75, 2.0];
6986 let rhs = array![1.0, -0.5, 2.0];
6987 let penalty = Array2::from_diag(&array![0.25, 0.5, 0.75]);
6988
6989 let sparse_sol = sparse_design
6990 .solve_system(&weights, &rhs, Some(&penalty))
6991 .expect("sparse solve should factorize natively");
6992 let dense_sol = dense_design
6993 .solve_system(&weights, &rhs, Some(&penalty))
6994 .expect("dense solve should factorize");
6995
6996 for i in 0..rhs.len() {
6997 assert!(
6998 (sparse_sol[i] - dense_sol[i]).abs() < 1e-10,
6999 "solution mismatch at {i}: sparse={} dense={}",
7000 sparse_sol[i],
7001 dense_sol[i]
7002 );
7003 }
7004 }
7005
7006 #[test]
7007 fn solve_system_stabilizes_indefinite_penalty_and_returns_finite_solution() {
7008 let design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(array![
7009 [1.0, 0.0],
7010 [0.0, 0.0]
7011 ]));
7012 let weights = array![1.0, 1.0];
7013 let rhs = array![2.0, 0.0];
7014 let penalty = array![[0.0, 0.0], [0.0, -1e-12]];
7015
7016 let beta = design
7017 .solve_system(&weights, &rhs, Some(&penalty))
7018 .expect("solve_system should stabilize indefinite systems");
7019
7020 assert!(beta.iter().all(|v| v.is_finite()));
7021 assert!((beta[0] - 2.0).abs() < 1e-10);
7022 assert!(beta[1].abs() < 1e-8);
7023 }
7024
7025 #[test]
7026 fn explicit_matrix_free_pcg_matches_exact_large_dense_weighted_penalized_solve() {
7027 let n = 48usize;
7028 let p = 520usize;
7029 let mut x = Array2::<f64>::zeros((n, p));
7030 for i in 0..n {
7031 for j in 0..p {
7032 x[[i, j]] = (((i + 3) * (j + 5)) % 17) as f64 / 17.0
7033 + 0.02 * (i as f64)
7034 + 0.001 * (j as f64);
7035 }
7036 }
7037 let design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x.clone()));
7038 let weights = Array1::from_iter((0..n).map(|i| 0.5 + (i as f64) / (2.0 * n as f64)));
7039 let rhs = Array1::from_iter((0..p).map(|j| ((j % 13) as f64 - 6.0) / 13.0));
7040 let penalty = Array2::from_diag(&Array1::from_iter(
7041 (0..p).map(|j| 0.1 + 0.005 * ((j % 7) as f64)),
7042 ));
7043 let ridge = 1e-8;
7044
7045 let pcg = design
7046 .solve_system_matrix_free_pcg(&weights, &rhs, Some(&penalty), ridge)
7047 .expect("matrix-free pcg solve");
7048 let exact = exact_weighted_penalized_solve(&x, &weights, &rhs, &penalty, ridge);
7049 for i in 0..p {
7050 assert!(
7051 (pcg[i] - exact[i]).abs() < 1e-5,
7052 "solution mismatch at {i}: pcg={} exact={}",
7053 pcg[i],
7054 exact[i]
7055 );
7056 }
7057 let mut h = x
7058 .t()
7059 .dot(&(x.clone() * weights.view().insert_axis(Axis(1))));
7060 h += &penalty;
7061 for i in 0..p {
7062 h[[i, i]] += ridge;
7063 }
7064 let residual = h.dot(&pcg) - &rhs;
7065 let residual_norm = residual.dot(&residual).sqrt();
7066 let rhs_norm = rhs.dot(&rhs).sqrt();
7076 let residual_tol = 1e-5 * (1.0 + rhs_norm);
7077 assert!(
7078 residual_norm < residual_tol,
7079 "residual_norm={residual_norm} exceeds tol={residual_tol} (rhs_norm={rhs_norm})"
7080 );
7081 }
7082
7083 #[test]
7084 fn policy_solve_matches_explicit_matrix_free_pcg_on_large_dense_system() {
7085 let n = 40usize;
7086 let p = 520usize;
7087 let mut x = Array2::<f64>::zeros((n, p));
7088 for i in 0..n {
7089 for j in 0..p {
7090 x[[i, j]] = (((2 * i + j + 11) % 23) as f64 / 23.0) + 0.0005 * (j as f64);
7091 }
7092 }
7093 let design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x));
7094 let weights = Array1::from_iter((0..n).map(|i| 1.0 + 0.01 * i as f64));
7095 let rhs = Array1::from_iter((0..p).map(|j| ((j % 5) as f64) - 2.0));
7096 let penalty = Array2::from_diag(&Array1::from_iter(
7097 (0..p).map(|j| 0.2 + 0.01 * ((j % 3) as f64)),
7098 ));
7099 let ridge_floor = 1e-8;
7100
7101 let explicit = design
7102 .solve_system_matrix_free_pcg(&weights, &rhs, Some(&penalty), ridge_floor)
7103 .expect("explicit pcg");
7104 let policy = design
7105 .solve_systemwith_policy(
7106 &weights,
7107 &rhs,
7108 Some(&penalty),
7109 ridge_floor,
7110 RidgePolicy::solver_only(),
7111 )
7112 .expect("policy solve");
7113 for i in 0..p {
7114 let tol = 1e-5 * (1.0 + explicit[i].abs());
7123 assert!(
7124 (explicit[i] - policy[i]).abs() < tol,
7125 "policy mismatch at {i}: explicit={} policy={} (tol={tol})",
7126 explicit[i],
7127 policy[i]
7128 );
7129 }
7130 }
7131
7132 #[test]
7133 fn explicit_matrix_free_pcg_reports_convergence_diagnostics() {
7134 let n = 36usize;
7135 let p = 2160usize;
7136 let mut x = Array2::<f64>::zeros((n, p));
7137 for i in 0..n {
7138 for j in 0..p {
7139 x[[i, j]] = (((3 * i + 5 * j + 7) % 29) as f64 / 29.0)
7140 + 0.015 * (i as f64)
7141 + 1e-4 * j as f64;
7142 }
7143 }
7144 let design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x.clone()));
7145 assert!(design.should_use_matrix_free_pcg());
7146 let weights = Array1::from_iter((0..n).map(|i| 0.75 + 0.01 * i as f64));
7147 let rhs = Array1::from_iter((0..p).map(|j| ((j % 9) as f64 - 4.0) / 9.0));
7148 let penalty = Array2::from_diag(&Array1::from_iter(
7149 (0..p).map(|j| 0.05 + 0.002 * ((j % 11) as f64)),
7150 ));
7151 let ridge = 1e-8;
7152
7153 let (pcg, info): (Array1<f64>, PcgSolveInfo) = design
7154 .solve_system_matrix_free_pcg_with_info(&weights, &rhs, Some(&penalty), ridge)
7155 .expect("pcg with info");
7156 assert!(info.converged);
7157 assert!(info.iterations > 0);
7158 assert!(info.relative_residual_norm.is_finite());
7159 assert!(info.relative_residual_norm < 1e-6);
7160
7161 let exact = exact_weighted_penalized_solve(&x, &weights, &rhs, &penalty, ridge);
7162 for i in 0..p {
7163 assert!(
7164 (pcg[i] - exact[i]).abs() < 1e-5,
7165 "solution mismatch at {i}: pcg={} exact={}",
7166 pcg[i],
7167 exact[i]
7168 );
7169 }
7170 }
7171
7172 #[test]
7173 fn compute_xtwy_dense_allocationfree_matches_matvec() {
7174 let n = 2_000usize;
7175 let p = 64usize;
7176 let mut x = Array2::<f64>::zeros((n, p));
7177 let mut y = Array1::<f64>::zeros(n);
7178 let mut w = Array1::<f64>::zeros(n);
7179 for i in 0..n {
7180 y[i] = ((i % 17) as f64 - 8.0) * 0.1;
7181 w[i] = 0.25 + ((i % 11) as f64) * 0.05;
7182 for j in 0..p {
7183 x[[i, j]] = (((i * 13 + j * 7) % 97) as f64) / 97.0;
7184 }
7185 }
7186
7187 let reference = {
7188 let wy = Array1::from_shape_fn(n, |i| y[i] * w[i]);
7189 fast_atv(&x, &wy)
7190 };
7191 let fused = dense_transpose_weighted_response(&x, &w, &y, None);
7192 for j in 0..p {
7193 assert!(
7194 (reference[j] - fused[j]).abs() < 1e-10,
7195 "mismatch at column {j}: ref={} fused={}",
7196 reference[j],
7197 fused[j]
7198 );
7199 }
7200 }
7201
7202 #[test]
7203 fn large_lazy_dense_materialization_streams_chunks_without_to_dense_fallback() {
7204 let n = 11_000usize;
7205 let p = 128usize;
7206 let op = Arc::new(ChunkOnlyOperator {
7207 n,
7208 p,
7209 row_chunk_calls: AtomicUsize::new(0),
7210 materialization_policy: None,
7211 });
7212 let design = DenseDesignMatrix::from(Arc::clone(&op));
7213
7214 let ledger = ledger_read_guard();
7219 assert_eq!(*ledger, (), "ledger read guard is held for this test");
7220 let dense = design.to_dense_arc();
7221
7222 assert_eq!(dense.dim(), (n, p));
7223 assert!(
7224 op.row_chunk_calls.load(Ordering::SeqCst) > 1,
7225 "expected dense materialization to stream more than one row chunk"
7226 );
7227 for &(i, j) in &[(0, 0), (8_191, 127), (8_192, 0), (10_999, 64)] {
7228 assert_eq!(dense[[i, j]], op.value(i, j));
7229 }
7230 assert!(
7231 design.as_dense_ref().is_some(),
7232 "as_dense_ref must expose a populated LazyDense memo so storage certificates can observe it"
7233 );
7234 }
7235
7236 #[test]
7237 fn construction_policy_survives_nested_coefficient_and_block_operators() {
7238 let strict = ResourcePolicy::analytic_operator_required().material_policy();
7239 let op = Arc::new(ChunkOnlyOperator {
7240 n: 32,
7241 p: 2,
7242 row_chunk_calls: AtomicUsize::new(0),
7243 materialization_policy: Some(strict),
7244 });
7245 let inner = DenseDesignMatrix::from(Arc::clone(&op));
7246 let transformed = CoefficientTransformOperator::new(inner, Array2::<f64>::eye(2))
7247 .expect("coefficient transform");
7248 let block = BlockDesignOperator::new(vec![DesignBlock::Dense(DenseDesignMatrix::from(
7249 Arc::new(transformed),
7250 ))])
7251 .expect("block design");
7252 let design = DenseDesignMatrix::from(Arc::new(block));
7253
7254 let error = design
7255 .try_to_dense_arc("nested construction-policy regression")
7256 .expect_err("strict construction policy must survive every wrapper");
7257 assert!(error.contains("construction policy requires streamed storage"));
7258 assert_eq!(op.row_chunk_calls.load(Ordering::SeqCst), 0);
7259 assert!(design.as_dense_ref().is_none());
7260 }
7261
7262 #[test]
7265 fn governed_to_dense_reserves_and_policy_refusals_are_typed() {
7266 let op = Arc::new(ChunkOnlyOperator {
7267 n: 128,
7268 p: 4,
7269 row_chunk_calls: AtomicUsize::new(0),
7270 materialization_policy: None,
7271 });
7272 let design = DesignMatrix::Dense(DenseDesignMatrix::from(Arc::clone(&op)));
7273
7274 let ledger = ledger_read_guard();
7278 assert_eq!(*ledger, (), "ledger read guard is held for this test");
7279 let dense = design
7280 .try_to_dense_governed("governed dense regression")
7281 .expect("small governed materialization");
7282 assert_eq!(dense.dim(), (128, 4));
7283 assert_eq!(dense.reserved_bytes(), 128 * 4 * std::mem::size_of::<f64>());
7284
7285 let strict = ResourcePolicy::analytic_operator_required().material_policy();
7286 let err = design
7287 .try_to_dense_governed_with_policy(&strict, "regression strict refuses")
7288 .expect_err("strict policy must refuse lazy materialization");
7289 assert!(matches!(err, MatrixMaterializationError::Forbidden { .. }));
7290
7291 let mut tight = ResourcePolicy::default_library().material_policy();
7292 tight.max_single_dense_bytes = 1;
7293 let size_err = design
7294 .try_to_dense_governed_with_policy(&tight, "regression tight refuses")
7295 .expect_err("undersized cap must refuse lazy materialization");
7296 assert!(matches!(
7297 size_err,
7298 MatrixMaterializationError::TooLarge { .. }
7299 ));
7300 }
7301
7302 #[test]
7303 fn try_to_dense_by_chunks_writes_directly_into_output_slices() {
7304 let n = 11_000usize;
7305 let p = 128usize;
7306 let op = Arc::new(ChunkOnlyOperator {
7307 n,
7308 p,
7309 row_chunk_calls: AtomicUsize::new(0),
7310 materialization_policy: None,
7311 });
7312 let design = DesignMatrix::Dense(DenseDesignMatrix::from(Arc::clone(&op)));
7313
7314 let dense = design
7315 .try_to_dense_by_chunks("large chunked regression")
7316 .expect("chunked materialization");
7317
7318 assert_eq!(dense.dim(), (n, p));
7319 assert!(
7320 op.row_chunk_calls.load(Ordering::SeqCst) > 1,
7321 "expected direct chunked conversion to use bounded row chunks"
7322 );
7323 for &(i, j) in &[(1, 7), (4_096, 12), (8_193, 63), (10_998, 127)] {
7324 assert_eq!(dense[[i, j]], op.value(i, j));
7325 }
7326 }
7327
7328 #[test]
7329 fn tensor_product_design_operator_matches_dense_2d() {
7330 use super::{DenseDesignOperator, TensorProductDesignOperator};
7331
7332 let n = 10;
7334 let q1 = 4;
7335 let q2 = 3;
7336 let mut b1 = Array2::<f64>::zeros((n, q1));
7337 let mut b2 = Array2::<f64>::zeros((n, q2));
7338 for i in 0..n {
7340 let t1 = i as f64 / (n - 1) as f64 * (q1 - 1) as f64;
7341 let j1 = (t1.floor() as usize).min(q1 - 2);
7342 let frac1 = t1 - j1 as f64;
7343 b1[[i, j1]] = 1.0 - frac1;
7344 b1[[i, j1 + 1]] = frac1;
7345
7346 let t2 = i as f64 / (n - 1) as f64 * (q2 - 1) as f64;
7347 let j2 = (t2.floor() as usize).min(q2 - 2);
7348 let frac2 = t2 - j2 as f64;
7349 b2[[i, j2]] = 1.0 - frac2;
7350 b2[[i, j2 + 1]] = frac2;
7351 }
7352
7353 let op = TensorProductDesignOperator::new(vec![Arc::new(b1.clone()), Arc::new(b2.clone())])
7354 .expect("fixture marginals share a row count");
7355
7356 let p = q1 * q2;
7358 let mut dense = Array2::<f64>::zeros((n, p));
7359 for i in 0..n {
7360 for j1 in 0..q1 {
7361 for j2 in 0..q2 {
7362 dense[[i, j1 * q2 + j2]] = b1[[i, j1]] * b2[[i, j2]];
7363 }
7364 }
7365 }
7366
7367 let op_dense = op.to_dense();
7369 let max_diff = (&op_dense - &dense)
7370 .iter()
7371 .map(|v: &f64| v.abs())
7372 .fold(0.0f64, f64::max);
7373 assert!(max_diff < 1e-14, "to_dense mismatch: max_diff={max_diff}");
7374
7375 let beta = Array1::from_vec((0..p).map(|j| (j as f64 + 1.0) * 0.1).collect());
7377 let ref_result = dense.dot(&beta);
7378 let op_result = op.apply(&beta);
7379 let max_diff = (&op_result - &ref_result)
7380 .iter()
7381 .map(|v: &f64| v.abs())
7382 .fold(0.0f64, f64::max);
7383 assert!(max_diff < 1e-12, "apply mismatch: max_diff={max_diff}");
7384
7385 let v = Array1::from_vec((0..n).map(|i| (i as f64 + 1.0) * 0.3).collect());
7387 let ref_xt_v = dense.t().dot(&v);
7388 let op_xt_v = op.apply_transpose(&v);
7389 let max_diff = (&op_xt_v - &ref_xt_v)
7390 .iter()
7391 .map(|v: &f64| v.abs())
7392 .fold(0.0f64, f64::max);
7393 assert!(
7394 max_diff < 1e-12,
7395 "apply_transpose mismatch: max_diff={max_diff}"
7396 );
7397
7398 let w = Array1::from_vec((0..n).map(|i| 1.0 + i as f64 * 0.1).collect());
7400 let ref_xtwx = {
7401 let mut out = Array2::<f64>::zeros((p, p));
7402 for i in 0..n {
7403 for a in 0..p {
7404 for b in 0..p {
7405 out[[a, b]] += w[i] * dense[[i, a]] * dense[[i, b]];
7406 }
7407 }
7408 }
7409 out
7410 };
7411 let op_xtwx = op
7412 .diag_xtw_x(&w)
7413 .expect("fixture design and weight vector share n rows");
7414 let max_diff = (&op_xtwx - &ref_xtwx)
7415 .iter()
7416 .map(|v: &f64| v.abs())
7417 .fold(0.0f64, f64::max);
7418 assert!(max_diff < 1e-10, "diag_xtw_x mismatch: max_diff={max_diff}");
7419 }
7420
7421 #[test]
7422 fn tensor_product_design_operator_3d() {
7423 use super::{DenseDesignOperator, TensorProductDesignOperator};
7424
7425 let n = 8;
7426 let dims = [3, 2, 2];
7427 let mut marginals: Vec<Array2<f64>> = Vec::new();
7428 for &q in &dims {
7429 let mut b = Array2::<f64>::zeros((n, q));
7430 for i in 0..n {
7431 let t = i as f64 / (n - 1) as f64 * (q - 1) as f64;
7432 let j = (t.floor() as usize).min(q - 2);
7433 let frac = t - j as f64;
7434 b[[i, j]] = 1.0 - frac;
7435 b[[i, j + 1]] = frac;
7436 }
7437 marginals.push(b);
7438 }
7439
7440 let op = TensorProductDesignOperator::new(
7441 marginals.iter().map(|m| Arc::new(m.clone())).collect(),
7442 )
7443 .expect("fixture marginals share a row count");
7444
7445 let p: usize = dims.iter().copied().product();
7447 let mut dense = Array2::<f64>::zeros((n, p));
7448 for i in 0..n {
7449 for j0 in 0..dims[0] {
7450 for j1 in 0..dims[1] {
7451 for j2 in 0..dims[2] {
7452 let col = j0 * dims[1] * dims[2] + j1 * dims[2] + j2;
7453 dense[[i, col]] =
7454 marginals[0][[i, j0]] * marginals[1][[i, j1]] * marginals[2][[i, j2]];
7455 }
7456 }
7457 }
7458 }
7459
7460 let op_dense = op.to_dense();
7461 let max_diff = (&op_dense - &dense)
7462 .iter()
7463 .map(|v: &f64| v.abs())
7464 .fold(0.0f64, f64::max);
7465 assert!(
7466 max_diff < 1e-14,
7467 "3D to_dense mismatch: max_diff={max_diff}"
7468 );
7469
7470 let beta = Array1::from_vec((0..p).map(|j| (j as f64).sin()).collect());
7472 let xb = op.apply(&beta);
7473 let xtxb = op.apply_transpose(&xb);
7474 let ref_xtxb = dense.t().dot(&dense.dot(&beta));
7475 let max_diff = (&xtxb - &ref_xtxb)
7476 .iter()
7477 .map(|v: &f64| v.abs())
7478 .fold(0.0f64, f64::max);
7479 assert!(max_diff < 1e-10, "3D X'Xβ mismatch: max_diff={max_diff}");
7480 }
7481
7482 #[test]
7483 fn sparse_weighted_crossprod_parallel_path_matches_dense_reference() {
7484 use faer::sparse::Triplet;
7485
7486 let n = 4096;
7487 let p = 192;
7488 let mut triplets = Vec::with_capacity(n * 4);
7489 let mut dense = Array2::<f64>::zeros((n, p));
7490 for i in 0..n {
7491 let base = (i * 37) % p;
7492 for k in 0..4 {
7493 let col = (base + k * 11) % p;
7494 let val = ((i + 3 * k + 1) as f64).sin() * 0.25 + 0.5;
7495 triplets.push(Triplet::new(i, col, val));
7496 dense[[i, col]] = val;
7497 }
7498 }
7499 let sparse = faer::sparse::SparseColMat::try_new_from_triplets(n, p, &triplets)
7500 .expect("fixture triplets lie inside the declared shape");
7501 let design = DesignMatrix::Sparse(SparseDesignMatrix::new(sparse));
7502 let weights = Array1::from_iter((0..n).map(|i| match i % 7 {
7503 0 => 0.0,
7504 r => 0.5 + r as f64 * 0.125,
7505 }));
7506
7507 let got = <DesignMatrix as LinearOperator>::xt_diag_x_signed_op(
7508 &design,
7509 FiniteSignedWeightsView::try_from_array(&weights)
7510 .expect("fixture weights are finite and signed-representable"),
7511 )
7512 .expect("fixture design and weight vector share n rows");
7513 let mut reference = Array2::<f64>::zeros((p, p));
7514 for i in 0..n {
7515 let wi = weights[i];
7516 if wi == 0.0 {
7517 continue;
7518 }
7519 for a in 0..p {
7520 let xa = dense[[i, a]];
7521 if xa == 0.0 {
7522 continue;
7523 }
7524 for b in 0..p {
7525 reference[[a, b]] += wi * xa * dense[[i, b]];
7526 }
7527 }
7528 }
7529 let max_diff = (&got - &reference)
7530 .iter()
7531 .map(|v: &f64| v.abs())
7532 .fold(0.0_f64, f64::max);
7533 assert!(
7534 max_diff < 1e-10,
7535 "sparse xtwx mismatch: max_diff={max_diff}"
7536 );
7537
7538 let got_diag = design
7539 .diag_gram(&weights)
7540 .expect("fixture design and weight vector share n rows");
7541 let ref_diag = reference.diag().to_owned();
7542 let max_diag_diff = (&got_diag - &ref_diag)
7543 .iter()
7544 .map(|v: &f64| v.abs())
7545 .fold(0.0_f64, f64::max);
7546 assert!(
7547 max_diag_diff < 1e-10,
7548 "sparse diag gram mismatch: max_diff={max_diag_diff}"
7549 );
7550 }
7551
7552 #[test]
7553 fn rowwise_kronecker_sparse_structured_xtwx_matches_dense_reference() {
7554 use faer::sparse::Triplet;
7555
7556 let n = 2048;
7557 let p_cov = 64;
7558 let p_time = 6;
7559 let mut triplets = Vec::with_capacity(n * 3);
7560 let mut cov_dense = Array2::<f64>::zeros((n, p_cov));
7561 for i in 0..n {
7562 let base = (i * 17) % p_cov;
7563 for k in 0..3 {
7564 let col = (base + k * 7) % p_cov;
7565 let val = 0.2 + (((i + k) % 13) as f64) / 17.0;
7566 triplets.push(Triplet::new(i, col, val));
7567 cov_dense[[i, col]] = val;
7568 }
7569 }
7570 let cov_sparse = faer::sparse::SparseColMat::try_new_from_triplets(n, p_cov, &triplets)
7571 .expect("fixture triplets lie inside the declared shape");
7572 let cov = DesignMatrix::Sparse(SparseDesignMatrix::new(cov_sparse));
7573 let mut time = Array2::<f64>::zeros((n, p_time));
7574 for i in 0..n {
7575 for t in 0..p_time {
7576 time[[i, t]] = (((i + 1) * (t + 3)) as f64).cos() * 0.1 + 0.4;
7577 }
7578 }
7579 let op = RowwiseKroneckerOperator::new(cov, Arc::new(time.clone()))
7580 .expect("covariate and time marginals share n rows");
7581 let weights = Array1::from_iter((0..n).map(|i| 0.25 + ((i % 11) as f64) * 0.05));
7582 let got = op
7583 .diag_xtw_x(&weights)
7584 .expect("fixture design and weight vector share n rows");
7585
7586 let p_total = p_cov * p_time;
7587 let mut reference = Array2::<f64>::zeros((p_total, p_total));
7588 for i in 0..n {
7589 for c1 in 0..p_cov {
7590 let x1 = cov_dense[[i, c1]];
7591 if x1 == 0.0 {
7592 continue;
7593 }
7594 for t1 in 0..p_time {
7595 let a = c1 * p_time + t1;
7596 let xa = x1 * time[[i, t1]];
7597 for c2 in 0..p_cov {
7598 let x2 = cov_dense[[i, c2]];
7599 if x2 == 0.0 {
7600 continue;
7601 }
7602 for t2 in 0..p_time {
7603 let b = c2 * p_time + t2;
7604 reference[[a, b]] += weights[i] * xa * x2 * time[[i, t2]];
7605 }
7606 }
7607 }
7608 }
7609 }
7610 let max_diff = (&got - &reference)
7611 .iter()
7612 .map(|v: &f64| v.abs())
7613 .fold(0.0_f64, f64::max);
7614 assert!(
7615 max_diff < 1e-9,
7616 "rowwise kronecker sparse xtwx mismatch: max_diff={max_diff}"
7617 );
7618 }
7619
7620 #[test]
7621 fn embedded_column_block_zero_row_local_materializes_empty_global_width() {
7622 let local = Array2::<f64>::zeros((0, 0));
7623 let out = EmbeddedColumnBlock::new(&local, 2..5, 7).materialize();
7624 assert_eq!(out.dim(), (0, 7));
7625 }
7626
7627 #[test]
7641 fn densification_refusal_names_both_operands_not_just_the_size() {
7642 let starved = ResourcePolicy {
7646 max_single_materialization_bytes: 0,
7647 ..ResourcePolicy::default_library()
7648 };
7649 let reason = super::panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
7650 "unit_test", 300, 12, &starved,
7651 )
7652 .expect_err("a zero cap must refuse a 28,800-byte design");
7653
7654 assert!(
7658 reason.contains("28800"),
7659 "refusal must state the exact size in bytes: {reason}"
7660 );
7661 assert!(
7662 reason.contains("cap"),
7663 "refusal must name the cap it compared against: {reason}"
7664 );
7665
7666 let roomy = ResourcePolicy {
7671 max_single_materialization_bytes: 1 << 30,
7672 ..ResourcePolicy::default_library()
7673 };
7674 super::panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
7675 "unit_test", 300, 12, &roomy,
7676 )
7677 .expect("a 28,800-byte design must be admitted under a 1 GiB cap");
7678
7679 super::panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
7684 "unit_test",
7685 300,
7686 12,
7687 &ResourcePolicy {
7688 max_single_materialization_bytes: 28_799,
7689 ..ResourcePolicy::default_library()
7690 },
7691 )
7692 .expect_err("one byte below the request must refuse");
7693 super::panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
7694 "unit_test",
7695 300,
7696 12,
7697 &ResourcePolicy {
7698 max_single_materialization_bytes: 28_800,
7699 ..ResourcePolicy::default_library()
7700 },
7701 )
7702 .expect("exactly the request must be admitted");
7703 }
7704}