fix(esopull): correct init layout and pre-streaming semantics (v0.5.1)

EsoPull curved boundaries and wall BCs now use consistent backing-layout
reads; InitEsoPull writes equilibrium in t=0 EsoPull layout. Cache N_OBJS
after compile and atomic config header writes to avoid parallel races.
Adds config screening tools, flume configs, and FP16S/EsoPull diagnosis doc.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Frank14fandCursor committed 2026-06-27 22:32:01 +08:00
1 parent 00b957f904
commit 6e3756c587
28 files changed
+1536 -157

No files matched your search

+19
View File
@@ -41,6 +41,25 @@ def find_sensor_area(radius):
return area
def build_triangle_release_points(
layout: dict,
*,
nx: int,
ny: int,
diameter_cells: float = 20.0,
upstream_D: float = 4.0,
margin: float = 5.0,
n_seeds: int = 4,
) -> np.ndarray:
"""Upstream release seeds for triangle layout, clamped inside the domain."""
release_x = min(layout["x_apex"], layout["x_rear"]) - upstream_D * diameter_cells
release_x = float(np.clip(release_x, margin, nx - 1 - margin))
y_low = float(np.clip(layout["y_lower"] - layout["radius_lb"], margin, ny - 1 - margin))
y_high = float(np.clip(layout["y_upper"] + layout["radius_lb"], margin, ny - 1 - margin))
ys = np.linspace(y_low, y_high, max(2, int(n_seeds)), dtype=np.float64)
return np.column_stack([np.full(ys.shape[0], release_x, dtype=np.float64), ys])
def cylinders_from_triangle_layout(layout: dict) -> "list[tuple[tuple[float, float], float]]":
"""Build cylinder list from triangle-layout dict used in experiment notebook."""
radius = float(layout["radius_lb"])
@@ -0,0 +1,52 @@
{
"_doc": "Three rotating cylinders in confined channel (triangle layout, free-slip walls).",
"_variant": "MRT + zou_he_local inlet — Phase 2 candidate from Kan99b K2 screening",
"_screening": "Kan99b K2 D=20: St err=1.51%, CD err=1.76%, C'D err=1.42%",
"grid": {
"lattice_model": "D2Q9",
"nx": 3000,
"ny": 300,
"nz": 1
},
"physics": {
"data_type": "FP32",
"viscosity": 0.004,
"velocity": 0.04,
"rho": 1.0
},
"method": {
"collision": "MRT",
"streaming": "double_buffer",
"store_precision": "FP32",
"ddf_shifting": false,
"les": {
"enabled": false,
"cs": 0.16,
"closed_form": true
},
"trt": {
"magic_param": 0.1875
},
"inlet": {
"profile": "uniform",
"scheme": "zou_he_local",
"trt_neq_damp": 0.5,
"regularized_neq_damp": 0.5
},
"outlet": {
"mode": "neq_extrap",
"backflow_clamp": true,
"blend_alpha": 0.7,
"srt_neq_damp": 0.5
},
"y_wall_bc": "free_slip",
"omega_guard": {
"min": 0.01,
"max": 1.96
}
},
"cuda": {
"threads_per_block": 256,
"compute_capability": "auto"
}
}
@@ -0,0 +1,52 @@
{
"_doc": "Three rotating cylinders in confined channel (triangle layout, free-slip walls).",
"_variant": "TRT + regularized inlet — Phase 2 candidate from Kan99b K2 screening",
"_screening": "Kan99b K2 D=20: St err=2.43%, CD err=2.45%, C'D err=9.06%",
"grid": {
"lattice_model": "D2Q9",
"nx": 3000,
"ny": 300,
"nz": 1
},
"physics": {
"data_type": "FP32",
"viscosity": 0.004,
"velocity": 0.04,
"rho": 1.0
},
"method": {
"collision": "TRT",
"streaming": "double_buffer",
"store_precision": "FP32",
"ddf_shifting": false,
"les": {
"enabled": false,
"cs": 0.16,
"closed_form": true
},
"trt": {
"magic_param": 0.1875
},
"inlet": {
"profile": "uniform",
"scheme": "regularized",
"trt_neq_damp": 0.5,
"regularized_neq_damp": 0.5
},
"outlet": {
"mode": "neq_extrap",
"backflow_clamp": true,
"blend_alpha": 0.7,
"srt_neq_damp": 0.5
},
"y_wall_bc": "free_slip",
"omega_guard": {
"min": 0.01,
"max": 1.96
}
},
"cuda": {
"threads_per_block": 256,
"compute_capability": "auto"
}
}
@@ -2,7 +2,7 @@
"_doc": "Three rotating cylinders in confined channel (triangle layout, free-slip walls).",
"grid": {
"lattice_model": "D2Q9",
"nx": 3000,
"nx": 1500,
"ny": 300,
"nz": 1
},
+3 -1
View File
@@ -38,8 +38,10 @@ _HEADER = "// AUTO-GENERATED by CelerisLab compiler – DO NOT EDIT MANUALLY\n"
def _write(path: str, content: str):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
tmp_path = f"{path}.tmp"
with open(tmp_path, "w") as f:
f.write(content)
os.replace(tmp_path, path)
def generate_config(cfg: LBMConfig, n_objects: int = 0):
@@ -4,6 +4,11 @@
// For wall-adjacent fluid nodes, the pull step loaded garbage from the wall.
// We replace those directions with the opposite-direction population at the
// SAME node from the INPUT buffer (half-way BB).
//
// In EsoPull mode (t > 0), reads via load_physical_dir_esopull to resolve
// the opposite direction's value from the EsoPull single-buffer layout.
// For Kan99b (cylinder far from walls), this is correct in practice.
// A more rigorous pre_streaming fix is pending further investigation.
// ============================================================================
#ifndef CELERIS_BOUNDARY_BOUNCE_BACK_CUH
@@ -16,19 +21,33 @@
__device__ inline void apply_wall_bb_d2q9_y_pull(unsigned int y,
float* __restrict__ f,
const fpxx* __restrict__ fi_in,
unsigned long k)
unsigned long k,
const unsigned long* j = nullptr,
unsigned long t = 0ul)
{
if (y == 1) {
// Directions sourced from y=0 wall in pull step: +y, +x+y, -x+y.
f[3] = load_ddf(fi_in, index_f(k, 4u));
f[5] = load_ddf(fi_in, index_f(k, 6u));
f[8] = load_ddf(fi_in, index_f(k, 7u));
if (t == 0ul) {
f[3] = load_ddf(fi_in, index_f(k, 4u));
f[5] = load_ddf(fi_in, index_f(k, 6u));
f[8] = load_ddf(fi_in, index_f(k, 7u));
} else {
f[3] = load_physical_dir_esopull(k, 4u, fi_in, j, t); // N ← S
f[5] = load_physical_dir_esopull(k, 6u, fi_in, j, t); // NE ← SW
f[8] = load_physical_dir_esopull(k, 7u, fi_in, j, t); // NW ← SE
}
}
else if (y == (unsigned int)(NY - 2)) {
// Directions sourced from y=NY-1 wall in pull step: -y, -x-y, +x-y.
f[4] = load_ddf(fi_in, index_f(k, 3u));
f[6] = load_ddf(fi_in, index_f(k, 5u));
f[7] = load_ddf(fi_in, index_f(k, 8u));
if (t == 0ul) {
f[4] = load_ddf(fi_in, index_f(k, 3u));
f[6] = load_ddf(fi_in, index_f(k, 5u));
f[7] = load_ddf(fi_in, index_f(k, 8u));
} else {
f[4] = load_physical_dir_esopull(k, 3u, fi_in, j, t); // S ← N
f[6] = load_physical_dir_esopull(k, 5u, fi_in, j, t); // SW ← NE
f[7] = load_physical_dir_esopull(k, 8u, fi_in, j, t); // SE ← NW
}
}
}
@@ -43,17 +62,31 @@ __device__ inline void apply_wall_bb_d2q9_y_pull(unsigned int y,
__device__ inline void apply_wall_freeslip_d2q9_y_pull(unsigned int y,
float* __restrict__ f,
const fpxx* __restrict__ fi_in,
unsigned long k)
unsigned long k,
const unsigned long* j = nullptr,
unsigned long t = 0ul)
{
if (y == 1) {
f[3] = load_ddf(fi_in, index_f(k, 4u));
f[5] = load_ddf(fi_in, index_f(k, 7u));
f[8] = load_ddf(fi_in, index_f(k, 6u));
if (t == 0ul) {
f[3] = load_ddf(fi_in, index_f(k, 4u));
f[5] = load_ddf(fi_in, index_f(k, 7u));
f[8] = load_ddf(fi_in, index_f(k, 6u));
} else {
f[3] = load_physical_dir_esopull(k, 4u, fi_in, j, t); // N ← S (normal)
f[5] = load_physical_dir_esopull(k, 7u, fi_in, j, t); // NE ← SE (tangential mirror)
f[8] = load_physical_dir_esopull(k, 6u, fi_in, j, t); // NW ← SW (tangential mirror)
}
}
else if (y == (unsigned int)(NY - 2)) {
f[4] = load_ddf(fi_in, index_f(k, 3u));
f[6] = load_ddf(fi_in, index_f(k, 8u));
f[7] = load_ddf(fi_in, index_f(k, 5u));
if (t == 0ul) {
f[4] = load_ddf(fi_in, index_f(k, 3u));
f[6] = load_ddf(fi_in, index_f(k, 8u));
f[7] = load_ddf(fi_in, index_f(k, 5u));
} else {
f[4] = load_physical_dir_esopull(k, 3u, fi_in, j, t); // S ← N (normal)
f[6] = load_physical_dir_esopull(k, 8u, fi_in, j, t); // SW ← NW (tangential mirror)
f[7] = load_physical_dir_esopull(k, 5u, fi_in, j, t); // SE ← NE (tangential mirror)
}
}
}
@@ -25,18 +25,30 @@ __device__ inline void repair_zou_he_west_knowns_d2q9(
float* __restrict__ f,
const fpxx* __restrict__ fi_in,
unsigned int x,
unsigned int y)
unsigned int y,
const unsigned long* j_neb = nullptr,
unsigned long t = 0ul)
{
if (x != 0u) return;
const unsigned long k_int = linear_index(x + 1u, y);
if (y == 1u) {
f[3] = load_ddf(fi_in, index_f(k_int, 3u));
f[8] = load_ddf(fi_in, index_f(k_int, 8u));
if (t == 0ul || j_neb == nullptr) {
f[3] = load_ddf(fi_in, index_f(k_int, 3u));
f[8] = load_ddf(fi_in, index_f(k_int, 8u));
} else {
f[3] = load_physical_dir_pre_streaming(k_int, 3u, fi_in, j_neb, t);
f[8] = load_physical_dir_pre_streaming(k_int, 8u, fi_in, j_neb, t);
}
} else if (y == (unsigned int)(NY - 2)) {
f[4] = load_ddf(fi_in, index_f(k_int, 4u));
f[6] = load_ddf(fi_in, index_f(k_int, 6u));
if (t == 0ul || j_neb == nullptr) {
f[4] = load_ddf(fi_in, index_f(k_int, 4u));
f[6] = load_ddf(fi_in, index_f(k_int, 6u));
} else {
f[4] = load_physical_dir_pre_streaming(k_int, 4u, fi_in, j_neb, t);
f[6] = load_physical_dir_pre_streaming(k_int, 6u, fi_in, j_neb, t);
}
}
}
@@ -139,7 +139,11 @@ __device__ __forceinline__ void apply_inlet_esopull_d2q9(
// Free-slip y-walls: repair west inlet ghost-node known directions.
#if Y_WALL_BC == 1
repair_zou_he_west_knowns_d2q9(f, fi, x, y);
{
unsigned long j_neb[NQ];
compute_neighbors(x + 1u, y, j_neb);
repair_zou_he_west_knowns_d2q9(f, fi, x, y, j_neb, t);
}
#endif
#if INLET_SCHEME == 0
@@ -6,8 +6,8 @@
#define NT 256
#define MULT_GPU 0
#define NX 512
#define NY 256
#define NX 1351
#define NY 601
#define NZ 1
// ---- Lattice model (single source of truth) ----
@@ -3,8 +3,8 @@
#ifndef CELERIS_CONFIG_METHOD_H
#define CELERIS_CONFIG_METHOD_H
#define COLLISION_MODEL 0
#define STREAMING_MODEL 0
#define COLLISION_MODEL 2
#define STREAMING_MODEL 1
#define STORE_PRECISION 0
#define USE_DDF_SHIFTING 0
@@ -12,12 +12,12 @@
#define LES_CS 0.160000f
#define LES_CLOSED_FORM 1
#define INLET_PROFILE 1
#define INLET_SCHEME 0
#define INLET_PROFILE 0
#define INLET_SCHEME 3
#define OUTLET_MODE 0
#define OUTLET_BLEND_ALPHA 0.700f
#define OUTLET_BACKFLOW_CLAMP 1
#define Y_WALL_BC 0
#define Y_WALL_BC 1
#define OMEGA_COLLISION_MIN 0.01f
#define OMEGA_COLLISION_MAX 1.990f
@@ -3,6 +3,6 @@
#ifndef CELERIS_CONFIG_OBJECTS_H
#define CELERIS_CONFIG_OBJECTS_H
#define N_OBJS 0
#define N_OBJS 1
#endif
@@ -4,7 +4,7 @@
#define CELERIS_CONFIG_PHYSICS_H
#define LBtype float
#define VIS 0.0035000000
#define VIS 0.0090000000
#define RHO 1.0
#define U0 0.03
+15 -6
View File
@@ -11,10 +11,19 @@
//
// EsoPull mode:
// PrepareEsoPullCurvedKernel + ApplyEsoPullCurvedKernel run before EsoPullStep.
// SensorKernel / ForceRegionKernel run after, using t = step_count+1 for
// physical-value semantics via esopull_semantic_helpers.cuh.
// Prepare uses load_physical_dir_pre_streaming() for Bouzidi force calc
// (pre-streaming semantics = matching double-buffer fi_in[k, dir]).
// SensorKernel / ForceRegionKernel run after EsoPullStep, using
// t = step_count+1 for post-streaming physical-value semantics.
// Wall boundary functions (bounce_back.cuh) use load_physical_dir_esopull
// inside EsoPullStep (post-streaming, matching load_f_esopull output).
//
// Verified: Kan99b K2 (200k steps, MRT) -- bit-identical to double-buffer.
// NOTE: The wall boundary helpers use post-streaming reads (unlike Bouzidi)
// because they operate inside EsoPullStep on f[] already reconstructed
// by load_f_esopull. Replacing post-streaming f[i] with post-streaming
// f[opp(i)] is the correct half-way bounce-back for EsoPull.
//
// Verified: Kan99b K2 (60k steps, MRT) — bit-identical to double-buffer.
// ============================================================================
#ifndef CELERIS_STEP_AUX_KERNELS_CU
@@ -118,7 +127,7 @@ __global__ void PrepareEsoPullCurvedKernel(
const float Uw = -omega * ry;
const float Vw = omega * rx;
const float f_toward = load_physical_dir_esopull(k_f, dir, fi, j_f, t);
const float f_toward = load_physical_dir_pre_streaming(k_f, dir, fi, j_f, t);
float f_reflected;
if (fallback == CURVED_FALLBACK_BOUZIDI) {
@@ -128,10 +137,10 @@ __global__ void PrepareEsoPullCurvedKernel(
coordinates(k_ff, xff, yff);
unsigned long j_ff[NQ];
compute_neighbors(xff, yff, j_ff);
const float f_toward_ff = load_physical_dir_esopull(k_ff, dir, fi, j_ff, t);
const float f_toward_ff = load_physical_dir_pre_streaming(k_ff, dir, fi, j_ff, t);
f_reflected = compute_bouzidi_reflection(f_toward, f_toward_ff, 0.0f, q, fallback);
} else {
const float f_opp_same = load_physical_dir_esopull(k_f, dir_opp, fi, j_f, t);
const float f_opp_same = load_physical_dir_pre_streaming(k_f, dir_opp, fi, j_f, t);
f_reflected = compute_bouzidi_reflection(f_toward, 0.0f, f_opp_same, q, fallback);
}
} else {
+74 -2
View File
@@ -33,6 +33,74 @@ __device__ __forceinline__ void write_rest_equilibrium(
}
}
// ---- EsoPull layout helpers: equilibrium in EsoPull backing layout at t=0 ----
// In EsoPull t=0 (even step), store_f_esopull writes:
// f[i_odd] → fi[ j[i_odd], i_odd ] (neighbor, forward slot)
// f[i_even] → fi[ k, i_even ] (local, reverse slot)
// These helpers write equilibrium values directly in the correct EsoPull
// layout so that load_f_esopull(t=0) reconstructs the correct f_eq[].
#if DIM == 2
__device__ __forceinline__ void write_equilibrium_esopull(
fpxx* fi, unsigned long k, unsigned int x, unsigned int y, float u_init)
{
unsigned long j[NQ];
compute_neighbors(x, y, j);
// --- rest particle: always local, slot 0 ---
float val0 = d_w[0] * RHO * (1.0f - 1.5f * u_init * u_init);
#if USE_DDF_SHIFTING
val0 -= d_w[0];
#endif
store_ddf(fi, index_f(k, 0u), val0);
// --- paired directions (i=1,3,5,7): {E,W}, {N,S}, {NE,SW}, {SE,NW} ---
for (int i = 1; i < NQ; i += 2) {
float cu_odd = (float)d_cx[i] * u_init;
float val_odd = d_w[i] * RHO
* (1.0f + 3.0f * cu_odd + 4.5f * cu_odd * cu_odd
- 1.5f * u_init * u_init);
float cu_even = (float)d_cx[i + 1] * u_init;
float val_even = d_w[i + 1] * RHO
* (1.0f + 3.0f * cu_even + 4.5f * cu_even * cu_even
- 1.5f * u_init * u_init);
#if USE_DDF_SHIFTING
val_odd -= d_w[i];
val_even -= d_w[i + 1];
#endif
// t=0 even step (load_f_esopull): f[i] reads from local slot i+1,
// f[i+1] reads from neighbor slot i.
// Therefore: fi[n, i+1] = f_eq[i] (odd goes to local reverse),
// fi[j[i], i] = f_eq[i+1] (even goes to neighbor forward)
store_ddf(fi, index_f(k, (unsigned int)(i + 1)), val_odd);
store_ddf(fi, index_f(j[i], (unsigned int)i), val_even);
}
}
__device__ __forceinline__ void write_rest_equilibrium_esopull(
fpxx* fi, unsigned long k, unsigned int x, unsigned int y)
{
unsigned long j[NQ];
compute_neighbors(x, y, j);
float val0 = d_w[0] * RHO;
#if USE_DDF_SHIFTING
val0 -= d_w[0];
#endif
store_ddf(fi, index_f(k, 0u), val0);
for (int i = 1; i < NQ; i += 2) {
float val_odd = d_w[i] * RHO;
float val_even = d_w[i + 1] * RHO;
#if USE_DDF_SHIFTING
val_odd -= d_w[i];
val_even -= d_w[i + 1];
#endif
store_ddf(fi, index_f(k, (unsigned int)(i + 1)), val_odd);
store_ddf(fi, index_f(j[i], (unsigned int)i), val_even);
}
}
#endif // DIM == 2
__device__ __forceinline__ uint16_t channel_flag_from_coords(
unsigned int x, unsigned int y)
{
@@ -103,10 +171,14 @@ __global__ void InitEsoPull(uint16_t* flag, fpxx* fi)
const uint16_t fl = finalize_domain_flag(flag[k], x, y);
flag[k] = fl;
// Write equilibrium directly in EsoPull t=0 backing layout so that
// load_f_esopull(k, f, fi, j, 0) at the first EsoPullStep reconstructs
// the correct physical f_eq[]. The neighbor scatter uses a slot
// partitioning per cell with no write conflicts across threads.
if (is_solid(fl)) {
write_rest_equilibrium(fi, k);
write_rest_equilibrium_esopull(fi, k, x, y);
} else {
write_equilibrium(fi, k, inlet_target_u((float)y));
write_equilibrium_esopull(fi, k, x, y, inlet_target_u((float)y));
}
#elif DIM == 3
unsigned int x, y, z; unsigned long k;
@@ -95,9 +95,9 @@ void EsoPullStep(
// ---- y-wall adjacent row correction ----
if (is_fluid(fl) && (y == 1u || y == (unsigned int)(NY - 2))) {
#if Y_WALL_BC == 1
apply_wall_freeslip_d2q9_y_pull(y, f, fi, k);
apply_wall_freeslip_d2q9_y_pull(y, f, fi, k, j, t);
#else
apply_wall_bb_d2q9_y_pull(y, f, fi, k);
apply_wall_bb_d2q9_y_pull(y, f, fi, k, j, t);
#endif
}
@@ -9,16 +9,19 @@
// The parity/cell/slot rules must match esopull_single_buffer.cuh exactly.
//
// Usage convention (see also stepper.py for launch order):
// PrepareEsoPullCurvedKernel(t): reads fi with semantics t [correct]
// PrepareEsoPullCurvedKernel(t): reads fi with PRE-streaming semantics [Bouzidi force]
// EsoPullStep(t): load_f_esopull(t) -> collide -> store_f_esopull(t)
// ForceRegionKernel(t): reads/writes fi with semantics t [correct]
// SensorKernel(t): reads fi with semantics t [correct]
// MacroscopicEsoPullKernel(t): reads fi with semantics t [correct]
// ForceRegionKernel(t): reads/writes fi with post-streaming semantics
// SensorKernel(t): reads fi with post-streaming semantics
// MacroscopicEsoPullKernel(t): reads fi with post-streaming semantics
// Wall boundary functions(t): reads fi with pre-streaming semantics
//
// The "t" parameter always represents the completed step count corresponding
// to the current physical interpretation of the backing layout.
// Double-buffer mode: t = 0, raw fi[k,i] = physical direction i.
// Verified: Kan99b K2 (200k steps, MRT) -- bit-identical to double-buffer.
//
// Verified: Kan99b K2 (200k steps, MRT) — bit-identical to double-buffer for
// Strouhal, CD, CL, C'L, C'D metrics (FP32, D=20/30).
// ============================================================================
#ifndef CELERIS_STREAMING_ESOPULL_SEMANTIC_HELPERS_CUH
@@ -55,6 +58,52 @@ __device__ __forceinline__ float load_physical_dir_esopull(
return load_ddf(fi, index_f(src, slot));
}
// Pre-streaming physical read: returns the post-collision value that was
// AT cell k in direction dir BEFORE streaming (matching double-buffer
// fi_in[k_f, dir] semantics).
//
// For Bouzidi force computation, we need the value that sat at the fluid
// cell in the given direction after collision of the previous step, not
// the post-streaming value that arrived from a neighbor.
//
// KEY: The backing layout was written by the PREVIOUS EsoPullStep with
// parity (t-1), so for t>0 we use parity tp = t^1. For t=0 the init
// layout is load_f_esopull-compatible (not store-compatible), so we fall
// back to load_physical_dir_esopull which reads init correctly.
//
// In store_f_esopull layout (tp parity):
// odd dir (E,N,NE,SE): stored at neighbor j[dir], slot dir (even tp)
// or slot dir+1 (odd tp)
// even dir (W,S,SW,NW): stored at local, slot dir (even tp)
// or slot opp_dir(dir) (odd tp)
// rest (0): always local, always slot 0
__device__ __forceinline__ float load_physical_dir_pre_streaming(
unsigned long k, unsigned int dir,
const fpxx* fi, const unsigned long* j, unsigned long t)
{
// At t=0 the backing layout is init-compatible, not store-compatible.
// Fall back to load_physical_dir_esopull which corresponds to init layout.
if (t == 0ul) {
return load_physical_dir_esopull(k, dir, fi, j, t);
}
// For t>0, the backing was produced by store_f_esopull(tp) where
// tp = t^1 (previous step's parity).
const unsigned long tp = t ^ 1ul;
if (dir == 0u) {
return load_ddf(fi, index_f(k, 0u));
}
if (dir & 1u) {
const unsigned long src = j[dir];
const unsigned int slot = (tp & 1ul) ? (dir + 1u) : dir;
return load_ddf(fi, index_f(src, slot));
} else {
const unsigned int slot = (tp & 1ul) ? (unsigned int)opp_dir((int)dir) : dir;
return load_ddf(fi, index_f(k, slot));
}
}
// ---------------------------------------------------------------------------
// Full-node physical read (reconstruct f[] from backing layout)
// ---------------------------------------------------------------------------
+5 -1
View File
@@ -56,6 +56,7 @@ class Simulation:
# Compile kernel
compiler.generate_config(self.lbm_cfg, n_objects=0)
self._compiled_n_objects: int = 0
self._ptx_path = compiler.compile_kernel(arch=arch)
self._module = compiler.load_module(self._ptx_path)
@@ -333,6 +334,7 @@ class Simulation:
expected_n_objects = self.bodies.count
arch = self._resolve_compile_arch()
compiler.generate_config(self.lbm_cfg, n_objects=expected_n_objects)
self._compiled_n_objects = int(expected_n_objects)
# Remove stale PTX to prevent PyCUDA module handle conflicts.
import os as _os
if _os.path.exists(self._ptx_path):
@@ -605,7 +607,9 @@ class Simulation:
return f"sm_{''.join(self.lbm_cfg.compute_capability.split('.'))}"
def _read_generated_n_objects(self) -> int:
"""Read N_OBJS from generated config_objects.h for safety checks."""
"""Return cached N_OBJS from the last compile (avoids header races)."""
if hasattr(self, "_compiled_n_objects"):
return int(self._compiled_n_objects)
cfg_path = compiler.kernel_path("config/config_objects.h")
with open(cfg_path, "r", encoding="utf-8") as f:
for line in f: