1extern crate alloc;
18
19use alloc::string::String;
20use alloc::vec::Vec;
21use core::f64::consts::PI;
22
23use nalgebra::Cholesky;
24use rand::prelude::*;
25use rand::SeedableRng;
26use rand_xoshiro::Xoshiro256PlusPlus;
27
28use crate::analysis::mde::estimate_mde;
29use crate::constants::DEFAULT_SEED;
30use crate::math;
31use crate::preflight::{run_core_checks, PreflightResult};
32use crate::statistics::{
33 bootstrap_difference_covariance, bootstrap_difference_covariance_discrete, AcquisitionStream,
34 OnlineStats,
35};
36use crate::types::{Matrix9, Vector9};
37
38use super::CalibrationSnapshot;
39
40#[cfg(feature = "parallel")]
41use rayon::prelude::*;
42
43#[cfg(feature = "parallel")]
46#[inline]
47fn counter_rng_seed(base_seed: u64, counter: u64) -> u64 {
48 let mut z = base_seed.wrapping_add(counter.wrapping_mul(0x9e3779b97f4a7c15));
49 z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
50 z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
51 z ^ (z >> 31)
52}
53
54const CONSERVATIVE_PRIOR_SCALE: f64 = 1.5;
57
58const TARGET_EXCEEDANCE: f64 = 0.62;
60
61const PRIOR_CALIBRATION_SAMPLES: usize = 50_000;
63
64const MAX_CALIBRATION_ITERATIONS: usize = 20;
66
67const CONDITION_NUMBER_THRESHOLD: f64 = 1e4;
69
70const DIAGONAL_FLOOR: f64 = 1e-12;
72
73pub const NU: f64 = 4.0;
75
76#[derive(Debug, Clone)]
91pub struct Calibration {
92 pub sigma_rate: Matrix9,
96
97 pub block_length: usize,
100
101 pub sigma_t: f64,
104
105 pub l_r: Matrix9,
108
109 pub prior_cov_marginal: Matrix9,
112
113 pub theta_ns: f64,
115
116 pub calibration_samples: usize,
118
119 pub discrete_mode: bool,
122
123 pub mde_shift_ns: f64,
125
126 pub mde_tail_ns: f64,
128
129 pub calibration_snapshot: CalibrationSnapshot,
131
132 pub timer_resolution_ns: f64,
134
135 pub samples_per_second: f64,
138
139 pub c_floor: f64,
143
144 pub projection_mismatch_thresh: f64,
147
148 pub theta_tick: f64,
151
152 pub theta_eff: f64,
155
156 pub theta_floor_initial: f64,
158
159 pub rng_seed: u64,
161
162 pub batch_k: u32,
166
167 pub preflight_result: PreflightResult,
171}
172
173impl Calibration {
174 #[allow(clippy::too_many_arguments)]
176 pub fn new(
177 sigma_rate: Matrix9,
178 block_length: usize,
179 sigma_t: f64,
180 l_r: Matrix9,
181 theta_ns: f64,
182 calibration_samples: usize,
183 discrete_mode: bool,
184 mde_shift_ns: f64,
185 mde_tail_ns: f64,
186 calibration_snapshot: CalibrationSnapshot,
187 timer_resolution_ns: f64,
188 samples_per_second: f64,
189 c_floor: f64,
190 projection_mismatch_thresh: f64,
191 theta_tick: f64,
192 theta_eff: f64,
193 theta_floor_initial: f64,
194 rng_seed: u64,
195 batch_k: u32,
196 ) -> Self {
197 let r = l_r * l_r.transpose();
200 let prior_cov_marginal = r * (2.0 * sigma_t * sigma_t);
201
202 Self {
203 sigma_rate,
204 block_length,
205 sigma_t,
206 l_r,
207 prior_cov_marginal,
208 theta_ns,
209 calibration_samples,
210 discrete_mode,
211 mde_shift_ns,
212 mde_tail_ns,
213 calibration_snapshot,
214 timer_resolution_ns,
215 samples_per_second,
216 c_floor,
217 projection_mismatch_thresh,
218 theta_tick,
219 theta_eff,
220 theta_floor_initial,
221 rng_seed,
222 batch_k,
223 preflight_result: PreflightResult::new(),
224 }
225 }
226
227 #[allow(clippy::too_many_arguments)]
229 pub fn with_preflight(
230 sigma_rate: Matrix9,
231 block_length: usize,
232 sigma_t: f64,
233 l_r: Matrix9,
234 theta_ns: f64,
235 calibration_samples: usize,
236 discrete_mode: bool,
237 mde_shift_ns: f64,
238 mde_tail_ns: f64,
239 calibration_snapshot: CalibrationSnapshot,
240 timer_resolution_ns: f64,
241 samples_per_second: f64,
242 c_floor: f64,
243 projection_mismatch_thresh: f64,
244 theta_tick: f64,
245 theta_eff: f64,
246 theta_floor_initial: f64,
247 rng_seed: u64,
248 batch_k: u32,
249 preflight_result: PreflightResult,
250 ) -> Self {
251 let r = l_r * l_r.transpose();
254 let prior_cov_marginal = r * (2.0 * sigma_t * sigma_t);
255
256 Self {
257 sigma_rate,
258 block_length,
259 sigma_t,
260 l_r,
261 prior_cov_marginal,
262 theta_ns,
263 calibration_samples,
264 discrete_mode,
265 mde_shift_ns,
266 mde_tail_ns,
267 calibration_snapshot,
268 timer_resolution_ns,
269 samples_per_second,
270 c_floor,
271 projection_mismatch_thresh,
272 theta_tick,
273 theta_eff,
274 theta_floor_initial,
275 rng_seed,
276 batch_k,
277 preflight_result,
278 }
279 }
280
281 pub fn n_eff(&self, n: usize) -> usize {
290 if self.block_length == 0 {
291 return n.max(1);
292 }
293 (n / self.block_length).max(1)
294 }
295
296 pub fn covariance_for_n(&self, n: usize) -> Matrix9 {
303 if n == 0 {
304 return self.sigma_rate; }
306 let n_eff = self.n_eff(n);
307 self.sigma_rate / (n_eff as f64)
308 }
309
310 pub fn covariance_for_n_raw(&self, n: usize) -> Matrix9 {
316 if n == 0 {
317 return self.sigma_rate; }
319 self.sigma_rate / (n as f64)
320 }
321
322 pub fn estimate_collection_time_secs(&self, n: usize) -> f64 {
326 if self.samples_per_second <= 0.0 {
327 return 0.0;
328 }
329 n as f64 / self.samples_per_second
330 }
331
332 pub fn to_summary(&self) -> crate::ffi_summary::CalibrationSummary {
334 crate::ffi_summary::CalibrationSummary {
335 block_length: self.block_length,
336 calibration_samples: self.calibration_samples,
337 discrete_mode: self.discrete_mode,
338 timer_resolution_ns: self.timer_resolution_ns,
339 theta_ns: self.theta_ns,
340 theta_eff: self.theta_eff,
341 theta_floor_initial: self.theta_floor_initial,
342 theta_tick: self.theta_tick,
343 mde_shift_ns: self.mde_shift_ns,
344 mde_tail_ns: self.mde_tail_ns,
345 projection_mismatch_thresh: self.projection_mismatch_thresh,
346 samples_per_second: self.samples_per_second,
347 }
348 }
349}
350
351#[derive(Debug, Clone)]
355pub struct CalibrationConfig {
356 pub calibration_samples: usize,
358
359 pub bootstrap_iterations: usize,
361
362 pub timer_resolution_ns: f64,
364
365 pub theta_ns: f64,
367
368 pub alpha: f64,
370
371 pub seed: u64,
373
374 pub skip_preflight: bool,
376
377 pub force_discrete_mode: bool,
379}
380
381impl Default for CalibrationConfig {
382 fn default() -> Self {
383 Self {
384 calibration_samples: 5000,
385 bootstrap_iterations: 200, timer_resolution_ns: 1.0,
387 theta_ns: 100.0,
388 alpha: 0.01,
389 seed: DEFAULT_SEED,
390 skip_preflight: false,
391 force_discrete_mode: false,
392 }
393 }
394}
395
396#[derive(Debug, Clone)]
398pub enum CalibrationError {
399 TooFewSamples {
401 collected: usize,
403 minimum: usize,
405 },
406
407 CovarianceEstimationFailed {
409 reason: String,
411 },
412
413 PreflightCheckFailed {
415 check: String,
417 message: String,
419 },
420
421 TimerTooCoarse {
423 resolution_ns: f64,
425 operation_ns: f64,
427 },
428}
429
430impl core::fmt::Display for CalibrationError {
431 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
432 match self {
433 CalibrationError::TooFewSamples { collected, minimum } => {
434 write!(
435 f,
436 "Too few samples: collected {}, need at least {}",
437 collected, minimum
438 )
439 }
440 CalibrationError::CovarianceEstimationFailed { reason } => {
441 write!(f, "Covariance estimation failed: {}", reason)
442 }
443 CalibrationError::PreflightCheckFailed { check, message } => {
444 write!(f, "Preflight check '{}' failed: {}", check, message)
445 }
446 CalibrationError::TimerTooCoarse {
447 resolution_ns,
448 operation_ns,
449 } => {
450 write!(
451 f,
452 "Timer resolution ({:.1}ns) too coarse for operation ({:.1}ns)",
453 resolution_ns, operation_ns
454 )
455 }
456 }
457 }
458}
459
460pub fn calibrate(
478 baseline_samples: &[u64],
479 sample_samples: &[u64],
480 ns_per_tick: f64,
481 config: &CalibrationConfig,
482 samples_per_second: f64,
483) -> Result<Calibration, CalibrationError> {
484 let n = baseline_samples.len().min(sample_samples.len());
485
486 const MIN_CALIBRATION_SAMPLES: usize = 100;
488 if n < MIN_CALIBRATION_SAMPLES {
489 return Err(CalibrationError::TooFewSamples {
490 collected: n,
491 minimum: MIN_CALIBRATION_SAMPLES,
492 });
493 }
494
495 let baseline_ns: Vec<f64> = baseline_samples[..n]
497 .iter()
498 .map(|&t| t as f64 * ns_per_tick)
499 .collect();
500 let sample_ns: Vec<f64> = sample_samples[..n]
501 .iter()
502 .map(|&t| t as f64 * ns_per_tick)
503 .collect();
504
505 let unique_baseline = count_unique(&baseline_ns);
507 let unique_sample = count_unique(&sample_ns);
508 let min_uniqueness = (unique_baseline as f64 / n as f64).min(unique_sample as f64 / n as f64);
509 let discrete_mode = config.force_discrete_mode || min_uniqueness < 0.10;
510
511 let mut acquisition_stream = AcquisitionStream::with_capacity(2 * n);
513 acquisition_stream.push_batch_interleaved(&baseline_ns, &sample_ns);
514 let interleaved = acquisition_stream.to_timing_samples();
515
516 let cov_estimate = if discrete_mode {
518 bootstrap_difference_covariance_discrete(
519 &baseline_ns,
520 &sample_ns,
521 config.bootstrap_iterations,
522 config.seed,
523 )
524 } else {
525 bootstrap_difference_covariance(
526 &interleaved,
527 config.bootstrap_iterations,
528 config.seed,
529 false,
530 )
531 };
532
533 if !cov_estimate.is_stable() {
535 return Err(CalibrationError::CovarianceEstimationFailed {
536 reason: String::from("Covariance matrix is not positive definite"),
537 });
538 }
539
540 let sigma_rate = cov_estimate.matrix * (n as f64);
542
543 let mde = estimate_mde(&cov_estimate.matrix, config.alpha);
545
546 let preflight_result = if config.skip_preflight {
548 PreflightResult::new()
549 } else {
550 run_core_checks(
551 &baseline_ns,
552 &sample_ns,
553 config.timer_resolution_ns,
554 config.seed,
555 )
556 };
557
558 let calibration_snapshot = compute_calibration_snapshot(&baseline_ns, &sample_ns);
560
561 let c_floor = compute_c_floor_9d(&sigma_rate, config.seed);
563
564 let theta_tick = config.timer_resolution_ns;
566
567 let theta_floor_initial = (c_floor / (n as f64).sqrt()).max(theta_tick);
569
570 let theta_eff = if config.theta_ns > 0.0 {
572 config.theta_ns.max(theta_floor_initial)
573 } else {
574 theta_floor_initial
575 };
576
577 let (sigma_t, l_r) =
579 calibrate_t_prior_scale(&sigma_rate, theta_eff, n, discrete_mode, config.seed);
580
581 Ok(Calibration::with_preflight(
582 sigma_rate,
583 cov_estimate.block_size,
584 sigma_t,
585 l_r,
586 config.theta_ns,
587 n,
588 discrete_mode,
589 mde.shift_ns,
590 mde.tail_ns,
591 calibration_snapshot,
592 config.timer_resolution_ns,
593 samples_per_second,
594 c_floor,
595 cov_estimate.q_thresh,
596 theta_tick,
597 theta_eff,
598 theta_floor_initial,
599 config.seed,
600 1, preflight_result,
602 ))
603}
604
605fn count_unique(values: &[f64]) -> usize {
607 use alloc::collections::BTreeSet;
608
609 let buckets: BTreeSet<i64> = values.iter().map(|&v| (v * 1000.0) as i64).collect();
612 buckets.len()
613}
614
615fn compute_calibration_snapshot(baseline_ns: &[f64], sample_ns: &[f64]) -> CalibrationSnapshot {
617 let mut baseline_stats = OnlineStats::new();
618 let mut sample_stats = OnlineStats::new();
619
620 for &t in baseline_ns {
621 baseline_stats.update(t);
622 }
623 for &t in sample_ns {
624 sample_stats.update(t);
625 }
626
627 CalibrationSnapshot::new(baseline_stats.finalize(), sample_stats.finalize())
628}
629
630pub fn compute_prior_cov_9d(
646 sigma_rate: &Matrix9,
647 sigma_prior: f64,
648 discrete_mode: bool,
649) -> Matrix9 {
650 let r = compute_correlation_matrix(sigma_rate);
652
653 let r = apply_correlation_regularization(&r, discrete_mode);
657
658 r * (sigma_prior * sigma_prior)
660}
661
662fn compute_correlation_matrix(sigma: &Matrix9) -> Matrix9 {
667 let mut d_inv_sqrt = [0.0_f64; 9];
669 for i in 0..9 {
670 let var = sigma[(i, i)].max(DIAGONAL_FLOOR);
671 d_inv_sqrt[i] = 1.0 / math::sqrt(var);
672 }
673
674 let mut r = *sigma;
676 for i in 0..9 {
677 for j in 0..9 {
678 r[(i, j)] *= d_inv_sqrt[i] * d_inv_sqrt[j];
679 }
680 }
681
682 r
683}
684
685fn estimate_condition_number(r: &Matrix9) -> f64 {
691 let diag: Vec<f64> = (0..9).map(|i| r[(i, i)].abs()).collect();
693 let max_diag = diag.iter().cloned().fold(0.0_f64, f64::max);
694 let min_diag = diag.iter().cloned().fold(f64::INFINITY, f64::min);
695
696 if min_diag < DIAGONAL_FLOOR {
697 return f64::INFINITY;
698 }
699
700 max_diag / min_diag
703}
704
705fn is_fragile_regime(r: &Matrix9, discrete_mode: bool) -> bool {
712 if discrete_mode {
713 return true;
714 }
715
716 let cond = estimate_condition_number(r);
717 if cond > CONDITION_NUMBER_THRESHOLD {
718 return true;
719 }
720
721 Cholesky::new(*r).is_none()
723}
724
725fn apply_correlation_regularization(r: &Matrix9, discrete_mode: bool) -> Matrix9 {
730 let mut r = *r;
731
732 if is_fragile_regime(&r, discrete_mode) {
734 let cond = estimate_condition_number(&r);
736 let lambda = if cond > CONDITION_NUMBER_THRESHOLD * 10.0 {
737 0.2 } else if cond > CONDITION_NUMBER_THRESHOLD {
739 0.1 } else if discrete_mode {
741 0.05 } else {
743 0.01 };
745
746 let identity = Matrix9::identity();
747 r = r * (1.0 - lambda) + identity * lambda;
748 }
749
750 for &eps in &[1e-10, 1e-9, 1e-8, 1e-7, 1e-6] {
753 let r_jittered = r + Matrix9::identity() * eps;
754 if Cholesky::new(r_jittered).is_some() {
755 return r_jittered;
756 }
757 }
758
759 r + Matrix9::identity() * 1e-5
761}
762
763fn compute_median_se(sigma_rate: &Matrix9, n_cal: usize) -> f64 {
765 let mut ses: Vec<f64> = (0..9)
766 .map(|i| {
767 let var = sigma_rate[(i, i)].max(DIAGONAL_FLOOR);
768 math::sqrt(var / n_cal.max(1) as f64)
769 })
770 .collect();
771 ses.sort_by(|a, b| a.total_cmp(b));
772 ses[4] }
774
775pub fn calibrate_t_prior_scale(
794 sigma_rate: &Matrix9,
795 theta_eff: f64,
796 n_cal: usize,
797 discrete_mode: bool,
798 seed: u64,
799) -> (f64, Matrix9) {
800 let median_se = compute_median_se(sigma_rate, n_cal);
801
802 let r = compute_correlation_matrix(sigma_rate);
804 let r_reg = apply_correlation_regularization(&r, discrete_mode);
805 let l_r = match Cholesky::new(r_reg) {
806 Some(c) => c.l().into_owned(),
807 None => Matrix9::identity(),
808 };
809
810 let normalized_effects = precompute_t_prior_effects(&l_r, seed);
823
824 let mut lo = theta_eff * 0.05;
826 let mut hi = (theta_eff * 50.0).max(10.0 * median_se);
827
828 for _ in 0..MAX_CALIBRATION_ITERATIONS {
829 let mid = (lo + hi) / 2.0;
830
831 let threshold = theta_eff / mid;
833 let count = normalized_effects
834 .iter()
835 .filter(|&&m| m > threshold)
836 .count();
837 let exceedance = count as f64 / normalized_effects.len() as f64;
838
839 if (exceedance - TARGET_EXCEEDANCE).abs() < 0.01 {
840 return (mid, l_r); }
842
843 if exceedance > TARGET_EXCEEDANCE {
844 hi = mid;
846 } else {
847 lo = mid;
849 }
850 }
851
852 (theta_eff * CONSERVATIVE_PRIOR_SCALE, l_r)
854}
855
856fn precompute_t_prior_effects(l_r: &Matrix9, seed: u64) -> Vec<f64> {
865 use rand_distr::Gamma;
866
867 #[cfg(feature = "parallel")]
868 {
869 let l_r = *l_r;
870 (0..PRIOR_CALIBRATION_SAMPLES)
871 .into_par_iter()
872 .map(|i| {
873 let mut rng = Xoshiro256PlusPlus::seed_from_u64(counter_rng_seed(seed, i as u64));
874 let gamma_dist = Gamma::new(NU / 2.0, 2.0 / NU).unwrap();
875
876 let lambda: f64 = gamma_dist.sample(&mut rng);
878 let inv_sqrt_lambda = 1.0 / math::sqrt(lambda.max(DIAGONAL_FLOOR));
879
880 let mut z = Vector9::zeros();
882 for j in 0..9 {
883 z[j] = sample_standard_normal(&mut rng);
884 }
885
886 let w = l_r * z;
888 let max_w = w.iter().map(|x| x.abs()).fold(0.0_f64, f64::max);
889 max_w * inv_sqrt_lambda
890 })
891 .collect()
892 }
893
894 #[cfg(not(feature = "parallel"))]
895 {
896 let mut rng = Xoshiro256PlusPlus::seed_from_u64(seed);
897 let gamma_dist = Gamma::new(NU / 2.0, 2.0 / NU).unwrap();
898 let mut effects = Vec::with_capacity(PRIOR_CALIBRATION_SAMPLES);
899
900 for _ in 0..PRIOR_CALIBRATION_SAMPLES {
901 let lambda: f64 = gamma_dist.sample(&mut rng);
902 let inv_sqrt_lambda = 1.0 / math::sqrt(lambda.max(DIAGONAL_FLOOR));
903
904 let mut z = Vector9::zeros();
905 for i in 0..9 {
906 z[i] = sample_standard_normal(&mut rng);
907 }
908
909 let w = l_r * z;
910 let max_w = w.iter().map(|x| x.abs()).fold(0.0_f64, f64::max);
911 effects.push(max_w * inv_sqrt_lambda);
912 }
913 effects
914 }
915}
916
917pub fn compute_c_floor_9d(sigma_rate: &Matrix9, seed: u64) -> f64 {
923 let chol = match Cholesky::new(*sigma_rate) {
924 Some(c) => c,
925 None => {
926 let trace: f64 = (0..9).map(|i| sigma_rate[(i, i)]).sum();
928 return math::sqrt(trace / 9.0) * 2.5; }
930 };
931 let l = chol.l().into_owned();
932
933 #[cfg(feature = "parallel")]
935 let mut max_effects: Vec<f64> = (0..PRIOR_CALIBRATION_SAMPLES)
936 .into_par_iter()
937 .map(|i| {
938 let mut rng = Xoshiro256PlusPlus::seed_from_u64(counter_rng_seed(seed, i as u64));
939 let mut z = Vector9::zeros();
940 for j in 0..9 {
941 z[j] = sample_standard_normal(&mut rng);
942 }
943 let sample = l * z;
944 sample.iter().map(|x| x.abs()).fold(0.0_f64, f64::max)
945 })
946 .collect();
947
948 #[cfg(not(feature = "parallel"))]
949 let mut max_effects: Vec<f64> = {
950 let mut rng = Xoshiro256PlusPlus::seed_from_u64(seed);
951 let mut effects = Vec::with_capacity(PRIOR_CALIBRATION_SAMPLES);
952 for _ in 0..PRIOR_CALIBRATION_SAMPLES {
953 let mut z = Vector9::zeros();
954 for i in 0..9 {
955 z[i] = sample_standard_normal(&mut rng);
956 }
957 let sample = l * z;
958 let max_effect = sample.iter().map(|x| x.abs()).fold(0.0_f64, f64::max);
959 effects.push(max_effect);
960 }
961 effects
962 };
963
964 let idx =
966 ((PRIOR_CALIBRATION_SAMPLES as f64 * 0.95) as usize).min(PRIOR_CALIBRATION_SAMPLES - 1);
967 let (_, &mut percentile_95, _) = max_effects.select_nth_unstable_by(idx, |a, b| a.total_cmp(b));
968 percentile_95
969}
970
971fn sample_standard_normal<R: Rng>(rng: &mut R) -> f64 {
973 let u1: f64 = rng.random();
974 let u2: f64 = rng.random();
975 math::sqrt(-2.0 * math::ln(u1.max(1e-12))) * math::cos(2.0 * PI * u2)
976}
977
978#[cfg(test)]
979mod tests {
980 use super::*;
981 use crate::statistics::StatsSnapshot;
982
983 fn make_test_calibration() -> Calibration {
984 let snapshot = CalibrationSnapshot::new(
985 StatsSnapshot {
986 count: 1000,
987 mean: 100.0,
988 variance: 25.0,
989 autocorr_lag1: 0.1,
990 },
991 StatsSnapshot {
992 count: 1000,
993 mean: 105.0,
994 variance: 30.0,
995 autocorr_lag1: 0.12,
996 },
997 );
998
999 let sigma_rate = Matrix9::identity() * 1000.0;
1000 let theta_eff = 100.0;
1001
1002 Calibration::new(
1003 sigma_rate,
1004 10, 100.0, Matrix9::identity(), theta_eff, 5000, false, 5.0, 10.0, snapshot, 1.0, 10000.0, 10.0, 18.48, 0.001, theta_eff, 0.1, 42, 1, )
1023 }
1024
1025 #[test]
1026 fn test_covariance_scaling() {
1027 let cal = make_test_calibration();
1028 let cov_1000 = cal.covariance_for_n(1000);
1034 assert!(
1035 (cov_1000[(0, 0)] - 10.0).abs() < 1e-10,
1036 "expected 10.0, got {}",
1037 cov_1000[(0, 0)]
1038 );
1039
1040 let cov_2000 = cal.covariance_for_n(2000);
1043 assert!(
1044 (cov_2000[(0, 0)] - 5.0).abs() < 1e-10,
1045 "expected 5.0, got {}",
1046 cov_2000[(0, 0)]
1047 );
1048 }
1049
1050 #[test]
1051 fn test_n_eff() {
1052 let cal = make_test_calibration();
1053 assert_eq!(cal.n_eff(100), 10);
1057 assert_eq!(cal.n_eff(1000), 100);
1058 assert_eq!(cal.n_eff(10), 1);
1059 assert_eq!(cal.n_eff(5), 1); assert_eq!(cal.n_eff(0), 1); }
1062
1063 #[test]
1064 fn test_covariance_zero_n() {
1065 let cal = make_test_calibration();
1066 let cov = cal.covariance_for_n(0);
1067 assert!((cov[(0, 0)] - 1000.0).abs() < 1e-10);
1069 }
1070
1071 #[test]
1072 fn test_estimate_collection_time() {
1073 let cal = make_test_calibration();
1074
1075 let time = cal.estimate_collection_time_secs(1000);
1077 assert!((time - 0.1).abs() < 1e-10);
1078 }
1079
1080 #[test]
1081 fn test_compute_prior_cov_9d_unit_diagonal() {
1082 let sigma_rate = Matrix9::identity();
1084 let prior = compute_prior_cov_9d(&sigma_rate, 10.0, false);
1085
1086 let expected = 100.0;
1091 for i in 0..9 {
1092 assert!(
1093 (prior[(i, i)] - expected).abs() < 1.0,
1094 "Diagonal {} was {}, expected ~{}",
1095 i,
1096 prior[(i, i)],
1097 expected
1098 );
1099 }
1100 }
1101
1102 #[test]
1103 fn test_c_floor_computation() {
1104 let sigma_rate = Matrix9::identity() * 100.0;
1105 let c_floor = compute_c_floor_9d(&sigma_rate, 42);
1106
1107 assert!(c_floor > 15.0, "c_floor {} should be > 15", c_floor);
1109 assert!(c_floor < 40.0, "c_floor {} should be < 40", c_floor);
1110 }
1111
1112 #[test]
1113 fn test_calibration_config_default() {
1114 let config = CalibrationConfig::default();
1115 assert_eq!(config.calibration_samples, 5000);
1116 assert_eq!(config.bootstrap_iterations, 200);
1117 assert!((config.theta_ns - 100.0).abs() < 1e-10);
1118 assert!((config.timer_resolution_ns - 1.0).abs() < 1e-10);
1119 assert!(!config.skip_preflight);
1120 assert!(!config.force_discrete_mode);
1121 }
1122
1123 fn reference_t_prior_exceedance(l_r: &Matrix9, sigma: f64, theta: f64, seed: u64) -> f64 {
1130 use rand_distr::Gamma;
1131
1132 let mut rng = Xoshiro256PlusPlus::seed_from_u64(seed);
1133 let gamma_dist = Gamma::new(NU / 2.0, 2.0 / NU).unwrap();
1134 let mut count = 0usize;
1135
1136 for _ in 0..PRIOR_CALIBRATION_SAMPLES {
1137 let lambda: f64 = gamma_dist.sample(&mut rng);
1138 let scale = sigma / crate::math::sqrt(lambda.max(DIAGONAL_FLOOR));
1139
1140 let mut z = Vector9::zeros();
1141 for i in 0..9 {
1142 z[i] = sample_standard_normal(&mut rng);
1143 }
1144
1145 let delta = l_r * z * scale;
1146 let max_effect = delta.iter().map(|x| x.abs()).fold(0.0_f64, f64::max);
1147 if max_effect > theta {
1148 count += 1;
1149 }
1150 }
1151
1152 count as f64 / PRIOR_CALIBRATION_SAMPLES as f64
1153 }
1154
1155 fn optimized_t_prior_exceedance(normalized_effects: &[f64], sigma: f64, theta: f64) -> f64 {
1157 let threshold = theta / sigma;
1158 let count = normalized_effects
1159 .iter()
1160 .filter(|&&m| m > threshold)
1161 .count();
1162 count as f64 / normalized_effects.len() as f64
1163 }
1164
1165 #[test]
1170 fn test_t_prior_precompute_exceedance_matches_reference() {
1171 let l_r = Matrix9::identity();
1174 let theta = 10.0;
1175 let seed = 12345u64;
1176
1177 let normalized_effects = precompute_t_prior_effects(&l_r, seed);
1179
1180 for sigma in [5.0, 10.0, 15.0, 20.0, 30.0] {
1182 let optimized = optimized_t_prior_exceedance(&normalized_effects, sigma, theta);
1183 let reference = reference_t_prior_exceedance(&l_r, sigma, theta, seed);
1184
1185 assert!(
1188 (0.0..=1.0).contains(&optimized),
1189 "Optimized exceedance {} out of range for sigma={}",
1190 optimized,
1191 sigma
1192 );
1193 assert!(
1194 (0.0..=1.0).contains(&reference),
1195 "Reference exceedance {} out of range for sigma={}",
1196 reference,
1197 sigma
1198 );
1199
1200 println!(
1204 "sigma={}: optimized={:.4}, reference={:.4}",
1205 sigma, optimized, reference
1206 );
1207 }
1208 }
1209
1210 #[test]
1211 fn test_t_prior_exceedance_monotonicity() {
1212 let l_r = Matrix9::identity();
1214 let theta = 10.0;
1215 let seed = 42u64;
1216
1217 let normalized_effects = precompute_t_prior_effects(&l_r, seed);
1218
1219 let mut prev_exceedance = 0.0;
1220 for sigma in [1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0] {
1221 let exceedance = optimized_t_prior_exceedance(&normalized_effects, sigma, theta);
1222
1223 assert!(
1224 exceedance >= prev_exceedance,
1225 "Exceedance should increase with sigma: sigma={}, exc={}, prev={}",
1226 sigma,
1227 exceedance,
1228 prev_exceedance
1229 );
1230 prev_exceedance = exceedance;
1231 }
1232
1233 let large_sigma_exc = optimized_t_prior_exceedance(&normalized_effects, 1000.0, theta);
1235 assert!(
1236 large_sigma_exc > 0.99,
1237 "Exceedance at large sigma should be ~1, got {}",
1238 large_sigma_exc
1239 );
1240
1241 let small_sigma_exc = optimized_t_prior_exceedance(&normalized_effects, 0.1, theta);
1243 assert!(
1244 small_sigma_exc < 0.01,
1245 "Exceedance at small sigma should be ~0, got {}",
1246 small_sigma_exc
1247 );
1248 }
1249
1250 #[test]
1251 fn test_calibrate_t_prior_scale_finds_target_exceedance() {
1252 let sigma_rate = Matrix9::identity() * 100.0;
1254 let theta_eff = 10.0;
1255 let n_cal = 5000;
1256 let discrete_mode = false;
1257 let seed = 42u64;
1258
1259 let (sigma_t, l_r) =
1260 calibrate_t_prior_scale(&sigma_rate, theta_eff, n_cal, discrete_mode, seed);
1261
1262 let normalized_effects = precompute_t_prior_effects(&l_r, seed);
1264 let exceedance = optimized_t_prior_exceedance(&normalized_effects, sigma_t, theta_eff);
1265
1266 assert!(
1267 (exceedance - TARGET_EXCEEDANCE).abs() < 0.05,
1268 "Calibrated t-prior exceedance {} should be near target {}",
1269 exceedance,
1270 TARGET_EXCEEDANCE
1271 );
1272 }
1273
1274 #[test]
1275 fn test_calibration_determinism() {
1276 let sigma_rate = Matrix9::identity() * 100.0;
1278 let theta_eff = 10.0;
1279 let n_cal = 5000;
1280 let discrete_mode = false;
1281 let seed = 12345u64;
1282
1283 let (sigma_t_1, _) =
1284 calibrate_t_prior_scale(&sigma_rate, theta_eff, n_cal, discrete_mode, seed);
1285 let (sigma_t_2, _) =
1286 calibrate_t_prior_scale(&sigma_rate, theta_eff, n_cal, discrete_mode, seed);
1287
1288 assert!(
1289 (sigma_t_1 - sigma_t_2).abs() < 1e-10,
1290 "Same seed should give same sigma_t: {} vs {}",
1291 sigma_t_1,
1292 sigma_t_2
1293 );
1294 }
1295
1296 #[test]
1297 fn test_precomputed_effects_distribution() {
1298 let l_r = Matrix9::identity();
1300 let seed = 42u64;
1301
1302 let effects = precompute_t_prior_effects(&l_r, seed);
1303
1304 assert!(
1306 effects.iter().all(|&m| m > 0.0),
1307 "All effects should be positive"
1308 );
1309
1310 let mean: f64 = effects.iter().sum::<f64>() / effects.len() as f64;
1312 assert!(
1314 mean > 1.0 && mean < 10.0,
1315 "Mean effect {} should be in reasonable range",
1316 mean
1317 );
1318
1319 let variance: f64 =
1321 effects.iter().map(|&m| (m - mean).powi(2)).sum::<f64>() / (effects.len() - 1) as f64;
1322 assert!(variance > 0.1, "Effects should have non-trivial variance");
1323 }
1324
1325 #[test]
1326 #[ignore] fn bench_calibration_timing() {
1328 use std::time::Instant;
1329
1330 let sigma_rate = Matrix9::identity() * 10000.0;
1331 let theta_eff = 100.0;
1332 let n_cal = 5000;
1333 let discrete_mode = false;
1334
1335 let _ = calibrate_t_prior_scale(&sigma_rate, theta_eff, n_cal, discrete_mode, 1);
1337
1338 let iterations = 10;
1340 let start = Instant::now();
1341 for i in 0..iterations {
1342 let _ = calibrate_t_prior_scale(&sigma_rate, theta_eff, n_cal, discrete_mode, i as u64);
1343 }
1344 let t_prior_time = start.elapsed();
1345
1346 println!(
1347 "\n=== Calibration Performance ===\n\
1348 \n\
1349 T-prior calibration: {:?} per call\n\
1350 ({} iterations averaged)",
1351 t_prior_time / iterations as u32,
1352 iterations
1353 );
1354 }
1355}