pmcore/algorithms/nonparametric/mod.rs
1//! Non-parametric algorithm implementations
2//!
3//! This module contains the trait definition and implementations for non-parametric
4//! population pharmacokinetic algorithms. These algorithms estimate the population
5//! distribution as a discrete set of support points with associated probability weights.
6//!
7//! # Available Algorithms
8//!
9//! - [`NPAG`](npag): Non-Parametric Adaptive Grid
10//! - [`NPOD`](npod): Non-Parametric Optimal Design
11//! - [`NPMAP`](npmap): Maximum a posteriori reweighting
12//!
13//! # Algorithm Selection
14//!
15//! Use the [`NonParametricAlgorithm`] enum to select and configure an algorithm. Each
16//! variant wraps its algorithm-specific configuration struct (e.g. [`NpagConfig`]). The
17//! internal execution state used while fitting implements the [`NonParametricRunner`]
18//! trait, which defines the common interface for initialization, estimation, condensation,
19//! expansion, and convergence evaluation.
20
21// Shared error-model factor optimization
22pub mod error_optim;
23
24// Algorithm implementations
25pub mod ncnpag;
26pub mod npag;
27pub mod npmap;
28pub mod npod;
29
30// Incremental, observable fitting (stepping handle + per-cycle observers).
31pub mod controller;
32
33// Re-export algorithm structs
34pub use ncnpag::NCNPAG;
35pub use npag::NPAG;
36pub use npmap::NPMAP;
37pub use npod::NPOD;
38
39// Re-export per-algorithm configuration structs
40pub use error_optim::ErrorOptimConfig;
41pub use ncnpag::NcnpagConfig;
42pub use npag::NpagConfig;
43pub use npmap::NpmapConfig;
44pub use npod::NpodConfig;
45
46// Re-export the incremental fitting API
47pub use controller::{CycleFlow, FitController, FitObserver};
48
49use crate::algorithms::{Algorithm, NonParametricRunner};
50use crate::estimation::nonparametric::NonParametricResult;
51use crate::estimation::{EstimationProblem, NonParametric};
52use anyhow::Result;
53use pharmsol::prelude::simulator::Equation;
54
55/// The non-parametric algorithms supported by PMcore.
56///
57/// Use the constructors to select an algorithm with its default configuration:
58///
59/// ```no_run
60/// use pmcore::prelude::*;
61///
62/// // Default NPAG configuration.
63/// let algorithm = NonParametricAlgorithm::npag();
64/// ```
65///
66/// To customize an algorithm, build its configuration struct (which exposes only the
67/// setters valid for that algorithm) and pass it directly to
68/// [`fit_with`](crate::estimation::EstimationProblem::fit_with):
69///
70/// ```no_run
71/// use pmcore::prelude::*;
72///
73/// // NPAG with a tighter convergence criterion and a cycle cap.
74/// let config = NpagConfig::new().eps(0.1).max_cycles(500);
75/// // `problem.fit_with(config)` accepts the config directly.
76/// ```
77///
78/// Each configuration type ([`NpagConfig`], [`NpodConfig`], [`NpmapConfig`]) implements
79/// [`Algorithm`] by delegating to the matching enum variant, so configs can be passed to
80/// `fit_with` without converting them first.
81#[derive(Debug, Clone)]
82pub enum NonParametricAlgorithm {
83 /// Non-Parametric Adaptive Grid.
84 Npag(NpagConfig),
85 /// Non-Parametric Optimal Design.
86 Npod(NpodConfig),
87 /// Non-parametric maximum a posteriori (posterior probability reweighting).
88 Npmap(NpmapConfig),
89 /// Non-collapsing NPAG (single-pass Bayesian reweighting of fixed support points).
90 Ncnpag(NcnpagConfig),
91}
92
93impl Default for NonParametricAlgorithm {
94 fn default() -> Self {
95 Self::npag()
96 }
97}
98
99impl From<NpagConfig> for NonParametricAlgorithm {
100 fn from(config: NpagConfig) -> Self {
101 Self::Npag(config)
102 }
103}
104
105impl From<NpodConfig> for NonParametricAlgorithm {
106 fn from(config: NpodConfig) -> Self {
107 Self::Npod(config)
108 }
109}
110
111impl From<NpmapConfig> for NonParametricAlgorithm {
112 fn from(config: NpmapConfig) -> Self {
113 Self::Npmap(config)
114 }
115}
116
117impl From<NcnpagConfig> for NonParametricAlgorithm {
118 fn from(config: NcnpagConfig) -> Self {
119 Self::Ncnpag(config)
120 }
121}
122
123impl NonParametricAlgorithm {
124 /// The Non-Parametric Adaptive Grid (NPAG) algorithm with its default configuration.
125 pub fn npag() -> Self {
126 Self::Npag(NpagConfig::default())
127 }
128
129 /// The Non-Parametric Optimal Design (NPOD) algorithm with its default configuration.
130 pub fn npod() -> Self {
131 Self::Npod(NpodConfig::default())
132 }
133
134 /// The non-parametric maximum a posteriori (NPMAP) algorithm with its default
135 /// configuration.
136 pub fn npmap() -> Self {
137 Self::Npmap(NpmapConfig::default())
138 }
139
140 /// The non-collapsing NPAG (NCNPAG) algorithm with its default configuration.
141 pub fn ncnpag() -> Self {
142 Self::Ncnpag(NcnpagConfig::default())
143 }
144
145 /// Build the internal, mutable execution state (runner) for this algorithm.
146 ///
147 /// Both [`fit`](Algorithm::fit) and the stepping
148 /// [`FitController`](crate::algorithms::nonparametric::controller::FitController)
149 /// build on this primitive.
150 pub(crate) fn into_runner<E: Equation + Send + 'static>(
151 self,
152 problem: EstimationProblem<E, NonParametric>,
153 ) -> Result<Box<dyn NonParametricRunner<E>>> {
154 // `problem.prior` is the prior `Theta` (which also carries the parameter
155 // space) and `problem.error_models` is strictly `AssayErrorModels`.
156 let runner: Box<dyn NonParametricRunner<E>> = match self {
157 Self::Npag(config) => Box::new(NPAG::from_parts(
158 problem.model.equation,
159 problem.data,
160 problem.error_models,
161 problem.prior,
162 config,
163 )?),
164 Self::Npod(config) => Box::new(NPOD::from_parts(
165 problem.model.equation,
166 problem.data,
167 problem.error_models,
168 problem.prior,
169 config,
170 )?),
171 Self::Npmap(config) => Box::new(NPMAP::from_parts(
172 problem.model.equation,
173 problem.data,
174 problem.error_models,
175 problem.prior,
176 config,
177 )?),
178 Self::Ncnpag(config) => Box::new(NCNPAG::from_parts(
179 problem.model.equation,
180 problem.data,
181 problem.error_models,
182 problem.prior,
183 config,
184 )?),
185 };
186 Ok(runner)
187 }
188}
189
190impl<E: Equation + Send + 'static> Algorithm<E, NonParametric> for NonParametricAlgorithm {
191 type Output = NonParametricResult<E>;
192
193 fn fit(self, problem: EstimationProblem<E, NonParametric>) -> Result<Self::Output> {
194 let mut runner = self.into_runner(problem)?;
195 runner.fit()
196 }
197}
198
199// Each configuration struct delegates to its matching `NonParametricAlgorithm` variant so it
200// can be passed directly to `fit_with`. This keeps the variant-specific setters on the config
201// types (compile-time checked) while the enum remains the single source of fitting logic.
202impl<E: Equation + Send + 'static> Algorithm<E, NonParametric> for NpagConfig {
203 type Output = NonParametricResult<E>;
204
205 fn fit(self, problem: EstimationProblem<E, NonParametric>) -> Result<Self::Output> {
206 NonParametricAlgorithm::from(self).fit(problem)
207 }
208}
209
210impl<E: Equation + Send + 'static> Algorithm<E, NonParametric> for NpodConfig {
211 type Output = NonParametricResult<E>;
212
213 fn fit(self, problem: EstimationProblem<E, NonParametric>) -> Result<Self::Output> {
214 NonParametricAlgorithm::from(self).fit(problem)
215 }
216}
217
218impl<E: Equation + Send + 'static> Algorithm<E, NonParametric> for NpmapConfig {
219 type Output = NonParametricResult<E>;
220
221 fn fit(self, problem: EstimationProblem<E, NonParametric>) -> Result<Self::Output> {
222 NonParametricAlgorithm::from(self).fit(problem)
223 }
224}
225
226impl<E: Equation + Send + 'static> Algorithm<E, NonParametric> for NcnpagConfig {
227 type Output = NonParametricResult<E>;
228
229 fn fit(self, problem: EstimationProblem<E, NonParametric>) -> Result<Self::Output> {
230 NonParametricAlgorithm::from(self).fit(problem)
231 }
232}