Skip to content
Open
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
174 changes: 174 additions & 0 deletions examples/api_comparison.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
//! Comparison example demonstrating both the old (tuple-based) and new (builder) APIs.
//!
//! This example shows that both APIs produce identical results, ensuring backward compatibility
//! while providing a more ergonomic builder pattern for new code.
//!
//! The new builder API uses the type-state pattern to enforce required fields at compile time,
//! while making optional fields (lag, fa, init) truly optional with sensible defaults.

use pharmsol::prelude::models::one_compartment;
use pharmsol::*;

fn main() {
println!("=== API Comparison: Old (Tuple) vs New (Builder) ===\n");

// Create a simple subject for testing
let subject = Subject::builder("comparison_test")
.infusion(0.0, 500.0, 0, 0.5)
.observation(0.5, 0.0, 0)
.observation(1.0, 0.0, 0)
.observation(2.0, 0.0, 0)
.observation(4.0, 0.0, 0)
.observation(8.0, 0.0, 0)
.build();

let params = vec![1.02282724609375, 194.51904296875]; // ke, v

println!("--- ODE Models ---");

let ode_old = equation::ODE::new(
|x, p, _t, dx, _b, 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;
},
(1, 1),
);

let ode_minimal = equation::ODE::builder()
.diffeq(|x, p, _t, dx, _b, rateiv, _cov| {
fetch_params!(p, ke, _v);
dx[0] = -ke * x[0] + rateiv[0];
})
.out(|x, p, _t, _cov, y| {
fetch_params!(p, _ke, v);
y[0] = x[0] / v;
})
.nstates(1)
.nouteqs(1)
.build();

// Also show full specification with optional fields
let ode_full = equation::ODE::builder()
.diffeq(|x, p, _t, dx, _b, rateiv, _cov| {
fetch_params!(p, ke, _v);
dx[0] = -ke * x[0] + rateiv[0];
})
.out(|x, p, _t, _cov, y| {
fetch_params!(p, _ke, v);
y[0] = x[0] / v;
})
.neqs(Neqs::new(1, 1)) // Can also use Neqs struct
.build();

// Compare predictions
let pred_old = ode_old.estimate_predictions(&subject, &params).unwrap();
let pred_minimal = ode_minimal.estimate_predictions(&subject, &params).unwrap();
let pred_full = ode_full.estimate_predictions(&subject, &params).unwrap();

println!("ODE Predictions (Old API with all 6 args):");
for p in pred_old.flat_predictions() {
print!("{:.9} ", p);
}
println!("\n");

println!("ODE Predictions (New Builder - minimal, 4 required fields only):");
for p in pred_minimal.flat_predictions() {
print!("{:.9} ", p);
}
println!("\n");

println!("ODE Predictions (New Builder - all fields explicit):");
for p in pred_full.flat_predictions() {
print!("{:.9} ", p);
}
println!("\n");

// Verify they match
let old_preds = pred_old.flat_predictions();
let minimal_preds = pred_minimal.flat_predictions();
let full_preds = pred_full.flat_predictions();

let all_match = old_preds
.iter()
.zip(minimal_preds.iter())
.zip(full_preds.iter())
.all(|((a, b), c)| (a - b).abs() < 1e-12 && (a - c).abs() < 1e-12);

println!("All ODE APIs produce identical results: {} ✓", all_match);
println!();

// =========================================================================
// Analytical: Old API (tuple-based)
// =========================================================================
println!("--- Analytical Models ---");

let analytical_old = equation::Analytical::new(
one_compartment,
|_p, _t, _cov| {},
|_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;
},
(1, 1), // Old tuple-based Neqs
);

// =========================================================================
// Analytical: New API (builder pattern) - MINIMAL version
// Required fields: eq, seq_eq, out, nstates, nouteqs
// =========================================================================
let analytical_minimal = equation::Analytical::builder()
.eq(one_compartment)
.seq_eq(|_p, _t, _cov| {})
.out(|x, p, _t, _cov, y| {
fetch_params!(p, _ke, v);
y[0] = x[0] / v;
})
.nstates(1)
.nouteqs(1)
.build();

// Compare predictions
let an_pred_old = analytical_old
.estimate_predictions(&subject, &params)
.unwrap();
let an_pred_minimal = analytical_minimal
.estimate_predictions(&subject, &params)
.unwrap();

println!("Analytical Predictions (Old API with all 7 args):");
for p in an_pred_old.flat_predictions() {
print!("{:.9} ", p);
}
println!("\n");

println!("Analytical Predictions (New Builder - minimal, 5 required fields only):");
for p in an_pred_minimal.flat_predictions() {
print!("{:.9} ", p);
}
println!("\n");

// Verify they match
let an_old_preds = an_pred_old.flat_predictions();
let an_minimal_preds = an_pred_minimal.flat_predictions();

