oxirs_physics/gpu/
navier_stokes_kernel.rs1use super::{GpuError, GpuResult};
21
22#[derive(Debug, Clone, PartialEq)]
24pub struct PressureGrid {
25 pub nx: usize,
27 pub ny: usize,
29 pub dx: f64,
31 pub dy: f64,
33 pub pressure: Vec<f64>,
35 pub source: Vec<f64>,
37}
38
39impl PressureGrid {
40 #[inline]
42 pub fn len(&self) -> usize {
43 self.nx * self.ny
44 }
45
46 #[inline]
48 pub fn is_empty(&self) -> bool {
49 self.nx == 0 || self.ny == 0
50 }
51}
52
53#[derive(Debug, Default)]
55pub struct NavierStokesKernelDispatcher {
56 backend_ready: bool,
57}
58
59impl NavierStokesKernelDispatcher {
60 pub fn new() -> Self {
62 Self {
63 backend_ready: super::backend_available(),
64 }
65 }
66
67 pub fn is_available(&self) -> bool {
69 self.backend_ready
70 }
71
72 pub fn dispatch_pressure_jacobi(&self, grid: &PressureGrid) -> GpuResult<Vec<f64>> {
84 validate_grid(grid)?;
85 #[cfg(feature = "gpu")]
86 {
87 if !self.backend_ready {
88 return Err(GpuError::BackendUnavailable);
89 }
90 Ok(Self::cpu_reference_jacobi(grid))
94 }
95 #[cfg(not(feature = "gpu"))]
96 {
97 Err(GpuError::BackendUnavailable)
98 }
99 }
100
101 pub fn dispatch_pressure_solve(
109 &self,
110 grid: &PressureGrid,
111 n_sweeps: usize,
112 ) -> GpuResult<PressureSolveOutput> {
113 validate_grid(grid)?;
114 #[cfg(feature = "gpu")]
115 {
116 if !self.backend_ready {
117 return Err(GpuError::BackendUnavailable);
118 }
119 let mut g = grid.clone();
120 for _ in 0..n_sweeps {
121 g.pressure = Self::cpu_reference_jacobi(&g);
122 }
123 let residual = Self::residual_l2(&g);
124 Ok(PressureSolveOutput {
125 pressure: g.pressure,
126 residual_l2: residual,
127 })
128 }
129 #[cfg(not(feature = "gpu"))]
130 {
131 let _ = n_sweeps;
132 Err(GpuError::BackendUnavailable)
133 }
134 }
135
136 pub fn cpu_reference_jacobi(grid: &PressureGrid) -> Vec<f64> {
139 let mut next = grid.pressure.clone();
140 if grid.nx < 3 || grid.ny < 3 {
141 return next;
142 }
143 let dx2 = grid.dx * grid.dx;
144 let dy2 = grid.dy * grid.dy;
145 let denom = 2.0 * (dx2 + dy2);
146 for j in 1..grid.ny - 1 {
147 for i in 1..grid.nx - 1 {
148 let idx = j * grid.nx + i;
149 let east = grid.pressure[idx + 1];
150 let west = grid.pressure[idx - 1];
151 let north = grid.pressure[idx + grid.nx];
152 let south = grid.pressure[idx - grid.nx];
153 next[idx] = (dy2 * (east + west) + dx2 * (north + south)
154 - dx2 * dy2 * grid.source[idx])
155 / denom;
156 }
157 }
158 next
159 }
160
161 pub fn residual_l2(grid: &PressureGrid) -> f64 {
163 if grid.nx < 3 || grid.ny < 3 {
164 return 0.0;
165 }
166 let dx2 = grid.dx * grid.dx;
167 let dy2 = grid.dy * grid.dy;
168 let mut sum = 0.0;
169 let mut n = 0usize;
170 for j in 1..grid.ny - 1 {
171 for i in 1..grid.nx - 1 {
172 let idx = j * grid.nx + i;
173 let lap = (grid.pressure[idx + 1] + grid.pressure[idx - 1]
174 - 2.0 * grid.pressure[idx])
175 / dx2
176 + (grid.pressure[idx + grid.nx] + grid.pressure[idx - grid.nx]
177 - 2.0 * grid.pressure[idx])
178 / dy2;
179 let r = lap - grid.source[idx];
180 sum += r * r;
181 n += 1;
182 }
183 }
184 if n == 0 {
185 0.0
186 } else {
187 (sum / n as f64).sqrt()
188 }
189 }
190}
191
192#[derive(Debug, Clone)]
194pub struct PressureSolveOutput {
195 pub pressure: Vec<f64>,
197 pub residual_l2: f64,
199}
200
201fn validate_grid(grid: &PressureGrid) -> GpuResult<()> {
202 if grid.is_empty() {
203 return Err(GpuError::InvalidInput(
204 "pressure grid has zero extent".to_string(),
205 ));
206 }
207 if grid.dx <= 0.0 || grid.dy <= 0.0 || !grid.dx.is_finite() || !grid.dy.is_finite() {
208 return Err(GpuError::InvalidInput(format!(
209 "pressure grid spacing invalid: dx={}, dy={}",
210 grid.dx, grid.dy
211 )));
212 }
213 let expected = grid.len();
214 if grid.pressure.len() != expected {
215 return Err(GpuError::InvalidInput(format!(
216 "pressure buffer has {} entries, expected {}",
217 grid.pressure.len(),
218 expected
219 )));
220 }
221 if grid.source.len() != expected {
222 return Err(GpuError::InvalidInput(format!(
223 "source buffer has {} entries, expected {}",
224 grid.source.len(),
225 expected
226 )));
227 }
228 Ok(())
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn flat_grid(nx: usize, ny: usize) -> PressureGrid {
236 PressureGrid {
237 nx,
238 ny,
239 dx: 0.1,
240 dy: 0.1,
241 pressure: vec![0.0; nx * ny],
242 source: vec![0.0; nx * ny],
243 }
244 }
245
246 #[test]
247 fn dispatcher_availability_matches_feature() {
248 let d = NavierStokesKernelDispatcher::new();
249 #[cfg(feature = "gpu")]
250 assert!(d.is_available());
251 #[cfg(not(feature = "gpu"))]
252 assert!(!d.is_available());
253 }
254
255 #[test]
256 fn jacobi_no_feature_returns_unavailable() {
257 let d = NavierStokesKernelDispatcher::new();
258 let g = flat_grid(5, 5);
259 let result = d.dispatch_pressure_jacobi(&g);
260 #[cfg(not(feature = "gpu"))]
261 assert!(matches!(result, Err(GpuError::BackendUnavailable)));
262 #[cfg(feature = "gpu")]
263 {
264 let next = result.expect("dispatch should succeed under gpu feature");
265 assert_eq!(next.len(), g.len());
266 }
267 }
268
269 #[test]
270 fn jacobi_zero_source_zero_initial_stays_zero() {
271 let g = flat_grid(5, 5);
272 let next = NavierStokesKernelDispatcher::cpu_reference_jacobi(&g);
273 assert_eq!(next.len(), g.len());
274 for v in &next {
275 assert!((*v).abs() < 1e-15);
276 }
277 }
278
279 #[test]
280 fn jacobi_dirichlet_boundary_held_fixed() {
281 let mut g = flat_grid(5, 5);
282 let last = g.len() - 1;
283 g.pressure[0] = 1.0;
284 g.pressure[last] = 7.0;
285 let next = NavierStokesKernelDispatcher::cpu_reference_jacobi(&g);
286 assert_eq!(next[0], 1.0);
287 assert_eq!(next[last], 7.0);
288 }
289
290 #[test]
291 fn solve_reduces_residual() {
292 let d = NavierStokesKernelDispatcher::new();
293 let mut g = flat_grid(7, 7);
294 let centre = 3 * g.nx + 3;
296 g.source[centre] = -5.0;
297 let result = d.dispatch_pressure_solve(&g, 50);
298 #[cfg(not(feature = "gpu"))]
299 assert!(matches!(result, Err(GpuError::BackendUnavailable)));
300 #[cfg(feature = "gpu")]
301 {
302 let out = result.expect("solve should succeed under gpu feature");
303 let initial_residual = NavierStokesKernelDispatcher::residual_l2(&g);
304 assert!(
305 out.residual_l2 < initial_residual,
306 "residual must decrease after relaxation: before {initial_residual:e} after {:e}",
307 out.residual_l2
308 );
309 }
310 }
311
312 #[test]
313 fn invalid_grid_caught_eagerly() {
314 let d = NavierStokesKernelDispatcher::new();
315 let bad = PressureGrid {
316 nx: 3,
317 ny: 3,
318 dx: -0.1,
319 dy: 0.1,
320 pressure: vec![0.0; 9],
321 source: vec![0.0; 9],
322 };
323 let result = d.dispatch_pressure_jacobi(&bad);
324 assert!(matches!(result, Err(GpuError::InvalidInput(_))));
325 }
326
327 #[test]
328 fn invalid_buffer_size_caught_eagerly() {
329 let d = NavierStokesKernelDispatcher::new();
330 let bad = PressureGrid {
331 nx: 3,
332 ny: 3,
333 dx: 0.1,
334 dy: 0.1,
335 pressure: vec![0.0; 10], source: vec![0.0; 9],
337 };
338 let result = d.dispatch_pressure_jacobi(&bad);
339 assert!(matches!(result, Err(GpuError::InvalidInput(_))));
340 }
341}