1use std::collections::hash_map::DefaultHasher;
14use std::collections::{HashMap, VecDeque};
15use std::hash::{Hash, Hasher};
16use std::path::PathBuf;
17use std::sync::{Arc, Mutex, OnceLock};
18
19use ndarray::{Array1, Array2, ArrayView2};
20
21use crate::estimate::EstimationError;
22use crate::estimate::reml::DirectionalHyperParam;
23pub use gam_problem::LatentRetractionRegistry;
24use gam_runtime::warm_start::{Fingerprint, Fingerprinter};
25use gam_terms::basis::{DuchonNullspaceOrder, MaternNu, RadialScalarKind};
26use gam_terms::latent::{
27 AuxPriorFamily, AuxPriorStrength, LatentCoordValues, LatentIdMode, LatentManifold,
28};
29use gam_terms::smooth::{TermCollectionDesign, TermCollectionSpec};
30
31const DEFAULT_LATENT_CACHE_CAPACITY: usize = 4;
32const DEFAULT_PERSISTENT_LATENT_CACHE_CAPACITY: usize = 16;
33const DEFAULT_PERSISTENT_LATENT_CACHE_BYTE_BUDGET: usize = 1024 * 1024 * 1024;
34
35static PERSISTENT_LATENT_DESIGN_CACHE: OnceLock<Mutex<PersistentLatentDesignCache>> =
36 OnceLock::new();
37
38#[derive(Clone, Debug)]
40pub(crate) struct LatentFingerprint {
41 pub(crate) hash: u64,
42 pub(crate) len: usize,
43}
44
45impl LatentFingerprint {
46 pub(crate) fn from_flat(flat: &[f64]) -> Self {
47 let mut hasher = DefaultHasher::new();
48 flat.len().hash(&mut hasher);
49 for &value in flat {
50 value.to_bits().hash(&mut hasher);
51 }
52 Self {
53 hash: hasher.finish(),
54 len: flat.len(),
55 }
56 }
57}
58
59pub type CacheDigest = Fingerprint;
60
61fn cache_digest_builder(namespace: &str) -> Fingerprinter {
68 let mut out = Fingerprinter::new();
69 out.write_str(namespace);
70 out
71}
72
73#[derive(Clone)]
74pub enum LatentBasisKind {
75 Matern {
77 centers: Array2<f64>,
79 input_scale: gam_terms::IsotropicScale,
84 length_scale: gam_terms::OriginalUnits,
86 nu: MaternNu,
87 aniso_log_scales: Vec<f64>,
88 chunk_size: Option<usize>,
89 },
90 Duchon {
91 centers: Array2<f64>,
92 input_scale: gam_terms::IsotropicScale,
94 length_scale: Option<gam_terms::OriginalUnits>,
95 power: f64,
96 nullspace_order: DuchonNullspaceOrder,
97 aniso_log_scales: Vec<f64>,
98 },
99 Sphere {
100 centers: Array2<f64>,
101 penalty_order: usize,
102 chunk_size: Option<usize>,
103 },
104 PeriodicBspline {
105 domain_start: f64,
106 period: f64,
107 degree: usize,
108 num_basis: usize,
109 chunk_size: Option<usize>,
110 },
111 TensorBspline {
112 knots: Vec<Array1<f64>>,
113 degrees: Vec<usize>,
114 chunk_size: Option<usize>,
115 },
116 Pca {
117 basis_matrix: Array2<f64>,
118 centered: bool,
119 center_mean_fingerprint: Option<u64>,
120 smooth_penalty: f64,
121 pca_basis_path: Option<PathBuf>,
122 chunk_size: usize,
123 },
124}
125
126impl LatentBasisKind {
127 fn centers(&self) -> Option<&Array2<f64>> {
128 match self {
129 Self::Matern { centers, .. }
130 | Self::Duchon { centers, .. }
131 | Self::Sphere { centers, .. } => Some(centers),
132 Self::PeriodicBspline { .. } | Self::TensorBspline { .. } => None,
133 Self::Pca { .. } => None,
134 }
135 }
136
137 fn input_scale(&self) -> gam_terms::IsotropicScale {
145 match self {
146 Self::Matern { input_scale, .. } | Self::Duchon { input_scale, .. } => *input_scale,
147 Self::Sphere { .. }
148 | Self::PeriodicBspline { .. }
149 | Self::TensorBspline { .. }
150 | Self::Pca { .. } => gam_terms::IsotropicScale::ONE,
151 }
152 }
153
154 fn streams_radial_cache(&self) -> bool {
155 matches!(
156 self,
157 Self::Matern {
158 chunk_size: Some(_),
159 ..
160 } | Self::Sphere {
161 chunk_size: Some(_),
162 ..
163 }
164 )
165 }
166
167 fn cache_digest(&self) -> CacheDigest {
168 let mut hasher = cache_digest_builder("latent-basis-v1");
169 match self {
170 Self::Matern {
171 centers,
172 input_scale,
173 length_scale,
174 nu,
175 aniso_log_scales,
176 chunk_size,
177 } => {
178 hasher.write_usize(0);
179 hasher.write_usize(centers.nrows());
180 hasher.write_usize(centers.ncols());
181 hasher.write_f64(input_scale.get());
184 hasher.write_f64(length_scale.original_value());
185 hasher.write_usize(matern_nu_signature(*nu));
186 hasher.write_f64_slice(aniso_log_scales);
187 hash_optional_usize(*chunk_size, &mut hasher);
188 hasher.write_f64_array2(centers);
189 }
190 Self::Duchon {
191 centers,
192 input_scale,
193 length_scale,
194 power,
195 nullspace_order,
196 aniso_log_scales,
197 } => {
198 hasher.write_usize(1);
199 hasher.write_usize(centers.nrows());
200 hasher.write_usize(centers.ncols());
201 hasher.write_f64(input_scale.get());
202 hash_optional_f64(
203 length_scale.map(gam_terms::OriginalUnits::original_value),
204 &mut hasher,
205 );
206 hasher.write_u64(power.to_bits());
207 hash_duchon_nullspace_order(*nullspace_order, &mut hasher);
208 hasher.write_f64_slice(aniso_log_scales);
209 hasher.write_f64_array2(centers);
210 }
211 Self::Sphere {
212 centers,
213 penalty_order,
214 chunk_size,
215 } => {
216 hasher.write_usize(2);
217 hasher.write_usize(centers.nrows());
218 hasher.write_usize(centers.ncols());
219 hasher.write_usize(*penalty_order);
220 hash_optional_usize(*chunk_size, &mut hasher);
221 hasher.write_f64_array2(centers);
222 }
223 Self::PeriodicBspline {
224 domain_start,
225 period,
226 degree,
227 num_basis,
228 chunk_size,
229 } => {
230 hasher.write_usize(3);
231 hasher.write_f64(*domain_start);
232 hasher.write_f64(*period);
233 hasher.write_usize(*degree);
234 hasher.write_usize(*num_basis);
235 hash_optional_usize(*chunk_size, &mut hasher);
236 }
237 Self::TensorBspline {
238 knots,
239 degrees,
240 chunk_size,
241 } => {
242 hasher.write_usize(4);
243 hasher.write_usize(degrees.len());
244 for °ree in degrees {
245 hasher.write_usize(degree);
246 }
247 hash_optional_usize(*chunk_size, &mut hasher);
248 hasher.write_usize(knots.len());
249 for axis_knots in knots {
250 hasher.write_f64_array1(axis_knots);
251 }
252 }
253 Self::Pca {
254 basis_matrix,
255 centered,
256 center_mean_fingerprint,
257 smooth_penalty,
258 pca_basis_path,
259 chunk_size,
260 } => {
261 hasher.write_usize(5);
262 hasher.write_u8(*centered as u8);
263 if let Some(fp) = center_mean_fingerprint {
264 hasher.write_u64(*fp);
265 }
266 hasher.write_u64(smooth_penalty.to_bits());
267 if let Some(path) = pca_basis_path {
268 hasher.write_u8(1);
269 hasher.write_bytes(path.to_string_lossy().as_bytes());
270 if let Ok(meta) = std::fs::metadata(path) {
271 hasher.write_u64(meta.len());
272 if let Ok(modified) = meta.modified()
273 && let Ok(elapsed) =
274 modified.duration_since(std::time::SystemTime::UNIX_EPOCH)
275 {
276 hasher.write_u64(elapsed.as_secs());
277 hasher.write_u64(elapsed.subsec_nanos() as u64);
278 }
279 }
280 } else {
281 hasher.write_u8(0);
282 }
283 hasher.write_usize(*chunk_size);
284 hasher.write_usize(basis_matrix.nrows());
285 hasher.write_usize(basis_matrix.ncols());
286 hasher.write_f64_array2(basis_matrix);
287 }
288 }
289 hasher.finalize()
290 }
291}
292
293pub fn pca_center_mean_fingerprint(mean: &Array1<f64>) -> u64 {
294 let mut hasher = Fingerprinter::new();
295 hasher.write_usize(mean.len());
296 for &value in mean.iter() {
297 hasher.write_f64(value);
298 }
299 hasher.finish_u64()
300}
301
302fn matern_nu_signature(nu: MaternNu) -> usize {
303 match nu {
304 MaternNu::Half => 0,
305 MaternNu::ThreeHalves => 1,
306 MaternNu::FiveHalves => 2,
307 MaternNu::SevenHalves => 3,
308 MaternNu::NineHalves => 4,
309 }
310}
311
312fn hash_duchon_nullspace_order(order: DuchonNullspaceOrder, hasher: &mut Fingerprinter) {
313 match order {
314 DuchonNullspaceOrder::Zero => {
315 hasher.write_usize(0);
316 }
317 DuchonNullspaceOrder::Linear => {
318 hasher.write_usize(1);
319 }
320 DuchonNullspaceOrder::Degree(degree) => {
321 hasher.write_usize(2);
322 hasher.write_usize(degree);
323 }
324 }
325}
326
327fn hash_optional_f64(value: Option<f64>, hasher: &mut Fingerprinter) {
328 match value {
329 Some(value) => {
330 hasher.write_bool(true);
331 hasher.write_f64(value);
332 }
333 None => {
334 hasher.write_bool(false);
335 }
336 }
337}
338
339fn hash_optional_usize(value: Option<usize>, hasher: &mut Fingerprinter) {
340 match value {
341 Some(value) => {
342 hasher.write_bool(true);
343 hasher.write_usize(value);
344 }
345 None => {
346 hasher.write_bool(false);
347 }
348 }
349}
350
351fn latent_metadata_cache_digest(latent: &LatentCoordValues) -> CacheDigest {
352 let mut hasher = cache_digest_builder("latent-cache-metadata-v1");
353 hasher.write_usize(latent.n_obs());
354 hasher.write_usize(latent.latent_dim());
355 hash_latent_manifold(latent.manifold(), &mut hasher);
356 hash_latent_id_mode(latent.id_mode(), &mut hasher);
357 hasher.finalize()
358}
359
360pub fn latent_design_context_cache_digest(
361 data: ArrayView2<'_, f64>,
362 spec: &TermCollectionSpec,
363 term_index: gam_problem::SmoothTermIdx,
364 analytic_rho_count: usize,
365 feature_cols: &[usize],
366) -> Result<CacheDigest, EstimationError> {
367 let mut hasher = cache_digest_builder("latent-design-context-v1");
368 hasher.write_usize(data.nrows());
369 hasher.write_usize(data.ncols());
370 for row in 0..data.nrows() {
371 for col in 0..data.ncols() {
372 hasher.write_f64(data[[row, col]]);
373 }
374 }
375 let spec_bytes = serde_json::to_vec(spec).map_err(|err| {
376 EstimationError::InvalidInput(format!(
377 "failed to serialize latent design cache context: {err}"
378 ))
379 })?;
380 hasher.write_usize(spec_bytes.len());
381 hasher.write_bytes(&spec_bytes);
382 hasher.write_usize(term_index.get());
383 hasher.write_usize(analytic_rho_count);
384 hasher.write_usize(feature_cols.len());
385 for &col in feature_cols {
386 hasher.write_usize(col);
387 }
388 Ok(hasher.finalize())
389}
390
391fn hash_latent_id_mode(id_mode: &LatentIdMode, hasher: &mut Fingerprinter) {
392 match id_mode {
393 LatentIdMode::AuxPrior {
394 u,
395 family,
396 strength,
397 } => {
398 hasher.write_usize(0);
399 hasher.write_f64_array2(u);
400 hash_aux_prior_family(*family, hasher);
401 hash_aux_prior_strength(*strength, hasher);
402 }
403 LatentIdMode::AuxPriorDimSelection {
404 u,
405 family,
406 strength,
407 init_log_precision,
408 } => {
409 hasher.write_usize(1);
410 hasher.write_f64_array2(u);
411 hash_aux_prior_family(*family, hasher);
412 hash_aux_prior_strength(*strength, hasher);
413 hash_optional_vector(init_log_precision.as_ref(), hasher);
414 }
415 LatentIdMode::DimSelection { init_log_precision } => {
416 hasher.write_usize(2);
417 hash_optional_vector(init_log_precision.as_ref(), hasher);
418 }
419 LatentIdMode::IsometryToReference {
420 reference,
421 strength,
422 } => {
423 hasher.write_usize(5);
424 hasher.write_f64_array2(reference);
425 hash_aux_prior_strength(*strength, hasher);
426 }
427 LatentIdMode::AuxOutcome {
428 head,
429 init_log_precision,
430 } => {
431 hasher.write_usize(4);
432 hash_behavioral_head(head, hasher);
433 hash_optional_vector(init_log_precision.as_ref(), hasher);
434 }
435 LatentIdMode::None => {
436 hasher.write_usize(3);
437 }
438 }
439}
440
441fn hash_behavioral_head(
442 head: &gam_terms::decoders::behavioral_head::BehavioralHead,
443 hasher: &mut Fingerprinter,
444) {
445 use gam_terms::decoders::behavioral_head::AuxOutcomeFamily;
446 match head.family() {
447 AuxOutcomeFamily::Binomial => hasher.write_usize(0),
448 AuxOutcomeFamily::Multinomial { n_classes } => {
449 hasher.write_usize(1);
450 hasher.write_usize(n_classes);
451 }
452 }
453 hasher.write_usize(head.n_obs());
454 hasher.write_f64(head.effective_labeled_count());
455}
456
457fn hash_aux_prior_family(family: AuxPriorFamily, hasher: &mut Fingerprinter) {
458 hasher.write_usize(match family {
459 AuxPriorFamily::Ridge => 0,
460 AuxPriorFamily::Linear => 1,
461 });
462}
463
464fn hash_aux_prior_strength(strength: AuxPriorStrength, hasher: &mut Fingerprinter) {
465 match strength {
466 AuxPriorStrength::Auto => {
467 hasher.write_usize(0);
468 }
469 AuxPriorStrength::Fixed(value) => {
470 hasher.write_usize(1);
471 hasher.write_f64(value);
472 }
473 }
474}
475
476fn hash_optional_vector(vector: Option<&Array1<f64>>, hasher: &mut Fingerprinter) {
477 match vector {
478 Some(vector) => {
479 hasher.write_bool(true);
480 hasher.write_f64_array1(vector);
481 }
482 None => {
483 hasher.write_bool(false);
484 }
485 }
486}
487
488fn hash_latent_manifold(manifold: &LatentManifold, hasher: &mut Fingerprinter) {
489 match manifold {
490 LatentManifold::Euclidean => {
491 hasher.write_usize(0);
492 }
493 LatentManifold::Circle { period } => {
494 hasher.write_usize(1);
495 hasher.write_f64(*period);
496 }
497 LatentManifold::Sphere { dim } => {
498 hasher.write_usize(2);
499 hasher.write_usize(*dim);
500 }
501 LatentManifold::Interval { lo, hi } => {
502 hasher.write_usize(3);
503 hasher.write_f64(*lo);
504 hasher.write_f64(*hi);
505 }
506 LatentManifold::Product(parts) => {
507 hasher.write_usize(4);
508 hasher.write_usize(parts.len());
509 for part in parts {
510 hash_latent_manifold(part, hasher);
511 }
512 }
513 LatentManifold::ProductWithMetric { manifolds, weights } => {
514 hasher.write_usize(5);
515 hasher.write_usize(manifolds.len());
516 for part in manifolds {
517 hash_latent_manifold(part, hasher);
518 }
519 hasher.write_f64_slice(weights);
520 }
521 }
522}
523
524#[derive(Clone)]
525pub(crate) struct RadialDistanceMatrices {
526 pub(crate) squared: Array2<f64>,
527 pub(crate) distance: Array2<f64>,
528}
529
530#[derive(Clone)]
531pub(crate) struct BasisDerivativeJets {
532 pub(crate) phi: Option<Array2<f64>>,
533 pub(crate) q: Option<Array2<f64>>,
534 pub(crate) t: Option<Array2<f64>>,
535 pub(crate) phi_r: Option<Array2<f64>>,
536 pub(crate) phi_rr: Option<Array2<f64>>,
537 pub(crate) operator_resident: bool,
538}
539
540impl BasisDerivativeJets {
541 fn empty() -> Self {
542 Self {
543 phi: None,
544 q: None,
545 t: None,
546 phi_r: None,
547 phi_rr: None,
548 operator_resident: false,
549 }
550 }
551}
552
553#[derive(Clone)]
554pub struct CachedDesign {
555 pub(crate) latent_id: u64,
556 pub(crate) fingerprint: LatentFingerprint,
557 basis_digest: CacheDigest,
558 latent_metadata_digest: CacheDigest,
559 design_context_digest: CacheDigest,
560 latent_bits: Arc<[u64]>,
561 cacheable: bool,
562 pub design: TermCollectionDesign,
563 pub hyper_dirs: Vec<DirectionalHyperParam>,
564 pub(crate) radial_distances: RadialDistanceMatrices,
565 pub(crate) basis_derivative_jets: BasisDerivativeJets,
566}
567
568pub struct ComputedLatentDesign {
569 pub design: TermCollectionDesign,
570 pub hyper_dirs: Vec<DirectionalHyperParam>,
571}
572
573pub struct LatentDesignLookup<'a> {
574 pub cached: &'a CachedDesign,
575 pub entry_id: u64,
576}
577
578#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
579struct PersistentLatentDesignKey {
580 latent_id: u64,
581 flat_hash: u64,
582 basis_digest: CacheDigest,
583 latent_metadata_digest: CacheDigest,
584 design_context_digest: CacheDigest,
585}
586
587struct PersistentLatentDesignEntry {
588 fingerprint: LatentFingerprint,
589 cached: Arc<CachedDesign>,
590 bytes: usize,
591}
592
593pub(crate) struct PersistentLatentDesignCache {
594 entries: HashMap<PersistentLatentDesignKey, PersistentLatentDesignEntry>,
595 lru: VecDeque<PersistentLatentDesignKey>,
596 capacity: usize,
597 byte_budget: usize,
598 cache_bytes: usize,
599}
600
601impl Default for PersistentLatentDesignCache {
602 fn default() -> Self {
603 Self::new(DEFAULT_PERSISTENT_LATENT_CACHE_CAPACITY)
604 }
605}
606
607impl PersistentLatentDesignCache {
608 pub(crate) fn new(capacity: usize) -> Self {
609 Self {
610 entries: HashMap::new(),
611 lru: VecDeque::new(),
612 capacity: capacity.max(1),
613 byte_budget: DEFAULT_PERSISTENT_LATENT_CACHE_BYTE_BUDGET,
614 cache_bytes: 0,
615 }
616 }
617
618 pub(crate) fn lookup(
619 &mut self,
620 latent: &LatentCoordValues,
621 basis_digest: CacheDigest,
622 latent_metadata_digest: CacheDigest,
623 design_context_digest: CacheDigest,
624 fingerprint: &LatentFingerprint,
625 ) -> Result<Option<Arc<CachedDesign>>, EstimationError> {
626 let key = PersistentLatentDesignKey {
627 latent_id: latent.latent_id(),
628 flat_hash: fingerprint.hash,
629 basis_digest,
630 latent_metadata_digest,
631 design_context_digest,
632 };
633 let Some(entry) = self.entries.get(&key) else {
634 return Ok(None);
635 };
636 let cached = entry.cached.clone();
637 let entry_fingerprint = entry.fingerprint.clone();
638 self.touch(key);
639 if entry_fingerprint.len != fingerprint.len {
640 return Ok(None);
641 }
642 if entry_fingerprint.hash == fingerprint.hash
643 && cached.cacheable
644 && cached.basis_digest == basis_digest
645 && cached.latent_metadata_digest == latent_metadata_digest
646 && cached.design_context_digest == design_context_digest
647 && latent_bits_match(latent, &cached.latent_bits)
648 {
649 return Ok(Some(cached));
650 }
651 Ok(None)
652 }
653
654 pub(crate) fn insert(&mut self, cached: Arc<CachedDesign>) {
655 if !cached.cacheable {
656 return;
657 }
658 let bytes = cached.resident_byte_count();
659 if bytes > self.byte_budget {
660 return;
661 }
662 let key = PersistentLatentDesignKey {
663 latent_id: cached.latent_id,
664 flat_hash: cached.fingerprint.hash,
665 basis_digest: cached.basis_digest,
666 latent_metadata_digest: cached.latent_metadata_digest,
667 design_context_digest: cached.design_context_digest,
668 };
669 let entry = PersistentLatentDesignEntry {
670 fingerprint: cached.fingerprint.clone(),
671 cached,
672 bytes,
673 };
674 if let Some(old) = self.entries.insert(key, entry) {
675 self.cache_bytes = self.cache_bytes.saturating_sub(old.bytes);
676 }
677 self.cache_bytes = self.cache_bytes.saturating_add(bytes);
678 self.touch(key);
679 self.evict_to_limits();
680 }
681
682 fn evict_to_limits(&mut self) {
683 while self.entries.len() > self.capacity || self.cache_bytes > self.byte_budget {
684 let Some(evicted) = self.lru.pop_front() else {
685 break;
686 };
687 if let Some(entry) = self.entries.remove(&evicted) {
688 self.cache_bytes = self.cache_bytes.saturating_sub(entry.bytes);
689 }
690 }
691 }
692
693 fn touch(&mut self, key: PersistentLatentDesignKey) {
694 if let Some(index) = self.lru.iter().position(|queued| *queued == key) {
695 self.lru.remove(index);
696 }
697 self.lru.push_back(key);
698 }
699}
700
701pub struct LatentDesignCache {
702 entries: Vec<LatentDesignCacheEntry>,
703 capacity: usize,
704 clock: u64,
705 iteration: u64,
706 next_entry_id: u64,
707}
708
709struct LatentDesignCacheEntry {
710 id: u64,
711 cached: Arc<CachedDesign>,
712 last_used: u64,
713 iteration: u64,
714}
715
716impl Default for LatentDesignCache {
717 fn default() -> Self {
718 Self::new(DEFAULT_LATENT_CACHE_CAPACITY)
719 }
720}
721
722impl LatentDesignCache {
723 pub(crate) fn new(capacity: usize) -> Self {
724 Self {
725 entries: Vec::new(),
726 capacity: capacity.max(1),
727 clock: 0,
728 iteration: 0,
729 next_entry_id: 0,
730 }
731 }
732
733 pub fn invalidate(&mut self) {
734 self.entries.clear();
735 }
736
737 pub fn invalidate_all(&mut self) {
738 self.entries.clear();
739 self.clock = self.clock.wrapping_add(1);
740 self.iteration = self.iteration.wrapping_add(1);
741 }
742
743 pub fn lookup_or_compute<F>(
744 &mut self,
745 latent: Arc<LatentCoordValues>,
746 basis_kind: LatentBasisKind,
747 design_context_digest: CacheDigest,
748 compute: F,
749 ) -> Result<LatentDesignLookup<'_>, EstimationError>
750 where
751 F: FnOnce() -> Result<ComputedLatentDesign, EstimationError>,
752 {
753 self.iteration = self.iteration.wrapping_add(1);
754 self.clock = self.clock.wrapping_add(1);
755 let flat = latent.as_flat();
756 let flat_slice = flat
757 .as_slice()
758 .expect("LatentCoordValues flat storage must be contiguous");
759 let fingerprint = LatentFingerprint::from_flat(flat_slice);
760 let basis_digest = basis_kind.cache_digest();
761 let latent_metadata_digest = latent_metadata_cache_digest(&latent);
762 let cacheable = flat_slice.iter().all(|value| value.is_finite());
763 if cacheable
764 && let Some(index) = self.find_entry(
765 &latent,
766 basis_digest,
767 latent_metadata_digest,
768 design_context_digest,
769 )
770 {
771 self.entries[index].last_used = self.clock;
772 return Ok(LatentDesignLookup {
773 cached: self.entries[index].cached.as_ref(),
774 entry_id: self.entries[index].id,
775 });
776 }
777 if cacheable
778 && let Some(cached) = lookup_persistent_latent_design(
779 &latent,
780 basis_digest,
781 latent_metadata_digest,
782 design_context_digest,
783 &fingerprint,
784 )?
785 {
786 let id = self.next_entry_id;
787 self.next_entry_id = self.next_entry_id.wrapping_add(1);
788 self.insert(cached, id);
789 return self.lookup_inserted(id);
790 }
791
792 let computed = compute()?;
793 let radial_distances = if basis_kind.streams_radial_cache() {
794 RadialDistanceMatrices {
795 squared: Array2::<f64>::zeros((0, 0)),
796 distance: Array2::<f64>::zeros((0, 0)),
797 }
798 } else {
799 match basis_kind.centers() {
800 Some(centers) => {
801 build_radial_distances(&latent, centers, basis_kind.input_scale())?
802 }
803 None => RadialDistanceMatrices {
804 squared: Array2::<f64>::zeros((0, 0)),
805 distance: Array2::<f64>::zeros((0, 0)),
806 },
807 }
808 };
809 let basis_derivative_jets = build_basis_derivative_jets(&basis_kind, &radial_distances)?;
810 let id = self.next_entry_id;
811 self.next_entry_id = self.next_entry_id.wrapping_add(1);
812 let entry = Arc::new(CachedDesign {
813 latent_id: latent.latent_id(),
814 fingerprint,
815 basis_digest,
816 latent_metadata_digest,
817 design_context_digest,
818 latent_bits: latent_bits(&latent),
819 cacheable,
820 design: computed.design,
821 hyper_dirs: computed.hyper_dirs,
822 radial_distances,
823 basis_derivative_jets,
824 });
825 if cacheable {
826 insert_persistent_latent_design(Arc::clone(&entry))?;
827 }
828 self.insert(entry, id);
829 self.lookup_inserted(id)
830 }
831
832 fn find_entry(
833 &mut self,
834 latent: &LatentCoordValues,
835 basis_digest: CacheDigest,
836 latent_metadata_digest: CacheDigest,
837 design_context_digest: CacheDigest,
838 ) -> Option<usize> {
839 self.entries.iter().position(|entry| {
840 entry.cached.cacheable
841 && entry.cached.basis_digest == basis_digest
842 && entry.cached.latent_metadata_digest == latent_metadata_digest
843 && entry.cached.design_context_digest == design_context_digest
844 && entry.cached.latent_id == latent.latent_id()
845 && latent_bits_match(latent, &entry.cached.latent_bits)
846 })
847 }
848
849 fn lookup_inserted(&self, id: u64) -> Result<LatentDesignLookup<'_>, EstimationError> {
850 let Some(index) = self.entries.iter().position(|entry| entry.id == id) else {
851 return Err(EstimationError::InvalidInput(
852 "inserted latent design cache entry missing".to_string(),
853 ));
854 };
855 Ok(LatentDesignLookup {
856 cached: self.entries[index].cached.as_ref(),
857 entry_id: self.entries[index].id,
858 })
859 }
860
861 fn insert(&mut self, cached: Arc<CachedDesign>, id: u64) {
862 self.entries.push(LatentDesignCacheEntry {
863 id,
864 cached,
865 last_used: self.clock,
866 iteration: self.iteration,
867 });
868 while self.entries.len() > self.capacity {
869 if let Some(evict_index) = self
870 .entries
871 .iter()
872 .enumerate()
873 .min_by_key(|(_, entry)| (entry.last_used, entry.iteration))
874 .map(|(index, _)| index)
875 {
876 self.entries.remove(evict_index);
877 } else {
878 break;
879 }
880 }
881 }
882}
883
884impl CachedDesign {
885 fn resident_byte_count(&self) -> usize {
886 self.resident_scalar_count()
887 .saturating_mul(std::mem::size_of::<f64>())
888 .saturating_add(
889 self.hyper_dirs
890 .iter()
891 .map(DirectionalHyperParam::resident_byte_count)
892 .sum::<usize>(),
893 )
894 }
895
896 fn resident_scalar_count(&self) -> usize {
897 let mut count = self
898 .design
899 .design
900 .nrows()
901 .saturating_mul(self.design.design.ncols());
902 count = count.saturating_add(
903 self.design
904 .coefficient_lower_bounds
905 .as_ref()
906 .map_or(0, |values| values.len()),
907 );
908 count = count.saturating_add(self.radial_distances.squared.len());
909 count = count.saturating_add(self.radial_distances.distance.len());
910 count = count.saturating_add(
911 self.basis_derivative_jets
912 .phi
913 .as_ref()
914 .map_or(0, |values| values.len()),
915 );
916 count = count.saturating_add(
917 self.basis_derivative_jets
918 .q
919 .as_ref()
920 .map_or(0, |values| values.len()),
921 );
922 count = count.saturating_add(
923 self.basis_derivative_jets
924 .t
925 .as_ref()
926 .map_or(0, |values| values.len()),
927 );
928 count = count.saturating_add(
929 self.basis_derivative_jets
930 .phi_r
931 .as_ref()
932 .map_or(0, |values| values.len()),
933 );
934 count = count.saturating_add(
935 self.basis_derivative_jets
936 .phi_rr
937 .as_ref()
938 .map_or(0, |values| values.len()),
939 );
940 count.saturating_add(usize::from(self.basis_derivative_jets.operator_resident))
941 }
942}
943
944fn lookup_persistent_latent_design(
945 latent: &LatentCoordValues,
946 basis_digest: CacheDigest,
947 latent_metadata_digest: CacheDigest,
948 design_context_digest: CacheDigest,
949 fingerprint: &LatentFingerprint,
950) -> Result<Option<Arc<CachedDesign>>, EstimationError> {
951 let cache = PERSISTENT_LATENT_DESIGN_CACHE
952 .get_or_init(|| Mutex::new(PersistentLatentDesignCache::default()));
953 let mut guard = cache.lock().map_err(|_| {
954 EstimationError::InvalidInput("persistent latent design cache mutex poisoned".to_string())
955 })?;
956 guard.lookup(
957 latent,
958 basis_digest,
959 latent_metadata_digest,
960 design_context_digest,
961 fingerprint,
962 )
963}
964
965fn insert_persistent_latent_design(cached: Arc<CachedDesign>) -> Result<(), EstimationError> {
966 let cache = PERSISTENT_LATENT_DESIGN_CACHE
967 .get_or_init(|| Mutex::new(PersistentLatentDesignCache::default()));
968 let mut guard = cache.lock().map_err(|_| {
969 EstimationError::InvalidInput("persistent latent design cache mutex poisoned".to_string())
970 })?;
971 guard.insert(cached);
972 Ok(())
973}
974
975fn latent_bits(latent: &LatentCoordValues) -> Arc<[u64]> {
976 latent
977 .as_flat()
978 .iter()
979 .map(|value| value.to_bits())
980 .collect::<Vec<_>>()
981 .into()
982}
983
984fn latent_bits_match(latent: &LatentCoordValues, cached_bits: &[u64]) -> bool {
985 latent.as_flat().len() == cached_bits.len()
986 && latent
987 .as_flat()
988 .iter()
989 .zip(cached_bits.iter())
990 .all(|(value, bits)| value.to_bits() == *bits)
991}
992
993fn build_radial_distances(
994 latent: &LatentCoordValues,
995 centers: &Array2<f64>,
996 input_scale: gam_terms::IsotropicScale,
997) -> Result<RadialDistanceMatrices, EstimationError> {
998 let mut t = latent.as_matrix();
1001 input_scale.standardize(&mut t);
1002 if t.ncols() != centers.ncols() {
1003 return Err(EstimationError::InvalidInput(format!(
1004 "latent design cache center dimension mismatch: latent d={}, centers d={}",
1005 t.ncols(),
1006 centers.ncols()
1007 )));
1008 }
1009 let mut squared = Array2::<f64>::zeros((t.nrows(), centers.nrows()));
1010 let mut distance = Array2::<f64>::zeros((t.nrows(), centers.nrows()));
1011 for row in 0..t.nrows() {
1012 for center in 0..centers.nrows() {
1013 let mut r2 = 0.0_f64;
1014 for axis in 0..t.ncols() {
1015 let delta = t[[row, axis]] - centers[[center, axis]];
1016 r2 += delta * delta;
1017 }
1018 squared[[row, center]] = r2;
1019 distance[[row, center]] = r2.sqrt();
1020 }
1021 }
1022 Ok(RadialDistanceMatrices { squared, distance })
1023}
1024
1025fn build_basis_derivative_jets(
1026 basis_kind: &LatentBasisKind,
1027 distances: &RadialDistanceMatrices,
1028) -> Result<BasisDerivativeJets, EstimationError> {
1029 match basis_kind {
1030 LatentBasisKind::Matern {
1031 input_scale,
1032 length_scale,
1033 nu,
1034 chunk_size,
1035 ..
1036 } => {
1037 if chunk_size.is_some() {
1038 return Ok(BasisDerivativeJets {
1039 operator_resident: true,
1040 ..BasisDerivativeJets::empty()
1041 });
1042 }
1043 let radial = RadialScalarKind::Matern {
1046 length_scale: input_scale
1047 .to_standardized_units(*length_scale)
1048 .standardized_value(),
1049 nu: *nu,
1050 };
1051 let mut phi = Array2::<f64>::zeros(distances.distance.raw_dim());
1052 let mut q = Array2::<f64>::zeros(distances.distance.raw_dim());
1053 let mut t = Array2::<f64>::zeros(distances.distance.raw_dim());
1054 for row in 0..distances.distance.nrows() {
1055 for center in 0..distances.distance.ncols() {
1056 let (phi_value, q_value, t_value) = radial
1057 .eval_design_triplet(distances.distance[[row, center]])
1058 .map_err(EstimationError::from)?;
1059 phi[[row, center]] = phi_value;
1060 q[[row, center]] = q_value;
1061 t[[row, center]] = t_value;
1062 }
1063 }
1064 Ok(BasisDerivativeJets {
1065 phi: Some(phi),
1066 q: Some(q),
1067 t: Some(t),
1068 phi_r: None,
1069 phi_rr: None,
1070 operator_resident: false,
1071 })
1072 }
1073 LatentBasisKind::Duchon { .. } => Ok(BasisDerivativeJets {
1074 operator_resident: true,
1075 ..BasisDerivativeJets::empty()
1076 }),
1077 LatentBasisKind::Sphere { .. }
1078 | LatentBasisKind::PeriodicBspline { .. }
1079 | LatentBasisKind::Pca { .. }
1080 | LatentBasisKind::TensorBspline { .. } => Ok(BasisDerivativeJets {
1081 operator_resident: true,
1082 ..BasisDerivativeJets::empty()
1083 }),
1084 }
1085}