use pharmsol::prelude::models::{one_compartment, one_compartment_with_absorption};
use pharmsol::*;
const REL_TOL: f64 = 1e-2; const ABS_TOL: f64 = 1e-6;
fn parameters_for_analytical(
label: &str,
analytical: &equation::Analytical,
param_names: &[&str],
params: &[f64],
) -> Parameters {
assert_eq!(
param_names.len(),
params.len(),
"{label}: expected {} parameter value(s), got {}",
param_names.len(),
params.len()
);
Parameters::with_model(
analytical,
param_names.iter().copied().zip(params.iter().copied()),
)
.unwrap_or_else(|error| panic!("{label}: analytical parameters should validate: {error}"))
}
fn parameters_for_ode(
label: &str,
ode: &equation::ODE,
param_names: &[&str],
params: &[f64],
) -> Parameters {
assert_eq!(
param_names.len(),
params.len(),
"{label}: expected {} parameter value(s), got {}",
param_names.len(),
params.len()
);
Parameters::with_model(ode, param_names.iter().copied().zip(params.iter().copied()))
.unwrap_or_else(|error| panic!("{label}: ODE parameters should validate: {error}"))
}
fn with_one_compartment_analytical_metadata(
analytical: equation::Analytical,
model_name: &str,
) -> equation::Analytical {
analytical
.with_ndrugs(1)
.with_metadata(
equation::metadata::new(model_name)
.kind(equation::ModelKind::Analytical)
.parameters(["ke", "v"])
.states(["central"])
.outputs(["cp"])
.routes([
equation::Route::bolus("iv_bolus").to_state("central"),
equation::Route::infusion("iv").to_state("central"),
])
.analytical_kernel(equation::AnalyticalKernel::OneCompartment),
)
.expect("one-compartment analytical metadata should validate")
}
fn with_one_compartment_ode_metadata(ode: equation::ODE, model_name: &str) -> equation::ODE {
ode.with_ndrugs(1)
.with_metadata(
equation::metadata::new(model_name)
.parameters(["ke", "v"])
.states(["central"])
.outputs(["cp"])
.routes([
equation::Route::bolus("iv_bolus")
.to_state("central")
.expect_explicit_input(),
equation::Route::infusion("iv")
.to_state("central")
.expect_explicit_input(),
]),
)
.expect("one-compartment ODE metadata should validate")
}
fn with_absorption_analytical_metadata(
analytical: equation::Analytical,
model_name: &str,
) -> equation::Analytical {
analytical
.with_ndrugs(1)
.with_metadata(
equation::metadata::new(model_name)
.kind(equation::ModelKind::Analytical)
.parameters(["ka", "ke", "v"])
.states(["gut", "central"])
.outputs(["cp"])
.route(equation::Route::bolus("oral").to_state("gut"))
.analytical_kernel(equation::AnalyticalKernel::OneCompartmentWithAbsorption),
)
.expect("absorption analytical metadata should validate")
}
fn with_absorption_ode_metadata(ode: equation::ODE, model_name: &str) -> equation::ODE {
ode.with_ndrugs(1)
.with_metadata(
equation::metadata::new(model_name)
.parameters(["ka", "ke", "v"])
.states(["gut", "central"])
.outputs(["cp"])
.route(
equation::Route::bolus("oral")
.to_state("gut")
.expect_explicit_input(),
),
)
.expect("absorption ODE metadata should validate")
}
fn with_covariate_ode_metadata(ode: equation::ODE, model_name: &str) -> equation::ODE {
ode.with_ndrugs(1)
.with_metadata(
equation::metadata::new(model_name)
.parameters(["ke", "v"])
.covariates([equation::Covariate::continuous("wt")])
.states(["central"])
.outputs(["cp"])
.route(
equation::Route::bolus("iv_bolus")
.to_state("central")
.expect_explicit_input(),
),
)
.expect("covariate ODE metadata should validate")
}
fn assert_ode_matches_analytical(
label: &str,
analytical: &equation::Analytical,
ode: &equation::ODE,
subject: &Subject,
param_names: &[&str],
params: &[f64],
) {
let analytical_params = parameters_for_analytical(label, analytical, param_names, params);
let ode_params = parameters_for_ode(label, ode, param_names, params);
let analytical_predictions = analytical
.estimate_predictions(subject, &analytical_params)
.expect("analytical predictions should succeed");
let ode_predictions = ode
.estimate_predictions(subject, &ode_params)
.expect("ode predictions should succeed");
let expected = analytical_predictions.flat_predictions();
let actual = ode_predictions.flat_predictions();
assert_eq!(
expected.len(),
actual.len(),
"{}: prediction vector length mismatch (analytical={}, ode={})",
label,
expected.len(),
actual.len()
);
for (idx, (&reference, &candidate)) in expected.iter().zip(actual.iter()).enumerate() {
let abs_err = (reference - candidate).abs();
let rel_err = if reference.abs() > ABS_TOL {
abs_err / reference.abs()
} else {
abs_err
};
assert!(
abs_err <= ABS_TOL || rel_err <= REL_TOL,
"{}: prediction {} differs significantly\n analytical = {:.10}\n ode = {:.10}\n abs_err = {:.2e}\n rel_err = {:.2e}",
label,
idx,
reference,
candidate,
abs_err,
rel_err
);
}
}
#[test]
fn single_iv_bolus_matches_analytical() {
let subject = Subject::builder("single_bolus")
.bolus(0.0, 100.0, "iv_bolus")
.observation(1.0, 0.0, "cp")
.observation(2.0, 0.0, "cp")
.observation(4.0, 0.0, "cp")
.observation(8.0, 0.0, "cp")
.observation(12.0, 0.0, "cp")
.observation(24.0, 0.0, "cp")
.build();
let analytical = with_one_compartment_analytical_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"single_iv_bolus",
);
let ode = with_one_compartment_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, _rateiv, _cov| {
fetch_params!(p, ke, _v);
dx[0] = -ke * x[0] + b[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_nout(1),
"single_iv_bolus",
);
assert_ode_matches_analytical(
"single_iv_bolus",
&analytical,
&ode,
&subject,
&["ke", "v"],
&[0.1, 50.0],
);
}
#[test]
fn multiple_iv_boluses_match_analytical() {
let subject = Subject::builder("multiple_boluses")
.bolus(0.0, 100.0, "iv_bolus")
.observation(1.0, 0.0, "cp")
.observation(2.0, 0.0, "cp")
.bolus(4.0, 50.0, "iv_bolus") .observation(4.0, 0.0, "cp")
.observation(5.0, 0.0, "cp")
.observation(6.0, 0.0, "cp")
.bolus(8.0, 75.0, "iv_bolus") .observation(8.0, 0.0, "cp")
.observation(10.0, 0.0, "cp")
.observation(12.0, 0.0, "cp")
.observation(24.0, 0.0, "cp")
.build();
let analytical = with_one_compartment_analytical_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"multiple_iv_boluses",
);
let ode = with_one_compartment_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, _rateiv, _cov| {
fetch_params!(p, ke, _v);
dx[0] = -ke * x[0] + b[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_nout(1),
"multiple_iv_boluses",
);
assert_ode_matches_analytical(
"multiple_iv_boluses",
&analytical,
&ode,
&subject,
&["ke", "v"],
&[0.1, 50.0],
);
}
#[test]
fn oral_bolus_with_absorption_matches_analytical() {
let subject = Subject::builder("oral_bolus")
.bolus(0.0, 100.0, "oral")
.observation(0.5, 0.0, "cp")
.observation(1.0, 0.0, "cp")
.observation(2.0, 0.0, "cp")
.observation(4.0, 0.0, "cp")
.observation(8.0, 0.0, "cp")
.observation(12.0, 0.0, "cp")
.observation(24.0, 0.0, "cp")
.build();
let analytical = with_absorption_analytical_metadata(
equation::Analytical::new(
one_compartment_with_absorption,
|_p, _t, _cov| {},
|_p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|_p, _t, _cov, _x| {},
|x, p, _t, _cov, y| {
fetch_params!(p, _ka, _ke, v);
y[0] = x[1] / v; },
)
.with_nstates(2)
.with_nout(1),
"oral_bolus_absorption",
);
let ode = with_absorption_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, _rateiv, _cov| {
fetch_params!(p, ka, ke, _v);
dx[0] = -ka * x[0] + b[0]; dx[1] = ka * x[0] - ke * x[1]; },
|_p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|_p, _t, _cov, _x| {},
|x, p, _t, _cov, y| {
fetch_params!(p, _ka, _ke, v);
y[0] = x[1] / v;
},
)
.with_nstates(2)
.with_nout(1),
"oral_bolus_absorption",
);
assert_ode_matches_analytical(
"oral_bolus_absorption",
&analytical,
&ode,
&subject,
&["ka", "ke", "v"],
&[1.0, 0.1, 50.0],
);
}
#[test]
fn multiple_oral_doses_match_analytical() {
let subject = Subject::builder("multiple_oral")
.bolus(0.0, 100.0, "oral") .observation(1.0, 0.0, "cp")
.observation(2.0, 0.0, "cp")
.observation(4.0, 0.0, "cp")
.bolus(8.0, 100.0, "oral") .observation(8.0, 0.0, "cp")
.observation(9.0, 0.0, "cp")
.observation(10.0, 0.0, "cp")
.observation(12.0, 0.0, "cp")
.bolus(16.0, 100.0, "oral") .observation(16.0, 0.0, "cp")
.observation(17.0, 0.0, "cp")
.observation(20.0, 0.0, "cp")
.observation(24.0, 0.0, "cp")
.build();
let analytical = with_absorption_analytical_metadata(
equation::Analytical::new(
one_compartment_with_absorption,
|_p, _t, _cov| {},
|_p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|_p, _t, _cov, _x| {},
|x, p, _t, _cov, y| {
fetch_params!(p, _ka, _ke, v);
y[0] = x[1] / v;
},
)
.with_nstates(2)
.with_nout(1),
"multiple_oral_doses",
);
let ode = with_absorption_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, _rateiv, _cov| {
fetch_params!(p, ka, ke, _v);
dx[0] = -ka * x[0] + b[0];
dx[1] = ka * x[0] - ke * x[1];
},
|_p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|_p, _t, _cov, _x| {},
|x, p, _t, _cov, y| {
fetch_params!(p, _ka, _ke, v);
y[0] = x[1] / v;
},
)
.with_nstates(2)
.with_nout(1),
"multiple_oral_doses",
);
assert_ode_matches_analytical(
"multiple_oral_doses",
&analytical,
&ode,
&subject,
&["ka", "ke", "v"],
&[1.0, 0.1, 50.0],
);
}
#[test]
fn single_infusion_matches_analytical() {
let subject = Subject::builder("single_infusion")
.infusion(0.0, 100.0, "iv", 2.0) .observation(0.5, 0.0, "cp")
.observation(1.0, 0.0, "cp")
.observation(2.0, 0.0, "cp") .observation(3.0, 0.0, "cp")
.observation(4.0, 0.0, "cp")
.observation(8.0, 0.0, "cp")
.observation(12.0, 0.0, "cp")
.build();
let analytical = with_one_compartment_analytical_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"single_infusion",
);
let ode = with_one_compartment_ode_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"single_infusion",
);
assert_ode_matches_analytical(
"single_infusion",
&analytical,
&ode,
&subject,
&["ke", "v"],
&[0.1, 50.0],
);
}
#[test]
fn overlapping_infusions_match_analytical() {
let subject = Subject::builder("overlapping_infusions")
.infusion(0.0, 100.0, "iv", 4.0) .infusion(2.0, 50.0, "iv", 2.0) .observation(1.0, 0.0, "cp")
.observation(2.0, 0.0, "cp")
.observation(3.0, 0.0, "cp")
.observation(4.0, 0.0, "cp")
.observation(5.0, 0.0, "cp")
.observation(6.0, 0.0, "cp")
.observation(8.0, 0.0, "cp")
.observation(12.0, 0.0, "cp")
.build();
let analytical = with_one_compartment_analytical_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"overlapping_infusions",
);
let ode = with_one_compartment_ode_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"overlapping_infusions",
);
assert_ode_matches_analytical(
"overlapping_infusions",
&analytical,
&ode,
&subject,
&["ke", "v"],
&[0.1, 50.0],
);
}
#[test]
fn bolus_plus_infusion_matches_analytical() {
let subject = Subject::builder("bolus_plus_infusion")
.bolus(0.0, 100.0, "iv_bolus") .infusion(0.0, 200.0, "iv", 8.0) .observation(1.0, 0.0, "cp")
.observation(2.0, 0.0, "cp")
.observation(4.0, 0.0, "cp")
.observation(8.0, 0.0, "cp") .observation(10.0, 0.0, "cp")
.observation(12.0, 0.0, "cp")
.observation(24.0, 0.0, "cp")
.build();
let analytical = with_one_compartment_analytical_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"bolus_plus_infusion",
);
let ode = with_one_compartment_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, rateiv, _cov| {
fetch_params!(p, ke, _v);
dx[0] = -ke * x[0] + b[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_nout(1),
"bolus_plus_infusion",
);
assert_ode_matches_analytical(
"bolus_plus_infusion",
&analytical,
&ode,
&subject,
&["ke", "v"],
&[0.1, 50.0],
);
}
#[test]
fn complex_dosing_scenario_matches_analytical() {
let subject = Subject::builder("complex_dosing")
.bolus(0.0, 100.0, "oral") .observation(1.0, 0.0, "cp")
.observation(2.0, 0.0, "cp")
.observation(4.0, 0.0, "cp")
.bolus(6.0, 150.0, "oral") .observation(6.0, 0.0, "cp")
.observation(7.0, 0.0, "cp")
.observation(8.0, 0.0, "cp")
.bolus(12.0, 100.0, "oral") .observation(12.0, 0.0, "cp")
.observation(14.0, 0.0, "cp")
.observation(18.0, 0.0, "cp")
.observation(24.0, 0.0, "cp")
.build();
let analytical = with_absorption_analytical_metadata(
equation::Analytical::new(
one_compartment_with_absorption,
|_p, _t, _cov| {},
|_p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|_p, _t, _cov, _x| {},
|x, p, _t, _cov, y| {
fetch_params!(p, _ka, _ke, v);
y[0] = x[1] / v;
},
)
.with_nstates(2)
.with_nout(1),
"complex_dosing",
);
let ode = with_absorption_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, _rateiv, _cov| {
fetch_params!(p, ka, ke, _v);
dx[0] = -ka * x[0] + b[0]; dx[1] = ka * x[0] - ke * x[1]; },
|_p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|_p, _t, _cov, _x| {},
|x, p, _t, _cov, y| {
fetch_params!(p, _ka, _ke, v);
y[0] = x[1] / v;
},
)
.with_nstates(2)
.with_nout(1),
"complex_dosing",
);
assert_ode_matches_analytical(
"complex_dosing",
&analytical,
&ode,
&subject,
&["ka", "ke", "v"],
&[1.0, 0.1, 50.0],
);
}
#[test]
fn mixed_bolus_infusion_iv_matches_analytical() {
let subject = Subject::builder("mixed_iv")
.bolus(0.0, 100.0, "iv_bolus") .observation(1.0, 0.0, "cp")
.observation(2.0, 0.0, "cp")
.infusion(4.0, 200.0, "iv", 4.0) .observation(4.0, 0.0, "cp")
.observation(5.0, 0.0, "cp")
.observation(6.0, 0.0, "cp")
.bolus(8.0, 50.0, "iv_bolus") .observation(8.0, 0.0, "cp")
.observation(9.0, 0.0, "cp")
.observation(10.0, 0.0, "cp")
.observation(12.0, 0.0, "cp")
.observation(24.0, 0.0, "cp")
.build();
let analytical = with_one_compartment_analytical_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"mixed_iv_bolus_infusion",
);
let ode = with_one_compartment_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, rateiv, _cov| {
fetch_params!(p, ke, _v);
dx[0] = -ke * x[0] + b[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_nout(1),
"mixed_iv_bolus_infusion",
);
assert_ode_matches_analytical(
"mixed_iv_bolus_infusion",
&analytical,
&ode,
&subject,
&["ke", "v"],
&[0.1, 50.0],
);
}
#[test]
fn bolus_at_observation_time_matches_analytical() {
let subject = Subject::builder("simultaneous")
.bolus(0.0, 100.0, "iv_bolus")
.observation(0.0, 0.0, "cp") .observation(1.0, 0.0, "cp")
.bolus(2.0, 50.0, "iv_bolus")
.observation(2.0, 0.0, "cp") .observation(3.0, 0.0, "cp")
.observation(4.0, 0.0, "cp")
.build();
let analytical = with_one_compartment_analytical_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"bolus_at_observation_time",
);
let ode = with_one_compartment_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, _rateiv, _cov| {
fetch_params!(p, ke, _v);
dx[0] = -ke * x[0] + b[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_nout(1),
"bolus_at_observation_time",
);
assert_ode_matches_analytical(
"bolus_at_observation_time",
&analytical,
&ode,
&subject,
&["ke", "v"],
&[0.1, 50.0],
);
}
#[test]
fn very_fast_elimination_matches_analytical() {
let subject = Subject::builder("fast_elimination")
.bolus(0.0, 100.0, "iv_bolus")
.observation(0.1, 0.0, "cp")
.observation(0.2, 0.0, "cp")
.observation(0.5, 0.0, "cp")
.observation(1.0, 0.0, "cp")
.observation(2.0, 0.0, "cp")
.build();
let analytical = with_one_compartment_analytical_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"fast_elimination",
);
let ode = with_one_compartment_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, _rateiv, _cov| {
fetch_params!(p, ke, _v);
dx[0] = -ke * x[0] + b[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_nout(1),
"fast_elimination",
);
assert_ode_matches_analytical(
"fast_elimination",
&analytical,
&ode,
&subject,
&["ke", "v"],
&[2.0, 50.0],
);
}
#[test]
fn very_slow_elimination_matches_analytical() {
let subject = Subject::builder("slow_elimination")
.bolus(0.0, 100.0, "iv_bolus")
.observation(24.0, 0.0, "cp")
.observation(48.0, 0.0, "cp")
.observation(72.0, 0.0, "cp")
.observation(96.0, 0.0, "cp")
.observation(168.0, 0.0, "cp") .build();
let analytical = with_one_compartment_analytical_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"slow_elimination",
);
let ode = with_one_compartment_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, _rateiv, _cov| {
fetch_params!(p, ke, _v);
dx[0] = -ke * x[0] + b[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_nout(1),
"slow_elimination",
);
assert_ode_matches_analytical(
"slow_elimination",
&analytical,
&ode,
&subject,
&["ke", "v"],
&[0.01, 50.0],
);
}
#[test]
fn rapid_absorption_matches_analytical() {
let subject = Subject::builder("rapid_absorption")
.bolus(0.0, 100.0, "oral")
.observation(0.1, 0.0, "cp")
.observation(0.25, 0.0, "cp")
.observation(0.5, 0.0, "cp")
.observation(1.0, 0.0, "cp")
.observation(2.0, 0.0, "cp")
.observation(4.0, 0.0, "cp")
.build();
let analytical = with_absorption_analytical_metadata(
equation::Analytical::new(
one_compartment_with_absorption,
|_p, _t, _cov| {},
|_p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|_p, _t, _cov, _x| {},
|x, p, _t, _cov, y| {
fetch_params!(p, _ka, _ke, v);
y[0] = x[1] / v;
},
)
.with_nstates(2)
.with_nout(1),
"rapid_absorption",
);
let ode = with_absorption_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, _rateiv, _cov| {
fetch_params!(p, ka, ke, _v);
dx[0] = -ka * x[0] + b[0];
dx[1] = ka * x[0] - ke * x[1];
},
|_p, _t, _cov| lag! {},
|_p, _t, _cov| fa! {},
|_p, _t, _cov, _x| {},
|x, p, _t, _cov, y| {
fetch_params!(p, _ka, _ke, v);
y[0] = x[1] / v;
},
)
.with_nstates(2)
.with_nout(1),
"rapid_absorption",
);
assert_ode_matches_analytical(
"rapid_absorption",
&analytical,
&ode,
&subject,
&["ka", "ke", "v"],
&[10.0, 0.1, 50.0],
);
}
#[test]
fn time_varying_covariates_work_correctly() {
let subject = Subject::builder("covariates")
.bolus(0.0, 100.0, "iv_bolus")
.covariate("wt", 0.0, 70.0)
.observation(1.0, 0.0, "cp")
.covariate("wt", 2.0, 75.0) .observation(2.0, 0.0, "cp")
.observation(4.0, 0.0, "cp")
.covariate("wt", 6.0, 72.0) .observation(6.0, 0.0, "cp")
.observation(8.0, 0.0, "cp")
.build();
let ode = with_covariate_ode_metadata(
equation::ODE::new(
|x, p, t, dx, b, _rateiv, cov| {
fetch_params!(p, ke_ref, _v);
fetch_cov!(cov, t, wt);
let ke = ke_ref * (wt / 70.0_f64).powf(0.75);
dx[0] = -ke * x[0] + b[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_nout(1),
"time_varying_covariates",
);
let parameters =
parameters_for_ode("time_varying_covariates", &ode, &["ke", "v"], &[0.1, 50.0]);
let result = ode.estimate_predictions(&subject, ¶meters);
assert!(result.is_ok(), "ODE with covariates should succeed");
let predictions = result.unwrap();
let preds = predictions.flat_predictions();
for (i, &pred) in preds.iter().enumerate() {
assert!(
pred > 0.0,
"Prediction {} should be positive, got {}",
i,
pred
);
}
for (i, pred) in preds.iter().copied().enumerate().skip(1) {
assert!(
pred < 3.0, "Prediction {} seems too high: {}",
i,
pred
);
}
}
#[test]
fn likelihood_calculation_matches_analytical() {
let subject = Subject::builder("likelihood")
.bolus(0.0, 100.0, "iv_bolus")
.observation(1.0, 1.8, "cp") .observation(2.0, 1.6, "cp")
.observation(4.0, 1.3, "cp")
.observation(8.0, 0.8, "cp")
.build();
let analytical = with_one_compartment_analytical_metadata(
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;
},
)
.with_nstates(1)
.with_nout(1),
"likelihood_calculation",
);
let ode = with_one_compartment_ode_metadata(
equation::ODE::new(
|x, p, _t, dx, b, _rateiv, _cov| {
fetch_params!(p, ke, _v);
dx[0] = -ke * x[0] + b[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_nout(1),
"likelihood_calculation",
);
let error_models = AssayErrorModels::default()
.add(
0,
AssayErrorModel::additive(ErrorPoly::new(0.0, 0.1, 0.0, 0.0), 0.0),
)
.unwrap();
let analytical_params = parameters_for_analytical(
"likelihood_calculation",
&analytical,
&["ke", "v"],
&[0.1, 50.0],
);
let ode_params = parameters_for_ode("likelihood_calculation", &ode, &["ke", "v"], &[0.1, 50.0]);
let ll_analytical = analytical
.estimate_log_likelihood(&subject, &analytical_params, &error_models)
.expect("analytical likelihood")
.exp();
let ll_ode = ode
.estimate_log_likelihood(&subject, &ode_params, &error_models)
.expect("ode likelihood")
.exp();
let ll_diff = (ll_analytical - ll_ode).abs();
let ll_rel_diff = ll_diff / ll_analytical.abs().max(1e-10);
assert!(
ll_rel_diff < 0.01, "Likelihoods should match: analytical={:.6}, ode={:.6}, rel_diff={:.2e}",
ll_analytical,
ll_ode,
ll_rel_diff
);
}