use crate::linalg::{
faer::{FaerCholesky, FaerLu},
quadratic_form_sparse,
};
use simplicial::linalg::{CooMatrix, CsrMatrix, Matrix, Vector};
pub struct Tableau {
pub a: Matrix,
pub b: Vector,
pub c: Vector,
pub s: usize,
}
impl Tableau {
fn new(a: Matrix, b: Vector, c: Vector) -> Self {
let s = b.len();
assert!(a.nrows() == s && a.ncols() == s);
assert!(c.len() == s);
Self { a, b, c, s }
}
pub fn gauss_legendre(s: usize) -> Self {
collocation_tableau(gauss_legendre_nodes(s))
}
pub fn radau_iia(s: usize) -> Self {
collocation_tableau(radau_iia_nodes(s))
}
}
fn collocation_tableau(c: Vector) -> Tableau {
let s = c.len();
let mut vander = Matrix::zeros(s, s);
for k in 0..s {
for j in 0..s {
vander[(k, j)] = c[j].powi(k as i32);
}
}
let lu = vander.lu();
let b_rhs = Vector::from_fn(s, |k, _| 1.0 / f64::from(k as u32 + 1));
let b = lu
.solve(&b_rhs)
.expect("node Vandermonde is nonsingular for distinct collocation nodes");
let mut a = Matrix::zeros(s, s);
for i in 0..s {
let a_rhs = Vector::from_fn(s, |k, _| c[i].powi(k as i32 + 1) / f64::from(k as u32 + 1));
let a_row = lu
.solve(&a_rhs)
.expect("node Vandermonde is nonsingular for distinct collocation nodes");
a.row_mut(i).copy_from(&a_row.transpose());
}
Tableau::new(a, b, c)
}
fn gauss_legendre_nodes(s: usize) -> Vector {
map_unit(sorted_eigenvalues(legendre_jacobi(s)))
}
fn radau_iia_nodes(s: usize) -> Vector {
let mut jacobi = legendre_jacobi(s);
if s >= 1 {
let a = 1.0;
let last = s - 1;
if s >= 2 {
let beta = jacobi[(last, last - 1)];
let block: Matrix =
jacobi.view((0, 0), (last, last)).into_owned() - a * Matrix::identity(last, last);
let mut e = Vector::zeros(last);
e[last - 1] = beta * beta;
let delta = block
.lu()
.solve(&e)
.expect("Radau endpoint block is nonsingular (a is not an interior node)");
jacobi[(last, last)] = a + delta[last - 1];
} else {
jacobi[(last, last)] = a;
}
}
map_unit(sorted_eigenvalues(jacobi))
}
fn legendre_jacobi(s: usize) -> Matrix {
let mut t = Matrix::zeros(s, s);
for j in 1..s {
let beta = j as f64 / (4.0 * (j * j) as f64 - 1.0).sqrt();
t[(j - 1, j)] = beta;
t[(j, j - 1)] = beta;
}
t
}
fn sorted_eigenvalues(m: Matrix) -> Vector {
let mut vals = m.symmetric_eigenvalues();
vals.as_mut_slice().sort_by(f64::total_cmp);
vals
}
fn map_unit(x: Vector) -> Vector {
x.map(|xi| 0.5 * (xi + 1.0))
}
pub struct LinearIrk {
tableau: Tableau,
op: CsrMatrix,
dt: f64,
ndofs: usize,
stage_lu: FaerLu,
}
impl LinearIrk {
pub fn new(tableau: Tableau, mass: &CsrMatrix, op: CsrMatrix, dt: f64) -> Self {
let ndofs = mass.nrows();
assert_eq!(mass.ncols(), ndofs);
assert_eq!(op.nrows(), ndofs);
assert_eq!(op.ncols(), ndofs);
let stage_matrix = stage_matrix(&tableau.a, mass, &op, dt);
let stage_lu = FaerLu::new(stage_matrix);
Self {
tableau,
op,
dt,
ndofs,
stage_lu,
}
}
pub fn step(&self, y0: &Vector, t0: f64, forcing: impl Fn(f64) -> Vector) -> Vector {
let s = self.tableau.s;
let d = self.ndofs;
let ay0 = &self.op * y0;
let mut rhs = Vector::zeros(s * d);
for i in 0..s {
let stage_time = t0 + self.tableau.c[i] * self.dt;
let fi = &ay0 + forcing(stage_time);
rhs.rows_mut(i * d, d).copy_from(&fi);
}
let k = self.stage_lu.solve(&rhs);
let mut y1 = y0.clone();
for i in 0..s {
y1 += self.dt * self.tableau.b[i] * k.rows(i * d, d);
}
y1
}
}
fn stage_matrix(a_tab: &Matrix, mass: &CsrMatrix, op: &CsrMatrix, dt: f64) -> CsrMatrix {
let s = a_tab.nrows();
let d = mass.nrows();
let mut coo = CooMatrix::new(s * d, s * d);
for i in 0..s {
for (r, c, &v) in mass.triplet_iter() {
coo.push(i * d + r, i * d + c, v);
}
for j in 0..s {
let coeff = -dt * a_tab[(i, j)];
if coeff != 0.0 {
for (r, c, &v) in op.triplet_iter() {
coo.push(i * d + r, j * d + c, coeff * v);
}
}
}
}
CsrMatrix::from(&coo)
}
pub struct Leapfrog {
idx0: Vec<usize>,
idx1: Vec<usize>,
mass0: CsrMatrix,
mass1: CsrMatrix,
chol0: FaerCholesky,
chol1: FaerCholesky,
a01: CsrMatrix,
a10: CsrMatrix,
dt: f64,
ndofs: usize,
}
impl Leapfrog {
pub fn new(mass: &CsrMatrix, op: &CsrMatrix, color: &[bool], dt: f64) -> Self {
let ndofs = mass.nrows();
assert_eq!(mass.ncols(), ndofs);
assert_eq!(op.nrows(), ndofs);
assert_eq!(op.ncols(), ndofs);
assert_eq!(color.len(), ndofs);
let idx0: Vec<usize> = (0..ndofs).filter(|&i| !color[i]).collect();
let idx1: Vec<usize> = (0..ndofs).filter(|&i| color[i]).collect();
let (n0, n1) = (idx0.len(), idx1.len());
let mut local = vec![0usize; ndofs];
for (l, &g) in idx0.iter().enumerate() {
local[g] = l;
}
for (l, &g) in idx1.iter().enumerate() {
local[g] = l;
}
for (r, c, &v) in mass.triplet_iter() {
assert!(
v == 0.0 || color[r] == color[c],
"mass couples the two colors"
);
}
for (r, c, &v) in op.triplet_iter() {
assert!(
v == 0.0 || color[r] != color[c],
"operator couples within a color"
);
}
let block = |src: &CsrMatrix, want_r: bool, want_c: bool, nr: usize, nc: usize| {
let mut coo = CooMatrix::new(nr, nc);
for (r, c, &v) in src.triplet_iter() {
if color[r] == want_r && color[c] == want_c {
coo.push(local[r], local[c], v);
}
}
CsrMatrix::from(&coo)
};
let mass0 = block(mass, false, false, n0, n0);
let mass1 = block(mass, true, true, n1, n1);
let a01 = block(op, false, true, n0, n1);
let a10 = block(op, true, false, n1, n0);
Self {
idx0,
idx1,
chol0: FaerCholesky::new(mass0.clone()),
chol1: FaerCholesky::new(mass1.clone()),
mass0,
mass1,
a01,
a10,
dt,
ndofs,
}
}
fn gather(&self, y: &Vector, idx: &[usize]) -> Vector {
Vector::from_iterator(idx.len(), idx.iter().map(|&g| y[g]))
}
fn scatter(&self, y: &mut Vector, idx: &[usize], v: &Vector) {
for (l, &g) in idx.iter().enumerate() {
y[g] = v[l];
}
}
pub fn step(&self, y0: &Vector) -> Vector {
let mut q = self.gather(y0, &self.idx0);
let mut p = self.gather(y0, &self.idx1);
p += 0.5 * self.dt * self.chol1.solve(&(&self.a10 * &q));
q += self.dt * self.chol0.solve(&(&self.a01 * &p));
p += 0.5 * self.dt * self.chol1.solve(&(&self.a10 * &q));
let mut y1 = Vector::zeros(self.ndofs);
self.scatter(&mut y1, &self.idx0, &q);
self.scatter(&mut y1, &self.idx1, &p);
y1
}
pub fn conserved_energy(&self, y: &Vector) -> f64 {
let q = self.gather(y, &self.idx0);
let p = self.gather(y, &self.idx1);
let a10q = &self.a10 * &q;
let defect = a10q.dot(&self.chol1.solve(&a10q));
0.5 * quadratic_form_sparse(&self.mass0, &q) + 0.5 * quadratic_form_sparse(&self.mass1, &p)
- self.dt * self.dt / 8.0 * defect
}
}
#[cfg(test)]
mod test {
use super::*;
use approx::assert_relative_eq;
fn oscillator(omega: f64) -> CsrMatrix {
let mut coo = CooMatrix::new(2, 2);
coo.push(0, 1, 1.0);
coo.push(1, 0, -omega * omega);
CsrMatrix::from(&coo)
}
fn identity(n: usize) -> CsrMatrix {
let mut coo = CooMatrix::new(n, n);
for i in 0..n {
coo.push(i, i, 1.0);
}
CsrMatrix::from(&coo)
}
#[test]
fn low_stage_tableaus_match_classical_coefficients() {
let sqrt3 = 3f64.sqrt();
let gl1 = Tableau::gauss_legendre(1);
assert_relative_eq!(gl1.a, Matrix::from_row_slice(1, 1, &[0.5]));
assert_relative_eq!(gl1.b, Vector::from_row_slice(&[1.0]));
assert_relative_eq!(gl1.c, Vector::from_row_slice(&[0.5]));
let gl2 = Tableau::gauss_legendre(2);
assert_relative_eq!(
gl2.a,
Matrix::from_row_slice(2, 2, &[0.25, 0.25 - sqrt3 / 6.0, 0.25 + sqrt3 / 6.0, 0.25])
);
assert_relative_eq!(gl2.b, Vector::from_row_slice(&[0.5, 0.5]));
assert_relative_eq!(
gl2.c,
Vector::from_row_slice(&[0.5 - sqrt3 / 6.0, 0.5 + sqrt3 / 6.0])
);
let r1 = Tableau::radau_iia(1);
assert_relative_eq!(r1.a, Matrix::from_row_slice(1, 1, &[1.0]));
assert_relative_eq!(r1.b, Vector::from_row_slice(&[1.0]));
assert_relative_eq!(r1.c, Vector::from_row_slice(&[1.0]));
let r2 = Tableau::radau_iia(2);
assert_relative_eq!(
r2.a,
Matrix::from_row_slice(2, 2, &[5.0 / 12.0, -1.0 / 12.0, 3.0 / 4.0, 1.0 / 4.0])
);
assert_relative_eq!(r2.b, Vector::from_row_slice(&[3.0 / 4.0, 1.0 / 4.0]));
assert_relative_eq!(r2.c, Vector::from_row_slice(&[1.0 / 3.0, 1.0]));
}
#[test]
fn collocation_tableaus_satisfy_order_conditions() {
for s in 1..=6 {
for tab in [Tableau::gauss_legendre(s), Tableau::radau_iia(s)] {
assert_eq!(tab.s, s);
for i in 0..s {
assert_relative_eq!(tab.c[i], tab.a.row(i).sum(), epsilon = 1e-12);
}
for k in 1..=s {
let kf = k as f64;
for i in 0..s {
let lhs: f64 = (0..s)
.map(|j| tab.a[(i, j)] * tab.c[j].powi(k as i32 - 1))
.sum();
assert_relative_eq!(lhs, tab.c[i].powi(k as i32) / kf, epsilon = 1e-11);
}
let quad: f64 = (0..s).map(|i| tab.b[i] * tab.c[i].powi(k as i32 - 1)).sum();
assert_relative_eq!(quad, 1.0 / kf, epsilon = 1e-11);
}
}
let radau = Tableau::radau_iia(s);
assert_relative_eq!(radau.c[s - 1], 1.0, epsilon = 1e-12);
}
}
#[test]
fn gauss_legendre_conserves_energy_exactly() {
let omega = 1.7;
let op = oscillator(omega);
let mass = identity(2);
for s in 1..=4 {
let dt = 0.3;
let irk = LinearIrk::new(Tableau::gauss_legendre(s), &mass, op.clone(), dt);
let mut y = Vector::from_row_slice(&[1.0, 0.0]);
let energy0 = 0.5 * (y[1] * y[1] + omega * omega * y[0] * y[0]);
let mut t = 0.0;
for _ in 0..500 {
y = irk.step(&y, t, |_| Vector::zeros(2));
t += dt;
}
let energy = 0.5 * (y[1] * y[1] + omega * omega * y[0] * y[0]);
assert_relative_eq!(energy, energy0, epsilon = 1e-10);
}
}
#[test]
fn radau_iia_is_monotone_and_accurate_for_stiff_decay() {
let lambda = 500.0;
let mut coo = CooMatrix::new(1, 1);
coo.push(0, 0, -lambda);
let op = CsrMatrix::from(&coo);
let mass = identity(1);
let dt = 0.05; let irk = LinearIrk::new(Tableau::radau_iia(2), &mass, op, dt);
let mut y = Vector::from_row_slice(&[1.0]);
let mut t = 0.0;
for _ in 0..10 {
let y_next = irk.step(&y, t, |_| Vector::zeros(1));
assert!(y_next[0].abs() <= y[0].abs(), "decay must stay monotone");
y = y_next;
t += dt;
}
let exact = (-lambda * t).exp();
assert_relative_eq!(y[0], exact, epsilon = 1e-2);
}
#[test]
fn radau_iia_solves_index_one_dae_with_singular_mass() {
let lambda = 2.0;
let mut m = CooMatrix::new(2, 2);
m.push(1, 1, 1.0);
let mass = CsrMatrix::from(&m);
let mut a = CooMatrix::new(2, 2);
a.push(0, 0, -1.0);
a.push(0, 1, 1.0);
a.push(1, 0, -lambda);
let op = CsrMatrix::from(&a);
let dt = 0.05;
let irk = LinearIrk::new(Tableau::radau_iia(2), &mass, op, dt);
let mut y = Vector::from_row_slice(&[1.0, 1.0]);
let mut t = 0.0;
for _ in 0..40 {
y = irk.step(&y, t, |_| Vector::zeros(2));
t += dt;
assert_relative_eq!(y[0], y[1], epsilon = 1e-9);
}
let exact = (-lambda * t).exp();
assert_relative_eq!(y[1], exact, epsilon = 1e-4);
}
#[test]
fn gauss_legendre_conserves_energy_on_singular_mass_wave_dae() {
let mut m = CooMatrix::new(3, 3);
m.push(1, 1, 1.0);
m.push(2, 2, 1.0);
let mass = CsrMatrix::from(&m);
let mut a = CooMatrix::new(3, 3);
a.push(0, 0, -1.0);
a.push(0, 1, 1.0); a.push(1, 2, 1.0); a.push(2, 0, -1.0); let op = CsrMatrix::from(&a);
let dt = 0.3;
let irk = LinearIrk::new(Tableau::gauss_legendre(2), &mass, op, dt);
let mut y = Vector::from_row_slice(&[1.0, 1.0, 0.0]);
let energy0 = 0.5 * (y[1] * y[1] + y[2] * y[2]);
let mut t = 0.0;
for _ in 0..500 {
y = irk.step(&y, t, |_| Vector::zeros(3));
t += dt;
assert_relative_eq!(y[0], y[1], epsilon = 1e-9);
}
let energy = 0.5 * (y[1] * y[1] + y[2] * y[2]);
assert_relative_eq!(energy, energy0, epsilon = 1e-9);
}
#[test]
fn constant_forcing_reaches_steady_state() {
let lambda = 3.0;
let mut coo = CooMatrix::new(1, 1);
coo.push(0, 0, -lambda);
let op = CsrMatrix::from(&coo);
let mass = identity(1);
let force = 6.0;
let dt = 0.2;
let irk = LinearIrk::new(Tableau::radau_iia(2), &mass, op, dt);
let mut y = Vector::from_row_slice(&[0.0]);
let mut t = 0.0;
for _ in 0..200 {
y = irk.step(&y, t, |_| Vector::from_row_slice(&[force]));
t += dt;
}
assert_relative_eq!(y[0], force / lambda, epsilon = 1e-6);
}
#[test]
fn leapfrog_conserves_staggered_energy_exactly() {
let mut m = CooMatrix::new(2, 2);
m.push(0, 0, 2.0);
m.push(1, 1, 3.0);
let mass = CsrMatrix::from(&m);
let mut a = CooMatrix::new(2, 2);
a.push(0, 1, 1.0);
a.push(1, 0, -1.0);
let op = CsrMatrix::from(&a);
let color = [false, true];
let dt = 0.2;
let lf = Leapfrog::new(&mass, &op, &color, dt);
let mut y = Vector::from_row_slice(&[1.0, 0.5]);
let e0 = lf.conserved_energy(&y);
assert!(e0 > 0.0);
for _ in 0..1000 {
y = lf.step(&y);
assert_relative_eq!(lf.conserved_energy(&y), e0, epsilon = 1e-10 * e0.max(1.0));
}
}
}