1#[allow(dead_code)]
16#[derive(Debug, Clone)]
17pub struct GpuEulerGrid {
18 pub nx: usize,
20 pub ny: usize,
22 pub nz: usize,
24 pub dx: f64,
26 pub rho: Vec<f64>,
28 pub u: Vec<f64>,
30 pub v: Vec<f64>,
32 pub w: Vec<f64>,
34 pub p: Vec<f64>,
36}
37
38impl GpuEulerGrid {
39 pub fn new(nx: usize, ny: usize, nz: usize, dx: f64) -> Self {
41 let nc = nx * ny * nz;
42 Self {
43 nx,
44 ny,
45 nz,
46 dx,
47 rho: vec![1.0; nc],
48 u: vec![0.0; (nx + 1) * ny * nz],
49 v: vec![0.0; nx * (ny + 1) * nz],
50 w: vec![0.0; nx * ny * (nz + 1)],
51 p: vec![0.0; nc],
52 }
53 }
54
55 pub fn index(&self, i: usize, j: usize, k: usize) -> usize {
57 k * self.nx * self.ny + j * self.nx + i
58 }
59
60 pub fn cell_count(&self) -> usize {
62 self.nx * self.ny * self.nz
63 }
64
65 pub fn max_velocity(&self) -> f64 {
67 let mu = self.u.iter().cloned().fold(0.0_f64, |a, v| a.max(v.abs()));
68 let mv = self.v.iter().cloned().fold(0.0_f64, |a, v| a.max(v.abs()));
69 let mw = self.w.iter().cloned().fold(0.0_f64, |a, v| a.max(v.abs()));
70 mu.max(mv).max(mw)
71 }
72
73 pub fn cfl_timestep(&self, cfl: f64) -> f64 {
75 let vmax = self.max_velocity();
76 if vmax < 1e-15 {
77 return self.dx;
78 }
79 cfl * self.dx / vmax
80 }
81
82 pub fn gpu_apply_gravity(&mut self, gravity: f64, dt: f64) {
84 for val in self.v.iter_mut() {
85 *val += gravity * dt;
86 }
87 }
88
89 pub fn gpu_compute_divergence(&self) -> Vec<f64> {
91 let nc = self.cell_count();
92 let mut div = vec![0.0f64; nc];
93 let dx = self.dx;
94 for k in 0..self.nz {
95 for j in 0..self.ny {
96 for i in 0..self.nx {
97 let idx = self.index(i, j, k);
98 let ui_p = self.u[k * (self.nx + 1) * self.ny + j * (self.nx + 1) + i + 1];
100 let ui_m = self.u[k * (self.nx + 1) * self.ny + j * (self.nx + 1) + i];
101 let vj_p = self.v[k * self.nx * (self.ny + 1) + (j + 1) * self.nx + i];
103 let vj_m = self.v[k * self.nx * (self.ny + 1) + j * self.nx + i];
104 let wk_p = self.w[(k + 1) * self.nx * self.ny + j * self.nx + i];
106 let wk_m = self.w[k * self.nx * self.ny + j * self.nx + i];
107 div[idx] = (ui_p - ui_m + vj_p - vj_m + wk_p - wk_m) / dx;
108 }
109 }
110 }
111 div
112 }
113
114 pub fn divergence_at(&self, i: usize, j: usize, k: usize) -> f64 {
116 let div = self.gpu_compute_divergence();
117 div[self.index(i, j, k)]
118 }
119
120 pub fn vorticity_at(&self, i: usize, j: usize, k: usize) -> [f64; 3] {
124 let dx = self.dx;
125 let ip = i.min(self.nx - 1);
127 let im = if i > 0 { i - 1 } else { 0 };
128 let jp = j.min(self.ny - 1);
129 let jm = if j > 0 { j - 1 } else { 0 };
130 let kp = k.min(self.nz - 1);
131 let km = if k > 0 { k - 1 } else { 0 };
132
133 let u_jm = self.u[km * (self.nx + 1) * self.ny + jm * (self.nx + 1) + i];
135 let u_jp = self.u[kp * (self.nx + 1) * self.ny + jp * (self.nx + 1) + i];
136 let u_km = self.u[km * (self.nx + 1) * self.ny + j * (self.nx + 1) + i];
137 let u_kp = self.u[kp * (self.nx + 1) * self.ny + j * (self.nx + 1) + i];
138
139 let v_im = self.v[k * self.nx * (self.ny + 1) + j * self.nx + im];
140 let v_ip = self.v[k * self.nx * (self.ny + 1) + j * self.nx + ip];
141 let v_km = self.v[km * self.nx * (self.ny + 1) + j * self.nx + i];
142 let v_kp = self.v[kp * self.nx * (self.ny + 1) + j * self.nx + i];
143
144 let w_im = self.w[k * self.nx * self.ny + j * self.nx + im];
145 let w_ip = self.w[k * self.nx * self.ny + j * self.nx + ip];
146 let w_jm = self.w[k * self.nx * self.ny + jm * self.nx + i];
147 let w_jp = self.w[k * self.nx * self.ny + jp * self.nx + i];
148
149 let wx = (w_jp - w_jm) / (2.0 * dx) - (v_kp - v_km) / (2.0 * dx);
150 let wy = (u_kp - u_km) / (2.0 * dx) - (w_ip - w_im) / (2.0 * dx);
151 let wz = (v_ip - v_im) / (2.0 * dx) - (u_jp - u_jm) / (2.0 * dx);
152 [wx, wy, wz]
153 }
154
155 pub fn gpu_enforce_solid_bc(&mut self) {
157 for k in 0..self.nz {
159 for j in 0..self.ny {
160 self.u[k * (self.nx + 1) * self.ny + j * (self.nx + 1)] = 0.0;
161 self.u[k * (self.nx + 1) * self.ny + j * (self.nx + 1) + self.nx] = 0.0;
162 }
163 }
164 for k in 0..self.nz {
166 for i in 0..self.nx {
167 self.v[k * self.nx * (self.ny + 1) + i] = 0.0;
168 self.v[k * self.nx * (self.ny + 1) + self.ny * self.nx + i] = 0.0;
169 }
170 }
171 for j in 0..self.ny {
173 for i in 0..self.nx {
174 self.w[j * self.nx + i] = 0.0;
175 self.w[self.nz * self.nx * self.ny + j * self.nx + i] = 0.0;
176 }
177 }
178 }
179
180 pub fn gpu_pressure_projection(&mut self, max_iter: usize, tol: f64) -> f64 {
184 let dx = self.dx;
185 let dx2 = dx * dx;
186 let div = self.gpu_compute_divergence();
187 let nc = self.cell_count();
188 let mut residual = 0.0f64;
189
190 for _iter in 0..max_iter {
191 let p_old = self.p.clone();
192 residual = 0.0;
193 for k in 0..self.nz {
194 for j in 0..self.ny {
195 for i in 0..self.nx {
196 let idx = self.index(i, j, k);
197 let px_p = if i + 1 < self.nx {
199 p_old[self.index(i + 1, j, k)]
200 } else {
201 p_old[idx]
202 };
203 let px_m = if i > 0 {
204 p_old[self.index(i - 1, j, k)]
205 } else {
206 p_old[idx]
207 };
208 let py_p = if j + 1 < self.ny {
209 p_old[self.index(i, j + 1, k)]
210 } else {
211 p_old[idx]
212 };
213 let py_m = if j > 0 {
214 p_old[self.index(i, j - 1, k)]
215 } else {
216 p_old[idx]
217 };
218 let pz_p = if k + 1 < self.nz {
219 p_old[self.index(i, j, k + 1)]
220 } else {
221 p_old[idx]
222 };
223 let pz_m = if k > 0 {
224 p_old[self.index(i, j, k - 1)]
225 } else {
226 p_old[idx]
227 };
228 let p_new =
229 (px_p + px_m + py_p + py_m + pz_p + pz_m - dx2 * div[idx]) / 6.0;
230 let diff = (p_new - self.p[idx]).abs();
231 if diff > residual {
232 residual = diff;
233 }
234 self.p[idx] = p_new;
235 }
236 }
237 }
238 if residual < tol {
239 break;
240 }
241 }
242 let _ = nc;
243 residual
244 }
245
246 pub fn gpu_update_velocity_from_pressure(&mut self, dt: f64) {
248 let dx = self.dx;
249 for k in 0..self.nz {
251 for j in 0..self.ny {
252 for i in 1..self.nx {
253 let idx_r = self.index(i, j, k);
254 let idx_l = self.index(i - 1, j, k);
255 let fi = k * (self.nx + 1) * self.ny + j * (self.nx + 1) + i;
256 self.u[fi] -= dt * (self.p[idx_r] - self.p[idx_l]) / dx;
257 }
258 }
259 }
260 for k in 0..self.nz {
262 for j in 1..self.ny {
263 for i in 0..self.nx {
264 let idx_t = self.index(i, j, k);
265 let idx_b = self.index(i, j - 1, k);
266 let fj = k * self.nx * (self.ny + 1) + j * self.nx + i;
267 self.v[fj] -= dt * (self.p[idx_t] - self.p[idx_b]) / dx;
268 }
269 }
270 }
271 for k in 1..self.nz {
273 for j in 0..self.ny {
274 for i in 0..self.nx {
275 let idx_f = self.index(i, j, k);
276 let idx_b = self.index(i, j, k - 1);
277 let fk = k * self.nx * self.ny + j * self.nx + i;
278 self.w[fk] -= dt * (self.p[idx_f] - self.p[idx_b]) / dx;
279 }
280 }
281 }
282 }
283
284 pub fn gpu_advect_semi_lagrange(&mut self, dt: f64) {
286 let dx = self.dx;
287 let nx = self.nx;
288 let ny = self.ny;
289 let nz = self.nz;
290
291 let u_old = self.u.clone();
293 let v_old = self.v.clone();
294 let w_old = self.w.clone();
295 for k in 0..nz {
296 for j in 0..ny {
297 for i in 0..=nx {
298 let fi = k * (nx + 1) * ny + j * (nx + 1) + i;
299 let vavg = if i < nx {
301 0.5 * (v_old[k * nx * (ny + 1) + j * nx + i]
302 + v_old[k * nx * (ny + 1) + (j + 1).min(ny) * nx + i])
303 } else {
304 0.0
305 };
306 let wavg = if i < nx {
307 0.5 * (w_old[k * nx * ny + j * nx + i]
308 + w_old[(k + 1).min(nz) * nx * ny + j * nx + i])
309 } else {
310 0.0
311 };
312 let xi = i as f64 * dx - u_old[fi] * dt;
313 let yi = (j as f64 + 0.5) * dx - vavg * dt;
314 let zi = (k as f64 + 0.5) * dx - wavg * dt;
315 self.u[fi] = trilinear_u(&u_old, xi, yi, zi, nx, ny, nz, dx);
316 }
317 }
318 }
319 for k in 0..nz {
321 for j in 0..=ny {
322 for i in 0..nx {
323 let fj = k * nx * (ny + 1) + j * nx + i;
324 let uavg = if j < ny {
325 0.5 * (u_old[k * (nx + 1) * ny + j * (nx + 1) + i]
326 + u_old[k * (nx + 1) * ny + j * (nx + 1) + i + 1])
327 } else {
328 0.0
329 };
330 let wavg = if j < ny {
331 0.5 * (w_old[k * nx * ny + j * nx + i]
332 + w_old[(k + 1).min(nz) * nx * ny + j * nx + i])
333 } else {
334 0.0
335 };
336 let xi = (i as f64 + 0.5) * dx - uavg * dt;
337 let yi = j as f64 * dx - v_old[fj] * dt;
338 let zi = (k as f64 + 0.5) * dx - wavg * dt;
339 self.v[fj] = trilinear_v(&v_old, xi, yi, zi, nx, ny, nz, dx);
340 }
341 }
342 }
343 for k in 0..=nz {
345 for j in 0..ny {
346 for i in 0..nx {
347 let fk = k * nx * ny + j * nx + i;
348 let uavg = if k < nz {
349 0.5 * (u_old[k * (nx + 1) * ny + j * (nx + 1) + i]
350 + u_old[k * (nx + 1) * ny + j * (nx + 1) + i + 1])
351 } else {
352 0.0
353 };
354 let xi = (i as f64 + 0.5) * dx - uavg * dt;
355 let zi = k as f64 * dx - w_old[fk] * dt;
356 self.w[fk] = trilinear_w(&w_old, xi, zi, i, j, k, nx, ny, nz, dx);
357 }
358 }
359 }
360 }
361
362 pub fn gpu_vorticity_confinement(&mut self, epsilon: f64) {
365 let nx = self.nx;
366 let ny = self.ny;
367 let nz = self.nz;
368 let dx = self.dx;
369 let mut eta: Vec<[f64; 3]> = vec![[0.0; 3]; nx * ny * nz];
371 for k in 0..nz {
372 for j in 0..ny {
373 for i in 0..nx {
374 eta[k * nx * ny + j * nx + i] = self.vorticity_at(i, j, k);
375 }
376 }
377 }
378 for k in 1..nz - 1 {
380 for j in 1..ny - 1 {
381 for i in 1..nx - 1 {
382 let mag = |e: [f64; 3]| (e[0] * e[0] + e[1] * e[1] + e[2] * e[2]).sqrt();
383 let m_ip = mag(eta[k * nx * ny + j * nx + i + 1]);
384 let m_im = mag(eta[k * nx * ny + j * nx + i - 1]);
385 let m_jp = mag(eta[k * nx * ny + (j + 1) * nx + i]);
386 let m_jm = mag(eta[k * nx * ny + (j - 1) * nx + i]);
387 let m_kp = mag(eta[(k + 1) * nx * ny + j * nx + i]);
388 let m_km = mag(eta[(k - 1) * nx * ny + j * nx + i]);
389 let nx_grad = (m_ip - m_im) / (2.0 * dx);
390 let ny_grad = (m_jp - m_jm) / (2.0 * dx);
391 let nz_grad = (m_kp - m_km) / (2.0 * dx);
392 let norm = (nx_grad * nx_grad + ny_grad * ny_grad + nz_grad * nz_grad).sqrt();
393 if norm > 1e-15 {
394 let omega = eta[k * nx * ny + j * nx + i];
395 let nx_n = nx_grad / norm;
396 let ny_n = ny_grad / norm;
397 let nz_n = nz_grad / norm;
398 let fx = epsilon * (ny_n * omega[2] - nz_n * omega[1]);
400 let fy = epsilon * (nz_n * omega[0] - nx_n * omega[2]);
401 let _fz = epsilon * (nx_n * omega[1] - ny_n * omega[0]);
402 let fj = k * nx * (ny + 1) + j * nx + i;
404 self.v[fj] += fy;
405 let fi = k * (nx + 1) * ny + j * (nx + 1) + i;
406 self.u[fi] += fx;
407 }
408 }
409 }
410 }
411 }
412
413 pub fn compute_stats(&self) -> FluidSimStats {
415 let max_velocity = self.max_velocity();
416 let total_kinetic_energy = self
417 .u
418 .iter()
419 .chain(self.v.iter())
420 .chain(self.w.iter())
421 .map(|&vi| 0.5 * vi * vi)
422 .sum();
423 let div = self.gpu_compute_divergence();
424 let max_divergence = div.iter().cloned().fold(0.0_f64, |a, v| a.max(v.abs()));
425 FluidSimStats {
426 max_velocity,
427 total_kinetic_energy,
428 max_divergence,
429 pressure_residual: 0.0,
430 }
431 }
432}
433
434fn clamp_idx(v: f64, n: usize) -> (usize, usize, f64) {
437 let scaled = v.max(0.0).min((n as f64) - 1.0 - 1e-9);
438 let i0 = scaled as usize;
439 let i1 = (i0 + 1).min(n - 1);
440 (i0, i1, scaled - i0 as f64)
441}
442
443#[allow(clippy::too_many_arguments)]
444fn trilinear_u(u: &[f64], x: f64, y: f64, z: f64, nx: usize, ny: usize, nz: usize, dx: f64) -> f64 {
445 let (i0, i1, tx) = clamp_idx(x / dx, nx + 1);
446 let (j0, j1, ty) = clamp_idx(y / dx - 0.5, ny);
447 let (k0, k1, tz) = clamp_idx(z / dx - 0.5, nz);
448 let idx = |i: usize, j: usize, k: usize| k * (nx + 1) * ny + j * (nx + 1) + i;
449 let u000 = u[idx(i0, j0, k0)];
450 let u100 = u[idx(i1, j0, k0)];
451 let u010 = u[idx(i0, j1, k0)];
452 let u110 = u[idx(i1, j1, k0)];
453 let u001 = u[idx(i0, j0, k1)];
454 let u101 = u[idx(i1, j0, k1)];
455 let u011 = u[idx(i0, j1, k1)];
456 let u111 = u[idx(i1, j1, k1)];
457 let lerp = |a: f64, b: f64, t: f64| a + t * (b - a);
458 lerp(
459 lerp(lerp(u000, u100, tx), lerp(u010, u110, tx), ty),
460 lerp(lerp(u001, u101, tx), lerp(u011, u111, tx), ty),
461 tz,
462 )
463}
464
465#[allow(clippy::too_many_arguments)]
466fn trilinear_v(v: &[f64], x: f64, y: f64, z: f64, nx: usize, ny: usize, nz: usize, dx: f64) -> f64 {
467 let (i0, i1, tx) = clamp_idx(x / dx - 0.5, nx);
468 let (j0, j1, ty) = clamp_idx(y / dx, ny + 1);
469 let (k0, k1, tz) = clamp_idx(z / dx - 0.5, nz);
470 let idx = |i: usize, j: usize, k: usize| k * nx * (ny + 1) + j * nx + i;
471 let v000 = v[idx(i0, j0, k0)];
472 let v100 = v[idx(i1, j0, k0)];
473 let v010 = v[idx(i0, j1, k0)];
474 let v110 = v[idx(i1, j1, k0)];
475 let v001 = v[idx(i0, j0, k1)];
476 let v101 = v[idx(i1, j0, k1)];
477 let v011 = v[idx(i0, j1, k1)];
478 let v111 = v[idx(i1, j1, k1)];
479 let lerp = |a: f64, b: f64, t: f64| a + t * (b - a);
480 lerp(
481 lerp(lerp(v000, v100, tx), lerp(v010, v110, tx), ty),
482 lerp(lerp(v001, v101, tx), lerp(v011, v111, tx), ty),
483 tz,
484 )
485}
486
487#[allow(clippy::too_many_arguments)]
488fn trilinear_w(
489 w: &[f64],
490 x: f64,
491 z: f64,
492 _i: usize,
493 j: usize,
494 k: usize,
495 nx: usize,
496 ny: usize,
497 nz: usize,
498 dx: f64,
499) -> f64 {
500 let (i0, i1, tx) = clamp_idx(x / dx - 0.5, nx);
502 let (k0, k1, tz) = clamp_idx(z / dx, nz + 1);
503 let idx = |ii: usize, jj: usize, kk: usize| kk * nx * ny + jj * nx + ii;
504 let j_c = j.min(ny - 1);
505 let w000 = w[idx(i0, j_c, k0)];
506 let w100 = w[idx(i1, j_c, k0)];
507 let w001 = w[idx(i0, j_c, k1)];
508 let w101 = w[idx(i1, j_c, k1)];
509 let lerp = |a: f64, b: f64, t: f64| a + t * (b - a);
510 let _ = (k, dx);
511 lerp(lerp(w000, w100, tx), lerp(w001, w101, tx), tz)
512}
513
514#[allow(dead_code)]
518#[derive(Debug, Clone)]
519pub struct FluidSimStats {
520 pub max_velocity: f64,
522 pub total_kinetic_energy: f64,
524 pub max_divergence: f64,
526 pub pressure_residual: f64,
528}
529
530pub fn marker_and_cell_init(
534 grid: &mut GpuEulerGrid,
535 region: [usize; 6],
536 u0: f64,
537 v0: f64,
538 w0: f64,
539) {
540 let [ri0, ri1, rj0, rj1, rk0, rk1] = region;
541 for k in rk0..rk1.min(grid.nz) {
542 for j in rj0..rj1.min(grid.ny) {
543 for i in ri0..ri1.min(grid.nx) {
544 let fi = k * (grid.nx + 1) * grid.ny + j * (grid.nx + 1) + i;
546 grid.u[fi] = u0;
547 let fj = k * grid.nx * (grid.ny + 1) + j * grid.nx + i;
549 grid.v[fj] = v0;
550 let fk = k * grid.nx * grid.ny + j * grid.nx + i;
552 grid.w[fk] = w0;
553 }
554 }
555 }
556}
557
558#[cfg(test)]
561mod tests {
562 use super::*;
563
564 fn make_grid() -> GpuEulerGrid {
565 GpuEulerGrid::new(4, 4, 4, 0.1)
566 }
567
568 #[test]
569 fn test_cell_count() {
570 let g = make_grid();
571 assert_eq!(g.cell_count(), 64);
572 }
573
574 #[test]
575 fn test_cell_count_custom() {
576 let g = GpuEulerGrid::new(3, 5, 7, 0.1);
577 assert_eq!(g.cell_count(), 3 * 5 * 7);
578 }
579
580 #[test]
581 fn test_index_origin() {
582 let g = make_grid();
583 assert_eq!(g.index(0, 0, 0), 0);
584 }
585
586 #[test]
587 fn test_index_last_cell() {
588 let g = make_grid();
589 assert_eq!(g.index(3, 3, 3), 63);
590 }
591
592 #[test]
593 fn test_index_roundtrip() {
594 let g = make_grid();
595 for k in 0..g.nz {
596 for j in 0..g.ny {
597 for i in 0..g.nx {
598 let flat = g.index(i, j, k);
599 let kk = flat / (g.nx * g.ny);
600 let jj = (flat % (g.nx * g.ny)) / g.nx;
601 let ii = flat % g.nx;
602 assert_eq!((ii, jj, kk), (i, j, k));
603 }
604 }
605 }
606 }
607
608 #[test]
609 fn test_initial_max_velocity_zero() {
610 let g = make_grid();
611 assert!(g.max_velocity() < 1e-15);
612 }
613
614 #[test]
615 fn test_cfl_timestep_positive() {
616 let mut g = make_grid();
617 g.u[10] = 1.0;
618 let dt = g.cfl_timestep(0.5);
619 assert!(dt > 0.0);
620 }
621
622 #[test]
623 fn test_cfl_timestep_no_velocity() {
624 let g = make_grid();
625 let dt = g.cfl_timestep(0.5);
626 assert_eq!(dt, g.dx);
627 }
628
629 #[test]
630 fn test_gravity_increases_v() {
631 let mut g = make_grid();
632 let v_before: Vec<f64> = g.v.clone();
633 g.gpu_apply_gravity(-9.81, 0.01);
634 let v_after = &g.v;
635 let changed = v_before
637 .iter()
638 .zip(v_after.iter())
639 .any(|(&a, &b)| (b - a).abs() > 1e-12);
640 assert!(changed);
641 }
642
643 #[test]
644 fn test_gravity_value() {
645 let mut g = GpuEulerGrid::new(2, 2, 2, 0.1);
646 g.gpu_apply_gravity(-10.0, 0.1);
647 assert!(g.v.iter().all(|&vi| (vi + 1.0).abs() < 1e-12));
649 }
650
651 #[test]
652 fn test_solid_bc_zeroes_boundary_u() {
653 let mut g = make_grid();
654 for v in g.u.iter_mut() {
655 *v = 5.0;
656 }
657 g.gpu_enforce_solid_bc();
658 for k in 0..g.nz {
660 for j in 0..g.ny {
661 assert_eq!(g.u[k * (g.nx + 1) * g.ny + j * (g.nx + 1)], 0.0);
662 }
663 }
664 }
665
666 #[test]
667 fn test_solid_bc_zeroes_boundary_v() {
668 let mut g = make_grid();
669 for v in g.v.iter_mut() {
670 *v = 3.0;
671 }
672 g.gpu_enforce_solid_bc();
673 for k in 0..g.nz {
675 for i in 0..g.nx {
676 assert_eq!(g.v[k * g.nx * (g.ny + 1) + i], 0.0);
677 }
678 }
679 }
680
681 #[test]
682 fn test_solid_bc_zeroes_boundary_w() {
683 let mut g = make_grid();
684 for w in g.w.iter_mut() {
685 *w = 2.0;
686 }
687 g.gpu_enforce_solid_bc();
688 for j in 0..g.ny {
690 for i in 0..g.nx {
691 assert_eq!(g.w[j * g.nx + i], 0.0);
692 }
693 }
694 }
695
696 #[test]
697 fn test_divergence_zero_initially() {
698 let g = make_grid();
699 let div = g.gpu_compute_divergence();
700 assert!(div.iter().all(|&d| d.abs() < 1e-12));
701 }
702
703 #[test]
704 fn test_pressure_projection_reduces_divergence() {
705 let mut g = GpuEulerGrid::new(4, 4, 4, 0.1);
706 marker_and_cell_init(&mut g, [1, 3, 1, 3, 1, 3], 1.0, 0.5, 0.25);
708 let div_before: f64 = g.gpu_compute_divergence().iter().map(|v| v.abs()).sum();
709 g.gpu_pressure_projection(100, 1e-6);
710 g.gpu_update_velocity_from_pressure(0.01);
711 let div_after: f64 = g.gpu_compute_divergence().iter().map(|v| v.abs()).sum();
712 assert!(div_after <= div_before + 1e-8);
714 }
715
716 #[test]
717 fn test_pressure_projection_returns_residual() {
718 let mut g = make_grid();
719 let res = g.gpu_pressure_projection(10, 1e-6);
720 assert!(res >= 0.0);
721 }
722
723 #[test]
724 fn test_update_velocity_from_pressure_finite() {
725 let mut g = make_grid();
726 g.p[0] = 10.0;
727 g.gpu_update_velocity_from_pressure(0.01);
728 assert!(g.u.iter().all(|v| v.is_finite()));
729 assert!(g.v.iter().all(|v| v.is_finite()));
730 }
731
732 #[test]
733 fn test_marker_and_cell_init_sets_u() {
734 let mut g = make_grid();
735 marker_and_cell_init(&mut g, [0, 2, 0, 2, 0, 2], 3.0, 0.0, 0.0);
736 assert!(g.u.iter().any(|&v| (v - 3.0).abs() < 1e-12));
738 }
739
740 #[test]
741 fn test_vorticity_at_zero_field() {
742 let g = make_grid();
743 let vort = g.vorticity_at(2, 2, 2);
744 assert!(vort.iter().all(|v| v.abs() < 1e-12));
745 }
746
747 #[test]
748 fn test_divergence_at_cell() {
749 let g = make_grid();
750 let d = g.divergence_at(0, 0, 0);
751 assert!(d.abs() < 1e-12);
752 }
753
754 #[test]
755 fn test_stats_max_velocity_nonneg() {
756 let g = make_grid();
757 let stats = g.compute_stats();
758 assert!(stats.max_velocity >= 0.0);
759 }
760
761 #[test]
762 fn test_stats_kinetic_energy_nonneg() {
763 let g = make_grid();
764 let stats = g.compute_stats();
765 assert!(stats.total_kinetic_energy >= 0.0);
766 }
767
768 #[test]
769 fn test_stats_max_divergence_nonneg() {
770 let g = make_grid();
771 let stats = g.compute_stats();
772 assert!(stats.max_divergence >= 0.0);
773 }
774
775 #[test]
776 fn test_advect_does_not_panic() {
777 let mut g = GpuEulerGrid::new(4, 4, 4, 0.1);
778 g.u[5] = 0.5;
779 g.gpu_advect_semi_lagrange(0.01);
780 }
782
783 #[test]
784 fn test_vorticity_confinement_does_not_panic() {
785 let mut g = GpuEulerGrid::new(4, 4, 4, 0.1);
786 g.u[10] = 1.0;
787 g.gpu_vorticity_confinement(0.01);
788 }
789
790 #[test]
791 fn test_grid_clone() {
792 let g = make_grid();
793 let g2 = g.clone();
794 assert_eq!(g2.cell_count(), g.cell_count());
795 }
796
797 #[test]
798 fn test_fluid_sim_stats_debug() {
799 let stats = FluidSimStats {
800 max_velocity: 1.0,
801 total_kinetic_energy: 2.0,
802 max_divergence: 0.1,
803 pressure_residual: 0.01,
804 };
805 let s = format!("{stats:?}");
806 assert!(s.contains("FluidSimStats"));
807 }
808
809 #[test]
810 fn test_cfl_formula() {
811 let mut g = GpuEulerGrid::new(4, 4, 4, 0.2);
812 g.u[0] = 2.0;
813 let dt = g.cfl_timestep(0.5);
814 assert!((dt - 0.5 * 0.2 / 2.0).abs() < 1e-12);
815 }
816
817 #[test]
818 fn test_rho_initialised_to_one() {
819 let g = make_grid();
820 assert!(g.rho.iter().all(|&r| (r - 1.0).abs() < 1e-12));
821 }
822}