use crate::state::States;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Method {
Trapezoidal,
Gear,
TrBdf2,
}
pub(crate) const TRBDF2_GAMMA: f64 = 2.0 - std::f64::consts::SQRT_2;
pub(crate) const TRBDF2_LTE_K: f64 = {
let g = TRBDF2_GAMMA;
(-3.0 * g * g + 4.0 * g - 2.0) / (12.0 * (2.0 - g))
};
#[allow(clippy::eq_op, clippy::needless_range_loop)]
pub(crate) fn com_cof(
method: Method,
order: usize,
delta: f64,
delta_old: &[f64; 8],
) -> Result<[f64; 7], crate::error::Error> {
let mut ag = [0.0f64; 7];
match method {
Method::Trapezoidal => match order {
1 => {
ag[0] = 1.0 / delta;
ag[1] = -1.0 / delta;
}
2 => {
ag[0] = 1.0 / delta / (1.0 - 0.5);
ag[1] = 0.5 / (1.0 - 0.5);
}
_ => {
return Err(crate::error::Error::Build(
"illegal integration order".into(),
));
}
},
Method::TrBdf2 => {
return Err(crate::error::Error::Build(
"TR-BDF2 has no single-step coefficients".into(),
));
}
Method::Gear => {
if order == 0 || order > 6 {
return Err(crate::error::Error::Build(
"illegal integration order".into(),
));
}
let mut mat = [[0.0f64; 8]; 8];
ag[1] = -1.0 / delta;
for e in mat[0].iter_mut().take(order + 1) {
*e = 1.0;
}
for row in mat.iter_mut().take(order + 1).skip(1) {
row[0] = 0.0;
}
let mut arg = 0.0;
for i in 1..=order {
arg += delta_old[i - 1];
let mut arg1 = 1.0;
for j in 1..=order {
arg1 *= arg / delta;
mat[j][i] = arg1;
}
}
for i in 1..=order {
for j in i + 1..=order {
mat[j][i] /= mat[i][i];
for k in i + 1..=order {
mat[j][k] -= mat[j][i] * mat[i][k];
}
}
}
for i in 1..=order {
for j in i + 1..=order {
ag[j] -= mat[j][i] * ag[i];
}
}
ag[order] /= mat[order][order];
for i in (0..order).rev() {
for j in i + 1..=order {
ag[i] -= mat[i][j] * ag[j];
}
ag[i] /= mat[i][i];
}
}
}
Ok(ag)
}
pub(crate) fn integrate(
st: &mut crate::state::StateView,
ag: &[f64; 7],
order: usize,
method: Method,
cap: f64,
q: usize,
) -> (f64, f64) {
let c = q + 1;
match method {
Method::Trapezoidal => match order {
1 => {
let v = ag[0] * st.get(0, q) + ag[1] * st.get(1, q);
st.set(0, c, v);
}
_ => {
let v = -st.get(1, c) * ag[1] + ag[0] * (st.get(0, q) - st.get(1, q));
st.set(0, c, v);
}
},
Method::Gear | Method::TrBdf2 => {
let mut v = 0.0;
for (bank, a) in ag.iter().enumerate().take(order + 1).skip(1) {
v += a * st.get(bank, q);
}
v += ag[0] * st.get(0, q);
st.set(0, c, v);
}
}
let ceq = st.get(0, c) - ag[0] * st.get(0, q);
let geq = ag[0] * cap;
(geq, ceq)
}
pub(crate) struct TerrCtx<'a> {
pub states: &'a States,
pub sol: &'a [f64],
pub time: f64,
pub delta_old: &'a [f64; 8],
pub step_old: &'a [f64; 8],
pub order: usize,
pub method: Method,
pub delta: f64,
pub reltol: f64,
pub abstol: f64,
pub chgtol: f64,
pub trtol: f64,
}
pub(crate) fn terr(q: usize, ctx: &TerrCtx, time_step: &mut f64) {
if ctx.method == Method::TrBdf2 {
terr_trbdf2(q, ctx, time_step);
return;
}
const GEAR_COEFF: [f64; 6] = [
0.5,
0.2222222222,
0.1363636364,
0.096,
0.07299270073,
0.05830903790,
];
const TRAP_COEFF: [f64; 2] = [0.5, 0.08333333333];
let c = q + 1;
let st = ctx.states;
let volttol = ctx.abstol + ctx.reltol * f64::max(st.get(0, c).abs(), st.get(1, c).abs());
let chargetol = f64::max(st.get(0, q).abs(), st.get(1, q).abs());
let chargetol = ctx.reltol * f64::max(chargetol, ctx.chgtol) / ctx.delta;
let tol = f64::max(volttol, chargetol);
let mut diff = [0.0f64; 8];
let mut deltmp = [0.0f64; 8];
for i in (0..=ctx.order + 1).rev() {
diff[i] = st.get(i, q);
}
deltmp[..=ctx.order].copy_from_slice(&ctx.delta_old[..=ctx.order]);
let mut j = ctx.order;
loop {
for i in 0..=j {
diff[i] = (diff[i] - diff[i + 1]) / deltmp[i];
}
if j == 0 {
break;
}
j -= 1;
for i in 0..=j {
deltmp[i] = deltmp[i + 1] + ctx.delta_old[i];
}
}
let factor = match ctx.method {
Method::Gear => GEAR_COEFF[ctx.order - 1],
Method::Trapezoidal | Method::TrBdf2 => TRAP_COEFF[ctx.order - 1],
};
let mut del = ctx.trtol * tol / f64::max(ctx.abstol, factor * diff[0].abs());
if ctx.order == 2 {
del = del.sqrt();
} else if ctx.order > 2 {
del = (del.ln() / ctx.order as f64).exp();
}
*time_step = time_step.min(del);
}
fn terr_trbdf2(q: usize, ctx: &TerrCtx, time_step: &mut f64) {
let c = q + 1;
let st = ctx.states;
let (h2, h1) = (ctx.delta_old[0], ctx.delta_old[1]);
let h = h1 + h2;
let volttol = ctx.abstol + ctx.reltol * f64::max(st.get(0, c).abs(), st.get(2, c).abs());
let chargetol = f64::max(st.get(0, q).abs(), st.get(2, q).abs());
let chargetol = ctx.reltol * f64::max(chargetol, ctx.chgtol) / h;
let tol = f64::max(volttol, chargetol);
let (g0, gg, g1) = (st.get(2, c), st.get(1, c), st.get(0, c));
let dd2 = ((g1 - gg) / h2 - (gg - g0) / h1) / h;
let measure = TRBDF2_LTE_K.abs() * dd2.abs() / 3.0;
let del = (ctx.trtol * tol / f64::max(ctx.abstol, measure)).sqrt();
*time_step = time_step.min(del);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trap_coefficients() {
let d = [1e-3; 8];
let ag = com_cof(Method::Trapezoidal, 1, 1e-3, &d).unwrap();
assert_eq!(ag[0], 1e3);
assert_eq!(ag[1], -1e3);
let ag = com_cof(Method::Trapezoidal, 2, 1e-3, &d).unwrap();
assert_eq!(ag[0], 2e3);
assert_eq!(ag[1], 1.0);
}
#[test]
fn gear_order_1_and_2_match_known_bdf() {
let d = [1e-3; 8];
let ag = com_cof(Method::Gear, 1, 1e-3, &d).unwrap();
assert!((ag[0] - 1e3).abs() < 1e-6);
assert!((ag[1] + 1e3).abs() < 1e-6);
let ag = com_cof(Method::Gear, 2, 1e-3, &d).unwrap();
assert!((ag[0] - 1.5e3).abs() < 1e-6, "{}", ag[0]);
assert!((ag[1] + 2e3).abs() < 1e-6);
assert!((ag[2] - 0.5e3).abs() < 1e-6);
}
#[test]
fn trap_integrate_rc_step() {
let mut st = States::new(2);
let d = [1.0; 8];
let ag = com_cof(Method::Trapezoidal, 1, 1.0, &d).unwrap();
st.set(1, 0, 0.0); st.set(0, 0, 1.0); let (geq, ceq) = integrate(&mut st.view(), &ag, 1, Method::Trapezoidal, 1.0, 0);
assert_eq!(geq, 1.0);
assert_eq!(st.get(0, 1), 1.0); assert_eq!(ceq, 0.0);
}
fn trbdf2_decay(lambda: f64, h: f64) -> f64 {
let mut st = States::new(2);
let mut d = [h; 8];
st.set(0, 0, 1.0);
st.set(0, 1, -lambda);
let solve = |st: &mut States, ag: &[f64; 7], order, method| {
st.set(0, 0, 0.0);
let (_, ceq) = integrate(&mut st.view(), ag, order, method, 1.0, 0);
let q = -ceq / (ag[0] + lambda);
st.set(0, 0, q);
integrate(&mut st.view(), ag, order, method, 1.0, 0);
};
let d1 = TRBDF2_GAMMA * h;
st.rotate(2);
d[0] = d1;
let ag = com_cof(Method::Trapezoidal, 2, d1, &d).unwrap();
solve(&mut st, &ag, 2, Method::Trapezoidal);
st.rotate(2);
d.copy_within(0..7, 1);
d[0] = h - d1;
let ag = com_cof(Method::Gear, 2, h - d1, &d).unwrap();
solve(&mut st, &ag, 2, Method::Gear);
st.get(0, 0)
}
#[test]
fn trbdf2_stages_share_the_jacobian_coefficient() {
let h = 1e-9;
let d1 = TRBDF2_GAMMA * h;
let mut d = [h; 8];
d[0] = d1;
let tr = com_cof(Method::Trapezoidal, 2, d1, &d).unwrap();
d.copy_within(0..7, 1);
d[0] = h - d1;
let bdf = com_cof(Method::Gear, 2, h - d1, &d).unwrap();
let rel = (tr[0] - bdf[0]).abs() / tr[0];
assert!(rel < 1e-12, "{} vs {}", tr[0], bdf[0]);
let g = TRBDF2_GAMMA;
let w = (1.0 - g) / (2.0 - g) * h;
assert!((bdf[0] * w - 1.0).abs() < 1e-12);
assert!((bdf[1] * w + 1.0 / (g * (2.0 - g))).abs() < 1e-12);
assert!((bdf[2] * w - (1.0 - g).powi(2) / (g * (2.0 - g))).abs() < 1e-12);
assert!(com_cof(Method::TrBdf2, 2, h, &d).is_err());
}
#[test]
fn trbdf2_is_l_stable_where_trap_rings() {
let (lambda, h) = (1e15, 1e-9);
let trap = (1.0 - lambda * h / 2.0) / (1.0 + lambda * h / 2.0);
assert!(trap < -0.99999);
let r = trbdf2_decay(lambda, h);
assert!(r.abs() < 1e-5, "{r}");
let (lambda, h) = (1.0, 1e-2);
let err = (trbdf2_decay(lambda, h) - (-lambda * h).exp()).abs();
let pred = TRBDF2_LTE_K.abs() * (lambda * h).powi(3);
assert!((err / pred - 1.0).abs() < 0.05, "{err} vs {pred}");
}
#[test]
fn trbdf2_lte_estimate_is_exact_on_a_cubic() {
let (t0, h) = (0.3, 1e-2);
let d1 = TRBDF2_GAMMA * h;
let mut st = States::new(2);
for (bank, t) in [(2, t0), (1, t0 + d1), (0, t0 + h)] {
st.set(bank, 0, t * t * t);
st.set(bank, 1, 3.0 * t * t);
}
let mut d = [h; 8];
d[0] = h - d1;
d[1] = d1;
let ctx = TerrCtx {
states: &st,
sol: &[],
time: t0 + h,
delta_old: &d,
step_old: &[h; 8],
order: 2,
method: Method::TrBdf2,
delta: h,
reltol: 1e-3,
abstol: 1e-12,
chgtol: 1e-14,
trtol: 7.0,
};
let mut del = f64::MAX;
terr(0, &ctx, &mut del);
let tq = f64::max((t0 + h).powi(3), t0.powi(3));
let ti = 3.0 * f64::max((t0 + h).powi(2), t0.powi(2));
let tol = f64::max(1e-12 + 1e-3 * ti, 1e-3 * tq / h);
let want = (7.0 * tol / (TRBDF2_LTE_K.abs() * 3.0 / 3.0)).sqrt();
assert!((del / want - 1.0).abs() < 1e-6, "{del} vs {want}");
}
}