pmcore/algorithms/nonparametric/
error_optim.rs1use anyhow::Result;
4use pharmsol::prelude::{
5 data::{AssayErrorModels, Data},
6 simulator::Equation,
7};
8use serde::{Deserialize, Serialize};
9
10use crate::estimation::nonparametric::{calculate_psi, ipm::burke, Psi, Theta, Weights};
11
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct ErrorOptimConfig {
15 pub step: f64,
17 pub min_step: f64,
19 pub growth: f64,
21 pub shrink: f64,
23}
24
25impl Default for ErrorOptimConfig {
26 fn default() -> Self {
27 Self {
28 step: 0.1,
29 min_step: 0.01,
30 growth: 4.0,
31 shrink: 0.5,
32 }
33 }
34}
35
36impl ErrorOptimConfig {
37 pub fn new() -> Self {
38 Self::default()
39 }
40
41 pub fn step(mut self, step: f64) -> Self {
42 self.step = step;
43 self
44 }
45
46 pub fn min_step(mut self, min_step: f64) -> Self {
47 self.min_step = min_step;
48 self
49 }
50
51 pub fn growth(mut self, growth: f64) -> Self {
52 self.growth = growth;
53 self
54 }
55
56 pub fn shrink(mut self, shrink: f64) -> Self {
57 self.shrink = shrink;
58 self
59 }
60}
61
62#[allow(clippy::too_many_arguments)]
75pub(crate) fn optimize_error_models<E: Equation + Send + 'static>(
76 equation: &E,
77 data: &Data,
78 theta: &Theta,
79 error_models: &mut AssayErrorModels,
80 gamma_delta: &mut [f64],
81 objf: &mut f64,
82 lambda: &mut Weights,
83 psi: &mut Psi,
84 config: &ErrorOptimConfig,
85) -> Result<()> {
86 error_models
87 .clone()
88 .iter_mut()
89 .filter_map(|(outeq, em)| {
90 if em.optimize() {
91 Some((outeq, em))
92 } else {
93 None
94 }
95 })
96 .try_for_each(|(outeq, em)| -> Result<()> {
97 let gamma_up = em.factor()? * (1.0 + gamma_delta[outeq]);
100 let gamma_down = em.factor()? / (1.0 + gamma_delta[outeq]);
101
102 let mut error_model_up = error_models.clone();
103 error_model_up.set_factor(outeq, gamma_up)?;
104
105 let mut error_model_down = error_models.clone();
106 error_model_down.set_factor(outeq, gamma_down)?;
107
108 let psi_up = calculate_psi(equation, data, theta, &error_model_up, false)?;
109 let psi_down = calculate_psi(equation, data, theta, &error_model_down, false)?;
110
111 let up = match burke(&psi_up) {
115 Ok((lambda, objf)) => Some((lambda, objf)),
116 Err(err) => {
117 tracing::warn!(
118 "Error in IPM during optim (up) for outeq {}: {:?}",
119 outeq,
120 err
121 );
122 None
123 }
124 };
125 let down = match burke(&psi_down) {
126 Ok((lambda, objf)) => Some((lambda, objf)),
127 Err(err) => {
128 tracing::warn!(
129 "Error in IPM during optim (down) for outeq {}: {:?}",
130 outeq,
131 err
132 );
133 None
134 }
135 };
136
137 let mut best: Option<(f64, Weights, Psi, f64)> = None;
141 if let Some((lambda_up, objf_up)) = up {
142 if objf_up > *objf {
143 best = Some((objf_up, lambda_up, psi_up, gamma_up));
144 }
145 }
146 if let Some((lambda_down, objf_down)) = down {
147 let threshold = best.as_ref().map_or(*objf, |(o, ..)| *o);
148 if objf_down > threshold {
149 best = Some((objf_down, lambda_down, psi_down, gamma_down));
150 }
151 }
152 if let Some((new_objf, new_lambda, new_psi, gamma)) = best {
153 error_models.set_factor(outeq, gamma)?;
154 *objf = new_objf;
155 gamma_delta[outeq] *= config.growth;
156 *lambda = new_lambda;
157 *psi = new_psi;
158 }
159 gamma_delta[outeq] *= config.shrink;
160 if gamma_delta[outeq] <= config.min_step {
161 gamma_delta[outeq] = config.step;
162 }
163 Ok(())
164 })?;
165
166 Ok(())
167}