use crate::Data;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SparsePKResult {
pub auc: f64,
pub auc_se: f64,
pub auc_ci_lower: f64,
pub auc_ci_upper: f64,
pub n_timepoints: usize,
pub mean_concentrations: Vec<f64>,
pub n_per_timepoint: Vec<usize>,
pub times: Vec<f64>,
}
pub fn sparse_auc(
times: &[f64],
concentrations: &[f64],
time_tolerance: Option<f64>,
) -> Option<SparsePKResult> {
if times.is_empty() || times.len() != concentrations.len() {
return None;
}
let tol = time_tolerance.unwrap_or(0.0);
let mut time_groups: Vec<(f64, Vec<f64>)> = Vec::new();
let mut indices: Vec<usize> = (0..times.len()).collect();
indices.sort_by(|&a, &b| times[a].partial_cmp(×[b]).unwrap());
for &idx in &indices {
let t = times[idx];
let c = concentrations[idx];
let matched = time_groups
.iter_mut()
.find(|(gt, _)| (t - *gt).abs() <= tol);
if let Some((_, group)) = matched {
group.push(c);
} else {
time_groups.push((t, vec![c]));
}
}
time_groups.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
if time_groups.len() < 2 {
return None;
}
let n_timepoints = time_groups.len();
let group_times: Vec<f64> = time_groups.iter().map(|(t, _)| *t).collect();
let n_per_timepoint: Vec<usize> = time_groups.iter().map(|(_, g)| g.len()).collect();
let mean_concentrations: Vec<f64> = time_groups
.iter()
.map(|(_, group)| {
let n = group.len() as f64;
group.iter().sum::<f64>() / n
})
.collect();
let variances: Vec<f64> = time_groups
.iter()
.map(|(_, group)| {
let n = group.len() as f64;
if n < 2.0 {
return 0.0; }
let mean = group.iter().sum::<f64>() / n;
group.iter().map(|c| (c - mean).powi(2)).sum::<f64>() / (n - 1.0)
})
.collect();
let mut auc = 0.0;
for i in 0..n_timepoints - 1 {
let dt = group_times[i + 1] - group_times[i];
auc += (mean_concentrations[i] + mean_concentrations[i + 1]) * dt / 2.0;
}
let mut weights = vec![0.0; n_timepoints];
for i in 0..n_timepoints - 1 {
let dt = group_times[i + 1] - group_times[i];
weights[i] += dt / 2.0;
weights[i + 1] += dt / 2.0;
}
let auc_variance: f64 = (0..n_timepoints)
.map(|j| {
let n_j = n_per_timepoint[j] as f64;
if n_j > 0.0 {
weights[j].powi(2) * variances[j] / n_j
} else {
0.0
}
})
.sum();
let auc_se = auc_variance.sqrt();
let z = 1.96;
let auc_ci_lower = auc - z * auc_se;
let auc_ci_upper = auc + z * auc_se;
Some(SparsePKResult {
auc,
auc_se,
auc_ci_lower,
auc_ci_upper,
n_timepoints,
mean_concentrations,
n_per_timepoint,
times: group_times,
})
}
pub fn sparse_auc_from_data(
data: &Data,
outeq: usize,
time_tolerance: Option<f64>,
) -> Option<SparsePKResult> {
let (mut all_times, mut all_concs) = (Vec::new(), Vec::new());
for subject in data.subjects() {
for occasion in subject.occasions() {
let (times, concs, _censoring) = occasion.get_observations(outeq);
all_times.extend(times);
all_concs.extend(concs);
}
}
sparse_auc(&all_times, &all_concs, time_tolerance)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sparse_auc_basic() {
let times = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 4.0, 4.0, 4.0, 8.0, 8.0, 8.0];
let concs = vec![
0.0, 0.0, 0.0, 10.0, 12.0, 11.0, 5.0, 4.0, 6.0, 1.0, 1.5, 1.2,
];
let result = sparse_auc(×, &concs, None).unwrap();
assert_eq!(result.n_timepoints, 4);
assert!(result.auc > 0.0);
assert!(result.auc_se >= 0.0);
assert!(result.auc_ci_lower <= result.auc);
assert!(result.auc_ci_upper >= result.auc);
assert!((result.mean_concentrations[0] - 0.0).abs() < 1e-10);
assert!((result.mean_concentrations[1] - 11.0).abs() < 1e-10);
assert!((result.mean_concentrations[2] - 5.0).abs() < 1e-10);
}
#[test]
fn test_sparse_auc_single_timepoint() {
let times = vec![0.0, 0.0];
let concs = vec![10.0, 12.0];
assert!(sparse_auc(×, &concs, None).is_none());
}
#[test]
fn test_sparse_auc_with_tolerance() {
let times = vec![0.0, 0.01, 1.0, 0.99];
let concs = vec![0.0, 0.0, 10.0, 12.0];
let result = sparse_auc(×, &concs, Some(0.05)).unwrap();
assert_eq!(result.n_timepoints, 2); }
#[test]
fn test_sparse_auc_empty() {
assert!(sparse_auc(&[], &[], None).is_none());
}
#[test]
fn test_sparse_auc_known_values() {
let times = vec![0.0, 0.0, 2.0, 2.0];
let concs = vec![10.0, 10.0, 5.0, 5.0];
let result = sparse_auc(×, &concs, None).unwrap();
assert!((result.auc - 15.0).abs() < 1e-10);
assert!((result.auc_se - 0.0).abs() < 1e-10);
}
}