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