use crate::xasproc::mathutils::index_of;
pub fn peak(energy: &[f64], y: &[f64], lo: f64, hi: f64) -> Option<(f64, f64)> {
extremum(energy, y, lo, hi, true)
}
pub fn valley(energy: &[f64], y: &[f64], lo: f64, hi: f64) -> Option<(f64, f64)> {
extremum(energy, y, lo, hi, false)
}
fn extremum(energy: &[f64], y: &[f64], lo: f64, hi: f64, want_max: bool) -> Option<(f64, f64)> {
let n = energy.len().min(y.len());
if n == 0 {
return None;
}
let slice = &energy[..n];
let (mut a, mut b) = (index_of(slice, lo), index_of(slice, hi));
if a > b {
std::mem::swap(&mut a, &mut b);
}
let mut best = a;
for i in a..=b {
let better = if want_max {
y[i] > y[best]
} else {
y[i] < y[best]
};
if better {
best = i;
}
}
Some((energy[best], y[best]))
}
pub fn x_at_y(energy: &[f64], y: &[f64], target: f64) -> Option<f64> {
let n = energy.len().min(y.len());
for i in 1..n {
let (y0, y1) = (y[i - 1], y[i]);
if (y0 - target) * (y1 - target) <= 0.0 {
let denom = y1 - y0;
if denom.abs() < 1.0e-300 {
return Some(energy[i - 1]);
}
let frac = (target - y0) / denom;
return Some(energy[i - 1] + frac * (energy[i] - energy[i - 1]));
}
}
None
}
pub fn arctan_step(energy: &[f64], amplitude: f64, center: f64, sigma: f64) -> Vec<f64> {
use std::f64::consts::PI;
let denom = sigma.abs().max(1.0e-30);
let sgn = if sigma > 0.0 {
1.0
} else if sigma < 0.0 {
-1.0
} else {
0.0
};
energy
.iter()
.map(|&e| amplitude * (0.5 + (sgn * (e - center) / denom).atan() / PI))
.collect()
}
pub fn centroid(energy: &[f64], weights: &[f64]) -> Option<f64> {
let n = energy.len().min(weights.len());
if n == 0 {
return None;
}
let mut num = 0.0;
let mut den = 0.0;
for i in 0..n {
num += energy[i] * weights[i];
den += weights[i];
}
if den.abs() < 1.0e-300 {
return None;
}
Some(num / den)
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
fn ramp() -> (Vec<f64>, Vec<f64>) {
let e: Vec<f64> = (0..=10).map(|i| i as f64).collect();
let y = vec![0.0, -0.5, -1.0, 0.0, 1.5, 3.0, 4.0, 3.2, 2.0, 1.0, 0.5];
(e, y)
}
#[test]
fn peak_finds_max_in_region_either_bound_order() {
let (e, y) = ramp();
assert_eq!(peak(&e, &y, 3.0, 9.0), Some((6.0, 4.0)));
assert_eq!(peak(&e, &y, 9.0, 3.0), Some((6.0, 4.0)));
assert_eq!(peak(&e, &y, 7.0, 10.0), Some((7.0, 3.2)));
}
#[test]
fn valley_finds_min_in_region() {
let (e, y) = ramp();
assert_eq!(valley(&e, &y, 0.0, 5.0), Some((2.0, -1.0)));
}
#[test]
fn x_at_y_interpolates_rising_crossing() {
let e: Vec<f64> = (0..=5).map(|i| i as f64).collect();
let y: Vec<f64> = e.iter().map(|&x| 2.0 * x).collect();
let x = x_at_y(&e, &y, 5.0).unwrap();
assert!((x - 2.5).abs() < 1e-12, "got {x}");
}
#[test]
fn x_at_y_handles_falling_and_unreached() {
let e: Vec<f64> = (0..=4).map(|i| i as f64).collect();
let y = vec![4.0, 3.0, 2.0, 1.0, 0.0]; let x = x_at_y(&e, &y, 1.5).unwrap();
assert!((x - 2.5).abs() < 1e-12, "got {x}");
assert_eq!(x_at_y(&e, &y, 9.0), None);
}
#[test]
fn arctan_step_matches_lmfit_shape() {
let amp = 2.0;
let center = 10.0;
let sigma = 1.0;
let e = vec![center - sigma, center, center + sigma];
let s = arctan_step(&e, amp, center, sigma);
assert!((s[0] - 0.5).abs() < 1e-12, "lo {}", s[0]);
assert!((s[1] - 1.0).abs() < 1e-12, "mid {}", s[1]);
assert!((s[2] - 1.5).abs() < 1e-12, "hi {}", s[2]);
let far = arctan_step(&[center + 1e6, center - 1e6], amp, center, sigma);
assert!((far[0] - amp).abs() < amp * 1e-6 / PI + 1e-9);
assert!(far[1].abs() < amp * 1e-6 / PI + 1e-9);
}
#[test]
fn centroid_of_symmetric_weights_is_center() {
let e = vec![8.0, 9.0, 10.0, 11.0, 12.0];
let w = vec![0.0, 1.0, 2.0, 1.0, 0.0];
let c = centroid(&e, &w).unwrap();
assert!((c - 10.0).abs() < 1e-12, "got {c}");
assert_eq!(centroid(&e, &[0.0; 5]), None);
}
}