use super::*;
use crate::hills::smooth;
fn make_hill_with_gaps() -> ActiveHill {
let mut h = ActiveHill::new(524.270, 100.0, 0.0, 0.9, 0, true);
h.add_gap();
h.add_gap();
h.mz_profile.push(524.275);
h.intensity_profile.push(150.0);
h.rt_profile.push(0.3);
h.im_profile.push(0.91);
h.n_real += 1;
h.last_scan_seen = 3;
h.add_gap();
h.mz_profile.push(524.272);
h.intensity_profile.push(120.0);
h.rt_profile.push(0.5);
h.im_profile.push(0.905);
h.n_real += 1;
h.last_scan_seen = 5;
h
}
#[test]
fn mz_kish_se_zero_for_singleton() {
let h = ActiveHill::new(500.0, 1000.0, 0.0, 0.0, 0, true);
assert_eq!(h.mz_kish_se(), 0.0);
}
#[test]
fn mz_kish_se_uniform_intensity_matches_classic_se() {
let mut h = ActiveHill::new(499.99, 1000.0, 0.0, 0.0, 0, true);
for (i, mz) in [499.995f32, 500.0, 500.005, 500.01].iter().enumerate() {
h.mz_profile.push(*mz);
h.intensity_profile.push(1000.0);
h.rt_profile.push((i + 1) as f32 * 0.1);
h.im_profile.push(0.0);
h.n_real += 1;
h.last_scan_seen = i + 1;
}
let std = h.mz_weighted_std();
let se = h.mz_kish_se();
let expected_se = std / (5.0_f64).sqrt();
assert!(
(se - expected_se).abs() < 1e-6,
"SE {se} not within 1e-6 of weighted_std/√n = {expected_se}",
);
}
#[test]
fn mz_kish_se_skewed_intensity_inflates_se() {
let mut h = ActiveHill::new(500.000, 100_000.0, 0.0, 0.0, 0, true);
for (i, mz) in [500.005f32, 500.010, 500.015, 500.020].iter().enumerate() {
h.mz_profile.push(*mz);
h.intensity_profile.push(1000.0);
h.rt_profile.push((i + 1) as f32 * 0.1);
h.im_profile.push(0.0);
h.n_real += 1;
h.last_scan_seen = i + 1;
}
let std = h.mz_weighted_std();
let se = h.mz_kish_se();
assert!(
se > std * 0.5,
"skewed-intensity SE {se} should be close to weighted_std {std} (neff ≈ 1)",
);
assert!(
se >= std / (5.0_f64).sqrt(),
"skewed-intensity SE {se} should exceed the unweighted √n estimate {}",
std / (5.0_f64).sqrt(),
);
}
#[test]
fn mz_kish_se_ignores_gaps() {
let h = make_hill_with_gaps();
let se = h.mz_kish_se();
assert!(se > 0.0 && se.is_finite(), "expected non-zero SE, got {se}");
assert!(se < 0.005, "SE {se} seems implausibly large");
}
#[test]
fn mz_std_without_smoothing_is_small() {
let h = make_hill_with_gaps();
let mean = h.mz_weighted_mean();
let std = h.mz_weighted_std();
assert!((mean - 524.273).abs() < 1e-3, "mean was {mean}");
assert!(std < 0.01, "expected tight m/z std (<0.01 Da), got {std}");
}
#[test]
fn mz_std_unaffected_by_smoothing_gap_fill() {
let mut h = make_hill_with_gaps();
smooth::apply_intensity_filters(&mut h.intensity_profile, true, true, 1);
let mean = h.mz_weighted_mean();
let std = h.mz_weighted_std();
assert!(
(mean - 524.273).abs() < 0.01,
"smoothing should not shift mean far from m/z cluster, got {mean}",
);
assert!(
std < 0.01,
"smoothing should not inflate mz_std (gaps must not contribute); got {std}",
);
}
#[test]
fn apex_rt_interpolates_when_apex_lands_on_gap() {
let mut h = make_hill_with_gaps();
h.intensity_profile[1] = 0.0; h.intensity_profile[3] = 10_000.0; smooth::apply_intensity_filters(&mut h.intensity_profile, true, true, 2);
let rt = h.apex_rt();
assert!(
rt > 0.0 && rt.is_finite(),
"apex_rt should not collapse to 0 when apex lands on a NaN slot; got {rt}",
);
let lo = h.min_rt();
let hi = h.max_rt();
assert!(
rt >= lo && rt <= hi,
"interpolated apex_rt {rt} must lie within [{lo}, {hi}]",
);
}
#[test]
fn im_std_unaffected_by_smoothing_gap_fill() {
let mut h = make_hill_with_gaps();
smooth::apply_intensity_filters(&mut h.intensity_profile, true, true, 1);
let mean = h.im_weighted_mean();
let std = h.im_weighted_std();
assert!((mean - 0.905).abs() < 0.01, "im mean drifted: {mean}");
assert!(std < 0.01, "im_std inflated by gap-fill: {std}");
}