pharmsol/simulator/equation/analytical/
mod.rs

1pub mod one_compartment_models;
2pub mod three_compartment_models;
3pub mod two_compartment_models;
4
5pub use one_compartment_models::*;
6pub use three_compartment_models::*;
7pub use two_compartment_models::*;
8
9use crate::PharmsolError;
10use crate::{
11    data::Covariates, simulator::*, Equation, EquationPriv, EquationTypes, Observation, Subject,
12};
13use cached::proc_macro::cached;
14use cached::UnboundCache;
15
16/// Model equation using analytical solutions.
17///
18/// This implementation uses closed-form analytical solutions for the model
19/// equations rather than numerical integration.
20#[derive(Clone, Debug)]
21pub struct Analytical {
22    eq: AnalyticalEq,
23    seq_eq: SecEq,
24    lag: Lag,
25    fa: Fa,
26    init: Init,
27    out: Out,
28    neqs: Neqs,
29}
30
31impl Analytical {
32    /// Create a new Analytical equation model.
33    ///
34    /// # Parameters
35    /// - `eq`: The analytical equation function
36    /// - `seq_eq`: The secondary equation function
37    /// - `lag`: The lag time function
38    /// - `fa`: The fraction absorbed function
39    /// - `init`: The initial state function
40    /// - `out`: The output equation function
41    /// - `neqs`: The number of states and output equations
42    pub fn new(
43        eq: AnalyticalEq,
44        seq_eq: SecEq,
45        lag: Lag,
46        fa: Fa,
47        init: Init,
48        out: Out,
49        neqs: Neqs,
50    ) -> Self {
51        Self {
52            eq,
53            seq_eq,
54            lag,
55            fa,
56            init,
57            out,
58            neqs,
59        }
60    }
61}
62
63impl EquationTypes for Analytical {
64    type S = V;
65    type P = SubjectPredictions;
66}
67
68impl EquationPriv for Analytical {
69    // #[inline(always)]
70    // fn get_init(&self) -> &Init {
71    //     &self.init
72    // }
73
74    // #[inline(always)]
75    // fn get_out(&self) -> &Out {
76    //     &self.out
77    // }
78
79    // #[inline(always)]
80    // fn get_lag(&self, spp: &[f64]) -> Option<HashMap<usize, f64>> {
81    //     Some((self.lag)(&V::from_vec(spp.to_owned())))
82    // }
83
84    // #[inline(always)]
85    // fn get_fa(&self, spp: &[f64]) -> Option<HashMap<usize, f64>> {
86    //     Some((self.fa)(&V::from_vec(spp.to_owned())))
87    // }
88
89    #[inline(always)]
90    fn lag(&self) -> &Lag {
91        &self.lag
92    }
93
94    #[inline(always)]
95    fn fa(&self) -> &Fa {
96        &self.fa
97    }
98
99    #[inline(always)]
100    fn get_nstates(&self) -> usize {
101        self.neqs.0
102    }
103
104    #[inline(always)]
105    fn get_nouteqs(&self) -> usize {
106        self.neqs.1
107    }
108    #[inline(always)]
109    fn solve(
110        &self,
111        x: &mut Self::S,
112        support_point: &Vec<f64>,
113        covariates: &Covariates,
114        infusions: &Vec<Infusion>,
115        ti: f64,
116        tf: f64,
117    ) -> Result<(), PharmsolError> {
118        if ti == tf {
119            return Ok(());
120        }
121
122        // 1) Build and sort event times
123        let mut ts = Vec::new();
124        ts.push(ti);
125        ts.push(tf);
126        for inf in infusions {
127            let t0 = inf.time();
128            let t1 = t0 + inf.duration();
129            if t0 > ti && t0 < tf {
130                ts.push(t0)
131            }
132            if t1 > ti && t1 < tf {
133                ts.push(t1)
134            }
135        }
136        ts.sort_by(|a, b| a.partial_cmp(b).unwrap());
137        ts.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
138
139        // 2) March over each sub-interval
140        let mut current_t = ts[0];
141        for &next_t in &ts[1..] {
142            // prepare support and infusion rate for [current_t .. next_t]
143            let mut sp = V::from_vec(support_point.to_owned());
144            let mut rateiv = V::from_vec(vec![0.0; 3]);
145            for inf in infusions {
146                let s = inf.time();
147                let e = s + inf.duration();
148                if current_t >= s && next_t <= e {
149                    rateiv[inf.input()] += inf.amount() / inf.duration();
150                }
151            }
152
153            // advance the support-point to next_t
154            (self.seq_eq)(&mut sp, next_t, covariates);
155
156            // advance state by dt
157            let dt = next_t - current_t;
158            *x = (self.eq)(x, &sp, dt, rateiv, covariates);
159
160            current_t = next_t;
161        }
162
163        Ok(())
164    }
165
166    #[inline(always)]
167    fn process_observation(
168        &self,
169        support_point: &Vec<f64>,
170        observation: &Observation,
171        error_models: Option<&ErrorModels>,
172        _time: f64,
173        covariates: &Covariates,
174        x: &mut Self::S,
175        likelihood: &mut Vec<f64>,
176        output: &mut Self::P,
177    ) -> Result<(), PharmsolError> {
178        let mut y = V::zeros(self.get_nouteqs());
179        let out = &self.out;
180        (out)(
181            x,
182            &V::from_vec(support_point.clone()),
183            observation.time(),
184            covariates,
185            &mut y,
186        );
187        let pred = y[observation.outeq()];
188        let pred = observation.to_prediction(pred, x.as_slice().to_vec());
189        if let Some(error_models) = error_models {
190            likelihood.push(pred.likelihood(error_models)?);
191        }
192        output.add_prediction(pred);
193        Ok(())
194    }
195    #[inline(always)]
196    fn initial_state(&self, spp: &Vec<f64>, covariates: &Covariates, occasion_index: usize) -> V {
197        let init = &self.init;
198        let mut x = V::zeros(self.get_nstates());
199        if occasion_index == 0 {
200            (init)(&V::from_vec(spp.to_vec()), 0.0, covariates, &mut x);
201        }
202        x
203    }
204}
205
206impl Equation for Analytical {
207    fn estimate_likelihood(
208        &self,
209        subject: &Subject,
210        support_point: &Vec<f64>,
211        error_models: &ErrorModels,
212        cache: bool,
213    ) -> Result<f64, PharmsolError> {
214        _estimate_likelihood(self, subject, support_point, error_models, cache)
215    }
216    fn kind() -> crate::EqnKind {
217        crate::EqnKind::Analytical
218    }
219}
220fn spphash(spp: &[f64]) -> u64 {
221    spp.iter().fold(0, |acc, x| acc + x.to_bits())
222}
223
224#[inline(always)]
225#[cached(
226    ty = "UnboundCache<String, SubjectPredictions>",
227    create = "{ UnboundCache::with_capacity(100_000) }",
228    convert = r#"{ format!("{}{}", subject.id(), spphash(support_point)) }"#,
229    result = "true"
230)]
231fn _subject_predictions(
232    ode: &Analytical,
233    subject: &Subject,
234    support_point: &Vec<f64>,
235) -> Result<SubjectPredictions, PharmsolError> {
236    Ok(ode.simulate_subject(subject, support_point, None)?.0)
237}
238
239fn _estimate_likelihood(
240    ode: &Analytical,
241    subject: &Subject,
242    support_point: &Vec<f64>,
243    error_models: &ErrorModels,
244    cache: bool,
245) -> Result<f64, PharmsolError> {
246    let ypred = if cache {
247        _subject_predictions(ode, subject, support_point)
248    } else {
249        _subject_predictions_no_cache(ode, subject, support_point)
250    }?;
251    ypred.likelihood(error_models)
252}