use crate::ValidationError;
pub fn masked_values(
name: &str,
values: &[f64],
mask: &[bool],
) -> Result<Vec<f64>, ValidationError> {
if values.len() != mask.len() {
return Err(ValidationError::MaskValuesLength {
mask: name.into(),
mask_voxels: mask.len(),
values: values.len(),
});
}
let mut selected = Vec::new();
for (index, inside) in mask.iter().enumerate() {
if !inside {
continue;
}
let value = values[index];
if !value.is_finite() || value < 0.0 {
return Err(ValidationError::InvalidMaskedDose {
mask: name.into(),
index,
});
}
selected.push(value);
}
if selected.is_empty() {
return Err(ValidationError::EmptyMask(name.into()));
}
Ok(selected)
}
pub fn mean(selected: &[f64]) -> f64 {
selected.iter().sum::<f64>() / selected.len() as f64
}
pub fn dose_covering_percent(selected: &[f64], percent: f64) -> Result<f64, ValidationError> {
if selected.is_empty() {
return Err(ValidationError::EmptyMask("dx".into()));
}
if !percent.is_finite() || percent <= 0.0 || percent > 100.0 {
return Err(ValidationError::InvalidStatistic {
name: "dx_percent",
reason: format!("coverage percent {percent} must lie in (0, 100]"),
});
}
let mut sorted = selected.to_vec();
sorted.sort_by(f64::total_cmp);
let position = (1.0 - percent / 100.0) * (sorted.len() - 1) as f64;
let lower = position.floor() as usize;
let upper = (lower + 1).min(sorted.len() - 1);
let fraction = position - lower as f64;
Ok(sorted[lower] * (1.0 - fraction) + sorted[upper] * fraction)
}
pub fn volume_at_least(selected: &[f64], level: f64) -> Result<f64, ValidationError> {
if selected.is_empty() {
return Err(ValidationError::EmptyMask("vx".into()));
}
if !level.is_finite() || level < 0.0 {
return Err(ValidationError::InvalidStatistic {
name: "vx_level",
reason: format!("dose level {level} must be finite non-negative"),
});
}
let covered = selected.iter().filter(|value| **value >= level).count();
Ok(covered as f64 / selected.len() as f64)
}
pub fn equivalent_uniform_dose(selected: &[f64], a: f64) -> Result<f64, ValidationError> {
if selected.is_empty() {
return Err(ValidationError::EmptyMask("eud".into()));
}
if !a.is_finite() {
return Err(ValidationError::InvalidStatistic {
name: "eud_parameter",
reason: "organ parameter must be finite".into(),
});
}
if a == 0.0 {
if selected.contains(&0.0) {
return Ok(0.0);
}
let log_mean = selected.iter().map(|value| value.ln()).sum::<f64>() / selected.len() as f64;
return Ok(log_mean.exp());
}
if a < 0.0 && selected.contains(&0.0) {
return Ok(0.0);
}
let scale = if a > 0.0 {
selected.iter().copied().fold(0.0, f64::max)
} else {
selected.iter().copied().fold(f64::INFINITY, f64::min)
};
if scale == 0.0 {
return Ok(0.0);
}
let powered = selected
.iter()
.map(|value| (value / scale).powf(a))
.sum::<f64>()
/ selected.len() as f64;
if !powered.is_finite() || powered < 0.0 {
return Err(ValidationError::InvalidStatistic {
name: "eud_parameter",
reason: "power mean overflowed or is undefined for these doses".into(),
});
}
Ok(scale * powered.powf(1.0 / a))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn masked_values_selects_and_validates() {
assert_eq!(
masked_values("m", &[1.0, -1.0, 3.0], &[true, false, true]).unwrap(),
vec![1.0, 3.0]
);
assert!(masked_values("m", &[1.0], &[true, true]).is_err());
assert!(masked_values("m", &[1.0], &[false]).is_err());
assert!(masked_values("m", &[f64::NAN], &[true]).is_err());
}
#[test]
fn dx_reads_the_discrete_dvh() {
let doses = vec![0.0, 1.0, 2.0, 3.0, 4.0];
assert_eq!(dose_covering_percent(&doses, 100.0).unwrap(), 0.0);
assert_eq!(dose_covering_percent(&doses, 50.0).unwrap(), 2.0);
assert_eq!(dose_covering_percent(&doses, 25.0).unwrap(), 3.0);
assert!((dose_covering_percent(&doses, 90.0).unwrap() - 0.4).abs() < 1e-12);
assert!(dose_covering_percent(&doses, 0.0).is_err());
assert!(dose_covering_percent(&doses, 101.0).is_err());
}
#[test]
fn vx_counts_covered_fraction() {
let doses = vec![0.0, 1.0, 2.0, 3.0];
assert_eq!(volume_at_least(&doses, 0.0).unwrap(), 1.0);
assert_eq!(volume_at_least(&doses, 1.0).unwrap(), 0.75);
assert_eq!(volume_at_least(&doses, 4.0).unwrap(), 0.0);
assert!(volume_at_least(&doses, -1.0).is_err());
}
#[test]
fn eud_covers_the_standard_limits() {
let doses = vec![1.0, 2.0, 3.0, 4.0];
assert_eq!(equivalent_uniform_dose(&doses, 1.0).unwrap(), 2.5);
let serial = equivalent_uniform_dose(&doses, 1000.0).unwrap();
assert!((serial - 4.0).abs() < 0.01);
let parallel = equivalent_uniform_dose(&doses, -1000.0).unwrap();
assert!((parallel - 1.0).abs() < 0.01);
let geo = equivalent_uniform_dose(&doses, 0.0).unwrap();
assert!((geo - (24.0_f64).powf(0.25)).abs() < 1e-12);
let with_zero = vec![0.0, 2.0];
assert_eq!(equivalent_uniform_dose(&with_zero, -5.0).unwrap(), 0.0);
assert_eq!(equivalent_uniform_dose(&with_zero, 0.0).unwrap(), 0.0);
assert!(equivalent_uniform_dose(&with_zero, 2.0).unwrap() > 0.0);
assert!(equivalent_uniform_dose(&doses, f64::INFINITY).is_err());
}
}