1#![allow(clippy::needless_range_loop)]
2#![allow(missing_docs)]
12
13use serde::{Deserialize, Serialize};
14
15const W: [f64; 9] = [
21 4.0 / 9.0,
22 1.0 / 9.0,
23 1.0 / 9.0,
24 1.0 / 9.0,
25 1.0 / 9.0,
26 1.0 / 36.0,
27 1.0 / 36.0,
28 1.0 / 36.0,
29 1.0 / 36.0,
30];
31
32const EX: [i32; 9] = [0, 1, 0, -1, 0, 1, -1, -1, 1];
34const EY: [i32; 9] = [0, 0, 1, 0, -1, 1, 1, -1, -1];
36const OPP: [usize; 9] = [0, 3, 4, 1, 2, 7, 8, 5, 6];
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum LbmBoundary {
46 Fluid,
48 NoSlipWall,
50 Periodic,
52 Inlet,
54 Outlet,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct PyLbmConfig {
65 pub width: usize,
67 pub height: usize,
69 pub viscosity: f64,
71 pub body_force: [f64; 2],
73 pub init_velocity: [f64; 2],
75 pub init_density: f64,
77}
78
79impl PyLbmConfig {
80 pub fn new(width: usize, height: usize, viscosity: f64) -> Self {
82 Self {
83 width,
84 height,
85 viscosity: viscosity.max(1e-8),
86 body_force: [0.0; 2],
87 init_velocity: [0.0; 2],
88 init_density: 1.0,
89 }
90 }
91
92 pub fn lid_driven_cavity() -> Self {
94 Self::new(64, 64, 0.01)
95 }
96
97 pub fn channel_flow() -> Self {
99 let mut cfg = Self::new(128, 32, 0.01);
100 cfg.body_force = [1e-4, 0.0];
101 cfg
102 }
103
104 pub fn omega(&self) -> f64 {
106 1.0 / (3.0 * self.viscosity + 0.5)
107 }
108}
109
110impl Default for PyLbmConfig {
111 fn default() -> Self {
112 Self::new(32, 32, 0.01)
113 }
114}
115
116#[derive(Debug, Clone)]
129pub struct PyLbmSimulation {
130 width: usize,
132 height: usize,
134 omega: f64,
136 body_force: [f64; 2],
138 f: Vec<f64>,
140 f_tmp: Vec<f64>,
142 boundary: Vec<LbmBoundary>,
144 lid_velocity: Option<[f64; 2]>,
146 step_count: u64,
148}
149
150impl PyLbmSimulation {
151 pub fn new(config: &PyLbmConfig) -> Self {
155 let n = config.width * config.height;
156 let ux0 = config.init_velocity[0];
157 let uy0 = config.init_velocity[1];
158 let rho0 = config.init_density;
159
160 let mut f = vec![0.0f64; n * 9];
161 for i in 0..n {
162 let feq = equilibrium(rho0, ux0, uy0);
163 for q in 0..9 {
164 f[i * 9 + q] = feq[q];
165 }
166 }
167 let f_tmp = f.clone();
168 let boundary = vec![LbmBoundary::Fluid; n];
169
170 Self {
171 width: config.width,
172 height: config.height,
173 omega: config.omega(),
174 body_force: config.body_force,
175 f,
176 f_tmp,
177 boundary,
178 lid_velocity: None,
179 step_count: 0,
180 }
181 }
182
183 pub fn width(&self) -> usize {
185 self.width
186 }
187
188 pub fn height(&self) -> usize {
190 self.height
191 }
192
193 pub fn step_count(&self) -> u64 {
195 self.step_count
196 }
197
198 pub fn set_boundary(&mut self, x: usize, y: usize, btype: LbmBoundary) {
200 if x < self.width && y < self.height {
201 self.boundary[y * self.width + x] = btype;
202 }
203 }
204
205 pub fn get_boundary(&self, x: usize, y: usize) -> Option<LbmBoundary> {
207 if x < self.width && y < self.height {
208 Some(self.boundary[y * self.width + x])
209 } else {
210 None
211 }
212 }
213
214 pub fn add_top_wall(&mut self) {
216 let y = self.height - 1;
217 for x in 0..self.width {
218 self.set_boundary(x, y, LbmBoundary::NoSlipWall);
219 }
220 }
221
222 pub fn add_bottom_wall(&mut self) {
224 for x in 0..self.width {
225 self.set_boundary(x, 0, LbmBoundary::NoSlipWall);
226 }
227 }
228
229 pub fn add_enclosing_walls(&mut self) {
231 for x in 0..self.width {
232 self.set_boundary(x, 0, LbmBoundary::NoSlipWall);
233 self.set_boundary(x, self.height - 1, LbmBoundary::NoSlipWall);
234 }
235 for y in 0..self.height {
236 self.set_boundary(0, y, LbmBoundary::NoSlipWall);
237 self.set_boundary(self.width - 1, y, LbmBoundary::NoSlipWall);
238 }
239 }
240
241 pub fn set_lid_velocity(&mut self, vel: Option<[f64; 2]>) {
245 self.lid_velocity = vel;
246 }
247
248 pub fn velocity_at(&self, x: usize, y: usize) -> [f64; 2] {
252 if x >= self.width || y >= self.height {
253 return [0.0; 2];
254 }
255 let cell = y * self.width + x;
256 if self.boundary[cell] == LbmBoundary::NoSlipWall {
257 return [0.0; 2];
258 }
259 let idx = cell * 9;
260 let rho: f64 = self.f[idx..idx + 9].iter().sum();
261 if rho < 1e-15 {
262 return [0.0; 2];
263 }
264 let mut ux = 0.0f64;
265 let mut uy = 0.0f64;
266 for q in 0..9 {
267 ux += EX[q] as f64 * self.f[idx + q];
268 uy += EY[q] as f64 * self.f[idx + q];
269 }
270 [ux / rho, uy / rho]
271 }
272
273 pub fn density_at(&self, x: usize, y: usize) -> f64 {
275 if x >= self.width || y >= self.height {
276 return 0.0;
277 }
278 let idx = (y * self.width + x) * 9;
279 self.f[idx..idx + 9].iter().sum()
280 }
281
282 pub fn get_velocity_field(&self) -> Vec<f64> {
285 let n = self.width * self.height;
286 let mut out = vec![0.0f64; n * 2];
287 for y in 0..self.height {
288 for x in 0..self.width {
289 let v = self.velocity_at(x, y);
290 let cell = y * self.width + x;
291 out[cell * 2] = v[0];
292 out[cell * 2 + 1] = v[1];
293 }
294 }
295 out
296 }
297
298 pub fn get_density_field(&self) -> Vec<f64> {
301 let n = self.width * self.height;
302 let mut out = vec![0.0f64; n];
303 for y in 0..self.height {
304 for x in 0..self.width {
305 out[y * self.width + x] = self.density_at(x, y);
306 }
307 }
308 out
309 }
310
311 pub fn step(&mut self) {
319 let w = self.width;
320 let h = self.height;
321 let omega = self.omega;
322 let bfx = self.body_force[0];
323 let bfy = self.body_force[1];
324
325 for y in 0..h {
327 for x in 0..w {
328 let cell = y * w + x;
329 let idx = cell * 9;
330 if self.boundary[cell] == LbmBoundary::NoSlipWall {
331 for q in 0..9 {
333 self.f_tmp[idx + q] = self.f[idx + q];
334 }
335 continue;
336 }
337 let rho: f64 = self.f[idx..idx + 9].iter().sum();
338 let mut ux = 0.0f64;
339 let mut uy = 0.0f64;
340 for q in 0..9 {
341 ux += EX[q] as f64 * self.f[idx + q];
342 uy += EY[q] as f64 * self.f[idx + q];
343 }
344 let rho_inv = if rho > 1e-15 { 1.0 / rho } else { 0.0 };
345 ux = ux * rho_inv + bfx / (2.0 * omega * rho.max(1e-15));
346 uy = uy * rho_inv + bfy / (2.0 * omega * rho.max(1e-15));
347 let feq = equilibrium(rho, ux, uy);
348 for q in 0..9 {
349 self.f_tmp[idx + q] = self.f[idx + q] * (1.0 - omega) + feq[q] * omega;
350 }
351 }
352 }
353
354 if let Some(lid_vel) = self.lid_velocity {
356 let y = h - 1;
357 for x in 0..w {
358 let cell = y * w + x;
359 let idx = cell * 9;
360 let rho = self.f[idx..idx + 9].iter().sum::<f64>().max(0.01);
361 let feq = equilibrium(rho, lid_vel[0], lid_vel[1]);
362 self.f_tmp[idx..(9 + idx)].copy_from_slice(&feq);
363 }
364 }
365
366 let f_src = self.f_tmp.clone();
368 for y in 0..h {
369 for x in 0..w {
370 let dst_cell = y * w + x;
371 let dst_idx = dst_cell * 9;
372 #[allow(clippy::manual_memcpy)]
373 for q in 0..9 {
374 let src_x = ((x as isize - EX[q] as isize).rem_euclid(w as isize)) as usize;
375 let src_y = ((y as isize - EY[q] as isize).rem_euclid(h as isize)) as usize;
376 let src_idx = (src_y * w + src_x) * 9;
377 self.f[dst_idx + q] = f_src[src_idx + q];
378 }
379 }
380 }
381
382 for y in 0..h {
384 for x in 0..w {
385 let cell = y * w + x;
386 if self.boundary[cell] != LbmBoundary::NoSlipWall {
387 continue;
388 }
389 let idx = cell * 9;
390 let mut tmp = [0.0f64; 9];
391 for q in 0..9 {
392 tmp[OPP[q]] = self.f[idx + q];
393 }
394 self.f[idx..(9 + idx)].copy_from_slice(&tmp);
395 }
396 }
397
398 self.step_count += 1;
399 }
400
401 pub fn run(&mut self, steps: u64) {
403 for _ in 0..steps {
404 self.step();
405 }
406 }
407}
408
409#[inline]
414fn equilibrium(rho: f64, ux: f64, uy: f64) -> [f64; 9] {
415 let u2 = ux * ux + uy * uy;
416 let mut feq = [0.0f64; 9];
417 for q in 0..9 {
418 let eu = EX[q] as f64 * ux + EY[q] as f64 * uy;
419 feq[q] = W[q] * rho * (1.0 + 3.0 * eu + 4.5 * eu * eu - 1.5 * u2);
420 }
421 feq
422}
423
424#[cfg(test)]
429mod tests {
430
431 use crate::LbmBoundary;
432 use crate::PyLbmConfig;
433 use crate::PyLbmSimulation;
434
435 fn small_sim() -> PyLbmSimulation {
436 PyLbmSimulation::new(&PyLbmConfig::new(8, 8, 0.1))
437 }
438
439 #[test]
440 fn test_lbm_creation() {
441 let sim = small_sim();
442 assert_eq!(sim.width(), 8);
443 assert_eq!(sim.height(), 8);
444 assert_eq!(sim.step_count(), 0);
445 }
446
447 #[test]
448 fn test_lbm_initial_density_near_one() {
449 let sim = small_sim();
450 for y in 0..8 {
451 for x in 0..8 {
452 let rho = sim.density_at(x, y);
453 assert!((rho - 1.0).abs() < 1e-10, "rho at ({},{}) = {}", x, y, rho);
454 }
455 }
456 }
457
458 #[test]
459 fn test_lbm_initial_velocity_near_zero() {
460 let sim = small_sim();
461 let v = sim.velocity_at(0, 0);
462 assert!(v[0].abs() < 1e-10 && v[1].abs() < 1e-10);
463 }
464
465 #[test]
466 fn test_lbm_step_advances_count() {
467 let mut sim = small_sim();
468 sim.step();
469 sim.step();
470 assert_eq!(sim.step_count(), 2);
471 }
472
473 #[test]
474 fn test_lbm_run_n_steps() {
475 let mut sim = small_sim();
476 sim.run(10);
477 assert_eq!(sim.step_count(), 10);
478 }
479
480 #[test]
481 fn test_lbm_velocity_field_length() {
482 let sim = small_sim();
483 assert_eq!(sim.get_velocity_field().len(), 8 * 8 * 2);
484 }
485
486 #[test]
487 fn test_lbm_density_field_length() {
488 let sim = small_sim();
489 assert_eq!(sim.get_density_field().len(), 64);
490 }
491
492 #[test]
493 fn test_lbm_set_boundary_wall() {
494 let mut sim = small_sim();
495 sim.set_boundary(0, 0, LbmBoundary::NoSlipWall);
496 assert_eq!(sim.get_boundary(0, 0), Some(LbmBoundary::NoSlipWall));
497 }
498
499 #[test]
500 fn test_lbm_velocity_zero_on_wall() {
501 let mut sim = small_sim();
502 sim.set_boundary(3, 3, LbmBoundary::NoSlipWall);
503 let v = sim.velocity_at(3, 3);
504 assert!(v[0].abs() < 1e-12 && v[1].abs() < 1e-12);
505 }
506
507 #[test]
508 fn test_lbm_add_top_wall() {
509 let mut sim = small_sim();
510 sim.add_top_wall();
511 for x in 0..8 {
512 assert_eq!(sim.get_boundary(x, 7), Some(LbmBoundary::NoSlipWall));
513 }
514 }
515
516 #[test]
517 fn test_lbm_add_bottom_wall() {
518 let mut sim = small_sim();
519 sim.add_bottom_wall();
520 for x in 0..8 {
521 assert_eq!(sim.get_boundary(x, 0), Some(LbmBoundary::NoSlipWall));
522 }
523 }
524
525 #[test]
526 fn test_lbm_add_enclosing_walls() {
527 let mut sim = small_sim();
528 sim.add_enclosing_walls();
529 assert_eq!(sim.get_boundary(0, 0), Some(LbmBoundary::NoSlipWall));
531 assert_eq!(sim.get_boundary(7, 7), Some(LbmBoundary::NoSlipWall));
532 assert_eq!(sim.get_boundary(0, 7), Some(LbmBoundary::NoSlipWall));
533 assert_eq!(sim.get_boundary(7, 0), Some(LbmBoundary::NoSlipWall));
534 }
535
536 #[test]
537 fn test_lbm_out_of_bounds_boundary() {
538 let sim = small_sim();
539 assert!(sim.get_boundary(99, 99).is_none());
540 }
541
542 #[test]
543 fn test_lbm_body_force_creates_flow() {
544 let mut cfg = PyLbmConfig::new(16, 8, 0.1);
545 cfg.body_force = [1e-3, 0.0];
546 let mut sim = PyLbmSimulation::new(&cfg);
547 sim.run(200);
549 let v = sim.velocity_at(8, 4);
550 assert!(
551 v[0] > 0.0,
552 "body force in +x should drive positive ux, got {}",
553 v[0]
554 );
555 }
556
557 #[test]
558 fn test_lbm_lid_velocity_drives_flow() {
559 let mut sim = PyLbmSimulation::new(&PyLbmConfig::new(16, 16, 0.02));
560 sim.add_enclosing_walls();
561 sim.set_lid_velocity(Some([0.1, 0.0]));
562 sim.run(100);
563 let v = sim.velocity_at(8, 14);
565 assert!(v[0].abs() > 1e-6, "lid should drive flow, got ux={}", v[0]);
566 }
567
568 #[test]
569 fn test_lbm_omega_from_config() {
570 let cfg = PyLbmConfig::new(8, 8, 1.0 / 6.0);
571 assert!((cfg.omega() - 1.0).abs() < 1e-10);
573 }
574
575 #[test]
576 fn test_lbm_channel_flow_config() {
577 let cfg = PyLbmConfig::channel_flow();
578 assert_eq!(cfg.width, 128);
579 assert!(cfg.body_force[0] > 0.0);
580 }
581
582 #[test]
583 fn test_lbm_density_conserved_no_body_force() {
584 let mut sim = small_sim();
586 let rho_before: f64 = (0..8usize)
587 .flat_map(|y| (0..8usize).map(move |x| (x, y)))
588 .map(|(x, y)| sim.density_at(x, y))
589 .sum();
590 sim.run(10);
591 let rho_after: f64 = (0..8usize)
592 .flat_map(|y| (0..8usize).map(move |x| (x, y)))
593 .map(|(x, y)| sim.density_at(x, y))
594 .sum();
595 assert!(
596 (rho_after - rho_before).abs() / rho_before < 1e-6,
597 "density not conserved: before={} after={}",
598 rho_before,
599 rho_after
600 );
601 }
602}
603
604const D3Q19_EX: [i32; 19] = [0, 1, -1, 0, 0, 0, 0, 1, -1, 1, -1, 1, -1, 1, -1, 0, 0, 0, 0];
612const D3Q19_EY: [i32; 19] = [0, 0, 0, 1, -1, 0, 0, 1, 1, -1, -1, 0, 0, 0, 0, 1, -1, 1, -1];
613const D3Q19_EZ: [i32; 19] = [0, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, 1, 1, -1, -1, 1, 1, -1, -1];
614
615const D3Q19_W: [f64; 19] = [
617 1.0 / 3.0,
618 1.0 / 18.0,
619 1.0 / 18.0,
620 1.0 / 18.0,
621 1.0 / 18.0,
622 1.0 / 18.0,
623 1.0 / 18.0,
624 1.0 / 36.0,
625 1.0 / 36.0,
626 1.0 / 36.0,
627 1.0 / 36.0,
628 1.0 / 36.0,
629 1.0 / 36.0,
630 1.0 / 36.0,
631 1.0 / 36.0,
632 1.0 / 36.0,
633 1.0 / 36.0,
634 1.0 / 36.0,
635 1.0 / 36.0,
636];
637
638const D3Q19_OPP: [usize; 19] = [
640 0, 2, 1, 4, 3, 6, 5, 8, 7, 10, 9, 12, 11, 14, 13, 16, 15, 18, 17,
641];
642
643#[derive(Debug, Clone, Serialize, Deserialize)]
645pub struct PyLbm3dConfig {
646 pub nx: usize,
648 pub ny: usize,
650 pub nz: usize,
652 pub viscosity: f64,
654 pub body_force: [f64; 3],
656 pub init_density: f64,
658 pub init_velocity: [f64; 3],
660}
661
662impl PyLbm3dConfig {
663 pub fn new(nx: usize, ny: usize, nz: usize, viscosity: f64) -> Self {
665 Self {
666 nx,
667 ny,
668 nz,
669 viscosity: viscosity.max(1e-8),
670 body_force: [0.0; 3],
671 init_density: 1.0,
672 init_velocity: [0.0; 3],
673 }
674 }
675
676 pub fn omega(&self) -> f64 {
678 1.0 / (3.0 * self.viscosity + 0.5)
679 }
680
681 pub fn small() -> Self {
683 Self::new(8, 8, 8, 0.1)
684 }
685}
686
687#[derive(Debug, Clone)]
691pub struct PyLbm3dSimulation {
692 nx: usize,
694 ny: usize,
696 nz: usize,
698 omega: f64,
700 body_force: [f64; 3],
702 f: Vec<f64>,
704 f_tmp: Vec<f64>,
706 boundary: Vec<LbmBoundary>,
708 step_count: u64,
710}
711
712impl PyLbm3dSimulation {
713 pub fn new(config: &PyLbm3dConfig) -> Self {
715 let n = config.nx * config.ny * config.nz;
716 let ux0 = config.init_velocity[0];
717 let uy0 = config.init_velocity[1];
718 let uz0 = config.init_velocity[2];
719 let rho0 = config.init_density;
720 let bfx = config.body_force[0];
721 let bfy = config.body_force[1];
722 let bfz = config.body_force[2];
723
724 let mut f = vec![0.0f64; n * 19];
725 for i in 0..n {
726 let feq = d3q19_equilibrium(rho0, ux0 + bfx, uy0 + bfy, uz0 + bfz);
727 for q in 0..19 {
728 f[i * 19 + q] = feq[q];
729 }
730 }
731 let f_tmp = f.clone();
732 let boundary = vec![LbmBoundary::Fluid; n];
733
734 Self {
735 nx: config.nx,
736 ny: config.ny,
737 nz: config.nz,
738 omega: config.omega(),
739 body_force: config.body_force,
740 f,
741 f_tmp,
742 boundary,
743 step_count: 0,
744 }
745 }
746
747 pub fn dimensions(&self) -> (usize, usize, usize) {
749 (self.nx, self.ny, self.nz)
750 }
751
752 pub fn step_count(&self) -> u64 {
754 self.step_count
755 }
756
757 pub fn set_boundary(&mut self, x: usize, y: usize, z: usize, btype: LbmBoundary) {
759 if x < self.nx && y < self.ny && z < self.nz {
760 self.boundary[z * self.ny * self.nx + y * self.nx + x] = btype;
761 }
762 }
763
764 pub fn get_boundary(&self, x: usize, y: usize, z: usize) -> Option<LbmBoundary> {
766 if x < self.nx && y < self.ny && z < self.nz {
767 Some(self.boundary[z * self.ny * self.nx + y * self.nx + x])
768 } else {
769 None
770 }
771 }
772
773 pub fn density_at(&self, x: usize, y: usize, z: usize) -> f64 {
775 if x >= self.nx || y >= self.ny || z >= self.nz {
776 return 0.0;
777 }
778 let idx = (z * self.ny * self.nx + y * self.nx + x) * 19;
779 self.f[idx..idx + 19].iter().sum()
780 }
781
782 pub fn velocity_at(&self, x: usize, y: usize, z: usize) -> [f64; 3] {
784 if x >= self.nx || y >= self.ny || z >= self.nz {
785 return [0.0; 3];
786 }
787 let cell = z * self.ny * self.nx + y * self.nx + x;
788 if self.boundary[cell] == LbmBoundary::NoSlipWall {
789 return [0.0; 3];
790 }
791 let idx = cell * 19;
792 let rho: f64 = self.f[idx..idx + 19].iter().sum();
793 if rho < 1e-15 {
794 return [0.0; 3];
795 }
796 let mut ux = 0.0f64;
797 let mut uy = 0.0f64;
798 let mut uz = 0.0f64;
799 for q in 0..19 {
800 ux += D3Q19_EX[q] as f64 * self.f[idx + q];
801 uy += D3Q19_EY[q] as f64 * self.f[idx + q];
802 uz += D3Q19_EZ[q] as f64 * self.f[idx + q];
803 }
804 [ux / rho, uy / rho, uz / rho]
805 }
806
807 pub fn step(&mut self) {
809 let nx = self.nx;
810 let ny = self.ny;
811 let nz = self.nz;
812 let omega = self.omega;
813 let bfx = self.body_force[0];
814 let bfy = self.body_force[1];
815 let bfz = self.body_force[2];
816
817 for z in 0..nz {
819 for y in 0..ny {
820 for x in 0..nx {
821 let cell = z * ny * nx + y * nx + x;
822 let idx = cell * 19;
823 if self.boundary[cell] == LbmBoundary::NoSlipWall {
824 for q in 0..19 {
825 self.f_tmp[idx + q] = self.f[idx + q];
826 }
827 continue;
828 }
829 let rho: f64 = self.f[idx..idx + 19].iter().sum();
830 let mut ux = 0.0f64;
831 let mut uy = 0.0f64;
832 let mut uz = 0.0f64;
833 for q in 0..19 {
834 ux += D3Q19_EX[q] as f64 * self.f[idx + q];
835 uy += D3Q19_EY[q] as f64 * self.f[idx + q];
836 uz += D3Q19_EZ[q] as f64 * self.f[idx + q];
837 }
838 let rho_inv = if rho > 1e-15 { 1.0 / rho } else { 0.0 };
839 ux = ux * rho_inv + bfx / (2.0 * omega * rho.max(1e-15));
840 uy = uy * rho_inv + bfy / (2.0 * omega * rho.max(1e-15));
841 uz = uz * rho_inv + bfz / (2.0 * omega * rho.max(1e-15));
842 let feq = d3q19_equilibrium(rho, ux, uy, uz);
843 for q in 0..19 {
844 self.f_tmp[idx + q] = self.f[idx + q] * (1.0 - omega) + feq[q] * omega;
845 }
846 }
847 }
848 }
849
850 let f_src = self.f_tmp.clone();
852 for z in 0..nz {
853 for y in 0..ny {
854 for x in 0..nx {
855 let dst_cell = z * ny * nx + y * nx + x;
856 let dst_idx = dst_cell * 19;
857 #[allow(clippy::manual_memcpy)]
858 for q in 0..19 {
859 let sx =
860 ((x as isize - D3Q19_EX[q] as isize).rem_euclid(nx as isize)) as usize;
861 let sy =
862 ((y as isize - D3Q19_EY[q] as isize).rem_euclid(ny as isize)) as usize;
863 let sz =
864 ((z as isize - D3Q19_EZ[q] as isize).rem_euclid(nz as isize)) as usize;
865 let src_idx = (sz * ny * nx + sy * nx + sx) * 19;
866 self.f[dst_idx + q] = f_src[src_idx + q];
867 }
868 }
869 }
870 }
871
872 for z in 0..nz {
874 for y in 0..ny {
875 for x in 0..nx {
876 let cell = z * ny * nx + y * nx + x;
877 if self.boundary[cell] != LbmBoundary::NoSlipWall {
878 continue;
879 }
880 let idx = cell * 19;
881 let mut tmp = [0.0f64; 19];
882 for q in 0..19 {
883 tmp[D3Q19_OPP[q]] = self.f[idx + q];
884 }
885 self.f[idx..(19 + idx)].copy_from_slice(&tmp);
886 }
887 }
888 }
889
890 self.step_count += 1;
891 }
892
893 pub fn run(&mut self, steps: u64) {
895 for _ in 0..steps {
896 self.step();
897 }
898 }
899
900 pub fn density_field(&self) -> Vec<f64> {
902 let n = self.nx * self.ny * self.nz;
903 let mut out = vec![0.0f64; n];
904 for z in 0..self.nz {
905 for y in 0..self.ny {
906 for x in 0..self.nx {
907 let cell = z * self.ny * self.nx + y * self.nx + x;
908 out[cell] = self.density_at(x, y, z);
909 }
910 }
911 }
912 out
913 }
914}
915
916fn d3q19_equilibrium(rho: f64, ux: f64, uy: f64, uz: f64) -> [f64; 19] {
918 let u2 = ux * ux + uy * uy + uz * uz;
919 let mut feq = [0.0f64; 19];
920 for q in 0..19 {
921 let eu = D3Q19_EX[q] as f64 * ux + D3Q19_EY[q] as f64 * uy + D3Q19_EZ[q] as f64 * uz;
922 feq[q] = D3Q19_W[q] * rho * (1.0 + 3.0 * eu + 4.5 * eu * eu - 1.5 * u2);
923 }
924 feq
925}
926
927#[allow(dead_code)]
935pub fn apply_moving_wall_3d(sim: &mut PyLbm3dSimulation, z_layer: usize, vel: [f64; 3]) {
936 let nz = sim.nz;
937 if z_layer >= nz {
938 return;
939 }
940 let nx = sim.nx;
941 let ny = sim.ny;
942 for y in 0..ny {
943 for x in 0..nx {
944 let cell = z_layer * ny * nx + y * nx + x;
945 let idx = cell * 19;
946 let rho: f64 = sim.f[idx..idx + 19].iter().sum::<f64>().max(0.01);
947 let feq = d3q19_equilibrium(rho, vel[0], vel[1], vel[2]);
948 sim.f[idx..(19 + idx)].copy_from_slice(&feq);
949 }
950 }
951}
952
953#[cfg(test)]
958mod d3q19_tests {
959
960 use crate::LbmBoundary;
961
962 use crate::lbm_api::PyLbm3dConfig;
963 use crate::lbm_api::PyLbm3dSimulation;
964 use crate::lbm_api::apply_moving_wall_3d;
965 use crate::lbm_api::d3q19_equilibrium;
966
967 fn small_3d() -> PyLbm3dSimulation {
968 PyLbm3dSimulation::new(&PyLbm3dConfig::small())
969 }
970
971 #[test]
972 fn test_d3q19_creation() {
973 let sim = small_3d();
974 let (nx, ny, nz) = sim.dimensions();
975 assert_eq!(nx, 8);
976 assert_eq!(ny, 8);
977 assert_eq!(nz, 8);
978 assert_eq!(sim.step_count(), 0);
979 }
980
981 #[test]
982 fn test_d3q19_initial_density() {
983 let sim = small_3d();
984 let rho = sim.density_at(4, 4, 4);
985 assert!((rho - 1.0).abs() < 1e-10, "rho = {}", rho);
986 }
987
988 #[test]
989 fn test_d3q19_initial_velocity_near_zero() {
990 let sim = small_3d();
991 let v = sim.velocity_at(0, 0, 0);
992 assert!(v[0].abs() < 1e-10 && v[1].abs() < 1e-10 && v[2].abs() < 1e-10);
993 }
994
995 #[test]
996 fn test_d3q19_step_advances_count() {
997 let mut sim = small_3d();
998 sim.step();
999 sim.step();
1000 assert_eq!(sim.step_count(), 2);
1001 }
1002
1003 #[test]
1004 fn test_d3q19_run_n_steps() {
1005 let mut sim = small_3d();
1006 sim.run(10);
1007 assert_eq!(sim.step_count(), 10);
1008 }
1009
1010 #[test]
1011 fn test_d3q19_density_field_length() {
1012 let sim = small_3d();
1013 assert_eq!(sim.density_field().len(), 8 * 8 * 8);
1014 }
1015
1016 #[test]
1017 fn test_d3q19_set_get_boundary() {
1018 let mut sim = small_3d();
1019 sim.set_boundary(1, 1, 1, LbmBoundary::NoSlipWall);
1020 assert_eq!(sim.get_boundary(1, 1, 1), Some(LbmBoundary::NoSlipWall));
1021 }
1022
1023 #[test]
1024 fn test_d3q19_out_of_bounds() {
1025 let sim = small_3d();
1026 assert!(sim.get_boundary(99, 0, 0).is_none());
1027 }
1028
1029 #[test]
1030 fn test_d3q19_velocity_zero_on_wall() {
1031 let mut sim = small_3d();
1032 sim.set_boundary(2, 2, 2, LbmBoundary::NoSlipWall);
1033 let v = sim.velocity_at(2, 2, 2);
1034 assert!(v[0].abs() < 1e-12 && v[1].abs() < 1e-12 && v[2].abs() < 1e-12);
1035 }
1036
1037 #[test]
1038 fn test_d3q19_body_force_creates_flow() {
1039 let mut cfg = PyLbm3dConfig::new(8, 8, 8, 0.1);
1040 cfg.body_force = [1e-3, 0.0, 0.0];
1041 let mut sim = PyLbm3dSimulation::new(&cfg);
1042 sim.run(50);
1043 let v = sim.velocity_at(4, 4, 4);
1044 assert!(v[0] > 0.0, "body force should drive flow, ux={}", v[0]);
1045 }
1046
1047 #[test]
1048 fn test_d3q19_density_conserved() {
1049 let mut sim = small_3d();
1050 let rho_before: f64 = sim.density_field().iter().sum();
1051 sim.run(5);
1052 let rho_after: f64 = sim.density_field().iter().sum();
1053 assert!(
1054 (rho_after - rho_before).abs() / rho_before < 1e-6,
1055 "density not conserved: {} vs {}",
1056 rho_before,
1057 rho_after
1058 );
1059 }
1060
1061 #[test]
1062 fn test_d3q19_config_omega() {
1063 let cfg = PyLbm3dConfig::new(4, 4, 4, 1.0 / 6.0);
1064 assert!((cfg.omega() - 1.0).abs() < 1e-10);
1065 }
1066
1067 #[test]
1068 fn test_lbm3d_config_small() {
1069 let cfg = PyLbm3dConfig::small();
1070 assert_eq!(cfg.nx, 8);
1071 assert_eq!(cfg.ny, 8);
1072 assert_eq!(cfg.nz, 8);
1073 }
1074
1075 #[test]
1076 fn test_d3q19_equilibrium_sum_to_rho() {
1077 let feq = d3q19_equilibrium(1.5, 0.1, 0.0, -0.05);
1078 let total: f64 = feq.iter().sum();
1079 assert!((total - 1.5).abs() < 1e-10, "equilibrium sum = {}", total);
1080 }
1081
1082 #[test]
1083 fn test_moving_wall_3d_changes_velocity() {
1084 let mut sim = small_3d();
1085 let v_before = sim.velocity_at(4, 4, 7);
1086 apply_moving_wall_3d(&mut sim, 7, [0.1, 0.0, 0.0]);
1087 let v_after = sim.velocity_at(4, 4, 7);
1088 let _ = (v_before, v_after); }
1092}
1093
1094#[derive(Debug, Clone, Serialize, Deserialize)]
1100#[allow(dead_code)]
1101pub struct MrtRelaxation {
1102 pub s0: f64,
1104 pub s1: f64,
1106 pub s2: f64,
1108 pub s3: f64,
1110 pub s4: f64,
1112 pub s5: f64,
1114 pub s6: f64,
1116 pub s7: f64,
1118 pub s8: f64,
1120}
1121
1122impl MrtRelaxation {
1123 pub fn from_viscosity(viscosity: f64) -> Self {
1125 let s_nu = 1.0 / (3.0 * viscosity + 0.5);
1126 Self {
1127 s0: 1.0,
1128 s1: 1.4,
1129 s2: 1.4,
1130 s3: 1.0,
1131 s4: 1.2,
1132 s5: 1.0,
1133 s6: 1.2,
1134 s7: s_nu,
1135 s8: s_nu,
1136 }
1137 }
1138
1139 pub fn as_array(&self) -> [f64; 9] {
1141 [
1142 self.s0, self.s1, self.s2, self.s3, self.s4, self.s5, self.s6, self.s7, self.s8,
1143 ]
1144 }
1145}
1146
1147impl Default for MrtRelaxation {
1148 fn default() -> Self {
1149 Self::from_viscosity(0.1)
1150 }
1151}
1152
1153#[allow(dead_code)]
1162pub fn zou_he_velocity_inlet(sim: &mut PyLbmSimulation, ux_in: f64) {
1163 let w = sim.width;
1164 let h = sim.height;
1165 for y in 1..(h - 1) {
1166 let cell = y * w; let idx = cell * 9;
1168 let rho = (sim.f[idx]
1169 + sim.f[idx + 2]
1170 + sim.f[idx + 4]
1171 + 2.0 * (sim.f[idx + 3] + sim.f[idx + 6] + sim.f[idx + 7]))
1172 / (1.0 - ux_in);
1173 let feq = equilibrium(rho, ux_in, 0.0);
1174 sim.f[idx..(9 + idx)].copy_from_slice(&feq);
1175 }
1176}
1177
1178#[allow(dead_code)]
1180pub fn zou_he_pressure_outlet(sim: &mut PyLbmSimulation) {
1181 let w = sim.width;
1182 let h = sim.height;
1183 let x = w - 1;
1184 for y in 1..(h - 1) {
1185 let cell = y * w + x;
1186 let cell_prev = y * w + x - 1;
1187 let idx = cell * 9;
1188 let idx_prev = cell_prev * 9;
1189 for q in 0..9 {
1191 sim.f[idx + q] = sim.f[idx_prev + q];
1192 }
1193 }
1194}
1195
1196impl PyLbmSimulation {
1201 pub fn mean_density(&self) -> f64 {
1203 let mut sum = 0.0;
1204 let mut count = 0usize;
1205 for y in 0..self.height {
1206 for x in 0..self.width {
1207 let cell = y * self.width + x;
1208 if self.boundary[cell] != LbmBoundary::NoSlipWall {
1209 sum += self.density_at(x, y);
1210 count += 1;
1211 }
1212 }
1213 }
1214 if count == 0 { 0.0 } else { sum / count as f64 }
1215 }
1216
1217 pub fn max_speed(&self) -> f64 {
1219 let mut max_v = 0.0f64;
1220 for y in 0..self.height {
1221 for x in 0..self.width {
1222 let cell = y * self.width + x;
1223 if self.boundary[cell] == LbmBoundary::NoSlipWall {
1224 continue;
1225 }
1226 let v = self.velocity_at(x, y);
1227 let speed = (v[0] * v[0] + v[1] * v[1]).sqrt();
1228 if speed > max_v {
1229 max_v = speed;
1230 }
1231 }
1232 }
1233 max_v
1234 }
1235
1236 pub fn vorticity_field(&self) -> Vec<f64> {
1240 let w = self.width;
1241 let h = self.height;
1242 let mut out = vec![0.0f64; w * h];
1243 for y in 1..(h - 1) {
1244 for x in 1..(w - 1) {
1245 let vxp = self.velocity_at(x, y + 1)[0];
1246 let vxm = self.velocity_at(x, y - 1)[0];
1247 let vyp = self.velocity_at(x + 1, y)[1];
1248 let vym = self.velocity_at(x - 1, y)[1];
1249 out[y * w + x] = (vyp - vym) * 0.5 - (vxp - vxm) * 0.5;
1250 }
1251 }
1252 out
1253 }
1254
1255 pub fn enstrophy(&self) -> f64 {
1257 self.vorticity_field().iter().map(|&w| 0.5 * w * w).sum()
1258 }
1259
1260 pub fn reynolds_number(&self, omega: f64) -> f64 {
1262 let nu = (1.0 / omega - 0.5) / 3.0;
1263 let u_max = self.max_speed();
1264 let l = self.height as f64 * 0.5;
1265 if nu < 1e-15 {
1266 return 0.0;
1267 }
1268 l * u_max / nu
1269 }
1270
1271 pub fn speed_field(&self) -> Vec<f64> {
1273 let w = self.width;
1274 let h = self.height;
1275 let mut out = vec![0.0f64; w * h];
1276 for y in 0..h {
1277 for x in 0..w {
1278 let v = self.velocity_at(x, y);
1279 out[y * w + x] = (v[0] * v[0] + v[1] * v[1]).sqrt();
1280 }
1281 }
1282 out
1283 }
1284
1285 pub fn reset_to_equilibrium(&mut self, rho: f64, ux: f64, uy: f64) {
1287 let feq = equilibrium(rho, ux, uy);
1288 let n = self.width * self.height;
1289 for i in 0..n {
1290 for q in 0..9 {
1291 self.f[i * 9 + q] = feq[q];
1292 self.f_tmp[i * 9 + q] = feq[q];
1293 }
1294 }
1295 }
1296
1297 pub fn add_circular_obstacle(&mut self, cx: usize, cy: usize, r: usize) {
1299 let r2 = (r * r) as isize;
1300 for y in 0..self.height {
1301 for x in 0..self.width {
1302 let dx = x as isize - cx as isize;
1303 let dy = y as isize - cy as isize;
1304 if dx * dx + dy * dy <= r2 {
1305 self.set_boundary(x, y, LbmBoundary::NoSlipWall);
1306 }
1307 }
1308 }
1309 }
1310
1311 pub fn fluid_cell_count(&self) -> usize {
1313 self.boundary
1314 .iter()
1315 .filter(|&&b| b == LbmBoundary::Fluid)
1316 .count()
1317 }
1318
1319 pub fn wall_cell_count(&self) -> usize {
1321 self.boundary
1322 .iter()
1323 .filter(|&&b| b == LbmBoundary::NoSlipWall)
1324 .count()
1325 }
1326}
1327
1328#[derive(Debug, Clone)]
1334#[allow(dead_code)]
1335pub struct LbmStats {
1336 pub step_count: u64,
1338 pub mean_density: f64,
1340 pub max_speed: f64,
1342 pub enstrophy: f64,
1344 pub fluid_cell_count: usize,
1346 pub wall_cell_count: usize,
1348}
1349
1350impl PyLbmSimulation {
1351 pub fn collect_stats(&self) -> LbmStats {
1353 LbmStats {
1354 step_count: self.step_count,
1355 mean_density: self.mean_density(),
1356 max_speed: self.max_speed(),
1357 enstrophy: self.enstrophy(),
1358 fluid_cell_count: self.fluid_cell_count(),
1359 wall_cell_count: self.wall_cell_count(),
1360 }
1361 }
1362}
1363
1364#[cfg(test)]
1369mod lbm_ext_tests {
1370
1371 use crate::LbmBoundary;
1372 use crate::PyLbmConfig;
1373 use crate::PyLbmSimulation;
1374 use crate::lbm_api::MrtRelaxation;
1375 use crate::lbm_api::PyLbm3dConfig;
1376 use crate::lbm_api::PyLbm3dSimulation;
1377
1378 use crate::lbm_api::zou_he_pressure_outlet;
1379 use crate::lbm_api::zou_he_velocity_inlet;
1380
1381 fn small_sim() -> PyLbmSimulation {
1382 PyLbmSimulation::new(&PyLbmConfig::new(16, 8, 0.1))
1383 }
1384
1385 #[test]
1388 fn test_mrt_from_viscosity() {
1389 let mrt = MrtRelaxation::from_viscosity(1.0 / 6.0);
1390 let s7 = mrt.s7;
1391 assert!((s7 - 1.0).abs() < 1e-10, "s7={}", s7);
1392 }
1393
1394 #[test]
1395 fn test_mrt_as_array_length() {
1396 let mrt = MrtRelaxation::default();
1397 assert_eq!(mrt.as_array().len(), 9);
1398 }
1399
1400 #[test]
1401 fn test_mrt_rates_positive() {
1402 let mrt = MrtRelaxation::from_viscosity(0.02);
1403 for s in mrt.as_array() {
1404 assert!(s > 0.0, "rate must be positive: {}", s);
1405 }
1406 }
1407
1408 #[test]
1411 fn test_mean_density_initial() {
1412 let sim = small_sim();
1413 let rho = sim.mean_density();
1414 assert!((rho - 1.0).abs() < 1e-8, "rho = {}", rho);
1415 }
1416
1417 #[test]
1418 fn test_max_speed_initial_zero() {
1419 let sim = small_sim();
1420 assert!(sim.max_speed() < 1e-10);
1421 }
1422
1423 #[test]
1424 fn test_vorticity_field_length() {
1425 let sim = small_sim();
1426 let vort = sim.vorticity_field();
1427 assert_eq!(vort.len(), 16 * 8);
1428 }
1429
1430 #[test]
1431 fn test_enstrophy_initial_near_zero() {
1432 let sim = small_sim();
1433 assert!(sim.enstrophy() < 1e-10);
1434 }
1435
1436 #[test]
1437 fn test_speed_field_length() {
1438 let sim = small_sim();
1439 assert_eq!(sim.speed_field().len(), 16 * 8);
1440 }
1441
1442 #[test]
1443 fn test_reset_to_equilibrium() {
1444 let mut sim = small_sim();
1445 sim.run(50);
1446 sim.reset_to_equilibrium(1.0, 0.0, 0.0);
1447 let rho = sim.density_at(8, 4);
1448 assert!((rho - 1.0).abs() < 1e-8);
1449 }
1450
1451 #[test]
1452 fn test_fluid_cell_count_all_fluid_initially() {
1453 let sim = small_sim();
1454 assert_eq!(sim.fluid_cell_count(), 16 * 8);
1455 }
1456
1457 #[test]
1458 fn test_wall_cell_count_zero_initially() {
1459 let sim = small_sim();
1460 assert_eq!(sim.wall_cell_count(), 0);
1461 }
1462
1463 #[test]
1464 fn test_fluid_wall_count_after_enclosing_walls() {
1465 let mut sim = small_sim();
1466 sim.add_enclosing_walls();
1467 let walls = sim.wall_cell_count();
1468 let fluid = sim.fluid_cell_count();
1469 assert_eq!(walls + fluid, 16 * 8);
1470 assert!(walls > 0);
1471 }
1472
1473 #[test]
1474 fn test_circular_obstacle_adds_walls() {
1475 let mut sim = small_sim();
1476 sim.add_circular_obstacle(8, 4, 2);
1477 assert!(sim.wall_cell_count() > 0);
1478 }
1479
1480 #[test]
1481 fn test_circular_obstacle_center_is_wall() {
1482 let mut sim = small_sim();
1483 sim.add_circular_obstacle(8, 4, 2);
1484 assert_eq!(sim.get_boundary(8, 4), Some(LbmBoundary::NoSlipWall));
1485 }
1486
1487 #[test]
1488 fn test_reynolds_number_positive_after_flow() {
1489 let mut cfg = PyLbmConfig::new(32, 16, 0.02);
1490 cfg.body_force = [1e-4, 0.0];
1491 let mut sim = PyLbmSimulation::new(&cfg);
1492 sim.run(200);
1493 let re = sim.reynolds_number(cfg.omega());
1494 let _ = re; }
1496
1497 #[test]
1500 fn test_zou_he_velocity_inlet_no_panic() {
1501 let mut sim = small_sim();
1502 zou_he_velocity_inlet(&mut sim, 0.05);
1503 }
1505
1506 #[test]
1507 fn test_zou_he_pressure_outlet_no_panic() {
1508 let mut sim = small_sim();
1509 zou_he_pressure_outlet(&mut sim);
1510 }
1511
1512 #[test]
1515 fn test_collect_stats_initial() {
1516 let sim = small_sim();
1517 let stats = sim.collect_stats();
1518 assert_eq!(stats.step_count, 0);
1519 assert!((stats.mean_density - 1.0).abs() < 1e-8);
1520 }
1521
1522 #[test]
1523 fn test_collect_stats_after_run() {
1524 let mut sim = small_sim();
1525 sim.run(10);
1526 let stats = sim.collect_stats();
1527 assert_eq!(stats.step_count, 10);
1528 }
1529
1530 #[test]
1531 fn test_lbm_config_omega_range() {
1532 let cfg = PyLbmConfig::new(8, 8, 0.1);
1533 let omega = cfg.omega();
1534 assert!((omega - 1.25).abs() < 1e-10);
1536 }
1537
1538 #[test]
1541 fn test_d3q19_velocity_field_size() {
1542 let sim = PyLbm3dSimulation::new(&PyLbm3dConfig::small());
1543 let (nx, ny, nz) = sim.dimensions();
1544 let density = sim.density_field();
1545 assert_eq!(density.len(), nx * ny * nz);
1546 }
1547
1548 #[test]
1549 fn test_d3q19_enclosing_walls_boundary_check() {
1550 let mut sim = PyLbm3dSimulation::new(&PyLbm3dConfig::new(4, 4, 4, 0.1));
1551 for y in 0..4 {
1553 for x in 0..4 {
1554 sim.set_boundary(x, y, 0, LbmBoundary::NoSlipWall);
1555 }
1556 }
1557 assert_eq!(sim.get_boundary(2, 2, 0), Some(LbmBoundary::NoSlipWall));
1558 assert_eq!(sim.get_boundary(2, 2, 2), Some(LbmBoundary::Fluid));
1559 }
1560
1561 #[test]
1562 fn test_d3q19_body_force_x_creates_positive_flow() {
1563 let mut cfg = PyLbm3dConfig::new(8, 8, 8, 0.05);
1564 cfg.body_force = [5e-4, 0.0, 0.0];
1565 let mut sim = PyLbm3dSimulation::new(&cfg);
1566 sim.run(100);
1567 let v = sim.velocity_at(4, 4, 4);
1568 assert!(
1569 v[0] > 0.0,
1570 "body force +x should drive ux > 0, got {}",
1571 v[0]
1572 );
1573 }
1574
1575 #[test]
1576 fn test_d3q19_config_new_clamps_viscosity() {
1577 let cfg = PyLbm3dConfig::new(4, 4, 4, 0.0);
1578 assert!(cfg.viscosity >= 1e-8);
1579 }
1580
1581 #[test]
1582 fn test_lbm2d_config_clamps_viscosity() {
1583 let cfg = PyLbmConfig::new(4, 4, 0.0);
1584 assert!(cfg.viscosity >= 1e-8);
1585 }
1586
1587 #[test]
1588 fn test_d3q19_out_of_bounds_velocity() {
1589 let sim = PyLbm3dSimulation::new(&PyLbm3dConfig::small());
1590 let v = sim.velocity_at(100, 0, 0);
1591 for &vi in &v {
1592 assert!((vi).abs() < 1e-15);
1593 }
1594 }
1595
1596 #[test]
1597 fn test_vorticity_boundary_cells_zero() {
1598 let sim = small_sim();
1599 let vort = sim.vorticity_field();
1600 assert!((vort[0]).abs() < 1e-15);
1602 }
1603
1604 #[test]
1605 fn test_speed_field_initial_near_zero() {
1606 let sim = small_sim();
1607 let speed = sim.speed_field();
1608 for &s in &speed {
1609 assert!(s < 1e-10, "initial speed should be near zero, got {}", s);
1610 }
1611 }
1612}