Skip to main content

oxiphysics_python/world_api/
lbm.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! LBM (Lattice Boltzmann Method) Simulation.
5
6#![allow(missing_docs)]
7
8// ===========================================================================
9// LBM (Lattice Boltzmann Method) Simulation
10// ===========================================================================
11
12/// Configuration for a 2-D D2Q9 Lattice-Boltzmann simulation.
13#[derive(Debug, Clone)]
14#[allow(dead_code)]
15pub struct PyLbmConfig {
16    /// Grid width (number of cells in X direction).
17    pub width: usize,
18    /// Grid height (number of cells in Y direction).
19    pub height: usize,
20    /// Kinematic viscosity (nu). Controls the relaxation rate.
21    pub viscosity: f64,
22    /// External body-force acceleration `[fx, fy]` applied to the fluid.
23    pub body_force: [f64; 2],
24}
25
26impl PyLbmConfig {
27    /// Create a new LBM configuration.
28    pub fn new(width: usize, height: usize, viscosity: f64) -> Self {
29        Self {
30            width,
31            height,
32            viscosity: viscosity.max(1e-6),
33            body_force: [0.0; 2],
34        }
35    }
36
37    /// Create a default lid-driven cavity configuration (64×64).
38    pub fn lid_driven_cavity() -> Self {
39        Self::new(64, 64, 0.01)
40    }
41
42    /// Compute the relaxation parameter omega from viscosity.
43    ///
44    /// `omega = 1 / (3 * nu + 0.5)`
45    pub fn omega(&self) -> f64 {
46        1.0 / (3.0 * self.viscosity + 0.5)
47    }
48}
49
50/// D2Q9 Lattice-Boltzmann grid simulation.
51///
52/// Stores distribution functions for all 9 velocity directions at every cell.
53/// Uses the BGK collision operator with a single relaxation time.
54#[derive(Debug, Clone)]
55#[allow(dead_code)]
56pub struct PyLbmGrid {
57    /// Number of cells in X.
58    width: usize,
59    /// Number of cells in Y.
60    height: usize,
61    /// Relaxation rate omega.
62    omega: f64,
63    /// External body force `[fx, fy]`.
64    body_force: [f64; 2],
65    /// Distribution functions, indexed `[cell_idx * 9 + q]`.
66    f: Vec<f64>,
67    /// Post-collision / swap buffer.
68    f_tmp: Vec<f64>,
69    /// Accumulated simulation steps.
70    step_count: u64,
71}
72
73// D2Q9 weights and discrete velocities
74const D2Q9_W: [f64; 9] = [
75    4.0 / 9.0,
76    1.0 / 9.0,
77    1.0 / 9.0,
78    1.0 / 9.0,
79    1.0 / 9.0,
80    1.0 / 36.0,
81    1.0 / 36.0,
82    1.0 / 36.0,
83    1.0 / 36.0,
84];
85// ex, ey for each of the 9 directions
86const D2Q9_EX: [f64; 9] = [0.0, 1.0, 0.0, -1.0, 0.0, 1.0, -1.0, -1.0, 1.0];
87const D2Q9_EY: [f64; 9] = [0.0, 0.0, 1.0, 0.0, -1.0, 1.0, 1.0, -1.0, -1.0];
88
89impl PyLbmGrid {
90    /// Create a new LBM grid from configuration. All cells initialised to
91    /// equilibrium with unit density and zero velocity.
92    pub fn new(config: &PyLbmConfig) -> Self {
93        let n = config.width * config.height;
94        let mut f = vec![0.0f64; n * 9];
95        // Initialise each cell to equilibrium at rho=1, u=(0,0)
96        for i in 0..n {
97            for q in 0..9 {
98                f[i * 9 + q] = D2Q9_W[q];
99            }
100        }
101        Self {
102            width: config.width,
103            height: config.height,
104            omega: config.omega(),
105            body_force: config.body_force,
106            f: f.clone(),
107            f_tmp: f,
108            step_count: 0,
109        }
110    }
111
112    /// Return the grid width.
113    pub fn width(&self) -> usize {
114        self.width
115    }
116
117    /// Return the grid height.
118    pub fn height(&self) -> usize {
119        self.height
120    }
121
122    /// Return the number of completed steps.
123    pub fn step_count(&self) -> u64 {
124        self.step_count
125    }
126
127    /// Advance the simulation by one LBM time step.
128    ///
129    /// Performs: collision (BGK) + streaming + periodic bounce-back walls.
130    pub fn step(&mut self) {
131        let w = self.width;
132        let h = self.height;
133        let omega = self.omega;
134        let bfx = self.body_force[0];
135        let bfy = self.body_force[1];
136
137        // --- Collision + compute macroscopic fields ---
138        for y in 0..h {
139            for x in 0..w {
140                let idx = (y * w + x) * 9;
141                // Density
142                let rho: f64 = self.f[idx..idx + 9].iter().sum();
143                // Velocity
144                let mut ux = 0.0f64;
145                let mut uy = 0.0f64;
146                for q in 0..9 {
147                    ux += D2Q9_EX[q] * self.f[idx + q];
148                    uy += D2Q9_EY[q] * self.f[idx + q];
149                }
150                let rho_inv = if rho > 1e-15 { 1.0 / rho } else { 0.0 };
151                ux = ux * rho_inv + bfx * 0.5 / omega;
152                uy = uy * rho_inv + bfy * 0.5 / omega;
153                let u2 = ux * ux + uy * uy;
154                // BGK collision → post-collision stored in f_tmp
155                for q in 0..9 {
156                    let eu = D2Q9_EX[q] * ux + D2Q9_EY[q] * uy;
157                    let feq = D2Q9_W[q] * rho * (1.0 + 3.0 * eu + 4.5 * eu * eu - 1.5 * u2);
158                    self.f_tmp[idx + q] = self.f[idx + q] * (1.0 - omega) + feq * omega;
159                }
160            }
161        }
162
163        // --- Streaming with periodic boundaries ---
164        let f_src = self.f_tmp.clone();
165        for y in 0..h {
166            for x in 0..w {
167                let dst_idx = (y * w + x) * 9;
168                #[allow(clippy::manual_memcpy)]
169                for q in 0..9 {
170                    let src_x =
171                        ((x as isize - D2Q9_EX[q] as isize).rem_euclid(w as isize)) as usize;
172                    let src_y =
173                        ((y as isize - D2Q9_EY[q] as isize).rem_euclid(h as isize)) as usize;
174                    let src_idx = (src_y * w + src_x) * 9;
175                    self.f[dst_idx + q] = f_src[src_idx + q];
176                }
177            }
178        }
179
180        self.step_count += 1;
181    }
182
183    /// Get the macroscopic velocity `[ux, uy]` at cell `(x, y)`.
184    ///
185    /// Returns `[0.0, 0.0]` if coordinates are out of bounds.
186    pub fn velocity_at(&self, x: usize, y: usize) -> [f64; 2] {
187        if x >= self.width || y >= self.height {
188            return [0.0; 2];
189        }
190        let idx = (y * self.width + x) * 9;
191        let rho: f64 = self.f[idx..idx + 9].iter().sum();
192        if rho < 1e-15 {
193            return [0.0; 2];
194        }
195        let mut ux = 0.0f64;
196        let mut uy = 0.0f64;
197        for q in 0..9 {
198            ux += D2Q9_EX[q] * self.f[idx + q];
199            uy += D2Q9_EY[q] * self.f[idx + q];
200        }
201        [ux / rho, uy / rho]
202    }
203
204    /// Get the macroscopic density at cell `(x, y)`.
205    pub fn density_at(&self, x: usize, y: usize) -> f64 {
206        if x >= self.width || y >= self.height {
207            return 0.0;
208        }
209        let idx = (y * self.width + x) * 9;
210        self.f[idx..idx + 9].iter().sum()
211    }
212
213    /// Return the entire velocity field as a flat `Vec`f64` of `\[ux, uy\]` pairs,
214    /// row-major order (x-major).
215    pub fn velocity_field(&self) -> Vec<f64> {
216        let n = self.width * self.height;
217        let mut out = vec![0.0f64; n * 2];
218        for y in 0..self.height {
219            for x in 0..self.width {
220                let cell = y * self.width + x;
221                let v = self.velocity_at(x, y);
222                out[cell * 2] = v[0];
223                out[cell * 2 + 1] = v[1];
224            }
225        }
226        out
227    }
228
229    /// Return the entire density field as a flat `Vec`f64`, row-major.
230    pub fn density_field(&self) -> Vec<f64> {
231        let n = self.width * self.height;
232        let mut out = vec![0.0f64; n];
233        for y in 0..self.height {
234            for x in 0..self.width {
235                out[y * self.width + x] = self.density_at(x, y);
236            }
237        }
238        out
239    }
240}