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 = 8 * 1024 * 1024;
32const OPERATOR_ROW_CHUNK_SIZE: usize = 256;
33const DENSE_ROW_PARALLEL_MIN_NP: u64 = 200_000;
37const WEIGHTED_CROSSPROD_PARALLEL_MIN_FLOPS: u64 = 500_000;
38const SPARSE_ROW_PARALLEL_MIN_FLOPS: u64 = 100_000;
39const TENSOR_GEMM_MAX_INTERMEDIATE_BYTES: usize = 128 * 1024 * 1024; pub use crate::utils::PcgSolveInfo;
44
45mod sparse_hessian;
46pub use sparse_hessian::{SparseHessianAccumulator, SparseHessianSymbolic};
47
48mod weights;
49pub use weights::{FiniteSignedWeightsView, PsdWeightsView, SignedWeightsArc, SignedWeightsView};
50
51#[derive(Debug, Clone)]
56pub enum MatrixError {
57 DimensionMismatch { reason: String },
60 DensificationRefused { reason: String },
64}
65
66crate::impl_reason_error_boilerplate! {
67 MatrixError {
68 DimensionMismatch,
69 DensificationRefused,
70 }
71}
72
73#[inline]
74fn dense_materialization_chunk_rows(nrows: usize, ncols: usize) -> usize {
75 rows_for_target_bytes(CHUNKED_DENSE_MATERIALIZATION_BYTES, ncols)
76 .max(1)
77 .min(nrows.max(1))
78}
79
80fn dense_operator_to_dense_by_chunks<O: DenseDesignOperator + ?Sized>(
81 op: &O,
82) -> Result<Array2<f64>, MatrixMaterializationError> {
83 let n = op.nrows();
84 let p = op.ncols();
85 let chunk_rows = dense_materialization_chunk_rows(n, p);
86 let mut out = Array2::<f64>::zeros((n, p));
87 for start in (0..n).step_by(chunk_rows) {
88 let end = (start + chunk_rows).min(n);
89 let slice = out.slice_mut(s![start..end, ..]);
90 op.row_chunk_into(start..end, slice)?;
91 }
92 Ok(out)
93}
94
95fn governed_dense_operator_to_dense_by_chunks<O: DenseDesignOperator + ?Sized>(
98 op: &O,
99 policy: &MaterializationPolicy,
100 context: &'static str,
101) -> Result<Governed<Array2<f64>>, MatrixMaterializationError> {
102 let effective_policy =
103 merge_operator_materialization_policies(Some(policy.clone()), op.materialization_policy())
104 .expect("caller policy is always present");
105 if !effective_policy.allow_operator_materialization {
106 crate::governed_capture::record_governed_decision(
107 context,
108 op.nrows(),
109 op.ncols(),
110 None,
111 crate::governed_capture::GovernedArm::Ineligible,
112 );
113 return Err(MatrixMaterializationError::Forbidden {
114 context,
115 mode: gam_runtime::resource::DerivativeStorageMode::AnalyticOperatorRequired,
116 });
117 }
118 let bytes = dense_f64_bytes(op.nrows(), op.ncols()).unwrap_or(usize::MAX);
119 if bytes > effective_policy.max_single_dense_bytes {
120 crate::governed_capture::record_governed_decision(
121 context,
122 op.nrows(),
123 op.ncols(),
124 Some(bytes),
125 crate::governed_capture::GovernedArm::Ineligible,
126 );
127 return Err(MatrixMaterializationError::TooLarge {
128 context,
129 nrows: op.nrows(),
130 ncols: op.ncols(),
131 bytes,
132 limit_bytes: effective_policy.max_single_dense_bytes,
133 });
134 }
135 let reservation = match MemoryGovernor::global().try_reserve_dense_f64(
136 op.nrows(),
137 op.ncols(),
138 context,
139 ) {
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);
201 return Err(MatrixError::DensificationRefused {
202 reason: format!(
203 "{context}: refusing to densify operator-backed design {n}x{p} (~{gib:.2} GiB); use matrix-free or chunked code"
204 ),
205 }
206 .into());
207 }
208 Ok(())
209}
210
211fn merge_operator_materialization_policies(
212 left: Option<MaterializationPolicy>,
213 right: Option<MaterializationPolicy>,
214) -> Option<MaterializationPolicy> {
215 match (left, right) {
216 (None, policy) | (policy, None) => policy,
217 (Some(left), Some(right)) => Some(MaterializationPolicy {
218 max_single_dense_bytes: left
219 .max_single_dense_bytes
220 .min(right.max_single_dense_bytes),
221 max_cached_dense_bytes: left
222 .max_cached_dense_bytes
223 .min(right.max_cached_dense_bytes),
224 row_chunk_target_bytes: left
225 .row_chunk_target_bytes
226 .min(right.row_chunk_target_bytes),
227 allow_operator_materialization: left.allow_operator_materialization
228 && right.allow_operator_materialization,
229 allow_diagnostic_materialization: left.allow_diagnostic_materialization
230 && right.allow_diagnostic_materialization,
231 }),
232 }
233}
234
235fn enforce_operator_materialization_policy(
236 op: &dyn DenseDesignOperator,
237 context: &str,
238) -> Result<(), String> {
239 let Some(policy) = op.materialization_policy() else {
240 return Ok(());
241 };
242 if !policy.allow_operator_materialization {
243 return Err(MatrixError::DensificationRefused {
244 reason: format!(
245 "{context}: refusing to densify {}x{} operator-backed design because its \
246 construction policy requires streamed storage",
247 op.nrows(),
248 op.ncols(),
249 ),
250 }
251 .into());
252 }
253 let bytes = checked_dense_nbytes(op.nrows(), op.ncols(), context)?;
254 if bytes > policy.max_single_dense_bytes {
255 return Err(MatrixError::DensificationRefused {
256 reason: format!(
257 "{context}: refusing to densify {}x{} operator-backed design ({bytes} bytes); \
258 its construction-policy limit is {} bytes",
259 op.nrows(),
260 op.ncols(),
261 policy.max_single_dense_bytes,
262 ),
263 }
264 .into());
265 }
266 Ok(())
267}
268
269#[inline]
274fn certify_signed_weights<'a>(
275 context: &str,
276 weights: &'a Array1<f64>,
277 expected_len: usize,
278) -> Result<FiniteSignedWeightsView<'a>, String> {
279 if weights.len() != expected_len {
280 return Err(MatrixError::DimensionMismatch {
281 reason: format!(
282 "{context} weight length mismatch: weights={}, nrows={expected_len}",
283 weights.len()
284 ),
285 }
286 .into());
287 }
288 FiniteSignedWeightsView::try_from_array(weights)
289 .map_err(|reason| format!("{context}: {reason}"))
290}
291
292fn weighted_crossprod_dense(
293 left: &Array2<f64>,
294 weights: &Array1<f64>,
295 right: &Array2<f64>,
296) -> Result<Array2<f64>, String> {
297 if left.nrows() != weights.len() || right.nrows() != weights.len() {
298 return Err(MatrixError::DimensionMismatch {
299 reason: format!(
300 "weighted_crossprod_dense row mismatch: left={}, weights={}, right={}",
301 left.nrows(),
302 weights.len(),
303 right.nrows()
304 ),
305 }
306 .into());
307 }
308 certify_signed_weights("weighted_crossprod_dense", weights, left.nrows())?;
309 Ok(weighted_crossprod_dense_view(left, weights.view(), right))
310}
311
312fn weighted_crossprod_dense_view(
313 left: &Array2<f64>,
314 weights: ArrayView1<'_, f64>,
315 right: &Array2<f64>,
316) -> Array2<f64> {
317 let n = weights.len();
318 let p_left = left.ncols();
319 let p_right = right.ncols();
320 let work = (n as u64)
321 .saturating_mul(p_left as u64)
322 .saturating_mul(p_right as u64);
323 if rayon::current_num_threads() <= 1 || work < WEIGHTED_CROSSPROD_PARALLEL_MIN_FLOPS {
324 return weighted_crossprod_dense_rows(left, weights, right, 0..n);
325 }
326
327 let min_parallel_work = WEIGHTED_CROSSPROD_PARALLEL_MIN_FLOPS.min(usize::MAX as u64) as usize;
328 let Some(chunk_rows) = crate::parallel::row_reduction_chunk_rows(
329 n,
330 p_left.saturating_mul(p_right),
331 p_left.saturating_mul(p_right),
332 min_parallel_work,
333 ) else {
334 return weighted_crossprod_dense_rows(left, weights, right, 0..n);
335 };
336 let starts: Vec<usize> = (0..n).step_by(chunk_rows).collect();
337 let partials: Vec<Array2<f64>> = starts
338 .into_par_iter()
339 .map(|start| {
340 weighted_crossprod_dense_rows(left, weights, right, start..(start + chunk_rows).min(n))
341 })
342 .collect();
343 let mut out = Array2::<f64>::zeros((p_left, p_right));
344 for partial in &partials {
345 out += partial;
346 }
347 out
348}
349
350fn weighted_crossprod_dense_rows(
351 left: &Array2<f64>,
352 weights: ArrayView1<'_, f64>,
353 right: &Array2<f64>,
354 rows: Range<usize>,
355) -> Array2<f64> {
356 let p_left = left.ncols();
364 let p_right = right.ncols();
365 let mut out = Array2::<f64>::zeros((p_left, p_right));
366 if left.is_standard_layout()
367 && right.is_standard_layout()
368 && let (Some(lx), Some(rx), Some(w)) =
369 (left.as_slice(), right.as_slice(), weights.as_slice())
370 {
371 let out_slice = out.as_slice_mut().expect("zeros are contiguous");
372 for i in rows {
373 let wi = w[i];
374 if wi == 0.0 {
375 continue;
376 }
377 let l_row = &lx[i * p_left..i * p_left + p_left];
378 let r_row = &rx[i * p_right..i * p_right + p_right];
379 for a in 0..p_left {
380 let scaled = wi * l_row[a];
381 if scaled == 0.0 {
382 continue;
383 }
384 let out_row = &mut out_slice[a * p_right..a * p_right + p_right];
385 for b in 0..p_right {
386 out_row[b] += scaled * r_row[b];
387 }
388 }
389 }
390 return out;
391 }
392 for i in rows {
393 let wi = weights[i];
394 if wi == 0.0 {
395 continue;
396 }
397 for a in 0..p_left {
398 let scaled = wi * left[[i, a]];
399 if scaled == 0.0 {
400 continue;
401 }
402 for b in 0..p_right {
403 out[[a, b]] += scaled * right[[i, b]];
404 }
405 }
406 }
407 out
408}
409
410pub struct DenseRightProductView<'a> {
411 base: &'a Array2<f64>,
412 first: Option<&'a Array2<f64>>,
413 second: Option<&'a Array2<f64>>,
414}
415
416impl<'a> DenseRightProductView<'a> {
417 pub fn new(base: &'a Array2<f64>) -> Self {
418 Self {
419 base,
420 first: None,
421 second: None,
422 }
423 }
424
425 pub fn with_factor(mut self, factor: &'a Array2<f64>) -> Self {
426 if self.first.is_none() {
427 self.first = Some(factor);
428 } else if self.second.is_none() {
429 self.second = Some(factor);
430 } else {
431 std::panic::panic_any("DenseRightProductView supports at most two right factors");
437 }
438 self
439 }
440
441 pub fn with_optional_factor(self, factor: Option<&'a Array2<f64>>) -> Self {
442 match factor {
443 Some(factor) => self.with_factor(factor),
444 None => self,
445 }
446 }
447
448 pub fn materialize(&self) -> Array2<f64> {
449 let mut out = self.base.clone();
450 if let Some(factor) = self.first {
451 out = fast_ab(&out, factor);
452 }
453 if let Some(factor) = self.second {
454 out = fast_ab(&out, factor);
455 }
456 out
457 }
458
459 fn transformed_ncols(&self) -> usize {
460 if let Some(factor) = self.second {
461 factor.ncols()
462 } else if let Some(factor) = self.first {
463 factor.ncols()
464 } else {
465 self.base.ncols()
466 }
467 }
468}
469
470pub struct EmbeddedColumnBlock<'a> {
471 local: &'a Array2<f64>,
472 global_range: Range<usize>,
473 total_cols: usize,
474}
475
476impl<'a> EmbeddedColumnBlock<'a> {
477 pub fn new(local: &'a Array2<f64>, global_range: Range<usize>, total_cols: usize) -> Self {
478 Self {
479 local,
480 global_range,
481 total_cols,
482 }
483 }
484
485 pub fn materialize(&self) -> Array2<f64> {
486 if self.local.nrows() == 0 {
487 return Array2::<f64>::zeros((0, self.total_cols));
488 }
489 assert_eq!(
490 self.local.ncols(),
491 self.global_range.len(),
492 "embedded column block width mismatch"
493 );
494 let mut out = Array2::<f64>::zeros((self.local.nrows(), self.total_cols));
495 out.slice_mut(ndarray::s![.., self.global_range.clone()])
496 .assign(self.local);
497 out
498 }
499}
500
501pub struct EmbeddedSquareBlock<'a> {
502 local: &'a Array2<f64>,
503 global_range: Range<usize>,
504 total_dim: usize,
505}
506
507impl<'a> EmbeddedSquareBlock<'a> {
508 pub fn new(local: &'a Array2<f64>, global_range: Range<usize>, total_dim: usize) -> Self {
509 Self {
510 local,
511 global_range,
512 total_dim,
513 }
514 }
515
516 pub fn materialize(&self) -> Array2<f64> {
517 let mut out = Array2::<f64>::zeros((self.total_dim, self.total_dim));
518 out.slice_mut(ndarray::s![
519 self.global_range.clone(),
520 self.global_range.clone()
521 ])
522 .assign(self.local);
523 out
524 }
525}
526
527struct PenalizedWeightedNormalOperator<'a, O: LinearOperator + ?Sized> {
528 operator: &'a O,
529 weights: &'a Array1<f64>,
530 finite_weights: FiniteSignedWeightsView<'a>,
531 penalty: Option<&'a Array2<f64>>,
532 ridge: f64,
533}
534
535impl<'a, O: LinearOperator + ?Sized> PenalizedWeightedNormalOperator<'a, O> {
536 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
537 self.operator
538 .apply_weighted_normal(self.finite_weights, vector, self.penalty, self.ridge)
539 }
540
541 fn jacobi_preconditioner(&self) -> Result<Array1<f64>, String> {
542 let mut diag = self.operator.diag_gram(self.weights)?;
543 if let Some(pen) = self.penalty {
544 for i in 0..diag.len() {
545 diag[i] += pen[[i, i]];
546 }
547 }
548 if self.ridge > 0.0 {
549 for i in 0..diag.len() {
550 diag[i] += self.ridge;
551 }
552 }
553 Ok(diag)
554 }
555}
556
557#[inline]
558fn dense_diag_gram_view(matrix: &Array2<f64>, weights: ArrayView1<'_, f64>) -> Array1<f64> {
559 let p = matrix.ncols();
563 let n = matrix.nrows();
564 let large = (n as u64) * (p as u64) >= DENSE_ROW_PARALLEL_MIN_NP;
565 let parallel = large && rayon::current_thread_index().is_none();
566 if matrix.is_standard_layout()
569 && let (Some(x), Some(w)) = (matrix.as_slice(), weights.as_slice())
570 {
571 if parallel {
572 return crate::pairwise_reduce::par_deterministic_block_fold(
576 n,
577 |range: core::ops::Range<usize>| {
578 let mut acc = vec![0.0_f64; p];
579 for i in range {
580 let wi = w[i];
581 if wi != 0.0 {
582 let row = &x[i * p..i * p + p];
583 for j in 0..p {
584 let xij = row[j];
585 acc[j] += wi * xij * xij;
586 }
587 }
588 }
589 acc
590 },
591 |mut a, b| {
592 for (av, bv) in a.iter_mut().zip(b) {
593 *av += bv;
594 }
595 a
596 },
597 )
598 .unwrap_or_else(|| vec![0.0_f64; p])
599 .into();
600 }
601 let mut diag = Array1::<f64>::zeros(p);
602 let diag_slice = diag.as_slice_mut().expect("zeros are contiguous");
603 for i in 0..n {
604 let wi = w[i];
605 if wi == 0.0 {
606 continue;
607 }
608 let row = &x[i * p..i * p + p];
609 for j in 0..p {
610 let xij = row[j];
611 diag_slice[j] += wi * xij * xij;
612 }
613 }
614 return diag;
615 }
616 let mut diag = Array1::<f64>::zeros(p);
617 for i in 0..n {
618 let wi = weights[i];
619 if wi == 0.0 {
620 continue;
621 }
622 for j in 0..p {
623 let xij = matrix[[i, j]];
624 diag[j] += wi * xij * xij;
625 }
626 }
627 diag
628}
629
630fn sparse_csr_weighted_xtwx(
631 row_ptr: &[usize],
632 col_idx: &[usize],
633 vals: &[f64],
634 n: usize,
635 p: usize,
636 weights: ArrayView1<'_, f64>,
637) -> Array2<f64> {
638 let nnz = vals.len() as u64;
639 let avg = nnz.checked_div(n.max(1) as u64).unwrap_or(0);
640 let work = (n as u64).saturating_mul(avg.saturating_mul(avg));
641 if rayon::current_num_threads() <= 1 || work < SPARSE_ROW_PARALLEL_MIN_FLOPS {
642 return sparse_csr_weighted_xtwx_rows(row_ptr, col_idx, vals, p, weights, 0..n);
643 }
644
645 let min_parallel_work = SPARSE_ROW_PARALLEL_MIN_FLOPS.min(usize::MAX as u64) as usize;
646 let Some(chunk_rows) = crate::parallel::row_reduction_chunk_rows(
647 n,
648 avg.min(usize::MAX as u64) as usize,
649 p.saturating_mul(p),
650 min_parallel_work,
651 ) else {
652 return sparse_csr_weighted_xtwx_rows(row_ptr, col_idx, vals, p, weights, 0..n);
653 };
654 let starts: Vec<usize> = (0..n).step_by(chunk_rows).collect();
655 let partials: Vec<Array2<f64>> = starts
656 .into_par_iter()
657 .map(|start| {
658 sparse_csr_weighted_xtwx_rows(
659 row_ptr,
660 col_idx,
661 vals,
662 p,
663 weights,
664 start..(start + chunk_rows).min(n),
665 )
666 })
667 .collect();
668 let mut xtwx = Array2::<f64>::zeros((p, p));
669 for partial in &partials {
670 xtwx += partial;
671 }
672 xtwx
673}
674
675fn sparse_csr_weighted_xtwx_rows(
676 row_ptr: &[usize],
677 col_idx: &[usize],
678 vals: &[f64],
679 p: usize,
680 weights: ArrayView1<'_, f64>,
681 rows: Range<usize>,
682) -> Array2<f64> {
683 let mut xtwx = Array2::<f64>::zeros((p, p));
690 for i in rows {
691 let wi = weights[i];
692 if wi == 0.0 {
693 continue;
694 }
695 let start = row_ptr[i];
696 let end = row_ptr[i + 1];
697 for a_ptr in start..end {
698 let a = col_idx[a_ptr];
699 let wxa = wi * vals[a_ptr];
700 for b_ptr in a_ptr..end {
701 let b = col_idx[b_ptr];
702 let v = wxa * vals[b_ptr];
703 xtwx[[a, b]] += v;
704 if a != b {
705 xtwx[[b, a]] += v;
706 }
707 }
708 }
709 }
710 xtwx
711}
712
713pub fn streaming_sparse_csc_xt_diag_x(
714 col_ptr: &[usize],
715 row_idx: &[usize],
716 vals: &[f64],
717 n: usize,
718 p: usize,
719 weights: ArrayView1<'_, f64>,
720 out: &mut Array2<f64>,
721) {
722 if n == 0 || p == 0 {
723 return;
724 }
725
726 let chunk_rows = dense_materialization_chunk_rows(n, p);
727 let par = effective_global_parallelism();
728 let mut x_chunk = Array2::<f64>::zeros((chunk_rows, p).f());
729 let mut wx_chunk = Array2::<f64>::zeros((chunk_rows, p).f());
730
731 {
732 let mut out_view = array2_to_matmut(out);
733
734 for start in (0..n).step_by(chunk_rows) {
735 let rows = (n - start).min(chunk_rows);
736 {
737 let mut x_slice = x_chunk.slice_mut(s![0..rows, ..]);
738 let mut wx_slice = wx_chunk.slice_mut(s![0..rows, ..]);
739 x_slice.fill(0.0);
740 wx_slice.fill(0.0);
741 let end = start + rows;
742 for col in 0..p {
743 let col_start = col_ptr[col];
744 let col_end = col_ptr[col + 1];
745 let rows_for_col = &row_idx[col_start..col_end];
746 let local_start = rows_for_col.partition_point(|&row| row < start);
747 let local_end = rows_for_col.partition_point(|&row| row < end);
748 for local_ptr in local_start..local_end {
749 let ptr = col_start + local_ptr;
750 let row = row_idx[ptr];
751 let local = row - start;
752 let wi = weights[row];
753 let value = vals[ptr];
754 x_slice[[local, col]] += value;
755 wx_slice[[local, col]] += wi * value;
756 }
757 }
758 }
759 let x_slice = x_chunk.slice(s![0..rows, ..]);
760 let wx_slice = wx_chunk.slice(s![0..rows, ..]);
761 let x_view = FaerArrayView::new(&x_slice);
762 let wx_view = FaerArrayView::new(&wx_slice);
763 matmul(
764 out_view.as_mut(),
765 Accum::Add,
766 x_view.as_ref().transpose(),
767 wx_view.as_ref(),
768 1.0,
769 par,
770 );
771 }
772 }
773}
774
775fn sparse_csr_diag_gram(
776 row_ptr: &[usize],
777 col_idx: &[usize],
778 vals: &[f64],
779 n: usize,
780 p: usize,
781 weights: ArrayView1<'_, f64>,
782) -> Array1<f64> {
783 let work = vals.len() as u64;
784 if rayon::current_num_threads() <= 1 || work < SPARSE_ROW_PARALLEL_MIN_FLOPS {
785 return sparse_csr_diag_gram_rows(row_ptr, col_idx, vals, p, weights, 0..n);
786 }
787 let min_parallel_work = SPARSE_ROW_PARALLEL_MIN_FLOPS.min(usize::MAX as u64) as usize;
788 let Some(chunk_rows) = crate::parallel::row_reduction_chunk_rows(n, 1, p, min_parallel_work)
789 else {
790 return sparse_csr_diag_gram_rows(row_ptr, col_idx, vals, p, weights, 0..n);
791 };
792 let starts: Vec<usize> = (0..n).step_by(chunk_rows).collect();
793 let partials: Vec<Array1<f64>> = starts
794 .into_par_iter()
795 .map(|start| {
796 sparse_csr_diag_gram_rows(
797 row_ptr,
798 col_idx,
799 vals,
800 p,
801 weights,
802 start..(start + chunk_rows).min(n),
803 )
804 })
805 .collect();
806 let mut diag = Array1::<f64>::zeros(p);
807 for partial in &partials {
808 diag += partial;
809 }
810 diag
811}
812
813fn sparse_csr_diag_gram_rows(
814 row_ptr: &[usize],
815 col_idx: &[usize],
816 vals: &[f64],
817 p: usize,
818 weights: ArrayView1<'_, f64>,
819 rows: Range<usize>,
820) -> Array1<f64> {
821 let mut diag = Array1::<f64>::zeros(p);
826 for i in rows {
827 let wi = weights[i];
828 if wi == 0.0 {
829 continue;
830 }
831 for idx in row_ptr[i]..row_ptr[i + 1] {
832 let j = col_idx[idx];
833 let xij = vals[idx];
834 diag[j] += wi * xij * xij;
835 }
836 }
837 diag
838}
839
840#[inline]
841fn dense_transpose_weighted_response(
842 matrix: &Array2<f64>,
843 weights: &Array1<f64>,
844 y: &Array1<f64>,
845 row_scale: Option<&Array1<f64>>,
846) -> Array1<f64> {
847 let p = matrix.ncols();
852 let n = matrix.nrows();
853 let mut out = Array1::<f64>::zeros(p);
854 if matrix.is_standard_layout()
855 && let (Some(x), Some(w), Some(yslice)) =
856 (matrix.as_slice(), weights.as_slice(), y.as_slice())
857 {
858 let scale_slice = row_scale.and_then(|s| s.as_slice());
859 let out_slice = out.as_slice_mut().expect("zeros are contiguous");
860 for i in 0..n {
861 let mut scaled = yslice[i] * w[i];
862 if let Some(s) = scale_slice {
863 scaled *= s[i];
864 } else if let Some(scale) = row_scale {
865 scaled *= scale[i];
866 }
867 if scaled == 0.0 {
868 continue;
869 }
870 let row = &x[i * p..i * p + p];
871 for j in 0..p {
872 out_slice[j] += row[j] * scaled;
873 }
874 }
875 return out;
876 }
877 for i in 0..n {
878 let mut scaled = y[i] * weights[i];
879 if let Some(scale) = row_scale {
880 scaled *= scale[i];
881 }
882 if scaled == 0.0 {
883 continue;
884 }
885 for j in 0..p {
886 out[j] += matrix[[i, j]] * scaled;
887 }
888 }
889 out
890}
891
892#[inline]
893fn dense_transpose_weighted_response_view(
894 matrix: &Array2<f64>,
895 weights: ArrayView1<'_, f64>,
896 y: ArrayView1<'_, f64>,
897) -> Array1<f64> {
898 let p = matrix.ncols();
901 let n = matrix.nrows();
902 let mut out = Array1::<f64>::zeros(p);
903 if matrix.is_standard_layout()
904 && let (Some(x), Some(w), Some(yslice)) =
905 (matrix.as_slice(), weights.as_slice(), y.as_slice())
906 {
907 let out_slice = out.as_slice_mut().expect("zeros are contiguous");
908 for i in 0..n {
909 let scaled = yslice[i] * w[i];
910 if scaled == 0.0 {
911 continue;
912 }
913 let row = &x[i * p..i * p + p];
914 for j in 0..p {
915 out_slice[j] += row[j] * scaled;
916 }
917 }
918 return out;
919 }
920 for i in 0..n {
921 let scaled = y[i] * weights[i];
922 if scaled == 0.0 {
923 continue;
924 }
925 for j in 0..p {
926 out[j] += matrix[[i, j]] * scaled;
927 }
928 }
929 out
930}
931
932#[derive(Clone)]
933pub struct SparseDesignMatrix {
934 matrix: SparseColMat<usize, f64>,
935 dense_cache: Arc<OnceLock<(Arc<Array2<f64>>, MemoryReservation)>>,
938 csr_cache: Arc<OnceLock<Arc<SparseRowMat<usize, f64>>>>,
939 hessian_pattern_cache: Arc<OnceLock<Arc<SparseHessianSymbolic>>>,
943}
944
945impl SparseDesignMatrix {
946 pub fn new(matrix: SparseColMat<usize, f64>) -> Self {
947 Self {
948 matrix,
949 dense_cache: Arc::new(OnceLock::new()),
950 csr_cache: Arc::new(OnceLock::new()),
951 hessian_pattern_cache: Arc::new(OnceLock::new()),
952 }
953 }
954
955 pub fn hessian_accumulator_template(&self) -> Option<SparseHessianAccumulator> {
967 if let Some(sym) = self.hessian_pattern_cache.get() {
968 return Some(SparseHessianAccumulator::from_symbolic(Arc::clone(sym)));
969 }
970 let csr = self.to_csr_arc()?;
971 let sym = self.hessian_pattern_cache.get_or_init(|| {
974 SparseHessianAccumulator::build_symbolic(&[&csr], self.matrix.ncols())
975 });
976 Some(SparseHessianAccumulator::from_symbolic(Arc::clone(sym)))
977 }
978
979 fn dense_nbytes(&self) -> Result<usize, String> {
980 self.matrix
981 .nrows()
982 .checked_mul(self.matrix.ncols())
983 .and_then(|cells| cells.checked_mul(std::mem::size_of::<f64>()))
984 .ok_or_else(|| {
985 format!(
986 "dense size overflow for sparse design {}x{}",
987 self.matrix.nrows(),
988 self.matrix.ncols()
989 )
990 })
991 }
992
993 fn materialize_dense_arc(&self) -> Arc<Array2<f64>> {
994 let mut out = Array2::<f64>::zeros((self.matrix.nrows(), self.matrix.ncols()));
995 let (symbolic, values) = self.matrix.parts();
996 let col_ptr = symbolic.col_ptr();
997 let row_idx = symbolic.row_idx();
998 for col in 0..self.matrix.ncols() {
999 let start = col_ptr[col];
1000 let end = col_ptr[col + 1];
1001 for idx in start..end {
1002 out[[row_idx[idx], col]] += values[idx];
1003 }
1004 }
1005 Arc::new(out)
1006 }
1007
1008 pub fn try_to_dense_arc(&self, context: &str) -> Result<Arc<Array2<f64>>, String> {
1009 if let Some((cached, _)) = self.dense_cache.get() {
1010 return Ok(cached.clone());
1011 }
1012 let dense_bytes = self.dense_nbytes()?;
1013 let governor = MemoryGovernor::global();
1014 if dense_bytes > governor.single_materialization_cap_bytes() {
1015 let gib = dense_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
1016 return Err(MatrixError::DensificationRefused {
1017 reason: format!(
1018 "{context}: refusing to densify sparse design {}x{} (~{gib:.2} GiB, over the process memory budget); use sparse or matrix-free code",
1019 self.matrix.nrows(),
1020 self.matrix.ncols(),
1021 ),
1022 }
1023 .into());
1024 }
1025 let reservation = governor.try_reserve(dense_bytes, context).map_err(|err| {
1033 String::from(MatrixError::DensificationRefused {
1034 reason: format!(
1035 "{context}: refusing to densify sparse design {}x{}: {err}",
1036 self.matrix.nrows(),
1037 self.matrix.ncols(),
1038 ),
1039 })
1040 })?;
1041 Ok(self
1042 .dense_cache
1043 .get_or_init(|| (self.materialize_dense_arc(), reservation))
1044 .0
1045 .clone())
1046 }
1047
1048 pub fn try_to_dense_governed(
1053 &self,
1054 context: &str,
1055 ) -> Result<Governed<Arc<Array2<f64>>>, String> {
1056 let governor = MemoryGovernor::global();
1057 let (nrows, ncols) = (self.matrix.nrows(), self.matrix.ncols());
1058 if let Some((cached, _)) = self.dense_cache.get() {
1059 let reservation = governor
1062 .try_reserve(0, context)
1063 .expect("zero-byte reservation cannot exceed any budget");
1064 crate::governed_capture::record_governed_decision(
1065 context,
1066 nrows,
1067 ncols,
1068 Some(0),
1069 crate::governed_capture::GovernedArm::CacheHit,
1070 );
1071 return Ok(reservation.bind(cached.clone()));
1072 }
1073 let dense_bytes = match self.dense_nbytes() {
1074 Ok(bytes) => bytes,
1075 Err(err) => {
1076 crate::governed_capture::record_governed_decision(
1077 context,
1078 nrows,
1079 ncols,
1080 None,
1081 crate::governed_capture::GovernedArm::Ineligible,
1082 );
1083 return Err(err);
1084 }
1085 };
1086 let reservation = match governor.try_reserve(dense_bytes, context) {
1087 Ok(reservation) => reservation,
1088 Err(err) => {
1089 crate::governed_capture::record_governed_decision(
1093 context,
1094 nrows,
1095 ncols,
1096 Some(dense_bytes),
1097 crate::governed_capture::GovernedArm::Refused,
1098 );
1099 return Err(String::from(MatrixError::DensificationRefused {
1100 reason: format!(
1101 "{context}: refusing to densify sparse design {nrows}x{ncols}: {err}"
1102 ),
1103 }));
1104 }
1105 };
1106 crate::governed_capture::record_governed_decision(
1107 context,
1108 nrows,
1109 ncols,
1110 Some(dense_bytes),
1111 crate::governed_capture::GovernedArm::Admitted,
1112 );
1113 Ok(reservation.bind(self.materialize_dense_arc()))
1114 }
1115
1116 pub fn to_dense_arc(&self) -> Arc<Array2<f64>> {
1117 self.try_to_dense_arc("SparseDesignMatrix::to_dense_arc")
1118 .unwrap_or_else(|msg| {
1119 let bt = std::backtrace::Backtrace::force_capture();
1120 std::panic::panic_any(format!("{msg}\nbacktrace:\n{bt}"))
1127 })
1128 }
1129
1130 pub fn to_csr_arc(&self) -> Option<Arc<SparseRowMat<usize, f64>>> {
1131 if let Some(cached) = self.csr_cache.get() {
1132 return Some(cached.clone());
1133 }
1134 let csr = self.matrix.as_ref().to_row_major().ok()?;
1135 let arc = Arc::new(csr);
1136 self.csr_cache.set(arc.clone()).ok();
1137 Some(arc)
1138 }
1139
1140 fn row_chunk_into(
1141 &self,
1142 rows: Range<usize>,
1143 mut out: ArrayViewMut2<'_, f64>,
1144 ) -> Result<(), MatrixMaterializationError> {
1145 if out.nrows() != rows.end - rows.start || out.ncols() != self.ncols() {
1146 return Err(MatrixMaterializationError::MissingRowChunk {
1147 context: "SparseDesignMatrix::row_chunk_into shape mismatch",
1148 });
1149 }
1150 out.fill(0.0);
1151 let csr = self
1152 .to_csr_arc()
1153 .ok_or(MatrixMaterializationError::MissingRowChunk {
1154 context: "SparseDesignMatrix::row_chunk_into: failed to obtain CSR view",
1155 })?;
1156 let symbolic = csr.symbolic();
1157 let row_ptr = symbolic.row_ptr();
1158 let col_idx = symbolic.col_idx();
1159 let values = csr.val();
1160 for (local_row, row) in rows.enumerate() {
1161 for ptr in row_ptr[row]..row_ptr[row + 1] {
1162 out[[local_row, col_idx[ptr]]] = values[ptr];
1163 }
1164 }
1165 Ok(())
1166 }
1167}
1168
1169impl Deref for SparseDesignMatrix {
1170 type Target = SparseColMat<usize, f64>;
1171 fn deref(&self) -> &Self::Target {
1172 &self.matrix
1173 }
1174}
1175
1176impl AsRef<SparseColMat<usize, f64>> for SparseDesignMatrix {
1177 fn as_ref(&self) -> &SparseColMat<usize, f64> {
1178 &self.matrix
1179 }
1180}
1181
1182pub trait DenseDesignOperator: LinearOperator + Send + Sync {
1190 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
1191 let n = self.nrows();
1193 if weights.len() != n || y.len() != n {
1194 return Err(format!(
1195 "DenseDesignOperator::compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
1196 weights.len(),
1197 y.len(),
1198 n
1199 ));
1200 }
1201 certify_signed_weights("DenseDesignOperator::compute_xtwy", weights, n)?;
1202 let mut wy = Array1::<f64>::zeros(n);
1205 ndarray::Zip::from(&mut wy)
1206 .and(weights)
1207 .and(y)
1208 .par_for_each(|o, &w, &yi| *o = w * yi);
1209 Ok(self.apply_transpose(&wy))
1210 }
1211
1212 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
1213 if middle.nrows() != self.ncols() || middle.ncols() != self.ncols() {
1216 return Err(format!(
1217 "DenseDesignOperator::quadratic_form_diag dimension mismatch: {}x{} vs expected {}x{}",
1218 middle.nrows(),
1219 middle.ncols(),
1220 self.ncols(),
1221 self.ncols()
1222 ));
1223 }
1224 let n = self.nrows();
1225 let mut out = Array1::<f64>::zeros(n);
1226 let chunk_size = (8 * 1024 * 1024 / (self.ncols().max(1) * 8 * 2))
1228 .max(16)
1229 .min(n.max(1));
1230 let mut start = 0;
1231 while start < n {
1232 let end = (start + chunk_size).min(n);
1233 let x_chunk = self.try_row_chunk(start..end).map_err(|e| e.to_string())?;
1234 let xm_chunk = fast_ab(&x_chunk, middle);
1235 let mut chunk_out = out.slice_mut(ndarray::s![start..end]);
1236 ndarray::Zip::from(&mut chunk_out)
1237 .and(x_chunk.rows())
1238 .and(xm_chunk.rows())
1239 .par_for_each(|o, xr, xmr| *o = xr.dot(&xmr).max(0.0));
1242 start = end;
1243 }
1244 Ok(out)
1245 }
1246
1247 fn row_chunk_into(
1250 &self,
1251 rows: Range<usize>,
1252 out: ArrayViewMut2<'_, f64>,
1253 ) -> Result<(), MatrixMaterializationError>;
1254
1255 fn try_row_chunk(&self, rows: Range<usize>) -> Result<Array2<f64>, MatrixMaterializationError> {
1258 let mut out = Array2::<f64>::zeros((rows.end - rows.start, self.ncols()));
1259 self.row_chunk_into(rows, out.view_mut())?;
1260 Ok(out)
1261 }
1262
1263 fn as_dense_ref(&self) -> Option<&Array2<f64>> {
1265 None
1266 }
1267
1268 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
1273 None
1274 }
1275
1276 fn apply_columns(&self, cols: &[usize]) -> Array2<f64> {
1284 let n = self.nrows();
1285 let p = self.ncols();
1286 let mut out = Array2::<f64>::zeros((n, cols.len()));
1287 let mut e = Array1::<f64>::zeros(p);
1288 for (k, &j) in cols.iter().enumerate() {
1289 assert!(
1290 j < p,
1291 "DenseDesignOperator::apply_columns: column index {j} out of bounds (ncols={p})"
1292 );
1293 e[j] = 1.0;
1294 let col = self.apply(&e);
1295 e[j] = 0.0;
1296 out.column_mut(k).assign(&col);
1297 }
1298 out
1299 }
1300
1301 fn to_dense(&self) -> Array2<f64>;
1305
1306 fn estimated_dense_bytes(&self) -> usize {
1307 self.nrows()
1308 .saturating_mul(self.ncols())
1309 .saturating_mul(std::mem::size_of::<f64>())
1310 }
1311
1312 fn try_to_dense_with_policy(
1313 &self,
1314 policy: &MaterializationPolicy,
1315 context: &'static str,
1316 ) -> Result<Arc<Array2<f64>>, MatrixMaterializationError> {
1317 let effective_policy = merge_operator_materialization_policies(
1318 Some(policy.clone()),
1319 self.materialization_policy(),
1320 )
1321 .expect("caller policy is always present");
1322 let bytes = self.estimated_dense_bytes();
1323 if !effective_policy.allow_operator_materialization {
1324 return Err(MatrixMaterializationError::Forbidden {
1325 context,
1326 mode: gam_runtime::resource::DerivativeStorageMode::AnalyticOperatorRequired,
1327 });
1328 }
1329 if bytes > effective_policy.max_single_dense_bytes {
1330 return Err(MatrixMaterializationError::TooLarge {
1331 context,
1332 nrows: self.nrows(),
1333 ncols: self.ncols(),
1334 bytes,
1335 limit_bytes: effective_policy.max_single_dense_bytes,
1336 });
1337 }
1338 dense_operator_to_dense_by_chunks(self).map(Arc::new)
1339 }
1340
1341 fn try_to_dense_governed_with_policy(
1345 &self,
1346 policy: &MaterializationPolicy,
1347 context: &'static str,
1348 ) -> Result<Governed<Array2<f64>>, MatrixMaterializationError> {
1349 governed_dense_operator_to_dense_by_chunks(self, policy, context)
1350 }
1351
1352 fn to_dense_arc(&self) -> Arc<Array2<f64>> {
1359 Arc::new(
1360 dense_operator_to_dense_by_chunks(self)
1361 .expect("DenseDesignOperator::to_dense_arc: row-chunk materialization failed"),
1362 )
1363 }
1364}
1365
1366#[derive(Clone)]
1378pub struct LazyDense {
1379 op: Arc<dyn DenseDesignOperator>,
1380 dense_memo: Arc<OnceLock<Governed<Arc<Array2<f64>>>>>,
1381}
1382
1383impl LazyDense {
1384 fn new(op: Arc<dyn DenseDesignOperator>) -> Self {
1385 Self {
1386 op,
1387 dense_memo: Arc::new(OnceLock::new()),
1388 }
1389 }
1390
1391 fn operator_arc_identity(&self) -> usize {
1392 Arc::as_ptr(&self.op) as *const () as usize
1393 }
1394
1395 fn try_governed_dense_arc(&self, context: &str) -> Result<Arc<Array2<f64>>, String> {
1401 enforce_operator_materialization_policy(self.op.as_ref(), context)?;
1402 if let Some(governed) = self.dense_memo.get() {
1403 return Ok(Arc::clone(governed.as_ref()));
1404 }
1405 let reservation = MemoryGovernor::global()
1406 .try_reserve_dense_f64(self.op.nrows(), self.op.ncols(), context)
1407 .map_err(|err| {
1408 format!(
1409 "{context}: refusing to densify {}x{} operator-backed design: {err}",
1410 self.op.nrows(),
1411 self.op.ncols(),
1412 )
1413 })?;
1414 let dense = dense_operator_to_dense_by_chunks(self.op.as_ref()).map_err(|err| {
1415 format!(
1416 "{context}: failed to materialize {}x{} operator-backed design via row chunks: {err}",
1417 self.op.nrows(),
1418 self.op.ncols(),
1419 )
1420 })?;
1421 Ok(Arc::clone(
1424 self.dense_memo
1425 .get_or_init(|| reservation.bind(Arc::new(dense)))
1426 .as_ref(),
1427 ))
1428 }
1429}
1430
1431impl std::ops::Deref for LazyDense {
1432 type Target = Arc<dyn DenseDesignOperator>;
1433
1434 fn deref(&self) -> &Self::Target {
1435 &self.op
1436 }
1437}
1438
1439#[derive(Clone)]
1440pub enum DenseDesignMatrix {
1441 Materialized(Arc<Array2<f64>>),
1442 Lazy(LazyDense),
1443}
1444
1445impl std::fmt::Debug for DenseDesignMatrix {
1446 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1447 match self {
1448 Self::Materialized(matrix) => {
1449 write!(
1450 f,
1451 "DenseDesignMatrix::Materialized({}x{})",
1452 matrix.nrows(),
1453 matrix.ncols()
1454 )
1455 }
1456 Self::Lazy(op) => write!(f, "DenseDesignMatrix::Lazy({}x{})", op.nrows(), op.ncols()),
1457 }
1458 }
1459}
1460
1461impl From<Arc<Array2<f64>>> for DenseDesignMatrix {
1462 fn from(value: Arc<Array2<f64>>) -> Self {
1463 Self::Materialized(value)
1464 }
1465}
1466
1467impl From<Array2<f64>> for DenseDesignMatrix {
1468 fn from(value: Array2<f64>) -> Self {
1469 Self::Materialized(Arc::new(value))
1470 }
1471}
1472
1473impl<T> From<Arc<T>> for DenseDesignMatrix
1474where
1475 T: DenseDesignOperator + 'static,
1476{
1477 fn from(value: Arc<T>) -> Self {
1478 Self::Lazy(LazyDense::new(value))
1479 }
1480}
1481
1482impl DenseDesignMatrix {
1483 pub fn cache_identity(&self) -> usize {
1492 match self {
1493 Self::Materialized(matrix) => Arc::as_ptr(matrix) as *const () as usize,
1494 Self::Lazy(lazy) => lazy.operator_arc_identity(),
1495 }
1496 }
1497
1498 pub fn nrows(&self) -> usize {
1499 match self {
1500 Self::Materialized(matrix) => matrix.nrows(),
1501 Self::Lazy(op) => op.nrows(),
1502 }
1503 }
1504
1505 pub fn ncols(&self) -> usize {
1506 match self {
1507 Self::Materialized(matrix) => matrix.ncols(),
1508 Self::Lazy(op) => op.ncols(),
1509 }
1510 }
1511
1512 pub fn as_dense_ref(&self) -> Option<&Array2<f64>> {
1513 match self {
1514 Self::Materialized(matrix) => Some(matrix.as_ref()),
1515 Self::Lazy(lazy) => lazy
1516 .dense_memo
1517 .get()
1518 .map(|governed| governed.as_ref().as_ref())
1519 .or_else(|| lazy.op.as_dense_ref()),
1520 }
1521 }
1522
1523 pub const fn is_materialized_dense(&self) -> bool {
1524 matches!(self, Self::Materialized(_))
1525 }
1526
1527 pub const fn is_operator_backed(&self) -> bool {
1528 matches!(self, Self::Lazy(_))
1529 }
1530
1531 pub fn to_dense(&self) -> Array2<f64> {
1532 match self {
1533 Self::Materialized(matrix) => matrix.as_ref().clone(),
1534 Self::Lazy(lazy) => {
1535 let policy = ResourcePolicy::default_library();
1536 panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
1537 "DenseDesignMatrix::to_dense",
1538 lazy.nrows(),
1539 lazy.ncols(),
1540 &policy,
1541 )
1542 .unwrap_or_else(|reason| std::panic::panic_any(reason));
1543 enforce_operator_materialization_policy(
1544 lazy.op.as_ref(),
1545 "DenseDesignMatrix::to_dense",
1546 )
1547 .unwrap_or_else(|reason| std::panic::panic_any(reason));
1548 if let Some(governed) = lazy.dense_memo.get() {
1549 return governed.as_ref().as_ref().clone();
1552 }
1553 let construction_charge = MemoryGovernor::global()
1559 .try_reserve_dense_f64(
1560 lazy.nrows(),
1561 lazy.ncols(),
1562 "DenseDesignMatrix::to_dense",
1563 )
1564 .unwrap_or_else(|err| std::panic::panic_any(err.to_string()));
1566 let dense =
1567 dense_operator_to_dense_by_chunks(lazy.op.as_ref()).unwrap_or_else(|err| {
1568 std::panic::panic_any(format!(
1569 "DenseDesignMatrix::to_dense: failed to materialize {}x{} \
1570 operator-backed design via row chunks: {err}",
1571 lazy.nrows(),
1572 lazy.ncols(),
1573 ))
1574 });
1575 drop(construction_charge);
1578 dense
1579 }
1580 }
1581 }
1582
1583 pub fn to_dense_arc(&self) -> Arc<Array2<f64>> {
1584 match self {
1585 Self::Materialized(matrix) => Arc::clone(matrix),
1586 Self::Lazy(lazy) => {
1587 let policy = ResourcePolicy::default_library();
1588 panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
1589 "DenseDesignMatrix::to_dense_arc",
1590 lazy.nrows(),
1591 lazy.ncols(),
1592 &policy,
1593 )
1594 .unwrap_or_else(|reason| std::panic::panic_any(reason));
1595 lazy.try_governed_dense_arc("DenseDesignMatrix::to_dense_arc")
1596 .unwrap_or_else(|msg| std::panic::panic_any(msg))
1598 }
1599 }
1600 }
1601
1602 pub fn try_to_dense_arc(&self, context: &str) -> Result<Arc<Array2<f64>>, String> {
1603 let policy = ResourcePolicy::default_library();
1615 self.try_to_dense_arc_with_policy(context, &policy)
1616 }
1617
1618 pub fn try_to_dense_arc_with_policy(
1630 &self,
1631 context: &str,
1632 policy: &ResourcePolicy,
1633 ) -> Result<Arc<Array2<f64>>, String> {
1634 match self {
1635 Self::Materialized(matrix) => Ok(Arc::clone(matrix)),
1636 Self::Lazy(lazy) => {
1637 panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
1638 context,
1639 lazy.nrows(),
1640 lazy.ncols(),
1641 policy,
1642 )?;
1643 lazy.try_governed_dense_arc(context)
1644 }
1645 }
1646 }
1647
1648 pub fn try_row_chunk(
1649 &self,
1650 rows: Range<usize>,
1651 ) -> Result<Array2<f64>, MatrixMaterializationError> {
1652 match self {
1653 Self::Materialized(matrix) => Ok(matrix.slice(s![rows, ..]).to_owned()),
1654 Self::Lazy(op) => op.try_row_chunk(rows),
1655 }
1656 }
1657
1658 pub fn row_chunk_into(
1659 &self,
1660 rows: Range<usize>,
1661 out: ArrayViewMut2<'_, f64>,
1662 ) -> Result<(), MatrixMaterializationError> {
1663 match self {
1664 Self::Materialized(matrix) => {
1665 let mut out = out;
1666 out.assign(&matrix.slice(s![rows, ..]));
1667 Ok(())
1668 }
1669 Self::Lazy(op) => op.row_chunk_into(rows, out),
1670 }
1671 }
1672}
1673
1674impl LinearOperator for DenseDesignMatrix {
1675 fn nrows(&self) -> usize {
1676 DenseDesignMatrix::nrows(self)
1677 }
1678
1679 fn ncols(&self) -> usize {
1680 DenseDesignMatrix::ncols(self)
1681 }
1682
1683 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
1684 match self {
1685 Self::Materialized(matrix) => fast_av(matrix, vector),
1686 Self::Lazy(op) => op.apply(vector),
1687 }
1688 }
1689
1690 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
1691 match self {
1692 Self::Materialized(matrix) => fast_atv(matrix, vector),
1693 Self::Lazy(op) => op.apply_transpose(vector),
1694 }
1695 }
1696
1697 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
1698 certify_signed_weights("DenseDesignMatrix::diag_xtw_x", weights, self.nrows())?;
1699 match self {
1700 Self::Materialized(matrix) => {
1701 let mut xtwx = Array2::<f64>::zeros((matrix.ncols(), matrix.ncols()));
1702 stream_weighted_crossprod_into(
1703 matrix,
1704 weights,
1705 &mut xtwx,
1706 CrossprodStructure::Full,
1707 CrossprodAccum::Replace,
1708 effective_global_parallelism(),
1709 );
1710 Ok(xtwx)
1711 }
1712 Self::Lazy(op) => op.diag_xtw_x(weights),
1713 }
1714 }
1715
1716 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
1717 certify_signed_weights("DenseDesignMatrix::diag_gram", weights, self.nrows())?;
1720 match self {
1721 Self::Materialized(matrix) => {
1722 let n = matrix.nrows();
1723 let p = matrix.ncols();
1724 if (n as u64) * (p as u64) < DENSE_ROW_PARALLEL_MIN_NP {
1725 let mut diag = Array1::<f64>::zeros(p);
1726 for i in 0..n {
1727 let wi = weights[i];
1728 if wi == 0.0 {
1729 continue;
1730 }
1731 for j in 0..p {
1732 let xij = matrix[[i, j]];
1733 diag[j] += wi * xij * xij;
1734 }
1735 }
1736 return Ok(diag);
1737 }
1738 let diag = crate::pairwise_reduce::par_deterministic_block_fold(
1741 n,
1742 |range: core::ops::Range<usize>| {
1743 let mut acc = Array1::<f64>::zeros(p);
1744 for i in range {
1745 let wi = weights[i];
1746 if wi != 0.0 {
1747 for j in 0..p {
1748 let xij = matrix[[i, j]];
1749 acc[j] += wi * xij * xij;
1750 }
1751 }
1752 }
1753 acc
1754 },
1755 |mut a, b| {
1756 a += &b;
1757 a
1758 },
1759 )
1760 .unwrap_or_else(|| Array1::<f64>::zeros(p));
1761 Ok(diag)
1762 }
1763 Self::Lazy(op) => op.diag_gram(weights),
1764 }
1765 }
1766
1767 fn apply_weighted_normal(
1768 &self,
1769 weights: FiniteSignedWeightsView<'_>,
1770 vector: &Array1<f64>,
1771 penalty: Option<&Array2<f64>>,
1772 ridge: f64,
1773 ) -> Array1<f64> {
1774 assert_eq!(
1775 weights.len(),
1776 self.nrows(),
1777 "DenseDesignMatrix::apply_weighted_normal weight length mismatch"
1778 );
1779 assert_eq!(
1780 vector.len(),
1781 self.ncols(),
1782 "DenseDesignMatrix::apply_weighted_normal vector length mismatch"
1783 );
1784 let weights_view = weights.view();
1789 match self {
1790 Self::Materialized(matrix) => {
1791 let n = matrix.nrows();
1792 let p = matrix.ncols();
1793 let mut out = if (n as u64) * (p as u64) < DENSE_ROW_PARALLEL_MIN_NP {
1794 let mut out = Array1::<f64>::zeros(p);
1795 for i in 0..n {
1796 let wi = weights_view[i];
1797 if wi == 0.0 {
1798 continue;
1799 }
1800 let mut row_dot = 0.0_f64;
1801 for j in 0..p {
1802 row_dot += matrix[[i, j]] * vector[j];
1803 }
1804 if row_dot == 0.0 {
1805 continue;
1806 }
1807 let scaled = wi * row_dot;
1808 for j in 0..p {
1809 out[j] += scaled * matrix[[i, j]];
1810 }
1811 }
1812 out
1813 } else {
1814 crate::pairwise_reduce::par_deterministic_block_fold(
1817 n,
1818 |range: core::ops::Range<usize>| {
1819 let mut acc = Array1::<f64>::zeros(p);
1820 for i in range {
1821 let wi = weights_view[i];
1822 if wi != 0.0 {
1823 let mut row_dot = 0.0_f64;
1824 for j in 0..p {
1825 row_dot += matrix[[i, j]] * vector[j];
1826 }
1827 if row_dot != 0.0 {
1828 let scaled = wi * row_dot;
1829 for j in 0..p {
1830 acc[j] += scaled * matrix[[i, j]];
1831 }
1832 }
1833 }
1834 }
1835 acc
1836 },
1837 |mut a, b| {
1838 a += &b;
1839 a
1840 },
1841 )
1842 .unwrap_or_else(|| Array1::<f64>::zeros(p))
1843 };
1844 if let Some(pen) = penalty {
1845 out += &fast_av(pen, vector);
1846 }
1847 if ridge > 0.0 {
1848 for j in 0..p {
1849 out[j] += ridge * vector[j];
1850 }
1851 }
1852 out
1853 }
1854 Self::Lazy(op) => op.apply_weighted_normal(weights, vector, penalty, ridge),
1855 }
1856 }
1857
1858 fn uses_matrix_free_pcg(&self) -> bool {
1859 match self {
1860 Self::Materialized(_) => true,
1861 Self::Lazy(op) => op.uses_matrix_free_pcg(),
1862 }
1863 }
1864}
1865
1866impl DenseDesignOperator for DenseDesignMatrix {
1867 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
1868 if y.len() != self.nrows() {
1869 return Err(format!(
1870 "DenseDesignMatrix::compute_xtwy response length mismatch: y={}, nrows={}",
1871 y.len(),
1872 self.nrows()
1873 ));
1874 }
1875 certify_signed_weights("DenseDesignMatrix::compute_xtwy", weights, self.nrows())?;
1876 match self {
1877 Self::Materialized(matrix) => {
1878 Ok(dense_transpose_weighted_response(matrix, weights, y, None))
1879 }
1880 Self::Lazy(op) => op.compute_xtwy(weights, y),
1881 }
1882 }
1883
1884 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
1885 match self {
1886 Self::Materialized(matrix) => {
1887 if middle.nrows() != matrix.ncols() || middle.ncols() != matrix.ncols() {
1888 return Err(format!(
1889 "quadratic_form_diag dimension mismatch: matrix is {}x{}, expected {}x{}",
1890 middle.nrows(),
1891 middle.ncols(),
1892 matrix.ncols(),
1893 matrix.ncols()
1894 ));
1895 }
1896 let xc = fast_ab(matrix, middle);
1897 let n = matrix.nrows();
1898 let p = matrix.ncols();
1899 let mut out = Array1::<f64>::zeros(n);
1900 if matrix.is_standard_layout()
1901 && xc.is_standard_layout()
1902 && let (Some(m_all), Some(xc_all), Some(out_slice)) =
1903 (matrix.as_slice(), xc.as_slice(), out.as_slice_mut())
1904 {
1905 use rayon::iter::{IndexedParallelIterator, ParallelIterator};
1910 use rayon::slice::ParallelSliceMut;
1911 out_slice
1912 .par_chunks_mut(1)
1913 .enumerate()
1914 .for_each(|(i, slot)| {
1915 let off = i * p;
1916 let m_row = &m_all[off..off + p];
1917 let xc_row = &xc_all[off..off + p];
1918 let mut acc = 0.0_f64;
1919 for j in 0..p {
1920 acc += m_row[j] * xc_row[j];
1921 }
1922 slot[0] = acc.max(0.0);
1925 });
1926 } else {
1927 for i in 0..n {
1928 out[i] = matrix.row(i).dot(&xc.row(i)).max(0.0);
1931 }
1932 }
1933 Ok(out)
1934 }
1935 Self::Lazy(op) => op.quadratic_form_diag(middle),
1936 }
1937 }
1938
1939 fn as_dense_ref(&self) -> Option<&Array2<f64>> {
1940 DenseDesignMatrix::as_dense_ref(self)
1941 }
1942
1943 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
1944 match self {
1945 Self::Materialized(_) => None,
1946 Self::Lazy(lazy) => lazy.op.materialization_policy(),
1947 }
1948 }
1949
1950 fn row_chunk_into(
1951 &self,
1952 rows: Range<usize>,
1953 mut out: ArrayViewMut2<'_, f64>,
1954 ) -> Result<(), MatrixMaterializationError> {
1955 if out.nrows() != rows.end - rows.start || out.ncols() != self.ncols() {
1956 return Err(MatrixMaterializationError::MissingRowChunk {
1957 context: "DenseDesignMatrix::row_chunk_into shape mismatch",
1958 });
1959 }
1960 match self {
1961 Self::Materialized(matrix) => {
1962 out.assign(&matrix.slice(s![rows, ..]));
1963 Ok(())
1964 }
1965 Self::Lazy(op) => op.row_chunk_into(rows, out),
1966 }
1967 }
1968
1969 fn to_dense(&self) -> Array2<f64> {
1970 DenseDesignMatrix::to_dense(self)
1971 }
1972
1973 fn to_dense_arc(&self) -> Arc<Array2<f64>> {
1974 DenseDesignMatrix::to_dense_arc(self)
1975 }
1976}
1977
1978pub struct ReparamOperator {
1993 x_original: DesignMatrix,
1994 qs: Arc<Array2<f64>>,
1995 n: usize,
1996 p: usize,
1997}
1998
1999impl ReparamOperator {
2000 pub fn new(x_original: DesignMatrix, qs: Arc<Array2<f64>>) -> Self {
2001 let n = x_original.nrows();
2002 let p = qs.ncols();
2003 assert_eq!(
2004 x_original.ncols(),
2005 qs.nrows(),
2006 "ReparamOperator: X cols ({}) must match Qs rows ({})",
2007 x_original.ncols(),
2008 qs.nrows()
2009 );
2010 Self {
2011 x_original,
2012 qs,
2013 n,
2014 p,
2015 }
2016 }
2017
2018 pub fn x_original(&self) -> &DesignMatrix {
2020 &self.x_original
2021 }
2022
2023 pub fn qs(&self) -> &Array2<f64> {
2025 &self.qs
2026 }
2027}
2028
2029impl LinearOperator for ReparamOperator {
2030 fn nrows(&self) -> usize {
2031 self.n
2032 }
2033
2034 fn ncols(&self) -> usize {
2035 self.p
2036 }
2037
2038 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
2039 let qv = self.qs.dot(vector);
2041 self.x_original.apply(&qv)
2042 }
2043
2044 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
2045 let xtv = self.x_original.apply_transpose(vector);
2047 fast_atv(&self.qs, &xtv)
2048 }
2049
2050 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
2051 let xtwx = self.x_original.diag_xtw_x(weights)?;
2054 let tmp = fast_atb(&self.qs, &xtwx);
2055 Ok(fast_ab(&tmp, &self.qs))
2056 }
2057
2058 fn apply_weighted_normal(
2059 &self,
2060 weights: FiniteSignedWeightsView<'_>,
2061 vector: &Array1<f64>,
2062 penalty: Option<&Array2<f64>>,
2063 ridge: f64,
2064 ) -> Array1<f64> {
2065 assert_eq!(
2066 weights.len(),
2067 self.x_original.nrows(),
2068 "ReparamOperator::apply_weighted_normal weight length mismatch"
2069 );
2070 assert_eq!(
2071 vector.len(),
2072 self.qs.ncols(),
2073 "ReparamOperator::apply_weighted_normal vector length mismatch"
2074 );
2075 let weights = weights.view();
2078 let qv = self.qs.dot(vector);
2079 let xqv = self.x_original.apply(&qv);
2080 let mut wxqv = xqv;
2081 for i in 0..wxqv.len() {
2082 wxqv[i] *= weights[i];
2083 }
2084 let xtw = self.x_original.apply_transpose(&wxqv);
2085 let mut out = fast_atv(&self.qs, &xtw);
2086 if let Some(pen) = penalty {
2087 out += &fast_av(pen, vector);
2088 }
2089 if ridge > 0.0 {
2090 out.scaled_add(ridge, vector);
2092 }
2093 out
2094 }
2095}
2096
2097impl DenseDesignOperator for ReparamOperator {
2098 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
2099 let xtwy = self.x_original.compute_xtwy(weights, y)?;
2101 Ok(fast_atv(&self.qs, &xtwy))
2102 }
2103
2104 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
2105 let qm = fast_ab(&self.qs, middle);
2108 let m_orig = fast_ab(&qm, &self.qs.t().to_owned());
2109 self.x_original.quadratic_form_diag(&m_orig)
2110 }
2111
2112 fn to_dense(&self) -> Array2<f64> {
2113 match &self.x_original {
2114 DesignMatrix::Dense(x) => fast_ab(x.to_dense_arc().as_ref(), &self.qs),
2115 _ => {
2116 let x_dense = self.x_original.to_dense();
2117 fast_ab(&x_dense, &self.qs)
2118 }
2119 }
2120 }
2121
2122 fn to_dense_arc(&self) -> Arc<Array2<f64>> {
2123 Arc::new(self.to_dense())
2124 }
2125
2126 fn as_dense_ref(&self) -> Option<&Array2<f64>> {
2127 None
2128 }
2129
2130 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
2131 self.x_original.materialization_policy()
2132 }
2133
2134 fn apply_columns(&self, cols: &[usize]) -> Array2<f64> {
2135 let qs_cols = self.qs.select(Axis(1), cols);
2138 match &self.x_original {
2139 DesignMatrix::Dense(x) => match x.as_dense_ref() {
2140 Some(x_dense) => fast_ab(x_dense, &qs_cols),
2141 None => {
2142 let n = self.n;
2143 let mut out = Array2::<f64>::zeros((n, cols.len()));
2144 for k in 0..cols.len() {
2145 let col = qs_cols.column(k).to_owned();
2146 let xc = self.x_original.apply(&col);
2147 out.column_mut(k).assign(&xc);
2148 }
2149 out
2150 }
2151 },
2152 DesignMatrix::Sparse(_) => {
2153 let n = self.n;
2155 let mut out = Array2::<f64>::zeros((n, cols.len()));
2156 for k in 0..cols.len() {
2157 let col = qs_cols.column(k).to_owned();
2158 let xc = self.x_original.apply(&col);
2159 out.column_mut(k).assign(&xc);
2160 }
2161 out
2162 }
2163 }
2164 }
2165
2166 fn row_chunk_into(
2167 &self,
2168 rows: Range<usize>,
2169 mut out: ArrayViewMut2<'_, f64>,
2170 ) -> Result<(), MatrixMaterializationError> {
2171 if out.nrows() != rows.end - rows.start || out.ncols() != self.p {
2172 return Err(MatrixMaterializationError::MissingRowChunk {
2173 context: "ReparamOperator::row_chunk_into shape mismatch",
2174 });
2175 }
2176 match &self.x_original {
2177 DesignMatrix::Dense(x) => {
2178 let chunk = x.try_row_chunk(rows)?;
2179 out.assign(&fast_ab(&chunk, &self.qs));
2180 }
2181 DesignMatrix::Sparse(sdm) => {
2182 let csr = sdm
2184 .to_csr_arc()
2185 .ok_or(MatrixMaterializationError::MissingRowChunk {
2186 context: "ReparamOperator::row_chunk_into: failed to obtain CSR view",
2187 })?;
2188 let sym = csr.symbolic();
2189 let row_ptr = sym.row_ptr();
2190 let col_idx = sym.col_idx();
2191 let vals = csr.val();
2192 let chunk_rows = rows.end - rows.start;
2193 let p_inner = sdm.ncols();
2194 let mut chunk = Array2::<f64>::zeros((chunk_rows, p_inner));
2195 for (local, global) in (rows.start..rows.end).enumerate() {
2196 for ptr in row_ptr[global]..row_ptr[global + 1] {
2197 chunk[[local, col_idx[ptr]]] = vals[ptr];
2198 }
2199 }
2200 out.assign(&fast_ab(&chunk, &self.qs));
2201 }
2202 }
2203 Ok(())
2204 }
2205}
2206
2207#[derive(Clone)]
2217pub struct RandomEffectOperator {
2218 pub group_ids: Vec<Option<usize>>,
2222 pub n: usize,
2224 pub num_groups: usize,
2226}
2227
2228impl RandomEffectOperator {
2229 pub fn new(group_ids: Vec<Option<usize>>, num_groups: usize) -> Self {
2230 let n = group_ids.len();
2231 Self {
2232 group_ids,
2233 n,
2234 num_groups,
2235 }
2236 }
2237
2238 pub fn weighted_cross_with_dense(
2244 &self,
2245 dense: &Array2<f64>,
2246 weights: &Array1<f64>,
2247 ) -> Result<Array2<f64>, String> {
2248 if dense.nrows() != self.n {
2249 return Err(format!(
2250 "RandomEffectOperator::weighted_cross_with_dense row mismatch: dense={}, nrows={}",
2251 dense.nrows(),
2252 self.n
2253 ));
2254 }
2255 certify_signed_weights(
2256 "RandomEffectOperator::weighted_cross_with_dense",
2257 weights,
2258 self.n,
2259 )?;
2260 let p_dense = dense.ncols();
2261 let mut cross = Array2::<f64>::zeros((p_dense, self.num_groups));
2262 for i in 0..self.n {
2263 if let Some(g) = self.group_ids[i] {
2264 let wi = weights[i];
2265 if wi == 0.0 {
2266 continue;
2267 }
2268 for j in 0..p_dense {
2269 cross[[j, g]] += wi * dense[[i, j]];
2270 }
2271 }
2272 }
2273 Ok(cross)
2274 }
2275
2276 pub fn weighted_cross_with_re(
2280 &self,
2281 other: &RandomEffectOperator,
2282 weights: &Array1<f64>,
2283 ) -> Result<Array2<f64>, String> {
2284 if other.n != self.n {
2285 return Err(format!(
2286 "RandomEffectOperator::weighted_cross_with_re row mismatch: other={}, nrows={}",
2287 other.n, self.n
2288 ));
2289 }
2290 certify_signed_weights(
2291 "RandomEffectOperator::weighted_cross_with_re",
2292 weights,
2293 self.n,
2294 )?;
2295 let mut cross = Array2::<f64>::zeros((self.num_groups, other.num_groups));
2296 for i in 0..self.n {
2297 if let (Some(a), Some(b)) = (self.group_ids[i], other.group_ids[i]) {
2298 let wi = weights[i];
2299 if wi != 0.0 {
2300 cross[[a, b]] += wi;
2301 }
2302 }
2303 }
2304 Ok(cross)
2305 }
2306}
2307
2308impl LinearOperator for RandomEffectOperator {
2309 fn nrows(&self) -> usize {
2310 self.n
2311 }
2312
2313 fn ncols(&self) -> usize {
2314 self.num_groups
2315 }
2316
2317 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
2319 use rayon::prelude::*;
2320 let out: Vec<f64> = self
2321 .group_ids
2322 .par_iter()
2323 .map(|g| g.map(|g| vector[g]).unwrap_or(0.0))
2324 .collect();
2325 Array1::from(out)
2326 }
2327
2328 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
2330 let mut out = Array1::<f64>::zeros(self.num_groups);
2331 for i in 0..self.n {
2332 if let Some(g) = self.group_ids[i] {
2333 out[g] += vector[i];
2334 }
2335 }
2336 out
2337 }
2338
2339 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
2341 certify_signed_weights("RandomEffectOperator::diag_xtw_x", weights, self.n)?;
2342 let q = self.num_groups;
2343 let mut xtwx = Array2::<f64>::zeros((q, q));
2344 for i in 0..self.n {
2345 if let Some(g) = self.group_ids[i] {
2346 xtwx[[g, g]] += weights[i];
2347 }
2348 }
2349 Ok(xtwx)
2350 }
2351
2352 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
2354 certify_signed_weights("RandomEffectOperator::diag_gram", weights, self.n)?;
2355 let mut diag = Array1::<f64>::zeros(self.num_groups);
2356 for i in 0..self.n {
2357 if let Some(g) = self.group_ids[i] {
2358 diag[g] += weights[i];
2359 }
2360 }
2361 Ok(diag)
2362 }
2363
2364 fn apply_weighted_normal(
2366 &self,
2367 weights: FiniteSignedWeightsView<'_>,
2368 vector: &Array1<f64>,
2369 penalty: Option<&Array2<f64>>,
2370 ridge: f64,
2371 ) -> Array1<f64> {
2372 assert_eq!(
2373 weights.len(),
2374 self.n,
2375 "RandomEffectOperator::apply_weighted_normal weight length mismatch"
2376 );
2377 assert_eq!(
2378 vector.len(),
2379 self.num_groups,
2380 "RandomEffectOperator::apply_weighted_normal vector length mismatch"
2381 );
2382 let weights = weights.view();
2386 let mut group_wacc = Array1::<f64>::zeros(self.num_groups);
2387 for i in 0..self.n {
2388 if let Some(g) = self.group_ids[i] {
2389 group_wacc[g] += weights[i];
2390 }
2391 }
2392 let mut out = Array1::<f64>::zeros(self.num_groups);
2393 for g in 0..self.num_groups {
2394 out[g] = group_wacc[g] * vector[g];
2395 }
2396 if let Some(pen) = penalty {
2397 out += &pen.dot(vector);
2398 }
2399 if ridge > 0.0 {
2400 for g in 0..self.num_groups {
2401 out[g] += ridge * vector[g];
2402 }
2403 }
2404 out
2405 }
2406
2407 fn uses_matrix_free_pcg(&self) -> bool {
2408 true
2409 }
2410}
2411
2412impl DenseDesignOperator for RandomEffectOperator {
2413 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
2414 if weights.len() != self.n || y.len() != self.n {
2415 return Err(format!(
2416 "RandomEffectOperator::compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
2417 weights.len(),
2418 y.len(),
2419 self.n
2420 ));
2421 }
2422 certify_signed_weights("RandomEffectOperator::compute_xtwy", weights, self.n)?;
2423 let mut out = Array1::<f64>::zeros(self.num_groups);
2424 for i in 0..self.n {
2425 if let Some(g) = self.group_ids[i] {
2426 let wi = weights[i];
2427 out[g] += wi * y[i];
2428 }
2429 }
2430 Ok(out)
2431 }
2432
2433 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
2435 use rayon::prelude::*;
2436 let out: Vec<f64> = self
2437 .group_ids
2438 .par_iter()
2439 .map(|g| g.map(|g| middle[[g, g]].max(0.0)).unwrap_or(0.0))
2440 .collect();
2441 Ok(Array1::from(out))
2442 }
2443
2444 fn row_chunk_into(
2445 &self,
2446 rows: Range<usize>,
2447 mut out: ArrayViewMut2<'_, f64>,
2448 ) -> Result<(), MatrixMaterializationError> {
2449 if out.nrows() != rows.end - rows.start || out.ncols() != self.num_groups {
2450 return Err(MatrixMaterializationError::MissingRowChunk {
2451 context: "RandomEffectOperator::row_chunk_into shape mismatch",
2452 });
2453 }
2454 out.fill(0.0);
2455 for (local, global) in rows.enumerate() {
2456 if let Some(g) = self.group_ids[global] {
2457 out[[local, g]] = 1.0;
2458 }
2459 }
2460 Ok(())
2461 }
2462
2463 fn to_dense(&self) -> Array2<f64> {
2465 let mut out = Array2::<f64>::zeros((self.n, self.num_groups));
2466 ndarray::Zip::indexed(out.rows_mut()).par_for_each(|i, mut row| {
2467 if let Some(g) = self.group_ids[i] {
2468 row[g] = 1.0;
2469 }
2470 });
2471 out
2472 }
2473}
2474
2475#[derive(Clone)]
2481pub enum DesignBlock {
2482 Dense(DenseDesignMatrix),
2483 Sparse(SparseDesignMatrix),
2484 RandomEffect(Arc<RandomEffectOperator>),
2485 Intercept(usize),
2487}
2488
2489impl DesignBlock {
2490 pub fn nrows(&self) -> usize {
2491 match self {
2492 Self::Dense(d) => d.nrows(),
2493 Self::Sparse(s) => s.nrows(),
2494 Self::RandomEffect(op) => op.nrows(),
2495 Self::Intercept(n) => *n,
2496 }
2497 }
2498
2499 pub fn ncols(&self) -> usize {
2500 match self {
2501 Self::Dense(d) => d.ncols(),
2502 Self::Sparse(s) => s.ncols(),
2503 Self::RandomEffect(op) => op.ncols(),
2504 Self::Intercept(_) => 1,
2505 }
2506 }
2507
2508 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
2509 match self {
2510 Self::Dense(design) => design.materialization_policy(),
2511 Self::Sparse(_) | Self::RandomEffect(_) | Self::Intercept(_) => None,
2512 }
2513 }
2514
2515 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
2516 match self {
2517 Self::Dense(d) => d.apply(vector),
2518 Self::Sparse(s) => DesignMatrix::Sparse(s.clone()).apply(vector),
2519 Self::RandomEffect(op) => op.apply(vector),
2520 Self::Intercept(n) => Array1::from_elem(*n, vector[0]),
2521 }
2522 }
2523
2524 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
2525 match self {
2526 Self::Dense(d) => d.apply_transpose(vector),
2527 Self::Sparse(s) => DesignMatrix::Sparse(s.clone()).apply_transpose(vector),
2528 Self::RandomEffect(op) => op.apply_transpose(vector),
2529 Self::Intercept(_) => {
2530 let sum: f64 = vector.iter().sum();
2531 Array1::from_vec(vec![sum])
2532 }
2533 }
2534 }
2535
2536 fn try_row_chunk(&self, rows: Range<usize>) -> Result<Array2<f64>, MatrixMaterializationError> {
2537 match self {
2538 Self::Dense(d) => d.try_row_chunk(rows),
2539 Self::Sparse(s) => DesignMatrix::Sparse(s.clone()).try_row_chunk(rows),
2540 Self::RandomEffect(op) => op.try_row_chunk(rows),
2541 Self::Intercept(_) => Ok(Array2::ones((rows.end - rows.start, 1))),
2542 }
2543 }
2544
2545 fn row_chunk_into(
2546 &self,
2547 rows: Range<usize>,
2548 mut out: ArrayViewMut2<'_, f64>,
2549 ) -> Result<(), MatrixMaterializationError> {
2550 if out.nrows() != rows.end - rows.start || out.ncols() != self.ncols() {
2551 return Err(MatrixMaterializationError::MissingRowChunk {
2552 context: "DesignBlock::row_chunk_into shape mismatch",
2553 });
2554 }
2555 match self {
2556 Self::Dense(d) => d.row_chunk_into(rows, out),
2557 Self::Sparse(s) => s.row_chunk_into(rows, out),
2558 Self::RandomEffect(op) => op.row_chunk_into(rows, out),
2559 Self::Intercept(_) => {
2560 out.fill(1.0);
2561 Ok(())
2562 }
2563 }
2564 }
2565
2566 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
2567 certify_signed_weights("DesignBlock::diag_xtw_x", weights, self.nrows())?;
2568 match self {
2569 Self::Dense(d) => d.diag_xtw_x(weights),
2570 Self::Sparse(s) => DesignMatrix::Sparse(s.clone()).diag_xtw_x(weights),
2571 Self::RandomEffect(op) => op.diag_xtw_x(weights),
2572 Self::Intercept(_) => {
2573 let sum: f64 = weights.iter().sum();
2580 Ok(Array2::from_elem((1, 1), sum))
2581 }
2582 }
2583 }
2584
2585 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
2586 certify_signed_weights("DesignBlock::diag_gram", weights, self.nrows())?;
2587 match self {
2588 Self::Dense(d) => d.diag_gram(weights),
2589 Self::Sparse(s) => DesignMatrix::Sparse(s.clone()).diag_gram(weights),
2590 Self::RandomEffect(op) => op.diag_gram(weights),
2591 Self::Intercept(_) => {
2592 let sum: f64 = weights.iter().sum();
2596 Ok(Array1::from_vec(vec![sum]))
2597 }
2598 }
2599 }
2600
2601 fn to_dense(&self) -> Array2<f64> {
2603 match self {
2604 Self::Dense(d) => d.to_dense(),
2605 Self::Sparse(s) => s.to_dense_arc().as_ref().clone(),
2606 Self::RandomEffect(op) => op.to_dense(),
2607 Self::Intercept(n) => Array2::ones((*n, 1)),
2608 }
2609 }
2610}
2611
2612#[derive(Clone)]
2619pub struct BlockDesignOperator {
2620 pub blocks: Vec<DesignBlock>,
2621 pub col_offsets: Vec<usize>,
2623 pub total_cols: usize,
2624 pub n: usize,
2625}
2626
2627impl BlockDesignOperator {
2628 pub fn new(blocks: Vec<DesignBlock>) -> Result<Self, String> {
2629 if blocks.is_empty() {
2630 return Err("BlockDesignOperator: need at least one block".to_string());
2631 }
2632 let n = blocks[0].nrows();
2633 for (i, b) in blocks.iter().enumerate() {
2634 if b.nrows() != n {
2635 return Err(format!(
2636 "BlockDesignOperator: block {i} has {} rows, expected {n}",
2637 b.nrows()
2638 ));
2639 }
2640 }
2641 let mut col_offsets = Vec::with_capacity(blocks.len() + 1);
2642 col_offsets.push(0);
2643 for b in &blocks {
2644 col_offsets.push(col_offsets.last().unwrap() + b.ncols());
2645 }
2646 let total_cols = *col_offsets.last().unwrap();
2647 Ok(Self {
2648 blocks,
2649 col_offsets,
2650 total_cols,
2651 n,
2652 })
2653 }
2654
2655 fn weighted_cross_chunked(
2656 &self,
2657 left: &DesignBlock,
2658 right: &DesignBlock,
2659 weights: &Array1<f64>,
2660 ) -> Result<Array2<f64>, String> {
2661 let pi = left.ncols();
2662 let pj = right.ncols();
2663 let mut cross = Array2::<f64>::zeros((pi, pj));
2664 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2665 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2666 let left_chunk = left.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2667 let right_chunk = right.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2668 for local in 0..(end - start) {
2669 let wi = weights[start + local];
2677 if wi == 0.0 {
2678 continue;
2679 }
2680 for a in 0..pi {
2681 let scaled = wi * left_chunk[[local, a]];
2682 if scaled == 0.0 {
2683 continue;
2684 }
2685 for b in 0..pj {
2686 cross[[a, b]] += scaled * right_chunk[[local, b]];
2687 }
2688 }
2689 }
2690 }
2691 Ok(cross)
2692 }
2693
2694 fn quadratic_form_diag_cross_chunked(
2695 &self,
2696 block_a: &DesignBlock,
2697 block_b: &DesignBlock,
2698 m_ab: &Array2<f64>,
2699 ) -> Result<Array1<f64>, String> {
2700 let mut out = Array1::<f64>::zeros(self.n);
2701 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2702 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2703 let a_chunk = block_a
2704 .try_row_chunk(start..end)
2705 .map_err(|e| e.to_string())?;
2706 let b_chunk = block_b
2707 .try_row_chunk(start..end)
2708 .map_err(|e| e.to_string())?;
2709 let a_m = fast_ab(&a_chunk, m_ab);
2710 for local in 0..(end - start) {
2711 out[start + local] = a_m.row(local).dot(&b_chunk.row(local));
2712 }
2713 }
2714 Ok(out)
2715 }
2716
2717 fn cross_block(
2719 &self,
2720 i: usize,
2721 j: usize,
2722 weights: &Array1<f64>,
2723 ) -> Result<Array2<f64>, String> {
2724 match (&self.blocks[i], &self.blocks[j]) {
2725 (DesignBlock::Dense(d_i), DesignBlock::Dense(d_j)) => {
2727 if let (Some(xi), Some(xj)) = (d_i.as_dense_ref(), d_j.as_dense_ref()) {
2728 weighted_crossprod_dense(xi, weights, xj)
2729 } else {
2730 self.weighted_cross_chunked(&self.blocks[i], &self.blocks[j], weights)
2731 }
2732 }
2733 (DesignBlock::Dense(_), DesignBlock::Sparse(_))
2734 | (DesignBlock::Sparse(_), DesignBlock::Dense(_))
2735 | (DesignBlock::Sparse(_), DesignBlock::Sparse(_))
2736 | (DesignBlock::Sparse(_), DesignBlock::RandomEffect(_))
2737 | (DesignBlock::RandomEffect(_), DesignBlock::Sparse(_)) => {
2738 self.weighted_cross_chunked(&self.blocks[i], &self.blocks[j], weights)
2739 }
2740
2741 (DesignBlock::Dense(d), DesignBlock::RandomEffect(re)) => {
2743 if let Some(dense) = d.as_dense_ref() {
2744 re.weighted_cross_with_dense(dense, weights)
2745 } else {
2746 self.weighted_cross_chunked(&self.blocks[i], &self.blocks[j], weights)
2747 }
2748 }
2749 (DesignBlock::RandomEffect(re), DesignBlock::Dense(d)) => {
2750 if let Some(dense) = d.as_dense_ref() {
2751 let cross_t = re.weighted_cross_with_dense(dense, weights)?;
2752 Ok(cross_t.t().to_owned())
2753 } else {
2754 self.weighted_cross_chunked(&self.blocks[i], &self.blocks[j], weights)
2755 }
2756 }
2757
2758 (DesignBlock::RandomEffect(re_a), DesignBlock::RandomEffect(re_b)) => {
2760 re_a.weighted_cross_with_re(re_b, weights)
2761 }
2762
2763 (DesignBlock::Intercept(_), other) => {
2766 let pj = other.ncols();
2769 let mut cross = Array2::<f64>::zeros((1, pj));
2770 let row = other.apply_transpose(weights);
2771 cross.row_mut(0).assign(&row);
2772 Ok(cross)
2773 }
2774 (other, DesignBlock::Intercept(_)) => {
2775 let pi = other.ncols();
2776 let mut cross = Array2::<f64>::zeros((pi, 1));
2777 let col = other.apply_transpose(weights);
2778 cross.column_mut(0).assign(&col);
2779 Ok(cross)
2780 }
2781 }
2782 }
2783
2784 fn quadratic_form_diag_block(
2786 &self,
2787 block: &DesignBlock,
2788 m_kk: &Array2<f64>,
2789 ) -> Result<Array1<f64>, String> {
2790 match block {
2791 DesignBlock::Dense(d) => {
2792 if let Some(dense) = d.as_dense_ref() {
2793 let xm = fast_ab(dense, m_kk);
2794 let mut out = Array1::<f64>::zeros(self.n);
2795 ndarray::Zip::from(&mut out)
2796 .and(dense.rows())
2797 .and(xm.rows())
2798 .par_for_each(|o, dr, xmr| *o = dr.dot(&xmr));
2799 Ok(out)
2800 } else {
2801 d.quadratic_form_diag(m_kk)
2802 }
2803 }
2804 DesignBlock::Sparse(s) => {
2805 let sparse = DesignMatrix::Sparse(s.clone());
2806 sparse.quadratic_form_diag(m_kk)
2807 }
2808 DesignBlock::RandomEffect(re) => {
2809 use rayon::prelude::*;
2810 let out: Vec<f64> = re
2811 .group_ids
2812 .par_iter()
2813 .map(|g| g.map(|g| m_kk[[g, g]]).unwrap_or(0.0))
2814 .collect();
2815 Ok(Array1::from(out))
2816 }
2817 DesignBlock::Intercept(_) => {
2818 Ok(Array1::from_elem(self.n, m_kk[[0, 0]]))
2820 }
2821 }
2822 }
2823
2824 fn quadratic_form_diag_cross(
2826 &self,
2827 block_a: &DesignBlock,
2828 block_b: &DesignBlock,
2829 m_ab: &Array2<f64>,
2830 ) -> Result<Array1<f64>, String> {
2831 match (block_a, block_b) {
2832 (DesignBlock::Dense(da), DesignBlock::Dense(db)) => {
2833 if let (Some(da), Some(db)) = (da.as_dense_ref(), db.as_dense_ref()) {
2834 let da_m = fast_ab(da, m_ab);
2835 let mut out = Array1::<f64>::zeros(self.n);
2836 ndarray::Zip::from(&mut out)
2837 .and(da_m.rows())
2838 .and(db.rows())
2839 .par_for_each(|o, ar, br| *o = ar.dot(&br));
2840 Ok(out)
2841 } else {
2842 self.quadratic_form_diag_cross_chunked(block_a, block_b, m_ab)
2843 }
2844 }
2845 (DesignBlock::Dense(_), DesignBlock::Sparse(_))
2846 | (DesignBlock::Sparse(_), DesignBlock::Dense(_))
2847 | (DesignBlock::Sparse(_), DesignBlock::Sparse(_))
2848 | (DesignBlock::Sparse(_), DesignBlock::RandomEffect(_))
2849 | (DesignBlock::RandomEffect(_), DesignBlock::Sparse(_)) => {
2850 self.quadratic_form_diag_cross_chunked(block_a, block_b, m_ab)
2851 }
2852 (DesignBlock::Dense(d), DesignBlock::RandomEffect(re)) => {
2853 let mut out = Array1::<f64>::zeros(self.n);
2854 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2855 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2856 let chunk = d.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2857 for local in 0..chunk.nrows() {
2858 let i = start + local;
2859 if let Some(g) = re.group_ids[i] {
2860 let mut val = 0.0;
2861 for j in 0..chunk.ncols() {
2862 val += chunk[[local, j]] * m_ab[[j, g]];
2863 }
2864 out[i] = val;
2865 }
2866 }
2867 }
2868 Ok(out)
2869 }
2870 (DesignBlock::RandomEffect(re), DesignBlock::Dense(d)) => {
2871 let mut out = Array1::<f64>::zeros(self.n);
2872 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2873 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2874 let chunk = d.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2875 for local in 0..chunk.nrows() {
2876 let i = start + local;
2877 if let Some(g) = re.group_ids[i] {
2878 let mut val = 0.0;
2879 for j in 0..chunk.ncols() {
2880 val += m_ab[[g, j]] * chunk[[local, j]];
2881 }
2882 out[i] = val;
2883 }
2884 }
2885 }
2886 Ok(out)
2887 }
2888 (DesignBlock::RandomEffect(re_a), DesignBlock::RandomEffect(re_b)) => {
2889 use rayon::prelude::*;
2890 let out: Vec<f64> = re_a
2891 .group_ids
2892 .par_iter()
2893 .zip(re_b.group_ids.par_iter())
2894 .map(|(ga, gb)| match (ga, gb) {
2895 (Some(ga), Some(gb)) => m_ab[[*ga, *gb]],
2896 _ => 0.0,
2897 })
2898 .collect();
2899 Ok(Array1::from(out))
2900 }
2901
2902 (DesignBlock::Intercept(_), other) => {
2904 let m_row = m_ab.row(0);
2905 let mut out = Array1::<f64>::zeros(self.n);
2906 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2907 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2908 let chunk = other.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2909 for local in 0..(end - start) {
2910 out[start + local] = chunk.row(local).dot(&m_row);
2911 }
2912 }
2913 Ok(out)
2914 }
2915 (other, DesignBlock::Intercept(_)) => {
2916 let m_col = m_ab.column(0);
2917 let mut out = Array1::<f64>::zeros(self.n);
2918 for start in (0..self.n).step_by(OPERATOR_ROW_CHUNK_SIZE) {
2919 let end = (start + OPERATOR_ROW_CHUNK_SIZE).min(self.n);
2920 let chunk = other.try_row_chunk(start..end).map_err(|e| e.to_string())?;
2921 for local in 0..(end - start) {
2922 out[start + local] = chunk.row(local).dot(&m_col);
2923 }
2924 }
2925 Ok(out)
2926 }
2927 }
2928 }
2929}
2930
2931impl LinearOperator for BlockDesignOperator {
2932 fn nrows(&self) -> usize {
2933 self.n
2934 }
2935
2936 fn ncols(&self) -> usize {
2937 self.total_cols
2938 }
2939
2940 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
2941 let mut out = Array1::<f64>::zeros(self.n);
2942 for (idx, block) in self.blocks.iter().enumerate() {
2943 let start = self.col_offsets[idx];
2944 let end = self.col_offsets[idx + 1];
2945 let slice = vector.slice(s![start..end]).to_owned();
2946 let contribution = block.apply(&slice);
2947 out += &contribution;
2948 }
2949 out
2950 }
2951
2952 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
2953 let mut out = Array1::<f64>::zeros(self.total_cols);
2954 for (idx, block) in self.blocks.iter().enumerate() {
2955 let start = self.col_offsets[idx];
2956 let end = self.col_offsets[idx + 1];
2957 let transposed = block.apply_transpose(vector);
2958 out.slice_mut(s![start..end]).assign(&transposed);
2959 }
2960 out
2961 }
2962
2963 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
2964 certify_signed_weights("BlockDesignOperator::diag_xtw_x", weights, self.n)?;
2965 let p = self.total_cols;
2966 let mut result = Array2::<f64>::zeros((p, p));
2967
2968 for (idx, block) in self.blocks.iter().enumerate() {
2970 let start = self.col_offsets[idx];
2971 let end = self.col_offsets[idx + 1];
2972 let block_xtwx = block.diag_xtw_x(weights)?;
2973 result
2974 .slice_mut(s![start..end, start..end])
2975 .assign(&block_xtwx);
2976 }
2977
2978 let weighted_dense: Vec<Option<Array2<f64>>> = self
2994 .blocks
2995 .iter()
2996 .map(|block| match block {
2997 DesignBlock::Dense(d) => d.as_dense_ref().map(|x| {
2998 x * &weights.view().insert_axis(Axis(1))
3001 }),
3002 _ => None,
3003 })
3004 .collect();
3005
3006 for i in 0..self.blocks.len() {
3007 for j in (i + 1)..self.blocks.len() {
3008 let cross = match (&weighted_dense[i], &self.blocks[j]) {
3009 (Some(wx_i), DesignBlock::Dense(d_j)) => match d_j.as_dense_ref() {
3012 Some(x_j) => fast_atb(wx_i, x_j),
3013 None => self.cross_block(i, j, weights)?,
3014 },
3015 _ => self.cross_block(i, j, weights)?,
3016 };
3017 let si = self.col_offsets[i];
3018 let ei = self.col_offsets[i + 1];
3019 let sj = self.col_offsets[j];
3020 let ej = self.col_offsets[j + 1];
3021 result.slice_mut(s![si..ei, sj..ej]).assign(&cross);
3022 result.slice_mut(s![sj..ej, si..ei]).assign(&cross.t());
3023 }
3024 }
3025
3026 Ok(result)
3027 }
3028
3029 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
3030 certify_signed_weights("BlockDesignOperator::diag_gram", weights, self.n)?;
3031 let mut out = Array1::<f64>::zeros(self.total_cols);
3032 for (idx, block) in self.blocks.iter().enumerate() {
3033 let start = self.col_offsets[idx];
3034 let end = self.col_offsets[idx + 1];
3035 let block_diag = block.diag_gram(weights)?;
3036 out.slice_mut(s![start..end]).assign(&block_diag);
3037 }
3038 Ok(out)
3039 }
3040
3041 fn apply_weighted_normal(
3042 &self,
3043 weights: FiniteSignedWeightsView<'_>,
3044 vector: &Array1<f64>,
3045 penalty: Option<&Array2<f64>>,
3046 ridge: f64,
3047 ) -> Array1<f64> {
3048 assert_eq!(
3049 weights.len(),
3050 self.n,
3051 "BlockDesignOperator::apply_weighted_normal weight length mismatch"
3052 );
3053 assert_eq!(
3054 vector.len(),
3055 self.total_cols,
3056 "BlockDesignOperator::apply_weighted_normal vector length mismatch"
3057 );
3058 let weights = weights.view();
3060 let xv = self.apply(vector);
3061 let mut weighted = xv;
3062 for i in 0..weighted.len() {
3063 weighted[i] *= weights[i];
3064 }
3065 let mut out = self.apply_transpose(&weighted);
3066 if let Some(pen) = penalty {
3067 out += &fast_av(pen, vector);
3068 }
3069 if ridge > 0.0 {
3070 out.scaled_add(ridge, vector);
3072 }
3073 out
3074 }
3075
3076 fn uses_matrix_free_pcg(&self) -> bool {
3077 self.blocks
3079 .iter()
3080 .any(|b| matches!(b, DesignBlock::RandomEffect(_) | DesignBlock::Intercept(_)))
3081 }
3082}
3083
3084impl DenseDesignOperator for BlockDesignOperator {
3085 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
3086 self.blocks.iter().fold(None, |policy, block| {
3087 merge_operator_materialization_policies(policy, block.materialization_policy())
3088 })
3089 }
3090
3091 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
3092 if weights.len() != self.n || y.len() != self.n {
3093 return Err(format!(
3094 "BlockDesignOperator::compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
3095 weights.len(),
3096 y.len(),
3097 self.n
3098 ));
3099 }
3100 certify_signed_weights("BlockDesignOperator::compute_xtwy", weights, self.n)?;
3101 let mut wy = Array1::<f64>::zeros(self.n);
3102 ndarray::Zip::from(&mut wy)
3103 .and(weights)
3104 .and(y)
3105 .par_for_each(|o, &w, &yi| *o = w * yi);
3106 Ok(self.apply_transpose(&wy))
3107 }
3108
3109 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
3110 let mut out = Array1::<f64>::zeros(self.n);
3113 let nb = self.blocks.len();
3114
3115 for k in 0..nb {
3117 let sk = self.col_offsets[k];
3118 let ek = self.col_offsets[k + 1];
3119 let m_kk = middle.slice(s![sk..ek, sk..ek]).to_owned();
3120 let block_diag = self.quadratic_form_diag_block(&self.blocks[k], &m_kk)?;
3121 out += &block_diag;
3122 }
3123
3124 for a in 0..nb {
3126 for b in (a + 1)..nb {
3127 let sa = self.col_offsets[a];
3128 let ea = self.col_offsets[a + 1];
3129 let sb = self.col_offsets[b];
3130 let eb = self.col_offsets[b + 1];
3131 let m_ab = middle.slice(s![sa..ea, sb..eb]);
3132
3133 let cross_diag = self.quadratic_form_diag_cross(
3134 &self.blocks[a],
3135 &self.blocks[b],
3136 &m_ab.to_owned(),
3137 )?;
3138 for i in 0..self.n {
3139 out[i] += 2.0 * cross_diag[i];
3140 }
3141 }
3142 }
3143
3144 for v in out.iter_mut() {
3146 *v = v.max(0.0);
3147 }
3148 Ok(out)
3149 }
3150
3151 fn row_chunk_into(
3152 &self,
3153 rows: Range<usize>,
3154 mut out: ArrayViewMut2<'_, f64>,
3155 ) -> Result<(), MatrixMaterializationError> {
3156 if out.nrows() != rows.end - rows.start || out.ncols() != self.total_cols {
3157 return Err(MatrixMaterializationError::MissingRowChunk {
3158 context: "BlockDesignOperator::row_chunk_into shape mismatch",
3159 });
3160 }
3161 for (idx, block) in self.blocks.iter().enumerate() {
3162 let cs = self.col_offsets[idx];
3163 let ce = self.col_offsets[idx + 1];
3164 block.row_chunk_into(rows.clone(), out.slice_mut(s![.., cs..ce]))?;
3165 }
3166 Ok(())
3167 }
3168
3169 fn to_dense(&self) -> Array2<f64> {
3170 let mut out = Array2::<f64>::zeros((self.n, self.total_cols));
3171 for (idx, block) in self.blocks.iter().enumerate() {
3172 let start = self.col_offsets[idx];
3173 let end = self.col_offsets[idx + 1];
3174 let dense_block = block.to_dense();
3175 out.slice_mut(s![.., start..end]).assign(&dense_block);
3176 }
3177 out
3178 }
3179}
3180
3181#[derive(Clone)]
3195pub struct MultiChannelOperator {
3196 pub channels: Vec<DesignMatrix>,
3198 pub n_per_channel: usize,
3200 pub p: usize,
3202}
3203
3204impl MultiChannelOperator {
3205 pub fn new(channels: Vec<DesignMatrix>) -> Result<Self, String> {
3206 if channels.is_empty() {
3207 return Err("MultiChannelOperator: need at least one channel".to_string());
3208 }
3209 let n = channels[0].nrows();
3210 let p = channels[0].ncols();
3211 for (i, ch) in channels.iter().enumerate() {
3212 if ch.nrows() != n {
3213 return Err(format!(
3214 "MultiChannelOperator: channel {i} has {} rows, expected {n}",
3215 ch.nrows()
3216 ));
3217 }
3218 if ch.ncols() != p {
3219 return Err(format!(
3220 "MultiChannelOperator: channel {i} has {} cols, expected {p}",
3221 ch.ncols()
3222 ));
3223 }
3224 }
3225 Ok(Self {
3226 channels,
3227 n_per_channel: n,
3228 p,
3229 })
3230 }
3231}
3232
3233impl LinearOperator for MultiChannelOperator {
3234 fn nrows(&self) -> usize {
3235 self.n_per_channel * self.channels.len()
3236 }
3237
3238 fn ncols(&self) -> usize {
3239 self.p
3240 }
3241
3242 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
3243 let total = self.nrows();
3244 let mut out = Array1::<f64>::zeros(total);
3245 let n = self.n_per_channel;
3246 for (i, ch) in self.channels.iter().enumerate() {
3247 let ch_result = ch.matrixvectormultiply(vector);
3248 out.slice_mut(s![i * n..(i + 1) * n]).assign(&ch_result);
3249 }
3250 out
3251 }
3252
3253 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
3254 let n = self.n_per_channel;
3255 let mut out = Array1::<f64>::zeros(self.p);
3256 for (i, ch) in self.channels.iter().enumerate() {
3257 out += &ch.apply_transpose_view(vector.slice(s![i * n..(i + 1) * n]));
3258 }
3259 out
3260 }
3261
3262 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
3263 let n = self.n_per_channel;
3264 certify_signed_weights("MultiChannelOperator::diag_xtw_x", weights, self.nrows())?;
3265 let mut xtwx = Array2::<f64>::zeros((self.p, self.p));
3266 for (i, ch) in self.channels.iter().enumerate() {
3267 let channel_weights = weights.slice(s![i * n..(i + 1) * n]).to_owned();
3268 let ch_xtwx = ch.diag_xtw_x(&channel_weights)?;
3269 xtwx += &ch_xtwx;
3270 }
3271 Ok(xtwx)
3272 }
3273
3274 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
3275 let n = self.n_per_channel;
3276 certify_signed_weights("MultiChannelOperator::diag_gram", weights, self.nrows())?;
3277 let mut diag = Array1::<f64>::zeros(self.p);
3278 for (i, ch) in self.channels.iter().enumerate() {
3279 diag += &ch.diag_gram_view(weights.slice(s![i * n..(i + 1) * n]))?;
3280 }
3281 Ok(diag)
3282 }
3283
3284 fn uses_matrix_free_pcg(&self) -> bool {
3285 true
3286 }
3287}
3288
3289impl DenseDesignOperator for MultiChannelOperator {
3290 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
3291 self.channels.iter().fold(None, |policy, channel| {
3292 merge_operator_materialization_policies(policy, channel.materialization_policy())
3293 })
3294 }
3295
3296 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
3297 let n = self.n_per_channel;
3298 let total = self.nrows();
3299 if weights.len() != total || y.len() != total {
3300 return Err(format!(
3301 "MultiChannelOperator::compute_xtwy: weights={}, y={}, nrows={}",
3302 weights.len(),
3303 y.len(),
3304 total
3305 ));
3306 }
3307 certify_signed_weights("MultiChannelOperator::compute_xtwy", weights, total)?;
3308 let mut out = Array1::<f64>::zeros(self.p);
3309 for (i, ch) in self.channels.iter().enumerate() {
3310 out += &ch.compute_xtwy_view(
3311 weights.slice(s![i * n..(i + 1) * n]),
3312 y.slice(s![i * n..(i + 1) * n]),
3313 )?;
3314 }
3315 Ok(out)
3316 }
3317
3318 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
3319 let n = self.n_per_channel;
3320 let mut out = Array1::<f64>::zeros(self.nrows());
3321 for (i, ch) in self.channels.iter().enumerate() {
3322 let ch_diag = ch.quadratic_form_diag(middle)?;
3323 out.slice_mut(s![i * n..(i + 1) * n]).assign(&ch_diag);
3324 }
3325 Ok(out)
3326 }
3327
3328 fn to_dense(&self) -> Array2<f64> {
3329 let total = self.nrows();
3330 let n = self.n_per_channel;
3331 let mut out = Array2::<f64>::zeros((total, self.p));
3332 for (i, ch) in self.channels.iter().enumerate() {
3333 let dense = ch.to_dense();
3334 out.slice_mut(s![i * n..(i + 1) * n, ..]).assign(&dense);
3335 }
3336 out
3337 }
3338
3339 fn row_chunk_into(
3340 &self,
3341 rows: Range<usize>,
3342 mut out: ArrayViewMut2<'_, f64>,
3343 ) -> Result<(), MatrixMaterializationError> {
3344 if out.nrows() != rows.end - rows.start || out.ncols() != self.p {
3345 return Err(MatrixMaterializationError::MissingRowChunk {
3346 context: "MultiChannelOperator::row_chunk_into shape mismatch",
3347 });
3348 }
3349 let n = self.n_per_channel;
3350 let mut local = 0usize;
3351 let mut global = rows.start;
3352 while global < rows.end {
3353 let ch_idx = global / n;
3354 let ch_local_start = global % n;
3355 let ch_local_end = ((ch_idx + 1) * n).min(rows.end) - ch_idx * n;
3356 let segment_len = ch_local_end - ch_local_start;
3357 self.channels[ch_idx].row_chunk_into(
3358 ch_local_start..ch_local_end,
3359 out.slice_mut(s![local..local + segment_len, ..]),
3360 )?;
3361 local += segment_len;
3362 global += segment_len;
3363 }
3364 Ok(())
3365 }
3366}
3367
3368mod kronecker;
3370pub use kronecker::*;
3371
3372pub struct CoefficientTransformOperator {
3379 inner: DenseDesignMatrix,
3380 transform: Arc<Array2<f64>>,
3381 n: usize,
3382 p_out: usize,
3383 materialized: OnceLock<Option<Arc<Array2<f64>>>>,
3387}
3388
3389impl CoefficientTransformOperator {
3390 const MATERIALIZE_MAX_BYTES: usize = 1024 * 1024 * 1024;
3393
3394 pub fn new(inner: DenseDesignMatrix, transform: Array2<f64>) -> Result<Self, String> {
3395 let p_inner = inner.ncols();
3396 if transform.nrows() != p_inner {
3397 return Err(format!(
3398 "CoefficientTransformOperator: inner has {} cols but transform has {} rows",
3399 p_inner,
3400 transform.nrows(),
3401 ));
3402 }
3403 let n = inner.nrows();
3404 let p_out = transform.ncols();
3405 Ok(Self {
3406 inner,
3407 transform: Arc::new(transform),
3408 n,
3409 p_out,
3410 materialized: OnceLock::new(),
3411 })
3412 }
3413
3414 fn materialized_combined(&self) -> Option<&Array2<f64>> {
3419 if let Some(slot) = self.materialized.get() {
3420 return slot.as_ref().map(|a| a.as_ref());
3421 }
3422 if self.inner.is_operator_backed() {
3423 if self.materialized.set(None).is_err() {
3424 return self
3425 .materialized
3426 .get()
3427 .and_then(|opt| opt.as_ref().map(|a| a.as_ref()));
3428 }
3429 return None;
3430 }
3431 let bytes = self
3432 .n
3433 .checked_mul(self.p_out)
3434 .and_then(|cells| cells.checked_mul(std::mem::size_of::<f64>()));
3435 let computed = match bytes {
3436 Some(b) if b <= Self::MATERIALIZE_MAX_BYTES => self
3437 .inner
3438 .as_dense_ref()
3439 .map(|x| Arc::new(fast_ab(x, &self.transform))),
3440 _ => None,
3441 };
3442 if self.materialized.set(computed).is_err() {
3443 return self
3444 .materialized
3445 .get()
3446 .and_then(|opt| opt.as_ref().map(|a| a.as_ref()));
3447 }
3448 self.materialized
3449 .get()
3450 .and_then(|opt| opt.as_ref().map(|a| a.as_ref()))
3451 }
3452}
3453
3454impl LinearOperator for CoefficientTransformOperator {
3455 fn nrows(&self) -> usize {
3456 self.n
3457 }
3458 fn ncols(&self) -> usize {
3459 self.p_out
3460 }
3461 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
3462 if let Some(combined) = self.materialized_combined() {
3463 return fast_av(combined, vector);
3464 }
3465 let tv = fast_av(&self.transform, vector);
3466 self.inner.apply(&tv)
3467 }
3468 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
3469 if let Some(combined) = self.materialized_combined() {
3470 return fast_atv(combined, vector);
3471 }
3472 let xtv = self.inner.apply_transpose(vector);
3473 fast_atv(&self.transform, &xtv)
3474 }
3475 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
3476 certify_signed_weights("CoefficientTransformOperator::diag_xtw_x", weights, self.n)?;
3477 if let Some(combined) = self.materialized_combined() {
3478 let mut xtwx = Array2::<f64>::zeros((self.p_out, self.p_out));
3479 stream_weighted_crossprod_into(
3480 combined,
3481 weights,
3482 &mut xtwx,
3483 CrossprodStructure::Full,
3484 CrossprodAccum::Replace,
3485 effective_global_parallelism(),
3486 );
3487 return Ok(xtwx);
3488 }
3489 let inner_xtwx = self.inner.diag_xtw_x(weights)?;
3490 let tmp = fast_ab(&self.transform.t().to_owned(), &inner_xtwx);
3492 Ok(fast_ab(&tmp, &self.transform))
3493 }
3494}
3495
3496impl DenseDesignOperator for CoefficientTransformOperator {
3497 fn as_dense_ref(&self) -> Option<&Array2<f64>> {
3505 self.materialized_combined()
3506 }
3507
3508 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
3509 self.inner.materialization_policy()
3510 }
3511
3512 fn to_dense(&self) -> Array2<f64> {
3513 if let Some(combined) = self.materialized_combined() {
3514 return combined.clone();
3515 }
3516 let x = self.inner.to_dense();
3517 fast_ab(&x, &self.transform)
3518 }
3519 fn row_chunk_into(
3520 &self,
3521 rows: Range<usize>,
3522 mut out: ArrayViewMut2<'_, f64>,
3523 ) -> Result<(), MatrixMaterializationError> {
3524 if out.nrows() != rows.end - rows.start || out.ncols() != self.p_out {
3525 return Err(MatrixMaterializationError::MissingRowChunk {
3526 context: "CoefficientTransformOperator::row_chunk_into shape mismatch",
3527 });
3528 }
3529 if let Some(combined) = self.materialized_combined() {
3530 out.assign(&combined.slice(s![rows, ..]));
3531 return Ok(());
3532 }
3533 let chunk = self.inner.try_row_chunk(rows)?;
3534 out.assign(&fast_ab(&chunk, &self.transform));
3535 Ok(())
3536 }
3537}
3538
3539pub struct ConditionedDesign {
3568 inner: DesignMatrix,
3569 columns: Vec<(usize, f64, f64)>,
3571}
3572
3573impl ConditionedDesign {
3574 pub fn new(inner: DesignMatrix, columns: Vec<(usize, f64, f64)>) -> Self {
3575 Self { inner, columns }
3576 }
3577}
3578
3579impl LinearOperator for ConditionedDesign {
3580 fn nrows(&self) -> usize {
3581 self.inner.nrows()
3582 }
3583
3584 fn ncols(&self) -> usize {
3585 self.inner.ncols()
3586 }
3587
3588 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
3590 let mut scaled = vector.clone();
3591 let mut shift = 0.0;
3592 for &(j, mean, scale) in &self.columns {
3593 scaled[j] /= scale;
3594 shift += mean * scaled[j];
3595 }
3596 let mut result = self.inner.apply(&scaled);
3597 if shift != 0.0 {
3598 result.mapv_inplace(|v| v - shift);
3599 }
3600 result
3601 }
3602
3603 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
3605 let mut result = self.inner.apply_transpose(vector);
3606 let sum_u: f64 = vector.iter().sum();
3607 for &(j, mean, scale) in &self.columns {
3608 result[j] = (result[j] - mean * sum_u) / scale;
3609 }
3610 result
3611 }
3612
3613 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
3615 certify_signed_weights("ConditionedDesign::diag_xtw_x", weights, self.nrows())?;
3616 let mut base = self.inner.diag_xtw_x(weights)?;
3617 if self.columns.is_empty() {
3618 return Ok(base);
3619 }
3620 let p = base.ncols();
3621 let sum_w: f64 = weights.sum();
3622 let cw = self.inner.apply_transpose(weights);
3623
3624 let mut a = vec![1.0_f64; p];
3626 let mut d = vec![0.0_f64; p];
3627 for &(j, mean, scale) in &self.columns {
3628 a[j] = 1.0 / scale;
3629 d[j] = mean / scale;
3630 }
3631
3632 for i in 0..p {
3634 for j in i..p {
3635 let val = a[i] * base[[i, j]] * a[j] - a[i] * cw[i] * d[j] - d[i] * cw[j] * a[j]
3636 + sum_w * d[i] * d[j];
3637 base[[i, j]] = val;
3638 base[[j, i]] = val;
3639 }
3640 }
3641 Ok(base)
3642 }
3643
3644 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
3646 certify_signed_weights("ConditionedDesign::diag_gram", weights, self.nrows())?;
3647 let mut result = self.inner.diag_gram(weights)?;
3648 if self.columns.is_empty() {
3649 return Ok(result);
3650 }
3651 let sum_w: f64 = weights.sum();
3652 let cw = self.inner.apply_transpose(weights);
3653 for &(j, mean, scale) in &self.columns {
3654 let a_j = 1.0 / scale;
3655 let d_j = mean / scale;
3656 result[j] = a_j * a_j * result[j] - 2.0 * a_j * cw[j] * d_j + sum_w * d_j * d_j;
3657 }
3658 Ok(result)
3659 }
3660
3661 fn uses_matrix_free_pcg(&self) -> bool {
3662 match &self.inner {
3663 DesignMatrix::Dense(_) => true,
3664 DesignMatrix::Sparse(_) => false,
3665 }
3666 }
3667}
3668
3669impl DenseDesignOperator for ConditionedDesign {
3670 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
3671 self.inner.materialization_policy()
3672 }
3673
3674 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
3676 if y.len() != self.nrows() {
3677 return Err(format!(
3678 "ConditionedDesign::compute_xtwy response length mismatch: y={}, nrows={}",
3679 y.len(),
3680 self.nrows()
3681 ));
3682 }
3683 certify_signed_weights("ConditionedDesign::compute_xtwy", weights, self.nrows())?;
3684 let mut result = self.inner.compute_xtwy(weights, y)?;
3685 if self.columns.is_empty() {
3686 return Ok(result);
3687 }
3688 let sum_wy: f64 = weights.iter().zip(y.iter()).map(|(&w, &yi)| w * yi).sum();
3689 for &(j, mean, scale) in &self.columns {
3690 result[j] = (result[j] - mean * sum_wy) / scale;
3691 }
3692 Ok(result)
3693 }
3694
3695 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
3697 if self.columns.is_empty() {
3698 return self.inner.quadratic_form_diag(middle);
3699 }
3700 let p = self.ncols();
3701 let mut d = Array1::zeros(p);
3702 for &(j, mean, scale) in &self.columns {
3703 d[j] = mean / scale;
3704 }
3705
3706 let mut ama = middle.clone();
3708 for &(j, _, scale) in &self.columns {
3709 for k in 0..p {
3710 ama[[j, k]] /= scale;
3711 ama[[k, j]] /= scale;
3712 }
3713 }
3714
3715 let md = middle.dot(&d);
3717 let mut amd = md;
3718 for &(j, _, scale) in &self.columns {
3719 amd[j] /= scale;
3720 }
3721
3722 let dtmd: f64 = d.dot(&middle.dot(&d));
3723
3724 let mut result = self.inner.quadratic_form_diag(&ama)?;
3725 let x_amd = self.inner.apply(&amd);
3726 for i in 0..result.len() {
3727 result[i] = (result[i] - 2.0 * x_amd[i] + dtmd).max(0.0);
3728 }
3729 Ok(result)
3730 }
3731
3732 fn row_chunk_into(
3733 &self,
3734 rows: Range<usize>,
3735 mut out: ArrayViewMut2<'_, f64>,
3736 ) -> Result<(), MatrixMaterializationError> {
3737 if out.nrows() != rows.end - rows.start || out.ncols() != self.ncols() {
3738 return Err(MatrixMaterializationError::MissingRowChunk {
3739 context: "ConditionedDesign::row_chunk_into shape mismatch",
3740 });
3741 }
3742 let mut chunk = self.inner.try_row_chunk(rows)?;
3743 for &(j, mean, scale) in &self.columns {
3744 chunk.column_mut(j).mapv_inplace(|v| (v - mean) / scale);
3745 }
3746 out.assign(&chunk);
3747 Ok(())
3748 }
3749
3750 fn to_dense(&self) -> Array2<f64> {
3751 let mut dense = self.inner.to_dense();
3752 for &(j, mean, scale) in &self.columns {
3753 dense.column_mut(j).mapv_inplace(|v| (v - mean) / scale);
3754 }
3755 dense
3756 }
3757}
3758
3759#[derive(Clone)]
3769pub enum DesignMatrix {
3770 Dense(DenseDesignMatrix),
3771 Sparse(SparseDesignMatrix),
3772}
3773
3774impl std::fmt::Debug for DesignMatrix {
3775 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3776 match self {
3777 Self::Dense(m) => write!(f, "DesignMatrix::Dense({}x{})", m.nrows(), m.ncols()),
3778 Self::Sparse(s) => write!(f, "DesignMatrix::Sparse({}x{})", s.nrows(), s.ncols()),
3779 }
3780 }
3781}
3782
3783mod symmetric;
3785pub use symmetric::*;
3786pub trait FactorizedSystem: Send + Sync {
3788 fn solve(&self, rhs: &Array1<f64>) -> Result<Array1<f64>, String>;
3790
3791 fn solvemulti(&self, rhs: &Array2<f64>) -> Result<Array2<f64>, String>;
3793
3794 fn logdet(&self) -> f64;
3796}
3797
3798pub trait LinearOperator {
3799 fn nrows(&self) -> usize;
3800 fn ncols(&self) -> usize;
3801 fn apply(&self, vector: &Array1<f64>) -> Array1<f64>;
3802 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64>;
3803 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String>;
3804
3805 fn xt_diag_x_signed_op(
3811 &self,
3812 weights: FiniteSignedWeightsView<'_>,
3813 ) -> Result<Array2<f64>, String> {
3814 self.diag_xtw_x(&weights.view().to_owned())
3815 }
3816
3817 fn xt_diag_x_psd_op(&self, weights: PsdWeightsView<'_>) -> Result<SymmetricMatrix, String> {
3822 FiniteSignedWeightsView::try_new(weights.view())
3823 .map_err(|reason| format!("LinearOperator::xt_diag_x_psd_op: {reason}"))?;
3824 let xtwx = self.diag_xtw_x(&weights.view().to_owned())?;
3825 Ok(SymmetricMatrix::Dense(xtwx))
3826 }
3827
3828 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
3829 let xtwx = self.diag_xtw_x(weights)?;
3830 Ok(Array1::from_iter((0..self.ncols()).map(|j| xtwx[[j, j]])))
3831 }
3832 fn apply_weighted_normal(
3833 &self,
3834 weights: FiniteSignedWeightsView<'_>,
3835 vector: &Array1<f64>,
3836 penalty: Option<&Array2<f64>>,
3837 ridge: f64,
3838 ) -> Array1<f64> {
3839 assert_eq!(
3840 weights.len(),
3841 self.nrows(),
3842 "apply_weighted_normal weight length mismatch"
3843 );
3844 assert_eq!(
3845 vector.len(),
3846 self.ncols(),
3847 "apply_weighted_normal vector length mismatch"
3848 );
3849 let weights = weights.view();
3850 let xv = self.apply(vector);
3851 let mut weighted_xv = xv;
3852 for i in 0..weighted_xv.len() {
3853 weighted_xv[i] *= weights[i];
3854 }
3855 let mut out = self.apply_transpose(&weighted_xv);
3856 if let Some(pen) = penalty {
3857 out += &fast_av(pen, vector);
3858 }
3859 if ridge > 0.0 {
3860 out.scaled_add(ridge, vector);
3862 }
3863 out
3864 }
3865 fn uses_matrix_free_pcg(&self) -> bool {
3866 false
3867 }
3868 fn solve_system_matrix_free_pcg_try(
3869 &self,
3870 weights: &Array1<f64>,
3871 rhs: &Array1<f64>,
3872 penalty: Option<&Array2<f64>>,
3873 baseridge: f64,
3874 ) -> Result<Array1<f64>, String> {
3875 self.solve_system_matrix_free_pcg_with_info_try(weights, rhs, penalty, baseridge)
3876 .map(|(solution, _)| solution)
3877 }
3878 fn solve_system_matrix_free_pcg_with_info_try(
3879 &self,
3880 weights: &Array1<f64>,
3881 rhs: &Array1<f64>,
3882 penalty: Option<&Array2<f64>>,
3883 baseridge: f64,
3884 ) -> Result<(Array1<f64>, PcgSolveInfo), String> {
3885 if rhs.len() != self.ncols() {
3886 return Err(format!(
3887 "solve_system_matrix_free_pcg rhs dimension mismatch: rhs length {} != ncols {}",
3888 rhs.len(),
3889 self.ncols()
3890 ));
3891 }
3892 if !self.uses_matrix_free_pcg() {
3893 return Err("matrix-free PCG is only enabled for eligible operator types".to_string());
3894 }
3895 if let Some(pen) = penalty
3896 && (pen.nrows() != self.ncols() || pen.ncols() != self.ncols())
3897 {
3898 return Err(format!(
3899 "solve_system_matrix_free_pcg penalty shape mismatch: got {}x{}, expected {}x{}",
3900 pen.nrows(),
3901 pen.ncols(),
3902 self.ncols(),
3903 self.ncols()
3904 ));
3905 }
3906 let p = self.ncols();
3907 let finite_weights = certify_signed_weights(
3908 "solve_system_matrix_free_pcg_with_info_try",
3909 weights,
3910 self.nrows(),
3911 )?;
3912 if !(baseridge.is_finite() && baseridge >= 0.0) {
3913 return Err(format!(
3914 "matrix-free PCG ridge must be finite and non-negative, got {baseridge:?}"
3915 ));
3916 }
3917 let normal_op = PenalizedWeightedNormalOperator {
3918 operator: self,
3919 weights,
3920 finite_weights,
3921 penalty,
3922 ridge: baseridge,
3923 };
3924 let preconditioner = normal_op.jacobi_preconditioner()?;
3925 let attempt_started = std::time::Instant::now();
3926 let (solution, info) = crate::utils::solve_spd_pcg_with_info(
3927 |v| normal_op.apply(v),
3928 rhs,
3929 &preconditioner,
3930 MATRIX_FREE_PCG_REL_TOL,
3931 MATRIX_FREE_PCG_MAX_ITER.max(4 * p),
3932 )
3933 .ok_or_else(|| {
3934 format!("matrix-free PCG broke down for explicitly requested ridge {baseridge:.3e}")
3935 })?;
3936 if !solution.iter().all(|value| value.is_finite()) {
3937 return Err("matrix-free PCG produced a non-finite solution".to_string());
3938 }
3939 log::debug!(
3940 "[matrix-free PCG] solved: p={p} ridge={baseridge:.3e} iters={} converged={} rel_resid={:.3e} elapsed={:.3}s",
3941 info.iterations,
3942 info.converged,
3943 info.relative_residual_norm,
3944 attempt_started.elapsed().as_secs_f64(),
3945 );
3946 Ok((solution, info))
3947 }
3948 fn factorize_system(
3949 &self,
3950 weights: &Array1<f64>,
3951 penalty: Option<&Array2<f64>>,
3952 ) -> Result<Box<dyn FactorizedSystem>, String> {
3953 let mut system = self.diag_xtw_x(weights)?;
3954 if let Some(pen) = penalty {
3955 if pen.nrows() != system.nrows() || pen.ncols() != system.ncols() {
3956 return Err(format!(
3957 "factorize_system penalty shape mismatch: got {}x{}, expected {}x{}",
3958 pen.nrows(),
3959 pen.ncols(),
3960 system.nrows(),
3961 system.ncols()
3962 ));
3963 }
3964 system += pen;
3965 }
3966 let factor = crate::utils::StableSolver::new()
3967 .factorize(&system)
3968 .map_err(|e| format!("factorize_system failed: {e:?}"))?;
3969 Ok(Box::new(factor))
3970 }
3971 fn solve_system(
3972 &self,
3973 weights: &Array1<f64>,
3974 rhs: &Array1<f64>,
3975 penalty: Option<&Array2<f64>>,
3976 ) -> Result<Array1<f64>, String> {
3977 self.solve_systemwith_policy(weights, rhs, penalty, 0.0, RidgePolicy::solver_only())
3978 }
3979 fn solve_systemwith_policy(
3980 &self,
3981 weights: &Array1<f64>,
3982 rhs: &Array1<f64>,
3983 penalty: Option<&Array2<f64>>,
3984 ridge_floor: f64,
3985 ridge_policy: RidgePolicy,
3986 ) -> Result<Array1<f64>, String> {
3987 if rhs.len() != self.ncols() {
3988 return Err(format!(
3989 "solve_systemwith_policy rhs dimension mismatch: rhs length {} != ncols {}",
3990 rhs.len(),
3991 self.ncols()
3992 ));
3993 }
3994 if !(ridge_floor.is_finite() && ridge_floor >= 0.0) {
3995 return Err(format!(
3996 "solve_systemwith_policy ridge floor must be finite and non-negative, got {ridge_floor:?}"
3997 ));
3998 }
3999 let ridge = ridge_floor;
4000 if self.uses_matrix_free_pcg() && self.ncols() >= MATRIX_FREE_PCG_MIN_P {
4004 return self.solve_system_matrix_free_pcg_try(weights, rhs, penalty, ridge);
4005 }
4006 let mut system = self.diag_xtw_x(weights)?;
4007 if let Some(pen) = penalty {
4008 if pen.nrows() != system.nrows() || pen.ncols() != system.ncols() {
4009 return Err(format!(
4010 "solve_systemwith_policy penalty shape mismatch: got {}x{}, expected {}x{}",
4011 pen.nrows(),
4012 pen.ncols(),
4013 system.nrows(),
4014 system.ncols()
4015 ));
4016 }
4017 system += pen;
4018 }
4019 if ridge > 0.0 {
4020 for diagonal in 0..system.nrows() {
4021 system[[diagonal, diagonal]] += ridge;
4022 }
4023 }
4024 let factor = crate::utils::StableSolver::new()
4025 .factorize(&system)
4026 .map_err(|error| {
4027 format!(
4028 "solve_systemwith_policy ({ridge_policy:?}) exact factorization failed at ridge {ridge:.3e}: {error:?}"
4029 )
4030 })?;
4031 let mut solution = rhs.clone();
4032 let mut solution_matrix = crate::faer_ndarray::array1_to_col_matmut(&mut solution);
4033 factor.solve_in_place(solution_matrix.as_mut());
4034 if solution.iter().all(|value| value.is_finite()) {
4035 Ok(solution)
4036 } else {
4037 Err("solve_systemwith_policy produced a non-finite solution".to_string())
4038 }
4039 }
4040}
4041
4042impl LinearOperator for DesignMatrix {
4043 fn uses_matrix_free_pcg(&self) -> bool {
4044 match self {
4045 Self::Dense(matrix) => matrix.uses_matrix_free_pcg(),
4046 Self::Sparse(_) => false,
4047 }
4048 }
4049
4050 fn nrows(&self) -> usize {
4051 match self {
4052 Self::Dense(matrix) => matrix.nrows(),
4053 Self::Sparse(matrix) => matrix.nrows(),
4054 }
4055 }
4056
4057 fn ncols(&self) -> usize {
4058 match self {
4059 Self::Dense(matrix) => matrix.ncols(),
4060 Self::Sparse(matrix) => matrix.ncols(),
4061 }
4062 }
4063
4064 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
4065 match self {
4066 Self::Dense(matrix) => matrix.apply(vector),
4067 Self::Sparse(matrix) => {
4068 let mut output = Array1::<f64>::zeros(matrix.nrows());
4069 let (symbolic, values) = matrix.parts();
4070 let col_ptr = symbolic.col_ptr();
4071 let row_idx = symbolic.row_idx();
4072 for col in 0..matrix.ncols() {
4073 let start = col_ptr[col];
4074 let end = col_ptr[col + 1];
4075 let x = vector[col];
4076 for idx in start..end {
4077 let row = row_idx[idx];
4078 output[row] += values[idx] * x;
4079 }
4080 }
4081 output
4082 }
4083 }
4084 }
4085
4086 fn apply_weighted_normal(
4087 &self,
4088 weights: FiniteSignedWeightsView<'_>,
4089 vector: &Array1<f64>,
4090 penalty: Option<&Array2<f64>>,
4091 ridge: f64,
4092 ) -> Array1<f64> {
4093 assert_eq!(
4094 weights.len(),
4095 self.nrows(),
4096 "DesignMatrix::apply_weighted_normal weight length mismatch"
4097 );
4098 assert_eq!(
4099 vector.len(),
4100 self.ncols(),
4101 "DesignMatrix::apply_weighted_normal vector length mismatch"
4102 );
4103 let weights_view = weights.view();
4104 match self {
4105 Self::Dense(matrix) => matrix.apply_weighted_normal(weights, vector, penalty, ridge),
4106 Self::Sparse(_) => {
4107 let sparse = self
4108 .as_sparse()
4109 .expect("DesignMatrix::Sparse must expose sparse view");
4110 let mut out = if let Some(csr) = sparse.to_csr_arc() {
4111 let sym = csr.symbolic();
4112 let row_ptr = sym.row_ptr();
4113 let col_idx = sym.col_idx();
4114 let vals = csr.val();
4115 let mut fused = Array1::<f64>::zeros(self.ncols());
4116 for i in 0..self.nrows() {
4117 let wi = weights_view[i];
4118 if wi == 0.0 {
4119 continue;
4120 }
4121 let start = row_ptr[i];
4122 let end = row_ptr[i + 1];
4123 let mut row_dot = 0.0_f64;
4124 for ptr in start..end {
4125 row_dot += vals[ptr] * vector[col_idx[ptr]];
4126 }
4127 if row_dot == 0.0 {
4128 continue;
4129 }
4130 let scaled = wi * row_dot;
4131 for ptr in start..end {
4132 fused[col_idx[ptr]] += vals[ptr] * scaled;
4133 }
4134 }
4135 fused
4136 } else {
4137 let xv = self.apply(vector);
4138 let mut weighted_xv = xv;
4139 for i in 0..weighted_xv.len() {
4140 weighted_xv[i] *= weights_view[i];
4141 }
4142 self.apply_transpose(&weighted_xv)
4143 };
4144 if let Some(pen) = penalty {
4145 out += &fast_av(pen, vector);
4146 }
4147 if ridge > 0.0 {
4148 for j in 0..out.len() {
4149 out[j] += ridge * vector[j];
4150 }
4151 }
4152 out
4153 }
4154 }
4155 }
4156
4157 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
4158 match self {
4159 Self::Dense(matrix) => matrix.apply_transpose(vector),
4160 Self::Sparse(matrix) => {
4161 let mut output = Array1::<f64>::zeros(matrix.ncols());
4162 let (symbolic, values) = matrix.parts();
4163 let col_ptr = symbolic.col_ptr();
4164 let row_idx = symbolic.row_idx();
4165 for col in 0..matrix.ncols() {
4166 let mut acc = 0.0;
4167 let start = col_ptr[col];
4168 let end = col_ptr[col + 1];
4169 for idx in start..end {
4170 let row = row_idx[idx];
4171 acc += values[idx] * vector[row];
4172 }
4173 output[col] = acc;
4174 }
4175 output
4176 }
4177 }
4178 }
4179
4180 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
4181 certify_signed_weights("DesignMatrix::diag_xtw_x", weights, self.nrows())?;
4182 let p = self.ncols();
4183 match self {
4184 Self::Dense(x) => x.diag_xtw_x(weights),
4185 Self::Sparse(xs) => {
4186 let n = self.nrows();
4209 let nnz_x = xs.as_ref().val().len();
4210 let avg_nnz_row = if n > 0 { nnz_x / n } else { p };
4211 let dense_regime = 4 * avg_nnz_row >= p;
4212 if dense_regime {
4213 let mut xtwx = Array2::<f64>::zeros((p, p));
4214 if let Ok(xd) =
4218 xs.try_to_dense_governed("DesignMatrix::diag_xtw_x dense sparse route")
4219 {
4220 stream_weighted_crossprod_into(
4221 &**xd,
4222 weights,
4223 &mut xtwx,
4224 CrossprodStructure::Full,
4225 CrossprodAccum::Replace,
4226 effective_global_parallelism(),
4227 );
4228 } else {
4229 let (symbolic, values) = xs.parts();
4230 streaming_sparse_csc_xt_diag_x(
4231 symbolic.col_ptr(),
4232 symbolic.row_idx(),
4233 values,
4234 n,
4235 p,
4236 weights.view(),
4237 &mut xtwx,
4238 );
4239 }
4240 return Ok(xtwx);
4241 }
4242 let csr = xs
4243 .to_csr_arc()
4244 .ok_or_else(|| "failed to obtain CSR view in xt_diag_x".to_string())?;
4245 let sym = csr.symbolic();
4246 Ok(sparse_csr_weighted_xtwx(
4247 sym.row_ptr(),
4248 sym.col_idx(),
4249 csr.val(),
4250 n,
4251 p,
4252 weights.view(),
4253 ))
4254 }
4255 }
4256 }
4257
4258 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
4259 certify_signed_weights("DesignMatrix::diag_gram", weights, self.nrows())?;
4260 let p = self.ncols();
4261 match self {
4262 Self::Dense(x) => x.diag_gram(weights),
4263 Self::Sparse(xs) => {
4264 let csr = xs
4265 .to_csr_arc()
4266 .ok_or_else(|| "failed to obtain CSR view in diag_gram".to_string())?;
4267 let sym = csr.symbolic();
4268 Ok(sparse_csr_diag_gram(
4269 sym.row_ptr(),
4270 sym.col_idx(),
4271 csr.val(),
4272 self.nrows(),
4273 p,
4274 weights.view(),
4275 ))
4276 }
4277 }
4278 }
4279
4280 fn factorize_system(
4281 &self,
4282 weights: &Array1<f64>,
4283 penalty: Option<&Array2<f64>>,
4284 ) -> Result<Box<dyn FactorizedSystem>, String> {
4285 if weights.len() != self.nrows() {
4286 return Err(format!(
4287 "factorize_system dimension mismatch: weights length {} != nrows {}",
4288 weights.len(),
4289 self.nrows()
4290 ));
4291 }
4292 match self {
4293 Self::Dense(_) => self.factorize_system_dense(weights, penalty),
4294 Self::Sparse(matrix) => {
4295 let system = assemble_sparseweighted_gram_system(matrix, weights, penalty)?;
4296 let factor = crate::sparse_exact::factorize_sparse_spd(&system)
4297 .map_err(|e| format!("factorize_system failed: {e:?}"))?;
4298 Ok(Box::new(factor))
4299 }
4300 }
4301 }
4302}
4303
4304impl DenseDesignOperator for DesignMatrix {
4305 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
4306 match self {
4307 Self::Dense(design) => design.materialization_policy(),
4308 Self::Sparse(_) => None,
4309 }
4310 }
4311
4312 fn compute_xtwy(&self, weights: &Array1<f64>, y: &Array1<f64>) -> Result<Array1<f64>, String> {
4313 if weights.len() != self.nrows() || y.len() != self.nrows() {
4314 return Err(format!(
4315 "compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
4316 weights.len(),
4317 y.len(),
4318 self.nrows()
4319 ));
4320 }
4321 certify_signed_weights("DesignMatrix::compute_xtwy", weights, self.nrows())?;
4322 match self {
4323 Self::Dense(x) => x.compute_xtwy(weights, y),
4324 Self::Sparse(xs) => {
4325 let csr = xs
4326 .as_ref()
4327 .to_row_major()
4328 .map_err(|_| "failed to obtain CSR view in compute_xtwy".to_string())?;
4329 let sym = csr.symbolic();
4330 let row_ptr = sym.row_ptr();
4331 let col_idx = sym.col_idx();
4332 let vals = csr.val();
4333 let mut out = Array1::<f64>::zeros(xs.ncols());
4334 for i in 0..xs.nrows() {
4335 let scaled = weights[i] * y[i];
4336 if scaled == 0.0 {
4337 continue;
4338 }
4339 for idx in row_ptr[i]..row_ptr[i + 1] {
4340 out[col_idx[idx]] += vals[idx] * scaled;
4341 }
4342 }
4343 Ok(out)
4344 }
4345 }
4346 }
4347
4348 fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
4349 if middle.nrows() != self.ncols() || middle.ncols() != self.ncols() {
4350 return Err(format!(
4351 "quadratic_form_diag dimension mismatch: matrix is {}x{}, expected {}x{}",
4352 middle.nrows(),
4353 middle.ncols(),
4354 self.ncols(),
4355 self.ncols()
4356 ));
4357 }
4358
4359 match self {
4360 Self::Dense(xd) => xd.quadratic_form_diag(middle),
4361 Self::Sparse(xs) => {
4362 let csr = xs
4363 .to_csr_arc()
4364 .ok_or_else(|| "quadratic_form_diag: failed to obtain CSR view".to_string())?;
4365 let sym = csr.symbolic();
4366 let row_ptr = sym.row_ptr();
4367 let col_idx = sym.col_idx();
4368 let vals = csr.val();
4369 let mut out = Array1::<f64>::zeros(self.nrows());
4370 for i in 0..xs.nrows() {
4371 let start = row_ptr[i];
4372 let end = row_ptr[i + 1];
4373 let mut acc = 0.0_f64;
4374 for a in start..end {
4375 let j = col_idx[a];
4376 let xij = vals[a];
4377 for b in start..end {
4378 let k = col_idx[b];
4379 let xik = vals[b];
4380 acc += xij * middle[[j, k]] * xik;
4381 }
4382 }
4383 out[i] = acc.max(0.0);
4384 }
4385 Ok(out)
4386 }
4387 }
4388 }
4389
4390 fn row_chunk_into(
4391 &self,
4392 rows: Range<usize>,
4393 out: ArrayViewMut2<'_, f64>,
4394 ) -> Result<(), MatrixMaterializationError> {
4395 if out.nrows() != rows.end - rows.start || out.ncols() != self.ncols() {
4396 return Err(MatrixMaterializationError::MissingRowChunk {
4397 context: "DesignMatrix::row_chunk_into shape mismatch",
4398 });
4399 }
4400 match self {
4401 Self::Dense(matrix) => matrix.row_chunk_into(rows, out),
4402 Self::Sparse(matrix) => matrix.row_chunk_into(rows, out),
4403 }
4404 }
4405
4406 fn to_dense(&self) -> Array2<f64> {
4407 DesignMatrix::to_dense(self)
4408 }
4409}
4410
4411impl LinearOperator for DenseRightProductView<'_> {
4412 fn nrows(&self) -> usize {
4413 self.base.nrows()
4414 }
4415
4416 fn ncols(&self) -> usize {
4417 self.transformed_ncols()
4418 }
4419
4420 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
4421 let rhs;
4422 let v = match (self.second, self.first) {
4423 (None, None) => vector,
4424 (Some(s), None) => {
4425 rhs = fast_av(s, vector);
4426 &rhs
4427 }
4428 (None, Some(f)) => {
4429 rhs = fast_av(f, vector);
4430 &rhs
4431 }
4432 (Some(s), Some(f)) => {
4433 let tmp = fast_av(s, vector);
4434 rhs = fast_av(f, &tmp);
4435 &rhs
4436 }
4437 };
4438 fast_av(self.base, v)
4439 }
4440
4441 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
4442 let mut out = fast_atv(self.base, vector);
4443 if let Some(factor) = self.first {
4444 out = fast_atv(factor, &out);
4445 }
4446 if let Some(factor) = self.second {
4447 out = fast_atv(factor, &out);
4448 }
4449 out
4450 }
4451
4452 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
4453 if weights.len() != self.nrows() {
4454 return Err(format!(
4455 "xt_diag_x dimension mismatch: weights length {} != nrows {}",
4456 weights.len(),
4457 self.nrows()
4458 ));
4459 }
4460 certify_signed_weights("DenseRightProductView::diag_xtw_x", weights, self.nrows())?;
4461 let mut gram = fast_xt_diag_x(self.base, weights);
4462 if let Some(factor) = self.first {
4463 gram = fast_ab(&fast_atb(factor, &gram), factor);
4464 }
4465 if let Some(factor) = self.second {
4466 gram = fast_ab(&fast_atb(factor, &gram), factor);
4467 }
4468 Ok(gram)
4469 }
4470
4471 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
4472 Ok(self.diag_xtw_x(weights)?.diag().to_owned())
4473 }
4474}
4475
4476impl DenseRightProductView<'_> {
4477 pub fn compute_xtwy(
4478 &self,
4479 weights: &Array1<f64>,
4480 y: &Array1<f64>,
4481 ) -> Result<Array1<f64>, String> {
4482 if weights.len() != self.nrows() || y.len() != self.nrows() {
4483 return Err(format!(
4484 "compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
4485 weights.len(),
4486 y.len(),
4487 self.nrows()
4488 ));
4489 }
4490 certify_signed_weights("DenseRightProductView::compute_xtwy", weights, self.nrows())?;
4491 let weighted_xty = dense_transpose_weighted_response(self.base, weights, y, None);
4492 let mut out = weighted_xty;
4493 if let Some(factor) = self.first {
4494 out = fast_atv(factor, &out);
4495 }
4496 if let Some(factor) = self.second {
4497 out = fast_atv(factor, &out);
4498 }
4499 Ok(out)
4500 }
4501
4502 pub fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
4503 let dense = self.materialize();
4504 DesignMatrix::Dense(DenseDesignMatrix::from(dense)).quadratic_form_diag(middle)
4505 }
4506}
4507
4508impl LinearOperator for EmbeddedColumnBlock<'_> {
4509 fn nrows(&self) -> usize {
4510 self.local.nrows()
4511 }
4512
4513 fn ncols(&self) -> usize {
4514 self.total_cols
4515 }
4516
4517 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
4518 fast_av(
4519 self.local,
4520 &vector.slice(ndarray::s![self.global_range.clone()]),
4521 )
4522 }
4523
4524 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
4525 let mut out = Array1::<f64>::zeros(self.total_cols);
4526 out.slice_mut(ndarray::s![self.global_range.clone()])
4527 .assign(&fast_atv(self.local, vector));
4528 out
4529 }
4530
4531 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
4532 if weights.len() != self.nrows() {
4533 return Err(format!(
4534 "xt_diag_x dimension mismatch: weights length {} != nrows {}",
4535 weights.len(),
4536 self.nrows()
4537 ));
4538 }
4539 certify_signed_weights("EmbeddedColumnBlock::diag_xtw_x", weights, self.nrows())?;
4540 let mut out = Array2::<f64>::zeros((self.total_cols, self.total_cols));
4541 let local = fast_xt_diag_x(self.local, weights);
4542 out.slice_mut(ndarray::s![
4543 self.global_range.clone(),
4544 self.global_range.clone()
4545 ])
4546 .assign(&local);
4547 Ok(out)
4548 }
4549
4550 fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
4551 let mut out = Array1::<f64>::zeros(self.total_cols);
4552 let local =
4553 DesignMatrix::Dense(DenseDesignMatrix::from(self.local.clone())).diag_gram(weights)?;
4554 out.slice_mut(ndarray::s![self.global_range.clone()])
4555 .assign(&local);
4556 Ok(out)
4557 }
4558}
4559
4560impl EmbeddedColumnBlock<'_> {
4561 pub fn compute_xtwy(
4562 &self,
4563 weights: &Array1<f64>,
4564 y: &Array1<f64>,
4565 ) -> Result<Array1<f64>, String> {
4566 if weights.len() != self.nrows() || y.len() != self.nrows() {
4567 return Err(format!(
4568 "compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
4569 weights.len(),
4570 y.len(),
4571 self.nrows()
4572 ));
4573 }
4574 certify_signed_weights("EmbeddedColumnBlock::compute_xtwy", weights, self.nrows())?;
4575 let local = dense_transpose_weighted_response(self.local, weights, y, None);
4576 let mut out = Array1::<f64>::zeros(self.total_cols);
4577 out.slice_mut(ndarray::s![self.global_range.clone()])
4578 .assign(&local);
4579 Ok(out)
4580 }
4581
4582 pub fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
4583 let middle_local = middle
4584 .slice(ndarray::s![
4585 self.global_range.clone(),
4586 self.global_range.clone()
4587 ])
4588 .to_owned();
4589 DesignMatrix::Dense(DenseDesignMatrix::from(self.local.clone()))
4590 .quadratic_form_diag(&middle_local)
4591 }
4592}
4593
4594impl DesignMatrix {
4595 fn factorize_system_dense(
4596 &self,
4597 weights: &Array1<f64>,
4598 penalty: Option<&Array2<f64>>,
4599 ) -> Result<Box<dyn FactorizedSystem>, String> {
4600 let mut system = self.diag_xtw_x(weights)?;
4601 if let Some(pen) = penalty {
4602 if pen.nrows() != system.nrows() || pen.ncols() != system.ncols() {
4603 return Err(format!(
4604 "factorize_system penalty shape mismatch: got {}x{}, expected {}x{}",
4605 pen.nrows(),
4606 pen.ncols(),
4607 system.nrows(),
4608 system.ncols()
4609 ));
4610 }
4611 system += pen;
4612 }
4613 let factor = crate::utils::StableSolver::new()
4614 .factorize(&system)
4615 .map_err(|e| format!("factorize_system failed: {e:?}"))?;
4616 Ok(Box::new(factor))
4617 }
4618}
4619
4620fn assemble_sparseweighted_gram_system(
4621 matrix: &SparseDesignMatrix,
4622 weights: &Array1<f64>,
4623 penalty: Option<&Array2<f64>>,
4624) -> Result<SparseColMat<usize, f64>, String> {
4625 certify_signed_weights(
4626 "assemble_sparseweighted_gram_system",
4627 weights,
4628 matrix.nrows(),
4629 )?;
4630 let csr = matrix
4631 .to_csr_arc()
4632 .ok_or_else(|| "failed to obtain CSR view in factorize_system".to_string())?;
4633 let sym = csr.symbolic();
4634 let row_ptr = sym.row_ptr();
4635 let col_idx = sym.col_idx();
4636 let vals = csr.val();
4637 let p = matrix.ncols();
4638 let mut upper = BTreeMap::<(usize, usize), f64>::new();
4639
4640 for i in 0..csr.nrows() {
4641 let wi = weights[i];
4642 if wi == 0.0 {
4643 continue;
4644 }
4645 let start = row_ptr[i];
4646 let end = row_ptr[i + 1];
4647 for a_ptr in start..end {
4648 let a = col_idx[a_ptr];
4649 let xa = vals[a_ptr];
4650 for b_ptr in a_ptr..end {
4651 let b = col_idx[b_ptr];
4652 let xb = vals[b_ptr];
4653 let key = if a <= b { (a, b) } else { (b, a) };
4654 *upper.entry(key).or_insert(0.0) += wi * xa * xb;
4655 }
4656 }
4657 }
4658
4659 if let Some(pen) = penalty {
4660 if pen.nrows() != p || pen.ncols() != p {
4661 return Err(format!(
4662 "factorize_system penalty shape mismatch: got {}x{}, expected {}x{}",
4663 pen.nrows(),
4664 pen.ncols(),
4665 p,
4666 p
4667 ));
4668 }
4669 for i in 0..p {
4670 for j in i..p {
4671 let value = pen[[i, j]];
4672 if value != 0.0 {
4673 *upper.entry((i, j)).or_insert(0.0) += value;
4674 }
4675 }
4676 }
4677 }
4678
4679 let mut triplets = Vec::with_capacity(upper.len());
4680 for ((row, col), value) in upper {
4681 if value != 0.0 {
4682 triplets.push(Triplet::new(row, col, value));
4683 }
4684 }
4685 SparseColMat::try_new_from_triplets(p, p, &triplets)
4686 .map_err(|_| "failed to build sparse penalized system".to_string())
4687}
4688
4689impl DesignMatrix {
4690 pub fn hstack(blocks: Vec<DesignMatrix>) -> Result<Self, String> {
4696 if blocks.is_empty() {
4697 return Err("DesignMatrix::hstack requires at least one block".to_string());
4698 }
4699 if blocks.len() == 1 {
4700 return Ok(blocks.into_iter().next().expect("non-empty block list"));
4701 }
4702 let operator =
4703 BlockDesignOperator::new(blocks.into_iter().map(DesignBlock::from).collect())?;
4704 Ok(Self::Dense(DenseDesignMatrix::from(Arc::new(operator))))
4705 }
4706
4707 pub fn nrows(&self) -> usize {
4708 <Self as LinearOperator>::nrows(self)
4709 }
4710
4711 pub fn ncols(&self) -> usize {
4712 <Self as LinearOperator>::ncols(self)
4713 }
4714
4715 pub fn try_row_chunk(
4721 &self,
4722 rows: Range<usize>,
4723 ) -> Result<Array2<f64>, MatrixMaterializationError> {
4724 match self {
4725 Self::Dense(matrix) => matrix.try_row_chunk(rows),
4726 Self::Sparse(matrix) => {
4727 let csr =
4728 matrix
4729 .to_csr_arc()
4730 .ok_or(MatrixMaterializationError::MissingRowChunk {
4731 context: "DesignMatrix::try_row_chunk: failed to obtain CSR view",
4732 })?;
4733 let sym = csr.symbolic();
4734 let row_ptr = sym.row_ptr();
4735 let col_idx = sym.col_idx();
4736 let vals = csr.val();
4737 let chunk_rows = rows.end - rows.start;
4738 let ncols = self.ncols();
4739 let mut out = Array2::<f64>::zeros((chunk_rows, ncols));
4740 for (local_row, row) in rows.enumerate() {
4741 for ptr in row_ptr[row]..row_ptr[row + 1] {
4742 out[[local_row, col_idx[ptr]]] = vals[ptr];
4743 }
4744 }
4745 Ok(out)
4746 }
4747 }
4748 }
4749
4750 pub fn row_chunk_into(
4756 &self,
4757 rows: Range<usize>,
4758 out: ArrayViewMut2<'_, f64>,
4759 ) -> Result<(), MatrixMaterializationError> {
4760 <Self as DenseDesignOperator>::row_chunk_into(self, rows, out)
4761 }
4762
4763 pub fn try_to_dense_governed(
4769 &self,
4770 context: &'static str,
4771 ) -> Result<Governed<Array2<f64>>, MatrixMaterializationError> {
4772 self.try_to_dense_governed_with_policy(
4773 &ResourcePolicy::default_library().material_policy(),
4774 context,
4775 )
4776 }
4777
4778 pub fn try_to_dense_governed_with_policy(
4781 &self,
4782 policy: &MaterializationPolicy,
4783 context: &'static str,
4784 ) -> Result<Governed<Array2<f64>>, MatrixMaterializationError> {
4785 governed_dense_operator_to_dense_by_chunks(self, policy, context)
4786 }
4787
4788 pub fn try_to_dense_by_chunks(&self, context: &str) -> Result<Array2<f64>, String> {
4789 let n = self.nrows();
4790 let p = self.ncols();
4791 let chunk_rows = dense_materialization_chunk_rows(n, p);
4792 let mut out = Array2::<f64>::zeros((n, p));
4793 for start in (0..n).step_by(chunk_rows) {
4794 let end = (start + chunk_rows).min(n);
4795 let slice = out.slice_mut(s![start..end, ..]);
4796 self.row_chunk_into(start..end, slice)
4797 .map_err(|err| format!("{context}: failed to materialize row chunk: {err}"))?;
4798 }
4799 Ok(out)
4800 }
4801
4802 pub fn try_to_dense_by_chunks_budgeted(
4808 &self,
4809 context: &str,
4810 max_bytes: usize,
4811 ) -> Result<Array2<f64>, String> {
4812 let n = self.nrows();
4813 let p = self.ncols();
4814 let dense_bytes = checked_dense_nbytes(n, p, context)?;
4815 if dense_bytes > max_bytes {
4816 let gib = dense_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
4817 let cap_gib = max_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
4818 return Err(MatrixError::DensificationRefused {
4819 reason: format!(
4820 "{context}: refusing to densify {n}x{p} (~{gib:.2} GiB, cap ~{cap_gib:.2} GiB)"
4821 ),
4822 }
4823 .into());
4824 }
4825 self.try_to_dense_by_chunks(context)
4826 }
4827
4828 pub fn dot_row(&self, row: usize, beta: &Array1<f64>) -> f64 {
4831 self.dot_row_view(row, beta.view())
4832 }
4833
4834 pub fn dot_row_view(&self, row: usize, beta: ArrayView1<'_, f64>) -> f64 {
4835 assert_eq!(
4836 beta.len(),
4837 self.ncols(),
4838 "DesignMatrix::dot_row_view length mismatch: beta={}, ncols={}",
4839 beta.len(),
4840 self.ncols()
4841 );
4842 match self {
4843 Self::Dense(matrix) => {
4844 if let Some(dense) = matrix.as_dense_ref() {
4845 dense.row(row).dot(&beta)
4846 } else {
4847 matrix
4848 .try_row_chunk(row..row + 1)
4849 .expect("DesignMatrix::dot_row_view: try_row_chunk must succeed")
4850 .row(0)
4851 .dot(&beta)
4852 }
4853 }
4854 Self::Sparse(matrix) => {
4855 let csr = matrix
4863 .to_csr_arc()
4864 .expect("DesignMatrix::dot_row: failed to obtain CSR view");
4865 let sym = csr.symbolic();
4866 let row_ptr = sym.row_ptr();
4867 let col_idx = sym.col_idx();
4868 let vals = csr.val();
4869 let mut out = 0.0;
4870 for ptr in row_ptr[row]..row_ptr[row + 1] {
4871 out += vals[ptr] * beta[col_idx[ptr]];
4872 }
4873 out
4874 }
4875 }
4876 }
4877
4878 pub fn axpy_row_into(
4880 &self,
4881 row: usize,
4882 alpha: f64,
4883 out: &mut ArrayViewMut1<'_, f64>,
4884 ) -> Result<(), String> {
4885 self.axpy_row_into_impl(row, alpha, out, false, "axpy_row_into")
4886 }
4887
4888 pub fn squared_axpy_row_into(
4891 &self,
4892 row: usize,
4893 alpha: f64,
4894 out: &mut ArrayViewMut1<'_, f64>,
4895 ) -> Result<(), String> {
4896 self.axpy_row_into_impl(row, alpha, out, true, "squared_axpy_row_into")
4897 }
4898
4899 #[inline]
4906 fn axpy_row_into_impl(
4907 &self,
4908 row: usize,
4909 alpha: f64,
4910 out: &mut ArrayViewMut1<'_, f64>,
4911 square: bool,
4912 method: &str,
4913 ) -> Result<(), String> {
4914 if out.len() != self.ncols() {
4915 return Err(format!(
4916 "DesignMatrix::{method} length mismatch: out={}, ncols={}",
4917 out.len(),
4918 self.ncols()
4919 ));
4920 }
4921 if alpha == 0.0 {
4922 return Ok(());
4923 }
4924 let scale = |value: f64| {
4926 if square {
4927 alpha * value * value
4928 } else {
4929 alpha * value
4930 }
4931 };
4932 match self {
4933 Self::Dense(matrix) => {
4934 if let Some(dense) = matrix.as_dense_ref() {
4935 for (dst, &value) in out.iter_mut().zip(dense.row(row).iter()) {
4936 *dst += scale(value);
4937 }
4938 } else {
4939 let chunk = matrix
4940 .try_row_chunk(row..row + 1)
4941 .map_err(|e| format!("DesignMatrix::{method}: {e}"))?;
4942 for (dst, &value) in out.iter_mut().zip(chunk.row(0).iter()) {
4943 *dst += scale(value);
4944 }
4945 }
4946 }
4947 Self::Sparse(matrix) => {
4948 let csr = matrix
4953 .to_csr_arc()
4954 .ok_or_else(|| format!("DesignMatrix::{method}: failed to obtain CSR view"))?;
4955 let sym = csr.symbolic();
4956 let row_ptr = sym.row_ptr();
4957 let col_idx = sym.col_idx();
4958 let vals = csr.val();
4959 for ptr in row_ptr[row]..row_ptr[row + 1] {
4960 out[col_idx[ptr]] += scale(vals[ptr]);
4961 }
4962 }
4963 }
4964 Ok(())
4965 }
4966
4967 pub fn crossdiag_axpy_row_into(
4973 &self,
4974 row: usize,
4975 other: &DesignMatrix,
4976 alpha: f64,
4977 out: &mut ArrayViewMut1<'_, f64>,
4978 ) -> Result<(), String> {
4979 assert_eq!(self.ncols(), other.ncols());
4980 assert_eq!(out.len(), self.ncols());
4981 if alpha == 0.0 {
4982 return Ok(());
4983 }
4984 match (self, other) {
4985 (Self::Dense(lhs), Self::Dense(rhs)) => {
4986 let lhs_chunk;
4987 let rhs_chunk;
4988 let x = if let Some(lhs_dense) = lhs.as_dense_ref() {
4989 lhs_dense.row(row)
4990 } else {
4991 lhs_chunk = lhs
4992 .try_row_chunk(row..row + 1)
4993 .map_err(|e| format!("crossdiag_axpy_row_into lhs: {e}"))?;
4994 lhs_chunk.row(0)
4995 };
4996 let y = if let Some(rhs_dense) = rhs.as_dense_ref() {
4997 rhs_dense.row(row)
4998 } else {
4999 rhs_chunk = rhs
5000 .try_row_chunk(row..row + 1)
5001 .map_err(|e| format!("crossdiag_axpy_row_into rhs: {e}"))?;
5002 rhs_chunk.row(0)
5003 };
5004 for (dst, (&xi, &yi)) in out.iter_mut().zip(x.iter().zip(y.iter())) {
5005 *dst += alpha * xi * yi;
5006 }
5007 }
5008 (Self::Sparse(lhs), Self::Sparse(rhs)) => {
5009 let lhs_csr = lhs.to_csr_arc().ok_or_else(|| {
5015 "crossdiag_axpy_row_into: failed to obtain lhs CSR view".to_string()
5016 })?;
5017 let rhs_csr = rhs.to_csr_arc().ok_or_else(|| {
5018 "crossdiag_axpy_row_into: failed to obtain rhs CSR view".to_string()
5019 })?;
5020 let lhs_sym = lhs_csr.symbolic();
5021 let rhs_sym = rhs_csr.symbolic();
5022 let lhs_rp = lhs_sym.row_ptr();
5023 let rhs_rp = rhs_sym.row_ptr();
5024 let lhs_ci = lhs_sym.col_idx();
5025 let rhs_ci = rhs_sym.col_idx();
5026 let lhs_v = lhs_csr.val();
5027 let rhs_v = rhs_csr.val();
5028 let mut li = lhs_rp[row];
5030 let mut ri = rhs_rp[row];
5031 let l_end = lhs_rp[row + 1];
5032 let r_end = rhs_rp[row + 1];
5033 while li < l_end && ri < r_end {
5034 let lc = lhs_ci[li];
5035 let rc = rhs_ci[ri];
5036 if lc == rc {
5037 out[lc] += alpha * lhs_v[li] * rhs_v[ri];
5038 li += 1;
5039 ri += 1;
5040 } else if lc < rc {
5041 li += 1;
5042 } else {
5043 ri += 1;
5044 }
5045 }
5046 }
5047 _ => {
5048 let (sparse_mat, dense_mat) = match (self, other) {
5050 (Self::Sparse(s), Self::Dense(d)) => (s, d),
5051 (Self::Dense(d), Self::Sparse(s)) => (s, d),
5052 _ => {
5055 return Err(
5056 "crossdiag_axpy_row_into: mixed-arm dispatch reached non-mixed pair"
5057 .to_string(),
5058 );
5059 }
5060 };
5061 let csr = sparse_mat.to_csr_arc().ok_or_else(|| {
5065 "crossdiag_axpy_row_into: failed to obtain CSR view".to_string()
5066 })?;
5067 let sym = csr.symbolic();
5068 let row_ptr = sym.row_ptr();
5069 let col_idx = sym.col_idx();
5070 let vals = csr.val();
5071 let dense_chunk;
5072 let dense_row = if let Some(dense_ref) = dense_mat.as_dense_ref() {
5073 dense_ref.row(row)
5074 } else {
5075 dense_chunk = dense_mat
5076 .try_row_chunk(row..row + 1)
5077 .map_err(|e| format!("crossdiag_axpy_row_into dense chunk: {e}"))?;
5078 dense_chunk.row(0)
5079 };
5080 for ptr in row_ptr[row]..row_ptr[row + 1] {
5081 let c = col_idx[ptr];
5082 out[c] += alpha * vals[ptr] * dense_row[c];
5083 }
5084 }
5085 }
5086 Ok(())
5087 }
5088
5089 pub fn syr_row_into(
5091 &self,
5092 row: usize,
5093 alpha: f64,
5094 target: &mut Array2<f64>,
5095 ) -> Result<(), String> {
5096 self.syr_row_into_view(row, alpha, target.view_mut())
5097 }
5098
5099 pub fn syr_row_into_view(
5102 &self,
5103 row: usize,
5104 alpha: f64,
5105 mut target: ArrayViewMut2<'_, f64>,
5106 ) -> Result<(), String> {
5107 if target.nrows() != self.ncols() || target.ncols() != self.ncols() {
5108 return Err(format!(
5109 "DesignMatrix::syr_row_into shape mismatch: target={}x{}, ncols={}",
5110 target.nrows(),
5111 target.ncols(),
5112 self.ncols()
5113 ));
5114 }
5115 if alpha == 0.0 {
5116 return Ok(());
5117 }
5118 match self {
5119 Self::Dense(matrix) => {
5120 if let Some(dense) = matrix.as_dense_ref() {
5121 let x = dense.row(row);
5122 for i in 0..x.len() {
5123 let xi = x[i];
5124 if xi == 0.0 {
5125 continue;
5126 }
5127 for j in 0..x.len() {
5128 target[[i, j]] += alpha * xi * x[j];
5129 }
5130 }
5131 } else {
5132 let chunk = matrix
5133 .try_row_chunk(row..row + 1)
5134 .map_err(|e| format!("DesignMatrix::syr_row_into: {e}"))?;
5135 let x = chunk.row(0);
5136 for i in 0..x.len() {
5137 let xi = x[i];
5138 if xi == 0.0 {
5139 continue;
5140 }
5141 for j in 0..x.len() {
5142 target[[i, j]] += alpha * xi * x[j];
5143 }
5144 }
5145 }
5146 }
5147 Self::Sparse(matrix) => {
5148 let csr = matrix.to_csr_arc().ok_or_else(|| {
5153 "DesignMatrix::syr_row_into: failed to obtain CSR view".to_string()
5154 })?;
5155 let sym = csr.symbolic();
5156 let row_ptr = sym.row_ptr();
5157 let col_idx = sym.col_idx();
5158 let vals = csr.val();
5159 for ptr_i in row_ptr[row]..row_ptr[row + 1] {
5160 let i = col_idx[ptr_i];
5161 let xi = vals[ptr_i];
5162 for ptr_j in row_ptr[row]..row_ptr[row + 1] {
5163 let j = col_idx[ptr_j];
5164 target[[i, j]] += alpha * xi * vals[ptr_j];
5165 }
5166 }
5167 }
5168 }
5169 Ok(())
5170 }
5171
5172 pub fn row_outer_into(
5177 &self,
5178 row: usize,
5179 other: &DesignMatrix,
5180 alpha: f64,
5181 target: &mut Array2<f64>,
5182 ) -> Result<(), String> {
5183 self.row_outer_into_view(row, other, alpha, target.view_mut())
5184 }
5185
5186 pub fn row_outer_into_view(
5189 &self,
5190 row: usize,
5191 other: &DesignMatrix,
5192 alpha: f64,
5193 mut target: ArrayViewMut2<'_, f64>,
5194 ) -> Result<(), String> {
5195 if target.nrows() != self.ncols() || target.ncols() != other.ncols() {
5196 return Err(format!(
5197 "DesignMatrix::row_outer_into shape mismatch: target={}x{}, lhs={}, rhs={}",
5198 target.nrows(),
5199 target.ncols(),
5200 self.ncols(),
5201 other.ncols()
5202 ));
5203 }
5204 if alpha == 0.0 {
5205 return Ok(());
5206 }
5207 match (self, other) {
5208 (Self::Dense(lhs), Self::Dense(rhs)) => {
5209 let lhs_chunk;
5210 let rhs_chunk;
5211 let x = if let Some(lhs_dense) = lhs.as_dense_ref() {
5212 lhs_dense.row(row)
5213 } else {
5214 lhs_chunk = lhs
5215 .try_row_chunk(row..row + 1)
5216 .map_err(|e| format!("row_outer_into_view lhs: {e}"))?;
5217 lhs_chunk.row(0)
5218 };
5219 let y = if let Some(rhs_dense) = rhs.as_dense_ref() {
5220 rhs_dense.row(row)
5221 } else {
5222 rhs_chunk = rhs
5223 .try_row_chunk(row..row + 1)
5224 .map_err(|e| format!("row_outer_into_view rhs: {e}"))?;
5225 rhs_chunk.row(0)
5226 };
5227 for i in 0..x.len() {
5228 let xi = x[i];
5229 if xi == 0.0 {
5230 continue;
5231 }
5232 for j in 0..y.len() {
5233 target[[i, j]] += alpha * xi * y[j];
5234 }
5235 }
5236 }
5237 (Self::Sparse(lhs), Self::Sparse(rhs)) => {
5238 let lhs_csr = lhs
5243 .to_csr_arc()
5244 .ok_or_else(|| "row_outer_into: failed to obtain lhs CSR view".to_string())?;
5245 let rhs_csr = rhs
5247 .to_csr_arc()
5248 .ok_or_else(|| "row_outer_into: failed to obtain rhs CSR view".to_string())?;
5249 let lhs_sym = lhs_csr.symbolic();
5250 let rhs_sym = rhs_csr.symbolic();
5251 let lhs_rp = lhs_sym.row_ptr();
5252 let rhs_rp = rhs_sym.row_ptr();
5253 let lhs_ci = lhs_sym.col_idx();
5254 let rhs_ci = rhs_sym.col_idx();
5255 let lhs_v = lhs_csr.val();
5256 let rhs_v = rhs_csr.val();
5257 for pi in lhs_rp[row]..lhs_rp[row + 1] {
5258 let i = lhs_ci[pi];
5259 let xi = lhs_v[pi];
5260 for pj in rhs_rp[row]..rhs_rp[row + 1] {
5261 let j = rhs_ci[pj];
5262 target[[i, j]] += alpha * xi * rhs_v[pj];
5263 }
5264 }
5265 }
5266 _ => {
5267 let x = self
5269 .try_row_chunk(row..row + 1)
5270 .map_err(|e| format!("row_outer_into_view lhs: {e}"))?;
5271 let x_row = x.row(0);
5272 let y = other
5273 .try_row_chunk(row..row + 1)
5274 .map_err(|e| format!("row_outer_into_view rhs: {e}"))?;
5275 let y_row = y.row(0);
5276 for i in 0..x_row.len() {
5277 let xi = x_row[i];
5278 if xi == 0.0 {
5279 continue;
5280 }
5281 for j in 0..y_row.len() {
5282 target[[i, j]] += alpha * xi * y_row[j];
5283 }
5284 }
5285 }
5286 }
5287 Ok(())
5288 }
5289
5290 pub fn apply_view_into(&self, vector: ArrayView1<'_, f64>, mut output: ArrayViewMut1<'_, f64>) {
5296 assert_eq!(self.ncols(), vector.len());
5297 assert_eq!(self.nrows(), output.len());
5298 match self {
5299 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => {
5300 crate::dense::matvec_into(matrix.as_ref(), vector, output);
5301 }
5302 Self::Dense(DenseDesignMatrix::Lazy(operator)) => {
5303 output.assign(&operator.apply(&vector.to_owned()));
5304 }
5305 Self::Sparse(matrix) => {
5306 output.fill(0.0);
5307 let (symbolic, values) = matrix.parts();
5308 let col_ptr = symbolic.col_ptr();
5309 let row_idx = symbolic.row_idx();
5310 for col in 0..matrix.ncols() {
5311 let x = vector[col];
5312 if x == 0.0 {
5313 continue;
5314 }
5315 for idx in col_ptr[col]..col_ptr[col + 1] {
5316 output[row_idx[idx]] += values[idx] * x;
5317 }
5318 }
5319 }
5320 }
5321 }
5322
5323 pub fn apply_view(&self, vector: ArrayView1<'_, f64>) -> Array1<f64> {
5325 let mut output = Array1::<f64>::zeros(self.nrows());
5326 self.apply_view_into(vector, output.view_mut());
5327 output
5328 }
5329
5330 pub fn transpose_apply_view_into(
5332 &self,
5333 vector: ArrayView1<'_, f64>,
5334 mut output: ArrayViewMut1<'_, f64>,
5335 ) {
5336 assert_eq!(self.nrows(), vector.len());
5337 assert_eq!(self.ncols(), output.len());
5338 match self {
5339 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => {
5340 crate::dense::transpose_matvec_into(matrix.as_ref(), vector, output);
5341 }
5342 Self::Dense(DenseDesignMatrix::Lazy(operator)) => {
5343 output.assign(&operator.apply_transpose(&vector.to_owned()));
5344 }
5345 Self::Sparse(matrix) => {
5346 let (symbolic, values) = matrix.parts();
5347 let col_ptr = symbolic.col_ptr();
5348 let row_idx = symbolic.row_idx();
5349 for col in 0..matrix.ncols() {
5350 let mut value = 0.0;
5351 for idx in col_ptr[col]..col_ptr[col + 1] {
5352 value += values[idx] * vector[row_idx[idx]];
5353 }
5354 output[col] = value;
5355 }
5356 }
5357 }
5358 }
5359
5360 pub fn column_into(&self, col: usize, mut output: ArrayViewMut1<'_, f64>) {
5362 assert!(col < self.ncols());
5363 assert_eq!(self.nrows(), output.len());
5364 match self {
5365 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => {
5366 output.assign(&matrix.column(col));
5367 }
5368 Self::Dense(DenseDesignMatrix::Lazy(operator)) => {
5369 let mut basis = Array1::<f64>::zeros(operator.ncols());
5370 basis[col] = 1.0;
5371 output.assign(&operator.apply(&basis));
5372 }
5373 Self::Sparse(matrix) => {
5374 output.fill(0.0);
5375 let (symbolic, values) = matrix.parts();
5376 let col_ptr = symbolic.col_ptr();
5377 let row_idx = symbolic.row_idx();
5378 for idx in col_ptr[col]..col_ptr[col + 1] {
5379 output[row_idx[idx]] += values[idx];
5380 }
5381 }
5382 }
5383 }
5384
5385 #[inline]
5397 pub fn get(&self, i: usize, j: usize) -> f64 {
5398 match self {
5399 Self::Dense(matrix) => match matrix.as_dense_ref() {
5400 Some(dense) => dense[[i, j]],
5401 None => {
5406 let mut e_j = Array1::<f64>::zeros(matrix.ncols());
5407 e_j[j] = 1.0;
5408 matrix.apply(&e_j)[i]
5409 }
5410 },
5411 Self::Sparse(sp) => {
5412 let dense = sp
5420 .try_to_dense_arc("DesignMatrix::get")
5421 .unwrap_or_else(|msg| std::panic::panic_any(msg));
5422 dense[[i, j]]
5423 }
5424 }
5425 }
5426
5427 pub fn extract_column(&self, j: usize) -> Array1<f64> {
5433 let mut column = Array1::zeros(self.nrows());
5434 self.column_into(j, column.view_mut());
5435 column
5436 }
5437
5438 pub fn extract_columns(&self, cols: &[usize]) -> Array2<f64> {
5445 match self {
5446 Self::Dense(m) => match m {
5447 DenseDesignMatrix::Materialized(mat) => mat.select(Axis(1), cols),
5448 DenseDesignMatrix::Lazy(op) => op.apply_columns(cols),
5449 },
5450 Self::Sparse(sp) => {
5451 let n = sp.nrows();
5452 let mut out = Array2::<f64>::zeros((n, cols.len()));
5453 let (symbolic, values) = sp.parts();
5454 let col_ptr = symbolic.col_ptr();
5455 let row_idx = symbolic.row_idx();
5456 for (k, &j) in cols.iter().enumerate() {
5457 let start = col_ptr[j];
5458 let end = col_ptr[j + 1];
5459 let mut out_col = out.column_mut(k);
5460 for idx in start..end {
5461 out_col[row_idx[idx]] += values[idx];
5462 }
5463 }
5464 out
5465 }
5466 }
5467 }
5468
5469 pub fn as_dense_ref(&self) -> Option<&Array2<f64>> {
5471 match self {
5472 Self::Dense(matrix) => matrix.as_dense_ref(),
5473 Self::Sparse(_) => None,
5474 }
5475 }
5476
5477 pub const fn is_materialized_dense(&self) -> bool {
5478 matches!(self, Self::Dense(DenseDesignMatrix::Materialized(_)))
5479 }
5480
5481 pub const fn is_operator_backed(&self) -> bool {
5482 match self {
5483 Self::Dense(matrix) => matrix.is_operator_backed(),
5484 Self::Sparse(_) => false,
5485 }
5486 }
5487
5488 pub const fn is_sparse(&self) -> bool {
5494 matches!(self, Self::Sparse(_))
5495 }
5496
5497 pub fn as_dense_cow(&self) -> Cow<'_, Array2<f64>> {
5503 match self {
5504 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => Cow::Borrowed(matrix.as_ref()),
5505 Self::Dense(DenseDesignMatrix::Lazy(op)) => match op.as_dense_ref() {
5506 Some(dense) => Cow::Borrowed(dense),
5507 None => std::panic::panic_any(format!(
5514 "DesignMatrix::as_dense_cow called on operator-backed design ({}x{}); use row chunks or matrix-vector products",
5515 op.nrows(),
5516 op.ncols()
5517 )),
5518 },
5519 Self::Sparse(matrix) => Cow::Owned(
5520 matrix
5521 .try_to_dense_arc("DesignMatrix::as_dense_cow")
5522 .unwrap_or_else(|msg| std::panic::panic_any(msg))
5528 .as_ref()
5529 .clone(),
5530 ),
5531 }
5532 }
5533
5534 pub fn to_dense_cow(&self) -> Cow<'_, Array2<f64>> {
5543 match self {
5544 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => Cow::Borrowed(matrix.as_ref()),
5545 Self::Dense(DenseDesignMatrix::Lazy(lazy)) => {
5546 if let Some(dense) = lazy.as_dense_ref() {
5547 Cow::Borrowed(dense)
5548 } else {
5549 let policy = ResourcePolicy::default_library();
5550 panic_or_error_if_large_scale_mode_and_to_dense_called_with_policy(
5551 "DesignMatrix::to_dense_cow",
5552 lazy.nrows(),
5553 lazy.ncols(),
5554 &policy,
5555 )
5556 .unwrap_or_else(|reason| std::panic::panic_any(reason));
5557 lazy.try_governed_dense_arc("DesignMatrix::to_dense_cow")
5563 .unwrap_or_else(|msg| std::panic::panic_any(msg));
5565 Cow::Borrowed(
5566 lazy.dense_memo
5567 .get()
5568 .expect("memo initialized by try_governed_dense_arc just above")
5569 .as_ref()
5570 .as_ref(),
5571 )
5572 }
5573 }
5574 Self::Sparse(matrix) => Cow::Owned(
5575 matrix
5576 .try_to_dense_arc("DesignMatrix::to_dense_cow")
5577 .unwrap_or_else(|msg| std::panic::panic_any(msg))
5583 .as_ref()
5584 .clone(),
5585 ),
5586 }
5587 }
5588
5589 pub fn to_dense(&self) -> Array2<f64> {
5600 match self {
5601 Self::Dense(matrix) => matrix.to_dense(),
5602 Self::Sparse(matrix) => matrix
5603 .try_to_dense_arc("DesignMatrix::to_dense")
5604 .unwrap_or_else(|msg| std::panic::panic_any(msg))
5606 .as_ref()
5607 .clone(),
5608 }
5609 }
5610
5611 pub fn to_dense_arc(&self) -> Arc<Array2<f64>> {
5613 match self {
5614 Self::Dense(matrix) => matrix.to_dense_arc(),
5615 Self::Sparse(matrix) => matrix
5616 .try_to_dense_arc("DesignMatrix::to_dense_arc")
5617 .unwrap_or_else(|msg| std::panic::panic_any(msg)),
5619 }
5620 }
5621
5622 pub fn try_to_dense_arc(&self, context: &str) -> Result<Arc<Array2<f64>>, String> {
5623 match self {
5624 Self::Dense(matrix) => matrix.try_to_dense_arc(context),
5625 Self::Sparse(matrix) => matrix.try_to_dense_arc(context),
5626 }
5627 }
5628
5629 pub fn try_to_dense_arc_with_policy(
5632 &self,
5633 context: &str,
5634 policy: &ResourcePolicy,
5635 ) -> Result<Arc<Array2<f64>>, String> {
5636 match self {
5637 Self::Dense(matrix) => matrix.try_to_dense_arc_with_policy(context, policy),
5638 Self::Sparse(matrix) => matrix.try_to_dense_arc(context),
5639 }
5640 }
5641
5642 pub fn to_csr_cache(&self) -> Option<SparseRowMat<usize, f64>> {
5643 match self {
5644 Self::Dense(_) => None,
5645 Self::Sparse(matrix) => matrix.to_csr_arc().map(|arc| (*arc).clone()),
5646 }
5647 }
5648
5649 pub fn as_sparse(&self) -> Option<&SparseDesignMatrix> {
5650 match self {
5651 Self::Sparse(matrix) => Some(matrix),
5652 Self::Dense(_) => None,
5653 }
5654 }
5655
5656 pub fn as_dense(&self) -> Option<&Array2<f64>> {
5657 match self {
5658 Self::Dense(matrix) => matrix.as_dense_ref(),
5659 Self::Sparse(_) => None,
5660 }
5661 }
5662
5663 fn apply_transpose_view(&self, vector: ArrayView1<'_, f64>) -> Array1<f64> {
5664 match self {
5665 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => fast_atv(matrix, &vector),
5666 Self::Dense(DenseDesignMatrix::Lazy(op)) => op.apply_transpose(&vector.to_owned()),
5667 Self::Sparse(matrix) => {
5668 let mut output = Array1::<f64>::zeros(matrix.ncols());
5669 let (symbolic, values) = matrix.parts();
5670 let col_ptr = symbolic.col_ptr();
5671 let row_idx = symbolic.row_idx();
5672 for col in 0..matrix.ncols() {
5673 let mut acc = 0.0;
5674 let start = col_ptr[col];
5675 let end = col_ptr[col + 1];
5676 for idx in start..end {
5677 acc += values[idx] * vector[row_idx[idx]];
5678 }
5679 output[col] = acc;
5680 }
5681 output
5682 }
5683 }
5684 }
5685
5686 fn diag_gram_view(&self, weights: ArrayView1<'_, f64>) -> Result<Array1<f64>, String> {
5687 if weights.len() != self.nrows() {
5688 return Err(format!(
5689 "diag_gram dimension mismatch: weights length {} != nrows {}",
5690 weights.len(),
5691 self.nrows()
5692 ));
5693 }
5694 FiniteSignedWeightsView::try_new(weights)
5695 .map_err(|reason| format!("DesignMatrix::diag_gram_view: {reason}"))?;
5696 match self {
5697 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => {
5698 Ok(dense_diag_gram_view(matrix, weights))
5699 }
5700 Self::Dense(DenseDesignMatrix::Lazy(op)) => op.diag_gram(&weights.to_owned()),
5701 Self::Sparse(xs) => {
5702 let p = xs.ncols();
5703 let csr = xs
5704 .to_csr_arc()
5705 .ok_or_else(|| "failed to obtain CSR view in diag_gram".to_string())?;
5706 let sym = csr.symbolic();
5707 Ok(sparse_csr_diag_gram(
5708 sym.row_ptr(),
5709 sym.col_idx(),
5710 csr.val(),
5711 xs.nrows(),
5712 p,
5713 weights,
5714 ))
5715 }
5716 }
5717 }
5718
5719 fn compute_xtwy_view(
5720 &self,
5721 weights: ArrayView1<'_, f64>,
5722 y: ArrayView1<'_, f64>,
5723 ) -> Result<Array1<f64>, String> {
5724 if weights.len() != self.nrows() || y.len() != self.nrows() {
5725 return Err(format!(
5726 "compute_xtwy dimension mismatch: weights={}, y={}, nrows={}",
5727 weights.len(),
5728 y.len(),
5729 self.nrows()
5730 ));
5731 }
5732 FiniteSignedWeightsView::try_new(weights)
5733 .map_err(|reason| format!("DesignMatrix::compute_xtwy_view: {reason}"))?;
5734 match self {
5735 Self::Dense(DenseDesignMatrix::Materialized(matrix)) => {
5736 Ok(dense_transpose_weighted_response_view(matrix, weights, y))
5737 }
5738 Self::Dense(DenseDesignMatrix::Lazy(op)) => {
5739 op.compute_xtwy(&weights.to_owned(), &y.to_owned())
5740 }
5741 Self::Sparse(xs) => {
5742 let csr = xs
5743 .as_ref()
5744 .to_row_major()
5745 .map_err(|_| "failed to obtain CSR view in compute_xtwy".to_string())?;
5746 let sym = csr.symbolic();
5747 let row_ptr = sym.row_ptr();
5748 let col_idx = sym.col_idx();
5749 let vals = csr.val();
5750 let mut out = Array1::<f64>::zeros(xs.ncols());
5751 for i in 0..xs.nrows() {
5752 let scaled = weights[i] * y[i];
5753 if scaled == 0.0 {
5754 continue;
5755 }
5756 for idx in row_ptr[i]..row_ptr[i + 1] {
5757 out[col_idx[idx]] += vals[idx] * scaled;
5758 }
5759 }
5760 Ok(out)
5761 }
5762 }
5763 }
5764
5765 pub fn dot(&self, vector: &Array1<f64>) -> Array1<f64> {
5766 <Self as LinearOperator>::apply(self, vector)
5767 }
5768
5769 pub fn matrixvectormultiply(&self, vector: &Array1<f64>) -> Array1<f64> {
5770 <Self as LinearOperator>::apply(self, vector)
5771 }
5772
5773 pub fn transpose_vector_multiply(&self, vector: &Array1<f64>) -> Array1<f64> {
5774 <Self as LinearOperator>::apply_transpose(self, vector)
5775 }
5776
5777 pub fn compute_xtwy(
5778 &self,
5779 weights: &Array1<f64>,
5780 y: &Array1<f64>,
5781 ) -> Result<Array1<f64>, String> {
5782 <Self as DenseDesignOperator>::compute_xtwy(self, weights, y)
5783 }
5784
5785 pub fn diag_gram(&self, weights: &Array1<f64>) -> Result<Array1<f64>, String> {
5786 <Self as LinearOperator>::diag_gram(self, weights)
5787 }
5788
5789 pub fn quadratic_form_diag(&self, middle: &Array2<f64>) -> Result<Array1<f64>, String> {
5790 <Self as DenseDesignOperator>::quadratic_form_diag(self, middle)
5791 }
5792
5793 pub fn apply_weighted_normal(
5794 &self,
5795 weights: &Array1<f64>,
5796 vector: &Array1<f64>,
5797 penalty: Option<&Array2<f64>>,
5798 ridge: f64,
5799 ) -> Result<Array1<f64>, String> {
5800 let finite =
5801 certify_signed_weights("DesignMatrix::apply_weighted_normal", weights, self.nrows())?;
5802 if vector.len() != self.ncols() {
5803 return Err(format!(
5804 "DesignMatrix::apply_weighted_normal vector length mismatch: vector={}, ncols={}",
5805 vector.len(),
5806 self.ncols()
5807 ));
5808 }
5809 Ok(<Self as LinearOperator>::apply_weighted_normal(
5810 self, finite, vector, penalty, ridge,
5811 ))
5812 }
5813
5814 pub fn solve_system(
5815 &self,
5816 weights: &Array1<f64>,
5817 rhs: &Array1<f64>,
5818 penalty: Option<&Array2<f64>>,
5819 ) -> Result<Array1<f64>, String> {
5820 <Self as LinearOperator>::solve_system(self, weights, rhs, penalty)
5821 }
5822
5823 pub fn solve_systemwith_policy(
5824 &self,
5825 weights: &Array1<f64>,
5826 rhs: &Array1<f64>,
5827 penalty: Option<&Array2<f64>>,
5828 ridge_floor: f64,
5829 ridge_policy: RidgePolicy,
5830 ) -> Result<Array1<f64>, String> {
5831 <Self as LinearOperator>::solve_systemwith_policy(
5832 self,
5833 weights,
5834 rhs,
5835 penalty,
5836 ridge_floor,
5837 ridge_policy,
5838 )
5839 }
5840
5841 pub fn solve_system_matrix_free_pcg(
5842 &self,
5843 weights: &Array1<f64>,
5844 rhs: &Array1<f64>,
5845 penalty: Option<&Array2<f64>>,
5846 ridge_floor: f64,
5847 ) -> Result<Array1<f64>, String> {
5848 <Self as LinearOperator>::solve_system_matrix_free_pcg_try(
5849 self,
5850 weights,
5851 rhs,
5852 penalty,
5853 ridge_floor,
5854 )
5855 }
5856
5857 pub fn solve_system_matrix_free_pcg_with_info(
5858 &self,
5859 weights: &Array1<f64>,
5860 rhs: &Array1<f64>,
5861 penalty: Option<&Array2<f64>>,
5862 ridge_floor: f64,
5863 ) -> Result<(Array1<f64>, PcgSolveInfo), String> {
5864 <Self as LinearOperator>::solve_system_matrix_free_pcg_with_info_try(
5865 self,
5866 weights,
5867 rhs,
5868 penalty,
5869 ridge_floor,
5870 )
5871 }
5872
5873 pub fn should_use_matrix_free_pcg(&self) -> bool {
5874 <Self as LinearOperator>::uses_matrix_free_pcg(self)
5875 && self.ncols() >= MATRIX_FREE_PCG_MIN_P
5876 }
5877
5878 pub fn factorize_system(
5879 &self,
5880 weights: &Array1<f64>,
5881 penalty: Option<&Array2<f64>>,
5882 ) -> Result<Box<dyn FactorizedSystem>, String> {
5883 <Self as LinearOperator>::factorize_system(self, weights, penalty)
5884 }
5885}
5886
5887impl<'a> From<ArrayView2<'a, f64>> for DesignMatrix {
5888 fn from(value: ArrayView2<'a, f64>) -> Self {
5889 Self::Dense(DenseDesignMatrix::from(value.to_owned()))
5890 }
5891}
5892
5893impl From<Array2<f64>> for DesignMatrix {
5894 fn from(value: Array2<f64>) -> Self {
5895 Self::Dense(DenseDesignMatrix::from(value))
5896 }
5897}
5898
5899impl From<Arc<Array2<f64>>> for DesignMatrix {
5900 fn from(value: Arc<Array2<f64>>) -> Self {
5901 Self::Dense(DenseDesignMatrix::from(value))
5902 }
5903}
5904
5905impl From<&Array2<f64>> for DesignMatrix {
5906 fn from(value: &Array2<f64>) -> Self {
5907 Self::Dense(DenseDesignMatrix::from(value.clone()))
5908 }
5909}
5910
5911impl From<DenseDesignMatrix> for DesignMatrix {
5912 fn from(value: DenseDesignMatrix) -> Self {
5913 Self::Dense(value)
5914 }
5915}
5916
5917impl From<SparseColMat<usize, f64>> for DesignMatrix {
5918 fn from(value: SparseColMat<usize, f64>) -> Self {
5919 Self::Sparse(SparseDesignMatrix::new(value))
5920 }
5921}
5922
5923impl From<&SparseColMat<usize, f64>> for DesignMatrix {
5924 fn from(value: &SparseColMat<usize, f64>) -> Self {
5925 Self::Sparse(SparseDesignMatrix::new(value.clone()))
5926 }
5927}
5928
5929impl From<&DesignMatrix> for DesignMatrix {
5930 fn from(value: &DesignMatrix) -> Self {
5931 value.clone()
5932 }
5933}
5934
5935impl From<DesignMatrix> for DesignBlock {
5936 fn from(value: DesignMatrix) -> Self {
5937 match value {
5938 DesignMatrix::Dense(matrix) => Self::Dense(matrix),
5939 DesignMatrix::Sparse(matrix) => Self::Sparse(matrix),
5940 }
5941 }
5942}
5943
5944impl From<&DesignMatrix> for DesignBlock {
5945 fn from(value: &DesignMatrix) -> Self {
5946 match value {
5947 DesignMatrix::Dense(matrix) => Self::Dense(matrix.clone()),
5948 DesignMatrix::Sparse(matrix) => Self::Sparse(matrix.clone()),
5949 }
5950 }
5951}
5952
5953#[cfg(test)]
5954mod tests {
5955 use super::{
5956 BlockDesignOperator, CoefficientTransformOperator, ConditionedDesign, DenseDesignMatrix,
5957 DenseDesignOperator, DesignBlock, DesignMatrix, EmbeddedColumnBlock,
5958 FiniteSignedWeightsView, MultiChannelOperator, PsdWeightsView, RandomEffectOperator,
5959 ReparamOperator, RowwiseKroneckerOperator, SparseDesignMatrix,
5960 dense_operator_to_dense_by_chunks, dense_transpose_weighted_response, fast_atv, fast_av,
5961 streaming_sparse_csc_xt_diag_x, weighted_crossprod_dense_view, xt_diag_x_symmetric,
5962 };
5963 use crate::matrix::LinearOperator;
5964 use crate::test_support::no_densify_design;
5965 use crate::types::RidgePolicy;
5966 use crate::utils::{PcgSolveInfo, StableSolver};
5967 use faer::sparse::{SparseColMat, SymbolicSparseColMat, Triplet};
5968 use gam_runtime::resource::{
5969 MaterializationPolicy, MatrixMaterializationError, MemoryGovernor, ResourcePolicy,
5970 };
5971 use ndarray::{Array1, Array2, ArrayViewMut2, Axis, array, s};
5972 use std::ops::Range;
5973 use std::sync::Arc;
5974 use std::sync::atomic::{AtomicUsize, Ordering};
5975
5976 struct ChunkOnlyOperator {
5977 n: usize,
5978 p: usize,
5979 row_chunk_calls: AtomicUsize,
5980 materialization_policy: Option<MaterializationPolicy>,
5981 }
5982
5983 impl ChunkOnlyOperator {
5984 fn value(&self, i: usize, j: usize) -> f64 {
5985 ((i % 251) as f64) * 0.25 - ((j % 127) as f64) * 0.5 + ((i + j) % 7) as f64
5986 }
5987 }
5988
5989 impl LinearOperator for ChunkOnlyOperator {
5990 fn nrows(&self) -> usize {
5991 self.n
5992 }
5993
5994 fn ncols(&self) -> usize {
5995 self.p
5996 }
5997
5998 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
5999 let mut out = Array1::<f64>::zeros(self.n);
6000 for i in 0..self.n {
6001 let mut acc = 0.0;
6002 for j in 0..self.p {
6003 acc += self.value(i, j) * vector[j];
6004 }
6005 out[i] = acc;
6006 }
6007 out
6008 }
6009
6010 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
6011 let mut out = Array1::<f64>::zeros(self.p);
6012 for i in 0..self.n {
6013 for j in 0..self.p {
6014 out[j] += self.value(i, j) * vector[i];
6015 }
6016 }
6017 out
6018 }
6019
6020 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
6021 let dense = dense_operator_to_dense_by_chunks(self).map_err(|err| err.to_string())?;
6022 let psd = PsdWeightsView::try_new(weights.view())?;
6023 Ok(weighted_crossprod_dense_view(&dense, psd.view(), &dense))
6024 }
6025 }
6026
6027 impl DenseDesignOperator for ChunkOnlyOperator {
6028 fn materialization_policy(&self) -> Option<MaterializationPolicy> {
6029 self.materialization_policy.clone()
6030 }
6031
6032 fn row_chunk_into(
6033 &self,
6034 rows: Range<usize>,
6035 mut out: ArrayViewMut2<'_, f64>,
6036 ) -> Result<(), MatrixMaterializationError> {
6037 self.row_chunk_calls.fetch_add(1, Ordering::SeqCst);
6038 if out.nrows() != rows.end - rows.start || out.ncols() != self.p {
6039 return Err(MatrixMaterializationError::MissingRowChunk {
6040 context: "ChunkOnlyOperator::row_chunk_into shape mismatch",
6041 });
6042 }
6043 for (local, row) in rows.enumerate() {
6044 for col in 0..self.p {
6045 out[[local, col]] = self.value(row, col);
6046 }
6047 }
6048 Ok(())
6049 }
6050
6051 fn to_dense(&self) -> Array2<f64> {
6052 panic!("ChunkOnlyOperator::to_dense fallback must not be used")
6054 }
6055 }
6056
6057 struct DirectFillOnlyOperator {
6058 values: Array2<f64>,
6059 row_chunk_calls: AtomicUsize,
6060 }
6061
6062 impl LinearOperator for DirectFillOnlyOperator {
6063 fn nrows(&self) -> usize {
6064 self.values.nrows()
6065 }
6066
6067 fn ncols(&self) -> usize {
6068 self.values.ncols()
6069 }
6070
6071 fn apply(&self, vector: &Array1<f64>) -> Array1<f64> {
6072 self.values.dot(vector)
6073 }
6074
6075 fn apply_transpose(&self, vector: &Array1<f64>) -> Array1<f64> {
6076 self.values.t().dot(vector)
6077 }
6078
6079 fn diag_xtw_x(&self, weights: &Array1<f64>) -> Result<Array2<f64>, String> {
6080 let mut out = Array2::<f64>::zeros((self.ncols(), self.ncols()));
6081 for row in 0..self.nrows() {
6082 for left in 0..self.ncols() {
6083 for right in 0..self.ncols() {
6084 out[[left, right]] +=
6085 weights[row] * self.values[[row, left]] * self.values[[row, right]];
6086 }
6087 }
6088 }
6089 Ok(out)
6090 }
6091 }
6092
6093 impl DenseDesignOperator for DirectFillOnlyOperator {
6094 fn row_chunk_into(
6095 &self,
6096 rows: Range<usize>,
6097 mut out: ArrayViewMut2<'_, f64>,
6098 ) -> Result<(), MatrixMaterializationError> {
6099 self.row_chunk_calls.fetch_add(1, Ordering::SeqCst);
6100 if rows.end > self.nrows()
6101 || out.nrows() != rows.end - rows.start
6102 || out.ncols() != self.ncols()
6103 {
6104 return Err(MatrixMaterializationError::MissingRowChunk {
6105 context: "DirectFillOnlyOperator::row_chunk_into shape mismatch",
6106 });
6107 }
6108 out.assign(&self.values.slice(s![rows, ..]));
6109 Ok(())
6110 }
6111
6112 fn try_row_chunk(
6113 &self,
6114 rows: Range<usize>,
6115 ) -> Result<Array2<f64>, MatrixMaterializationError> {
6116 panic!(
6117 "DirectFillOnlyOperator owned row chunk {}..{} is forbidden",
6118 rows.start, rows.end
6119 )
6120 }
6121
6122 fn to_dense(&self) -> Array2<f64> {
6123 panic!("DirectFillOnlyOperator dense materialization is forbidden")
6124 }
6125 }
6126
6127 fn exact_weighted_penalized_solve(
6128 design: &Array2<f64>,
6129 weights: &Array1<f64>,
6130 rhs: &Array1<f64>,
6131 penalty: &Array2<f64>,
6132 ridge: f64,
6133 ) -> Array1<f64> {
6134 let mut h = design
6135 .t()
6136 .dot(&(design * &weights.view().insert_axis(Axis(1))));
6137 h += penalty;
6138 if ridge > 0.0 {
6139 for i in 0..h.nrows() {
6140 h[[i, i]] += ridge;
6141 }
6142 }
6143 let factor = StableSolver::new()
6144 .factorize(&h)
6145 .expect("exact reference factorization");
6146 let mut solution = rhs.clone();
6147 let mut solution_matrix = crate::faer_ndarray::array1_to_col_matmut(&mut solution);
6148 factor.solve_in_place(solution_matrix.as_mut());
6149 assert!(solution.iter().all(|value| value.is_finite()));
6150 solution
6151 }
6152
6153 #[test]
6154 fn fast_av_matches_ndarray_dot() {
6155 let x = array![[1.0, 2.0, -1.0], [0.5, -3.0, 4.0], [2.0, 0.0, 1.5]];
6156 let v = array![0.25, -1.0, 2.0];
6157 let expected = x.dot(&v);
6158 let got = fast_av(&x, &v);
6159 for i in 0..expected.len() {
6160 assert!((expected[i] - got[i]).abs() < 1e-12);
6161 }
6162 }
6163
6164 #[test]
6165 fn fast_atv_matches_ndarray_dot() {
6166 let x = array![[1.0, 2.0, -1.0], [0.5, -3.0, 4.0], [2.0, 0.0, 1.5]];
6167 let v = array![0.25, -1.0, 2.0];
6168 let expected = x.t().dot(&v);
6169 let got = fast_atv(&x, &v);
6170 for i in 0..expected.len() {
6171 assert!((expected[i] - got[i]).abs() < 1e-12);
6172 }
6173 }
6174
6175 #[test]
6176 fn sparse_to_dense_accumulates_duplicate_entries() {
6177 let symbolic = SymbolicSparseColMat::new_unsorted_checked(
6180 3,
6181 2,
6182 vec![0_usize, 2, 3],
6183 None,
6184 vec![1_usize, 1, 0],
6185 );
6186 let sparse = SparseColMat::new(symbolic, vec![2.0_f64, 3.5, -1.0]);
6187 let design = DesignMatrix::from(sparse);
6188 let dense = design.to_dense_arc();
6189
6190 assert!((dense[[1, 0]] - 5.5).abs() < 1e-12);
6191 assert!((dense[[0, 1]] + 1.0).abs() < 1e-12);
6192
6193 let v = array![4.0, -2.0];
6194 let y_sparse = design.matrixvectormultiply(&v);
6195 let y_dense = dense.dot(&v);
6196 for i in 0..y_sparse.len() {
6197 assert!((y_sparse[i] - y_dense[i]).abs() < 1e-12);
6198 }
6199 }
6200
6201 #[test]
6202 fn sparse_column_extractors_accumulate_duplicate_entries() {
6203 let symbolic = SymbolicSparseColMat::new_unsorted_checked(
6208 3,
6209 2,
6210 vec![0_usize, 2, 3],
6211 None,
6212 vec![1_usize, 1, 0],
6213 );
6214 let sparse = SparseColMat::new(symbolic, vec![2.0_f64, 3.5, -1.0]);
6215 let design = DesignMatrix::from(sparse);
6216 let dense = design.to_dense();
6217
6218 let mut col0 = Array1::<f64>::zeros(3);
6219 design.column_into(0, col0.view_mut());
6220 assert!((col0[1] - 5.5).abs() < 1e-12);
6221 for i in 0..3 {
6222 assert!((col0[i] - dense[[i, 0]]).abs() < 1e-12);
6223 }
6224
6225 let block = design.extract_columns(&[0, 1]);
6226 assert!((block[[1, 0]] - 5.5).abs() < 1e-12);
6227 assert!((block[[0, 1]] + 1.0).abs() < 1e-12);
6228 for i in 0..3 {
6229 for (k, &j) in [0usize, 1].iter().enumerate() {
6230 assert!((block[[i, k]] - dense[[i, j]]).abs() < 1e-12);
6231 }
6232 }
6233 }
6234
6235 #[test]
6236 fn huge_sparse_densification_is_rejected_before_allocation() {
6237 let sparse = SparseColMat::try_new_from_triplets(1usize << 44, 4, &[])
6241 .expect("empty sparse matrix should build");
6242 let design = SparseDesignMatrix::new(sparse);
6243 let err = design
6244 .try_to_dense_arc("matrix test")
6245 .expect_err("huge sparse densification should be rejected");
6246 assert!(err.contains("refusing to densify sparse design"));
6247 }
6248
6249 #[test]
6254 fn sparse_densification_reserves_ledger_and_full_ledger_streams() {
6255 let triplets = [
6256 Triplet::new(0, 0, 1.0),
6257 Triplet::new(0, 1, -2.0),
6258 Triplet::new(1, 0, 0.5),
6259 Triplet::new(1, 1, 3.0),
6260 Triplet::new(2, 0, -1.5),
6261 Triplet::new(2, 1, 0.25),
6262 ];
6263 let sparse = SparseColMat::try_new_from_triplets(3, 2, &triplets).expect("sparse");
6264 let governor = MemoryGovernor::global();
6265
6266 let design = SparseDesignMatrix::new(sparse.clone());
6267 let before = governor.reserved_bytes();
6268 let governed = design
6269 .try_to_dense_governed("governed sparse test")
6270 .expect("small governed densification succeeds");
6271 assert_eq!(
6272 governor.reserved_bytes(),
6273 before + 3 * 2 * std::mem::size_of::<f64>(),
6274 "governed densification must charge its dense footprint"
6275 );
6276 assert_eq!(governed.dim(), (3, 2));
6277 drop(governed);
6278 assert_eq!(
6279 governor.reserved_bytes(),
6280 before,
6281 "dropping the governed owner must release its charge"
6282 );
6283
6284 let dense = design.to_dense_arc();
6292
6293 let filler = governor
6297 .try_reserve(governor.remaining_bytes(), "test ledger filler")
6298 .expect("filling the remaining budget succeeds");
6299 let pressured = SparseDesignMatrix::new(sparse.clone());
6300 assert!(
6301 pressured
6302 .try_to_dense_governed("governed sparse test under pressure")
6303 .is_err(),
6304 "a full ledger must refuse governed densification"
6305 );
6306 let weights = array![1.0, -2.0, 0.5];
6307 let gram = xt_diag_x_symmetric(&DesignMatrix::from(sparse.clone()), &weights)
6308 .expect("streaming fallback under a full ledger");
6309 let mut expected = Array2::<f64>::zeros((2, 2));
6310 for row in 0..3 {
6311 for a in 0..2 {
6312 for b in 0..2 {
6313 expected[[a, b]] += weights[row] * dense[[row, a]] * dense[[row, b]];
6314 }
6315 }
6316 }
6317 let got = gram.as_dense().expect("dense symmetric result");
6318 for a in 0..2 {
6319 for b in 0..2 {
6320 assert!(
6321 (got[[a, b]] - expected[[a, b]]).abs() < 1e-12,
6322 "streaming fallback Gram mismatch at ({a}, {b})"
6323 );
6324 }
6325 }
6326 drop(filler);
6327 }
6328
6329 #[test]
6330 fn streaming_sparse_csc_xt_diag_x_matches_dense_signed_weights() {
6331 let sparse = SparseColMat::try_new_from_triplets(
6332 4,
6333 3,
6334 &[
6335 Triplet::new(0, 0, 1.0),
6336 Triplet::new(1, 0, 2.0),
6337 Triplet::new(2, 0, -1.0),
6338 Triplet::new(0, 1, 0.5),
6339 Triplet::new(1, 1, -3.0),
6340 Triplet::new(3, 1, 4.0),
6341 Triplet::new(0, 2, 2.0),
6342 Triplet::new(2, 2, 1.5),
6343 Triplet::new(3, 2, -0.25),
6344 ],
6345 )
6346 .expect("sparse matrix");
6347 let design = SparseDesignMatrix::new(sparse.clone());
6348 let dense = design.to_dense_arc();
6349 let weights = array![1.0, -2.0, 0.5, -1.5];
6350 let (symbolic, values) = sparse.parts();
6351 let mut got = Array2::<f64>::zeros((3, 3));
6352 streaming_sparse_csc_xt_diag_x(
6353 symbolic.col_ptr(),
6354 symbolic.row_idx(),
6355 values,
6356 4,
6357 3,
6358 weights.view(),
6359 &mut got,
6360 );
6361
6362 let mut expected = Array2::<f64>::zeros((3, 3));
6363 for row in 0..4 {
6364 for a in 0..3 {
6365 for b in 0..3 {
6366 expected[[a, b]] += weights[row] * dense[[row, a]] * dense[[row, b]];
6367 }
6368 }
6369 }
6370 let max_diff = (&got - &expected)
6371 .iter()
6372 .map(|v| v.abs())
6373 .fold(0.0_f64, f64::max);
6374 assert!(
6375 max_diff < 1e-12,
6376 "streamed sparse weighted Gram mismatch: max_diff={max_diff}"
6377 );
6378 }
6379
6380 #[test]
6381 fn block_design_row_chunk_into_fills_mixed_blocks_without_owned_child_chunks() {
6382 let eager = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]];
6383 let lazy = Arc::new(DirectFillOnlyOperator {
6384 values: array![[10.0, 11.0], [12.0, 13.0], [14.0, 15.0], [16.0, 17.0]],
6385 row_chunk_calls: AtomicUsize::new(0),
6386 });
6387 let sparse = SparseColMat::try_new_from_triplets(
6388 4,
6389 3,
6390 &[
6391 Triplet::new(0, 0, 20.0),
6392 Triplet::new(1, 1, 21.0),
6393 Triplet::new(2, 2, 22.0),
6394 Triplet::new(3, 0, 23.0),
6395 ],
6396 )
6397 .expect("sparse block");
6398 let random_effect = Arc::new(RandomEffectOperator::new(
6399 vec![Some(0), None, Some(1), Some(0)],
6400 2,
6401 ));
6402 let op = BlockDesignOperator::new(vec![
6403 DesignBlock::Dense(DenseDesignMatrix::from(eager)),
6404 DesignBlock::Dense(DenseDesignMatrix::from(Arc::clone(&lazy))),
6405 DesignBlock::Sparse(SparseDesignMatrix::new(sparse)),
6406 DesignBlock::RandomEffect(random_effect),
6407 DesignBlock::Intercept(4),
6408 ])
6409 .expect("mixed block design");
6410
6411 let mut got = Array2::<f64>::from_elem((3, 10), f64::NAN);
6412 op.row_chunk_into(1..4, got.view_mut())
6413 .expect("mixed block direct row fill");
6414
6415 assert_eq!(
6416 got,
6417 array![
6418 [3.0, 4.0, 12.0, 13.0, 0.0, 21.0, 0.0, 0.0, 0.0, 1.0],
6419 [5.0, 6.0, 14.0, 15.0, 0.0, 0.0, 22.0, 0.0, 1.0, 1.0],
6420 [7.0, 8.0, 16.0, 17.0, 23.0, 0.0, 0.0, 1.0, 0.0, 1.0],
6421 ]
6422 );
6423 assert_eq!(lazy.row_chunk_calls.load(Ordering::SeqCst), 1);
6424 }
6425
6426 #[test]
6427 fn multi_channel_row_chunk_into_crosses_boundary_without_owned_channel_chunks() {
6428 let first = Arc::new(DirectFillOnlyOperator {
6429 values: array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]],
6430 row_chunk_calls: AtomicUsize::new(0),
6431 });
6432 let second = Arc::new(DirectFillOnlyOperator {
6433 values: array![[10.0, 20.0], [30.0, 40.0], [50.0, 60.0]],
6434 row_chunk_calls: AtomicUsize::new(0),
6435 });
6436 let op = MultiChannelOperator::new(vec![
6437 DesignMatrix::Dense(DenseDesignMatrix::from(Arc::clone(&first))),
6438 DesignMatrix::Dense(DenseDesignMatrix::from(Arc::clone(&second))),
6439 ])
6440 .expect("direct-fill multi-channel operator");
6441
6442 let mut got = Array2::<f64>::from_elem((3, 2), f64::NAN);
6443 op.row_chunk_into(2..5, got.view_mut())
6444 .expect("cross-channel direct row fill");
6445
6446 assert_eq!(got, array![[5.0, 6.0], [10.0, 20.0], [30.0, 40.0]]);
6447 assert_eq!(first.row_chunk_calls.load(Ordering::SeqCst), 1);
6448 assert_eq!(second.row_chunk_calls.load(Ordering::SeqCst), 1);
6449 }
6450
6451 #[test]
6452 fn multi_channel_operator_view_paths_match_stacked_dense_reference() {
6453 let dense_channel = array![[1.0, 2.0], [0.5, -1.0], [3.0, 0.25]];
6454 let sparse_dense = array![[0.0, 1.5], [2.0, 0.0], [-1.0, 0.75]];
6455 let sparse = SparseColMat::try_new_from_triplets(
6456 3,
6457 2,
6458 &[
6459 Triplet::new(1, 0, 2.0),
6460 Triplet::new(2, 0, -1.0),
6461 Triplet::new(0, 1, 1.5),
6462 Triplet::new(2, 1, 0.75),
6463 ],
6464 )
6465 .expect("sparse channel");
6466 let op = MultiChannelOperator::new(vec![
6467 DesignMatrix::Dense(DenseDesignMatrix::from(dense_channel.clone())),
6468 DesignMatrix::from(sparse),
6469 ])
6470 .expect("multi-channel operator");
6471 let mut stacked = Array2::<f64>::zeros((6, 2));
6472 stacked.slice_mut(s![0..3, ..]).assign(&dense_channel);
6473 stacked.slice_mut(s![3..6, ..]).assign(&sparse_dense);
6474
6475 let beta = array![0.25, -0.4];
6476 let expected_apply = stacked.dot(&beta);
6477 let got_apply = op.apply(&beta);
6478 for i in 0..expected_apply.len() {
6479 assert!((expected_apply[i] - got_apply[i]).abs() < 1e-12);
6480 }
6481
6482 let probe = array![0.5, -1.0, 0.25, 1.5, -0.75, 0.2];
6483 let expected_transpose = stacked.t().dot(&probe);
6484 let got_transpose = op.apply_transpose(&probe);
6485 for i in 0..expected_transpose.len() {
6486 assert!((expected_transpose[i] - got_transpose[i]).abs() < 1e-12);
6487 }
6488
6489 let weights = array![1.0, -0.5, 0.75, 2.0, -0.25, 1.5];
6490 let weighted = stacked.clone() * weights.view().insert_axis(Axis(1));
6491 let expected_xtwx = stacked.t().dot(&weighted);
6492 let got_xtwx = op.diag_xtw_x(&weights).expect("multi-channel xtwx");
6493 for i in 0..expected_xtwx.nrows() {
6494 for j in 0..expected_xtwx.ncols() {
6495 assert!((expected_xtwx[[i, j]] - got_xtwx[[i, j]]).abs() < 1e-12);
6496 }
6497 }
6498
6499 let expected_diag = Array1::from_iter((0..2).map(|j| expected_xtwx[[j, j]]));
6500 let got_diag = op.diag_gram(&weights).expect("multi-channel diag gram");
6501 for i in 0..expected_diag.len() {
6502 assert!((expected_diag[i] - got_diag[i]).abs() < 1e-12);
6503 }
6504
6505 let y = array![1.0, 0.5, -0.25, 2.0, -1.0, 0.75];
6506 let expected_xtwy = stacked.t().dot(&(&weights * &y));
6507 let got_xtwy = op.compute_xtwy(&weights, &y).expect("multi-channel xtwy");
6508 for i in 0..expected_xtwy.len() {
6509 assert!((expected_xtwy[i] - got_xtwy[i]).abs() < 1e-12);
6510 }
6511 }
6512
6513 #[test]
6514 fn random_effect_weighted_operators_preserve_signed_curvature() {
6515 let op = RandomEffectOperator::new(vec![Some(0), Some(1), Some(0), None, Some(1)], 2);
6516 let weights = array![2.0, -3.0, 0.5, -7.0, 1.25];
6517 let expected_diag = array![2.5, -1.75];
6518
6519 let gram = op.diag_xtw_x(&weights).expect("signed random-effect Gram");
6520 assert_eq!(gram, Array2::from_diag(&expected_diag));
6521 assert_eq!(op.diag_gram(&weights).unwrap(), expected_diag);
6522
6523 let dense = array![[1.0, 2.0], [3.0, -1.0], [4.0, 0.5], [9.0, 9.0], [-2.0, 3.0]];
6524 let cross = op
6525 .weighted_cross_with_dense(&dense, &weights)
6526 .expect("signed dense/random-effect cross product");
6527 let re_dense = op.to_dense();
6528 let expected_cross = dense
6529 .t()
6530 .dot(&(&re_dense * &weights.view().insert_axis(Axis(1))));
6531 assert_eq!(cross, expected_cross);
6532
6533 let beta = array![4.0, -2.0];
6534 let finite = FiniteSignedWeightsView::try_from_array(&weights).unwrap();
6535 let normal = op.apply_weighted_normal(finite, &beta, None, 0.0);
6536 assert_eq!(normal, &expected_diag * &beta);
6537
6538 let y = array![1.0, 2.0, -4.0, 100.0, 0.5];
6539 let got_xtwy = op.compute_xtwy(&weights, &y).unwrap();
6540 let expected_xtwy = re_dense.t().dot(&(&weights * &y));
6541 assert_eq!(got_xtwy, expected_xtwy);
6542 }
6543
6544 #[test]
6545 fn conditioned_design_signed_gram_and_response_match_materialized_reference() {
6546 let raw = array![[1.0, 5.0], [2.0, -1.0], [-3.0, 2.0], [4.0, 7.0]];
6547 let conditioned = ConditionedDesign::new(
6548 DesignMatrix::Dense(DenseDesignMatrix::from(raw)),
6549 vec![(1, 2.0, 3.0)],
6550 );
6551 let dense = conditioned.to_dense();
6552 let weights = array![2.0, -4.0, 0.5, -1.5];
6553 let weighted = &dense * &weights.view().insert_axis(Axis(1));
6554 let expected_gram = dense.t().dot(&weighted);
6555 let got_gram = conditioned.diag_xtw_x(&weights).unwrap();
6556 assert!(
6557 (&got_gram - &expected_gram)
6558 .iter()
6559 .all(|value| value.abs() < 1e-12)
6560 );
6561 let got_diag = conditioned.diag_gram(&weights).unwrap();
6562 assert!(
6563 (&got_diag - &expected_gram.diag())
6564 .iter()
6565 .all(|value| value.abs() < 1e-12)
6566 );
6567
6568 let y = array![0.5, -2.0, 3.0, 1.25];
6569 let expected_xtwy = dense.t().dot(&(&weights * &y));
6570 let got_xtwy = conditioned.compute_xtwy(&weights, &y).unwrap();
6571 assert!(
6572 (&got_xtwy - &expected_xtwy)
6573 .iter()
6574 .all(|value| value.abs() < 1e-12)
6575 );
6576 }
6577
6578 #[test]
6579 fn weighted_operator_certification_reports_smallest_nonfinite_row() {
6580 let channel = DesignMatrix::Dense(DenseDesignMatrix::from(array![[1.0], [2.0], [3.0]]));
6581 let op = MultiChannelOperator::new(vec![channel]).unwrap();
6582 let bad = array![1.0, f64::NAN, f64::INFINITY];
6583
6584 for err in [
6585 op.diag_xtw_x(&bad).unwrap_err(),
6586 op.diag_gram(&bad).unwrap_err(),
6587 op.compute_xtwy(&bad, &array![1.0, 1.0, 1.0]).unwrap_err(),
6588 ] {
6589 assert!(err.contains("row 1"), "unexpected diagnostic: {err}");
6590 }
6591 }
6592
6593 #[test]
6600 fn block_design_fused_dense_cross_matches_stacked_reference_xtwx() {
6601 let b0 = array![
6602 [1.0, 2.0],
6603 [0.5, -1.0],
6604 [3.0, 0.25],
6605 [-2.0, 1.5],
6606 [0.75, -0.5],
6607 ];
6608 let b1 = array![
6609 [-1.0, 0.5, 2.0],
6610 [1.5, -0.25, 0.0],
6611 [0.0, 1.0, -1.5],
6612 [2.0, 0.5, 1.0],
6613 [-0.5, -1.0, 0.25],
6614 ];
6615 let b2 = array![[0.5], [-1.0], [2.0], [0.25], [-0.75]];
6616
6617 let mut stacked = Array2::<f64>::zeros((5, 6));
6618 stacked.slice_mut(s![.., 0..2]).assign(&b0);
6619 stacked.slice_mut(s![.., 2..5]).assign(&b1);
6620 stacked.slice_mut(s![.., 5..6]).assign(&b2);
6621
6622 let blocks = vec![
6623 DesignBlock::Dense(DenseDesignMatrix::from(b0)),
6624 DesignBlock::Dense(DenseDesignMatrix::from(b1)),
6625 DesignBlock::Dense(DenseDesignMatrix::from(b2)),
6626 ];
6627 let op = BlockDesignOperator::new(blocks).expect("block design");
6628
6629 let weights = array![1.5, -0.5, 2.0, -1.0, 0.75];
6631 let weighted = stacked.clone() * weights.view().insert_axis(Axis(1));
6632 let expected = stacked.t().dot(&weighted);
6633
6634 let got = op.diag_xtw_x(&weights).expect("block fused xtwx");
6635 assert_eq!(got.dim(), (6, 6));
6636 let max_diff = (&got - &expected)
6637 .iter()
6638 .map(|v| v.abs())
6639 .fold(0.0_f64, f64::max);
6640 assert!(
6641 max_diff < 1e-10,
6642 "fused block Dense×Dense Gram mismatch: max_diff={max_diff}"
6643 );
6644 }
6645
6646 #[test]
6647 fn block_design_intercept_cross_and_diag_are_sign_honest() {
6648 let x = array![[2.0], [5.0], [-1.0], [3.0]];
6655 let mut stacked = Array2::<f64>::zeros((4, 2));
6656 stacked.column_mut(0).fill(1.0);
6657 stacked.slice_mut(s![.., 1..2]).assign(&x);
6658
6659 let blocks = vec![
6660 DesignBlock::Intercept(4),
6661 DesignBlock::Dense(DenseDesignMatrix::from(x)),
6662 ];
6663 let op = BlockDesignOperator::new(blocks).expect("block design");
6664
6665 let weights = array![3.0, -1.0, 2.0, -0.5];
6666 let weighted = stacked.clone() * weights.view().insert_axis(Axis(1));
6667 let expected = stacked.t().dot(&weighted);
6668
6669 let got = op.diag_xtw_x(&weights).expect("block fused xtwx");
6670 assert_eq!(got.dim(), (2, 2));
6671 let max_diff = (&got - &expected)
6672 .iter()
6673 .map(|v| v.abs())
6674 .fold(0.0_f64, f64::max);
6675 assert!(
6676 max_diff < 1e-10,
6677 "intercept-block Gram mismatch: got={got:?} expected={expected:?} max_diff={max_diff}"
6678 );
6679
6680 let intercept_block = &op.blocks[0];
6682 let diag = intercept_block
6683 .diag_xtw_x(&weights)
6684 .expect("intercept diag_xtw_x");
6685 assert!((diag[[0, 0]] - weights.sum()).abs() < 1e-12);
6686 let gram = intercept_block
6687 .diag_gram(&weights)
6688 .expect("intercept diag_gram");
6689 assert!((gram[0] - weights.sum()).abs() < 1e-12);
6690 }
6691
6692 #[test]
6693 #[should_panic(expected = "ReparamOperator: X cols (2) must match Qs rows (3)")]
6694 fn reparam_operator_rejects_incompatible_transform_shape() {
6695 let x = array![[1.0, 2.0], [0.5, -1.0]];
6696 let qs = Arc::new(Array2::<f64>::zeros((3, 1)));
6697 ReparamOperator::new(DesignMatrix::Dense(DenseDesignMatrix::from(x)), qs);
6698 }
6699
6700 #[test]
6712 fn coefficient_transform_operator_exposes_cached_dense_to_block_dispatch() {
6713 let inner = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
6714 let transform = array![[0.5, -1.0, 2.0], [1.0, 0.0, -0.5]];
6715 let expected = inner.dot(&transform);
6716
6717 let op =
6718 CoefficientTransformOperator::new(DenseDesignMatrix::from(inner), transform.clone())
6719 .expect("coefficient transform operator");
6720 let dense_design = DenseDesignMatrix::from(Arc::new(op));
6721
6722 let probe = Array1::from_elem(3, 1.0);
6727 let warmed = dense_design.apply_transpose(&probe);
6728 assert_eq!(warmed.len(), expected.ncols());
6729
6730 let dense_ref = dense_design
6731 .as_dense_ref()
6732 .expect("DenseDesignMatrix::as_dense_ref must reach the cached X·T");
6733 assert_eq!(dense_ref.dim(), expected.dim());
6734 for ((r, c), v) in expected.indexed_iter() {
6735 assert!((dense_ref[[r, c]] - v).abs() < 1e-12);
6736 }
6737 }
6738
6739 #[test]
6740 fn coefficient_transform_operator_preserves_lazy_inner_storage() {
6741 let inner_values = array![[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]];
6742 let transform = array![[0.5, -1.0], [1.0, 0.25]];
6743 let expected = inner_values.dot(&transform);
6744 let DesignMatrix::Dense(inner) = no_densify_design(inner_values) else {
6745 panic!("no-densify fixture must be dense-operator-backed");
6746 };
6747 let op = CoefficientTransformOperator::new(inner, transform)
6748 .expect("coefficient transform operator");
6749 let dense_design = DenseDesignMatrix::from(Arc::new(op));
6750
6751 let probe = Array1::from_elem(2, 1.0);
6752 let got = dense_design.apply(&probe);
6753 let want = expected.dot(&probe);
6754 for (got_i, want_i) in got.iter().zip(want.iter()) {
6755 assert!((got_i - want_i).abs() < 1e-12);
6756 }
6757 assert!(
6758 dense_design.as_dense_ref().is_none(),
6759 "coefficient transform must not materialize an operator-backed inner design"
6760 );
6761 }
6762
6763 #[test]
6764 fn design_matrix_hstack_preserves_lazy_blocks() {
6765 let left_dense = array![[1.0, 2.0], [3.0, 4.0]];
6766 let right_dense = array![[5.0], [6.0]];
6767 let left = no_densify_design(left_dense.clone());
6768 let right = no_densify_design(right_dense.clone());
6769 let stacked = DesignMatrix::hstack(vec![left, right]).expect("stacked design");
6770
6771 assert!(stacked.as_dense_ref().is_none());
6772 assert!(!stacked.is_materialized_dense());
6773 assert!(stacked.is_operator_backed());
6774 assert_eq!(stacked.nrows(), 2);
6775 assert_eq!(stacked.ncols(), 3);
6776
6777 let beta = array![0.25, -0.5, 2.0];
6778 let expected = array![9.25, 10.75];
6779 let got = stacked.dot(&beta);
6780 for i in 0..expected.len() {
6781 assert!((got[i] - expected[i]).abs() < 1e-12);
6782 }
6783
6784 let chunk = stacked
6785 .try_row_chunk(0..2)
6786 .expect("stacked.try_row_chunk must succeed");
6787 assert_eq!(chunk, array![[1.0, 2.0, 5.0], [3.0, 4.0, 6.0]]);
6788 }
6789
6790 #[test]
6791 #[should_panic(expected = "DesignMatrix::as_dense_cow called on operator-backed design")]
6792 fn design_matrix_as_dense_cow_rejects_operator_backed_designs() {
6793 let design = no_densify_design(array![[1.0, 2.0], [3.0, 4.0]]);
6794 design.as_dense_cow();
6795 }
6796
6797 #[test]
6798 fn sparse_factorized_solve_matches_dense_operator_solve() {
6799 let triplets = vec![
6800 Triplet::new(0usize, 0usize, 1.0),
6801 Triplet::new(1, 0, 2.0),
6802 Triplet::new(1, 1, -1.0),
6803 Triplet::new(2, 1, 3.0),
6804 Triplet::new(2, 2, 0.5),
6805 ];
6806 let sparse = SparseColMat::try_new_from_triplets(3, 3, &triplets)
6807 .expect("sparse design should build");
6808 let sparse_design = DesignMatrix::from(sparse);
6809 let dense_design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(
6810 sparse_design.to_dense(),
6811 ));
6812 let weights = array![1.5, 0.75, 2.0];
6813 let rhs = array![1.0, -0.5, 2.0];
6814 let penalty = Array2::from_diag(&array![0.25, 0.5, 0.75]);
6815
6816 let sparse_sol = sparse_design
6817 .solve_system(&weights, &rhs, Some(&penalty))
6818 .expect("sparse solve should factorize natively");
6819 let dense_sol = dense_design
6820 .solve_system(&weights, &rhs, Some(&penalty))
6821 .expect("dense solve should factorize");
6822
6823 for i in 0..rhs.len() {
6824 assert!(
6825 (sparse_sol[i] - dense_sol[i]).abs() < 1e-10,
6826 "solution mismatch at {i}: sparse={} dense={}",
6827 sparse_sol[i],
6828 dense_sol[i]
6829 );
6830 }
6831 }
6832
6833 #[test]
6834 fn solve_system_stabilizes_indefinite_penalty_and_returns_finite_solution() {
6835 let design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(array![
6836 [1.0, 0.0],
6837 [0.0, 0.0]
6838 ]));
6839 let weights = array![1.0, 1.0];
6840 let rhs = array![2.0, 0.0];
6841 let penalty = array![[0.0, 0.0], [0.0, -1e-12]];
6842
6843 let beta = design
6844 .solve_system(&weights, &rhs, Some(&penalty))
6845 .expect("solve_system should stabilize indefinite systems");
6846
6847 assert!(beta.iter().all(|v| v.is_finite()));
6848 assert!((beta[0] - 2.0).abs() < 1e-10);
6849 assert!(beta[1].abs() < 1e-8);
6850 }
6851
6852 #[test]
6853 fn explicit_matrix_free_pcg_matches_exact_large_dense_weighted_penalized_solve() {
6854 let n = 48usize;
6855 let p = 520usize;
6856 let mut x = Array2::<f64>::zeros((n, p));
6857 for i in 0..n {
6858 for j in 0..p {
6859 x[[i, j]] = (((i + 3) * (j + 5)) % 17) as f64 / 17.0
6860 + 0.02 * (i as f64)
6861 + 0.001 * (j as f64);
6862 }
6863 }
6864 let design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x.clone()));
6865 let weights = Array1::from_iter((0..n).map(|i| 0.5 + (i as f64) / (2.0 * n as f64)));
6866 let rhs = Array1::from_iter((0..p).map(|j| ((j % 13) as f64 - 6.0) / 13.0));
6867 let penalty = Array2::from_diag(&Array1::from_iter(
6868 (0..p).map(|j| 0.1 + 0.005 * ((j % 7) as f64)),
6869 ));
6870 let ridge = 1e-8;
6871
6872 let pcg = design
6873 .solve_system_matrix_free_pcg(&weights, &rhs, Some(&penalty), ridge)
6874 .expect("matrix-free pcg solve");
6875 let exact = exact_weighted_penalized_solve(&x, &weights, &rhs, &penalty, ridge);
6876 for i in 0..p {
6877 assert!(
6878 (pcg[i] - exact[i]).abs() < 1e-5,
6879 "solution mismatch at {i}: pcg={} exact={}",
6880 pcg[i],
6881 exact[i]
6882 );
6883 }
6884 let mut h = x
6885 .t()
6886 .dot(&(x.clone() * weights.view().insert_axis(Axis(1))));
6887 h += &penalty;
6888 for i in 0..p {
6889 h[[i, i]] += ridge;
6890 }
6891 let residual = h.dot(&pcg) - &rhs;
6892 let residual_norm = residual.dot(&residual).sqrt();
6893 assert!(residual_norm < 1e-4, "residual_norm={residual_norm}");
6894 }
6895
6896 #[test]
6897 fn policy_solve_matches_explicit_matrix_free_pcg_on_large_dense_system() {
6898 let n = 40usize;
6899 let p = 520usize;
6900 let mut x = Array2::<f64>::zeros((n, p));
6901 for i in 0..n {
6902 for j in 0..p {
6903 x[[i, j]] = (((2 * i + j + 11) % 23) as f64 / 23.0) + 0.0005 * (j as f64);
6904 }
6905 }
6906 let design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x));
6907 let weights = Array1::from_iter((0..n).map(|i| 1.0 + 0.01 * i as f64));
6908 let rhs = Array1::from_iter((0..p).map(|j| ((j % 5) as f64) - 2.0));
6909 let penalty = Array2::from_diag(&Array1::from_iter(
6910 (0..p).map(|j| 0.2 + 0.01 * ((j % 3) as f64)),
6911 ));
6912 let ridge_floor = 1e-8;
6913
6914 let explicit = design
6915 .solve_system_matrix_free_pcg(&weights, &rhs, Some(&penalty), ridge_floor)
6916 .expect("explicit pcg");
6917 let policy = design
6918 .solve_systemwith_policy(
6919 &weights,
6920 &rhs,
6921 Some(&penalty),
6922 ridge_floor,
6923 RidgePolicy::solver_only(),
6924 )
6925 .expect("policy solve");
6926 for i in 0..p {
6927 let tol = 1e-5 * (1.0 + explicit[i].abs());
6936 assert!(
6937 (explicit[i] - policy[i]).abs() < tol,
6938 "policy mismatch at {i}: explicit={} policy={} (tol={tol})",
6939 explicit[i],
6940 policy[i]
6941 );
6942 }
6943 }
6944
6945 #[test]
6946 fn explicit_matrix_free_pcg_reports_convergence_diagnostics() {
6947 let n = 36usize;
6948 let p = 2160usize;
6949 let mut x = Array2::<f64>::zeros((n, p));
6950 for i in 0..n {
6951 for j in 0..p {
6952 x[[i, j]] = (((3 * i + 5 * j + 7) % 29) as f64 / 29.0)
6953 + 0.015 * (i as f64)
6954 + 1e-4 * j as f64;
6955 }
6956 }
6957 let design = DesignMatrix::Dense(crate::matrix::DenseDesignMatrix::from(x.clone()));
6958 assert!(design.should_use_matrix_free_pcg());
6959 let weights = Array1::from_iter((0..n).map(|i| 0.75 + 0.01 * i as f64));
6960 let rhs = Array1::from_iter((0..p).map(|j| ((j % 9) as f64 - 4.0) / 9.0));
6961 let penalty = Array2::from_diag(&Array1::from_iter(
6962 (0..p).map(|j| 0.05 + 0.002 * ((j % 11) as f64)),
6963 ));
6964 let ridge = 1e-8;
6965
6966 let (pcg, info): (Array1<f64>, PcgSolveInfo) = design
6967 .solve_system_matrix_free_pcg_with_info(&weights, &rhs, Some(&penalty), ridge)
6968 .expect("pcg with info");
6969 assert!(info.converged);
6970 assert!(info.iterations > 0);
6971 assert!(info.relative_residual_norm.is_finite());
6972 assert!(info.relative_residual_norm < 1e-6);
6973
6974 let exact = exact_weighted_penalized_solve(&x, &weights, &rhs, &penalty, ridge);
6975 for i in 0..p {
6976 assert!(
6977 (pcg[i] - exact[i]).abs() < 1e-5,
6978 "solution mismatch at {i}: pcg={} exact={}",
6979 pcg[i],
6980 exact[i]
6981 );
6982 }
6983 }
6984
6985 #[test]
6986 fn compute_xtwy_dense_allocationfree_matches_matvec() {
6987 let n = 2_000usize;
6988 let p = 64usize;
6989 let mut x = Array2::<f64>::zeros((n, p));
6990 let mut y = Array1::<f64>::zeros(n);
6991 let mut w = Array1::<f64>::zeros(n);
6992 for i in 0..n {
6993 y[i] = ((i % 17) as f64 - 8.0) * 0.1;
6994 w[i] = 0.25 + ((i % 11) as f64) * 0.05;
6995 for j in 0..p {
6996 x[[i, j]] = (((i * 13 + j * 7) % 97) as f64) / 97.0;
6997 }
6998 }
6999
7000 let reference = {
7001 let wy = Array1::from_shape_fn(n, |i| y[i] * w[i]);
7002 fast_atv(&x, &wy)
7003 };
7004 let fused = dense_transpose_weighted_response(&x, &w, &y, None);
7005 for j in 0..p {
7006 assert!(
7007 (reference[j] - fused[j]).abs() < 1e-10,
7008 "mismatch at column {j}: ref={} fused={}",
7009 reference[j],
7010 fused[j]
7011 );
7012 }
7013 }
7014
7015 #[test]
7016 fn large_lazy_dense_materialization_streams_chunks_without_to_dense_fallback() {
7017 let n = 11_000usize;
7018 let p = 128usize;
7019 let op = Arc::new(ChunkOnlyOperator {
7020 n,
7021 p,
7022 row_chunk_calls: AtomicUsize::new(0),
7023 materialization_policy: None,
7024 });
7025 let design = DenseDesignMatrix::from(Arc::clone(&op));
7026
7027 let dense = design.to_dense_arc();
7028
7029 assert_eq!(dense.dim(), (n, p));
7030 assert!(
7031 op.row_chunk_calls.load(Ordering::SeqCst) > 1,
7032 "expected dense materialization to stream more than one row chunk"
7033 );
7034 for &(i, j) in &[(0, 0), (8_191, 127), (8_192, 0), (10_999, 64)] {
7035 assert_eq!(dense[[i, j]], op.value(i, j));
7036 }
7037 assert!(
7038 design.as_dense_ref().is_some(),
7039 "as_dense_ref must expose a populated LazyDense memo so storage certificates can observe it"
7040 );
7041 }
7042
7043 #[test]
7044 fn construction_policy_survives_nested_coefficient_and_block_operators() {
7045 let strict = ResourcePolicy::analytic_operator_required().material_policy();
7046 let op = Arc::new(ChunkOnlyOperator {
7047 n: 32,
7048 p: 2,
7049 row_chunk_calls: AtomicUsize::new(0),
7050 materialization_policy: Some(strict),
7051 });
7052 let inner = DenseDesignMatrix::from(Arc::clone(&op));
7053 let transformed = CoefficientTransformOperator::new(inner, Array2::<f64>::eye(2))
7054 .expect("coefficient transform");
7055 let block = BlockDesignOperator::new(vec![DesignBlock::Dense(DenseDesignMatrix::from(
7056 Arc::new(transformed),
7057 ))])
7058 .expect("block design");
7059 let design = DenseDesignMatrix::from(Arc::new(block));
7060
7061 let error = design
7062 .try_to_dense_arc("nested construction-policy regression")
7063 .expect_err("strict construction policy must survive every wrapper");
7064 assert!(error.contains("construction policy requires streamed storage"));
7065 assert_eq!(op.row_chunk_calls.load(Ordering::SeqCst), 0);
7066 assert!(design.as_dense_ref().is_none());
7067 }
7068
7069 #[test]
7072 fn governed_to_dense_reserves_and_policy_refusals_are_typed() {
7073 let op = Arc::new(ChunkOnlyOperator {
7074 n: 128,
7075 p: 4,
7076 row_chunk_calls: AtomicUsize::new(0),
7077 materialization_policy: None,
7078 });
7079 let design = DesignMatrix::Dense(DenseDesignMatrix::from(Arc::clone(&op)));
7080
7081 let dense = design
7082 .try_to_dense_governed("governed dense regression")
7083 .expect("small governed materialization");
7084 assert_eq!(dense.dim(), (128, 4));
7085 assert_eq!(dense.reserved_bytes(), 128 * 4 * std::mem::size_of::<f64>());
7086
7087 let strict = ResourcePolicy::analytic_operator_required().material_policy();
7088 let err = design
7089 .try_to_dense_governed_with_policy(&strict, "regression strict refuses")
7090 .expect_err("strict policy must refuse lazy materialization");
7091 assert!(matches!(err, MatrixMaterializationError::Forbidden { .. }));
7092
7093 let mut tight = ResourcePolicy::default_library().material_policy();
7094 tight.max_single_dense_bytes = 1;
7095 let size_err = design
7096 .try_to_dense_governed_with_policy(&tight, "regression tight refuses")
7097 .expect_err("undersized cap must refuse lazy materialization");
7098 assert!(matches!(
7099 size_err,
7100 MatrixMaterializationError::TooLarge { .. }
7101 ));
7102 }
7103
7104 #[test]
7105 fn try_to_dense_by_chunks_writes_directly_into_output_slices() {
7106 let n = 11_000usize;
7107 let p = 128usize;
7108 let op = Arc::new(ChunkOnlyOperator {
7109 n,
7110 p,
7111 row_chunk_calls: AtomicUsize::new(0),
7112 materialization_policy: None,
7113 });
7114 let design = DesignMatrix::Dense(DenseDesignMatrix::from(Arc::clone(&op)));
7115
7116 let dense = design
7117 .try_to_dense_by_chunks("large chunked regression")
7118 .expect("chunked materialization");
7119
7120 assert_eq!(dense.dim(), (n, p));
7121 assert!(
7122 op.row_chunk_calls.load(Ordering::SeqCst) > 1,
7123 "expected direct chunked conversion to use bounded row chunks"
7124 );
7125 for &(i, j) in &[(1, 7), (4_096, 12), (8_193, 63), (10_998, 127)] {
7126 assert_eq!(dense[[i, j]], op.value(i, j));
7127 }
7128 }
7129
7130 #[test]
7131 fn tensor_product_design_operator_matches_dense_2d() {
7132 use super::{DenseDesignOperator, TensorProductDesignOperator};
7133
7134 let n = 10;
7136 let q1 = 4;
7137 let q2 = 3;
7138 let mut b1 = Array2::<f64>::zeros((n, q1));
7139 let mut b2 = Array2::<f64>::zeros((n, q2));
7140 for i in 0..n {
7142 let t1 = i as f64 / (n - 1) as f64 * (q1 - 1) as f64;
7143 let j1 = (t1.floor() as usize).min(q1 - 2);
7144 let frac1 = t1 - j1 as f64;
7145 b1[[i, j1]] = 1.0 - frac1;
7146 b1[[i, j1 + 1]] = frac1;
7147
7148 let t2 = i as f64 / (n - 1) as f64 * (q2 - 1) as f64;
7149 let j2 = (t2.floor() as usize).min(q2 - 2);
7150 let frac2 = t2 - j2 as f64;
7151 b2[[i, j2]] = 1.0 - frac2;
7152 b2[[i, j2 + 1]] = frac2;
7153 }
7154
7155 let op = TensorProductDesignOperator::new(vec![Arc::new(b1.clone()), Arc::new(b2.clone())])
7156 .unwrap();
7157
7158 let p = q1 * q2;
7160 let mut dense = Array2::<f64>::zeros((n, p));
7161 for i in 0..n {
7162 for j1 in 0..q1 {
7163 for j2 in 0..q2 {
7164 dense[[i, j1 * q2 + j2]] = b1[[i, j1]] * b2[[i, j2]];
7165 }
7166 }
7167 }
7168
7169 let op_dense = op.to_dense();
7171 let max_diff = (&op_dense - &dense)
7172 .iter()
7173 .map(|v: &f64| v.abs())
7174 .fold(0.0f64, f64::max);
7175 assert!(max_diff < 1e-14, "to_dense mismatch: max_diff={max_diff}");
7176
7177 let beta = Array1::from_vec((0..p).map(|j| (j as f64 + 1.0) * 0.1).collect());
7179 let ref_result = dense.dot(&beta);
7180 let op_result = op.apply(&beta);
7181 let max_diff = (&op_result - &ref_result)
7182 .iter()
7183 .map(|v: &f64| v.abs())
7184 .fold(0.0f64, f64::max);
7185 assert!(max_diff < 1e-12, "apply mismatch: max_diff={max_diff}");
7186
7187 let v = Array1::from_vec((0..n).map(|i| (i as f64 + 1.0) * 0.3).collect());
7189 let ref_xt_v = dense.t().dot(&v);
7190 let op_xt_v = op.apply_transpose(&v);
7191 let max_diff = (&op_xt_v - &ref_xt_v)
7192 .iter()
7193 .map(|v: &f64| v.abs())
7194 .fold(0.0f64, f64::max);
7195 assert!(
7196 max_diff < 1e-12,
7197 "apply_transpose mismatch: max_diff={max_diff}"
7198 );
7199
7200 let w = Array1::from_vec((0..n).map(|i| 1.0 + i as f64 * 0.1).collect());
7202 let ref_xtwx = {
7203 let mut out = Array2::<f64>::zeros((p, p));
7204 for i in 0..n {
7205 for a in 0..p {
7206 for b in 0..p {
7207 out[[a, b]] += w[i] * dense[[i, a]] * dense[[i, b]];
7208 }
7209 }
7210 }
7211 out
7212 };
7213 let op_xtwx = op.diag_xtw_x(&w).unwrap();
7214 let max_diff = (&op_xtwx - &ref_xtwx)
7215 .iter()
7216 .map(|v: &f64| v.abs())
7217 .fold(0.0f64, f64::max);
7218 assert!(max_diff < 1e-10, "diag_xtw_x mismatch: max_diff={max_diff}");
7219 }
7220
7221 #[test]
7222 fn tensor_product_design_operator_3d() {
7223 use super::{DenseDesignOperator, TensorProductDesignOperator};
7224
7225 let n = 8;
7226 let dims = [3, 2, 2];
7227 let mut marginals: Vec<Array2<f64>> = Vec::new();
7228 for &q in &dims {
7229 let mut b = Array2::<f64>::zeros((n, q));
7230 for i in 0..n {
7231 let t = i as f64 / (n - 1) as f64 * (q - 1) as f64;
7232 let j = (t.floor() as usize).min(q - 2);
7233 let frac = t - j as f64;
7234 b[[i, j]] = 1.0 - frac;
7235 b[[i, j + 1]] = frac;
7236 }
7237 marginals.push(b);
7238 }
7239
7240 let op = TensorProductDesignOperator::new(
7241 marginals.iter().map(|m| Arc::new(m.clone())).collect(),
7242 )
7243 .unwrap();
7244
7245 let p: usize = dims.iter().copied().product();
7247 let mut dense = Array2::<f64>::zeros((n, p));
7248 for i in 0..n {
7249 for j0 in 0..dims[0] {
7250 for j1 in 0..dims[1] {
7251 for j2 in 0..dims[2] {
7252 let col = j0 * dims[1] * dims[2] + j1 * dims[2] + j2;
7253 dense[[i, col]] =
7254 marginals[0][[i, j0]] * marginals[1][[i, j1]] * marginals[2][[i, j2]];
7255 }
7256 }
7257 }
7258 }
7259
7260 let op_dense = op.to_dense();
7261 let max_diff = (&op_dense - &dense)
7262 .iter()
7263 .map(|v: &f64| v.abs())
7264 .fold(0.0f64, f64::max);
7265 assert!(
7266 max_diff < 1e-14,
7267 "3D to_dense mismatch: max_diff={max_diff}"
7268 );
7269
7270 let beta = Array1::from_vec((0..p).map(|j| (j as f64).sin()).collect());
7272 let xb = op.apply(&beta);
7273 let xtxb = op.apply_transpose(&xb);
7274 let ref_xtxb = dense.t().dot(&dense.dot(&beta));
7275 let max_diff = (&xtxb - &ref_xtxb)
7276 .iter()
7277 .map(|v: &f64| v.abs())
7278 .fold(0.0f64, f64::max);
7279 assert!(max_diff < 1e-10, "3D X'Xβ mismatch: max_diff={max_diff}");
7280 }
7281
7282 #[test]
7283 fn sparse_weighted_crossprod_parallel_path_matches_dense_reference() {
7284 use faer::sparse::Triplet;
7285
7286 let n = 4096;
7287 let p = 192;
7288 let mut triplets = Vec::with_capacity(n * 4);
7289 let mut dense = Array2::<f64>::zeros((n, p));
7290 for i in 0..n {
7291 let base = (i * 37) % p;
7292 for k in 0..4 {
7293 let col = (base + k * 11) % p;
7294 let val = ((i + 3 * k + 1) as f64).sin() * 0.25 + 0.5;
7295 triplets.push(Triplet::new(i, col, val));
7296 dense[[i, col]] = val;
7297 }
7298 }
7299 let sparse = faer::sparse::SparseColMat::try_new_from_triplets(n, p, &triplets).unwrap();
7300 let design = DesignMatrix::Sparse(SparseDesignMatrix::new(sparse));
7301 let weights = Array1::from_iter((0..n).map(|i| match i % 7 {
7302 0 => 0.0,
7303 r => 0.5 + r as f64 * 0.125,
7304 }));
7305
7306 let got = <DesignMatrix as LinearOperator>::xt_diag_x_signed_op(
7307 &design,
7308 FiniteSignedWeightsView::try_from_array(&weights).unwrap(),
7309 )
7310 .unwrap();
7311 let mut reference = Array2::<f64>::zeros((p, p));
7312 for i in 0..n {
7313 let wi = weights[i];
7314 if wi == 0.0 {
7315 continue;
7316 }
7317 for a in 0..p {
7318 let xa = dense[[i, a]];
7319 if xa == 0.0 {
7320 continue;
7321 }
7322 for b in 0..p {
7323 reference[[a, b]] += wi * xa * dense[[i, b]];
7324 }
7325 }
7326 }
7327 let max_diff = (&got - &reference)
7328 .iter()
7329 .map(|v: &f64| v.abs())
7330 .fold(0.0_f64, f64::max);
7331 assert!(
7332 max_diff < 1e-10,
7333 "sparse xtwx mismatch: max_diff={max_diff}"
7334 );
7335
7336 let got_diag = design.diag_gram(&weights).unwrap();
7337 let ref_diag = reference.diag().to_owned();
7338 let max_diag_diff = (&got_diag - &ref_diag)
7339 .iter()
7340 .map(|v: &f64| v.abs())
7341 .fold(0.0_f64, f64::max);
7342 assert!(
7343 max_diag_diff < 1e-10,
7344 "sparse diag gram mismatch: max_diff={max_diag_diff}"
7345 );
7346 }
7347
7348 #[test]
7349 fn rowwise_kronecker_sparse_structured_xtwx_matches_dense_reference() {
7350 use faer::sparse::Triplet;
7351
7352 let n = 2048;
7353 let p_cov = 64;
7354 let p_time = 6;
7355 let mut triplets = Vec::with_capacity(n * 3);
7356 let mut cov_dense = Array2::<f64>::zeros((n, p_cov));
7357 for i in 0..n {
7358 let base = (i * 17) % p_cov;
7359 for k in 0..3 {
7360 let col = (base + k * 7) % p_cov;
7361 let val = 0.2 + (((i + k) % 13) as f64) / 17.0;
7362 triplets.push(Triplet::new(i, col, val));
7363 cov_dense[[i, col]] = val;
7364 }
7365 }
7366 let cov_sparse =
7367 faer::sparse::SparseColMat::try_new_from_triplets(n, p_cov, &triplets).unwrap();
7368 let cov = DesignMatrix::Sparse(SparseDesignMatrix::new(cov_sparse));
7369 let mut time = Array2::<f64>::zeros((n, p_time));
7370 for i in 0..n {
7371 for t in 0..p_time {
7372 time[[i, t]] = (((i + 1) * (t + 3)) as f64).cos() * 0.1 + 0.4;
7373 }
7374 }
7375 let op = RowwiseKroneckerOperator::new(cov, Arc::new(time.clone())).unwrap();
7376 let weights = Array1::from_iter((0..n).map(|i| 0.25 + ((i % 11) as f64) * 0.05));
7377 let got = op.diag_xtw_x(&weights).unwrap();
7378
7379 let p_total = p_cov * p_time;
7380 let mut reference = Array2::<f64>::zeros((p_total, p_total));
7381 for i in 0..n {
7382 for c1 in 0..p_cov {
7383 let x1 = cov_dense[[i, c1]];
7384 if x1 == 0.0 {
7385 continue;
7386 }
7387 for t1 in 0..p_time {
7388 let a = c1 * p_time + t1;
7389 let xa = x1 * time[[i, t1]];
7390 for c2 in 0..p_cov {
7391 let x2 = cov_dense[[i, c2]];
7392 if x2 == 0.0 {
7393 continue;
7394 }
7395 for t2 in 0..p_time {
7396 let b = c2 * p_time + t2;
7397 reference[[a, b]] += weights[i] * xa * x2 * time[[i, t2]];
7398 }
7399 }
7400 }
7401 }
7402 }
7403 let max_diff = (&got - &reference)
7404 .iter()
7405 .map(|v: &f64| v.abs())
7406 .fold(0.0_f64, f64::max);
7407 assert!(
7408 max_diff < 1e-9,
7409 "rowwise kronecker sparse xtwx mismatch: max_diff={max_diff}"
7410 );
7411 }
7412
7413 #[test]
7414 fn embedded_column_block_zero_row_local_materializes_empty_global_width() {
7415 let local = Array2::<f64>::zeros((0, 0));
7416 let out = EmbeddedColumnBlock::new(&local, 2..5, 7).materialize();
7417 assert_eq!(out.dim(), (0, 7));
7418 }
7419}