From dd744a20d54ffbfb40c26848b49ee215809d7e82 Mon Sep 17 00:00:00 2001 From: Markus <66058642+mhovd@users.noreply.github.com> Date: Fri, 16 May 2025 12:11:40 +0200 Subject: [PATCH 1/5] WIP: Needs new pharmsol --- src/lib.rs | 5 ++ src/mmopt/mod.rs | 210 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 src/mmopt/mod.rs diff --git a/src/lib.rs b/src/lib.rs index 1416e5ec1..990262988 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,9 @@ pub mod routines; // Structures pub mod structs; +// MMopt +pub mod mmopt; + // Re-export commonly used items pub use anyhow::Result; pub use std::collections::HashMap; @@ -41,6 +44,8 @@ pub mod prelude { pub use crate::routines::settings::*; pub use crate::structs::*; + pub use crate::mmopt::*; + //Alma re-exports pub mod simulator { pub use pharmsol::prelude::simulator::*; diff --git a/src/mmopt/mod.rs b/src/mmopt/mod.rs new file mode 100644 index 000000000..5a77def9e --- /dev/null +++ b/src/mmopt/mod.rs @@ -0,0 +1,210 @@ +use anyhow::Result; +use faer::Mat; +use pharmsol::{ + prelude::simulator::SubjectPredictions, Data, Equation, ErrorModel, Predictions, Subject, +}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; +use serde_json::error; +use std::fmt::Error; + +use crate::structs::theta::Theta; + +pub struct PredictionsContainer { + pub matrix: Mat, + pub times: Vec, + pub probs: Vec, +} + +impl PredictionsContainer { + fn matrix(&self) -> &Mat { + &self.matrix + } + + fn nsub(&self) -> usize { + self.matrix.ncols() + } + fn nout(&self) -> usize { + self.matrix.nrows() + } +} + +struct CostMatrix { + matrix: Option>, + auc: f64, + cmax: f64, + cmin: f64, +} + +impl CostMatrix { + pub fn new(auc: f64, cmax: f64, cmin: f64) -> Self { + !unimplemented!() + } +} + +/// The results of a multiple-model optimization +/// +/// +#[derive(Debug)] +pub struct MmoptResult { + // Optimal sample times + pub times: Vec, + // Bayes risk + pub risk: f64, +} + +pub fn mmopt( + theta: &Theta, + subject: &Subject, + equation: impl Equation, + errormodel: ErrorModel, + nsamp: usize, +) -> Result { + // Check that subject contains only one Occasion + if subject.occasions().len() != 1 { + return Err(anyhow::anyhow!("Subject must contain only one Occasion")); + } + + // Generate predictions + let predictions = theta + .matrix() + .row_iter() + .map(|theta_row| { + let support_point: Vec = theta_row.iter().cloned().collect(); + let predictions = equation + .estimate_predictions(&subject, &support_point) + .get_predictions(); + predictions + }) + .collect::>(); + + // Times vector + let times = predictions[0].iter().map(|p| p.time()).collect::>(); + + // Generate prediction matrix + let pred_matrix = Mat::from_fn(predictions[0].len(), theta.nspp(), |i, j| { + predictions[j][i].prediction().to_owned() + }); + + // Generate sample candidate indices + let candidate_indices = generate_combinations(times.len(), nsamp); + + // em + let e = errormodel.; + + let (best_combo, min_risk) = candidate_indices + .par_iter() + .map(|combo| { + let mut risk = 0.0; + // Compare the i-th and the j-th subject predictions + for i in 0..theta.nspp() { + for j in 0..theta.nspp() { + if i != j { + let i_obs: Vec = pred_matrix + .col(i) + .iter() + .enumerate() + .filter_map(|(k, &x)| if combo.contains(&k) { Some(x) } else { None }) + .collect(); + + let j_obs: Vec = pred_matrix + .col(j) + .iter() + .enumerate() + .filter_map(|(k, &x)| if combo.contains(&k) { Some(x) } else { None }) + .collect(); + + let i_var: Vec = + i_obs.iter().map(|&x| errormodel.(x)).collect(); + let j_var: Vec = + j_obs.iter().map(|&x| errorpoly.variance(x)).collect(); + + let sum_k_ijn: f64 = i_obs + .iter() + .zip(j_obs.iter()) + .zip(i_var.iter()) + .zip(j_var.iter()) + .map(|(((y_i, y_j), i_var), j_var)| { + let denominator = i_var + j_var; + let term1 = (y_i - y_j).powi(2) / (4.0 * denominator); + let term2 = 0.5 * ((i_var + j_var) / 2.0).ln(); + let term3 = -0.25 * (i_var * j_var).ln(); + term1 + term2 + term3 + }) + .collect::>() + .iter() + .sum::(); + + let prob_i = predictions.probs[i]; + let prob_j = predictions.probs[j]; + let cost = cost_matrix.matrix[(i, j)]; + let risk_component = prob_i * prob_j * (-sum_k_ijn).exp() * cost; + risk += risk_component; + } + } + } + + (combo.clone(), risk) + }) + .min_by(|(_, risk_a), (_, risk_b)| risk_a.partial_cmp(risk_b).unwrap()) + .unwrap(); + + let res = MmoptResult { + best_combo_indices: best_combo.clone(), + best_combo_times: best_combo + .iter() + .map(|&index| predictions.times[index]) + .collect(), + min_risk, + }; + + Ok(res) +} + +fn generate_combinations(m: usize, n: usize) -> Vec> { + fn backtrack( + m: usize, + n: usize, + start: usize, + current: &mut Vec, + results: &mut Vec>, + ) { + if current.len() == n { + results.push(current.clone()); + return; + } + + for i in start..m { + current.push(i); + backtrack(m, n, i + 1, current, results); + current.pop(); + } + } + + let mut results = Vec::new(); + let mut current = Vec::new(); + backtrack(m, n, 0, &mut current, &mut results); + results +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_combinations() { + let m = 5; + let n = 3; + let combinations = generate_combinations(m, n); + assert_eq!(combinations.len(), 10); + assert_eq!(combinations[0], vec![0, 1, 2]); + assert_eq!(combinations[1], vec![0, 1, 3]); + assert_eq!(combinations[2], vec![0, 1, 4]); + assert_eq!(combinations[3], vec![0, 2, 3]); + assert_eq!(combinations[4], vec![0, 2, 4]); + assert_eq!(combinations[5], vec![0, 3, 4]); + assert_eq!(combinations[6], vec![1, 2, 3]); + assert_eq!(combinations[7], vec![1, 2, 4]); + assert_eq!(combinations[8], vec![1, 3, 4]); + assert_eq!(combinations[9], vec![2, 3, 4]); + } +} From a6beba4b0537adee4eb0c988929c41b6cbaebaea Mon Sep 17 00:00:00 2001 From: Markus <66058642+mhovd@users.noreply.github.com> Date: Thu, 22 May 2025 16:13:20 +0200 Subject: [PATCH 2/5] WIP --- src/mmopt/mod.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/mmopt/mod.rs b/src/mmopt/mod.rs index 5a77def9e..2b070479d 100644 --- a/src/mmopt/mod.rs +++ b/src/mmopt/mod.rs @@ -88,9 +88,6 @@ pub fn mmopt( // Generate sample candidate indices let candidate_indices = generate_combinations(times.len(), nsamp); - // em - let e = errormodel.; - let (best_combo, min_risk) = candidate_indices .par_iter() .map(|combo| { @@ -114,7 +111,7 @@ pub fn mmopt( .collect(); let i_var: Vec = - i_obs.iter().map(|&x| errormodel.(x)).collect(); + i_obs.iter().map(|&x| errormodel.variance(x)).collect(); let j_var: Vec = j_obs.iter().map(|&x| errorpoly.variance(x)).collect(); @@ -148,13 +145,10 @@ pub fn mmopt( .min_by(|(_, risk_a), (_, risk_b)| risk_a.partial_cmp(risk_b).unwrap()) .unwrap(); + let times = best_combo.iter().map(|&i| times[i]).collect::>(); let res = MmoptResult { - best_combo_indices: best_combo.clone(), - best_combo_times: best_combo - .iter() - .map(|&index| predictions.times[index]) - .collect(), - min_risk, + times: times, + risk: min_risk, }; Ok(res) From e915e7d113e1d145f4b7fdfb85e9872ded48c4ae Mon Sep 17 00:00:00 2001 From: Markus Date: Sun, 13 Jul 2025 15:46:09 +0200 Subject: [PATCH 3/5] Refactor --- src/mmopt/mod.rs | 134 +++++++++++++++++++---------------------------- 1 file changed, 55 insertions(+), 79 deletions(-) diff --git a/src/mmopt/mod.rs b/src/mmopt/mod.rs index 2b070479d..0b772508a 100644 --- a/src/mmopt/mod.rs +++ b/src/mmopt/mod.rs @@ -1,33 +1,10 @@ -use anyhow::Result; +use anyhow::{Ok, Result}; use faer::Mat; -use pharmsol::{ - prelude::simulator::SubjectPredictions, Data, Equation, ErrorModel, Predictions, Subject, -}; +use pharmsol::{Equation, ErrorModel, Predictions, Subject}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; -use serde_json::error; -use std::fmt::Error; use crate::structs::theta::Theta; -pub struct PredictionsContainer { - pub matrix: Mat, - pub times: Vec, - pub probs: Vec, -} - -impl PredictionsContainer { - fn matrix(&self) -> &Mat { - &self.matrix - } - - fn nsub(&self) -> usize { - self.matrix.ncols() - } - fn nout(&self) -> usize { - self.matrix.nrows() - } -} - struct CostMatrix { matrix: Option>, auc: f64, @@ -52,6 +29,7 @@ pub struct MmoptResult { pub risk: f64, } +/// Perform multiple-model optimization to determine optimal sample times pub fn mmopt( theta: &Theta, subject: &Subject, @@ -72,6 +50,7 @@ pub fn mmopt( let support_point: Vec = theta_row.iter().cloned().collect(); let predictions = equation .estimate_predictions(&subject, &support_point) + .unwrap() .get_predictions(); predictions }) @@ -91,67 +70,64 @@ pub fn mmopt( let (best_combo, min_risk) = candidate_indices .par_iter() .map(|combo| { - let mut risk = 0.0; - // Compare the i-th and the j-th subject predictions - for i in 0..theta.nspp() { - for j in 0..theta.nspp() { - if i != j { - let i_obs: Vec = pred_matrix - .col(i) - .iter() - .enumerate() - .filter_map(|(k, &x)| if combo.contains(&k) { Some(x) } else { None }) - .collect(); - - let j_obs: Vec = pred_matrix - .col(j) - .iter() - .enumerate() - .filter_map(|(k, &x)| if combo.contains(&k) { Some(x) } else { None }) - .collect(); - - let i_var: Vec = - i_obs.iter().map(|&x| errormodel.variance(x)).collect(); - let j_var: Vec = - j_obs.iter().map(|&x| errorpoly.variance(x)).collect(); - - let sum_k_ijn: f64 = i_obs - .iter() - .zip(j_obs.iter()) - .zip(i_var.iter()) - .zip(j_var.iter()) - .map(|(((y_i, y_j), i_var), j_var)| { - let denominator = i_var + j_var; - let term1 = (y_i - y_j).powi(2) / (4.0 * denominator); - let term2 = 0.5 * ((i_var + j_var) / 2.0).ln(); - let term3 = -0.25 * (i_var * j_var).ln(); - term1 + term2 + term3 - }) - .collect::>() - .iter() - .sum::(); - - let prob_i = predictions.probs[i]; - let prob_j = predictions.probs[j]; - let cost = cost_matrix.matrix[(i, j)]; - let risk_component = prob_i * prob_j * (-sum_k_ijn).exp() * cost; - risk += risk_component; - } - } - } - + let risk = calculate_risk(combo, &pred_matrix, theta, &errormodel).unwrap(); (combo.clone(), risk) }) .min_by(|(_, risk_a), (_, risk_b)| risk_a.partial_cmp(risk_b).unwrap()) .unwrap(); - let times = best_combo.iter().map(|&i| times[i]).collect::>(); - let res = MmoptResult { - times: times, + let optimal_times = best_combo.iter().map(|&i| times[i]).collect(); + Ok(MmoptResult { + times: optimal_times, risk: min_risk, - }; + }) +} + +/// Calculate the risk for a specific combination of sample times +fn calculate_risk( + combo: &[usize], + pred_matrix: &Mat, + theta: &Theta, + errormodel: &ErrorModel, +) -> Result { + let nspp = theta.nspp(); + let prob_uniform = 1.0 / nspp as f64; // Uniform probability for each support point + + let risk = (0..nspp) + .flat_map(|i| (0..nspp).map(move |j| (i, j))) + .filter(|(i, j)| i != j) + .map(|(i, j)| { + // Extract observations for the selected time points + let i_obs: Vec = combo.iter().map(|&k| pred_matrix[(k, i)]).collect(); + + let j_obs: Vec = combo.iter().map(|&k| pred_matrix[(k, j)]).collect(); + + // Calculate the sum of log-likelihood differences + let sum_k_ijn: f64 = i_obs + .iter() + .zip(j_obs.iter()) + .map(|(&y_i, &y_j)| { + let i_var = errormodel.variance_from_value(y_i).unwrap(); + let j_var = errormodel.variance_from_value(y_j).unwrap(); + let denominator = i_var + j_var; + + let term1 = (y_i - y_j).powi(2) / (4.0 * denominator); + let term2 = 0.5 * (denominator / 2.0).ln(); + let term3 = -0.25 * (i_var * j_var).ln(); + + term1 + term2 + term3 + }) + .sum(); + + // For now, assume unit cost matrix (cost = 1.0 for all pairs) + // This can be parameterized later if needed + let cost = 1.0; + + prob_uniform * prob_uniform * (-sum_k_ijn).exp() * cost + }) + .sum(); - Ok(res) + Ok(risk) } fn generate_combinations(m: usize, n: usize) -> Vec> { From a4ce2b7fe292e4d2d6079497372fbe10595312c7 Mon Sep 17 00:00:00 2001 From: Markus Date: Sun, 13 Jul 2025 15:48:59 +0200 Subject: [PATCH 4/5] Update mod.rs --- src/mmopt/mod.rs | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/mmopt/mod.rs b/src/mmopt/mod.rs index 0b772508a..1580681cb 100644 --- a/src/mmopt/mod.rs +++ b/src/mmopt/mod.rs @@ -5,19 +5,6 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use crate::structs::theta::Theta; -struct CostMatrix { - matrix: Option>, - auc: f64, - cmax: f64, - cmin: f64, -} - -impl CostMatrix { - pub fn new(auc: f64, cmax: f64, cmin: f64) -> Self { - !unimplemented!() - } -} - /// The results of a multiple-model optimization /// /// @@ -36,6 +23,7 @@ pub fn mmopt( equation: impl Equation, errormodel: ErrorModel, nsamp: usize, + weights: Vec, ) -> Result { // Check that subject contains only one Occasion if subject.occasions().len() != 1 { @@ -70,7 +58,8 @@ pub fn mmopt( let (best_combo, min_risk) = candidate_indices .par_iter() .map(|combo| { - let risk = calculate_risk(combo, &pred_matrix, theta, &errormodel).unwrap(); + let risk = + calculate_risk(combo, &pred_matrix, theta, &errormodel, weights.clone()).unwrap(); (combo.clone(), risk) }) .min_by(|(_, risk_a), (_, risk_b)| risk_a.partial_cmp(risk_b).unwrap()) @@ -89,9 +78,9 @@ fn calculate_risk( pred_matrix: &Mat, theta: &Theta, errormodel: &ErrorModel, + weights: Vec, ) -> Result { let nspp = theta.nspp(); - let prob_uniform = 1.0 / nspp as f64; // Uniform probability for each support point let risk = (0..nspp) .flat_map(|i| (0..nspp).map(move |j| (i, j))) @@ -123,7 +112,7 @@ fn calculate_risk( // This can be parameterized later if needed let cost = 1.0; - prob_uniform * prob_uniform * (-sum_k_ijn).exp() * cost + weights[i] * weights[j] * (-sum_k_ijn).exp() * cost }) .sum(); From aef09c471790b776c0b228a93f89b356e4a2856b Mon Sep 17 00:00:00 2001 From: Markus Date: Sun, 13 Jul 2025 15:49:54 +0200 Subject: [PATCH 5/5] Update mod.rs --- src/mmopt/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/mmopt/mod.rs b/src/mmopt/mod.rs index 1580681cb..09e94e36a 100644 --- a/src/mmopt/mod.rs +++ b/src/mmopt/mod.rs @@ -108,9 +108,8 @@ fn calculate_risk( }) .sum(); - // For now, assume unit cost matrix (cost = 1.0 for all pairs) - // This can be parameterized later if needed - let cost = 1.0; + // No cost for getting it right + let cost = if i == j { 0.0 } else { 1.0 }; weights[i] * weights[j] * (-sum_k_ijn).exp() * cost })