1use crate::state::States;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Method {
12 Trapezoidal,
15 Gear,
17 TrBdf2,
26}
27
28pub(crate) const TRBDF2_GAMMA: f64 = 2.0 - std::f64::consts::SQRT_2;
32
33pub(crate) const TRBDF2_LTE_K: f64 = {
37 let g = TRBDF2_GAMMA;
38 (-3.0 * g * g + 4.0 * g - 2.0) / (12.0 * (2.0 - g))
39};
40
41#[allow(clippy::eq_op, clippy::needless_range_loop)]
46pub(crate) fn com_cof(
47 method: Method,
48 order: usize,
49 delta: f64,
50 delta_old: &[f64; 8],
51) -> Result<[f64; 7], crate::error::Error> {
52 let mut ag = [0.0f64; 7];
53 match method {
54 Method::Trapezoidal => match order {
55 1 => {
56 ag[0] = 1.0 / delta;
57 ag[1] = -1.0 / delta;
58 }
59 2 => {
60 ag[0] = 1.0 / delta / (1.0 - 0.5);
62 ag[1] = 0.5 / (1.0 - 0.5);
63 }
64 _ => {
65 return Err(crate::error::Error::Build(
66 "illegal integration order".into(),
67 ));
68 }
69 },
70 Method::TrBdf2 => {
74 return Err(crate::error::Error::Build(
75 "TR-BDF2 has no single-step coefficients".into(),
76 ));
77 }
78 Method::Gear => {
79 if order == 0 || order > 6 {
80 return Err(crate::error::Error::Build(
81 "illegal integration order".into(),
82 ));
83 }
84 let mut mat = [[0.0f64; 8]; 8];
87 ag[1] = -1.0 / delta;
88 for e in mat[0].iter_mut().take(order + 1) {
89 *e = 1.0;
90 }
91 for row in mat.iter_mut().take(order + 1).skip(1) {
92 row[0] = 0.0;
93 }
94 let mut arg = 0.0;
95 for i in 1..=order {
96 arg += delta_old[i - 1];
97 let mut arg1 = 1.0;
98 for j in 1..=order {
99 arg1 *= arg / delta;
100 mat[j][i] = arg1;
101 }
102 }
103 for i in 1..=order {
105 for j in i + 1..=order {
106 mat[j][i] /= mat[i][i];
107 for k in i + 1..=order {
108 mat[j][k] -= mat[j][i] * mat[i][k];
109 }
110 }
111 }
112 for i in 1..=order {
114 for j in i + 1..=order {
115 ag[j] -= mat[j][i] * ag[i];
116 }
117 }
118 ag[order] /= mat[order][order];
120 for i in (0..order).rev() {
121 for j in i + 1..=order {
122 ag[i] -= mat[i][j] * ag[j];
123 }
124 ag[i] /= mat[i][i];
125 }
126 }
127 }
128 Ok(ag)
129}
130
131pub(crate) fn integrate(
135 st: &mut crate::state::StateView,
136 ag: &[f64; 7],
137 order: usize,
138 method: Method,
139 cap: f64,
140 q: usize,
141) -> (f64, f64) {
142 let c = q + 1;
143 match method {
144 Method::Trapezoidal => match order {
145 1 => {
146 let v = ag[0] * st.get(0, q) + ag[1] * st.get(1, q);
147 st.set(0, c, v);
148 }
149 _ => {
150 let v = -st.get(1, c) * ag[1] + ag[0] * (st.get(0, q) - st.get(1, q));
151 st.set(0, c, v);
152 }
153 },
154 Method::Gear | Method::TrBdf2 => {
157 let mut v = 0.0;
158 for (bank, a) in ag.iter().enumerate().take(order + 1).skip(1) {
159 v += a * st.get(bank, q);
160 }
161 v += ag[0] * st.get(0, q);
162 st.set(0, c, v);
163 }
164 }
165 let ceq = st.get(0, c) - ag[0] * st.get(0, q);
166 let geq = ag[0] * cap;
167 (geq, ceq)
168}
169
170pub(crate) struct TerrCtx<'a> {
172 pub states: &'a States,
173 pub sol: &'a [f64],
176 pub time: f64,
177 pub delta_old: &'a [f64; 8],
182 pub step_old: &'a [f64; 8],
186 pub order: usize,
187 pub method: Method,
188 pub delta: f64,
190 pub reltol: f64,
191 pub abstol: f64,
192 pub chgtol: f64,
193 pub trtol: f64,
194}
195
196pub(crate) fn terr(q: usize, ctx: &TerrCtx, time_step: &mut f64) {
199 if ctx.method == Method::TrBdf2 {
200 terr_trbdf2(q, ctx, time_step);
201 return;
202 }
203 const GEAR_COEFF: [f64; 6] = [
204 0.5,
205 0.2222222222,
206 0.1363636364,
207 0.096,
208 0.07299270073,
209 0.05830903790,
210 ];
211 const TRAP_COEFF: [f64; 2] = [0.5, 0.08333333333];
212
213 let c = q + 1;
214 let st = ctx.states;
215 let volttol = ctx.abstol + ctx.reltol * f64::max(st.get(0, c).abs(), st.get(1, c).abs());
216 let chargetol = f64::max(st.get(0, q).abs(), st.get(1, q).abs());
217 let chargetol = ctx.reltol * f64::max(chargetol, ctx.chgtol) / ctx.delta;
218 let tol = f64::max(volttol, chargetol);
219
220 let mut diff = [0.0f64; 8];
222 let mut deltmp = [0.0f64; 8];
223 for i in (0..=ctx.order + 1).rev() {
224 diff[i] = st.get(i, q);
225 }
226 deltmp[..=ctx.order].copy_from_slice(&ctx.delta_old[..=ctx.order]);
227 let mut j = ctx.order;
228 loop {
229 for i in 0..=j {
230 diff[i] = (diff[i] - diff[i + 1]) / deltmp[i];
231 }
232 if j == 0 {
233 break;
234 }
235 j -= 1;
236 for i in 0..=j {
237 deltmp[i] = deltmp[i + 1] + ctx.delta_old[i];
238 }
239 }
240
241 let factor = match ctx.method {
242 Method::Gear => GEAR_COEFF[ctx.order - 1],
243 Method::Trapezoidal | Method::TrBdf2 => TRAP_COEFF[ctx.order - 1],
245 };
246 let mut del = ctx.trtol * tol / f64::max(ctx.abstol, factor * diff[0].abs());
247 if ctx.order == 2 {
248 del = del.sqrt();
249 } else if ctx.order > 2 {
250 del = (del.ln() / ctx.order as f64).exp();
251 }
252 *time_step = time_step.min(del);
253}
254
255fn terr_trbdf2(q: usize, ctx: &TerrCtx, time_step: &mut f64) {
271 let c = q + 1;
272 let st = ctx.states;
273 let (h2, h1) = (ctx.delta_old[0], ctx.delta_old[1]);
274 let h = h1 + h2;
275 let volttol = ctx.abstol + ctx.reltol * f64::max(st.get(0, c).abs(), st.get(2, c).abs());
276 let chargetol = f64::max(st.get(0, q).abs(), st.get(2, q).abs());
277 let chargetol = ctx.reltol * f64::max(chargetol, ctx.chgtol) / h;
278 let tol = f64::max(volttol, chargetol);
279
280 let (g0, gg, g1) = (st.get(2, c), st.get(1, c), st.get(0, c));
281 let dd2 = ((g1 - gg) / h2 - (gg - g0) / h1) / h;
282 let measure = TRBDF2_LTE_K.abs() * dd2.abs() / 3.0;
283 let del = (ctx.trtol * tol / f64::max(ctx.abstol, measure)).sqrt();
284 *time_step = time_step.min(del);
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn trap_coefficients() {
293 let d = [1e-3; 8];
294 let ag = com_cof(Method::Trapezoidal, 1, 1e-3, &d).unwrap();
295 assert_eq!(ag[0], 1e3);
296 assert_eq!(ag[1], -1e3);
297 let ag = com_cof(Method::Trapezoidal, 2, 1e-3, &d).unwrap();
298 assert_eq!(ag[0], 2e3);
299 assert_eq!(ag[1], 1.0);
300 }
301
302 #[test]
303 fn gear_order_1_and_2_match_known_bdf() {
304 let d = [1e-3; 8];
305 let ag = com_cof(Method::Gear, 1, 1e-3, &d).unwrap();
307 assert!((ag[0] - 1e3).abs() < 1e-6);
308 assert!((ag[1] + 1e3).abs() < 1e-6);
309 let ag = com_cof(Method::Gear, 2, 1e-3, &d).unwrap();
311 assert!((ag[0] - 1.5e3).abs() < 1e-6, "{}", ag[0]);
312 assert!((ag[1] + 2e3).abs() < 1e-6);
313 assert!((ag[2] - 0.5e3).abs() < 1e-6);
314 }
315
316 #[test]
317 fn trap_integrate_rc_step() {
318 let mut st = States::new(2);
321 let d = [1.0; 8];
322 let ag = com_cof(Method::Trapezoidal, 1, 1.0, &d).unwrap();
323 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);
326 assert_eq!(geq, 1.0);
327 assert_eq!(st.get(0, 1), 1.0); assert_eq!(ceq, 0.0);
329 }
330
331 fn trbdf2_decay(lambda: f64, h: f64) -> f64 {
335 let mut st = States::new(2);
336 let mut d = [h; 8];
337 st.set(0, 0, 1.0);
339 st.set(0, 1, -lambda);
340 let solve = |st: &mut States, ag: &[f64; 7], order, method| {
341 st.set(0, 0, 0.0);
342 let (_, ceq) = integrate(&mut st.view(), ag, order, method, 1.0, 0);
343 let q = -ceq / (ag[0] + lambda);
344 st.set(0, 0, q);
345 integrate(&mut st.view(), ag, order, method, 1.0, 0);
346 };
347 let d1 = TRBDF2_GAMMA * h;
348 st.rotate(2);
349 d[0] = d1;
350 let ag = com_cof(Method::Trapezoidal, 2, d1, &d).unwrap();
351 solve(&mut st, &ag, 2, Method::Trapezoidal);
352 st.rotate(2);
353 d.copy_within(0..7, 1);
354 d[0] = h - d1;
355 let ag = com_cof(Method::Gear, 2, h - d1, &d).unwrap();
356 solve(&mut st, &ag, 2, Method::Gear);
357 st.get(0, 0)
358 }
359
360 #[test]
361 fn trbdf2_stages_share_the_jacobian_coefficient() {
362 let h = 1e-9;
365 let d1 = TRBDF2_GAMMA * h;
366 let mut d = [h; 8];
367 d[0] = d1;
368 let tr = com_cof(Method::Trapezoidal, 2, d1, &d).unwrap();
369 d.copy_within(0..7, 1);
370 d[0] = h - d1;
371 let bdf = com_cof(Method::Gear, 2, h - d1, &d).unwrap();
372 let rel = (tr[0] - bdf[0]).abs() / tr[0];
373 assert!(rel < 1e-12, "{} vs {}", tr[0], bdf[0]);
374 let g = TRBDF2_GAMMA;
377 let w = (1.0 - g) / (2.0 - g) * h;
378 assert!((bdf[0] * w - 1.0).abs() < 1e-12);
379 assert!((bdf[1] * w + 1.0 / (g * (2.0 - g))).abs() < 1e-12);
380 assert!((bdf[2] * w - (1.0 - g).powi(2) / (g * (2.0 - g))).abs() < 1e-12);
381 assert!(com_cof(Method::TrBdf2, 2, h, &d).is_err());
382 }
383
384 #[test]
385 fn trbdf2_is_l_stable_where_trap_rings() {
386 let (lambda, h) = (1e15, 1e-9);
389 let trap = (1.0 - lambda * h / 2.0) / (1.0 + lambda * h / 2.0);
390 assert!(trap < -0.99999);
391 let r = trbdf2_decay(lambda, h);
392 assert!(r.abs() < 1e-5, "{r}");
393 let (lambda, h) = (1.0, 1e-2);
395 let err = (trbdf2_decay(lambda, h) - (-lambda * h).exp()).abs();
396 let pred = TRBDF2_LTE_K.abs() * (lambda * h).powi(3);
397 assert!((err / pred - 1.0).abs() < 0.05, "{err} vs {pred}");
398 }
399
400 #[test]
401 fn trbdf2_lte_estimate_is_exact_on_a_cubic() {
402 let (t0, h) = (0.3, 1e-2);
405 let d1 = TRBDF2_GAMMA * h;
406 let mut st = States::new(2);
407 for (bank, t) in [(2, t0), (1, t0 + d1), (0, t0 + h)] {
408 st.set(bank, 0, t * t * t);
409 st.set(bank, 1, 3.0 * t * t);
410 }
411 let mut d = [h; 8];
412 d[0] = h - d1;
413 d[1] = d1;
414 let ctx = TerrCtx {
415 states: &st,
416 sol: &[],
417 time: t0 + h,
418 delta_old: &d,
419 step_old: &[h; 8],
420 order: 2,
421 method: Method::TrBdf2,
422 delta: h,
423 reltol: 1e-3,
424 abstol: 1e-12,
425 chgtol: 1e-14,
426 trtol: 7.0,
427 };
428 let mut del = f64::MAX;
429 terr(0, &ctx, &mut del);
430 let tq = f64::max((t0 + h).powi(3), t0.powi(3));
431 let ti = 3.0 * f64::max((t0 + h).powi(2), t0.powi(2));
432 let tol = f64::max(1e-12 + 1e-3 * ti, 1e-3 * tq / h);
433 let want = (7.0 * tol / (TRBDF2_LTE_K.abs() * 3.0 / 3.0)).sqrt();
434 assert!((del / want - 1.0).abs() < 1e-6, "{del} vs {want}");
435 }
436}