1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
#![deny(missing_docs)]
//! An energy-conserving integrator.
/// Solves a linear system: A x = b.
///
/// I assume `x` contains the previous solution of the system.
/// The result A^{-1} b is first stored in `b`, which are compared with `x` to measure how close to the convergence the system is.
#[cfg(not(feature = "explicit"))]
fn solve_linear_system(nitems: usize, a: &mut Vec<f64>, x: &mut Vec<f64>, b: &mut Vec<f64>) -> f64 {
// Classic Gaussian elimination.
{
// forward elimination
for i in 0..nitems {
for ii in i + 1..nitems {
let v: f64 = a[ii * nitems + i] / a[i * nitems + i];
for j in i + 1..nitems {
a[ii * nitems + j] -= v * a[i * nitems + j];
}
b[ii] -= v * b[i];
}
}
// backward substitution
for i in (0..nitems).rev() {
for j in i + 1..nitems {
b[i] -= a[i * nitems + j] * b[j];
}
b[i] /= a[i * nitems + i];
}
}
// now `b` stores the solution
// check convergence and update `x` by copying `b`
let mut residual: f64 = 0.;
for i in 0..nitems {
residual += (b[i] - x[i]).abs();
x[i] = b[i];
}
return residual;
}
/// Core function of the integrator.
///
/// This function solves [the discrete governing equations](https://naokihori.github.io/Pendulum/discrete/time_marcher.html).
/// The main purpose is to iteratively update the difference of the angular velocities `dv` until
/// converged.
#[cfg(not(feature = "explicit"))]
fn kernel(dt: f64, nitems: usize, poss: &mut Vec<f64>, vels: &mut Vec<f64>) -> bool {
// sinc function: sin(x) / x
let sinc: fn(f64) -> f64 = |x: f64| -> f64 {
if 0. == x {
1.
} else {
x.sin() / x
}
};
/// Computes updated angular velocity and the position of a point.
///
/// To update the angle, the Crank-Nocolson scheme is adopted.
fn get_new_values(v_old: f64, a_old: f64, dv: f64, dt: f64) -> [f64; 2] {
let v_new: f64 = v_old + dv;
let a_new: f64 = a_old + 0.5 * (v_old + v_new) * dt;
return [v_new, a_new];
}
// initialise local arrays
// difference of vels
let mut dvels: Vec<f64> = vec![0.; nitems];
// right-hand-side vector
let mut rhs: Vec<f64> = vec![0.; nitems];
// left-hand-side array
let mut lhs: Vec<f64> = vec![0.; nitems * nitems];
// Crank-Nicolson iteration
// set max number of interations
// TODO: this is purely ad-hoc
let itermax: usize = nitems << 1;
for _iter in 0..itermax {
// compute LHS array and RHS vector
for i in 0..nitems {
// expand i-th point information
let vi_old: f64 = vels[i];
let ai_old: f64 = poss[i];
let dveli: f64 = dvels[i];
let [vi_new, ai_new]: [f64; 2] = get_new_values(vi_old, ai_old, dveli, dt);
// potential energy contribution
rhs[i] = (nitems - i) as f64
* sinc(0.5 * ai_new - 0.5 * ai_old)
* (0.5 * ai_new + 0.5 * ai_old).cos()
* dt;
// interactive effects
for j in 0..nitems {
// expand j-th point information
let vj_old: f64 = vels[j];
let aj_old: f64 = poss[j];
let dvelj: f64 = dvels[j];
let [vj_new, aj_new]: [f64; 2] = get_new_values(vj_old, aj_old, dvelj, dt);
// mass matrix
let m: f64 = nitems as f64 - (if i > j { i } else { j }) as f64;
let cij_old: f64 = if i == j { 1. } else { (ai_old - aj_old).cos() };
let cij_new: f64 = if i == j { 1. } else { (ai_new - aj_new).cos() };
let numer: f64 = f64::EPSILON + (0.5 * vi_new * cij_new + 0.5 * vi_old * cij_old);
let denom: f64 =
f64::EPSILON + (0.5 * vi_new + 0.5 * vi_old) * (0.5 * cij_new + 0.5 * cij_old);
let cor: f64 = 0.5 + 0.5 * numer / denom;
lhs[i * nitems + j] = m * cor * (0.5 * cij_new + 0.5 * cij_old);
// kinetic energy contribution
let vj: f64 = 0.5 * vj_new + 0.5 * vj_old;
rhs[i] -= m
* vj
* vj
* sinc(0.5 * ai_new - 0.5 * ai_old - 0.5 * aj_new + 0.5 * aj_old)
* (0.5 * ai_new + 0.5 * ai_old - 0.5 * aj_new - 0.5 * aj_old).sin()
* dt;
}
}
// solve Ax = b, where
// A: lhs
// x: dvels
// b: rhs
let residual: f64 = solve_linear_system(nitems, &mut lhs, &mut dvels, &mut rhs);
// terminate iteration when converged
if f64::EPSILON > residual {
// finally update information
for i in 0..nitems {
const PI: f64 = std::f64::consts::PI;
let [v, mut p]: [f64; 2] = get_new_values(vels[i], poss[i], dvels[i], dt);
if p < -PI {
p += 2. * PI;
}
if PI < p {
p -= 2. * PI;
}
vels[i] = v;
poss[i] = p;
}
return true;
}
}
// not converged
return false;
}
/// Entry point of the integrator.
///
/// There is no guarantee that the integrator converges with the given time step size `dt`.
/// As a remedy, I try to integrate the equation anyway, and make `dt` halved if it turns out to fail.
/// With this approach, however, `dt` gets smaller and smaller as proceeds.
/// To avoid unnecessarily small `dt`, I first double it.
#[cfg(not(feature = "explicit"))]
pub fn entrypoint(p: &mut crate::Pendulum) -> f64 {
let dt: &mut f64 = &mut p.dt;
let nitems: usize = p.nitems;
let mut poss: &mut Vec<f64> = &mut p.poss;
let mut vels: &mut Vec<f64> = &mut p.vels;
*dt *= 2.;
loop {
if kernel(*dt, nitems, &mut poss, &mut vels) {
return *dt;
}
*dt *= 0.5;
}
}