let analytical_match = an_old_preds
.iter()
.zip(an_minimal_preds.iter())
.all(|(a, b)| (a - b).abs() < 1e-12);

println!(
"Analytical APIs produce identical results: {} ✓",
analytical_match
);
println!();
}
36 changes: 16 additions & 20 deletions examples/bke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,30 @@ fn main() {
.missing_observation(12.0, 0)
.build();

let an = equation::Analytical::new(
one_compartment,
|_p, _t, _cov| {},
|_p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|_p, _t, _cov, _x| {},
|x, p, _t, _cov, y| {
let an = equation::Analytical::builder()
.eq(one_compartment)
.seq_eq(|_p, _t, _cov| {})
.out(|x, p, _t, _cov, y| {
fetch_params!(p, _ke, v);
y[0] = x[0] / v;
},
(1, 1),
);
})
.nstates(1)
.nouteqs(1)
.build();

let ode = equation::ODE::new(
|x, p, _t, dx, _b, rateiv, _cov| {
let ode = equation::ODE::builder()
.diffeq(|x, p, _t, dx, _b, rateiv, _cov| {
// fetch_cov!(cov, t, wt);
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| {
})
.out(|x, p, _t, _cov, y| {
fetch_params!(p, _ke, v);
y[0] = x[0] / v;
},
(1, 1),
);
})
.nstates(1)
.nouteqs(1)
.build();

let mut ems = ErrorModels::new()
.add(
Expand Down
18 changes: 8 additions & 10 deletions examples/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
use pharmsol::*;

fn main() {
let ode = equation::ODE::new(
|x, p, t, dx, _b, rateiv, cov| {
let ode = equation::ODE::builder()
.diffeq(|x, p, t, dx, _b, rateiv, cov| {
fetch_cov!(cov, t, WT);
fetch_params!(p, CL0, V0, Vp0, Q0);

Expand All @@ -20,18 +20,16 @@ fn main() {

dx[0] = -Ke * x[0] - KCP * x[0] + KPC * x[1] + rateiv[0];
dx[1] = KCP * x[0] - KPC * x[1];
},
|p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|_p, _t, _cov, _x| {},
|x, p, t, cov, y| {
})
.out(|x, p, t, cov, y| {
fetch_cov!(cov, t, WT);
fetch_params!(p, CL0, V0, Vp0, Q0);
let V = V0 / (WT / 85.0);
y[0] = x[0] / V;
},
(2, 1),
);
})
.nstates(2)
.nouteqs(1)
.build();

let subject = data::Subject::builder("id1")
.infusion(0.0, 3235.0, 0, 0.005)
Expand Down
18 changes: 8 additions & 10 deletions examples/exa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,18 @@ fn main() {
.build();

// Create ODE model directly
let ode = equation::ODE::new(
|x, p, _t, dx, b, rateiv, _cov| {
let ode = equation::ODE::builder()
.diffeq(|x, p, _t, dx, b, rateiv, _cov| {
fetch_params!(p, ke, _v);
dx[0] = -ke * x[0] + rateiv[0] + b[0];
},
|_p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|_p, _t, _cov, _x| {},
|x, p, _t, _cov, y| {
})
.out(|x, p, _t, _cov, y| {
fetch_params!(p, _ke, v);
y[0] = x[0] / v;
},
(1, 1),
);
})
.nstates(1)
.nouteqs(1)
.build();

let test_dir = std::env::current_dir().expect("Failed to get current directory");
let model_output_path = test_dir.join("test_model.pkm");
Expand Down
30 changes: 16 additions & 14 deletions examples/gendata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,35 @@ fn main() {
.repeat(5, 0.2)
.build();

let sde = equation::SDE::new(
|x, p, _t, dx, _rateiv, _cov| {
// Type-state builder: only required fields + init (which is used here)
// lag and fa are optional and default to no-op
let sde = equation::SDE::builder()
.drift(|x, p, _t, dx, _rateiv, _cov| {
// automatically defined
fetch_params!(p, ke0);
// let ke0 = 1.2;
dx[1] = -x[1] + ke0;
let ke = x[1];
// user defined
dx[0] = -ke * x[0];
},
|p, d| {
})
.diffusion(|p, d| {
fetch_params!(p, _ke0);
d[1] = 0.1;
},
|_p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|p, _t, _cov, x| {
})
// init is specified because we need non-zero initial state
.init(|p, _t, _cov, x| {
fetch_params!(p, ke0);
x[1] = ke0;
},
|x, p, _t, _cov, y| {
})
.out(|x, p, _t, _cov, y| {
fetch_params!(p, _ke0);
y[0] = x[0] / 50.0;
},
(2, 1),
1,
);
})
.nstates(2)
.nouteqs(1)
.nparticles(1)
.build();

let ke_dist = rand_distr::Normal::new(1.2, 0.12).unwrap();
// let v_dist = rand_distr::Normal::new(50.0, 10.0).unwrap();
Expand Down
Loading