Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aa3889c6b | ||
|
|
b01f38ca7a | ||
|
|
fb8349b7a3 | ||
|
|
a2cbca8401 | ||
|
|
8320ac672f | ||
|
|
5e7cf3e78c | ||
|
|
ab8407c968 | ||
|
|
12d3f11dd8 | ||
|
|
cba6bca19e | ||
|
|
85c34cbf7f | ||
|
|
22d0b24013 | ||
|
|
e81a103f30 | ||
|
|
90849a55ca | ||
|
|
7aa66ff054 | ||
|
|
d449c63137 | ||
|
|
3b9efd9696 | ||
|
|
8e378aa047 |
@@ -112,21 +112,18 @@ 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.hierarchy.pairs[0].coarse_shape)),
|
||||
np.broadcast_to(rest, (9, *run.hierarchy.pairs[0].fine_shape)))
|
||||
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
|
||||
np.broadcast_to(rest, (9, *run.patch.fine_shape)))
|
||||
run.step(128) # coarse steps; fine level takes two per step
|
||||
coarse_g, fine_g = run.download() # NaN on covered, ghost and solid cells
|
||||
```
|
||||
|
||||
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)`:
|
||||
`run.hierarchy.pairs[0].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
|
||||
- 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
|
||||
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:
|
||||
run.initialize(np.broadcast_to(rest, (9, *run.hierarchy.pairs[0].coarse_shape)),
|
||||
np.broadcast_to(rest, (9, *run.hierarchy.pairs[0].fine_shape))) # (9, 32, 64)
|
||||
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,40 +182,24 @@ 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:
|
||||
shapes = (run.hierarchy.pairs[0].coarse_shape, run.hierarchy.pairs[0].fine_shape, run.hierarchy.pairs[1].fine_shape)
|
||||
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)
|
||||
run.set_jet_amplitude(0, -.002, level=2) # explicit level selects the driven level
|
||||
run.set_jet_amplitude(0, -.002, level=2) # default level=1 still addresses L1
|
||||
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
|
||||
|
||||
|
||||
@@ -1,57 +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.
|
||||
|
||||
## Redundant periodic stepper alias removed
|
||||
|
||||
`PeriodicPatchStepper` is no longer exported from `lbm.refinement_stepper`.
|
||||
Use `FixedPatchStepper` with the same arguments, including explicit
|
||||
`level_blocks`. The old name was an alias for that exact class; periodic
|
||||
boundaries remain the default. Existing imports of the old name must be updated.
|
||||
|
||||
## 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.
|
||||
|
||||
## 7afb5ad: `Simulation` fails closed after a Jet amplitude device-write failure
|
||||
|
||||
`Simulation.set_jet_amplitude` previously only documented that a device error
|
||||
leaves the per-link velocity column undefined; the instance stayed usable.
|
||||
Now any error after input validation (internal stream creation or the upload)
|
||||
marks the Simulation failed. Every later device-touching public method,
|
||||
including the `stream` property (guard added in the follow-up repair commit on
|
||||
`claude/jet-update-consistency`), raises
|
||||
`RuntimeError("Simulation failed (...); close() and recreate it")`. The host
|
||||
amplitude keeps its previous value. `close()` still works and stays retryable;
|
||||
cached host readers and host staging calls are not blocked. Invalid input
|
||||
(`ValueError`/`IndexError`) is unchanged: it raises before any change and the
|
||||
instance stays usable. `FixedPatchStepper.set_jet_amplitude` already failed the
|
||||
instance (status `failed`) and is unchanged.
|
||||
@@ -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.
|
||||
@@ -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_finest` 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.
|
||||
@@ -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) remained SRT-only; it was removed in 8e1feff and its single-level capability, still SRT-only, is `FixedPatchStepper(refinements=[])`.
|
||||
|
||||
## 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,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`:
|
||||
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
# src coherence stage (from main 914906e)
|
||||
|
||||
Owner direction 2026-10-28: unified style over historical compatibility, inside `src`.
|
||||
No numerical change is intended. Stages after 5f69eba: 65339ab (one boundary/Jet record,
|
||||
host preparation helpers, pair accessors), c5a3cdb (`block_collide` families), 442979b
|
||||
(exchange metadata on the boundary record). Each was checked by parent/candidate
|
||||
config.h/PTX comparison and host/GPU equality runs; c5a3cdb changed the PTX of the two
|
||||
channel-root SRT modules only (omega load/clamp moved, outputs exact).
|
||||
|
||||
## Conventions (defined once, in `lbm.hierarchy.Hierarchy`)
|
||||
|
||||
- Level `l = 0 .. depth-1` (0 = root). Pair `hierarchy.pairs[l-1]` builds level `l`.
|
||||
- Array shapes are `(ny, nx)`; rectangles, origins and centers are `(x, y)`.
|
||||
- Coarse/fine name the two members (parent/child) of ANY pair, not absolute L0/L1:
|
||||
`fine_shape`, `fine_origin`, `prepare_fine_boundary`, `blocked_fine_tables`,
|
||||
`blocked_coarse_tables`.
|
||||
- Refined levels (l >= 1) of FixedPatchStepper place bodies/Jets in the root EDGE frame
|
||||
(root cell `i` = `[i, i+1)`). Uniform `Simulation` objects AND the static circles of a
|
||||
single-level (depth 1) FixedPatchStepper use the integer-NODE frame. Nothing converts
|
||||
between them. This concerns input placement only: within the EDGE frame, refined
|
||||
levels still project root-edge coordinates to fine-local cell indices with the
|
||||
half-cell shift (`prepare_fine_boundary`: `(v - o)/dx - .5`; hits back via `fine_hits_to_edge_frame`
|
||||
`origin + dx*(hit + .5)`), using `edge_frame`/`fine_origin` below.
|
||||
- `Hierarchy.edge_frame(l)` -> `((ox, oy), h)`: cell `(i, j)` of level `l`'s allocated
|
||||
array (ghost band and holes included) covers `[o + h*i, o + h*(i+1))`. A scalar
|
||||
FixedPatch2D window carries the half-coarse-cell ghost shift `bounds - HALO/2` in
|
||||
`fine_origin`; a set's level-wide grid has origin 0. Level 1 boundaries are built from
|
||||
the root config (the wall-aware fields live there); deeper ones from the parent frame.
|
||||
- `FixedPatch2D.fine_level_offset` (was `fine_level_origin`) is an array INDEX offset
|
||||
`(y, x)`, not an `(x, y)` origin.
|
||||
|
||||
## FixedPatchStepper migration map
|
||||
|
||||
Historical: left column = names at or after 914906e that no longer exist; right column =
|
||||
the current name.
|
||||
|
||||
| removed / changed | replacement |
|
||||
|---|---|
|
||||
| `patch`, `child_patch` | `hierarchy.pairs[0]`, `hierarchy.pairs[1]` |
|
||||
| `base_layout`, `fine_layout`, `child_layout` (stepper properties) | `layouts[0]`, `layouts[1]`, `layouts[2]` (`FixedPatchSet2D.fine_layout(block)` is unchanged) |
|
||||
| `fine_boundary`, `child_boundary` | `boundaries[1]`, `boundaries[2]` (host geometry; `None` = no bodies/Jets) |
|
||||
| `_jets` | `jets[1]` |
|
||||
| `_boundaries[l]` (dict from L1, level keys), `_level_boundaries` (b4eacf7), `_device_boundaries` (65339ab) | `_boundaries[l]`: the one combined per-level `_BlockedBoundary` record (list from L0, `None` at L0 unless single-level static circles): host geometry `.host`, `.tables`, `.jets`, `.jet_cache`, exchange `.spacing`/`.centers`, device handles |
|
||||
| `_fine_tables`, `_actuated`, `_jet_cache` | `_boundaries[1].tables` / `.actuated` / `.jet_cache` |
|
||||
| `_level_jets` (private) | `_boundaries[l].jets` (public `jets[l]` is derived) |
|
||||
| `_exchange_spacing[l]`, `_exchange_centers[l]` (private) | `_boundaries[l].spacing` (h_l, root units) / `.centers` (root edge frame); only levels with a boundary report exchange |
|
||||
| `_level_blocks` (private) | `_layouts[l].block` |
|
||||
| `_remap_cfg` (private) | `_root_cfg` |
|
||||
| `refinement_tables.blocked_base_tables` (also via `refinement_stepper`) | `refinement_tables.blocked_coarse_tables` (import from `refinement_tables`; the stepper no longer re-exports) |
|
||||
| `_fine_leaf`, `_child_leaf` | `leaf[1]`, `leaf[2]` (copies) |
|
||||
| `_middle_kind` | `hierarchy.kinds[1]` |
|
||||
| `_boundary_hosts` (private) | public `boundaries[l]` = `_boundaries[l].host` (host geometry only; the private record is the combined one) |
|
||||
| `remap_child(rects, child_objects=...)` | `remap_finest(rects, objects=...)` |
|
||||
| `set_jet_amplitude(i, a)` (implicit level 1) | `set_jet_amplitude(i, a, level=l)` (required) |
|
||||
| `read_raw_exchange(b)` / `read_exchange_window(b)` (implicit level 1) | `read_raw_exchange(b, level=l)` / `read_exchange_window(b, level=l)` (required) |
|
||||
| `_level_name(l)` "base/fine/child" messages | `"level l"` |
|
||||
| `refinement.MIN_PATCH_GAP` | deleted (no gap policy exists) |
|
||||
| `params.cuh HaloPlan`, `collide_trt_no_force` | deleted (zero references) |
|
||||
|
||||
Kept: `step_count`, constructor keywords (`refinements`, `level_blocks`, `level_objects`,
|
||||
`level_jets`, `coarse_boundary`, `explosion`), `Simulation.set_jet_amplitude(i, a)` (one level),
|
||||
`RemapTopology.from_config(..., child_objects=, child_jets=)` (internal remap helper; "child" =
|
||||
the replaced level relative to its parent).
|
||||
|
||||
## Responsibilities
|
||||
|
||||
- Construction: `_check_levels` (inputs); host preparation, numpy only, before any device
|
||||
work: `_level_layouts`, `_kernel_configs` (per-level kernel configs), `_host_boundaries`,
|
||||
`_prepare_blocked_boundary`, `_fluid_leaves`; `Hierarchy.edge_frame` (geometry frames);
|
||||
then `_planned_bytes()` (memory preflight) and `_build_level` / `_build_pair` /
|
||||
`_upload_boundary` (device). A construction failure closes the partial instance.
|
||||
Observation reads share `_observed_boundary` + `_summarize`.
|
||||
- Pair accessors: `rectangles` (a scalar FixedPatch2D gives `(bounds,)`) and
|
||||
`fine_level_offset` (`(0, 0)` for a set) on both pair types; the types and their
|
||||
storage frames stay distinct.
|
||||
- Per-level state, by lifecycle:
|
||||
- Fixed plan structure (built once in `__init__`, read by `_planned_bytes()` before any
|
||||
allocation, readable after `close()` through the public views): `_layouts`,
|
||||
`_level_cfgs`, `_leaf`, `_state`, and in each `_boundaries[l]` record the host geometry
|
||||
`.host`, link tables, `.jet_cache`, `.spacing`, `.centers`.
|
||||
- Mutable boundary data in the same record: `.jets` and `.tables["surface_velocity"]`,
|
||||
changed only by `set_jet_amplitude` (host prepare -> device write -> commit; an uncertain
|
||||
write fails the instance); device handles (`.flags`, `.device`, `.kernel`, ledgers) are
|
||||
set by `_upload_boundary` and cleared by `close()`.
|
||||
- Runtime: `_levels` (`_Level`) holds compiled modules, kernels, the two population
|
||||
buffers (swapped per step) and transfer/kind handles. It is created only after the
|
||||
memory preflight and cleared on `close()`; `.layout`/`.boundary` reference the plan for
|
||||
`_hop`. Folding the plan into `_Level` would mix both lifecycles in one record.
|
||||
- Blocked kernels: `operators/block_collision.cuh block_collide` is the one collision extension
|
||||
point of `BlockStep` and `BlockChannelStep`. It computes rho/u once and reads no runtime
|
||||
force (`d_params.fx/fy`). Two compile-time families: uniform-equivalent (MRT on any level,
|
||||
SRT on the channel root `BLOCK_ROOT_BC == 2`): clamped omega, `collide_mrt_no_force` or
|
||||
`collide_srt` with its zero Guo term; and level SRT (`BLOCK_ROOT_BC` 0/1): unclamped omega,
|
||||
`collide_srt_no_force`, with the shifted direct-momentum feq under `USE_DDF_SHIFTING`.
|
||||
The uniform path keeps `collide_dispatch` (runtime force, LES, TRT).
|
||||
- `explosion="projected_linear"` stays an optional capability; `"constant"` stays default.
|
||||
|
||||
## Not done / open
|
||||
|
||||
- Two per-level config derivations stay distinct on purpose: `Hierarchy.configs`
|
||||
(`_middle_config`, wall fields reset on every refined level, geometry validation) vs.
|
||||
`FixedPatchStepper._kernel_configs` (reset only on level 1 of a slot_reservoir root;
|
||||
generates each level's config.h). Folding them needs a config.h/PTX diff gate.
|
||||
- No single-`Simulation` facade over FixedPatchStepper: the two own different state models
|
||||
(uniform node frame, body sync, forcing, checkpoint vs. fixed blocked hierarchy, edge
|
||||
frame, remap-as-new-instance). A facade would only forward calls; no concrete need found.
|
||||
- Deferred, each needing its own evidence first: sharing the channel-root cell/donor selector
|
||||
with OneStep (the closure functions are already shared; a shared selector changes uniform
|
||||
signatures and needs a uniform PTX gate); merging FixedPatch2D into FixedPatchSet2D
|
||||
(different origin, ghost band, layout size and addressing; needs a geometry/parity table and
|
||||
would need tiles that are not grid-aligned); one `clamp_omega` helper for uniform and
|
||||
blocked paths (it lives in the hash-pinned LES header `turbulence_smag.cuh`).
|
||||
- Tests, examples and validation callers are migrated separately (pinned runners stay frozen).
|
||||
@@ -1,393 +0,0 @@
|
||||
# Evidence index (draft, R0)
|
||||
|
||||
Active-tree evidence at candidate 0f8fdbc30ea640c764b38ee598d8f4d135c81c85, classified for the release cleanup
|
||||
(thread C0C78UFKGRM 1793021833.593279). Draft for review: nothing is removed by this file.
|
||||
Recovery of any listed file: `git show 0f8fdbc3:<path>` (blob id given; an immutable tag is requested as anchor).
|
||||
|
||||
Classes: K = conclusion kept in the active tree; K-code = tested research code kept; D-snapshot = pre-Gitea
|
||||
patch/diff transport (code landed in git from ed1a6ee on; not yet verified hunk by hunk); P-pin = generated hash inventories, git-status dumps and manifests;
|
||||
R-raw = run outputs, logs, signals, frames, one-shot run scripts and handoff notes. D, P, R leave the active tree in R1.
|
||||
No file under src/ or a test reads any of these paths (git grep at 0f8fdbc3); runners under tests/validation
|
||||
that read research/j* are handled in R2.
|
||||
|
||||
| Class | Files | Bytes |
|
||||
|---|---:|---:|
|
||||
| K | 31 | 196,506 |
|
||||
| K-code | 3 | 24,324 |
|
||||
| D-snapshot | 18 | 338,948 |
|
||||
| P-pin | 50 | 979,177 |
|
||||
| R-raw | 142 | 3,790,554 |
|
||||
|
||||
## (root)
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| BASELINE_JET_AMR.txt | R-raw | 5,244 | 62aa2a66dc05 |
|
||||
|
||||
## output
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| round3_compatibility_summary.md | R-raw | 4,366 | f243318f5591 |
|
||||
|
||||
## research/fine_surface_20261014
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 54,507 | 71a4543ead37 |
|
||||
| cpu_device_guard_cmd.txt | R-raw | 241 | 06e0f35d92b3 |
|
||||
| cpu_device_guard_exit.txt | R-raw | 2 | 573541ac9702 |
|
||||
| cpu_device_guard_stdout.txt | R-raw | 98 | 424d584a031c |
|
||||
| cpu_existing_runs.json | R-raw | 650 | 3179536407b7 |
|
||||
| cpu_final_run.json | R-raw | 386 | 523fadb3fec9 |
|
||||
| cpu_final_stdout.txt | R-raw | 99 | 9cfe73da3858 |
|
||||
| cpu_lifecycle_run.json | R-raw | 387 | 5b403b9e2f30 |
|
||||
| cpu_lifecycle_stdout.txt | R-raw | 112 | 3c5c49b285fa |
|
||||
| cpu_maps_stdout.txt | R-raw | 95 | 71dd87f9faf9 |
|
||||
| cpu_preallocation_run.json | R-raw | 337 | fa6e96d98cdd |
|
||||
| cpu_preallocation_stdout.txt | R-raw | 98 | 5d643576f251 |
|
||||
| cpu_shared_kernel_stdout.txt | R-raw | 98 | 8a0c046bb9b1 |
|
||||
| cpu_surface_command.txt | R-raw | 113 | 2a5ff31e8d06 |
|
||||
| cpu_surface_exit.txt | R-raw | 2 | 573541ac9702 |
|
||||
| cpu_surface_projection_command.txt | R-raw | 171 | 70b7f5c62c1a |
|
||||
| cpu_surface_projection_exit.txt | R-raw | 2 | 573541ac9702 |
|
||||
| cpu_surface_projection_stdout.txt | R-raw | 113 | efd37a149cc1 |
|
||||
| cpu_surface_stdout.txt | R-raw | 99 | 9cfe73da3858 |
|
||||
| gpu_preparation_run1.py | R-raw | 14,043 | ee586d035b7a |
|
||||
| gpu_run.json | R-raw | 2,943 | 15b8ba70d528 |
|
||||
| gpu_stdout.txt | R-raw | 5,424 | c5216e0267f3 |
|
||||
| gpu_test_run1.py | R-raw | 15,479 | f30376abc97f |
|
||||
| incremental.diff | D-snapshot | 53,853 | 318d92e7d177 |
|
||||
| manifest.json | P-pin | 8,411 | 8281338416af |
|
||||
| report.txt | K | 8,373 | 6b9e5123e414 |
|
||||
|
||||
## research/fine_surface_joint_20261014
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline_00.json | P-pin | 21,302 | 4ae101ebd02b |
|
||||
| baseline_01.json | P-pin | 26,191 | 548cf7b3b494 |
|
||||
| baseline_02.json | P-pin | 22,440 | e0421e7a4052 |
|
||||
| baseline_03.json | P-pin | 8,584 | da99b45f32a4 |
|
||||
| blow_boundary.csv | R-raw | 12,536 | e13363b69457 |
|
||||
| blow_collisions.csv | R-raw | 19,145 | 5d5b395486bb |
|
||||
| blow_ledger.csv | R-raw | 15,977 | b43bbfd0b8d2 |
|
||||
| blow_propagation.csv | R-raw | 9,714 | f06d80d296ca |
|
||||
| blow_signals.npz | R-raw | 221,114 | 528adf75baa2 |
|
||||
| blow_summary.json | R-raw | 4,113 | 32285ac15be3 |
|
||||
| command.txt | R-raw | 232 | 510ecb2925dd |
|
||||
| criteria.txt | K | 7,480 | dd36e76a3be1 |
|
||||
| frozen.json | R-raw | 11,972 | b5a3324e8686 |
|
||||
| gpu_run.json | R-raw | 1,955 | 67ac4785f7e4 |
|
||||
| incremental.diff | D-snapshot | 625 | e03c132d22df |
|
||||
| integrity.json | P-pin | 826 | a8b6e4be88d2 |
|
||||
| manifest.json | P-pin | 4,388 | d2af50260c39 |
|
||||
| regions.json | R-raw | 27,525 | 7bba26904221 |
|
||||
| report.txt | K | 8,207 | d7571e068ad3 |
|
||||
| result.json | R-raw | 11,442 | 9580c2686e70 |
|
||||
| review.txt | K | 2,117 | bbe1d8a83d47 |
|
||||
| runner.py | R-raw | 5,847 | 812973acc000 |
|
||||
| stdout.txt | R-raw | 19,940 | c9218e2e48fc |
|
||||
| suction_boundary.csv | R-raw | 12,505 | b870be05e6c0 |
|
||||
| suction_collisions.csv | R-raw | 19,173 | 2de48c0a84ab |
|
||||
| suction_ledger.csv | R-raw | 16,208 | 92cf1f44911e |
|
||||
| suction_propagation.csv | R-raw | 9,720 | f017bcc3ca6f |
|
||||
| suction_signals.npz | R-raw | 220,058 | 66d06cf2d909 |
|
||||
| suction_summary.json | R-raw | 4,126 | d70a1d371950 |
|
||||
| validate.py | R-raw | 19,576 | 37441febd168 |
|
||||
| zero_boundary.csv | R-raw | 11,491 | 65c92e48c974 |
|
||||
| zero_collisions.csv | R-raw | 18,915 | 027cb8ff6b23 |
|
||||
| zero_ledger.csv | R-raw | 16,051 | 34a99bb89dd7 |
|
||||
| zero_signals.npz | R-raw | 127,958 | a966f093a9cd |
|
||||
| zero_summary.json | R-raw | 1,508 | 6e033f991438 |
|
||||
|
||||
## research/fixed_patch_jet_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 69,721 | 91ad9c4d1e88 |
|
||||
| before.json | R-raw | 690 | 06e1b6278a29 |
|
||||
| fixed_patch_jet.py | R-raw | 9,646 | 6fbdcd9a076b |
|
||||
| incremental.patch | D-snapshot | 9,891 | b43129d82c69 |
|
||||
| manifest.json | P-pin | 1,967 | 08daa0248f60 |
|
||||
| notes.txt | K | 2,214 | 50070fe54efb |
|
||||
| results.json | R-raw | 1,100 | d959a6a1cc2c |
|
||||
| stdout.txt | R-raw | 1,272 | eccdc0f6b16a |
|
||||
|
||||
## research/fixed_patch_jet_closure_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 72,692 | 8a157af82ecf |
|
||||
| cpu_results.json | R-raw | 628 | 8e4a1c090c93 |
|
||||
| cpu_stdout.txt | R-raw | 590 | d800b783fe47 |
|
||||
| incremental.patch | D-snapshot | 3,490 | a9a486c68afd |
|
||||
| layout_uncropped.png | R-raw | 51,785 | 0cf1cfa5e99d |
|
||||
| manifest.json | P-pin | 1,849 | 31cf0c739241 |
|
||||
| notes.txt | K | 3,023 | 0f7e7bdfd0e6 |
|
||||
|
||||
## research/fixed_patch_jet_records_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 71,228 | a8e967f76e08 |
|
||||
| before.json | R-raw | 690 | ebd6f4251fc2 |
|
||||
| fixed_patch_jet.py | R-raw | 10,650 | f6b299885da0 |
|
||||
| incremental.patch | D-snapshot | 5,539 | c7f4d0c02ccc |
|
||||
| manifest.json | P-pin | 2,057 | 1d616c2541df |
|
||||
| notes.txt | K | 2,449 | b58cec14d359 |
|
||||
| results.json | R-raw | 1,136 | 788af64a6ebd |
|
||||
| stdout.txt | R-raw | 20,750 | a765b06deefa |
|
||||
|
||||
## research/j1-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 7,667 | 8f469daef054 |
|
||||
| baseline-status.txt | P-pin | 2,601 | 64ca05ab952e |
|
||||
| baseline.json | P-pin | 23,052 | ab5852bcb0ef |
|
||||
| incremental.diff | D-snapshot | 29,912 | dd9c4e688888 |
|
||||
| results.json | R-raw | 68,294 | 56547aa8a678 |
|
||||
|
||||
## research/j2-slot-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 12,930 | 69fa4d3bccd3 |
|
||||
| baseline-status.txt | P-pin | 2,825 | e711afb0016c |
|
||||
| baseline.json | P-pin | 24,660 | 286e023da94f |
|
||||
| incremental.diff | D-snapshot | 25,231 | 649f3b5d168d |
|
||||
| results.json | R-raw | 477,249 | b4272e25179d |
|
||||
|
||||
## research/j3-coverage-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 11,409 | 5887cf73dce2 |
|
||||
| baseline-status.txt | P-pin | 3,055 | 78e88356caa3 |
|
||||
| baseline.json | P-pin | 30,407 | 095972547a7f |
|
||||
| incremental.diff | D-snapshot | 20,114 | f0d502dc5c5b |
|
||||
| negative-budget-20744.h5 | R-raw | 88,017 | 693b133be84d |
|
||||
| negative-end-20760.h5 | R-raw | 88,174 | 429270d263d4 |
|
||||
| restart-21280.h5 | R-raw | 88,062 | 74900affae38 |
|
||||
| restart-reference-21296.h5 | R-raw | 88,154 | 7510ee97885b |
|
||||
| results.json | R-raw | 296,314 | 7806b10e65cb |
|
||||
| verification.json | K | 1,468 | 78a07c47aea4 |
|
||||
|
||||
## research/j3-lag-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 11,732 | 34fe5dbb2cae |
|
||||
| baseline-status.txt | P-pin | 2,968 | 6ae6094ea9d7 |
|
||||
| baseline.json | P-pin | 26,505 | 5c289d14ff30 |
|
||||
| end-21280.h5 | R-raw | 92,366 | f30b209179ab |
|
||||
| incremental.diff | D-snapshot | 15,790 | 5c6c485e1f75 |
|
||||
| results.json | R-raw | 21,718 | 2bf58939129c |
|
||||
| start-20760.h5 | R-raw | 92,247 | ae0abd05190c |
|
||||
|
||||
## research/j3-offline-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 6,271 | a76e1b486bd2 |
|
||||
| lags.csv | R-raw | 1,104 | dd3136dd2dc2 |
|
||||
| localization.png | R-raw | 136,013 | 74b56f3e814c |
|
||||
| regions.csv | R-raw | 2,126 | 5d361e6872af |
|
||||
| results.json | R-raw | 8,852 | bf77b0f534d4 |
|
||||
|
||||
## research/j3-reservoir-2026-10-12
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| REVIEW.md | K | 13,563 | a74b67a64e0d |
|
||||
| baseline-status.txt | P-pin | 2,896 | a318e077dfae |
|
||||
| baseline.json | P-pin | 25,571 | 060502ea7dd8 |
|
||||
| incremental.diff | D-snapshot | 22,898 | 8e2c79a86c98 |
|
||||
| results.json | R-raw | 182,747 | 7fd5e51a1785 |
|
||||
|
||||
## research/refinement_example_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 8,286 | 591478100da8 |
|
||||
| before.json | R-raw | 818 | 7ab0962775b1 |
|
||||
| incremental.patch | D-snapshot | 8,682 | 7ad8719d0167 |
|
||||
| manifest.json | P-pin | 1,798 | bf1eee8a7cf8 |
|
||||
| notes.txt | K | 1,185 | 7ff019ad023d |
|
||||
| results.json | R-raw | 1,118 | 94697b0988b9 |
|
||||
| stdout.txt | R-raw | 325 | 501446f557f4 |
|
||||
|
||||
## research/refinement_followup_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 4,887 | af65e889f20e |
|
||||
| baseline.json | P-pin | 5,343 | 7c60ad8ad692 |
|
||||
| cleanup.json | R-raw | 1,253 | 15cf342fba2e |
|
||||
| cpu_results.json | R-raw | 1,056 | 1166dffd2a09 |
|
||||
| cpu_results_attempt1.json | R-raw | 642 | d139d5508105 |
|
||||
| cpu_stdout.txt | R-raw | 499 | d1b33ea20563 |
|
||||
| cpu_stdout_attempt1.txt | R-raw | 879 | 6a9fe7fce39a |
|
||||
| gpu_before.json | R-raw | 589 | 46762a0ab207 |
|
||||
| gpu_before_no_launch.json | R-raw | 589 | 074982d1b2d4 |
|
||||
| gpu_rectangle_before.json | R-raw | 589 | 831e3793339f |
|
||||
| gpu_rectangle_before_no_launch_1791886104981638567.json | R-raw | 589 | c33885d50bfd |
|
||||
| gpu_rectangle_results.json | R-raw | 1,003 | 9c6da219ac6c |
|
||||
| gpu_rectangle_stdout.txt | R-raw | 332 | 9185bf303954 |
|
||||
| gpu_results.json | R-raw | 1,189 | 53e4f1c44de1 |
|
||||
| gpu_seed1_before.json | R-raw | 589 | c77ea3485f84 |
|
||||
| gpu_seed1_results.json | R-raw | 996 | b718e8af84e4 |
|
||||
| gpu_seed1_stdout.txt | R-raw | 306 | 4d7de0897137 |
|
||||
| gpu_stdout.txt | R-raw | 68 | b0956fe3cdb0 |
|
||||
| incremental.patch | D-snapshot | 16,836 | 9df25d8c177a |
|
||||
| manifest.json | P-pin | 4,410 | 177704d6f1ae |
|
||||
| run_gpu.py | R-raw | 5,237 | b5af3f0d4f79 |
|
||||
|
||||
## research/refinement_prepare_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 8,939 | 8e210c28c7c5 |
|
||||
| baseline.json | P-pin | 46,348 | d5f257216b8d |
|
||||
| incremental.patch | D-snapshot | 15,751 | 0ddb32550584 |
|
||||
| manifest.json | P-pin | 2,113 | 9fd94ccf2c3e |
|
||||
| validation.json | R-raw | 3,567 | f057e7cbcd35 |
|
||||
| validation.txt | R-raw | 873 | 594daa45c58c |
|
||||
|
||||
## research/refinement_slot_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 67,717 | d498081e7c50 |
|
||||
| before.json | R-raw | 690 | 430e76b3aa8a |
|
||||
| cpu_results.json | R-raw | 1,421 | 4f6bc4d9428f |
|
||||
| incremental.patch | D-snapshot | 18,676 | 67a3e8cc9e68 |
|
||||
| manifest.json | P-pin | 2,270 | 6515458290f4 |
|
||||
| notes.txt | K | 2,612 | 08a713ba4b37 |
|
||||
| results.json | R-raw | 1,160 | 03e683027ba8 |
|
||||
| stdout.txt | R-raw | 643 | d57b91769826 |
|
||||
|
||||
## research/refinement_slot_followup_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 68,536 | 89a26ce79438 |
|
||||
| before.json | R-raw | 690 | e707df192ff2 |
|
||||
| incremental.patch | D-snapshot | 495 | c6f6fa11237d |
|
||||
| manifest.json | P-pin | 1,920 | 4840243a33aa |
|
||||
| notes.txt | K | 1,549 | 4b77583ca116 |
|
||||
| planar_velocity.cuh | R-raw | 1,911 | 87db7466c843 |
|
||||
| planar_velocity.sha256 | P-pin | 251 | 6694c4a1c1f0 |
|
||||
| results.json | R-raw | 1,050 | 6ef6eda854c6 |
|
||||
| stdout.txt | R-raw | 1,160 | 461860351c0c |
|
||||
|
||||
## research/refinement_step_20261013
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 10,671 | 88d6f24743f5 |
|
||||
| baseline.json | P-pin | 40,620 | 20f61559dd38 |
|
||||
| cpu_validation.json | R-raw | 1,231 | 7999fd30018c |
|
||||
| gpu_failure_validation.json | R-raw | 868 | 5f6db65ee4f9 |
|
||||
| gpu_failure_validation.txt | R-raw | 248 | d9a570a1e9e9 |
|
||||
| gpu_validation.json | R-raw | 672 | 2028f59e1055 |
|
||||
| gpu_validation.txt | R-raw | 632 | 9699918ee127 |
|
||||
| incremental.patch | D-snapshot | 26,516 | d95f9347633d |
|
||||
| manifest.json | P-pin | 2,075 | 32a7f1a7309e |
|
||||
|
||||
## research/repair-2026-10-10
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| TRON_HANDOFF.md | R-raw | 25,208 | a339786c7944 |
|
||||
| baseline-hashes.json | P-pin | 16,020 | 9b4e24cb1d10 |
|
||||
| baseline-status.txt | P-pin | 429 | 515be249b638 |
|
||||
| checkpoint-domain-2026-10-12.diff | D-snapshot | 14,443 | 035ed630dda1 |
|
||||
| classic-results.json | R-raw | 244,729 | 7297aa0d5bca |
|
||||
| classic-signals.npz | R-raw | 630,332 | 8cf6d1218aa0 |
|
||||
| final-gpu.log | R-raw | 191 | a0eec1a8ea31 |
|
||||
| final-unit.log | R-raw | 188 | 94efba2f1d42 |
|
||||
| provenance.json | P-pin | 1,020 | 25e22b4f0696 |
|
||||
| repair-architecture.md | K | 9,498 | 12051c3e8571 |
|
||||
| repair-numerical.md | K | 10,769 | 0c5757fe5f36 |
|
||||
| repair-state.md | K | 8,481 | 3c7f2d5b687a |
|
||||
| simplification-checks.log | R-raw | 1,045 | fa9d464be740 |
|
||||
|
||||
## research/shear_refinement
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.md | K-code | 4,142 | 67bdd8ed622b |
|
||||
| boundary_shear_tagging.py | K-code | 13,859 | b5415a1c4ea1 |
|
||||
| example.py | K-code | 6,323 | 66295d3f0273 |
|
||||
|
||||
## research/surface_actuation_20261014
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| baseline.json | P-pin | 54,139 | 0ad06888dbeb |
|
||||
| cpu_checkpoint_run.json | R-raw | 400 | 39681b60ff15 |
|
||||
| cpu_checkpoint_stdout.txt | R-raw | 114 | 929e4b8d4f46 |
|
||||
| cpu_initial.txt | R-raw | 620 | 29213c58a679 |
|
||||
| cpu_surface_command.txt | R-raw | 118 | 6d8c615a6e03 |
|
||||
| cpu_surface_exit.txt | R-raw | 2 | 573541ac9702 |
|
||||
| cpu_surface_stdout.txt | R-raw | 99 | ee9237fc26e5 |
|
||||
| gpu_run.json | R-raw | 1,698 | dc8f87eafaa9 |
|
||||
| gpu_stdout.txt | R-raw | 159 | b262891e52c3 |
|
||||
| incremental.diff | D-snapshot | 50,206 | 43090a1a8b11 |
|
||||
| manifest.json | P-pin | 7,527 | 35daa01271cb |
|
||||
| report.txt | K | 5,078 | 1162a6479f9f |
|
||||
|
||||
## research/wall_seam_pulse_20261014
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| analyze.py | R-raw | 10,343 | ed7cf6999a49 |
|
||||
| interpretation.txt | K | 3,514 | 90935db2aa99 |
|
||||
| results.json | R-raw | 15,137 | 678a0759871a |
|
||||
| run.json | R-raw | 1,170 | 53e303fd0d5d |
|
||||
| sources.csv | R-raw | 2,313 | 40d3736b8ef1 |
|
||||
| stdout.txt | R-raw | 1,179 | 756d8c38e213 |
|
||||
|
||||
## tests/validation/multigrid/cpuref_0_3_evidence
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 4,133 | 62897bfd9812 |
|
||||
| README_grok.md | K | 1,751 | 2d36e05661a8 |
|
||||
| baseline.json | P-pin | 39,291 | 188491096547 |
|
||||
| manifest.json | P-pin | 3,495 | 10c19f5fe5ca |
|
||||
| results_horsea.json | R-raw | 2,281 | 3b163384e376 |
|
||||
| stdout_grok.txt | R-raw | 7,143 | 5a0c270ea4ae |
|
||||
| stdout_horsea.txt | R-raw | 9,013 | adff8bb8d295 |
|
||||
|
||||
## tests/validation/multigrid/local_stress_v1_evidence
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 8,239 | 3513fd294223 |
|
||||
| TABLE_local_stress_observable_v1.md | K | 2,967 | 6115dcde0f46 |
|
||||
| baseline.json | P-pin | 43,991 | ab631ea4f2ad |
|
||||
| comparison.json | R-raw | 13,248 | d7d1b36eb247 |
|
||||
| manifest.json | P-pin | 4,132 | c6c44d8125cd |
|
||||
| results_horsea.json | R-raw | 2,671 | 79bdf001f948 |
|
||||
| run_once.py | R-raw | 4,313 | fa9c5239e993 |
|
||||
| source_manifest.json | P-pin | 622 | 0516f1860407 |
|
||||
| stdout_horsea.txt | R-raw | 17,091 | a20c554693df |
|
||||
| stdout_local_stress_observable_v1.txt | R-raw | 12,238 | 692ffe1a05da |
|
||||
|
||||
## tests/validation/multigrid/shear_v0_2_evidence
|
||||
|
||||
| File | Class | Bytes | Blob |
|
||||
|---|---|---:|---|
|
||||
| README.txt | K | 7,220 | 772f74ad40ff |
|
||||
| baseline.json | P-pin | 41,751 | 70d9c9fa71c0 |
|
||||
| comparison.json | R-raw | 7,679 | 0af8736f9a01 |
|
||||
| joint_report.md | K | 6,110 | eed871221298 |
|
||||
| manifest.json | P-pin | 3,796 | b22d03a1ba9e |
|
||||
| results_horsea.json | R-raw | 1,957 | b2cd48a07a08 |
|
||||
| run_once.py | R-raw | 3,245 | 0b8d5e21f326 |
|
||||
| source_manifest.json | P-pin | 574 | 4d1c742690c7 |
|
||||
| stdout_grok.txt | R-raw | 7,950 | 0e80b1aba7f6 |
|
||||
| stdout_horsea.txt | R-raw | 11,421 | 3c867cdac197 |
|
||||
@@ -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,11 +51,11 @@ 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
|
||||
`set_jet_amplitude(index, value, level=2)` (`level` is required); L1 bodies/Jets remain unsupported.
|
||||
`set_jet_amplitude(index, value, level=2)`; L1 bodies/Jets remain unsupported.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Device state lifecycle (uniform Simulation, FixedPatchStepper)
|
||||
|
||||
State is grouped by what a change invalidates. This file describes the code as
|
||||
it is; it adds no framework. Line references are to candidate 0f8fdbc3.
|
||||
|
||||
| Class | Contents | How it changes | Owner rule |
|
||||
|---|---|---|---|
|
||||
| 1. Module and its globals | Compile-time (visible to the compiler, fixed per module): `config.h` macros incl. `NX`/`NY`, `COLLISION_MODEL`, `BLOCK_ROOT_BC`, `WALL_*`. Runtime per-module globals (not visible to the compiler; written by host code): `__constant__` `d_params`, `d_w`, `d_cx`, `d_cy`, `d_block_channel`, `d_block_wall`; plus bound kernel handles | Globals are written once by the owner right after `load_module`. Uniform grid: `Simulation._recompile` (construction, body-topology sync) loads a new module and re-uploads; `update_runtime_params` rewrites `d_params` (omega, force, n_objects) between runs. Steppers: never rewritten after construction, so they are immutable for the instance's lifetime although they are runtime values. The blocked slot/reservoir root reads its slot from the `d_block_wall` constant (the former dense route used `WALL_*` macros; both were bit-identical in the parity gates). | Every owner loads its own module (`compiler.load_module` = `module_from_file`, no cache). A loaded module is never shared between live owners: globals are per module, so sharing would share mutable `d_params`. |
|
||||
| 2. Geometry, layout, tables | flags, curved-link tables, block layouts and halo pairs, transfer (explode/coalesce) tables, block kinds | Immutable for the life of an instance. Uniform grid: rebuilt by body sync. `FixedPatchStepper`: a geometry change is a new instance (`remap_finest`), committed only after a successful transfer. | No in-place table edits. |
|
||||
| 3. Controls and dependent boundary columns | Jet amplitude -> per-link `surface_velocity`; body actions (uniform grid) | Between steps, without a table or module rebuild (`set_jet_amplitude`). | Input (index, level, amplitude) is validated before any device write; invalid input changes nothing and the instance stays usable. The host copy commits only after the upload returned. Any error after validation leaves the device column undefined and fails the instance: `FixedPatchStepper` status `failed`, `Simulation._failed`; every later device-touching public method (`Simulation`: `stream`, `set_jet_amplitude`, `initialize`, `sync_bodies`, `run`/`step`, `read_surface_exchange`, `read_body(s)`, `get_macroscopic`, `get_ddf`, `update_runtime_params`, `snapshot`, `restore`, save/load checkpoint) raises; cached host readers and host-side staging (e.g. `get_flags`, `add_body`/`remove_body` before a sync) do not touch the device and stay callable. `close()` still releases resources and stays retryable. |
|
||||
| 4. Per-step state | population double buffers and their parity, raw-exchange ledgers, observations | Device-only during `step(n)` / `run(n)`; buffer swaps are host-handle swaps. | Valid on the host only after the single wait at the end of the call. |
|
||||
|
||||
Storage routes are separate concerns:
|
||||
|
||||
- Original uniform grid (`uniform.cu`, `Simulation`): whole-domain `NX`/`NY`.
|
||||
- Blocked levels (`blocked.cu`; every `FixedPatchStepper` level, including the
|
||||
single level of `refinements=[]`): `NX`/`NY` are one block window including halo.
|
||||
- Dense refinement levels (`refinement.cu`) were removed: `level_blocks` is
|
||||
required and every refinement level is blocked.
|
||||
|
||||
A single-rectangle refinement (`FixedPatch2D`) is stored in its own fine
|
||||
window frame (`FixedPatchStepper.__init__`,
|
||||
`BlockLayout2D.build(*pair.fine_shape, ...)`). Only a rectangle set
|
||||
(`FixedPatchSet2D`) uses the level-wide frame `(2*ny, 2*nx)`, whose
|
||||
`fine_layout` requires that full grid to be divisible by the tile
|
||||
(`refinement.py:240-252`).
|
||||
@@ -1,89 +0,0 @@
|
||||
# Supported settings: 2D Jet + fixed multilevel refinement (candidate 888c8038)
|
||||
|
||||
Every entry names the guard that enforces it (file and function or kernel guard,
|
||||
at source revision 888c80381c006e976be2af8a452288729fc17c1b) 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` `FixedPatch2D.from_config`; `#error` guards in `lbm/kernels/blocked.cu` |
|
||||
| Precision / streaming | FP32 storage, double buffer | same guards |
|
||||
| Collision on refinement levels | SRT or MRT; TRT and LES rejected | `lbm/refinement.py` `FixedPatch2D.from_config` (re-checked for fine boundaries in `prepare_fine_boundary`) |
|
||||
| Shifted (ddf_shifting) storage | supported (every level is blocked); one representation on all levels | `lbm/refinement_stepper.py` `FixedPatchStepper._check_storage` |
|
||||
| Fine relaxation | 1/tau_fine must lie inside the configured omega guards | `lbm/refinement.py` `FixedPatch2D.from_config` |
|
||||
| Single blocked level `FixedPatchStepper(refinements=[], level_blocks=[(by, bx)], level_objects=[circles])` (replaces `PeriodicBlockStepper`, removed in 8e1feff) | doubly periodic, SRT only, no LES/Jets/projection/exchange observation, stationary passive circles; `download()` returns a 1-tuple | `lbm/refinement_stepper.py` `_check_levels`, `FixedPatchStepper.__init__`; `lbm/blocks.py` `static_circle_tables` |
|
||||
| Refinement storage | blocked on every level: `level_blocks` is required, one tile `(by, bx)` per level dividing that level; a whole-level tile (`(ny, nx)` at the root, the fine window shape for one rectangle) always divides. Dense and mixed dense/blocked storage were removed (parity of D/D, D/B and B/B recorded at 1573942 and 2469af7) | `lbm/refinement_stepper.py` `_check_levels`, `FixedPatchStepper.__init__` |
|
||||
|
||||
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` `_check_levels` |
|
||||
| Single L1 rectangle | two-coarse-cell margin to the domain edge | `lbm/refinement.py` `FixedPatch2D.from_config` |
|
||||
| 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` `_check_levels`; `lbm/refinement.py` `_check_parent_pull_closure` |
|
||||
| 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` `_check_levels`; `lbm/hierarchy.py` `build_hierarchy` |
|
||||
| Nested transfer support | child only over inherited ACTIVE cells, donors/receivers uncovered ACTIVE, no child over the parent restriction footprint | `lbm/refinement.py` `_nested_kind` |
|
||||
| Tile shape | positive `(by, bx)`; the level allocation must be divisible by the tile (no partial tiles yet) | `lbm/refinement_stepper.py` `_check_levels`, `lbm/refinement.py` `FixedPatchSet2D.fine_layout` (shape); `lbm/blocks.py` `BlockLayout2D.build` (divisibility) |
|
||||
| Coarse boundary | `periodic`, `slot_reservoir` or `channel` | `lbm/refinement_stepper.py` `FixedPatchStepper.__init__` |
|
||||
| `slot_reservoir` | on-site velocity bottom slot, reservoir top, velocity 0, 2 <= slot width < nx, patch rows 4..ny-4 | `lbm/refinement_stepper.py` `FixedPatchStepper.__init__`; `lbm/refinement.py` `prepare_fine_boundary` |
|
||||
| `channel` | all levels blocked; Zou-He inlet, pressure-NEQ outlet, stationary bounce-back walls; transfers away from the closure | `lbm/channel.py` `validate_channel_config`, `validate_channel_transfers` |
|
||||
| Explosion | `constant` (default); `projected_linear` only periodic, all-blocked SRT, no bodies/Jets, no LES (experimental) | `lbm/refinement_stepper.py` `FixedPatchStepper.__init__` |
|
||||
| L2 runtime remap (`remap_finest`) | periodic three-level L2 sets only, no L0/L1 bodies, identical pinned boundary | `lbm/refinement_stepper.py` `FixedPatchStepper.remap_finest` |
|
||||
|
||||
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` `prepare_fine_boundary` |
|
||||
| Level | not on L0; only on the finest level of a nested hierarchy | `lbm/refinement_stepper.py` `_check_levels` |
|
||||
| Jet condition | velocity only; `pressure` is reserved and raises NotImplementedError | `body/jets.py` `Jet.__post_init__` |
|
||||
| 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` `Jet.__post_init__`, `jet_link_shapes`, `_surface_links` |
|
||||
| Jets with shifted storage | only in the all-blocked stepper with SRT and no LES | `body/jets.py` `_surface_links` |
|
||||
| SurfaceActuation | unshifted storage only | `body/objects.py` `SimObject.validate_surface_actuation` |
|
||||
| Raw boundary exchange (force) | blocked body levels only; the window ledger requires `observe_raw_exchange` | `lbm/refinement_stepper.py` `FixedPatchStepper.__init__` |
|
||||
| Checkpoint with Jets | refused | `simulation.py` `Simulation.save_checkpoint` |
|
||||
|
||||
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_finest`),
|
||||
moving bodies on refined levels, pressure Jets, Jet checkpoints, LES or TRT on refined
|
||||
levels, partial (non-dividing) tiles.
|
||||
@@ -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 `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,
|
||||
@@ -77,12 +71,11 @@ PYTHONPATH=src python -m unittest discover -s tests/unit -p 'test_synchronized_r
|
||||
|
||||
## Explicit runtime continuation
|
||||
|
||||
`replacement = current.remap_finest(rectangles)` returns a new ready
|
||||
`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,
|
||||
@@ -99,7 +92,7 @@ resources; an actual old-device synchronization/read failure marks the old
|
||||
stepper failed, consistent with its existing failure semantics.
|
||||
|
||||
```python
|
||||
replacement = current.remap_finest([(20, 16, 36, 40)])
|
||||
replacement = current.remap_child([(20, 16, 36, 40)])
|
||||
# Explicitly choose the returned continuation only after success.
|
||||
current.close()
|
||||
current = replacement
|
||||
@@ -119,25 +112,24 @@ Per-population conservation is not an advancement target under collision.
|
||||
|
||||
### Bounded pinned-body extension
|
||||
|
||||
`replacement = current.remap_finest(rectangles, 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.
|
||||
`replacement = current.remap_child(rectangles, child_objects=original_objects)`
|
||||
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.boundaries[2].objects`: those are projected fine-local copies.
|
||||
Do not pass `current.child_boundary.objects`: those are projected fine-local copies.
|
||||
There is no retained mutable original-object registry. Before reading device
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
@@ -0,0 +1,52 @@
|
||||
# First-failure uniform SRT diagnostic (prepared, not executed)
|
||||
|
||||
Purpose: locate the first physical failure in the unchanged uniform D80 SRT control that was healthy at executed step 160 and nonfinite at the next observation, executed step 320. The original failure's summary `steps=160` is the last accepted observation, not the last executed step. This diagnostic does not select different numerical parameters, run an AMR case or perform a new benchmark campaign.
|
||||
|
||||
## Exact source and configuration
|
||||
|
||||
The default source is the containing repository (override with `--source`); the exact pinned production/runtime source is commit `9805383f9c50d867e8555471baf5dbda8333a652`. The manifest pins production files and the helper/runner dependencies directly against the original failure's `source_before.json`. All pinned contents match. Import guards require CelerisLab and the runner from the selected pinned checkout.
|
||||
|
||||
The script calls the original `run_refined_dfg.config(helper,80)` factory and unchanged `Simulation(...).initialize()`. Thus it retains D80, U=.01, ν=.008, τ=.524, ω≈1.9084, shifted FP32, SRT, double buffer, parabolic local Zou–He inlet, all-nine pressure-NEQ damping .5, backflow clamp disabled, identical cylinder geometry and initialization. No clipping, equilibrium rewrite or solver patch is added.
|
||||
|
||||
The inlet overload in this source may differ by FP32 rounding from older d79 code even where the algebra matches; that is not changed here and is not established as the cause of failure. MRT's outlet branch also differs. Neither prior MRT success nor this diagnostic establishes an SRT/outlet/body attribution.
|
||||
|
||||
## Observation and stop contract
|
||||
|
||||
- At most 320 actual solver steps, starting from the original fresh initialization. Stop at the first physical health failure (or wall deadline).
|
||||
- Read the production macroscopic path, its decoded populations, and force after every completed step. Preserve the actual executed count separately from the last observed count.
|
||||
- Independently report nonfinite physical populations, nonfinite physical macros and nonfinite reaction. Apply the original density [.5,1.5] and Mach≤.2 checks to physical FLUID nodes. A new population-only check may stop earlier than the original macro-only health check; the report explicitly distinguishes it.
|
||||
- Per-step `Simulation.run(1)` resets/reads the observation buffer for a one-step force window. This changes observation cadence and force accumulation windows, not configuration or fluid updates. A cumulative-force-only overflow from the original 100/60-step batching would not be reproduced identically; that limitation is explicit. Original macro warnings already show nonfinite populations/macros, so this instrument prioritizes locating the physical failure.
|
||||
- Ghost/inactive/body-solid populations are reported in separate categories; their NaNs do not independently trigger physical failure. Category masks are disjoint and complete. Boundary subtypes are decoded as the nibble, not mistaken for independent bit flags.
|
||||
- Physical region labels are body-near (within radius+3 cells), outlet/inlet-near (three fluid columns), wall-near (three fluid rows), and remaining bulk. These locate symptoms, not causal mechanisms.
|
||||
- Initial, preceding and firstbad/final compact reports retain category counts, finite global extrema and positions, the first 16 offending nodes in row-major order, their bad q indices/all populations, and up to four 5×5 population/flag neighborhoods. “First” identifies earliest sampled step; within that step the list is row-major, not an asserted temporal order among GPU threads.
|
||||
|
||||
## Bounded evidence
|
||||
|
||||
Only the immediately preceding and firstbad/final active-buffer states are retained. `states.npz` contains their exact raw shifted FP32 values, production-decoded FP32 populations and step numbers, plus flags. This is ~81 MiB at the exact D80 shape, with a 128 MiB artifact limit enforced. There are no saved field trajectories. Initial conditions are reproducible from the source/config; only their diagnostic summary is retained.
|
||||
|
||||
These are diagnostic snapshots, not full restart checkpoints (spare buffers and observation/control state are not a restart bundle). `diagnostics.json` stores nonfinite numbers as explicit `NaN`, `+Inf`, `-Inf` strings and is strict finite JSON; NPZ preserves actual IEEE values. A force-only failure can have no offending fluid node. A clean cap means no sampled failure through 320, not benchmark acceptance.
|
||||
|
||||
## CPU validation
|
||||
|
||||
```sh
|
||||
python tests/validation/uniform_srt_diagnostic/test_diagnostic.py
|
||||
python tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py --output /tmp/new-srt-preflight
|
||||
```
|
||||
|
||||
Seven CPU-only tests pass: source/config, complete category partition, inactive nonfinite exclusion, independent population/macro/force reasons, health thresholds, bounded two-state artifact writing, and deadline/source guards. `py_compile` and the real-source default preflight pass. No CUDA import is required by preflight/tests and no GPU was executed here.
|
||||
|
||||
## Root-only admitted execution
|
||||
|
||||
After independent review and root GPU admission:
|
||||
|
||||
```sh
|
||||
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s <hard-budget>s python tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py --source <exact-9805383-checkout> --execute --gpu-admitted --wall-seconds <soft-budget> --output <new-output-directory>
|
||||
```
|
||||
|
||||
The external watchdog is required for a blocked CUDA call; cooperative wall checks cannot interrupt one. Budget should allow per-step downloads and ~81 MiB evidence writing; this diagnostic is not a performance measurement. The step ceiling cannot be increased by a CLI flag. `READY` is emitted after the first completed step and all observation paths. First physical failure is expected diagnostic evidence and returns exit 2; step cap returns 0. `preflight.json` records the pre-execution configuration, not a completion receipt.
|
||||
|
||||
Original failure files, production source, original pilot runner and gates are untouched. This test-only handoff is based on exact commit 9805383; added diagnostic files are not part of the historical failure source manifest.
|
||||
|
||||
## Historical reporting undercount (not changed here)
|
||||
|
||||
The failed pilot executed to step 320 before its second observation failed. Its summary `steps=160` reflects `begin`, which advances only after an accepted observation. This is a reporting undercount, not evidence that the second batch did not execute. The original failure package and runner/gates are preserved unchanged. This diagnostic records both `executed_steps` and `observation_step`, and labels previous-state steps explicitly. Fixing the historical runner's reporting is a separate change.
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Public API demo: fixed 2:1 patch with a stationary, optionally actuated circle.
|
||||
|
||||
From this checkout, after checking the selected physical GPU is idle:
|
||||
PYTHONPATH="$PWD/src" CUDA_VISIBLE_DEVICES=0 python examples/fixed_patch_circle.py
|
||||
... --speed -0.002 --mode slot_reservoir
|
||||
Both levels use blocked storage (--base-block, --fine-block tiles).
|
||||
PYTHONPATH="$PWD/src" CUDA_VISIBLE_DEVICES=0 python examples/fixed_patch_circle.py --fine-block 16 20
|
||||
... --speed -0.002 --fine-block 16 20 --mode slot_reservoir
|
||||
Omit --fine-block for the (equivalent) dense fine storage.
|
||||
|
||||
A short smoke run (default 128 coarse steps): it shows setup, stepping and
|
||||
fluid-only diagnostics, not a developed flow, accuracy or long-time stability.
|
||||
@@ -40,13 +40,13 @@ def config(mode):
|
||||
def leaf_masks(run, mode):
|
||||
"""Known fluid leaves: uncovered coarse cells (minus wall rows in slot mode)
|
||||
and fine ACTIVE cells outside the body."""
|
||||
p = run.hierarchy.pairs[0]
|
||||
p = run.patch
|
||||
x0, y0, x1, y1 = p.bounds
|
||||
coarse = np.ones(p.coarse_shape, bool)
|
||||
coarse[y0:y1, x0:x1] = False
|
||||
if mode == "slot_reservoir":
|
||||
coarse[[0, -1]] = False
|
||||
solid = (run.boundaries[1].flags.reshape(p.fine_shape) & SOLID) != 0
|
||||
solid = (run.fine_boundary.flags.reshape(p.fine_shape) & SOLID) != 0
|
||||
return coarse, (p.fine_kind == ACTIVE) & ~solid
|
||||
|
||||
|
||||
@@ -71,10 +71,8 @@ def main():
|
||||
parser.add_argument("--mode", choices=("periodic", "slot_reservoir"), default="periodic")
|
||||
parser.add_argument("--speed", type=float, default=.002,
|
||||
help="normal surface velocity, coarse lattice units (+ blows, - sucks, 0 passive)")
|
||||
parser.add_argument("--base-block", type=int, nargs=2, metavar=("BY", "BX"), default=(12, 8),
|
||||
help="root tile; must divide the 24 x 32 root")
|
||||
parser.add_argument("--fine-block", type=int, nargs=2, metavar=("BY", "BX"), default=(16, 20),
|
||||
help="fine tile; must divide the whole fine allocation (32 x 40 here)")
|
||||
parser.add_argument("--fine-block", type=int, nargs=2, metavar=("BY", "BX"),
|
||||
help="blocked fine storage; must divide the whole fine allocation (32 x 40 here)")
|
||||
parser.add_argument("--steps", type=int, default=128)
|
||||
args = parser.parse_args()
|
||||
if args.steps < 0:
|
||||
@@ -88,20 +86,20 @@ def main():
|
||||
body = SimObject(0, geometry=CircleGeometry(*CENTER, RADIUS), center=CENTER, radius=RADIUS,
|
||||
surface_actuation=spec)
|
||||
cfg = config(args.mode)
|
||||
fine_block = tuple(args.fine_block) if args.fine_block else None
|
||||
# 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=[tuple(args.base_block), tuple(args.fine_block)],
|
||||
level_objects=[(), (body,)]) as run:
|
||||
p = run.hierarchy.pairs[0]
|
||||
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.
|
||||
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
||||
run.initialize(np.broadcast_to(rest, (9, *p.coarse_shape)),
|
||||
np.broadcast_to(rest, (9, *p.fine_shape)))
|
||||
storage = f"blocked root {tuple(args.base_block)} fine {tuple(args.fine_block)}"
|
||||
print(f"mode={args.mode} storage={storage} speed={args.speed:+g} "
|
||||
storage = f"blocked {fine_block}" if fine_block else "dense"
|
||||
print(f"mode={args.mode} fine storage={storage} speed={args.speed:+g} "
|
||||
f"(fine allocation {p.fine_shape[0]}x{p.fine_shape[1]}, fine origin {p.fine_origin})")
|
||||
masks = leaf_masks(run, args.mode)
|
||||
start = leaf_diagnostics(*run.download(), masks)
|
||||
|
||||
@@ -31,7 +31,6 @@ from CelerisLab.lbm.refinement import ACTIVE
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
|
||||
BOUNDS, AMPLITUDE, WIDTH = (16, 4, 32, 28), .02, 12
|
||||
LEVEL_BLOCKS = [(12, 8), (8, 8)] # blocked storage: 48x48 root, 56x40 fine window
|
||||
TIMES, MAX_MACH, MAX_SECONDS = (0, 300, 1200), .1, 120
|
||||
|
||||
|
||||
@@ -133,8 +132,7 @@ def main():
|
||||
lattice_model='D2Q9', collision='SRT', streaming='double_buffer',
|
||||
store_precision='FP32', ddf_shifting=False, les_enabled=False)
|
||||
report = dict(config=dict(nx=48, ny=48, bounds=BOUNDS, W=WIDTH, A=AMPLITUDE, nu=.1,
|
||||
rho=1, tau_coarse=.8, tau_fine=1.1, coarse_boundary='slot_reservoir',
|
||||
level_blocks=LEVEL_BLOCKS),
|
||||
rho=1, tau_coarse=.8, tau_fine=1.1, coarse_boundary='slot_reservoir'),
|
||||
guards=dict(check_every=50, positive_density=True, finite=True,
|
||||
max_Mach=MAX_MACH, max_seconds=MAX_SECONDS,
|
||||
timing='cooperative checks, not an exact interruption deadline'),
|
||||
@@ -150,10 +148,9 @@ 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],
|
||||
level_blocks=LEVEL_BLOCKS) as run:
|
||||
p = run.hierarchy.pairs[0]
|
||||
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
|
||||
masks[0][4:28, 16:32] = False
|
||||
|
||||
@@ -16,7 +16,7 @@ from CelerisLab.config import load_lbm_config
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
from CelerisLab.lbm.equilibrium import compute_feq_d2q9, compute_macro_from_ddf
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
from CelerisLab.lbm.refinement_stepper import PeriodicPatchStepper
|
||||
|
||||
|
||||
def shear_equilibrium(shape, origin, dx, period):
|
||||
@@ -48,9 +48,8 @@ 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, FixedPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)],
|
||||
level_blocks=[(16, 16), (24, 24)]) as run:
|
||||
p = run.hierarchy.pairs[0]
|
||||
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)
|
||||
coarse_leaf = np.ones(p.coarse_shape, dtype=bool)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,7 @@ Responsibilities:
|
||||
|
||||
Design::
|
||||
Packed ``obs`` layout matches ``generate_config(..., n_objects=count)`` and
|
||||
``config_obs.h`` macros. Host sizing uses :func:`~CelerisLab.cuda.compiler.obs_layout`.
|
||||
``config_obs.h`` macros. Host sizing uses :func:`~CelerisLab.cuda.compiler_v2.obs_layout`.
|
||||
|
||||
Pending edits are lightweight: only the "intent" (add / remove) is stored.
|
||||
All derived quantities (flags, compact lists, GPU buffers) are built at
|
||||
@@ -27,8 +27,8 @@ 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 ..cuda.compiler import obs_layout
|
||||
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 (
|
||||
FLUID, SOLID,
|
||||
@@ -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,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],
|
||||
|
||||
@@ -7,7 +7,7 @@ Two JSON files:
|
||||
config_body.json – object list
|
||||
|
||||
LBMConfig / BodyConfig are plain dataclasses; they translate to
|
||||
macro dicts consumed by `cuda/compiler.py` for header generation.
|
||||
macro dicts consumed by `cuda/compiler_v2.py` for header generation.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
@@ -60,14 +60,14 @@ Python `config.py` 只负责读取和校验,不是配置位置。显式配置
|
||||
|
||||
**`lbm/kernels/config/*.h` 由编译器根据 `LBMConfig` 自动生成,请勿手改。**
|
||||
|
||||
仓库中检入的 `config/*.h` 可能是历史版本。`compiler.generate_config` 会复制 kernel 源码到独立临时构建目录,并在副本中生成配置;不会改写包内头文件或 PTX。返回目录须显式传给 `compile_kernel(build_dir=...)`,编译结果与配置摘要均保存在该目录。`Simulation` 负责目录清理;直接使用编译器的调用者须调用 `cleanup_build_dir()`。跳过生成配置直接编译仓库中的 `uniform.cu` 属不受支持用法。
|
||||
仓库中检入的 `config/*.h` 可能是历史版本。`compiler_v2.generate_config` 会复制 kernel 源码到独立临时构建目录,并在副本中生成配置;不会改写包内头文件或 PTX。返回目录须显式传给 `compile_kernel(build_dir=...)`,编译结果与配置摘要均保存在该目录。`Simulation` 负责目录清理;直接使用编译器的调用者须调用 `cleanup_build_dir()`。跳过生成配置直接编译仓库中的 `kernel_v2.cu` 属不受支持用法。
|
||||
|
||||
```
|
||||
config_lbm.json
|
||||
↓ load_lbm_config()
|
||||
config.py:LBMConfig (读取 + 校验)
|
||||
↓ to_macros()
|
||||
cuda/compiler.py (独立构建目录中生成 config/*.h)
|
||||
cuda/compiler_v2.py (独立构建目录中生成 config/*.h)
|
||||
↓
|
||||
config_grid.h → NX, NY, NZ, NT, LATTICE_MODEL;
|
||||
DIM / NQ 由 LATTICE_MODEL 推导写入(与 grid.lattice_model 一致,非 JSON 顶层字段)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""CUDA build helpers (compilation itself does not need a live context)."""
|
||||
from . import compiler
|
||||
from . import compiler_v2 as compiler
|
||||
|
||||
__all__ = ["compiler"]
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# CelerisLab/cuda/compiler.py
|
||||
# CelerisLab/cuda/compiler_v2.py
|
||||
"""
|
||||
Kernel configuration, compilation, and module loading.
|
||||
|
||||
Owners call build_module(cfg, entry, arch=...), then set_params(module, omega, ...).
|
||||
build_module runs, in a private directory it removes afterwards:
|
||||
Workflow:
|
||||
1. build_dir = generate_config(lbm_cfg, n_objects) — isolated source snapshot
|
||||
2. compile_kernel(arch, source=entry, build_dir=build_dir) — nvcc -ptx
|
||||
2. compile_kernel(arch, build_dir=build_dir) — nvcc -ptx kernel_v2.cu
|
||||
3. load_module(ptx_path) — PyCUDA module from PTX
|
||||
Entries: uniform.cu (uniform grid), blocked.cu (every blocked level, single or refined).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -18,6 +16,7 @@ import shutil
|
||||
import tempfile
|
||||
import hashlib
|
||||
import json
|
||||
from contextvars import ContextVar
|
||||
from dataclasses import asdict
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
@@ -34,6 +33,9 @@ _KERNEL_DIR = os.path.join(
|
||||
)
|
||||
|
||||
|
||||
_last_build_dir = ContextVar("celeris_last_build_dir", default=None)
|
||||
_last_ptx_path = ContextVar("celeris_last_ptx_path", default=None)
|
||||
|
||||
|
||||
def kernel_path(filename: str) -> str:
|
||||
return os.path.join(_KERNEL_DIR, filename)
|
||||
@@ -52,16 +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 to compile_kernel(build_dir=...). The caller owns cleanup_build_dir() once the loaded module is no longer
|
||||
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")
|
||||
@@ -71,19 +71,20 @@ 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)
|
||||
raise
|
||||
_last_build_dir.set(build_dir)
|
||||
_last_ptx_path.set(None)
|
||||
return build_dir
|
||||
|
||||
|
||||
@@ -95,9 +96,12 @@ def cleanup_build_dir(build_dir: Optional[str]) -> None:
|
||||
if path == Path(_KERNEL_DIR).resolve() or not (path / ".celeris-build").is_file():
|
||||
raise ValueError("Refusing to remove a directory not owned by the compiler")
|
||||
shutil.rmtree(path)
|
||||
if _last_build_dir.get() == str(build_dir):
|
||||
_last_build_dir.set(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
|
||||
@@ -161,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']}
|
||||
@@ -240,9 +241,13 @@ def _render_config(m: dict, n_objects: int, cdir: str, *, block_root_bc: int = 0
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compilation
|
||||
# ---------------------------------------------------------------------------
|
||||
def compile_kernel(arch: str = "sm_70", source: str = "uniform.cu",
|
||||
output: Optional[str] = None, *, build_dir: str) -> str:
|
||||
"""Compile the snapshot in build_dir (from generate_config); never package headers."""
|
||||
def compile_kernel(arch: str = "sm_70", source: str = "kernel_v2.cu",
|
||||
output: Optional[str] = None, *,
|
||||
build_dir: Optional[str] = None) -> str:
|
||||
"""Compile a generated private source snapshot; never use package headers."""
|
||||
# A failed attempt must not make implicit load_module() reuse older code.
|
||||
_last_ptx_path.set(None)
|
||||
build_dir = build_dir or _last_build_dir.get()
|
||||
if not build_dir or not (Path(build_dir) / ".celeris-build").is_file():
|
||||
raise RuntimeError("Call generate_config() and pass its build_dir before compiling")
|
||||
root = Path(build_dir).resolve()
|
||||
@@ -267,36 +272,23 @@ def compile_kernel(arch: str = "sm_70", source: str = "uniform.cu",
|
||||
finally:
|
||||
if os.path.exists(tmp):
|
||||
os.unlink(tmp)
|
||||
_last_ptx_path.set(str(dst))
|
||||
return str(dst)
|
||||
|
||||
|
||||
def load_module(ptx_path: str):
|
||||
"""Load one compiled PTX file as a new PyCUDA module."""
|
||||
def load_module(ptx_path: Optional[str] = None):
|
||||
"""Load explicit PTX or the last PTX compiled in this Python context."""
|
||||
ptx_path = ptx_path or _last_ptx_path.get()
|
||||
if not ptx_path:
|
||||
raise RuntimeError("No compiled PTX; call compile_kernel() first")
|
||||
import pycuda.driver as cuda
|
||||
return cuda.module_from_file(ptx_path)
|
||||
|
||||
|
||||
def build_module(cfg: LBMConfig, source: str, *, arch: str, n_objects: int = 0,
|
||||
block_root_bc: int = 0):
|
||||
"""Generate, compile and load one private module; the build directory is removed.
|
||||
# ---------------------------------------------------------------------------
|
||||
# Back-compat helpers (used by test scripts)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
The single build path of every owner (Simulation, FixedPatchStepper levels).
|
||||
Each call returns a NEW module: callers must not share it, because its
|
||||
globals (d_params, ...) are mutable per module.
|
||||
"""
|
||||
build_dir = generate_config(cfg, n_objects=n_objects, block_root_bc=block_root_bc)
|
||||
try:
|
||||
return load_module(compile_kernel(arch, source=source, build_dir=build_dir))
|
||||
finally:
|
||||
cleanup_build_dir(build_dir)
|
||||
|
||||
|
||||
def set_params(module, omega: float, fx: float = 0.0, fy: float = 0.0, fz: float = 0.0,
|
||||
n_objects: int = 0) -> None:
|
||||
"""Write the module's __constant__ LBMParams (core/params.cuh layout)."""
|
||||
import struct
|
||||
import pycuda.driver as cuda
|
||||
ptr, size = module.get_global("d_params")
|
||||
if size != struct.calcsize("=ffffI"):
|
||||
raise RuntimeError("LBMParams host/device layout mismatch")
|
||||
cuda.memcpy_htod(ptr, struct.pack("=ffffI", omega, fx, fy, fz, n_objects))
|
||||
def compile_kernel_v2(arch: str = "sm_70") -> str:
|
||||
"""Alias for compile_kernel() kept for test-script compatibility."""
|
||||
return compile_kernel(arch=arch)
|
||||
@@ -2,7 +2,7 @@
|
||||
"""Packed ``obs`` buffer layout for force / torque / sensor telemetry.
|
||||
|
||||
Usage::
|
||||
Host code (e.g. ``ObjectManager``) and ``compiler.generate_config`` must use
|
||||
Host code (e.g. ``ObjectManager``) and ``compiler_v2.generate_config`` must use
|
||||
the same ``dim`` and ``n_objects`` so allocation sizes match device macros.
|
||||
|
||||
Design::
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
# CelerisLab/lbm/block_stepper.py
|
||||
"""Opt-in periodic backend for one BlockLayout2D level, optional static circles.
|
||||
|
||||
D2Q9/SRT/FP32 unforced double buffers. Each step refreshes every halo from
|
||||
its owner core, then pulls/collides core cells into the spare buffer, then
|
||||
swaps. Only halo pair tables are uploaded; dense ownership stays on the host.
|
||||
The caller owns the CudaContext and must close this backend before it.
|
||||
Optional ``objects`` are stationary passive circles (static linear BFL via
|
||||
receiver-local staging: halo refresh -> cut links -> pull, no copy between).
|
||||
No force/actuation readout, motion, walls, refinement, checkpoint or runtime
|
||||
parameter mutation; wall/slot/inlet fields in cfg are ignored.
|
||||
"""
|
||||
from dataclasses import replace
|
||||
from numbers import Integral
|
||||
import struct
|
||||
from threading import RLock
|
||||
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
|
||||
from ..cuda import compiler_v2 as compiler
|
||||
from .blocks import NQ, BlockLayout2D, blocked_link_tables
|
||||
from .descriptors import D2Q9_EX, D2Q9_EY, FLUID, SOLID
|
||||
from .population_storage import _StoredPopulations, _encode_physical, _decode_stored
|
||||
|
||||
MAX_GRID_DIM = 65535 # CUDA gridDim.y/z limit (rows, blocks)
|
||||
|
||||
|
||||
def _static_circles(layout, objects):
|
||||
"""Host tables for stationary circles; rejects before any CUDA work.
|
||||
|
||||
Every periodic neighbor of every fluid core is inspected, including domain
|
||||
edges that CircleGeometry does not enumerate, so clipped or incomplete
|
||||
geometry cannot pass. Each physical link has one receiver-owning block.
|
||||
"""
|
||||
from ..body.geometry.circle import CircleGeometry
|
||||
|
||||
ny, nx = layout.shape
|
||||
flags = np.full(ny * nx, FLUID, np.uint16)
|
||||
for obj in objects:
|
||||
if (type(obj.geometry) is not CircleGeometry or obj.is_sensor or obj.is_force_region
|
||||
or obj.surface_actuation is not None
|
||||
or any(getattr(obj.state, name) != 0 for name in ("vx", "vy", "omega"))
|
||||
or any(vars(obj.control).values())):
|
||||
raise ValueError("Block bodies must be stationary passive circles")
|
||||
cx, cy = obj.geometry.center
|
||||
radius = obj.geometry._radius
|
||||
if not np.isfinite([cx, cy, radius]).all() or radius <= 0:
|
||||
raise ValueError("Block circle center/radius must be finite with radius > 0")
|
||||
mask = obj.geometry.build_flag_mask(nx, ny)
|
||||
solid = (mask & SOLID) != 0
|
||||
if not solid.any() or np.any(flags[solid] & SOLID):
|
||||
raise ValueError("Block circles must be resolved and disjoint")
|
||||
flags[solid] = mask[solid]
|
||||
solid = ((flags & SOLID) != 0).reshape(ny, nx)
|
||||
if solid[[0, -1]].any() or solid[:, [0, -1]].any():
|
||||
raise ValueError("Block circles must not touch the domain edge")
|
||||
links = [link for obj in objects
|
||||
for link in obj.geometry.build_cut_links(nx, ny, domain_flags=flags)]
|
||||
def source(r, q):
|
||||
y, x = divmod(r, nx)
|
||||
return (y + D2Q9_EY[q]) % ny, (x + D2Q9_EX[q]) % nx
|
||||
receivers = {(int(link.fluid_idx), int(link.dir)) for link in links}
|
||||
required = {(int(r), q) for r in np.flatnonzero(~solid.ravel())
|
||||
for q in range(1, NQ) if solid[source(r, q)]}
|
||||
if len(receivers) != len(links) or receivers != required:
|
||||
raise ValueError("Cut links must cover every fluid pull from a solid exactly once")
|
||||
tables, _ = blocked_link_tables(
|
||||
layout, flags, np.array([link.fluid_idx for link in links], np.uint32),
|
||||
np.array([link.dir for link in links], np.uint8),
|
||||
np.array([link.q for link in links], np.float32),
|
||||
np.array([link.fallback for link in links], np.uint8))
|
||||
return dict(solid=solid, **tables)
|
||||
|
||||
|
||||
class PeriodicBlockStepper:
|
||||
"""G->G stepping of Q-major (9, ny, nx) postcollision populations.
|
||||
|
||||
On a device/launch failure the instance becomes unusable: close and
|
||||
recreate it rather than assuming a partially advanced buffer rolled back.
|
||||
"""
|
||||
def __init__(self, cfg, layout, *, context, objects=()):
|
||||
if not isinstance(layout, BlockLayout2D) or layout.periodic != (True, True):
|
||||
raise ValueError("PeriodicBlockStepper requires a doubly periodic BlockLayout2D")
|
||||
checked = replace(cfg)
|
||||
checked.validate()
|
||||
if (not checked.is_d2q9 or checked.collision != "SRT"
|
||||
or checked.streaming != "double_buffer" or checked.store_precision != "FP32"
|
||||
or checked.les_enabled
|
||||
or (checked.ny, checked.nx) != layout.shape):
|
||||
raise ValueError("PeriodicBlockStepper requires matching D2Q9/SRT/FP32/"
|
||||
"double_buffer config without LES")
|
||||
ly, lx = layout.local_shape
|
||||
pairs = len(layout.halo_dst)
|
||||
tpb = checked.threads_per_block
|
||||
if layout.nblocks > MAX_GRID_DIM or ly > MAX_GRID_DIM or not 0 < pairs < 2**31:
|
||||
raise ValueError("block count, block rows or halo pairs exceed launch limits")
|
||||
objects = tuple(objects) # an empty iterable takes the body-free path
|
||||
self.boundary = _static_circles(layout, objects) if objects else None
|
||||
links = 0 if self.boundary is None else len(self.boundary["cell"])
|
||||
if links >= 2**31:
|
||||
raise ValueError("cut links exceed launch limits")
|
||||
self._ddf_shifting = bool(checked.ddf_shifting)
|
||||
self.layout = layout
|
||||
self.ctx = context
|
||||
self._links = links
|
||||
self._link_grid = ((links + tpb - 1) // tpb, 1)
|
||||
# Wall/slot fields are not applied; neutralize them like the fine patch level.
|
||||
self._local_cfg = replace(checked, nx=lx, ny=ly, y_wall_bc="bounce_back", top_wall_bc="velocity",
|
||||
wall_slot_width=0, wall_normal_velocity=0.0, velocity=0.0)
|
||||
self._block = (tpb, 1, 1)
|
||||
self._grid = ((lx + tpb - 1) // tpb, ly, layout.nblocks)
|
||||
self._copy_grid = ((pairs + tpb - 1) // tpb, 1)
|
||||
self._bytes = int(np.prod(layout.blocked_shape)) * 4
|
||||
self._lock = RLock()
|
||||
self._status = "new"
|
||||
self._step_count = 0
|
||||
self._allocations = []
|
||||
self._stream = self._module = self._copy = self._step = self._curved = None
|
||||
self._tables = {}
|
||||
with self.ctx.activate():
|
||||
try:
|
||||
free, _ = cuda.mem_get_info()
|
||||
tables = sum(a.nbytes for k, a in (self.boundary or {}).items() if k != "solid")
|
||||
if 2 * self._bytes + layout.halo_dst.nbytes + layout.halo_src.nbytes + tables > free:
|
||||
raise MemoryError("blocked buffers exceed free device memory")
|
||||
self._stream = cuda.Stream()
|
||||
config = self._local_cfg
|
||||
config.validate()
|
||||
if not np.isclose(config.omega, 1 / (3 * checked.viscosity + .5), rtol=1e-14, atol=0):
|
||||
raise RuntimeError("block omega/tau mismatch")
|
||||
build = compiler.generate_config(config)
|
||||
try:
|
||||
ptx = compiler.compile_kernel(self.ctx.sm_arch, source="blocks.cu", build_dir=build)
|
||||
module = compiler.load_module(ptx)
|
||||
finally:
|
||||
compiler.cleanup_build_dir(build)
|
||||
ptr, size = module.get_global("d_params")
|
||||
if size != struct.calcsize("=ffffI"):
|
||||
raise RuntimeError("LBMParams host/device layout mismatch")
|
||||
cuda.memcpy_htod(ptr, struct.pack("=ffffI", config.omega, 0, 0, 0, 0))
|
||||
self._module = module
|
||||
self._current = self._allocate(self._bytes)
|
||||
self._spare = self._allocate(self._bytes)
|
||||
self._dst = self._upload(layout.halo_dst)
|
||||
self._src = self._upload(layout.halo_src)
|
||||
self._copy = module.get_function("BlockHaloCopy")
|
||||
self._step = module.get_function("BlockStep")
|
||||
if self.boundary is not None:
|
||||
self._tables = {k: self._upload(a) for k, a in self.boundary.items() if k != "solid"}
|
||||
self._curved = module.get_function("BlockCurvedBoundary")
|
||||
except BaseException:
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
pass # Preserve the original construction failure.
|
||||
raise
|
||||
|
||||
def _allocate(self, size):
|
||||
pointer = cuda.mem_alloc(size)
|
||||
self._allocations.append(pointer)
|
||||
return pointer
|
||||
|
||||
def _upload(self, array):
|
||||
array = np.ascontiguousarray(array)
|
||||
pointer = self._allocate(array.nbytes)
|
||||
cuda.memcpy_htod(pointer, array)
|
||||
return pointer
|
||||
|
||||
def _require(self, *states):
|
||||
if self._status not in states:
|
||||
raise RuntimeError(f"Block stepper is {self._status}; expected {' or '.join(states)}")
|
||||
|
||||
@property
|
||||
def step_count(self):
|
||||
return self._step_count
|
||||
|
||||
def initialize(self, g):
|
||||
"""Upload dense G_0; the spare buffer starts NaN so stale halos cannot hide."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("new", "ready")
|
||||
dense = np.array(g, dtype=np.float64 if self._ddf_shifting else np.float32,
|
||||
order="C", copy=True)
|
||||
if dense.shape != (NQ, *self.layout.shape):
|
||||
raise ValueError("Expected a Q-major (9, ny, nx) array")
|
||||
fluid = np.ones(self.layout.shape, bool) if self.boundary is None else ~self.boundary["solid"]
|
||||
if not np.isfinite(dense[:, fluid]).all() or (dense[:, fluid].sum(axis=0) <= 0).any():
|
||||
raise ValueError("Fluid populations must be finite with positive density")
|
||||
dense[:, ~fluid] = np.nan # Solid cores are deliberately invalid.
|
||||
encoded = _encode_physical(dense, self._ddf_shifting)
|
||||
values = encoded[:, fluid]
|
||||
if (not np.isfinite(values).all()
|
||||
or (values.sum(axis=0, dtype=np.float64) + int(self._ddf_shifting) <= 0).any()):
|
||||
raise ValueError("Stored fluid populations must be finite with positive density")
|
||||
blocked = self.layout.to_blocked(encoded)
|
||||
spare = np.full_like(blocked, np.nan)
|
||||
try:
|
||||
cuda.memcpy_htod(self._current, blocked)
|
||||
cuda.memcpy_htod(self._spare, spare)
|
||||
self._step_count = 0
|
||||
self._status = "ready"
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
|
||||
def step(self, n=1):
|
||||
"""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")
|
||||
kw = dict(block=self._block, stream=self._stream)
|
||||
try:
|
||||
self._status = "advancing"
|
||||
for _ in range(n):
|
||||
self._copy(self._current, self._dst, self._src,
|
||||
np.uint32(len(self.layout.halo_dst)), grid=self._copy_grid, **kw)
|
||||
if self._curved is not None:
|
||||
t = self._tables
|
||||
self._curved(self._current, t["block"], t["cell"], t["dir"], t["q"],
|
||||
t["fallback"], np.uint32(self._links), np.uintp(0), # no actuation
|
||||
np.uintp(0), np.uintp(0), np.uintp(0), # no observation
|
||||
grid=self._link_grid, **kw)
|
||||
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._stream.synchronize()
|
||||
self._current, self._spare = self._spare, self._current
|
||||
self._step_count += 1
|
||||
self._status = "ready"
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
|
||||
def download(self):
|
||||
"""Return physical core populations (FP64 when shifted); no host cache."""
|
||||
snapshot = self._download_raw()
|
||||
return _decode_stored(snapshot.values[0], snapshot.shifted)
|
||||
|
||||
def _download_raw(self):
|
||||
"""Return one tagged dense FP32 population array without conversion."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("ready")
|
||||
blocked = np.empty(self.layout.blocked_shape, np.float32)
|
||||
try:
|
||||
cuda.memcpy_dtoh(blocked, self._current)
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
return _StoredPopulations((self.layout.to_dense(blocked),), self._ddf_shifting)
|
||||
|
||||
def close(self):
|
||||
"""Release owned resources; retain them for retry if context activation fails."""
|
||||
with self._lock:
|
||||
if self._status == "closed":
|
||||
return
|
||||
self._status = "failed"
|
||||
with self.ctx.activate():
|
||||
self._status = "closed"
|
||||
error = None
|
||||
try:
|
||||
if self._stream is not None:
|
||||
self._stream.synchronize()
|
||||
except Exception as exc:
|
||||
error = exc
|
||||
self._copy = self._step = self._curved = self._module = None
|
||||
self._tables = {}
|
||||
for pointer in self._allocations:
|
||||
try:
|
||||
pointer.free()
|
||||
except Exception as exc:
|
||||
error = error or exc
|
||||
self._allocations.clear()
|
||||
self._stream = None
|
||||
if error is not None:
|
||||
raise error
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
if exc[0] is None:
|
||||
raise
|
||||
@@ -18,9 +18,7 @@ all NQ populations from an owner core cell into another block's halo cell.
|
||||
Periodicity is explicit per axis. Across a non-periodic exterior, halo cells
|
||||
have no pair and stay invalid (NaN after packing): no wrap, no implied BC.
|
||||
|
||||
Host preparation plus thin launch wrappers: no CUDA import or allocation
|
||||
(launch_* only call kernels and device tables the caller owns), no refinement
|
||||
levels; level_config and static_circle_tables prepare one blocked level.
|
||||
Preparation only: no CUDA, no boundary conditions, no refinement levels.
|
||||
The one-cell same-level halo here is unrelated to the four-cell fine halo of
|
||||
the fixed refinement patch (refinement.py), whose semantics are unchanged.
|
||||
|
||||
@@ -33,7 +31,7 @@ receiver-local boundary values: the solid's owner-core copy of that slot may
|
||||
be stale, which is distinct from the unique core ownership of stored state.
|
||||
No moving geometry is implied.
|
||||
"""
|
||||
from dataclasses import dataclass, replace
|
||||
from dataclasses import dataclass
|
||||
from numbers import Integral
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -375,84 +373,3 @@ def blocked_link_tables(layout, flags, fluid_idx, dir, q, fallback):
|
||||
dir=columns[1].astype(np.uint8), q=columns[2].astype(np.float32),
|
||||
fallback=columns[3].astype(np.uint8))
|
||||
return tables, mapped
|
||||
|
||||
|
||||
# One blocked level as FixedPatchStepper builds and launches it (any depth). Raw argument orders of the blocked.cu kernels live only here.
|
||||
|
||||
def level_config(cfg, layout, tau):
|
||||
"""Kernel config of one blocked level: NX/NY = block window incl. halo.
|
||||
|
||||
Physical wall/slot/inlet fields are neutralized; a blocked root gets them
|
||||
from its module globals (d_block_wall/d_block_channel) instead.
|
||||
"""
|
||||
ly, lx = layout.local_shape
|
||||
local = replace(cfg, nx=lx, ny=ly, y_wall_bc="bounce_back", top_wall_bc="velocity",
|
||||
wall_slot_width=0, wall_normal_velocity=0.0, velocity=0.0)
|
||||
local.validate()
|
||||
if not np.isclose(local.omega, 1 / tau, rtol=1e-14, atol=0):
|
||||
raise RuntimeError("block level omega/tau mismatch")
|
||||
return local
|
||||
|
||||
|
||||
def launch_halo_copy(kernel, buffer, dst, src, count, *, block, stream):
|
||||
"""BlockHaloCopy over ``count`` uploaded (dst, src) block-cell pairs."""
|
||||
kernel(buffer, dst, src, np.uint32(count),
|
||||
grid=((count + block[0] - 1) // block[0], 1), block=block, stream=stream)
|
||||
|
||||
|
||||
def launch_curved_boundary(kernel, buffer, tables, count, *, block, stream,
|
||||
velocity=None, rx=None, ry=None, raw=None):
|
||||
"""BlockCurvedBoundary on uploaded link tables; None = no actuation/observation."""
|
||||
none = np.uintp(0)
|
||||
kernel(buffer, tables["block"], tables["cell"], tables["dir"], tables["q"], tables["fallback"],
|
||||
np.uint32(count), none if velocity is None else velocity,
|
||||
none if rx is None else rx, none if ry is None else ry, none if raw is None else raw,
|
||||
grid=((count + block[0] - 1) // block[0], 1), block=block, stream=stream)
|
||||
|
||||
|
||||
def static_circle_tables(layout, objects):
|
||||
"""Host tables for stationary circles; rejects before any CUDA work.
|
||||
|
||||
Every periodic neighbor of every fluid core is inspected, including domain
|
||||
edges that CircleGeometry does not enumerate, so clipped or incomplete
|
||||
geometry cannot pass. Each physical link has one receiver-owning block.
|
||||
"""
|
||||
from ..body.geometry.circle import CircleGeometry
|
||||
from .descriptors import FLUID, SOLID
|
||||
|
||||
ny, nx = layout.shape
|
||||
flags = np.full(ny * nx, FLUID, np.uint16)
|
||||
for obj in objects:
|
||||
if (type(obj.geometry) is not CircleGeometry or obj.is_sensor or obj.is_force_region
|
||||
or obj.surface_actuation is not None
|
||||
or any(getattr(obj.state, name) != 0 for name in ("vx", "vy", "omega"))
|
||||
or any(vars(obj.control).values())):
|
||||
raise ValueError("Block bodies must be stationary passive circles")
|
||||
cx, cy = obj.geometry.center
|
||||
radius = obj.geometry._radius
|
||||
if not np.isfinite([cx, cy, radius]).all() or radius <= 0:
|
||||
raise ValueError("Block circle center/radius must be finite with radius > 0")
|
||||
mask = obj.geometry.build_flag_mask(nx, ny)
|
||||
solid = (mask & SOLID) != 0
|
||||
if not solid.any() or np.any(flags[solid] & SOLID):
|
||||
raise ValueError("Block circles must be resolved and disjoint")
|
||||
flags[solid] = mask[solid]
|
||||
solid = ((flags & SOLID) != 0).reshape(ny, nx)
|
||||
if solid[[0, -1]].any() or solid[:, [0, -1]].any():
|
||||
raise ValueError("Block circles must not touch the domain edge")
|
||||
links = [link for obj in objects
|
||||
for link in obj.geometry.build_cut_links(nx, ny, domain_flags=flags)]
|
||||
def source(r, q):
|
||||
y, x = divmod(r, nx)
|
||||
return (y + D2Q9_EY[q]) % ny, (x + D2Q9_EX[q]) % nx
|
||||
receivers = {(int(link.fluid_idx), int(link.dir)) for link in links}
|
||||
required = {(int(r), q) for r in np.flatnonzero(~solid.ravel())
|
||||
for q in range(1, NQ) if solid[source(r, q)]}
|
||||
if len(receivers) != len(links) or receivers != required:
|
||||
raise ValueError("Cut links must cover every fluid pull from a solid exactly once")
|
||||
tables, _ = blocked_link_tables(
|
||||
layout, flags, np.array([link.fluid_idx for link in links], np.uint32),
|
||||
np.array([link.dir for link in links], np.uint8),
|
||||
np.array([link.q for link in links], np.float32),
|
||||
np.array([link.fallback for link in links], np.uint8))
|
||||
return dict(solid=solid, **tables)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -21,24 +21,7 @@ from .refinement import (ACTIVE, OUTER_GHOST, FixedPatch2D, FixedPatchSet2D,
|
||||
@dataclass(frozen=True, eq=False)
|
||||
class Hierarchy:
|
||||
"""configs[l]: validated LBMConfig of level l; pairs[l]: tables of (l, l+1);
|
||||
kinds[l]: composed per-cell kind (child coverage OUTER on every non-finest level).
|
||||
|
||||
Index conventions (the single place they are defined):
|
||||
* level l = 0 (root) .. depth-1; pair index p = l-1 builds level l from level p;
|
||||
* array shapes are (ny, nx); rectangles, origins and centers are (x, y);
|
||||
* a pair's two members are its COARSE (parent) and FINE (child) level, for every
|
||||
pair, not absolute L0/L1 (fine_shape, fine_origin, blocked_coarse_tables, ...);
|
||||
* refined levels (l >= 1) of FixedPatchStepper place bodies and Jets in the root
|
||||
EDGE frame: root cell i covers [i, i+1), center i+.5. Uniform Simulation objects
|
||||
and the static circles of a single-level (depth 1) FixedPatchStepper use the
|
||||
integer-NODE frame (CircleGeometry.build_flag_mask at integer (x, y)). No
|
||||
NODE<->EDGE input conversion is applied anywhere; inside the EDGE frame, refined
|
||||
boundaries still project root-edge coordinates to fine-local cell indices with
|
||||
the half-cell shift ((v - o)/h - .5, see prepare_fine_boundary);
|
||||
* cell (i, j) of level l's allocated array (ghost band and holes included) covers
|
||||
[o + h*i, o + h*(i+1)) with (o, h) = edge_frame(l); level 1 is built from the root
|
||||
config directly, deeper levels from their parent frame (see edge_frame).
|
||||
"""
|
||||
kinds[l]: composed per-cell kind (child coverage OUTER on every non-finest level)."""
|
||||
configs: tuple
|
||||
pairs: tuple
|
||||
kinds: tuple
|
||||
@@ -47,18 +30,6 @@ class Hierarchy:
|
||||
def depth(self):
|
||||
return len(self.kinds)
|
||||
|
||||
def edge_frame(self, level):
|
||||
"""(origin (x, y), spacing) of level ``level``'s array in the root edge frame.
|
||||
|
||||
Each pair's fine_origin is in parent cells: zero for a set (level-wide grid),
|
||||
the shifted window origin for a scalar FixedPatch2D.
|
||||
"""
|
||||
origin, spacing = (0.0, 0.0), 1.0
|
||||
for pair in self.pairs[:level]:
|
||||
origin = tuple(o + spacing * f for o, f in zip(origin, pair.fine_origin))
|
||||
spacing /= 2
|
||||
return origin, spacing
|
||||
|
||||
|
||||
def is_rectangle(value):
|
||||
value = tuple(value)
|
||||
@@ -115,14 +86,14 @@ def _check_chain(coarse, fine, pair):
|
||||
def leaf_partition(h):
|
||||
"""Base-cell fraction published as ACTIVE over all levels (exact: powers of two).
|
||||
|
||||
Window (scalar) levels are offset by their fine_level_offset first.
|
||||
Window (scalar) levels are offset by their fine_level_origin first.
|
||||
"""
|
||||
base = np.zeros(h.kinds[0].shape)
|
||||
oy = ox = 0
|
||||
for level, kind in enumerate(h.kinds):
|
||||
if level:
|
||||
pair = h.pairs[level - 1]
|
||||
py, px = pair.fine_level_offset
|
||||
py, px = (pair.fine_level_origin if isinstance(pair, FixedPatch2D) else (0, 0))
|
||||
oy, ox = 2 * oy + py, 2 * ox + px
|
||||
y, x = np.nonzero(kind == ACTIVE)
|
||||
np.add.at(base, ((y + oy) >> level, (x + ox) >> level), 0.25 ** level)
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
// CelerisLab/lbm/kernels/blocked.cu
|
||||
// One fixed-size blocked level (lbm/blocks.py layout), specialized at compile
|
||||
// time for periodic/fine, slot/reservoir or channel stepping. 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).
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include "config.h"
|
||||
#include "core/precision.cuh"
|
||||
#include "core/descriptors.cuh"
|
||||
#include "core/layout.cuh"
|
||||
#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"
|
||||
|
||||
#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"
|
||||
#endif
|
||||
|
||||
#if BLOCK_ROOT_BC == 1
|
||||
#include "boundary/block_wall.cuh"
|
||||
#endif
|
||||
#include "operators/block_collision.cuh"
|
||||
|
||||
// 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"
|
||||
#else
|
||||
#include "step/block_channel.cu"
|
||||
#endif
|
||||
@@ -0,0 +1,162 @@
|
||||
// CelerisLab/lbm/kernels/blocks.cu
|
||||
// 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).
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include "config.h"
|
||||
#include "core/precision.cuh"
|
||||
#include "core/descriptors.cuh"
|
||||
#include "core/layout.cuh"
|
||||
#include "core/params.cuh"
|
||||
#include "core/flags.cuh"
|
||||
#include "core/obs.cuh"
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#include "streaming/pull_double_buffer.cuh"
|
||||
#include "boundary/curved_boundary.cuh"
|
||||
#include "boundary/planar_velocity.cuh"
|
||||
|
||||
#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
|
||||
|
||||
// Fixed-patch cell kinds, matching refinement.py ACTIVE/INNER_GHOST/OUTER_GHOST.
|
||||
constexpr unsigned char KIND_ACTIVE = 0u, KIND_INNER = 1u, KIND_OUTER = 2u;
|
||||
|
||||
// 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
|
||||
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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -44,4 +44,16 @@ struct RigidBodyControl2D {
|
||||
int control_mode; // 0=force, 1=velocity, 2=displacement
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Halo plan (multi-GPU / AMR 预留)
|
||||
// ============================================================================
|
||||
struct HaloPlan {
|
||||
int face_id;
|
||||
int peer_gpu;
|
||||
int level;
|
||||
size_t count;
|
||||
int* send_idx;
|
||||
int* recv_idx;
|
||||
};
|
||||
|
||||
#endif // CELERIS_CORE_PARAMS_CUH
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// CelerisLab – uniform.cu
|
||||
// CelerisLab – kernel_v2.cu
|
||||
// ============================================================================
|
||||
// Compilation entry point (Stage 2 Architecture).
|
||||
//
|
||||
@@ -7,7 +7,7 @@
|
||||
//
|
||||
// File layout (each <300 lines):
|
||||
// operators/helpers.cuh — bounce_back_swap, collide_dispatch
|
||||
// step/init_flow.cu — InitTubeFlow, InitEsoPull
|
||||
// step/init_flow.cu — InitTubeFlow_v2, InitEsoPull
|
||||
// step/one_step_double.cu — OneStep (pull double-buffer)
|
||||
// step/one_step_esopull.cu — EsoPullStep (single-buffer)
|
||||
// step/aux_kernels.cu — CurvedBoundaryKernel, SensorKernel
|
||||
@@ -1,47 +0,0 @@
|
||||
// CelerisLab – operators/block_collision.cuh
|
||||
// The one collision extension point of the blocked steps (BlockStep, BlockChannelStep).
|
||||
// Unforced by construction: FixedPatchStepper supplies no forcing, so no runtime
|
||||
// d_params.fx/fy branch is read. Two compile-time arithmetic families:
|
||||
// uniform-equivalent (MRT on every level; SRT on the channel root, BLOCK_ROOT_BC 2):
|
||||
// collide_dispatch arithmetic with zero force and no LES: clamped omega;
|
||||
// SRT keeps the zero Guo forcing term of collide_srt;
|
||||
// level SRT (BLOCK_ROOT_BC 0/1): unclamped omega, no forcing term; shifted storage
|
||||
// builds feq from the retained drho and direct momentum (numerical variant).
|
||||
#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) {
|
||||
float rho, ux, uy;
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
#if COLLISION_MODEL == 2 || BLOCK_ROOT_BC == 2
|
||||
const float omega = fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega));
|
||||
#if COLLISION_MODEL == 2
|
||||
collide_mrt_no_force(f, rho, ux, uy, omega);
|
||||
#else
|
||||
float feq[NQ], forcing[NQ] = {};
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
collide_srt(f, feq, forcing, omega);
|
||||
#endif
|
||||
#else
|
||||
float feq[NQ];
|
||||
#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
|
||||
@@ -57,4 +57,25 @@ __device__ __forceinline__ void collide_trt(float* __restrict__ f,
|
||||
}
|
||||
}
|
||||
|
||||
__device__ __forceinline__ void collide_trt_no_force(float* __restrict__ f,
|
||||
const float* __restrict__ feq,
|
||||
float omega)
|
||||
{
|
||||
const float wp = omega;
|
||||
const float wm = compute_omega_minus(wp);
|
||||
|
||||
f[0] = f[0] - wp * (f[0] - feq[0]);
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 1; i < NQ; i += 2) {
|
||||
const int ib = i + 1;
|
||||
const float fi = f[i], fb = f[ib];
|
||||
const float feqi = feq[i], feqb = feq[ib];
|
||||
const float delta_s = 0.5f * ((fi - feqi) + (fb - feqb));
|
||||
const float delta_a = 0.5f * ((fi - feqi) - (fb - feqb));
|
||||
f[i] = fi - wp * delta_s - wm * delta_a;
|
||||
f[ib] = fb - wp * delta_s + wm * delta_a;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // CELERIS_OPERATORS_COLLISION_TRT_CUH
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
// nu_t = (C_s * Delta)^2 * |S|, Delta = 1 (lattice unit)
|
||||
// omega_eff = 1 / (3*(nu + nu_t) + 0.5)
|
||||
//
|
||||
// |S| = sqrt(2 S_ij S_ij), with Delta = 1 in lattice units.
|
||||
// The stress estimate uses self-consistent tau_eff; tau0 is molecular.
|
||||
// Closed form uses full stress. Iteration uses deviatoric stress and a cap.
|
||||
// |S| is estimated from non-equilibrium second-order moments using tau0.
|
||||
// ============================================================================
|
||||
|
||||
#ifndef CELERIS_OPERATORS_TURBULENCE_SMAG_CUH
|
||||
@@ -67,12 +65,12 @@ __device__ __forceinline__ float compute_omega_smag(const float* __restrict__ f,
|
||||
|
||||
#if LES_CLOSED_FORM
|
||||
// Closed-form Smagorinsky (no iteration):
|
||||
// ω = 2 / (τ₀ + √(τ₀² + 18·√2·Cs²·√Q / ρ))
|
||||
// ω = 2 / (τ₀ + √(τ₀² + 18·Cs²·√Q / ρ))
|
||||
// where Q = Πneq_ij · Πneq_ij (Frobenius norm squared)
|
||||
const float Q = pixx*pixx + piyy*piyy + 2.0f*pixy*pixy;
|
||||
const float sqrtQ = sqrtf(fmaxf(Q, 0.0f));
|
||||
const float smag_const_sq = LES_CS * LES_CS;
|
||||
const float discriminant = tau0*tau0 + 18.0f * sqrtf(2.0f) * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float discriminant = tau0*tau0 + 18.0f * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float tau_eff = 0.5f * (tau0 + sqrtf(fmaxf(discriminant, tau0*tau0)));
|
||||
return clamp_omega(1.0f / fmaxf(tau_eff, 0.500001f));
|
||||
#else
|
||||
@@ -142,7 +140,7 @@ __device__ __forceinline__ float compute_omega_smag(const float* __restrict__ f,
|
||||
+ 2.0f*(pixy*pixy + pixz*pixz + piyz*piyz);
|
||||
const float sqrtQ = sqrtf(fmaxf(Q, 0.0f));
|
||||
const float smag_const_sq = LES_CS * LES_CS;
|
||||
const float discriminant = tau0*tau0 + 18.0f * sqrtf(2.0f) * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float discriminant = tau0*tau0 + 18.0f * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float tau_eff = 0.5f * (tau0 + sqrtf(fmaxf(discriminant, tau0*tau0)));
|
||||
return clamp_omega(1.0f / fmaxf(tau_eff, 0.500001f));
|
||||
#else
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// Fixed, unforced internal 2:1 patch. Separate compilation entry;
|
||||
// default OneStep/channel kernels are deliberately not included.
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include "config.h"
|
||||
#include "core/precision.cuh"
|
||||
#include "core/descriptors.cuh"
|
||||
#include "core/layout.cuh"
|
||||
#include "core/params.cuh"
|
||||
#include "core/flags.cuh"
|
||||
#include "core/obs.cuh"
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#include "streaming/pull_double_buffer.cuh"
|
||||
#include "boundary/planar_velocity.cuh"
|
||||
#include "boundary/curved_boundary.cuh"
|
||||
|
||||
extern "C" {
|
||||
#include "step/curved_double.cu"
|
||||
}
|
||||
|
||||
#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.
|
||||
#ifdef CELERIS_REFINEMENT_AUDIT
|
||||
extern "C" {
|
||||
__device__ unsigned int collision_counts[TOTAL_CELLS];
|
||||
__device__ unsigned int explosion_counts[TOTAL_CELLS];
|
||||
__device__ unsigned int incoming_counts[NQ * TOTAL_CELLS];
|
||||
}
|
||||
#endif
|
||||
|
||||
__device__ __forceinline__ void patch_collide(float* f, unsigned long k) {
|
||||
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);
|
||||
#ifdef CELERIS_REFINEMENT_AUDIT
|
||||
++collision_counts[k];
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" __global__ void PatchExplode(
|
||||
const float* coarse, float* fine, const uint32_t* targets,
|
||||
const uint32_t* donors, unsigned int count, unsigned long coarse_cells)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
for (unsigned int q = 0; q < NQ; ++q)
|
||||
fine[index_f(targets[row], q)] = coarse[q * coarse_cells + donors[row]];
|
||||
#ifdef CELERIS_REFINEMENT_AUDIT
|
||||
++explosion_counts[targets[row]];
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" __global__ void PatchFineStep(const float* in, float* out, const uint16_t* flags) {
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x >= NX || y >= NY) return;
|
||||
float f[NQ];
|
||||
// Outer halo is invalid after EACH hop, including when reusing the buffer
|
||||
// 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]))) {
|
||||
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.
|
||||
stream_pull_load(k, f, in, j);
|
||||
if (x >= 4 && y >= 4 && x < NX - 4 && y < NY - 4)
|
||||
patch_collide(f, k);
|
||||
}
|
||||
stream_pull_store(k, f, out);
|
||||
}
|
||||
|
||||
extern "C" __global__ void PatchCoarseStep(
|
||||
const float* in, float* out, const float* fine,
|
||||
const uint64_t* targets, const uint64_t* children, unsigned int count,
|
||||
unsigned int x0, unsigned int y0, unsigned int x1, unsigned int y1,
|
||||
unsigned int wall_aware)
|
||||
{
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x >= NX || y >= NY) return;
|
||||
// Both buffers were poisoned at initialization; covered cells stay invalid.
|
||||
if (x >= x0 && x < x1 && y >= y0 && y < y1) return;
|
||||
if (wall_aware && (y == 0 || y == NY - 1)) return;
|
||||
unsigned long j[NQ];
|
||||
compute_neighbors(x, y, j);
|
||||
float f[NQ];
|
||||
stream_pull_load(k, f, in, j);
|
||||
if (x >= x0 - 1 && x <= x1 && y >= y0 - 1 && y <= y1) {
|
||||
for (unsigned int q = 1; q < NQ; ++q) {
|
||||
uint64_t slot = index_f(k, q);
|
||||
unsigned int lo = 0, hi = count;
|
||||
while (lo < hi) {
|
||||
unsigned int mid = lo + (hi - lo) / 2;
|
||||
if (targets[mid] < slot) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
if (lo < count && targets[lo] == slot) {
|
||||
const uint64_t* s = children + 4ul * lo;
|
||||
f[q] = 0.25f * (fine[s[0]] + fine[s[1]] + fine[s[2]] + fine[s[3]]);
|
||||
#ifdef CELERIS_REFINEMENT_AUDIT
|
||||
++incoming_counts[slot];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
// Includes the eight grazing-corner links with UNCOVERED coarse donors.
|
||||
// All replacement arrivals must be in place before computing rho/u.
|
||||
if (wall_aware) {
|
||||
if (y == 1) reconstruct_bottom_velocity(f, 0.0f, bottom_wall_normal_velocity(x));
|
||||
else if (y == NY - 2) reconstruct_top_reservoir(f, RHO);
|
||||
}
|
||||
patch_collide(f, k);
|
||||
stream_pull_store(k, f, out);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
// ApplyEsoPullCurvedKernel, SensorKernel, ForceRegionKernel.
|
||||
//
|
||||
// Launched on compact lists (n_curved / n_sensor / n_cells threads).
|
||||
// Included by uniform.cu. No standalone compilation.
|
||||
// Included by kernel_v2.cu. No standalone compilation.
|
||||
//
|
||||
// Double-buffer mode:
|
||||
// CurvedBoundaryKernel runs BEFORE OneStep.
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// CelerisLab/lbm/kernels/step/block_boundary.cu
|
||||
// Receiver-local curved-boundary staging and its raw-exchange accumulation.
|
||||
// Part of the blocked.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,19 +31,36 @@ 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) {
|
||||
// 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);
|
||||
}
|
||||
if (!wall && !outlet) block_collide(f);
|
||||
}
|
||||
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 blocked.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 blocked.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 and coarse_cells
|
||||
// is the parent's block window 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.
|
||||
// Child summation order ((c0 + c1) + c2) + c3 is part of the numerics; 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]]);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
//
|
||||
// Decodes the EsoPull backing layout into physical distributions, then
|
||||
// computes rho/ux/uy. Output is SoA: rho_out[N], ux_out[N], uy_out[N].
|
||||
// Included by uniform.cu. No standalone compilation.
|
||||
// Included by kernel_v2.cu. No standalone compilation.
|
||||
//
|
||||
// This kernel is the sole path for macroscopic diagnostics in EsoPull mode.
|
||||
// It uses the shared semantic helper layer (esopull_semantic_helpers.cuh).
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// or encodes physical DDF back to EsoPull backing layout.
|
||||
// Each thread handles one cell from a compact index list.
|
||||
//
|
||||
// Included by uniform.cu. No standalone compilation.
|
||||
// Included by kernel_v2.cu. No standalone compilation.
|
||||
// ============================================================================
|
||||
|
||||
#ifndef CELERIS_STEP_ESOPULL_SYNC_CU
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// CelerisLab – step/init_flow.cu
|
||||
// Channel-flow initialization kernels (double-buffer and EsoPull).
|
||||
// Included by uniform.cu after all headers. No standalone compilation.
|
||||
// Included by kernel_v2.cu after all headers. No standalone compilation.
|
||||
// ============================================================================
|
||||
|
||||
#ifndef CELERIS_STEP_INIT_FLOW_CU
|
||||
@@ -73,9 +73,9 @@ __device__ __forceinline__ uint16_t finalize_domain_flag(
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// InitTubeFlow — double-buffer channel initialization
|
||||
// InitTubeFlow_v2 — double-buffer channel initialization
|
||||
// ============================================================================
|
||||
__global__ void InitTubeFlow(uint16_t* flag, fpxx* fi)
|
||||
__global__ void InitTubeFlow_v2(uint16_t* flag, fpxx* fi)
|
||||
{
|
||||
#if DIM == 2
|
||||
unsigned int x, y; unsigned long k;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// CelerisLab – step/one_step_double.cu
|
||||
// Double-buffer PULL streaming step kernel.
|
||||
// Included by uniform.cu. No standalone compilation.
|
||||
// Included by kernel_v2.cu. No standalone compilation.
|
||||
// ============================================================================
|
||||
|
||||
#ifndef CELERIS_STEP_ONE_STEP_DOUBLE_CU
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// CelerisLab – step/one_step_esopull.cu
|
||||
// Esoteric-Pull single-buffer step kernel.
|
||||
// Included by uniform.cu. No standalone compilation.
|
||||
// Included by kernel_v2.cu. No standalone compilation.
|
||||
//
|
||||
// CURVED BC: supported via PrepareEsoPullCurvedKernel + ApplyEsoPullCurvedKernel
|
||||
// in aux_kernels.cu, launched by stepper.py before EsoPullStep.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
@@ -115,16 +115,10 @@ class FixedPatch2D:
|
||||
return self.bounds[0] - HALO / 2, self.bounds[1] - HALO / 2
|
||||
|
||||
@property
|
||||
def fine_level_offset(self) -> tuple[int, int]:
|
||||
"""Array index (y, x) of this window's first cell on the level-wide 2:1 fine grid
|
||||
(an index offset, not an (x, y) origin)."""
|
||||
def fine_level_origin(self) -> tuple[int, int]:
|
||||
"""(y, x) of this window's first cell on the level-wide 2:1 fine grid."""
|
||||
return 2 * self.bounds[1] - HALO, 2 * self.bounds[0] - HALO
|
||||
|
||||
@property
|
||||
def rectangles(self) -> tuple:
|
||||
"""The one parent rectangle, as a sequence like FixedPatchSet2D.rectangles."""
|
||||
return (self.bounds,)
|
||||
|
||||
@property
|
||||
def covered(self) -> np.ndarray:
|
||||
"""Dense coarse mask of cells replaced by the patch (not leaves)."""
|
||||
@@ -157,6 +151,7 @@ class FixedPatch2D:
|
||||
raise ValueError("levels are not synchronized: fine_steps must equal 2*coarse_steps")
|
||||
|
||||
|
||||
MIN_PATCH_GAP = 4 # Legacy exported constant; union transfers impose no gap policy.
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
@@ -230,18 +225,11 @@ class FixedPatchSet2D:
|
||||
def bounds(self) -> tuple:
|
||||
return tuple(p.bounds for p in self.patches)
|
||||
|
||||
rectangles = bounds
|
||||
|
||||
@property
|
||||
def fine_origin(self) -> tuple[float, float]:
|
||||
"""Lower edge of the level-wide fine grid in coarse physical units."""
|
||||
return 0.0, 0.0
|
||||
|
||||
@property
|
||||
def fine_level_offset(self) -> tuple[int, int]:
|
||||
"""The level-wide grid itself: zero (y, x) index offset."""
|
||||
return 0, 0
|
||||
|
||||
@property
|
||||
def covered(self) -> np.ndarray:
|
||||
mask = np.zeros(self.coarse_shape, bool)
|
||||
@@ -463,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")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -36,7 +36,7 @@ def blocked_kind(layout, dense_kind):
|
||||
kind[owner[owned]] = np.asarray(dense_kind).ravel()[owned]
|
||||
return kind
|
||||
|
||||
def blocked_coarse_tables(patch, layout, order, wall_aware=False, *, composed_kind=None):
|
||||
def blocked_base_tables(patch, layout, order, wall_aware=False, *, composed_kind=None):
|
||||
"""Parent donor cells and receiver-local arrival slots, in transfer row order.
|
||||
|
||||
Stage arrivals only after child reads and parent halo gather, immediately
|
||||
@@ -66,50 +66,6 @@ def blocked_coarse_tables(patch, layout, order, wall_aware=False, *, composed_ki
|
||||
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,9 +85,6 @@ class _BlockedBoundary:
|
||||
last_raw: object = None
|
||||
interval_integral: object = None
|
||||
accumulate: object = None
|
||||
window_integral: object = None # optional observation-window FP64 ledger
|
||||
spacing: float = None # level spacing h_l in root units (exchange scaling)
|
||||
centers: tuple = () # body reference centers in the root edge frame
|
||||
|
||||
|
||||
def _prepare_blocked_boundary(boundary, layout, cfg, jets, *, _shifted_blocked=False):
|
||||
@@ -144,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)
|
||||
|
||||
@@ -28,7 +28,7 @@ class LBMStepper:
|
||||
self.boundary_values_gpu = cuda.mem_alloc((6 * cfg.nx + 2 * cfg.ny * cfg.nq) * 4)
|
||||
else:
|
||||
self.step_fn = module.get_function("OneStep")
|
||||
self.init_fn = module.get_function("InitTubeFlow")
|
||||
self.init_fn = module.get_function("InitTubeFlow_v2")
|
||||
|
||||
self.curved_fn = module.get_function("CurvedBoundaryKernel")
|
||||
self.prepare_esopull_curved_fn = module.get_function("PrepareEsoPullCurvedKernel")
|
||||
|
||||
@@ -25,7 +25,7 @@ import pycuda.driver as cuda
|
||||
|
||||
from .config import LBMConfig, BodyConfig, load_lbm_config, load_body_config
|
||||
from .cuda.context import CudaContext, context_bound
|
||||
from .cuda import compiler
|
||||
from .cuda import compiler_v2 as compiler
|
||||
from .lbm.field import LBMField
|
||||
from .lbm.stepper import LBMStepper
|
||||
from .body.objects import SimObject
|
||||
@@ -46,8 +46,8 @@ class Simulation:
|
||||
device_id: int = 0):
|
||||
# Internal stream for async DTOH coordination
|
||||
self._stream: Optional[cuda.Stream] = None
|
||||
self._build_dir = None
|
||||
self._closed = False
|
||||
self._failed = None # reason after an uncertain device write; only close() remains
|
||||
|
||||
# Load configs
|
||||
self.lbm_cfg = load_lbm_config(lbm_config_path)
|
||||
@@ -59,7 +59,9 @@ class Simulation:
|
||||
try:
|
||||
with self.ctx.activate():
|
||||
arch = self._resolve_compile_arch()
|
||||
self._module = compiler.build_module(self.lbm_cfg, "uniform.cu", arch=arch)
|
||||
self._build_dir = compiler.generate_config(self.lbm_cfg, n_objects=0)
|
||||
self._ptx_path = compiler.compile_kernel(arch=arch, build_dir=self._build_dir)
|
||||
self._module = compiler.load_module(self._ptx_path)
|
||||
self._compiled_n_objects = 0
|
||||
self._allocation_signature = self.lbm_cfg.allocation_signature()
|
||||
self._compiled_viscosity = self.lbm_cfg.viscosity
|
||||
@@ -90,7 +92,6 @@ class Simulation:
|
||||
@context_bound
|
||||
def stream(self) -> cuda.Stream:
|
||||
"""Internal CUDA stream for async DTOH coordination."""
|
||||
self._require_usable()
|
||||
if self._stream is None:
|
||||
self._stream = cuda.Stream()
|
||||
return self._stream
|
||||
@@ -174,27 +175,15 @@ class Simulation:
|
||||
def set_jet_amplitude(self, index: int, amplitude: float) -> None:
|
||||
"""Change one jet's signed amplitude; re-uploads per-link velocity only.
|
||||
|
||||
Invalid input raises before any change. Any error after validation
|
||||
(stream creation or upload) leaves the host amplitude unchanged but the
|
||||
device column undefined, so the Simulation is marked failed: every later
|
||||
device-touching public method (incl. ``stream``) raises; close() still
|
||||
works. Close and recreate it.
|
||||
Invalid input changes nothing. A device error leaves the host amplitude
|
||||
unchanged but the device column undefined: treat it like any other CUDA
|
||||
exception here and close/recreate the Simulation before stepping again.
|
||||
"""
|
||||
self._require_usable()
|
||||
jet = self.bodies.jets[index].with_amplitude(amplitude)
|
||||
if self._initialized:
|
||||
try:
|
||||
self.field.curved.set_jet_amplitude(index, amplitude, self.stream)
|
||||
except BaseException as exc:
|
||||
self._failed = f"jet amplitude upload failed: {exc!r}"
|
||||
raise
|
||||
self.field.curved.set_jet_amplitude(index, amplitude, self.stream)
|
||||
self.bodies.jets[index] = jet # later geometry syncs keep the new amplitude
|
||||
|
||||
def _require_usable(self):
|
||||
failed = getattr(self, "_failed", None)
|
||||
if failed is not None:
|
||||
raise RuntimeError(f"Simulation failed ({failed}); close() and recreate it")
|
||||
|
||||
def remove_body(self, id: int) -> None:
|
||||
"""Remove a body from the simulation.
|
||||
|
||||
@@ -347,20 +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.
|
||||
"""
|
||||
self._require_usable()
|
||||
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, *,
|
||||
@@ -380,7 +362,6 @@ class Simulation:
|
||||
BodyTelemetry with fields ``force``, ``torque``, ``sensor``.
|
||||
``sensor`` is a dim-length array of zeros for non-sensor bodies.
|
||||
"""
|
||||
self._require_usable()
|
||||
if self.bodies.obs_pinned is None:
|
||||
raise RuntimeError("No obs buffer. Call run() first.")
|
||||
if stream is None:
|
||||
@@ -400,7 +381,6 @@ class Simulation:
|
||||
This is the most efficient readback for DRL control loops
|
||||
(single contiguous D2H copy, no Python per-body overhead).
|
||||
"""
|
||||
self._require_usable()
|
||||
if self.bodies.obs_pinned is None:
|
||||
raise RuntimeError("No obs buffer. Call run() first.")
|
||||
if stream is None:
|
||||
@@ -426,8 +406,15 @@ class Simulation:
|
||||
self.lbm_cfg.validate_runtime()
|
||||
self._assert_allocation_contract()
|
||||
arch = self._resolve_compile_arch()
|
||||
self._module = compiler.build_module(self.lbm_cfg, "uniform.cu", arch=arch,
|
||||
n_objects=expected_n_objects)
|
||||
next_dir = compiler.generate_config(self.lbm_cfg, n_objects=expected_n_objects)
|
||||
try:
|
||||
next_ptx = compiler.compile_kernel(arch=arch, build_dir=next_dir)
|
||||
next_module = compiler.load_module(next_ptx)
|
||||
except Exception:
|
||||
compiler.cleanup_build_dir(next_dir)
|
||||
raise
|
||||
old_dir = self._build_dir
|
||||
self._build_dir, self._ptx_path, self._module = next_dir, next_ptx, next_module
|
||||
self._compiled_n_objects = int(expected_n_objects)
|
||||
|
||||
# Reconnect field and stepper to new module
|
||||
@@ -445,6 +432,7 @@ class Simulation:
|
||||
self.stepper._step_count = _prev_step_count
|
||||
|
||||
self._assert_object_count_contract(expected_count=expected_n_objects)
|
||||
compiler.cleanup_build_dir(old_dir)
|
||||
|
||||
def recompile(self):
|
||||
"""Re-generate config headers and recompile kernel.
|
||||
@@ -463,22 +451,18 @@ class Simulation:
|
||||
@context_bound
|
||||
def initialize(self):
|
||||
"""Initialize flow field and sync objects to GPU."""
|
||||
self._require_usable()
|
||||
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)
|
||||
@@ -496,7 +480,6 @@ class Simulation:
|
||||
In EsoPull mode, the backing layout is decoded to physical DDF on
|
||||
GPU for the patched cells only, patched on host, then encoded back.
|
||||
"""
|
||||
self._require_usable()
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Call initialize() before sync_bodies()")
|
||||
|
||||
@@ -592,7 +575,6 @@ class Simulation:
|
||||
sensor) on GPU and reset the step accumulator before stepping.
|
||||
checkpoint_interval: Nonnegative integer; if >0, save every N steps.
|
||||
"""
|
||||
self._require_usable()
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Call initialize() first")
|
||||
steps = operator.index(steps)
|
||||
@@ -657,12 +639,10 @@ class Simulation:
|
||||
@context_bound
|
||||
def get_macroscopic(self) -> Dict[str, np.ndarray]:
|
||||
"""Download DDF and return rho, ux, uy [, uz]."""
|
||||
self._require_usable()
|
||||
return self.field.get_macroscopic(step_id=self.stepper.step_count)
|
||||
|
||||
@context_bound
|
||||
def get_ddf(self) -> np.ndarray:
|
||||
self._require_usable()
|
||||
self.field.download_ddf(step_id=self.stepper.step_count)
|
||||
return self.field.ddf.copy()
|
||||
|
||||
@@ -676,27 +656,23 @@ class Simulation:
|
||||
|
||||
Accepted: omega, fx, fy, fz, n_objects.
|
||||
"""
|
||||
self._require_usable()
|
||||
self.field.update_params(**kwargs)
|
||||
|
||||
# -- Snapshots -----------------------------------------------------------
|
||||
@context_bound
|
||||
def snapshot(self):
|
||||
"""Snapshot the flow field; restore keeps the current clock and controls."""
|
||||
self._require_usable()
|
||||
self.field.snapshot(step_id=self.stepper.step_count)
|
||||
|
||||
@context_bound
|
||||
def restore(self):
|
||||
"""Restore the saved flow field at the current streaming parity."""
|
||||
self._require_usable()
|
||||
self.field.restore(step_id=self.stepper.step_count)
|
||||
|
||||
# -- Checkpoint / Restore (HDF5) ----------------------------------------
|
||||
@context_bound
|
||||
def save_checkpoint(self, path: str = None):
|
||||
"""Save full simulation state to an HDF5 file."""
|
||||
self._require_usable()
|
||||
from .common.checkpoint import save_checkpoint
|
||||
if self.bodies.jets:
|
||||
raise NotImplementedError("Jets are not checkpointed yet")
|
||||
@@ -706,7 +682,6 @@ class Simulation:
|
||||
@context_bound
|
||||
def load_checkpoint(self, path: str):
|
||||
"""Restore simulation state from an HDF5 checkpoint."""
|
||||
self._require_usable()
|
||||
from .common.checkpoint import load_checkpoint
|
||||
if not self._initialized:
|
||||
self.initialize()
|
||||
@@ -731,6 +706,8 @@ class Simulation:
|
||||
self._module = None
|
||||
self._stream = None
|
||||
ctx.close()
|
||||
compiler.cleanup_build_dir(getattr(self, "_build_dir", None))
|
||||
self._build_dir = None
|
||||
self._closed = True
|
||||
|
||||
def __enter__(self):
|
||||
|
||||
@@ -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()
|
||||
@@ -1,53 +0,0 @@
|
||||
"""Direct Simulation Jet control upload; boundary-law accuracy is tested elsewhere."""
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
|
||||
from CelerisLab.body.jets import Jet, Radial, Sector
|
||||
from CelerisLab.lbm.descriptors import FLUID
|
||||
from ._cases import make_sim
|
||||
|
||||
|
||||
def test_simulation_jet_amplitude_upload_and_continued_steps():
|
||||
# Same grid/circle/sector as the existing uniform surface-actuation fixture.
|
||||
center = (16., 12.)
|
||||
with make_sim(nx=32, ny=24, velocity=0) as sim:
|
||||
sim.add_body("circle", center=center, radius=4.3)
|
||||
index = sim.add_jet(Jet(Sector(center, .7, .85, 4.4),
|
||||
Radial(center, inward=False), "quartic", .002))
|
||||
sim.initialize()
|
||||
curved = sim.field.curved
|
||||
shape, jet_id = curved.jet_shape.copy(), curved.jet_id.copy()
|
||||
flags = sim.get_flags()
|
||||
driven = jet_id == index
|
||||
assert driven.any() and (~driven).any()
|
||||
velocity_pointer = int(curved.surface_velocity_gpu)
|
||||
previous = None
|
||||
for interval, amplitude in enumerate((.002, -.001)):
|
||||
if interval:
|
||||
count = sim.stepper.step_count
|
||||
sim.set_jet_amplitude(index, amplitude)
|
||||
assert sim.stepper.step_count == count # update does not step
|
||||
expected = np.zeros_like(curved.surface_velocity)
|
||||
expected[driven] = (amplitude * shape[driven]).astype(np.float32)
|
||||
actual = np.empty_like(expected)
|
||||
with sim.ctx.activate():
|
||||
cuda.memcpy_dtoh(actual, curved.surface_velocity_gpu)
|
||||
np.testing.assert_array_equal(actual, expected) # actual device column
|
||||
np.testing.assert_array_equal(curved.surface_velocity, expected)
|
||||
assert np.any(actual[driven] != 0)
|
||||
if previous is not None:
|
||||
assert not np.array_equal(actual[driven], previous[driven])
|
||||
previous = actual.copy()
|
||||
assert sim.bodies.jets[index].amplitude == amplitude
|
||||
assert curved.jets[index].amplitude == amplitude
|
||||
assert int(curved.surface_velocity_gpu) == velocity_pointer
|
||||
np.testing.assert_array_equal(curved.jet_shape, shape)
|
||||
np.testing.assert_array_equal(curved.jet_id, jet_id)
|
||||
np.testing.assert_array_equal(sim.get_flags(), flags)
|
||||
sim.run(2)
|
||||
assert sim.stepper.step_count == 2 * (interval + 1)
|
||||
assert sim._failed is None
|
||||
macro = sim.get_macroscopic()
|
||||
fluid = (flags.reshape(24, 32) & FLUID) != 0
|
||||
assert all(np.isfinite(value[fluid]).all() for value in macro.values())
|
||||
assert np.all(macro["rho"][fluid] > 0)
|
||||
@@ -6,14 +6,14 @@ import pycuda.driver as cuda
|
||||
import pytest
|
||||
|
||||
import CelerisLab
|
||||
from CelerisLab.cuda import compiler
|
||||
from CelerisLab.cuda import compiler_v2
|
||||
from ._cases import make_sim
|
||||
|
||||
|
||||
def test_source_provenance():
|
||||
root = Path(__file__).resolve().parents[2] / "src/CelerisLab"
|
||||
assert Path(CelerisLab.__file__).resolve().parent == root
|
||||
assert Path(compiler.kernel_path("uniform.cu")).resolve() == root / "lbm/kernels/uniform.cu"
|
||||
assert Path(compiler_v2.kernel_path("kernel_v2.cu")).resolve() == root / "lbm/kernels/kernel_v2.cu"
|
||||
|
||||
|
||||
def test_two_contexts_interleaved_and_non_lifo_close():
|
||||
@@ -21,7 +21,7 @@ def test_two_contexts_interleaved_and_non_lifo_close():
|
||||
first = make_sim(nx=32, ny=16)
|
||||
second = make_sim(nx=48, ny=24, streaming="esopull", shifted=True)
|
||||
try:
|
||||
assert first._module is not second._module
|
||||
assert first._build_dir != second._build_dir
|
||||
assert cuda.Context.get_current() == before
|
||||
first.initialize()
|
||||
second.initialize()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,8 +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"
|
||||
#include "operators/block_collision.cuh"
|
||||
constexpr unsigned char KIND_OUTER=2;
|
||||
#include "step/block_channel.cu"
|
||||
#endif
|
||||
int main(){
|
||||
|
||||
@@ -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
|
||||
@@ -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])
|
||||
@@ -126,7 +100,8 @@ class BuildContracts(unittest.TestCase):
|
||||
def work(nx, count):
|
||||
build = compiler.generate_config(LBMConfig(nx=nx), count, build_root=root)
|
||||
barrier.wait()
|
||||
output = compiler.compile_kernel(build_dir=build)
|
||||
# Compatibility form is context-local even without explicit path.
|
||||
output = compiler.compile_kernel()
|
||||
data = json.loads(Path(output).read_text())
|
||||
self.assertEqual(data, {"nx": nx, "objects": count})
|
||||
metadata = json.loads(Path(output + ".json").read_text())
|
||||
@@ -155,7 +130,7 @@ class BuildContracts(unittest.TestCase):
|
||||
compiler.cleanup_build_dir(a)
|
||||
compiler.cleanup_build_dir(b)
|
||||
with self.assertRaises(RuntimeError):
|
||||
compiler.compile_kernel(build_dir=a) # removed snapshot is never compiled
|
||||
compiler.compile_kernel()
|
||||
|
||||
def test_failed_compile_cannot_implicitly_load_previous_artifact(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
@@ -168,7 +143,8 @@ class BuildContracts(unittest.TestCase):
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
compiler.compile_kernel(build_dir=build)
|
||||
self.assertEqual(Path(previous).read_bytes(), old_bytes)
|
||||
self.assertEqual(sorted(Path(build).glob("uniform-*.ptx")), []) # no partial output
|
||||
with self.assertRaisesRegex(RuntimeError, "No compiled PTX"):
|
||||
compiler.load_module()
|
||||
compiler.cleanup_build_dir(build)
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
from CelerisLab.lbm.channel import channel_masks, validate_channel_config, validate_channel_transfers
|
||||
from CelerisLab.lbm.refinement import FixedPatch2D
|
||||
from CelerisLab.lbm.refinement_tables import blocked_coarse_tables
|
||||
from CelerisLab.lbm.refinement_tables import blocked_base_tables
|
||||
from .test_numerical_operators import compile_run
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ def transfer(bounds, block=(4, 6)):
|
||||
cfg = LBMConfig(nx=24, ny=20, viscosity=.1)
|
||||
pair = FixedPatch2D.from_config(cfg, bounds)
|
||||
layout = BlockLayout2D.build(20, 24, *block)
|
||||
targets = blocked_coarse_tables(pair, layout, np.argsort(pair.coalesce_coarse))[1]
|
||||
targets = blocked_base_tables(pair, layout, np.argsort(pair.coalesce_coarse))[1]
|
||||
return pair, layout, targets
|
||||
|
||||
|
||||
@@ -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"):
|
||||
@@ -74,14 +74,14 @@ def test_evolved_ghost_snapshot_and_physical_download():
|
||||
from .test_refinement_three_level_multipatch import host_module
|
||||
host = host_module()
|
||||
storage = {}
|
||||
host.compiler.build_module.return_value.get_global.side_effect = lambda name: (name, 12 if name == "d_block_channel" else 20)
|
||||
host.compiler.load_module.return_value.get_global.side_effect = lambda name: (name, 12 if name == "d_block_channel" else 20)
|
||||
host.cuda.mem_alloc.side_effect = lambda size: Mock()
|
||||
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)])
|
||||
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]
|
||||
@@ -96,7 +96,6 @@ def test_evolved_ghost_snapshot_and_physical_download():
|
||||
arrays[0][0, 0, 0] = np.nan
|
||||
with pytest.raises(ValueError, match="finite"):
|
||||
backend.initialize(*arrays)
|
||||
assert backend._levels[0].kernel == host.compiler.build_module.return_value.get_function("BlockChannelStep")
|
||||
assert [call.kwargs["block_root_bc"] for call in host.compiler.build_module.call_args_list] == [2, 0]
|
||||
assert backend._levels[0].kernel == host.compiler.load_module.return_value.get_function("BlockChannelStep")
|
||||
finally:
|
||||
backend.close()
|
||||
|
||||
@@ -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,70 +0,0 @@
|
||||
"""The public examples run all-blocked: their level tiles must reach the device unchanged.
|
||||
|
||||
Host only: a context whose activation raises proves construction (layouts, tables,
|
||||
boundary checks) succeeded up to the first CUDA call.
|
||||
"""
|
||||
import ast
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||
from CelerisLab.body.objects import SimObject
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
|
||||
EXAMPLES = Path(__file__).resolve().parents[2] / "examples"
|
||||
|
||||
|
||||
def literal(name, constant):
|
||||
"""Module-level literal from an example, read without importing it (no pycuda)."""
|
||||
tree = ast.parse((EXAMPLES / name).read_text())
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
names = target.elts if isinstance(target, ast.Tuple) else [target]
|
||||
for i, t in enumerate(names):
|
||||
if isinstance(t, ast.Name) and t.id == constant:
|
||||
value = node.value.elts[i] if isinstance(target, ast.Tuple) else node.value
|
||||
return ast.literal_eval(value)
|
||||
raise KeyError(constant)
|
||||
|
||||
|
||||
def forbidden():
|
||||
return Mock(activate=Mock(side_effect=AssertionError("CUDA reached")), sm_arch="sm_70")
|
||||
|
||||
|
||||
class ExampleTiles(unittest.TestCase):
|
||||
def test_fixed_patch_jet_tiles(self):
|
||||
bounds, blocks = literal("fixed_patch_jet.py", "BOUNDS"), literal("fixed_patch_jet.py", "LEVEL_BLOCKS")
|
||||
width, amplitude = literal("fixed_patch_jet.py", "WIDTH"), literal("fixed_patch_jet.py", "AMPLITUDE")
|
||||
cfg = LBMConfig(nx=48, ny=48, viscosity=.1, velocity=0., wall_slot_width=width,
|
||||
wall_normal_velocity=amplitude, y_wall_bc="on_site_velocity",
|
||||
top_wall_bc="reservoir")
|
||||
with self.assertRaisesRegex(AssertionError, "CUDA reached"):
|
||||
FixedPatchStepper(cfg, context=forbidden(), coarse_boundary="slot_reservoir",
|
||||
refinements=[bounds], level_blocks=[tuple(b) for b in blocks])
|
||||
|
||||
def test_fixed_periodic_patch_tiles(self):
|
||||
with self.assertRaisesRegex(AssertionError, "CUDA reached"):
|
||||
FixedPatchStepper(LBMConfig(nx=16, ny=16, viscosity=.1, velocity=1e-3), context=forbidden(),
|
||||
refinements=[(4, 4, 12, 12)], level_blocks=[(16, 16), (24, 24)])
|
||||
|
||||
def test_fixed_patch_circle_default_tiles(self):
|
||||
bounds = literal("fixed_patch_circle.py", "BOUNDS")
|
||||
nx, ny = literal("fixed_patch_circle.py", "NX"), literal("fixed_patch_circle.py", "NY")
|
||||
center, radius = literal("fixed_patch_circle.py", "CENTER"), literal("fixed_patch_circle.py", "RADIUS")
|
||||
body = SimObject(0, geometry=CircleGeometry(*center, radius), center=center, radius=radius)
|
||||
periodic = LBMConfig(nx=nx, ny=ny, viscosity=.1)
|
||||
slot = replace(periodic, velocity=0, wall_normal_velocity=.001, wall_slot_width=8,
|
||||
y_wall_bc="on_site_velocity", top_wall_bc="reservoir")
|
||||
for mode, cfg in (("periodic", periodic), ("slot_reservoir", slot)):
|
||||
with self.subTest(mode=mode), self.assertRaisesRegex(AssertionError, "CUDA reached"):
|
||||
FixedPatchStepper(cfg, context=forbidden(), coarse_boundary=mode,
|
||||
refinements=[bounds], level_blocks=[(12, 8), (16, 20)],
|
||||
level_objects=[(), (body,)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -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__":
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
"""Jet amplitude update failure contract (host doubles only, no CUDA numerics).
|
||||
|
||||
Both owners validate before any device write, commit host state only after the
|
||||
upload returns, and refuse every later call except close() after an uncertain
|
||||
device write.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("pycuda.driver")
|
||||
from CelerisLab import simulation as module
|
||||
from CelerisLab.body.jets import Jet, Radial, Sector
|
||||
from tests.unit.test_pinned_body_remap import ready
|
||||
from tests.unit.test_simulation_lifecycle import make_simulation
|
||||
|
||||
BAD = (float("nan"), float("inf"), 1.)
|
||||
|
||||
|
||||
class Curved:
|
||||
"""Records uploads; ``fail`` raises once the write was attempted."""
|
||||
def __init__(self):
|
||||
self.writes, self.fail = [], False
|
||||
|
||||
def set_jet_amplitude(self, index, amplitude, stream):
|
||||
self.writes.append((index, amplitude))
|
||||
if self.fail:
|
||||
raise RuntimeError("upload failed")
|
||||
|
||||
|
||||
def simulation(monkeypatch):
|
||||
sim = make_simulation(monkeypatch)
|
||||
jet = Jet(Sector((6., 5.), 0., .5, 4.), Radial((6., 5.)), "uniform", 1e-3)
|
||||
sim._failed, sim._initialized = None, True
|
||||
sim.bodies, sim.field = SimpleNamespace(jets=[jet]), SimpleNamespace(curved=Curved())
|
||||
return sim, jet
|
||||
|
||||
|
||||
def test_simulation_invalid_input_writes_nothing_and_stays_usable(monkeypatch):
|
||||
sim, jet = simulation(monkeypatch)
|
||||
for amplitude in BAD:
|
||||
with pytest.raises(ValueError):
|
||||
sim.set_jet_amplitude(0, amplitude)
|
||||
with pytest.raises(IndexError):
|
||||
sim.set_jet_amplitude(1, 2e-3)
|
||||
assert sim.field.curved.writes == [] and sim.bodies.jets[0] is jet and sim._failed is None
|
||||
sim.set_jet_amplitude(0, 2e-3) # retry after a rejected input
|
||||
assert sim.field.curved.writes == [(0, 2e-3)] and sim.bodies.jets[0].amplitude == 2e-3
|
||||
sim.close()
|
||||
|
||||
|
||||
def test_simulation_upload_failure_keeps_host_and_blocks_reuse(monkeypatch):
|
||||
sim, jet = simulation(monkeypatch)
|
||||
sim.field.curved.fail = True
|
||||
with pytest.raises(RuntimeError, match="upload failed"):
|
||||
sim.set_jet_amplitude(0, 2e-3)
|
||||
assert sim.bodies.jets[0] is jet and sim._failed is not None
|
||||
sim.field.curved.fail = False
|
||||
for call in (lambda: sim.set_jet_amplitude(0, 2e-3), lambda: sim.run(1), sim.step,
|
||||
sim.sync_bodies, sim.initialize, sim.get_ddf, sim.get_macroscopic,
|
||||
lambda: sim.read_surface_exchange(0), lambda: sim.read_body(0),
|
||||
sim.read_bodies, sim.snapshot, sim.restore, sim.save_checkpoint,
|
||||
lambda: sim.load_checkpoint("x.h5"), lambda: sim.update_runtime_params(omega=1.)):
|
||||
with pytest.raises(RuntimeError, match="Simulation failed"):
|
||||
call()
|
||||
assert sim.field.curved.writes == [(0, 2e-3)]
|
||||
sim.close() # cleanup still works
|
||||
assert sim._closed and sim.ctx.is_closed
|
||||
|
||||
|
||||
def test_simulation_stream_creation_failure_fails_closed(monkeypatch):
|
||||
sim, jet = simulation(monkeypatch)
|
||||
created = []
|
||||
def stream():
|
||||
created.append(1)
|
||||
raise RuntimeError("stream creation failed")
|
||||
monkeypatch.setattr(module.cuda, "Stream", stream, raising=False)
|
||||
sim._stream = None
|
||||
with pytest.raises(RuntimeError, match="stream creation failed"):
|
||||
sim.set_jet_amplitude(0, 2e-3)
|
||||
assert sim.bodies.jets[0] is jet and sim._failed is not None
|
||||
assert sim.field.curved.writes == [] and created == [1]
|
||||
for call in (lambda: sim.stream, lambda: sim.set_jet_amplitude(0, 2e-3)):
|
||||
with pytest.raises(RuntimeError, match="Simulation failed"):
|
||||
call()
|
||||
assert created == [1] # the public stream property does not retry creation
|
||||
sim.ctx.detach_failure = True
|
||||
with pytest.raises(RuntimeError, match="detach failed"):
|
||||
sim.close()
|
||||
assert not sim._closed
|
||||
sim.close() # close stays retryable after the failure
|
||||
assert sim._closed and sim.ctx.is_closed
|
||||
|
||||
|
||||
def test_simulation_before_initialize_commits_host_only(monkeypatch):
|
||||
sim, _ = simulation(monkeypatch)
|
||||
sim._initialized = False
|
||||
sim.set_jet_amplitude(0, -2e-3)
|
||||
assert sim.field.curved.writes == [] and sim.bodies.jets[0].amplitude == -2e-3
|
||||
sim.close()
|
||||
|
||||
|
||||
def stepper_state(run):
|
||||
boundary = run._boundaries[2]
|
||||
return boundary.tables["surface_velocity"].copy(), boundary.jets, run.jets[2]
|
||||
|
||||
|
||||
def test_stepper_invalid_input_writes_nothing_and_stays_ready():
|
||||
run, module, _, _, _ = ready()
|
||||
try:
|
||||
velocity, jets, level_jets = stepper_state(run)
|
||||
module.cuda.memcpy_htod.reset_mock()
|
||||
for args, kw, error in [((0, a), {"level": 2}, ValueError) for a in BAD] + [
|
||||
((1, .01), {"level": 2}, IndexError), ((0, .01), {"level": 3}, ValueError)]:
|
||||
with pytest.raises(error):
|
||||
run.set_jet_amplitude(*args, **kw)
|
||||
module.cuda.memcpy_htod.assert_not_called()
|
||||
np.testing.assert_array_equal(run._boundaries[2].tables["surface_velocity"], velocity)
|
||||
assert (run._boundaries[2].jets, run.jets[2]) == (jets, level_jets)
|
||||
assert run._status == "ready"
|
||||
run.set_jet_amplitude(0, .017, level=2)
|
||||
assert run.jets[2][0].amplitude == .017
|
||||
finally:
|
||||
run.close()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("where", ["upload", "synchronize"])
|
||||
def test_stepper_device_failure_keeps_host_and_blocks_reuse(where):
|
||||
run, module, _, _, _ = ready()
|
||||
try:
|
||||
velocity, jets, level_jets = stepper_state(run)
|
||||
if where == "upload":
|
||||
module.cuda.memcpy_htod.side_effect = RuntimeError("upload failed")
|
||||
else:
|
||||
run._stream.synchronize.side_effect = RuntimeError("upload failed")
|
||||
with pytest.raises(RuntimeError, match="upload failed"):
|
||||
run.set_jet_amplitude(0, .017, level=2)
|
||||
np.testing.assert_array_equal(run._boundaries[2].tables["surface_velocity"], velocity)
|
||||
assert (run._boundaries[2].jets, run.jets[2]) == (jets, level_jets)
|
||||
assert run._status == "failed"
|
||||
module.cuda.memcpy_htod.side_effect = None
|
||||
run._stream.synchronize.side_effect = None
|
||||
for call in (lambda: run.set_jet_amplitude(0, .017, level=2), run.step, run.download):
|
||||
with pytest.raises(RuntimeError, match="failed"):
|
||||
call()
|
||||
finally:
|
||||
run.close()
|
||||
assert run._status == "closed"
|
||||
@@ -188,57 +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))
|
||||
sim._require_usable = Simulation._require_usable.__get__(sim)
|
||||
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))
|
||||
sim._require_usable = Simulation._require_usable.__get__(sim)
|
||||
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)
|
||||
|
||||
@@ -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):
|
||||
@@ -45,81 +41,45 @@ class GuardTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(RuntimeError, "velocity/profile"):
|
||||
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")
|
||||
def test_baseline_pins(self):
|
||||
self.assertEqual(
|
||||
source_guard(ROOT, "baseline")["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)
|
||||
# Rebuild the reviewed candidate from actual pinned headers, not fixtures.
|
||||
candidate = root / "header_candidate"
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(HERE / "les/prepare.py"),
|
||||
"--repo",
|
||||
str(ROOT),
|
||||
"--output-dir",
|
||||
str(candidate),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
shutil.copyfile(candidate / "patched" / HEADER, root / HEADER)
|
||||
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:
|
||||
|
||||
@@ -94,7 +94,7 @@ class MultiplePinnedRemap(unittest.TestCase):
|
||||
run.set_jet_amplitude(0, .017, level=2)
|
||||
run.set_jet_amplitude(1, -.011, level=2)
|
||||
before = run.download()
|
||||
clone = run.remap_finest((PINS+MOVED+ADDED)[::-1], objects=bodies)
|
||||
clone = run.remap_child((PINS+MOVED+ADDED)[::-1], child_objects=bodies)
|
||||
try:
|
||||
self.assertEqual(tuple(j.amplitude for j in clone._boundaries[2].jets), (.017, -.011))
|
||||
np.testing.assert_array_equal(clone._boundaries[2].tables['surface_velocity'],
|
||||
@@ -116,7 +116,7 @@ class MultiplePinnedRemap(unittest.TestCase):
|
||||
start = len(allocations)
|
||||
module.cuda.memcpy_dtoh.reset_mock()
|
||||
with self.assertRaisesRegex(ValueError, 'boundary'):
|
||||
run.remap_finest(PINS+FLUID, objects=changed)
|
||||
run.remap_child(PINS+FLUID, child_objects=changed)
|
||||
self.assertEqual(len(allocations), start)
|
||||
module.cuda.memcpy_dtoh.assert_not_called()
|
||||
with self.assertRaisesRegex(ValueError, 'pinned'):
|
||||
@@ -128,7 +128,7 @@ class MultiplePinnedRemap(unittest.TestCase):
|
||||
cases = [(PINS+FLUID, bodies[:1]), (PINS+FLUID, bodies[::-1]),
|
||||
(PINS+FLUID, (bodies[0], body(center=(32.1, 10.)))),
|
||||
(PINS+FLUID, (bodies[0], body(SurfaceActuation(0., .7, 0.), (32., 10.)))),
|
||||
(PINS+FLUID, run.boundaries[2].objects)]
|
||||
(PINS+FLUID, run.child_boundary.objects)]
|
||||
for index in range(2):
|
||||
changed = list(PINS+FLUID)
|
||||
x0, y0, x1, y1 = changed[index]
|
||||
@@ -141,7 +141,7 @@ class MultiplePinnedRemap(unittest.TestCase):
|
||||
run._stream.synchronize.reset_mock()
|
||||
for rects, supplied in cases:
|
||||
with self.subTest(rects=rects), self.assertRaises(ValueError):
|
||||
run.remap_finest(rects, objects=supplied)
|
||||
run.remap_child(rects, child_objects=supplied)
|
||||
self.assertEqual(len(allocations), start)
|
||||
module.cuda.memcpy_dtoh.assert_not_called()
|
||||
run._stream.synchronize.assert_not_called()
|
||||
|
||||
@@ -135,6 +135,6 @@ def test_d2_entrypoint_compiles_in_isolation(tmp_path, model, shift, storage):
|
||||
source=re.sub(r"(#define\s+"+key+r"\s+)\S+",lambda match: match[1]+str(value),source)
|
||||
path.write_text(source)
|
||||
result=subprocess.run([nvcc,"-ptx","-arch=sm_70","-std=c++17",
|
||||
str(copy/"uniform.cu"),"-o",str(tmp_path/"kernel.ptx")],
|
||||
str(copy/"kernel_v2.cu"),"-o",str(tmp_path/"kernel.ptx")],
|
||||
capture_output=True,text=True)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
@@ -61,11 +61,11 @@ class PinnedBodyRemap(unittest.TestCase):
|
||||
run,module,allocations,storage,objects=ready()
|
||||
run.set_jet_amplitude(0,.017,level=2)
|
||||
before=run.download()
|
||||
clone=run.remap_finest(B,objects=objects)
|
||||
clone=run.remap_child(B,child_objects=objects)
|
||||
self.assertEqual(clone._boundaries[2].jets[0].amplitude,.017)
|
||||
np.testing.assert_array_equal(clone._boundaries[2].tables['surface_velocity'],
|
||||
run._boundaries[2].tables['surface_velocity'])
|
||||
self.assertTrue(same_physical_boundary(run.boundaries[2],clone.boundaries[2]))
|
||||
self.assertTrue(same_physical_boundary(run.child_boundary,clone.child_boundary))
|
||||
clone.step()
|
||||
self.assertEqual((run.step_count,clone.step_count),(7,8))
|
||||
for got,want in zip(run.download(),before): np.testing.assert_array_equal(got,want)
|
||||
@@ -73,7 +73,7 @@ class PinnedBodyRemap(unittest.TestCase):
|
||||
|
||||
def test_static_body_without_actuation(self):
|
||||
run,module,allocations,storage,objects=ready(jets=())
|
||||
clone=run.remap_finest(A,objects=objects)
|
||||
clone=run.remap_child(A,child_objects=objects)
|
||||
self.assertFalse(clone._boundaries[2].actuated)
|
||||
self.assertNotIn('surface_velocity',clone._boundaries[2].tables)
|
||||
for a,b in zip(run.download(),clone.download()): np.testing.assert_array_equal(a,b)
|
||||
@@ -82,13 +82,13 @@ class PinnedBodyRemap(unittest.TestCase):
|
||||
def test_reject_before_device_reads_or_allocation(self):
|
||||
run,module,allocations,storage,objects=ready()
|
||||
start=len(allocations)
|
||||
cases=[(B,()),(B,run.boundaries[2].objects),(B,(body(center=(12.1,14.)),)),
|
||||
cases=[(B,()),(B,run.child_boundary.objects),(B,(body(center=(12.1,14.)),)),
|
||||
(((24,16,36,40),B[1]),objects),(((16,28,32,40),B[1]),objects),
|
||||
((B[1],),objects)]
|
||||
module.cuda.memcpy_dtoh.reset_mock()
|
||||
for rects,objs in cases:
|
||||
with self.subTest(rects=rects),self.assertRaises(ValueError):
|
||||
run.remap_finest(rects,objects=objs)
|
||||
run.remap_child(rects,child_objects=objs)
|
||||
self.assertEqual(len(allocations),start)
|
||||
module.cuda.memcpy_dtoh.assert_not_called()
|
||||
self.assertEqual((run._status,run.step_count),('ready',7))
|
||||
@@ -100,9 +100,9 @@ class PinnedBodyRemap(unittest.TestCase):
|
||||
(body(SurfaceActuation(0.,.7,speed)),),())
|
||||
for spec in (SurfaceActuation(0.,.7,.003),SurfaceActuation(2.,.7,speed)):
|
||||
module.cuda.memcpy_dtoh.reset_mock()
|
||||
with self.assertRaises(ValueError): run.remap_finest(B,objects=(body(spec),))
|
||||
with self.assertRaises(ValueError): run.remap_child(B,child_objects=(body(spec),))
|
||||
module.cuda.memcpy_dtoh.assert_not_called()
|
||||
clone=run.remap_finest(B,objects=objects)
|
||||
clone=run.remap_child(B,child_objects=objects)
|
||||
clone.close();run.close()
|
||||
|
||||
def test_allocation_failure_keeps_old_ready_and_count(self):
|
||||
@@ -117,7 +117,7 @@ class PinnedBodyRemap(unittest.TestCase):
|
||||
return allocate(size)
|
||||
with patch.object(module.cuda,'mem_alloc',side_effect=fail):
|
||||
with self.assertRaisesRegex(RuntimeError,'allocation'):
|
||||
run.remap_finest(B,objects=objects)
|
||||
run.remap_child(B,child_objects=objects)
|
||||
self.assertEqual((run._status,run.step_count),('ready',7))
|
||||
for got,want in zip(run.download(),before): np.testing.assert_array_equal(got,want)
|
||||
for a in saved: a.free.assert_not_called()
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
|
||||
import CelerisLab
|
||||
from CelerisLab.config import LBMConfig, load_lbm_config
|
||||
from CelerisLab.cuda import compiler
|
||||
from CelerisLab.cuda import compiler_v2 as compiler
|
||||
from CelerisLab.common.checkpoint import save_checkpoint, load_checkpoint
|
||||
from .test_numerical_operators import compile_run
|
||||
from .test_state_regressions import device, make_field, manager
|
||||
@@ -18,7 +18,7 @@ from .test_state_regressions import device, make_field, manager
|
||||
def test_planar_helper_moments_and_known_populations(tmp_path):
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
assert Path(CelerisLab.__file__).resolve() == root/'src/CelerisLab/__init__.py'
|
||||
assert Path(compiler.kernel_path('uniform.cu')).resolve() == root/'src/CelerisLab/lbm/kernels/uniform.cu'
|
||||
assert Path(compiler.kernel_path('kernel_v2.cu')).resolve() == root/'src/CelerisLab/lbm/kernels/kernel_v2.cu'
|
||||
lines = compile_run(tmp_path, 'planar_velocity.cpp', dict(WALL_NORMAL_VELOCITY='0.0f'))
|
||||
cx = np.array([0,1,-1,0,0,1,-1,1,-1])
|
||||
cy = np.array([0,0,0,1,-1,1,-1,-1,1])
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -1,196 +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.build_module.side_effect = lambda *a, **k: load(None)
|
||||
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_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 constant path still allows MRT.
|
||||
for options in ({'cfg':replace(CFG,collision='MRT')},):
|
||||
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_finest(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_finest(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()
|
||||
@@ -25,7 +25,7 @@ def backend(observe=True, obj=None):
|
||||
module = Mock()
|
||||
module.get_global.return_value = (Mock(), 20)
|
||||
module.get_function.side_effect = lambda name: Mock(side_effect=lambda *a, **k: calls.append((name, a, k)))
|
||||
host.compiler.build_module.return_value = module
|
||||
host.compiler.load_module.return_value = module
|
||||
host.cuda.mem_alloc.side_effect = lambda n: Mock(size=n)
|
||||
host.cuda.memcpy_dtoh.side_effect = lambda array, pointer: array.fill(0)
|
||||
ctx = Mock(activate=nullcontext, sm_arch='sm_70')
|
||||
@@ -85,7 +85,7 @@ class RawExchange(unittest.TestCase):
|
||||
|
||||
def test_four_level_schedule_publication_and_fixed_storage(self):
|
||||
run, host, calls, arrays = backend()
|
||||
b = run._boundaries[3]
|
||||
b = run._level_boundaries[3]
|
||||
rows = len(b.tables['cell'])
|
||||
self.assertEqual(b.last_raw.size, rows * 16)
|
||||
self.assertEqual(b.interval_integral.size, rows * 32)
|
||||
@@ -128,7 +128,7 @@ class RawExchange(unittest.TestCase):
|
||||
|
||||
def test_disabled_has_nulls_and_no_observation_allocation_or_launch(self):
|
||||
run, host, calls, _ = backend(False)
|
||||
b = run._boundaries[3]
|
||||
b = run._level_boundaries[3]
|
||||
self.assertIs(b.last_raw, b.interval_integral)
|
||||
self.assertIs(b.interval_integral, None)
|
||||
run.step()
|
||||
@@ -153,6 +153,14 @@ class RawExchange(unittest.TestCase):
|
||||
run._stream.synchronize.side_effect = None
|
||||
run.close()
|
||||
|
||||
def test_dense_observation_rejected_before_device_access(self):
|
||||
host = host_module()
|
||||
ctx = Mock()
|
||||
with self.assertRaisesRegex(ValueError, 'blocked body'):
|
||||
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):
|
||||
for level in (1, 2, 3, 7):
|
||||
self._check_weights_split_and_window(level)
|
||||
|
||||
@@ -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()
|
||||
@@ -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
-385
@@ -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,47 +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):
|
||||
"""The frozen manifest verify_source selects for (collision, observation_mode).
|
||||
|
||||
Source identity is owned by test_source_pin; this reads no checkout files.
|
||||
"""
|
||||
return r.source_manifest(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)
|
||||
@@ -224,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,
|
||||
@@ -243,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(),
|
||||
@@ -263,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():
|
||||
@@ -293,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(
|
||||
@@ -305,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():
|
||||
@@ -338,326 +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
|
||||
refined_calls = []
|
||||
refined = SimpleNamespace(set_jet_amplitude=lambda i, a, *, level: refined_calls.append((i, a, level)))
|
||||
n, event = r.jet_interval(refined, "reverse", switch, 80, diameter, level=1)
|
||||
assert n == 80 and event["step"] == switch and event["tstar"] == 300
|
||||
assert refined_calls == [(0, -.001, 1), (1, .001, 1)]
|
||||
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)
|
||||
|
||||
@@ -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}):
|
||||
@@ -177,7 +177,7 @@ class FixedPatchSetTests(unittest.TestCase):
|
||||
self.assertEqual((fy, fx), (32, 48))
|
||||
rows = cells = 0
|
||||
for p in s.patches:
|
||||
y0, x0 = p.fine_level_offset
|
||||
y0, x0 = p.fine_level_origin
|
||||
ly, lx = p.fine_shape
|
||||
np.testing.assert_array_equal(s.fine_kind[y0:y0 + ly, x0:x0 + lx], p.fine_kind)
|
||||
gy, gx = np.divmod(p.ghost_fine.astype(np.int64), lx)
|
||||
@@ -224,7 +224,7 @@ class FixedPatchSetTests(unittest.TestCase):
|
||||
layout = s.fine_layout((4, 4))
|
||||
support = np.zeros(s.fine_shape, bool)
|
||||
for p in s.patches:
|
||||
y0, x0 = p.fine_level_offset
|
||||
y0, x0 = p.fine_level_origin
|
||||
support[y0:y0 + p.fine_shape[0], x0:x0 + p.fine_shape[1]] = True
|
||||
np.testing.assert_array_equal(layout.owned, support)
|
||||
self.assertTrue(layout.owned.ravel()[s.ghost_fine.astype(np.intp)].all())
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
"""Host guards and source contract for MRT reuse in refined/blocked stepping (no CUDA)."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
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 / "blocked.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)
|
||||
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);", block_collision)
|
||||
self.assertIn("#if COLLISION_MODEL == 2 || BLOCK_ROOT_BC == 2", block_collision)
|
||||
self.assertEqual(channel.count("block_collide(f);"), 1)
|
||||
# Executable text only: the header comment explains the absent force read.
|
||||
code = re.sub(r"//[^\n]*", "", channel + "\n" + block_collision)
|
||||
self.assertIn("d_params.omega", code)
|
||||
for text in ("collide_mrt_unforced", "collide_dispatch(", "d_params.fx", "d_params.fy"):
|
||||
self.assertNotIn(text, code)
|
||||
for source in (blocks,):
|
||||
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)
|
||||
# One unforced MRT call, in block_collide only; the steps never collide directly.
|
||||
self.assertEqual(block_collision.count("collide_mrt_no_force(f, rho, ux, uy, omega);"), 1)
|
||||
self.assertEqual(block_collision.count("compute_rho_u(f, rho, ux, uy);"), 1)
|
||||
self.assertEqual(block_collision.count("fmaxf(OMEGA_COLLISION_MIN, d_params.omega)"), 1)
|
||||
for source in (blocks, channel):
|
||||
self.assertNotIn("collide_mrt", source)
|
||||
self.assertNotIn("collide_srt", source)
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -22,13 +22,12 @@ def host_module():
|
||||
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 in (
|
||||
'cuda','curved_links'))]
|
||||
'cuda','block_stepper','curved_links'))]
|
||||
module=types.ModuleType('CelerisLab.lbm._test_host_refinement')
|
||||
module.__package__='CelerisLab.lbm'
|
||||
from CelerisLab.body.jets import surface_link_velocity
|
||||
module.__dict__.update(cuda=Mock(),compiler=Mock(),
|
||||
module.__dict__.update(cuda=Mock(),compiler=Mock(),MAX_GRID_DIM=65535,
|
||||
surface_link_velocity=surface_link_velocity)
|
||||
module.cuda.mem_get_info.return_value=(1<<40,1<<40) # free-memory preflight passes
|
||||
exec(compile(tree,str(path),'exec'),module.__dict__)
|
||||
return module
|
||||
|
||||
@@ -141,7 +140,7 @@ class Gate(unittest.TestCase):
|
||||
def test_sparse_parent_tables_preserve_inherited_kinds(self):
|
||||
child,kind=r.nest_patch_set(CFG,self.parent,CHILDREN)
|
||||
layout=self.parent.fine_layout((4,4));order=np.argsort(child.coalesce_coarse)
|
||||
donors,targets,bkind=host.blocked_coarse_tables(child,layout,order,composed_kind=kind)
|
||||
donors,targets,bkind=host.blocked_base_tables(child,layout,order,composed_kind=kind)
|
||||
self.assertTrue((donors < layout.hole).all())
|
||||
self.assertTrue((targets < np.prod(layout.blocked_shape)).all())
|
||||
np.testing.assert_array_equal(bkind[layout.owner[layout.owned]],kind[layout.owned])
|
||||
@@ -158,10 +157,10 @@ class Gate(unittest.TestCase):
|
||||
hole=int(np.flatnonzero(~layout.owned.ravel())[0])
|
||||
bad=child.ghost_coarse.copy();bad[0]=hole
|
||||
with self.assertRaisesRegex(ValueError,'owned uncovered'):
|
||||
host.blocked_coarse_tables(replace(child,ghost_coarse=bad),layout,order,composed_kind=kind)
|
||||
host.blocked_base_tables(replace(child,ghost_coarse=bad),layout,order,composed_kind=kind)
|
||||
bad=child.coalesce_coarse.copy();bad[0]=hole
|
||||
with self.assertRaisesRegex(ValueError,'owned uncovered'):
|
||||
host.blocked_coarse_tables(replace(child,coalesce_coarse=bad),layout,order,composed_kind=kind)
|
||||
host.blocked_base_tables(replace(child,coalesce_coarse=bad),layout,order,composed_kind=kind)
|
||||
|
||||
|
||||
def bodies_and_jets(speed=.002):
|
||||
@@ -174,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):
|
||||
@@ -194,9 +191,9 @@ def mock_backend(*, children=CHILDREN, bodies=False, fail_compile=False, fail_up
|
||||
gpu.get_function.side_effect=lambda name: Mock(name=name)
|
||||
gpu.get_global.side_effect=lambda name: (Mock(),20 if name=='d_params' else 16)
|
||||
return gpu
|
||||
module.compiler.build_module.side_effect=lambda *a,**k: load(None)
|
||||
module.compiler.load_module.side_effect=load
|
||||
if fail_compile:
|
||||
module.compiler.build_module.side_effect=[load(None),load(None),RuntimeError('L2 compile failed')]
|
||||
module.compiler.compile_kernel.side_effect=['first','second',RuntimeError('L2 compile failed')]
|
||||
if fail_upload:
|
||||
def upload(*args):
|
||||
if len(allocations)>20:
|
||||
@@ -204,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()
|
||||
@@ -216,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()
|
||||
|
||||
@@ -316,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__':
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""CPU mocks execute production remap lifecycle; no GPU numerical claim."""
|
||||
from dataclasses import replace
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -30,7 +29,7 @@ class RuntimeChildRemap(unittest.TestCase):
|
||||
saved = {key: value.copy() for key, value in storage.items()}
|
||||
old_allocations = list(allocations)
|
||||
module.cuda.memcpy_dtoh.reset_mock()
|
||||
clone = run.remap_finest(A)
|
||||
clone = run.remap_child(A)
|
||||
self.assertEqual(module.cuda.memcpy_dtoh.call_count, 3)
|
||||
for a, b in zip(clone.download(), before):
|
||||
np.testing.assert_array_equal(a, b)
|
||||
@@ -49,7 +48,7 @@ class RuntimeChildRemap(unittest.TestCase):
|
||||
run, module, allocations, storage = ready_backend()
|
||||
before = run.download()
|
||||
expected = remap(topology(A), topology(B), before).g
|
||||
clone = run.remap_finest(B)
|
||||
clone = run.remap_child(B)
|
||||
for level, want, got in zip(clone._levels, expected, clone.download()):
|
||||
np.testing.assert_array_equal(got, want)
|
||||
for ptr in (level.current, level.spare):
|
||||
@@ -66,18 +65,17 @@ class RuntimeChildRemap(unittest.TestCase):
|
||||
module.cuda.memcpy_dtoh.reset_mock()
|
||||
for status in ('new', 'failed', 'closed', 'advancing'):
|
||||
run._status = status
|
||||
with self.assertRaises(RuntimeError): run.remap_finest(B)
|
||||
with self.assertRaises(RuntimeError): run.remap_child(B)
|
||||
run._status = 'ready'
|
||||
for bad in ([], [(40,16,48,40)], [(16,16,32,24),(16,27,32,55)]):
|
||||
with self.assertRaises(ValueError): run.remap_finest(bad)
|
||||
records, pairs = run._boundaries, run.hierarchy.pairs
|
||||
with self.assertRaises(ValueError): run.remap_child(bad)
|
||||
hosts, pairs = run._boundary_hosts, run.hierarchy.pairs
|
||||
for attribute, value in (('_wall_aware', True),
|
||||
# level-2 record whose host geometry differs from objects=()
|
||||
('_boundaries', [*records[:2], SimpleNamespace(host=object(), jets=())]),
|
||||
('_boundaries', [records[0], object(), records[2]]), # level-1 body
|
||||
('_boundary_hosts', [*hosts[:2], object()]), # child boundary
|
||||
('_boundary_hosts', [hosts[0], object(), hosts[2]]), # fine boundary
|
||||
('hierarchy', replace(run.hierarchy, pairs=(pairs[0], None)))):
|
||||
with patch.object(run, attribute, value):
|
||||
with self.assertRaises(ValueError): run.remap_finest(B)
|
||||
with self.assertRaises(ValueError): run.remap_child(B)
|
||||
self.assertEqual(len(allocations), start)
|
||||
module.cuda.memcpy_dtoh.assert_not_called()
|
||||
run.close()
|
||||
@@ -88,7 +86,7 @@ class RuntimeChildRemap(unittest.TestCase):
|
||||
before = run.download()
|
||||
old_allocations = list(allocations)
|
||||
if failure == 'construction':
|
||||
operation = patch.object(module.compiler, 'build_module', side_effect=RuntimeError('candidate compile'))
|
||||
operation = patch.object(module.compiler, 'compile_kernel', side_effect=RuntimeError('candidate compile'))
|
||||
else:
|
||||
initialize = type(run).initialize
|
||||
def fail(candidate, *arrays):
|
||||
@@ -104,7 +102,7 @@ class RuntimeChildRemap(unittest.TestCase):
|
||||
initialize(candidate, *arrays)
|
||||
operation = patch.object(type(run), 'initialize', fail)
|
||||
with operation, self.assertRaisesRegex(RuntimeError, 'candidate'):
|
||||
run.remap_finest(B)
|
||||
run.remap_child(B)
|
||||
self.assertEqual((run._status, run.step_count), ('ready', 7))
|
||||
for a, b in zip(run.download(), before): np.testing.assert_array_equal(a, b)
|
||||
for allocation in old_allocations: allocation.free.assert_not_called()
|
||||
@@ -118,7 +116,7 @@ class RuntimeChildRemap(unittest.TestCase):
|
||||
target = run._stream.synchronize if operation == 'sync' else module.cuda.memcpy_dtoh
|
||||
target.side_effect = RuntimeError('old device failure')
|
||||
with self.assertRaisesRegex(RuntimeError, 'old device'):
|
||||
run.remap_finest(B)
|
||||
run.remap_child(B)
|
||||
self.assertEqual(run._status, 'failed')
|
||||
self.assertEqual(len(allocations), start)
|
||||
target.side_effect = None
|
||||
|
||||
@@ -54,20 +54,21 @@ 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.build_module.assert_not_called()
|
||||
module.compiler.compile_kernel.assert_not_called()
|
||||
|
||||
def test_switch_upload_only_then_raw_remap_keeps_live_velocity(self):
|
||||
run,module,allocations,storage,objects=ready();clone=None
|
||||
try:
|
||||
boundary=run._boundaries[2]
|
||||
boundary=run._level_boundaries[2]
|
||||
before=run._download_raw()
|
||||
original=boundary.tables['surface_velocity'].copy()
|
||||
module.cuda.memcpy_htod.reset_mock()
|
||||
@@ -77,13 +78,13 @@ class ShiftedJets(unittest.TestCase):
|
||||
np.testing.assert_array_equal(boundary.tables['surface_velocity'],-original)
|
||||
self.assertEqual(run.step_count,7)
|
||||
for a,b in zip(before.values,run._download_raw().values):self.assertEqual(a.tobytes(),b.tobytes())
|
||||
clone=run.remap_finest(B,objects=objects)
|
||||
self.assertEqual(clone.jets[2][0].amplitude,-jet().amplitude)
|
||||
np.testing.assert_array_equal(clone._boundaries[2].tables['surface_velocity'],
|
||||
clone=run.remap_child(B,child_objects=objects)
|
||||
self.assertEqual(clone._level_jets[2][0].amplitude,-jet().amplitude)
|
||||
np.testing.assert_array_equal(clone._level_boundaries[2].tables['surface_velocity'],
|
||||
boundary.tables['surface_velocity'])
|
||||
self.assertEqual(clone.step_count,7)
|
||||
saved=run._download_raw()
|
||||
with self.assertRaises(ValueError):run.remap_finest(B,objects=(body(center=(12.1,14.)),))
|
||||
with self.assertRaises(ValueError):run.remap_child(B,child_objects=(body(center=(12.1,14.)),))
|
||||
for a,b in zip(saved.values,run._download_raw().values):self.assertEqual(a.tobytes(),b.tobytes())
|
||||
finally:
|
||||
if clone is not None:clone.close()
|
||||
@@ -92,12 +93,12 @@ class ShiftedJets(unittest.TestCase):
|
||||
def test_invalid_amplitude_does_not_upload_or_commit(self):
|
||||
run,module,_,_,_=ready()
|
||||
try:
|
||||
before=run._boundaries[2].tables['surface_velocity'].copy()
|
||||
before=run._level_boundaries[2].tables['surface_velocity'].copy()
|
||||
module.cuda.memcpy_htod.reset_mock()
|
||||
for amplitude in (float('nan'),float('inf'),1.):
|
||||
with self.assertRaises(ValueError):run.set_jet_amplitude(0,amplitude,level=2)
|
||||
module.cuda.memcpy_htod.assert_not_called()
|
||||
np.testing.assert_array_equal(before,run._boundaries[2].tables['surface_velocity'])
|
||||
np.testing.assert_array_equal(before,run._level_boundaries[2].tables['surface_velocity'])
|
||||
finally:run.close()
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""CPU host contracts; mocked CUDA executes real lifecycle, not numerical kernels."""
|
||||
import ast
|
||||
import types
|
||||
from pathlib import Path
|
||||
from contextlib import nullcontext
|
||||
from dataclasses import replace
|
||||
import unittest
|
||||
@@ -75,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')
|
||||
@@ -95,7 +96,7 @@ class ShiftedStorage(unittest.TestCase):
|
||||
before = run._download_raw()
|
||||
expected = remap(topology(A),topology(B),before.values)
|
||||
with patch.object(run,'download',side_effect=AssertionError('public decode used')):
|
||||
clone = run.remap_finest(B)
|
||||
clone = run.remap_child(B)
|
||||
self.assertEqual((run.step_count,clone.step_count),(7,7))
|
||||
self.assertFalse(clone._exchange_valid)
|
||||
for level, want, got in zip(clone._levels,expected.g,clone._download_raw().values):
|
||||
@@ -114,7 +115,7 @@ class ShiftedStorage(unittest.TestCase):
|
||||
physical = tuple(np.broadcast_to(_WEIGHTS, level.shape).copy() for level in run._levels)
|
||||
for a in physical: a[1] += 2**-35
|
||||
run.initialize(*physical)
|
||||
for a,mask in zip(run._download_raw().values,run.leaf):
|
||||
for a,mask in zip(run._download_raw().values,run._leaf):
|
||||
self.assertTrue((a[1,mask] == np.float32(2**-35)).all())
|
||||
self.assertEqual(run.step_count,0)
|
||||
module.cuda.memcpy_htod.reset_mock()
|
||||
@@ -125,24 +126,33 @@ class ShiftedStorage(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError,'mixed'): run._download_raw()
|
||||
finally: run.close()
|
||||
|
||||
def test_missing_tiles_rejected_before_compilation(self):
|
||||
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,'must be a tile'):
|
||||
module.FixedPatchStepper(cfg,context=None,refinements=[(4,4,20,28)],
|
||||
level_blocks=blocks)
|
||||
module.compiler.build_module.assert_not_called()
|
||||
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):
|
||||
module = backend.host_module() # single blocked level: FixedPatchStepper(refinements=[])
|
||||
cuda, compiler = module.cuda, module.compiler
|
||||
path = Path(backend.r.__file__).with_name('block_stepper.py')
|
||||
tree = ast.parse(path.read_text())
|
||||
tree.body = [n for n in tree.body if not (
|
||||
isinstance(n, ast.Import) and any(a.name == 'pycuda.driver' for a in n.names)
|
||||
or isinstance(n, ast.ImportFrom) and n.module == 'cuda')]
|
||||
module = types.ModuleType('CelerisLab.lbm._test_block_host')
|
||||
module.__package__ = 'CelerisLab.lbm'
|
||||
cuda, compiler = Mock(), Mock()
|
||||
module.__dict__.update(cuda=cuda,compiler=compiler)
|
||||
exec(compile(tree,str(path),'exec'),module.__dict__)
|
||||
cuda.mem_get_info.return_value=(10**9,10**9)
|
||||
cuda.mem_alloc.side_effect=lambda size: Mock()
|
||||
gpu=Mock();gpu.get_global.return_value=(Mock(),20)
|
||||
compiler.build_module.return_value=gpu
|
||||
compiler.load_module.return_value=gpu
|
||||
context=Mock(activate=nullcontext,sm_arch='sm_70')
|
||||
layout=module.BlockLayout2D.build(16,16,4,4)
|
||||
cfg=replace(backend.CFG,nx=16,ny=16,ddf_shifting=True)
|
||||
run=module.FixedPatchStepper(cfg,context=context,refinements=[],level_blocks=[(4,4)])
|
||||
run=module.PeriodicBlockStepper(cfg,layout,context=context)
|
||||
storage={}
|
||||
cuda.memcpy_htod.side_effect=lambda p,a: storage.__setitem__(id(p),np.array(a,copy=True))
|
||||
cuda.memcpy_dtoh.side_effect=lambda a,p: np.copyto(a,storage[id(p)])
|
||||
@@ -153,8 +163,7 @@ class ShiftedStorage(unittest.TestCase):
|
||||
self.assertTrue(raw.shifted)
|
||||
self.assertEqual(len(raw.values),1)
|
||||
self.assertTrue((raw.values[0][1]==np.float32(2**-35)).all())
|
||||
(down,)=run.download()
|
||||
np.testing.assert_array_equal(down,physical)
|
||||
np.testing.assert_array_equal(run.download(),physical)
|
||||
finally: run.close()
|
||||
|
||||
def test_passive_shifted_body_raw_remap_preserves_support(self):
|
||||
|
||||
@@ -36,10 +36,12 @@ def make_simulation(monkeypatch):
|
||||
sim = module.Simulation.__new__(module.Simulation)
|
||||
sim.ctx = Owner()
|
||||
sim._closed = False
|
||||
sim._build_dir = "private-build"
|
||||
for name in ("stepper", "bodies", "field", "_module", "_stream"):
|
||||
setattr(sim, name, object())
|
||||
monkeypatch.setattr(module, "cuda", SimpleNamespace(
|
||||
Context=SimpleNamespace(synchronize=lambda: None)))
|
||||
monkeypatch.setattr(module.compiler, "cleanup_build_dir", lambda path: None)
|
||||
return sim
|
||||
|
||||
|
||||
@@ -55,7 +57,7 @@ def test_close_inside_activation_preserves_resources(monkeypatch):
|
||||
assert sim._closed and sim.ctx.is_closed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("failure", ["synchronize", "detach"])
|
||||
@pytest.mark.parametrize("failure", ["synchronize", "detach", "directory"])
|
||||
def test_close_failure_is_retryable(monkeypatch, failure):
|
||||
sim = make_simulation(monkeypatch)
|
||||
if failure == "detach":
|
||||
@@ -66,12 +68,17 @@ def test_close_failure_is_retryable(monkeypatch, failure):
|
||||
calls.append(args)
|
||||
if len(calls) == 1:
|
||||
raise RuntimeError(failure + " failed")
|
||||
monkeypatch.setattr(module.cuda.Context, "synchronize", fail_once)
|
||||
if failure == "synchronize":
|
||||
monkeypatch.setattr(module.cuda.Context, "synchronize", fail_once)
|
||||
else:
|
||||
monkeypatch.setattr(module.compiler, "cleanup_build_dir", fail_once)
|
||||
with pytest.raises(RuntimeError, match="failed"):
|
||||
sim.close()
|
||||
assert not sim._closed
|
||||
assert sim._build_dir == "private-build"
|
||||
sim.close()
|
||||
assert sim._closed and sim.ctx.is_closed
|
||||
assert sim._build_dir is None
|
||||
sim.close()
|
||||
|
||||
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
"""Host contract of FixedPatchStepper(refinements=[]): one blocked level (mocked driver).
|
||||
|
||||
Per step: halo refresh -> static BFL (circles only) -> BlockStep with a null
|
||||
kind, one stream wait per step(n) call, NaN spare after initialize. An upload,
|
||||
launch, wait or download failure fails the instance and keeps its step count;
|
||||
close retains resources for retry when context activation fails.
|
||||
"""
|
||||
from contextlib import nullcontext
|
||||
from unittest.mock import MagicMock, Mock
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||
from CelerisLab.body.objects import SimObject
|
||||
from CelerisLab.config import LBMConfig
|
||||
from tests.unit.test_refinement_three_level_multipatch import host_module
|
||||
|
||||
|
||||
def circle(center=(15.5, 11.5), radius=4.3):
|
||||
return SimObject(0, geometry=CircleGeometry(*center, radius), center=center, radius=radius)
|
||||
|
||||
|
||||
def backend(objects=(), failing_kernel=None, initialize=True):
|
||||
host, log = host_module(), []
|
||||
host.armed = False # set True to make failing_kernel raise
|
||||
|
||||
def launch(name):
|
||||
def run(*a, **k):
|
||||
log.append((name, a))
|
||||
if name == failing_kernel and host.armed:
|
||||
raise RuntimeError(name + " launch failed")
|
||||
return run
|
||||
|
||||
module = Mock()
|
||||
module.get_global.return_value = (Mock(), 20)
|
||||
module.get_function.side_effect = lambda name: Mock(side_effect=launch(name))
|
||||
host.compiler.build_module.return_value = module
|
||||
host.cuda.mem_alloc.side_effect = lambda size: Mock(size=size)
|
||||
host.cuda.memcpy_htod.side_effect = lambda pointer, array: log.append(("upload", (pointer, array)))
|
||||
run = host.FixedPatchStepper(LBMConfig(nx=32, ny=24, viscosity=.1),
|
||||
context=Mock(activate=nullcontext, sm_arch="sm_70"),
|
||||
refinements=[], level_blocks=[(12, 16)], level_objects=[objects])
|
||||
run._stream.synchronize.side_effect = lambda: log.append(("sync", ()))
|
||||
log.clear()
|
||||
if initialize:
|
||||
run.initialize(np.full(run._levels[0].shape, 1 / 9, np.float32))
|
||||
return run, log, host
|
||||
|
||||
|
||||
def state():
|
||||
return np.full((9, 24, 32), 1 / 9, np.float32)
|
||||
|
||||
|
||||
class SingleLevel(unittest.TestCase):
|
||||
def test_spare_starts_nan_and_current_holds_state(self):
|
||||
run, log, _ = backend()
|
||||
level = run._levels[0]
|
||||
uploads = {id(a[0]): a[1] for name, a in log if name == "upload"}
|
||||
self.assertTrue(np.isnan(uploads[id(level.spare)]).all())
|
||||
self.assertTrue(np.isfinite(level.layout.to_dense(uploads[id(level.current)])).all())
|
||||
|
||||
def test_launch_order_per_step(self):
|
||||
for objects, order in (((), ["BlockHaloCopy", "BlockStep"]),
|
||||
((circle(),), ["BlockHaloCopy", "BlockCurvedBoundary", "BlockStep"])):
|
||||
with self.subTest(bodies=len(objects)):
|
||||
run, log, _ = backend(objects)
|
||||
log.clear()
|
||||
run.step(2)
|
||||
self.assertEqual([name for name, _ in log], order * 2 + ["sync"])
|
||||
step = [a for name, a in log if name == "BlockStep"][0]
|
||||
self.assertEqual(int(step[4]), 0) # null kind: every core cell ACTIVE
|
||||
|
||||
def test_circle_solids_are_not_state(self):
|
||||
run, _, _ = backend((circle(),))
|
||||
solid = run._boundaries[0].tables["solid"]
|
||||
self.assertTrue(solid.any())
|
||||
np.testing.assert_array_equal(run._state[0], ~solid)
|
||||
|
||||
def test_initialize_upload_failure_fails_instance(self):
|
||||
run, _, host = backend(initialize=False)
|
||||
host.cuda.memcpy_htod.side_effect = RuntimeError("upload failed")
|
||||
with self.assertRaisesRegex(RuntimeError, "upload failed"):
|
||||
run.initialize(state())
|
||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||
run.step(1)
|
||||
|
||||
def test_launch_failure_fails_instance_and_keeps_step_count(self):
|
||||
for kernel, objects in (("BlockHaloCopy", ()), ("BlockCurvedBoundary", (circle(),)),
|
||||
("BlockStep", ())):
|
||||
with self.subTest(kernel=kernel):
|
||||
run, log, host = backend(objects, failing_kernel=kernel)
|
||||
run.step(1)
|
||||
host.armed = True
|
||||
log.clear()
|
||||
with self.assertRaisesRegex(RuntimeError, kernel + " launch failed"):
|
||||
run.step(2)
|
||||
self.assertEqual(run.step_count, 1)
|
||||
self.assertNotIn("sync", [name for name, _ in log])
|
||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||
run.download()
|
||||
|
||||
def test_free_memory_preflight_before_stream_and_buffers(self):
|
||||
for cfg, options in ((LBMConfig(nx=32, ny=24, viscosity=.1),
|
||||
dict(refinements=[], level_blocks=[(12, 16)])),
|
||||
(LBMConfig(nx=48, ny=48, viscosity=.02), # every level counts
|
||||
dict(refinements=[[(6, 6, 26, 26)]], level_blocks=[(4, 4)] * 2))):
|
||||
with self.subTest(depth=len(options["refinements"]) + 1):
|
||||
host = host_module()
|
||||
host.cuda.mem_get_info.return_value = (1024, 1 << 30)
|
||||
with self.assertRaisesRegex(MemoryError, "exceed free device memory"):
|
||||
host.FixedPatchStepper(cfg, context=Mock(activate=nullcontext, sm_arch="sm_70"),
|
||||
**options)
|
||||
host.cuda.Stream.assert_not_called()
|
||||
host.cuda.mem_alloc.assert_not_called()
|
||||
host.compiler.build_module.assert_not_called()
|
||||
|
||||
def test_wait_failure_after_a_good_step_keeps_its_count(self):
|
||||
run, _, _ = 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_final_wait_failure_fails_instance_and_keeps_step_count(self):
|
||||
run, _, _ = backend()
|
||||
run._stream.synchronize.side_effect = RuntimeError("device error")
|
||||
with self.assertRaisesRegex(RuntimeError, "device error"):
|
||||
run.step(3)
|
||||
self.assertEqual(run.step_count, 0)
|
||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||
run.step(1)
|
||||
|
||||
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)
|
||||
|
||||
def roles(run, log): # same launch order and buffer roles as three step(1) calls
|
||||
level = run._levels[0]
|
||||
tags = {id(level.current): "c", id(level.spare): "s"}
|
||||
return [(n, tuple(tags.get(id(x), "-") for x in a)) for n, a in log if n != "sync"]
|
||||
self.assertEqual(roles(many, log_many), roles(single, log_single))
|
||||
self.assertIsNot(many._levels[0].current, many._levels[0].spare)
|
||||
self.assertEqual((many.step_count, single.step_count), (3, 3))
|
||||
|
||||
def test_step_zero_launches_and_waits_for_nothing(self):
|
||||
run, log, _ = backend()
|
||||
log.clear()
|
||||
run.step(0)
|
||||
self.assertEqual((log, run.step_count), ([], 0))
|
||||
|
||||
def test_launch_failure_mid_batch_fails_without_waiting(self):
|
||||
run, log, host = backend()
|
||||
step = run._levels[0].kernel
|
||||
calls = []
|
||||
|
||||
def failing(*args, **kwargs):
|
||||
calls.append(1)
|
||||
if len(calls) == 2:
|
||||
raise RuntimeError("launch failure")
|
||||
return step(*args, **kwargs)
|
||||
|
||||
run._levels[0].kernel = failing
|
||||
log.clear()
|
||||
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()
|
||||
|
||||
def test_initialize_rejects_bad_state_before_upload(self):
|
||||
run, log, _ = backend(initialize=False)
|
||||
good = state()
|
||||
for bad in (good[:, :-1], np.where(np.arange(32) == 3, np.nan, good), -good):
|
||||
with self.assertRaises(ValueError):
|
||||
run.initialize(bad)
|
||||
self.assertNotIn("upload", [name for name, _ in log])
|
||||
run.initialize(good)
|
||||
self.assertEqual([name for name, _ in log].count("upload"), 2)
|
||||
|
||||
def test_download_failure_fails_instance(self):
|
||||
run, _, host = backend()
|
||||
host.cuda.memcpy_dtoh.side_effect = RuntimeError("read failed")
|
||||
with self.assertRaisesRegex(RuntimeError, "read failed"):
|
||||
run.download()
|
||||
for operation in (run.download, run.step):
|
||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||
operation()
|
||||
|
||||
def test_activation_failure_retains_resources_for_retry(self):
|
||||
run, _, _ = backend()
|
||||
allocations, stream = run._allocations[:], run._stream
|
||||
unavailable = MagicMock()
|
||||
unavailable.__enter__.side_effect = RuntimeError("activation failed")
|
||||
run.ctx.activate = Mock(side_effect=[unavailable, nullcontext()])
|
||||
with self.assertRaisesRegex(RuntimeError, "activation failed"):
|
||||
run.close()
|
||||
self.assertEqual(run._status, "failed")
|
||||
self.assertEqual(run._allocations, allocations)
|
||||
self.assertIs(run._stream, stream)
|
||||
run.close()
|
||||
self.assertEqual(run._status, "closed")
|
||||
self.assertEqual(run._levels, [])
|
||||
for allocation in allocations:
|
||||
allocation.free.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,99 +0,0 @@
|
||||
"""Direct-current-header regression; historical candidate manifests stay frozen."""
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPERS = ROOT / "tests/validation/les"
|
||||
sys.path.insert(0, str(HELPERS))
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"smagorinsky_oracle", HELPERS / "test_candidate.py"
|
||||
)
|
||||
oracle = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(oracle)
|
||||
HEADER = Path("src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh")
|
||||
DESCRIPTORS = Path("src/CelerisLab/lbm/kernels/core/descriptors.cuh")
|
||||
|
||||
|
||||
class SmagorinskyNormalization(unittest.TestCase):
|
||||
def test_current_header_matches_independent_tensor_oracle(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
build_dir = Path(directory)
|
||||
for dim in (2, 3):
|
||||
for cs in (0.0, 0.16):
|
||||
# Compile directly against this checkout, not saved candidate headers.
|
||||
closed = oracle.build(
|
||||
ROOT.parent, ROOT.name, dim, cs, 1, 3, build_dir
|
||||
)
|
||||
converged = oracle.build(
|
||||
ROOT.parent, ROOT.name, dim, cs, 0, 200, build_dir
|
||||
)
|
||||
for rho in (0.7, 1.0, 1.3):
|
||||
for tau0 in (0.56, 0.8):
|
||||
for kind, magnitude in (
|
||||
("equilibrium", 0.0),
|
||||
("shear", -0.02),
|
||||
("shear", 0.02),
|
||||
("extension", -0.02),
|
||||
("extension", 0.02),
|
||||
):
|
||||
stress = oracle.tensor(
|
||||
dim, kind, magnitude * rho, 0, dim - 1
|
||||
)
|
||||
f, eq = oracle.state(dim, rho, stress)
|
||||
expected = 1 / oracle.standard_tau(
|
||||
tau0, cs, oracle.norm(stress), rho
|
||||
)
|
||||
with self.subTest(
|
||||
dim=dim,
|
||||
cs=cs,
|
||||
rho=rho,
|
||||
tau=tau0,
|
||||
kind=kind,
|
||||
sign=magnitude,
|
||||
):
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, rho, tau0), expected, delta=3e-6
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
converged(f, eq, rho, tau0),
|
||||
expected,
|
||||
delta=3e-6,
|
||||
)
|
||||
if cs == 0:
|
||||
f, eq = oracle.state(
|
||||
dim, 1.0, oracle.tensor(dim, "equilibrium", 0.0)
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, 1.0, 0.50001), 1.99, delta=3e-6
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, 1.0, 200.0), 0.01, delta=3e-6
|
||||
)
|
||||
|
||||
def test_removing_sqrt_two_is_detected(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for relative in (HEADER, DESCRIPTORS):
|
||||
target = root / "old_convention" / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / relative, target)
|
||||
target = root / "old_convention" / HEADER
|
||||
text = target.read_text()
|
||||
factor = "18.0f * sqrtf(2.0f) * smag_const_sq"
|
||||
self.assertEqual(text.count(factor), 2)
|
||||
target.write_text(text.replace(factor, "18.0f * smag_const_sq"))
|
||||
for dim in (2, 3):
|
||||
old = oracle.build(root, "old_convention", dim, 0.16, 1, 3, root)
|
||||
stress = oracle.tensor(dim, "shear", 0.02)
|
||||
f, eq = oracle.state(dim, 1.0, stress)
|
||||
expected = 1 / oracle.standard_tau(0.8, 0.16, oracle.norm(stress), 1.0)
|
||||
self.assertGreater(abs(old(f, eq, 1.0, 0.8) - expected), 1e-3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user