Skip to main content

pmcore/algorithms/nonparametric/
controller.rs

1//! Step through a fit cycle by cycle, rather than running it all at once.
2//!
3//! [`fit_with`](crate::estimation::EstimationProblem::fit_with) runs to the end and gives back
4//! only the final result. For debugging or third-party monitoring, you can instead drive the fit yourself and inspect the state between cycles.
5//!
6//! - [`FitController`] — hold the fit and advance it one [`step`](FitController::step) at a
7//!   time, inspecting state in between.
8//! - [`fit_with_observer`](crate::estimation::EstimationProblem::fit_with_observer) — let it
9//!   run, but get a callback after every cycle.
10//!
11//! Both drive the same underlying runner and finish at the same result.
12
13use anyhow::Result;
14use pharmsol::prelude::{data::AssayErrorModels, simulator::Equation};
15
16use crate::algorithms::{NonParametricRunner, Status, StopReason};
17use crate::estimation::nonparametric::{NonParametricResult, Psi, Theta};
18use crate::estimation::{EstimationProblem, NonParametric};
19
20use super::NonParametricAlgorithm;
21
22/// A running fit that you advance one cycle at a time.
23///
24/// It's ready to go as soon as it's built, so the first [`step`](Self::step) runs cycle 1.
25/// The accessors read the current state between steps, and [`finish`](Self::finish) or
26/// [`into_result`](Self::into_result) turn it into a result.
27pub struct FitController<E: Equation + Send + 'static> {
28    runner: Box<dyn NonParametricRunner<E>>,
29}
30
31impl<E: Equation + Send + 'static> FitController<E> {
32    /// Build a controller and initialize it, ready for the first [`step`](Self::step).
33    pub(crate) fn new(
34        algorithm: NonParametricAlgorithm,
35        problem: EstimationProblem<E, NonParametric>,
36    ) -> Result<Self> {
37        let mut runner = algorithm.into_runner(problem)?;
38        runner.initialize()?;
39        Ok(Self { runner })
40    }
41
42    /// Run one cycle and return the new [`Status`].
43    ///
44    /// [`Continue`](Status::Continue) means there's more to do; [`Stop`](Status::Stop) means the
45    /// algorithm is finished and further steps won't change the outcome.
46    pub fn step(&mut self) -> Result<Status> {
47        self.runner.next_cycle()
48    }
49
50    /// Flag the fit as [`Aborted`](StopReason::Aborted) so the result shows an early stop.
51    ///
52    /// This only sets the status; it won't halt a later [`step`](Self::step), which runs a full
53    /// cycle and overwrites it. Call it just before [`into_result`](Self::into_result).
54    pub fn request_stop(&mut self) {
55        self.runner.set_status(Status::Stop(StopReason::Aborted));
56    }
57
58    /// The current cycle number (0 before the first [`step`](Self::step)).
59    pub fn cycle(&self) -> usize {
60        self.runner.cycle()
61    }
62
63    /// The current [`Status`] of the fit.
64    pub fn status(&self) -> &Status {
65        self.runner.status()
66    }
67
68    /// The current support points and weights.
69    pub fn theta(&self) -> &Theta {
70        self.runner.theta()
71    }
72
73    /// The current likelihood matrix.
74    pub fn psi(&self) -> &Psi {
75        self.runner.psi()
76    }
77
78    /// The current log-likelihood.
79    pub fn likelihood(&self) -> f64 {
80        self.runner.likelihood()
81    }
82
83    /// The current negative two log-likelihood (objective function).
84    pub fn n2ll(&self) -> f64 {
85        self.runner.n2ll()
86    }
87
88    /// The current error models (updated when the algorithm optimizes them).
89    pub fn error_models(&self) -> &AssayErrorModels {
90        self.runner.error_models()
91    }
92
93    /// Run the fit to completion and return the result.
94    ///
95    /// Safe to call anytime: if the fit has already stopped, it just returns the result.
96    pub fn finish(mut self) -> Result<NonParametricResult<E>> {
97        while self.status().is_continue() {
98            self.step()?;
99        }
100        self.runner.into_result()
101    }
102
103    /// Turn the current state into a result as-is, without running more cycles.
104    pub fn into_result(self) -> Result<NonParametricResult<E>> {
105        self.runner.into_result()
106    }
107}
108
109/// An observer's verdict after a cycle: keep going, or stop.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum CycleFlow {
112    /// Keep going — the algorithm may still stop on its own.
113    Continue,
114    /// Stop now.
115    Stop,
116}
117
118/// A callback run after every cycle of a fit.
119///
120/// It receives the live [`FitController`] to read the current state, and returns a
121/// [`CycleFlow`] to keep going or bail out. Any `FnMut(&FitController<E>) -> CycleFlow` works,
122/// so you can hand a plain closure to
123/// [`fit_with_observer`](crate::estimation::EstimationProblem::fit_with_observer).
124pub trait FitObserver<E: Equation + Send + 'static> {
125    fn on_cycle(&mut self, controller: &FitController<E>) -> CycleFlow;
126}
127
128impl<E, F> FitObserver<E> for F
129where
130    E: Equation + Send + 'static,
131    F: FnMut(&FitController<E>) -> CycleFlow,
132{
133    fn on_cycle(&mut self, controller: &FitController<E>) -> CycleFlow {
134        self(controller)
135    }
136}
137
138impl<E: Equation + Send + 'static> EstimationProblem<E, NonParametric> {
139    /// Start a fit you drive yourself, one [`step`](FitController::step) at a time.
140    ///
141    /// Takes any non-parametric config (`NpagConfig`, `NpodConfig`, `NpmapConfig`) or a
142    /// [`NonParametricAlgorithm`].
143    ///
144    /// ```no_run
145    /// use pmcore::prelude::*;
146    /// # fn run(problem: EstimationProblem<pmcore::prelude::ODE, NonParametric>) -> Result<()> {
147    /// let mut controller = problem.fit_controller(NpagConfig::new())?;
148    /// while controller.step()?.is_continue() {
149    ///     println!("cycle {} | -2LL {:.4}", controller.cycle(), controller.n2ll());
150    /// }
151    /// let result = controller.into_result()?;
152    /// # let _ = result; Ok(())
153    /// # }
154    /// ```
155    pub fn fit_controller(
156        self,
157        algorithm: impl Into<NonParametricAlgorithm>,
158    ) -> Result<FitController<E>> {
159        FitController::new(algorithm.into(), self)
160    }
161
162    /// Run a fit to completion, calling `observer` after every cycle.
163    ///
164    /// The observer sees every cycle, the last one included, and can return
165    /// [`CycleFlow::Stop`] to bail out early. Returns the same result as
166    /// [`fit_with`](Self::fit_with).
167    ///
168    /// ```no_run
169    /// use pmcore::prelude::*;
170    /// # fn run(problem: EstimationProblem<pmcore::prelude::ODE, NonParametric>) -> Result<()> {
171    /// let result = problem.fit_with_observer(NpagConfig::new(), |c: &FitController<_>| {
172    ///     println!("cycle {} | -2LL {:.4}", c.cycle(), c.n2ll());
173    ///     CycleFlow::Continue
174    /// })?;
175    /// # let _ = result; Ok(())
176    /// # }
177    /// ```
178    pub fn fit_with_observer<O: FitObserver<E>>(
179        self,
180        algorithm: impl Into<NonParametricAlgorithm>,
181        mut observer: O,
182    ) -> Result<NonParametricResult<E>> {
183        let mut controller = self.fit_controller(algorithm)?;
184        loop {
185            let status = controller.step()?;
186            let flow = observer.on_cycle(&controller);
187            // Stop on the algorithm's own criteria, or when the observer asks us to.
188            if status.is_stop() {
189                break;
190            }
191            if flow == CycleFlow::Stop {
192                controller.request_stop();
193                break;
194            }
195        }
196        controller.into_result()
197    }
198}