use crate::error::{NdimageError, NdimageResult};
fn validate_equal_shape(ch1: &[Vec<f64>], ch2: &[Vec<f64>]) -> NdimageResult<(usize, usize)> {
let rows = ch1.len();
if rows == 0 {
return Err(NdimageError::InvalidInput("Empty channel".into()));
}
let cols = ch1[0].len();
if ch2.len() != rows || ch2.iter().any(|r| r.len() != cols)
|| ch1.iter().any(|r| r.len() != cols)
{
return Err(NdimageError::DimensionError(
"channels must have equal shape".into(),
));
}
Ok((rows, cols))
}
fn flatten_pair(ch1: &[Vec<f64>], ch2: &[Vec<f64>]) -> (Vec<f64>, Vec<f64>) {
let a: Vec<f64> = ch1.iter().flat_map(|r| r.iter().copied()).collect();
let b: Vec<f64> = ch2.iter().flat_map(|r| r.iter().copied()).collect();
(a, b)
}
fn mean(v: &[f64]) -> f64 {
if v.is_empty() { return 0.0; }
v.iter().sum::<f64>() / v.len() as f64
}
pub fn pearson_colocalization(
channel1: &[Vec<f64>],
channel2: &[Vec<f64>],
) -> NdimageResult<f64> {
validate_equal_shape(channel1, channel2)?;
let (a, b) = flatten_pair(channel1, channel2);
Ok(pearson_vectors(&a, &b))
}
fn pearson_vectors(a: &[f64], b: &[f64]) -> f64 {
let n = a.len();
if n == 0 { return 0.0; }
let ma = mean(a);
let mb = mean(b);
let mut cov = 0.0f64;
let mut va = 0.0f64;
let mut vb = 0.0f64;
for i in 0..n {
let da = a[i] - ma;
let db = b[i] - mb;
cov += da * db;
va += da * da;
vb += db * db;
}
let denom = (va * vb).sqrt();
if denom < 1e-300 { return 0.0; }
(cov / denom).clamp(-1.0, 1.0)
}
pub fn manders_coefficients(
channel1: &[Vec<f64>],
channel2: &[Vec<f64>],
threshold1: f64,
threshold2: f64,
) -> NdimageResult<(f64, f64)> {
validate_equal_shape(channel1, channel2)?;
let mut sum1 = 0.0f64;
let mut sum2 = 0.0f64;
let mut m1_num = 0.0f64;
let mut m2_num = 0.0f64;
for (r1, r2) in channel1.iter().zip(channel2.iter()) {
for (&v1, &v2) in r1.iter().zip(r2.iter()) {
sum1 += v1;
sum2 += v2;
if v2 > threshold2 { m1_num += v1; }
if v1 > threshold1 { m2_num += v2; }
}
}
let m1 = if sum1 < 1e-300 { 0.0 } else { (m1_num / sum1).clamp(0.0, 1.0) };
let m2 = if sum2 < 1e-300 { 0.0 } else { (m2_num / sum2).clamp(0.0, 1.0) };
Ok((m1, m2))
}
pub fn li_intensity_correlation(
channel1: &[Vec<f64>],
channel2: &[Vec<f64>],
) -> NdimageResult<f64> {
validate_equal_shape(channel1, channel2)?;
let (a, b) = flatten_pair(channel1, channel2);
let n = a.len();
if n == 0 { return Ok(0.0); }
let ma = mean(&a);
let mb = mean(&b);
let mut pos = 0i64;
let mut neg = 0i64;
for i in 0..n {
let prod = (a[i] - ma) * (b[i] - mb);
if prod > 0.0 { pos += 1; }
else if prod < 0.0 { neg += 1; }
}
Ok((pos - neg) as f64 / n as f64)
}
pub fn costes_threshold(
channel1: &[Vec<f64>],
channel2: &[Vec<f64>],
) -> NdimageResult<(f64, f64)> {
validate_equal_shape(channel1, channel2)?;
let (a, b) = flatten_pair(channel1, channel2);
let n = a.len();
if n == 0 {
return Err(NdimageError::InvalidInput("Empty image".into()));
}
let ma = mean(&a);
let mb = mean(&b);
let mut cov = 0.0f64;
let mut var_a = 0.0f64;
for i in 0..n {
cov += (a[i] - ma) * (b[i] - mb);
var_a += (a[i] - ma).powi(2);
}
if var_a < 1e-300 {
return Err(NdimageError::ComputationError(
"channel1 has zero variance — cannot compute Costes threshold".into(),
));
}
let slope = cov / var_a;
let intercept = mb - slope * ma;
let max_a = a.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let max_b = b.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let n_steps = 256usize;
let mut best_t1 = max_a;
let mut best_t2 = max_b;
for step in 0..=n_steps {
let t2 = max_b * (1.0 - step as f64 / n_steps as f64);
let t1 = slope * t2 + intercept;
let below_a: Vec<f64> = (0..n).filter(|&i| a[i] <= t1 && b[i] <= t2).map(|i| a[i]).collect();
let below_b: Vec<f64> = (0..n).filter(|&i| a[i] <= t1 && b[i] <= t2).map(|i| b[i]).collect();
if below_a.len() < 2 { break; }
let r = pearson_vectors(&below_a, &below_b);
if r <= 0.0 {
best_t1 = t1;
best_t2 = t2;
break;
}
}
Ok((best_t1, best_t2))
}
#[cfg(test)]
mod tests {
use super::*;
fn const_channel(val: f64, rows: usize, cols: usize) -> Vec<Vec<f64>> {
vec![vec![val; cols]; rows]
}
fn make_channel(data: &[&[f64]]) -> Vec<Vec<f64>> {
data.iter().map(|r| r.to_vec()).collect()
}
#[test]
fn test_pearson_identical() {
let ch = make_channel(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
let r = pearson_colocalization(&ch, &ch).expect("pearson failed");
assert!((r - 1.0).abs() < 1e-10, "identical channels → r=1, got {}", r);
}
#[test]
fn test_pearson_anticorrelated() {
let ch1 = make_channel(&[&[1.0, 2.0, 3.0]]);
let ch2 = make_channel(&[&[3.0, 2.0, 1.0]]);
let r = pearson_colocalization(&ch1, &ch2).expect("pearson failed");
assert!((r + 1.0).abs() < 1e-10, "anti-correlated → r=-1, got {}", r);
}
#[test]
fn test_pearson_zero_variance() {
let ch1 = const_channel(1.0, 3, 3);
let ch2 = const_channel(2.0, 3, 3);
let r = pearson_colocalization(&ch1, &ch2).expect("pearson failed");
assert_eq!(r, 0.0);
}
#[test]
fn test_manders_perfect() {
let ch1 = make_channel(&[&[1.0, 0.0], &[0.0, 1.0]]);
let ch2 = make_channel(&[&[1.0, 0.0], &[0.0, 1.0]]);
let (m1, m2) = manders_coefficients(&ch1, &ch2, 0.5, 0.5).expect("manders failed");
assert!((m1 - 1.0).abs() < 1e-10);
assert!((m2 - 1.0).abs() < 1e-10);
}
#[test]
fn test_manders_no_overlap() {
let ch1 = make_channel(&[&[1.0, 0.0]]);
let ch2 = make_channel(&[&[0.0, 1.0]]);
let (m1, m2) = manders_coefficients(&ch1, &ch2, 0.5, 0.5).expect("manders failed");
assert!((m1 - 0.0).abs() < 1e-10);
assert!((m2 - 0.0).abs() < 1e-10);
}
#[test]
fn test_li_icq_colocalized() {
let ch1 = make_channel(&[&[0.0, 1.0], &[1.0, 0.0]]);
let ch2 = make_channel(&[&[0.0, 1.0], &[1.0, 0.0]]);
let icq = li_intensity_correlation(&ch1, &ch2).expect("li icq failed");
assert!(icq > 0.0, "expected positive ICQ, got {}", icq);
}
#[test]
fn test_li_icq_range() {
let ch1 = make_channel(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
let ch2 = make_channel(&[&[6.0, 5.0, 4.0], &[3.0, 2.0, 1.0]]);
let icq = li_intensity_correlation(&ch1, &ch2).expect("li icq failed");
assert!(icq >= -0.5 && icq <= 0.5, "ICQ out of range: {}", icq);
}
#[test]
fn test_costes_threshold_returns_valid_range() {
let mut ch1 = vec![vec![0.0f64; 20]; 20];
let mut ch2 = vec![vec![0.0f64; 20]; 20];
for r in 0..20 {
for c in 0..20 {
ch1[r][c] = (r * 20 + c) as f64;
ch2[r][c] = (r * 20 + c) as f64 + 1.0; }
}
let (t1, t2) = costes_threshold(&ch1, &ch2).expect("costes failed");
assert!(t1 >= 0.0 && t2 >= 0.0, "thresholds must be non-negative: t1={}, t2={}", t1, t2);
}
#[test]
fn test_dimension_mismatch() {
let ch1 = vec![vec![1.0, 2.0]; 3];
let ch2 = vec![vec![1.0, 2.0]; 4];
assert!(pearson_colocalization(&ch1, &ch2).is_err());
}
}