Skip to main content

numra_pde/
discretize.rs

1//! Finite difference discretization schemes.
2//!
3//! Author: Moussa Leblouba
4//! Date: 4 February 2026
5//! Modified: 2 May 2026
6
7use crate::grid::Grid1D;
8use numra_core::Scalar;
9
10/// Difference scheme type.
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub enum DifferenceScheme {
13    /// Central differences (second-order accurate)
14    Central,
15    /// Forward differences (first-order accurate)
16    Forward,
17    /// Backward differences (first-order accurate)
18    Backward,
19    /// Fourth-order central differences
20    Central4,
21}
22
23/// Finite difference stencil coefficients.
24#[derive(Clone, Debug)]
25pub struct Stencil<S: Scalar> {
26    /// Coefficients for each point in the stencil
27    pub coeffs: Vec<S>,
28    /// Offset of the leftmost point from center
29    pub offset: i32,
30}
31
32impl<S: Scalar> Stencil<S> {
33    /// Create a first derivative central difference stencil.
34    pub fn d1_central(dx: S) -> Self {
35        let inv_2dx = S::ONE / (S::from_f64(2.0) * dx);
36        Self {
37            coeffs: vec![-inv_2dx, S::ZERO, inv_2dx],
38            offset: -1,
39        }
40    }
41
42    /// Create a first derivative forward difference stencil.
43    pub fn d1_forward(dx: S) -> Self {
44        let inv_dx = S::ONE / dx;
45        Self {
46            coeffs: vec![-inv_dx, inv_dx],
47            offset: 0,
48        }
49    }
50
51    /// Create a first derivative backward difference stencil.
52    pub fn d1_backward(dx: S) -> Self {
53        let inv_dx = S::ONE / dx;
54        Self {
55            coeffs: vec![-inv_dx, inv_dx],
56            offset: -1,
57        }
58    }
59
60    /// Create a second derivative central difference stencil.
61    pub fn d2_central(dx: S) -> Self {
62        let inv_dx2 = S::ONE / (dx * dx);
63        Self {
64            coeffs: vec![inv_dx2, -S::from_f64(2.0) * inv_dx2, inv_dx2],
65            offset: -1,
66        }
67    }
68
69    /// Create a fourth-order accurate second derivative stencil.
70    pub fn d2_central4(dx: S) -> Self {
71        let inv_dx2 = S::ONE / (dx * dx);
72        let c = inv_dx2 / S::from_f64(12.0);
73        Self {
74            coeffs: vec![
75                -c,
76                S::from_f64(16.0) * c,
77                -S::from_f64(30.0) * c,
78                S::from_f64(16.0) * c,
79                -c,
80            ],
81            offset: -2,
82        }
83    }
84
85    /// Apply stencil at point i to array u.
86    pub fn apply(&self, u: &[S], i: usize) -> S {
87        let mut result = S::ZERO;
88        for (k, &coeff) in self.coeffs.iter().enumerate() {
89            let idx = (i as i32 + self.offset + k as i32) as usize;
90            result = result + coeff * u[idx];
91        }
92        result
93    }
94}
95
96/// Finite Difference Method utilities.
97pub struct FDM;
98
99impl FDM {
100    /// Compute first derivative using central differences.
101    ///
102    /// `du/dx ≈ (u[i+1] - u[i-1]) / (2*dx)`
103    pub fn d1_central<S: Scalar>(u: &[S], dx: S, i: usize) -> S {
104        let inv_2dx = S::ONE / (S::from_f64(2.0) * dx);
105        (u[i + 1] - u[i - 1]) * inv_2dx
106    }
107
108    /// Compute first derivative using forward difference.
109    ///
110    /// `du/dx ≈ (u[i+1] - u[i]) / dx`
111    pub fn d1_forward<S: Scalar>(u: &[S], dx: S, i: usize) -> S {
112        (u[i + 1] - u[i]) / dx
113    }
114
115    /// Compute first derivative using backward difference.
116    ///
117    /// `du/dx ≈ (u[i] - u[i-1]) / dx`
118    pub fn d1_backward<S: Scalar>(u: &[S], dx: S, i: usize) -> S {
119        (u[i] - u[i - 1]) / dx
120    }
121
122    /// Compute second derivative using central differences.
123    ///
124    /// `d²u/dx² ≈ (u[i+1] - 2*u[i] + u[i-1]) / dx²`
125    pub fn d2_central<S: Scalar>(u: &[S], dx: S, i: usize) -> S {
126        let inv_dx2 = S::ONE / (dx * dx);
127        (u[i + 1] - S::from_f64(2.0) * u[i] + u[i - 1]) * inv_dx2
128    }
129
130    /// Compute second derivative using fourth-order central differences.
131    ///
132    /// `d²u/dx² ≈ (-u[i+2] + 16*u[i+1] - 30*u[i] + 16*u[i-1] - u[i-2]) / (12*dx²)`
133    pub fn d2_central4<S: Scalar>(u: &[S], dx: S, i: usize) -> S {
134        let inv_12dx2 = S::ONE / (S::from_f64(12.0) * dx * dx);
135        (-u[i + 2] + S::from_f64(16.0) * u[i + 1] - S::from_f64(30.0) * u[i]
136            + S::from_f64(16.0) * u[i - 1]
137            - u[i - 2])
138            * inv_12dx2
139    }
140
141    /// Compute first derivative at left boundary using one-sided differences.
142    ///
143    /// `du/dx ≈ (-3*u[0] + 4*u[1] - u[2]) / (2*dx)`
144    pub fn d1_left_boundary<S: Scalar>(u: &[S], dx: S) -> S {
145        let inv_2dx = S::ONE / (S::from_f64(2.0) * dx);
146        (-S::from_f64(3.0) * u[0] + S::from_f64(4.0) * u[1] - u[2]) * inv_2dx
147    }
148
149    /// Compute first derivative at right boundary using one-sided differences.
150    ///
151    /// `du/dx ≈ (3*u[n-1] - 4*u[n-2] + u[n-3]) / (2*dx)`
152    pub fn d1_right_boundary<S: Scalar>(u: &[S], dx: S) -> S {
153        let n = u.len();
154        let inv_2dx = S::ONE / (S::from_f64(2.0) * dx);
155        (S::from_f64(3.0) * u[n - 1] - S::from_f64(4.0) * u[n - 2] + u[n - 3]) * inv_2dx
156    }
157
158    /// Apply Laplacian operator to entire field (interior points only).
159    ///
160    /// Returns vector of size n-2 (excluding boundary points).
161    pub fn laplacian_1d<S: Scalar>(u: &[S], dx: S) -> Vec<S> {
162        let n = u.len();
163        let inv_dx2 = S::ONE / (dx * dx);
164        let two = S::from_f64(2.0);
165
166        (1..n - 1)
167            .map(|i| (u[i + 1] - two * u[i] + u[i - 1]) * inv_dx2)
168            .collect()
169    }
170
171    /// Apply Laplacian operator to entire 2D field (interior points only).
172    ///
173    /// Uses a 5-point stencil.
174    pub fn laplacian_2d<S: Scalar>(u: &[S], nx: usize, ny: usize, dx: S, dy: S) -> Vec<S> {
175        let inv_dx2 = S::ONE / (dx * dx);
176        let inv_dy2 = S::ONE / (dy * dy);
177        let two = S::from_f64(2.0);
178
179        let mut result = vec![S::ZERO; (nx - 2) * (ny - 2)];
180
181        for j in 1..ny - 1 {
182            for i in 1..nx - 1 {
183                let idx = j * nx + i;
184                let d2x = (u[idx + 1] - two * u[idx] + u[idx - 1]) * inv_dx2;
185                let d2y = (u[idx + nx] - two * u[idx] + u[idx - nx]) * inv_dy2;
186
187                let result_idx = (j - 1) * (nx - 2) + (i - 1);
188                result[result_idx] = d2x + d2y;
189            }
190        }
191
192        result
193    }
194
195    /// Compute gradient at all interior points.
196    pub fn gradient_1d<S: Scalar>(u: &[S], dx: S) -> Vec<S> {
197        let n = u.len();
198        let inv_2dx = S::ONE / (S::from_f64(2.0) * dx);
199
200        (1..n - 1)
201            .map(|i| (u[i + 1] - u[i - 1]) * inv_2dx)
202            .collect()
203    }
204
205    /// Build sparse Laplacian matrix for 1D domain with given BCs.
206    ///
207    /// Returns a tridiagonal matrix in dense form for simplicity.
208    /// For large problems, use sparse matrix formats.
209    pub fn laplacian_matrix_1d<S: Scalar>(n_interior: usize, dx: S) -> Vec<Vec<S>> {
210        let inv_dx2 = S::ONE / (dx * dx);
211        let two = S::from_f64(2.0);
212
213        let mut matrix = vec![vec![S::ZERO; n_interior]; n_interior];
214
215        for i in 0..n_interior {
216            matrix[i][i] = -two * inv_dx2;
217            if i > 0 {
218                matrix[i][i - 1] = inv_dx2;
219            }
220            if i < n_interior - 1 {
221                matrix[i][i + 1] = inv_dx2;
222            }
223        }
224
225        matrix
226    }
227}
228
229/// Non-uniform grid finite differences.
230///
231/// Infrastructure for future non-uniform grid support.
232#[allow(dead_code)]
233pub struct NonUniformFDM;
234
235#[allow(dead_code)]
236impl NonUniformFDM {
237    /// Second derivative on non-uniform grid.
238    ///
239    /// Uses the formula for variable spacing:
240    /// d²u/dx² ≈ 2*[(u[i+1] - u[i])/(dx_plus) - (u[i] - u[i-1])/(dx_minus)] / (dx_plus + dx_minus)
241    pub fn d2<S: Scalar>(u: &[S], grid: &Grid1D<S>, i: usize) -> S {
242        let dx_minus = grid.dx(i - 1);
243        let dx_plus = grid.dx(i);
244        let sum_dx = dx_minus + dx_plus;
245
246        let two = S::from_f64(2.0);
247        two * ((u[i + 1] - u[i]) / dx_plus - (u[i] - u[i - 1]) / dx_minus) / sum_dx
248    }
249
250    /// First derivative on non-uniform grid using central difference.
251    pub fn d1<S: Scalar>(u: &[S], grid: &Grid1D<S>, i: usize) -> S {
252        let dx_minus = grid.dx(i - 1);
253        let dx_plus = grid.dx(i);
254        let sum_dx = dx_minus + dx_plus;
255
256        (dx_minus * dx_minus * u[i + 1] + (dx_plus * dx_plus - dx_minus * dx_minus) * u[i]
257            - dx_plus * dx_plus * u[i - 1])
258            / (dx_minus * dx_plus * sum_dx)
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265
266    #[test]
267    fn test_d1_central() {
268        // f(x) = x², f'(x) = 2x
269        // At x = 1 (middle of [0, 2]), f'(1) = 2
270        let u = vec![0.0, 1.0, 4.0]; // x² at x = 0, 1, 2
271        let dx = 1.0;
272        let deriv = FDM::d1_central(&u, dx, 1);
273        assert!((deriv - 2.0).abs() < 1e-10);
274    }
275
276    #[test]
277    fn test_d2_central() {
278        // f(x) = x², f''(x) = 2
279        let u = vec![0.0, 1.0, 4.0]; // x² at x = 0, 1, 2
280        let dx = 1.0;
281        let deriv = FDM::d2_central(&u, dx, 1);
282        assert!((deriv - 2.0).abs() < 1e-10);
283    }
284
285    #[test]
286    fn test_d2_central_sine() {
287        // f(x) = sin(x), f''(x) = -sin(x)
288        let n = 101;
289        let dx = 0.1;
290        let u: Vec<f64> = (0..n).map(|i| (i as f64 * dx).sin()).collect();
291
292        // Check at middle point
293        let i = 50;
294        let x = i as f64 * dx;
295        let d2u = FDM::d2_central(&u, dx, i);
296        let exact = -(x).sin();
297        assert!(
298            (d2u - exact).abs() < 0.001,
299            "d2u = {}, exact = {}",
300            d2u,
301            exact
302        );
303    }
304
305    #[test]
306    fn test_boundary_derivatives() {
307        // f(x) = x², f'(x) = 2x
308        // More stable test function
309        let u = vec![0.0, 1.0, 4.0, 9.0, 16.0]; // x² at x = 0, 1, 2, 3, 4
310        let dx = 1.0;
311
312        let d1_left = FDM::d1_left_boundary(&u, dx);
313        let d1_right = FDM::d1_right_boundary(&u, dx);
314
315        // f'(0) = 0
316        assert!((d1_left - 0.0).abs() < 0.1, "d1_left = {}", d1_left);
317        // f'(4) = 8
318        assert!((d1_right - 8.0).abs() < 0.1, "d1_right = {}", d1_right);
319    }
320
321    #[test]
322    fn test_laplacian_1d() {
323        // f(x) = sin(πx), f''(x) = -π²sin(πx)
324        let n = 101;
325        let dx = 1.0 / (n as f64 - 1.0);
326        let u: Vec<f64> = (0..n)
327            .map(|i| (std::f64::consts::PI * i as f64 * dx).sin())
328            .collect();
329
330        let lap = FDM::laplacian_1d(&u, dx);
331
332        // Check at x = 0.5
333        let i = 50;
334        let x = i as f64 * dx;
335        let exact = -std::f64::consts::PI.powi(2) * (std::f64::consts::PI * x).sin();
336        let computed = lap[i - 1];
337
338        assert!(
339            (computed - exact).abs() < 0.1,
340            "computed = {}, exact = {}",
341            computed,
342            exact
343        );
344    }
345
346    #[test]
347    fn test_stencil() {
348        let dx = 0.1;
349        let stencil = Stencil::d2_central(dx);
350
351        let u = vec![0.0, 0.01, 0.04]; // x² at x = 0, 0.1, 0.2
352        let d2u = stencil.apply(&u, 1);
353        assert!((d2u - 2.0).abs() < 1e-10);
354    }
355
356    #[test]
357    fn test_non_uniform_d2() {
358        // f(x) = x², f''(x) = 2
359        let points = vec![0.0, 0.3, 0.8, 1.5];
360        let grid = Grid1D::from_points(points);
361        let u: Vec<f64> = grid.points().iter().map(|x| x * x).collect();
362
363        let d2u = NonUniformFDM::d2(&u, &grid, 1);
364        assert!((d2u - 2.0).abs() < 0.1, "d2u = {}", d2u);
365
366        let d2u = NonUniformFDM::d2(&u, &grid, 2);
367        assert!((d2u - 2.0).abs() < 0.1, "d2u = {}", d2u);
368    }
369}