From 5442dc6b18c40a85817803bdf133b4a22584c8ca Mon Sep 17 00:00:00 2001 From: Markus <66058642+mhovd@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:02:41 +0200 Subject: [PATCH 1/2] feat: Update to pharmsol 0.29.0 Breaking change because of features --- Cargo.toml | 11 +- examples/bestdose/main.rs | 5 +- examples/bimodal_ke_backend_compare.rs | 315 ------------------------- src/model/mod.rs | 6 +- tests/onecomp.rs | 2 +- 5 files changed, 7 insertions(+), 332 deletions(-) delete mode 100644 examples/bimodal_ke_backend_compare.rs diff --git a/Cargo.toml b/Cargo.toml index 654af06e4..37cf1ebf3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ tracing-subscriber = { version = "0.3.19", features = [ "time", ] } faer = "0.24.0" -pharmsol = { version = "=0.28.8" } +pharmsol = { version = "=0.29.0" } anyhow = "1.0.100" rayon = "1.10.0" rand = "0.10.1" @@ -37,10 +37,7 @@ rand = "0.10.1" default = [] json = [] exa = [] -dsl-core = ["pharmsol/dsl-core"] -dsl-jit = ["pharmsol/dsl-jit"] -dsl-aot = ["pharmsol/dsl-aot"] -dsl-aot-load = ["pharmsol/dsl-aot-load"] +dsl = ["pharmsol/dsl"] [profile.release] codegen-units = 1 @@ -59,8 +56,8 @@ name = "bimodal_ke" harness = false # The BestDose example defines its model from a DSL text string, which requires -# a runtime DSL backend. Build/run it with: `--features dsl-jit`. +# a runtime DSL backend. Build/run it with: `--features dsl`. [[example]] name = "bestdose" path = "examples/bestdose/main.rs" -required-features = ["dsl-jit"] +required-features = ["dsl"] diff --git a/examples/bestdose/main.rs b/examples/bestdose/main.rs index 82a682b31..ed1ac12e2 100644 --- a/examples/bestdose/main.rs +++ b/examples/bestdose/main.rs @@ -1,7 +1,5 @@ use anyhow::Result; -use pharmsol::dsl::{ - compile_module_source_to_runtime, CompiledRuntimeModel, RuntimeCompilationTarget, -}; +use pharmsol::dsl::{compile_module_source_to_runtime, CompiledRuntimeModel}; use pharmsol::Equation; use pmcore::bestdose::{BestDoseOptions, BestDoseProblem, DoseRange, Target}; use pmcore::prelude::*; @@ -34,7 +32,6 @@ fn main() -> Result<()> { let eq = match compile_module_source_to_runtime( MODEL_SOURCE, Some("bestdose_one_compartment"), - RuntimeCompilationTarget::Jit, |_, _| {}, ) .map_err(|e| anyhow::anyhow!("failed to compile DSL model: {e}"))? diff --git a/examples/bimodal_ke_backend_compare.rs b/examples/bimodal_ke_backend_compare.rs deleted file mode 100644 index 81cf95807..000000000 --- a/examples/bimodal_ke_backend_compare.rs +++ /dev/null @@ -1,315 +0,0 @@ -use std::time::{Duration, Instant}; - -use anyhow::{anyhow, Result}; -use pharmsol::Cache; -use pmcore::prelude::*; - -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load"), - feature = "dsl-wasm" -))] -use anyhow::{bail, Context}; -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load"), - feature = "dsl-wasm" -))] -use pharmsol::dsl::{self, CompiledRuntimeModel, RuntimeBackend, RuntimeCompilationTarget}; -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load"), - feature = "dsl-wasm" -))] -use std::fs; -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load"), - feature = "dsl-wasm" -))] -use std::path::PathBuf; -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load"), - feature = "dsl-wasm" -))] -use std::time::{SystemTime, UNIX_EPOCH}; - -const DATA_PATH: &str = "examples/bimodal_ke/bimodal_ke.csv"; -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load"), - feature = "dsl-wasm" -))] -const BIMODAL_KE_DSL: &str = r#" -name = bimodal_ke -kind = ode - -params = ke, v -states = central -outputs = outeq_1 - -infusion(input_1) -> central - -dx(central) = -ke * central - -out(outeq_1) = central / v -"#; - -#[derive(Debug, Clone)] -struct ComparisonResult { - label: &'static str, - compile_time: Duration, - fit_time: Duration, - total_time: Duration, - objf: f64, -} - -fn main() -> Result<()> { - let mut results = Vec::new(); - results.push(run_legacy()?); - results.push(run_macro()?); - - #[cfg(feature = "dsl-jit")] - results.push(run_runtime_jit()?); - - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - results.push(run_runtime_native_aot()?); - - #[cfg(feature = "dsl-wasm")] - results.push(run_runtime_wasm()?); - - print_summary(&results)?; - - Ok(()) -} - -fn legacy_equation() -> equation::ODE { - equation::ODE::new( - |x, p, _t, dx, _bolus, rateiv, _cov| { - fetch_params!(p, ke, _v); - dx[0] = -ke * x[0] + rateiv[0]; - }, - |_p, _t, _cov| lag! {}, - |_p, _t, _cov| fa! {}, - |_p, _t, _cov, _x| {}, - |x, p, _t, _cov, y| { - fetch_params!(p, _ke, v); - y[0] = x[0] / v; - }, - ) - .with_nstates(1) - .with_ndrugs(1) - .with_nout(1) - .with_metadata( - equation::metadata::new("bimodal_ke_legacy") - .parameters(["ke", "v"]) - .states(["central"]) - .outputs(["outeq_1"]) - .route( - equation::Route::infusion("input_1") - .to_state("central") - .inject_input_to_destination(), - ), - ) - .expect("legacy bimodal_ke metadata should validate") - .enable_cache() - .with_solver(OdeSolver::ExplicitRk(ExplicitRkTableau::Tsit45)) -} - -fn macro_equation() -> equation::ODE { - ode! { - name: "bimodal_ke", - params: [ke, v], - states: [central], - outputs: [outeq_1], - routes: [ - infusion(input_1) -> central, - ], - diffeq: |x, _t, dx| { - dx[central] = -ke * x[central]; - }, - out: |x, _t, y| { - y[outeq_1] = x[central] / v; - }, - } - .enable_cache() - .with_solver(OdeSolver::ExplicitRk(ExplicitRkTableau::Tsit45)) -} - -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load"), - feature = "dsl-wasm" -))] -fn artifact_root(name: &str) -> Result { - let stamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .context("system clock is before unix epoch")? - .as_millis(); - let path = std::env::current_dir()? - .join("target") - .join("bimodal_ke_backend_compare") - .join(format!("{name}-{stamp}-{}", std::process::id())); - fs::create_dir_all(&path)?; - Ok(path) -} - -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load"), - feature = "dsl-wasm" -))] -fn compile_runtime_ode( - label: &'static str, - expected_backend: RuntimeBackend, - target: RuntimeCompilationTarget, -) -> Result<(pharmsol::dsl::RuntimeOdeModel, Duration)> { - let started = Instant::now(); - let compiled = dsl::compile_module_source_to_runtime( - BIMODAL_KE_DSL, - Some("bimodal_ke"), - target, - |_, _| {}, - )?; - let compile_time = started.elapsed(); - - let model = match compiled { - CompiledRuntimeModel::Ode(model) => model - .enable_cache() - .with_solver(OdeSolver::ExplicitRk(ExplicitRkTableau::Tsit45)), - CompiledRuntimeModel::Analytical(_) => { - bail!("{label} compiled bimodal_ke as analytical instead of ode") - } - CompiledRuntimeModel::Sde(_) => bail!("{label} compiled bimodal_ke as sde instead of ode"), - }; - - if model.backend() != expected_backend { - bail!( - "{label} compiled to {:?}, expected {:?}", - model.backend(), - expected_backend - ); - } - - Ok((model, compile_time)) -} - -fn run_case( - label: &'static str, - compile_time: Duration, - equation: E, -) -> Result { - let data = data::read_pmetrics(DATA_PATH)?; - let fit_started = Instant::now(); - let parameters = ParameterSpace::bounded() - .add("ke", 0.001, 3.0) - .add("v", 25.0, 250.0); - let prior = Theta::sobol_default(¶meters)?; - let error_models = AssayErrorModels::new().add( - "outeq_1", - AssayErrorModel::additive(ErrorPoly::new(0.0, 0.5, 0.0, 0.0), 0.0), - )?; - let result = EstimationProblem::nonparametric(equation, data, prior, error_models)? - .fit_with(NonParametricAlgorithm::npag())?; - let fit_time = fit_started.elapsed(); - - summarize_result(label, compile_time, fit_time, &result) -} - -fn summarize_result( - label: &'static str, - compile_time: Duration, - fit_time: Duration, - result: &NonParametricResult, -) -> Result { - Ok(ComparisonResult { - label, - compile_time, - fit_time, - total_time: compile_time + fit_time, - objf: result.objf(), - }) -} - -fn run_legacy() -> Result { - let started = Instant::now(); - let equation = legacy_equation(); - run_case("legacy-handwritten", started.elapsed(), equation) -} - -fn run_macro() -> Result { - let started = Instant::now(); - let equation = macro_equation(); - run_case("macro-ode", started.elapsed(), equation) -} - -#[cfg(feature = "dsl-jit")] -fn run_runtime_jit() -> Result { - let (equation, compile_time) = compile_runtime_ode( - "dsl-jit", - RuntimeBackend::Jit, - RuntimeCompilationTarget::Jit, - )?; - run_case("dsl-jit", compile_time, equation) -} - -#[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] -fn run_runtime_native_aot() -> Result { - let root = artifact_root("native-aot")?; - let (equation, compile_time) = compile_runtime_ode( - "dsl-native-aot", - RuntimeBackend::NativeAot, - RuntimeCompilationTarget::NativeAot( - dsl::NativeAotCompileOptions::new(root.join("build")) - .with_output(root.join("bimodal_ke_runtime_aot.pkm")), - ), - )?; - run_case("dsl-native-aot", compile_time, equation) -} - -#[cfg(feature = "dsl-wasm")] -fn run_runtime_wasm() -> Result { - let (equation, compile_time) = compile_runtime_ode( - "dsl-wasm", - RuntimeBackend::Wasm, - RuntimeCompilationTarget::Wasm, - )?; - run_case("dsl-wasm", compile_time, equation) -} - -fn print_summary(results: &[ComparisonResult]) -> Result<()> { - let baseline = results - .iter() - .find(|result| result.label == "legacy-handwritten") - .ok_or_else(|| anyhow!("missing legacy-handwritten baseline"))?; - - println!("bimodal_ke NPAG backend comparison"); - println!("dataset: {DATA_PATH}"); - println!("cache: on"); - #[cfg(not(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load"), - feature = "dsl-wasm" - )))] - println!("runtime DSL backends skipped; enable dsl-jit, dsl-aot+dsl-aot-load, or dsl-wasm to include them"); - println!(); - println!( - "{:<18} {:>12} {:>12} {:>12} {:>14} {:>14}", - "representation", "compile s", "fit s", "total s", "objf diff", "objf" - ); - - for result in results { - println!( - "{:<18} {:>12.3} {:>12.3} {:>12.3} {:>14.6} {:>14.6}", - result.label, - result.compile_time.as_secs_f64(), - result.fit_time.as_secs_f64(), - result.total_time.as_secs_f64(), - (result.objf - baseline.objf).abs(), - result.objf, - ); - } - - Ok(()) -} diff --git a/src/model/mod.rs b/src/model/mod.rs index 861d16f1c..9467057ec 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -139,11 +139,7 @@ impl_metadata_opt!(ODE, Analytical, SDE); macro_rules! impl_metadata_some { ($($t:ty),+) => { $( - #[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load"), - all(feature = "dsl-wasm", not(all(target_arch = "wasm32", target_os = "unknown"))) - ))] + #[cfg(feature = "dsl")] impl EquationMetadataSource for $t { fn equation_metadata(&self) -> Option<&ValidatedModelMetadata> { Some(self.metadata()) diff --git a/tests/onecomp.rs b/tests/onecomp.rs index 4520ab4b9..c63509b52 100644 --- a/tests/onecomp.rs +++ b/tests/onecomp.rs @@ -131,7 +131,7 @@ fn test_one_compartment_npod() -> Result<()> { .fit_with(NonParametricAlgorithm::npod())?; // Check the results - assert_eq!(result.cycles(), 12); + assert_eq!(result.cycles(), 11); assert!(result.objf() - 565.7749 < 0.01); Ok(()) From e43b315592fef0c3226ea6aa0832dfed022e730c Mon Sep 17 00:00:00 2001 From: Markus <66058642+mhovd@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:53:17 +0200 Subject: [PATCH 2/2] Restore example --- Cargo.toml | 5 + examples/bimodal_ke_backend_compare.rs | 181 +++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 examples/bimodal_ke_backend_compare.rs diff --git a/Cargo.toml b/Cargo.toml index 37cf1ebf3..8854a4c1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,3 +61,8 @@ harness = false name = "bestdose" path = "examples/bestdose/main.rs" required-features = ["dsl"] + +[[example]] +name = "bimodal_ke_backend_compare" +path = "examples/bimodal_ke_backend_compare.rs" +required-features = ["dsl"] diff --git a/examples/bimodal_ke_backend_compare.rs b/examples/bimodal_ke_backend_compare.rs new file mode 100644 index 000000000..0a5488aae --- /dev/null +++ b/examples/bimodal_ke_backend_compare.rs @@ -0,0 +1,181 @@ +use std::time::{Duration, Instant}; + +use anyhow::{anyhow, Result}; +use pharmsol::dsl::{compile_module_source_to_runtime, CompiledRuntimeModel, RuntimeOdeModel}; +use pharmsol::Cache; +use pmcore::prelude::*; + +const DATA_PATH: &str = "examples/bimodal_ke/bimodal_ke.csv"; + +const BIMODAL_KE_DSL: &str = r#" +name = bimodal_ke +kind = ode + +params = ke, v +states = central +outputs = outeq_1 + +infusion(input_1) -> central + +dx(central) = -ke * central + +out(outeq_1) = central / v +"#; + +#[derive(Debug, Clone)] +struct ComparisonResult { + label: &'static str, + compile_time: Duration, + fit_time: Duration, + total_time: Duration, + objf: f64, +} + +fn main() -> Result<()> { + let results = vec![run_legacy()?, run_macro()?, run_dsl()?]; + + print_summary(&results)?; + + Ok(()) +} + +fn legacy_equation() -> equation::ODE { + equation::ODE::new( + |x, p, _t, dx, _bolus, rateiv, _cov| { + fetch_params!(p, ke, _v); + dx[0] = -ke * x[0] + rateiv[0]; + }, + |_p, _t, _cov| lag! {}, + |_p, _t, _cov| fa! {}, + |_p, _t, _cov, _x| {}, + |x, p, _t, _cov, y| { + fetch_params!(p, _ke, v); + y[0] = x[0] / v; + }, + ) + .with_nstates(1) + .with_ndrugs(1) + .with_nout(1) + .with_metadata( + equation::metadata::new("bimodal_ke_legacy") + .parameters(["ke", "v"]) + .states(["central"]) + .outputs(["outeq_1"]) + .route( + equation::Route::infusion("input_1") + .to_state("central") + .inject_input_to_destination(), + ), + ) + .expect("legacy bimodal_ke metadata should validate") + .enable_cache() + .with_solver(OdeSolver::ExplicitRk(ExplicitRkTableau::Tsit45)) +} + +fn macro_equation() -> equation::ODE { + ode! { + name: "bimodal_ke", + params: [ke, v], + states: [central], + outputs: [outeq_1], + routes: [ + infusion(input_1) -> central, + ], + diffeq: |x, _t, dx| { + dx[central] = -ke * x[central]; + }, + out: |x, _t, y| { + y[outeq_1] = x[central] / v; + }, + } + .enable_cache() + .with_solver(OdeSolver::ExplicitRk(ExplicitRkTableau::Tsit45)) +} + +fn dsl_equation() -> Result { + let compiled = compile_module_source_to_runtime(BIMODAL_KE_DSL, Some("bimodal_ke"), |_, _| {}) + .map_err(|e| anyhow!("failed to compile DSL model: {e}"))?; + + match compiled { + CompiledRuntimeModel::Ode(model) => Ok(model + .enable_cache() + .with_solver(OdeSolver::ExplicitRk(ExplicitRkTableau::Tsit45))), + other => Err(anyhow!("expected an ODE model, got {:?}", other.kind())), + } +} + +fn run_case( + label: &'static str, + compile_time: Duration, + equation: E, +) -> Result { + let data = data::read_pmetrics(DATA_PATH)?; + let fit_started = Instant::now(); + let parameters = ParameterSpace::bounded() + .add("ke", 0.001, 3.0) + .add("v", 25.0, 250.0); + let prior = Theta::sobol_default(¶meters)?; + let error_models = AssayErrorModels::new().add( + "outeq_1", + AssayErrorModel::additive(ErrorPoly::new(0.0, 0.5, 0.0, 0.0), 0.0), + )?; + let result = EstimationProblem::nonparametric(equation, data, prior, error_models)? + .fit_with(NonParametricAlgorithm::npag())?; + let fit_time = fit_started.elapsed(); + + Ok(ComparisonResult { + label, + compile_time, + fit_time, + total_time: compile_time + fit_time, + objf: result.objf(), + }) +} + +fn run_legacy() -> Result { + let started = Instant::now(); + let equation = legacy_equation(); + run_case("handwritten", started.elapsed(), equation) +} + +fn run_macro() -> Result { + let started = Instant::now(); + let equation = macro_equation(); + run_case("macro-ode", started.elapsed(), equation) +} + +fn run_dsl() -> Result { + let started = Instant::now(); + let equation = dsl_equation()?; + run_case("dsl-runtime", started.elapsed(), equation) +} + +fn print_summary(results: &[ComparisonResult]) -> Result<()> { + let baseline = results + .iter() + .find(|result| result.label == "handwritten") + .ok_or_else(|| anyhow!("missing handwritten baseline"))?; + + println!("bimodal_ke NPAG backend comparison"); + println!("dataset: {DATA_PATH}"); + println!("cache: on"); + println!(); + println!( + "{:<18} {:>12} {:>12} {:>12} {:>14} {:>14}", + "representation", "compile s", "fit s", "total s", "objf diff", "objf" + ); + + for result in results { + println!( + "{:<18} {:>12.3} {:>12.3} {:>12.3} {:>14.6} {:>14.6}", + result.label, + result.compile_time.as_secs_f64(), + result.fit_time.as_secs_f64(), + result.total_time.as_secs_f64(), + (result.objf - baseline.objf).abs(), + result.objf, + ); + } + + Ok(()) +}