use super::config::IntegrationMethod;
pub type MethodCoeffs = [[f64; 12]; 13];
pub type TestCoeffs = [[f64; 12]; 3];
#[allow(
clippy::needless_range_loop,
clippy::assign_op_pattern,
reason = "index arithmetic and recurrence assignments mirror the JEOD/Fortran source line-by-line for auditability against DCFODE"
)]
#[allow(
clippy::cast_precision_loss,
reason = "all casts are loop counters nq/ii ≤ 12 (Adams) / ≤ 5 (BDF), exactly representable in f64"
)]
pub fn calculate_integration_coefficients(method: IntegrationMethod) -> (MethodCoeffs, TestCoeffs) {
let mut method_coeffs: MethodCoeffs = [[0.0; 12]; 13];
let mut test_coeffs: TestCoeffs = [[0.0; 12]; 3];
let mut poly_coeff = [0.0_f64; 13];
match method {
IntegrationMethod::ImplicitAdamsNonStiff => {
method_coeffs[0][0] = 1.0;
method_coeffs[1][0] = 1.0;
test_coeffs[0][0] = 0.0;
test_coeffs[1][0] = 2.0;
test_coeffs[0][1] = 1.0;
test_coeffs[2][11] = 0.0;
poly_coeff[0] = 1.0;
let mut rqfac = 1.0_f64;
for nq in 2..=12 {
let rq1fac = rqfac;
rqfac /= nq as f64;
poly_coeff[nq - 1] = 0.0;
for ii in (1..=nq - 1).rev() {
poly_coeff[ii] = poly_coeff[ii - 1] + (nq - 1) as f64 * poly_coeff[ii];
}
poly_coeff[0] = (nq - 1) as f64 * poly_coeff[0];
let mut pint = poly_coeff[0];
let mut xpin = poly_coeff[0] / 2.0;
let mut tsign = 1.0_f64;
for ii in 2..=nq {
tsign = -tsign;
pint += tsign * poly_coeff[ii - 1] / ii as f64;
xpin += tsign * poly_coeff[ii - 1] / (ii + 1) as f64;
}
method_coeffs[0][nq - 1] = pint * rq1fac;
method_coeffs[1][nq - 1] = 1.0;
for ii in 2..=nq {
method_coeffs[ii][nq - 1] = rq1fac * poly_coeff[ii - 1] / ii as f64;
}
let agamq = rqfac * xpin;
let ragq = 1.0 / agamq;
test_coeffs[1][nq - 1] = ragq;
if nq < 12 {
test_coeffs[0][nq] = ragq * rqfac / (nq + 1) as f64;
}
test_coeffs[2][nq - 2] = ragq;
}
}
IntegrationMethod::ImplicitBackDiffStiff => {
poly_coeff[0] = 1.0;
let mut rq1fac = 1.0_f64;
for nq in 1..=5 {
let nqp1 = nq + 1;
poly_coeff[nq] = 0.0;
for ii in (1..=nq).rev() {
poly_coeff[ii] = poly_coeff[ii - 1] + nq as f64 * poly_coeff[ii];
}
poly_coeff[0] = nq as f64 * poly_coeff[1];
for ii in 0..=nq {
method_coeffs[ii][nq - 1] = poly_coeff[ii] / poly_coeff[1];
}
method_coeffs[1][nq - 1] = 1.0;
test_coeffs[0][nq - 1] = rq1fac;
test_coeffs[1][nq - 1] = nqp1 as f64 / method_coeffs[0][nq - 1];
test_coeffs[2][nq - 1] = (nq + 2) as f64 / method_coeffs[0][nq - 1];
rq1fac /= nq as f64;
}
}
}
(method_coeffs, test_coeffs)
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(
clippy::float_cmp,
reason = "coefficient generation is exact rational arithmetic in f64; the known Adams-Moulton values are representable exactly"
)]
fn assert_exact(got: f64, want: f64, label: &str) {
assert_eq!(got, want, "{label}: got {got}, want {want}");
}
fn assert_close(got: f64, want: f64, label: &str) {
let tol = 1e-15 * want.abs().max(1.0);
assert!(
(got - want).abs() <= tol,
"{label}: got {got}, want {want} (|Δ| > {tol:.3e})"
);
}
#[test]
fn adams_low_order_coefficients_match_known_adams_moulton() {
let (el, _) = calculate_integration_coefficients(IntegrationMethod::ImplicitAdamsNonStiff);
assert_exact(el[0][0], 1.0, "AM1 el0");
assert_exact(el[1][0], 1.0, "AM1 el1");
assert_exact(el[0][1], 0.5, "AM2 el0");
assert_exact(el[1][1], 1.0, "AM2 el1");
assert_exact(el[2][1], 0.5, "AM2 el2");
assert_close(el[0][2], 5.0 / 12.0, "AM3 el0");
assert_exact(el[1][2], 1.0, "AM3 el1");
assert_exact(el[2][2], 3.0 / 4.0, "AM3 el2");
assert_close(el[3][2], 1.0 / 6.0, "AM3 el3");
}
#[test]
fn adams_el0_matches_known_beta0_orders_4_to_8() {
let (el, _) = calculate_integration_coefficients(IntegrationMethod::ImplicitAdamsNonStiff);
assert_close(el[0][3], 9.0 / 24.0, "AM4 el0");
assert_close(el[0][4], 251.0 / 720.0, "AM5 el0");
assert_close(el[0][5], 95.0 / 288.0, "AM6 el0");
assert_close(el[0][6], 19_087.0 / 60_480.0, "AM7 el0");
assert_close(el[0][7], 5_257.0 / 17_280.0, "AM8 el0");
}
#[test]
fn adams_full_el_vector_orders_4_5_match_elco() {
let (el, _) = calculate_integration_coefficients(IntegrationMethod::ImplicitAdamsNonStiff);
assert_close(el[0][3], 3.0 / 8.0, "AM4 el0");
assert_exact(el[1][3], 1.0, "AM4 el1");
assert_close(el[2][3], 11.0 / 12.0, "AM4 el2");
assert_close(el[3][3], 1.0 / 3.0, "AM4 el3");
assert_close(el[4][3], 1.0 / 24.0, "AM4 el4");
assert_close(el[0][4], 251.0 / 720.0, "AM5 el0");
assert_exact(el[1][4], 1.0, "AM5 el1");
assert_close(el[2][4], 25.0 / 24.0, "AM5 el2");
assert_close(el[3][4], 35.0 / 72.0, "AM5 el3");
assert_close(el[4][4], 5.0 / 48.0, "AM5 el4");
assert_close(el[5][4], 1.0 / 120.0, "AM5 el5");
}
#[test]
fn bdf_order1_is_backward_euler() {
let (el, _) = calculate_integration_coefficients(IntegrationMethod::ImplicitBackDiffStiff);
assert_exact(el[0][0], 1.0, "BDF1 el0");
assert_exact(el[1][0], 1.0, "BDF1 el1");
}
#[test]
fn adams_el1_row_is_unity_for_all_orders() {
let (el, _) = calculate_integration_coefficients(IntegrationMethod::ImplicitAdamsNonStiff);
for nq in 1..=12 {
assert_exact(el[1][nq - 1], 1.0, "Adams el1 row");
}
}
}