use ndarray::Array1;
use crate::compute::dielectric::K_B;
#[derive(Debug, Clone)]
pub struct KramersKronigCheck {
pub recovered: Array1<f64>,
pub mae: f64,
pub passed: bool,
}
fn discrete_kramers_kronig(
omega: &Array1<f64>,
eps_imag: &Array1<f64>,
eps_inf: f64,
) -> Array1<f64> {
let n = omega.len();
let mut recovered = Array1::<f64>::from_elem(n, eps_inf);
for i in 0..n {
let omega_i = omega[i];
let mut acc = 0.0;
for j in 0..n {
if j == i {
continue;
}
let omega_j = omega[j];
let denom = omega_j * omega_j - omega_i * omega_i;
if denom.abs() < 1e-30 {
continue;
}
let dw = if j == 0 {
omega[1] - omega[0]
} else if j == n - 1 {
omega[n - 1] - omega[n - 2]
} else {
0.5 * (omega[j + 1] - omega[j - 1])
};
acc += eps_imag[j] * omega_j / denom * dw;
}
recovered[i] += (2.0 / std::f64::consts::PI) * acc;
}
recovered
}
pub fn kramers_kronig_check(
omega: &Array1<f64>,
eps_real: &Array1<f64>,
eps_imag: &Array1<f64>,
eps_inf: f64,
) -> Result<KramersKronigCheck, String> {
require_same_len("frequency", omega.len(), "eps_real", eps_real.len())?;
require_same_len("frequency", omega.len(), "eps_imag", eps_imag.len())?;
if omega.len() < 3 {
return Err("kramers_kronig_check requires at least 3 frequency points".into());
}
let recovered = discrete_kramers_kronig(omega, eps_imag, eps_inf);
let sum_abs: f64 = recovered
.iter()
.zip(eps_real.iter())
.map(|(r, e)| (r - e).abs())
.sum();
let mae = sum_abs / (eps_real.len() as f64);
let dynamic_range = eps_real.iter().copied().fold(f64::NEG_INFINITY, f64::max)
- eps_real.iter().copied().fold(f64::INFINITY, f64::min);
let tol = (dynamic_range.abs() * 0.1).max(1e-2);
Ok(KramersKronigCheck {
recovered,
mae,
passed: mae < tol,
})
}
#[derive(Debug, Clone, Copy)]
pub struct SumRuleCheck {
pub integral: f64,
pub expected: f64,
pub relative_error: f64,
pub passed: bool,
}
pub fn conductivity_sum_rule_check(
omega: &Array1<f64>,
sigma: &Array1<f64>,
current_sq_mean: f64,
volume: f64,
temperature: f64,
) -> Result<SumRuleCheck, String> {
require_same_len("frequency", omega.len(), "conductivity", sigma.len())?;
require_positive("volume", volume)?;
require_positive("temperature", temperature)?;
if !current_sq_mean.is_finite() || current_sq_mean < 0.0 {
return Err("current_sq_mean must be finite and non-negative".into());
}
if omega.len() < 2 {
return Err("conductivity_sum_rule_check requires at least 2 frequency points".into());
}
let mut integral = 0.0;
for i in 1..omega.len() {
let dw = omega[i] - omega[i - 1];
integral += 0.5 * (sigma[i] + sigma[i - 1]) * dw;
}
let expected =
std::f64::consts::PI * 0.5 * current_sq_mean / (3.0 * volume * K_B * temperature);
let denom = expected.abs().max(1e-30);
let relative_error = (integral - expected) / denom;
Ok(SumRuleCheck {
integral,
expected,
relative_error,
passed: relative_error.abs() < 0.05,
})
}
#[derive(Debug, Clone)]
pub struct RouteAgreementCheck {
pub pairwise: Vec<(String, f64)>,
pub max_rms: f64,
pub passed: bool,
}
pub fn route_agreement_check(
entries: &[(String, Array1<f64>)],
) -> Result<RouteAgreementCheck, String> {
if entries.len() < 2 {
return Err("route_agreement_check needs at least two named result arrays".into());
}
let expected_len = entries[0].1.len();
for (name, arr) in entries {
if arr.len() != expected_len {
return Err(format!(
"route_agreement_check: '{name}' has length {} != expected {expected_len}",
arr.len()
));
}
}
let mut pairwise = Vec::new();
let mut max_rms = 0.0_f64;
for i in 0..entries.len() {
for j in (i + 1)..entries.len() {
let (name_i, arr_i) = &entries[i];
let (name_j, arr_j) = &entries[j];
let mut sum_sq = 0.0;
let mut norm = 0.0;
for k in 0..expected_len {
let diff = arr_i[k] - arr_j[k];
sum_sq += diff * diff;
norm += 0.5 * (arr_i[k].abs() + arr_j[k].abs());
}
let rms_abs = (sum_sq / (expected_len as f64)).sqrt();
let scale = (norm / (expected_len as f64)).max(1e-30);
let rms_rel = rms_abs / scale;
max_rms = max_rms.max(rms_rel);
pairwise.push((format!("{name_i}_vs_{name_j}"), rms_rel));
}
}
Ok(RouteAgreementCheck {
pairwise,
max_rms,
passed: max_rms < 0.10,
})
}
fn require_same_len(name_a: &str, a: usize, name_b: &str, b: usize) -> Result<(), String> {
if a != b {
return Err(format!("length mismatch: {name_a}={a}, {name_b}={b}"));
}
Ok(())
}
fn require_positive(name: &str, value: f64) -> Result<(), String> {
if !(value.is_finite() && value > 0.0) {
return Err(format!("{name} must be positive and finite, got {value}"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kk_recovers_eps_inf_for_zero_loss() {
let omega = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let eps_imag = Array1::zeros(4);
let eps_real = Array1::from_elem(4, 2.5);
let out = kramers_kronig_check(&omega, &eps_real, &eps_imag, 2.5).unwrap();
assert!(out.mae < 1e-12, "mae {}", out.mae);
assert!(out.passed);
}
#[test]
fn kk_requires_three_points() {
let omega = Array1::from_vec(vec![1.0, 2.0]);
let z = Array1::zeros(2);
assert!(kramers_kronig_check(&omega, &z, &z, 1.0).is_err());
}
#[test]
fn sum_rule_matches_constructed_integral() {
let omega = Array1::from_vec(vec![0.0, 2.0, 4.0]);
let sigma = Array1::from_elem(3, 2.0);
let volume = 100.0;
let temperature = 300.0;
let expected = 8.0;
let current_sq_mean =
expected * 3.0 * volume * K_B * temperature / (std::f64::consts::PI * 0.5);
let out = conductivity_sum_rule_check(&omega, &sigma, current_sq_mean, volume, temperature)
.unwrap();
assert!((out.integral - 8.0).abs() < 1e-12);
assert!(out.relative_error.abs() < 1e-12, "{}", out.relative_error);
assert!(out.passed);
}
#[test]
fn sum_rule_rejects_nonpositive_volume() {
let omega = Array1::from_vec(vec![0.0, 1.0]);
let sigma = Array1::from_elem(2, 1.0);
assert!(conductivity_sum_rule_check(&omega, &sigma, 1.0, 0.0, 300.0).is_err());
}
#[test]
fn route_agreement_identical_is_zero() {
let a = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let entries = vec![("r1".to_string(), a.clone()), ("r2".to_string(), a)];
let out = route_agreement_check(&entries).unwrap();
assert_eq!(out.pairwise.len(), 1);
assert_eq!(out.pairwise[0].0, "r1_vs_r2");
assert!(out.max_rms < 1e-12);
assert!(out.passed);
}
#[test]
fn route_agreement_flags_divergence() {
let a = Array1::from_vec(vec![1.0, 1.0, 1.0]);
let b = Array1::from_vec(vec![2.0, 2.0, 2.0]);
let entries = vec![("a".to_string(), a), ("b".to_string(), b)];
let out = route_agreement_check(&entries).unwrap();
assert!(out.max_rms > 0.1);
assert!(!out.passed);
}
}