Ran into this when testing sensitivities on low-magnitude parameters, such as particle diffusivities (magnitude ~5e-14) in electrochemical particle models.
Forward-sensitivity error control is not invariant under an equivalent rescaling of a model parameter. For sufficiently small unitful parameters, BDF can reach the minimum timestep even though the same primal problem expressed with dimensionless parameter solves normally.
sens_atol currently supplies one state-sized absolute-tolerance vector that is reused for every sensitivity vector. Since
has units [y] / [p_j], sensitivities for parameters with different scales need different absolute error weights. A single shared vector cannot represent that for a mixed-scale parameter set.
MWE
Tested the current state of main.
Cargo.toml:
[package]
name = "diffsol-sens-scaling"
version = "0.1.0"
edition = "2021"
[dependencies]
diffsol = "0.14.2"
src/main.rs:
use diffsol::{NalgebraLU, NalgebraMat, OdeBuilder, SensitivitiesOdeSolverMethod};
const PHYSICAL_PARAMETER: f64 = 3.3e-14;
const FREQUENCY: f64 = 1e3;
const STIFFNESS: f64 = 1e3;
const TOLERANCE: f64 = 1e-7;
fn solve(label: &str, parameter: f64, parameter_scale: f64) {
let problem = OdeBuilder::<NalgebraMat<f64>>::new()
.p([parameter])
.rtol(TOLERANCE)
.atol([TOLERANCE])
.sens_rtol(TOLERANCE)
.sens_atol([TOLERANCE])
.rhs_sens_implicit(
move |x, p, t, y| {
y[0] = STIFFNESS * (parameter_scale * p[0] * (FREQUENCY * t).sin() - x[0]);
},
move |_x, _p, _t, v, y| {
y[0] = -STIFFNESS * v[0];
},
move |_x, _p, t, v, y| {
y[0] = STIFFNESS * parameter_scale * (FREQUENCY * t).sin() * v[0];
},
)
.init_sens(
|_p, _t, y| {
y[0] = 0.0;
},
|_p, _t, _v, y| {
y[0] = 0.0;
},
1,
)
.build()
.unwrap();
let mut solver = problem.bdf_sens::<NalgebraLU<f64>>().unwrap();
let result = solver.solve_dense_sensitivities(&[0.0, 1e-3]);
let stats = solver.get_statistics();
println!(
"{label}: result={:?}, steps={}, error_failures={}",
result.as_ref().map(|_| ()),
stats.number_of_steps,
stats.number_of_error_test_failures,
);
}
fn main() {
// p = 3.3e-14, with the RHS depending on p / 3.3e-14.
solve(
"raw parameter",
PHYSICAL_PARAMETER,
1.0 / PHYSICAL_PARAMETER,
);
// q = p / 3.3e-14. This produces the same primal equation.
solve("dimensionless parameter", 1.0, 1.0);
}
Output:
raw parameter: result=Err(OdeSolverError(StepSizeTooSmall {
time: 6.324555847383075e-8
})), steps=2, error_failures=19
dimensionless parameter: result=Ok(()), steps=48, error_failures=0
The primal equation is identical in both cases. Only the parameterization and therefore the scale of dy/dp differ. For one raw parameter, this can be worked around manually by setting its sensitivity absolute tolerance to approximately
state_atol / abs(parameter)
but this is not possible for multiple differently scaled parameters that affect the same state because sens_atol is shared across all sensitivity vectors.
Possible Solutions
- parameter scales such as
sens_param_scales / pbar, used to apply the state error weights to pbar[j] * S[j]; or
- one absolute-tolerance vector per sensitivity parameter?
The first option would mirror SUNDIALS IDAS estimated sensitivity tolerances: atol_s[j][i] = atol_y[i] / abs(pbar[j]).
Reference:
https://sundials.readthedocs.io/en/v7.4.0/idas/Mathematics_link.html#selection-of-the-absolute-tolerances-for-sensitivity-variables
Ran into this when testing sensitivities on low-magnitude parameters, such as particle diffusivities (magnitude ~5e-14) in electrochemical particle models.
Forward-sensitivity error control is not invariant under an equivalent rescaling of a model parameter. For sufficiently small unitful parameters, BDF can reach the minimum timestep even though the same primal problem expressed with dimensionless parameter solves normally.
sens_atolcurrently supplies one state-sized absolute-tolerance vector that is reused for every sensitivity vector. Sincehas units
[y] / [p_j], sensitivities for parameters with different scales need different absolute error weights. A single shared vector cannot represent that for a mixed-scale parameter set.MWE
Tested the current state of
main.Cargo.toml:src/main.rs:Output:
The primal equation is identical in both cases. Only the parameterization and therefore the scale of
dy/dpdiffer. For one raw parameter, this can be worked around manually by setting its sensitivity absolute tolerance to approximatelybut this is not possible for multiple differently scaled parameters that affect the same state because
sens_atolis shared across all sensitivity vectors.Possible Solutions
sens_param_scales/pbar, used to apply the state error weights topbar[j] * S[j]; orThe first option would mirror SUNDIALS IDAS estimated sensitivity tolerances:
atol_s[j][i] = atol_y[i] / abs(pbar[j]).Reference:
https://sundials.readthedocs.io/en/v7.4.0/idas/Mathematics_link.html#selection-of-the-absolute-tolerances-for-sensitivity-variables