Skip to main content

rust_physics_engine/
fields.rs

1//! Uniform-grid scalar fields.
2//!
3//! Minimal backfill of the Part 2 `ScalarField2`/`ScalarField3` types
4//! that later roadmap phases build on: row-major storage with grid
5//! spacing and bilinear/trilinear sampling.
6
7/// 2D scalar field on an nx×ny uniform grid with spacing dx
8/// (row-major: index = y·nx + x; physical position of node (i, j) is
9/// (i·dx, j·dx)).
10#[derive(Debug, Clone, PartialEq)]
11pub struct ScalarField2 {
12    pub nx: usize,
13    pub ny: usize,
14    pub dx: f64,
15    pub data: Vec<f64>,
16}
17
18impl ScalarField2 {
19    /// Zero-filled field.
20    #[must_use]
21    pub fn new(nx: usize, ny: usize, dx: f64) -> Self {
22        Self { nx, ny, dx, data: vec![0.0; nx * ny] }
23    }
24
25    /// Build from a function of the node position (x, y).
26    #[must_use]
27    pub fn from_fn(nx: usize, ny: usize, dx: f64, f: impl Fn(f64, f64) -> f64) -> Self {
28        let mut data = Vec::with_capacity(nx * ny);
29        for j in 0..ny {
30            for i in 0..nx {
31                data.push(f(i as f64 * dx, j as f64 * dx));
32            }
33        }
34        Self { nx, ny, dx, data }
35    }
36
37    /// Node value.
38    #[must_use]
39    pub fn get(&self, i: usize, j: usize) -> f64 {
40        self.data[j * self.nx + i]
41    }
42
43    /// Set a node value.
44    pub fn set(&mut self, i: usize, j: usize, v: f64) {
45        self.data[j * self.nx + i] = v;
46    }
47
48    /// Bilinear sample at a physical position (clamped to the grid).
49    #[must_use]
50    pub fn sample(&self, x: f64, y: f64) -> f64 {
51        let fx = (x / self.dx).clamp(0.0, (self.nx - 1) as f64);
52        let fy = (y / self.dx).clamp(0.0, (self.ny - 1) as f64);
53        let i0 = fx.floor() as usize;
54        let j0 = fy.floor() as usize;
55        let i1 = (i0 + 1).min(self.nx - 1);
56        let j1 = (j0 + 1).min(self.ny - 1);
57        let tx = fx - i0 as f64;
58        let ty = fy - j0 as f64;
59        self.get(i0, j0) * (1.0 - tx) * (1.0 - ty)
60            + self.get(i1, j0) * tx * (1.0 - ty)
61            + self.get(i0, j1) * (1.0 - tx) * ty
62            + self.get(i1, j1) * tx * ty
63    }
64
65    /// Smallest and largest node values.
66    #[must_use]
67    pub fn min_max(&self) -> (f64, f64) {
68        let mut lo = f64::INFINITY;
69        let mut hi = f64::NEG_INFINITY;
70        for &v in &self.data {
71            lo = lo.min(v);
72            hi = hi.max(v);
73        }
74        (lo, hi)
75    }
76}
77
78/// 3D scalar field on an nx×ny×nz uniform grid with spacing dx
79/// (index = (k·ny + j)·nx + i).
80#[derive(Debug, Clone, PartialEq)]
81pub struct ScalarField3 {
82    pub nx: usize,
83    pub ny: usize,
84    pub nz: usize,
85    pub dx: f64,
86    pub data: Vec<f64>,
87}
88
89impl ScalarField3 {
90    /// Zero-filled field.
91    #[must_use]
92    pub fn new(nx: usize, ny: usize, nz: usize, dx: f64) -> Self {
93        Self { nx, ny, nz, dx, data: vec![0.0; nx * ny * nz] }
94    }
95
96    /// Node value.
97    #[must_use]
98    pub fn get(&self, i: usize, j: usize, k: usize) -> f64 {
99        self.data[(k * self.ny + j) * self.nx + i]
100    }
101
102    /// Set a node value.
103    pub fn set(&mut self, i: usize, j: usize, k: usize, v: f64) {
104        self.data[(k * self.ny + j) * self.nx + i] = v;
105    }
106
107    /// Trilinear sample at a physical position (clamped to the grid).
108    #[must_use]
109    pub fn sample(&self, x: f64, y: f64, z: f64) -> f64 {
110        let fx = (x / self.dx).clamp(0.0, (self.nx - 1) as f64);
111        let fy = (y / self.dx).clamp(0.0, (self.ny - 1) as f64);
112        let fz = (z / self.dx).clamp(0.0, (self.nz - 1) as f64);
113        let i0 = fx.floor() as usize;
114        let j0 = fy.floor() as usize;
115        let k0 = fz.floor() as usize;
116        let i1 = (i0 + 1).min(self.nx - 1);
117        let j1 = (j0 + 1).min(self.ny - 1);
118        let k1 = (k0 + 1).min(self.nz - 1);
119        let tx = fx - i0 as f64;
120        let ty = fy - j0 as f64;
121        let tz = fz - k0 as f64;
122        let c00 = self.get(i0, j0, k0) * (1.0 - tx) + self.get(i1, j0, k0) * tx;
123        let c10 = self.get(i0, j1, k0) * (1.0 - tx) + self.get(i1, j1, k0) * tx;
124        let c01 = self.get(i0, j0, k1) * (1.0 - tx) + self.get(i1, j0, k1) * tx;
125        let c11 = self.get(i0, j1, k1) * (1.0 - tx) + self.get(i1, j1, k1) * tx;
126        let c0 = c00 * (1.0 - ty) + c10 * ty;
127        let c1 = c01 * (1.0 - ty) + c11 * ty;
128        c0 * (1.0 - tz) + c1 * tz
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_scalar_field2_sampling() {
138        let f = ScalarField2::from_fn(11, 11, 0.1, |x, y| 2.0 * x + 3.0 * y);
139        assert!((f.get(5, 5) - 2.5).abs() < 1e-12);
140        // Bilinear reproduces linear functions between nodes.
141        assert!((f.sample(0.33, 0.47) - (0.66 + 1.41)).abs() < 1e-12);
142        let (lo, hi) = f.min_max();
143        assert!((lo - 0.0).abs() < 1e-12 && (hi - 5.0).abs() < 1e-12);
144    }
145
146    #[test]
147    fn test_scalar_field2_new_is_zero_and_set_writes_one_node() {
148        let mut f = ScalarField2::new(5, 4, 0.25);
149        assert_eq!(f.nx, 5);
150        assert_eq!(f.ny, 4);
151        assert_eq!(f.dx, 0.25);
152        assert_eq!(f.data.len(), 20);
153        assert!(f.data.iter().all(|&v| v == 0.0), "new() is zero-filled");
154        assert_eq!(f.min_max(), (0.0, 0.0));
155        // set writes exactly one node and leaves the rest untouched.
156        f.set(3, 2, -4.5);
157        assert_eq!(f.get(3, 2), -4.5);
158        assert_eq!(f.data[2 * 5 + 3], -4.5, "row-major index j*nx + i");
159        assert_eq!(f.data.iter().filter(|&&v| v != 0.0).count(), 1);
160        assert_eq!(f.min_max(), (-4.5, 0.0));
161        // Sampling at that node returns the stored value; halfway to a
162        // zero neighbour is half of it (bilinear on a linear segment).
163        assert!((f.sample(3.0 * 0.25, 2.0 * 0.25) - (-4.5)).abs() < 1e-12);
164        assert!((f.sample(3.5 * 0.25, 2.0 * 0.25) - (-2.25)).abs() < 1e-12);
165    }
166
167    #[test]
168    fn test_scalar_field2_set_linear_field_has_constant_gradient() {
169        // Build g(x, y) = 3x - 2y node by node with set(), then check
170        // the central-difference gradient is exactly (3, -2) at every
171        // interior node (central differences are exact for linear data).
172        let (nx, ny, dx) = (9usize, 7usize, 0.5_f64);
173        let mut f = ScalarField2::new(nx, ny, dx);
174        for j in 0..ny {
175            for i in 0..nx {
176                f.set(i, j, 3.0 * (i as f64 * dx) - 2.0 * (j as f64 * dx));
177            }
178        }
179        // set() must agree with the equivalent from_fn construction.
180        let reference = ScalarField2::from_fn(nx, ny, dx, |x, y| 3.0 * x - 2.0 * y);
181        assert_eq!(f, reference);
182        for j in 1..ny - 1 {
183            for i in 1..nx - 1 {
184                let gx = (f.get(i + 1, j) - f.get(i - 1, j)) / (2.0 * dx);
185                let gy = (f.get(i, j + 1) - f.get(i, j - 1)) / (2.0 * dx);
186                assert!((gx - 3.0).abs() < 1e-12, "d/dx at ({i}, {j}) = {gx}");
187                assert!((gy + 2.0).abs() < 1e-12, "d/dy at ({i}, {j}) = {gy}");
188                // The 5-point Laplacian of a linear field vanishes.
189                let lap = (f.get(i + 1, j) + f.get(i - 1, j) + f.get(i, j + 1)
190                    + f.get(i, j - 1)
191                    - 4.0 * f.get(i, j))
192                    / (dx * dx);
193                assert!(lap.abs() < 1e-11, "laplacian at ({i}, {j}) = {lap}");
194            }
195        }
196        let (lo, hi) = f.min_max();
197        assert!((lo - (-2.0 * (ny - 1) as f64 * dx)).abs() < 1e-12);
198        assert!((hi - 3.0 * (nx - 1) as f64 * dx).abs() < 1e-12);
199    }
200
201    #[test]
202    fn test_scalar_field3_sampling() {
203        let mut f = ScalarField3::new(4, 4, 4, 1.0);
204        f.set(1, 2, 3, 7.0);
205        assert_eq!(f.get(1, 2, 3), 7.0);
206        // Trilinear at the node itself.
207        assert!((f.sample(1.0, 2.0, 3.0) - 7.0).abs() < 1e-12);
208        // Halfway to a zero neighbor.
209        assert!((f.sample(1.5, 2.0, 3.0) - 3.5).abs() < 1e-12);
210    }
211}