Example/docs closure fixes

- README: close the Manual stream control code fence before the new
  section; snippet now builds rest-equilibrium inputs (runnable as shown).
- Example: diagnostics use the known fluid-leaf masks (uncovered coarse
  minus slot-mode wall rows; fine ACTIVE minus solids) and raise on any
  non-finite population or rho <= 0 there instead of filtering by
  finiteness. No solver changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-15 05:11:13 +00:00
1 parent 5304525e39
commit 749f103ea8
2 files changed
+30 -8

No files matched your search

+7 -1
View File
@@ -90,6 +90,7 @@ sim.run(100, stream=stream, upload_act=False, sync_obs=False)
# Later: sync and read # Later: sync and read
stream.synchronize() stream.synchronize()
obs = sim.read_bodies(stream=stream) # sync already done, just read pinned buffer obs = sim.read_bodies(stream=stream) # sync already done, just read pinned buffer
```
### Fixed 2:1 refinement patch (experimental backend) ### Fixed 2:1 refinement patch (experimental backend)
@@ -99,10 +100,12 @@ normal surface velocity (blowing/suction). It is a standalone backend, not part
of `Simulation`. Runnable demo: `examples/fixed_patch_circle.py`. of `Simulation`. Runnable demo: `examples/fixed_patch_circle.py`.
```python ```python
import numpy as np
from CelerisLab.body.geometry.circle import CircleGeometry from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.body.objects import SimObject, SurfaceActuation from CelerisLab.body.objects import SimObject, SurfaceActuation
from CelerisLab.config import LBMConfig from CelerisLab.config import LBMConfig
from CelerisLab.cuda.context import CudaContext from CelerisLab.cuda.context import CudaContext
from CelerisLab.lbm.equilibrium import compute_feq_d2q9
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
cfg = LBMConfig(nx=32, ny=24, viscosity=.1) cfg = LBMConfig(nx=32, ny=24, viscosity=.1)
@@ -110,7 +113,10 @@ body = SimObject(0, geometry=CircleGeometry(16.0, 12.0, 2.15), center=(16.0, 12.
surface_actuation=SurfaceActuation(0.7, 0.85, 0.002)) # angle, half-width, speed surface_actuation=SurfaceActuation(0.7, 0.85, 0.002)) # angle, half-width, speed
with CudaContext(0) as ctx, FixedPatchStepper(cfg, (8, 6, 24, 18), context=ctx, with CudaContext(0) as ctx, FixedPatchStepper(cfg, (8, 6, 24, 18), context=ctx,
fine_objects=[body], fine_block=(16, 20)) as run: fine_objects=[body], fine_block=(16, 20)) as run:
run.initialize(coarse_g, fine_g) # (9, ny, nx) postcollision populations per level rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
# (9, ny, nx) postcollision populations per level; non-leaf cells are discarded.
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
np.broadcast_to(rest, (9, *run.patch.fine_shape)))
run.step(128) # coarse steps; fine level takes two per step run.step(128) # coarse steps; fine level takes two per step
coarse_g, fine_g = run.download() # NaN on covered, ghost and solid cells coarse_g, fine_g = run.download() # NaN on covered, ghost and solid cells
``` ```
+23 -7
View File
@@ -19,8 +19,9 @@ from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.body.objects import SimObject, SurfaceActuation from CelerisLab.body.objects import SimObject, SurfaceActuation
from CelerisLab.config import LBMConfig from CelerisLab.config import LBMConfig
from CelerisLab.cuda.context import CudaContext from CelerisLab.cuda.context import CudaContext
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY, SOLID
from CelerisLab.lbm.equilibrium import compute_feq_d2q9 from CelerisLab.lbm.equilibrium import compute_feq_d2q9
from CelerisLab.lbm.refinement import ACTIVE
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
NX, NY, BOUNDS = 32, 24, (8, 6, 24, 18) # coarse cells; patch is half-open (x0, y0, x1, y1) NX, NY, BOUNDS = 32, 24, (8, 6, 24, 18) # coarse cells; patch is half-open (x0, y0, x1, y1)
@@ -35,15 +36,29 @@ def config(mode):
wall_slot_width=8, y_wall_bc="on_site_velocity", top_wall_bc="reservoir") wall_slot_width=8, y_wall_bc="on_site_velocity", top_wall_bc="reservoir")
def leaf_diagnostics(coarse, fine): def leaf_masks(run, mode):
"""Fluid leaves only (downloads are NaN on covered/ghost/solid cells). """Known fluid leaves: uncovered coarse cells (minus wall rows in slot mode)
and fine ACTIVE cells outside the body."""
p = run.patch
x0, y0, x1, y1 = p.bounds
coarse = np.ones(p.coarse_shape, bool)
coarse[y0:y1, x0:x1] = False
if mode == "slot_reservoir":
coarse[[0, -1]] = False
solid = (run.fine_boundary.flags.reshape(p.fine_shape) & SOLID) != 0
return coarse, (p.fine_kind == ACTIVE) & ~solid
def leaf_diagnostics(coarse, fine, masks):
"""Fluid leaves only; a non-finite population or rho <= 0 there is an error.
Mass counts a fine cell as 1/4 of a coarse cell (2D, 2:1).""" Mass counts a fine cell as 1/4 of a coarse cell (2D, 2:1)."""
out = {} out = {}
for name, g, area in (("coarse", coarse, 1.0), ("fine", fine, .25)): for name, g, leaf, area in (("coarse", coarse, masks[0], 1.0), ("fine", fine, masks[1], .25)):
leaf = np.isfinite(g).all(axis=0)
f = g[:, leaf].astype(np.float64) f = g[:, leaf].astype(np.float64)
rho = f.sum(axis=0) rho = f.sum(axis=0)
if not np.isfinite(f).all() or (rho <= 0).any():
raise RuntimeError(f"non-finite population or non-positive density on a {name} fluid leaf")
ux = (np.array(D2Q9_EX)[:, None] * f).sum(axis=0) / rho ux = (np.array(D2Q9_EX)[:, None] * f).sum(axis=0) / rho
uy = (np.array(D2Q9_EY)[:, None] * f).sum(axis=0) / rho uy = (np.array(D2Q9_EY)[:, None] * f).sum(axis=0) / rho
out[name] = (area * rho.sum(), rho.min(), rho.max(), np.hypot(ux, uy).max()) out[name] = (area * rho.sum(), rho.min(), rho.max(), np.hypot(ux, uy).max())
@@ -83,11 +98,12 @@ def main():
storage = f"blocked {fine_block}" if fine_block else "dense" storage = f"blocked {fine_block}" if fine_block else "dense"
print(f"mode={args.mode} fine storage={storage} speed={args.speed:+g} " print(f"mode={args.mode} fine storage={storage} speed={args.speed:+g} "
f"(fine allocation {p.fine_shape[0]}x{p.fine_shape[1]}, fine origin {p.fine_origin})") f"(fine allocation {p.fine_shape[0]}x{p.fine_shape[1]}, fine origin {p.fine_origin})")
start = leaf_diagnostics(*run.download()) masks = leaf_masks(run, args.mode)
start = leaf_diagnostics(*run.download(), masks)
for _ in range(0, args.steps, 32): for _ in range(0, args.steps, 32):
run.step(min(32, args.steps - run.step_count)) run.step(min(32, args.steps - run.step_count))
coarse, fine = run.download() # synchronized postcollision G, NaN on non-leaves coarse, fine = run.download() # synchronized postcollision G, NaN on non-leaves
d = leaf_diagnostics(coarse, fine) d = leaf_diagnostics(coarse, fine, masks)
mass = d["coarse"][0] + d["fine"][0] mass = d["coarse"][0] + d["fine"][0]
rho_lo = min(d["coarse"][1], d["fine"][1]) rho_lo = min(d["coarse"][1], d["fine"][1])
rho_hi = max(d["coarse"][2], d["fine"][2]) rho_hi = max(d["coarse"][2], d["fine"][2])