pub fn fill_gaps(profile: &mut [f32]) {
let n = profile.len();
let mut i = 0;
while i < n {
if profile[i] == 0.0 {
let gap_start = i;
while i < n && profile[i] == 0.0 {
i += 1;
}
let gap_end = i;
let left_val = if gap_start > 0 {
profile[gap_start - 1]
} else {
0.0
};
let right_val = if gap_end < n { profile[gap_end] } else { 0.0 };
let span = (gap_end - gap_start + 1) as f32;
for (offset, slot) in profile[gap_start..gap_end].iter_mut().enumerate() {
let t = (offset + 1) as f32 / span;
*slot = left_val + (right_val - left_val) * t;
}
} else {
i += 1;
}
}
}
pub fn running_average(profile: &mut Vec<f32>, half_window: usize) {
if half_window == 0 || profile.len() < 2 {
return;
}
let n = profile.len();
let mut out = Vec::with_capacity(n);
for i in 0..n {
let lo = i.saturating_sub(half_window);
let hi = (i + half_window + 1).min(n);
let count = (hi - lo) as f32;
let sum: f32 = profile[lo..hi].iter().sum();
out.push(sum / count);
}
*profile = out;
}
pub fn apply_intensity_filters(
profile: &mut Vec<f32>,
gap_fill: bool,
smoothing: bool,
half_window: usize,
) {
if gap_fill {
fill_gaps(profile);
}
if smoothing {
running_average(profile, half_window);
}
}
pub fn compute_hill_score(profile: &[f32]) -> f64 {
let n = profile.len();
if n < 2 {
return 1.0;
}
let apex = profile
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
let mut n_correct = 0usize;
let mut n_total = 0usize;
for i in 0..apex {
n_total += 1;
if profile[i] <= profile[i + 1] {
n_correct += 1;
}
}
for i in apex..n - 1 {
n_total += 1;
if profile[i] >= profile[i + 1] {
n_correct += 1;
}
}
if n_total == 0 {
return 1.0;
}
n_correct as f64 / n_total as f64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fill_single_gap() {
let mut p = vec![100.0f32, 0.0, 200.0];
fill_gaps(&mut p);
assert!((p[1] - 150.0).abs() < 1e-4, "single gap should be midpoint");
}
#[test]
fn fill_multi_gap() {
let mut p = vec![0.0f32, 0.0, 100.0, 0.0, 0.0, 0.0, 200.0, 0.0, 0.0];
fill_gaps(&mut p);
assert!((p[3] - 125.0).abs() < 1e-3);
assert!((p[4] - 150.0).abs() < 1e-3);
assert!((p[5] - 175.0).abs() < 1e-3);
}
#[test]
fn running_average_half1() {
let mut p = vec![0.0f32, 2.0, 4.0, 6.0, 8.0];
running_average(&mut p, 1);
assert!((p[0] - 1.0).abs() < 1e-4);
assert!((p[2] - 4.0).abs() < 1e-4);
assert!((p[4] - 7.0).abs() < 1e-4);
}
#[test]
fn noop_on_zero_window() {
let mut p = vec![1.0f32, 2.0, 3.0];
running_average(&mut p, 0);
assert_eq!(p, vec![1.0, 2.0, 3.0]);
}
#[test]
fn score_perfect_hill() {
let p = vec![10.0f32, 50.0, 100.0, 80.0, 20.0];
assert!((compute_hill_score(&p) - 1.0).abs() < 1e-6);
}
#[test]
fn score_noisy_hill() {
let p = vec![10.0f32, 80.0, 50.0, 100.0, 60.0, 30.0, 40.0, 5.0];
let s = compute_hill_score(&p);
assert!((s - 5.0 / 7.0).abs() < 1e-6, "got {s}");
}
#[test]
fn score_single_scan() {
assert!((compute_hill_score(&[42.0f32]) - 1.0).abs() < 1e-6);
}
}