Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
uses: Swatinem/rust-cache@v2
- name: Setup pages
id: pages
uses: actions/configure-pages@v5
uses: actions/configure-pages@v6
- name: Clean docs folder
run: cargo clean --doc
- name: Build docs
Expand All @@ -32,7 +32,7 @@ jobs:
- name: Remove lock file
run: rm target/doc/.lock
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
uses: actions/upload-pages-artifact@v5
with:
path: target/doc
deploy:
Expand All @@ -45,4 +45,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
24 changes: 20 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
/Cargo.lock
*.log
theta.csv
cycles.csv
pred.csv
obs.csv
time.csv
n_psi.csv
psi.csv
r.csv
correlation.csv
/docs
diagnostics.json
predictions.csv
summary.csv
summary.json
iterations.csv
population.csv
shrinkage.csv
statistics.csv
posterior.csv
simulation_output.csv
/examples/rosuva/*
Expand All @@ -17,6 +25,8 @@ simulation_output.csv
/examples/data/iohexol*
/examples/data/rosuva*
/examples/data/vori*
/examples/paper_benchmarks
/examples/*/output
/.idea
stop
.vscode
Expand All @@ -28,7 +38,13 @@ settings.json
log.txt
op.csv
*results.txt
covs.csv
covariates.csv
individual_effects.csv
individual_parameters.csv
residual_error.csv
error_theta.csv
lcov.info
Fortran/
Fortran/
paper/
docs/
examples/**/outputs/
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.25.2](https://github.com/LAPKB/PMcore/compare/v0.25.1...v0.25.2) - 2026-04-20

### Fixed

- Add wrappers for analytical solutions ([#272](https://github.com/LAPKB/PMcore/pull/272))

## [0.25.1](https://github.com/LAPKB/PMcore/compare/v0.25.0...v0.25.1) - 2026-04-13

### Other

- Update rand requirement from 0.9.0 to 0.10.1 ([#270](https://github.com/LAPKB/PMcore/pull/270))

## [0.25.0](https://github.com/LAPKB/PMcore/compare/v0.24.0...v0.25.0) - 2026-04-11

### Added

- Bump pharmsol and update examples ([#269](https://github.com/LAPKB/PMcore/pull/269))
- Use pharmsol 0.25 ([#268](https://github.com/LAPKB/PMcore/pull/268))

### Other

- Update faer requirement from 0.23.1 to 0.24.0 ([#241](https://github.com/LAPKB/PMcore/pull/241))
- Update rand requirement from 0.9.0 to 0.10.0 ([#244](https://github.com/LAPKB/PMcore/pull/244))

## [0.24.0](https://github.com/LAPKB/PMcore/compare/v0.23.0...v0.24.0) - 2026-04-01

### Added
Expand Down
8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pmcore"
version = "0.24.0"
version = "0.25.2"
edition = "2021"
authors = [
"Julián D. Otálvaro <juliandavid347@gmail.com>",
Expand All @@ -27,11 +27,11 @@ tracing-subscriber = { version = "0.3.19", features = [
"fmt",
"time",
] }
faer = "0.23.1"
pharmsol = "=0.24.1"
rand = "0.9.0"
faer = "0.24.0"
pharmsol = "=0.26.1"
anyhow = "1.0.100"
rayon = "1.10.0"
rand = "0.10.1"

[features]
default = []
Expand Down
65 changes: 32 additions & 33 deletions benches/bimodal_ke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ fn create_equation() -> equation::ODE {
)
}

fn create_parameters() -> Parameters {
Parameters::new()
.add("ke", 0.001, 3.0)
.add("v", 25.0, 250.0)
fn create_parameter_space() -> ParameterSpace {
ParameterSpace::new()
.add(ParameterSpec::bounded("ke", 0.001, 3.0))
.add(ParameterSpec::bounded("v", 25.0, 250.0))
}

fn create_error_models() -> Result<AssayErrorModels> {
Expand All @@ -37,51 +37,50 @@ fn load_data() -> Result<data::Data> {
Ok(data::read_pmetrics("examples/bimodal_ke/bimodal_ke.csv")?)
}

fn setup_with_algorithm(algorithm: Algorithm) -> Result<(Settings, equation::ODE, data::Data)> {
let params = create_parameters();
fn setup_with_algorithm(method: NonparametricMethod) -> Result<EstimationProblem<equation::ODE>> {
let ems = create_error_models()?;

let mut settings = Settings::builder()
.set_algorithm(algorithm)
.set_parameters(params)
.set_error_models(ems)
.build();

settings.set_cycles(1000);
settings.set_prior(Prior::sobol(2048, 22));
settings.disable_output();
settings.set_progress(false);

let observations = ObservationSpec::new()
.add_channel(ObservationChannel::continuous(1, "cp"))
.with_assay_error_models(ems);
let model = ModelDefinition::builder(create_equation())
.parameters(create_parameter_space())
.observations(observations)
.build()?;
let data = load_data()?;
Ok((settings, create_equation(), data))
EstimationProblem::builder(model, data)
.method(EstimationMethod::Nonparametric(method))
.output(OutputPlan::disabled())
.runtime(RuntimeOptions {
cycles: 1000,
progress: false,
prior: Some(Prior::sobol(2048, 22)),
..RuntimeOptions::default()
})
.build()
}

fn setup_npag() -> Result<(Settings, equation::ODE, data::Data)> {
setup_with_algorithm(Algorithm::NPAG)
fn setup_npag() -> Result<EstimationProblem<equation::ODE>> {
setup_with_algorithm(NonparametricMethod::Npag(NpagOptions))
}

fn setup_npod() -> Result<(Settings, equation::ODE, data::Data)> {
setup_with_algorithm(Algorithm::NPOD)
fn setup_npod() -> Result<EstimationProblem<equation::ODE>> {
setup_with_algorithm(NonparametricMethod::Npod(NpodOptions))
}

fn setup_postprob() -> Result<(Settings, equation::ODE, data::Data)> {
setup_with_algorithm(Algorithm::POSTPROB)
fn setup_postprob() -> Result<EstimationProblem<equation::ODE>> {
setup_with_algorithm(NonparametricMethod::Postprob(PostProbOptions))
}

fn benchmark_algorithm<F>(c: &mut Criterion, bench_name: &str, setup_fn: F)
where
F: Fn() -> Result<(Settings, equation::ODE, data::Data)>,
F: Fn() -> Result<EstimationProblem<equation::ODE>>,
{
let (settings, eq, data) = setup_fn().unwrap();
let problem = setup_fn().unwrap();

c.bench_function(bench_name, |b| {
b.iter_with_setup(
|| (settings.clone(), eq.clone(), data.clone()),
|(s, e, d)| {
let mut algorithm = dispatch_algorithm(s, e, d).unwrap();
let result = algorithm.fit().unwrap();
black_box(result)
},
|| problem.clone(),
|problem| black_box(problem.run().unwrap()),
)
});
}
Expand Down
31 changes: 7 additions & 24 deletions examples/bestdose.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use anyhow::Result;
use pmcore::bestdose::{BestDosePosterior, DoseRange, Target};

use pmcore::bestdose::{BestDoseConfig, BestDosePosterior, DoseRange, Target};
use pmcore::prelude::*;
use pmcore::routines::initialization::parse_prior;

fn main() -> Result<()> {
// Example model
Expand All @@ -18,23 +16,15 @@ fn main() -> Result<()> {
},
};

let params = Parameters::new()
.add("ke", 0.001, 3.0)
.add("v", 25.0, 250.0);
let parameter_space = ParameterSpace::new()
.add(ParameterSpec::bounded("ke", 0.001, 3.0))
.add(ParameterSpec::bounded("v", 25.0, 250.0));

let ems = AssayErrorModels::new().add(
0,
AssayErrorModel::additive(ErrorPoly::new(0.0, 0.20, 0.0, 0.0), 0.0),
)?;

// Make settings
let mut settings = Settings::builder()
.set_algorithm(Algorithm::NPAG)
.set_parameters(params)
.set_error_models(ems.clone())
.build();

settings.disable_output();
let config = BestDoseConfig::new(parameter_space.clone(), ems.clone()).with_progress(false);

// Generate a patient with known parameters
// Ke = 0.5, V = 50
Expand Down Expand Up @@ -71,21 +61,14 @@ fn main() -> Result<()> {
.observation(18.0, conc(6.0, 75.0) + conc(18.0, 150.0), 0)
.build();

let (theta, prior) = parse_prior(
&"examples/bimodal_ke/output/theta.csv".to_string(),
&settings,
)
.unwrap();
let (theta, prior) = read_prior("examples/bimodal_ke/output/theta.csv", &parameter_space)?;

// Example usage - two-stage API:
// Stage 1: Compute posterior (expensive, done once)
// Stage 2: Optimize doses (can be called multiple times with different params)
let posterior = BestDosePosterior::compute(
&theta,
&prior.unwrap(),
Some(past_data.clone()), // Optional: past data for Bayesian updating
eq.clone(),
settings.clone(),
config.clone(),
)?;

println!("Optimizing dose...");
Expand Down
39 changes: 16 additions & 23 deletions examples/bestdose_auc.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use anyhow::Result;
use pmcore::bestdose::{BestDosePosterior, DoseRange, Target};
use pmcore::bestdose::{BestDoseConfig, BestDosePosterior, DoseRange, Target};
use pmcore::prelude::*;
use pmcore::routines::initialization::parse_prior;

fn main() -> Result<()> {
tracing_subscriber::fmt::init();
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::new("info,diffsol=off"))
.init();

println!("BestDose AUC Target - Minimal Example\n");
println!("======================================\n");
Expand All @@ -22,30 +23,22 @@ fn main() -> Result<()> {
};

// Minimal parameter ranges
let params = Parameters::new()
.add("ke", 0.001, 3.0)
.add("v", 25.0, 250.0);
let parameter_space = ParameterSpace::new()
.add(ParameterSpec::bounded("ke", 0.001, 3.0))
.add(ParameterSpec::bounded("v", 25.0, 250.0));

let ems = AssayErrorModels::new().add(
0,
AssayErrorModel::additive(ErrorPoly::new(0.0, 0.20, 0.0, 0.0), 0.0),
)?;

let mut settings = Settings::builder()
.set_algorithm(Algorithm::NPAG)
.set_parameters(params)
.set_error_models(ems.clone())
.build();

settings.disable_output();
settings.set_idelta(60.0); // 1 hour intervals for AUC calculation
let config = BestDoseConfig::new(parameter_space.clone(), ems.clone())
.with_progress(false)
.with_prediction_interval(60.0);

// Load realistic prior from previous NPAG run (47 support points)
println!("Loading prior from bimodal_ke example...");
let (theta, prior) = parse_prior(
&"examples/bimodal_ke/output/theta.csv".to_string(),
&settings,
)?;
let (theta, prior) = read_prior("examples/bimodal_ke/output/theta.csv", &parameter_space)?;
let weights = prior.as_ref().unwrap();

println!("Prior: {} support points\n", theta.matrix().nrows());
Expand All @@ -67,16 +60,16 @@ fn main() -> Result<()> {
weights,
None, // No past data - use prior directly
eq.clone(),
settings.clone(),
config.clone(),
)?;

println!("Optimizing dose...\n");
let optimal = posterior.optimize(
target_data.clone(),
None,
DoseRange::new(100.0, 2000.0), // Wider range for AUC targets
0.8, // for AUC targets higher bias_weight usually works best
Target::AUCFromZero, // Cumulative AUC from time 0
DoseRange::new(100.0, 2000.0),
0.8,
Target::AUCFromZero,
)?;

let opt_doses = optimal.doses();
Expand Down Expand Up @@ -143,7 +136,7 @@ fn main() -> Result<()> {
None,
DoseRange::new(50.0, 500.0),
0.8,
Target::AUCFromLastDose, // Interval AUC from last dose!
Target::AUCFromLastDose,
)?;

let doses: Vec<f64> = optimal_interval.doses();
Expand Down
Loading
Loading