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