1use std::any::Any;
8use std::collections::HashMap;
9use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
10use std::sync::{Arc, Condvar, Mutex};
11
12use ndarray::{Array1, Array2, ArrayView1, ArrayView2, ArrayViewMut1, ArrayViewMut2};
13use rayon::iter::{IntoParallelIterator, ParallelIterator};
14
15#[macro_use]
16mod macros;
17
18pub mod basis_error;
19pub mod block_count_error;
20pub mod block_role;
21pub mod block_spec;
22pub mod coefficient_prior_mean;
23mod constraint_set;
24pub mod custom_family_blockwise;
25pub mod custom_family_error;
26pub mod diagnostics;
27pub mod dispersion;
28pub mod dispersion_cov;
29pub mod estimation_error;
30pub mod execution_path;
31pub mod family_options;
32pub mod finite_validation;
33pub mod fisher_rao;
34pub mod gauge;
35pub mod identifiability_audit;
36pub mod joint_penalty;
37mod linear_constraints;
38pub mod log_strength;
39pub mod monotone_root_error;
40pub mod outer_subsample;
41pub mod penalty_coordinate;
42pub mod penalty_matrix;
43mod pseudo_logdet;
44pub mod psi_design_contract;
45pub mod psi_terms;
46pub mod riemannian_retraction;
47pub mod rho_posterior;
51pub mod roundoff;
52pub mod row_measure;
53pub mod row_metric;
54pub mod schedule;
55pub mod serde_extended_real;
59pub mod serde_finite;
60pub mod laplace_sampler_contract;
64mod seeding;
65pub mod solver_contract;
66pub mod test_support;
70pub mod topology_certificates;
71pub mod types;
72
73pub use riemannian_retraction::LatentRetractionRegistry;
74pub use row_measure::RowSubsampleMask;
75
76pub use basis_error::BasisError;
77pub use block_count_error::BlockCountMismatch;
78pub use block_role::BlockRole;
79pub use block_spec::{
80 AdditiveBlockJacobian, BlockEffectiveJacobian, BlockGeometryDirectionalDerivative,
81 BlockWorkingSet, CoefficientCoordinate, FamilyChannelHessian, FamilyLinearizationState,
82 GaugeComposedJacobian, ParameterBlockSpec, ParameterBlockState, RowScaledJacobian,
83};
84pub use coefficient_prior_mean::{CoefficientPriorMean, PriorMeanError};
85pub use constraint_set::{
86 ConstraintRowId, ConstraintSet, ContractFeasibleStep, ContractFeasibleStepError,
87 KhatriRaoConeConstraints, PRIMAL_FEASIBILITY_TOL, PlacedConstraintBlock,
88 feasibility_quantities_are_finite,
89};
90pub use custom_family_blockwise::{
91 CUSTOM_FAMILY_RIDGE_FLOOR, ExactNewtonOuterCurvature, validate_blockspec_consistency,
92};
93pub use custom_family_error::{
94 CustomFamilyError, InnerConvergenceTerminalState, JointNewtonTerminalReason,
95 relative_stationarity,
96};
97pub use dispersion::{Dispersion, DispersionError};
98pub use dispersion_cov::{
99 CovarianceStandardErrorError, PhiScaledCovariance, UnscaledPrecision, se_from_covariance,
100};
101pub use estimation_error::{
102 EstimationError, FixedLambdaCheckpoint, FixedLambdaResidualKind, FixedLambdaSolverStage,
103 FixedLambdaStallReason, FixedLambdaStationarityEvidence, StationarityRung,
104 StationarityStandard,
105};
106pub use estimation_error::FitStationarityEvidence;
107pub use execution_path::ExecutionPath;
108pub use family_options::{ExactNewtonOuterObjective, ExactOuterDerivativeOrder};
109pub use finite_validation::{
110 bail_if_cached_beta_non_finite, ensure_finite_scalar, ensure_finite_scalar_estimation,
111 validate_all_finite, validate_all_finite_estimation, validate_all_finite_trial_point,
112};
113pub use serde_finite::{NonFiniteFloat, ensure_serialized_floats_are_finite};
114pub use fisher_rao::{
115 FisherRaoDefiniteness, normalize_fisher_rao_blocks, normalize_fisher_rao_blocks_pd,
116};
117pub use roundoff::{roundoff_growth_factor, weighted_residual_is_at_roundoff_floor};
118use gam_linalg::dense;
119pub use gam_linalg::faer_ndarray::{in_nested_parallel_region, with_nested_parallel};
120pub use gauge::Gauge;
121pub use identifiability_audit::{
122 AliasedPair, BlockIdentity, DroppedColumn, IdentifiabilityAudit, JointRankCertificate,
123 MapUniquenessError,
124};
125pub use joint_penalty::{JointPenaltyBundle, JointPenaltyError, JointPenaltySpec};
126pub use linear_constraints::LinearInequalityConstraints;
127pub use log_strength::{
128 IndexedLogStrengthDomainError, LOG_STRENGTH_MAX, LOG_STRENGTH_MIN, LogStrengthDomainError,
129 PhysicalStrengthDomainError, checked_exp_log_strength, checked_exp_log_strengths,
130 checked_log_strength, validate_log_strength, validate_log_strengths,
131};
132pub use monotone_root_error::MonotoneRootError;
133pub use penalty_coordinate::{PenaltyCoordinate, project_block_root_out_of_null_directions};
134pub use penalty_matrix::PenaltyMatrix;
135pub use pseudo_logdet::PseudoLogdetMode;
136pub use psi_design_contract::{
137 CustomFamilyBlockPsiDerivative, CustomFamilyHyperAxis, CustomFamilyHyperLayout,
138 CustomFamilyPsiDerivativeOperator, JointHessianSourcePreference,
139 MaterializablePsiDerivativeOperator, MaterializationIntent, SharedCustomFamilyHyperLayout,
140};
141pub use psi_terms::{
142 ExactNewtonJointPsiSecondOrderContracted, ExactNewtonJointPsiSecondOrderTerms,
143 ExactNewtonJointPsiTerms, ExactNewtonJointPsiWorkspace,
144};
145pub use row_metric::{
146 FisherFactorKind, MetricProvenance, RowMetric, WeightField, pack_probe_factors,
147};
148pub use schedule::{GumbelTemperatureSchedule, ScheduleKind};
149pub use seeding::{OrderedRhoBounds, SeedConfig, SeedRiskProfile};
150pub use solver_contract::{
151 DeclaredHessianForm, Derivative, EfsEval, FixedPointCertificateEval,
152 FixedPointCoordinateCertificate, HessianMaterialization, HessianOperator, HessianValue,
153 ObjectiveEvalError, OuterEval, OuterStrategyError,
154};
155pub use types::*;
156
157#[cold]
158fn reml_contract_panic(message: impl Into<String>) -> ! {
159 std::panic::panic_any(message.into())
160}
161
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164pub enum EvalMode {
165 ValueOnly,
167 ValueAndGradient,
169 ValueGradientHessian,
171}
172
173struct NonDowncastableHyperOperator;
176
177static NON_DOWNCASTABLE_HYPER_OPERATOR: NonDowncastableHyperOperator = NonDowncastableHyperOperator;
178
179pub trait HyperOperator: Send + Sync {
180 fn dim(&self) -> usize;
183
184 fn mul_vec(&self, v: &Array1<f64>) -> Array1<f64>;
186
187 fn as_any(&self) -> &(dyn Any + 'static) {
191 &NON_DOWNCASTABLE_HYPER_OPERATOR
192 }
193
194 fn mul_vec_view(&self, v: ArrayView1<'_, f64>) -> Array1<f64> {
196 self.mul_vec(&v.to_owned())
197 }
198
199 fn mul_vec_into(&self, v: ArrayView1<'_, f64>, mut out: ArrayViewMut1<'_, f64>) {
201 out.assign(&self.mul_vec_view(v));
202 }
203
204 fn mul_mat(&self, factor: &Array2<f64>) -> Array2<f64> {
207 let p = factor.nrows();
208 let k = factor.ncols();
209 let mut out = Array2::<f64>::zeros((p, k));
210 if rayon::current_thread_index().is_some() {
211 for col in 0..k {
212 let bv = out.column_mut(col);
213 self.mul_vec_into(factor.column(col), bv);
214 }
215 return out;
216 }
217 let cols: Vec<Array1<f64>> = (0..k)
218 .into_par_iter()
219 .map(|col| {
220 let mut bv = Array1::<f64>::zeros(p);
221 self.mul_vec_into(factor.column(col), bv.view_mut());
222 bv
223 })
224 .collect();
225 for (col, bv) in cols.into_iter().enumerate() {
226 out.column_mut(col).assign(&bv);
227 }
228 out
229 }
230
231 fn trace_projected_factor(&self, factor: &Array2<f64>) -> f64 {
233 let op_factor = self.mul_mat(factor);
234 factor
235 .iter()
236 .zip(op_factor.iter())
237 .map(|(&f, &bf)| f * bf)
238 .sum()
239 }
240
241 fn projection_design_id(&self) -> Option<usize> {
250 None
251 }
252
253 fn trace_projected_factor_cached(
254 &self,
255 factor: &Array2<f64>,
256 factor_cache: &ProjectedFactorCache,
257 ) -> f64 {
258 assert!(std::mem::size_of_val(factor_cache) > 0);
262 match self.projection_design_id() {
263 Some(design_id) => {
264 let key = ProjectedFactorKey::from_factor_view(design_id, factor.view());
265 let projected = factor_cache.get_or_insert_with(key, || self.mul_mat(factor));
266 factor
267 .iter()
268 .zip(projected.iter())
269 .map(|(&f, &bf)| f * bf)
270 .sum()
271 }
272 None => self.trace_projected_factor(factor),
273 }
274 }
275
276 fn projected_matrix(&self, factor: &Array2<f64>) -> Array2<f64> {
278 let op_factor = self.mul_mat(factor);
279 gam_linalg::faer_ndarray::fast_atb(factor, &op_factor)
280 }
281
282 fn projected_matrix_cached(
285 &self,
286 factor: &Array2<f64>,
287 factor_cache: &ProjectedFactorCache,
288 ) -> Array2<f64> {
289 assert!(std::mem::size_of_val(factor_cache) > 0);
290 match self.projection_design_id() {
291 Some(design_id) => {
292 let key = ProjectedFactorKey::from_factor_view(design_id, factor.view());
293 let projected = factor_cache.get_or_insert_with(key, || self.mul_mat(factor));
294 gam_linalg::faer_ndarray::fast_atb(factor, projected.as_ref())
295 }
296 None => self.projected_matrix(factor),
297 }
298 }
299
300 fn mul_basis_columns_into(&self, start: usize, mut out: ArrayViewMut2<'_, f64>) {
302 let cols = out.ncols();
303 let dim = out.nrows();
304 assert!(start + cols <= dim);
305 let mut basis = Array1::<f64>::zeros(dim);
306 for local_col in 0..cols {
307 let global_col = start + local_col;
308 basis[global_col] = 1.0;
309 self.mul_vec_into(basis.view(), out.column_mut(local_col));
310 basis[global_col] = 0.0;
311 }
312 }
313
314 fn scaled_add_mul_vec(
316 &self,
317 v: ArrayView1<'_, f64>,
318 scale: f64,
319 mut out: ArrayViewMut1<'_, f64>,
320 ) {
321 if scale == 0.0 {
322 return;
323 }
324 let mut work = Array1::<f64>::zeros(out.len());
325 self.mul_vec_into(v, work.view_mut());
326 out.scaled_add(scale, &work);
327 }
328
329 fn bilinear(&self, v: &Array1<f64>, u: &Array1<f64>) -> f64 {
331 let mut bv = Array1::<f64>::zeros(v.len());
332 self.mul_vec_into(v.view(), bv.view_mut());
333 u.dot(&bv)
334 }
335
336 fn bilinear_view(&self, v: ArrayView1<'_, f64>, u: ArrayView1<'_, f64>) -> f64 {
338 let mut bv = Array1::<f64>::zeros(v.len());
339 self.mul_vec_into(v, bv.view_mut());
340 u.dot(&bv)
341 }
342
343 fn has_fast_bilinear_view(&self) -> bool {
345 false
346 }
347
348 fn to_dense(&self) -> Array2<f64> {
350 let p = self.dim();
351 let mut out = Array2::<f64>::zeros((p, p));
352 let mut basis = Array1::<f64>::zeros(p);
353 for j in 0..p {
354 basis[j] = 1.0;
355 self.mul_vec_into(basis.view(), out.column_mut(j));
356 basis[j] = 0.0;
357 }
358 out
359 }
360
361 fn is_implicit(&self) -> bool;
363
364 fn block_local_data(&self) -> Option<(&Array2<f64>, usize, usize)> {
366 None
367 }
368}
369
370#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
371pub struct ProjectedFactorKey {
372 pub(crate) design_id: usize,
373 pub(crate) factor_ptr: usize,
374 pub(crate) rows: usize,
375 pub(crate) cols: usize,
376 pub(crate) row_stride: isize,
377 pub(crate) col_stride: isize,
378 pub(crate) value_hash: u64,
379 pub(crate) value_hash2: u64,
380}
381
382impl ProjectedFactorKey {
383 pub fn from_factor_view(design_id: usize, factor: ArrayView2<'_, f64>) -> Self {
384 let strides = factor.strides();
385 let (value_hash, value_hash2) = projected_factor_value_fingerprint(factor);
386 Self {
387 design_id,
388 factor_ptr: factor.as_ptr() as usize,
389 rows: factor.nrows(),
390 cols: factor.ncols(),
391 row_stride: strides[0],
392 col_stride: strides[1],
393 value_hash,
394 value_hash2,
395 }
396 }
397
398 pub fn synthetic(seed: u64) -> Self {
403 Self {
404 design_id: 1,
405 factor_ptr: seed as usize,
406 rows: 1,
407 cols: 1,
408 row_stride: 1,
409 col_stride: 1,
410 value_hash: seed,
411 value_hash2: seed.wrapping_mul(31),
412 }
413 }
414}
415
416pub(crate) fn projected_factor_value_fingerprint(factor: ArrayView2<'_, f64>) -> (u64, u64) {
417 let mut h1 = 0xcbf2_9ce4_8422_2325_u64;
418 let mut h2 = 0x9e37_79b1_85eb_ca87_u64;
419 for (idx, value) in factor.iter().enumerate() {
420 let bits = value.to_bits();
421 let mixed = bits.wrapping_add((idx as u64).wrapping_mul(0x517c_c1b7_2722_0a95));
422 h1 ^= mixed;
423 h1 = h1.wrapping_mul(0x0000_0100_0000_01b3);
424 h2 ^= bits.rotate_left((idx & 63) as u32);
425 h2 = h2.wrapping_mul(0x94d0_49bb_1331_11eb).rotate_left(27);
426 }
427 (h1, h2)
428}
429
430pub struct ProjectedFactorCache {
432 pub(crate) inner: Mutex<ProjectedFactorCacheInner>,
433}
434
435pub(crate) struct ProjectedFactorCacheInner {
436 pub(crate) entries: HashMap<ProjectedFactorKey, ProjectedFactorEntry>,
437 pub(crate) in_progress: HashMap<ProjectedFactorKey, Arc<ProjectedFactorInProgress>>,
438 pub(crate) next_seq: u64,
439 pub(crate) total_bytes: usize,
440 pub(crate) budget_bytes: usize,
441}
442
443pub(crate) struct ProjectedFactorInProgress {
444 pub(crate) state: Mutex<Option<ProjectedFactorInProgressState>>,
445 pub(crate) ready: Condvar,
446 pub(crate) waiter_count: std::sync::atomic::AtomicUsize,
447 pub(crate) subscriber_arrived: (Mutex<()>, Condvar),
448}
449
450pub(crate) enum ProjectedFactorInProgressState {
451 Ready(Arc<Array2<f64>>),
452 Failed,
453}
454
455pub(crate) struct ProjectedFactorEntry {
456 pub(crate) value: Arc<Array2<f64>>,
457 pub(crate) bytes: usize,
458 pub(crate) last_used: u64,
459}
460
461impl Default for ProjectedFactorCache {
462 fn default() -> Self {
463 Self::with_budget(Self::DEFAULT_BUDGET_BYTES)
464 }
465}
466
467impl ProjectedFactorCache {
468 pub const DEFAULT_BUDGET_BYTES: usize = 2 * 1024 * 1024 * 1024;
469
470 pub fn with_budget(budget_bytes: usize) -> Self {
471 Self {
472 inner: Mutex::new(ProjectedFactorCacheInner {
473 entries: HashMap::new(),
474 in_progress: HashMap::new(),
475 next_seq: 0,
476 total_bytes: 0,
477 budget_bytes,
478 }),
479 }
480 }
481
482 pub fn get_or_insert_with(
483 &self,
484 key: ProjectedFactorKey,
485 compute: impl FnOnce() -> Array2<f64>,
486 ) -> Arc<Array2<f64>> {
487 enum CacheLookup {
488 Hit(Arc<Array2<f64>>),
489 Wait(Arc<ProjectedFactorInProgress>),
490 Compute(Arc<ProjectedFactorInProgress>),
491 }
492
493 let lookup = {
494 let mut inner = self
495 .inner
496 .lock()
497 .expect("projected factor cache lock poisoned");
498 inner.next_seq += 1;
499 let now = inner.next_seq;
500 if let Some(entry) = inner.entries.get_mut(&key) {
501 entry.last_used = now;
502 CacheLookup::Hit(entry.value.clone())
503 } else if let Some(waiter) = inner.in_progress.get(&key) {
504 CacheLookup::Wait(waiter.clone())
505 } else {
506 let marker = Arc::new(ProjectedFactorInProgress {
507 state: Mutex::new(None),
508 ready: Condvar::new(),
509 waiter_count: std::sync::atomic::AtomicUsize::new(0),
510 subscriber_arrived: (Mutex::new(()), Condvar::new()),
511 });
512 inner.in_progress.insert(key, marker.clone());
513 CacheLookup::Compute(marker)
514 }
515 };
516
517 match lookup {
518 CacheLookup::Hit(value) => value,
519 CacheLookup::Wait(marker) => {
520 marker
521 .waiter_count
522 .fetch_add(1, std::sync::atomic::Ordering::AcqRel);
523 let (lock, cv) = &marker.subscriber_arrived;
524 drop(
525 lock.lock()
526 .expect("subscriber-arrived notification lock poisoned"),
527 );
528 cv.notify_all();
529 let mut guard = marker
530 .state
531 .lock()
532 .expect("projected factor in-progress lock poisoned");
533 let result = loop {
534 match guard.as_ref() {
535 Some(ProjectedFactorInProgressState::Ready(value)) => {
536 break value.clone();
537 }
538 Some(ProjectedFactorInProgressState::Failed) => {
539 marker
540 .waiter_count
541 .fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
542 reml_contract_panic("projected factor cache producer panicked")
543 }
544 None => {
545 guard = marker
546 .ready
547 .wait(guard)
548 .expect("projected factor in-progress wait poisoned");
549 }
550 }
551 };
552 marker
553 .waiter_count
554 .fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
555 result
556 }
557 CacheLookup::Compute(marker) => {
558 let computed = match catch_unwind(AssertUnwindSafe(|| Arc::new(compute()))) {
559 Ok(value) => value,
560 Err(payload) => {
561 let mut inner = self
562 .inner
563 .lock()
564 .expect("projected factor cache lock poisoned");
565 inner.in_progress.remove(&key);
566 drop(inner);
567
568 let mut guard = marker
569 .state
570 .lock()
571 .expect("projected factor in-progress lock poisoned");
572 *guard = Some(ProjectedFactorInProgressState::Failed);
573 marker.ready.notify_all();
574 resume_unwind(payload);
575 }
576 };
577 let bytes = computed.len().saturating_mul(std::mem::size_of::<f64>());
578 let mut inner = self
579 .inner
580 .lock()
581 .expect("projected factor cache lock poisoned");
582 inner.next_seq += 1;
583 let now = inner.next_seq;
584
585 if inner.budget_bytes > 0 && bytes <= inner.budget_bytes {
586 while inner.total_bytes.saturating_add(bytes) > inner.budget_bytes
587 && !inner.entries.is_empty()
588 {
589 let Some(oldest_key) = inner
590 .entries
591 .iter()
592 .min_by_key(|(_, e)| e.last_used)
593 .map(|(k, _)| *k)
594 else {
595 break;
596 };
597 if let Some(removed) = inner.entries.remove(&oldest_key) {
598 inner.total_bytes = inner.total_bytes.saturating_sub(removed.bytes);
599 }
600 }
601 }
602
603 let value = if let Some(entry) = inner.entries.get_mut(&key) {
604 entry.last_used = now;
605 entry.value.clone()
606 } else {
607 inner.entries.insert(
608 key,
609 ProjectedFactorEntry {
610 value: computed.clone(),
611 bytes,
612 last_used: now,
613 },
614 );
615 inner.total_bytes = inner.total_bytes.saturating_add(bytes);
616 computed
617 };
618 inner.in_progress.remove(&key);
619 drop(inner);
620
621 let mut guard = marker
622 .state
623 .lock()
624 .expect("projected factor in-progress lock poisoned");
625 *guard = Some(ProjectedFactorInProgressState::Ready(value.clone()));
626 marker.ready.notify_all();
627 value
628 }
629 }
630 }
631
632 pub fn len(&self) -> usize {
633 self.inner
634 .lock()
635 .map(|inner| inner.entries.len())
636 .unwrap_or(0)
637 }
638
639 pub fn total_bytes(&self) -> usize {
640 self.inner
641 .lock()
642 .map(|inner| inner.total_bytes)
643 .unwrap_or(0)
644 }
645
646 pub fn is_empty(&self) -> bool {
647 self.len() == 0
648 }
649
650 pub fn wait_for_subscriber(
661 &self,
662 key: ProjectedFactorKey,
663 timeout: std::time::Duration,
664 ) -> bool {
665 let marker = {
666 let inner = self
667 .inner
668 .lock()
669 .expect("projected factor cache lock poisoned");
670 let Some(m) = inner.in_progress.get(&key) else {
671 return false;
672 };
673 Arc::clone(m)
674 };
675 if marker
676 .waiter_count
677 .load(std::sync::atomic::Ordering::Acquire)
678 > 0
679 {
680 return true;
681 }
682 let (lock, cv) = &marker.subscriber_arrived;
683 let mut guard = lock
684 .lock()
685 .expect("subscriber-arrived notification lock poisoned");
686 let deadline = std::time::Instant::now() + timeout;
687 loop {
688 if marker
689 .waiter_count
690 .load(std::sync::atomic::Ordering::Acquire)
691 > 0
692 {
693 return true;
694 }
695 let now = std::time::Instant::now();
696 if now >= deadline {
697 return false;
698 }
699 let (next_guard, result) = cv
700 .wait_timeout(guard, deadline - now)
701 .expect("subscriber-arrived wait poisoned");
702 guard = next_guard;
703 if result.timed_out()
704 && marker
705 .waiter_count
706 .load(std::sync::atomic::Ordering::Acquire)
707 == 0
708 {
709 return false;
710 }
711 }
712 }
713}
714
715#[derive(Clone)]
716pub struct DenseMatrixHyperOperator {
717 pub matrix: Array2<f64>,
718}
719
720impl HyperOperator for DenseMatrixHyperOperator {
721 fn dim(&self) -> usize {
722 self.matrix.nrows()
723 }
724
725 fn mul_vec(&self, v: &Array1<f64>) -> Array1<f64> {
726 self.matrix.dot(v)
727 }
728
729 fn as_any(&self) -> &(dyn Any + 'static) {
730 self
731 }
732
733 fn mul_vec_view(&self, v: ArrayView1<'_, f64>) -> Array1<f64> {
734 self.matrix.dot(&v)
735 }
736
737 fn mul_vec_into(&self, v: ArrayView1<'_, f64>, mut out: ArrayViewMut1<'_, f64>) {
738 assert_eq!(self.matrix.ncols(), v.len());
739 assert_eq!(self.matrix.nrows(), out.len());
740 for (row, out_value) in self.matrix.rows().into_iter().zip(out.iter_mut()) {
741 *out_value = row.dot(&v);
742 }
743 }
744
745 fn mul_basis_columns_into(&self, start: usize, mut out: ArrayViewMut2<'_, f64>) {
746 let end = start + out.ncols();
747 assert!(end <= self.matrix.ncols());
748 out.assign(&self.matrix.slice(ndarray::s![.., start..end]));
749 }
750
751 fn scaled_add_mul_vec(
752 &self,
753 v: ArrayView1<'_, f64>,
754 scale: f64,
755 mut out: ArrayViewMut1<'_, f64>,
756 ) {
757 assert_eq!(self.matrix.ncols(), v.len());
758 assert_eq!(self.matrix.nrows(), out.len());
759 if scale == 0.0 {
760 return;
761 }
762 for (row, out_value) in self.matrix.rows().into_iter().zip(out.iter_mut()) {
763 *out_value += scale * row.dot(&v);
764 }
765 }
766
767 fn bilinear(&self, v: &Array1<f64>, u: &Array1<f64>) -> f64 {
768 dense::bilinear(&self.matrix, v.view(), u.view())
769 }
770
771 fn bilinear_view(&self, v: ArrayView1<'_, f64>, u: ArrayView1<'_, f64>) -> f64 {
772 dense::bilinear(&self.matrix, v, u)
773 }
774
775 fn to_dense(&self) -> Array2<f64> {
776 self.matrix.clone()
777 }
778
779 fn is_implicit(&self) -> bool {
780 false
781 }
782}
783
784#[derive(Clone)]
785pub struct BlockLocalDrift {
786 pub local: Array2<f64>,
787 pub start: usize,
788 pub end: usize,
789 pub total_dim: usize,
790}
791
792impl HyperOperator for BlockLocalDrift {
793 fn dim(&self) -> usize {
794 self.total_dim
795 }
796
797 fn mul_vec(&self, v: &Array1<f64>) -> Array1<f64> {
798 assert_eq!(v.len(), self.total_dim);
799 let mut out = Array1::zeros(self.total_dim);
800 self.mul_vec_into(v.view(), out.view_mut());
801 out
802 }
803
804 fn as_any(&self) -> &(dyn Any + 'static) {
805 self
806 }
807
808 fn mul_vec_into(&self, v: ArrayView1<'_, f64>, mut out: ArrayViewMut1<'_, f64>) {
809 assert_eq!(v.len(), self.total_dim);
810 assert_eq!(out.len(), self.total_dim);
811 out.fill(0.0);
812 let v_block = v.slice(ndarray::s![self.start..self.end]);
813 let mut out_block = out.slice_mut(ndarray::s![self.start..self.end]);
814 dense::matvec_into(&self.local, v_block, out_block.view_mut());
815 }
816
817 fn scaled_add_mul_vec(
818 &self,
819 v: ArrayView1<'_, f64>,
820 scale: f64,
821 mut out: ArrayViewMut1<'_, f64>,
822 ) {
823 assert_eq!(v.len(), self.total_dim);
824 assert_eq!(out.len(), self.total_dim);
825 if scale == 0.0 {
826 return;
827 }
828 let v_block = v.slice(ndarray::s![self.start..self.end]);
829 let out_block = out.slice_mut(ndarray::s![self.start..self.end]);
830 dense::matvec_scaled_add_into(&self.local, v_block, scale, out_block);
831 }
832
833 fn bilinear(&self, v: &Array1<f64>, u: &Array1<f64>) -> f64 {
834 self.bilinear_view(v.view(), u.view())
835 }
836
837 fn bilinear_view(&self, v: ArrayView1<'_, f64>, u: ArrayView1<'_, f64>) -> f64 {
838 assert_eq!(v.len(), self.total_dim);
839 assert_eq!(u.len(), self.total_dim);
840 let v_block = v.slice(ndarray::s![self.start..self.end]);
841 let u_block = u.slice(ndarray::s![self.start..self.end]);
842 dense::bilinear(&self.local, v_block, u_block)
843 }
844
845 fn to_dense(&self) -> Array2<f64> {
846 let p = self.total_dim;
847 let mut out = Array2::zeros((p, p));
848 out.slice_mut(ndarray::s![self.start..self.end, self.start..self.end])
849 .assign(&self.local);
850 out
851 }
852
853 fn is_implicit(&self) -> bool {
854 false
855 }
856
857 fn block_local_data(&self) -> Option<(&Array2<f64>, usize, usize)> {
858 Some((&self.local, self.start, self.end))
859 }
860}
861
862#[derive(Clone)]
863pub struct HyperCoordDrift {
864 pub dense: Option<Array2<f64>>,
865 pub block_local: Option<BlockLocalDrift>,
866 pub operator: Option<Arc<dyn HyperOperator>>,
867}
868
869impl HyperCoordDrift {
870 pub fn none() -> Self {
871 Self {
872 dense: None,
873 block_local: None,
874 operator: None,
875 }
876 }
877
878 pub fn from_dense(dense: Array2<f64>) -> Self {
879 Self {
880 dense: Some(dense),
881 block_local: None,
882 operator: None,
883 }
884 }
885
886 pub fn from_operator(operator: Arc<dyn HyperOperator>) -> Self {
887 Self {
888 dense: None,
889 block_local: None,
890 operator: Some(operator),
891 }
892 }
893
894 pub fn from_parts(
895 dense: Option<Array2<f64>>,
896 operator: Option<Arc<dyn HyperOperator>>,
897 ) -> Self {
898 let dense = dense.filter(|mat| !(operator.is_some() && mat.is_empty()));
899 Self {
900 dense,
901 block_local: None,
902 operator,
903 }
904 }
905
906 pub fn from_block_local_and_operator(
907 local: Array2<f64>,
908 start: usize,
909 end: usize,
910 total_dim: usize,
911 operator: Option<Arc<dyn HyperOperator>>,
912 ) -> Self {
913 Self {
914 dense: None,
915 block_local: Some(BlockLocalDrift {
916 local,
917 start,
918 end,
919 total_dim,
920 }),
921 operator,
922 }
923 }
924
925 pub fn has_operator(&self) -> bool {
926 self.operator.is_some()
927 }
928
929 pub fn uses_operator_fast_path(&self) -> bool {
930 self.operator.is_some() || self.block_local.is_some()
931 }
932
933 pub fn operator_ref(&self) -> Option<&dyn HyperOperator> {
934 self.operator.as_ref().map(Arc::as_ref)
935 }
936
937 pub fn materialize(&self) -> Array2<f64> {
938 let p = self.infer_dim();
939 if p == 0 {
940 return Array2::zeros((0, 0));
941 }
942 let mut out = self.dense.clone().unwrap_or_else(|| Array2::zeros((p, p)));
943 if let Some(bl) = &self.block_local {
944 out.slice_mut(ndarray::s![bl.start..bl.end, bl.start..bl.end])
945 .scaled_add(1.0, &bl.local);
946 }
947 if let Some(op) = &self.operator {
948 out += &op.to_dense();
949 }
950 out
951 }
952
953 pub fn apply(&self, v: &Array1<f64>) -> Array1<f64> {
954 let mut out = Array1::zeros(v.len());
955 self.scaled_add_apply(v.view(), 1.0, &mut out);
956 out
957 }
958
959 pub fn scaled_add_apply(&self, v: ArrayView1<'_, f64>, scale: f64, out: &mut Array1<f64>) {
960 assert_eq!(v.len(), out.len());
961 if scale == 0.0 {
962 return;
963 }
964 if let Some(dense) = &self.dense {
965 dense::matvec_scaled_add_into(dense, v, scale, out.view_mut());
966 }
967 if let Some(bl) = &self.block_local {
968 let v_block = v.slice(ndarray::s![bl.start..bl.end]);
969 let out_block = out.slice_mut(ndarray::s![bl.start..bl.end]);
970 dense::matvec_scaled_add_into(&bl.local, v_block, scale, out_block);
971 }
972 if let Some(op) = &self.operator {
973 op.scaled_add_mul_vec(v, scale, out.view_mut());
974 }
975 }
976
977 pub(crate) fn infer_dim(&self) -> usize {
978 if let Some(d) = &self.dense {
979 return d.nrows();
980 }
981 if let Some(op) = &self.operator {
982 return op.dim();
983 }
984 if let Some(bl) = &self.block_local {
985 return bl.total_dim;
986 }
987 0
988 }
989}
990
991#[derive(Clone)]
992pub struct HyperCoord {
993 pub a: f64,
994 pub g: Array1<f64>,
995 pub drift: HyperCoordDrift,
996 pub ld_s: f64,
997 pub b_depends_on_beta: bool,
998 pub is_penalty_like: bool,
999 pub firth_g: Option<Array1<f64>>,
1000 pub tk_eta_fixed: Option<Array1<f64>>,
1001 pub tk_x_fixed: Option<Array2<f64>>,
1002}
1003
1004#[derive(Clone)]
1005pub struct HyperCoordPair {
1006 pub a: f64,
1007 pub g: Array1<f64>,
1008 pub b_mat: Array2<f64>,
1009 pub b_operator: Option<Arc<dyn HyperOperator>>,
1010 pub ld_s: f64,
1011}
1012
1013pub type HyperCoordPairResult = Result<HyperCoordPair, String>;
1020
1021pub type HyperCoordPairFn = Arc<dyn Fn(usize, usize) -> HyperCoordPairResult + Send + Sync>;
1032
1033impl HyperCoordPair {
1034 pub fn zero() -> Self {
1035 Self {
1036 a: 0.0,
1037 g: Array1::zeros(0),
1038 b_mat: Array2::zeros((0, 0)),
1039 b_operator: None,
1040 ld_s: 0.0,
1041 }
1042 }
1043}
1044
1045#[derive(Clone)]
1046pub enum DriftDerivResult {
1047 Dense(Array2<f64>),
1048 Operator(Arc<dyn HyperOperator>),
1049}
1050
1051impl std::fmt::Debug for DriftDerivResult {
1052 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1053 match self {
1054 Self::Dense(matrix) => f
1055 .debug_tuple("Dense")
1056 .field(&format_args!("{}x{}", matrix.nrows(), matrix.ncols()))
1057 .finish(),
1058 Self::Operator(_) => f
1059 .debug_tuple("Operator")
1060 .field(&"<hyper-operator>")
1061 .finish(),
1062 }
1063 }
1064}
1065
1066impl DriftDerivResult {
1067 pub fn into_operator(self) -> Arc<dyn HyperOperator> {
1068 match self {
1069 Self::Dense(matrix) => Arc::new(DenseMatrixHyperOperator { matrix }),
1070 Self::Operator(operator) => operator,
1071 }
1072 }
1073
1074 pub fn apply(&self, v: &Array1<f64>) -> Array1<f64> {
1075 match self {
1076 Self::Dense(matrix) => matrix.dot(v),
1077 Self::Operator(operator) => operator.mul_vec(v),
1078 }
1079 }
1080}
1081
1082pub type FixedDriftDerivFn =
1083 Box<dyn Fn(usize, &Array1<f64>) -> Result<Option<DriftDerivResult>, String> + Send + Sync>;
1084
1085pub type SharedFixedDriftDerivFn =
1093 Arc<dyn Fn(usize, &Array1<f64>) -> Result<Option<DriftDerivResult>, String> + Send + Sync>;
1094
1095pub struct ContractedPsiSecondOrder {
1096 pub objective: Array1<f64>,
1097 pub score: Array2<f64>,
1098 pub hessian: Vec<DriftDerivResult>,
1099 pub ld_s: Array1<f64>,
1100}
1101
1102pub type ContractedPsiSecondOrderFn =
1103 Arc<dyn Fn(&[f64]) -> Result<Option<ContractedPsiSecondOrder>, String> + Send + Sync>;