1use super::reml_outer_engine::{
11 BarrierConfig, ContractedPsiSecondOrderFn, DispersionHandling, EvalMode, FixedDriftDerivFn,
12 HessianDerivativeProvider, HessianFactorization, HyperCoord, HyperCoordPairResult,
13 InnerSolution, InnerSolutionBuilder, PenaltyCoordinate, PenaltyLogdetDerivs,
14 PenaltySubspaceTrace, RemlLamlResult, penalty_matrix_root, reml_laml_evaluate,
15};
16use crate::model_types::ProjectedKktResidual;
17use gam_linalg::faer_ndarray::fast_xt_diag_y;
18use ndarray::{Array1, Array2};
19use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
20use rayon::slice::ParallelSliceMut;
21use std::sync::Arc;
22
23pub(crate) const DENSE_WEIGHTED_PRODUCT_PAR_FLOPS: usize = 8_000_000;
32pub(crate) const DENSE_ROW_SCALE_PAR_CELLS: usize = 64 * 1024;
33
34#[derive(Clone, Copy)]
35pub(crate) enum DenseRowScaleMode {
36 Direct,
37 InversePositiveOrZero,
38}
39
40pub(crate) fn row_scale_dense_into(x: &Array2<f64>, scale: &Array1<f64>, out: &mut Array2<f64>) {
47 assert_eq!(x.nrows(), scale.len(), "scale length must match row count");
48 if out.raw_dim() != x.raw_dim() {
49 *out = Array2::<f64>::zeros(x.raw_dim());
50 }
51 out.assign(x);
52 row_scale_dense_in_place(out, scale, DenseRowScaleMode::Direct);
53}
54
55pub(crate) fn row_scale_dense_in_place_by_inverse_positive_or_zero(
58 out: &mut Array2<f64>,
59 scale: &Array1<f64>,
60) {
61 row_scale_dense_in_place(out, scale, DenseRowScaleMode::InversePositiveOrZero);
62}
63
64pub(crate) fn row_scale_dense_in_place(
65 out: &mut Array2<f64>,
66 scale: &Array1<f64>,
67 mode: DenseRowScaleMode,
68) {
69 assert_eq!(
70 out.nrows(),
71 scale.len(),
72 "scale length must match row count"
73 );
74 let ncols = out.ncols();
75 if ncols == 0 {
76 return;
77 }
78
79 let cells = out.nrows().saturating_mul(ncols);
80 if cells >= DENSE_ROW_SCALE_PAR_CELLS
81 && rayon::current_num_threads() > 1
82 && out.is_standard_layout()
83 && let Some(slice) = out.as_slice_memory_order_mut()
84 {
85 slice
86 .par_chunks_mut(ncols)
87 .zip(
88 scale
89 .as_slice()
90 .expect("Array1 must be contiguous")
91 .par_iter(),
92 )
93 .for_each(|(row_values, &w)| scale_dense_row_values(row_values, w, mode));
94 return;
95 }
96
97 ndarray::Zip::from(out.rows_mut())
98 .and(scale.view())
99 .for_each(|mut row, &w| {
100 if let Some(row_values) = row.as_slice_mut() {
101 scale_dense_row_values(row_values, w, mode);
102 } else {
103 match mode {
104 DenseRowScaleMode::Direct => row *= w,
105 DenseRowScaleMode::InversePositiveOrZero => {
106 if w > 0.0 {
107 row *= w.recip();
108 } else {
109 row.fill(0.0);
110 }
111 }
112 }
113 }
114 });
115}
116
117#[inline]
118pub(crate) fn scale_dense_row_values(row_values: &mut [f64], scale: f64, mode: DenseRowScaleMode) {
119 match mode {
120 DenseRowScaleMode::Direct => {
121 for value in row_values {
122 *value *= scale;
123 }
124 }
125 DenseRowScaleMode::InversePositiveOrZero => {
126 if scale > 0.0 {
127 let inv = scale.recip();
128 for value in row_values {
129 *value *= inv;
130 }
131 } else {
132 for value in row_values {
133 *value = 0.0;
134 }
135 }
136 }
137 }
138}
139
140pub(crate) fn accumulate_weighted_cross_rows(
141 out: &mut Array2<f64>,
142 left: &Array2<f64>,
143 right: &Array2<f64>,
144 weights: &Array1<f64>,
145 row_start: usize,
146 row_end: usize,
147) {
148 let p = left.ncols();
149 let q = right.ncols();
150 for i in row_start..row_end {
151 let wi = weights[i];
152 if wi == 0.0 {
153 continue;
154 }
155 for a in 0..p {
156 let scaled = wi * left[[i, a]];
157 if scaled == 0.0 {
158 continue;
159 }
160 for b in 0..q {
161 out[[a, b]] += scaled * right[[i, b]];
162 }
163 }
164 }
165}
166
167pub(crate) fn accumulate_xt_diag_x_upper_rows(
168 out: &mut Array2<f64>,
169 x: &Array2<f64>,
170 diag: &Array1<f64>,
171 row_start: usize,
172 row_end: usize,
173) {
174 let p = x.ncols();
175 for i in row_start..row_end {
176 let wi = diag[i];
177 if wi == 0.0 {
178 continue;
179 }
180 for a in 0..p {
181 let scaled = wi * x[[i, a]];
182 if scaled == 0.0 {
183 continue;
184 }
185 for b in a..p {
186 out[[a, b]] += scaled * x[[i, b]];
187 }
188 }
189 }
190}
191
192pub(crate) fn weighted_cross_dense(
197 left: &Array2<f64>,
198 right: &Array2<f64>,
199 weights: &Array1<f64>,
200) -> Array2<f64> {
201 assert_eq!(left.nrows(), right.nrows());
202 assert_eq!(left.nrows(), weights.len());
203 let n = weights.len();
204 let p = left.ncols();
205 let q = right.ncols();
206 if n == 0 || p == 0 || q == 0 {
207 return Array2::<f64>::zeros((p, q));
208 }
209
210 let work = n.saturating_mul(p).saturating_mul(q);
211 if rayon::current_num_threads() <= 1 || work < DENSE_WEIGHTED_PRODUCT_PAR_FLOPS {
212 return fast_xt_diag_y(left, weights, right);
213 }
214
215 gam_linalg::pairwise_reduce::par_deterministic_block_fold(
221 n,
222 |range: core::ops::Range<usize>| {
223 let mut local = Array2::<f64>::zeros((p, q));
224 accumulate_weighted_cross_rows(
225 &mut local,
226 left,
227 right,
228 weights,
229 range.start,
230 range.end,
231 );
232 local
233 },
234 |mut a, b| {
235 a += &b;
236 a
237 },
238 )
239 .unwrap_or_else(|| Array2::<f64>::zeros((p, q)))
240}
241
242pub(crate) fn xt_diag_x_dense_into(
247 x: &Array2<f64>,
248 diag: &Array1<f64>,
249 weighted: &mut Array2<f64>,
250) -> Array2<f64> {
251 let (n, p) = x.dim();
252 assert_eq!(diag.len(), n, "diag length must match row count");
253 if n == 0 || p == 0 {
254 return Array2::<f64>::zeros((p, p));
255 }
256
257 let work = n.saturating_mul(p).saturating_mul(p);
258 if rayon::current_num_threads() <= 1 || work < DENSE_WEIGHTED_PRODUCT_PAR_FLOPS {
259 row_scale_dense_into(x, diag, weighted);
260 return gam_linalg::faer_ndarray::fast_atb(x, weighted);
261 }
262
263 let mut out = gam_linalg::pairwise_reduce::par_deterministic_block_fold(
267 n,
268 |range: core::ops::Range<usize>| {
269 let mut local = Array2::<f64>::zeros((p, p));
270 accumulate_xt_diag_x_upper_rows(&mut local, x, diag, range.start, range.end);
271 local
272 },
273 |mut a, b| {
274 a += &b;
275 a
276 },
277 )
278 .unwrap_or_else(|| Array2::<f64>::zeros((p, p)));
279 for a in 0..p {
280 for b in 0..a {
281 out[[a, b]] = out[[b, a]];
282 }
283 }
284 out
285}
286
287pub struct InnerAssembly<'dp> {
297 pub log_likelihood: f64,
299 pub penalty_quadratic: f64,
300 pub beta: Array1<f64>,
301 pub n_observations: usize,
302 pub hessian_op: std::sync::Arc<dyn HessianFactorization>,
303 pub penalty_coords: Vec<PenaltyCoordinate>,
304 pub penalty_logdet: PenaltyLogdetDerivs,
305 pub dispersion: DispersionHandling,
306 pub rho_curvature_scale: f64,
307 pub rho_prior: gam_problem::RhoPrior,
308 pub hessian_logdet_correction: f64,
309 pub penalty_subspace_trace: Option<Arc<PenaltySubspaceTrace>>,
310
311 pub deriv_provider: Option<Box<dyn HessianDerivativeProvider + 'dp>>,
313 pub firth: Option<crate::estimate::reml::reml_outer_engine::ExactJeffreysTerm>,
319 pub nullspace_dim: Option<f64>,
320 pub barrier_config: Option<BarrierConfig>,
321 pub kkt_residual: Option<ProjectedKktResidual>,
322 pub active_constraints: Option<Arc<crate::model_types::ActiveLinearConstraintBlock>>,
327
328 pub ext_coords: Vec<HyperCoord>,
330 pub ext_coord_pair_fn:
331 Option<Box<dyn Fn(usize, usize) -> HyperCoordPairResult + Send + Sync>>,
332 pub rho_ext_pair_fn:
333 Option<Box<dyn Fn(usize, usize) -> HyperCoordPairResult + Send + Sync>>,
334 pub fixed_drift_deriv: Option<FixedDriftDerivFn>,
335 pub contracted_psi_second_order: Option<ContractedPsiSecondOrderFn>,
339}
340
341impl<'dp> InnerAssembly<'dp> {
342 pub fn build(self) -> InnerSolution<'dp> {
344 let mut builder = InnerSolutionBuilder::new(
345 self.log_likelihood,
346 self.penalty_quadratic,
347 self.beta,
348 self.n_observations,
349 self.hessian_op,
350 self.penalty_coords,
351 self.penalty_logdet,
352 self.dispersion,
353 );
354 builder = builder.rho_curvature_scale(self.rho_curvature_scale);
355 builder = builder.rho_prior(self.rho_prior);
356 builder = builder.hessian_logdet_correction(self.hessian_logdet_correction);
357 builder = builder.penalty_subspace_trace(self.penalty_subspace_trace);
358
359 if let Some(dp) = self.deriv_provider {
360 builder = builder.deriv_provider(dp);
361 }
362 builder = builder.firth_term(self.firth);
363 if let Some(nd) = self.nullspace_dim {
364 builder = builder.nullspace_dim_override(nd);
365 }
366 builder = builder.barrier_config(self.barrier_config);
367 builder = builder.kkt_residual(self.kkt_residual);
368 builder = builder.active_constraints(self.active_constraints);
369
370 if !self.ext_coords.is_empty() {
371 builder = builder.ext_coords(self.ext_coords);
372 }
373 if let Some(f) = self.ext_coord_pair_fn {
374 builder = builder.ext_coord_pair_fn(f);
375 }
376 if let Some(f) = self.rho_ext_pair_fn {
377 builder = builder.rho_ext_pair_fn(f);
378 }
379 if let Some(f) = self.fixed_drift_deriv {
380 builder = builder.fixed_drift_deriv(f);
381 }
382 builder = builder.contracted_psi_second_order(self.contracted_psi_second_order);
383
384 builder.build()
385 }
386
387 pub fn evaluate(
389 self,
390 rho: &[f64],
391 mode: EvalMode,
392 prior: Option<(f64, Array1<f64>, Option<Array2<f64>>)>,
393 ) -> Result<RemlLamlResult, String> {
394 let solution = self.build();
395 reml_laml_evaluate(&solution, rho, mode, prior)
396 }
397}
398
399pub fn evaluate_solution(
405 solution: &InnerSolution<'_>,
406 rho: &[f64],
407 mode: EvalMode,
408 prior: Option<(f64, Array1<f64>, Option<Array2<f64>>)>,
409) -> Result<RemlLamlResult, String> {
410 reml_laml_evaluate(solution, rho, mode, prior)
411}
412
413pub struct PenaltyBlockDesc<'a> {
419 pub matrix: &'a Array2<f64>,
420 pub range_start: usize,
421 pub range_end: usize,
422}
423
424pub fn penalty_coords_from_blocks(
429 blocks: &[PenaltyBlockDesc],
430 total_dim: usize,
431) -> Result<Vec<PenaltyCoordinate>, String> {
432 blocks
433 .iter()
434 .map(|b| {
435 let root = penalty_matrix_root(b.matrix)?;
436 Ok(PenaltyCoordinate::from_block_root(
437 root,
438 b.range_start,
439 b.range_end,
440 total_dim,
441 ))
442 })
443 .collect()
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449 use approx::assert_relative_eq;
450 use ndarray::Array2;
451
452 pub(crate) fn assert_matrix_close(
453 got: &Array2<f64>,
454 expected: &Array2<f64>,
455 epsilon: f64,
456 max_relative: f64,
457 ) {
458 assert_eq!(got.dim(), expected.dim());
459 for ((i, j), &value) in got.indexed_iter() {
460 assert_relative_eq!(
461 value,
462 expected[[i, j]],
463 epsilon = epsilon,
464 max_relative = max_relative
465 );
466 }
467 }
468
469 pub(crate) fn deterministic_matrix(n: usize, p: usize, phase: f64) -> Array2<f64> {
470 Array2::from_shape_fn((n, p), |(i, j)| {
471 let a = ((i as f64 + 1.0) * (j as f64 + 3.0) + phase).sin();
472 let b = ((i as f64 + 5.0) / (j as f64 + 2.0) + phase).cos();
473 0.25 * a + 0.75 * b
474 })
475 }
476
477 pub(crate) fn deterministic_weights(n: usize) -> Array1<f64> {
478 Array1::from_shape_fn(n, |i| {
479 if i % 17 == 0 {
480 0.0
481 } else {
482 0.2 + ((i as f64 + 1.0) * 0.013).sin().abs()
483 }
484 })
485 }
486
487 pub(crate) fn weighted_cross_reference(
488 left: &Array2<f64>,
489 right: &Array2<f64>,
490 weights: &Array1<f64>,
491 ) -> Array2<f64> {
492 let mut out = Array2::<f64>::zeros((left.ncols(), right.ncols()));
493 for i in 0..weights.len() {
494 for a in 0..left.ncols() {
495 let scaled = weights[i] * left[[i, a]];
496 for b in 0..right.ncols() {
497 out[[a, b]] += scaled * right[[i, b]];
498 }
499 }
500 }
501 out
502 }
503
504 #[test]
505 pub(crate) fn row_scale_dense_into_reuses_buffer_and_matches_reference() {
506 let x = deterministic_matrix(37, 11, 0.3);
507 let weights = deterministic_weights(x.nrows());
508 let mut out = Array2::<f64>::zeros(x.raw_dim());
509 let ptr = out.as_ptr();
510 row_scale_dense_into(&x, &weights, &mut out);
511 assert_eq!(out.as_ptr(), ptr);
512 for i in 0..x.nrows() {
513 for j in 0..x.ncols() {
514 assert_relative_eq!(out[[i, j]], x[[i, j]] * weights[i], epsilon = 0.0);
515 }
516 }
517 }
518
519 #[test]
520 pub(crate) fn weighted_cross_dense_matches_rowwise_reference_at_large_scale_block_size() {
521 let left = deterministic_matrix(2048, 96, 0.1);
522 let right = deterministic_matrix(2048, 64, 0.7);
523 let weights = deterministic_weights(left.nrows());
524 let got = weighted_cross_dense(&left, &right, &weights);
525 let expected = weighted_cross_reference(&left, &right, &weights);
526 assert_matrix_close(&got, &expected, 5e-10, 5e-12);
527 }
528
529 #[test]
530 pub(crate) fn xt_diag_x_dense_into_matches_symmetric_reference_at_large_scale_block_size() {
531 let x = deterministic_matrix(1024, 96, 1.1);
532 let weights = deterministic_weights(x.nrows());
533 let mut scratch = Array2::<f64>::zeros((0, 0));
534 let got = xt_diag_x_dense_into(&x, &weights, &mut scratch);
535 let expected = weighted_cross_reference(&x, &x, &weights);
536 assert_matrix_close(&got, &expected, 3e-10, 5e-12);
537 for i in 0..got.nrows() {
538 for j in 0..got.ncols() {
539 assert_relative_eq!(got[[i, j]], got[[j, i]], epsilon = 0.0);
540 }
541 }
542 }
543}