diff --git a/pharmsol-macros/src/lib.rs b/pharmsol-macros/src/lib.rs index 691b8598..af7ff54a 100644 --- a/pharmsol-macros/src/lib.rs +++ b/pharmsol-macros/src/lib.rs @@ -3208,6 +3208,45 @@ fn expand_sde_out( // Proc macros // --------------------------------------------------------------------------- +/// Define an ODE (ordinary differential equation) model. +/// +/// This is the primary entry point for building pharmacometric ODE models. +/// The macro generates and validates an `ODE` model and automatically generates its metadata +/// (parameter names, state labels, output labels, route declarations). +/// +/// # Fields +/// +/// | Field | Required | Description | +/// |-------|----------|-------------| +/// | `name` | yes | Model name (`"my_model"`) | +/// | `params` | yes | Parameter identifiers `[ka, ke, v]` | +/// | `covariates` | no | Covariate identifiers `[wt, age]` | +/// | `states` | yes | State identifiers `[gut, central]` | +/// | `outputs` | yes | Output identifiers `[cp]` | +/// | `routes` | no | Route declarations `[bolus(oral) -> gut, infusion(iv) -> central]` | +/// | `diffeq` | yes | Closure `\|x, p, t, dx, cov\| { … }` writing derivatives into `dx` | +/// | `lag` | no | Closure returning route‑specific lag times via `lag! { route => expr }` | +/// | `fa` | no | Closure returning bioavailability fractions | +/// | `init` | no | Closure setting initial state values | +/// | `out` | yes | Closure `\|x, p, t, cov, y\| { … }` mapping states to outputs | +/// +/// # Example +/// +/// ```ignore +/// let model = ode! { +/// name: "one_cmt_iv", +/// params: [ke, v], +/// states: [central], +/// outputs: [cp], +/// routes: [infusion(iv) -> central], +/// diffeq: |x, _p, _t, dx, _cov| { +/// dx[central] = -ke * x[central]; +/// }, +/// out: |x, _p, _t, _cov, y| { +/// y[cp] = x[central] / v; +/// }, +/// }; +/// ``` #[proc_macro] pub fn ode(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as OdeInput); @@ -3365,6 +3404,45 @@ pub fn ode(input: TokenStream) -> TokenStream { .into() } +/// Define an analytical (closed‑form) PK model. +/// +/// Builds a model that uses a built‑in analytical solution. The macro validates that the declared parameters +/// match the chosen analytical solution's requirements and generates an `Analytical` value +/// with full metadata. +/// +/// # Fields +/// +/// | Field | Required | Description | +/// |-------|----------|-------------| +/// | `name` | yes | Model name | +/// | `params` | yes | Parameter identifiers | +/// | `derived` | no | Derived parameter identifiers (computed in `derive`) | +/// | `covariates` | no | Covariate identifiers | +/// | `states` | yes | State identifiers | +/// | `outputs` | yes | Output identifiers | +/// | `routes` | no | Route declarations | +/// | `structure` | yes | Built‑in kernel name, e.g. `one_compartment` or `one_compartment_with_absorption` | +/// | `derive` | no | Closure `\|t\| { … }` computing derived parameters from primaries and covariates | +/// | `lag` | no | Lag‑time closure | +/// | `fa` | no | Bioavailability closure | +/// | `init` | no | Initial‑state closure | +/// | `out` | yes | Output mapping closure | +/// +/// # Example +/// +/// ```ignore +/// let model = analytical! { +/// name: "one_cmt_oral", +/// params: [ka, ke, v], +/// states: [gut, central], +/// outputs: [cp], +/// routes: [bolus(oral) -> gut], +/// structure: one_compartment_with_absorption, +/// out: |x, _t, y| { +/// y[cp] = x[central] / v; +/// }, +/// }; +/// ``` #[proc_macro] pub fn analytical(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as AnalyticalInput); @@ -3506,7 +3584,6 @@ pub fn analytical(input: TokenStream) -> TokenStream { let nstates = input.states.len(); let ndrugs = dense_index_len(&route_bindings); let nout = input.outputs.len(); - let name = &input.name; let params = &input.params; let covariates = &input.covariates; @@ -3550,6 +3627,51 @@ pub fn analytical(input: TokenStream) -> TokenStream { .into() } +/// Define an SDE (stochastic differential equation) model. +/// +/// Builds a particle‑based stochastic model with a drift term, a diffusion +/// term, and a configurable number of particles. The macro generates an `SDE` +/// value with full metadata. +/// +/// # Fields +/// +/// | Field | Required | Description | +/// |-------|----------|-------------| +/// | `name` | yes | Model name | +/// | `params` | yes | Parameter identifiers | +/// | `covariates` | no | Covariate identifiers | +/// | `states` | yes | State identifiers | +/// | `outputs` | yes | Output identifiers | +/// | `particles` | yes | Number of particles for the simulation | +/// | `routes` | no | Route declarations | +/// | `drift` | yes | Closure `\|x, p, t, dx, cov\| { … }` for the deterministic drift | +/// | `diffusion` | yes | Closure `\|p, sigma\| { … }` setting per‑state diffusion coefficients | +/// | `lag` | no | Lag‑time closure | +/// | `fa` | no | Bioavailability closure | +/// | `init` | no | Initial‑state closure | +/// | `out` | yes | Output mapping closure | +/// +/// # Example +/// +/// ```ignore +/// let model = sde! { +/// name: "one_cmt_sde", +/// params: [ke, sigma_ke, v], +/// states: [central], +/// outputs: [cp], +/// particles: 16, +/// routes: [infusion(iv) -> central], +/// drift: |x, _p, _t, dx, _cov| { +/// dx[central] = -ke * x[central]; +/// }, +/// diffusion: |_p, sigma| { +/// sigma[central] = sigma_ke; +/// }, +/// out: |x, _p, _t, _cov, y| { +/// y[cp] = x[central] / v; +/// }, +/// }; +/// ``` #[proc_macro] pub fn sde(input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as SdeInput); diff --git a/src/optimize/effect.rs b/src/optimize/effect.rs index e35fe95d..c31d6ee6 100644 --- a/src/optimize/effect.rs +++ b/src/optimize/effect.rs @@ -1,3 +1,9 @@ +//! Maximum-effect (`E2`) optimization for dual-site pharmacodynamic models. +//! +//! The central entry point is [`get_e2`], which computes the maximum achievable +//! effect for a model with two binding sites via Nelder‑Mead optimization in +//! log‑space. + use argmin::{ core::{CostFunction, Executor, TerminationReason, TerminationStatus}, solver::neldermead::NelderMead, diff --git a/src/optimize/mod.rs b/src/optimize/mod.rs index 14f2cd50..b044108b 100644 --- a/src/optimize/mod.rs +++ b/src/optimize/mod.rs @@ -1,2 +1,11 @@ +//! Optimizer-oriented helpers for pharmacometric workflows. +//! +//! This module provides optimization utilities built on [`argmin`]: +//! +//! - [`effect`] — Find the maximum effect (`E2`) for dual-site PD models +//! via Nelder‑Mead optimization in log‑space. +//! - [`parameters`] — Nelder‑Mead parameter refinement for an [`Equation`] +//! against a [`Data`] set and [`AssayErrorModels`]. + pub mod effect; pub mod parameters; diff --git a/src/optimize/parameters.rs b/src/optimize/parameters.rs index c64df672..fcf21a14 100644 --- a/src/optimize/parameters.rs +++ b/src/optimize/parameters.rs @@ -1,3 +1,11 @@ +//! Nelder‑Mead parameter refinement for pharmacometric models. +//! +//! This module provides a [`ParameterOptimizer`] that refines a single parameter +//! Given an [`Equation`], observed [`Data`], and [`AssayErrorModels`] via +//! Nelder‑Mead optimization in log‑space. The optimizer finds the parameter vector +//! that minimizes the negative log-likelihood of the model predictions against the data, +//! as measured by the provided error models. + use argmin::{ core::{CostFunction, Error, Executor}, solver::neldermead::NelderMead, @@ -7,6 +15,7 @@ use ndarray::{Array1, Axis}; use crate::{prelude::simulator::log_likelihood_matrix, AssayErrorModels, Data, Equation}; +/// Optimizer that refines a single parameter vector against observed data. pub struct ParameterOptimizer<'a, E: Equation> { equation: &'a E, data: &'a Data, @@ -44,6 +53,12 @@ impl CostFunction for ParameterOptimizer<'_, E> { } impl<'a, E: Equation> ParameterOptimizer<'a, E> { + /// Create a new optimizer. + /// + /// * `equation` — the model to evaluate. + /// * `data` — observed subject data. + /// * `sig` — assay error models per output. + /// * `pyl` — reference (target) likelihood vector. pub fn new( equation: &'a E, data: &'a Data, @@ -58,6 +73,8 @@ impl<'a, E: Equation> ParameterOptimizer<'a, E> { } } + /// Optimize the parameters to minimize the negative log-likelihood against the data. + pub fn optimize_point(self, parameters: Array1) -> Result, Error> { let simplex = create_initial_simplex(¶meters.to_vec()); let solver: NelderMead, f64> = NelderMead::new(simplex).with_sd_tolerance(1e-2)?;