use super::*;
const PIT_CERTIFIED_DOMAIN_ROUNDOFF_ULPS: f64 = 256.0;
pub fn transformation_normal_pit_score(
h: f64,
lower: f64,
upper: f64,
clip_eps: f64,
) -> Result<f64, String> {
if !(clip_eps.is_finite() && clip_eps > 0.0 && clip_eps < 0.5) {
return Err(TransformationNormalError::InvalidInput {
reason: format!(
"transformation-normal PIT requires clip_eps in (0, 0.5), got {clip_eps}"
),
}
.into());
}
if !(h.is_finite() && lower.is_finite() && upper.is_finite()) {
return Err(TransformationNormalError::InvalidInput { reason: format!(
"transformation-normal PIT requires finite h/lower/upper, got h={h}, lower={lower}, upper={upper}"
) }.into());
}
if upper <= lower {
return Err(TransformationNormalError::MonotonicityViolated { reason: format!(
"transformation-normal PIT endpoint order violated: lower={lower:.6e}, upper={upper:.6e}"
) }.into());
}
let support = upper - lower;
let domain_scale = support.max(lower.abs()).max(upper.abs());
let domain_tol = PIT_CERTIFIED_DOMAIN_ROUNDOFF_ULPS * f64::EPSILON * domain_scale;
if h < lower - domain_tol {
return Err(TransformationNormalError::OutsideCertifiedDomain { reason: format!(
"transformation-normal PIT: transformed response h={h:.6e} lies below the certified \
support lower endpoint {lower:.6e} by {:.6e} (> boundary-roundoff floor \
{domain_tol:.3e}); positivity is certified only on the training support \
[{lower:.6e}, {upper:.6e}], so this response/covariate is outside the fitted domain",
lower - h
) }.into());
}
if h > upper + domain_tol {
return Err(TransformationNormalError::OutsideCertifiedDomain { reason: format!(
"transformation-normal PIT: transformed response h={h:.6e} lies above the certified \
support upper endpoint {upper:.6e} by {:.6e} (> boundary-roundoff floor \
{domain_tol:.3e}); positivity is certified only on the training support \
[{lower:.6e}, {upper:.6e}], so this response/covariate is outside the fitted domain",
h - upper
) }.into());
}
let h_inside = h.clamp(lower, upper);
let u = if h_inside <= lower {
0.0
} else if h_inside >= upper {
1.0
} else {
let log_num = log_normal_cdf_diff(h_inside, lower)?;
let log_den = log_normal_cdf_diff(upper, lower)?;
let ratio = (log_num - log_den).exp();
if !(ratio.is_finite() && (-1.0e-12..=1.0 + 1.0e-12).contains(&ratio)) {
return Err(TransformationNormalError::NumericalFailure { reason: format!(
"transformation-normal PIT probability is not representable: h={h:.6e}, lower={lower:.6e}, upper={upper:.6e}, ratio={ratio}"
) }.into());
}
ratio.clamp(0.0, 1.0)
};
standard_normal_quantile(u.clamp(clip_eps, 1.0 - clip_eps))
.map_err(|err| format!("transformation-normal PIT quantile failed: {err}"))
}
pub(crate) fn scop_second_order_h(
rv: ArrayView1<'_, f64>,
rd: ArrayView1<'_, f64>,
p_resp: usize,
alpha_i: &[f64],
alpha_j: &[f64],
alpha_ij: &[f64],
) -> [f64; 6] {
let mut h_i = 0.0;
let mut h_j = 0.0;
let mut h_ij = 0.0;
let mut hp_i = 0.0;
let mut hp_j = 0.0;
let mut hp_ij = 0.0;
for k in 0..p_resp {
h_i += rv[k] * alpha_i[k];
h_j += rv[k] * alpha_j[k];
h_ij += rv[k] * alpha_ij[k];
hp_i += rd[k] * alpha_i[k];
hp_j += rd[k] * alpha_j[k];
hp_ij += rd[k] * alpha_ij[k];
}
[h_i, h_j, h_ij, hp_i, hp_j, hp_ij]
}
pub(crate) fn scop_second_order_endpoints(
endpoint_basis: [&[f64]; 2],
p_resp: usize,
alpha_i: &[f64],
alpha_j: &[f64],
alpha_ij: &[f64],
) -> ([f64; 2], [f64; 2], [f64; 2]) {
let mut endpoint_i = [0.0; 2];
let mut endpoint_j = [0.0; 2];
let mut endpoint_ij = [0.0; 2];
for e in 0..2 {
let basis = endpoint_basis[e];
for k in 0..p_resp {
endpoint_i[e] += basis[k] * alpha_i[k];
endpoint_j[e] += basis[k] * alpha_j[k];
endpoint_ij[e] += basis[k] * alpha_ij[k];
}
}
(endpoint_i, endpoint_j, endpoint_ij)
}
pub(crate) fn scop_psi_marginal(
rv: ArrayView1<'_, f64>,
rd: ArrayView1<'_, f64>,
p_resp: usize,
endpoint_basis: [&[f64]; 2],
alpha_psi: &[f64],
) -> (f64, f64, [f64; 2]) {
let mut h_psi = 0.0;
let mut hp_psi = 0.0;
for k in 0..p_resp {
h_psi += rv[k] * alpha_psi[k];
hp_psi += rd[k] * alpha_psi[k];
}
let mut endpoint_psi = [0.0; 2];
for e in 0..2 {
let basis = endpoint_basis[e];
for k in 0..p_resp {
endpoint_psi[e] += basis[k] * alpha_psi[k];
}
}
(h_psi, hp_psi, endpoint_psi)
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
const EPS: f64 = 1.0e-12;
#[test]
fn pit_rejects_clip_eps_outside_open_half_interval() {
for bad in [0.0, -1.0e-3, 0.5, 0.6, f64::NAN, f64::INFINITY] {
assert!(transformation_normal_pit_score(0.0, -1.0, 1.0, bad).is_err());
}
assert!(transformation_normal_pit_score(0.0, -1.0, 1.0, 0.25).is_ok());
}
#[test]
fn pit_rejects_nonfinite_h_lower_upper() {
assert!(transformation_normal_pit_score(f64::NAN, -1.0, 1.0, EPS).is_err());
assert!(transformation_normal_pit_score(0.0, f64::NEG_INFINITY, 1.0, EPS).is_err());
assert!(transformation_normal_pit_score(0.0, -1.0, f64::INFINITY, EPS).is_err());
}
#[test]
fn pit_rejects_endpoint_order_violation() {
assert!(transformation_normal_pit_score(0.0, 1.0, 1.0, EPS).is_err());
assert!(transformation_normal_pit_score(0.0, 1.0, 0.5, EPS).is_err());
}
#[test]
fn pit_symmetric_midpoint_maps_to_zero() {
let u = transformation_normal_pit_score(0.0, -2.0, 2.0, EPS).unwrap();
assert!(u.abs() < 1e-9, "expected ~0, got {u}");
}
#[test]
fn pit_at_lower_endpoint_saturates_but_below_domain_refuses() {
let clip = 1e-6;
let expected = standard_normal_quantile(clip).unwrap();
let at = transformation_normal_pit_score(-1.0, -1.0, 1.0, clip).unwrap();
assert!((at - expected).abs() < 1e-12);
assert!(expected < -3.0);
let roundoff = -1.0 - 8.0 * f64::EPSILON;
let at_roundoff = transformation_normal_pit_score(roundoff, -1.0, 1.0, clip).unwrap();
assert!((at_roundoff - expected).abs() < 1e-12);
let err = transformation_normal_pit_score(-1.5, -1.0, 1.0, clip)
.expect_err("h far below lower must refuse, not clamp");
assert!(err.contains("certified"), "message names the domain: {err}");
assert!(err.contains("-1.500000e0"), "message names h: {err}");
assert!(err.contains("outside the fitted domain"), "message: {err}");
}
#[test]
fn pit_at_upper_endpoint_saturates_but_above_domain_refuses() {
let clip = 1e-6;
let expected = standard_normal_quantile(1.0 - clip).unwrap();
let at = transformation_normal_pit_score(1.0, -1.0, 1.0, clip).unwrap();
assert!((at - expected).abs() < 1e-12);
assert!(expected > 3.0);
let roundoff = 1.0 + 8.0 * f64::EPSILON;
let at_roundoff = transformation_normal_pit_score(roundoff, -1.0, 1.0, clip).unwrap();
assert!((at_roundoff - expected).abs() < 1e-12);
let err = transformation_normal_pit_score(2.0, -1.0, 1.0, clip)
.expect_err("h far above upper must refuse, not clamp");
assert!(err.contains("certified"), "message names the domain: {err}");
assert!(err.contains("2.000000e0"), "message names h: {err}");
assert!(err.contains("outside the fitted domain"), "message: {err}");
}
#[test]
fn pit_is_monotone_increasing_in_h() {
let clip = 1e-9;
let a = transformation_normal_pit_score(-0.5, -2.0, 2.0, clip).unwrap();
let b = transformation_normal_pit_score(0.0, -2.0, 2.0, clip).unwrap();
let c = transformation_normal_pit_score(0.5, -2.0, 2.0, clip).unwrap();
assert!(a < b && b < c, "not monotone: {a} {b} {c}");
}
#[test]
fn scop_second_order_h_is_linear_in_the_directional_coordinates() {
let rv = array![3.0];
let rd = array![5.0];
let ai = [2.0];
let aj = [7.0];
let aij = [11.0];
let out = scop_second_order_h(rv.view(), rd.view(), 1, &ai, &aj, &aij);
assert_eq!(
out,
[
3.0 * 2.0,
3.0 * 7.0,
3.0 * 11.0,
5.0 * 2.0,
5.0 * 7.0,
5.0 * 11.0
]
);
}
#[test]
fn scop_second_order_h_p_resp_two_matches_hand_formula() {
let rv = array![1.0, 4.0];
let rd = array![1.0, 6.0];
let ai = [1.0, 3.0];
let aj = [1.0, 5.0];
let aij = [1.0, 7.0];
let out = scop_second_order_h(rv.view(), rd.view(), 2, &ai, &aj, &aij);
assert_eq!(
out,
[
1.0 + 4.0 * 3.0,
1.0 + 4.0 * 5.0,
1.0 + 4.0 * 7.0,
1.0 + 6.0 * 3.0,
1.0 + 6.0 * 5.0,
1.0 + 6.0 * 7.0
]
);
}
#[test]
fn scop_second_order_endpoints_matches_hand_formula() {
let lower = [1.0, 2.0];
let upper = [3.0, 4.0];
let ai = [1.0, 6.0];
let aj = [1.0, 7.0];
let aij = [1.0, 8.0];
let (ei, ej, eij) = scop_second_order_endpoints([&lower, &upper], 2, &ai, &aj, &aij);
assert_eq!(ei[0], 1.0 + 2.0 * 6.0);
assert_eq!(ei[1], 3.0 + 4.0 * 6.0);
assert_eq!(ej[0], 1.0 + 2.0 * 7.0);
assert_eq!(ej[1], 3.0 + 4.0 * 7.0);
assert_eq!(eij[0], 1.0 + 2.0 * 8.0);
assert_eq!(eij[1], 3.0 + 4.0 * 8.0);
}
#[test]
fn scop_psi_marginal_matches_hand_formula() {
let rv = array![1.0, 4.0];
let rd = array![1.0, 6.0];
let lower = [1.0, 2.0];
let upper = [3.0, 4.0];
let alpha_psi = [9.0, 10.0];
let (h_psi, hp_psi, endpoint_psi) =
scop_psi_marginal(rv.view(), rd.view(), 2, [&lower, &upper], &alpha_psi);
assert_eq!(h_psi, 9.0 + 4.0 * 10.0);
assert_eq!(hp_psi, 9.0 + 6.0 * 10.0);
assert_eq!(endpoint_psi[0], 1.0 * 9.0 + 2.0 * 10.0);
assert_eq!(endpoint_psi[1], 3.0 * 9.0 + 4.0 * 10.0);
}
}