Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0380cd0
NPSAH v0.1
Siel Dec 13, 2025
38cf249
WIP: Testing new algorithms
Siel Dec 14, 2025
2637a12
basic struct
Siel Dec 15, 2025
dd1b093
wip: SAEM's outputs
Siel Jan 2, 2026
4d3040d
wip: cleaning outputs
Siel Jan 2, 2026
411833d
wip: cleaning up
Siel Jan 3, 2026
c529859
wip: cleaning up
Siel Jan 3, 2026
270c364
playing with non-parametric algorithms
Siel Jan 22, 2026
394c6b5
use git dependency
Siel Feb 5, 2026
cf4476d
use git dependency
Siel Feb 5, 2026
cc305bd
wip: saving progress so far
Siel Mar 6, 2026
c11d38f
gh dependency
Siel Mar 10, 2026
bd44d55
Merge branch 'main' into parametric
Siel Mar 25, 2026
fda9b20
merged main
Siel Mar 25, 2026
388a950
basic saem example seems to be working
Siel Mar 26, 2026
013bec5
Full redesign of PMcore's structure. This was done because in the pre…
Siel Mar 27, 2026
dd4be93
remove bloat
Siel Mar 27, 2026
708d8c7
cleaning a bit more
Siel Mar 27, 2026
8c90072
remove bloat
Siel Mar 27, 2026
f9064aa
update .gitignore
Siel Mar 27, 2026
bd7a88b
fmt
Siel Mar 27, 2026
60d74c8
chore: merged main into unified-platform
Siel Apr 1, 2026
c046f1a
chore: fmt
Siel Apr 1, 2026
3d0cfa8
wip:saving current state
Siel Apr 9, 2026
5922da4
use release pharmsol
Siel Apr 11, 2026
0a4d961
chore: sync local changes for unified-platform-m1
Siel Apr 13, 2026
15c26b4
Merge origin/main into feature/unified-platform-m1-main-prep
Siel Apr 28, 2026
bb4b6d4
Extract unified platform structure branch
Siel Apr 28, 2026
ccad512
Restore parametric unified platform layer
Siel Apr 28, 2026
1b0624d
Align parametric split with prep tree
Siel Apr 28, 2026
9d8686e
Run cargo fmt
Siel Apr 28, 2026
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
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/
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,22 @@ pharmsol = "=0.26.1"
anyhow = "1.0.100"
rayon = "1.10.0"
rand = "0.10.1"
cobyla = "0.8.0"
rand_distr = "0.6.0"
statrs = "0.18"

[features]
default = []
json = ["pharmsol/json"]
exa = ["pharmsol/exa"]

[profile.release]
codegen-units = 1
opt-level = 3

[profile.test]
inherits = "release"

[dev-dependencies]
approx = "0.5.1"
criterion = { version = "0.8" }
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