1use ndarray::{Array1, ArrayView1};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct SigmaJet1 {
5 pub sigma: f64,
6 pub d1: f64,
7}
8
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct SigmaJet3 {
11 pub sigma: f64,
12 pub d1: f64,
13 pub d2: f64,
14 pub d3: f64,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub(crate) struct SigmaJet4 {
19 pub sigma: f64,
20 pub d1: f64,
21 pub d2: f64,
22 pub d3: f64,
23 pub d4: f64,
24}
25
26#[inline]
32pub fn safe_exp(eta: f64) -> f64 {
33 eta.exp()
34}
35
36#[inline]
37pub fn exp_sigma_jet1_scalar(eta: f64) -> SigmaJet1 {
38 let sigma = safe_exp(eta);
39 SigmaJet1 { sigma, d1: sigma }
40}
41
42#[inline]
43pub fn exp_sigma_from_eta_scalar(eta: f64) -> f64 {
44 safe_exp(eta)
45}
46
47pub const EXP_SATURATION_MAX_ARG: f64 = 709.78271289338;
62
63#[inline]
74pub fn exp_neg_stable(x: f64) -> f64 {
75 (-x).min(EXP_SATURATION_MAX_ARG).exp()
76}
77
78#[inline]
86pub fn exp_sigma_inverse_from_eta_scalar(eta: f64) -> f64 {
87 exp_neg_stable(eta)
88}
89
90#[inline]
101pub fn survival_q0_from_eta(eta_t: f64, eta_ls: f64) -> f64 {
102 if eta_t == 0.0 {
103 return 0.0;
104 }
105 let log_abs = eta_t.abs().ln() - eta_ls;
106 if log_abs > EXP_SATURATION_MAX_ARG {
107 return if eta_t > 0.0 { -f64::MAX } else { f64::MAX };
108 }
109 if -eta_ls > EXP_SATURATION_MAX_ARG {
110 let mag = log_abs.exp();
111 return if eta_t > 0.0 { -mag } else { mag };
112 }
113 let q = -eta_t * exp_sigma_inverse_from_eta_scalar(eta_ls);
114 if q.is_finite() {
115 q
116 } else {
117 if eta_t > 0.0 { -f64::MAX } else { f64::MAX }
120 }
121}
122
123#[inline]
124pub fn exp_sigma_eta_for_sigma_scalar(sigma: f64) -> f64 {
125 assert!(
126 sigma.is_finite(),
127 "exp sigma inverse link requires finite sigma: sigma={sigma}"
128 );
129 assert!(
130 sigma > 0.0,
131 "exp sigma inverse link: sigma must be positive (got sigma={sigma})"
132 );
133 sigma.ln()
134}
135
136#[inline]
137pub fn exp_sigma_jet3_scalar(eta: f64) -> SigmaJet3 {
138 let jet = exp_sigma_jet4_scalar(eta);
139 SigmaJet3 {
140 sigma: jet.sigma,
141 d1: jet.d1,
142 d2: jet.d2,
143 d3: jet.d3,
144 }
145}
146
147#[inline]
148pub fn exp_sigma_derivs_up_to_third_scalar(eta: f64) -> (f64, f64, f64, f64) {
149 let jet = exp_sigma_jet3_scalar(eta);
150 (jet.sigma, jet.d1, jet.d2, jet.d3)
151}
152
153pub fn exp_sigma_derivs_up_to_third(
154 eta: ArrayView1<'_, f64>,
155) -> (Array1<f64>, Array1<f64>, Array1<f64>, Array1<f64>) {
156 let n = eta.len();
157 let mut sigma = Array1::<f64>::uninit(n);
158 let mut d1 = Array1::<f64>::uninit(n);
159 let mut d2 = Array1::<f64>::uninit(n);
160 let mut d3 = Array1::<f64>::uninit(n);
161 for i in 0..n {
162 let jet = exp_sigma_jet3_scalar(eta[i]);
163 sigma[i].write(jet.sigma);
164 d1[i].write(jet.d1);
165 d2[i].write(jet.d2);
166 d3[i].write(jet.d3);
167 }
168 unsafe {
171 (
172 sigma.assume_init(),
173 d1.assume_init(),
174 d2.assume_init(),
175 d3.assume_init(),
176 )
177 }
178}
179
180#[inline]
181pub(crate) fn exp_sigma_jet4_scalar(eta: f64) -> SigmaJet4 {
182 let sigma = safe_exp(eta);
183 SigmaJet4 {
184 sigma,
185 d1: sigma,
186 d2: sigma,
187 d3: sigma,
188 d4: sigma,
189 }
190}
191
192#[inline]
193pub fn exp_sigma_derivs_up_to_fourth_scalar(eta: f64) -> (f64, f64, f64, f64, f64) {
194 let jet = exp_sigma_jet4_scalar(eta);
195 (jet.sigma, jet.d1, jet.d2, jet.d3, jet.d4)
196}
197
198pub fn exp_sigma_derivs_up_to_fourth(
199 eta: ArrayView1<'_, f64>,
200) -> (
201 Array1<f64>,
202 Array1<f64>,
203 Array1<f64>,
204 Array1<f64>,
205 Array1<f64>,
206) {
207 let n = eta.len();
208 let mut sigma = Array1::<f64>::uninit(n);
209 let mut d1 = Array1::<f64>::uninit(n);
210 let mut d2 = Array1::<f64>::uninit(n);
211 let mut d3 = Array1::<f64>::uninit(n);
212 let mut d4 = Array1::<f64>::uninit(n);
213 for i in 0..n {
214 let jet = exp_sigma_jet4_scalar(eta[i]);
215 sigma[i].write(jet.sigma);
216 d1[i].write(jet.d1);
217 d2[i].write(jet.d2);
218 d3[i].write(jet.d3);
219 d4[i].write(jet.d4);
220 }
221 unsafe {
224 (
225 sigma.assume_init(),
226 d1.assume_init(),
227 d2.assume_init(),
228 d3.assume_init(),
229 d4.assume_init(),
230 )
231 }
232}
233
234pub const LOGB_SIGMA_FLOOR: f64 = 0.01;
282
283#[inline]
284pub fn logb_sigma_jet1_scalar(eta: f64) -> SigmaJet1 {
285 let s = safe_exp(eta);
286 SigmaJet1 {
287 sigma: LOGB_SIGMA_FLOOR + s,
288 d1: s,
289 }
290}
291
292#[inline]
293pub fn logb_sigma_from_eta_scalar(eta: f64) -> f64 {
294 LOGB_SIGMA_FLOOR + safe_exp(eta)
295}
296
297#[inline]
309pub fn logb_sigma_from_eta_with_floor_scalar(floor: f64, eta: f64) -> f64 {
310 floor + safe_exp(eta)
311}
312
313#[inline]
314pub fn logb_sigma_eta_for_sigma_scalar(sigma: f64) -> f64 {
315 assert!(
316 sigma.is_finite(),
317 "logb sigma inverse link requires finite sigma: sigma={sigma}"
318 );
319 assert!(
320 sigma > LOGB_SIGMA_FLOOR,
321 "logb sigma inverse link: sigma must exceed LOGB_SIGMA_FLOOR (got sigma={sigma}, floor={LOGB_SIGMA_FLOOR})"
322 );
323 (sigma - LOGB_SIGMA_FLOOR).ln()
324}
325
326#[inline]
327pub fn logb_sigma_jet3_scalar(eta: f64) -> SigmaJet3 {
328 let jet = logb_sigma_jet4_scalar(eta);
329 SigmaJet3 {
330 sigma: jet.sigma,
331 d1: jet.d1,
332 d2: jet.d2,
333 d3: jet.d3,
334 }
335}
336
337#[inline]
338pub fn logb_sigma_derivs_up_to_third_scalar(eta: f64) -> (f64, f64, f64, f64) {
339 let jet = logb_sigma_jet3_scalar(eta);
340 (jet.sigma, jet.d1, jet.d2, jet.d3)
341}
342
343#[inline]
344pub(crate) fn logb_sigma_jet4_scalar(eta: f64) -> SigmaJet4 {
345 let s = safe_exp(eta);
346 SigmaJet4 {
347 sigma: LOGB_SIGMA_FLOOR + s,
348 d1: s,
349 d2: s,
350 d3: s,
351 d4: s,
352 }
353}
354
355#[inline]
356pub fn logb_sigma_derivs_up_to_fourth_scalar(eta: f64) -> (f64, f64, f64, f64, f64) {
357 let jet = logb_sigma_jet4_scalar(eta);
358 (jet.sigma, jet.d1, jet.d2, jet.d3, jet.d4)
359}
360
361pub fn logb_sigma_derivs_up_to_fourth(
362 eta: ArrayView1<'_, f64>,
363) -> (
364 Array1<f64>,
365 Array1<f64>,
366 Array1<f64>,
367 Array1<f64>,
368 Array1<f64>,
369) {
370 let n = eta.len();
371 let mut sigma = Array1::<f64>::uninit(n);
372 let mut d1 = Array1::<f64>::uninit(n);
373 let mut d2 = Array1::<f64>::uninit(n);
374 let mut d3 = Array1::<f64>::uninit(n);
375 let mut d4 = Array1::<f64>::uninit(n);
376 for i in 0..n {
377 let jet = logb_sigma_jet4_scalar(eta[i]);
378 sigma[i].write(jet.sigma);
379 d1[i].write(jet.d1);
380 d2[i].write(jet.d2);
381 d3[i].write(jet.d3);
382 d4[i].write(jet.d4);
383 }
384 unsafe {
387 (
388 sigma.assume_init(),
389 d1.assume_init(),
390 d2.assume_init(),
391 d3.assume_init(),
392 d4.assume_init(),
393 )
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use std::fs;
401 use std::path::Path;
402
403 fn collect_rs_files(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
404 let Ok(entries) = fs::read_dir(dir) else {
405 return;
406 };
407 for entry in entries.flatten() {
408 let path = entry.path();
409 if path.is_dir() {
410 collect_rs_files(&path, out);
411 continue;
412 }
413 if path.extension().and_then(|e| e.to_str()) == Some("rs") {
414 out.push(path);
415 }
416 }
417 }
418
419 fn stripwhitespace(s: &str) -> String {
420 s.chars().filter(|c| !c.is_whitespace()).collect()
421 }
422
423 #[test]
424 fn forbid_bounded_sigma_link_pattern_in_source() {
425 let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
426 let mut files = Vec::new();
427 collect_rs_files(&root, &mut files);
428
429 let self_file = root.join("sigma_link.rs");
435
436 let bad_patterns = [
437 "bounded_sigma",
438 "model.sigma_min",
439 "model.sigma_max",
440 "payload.sigma_min",
441 "payload.sigma_max",
442 "survival_sigma_min",
443 "survival_sigma_max",
444 "fnsafe_sigma_from_eta(",
445 "fnsigma_and_deriv_from_eta(",
446 "fnsigma_from_eta_scalar(",
447 ];
448
449 for file in files {
450 if file == self_file {
451 continue;
452 }
453 let Ok(content) = fs::read_to_string(&file) else {
454 continue;
455 };
456 let compact = stripwhitespace(&content);
457 for pat in bad_patterns {
458 assert!(
459 !compact.contains(pat),
460 "forbidden sigma link pattern '{pat}' found in {}",
461 file.display()
462 );
463 }
464 }
465 }
466
467 fn assert_sigma_derivs_match_fd(
474 sigma_at: impl Fn(f64) -> f64,
475 derivs_at: impl Fn(f64) -> (f64, f64, f64, f64),
476 ) {
477 let h = 1e-5;
478 let h3 = 2e-3;
479 let points = [-6.0, -3.5, -1.2, 0.0, 0.8, 2.1, 6.0];
480
481 for &eta in &points {
482 let (s, d1, d2, d3) = derivs_at(eta);
483 let s_plus = sigma_at(eta + h);
484 let s_minus = sigma_at(eta - h);
485
486 let d1fd = (s_plus - s_minus) / (2.0 * h);
487 let d2fd = (s_plus - 2.0 * s + s_minus) / (h * h);
488 let d2_at = |x: f64| {
489 let xp = sigma_at(x + h3);
490 let xc = sigma_at(x);
491 let xm = sigma_at(x - h3);
492 (xp - 2.0 * xc + xm) / (h3 * h3)
493 };
494 let d3fd = (d2_at(eta + h3) - d2_at(eta - h3)) / (2.0 * h3);
495
496 let d1_scale = d1.abs().max(d1fd.abs()).max(1.0);
497 let d2_scale = d2.abs().max(d2fd.abs()).max(1.0);
498 let d3_scale = d3.abs().max(d3fd.abs()).max(1.0);
499
500 assert!((d1 - d1fd).abs() < 1e-8 * d1_scale);
501 assert!((d2 - d2fd).abs() < 1e-5 * d2_scale);
502 assert!((d3 - d3fd).abs() < 5e-4 * d3_scale);
503 }
504 }
505
506 #[test]
507 fn exp_sigma_derivatives_match_finite_difference() {
508 assert_sigma_derivs_match_fd(
509 exp_sigma_from_eta_scalar,
510 exp_sigma_derivs_up_to_third_scalar,
511 );
512 }
513
514 #[test]
515 fn exp_sigma_fourth_derivative_matches_finite_difference() {
516 let h = 2e-3;
517 let points = [-6.0, -3.0, -1.1, 0.0, 0.6, 1.9, 5.5];
518
519 let d3_at = |x: f64| exp_sigma_derivs_up_to_third_scalar(x).3;
520 for &eta in &points {
521 let (_, d1_4, d2_4, d3_4, d4_4) = exp_sigma_derivs_up_to_fourth_scalar(eta);
522 let (_, d1_3, d2_3, d3_3) = exp_sigma_derivs_up_to_third_scalar(eta);
523 assert!((d1_4 - d1_3).abs() < 1e-12);
524 assert!((d2_4 - d2_3).abs() < 1e-12);
525 assert!((d3_4 - d3_3).abs() < 1e-12);
526
527 let d4fd = (d3_at(eta + h) - d3_at(eta - h)) / (2.0 * h);
528 let d4_scale = d4_4.abs().max(d4fd.abs()).max(1.0);
529 assert!((d4_4 - d4fd).abs() < 5e-4 * d4_scale);
530 }
531 }
532
533 #[test]
534 fn exp_sigmavectorized_up_to_fourth_matches_scalar() {
535 let eta = Array1::from_vec(vec![-701.0, -4.2, -1.4, -0.2, 0.4, 1.9, 3.1, 701.0]);
536 let (s, d1, d2, d3, d4) = exp_sigma_derivs_up_to_fourth(eta.view());
537 for i in 0..eta.len() {
538 let (ss, d1s, d2s, d3s, d4s) = exp_sigma_derivs_up_to_fourth_scalar(eta[i]);
539 assert!((s[i] - ss).abs() < 1e-12);
540 assert!((d1[i] - d1s).abs() < 1e-12);
541 assert!((d2[i] - d2s).abs() < 1e-12);
542 assert!((d3[i] - d3s).abs() < 1e-12);
543 assert!((d4[i] - d4s).abs() < 1e-12);
544 }
545 }
546
547 #[test]
548 fn exp_sigma_inverse_accepts_positive_sigma() {
549 let eta = exp_sigma_eta_for_sigma_scalar(2.5);
550 assert!(eta.is_finite());
551 assert!((eta - 2.5_f64.ln()).abs() < 1e-12);
552 }
553
554 #[test]
555 #[should_panic(expected = "sigma must be positive")]
556 fn exp_sigma_inverse_rejects_non_positive_sigma() {
557 exp_sigma_eta_for_sigma_scalar(0.0);
558 }
559
560 #[test]
561 fn safe_exp_matches_native_exp_semantics() {
562 assert!(safe_exp(0.0).is_finite());
563 assert!(safe_exp(700.0).is_finite());
564 assert!(safe_exp(-700.0).is_finite());
565 assert!(safe_exp(1000.0).is_infinite());
566 assert_eq!(safe_exp(-1000.0), 0.0);
567 assert!(safe_exp(f64::MAX).is_infinite());
568 assert_eq!(safe_exp(f64::MIN), 0.0);
569 assert!((safe_exp(1.0) - 1.0_f64.exp()).abs() < 1e-15);
570 assert!((safe_exp(-5.0) - (-5.0_f64).exp()).abs() < 1e-15);
571 }
572
573 #[test]
574 fn exp_sigma_derivatives_match_exact_exp_in_far_tails() {
575 for &eta in &[709.0, -745.0] {
576 let (sigma, d1, d2, d3, d4) = exp_sigma_derivs_up_to_fourth_scalar(eta);
577 assert_eq!(sigma, eta.exp());
578 assert_eq!(d1, sigma);
579 assert_eq!(d2, sigma);
580 assert_eq!(d3, sigma);
581 assert_eq!(d4, sigma);
582 }
583 }
584
585 #[test]
586 fn logb_sigma_floor_bounds_below_for_arbitrarily_negative_eta() {
587 for &eta in &[-1000.0, -100.0, -50.0, -10.0] {
588 let sigma = logb_sigma_from_eta_scalar(eta);
589 assert!(sigma >= LOGB_SIGMA_FLOOR);
590 assert!(sigma.is_finite());
591 let inv_s2 = (sigma * sigma).recip();
592 assert!(inv_s2 <= LOGB_SIGMA_FLOOR.powi(-2) + 1e-12);
593 }
594 }
595
596 #[test]
597 fn logb_sigma_recovers_exp_link_in_upper_regime() {
598 for &eta in &[3.0, 5.0, 10.0] {
599 let logb = logb_sigma_from_eta_scalar(eta);
600 let pure_exp = exp_sigma_from_eta_scalar(eta);
601 let rel_err = (logb - pure_exp).abs() / pure_exp;
602 assert!(rel_err < 1e-2);
603 }
604 }
605
606 #[test]
607 fn logb_sigma_jet_d1_through_d4_match_pure_exp_eta() {
608 for &eta in &[-3.0_f64, 0.0, 2.0] {
609 let s = eta.exp();
610 let jet1 = logb_sigma_jet1_scalar(eta);
611 let jet3 = logb_sigma_jet3_scalar(eta);
612 let jet4 = logb_sigma_jet4_scalar(eta);
613 assert!((jet1.sigma - (LOGB_SIGMA_FLOOR + s)).abs() < 1e-12);
614 assert!((jet1.d1 - s).abs() < 1e-12);
615 assert!((jet3.sigma - (LOGB_SIGMA_FLOOR + s)).abs() < 1e-12);
616 assert!((jet3.d1 - s).abs() < 1e-12);
617 assert!((jet3.d2 - s).abs() < 1e-12);
618 assert!((jet3.d3 - s).abs() < 1e-12);
619 assert!((jet4.d4 - s).abs() < 1e-12);
620 }
621 }
622
623 #[test]
624 fn logb_sigma_derivatives_match_finite_difference() {
625 assert_sigma_derivs_match_fd(
626 logb_sigma_from_eta_scalar,
627 logb_sigma_derivs_up_to_third_scalar,
628 );
629 }
630
631 #[test]
632 fn logb_sigma_inverse_round_trip() {
633 for &sigma in &[
634 LOGB_SIGMA_FLOOR + 1e-3,
635 LOGB_SIGMA_FLOOR + 0.5,
636 1.0,
637 10.0,
638 1e6,
639 ] {
640 let eta = logb_sigma_eta_for_sigma_scalar(sigma);
641 let recovered = logb_sigma_from_eta_scalar(eta);
642 let scale = sigma.abs().max(1.0);
643 assert!((recovered - sigma).abs() < 1e-10 * scale);
644 }
645 }
646
647 #[test]
648 #[should_panic(expected = "sigma must exceed LOGB_SIGMA_FLOOR")]
649 fn logb_sigma_inverse_rejects_sigma_at_floor() {
650 logb_sigma_eta_for_sigma_scalar(LOGB_SIGMA_FLOOR);
651 }
652
653 #[test]
654 fn logb_sigma_vectorized_matches_scalar() {
655 let eta = Array1::from_vec(vec![-701.0, -4.2, -1.4, -0.2, 0.4, 1.9, 3.1, 701.0]);
656 let (s, d1, d2, d3, d4) = logb_sigma_derivs_up_to_fourth(eta.view());
657 for i in 0..eta.len() {
658 let (ss, d1s, d2s, d3s, d4s) = logb_sigma_derivs_up_to_fourth_scalar(eta[i]);
659 assert!((s[i] - ss).abs() < 1e-12);
660 assert!((d1[i] - d1s).abs() < 1e-12);
661 assert!((d2[i] - d2s).abs() < 1e-12);
662 assert!((d3[i] - d3s).abs() < 1e-12);
663 assert!((d4[i] - d4s).abs() < 1e-12);
664 }
665 }
666}