lbm-gpu: the temperature lattice on the GPU

The D3Q7 scalar lattice steps on the device after the flow's step, in
its own A–B pair of buffers: BGK collision with the velocity the
momentum collision leaves in the moments buffer, pull streaming with
the links' rules read through each group's thermal condition
(anti-bounce-back for a held temperature, bounce-back for an adiabatic
face, a flux added to the bounced population), inlets at their held
temperature and outlets copying their neighbour. The populations are
stored as deviations from w_q T₀ so single precision keeps the digits
of the differences. The heat each link exchanged goes into the fourth
component of the momentum-exchange contributions and the group sums;
the buoyant acceleration −β g θ is read per cell in the collision, the
open boundaries and the moments. A temperature that stops being a
number ends the chunk with Error::Unstable like the flow's checks.

Flow exposes what the backend needs to build the device state (the
thermal settings and populations).

Verification against the CPU on lavapipe: plates held at 20 and 21
(temperatures within 1.4e-5 of the range, the wall heats within 6e-5
and both against k ΔT A / H), a heated channel with a parabolic inlet
carrying its temperature, a flux wall, a held wall and an outlet
(temperature 3e-7, heat 1e-7) and a buoyant cavity at Ra 10³
(velocity 1.3e-6, temperature 6e-7, heat 2.4e-6). The tests now fail
on a shader that does not compile instead of skipping; only a missing
device skips.

Part of #157.
This commit is contained in:
Christian Svensson authored and Irakli Kourtis committed 2026-10-17 08:32:30 -04:00
1 parent 190f958b23
commit 99b0b0cfa4
7 files changed
+508 -39

No files matched your search

+37
View File
@@ -143,6 +143,33 @@ pub struct ThermalState {
heat: Vec<f64>,
}
impl ThermalState {
/// The scalar relaxation rate 1 / τ_T.
pub fn omega(&self) -> f64 {
self.omega
}
/// The initial temperature (and the reference of the buoyancy).
pub fn initial(&self) -> f64 {
self.initial
}
/// `−β g` in lattice units; zero without buoyancy.
pub fn buoyancy(&self) -> [f64; 3] {
self.buoyancy
}
/// Each force group's thermal condition (fluxes in lattice units).
pub fn groups(&self) -> &[ThermalCondition] {
&self.groups
}
/// Per open cell, the temperature it holds, or `None` to take its neighbour's.
pub fn open(&self) -> &[Option<f64>] {
&self.open
}
}
/// The flow state.
#[derive(Debug, Clone)]
pub struct Flow {
@@ -474,6 +501,16 @@ impl Flow {
self.thermal.is_some()
}
/// The temperature lattice and its settings, with heat.
pub fn thermal(&self) -> Option<&ThermalState> {
self.thermal.as_ref()
}
/// Scalar population `q` (0..7) of a cell; zero without heat.
pub fn thermal_population(&self, q: usize, cell: usize) -> f64 {
self.thermal.as_ref().and_then(|t| t.g.get(q * self.stride + cell).copied()).unwrap_or(0.0)
}
/// The heat each group received in the last step (lattice units), empty without heat.
pub fn heat(&self) -> &[f64] {
self.thermal.as_ref().map_or(&[], |t| &t.heat)