use ndarray::{Array1, Array2, Array3};
use crate::compute::error::ComputeError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SurvivalMethod {
Continuous,
Intermittent,
Ssp,
}
impl SurvivalMethod {
pub fn parse(s: &str) -> Result<Self, ComputeError> {
match s.to_ascii_lowercase().as_str() {
"continuous" | "cr" | "rf" => Ok(SurvivalMethod::Continuous),
"intermittent" | "imm" => Ok(SurvivalMethod::Intermittent),
"ssp" => Ok(SurvivalMethod::Ssp),
other => Err(ComputeError::OutOfRange {
field: "method (expected continuous|intermittent|ssp)",
value: other.to_string(),
}),
}
}
}
#[derive(Debug, Clone)]
pub struct PersistResult {
pub lag_times: Array1<f64>,
pub correlation: Array1<f64>,
}
#[inline]
fn mic_dist2(a: [f64; 3], b: [f64; 3], l: [f64; 3]) -> f64 {
let mut s = 0.0;
for d in 0..3 {
let mut dx = b[d] - a[d];
if l[d] > 0.0 {
dx -= (dx / l[d]).round() * l[d];
}
s += dx * dx;
}
s
}
#[allow(clippy::too_many_arguments)]
pub fn pair_survival_tcf(
coords_i: &Array3<f64>,
coords_j: &Array3<f64>,
box_lengths: &Array2<f64>,
r0: f64,
r1: f64,
method: SurvivalMethod,
dt: f64,
max_correlation_time: usize,
exclude_self: bool,
) -> Result<PersistResult, ComputeError> {
let si = coords_i.shape();
let sj = coords_j.shape();
if si[2] != 3 {
return Err(ComputeError::DimensionMismatch {
expected: 3,
got: si[2],
what: "coords_i (expected (n_frames, n_i, 3))",
});
}
if sj[2] != 3 {
return Err(ComputeError::DimensionMismatch {
expected: 3,
got: sj[2],
what: "coords_j (expected (n_frames, n_j, 3))",
});
}
let n_frames = si[0];
if sj[0] != n_frames {
return Err(ComputeError::DimensionMismatch {
expected: n_frames,
got: sj[0],
what: "coords_i / coords_j frame count",
});
}
let bl = box_lengths.shape();
if bl[0] != n_frames || bl[1] != 3 {
return Err(ComputeError::DimensionMismatch {
expected: n_frames,
got: bl[0],
what: "box_lengths (expected (n_frames, 3))",
});
}
let n_i = si[1];
let n_j = sj[1];
if n_frames < 2 || n_i == 0 || n_j == 0 {
return Err(ComputeError::EmptyInput);
}
if r0 <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "r0",
value: r0.to_string(),
});
}
if r1 < r0 {
return Err(ComputeError::OutOfRange {
field: "r1 (require r1 >= r0)",
value: r1.to_string(),
});
}
if dt <= 0.0 {
return Err(ComputeError::OutOfRange {
field: "dt",
value: dt.to_string(),
});
}
let same_species = exclude_self && n_i == n_j;
let max_lag = max_correlation_time.min(n_frames - 1);
let r0_2 = r0 * r0;
let r1_2 = r1 * r1;
let mut acc = vec![0.0_f64; max_lag + 1];
let pos = |coords: &Array3<f64>, t: usize, a: usize| -> [f64; 3] {
[coords[[t, a, 0]], coords[[t, a, 1]], coords[[t, a, 2]]]
};
for t0 in 0..n_frames {
let lmax = max_lag.min(n_frames - 1 - t0);
let l0 = [
box_lengths[[t0, 0]],
box_lengths[[t0, 1]],
box_lengths[[t0, 2]],
];
for i in 0..n_i {
let pi0 = pos(coords_i, t0, i);
for j in 0..n_j {
if same_species && i == j {
continue;
}
if mic_dist2(pi0, pos(coords_j, t0, j), l0) > r0_2 {
continue;
}
acc[0] += 1.0; match method {
SurvivalMethod::Continuous | SurvivalMethod::Ssp => {
#[allow(clippy::needless_range_loop)]
for tau in 1..=lmax {
let t = t0 + tau;
let lt = [
box_lengths[[t, 0]],
box_lengths[[t, 1]],
box_lengths[[t, 2]],
];
let d2 = mic_dist2(pos(coords_i, t, i), pos(coords_j, t, j), lt);
if d2 <= r1_2 {
acc[tau] += 1.0;
} else {
break;
}
}
}
SurvivalMethod::Intermittent => {
#[allow(clippy::needless_range_loop)]
for tau in 1..=lmax {
let t = t0 + tau;
let lt = [
box_lengths[[t, 0]],
box_lengths[[t, 1]],
box_lengths[[t, 2]],
];
let d2 = mic_dist2(pos(coords_i, t, i), pos(coords_j, t, j), lt);
if d2 <= r1_2 {
acc[tau] += 1.0;
}
}
}
}
}
}
}
let mut correlation = Array1::<f64>::zeros(max_lag + 1);
for tau in 0..=max_lag {
let n_origins = (n_frames - tau) as f64;
correlation[tau] = acc[tau] / (n_origins * n_i as f64);
}
let lag_times = Array1::from_iter((0..=max_lag).map(|i| i as f64 * dt));
Ok(PersistResult {
lag_times,
correlation,
})
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::{Array2, Array3};
fn coords(frames: &[Vec<[f64; 3]>]) -> Array3<f64> {
let n_frames = frames.len();
let n = frames[0].len();
let mut a = Array3::<f64>::zeros((n_frames, n, 3));
for (t, fr) in frames.iter().enumerate() {
for (k, p) in fr.iter().enumerate() {
for d in 0..3 {
a[[t, k, d]] = p[d];
}
}
}
a
}
fn boxes(n_frames: usize, l: f64) -> Array2<f64> {
let mut b = Array2::<f64>::zeros((n_frames, 3));
for t in 0..n_frames {
for d in 0..3 {
b[[t, d]] = l;
}
}
b
}
#[test]
fn permanently_bonded_pair_survives_fully() {
let ci = coords(&[
vec![[0.0, 0.0, 0.0]],
vec![[0.0, 0.0, 0.0]],
vec![[0.0, 0.0, 0.0]],
vec![[0.0, 0.0, 0.0]],
]);
let cj = coords(&[
vec![[0.5, 0.0, 0.0]],
vec![[0.5, 0.0, 0.0]],
vec![[0.5, 0.0, 0.0]],
vec![[0.5, 0.0, 0.0]],
]);
let b = boxes(4, 100.0);
let r = pair_survival_tcf(
&ci,
&cj,
&b,
1.0,
1.0,
SurvivalMethod::Continuous,
1.0,
3,
false,
)
.unwrap();
for tau in 0..=3 {
assert!(
(r.correlation[tau] - 1.0).abs() < 1e-12,
"C({tau})={}",
r.correlation[tau]
);
}
}
#[test]
fn continuous_breaks_permanently_intermittent_reforms() {
let ci = coords(&[
vec![[0.0, 0.0, 0.0]],
vec![[0.0, 0.0, 0.0]],
vec![[0.0, 0.0, 0.0]],
vec![[0.0, 0.0, 0.0]],
]);
let cj = coords(&[
vec![[0.5, 0.0, 0.0]],
vec![[0.5, 0.0, 0.0]],
vec![[5.0, 0.0, 0.0]], vec![[0.5, 0.0, 0.0]], ]);
let b = boxes(4, 100.0);
let cont = pair_survival_tcf(
&ci,
&cj,
&b,
1.0,
1.0,
SurvivalMethod::Continuous,
1.0,
3,
false,
)
.unwrap();
let imm = pair_survival_tcf(
&ci,
&cj,
&b,
1.0,
1.0,
SurvivalMethod::Intermittent,
1.0,
3,
false,
)
.unwrap();
assert!(
cont.correlation[3].abs() < 1e-12,
"continuous C(3)={}",
cont.correlation[3]
);
assert!(
(imm.correlation[3] - 1.0).abs() < 1e-12,
"intermittent C(3)={}",
imm.correlation[3]
);
}
#[test]
fn ssp_uses_inner_birth_outer_survival_cutoff() {
let ci = coords(&[vec![[0.0, 0.0, 0.0]], vec![[0.0, 0.0, 0.0]]]);
let cj = coords(&[vec![[0.5, 0.0, 0.0]], vec![[1.5, 0.0, 0.0]]]);
let b = boxes(2, 100.0);
let ssp =
pair_survival_tcf(&ci, &cj, &b, 1.0, 2.0, SurvivalMethod::Ssp, 1.0, 1, false).unwrap();
assert!(
(ssp.correlation[0] - 0.5).abs() < 1e-12,
"C(0)={}",
ssp.correlation[0]
);
assert!((ssp.correlation[1] - 1.0).abs() < 1e-12); let cont = pair_survival_tcf(
&ci,
&cj,
&b,
1.0,
1.0,
SurvivalMethod::Continuous,
1.0,
1,
false,
)
.unwrap();
assert!(cont.correlation[1].abs() < 1e-12);
}
#[test]
fn coordination_number_at_lag_zero() {
let ci = coords(&[vec![[0.0, 0.0, 0.0]], vec![[0.0, 0.0, 0.0]]]);
let cj = coords(&[
vec![[0.5, 0.0, 0.0], [0.0, 0.5, 0.0]],
vec![[0.5, 0.0, 0.0], [0.0, 0.5, 0.0]],
]);
let b = boxes(2, 100.0);
let r = pair_survival_tcf(
&ci,
&cj,
&b,
1.0,
1.0,
SurvivalMethod::Intermittent,
1.0,
1,
false,
)
.unwrap();
assert!(
(r.correlation[0] - 2.0).abs() < 1e-12,
"C(0)={}",
r.correlation[0]
);
}
#[test]
fn minimum_image_wraps_across_boundary() {
let ci = coords(&[vec![[0.2, 0.0, 0.0]], vec![[0.2, 0.0, 0.0]]]);
let cj = coords(&[vec![[9.8, 0.0, 0.0]], vec![[9.8, 0.0, 0.0]]]);
let b = boxes(2, 10.0);
let r = pair_survival_tcf(
&ci,
&cj,
&b,
1.0,
1.0,
SurvivalMethod::Continuous,
1.0,
1,
false,
)
.unwrap();
assert!((r.correlation[0] - 1.0).abs() < 1e-12); }
#[test]
fn exclude_self_drops_diagonal() {
let c = coords(&[
vec![[0.0, 0.0, 0.0], [0.5, 0.0, 0.0]],
vec![[0.0, 0.0, 0.0], [0.5, 0.0, 0.0]],
]);
let b = boxes(2, 100.0);
let r = pair_survival_tcf(
&c,
&c,
&b,
1.0,
1.0,
SurvivalMethod::Intermittent,
1.0,
1,
true,
)
.unwrap();
assert!(
(r.correlation[0] - 1.0).abs() < 1e-12,
"C(0)={}",
r.correlation[0]
);
}
#[test]
fn rejects_bad_inputs() {
let c = coords(&[vec![[0.0, 0.0, 0.0]], vec![[0.0, 0.0, 0.0]]]);
let b = boxes(2, 10.0);
assert!(
pair_survival_tcf(&c, &c, &b, 0.0, 1.0, SurvivalMethod::Ssp, 1.0, 1, false).is_err()
);
assert!(
pair_survival_tcf(&c, &c, &b, 2.0, 1.0, SurvivalMethod::Ssp, 1.0, 1, false).is_err()
);
assert!(SurvivalMethod::parse("bogus").is_err());
assert_eq!(SurvivalMethod::parse("SSP").unwrap(), SurvivalMethod::Ssp);
}
}