1use std::fs;
2use std::path::Path;
3
4use crate::estimation::nonparametric::{NonParametricResult, Psi, Theta};
5use crate::estimation::{EstimationProblem, Framework};
6use crate::results::FitResult;
7
8use anyhow::Context;
9use anyhow::Result;
10use ndarray::parallel::prelude::{IntoParallelIterator, ParallelIterator};
11
12use pharmsol::prelude::{data::Data, simulator::Equation};
13
14use pharmsol::{Predictions, Subject};
15use serde::{Deserialize, Serialize};
16
17pub trait Algorithm<E: Equation, F: crate::estimation::Framework> {
23 type Output: FitResult;
25
26 fn fit(self, problem: EstimationProblem<E, F>) -> Result<Self::Output>;
29}
30
31pub mod nonparametric;
33pub mod parametric;
34
35impl<E: Equation, F: Framework> EstimationProblem<E, F> {
36 pub fn fit_with<A>(self, algorithm: A) -> Result<A::Output>
39 where
40 A: Algorithm<E, F>,
41 {
42 algorithm.fit(self)
43 }
44}
45
46pub trait NonParametricRunner<E: Equation + Send + 'static>: Sync + Send + 'static {
47 fn check_zero_probability_subjects(&self) -> Result<()> {
56 let psi = self.psi().matrix();
57
58 let nonfinite = psi
60 .row_iter()
61 .flat_map(|row| row.iter().copied())
62 .filter(|v| !v.is_finite())
63 .count();
64 if nonfinite > 0 {
65 tracing::warn!(
66 "Psi matrix contains {} non-finite value(s) of {} total",
67 nonfinite,
68 psi.nrows() * psi.ncols()
69 );
70 }
71
72 let subjects = self.data().subjects();
74 let flagged: Vec<usize> = (0..psi.nrows())
75 .filter(|&i| {
76 let probability: f64 = (0..psi.ncols()).map(|j| psi[(i, j)]).sum();
77 !probability.is_finite() || probability == 0.0
78 })
79 .collect();
80
81 if flagged.is_empty() {
82 return Ok(());
83 }
84
85 tracing::error!(
86 "{}/{} subjects have zero probability given the model",
87 flagged.len(),
88 psi.nrows()
89 );
90
91 for &i in &flagged {
92 self.log_zero_probability_subject(subjects[i]);
93 }
94
95 let ids: Vec<&String> = flagged.iter().map(|&i| subjects[i].id()).collect();
96 Err(anyhow::anyhow!(
97 "The probability of {}/{} subjects is zero given the model. Affected subjects: {:?}",
98 flagged.len(),
99 psi.nrows(),
100 ids
101 ))
102 }
103
104 fn log_zero_probability_subject(&self, subject: &Subject) {
107 tracing::debug!("Subject with zero probability: {}", subject.id());
108
109 let error_model = self.error_models().clone();
110
111 let mut results: Vec<_> = self
113 .theta()
114 .matrix()
115 .row_iter()
116 .enumerate()
117 .collect::<Vec<_>>()
118 .into_par_iter()
119 .map(|(i, spp)| {
120 let support_point: Vec<f64> = spp.iter().copied().collect();
121 let (pred, ll) = self
122 .equation()
123 .simulate_subject_dense(subject, &support_point, Some(&error_model))
124 .unwrap(); (i, support_point, pred.get_predictions(), ll)
126 })
127 .collect();
128
129 let mut nan = 0;
131 let mut pos_inf = 0;
132 let mut neg_inf = 0;
133 let mut zero = 0;
134 let mut valid = 0;
135 for (_, _, _, ll) in &results {
136 match ll {
137 Some(v) if v.is_nan() => nan += 1,
138 Some(v) if v.is_infinite() && v.is_sign_positive() => pos_inf += 1,
139 Some(v) if v.is_infinite() => neg_inf += 1,
140 Some(v) if *v == 0.0 => zero += 1,
141 Some(_) => valid += 1,
142 None => nan += 1,
143 }
144 }
145
146 let total = results.len();
147 let pct = |n: usize| 100.0 * n as f64 / total as f64;
148 tracing::debug!(
149 "\tLikelihood analysis for subject {} ({} support points):",
150 subject.id(),
151 total
152 );
153 tracing::debug!("\tNaN likelihoods: {} ({:.1}%)", nan, pct(nan));
154 tracing::debug!("\t+Inf likelihoods: {} ({:.1}%)", pos_inf, pct(pos_inf));
155 tracing::debug!("\t-Inf likelihoods: {} ({:.1}%)", neg_inf, pct(neg_inf));
156 tracing::debug!("\tZero likelihoods: {} ({:.1}%)", zero, pct(zero));
157 tracing::debug!("\tValid likelihoods: {} ({:.1}%)", valid, pct(valid));
158
159 results.sort_by(|a, b| {
161 b.3.unwrap_or(f64::NEG_INFINITY)
162 .partial_cmp(&a.3.unwrap_or(f64::NEG_INFINITY))
163 .unwrap_or(std::cmp::Ordering::Equal)
164 });
165
166 const TAKE: usize = 3;
167 tracing::debug!("Top {} most likely support points:", TAKE);
168 for (i, support_point, preds, ll) in results.iter().take(TAKE) {
169 tracing::debug!("\tSupport point #{}: {:?}", i, support_point);
170 tracing::debug!("\t\tLog-likelihood: {:?}", ll);
171 tracing::debug!(
172 "\t\tTimes: {:?}",
173 preds.iter().map(|x| x.time()).collect::<Vec<f64>>()
174 );
175 tracing::debug!(
176 "\t\tObservations: {:?}",
177 preds
178 .iter()
179 .map(|x| x.observation())
180 .collect::<Vec<Option<f64>>>()
181 );
182 tracing::debug!(
183 "\t\tPredictions: {:?}",
184 preds.iter().map(|x| x.prediction()).collect::<Vec<f64>>()
185 );
186 tracing::debug!(
187 "\t\tOuteqs: {:?}",
188 preds.iter().map(|x| x.outeq()).collect::<Vec<usize>>()
189 );
190 tracing::debug!(
191 "\t\tStates: {:?}",
192 preds
193 .iter()
194 .map(|x| x.state().to_vec())
195 .collect::<Vec<Vec<f64>>>()
196 );
197 }
198 tracing::debug!("=====================");
199 }
200
201 fn error_models(&self) -> &pharmsol::prelude::data::AssayErrorModels;
202 fn equation(&self) -> &E;
204 fn data(&self) -> &Data;
206
207 fn increment_cycle(&mut self) -> usize;
209 fn cycle(&self) -> usize;
211 fn set_theta(&mut self, theta: Theta);
213 fn theta(&self) -> Θ
215 fn psi(&self) -> Ψ
217 fn likelihood(&self) -> f64;
219 fn n2ll(&self) -> f64 {
221 -2.0 * self.likelihood()
222 }
223 fn status(&self) -> &Status;
225 fn set_status(&mut self, status: Status);
227 fn evaluation(&mut self) -> Result<Status>;
229
230 fn log_cycle_state(&mut self);
232
233 fn initialize(&mut self) -> Result<()> {
235 if Path::new("stop").exists() {
237 tracing::info!("Removing existing stop file prior to run");
238 fs::remove_file("stop").context("Unable to remove previous stop file")?;
239 }
240 self.set_status(Status::Continue);
241
242 Ok(())
243 }
244 fn estimation(&mut self) -> Result<()>;
245 fn condensation(&mut self) -> Result<()>;
251
252 fn optimizations(&mut self) -> Result<()>;
257
258 fn expansion(&mut self) -> Result<()>;
263
264 fn next_cycle(&mut self) -> Result<Status> {
270 let cycle = self.increment_cycle();
271
272 if cycle > 1 {
273 self.expansion()?;
274 }
275
276 let span = tracing::info_span!("", "{}", format!("Cycle {}", self.cycle()));
277 let _enter = span.enter();
278 self.estimation()?;
279 self.condensation()?;
280 self.optimizations()?;
281 self.evaluation()
282 }
283
284 fn fit(&mut self) -> Result<NonParametricResult<E>> {
290 self.initialize()?;
291 while let Status::Continue = self.next_cycle()? {}
292 self.into_result()
293 }
294
295 #[allow(clippy::wrong_self_convention)]
296 fn into_result(&self) -> Result<NonParametricResult<E>>;
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
301pub enum Status {
302 Continue,
303 Stop(StopReason),
304}
305
306impl Status {
307 pub fn is_continue(&self) -> bool {
309 matches!(self, Status::Continue)
310 }
311
312 pub fn is_stop(&self) -> bool {
314 matches!(self, Status::Stop(_))
315 }
316
317 pub fn stop_reason(&self) -> Option<&StopReason> {
319 match self {
320 Status::Stop(reason) => Some(reason),
321 Status::Continue => None,
322 }
323 }
324
325 pub fn converged(&self) -> bool {
327 matches!(self, Status::Stop(StopReason::Converged))
328 }
329}
330
331impl std::fmt::Display for Status {
332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
333 match self {
334 Status::Continue => write!(f, "Continue"),
335 Status::Stop(reason) => write!(f, "Stopped ({reason})"),
336 }
337 }
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342pub enum StopReason {
343 Converged,
345 MaxCycles,
347 StopFile,
349 Aborted,
352}
353
354impl std::fmt::Display for StopReason {
355 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356 let reason = match self {
357 StopReason::Converged => "converged",
358 StopReason::MaxCycles => "maximum cycles reached",
359 StopReason::StopFile => "stop file detected",
360 StopReason::Aborted => "aborted",
361 };
362 f.write_str(reason)
363 }
364}