Compare commits

..
Author SHA1 Message Date
TRON 5465832fe5 fix: normalize closed-form Smagorinsky strain magnitude 2026-10-18 01:14:24 -07:00
107 changed files with 855 additions and 7056 deletions
+38 -62
View File
@@ -112,9 +112,8 @@ from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
cfg = LBMConfig(nx=32, ny=24, viscosity=.1)
body = SimObject(0, geometry=CircleGeometry(16.0, 12.0, 2.15), center=(16.0, 12.0), radius=2.15,
surface_actuation=SurfaceActuation(0.7, 0.85, 0.002)) # angle, half-width, speed
with CudaContext(0) as ctx, FixedPatchStepper(cfg, context=ctx, refinements=[(8, 6, 24, 18)],
level_blocks=[None, (16, 20)],
level_objects=[(), (body,)]) as run:
with CudaContext(0) as ctx, FixedPatchStepper(cfg, (8, 6, 24, 18), context=ctx,
fine_objects=[body], fine_block=(16, 20)) as run:
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)),
@@ -124,9 +123,7 @@ with CudaContext(0) as ctx, FixedPatchStepper(cfg, context=ctx, refinements=[(8,
```
Scope and limits:
- The examples below use D2Q9, SRT, FP32, double buffer, unshifted, no LES or
forcing. Fixed 2:1 hierarchies use level lists of arbitrary depth; two- and
three-level examples below use that same constructor.
- D2Q9, SRT, FP32, double buffer, unshifted, no LES or forcing; fixed 2:1 patch(es), two levels (or one nested third).
- `coarse_boundary="periodic"` (default) or `"slot_reservoir"` (bottom slot, top
reservoir; patch needs `y0 >= 4`, `y1 <= ny-4`). Periodic mode ignores the wall/slot config fields.
- Object centers/radii use the coarse **cell-edge** frame (cell `I` spans `[I, I+1)`),
@@ -134,51 +131,46 @@ Scope and limits:
- Bodies must be stationary circles whose whole boundary stencil lies in the fine
active region, clear of the coarse/fine transfer cells; violations are rejected
before any GPU allocation.
- `level_blocks=[None, (by, bx)]` stores the fine level in fixed-size blocks; `by`/`bx` must
- `fine_block=(by, bx)` stores the fine level in fixed-size blocks; `by`/`bx` must
divide the WHOLE fine allocation, i.e. `2*(y1-y0)+8` by `2*(x1-x0)+8` including the
four-cell ghost band. It gives results identical to dense storage and is
infrastructure for future block AMR, not a speedup.
- Several regions: pass a sequence of `(x0, y0, x1, y1)` rectangles as
`refinements[0]`, with both levels blocked (`level_blocks`). Transfers are built once
from their union; adjacent, overlapping and concave unions are accepted when
their actual dependencies close. There is no fixed rectangle-gap rule.
The fine level is then one level-wide grid `(2*ny, 2*nx)`:
- Several separated patches: pass a sequence of `(x0, y0, x1, y1)`
rectangles as `bounds`, at least 4 coarse cells apart, with both `fine_block`
and `base_block`. The fine level is then one level-wide grid `(2*ny, 2*nx)`:
`run.patch.fine_shape` gives it, `initialize`/`download` use it, and only the
grid-aligned tiles intersecting the union plus its four-cell ghost band are stored.
Block sizes must divide the level-wide grid; rectangle edges need not align
with tiles. Order of rectangles does not matter. L1 bodies/Jets (`level_objects[1]`/`level_jets[1]`) work
as for one patch: each circle (same coarse cell-edge frame) must lie wholly
inside the union's active region with
patch windows (active region plus 4-cell ghost band, origin `(2*y0-4, 2*x0-4)`)
are stored; each window must align with `fine_block`. Order of rectangles does
not matter. `fine_objects`/`jets` work as for one patch: each circle (same
coarse cell-edge frame) must lie wholly inside one patch's active region with
its boundary stencil clear of transfer cells; a jet may not select links of two
different bodies, even across regions. Wall-touching patches remain unsupported.
For nesting, add further `refinements` entries as shown below.
different bodies, even across patches. No wall-touching patches or nesting yet.
```python
cfg = LBMConfig(nx=32, ny=16, viscosity=.1)
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
with CudaContext(0) as ctx, FixedPatchStepper(
cfg, context=ctx, refinements=[((4, 4, 12, 12), (16, 4, 24, 12))],
level_blocks=[(4, 4), (4, 4)]) as run:
cfg, ((4, 4, 12, 12), (16, 4, 24, 12)), context=ctx,
fine_block=(4, 4), base_block=(4, 4)) as run:
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
np.broadcast_to(rest, (9, *run.patch.fine_shape))) # (9, 32, 64)
run.step(32)
coarse_g, fine_g = run.download() # coarse: NaN on every covered cell;
# fine: NaN on every non-ACTIVE cell
```
- Three-level nested windows: pass two scalar rectangles in `refinements` —
L1 in L0 cells, L2 in **L1 window cells** (the parent's fine grid including its
4-cell ghost band) — plus all three `level_blocks` entries. L2 must sit inside
L1's ACTIVE cells, away from L1's ghost band and transfer footprint (checked
before any GPU allocation). `initialize` and `download` take/return three
arrays; each level is NaN on its non-ACTIVE (and child-covered) cells. One
coarse step = 2 L1 steps = 4 L2 steps. One L1 window only in this scalar
chain. Optional stationary circles and Jets live on L2 via `level_objects[2]`
and `level_jets[2]`; L1 bodies/Jets retain their L1 meaning and must be empty
in this mode. All object and Jet geometry uses the **L0 edge frame**, not the
child bounds' L1 window frame. L2 has dx=dt=0.25, so prescribed lattice
velocity is unchanged. Each whole body and its BFL stencil must be inside L2
ACTIVE cells, outside the transfer footprint. A Jet may select one physical
body across multiple blocks, never two bodies.
- Three levels (one nested chain): add `child=(x0, y0, x1, y1)` in
**L1 window cells** (the patch's fine grid including its 4-cell ghost band) and
`child_block`, with `fine_block` and `base_block`. L2 must sit inside L1's
ACTIVE cells, away from L1's ghost band and transfer footprint (checked before
any GPU allocation). `initialize` and `download` take/return three arrays; each
level is NaN on its non-ACTIVE (and child-covered) cells. One coarse step = 2 L1
steps = 4 L2 steps. One L1 patch only. Optional stationary circles and Jets
live on L2 via `child_objects` and `child_jets`; `fine_objects`/`jets` retain
their L1 meaning and must be empty in this mode. All object and Jet geometry
uses the **L0 edge frame**, not the child bounds' L1 window frame. L2 has
dx=dt=0.25, so prescribed lattice velocity is unchanged. Each whole body and
its BFL stencil must be inside L2 ACTIVE cells, outside the transfer footprint.
A Jet may select one physical body across multiple blocks, never two bodies.
```python
from CelerisLab.body.geometry.circle import CircleGeometry
@@ -190,9 +182,9 @@ Scope and limits:
jet = Jet(Tube((6.5, 8.), 0., 3., .3), Parallel(0.), amplitude=.002)
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
with CudaContext(0) as ctx, FixedPatchStepper(
cfg, context=ctx, refinements=[(4, 4, 12, 12), (8, 8, 16, 16)],
level_blocks=[(4, 4)] * 3, level_objects=[(), (), (body,)],
level_jets=[(), (), (jet,)]) as run:
cfg, (4, 4, 12, 12), context=ctx, fine_block=(4, 4), base_block=(4, 4),
child=(8, 8, 16, 16), child_block=(4, 4),
child_objects=[body], child_jets=[jet]) as run:
shapes = (run.patch.coarse_shape, run.patch.fine_shape, run.child_patch.fine_shape)
run.initialize(*(np.broadcast_to(rest, (9, *s)) for s in shapes)) # 16², 24², 24²
run.step(4)
@@ -200,30 +192,14 @@ Scope and limits:
run.step(4)
coarse_g, l1_g, l2_g = run.download() # solids/covered/nonleaves are NaN
```
- Level lists (the only constructor): `refinements[l]` describes level `l+1`
in allocated level-`l` integer cells; use a rectangle list for a union, or a
scalar rectangle for a single window (a scalar child requires a scalar
parent). `level_blocks`, `level_objects` and `level_jets` each have one entry
per level, including L0. Three or more levels require all-blocked storage and
bodies/Jets only on the finest level. Every adjacent pair must preserve
inherited ACTIVE coverage, valid donors/receivers, surviving pulls and the
upstream transfer footprint. Level `l` takes `2**l` steps per base interval.
This describes implemented topology and scheduling, not accuracy or
convergence qualification.
- Blocked body levels can enable `observe_raw_exchange=True` and call
`read_raw_exchange(body_id, level=...)` after a completed base interval.
It reduces unique cut-link observations into wall/aperture/total force,
torque and mass exchange in base-lattice units per unit depth, for the last
base interval (default) or `window="last_substep"`.
`observe_window_exchange=True`, alongside `observe_raw_exchange=True`, enables
`start_exchange_window()` / `read_exchange_window(body_id, level=...)` across
completed base intervals. These are raw boundary-reaction diagnostics, not a general
stress or nozzle-load accuracy claim; dense body layouts are unsupported.
- Not available: checkpoints, moving bodies, L1 bodies/Jets in a three-level
chain, automatic dynamic refinement, `Simulation` integration. Qualification
depends on the tested configuration; see [the validation scope](docs/validation/2d_scope.md).
CPU topology/schedule tests alone do not establish GPU numerical correctness,
general convergence or long-time stability.
- Not available: per-link surface exchange/force readout in blocked mode,
checkpoints, moving bodies, L1 bodies/Jets in a three-level chain,
several patches per level with nesting, dynamic refinement, `Simulation`
integration. Validation so far is storage/operator equivalence and short runs,
not accuracy, convergence or long-time stability. The new L2-body GPU gates
(layouts, masks, amplitude reversal, legacy equivalence) and independent
every-substep numerical replay remain required acceptance checks; CPU tests
alone do not establish GPU numerical correctness.
## Installation
-35
View File
@@ -1,35 +0,0 @@
# Compatibility and API changes
Changes that can make a previously accepted setup or call behave differently.
Each entry names the commit, the affected entry points and the observable result.
## 87c3a31: cut-link ownership is validated for every body
`validate_cut_link_ownership` (`body/preprocess/flag_overlay.py`) now runs for
passive bodies as well as Jet/aperture bodies. Every cut link must have an
active fluid receiver (FLUID set, SOLID clear), and each `(receiver, direction)`
population may have only one writer.
Rejected with `ValueError`, before flags, populations or device buffers change:
1. Overlapping or touching passive solids whose cut links write the same
population ("Boundary requires unique cut-link writers").
2. A body whose cut-link receiver is a non-fluid cell, e.g. a channel wall row
or the inlet/outlet column ("Cut links must have active fluid receivers").
Both were previously accepted silently for passive bodies; the interpolated
boundary then had undefined results. The rules apply to the cut links of all
bodies together, so a passive body overlapping a Jet body is rejected as well.
The check runs in `Simulation.initialize`, runtime body sync (`sync_bodies`),
checkpoint save and checkpoint load (`common/checkpoint.py`). Load validates
before restoring anything, so a checkpoint whose saved geometry violates either
rule no longer restores; rebuild the geometry with separated bodies.
## 87c3a31: `Simulation.read_surface_exchange` accepts Jet-driven bodies
`read_surface_exchange(id)` previously required `SurfaceActuation` on the body.
It now also accepts bodies that own Jet aperture links, including Jets with zero
or negative amplitude (the aperture test is geometric). Bodies with neither
`SurfaceActuation` nor aperture links still raise
`ValueError("Body has no surface actuation")`. The value is unchanged: the latest
step's wall/aperture exchange, not total nozzle force.
@@ -1,62 +0,0 @@
# Selective LES normalization integration on dev8403956
This change reuses the reviewed correction from 5465832 without merging its
branch history. The only production change is the two closed-form coefficients
18 -> 18 sqrt(2) and their explanatory comments in turbulence_smag.cuh. Its SHA256
is a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa.
Defaults, iterative/deviatoric/capped closure, collision dispatch, AMR guards,
and all other production source files are unchanged.
## Separate source contract; unchanged experiment
The single existing flow runner now accepts `--experiment dev8403956`.
Its separate source_pins_dev8403956.json pins all 101 production files from the
committed 8403956 baseline; corrected mode permits only the exact reviewed
header substitution. Results record the actual checkout commit, import path,
experiment name and manifest SHA256. No arbitrary manifest-path override exists.
Omitting the option still selects the original historical source manifest.
That manifest, the original paired receipts, scalar preparation manifests and
independent continuum oracle are unchanged. Historical baseline and corrected
receipts remain evidence for their own exact sources, not new dev receipts.
Historical guards intentionally reject this different dev source tree. Eight
production files already differed between the historical source snapshot and
8403956; neither the old manifest nor its acceptance is silently regenerated.
The physical experiment and every numerical acceptance threshold are unchanged:
8 x 34 nodes, halfway y walls, periodic x, unforced D2Q9/SRT, shifted FP32,
double-buffer streaming, Cs=0/.16/1, checkpoints 519/1038/2075. Each case has one
actual advancing warm-up followed by complete initial-state restoration before
READY. Total launches are 6,225 measured plus three warm-ups. Cs=1 remains an
amplified diagnostic, not a physical coefficient recommendation.
## Host checks
PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s tests/unit -p 'test_smagorinsky_normalization.py' -v
PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s tests/unit -p 'test_les_flow_*.py' -v
The direct-current-header test is copied unchanged from reviewed 5465832:
120 tensor parameter cases, Cs=0/equilibrium and omega guards, converged
trace-free iterative comparison, and mutation detection in both lattice
algebras. This is scalar D2/D3 algebra coverage, not 3D-flow qualification.
Flow tests retain analytic/reference checks and add current named-source,
reverse-patched baseline, variant rejection, unrelated-source tampering and
immutable historical-default checks. The temporary baseline is constructed
only inside a test directory; production is never reverse-patched.
## Pending final-commit GPU gate
After independent review and explicit executor authorization, run from the
exact committed candidate checkout, selecting the authorized physical GPU:
CUDA_VISIBLE_DEVICES=<physical-GPU> PYTHONDONTWRITEBYTECODE=1 python tests/validation/run_uniform_les_flow.py --root "$PWD" --experiment dev8403956 --variant corrected --run-gpu --device 0 --output <new-evidence-directory>/corrected.json
Retain all source hashes, exact commit/import/device identity, manifest hash,
three READY/reset records, complete result JSON, stdout/stderr and actual exit.
Require all frozen gates, DONE passed=true and exit 0. Compare with historical
corrected evidence without relabeling it as current evidence. The old-vs-new
normalization discrimination already exists; no new baseline GPU run is needed
unless review finds a changed exercised uniform solver path.
No GPU result is claimed by this integration preparation. This does not expand
LES qualification to MRT, forcing, AMR LES or three-dimensional turbulent flows.
@@ -0,0 +1,70 @@
# Minimal Smagorinsky normalization integration candidate
This local candidate changes only the two closed-form D2Q9/D3Q19 coefficients
from 18 to 18 sqrt(2), plus their convention comments. The intended convention
is |S| = sqrt(2 S:S), with self-consistent effective tau. No Cs default,
iterative formula/cap/iteration count, collision dispatch, AMR guard, inlet,
channel-parity rule, or unrelated source file changes.
The resulting turbulence_smag.cuh SHA256 is
`a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa`,
exactly the independently reviewed temporary correction used in the paired
scalar and end-to-end flow experiments. Its uncorrected baseline remains at
validation checkpoint `9805383f9c50d867e8555471baf5dbda8333a652`.
## Evidence and limits
The separately audited scalar-helper GPU diagnostic on the cd3 source campaign
covered 48 cases per variant. All 32 paired iterative results were bit-identical;
maximum patched closed-form error was approximately 1.19e-7, versus 0.06454 for
the baseline against the same standard convention. This is scalar evidence,
not full-collision or turbulence qualification.
The independently reviewed wall-bounded, x-periodic SRT/shifted-FP32/double-buffer
shear experiment distinguished baseline from corrected source under identical
predeclared criteria. Baseline Cs=1 SGS decrement errors at the last two
checkpoints were approximately 28.50% and 28.05%; corrected errors were 0.0628%
and 0.00612%. Corrected maximum Cs=1 profile error was approximately 0.11344%,
and the Cs=0 observations were identical between variants. Cs=1 is an amplified
normalization diagnostic, not a recommended physical coefficient. These data
qualify this bounded normalization comparison, not general turbulence accuracy.
The current direct-header host regression uses the existing independent
Hermite tensor construction and quadratic bisection oracle. It covers 120
parameter subcases across D2/D3, Cs=0/.16, rho=.7/1/1.3, tau0=.56/.8,
equilibrium, both signs of shear and trace-free extension, including a 3D axis.
It also checks converged iterative agreement, Cs=0 omega guard bounds, and
explicitly proves that removing sqrt(2) in temporary headers fails the oracle.
No stored candidate header or full solver fixture is added.
MRT full-trace/bulk coupling, forcing/Guo correction, other streaming/storage
paths, three-dimensional turbulent flows, and AMR LES remain uncertified.
Full-stress closed form and deviatoric/capped iteration intentionally need not
agree on isotropic or cap-active stresses. Existing guards remain unchanged.
## Run and retain provenance
From this candidate checkout:
PYTHONPATH=src python tests/unit/test_smagorinsky_normalization.py -v
PYTHONPATH=src python tests/unit/test_les_flow_runner.py -v
The flow runner already supports `--variant corrected` against the exact
reviewed helper hash. Its source-pin tests now exercise that current variant;
they still verify that the archived baseline variant rejects the corrected
source and that unrelated source tampering is rejected.
Historical Re100, Jet, matched-DFG and paired-scalar manifests/runners are not
repinned or weakened. Their baseline-source guards are expected to reject this
changed production tree. Run those archived source-specific checks on the clean
9805383 checkpoint; their old GPU evidence does not become evidence for this
candidate. The new direct-current-header test is the regression for corrected
production. Do not describe the historical full suite as passing on this tree.
Before publication or main integration, obtain final independent diff review
and rerun the applicable scalar/flow GPU commands on the exact final candidate
commit after the executor reconnects. Retain new checkout/import/source-hash
receipts and compare against the frozen baseline using separate processes.
A new commit receipt is required even though its corrected header matches the
previously tested temporary correction. No GPU rerun, publication, or main
merge is claimed by this candidate preparation.
@@ -1,57 +0,0 @@
# Experimental projected G explosion
`FixedPatchStepper(..., explosion="projected_linear")` changes only the existing
G-to-G explosion reconstruction. The omitted/default `"constant"` selector retains
the same kernel, launch arguments and device allocations. Projected mode adds two
compact tables per adjacent pair; recursion, collision, BFL, restriction, buffer
ownership and synchronized publication are unchanged. `remap_child` preserves it.
Runtime eligibility is periodic-root, fully blocked D2Q9/SRT/FP32, body/Jet-free,
without LES; the fixed-patch backend has no forcing interface. Shifted and unshifted
storage use the same reconstruction. One generic builder handles scalar windows,
unique union rows and all adjacent hierarchy pairs without depth/rectangle caps.
Donors are original explosion donors intersected with owned, uncovered ACTIVE
cells. An axis uses centered differences only if both neighbors are legal;
otherwise both addresses repeat the center. Nested layout periodicity is explicit.
No covered cells, holes, inactive cells or stale halo copies enter the stencil.
The 33-line kernel is unchanged from our `519473a` experiment and retains its
mathematical attribution. Host tests cover union decomposition, concavity, narrow
gaps, grazing packet paths, offsets, sparse/nested owner addresses, three/four-level
source timing, unchanged constant dispatch, remapping and shifted/unshifted actual
kernel arithmetic compiled as host C++. These are not CUDA or flow validation.
## Reviewed bounded evidence
The `1aa8110` CUDA gate, with production exactly `a135252`, passed exact-dev
omitted/default and explicit-constant equivalence, per-population reconstruction,
scalar/union/four-level scheduling, alternate layouts, and changed remapping in
both storage modes. Operator maximum error was 2.98023224e-8; the eight-step
concave replay maximum was 1.58037614e-7. Whole-step mass/momentum remain
finite diagnostics, not an FP32 conservation certificate. Reviewed evidence:
`7aa66ff0542175092753ea099f40b903ece0527e`.
The fresh four-case `2cf9853` N128/256 Taylor–Green repeat retained all `11901a7`
settings and gates. All 20 main scalar rows and 260 early-pressure samples matched
the older receipts exactly; this is not field-array equality. Projected worst
velocity was 0.001995299260844 versus 0.002 (only 0.235% margin), pressure
0.012157174904932 and energy 0.001184362645162. Constant velocity
0.004418076615223 failed its gate, so the aggregate verdict remains false.
The projected early sampled pressure peak was 0.021621453573334; it remains a
descriptive sampled maximum. Reviewed evidence: `90849a55cad4a0da9b72747da7bfe6eb75962a44`.
The old N64 projected failures remain relevant. These results support only this
bounded plumbing gate and short periodic Re100 smooth-decay experiment. They do
not establish union/multilevel physics accuracy, convergence, long-time stability,
body/Jet, boundary, forcing, MRT or LES suitability. Constant remains the default.
## Separate LES source profile
The two older LES source manifests and the historical default are unchanged and
correctly reject this different source tree. The existing LES runner now offers
`--experiment a135252`, pinning all 101 actual committed corrected source files.
Its explicit baseline-header override describes only a synthetic isolated reverse
control, never the committed a135252 header. The unchanged corrected-only uniform
LES regression (Cs=0/.16/1) is the remaining integration gate; no LES result is
inherited from the projected evidence. The separate historical jet-CV `channel.py`
pin mismatch predates this port and is not addressed here.
-34
View File
@@ -1,34 +0,0 @@
# MRT reuse in blocked/refined stepping (candidate)
Base `9805383`. Opt-in by `LBMConfig(collision="MRT")`; SRT remains the default and its
kernel statements, including the shifted retained-delta/direct-momentum path, are unchanged
under the alternate compile branch.
## What changes
- `blocks.cu` (`BlockStep`), `step/block_channel.cu` (`BlockChannelStep`) and `refinement.cu`
(`patch_collide`) gain a compile-time `COLLISION_MODEL == 2` branch that repeats the uniform
`collide_dispatch` sequence with zero forcing: `compute_rho_u`, omega clamp to
`[OMEGA_COLLISION_MIN, OMEGA_COLLISION_MAX]`, `collide_mrt_no_force`.
- Kernel and host guards accept SRT or MRT only. TRT, LES, non-FP32 storage, non-double-buffer
streaming, and shifted dense/mixed refinement remain rejected.
- Transfers, subcycling, BFL, raw exchange ledgers and channel closures are untouched. The
existing pressure-NEQ outlet selects its MRT branch (unknowns-only NEQ extrapolation) from
`COLLISION_MODEL`, so MRT channel parity must use uniform MRT as reference.
## Contracts (existing operator, not redefined here)
- Shifted storage uses the existing MRT representation (equilibrium moments from `rho - 1`),
not the SRT retained-delta/direct-momentum precision. Mass and momentum are conserved
moments (relaxation rate 0).
- Kinetic rates are the operator's fixed `s_e = s_eps = s_q = 1.2` on every level; the shear
rate is the existing per-level omega. Under acoustic 2:1 scaling the physical bulk viscosity
therefore halves on each finer level. No rate rescaling is applied.
- No LES, no external forcing (`d_params` force fields stay zero).
- Shifted Jets stay SRT-only: `body/jets.py` keeps its existing guard. This extension does not
provide shifted MRT Jets.
- `PeriodicBlockStepper` (uniform blocked stepper) remains SRT-only.
## Evidence status
CPU: `tests/unit/test_refinement_mrt.py` (host guards, source contract). GPU:
`tests/validation/multigrid/test_refinement_mrt_gpu.py` (uniform-MRT channel parity, AMR MRT
smoke) and unchanged SRT GPU tests. Strict parity may be subject to cross-kernel FMA
contraction differences already documented for SRT. Not a DFG/benchmark validation.
+1 -1
View File
@@ -1,6 +1,6 @@
# Blocked AMR root channel increment
Opt in with `FixedPatchStepper(..., coarse_boundary="channel", refinements=[bounds], level_blocks=[(by,bx), (fy,fx)])`. Every level must be blocked. The supported boundary is the existing uniform D2Q9/SRT/FP32 double-buffer channel: local Zou-He west inlet, pressure-NEQ east outlet, stationary halfway bounce-back y walls. Uniform and parabolic inlet profiles and shifted/unshifted storage are supported; the existing configuration guards still reject MRT/LES and incompatible storage. This does not change the external DFG MRT reference.
Opt in with `FixedPatchStepper(..., coarse_boundary="channel", base_block=(by,bx), fine_block=(fy,fx))`, or the existing level-list form. Every level must be blocked. The supported boundary is the existing uniform D2Q9/SRT/FP32 double-buffer channel: local Zou-He west inlet, pressure-NEQ east outlet, stationary halfway bounce-back y walls. Uniform and parabolic inlet profiles and shifted/unshifted storage are supported; the existing configuration guards still reject MRT/LES and incompatible storage. This does not change the external DFG MRT reference.
`step/block_channel.cu` is a small root-only kernel. Fine kernels, transfer kernels and recursive hop scheduling are unchanged. Global coordinates and the global profile height/velocity are separate from block-local storage strides. Corner ownership and collision phases match `OneStep`:
+21 -33
View File
@@ -1,26 +1,23 @@
# Fixed three-level rectangle unions
# Fixed three-level separated patches
This page describes three-level `FixedPatchStepper` setups built from the
`refinements` level list, with rectangle unions on either adjacent pair. The
same constructor supports arbitrary depth; three or more levels require
all-blocked storage and bodies/Jets only on the finest level. This is not
general AMR: there is no per-region timestep, moving geometry, automatic
dynamic refinement or L1 body support in the three-level form.
`FixedPatchStepper` supports exactly three static, blocked levels with separated
rectangle sets on either adjacent pair. This is not general AMR: there is no
per-region timestep, moving geometry, dynamic refinement or L1 body support.
All regions share one current/spare buffer pair per level. The existing schedule
advances L0 once, L1 twice and L2 four times per base step, with aggregate transfers.
No CUDA kernel changes are involved.
## Explicit coordinate frames
For a single scalar parent patch, pass a scalar rectangle as `refinements[1]`:
`(x0,y0,x1,y1)` in half-open integer indices of the allocated L1 window,
including its ghost band.
Keep the existing scalar `child=(x0,y0,x1,y1)` for a single scalar parent patch.
Its coordinates retain their original meaning: half-open integer indices in the
allocated L1 window, including its ghost band.
A rectangle list as `refinements[1]` accepts a scalar or a set parent and
interprets every rectangle in the actual allocated L1 array, including ghost
bands and holes. It never infers region-local coordinates. The physical root
cell edge corresponding to an L1 index `(x,y)` is
`parent.fine_origin + (x,y)/2`:
The new keyword `child_rectangles_in_parent=[...]` is mutually exclusive with
`child`. It accepts a scalar or a set parent and interprets every rectangle in
the actual allocated L1 array, including ghost bands and holes. It never infers
region-local coordinates. The physical root cell edge corresponding to an L1
index `(x,y)` is `parent.fine_origin + (x,y)/2`:
- A scalar L1 parent has a shifted window origin.
- A set L1 parent has origin `(0,0)` and public shape `(2*ny,2*nx)`.
@@ -33,23 +30,20 @@ are physically equivalent to parent set `[(4,4,12,12)]` with child set
`[(8,8,16,16)]`. Reusing the same numbers after changing the parent frame can
move the physical region.
Both adjacent pairs are built from their unions, without a fixed rectangle-gap
rule. Adjacent, overlapping and concave unions are accepted only when their
actual transfer dependencies close. The whole child union must preserve
inherited ACTIVE coverage, uncovered ACTIVE donors/receivers, surviving pulls, and the upstream
Both adjacent pairs require a Chebyshev gap of at least four in their own
parent-cell units. The whole child union must preserve inherited ACTIVE
coverage, uncovered ACTIVE donors/receivers, surviving pulls, and the upstream
restriction footprint. Sparse holes remain unowned; inherited INNER remains
stream-only and OUTER remains invalid. Block sizes must divide their allocated
level grids. Sparse storage owns every grid-aligned tile intersecting the union
plus its four-cell ghost band; rectangle and support edges need not align with
tile edges.
stream-only and OUTER remains invalid. Block sizes must align with every
support edge, including its four-cell ghost band.
## Example
```python
run = FixedPatchStepper(
cfg, context=ctx,
refinements=[[(4, 4, 20, 28), (24, 4, 44, 28)], [(16, 16, 32, 40), (56, 16, 80, 40)]],
level_blocks=[(4, 4)] * 3,
cfg, [(4, 4, 20, 28), (24, 4, 44, 28)], context=ctx,
base_block=(4, 4), fine_block=(4, 4), child_block=(4, 4),
child_rectangles_in_parent=[(16, 16, 32, 40), (56, 16, 80, 40)],
)
```
@@ -57,7 +51,7 @@ This example uses a 48x32 root. `initialize` and `download` use three Q-major
dense arrays. Public arrays and masks still scale with the full allocated
extent; sparse device buffers do not remove that host-memory limit.
Stationary whole-circle L2 bodies and Jets (`level_objects[2]`, `level_jets[2]`) use root edge
Stationary whole-circle `child_objects` and `child_jets` use root edge
coordinates and a single L2 boundary object. Body IDs are physical object-list
positions, not region IDs or caller `obj_id`. Complete boundary stencils must
avoid the transfer footprint. Change a child Jet with
@@ -71,12 +65,6 @@ CPU geometry, table, pre-CUDA rejection, schedule and cleanup tests:
PYTHONPATH=src python -m unittest discover -s tests/unit -p 'test_refinement*.py' -v
```
`tests/unit/test_union_refinement.py` covers union dependencies and tile
ownership; `tests/unit/test_hierarchy.py` covers composed geometry through four
levels. `tests/unit/test_raw_exchange.py` checks the four-level host schedule
with CUDA mocks. These establish topology/control-flow coverage, not numerical
qualification at every depth or interface shape.
Explicit GPU gates are in
`tests/validation/multigrid/test_refinement_three_level_multipatch_gpu.py`.
They cover physical-frame equivalence, twins, asymmetric refinement, multiple
-87
View File
@@ -1,87 +0,0 @@
# Supported settings: 2D Jet + fixed multilevel refinement (candidate c95a7ba)
Every entry names the guard that enforces it (file:line at c95a7ba) and, where one
exists, an existing test that exercises it. A setting not listed here is not
claimed. Observations at the end are tied to the exact case in which they were
measured; they are not general bounds.
## Lattice, storage, collision
| Setting | Supported | Guard |
|---|---|---|
| Dimension / lattice | 2D D2Q9 only (3D refinement is future work) | `lbm/refinement.py:65`, `lbm/kernels/blocks.cu:25`, `lbm/kernels/refinement.cu:26` |
| Precision / streaming | FP32 storage, double buffer | same guards |
| Collision on refinement levels | SRT or MRT; TRT and LES rejected | `lbm/refinement.py:65` (re-checked for fine boundaries at `lbm/refinement.py:457`) |
| Shifted (ddf_shifting) storage | only when every level is blocked; dense or mixed rejected | `lbm/refinement_stepper.py:198`, `:469`; `lbm/kernels/refinement.cu:26` (dense patch kernel unshifted only) |
| Fine relaxation | 1/tau_fine must lie inside the configured omega guards | `lbm/refinement.py:80` |
| Uniform blocked backend `PeriodicBlockStepper` | doubly periodic, SRT only, stationary passive circles | `lbm/block_stepper.py:84`, `:91`, `:45`-`:67` |
Tests: `tests/unit/test_refinement_mrt.py`, `tests/validation/multigrid/test_refinement_mrt_gpu.py`,
`tests/unit/test_shifted_storage.py`, `tests/unit/test_shifted_block_operators.py`.
## Refinement hierarchy (`FixedPatchStepper(cfg, *, context, refinements, ...)`)
| Setting | Supported | Guard |
|---|---|---|
| Depth | at least one refinement level; one entry per level in `level_blocks`/`level_objects`/`level_jets` | `lbm/refinement_stepper.py:72`, `:75` |
| Single L1 rectangle | two-coarse-cell margin to the domain edge | `lbm/refinement.py:73` |
| Several L1 rectangles (union) | require `level_blocks[0]` and `level_blocks[1]`; transfers must close (dependency checks, no fixed gap rule) | `lbm/refinement_stepper.py:84`; `lbm/refinement.py:283`-`:295` |
| Nested levels | depth >= 3 must be all blocked; a blocked level requires blocked levels below; a scalar child needs a scalar parent | `lbm/refinement_stepper.py:82`, `:87`; `lbm/hierarchy.py:54` |
| Nested transfer support | child only over inherited ACTIVE cells, donors/receivers uncovered ACTIVE, no child over the parent restriction footprint | `lbm/refinement.py:391`-`:407` |
| Tile shape | positive `(by, bx)`; the level allocation must be divisible by the tile (no partial tiles yet) | `lbm/refinement_stepper.py:78`, `lbm/refinement.py:246` (shape); `lbm/blocks.py:134` (divisibility) |
| Coarse boundary | `periodic`, `slot_reservoir` or `channel` | `lbm/refinement_stepper.py:177` |
| `slot_reservoir` | on-site velocity bottom slot, reservoir top, velocity 0, 2 <= slot width < nx, patch rows 4..ny-4 | `lbm/refinement_stepper.py:205`, `:212`; `lbm/refinement.py:546` |
| `channel` | all levels blocked; Zou-He inlet, pressure-NEQ outlet, stationary bounce-back walls; transfers away from the closure | `lbm/channel.py:22`, `:26`, `:43`, `:54` |
| Explosion | `constant` (default); `projected_linear` only periodic, all-blocked SRT, no bodies/Jets, no LES (experimental) | `lbm/refinement_stepper.py:188`, `:193` |
| L2 runtime remap (`remap_child`) | periodic three-level L2 sets only, no L0/L1 bodies, identical pinned boundary | `lbm/refinement_stepper.py:557`, `:567`-`:581` |
Tests: `tests/unit/test_refinement_three_level_multipatch.py`, `tests/validation/multigrid/test_refinement_multipatch.py`,
`test_refinement_three_level.py`, `test_refinement_three_level_multipatch_gpu.py`, `test_refinement_blocked_base.py`,
`test_refinement_blocked_fine.py`, `test_channel_root_gpu.py`, `tests/unit/test_channel_root.py`,
`tests/unit/test_projected_pair_stepper.py`, `test_runtime_child_remap_gpu.py`, `test_pinned_body_remap_gpu.py`.
## Bodies and Jets on refined levels
| Setting | Supported | Guard |
|---|---|---|
| Bodies | stationary circles without sensor/force-region roles, entirely inside the patch union, fully resolved, disjoint, stencil away from the two-hop transfer footprint | `lbm/refinement.py:474`-`:542` |
| Level | not on L0; only on the finest level of a nested hierarchy | `lbm/refinement_stepper.py:80`, `:89` |
| Jet condition | velocity only; `pressure` is reserved and raises NotImplementedError | `body/jets.py:148`, `:150` |
| Jet geometry | each Jet selects resolved links of exactly one stationary body; no Jet/Jet or Jet/SurfaceActuation overlap; abs(amplitude) < 1 lattice | `body/jets.py:155`, `:175`, `:177`, `:255`, `:261`, `:263` |
| Jets with shifted storage | only in the all-blocked stepper with SRT and no LES | `body/jets.py:249` |
| SurfaceActuation | unshifted storage only | `body/objects.py:155` |
| Raw boundary exchange (force) | blocked body levels only; the window ledger requires `observe_raw_exchange` | `lbm/refinement_stepper.py:264`, `:269` |
| Checkpoint with Jets | refused | `simulation.py:687` |
Raw exchange is the cut-link momentum reaction (wall + aperture), not a validated
nozzle thrust or stress load. Jet amplitude is a lattice velocity, not a flow rate.
Tests: `tests/unit/test_jets.py`, `test_jets_review.py`, `test_shifted_jets.py`, `test_raw_exchange.py`,
`tests/validation/multigrid/test_refinement_jets.py`, `test_shifted_jets_gpu.py`, `test_raw_exchange_gpu.py`,
`test_exchange_window.py`, `test_fine_surface_gpu.py`.
## Observations from specific tested cases (not general bounds)
- SRT channel root vs uniform OneStep, strict-parity test
`tests/validation/multigrid/test_channel_root_gpu.py::ChannelRootGPU::test_uniform_matched_root`:
in one of its 18 sub-cases (2 storage modes x 3 profile/clamp/damp settings x 3 blocks; which
one is not recorded), 1 of the 4320 compared values differs, max abs 2.33e-10, max rel 1.77e-7
(about 2 ULP). The test remains failed; its cause and any general bound are unresolved.
- MRT root parity test
`tests/validation/multigrid/test_refinement_mrt_gpu.py::RefinementMRTGPU::test_uniform_matched_mrt_root`
(12 sub-cases: 2 storage modes x 2 profiles x 3 blocks, default outlet settings) passed bitwise
in the Horsea focused GPU run at K1 7813334 (2026-10-21).
- Periodic Taylor-Green Re 100, measured at 36ffc8b1 (V3 matched pair, N128 depth 2):
every per-sample screen passed; the constant explosion's aggregate worst velocity L2,
0.004418, exceeds its preregistered 0.002 budget, while projected_linear passed that budget.
- Curved-wall pressure (pilot harness, exact-q analytic annulus, tau 0.8): the coarse swirl r1
case exceeded its terminal density-gain cap (1.105e-4 vs 1e-4), so the frozen 9-case matrix
is NOT_ESTABLISHED. No validated fix exists. This is a pilot harness, not production code.
- DFG 2D-1 Re 20 (V4, SRT, t* 60, pressure-NEQ outlet and local Zou-He inlet): refined and uniform
runs were NOT_STEADY under the frozen steady rule, so no interval verdict was given.
## Not supported in this release
3D, dynamic/automatic AMR (tagging, time-varying hierarchies beyond `remap_child`),
moving bodies on refined levels, pressure Jets, Jet checkpoints, LES or TRT on refined
levels, partial (non-dividing) tiles.
+25 -37
View File
@@ -1,15 +1,11 @@
# Synchronized leaf G remap and manual continuation
This is a CPU state-transfer component, **not a runtime adaptive solver**.
It changes only an L2 rectangle union in a three-level periodic hierarchy.
The body-free case is shown first; stationary L2 bodies/Jets follow the bounded
support contract below. L0/L1 topology, relaxation parameters, direction order,
It changes only a separated L2 rectangle set in a three-level, body-free,
periodic hierarchy. L0/L1 topology, relaxation parameters, direction order,
physical frame and completed time remain unchanged. Existing geometry checks
validate child support/nesting and actual transfer dependencies, without a fixed
rectangle-gap rule. The three-level remap limit is unchanged by the stepper's
arbitrary-depth level-list support. The CPU helper itself adds no CUDA,
scheduler, allocator, boundary handling, collision or ghost refill. A bounded
runtime adapter is described below.
validate child support/nesting and separation. The CPU helper itself adds no CUDA, scheduler, allocator, boundary handling,
collision or ghost refill. A bounded runtime adapter is described below.
```python
from CelerisLab.lbm.refinement import FixedPatchSet2D
@@ -33,13 +29,11 @@ origin is retained. Physical lower edges are `origin + spacing*(x,y)` on L0,
`origin + spacing*(parent.fine_origin + (x,y)/4)` on L2.
Factory options `coarse_boundary` and `objects` must declare `"periodic"` and
an empty collection; L2 bodies use `child_objects` as described below.
LBMConfig's channel settings do not select this operator's boundary mode.
Geometry metadata is factory-created;
an empty body collection (all levels). LBMConfig's channel settings do not
select this operator's boundary mode. Geometry metadata is factory-created;
manual mutation/forging of metadata is unsupported. Caller certifies the
actual state matches the declared boundary and has no forcing, that all device
work and old readers are quiescent, and that inputs are the **current owned-leaf
postcollision G**.
actual state has no bodies/forcing, that all device work and old readers are
quiescent, and that inputs are the **current owned-leaf postcollision G**.
Count/phase stamps check consistency, not freshness or active-buffer identity.
`current_g` is three NumPy Q-major arrays `(9,ny,nx)`. For blocked inputs,
@@ -79,10 +73,9 @@ PYTHONPATH=src python -m unittest discover -s tests/unit -p 'test_synchronized_r
`replacement = current.remap_child(rectangles)` returns a new ready
`FixedPatchStepper` at the **same absolute step_count**. It supports only
periodic three-level runs with no L0/L1 bodies and an existing L2
`FixedPatchSet2D`; stationary L2 bodies/Jets follow the support contract below.
Scalar or union L1 parents are supported; scalar `child=` is deliberately
rejected. L0/L1, relaxation and block sizes stay fixed.
body-free periodic three-level runs whose existing L2 uses
`child_rectangles_in_parent`. Scalar or union L1 parents are supported; scalar
`child=` is deliberately rejected. L0/L1, relaxation and block sizes stay fixed.
No tagger, scheduler, in-place replacement, or general rollback is provided.
The method holds the stepper lock, validates geometry, synchronizes its stream,
@@ -120,14 +113,13 @@ Per-population conservation is not an advancement target under collision.
### Bounded pinned-body extension
`replacement = current.remap_child(rectangles, child_objects=original_objects)`
also accepts stationary Circles inside the L2 union. Each body's **physical
boundary, complete BFL support and current per-link velocity must remain
identical**. Rectangles are not pinned: they may be split, merged or resized,
and other fluid coverage may change, including within the same connected union,
provided that physical support is unchanged. Existing nesting, union transfer
dependencies, full-circle containment, reverse cut-link
coverage and two-hop BFL transfer-footprint checks still apply. This is manual
remapping, not adaptive body tracking or runtime body editing.
also accepts stationary Circles in separated L2 rectangles. Every body’s
**complete containing rectangle must remain identical**. Several bodies may
share one pinned rectangle; there need not be any fluid-only rectangles.
Separated fluid-only rectangles may be added, removed or moved. Existing
nesting, four-cell gap, full-circle containment, reverse cut-link coverage and two-hop BFL
transfer-footprint checks still apply. This is manual remapping, not adaptive
body tracking or runtime body editing.
Supply all original root edge-frame definitions in their construction order.
Do not pass `current.child_boundary.objects`: those are projected fine-local copies.
@@ -136,8 +128,8 @@ state, remapping reprojects the supplied definition onto the old topology and
compares exact ordered circle centers/radii, flags, physical cut links, body
IDs, q, lever arms, hits, BFL support,
legacy SurfaceActuation specification and current per-link velocity against
the existing boundary. Omission, changed geometry/actuation or changed physical
boundary/BFL support fail without device reads or allocations; reordered bodies fail
the existing boundary. Omission, changed geometry/actuation and changed body
rectangle fail without device reads or allocations; reordered bodies fail
too. Root object IDs are still assigned by list position, as in the constructor.
Jets are taken from the live L2 boundary, including the most recent successful
@@ -149,16 +141,12 @@ boundary and velocities must match exactly.
typed `FineBoundary2D`; it derives L2 fluid leaves as ACTIVE minus SOLID. No
caller mask is accepted. Solids, ghosts and holes are never donors and remain
NaN, physical-fluid overlap is exact, and exposed L1 still restricts all four
old fluid children. The helper checks old/new physical boundary and velocity
equality too. Its immutable `pinned_rectangles` is legacy containment metadata,
not a remap eligibility constraint; it may be empty for a seam-spanning body.
Scalar-parent window offsets remain part of the coordinate conversion.
old fluid children. The helper checks the old/new body pins and boundary too.
Its immutable `pinned_rectangles` is a canonical, deduplicated tuple, empty for body-free layouts; rectangle input
order does not affect it. Scalar-parent window offsets remain part of the
coordinate conversion.
Focused CPU mocks: `tests/unit/test_pinned_body_remap.py`.
`tests/unit/test_union_body_support.py` also checks seam-spanning circles,
rectangle repartition/resize, unchanged physical support and live-Jet
preservation. These host checks do not establish GPU evolution accuracy.
GPU declarations:
Focused CPU mocks: `tests/unit/test_pinned_body_remap.py`. GPU declarations:
`tests/validation/multigrid/test_pinned_body_remap_gpu.py`, covering no-op
next-step bitwise identity, moved-fluid A→B→A, current-amplitude preservation,
physical boundary/velocity equality, source independence and absolute counts.
-53
View File
@@ -1,53 +0,0 @@
# Bounded 2D implementation and validation scope
This inventory starts from dev `8403956a9afc0b0bb89265cec374392013d89016` and
integrates only the reviewed LES normalization production correction from
`95cb6d2b9f43c1bdd46cbf27f03f0dfc0877cfb0`. The merged production bytes match that
tested candidate exactly; the scope-test changes are retained from `53fad767`.
GPU receipts remain associated with their exact tested commits, not relabeled as
a new merged-commit run. Historical source identities and gates remain unchanged.
## Implementation boundaries
- Uniform D2Q9 supports SRT, TRT and MRT. An available collision selector does not
qualify every wall, inlet/outlet, forcing, storage or LES combination.
- Fixed refinement supports SRT/MRT, FP32 and double-buffer streaming; TRT and LES
are rejected. Shifted refinement requires all-blocked storage. Refined channel
boundaries have narrower configuration guards than uniform boundaries.
- Uniform checkpoints support compatible same-method continuation. Changing
collision or LES parameters is a different evolution, not exact continuation.
Jet checkpoints and refined checkpoints are unsupported; synchronized remapping
is not persistent restart.
- Keep these conclusions 2D. Local D3Q19 operator tests do not establish a supported
3D Simulation runtime.
## Evidence and remaining gaps
| Area | Available evidence | Boundary of the conclusion |
|---|---|---|
| MRT cylinder flow and observation windows | Full matched uniform/refined MRT DFG and the window-ledger campaign passed their frozen periodicity, reference and matched-comparison gates. The window campaign used `185a919`; its MRT/no-LES numerical path remains unchanged. Current dev also contains independently scoped test/documentation changes and the LES correction, which do not extend MRT qualification. | This does not qualify other collision/boundary combinations or every refinement interface. Timing improvements are not additional accuracy evidence. |
| Uniform forced channel | Six Poiseuille GPU cases on `8403956`: SRT/TRT/MRT at wall-normal resolutions H16/H32 passed. | This is the tested analytic channel configuration, not open-boundary or all-layout qualification. |
| Uniform restart | Ten SRT/MRT checkpoint GPU tests passed on `8403956`; the separate representative TRT same-method double-buffer continuation GPU case also passed (one test). | This covers the tested same-method continuation configurations. No LES, Jet or refined checkpoint claim follows. |
| Taylor–Green refinement | Independent topology checks are available. The actual-rate phase GPU regression passed one test across two layouts, with errors 1.584e-7 and 4.500e-9. | The physical interface-accuracy gap remains open. Passing topology and phase checks does not establish convergence or close that accuracy gate. |
| Porous Couette and annulus | Prior campaign evidence exists and must remain associated with its original setup and source. | The reconstructed annulus is a validation helper, not qualification of a public boundary-condition API. Do not infer absence of evidence from a missing current rerun. |
| LES normalization | The reviewed correction is integrated. Exact `95cb6d2` GPU validation passed all nine observations and frozen gates, with three warm-up/reset records, verified 101-source/import identity, DONE and exit 0. Last two SGS decrement relative errors were 6.276e-4 and 6.124e-5 against the unchanged 0.15 gate. | Scope is unforced uniform D2Q9/SRT, shifted FP32, double-buffer streaming and the tested wall-bounded x-periodic shear. No TRT/MRT LES, forcing, EsoPull, objects, refinement or 3D turbulence qualification follows. |
Historical uniform SRT cylinder-startup and inlet-rounding findings remain separate
from the passed MRT DFG result. Raw exchange/window consistency is a measurement
contract, not a general stress or force-accuracy certification. Existing source-pinned
runners may intentionally reject a newer checkout; never silently repin old receipts.
## Remaining work
The related host checks passed (67 checks); they do not replace the GPU or
physical-accuracy evidence above. Preserve the existing SRT checkpoint cases,
historical LES manifests and all frozen numerical criteria.
Continue the TG accuracy investigation using its actual fine-level rates and
the established analytic convention. Keep the physical accuracy gate open until
the relevant comparison passes. Review this integration independently before
publication; no new merged-commit GPU run is claimed here.
No repeat of the accepted full MRT DFG or Cartesian expansion across unsupported
combinations is required by this document. Main integration remains a separate
review of complete, bounded feature scope.
-49
View File
@@ -1,49 +0,0 @@
# DFG 2D-1 (Re 20) against the official reference intervals
`tests/validation/run_dfg_re20.py` runs the steady DFG 2D-1 case on the refined
channel root (D40 root, D80 wide fine patch) and on the uniform D80 grid. Geometry,
inflow, initial state, layout and observation are those of the pinned Re100 runner
(`run_refined_dfg.py`, `re100_helpers.py`, both unchanged); only the lattice
viscosity is set for Re 20 (root tau 0.56, fine 0.62, uniform 0.62, lattice speed 0.01).
Label: "compared against the official reference bands", not a full benchmark
certification: the outlet is pressure-NEQ (`neq_extrap`), not do-nothing, and the
inlet is local Zou-He.
## Quantities
- Cd = 2 Fx / (rho U^2 D), Cl = 2 Fy / (rho U^2 D), interval means over each sample.
- p_diff = p(0.15, 0.2) - p(0.25, 0.2), linear extrapolation to the exact surface
points (`re100_helpers.pressure_probes`). The helper returns dp / (rho U^2) in
scaled units; p_diff = dp * 0.04 for U = 0.2 m/s, rho = 1. Both are reported.
- Official intervals (Schaefer & Turek 1996): Cd [5.57, 5.59], Cl [0.0104, 0.0110],
p_diff [0.1172, 0.1176].
## Bounds and steady rule (frozen before any run)
- End time t* = 60 (30 s physical): 240,000 refined root steps, 480,000 uniform steps.
- One sample every t* = 0.5 (0.25 s) at the end of a synchronized root step; Cd and
Cl are interval means. Trailing window t* 50..60 inclusive (21 samples).
- Steady rule (our engineering choice, not official): in the window, for each of Cd,
Cl, p_diff, max - min <= 10 % and |least-squares slope x window| <= 2 % of its
official interval width (range 2e-3 / 6e-5 / 4e-5; slope 4e-4 / 1.2e-5 / 8e-6).
Absolute limits, so the small Cl is never a divisor. Never relaxed after a run.
- The verdict uses the window mean; the distance to the nearest interval edge is
reported for each quantity (negative outside).
- Force sign: fluid on cylinder, x along the inflow, y up from the bottom wall; Cd > 0
and the official Cl > 0 (`cd_positive` is reported).
- Not steady at t* = 60: status `NOT_STEADY`, no interval verdict. Health limits
(Mach <= 0.2, rho in [0.5, 1.5]) stop the run with `UNHEALTHY`.
- Steady: `WITHIN_INTERVALS` or `OUTSIDE_INTERVALS`, with the per-quantity verdict.
## Run
```
python tests/validation/run_dfg_re20.py --output <new-dir> --plan
CUDA_VISIBLE_DEVICES=<n> python tests/validation/run_dfg_re20.py --case both \
--collision SRT --output <new-dir> --max-wall-seconds <cap>
```
The uniform D80 control separates method error from refinement error. `--plan` writes `plan.json` only. A GPU run writes `plan.json` and `result.json`
(per case: lattice parameters, every sample, window ranges, status, verdict).
Host contract tests: `tests/unit/test_dfg_re20.py`.
-71
View File
@@ -1,71 +0,0 @@
# Matched MRT DFG pilot preparation
Preparation only: this test-only extension reuses the existing runner and existing MRT
operator. GPU admission awaits reviewed MRT capability results; no pilot/full execution
is implied by CPU preflight. No production, historical SRT manifest, reference data,
periodic gate, geometry, initialization or observation algorithm is changed here.
## Selection and provenance
Use `--collision MRT` for **both** uniform D80 and refined root D40/fine D80. There are
no independently selectable collision models per case. Default remains SRT.
MRT verifies `data/refined_dfg/source_manifest_mrt.json`, pinned to reviewed numerical
source snapshot `3ea131e92c057843034f11a1c574301a2430dfce`, including the shared geometry,
pressure/cycle helpers and official reference. Test-only commits may change HEAD without
changing this numerical source pin. Plan, READY, case summaries and comparison separately
record `checkout_revision` and `checkout_dirty` using existing source-provenance helper;
the runner hash remains separate. Collision is recorded in every receipt, and full-mode
validation rejects missing or different collision labels in plan, either summary or either
READY receipt, in addition to existing source/runner/health/time checks.
The historical SRT manifest remains byte-for-byte unchanged. SRT on the MRT candidate
checkout deliberately fails that source pin; use a matching historical checkout via
`--source` for SRT. No fallback or automatic repinning is permitted. Historical SRT
failure/diagnostic evidence remains attached to its original source and runner hashes;
it cannot authorize an MRT full run. The existing failed-observation step-reporting
undercount is not changed in this narrowly scoped selection patch.
## Frozen physical comparison
All settings remain those documented in `refined_dfg.md`: U_lattice=.01, physical mean
U=1 m/s, D=.1 m, shear ν=.001 m²/s, fixed physical domain/body/wall planes, patch
(40,24,320,140), shifted FP32 double buffers, no LES/forcing/Jets, finest dx=.00125 m,
finest dt=.0000125 s, unchanged half-cell frame conversion and axis-specific phase.
Force interval accumulation, physical leaf mass, pressure interpolation, flux sampling,
safety thresholds, t*=5 pilot and t*=300 cap are unchanged. Existing accuracy/periodicity
criteria are imported unchanged. Both MRT cases are fresh runs; old MRT or SRT results
are not substituted as controls.
Both cases use the **existing MRT pressure-NEQ branch**: only unknown directions q2/6/8
are extrapolated. This differs from SRT's all-nine .5-damped branch; the retained
`srt_neq_damp=.5` configuration value is inactive under MRT. Backflow clamp remains off.
The outlet is still noncanonical relative to exact DFG do-nothing conditions.
The reused MRT operator keeps fixed kinetic rates `s_e=s_eps=s_q=1.2` on all levels;
only the shear rate follows existing per-level viscosity. Physical bulk relaxation
therefore differs between coarse and fine levels. Transfer operators and subcycling
are unchanged. This is the candidate's declared behavior, not a new fully mode-scaled
MRT transfer scheme. Do not tune rates, the patch or gates after seeing outcomes.
Capability smoke/parity gates do not establish DFG accuracy or speedup.
## CPU and admission commands
From this checkout:
```sh
python tests/unit/test_refined_dfg.py
python tests/validation/run_refined_dfg.py --collision MRT --mode plan --output /tmp/new-mrt-dfg-plan
```
After root verifies the candidate MRT root/parity/refinement capability results and admits
an idle device, the paired pilot may use the existing bounds and external watchdog:
```sh
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s <hard-budget>s python tests/validation/run_refined_dfg.py --collision MRT --mode pilot --gpu-gates-approved --max-wall-seconds <soft-budget> --output <new-mrt-pilot-directory>
```
The flag is caller attestation, not evidence. An unhealthy first case prevents the second.
Existing strict pilot receipt validation and calibrated wall-time controls remain; full
mode is not being admitted as part of this preparation. Root must review both healthy
pilot outcomes and separately decide any full-run admission.
-58
View File
@@ -1,58 +0,0 @@
# Refined DFG MRT timing harness (measurement only)
`tests/validation/time_refined_dfg.py` times the matched MRT refined/uniform DFG Re100 setup of
`run_refined_dfg.py` (same configuration, geometry and initial state functions). No numerical,
physics-gate or production change; no force or accuracy result is produced.
| Variant | Loop | Force reads |
|---|---|---|
| A | refined `step(1)` + `read_raw_exchange` per base step | runner-like force-read workload (values discarded) |
| B | refined `step(1)` only, no host reads (`observe_raw_exchange` stays on) | timing-only |
| C | refined single `step(N)` | timing-only; ledger keeps the final base interval only |
| E | uniform `sim.run(<=100)` + `read_force` | runner-like force-read workload (values discarded) |
| E' | uniform single `sim.run(2N)` | timing-only; at most a whole-window average, different summation order |
- No variant retains a valid force history; A/E omit the runner's interval aggregation and
validation, and no variant includes macro/probe/CSV costs. Results are not full benchmark
throughput.
- Status lines: `CONSTRUCTED` (construction/JIT complete) and `WARM_READY` (first warm-up and
health check passed) per case.
- Equal simulated duration: N refined base steps vs 2N uniform D80 steps.
- Every repeat starts from the same fresh state (refined `initialize`, uniform `snapshot/restore`)
and the same warm-up; construction/JIT, warm-up, downloads and parity checks are untimed.
- The actual stepping stream is synchronized explicitly before and after each timed loop; the
wall timer (`time.perf_counter`) is the only source. No CUDA-event span is called kernel time.
- `--max-wall-seconds` is required for GPU runs (outer timeout, checked between variants and
every 100 steps in per-step loops).
- Final populations and NaN masks must be bitwise identical to the first A repeat (A/B/C) or the
first E repeat (E/E'); refined and uniform grids are never compared. Leaf/fluid populations
must be finite. Physical health (finite leaf/fluid macros, rho in [0.5, 1.5], max Mach <= 0.2,
the existing runner limits) is checked after warm-up and after each timed window. Matching NaN
is accepted only as a storage mask; any infinity or level-count mismatch fails parity. All
parity/health checks are untimed.
- Cases run sequentially (uniform, then refined) in one process with startup serialized;
variants are interleaved within each repeat. A-B and B-C are measured comparative
differences under GPU variability, not exact causal decompositions.
Usage (root selects one verified idle GPU):
PYTHONPATH=src python -m unittest tests/unit/test_time_refined_dfg.py
python tests/validation/time_refined_dfg.py --output <new> --plan
CUDA_VISIBLE_DEVICES=<gpu> python tests/validation/time_refined_dfg.py --output <new> --max-wall-seconds <s>
Defaults: 2000 base steps, 200 warm-up base steps, 3 repeats. Profiler capture and the
`observe_raw_exchange=False` variant are deferred until these measurements exist.
## A versus observation-window ledger (`tests/validation/time_window_dfg.py`)
Measurement-only comparison on the same shifted-MRT refined setup: A (per-base `step(1)` +
`read_raw_exchange`, summed per 80-base-step interval), W (`start_exchange_window`, `step(80)`,
one window read) and W1 (window with 80 x `step(1)`, isolating the read pattern from call
batching). Same fresh state, warm-up and simulated duration; populations must be bitwise equal
to the first A repeat and per-interval total impulses within the documented engineering
tolerance, all outside timing. The candidate source is recorded (checkout revision, dirty flag,
file hashes); frozen refined-DFG manifests/results are not used or changed.
PYTHONPATH=src python -m unittest tests/unit/test_time_window_dfg.py
python tests/validation/time_window_dfg.py --output <new> --plan
CUDA_VISIBLE_DEVICES=<gpu> python tests/validation/time_window_dfg.py --output <new> --max-wall-seconds <s>
-117
View File
@@ -1,117 +0,0 @@
# Opt-in observation-window ledger in the DFG runner
This test-only integration adds `--exchange-window` to `run_refined_dfg.py`. It uses the
existing production ledger; no collision, boundary, transfer, physical setup, reference,
periodicity gate or acceptance threshold changes. Default remains the historical
per-base-step observation path. Uniform stepping/force accumulation is unchanged.
For the refined case, each existing observation interval opens a new ledger at completed
base step `begin`, advances exactly the same `n` base intervals, and reads the integrated
root-lattice impulse for `[begin, begin+n]`. The contract requires root units, `window`
kind, `2*n` fine samples, matching endpoints, and fine dt=.5. The existing coefficient
calculation still divides this impulse by `n` and `.5*U_lattice**2*D_root`; force timestamps
remain `(begin+n/2)*U_lattice/D_root`. It does not divide an already averaged force again.
Pressure/macro/physical-leaf mass measurements and safety checks remain at the original
interval end. Pilot interval is 80 root steps; early full-run intervals remain 2000, and
any final shortened interval uses its actual `n`.
The opt-in uses one `step(n)` call and one window read per observation, rather than `n`
Python calls/readbacks. Internal subcycling and per-base synchronization remain owned by
the same backend. Cooperative deadline checks surround that complete interval; unlike
the default path they cannot interrupt between base steps inside `step(n)`. The existing
external hard watchdog is still required. The full-run cadence/cap is documented here,
but this preparation does not admit a full run or a GPU operation.
## Provenance and receipt separation
`source_manifest_window.json` pins the reviewed a233c9032487a6006e950d1e327079b00466c410
snapshot (production ledger origin d930) plus existing helper/reference files. Both SRT
and MRT are supported by this explicit opt-in source pin; collision selection remains
independent and shared by the two cases. Existing SRT and MRT manifests are untouched.
Default mode continues to reject the changed candidate under those historical pins;
use a matching historical `--source` checkout to run its default path. There is no
silent repinning, source fallback, or reuse of historical outputs as candidate evidence.
Plan, READY, summaries, timeout/comparison receipts now record `observation_mode` as
`last_base_step` or `window`, alongside collision and actual checkout provenance. On a
uniform receipt this labels its **paired campaign's refined observation mode**, not a
change to uniform observation. Full-mode pilot validation rejects missing or mismatched
mode labels in the plan, either case summary, and either READY receipt. Source/runner
hash, collision, completeness and health checks remain. Cross-mode pilot reuse is denied.
The earlier short A/W/W1 timing/capability evidence is not a full-run speedup or accuracy
claim. Mathematical impulse integration/units are unchanged; floating-point reduction
order can differ, so bitwise equality of force sums is not promised. Physics acceptance
continues to use the existing frozen gates. Historical failures/results are untouched.
CPU preparation commands, from the repository root:
```sh
python tests/unit/test_refined_dfg.py
PYTHONPATH=src python -m unittest tests.unit.test_raw_exchange_window tests.unit.test_time_window_dfg -v
python tests/validation/run_refined_dfg.py --collision MRT --exchange-window --mode plan --output /tmp/new-window-dfg-plan
```
Root must independently review and admit any execution. An admitted paired pilot uses
the existing watchdog, device selection, `--gpu-gates-approved`, positive
`--max-wall-seconds` and a new output directory, adding only `--exchange-window` to the
selected collision's pilot command. No GPU or remote action was performed for this patch.
## Public Jet diagnostic option
`--collision MRT --exchange-window --jet-scenario zero|fixed|reverse` selects
unshifted FP32, no LES, constant transfer on both existing public backends. Default
`none` and the historical manifests remain unchanged. `zero` registers both Jets at
zero amplitude and retains every existing DFG verdict, including pressure. `fixed`
uses outward radial parabolic sectors at pi/2 and 3pi/2, half-width pi/12, peaks
(+.001,-.001). Both use the existing D40/D80 geometry and U=.01. Sector origins are
(80.5,81) in root edge coordinates and (160,160.5) in uniform node coordinates;
radial extent is [0,1.005R]. `reverse` keeps those peaks through t*=300, updates both
public setters at that completed boundary, and continues to 305 with opposite signs.
Fixed diagnostic windows are 250–260, 260–270, 270–280, 280–290 and 290–300; reversal
adds 300–305. Histories retain exact interval endpoints and root/uniform lattice
impulses. The refined window uses total exchange; uniform uses public `read_force`
batch averages. Driven windows report mean/min/max/RMS Cd/Cl without applying DFG
reference or periodicity gates. Exit zero means healthy execution, not qualification;
`matched_case_pass` remains false. No independent CV or stress-load claim is made.
This opt-in records current source/helper hashes with the existing manifest format,
checkout revision/dirty state and runner hash. Pilot/full must match that snapshot
and the exact scenario. It does not relabel current source as historical evidence.
Use the existing bounded plan/pilot/full commands with the three option flags above;
full requires the matching completed t*=5 paired pilot. `step_seconds` includes force
readback; observation/overhead and total wall time are separate, not kernel throughput.
Add `--observe-cv` to record independent scalar control-volume samples from the
already downloaded public macros. The pilot exercises this path at t*=.02..5;
full records 501 endpoints at 290..300. Each `cv_history.jsonl` row contains actual
root time, preceding interval start/duration/impulse, and two inventories/outward
fluxes ordered [mass,Px,Py]. Exclude the first row's preceding impulse: that row
supplies the initial inventory. Uniform native impulse converts by .5² to root
units; refined ledger impulse is already in root units. Explicit native-aligned
contours differ by .25 root cells in x and are recorded in the plan. Cropping
retains the two-cell face-gradient stencils. No fields or per-link arrays are saved.
`cv_observation_seconds` is the CV portion of observation/overhead time. Helper hash
and the observation flag must match pilot/full. Time integration, temporal
coarsening and any physical assessment remain separate offline work.
## Bounded resolution and placement controls
Defaults remain `--case both --uniform-diameter 80 --placement wide`. Single-case
runs use `--case uniform` or `--case refined` and never report a matched-case pass.
Their own reference/periodicity or driven-diagnostic results remain available.
A single zero-Jet full run exits successfully only when its periodicity and the
unchanged complete DFG reference verdict pass; driven completion is not qualification.
The D160 option requires `--case uniform --uniform-diameter 160` and an explicit
public Jet scenario. U and Jet speeds stay .01 and ±.001; viscosity is .016,
grid 3521×658, center (320,320.5), radius80. CV uses root spacing/time factor .25,
origin (.375,.75), impulse factor .0625, root viscosity .004, and x-face shift .125.
`--case refined --placement compact` selects root bounds (48,48,128,128), fine
168×168 and origin (46,46), with the same D40/fineD80 body/Jet resolution. Its CV
contours are (52,52,112,112) and (50,50,120,120); complete face-gradient stencils
are clear of both solids and the transfer footprint. Compact is refined-only.
CV metadata gives each grid's spacing/origin/contours explicitly. Case, uniform
diameter and placement must match the newly completed pilot in every receipt.
+2 -2
View File
@@ -90,8 +90,8 @@ def main():
# The caller owns the CUDA context; the stepper must be closed first (the
# nested with-statement does that).
with CudaContext(0) as context, FixedPatchStepper(
cfg, context=context, coarse_boundary=args.mode, refinements=[BOUNDS],
level_blocks=[None, fine_block], level_objects=[(), (body,)]) as run:
cfg, BOUNDS, context=context, coarse_boundary=args.mode,
fine_objects=[body], fine_block=fine_block) as run:
p = run.patch
# Rest equilibrium is already postcollision G, so no initial collision is
# needed. Non-leaf entries (covered, ghost, solid) are discarded by initialize.
+2 -2
View File
@@ -148,8 +148,8 @@ def main():
samples=[], status='running', last_synchronized_coarse_step=0)
start, run = time.monotonic(), None
try:
with CudaContext(0) as context, FixedPatchStepper(cfg, context=context,
coarse_boundary='slot_reservoir', refinements=[BOUNDS]) as run:
with CudaContext(0) as context, FixedPatchStepper(cfg, BOUNDS, context=context,
coarse_boundary='slot_reservoir') as run:
p = run.patch
masks = [np.ones(p.coarse_shape, bool), p.fine_kind == ACTIVE]
masks[0][[0, -1]] = False
+1 -1
View File
@@ -48,7 +48,7 @@ def main():
nx=16, ny=16, viscosity=.1, velocity=1e-3, rho=1.0,
lattice_model="D2Q9", collision="SRT", streaming="double_buffer",
store_precision="FP32", ddf_shifting=False, les_enabled=False)
with CudaContext(0) as context, PeriodicPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)]) as run:
with CudaContext(0) as context, PeriodicPatchStepper(cfg, (4, 4, 12, 12), context=context) as run:
p = run.patch
coarse = shear_equilibrium(p.coarse_shape, (0, 0), 1.0, cfg.ny)
fine = shear_equilibrium(p.fine_shape, p.fine_origin, .5, cfg.ny)
+8 -16
View File
@@ -207,30 +207,22 @@ def surface_link_velocity(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
jets=(), hits=None, *, _shifted_blocked=False):
"""Per-link prescribed surface velocity (n, 2) FP32 and aperture mask.
See _surface_links, which also returns the unit-amplitude jet shapes.
"""
return _surface_links(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry, jets, hits,
_shifted_blocked=_shifted_blocked)[:2]
def _surface_links(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
jets=(), hits=None, *, _shifted_blocked=False):
"""(velocity, aperture, jet shape, jet id); the last two are None without jets.
Host-only and storage-neutral: rows follow the given physical cut-link
order. Returns all None when neither an object carries surface
order. Returns (None, None) when neither an object carries surface
actuation nor a jet is given. Jets need true FP64 ``hits`` (n, 2) and may
not share a link with each other or with a legacy aperture.
Shifted storage is privately enabled only by the all-blocked stepper.
"""
enabled = [obj for obj in objects if obj.surface_actuation is not None]
if not enabled and not jets:
return None, None, None, None
from .preprocess.flag_overlay import validate_cut_link_ownership
validate_cut_link_ownership(flags, fluid_idx, dir, cfg.nq)
return None, None
from ..lbm.descriptors import FLUID, SOLID
if np.any((flags[fluid_idx] & FLUID) == 0) or np.any(flags[fluid_idx] & SOLID):
raise ValueError("Surface cut links must have active fluid receivers")
if len(np.unique(fluid_idx.astype(np.uint64)*cfg.nq+dir)) != len(dir):
raise ValueError("Surface boundary requires unique cut-link writers")
surface_velocity = np.zeros((len(dir), 2), np.float32)
surface_aperture = np.zeros(len(dir), bool)
shape = jet_id = None
for obj in enabled:
obj.validate_surface_actuation(cfg)
selected = body_id == obj.obj_id
@@ -263,4 +255,4 @@ def _surface_links(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
raise ValueError("Jet overlaps a SurfaceActuation aperture")
surface_velocity[driven] = jet_link_velocity(jets, shape, jet_id)[driven]
surface_aperture |= driven
return surface_velocity, surface_aperture, shape, jet_id
return surface_velocity, surface_aperture
+12 -17
View File
@@ -27,7 +27,7 @@ import pycuda.driver as cuda
from .objects import SimObject
from .registry import BodyRegistry
from .preprocess.flag_overlay import merge_flag_masks, validate_cut_link_ownership
from .preprocess.flag_overlay import merge_flag_masks
from ..cuda.compiler_v2 import obs_layout
from ..cuda.obs_layout import ObsLayout
from ..lbm.descriptors import (
@@ -369,9 +369,9 @@ class ObjectManager:
Args:
objects: List of SimObject instances to process.
domain_flags: Optional uint16 flags for receiver/donor classification.
domain_flags: Optional uint16 flags for donor-cell classification.
sensor_cell_counts_out: Int array to fill with per-object sensor
cell counts after the full geometry has passed validation.
cell counts. If provided, ``fill(0)`` is called first.
hits_out: List that receives one FP64 (n, 2) array of true hit
points row-aligned with the cut-link columns (for Jets).
@@ -394,8 +394,8 @@ class ObjectManager:
active_fluid = ((domain_flags & FLUID) != 0) & ((domain_flags & SOLID) == 0)
force_owners: set[int] = set()
sensor_counts = (np.zeros_like(sensor_cell_counts_out)
if sensor_cell_counts_out is not None else None)
if sensor_cell_counts_out is not None:
sensor_cell_counts_out.fill(0)
ez = None
if self.cfg and self.cfg.is_d3q19:
@@ -430,9 +430,9 @@ class ObjectManager:
if len(cells) > 0:
s_cells.append(cells)
s_ids.append(ids)
if (sensor_counts is not None
and 0 <= obj.obj_id < sensor_counts.size):
sensor_counts[obj.obj_id] = int(len(cells))
if (sensor_cell_counts_out is not None
and 0 <= obj.obj_id < sensor_cell_counts_out.size):
sensor_cell_counts_out[obj.obj_id] = int(len(cells))
if obj.is_force_region and hasattr(obj, 'get_sensor_list'):
cells, ids = obj.get_sensor_list(self.nx, self.ny, self.nz)
keep = active_fluid[cells]
@@ -461,9 +461,8 @@ class ObjectManager:
r_fr_ids = np.concatenate(fr_ids) if fr_ids else np.zeros(0, dtype=np.int32)
r_hit = np.concatenate(cl_hit) if cl_hit else np.zeros((0, 2))
# Passive walls need the same ownership guarantees as driven walls.
# Overlapping solids are not a supported union-boundary operation.
validate_cut_link_ownership(domain_flags, r_idx, r_dir, self.nq)
if hits_out is not None:
hits_out.append(r_hit)
if self.jets or any(obj.surface_actuation is not None for obj in objects):
# Reject unresolved apertures/ownership before sync or restart mutates state.
from ..lbm.curved_links import CurvedLinkSoA
@@ -471,11 +470,6 @@ class ObjectManager:
links.assign_host(r_idx, r_dir, r_q, r_bid, r_rx, r_ry, r_rz, r_fallback)
links.assign_surface(objects, self.cfg, domain_flags, jets=self.jets, hits=r_hit)
if sensor_cell_counts_out is not None:
sensor_cell_counts_out[:] = sensor_counts
if hits_out is not None:
hits_out.append(r_hit)
return (
r_idx, r_dir, r_q, r_bid,
r_rx, r_ry, r_rz, r_fallback,
@@ -493,6 +487,8 @@ class ObjectManager:
the caller has already prepared and uploaded the final flag field
for this geometry, such as during initialization.
"""
self._telemetry_field = field
next_flags = self.build_flags(field.build_channel_flags()) if rebuild_flags else field.flag
hits = []
@@ -503,7 +499,6 @@ class ObjectManager:
fr_cells, fr_obj_id,
) = self.build_compact_lists(domain_flags=next_flags, hits_out=hits)
self._telemetry_field = field
if rebuild_flags:
field.flag = next_flags
field.upload_flags()
+1 -10
View File
@@ -1,6 +1,6 @@
# CelerisLab/body/preprocess/flag_overlay.py
"""
Flag overlay and cut-link ownership checks against the merged domain.
Flag overlay: merge per-object flag masks onto a clean domain base.
"""
from __future__ import annotations
@@ -12,15 +12,6 @@ import numpy as np
from ...lbm.descriptors import FLUID, SOLID, SENSOR_FLAG, FRC_REGION
def validate_cut_link_ownership(flags, fluid_idx, directions, nq):
"""Reject solid receivers and multiple writers to one population."""
if np.any((flags[fluid_idx] & FLUID) == 0) or np.any(flags[fluid_idx] & SOLID):
raise ValueError("Cut links must have active fluid receivers")
writers = fluid_idx.astype(np.uint64) * nq + directions
if len(np.unique(writers)) != len(directions):
raise ValueError("Boundary requires unique cut-link writers")
def merge_flag_masks(
base: np.ndarray,
masks: List[np.ndarray],
+4 -12
View File
@@ -54,18 +54,14 @@ def _write(path: str, content: str):
def generate_config(cfg: LBMConfig, n_objects: int = 0,
*, build_root: Optional[str] = None, block_root_bc: int = 0) -> str:
*, build_root: Optional[str] = None) -> str:
"""Snapshot kernels and render headers in a fresh private directory.
Returns its path. Pass it explicitly to compile_kernel for interleaved
builds. Legacy generate_config(); compile_kernel() remains context-local.
The caller owns cleanup_build_dir() once the loaded module is no longer
needed for recompilation/debugging. No package files are changed.
block_root_bc specializes a blocked root module; fine/dense builds use 0.
"""
if (isinstance(block_root_bc, bool) or not isinstance(block_root_bc, int)
or block_root_bc not in (0, 1, 2)):
raise ValueError("block_root_bc must be an integer mode 0, 1 or 2")
macros = cfg.to_macros()
if isinstance(n_objects, bool) or not isinstance(n_objects, int) or n_objects < 0:
raise ValueError("n_objects must be a nonnegative integer")
@@ -75,15 +71,14 @@ def generate_config(cfg: LBMConfig, n_objects: int = 0,
try:
shutil.copytree(_KERNEL_DIR, build_dir, dirs_exist_ok=True,
ignore=shutil.ignore_patterns("*.ptx", "*.cubin", "*.tmp"))
_render_config(macros, n_objects, os.path.join(build_dir, "config"),
block_root_bc=block_root_bc)
_render_config(macros, n_objects, os.path.join(build_dir, "config"))
digest = hashlib.sha256()
for path in sorted(Path(build_dir).rglob("*")):
if path.is_file():
digest.update(str(path.relative_to(build_dir)).encode())
digest.update(path.read_bytes())
metadata = {"source_sha256": digest.hexdigest(), "config": asdict(cfg),
"n_objects": n_objects, "block_root_bc": block_root_bc}
"n_objects": n_objects}
_write(os.path.join(build_dir, ".celeris-build"), json.dumps(metadata, sort_keys=True))
except BaseException:
shutil.rmtree(build_dir)
@@ -106,7 +101,7 @@ def cleanup_build_dir(build_dir: Optional[str]) -> None:
_last_ptx_path.set(None)
def _render_config(m: dict, n_objects: int, cdir: str, *, block_root_bc: int = 0):
def _render_config(m: dict, n_objects: int, cdir: str):
_write(os.path.join(cdir, "config_grid.h"), f"""{_HEADER}\
// Layer: Global / Grid
#ifndef CELERIS_CONFIG_GRID_H
@@ -170,9 +165,6 @@ def _render_config(m: dict, n_objects: int, cdir: str, *, block_root_bc: int = 0
#define STORE_PRECISION {m['STORE_PRECISION']}
#define USE_DDF_SHIFTING {m['USE_DDF_SHIFTING']}
// Internal blocked root mode: 0=periodic/fine, 1=slot/reservoir, 2=channel.
#define BLOCK_ROOT_BC {block_root_bc}
#define USE_LES {m['USE_LES']}
#define LES_CS {m['LES_CS']}
#define LES_CLOSED_FORM {m['LES_CLOSED_FORM']}
+4 -14
View File
@@ -204,21 +204,11 @@ class PeriodicBlockStepper:
raise
def step(self, n=1):
"""Advance n steps: halo refresh, then core pull/collide, per step.
All n steps are enqueued in order on the one stream and the host waits
once before returning (same launches and bits as n step(1) calls).
Asynchronous device errors surface at that single wait, so they are not
attributed to the step that caused them; call step(1) repeatedly to
localize one. On any failure the instance is failed and step_count keeps
its entry value.
"""
"""Advance n steps: halo refresh, then core pull/collide, per step."""
if isinstance(n, bool) or not isinstance(n, Integral) or n < 0:
raise ValueError("n must be a nonnegative integer")
with self._lock, self.ctx.activate():
self._require("ready")
if n == 0:
return
kw = dict(block=self._block, stream=self._stream)
try:
self._status = "advancing"
@@ -234,9 +224,9 @@ class PeriodicBlockStepper:
self._step(self._current, self._spare, np.uint32(self.layout.halo),
self._tables.get("flags", np.uintp(0)), np.uintp(0), # no kind
grid=self._grid, **kw)
self._current, self._spare = self._spare, self._current # host handles only
self._stream.synchronize()
self._step_count += n
self._stream.synchronize()
self._current, self._spare = self._spare, self._current
self._step_count += 1
self._status = "ready"
except BaseException:
self._status = "failed"
+2 -2
View File
@@ -1,4 +1,4 @@
"""Host contract for the blocked root's uniform SRT/MRT channel boundary.
"""Host contract for the blocked root's uniform-SRT channel boundary.
Boundary ghosts are evolved state, but are not physical fluid leaves. Halo
ownership remains periodic to reproduce the uniform kernel's wrapped pulls.
@@ -29,7 +29,7 @@ def validate_channel_config(cfg, blocks):
def validate_channel_transfers(pair, layout, targets):
"""Reject actual exchange/closure overlap, not a padded geometric envelope.
Explosion donors and restriction receivers must use ordinary bulk collision
Explosion donors and restriction receivers must use ordinary bulk SRT
semantics. Restriction writes receiver-local source slots; these may not
overwrite the channel's extra immutable-input reads (pressure donor and
same-node halfway-wall corrections). The slot check deliberately respects
+4 -2
View File
@@ -94,10 +94,12 @@ class CurvedLinkSoA:
"""
self.surface_velocity = self.aperture = None
self.jets, self.jet_shape, self.jet_id = tuple(jets), None, None
from ..body.jets import _surface_links
self.surface_velocity, self.aperture, self.jet_shape, self.jet_id = _surface_links(
self.surface_velocity, self.aperture = surface_link_velocity(
objects, cfg, flags, self.fluid_idx, self.dir, self.body_id, self.rx, self.ry,
jets=self.jets, hits=hits)
if self.jets:
from ..body.jets import jet_link_shapes
self.jet_shape, self.jet_id = jet_link_shapes(self.jets, hits)
def set_jet_amplitude(self, index, amplitude, stream=None):
"""Rewrite jet rows from cached shapes; re-upload only the velocity column.
+139 -26
View File
@@ -1,6 +1,5 @@
// CelerisLab/lbm/kernels/blocks.cu
// One fixed-size blocked level (lbm/blocks.py layout), specialized at compile
// time for periodic/fine, slot/reservoir or channel stepping. Separate
// One periodic level of fixed-size blocks (lbm/blocks.py layout). Separate
// compilation entry: NX, NY are the block window including halo, so index_f
// and linear_index address one block; blockIdx.z selects the block, whose
// populations start at b*NQ*TOTAL_CELLS (block-contiguous, Q-major inside).
@@ -13,37 +12,151 @@
#include "core/params.cuh"
#include "core/flags.cuh"
#include "core/obs.cuh"
#include "core/cell_kind.cuh"
#include "operators/macro.cuh"
#include "operators/collision_srt.cuh"
#if COLLISION_MODEL == 2
#include "operators/collision_mrt.cuh"
#endif
#include "streaming/pull_double_buffer.cuh"
#include "boundary/curved_boundary.cuh"
#include "boundary/planar_velocity.cuh"
#if DIM != 2 || NQ != 9 || (COLLISION_MODEL != 0 && COLLISION_MODEL != 2) || STORE_PRECISION != 0 || USE_LES || STREAMING_MODEL != 0
#error "Blocks require FP32 D2Q9 SRT or MRT double buffers without LES"
#endif
#ifndef BLOCK_ROOT_BC
#error "Blocks require a generated BLOCK_ROOT_BC mode"
#elif BLOCK_ROOT_BC < 0 || BLOCK_ROOT_BC > 2
#error "BLOCK_ROOT_BC must be 0, 1 or 2"
#if DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_LES || STREAMING_MODEL != 0
#error "Blocks require FP32 D2Q9 SRT double buffers without LES"
#endif
#if BLOCK_ROOT_BC == 1
#include "boundary/block_wall.cuh"
#endif
#if BLOCK_ROOT_BC != 2
#include "operators/block_collision.cuh"
#endif
// Fixed-patch cell kinds, matching refinement.py ACTIVE/INNER_GHOST/OUTER_GHOST.
constexpr unsigned char KIND_ACTIVE = 0u, KIND_INNER = 1u, KIND_OUTER = 2u;
// Kernels, in definition order: transfers, boundary staging, then the one full
// step selected for this module (BlockStep, or BlockChannelStep for a channel root).
#include "step/block_transfer.cu"
#include "step/block_boundary.cu"
#if BLOCK_ROOT_BC != 2
#include "step/block_step.cu"
// Zero-initialized for ordinary blocks/fine levels. Set only for parent walls;
// NX/NY remain local storage strides, never physical wall coordinates.
struct BlockWall { unsigned int nx, ny, slot_width; float amplitude; };
extern "C" { __constant__ BlockWall d_block_wall; }
// Halo refresh: block cell ids b*TOTAL_CELLS+c, sources are always core cells.
extern "C" __global__ void BlockHaloCopy(
fpxx* f, const uint64_t* dst, const uint64_t* src, unsigned int count)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
const uint64_t d = dst[row], s = src[row];
fpxx* to = f + (d / TOTAL_CELLS) * NQ * TOTAL_CELLS;
const fpxx* from = f + (s / TOTAL_CELLS) * NQ * TOTAL_CELLS;
for (unsigned int q = 0; q < NQ; ++q)
to[index_f(d % TOTAL_CELLS, q)] = from[index_f(s % TOTAL_CELLS, q)];
}
// Fixed-patch explosion into blocked fine storage: target = owner block cell
// id of a patch-ghost cell; donors are parent block cell ids. Dense parent
// storage is the one-window case with coarse_cells = full domain size.
extern "C" __global__ void BlockExplode(
const fpxx* coarse, fpxx* fine, const uint64_t* target, const uint64_t* donor,
unsigned int count, unsigned long coarse_cells)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
const uint64_t t = target[row];
fpxx* to = fine + (t / TOTAL_CELLS) * NQ * TOTAL_CELLS;
const uint64_t d = donor[row];
const fpxx* from = coarse + (d / coarse_cells) * NQ * coarse_cells;
for (unsigned int q = 0; q < NQ; ++q)
to[index_f(t % TOTAL_CELLS, q)] = from[q * coarse_cells + d % coarse_cells];
}
// Targets are receiver-local SOURCE slots for the immediately following pull.
// Keep child summation order identical to PatchCoarseStep; no collision here.
extern "C" __global__ void BlockCoalesce(
const fpxx* fine, fpxx* coarse, const uint64_t* target, const uint64_t* children,
unsigned int count)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
const uint64_t* s = children + 4ul * row;
coarse[target[row]] = 0.25f * (fine[s[0]] + fine[s[1]] + fine[s[2]] + fine[s[3]]);
}
// Static cut links, receiver-local staging: cell is the receiver's local core
// cell, so the existing BFL writes the solid-source window image that this
// block's next pull reads (no halo copy between this kernel and BlockStep).
// surface_velocity (nullable, 2 floats per link): prescribed fluid velocity on
// fixed geometry, same convention as CurvedBoundaryKernel. Optional raw rows
// (jx,jy,torque,dm) are unique physical links, never body-observation slots.
extern "C" __global__ void BlockCurvedBoundary(
fpxx* f, const uint32_t* block, const uint32_t* cell, const unsigned char* dir,
const float* q, const unsigned char* fallback, unsigned int count,
const float* surface_velocity, const float* rx, const float* ry, float* raw)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
float Uw = 0.0f, Vw = 0.0f;
if (surface_velocity != nullptr) {
const float u = surface_velocity[2 * row], v = surface_velocity[2 * row + 1];
if (u != 0.0f || v != 0.0f) { Uw = u; Vw = v; }
}
apply_bouzidi_link(dir[row], q[row], cell[row], f + (unsigned long)block[row] * NQ * TOTAL_CELLS,
Uw, Vw, rx ? rx[row] : 0.0f, ry ? ry[row] : 0.0f,
fallback[row], nullptr, -1, raw ? raw + 4ul * row : nullptr,
raw ? raw + 4ul * row + 3 : nullptr);
}
// Same-stream BFL -> this row-owned accumulation -> pull step. No atomics,
// no cross-row reduction and no history allocation proportional to substeps.
extern "C" __global__ void BlockAccumulateExchange(
const float* raw, double* integral, unsigned int count, double h, double dt)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
const unsigned long k = 4ul * row;
const double impulse = h * dt;
integral[k] += impulse * (double)raw[k];
integral[k + 1] += impulse * (double)raw[k + 1];
integral[k + 2] += h * impulse * (double)raw[k + 2];
integral[k + 3] += h * h * (double)raw[k + 3];
}
// Pull/collide/store core cells only; halo cells of out are left untouched.
// flags (block cell ids, nullable): solid cores store NaN and never collide.
// kind (block cell ids, nullable = all ACTIVE): OUTER skips the pull and
// stores NaN, INNER pulls without collision, ACTIVE pulls and collides.
extern "C" __global__ void BlockStep(const fpxx* in, fpxx* out, unsigned int halo,
const uint16_t* flags, const unsigned char* kind) {
unsigned int x, y; unsigned long k;
index_from_thread(x, y, k);
if (x < halo || y < halo || x >= NX - halo || y >= NY - halo) return;
const unsigned long base = (unsigned long)blockIdx.z * NQ * TOTAL_CELLS;
const unsigned long cell = (unsigned long)blockIdx.z * TOTAL_CELLS + k;
const unsigned char cell_kind = kind != nullptr ? kind[cell] : KIND_ACTIVE;
float f[NQ];
if (cell_kind == KIND_OUTER || (flags != nullptr && is_solid(flags[cell]))) {
for (int q = 0; q < NQ; ++q) f[q] = NAN;
} else {
unsigned long j[NQ];
compute_neighbors(x, y, j); // Core neighbors stay inside the window: no wrap.
stream_pull_load(k, f, in + base, j);
if (cell_kind == KIND_ACTIVE) {
if (d_block_wall.nx != 0) {
const unsigned int bx = NX - 2 * halo, by = NY - 2 * halo;
const unsigned int nbx = d_block_wall.nx / bx;
const unsigned int gx = (blockIdx.z % nbx) * bx + x - halo;
const unsigned int gy = (blockIdx.z / nbx) * by + y - halo;
if (gy == 1)
reconstruct_bottom_velocity(f, 0.0f, bottom_wall_normal_velocity(
gx, d_block_wall.nx, d_block_wall.slot_width, d_block_wall.amplitude));
else if (gy == d_block_wall.ny - 2) reconstruct_top_reservoir(f, RHO);
}
float rho, ux, uy, feq[NQ];
compute_rho_u(f, rho, ux, uy);
#if USE_DDF_SHIFTING
// Preserve the density perturbation before adding the unit reference.
const float drho = f[0]+f[1]+f[2]+f[3]+f[4]+f[5]+f[6]+f[7]+f[8];
const float jx = f[1]-f[2]+f[5]-f[6]+f[7]-f[8];
const float jy = f[3]-f[4]+f[5]-f[6]-f[7]+f[8];
compute_feq_from_momentum(drho, jx, jy, ux, uy, feq);
#else
#include "step/block_channel.cu"
compute_feq(rho, ux, uy, feq);
#endif
collide_srt_no_force(f, feq, d_params.omega);
}
}
stream_pull_store(k, f, out + base);
}
// Root channel closure; fine and periodic levels keep BlockStep.
#include "step/block_channel.cu"
@@ -1,20 +0,0 @@
// Physical parent walls; NX/NY remain local storage strides.
#ifndef CELERIS_BOUNDARY_BLOCK_WALL_CUH
#define CELERIS_BOUNDARY_BLOCK_WALL_CUH
#include "planar_velocity.cuh"
struct BlockWall { unsigned int nx, ny, slot_width; float amplitude; };
extern "C" { __constant__ BlockWall d_block_wall; }
__device__ __forceinline__ void block_wall(float* f, unsigned int x, unsigned int y,
unsigned int halo) {
unsigned int gx, gy;
block_global_xy(x, y, halo, d_block_wall.nx, gx, gy);
if (gy == 1)
reconstruct_bottom_velocity(f, 0.0f, bottom_wall_normal_velocity(
gx, d_block_wall.nx, d_block_wall.slot_width, d_block_wall.amplitude));
else if (gy == d_block_wall.ny - 2) reconstruct_top_reservoir(f, RHO);
}
#endif
@@ -15,13 +15,10 @@
// D2Q9 y-wall half-way bounce-back for pull double-buffer streaming.
// ---------------------------------------------------------------------------
#if LATTICE_MODEL == LATTICE_D2Q9
// Rows 1 and top_row (= physical ny - 2) next to walls at rows 0 and ny - 1.
// y is the physical row; k addresses the node in fi_in (a block window may
// have NX/NY strides different from the physical extent).
__device__ inline void apply_wall_bb_d2q9_rows(unsigned int y, unsigned int top_row,
float* __restrict__ f,
const fpxx* __restrict__ fi_in,
unsigned long k)
__device__ inline void apply_wall_bb_d2q9_y_pull(unsigned int y,
float* __restrict__ f,
const fpxx* __restrict__ fi_in,
unsigned long k)
{
if (y == 1) {
// Directions sourced from y=0 wall in pull step: +y, +x+y, -x+y.
@@ -29,22 +26,14 @@ __device__ inline void apply_wall_bb_d2q9_rows(unsigned int y, unsigned int top_
f[5] = load_ddf(fi_in, index_f(k, 6u));
f[8] = load_ddf(fi_in, index_f(k, 7u));
}
else if (y == top_row) {
// Directions sourced from the y = ny - 1 wall in pull step: -y, -x-y, +x-y.
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));
}
}
__device__ inline void apply_wall_bb_d2q9_y_pull(unsigned int y,
float* __restrict__ f,
const fpxx* __restrict__ fi_in,
unsigned long k)
{
apply_wall_bb_d2q9_rows(y, (unsigned int)(NY - 2), f, fi_in, k);
}
// ---------------------------------------------------------------------------
// D2Q9 y-wall free-slip (specular reflection) for pull double-buffer streaming.
//
@@ -8,9 +8,6 @@
#define STORE_PRECISION 0
#define USE_DDF_SHIFTING 0
// Internal blocked root mode: 0=periodic/fine, 1=slot/reservoir, 2=channel.
#define BLOCK_ROOT_BC 0
#define USE_LES 0
#define LES_CS 0.160000f
#define LES_CLOSED_FORM 1
@@ -1,16 +0,0 @@
// CelerisLab – core/cell_kind.cuh
// Fixed-patch cell kinds stored per block cell, matching lbm/refinement.py
// ACTIVE/INNER_GHOST/OUTER_GHOST: OUTER is invalid (stores NaN, no pull),
// INNER streams without collision, ACTIVE pulls and collides.
#ifndef CELERIS_CORE_CELL_KIND_CUH
#define CELERIS_CORE_CELL_KIND_CUH
constexpr unsigned char KIND_ACTIVE = 0u, KIND_INNER = 1u, KIND_OUTER = 2u;
// NaN populations mark a cell without valid state (OUTER ghost or solid core).
__device__ __forceinline__ void fill_invalid(float* f)
{
for (int q = 0; q < NQ; ++q) f[q] = NAN;
}
#endif
@@ -53,18 +53,6 @@ __device__ __forceinline__ void index_from_thread(unsigned int& x,
k = (unsigned long)y * NX + x;
}
// Global cell of window cell (x, y) in block blockIdx.z of a row-major,
// full-domain block layout domain_nx cells wide. NX/NY are the local strides.
__device__ __forceinline__ void block_global_xy(unsigned int x, unsigned int y,
unsigned int halo, unsigned int domain_nx,
unsigned int& gx, unsigned int& gy)
{
const unsigned int bx = NX - 2 * halo, by = NY - 2 * halo;
const unsigned int nbx = domain_nx / bx;
gx = (blockIdx.z % nbx) * bx + x - halo;
gy = (blockIdx.z / nbx) * by + y - halo;
}
// ---------------------------------------------------------------------------
// Neighbor list (periodic BCs via modular arithmetic)
// ---------------------------------------------------------------------------
@@ -1,31 +0,0 @@
// Collision used by periodic/fine and slot/reservoir BlockStep.
#ifndef CELERIS_OPERATORS_BLOCK_COLLISION_CUH
#define CELERIS_OPERATORS_BLOCK_COLLISION_CUH
#include "../core/params.cuh"
#include "macro.cuh"
#include "collision_srt.cuh"
#if COLLISION_MODEL == 2
#include "collision_mrt.cuh"
#endif
__device__ __forceinline__ void block_collide(float* f) {
#if COLLISION_MODEL == 2
collide_mrt_unforced(f); // SRT path below unchanged.
#else
float rho, ux, uy, feq[NQ];
compute_rho_u(f, rho, ux, uy);
#if USE_DDF_SHIFTING
// Preserve the density perturbation before adding the unit reference.
const float drho = f[0]+f[1]+f[2]+f[3]+f[4]+f[5]+f[6]+f[7]+f[8];
const float jx = f[1]-f[2]+f[5]-f[6]+f[7]-f[8];
const float jy = f[3]-f[4]+f[5]-f[6]-f[7]+f[8];
compute_feq_from_momentum(drho, jx, jy, ux, uy, feq);
#else
compute_feq(rho, ux, uy, feq);
#endif
collide_srt_no_force(f, feq, d_params.omega);
#endif
}
#endif
@@ -1,14 +0,0 @@
// CelerisLab – operators/bounce_back_swap.cuh
// Full-way bounce-back at a wall node: swap every opposite pair (paired
// order q, q+1). Needs only NQ; shared by the uniform and blocked steps.
#ifndef CELERIS_OPERATORS_BOUNCE_BACK_SWAP_CUH
#define CELERIS_OPERATORS_BOUNCE_BACK_SWAP_CUH
__device__ __forceinline__ void bounce_back_swap(float* f) {
#pragma unroll
for (int i = 1; i < NQ; i += 2) {
float t = f[i]; f[i] = f[i+1]; f[i+1] = t;
}
}
#endif
@@ -114,16 +114,6 @@ __device__ __forceinline__ void collide_mrt_no_force(float* __restrict__ g,
collide_mrt(g, rho, ux, uy, Fin, omega);
}
// Unforced MRT with the clamped device omega, as in collide_dispatch with zero
// forcing. Shared by the blocked, channel-root and dense-patch collisions.
__device__ __forceinline__ void collide_mrt_unforced(float* __restrict__ g)
{
float rho, ux, uy;
compute_rho_u(g, rho, ux, uy);
collide_mrt_no_force(g, rho, ux, uy,
fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega)));
}
#elif LATTICE_MODEL == LATTICE_D3Q19
__device__ __forceinline__ void collide_mrt(float* __restrict__ g,
@@ -5,7 +5,12 @@
#ifndef CELERIS_OPERATORS_HELPERS_CUH
#define CELERIS_OPERATORS_HELPERS_CUH
#include "bounce_back_swap.cuh"
__device__ __forceinline__ void bounce_back_swap(float* f) {
#pragma unroll
for (int i = 1; i < NQ; i += 2) {
float t = f[i]; f[i] = f[i+1]; f[i+1] = t;
}
}
__device__ __forceinline__ void collide_dispatch(
float* f, float rho_n,
+3 -11
View File
@@ -9,12 +9,8 @@
#include "core/params.cuh"
#include "core/flags.cuh"
#include "core/obs.cuh"
#include "core/cell_kind.cuh"
#include "operators/macro.cuh"
#include "operators/collision_srt.cuh"
#if COLLISION_MODEL == 2
#include "operators/collision_mrt.cuh"
#endif
#include "streaming/pull_double_buffer.cuh"
#include "boundary/planar_velocity.cuh"
#include "boundary/curved_boundary.cuh"
@@ -23,8 +19,8 @@ extern "C" {
#include "step/curved_double.cu"
}
#if DIM != 2 || NQ != 9 || (COLLISION_MODEL != 0 && COLLISION_MODEL != 2) || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
#error "Refinement requires unshifted FP32 D2Q9 SRT or MRT double buffers without LES"
#if DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
#error "Refinement requires unshifted FP32 D2Q9 SRT double buffers without LES"
#endif
// Enabled only in the validation build; no diagnostic arrays in normal builds.
@@ -37,14 +33,10 @@ __device__ unsigned int incoming_counts[NQ * TOTAL_CELLS];
#endif
__device__ __forceinline__ void patch_collide(float* f, unsigned long k) {
#if COLLISION_MODEL == 2
collide_mrt_unforced(f);
#else
float rho, ux, uy, feq[NQ];
compute_rho_u(f, rho, ux, uy);
compute_feq(rho, ux, uy, feq);
collide_srt_no_force(f, feq, d_params.omega);
#endif
#ifdef CELERIS_REFINEMENT_AUDIT
++collision_counts[k];
#endif
@@ -72,7 +64,7 @@ extern "C" __global__ void PatchFineStep(const float* in, float* out, const uint
// that held explosion data. Inner halo only streams; it never collides.
if (x < 2 || y < 2 || x >= NX - 2 || y >= NY - 2
|| (flags != nullptr && is_solid(flags[k]))) {
fill_invalid(f);
for (int q = 0; q < NQ; ++q) f[q] = NAN;
} else {
unsigned long j[NQ];
compute_neighbors(x, y, j); // These interior targets never wrap.
@@ -1,42 +0,0 @@
// CelerisLab/lbm/kernels/step/block_boundary.cu
// Receiver-local curved-boundary staging and its raw-exchange accumulation.
// Part of the blocks.cu compilation entry; relies on its includes and guards.
// Static cut links, receiver-local staging: cell is the receiver's local core
// cell, so the existing BFL writes the solid-source window image that this
// block's next pull reads (no halo copy between this kernel and BlockStep).
// surface_velocity (nullable, 2 floats per link): prescribed fluid velocity on
// fixed geometry, same convention as CurvedBoundaryKernel. Optional raw rows
// (jx,jy,torque,dm) are unique physical links, never body-observation slots.
extern "C" __global__ void BlockCurvedBoundary(
fpxx* f, const uint32_t* block, const uint32_t* cell, const unsigned char* dir,
const float* q, const unsigned char* fallback, unsigned int count,
const float* surface_velocity, const float* rx, const float* ry, float* raw)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
float Uw = 0.0f, Vw = 0.0f;
if (surface_velocity != nullptr) {
const float u = surface_velocity[2 * row], v = surface_velocity[2 * row + 1];
if (u != 0.0f || v != 0.0f) { Uw = u; Vw = v; }
}
apply_bouzidi_link(dir[row], q[row], cell[row], f + (unsigned long)block[row] * NQ * TOTAL_CELLS,
Uw, Vw, rx ? rx[row] : 0.0f, ry ? ry[row] : 0.0f,
fallback[row], nullptr, -1, raw ? raw + 4ul * row : nullptr,
raw ? raw + 4ul * row + 3 : nullptr);
}
// Same-stream BFL -> this row-owned accumulation -> pull step. No atomics,
// no cross-row reduction and no history allocation proportional to substeps.
extern "C" __global__ void BlockAccumulateExchange(
const float* raw, double* integral, unsigned int count, double h, double dt)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
const unsigned long k = 4ul * row;
const double impulse = h * dt;
integral[k] += impulse * (double)raw[k];
integral[k + 1] += impulse * (double)raw[k + 1];
integral[k + 2] += h * impulse * (double)raw[k + 2];
integral[k + 3] += h * h * (double)raw[k + 3];
}
@@ -1,12 +1,5 @@
#if BLOCK_ROOT_BC != 2
#error "BlockChannelStep requires BLOCK_ROOT_BC == 2"
#endif
// Root-only blocked counterpart of OneStep's D2Q9/SRT channel closure.
// NX/NY are LOCAL strides; all physical boundary decisions use global coordinates.
#include "../core/cell_kind.cuh"
#include "../operators/bounce_back_swap.cuh"
#include "../boundary/bounce_back.cuh"
#include "../boundary/inlet/common.cuh"
#include "../boundary/inlet/zou_he_local.cuh"
#include "../boundary/outlet/pressure_neq.cuh"
@@ -22,13 +15,15 @@ extern "C" __global__ void BlockChannelStep(
unsigned int x, y; unsigned long k;
index_from_thread(x, y, k);
if (x < halo || y < halo || x >= NX - halo || y >= NY - halo) return;
unsigned int gx, gy;
block_global_xy(x, y, halo, d_block_channel.nx, gx, gy);
const unsigned int bx = NX - 2 * halo, by = NY - 2 * halo;
const unsigned int nbx = d_block_channel.nx / bx;
const unsigned int gx = (blockIdx.z % nbx) * bx + x - halo;
const unsigned int gy = (blockIdx.z / nbx) * by + y - halo;
const unsigned long base = (unsigned long)blockIdx.z * NQ * TOTAL_CELLS;
const unsigned long cell = (unsigned long)blockIdx.z * TOTAL_CELLS + k;
float f[NQ];
if (kind != nullptr && kind[cell] == KIND_OUTER) {
fill_invalid(f);
for (int q = 0; q < NQ; ++q) f[q] = NAN;
} else {
unsigned long j[NQ];
compute_neighbors(x, y, j);
@@ -36,29 +31,35 @@ extern "C" __global__ void BlockChannelStep(
const bool wall = gy == 0 || gy == d_block_channel.ny - 1;
const bool outlet = gx == d_block_channel.nx - 1;
if (wall) {
bounce_back_swap(f); // Wall owns all four corners; ghosts never collide.
// Wall owns all four corners. Ghosts pull and swap, never collide.
for (int q = 1; q < NQ; q += 2) {
const float t = f[q]; f[q] = f[q + 1]; f[q + 1] = t;
}
} else if (gx == 0) {
apply_zou_he_left_velocity_inlet_d2q9(
f, inlet_target_u((float)gy, d_block_channel.ny, d_block_channel.velocity), 0.0f);
} else if (outlet) {
// Immutable input donor, receiver-local (also valid across a block seam).
float donor[NQ];
load_node(in + base, k - 1, donor);
for (int q = 0; q < NQ; ++q)
donor[q] = load_ddf(in + base, index_f(k - 1, q));
apply_pressure_outlet_d2q9(f, donor);
} else {
apply_wall_bb_d2q9_rows(gy, d_block_channel.ny - 2, f, in + base, k);
} else if (gy == 1) {
f[3] = load_ddf(in + base, index_f(k, 4));
f[5] = load_ddf(in + base, index_f(k, 6));
f[8] = load_ddf(in + base, index_f(k, 7));
} else if (gy == d_block_channel.ny - 2) {
f[4] = load_ddf(in + base, index_f(k, 3));
f[6] = load_ddf(in + base, index_f(k, 5));
f[7] = load_ddf(in + base, index_f(k, 8));
}
if (!wall && !outlet) {
#if COLLISION_MODEL == 2
collide_mrt_unforced(f);
#else
// Match uniform SRT arithmetic, including its shifted equilibrium path.
float rho, ux, uy, feq[NQ], forcing[NQ] = {};
compute_rho_u(f, rho, ux, uy);
compute_feq(rho, ux, uy, feq);
const float omega = fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega));
collide_srt(f, feq, forcing, omega);
#endif
}
}
stream_pull_store(k, f, out + base);
@@ -1,35 +0,0 @@
// CelerisLab/lbm/kernels/step/block_step.cu
// Generic blocked pull/collide/store for periodic/fine and slot/reservoir levels.
// Part of the blocks.cu compilation entry; relies on its includes and guards.
#if BLOCK_ROOT_BC == 2
#error "BlockStep is not built for the channel root (BLOCK_ROOT_BC == 2)"
#endif
// Pull/collide/store core cells only; halo cells of out are left untouched.
// flags (block cell ids, nullable): solid cores store NaN and never collide.
// kind (block cell ids, nullable = all ACTIVE): OUTER skips the pull and
// stores NaN, INNER pulls without collision, ACTIVE pulls and collides.
extern "C" __global__ void BlockStep(const fpxx* in, fpxx* out, unsigned int halo,
const uint16_t* flags, const unsigned char* kind) {
unsigned int x, y; unsigned long k;
index_from_thread(x, y, k);
if (x < halo || y < halo || x >= NX - halo || y >= NY - halo) return;
const unsigned long base = (unsigned long)blockIdx.z * NQ * TOTAL_CELLS;
const unsigned long cell = (unsigned long)blockIdx.z * TOTAL_CELLS + k;
const unsigned char cell_kind = kind != nullptr ? kind[cell] : KIND_ACTIVE;
float f[NQ];
if (cell_kind == KIND_OUTER || (flags != nullptr && is_solid(flags[cell]))) {
fill_invalid(f);
} else {
unsigned long j[NQ];
compute_neighbors(x, y, j); // Core neighbors stay inside the window: no wrap.
stream_pull_load(k, f, in + base, j);
if (cell_kind == KIND_ACTIVE) {
#if BLOCK_ROOT_BC == 1
block_wall(f, x, y, halo);
#endif
block_collide(f);
}
}
stream_pull_store(k, f, out + base);
}
@@ -1,78 +0,0 @@
// CelerisLab/lbm/kernels/step/block_transfer.cu
// Same-level halo refresh and 2:1 explosion/coalescence between blocked levels.
// Part of the blocks.cu compilation entry; relies on its includes and guards.
// Halo refresh: block cell ids b*TOTAL_CELLS+c, sources are always core cells.
extern "C" __global__ void BlockHaloCopy(
fpxx* f, const uint64_t* dst, const uint64_t* src, unsigned int count)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
const uint64_t d = dst[row], s = src[row];
fpxx* to = f + (d / TOTAL_CELLS) * NQ * TOTAL_CELLS;
const fpxx* from = f + (s / TOTAL_CELLS) * NQ * TOTAL_CELLS;
for (unsigned int q = 0; q < NQ; ++q)
to[index_f(d % TOTAL_CELLS, q)] = from[index_f(s % TOTAL_CELLS, q)];
}
// Fixed-patch explosion into blocked fine storage: target = owner block cell
// id of a patch-ghost cell; donors are parent block cell ids. Dense parent
// storage is the one-window case with coarse_cells = full domain size.
extern "C" __global__ void BlockExplode(
const fpxx* coarse, fpxx* fine, const uint64_t* target, const uint64_t* donor,
unsigned int count, unsigned long coarse_cells)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
const uint64_t t = target[row];
fpxx* to = fine + (t / TOTAL_CELLS) * NQ * TOTAL_CELLS;
const uint64_t d = donor[row];
const fpxx* from = coarse + (d / coarse_cells) * NQ * coarse_cells;
for (unsigned int q = 0; q < NQ; ++q)
to[index_f(t % TOTAL_CELLS, q)] = from[q * coarse_cells + d % coarse_cells];
}
// Experimental Schornbaum--Ruede (arXiv:1508.07982, Algorithm 2)
// and Chen et al. (doi:10.1016/j.physa.2005.09.036) population interpolation.
// Independent implementation of the mathematical stencil; no upstream source copied.
// Stencil cells are owner cores; a disabled axis repeats its center twice.
extern "C" __global__ void BlockExplodeProjected(
const fpxx* coarse, fpxx* fine, const uint64_t* target, const uint64_t* donor,
unsigned int count, unsigned long coarse_cells,
const uint64_t* neighbors, const float* offsets)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
const uint64_t t = target[row], d = donor[row];
fpxx* to = fine + (t / TOTAL_CELLS) * NQ * TOTAL_CELLS;
for (unsigned int q = 0; q < NQ; ++q) {
float v[4];
for (unsigned int a = 0; a < 4; ++a) {
const uint64_t n = neighbors[4ul * row + a];
v[a] = coarse[(n / coarse_cells) * NQ * coarse_cells
+ q * coarse_cells + n % coarse_cells];
}
float gx = .5f * (v[1] - v[0]), gy = .5f * (v[3] - v[2]);
const float ex = d_cx[q], ey = d_cy[q];
if (q != 0) {
const float along = (ex * gx + ey * gy) / (ex * ex + ey * ey);
gx -= ex * along;
gy -= ey * along;
}
to[index_f(t % TOTAL_CELLS, q)] =
coarse[(d / coarse_cells) * NQ * coarse_cells + q * coarse_cells + d % coarse_cells]
+ offsets[2ul * row] * gx + offsets[2ul * row + 1] * gy;
}
}
// Targets are receiver-local SOURCE slots for the immediately following pull.
// Keep child summation order identical to PatchCoarseStep; no collision here.
extern "C" __global__ void BlockCoalesce(
const fpxx* fine, fpxx* coarse, const uint64_t* target, const uint64_t* children,
unsigned int count)
{
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= count) return;
const uint64_t* s = children + 4ul * row;
coarse[target[row]] = 0.25f * (fine[s[0]] + fine[s[1]] + fine[s[2]] + fine[s[3]]);
}
@@ -21,14 +21,6 @@
// i.e. the population traveling in direction i at node n
// arrived from the node at n - c_i = neighbor in direction opp(i)
// ---------------------------------------------------------------------------
// load_node: all NQ populations of node n as stored (no streaming).
__device__ __forceinline__ void load_node(const fpxx* __restrict__ fi, unsigned long n,
float* __restrict__ f)
{
for (int q = 0; q < NQ; ++q)
f[q] = load_ddf(fi, index_f(n, q));
}
__device__ __forceinline__ void stream_pull_load(unsigned long n,
float* __restrict__ f,
const fpxx* __restrict__ fi,
+4 -15
View File
@@ -7,26 +7,16 @@ import numpy as np
def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
time_end, window, reference_center, duration=None):
time_end, window, reference_center):
"""Reduce FP64 interval integrals or convert FP32 last-substep raw rows.
dt_l=h_l under acoustic 2:1 refinement. Local momentum j -> base force
h*j -> impulse h*dt*j; torque adds one h; mass is h**2*dm.
window="window" reduces an FP64 observation-window ledger spanning
``duration`` (integer >= 1) base intervals; rates divide by that duration.
"""
if window not in ("last_base_step", "last_substep", "window"):
if window not in ("last_base_step", "last_substep"):
raise ValueError("unknown exchange window")
if window == "window":
if isinstance(duration, bool) or not isinstance(duration, (int, np.integer)) or duration < 1:
raise ValueError("window duration must be an integer number of base intervals >= 1")
elif duration is not None:
raise ValueError("duration applies only to window='window'")
values = np.array(rows, dtype=np.float64, copy=True)
if window == "window":
duration = float(duration)
else:
duration = 1.0 if window == "last_base_step" else h
duration = 1.0 if window == "last_base_step" else h
if window == "last_substep":
values *= np.array([h*h, h*h, h*h*h, h*h])
selected = np.asarray(body_ids) == body_id
@@ -41,7 +31,6 @@ def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
return dict(wall=reduce(selected & ~aperture), aperture=reduce(selected & aperture),
total=reduce(selected), body_id=int(body_id), level=int(level),
time_start=time_end-duration, time_end=time_end, dt=h,
sample_count=(round(duration/h) if window == "window"
else round(1/h) if window == "last_base_step" else 1),
sample_count=round(1/h) if window == "last_base_step" else 1,
window=window, units="base_lattice_per_unit_depth",
definition="raw_boundary_exchange", reference_center=tuple(reference_center), valid=True)
+4 -4
View File
@@ -52,17 +52,17 @@ class FixedPatch2D:
def from_config(cls, cfg: LBMConfig, bounds) -> "FixedPatch2D":
"""Prepare maps without changing cfg or allocating coarse-sized tables.
The backend contract is D2Q9/SRT or MRT/FP32/double_buffer,
The initial backend contract is D2Q9/SRT/FP32/double_buffer,
without LES. Two coarse cells of margin keep all halo donors in bounds;
this does NOT certify that those donors are fluid or periodic.
"""
checked = replace(cfg)
checked.validate()
if (not checked.is_d2q9 or checked.collision not in ("SRT", "MRT")
if (not checked.is_d2q9 or checked.collision != "SRT"
or checked.streaming != "double_buffer"
or checked.store_precision != "FP32"
or checked.les_enabled):
raise ValueError("FixedPatch2D requires D2Q9/SRT or MRT/FP32/double_buffer without LES")
raise ValueError("FixedPatch2D requires D2Q9/SRT/FP32/double_buffer without LES")
bounds = tuple(bounds)
if len(bounds) != 4 or any(isinstance(v, (bool, np.bool_))
or not isinstance(v, Integral) for v in bounds):
@@ -451,7 +451,7 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
checked = replace(cfg)
checked.validate()
if (patch.coarse_shape != (cfg.ny, cfg.nx) or not checked.is_d2q9
or checked.collision not in ("SRT", "MRT") or checked.streaming != "double_buffer"
or checked.collision != "SRT" or checked.streaming != "double_buffer"
or checked.store_precision != "FP32" or checked.les_enabled
or patch.tau_coarse != 3*checked.viscosity+.5):
raise ValueError("Fine boundary requires the matching fixed-patch configuration")
+147 -181
View File
@@ -22,7 +22,7 @@ from .block_stepper import MAX_GRID_DIM
from .hierarchy import build_hierarchy, is_rectangle
from .refinement import ACTIVE, HALO, FixedPatch2D, FixedPatchSet2D, prepare_fine_boundary
from .refinement_tables import (blocked_base_tables, blocked_fine_tables, # noqa: F401 (re-export)
blocked_kind, projected_explosion_tables, _prepare_blocked_boundary,
blocked_kind, _prepare_blocked_boundary,
BLOCKED_BOUNDARY_DEVICE_COLUMNS)
from .descriptors import SOLID
from .raw_exchange import summarize_exchange
@@ -41,8 +41,6 @@ class _Pair:
n_ghosts: int
n_rows: int
parent_cells: int # parent block window cells (explode donor stride)
neighbors: object = None # projected-only owner-core cells; constant allocates neither
offsets: object = None
@dataclass
@@ -65,8 +63,52 @@ def _level_name(level):
return ("base", "fine", "child")[level] if level < 3 else f"level {level}"
def _legacy_levels(bounds, *, fine_block, base_block, child, child_block, child_rectangles_in_parent,
fine_objects, jets, child_objects, child_jets):
"""Normalise the original two/three-level keywords to per-level lists (original errors)."""
if child is not None and child_rectangles_in_parent is not None:
raise ValueError("child and child_rectangles_in_parent are mutually exclusive")
nested = child is not None or child_rectangles_in_parent is not None
bounds = tuple(bounds)
if not is_rectangle(bounds) and (fine_block is None or base_block is None):
raise ValueError("several patches require fine_block and base_block")
fine_objects, jets = tuple(fine_objects), tuple(jets)
child_objects, child_jets = tuple(child_objects), tuple(child_jets)
if not nested and (child_objects or child_jets):
raise ValueError("child_objects/child_jets require a nested child")
if base_block is not None and fine_block is None:
raise ValueError("base_block requires fine_block")
refinements, blocks = [bounds], [base_block, fine_block]
objects, level_jets = [(), fine_objects], [(), jets]
if nested:
if child is not None and not is_rectangle(bounds):
raise ValueError("scalar child requires one parent patch; use child_rectangles_in_parent")
if fine_block is None or base_block is None or child_block is None:
raise ValueError("a nested child needs fine_block, base_block and child_block")
if fine_objects or jets:
raise ValueError("a nested child supports bodies/Jets only on L2; use child_objects/child_jets")
refinements.append(tuple(child) if child is not None else tuple(child_rectangles_in_parent))
blocks.append(child_block)
objects.append(child_objects)
level_jets.append(child_jets)
return refinements, blocks, objects, level_jets
def _initial_levels(level_g, coarse_g, fine_g, child_g):
"""One tuple of per-level arrays from initialize()'s positional or legacy named form."""
if coarse_g is None and fine_g is None:
if child_g is not None and len(level_g) != 2:
raise TypeError("child_g needs exactly the coarse and fine arrays before it")
return tuple(level_g) + (() if child_g is None else (child_g,))
if level_g:
raise TypeError("pass level arrays positionally or as coarse_g/fine_g/child_g, not both")
if coarse_g is None or fine_g is None:
raise TypeError("coarse_g and fine_g are both required in the named form")
return (coarse_g, fine_g) + (() if child_g is None else (child_g,))
def _check_levels(refinements, blocks, objects, jets):
"""Candidate limits of the level lists, checked before any device work."""
"""Candidate limits shared by the list and the legacy entry points."""
depth = len(refinements) + 1
if depth < 2:
raise ValueError("at least one refinement level is required")
@@ -75,13 +117,13 @@ def _check_levels(refinements, blocks, objects, jets):
raise ValueError(f"{name} needs one entry per level ({depth})")
for level, block in enumerate(blocks):
if block is not None and len(block) != 2:
raise ValueError(f"level_blocks[{level}] must be (by, bx)")
raise ValueError(f"{_level_name(level)}_block must be (by, bx)")
if objects[0] or jets[0]:
raise ValueError("base-level bodies/Jets are not supported")
if any(blocks[level - 1] is not None and blocks[level] is None for level in range(1, depth)):
raise ValueError("a blocked level requires blocked refined levels below it")
if not is_rectangle(refinements[0]) and (blocks[0] is None or blocks[1] is None):
raise ValueError("several L1 patches require level_blocks[0] and level_blocks[1]")
raise ValueError("several patches require fine_block and base_block")
if depth > 2:
if any(block is None for block in blocks):
raise ValueError("three or more levels must all be blocked")
@@ -90,14 +132,7 @@ def _check_levels(refinements, blocks, objects, jets):
class FixedPatchStepper:
"""D2Q9 SRT/MRT FP32 fixed 2:1 refinement hierarchy, built from level lists.
``refinements[l]`` gives level l+1 in level-l integer cells: one rectangle
(x0, y0, x1, y1) (FixedPatch2D, a fine window with its own origin) or a
sequence of rectangles (FixedPatchSet2D, a dependency-validated union on one
level-wide grid, public fine arrays (2*ny, 2*nx), NaN outside ACTIVE cells);
see lbm.hierarchy. ``level_blocks``, ``level_objects`` and ``level_jets``
hold one entry per level, L0 included (L0 bodies/Jets are not supported).
"""D2Q9/SRT/FP32, one unforced internal 2:1 patch in an x-periodic box.
``coarse_boundary`` is ``periodic`` (default) or ``slot_reservoir``:
a bottom on-site slot and top fixed-density reservoir from ``cfg``.
@@ -108,91 +143,108 @@ class FixedPatchStepper:
actual channel closure footprint. Channel remapping is not supported.
Periodic mode does not apply the wall/slot boundary fields in ``cfg``.
Outer walls remain coarse; fine halo donors must be ordinary fluid.
``fine_objects`` accepts fixed circles in coarse physical coordinates;
velocities are in coarse lattice units (dx=dt=1). Independent local copies
are projected at fine cell centers. Their complete stencil must be active
and disjoint from the actual two-hop transfer footprint.
These use the edge/cell-center frame (coarse centers I+.5), whereas ordinary Simulation objects use the integer-node frame.
Bodies are fixed circles in root (L0) physical coordinates; velocities are
in root lattice units (dx=dt=1). Independent local copies are projected at
fine cell centers; their complete stencil must be active and disjoint from
the actual two-hop transfer footprint. These use the edge/cell-center frame
(coarse centers I+.5), whereas ordinary Simulation objects use the
integer-node frame. Jets (body.jets.Jet) use the same frame and drive cut
links selected at their true hits; ``set_jet_amplitude(index, amplitude,
level=l)`` re-uploads the per-link velocity column only.
``initialize(*level_g)`` accepts one physical Q-major (9, ny, nx) array per
level, already in postcollision phase. It does not collide or build channel
flags. Every ``step()`` publishes only after all levels complete on one
stream. On a device/launch failure the instance becomes unusable: close and
recreate it, rather than pretending a partially advanced buffer rolled back.
A ``level_blocks`` entry (by, bx) stores that level in blocks.cu storage:
the ENTIRE level shape, including the four-cell patch ghost band, is
partitioned by a non-periodic BlockLayout2D with one-cell same-level halos.
Patch kinds stay per cell (OUTER invalid, INNER stream only, ACTIVE
collide); halos are gathered before each hop. Public arrays keep the dense
shapes; only the allocation differs. A blocked level requires blocked
refined levels below it; several L1 rectangles and depth >= 3 require all
levels blocked. Blocked bodies are stationary circles, optionally
surface-actuated: prepare_fine_boundary decides legality on the dense grid,
then cut links are staged receiver-local per hop: halo -> BFL -> step, same
buffer. A blocked parent gathers halos before receiver-local coalescence
staging and pull/collision; covered cells are poisoned on every step.
``initialize(coarse_g, fine_g)`` accepts physical Q-major (9,ny,nx) arrays
already in postcollision phase. It does not collide or build channel flags.
Every ``step()`` publishes only after both levels complete on one stream.
On a device/launch failure the instance becomes unusable: close and recreate
it, rather than pretending the partially advanced fine buffer rolled back.
``fine_block=(by, bx)`` (opt-in) stores the fine level in
blocks.cu storage: the ENTIRE fine_shape, including the four-cell patch
ghost band, is partitioned by a non-periodic BlockLayout2D with one-cell
same-level halos. Patch kinds stay per cell (OUTER invalid, INNER stream
only, ACTIVE collide); halos are gathered before each fine hop. Public
arrays keep the dense (9, ny, nx) fine shape; only the allocation differs.
Blocked fine_objects are stationary circles, optionally surface-actuated:
prepare_fine_boundary decides legality on the dense fine grid, then cut
links are staged receiver-local per hop: halo -> BFL -> step, same buffer.
``base_block=(by, bx)`` also blocks the parent; it requires ``fine_block``.
Parent halos precede receiver-local coalescence staging and pull/collision;
covered cells are explicitly poisoned on every step. Public shapes stay dense.
``observe_raw_exchange=True`` records blocked physical cut-link exchange
without changing populations. read_raw_exchange reports the last complete
base interval or last substep in base-lattice units per unit depth. This
is boundary momentum reaction, not a validated nozzle/stress load. Dense
body levels are not supported by this diagnostic.
``observe_window_exchange=True`` (requires observe_raw_exchange) adds a second
per-link FP64 ledger accumulated by the same kernel between
start_exchange_window() and read_exchange_window(); disabled by default.
body layouts are not supported by this new diagnostic in this candidate.
``jets`` (body.jets.Jet) use the same edge frame as ``fine_objects`` and
drive fine cut links selected at their true hits; ``set_jet_amplitude``
re-uploads the per-link velocity column only.
Nested levels: a child set's coordinates index the allocated parent array,
including ghost bands and holes: root edge = parent fine_origin + index / 2
(zero origin for a parent set; a scalar parent keeps its shifted window
origin). A scalar child requires a scalar parent. Rectangle unions impose no
fixed gap rule: adjacent, overlapping and concave unions are accepted when
their actual transfer dependencies close (FixedPatchSet2D /
nest_patch_set). All regions share one buffer pair per level and the fixed
schedule of 2**l hops per base step at level l. Bodies/Jets are supported only on the
finest level of a nested hierarchy (L2 dx=dt=.25 in root units, unchanged
lattice velocity); every whole circle and its BFL stencil must fit strictly
away from that level's transfer footprint.
Several patches: pass ``bounds`` as a sequence of (x0, y0, x1, y1)
rectangles (FixedPatchSet2D, dependency-validated union). This requires
fine_block and base_block; fine_objects/jets are projected once onto the
level-wide fine grid (each circle inside the union, body ids = object
positions, jet and writer checks level-wide), and
uses one level-wide fine grid (2*ny, 2*nx): public fine arrays have that
shape, NaN outside the patches' ACTIVE cells. The step sequence is the
blocked one above, with unique union transfers in one launch each.
``explosion="projected_linear"`` selects experimental transverse linear
reconstruction on each adjacent pair. Default "constant" is unchanged.
Projected mode requires periodic root, all-blocked SRT levels without
bodies/Jets or LES; this backend supplies no forcing. Geometry is generic,
but union/multilevel GPU and matched-physics qualification remain pending.
Three-level separated unions: ``child_rectangles_in_parent=[...]`` is
mutually exclusive with scalar ``child`` and accepts either parent form.
Coordinates index the allocated L1 array, including ghost bands and holes:
root edge = patch.fine_origin + L1 index / 2. A parent set has origin zero;
a scalar parent retains its shifted window origin. The child set's public
extent is twice the allocated L1 shape, even though storage is sparse.
Each pair requires gap >= 4 in its own parent-cell units. All regions share
one buffer pair per level and the fixed 1:2:4 schedule; L2-only bodies/Jets
below apply unchanged, with object IDs assigned over the whole level.
One recursive hop advances every level: a child explodes from its parent's
current hop source, takes two hops, then the parent gathers halos, stages
restriction, applies BFL and steps.
Three-level single chain: ``child`` bounds use L1 window cells; all three
levels must be blocked. ``child_objects`` and ``child_jets`` use the L0
edge frame, with L2 dx=dt=.25 and unchanged lattice velocity. L1 bodies
and Jets remain unsupported in a chain. Every whole circle and its BFL
stencil must fit strictly away from the L2 transfer footprint. L2 runs
halo -> BFL -> step four times per base step. Runtime child Jet changes
require ``set_jet_amplitude(index, amplitude, level=2)``.
Level lists (any depth): ``refinements[l]`` gives level l+1 in level-l
integer cells (scalar window or rectangle list, see lbm.hierarchy);
``level_blocks``/``level_objects``/``level_jets`` hold one entry per level,
L0 included. They are mutually exclusive with the legacy keywords above,
which are normalised to the same lists. One recursive hop advances every
level: a child explodes from its parent's current hop source, takes two
hops, then the parent gathers halos, stages restriction, applies BFL and
steps. Depth >= 3 must be fully blocked with bodies/Jets on the finest level.
"""
def __init__(self, cfg, *, context, refinements, coarse_boundary="periodic",
level_blocks=None, level_objects=None, level_jets=None,
observe_raw_exchange=False, observe_window_exchange=False, explosion="constant"):
def __init__(self, cfg, bounds=None, *, context, coarse_boundary="periodic", fine_objects=(),
fine_block=None, base_block=None, jets=(), child=None, child_block=None,
child_objects=(), child_jets=(), child_rectangles_in_parent=None,
refinements=None, level_blocks=None, level_objects=None, level_jets=None,
observe_raw_exchange=False):
if HALO != 4:
raise RuntimeError("refinement.cu requires a four-cell fine halo")
if coarse_boundary not in ("periodic", "slot_reservoir", "channel"):
raise ValueError("coarse_boundary must be periodic, slot_reservoir or channel")
depth = len(refinements) + 1
level_blocks = [None] * depth if level_blocks is None else list(level_blocks)
level_objects = [()] * depth if level_objects is None else list(level_objects)
level_jets = [()] * depth if level_jets is None else list(level_jets)
if refinements is None:
if bounds is None:
raise ValueError("bounds or refinements is required")
if level_blocks is not None or level_objects is not None or level_jets is not None:
raise ValueError("level_blocks/level_objects/level_jets require refinements")
refinements, level_blocks, level_objects, level_jets = _legacy_levels(
bounds, fine_block=fine_block, base_block=base_block, child=child,
child_block=child_block, child_rectangles_in_parent=child_rectangles_in_parent,
fine_objects=fine_objects, jets=jets, child_objects=child_objects,
child_jets=child_jets)
else:
if (bounds is not None or child is not None or child_rectangles_in_parent is not None
or fine_block is not None or base_block is not None or child_block is not None
or tuple(fine_objects) or tuple(jets) or tuple(child_objects) or tuple(child_jets)):
raise ValueError("refinements and the legacy patch keywords are mutually exclusive")
depth = len(refinements) + 1
level_blocks = [None] * depth if level_blocks is None else list(level_blocks)
level_objects = [()] * depth if level_objects is None else list(level_objects)
level_jets = [()] * depth if level_jets is None else list(level_jets)
level_objects = [tuple(o) for o in level_objects]
level_jets = [tuple(j) for j in level_jets]
level_blocks = [None if b is None else tuple(b) for b in level_blocks]
_check_levels(refinements, level_blocks, level_objects, level_jets)
depth = len(refinements) + 1
if explosion not in ("constant", "projected_linear"):
raise ValueError("explosion must be constant or projected_linear")
if explosion == "projected_linear" and (
coarse_boundary != "periodic" or any(b is None for b in level_blocks)
or any(level_objects) or any(level_jets)
or cfg.collision != "SRT" or cfg.les_enabled):
raise ValueError("projected explosion requires periodic all-blocked SRT levels "
"without bodies/Jets or LES")
self._explosion = explosion
self._ddf_shifting = bool(cfg.ddf_shifting)
if self._ddf_shifting and any(b is None for b in level_blocks):
raise ValueError("shifted populations require all-blocked levels; dense/mixed is unsupported")
@@ -263,13 +315,6 @@ class FixedPatchStepper:
for b, layout in zip(self._boundary_hosts, self._layouts)):
raise ValueError("raw exchange currently requires blocked body levels")
self._observe_raw_exchange = observe_raw_exchange
if not isinstance(observe_window_exchange, bool):
raise ValueError("observe_window_exchange must be bool")
if observe_window_exchange and not observe_raw_exchange:
raise ValueError("observe_window_exchange requires observe_raw_exchange")
self._observe_window = observe_window_exchange
self._window_valid = False
self._window_start = None
self._exchange_valid = False
self._exchange_spacing = tuple(spacings)
self._exchange_centers = tuple(tuple(tuple(o.geometry.center) for o in objects)
@@ -320,8 +365,6 @@ class FixedPatchStepper:
count = len(b.tables["cell"])
b.last_raw = self._allocate(count * 4 * 4)
b.interval_integral = self._allocate(count * 4 * 8)
if self._observe_window:
b.window_integral = self._allocate(count * 4 * 8)
b.accumulate = level.module.get_function("BlockAccumulateExchange")
b.kernel = level.module.get_function("BlockCurvedBoundary")
if self._level_boundaries[1] is not None:
@@ -362,10 +405,7 @@ class FixedPatchStepper:
raise RuntimeError("level omega/tau mismatch")
host = self._boundary_hosts[index]
n_objects = len(host.objects) if name == "PatchFineStep" and host is not None else 0
block_root_bc = 0
if index == 0 and layout is not None:
block_root_bc = 2 if self._channel else 1 if self._wall_aware else 0
build = compiler.generate_config(config, n_objects=n_objects, block_root_bc=block_root_bc)
build = compiler.generate_config(config, n_objects=n_objects)
try:
ptx = compiler.compile_kernel(self.ctx.sm_arch, source=source, build_dir=build)
module = compiler.load_module(ptx)
@@ -427,11 +467,6 @@ class FixedPatchStepper:
self._upload(children), len(ghosts), len(targets),
int(np.prod(pair.coarse_shape if parent.layout is None
else parent.layout.local_shape)))
if self._explosion == "projected_linear":
neighbors, offsets = projected_explosion_tables(
pair, parent.layout, self.hierarchy.kinds[index])
result.explode = child.module.get_function("BlockExplodeProjected")
result.neighbors, result.offsets = self._upload(neighbors), self._upload(offsets)
if child.layout is not None:
child.kind = self._upload(blocked_kind(child.layout, self.hierarchy.kinds[index + 1]))
if index == 0 and base is not None:
@@ -471,13 +506,13 @@ class FixedPatchStepper:
if self._ddf_shifting and any(layout is None for layout in self._layouts):
raise ValueError("shifted populations require all-blocked levels")
def initialize(self, *level_g):
def initialize(self, *level_g, coarse_g=None, fine_g=None, child_g=None):
"""Upload physical postcollision G, encoding before any FP32 rounding.
Shifted input should be physical FP64 to retain small perturbations.
Public initialization resets the step count; non-leaves are discarded.
"""
given = tuple(level_g)
given = _initial_levels(level_g, coarse_g, fine_g, child_g)
with self._lock, self.ctx.activate():
self._require("new", "ready")
self._check_storage()
@@ -524,7 +559,6 @@ class FixedPatchStepper:
cuda.memset_d8(self._fine_curved.link_exchange_gpu, 0, self._fine_curved.count * 12)
self._step_count = 0
self._exchange_valid = False
self._window_valid = False
self._status = "ready"
except BaseException:
self._status = "failed"
@@ -601,11 +635,11 @@ class FixedPatchStepper:
old, new, source, base_count=count, level_steps=(count, 2*count, 4*count),
phase="postcollision_g", **options)
candidate = type(self)(
cfg, context=self.ctx, refinements=[pairs[0].bounds, new.child.bounds],
level_blocks=[base_block, fine_block, child_block],
level_objects=[(), (), child_objects], level_jets=[(), (), jets],
observe_raw_exchange=self._observe_raw_exchange,
observe_window_exchange=self._observe_window, explosion=self._explosion)
cfg, pairs[0].bounds, context=self.ctx, base_block=base_block,
fine_block=fine_block, child_block=child_block,
child_rectangles_in_parent=new.child.bounds,
child_objects=child_objects, child_jets=jets,
observe_raw_exchange=self._observe_raw_exchange)
try:
if self._ddf_shifting:
candidate._initialize_raw(_StoredPopulations(transferred.g, transferred.shifted))
@@ -698,7 +732,6 @@ class FixedPatchStepper:
pair, level = self._pairs[index - 1], self._levels[index]
pair.explode(parent_buffer, level.current, pair.ghosts, pair.donors, np.uint32(pair.n_ghosts),
np.uint64(pair.parent_cells),
*((pair.neighbors, pair.offsets) if pair.neighbors is not None else ()),
grid=((pair.n_ghosts + self._block[0] - 1) // self._block[0], 1), **kw)
self._hop(index, level.current, level.spare, **kw)
self._hop(index, level.spare, level.current, **kw)
@@ -738,10 +771,6 @@ class FixedPatchStepper:
b.accumulate(b.last_raw, b.interval_integral, np.uint32(count),
np.float64(h), np.float64(h),
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
if self._window_valid and b.window_integral is not None:
b.accumulate(b.last_raw, b.window_integral, np.uint32(count),
np.float64(h), np.float64(h),
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
flags = np.uintp(0) if level.boundary is None else level.boundary.flags
level.kernel(src, dst, np.uint32(level.layout.halo), flags, level.kind, grid=level.grid, **kw)
@@ -752,94 +781,32 @@ class FixedPatchStepper:
grid=((pairs + self._block[0] - 1) // self._block[0], 1), **kw)
def step(self, n=1):
"""Advance G→G by n base intervals: level l takes 2**l hops per interval.
All n intervals are enqueued in order on the one stream, and the host waits
once, before returning; step(0) launches and waits for nothing. On return
the device work is complete, step_count and an open window cover all n
intervals, and the raw-exchange ledger holds the final interval only.
Asynchronous device errors surface at that single wait, so they are not
attributed to the interval that caused them; call step(1) repeatedly to
localize one (same launches, same bits). On any failure the instance is
failed, the exchange ledger and window are invalid, and step_count keeps
its value from entry to the failing call.
"""
"""Advance G→G by n base intervals: level l takes 2**l hops per interval."""
if isinstance(n, bool) or not isinstance(n, Integral) or n < 0:
raise ValueError("n must be a nonnegative integer")
with self._lock, self.ctx.activate():
self._require("ready")
if n == 0:
return
root = self._levels[0]
kw = dict(block=self._block, stream=self._stream)
try:
self._status = "advancing"
self._exchange_valid = False
for _ in range(n):
if self._observe_raw_exchange: # stream-ordered: holds the last interval only
if self._observe_raw_exchange:
self._exchange_valid = False
for b in self._level_boundaries:
if b is not None:
cuda.memset_d8_async(b.interval_integral, 0,
len(b.tables["cell"]) * 32, self._stream)
self._hop(0, root.current, root.spare, **kw)
root.current, root.spare = root.spare, root.current # host handles only
self._stream.synchronize()
self._step_count += n
self._exchange_valid = self._observe_raw_exchange
self._stream.synchronize()
root.current, root.spare = root.spare, root.current
self._step_count += 1
self._exchange_valid = self._observe_raw_exchange
self._status = "ready"
except BaseException:
self._window_valid = False
self._status = "failed"
raise
def start_exchange_window(self):
"""Zero the window ledgers and open a window at the current completed step count."""
with self._lock, self.ctx.activate():
self._require("ready")
if not self._observe_window:
raise RuntimeError("window exchange observation is disabled")
self._window_valid = False
try:
for b in self._level_boundaries:
if b is not None and b.window_integral is not None:
cuda.memset_d8(b.window_integral, 0, len(b.tables["cell"]) * 32)
except BaseException:
self._status = "failed"
raise
self._window_start = self._step_count
self._window_valid = True
def read_exchange_window(self, body_id, *, level=1):
"""Non-destructive read of [window start, current step count] in base-lattice units."""
with self._lock, self.ctx.activate():
self._require("ready")
if not self._observe_window:
raise RuntimeError("window exchange observation is disabled")
if not self._window_valid:
raise RuntimeError("no valid exchange window; call start_exchange_window()")
duration = self._step_count - self._window_start
if duration < 1:
raise RuntimeError("exchange window requires at least one completed base interval")
if (isinstance(level, bool) or not isinstance(level, Integral)
or not 0 <= level < len(self._levels)):
raise ValueError("level must name an existing level")
b = self._level_boundaries[level]
if (b is None or isinstance(body_id, bool) or not isinstance(body_id, Integral)
or not 0 <= body_id < len(b.host.objects)):
raise ValueError("body_id must name a body on this level")
rows = np.empty((len(b.tables["cell"]), 4), np.float64)
try:
cuda.memcpy_dtoh(rows, b.window_integral)
except BaseException:
self._status = "failed"
raise
return summarize_exchange(rows, b.tables["body_id"], b.tables["aperture"],
body_id=body_id, level=level, h=self._exchange_spacing[level],
time_end=self._step_count, window="window",
reference_center=self._exchange_centers[level][body_id],
duration=duration)
def read_raw_exchange(self, body_id, *, level=1, window="last_base_step"):
"""Read raw boundary reaction about its geometric center in the root edge frame.
@@ -918,7 +885,6 @@ class FixedPatchStepper:
if b is not None:
b.flags = b.device = b.kernel = None
b.last_raw = b.interval_integral = b.accumulate = None
b.window_integral = None
self._levels.clear()
self._pairs = []
if getattr(self, "_fine_curved", None) is not None:
+4 -51
View File
@@ -8,7 +8,7 @@ from dataclasses import dataclass, replace
import numpy as np
from .blocks import blocked_link_tables
from ..body.jets import _surface_links
from ..body.jets import jet_link_shapes, surface_link_velocity
from .descriptors import D2Q9_EX, D2Q9_EY
from .refinement import ACTIVE, OUTER_GHOST
@@ -66,50 +66,6 @@ def blocked_base_tables(patch, layout, order, wall_aware=False, *, composed_kind
blocked_kind(layout, dense_kind))
def projected_explosion_tables(pair, parent_layout, composed_parent_kind):
"""Experimental transverse linear explosion, in the pair's existing row order.
Schornbaum & Rüde (2016), arXiv:1508.07982, Algorithm 2, following
Chen et al., doi:10.1016/j.physa.2005.09.036. Independently implemented
from the mathematics: centered gradients, perpendicular projection and
child offsets +/-1/4. The kernel applies the projection per population.
Only original explosion donors that are owned, uncovered ACTIVE cells
may supply gradients. Both axis neighbors must be legal; otherwise repeat
the center twice for zero slope. Owner-core addresses avoid stale halos.
The same rule applies to scalar windows, unions and every adjacent pair;
nested layouts retain their own per-axis (usually nonperiodic) topology.
"""
ny, nx = pair.coarse_shape
kind = np.asarray(composed_parent_kind)
if parent_layout.shape != pair.coarse_shape or kind.shape != pair.coarse_shape:
raise ValueError("projected explosion parent layout/kind has the wrong shape")
donor = pair.ghost_coarse.astype(np.intp)
legal = np.zeros(ny * nx, bool)
legal[donor] = True
legal &= (parent_layout.owned.ravel() & ~pair.covered.ravel()
& (kind.ravel() == ACTIVE))
if not legal[donor].all():
raise ValueError("projected explosion donors need owned uncovered parent ACTIVE cells")
y, x = np.divmod(donor, nx)
neighbors = np.repeat(donor[:, None], 4, axis=1)
for axis, (dx, dy) in enumerate(((1, 0), (0, 1))):
minus = ((y - dy) % ny) * nx + (x - dx) % nx
plus = ((y + dy) % ny) * nx + (x + dx) % nx
valid = legal[minus] & legal[plus]
if not parent_layout.periodic[axis]:
coordinate, extent = (x, nx) if axis == 0 else (y, ny)
valid &= (coordinate > 0) & (coordinate < extent - 1)
neighbors[valid, 2 * axis] = minus[valid]
neighbors[valid, 2 * axis + 1] = plus[valid]
fy, fx = np.divmod(pair.ghost_fine, pair.fine_shape[1])
ox, oy = pair.fine_origin
offsets = np.column_stack((ox + (fx + .5) / 2 - (x + .5),
oy + (fy + .5) / 2 - (y + .5)))
return (np.ascontiguousarray(parent_layout.owner.ravel()[neighbors], dtype=np.uint64),
np.ascontiguousarray(offsets, dtype=np.float32))
# Required device columns plus optional actuation; observation arms are uploaded
# separately only when enabled. body_id/aperture remain host reduction metadata.
BLOCKED_BOUNDARY_DEVICE_COLUMNS = ("block", "cell", "dir", "q", "fallback", "surface_velocity")
@@ -129,7 +85,6 @@ class _BlockedBoundary:
last_raw: object = None
interval_integral: object = None
accumulate: object = None
window_integral: object = None # optional observation-window FP64 ledger
def _prepare_blocked_boundary(boundary, layout, cfg, jets, *, _shifted_blocked=False):
@@ -142,13 +97,11 @@ def _prepare_blocked_boundary(boundary, layout, cfg, jets, *, _shifted_blocked=F
tables.update(body_id=np.asarray(body_id, np.int32), rx=np.asarray(rx, np.float32),
ry=np.asarray(ry, np.float32), aperture=np.zeros(len(dirs), bool))
actuated = bool(jets) or any(o.surface_actuation is not None for o in boundary.objects)
jet_cache = None
if actuated:
cfg = replace(cfg)
cfg.validate()
tables["surface_velocity"], tables["aperture"], shape, jet_id = _surface_links(
tables["surface_velocity"], tables["aperture"] = surface_link_velocity(
boundary.objects, cfg, boundary.flags, fluid_idx, dirs, body_id, rx, ry,
jets=jets, hits=boundary.curved_hits, _shifted_blocked=_shifted_blocked)
if jets:
jet_cache = (shape, jet_id)
return _BlockedBoundary(boundary, tables, jets, actuated, jet_cache)
return _BlockedBoundary(boundary, tables, jets, actuated,
jet_link_shapes(jets, boundary.curved_hits) if jets else None)
+8 -17
View File
@@ -336,19 +336,13 @@ class Simulation:
def read_surface_exchange(self, id: int):
"""Latest step's wall/aperture (Fx,Fy,Tz) exchange, not total nozzle force.
Supports SurfaceActuation and Jet-driven bodies, including zero-amplitude
apertures. Passive bodies without an aperture are rejected.
Unlike read_body this is not accumulated/time-averaged. It resets after
geometry sync or checkpoint load; advance before interpreting it.
"""
obj = self.bodies.get(id)
curved = self.field.curved
has_aperture = (curved.aperture is not None
and np.any(curved.aperture & (curved.body_id == id)))
if obj.surface_actuation is None and not has_aperture:
if self.bodies.get(id).surface_actuation is None:
raise ValueError("Body has no surface actuation")
self.stream.synchronize()
return curved.surface_exchange(id)
return self.field.curved.surface_exchange(id)
@context_bound
def read_body(self, id: int, *,
@@ -457,21 +451,18 @@ class Simulation:
@context_bound
def initialize(self):
"""Initialize flow field and sync objects to GPU."""
self._assert_allocation_contract()
# Validate the complete geometry before flags, populations or modules
# change. Compact-list construction publishes no outputs on failure.
next_flags = self.bodies.build_flags(self.field.build_channel_flags())
if self.bodies.jets and self.bodies.count == 0:
raise ValueError("Jets need bodies with cut links")
self.bodies.build_compact_lists_for(self.bodies.objects, next_flags)
# Recompile if objects were added after construction.
self._assert_allocation_contract()
if self._read_generated_n_objects() != self.bodies.count:
self.recompile()
self.field.flag = next_flags
self.field.flag = self.field.build_channel_flags()
if self.bodies.count > 0:
self.field.flag = self.bodies.build_flags(self.field.flag)
self.field.upload_flags()
if self.bodies.jets and self.bodies.count == 0:
raise ValueError("Jets need bodies with cut links")
self.stepper.initialize()
if self.bodies.count > 0:
self.bodies.sync_to_gpu(self.field, rebuild_flags=False)
@@ -1,66 +0,0 @@
"""Checkpoint save -> load round trip for a scene with passive bodies. Requires GPU.
A fresh Simulation with the same scene, restored from the checkpoint must hold the same populations,
flags, body count and step count, and must stay bitwise identical to the original
after both continue for the same number of steps. Checkpoints with Jets are
refused (documented limitation) and are not covered here.
"""
import os
import tempfile
import unittest
import numpy as np
from ._cases import make_sim
# Three separated cylinders (pinball-like); cut-link receivers stay in bulk fluid.
CIRCLES = (((40, 32), 6), ((56, 22), 6), ((56, 42), 6))
def scene():
sim = make_sim()
sim.initialize()
for center, radius in CIRCLES:
sim.add_body("circle", center=center, radius=radius)
sim.sync_bodies()
return sim
class CheckpointBodiesGPU(unittest.TestCase):
def test_round_trip_is_bitwise_and_continues_identically(self):
sim = scene()
sim.run(60)
with tempfile.NamedTemporaryFile(suffix=".h5", delete=False) as f:
path = f.name
restored = None
try:
saved = sim.save_checkpoint(path)
# The loader requires the same body count and geometry: rebuild the scene.
restored = scene()
restored.load_checkpoint(saved)
self.assertEqual(restored.bodies.count, len(CIRCLES))
self.assertEqual(restored.stepper.step_count, sim.stepper.step_count)
np.testing.assert_array_equal(restored.get_flags(), sim.get_flags())
a, b = sim.get_ddf(), restored.get_ddf()
self.assertEqual(a.dtype, b.dtype)
self.assertEqual(a.tobytes(), b.tobytes())
sim.run(40)
restored.run(40)
self.assertEqual(restored.stepper.step_count, sim.stepper.step_count)
a, b = sim.get_ddf(), restored.get_ddf()
self.assertTrue(np.isfinite(a).all())
self.assertEqual(a.tobytes(), b.tobytes())
for i in range(len(CIRCLES)):
self.assertTrue(np.all(np.isfinite(restored.read_force(i))))
finally:
if restored is not None:
restored.close()
os.unlink(path)
sim.close()
if __name__ == "__main__":
unittest.main()
+3 -8
View File
@@ -65,14 +65,9 @@ def test_shifted_topology_add_remove_preserves_cache_and_finite_flow(streaming,p
assert np.max(macro['rho'][fluid]) < 1.5
@pytest.mark.parametrize('collision,streaming', [
pytest.param('SRT', 'double_buffer', id='double_buffer'),
pytest.param('SRT', 'esopull', id='esopull'),
pytest.param('TRT', 'double_buffer', id='trt-double_buffer'),
])
def test_checkpoint_exact_continuation_and_controls(collision,streaming,tmp_path):
# Same collision throughout save/load/continue; this is not a method-switch restart.
with make_sim(nx=48,ny=24,streaming=streaming,shifted=True,collision=collision) as sim:
@pytest.mark.parametrize('streaming',['double_buffer','esopull'])
def test_checkpoint_exact_continuation_and_controls(streaming,tmp_path):
with make_sim(nx=48,ny=24,streaming=streaming,shifted=True) as sim:
sim.add_body('circle',center=(14,12),radius=3)
sim.add_body('force_region',center=(30,12),radius=2)
sim.initialize()
+1 -2
View File
@@ -16,7 +16,6 @@
#define NZ 1
#define NT 1
#define COLLISION_MODEL 0
#define BLOCK_ROOT_BC 2
#define USE_LES 0
#define RHO 1.f
#define U0 .025f
@@ -41,7 +40,7 @@ inline float __fdiv_rn(float a,float b){return a/b;}
#include "boundary/inlet_outlet.cuh"
#include "step/one_step_double.cu"
#else
#include "core/cell_kind.cuh"
constexpr unsigned char KIND_OUTER=2;
#include "step/block_channel.cu"
#endif
int main(){
-46
View File
@@ -1,46 +0,0 @@
"""Check historical source manifests against their own recorded commit, not today's checkout."""
import io
from pathlib import Path
import subprocess
import tarfile
ROOT = Path(__file__).resolve().parents[2]
def _require(commit):
"""Missing recorded history is an explicit failure, never a skip or a repin."""
found = subprocess.run(
["git", "-C", str(ROOT), "cat-file", "-e", commit + "^{commit}"],
capture_output=True,
)
if found.returncode:
raise AssertionError(
f"Required history missing: commit {commit} is not in this clone; "
"fetch it instead of repinning or substituting"
)
def tracked(commit, prefix="src"):
"""Tracked paths under prefix at the recorded commit."""
_require(commit)
return set(
subprocess.check_output(
["git", "-C", str(ROOT), "ls-tree", "-r", "--name-only", commit, prefix],
text=True,
).splitlines()
)
def historical_tree(commit, dest):
"""Extract the tracked tree of the recorded commit into dest."""
_require(commit)
archive = subprocess.check_output(
["git", "-C", str(ROOT), "archive", "--format=tar", commit]
)
dest = Path(dest)
dest.mkdir(parents=True, exist_ok=True)
extra = {"filter": "data"} if hasattr(tarfile, "data_filter") else {}
with tarfile.open(fileobj=io.BytesIO(archive)) as tar:
tar.extractall(dest, **extra)
return dest
-104
View File
@@ -1,104 +0,0 @@
"""Host contract of PeriodicBlockStepper.step(n): one stream wait per call (mocked driver)."""
import ast
from contextlib import nullcontext
import importlib.util
from pathlib import Path
import types
from unittest.mock import Mock
import unittest
import numpy as np
from CelerisLab.config import LBMConfig
from CelerisLab.lbm.blocks import BlockLayout2D
def host_module():
"""block_stepper with pycuda/compiler replaced by mocks; no GPU context."""
# Locate the source without importing it: importing block_stepper loads pycuda.driver,
# which CPU-only import-boundary tests (test_build_contracts) must never see.
path = Path(importlib.util.find_spec("CelerisLab.lbm.block_stepper").origin)
tree = ast.parse(path.read_text())
tree.body = [node for node in tree.body if not (
isinstance(node, ast.Import) and any(a.name == "pycuda.driver" for a in node.names)
or isinstance(node, ast.ImportFrom) and node.module == "cuda")]
module = types.ModuleType("CelerisLab.lbm._test_host_block_stepper")
module.__package__ = "CelerisLab.lbm"
module.__dict__.update(cuda=Mock(), compiler=Mock())
exec(compile(tree, str(path), "exec"), module.__dict__)
return module
def backend():
host, log = host_module(), []
host.cuda.mem_get_info.return_value = (1 << 40, 1 << 40)
host.cuda.mem_alloc.side_effect = lambda size: Mock(size=size) # distinct buffers
module = Mock()
module.get_global.return_value = (Mock(), 20)
module.get_function.side_effect = lambda name: Mock(
side_effect=lambda *a, **k: log.append((name, a[:2])))
host.compiler.load_module.return_value = module
cfg = LBMConfig(nx=16, ny=16, viscosity=.1)
run = host.PeriodicBlockStepper(cfg, BlockLayout2D.build(16, 16, 8, 8),
context=Mock(activate=nullcontext, sm_arch="sm_70"))
run._stream.synchronize.side_effect = lambda: log.append(("sync", ()))
run.initialize(np.full((9, 16, 16), 1 / 9))
log.clear()
return run, log
class BlockStepSync(unittest.TestCase):
def test_step_n_waits_once_and_matches_n_single_steps(self):
many, log_many = backend()
many.step(3)
single, log_single = backend()
for _ in range(3):
single.step(1)
names = [name for name, _ in log_many]
self.assertEqual(names.count("sync"), 1)
self.assertEqual(names[-1], "sync")
self.assertEqual(names.count("BlockStep"), 3)
# Same launch order and buffer roles as three step(1) calls.
strip = lambda log: [(n, tuple(id(x) for x in a)) for n, a in log if n != "sync"]
roles = lambda run, log: [(n, tuple({id(run._current): "c", id(run._spare): "s"}.get(x, "-")
for x in ids)) for n, ids in strip(log)]
self.assertEqual([n for n, _ in strip(log_many)], [n for n, _ in strip(log_single)])
self.assertEqual(roles(many, log_many), roles(single, log_single))
self.assertIsNot(many._current, many._spare)
self.assertEqual((many.step_count, single.step_count), (3, 3))
def test_step_zero_launches_and_waits_for_nothing(self):
run, log = backend()
run.step(0)
self.assertEqual((log, run.step_count), ([], 0))
def test_failure_keeps_entry_step_count(self):
run, log = backend()
run.step(1)
run._stream.synchronize.side_effect = RuntimeError("device fault")
with self.assertRaises(RuntimeError):
run.step(2)
self.assertEqual((run.step_count, run._status), (1, "failed"))
def test_launch_failure_mid_batch_fails_closed_without_waiting(self):
run, log = backend()
calls = []
step = run._step
def failing(*args, **kwargs):
calls.append(1)
if len(calls) == 2:
raise RuntimeError("launch failure")
return step(*args, **kwargs)
run._step = failing
with self.assertRaises(RuntimeError):
run.step(3)
self.assertNotIn("sync", [name for name, _ in log])
self.assertEqual((run.step_count, run._status), (0, "failed"))
with self.assertRaises(RuntimeError):
run.download()
if __name__ == "__main__":
unittest.main()
-26
View File
@@ -83,32 +83,6 @@ assert abs(sum(compute_feq_d2q9(1.,0.,0.)) - 1.) < 1e-6
class BuildContracts(unittest.TestCase):
def test_block_root_modes_are_rendered_and_recorded(self):
with tempfile.TemporaryDirectory() as root:
for mode in (0, 1, 2):
with self.subTest(mode=mode):
build = compiler.generate_config(LBMConfig(), build_root=root, block_root_bc=mode)
try:
method = Path(build, "config/config_method.h").read_text()
self.assertIn(f"#define BLOCK_ROOT_BC {mode}\n", method)
metadata = json.loads(Path(build, ".celeris-build").read_text())
self.assertEqual(metadata["block_root_bc"], mode)
self.assertNotIn("block_root_bc", metadata["config"])
finally:
compiler.cleanup_build_dir(build)
build = compiler.generate_config(LBMConfig(), build_root=root)
try:
self.assertEqual(json.loads(Path(build, ".celeris-build").read_text())["block_root_bc"], 0)
finally:
compiler.cleanup_build_dir(build)
def test_invalid_block_root_mode_fails_before_snapshot(self):
for mode in (-1, 3, True, False, 1.0, "1", None):
with self.subTest(mode=mode), patch.object(compiler.tempfile, "mkdtemp") as allocate:
with self.assertRaisesRegex(ValueError, "block_root_bc"):
compiler.generate_config(LBMConfig(), block_root_bc=mode)
allocate.assert_not_called()
@staticmethod
def fake_compile(command, check):
source = Path(command[-3])
+3 -4
View File
@@ -50,7 +50,7 @@ def test_masks_and_minimal_footprint():
def test_actual_closure_read_conflicts():
pair, layout, targets = transfer((6, 6, 18, 14))
validate_channel_transfers(pair, layout, targets)
for target in (layout.slot(int(layout.owner[5, -1]) - 1, 2), # integer under NumPy 1 and 2
for target in (layout.slot(layout.owner[5, -1]-1, 2),
layout.slot(layout.owner[1, 4], 4),
layout.slot(layout.owner[-2, 4], 3)):
with pytest.raises(ValueError, match="immutable"):
@@ -80,8 +80,8 @@ def test_evolved_ghost_snapshot_and_physical_download():
host.cuda.memcpy_dtoh.side_effect = lambda a, ptr: np.copyto(a, storage[id(ptr)])
context = Mock(activate=nullcontext, sm_arch="sm_70")
cfg = LBMConfig(nx=24, ny=20, viscosity=.1, velocity=.025)
backend = host.FixedPatchStepper(cfg, context=context, coarse_boundary="channel",
refinements=[(6, 6, 18, 14)], level_blocks=[(4, 6), (4, 4)])
backend = host.FixedPatchStepper(cfg, (6, 6, 18, 14), context=context,
coarse_boundary="channel", base_block=(4, 6), fine_block=(4, 4))
try:
weights = np.array([4/9]+[1/9]*4+[1/36]*4)[:, None, None]
arrays = [np.broadcast_to(weights, level.shape).copy() for level in backend._levels]
@@ -97,6 +97,5 @@ def test_evolved_ghost_snapshot_and_physical_download():
with pytest.raises(ValueError, match="finite"):
backend.initialize(*arrays)
assert backend._levels[0].kernel == host.compiler.load_module.return_value.get_function("BlockChannelStep")
assert [call.kwargs["block_root_bc"] for call in host.compiler.generate_config.call_args_list] == [2, 0]
finally:
backend.close()
-79
View File
@@ -1,79 +0,0 @@
"""CPU-only contract tests for run_dfg_re20. Not CUDA or DFG acceptance evidence."""
import sys
from pathlib import Path
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
import run_dfg_re20 as v # noqa: E402
import run_refined_dfg as rr # noqa: E402
h, _ = rr.helpers(rr.DEFAULT_SOURCE)
def rows(values, tstar_end=v.TSTAR_MAX):
n = int(round(tstar_end / v.SAMPLE_TSTAR))
return [dict(tstar=(i + 1) * v.SAMPLE_TSTAR, **values(i)) for i in range(n)]
class Re20Contract(unittest.TestCase):
def test_lattice_parameters_and_bounds(self):
root, uniform = v.lattice_parameters(40), v.lattice_parameters(80)
self.assertAlmostEqual(root["tau"], 0.56, places=12)
self.assertAlmostEqual(uniform["tau"], 0.62, places=12)
self.assertEqual((root["steps"], root["interval"]), (240000, 2000))
self.assertEqual((uniform["steps"], uniform["interval"]), (480000, 4000))
self.assertAlmostEqual(v.plan()["refined_fine_tau"], 0.62, places=12)
def test_config_only_changes_viscosity(self):
base = v._re100_config(h, 40, "SRT", "none")
cfg, body, p = v.config(h, 40, "SRT")
self.assertAlmostEqual(cfg["physics"]["viscosity"], 0.01 * 40 / 20, places=15)
self.assertEqual((p["re"], p["viscosity"]), (20.0, cfg["physics"]["viscosity"]))
base[0]["physics"]["viscosity"] = cfg["physics"]["viscosity"]
self.assertEqual(base[0], cfg)
self.assertEqual(base[1], body)
with self.assertRaisesRegex(ValueError, "no Jets"):
v.config(h, 40, "MRT", "zero")
def test_units(self):
s = v.sample([2.0, -0.5], 4, 40, 3.0)
den = rr.SPEED**2 * 40 / 2
self.assertAlmostEqual(s["cd"], 2.0 / 4 / den)
self.assertAlmostEqual(s["cl"], -0.5 / 4 / den)
self.assertAlmostEqual(s["p_diff"], 3.0 * 0.04)
self.assertEqual(s["dp_scaled"], 3.0)
def test_steady_and_verdict(self):
inside = dict(cd=5.58, cl=0.0106, p_diff=0.1174)
out = v.summarize(rows(lambda i: inside))
self.assertEqual((out["status"], out["steady"], out["window_samples"]),
("WITHIN_INTERVALS", True, 21))
self.assertTrue(out["cd_positive"])
self.assertAlmostEqual(out["edge_distance"]["cl"], 0.0002)
self.assertTrue(all(out["verdict"].values()))
low = v.summarize(rows(lambda i: dict(inside, cl=0.0100)))
self.assertEqual(low["status"], "OUTSIDE_INTERVALS")
self.assertFalse(low["verdict"]["cl"])
def test_slow_drift_gives_no_verdict(self):
# Range 2e-5 passes (limit 6e-5); slope * window 2e-5 exceeds 1.2e-5.
out = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106 + 1e-6 * i, p_diff=0.1174)))
self.assertEqual((out["status"], out["verdict"]), ("NOT_STEADY", None))
self.assertLessEqual(out["ranges"]["cl"], out["range_limits"]["cl"])
self.assertGreater(out["drifts"]["cl"], out["slope_limits"]["cl"])
def test_noise_range(self):
quiet = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106 + 2e-5 * (i % 2), p_diff=0.1174)))
self.assertTrue(quiet["steady"]) # within the range limit, no trend
noisy = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106 + 1e-4 * (i % 2), p_diff=0.1174)))
self.assertEqual(noisy["status"], "NOT_STEADY")
self.assertGreater(noisy["ranges"]["cl"], noisy["range_limits"]["cl"])
def test_incomplete_run_gives_no_verdict(self):
out = v.summarize(rows(lambda i: dict(cd=5.58, cl=0.0106, p_diff=0.1174), tstar_end=30))
self.assertEqual((out["status"], out["verdict"]), ("INCOMPLETE", None))
if __name__ == "__main__":
unittest.main()
+1 -43
View File
@@ -8,7 +8,6 @@ import numpy as np
ROOT = Path(__file__).resolve().parents[2]
REPO = ROOT
sys.path.insert(0, str(ROOT / "tests/validation"))
sys.path.insert(0, str(ROOT / "tests/unit"))
sys.path.insert(0, str(REPO / "src"))
from run_jet_circle_cv import (
CX,
@@ -17,10 +16,8 @@ from run_jet_circle_cv import (
case,
state,
cv,
cv_from_macros,
aperture_probe,
measured_aperture_flux,
PIN,
verify,
)
@@ -31,39 +28,6 @@ def eq(rho, u, v):
class Host(unittest.TestCase):
def test_public_macros_pressure_advection_and_cropped_frames(self):
for h, origin, shape, rect in (
(.5, (0., 0.), (48, 64), (5., 4., 25., 20.)),
(.5, (.25, .5), (48, 64), (5.25, 4., 25.25, 20.)),
(.25, (.375, .75), (96, 128), (5.125, 4., 25.125, 20.)),
):
y, x = np.indices(shape)
x = origin[0] + h * (x + .5)
k, u, v = 1e-5, .02, -.01
rho = 1 + 3 * k * x # p=(rho-1)/3=k*x
ux, uy, fluid = np.full(shape, u), np.full(shape, v), np.ones(shape, bool)
result = cv_from_macros(rho, ux, uy, fluid, origin, rect, .004, h)
expected = 320 * np.array([3*k*u, k + 3*k*u*u, 3*k*u*v])
np.testing.assert_allclose(result[1], expected, rtol=1e-11, atol=1e-13)
np.testing.assert_allclose(result, cv(eq(rho, u, v), fluid, origin, rect, .004, h), atol=1e-12)
x0, y0, x1, y1 = np.rint((np.array(rect)-np.tile(origin, 2))/h).astype(int)
selection = np.s_[y0-2:y1+2, x0-2:x1+2]
cropped_origin = np.asarray(origin) + h*np.array([x0-2, y0-2])
cropped = cv_from_macros(rho[selection], ux[selection], uy[selection],
fluid[selection], cropped_origin, rect, .004, h)
np.testing.assert_array_equal(cropped, result)
def test_public_macros_full_normal_viscous_stress(self):
y, x = np.indices((48, 64))
x = (x + .5) * .5
rho, ux, uy, fluid = np.ones_like(x), 1e-4*x*x, np.zeros_like(x), np.ones(x.shape, bool)
args = (rho, ux, uy, fluid, (0., 0.), (5., 4., 25., 20.))
_, viscous = cv_from_macros(*args, .004)
_, inviscid = cv_from_macros(*args, 0.)
np.testing.assert_allclose(viscous-inviscid, [0, -4*.004*1e-4*320, 0], atol=1e-13)
with self.assertRaisesRegex(ValueError, "matching 2D"):
cv_from_macros(rho, ux, uy[:-1], fluid, (0, 0), (5, 4, 25, 20), .004)
def test_descriptor_and_uniform_closed_flux(self):
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
@@ -168,13 +132,7 @@ class Host(unittest.TestCase):
self.assertEqual(a["steps"] * 4, b["steps"])
def test_source_pin(self):
from tempfile import TemporaryDirectory
from historical_tree import historical_tree
with self.assertRaisesRegex(RuntimeError, "Source hash mismatch"):
verify(REPO)
with TemporaryDirectory() as directory:
verify(historical_tree(PIN, directory))
verify(REPO)
if __name__ == "__main__":
-49
View File
@@ -188,55 +188,6 @@ def jet_case(device, jets):
return field, bodies
def test_public_surface_exchange_for_jet_preserves_aperture_at_zero_and_reverse(device):
from CelerisLab.simulation import Simulation
jet = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER, inward=False), "quartic", 2e-3)
field, bodies = jet_case(device, [jet])
sim = SimpleNamespace(bodies=bodies, field=field, _initialized=True,
stream=SimpleNamespace(synchronize=device.synchronize))
cl = field.curved
assert bodies.get(0).surface_actuation is None
aperture, velocity = cl.aperture, cl.surface_velocity.copy()
assert aperture.any() and (~aperture).any()
# Synthetic per-link exchanges test readback/splitting, not kernel physics.
values = np.arange(cl.count * 3, dtype=np.float32).reshape(-1, 3)
device.htod(cl.link_exchange_gpu, values)
for amplitude in (2e-3, 0., -1e-3):
Simulation.set_jet_amplitude(sim, 0, amplitude)
result = Simulation.read_surface_exchange(sim, 0)
assert cl.aperture is aperture
np.testing.assert_array_equal(aperture, cl.jet_id >= 0)
np.testing.assert_allclose(cl.surface_velocity, velocity * (amplitude / 2e-3))
for name, mask in (("wall", ~aperture), ("aperture", aperture)):
np.testing.assert_array_equal(result[name], values[mask].sum(axis=0, dtype=np.float64))
def test_public_surface_exchange_keeps_legacy_and_passive_guards(device):
from CelerisLab.simulation import Simulation
field = make_field(device)
bodies = manager(field, [circle(SurfaceActuation(0., .6, .002), radius=2.)])
bodies.sync_to_gpu(field)
sim = SimpleNamespace(bodies=bodies, field=field,
stream=SimpleNamespace(synchronize=device.synchronize))
for value in Simulation.read_surface_exchange(sim, 0).values():
np.testing.assert_array_equal(value, 0.)
field.curved.free_gpu_columns()
with pytest.raises(RuntimeError, match="diagnostic is allocated"):
Simulation.read_surface_exchange(sim, 0)
# An exchange buffer elsewhere in the scene must not admit a passive body.
for jets in ([], [Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER))]):
field, bodies = jet_case(device, jets)
passive = bodies.add(circle(center=(2., 3.), radius=.6))
bodies.sync_to_gpu(field)
sim.bodies, sim.field = bodies, field
with pytest.raises(ValueError, match="Body has no surface actuation"):
Simulation.read_surface_exchange(sim, passive)
if not jets:
with pytest.raises(ValueError, match="Body has no surface actuation"):
Simulation.read_surface_exchange(sim, 0)
def test_manager_passes_true_hits_and_reuploads_only_velocity(device):
# CPU memory double only: checks host/device bookkeeping, not GPU numerics.
jet = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER, inward=False), "quartic", 2e-3)
+13 -66
View File
@@ -15,10 +15,6 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
from run_uniform_les_flow import source_guard, decode_macros, HERE, HEADER
ROOT = HERE.parents[1]
sys.path.insert(0, str(ROOT / "tests/unit"))
from historical_tree import historical_tree, tracked
A135 = "a135252ae78b47b57d7e77d19001700b3bedac8b"
class GuardTests(unittest.TestCase):
@@ -46,80 +42,31 @@ class GuardTests(unittest.TestCase):
decode_macros(invalid, w, cx, cy)
def test_current_corrected_pins(self):
with TemporaryDirectory() as directory:
root = historical_tree(A135, directory)
with patch("run_uniform_les_flow.subprocess.check_output", return_value="historical-tree\n"):
identity = source_guard(root, "corrected", "a135252")
# base_commit comes from the pins file; the mock only feeds checkout_commit.
self.assertEqual(identity["base_commit"], A135)
self.assertEqual(identity["checkout_commit"], "historical-tree")
self.assertEqual(
source_guard(ROOT, "corrected")["base_commit"],
"cd3a657f9d3833e812c3cca173525f165b48710a",
)
def test_corrected_and_tamper_guard(self):
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
pins = json.loads((HERE / "data/les_flow/source_pins.json").read_text())
with TemporaryDirectory() as directory:
origin = historical_tree(A135, Path(directory) / "origin")
root = Path(directory) / "fixture"
root = Path(directory)
for name in pins["sources"]:
dest = root / name
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(origin / name, dest)
shutil.copyfile(ROOT / name, dest)
# Current production is the corrected variant; archived baseline pins stay frozen.
with patch(
"run_uniform_les_flow.subprocess.check_output",
return_value="isolated-fixture\n",
):
source_guard(root, "corrected", "a135252")
source_guard(root, "corrected")
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
source_guard(root, "baseline", "a135252")
for name in ("kernels/step/one_step_double.cu", "kernels/blocks.cu",
"refinement_stepper.py", "refinement_tables.py"):
target = root / "src/CelerisLab/lbm" / name
original = target.read_bytes()
target.write_bytes(original + b"\n// tamper\n")
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
source_guard(root, "corrected", "a135252")
target.write_bytes(original)
def test_historical_default_and_manifest_unchanged(self):
historical = {
"historical": ("source_pins.json", "87ca033619da1fc02bee822bfc5681b518a92a155c3c3d1dbbe5265c96066b39"),
"dev8403956": ("source_pins_dev8403956.json", "58187908485f2af7c78677d53de98015878372444265ce9e667683b8ef5d410f"),
}
for experiment, (name, expected) in historical.items():
manifest = HERE / "data/les_flow" / name
self.assertEqual(hashlib.sha256(manifest.read_bytes()).hexdigest(), expected)
for variant in ("baseline", "corrected"):
source_guard(root, "baseline")
target = root / "src/CelerisLab/lbm/kernels/step/one_step_double.cu"
target.write_bytes(target.read_bytes() + b"\n// tamper\n")
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
source_guard(ROOT, variant, experiment)
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
source_guard(ROOT, "corrected") # Omission still uses the historical profile.
def test_current_manifest_pins_actual_corrected_tree(self):
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
self.assertEqual(pins["base_commit"], A135)
self.assertEqual(set(pins["sources"]), tracked(A135))
with TemporaryDirectory() as directory:
root = historical_tree(A135, directory)
for name, expected in pins["sources"].items():
self.assertEqual(hashlib.sha256((root / name).read_bytes()).hexdigest(), expected)
self.assertEqual(pins["sources"][HEADER], pins["candidate_header_sha256"])
self.assertNotEqual(pins["sources"][HEADER], pins["baseline_header_sha256"])
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
source_guard(root, "baseline", "a135252")
def test_named_baseline_accepts_only_reversed_header(self):
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
with TemporaryDirectory() as directory:
origin = historical_tree(A135, Path(directory) / "origin")
root = Path(directory) / "fixture"
for name in pins["sources"]:
dest = root / name
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(origin / name, dest)
subprocess.run(["git", "apply", "--reverse", str(HERE / "les/data/smagorinsky_normalization.patch")], cwd=root, check=True)
with patch("run_uniform_les_flow.subprocess.check_output", return_value="baseline-fixture\n"):
source_guard(root, "baseline", "a135252")
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
source_guard(root, "corrected", "a135252")
source_guard(root, "corrected")
def test_gpu_requires_opt_in_before_import(self):
with TemporaryDirectory() as directory:
-394
View File
@@ -1,394 +0,0 @@
"""CPU stencil and actual closed-interface packet budgets; no CUDA claims."""
import numpy as np
import unittest
from pathlib import Path
from dataclasses import replace
import shutil
import subprocess
import tempfile
from CelerisLab.config import LBMConfig
from CelerisLab.lbm.blocks import BlockLayout2D
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
from CelerisLab.lbm.refinement import FixedPatch2D, FixedPatchSet2D, ACTIVE, OUTER_GHOST
from CelerisLab.lbm.refinement_tables import projected_explosion_tables
from CelerisLab.lbm.hierarchy import build_hierarchy
from tests.unit.test_hierarchy import CFG, FOUR
E = np.array([D2Q9_EX, D2Q9_EY]).T
W = np.array([4/9] + [1/9]*4 + [1/36]*4)
def setup(union=False, bounds=(4, 4, 12, 12)):
cfg = LBMConfig(nx=16, ny=16, viscosity=.1)
p = (FixedPatchSet2D.from_config(cfg, [bounds]) if union else
FixedPatch2D.from_config(cfg, bounds))
layout = BlockLayout2D.build(16, 16, 4, 4, periodic=(True, True))
neighbors, offsets = projected_explosion_tables(p, layout, np.where(p.covered, OUTER_GHOST, ACTIVE))
inverse = np.full(layout.nblocks * np.prod(layout.local_shape), -1, dtype=int)
inverse[layout.owner.ravel()] = np.arange(256)
return p, inverse[neighbors], offsets
def reconstruct(c, p, neighbors, offsets, project=True):
# Independent NumPy evaluation of the documented stencil; neighbors have
# been decoded from actual blocked owner IDs, not recomputed geometry.
values = c.reshape(9, -1)[:, neighbors]
g = .5 * (values[:, :, 1::2] - values[:, :, ::2])
if project:
g[1:] -= (np.einsum('qra,qa->qr', g[1:], E[1:]) /
np.sum(E[1:]**2, axis=1)[:, None])[:, :, None] * E[1:, None, :]
out = c.reshape(9, -1)[:, p.ghost_coarse] + np.einsum('qra,ra->qr', g, offsets)
return out, g
def check_legal_footprint_mean_and_characteristic(union, bounds):
p, neighbors, offsets = setup(union, bounds)
assert set(neighbors.ravel()) <= set(p.ghost_coarse)
assert not p.covered.ravel()[neighbors].any()
assert (neighbors >= 0).all()
c = np.random.default_rng(72).normal(size=(9,16,16))
c[:, p.covered] = np.nan # poison every forbidden donor
out, g = reconstruct(c, p, neighbors, offsets)
assert np.isfinite(out).all()
np.testing.assert_allclose(np.einsum('qra,qa->qr', g, E), 0, atol=5e-16)
for d in np.unique(p.ghost_coarse):
rows = p.ghost_coarse == d
assert rows.sum() == 4
np.testing.assert_allclose(out[:, rows].mean(axis=1), c.reshape(9,-1)[:,d], atol=5e-16)
for q in range(1,9):
delta = offsets[rows, None, :] - offsets[None, rows, :]
# Parallel displacements have zero 2D cross product.
parallel = np.isclose(delta[...,0]*E[q,1]-delta[...,1]*E[q,0], 0)
diff = out[q, rows][:,None]-out[q, rows][None,:]
np.testing.assert_allclose(diff[parallel], 0, atol=5e-16)
const = np.broadcast_to(W[:,None,None], (9,16,16)).copy()
constant, _ = reconstruct(const,p,neighbors,offsets)
np.testing.assert_array_equal(constant, np.broadcast_to(W[:,None], constant.shape))
def collide(f, tau):
rho = f.sum(axis=0)
u = np.einsum('qa,q...->a...', E, f) / rho
cu = np.einsum('qa,a...->q...', E, u)
eq = W[:,None] * rho * (1 + 3*cu + 4.5*cu**2 - 1.5*np.sum(u*u,axis=0))
return f + (eq-f)/tau
def totals(c, f, p):
q = c[:,~p.covered].sum(axis=1)+.25*f[:,p.fine_kind==ACTIVE].sum(axis=1)
return np.r_[q.sum(), E.T@q]
def check_full_two_hop_closed_corner_budget(union, collision, bounds):
p, neighbors, offsets = setup(union,bounds)
rng=np.random.default_rng(17)
c=W[:,None,None]*(1+rng.normal(0,.002,(9,16,16)))
f=W[:,None,None]*(1+rng.normal(0,.002,(9,*p.fine_shape)))
start=totals(c,f,p)
# Count exactly the eight coarse diagonal links whose source is also
# uncovered, but whose midpoint lies on the closed rectangular interface.
q, k=np.divmod(p.coalesce_coarse.astype(np.int64),256)
y,x=np.divmod(k,16)
grazing=~p.covered[y-E[q,1],x-E[q,0]]
assert grazing.sum()==8
c[:,p.covered]=np.nan
for _ in range(8):
f.reshape(9,-1)[:,p.ghost_fine]=reconstruct(c,p,neighbors,offsets)[0]
for _ in range(2):
f=np.array([np.roll(f[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
f[:,p.fine_kind==OUTER_GHOST]=np.nan
if collision:
f[:,p.fine_kind==ACTIVE]=collide(f[:,p.fine_kind==ACTIVE],p.tau_fine)
out=np.array([np.roll(c[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
packets=f.reshape(-1)[p.coalesce_fine].mean(axis=1)
assert np.isfinite(packets[grazing]).all()
out.reshape(-1)[p.coalesce_coarse]=packets
if collision:out[:,~p.covered]=collide(out[:,~p.covered],p.tau_coarse)
out[:,p.covered]=np.nan
c=out
np.testing.assert_allclose(totals(c,f,p),start,rtol=0,atol=2e-12)
def check_projection_is_distinguished_by_characteristic_not_global_mass():
p,n,o=setup()
c=np.random.default_rng(8).normal(size=(9,16,16))
_,g=reconstruct(c,p,n,o,project=False)
assert np.max(abs(np.einsum('qra,qa->qr',g,E))) > .1
class ProjectedExplosionTests(unittest.TestCase):
def test_stencil(self):
for union in (False, True):
for bounds in ((4,4,12,12),(2,3,9,10),(2,2,3,3)):
with self.subTest(union=union,bounds=bounds):
check_legal_footprint_mean_and_characteristic(union,bounds)
def test_closed_interface(self):
for union in (False,True):
for collision in (False,True):
for bounds in ((4,4,12,12),(2,3,14,12),(2,2,3,3)):
with self.subTest(union=union,collision=collision,bounds=bounds):
check_full_two_hop_closed_corner_budget(union,collision,bounds)
def test_projection(self):
check_projection_is_distinguished_by_characteristic_not_global_mass()
def test_affine_and_shift_invariance(self):
p,n,o=setup()
y,x=np.indices((16,16))
c=np.array([.7+q*.01+x*.03+y*.05 for q in range(9)])
out,g=reconstruct(c,p,n,o)
available=n[:,::2]!=n[:,1::2]
gradient=np.array([.03,.05])*available
expected=np.broadcast_to(gradient,(9,*gradient.shape)).copy()
expected[1:]-=(np.einsum('qra,qa->qr',expected[1:],E[1:])/
np.sum(E[1:]**2,axis=1)[:,None])[:,:,None]*E[1:,None,:]
np.testing.assert_allclose(g,expected,atol=2e-16)
shifted,_=reconstruct(c-W[:,None,None],p,n,o)
np.testing.assert_allclose(out-W[:,None],shifted,atol=5e-16)
class ProjectedKernelTests(unittest.TestCase):
@unittest.skipUnless(shutil.which("g++"), "g++ is required for actual-kernel CPU arithmetic")
def test_actual_kernel_fp32_shifted_and_unshifted(self):
p, neighbors, offsets = setup(True)
parent = BlockLayout2D.build(16,16,4,4,periodic=(True,True))
child = p.fine_layout((8,4)) # deliberately unequal parent/child strides
ids, offsets = projected_explosion_tables(p,parent,np.where(p.covered,OUTER_GHOST,ACTIVE))
cells = int(np.prod(parent.local_shape))
targets = child.owner.ravel()[p.ghost_fine]
donors = parent.owner.ravel()[p.ghost_coarse]
source = (Path(__file__).resolve().parents[2] /
"src/CelerisLab/lbm/kernels/step/block_transfer.cu").read_text()
kernel = source.split('extern "C" __global__ void BlockExplodeProjected(',1)[1]
kernel = 'void BlockExplodeProjected(' + kernel.split('// Targets are receiver-local SOURCE slots',1)[0]
def array(name, values, kind):
v = np.asarray(values).ravel()
text = ','.join(str(int(x)) if kind=='uint64_t' else float(x).hex()+'f' for x in v)
return f"{kind} {name}[] = {{{text}}};\n"
for shifted in (False,True):
c=np.random.default_rng(11).normal(0,.001,(9,16,16))
if not shifted:c+=W[:,None,None]
c=c.astype(np.float32)
packed=np.full(parent.nblocks*9*cells,np.nan,np.float32)
for q in range(9):packed[parent.slot(parent.owner.ravel(),q)]=c[q].ravel()
# All halo/covered inputs are poisoned; kernel must use legal cores.
for q in range(9):packed[parent.slot(parent.owner.ravel()[p.covered.ravel()],q)]=np.nan
# Hex initializer needs a portable expression for poisoned NaNs.
data=array('coarse',packed,'float').replace('nanf','NAN')
code = '#include <cstdint>\n#include <cmath>\n#include <cstdio>\nusing fpxx=float;\n'
code += f'constexpr unsigned NQ=9, TOTAL_CELLS={int(np.prod(child.local_shape))};\n'
code += 'struct Id {unsigned x;}; Id blockIdx{0},blockDim{1},threadIdx{0};\n'
code += 'unsigned index_f(unsigned k,unsigned q){return q*TOTAL_CELLS+k;}\n'
code += 'int d_cx[]={0,1,-1,0,0,1,-1,1,-1},d_cy[]={0,0,0,1,-1,1,-1,-1,1};\n'+kernel
code += data+array('target',targets,'uint64_t')+array('donor',donors,'uint64_t')
code += array('neighbor',ids,'uint64_t')+array('offset',offsets,'float')
code += f'float fine[{child.nblocks*9*int(np.prod(child.local_shape))}]={{}};\n'
code += f'int main(){{for(unsigned r=0;r<{len(targets)};++r){{threadIdx.x=r;BlockExplodeProjected(coarse,fine,target,donor,{len(targets)},{cells},neighbor,offset);}}'
code += f'for(unsigned q=0;q<9;++q)for(unsigned r=0;r<{len(targets)};++r){{auto t=target[r];printf("%.9g\\n",fine[(t/TOTAL_CELLS)*9*TOTAL_CELLS+q*TOTAL_CELLS+t%TOTAL_CELLS]);}}}}'
with tempfile.TemporaryDirectory() as folder:
src=Path(folder)/'kernel.cpp'; binary=Path(folder)/'kernel'
src.write_text(code)
subprocess.run(['g++','-std=c++17','-O2',str(src),'-o',str(binary)],check=True,capture_output=True)
result=np.fromstring(subprocess.check_output([str(binary)],text=True),sep=' ').reshape(9,-1)
expected,_=reconstruct(c.astype(float),p,neighbors,offsets)
np.testing.assert_allclose(result,expected,atol=6e-8,rtol=2e-7)
UNIONS = [
[(8, 8, 24, 20)],
[(8, 8, 16, 20), (16, 8, 24, 20)], # abutting
[(8, 8, 18, 20), (14, 8, 24, 20)], # overlap
[(8, 8, 16, 20), (17, 8, 24, 20)], # gap 1
[(8, 8, 16, 20), (18, 8, 24, 20)], # gap 2
[(8, 8, 16, 20), (19, 8, 24, 20)], # gap 3
[(8, 8, 20, 14), (8, 14, 14, 22)], # L concavity
[(4, 4, 10, 10), (18, 16, 26, 26)], # separated
[(4, 4, 5, 7), (4, 5, 7, 7)], # known concave exit/re-entry path
]
def decode(layout, neighbors):
inverse = np.full(layout.hole, -1, dtype=int)
inverse[layout.owner[layout.owned]] = np.flatnonzero(layout.owned)
return inverse[neighbors]
def check_pair_stencil(pair, layout, kind):
ids, offsets = projected_explosion_tables(pair, layout, kind)
neighbors = decode(layout, ids)
legal = set(map(int, pair.ghost_coarse))
assert len(np.unique(pair.ghost_fine)) == len(pair.ghost_fine)
assert set(neighbors.ravel()) <= legal
assert layout.owned.ravel()[neighbors].all()
assert (kind.ravel()[neighbors] == ACTIVE).all()
assert not pair.covered.ravel()[neighbors].any()
ny, nx = pair.coarse_shape
for row, donor in enumerate(pair.ghost_coarse):
y, x = divmod(int(donor), nx)
for axis, (dx, dy) in enumerate(((1, 0), (0, 1))):
ends = [(x - dx, y - dy), (x + dx, y + dy)]
expected = []
for xx, yy in ends:
outside = not (0 <= xx < nx and 0 <= yy < ny)
if outside and not layout.periodic[axis]:
break
cell = (yy % ny) * nx + xx % nx
if cell not in legal:
break
expected.append(cell)
if len(expected) != 2:
expected = [int(donor)] * 2
np.testing.assert_array_equal(neighbors[row, 2*axis:2*axis+2], expected)
fy, fx = divmod(int(pair.ghost_fine[row]), pair.fine_shape[1])
expected = [pair.fine_origin[0]+(fx+.5)/2-(x+.5),
pair.fine_origin[1]+(fy+.5)/2-(y+.5)]
np.testing.assert_array_equal(offsets[row], expected)
assert set(offsets.ravel()) == {-.25, .25}
assert ids.dtype == np.uint64 and ids.flags.c_contiguous
assert offsets.dtype == np.float32 and offsets.flags.c_contiguous
return neighbors, offsets
class GenericProjectedPairs(unittest.TestCase):
def test_single_window_and_singleton_union_exact_stencils(self):
for bounds in ((4,4,12,12), (2,3,9,10), (2,2,3,3)):
a, an, ao = setup(False, bounds)
b, bn, bo = setup(True, bounds)
np.testing.assert_array_equal(a.ghost_coarse, b.ghost_coarse)
np.testing.assert_array_equal(an, bn)
np.testing.assert_array_equal(ao, bo)
# Coordinates differ between scalar and union arrays; physical rows don't.
ay, ax = np.divmod(a.ghost_fine, a.fine_shape[1])
by, bx = np.divmod(b.ghost_fine, b.fine_shape[1])
np.testing.assert_array_equal(ax + 2*a.fine_origin[0], bx)
np.testing.assert_array_equal(ay + 2*a.fine_origin[1], by)
def test_union_decomposition_permutation_and_topologies(self):
cfg = LBMConfig(nx=30, ny=30, viscosity=.1)
layout = BlockLayout2D.build(30,30,6,5,periodic=(True,True))
stencils = []
for rectangles in UNIONS:
pair = FixedPatchSet2D.from_config(cfg, rectangles)
kind = np.where(pair.covered, OUTER_GHOST, ACTIVE)
n, o = check_pair_stencil(pair, layout, kind)
other = FixedPatchSet2D.from_config(cfg, rectangles[::-1])
on, oo = projected_explosion_tables(other, layout, kind)
np.testing.assert_array_equal(n, decode(layout, on))
np.testing.assert_array_equal(o, oo)
stencils.append((pair.ghost_fine, pair.ghost_coarse, n, o))
c = np.random.default_rng(92).normal(size=(9,30,30))
c[:, pair.covered] = np.nan
values, gradients = reconstruct(c, pair, n, o)
self.assertTrue(np.isfinite(values).all())
np.testing.assert_allclose(np.einsum('qra,qa->qr', gradients, E),0,atol=1e-15)
# q0 keeps its full gradient; no division by zero or forced zero.
expected_q0 = .5*(c[0].ravel()[n[:,1::2]]-c[0].ravel()[n[:,::2]])
np.testing.assert_array_equal(gradients[0], expected_q0)
self.assertGreater(np.max(abs(gradients[0])), .1)
for donor in np.unique(pair.ghost_coarse):
rows = pair.ghost_coarse == donor
self.assertEqual(rows.sum(),4)
np.testing.assert_allclose(values[:,rows].mean(axis=1),c.reshape(9,-1)[:,donor],atol=1e-15)
for q in range(1,9):
delta = o[rows,None,:]-o[None,rows,:]
parallel = delta[...,0]*E[q,1] == delta[...,1]*E[q,0]
diff = values[q,rows][:,None]-values[q,rows][None,:]
np.testing.assert_allclose(diff[parallel],0,atol=1e-15)
for split in stencils[1:3]:
for expected, actual in zip(stencils[0],split):
np.testing.assert_array_equal(actual,expected)
def test_nested_scalar_origins_sparse_holes_and_unequal_strides(self):
cases = [(CFG, FOUR), (LBMConfig(nx=16,ny=16,viscosity=.02),
[(4,4,12,12),(8,8,16,16),(8,8,16,16)])]
for cfg, refs in cases:
hierarchy = build_hierarchy(cfg,refs)
for index,pair in enumerate(hierarchy.pairs):
if index == 0:
layout = BlockLayout2D.build(cfg.ny,cfg.nx,4,4,periodic=(True,True))
else:
prior = hierarchy.pairs[index-1]
layout = (prior.fine_layout((4,8)) if isinstance(prior,FixedPatchSet2D)
else BlockLayout2D.build(*prior.fine_shape,4,8,periodic=(False,False)))
kind = hierarchy.kinds[index]
n,o = check_pair_stencil(pair,layout,kind)
c = np.random.default_rng(index).normal(size=(9,*pair.coarse_shape))
c[:,~layout.owned | (kind != ACTIVE)] = np.nan
self.assertTrue(np.isfinite(reconstruct(c,pair,n,o)[0]).all())
if index and isinstance(pair,FixedPatchSet2D):
self.assertTrue((~layout.owned).any())
self.assertEqual(layout.periodic, (index == 0, index == 0))
def test_periodicity_is_per_axis_and_missing_pairs_repeat_center(self):
cfg=LBMConfig(nx=16,ny=16,viscosity=.1)
p=FixedPatch2D.from_config(cfg,(2,2,14,14))
kind=np.where(p.covered,OUTER_GHOST,ACTIVE)
results=[]
for periodic in ((False,False),(True,False),(False,True),(True,True)):
layout=BlockLayout2D.build(16,16,4,4,periodic=periodic)
n,_=check_pair_stencil(p,layout,kind)
results.append(n)
y,x=np.divmod(p.ghost_coarse,16)
for axis,coordinate in enumerate((x,y)):
edge=(coordinate==0)|(coordinate==15)
if not periodic[axis]:
np.testing.assert_array_equal(n[edge,2*axis:2*axis+2],
np.repeat(p.ghost_coarse[edge,None],2,axis=1))
self.assertTrue((results[0]!=results[1]).any())
self.assertTrue((results[0]!=results[2]).any())
self.assertTrue((results[1]!=results[3]).any())
def test_invalid_parent_donors_reject_before_owner_lookup(self):
p,_,_=setup()
layout=BlockLayout2D.build(16,16,4,4)
kind=np.where(p.covered,OUTER_GHOST,ACTIVE)
for nonactive in (1,2,255):
bad=kind.copy(); bad.ravel()[p.ghost_coarse[0]]=nonactive
with self.assertRaisesRegex(ValueError,'ACTIVE'):
projected_explosion_tables(p,layout,bad)
with self.assertRaisesRegex(ValueError,'shape'):
projected_explosion_tables(p,layout,kind[:1])
hole_layout=BlockLayout2D.build(16,16,4,4,origins=[(0,0)])
with self.assertRaisesRegex(ValueError,'owned'):
projected_explosion_tables(p,hole_layout,kind)
def test_generic_two_hop_mass_and_momentum_budget(self):
cfg=LBMConfig(nx=30,ny=30,viscosity=.1)
layout=BlockLayout2D.build(30,30,6,5)
grazing_counts=set()
for rectangles in UNIONS:
pair=FixedPatchSet2D.from_config(cfg,rectangles)
ids,o=projected_explosion_tables(pair,layout,np.where(pair.covered,OUTER_GHOST,ACTIVE))
n=decode(layout,ids)
q,k=np.divmod(pair.coalesce_coarse.astype(np.int64),900)
y,x=np.divmod(k,30)
grazing=~pair.covered[(y-E[q,1])%30,(x-E[q,0])%30]
grazing_counts.add(int(grazing.sum()))
self.assertTrue(grazing.any())
for collision in (False,True):
rng=np.random.default_rng(17)
c=W[:,None,None]*(1+rng.normal(0,.002,(9,30,30)))
f=W[:,None,None]*(1+rng.normal(0,.002,(9,*pair.fine_shape)))
start=totals(c,f,pair)
c[:,pair.covered]=np.nan
for _ in range(4):
f.reshape(9,-1)[:,pair.ghost_fine]=reconstruct(c,pair,n,o)[0]
for _ in range(2):
f=np.array([np.roll(f[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
f[:,pair.fine_kind==OUTER_GHOST]=np.nan
if collision:
f[:,pair.fine_kind==ACTIVE]=collide(f[:,pair.fine_kind==ACTIVE],pair.tau_fine)
out=np.array([np.roll(c[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
packets=f.reshape(-1)[pair.coalesce_fine].mean(axis=1)
self.assertTrue(np.isfinite(packets[grazing]).all())
out.reshape(-1)[pair.coalesce_coarse]=packets
if collision:
out[:,~pair.covered]=collide(out[:,~pair.covered],pair.tau_coarse)
out[:,pair.covered]=np.nan
c=out
np.testing.assert_allclose(totals(c,f,pair),start,rtol=0,atol=4e-12)
self.assertGreater(len(grazing_counts),1) # not a hard-coded eight-corner oracle
-162
View File
@@ -1,162 +0,0 @@
"""Host-only checks for the opt-in CUDA gate; never imports or compiles CUDA."""
from copy import deepcopy
import importlib.util
from pathlib import Path
import unittest
from unittest.mock import patch
import numpy as np
from CelerisLab.config import LBMConfig
from CelerisLab.lbm.blocks import BlockLayout2D
from CelerisLab.lbm.hierarchy import build_hierarchy
from CelerisLab.lbm.refinement import ACTIVE, OUTER_GHOST, FixedPatch2D, FixedPatchSet2D
from CelerisLab.lbm.refinement_tables import projected_explosion_tables
from tests.unit.test_hierarchy import CFG, FOUR
from tests.unit.test_projected_explosion import decode, reconstruct, totals
PATH = Path(__file__).resolve().parents[1] / 'validation/multigrid/check_projected_pairs.py'
spec = importlib.util.spec_from_file_location('projected_pair_gate', PATH)
gate = importlib.util.module_from_spec(spec)
spec.loader.exec_module(gate)
class ProjectedGateOracle(unittest.TestCase):
def check_pair(self, pair, layout, kind):
ids, offsets = projected_explosion_tables(pair, layout, kind)
neighbors = decode(layout, ids)
coarse = np.random.default_rng(731).normal(size=(9, *pair.coarse_shape))
legal = np.zeros(pair.coarse_shape, bool)
legal.ravel()[pair.ghost_coarse] = True
coarse[:, ~legal] = np.nan
expected, _ = reconstruct(coarse, pair, neighbors, offsets)
# The coordinate oracle must not accidentally share production tables.
with patch('CelerisLab.lbm.refinement_tables.projected_explosion_tables',
side_effect=AssertionError('oracle used production stencil')):
actual = gate.independent_explosion(coarse, pair, layout, kind)
np.testing.assert_allclose(actual, expected, rtol=0, atol=2e-15)
center = coarse.reshape(9, -1)[:, pair.ghost_coarse]
self.assertTrue(np.isfinite(actual).all())
self.assertGreater(np.max(abs(actual[0] - center[0])), .01)
self.assertGreater(np.max(abs(actual[1:] - center[1:])), .01)
return coarse, neighbors, offsets, actual
def test_scalar_union_nested_sparse_and_unequal_strides(self):
def fine_layout(pair, block):
return (pair.fine_layout(block) if isinstance(pair, FixedPatchSet2D) else
BlockLayout2D.build(*pair.fine_shape, *block, periodic=(False,False)))
small = LBMConfig(nx=16, ny=16, viscosity=.02)
fixtures = [(small, [(4,4,12,12), (8,8,16,16), (8,8,16,16)]),
(CFG, FOUR), (small, [[(4,4,5,7), (4,5,7,7)]])]
for cfg, refs in fixtures:
hierarchy = build_hierarchy(cfg, refs)
for index, pair in enumerate(hierarchy.pairs):
block, child_block = ((4,4), (4,8)) if index % 2 == 0 else ((4,8), (4,4))
layout = (BlockLayout2D.build(cfg.ny, cfg.nx, *block) if index == 0
else fine_layout(hierarchy.pairs[index-1], block))
with self.subTest(refs=refs, pair=index):
self.assertNotEqual(np.prod(layout.local_shape),
np.prod(fine_layout(pair, child_block).local_shape))
if index and isinstance(pair, FixedPatchSet2D):
self.assertTrue((~layout.owned).any())
self.check_pair(pair, layout, hierarchy.kinds[index])
def test_axis_periodicity_and_mean_preserving_mutants(self):
pair = FixedPatch2D.from_config(LBMConfig(nx=16,ny=16,viscosity=.1), (2,2,14,14))
kind = np.where(pair.covered, OUTER_GHOST, ACTIVE)
outputs = []
for periodic in ((False,False), (True,False), (False,True), (True,True)):
layout = BlockLayout2D.build(16,16,4,4,periodic=periodic)
coarse, neighbors, offsets, actual = self.check_pair(pair, layout, kind)
outputs.append(actual)
donors, first, inverse = np.unique(pair.ghost_coarse, return_index=True, return_inverse=True)
wrong = neighbors[first[np.roll(np.arange(len(donors)), 1)]][inverse]
for bad_neighbors, bad_offsets in ((neighbors, -offsets), (wrong, offsets)):
mutant, _ = reconstruct(coarse, pair, bad_neighbors, bad_offsets)
self.assertGreater(np.max(abs(mutant - actual)), .01)
for donor in donors:
rows = pair.ghost_coarse == donor
self.assertEqual(rows.sum(), 4)
np.testing.assert_allclose(mutant[:,rows].mean(axis=1),
coarse.reshape(9,-1)[:,donor], rtol=0, atol=1e-15)
bad_kind = kind.copy(); bad_kind.ravel()[pair.ghost_coarse[0]] = OUTER_GHOST
with self.assertRaisesRegex(AssertionError, 'illegal center donor'):
gate.independent_explosion(coarse, pair, layout, bad_kind)
for i, j in ((0,1), (0,2), (1,3), (2,3)):
self.assertGreater(np.max(abs(outputs[i] - outputs[j])), .01)
def test_concave_replay_preserves_existing_fp64_budget(self):
cfg, refs, blocks = gate.cases()['concave']
hierarchy = build_hierarchy(cfg, refs); pair = hierarchy.pairs[0]
layout = BlockLayout2D.build(cfg.ny, cfg.nx, *blocks[0])
rng = np.random.default_rng(17)
values = tuple(gate.W[:,None,None] * (1 + rng.normal(0,.002,(9,*shape)))
for shape in (pair.coarse_shape, pair.fine_shape))
start = totals(*values, pair)
values[0][:,pair.covered] = np.nan
for _ in range(8):
values = gate.replay_two_hops(values, pair, layout, hierarchy.kinds[0])
np.testing.assert_allclose(totals(*values, pair), start, rtol=0, atol=4e-12)
self.assertEqual(gate.REPLAY_ATOL, 3e-6) # CUDA replay bound is separate.
class ProjectedGateReceipts(unittest.TestCase):
def test_digest_exact_finite_bits_and_nan_masks(self):
values = np.array([0., -0., np.nan, 1.], dtype=np.float32)
self.assertEqual(gate.digest(values), gate.digest(values.copy()))
payload = values.copy(); payload.view(np.uint32)[2] = 0x7fc00001
self.assertEqual(gate.digest(values), gate.digest(payload))
variants = [np.array([-0., -0., np.nan, 1.], np.float32),
np.array([0., np.nan, -0., 1.], np.float32),
np.array([0., -0., np.nan, np.nextafter(np.float32(1), np.float32(2))], np.float32),
values.reshape(2,2), values.astype(np.float64)]
for changed in variants:
self.assertNotEqual(gate.digest(values), gate.digest(changed))
with self.assertRaisesRegex(AssertionError, 'infinite'):
gate.digest(np.array([np.inf]))
def test_snapshot_negative_controls_and_exact_dev_options(self):
snapshot = [dict(step=0, levels=[dict(raw='raw',public='public',active=4)],
mass_momentum=[1.,0.,0.],observed_level_hops=[0,0])]
gate.compare_snapshots(snapshot, deepcopy(snapshot))
diagnostic = deepcopy(snapshot); diagnostic[0]['mass_momentum'][0] = 2.
gate.compare_snapshots(snapshot, diagnostic) # Diagnostics are not bitwise gate fields.
mutants = [[]]
for key, value in (('raw','changed'), ('public','changed'), ('active',3)):
changed = deepcopy(snapshot); changed[0]['levels'][0][key] = value
mutants.append(changed)
changed = deepcopy(snapshot); changed[0]['step'] = 1; mutants.append(changed)
changed = deepcopy(snapshot); changed[0]['observed_level_hops'] = [0,1]; mutants.append(changed)
for changed in mutants:
with self.assertRaises(AssertionError):
gate.compare_snapshots(snapshot, changed)
self.assertEqual(gate.default_options('omitted'), {})
self.assertEqual(gate.default_options('constant'), {'explosion':'constant'})
self.assertEqual(gate.default_options('projected_linear'), {'explosion':'projected_linear'})
def test_baseline_receipt_rejects_provenance_and_contract_mutants(self):
keys = [f'{name}:shifted={shifted}' for name in gate.cases() for shifted in (False,True)]
good = dict(completed=True,dry_run=False,mode='baseline',production_revision=gate.BASE,
source=dict(revision=gate.BASE,dirty=False),driver_sha256='driver',
stops=list(gate.STOPS),operator_tolerance=[6e-8,2e-7],
rest_tolerance=[3e-7,1e-6],replay_tolerance=[3e-6,0],default_snapshots=dict.fromkeys(keys, []),
actual_build_sidecars=[{'ptx_sha256':'captured'}])
self.assertIs(gate.baseline_receipt(good, 'driver', keys), good)
mutants = dict(completed=False,dry_run=True,mode='candidate',production_revision='wrong',
driver_sha256='wrong',stops=[0,1,2],operator_tolerance=[1e-6,2e-7],
rest_tolerance=[3e-7,1e-5],replay_tolerance=[3e-6,1e-7],actual_build_sidecars=[],default_snapshots={})
for field, value in mutants.items():
bad = deepcopy(good); bad[field] = value
with self.subTest(field=field), self.assertRaises(ValueError):
gate.baseline_receipt(bad, 'driver', keys)
for changes in (dict(revision='wrong'), dict(dirty=True), dict(dirty=None)):
bad = deepcopy(good); bad['source'].update(changes)
with self.assertRaises(ValueError):
gate.baseline_receipt(bad, 'driver', keys)
bad = deepcopy(good); bad['default_snapshots']['unexpected'] = []
with self.assertRaises(ValueError):
gate.baseline_receipt(bad, 'driver', keys)
if __name__ == '__main__':
unittest.main()
-198
View File
@@ -1,198 +0,0 @@
"""Production host dispatch with CUDA doubles; no CUDA compilation or flow runs."""
from contextlib import nullcontext
from dataclasses import replace
from unittest.mock import Mock
import unittest
import numpy as np
from CelerisLab.config import LBMConfig
from tests.unit.test_hierarchy import CFG, FOUR
from tests.unit.test_refinement_three_level_multipatch import host_module
def backend(**options):
host = host_module()
calls, allocations, storage = [], [], {}
context = Mock(activate=nullcontext, sm_arch='sm_70')
host.cuda.mem_alloc.side_effect = lambda size: allocate(size)
def allocate(size):
pointer = Mock(name=f'buffer{len(allocations)}', size=size)
allocations.append(pointer)
return pointer
def load(_):
module = Mock()
module.get_global.return_value = (Mock(), 20)
module.get_function.side_effect = lambda name: Mock(
name=name, side_effect=lambda *a, **kw: calls.append((name, a, kw)))
return module
host.compiler.load_module.side_effect = load
host.cuda.memcpy_htod.side_effect = lambda ptr, a: storage.__setitem__(id(ptr), np.array(a, copy=True))
host.cuda.memcpy_dtoh.side_effect = lambda a, ptr: np.copyto(a, storage[id(ptr)])
cfg = options.pop('cfg', CFG)
refinements = options.pop('refinements', FOUR)
blocks = options.pop('level_blocks', [(4, 4)] * (len(refinements) + 1))
run = host.FixedPatchStepper(cfg, context=context, refinements=refinements,
level_blocks=blocks, **options)
run.initialize(*(np.full(level.shape, 1/9, np.float32) for level in run._levels))
return run, host, calls, allocations, storage
def trace(run, calls, allocations, storage):
"""Stable pointer identities expose allocations/arguments, not just kernel names."""
pointer = {id(value): index for index, value in enumerate(allocations)}
launches = [(name, tuple(('ptr', pointer[id(a)]) if id(a) in pointer else int(a)
for a in args), kw['grid'], kw['block'])
for name, args, kw in calls]
uploads = [(index, value.dtype.str, value.shape, value.tobytes())
for index, ptr in enumerate(allocations) if (value := storage.get(id(ptr))) is not None]
return [p.size for p in allocations], uploads, launches
class ProjectedPairDispatch(unittest.TestCase):
def test_omitted_and_constant_have_identical_allocations_uploads_and_launches(self):
results = []
for options in ({}, {'explosion': 'constant'}):
run, host, calls, allocations, storage = backend(**options)
self.assertEqual(run._explosion, 'constant')
self.assertTrue(all(p.neighbors is None and p.offsets is None for p in run._pairs))
run.step()
explosion = [args for name, args, _ in calls if name == 'BlockExplode']
self.assertEqual(len(explosion), 7)
self.assertTrue(all(len(args) == 6 for args in explosion))
self.assertFalse(any(name == 'BlockExplodeProjected' for name, _, _ in calls))
results.append(trace(run, calls, allocations, storage))
run.close()
self.assertEqual(*results)
def test_projected_adds_exactly_two_tables_per_adjacent_pair(self):
for depth in (2, 3, 4):
constant, _, _, ca, _ = backend(refinements=FOUR[:depth-1])
projected, _, calls, pa, storage = backend(
refinements=FOUR[:depth-1], explosion='projected_linear')
self.assertEqual(len(pa)-len(ca), 2*(depth-1))
for pair, geometry in zip(projected._pairs, projected.hierarchy.pairs):
self.assertEqual(storage[id(pair.neighbors)].shape, (len(geometry.ghost_fine), 4))
self.assertEqual(storage[id(pair.offsets)].shape, (len(geometry.ghost_fine), 2))
self.assertEqual(pair.neighbors.size, len(geometry.ghost_fine)*4*8)
self.assertEqual(pair.offsets.size, len(geometry.ghost_fine)*2*4)
projected.step()
explosions = [args for name, args, _ in calls if name == 'BlockExplodeProjected']
self.assertEqual(len(explosions), 2**(depth-1)-1)
self.assertTrue(all(len(args) == 8 for args in explosions))
self.assertFalse(any(name == 'BlockExplode' for name, _, _ in calls))
for args in explosions:
pair = next(p for p in projected._pairs if args[2] is p.ghosts)
self.assertIs(args[6], pair.neighbors)
self.assertIs(args[7], pair.offsets)
self.assertEqual(args[5], pair.parent_cells)
constant.close(); projected.close()
def test_three_and_four_level_schedule_has_fresh_midpoint_sources(self):
for depth in (3, 4):
for explosion in ('constant', 'projected_linear'):
run, host, calls, allocations, storage = backend(
refinements=FOUR[:depth-1], explosion=explosion)
before = len(allocations)
events = []
for index, pair in enumerate(run._pairs):
pair.explode.side_effect = lambda source, *a, index=index, **kw: events.append(('explode', index, source))
pair.coalesce.side_effect = lambda child, source, *a, index=index, **kw: events.append(('restrict', index, source))
for index, level in enumerate(run._levels):
level.kernel.side_effect = lambda source, *a, index=index, **kw: events.append(('step', index, source))
level.halo_copy.side_effect = lambda source, *a, index=index, **kw: events.append(('halo', index, source))
expected = []
def hop(index, source):
if index+1 < depth:
expected.append(('explode', index, source))
child = run._levels[index+1]
hop(index+1, child.current)
hop(index+1, child.spare)
expected.append(('halo', index, source))
if index+1 < depth:
expected.append(('restrict', index, source))
expected.append(('step', index, source))
hop(0, run._levels[0].current)
run.step()
self.assertEqual(events, expected)
self.assertEqual([level.kernel.call_count for level in run._levels], [2**i for i in range(depth)])
self.assertEqual(len(allocations), before)
self.assertEqual(run.step_count, 1)
for index in range(1, depth-1):
feeds = [source for name, pair, source in events if name == 'explode' and pair == index]
self.assertEqual(feeds, [run._levels[index].current, run._levels[index].spare] * (2**(index-1)))
run.close()
def test_scalar_windows_follow_the_same_pair_path(self):
run, _, _, _, _ = backend(cfg=LBMConfig(nx=16,ny=16,viscosity=.02),
refinements=[(4,4,12,12),(8,8,16,16),(8,8,16,16)],
level_blocks=[(4,4),(4,8),(8,4),(4,4)], explosion='projected_linear')
self.assertTrue(all(pair.neighbors is not None for pair in run._pairs))
self.assertEqual([pair.parent_cells for pair in run._pairs], [36,60,60])
run.step()
self.assertEqual([level.kernel.call_count for level in run._levels], [1,2,4,8])
run.close()
def test_physics_gate_precedes_context_and_allocations(self):
host = host_module()
context = Mock()
options = dict(refinements=FOUR[:1], level_blocks=[(4,4)]*2,
explosion='projected_linear', context=context)
for cfg, changes in [(CFG, {'explosion':'unknown'}),
(CFG, {'coarse_boundary':'slot_reservoir'}),
(CFG, {'coarse_boundary':'channel'}),
(CFG, {'level_blocks':[None,None], 'refinements':[(6,6,26,26)]}),
(CFG, {'level_objects':[(),(object(),)]}),
(CFG, {'level_jets':[(),(object(),)]}),
(replace(CFG,collision='MRT'), {}),
(replace(CFG,les_enabled=True), {})]:
with self.subTest(changes=changes, cfg=cfg), self.assertRaisesRegex(ValueError,'explosion'):
host.FixedPatchStepper(cfg, **dict(options, **changes))
context.activate.assert_not_called()
host.cuda.mem_alloc.assert_not_called()
# The legacy constant path still allows MRT and dense storage.
for options in ({'cfg':replace(CFG,collision='MRT')},
{'level_blocks':[None,None], 'refinements':[(6,6,26,26)]}):
run, _, _, _, _ = backend(**options)
run.close()
def test_remap_preserves_selector_tables_and_constant_default(self):
for explosion in ('constant','projected_linear'):
run, host, _, _, storage = backend(refinements=FOUR[:2], explosion=explosion)
run._step_count = 7
old = run.download()
clone = run.remap_child(FOUR[1])
self.assertEqual(clone._explosion, explosion)
self.assertEqual(clone.step_count, 7)
for pair in clone._pairs:
self.assertEqual(pair.neighbors is None, explosion == 'constant')
for expected, actual in zip(old, clone.download()):
np.testing.assert_array_equal(actual,expected)
clone.step()
self.assertEqual((run.step_count,clone.step_count), (7,8))
clone.close(); run.close()
def test_changed_remap_geometry_keeps_projected_in_both_representations(self):
changed = [(22,22,42,42), (72,68,76,76)]
for shifted in (False, True):
run, _, _, _, _ = backend(cfg=replace(CFG,ddf_shifting=shifted),
refinements=FOUR[:2], explosion='projected_linear')
run._step_count = 3
before = run.download()
clone = run.remap_child(changed)
self.assertEqual(clone._explosion, 'projected_linear')
self.assertEqual(clone.hierarchy.pairs[1].bounds, tuple(changed))
self.assertNotEqual(clone._pairs[1].n_ghosts, run._pairs[1].n_ghosts)
for old, new in zip(run._pairs, clone._pairs):
self.assertIsNot(old.neighbors, new.neighbors)
self.assertIsNotNone(new.neighbors)
for expected, actual in zip(before, run.download()):
np.testing.assert_array_equal(actual, expected)
clone.step()
self.assertEqual((run.step_count, clone.step_count), (3, 4))
clone.close(); run.close()
+2 -3
View File
@@ -157,9 +157,8 @@ class RawExchange(unittest.TestCase):
host = host_module()
ctx = Mock()
with self.assertRaisesRegex(ValueError, 'blocked body'):
host.FixedPatchStepper(LBMConfig(nx=32, ny=32, viscosity=.1), context=ctx,
refinements=[(4, 4, 28, 28)],
level_objects=[(), (body(radius=2.15),)], observe_raw_exchange=True)
host.FixedPatchStepper(LBMConfig(nx=32, ny=32, viscosity=.1), (4, 4, 28, 28),
context=ctx, fine_objects=[body(radius=2.15)], observe_raw_exchange=True)
ctx.activate.assert_not_called()
def test_all_depth_weights(self):
-45
View File
@@ -1,45 +0,0 @@
"""Host reduction contract for the optional observation-window exchange ledger."""
import unittest
import numpy as np
from CelerisLab.lbm.raw_exchange import summarize_exchange
def reduce(rows, window, duration=None, h=.5, time_end=10):
return summarize_exchange(rows, np.array([0, 0, 1]), np.array([False, True, False]),
body_id=0, level=1, h=h, time_end=time_end, window=window,
reference_center=(0., 0.), duration=duration)
class ExchangeWindowReduction(unittest.TestCase):
def test_window_duration_scaling(self):
rows = np.array([[1., 2., 3., 4.], [.5, -1., 0., 1.], [9., 9., 9., 9.]])
out = reduce(rows, "window", duration=4)
np.testing.assert_array_equal(out["total"]["impulse"], [1.5, 1.])
np.testing.assert_array_equal(out["total"]["force"], [1.5 / 4, 1. / 4])
self.assertEqual(out["total"]["torque"], 3. / 4)
self.assertEqual(out["total"]["mass_rate"], 5. / 4)
np.testing.assert_array_equal(out["aperture"]["impulse"], [.5, -1.])
self.assertEqual((out["time_start"], out["time_end"]), (6., 10))
self.assertEqual(out["sample_count"], 8) # duration / h fine substeps
self.assertEqual(out["window"], "window")
def test_existing_windows_unchanged(self):
rows = np.array([[1., 2., 3., 4.], [0., 0., 0., 0.], [0., 0., 0., 0.]])
base = reduce(rows, "last_base_step")
self.assertEqual((base["time_start"], base["sample_count"]), (9., 2))
np.testing.assert_array_equal(base["total"]["force"], [1., 2.])
sub = reduce(rows, "last_substep")
self.assertEqual(sub["sample_count"], 1)
def test_duration_validation(self):
rows = np.zeros((3, 4))
for bad in (None, 0, -1, 1.5, True):
with self.subTest(duration=bad), self.assertRaises(ValueError):
reduce(rows, "window", duration=bad)
with self.assertRaises(ValueError):
reduce(rows, "last_base_step", duration=2)
if __name__ == "__main__": unittest.main()
+2 -6
View File
@@ -5,7 +5,6 @@ import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tests/validation"))
sys.path.insert(0, str(ROOT / "tests/unit"))
import re100_helpers as h
import periodic as p
@@ -63,16 +62,13 @@ def test_periodic_synthetic():
assert not p.assess(rows()[:100])["periodic"]
def test_production_unmodified(tmp_path):
from historical_tree import historical_tree
def test_production_unmodified():
m = json.loads(
(ROOT / "tests/validation/data/dfg_re100/source_manifest.json").read_text()
)
assert m["commit"] == h.PIN
origin = historical_tree(m["commit"], tmp_path)
for name, digest in m["production_sha256"].items():
assert hashlib.sha256((origin / name).read_bytes()).hexdigest() == digest, name
assert hashlib.sha256((ROOT / name).read_bytes()).hexdigest() == digest, name
def test_reference():
+10 -395
View File
@@ -7,37 +7,13 @@ import numpy as np
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
sys.path.insert(0, str(Path(__file__).resolve().parent))
import run_refined_dfg as r
from historical_tree import historical_tree
from tempfile import TemporaryDirectory
h, periodic = r.helpers(r.DEFAULT_SOURCE)
def test_source_pin():
historical = json.loads((r.HERE / "data/refined_dfg/source_manifest.json").read_text())
assert historical["commit"].startswith("cd3a657")
mrt_history = json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text())
assert mrt_history["commit"].startswith("3ea131e")
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
r.verify_source(r.DEFAULT_SOURCE, "MRT")
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
r.verify_source(r.DEFAULT_SOURCE, "SRT")
with TemporaryDirectory() as directory:
for collision, mode, prefix in (
("SRT", "last_base_step", "cd3a657"),
("MRT", "last_base_step", "3ea131e"),
("MRT", "window", "a233c90"),
):
name = {"SRT": "source_manifest.json", "MRT": "source_manifest_mrt.json"}[collision]
name = "source_manifest_window.json" if mode == "window" else name
commit = json.loads((r.HERE / "data/refined_dfg" / name).read_text())["commit"]
assert commit.startswith(prefix)
root = historical_tree(commit, Path(directory) / commit)
assert r.verify_source(root, collision, mode)["commit"] == commit
assert r.verify_source(r.DEFAULT_SOURCE)["commit"].startswith("cd3a657")
def test_matched_units_and_planes():
@@ -157,62 +133,24 @@ def test_axis_specific_sampling_phase():
np.testing.assert_allclose(fine_y - ((k + 43) - 0.5) * 0.00125, 0, atol=1e-15)
def frozen_manifest(observation_mode):
"""Frozen MRT manifest JSON as recorded, without hashing the working tree."""
name = "source_manifest_window.json" if observation_mode == "window" else "source_manifest_mrt.json"
return json.loads((r.HERE / "data/refined_dfg" / name).read_text())
def selected_manifest(collision, observation_mode):
"""verify_source's manifest selection with its working-tree hash comparison neutralised.
Source identity is owned by test_source_pin; this exercises only which frozen manifest
verify_source selects and returns for (collision, observation_mode).
"""
from unittest.mock import patch
class Digest(str):
def __ne__(self, other):
return False
class Hash:
def __init__(self, data=b""):
pass
def hexdigest(self):
return Digest()
with patch.object(r.hashlib, "sha256", Hash):
return r.verify_source(r.DEFAULT_SOURCE, collision, observation_mode)
def test_full_requires_completed_pilot():
with unittest.TestCase().assertRaisesRegex(ValueError, "pilot-results"):
r.verify_pilot(None, frozen_manifest("window"))
r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE))
def pilot_fixture(path, observation_mode="window", jet_scenario="none", case_selection="both", uniform_diameter=80, placement="wide"):
def pilot_fixture(path):
"""Synthetic contract fixture only; never a physical result or run receipt."""
# Receipt-logic fixture: the frozen manifest verify_source returns for a matching tree.
# Source identity itself is checked only by test_source_pin.
manifest = (r.verify_source(r.DEFAULT_SOURCE, "MRT", "window", jet_scenario)
if observation_mode == "window" and jet_scenario != "none" else
frozen_manifest(observation_mode))
selection = dict(case_selection=case_selection, uniform_diameter=uniform_diameter, placement=placement)
manifest = r.verify_source(r.DEFAULT_SOURCE)
(path / "plan.json").write_text(
json.dumps(
dict(
source_manifest=manifest,
**selection,
collision="MRT",
jet_scenario=jet_scenario,
observation_mode=observation_mode,
bounds_root_edge=list(r.PLACEMENTS[placement]),
bounds_root_edge=list(r.BOUNDS),
runner_sha256=r.runner_hash(),
)
)
)
for case, diameter in r.selected_cases(case_selection, uniform_diameter):
for case, diameter in (("uniform", 80), ("refined", 40)):
dest = path / case
dest.mkdir()
interval = round(0.02 * diameter / r.SPEED)
@@ -239,10 +177,6 @@ def pilot_fixture(path, observation_mode="window", jet_scenario="none", case_sel
json.dumps(
dict(
case=case,
**selection,
collision="MRT",
jet_scenario=jet_scenario,
observation_mode=observation_mode,
mode="pilot",
status="pilot_complete",
healthy=True,
@@ -258,10 +192,6 @@ def pilot_fixture(path, observation_mode="window", jet_scenario="none", case_sel
json.dumps(
dict(
case=case,
**selection,
collision="MRT",
jet_scenario=jet_scenario,
observation_mode=observation_mode,
mode="pilot",
step=interval,
runner_sha256=r.runner_hash(),
@@ -278,7 +208,7 @@ def test_pilot_validation_and_calibration():
with TemporaryDirectory() as tmp:
path = Path(tmp)
manifest = pilot_fixture(path)
assert r.verify_pilot(path, manifest, "MRT", "window") == 3606.0
assert r.verify_pilot(path, manifest) == 3606.0
def test_reject_malformed_pilot_receipts():
@@ -308,7 +238,7 @@ def test_reject_malformed_pilot_receipts():
mutate(bad)
receipt.write_text(json.dumps(bad))
with unittest.TestCase().assertRaises(ValueError):
r.verify_pilot(path, manifest, "MRT", "window")
r.verify_pilot(path, manifest)
# Exact reviewer regression: previously accepted NaN endpoint.
receipt.write_text(
json.dumps(
@@ -320,11 +250,11 @@ def test_reject_malformed_pilot_receipts():
)
)
with unittest.TestCase().assertRaises(ValueError):
r.verify_pilot(path, manifest, "MRT", "window")
r.verify_pilot(path, manifest)
receipt.write_text(json.dumps(valid))
(path / "uniform/ready.json").write_text("{}")
with unittest.TestCase().assertRaises(ValueError):
r.verify_pilot(path, manifest, "MRT", "window")
r.verify_pilot(path, manifest)
def test_strict_finite_json_and_deadline():
@@ -353,321 +283,6 @@ def test_pressure_probes_actual_body_mask():
assert h.pressure_probes(np.ones(pair.fine_shape), fluid, p)["dp"] == 0.0
def test_matched_mrt_selection():
for diameter in (40, 80):
srt, sbody, sp = r.config(h, diameter)
mrt, mbody, mp = r.config(h, diameter, "MRT")
assert mrt["method"]["collision"] == "MRT"
mrt["method"]["collision"] = "SRT"
assert mrt == srt and mbody == sbody and mp == sp
srt_plan, mrt_plan = r.plan(h), r.plan(h, "MRT")
for key in ("root", "uniform", "bounds_root_edge", "finest_dx_m", "finest_dt_s"):
assert srt_plan[key] == mrt_plan[key]
assert "q2/6/8" in mrt_plan["outlet"] and "all nine" in srt_plan["outlet"]
cfg, pair, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT")
assert cfg.collision == "MRT" and pair.fine_shape == (240, 568)
assert len(host.boundary_cells) > 0
def test_reject_collision_receipts():
from tempfile import TemporaryDirectory
for rel in ("plan.json", "uniform/summary.json", "refined/summary.json",
"uniform/ready.json", "refined/ready.json"):
for invalid in ("SRT", None):
with TemporaryDirectory() as tmp:
path = Path(tmp)
manifest = pilot_fixture(path)
receipt = path / rel
record = json.loads(receipt.read_text())
if invalid is None:
record.pop("collision")
else:
record["collision"] = invalid
receipt.write_text(json.dumps(record))
with unittest.TestCase().assertRaises(ValueError):
r.verify_pilot(path, manifest, "MRT", "window")
def test_record_actual_checkout_separately():
from source_provenance import checkout_provenance
actual = checkout_provenance(r.DEFAULT_SOURCE)
assert len(actual["checkout_revision"]) == 40
assert isinstance(actual["checkout_dirty"], bool)
assert selected_manifest("MRT", "window")["production_origin_commit"].startswith("d930")
def test_window_plan_preserves_physics():
for collision in ("SRT", "MRT"):
baseline = r.plan(h, collision)
window = r.plan(h, collision, "window")
assert baseline.pop("observation_mode") == "last_base_step"
assert window.pop("observation_mode") == "window"
assert baseline == window
assert selected_manifest(collision, "window")["observation_mode"] == "window"
with unittest.TestCase().assertRaises(ValueError):
r.verify_source(r.DEFAULT_SOURCE, "MRT", "unknown")
def test_reject_observation_mode_receipts():
from tempfile import TemporaryDirectory
for mode, other in (("window", "last_base_step"), ("last_base_step", "window")):
with TemporaryDirectory() as tmp:
path = Path(tmp)
manifest = pilot_fixture(path, mode)
assert r.verify_pilot(path, manifest, "MRT", mode) == 3606.0
with unittest.TestCase().assertRaises(ValueError):
r.verify_pilot(path, manifest, "MRT", other)
for rel in ("plan.json", "uniform/summary.json", "refined/summary.json",
"uniform/ready.json", "refined/ready.json"):
for invalid in (other, None):
with TemporaryDirectory() as tmp:
path = Path(tmp)
manifest = pilot_fixture(path, mode)
receipt = path / rel
record = json.loads(receipt.read_text())
if invalid is None:
record.pop("observation_mode")
else:
record["observation_mode"] = invalid
receipt.write_text(json.dumps(record))
with unittest.TestCase().assertRaises(ValueError):
r.verify_pilot(path, manifest, "MRT", mode)
class ExchangeFixture:
"""Synthetic interval ledger for orchestration tests, not a solver or GPU result."""
def __init__(self):
self.step_count = 11
self.events = []
self.begin = None
self.override = {}
def start_exchange_window(self):
self.begin = self.step_count
self.events.append(("start", self.step_count))
def step(self, n):
self.events.append(("step", n))
self.step_count += n
def result(self, start, window):
from CelerisLab.lbm.raw_exchange import summarize_exchange
total = sum(range(start + 1, self.step_count + 1))
result = summarize_exchange([[total, -2*total, 0., 0.]], [0], [False],
body_id=0, level=1, h=.5, time_end=self.step_count, window=window,
reference_center=(80.5, 81.),
**({"duration": self.step_count-start} if window == "window" else {}))
result.update(self.override)
return result
def read_exchange_window(self, body_id, *, level):
assert body_id == 0 and level == 1
self.events.append(("read_window", self.step_count))
return self.result(self.begin, "window")
def read_raw_exchange(self, body_id, *, level, window):
assert body_id == 0 and level == 1 and window == "last_base_step"
self.events.append(("read_raw", self.step_count))
return self.result(self.step_count-1, window)
def test_window_interval_impulse_sampling_and_units():
base, window = ExchangeFixture(), ExchangeFixture()
for n in (80, 2000, 3): # Pilot/dense, early full, and final partial interval.
begin = base.step_count
a = r.advance_refined_interval(base, n, float("inf"), "last_base_step")
w = r.advance_refined_interval(window, n, float("inf"), "window")
np.testing.assert_array_equal(w, a)
expected = sum(range(begin+1, begin+n+1))
np.testing.assert_array_equal(w, [expected, -2*expected])
den = .5 * r.SPEED**2 * 40
np.testing.assert_array_equal(w/n/den, a/n/den)
assert window.step_count == base.step_count == begin+n
assert window.events[-3:] == [("start", begin), ("step", n), ("read_window", begin+n)]
assert (begin+n/2)*r.SPEED/40 == (window.step_count-n/2)*r.SPEED/40
assert not any(event[0] == "read_raw" for event in window.events)
def test_window_contract_rejection():
for key, value in (("units", "local"), ("window", "last_base_step"),
("sample_count", 2), ("time_start", 0), ("time_end", 0), ("dt", 1.)):
backend = ExchangeFixture()
backend.override[key] = value
with unittest.TestCase().assertRaisesRegex(RuntimeError, "window force"):
r.advance_refined_interval(backend, 80, float("inf"), "window")
backend = ExchangeFixture()
with unittest.TestCase().assertRaises(TimeoutError):
r.advance_refined_interval(backend, 80, 0, "window")
assert not backend.events
def test_public_jet_configuration_and_geometry():
for diameter, center, radius in ((40, (80.5, 81), 20), (80, (160, 160.5), 40)):
cfg, _, _ = r.config(h, diameter, "MRT", "fixed")
assert not cfg["method"]["ddf_shifting"] and not cfg["method"]["les"]["enabled"]
jets = r.public_jets(center, radius, "fixed")
assert [j.amplitude for j in jets] == [.001, -.001]
assert all(j.field.inward is False and j.profile == "parabolic" for j in jets)
assert all(j.region.r_max == radius * 1.005 for j in jets)
assert all(j.amplitude == 0 for j in r.public_jets(center, radius, "zero"))
cfg, _, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT", "fixed")
assert not cfg.ddf_shifting and len(host.boundary_cells) > 0
assert r.public_jets((0, 0), 1, "none") == ()
with unittest.TestCase().assertRaises(ValueError):
r.config(h, 40, "SRT", "fixed")
def test_public_jet_reversal_boundary_and_windows():
from types import SimpleNamespace
calls = []
handle = SimpleNamespace(set_jet_amplitude=lambda i, a: calls.append((i, a)))
for diameter in (40, 80):
boundary = round(250 * diameter / r.SPEED)
assert r.jet_interval(handle, "fixed", boundary - 3, 80, diameter) == (3, None)
switch = round(300 * diameter / r.SPEED)
assert r.jet_interval(handle, "reverse", switch - 3, 80, diameter) == (3, None)
assert not calls
n, event = r.jet_interval(handle, "reverse", switch, 80, diameter)
assert n == 80 and event["step"] == switch and event["tstar"] == 300
assert calls == [(0, -.001), (1, .001)]
calls.clear()
assert r.jet_interval(handle, "reverse", switch + 80, 80, diameter) == (80, None)
assert r.jet_interval(handle, "fixed", switch, 80, diameter) == (80, None)
assert not calls
rows = [dict(tstar=t, tstar_force=t - .5, cd=2.0, cl=-.1) for t in range(251, 306)]
windows = r.jet_diagnostics(rows, "reverse")
assert len(windows) == 6 and all(w["complete"] for w in windows)
assert all(w["cd"]["mean"] == 2 and np.isclose(w["cl"]["mean"], -.1) for w in windows)
assert windows[-1]["covered_tstar"] == 5
with unittest.TestCase().assertRaisesRegex(ValueError, "crosses"):
r.jet_diagnostics([dict(tstar=250.5, tstar_force=250, cd=1, cl=0)], "fixed")
assert r.plan(h, "MRT", "window", "reverse")["modes"]["full_tstar"] == 305
def test_public_jet_source_and_pilot_separation():
from tempfile import TemporaryDirectory
with TemporaryDirectory() as tmp:
path = Path(tmp)
manifest = pilot_fixture(path, jet_scenario="fixed")
assert manifest["source_capture"].startswith("current checkout")
assert len(manifest["checkout_revision"]) == 40
assert r.verify_pilot(path, manifest, "MRT", "window", "fixed") == 3606.0
for scenario in ("none", "zero", "reverse"):
with unittest.TestCase().assertRaisesRegex(ValueError, "Jet scenario"):
r.verify_pilot(path, manifest, "MRT", "window", scenario)
for relative in ("uniform/summary.json", "refined/ready.json"):
receipt = path / relative
record = json.loads(receipt.read_text())
receipt.write_text(json.dumps(dict(record, jet_scenario="zero")))
with unittest.TestCase().assertRaises(ValueError):
r.verify_pilot(path, manifest, "MRT", "window", "fixed")
receipt.write_text(json.dumps(record))
with unittest.TestCase().assertRaises(ValueError):
r.verify_source(r.DEFAULT_SOURCE, "MRT", "last_base_step", "fixed")
def test_cv_snapshot_common_units_and_window_endpoints():
records = []
for refined, shape, origin, diameter, impulse in (
(True, (240, 568), (38., 22.), 40, [4., -8.]),
(False, (330, 1761), (.25, .5), 80, [16., -32.]),
(False, (658, 3521), (.375, .75), 160, [64., -128.]),
):
rho, ux, uy, fluid = np.ones(shape), np.full(shape, .01), np.zeros(shape), np.ones(shape, bool)
begin, n = round(290*diameter/r.SPEED), round(.02*diameter/r.SPEED)
record = r.cv_snapshot(rho, ux, uy, fluid, origin, refined, begin, n, diameter, impulse)
records.append(record)
np.testing.assert_allclose(record["outward_flux"], 0, atol=1e-12)
assert r.cv_selected(begin, diameter, "full") and not r.cv_selected(begin-n, diameter, "full")
end = round(300*diameter/r.SPEED)
assert r.cv_selected(end, diameter, "full") and not r.cv_selected(end+n, diameter, "full")
assert r.cv_selected(n, diameter, "pilot")
assert sum(r.cv_selected(step, diameter, "full") for step in range(begin-n, end+n+1, n)) == 501
for key in ("time_root", "interval_start_root", "interval_dt_root", "interval_impulse_root", "inventory"):
for record in records[1:]:
np.testing.assert_allclose(records[0][key], record[key], rtol=0, atol=1e-10)
assert records[0]["interval_dt_root"] == 80 and np.isclose(records[0]["tstar"], 290.02)
np.testing.assert_array_equal(records[0]["interval_impulse_root"], [4, -8])
def test_cv_pilot_mode_and_helper_provenance():
from tempfile import TemporaryDirectory
manifest = r.verify_source(r.DEFAULT_SOURCE, "MRT", "window", "fixed", True)
assert "tests/validation/run_jet_circle_cv.py" in manifest["sha256"]
with TemporaryDirectory() as tmp:
path = Path(tmp)
old = pilot_fixture(path, jet_scenario="fixed")
with unittest.TestCase().assertRaisesRegex(ValueError, "CV observation mode"):
r.verify_pilot(path, old, "MRT", "window", "fixed", True)
for relative in ("plan.json", "uniform/summary.json", "refined/summary.json",
"uniform/ready.json", "refined/ready.json"):
file = path / relative
record = json.loads(file.read_text())
record.update(observe_cv=True, cv_samples=250)
if relative == "plan.json":
record["source_manifest"] = manifest
file.write_text(json.dumps(record))
assert r.verify_pilot(path, manifest, "MRT", "window", "fixed", True) == 3606
def test_compact_geometry_and_cv_stencils():
from CelerisLab.lbm.refinement import ACTIVE
from CelerisLab.lbm.descriptors import SOLID
_, pair, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT", "fixed", "compact")
assert pair.fine_shape == (168, 168) and tuple(pair.fine_origin) == (46, 46)
fluid = (pair.fine_kind == ACTIVE) & ((host.flags.reshape(pair.fine_shape) & SOLID) == 0)
rho, ux, uy = np.ones(pair.fine_shape), np.full(pair.fine_shape, .01), np.zeros(pair.fine_shape)
record = r.cv_snapshot(rho, ux, uy, fluid, pair.fine_origin, True, 1160000, 80, 40, [1, 2], "compact")
np.testing.assert_allclose(record["outward_flux"], 0, atol=1e-12)
stencil = np.zeros(pair.fine_shape, bool)
for rect in r.cv_contours(True, 40, "compact"):
x0, y0, x1, y1 = np.rint((rect - np.tile(pair.fine_origin, 2))/.5).astype(int)
stencil[y0-2:y1+2, x0-2:x0+2] = stencil[y0-2:y1+2, x1-2:x1+2] = True
stencil[y0-2:y0+2, x0-2:x1+2] = stencil[y1-2:y1+2, x0-2:x1+2] = True
assert fluid[stencil].all() and not stencil.ravel()[host.transfer_fine_cells].any()
def test_d160_config_and_single_case_receipts():
from tempfile import TemporaryDirectory
from unittest.mock import patch
cfg, _, p = r.config(h, 160, "MRT", "fixed")
assert (p["nx"], p["ny"], p["center"], p["radius"]) == (3521, 658, [320, 320.5], 80)
assert p["viscosity"] == .016 and p["mean_speed"] == .01 and not cfg["method"]["ddf_shifting"]
assert r.uniform_origin(160) == (.375, .75)
np.testing.assert_array_equal(r.cv_contours(False, 160), r.CV_CONTOURS + [.125, 0, .125, 0])
for scenario, expected_exit in (("fixed", 0), ("zero", 2)):
with TemporaryDirectory() as tmp:
path = Path(tmp)
manifest = pilot_fixture(path, jet_scenario=scenario, case_selection="uniform", uniform_diameter=160)
assert r.verify_pilot(path, manifest, "MRT", "window", scenario, False, "uniform", 160) == 1803
for selection, diameter, placement in (("both", 160, "wide"), ("uniform", 80, "wide"), ("uniform", 160, "compact")):
with unittest.TestCase().assertRaises(ValueError):
r.verify_pilot(path, manifest, "MRT", "window", scenario, False, selection, diameter, placement)
# Synthetic dispatch/result check: no CUDA imports or execution.
output = path / "full"
argv = ["runner", "--collision", "MRT", "--exchange-window", "--jet-scenario", scenario,
"--case", "uniform", "--uniform-diameter", "160", "--mode", "full",
"--pilot-results", str(path), "--output", str(output), "--max-wall-seconds", "10", "--gpu-gates-approved"]
result = dict(case="uniform", healthy=True, periodic=scenario == "zero", jet_diagnostic_windows=[],
status="driven_diagnostics_complete" if scenario == "fixed" else "periodic",
reference_comparison=dict(reference_agreement=False))
with patch.object(sys, "argv", argv), patch.dict(r.os.environ, CUDA_VISIBLE_DEVICES="mock-no-gpu"), \
patch.object(r, "run_case", return_value=result) as run:
assert r.main() == expected_exit and run.call_count == 1
comparison = json.loads((output / "comparison.json").read_text())
assert comparison["matched_case_pass"] is False and "refined_vs_uniform" not in comparison
assert comparison["status"].startswith("single_case_")
if __name__ == "__main__":
suite = unittest.TestSuite(
unittest.FunctionTestCase(fn)
+1 -1
View File
@@ -142,7 +142,7 @@ class FixedPatchTests(unittest.TestCase):
FixedPatch2D.from_config(LBMConfig(nx=nx, ny=ny, viscosity=.1), bounds)
def test_unsupported_backend_and_fine_relaxation_rejected(self):
for kwargs in ({"collision": "TRT"}, {"streaming": "esopull"},
for kwargs in ({"collision": "TRT"}, {"collision": "MRT"}, {"streaming": "esopull"},
{"les_enabled": True},
{"store_precision": "FP16S"}, {"lattice_model": "D3Q19"},
{"data_type": "FP64"}, {"omega_min": 1.0}):
-53
View File
@@ -1,53 +0,0 @@
"""Host guards and source contract for MRT reuse in refined/blocked stepping (no CUDA)."""
from pathlib import Path
import unittest
from CelerisLab.config import LBMConfig
from CelerisLab.lbm.refinement import FixedPatch2D
KERNELS = Path(__file__).resolve().parents[2] / "src/CelerisLab/lbm/kernels"
BOUNDS = (8, 6, 16, 14)
class RefinementMRTHost(unittest.TestCase):
def test_patch_accepts_implemented_models(self):
for collision in ("SRT", "MRT"):
with self.subTest(collision=collision):
FixedPatch2D.from_config(LBMConfig(nx=24, ny=20, viscosity=.1, collision=collision), BOUNDS)
def test_patch_rejects_unimplemented(self):
for options in (dict(collision="TRT"), dict(collision="MRT", les_enabled=True)):
with self.subTest(options=options), self.assertRaises(ValueError):
FixedPatch2D.from_config(LBMConfig(nx=24, ny=20, viscosity=.1, **options), BOUNDS)
def test_srt_statements_and_guards_preserved(self):
"""MRT is an additive compile-time branch; SRT statements and other guards remain."""
blocks = (KERNELS / "blocks.cu").read_text() + (KERNELS / "step/block_step.cu").read_text()
channel = (KERNELS / "step/block_channel.cu").read_text()
block_collision = (KERNELS / "operators/block_collision.cuh").read_text()
self.assertIn('#include "operators/block_collision.cuh"', blocks)
self.assertEqual(blocks.count("block_collide(f);"), 1)
refinement = (KERNELS / "refinement.cu").read_text()
for text in ("compute_feq_from_momentum(drho, jx, jy, ux, uy, feq);",
"compute_feq(rho, ux, uy, feq);",
"collide_srt_no_force(f, feq, d_params.omega);"):
self.assertIn(text, block_collision)
self.assertIn("collide_srt(f, feq, forcing, omega);", channel)
self.assertIn("collide_srt_no_force(f, feq, d_params.omega);", refinement)
for source in (blocks, refinement):
self.assertIn("(COLLISION_MODEL != 0 && COLLISION_MODEL != 2)", source)
self.assertIn("USE_LES", source)
self.assertIn("STORE_PRECISION != 0", source)
self.assertIn('#if COLLISION_MODEL == 2\n#include "operators/collision_mrt.cuh"\n#endif', source)
self.assertIn("USE_DDF_SHIFTING ||", refinement) # dense/mixed shifted stays rejected
# One shared unforced MRT body; each blocked/patch path calls it exactly once.
mrt = (KERNELS / "operators/collision_mrt.cuh").read_text()
self.assertEqual(mrt.count("void collide_mrt_unforced("), 1)
for source in (blocks + "\n" + block_collision, channel, refinement):
self.assertEqual(source.count("collide_mrt_unforced(f);"), 1)
self.assertNotIn("collide_mrt_no_force(", source)
self.assertNotIn("compute_rho_u(f, rho, ux, uy);\n collide_mrt", source)
self.assertNotRegex(source, r"compute_rho_u\(f, rho, ux, uy\);\s*collide_mrt")
if __name__ == "__main__": unittest.main()
@@ -173,10 +173,8 @@ def bodies_and_jets(speed=.002):
return bodies,jets
def levels(parents=PARENTS,children=CHILDREN,blocks=((4,4),)*3,objects=((),(),()),jets=((),(),())):
"""Level-list constructor keywords for the L0 set, L1 set and L2 set used here."""
return dict(refinements=[parents,children],level_blocks=list(blocks),
level_objects=list(objects),level_jets=list(jets))
KW=dict(base_block=(4,4),fine_block=(4,4),child_block=(4,4),
child_rectangles_in_parent=CHILDREN)
def mock_backend(*, children=CHILDREN, bodies=False, fail_compile=False, fail_upload=False):
@@ -203,8 +201,9 @@ def mock_backend(*, children=CHILDREN, bodies=False, fail_compile=False, fail_up
module.cuda.memcpy_htod.side_effect=upload
objects,jets=bodies_and_jets() if bodies else ((),())
try:
run=module.FixedPatchStepper(CFG,context=context,
**levels(children=children,objects=((),(),objects),jets=((),(),jets)))
run=module.FixedPatchStepper(CFG,PARENTS,context=context,
child_objects=objects,child_jets=jets,
**dict(KW,child_rectangles_in_parent=children))
except RuntimeError:
for allocation in allocations:
allocation.free.assert_called_once_with()
@@ -215,29 +214,28 @@ def mock_backend(*, children=CHILDREN, bodies=False, fail_compile=False, fail_up
class ConstructorAndSchedule(unittest.TestCase):
def test_invalid_inputs_fail_before_context_or_allocations(self):
context=Mock()
b=(4,4)
cases=[dict(children=(8,8,16,16)),dict(children=[]),
dict(children=[(16,16,32)]),
dict(children=[(16.5,16,32,40)]),
dict(children=[(16,16,32,24),(16,27,32,55)]),
dict(children=[(40,16,48,40)]),
dict(children=[(8,16,32,40)]),
dict(blocks=(b,b,None)),dict(blocks=(b,None,b)),dict(blocks=(None,b,b)),
dict(blocks=(b,b,(5,4))),dict(blocks=(b,(5,4),b)),
dict(blocks=(b,b,(4,))),dict(children=(16,16,32,40))]
cases=[dict(child=(8,8,16,16)),dict(child_rectangles_in_parent=[]),
dict(child_rectangles_in_parent=[(16,16,32)]),
dict(child_rectangles_in_parent=[(16.5,16,32,40)]),
dict(child_rectangles_in_parent=[(16,16,32,24),(16,27,32,55)]),
dict(child_rectangles_in_parent=[(40,16,48,40)]),
dict(child_rectangles_in_parent=[(8,16,32,40)]),
dict(child_block=None),dict(fine_block=None),dict(base_block=None),
dict(child_block=(5,4)),dict(fine_block=(5,4)),
dict(child_block=(4,)),dict(child_rectangles_in_parent=(16,16,32,40))]
for changes in cases:
with self.subTest(changes=changes),self.assertRaises((ValueError,TypeError)):
host.FixedPatchStepper(CFG,context=context,**levels(**changes))
host.FixedPatchStepper(CFG,PARENTS,context=context,**dict(KW,**changes))
with self.assertRaisesRegex(ValueError,'margin'):
host.FixedPatchStepper(CFG,context=context,**levels(parents=[(1,4,20,28),(23,4,44,28)]))
host.FixedPatchStepper(CFG,[(1,4,20,28),(23,4,44,28)],context=context,**KW)
objects,jets=bodies_and_jets()
with self.assertRaisesRegex(ValueError,'only on the finest level'):
host.FixedPatchStepper(CFG,context=context,**levels(objects=((),objects,())))
with self.assertRaisesRegex(ValueError,'only on L2'):
host.FixedPatchStepper(CFG,PARENTS,context=context,fine_objects=objects,**KW)
from CelerisLab.body.jets import Jet,Tube,Parallel
spanning=Jet(Tube((10.5,14.),0.,25.,.3),Parallel(0.),amplitude=0.)
with self.assertRaisesRegex(ValueError,'one jet'):
host.FixedPatchStepper(CFG,context=context,
**levels(objects=((),(),objects),jets=((),(),[spanning])))
host.FixedPatchStepper(CFG,PARENTS,context=context,child_objects=objects,
child_jets=[spanning],**KW)
context.activate.assert_not_called()
host.cuda.mem_alloc.assert_not_called()
@@ -315,23 +313,21 @@ class ConstructorAndSchedule(unittest.TestCase):
class RemovedLegacyAPI(unittest.TestCase):
"""The pre-level-list constructor/initialize forms are gone and fail loudly."""
class InitialLevels(unittest.TestCase):
"""initialize() entry adapter: positional per-level arrays or the legacy names, never both."""
def test_removed_constructor_forms_raise_type_error(self):
import inspect
context = Mock()
with self.assertRaises(TypeError): # positional bounds
host.FixedPatchStepper(CFG, PARENTS, context=context)
removed = dict(bounds=PARENTS, fine_block=(4, 4), base_block=(4, 4), fine_objects=(),
jets=(), child=(8, 8, 16, 16), child_block=(4, 4), child_objects=(),
child_jets=(), child_rectangles_in_parent=CHILDREN)
for name, value in removed.items():
with self.subTest(keyword=name), self.assertRaises(TypeError):
host.FixedPatchStepper(CFG, context=context, **levels(), **{name: value})
context.activate.assert_not_called()
self.assertEqual(list(inspect.signature(host.FixedPatchStepper.initialize).parameters),
["self", "level_g"]) # no coarse_g/fine_g/child_g
def test_forms(self):
a, b, c = object(), object(), object()
f = host._initial_levels
self.assertEqual(f((a, b), None, None, None), (a, b))
self.assertEqual(f((a, b, c), None, None, None), (a, b, c))
self.assertEqual(f((a, b), None, None, c), (a, b, c)) # old initialize(c, f, child_g=g)
self.assertEqual(f((), a, b, None), (a, b))
self.assertEqual(f((), a, b, c), (a, b, c))
for args in (((a,), None, None, c), ((a, b, c), None, None, c), ((a,), a, b, None),
((), a, None, None), ((), None, b, c)):
with self.subTest(args=args), self.assertRaises(TypeError):
f(*args)
if __name__=='__main__':
+6 -5
View File
@@ -54,13 +54,14 @@ class ShiftedJets(unittest.TestCase):
(Jet(Tube((10.,14.),0.,26.,.3),Parallel(0.)),))]
for objects,jets in cases:
with self.subTest(objects=len(objects),jets=len(jets)),self.assertRaises(ValueError):
module.FixedPatchStepper(CFG,context=None,refinements=[backend.PARENTS,A],
level_blocks=[(4,4)]*3,level_objects=[(),(),objects],level_jets=[(),(),jets])
module.FixedPatchStepper(CFG,backend.PARENTS,context=None,
base_block=(4,4),fine_block=(4,4),child_block=(4,4),
child_rectangles_in_parent=A,child_objects=objects,child_jets=jets)
for blocks in ((None,None,None),((4,4),None,(4,4))):
with self.assertRaises(ValueError):
module.FixedPatchStepper(CFG,context=None,refinements=[backend.PARENTS,A],
level_blocks=list(blocks),level_objects=[(),(),(body(),)],
level_jets=[(),(),(jet(),)])
module.FixedPatchStepper(CFG,backend.PARENTS,context=None,
base_block=blocks[0],fine_block=blocks[1],child_block=blocks[2],
child_rectangles_in_parent=A,child_objects=(body(),),child_jets=(jet(),))
module.cuda.mem_alloc.assert_not_called()
module.compiler.compile_kernel.assert_not_called()
+5 -8
View File
@@ -78,15 +78,13 @@ class ShiftedStorage(unittest.TestCase):
def test_shifted_density_and_tag_validation(self):
old,new = topology(A),topology(B)
a = tuple(np.where(m[None], np.float32(-.001), np.float32(np.nan)).repeat(9,axis=0)
for m in old.leaf_masks) # FP32 under NumPy 1 and 2 promotion rules
a = tuple(np.where(m[None], np.float32(-.001), np.nan).repeat(9,axis=0) for m in old.leaf_masks)
remap(old,new,a)
with self.assertRaisesRegex(ValueError,'representation'): remap(old,new,a,False)
with self.assertRaisesRegex(ValueError,'FP32'): remap(old,new,tuple(x.astype(np.float64) for x in a))
a[0][:,old.leaf_masks[0]] = -1
with self.assertRaisesRegex(ValueError,'positive density'): remap(old,new,a)
rest = tuple(np.where(m[None],np.float32(0),np.float32(np.nan)).repeat(9,axis=0)
for m in old.leaf_masks)
rest = tuple(np.where(m[None],np.float32(0),np.nan).repeat(9,axis=0) for m in old.leaf_masks)
remap(old,new,rest)
with self.assertRaisesRegex(ValueError,'positive density'):
remap_synchronized(old,new,rest,base_count=7,level_steps=(7,14,28),phase='postcollision_g')
@@ -131,10 +129,9 @@ class ShiftedStorage(unittest.TestCase):
def test_dense_or_mixed_shifted_rejected_before_compilation(self):
module=backend.host_module()
cfg=replace(backend.CFG,ddf_shifting=True)
for blocks in ([None,None],[(4,4),None],[None,(4,4)]):
with self.assertRaisesRegex(ValueError,'all-blocked|blocked refined levels below'):
module.FixedPatchStepper(cfg,context=None,refinements=[(4,4,20,28)],
level_blocks=blocks)
for kwargs in ({},dict(base_block=(4,4)),dict(fine_block=(4,4))):
with self.assertRaisesRegex(ValueError,'all-blocked|base_block requires fine_block'):
module.FixedPatchStepper(cfg,(4,4,20,28),context=None,**kwargs)
module.compiler.compile_kernel.assert_not_called()
def test_uniform_block_codec_and_raw_snapshot(self):
-75
View File
@@ -238,81 +238,6 @@ def test_overlapping_force_regions_rejected(device):
bodies.build_compact_lists(bodies.build_flags(field.flag))
@pytest.mark.parametrize("second_x,error", [(14., "unique cut-link writers"),
(17., "active fluid receivers")])
def test_passive_overlap_rejected_before_publishing_lists(second_x, error):
cfg = LBMConfig(nx=32, ny=24)
cfg.validate()
bodies = ObjectManager(cfg.nx, cfg.ny, cfg.nz, cfg.nq, cfg)
for x in (14., second_x):
bodies.add(obj(x=x, y=12., r=4.3))
bodies.add(obj(x=4., y=4., r=2., is_sensor=True))
counts = np.full(bodies.count, 91, np.int32)
hits = []
flags = bodies.build_flags(np.full(cfg.nx * cfg.ny, FLUID, np.uint16))
for domain_flags in (flags, None):
with pytest.raises(ValueError, match=error):
bodies.build_compact_lists_for(bodies.objects, domain_flags,
sensor_cell_counts_out=counts, hits_out=hits)
np.testing.assert_array_equal(counts, 91)
assert hits == []
@pytest.mark.parametrize("centers", [((10., 12.), (22., 12.)),
((10.5, 12.), (19.5, 12.))])
def test_separated_passive_bodies_keep_valid_writers(centers):
# The second case has a subcell physical gap with a resolved fluid column.
cfg = LBMConfig(nx=32, ny=24)
cfg.validate()
bodies = ObjectManager(cfg.nx, cfg.ny, cfg.nz, cfg.nq, cfg)
for x, y in centers:
bodies.add(obj(x=x, y=y, r=4.3))
flags = bodies.build_flags(np.full(cfg.nx * cfg.ny, FLUID, np.uint16))
indices, directions, _, ids, *_ = bodies.build_compact_lists(flags)
assert set(ids) == {0, 1}
assert np.all(flags[indices] & FLUID) and not np.any(flags[indices] & SOLID)
assert len(np.unique(indices.astype(np.uint64) * cfg.nq + directions)) == len(indices)
@pytest.mark.parametrize("entry", ["sync", "initialize", "runtime_edit"])
@pytest.mark.parametrize("second_x", [5., 6.])
def test_passive_overlap_failure_keeps_live_state(device, entry, second_x):
from CelerisLab.simulation import Simulation
field = make_field(device)
bodies = manager(field, [obj(), obj(x=9., y=3., r=1., is_sensor=True)])
bodies.sync_to_gpu(field)
overlapping = obj(x=second_x)
if entry == "runtime_edit":
bodies.stage_add(overlapping)
else:
bodies.add(overlapping)
objects = bodies.objects
flags, curved, counts = field.flag.copy(), field.curved.fluid_idx.copy(), bodies.sensor_cell_counts.copy()
action = bodies.action.copy()
buffers = [(block, bytes(block.data), block.freed) for block in device.blocks]
writes = device.writes
sim = SimpleNamespace(bodies=bodies, field=field, lbm_cfg=field.cfg,
_initialized=entry == "runtime_edit",
_assert_allocation_contract=lambda: None,
stream=SimpleNamespace(synchronize=device.synchronize))
with pytest.raises(ValueError, match="active fluid receivers|unique cut-link writers"):
if entry == "sync":
bodies.sync_to_gpu(field)
elif entry == "initialize":
Simulation.initialize(sim)
else:
Simulation.sync_bodies(sim)
assert bodies.objects == objects
assert bodies.has_pending_edit() == (entry == "runtime_edit")
assert sim._initialized == (entry == "runtime_edit")
assert device.writes == writes and len(device.blocks) == len(buffers)
assert all(bytes(block.data) == data and block.freed == freed for block, data, freed in buffers)
np.testing.assert_array_equal(field.flag, flags)
np.testing.assert_array_equal(field.curved.fluid_idx, curved)
np.testing.assert_array_equal(bodies.sensor_cell_counts, counts)
np.testing.assert_array_equal(bodies.action, action)
def test_action_upload_and_scratch_are_pinned(device):
field = make_field(device, streaming="esopull")
bodies = manager(field, [obj(is_force_region=True)])
-170
View File
@@ -1,170 +0,0 @@
"""Host contract of FixedPatchStepper.step(n): one stream wait per call (mocked driver).
All n base intervals are enqueued in order on the backend stream and the host
waits once before step returns. Launch order and buffer arguments must equal n
step(1) calls; completion metadata, raw-exchange and window ledgers, and failure
invalidation are published only after that wait.
"""
from contextlib import nullcontext
from unittest.mock import Mock
import unittest
import numpy as np
from CelerisLab.config import LBMConfig
from tests.unit.test_raw_exchange import body
from tests.unit.test_refinement_three_level_multipatch import host_module
BODY_LEVEL, HOPS = 3, 8 # body on L3: 2**3 hops per base interval
def backend(window=False):
"""Four-level host backend; every launch, memset and stream wait goes to one log."""
host = host_module()
log, allocations = [], []
def allocate(size):
allocations.append(Mock(size=size))
return allocations[-1]
module = Mock()
module.get_global.return_value = (Mock(), 20)
module.get_function.side_effect = lambda name: Mock(
side_effect=lambda *a, **k: log.append((name, a, k)))
host.compiler.load_module.return_value = module
host.cuda.mem_alloc.side_effect = allocate
host.cuda.memcpy_dtoh.side_effect = lambda array, pointer: array.fill(0)
host.cuda.memset_d8_async.side_effect = lambda *a: log.append(("memset", a, {}))
run = host.FixedPatchStepper(
LBMConfig(nx=48, ny=48, viscosity=.02), context=Mock(activate=nullcontext, sm_arch="sm_70"),
refinements=[[(6, 6, 26, 26)], [(20, 20, 44, 44)], [(56, 56, 72, 72)]],
level_blocks=[(4, 4)] * 4, level_objects=[(), (), (), (body(),)],
observe_raw_exchange=True, observe_window_exchange=window)
run._stream.synchronize.side_effect = lambda: log.append(("sync", (), {}))
run.initialize(*[np.full(level.shape, 1 / 9, np.float32) for level in run._levels])
log.clear()
return run, log, allocations
def signature(log, allocations):
"""Event names with device-pointer arguments replaced by allocation index."""
index = {id(a): i for i, a in enumerate(allocations)}
return [(name, tuple(index.get(id(a), "-") for a in args)) for name, args, _ in log]
def names(log):
return [name for name, _, _ in log]
class StepSync(unittest.TestCase):
def test_step1_waits_once_after_all_launches(self):
run, log, _ = backend()
run.step(1)
self.assertEqual(names(log).count("sync"), 1)
self.assertEqual(names(log)[-1], "sync")
self.assertEqual(names(log)[0], "memset")
self.assertEqual(names(log).count("BlockStep"), 1 + 2 + 4 + 8)
self.assertEqual(run.step_count, 1)
run.close()
def test_step_n_equals_n_step1_launches_with_one_wait(self):
single, single_log, single_alloc = backend()
for _ in range(3):
single.step(1)
batched, batched_log, batched_alloc = backend()
batched.step(3)
expected = [e for e in signature(single_log, single_alloc) if e[0] != "sync"]
got = signature(batched_log, batched_alloc)
self.assertEqual(got[-1][0], "sync")
self.assertEqual(names(batched_log).count("sync"), 1)
self.assertEqual(got[:-1], expected) # same order, same ping-pong buffers
self.assertEqual(names(batched_log).count("memset"), 3)
self.assertEqual(batched.step_count, single.step_count)
index = [{id(a): i for i, a in enumerate(alloc)} for alloc in (single_alloc, batched_alloc)]
self.assertEqual(index[1][id(batched._levels[0].current)],
index[0][id(single._levels[0].current)])
single.close()
batched.close()
def test_step0_launches_and_waits_for_nothing(self):
run, log, _ = backend()
run.step(2)
log.clear()
run.step(0)
self.assertEqual(log, [])
self.assertEqual(run.step_count, 2)
value = run.read_raw_exchange(0, level=BODY_LEVEL) # step(0) keeps the ledger valid
self.assertEqual((value["time_start"], value["time_end"]), (1, 2))
run.close()
def test_completion_metadata_covers_all_intervals(self):
run, _, _ = backend()
with self.assertRaisesRegex(RuntimeError, "completed"):
run.read_raw_exchange(0, level=BODY_LEVEL)
run.step(3)
value = run.read_raw_exchange(0, level=BODY_LEVEL)
self.assertEqual((value["time_start"], value["time_end"], value["sample_count"]),
(2, 3, HOPS)) # step(n) retains only the final base interval
run.step(2)
self.assertEqual(run.read_raw_exchange(0, level=BODY_LEVEL)["time_end"], 5)
run.close()
def test_window_accounting_across_batched_calls(self):
windows = []
for calls in ((1, 3), (4,), (1, 1, 1, 1)):
run, log, _ = backend(window=True)
run.step(1)
run.start_exchange_window()
log.clear()
for n in calls:
run.step(n)
self.assertEqual(names(log).count("BlockAccumulateExchange"), 2 * 4 * HOPS)
self.assertEqual(names(log).count("sync"), len(calls))
out = run.read_exchange_window(0, level=BODY_LEVEL)
windows.append((out["time_start"], out["time_end"], out["sample_count"]))
run.close()
self.assertEqual(windows, [(1., 5, 4 * HOPS)] * 3)
def test_failed_wait_publishes_nothing(self):
run, _, _ = backend(window=True)
run.step(2)
run.start_exchange_window()
run._stream.synchronize.side_effect = RuntimeError("device failure")
with self.assertRaisesRegex(RuntimeError, "device failure"):
run.step(3)
self.assertEqual(run.step_count, 2) # value at entry to the failing call
self.assertFalse(run._exchange_valid)
self.assertFalse(run._window_valid)
for read in (lambda: run.read_raw_exchange(0, level=BODY_LEVEL),
lambda: run.read_exchange_window(0, level=BODY_LEVEL), run.download,
lambda: run.step(1)):
with self.assertRaisesRegex(RuntimeError, "failed"):
read()
run._stream.synchronize.side_effect = None
run.close()
def test_launch_error_mid_batch_skips_wait_and_fails(self):
run, log, _ = backend()
run.step(1)
log.clear()
hop, calls = run._hop, []
def failing(index, *args, **kw):
if index == 0:
calls.append(1)
if len(calls) == 2:
raise RuntimeError("launch failure")
return hop(index, *args, **kw)
run._hop = failing
with self.assertRaisesRegex(RuntimeError, "launch failure"):
run.step(3)
self.assertNotIn("sync", names(log))
self.assertEqual(run.step_count, 1)
self.assertFalse(run._exchange_valid)
with self.assertRaisesRegex(RuntimeError, "failed"):
run.read_raw_exchange(0, level=BODY_LEVEL)
run.close()
if __name__ == "__main__":
unittest.main()
-86
View File
@@ -1,86 +0,0 @@
"""CPU contract of the measurement-only refined DFG timing harness (no CUDA)."""
from pathlib import Path
import sys
import unittest
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
import time_refined_dfg as t # noqa: E402
class TimingHarnessContract(unittest.TestCase):
def test_equal_simulated_duration(self):
s = t.schedule(2000, 200)
self.assertEqual(s["refined"], (200, 2000))
self.assertEqual(s["uniform"], (400, 4000)) # one refined base step = two D80 steps
self.assertAlmostEqual(s["tstar_window"], 2000 * .01 / 40)
for bad in ((0, 1), (1, -1)):
with self.assertRaises(ValueError):
t.schedule(*bad)
def test_timing_only_labels(self):
for name in ("B", "C", "E'"):
self.assertFalse(t.VARIANTS[name]["force_history_valid"])
self.assertTrue(t.VARIANTS[name]["timing_only"])
for name in ("A", "E"):
self.assertFalse(t.VARIANTS[name]["force_history_valid"]) # reads discarded
self.assertTrue(t.VARIANTS[name]["force_reads"])
self.assertIn("runner-like", t.VARIANTS[name]["force_note"])
self.assertEqual({v["case"] for v in t.VARIANTS.values()}, {"refined", "uniform"})
self.assertIn("final base interval", t.VARIANTS["C"]["force_note"])
self.assertIn("whole-window", t.VARIANTS["E'"]["force_note"])
def test_deadline(self):
import time
with self.assertRaises(TimeoutError):
t.check_deadline(time.monotonic() - 1)
t.check_deadline(time.monotonic() + 60)
def test_parity_detects_bits_and_nan_masks(self):
a = np.array([[1., np.nan], [2., 3.]], np.float32)
self.assertIsNone(t.same_state([a], [a.copy()]))
b = a.copy(); b[1, 1] = np.nextafter(np.float32(3), np.float32(4))
self.assertIn("bits", t.same_state([a], [b]))
c = a.copy(); c[0, 0] = np.nan
self.assertIn("NaN", t.same_state([a], [c]))
self.assertIn("level count", t.same_state([a], [a, a]))
inf = a.copy(); inf[1, 0] = np.inf
self.assertIn("infinite", t.same_state([inf], [inf.copy()])) # matching inf still fails
self.assertIn("infinite", t.same_state([a], [inf]))
def test_flat_uniform_ddf(self):
fluid = np.array([[True, False, True], [True, True, False]])
flat = np.ones(9 * fluid.size, np.float32) # Simulation.get_ddf() layout
self.assertTrue(t.uniform_populations_finite(flat, fluid))
flat[1 * fluid.size + 1] = np.nan # q=1, solid cell 1: ignored
self.assertTrue(t.uniform_populations_finite(flat, fluid))
flat[4 * fluid.size + 0] = np.nan # q=4, fluid cell 0: detected
self.assertFalse(t.uniform_populations_finite(flat, fluid))
self.assertTrue(t.uniform_populations_finite(np.ones((9,) + fluid.shape), fluid))
with self.assertRaises(RuntimeError):
t.uniform_populations_finite(np.ones(9 * fluid.size + 1), fluid)
def test_physical_health_limits(self):
ok = (np.array([1., 1.01]), np.array([.01, .02]), np.array([0., .01]))
health = t.physical_health([ok])
self.assertAlmostEqual(health["rho_max"], 1.01)
for bad in ((np.array([1., np.nan]),) + ok[1:],
(np.array([.4, 1.]),) + ok[1:],
(ok[0], np.array([.2, 0.]), ok[2]),
(np.array([]), np.array([]), np.array([]))):
with self.assertRaises(RuntimeError):
t.physical_health([bad])
def test_summary_reports_differences_without_claims(self):
rows = [dict(variant=v, seconds=s) for v, s in
(("A", 3.), ("A", 5.), ("B", 2.), ("C", 1.), ("E", 2.), ("E'", 1.5))]
s = t.summarize(rows, .5)
self.assertEqual(s["A"]["median_seconds"], 4.)
self.assertEqual(s["A_minus_B_seconds"], 2.)
self.assertEqual(s["B_minus_C_seconds"], 1.)
self.assertEqual(s["A_over_E"], 2.)
self.assertIn("not exact causal", s["interpretation"])
if __name__ == "__main__": unittest.main()
-38
View File
@@ -1,38 +0,0 @@
"""CPU contract of the A-versus-window timing harness (no CUDA)."""
from pathlib import Path
import sys
import unittest
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
import time_window_dfg as w # noqa: E402
class WindowTimingContract(unittest.TestCase):
def test_interval_and_variants(self):
self.assertEqual(w.INTERVAL, 80)
self.assertEqual(w.VARIANTS, ("A", "W", "W1"))
def test_components(self):
obs = dict(total=dict(impulse=np.array([1., 2.]), angular_impulse=3., mass_into_fluid=4.))
np.testing.assert_array_equal(w.components(obs), [1., 2., 3., 4.])
def test_interval_comparison(self):
ref = np.array([[1., -1., 0., 0.]])
scale = np.array([[10., 10., 1., 1.]])
self.assertEqual(w.compare_intervals(ref, scale, ref.copy()), 0.)
near = ref + np.array([[5e-9, 0., 0., 0.]])
self.assertLessEqual(w.compare_intervals(ref, scale, near), 1.)
far = ref + np.array([[1e-7, 0., 0., 0.]])
self.assertGreater(w.compare_intervals(ref, scale, far), 1.)
zero = np.zeros((1, 4))
self.assertGreater(w.compare_intervals(zero, zero, zero + 1e-12), 1.) # floor, no div-by-0
def test_candidate_identity_records_files(self):
identity = w.candidate_identity(Path(__file__).resolve().parents[2])
self.assertIn("checkout_revision", identity)
self.assertEqual(set(identity["sha256"]), set(w.CANDIDATE_FILES))
if __name__ == "__main__": unittest.main()
@@ -12,15 +12,13 @@ from source_provenance import checkout_provenance
import re100_helpers
import run_jet_circle_cv
sys.path.insert(0, str(ROOT / "tests/unit"))
from historical_tree import historical_tree, tracked
ORIGIN = "47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5"
def test_manifests_cover_current_source_and_preserve_origin(tmp_path):
assert re100_helpers.PIN == run_jet_circle_cv.PIN == ORIGIN
manifests = {}
def test_manifests_cover_current_source_and_preserve_origin():
names = subprocess.check_output(
["git", "-C", str(ROOT), "ls-files", "src"], text=True
).splitlines()
for group, hashes_key, revision_key in (
("dfg_re100", "production_sha256", "commit"),
("jet_circle_cv", "files", "revision"),
@@ -31,22 +29,18 @@ def test_manifests_cover_current_source_and_preserve_origin(tmp_path):
).read_text()
)
assert manifest[revision_key] == ORIGIN
assert set(manifest[hashes_key]) == set(names)
assert manifest["source_origin_commits"][-1] == ORIGIN
assert (
manifest["historical_checkpoints"]["production"]
== "d79e3f6f0a424f5bcc3a1a6d610313e49daf97a2"
)
assert "not inherited from d79" in manifest["experiment"]
manifests[group] = manifest[hashes_key]
# History-dependent checks last, so a missing commit masks nothing above.
names = tracked(ORIGIN)
origin = historical_tree(ORIGIN, tmp_path)
for group, hashes in manifests.items():
assert set(hashes) == set(names), group
for name, expected in hashes.items():
for name, expected in manifest[hashes_key].items():
assert (
hashlib.sha256((origin / name).read_bytes()).hexdigest() == expected
hashlib.sha256((ROOT / name).read_bytes()).hexdigest() == expected
), name
assert re100_helpers.PIN == run_jet_circle_cv.PIN == ORIGIN
def test_runtime_checkout_identity_is_separate_from_source_origin(tmp_path):
@@ -1,110 +0,0 @@
{
"base_commit": "a135252ae78b47b57d7e77d19001700b3bedac8b",
"production_origin_commit": "a135252ae78b47b57d7e77d19001700b3bedac8b",
"source_experiment": "Explicit a135252 uniform SRT LES regression. Sources pin the committed corrected tree; baseline is a synthetic isolated reverse-header control, not the committed a135252 source. Historical profiles and receipts remain unchanged.",
"sources": {
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
"src/CelerisLab/lbm/channel.py": "40d0354e5dc97bcdb7e7b5c1e7f9303a109b7134d8a3af88b8ff4cd75ab5a095",
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
"src/CelerisLab/lbm/kernels/blocks.cu": "668e5cf14e88bdcfa281c2fa6c75c0f8f8cdbbfb8ba2ca03064ae02ef7ad0057",
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa",
"src/CelerisLab/lbm/kernels/refinement.cu": "b45aad9c022cecb64f7e968ec3cae2d8385a3cc430ee409700c404a3f4dd8cf8",
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "ee8a31398b8e5233d977c816e6b8e060c1eb2ec71320e594226dbca55593c0d0",
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
"src/CelerisLab/lbm/raw_exchange.py": "9031a196006a0f74dee4028e6f3bb673bc19ef7959f9f04dde9e01f93293aeb3",
"src/CelerisLab/lbm/refinement.py": "100d5e46ac894f6008b61364cdad5d37c2ca7ed949d8d71405572d6366526417",
"src/CelerisLab/lbm/refinement_stepper.py": "536cd1d3482591e4ff57c4ae8e888c19e31701687d287aa509129df5b2a43be3",
"src/CelerisLab/lbm/refinement_tables.py": "0cbc0f29aabd0ab4ba1f912bd4d0ac2b430246584cb6ddf13dd28e10582accf3",
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c"
},
"candidate_header_sha256": "a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa",
"baseline_header_sha256": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501"
}
@@ -1,109 +0,0 @@
{
"base_commit": "8403956a9afc0b0bb89265cec374392013d89016",
"production_origin_commit": "8403956a9afc0b0bb89265cec374392013d89016",
"source_experiment": "Explicit dev8403956 uniform SRT LES experiment; historical 9805383/5465832 receipts remain historical. Corrected permits only the reviewed normalization header.",
"sources": {
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
"src/CelerisLab/lbm/channel.py": "40d0354e5dc97bcdb7e7b5c1e7f9303a109b7134d8a3af88b8ff4cd75ab5a095",
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
"src/CelerisLab/lbm/kernels/blocks.cu": "2fa894b20a29f006911ac3e3aad0793cc4aaf9ccd7a67d01c6320a0836b1d8ef",
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
"src/CelerisLab/lbm/kernels/refinement.cu": "b45aad9c022cecb64f7e968ec3cae2d8385a3cc430ee409700c404a3f4dd8cf8",
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "ee8a31398b8e5233d977c816e6b8e060c1eb2ec71320e594226dbca55593c0d0",
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
"src/CelerisLab/lbm/raw_exchange.py": "9031a196006a0f74dee4028e6f3bb673bc19ef7959f9f04dde9e01f93293aeb3",
"src/CelerisLab/lbm/refinement.py": "100d5e46ac894f6008b61364cdad5d37c2ca7ed949d8d71405572d6366526417",
"src/CelerisLab/lbm/refinement_stepper.py": "b6b835cf97f5aa045045bb4a47cfde34b89929d3872be410cb155727dfca2d33",
"src/CelerisLab/lbm/refinement_tables.py": "6eea593aaf791a2c644f1c74df0a41f6f5e62a2fce9feae76e19cf84b615607a",
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c"
},
"candidate_header_sha256": "a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa"
}
@@ -1,112 +0,0 @@
{
"commit": "3ea131e92c057843034f11a1c574301a2430dfce",
"production_origin_commit": "3ea131e92c057843034f11a1c574301a2430dfce",
"collision": "MRT",
"sha256": {
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
"src/CelerisLab/lbm/channel.py": "40d0354e5dc97bcdb7e7b5c1e7f9303a109b7134d8a3af88b8ff4cd75ab5a095",
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
"src/CelerisLab/lbm/kernels/blocks.cu": "2fa894b20a29f006911ac3e3aad0793cc4aaf9ccd7a67d01c6320a0836b1d8ef",
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
"src/CelerisLab/lbm/kernels/refinement.cu": "b45aad9c022cecb64f7e968ec3cae2d8385a3cc430ee409700c404a3f4dd8cf8",
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "ee8a31398b8e5233d977c816e6b8e060c1eb2ec71320e594226dbca55593c0d0",
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
"src/CelerisLab/lbm/raw_exchange.py": "8694202466eba848563e298656353b0f5db94e21a01fa05f51aad6dc2b9fd311",
"src/CelerisLab/lbm/refinement.py": "100d5e46ac894f6008b61364cdad5d37c2ca7ed949d8d71405572d6366526417",
"src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d",
"src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6",
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c",
"tests/validation/re100_helpers.py": "289c6e01075ff361573f862b90cfd61e5980c9a8efcbc78aef69979b4004fd60",
"tests/validation/periodic.py": "a2212c0e2705126327a5019e9cafe64eeca398da1b75061ff23630c9843697a6",
"tests/validation/data/dfg_re100/reference_cycle.json": "073d1bda1629697efebad95667bfe80598e217d708dbffd7d5f6c6999379c55f",
"tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c"
}
}
@@ -1,116 +0,0 @@
{
"commit": "a233c9032487a6006e950d1e327079b00466c410",
"production_origin_commit": "d9303234795fccf8591ba6274dfc544a84d956ff",
"observation_mode": "window",
"supported_collisions": [
"SRT",
"MRT"
],
"sha256": {
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
"src/CelerisLab/lbm/channel.py": "40d0354e5dc97bcdb7e7b5c1e7f9303a109b7134d8a3af88b8ff4cd75ab5a095",
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
"src/CelerisLab/lbm/kernels/blocks.cu": "2fa894b20a29f006911ac3e3aad0793cc4aaf9ccd7a67d01c6320a0836b1d8ef",
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
"src/CelerisLab/lbm/kernels/refinement.cu": "b45aad9c022cecb64f7e968ec3cae2d8385a3cc430ee409700c404a3f4dd8cf8",
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "ee8a31398b8e5233d977c816e6b8e060c1eb2ec71320e594226dbca55593c0d0",
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
"src/CelerisLab/lbm/raw_exchange.py": "9031a196006a0f74dee4028e6f3bb673bc19ef7959f9f04dde9e01f93293aeb3",
"src/CelerisLab/lbm/refinement.py": "100d5e46ac894f6008b61364cdad5d37c2ca7ed949d8d71405572d6366526417",
"src/CelerisLab/lbm/refinement_stepper.py": "b6b835cf97f5aa045045bb4a47cfde34b89929d3872be410cb155727dfca2d33",
"src/CelerisLab/lbm/refinement_tables.py": "6eea593aaf791a2c644f1c74df0a41f6f5e62a2fce9feae76e19cf84b615607a",
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c",
"tests/validation/data/dfg_re100/reference_cycle.json": "073d1bda1629697efebad95667bfe80598e217d708dbffd7d5f6c6999379c55f",
"tests/validation/periodic.py": "a2212c0e2705126327a5019e9cafe64eeca398da1b75061ff23630c9843697a6",
"tests/validation/re100_helpers.py": "289c6e01075ff361573f862b90cfd61e5980c9a8efcbc78aef69979b4004fd60",
"tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c"
}
}
@@ -1,425 +0,0 @@
"""Bounded exact-source CUDA plumbing gate; never a flow-accuracy certificate.
Run this driver externally against exact dev with --mode baseline first, then
against the experimental source with --mode candidate --baseline <receipt>.
Only scalar receipts/hashes and actual compiler sidecars are retained. Whole-step
mass/momentum are diagnostics: no verified FP32 conservation bound is claimed.
"""
import argparse
from contextlib import contextmanager
from dataclasses import replace
import hashlib
import json
import os
import shutil
from pathlib import Path
import subprocess
import sys
import numpy as np
BASE = 'ed9d8c937d446b854d6963bd92d718bb4fb98b96'
PRODUCTION = 'a135252ae78b47b57d7e77d19001700b3bedac8b'
STOPS = (0, 1, 2, 8)
OPERATOR_ATOL, OPERATOR_RTOL = 6e-8, 2e-7
REST_ATOL, REST_RTOL = 3e-7, 1e-6
REPLAY_ATOL = 3e-6
E = np.array([(0,0),(1,0),(-1,0),(0,1),(0,-1),(1,1),(-1,-1),(1,-1),(-1,1)])
W = np.array([4/9]+[1/9]*4+[1/36]*4)
def digest(array):
"""All finite bits (including signed zero) and exact NaN mask; no NaN payloads."""
array = np.asarray(array)
if np.isinf(array).any():
raise AssertionError('infinite population')
finite = np.isfinite(array)
h = hashlib.sha256()
h.update(str((array.shape, array.dtype.str)).encode())
h.update(np.ascontiguousarray(finite).tobytes())
h.update(np.ascontiguousarray(array[finite]).tobytes())
return h.hexdigest()
def independent_explosion(coarse, pair, layout, kind):
"""Coordinate oracle; never reads the production neighbor/offset tables."""
ny, nx = pair.coarse_shape
legal = np.zeros((ny,nx), bool)
legal.ravel()[pair.ghost_coarse] = True
legal &= layout.owned & ~pair.covered & (kind == 0)
if not legal.ravel()[pair.ghost_coarse].all():
raise AssertionError('illegal center donor')
out = np.empty((9,len(pair.ghost_fine)))
flat = np.asarray(coarse, np.float64).reshape(9,-1)
for row, (target, donor) in enumerate(zip(pair.ghost_fine,pair.ghost_coarse)):
y,x = divmod(int(donor),nx)
gradient = np.zeros((9,2))
for axis, (dx,dy) in enumerate(((1,0),(0,1))):
ends = [(x-dx,y-dy),(x+dx,y+dy)]
if not layout.periodic[axis] and any(not (0<=xx<nx and 0<=yy<ny) for xx,yy in ends):
continue
ends = [(xx%nx,yy%ny) for xx,yy in ends]
if all(legal[yy,xx] for xx,yy in ends):
(xm,ym),(xp,yp) = ends
gradient[:,axis] = (flat[:,yp*nx+xp]-flat[:,ym*nx+xm])/2
for q in range(1,9):
gradient[q] -= E[q]*np.dot(E[q],gradient[q])/np.dot(E[q],E[q])
fy,fx = divmod(int(target),pair.fine_shape[1])
offset = np.array([pair.fine_origin[0]+(fx+.5)/2-(x+.5),
pair.fine_origin[1]+(fy+.5)/2-(y+.5)])
out[:,row] = flat[:,int(donor)] + gradient@offset
return out
def default_options(selector):
# Exact dev has no explosion keyword. Baseline and omitted paths pass none.
return {} if selector == 'omitted' else {'explosion':selector}
def snapshot(run, raw):
from CelerisLab.lbm.refinement import ACTIVE, OUTER_GHOST
stored, public = raw(run), run.download()
hashes, totals = [], np.zeros(3)
for level,(values,visible,kind) in enumerate(zip(stored,public,run.hierarchy.kinds)):
active = kind == ACTIVE
if not np.isfinite(visible[:,active]).all() or not np.isnan(visible[:,~active]).all():
raise AssertionError('public ACTIVE/invalid mask mismatch')
if not np.isnan(values[:,kind==OUTER_GHOST]).all():
raise AssertionError('OUTER owner core is not poisoned')
population = visible[:,active].sum(axis=1,dtype=np.float64)*(.25**level)
totals += np.r_[population.sum(), E.T@population]
hashes.append(dict(raw=digest(values), public=digest(visible), active=int(active.sum())))
if not np.isfinite(totals).all():
raise AssertionError('nonfinite mass/momentum diagnostic')
return dict(step=run.step_count, levels=hashes, mass_momentum=totals.tolist())
def initialize(run, rest=False):
arrays, origin, spacing = [], np.zeros(2), 1.
for index,level in enumerate(run._levels):
if index:
origin += spacing*np.asarray(run.hierarchy.pairs[index-1].fine_origin)
spacing /= 2
y,x = np.indices(level.shape[1:],dtype=np.float64)
x,y = origin[0]+spacing*(x+.5),origin[1]+spacing*(y+.5)
q = np.arange(9)[:,None,None]
values = np.broadcast_to(W[:,None,None],level.shape).copy()
if not rest:
# q-distinct smooth non-equilibrium G; same physical frame on all levels.
values += 1e-4*np.sin(.17*x+.23*y+q)
arrays.append(values)
run.initialize(*arrays)
def operator_case(run, index):
import pycuda.driver as cuda
parent,child = run._levels[index:index+2]
geometry,pair = run.hierarchy.pairs[index],run._pairs[index]
c = np.random.default_rng(19+index).normal(0,.001,(9,*geometry.coarse_shape))
if not run._ddf_shifting:
c += W[:,None,None]
c = c.astype(np.float32)
legal = np.zeros(geometry.coarse_shape,bool)
legal.ravel()[geometry.ghost_coarse] = True
c[:,~legal] = np.nan
packed = parent.layout.to_blocked(c)
output = np.full(child.layout.blocked_shape,np.nan,np.float32)
cuda.memcpy_htod(parent.current,packed)
cuda.memcpy_htod(child.current,output)
calls, hop = [],run._hop
try:
run._hop = lambda *a,**kw: calls.append(a)
run._advance(index+1,parent.current,block=run._block,stream=run._stream)
run._stream.synchronize()
finally:
run._hop = hop
if calls != [(index+1,child.current,child.spare),(index+1,child.spare,child.current)]:
raise AssertionError('explosion changed two-hop launch contract')
cuda.memcpy_dtoh(output,child.current)
target = child.layout.owner.ravel()[geometry.ghost_fine]
slots = np.array([child.layout.slot(target,q) for q in range(9)])
actual = output.ravel()[slots]
expected = independent_explosion(c,geometry,parent.layout,run.hierarchy.kinds[index])
constant = c.reshape(9,-1)[:,geometry.ghost_coarse]
if not np.isfinite(actual).all() or np.max(abs(expected-constant)) <= 1e-5:
raise AssertionError('operator must exercise finite nonzero projected slopes')
np.testing.assert_allclose(actual,expected,atol=OPERATOR_ATOL,rtol=OPERATOR_RTOL)
untouched = np.ones(output.size,bool); untouched[slots.ravel()] = False
if not np.isnan(output.ravel()[untouched]).all():
raise AssertionError('explosion wrote outside its targets')
return dict(pair=index,rows=len(target),parent_stride=pair.parent_cells,
child_stride=int(np.prod(child.layout.local_shape)),
max_error_by_q=np.max(abs(actual-expected),axis=1).tolist(),
nonzero_projection=True,poisoned_nondonors=True,untouched_preserved=True)
def replay_two_hops(values, pair, layout, kind):
"""Existing FP64 two-hop algebra, using the independent coordinate oracle."""
from tests.unit.test_projected_explosion import collide
coarse,fine = (a.copy() for a in values)
fine.reshape(9,-1)[:,pair.ghost_fine] = independent_explosion(coarse,pair,layout,kind)
for _ in range(2):
fine = np.array([np.roll(fine[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
fine[:,pair.fine_kind==2] = np.nan
active = pair.fine_kind==0
fine[:,active] = collide(fine[:,active],pair.tau_fine)
out = np.array([np.roll(coarse[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
out.reshape(-1)[pair.coalesce_coarse] = fine.reshape(-1)[pair.coalesce_fine].mean(axis=1)
out[:,~pair.covered] = collide(out[:,~pair.covered],pair.tau_coarse)
out[:,pair.covered] = np.nan
return out,fine
def run_case(ctx, case, shifted, selector, raw, projected=False, replay=False):
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
cfg,refs,blocks = case
cfg = replace(cfg,velocity=0,ddf_shifting=shifted)
with FixedPatchStepper(cfg,context=ctx,refinements=refs,level_blocks=blocks,
**default_options(selector)) as run:
operator = [operator_case(run,i) for i in range(len(run._pairs))] if projected else []
counts = [0]*len(run._levels)
for index,level in enumerate(run._levels):
def counted(*a, index=index, kernel=level.kernel, **kw):
counts[index] += 1
return kernel(*a,**kw)
level.kernel = counted
def check_counts(stop):
if counts != [stop*2**index for index in range(len(counts))]:
raise AssertionError('actual level hop counts differ from acoustic schedule')
rest = []
if projected:
initialize(run,rest=True)
for stop in STOPS:
run.step(stop-run.step_count)
check_counts(stop)
snapshot(run,raw) # actual public and raw invalid masks at every stop
maximum = 0.
for values,kind in zip(run.download(),run.hierarchy.kinds):
active = values[:,kind==0]
np.testing.assert_allclose(active,np.broadcast_to(W[:,None],active.shape),
atol=REST_ATOL,rtol=REST_RTOL)
maximum = max(maximum,float(np.max(abs(active-W[:,None]))))
rest.append(dict(step=stop,max_absolute_error=maximum))
counts[:] = [0]*len(counts)
initialize(run)
reference = tuple(a.astype(np.float64) for a in run.download()) if replay else None
snapshots,replay_errors,previous = [],[],0
for stop in STOPS:
run.step(stop-run.step_count)
if replay:
for _ in range(stop-previous):
reference = replay_two_hops(reference,run.hierarchy.pairs[0],
run._levels[0].layout,run.hierarchy.kinds[0])
errors = []
for actual,expected,kind in zip(run.download(),reference,run.hierarchy.kinds):
np.testing.assert_allclose(actual[:,kind==0],expected[:,kind==0],
atol=REPLAY_ATOL,rtol=0)
errors.append(np.max(abs(actual[:,kind==0]-expected[:,kind==0]),axis=1).tolist())
replay_errors.append(dict(step=stop,max_error_by_level_q=errors))
check_counts(stop)
snapshots.append(dict(snapshot(run,raw),observed_level_hops=counts.copy())); previous = stop
change = (np.array(snapshots[-1]['mass_momentum'])-
np.array(snapshots[0]['mass_momentum'])).tolist()
return dict(operator=operator,snapshots=snapshots,rest_fixed_point=rest,
replay_max_errors=replay_errors,mass_momentum_change_diagnostic=change,
conservation_status='diagnostic_only_no_FP32_bound')
def compare_snapshots(actual,expected):
if len(actual) != len(expected):
raise AssertionError('snapshot count differs')
for got,want in zip(actual,expected):
if (got['step'] != want['step'] or got['levels'] != want['levels']
or got.get('observed_level_hops') != want.get('observed_level_hops')):
raise AssertionError('owner bits, public values or masks differ')
def cases():
from CelerisLab.config import LBMConfig
from tests.unit.test_hierarchy import CFG, FOUR
scalar = (LBMConfig(nx=16,ny=16,viscosity=.02),
[(4,4,12,12),(8,8,16,16),(8,8,16,16)],
[(4,4),(4,8),(6,4),(4,6)])
four = (CFG,FOUR,[(6,8),(8,6),(12,8),(16,12)])
concave = (LBMConfig(nx=30,ny=30,viscosity=.1),
[[(4,4,5,7),(4,5,7,7)]],[(6,5),(4,6)])
return dict(scalar=scalar,four=four,concave=concave)
def baseline_receipt(receipt, driver_hash, keys):
if (receipt.get('completed') is not True or receipt.get('dry_run') is not False
or receipt.get('mode') != 'baseline' or receipt.get('production_revision') != BASE
or receipt.get('source',{}).get('revision') != BASE
or receipt.get('source',{}).get('dirty') is not False
or receipt.get('driver_sha256') != driver_hash
or receipt.get('stops') != list(STOPS)
or receipt.get('operator_tolerance') != [OPERATOR_ATOL,OPERATOR_RTOL]
or receipt.get('rest_tolerance') != [REST_ATOL,REST_RTOL]
or receipt.get('replay_tolerance') != [REPLAY_ATOL,0]
or set(receipt.get('default_snapshots',{})) != set(keys)
or not receipt.get('actual_build_sidecars')):
raise ValueError('baseline receipt does not match the frozen exact-dev gate')
return receipt
def remap_case(ctx, shifted, raw):
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
cfg,refs,blocks = cases()['four']
with FixedPatchStepper(replace(cfg,velocity=0,ddf_shifting=shifted),context=ctx,
refinements=refs[:2],level_blocks=blocks[:3],explosion='projected_linear') as run:
initialize(run); run.step(1)
before = snapshot(run,raw)
with run.remap_child([(22,22,42,42),(72,68,76,76)]) as clone:
if clone._explosion != 'projected_linear' or any(p.neighbors is None for p in clone._pairs):
raise AssertionError('remap lost projected selection')
if clone.step_count != 1: raise AssertionError('remap lost synchronized count')
initial = snapshot(clone,raw)
clone.step(1)
after = snapshot(clone,raw)
if clone.step_count != 2: raise AssertionError('remap did not continue count')
compare_snapshots([snapshot(run,raw)],[before])
return dict(shifted=shifted,source_unchanged=True,initial=initial,continued=after)
@contextmanager
def capture_builds(report, save, baseline=None):
from CelerisLab.cuda import compiler_v2 as compiler
original = compiler.load_module
def load(path):
path = Path(path)
metadata = json.loads(Path(str(path)+'.json').read_text())
if metadata['ptx_sha256'] != hashlib.sha256(path.read_bytes()).hexdigest():
raise AssertionError('actual PTX and build sidecar disagree')
if baseline and (metadata['arch'],metadata['flags']) not in [
(m['arch'],m['flags']) for m in baseline['actual_build_sidecars']]:
raise AssertionError('baseline build architecture or flags differ')
report['actual_build_sidecars'].append(metadata)
save() # capture AMR metadata before production cleanup removes scratch
return original(str(path))
compiler.load_module = load
try:
yield
finally:
compiler.load_module = original
def preflight(repo, revision, mode):
git = lambda *a: subprocess.check_output(['git','-C',str(repo),*a],text=True).strip()
if git('rev-parse','HEAD') != revision or git('status','--porcelain'):
raise ValueError('requires the exact expected clean checkout')
source = BASE if mode == 'baseline' else PRODUCTION
if mode == 'baseline' and revision != BASE:
raise ValueError('baseline must be exact ed9d8c9')
if subprocess.call(['git','-C',str(repo),'diff','--quiet',source,'--','src']):
raise ValueError('production source differs from the frozen source revision')
return source
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--repo',type=Path,required=True)
parser.add_argument('--expected-revision',required=True)
parser.add_argument('--mode',choices=('baseline','candidate'),required=True)
parser.add_argument('--baseline',type=Path)
parser.add_argument('--output',type=Path,required=True)
parser.add_argument('--run-gpu',action='store_true')
parser.add_argument('--dry-run',action='store_true')
args = parser.parse_args()
if args.output.exists(): parser.error('output already exists')
selected = os.environ.get('CUDA_VISIBLE_DEVICES','')
if not args.dry_run and (not args.run_gpu or not selected or ',' in selected or selected=='-1'):
parser.error('--run-gpu and exactly one explicit CUDA_VISIBLE_DEVICES are required')
repo = args.repo.resolve()
source = preflight(repo,args.expected_revision,args.mode)
baseline = None
if args.mode == 'candidate':
if args.baseline is None: parser.error('candidate requires --baseline receipt')
baseline = json.loads(args.baseline.read_text())
sys.path[:0] = [str(repo/'src'),str(repo),str(repo/'tests/validation/multigrid'),
str(repo/'tests/validation/taylor_green')]
import CelerisLab
if Path(CelerisLab.__file__).resolve() != repo/'src/CelerisLab/__init__.py':
raise RuntimeError('wrong imported checkout')
from run_case import provenance
driver_hash = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
keys = [f'{name}:shifted={shifted}' for name in cases() for shifted in (False,True)]
if baseline is not None: baseline_receipt(baseline,driver_hash,keys)
report = dict(mode=args.mode,production_revision=source,source=provenance(repo),
driver_sha256=driver_hash,
device_selection=selected,completed=False,dry_run=args.dry_run,
operator_tolerance=[OPERATOR_ATOL,OPERATOR_RTOL],
rest_tolerance=[REST_ATOL,REST_RTOL],replay_tolerance=[REPLAY_ATOL,0],stops=STOPS,
conservation_status='diagnostic_only_no_FP32_bound',
actual_build_sidecars=[],default_snapshots={},default_equivalence=[],
projected_operator=[],union_multilevel_smoke=[],changed_remap=[])
args.output.parent.mkdir(parents=True,exist_ok=True)
def save():
text = json.dumps(report,indent=2,allow_nan=False)+'\n'
if len(text.encode()) > 8*1024*1024: raise RuntimeError('receipt exceeds 8 MiB cap')
temporary = args.output.with_suffix(args.output.suffix+'.tmp')
temporary.write_text(text); os.replace(temporary,args.output)
save()
if args.dry_run:
print('Source preflight only; no CUDA imports or execution.'); return
try:
from CelerisLab.cuda.context import CudaContext
from test_refinement_blocked_base import raw
with CudaContext(0) as ctx, capture_builds(report,save,baseline):
import pycuda.driver as cuda
report['device'] = dict(name=ctx.device.name(),pci_bus_id=ctx.device.pci_bus_id(),
capability=list(ctx.device.compute_capability()),
driver=cuda.get_driver_version(),runtime=list(cuda.get_version()))
nvcc = shutil.which('nvcc') or '/usr/local/cuda/bin/nvcc'
report['nvcc_version'] = subprocess.check_output([nvcc,'--version'],text=True).strip()
if baseline and (report['device'] != baseline.get('device')
or report['nvcc_version'] != baseline.get('nvcc_version')
or selected != baseline.get('device_selection')):
raise ValueError('baseline GPU/device selection or toolchain differs')
for name,case in cases().items():
for shifted in (False,True):
key = f'{name}:shifted={shifted}'
report['active_case'] = f'default:{key}'
result = run_case(ctx,case,shifted,'omitted',raw)
report['default_snapshots'][key] = result['snapshots']
if baseline:
compare_snapshots(result['snapshots'],baseline['default_snapshots'][key])
explicit = run_case(ctx,case,shifted,'constant',raw)
compare_snapshots(explicit['snapshots'],result['snapshots'])
report['default_equivalence'].append(dict(case=key,passed=True))
save()
if baseline:
for name,case in cases().items():
for shifted in (False,True):
report['active_case'] = f'projected:{name}:shifted={shifted}'
result = run_case(ctx,case,shifted,'projected_linear',raw,True,name=='concave')
cfg,refs,blocks = case
if name == 'four':
variant_refs = [list(reversed(rects)) for rects in refs]
elif name == 'concave':
variant_refs = [[(4,5,7,7),(4,4,5,5)]] # same union, disjoint decomposition
else:
variant_refs = refs
variant = (cfg,variant_refs,[(2,2)]*len(blocks))
alternate = run_case(ctx,variant,shifted,'projected_linear',raw,True)
compare_snapshots(alternate['snapshots'],result['snapshots'])
report['projected_operator'].append(dict(case=name,shifted=shifted,rows=result['operator']))
report['union_multilevel_smoke'].append(dict(case=name,shifted=shifted,result=result,
alternate_bits_identical=True))
save()
for shifted in (False,True):
report['active_case'] = f'changed-remap:shifted={shifted}'
report['changed_remap'].append(remap_case(ctx,shifted,raw)); save()
expected_builds = {(s['arch'],tuple(s['flags'])) for s in baseline['actual_build_sidecars']}
actual_builds = {(s['arch'],tuple(s['flags'])) for s in report['actual_build_sidecars']}
if actual_builds != expected_builds:
raise AssertionError('baseline build architecture or compiler flags differ')
report['completed'] = True
save()
except Exception as error:
report['error'] = f'{type(error).__name__}: {error}'
save(); raise
print(json.dumps(dict(completed=True,mode=args.mode,receipt=str(args.output))))
if __name__ == '__main__':
main()
@@ -192,7 +192,6 @@ class CpuContracts(unittest.TestCase):
backend._lock = RLock()
backend._status = "ready"
backend._step_count = 0
backend._ddf_shifting = False # unshifted storage, as for the default config
backend._stream = Mock()
backend._module = Mock()
backend._copy = backend._step = Mock()
@@ -27,7 +27,7 @@ def state(ny, nx):
def compile_module(cfg, context, source):
cfg.validate()
build = compiler.generate_config(cfg, block_root_bc=2 if source == "blocks.cu" else 0)
build = compiler.generate_config(cfg)
try:
module = compiler.load_module(compiler.compile_kernel(context.sm_arch, source=source, build_dir=build))
finally:
@@ -106,10 +106,9 @@ class ChannelRootGPU(unittest.TestCase):
cfg = LBMConfig(nx=48, ny=32, viscosity=.1, velocity=.015, ddf_shifting=shifted)
reference = None
for block in ((32,48), (8,8), (4,1)):
with FixedPatchStepper(cfg, context=context, coarse_boundary="channel",
refinements=[(8,6,40,26)], level_blocks=[block, (8,8)],
level_objects=[(), (body,) if bodies else ()],
level_jets=[(), (jet,) if bodies else ()]) as backend:
with FixedPatchStepper(cfg, (8,6,40,26), context=context,
coarse_boundary="channel", base_block=block, fine_block=(8,8),
fine_objects=(body,) if bodies else (), jets=(jet,) if bodies else ()) as backend:
arrays = [state(*level.shape[1:]) for level in backend._levels]
backend.initialize(*arrays)
backend.step(4)
@@ -1,261 +0,0 @@
"""Optional observation-window exchange ledger: host contract (mocked driver) and GPU gate.
Run CPU and GPU classes in separate processes; GPU callers select the device.
ExchangeWindowGPU uses the SRT/unshifted Jet fixture; ExchangeWindowShiftedMRTGPU covers a
shifted MRT, all-blocked, stationary-body case without Jets (no shifted-MRT Jet claim).
GPU tolerance (conservative engineering tolerance, not a universal proof): the window ledger
sums h*dt*raw per link over fine substeps, while the reference sums per-base interval ledgers
over links and then over base steps. Both are FP64 reorderings of the same FP32-exact substep
contributions, so the scale is the sum of the ACTUAL absolute substep contributions (read from
last_raw after every fine hop), not of per-base totals that could hide substep cancellation:
|window - reference| <= 4 * m * eps * sum|substep terms| + 1e-300, m = links * substeps.
"""
from contextlib import ExitStack
import unittest
from unittest.mock import patch
import numpy as np
import pycuda.driver as cuda
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
from test_refinement_blocked_base import mocked_backend
from test_refinement_blocked_fine import BOUNDS, circle, config
EPS = np.finfo(np.float64).eps
def build(m, window=True, raw=True):
return FixedPatchStepper(config("periodic"), context=m["context"], refinements=[BOUNDS],
level_blocks=[(3, 4), (16, 20)], level_objects=[(), (circle(),)],
observe_raw_exchange=raw, observe_window_exchange=window)
def same_bits(test, a, b):
"""Identical NaN masks, no infinities, identical finite FP32 bits (incl. signed zero)."""
a, b = np.asarray(a), np.asarray(b)
test.assertEqual(a.dtype, b.dtype)
test.assertFalse(np.isinf(a).any() or np.isinf(b).any())
np.testing.assert_array_equal(np.isnan(a), np.isnan(b))
finite = np.isfinite(a)
np.testing.assert_array_equal(a[finite].view(np.uint32), b[finite].view(np.uint32))
def accumulations(m):
return [c for c in m["calls"] if c[0] == "BlockAccumulateExchange"]
class ExchangeWindowCPU(unittest.TestCase):
def test_constructor_validation(self):
with mocked_backend() as m:
with self.assertRaisesRegex(ValueError, "requires observe_raw_exchange"):
build(m, window=True, raw=False)
with self.assertRaisesRegex(ValueError, "must be bool"):
build(m, window=1)
def test_state_machine_and_launches(self):
with mocked_backend() as m, ExitStack() as stack:
stack.enter_context(patch.object(cuda, "memset_d8"))
stack.enter_context(patch.object(cuda, "memset_d8_async"))
stack.enter_context(patch.object(cuda, "memcpy_dtoh",
side_effect=lambda out, src: out.fill(0)))
backend = build(m)
b = backend._level_boundaries[1]
self.assertIsNotNone(b.window_integral)
with self.assertRaises(RuntimeError):
backend.start_exchange_window() # not initialized
arrays = [np.full(level.shape, 1 / 9, np.float32) for level in backend._levels]
backend.initialize(*arrays)
with self.assertRaisesRegex(RuntimeError, "no valid exchange window"):
backend.read_exchange_window(0)
backend.step(1)
self.assertEqual(len(accumulations(m)), 2) # closed window: interval ledger only
backend.start_exchange_window()
with self.assertRaisesRegex(RuntimeError, "at least one completed"):
backend.read_exchange_window(0)
m["calls"].clear()
backend.step(2)
calls = accumulations(m)
self.assertEqual(len(calls), 8) # two fine hops x two ledgers x two base steps
self.assertEqual({id(c[1][1]) for c in calls},
{id(b.interval_integral), id(b.window_integral)})
out = backend.read_exchange_window(0)
self.assertEqual((out["time_start"], out["time_end"], out["sample_count"]), (1., 3, 4))
self.assertEqual(backend.read_exchange_window(0)["time_end"], 3) # non-destructive
backend.initialize(*arrays) # invalidates
with self.assertRaisesRegex(RuntimeError, "no valid exchange window"):
backend.read_exchange_window(0)
backend.start_exchange_window()
with patch.object(backend, "_hop", side_effect=RuntimeError("injected")):
with self.assertRaises(RuntimeError):
backend.step(1)
self.assertFalse(backend._window_valid)
with self.assertRaises(RuntimeError):
backend.read_exchange_window(0) # failed status
def test_disabled_default_has_no_window_cost(self):
with mocked_backend() as m, ExitStack() as stack:
stack.enter_context(patch.object(cuda, "memset_d8_async"))
backend = build(m, window=False)
self.assertIsNone(backend._level_boundaries[1].window_integral)
backend.initialize(*[np.full(l.shape, 1 / 9, np.float32) for l in backend._levels])
backend.step(1)
self.assertEqual(len(accumulations(m)), 2)
with self.assertRaisesRegex(RuntimeError, "disabled"):
backend.start_exchange_window()
class ExchangeWindowGPU(unittest.TestCase):
def test_window_matches_summed_base_reads_and_populations_unchanged(self):
from test_raw_exchange_gpu import BOUNDS as RB, CFG, circle as rc, jet, state
from CelerisLab.cuda.context import CudaContext
with CudaContext(0) as ctx:
kw = dict(context=ctx, refinements=[RB], level_blocks=[None, (16, 20)],
level_objects=[(), (rc(),)], level_jets=[(), (jet(),)],
observe_raw_exchange=True)
with FixedPatchStepper(CFG, observe_window_exchange=True, **kw) as windowed, \
FixedPatchStepper(CFG, **kw) as plain:
arrays = [state(level.shape) for level in windowed._levels]
windowed.initialize(*arrays)
plain.initialize(*arrays)
windowed.step(2)
plain.step(2)
windowed.start_exchange_window()
b = windowed._level_boundaries[1]
count = len(b.tables["cell"])
factors = np.array([.25, .25, .125, .25]) # h*dt, h*h*dt, h*h for level-1 h=.5
scale = np.zeros(4)
substeps = 0
kernel = windowed._levels[1].kernel
def audited(src, dst, *args, **kw):
nonlocal scale, substeps
windowed._stream.synchronize() # BFL and both accumulations are complete
raw = np.empty((count, 4), np.float32)
cuda.memcpy_dtoh(raw, b.last_raw)
scale = scale + (np.abs(raw.astype(np.float64)) * factors).sum(axis=0)
substeps += 1
kernel(src, dst, *args, **kw)
windowed._levels[1].kernel = audited
keys = ("impulse", "angular_impulse", "mass_into_fluid")
summed = {part: [0., 0., 0., 0.] for part in ("total", "wall", "aperture")}
steps = 0
for speed in (0., .002, .002, -.001, 0., 0.): # amplitude updates stay in-window
windowed.set_jet_amplitude(0, speed)
plain.set_jet_amplitude(0, speed)
windowed.step(1)
plain.step(1)
steps += 1
obs = windowed.read_raw_exchange(0)
for part in summed:
o = obs[part]
summed[part] = [summed[part][0] + o["impulse"][0], summed[part][1] + o["impulse"][1],
summed[part][2] + o["angular_impulse"],
summed[part][3] + o["mass_into_fluid"]]
for a, e in zip(windowed._download_raw().values, plain._download_raw().values):
same_bits(self, a, e)
out = windowed.read_exchange_window(0)
self.assertEqual((out["time_end"] - out["time_start"], out["sample_count"]),
(steps, 2 * steps))
self.assertEqual(substeps, 2 * steps)
bound = 4 * (count * substeps) * EPS * scale + 1e-300
for part in summed:
o = out[part]
got = np.array([o["impulse"][0], o["impulse"][1], o["angular_impulse"],
o["mass_into_fluid"]])
diff = np.abs(got - np.array(summed[part]))
self.assertTrue((diff <= bound).all(), f"{part}: {diff} > {bound}")
np.testing.assert_array_equal(o["force"], o["impulse"] / steps)
again = windowed.read_exchange_window(0)
np.testing.assert_array_equal(again["total"]["impulse"], out["total"]["impulse"])
windowed.initialize(*arrays)
with self.assertRaises(RuntimeError):
windowed.read_exchange_window(0)
def audit_substeps(stepper, b, count, record):
"""Wrap the level-1 step kernel: after BFL and accumulation, add |substep terms| to record."""
factors = np.array([.25, .25, .125, .25])
kernel = stepper._levels[1].kernel
def audited(src, dst, *args, **kw):
stepper._stream.synchronize()
raw = np.empty((count, 4), np.float32)
cuda.memcpy_dtoh(raw, b.last_raw)
record["scale"] = record["scale"] + (np.abs(raw.astype(np.float64)) * factors).sum(axis=0)
record["substeps"] += 1
kernel(src, dst, *args, **kw)
stepper._levels[1].kernel = audited
def components(obs):
o = obs["total"]
return np.array([o["impulse"][0], o["impulse"][1], o["angular_impulse"], o["mass_into_fluid"]])
class ExchangeWindowShiftedMRTGPU(unittest.TestCase):
"""Shifted MRT, all-blocked, stationary body, NO Jet (scoped separately from SRT/Jet)."""
def test_window_step0_reset_and_split_step(self):
from test_raw_exchange_gpu import BOUNDS as RB, circle as rc, state
from CelerisLab.config import LBMConfig
from CelerisLab.cuda.context import CudaContext
cfg = LBMConfig(nx=32, ny=24, viscosity=.1, collision="MRT", ddf_shifting=True)
with CudaContext(0) as ctx:
kw = dict(context=ctx, refinements=[RB], level_blocks=[(12, 8), (16, 20)],
level_objects=[(), (rc(),)], observe_raw_exchange=True)
with FixedPatchStepper(cfg, observe_window_exchange=True, **kw) as windowed, \
FixedPatchStepper(cfg, observe_window_exchange=True, **kw) as split, \
FixedPatchStepper(cfg, **kw) as plain:
arrays = [state(level.shape) for level in windowed._levels]
for run in (windowed, split, plain):
run.initialize(*arrays)
run.step(2)
if run is not plain:
run.start_exchange_window()
b = windowed._level_boundaries[1]
count = len(b.tables["cell"])
record = dict(scale=np.zeros(4), substeps=0)
audit_substeps(windowed, b, count, record)
steps, summed = 5, np.zeros(4)
for _ in range(steps):
windowed.step(1)
plain.step(1)
summed += components(windowed.read_raw_exchange(0))
for a, e in zip(windowed._download_raw().values, plain._download_raw().values):
same_bits(self, a, e)
split.step(steps) # one call: same ledger arithmetic as five step(1) calls
for a, e in zip(windowed._download_raw().values, split._download_raw().values):
same_bits(self, a, e)
rows = [np.empty((count, 4), np.float64) for _ in range(2)]
cuda.memcpy_dtoh(rows[0], b.window_integral)
cuda.memcpy_dtoh(rows[1], split._level_boundaries[1].window_integral)
np.testing.assert_array_equal(rows[0].view(np.uint64), rows[1].view(np.uint64))
out = windowed.read_exchange_window(0)
self.assertEqual(record["substeps"], 2 * steps)
self.assertEqual((out["time_end"] - out["time_start"], out["sample_count"]),
(steps, 2 * steps))
bound = 4 * (count * record["substeps"]) * EPS * record["scale"] + 1e-300
diff = np.abs(components(out) - summed)
self.assertTrue((diff <= bound).all(), f"{diff} > {bound}")
np.testing.assert_array_equal(split.read_exchange_window(0)["total"]["impulse"],
out["total"]["impulse"])
windowed.step(0) # no interval: window and its duration unchanged
again = windowed.read_exchange_window(0)
self.assertEqual(again["time_end"], out["time_end"])
np.testing.assert_array_equal(again["total"]["impulse"], out["total"]["impulse"])
windowed.start_exchange_window() # reset after a window
with self.assertRaisesRegex(RuntimeError, "at least one completed"):
windowed.read_exchange_window(0)
record.update(scale=np.zeros(4), substeps=0)
windowed.step(1)
single = components(windowed.read_raw_exchange(0))
reset = windowed.read_exchange_window(0)
self.assertEqual(reset["time_end"] - reset["time_start"], 1)
bound = 4 * (count * record["substeps"]) * EPS * record["scale"] + 1e-300
self.assertTrue((np.abs(components(reset) - single) <= bound).all())
if __name__ == "__main__": unittest.main()
@@ -140,8 +140,7 @@ def run_patch(speed, *, audit=True):
build = (patch.object(compiler, "compile_kernel", audit_compiler(compiler.compile_kernel))
if audit else nullcontext())
with build:
backend = implementation.FixedPatchStepper(cfg, context=context, refinements=[BOUNDS],
level_objects=[(), (source,)])
backend = implementation.FixedPatchStepper(cfg, BOUNDS, context=context, fine_objects=[source])
with backend, context.activate():
c, f = backend._levels
cl = backend._fine_curved
@@ -282,8 +281,7 @@ def test_fine_surface_four_hops(speed):
@pytest.mark.parametrize("missing", ["surface_velocity_gpu", "link_exchange_gpu"])
def test_missing_surface_device_buffer_rejects_before_launch(missing):
backend = implementation.FixedPatchStepper.__new__(implementation.FixedPatchStepper)
backend._boundary_hosts = [None, SimpleNamespace(objects=[object_spec(.002)])]
backend._level_jets = [(), ()]
backend.fine_boundary = SimpleNamespace(objects=[object_spec(.002)])
backend._fine_curved = SimpleNamespace(fluid_idx_gpu=object(),
surface_velocity_gpu=object(), link_exchange_gpu=object())
setattr(backend._fine_curved, missing, None)
@@ -327,7 +325,6 @@ def test_cross_interface_donor_rejects_before_cuda():
context.activate.side_effect = AssertionError("must reject before CUDA activation")
with patch.object(cuda, "mem_alloc") as allocate:
with pytest.raises(ValueError, match="donor crosses"):
implementation.FixedPatchStepper(cfg, context=context, refinements=[BOUNDS],
level_objects=[(), (source,)])
implementation.FixedPatchStepper(cfg, BOUNDS, context=context, fine_objects=[source])
context.activate.assert_not_called()
allocate.assert_not_called()
@@ -48,8 +48,9 @@ def make(context):
for (x, y), amplitude in zip(centers, (.003, -.002))
)
run = FixedPatchStepper(
CFG, context=context, refinements=[PARENTS, START], level_blocks=[(4, 4)] * 3,
level_objects=[(), (), objects], level_jets=[(), (), jets],
CFG, PARENTS, context=context, base_block=(4, 4), fine_block=(4, 4),
child_block=(4, 4), child_rectangles_in_parent=START,
child_objects=objects, child_jets=jets,
)
return run, objects
@@ -25,8 +25,9 @@ B = ((16,16,32,40), (60,16,76,40))
def make(ctx):
body = SimObject(71, geometry=CircleGeometry(12.,14.,.8),center=(12.,14.),radius=.8)
jet = Jet(Tube((10.5,14.),0.,3.,.3),Parallel(0.),'parabolic',.003)
run = FixedPatchStepper(CFG,context=ctx,refinements=[PARENTS,A],level_blocks=[(4,4)]*3,
level_objects=[(),(),(body,)],level_jets=[(),(),(jet,)])
run = FixedPatchStepper(CFG,PARENTS,context=ctx,base_block=(4,4),fine_block=(4,4),
child_block=(4,4),child_rectangles_in_parent=A,
child_objects=(body,),child_jets=(jet,))
return run,(body,)
@@ -90,18 +90,14 @@ def census(run, index, buffer):
return totals(dense, leaf), external, independent
def fine(block):
"""Two-level circle+jet construction on BOUNDS; block None keeps the dense fine level."""
return dict(refinements=[BOUNDS], level_blocks=[None, block],
level_objects=[(), (circle(),)], level_jets=[(), (jet(),)])
class RawExchangeGPU(unittest.TestCase):
def test_row_ledger_interval_jet_switch_and_no_population_effect(self):
with CudaContext(0) as ctx:
for block in ((32, 40), (16, 20), (8, 10)):
with FixedPatchStepper(CFG, context=ctx, **fine(block), observe_raw_exchange=True) as measured, \
FixedPatchStepper(CFG, context=ctx, **fine(block)) as plain:
with FixedPatchStepper(CFG, BOUNDS, context=ctx, fine_block=block,
fine_objects=[circle()], jets=[jet()], observe_raw_exchange=True) as measured, \
FixedPatchStepper(CFG, BOUNDS, context=ctx, fine_block=block,
fine_objects=[circle()], jets=[jet()]) as plain:
arrays = [state(l.shape) for l in measured._levels]
measured.initialize(*arrays)
plain.initialize(*arrays)
@@ -150,8 +146,9 @@ class RawExchangeGPU(unittest.TestCase):
"""Same fine resolution/initial data with dense storage: a numerical
reference for cut-link rows, not a uniform-whole-domain convergence study."""
with CudaContext(0) as ctx:
with FixedPatchStepper(CFG, context=ctx, **fine(None)) as dense, \
FixedPatchStepper(CFG, context=ctx, **fine((16, 20)), observe_raw_exchange=True) as blocked:
with FixedPatchStepper(CFG, BOUNDS, context=ctx, fine_objects=[circle()], jets=[jet()]) as dense, \
FixedPatchStepper(CFG, BOUNDS, context=ctx, fine_block=(16, 20),
fine_objects=[circle()], jets=[jet()], observe_raw_exchange=True) as blocked:
arrays = [state(l.shape) for l in blocked._levels]
dense.initialize(*arrays)
blocked.initialize(*arrays)
@@ -232,7 +229,8 @@ class RawExchangeGPU(unittest.TestCase):
layout = BlockLayout2D.build(48, 64, 8, 8)
with CudaContext(0) as ctx:
with PeriodicBlockStepper(fine_cfg, layout, context=ctx, objects=[obj]) as uniform, \
FixedPatchStepper(CFG, context=ctx, **fine((16, 20)), observe_raw_exchange=True) as amr:
FixedPatchStepper(CFG, BOUNDS, context=ctx, fine_block=(16, 20),
fine_objects=[circle()], jets=[jet()], observe_raw_exchange=True) as amr:
amr.initialize(*(state(l.shape) for l in amr._levels))
uniform.initialize(state((9, 48, 64)))
b = amr._level_boundaries[1]
@@ -83,10 +83,11 @@ def mocked_backend(mode="periodic", fail_alloc=None):
class BlockedBaseCPU(unittest.TestCase):
def test_reject_before_cuda(self):
context = Mock(activate=Mock(side_effect=AssertionError("CUDA reached")))
for blocks in ([(12, 8), None], [(7, 8), FINE_BLOCK], [(12,), FINE_BLOCK]):
with self.subTest(blocks=blocks), self.assertRaises(ValueError):
FixedPatchStepper(config("periodic"), context=context, refinements=[BOUNDS],
level_blocks=blocks)
for kwargs in ({"base_block": (12, 8)},
{"base_block": (7, 8), "fine_block": FINE_BLOCK},
{"base_block": (12,), "fine_block": FINE_BLOCK}):
with self.subTest(kwargs=kwargs), self.assertRaises(ValueError):
FixedPatchStepper(config("periodic"), BOUNDS, context=context, **kwargs)
context.activate.assert_not_called()
def test_all_arrival_rows_donors_and_kind_masks(self):
@@ -192,10 +193,9 @@ class BlockedBaseCPU(unittest.TestCase):
def test_launch_order_stream_wall_constants_and_buffer_swaps(self):
with mocked_backend("slot_reservoir") as m:
backend = FixedPatchStepper(config("slot_reservoir"), context=m["context"],
coarse_boundary="slot_reservoir", refinements=[BOUNDS],
level_blocks=[(3, 4), FINE_BLOCK],
level_objects=[(), (circle(actuation(.002)),)])
backend = FixedPatchStepper(config("slot_reservoir"), BOUNDS, context=m["context"],
coarse_boundary="slot_reservoir", fine_block=FINE_BLOCK,
base_block=(3, 4), fine_objects=[circle(actuation(.002))])
try:
backend.initialize(state(NY, NX, 1), state(*backend.patch.fine_shape, 2))
coarse, fine = backend._levels
@@ -223,8 +223,6 @@ class BlockedBaseCPU(unittest.TestCase):
self.assertIs(collide[4], coarse.kind)
walls = [a for ptr, a in m["uploads"] if isinstance(ptr, str) and ptr == "d_block_wall"]
self.assertEqual(len(walls), 1)
self.assertEqual([call.kwargs["block_root_bc"] for call in
compiler.generate_config.call_args_list], [1, 0])
width, height, slot, speed = struct.unpack("=IIIf", walls[0])
self.assertEqual((width, height, slot), (NX, NY, 8))
self.assertAlmostEqual(speed, .001)
@@ -238,16 +236,16 @@ class BlockedBaseCPU(unittest.TestCase):
for fail_at in (1, 6, 10):
with self.subTest(fail_at=fail_at), mocked_backend(fail_alloc=fail_at) as m:
with self.assertRaisesRegex(MemoryError, "injected allocation"):
FixedPatchStepper(config("periodic"), context=m["context"], refinements=[BOUNDS],
level_blocks=[(3, 4), FINE_BLOCK])
FixedPatchStepper(config("periodic"), BOUNDS, context=m["context"],
fine_block=FINE_BLOCK, base_block=(3, 4))
self.assertEqual(len(m["allocations"]), fail_at)
for allocation in m["allocations"]:
allocation.free.assert_called_once_with()
def test_failed_parent_stage_cannot_publish_or_resume(self):
with mocked_backend() as m:
backend = FixedPatchStepper(config("periodic"), context=m["context"], refinements=[BOUNDS],
level_blocks=[(3, 4), FINE_BLOCK])
backend = FixedPatchStepper(config("periodic"), BOUNDS, context=m["context"],
fine_block=FINE_BLOCK, base_block=(3, 4))
backend.initialize(state(NY, NX, 1), state(*backend.patch.fine_shape, 2))
m["functions"]["BlockCoalesce"].side_effect = RuntimeError("injected parent stage failure")
current = backend._levels[0].current
@@ -291,10 +289,9 @@ class BlockedBaseGPU(unittest.TestCase):
for base_block in (None, *layouts):
label = f"{mode} width={width} speed={speed} base={base_block}"
snapshots = []
with FixedPatchStepper(cfg, context=context, coarse_boundary=mode,
refinements=[BOUNDS], level_blocks=[base_block, FINE_BLOCK],
level_objects=[(), () if speed is None
else (circle(actuation(speed)),)]) as backend:
with FixedPatchStepper(cfg, BOUNDS, context=context, coarse_boundary=mode,
fine_block=FINE_BLOCK, base_block=base_block,
fine_objects=() if speed is None else [circle(actuation(speed))]) as backend:
backend.initialize(*arrays)
for step in (0, 1, 2, 8):
if step == 1 and base_block is not None:
@@ -105,10 +105,9 @@ class BlockedFineCPU(unittest.TestCase):
return Mock(activate=Mock(side_effect=AssertionError("CUDA reached")), sm_arch="sm_70")
def test_rejections_before_cuda(self):
for kw in ({"level_blocks": [None, (7, 7)]}, {"level_blocks": [None, (16,)]}):
for kw in ({"fine_block": (7, 7)}, {"fine_block": (16,)}):
with self.subTest(kw=kw), self.assertRaises(ValueError):
FixedPatchStepper(config("periodic"), context=self.forbidden_context(),
refinements=[BOUNDS], **kw)
FixedPatchStepper(config("periodic"), BOUNDS, context=self.forbidden_context(), **kw)
def test_single_block_layout_allocates_no_empty_halo_tables(self):
# A 1x1 non-periodic fine layout has zero halo pairs; CUDA rejects mem_alloc(0).
@@ -126,8 +125,8 @@ class BlockedFineCPU(unittest.TestCase):
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
patch.object(compiler, "load_module", return_value=module), \
patch.object(cuda, "mem_alloc", side_effect=alloc), patch.object(cuda, "memcpy_htod"):
backend = FixedPatchStepper(config("periodic"), context=context, refinements=[BOUNDS],
level_blocks=[None, FINE_BLOCKS[0]])
backend = FixedPatchStepper(config("periodic"), BOUNDS, context=context,
fine_block=FINE_BLOCKS[0])
self.assertEqual(len(backend.fine_layout.halo_dst), 0)
self.assertIsNone(backend._levels[1].halo_dst)
backend._gather_halo(backend._levels[1], Mock()) # no launch without pairs
@@ -178,9 +177,8 @@ class BlockedFineCPU(unittest.TestCase):
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
patch.object(cuda, "memcpy_htod"), \
patch.object(curved_links, "CurvedLinkSoA", side_effect=AssertionError("dense SoA built")):
backend = FixedPatchStepper(config("periodic"), context=context, refinements=[BOUNDS],
level_blocks=[None, FINE_BLOCKS[1]],
level_objects=[(), tuple(objects)])
backend = FixedPatchStepper(config("periodic"), BOUNDS, context=context,
fine_objects=objects, fine_block=FINE_BLOCKS[1])
backend.initialize(state(NY, NX, 1), state(*backend.patch.fine_shape, 2))
return backend, calls
@@ -291,9 +289,8 @@ class BlockedFineGPU(unittest.TestCase):
results = []
with CudaContext(0) as context:
for fine_block in (None, *FINE_BLOCKS):
with FixedPatchStepper(cfg, context=context, coarse_boundary=mode,
refinements=[BOUNDS],
level_blocks=[None, fine_block]) as backend:
with FixedPatchStepper(cfg, BOUNDS, context=context, coarse_boundary=mode,
fine_block=fine_block) as backend:
backend.initialize(g_coarse, g_fine)
backend.step(STEPS)
self.assertEqual(backend.step_count, STEPS)
@@ -321,9 +318,8 @@ class BlockedFineGPU(unittest.TestCase):
results = []
with CudaContext(0) as context:
for fine_block in (None, *FINE_BLOCKS):
with FixedPatchStepper(cfg, context=context, coarse_boundary=mode,
refinements=[BOUNDS], level_blocks=[None, fine_block],
level_objects=[(), (circle(),)]) as backend:
with FixedPatchStepper(cfg, BOUNDS, context=context, coarse_boundary=mode,
fine_objects=[circle()], fine_block=fine_block) as backend:
backend.initialize(g_coarse, g_fine)
backend.step(STEPS)
results.append((backend.download(), raw(backend)))
@@ -350,9 +346,9 @@ class BlockedFineGPU(unittest.TestCase):
results = []
with CudaContext(0) as context:
for fine_block in (None, *FINE_BLOCKS):
with FixedPatchStepper(cfg, context=context, coarse_boundary=mode,
refinements=[BOUNDS], level_blocks=[None, fine_block],
level_objects=[(), (circle(actuation(speed)),)]) as backend:
with FixedPatchStepper(cfg, BOUNDS, context=context, coarse_boundary=mode,
fine_objects=[circle(actuation(speed))],
fine_block=fine_block) as backend:
backend.initialize(g_coarse, g_fine)
backend.step(STEPS)
results.append((backend.download(), raw(backend)))
@@ -94,8 +94,7 @@ class FaultGuardsCPU(unittest.TestCase):
("slot_reservoir", (12, 4, 20, 21))):
with self.subTest(mode=mode, bounds=bounds):
with self.assertRaises(ValueError):
implementation.FixedPatchStepper(cfg, context=context, coarse_boundary=mode,
refinements=[bounds])
implementation.FixedPatchStepper(cfg, bounds, context=context, coarse_boundary=mode)
context.activate.assert_not_called()
# Both exact channel limits and the original periodic two-cell margin
# must reach context activation, with no allocation or device required.
@@ -103,14 +102,11 @@ class FaultGuardsCPU(unittest.TestCase):
("periodic", (2, 2, 6, 5))):
with self.subTest(mode=mode, bounds=bounds):
with self.assertRaisesRegex(RuntimeError, "validated before CUDA"):
implementation.FixedPatchStepper(cfg, context=context, coarse_boundary=mode,
refinements=[bounds])
implementation.FixedPatchStepper(cfg, bounds, context=context, coarse_boundary=mode)
def fake_backend(self):
backend = PeriodicPatchStepper.__new__(PeriodicPatchStepper)
backend._layouts = [None, None] # dense storage on both levels
backend._ddf_shifting = False # unshifted on every level, as _check_storage expects
backend._level_cfgs = [LBMConfig(nx=16, ny=16, viscosity=.1)] * 2 # ddf_shifting off
backend.fine_layout = None # dense fine storage
backend.ctx = Mock(activate=nullcontext)
backend._lock = RLock()
backend._status = "ready"
@@ -158,23 +154,12 @@ class FaultGuardsCPU(unittest.TestCase):
context = Mock(activate=nullcontext, sm_arch="sm_70")
with patch.object(implementation, "HALO", 2):
with self.assertRaisesRegex(RuntimeError, "four-cell"):
PeriodicPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)])
PeriodicPatchStepper(cfg, (4, 4, 12, 12), context=context)
p = implementation.FixedPatch2D.from_config(cfg, (4, 4, 12, 12))
from CelerisLab.lbm import hierarchy
bad = patch.object(implementation.FixedPatch2D, "from_config",
return_value=replace(p, tau_coarse=.9))
# A pair tau inconsistent with its level config is rejected twice over, both before
# any device allocation or upload: first by the hierarchy chain check, and, if that
# is bypassed, by the per-level omega/tau check in _build_level.
with patch.object(cuda, "Stream"), bad, patch.object(cuda, "mem_alloc") as alloc, \
patch.object(cuda, "memcpy_htod") as upload:
with self.assertRaisesRegex(RuntimeError, "config chain is inconsistent"):
PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds])
with patch.object(hierarchy, "_check_chain"), \
self.assertRaisesRegex(RuntimeError, "omega/tau"):
PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds])
alloc.assert_not_called()
upload.assert_not_called()
with patch.object(cuda, "Stream"), patch.object(implementation.FixedPatch2D, "from_config",
return_value=replace(p, tau_coarse=.9)):
with self.assertRaisesRegex(RuntimeError, "omega/tau"):
PeriodicPatchStepper(cfg, p.bounds, context=context)
module = Mock()
module.get_global.return_value = (Mock(), 16)
with patch.object(cuda, "Stream"), patch.object(compiler, "generate_config"), \
@@ -182,7 +167,7 @@ class FaultGuardsCPU(unittest.TestCase):
patch.object(compiler, "load_module", return_value=module), \
patch.object(cuda, "memcpy_htod") as upload:
with self.assertRaisesRegex(RuntimeError, "layout mismatch"):
PeriodicPatchStepper(cfg, context=context, refinements=[p.bounds])
PeriodicPatchStepper(cfg, p.bounds, context=context)
upload.assert_not_called()
@@ -256,7 +241,7 @@ class NormalBuildGPU(unittest.TestCase):
max_error = max_bc_error = 0.0
with CudaContext(0) as context:
with patch.object(compiler, "generate_config", side_effect=record_config):
backend = implementation.FixedPatchStepper(cfg, context=context, refinements=[bounds],
backend = implementation.FixedPatchStepper(cfg, bounds, context=context,
coarse_boundary="slot_reservoir")
with backend:
self.assertEqual(len(generated), 2)
@@ -326,8 +311,7 @@ class NormalBuildGPU(unittest.TestCase):
for a in (initial.coarse_f, initial.fine_f)))
max_pop = max_drift = max_moment = 0.0
with CudaContext(0) as context, PeriodicPatchStepper(
LBMConfig(nx=16, ny=16, viscosity=.1), context=context,
refinements=[(4, 4, 12, 12)]) as backend:
LBMConfig(nx=16, ny=16, viscosity=.1), (4, 4, 12, 12), context=context) as backend:
backend.initialize(*postcollision(state, spec, maps))
before = totals(backend.download(), maps)
for step in range(1, 9):
@@ -358,7 +342,7 @@ class NormalBuildGPU(unittest.TestCase):
fine_mask[4:-4, 4:-4] = True
initial[1][:, ~fine_mask] = np.nan
expected = scalar_interval(*[a.astype(np.float64) for a in initial], bounds)
with CudaContext(0) as context, PeriodicPatchStepper(cfg, context=context, refinements=[bounds]) as backend:
with CudaContext(0) as context, PeriodicPatchStepper(cfg, bounds, context=context) as backend:
self.assertEqual(backend.patch.fine_shape, (14, 16))
p = backend.patch
allocated = 2 * 9 * (17*13+16*14) * 4 + sum(a.nbytes for a in
@@ -7,7 +7,6 @@ At most eight coarse steps per case. No files, plots or flow arrays are saved.
import hashlib
import importlib.util
from contextlib import nullcontext
from dataclasses import replace
from pathlib import Path
import resource
import sys
@@ -22,7 +21,7 @@ import CelerisLab
from CelerisLab.config import LBMConfig
from CelerisLab.cuda import compiler_v2 as compiler
from CelerisLab.cuda.context import CudaContext
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper, PeriodicPatchStepper
from CelerisLab.lbm.refinement_stepper import PeriodicPatchStepper
ROOT = Path(__file__).resolve().parents[3]
POP_ATOL = 3e-6
@@ -61,69 +60,6 @@ def global_array(module, name, shape):
class CoupledGPU(unittest.TestCase):
def test_actual_tg_rates_one_interval(self):
"""Algebraic rate/phase check, not a new physical Taylor--Green benchmark.
The N64/Re100 TG rates are exercised on the existing small patch with
a nonequilibrium state. One interval checks g=C(f), including the
shifted blocked path; no accuracy gate or production source changes.
"""
self.assertEqual(Path(CelerisLab.__file__).resolve(), ROOT / "src/CelerisLab/__init__.py")
self.assertEqual(Path(compiler._KERNEL_DIR).resolve(), ROOT / "src/CelerisLab/lbm/kernels")
spec = replace(ref.make_spec(collision="srt"),
tau_coarse=.5576, tau_fine=.6152)
maps = ref.build_maps(spec)
state = ref.make_state(spec, velocity=(.03, -.02))
rng = np.random.default_rng(1)
state = ref.State(*(a * (1 + 1e-3 * rng.standard_normal(a.shape))
for a in (state.coarse_f, state.fine_f)))
initial = postcollision(state, spec, maps)
next_state, _ = ref.advance_coarse_once(spec, maps, state)
expected = postcollision(next_state, spec, maps)
arrivals = (next_state.coarse_f, next_state.fine_f)
cases = (("dense_unshifted", False, {}),
("blocked_shifted", True,
dict(level_blocks=[(4, 4), (6, 6)])))
with CudaContext(0) as context:
for label, shifted, storage in cases:
with self.subTest(storage=label):
cfg = LBMConfig(nx=16, ny=16, viscosity=.0192,
ddf_shifting=shifted)
with FixedPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)],
**storage) as backend:
self.assertAlmostEqual(backend.patch.tau_coarse, spec.tau_coarse)
self.assertAlmostEqual(backend.patch.tau_fine, spec.tau_fine)
self.assertEqual(tuple(level.shape for level in backend._levels),
((9, 16, 16), (9, 24, 24)))
# Physical FP64 g is encoded by initialize before FP32 rounding.
backend.initialize(*initial)
before = totals(backend.download(), maps)
backend.step(1)
actual = backend.download()
self.assertEqual(backend.step_count, 1)
backend.patch.check_synchronization(
1, next_state.tick, phase="postcollision_g")
for got, want, arrival in zip(actual, expected, arrivals):
np.testing.assert_array_equal(np.isnan(got), np.isnan(want))
np.testing.assert_allclose(got, want, rtol=0, atol=POP_ATOL,
equal_nan=True)
# A raw arrival-f comparison must fail on BOTH levels.
# Equilibrium-only data would make this control vacuous.
with self.assertRaises(AssertionError):
np.testing.assert_allclose(got, arrival, rtol=0,
atol=POP_ATOL, equal_nan=True)
np.testing.assert_allclose(totals(actual, maps), before,
rtol=0, atol=MOMENT_ATOL)
np.testing.assert_allclose(totals(actual, maps),
totals(expected, maps),
rtol=0, atol=MOMENT_ATOL)
error = max(float(np.nanmax(np.abs(a - e)))
for a, e in zip(actual, expected))
print(f"actual-TG-rate algebraic check: {label} steps=1 "
f"tau=.5576/.6152 max_population={error:.9g}; "
"wrong-phase controls PASS", flush=True)
def test_cleanup_attempts_every_allocation(self):
backend = PeriodicPatchStepper.__new__(PeriodicPatchStepper)
backend.ctx = Mock(activate=nullcontext)
@@ -148,7 +84,7 @@ class CoupledGPU(unittest.TestCase):
maps = ref.build_maps(spec)
with CudaContext(0) as context:
with PeriodicPatchStepper(LBMConfig(nx=16, ny=16, viscosity=.1),
context=context, refinements=[(4, 4, 12, 12)]) as backend:
(4, 4, 12, 12), context=context) as backend:
backend.initialize(*postcollision(ref.make_state(spec), spec, maps))
coarse, fine = backend._levels
@@ -187,7 +123,7 @@ class CoupledGPU(unittest.TestCase):
with CudaContext(0) as context:
with patch.object(compiler, "compile_kernel", audit_compile):
backend = PeriodicPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)])
backend = PeriodicPatchStepper(cfg, (4, 4, 12, 12), context=context)
with backend:
# Production allocations: four DDF arrays and four compact tables.
device_bytes = 2 * 9 * (16**2 + 24**2) * 4
@@ -270,7 +206,7 @@ class CoupledGPU(unittest.TestCase):
backend.download()
# Compile and execute the actual non-audited entry as well: one step.
with PeriodicPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)]) as plain:
with PeriodicPatchStepper(cfg, (4, 4, 12, 12), context=context) as plain:
rest = postcollision(ref.make_state(spec), spec, maps)
plain.initialize(*rest)
plain.step()
@@ -41,9 +41,8 @@ def mocked(objects, jets, fine_block):
patch.object(compiler, "load_module", return_value=module), \
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
patch.object(cuda, "memcpy_htod", htod):
backend = FixedPatchStepper(config("periodic"), context=context, refinements=[BOUNDS],
level_blocks=[None, fine_block], level_objects=[(), tuple(objects)],
level_jets=[(), tuple(jets)])
backend = FixedPatchStepper(config("periodic"), BOUNDS, context=context,
fine_objects=objects, fine_block=fine_block, jets=jets)
return backend, htod
@@ -150,9 +149,8 @@ class JetStepperCPU(unittest.TestCase):
def test_jet_overlapping_legacy_aperture_rejected_before_cuda(self):
with patch.object(cuda, "Stream", side_effect=AssertionError("CUDA touched")):
with self.assertRaisesRegex(ValueError, "SurfaceActuation"):
FixedPatchStepper(config("periodic"), context=Mock(), refinements=[BOUNDS],
level_objects=[(), (circle(actuation(SPEED)),)],
level_jets=[(), (jet(),)])
FixedPatchStepper(config("periodic"), BOUNDS, context=Mock(),
fine_objects=[circle(actuation(SPEED))], jets=[jet()])
def run(mode, fine_block, *, objects, jets, switch=None):
@@ -160,9 +158,8 @@ def run(mode, fine_block, *, objects, jets, switch=None):
cfg = config(mode)
patch_ = FixedPatch2D.from_config(cfg, BOUNDS)
with CudaContext(0) as context, FixedPatchStepper(
cfg, context=context, coarse_boundary=mode, refinements=[BOUNDS],
level_blocks=[None, fine_block], level_objects=[(), tuple(objects)],
level_jets=[(), tuple(jets)]) as backend:
cfg, BOUNDS, context=context, coarse_boundary=mode, fine_objects=objects,
fine_block=fine_block, jets=jets) as backend:
backend.initialize(state(NY, NX, 1), state(*patch_.fine_shape, 2))
if switch is None:
backend.step(STEPS)
@@ -1,96 +0,0 @@
"""MRT reuse GPU gates. Caller owns GPU admission (CUDA_VISIBLE_DEVICES).
Run: python -m unittest discover -s tests/validation/multigrid -p test_refinement_mrt_gpu.py
Parity compares the blocked MRT channel root with uniform MRT OneStep: the existing
pressure-NEQ outlet branch differs by collision model, so SRT is never the reference.
AMR checks are finite-evolution/split-invariance smoke tests, not benchmark validation.
"""
from dataclasses import replace
import unittest
import numpy as np
from CelerisLab.config import LBMConfig
from CelerisLab.cuda.context import CudaContext
from CelerisLab.lbm.population_storage import _encode_physical
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
from test_channel_root_gpu import run_root, state
from test_refinement_blocked_base import raw, exact
class RefinementMRTGPU(unittest.TestCase):
def test_uniform_matched_mrt_root(self):
with CudaContext(0) as context:
for shifted in (False, True):
for profile in ("uniform", "parabolic"):
cfg = LBMConfig(nx=24, ny=20, viscosity=.1, velocity=.025, collision="MRT",
ddf_shifting=shifted, inlet_profile=profile)
initial = _encode_physical(state(cfg.ny, cfg.nx), shifted)
expected = run_root(cfg, context, initial)
self.assertTrue(np.isfinite(expected).all())
for block in ((20, 24), (5, 6), (4, 1)):
np.testing.assert_array_equal(run_root(cfg, context, initial, block), expected)
def test_amr_mrt_smoke(self):
from CelerisLab.body.geometry.circle import CircleGeometry
from CelerisLab.body.objects import SimObject
center = (24., 16.)
body = SimObject(0, geometry=CircleGeometry(*center, 2.15), center=center, radius=2.15)
with CudaContext(0) as context:
for boundary in ("periodic", "channel"):
for shifted in (False, True):
cfg = LBMConfig(nx=48, ny=32, viscosity=.1, velocity=.015 if boundary == "channel" else 0.,
collision="MRT", ddf_shifting=shifted)
reference = None
for block in ((32, 48), (8, 8), (4, 1)):
with FixedPatchStepper(cfg, context=context, coarse_boundary=boundary,
refinements=[(8, 6, 40, 26)], level_blocks=[block, (8, 8)],
level_objects=[(), (body,)]) as backend:
backend.initialize(*[state(*level.shape[1:]) for level in backend._levels])
backend.step(8)
raw = backend._download_raw()
for a, mask in zip(raw.values, backend._state):
self.assertTrue(np.isfinite(a[:, mask]).all())
if reference is None: reference = raw.values
else:
for a, b in zip(reference, raw.values): np.testing.assert_array_equal(a, b)
def test_dense_versus_blocked_mrt_two_level(self):
"""Unshifted MRT: all-dense PatchCoarseStep/PatchFineStep vs mixed/all-blocked storage."""
from test_refinement_blocked_base import BASE_BLOCKS, FINE_BLOCK
from test_refinement_blocked_fine import NX, NY, BOUNDS
import test_refinement_blocked_fine as fixture
cfg = LBMConfig(nx=NX, ny=NY, viscosity=.1, collision="MRT")
with CudaContext(0) as context:
results = {}
layouts = ((None, None), *((base, FINE_BLOCK) for base in (None, *BASE_BLOCKS)))
for base_block, fine_block in layouts:
with FixedPatchStepper(cfg, context=context, refinements=[BOUNDS],
level_blocks=[base_block, fine_block]) as backend:
backend.initialize(*[fixture.state(*level.shape[1:], seed) for seed, level in enumerate(backend._levels)])
backend.step(4)
results[base_block, fine_block] = raw(backend)
for layout in layouts[1:]:
for level, (a, b) in enumerate(zip(results[layout], results[None, None])):
exact(self, a, b, f"layout={layout} level={level}")
def test_shifted_all_blocked_three_level_mrt_smoke(self):
"""Per-level shear rate omega_l = 1/(3*2**l*nu + 1/2); finite ACTIVE leaves."""
cfg = LBMConfig(nx=16, ny=16, viscosity=.1, collision="MRT", ddf_shifting=True)
kw = dict(refinements=[(4, 4, 12, 12), (8, 8, 16, 16)], level_blocks=[(4, 4)] * 3)
with CudaContext(0) as context:
with FixedPatchStepper(cfg, context=context, **kw) as backend:
self.assertEqual(len(backend._level_cfgs), 3)
for level, level_cfg in enumerate(backend._level_cfgs):
checked = replace(level_cfg)
checked.validate() # omega is derived only by validate()
self.assertEqual(checked.collision, "MRT")
self.assertTrue(checked.ddf_shifting)
self.assertTrue(np.isclose(checked.omega, 1 / (3 * 2**level * .1 + .5), rtol=1e-14))
backend.initialize(*[state(*level.shape[1:]) for level in backend._levels])
backend.step(4)
values = backend._download_raw().values
for a, mask in zip(values, backend._state):
self.assertTrue(np.isfinite(a[:, mask]).all())
if __name__ == "__main__": unittest.main()
@@ -26,12 +26,6 @@ FACE = ((4, 4, 8, 12), (12, 4, 16, 12))
S1 = ((4, 4, 12, 12), (20, 4, 28, 12))
def levels(bounds, blocks=(None, None), objects=(), jets=(), **rest):
"""Two-level constructor keywords: one L1 rectangle or set, per-level blocks, L1 bodies/Jets."""
return dict(refinements=[bounds], level_blocks=list(blocks),
level_objects=[(), tuple(objects)], level_jets=[(), tuple(jets)], **rest)
def level_fine(patch_set, windows):
"""Level-wide fine array with each patch window filled from `windows`."""
out = np.full((9, *patch_set.fine_shape), np.nan, np.float32)
@@ -51,21 +45,21 @@ class MultipatchCPU(unittest.TestCase):
def test_rejections_before_cuda(self):
cfg = config("periodic")
context = object() # never activated: every case must fail first
for kw in (dict(), dict(blocks=(None, (8, 4))), dict(blocks=((4, 4), None)),
dict(blocks=((4, 4), (8, 4)), objects=[circle()]),
dict(blocks=((4, 4), (5, 4)))):
for kw in (dict(), dict(fine_block=(8, 4)), dict(base_block=(4, 4)),
dict(fine_block=(8, 4), base_block=(4, 4), fine_objects=[circle()]),
dict(fine_block=(5, 4), base_block=(4, 4))):
with self.subTest(kw=kw), self.assertRaises(ValueError):
FixedPatchStepper(cfg, context=context, **levels(((4, 4, 8, 12), (14, 4, 18, 12)), **kw))
FixedPatchStepper(cfg, ((4, 4, 8, 12), (14, 4, 18, 12)), context=context, **kw)
with self.assertRaisesRegex(ValueError, "margin"):
FixedPatchStepper(cfg, context=context,
**levels(((1, 4, 8, 12), (10, 4, 14, 12)), blocks=((4, 4), (8, 4))))
FixedPatchStepper(cfg, ((1, 4, 8, 12), (10, 4, 14, 12)), context=context,
fine_block=(8, 4), base_block=(4, 4))
from CelerisLab.body.jets import Jet, Parallel, Tube
cfg32 = LBMConfig(nx=32, ny=16, viscosity=.1)
span = Jet(Tube((5., 8.), 0., 22., .6), Parallel(0.), amplitude=0.)
with self.assertRaisesRegex(ValueError, "one jet"): # same obj_id cannot bypass it
FixedPatchStepper(cfg32, context=context,
**levels(((2, 2, 14, 14), (18, 2, 30, 14)), blocks=((4, 4), (4, 4)),
objects=[body(8., 8.), body(24., 8.)], jets=[span]))
FixedPatchStepper(cfg32, ((2, 2, 14, 14), (18, 2, 30, 14)), context=context,
fine_block=(4, 4), base_block=(4, 4),
fine_objects=[body(8., 8.), body(24., 8.)], jets=[span])
class MultipatchGPU(unittest.TestCase):
@@ -73,7 +67,7 @@ class MultipatchGPU(unittest.TestCase):
def run_steps(self, context, cfg, bounds, coarse, fine, **kw):
out = []
with FixedPatchStepper(cfg, context=context, **levels(bounds, **kw)) as backend:
with FixedPatchStepper(cfg, bounds, context=context, **kw) as backend:
backend.initialize(coarse, fine)
for step in self.STEPS:
backend.step(step - backend.step_count)
@@ -88,7 +82,7 @@ class MultipatchGPU(unittest.TestCase):
p = FixedPatch2D.from_config(cfg, BOUNDS)
s = FixedPatchSet2D.from_config(cfg, (BOUNDS,))
coarse, fine = state(NY, NX, 1), state(*p.fine_shape, 2)
kw = dict(coarse_boundary=mode, blocks=((3, 4), (8, 4)))
kw = dict(coarse_boundary=mode, fine_block=(8, 4), base_block=(3, 4))
old = self.run_steps(context, cfg, BOUNDS, coarse, fine, **kw)
new = self.run_steps(context, cfg, (BOUNDS,), coarse, level_fine(s, [fine]), **kw)
for step, a, b in zip(self.STEPS, old, new):
@@ -105,7 +99,7 @@ class MultipatchGPU(unittest.TestCase):
one = FixedPatchSet2D.from_config(cfg16, (S1[0],))
two = FixedPatchSet2D.from_config(cfg32, S1)
coarse, fine = state(16, 16, 3), state(*one.patches[0].fine_shape, 4)
kw = dict(blocks=((4, 4), (4, 4)))
kw = dict(fine_block=(4, 4), base_block=(4, 4))
with CudaContext(0) as context:
ref = self.run_steps(context, cfg16, (S1[0],), coarse, level_fine(one, [fine]), **kw)
tiled = np.concatenate([coarse, coarse], axis=2)
@@ -124,7 +118,7 @@ class MultipatchGPU(unittest.TestCase):
s = FixedPatchSet2D.from_config(cfg, FACE)
coarse = state(16, 24, 5)
fine = level_fine(s, [state(*p.fine_shape, 6 + i) for i, p in enumerate(s.patches)])
kw = dict(blocks=((4, 4), (4, 4)))
kw = dict(fine_block=(4, 4), base_block=(4, 4))
leaf = s.fine_kind == ACTIVE
support = s.fine_layout((4, 4)).owned
outer = support & (s.fine_kind == OUTER_GHOST)
@@ -170,7 +164,7 @@ class MultipatchBodiesGPU(unittest.TestCase):
def run_steps(self, context, cfg, bounds, coarse, fine, switch=None, **kw):
out = []
with FixedPatchStepper(cfg, context=context, **levels(bounds, **kw)) as backend:
with FixedPatchStepper(cfg, bounds, context=context, **kw) as backend:
backend.initialize(coarse, fine)
for step in self.STEPS:
if switch is not None and step == 8:
@@ -188,7 +182,8 @@ class MultipatchBodiesGPU(unittest.TestCase):
p = FixedPatch2D.from_config(cfg, BOUNDS)
s = FixedPatchSet2D.from_config(cfg, (BOUNDS,))
coarse, fine = state(NY, NX, 1), state(*p.fine_shape, 2)
kw = dict(coarse_boundary=mode, blocks=((3, 4), (8, 4)), objects=[circle()])
kw = dict(coarse_boundary=mode, fine_block=(8, 4), base_block=(3, 4),
fine_objects=[circle()])
old = self.run_steps(context, cfg, BOUNDS, coarse, fine, **kw)
new = self.run_steps(context, cfg, (BOUNDS,), coarse, level_fine(s, [fine]), **kw)
for step, a, b in zip(self.STEPS, old, new):
@@ -207,17 +202,17 @@ class MultipatchBodiesGPU(unittest.TestCase):
two = FixedPatchSet2D.from_config(cfg32, self.PATCHES)
coarse, fine = state(16, 16, 3), state(*one.patches[0].fine_shape, 4)
jet = lambda x: Jet(Tube((x, 8.), 0., 6., .6), Parallel(0.), amplitude=.002)
kw = dict(blocks=((4, 4), (4, 4)))
kw = dict(fine_block=(4, 4), base_block=(4, 4))
with CudaContext(0) as context:
for jets in ((), (jet(5.),)):
ref = self.run_steps(context, cfg16, self.PATCHES[:1], coarse, level_fine(one, [fine]),
switch=[(0, -.001)] if jets else None,
objects=[body(8., 8.)], jets=jets, **kw)
fine_objects=[body(8., 8.)], jets=jets, **kw)
both = jets and (jet(5.), jet(21.))
got = self.run_steps(context, cfg32, self.PATCHES, np.concatenate([coarse, coarse], 2),
level_fine(two, [fine, fine]),
switch=[(0, -.001), (1, -.001)] if jets else None,
objects=[body(8., 8., 5), body(24., 8., 5)],
fine_objects=[body(8., 8., 5), body(24., 8., 5)],
jets=both or (), **kw)
for step, a, b in zip(self.STEPS, ref, got):
label = f"jets={len(jets)} step {step}"
@@ -27,16 +27,7 @@ from test_refinement_blocked_base import raw
CFG = LBMConfig(nx=16, ny=16, viscosity=.1)
L1, L2 = (4, 4, 12, 12), (8, 8, 16, 16)
B = (4, 4)
def levels(l1=L1, l2=L2, blocks=(B, B, B), objects=((), (), ()), jets=((), (), ())):
"""Level-list keywords for the scalar L1 patch with one scalar L2 child."""
return dict(refinements=[l1, l2], level_blocks=list(blocks),
level_objects=list(objects), level_jets=list(jets))
KW = levels()
KW = dict(fine_block=(4, 4), base_block=(4, 4), child=L2, child_block=(4, 4))
def child_body(spec=None):
@@ -61,29 +52,26 @@ def mocked_child():
patch.object(compiler, "load_module", return_value=module), \
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
patch.object(cuda, "memcpy_htod"):
return FixedPatchStepper(CFG, context=context,
**levels(objects=((), (), (child_body(),)), jets=((), (), (child_jet(),))))
return FixedPatchStepper(CFG, L1, context=context, child_objects=[child_body()],
child_jets=[child_jet()], **KW)
class ThreeLevelCPU(unittest.TestCase):
def test_rejections_before_cuda(self):
context = object() # never activated: every case must fail first
bad = (levels(blocks=(B, B, None)), levels(blocks=(B, None, B)),
levels(objects=((), (circle(),), ())), levels(l2=(4, 8, 16, 16)),
levels(blocks=(B, B, (5, 4))))
bad = (dict(KW, child_block=None), dict(KW, fine_block=None),
dict(KW, fine_objects=[circle()]), dict(KW, child=(4, 8, 16, 16)),
dict(KW, child_block=(5, 4)))
for kw in bad:
with self.subTest(kw=kw), self.assertRaises(ValueError):
FixedPatchStepper(CFG, context=context, **kw)
# L2 bodies/Jets without an L2 level (one entry too many), and an L2 Jet without a body.
two_level = dict(refinements=[L1], level_blocks=[None, None])
for kw in (dict(two_level, level_objects=[(), (), (child_body(),)]),
dict(two_level, level_jets=[(), (), (child_jet(),)]),
levels(jets=((), (), (child_jet(),)))):
FixedPatchStepper(CFG, L1, context=context, **kw)
for kw in (dict(child_objects=[child_body()]), dict(child_jets=[child_jet()]),
dict(KW, child_jets=[child_jet()])):
with self.assertRaises(ValueError):
FixedPatchStepper(CFG, context=context, **kw)
with self.assertRaises(ValueError): # several L1 patches + scalar child: not yet
FixedPatchStepper(LBMConfig(nx=32, ny=16, viscosity=.1), context=context,
**levels(l1=((4, 4, 12, 12), (20, 4, 28, 12))))
FixedPatchStepper(CFG, L1, context=context, **kw)
with self.assertRaises(ValueError): # several L1 patches + child: not yet
FixedPatchStepper(LBMConfig(nx=32, ny=16, viscosity=.1), ((4, 4, 12, 12), (20, 4, 28, 12)),
context=context, **KW)
def test_four_halo_bfl_steps_and_fresh_middle_feeds(self):
@@ -92,12 +80,11 @@ class ThreeLevelCPU(unittest.TestCase):
f, g = run._levels[1:]
b = g.boundary
events, feeds = [], []
advance, gather = run._advance, run._gather_halo
def record_feed(index, buffer, **kw): # index 2: L1 hop feeds L2
if index == 2:
self.assertIs(buffer, (f.current, f.spare)[len(feeds)])
feeds.append(buffer)
advance(index, buffer, **kw)
advance, gather = run._advance_child, run._gather_halo
def record_feed(buffer, **kw):
self.assertIs(buffer, (f.current, f.spare)[len(feeds)])
feeds.append(buffer)
advance(buffer, **kw)
def record_halo(level, buffer, **kw):
if level is g:
events.append(("halo", buffer))
@@ -108,7 +95,7 @@ class ThreeLevelCPU(unittest.TestCase):
self.assertIs(kind, g.kind)
events.append(("step", src))
g.kernel.side_effect = record_step
with patch.object(run, "_advance", side_effect=record_feed), \
with patch.object(run, "_advance_child", side_effect=record_feed), \
patch.object(run, "_gather_halo", side_effect=record_halo):
run.step()
self.assertEqual(feeds, [f.current, f.spare])
@@ -118,8 +105,8 @@ class ThreeLevelCPU(unittest.TestCase):
self.assertGreater(len(np.unique(b.tables["block"])), 1)
# Negative control: reusing L1's old source at the midpoint must fail.
feeds.clear()
with patch.object(run, "_advance", side_effect=lambda index, buffer, **kw:
record_feed(index, f.current if index == 2 else buffer, **kw)):
with patch.object(run, "_advance_child", side_effect=lambda buffer, **kw:
record_feed(f.current, **kw)):
with self.assertRaises(AssertionError):
run.step()
run.close()
@@ -168,7 +155,7 @@ class ThreeLevelGPU(unittest.TestCase):
def test_equilibrium_is_kept_on_every_level(self):
from CelerisLab.cuda.context import CudaContext
feq = np.asarray(compute_feq_d2q9(1.0, .03, -.02), np.float32)[:, None, None]
with CudaContext(0) as ctx, FixedPatchStepper(CFG, context=ctx, **KW) as run:
with CudaContext(0) as ctx, FixedPatchStepper(CFG, L1, context=ctx, **KW) as run:
shapes = ((16, 16), run.patch.fine_shape, run.child_patch.fine_shape)
run.initialize(*(np.broadcast_to(feq, (9, *s)).copy() for s in shapes))
run.step(8)
@@ -181,16 +168,15 @@ class ThreeLevelGPU(unittest.TestCase):
def test_nonequilibrium_masks_and_fresh_middle_feeds(self):
from CelerisLab.cuda.context import CudaContext
with CudaContext(0) as ctx, FixedPatchStepper(CFG, context=ctx, **KW) as run:
with CudaContext(0) as ctx, FixedPatchStepper(CFG, L1, context=ctx, **KW) as run:
p1, p2 = run.patch, run.child_patch
run.initialize(state(16, 16, 1), state(*p1.fine_shape, 2), state(*p2.fine_shape, 3))
f = run._levels[1]
feeds, original = [], run._advance
def record(index, buffer, **kw): # index 2: L1 hop feeds L2
if index == 2:
feeds.append(int(buffer))
original(index, buffer, **kw)
with patch.object(run, "_advance", side_effect=record):
feeds, original = [], run._advance_child
def record(buffer, **kw):
feeds.append(int(buffer))
original(buffer, **kw)
with patch.object(run, "_advance_child", side_effect=record):
cur, spare = int(f.current), int(f.spare)
run.step(1)
self.assertEqual(feeds, [cur, spare]) # current G, then the new G of hop 1
@@ -217,9 +203,8 @@ class ThreeLevelBodyGPU(unittest.TestCase):
CFG, velocity=0., wall_normal_velocity=.001, wall_slot_width=4,
y_wall_bc="on_site_velocity", top_wall_bc="reservoir")
with CudaContext(0) as ctx, FixedPatchStepper(
cfg, context=ctx, coarse_boundary=mode,
**levels(blocks=(B, B, block), objects=((), (), (child_body(spec),)),
jets=((), (), tuple(jets)))) as run:
cfg, L1, context=ctx, coarse_boundary=mode, child_objects=[child_body(spec)],
child_jets=jets, **dict(KW, child_block=block)) as run:
run.initialize(*(state(*level.shape[1:], i + 1) for i, level in enumerate(run._levels)))
snapshots = []
for stop in (0, 1, 2, 4, 8):
@@ -78,10 +78,10 @@ class ThreeLevelMultipatchGPU(unittest.TestCase):
def run_case(self, ctx, cfg=CFG, parents=PARENTS, children=CHILDREN,
mode='periodic', blocks=((4, 4), (4, 4)), objects=(), drive=(), switch=False,
scalar_child=None):
child = children if scalar_child is None else scalar_child
with FixedPatchStepper(config(cfg, mode), context=ctx, coarse_boundary=mode,
refinements=[parents, child], level_blocks=[(4, 4), blocks[0], blocks[1]],
level_objects=[(), (), objects], level_jets=[(), (), drive]) as run:
kwargs = dict(child_rectangles_in_parent=children) if scalar_child is None else dict(child=scalar_child)
with FixedPatchStepper(config(cfg, mode), parents, context=ctx, coarse_boundary=mode,
base_block=(4, 4), fine_block=blocks[0], child_block=blocks[1],
child_objects=objects, child_jets=drive, **kwargs) as run:
frame = frames(run)
run.initialize(*(physical_state(level.shape[1:], *f)
for level, f in zip(run._levels, frame)))
@@ -26,8 +26,9 @@ FP64_ATOL = 3e-6
def make(ctx):
return FixedPatchStepper(CFG, context=ctx, refinements=[PARENTS, A],
level_blocks=[(4, 4)] * 3)
return FixedPatchStepper(CFG, PARENTS, context=ctx, base_block=(4, 4),
fine_block=(4, 4), child_block=(4, 4),
child_rectangles_in_parent=A)
def initial(run, uniform):
@@ -136,8 +136,8 @@ class ShiftedJetsGPU(unittest.TestCase):
Parallel(0.),amplitude=.013)
for rho in (.99,1.01):
with self.subTest(rho=rho),CudaContext(0) as ctx:
with FixedPatchStepper(cfg,context=ctx,refinements=[(8,6,24,18)],level_blocks=[(4,4)]*2,
level_objects=[(),(obj,)],level_jets=[(),(whole,)],observe_raw_exchange=True) as run:
with FixedPatchStepper(cfg,(8,6,24,18),context=ctx,base_block=(4,4),fine_block=(4,4),
fine_objects=(obj,),jets=(whole,),observe_raw_exchange=True) as run:
run._initialize_raw(initial(run,rho,u=.013,v=0.))
expected=run._download_raw();ledger=Ledger(self,run,1)
self.assertTrue(run._level_boundaries[1].tables['aperture'].all())
@@ -153,8 +153,7 @@ class ShiftedJetsGPU(unittest.TestCase):
obj=circle((16.,12.),2.15);j=drive((16.,12.),2.15)
for rho in (.99,1.01):
with self.subTest(rho=rho),CudaContext(0) as ctx,ExitStack() as owned:
settings=dict(context=ctx,refinements=[(8,6,24,18)],level_blocks=[(4,4)]*2,
level_objects=[(),(obj,)],level_jets=[(),(j,)])
settings=dict(bounds=(8,6,24,18),context=ctx,base_block=(4,4),fine_block=(4,4),fine_objects=(obj,),jets=(j,))
measured=owned.enter_context(FixedPatchStepper(cfg,observe_raw_exchange=True,**settings))
plain=owned.enter_context(FixedPatchStepper(cfg,**settings))
measured._initialize_raw(initial(measured,rho));plain._initialize_raw(initial(plain,rho))
@@ -187,9 +186,8 @@ class ShiftedJetsGPU(unittest.TestCase):
parents=((4,4,20,28),(24,4,44,28));a=((16,16,32,40),(56,16,72,40));b=(a[0],(60,16,76,40))
obj=circle((12.,14.),.8);j=drive((12.,14.),.8)
with CudaContext(0) as ctx,ExitStack() as owned:
old=owned.enter_context(FixedPatchStepper(cfg,context=ctx,refinements=[parents,a],
level_blocks=[(4,4)]*3,level_objects=[(),(),(obj,)],level_jets=[(),(),(j,)],
observe_raw_exchange=True))
old=owned.enter_context(FixedPatchStepper(cfg,parents,context=ctx,base_block=(4,4),fine_block=(4,4),
child_block=(4,4),child_rectangles_in_parent=a,child_objects=(obj,),child_jets=(j,),observe_raw_exchange=True))
old._initialize_raw(initial(old,.99));ledger=Ledger(self,old,2)
for speed in (.002,-.001):old.set_jet_amplitude(0,speed,level=2);ledger.step()
before=old._download_raw();count=old.step_count
@@ -22,8 +22,9 @@ B=((20,16,36,40),)
def make(ctx):
return FixedPatchStepper(CFG,context=ctx,refinements=[PARENTS,A],
level_blocks=[(4,4)]*3)
return FixedPatchStepper(CFG,PARENTS,context=ctx,base_block=(4,4),
fine_block=(4,4),child_block=(4,4),
child_rectangles_in_parent=A)
def initial(run):
-222
View File
@@ -1,222 +0,0 @@
"""Steady DFG 2D-1 (Re 20): refined and uniform runs against the official reference intervals.
Not a full benchmark certification. Declared deviations from the official setup:
pressure-NEQ outlet (``neq_extrap``) instead of do-nothing, local Zou-He inlet.
Geometry, inflow profile, initial state, refined layout and observation reuse the
pinned Re100 runner (``run_refined_dfg``) and ``re100_helpers`` unchanged; only the
lattice viscosity is set for Re 20 (nu = U_lattice * D / 20 at the root level).
Units. The helpers use scaled DFG units with mean inflow speed 1, so
Cd = 2 Fx / (rho U^2 D) and Cl = 2 Fy / (rho U^2 D) are scale-free, and
``pressure_probes`` returns dp / (rho U^2). The official p_diff = p(0.15, 0.2) -
p(0.25, 0.2) uses U = 0.2 m/s, rho = 1, so p_diff = dp * 0.04. Both are reported;
only p_diff is compared with the official interval. Forces use the existing sign
convention: F is the force of the fluid on the cylinder, i.e. the refined raw
exchange impulse (window ledger, ``total.impulse``) or the uniform ``read_force``,
with x along the inflow and y up from the bottom wall. Cd must be positive, and the
official Cl is positive (centre 0.20 below the centreline 0.205), so a flipped sign
shows up as Cl outside its interval.
Sampling. One sample every SAMPLE_TSTAR (t* = U t / D; 0.25 s), taken at the end of
a completed root step with all levels synchronized; the step index is recorded. Cd
and Cl are means over the sample interval (window exchange ledger refined, per-batch
forces uniform), which filters acoustic and step-parity oscillation; p_diff is the
end-of-interval value at the exact surface points.
Steady rule (our engineering choice, frozen before any run; not official). Over the
trailing WINDOW_TSTAR (t* 50..60 inclusive, 21 samples), for each of Cd, Cl and
p_diff: range (max - min) <= 10 % and |least-squares slope * window| <= 2 % of the
width of its official interval. Absolute limits, so the small Cl is never a divisor;
the slope term bounds slow monotone drift that the range alone would pass. If the
rule fails at TSTAR_MAX the run reports "not steady" and gives no interval verdict;
limits are never relaxed after a run. The verdict uses the window mean, and the
distance of each mean to its nearest interval edge is reported (negative outside).
"""
import argparse
import json
import math
import os
from pathlib import Path
import sys
import time
from tempfile import TemporaryDirectory
import numpy as np
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import run_refined_dfg as rr # noqa: E402
RE = 20.0
U_PHYS = 0.2 # m/s mean inflow; rho = 1
TSTAR_MAX = 60.0 # 30 s physical
SAMPLE_TSTAR = 0.5 # 0.25 s
WINDOW_TSTAR = 10.0 # trailing 5 s, 21 samples
# Schaefer & Turek (1996), 2D-1 lower/upper bounds.
BANDS = dict(cd=(5.57, 5.59), cl=(0.0104, 0.0110), p_diff=(0.1172, 0.1176))
STEADY_RANGE = {k: (hi - lo) / 10 for k, (lo, hi) in BANDS.items()}
STEADY_SLOPE = {k: (hi - lo) / 50 for k, (lo, hi) in BANDS.items()}
DEVIATIONS = ("pressure-NEQ outlet (neq_extrap), not do-nothing", "local Zou-He inlet")
_re100_config = rr.config
def config(h, diameter, collision="SRT", jet_scenario="none"):
"""The pinned Re100 configuration with the lattice viscosity set for Re 20."""
if jet_scenario != "none":
raise ValueError("DFG 2D-1 runs have no Jets")
cfg, body, p = _re100_config(h, diameter, collision, jet_scenario)
nu = p["mean_speed"] * p["diameter"] / RE
cfg["physics"]["viscosity"] = nu
return cfg, body, dict(p, viscosity=nu, re=RE)
def lattice_parameters(diameter, speed=rr.SPEED):
nu = speed * diameter / RE
return dict(viscosity=nu, tau=0.5 + 3 * nu, steps=math.ceil(TSTAR_MAX * diameter / speed),
interval=round(SAMPLE_TSTAR * diameter / speed))
def sample(impulse, n, diameter, dp):
den = rr.SPEED**2 * diameter / 2
return dict(cd=float(impulse[0] / n / den), cl=float(impulse[1] / n / den),
dp_scaled=float(dp), p_diff=float(dp) * U_PHYS**2)
def summarize(rows):
"""Steady rule over the trailing window, then the interval verdict (or none)."""
if not rows or rows[-1]["tstar"] < TSTAR_MAX - 1e-9:
return dict(status="INCOMPLETE", steady=False, verdict=None)
window = [r for r in rows if r["tstar"] >= TSTAR_MAX - WINDOW_TSTAR - 1e-9]
t = np.array([r["tstar"] for r in window])
ranges = {k: max(r[k] for r in window) - min(r[k] for r in window) for k in BANDS}
drifts = {k: abs(np.polyfit(t, [r[k] for r in window], 1)[0]) * WINDOW_TSTAR for k in BANDS}
steady = all(ranges[k] <= STEADY_RANGE[k] and drifts[k] <= STEADY_SLOPE[k] for k in BANDS)
final = {k: float(np.mean([r[k] for r in window])) for k in BANDS}
edge = {k: min(final[k] - BANDS[k][0], BANDS[k][1] - final[k]) for k in BANDS}
result = dict(window_samples=len(window), ranges=ranges, drifts=drifts,
range_limits=STEADY_RANGE, slope_limits=STEADY_SLOPE, steady=steady,
window_mean=final, edge_distance=edge, bands=BANDS,
deviations=DEVIATIONS, cd_positive=final["cd"] > 0)
if not steady:
return dict(result, status="NOT_STEADY", verdict=None)
inside = {k: BANDS[k][0] <= final[k] <= BANDS[k][1] for k in BANDS}
return dict(result, status="WITHIN_INTERVALS" if all(inside.values()) else "OUTSIDE_INTERVALS",
verdict=inside)
def healthy(row):
return row["max_mach"] <= 0.2 and row["rho_min"] >= 0.5 and row["rho_max"] <= 1.5
def run_case(a, h, case):
from contextlib import ExitStack
from CelerisLab.cuda.context import CudaContext
from CelerisLab.lbm.descriptors import FLUID
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
from CelerisLab.simulation import Simulation
refined = case == "refined"
cfgdict, bodydict, p = config(h, 40 if refined else 80, a.collision)
lp = lattice_parameters(p["diameter"])
rows = []
with ExitStack() as stack:
if refined:
cfg, pair, body, host = rr.host_geometry(a.source, h, a.collision)
context = stack.enter_context(CudaContext(0))
backend = stack.enter_context(FixedPatchStepper(
cfg, context=context, coarse_boundary="channel", refinements=[rr.PLACEMENTS["wide"]],
level_blocks=[rr.BASE_BLOCK, rr.FINE_BLOCK], level_objects=[(), (body,)],
observe_raw_exchange=True, observe_window_exchange=True))
from CelerisLab.lbm.channel import channel_masks
from CelerisLab.lbm.refinement import ACTIVE
from CelerisLab.lbm.descriptors import SOLID
masks = [channel_masks(pair.coarse_shape)[0] & ~pair.covered,
(pair.fine_kind == ACTIVE)
& ((host.flags.reshape(pair.fine_shape) & SOLID) == 0)]
backend.initialize(rr.initial_state(pair.coarse_shape, (0.0, 0.0), 1.0, root=True),
rr.initial_state(pair.fine_shape, pair.fine_origin, 0.5))
probe_p = dict(p, center=[(80.5 - pair.fine_origin[0]) / 0.5 - 0.5,
(81.0 - pair.fine_origin[1]) / 0.5 - 0.5], radius=40.0)
else:
tmp = Path(stack.enter_context(TemporaryDirectory()))
(tmp / "lbm.json").write_text(json.dumps(cfgdict))
(tmp / "body.json").write_text(json.dumps(bodydict))
sim = stack.enter_context(Simulation(str(tmp / "lbm.json"), str(tmp / "body.json"),
device_id=0))
sim.initialize()
probe_p = p
fluid = (sim.field.flag.reshape(p["ny"], p["nx"]) & FLUID) != 0
begin = 0
while begin < lp["steps"]:
rr.check_deadline(a.deadline)
n = min(lp["interval"], lp["steps"] - begin)
if refined:
impulse = rr.advance_refined_interval(backend, n, a.deadline, "window")
fields = [rr.macros(g) for g in backend.download()]
rho, ux, uy = fields[1]
fluid = masks[1]
vals = [tuple(v[m] for v in f) for m, f in zip(masks, fields)]
else:
impulse, done = np.zeros(2), 0
while done < n:
batch = min(100, n - done)
sim.run(batch)
impulse += sim.read_force(0).astype(np.float64) * batch
done += batch
m = sim.get_macroscopic()
rho, ux, uy = [np.asarray(m[k], np.float64).reshape(p["ny"], p["nx"])
for k in ("rho", "ux", "uy")]
vals = [(rho[fluid], ux[fluid], uy[fluid])]
begin += n
row = dict(step=begin, tstar=begin * rr.SPEED / p["diameter"],
**sample(impulse, n, p["diameter"],
h.pressure_probes(rho, fluid, probe_p)["dp"]),
rho_min=float(min(v[0].min() for v in vals)),
rho_max=float(max(v[0].max() for v in vals)),
max_mach=float(max(np.sqrt(3 * (v[1]**2 + v[2]**2)).max() for v in vals)))
rr.ensure_finite(row)
rows.append(row)
if not healthy(row):
break
summary = summarize(rows) if healthy(rows[-1]) else dict(status="UNHEALTHY", steady=False,
verdict=None)
return dict(case=case, collision=a.collision, lattice=lp, rows=rows, **summary)
def plan(collision="SRT"):
return dict(re=RE, collision=collision, tstar_max=TSTAR_MAX, sample_tstar=SAMPLE_TSTAR,
window_tstar=WINDOW_TSTAR, bands=BANDS, range_limits=STEADY_RANGE, slope_limits=STEADY_SLOPE,
deviations=DEVIATIONS, pressure_scale=U_PHYS**2,
refined_root=lattice_parameters(40), refined_fine_tau=0.5 + 3 * 2 * rr.SPEED * 40 / RE,
uniform=lattice_parameters(80),
label="compared against the official reference bands (non-canonical neq_extrap outlet, zou_he_local inlet); not a full benchmark certification")
def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--case", choices=["refined", "uniform", "both"], default="refined")
parser.add_argument("--collision", choices=["SRT", "MRT"], default="SRT")
parser.add_argument("--source", type=Path, default=rr.DEFAULT_SOURCE)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--plan", action="store_true", help="Write the frozen plan; no CUDA")
parser.add_argument("--max-wall-seconds", type=float)
a = parser.parse_args()
a.source = a.source.resolve()
a.output.mkdir(parents=True, exist_ok=False)
(a.output / "plan.json").write_text(json.dumps(plan(a.collision), indent=2, allow_nan=False))
if a.plan:
return
if not a.max_wall_seconds or a.max_wall_seconds <= 0 or not os.environ.get("CUDA_VISIBLE_DEVICES"):
raise SystemExit("GPU run needs --max-wall-seconds > 0 and explicit CUDA_VISIBLE_DEVICES")
h, _ = rr.helpers(a.source)
rr.config = config # host_geometry builds its root configuration through this name
a.deadline = time.monotonic() + a.max_wall_seconds
cases = ["refined", "uniform"] if a.case == "both" else [a.case]
results = [run_case(a, h, case) for case in cases]
(a.output / "result.json").write_text(json.dumps(results, indent=2, allow_nan=False))
if __name__ == "__main__":
main()
+6 -23
View File
@@ -74,25 +74,7 @@ def cv(g, fluid, origin, rectangle, nu, h=0.5):
Cauchy stress from centred velocity gradients, NOT postcollision non-equilibrium.
Rectangle faces lie on cell edges; fluid mask excludes the staircase cylinder.
"""
return _cv_quantities(*moments(g), fluid, origin, rectangle, nu, h)
def cv_from_macros(rho, ux, uy, fluid, origin, rectangle, nu, h=0.5):
"""Same CV calculation from public macros, in one common length/time frame.
``origin`` is the lower cell edge, ``h`` the cell width, and ``nu`` uses
that frame. Returns [mass, Px, Py] inventory and outward total flux.
Crop fields/mask together and translate origin before calling if desired.
No boundary-exchange observation enters this independent estimator.
"""
rho = np.asarray(rho, np.float64)
if rho.ndim != 2 or any(np.shape(v) != rho.shape for v in (ux, uy, fluid)):
raise ValueError("CV macros and fluid mask must have matching 2D shapes")
vel = np.asarray([ux, uy], np.float64)
return _cv_quantities(rho, rho[None] * vel, vel, fluid, origin, rectangle, nu, h)
def _cv_quantities(rho, mom, vel, fluid, origin, rectangle, nu, h):
rho, mom, vel = moments(g)
x0, y0, x1, y1 = np.rint((np.array(rectangle) - np.tile(origin, 2)) / h).astype(int)
np.testing.assert_allclose(
np.array([x0, y0, x1, y1]) * h + np.tile(origin, 2), rectangle, atol=1e-12
@@ -279,11 +261,12 @@ def run_case(c, ctx, output, provenance, continuation=False):
PeriodicBlockStepper(fine_cfg, layout, context=ctx, objects=[fobj]) as uniform,
FixedPatchStepper(
cfg,
c["bounds"],
context=ctx,
refinements=[c["bounds"]],
level_blocks=[(8, 8), (8, 8)],
level_objects=[(), (obj,)],
level_jets=[(), tuple(jets)],
base_block=(8, 8),
fine_block=(8, 8),
fine_objects=[obj],
jets=jets,
observe_raw_exchange=True,
) as amr,
):

Some files were not shown because too many files have changed in this diff Show More