rustyqlib/core/fd_solvers/
adi.rs1use super::axis_operator::{AxisOperator, TensorGrid};
23
24fn apply_full(
26 grid: &TensorGrid,
27 ops: &[AxisOperator],
28 mixed: Option<&dyn Fn(&[f64]) -> Vec<f64>>,
29 u: &[f64],
30) -> Vec<f64> {
31 let mut out = vec![0.0; u.len()];
32 for op in ops {
33 for (o, v) in out.iter_mut().zip(op.apply(grid, u)) {
34 *o += v;
35 }
36 }
37 if let Some(a0) = mixed {
38 for (o, v) in out.iter_mut().zip(a0(u)) {
39 *o += v;
40 }
41 }
42 out
43}
44
45pub fn douglas_step(
54 grid: &TensorGrid,
55 ops: &[AxisOperator],
56 mixed: Option<&dyn Fn(&[f64]) -> Vec<f64>>,
57 u: &[f64],
58 dt: f64,
59 theta: f64,
60) -> Vec<f64> {
61 assert!(!ops.is_empty(), "at least one axis operator is required");
62 assert_eq!(u.len(), grid.len());
63 let f_u = apply_full(grid, ops, mixed, u);
64 let mut y: Vec<f64> = u.iter().zip(&f_u).map(|(ui, fi)| ui + dt * fi).collect();
65 for op in ops {
66 let a_u = op.apply(grid, u);
67 for (yi, ai) in y.iter_mut().zip(&a_u) {
68 *yi -= theta * dt * ai;
69 }
70 y = op.solve_shifted(grid, theta * dt, &y);
71 }
72 y
73}
74
75pub fn hundsdorfer_verwer_step(
87 grid: &TensorGrid,
88 ops: &[AxisOperator],
89 mixed: Option<&dyn Fn(&[f64]) -> Vec<f64>>,
90 u: &[f64],
91 dt: f64,
92 theta: f64,
93 mu: f64,
94) -> Vec<f64> {
95 assert!(!ops.is_empty(), "at least one axis operator is required");
96 assert_eq!(u.len(), grid.len());
97 let f_u = apply_full(grid, ops, mixed, u);
98 let y0: Vec<f64> = u.iter().zip(&f_u).map(|(ui, fi)| ui + dt * fi).collect();
99
100 let mut y = y0.clone();
102 for op in ops {
103 let a_u = op.apply(grid, u);
104 for (yi, ai) in y.iter_mut().zip(&a_u) {
105 *yi -= theta * dt * ai;
106 }
107 y = op.solve_shifted(grid, theta * dt, &y);
108 }
109
110 let f_y = apply_full(grid, ops, mixed, &y);
112 let mut yt: Vec<f64> = y0
113 .iter()
114 .zip(f_y.iter().zip(&f_u))
115 .map(|(y0i, (fyi, fui))| y0i + mu * dt * (fyi - fui))
116 .collect();
117 for op in ops {
118 let a_y = op.apply(grid, &y);
119 for (yi, ai) in yt.iter_mut().zip(&a_y) {
120 *yi -= theta * dt * ai;
121 }
122 yt = op.solve_shifted(grid, theta * dt, &yt);
123 }
124 yt
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use std::f64::consts::PI;
131
132 fn laplacian(grid: &TensorGrid, axis: usize, h: f64) -> AxisOperator {
134 let mut op = AxisOperator::zero(grid, axis);
135 let stride = grid.strides()[axis];
136 let n = grid.dims()[axis];
137 for i in 0..grid.len() {
138 let j = (i / stride) % n;
139 if j > 0 && j < n - 1 {
140 op.sub[i] = 1.0 / (h * h);
141 op.diag[i] = -2.0 / (h * h);
142 op.sup[i] = 1.0 / (h * h);
143 }
144 }
145 op
146 }
147
148 fn sine_product(grid: &TensorGrid, h: f64) -> Vec<f64> {
150 let dims = grid.dims().to_vec();
151 let strides = grid.strides();
152 (0..grid.len())
153 .map(|i| {
154 dims.iter()
155 .zip(&strides)
156 .map(|(&n, &s)| (PI * ((i / s) % n) as f64 * h).sin())
157 .product()
158 })
159 .collect()
160 }
161
162 fn max_abs_diff(a: &[f64], b: &[f64]) -> f64 {
163 a.iter().zip(b).map(|(x, y)| (x - y).abs()).fold(0.0, f64::max)
164 }
165
166 fn heat_decay(ndim: usize, n: usize, steps: usize, dt: f64, hv: bool) -> f64 {
169 let h = 1.0 / (n - 1) as f64;
170 let grid = TensorGrid::new(&vec![n; ndim]);
171 let ops: Vec<AxisOperator> = (0..ndim).map(|k| laplacian(&grid, k, h)).collect();
172 let mut u = sine_product(&grid, h);
173 for _ in 0..steps {
174 u = if hv {
175 hundsdorfer_verwer_step(&grid, &ops, None, &u, dt, 0.5, 0.5)
176 } else {
177 douglas_step(&grid, &ops, None, &u, dt, 0.5)
178 };
179 }
180 let exact = (-(ndim as f64) * PI * PI * (steps as f64 * dt)).exp();
181 let u0 = sine_product(&grid, h);
182 let (imax, _) = u0
184 .iter()
185 .enumerate()
186 .fold((0, 0.0), |acc, (i, &v)| if v > acc.1 { (i, v) } else { acc });
187 (u[imax] / (exact * u0[imax]) - 1.0).abs()
188 }
189
190 #[test]
191 fn one_dimension_reduces_to_crank_nicolson_heat_solution() {
192 let err = heat_decay(1, 41, 200, 5e-4, false);
194 assert!(err < 5e-3, "1-D heat relative error {err}");
195 }
196
197 #[test]
198 fn douglas_solves_the_2d_heat_equation() {
199 let err = heat_decay(2, 21, 100, 1e-3, false);
200 assert!(err < 1e-2, "2-D heat relative error {err}");
201 }
202
203 #[test]
204 fn douglas_and_hv_solve_the_3d_heat_equation() {
205 let do_err = heat_decay(3, 11, 50, 1e-3, false);
206 let hv_err = heat_decay(3, 11, 50, 1e-3, true);
207 assert!(do_err < 3e-2, "3-D Douglas relative error {do_err}");
208 assert!(hv_err < 3e-2, "3-D HV relative error {hv_err}");
209 }
210
211 #[test]
212 fn mixed_derivative_term_matches_an_explicit_reference() {
213 let n = 9;
217 let h = 1.0 / (n - 1) as f64;
218 let grid = TensorGrid::new(&[n, n]);
219 let ops = [laplacian(&grid, 0, h), laplacian(&grid, 1, h)];
220 let (sx, sy) = (grid.strides()[0], grid.strides()[1]);
221 let dims = grid.dims().to_vec();
222 let mixed = move |u: &[f64]| -> Vec<f64> {
223 (0..u.len())
224 .map(|i| {
225 let (jx, jy) = ((i / sx) % dims[0], (i / sy) % dims[1]);
226 if jx == 0 || jx == dims[0] - 1 || jy == 0 || jy == dims[1] - 1 {
227 0.0
228 } else {
229 (u[i + sx + sy] - u[i + sx - sy] - u[i - sx + sy] + u[i - sx - sy])
230 / (4.0 * h * h)
231 }
232 })
233 .collect()
234 };
235 let u0 = sine_product(&grid, h);
236 let t_end: f64 = 0.01;
237
238 let mut reference = u0.clone();
240 let dt_ref = 1e-5;
241 for _ in 0..(t_end / dt_ref).round() as usize {
242 let f = apply_full(&grid, &ops, Some(&mixed), &reference);
243 for (r, fi) in reference.iter_mut().zip(&f) {
244 *r += dt_ref * fi;
245 }
246 }
247
248 let dt = 1e-3;
249 let steps = (t_end / dt).round() as usize;
250 let mut douglas = u0.clone();
251 let mut hv = u0;
252 for _ in 0..steps {
253 douglas = douglas_step(&grid, &ops, Some(&mixed), &douglas, dt, 0.5);
254 hv = hundsdorfer_verwer_step(&grid, &ops, Some(&mixed), &hv, dt, 0.5, 0.5);
255 }
256 assert!(max_abs_diff(&douglas, &reference) < 1e-2, "douglas vs reference");
257 assert!(max_abs_diff(&hv, &reference) < 1e-2, "hv vs reference");
258
259 let mut no_mixed = sine_product(&grid, h);
261 for _ in 0..steps {
262 no_mixed = douglas_step(&grid, &ops, None, &no_mixed, dt, 0.5);
263 }
264 assert!(max_abs_diff(&no_mixed, &reference) > 1e-3, "mixed term had no effect");
265 }
266}