Compare commits
95
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9aa12e65d | ||
|
|
2b10b01c49 | ||
|
|
e18f7abe2b | ||
|
|
104f74632f | ||
|
|
452eaf32de | ||
|
|
3716b16555 | ||
|
|
ce393fa468 | ||
|
|
2ccd7d576e | ||
|
|
2469af70d0 | ||
|
|
850b66862f | ||
|
|
fd8f1da9f5 | ||
|
|
1573942673 | ||
|
|
8db7cee9d5 | ||
|
|
c6d5dc1cbc | ||
|
|
f520af8935 | ||
|
|
19db7d362f | ||
|
|
8c776309c9 | ||
|
|
8e1feffcad | ||
|
|
d58f06f0fa | ||
|
|
1318fc205c | ||
|
|
985db14249 | ||
|
|
f764d68c81 | ||
|
|
a0700ab370 | ||
|
|
36ad523662 | ||
|
|
d318911221 | ||
|
|
18e8ef45f0 | ||
|
|
21fe71d332 | ||
|
|
2b3c2a7529 | ||
|
|
dbcdb87081 | ||
|
|
0d62c3cfb8 | ||
|
|
96c5decf69 | ||
|
|
e4049a6183 | ||
|
|
733e4d1fe7 | ||
|
|
0f8fdbc30e | ||
|
|
e4538219b2 | ||
|
|
888c80381c | ||
|
|
bf5088f84c | ||
|
|
af406b44bf | ||
|
|
78c0f1811b | ||
|
|
0ddc6c3d62 | ||
|
|
43e226c59a | ||
|
|
93e692a40f | ||
|
|
33837697da | ||
|
|
b47af12c9d | ||
|
|
a3df5855a2 | ||
|
|
11e96a188b | ||
|
|
3a374faace | ||
|
|
fce24a64e8 | ||
|
|
d437faa501 | ||
|
|
26e49451ec | ||
|
|
e214480452 | ||
|
|
c414f6c7f6 | ||
|
|
decd419e94 | ||
|
|
1c47cb8570 | ||
|
|
ee7074a53d | ||
|
|
b97fc79af9 | ||
|
|
8e9fc72e19 | ||
|
|
d2cd1114ed | ||
|
|
c95a7baae4 | ||
|
|
f4372a84c0 | ||
|
|
e48498d089 | ||
|
|
531739454e | ||
|
|
b93de2ce24 | ||
|
|
a454f02355 | ||
|
|
580fcf6525 | ||
|
|
18f018befe | ||
|
|
ec9b3d721c | ||
|
|
84b76f49cc | ||
|
|
050d12b76c | ||
|
|
ec3e3b928a | ||
|
|
2be80324ad | ||
|
|
0fcd4ac4cb | ||
|
|
af0566db98 | ||
|
|
0b1b3d938f | ||
|
|
5167c79378 | ||
|
|
aedddc35b1 | ||
|
|
1cbdc479cf | ||
|
|
7821c6ad71 | ||
|
|
392314e1ae | ||
|
|
f3f1e3d588 | ||
|
|
3648ad70ef | ||
|
|
5399c9c0a6 | ||
|
|
54be33e006 | ||
|
|
1fa498c7d7 | ||
|
|
781333407e | ||
|
|
750e8a483e | ||
|
|
6a03f463bc | ||
|
|
46dc6ba086 | ||
|
|
e1efc18274 | ||
|
|
9660894a1b | ||
|
|
ba3772e3b8 | ||
|
|
b4692e6ec4 | ||
|
|
5731c5171b | ||
|
|
05de02ee60 | ||
|
|
87c3a3165e |
@@ -112,8 +112,9 @@ from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
|||||||
cfg = LBMConfig(nx=32, ny=24, viscosity=.1)
|
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,
|
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
|
surface_actuation=SurfaceActuation(0.7, 0.85, 0.002)) # angle, half-width, speed
|
||||||
with CudaContext(0) as ctx, FixedPatchStepper(cfg, (8, 6, 24, 18), context=ctx,
|
with CudaContext(0) as ctx, FixedPatchStepper(cfg, context=ctx, refinements=[(8, 6, 24, 18)],
|
||||||
fine_objects=[body], fine_block=(16, 20)) as run:
|
level_blocks=[None, (16, 20)],
|
||||||
|
level_objects=[(), (body,)]) as run:
|
||||||
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
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.
|
# (9, ny, nx) postcollision populations per level; non-leaf cells are discarded.
|
||||||
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
|
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
|
||||||
@@ -124,8 +125,8 @@ with CudaContext(0) as ctx, FixedPatchStepper(cfg, (8, 6, 24, 18), context=ctx,
|
|||||||
|
|
||||||
Scope and limits:
|
Scope and limits:
|
||||||
- The examples below use D2Q9, SRT, FP32, double buffer, unshifted, no LES or
|
- The examples below use D2Q9, SRT, FP32, double buffer, unshifted, no LES or
|
||||||
forcing. Fixed 2:1 hierarchies can use level lists of arbitrary depth; the
|
forcing. Fixed 2:1 hierarchies use level lists of arbitrary depth; two- and
|
||||||
legacy two/three-level forms are shown below.
|
three-level examples below use that same constructor.
|
||||||
- `coarse_boundary="periodic"` (default) or `"slot_reservoir"` (bottom slot, top
|
- `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.
|
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)`),
|
- Object centers/radii use the coarse **cell-edge** frame (cell `I` spans `[I, I+1)`),
|
||||||
@@ -133,50 +134,51 @@ Scope and limits:
|
|||||||
- Bodies must be stationary circles whose whole boundary stencil lies in the fine
|
- 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
|
active region, clear of the coarse/fine transfer cells; violations are rejected
|
||||||
before any GPU allocation.
|
before any GPU allocation.
|
||||||
- `fine_block=(by, bx)` stores the fine level in fixed-size blocks; `by`/`bx` must
|
- `level_blocks=[None, (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
|
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
|
four-cell ghost band. It gives results identical to dense storage and is
|
||||||
infrastructure for future block AMR, not a speedup.
|
infrastructure for future block AMR, not a speedup.
|
||||||
- Several regions: pass a sequence of `(x0, y0, x1, y1)` rectangles as
|
- Several regions: pass a sequence of `(x0, y0, x1, y1)` rectangles as
|
||||||
`bounds`, with both `fine_block` and `base_block`. Transfers are built once
|
`refinements[0]`, with both levels blocked (`level_blocks`). Transfers are built once
|
||||||
from their union; adjacent, overlapping and concave unions are accepted when
|
from their union; adjacent, overlapping and concave unions are accepted when
|
||||||
their actual dependencies close. There is no fixed rectangle-gap rule.
|
their actual dependencies close. There is no fixed rectangle-gap rule.
|
||||||
The fine level is then one level-wide grid `(2*ny, 2*nx)`:
|
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
|
`run.patch.fine_shape` gives it, `initialize`/`download` use it, and only the
|
||||||
grid-aligned tiles intersecting the union plus its four-cell ghost band are stored.
|
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
|
Block sizes must divide the level-wide grid; rectangle edges need not align
|
||||||
with tiles. Order of rectangles does not matter. `fine_objects`/`jets` work
|
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
|
as for one patch: each circle (same coarse cell-edge frame) must lie wholly
|
||||||
inside the union's active region with
|
inside the union's active region with
|
||||||
its boundary stencil clear of transfer cells; a jet may not select links of two
|
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.
|
different bodies, even across regions. Wall-touching patches remain unsupported.
|
||||||
For nesting, use the level lists or legacy child forms below.
|
For nesting, add further `refinements` entries as shown below.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
cfg = LBMConfig(nx=32, ny=16, viscosity=.1)
|
cfg = LBMConfig(nx=32, ny=16, viscosity=.1)
|
||||||
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
||||||
with CudaContext(0) as ctx, FixedPatchStepper(
|
with CudaContext(0) as ctx, FixedPatchStepper(
|
||||||
cfg, ((4, 4, 12, 12), (16, 4, 24, 12)), context=ctx,
|
cfg, context=ctx, refinements=[((4, 4, 12, 12), (16, 4, 24, 12))],
|
||||||
fine_block=(4, 4), base_block=(4, 4)) as run:
|
level_blocks=[(4, 4), (4, 4)]) as run:
|
||||||
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
|
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
|
||||||
np.broadcast_to(rest, (9, *run.patch.fine_shape))) # (9, 32, 64)
|
np.broadcast_to(rest, (9, *run.patch.fine_shape))) # (9, 32, 64)
|
||||||
run.step(32)
|
run.step(32)
|
||||||
coarse_g, fine_g = run.download() # coarse: NaN on every covered cell;
|
coarse_g, fine_g = run.download() # coarse: NaN on every covered cell;
|
||||||
# fine: NaN on every non-ACTIVE cell
|
# fine: NaN on every non-ACTIVE cell
|
||||||
```
|
```
|
||||||
- Legacy three-level scalar chain: add `child=(x0, y0, x1, y1)` in
|
- Three-level nested windows: pass two scalar rectangles in `refinements` —
|
||||||
**L1 window cells** (the patch's fine grid including its 4-cell ghost band) and
|
L1 in L0 cells, L2 in **L1 window cells** (the parent's fine grid including its
|
||||||
`child_block`, with `fine_block` and `base_block`. L2 must sit inside L1's
|
4-cell ghost band) — plus all three `level_blocks` entries. L2 must sit inside
|
||||||
ACTIVE cells, away from L1's ghost band and transfer footprint (checked before
|
L1's ACTIVE cells, away from L1's ghost band and transfer footprint (checked
|
||||||
any GPU allocation). `initialize` and `download` take/return three arrays; each
|
before any GPU allocation). `initialize` and `download` take/return three
|
||||||
level is NaN on its non-ACTIVE (and child-covered) cells. One coarse step = 2 L1
|
arrays; each level is NaN on its non-ACTIVE (and child-covered) cells. One
|
||||||
steps = 4 L2 steps. One L1 patch only. Optional stationary circles and Jets
|
coarse step = 2 L1 steps = 4 L2 steps. One L1 window only in this scalar
|
||||||
live on L2 via `child_objects` and `child_jets`; `fine_objects`/`jets` retain
|
chain. Optional stationary circles and Jets live on L2 via `level_objects[2]`
|
||||||
their L1 meaning and must be empty in this mode. All object and Jet geometry
|
and `level_jets[2]`; L1 bodies/Jets retain their L1 meaning and must be empty
|
||||||
uses the **L0 edge frame**, not the child bounds' L1 window frame. L2 has
|
in this mode. All object and Jet geometry uses the **L0 edge frame**, not the
|
||||||
dx=dt=0.25, so prescribed lattice velocity is unchanged. Each whole body and
|
child bounds' L1 window frame. L2 has dx=dt=0.25, so prescribed lattice
|
||||||
its BFL stencil must be inside L2 ACTIVE cells, outside the transfer footprint.
|
velocity is unchanged. Each whole body and its BFL stencil must be inside L2
|
||||||
A Jet may select one physical body across multiple blocks, never two bodies.
|
ACTIVE cells, outside the transfer footprint. A Jet may select one physical
|
||||||
|
body across multiple blocks, never two bodies.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||||
@@ -188,9 +190,9 @@ Scope and limits:
|
|||||||
jet = Jet(Tube((6.5, 8.), 0., 3., .3), Parallel(0.), amplitude=.002)
|
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]
|
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
||||||
with CudaContext(0) as ctx, FixedPatchStepper(
|
with CudaContext(0) as ctx, FixedPatchStepper(
|
||||||
cfg, (4, 4, 12, 12), context=ctx, fine_block=(4, 4), base_block=(4, 4),
|
cfg, context=ctx, refinements=[(4, 4, 12, 12), (8, 8, 16, 16)],
|
||||||
child=(8, 8, 16, 16), child_block=(4, 4),
|
level_blocks=[(4, 4)] * 3, level_objects=[(), (), (body,)],
|
||||||
child_objects=[body], child_jets=[jet]) as run:
|
level_jets=[(), (), (jet,)]) as run:
|
||||||
shapes = (run.patch.coarse_shape, run.patch.fine_shape, run.child_patch.fine_shape)
|
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.initialize(*(np.broadcast_to(rest, (9, *s)) for s in shapes)) # 16², 24², 24²
|
||||||
run.step(4)
|
run.step(4)
|
||||||
@@ -198,16 +200,16 @@ Scope and limits:
|
|||||||
run.step(4)
|
run.step(4)
|
||||||
coarse_g, l1_g, l2_g = run.download() # solids/covered/nonleaves are NaN
|
coarse_g, l1_g, l2_g = run.download() # solids/covered/nonleaves are NaN
|
||||||
```
|
```
|
||||||
- Level lists: `refinements[l]` describes level `l+1` in allocated level-`l`
|
- Level lists (the only constructor): `refinements[l]` describes level `l+1`
|
||||||
integer cells; use a rectangle list for a union, or a scalar rectangle for a
|
in allocated level-`l` integer cells; use a rectangle list for a union, or a
|
||||||
single window (a scalar child requires a scalar parent). `level_blocks`,
|
scalar rectangle for a single window (a scalar child requires a scalar
|
||||||
`level_objects` and `level_jets` each have one entry per level, including L0.
|
parent). `level_blocks`, `level_objects` and `level_jets` each have one entry
|
||||||
These lists are mutually exclusive with the legacy patch keywords. Three or
|
per level, including L0. Three or more levels require all-blocked storage and
|
||||||
more levels require all-blocked storage and bodies/Jets only on the finest
|
bodies/Jets only on the finest level. Every adjacent pair must preserve
|
||||||
level. Every adjacent pair must preserve inherited ACTIVE coverage, valid
|
inherited ACTIVE coverage, valid donors/receivers, surviving pulls and the
|
||||||
donors/receivers, surviving pulls and the upstream transfer footprint.
|
upstream transfer footprint. Level `l` takes `2**l` steps per base interval.
|
||||||
Level `l` takes `2**l` steps per base interval. This describes implemented
|
This describes implemented topology and scheduling, not accuracy or
|
||||||
topology and scheduling, not accuracy or convergence qualification.
|
convergence qualification.
|
||||||
- Blocked body levels can enable `observe_raw_exchange=True` and call
|
- Blocked body levels can enable `observe_raw_exchange=True` and call
|
||||||
`read_raw_exchange(body_id, level=...)` after a completed base interval.
|
`read_raw_exchange(body_id, level=...)` after a completed base interval.
|
||||||
It reduces unique cut-link observations into wall/aperture/total force,
|
It reduces unique cut-link observations into wall/aperture/total force,
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 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.
|
||||||
@@ -25,7 +25,7 @@ under the alternate compile branch.
|
|||||||
- No LES, no external forcing (`d_params` force fields stay zero).
|
- 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
|
- Shifted Jets stay SRT-only: `body/jets.py` keeps its existing guard. This extension does not
|
||||||
provide shifted MRT Jets.
|
provide shifted MRT Jets.
|
||||||
- `PeriodicBlockStepper` (uniform blocked stepper) remains SRT-only.
|
- `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
|
## Evidence status
|
||||||
CPU: `tests/unit/test_refinement_mrt.py` (host guards, source contract). GPU:
|
CPU: `tests/unit/test_refinement_mrt.py` (host guards, source contract). GPU:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Blocked AMR root channel increment
|
# Blocked AMR root channel increment
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
`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`:
|
`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`:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,393 @@
|
|||||||
|
# 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,8 +1,8 @@
|
|||||||
# Fixed three-level rectangle unions
|
# Fixed three-level rectangle unions
|
||||||
|
|
||||||
This page describes the legacy three-level entry point of `FixedPatchStepper`,
|
This page describes three-level `FixedPatchStepper` setups built from the
|
||||||
with rectangle unions on either adjacent pair. The `refinements` level-list
|
`refinements` level list, with rectangle unions on either adjacent pair. The
|
||||||
entry point also supports arbitrary depth; three or more levels require
|
same constructor supports arbitrary depth; three or more levels require
|
||||||
all-blocked storage and bodies/Jets only on the finest level. This is not
|
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
|
general AMR: there is no per-region timestep, moving geometry, automatic
|
||||||
dynamic refinement or L1 body support in the three-level form.
|
dynamic refinement or L1 body support in the three-level form.
|
||||||
@@ -12,15 +12,15 @@ No CUDA kernel changes are involved.
|
|||||||
|
|
||||||
## Explicit coordinate frames
|
## Explicit coordinate frames
|
||||||
|
|
||||||
Keep the existing scalar `child=(x0,y0,x1,y1)` for a single scalar parent patch.
|
For a single scalar parent patch, pass a scalar rectangle as `refinements[1]`:
|
||||||
Its coordinates retain their original meaning: half-open integer indices in the
|
`(x0,y0,x1,y1)` in half-open integer indices of the allocated L1 window,
|
||||||
allocated L1 window, including its ghost band.
|
including its ghost band.
|
||||||
|
|
||||||
The new keyword `child_rectangles_in_parent=[...]` is mutually exclusive with
|
A rectangle list as `refinements[1]` accepts a scalar or a set parent and
|
||||||
`child`. It accepts a scalar or a set parent and interprets every rectangle in
|
interprets every rectangle in the actual allocated L1 array, including ghost
|
||||||
the actual allocated L1 array, including ghost bands and holes. It never infers
|
bands and holes. It never infers region-local coordinates. The physical root
|
||||||
region-local coordinates. The physical root cell edge corresponding to an L1
|
cell edge corresponding to an L1 index `(x,y)` is
|
||||||
index `(x,y)` is `parent.fine_origin + (x,y)/2`:
|
`parent.fine_origin + (x,y)/2`:
|
||||||
|
|
||||||
- A scalar L1 parent has a shifted window origin.
|
- A scalar L1 parent has a shifted window origin.
|
||||||
- A set L1 parent has origin `(0,0)` and public shape `(2*ny,2*nx)`.
|
- A set L1 parent has origin `(0,0)` and public shape `(2*ny,2*nx)`.
|
||||||
@@ -47,9 +47,9 @@ tile edges.
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
run = FixedPatchStepper(
|
run = FixedPatchStepper(
|
||||||
cfg, [(4, 4, 20, 28), (24, 4, 44, 28)], context=ctx,
|
cfg, context=ctx,
|
||||||
base_block=(4, 4), fine_block=(4, 4), child_block=(4, 4),
|
refinements=[[(4, 4, 20, 28), (24, 4, 44, 28)], [(16, 16, 32, 40), (56, 16, 80, 40)]],
|
||||||
child_rectangles_in_parent=[(16, 16, 32, 40), (56, 16, 80, 40)],
|
level_blocks=[(4, 4)] * 3,
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ This example uses a 48x32 root. `initialize` and `download` use three Q-major
|
|||||||
dense arrays. Public arrays and masks still scale with the full allocated
|
dense arrays. Public arrays and masks still scale with the full allocated
|
||||||
extent; sparse device buffers do not remove that host-memory limit.
|
extent; sparse device buffers do not remove that host-memory limit.
|
||||||
|
|
||||||
Stationary whole-circle `child_objects` and `child_jets` use root edge
|
Stationary whole-circle L2 bodies and Jets (`level_objects[2]`, `level_jets[2]`) use root edge
|
||||||
coordinates and a single L2 boundary object. Body IDs are physical object-list
|
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
|
positions, not region IDs or caller `obj_id`. Complete boundary stencils must
|
||||||
avoid the transfer footprint. Change a child Jet with
|
avoid the transfer footprint. Change a child Jet with
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 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_child`), 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`). | Host copy commits only after the device upload succeeded; a device error fails the instance. |
|
||||||
|
| 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`).
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# 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_child`) | periodic three-level L2 sets only, no L0/L1 bodies, identical pinned boundary | `lbm/refinement_stepper.py` `FixedPatchStepper.remap_child` |
|
||||||
|
|
||||||
|
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_child`),
|
||||||
|
moving bodies on refined levels, pressure Jets, Jet checkpoints, LES or TRT on refined
|
||||||
|
levels, partial (non-dividing) tiles.
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# 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`.
|
||||||
@@ -56,3 +56,62 @@ Root must independently review and admit any execution. An admitted paired pilot
|
|||||||
the existing watchdog, device selection, `--gpu-gates-approved`, positive
|
the existing watchdog, device selection, `--gpu-gates-approved`, positive
|
||||||
`--max-wall-seconds` and a new output directory, adding only `--exchange-window` to the
|
`--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.
|
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.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""Public API demo: fixed 2:1 patch with a stationary, optionally actuated circle.
|
"""Public API demo: fixed 2:1 patch with a stationary, optionally actuated circle.
|
||||||
|
|
||||||
From this checkout, after checking the selected physical GPU is idle:
|
From this checkout, after checking the selected physical GPU is idle:
|
||||||
PYTHONPATH="$PWD/src" CUDA_VISIBLE_DEVICES=0 python examples/fixed_patch_circle.py --fine-block 16 20
|
PYTHONPATH="$PWD/src" CUDA_VISIBLE_DEVICES=0 python examples/fixed_patch_circle.py
|
||||||
... --speed -0.002 --fine-block 16 20 --mode slot_reservoir
|
... --speed -0.002 --mode slot_reservoir
|
||||||
Omit --fine-block for the (equivalent) dense fine storage.
|
Both levels use blocked storage (--base-block, --fine-block tiles).
|
||||||
|
|
||||||
A short smoke run (default 128 coarse steps): it shows setup, stepping and
|
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.
|
fluid-only diagnostics, not a developed flow, accuracy or long-time stability.
|
||||||
@@ -71,8 +71,10 @@ def main():
|
|||||||
parser.add_argument("--mode", choices=("periodic", "slot_reservoir"), default="periodic")
|
parser.add_argument("--mode", choices=("periodic", "slot_reservoir"), default="periodic")
|
||||||
parser.add_argument("--speed", type=float, default=.002,
|
parser.add_argument("--speed", type=float, default=.002,
|
||||||
help="normal surface velocity, coarse lattice units (+ blows, - sucks, 0 passive)")
|
help="normal surface velocity, coarse lattice units (+ blows, - sucks, 0 passive)")
|
||||||
parser.add_argument("--fine-block", type=int, nargs=2, metavar=("BY", "BX"),
|
parser.add_argument("--base-block", type=int, nargs=2, metavar=("BY", "BX"), default=(12, 8),
|
||||||
help="blocked fine storage; must divide the whole fine allocation (32 x 40 here)")
|
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("--steps", type=int, default=128)
|
parser.add_argument("--steps", type=int, default=128)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.steps < 0:
|
if args.steps < 0:
|
||||||
@@ -86,20 +88,20 @@ def main():
|
|||||||
body = SimObject(0, geometry=CircleGeometry(*CENTER, RADIUS), center=CENTER, radius=RADIUS,
|
body = SimObject(0, geometry=CircleGeometry(*CENTER, RADIUS), center=CENTER, radius=RADIUS,
|
||||||
surface_actuation=spec)
|
surface_actuation=spec)
|
||||||
cfg = config(args.mode)
|
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
|
# The caller owns the CUDA context; the stepper must be closed first (the
|
||||||
# nested with-statement does that).
|
# nested with-statement does that).
|
||||||
with CudaContext(0) as context, FixedPatchStepper(
|
with CudaContext(0) as context, FixedPatchStepper(
|
||||||
cfg, BOUNDS, context=context, coarse_boundary=args.mode,
|
cfg, context=context, coarse_boundary=args.mode, refinements=[BOUNDS],
|
||||||
fine_objects=[body], fine_block=fine_block) as run:
|
level_blocks=[tuple(args.base_block), tuple(args.fine_block)],
|
||||||
|
level_objects=[(), (body,)]) as run:
|
||||||
p = run.patch
|
p = run.patch
|
||||||
# Rest equilibrium is already postcollision G, so no initial collision is
|
# Rest equilibrium is already postcollision G, so no initial collision is
|
||||||
# needed. Non-leaf entries (covered, ghost, solid) are discarded by initialize.
|
# 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]
|
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)),
|
run.initialize(np.broadcast_to(rest, (9, *p.coarse_shape)),
|
||||||
np.broadcast_to(rest, (9, *p.fine_shape)))
|
np.broadcast_to(rest, (9, *p.fine_shape)))
|
||||||
storage = f"blocked {fine_block}" if fine_block else "dense"
|
storage = f"blocked root {tuple(args.base_block)} fine {tuple(args.fine_block)}"
|
||||||
print(f"mode={args.mode} fine storage={storage} speed={args.speed:+g} "
|
print(f"mode={args.mode} storage={storage} speed={args.speed:+g} "
|
||||||
f"(fine allocation {p.fine_shape[0]}x{p.fine_shape[1]}, fine origin {p.fine_origin})")
|
f"(fine allocation {p.fine_shape[0]}x{p.fine_shape[1]}, fine origin {p.fine_origin})")
|
||||||
masks = leaf_masks(run, args.mode)
|
masks = leaf_masks(run, args.mode)
|
||||||
start = leaf_diagnostics(*run.download(), masks)
|
start = leaf_diagnostics(*run.download(), masks)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from CelerisLab.lbm.refinement import ACTIVE
|
|||||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||||
|
|
||||||
BOUNDS, AMPLITUDE, WIDTH = (16, 4, 32, 28), .02, 12
|
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
|
TIMES, MAX_MACH, MAX_SECONDS = (0, 300, 1200), .1, 120
|
||||||
|
|
||||||
|
|
||||||
@@ -132,7 +133,8 @@ def main():
|
|||||||
lattice_model='D2Q9', collision='SRT', streaming='double_buffer',
|
lattice_model='D2Q9', collision='SRT', streaming='double_buffer',
|
||||||
store_precision='FP32', ddf_shifting=False, les_enabled=False)
|
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,
|
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'),
|
rho=1, tau_coarse=.8, tau_fine=1.1, coarse_boundary='slot_reservoir',
|
||||||
|
level_blocks=LEVEL_BLOCKS),
|
||||||
guards=dict(check_every=50, positive_density=True, finite=True,
|
guards=dict(check_every=50, positive_density=True, finite=True,
|
||||||
max_Mach=MAX_MACH, max_seconds=MAX_SECONDS,
|
max_Mach=MAX_MACH, max_seconds=MAX_SECONDS,
|
||||||
timing='cooperative checks, not an exact interruption deadline'),
|
timing='cooperative checks, not an exact interruption deadline'),
|
||||||
@@ -148,8 +150,9 @@ def main():
|
|||||||
samples=[], status='running', last_synchronized_coarse_step=0)
|
samples=[], status='running', last_synchronized_coarse_step=0)
|
||||||
start, run = time.monotonic(), None
|
start, run = time.monotonic(), None
|
||||||
try:
|
try:
|
||||||
with CudaContext(0) as context, FixedPatchStepper(cfg, BOUNDS, context=context,
|
with CudaContext(0) as context, FixedPatchStepper(cfg, context=context,
|
||||||
coarse_boundary='slot_reservoir') as run:
|
coarse_boundary='slot_reservoir', refinements=[BOUNDS],
|
||||||
|
level_blocks=LEVEL_BLOCKS) as run:
|
||||||
p = run.patch
|
p = run.patch
|
||||||
masks = [np.ones(p.coarse_shape, bool), p.fine_kind == ACTIVE]
|
masks = [np.ones(p.coarse_shape, bool), p.fine_kind == ACTIVE]
|
||||||
masks[0][[0, -1]] = False
|
masks[0][[0, -1]] = False
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from CelerisLab.config import load_lbm_config
|
|||||||
from CelerisLab.cuda.context import CudaContext
|
from CelerisLab.cuda.context import CudaContext
|
||||||
from CelerisLab.lbm.equilibrium import compute_feq_d2q9, compute_macro_from_ddf
|
from CelerisLab.lbm.equilibrium import compute_feq_d2q9, compute_macro_from_ddf
|
||||||
from CelerisLab.lbm.refinement import ACTIVE
|
from CelerisLab.lbm.refinement import ACTIVE
|
||||||
from CelerisLab.lbm.refinement_stepper import PeriodicPatchStepper
|
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||||
|
|
||||||
|
|
||||||
def shear_equilibrium(shape, origin, dx, period):
|
def shear_equilibrium(shape, origin, dx, period):
|
||||||
@@ -48,7 +48,8 @@ def main():
|
|||||||
nx=16, ny=16, viscosity=.1, velocity=1e-3, rho=1.0,
|
nx=16, ny=16, viscosity=.1, velocity=1e-3, rho=1.0,
|
||||||
lattice_model="D2Q9", collision="SRT", streaming="double_buffer",
|
lattice_model="D2Q9", collision="SRT", streaming="double_buffer",
|
||||||
store_precision="FP32", ddf_shifting=False, les_enabled=False)
|
store_precision="FP32", ddf_shifting=False, les_enabled=False)
|
||||||
with CudaContext(0) as context, PeriodicPatchStepper(cfg, (4, 4, 12, 12), context=context) as run:
|
with CudaContext(0) as context, FixedPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)],
|
||||||
|
level_blocks=[(16, 16), (24, 24)]) as run:
|
||||||
p = run.patch
|
p = run.patch
|
||||||
coarse = shear_equilibrium(p.coarse_shape, (0, 0), 1.0, cfg.ny)
|
coarse = shear_equilibrium(p.coarse_shape, (0, 0), 1.0, cfg.ny)
|
||||||
fine = shear_equilibrium(p.fine_shape, p.fine_origin, .5, cfg.ny)
|
fine = shear_equilibrium(p.fine_shape, p.fine_origin, .5, cfg.ny)
|
||||||
|
|||||||
@@ -207,22 +207,30 @@ def surface_link_velocity(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
|||||||
jets=(), hits=None, *, _shifted_blocked=False):
|
jets=(), hits=None, *, _shifted_blocked=False):
|
||||||
"""Per-link prescribed surface velocity (n, 2) FP32 and aperture mask.
|
"""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
|
Host-only and storage-neutral: rows follow the given physical cut-link
|
||||||
order. Returns (None, None) when neither an object carries surface
|
order. Returns all None when neither an object carries surface
|
||||||
actuation nor a jet is given. Jets need true FP64 ``hits`` (n, 2) and may
|
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.
|
not share a link with each other or with a legacy aperture.
|
||||||
Shifted storage is privately enabled only by the all-blocked stepper.
|
Shifted storage is privately enabled only by the all-blocked stepper.
|
||||||
"""
|
"""
|
||||||
enabled = [obj for obj in objects if obj.surface_actuation is not None]
|
enabled = [obj for obj in objects if obj.surface_actuation is not None]
|
||||||
if not enabled and not jets:
|
if not enabled and not jets:
|
||||||
return None, None
|
return None, None, None, None
|
||||||
from ..lbm.descriptors import FLUID, SOLID
|
from .preprocess.flag_overlay import validate_cut_link_ownership
|
||||||
if np.any((flags[fluid_idx] & FLUID) == 0) or np.any(flags[fluid_idx] & SOLID):
|
validate_cut_link_ownership(flags, fluid_idx, dir, cfg.nq)
|
||||||
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_velocity = np.zeros((len(dir), 2), np.float32)
|
||||||
surface_aperture = np.zeros(len(dir), bool)
|
surface_aperture = np.zeros(len(dir), bool)
|
||||||
|
shape = jet_id = None
|
||||||
for obj in enabled:
|
for obj in enabled:
|
||||||
obj.validate_surface_actuation(cfg)
|
obj.validate_surface_actuation(cfg)
|
||||||
selected = body_id == obj.obj_id
|
selected = body_id == obj.obj_id
|
||||||
@@ -255,4 +263,4 @@ def surface_link_velocity(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
|||||||
raise ValueError("Jet overlaps a SurfaceActuation aperture")
|
raise ValueError("Jet overlaps a SurfaceActuation aperture")
|
||||||
surface_velocity[driven] = jet_link_velocity(jets, shape, jet_id)[driven]
|
surface_velocity[driven] = jet_link_velocity(jets, shape, jet_id)[driven]
|
||||||
surface_aperture |= driven
|
surface_aperture |= driven
|
||||||
return surface_velocity, surface_aperture
|
return surface_velocity, surface_aperture, shape, jet_id
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Responsibilities:
|
|||||||
|
|
||||||
Design::
|
Design::
|
||||||
Packed ``obs`` layout matches ``generate_config(..., n_objects=count)`` and
|
Packed ``obs`` layout matches ``generate_config(..., n_objects=count)`` and
|
||||||
``config_obs.h`` macros. Host sizing uses :func:`~CelerisLab.cuda.compiler_v2.obs_layout`.
|
``config_obs.h`` macros. Host sizing uses :func:`~CelerisLab.cuda.compiler.obs_layout`.
|
||||||
|
|
||||||
Pending edits are lightweight: only the "intent" (add / remove) is stored.
|
Pending edits are lightweight: only the "intent" (add / remove) is stored.
|
||||||
All derived quantities (flags, compact lists, GPU buffers) are built at
|
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 .objects import SimObject
|
||||||
from .registry import BodyRegistry
|
from .registry import BodyRegistry
|
||||||
from .preprocess.flag_overlay import merge_flag_masks
|
from .preprocess.flag_overlay import merge_flag_masks, validate_cut_link_ownership
|
||||||
from ..cuda.compiler_v2 import obs_layout
|
from ..cuda.compiler import obs_layout
|
||||||
from ..cuda.obs_layout import ObsLayout
|
from ..cuda.obs_layout import ObsLayout
|
||||||
from ..lbm.descriptors import (
|
from ..lbm.descriptors import (
|
||||||
FLUID, SOLID,
|
FLUID, SOLID,
|
||||||
@@ -369,9 +369,9 @@ class ObjectManager:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
objects: List of SimObject instances to process.
|
objects: List of SimObject instances to process.
|
||||||
domain_flags: Optional uint16 flags for donor-cell classification.
|
domain_flags: Optional uint16 flags for receiver/donor classification.
|
||||||
sensor_cell_counts_out: Int array to fill with per-object sensor
|
sensor_cell_counts_out: Int array to fill with per-object sensor
|
||||||
cell counts. If provided, ``fill(0)`` is called first.
|
cell counts after the full geometry has passed validation.
|
||||||
hits_out: List that receives one FP64 (n, 2) array of true hit
|
hits_out: List that receives one FP64 (n, 2) array of true hit
|
||||||
points row-aligned with the cut-link columns (for Jets).
|
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)
|
active_fluid = ((domain_flags & FLUID) != 0) & ((domain_flags & SOLID) == 0)
|
||||||
force_owners: set[int] = set()
|
force_owners: set[int] = set()
|
||||||
|
|
||||||
if sensor_cell_counts_out is not None:
|
sensor_counts = (np.zeros_like(sensor_cell_counts_out)
|
||||||
sensor_cell_counts_out.fill(0)
|
if sensor_cell_counts_out is not None else None)
|
||||||
|
|
||||||
ez = None
|
ez = None
|
||||||
if self.cfg and self.cfg.is_d3q19:
|
if self.cfg and self.cfg.is_d3q19:
|
||||||
@@ -430,9 +430,9 @@ class ObjectManager:
|
|||||||
if len(cells) > 0:
|
if len(cells) > 0:
|
||||||
s_cells.append(cells)
|
s_cells.append(cells)
|
||||||
s_ids.append(ids)
|
s_ids.append(ids)
|
||||||
if (sensor_cell_counts_out is not None
|
if (sensor_counts is not None
|
||||||
and 0 <= obj.obj_id < sensor_cell_counts_out.size):
|
and 0 <= obj.obj_id < sensor_counts.size):
|
||||||
sensor_cell_counts_out[obj.obj_id] = int(len(cells))
|
sensor_counts[obj.obj_id] = int(len(cells))
|
||||||
if obj.is_force_region and hasattr(obj, 'get_sensor_list'):
|
if obj.is_force_region and hasattr(obj, 'get_sensor_list'):
|
||||||
cells, ids = obj.get_sensor_list(self.nx, self.ny, self.nz)
|
cells, ids = obj.get_sensor_list(self.nx, self.ny, self.nz)
|
||||||
keep = active_fluid[cells]
|
keep = active_fluid[cells]
|
||||||
@@ -461,8 +461,9 @@ class ObjectManager:
|
|||||||
r_fr_ids = np.concatenate(fr_ids) if fr_ids else np.zeros(0, dtype=np.int32)
|
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))
|
r_hit = np.concatenate(cl_hit) if cl_hit else np.zeros((0, 2))
|
||||||
if hits_out is not None:
|
# Passive walls need the same ownership guarantees as driven walls.
|
||||||
hits_out.append(r_hit)
|
# Overlapping solids are not a supported union-boundary operation.
|
||||||
|
validate_cut_link_ownership(domain_flags, r_idx, r_dir, self.nq)
|
||||||
if self.jets or any(obj.surface_actuation is not None for obj in objects):
|
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.
|
# Reject unresolved apertures/ownership before sync or restart mutates state.
|
||||||
from ..lbm.curved_links import CurvedLinkSoA
|
from ..lbm.curved_links import CurvedLinkSoA
|
||||||
@@ -470,6 +471,11 @@ class ObjectManager:
|
|||||||
links.assign_host(r_idx, r_dir, r_q, r_bid, r_rx, r_ry, r_rz, r_fallback)
|
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)
|
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 (
|
return (
|
||||||
r_idx, r_dir, r_q, r_bid,
|
r_idx, r_dir, r_q, r_bid,
|
||||||
r_rx, r_ry, r_rz, r_fallback,
|
r_rx, r_ry, r_rz, r_fallback,
|
||||||
@@ -487,8 +493,6 @@ class ObjectManager:
|
|||||||
the caller has already prepared and uploaded the final flag field
|
the caller has already prepared and uploaded the final flag field
|
||||||
for this geometry, such as during initialization.
|
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
|
next_flags = self.build_flags(field.build_channel_flags()) if rebuild_flags else field.flag
|
||||||
hits = []
|
hits = []
|
||||||
|
|
||||||
@@ -499,6 +503,7 @@ class ObjectManager:
|
|||||||
fr_cells, fr_obj_id,
|
fr_cells, fr_obj_id,
|
||||||
) = self.build_compact_lists(domain_flags=next_flags, hits_out=hits)
|
) = self.build_compact_lists(domain_flags=next_flags, hits_out=hits)
|
||||||
|
|
||||||
|
self._telemetry_field = field
|
||||||
if rebuild_flags:
|
if rebuild_flags:
|
||||||
field.flag = next_flags
|
field.flag = next_flags
|
||||||
field.upload_flags()
|
field.upload_flags()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CelerisLab/body/preprocess/flag_overlay.py
|
# CelerisLab/body/preprocess/flag_overlay.py
|
||||||
"""
|
"""
|
||||||
Flag overlay: merge per-object flag masks onto a clean domain base.
|
Flag overlay and cut-link ownership checks against the merged domain.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -12,6 +12,15 @@ import numpy as np
|
|||||||
from ...lbm.descriptors import FLUID, SOLID, SENSOR_FLAG, FRC_REGION
|
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(
|
def merge_flag_masks(
|
||||||
base: np.ndarray,
|
base: np.ndarray,
|
||||||
masks: List[np.ndarray],
|
masks: List[np.ndarray],
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Two JSON files:
|
|||||||
config_body.json – object list
|
config_body.json – object list
|
||||||
|
|
||||||
LBMConfig / BodyConfig are plain dataclasses; they translate to
|
LBMConfig / BodyConfig are plain dataclasses; they translate to
|
||||||
macro dicts consumed by `cuda/compiler_v2.py` for header generation.
|
macro dicts consumed by `cuda/compiler.py` for header generation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|||||||
@@ -60,14 +60,14 @@ Python `config.py` 只负责读取和校验,不是配置位置。显式配置
|
|||||||
|
|
||||||
**`lbm/kernels/config/*.h` 由编译器根据 `LBMConfig` 自动生成,请勿手改。**
|
**`lbm/kernels/config/*.h` 由编译器根据 `LBMConfig` 自动生成,请勿手改。**
|
||||||
|
|
||||||
仓库中检入的 `config/*.h` 可能是历史版本。`compiler_v2.generate_config` 会复制 kernel 源码到独立临时构建目录,并在副本中生成配置;不会改写包内头文件或 PTX。返回目录须显式传给 `compile_kernel(build_dir=...)`,编译结果与配置摘要均保存在该目录。`Simulation` 负责目录清理;直接使用编译器的调用者须调用 `cleanup_build_dir()`。跳过生成配置直接编译仓库中的 `kernel_v2.cu` 属不受支持用法。
|
仓库中检入的 `config/*.h` 可能是历史版本。`compiler.generate_config` 会复制 kernel 源码到独立临时构建目录,并在副本中生成配置;不会改写包内头文件或 PTX。返回目录须显式传给 `compile_kernel(build_dir=...)`,编译结果与配置摘要均保存在该目录。`Simulation` 负责目录清理;直接使用编译器的调用者须调用 `cleanup_build_dir()`。跳过生成配置直接编译仓库中的 `uniform.cu` 属不受支持用法。
|
||||||
|
|
||||||
```
|
```
|
||||||
config_lbm.json
|
config_lbm.json
|
||||||
↓ load_lbm_config()
|
↓ load_lbm_config()
|
||||||
config.py:LBMConfig (读取 + 校验)
|
config.py:LBMConfig (读取 + 校验)
|
||||||
↓ to_macros()
|
↓ to_macros()
|
||||||
cuda/compiler_v2.py (独立构建目录中生成 config/*.h)
|
cuda/compiler.py (独立构建目录中生成 config/*.h)
|
||||||
↓
|
↓
|
||||||
config_grid.h → NX, NY, NZ, NT, LATTICE_MODEL;
|
config_grid.h → NX, NY, NZ, NT, LATTICE_MODEL;
|
||||||
DIM / NQ 由 LATTICE_MODEL 推导写入(与 grid.lattice_model 一致,非 JSON 顶层字段)
|
DIM / NQ 由 LATTICE_MODEL 推导写入(与 grid.lattice_model 一致,非 JSON 顶层字段)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""CUDA build helpers (compilation itself does not need a live context)."""
|
"""CUDA build helpers (compilation itself does not need a live context)."""
|
||||||
from . import compiler_v2 as compiler
|
from . import compiler
|
||||||
|
|
||||||
__all__ = ["compiler"]
|
__all__ = ["compiler"]
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# CelerisLab/cuda/compiler_v2.py
|
# CelerisLab/cuda/compiler.py
|
||||||
"""
|
"""
|
||||||
Kernel configuration, compilation, and module loading.
|
Kernel configuration, compilation, and module loading.
|
||||||
|
|
||||||
Workflow:
|
Owners call build_module(cfg, entry, arch=...), then set_params(module, omega, ...).
|
||||||
|
build_module runs, in a private directory it removes afterwards:
|
||||||
1. build_dir = generate_config(lbm_cfg, n_objects) — isolated source snapshot
|
1. build_dir = generate_config(lbm_cfg, n_objects) — isolated source snapshot
|
||||||
2. compile_kernel(arch, build_dir=build_dir) — nvcc -ptx kernel_v2.cu
|
2. compile_kernel(arch, source=entry, build_dir=build_dir) — nvcc -ptx
|
||||||
3. load_module(ptx_path) — PyCUDA module from PTX
|
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
|
from __future__ import annotations
|
||||||
@@ -16,7 +18,6 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from contextvars import ContextVar
|
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@@ -33,9 +34,6 @@ _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:
|
def kernel_path(filename: str) -> str:
|
||||||
return os.path.join(_KERNEL_DIR, filename)
|
return os.path.join(_KERNEL_DIR, filename)
|
||||||
@@ -54,14 +52,16 @@ def _write(path: str, content: str):
|
|||||||
|
|
||||||
|
|
||||||
def generate_config(cfg: LBMConfig, n_objects: int = 0,
|
def generate_config(cfg: LBMConfig, n_objects: int = 0,
|
||||||
*, build_root: Optional[str] = None) -> str:
|
*, build_root: Optional[str] = None, block_root_bc: int = 0) -> str:
|
||||||
"""Snapshot kernels and render headers in a fresh private directory.
|
"""Snapshot kernels and render headers in a fresh private directory.
|
||||||
|
|
||||||
Returns its path. Pass it explicitly to compile_kernel for interleaved
|
Returns its path; pass it to compile_kernel(build_dir=...). The caller owns cleanup_build_dir() once the loaded module is no longer
|
||||||
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.
|
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()
|
macros = cfg.to_macros()
|
||||||
if isinstance(n_objects, bool) or not isinstance(n_objects, int) or n_objects < 0:
|
if isinstance(n_objects, bool) or not isinstance(n_objects, int) or n_objects < 0:
|
||||||
raise ValueError("n_objects must be a nonnegative integer")
|
raise ValueError("n_objects must be a nonnegative integer")
|
||||||
@@ -71,20 +71,19 @@ def generate_config(cfg: LBMConfig, n_objects: int = 0,
|
|||||||
try:
|
try:
|
||||||
shutil.copytree(_KERNEL_DIR, build_dir, dirs_exist_ok=True,
|
shutil.copytree(_KERNEL_DIR, build_dir, dirs_exist_ok=True,
|
||||||
ignore=shutil.ignore_patterns("*.ptx", "*.cubin", "*.tmp"))
|
ignore=shutil.ignore_patterns("*.ptx", "*.cubin", "*.tmp"))
|
||||||
_render_config(macros, n_objects, os.path.join(build_dir, "config"))
|
_render_config(macros, n_objects, os.path.join(build_dir, "config"),
|
||||||
|
block_root_bc=block_root_bc)
|
||||||
digest = hashlib.sha256()
|
digest = hashlib.sha256()
|
||||||
for path in sorted(Path(build_dir).rglob("*")):
|
for path in sorted(Path(build_dir).rglob("*")):
|
||||||
if path.is_file():
|
if path.is_file():
|
||||||
digest.update(str(path.relative_to(build_dir)).encode())
|
digest.update(str(path.relative_to(build_dir)).encode())
|
||||||
digest.update(path.read_bytes())
|
digest.update(path.read_bytes())
|
||||||
metadata = {"source_sha256": digest.hexdigest(), "config": asdict(cfg),
|
metadata = {"source_sha256": digest.hexdigest(), "config": asdict(cfg),
|
||||||
"n_objects": n_objects}
|
"n_objects": n_objects, "block_root_bc": block_root_bc}
|
||||||
_write(os.path.join(build_dir, ".celeris-build"), json.dumps(metadata, sort_keys=True))
|
_write(os.path.join(build_dir, ".celeris-build"), json.dumps(metadata, sort_keys=True))
|
||||||
except BaseException:
|
except BaseException:
|
||||||
shutil.rmtree(build_dir)
|
shutil.rmtree(build_dir)
|
||||||
raise
|
raise
|
||||||
_last_build_dir.set(build_dir)
|
|
||||||
_last_ptx_path.set(None)
|
|
||||||
return build_dir
|
return build_dir
|
||||||
|
|
||||||
|
|
||||||
@@ -96,12 +95,9 @@ def cleanup_build_dir(build_dir: Optional[str]) -> None:
|
|||||||
if path == Path(_KERNEL_DIR).resolve() or not (path / ".celeris-build").is_file():
|
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")
|
raise ValueError("Refusing to remove a directory not owned by the compiler")
|
||||||
shutil.rmtree(path)
|
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):
|
def _render_config(m: dict, n_objects: int, cdir: str, *, block_root_bc: int = 0):
|
||||||
_write(os.path.join(cdir, "config_grid.h"), f"""{_HEADER}\
|
_write(os.path.join(cdir, "config_grid.h"), f"""{_HEADER}\
|
||||||
// Layer: Global / Grid
|
// Layer: Global / Grid
|
||||||
#ifndef CELERIS_CONFIG_GRID_H
|
#ifndef CELERIS_CONFIG_GRID_H
|
||||||
@@ -165,6 +161,9 @@ def _render_config(m: dict, n_objects: int, cdir: str):
|
|||||||
#define STORE_PRECISION {m['STORE_PRECISION']}
|
#define STORE_PRECISION {m['STORE_PRECISION']}
|
||||||
#define USE_DDF_SHIFTING {m['USE_DDF_SHIFTING']}
|
#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 USE_LES {m['USE_LES']}
|
||||||
#define LES_CS {m['LES_CS']}
|
#define LES_CS {m['LES_CS']}
|
||||||
#define LES_CLOSED_FORM {m['LES_CLOSED_FORM']}
|
#define LES_CLOSED_FORM {m['LES_CLOSED_FORM']}
|
||||||
@@ -241,13 +240,9 @@ def _render_config(m: dict, n_objects: int, cdir: str):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Compilation
|
# Compilation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def compile_kernel(arch: str = "sm_70", source: str = "kernel_v2.cu",
|
def compile_kernel(arch: str = "sm_70", source: str = "uniform.cu",
|
||||||
output: Optional[str] = None, *,
|
output: Optional[str] = None, *, build_dir: str) -> str:
|
||||||
build_dir: Optional[str] = None) -> str:
|
"""Compile the snapshot in build_dir (from generate_config); never package headers."""
|
||||||
"""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():
|
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")
|
raise RuntimeError("Call generate_config() and pass its build_dir before compiling")
|
||||||
root = Path(build_dir).resolve()
|
root = Path(build_dir).resolve()
|
||||||
@@ -272,23 +267,36 @@ def compile_kernel(arch: str = "sm_70", source: str = "kernel_v2.cu",
|
|||||||
finally:
|
finally:
|
||||||
if os.path.exists(tmp):
|
if os.path.exists(tmp):
|
||||||
os.unlink(tmp)
|
os.unlink(tmp)
|
||||||
_last_ptx_path.set(str(dst))
|
|
||||||
return str(dst)
|
return str(dst)
|
||||||
|
|
||||||
|
|
||||||
def load_module(ptx_path: Optional[str] = None):
|
def load_module(ptx_path: str):
|
||||||
"""Load explicit PTX or the last PTX compiled in this Python context."""
|
"""Load one compiled PTX file as a new PyCUDA module."""
|
||||||
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
|
import pycuda.driver as cuda
|
||||||
return cuda.module_from_file(ptx_path)
|
return cuda.module_from_file(ptx_path)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
def build_module(cfg: LBMConfig, source: str, *, arch: str, n_objects: int = 0,
|
||||||
# Back-compat helpers (used by test scripts)
|
block_root_bc: int = 0):
|
||||||
# ---------------------------------------------------------------------------
|
"""Generate, compile and load one private module; the build directory is removed.
|
||||||
|
|
||||||
def compile_kernel_v2(arch: str = "sm_70") -> str:
|
The single build path of every owner (Simulation, FixedPatchStepper levels).
|
||||||
"""Alias for compile_kernel() kept for test-script compatibility."""
|
Each call returns a NEW module: callers must not share it, because its
|
||||||
return compile_kernel(arch=arch)
|
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))
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"""Packed ``obs`` buffer layout for force / torque / sensor telemetry.
|
"""Packed ``obs`` buffer layout for force / torque / sensor telemetry.
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
Host code (e.g. ``ObjectManager``) and ``compiler_v2.generate_config`` must use
|
Host code (e.g. ``ObjectManager``) and ``compiler.generate_config`` must use
|
||||||
the same ``dim`` and ``n_objects`` so allocation sizes match device macros.
|
the same ``dim`` and ``n_objects`` so allocation sizes match device macros.
|
||||||
|
|
||||||
Design::
|
Design::
|
||||||
|
|||||||
@@ -1,286 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -31,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.
|
be stale, which is distinct from the unique core ownership of stored state.
|
||||||
No moving geometry is implied.
|
No moving geometry is implied.
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
from numbers import Integral
|
from numbers import Integral
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
@@ -373,3 +373,84 @@ def blocked_link_tables(layout, flags, fluid_idx, dir, q, fallback):
|
|||||||
dir=columns[1].astype(np.uint8), q=columns[2].astype(np.float32),
|
dir=columns[1].astype(np.uint8), q=columns[2].astype(np.float32),
|
||||||
fallback=columns[3].astype(np.uint8))
|
fallback=columns[3].astype(np.uint8))
|
||||||
return tables, mapped
|
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)
|
||||||
|
|||||||
@@ -94,12 +94,10 @@ class CurvedLinkSoA:
|
|||||||
"""
|
"""
|
||||||
self.surface_velocity = self.aperture = None
|
self.surface_velocity = self.aperture = None
|
||||||
self.jets, self.jet_shape, self.jet_id = tuple(jets), None, None
|
self.jets, self.jet_shape, self.jet_id = tuple(jets), None, None
|
||||||
self.surface_velocity, self.aperture = surface_link_velocity(
|
from ..body.jets import _surface_links
|
||||||
|
self.surface_velocity, self.aperture, self.jet_shape, self.jet_id = _surface_links(
|
||||||
objects, cfg, flags, self.fluid_idx, self.dir, self.body_id, self.rx, self.ry,
|
objects, cfg, flags, self.fluid_idx, self.dir, self.body_id, self.rx, self.ry,
|
||||||
jets=self.jets, hits=hits)
|
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):
|
def set_jet_amplitude(self, index, amplitude, stream=None):
|
||||||
"""Rewrite jet rows from cached shapes; re-upload only the velocity column.
|
"""Rewrite jet rows from cached shapes; re-upload only the velocity column.
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// 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
|
||||||
|
#if BLOCK_ROOT_BC != 2
|
||||||
|
#include "operators/block_collision.cuh"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 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
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
// 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"
|
|
||||||
#if COLLISION_MODEL == 2
|
|
||||||
#include "operators/collision_mrt.cuh"
|
|
||||||
#endif
|
|
||||||
#include "streaming/pull_double_buffer.cuh"
|
|
||||||
#include "boundary/curved_boundary.cuh"
|
|
||||||
#include "boundary/planar_velocity.cuh"
|
|
||||||
|
|
||||||
#if DIM != 2 || NQ != 9 || (COLLISION_MODEL != 0 && COLLISION_MODEL != 2) || STORE_PRECISION != 0 || USE_LES || STREAMING_MODEL != 0
|
|
||||||
#error "Blocks require FP32 D2Q9 SRT or MRT double buffers without LES"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// 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];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Experimental Schornbaum--Ruede (arXiv:1508.07982, Algorithm 2)
|
|
||||||
// and Chen et al. (doi:10.1016/j.physa.2005.09.036) population interpolation.
|
|
||||||
// Independent implementation of the mathematical stencil; no upstream source copied.
|
|
||||||
// Stencil cells are owner cores; a disabled axis repeats its center twice.
|
|
||||||
extern "C" __global__ void BlockExplodeProjected(
|
|
||||||
const fpxx* coarse, fpxx* fine, const uint64_t* target, const uint64_t* donor,
|
|
||||||
unsigned int count, unsigned long coarse_cells,
|
|
||||||
const uint64_t* neighbors, const float* offsets)
|
|
||||||
{
|
|
||||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
|
||||||
if (row >= count) return;
|
|
||||||
const uint64_t t = target[row], d = donor[row];
|
|
||||||
fpxx* to = fine + (t / TOTAL_CELLS) * NQ * TOTAL_CELLS;
|
|
||||||
for (unsigned int q = 0; q < NQ; ++q) {
|
|
||||||
float v[4];
|
|
||||||
for (unsigned int a = 0; a < 4; ++a) {
|
|
||||||
const uint64_t n = neighbors[4ul * row + a];
|
|
||||||
v[a] = coarse[(n / coarse_cells) * NQ * coarse_cells
|
|
||||||
+ q * coarse_cells + n % coarse_cells];
|
|
||||||
}
|
|
||||||
float gx = .5f * (v[1] - v[0]), gy = .5f * (v[3] - v[2]);
|
|
||||||
const float ex = d_cx[q], ey = d_cy[q];
|
|
||||||
if (q != 0) {
|
|
||||||
const float along = (ex * gx + ey * gy) / (ex * ex + ey * ey);
|
|
||||||
gx -= ex * along;
|
|
||||||
gy -= ey * along;
|
|
||||||
}
|
|
||||||
to[index_f(t % TOTAL_CELLS, q)] =
|
|
||||||
coarse[(d / coarse_cells) * NQ * coarse_cells + q * coarse_cells + d % coarse_cells]
|
|
||||||
+ offsets[2ul * row] * gx + offsets[2ul * row + 1] * gy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Targets are receiver-local SOURCE slots for the immediately following pull.
|
|
||||||
// Keep child summation order identical to PatchCoarseStep; no collision here.
|
|
||||||
extern "C" __global__ void BlockCoalesce(
|
|
||||||
const fpxx* fine, fpxx* coarse, const uint64_t* target, const uint64_t* children,
|
|
||||||
unsigned int count)
|
|
||||||
{
|
|
||||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
|
||||||
if (row >= count) return;
|
|
||||||
const uint64_t* s = children + 4ul * row;
|
|
||||||
coarse[target[row]] = 0.25f * (fine[s[0]] + fine[s[1]] + fine[s[2]] + fine[s[3]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
#if COLLISION_MODEL == 2
|
|
||||||
// Uniform MRT sequence (collide_dispatch, zero forcing); SRT path below unchanged.
|
|
||||||
float rho, ux, uy;
|
|
||||||
compute_rho_u(f, rho, ux, uy);
|
|
||||||
collide_mrt_no_force(f, rho, ux, uy,
|
|
||||||
fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega)));
|
|
||||||
#else
|
|
||||||
float rho, ux, uy, feq[NQ];
|
|
||||||
compute_rho_u(f, rho, ux, uy);
|
|
||||||
#if USE_DDF_SHIFTING
|
|
||||||
// Preserve the density perturbation before adding the unit reference.
|
|
||||||
const float drho = f[0]+f[1]+f[2]+f[3]+f[4]+f[5]+f[6]+f[7]+f[8];
|
|
||||||
const float jx = f[1]-f[2]+f[5]-f[6]+f[7]-f[8];
|
|
||||||
const float jy = f[3]-f[4]+f[5]-f[6]-f[7]+f[8];
|
|
||||||
compute_feq_from_momentum(drho, jx, jy, ux, uy, feq);
|
|
||||||
#else
|
|
||||||
compute_feq(rho, ux, uy, feq);
|
|
||||||
#endif
|
|
||||||
collide_srt_no_force(f, feq, d_params.omega);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stream_pull_store(k, f, out + base);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Root channel closure; fine and periodic levels keep BlockStep.
|
|
||||||
#include "step/block_channel.cu"
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// 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,10 +15,13 @@
|
|||||||
// D2Q9 y-wall half-way bounce-back for pull double-buffer streaming.
|
// D2Q9 y-wall half-way bounce-back for pull double-buffer streaming.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
#if LATTICE_MODEL == LATTICE_D2Q9
|
#if LATTICE_MODEL == LATTICE_D2Q9
|
||||||
__device__ inline void apply_wall_bb_d2q9_y_pull(unsigned int y,
|
// Rows 1 and top_row (= physical ny - 2) next to walls at rows 0 and ny - 1.
|
||||||
float* __restrict__ f,
|
// y is the physical row; k addresses the node in fi_in (a block window may
|
||||||
const fpxx* __restrict__ fi_in,
|
// have NX/NY strides different from the physical extent).
|
||||||
unsigned long k)
|
__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)
|
||||||
{
|
{
|
||||||
if (y == 1) {
|
if (y == 1) {
|
||||||
// Directions sourced from y=0 wall in pull step: +y, +x+y, -x+y.
|
// Directions sourced from y=0 wall in pull step: +y, +x+y, -x+y.
|
||||||
@@ -26,14 +29,22 @@ __device__ inline void apply_wall_bb_d2q9_y_pull(unsigned int y,
|
|||||||
f[5] = load_ddf(fi_in, index_f(k, 6u));
|
f[5] = load_ddf(fi_in, index_f(k, 6u));
|
||||||
f[8] = load_ddf(fi_in, index_f(k, 7u));
|
f[8] = load_ddf(fi_in, index_f(k, 7u));
|
||||||
}
|
}
|
||||||
else if (y == (unsigned int)(NY - 2)) {
|
else if (y == top_row) {
|
||||||
// Directions sourced from y=NY-1 wall in pull step: -y, -x-y, +x-y.
|
// Directions sourced from the y = ny - 1 wall in pull step: -y, -x-y, +x-y.
|
||||||
f[4] = load_ddf(fi_in, index_f(k, 3u));
|
f[4] = load_ddf(fi_in, index_f(k, 3u));
|
||||||
f[6] = load_ddf(fi_in, index_f(k, 5u));
|
f[6] = load_ddf(fi_in, index_f(k, 5u));
|
||||||
f[7] = load_ddf(fi_in, index_f(k, 8u));
|
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.
|
// D2Q9 y-wall free-slip (specular reflection) for pull double-buffer streaming.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
#define STORE_PRECISION 0
|
#define STORE_PRECISION 0
|
||||||
#define USE_DDF_SHIFTING 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 USE_LES 0
|
||||||
#define LES_CS 0.160000f
|
#define LES_CS 0.160000f
|
||||||
#define LES_CLOSED_FORM 1
|
#define LES_CLOSED_FORM 1
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// 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,6 +53,18 @@ __device__ __forceinline__ void index_from_thread(unsigned int& x,
|
|||||||
k = (unsigned long)y * NX + 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)
|
// Neighbor list (periodic BCs via modular arithmetic)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Collision used by periodic/fine and slot/reservoir BlockStep.
|
||||||
|
#ifndef CELERIS_OPERATORS_BLOCK_COLLISION_CUH
|
||||||
|
#define CELERIS_OPERATORS_BLOCK_COLLISION_CUH
|
||||||
|
|
||||||
|
#include "../core/params.cuh"
|
||||||
|
#include "macro.cuh"
|
||||||
|
#include "collision_srt.cuh"
|
||||||
|
#if COLLISION_MODEL == 2
|
||||||
|
#include "collision_mrt.cuh"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
__device__ __forceinline__ void block_collide(float* f) {
|
||||||
|
#if COLLISION_MODEL == 2
|
||||||
|
collide_mrt_unforced(f); // SRT path below unchanged.
|
||||||
|
#else
|
||||||
|
float rho, ux, uy, feq[NQ];
|
||||||
|
compute_rho_u(f, rho, ux, uy);
|
||||||
|
#if USE_DDF_SHIFTING
|
||||||
|
// Preserve the density perturbation before adding the unit reference.
|
||||||
|
const float drho = f[0]+f[1]+f[2]+f[3]+f[4]+f[5]+f[6]+f[7]+f[8];
|
||||||
|
const float jx = f[1]-f[2]+f[5]-f[6]+f[7]-f[8];
|
||||||
|
const float jy = f[3]-f[4]+f[5]-f[6]-f[7]+f[8];
|
||||||
|
compute_feq_from_momentum(drho, jx, jy, ux, uy, feq);
|
||||||
|
#else
|
||||||
|
compute_feq(rho, ux, uy, feq);
|
||||||
|
#endif
|
||||||
|
collide_srt_no_force(f, feq, d_params.omega);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// CelerisLab – operators/bounce_back_swap.cuh
|
||||||
|
// Full-way bounce-back at a wall node: swap every opposite pair (paired
|
||||||
|
// order q, q+1). Needs only NQ; shared by the uniform and blocked steps.
|
||||||
|
#ifndef CELERIS_OPERATORS_BOUNCE_BACK_SWAP_CUH
|
||||||
|
#define CELERIS_OPERATORS_BOUNCE_BACK_SWAP_CUH
|
||||||
|
|
||||||
|
__device__ __forceinline__ void bounce_back_swap(float* f) {
|
||||||
|
#pragma unroll
|
||||||
|
for (int i = 1; i < NQ; i += 2) {
|
||||||
|
float t = f[i]; f[i] = f[i+1]; f[i+1] = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -114,6 +114,16 @@ __device__ __forceinline__ void collide_mrt_no_force(float* __restrict__ g,
|
|||||||
collide_mrt(g, rho, ux, uy, Fin, omega);
|
collide_mrt(g, rho, ux, uy, Fin, omega);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unforced MRT with the clamped device omega, as in collide_dispatch with zero
|
||||||
|
// forcing. Shared by the blocked, channel-root and dense-patch collisions.
|
||||||
|
__device__ __forceinline__ void collide_mrt_unforced(float* __restrict__ g)
|
||||||
|
{
|
||||||
|
float rho, ux, uy;
|
||||||
|
compute_rho_u(g, rho, ux, uy);
|
||||||
|
collide_mrt_no_force(g, rho, ux, uy,
|
||||||
|
fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega)));
|
||||||
|
}
|
||||||
|
|
||||||
#elif LATTICE_MODEL == LATTICE_D3Q19
|
#elif LATTICE_MODEL == LATTICE_D3Q19
|
||||||
|
|
||||||
__device__ __forceinline__ void collide_mrt(float* __restrict__ g,
|
__device__ __forceinline__ void collide_mrt(float* __restrict__ g,
|
||||||
|
|||||||
@@ -5,12 +5,7 @@
|
|||||||
#ifndef CELERIS_OPERATORS_HELPERS_CUH
|
#ifndef CELERIS_OPERATORS_HELPERS_CUH
|
||||||
#define CELERIS_OPERATORS_HELPERS_CUH
|
#define CELERIS_OPERATORS_HELPERS_CUH
|
||||||
|
|
||||||
__device__ __forceinline__ void bounce_back_swap(float* f) {
|
#include "bounce_back_swap.cuh"
|
||||||
#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(
|
__device__ __forceinline__ void collide_dispatch(
|
||||||
float* f, float rho_n,
|
float* f, float rho_n,
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
// 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"
|
|
||||||
#if COLLISION_MODEL == 2
|
|
||||||
#include "operators/collision_mrt.cuh"
|
|
||||||
#endif
|
|
||||||
#include "streaming/pull_double_buffer.cuh"
|
|
||||||
#include "boundary/planar_velocity.cuh"
|
|
||||||
#include "boundary/curved_boundary.cuh"
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
#include "step/curved_double.cu"
|
|
||||||
}
|
|
||||||
|
|
||||||
#if DIM != 2 || NQ != 9 || (COLLISION_MODEL != 0 && COLLISION_MODEL != 2) || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
|
|
||||||
#error "Refinement requires unshifted FP32 D2Q9 SRT or MRT double buffers without LES"
|
|
||||||
#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) {
|
|
||||||
#if COLLISION_MODEL == 2
|
|
||||||
float rho, ux, uy;
|
|
||||||
compute_rho_u(f, rho, ux, uy);
|
|
||||||
collide_mrt_no_force(f, rho, ux, uy,
|
|
||||||
fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega)));
|
|
||||||
#else
|
|
||||||
float rho, ux, uy, feq[NQ];
|
|
||||||
compute_rho_u(f, rho, ux, uy);
|
|
||||||
compute_feq(rho, ux, uy, feq);
|
|
||||||
collide_srt_no_force(f, feq, d_params.omega);
|
|
||||||
#endif
|
|
||||||
#ifdef CELERIS_REFINEMENT_AUDIT
|
|
||||||
++collision_counts[k];
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
// ApplyEsoPullCurvedKernel, SensorKernel, ForceRegionKernel.
|
||||||
//
|
//
|
||||||
// Launched on compact lists (n_curved / n_sensor / n_cells threads).
|
// Launched on compact lists (n_curved / n_sensor / n_cells threads).
|
||||||
// Included by kernel_v2.cu. No standalone compilation.
|
// Included by uniform.cu. No standalone compilation.
|
||||||
//
|
//
|
||||||
// Double-buffer mode:
|
// Double-buffer mode:
|
||||||
// CurvedBoundaryKernel runs BEFORE OneStep.
|
// CurvedBoundaryKernel runs BEFORE OneStep.
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// 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,5 +1,12 @@
|
|||||||
|
#if BLOCK_ROOT_BC != 2
|
||||||
|
#error "BlockChannelStep requires BLOCK_ROOT_BC == 2"
|
||||||
|
#endif
|
||||||
|
|
||||||
// Root-only blocked counterpart of OneStep's D2Q9/SRT channel closure.
|
// Root-only blocked counterpart of OneStep's D2Q9/SRT channel closure.
|
||||||
// NX/NY are LOCAL strides; all physical boundary decisions use global coordinates.
|
// 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/common.cuh"
|
||||||
#include "../boundary/inlet/zou_he_local.cuh"
|
#include "../boundary/inlet/zou_he_local.cuh"
|
||||||
#include "../boundary/outlet/pressure_neq.cuh"
|
#include "../boundary/outlet/pressure_neq.cuh"
|
||||||
@@ -15,15 +22,13 @@ extern "C" __global__ void BlockChannelStep(
|
|||||||
unsigned int x, y; unsigned long k;
|
unsigned int x, y; unsigned long k;
|
||||||
index_from_thread(x, y, k);
|
index_from_thread(x, y, k);
|
||||||
if (x < halo || y < halo || x >= NX - halo || y >= NY - halo) return;
|
if (x < halo || y < halo || x >= NX - halo || y >= NY - halo) return;
|
||||||
const unsigned int bx = NX - 2 * halo, by = NY - 2 * halo;
|
unsigned int gx, gy;
|
||||||
const unsigned int nbx = d_block_channel.nx / bx;
|
block_global_xy(x, y, halo, d_block_channel.nx, gx, gy);
|
||||||
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 base = (unsigned long)blockIdx.z * NQ * TOTAL_CELLS;
|
||||||
const unsigned long cell = (unsigned long)blockIdx.z * TOTAL_CELLS + k;
|
const unsigned long cell = (unsigned long)blockIdx.z * TOTAL_CELLS + k;
|
||||||
float f[NQ];
|
float f[NQ];
|
||||||
if (kind != nullptr && kind[cell] == KIND_OUTER) {
|
if (kind != nullptr && kind[cell] == KIND_OUTER) {
|
||||||
for (int q = 0; q < NQ; ++q) f[q] = NAN;
|
fill_invalid(f);
|
||||||
} else {
|
} else {
|
||||||
unsigned long j[NQ];
|
unsigned long j[NQ];
|
||||||
compute_neighbors(x, y, j);
|
compute_neighbors(x, y, j);
|
||||||
@@ -31,35 +36,21 @@ extern "C" __global__ void BlockChannelStep(
|
|||||||
const bool wall = gy == 0 || gy == d_block_channel.ny - 1;
|
const bool wall = gy == 0 || gy == d_block_channel.ny - 1;
|
||||||
const bool outlet = gx == d_block_channel.nx - 1;
|
const bool outlet = gx == d_block_channel.nx - 1;
|
||||||
if (wall) {
|
if (wall) {
|
||||||
// Wall owns all four corners. Ghosts pull and swap, never collide.
|
bounce_back_swap(f); // Wall owns all four corners; ghosts 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) {
|
} else if (gx == 0) {
|
||||||
apply_zou_he_left_velocity_inlet_d2q9(
|
apply_zou_he_left_velocity_inlet_d2q9(
|
||||||
f, inlet_target_u((float)gy, d_block_channel.ny, d_block_channel.velocity), 0.0f);
|
f, inlet_target_u((float)gy, d_block_channel.ny, d_block_channel.velocity), 0.0f);
|
||||||
} else if (outlet) {
|
} else if (outlet) {
|
||||||
// Immutable input donor, receiver-local (also valid across a block seam).
|
// Immutable input donor, receiver-local (also valid across a block seam).
|
||||||
float donor[NQ];
|
float donor[NQ];
|
||||||
for (int q = 0; q < NQ; ++q)
|
load_node(in + base, k - 1, donor);
|
||||||
donor[q] = load_ddf(in + base, index_f(k - 1, q));
|
|
||||||
apply_pressure_outlet_d2q9(f, donor);
|
apply_pressure_outlet_d2q9(f, donor);
|
||||||
} else if (gy == 1) {
|
} else {
|
||||||
f[3] = load_ddf(in + base, index_f(k, 4));
|
apply_wall_bb_d2q9_rows(gy, d_block_channel.ny - 2, f, in + base, k);
|
||||||
f[5] = load_ddf(in + base, index_f(k, 6));
|
|
||||||
f[8] = load_ddf(in + base, index_f(k, 7));
|
|
||||||
} else if (gy == d_block_channel.ny - 2) {
|
|
||||||
f[4] = load_ddf(in + base, index_f(k, 3));
|
|
||||||
f[6] = load_ddf(in + base, index_f(k, 5));
|
|
||||||
f[7] = load_ddf(in + base, index_f(k, 8));
|
|
||||||
}
|
}
|
||||||
if (!wall && !outlet) {
|
if (!wall && !outlet) {
|
||||||
#if COLLISION_MODEL == 2
|
#if COLLISION_MODEL == 2
|
||||||
// Match uniform MRT arithmetic (collide_dispatch, zero forcing).
|
collide_mrt_unforced(f);
|
||||||
float rho, ux, uy;
|
|
||||||
compute_rho_u(f, rho, ux, uy);
|
|
||||||
collide_mrt_no_force(f, rho, ux, uy,
|
|
||||||
fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega)));
|
|
||||||
#else
|
#else
|
||||||
// Match uniform SRT arithmetic, including its shifted equilibrium path.
|
// Match uniform SRT arithmetic, including its shifted equilibrium path.
|
||||||
float rho, ux, uy, feq[NQ], forcing[NQ] = {};
|
float rho, ux, uy, feq[NQ], forcing[NQ] = {};
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// 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
|
// 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].
|
// computes rho/ux/uy. Output is SoA: rho_out[N], ux_out[N], uy_out[N].
|
||||||
// Included by kernel_v2.cu. No standalone compilation.
|
// Included by uniform.cu. No standalone compilation.
|
||||||
//
|
//
|
||||||
// This kernel is the sole path for macroscopic diagnostics in EsoPull mode.
|
// This kernel is the sole path for macroscopic diagnostics in EsoPull mode.
|
||||||
// It uses the shared semantic helper layer (esopull_semantic_helpers.cuh).
|
// It uses the shared semantic helper layer (esopull_semantic_helpers.cuh).
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// or encodes physical DDF back to EsoPull backing layout.
|
// or encodes physical DDF back to EsoPull backing layout.
|
||||||
// Each thread handles one cell from a compact index list.
|
// Each thread handles one cell from a compact index list.
|
||||||
//
|
//
|
||||||
// Included by kernel_v2.cu. No standalone compilation.
|
// Included by uniform.cu. No standalone compilation.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
#ifndef CELERIS_STEP_ESOPULL_SYNC_CU
|
#ifndef CELERIS_STEP_ESOPULL_SYNC_CU
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// CelerisLab – step/init_flow.cu
|
// CelerisLab – step/init_flow.cu
|
||||||
// Channel-flow initialization kernels (double-buffer and EsoPull).
|
// Channel-flow initialization kernels (double-buffer and EsoPull).
|
||||||
// Included by kernel_v2.cu after all headers. No standalone compilation.
|
// Included by uniform.cu after all headers. No standalone compilation.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
#ifndef CELERIS_STEP_INIT_FLOW_CU
|
#ifndef CELERIS_STEP_INIT_FLOW_CU
|
||||||
@@ -73,9 +73,9 @@ __device__ __forceinline__ uint16_t finalize_domain_flag(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// InitTubeFlow_v2 — double-buffer channel initialization
|
// InitTubeFlow — double-buffer channel initialization
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
__global__ void InitTubeFlow_v2(uint16_t* flag, fpxx* fi)
|
__global__ void InitTubeFlow(uint16_t* flag, fpxx* fi)
|
||||||
{
|
{
|
||||||
#if DIM == 2
|
#if DIM == 2
|
||||||
unsigned int x, y; unsigned long k;
|
unsigned int x, y; unsigned long k;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// CelerisLab – step/one_step_double.cu
|
// CelerisLab – step/one_step_double.cu
|
||||||
// Double-buffer PULL streaming step kernel.
|
// Double-buffer PULL streaming step kernel.
|
||||||
// Included by kernel_v2.cu. No standalone compilation.
|
// Included by uniform.cu. No standalone compilation.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
#ifndef CELERIS_STEP_ONE_STEP_DOUBLE_CU
|
#ifndef CELERIS_STEP_ONE_STEP_DOUBLE_CU
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// CelerisLab – step/one_step_esopull.cu
|
// CelerisLab – step/one_step_esopull.cu
|
||||||
// Esoteric-Pull single-buffer step kernel.
|
// Esoteric-Pull single-buffer step kernel.
|
||||||
// Included by kernel_v2.cu. No standalone compilation.
|
// Included by uniform.cu. No standalone compilation.
|
||||||
//
|
//
|
||||||
// CURVED BC: supported via PrepareEsoPullCurvedKernel + ApplyEsoPullCurvedKernel
|
// CURVED BC: supported via PrepareEsoPullCurvedKernel + ApplyEsoPullCurvedKernel
|
||||||
// in aux_kernels.cu, launched by stepper.py before EsoPullStep.
|
// in aux_kernels.cu, launched by stepper.py before EsoPullStep.
|
||||||
|
|||||||
@@ -21,6 +21,14 @@
|
|||||||
// i.e. the population traveling in direction i at node n
|
// i.e. the population traveling in direction i at node n
|
||||||
// arrived from the node at n - c_i = neighbor in direction opp(i)
|
// 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,
|
__device__ __forceinline__ void stream_pull_load(unsigned long n,
|
||||||
float* __restrict__ f,
|
float* __restrict__ f,
|
||||||
const fpxx* __restrict__ fi,
|
const fpxx* __restrict__ fi,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// CelerisLab – kernel_v2.cu
|
// CelerisLab – uniform.cu
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Compilation entry point (Stage 2 Architecture).
|
// Compilation entry point (Stage 2 Architecture).
|
||||||
//
|
//
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
//
|
//
|
||||||
// File layout (each <300 lines):
|
// File layout (each <300 lines):
|
||||||
// operators/helpers.cuh — bounce_back_swap, collide_dispatch
|
// operators/helpers.cuh — bounce_back_swap, collide_dispatch
|
||||||
// step/init_flow.cu — InitTubeFlow_v2, InitEsoPull
|
// step/init_flow.cu — InitTubeFlow, InitEsoPull
|
||||||
// step/one_step_double.cu — OneStep (pull double-buffer)
|
// step/one_step_double.cu — OneStep (pull double-buffer)
|
||||||
// step/one_step_esopull.cu — EsoPullStep (single-buffer)
|
// step/one_step_esopull.cu — EsoPullStep (single-buffer)
|
||||||
// step/aux_kernels.cu — CurvedBoundaryKernel, SensorKernel
|
// step/aux_kernels.cu — CurvedBoundaryKernel, SensorKernel
|
||||||
@@ -14,15 +14,16 @@ from threading import RLock
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pycuda.driver as cuda
|
import pycuda.driver as cuda
|
||||||
|
|
||||||
from ..cuda import compiler_v2 as compiler
|
from ..cuda import compiler
|
||||||
from .blocks import BlockLayout2D
|
from .blocks import (BlockLayout2D, launch_curved_boundary, launch_halo_copy, level_config,
|
||||||
|
static_circle_tables)
|
||||||
from .channel import channel_masks, validate_channel_config, validate_channel_transfers
|
from .channel import channel_masks, validate_channel_config, validate_channel_transfers
|
||||||
from ..body.jets import jet_link_velocity
|
from ..body.jets import jet_link_velocity
|
||||||
from .block_stepper import MAX_GRID_DIM
|
from .hierarchy import build_hierarchy
|
||||||
from .hierarchy import build_hierarchy, is_rectangle
|
|
||||||
from .refinement import ACTIVE, HALO, FixedPatch2D, FixedPatchSet2D, prepare_fine_boundary
|
from .refinement import ACTIVE, HALO, FixedPatch2D, FixedPatchSet2D, prepare_fine_boundary
|
||||||
from .refinement_tables import (blocked_base_tables, blocked_fine_tables, # noqa: F401 (re-export)
|
from .refinement_tables import (blocked_base_tables, blocked_fine_tables, # noqa: F401 (re-export)
|
||||||
blocked_kind, projected_explosion_tables, _prepare_blocked_boundary,
|
blocked_kind, projected_explosion_tables, _prepare_blocked_boundary,
|
||||||
|
_BlockedBoundary,
|
||||||
BLOCKED_BOUNDARY_DEVICE_COLUMNS)
|
BLOCKED_BOUNDARY_DEVICE_COLUMNS)
|
||||||
from .descriptors import SOLID
|
from .descriptors import SOLID
|
||||||
from .raw_exchange import summarize_exchange
|
from .raw_exchange import summarize_exchange
|
||||||
@@ -65,148 +66,102 @@ def _level_name(level):
|
|||||||
return ("base", "fine", "child")[level] if level < 3 else f"level {level}"
|
return ("base", "fine", "child")[level] if level < 3 else f"level {level}"
|
||||||
|
|
||||||
|
|
||||||
def _legacy_levels(bounds, *, fine_block, base_block, child, child_block, child_rectangles_in_parent,
|
MAX_GRID_DIM = 65535 # CUDA gridDim.y/z limit (rows, blocks)
|
||||||
fine_objects, jets, child_objects, child_jets):
|
|
||||||
"""Normalise the original two/three-level keywords to per-level lists (original errors)."""
|
|
||||||
if child is not None and child_rectangles_in_parent is not None:
|
|
||||||
raise ValueError("child and child_rectangles_in_parent are mutually exclusive")
|
|
||||||
nested = child is not None or child_rectangles_in_parent is not None
|
|
||||||
bounds = tuple(bounds)
|
|
||||||
if not is_rectangle(bounds) and (fine_block is None or base_block is None):
|
|
||||||
raise ValueError("several patches require fine_block and base_block")
|
|
||||||
fine_objects, jets = tuple(fine_objects), tuple(jets)
|
|
||||||
child_objects, child_jets = tuple(child_objects), tuple(child_jets)
|
|
||||||
if not nested and (child_objects or child_jets):
|
|
||||||
raise ValueError("child_objects/child_jets require a nested child")
|
|
||||||
if base_block is not None and fine_block is None:
|
|
||||||
raise ValueError("base_block requires fine_block")
|
|
||||||
refinements, blocks = [bounds], [base_block, fine_block]
|
|
||||||
objects, level_jets = [(), fine_objects], [(), jets]
|
|
||||||
if nested:
|
|
||||||
if child is not None and not is_rectangle(bounds):
|
|
||||||
raise ValueError("scalar child requires one parent patch; use child_rectangles_in_parent")
|
|
||||||
if fine_block is None or base_block is None or child_block is None:
|
|
||||||
raise ValueError("a nested child needs fine_block, base_block and child_block")
|
|
||||||
if fine_objects or jets:
|
|
||||||
raise ValueError("a nested child supports bodies/Jets only on L2; use child_objects/child_jets")
|
|
||||||
refinements.append(tuple(child) if child is not None else tuple(child_rectangles_in_parent))
|
|
||||||
blocks.append(child_block)
|
|
||||||
objects.append(child_objects)
|
|
||||||
level_jets.append(child_jets)
|
|
||||||
return refinements, blocks, objects, level_jets
|
|
||||||
|
|
||||||
|
|
||||||
def _initial_levels(level_g, coarse_g, fine_g, child_g):
|
|
||||||
"""One tuple of per-level arrays from initialize()'s positional or legacy named form."""
|
|
||||||
if coarse_g is None and fine_g is None:
|
|
||||||
if child_g is not None and len(level_g) != 2:
|
|
||||||
raise TypeError("child_g needs exactly the coarse and fine arrays before it")
|
|
||||||
return tuple(level_g) + (() if child_g is None else (child_g,))
|
|
||||||
if level_g:
|
|
||||||
raise TypeError("pass level arrays positionally or as coarse_g/fine_g/child_g, not both")
|
|
||||||
if coarse_g is None or fine_g is None:
|
|
||||||
raise TypeError("coarse_g and fine_g are both required in the named form")
|
|
||||||
return (coarse_g, fine_g) + (() if child_g is None else (child_g,))
|
|
||||||
|
|
||||||
|
|
||||||
def _check_levels(refinements, blocks, objects, jets):
|
def _check_levels(refinements, blocks, objects, jets):
|
||||||
"""Candidate limits shared by the list and the legacy entry points."""
|
"""Candidate limits of the level lists, checked before any device work."""
|
||||||
depth = len(refinements) + 1
|
depth = len(refinements) + 1
|
||||||
if depth < 2:
|
|
||||||
raise ValueError("at least one refinement level is required")
|
|
||||||
for name, values in (("level_blocks", blocks), ("level_objects", objects), ("level_jets", jets)):
|
for name, values in (("level_blocks", blocks), ("level_objects", objects), ("level_jets", jets)):
|
||||||
if len(values) != depth:
|
if len(values) != depth:
|
||||||
raise ValueError(f"{name} needs one entry per level ({depth})")
|
raise ValueError(f"{name} needs one entry per level ({depth})")
|
||||||
for level, block in enumerate(blocks):
|
for level, block in enumerate(blocks):
|
||||||
if block is not None and len(block) != 2:
|
if (not isinstance(block, tuple) or len(block) != 2
|
||||||
raise ValueError(f"{_level_name(level)}_block must be (by, bx)")
|
or any(isinstance(v, (bool, np.bool_)) or not isinstance(v, Integral) or v <= 0
|
||||||
|
for v in block)):
|
||||||
|
raise ValueError(f"level_blocks[{level}] must be a tile (by, bx) of positive integers; "
|
||||||
|
"a whole-level tile ((ny, nx) at the root, the fine window shape "
|
||||||
|
"for one rectangle) always divides its level")
|
||||||
|
if depth == 1: # one blocked level: the root carries the static circles
|
||||||
|
if jets[0]:
|
||||||
|
raise ValueError("a single level has no Jets")
|
||||||
|
return
|
||||||
if objects[0] or jets[0]:
|
if objects[0] or jets[0]:
|
||||||
raise ValueError("base-level bodies/Jets are not supported")
|
raise ValueError("base-level bodies/Jets are not supported")
|
||||||
if any(blocks[level - 1] is not None and blocks[level] is None for level in range(1, depth)):
|
|
||||||
raise ValueError("a blocked level requires blocked refined levels below it")
|
|
||||||
if not is_rectangle(refinements[0]) and (blocks[0] is None or blocks[1] is None):
|
|
||||||
raise ValueError("several patches require fine_block and base_block")
|
|
||||||
if depth > 2:
|
if depth > 2:
|
||||||
if any(block is None for block in blocks):
|
|
||||||
raise ValueError("three or more levels must all be blocked")
|
|
||||||
if any(objects[level] or jets[level] for level in range(1, depth - 1)):
|
if any(objects[level] or jets[level] for level in range(1, depth - 1)):
|
||||||
raise ValueError("bodies/Jets are supported only on the finest level of a nested hierarchy")
|
raise ValueError("bodies/Jets are supported only on the finest level of a nested hierarchy")
|
||||||
|
|
||||||
|
|
||||||
class FixedPatchStepper:
|
class FixedPatchStepper:
|
||||||
"""D2Q9/SRT/FP32, one unforced internal 2:1 patch in an x-periodic box.
|
"""D2Q9 SRT/MRT FP32 fixed 2:1 refinement hierarchy, built from level lists.
|
||||||
|
|
||||||
|
``refinements[l]`` gives level l+1 in level-l integer cells: one rectangle
|
||||||
|
(x0, y0, x1, y1) (FixedPatch2D, a fine window with its own origin) or a
|
||||||
|
sequence of rectangles (FixedPatchSet2D, a dependency-validated union on one
|
||||||
|
level-wide grid, public fine arrays (2*ny, 2*nx), NaN outside ACTIVE cells);
|
||||||
|
see lbm.hierarchy. ``level_blocks`` (required), ``level_objects`` and
|
||||||
|
``level_jets`` hold one entry per level, L0 included (L0 bodies/Jets are not
|
||||||
|
supported).
|
||||||
|
``refinements=[]`` is one blocked level: periodic SRT root without LES,
|
||||||
|
Jets or exchange observation; its ``level_objects[0]`` are stationary
|
||||||
|
passive circles (static BFL).
|
||||||
|
|
||||||
``coarse_boundary`` is ``periodic`` (default) or ``slot_reservoir``:
|
``coarse_boundary`` is ``periodic`` (default) or ``slot_reservoir``:
|
||||||
a bottom on-site slot and top fixed-density reservoir from ``cfg``.
|
a bottom on-site slot and top fixed-density reservoir from ``cfg``.
|
||||||
``channel`` requires all-blocked levels and matches uniform SRT Zou-He
|
``channel`` matches uniform SRT Zou-He
|
||||||
inlet, pressure-NEQ outlet and stationary bounce-back y walls. Boundary
|
inlet, pressure-NEQ outlet and stationary bounce-back y walls. Boundary
|
||||||
ghosts evolve internally but public downloads exclude them; initialization
|
ghosts evolve internally but public downloads exclude them; initialization
|
||||||
must supply their finite positive-density state. Transfers must avoid the
|
must supply their finite positive-density state. Transfers must avoid the
|
||||||
actual channel closure footprint. Channel remapping is not supported.
|
actual channel closure footprint. Channel remapping is not supported.
|
||||||
Periodic mode does not apply the wall/slot boundary fields in ``cfg``.
|
Periodic mode does not apply the wall/slot boundary fields in ``cfg``.
|
||||||
Outer walls remain coarse; fine halo donors must be ordinary fluid.
|
Outer walls remain coarse; fine halo donors must be ordinary fluid.
|
||||||
``fine_objects`` accepts fixed circles in coarse physical coordinates;
|
|
||||||
velocities are in coarse lattice units (dx=dt=1). Independent local copies
|
|
||||||
are projected at fine cell centers. Their complete stencil must be active
|
|
||||||
and disjoint from the actual two-hop transfer footprint.
|
|
||||||
These use the edge/cell-center frame (coarse centers I+.5), whereas ordinary Simulation objects use the integer-node frame.
|
|
||||||
|
|
||||||
``initialize(coarse_g, fine_g)`` accepts physical Q-major (9,ny,nx) arrays
|
Bodies are fixed circles in root (L0) physical coordinates; velocities are
|
||||||
already in postcollision phase. It does not collide or build channel flags.
|
in root lattice units (dx=dt=1). Independent local copies are projected at
|
||||||
Every ``step()`` publishes only after both levels complete on one stream.
|
fine cell centers; their complete stencil must be active and disjoint from
|
||||||
On a device/launch failure the instance becomes unusable: close and recreate
|
the actual two-hop transfer footprint. These use the edge/cell-center frame
|
||||||
it, rather than pretending the partially advanced fine buffer rolled back.
|
(coarse centers I+.5), whereas ordinary Simulation objects use the
|
||||||
|
integer-node frame. Jets (body.jets.Jet) use the same frame and drive cut
|
||||||
|
links selected at their true hits; ``set_jet_amplitude(index, amplitude,
|
||||||
|
level=l)`` re-uploads the per-link velocity column only.
|
||||||
|
|
||||||
|
``initialize(*level_g)`` accepts one physical Q-major (9, ny, nx) array per
|
||||||
|
level, already in postcollision phase. It does not collide or build channel
|
||||||
|
flags. Every ``step()`` publishes only after all levels complete on one
|
||||||
|
stream. On a device/launch failure the instance becomes unusable: close and
|
||||||
|
recreate it, rather than pretending a partially advanced buffer rolled back.
|
||||||
|
|
||||||
|
Every level is stored in blocked.cu storage with its ``level_blocks`` tile
|
||||||
|
(by, bx): the ENTIRE level shape, including the four-cell patch ghost band,
|
||||||
|
is partitioned by a BlockLayout2D with one-cell same-level halos; the tile
|
||||||
|
must divide the level (a whole-level tile always does). Patch kinds stay
|
||||||
|
per cell (OUTER invalid, INNER stream only, ACTIVE collide); halos are
|
||||||
|
gathered before each hop. Public arrays are dense (9, ny, nx); only the
|
||||||
|
allocation is blocked. Bodies are stationary circles, optionally
|
||||||
|
surface-actuated: prepare_fine_boundary decides legality on the dense grid,
|
||||||
|
then cut links are staged receiver-local per hop: halo -> BFL -> step, same
|
||||||
|
buffer. A blocked parent gathers halos before receiver-local coalescence
|
||||||
|
staging and pull/collision; covered cells are poisoned on every step.
|
||||||
|
|
||||||
``fine_block=(by, bx)`` (opt-in) stores the fine level in
|
|
||||||
blocks.cu storage: the ENTIRE fine_shape, including the four-cell patch
|
|
||||||
ghost band, is partitioned by a non-periodic BlockLayout2D with one-cell
|
|
||||||
same-level halos. Patch kinds stay per cell (OUTER invalid, INNER stream
|
|
||||||
only, ACTIVE collide); halos are gathered before each fine hop. Public
|
|
||||||
arrays keep the dense (9, ny, nx) fine shape; only the allocation differs.
|
|
||||||
Blocked fine_objects are stationary circles, optionally surface-actuated:
|
|
||||||
prepare_fine_boundary decides legality on the dense fine grid, then cut
|
|
||||||
links are staged receiver-local per hop: halo -> BFL -> step, same buffer.
|
|
||||||
``base_block=(by, bx)`` also blocks the parent; it requires ``fine_block``.
|
|
||||||
Parent halos precede receiver-local coalescence staging and pull/collision;
|
|
||||||
covered cells are explicitly poisoned on every step. Public shapes stay dense.
|
|
||||||
``observe_raw_exchange=True`` records blocked physical cut-link exchange
|
``observe_raw_exchange=True`` records blocked physical cut-link exchange
|
||||||
without changing populations. read_raw_exchange reports the last complete
|
without changing populations. read_raw_exchange reports the last complete
|
||||||
base interval or last substep in base-lattice units per unit depth. This
|
base interval or last substep in base-lattice units per unit depth. This
|
||||||
is boundary momentum reaction, not a validated nozzle/stress load. Dense
|
is boundary momentum reaction, not a validated nozzle/stress load.
|
||||||
body layouts are not supported by this new diagnostic in this candidate.
|
|
||||||
``observe_window_exchange=True`` (requires observe_raw_exchange) adds a second
|
``observe_window_exchange=True`` (requires observe_raw_exchange) adds a second
|
||||||
per-link FP64 ledger accumulated by the same kernel between
|
per-link FP64 ledger accumulated by the same kernel between
|
||||||
start_exchange_window() and read_exchange_window(); disabled by default.
|
start_exchange_window() and read_exchange_window(); disabled by default.
|
||||||
``jets`` (body.jets.Jet) use the same edge frame as ``fine_objects`` and
|
|
||||||
drive fine cut links selected at their true hits; ``set_jet_amplitude``
|
|
||||||
re-uploads the per-link velocity column only.
|
|
||||||
|
|
||||||
Several patches: pass ``bounds`` as a sequence of (x0, y0, x1, y1)
|
Nested levels: a child set's coordinates index the allocated parent array,
|
||||||
rectangles (FixedPatchSet2D, dependency-validated union). This requires
|
including ghost bands and holes: root edge = parent fine_origin + index / 2
|
||||||
fine_block and base_block; fine_objects/jets are projected once onto the
|
(zero origin for a parent set; a scalar parent keeps its shifted window
|
||||||
level-wide fine grid (each circle inside the union, body ids = object
|
origin). A scalar child requires a scalar parent. Rectangle unions impose no
|
||||||
positions, jet and writer checks level-wide), and
|
fixed gap rule: adjacent, overlapping and concave unions are accepted when
|
||||||
uses one level-wide fine grid (2*ny, 2*nx): public fine arrays have that
|
their actual transfer dependencies close (FixedPatchSet2D /
|
||||||
shape, NaN outside the patches' ACTIVE cells. The step sequence is the
|
nest_patch_set). All regions share one buffer pair per level and the fixed
|
||||||
blocked one above, with unique union transfers in one launch each.
|
schedule of 2**l hops per base step at level l. Bodies/Jets are supported only on the
|
||||||
|
finest level of a nested hierarchy (L2 dx=dt=.25 in root units, unchanged
|
||||||
Three-level separated unions: ``child_rectangles_in_parent=[...]`` is
|
lattice velocity); every whole circle and its BFL stencil must fit strictly
|
||||||
mutually exclusive with scalar ``child`` and accepts either parent form.
|
away from that level's transfer footprint.
|
||||||
Coordinates index the allocated L1 array, including ghost bands and holes:
|
|
||||||
root edge = patch.fine_origin + L1 index / 2. A parent set has origin zero;
|
|
||||||
a scalar parent retains its shifted window origin. The child set's public
|
|
||||||
extent is twice the allocated L1 shape, even though storage is sparse.
|
|
||||||
Each pair requires gap >= 4 in its own parent-cell units. All regions share
|
|
||||||
one buffer pair per level and the fixed 1:2:4 schedule; L2-only bodies/Jets
|
|
||||||
below apply unchanged, with object IDs assigned over the whole level.
|
|
||||||
|
|
||||||
Three-level single chain: ``child`` bounds use L1 window cells; all three
|
|
||||||
levels must be blocked. ``child_objects`` and ``child_jets`` use the L0
|
|
||||||
edge frame, with L2 dx=dt=.25 and unchanged lattice velocity. L1 bodies
|
|
||||||
and Jets remain unsupported in a chain. Every whole circle and its BFL
|
|
||||||
stencil must fit strictly away from the L2 transfer footprint. L2 runs
|
|
||||||
halo -> BFL -> step four times per base step. Runtime child Jet changes
|
|
||||||
require ``set_jet_amplitude(index, amplitude, level=2)``.
|
|
||||||
|
|
||||||
``explosion="projected_linear"`` selects experimental transverse linear
|
``explosion="projected_linear"`` selects experimental transverse linear
|
||||||
reconstruction on each adjacent pair. Default "constant" is unchanged.
|
reconstruction on each adjacent pair. Default "constant" is unchanged.
|
||||||
@@ -214,60 +169,45 @@ class FixedPatchStepper:
|
|||||||
bodies/Jets or LES; this backend supplies no forcing. Geometry is generic,
|
bodies/Jets or LES; this backend supplies no forcing. Geometry is generic,
|
||||||
but union/multilevel GPU and matched-physics qualification remain pending.
|
but union/multilevel GPU and matched-physics qualification remain pending.
|
||||||
|
|
||||||
Level lists (any depth): ``refinements[l]`` gives level l+1 in level-l
|
One recursive hop advances every level: a child explodes from its parent's
|
||||||
integer cells (scalar window or rectangle list, see lbm.hierarchy);
|
current hop source, takes two hops, then the parent gathers halos, stages
|
||||||
``level_blocks``/``level_objects``/``level_jets`` hold one entry per level,
|
restriction, applies BFL and steps.
|
||||||
L0 included. They are mutually exclusive with the legacy keywords above,
|
|
||||||
which are normalised to the same lists. One recursive hop advances every
|
|
||||||
level: a child explodes from its parent's current hop source, takes two
|
|
||||||
hops, then the parent gathers halos, stages restriction, applies BFL and
|
|
||||||
steps. Depth >= 3 must be fully blocked with bodies/Jets on the finest level.
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, cfg, bounds=None, *, context, coarse_boundary="periodic", fine_objects=(),
|
def __init__(self, cfg, *, context, refinements, coarse_boundary="periodic",
|
||||||
fine_block=None, base_block=None, jets=(), child=None, child_block=None,
|
level_blocks=None, level_objects=None, level_jets=None,
|
||||||
child_objects=(), child_jets=(), child_rectangles_in_parent=None,
|
|
||||||
refinements=None, level_blocks=None, level_objects=None, level_jets=None,
|
|
||||||
observe_raw_exchange=False, observe_window_exchange=False, explosion="constant"):
|
observe_raw_exchange=False, observe_window_exchange=False, explosion="constant"):
|
||||||
if HALO != 4:
|
if HALO != 4:
|
||||||
raise RuntimeError("refinement.cu requires a four-cell fine halo")
|
raise RuntimeError("the refinement transfers require a four-cell fine halo")
|
||||||
if coarse_boundary not in ("periodic", "slot_reservoir", "channel"):
|
if coarse_boundary not in ("periodic", "slot_reservoir", "channel"):
|
||||||
raise ValueError("coarse_boundary must be periodic, slot_reservoir or channel")
|
raise ValueError("coarse_boundary must be periodic, slot_reservoir or channel")
|
||||||
if refinements is None:
|
depth = len(refinements) + 1
|
||||||
if bounds is None:
|
if level_blocks is None:
|
||||||
raise ValueError("bounds or refinements is required")
|
raise ValueError("level_blocks is required: one tile (by, bx) per level; a whole-level "
|
||||||
if level_blocks is not None or level_objects is not None or level_jets is not None:
|
"tile ((ny, nx) at the root, the fine window shape for one rectangle) "
|
||||||
raise ValueError("level_blocks/level_objects/level_jets require refinements")
|
"always divides its level")
|
||||||
refinements, level_blocks, level_objects, level_jets = _legacy_levels(
|
level_blocks = list(level_blocks)
|
||||||
bounds, fine_block=fine_block, base_block=base_block, child=child,
|
level_objects = [()] * depth if level_objects is None else list(level_objects)
|
||||||
child_block=child_block, child_rectangles_in_parent=child_rectangles_in_parent,
|
level_jets = [()] * depth if level_jets is None else list(level_jets)
|
||||||
fine_objects=fine_objects, jets=jets, child_objects=child_objects,
|
|
||||||
child_jets=child_jets)
|
|
||||||
else:
|
|
||||||
if (bounds is not None or child is not None or child_rectangles_in_parent is not None
|
|
||||||
or fine_block is not None or base_block is not None or child_block is not None
|
|
||||||
or tuple(fine_objects) or tuple(jets) or tuple(child_objects) or tuple(child_jets)):
|
|
||||||
raise ValueError("refinements and the legacy patch keywords are mutually exclusive")
|
|
||||||
depth = len(refinements) + 1
|
|
||||||
level_blocks = [None] * depth if level_blocks is None else list(level_blocks)
|
|
||||||
level_objects = [()] * depth if level_objects is None else list(level_objects)
|
|
||||||
level_jets = [()] * depth if level_jets is None else list(level_jets)
|
|
||||||
level_objects = [tuple(o) for o in level_objects]
|
level_objects = [tuple(o) for o in level_objects]
|
||||||
level_jets = [tuple(j) for j in level_jets]
|
level_jets = [tuple(j) for j in level_jets]
|
||||||
level_blocks = [None if b is None else tuple(b) for b in level_blocks]
|
level_blocks = [tuple(b) if isinstance(b, (list, tuple)) else b for b in level_blocks]
|
||||||
_check_levels(refinements, level_blocks, level_objects, level_jets)
|
_check_levels(refinements, level_blocks, level_objects, level_jets)
|
||||||
depth = len(refinements) + 1
|
depth = len(refinements) + 1
|
||||||
if explosion not in ("constant", "projected_linear"):
|
if explosion not in ("constant", "projected_linear"):
|
||||||
raise ValueError("explosion must be constant or projected_linear")
|
raise ValueError("explosion must be constant or projected_linear")
|
||||||
if explosion == "projected_linear" and (
|
if explosion == "projected_linear" and (
|
||||||
coarse_boundary != "periodic" or any(b is None for b in level_blocks)
|
coarse_boundary != "periodic"
|
||||||
or any(level_objects) or any(level_jets)
|
or any(level_objects) or any(level_jets)
|
||||||
or cfg.collision != "SRT" or cfg.les_enabled):
|
or cfg.collision != "SRT" or cfg.les_enabled):
|
||||||
raise ValueError("projected explosion requires periodic all-blocked SRT levels "
|
raise ValueError("projected explosion requires periodic SRT levels "
|
||||||
"without bodies/Jets or LES")
|
"without bodies/Jets or LES")
|
||||||
self._explosion = explosion
|
self._explosion = explosion
|
||||||
self._ddf_shifting = bool(cfg.ddf_shifting)
|
self._ddf_shifting = bool(cfg.ddf_shifting)
|
||||||
if self._ddf_shifting and any(b is None for b in level_blocks):
|
if depth == 1 and (coarse_boundary != "periodic" or cfg.collision != "SRT" or cfg.les_enabled
|
||||||
raise ValueError("shifted populations require all-blocked levels; dense/mixed is unsupported")
|
or explosion != "constant"
|
||||||
|
or observe_raw_exchange or observe_window_exchange):
|
||||||
|
raise ValueError("a single level requires a periodic SRT root without LES, projection "
|
||||||
|
"or exchange observation")
|
||||||
self._channel = coarse_boundary == "channel"
|
self._channel = coarse_boundary == "channel"
|
||||||
if self._channel:
|
if self._channel:
|
||||||
validate_channel_config(cfg, level_blocks)
|
validate_channel_config(cfg, level_blocks)
|
||||||
@@ -284,29 +224,29 @@ class FixedPatchStepper:
|
|||||||
raise ValueError("slot_reservoir requires y0 >= 4 and y1 <= ny-4")
|
raise ValueError("slot_reservoir requires y0 >= 4 and y1 <= ny-4")
|
||||||
self._layouts = []
|
self._layouts = []
|
||||||
for level, block in enumerate(level_blocks):
|
for level, block in enumerate(level_blocks):
|
||||||
layout = None
|
if level == 0:
|
||||||
if block is not None:
|
layout = BlockLayout2D.build(cfg.ny, cfg.nx, *block,
|
||||||
if level == 0:
|
periodic=(True, not self._wall_aware))
|
||||||
layout = BlockLayout2D.build(cfg.ny, cfg.nx, *block,
|
elif isinstance(pairs[level - 1], FixedPatchSet2D):
|
||||||
periodic=(True, not self._wall_aware))
|
layout = pairs[level - 1].fine_layout(block)
|
||||||
elif isinstance(pairs[level - 1], FixedPatchSet2D):
|
else:
|
||||||
layout = pairs[level - 1].fine_layout(block)
|
layout = BlockLayout2D.build(*pairs[level - 1].fine_shape, *block,
|
||||||
else:
|
periodic=(False, False))
|
||||||
layout = BlockLayout2D.build(*pairs[level - 1].fine_shape, *block,
|
if layout.nblocks > MAX_GRID_DIM or layout.local_shape[0] > MAX_GRID_DIM:
|
||||||
periodic=(False, False))
|
raise ValueError(f"{_level_name(level)} blocks exceed launch limits")
|
||||||
if layout.nblocks > MAX_GRID_DIM or layout.local_shape[0] > MAX_GRID_DIM:
|
|
||||||
raise ValueError(f"{_level_name(level)} blocks exceed launch limits")
|
|
||||||
self._layouts.append(layout)
|
self._layouts.append(layout)
|
||||||
if self._channel:
|
if self._channel:
|
||||||
base = blocked_base_tables(pairs[0], self._layouts[0], np.argsort(pairs[0].coalesce_coarse))
|
base = blocked_base_tables(pairs[0], self._layouts[0], np.argsort(pairs[0].coalesce_coarse))
|
||||||
validate_channel_transfers(pairs[0], self._layouts[0], base[1])
|
validate_channel_transfers(pairs[0], self._layouts[0], base[1])
|
||||||
# Kernel configurations per level (geometry configs live in self.hierarchy).
|
# Kernel configurations per level (geometry configs live in self.hierarchy).
|
||||||
fine_cfg = replace(cfg, nx=pairs[0].fine_shape[1], ny=pairs[0].fine_shape[0],
|
self._level_cfgs = [replace(cfg)]
|
||||||
viscosity=2 * cfg.viscosity)
|
if pairs:
|
||||||
if self._wall_aware:
|
fine_cfg = replace(cfg, nx=pairs[0].fine_shape[1], ny=pairs[0].fine_shape[0],
|
||||||
fine_cfg = replace(fine_cfg, y_wall_bc="bounce_back", top_wall_bc="velocity",
|
viscosity=2 * cfg.viscosity)
|
||||||
wall_slot_width=0, wall_normal_velocity=0.0, velocity=0.0)
|
if self._wall_aware:
|
||||||
self._level_cfgs = [replace(cfg), fine_cfg]
|
fine_cfg = replace(fine_cfg, y_wall_bc="bounce_back", top_wall_bc="velocity",
|
||||||
|
wall_slot_width=0, wall_normal_velocity=0.0, velocity=0.0)
|
||||||
|
self._level_cfgs.append(fine_cfg)
|
||||||
for pair in pairs[1:]:
|
for pair in pairs[1:]:
|
||||||
parent = self._level_cfgs[-1]
|
parent = self._level_cfgs[-1]
|
||||||
self._level_cfgs.append(replace(parent, nx=pair.fine_shape[1], ny=pair.fine_shape[0],
|
self._level_cfgs.append(replace(parent, nx=pair.fine_shape[1], ny=pair.fine_shape[0],
|
||||||
@@ -331,9 +271,6 @@ class FixedPatchStepper:
|
|||||||
spacings.append(spacing)
|
spacings.append(spacing)
|
||||||
if not isinstance(observe_raw_exchange, bool):
|
if not isinstance(observe_raw_exchange, bool):
|
||||||
raise ValueError("observe_raw_exchange must be bool")
|
raise ValueError("observe_raw_exchange must be bool")
|
||||||
if observe_raw_exchange and any(b is not None and layout is None
|
|
||||||
for b, layout in zip(self._boundary_hosts, self._layouts)):
|
|
||||||
raise ValueError("raw exchange currently requires blocked body levels")
|
|
||||||
self._observe_raw_exchange = observe_raw_exchange
|
self._observe_raw_exchange = observe_raw_exchange
|
||||||
if not isinstance(observe_window_exchange, bool):
|
if not isinstance(observe_window_exchange, bool):
|
||||||
raise ValueError("observe_window_exchange must be bool")
|
raise ValueError("observe_window_exchange must be bool")
|
||||||
@@ -347,12 +284,18 @@ class FixedPatchStepper:
|
|||||||
self._exchange_centers = tuple(tuple(tuple(o.geometry.center) for o in objects)
|
self._exchange_centers = tuple(tuple(tuple(o.geometry.center) for o in objects)
|
||||||
for objects in level_objects)
|
for objects in level_objects)
|
||||||
self._level_jets = level_jets
|
self._level_jets = level_jets
|
||||||
self._level_boundaries = [None] + [
|
root = None
|
||||||
|
if depth == 1 and level_objects[0]:
|
||||||
|
tables = static_circle_tables(self._layouts[0], level_objects[0])
|
||||||
|
root = _BlockedBoundary(None, tables, (), False)
|
||||||
|
self._level_boundaries = [root] + [
|
||||||
_prepare_blocked_boundary(self._boundary_hosts[level], self._layouts[level],
|
_prepare_blocked_boundary(self._boundary_hosts[level], self._layouts[level],
|
||||||
self._level_cfgs[level], level_jets[level],
|
self._level_cfgs[level], level_jets[level],
|
||||||
_shifted_blocked=self._ddf_shifting)
|
_shifted_blocked=self._ddf_shifting)
|
||||||
for level in range(1, depth)]
|
for level in range(1, depth)]
|
||||||
self._leaf = [kind == ACTIVE for kind in self.hierarchy.kinds]
|
self._leaf = [kind == ACTIVE for kind in self.hierarchy.kinds]
|
||||||
|
if root is not None:
|
||||||
|
self._leaf[0] &= ~root.tables["solid"]
|
||||||
for level in range(1, depth):
|
for level in range(1, depth):
|
||||||
host = self._boundary_hosts[level]
|
host = self._boundary_hosts[level]
|
||||||
if host is not None:
|
if host is not None:
|
||||||
@@ -370,11 +313,12 @@ class FixedPatchStepper:
|
|||||||
self._allocations = []
|
self._allocations = []
|
||||||
self._levels, self._pairs = [], []
|
self._levels, self._pairs = [], []
|
||||||
self._stream = None
|
self._stream = None
|
||||||
self._fine_flags = self._fine_curved = self._curved = None
|
|
||||||
self._link_tables = {}
|
|
||||||
self._block = (cfg.threads_per_block, 1, 1)
|
self._block = (cfg.threads_per_block, 1, 1)
|
||||||
with self.ctx.activate():
|
with self.ctx.activate():
|
||||||
try:
|
try:
|
||||||
|
free, _ = cuda.mem_get_info()
|
||||||
|
if self._planned_bytes() > free: # before any stream, module or buffer
|
||||||
|
raise MemoryError("level buffers exceed free device memory")
|
||||||
self._stream = cuda.Stream()
|
self._stream = cuda.Stream()
|
||||||
for level in range(depth):
|
for level in range(depth):
|
||||||
self._levels.append(self._build_level(level))
|
self._levels.append(self._build_level(level))
|
||||||
@@ -396,21 +340,6 @@ class FixedPatchStepper:
|
|||||||
b.window_integral = self._allocate(count * 4 * 8)
|
b.window_integral = self._allocate(count * 4 * 8)
|
||||||
b.accumulate = level.module.get_function("BlockAccumulateExchange")
|
b.accumulate = level.module.get_function("BlockAccumulateExchange")
|
||||||
b.kernel = level.module.get_function("BlockCurvedBoundary")
|
b.kernel = level.module.get_function("BlockCurvedBoundary")
|
||||||
if self._level_boundaries[1] is not None:
|
|
||||||
b = self._level_boundaries[1]
|
|
||||||
self._fine_flags, self._link_tables, self._curved = b.flags, b.device, b.kernel
|
|
||||||
elif self._boundary_hosts[1] is not None:
|
|
||||||
from .curved_links import CurvedLinkSoA
|
|
||||||
boundary = self._boundary_hosts[1]
|
|
||||||
self._fine_flags = self._upload(boundary.flags)
|
|
||||||
# SoA owns its allocations; never also register them in _allocations.
|
|
||||||
self._fine_curved = CurvedLinkSoA()
|
|
||||||
self._fine_curved.assign_host(*boundary.curved_host)
|
|
||||||
self._fine_curved.assign_surface(boundary.objects, fine_cfg, boundary.flags,
|
|
||||||
jets=self._level_jets[1],
|
|
||||||
hits=boundary.curved_hits)
|
|
||||||
self._fine_curved.upload()
|
|
||||||
self._curved = self._levels[1].module.get_function("CurvedBoundaryKernel")
|
|
||||||
except BaseException:
|
except BaseException:
|
||||||
try:
|
try:
|
||||||
self.close()
|
self.close()
|
||||||
@@ -418,93 +347,84 @@ class FixedPatchStepper:
|
|||||||
pass # Preserve the original construction failure.
|
pass # Preserve the original construction failure.
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def _planned_bytes(self):
|
||||||
|
"""Both G buffers, halo pairs and boundary columns of every level (transfer tables excluded).
|
||||||
|
|
||||||
|
A fail-fast lower bound only: passing it cannot guarantee that later
|
||||||
|
allocations succeed (other consumers, fragmentation, races).
|
||||||
|
"""
|
||||||
|
total = 0
|
||||||
|
for index, layout in enumerate(self._layouts):
|
||||||
|
total += 2 * 4 * int(np.prod(layout.blocked_shape))
|
||||||
|
total += layout.halo_dst.nbytes + layout.halo_src.nbytes
|
||||||
|
b = self._level_boundaries[index]
|
||||||
|
if b is not None:
|
||||||
|
total += sum(np.asarray(v).nbytes for k, v in b.tables.items()
|
||||||
|
if k == "flags" or k in BLOCKED_BOUNDARY_DEVICE_COLUMNS)
|
||||||
|
return total
|
||||||
|
|
||||||
def _build_level(self, index):
|
def _build_level(self, index):
|
||||||
"""Compile one level's step module and allocate its two G buffers."""
|
"""Compile one level's step module and allocate its two G buffers."""
|
||||||
layout, config = self._layouts[index], self._level_cfgs[index]
|
layout, config = self._layouts[index], self._level_cfgs[index]
|
||||||
pairs = self.hierarchy.pairs
|
pairs = self.hierarchy.pairs
|
||||||
tau = pairs[0].tau_coarse if index == 0 else pairs[index - 1].tau_fine
|
tau = (pairs[0].tau_coarse if pairs else 3 * config.viscosity + .5) if index == 0 \
|
||||||
name, source = ("PatchCoarseStep" if index == 0 else "PatchFineStep"), "refinement.cu"
|
else pairs[index - 1].tau_fine
|
||||||
if layout is not None:
|
config = level_config(config, layout, tau)
|
||||||
config = replace(config, nx=layout.local_shape[1], ny=layout.local_shape[0],
|
name = "BlockChannelStep" if index == 0 and self._channel else "BlockStep"
|
||||||
y_wall_bc="bounce_back", top_wall_bc="velocity",
|
block_root_bc = (2 if self._channel else 1 if self._wall_aware else 0) if index == 0 else 0
|
||||||
wall_slot_width=0, wall_normal_velocity=0.0, velocity=0.0)
|
module = compiler.build_module(config, "blocked.cu", arch=self.ctx.sm_arch,
|
||||||
name, source = ("BlockChannelStep" if index == 0 and self._channel else "BlockStep"), "blocks.cu"
|
block_root_bc=block_root_bc)
|
||||||
config.validate()
|
compiler.set_params(module, config.omega)
|
||||||
if not np.isclose(config.omega, 1 / tau, rtol=1e-14, atol=0):
|
|
||||||
raise RuntimeError("level omega/tau mismatch")
|
|
||||||
host = self._boundary_hosts[index]
|
|
||||||
n_objects = len(host.objects) if name == "PatchFineStep" and host is not None else 0
|
|
||||||
build = compiler.generate_config(config, n_objects=n_objects)
|
|
||||||
try:
|
|
||||||
ptx = compiler.compile_kernel(self.ctx.sm_arch, source=source, build_dir=build)
|
|
||||||
module = compiler.load_module(ptx)
|
|
||||||
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, n_objects))
|
|
||||||
shape = (9, *self.hierarchy.kinds[index].shape)
|
shape = (9, *self.hierarchy.kinds[index].shape)
|
||||||
size = int(np.prod(shape if layout is None else layout.blocked_shape)) * 4
|
size = int(np.prod(layout.blocked_shape)) * 4
|
||||||
grid = ((config.nx + self._block[0] - 1) // self._block[0], config.ny,
|
grid = ((config.nx + self._block[0] - 1) // self._block[0], config.ny, layout.nblocks)
|
||||||
1 if layout is None else layout.nblocks)
|
|
||||||
level = _Level(shape, module, self._allocate(size), self._allocate(size),
|
level = _Level(shape, module, self._allocate(size), self._allocate(size),
|
||||||
module.get_function(name), grid, layout)
|
module.get_function(name), grid, layout)
|
||||||
level.boundary = self._level_boundaries[index]
|
level.boundary = self._level_boundaries[index]
|
||||||
if layout is not None:
|
level.halo_copy = module.get_function("BlockHaloCopy")
|
||||||
level.halo_copy = module.get_function("BlockHaloCopy")
|
if len(layout.halo_dst):
|
||||||
if len(layout.halo_dst):
|
level.halo_dst = self._upload(layout.halo_dst)
|
||||||
level.halo_dst = self._upload(layout.halo_dst)
|
level.halo_src = self._upload(layout.halo_src)
|
||||||
level.halo_src = self._upload(layout.halo_src)
|
if index == 0 and self._channel:
|
||||||
if index == 0 and self._channel:
|
cfg = self._remap_cfg
|
||||||
cfg = self._remap_cfg
|
ptr, size = module.get_global("d_block_channel")
|
||||||
ptr, size = module.get_global("d_block_channel")
|
if size != struct.calcsize("=IIf"):
|
||||||
if size != struct.calcsize("=IIf"):
|
raise RuntimeError("Block channel host/device layout mismatch")
|
||||||
raise RuntimeError("Block channel host/device layout mismatch")
|
cuda.memcpy_htod(ptr, struct.pack("=IIf", cfg.nx, cfg.ny, cfg.velocity))
|
||||||
cuda.memcpy_htod(ptr, struct.pack("=IIf", cfg.nx, cfg.ny, cfg.velocity))
|
if index == 0 and self._wall_aware:
|
||||||
if index == 0 and self._wall_aware:
|
cfg = self._remap_cfg
|
||||||
cfg = self._remap_cfg
|
ptr, size = module.get_global("d_block_wall")
|
||||||
ptr, size = module.get_global("d_block_wall")
|
if size != struct.calcsize("=IIIf"):
|
||||||
if size != struct.calcsize("=IIIf"):
|
raise RuntimeError("Block wall host/device layout mismatch")
|
||||||
raise RuntimeError("Block wall host/device layout mismatch")
|
cuda.memcpy_htod(ptr, struct.pack("=IIIf", cfg.nx, cfg.ny,
|
||||||
cuda.memcpy_htod(ptr, struct.pack("=IIIf", cfg.nx, cfg.ny,
|
cfg.wall_slot_width, cfg.wall_normal_velocity))
|
||||||
cfg.wall_slot_width, cfg.wall_normal_velocity))
|
|
||||||
return level
|
return level
|
||||||
|
|
||||||
def _build_pair(self, index, pair):
|
def _build_pair(self, index, pair):
|
||||||
"""Upload the transfer tables of levels (index, index+1) and the child's block kinds."""
|
"""Upload the transfer tables of levels (index, index+1) and the child's block kinds."""
|
||||||
parent, child = self._levels[index], self._levels[index + 1]
|
parent, child = self._levels[index], self._levels[index + 1]
|
||||||
order = np.argsort(pair.coalesce_coarse)
|
order = np.argsort(pair.coalesce_coarse)
|
||||||
fine = None if child.layout is None else blocked_fine_tables(pair, child.layout, order)
|
ghosts, children, child_kind = blocked_fine_tables(pair, child.layout, order)
|
||||||
base = None
|
if index == 0:
|
||||||
if parent.layout is not None:
|
donors, targets, parent_kind = blocked_base_tables(pair, parent.layout, order,
|
||||||
if index == 0:
|
self._wall_aware)
|
||||||
base = blocked_base_tables(pair, parent.layout, order, self._wall_aware)
|
else:
|
||||||
else:
|
# Nested pairs keep inherited INNER/OUTER and sparse holes of the parent.
|
||||||
# Nested pairs keep inherited INNER/OUTER and sparse holes of the parent.
|
donors, targets, parent_kind = blocked_base_tables(
|
||||||
base = blocked_base_tables(pair, parent.layout, order,
|
pair, parent.layout, order, composed_kind=self.hierarchy.kinds[index])
|
||||||
composed_kind=self.hierarchy.kinds[index])
|
result = _Pair(child.module.get_function("BlockExplode"),
|
||||||
ghosts = pair.ghost_fine if fine is None else fine[0]
|
parent.module.get_function("BlockCoalesce"),
|
||||||
donors = pair.ghost_coarse if base is None else base[0]
|
self._upload(ghosts), self._upload(donors.astype(np.uint64)),
|
||||||
if fine is not None:
|
self._upload(targets), self._upload(children), len(ghosts), len(targets),
|
||||||
donors = donors.astype(np.uint64)
|
int(np.prod(parent.layout.local_shape)))
|
||||||
targets = pair.coalesce_coarse[order] if base is None else base[1]
|
|
||||||
children = pair.coalesce_fine[order] if fine is None else fine[1]
|
|
||||||
result = _Pair(child.module.get_function("PatchExplode" if fine is None else "BlockExplode"),
|
|
||||||
None if parent.layout is None else parent.module.get_function("BlockCoalesce"),
|
|
||||||
self._upload(ghosts), self._upload(donors), self._upload(targets),
|
|
||||||
self._upload(children), len(ghosts), len(targets),
|
|
||||||
int(np.prod(pair.coarse_shape if parent.layout is None
|
|
||||||
else parent.layout.local_shape)))
|
|
||||||
if self._explosion == "projected_linear":
|
if self._explosion == "projected_linear":
|
||||||
neighbors, offsets = projected_explosion_tables(
|
neighbors, offsets = projected_explosion_tables(
|
||||||
pair, parent.layout, self.hierarchy.kinds[index])
|
pair, parent.layout, self.hierarchy.kinds[index])
|
||||||
result.explode = child.module.get_function("BlockExplodeProjected")
|
result.explode = child.module.get_function("BlockExplodeProjected")
|
||||||
result.neighbors, result.offsets = self._upload(neighbors), self._upload(offsets)
|
result.neighbors, result.offsets = self._upload(neighbors), self._upload(offsets)
|
||||||
if child.layout is not None:
|
child.kind = self._upload(blocked_kind(child.layout, self.hierarchy.kinds[index + 1]))
|
||||||
child.kind = self._upload(blocked_kind(child.layout, self.hierarchy.kinds[index + 1]))
|
if index == 0:
|
||||||
if index == 0 and base is not None:
|
parent.kind = self._upload(parent_kind)
|
||||||
parent.kind = self._upload(base[2])
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _allocate(self, size):
|
def _allocate(self, size):
|
||||||
@@ -537,16 +457,14 @@ class FixedPatchStepper:
|
|||||||
def _check_storage(self):
|
def _check_storage(self):
|
||||||
if any(bool(c.ddf_shifting) != self._ddf_shifting for c in self._level_cfgs):
|
if any(bool(c.ddf_shifting) != self._ddf_shifting for c in self._level_cfgs):
|
||||||
raise ValueError("mixed population representations are unsupported")
|
raise ValueError("mixed population representations are unsupported")
|
||||||
if self._ddf_shifting and any(layout is None for layout in self._layouts):
|
|
||||||
raise ValueError("shifted populations require all-blocked levels")
|
|
||||||
|
|
||||||
def initialize(self, *level_g, coarse_g=None, fine_g=None, child_g=None):
|
def initialize(self, *level_g):
|
||||||
"""Upload physical postcollision G, encoding before any FP32 rounding.
|
"""Upload physical postcollision G, encoding before any FP32 rounding.
|
||||||
|
|
||||||
Shifted input should be physical FP64 to retain small perturbations.
|
Shifted input should be physical FP64 to retain small perturbations.
|
||||||
Public initialization resets the step count; non-leaves are discarded.
|
Public initialization resets the step count; non-leaves are discarded.
|
||||||
"""
|
"""
|
||||||
given = _initial_levels(level_g, coarse_g, fine_g, child_g)
|
given = tuple(level_g)
|
||||||
with self._lock, self.ctx.activate():
|
with self._lock, self.ctx.activate():
|
||||||
self._require("new", "ready")
|
self._require("new", "ready")
|
||||||
self._check_storage()
|
self._check_storage()
|
||||||
@@ -583,14 +501,13 @@ class FixedPatchStepper:
|
|||||||
or (values.sum(axis=0, dtype=np.float64) + int(self._ddf_shifting) <= 0).any()):
|
or (values.sum(axis=0, dtype=np.float64) + int(self._ddf_shifting) <= 0).any()):
|
||||||
raise ValueError("Leaf populations must be finite with positive density")
|
raise ValueError("Leaf populations must be finite with positive density")
|
||||||
self._mask_nonstate(*arrays)
|
self._mask_nonstate(*arrays)
|
||||||
arrays = [a if level.layout is None else level.layout.to_blocked(a)
|
arrays = [level.layout.to_blocked(a) for a, level in zip(arrays, self._levels)]
|
||||||
for a, level in zip(arrays, self._levels)]
|
|
||||||
try:
|
try:
|
||||||
for level, array in zip(self._levels, arrays):
|
for level, array in zip(self._levels, arrays):
|
||||||
cuda.memcpy_htod(level.current, array)
|
cuda.memcpy_htod(level.current, array)
|
||||||
cuda.memcpy_htod(level.spare, array)
|
# A single level starts its spare NaN so stale halos cannot hide.
|
||||||
if self._fine_curved is not None and self._fine_curved.link_exchange_gpu is not None:
|
cuda.memcpy_htod(level.spare, np.full_like(array, np.nan)
|
||||||
cuda.memset_d8(self._fine_curved.link_exchange_gpu, 0, self._fine_curved.count * 12)
|
if len(self._levels) == 1 else array)
|
||||||
self._step_count = 0
|
self._step_count = 0
|
||||||
self._exchange_valid = False
|
self._exchange_valid = False
|
||||||
self._window_valid = False
|
self._window_valid = False
|
||||||
@@ -670,10 +587,9 @@ class FixedPatchStepper:
|
|||||||
old, new, source, base_count=count, level_steps=(count, 2*count, 4*count),
|
old, new, source, base_count=count, level_steps=(count, 2*count, 4*count),
|
||||||
phase="postcollision_g", **options)
|
phase="postcollision_g", **options)
|
||||||
candidate = type(self)(
|
candidate = type(self)(
|
||||||
cfg, pairs[0].bounds, context=self.ctx, base_block=base_block,
|
cfg, context=self.ctx, refinements=[pairs[0].bounds, new.child.bounds],
|
||||||
fine_block=fine_block, child_block=child_block,
|
level_blocks=[base_block, fine_block, child_block],
|
||||||
child_rectangles_in_parent=new.child.bounds,
|
level_objects=[(), (), child_objects], level_jets=[(), (), jets],
|
||||||
child_objects=child_objects, child_jets=jets,
|
|
||||||
observe_raw_exchange=self._observe_raw_exchange,
|
observe_raw_exchange=self._observe_raw_exchange,
|
||||||
observe_window_exchange=self._observe_window, explosion=self._explosion)
|
observe_window_exchange=self._observe_window, explosion=self._explosion)
|
||||||
try:
|
try:
|
||||||
@@ -690,26 +606,6 @@ class FixedPatchStepper:
|
|||||||
raise
|
raise
|
||||||
return candidate
|
return candidate
|
||||||
|
|
||||||
def _apply_fine_boundary(self, buffer, **kw):
|
|
||||||
"""Stage solid source slots; exchange buffer holds this fine substep only."""
|
|
||||||
cl = self._fine_curved
|
|
||||||
if cl is None:
|
|
||||||
if self._boundary_hosts[1] is not None:
|
|
||||||
raise RuntimeError("Fine boundary device buffers are missing")
|
|
||||||
return
|
|
||||||
if self._curved is None or cl.fluid_idx_gpu is None:
|
|
||||||
raise RuntimeError("Fine boundary device buffers are missing")
|
|
||||||
driven = bool(self._level_jets[1]) or any(
|
|
||||||
obj.surface_actuation is not None for obj in self._boundary_hosts[1].objects)
|
|
||||||
if driven and (cl.surface_velocity_gpu is None or cl.link_exchange_gpu is None):
|
|
||||||
raise RuntimeError("Surface boundary device buffers are missing")
|
|
||||||
self._curved(buffer, cl.fluid_idx_gpu, cl.dir_gpu, cl.q_gpu,
|
|
||||||
cl.rx_gpu, cl.ry_gpu, cl.rz_gpu, cl.fallback_class_gpu, cl.body_id_gpu,
|
|
||||||
np.uintp(0), np.uintp(0), np.uint32(cl.count),
|
|
||||||
cl.surface_velocity_gpu if cl.surface_velocity_gpu is not None else np.uintp(0),
|
|
||||||
cl.link_exchange_gpu if cl.link_exchange_gpu is not None else np.uintp(0),
|
|
||||||
grid=((cl.count + self._block[0] - 1) // self._block[0], 1), **kw)
|
|
||||||
|
|
||||||
def set_jet_amplitude(self, index, amplitude, *, level=1):
|
def set_jet_amplitude(self, index, amplitude, *, level=1):
|
||||||
"""Replace one jet's amplitude between steps; no geometry/table rebuild.
|
"""Replace one jet's amplitude between steps; no geometry/table rebuild.
|
||||||
|
|
||||||
@@ -733,9 +629,7 @@ class FixedPatchStepper:
|
|||||||
driven = jet_id >= 0
|
driven = jet_id >= 0
|
||||||
velocity[driven] = jet_link_velocity(jets, shape, jet_id)[driven]
|
velocity[driven] = jet_link_velocity(jets, shape, jet_id)[driven]
|
||||||
try:
|
try:
|
||||||
if level == 1 and self._fine_curved is not None:
|
if velocity is not None:
|
||||||
self._fine_curved.set_jet_amplitude(index, amplitude, self._stream)
|
|
||||||
elif velocity is not None:
|
|
||||||
self._stream.synchronize() # no in-flight step reads the column
|
self._stream.synchronize() # no in-flight step reads the column
|
||||||
cuda.memcpy_htod(boundary.device["surface_velocity"], velocity)
|
cuda.memcpy_htod(boundary.device["surface_velocity"], velocity)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
@@ -753,15 +647,12 @@ class FixedPatchStepper:
|
|||||||
return
|
return
|
||||||
if b.kernel is None or b.flags is None:
|
if b.kernel is None or b.flags is None:
|
||||||
raise RuntimeError("Boundary device buffers are missing")
|
raise RuntimeError("Boundary device buffers are missing")
|
||||||
t, count = b.device, len(b.tables["cell"])
|
t = b.device
|
||||||
velocity = t.get("surface_velocity")
|
velocity = t.get("surface_velocity")
|
||||||
if b.actuated and velocity is None:
|
if b.actuated and velocity is None:
|
||||||
raise RuntimeError("Surface velocity device buffer is missing")
|
raise RuntimeError("Surface velocity device buffer is missing")
|
||||||
b.kernel(buffer, t["block"], t["cell"], t["dir"], t["q"], t["fallback"],
|
launch_curved_boundary(b.kernel, buffer, t, len(b.tables["cell"]), velocity=velocity,
|
||||||
np.uint32(count), velocity if velocity is not None else np.uintp(0),
|
rx=t.get("rx"), ry=t.get("ry"), raw=b.last_raw, **kw)
|
||||||
t.get("rx", np.uintp(0)), t.get("ry", np.uintp(0)),
|
|
||||||
b.last_raw if b.last_raw is not None else np.uintp(0),
|
|
||||||
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
|
|
||||||
|
|
||||||
def _advance(self, index, parent_buffer, **kw):
|
def _advance(self, index, parent_buffer, **kw):
|
||||||
"""Explode from the given parent buffer, then two hops of level ``index``."""
|
"""Explode from the given parent buffer, then two hops of level ``index``."""
|
||||||
@@ -780,19 +671,6 @@ class FixedPatchStepper:
|
|||||||
nested = index + 1 < len(self._levels)
|
nested = index + 1 < len(self._levels)
|
||||||
if nested:
|
if nested:
|
||||||
self._advance(index + 1, src, **kw)
|
self._advance(index + 1, src, **kw)
|
||||||
if level.layout is None:
|
|
||||||
if index == 0: # dense root: PatchCoarseStep fuses restriction and collision
|
|
||||||
pair = self._pairs[0]
|
|
||||||
level.kernel(src, dst, self._levels[1].current, pair.targets, pair.children,
|
|
||||||
np.uint32(pair.n_rows), *(np.uint32(v) for v in self.hierarchy.pairs[0].bounds),
|
|
||||||
np.uint32(self._wall_aware), grid=level.grid, **kw)
|
|
||||||
return
|
|
||||||
if self._boundary_hosts[index] is not None and self._fine_flags is None:
|
|
||||||
raise RuntimeError("Fine boundary flags are missing")
|
|
||||||
self._apply_fine_boundary(src, **kw)
|
|
||||||
flags = self._fine_flags if self._fine_flags is not None else np.uintp(0)
|
|
||||||
level.kernel(src, dst, flags, grid=level.grid, **kw)
|
|
||||||
return
|
|
||||||
# Child donors were read above; no halo gather may overwrite staged arrivals below.
|
# Child donors were read above; no halo gather may overwrite staged arrivals below.
|
||||||
self._gather_halo(level, src, **kw)
|
self._gather_halo(level, src, **kw)
|
||||||
if nested:
|
if nested:
|
||||||
@@ -813,36 +691,50 @@ class FixedPatchStepper:
|
|||||||
np.float64(h), np.float64(h),
|
np.float64(h), np.float64(h),
|
||||||
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
|
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
|
||||||
flags = np.uintp(0) if level.boundary is None else level.boundary.flags
|
flags = np.uintp(0) if level.boundary is None else level.boundary.flags
|
||||||
level.kernel(src, dst, np.uint32(level.layout.halo), flags, level.kind, grid=level.grid, **kw)
|
kind = np.uintp(0) if level.kind is None else level.kind # single level: all ACTIVE
|
||||||
|
level.kernel(src, dst, np.uint32(level.layout.halo), flags, kind, grid=level.grid, **kw)
|
||||||
|
|
||||||
def _gather_halo(self, level, buffer, **kw):
|
def _gather_halo(self, level, buffer, **kw):
|
||||||
pairs = len(level.layout.halo_dst)
|
pairs = len(level.layout.halo_dst)
|
||||||
if pairs:
|
if pairs:
|
||||||
level.halo_copy(buffer, level.halo_dst, level.halo_src, np.uint32(pairs),
|
launch_halo_copy(level.halo_copy, buffer, level.halo_dst, level.halo_src, pairs, **kw)
|
||||||
grid=((pairs + self._block[0] - 1) // self._block[0], 1), **kw)
|
|
||||||
|
|
||||||
def step(self, n=1):
|
def step(self, n=1):
|
||||||
"""Advance G→G by n base intervals: level l takes 2**l hops per interval."""
|
"""Advance G→G by n base intervals: level l takes 2**l hops per interval.
|
||||||
|
|
||||||
|
All n intervals are enqueued in order on the one stream, and the host waits
|
||||||
|
once, before returning; step(0) launches and waits for nothing. On return
|
||||||
|
the device work is complete, step_count and an open window cover all n
|
||||||
|
intervals, and the raw-exchange ledger holds the final interval only.
|
||||||
|
|
||||||
|
Asynchronous device errors surface at that single wait, so they are not
|
||||||
|
attributed to the interval that caused them; call step(1) repeatedly to
|
||||||
|
localize one (same launches, same bits). On any failure the instance is
|
||||||
|
failed, the exchange ledger and window are invalid, and step_count keeps
|
||||||
|
its value from entry to the failing call.
|
||||||
|
"""
|
||||||
if isinstance(n, bool) or not isinstance(n, Integral) or n < 0:
|
if isinstance(n, bool) or not isinstance(n, Integral) or n < 0:
|
||||||
raise ValueError("n must be a nonnegative integer")
|
raise ValueError("n must be a nonnegative integer")
|
||||||
with self._lock, self.ctx.activate():
|
with self._lock, self.ctx.activate():
|
||||||
self._require("ready")
|
self._require("ready")
|
||||||
|
if n == 0:
|
||||||
|
return
|
||||||
root = self._levels[0]
|
root = self._levels[0]
|
||||||
kw = dict(block=self._block, stream=self._stream)
|
kw = dict(block=self._block, stream=self._stream)
|
||||||
try:
|
try:
|
||||||
self._status = "advancing"
|
self._status = "advancing"
|
||||||
|
self._exchange_valid = False
|
||||||
for _ in range(n):
|
for _ in range(n):
|
||||||
if self._observe_raw_exchange:
|
if self._observe_raw_exchange: # stream-ordered: holds the last interval only
|
||||||
self._exchange_valid = False
|
|
||||||
for b in self._level_boundaries:
|
for b in self._level_boundaries:
|
||||||
if b is not None:
|
if b is not None:
|
||||||
cuda.memset_d8_async(b.interval_integral, 0,
|
cuda.memset_d8_async(b.interval_integral, 0,
|
||||||
len(b.tables["cell"]) * 32, self._stream)
|
len(b.tables["cell"]) * 32, self._stream)
|
||||||
self._hop(0, root.current, root.spare, **kw)
|
self._hop(0, root.current, root.spare, **kw)
|
||||||
self._stream.synchronize()
|
root.current, root.spare = root.spare, root.current # host handles only
|
||||||
root.current, root.spare = root.spare, root.current
|
self._stream.synchronize()
|
||||||
self._step_count += 1
|
self._step_count += n
|
||||||
self._exchange_valid = self._observe_raw_exchange
|
self._exchange_valid = self._observe_raw_exchange
|
||||||
self._status = "ready"
|
self._status = "ready"
|
||||||
except BaseException:
|
except BaseException:
|
||||||
self._window_valid = False
|
self._window_valid = False
|
||||||
@@ -943,7 +835,7 @@ class FixedPatchStepper:
|
|||||||
with self._lock, self.ctx.activate():
|
with self._lock, self.ctx.activate():
|
||||||
self._require("ready")
|
self._require("ready")
|
||||||
self._check_storage()
|
self._check_storage()
|
||||||
arrays = [np.empty(level.shape if level.layout is None else level.layout.blocked_shape,
|
arrays = [np.empty(level.layout.blocked_shape,
|
||||||
np.float32) for level in self._levels]
|
np.float32) for level in self._levels]
|
||||||
try:
|
try:
|
||||||
for level, array in zip(self._levels, arrays):
|
for level, array in zip(self._levels, arrays):
|
||||||
@@ -951,7 +843,7 @@ class FixedPatchStepper:
|
|||||||
except BaseException:
|
except BaseException:
|
||||||
self._status = "failed"
|
self._status = "failed"
|
||||||
raise
|
raise
|
||||||
arrays = [a if level.layout is None else level.layout.to_dense(a)
|
arrays = [level.layout.to_dense(a)
|
||||||
for a, level in zip(arrays, self._levels)]
|
for a, level in zip(arrays, self._levels)]
|
||||||
self._mask_nonstate(*arrays)
|
self._mask_nonstate(*arrays)
|
||||||
return _StoredPopulations(tuple(arrays), self._ddf_shifting)
|
return _StoredPopulations(tuple(arrays), self._ddf_shifting)
|
||||||
@@ -977,14 +869,6 @@ class FixedPatchStepper:
|
|||||||
b.window_integral = None
|
b.window_integral = None
|
||||||
self._levels.clear()
|
self._levels.clear()
|
||||||
self._pairs = []
|
self._pairs = []
|
||||||
if getattr(self, "_fine_curved", None) is not None:
|
|
||||||
try:
|
|
||||||
self._fine_curved.free()
|
|
||||||
except Exception as exc:
|
|
||||||
error = error or exc
|
|
||||||
self._fine_curved = None
|
|
||||||
self._curved = self._fine_flags = None
|
|
||||||
self._link_tables = {}
|
|
||||||
for pointer in self._allocations:
|
for pointer in self._allocations:
|
||||||
try:
|
try:
|
||||||
pointer.free()
|
pointer.free()
|
||||||
@@ -1005,8 +889,8 @@ class FixedPatchStepper:
|
|||||||
if exc[0] is None:
|
if exc[0] is None:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# Read-only compatibility views over the level lists. Internal code must not use them;
|
# Read-only compatibility views over the level lists, kept on purpose for the existing
|
||||||
# REMOVE AFTER the existing tests/docs migrate to hierarchy/_layouts/_level_* lists.
|
# tests and docs. Internal code must not use them; it reads hierarchy/_layouts/_level_*.
|
||||||
patch = property(lambda self: self.hierarchy.pairs[0])
|
patch = property(lambda self: self.hierarchy.pairs[0])
|
||||||
child_patch = property(lambda self: self.hierarchy.pairs[1] if self.hierarchy.depth > 2 else None)
|
child_patch = property(lambda self: self.hierarchy.pairs[1] if self.hierarchy.depth > 2 else None)
|
||||||
base_layout = property(lambda self: self._layouts[0])
|
base_layout = property(lambda self: self._layouts[0])
|
||||||
@@ -1026,7 +910,3 @@ class FixedPatchStepper:
|
|||||||
_fine_leaf = property(lambda self: self._leaf[1])
|
_fine_leaf = property(lambda self: self._leaf[1])
|
||||||
_child_leaf = property(lambda self: self._leaf[2] if len(self._leaf) > 2 else None)
|
_child_leaf = property(lambda self: self._leaf[2] if len(self._leaf) > 2 else None)
|
||||||
_middle_kind = property(lambda self: self.hierarchy.kinds[1] if self.hierarchy.depth > 2 else None)
|
_middle_kind = property(lambda self: self.hierarchy.kinds[1] if self.hierarchy.depth > 2 else None)
|
||||||
|
|
||||||
|
|
||||||
# Preserve the original entry point and its periodic default.
|
|
||||||
PeriodicPatchStepper = FixedPatchStepper
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from dataclasses import dataclass, replace
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from .blocks import blocked_link_tables
|
from .blocks import blocked_link_tables
|
||||||
from ..body.jets import jet_link_shapes, surface_link_velocity
|
from ..body.jets import _surface_links
|
||||||
from .descriptors import D2Q9_EX, D2Q9_EY
|
from .descriptors import D2Q9_EX, D2Q9_EY
|
||||||
from .refinement import ACTIVE, OUTER_GHOST
|
from .refinement import ACTIVE, OUTER_GHOST
|
||||||
|
|
||||||
@@ -142,11 +142,13 @@ 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),
|
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))
|
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)
|
actuated = bool(jets) or any(o.surface_actuation is not None for o in boundary.objects)
|
||||||
|
jet_cache = None
|
||||||
if actuated:
|
if actuated:
|
||||||
cfg = replace(cfg)
|
cfg = replace(cfg)
|
||||||
cfg.validate()
|
cfg.validate()
|
||||||
tables["surface_velocity"], tables["aperture"] = surface_link_velocity(
|
tables["surface_velocity"], tables["aperture"], shape, jet_id = _surface_links(
|
||||||
boundary.objects, cfg, boundary.flags, fluid_idx, dirs, body_id, rx, ry,
|
boundary.objects, cfg, boundary.flags, fluid_idx, dirs, body_id, rx, ry,
|
||||||
jets=jets, hits=boundary.curved_hits, _shifted_blocked=_shifted_blocked)
|
jets=jets, hits=boundary.curved_hits, _shifted_blocked=_shifted_blocked)
|
||||||
return _BlockedBoundary(boundary, tables, jets, actuated,
|
if jets:
|
||||||
jet_link_shapes(jets, boundary.curved_hits) if jets else None)
|
jet_cache = (shape, jet_id)
|
||||||
|
return _BlockedBoundary(boundary, tables, jets, actuated, jet_cache)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class LBMStepper:
|
|||||||
self.boundary_values_gpu = cuda.mem_alloc((6 * cfg.nx + 2 * cfg.ny * cfg.nq) * 4)
|
self.boundary_values_gpu = cuda.mem_alloc((6 * cfg.nx + 2 * cfg.ny * cfg.nq) * 4)
|
||||||
else:
|
else:
|
||||||
self.step_fn = module.get_function("OneStep")
|
self.step_fn = module.get_function("OneStep")
|
||||||
self.init_fn = module.get_function("InitTubeFlow_v2")
|
self.init_fn = module.get_function("InitTubeFlow")
|
||||||
|
|
||||||
self.curved_fn = module.get_function("CurvedBoundaryKernel")
|
self.curved_fn = module.get_function("CurvedBoundaryKernel")
|
||||||
self.prepare_esopull_curved_fn = module.get_function("PrepareEsoPullCurvedKernel")
|
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 .config import LBMConfig, BodyConfig, load_lbm_config, load_body_config
|
||||||
from .cuda.context import CudaContext, context_bound
|
from .cuda.context import CudaContext, context_bound
|
||||||
from .cuda import compiler_v2 as compiler
|
from .cuda import compiler
|
||||||
from .lbm.field import LBMField
|
from .lbm.field import LBMField
|
||||||
from .lbm.stepper import LBMStepper
|
from .lbm.stepper import LBMStepper
|
||||||
from .body.objects import SimObject
|
from .body.objects import SimObject
|
||||||
@@ -46,7 +46,6 @@ class Simulation:
|
|||||||
device_id: int = 0):
|
device_id: int = 0):
|
||||||
# Internal stream for async DTOH coordination
|
# Internal stream for async DTOH coordination
|
||||||
self._stream: Optional[cuda.Stream] = None
|
self._stream: Optional[cuda.Stream] = None
|
||||||
self._build_dir = None
|
|
||||||
self._closed = False
|
self._closed = False
|
||||||
|
|
||||||
# Load configs
|
# Load configs
|
||||||
@@ -59,9 +58,7 @@ class Simulation:
|
|||||||
try:
|
try:
|
||||||
with self.ctx.activate():
|
with self.ctx.activate():
|
||||||
arch = self._resolve_compile_arch()
|
arch = self._resolve_compile_arch()
|
||||||
self._build_dir = compiler.generate_config(self.lbm_cfg, n_objects=0)
|
self._module = compiler.build_module(self.lbm_cfg, "uniform.cu", arch=arch)
|
||||||
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._compiled_n_objects = 0
|
||||||
self._allocation_signature = self.lbm_cfg.allocation_signature()
|
self._allocation_signature = self.lbm_cfg.allocation_signature()
|
||||||
self._compiled_viscosity = self.lbm_cfg.viscosity
|
self._compiled_viscosity = self.lbm_cfg.viscosity
|
||||||
@@ -336,13 +333,19 @@ class Simulation:
|
|||||||
def read_surface_exchange(self, id: int):
|
def read_surface_exchange(self, id: int):
|
||||||
"""Latest step's wall/aperture (Fx,Fy,Tz) exchange, not total nozzle force.
|
"""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
|
Unlike read_body this is not accumulated/time-averaged. It resets after
|
||||||
geometry sync or checkpoint load; advance before interpreting it.
|
geometry sync or checkpoint load; advance before interpreting it.
|
||||||
"""
|
"""
|
||||||
if self.bodies.get(id).surface_actuation is None:
|
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:
|
||||||
raise ValueError("Body has no surface actuation")
|
raise ValueError("Body has no surface actuation")
|
||||||
self.stream.synchronize()
|
self.stream.synchronize()
|
||||||
return self.field.curved.surface_exchange(id)
|
return curved.surface_exchange(id)
|
||||||
|
|
||||||
@context_bound
|
@context_bound
|
||||||
def read_body(self, id: int, *,
|
def read_body(self, id: int, *,
|
||||||
@@ -406,15 +409,8 @@ class Simulation:
|
|||||||
self.lbm_cfg.validate_runtime()
|
self.lbm_cfg.validate_runtime()
|
||||||
self._assert_allocation_contract()
|
self._assert_allocation_contract()
|
||||||
arch = self._resolve_compile_arch()
|
arch = self._resolve_compile_arch()
|
||||||
next_dir = compiler.generate_config(self.lbm_cfg, n_objects=expected_n_objects)
|
self._module = compiler.build_module(self.lbm_cfg, "uniform.cu", arch=arch,
|
||||||
try:
|
n_objects=expected_n_objects)
|
||||||
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)
|
self._compiled_n_objects = int(expected_n_objects)
|
||||||
|
|
||||||
# Reconnect field and stepper to new module
|
# Reconnect field and stepper to new module
|
||||||
@@ -432,7 +428,6 @@ class Simulation:
|
|||||||
self.stepper._step_count = _prev_step_count
|
self.stepper._step_count = _prev_step_count
|
||||||
|
|
||||||
self._assert_object_count_contract(expected_count=expected_n_objects)
|
self._assert_object_count_contract(expected_count=expected_n_objects)
|
||||||
compiler.cleanup_build_dir(old_dir)
|
|
||||||
|
|
||||||
def recompile(self):
|
def recompile(self):
|
||||||
"""Re-generate config headers and recompile kernel.
|
"""Re-generate config headers and recompile kernel.
|
||||||
@@ -451,18 +446,21 @@ class Simulation:
|
|||||||
@context_bound
|
@context_bound
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""Initialize flow field and sync objects to GPU."""
|
"""Initialize flow field and sync objects to GPU."""
|
||||||
# Recompile if objects were added after construction.
|
|
||||||
self._assert_allocation_contract()
|
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.
|
||||||
if self._read_generated_n_objects() != self.bodies.count:
|
if self._read_generated_n_objects() != self.bodies.count:
|
||||||
self.recompile()
|
self.recompile()
|
||||||
|
|
||||||
self.field.flag = self.field.build_channel_flags()
|
self.field.flag = next_flags
|
||||||
if self.bodies.count > 0:
|
|
||||||
self.field.flag = self.bodies.build_flags(self.field.flag)
|
|
||||||
self.field.upload_flags()
|
self.field.upload_flags()
|
||||||
|
|
||||||
if self.bodies.jets and self.bodies.count == 0:
|
|
||||||
raise ValueError("Jets need bodies with cut links")
|
|
||||||
self.stepper.initialize()
|
self.stepper.initialize()
|
||||||
if self.bodies.count > 0:
|
if self.bodies.count > 0:
|
||||||
self.bodies.sync_to_gpu(self.field, rebuild_flags=False)
|
self.bodies.sync_to_gpu(self.field, rebuild_flags=False)
|
||||||
@@ -706,8 +704,6 @@ class Simulation:
|
|||||||
self._module = None
|
self._module = None
|
||||||
self._stream = None
|
self._stream = None
|
||||||
ctx.close()
|
ctx.close()
|
||||||
compiler.cleanup_build_dir(getattr(self, "_build_dir", None))
|
|
||||||
self._build_dir = None
|
|
||||||
self._closed = True
|
self._closed = True
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
"""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()
|
||||||
@@ -6,14 +6,14 @@ import pycuda.driver as cuda
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import CelerisLab
|
import CelerisLab
|
||||||
from CelerisLab.cuda import compiler_v2
|
from CelerisLab.cuda import compiler
|
||||||
from ._cases import make_sim
|
from ._cases import make_sim
|
||||||
|
|
||||||
|
|
||||||
def test_source_provenance():
|
def test_source_provenance():
|
||||||
root = Path(__file__).resolve().parents[2] / "src/CelerisLab"
|
root = Path(__file__).resolve().parents[2] / "src/CelerisLab"
|
||||||
assert Path(CelerisLab.__file__).resolve().parent == root
|
assert Path(CelerisLab.__file__).resolve().parent == root
|
||||||
assert Path(compiler_v2.kernel_path("kernel_v2.cu")).resolve() == root / "lbm/kernels/kernel_v2.cu"
|
assert Path(compiler.kernel_path("uniform.cu")).resolve() == root / "lbm/kernels/uniform.cu"
|
||||||
|
|
||||||
|
|
||||||
def test_two_contexts_interleaved_and_non_lifo_close():
|
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)
|
first = make_sim(nx=32, ny=16)
|
||||||
second = make_sim(nx=48, ny=24, streaming="esopull", shifted=True)
|
second = make_sim(nx=48, ny=24, streaming="esopull", shifted=True)
|
||||||
try:
|
try:
|
||||||
assert first._build_dir != second._build_dir
|
assert first._module is not second._module
|
||||||
assert cuda.Context.get_current() == before
|
assert cuda.Context.get_current() == before
|
||||||
first.initialize()
|
first.initialize()
|
||||||
second.initialize()
|
second.initialize()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
#define NZ 1
|
#define NZ 1
|
||||||
#define NT 1
|
#define NT 1
|
||||||
#define COLLISION_MODEL 0
|
#define COLLISION_MODEL 0
|
||||||
|
#define BLOCK_ROOT_BC 2
|
||||||
#define USE_LES 0
|
#define USE_LES 0
|
||||||
#define RHO 1.f
|
#define RHO 1.f
|
||||||
#define U0 .025f
|
#define U0 .025f
|
||||||
@@ -40,7 +41,7 @@ inline float __fdiv_rn(float a,float b){return a/b;}
|
|||||||
#include "boundary/inlet_outlet.cuh"
|
#include "boundary/inlet_outlet.cuh"
|
||||||
#include "step/one_step_double.cu"
|
#include "step/one_step_double.cu"
|
||||||
#else
|
#else
|
||||||
constexpr unsigned char KIND_OUTER=2;
|
#include "core/cell_kind.cuh"
|
||||||
#include "step/block_channel.cu"
|
#include "step/block_channel.cu"
|
||||||
#endif
|
#endif
|
||||||
int main(){
|
int main(){
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""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,6 +83,32 @@ assert abs(sum(compute_feq_d2q9(1.,0.,0.)) - 1.) < 1e-6
|
|||||||
|
|
||||||
|
|
||||||
class BuildContracts(unittest.TestCase):
|
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
|
@staticmethod
|
||||||
def fake_compile(command, check):
|
def fake_compile(command, check):
|
||||||
source = Path(command[-3])
|
source = Path(command[-3])
|
||||||
@@ -100,8 +126,7 @@ class BuildContracts(unittest.TestCase):
|
|||||||
def work(nx, count):
|
def work(nx, count):
|
||||||
build = compiler.generate_config(LBMConfig(nx=nx), count, build_root=root)
|
build = compiler.generate_config(LBMConfig(nx=nx), count, build_root=root)
|
||||||
barrier.wait()
|
barrier.wait()
|
||||||
# Compatibility form is context-local even without explicit path.
|
output = compiler.compile_kernel(build_dir=build)
|
||||||
output = compiler.compile_kernel()
|
|
||||||
data = json.loads(Path(output).read_text())
|
data = json.loads(Path(output).read_text())
|
||||||
self.assertEqual(data, {"nx": nx, "objects": count})
|
self.assertEqual(data, {"nx": nx, "objects": count})
|
||||||
metadata = json.loads(Path(output + ".json").read_text())
|
metadata = json.loads(Path(output + ".json").read_text())
|
||||||
@@ -130,7 +155,7 @@ class BuildContracts(unittest.TestCase):
|
|||||||
compiler.cleanup_build_dir(a)
|
compiler.cleanup_build_dir(a)
|
||||||
compiler.cleanup_build_dir(b)
|
compiler.cleanup_build_dir(b)
|
||||||
with self.assertRaises(RuntimeError):
|
with self.assertRaises(RuntimeError):
|
||||||
compiler.compile_kernel()
|
compiler.compile_kernel(build_dir=a) # removed snapshot is never compiled
|
||||||
|
|
||||||
def test_failed_compile_cannot_implicitly_load_previous_artifact(self):
|
def test_failed_compile_cannot_implicitly_load_previous_artifact(self):
|
||||||
with tempfile.TemporaryDirectory() as root:
|
with tempfile.TemporaryDirectory() as root:
|
||||||
@@ -143,8 +168,7 @@ class BuildContracts(unittest.TestCase):
|
|||||||
with self.assertRaises(subprocess.CalledProcessError):
|
with self.assertRaises(subprocess.CalledProcessError):
|
||||||
compiler.compile_kernel(build_dir=build)
|
compiler.compile_kernel(build_dir=build)
|
||||||
self.assertEqual(Path(previous).read_bytes(), old_bytes)
|
self.assertEqual(Path(previous).read_bytes(), old_bytes)
|
||||||
with self.assertRaisesRegex(RuntimeError, "No compiled PTX"):
|
self.assertEqual(sorted(Path(build).glob("uniform-*.ptx")), []) # no partial output
|
||||||
compiler.load_module()
|
|
||||||
compiler.cleanup_build_dir(build)
|
compiler.cleanup_build_dir(build)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ def test_masks_and_minimal_footprint():
|
|||||||
def test_actual_closure_read_conflicts():
|
def test_actual_closure_read_conflicts():
|
||||||
pair, layout, targets = transfer((6, 6, 18, 14))
|
pair, layout, targets = transfer((6, 6, 18, 14))
|
||||||
validate_channel_transfers(pair, layout, targets)
|
validate_channel_transfers(pair, layout, targets)
|
||||||
for target in (layout.slot(layout.owner[5, -1]-1, 2),
|
for target in (layout.slot(int(layout.owner[5, -1]) - 1, 2), # integer under NumPy 1 and 2
|
||||||
layout.slot(layout.owner[1, 4], 4),
|
layout.slot(layout.owner[1, 4], 4),
|
||||||
layout.slot(layout.owner[-2, 4], 3)):
|
layout.slot(layout.owner[-2, 4], 3)):
|
||||||
with pytest.raises(ValueError, match="immutable"):
|
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
|
from .test_refinement_three_level_multipatch import host_module
|
||||||
host = host_module()
|
host = host_module()
|
||||||
storage = {}
|
storage = {}
|
||||||
host.compiler.load_module.return_value.get_global.side_effect = lambda name: (name, 12 if name == "d_block_channel" else 20)
|
host.compiler.build_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.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_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)])
|
host.cuda.memcpy_dtoh.side_effect = lambda a, ptr: np.copyto(a, storage[id(ptr)])
|
||||||
context = Mock(activate=nullcontext, sm_arch="sm_70")
|
context = Mock(activate=nullcontext, sm_arch="sm_70")
|
||||||
cfg = LBMConfig(nx=24, ny=20, viscosity=.1, velocity=.025)
|
cfg = LBMConfig(nx=24, ny=20, viscosity=.1, velocity=.025)
|
||||||
backend = host.FixedPatchStepper(cfg, (6, 6, 18, 14), context=context,
|
backend = host.FixedPatchStepper(cfg, context=context, coarse_boundary="channel",
|
||||||
coarse_boundary="channel", base_block=(4, 6), fine_block=(4, 4))
|
refinements=[(6, 6, 18, 14)], level_blocks=[(4, 6), (4, 4)])
|
||||||
try:
|
try:
|
||||||
weights = np.array([4/9]+[1/9]*4+[1/36]*4)[:, None, None]
|
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]
|
arrays = [np.broadcast_to(weights, level.shape).copy() for level in backend._levels]
|
||||||
@@ -96,6 +96,7 @@ def test_evolved_ghost_snapshot_and_physical_download():
|
|||||||
arrays[0][0, 0, 0] = np.nan
|
arrays[0][0, 0, 0] = np.nan
|
||||||
with pytest.raises(ValueError, match="finite"):
|
with pytest.raises(ValueError, match="finite"):
|
||||||
backend.initialize(*arrays)
|
backend.initialize(*arrays)
|
||||||
assert backend._levels[0].kernel == host.compiler.load_module.return_value.get_function("BlockChannelStep")
|
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]
|
||||||
finally:
|
finally:
|
||||||
backend.close()
|
backend.close()
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""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()
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"""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,6 +8,7 @@ import numpy as np
|
|||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
REPO = ROOT
|
REPO = ROOT
|
||||||
sys.path.insert(0, str(ROOT / "tests/validation"))
|
sys.path.insert(0, str(ROOT / "tests/validation"))
|
||||||
|
sys.path.insert(0, str(ROOT / "tests/unit"))
|
||||||
sys.path.insert(0, str(REPO / "src"))
|
sys.path.insert(0, str(REPO / "src"))
|
||||||
from run_jet_circle_cv import (
|
from run_jet_circle_cv import (
|
||||||
CX,
|
CX,
|
||||||
@@ -16,8 +17,10 @@ from run_jet_circle_cv import (
|
|||||||
case,
|
case,
|
||||||
state,
|
state,
|
||||||
cv,
|
cv,
|
||||||
|
cv_from_macros,
|
||||||
aperture_probe,
|
aperture_probe,
|
||||||
measured_aperture_flux,
|
measured_aperture_flux,
|
||||||
|
PIN,
|
||||||
verify,
|
verify,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +31,39 @@ def eq(rho, u, v):
|
|||||||
|
|
||||||
|
|
||||||
class Host(unittest.TestCase):
|
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):
|
def test_descriptor_and_uniform_closed_flux(self):
|
||||||
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
||||||
|
|
||||||
@@ -132,7 +168,13 @@ class Host(unittest.TestCase):
|
|||||||
self.assertEqual(a["steps"] * 4, b["steps"])
|
self.assertEqual(a["steps"] * 4, b["steps"])
|
||||||
|
|
||||||
def test_source_pin(self):
|
def test_source_pin(self):
|
||||||
verify(REPO)
|
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))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -188,6 +188,55 @@ def jet_case(device, jets):
|
|||||||
return field, bodies
|
return field, bodies
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_surface_exchange_for_jet_preserves_aperture_at_zero_and_reverse(device):
|
||||||
|
from CelerisLab.simulation import Simulation
|
||||||
|
jet = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER, inward=False), "quartic", 2e-3)
|
||||||
|
field, bodies = jet_case(device, [jet])
|
||||||
|
sim = SimpleNamespace(bodies=bodies, field=field, _initialized=True,
|
||||||
|
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||||
|
cl = field.curved
|
||||||
|
assert bodies.get(0).surface_actuation is None
|
||||||
|
aperture, velocity = cl.aperture, cl.surface_velocity.copy()
|
||||||
|
assert aperture.any() and (~aperture).any()
|
||||||
|
# Synthetic per-link exchanges test readback/splitting, not kernel physics.
|
||||||
|
values = np.arange(cl.count * 3, dtype=np.float32).reshape(-1, 3)
|
||||||
|
device.htod(cl.link_exchange_gpu, values)
|
||||||
|
for amplitude in (2e-3, 0., -1e-3):
|
||||||
|
Simulation.set_jet_amplitude(sim, 0, amplitude)
|
||||||
|
result = Simulation.read_surface_exchange(sim, 0)
|
||||||
|
assert cl.aperture is aperture
|
||||||
|
np.testing.assert_array_equal(aperture, cl.jet_id >= 0)
|
||||||
|
np.testing.assert_allclose(cl.surface_velocity, velocity * (amplitude / 2e-3))
|
||||||
|
for name, mask in (("wall", ~aperture), ("aperture", aperture)):
|
||||||
|
np.testing.assert_array_equal(result[name], values[mask].sum(axis=0, dtype=np.float64))
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_surface_exchange_keeps_legacy_and_passive_guards(device):
|
||||||
|
from CelerisLab.simulation import Simulation
|
||||||
|
field = make_field(device)
|
||||||
|
bodies = manager(field, [circle(SurfaceActuation(0., .6, .002), radius=2.)])
|
||||||
|
bodies.sync_to_gpu(field)
|
||||||
|
sim = SimpleNamespace(bodies=bodies, field=field,
|
||||||
|
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||||
|
for value in Simulation.read_surface_exchange(sim, 0).values():
|
||||||
|
np.testing.assert_array_equal(value, 0.)
|
||||||
|
field.curved.free_gpu_columns()
|
||||||
|
with pytest.raises(RuntimeError, match="diagnostic is allocated"):
|
||||||
|
Simulation.read_surface_exchange(sim, 0)
|
||||||
|
|
||||||
|
# An exchange buffer elsewhere in the scene must not admit a passive body.
|
||||||
|
for jets in ([], [Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER))]):
|
||||||
|
field, bodies = jet_case(device, jets)
|
||||||
|
passive = bodies.add(circle(center=(2., 3.), radius=.6))
|
||||||
|
bodies.sync_to_gpu(field)
|
||||||
|
sim.bodies, sim.field = bodies, field
|
||||||
|
with pytest.raises(ValueError, match="Body has no surface actuation"):
|
||||||
|
Simulation.read_surface_exchange(sim, passive)
|
||||||
|
if not jets:
|
||||||
|
with pytest.raises(ValueError, match="Body has no surface actuation"):
|
||||||
|
Simulation.read_surface_exchange(sim, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_manager_passes_true_hits_and_reuploads_only_velocity(device):
|
def test_manager_passes_true_hits_and_reuploads_only_velocity(device):
|
||||||
# CPU memory double only: checks host/device bookkeeping, not GPU numerics.
|
# 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)
|
jet = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER, inward=False), "quartic", 2e-3)
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
|||||||
from run_uniform_les_flow import source_guard, decode_macros, HERE, HEADER
|
from run_uniform_les_flow import source_guard, decode_macros, HERE, HEADER
|
||||||
|
|
||||||
ROOT = HERE.parents[1]
|
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):
|
class GuardTests(unittest.TestCase):
|
||||||
@@ -42,19 +46,23 @@ class GuardTests(unittest.TestCase):
|
|||||||
decode_macros(invalid, w, cx, cy)
|
decode_macros(invalid, w, cx, cy)
|
||||||
|
|
||||||
def test_current_corrected_pins(self):
|
def test_current_corrected_pins(self):
|
||||||
self.assertEqual(
|
with TemporaryDirectory() as directory:
|
||||||
source_guard(ROOT, "corrected", "a135252")["base_commit"],
|
root = historical_tree(A135, directory)
|
||||||
"a135252ae78b47b57d7e77d19001700b3bedac8b",
|
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_corrected_and_tamper_guard(self):
|
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_a135252.json").read_text())
|
||||||
with TemporaryDirectory() as directory:
|
with TemporaryDirectory() as directory:
|
||||||
root = Path(directory)
|
origin = historical_tree(A135, Path(directory) / "origin")
|
||||||
|
root = Path(directory) / "fixture"
|
||||||
for name in pins["sources"]:
|
for name in pins["sources"]:
|
||||||
dest = root / name
|
dest = root / name
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copyfile(ROOT / name, dest)
|
shutil.copyfile(origin / name, dest)
|
||||||
with patch(
|
with patch(
|
||||||
"run_uniform_les_flow.subprocess.check_output",
|
"run_uniform_les_flow.subprocess.check_output",
|
||||||
return_value="isolated-fixture\n",
|
return_value="isolated-fixture\n",
|
||||||
@@ -87,23 +95,26 @@ class GuardTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_current_manifest_pins_actual_corrected_tree(self):
|
def test_current_manifest_pins_actual_corrected_tree(self):
|
||||||
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
|
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
|
||||||
tracked = subprocess.check_output(["git", "-C", str(ROOT), "ls-files", "src"], text=True)
|
self.assertEqual(pins["base_commit"], A135)
|
||||||
self.assertEqual(set(pins["sources"]), set(tracked.splitlines()))
|
self.assertEqual(set(pins["sources"]), tracked(A135))
|
||||||
for name, expected in pins["sources"].items():
|
with TemporaryDirectory() as directory:
|
||||||
self.assertEqual(hashlib.sha256((ROOT / name).read_bytes()).hexdigest(), expected)
|
root = historical_tree(A135, directory)
|
||||||
self.assertEqual(pins["sources"][HEADER], pins["candidate_header_sha256"])
|
for name, expected in pins["sources"].items():
|
||||||
self.assertNotEqual(pins["sources"][HEADER], pins["baseline_header_sha256"])
|
self.assertEqual(hashlib.sha256((root / name).read_bytes()).hexdigest(), expected)
|
||||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
self.assertEqual(pins["sources"][HEADER], pins["candidate_header_sha256"])
|
||||||
source_guard(ROOT, "baseline", "a135252")
|
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):
|
def test_named_baseline_accepts_only_reversed_header(self):
|
||||||
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
|
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
|
||||||
with TemporaryDirectory() as directory:
|
with TemporaryDirectory() as directory:
|
||||||
root = Path(directory)
|
origin = historical_tree(A135, Path(directory) / "origin")
|
||||||
|
root = Path(directory) / "fixture"
|
||||||
for name in pins["sources"]:
|
for name in pins["sources"]:
|
||||||
dest = root / name
|
dest = root / name
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copyfile(ROOT / name, dest)
|
shutil.copyfile(origin / name, dest)
|
||||||
subprocess.run(["git", "apply", "--reverse", str(HERE / "les/data/smagorinsky_normalization.patch")], cwd=root, check=True)
|
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"):
|
with patch("run_uniform_les_flow.subprocess.check_output", return_value="baseline-fixture\n"):
|
||||||
source_guard(root, "baseline", "a135252")
|
source_guard(root, "baseline", "a135252")
|
||||||
|
|||||||
@@ -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)
|
source=re.sub(r"(#define\s+"+key+r"\s+)\S+",lambda match: match[1]+str(value),source)
|
||||||
path.write_text(source)
|
path.write_text(source)
|
||||||
result=subprocess.run([nvcc,"-ptx","-arch=sm_70","-std=c++17",
|
result=subprocess.run([nvcc,"-ptx","-arch=sm_70","-std=c++17",
|
||||||
str(copy/"kernel_v2.cu"),"-o",str(tmp_path/"kernel.ptx")],
|
str(copy/"uniform.cu"),"-o",str(tmp_path/"kernel.ptx")],
|
||||||
capture_output=True,text=True)
|
capture_output=True,text=True)
|
||||||
assert result.returncode == 0, result.stderr
|
assert result.returncode == 0, result.stderr
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import pytest
|
|||||||
|
|
||||||
import CelerisLab
|
import CelerisLab
|
||||||
from CelerisLab.config import LBMConfig, load_lbm_config
|
from CelerisLab.config import LBMConfig, load_lbm_config
|
||||||
from CelerisLab.cuda import compiler_v2 as compiler
|
from CelerisLab.cuda import compiler
|
||||||
from CelerisLab.common.checkpoint import save_checkpoint, load_checkpoint
|
from CelerisLab.common.checkpoint import save_checkpoint, load_checkpoint
|
||||||
from .test_numerical_operators import compile_run
|
from .test_numerical_operators import compile_run
|
||||||
from .test_state_regressions import device, make_field, manager
|
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):
|
def test_planar_helper_moments_and_known_populations(tmp_path):
|
||||||
root = Path(__file__).resolve().parents[2]
|
root = Path(__file__).resolve().parents[2]
|
||||||
assert Path(CelerisLab.__file__).resolve() == root/'src/CelerisLab/__init__.py'
|
assert Path(CelerisLab.__file__).resolve() == root/'src/CelerisLab/__init__.py'
|
||||||
assert Path(compiler.kernel_path('kernel_v2.cu')).resolve() == root/'src/CelerisLab/lbm/kernels/kernel_v2.cu'
|
assert Path(compiler.kernel_path('uniform.cu')).resolve() == root/'src/CelerisLab/lbm/kernels/uniform.cu'
|
||||||
lines = compile_run(tmp_path, 'planar_velocity.cpp', dict(WALL_NORMAL_VELOCITY='0.0f'))
|
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])
|
cx = np.array([0,1,-1,0,0,1,-1,1,-1])
|
||||||
cy = np.array([0,0,0,1,-1,1,-1,-1,1])
|
cy = np.array([0,0,0,1,-1,1,-1,-1,1])
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class ProjectedKernelTests(unittest.TestCase):
|
|||||||
targets = child.owner.ravel()[p.ghost_fine]
|
targets = child.owner.ravel()[p.ghost_fine]
|
||||||
donors = parent.owner.ravel()[p.ghost_coarse]
|
donors = parent.owner.ravel()[p.ghost_coarse]
|
||||||
source = (Path(__file__).resolve().parents[2] /
|
source = (Path(__file__).resolve().parents[2] /
|
||||||
"src/CelerisLab/lbm/kernels/blocks.cu").read_text()
|
"src/CelerisLab/lbm/kernels/step/block_transfer.cu").read_text()
|
||||||
kernel = source.split('extern "C" __global__ void BlockExplodeProjected(',1)[1]
|
kernel = source.split('extern "C" __global__ void BlockExplodeProjected(',1)[1]
|
||||||
kernel = 'void BlockExplodeProjected(' + kernel.split('// Targets are receiver-local SOURCE slots',1)[0]
|
kernel = 'void BlockExplodeProjected(' + kernel.split('// Targets are receiver-local SOURCE slots',1)[0]
|
||||||
def array(name, values, kind):
|
def array(name, values, kind):
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def backend(**options):
|
|||||||
name=name, side_effect=lambda *a, **kw: calls.append((name, a, kw)))
|
name=name, side_effect=lambda *a, **kw: calls.append((name, a, kw)))
|
||||||
return module
|
return module
|
||||||
|
|
||||||
host.compiler.load_module.side_effect = load
|
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_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)])
|
host.cuda.memcpy_dtoh.side_effect = lambda a, ptr: np.copyto(a, storage[id(ptr)])
|
||||||
cfg = options.pop('cfg', CFG)
|
cfg = options.pop('cfg', CFG)
|
||||||
@@ -146,7 +146,6 @@ class ProjectedPairDispatch(unittest.TestCase):
|
|||||||
for cfg, changes in [(CFG, {'explosion':'unknown'}),
|
for cfg, changes in [(CFG, {'explosion':'unknown'}),
|
||||||
(CFG, {'coarse_boundary':'slot_reservoir'}),
|
(CFG, {'coarse_boundary':'slot_reservoir'}),
|
||||||
(CFG, {'coarse_boundary':'channel'}),
|
(CFG, {'coarse_boundary':'channel'}),
|
||||||
(CFG, {'level_blocks':[None,None], 'refinements':[(6,6,26,26)]}),
|
|
||||||
(CFG, {'level_objects':[(),(object(),)]}),
|
(CFG, {'level_objects':[(),(object(),)]}),
|
||||||
(CFG, {'level_jets':[(),(object(),)]}),
|
(CFG, {'level_jets':[(),(object(),)]}),
|
||||||
(replace(CFG,collision='MRT'), {}),
|
(replace(CFG,collision='MRT'), {}),
|
||||||
@@ -155,9 +154,8 @@ class ProjectedPairDispatch(unittest.TestCase):
|
|||||||
host.FixedPatchStepper(cfg, **dict(options, **changes))
|
host.FixedPatchStepper(cfg, **dict(options, **changes))
|
||||||
context.activate.assert_not_called()
|
context.activate.assert_not_called()
|
||||||
host.cuda.mem_alloc.assert_not_called()
|
host.cuda.mem_alloc.assert_not_called()
|
||||||
# The legacy constant path still allows MRT and dense storage.
|
# The constant path still allows MRT.
|
||||||
for options in ({'cfg':replace(CFG,collision='MRT')},
|
for options in ({'cfg':replace(CFG,collision='MRT')},):
|
||||||
{'level_blocks':[None,None], 'refinements':[(6,6,26,26)]}):
|
|
||||||
run, _, _, _, _ = backend(**options)
|
run, _, _, _, _ = backend(**options)
|
||||||
run.close()
|
run.close()
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def backend(observe=True, obj=None):
|
|||||||
module = Mock()
|
module = Mock()
|
||||||
module.get_global.return_value = (Mock(), 20)
|
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)))
|
module.get_function.side_effect = lambda name: Mock(side_effect=lambda *a, **k: calls.append((name, a, k)))
|
||||||
host.compiler.load_module.return_value = module
|
host.compiler.build_module.return_value = module
|
||||||
host.cuda.mem_alloc.side_effect = lambda n: Mock(size=n)
|
host.cuda.mem_alloc.side_effect = lambda n: Mock(size=n)
|
||||||
host.cuda.memcpy_dtoh.side_effect = lambda array, pointer: array.fill(0)
|
host.cuda.memcpy_dtoh.side_effect = lambda array, pointer: array.fill(0)
|
||||||
ctx = Mock(activate=nullcontext, sm_arch='sm_70')
|
ctx = Mock(activate=nullcontext, sm_arch='sm_70')
|
||||||
@@ -153,14 +153,6 @@ class RawExchange(unittest.TestCase):
|
|||||||
run._stream.synchronize.side_effect = None
|
run._stream.synchronize.side_effect = None
|
||||||
run.close()
|
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):
|
def test_all_depth_weights(self):
|
||||||
for level in (1, 2, 3, 7):
|
for level in (1, 2, 3, 7):
|
||||||
self._check_weights_split_and_window(level)
|
self._check_weights_split_and_window(level)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import pytest
|
|||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
sys.path.insert(0, str(ROOT / "tests/validation"))
|
sys.path.insert(0, str(ROOT / "tests/validation"))
|
||||||
|
sys.path.insert(0, str(ROOT / "tests/unit"))
|
||||||
import re100_helpers as h
|
import re100_helpers as h
|
||||||
import periodic as p
|
import periodic as p
|
||||||
|
|
||||||
@@ -62,13 +63,16 @@ def test_periodic_synthetic():
|
|||||||
assert not p.assess(rows()[:100])["periodic"]
|
assert not p.assess(rows()[:100])["periodic"]
|
||||||
|
|
||||||
|
|
||||||
def test_production_unmodified():
|
def test_production_unmodified(tmp_path):
|
||||||
|
from historical_tree import historical_tree
|
||||||
|
|
||||||
m = json.loads(
|
m = json.loads(
|
||||||
(ROOT / "tests/validation/data/dfg_re100/source_manifest.json").read_text()
|
(ROOT / "tests/validation/data/dfg_re100/source_manifest.json").read_text()
|
||||||
)
|
)
|
||||||
assert m["commit"] == h.PIN
|
assert m["commit"] == h.PIN
|
||||||
|
origin = historical_tree(m["commit"], tmp_path)
|
||||||
for name, digest in m["production_sha256"].items():
|
for name, digest in m["production_sha256"].items():
|
||||||
assert hashlib.sha256((ROOT / name).read_bytes()).hexdigest() == digest, name
|
assert hashlib.sha256((origin / name).read_bytes()).hexdigest() == digest, name
|
||||||
|
|
||||||
|
|
||||||
def test_reference():
|
def test_reference():
|
||||||
|
|||||||
+212
-10
@@ -7,7 +7,10 @@ import numpy as np
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
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
|
import run_refined_dfg as r
|
||||||
|
from historical_tree import historical_tree
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
h, periodic = r.helpers(r.DEFAULT_SOURCE)
|
h, periodic = r.helpers(r.DEFAULT_SOURCE)
|
||||||
|
|
||||||
@@ -17,11 +20,24 @@ def test_source_pin():
|
|||||||
assert historical["commit"].startswith("cd3a657")
|
assert historical["commit"].startswith("cd3a657")
|
||||||
mrt_history = json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text())
|
mrt_history = json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text())
|
||||||
assert mrt_history["commit"].startswith("3ea131e")
|
assert mrt_history["commit"].startswith("3ea131e")
|
||||||
assert r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")["commit"].startswith("a233c90")
|
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
|
||||||
|
r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")
|
||||||
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
|
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
|
||||||
r.verify_source(r.DEFAULT_SOURCE, "MRT")
|
r.verify_source(r.DEFAULT_SOURCE, "MRT")
|
||||||
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
|
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
|
||||||
r.verify_source(r.DEFAULT_SOURCE, "SRT")
|
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
|
||||||
|
|
||||||
|
|
||||||
def test_matched_units_and_planes():
|
def test_matched_units_and_planes():
|
||||||
@@ -141,28 +157,47 @@ def test_axis_specific_sampling_phase():
|
|||||||
np.testing.assert_allclose(fine_y - ((k + 43) - 0.5) * 0.00125, 0, atol=1e-15)
|
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():
|
def test_full_requires_completed_pilot():
|
||||||
with unittest.TestCase().assertRaisesRegex(ValueError, "pilot-results"):
|
with unittest.TestCase().assertRaisesRegex(ValueError, "pilot-results"):
|
||||||
r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE, "MRT", "window"))
|
r.verify_pilot(None, frozen_manifest("window"))
|
||||||
|
|
||||||
|
|
||||||
def pilot_fixture(path, observation_mode="window"):
|
def pilot_fixture(path, observation_mode="window", jet_scenario="none", case_selection="both", uniform_diameter=80, placement="wide"):
|
||||||
"""Synthetic contract fixture only; never a physical result or run receipt."""
|
"""Synthetic contract fixture only; never a physical result or run receipt."""
|
||||||
manifest = (r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")
|
# Receipt-logic fixture: the frozen manifest verify_source returns for a matching tree.
|
||||||
if observation_mode == "window" else
|
# Source identity itself is checked only by test_source_pin.
|
||||||
json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text()))
|
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)
|
||||||
(path / "plan.json").write_text(
|
(path / "plan.json").write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
dict(
|
dict(
|
||||||
source_manifest=manifest,
|
source_manifest=manifest,
|
||||||
|
**selection,
|
||||||
collision="MRT",
|
collision="MRT",
|
||||||
|
jet_scenario=jet_scenario,
|
||||||
observation_mode=observation_mode,
|
observation_mode=observation_mode,
|
||||||
bounds_root_edge=list(r.BOUNDS),
|
bounds_root_edge=list(r.PLACEMENTS[placement]),
|
||||||
runner_sha256=r.runner_hash(),
|
runner_sha256=r.runner_hash(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for case, diameter in (("uniform", 80), ("refined", 40)):
|
for case, diameter in r.selected_cases(case_selection, uniform_diameter):
|
||||||
dest = path / case
|
dest = path / case
|
||||||
dest.mkdir()
|
dest.mkdir()
|
||||||
interval = round(0.02 * diameter / r.SPEED)
|
interval = round(0.02 * diameter / r.SPEED)
|
||||||
@@ -189,7 +224,9 @@ def pilot_fixture(path, observation_mode="window"):
|
|||||||
json.dumps(
|
json.dumps(
|
||||||
dict(
|
dict(
|
||||||
case=case,
|
case=case,
|
||||||
|
**selection,
|
||||||
collision="MRT",
|
collision="MRT",
|
||||||
|
jet_scenario=jet_scenario,
|
||||||
observation_mode=observation_mode,
|
observation_mode=observation_mode,
|
||||||
mode="pilot",
|
mode="pilot",
|
||||||
status="pilot_complete",
|
status="pilot_complete",
|
||||||
@@ -206,7 +243,9 @@ def pilot_fixture(path, observation_mode="window"):
|
|||||||
json.dumps(
|
json.dumps(
|
||||||
dict(
|
dict(
|
||||||
case=case,
|
case=case,
|
||||||
|
**selection,
|
||||||
collision="MRT",
|
collision="MRT",
|
||||||
|
jet_scenario=jet_scenario,
|
||||||
observation_mode=observation_mode,
|
observation_mode=observation_mode,
|
||||||
mode="pilot",
|
mode="pilot",
|
||||||
step=interval,
|
step=interval,
|
||||||
@@ -341,7 +380,7 @@ def test_record_actual_checkout_separately():
|
|||||||
actual = checkout_provenance(r.DEFAULT_SOURCE)
|
actual = checkout_provenance(r.DEFAULT_SOURCE)
|
||||||
assert len(actual["checkout_revision"]) == 40
|
assert len(actual["checkout_revision"]) == 40
|
||||||
assert isinstance(actual["checkout_dirty"], bool)
|
assert isinstance(actual["checkout_dirty"], bool)
|
||||||
assert r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")["production_origin_commit"].startswith("d930")
|
assert selected_manifest("MRT", "window")["production_origin_commit"].startswith("d930")
|
||||||
|
|
||||||
|
|
||||||
def test_window_plan_preserves_physics():
|
def test_window_plan_preserves_physics():
|
||||||
@@ -351,7 +390,7 @@ def test_window_plan_preserves_physics():
|
|||||||
assert baseline.pop("observation_mode") == "last_base_step"
|
assert baseline.pop("observation_mode") == "last_base_step"
|
||||||
assert window.pop("observation_mode") == "window"
|
assert window.pop("observation_mode") == "window"
|
||||||
assert baseline == window
|
assert baseline == window
|
||||||
assert r.verify_source(r.DEFAULT_SOURCE, collision, "window")["observation_mode"] == "window"
|
assert selected_manifest(collision, "window")["observation_mode"] == "window"
|
||||||
with unittest.TestCase().assertRaises(ValueError):
|
with unittest.TestCase().assertRaises(ValueError):
|
||||||
r.verify_source(r.DEFAULT_SOURCE, "MRT", "unknown")
|
r.verify_source(r.DEFAULT_SOURCE, "MRT", "unknown")
|
||||||
|
|
||||||
@@ -451,6 +490,169 @@ def test_window_contract_rejection():
|
|||||||
assert not backend.events
|
assert not backend.events
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_jet_configuration_and_geometry():
|
||||||
|
for diameter, center, radius in ((40, (80.5, 81), 20), (80, (160, 160.5), 40)):
|
||||||
|
cfg, _, _ = r.config(h, diameter, "MRT", "fixed")
|
||||||
|
assert not cfg["method"]["ddf_shifting"] and not cfg["method"]["les"]["enabled"]
|
||||||
|
jets = r.public_jets(center, radius, "fixed")
|
||||||
|
assert [j.amplitude for j in jets] == [.001, -.001]
|
||||||
|
assert all(j.field.inward is False and j.profile == "parabolic" for j in jets)
|
||||||
|
assert all(j.region.r_max == radius * 1.005 for j in jets)
|
||||||
|
assert all(j.amplitude == 0 for j in r.public_jets(center, radius, "zero"))
|
||||||
|
cfg, _, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT", "fixed")
|
||||||
|
assert not cfg.ddf_shifting and len(host.boundary_cells) > 0
|
||||||
|
assert r.public_jets((0, 0), 1, "none") == ()
|
||||||
|
with unittest.TestCase().assertRaises(ValueError):
|
||||||
|
r.config(h, 40, "SRT", "fixed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_jet_reversal_boundary_and_windows():
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
handle = SimpleNamespace(set_jet_amplitude=lambda i, a: calls.append((i, a)))
|
||||||
|
for diameter in (40, 80):
|
||||||
|
boundary = round(250 * diameter / r.SPEED)
|
||||||
|
assert r.jet_interval(handle, "fixed", boundary - 3, 80, diameter) == (3, None)
|
||||||
|
switch = round(300 * diameter / r.SPEED)
|
||||||
|
assert r.jet_interval(handle, "reverse", switch - 3, 80, diameter) == (3, None)
|
||||||
|
assert not calls
|
||||||
|
n, event = r.jet_interval(handle, "reverse", switch, 80, diameter)
|
||||||
|
assert n == 80 and event["step"] == switch and event["tstar"] == 300
|
||||||
|
assert calls == [(0, -.001), (1, .001)]
|
||||||
|
calls.clear()
|
||||||
|
assert r.jet_interval(handle, "reverse", switch + 80, 80, diameter) == (80, None)
|
||||||
|
assert r.jet_interval(handle, "fixed", switch, 80, diameter) == (80, None)
|
||||||
|
assert not calls
|
||||||
|
rows = [dict(tstar=t, tstar_force=t - .5, cd=2.0, cl=-.1) for t in range(251, 306)]
|
||||||
|
windows = r.jet_diagnostics(rows, "reverse")
|
||||||
|
assert len(windows) == 6 and all(w["complete"] for w in windows)
|
||||||
|
assert all(w["cd"]["mean"] == 2 and np.isclose(w["cl"]["mean"], -.1) for w in windows)
|
||||||
|
assert windows[-1]["covered_tstar"] == 5
|
||||||
|
with unittest.TestCase().assertRaisesRegex(ValueError, "crosses"):
|
||||||
|
r.jet_diagnostics([dict(tstar=250.5, tstar_force=250, cd=1, cl=0)], "fixed")
|
||||||
|
assert r.plan(h, "MRT", "window", "reverse")["modes"]["full_tstar"] == 305
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_jet_source_and_pilot_separation():
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
path = Path(tmp)
|
||||||
|
manifest = pilot_fixture(path, jet_scenario="fixed")
|
||||||
|
assert manifest["source_capture"].startswith("current checkout")
|
||||||
|
assert len(manifest["checkout_revision"]) == 40
|
||||||
|
assert r.verify_pilot(path, manifest, "MRT", "window", "fixed") == 3606.0
|
||||||
|
for scenario in ("none", "zero", "reverse"):
|
||||||
|
with unittest.TestCase().assertRaisesRegex(ValueError, "Jet scenario"):
|
||||||
|
r.verify_pilot(path, manifest, "MRT", "window", scenario)
|
||||||
|
for relative in ("uniform/summary.json", "refined/ready.json"):
|
||||||
|
receipt = path / relative
|
||||||
|
record = json.loads(receipt.read_text())
|
||||||
|
receipt.write_text(json.dumps(dict(record, jet_scenario="zero")))
|
||||||
|
with unittest.TestCase().assertRaises(ValueError):
|
||||||
|
r.verify_pilot(path, manifest, "MRT", "window", "fixed")
|
||||||
|
receipt.write_text(json.dumps(record))
|
||||||
|
with unittest.TestCase().assertRaises(ValueError):
|
||||||
|
r.verify_source(r.DEFAULT_SOURCE, "MRT", "last_base_step", "fixed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cv_snapshot_common_units_and_window_endpoints():
|
||||||
|
records = []
|
||||||
|
for refined, shape, origin, diameter, impulse in (
|
||||||
|
(True, (240, 568), (38., 22.), 40, [4., -8.]),
|
||||||
|
(False, (330, 1761), (.25, .5), 80, [16., -32.]),
|
||||||
|
(False, (658, 3521), (.375, .75), 160, [64., -128.]),
|
||||||
|
):
|
||||||
|
rho, ux, uy, fluid = np.ones(shape), np.full(shape, .01), np.zeros(shape), np.ones(shape, bool)
|
||||||
|
begin, n = round(290*diameter/r.SPEED), round(.02*diameter/r.SPEED)
|
||||||
|
record = r.cv_snapshot(rho, ux, uy, fluid, origin, refined, begin, n, diameter, impulse)
|
||||||
|
records.append(record)
|
||||||
|
np.testing.assert_allclose(record["outward_flux"], 0, atol=1e-12)
|
||||||
|
assert r.cv_selected(begin, diameter, "full") and not r.cv_selected(begin-n, diameter, "full")
|
||||||
|
end = round(300*diameter/r.SPEED)
|
||||||
|
assert r.cv_selected(end, diameter, "full") and not r.cv_selected(end+n, diameter, "full")
|
||||||
|
assert r.cv_selected(n, diameter, "pilot")
|
||||||
|
assert sum(r.cv_selected(step, diameter, "full") for step in range(begin-n, end+n+1, n)) == 501
|
||||||
|
for key in ("time_root", "interval_start_root", "interval_dt_root", "interval_impulse_root", "inventory"):
|
||||||
|
for record in records[1:]:
|
||||||
|
np.testing.assert_allclose(records[0][key], record[key], rtol=0, atol=1e-10)
|
||||||
|
assert records[0]["interval_dt_root"] == 80 and np.isclose(records[0]["tstar"], 290.02)
|
||||||
|
np.testing.assert_array_equal(records[0]["interval_impulse_root"], [4, -8])
|
||||||
|
|
||||||
|
|
||||||
|
def test_cv_pilot_mode_and_helper_provenance():
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
manifest = r.verify_source(r.DEFAULT_SOURCE, "MRT", "window", "fixed", True)
|
||||||
|
assert "tests/validation/run_jet_circle_cv.py" in manifest["sha256"]
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
path = Path(tmp)
|
||||||
|
old = pilot_fixture(path, jet_scenario="fixed")
|
||||||
|
with unittest.TestCase().assertRaisesRegex(ValueError, "CV observation mode"):
|
||||||
|
r.verify_pilot(path, old, "MRT", "window", "fixed", True)
|
||||||
|
for relative in ("plan.json", "uniform/summary.json", "refined/summary.json",
|
||||||
|
"uniform/ready.json", "refined/ready.json"):
|
||||||
|
file = path / relative
|
||||||
|
record = json.loads(file.read_text())
|
||||||
|
record.update(observe_cv=True, cv_samples=250)
|
||||||
|
if relative == "plan.json":
|
||||||
|
record["source_manifest"] = manifest
|
||||||
|
file.write_text(json.dumps(record))
|
||||||
|
assert r.verify_pilot(path, manifest, "MRT", "window", "fixed", True) == 3606
|
||||||
|
|
||||||
|
|
||||||
|
def test_compact_geometry_and_cv_stencils():
|
||||||
|
from CelerisLab.lbm.refinement import ACTIVE
|
||||||
|
from CelerisLab.lbm.descriptors import SOLID
|
||||||
|
|
||||||
|
_, pair, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT", "fixed", "compact")
|
||||||
|
assert pair.fine_shape == (168, 168) and tuple(pair.fine_origin) == (46, 46)
|
||||||
|
fluid = (pair.fine_kind == ACTIVE) & ((host.flags.reshape(pair.fine_shape) & SOLID) == 0)
|
||||||
|
rho, ux, uy = np.ones(pair.fine_shape), np.full(pair.fine_shape, .01), np.zeros(pair.fine_shape)
|
||||||
|
record = r.cv_snapshot(rho, ux, uy, fluid, pair.fine_origin, True, 1160000, 80, 40, [1, 2], "compact")
|
||||||
|
np.testing.assert_allclose(record["outward_flux"], 0, atol=1e-12)
|
||||||
|
stencil = np.zeros(pair.fine_shape, bool)
|
||||||
|
for rect in r.cv_contours(True, 40, "compact"):
|
||||||
|
x0, y0, x1, y1 = np.rint((rect - np.tile(pair.fine_origin, 2))/.5).astype(int)
|
||||||
|
stencil[y0-2:y1+2, x0-2:x0+2] = stencil[y0-2:y1+2, x1-2:x1+2] = True
|
||||||
|
stencil[y0-2:y0+2, x0-2:x1+2] = stencil[y1-2:y1+2, x0-2:x1+2] = True
|
||||||
|
assert fluid[stencil].all() and not stencil.ravel()[host.transfer_fine_cells].any()
|
||||||
|
|
||||||
|
|
||||||
|
def test_d160_config_and_single_case_receipts():
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
cfg, _, p = r.config(h, 160, "MRT", "fixed")
|
||||||
|
assert (p["nx"], p["ny"], p["center"], p["radius"]) == (3521, 658, [320, 320.5], 80)
|
||||||
|
assert p["viscosity"] == .016 and p["mean_speed"] == .01 and not cfg["method"]["ddf_shifting"]
|
||||||
|
assert r.uniform_origin(160) == (.375, .75)
|
||||||
|
np.testing.assert_array_equal(r.cv_contours(False, 160), r.CV_CONTOURS + [.125, 0, .125, 0])
|
||||||
|
for scenario, expected_exit in (("fixed", 0), ("zero", 2)):
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
path = Path(tmp)
|
||||||
|
manifest = pilot_fixture(path, jet_scenario=scenario, case_selection="uniform", uniform_diameter=160)
|
||||||
|
assert r.verify_pilot(path, manifest, "MRT", "window", scenario, False, "uniform", 160) == 1803
|
||||||
|
for selection, diameter, placement in (("both", 160, "wide"), ("uniform", 80, "wide"), ("uniform", 160, "compact")):
|
||||||
|
with unittest.TestCase().assertRaises(ValueError):
|
||||||
|
r.verify_pilot(path, manifest, "MRT", "window", scenario, False, selection, diameter, placement)
|
||||||
|
# Synthetic dispatch/result check: no CUDA imports or execution.
|
||||||
|
output = path / "full"
|
||||||
|
argv = ["runner", "--collision", "MRT", "--exchange-window", "--jet-scenario", scenario,
|
||||||
|
"--case", "uniform", "--uniform-diameter", "160", "--mode", "full",
|
||||||
|
"--pilot-results", str(path), "--output", str(output), "--max-wall-seconds", "10", "--gpu-gates-approved"]
|
||||||
|
result = dict(case="uniform", healthy=True, periodic=scenario == "zero", jet_diagnostic_windows=[],
|
||||||
|
status="driven_diagnostics_complete" if scenario == "fixed" else "periodic",
|
||||||
|
reference_comparison=dict(reference_agreement=False))
|
||||||
|
with patch.object(sys, "argv", argv), patch.dict(r.os.environ, CUDA_VISIBLE_DEVICES="mock-no-gpu"), \
|
||||||
|
patch.object(r, "run_case", return_value=result) as run:
|
||||||
|
assert r.main() == expected_exit and run.call_count == 1
|
||||||
|
comparison = json.loads((output / "comparison.json").read_text())
|
||||||
|
assert comparison["matched_case_pass"] is False and "refined_vs_uniform" not in comparison
|
||||||
|
assert comparison["status"].startswith("single_case_")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
suite = unittest.TestSuite(
|
suite = unittest.TestSuite(
|
||||||
unittest.FunctionTestCase(fn)
|
unittest.FunctionTestCase(fn)
|
||||||
|
|||||||
@@ -22,23 +22,29 @@ class RefinementMRTHost(unittest.TestCase):
|
|||||||
|
|
||||||
def test_srt_statements_and_guards_preserved(self):
|
def test_srt_statements_and_guards_preserved(self):
|
||||||
"""MRT is an additive compile-time branch; SRT statements and other guards remain."""
|
"""MRT is an additive compile-time branch; SRT statements and other guards remain."""
|
||||||
blocks = (KERNELS / "blocks.cu").read_text()
|
blocks = (KERNELS / "blocked.cu").read_text() + (KERNELS / "step/block_step.cu").read_text()
|
||||||
channel = (KERNELS / "step/block_channel.cu").read_text()
|
channel = (KERNELS / "step/block_channel.cu").read_text()
|
||||||
refinement = (KERNELS / "refinement.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);",
|
for text in ("compute_feq_from_momentum(drho, jx, jy, ux, uy, feq);",
|
||||||
"compute_feq(rho, ux, uy, feq);",
|
"compute_feq(rho, ux, uy, feq);",
|
||||||
"collide_srt_no_force(f, feq, d_params.omega);"):
|
"collide_srt_no_force(f, feq, d_params.omega);"):
|
||||||
self.assertIn(text, blocks)
|
self.assertIn(text, block_collision)
|
||||||
self.assertIn("collide_srt(f, feq, forcing, omega);", channel)
|
self.assertIn("collide_srt(f, feq, forcing, omega);", channel)
|
||||||
self.assertIn("collide_srt_no_force(f, feq, d_params.omega);", refinement)
|
for source in (blocks,):
|
||||||
for source in (blocks, refinement):
|
|
||||||
self.assertIn("(COLLISION_MODEL != 0 && COLLISION_MODEL != 2)", source)
|
self.assertIn("(COLLISION_MODEL != 0 && COLLISION_MODEL != 2)", source)
|
||||||
self.assertIn("USE_LES", source)
|
self.assertIn("USE_LES", source)
|
||||||
self.assertIn("STORE_PRECISION != 0", source)
|
self.assertIn("STORE_PRECISION != 0", source)
|
||||||
self.assertIn('#if COLLISION_MODEL == 2\n#include "operators/collision_mrt.cuh"\n#endif', source)
|
self.assertIn('#if COLLISION_MODEL == 2\n#include "operators/collision_mrt.cuh"\n#endif', source)
|
||||||
self.assertIn("USE_DDF_SHIFTING ||", refinement) # dense/mixed shifted stays rejected
|
# One shared unforced MRT body; each blocked/patch path calls it exactly once.
|
||||||
for source in (blocks, channel, refinement):
|
mrt = (KERNELS / "operators/collision_mrt.cuh").read_text()
|
||||||
self.assertEqual(source.count("collide_mrt_no_force("), 1)
|
self.assertEqual(mrt.count("void collide_mrt_unforced("), 1)
|
||||||
|
for source in (blocks + "\n" + block_collision, channel):
|
||||||
|
self.assertEqual(source.count("collide_mrt_unforced(f);"), 1)
|
||||||
|
self.assertNotIn("collide_mrt_no_force(", source)
|
||||||
|
self.assertNotIn("compute_rho_u(f, rho, ux, uy);\n collide_mrt", source)
|
||||||
|
self.assertNotRegex(source, r"compute_rho_u\(f, rho, ux, uy\);\s*collide_mrt")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": unittest.main()
|
if __name__ == "__main__": unittest.main()
|
||||||
|
|||||||
@@ -22,12 +22,13 @@ def host_module():
|
|||||||
tree.body=[node for node in tree.body if not (
|
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)
|
isinstance(node,ast.Import) and any(a.name=='pycuda.driver' for a in node.names)
|
||||||
or isinstance(node,ast.ImportFrom) and node.module in (
|
or isinstance(node,ast.ImportFrom) and node.module in (
|
||||||
'cuda','block_stepper','curved_links'))]
|
'cuda','curved_links'))]
|
||||||
module=types.ModuleType('CelerisLab.lbm._test_host_refinement')
|
module=types.ModuleType('CelerisLab.lbm._test_host_refinement')
|
||||||
module.__package__='CelerisLab.lbm'
|
module.__package__='CelerisLab.lbm'
|
||||||
from CelerisLab.body.jets import surface_link_velocity
|
from CelerisLab.body.jets import surface_link_velocity
|
||||||
module.__dict__.update(cuda=Mock(),compiler=Mock(),MAX_GRID_DIM=65535,
|
module.__dict__.update(cuda=Mock(),compiler=Mock(),
|
||||||
surface_link_velocity=surface_link_velocity)
|
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__)
|
exec(compile(tree,str(path),'exec'),module.__dict__)
|
||||||
return module
|
return module
|
||||||
|
|
||||||
@@ -173,8 +174,10 @@ def bodies_and_jets(speed=.002):
|
|||||||
return bodies,jets
|
return bodies,jets
|
||||||
|
|
||||||
|
|
||||||
KW=dict(base_block=(4,4),fine_block=(4,4),child_block=(4,4),
|
def levels(parents=PARENTS,children=CHILDREN,blocks=((4,4),)*3,objects=((),(),()),jets=((),(),())):
|
||||||
child_rectangles_in_parent=CHILDREN)
|
"""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))
|
||||||
|
|
||||||
|
|
||||||
def mock_backend(*, children=CHILDREN, bodies=False, fail_compile=False, fail_upload=False):
|
def mock_backend(*, children=CHILDREN, bodies=False, fail_compile=False, fail_upload=False):
|
||||||
@@ -191,9 +194,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_function.side_effect=lambda name: Mock(name=name)
|
||||||
gpu.get_global.side_effect=lambda name: (Mock(),20 if name=='d_params' else 16)
|
gpu.get_global.side_effect=lambda name: (Mock(),20 if name=='d_params' else 16)
|
||||||
return gpu
|
return gpu
|
||||||
module.compiler.load_module.side_effect=load
|
module.compiler.build_module.side_effect=lambda *a,**k: load(None)
|
||||||
if fail_compile:
|
if fail_compile:
|
||||||
module.compiler.compile_kernel.side_effect=['first','second',RuntimeError('L2 compile failed')]
|
module.compiler.build_module.side_effect=[load(None),load(None),RuntimeError('L2 compile failed')]
|
||||||
if fail_upload:
|
if fail_upload:
|
||||||
def upload(*args):
|
def upload(*args):
|
||||||
if len(allocations)>20:
|
if len(allocations)>20:
|
||||||
@@ -201,9 +204,8 @@ def mock_backend(*, children=CHILDREN, bodies=False, fail_compile=False, fail_up
|
|||||||
module.cuda.memcpy_htod.side_effect=upload
|
module.cuda.memcpy_htod.side_effect=upload
|
||||||
objects,jets=bodies_and_jets() if bodies else ((),())
|
objects,jets=bodies_and_jets() if bodies else ((),())
|
||||||
try:
|
try:
|
||||||
run=module.FixedPatchStepper(CFG,PARENTS,context=context,
|
run=module.FixedPatchStepper(CFG,context=context,
|
||||||
child_objects=objects,child_jets=jets,
|
**levels(children=children,objects=((),(),objects),jets=((),(),jets)))
|
||||||
**dict(KW,child_rectangles_in_parent=children))
|
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
for allocation in allocations:
|
for allocation in allocations:
|
||||||
allocation.free.assert_called_once_with()
|
allocation.free.assert_called_once_with()
|
||||||
@@ -214,28 +216,29 @@ def mock_backend(*, children=CHILDREN, bodies=False, fail_compile=False, fail_up
|
|||||||
class ConstructorAndSchedule(unittest.TestCase):
|
class ConstructorAndSchedule(unittest.TestCase):
|
||||||
def test_invalid_inputs_fail_before_context_or_allocations(self):
|
def test_invalid_inputs_fail_before_context_or_allocations(self):
|
||||||
context=Mock()
|
context=Mock()
|
||||||
cases=[dict(child=(8,8,16,16)),dict(child_rectangles_in_parent=[]),
|
b=(4,4)
|
||||||
dict(child_rectangles_in_parent=[(16,16,32)]),
|
cases=[dict(children=(8,8,16,16)),dict(children=[]),
|
||||||
dict(child_rectangles_in_parent=[(16.5,16,32,40)]),
|
dict(children=[(16,16,32)]),
|
||||||
dict(child_rectangles_in_parent=[(16,16,32,24),(16,27,32,55)]),
|
dict(children=[(16.5,16,32,40)]),
|
||||||
dict(child_rectangles_in_parent=[(40,16,48,40)]),
|
dict(children=[(16,16,32,24),(16,27,32,55)]),
|
||||||
dict(child_rectangles_in_parent=[(8,16,32,40)]),
|
dict(children=[(40,16,48,40)]),
|
||||||
dict(child_block=None),dict(fine_block=None),dict(base_block=None),
|
dict(children=[(8,16,32,40)]),
|
||||||
dict(child_block=(5,4)),dict(fine_block=(5,4)),
|
dict(blocks=(b,b,None)),dict(blocks=(b,None,b)),dict(blocks=(None,b,b)),
|
||||||
dict(child_block=(4,)),dict(child_rectangles_in_parent=(16,16,32,40))]
|
dict(blocks=(b,b,(5,4))),dict(blocks=(b,(5,4),b)),
|
||||||
|
dict(blocks=(b,b,(4,))),dict(children=(16,16,32,40))]
|
||||||
for changes in cases:
|
for changes in cases:
|
||||||
with self.subTest(changes=changes),self.assertRaises((ValueError,TypeError)):
|
with self.subTest(changes=changes),self.assertRaises((ValueError,TypeError)):
|
||||||
host.FixedPatchStepper(CFG,PARENTS,context=context,**dict(KW,**changes))
|
host.FixedPatchStepper(CFG,context=context,**levels(**changes))
|
||||||
with self.assertRaisesRegex(ValueError,'margin'):
|
with self.assertRaisesRegex(ValueError,'margin'):
|
||||||
host.FixedPatchStepper(CFG,[(1,4,20,28),(23,4,44,28)],context=context,**KW)
|
host.FixedPatchStepper(CFG,context=context,**levels(parents=[(1,4,20,28),(23,4,44,28)]))
|
||||||
objects,jets=bodies_and_jets()
|
objects,jets=bodies_and_jets()
|
||||||
with self.assertRaisesRegex(ValueError,'only on L2'):
|
with self.assertRaisesRegex(ValueError,'only on the finest level'):
|
||||||
host.FixedPatchStepper(CFG,PARENTS,context=context,fine_objects=objects,**KW)
|
host.FixedPatchStepper(CFG,context=context,**levels(objects=((),objects,())))
|
||||||
from CelerisLab.body.jets import Jet,Tube,Parallel
|
from CelerisLab.body.jets import Jet,Tube,Parallel
|
||||||
spanning=Jet(Tube((10.5,14.),0.,25.,.3),Parallel(0.),amplitude=0.)
|
spanning=Jet(Tube((10.5,14.),0.,25.,.3),Parallel(0.),amplitude=0.)
|
||||||
with self.assertRaisesRegex(ValueError,'one jet'):
|
with self.assertRaisesRegex(ValueError,'one jet'):
|
||||||
host.FixedPatchStepper(CFG,PARENTS,context=context,child_objects=objects,
|
host.FixedPatchStepper(CFG,context=context,
|
||||||
child_jets=[spanning],**KW)
|
**levels(objects=((),(),objects),jets=((),(),[spanning])))
|
||||||
context.activate.assert_not_called()
|
context.activate.assert_not_called()
|
||||||
host.cuda.mem_alloc.assert_not_called()
|
host.cuda.mem_alloc.assert_not_called()
|
||||||
|
|
||||||
@@ -313,21 +316,23 @@ class ConstructorAndSchedule(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InitialLevels(unittest.TestCase):
|
class RemovedLegacyAPI(unittest.TestCase):
|
||||||
"""initialize() entry adapter: positional per-level arrays or the legacy names, never both."""
|
"""The pre-level-list constructor/initialize forms are gone and fail loudly."""
|
||||||
|
|
||||||
def test_forms(self):
|
def test_removed_constructor_forms_raise_type_error(self):
|
||||||
a, b, c = object(), object(), object()
|
import inspect
|
||||||
f = host._initial_levels
|
context = Mock()
|
||||||
self.assertEqual(f((a, b), None, None, None), (a, b))
|
with self.assertRaises(TypeError): # positional bounds
|
||||||
self.assertEqual(f((a, b, c), None, None, None), (a, b, c))
|
host.FixedPatchStepper(CFG, PARENTS, context=context)
|
||||||
self.assertEqual(f((a, b), None, None, c), (a, b, c)) # old initialize(c, f, child_g=g)
|
removed = dict(bounds=PARENTS, fine_block=(4, 4), base_block=(4, 4), fine_objects=(),
|
||||||
self.assertEqual(f((), a, b, None), (a, b))
|
jets=(), child=(8, 8, 16, 16), child_block=(4, 4), child_objects=(),
|
||||||
self.assertEqual(f((), a, b, c), (a, b, c))
|
child_jets=(), child_rectangles_in_parent=CHILDREN)
|
||||||
for args in (((a,), None, None, c), ((a, b, c), None, None, c), ((a,), a, b, None),
|
for name, value in removed.items():
|
||||||
((), a, None, None), ((), None, b, c)):
|
with self.subTest(keyword=name), self.assertRaises(TypeError):
|
||||||
with self.subTest(args=args), self.assertRaises(TypeError):
|
host.FixedPatchStepper(CFG, context=context, **levels(), **{name: value})
|
||||||
f(*args)
|
context.activate.assert_not_called()
|
||||||
|
self.assertEqual(list(inspect.signature(host.FixedPatchStepper.initialize).parameters),
|
||||||
|
["self", "level_g"]) # no coarse_g/fine_g/child_g
|
||||||
|
|
||||||
|
|
||||||
if __name__=='__main__':
|
if __name__=='__main__':
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class RuntimeChildRemap(unittest.TestCase):
|
|||||||
before = run.download()
|
before = run.download()
|
||||||
old_allocations = list(allocations)
|
old_allocations = list(allocations)
|
||||||
if failure == 'construction':
|
if failure == 'construction':
|
||||||
operation = patch.object(module.compiler, 'compile_kernel', side_effect=RuntimeError('candidate compile'))
|
operation = patch.object(module.compiler, 'build_module', side_effect=RuntimeError('candidate compile'))
|
||||||
else:
|
else:
|
||||||
initialize = type(run).initialize
|
initialize = type(run).initialize
|
||||||
def fail(candidate, *arrays):
|
def fail(candidate, *arrays):
|
||||||
|
|||||||
@@ -54,16 +54,15 @@ class ShiftedJets(unittest.TestCase):
|
|||||||
(Jet(Tube((10.,14.),0.,26.,.3),Parallel(0.)),))]
|
(Jet(Tube((10.,14.),0.,26.,.3),Parallel(0.)),))]
|
||||||
for objects,jets in cases:
|
for objects,jets in cases:
|
||||||
with self.subTest(objects=len(objects),jets=len(jets)),self.assertRaises(ValueError):
|
with self.subTest(objects=len(objects),jets=len(jets)),self.assertRaises(ValueError):
|
||||||
module.FixedPatchStepper(CFG,backend.PARENTS,context=None,
|
module.FixedPatchStepper(CFG,context=None,refinements=[backend.PARENTS,A],
|
||||||
base_block=(4,4),fine_block=(4,4),child_block=(4,4),
|
level_blocks=[(4,4)]*3,level_objects=[(),(),objects],level_jets=[(),(),jets])
|
||||||
child_rectangles_in_parent=A,child_objects=objects,child_jets=jets)
|
|
||||||
for blocks in ((None,None,None),((4,4),None,(4,4))):
|
for blocks in ((None,None,None),((4,4),None,(4,4))):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
module.FixedPatchStepper(CFG,backend.PARENTS,context=None,
|
module.FixedPatchStepper(CFG,context=None,refinements=[backend.PARENTS,A],
|
||||||
base_block=blocks[0],fine_block=blocks[1],child_block=blocks[2],
|
level_blocks=list(blocks),level_objects=[(),(),(body(),)],
|
||||||
child_rectangles_in_parent=A,child_objects=(body(),),child_jets=(jet(),))
|
level_jets=[(),(),(jet(),)])
|
||||||
module.cuda.mem_alloc.assert_not_called()
|
module.cuda.mem_alloc.assert_not_called()
|
||||||
module.compiler.compile_kernel.assert_not_called()
|
module.compiler.build_module.assert_not_called()
|
||||||
|
|
||||||
def test_switch_upload_only_then_raw_remap_keeps_live_velocity(self):
|
def test_switch_upload_only_then_raw_remap_keeps_live_velocity(self):
|
||||||
run,module,allocations,storage,objects=ready();clone=None
|
run,module,allocations,storage,objects=ready();clone=None
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
"""CPU host contracts; mocked CUDA executes real lifecycle, not numerical kernels."""
|
"""CPU host contracts; mocked CUDA executes real lifecycle, not numerical kernels."""
|
||||||
import ast
|
|
||||||
import types
|
|
||||||
from pathlib import Path
|
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
import unittest
|
import unittest
|
||||||
@@ -78,13 +75,15 @@ class ShiftedStorage(unittest.TestCase):
|
|||||||
|
|
||||||
def test_shifted_density_and_tag_validation(self):
|
def test_shifted_density_and_tag_validation(self):
|
||||||
old,new = topology(A),topology(B)
|
old,new = topology(A),topology(B)
|
||||||
a = tuple(np.where(m[None], np.float32(-.001), np.nan).repeat(9,axis=0) for m in old.leaf_masks)
|
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
|
||||||
remap(old,new,a)
|
remap(old,new,a)
|
||||||
with self.assertRaisesRegex(ValueError,'representation'): remap(old,new,a,False)
|
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))
|
with self.assertRaisesRegex(ValueError,'FP32'): remap(old,new,tuple(x.astype(np.float64) for x in a))
|
||||||
a[0][:,old.leaf_masks[0]] = -1
|
a[0][:,old.leaf_masks[0]] = -1
|
||||||
with self.assertRaisesRegex(ValueError,'positive density'): remap(old,new,a)
|
with self.assertRaisesRegex(ValueError,'positive density'): remap(old,new,a)
|
||||||
rest = tuple(np.where(m[None],np.float32(0),np.nan).repeat(9,axis=0) for m in old.leaf_masks)
|
rest = tuple(np.where(m[None],np.float32(0),np.float32(np.nan)).repeat(9,axis=0)
|
||||||
|
for m in old.leaf_masks)
|
||||||
remap(old,new,rest)
|
remap(old,new,rest)
|
||||||
with self.assertRaisesRegex(ValueError,'positive density'):
|
with self.assertRaisesRegex(ValueError,'positive density'):
|
||||||
remap_synchronized(old,new,rest,base_count=7,level_steps=(7,14,28),phase='postcollision_g')
|
remap_synchronized(old,new,rest,base_count=7,level_steps=(7,14,28),phase='postcollision_g')
|
||||||
@@ -126,33 +125,24 @@ class ShiftedStorage(unittest.TestCase):
|
|||||||
with self.assertRaisesRegex(ValueError,'mixed'): run._download_raw()
|
with self.assertRaisesRegex(ValueError,'mixed'): run._download_raw()
|
||||||
finally: run.close()
|
finally: run.close()
|
||||||
|
|
||||||
def test_dense_or_mixed_shifted_rejected_before_compilation(self):
|
def test_missing_tiles_rejected_before_compilation(self):
|
||||||
module=backend.host_module()
|
module=backend.host_module()
|
||||||
cfg=replace(backend.CFG,ddf_shifting=True)
|
cfg=replace(backend.CFG,ddf_shifting=True)
|
||||||
for kwargs in ({},dict(base_block=(4,4)),dict(fine_block=(4,4))):
|
for blocks in ([None,None],[(4,4),None],[None,(4,4)]):
|
||||||
with self.assertRaisesRegex(ValueError,'all-blocked|base_block requires fine_block'):
|
with self.assertRaisesRegex(ValueError,'must be a tile'):
|
||||||
module.FixedPatchStepper(cfg,(4,4,20,28),context=None,**kwargs)
|
module.FixedPatchStepper(cfg,context=None,refinements=[(4,4,20,28)],
|
||||||
module.compiler.compile_kernel.assert_not_called()
|
level_blocks=blocks)
|
||||||
|
module.compiler.build_module.assert_not_called()
|
||||||
|
|
||||||
def test_uniform_block_codec_and_raw_snapshot(self):
|
def test_uniform_block_codec_and_raw_snapshot(self):
|
||||||
path = Path(backend.r.__file__).with_name('block_stepper.py')
|
module = backend.host_module() # single blocked level: FixedPatchStepper(refinements=[])
|
||||||
tree = ast.parse(path.read_text())
|
cuda, compiler = module.cuda, module.compiler
|
||||||
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()
|
cuda.mem_alloc.side_effect=lambda size: Mock()
|
||||||
gpu=Mock();gpu.get_global.return_value=(Mock(),20)
|
gpu=Mock();gpu.get_global.return_value=(Mock(),20)
|
||||||
compiler.load_module.return_value=gpu
|
compiler.build_module.return_value=gpu
|
||||||
context=Mock(activate=nullcontext,sm_arch='sm_70')
|
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)
|
cfg=replace(backend.CFG,nx=16,ny=16,ddf_shifting=True)
|
||||||
run=module.PeriodicBlockStepper(cfg,layout,context=context)
|
run=module.FixedPatchStepper(cfg,context=context,refinements=[],level_blocks=[(4,4)])
|
||||||
storage={}
|
storage={}
|
||||||
cuda.memcpy_htod.side_effect=lambda p,a: storage.__setitem__(id(p),np.array(a,copy=True))
|
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)])
|
cuda.memcpy_dtoh.side_effect=lambda a,p: np.copyto(a,storage[id(p)])
|
||||||
@@ -163,7 +153,8 @@ class ShiftedStorage(unittest.TestCase):
|
|||||||
self.assertTrue(raw.shifted)
|
self.assertTrue(raw.shifted)
|
||||||
self.assertEqual(len(raw.values),1)
|
self.assertEqual(len(raw.values),1)
|
||||||
self.assertTrue((raw.values[0][1]==np.float32(2**-35)).all())
|
self.assertTrue((raw.values[0][1]==np.float32(2**-35)).all())
|
||||||
np.testing.assert_array_equal(run.download(),physical)
|
(down,)=run.download()
|
||||||
|
np.testing.assert_array_equal(down,physical)
|
||||||
finally: run.close()
|
finally: run.close()
|
||||||
|
|
||||||
def test_passive_shifted_body_raw_remap_preserves_support(self):
|
def test_passive_shifted_body_raw_remap_preserves_support(self):
|
||||||
|
|||||||
@@ -36,12 +36,10 @@ def make_simulation(monkeypatch):
|
|||||||
sim = module.Simulation.__new__(module.Simulation)
|
sim = module.Simulation.__new__(module.Simulation)
|
||||||
sim.ctx = Owner()
|
sim.ctx = Owner()
|
||||||
sim._closed = False
|
sim._closed = False
|
||||||
sim._build_dir = "private-build"
|
|
||||||
for name in ("stepper", "bodies", "field", "_module", "_stream"):
|
for name in ("stepper", "bodies", "field", "_module", "_stream"):
|
||||||
setattr(sim, name, object())
|
setattr(sim, name, object())
|
||||||
monkeypatch.setattr(module, "cuda", SimpleNamespace(
|
monkeypatch.setattr(module, "cuda", SimpleNamespace(
|
||||||
Context=SimpleNamespace(synchronize=lambda: None)))
|
Context=SimpleNamespace(synchronize=lambda: None)))
|
||||||
monkeypatch.setattr(module.compiler, "cleanup_build_dir", lambda path: None)
|
|
||||||
return sim
|
return sim
|
||||||
|
|
||||||
|
|
||||||
@@ -57,7 +55,7 @@ def test_close_inside_activation_preserves_resources(monkeypatch):
|
|||||||
assert sim._closed and sim.ctx.is_closed
|
assert sim._closed and sim.ctx.is_closed
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("failure", ["synchronize", "detach", "directory"])
|
@pytest.mark.parametrize("failure", ["synchronize", "detach"])
|
||||||
def test_close_failure_is_retryable(monkeypatch, failure):
|
def test_close_failure_is_retryable(monkeypatch, failure):
|
||||||
sim = make_simulation(monkeypatch)
|
sim = make_simulation(monkeypatch)
|
||||||
if failure == "detach":
|
if failure == "detach":
|
||||||
@@ -68,17 +66,12 @@ def test_close_failure_is_retryable(monkeypatch, failure):
|
|||||||
calls.append(args)
|
calls.append(args)
|
||||||
if len(calls) == 1:
|
if len(calls) == 1:
|
||||||
raise RuntimeError(failure + " failed")
|
raise RuntimeError(failure + " failed")
|
||||||
if failure == "synchronize":
|
monkeypatch.setattr(module.cuda.Context, "synchronize", fail_once)
|
||||||
monkeypatch.setattr(module.cuda.Context, "synchronize", fail_once)
|
|
||||||
else:
|
|
||||||
monkeypatch.setattr(module.compiler, "cleanup_build_dir", fail_once)
|
|
||||||
with pytest.raises(RuntimeError, match="failed"):
|
with pytest.raises(RuntimeError, match="failed"):
|
||||||
sim.close()
|
sim.close()
|
||||||
assert not sim._closed
|
assert not sim._closed
|
||||||
assert sim._build_dir == "private-build"
|
|
||||||
sim.close()
|
sim.close()
|
||||||
assert sim._closed and sim.ctx.is_closed
|
assert sim._closed and sim.ctx.is_closed
|
||||||
assert sim._build_dir is None
|
|
||||||
sim.close()
|
sim.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
"""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._level_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()
|
||||||
@@ -238,6 +238,81 @@ def test_overlapping_force_regions_rejected(device):
|
|||||||
bodies.build_compact_lists(bodies.build_flags(field.flag))
|
bodies.build_compact_lists(bodies.build_flags(field.flag))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("second_x,error", [(14., "unique cut-link writers"),
|
||||||
|
(17., "active fluid receivers")])
|
||||||
|
def test_passive_overlap_rejected_before_publishing_lists(second_x, error):
|
||||||
|
cfg = LBMConfig(nx=32, ny=24)
|
||||||
|
cfg.validate()
|
||||||
|
bodies = ObjectManager(cfg.nx, cfg.ny, cfg.nz, cfg.nq, cfg)
|
||||||
|
for x in (14., second_x):
|
||||||
|
bodies.add(obj(x=x, y=12., r=4.3))
|
||||||
|
bodies.add(obj(x=4., y=4., r=2., is_sensor=True))
|
||||||
|
counts = np.full(bodies.count, 91, np.int32)
|
||||||
|
hits = []
|
||||||
|
flags = bodies.build_flags(np.full(cfg.nx * cfg.ny, FLUID, np.uint16))
|
||||||
|
for domain_flags in (flags, None):
|
||||||
|
with pytest.raises(ValueError, match=error):
|
||||||
|
bodies.build_compact_lists_for(bodies.objects, domain_flags,
|
||||||
|
sensor_cell_counts_out=counts, hits_out=hits)
|
||||||
|
np.testing.assert_array_equal(counts, 91)
|
||||||
|
assert hits == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("centers", [((10., 12.), (22., 12.)),
|
||||||
|
((10.5, 12.), (19.5, 12.))])
|
||||||
|
def test_separated_passive_bodies_keep_valid_writers(centers):
|
||||||
|
# The second case has a subcell physical gap with a resolved fluid column.
|
||||||
|
cfg = LBMConfig(nx=32, ny=24)
|
||||||
|
cfg.validate()
|
||||||
|
bodies = ObjectManager(cfg.nx, cfg.ny, cfg.nz, cfg.nq, cfg)
|
||||||
|
for x, y in centers:
|
||||||
|
bodies.add(obj(x=x, y=y, r=4.3))
|
||||||
|
flags = bodies.build_flags(np.full(cfg.nx * cfg.ny, FLUID, np.uint16))
|
||||||
|
indices, directions, _, ids, *_ = bodies.build_compact_lists(flags)
|
||||||
|
assert set(ids) == {0, 1}
|
||||||
|
assert np.all(flags[indices] & FLUID) and not np.any(flags[indices] & SOLID)
|
||||||
|
assert len(np.unique(indices.astype(np.uint64) * cfg.nq + directions)) == len(indices)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("entry", ["sync", "initialize", "runtime_edit"])
|
||||||
|
@pytest.mark.parametrize("second_x", [5., 6.])
|
||||||
|
def test_passive_overlap_failure_keeps_live_state(device, entry, second_x):
|
||||||
|
from CelerisLab.simulation import Simulation
|
||||||
|
field = make_field(device)
|
||||||
|
bodies = manager(field, [obj(), obj(x=9., y=3., r=1., is_sensor=True)])
|
||||||
|
bodies.sync_to_gpu(field)
|
||||||
|
overlapping = obj(x=second_x)
|
||||||
|
if entry == "runtime_edit":
|
||||||
|
bodies.stage_add(overlapping)
|
||||||
|
else:
|
||||||
|
bodies.add(overlapping)
|
||||||
|
objects = bodies.objects
|
||||||
|
flags, curved, counts = field.flag.copy(), field.curved.fluid_idx.copy(), bodies.sensor_cell_counts.copy()
|
||||||
|
action = bodies.action.copy()
|
||||||
|
buffers = [(block, bytes(block.data), block.freed) for block in device.blocks]
|
||||||
|
writes = device.writes
|
||||||
|
sim = SimpleNamespace(bodies=bodies, field=field, lbm_cfg=field.cfg,
|
||||||
|
_initialized=entry == "runtime_edit",
|
||||||
|
_assert_allocation_contract=lambda: None,
|
||||||
|
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||||
|
with pytest.raises(ValueError, match="active fluid receivers|unique cut-link writers"):
|
||||||
|
if entry == "sync":
|
||||||
|
bodies.sync_to_gpu(field)
|
||||||
|
elif entry == "initialize":
|
||||||
|
Simulation.initialize(sim)
|
||||||
|
else:
|
||||||
|
Simulation.sync_bodies(sim)
|
||||||
|
assert bodies.objects == objects
|
||||||
|
assert bodies.has_pending_edit() == (entry == "runtime_edit")
|
||||||
|
assert sim._initialized == (entry == "runtime_edit")
|
||||||
|
assert device.writes == writes and len(device.blocks) == len(buffers)
|
||||||
|
assert all(bytes(block.data) == data and block.freed == freed for block, data, freed in buffers)
|
||||||
|
np.testing.assert_array_equal(field.flag, flags)
|
||||||
|
np.testing.assert_array_equal(field.curved.fluid_idx, curved)
|
||||||
|
np.testing.assert_array_equal(bodies.sensor_cell_counts, counts)
|
||||||
|
np.testing.assert_array_equal(bodies.action, action)
|
||||||
|
|
||||||
|
|
||||||
def test_action_upload_and_scratch_are_pinned(device):
|
def test_action_upload_and_scratch_are_pinned(device):
|
||||||
field = make_field(device, streaming="esopull")
|
field = make_field(device, streaming="esopull")
|
||||||
bodies = manager(field, [obj(is_force_region=True)])
|
bodies = manager(field, [obj(is_force_region=True)])
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
"""Host contract of FixedPatchStepper.step(n): one stream wait per call (mocked driver).
|
||||||
|
|
||||||
|
All n base intervals are enqueued in order on the backend stream and the host
|
||||||
|
waits once before step returns. Launch order and buffer arguments must equal n
|
||||||
|
step(1) calls; completion metadata, raw-exchange and window ledgers, and failure
|
||||||
|
invalidation are published only after that wait.
|
||||||
|
"""
|
||||||
|
from contextlib import nullcontext
|
||||||
|
from unittest.mock import Mock
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from CelerisLab.config import LBMConfig
|
||||||
|
from tests.unit.test_raw_exchange import body
|
||||||
|
from tests.unit.test_refinement_three_level_multipatch import host_module
|
||||||
|
|
||||||
|
BODY_LEVEL, HOPS = 3, 8 # body on L3: 2**3 hops per base interval
|
||||||
|
|
||||||
|
|
||||||
|
def backend(window=False):
|
||||||
|
"""Four-level host backend; every launch, memset and stream wait goes to one log."""
|
||||||
|
host = host_module()
|
||||||
|
log, allocations = [], []
|
||||||
|
|
||||||
|
def allocate(size):
|
||||||
|
allocations.append(Mock(size=size))
|
||||||
|
return allocations[-1]
|
||||||
|
|
||||||
|
module = Mock()
|
||||||
|
module.get_global.return_value = (Mock(), 20)
|
||||||
|
module.get_function.side_effect = lambda name: Mock(
|
||||||
|
side_effect=lambda *a, **k: log.append((name, a, k)))
|
||||||
|
host.compiler.build_module.return_value = module
|
||||||
|
host.cuda.mem_alloc.side_effect = allocate
|
||||||
|
host.cuda.memcpy_dtoh.side_effect = lambda array, pointer: array.fill(0)
|
||||||
|
host.cuda.memset_d8_async.side_effect = lambda *a: log.append(("memset", a, {}))
|
||||||
|
run = host.FixedPatchStepper(
|
||||||
|
LBMConfig(nx=48, ny=48, viscosity=.02), context=Mock(activate=nullcontext, sm_arch="sm_70"),
|
||||||
|
refinements=[[(6, 6, 26, 26)], [(20, 20, 44, 44)], [(56, 56, 72, 72)]],
|
||||||
|
level_blocks=[(4, 4)] * 4, level_objects=[(), (), (), (body(),)],
|
||||||
|
observe_raw_exchange=True, observe_window_exchange=window)
|
||||||
|
run._stream.synchronize.side_effect = lambda: log.append(("sync", (), {}))
|
||||||
|
run.initialize(*[np.full(level.shape, 1 / 9, np.float32) for level in run._levels])
|
||||||
|
log.clear()
|
||||||
|
return run, log, allocations
|
||||||
|
|
||||||
|
|
||||||
|
def signature(log, allocations):
|
||||||
|
"""Event names with device-pointer arguments replaced by allocation index."""
|
||||||
|
index = {id(a): i for i, a in enumerate(allocations)}
|
||||||
|
return [(name, tuple(index.get(id(a), "-") for a in args)) for name, args, _ in log]
|
||||||
|
|
||||||
|
|
||||||
|
def names(log):
|
||||||
|
return [name for name, _, _ in log]
|
||||||
|
|
||||||
|
|
||||||
|
class StepSync(unittest.TestCase):
|
||||||
|
def test_step1_waits_once_after_all_launches(self):
|
||||||
|
run, log, _ = backend()
|
||||||
|
run.step(1)
|
||||||
|
self.assertEqual(names(log).count("sync"), 1)
|
||||||
|
self.assertEqual(names(log)[-1], "sync")
|
||||||
|
self.assertEqual(names(log)[0], "memset")
|
||||||
|
self.assertEqual(names(log).count("BlockStep"), 1 + 2 + 4 + 8)
|
||||||
|
self.assertEqual(run.step_count, 1)
|
||||||
|
run.close()
|
||||||
|
|
||||||
|
def test_step_n_equals_n_step1_launches_with_one_wait(self):
|
||||||
|
single, single_log, single_alloc = backend()
|
||||||
|
for _ in range(3):
|
||||||
|
single.step(1)
|
||||||
|
batched, batched_log, batched_alloc = backend()
|
||||||
|
batched.step(3)
|
||||||
|
expected = [e for e in signature(single_log, single_alloc) if e[0] != "sync"]
|
||||||
|
got = signature(batched_log, batched_alloc)
|
||||||
|
self.assertEqual(got[-1][0], "sync")
|
||||||
|
self.assertEqual(names(batched_log).count("sync"), 1)
|
||||||
|
self.assertEqual(got[:-1], expected) # same order, same ping-pong buffers
|
||||||
|
self.assertEqual(names(batched_log).count("memset"), 3)
|
||||||
|
self.assertEqual(batched.step_count, single.step_count)
|
||||||
|
index = [{id(a): i for i, a in enumerate(alloc)} for alloc in (single_alloc, batched_alloc)]
|
||||||
|
self.assertEqual(index[1][id(batched._levels[0].current)],
|
||||||
|
index[0][id(single._levels[0].current)])
|
||||||
|
single.close()
|
||||||
|
batched.close()
|
||||||
|
|
||||||
|
def test_step0_launches_and_waits_for_nothing(self):
|
||||||
|
run, log, _ = backend()
|
||||||
|
run.step(2)
|
||||||
|
log.clear()
|
||||||
|
run.step(0)
|
||||||
|
self.assertEqual(log, [])
|
||||||
|
self.assertEqual(run.step_count, 2)
|
||||||
|
value = run.read_raw_exchange(0, level=BODY_LEVEL) # step(0) keeps the ledger valid
|
||||||
|
self.assertEqual((value["time_start"], value["time_end"]), (1, 2))
|
||||||
|
run.close()
|
||||||
|
|
||||||
|
def test_completion_metadata_covers_all_intervals(self):
|
||||||
|
run, _, _ = backend()
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "completed"):
|
||||||
|
run.read_raw_exchange(0, level=BODY_LEVEL)
|
||||||
|
run.step(3)
|
||||||
|
value = run.read_raw_exchange(0, level=BODY_LEVEL)
|
||||||
|
self.assertEqual((value["time_start"], value["time_end"], value["sample_count"]),
|
||||||
|
(2, 3, HOPS)) # step(n) retains only the final base interval
|
||||||
|
run.step(2)
|
||||||
|
self.assertEqual(run.read_raw_exchange(0, level=BODY_LEVEL)["time_end"], 5)
|
||||||
|
run.close()
|
||||||
|
|
||||||
|
def test_window_accounting_across_batched_calls(self):
|
||||||
|
windows = []
|
||||||
|
for calls in ((1, 3), (4,), (1, 1, 1, 1)):
|
||||||
|
run, log, _ = backend(window=True)
|
||||||
|
run.step(1)
|
||||||
|
run.start_exchange_window()
|
||||||
|
log.clear()
|
||||||
|
for n in calls:
|
||||||
|
run.step(n)
|
||||||
|
self.assertEqual(names(log).count("BlockAccumulateExchange"), 2 * 4 * HOPS)
|
||||||
|
self.assertEqual(names(log).count("sync"), len(calls))
|
||||||
|
out = run.read_exchange_window(0, level=BODY_LEVEL)
|
||||||
|
windows.append((out["time_start"], out["time_end"], out["sample_count"]))
|
||||||
|
run.close()
|
||||||
|
self.assertEqual(windows, [(1., 5, 4 * HOPS)] * 3)
|
||||||
|
|
||||||
|
def test_failed_wait_publishes_nothing(self):
|
||||||
|
run, _, _ = backend(window=True)
|
||||||
|
run.step(2)
|
||||||
|
run.start_exchange_window()
|
||||||
|
run._stream.synchronize.side_effect = RuntimeError("device failure")
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "device failure"):
|
||||||
|
run.step(3)
|
||||||
|
self.assertEqual(run.step_count, 2) # value at entry to the failing call
|
||||||
|
self.assertFalse(run._exchange_valid)
|
||||||
|
self.assertFalse(run._window_valid)
|
||||||
|
for read in (lambda: run.read_raw_exchange(0, level=BODY_LEVEL),
|
||||||
|
lambda: run.read_exchange_window(0, level=BODY_LEVEL), run.download,
|
||||||
|
lambda: run.step(1)):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||||
|
read()
|
||||||
|
run._stream.synchronize.side_effect = None
|
||||||
|
run.close()
|
||||||
|
|
||||||
|
def test_launch_error_mid_batch_skips_wait_and_fails(self):
|
||||||
|
run, log, _ = backend()
|
||||||
|
run.step(1)
|
||||||
|
log.clear()
|
||||||
|
hop, calls = run._hop, []
|
||||||
|
|
||||||
|
def failing(index, *args, **kw):
|
||||||
|
if index == 0:
|
||||||
|
calls.append(1)
|
||||||
|
if len(calls) == 2:
|
||||||
|
raise RuntimeError("launch failure")
|
||||||
|
return hop(index, *args, **kw)
|
||||||
|
run._hop = failing
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "launch failure"):
|
||||||
|
run.step(3)
|
||||||
|
self.assertNotIn("sync", names(log))
|
||||||
|
self.assertEqual(run.step_count, 1)
|
||||||
|
self.assertFalse(run._exchange_valid)
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||||
|
run.read_raw_exchange(0, level=BODY_LEVEL)
|
||||||
|
run.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -12,13 +12,15 @@ from source_provenance import checkout_provenance
|
|||||||
import re100_helpers
|
import re100_helpers
|
||||||
import run_jet_circle_cv
|
import run_jet_circle_cv
|
||||||
|
|
||||||
|
sys.path.insert(0, str(ROOT / "tests/unit"))
|
||||||
|
from historical_tree import historical_tree, tracked
|
||||||
|
|
||||||
ORIGIN = "47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5"
|
ORIGIN = "47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5"
|
||||||
|
|
||||||
|
|
||||||
def test_manifests_cover_current_source_and_preserve_origin():
|
def test_manifests_cover_current_source_and_preserve_origin(tmp_path):
|
||||||
names = subprocess.check_output(
|
assert re100_helpers.PIN == run_jet_circle_cv.PIN == ORIGIN
|
||||||
["git", "-C", str(ROOT), "ls-files", "src"], text=True
|
manifests = {}
|
||||||
).splitlines()
|
|
||||||
for group, hashes_key, revision_key in (
|
for group, hashes_key, revision_key in (
|
||||||
("dfg_re100", "production_sha256", "commit"),
|
("dfg_re100", "production_sha256", "commit"),
|
||||||
("jet_circle_cv", "files", "revision"),
|
("jet_circle_cv", "files", "revision"),
|
||||||
@@ -29,18 +31,22 @@ def test_manifests_cover_current_source_and_preserve_origin():
|
|||||||
).read_text()
|
).read_text()
|
||||||
)
|
)
|
||||||
assert manifest[revision_key] == ORIGIN
|
assert manifest[revision_key] == ORIGIN
|
||||||
assert set(manifest[hashes_key]) == set(names)
|
|
||||||
assert manifest["source_origin_commits"][-1] == ORIGIN
|
assert manifest["source_origin_commits"][-1] == ORIGIN
|
||||||
assert (
|
assert (
|
||||||
manifest["historical_checkpoints"]["production"]
|
manifest["historical_checkpoints"]["production"]
|
||||||
== "d79e3f6f0a424f5bcc3a1a6d610313e49daf97a2"
|
== "d79e3f6f0a424f5bcc3a1a6d610313e49daf97a2"
|
||||||
)
|
)
|
||||||
assert "not inherited from d79" in manifest["experiment"]
|
assert "not inherited from d79" in manifest["experiment"]
|
||||||
for name, expected in manifest[hashes_key].items():
|
manifests[group] = manifest[hashes_key]
|
||||||
|
# History-dependent checks last, so a missing commit masks nothing above.
|
||||||
|
names = tracked(ORIGIN)
|
||||||
|
origin = historical_tree(ORIGIN, tmp_path)
|
||||||
|
for group, hashes in manifests.items():
|
||||||
|
assert set(hashes) == set(names), group
|
||||||
|
for name, expected in hashes.items():
|
||||||
assert (
|
assert (
|
||||||
hashlib.sha256((ROOT / name).read_bytes()).hexdigest() == expected
|
hashlib.sha256((origin / name).read_bytes()).hexdigest() == expected
|
||||||
), name
|
), name
|
||||||
assert re100_helpers.PIN == run_jet_circle_cv.PIN == ORIGIN
|
|
||||||
|
|
||||||
|
|
||||||
def test_runtime_checkout_identity_is_separate_from_source_origin(tmp_path):
|
def test_runtime_checkout_identity_is_separate_from_source_origin(tmp_path):
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ original_module = gate.compile_module
|
|||||||
cache = {}
|
cache = {}
|
||||||
def retain_compile(*args, **kwargs):
|
def retain_compile(*args, **kwargs):
|
||||||
result = original_compile(*args, **kwargs)
|
result = original_compile(*args, **kwargs)
|
||||||
source = kwargs.get('source', 'kernel_v2.cu')
|
source = kwargs.get('source', 'uniform.cu')
|
||||||
shutil.copyfile(result, a.output / (Path(source).stem + '.ptx'))
|
shutil.copyfile(result, a.output / (Path(source).stem + '.ptx'))
|
||||||
metadata = Path(result + '.json')
|
metadata = Path(result + '.json')
|
||||||
if metadata.exists(): shutil.copyfile(metadata, a.output / (Path(source).stem + '.ptx.json'))
|
if metadata.exists(): shutil.copyfile(metadata, a.output / (Path(source).stem + '.ptx.json'))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Periodic body-free block backend: CPU contract doubles plus one short GPU case.
|
"""Single blocked level, FixedPatchStepper(refinements=[]): CPU contracts plus short GPU cases.
|
||||||
|
|
||||||
Frozen GPU gates (32x24, 8 steps, positive low-Mach non-equilibrium state):
|
Frozen GPU gates (32x24, 8 steps, positive low-Mach non-equilibrium state):
|
||||||
finite cores; bit-identical 1x1, 2x2 and 4x2 splits; max |G - FP64 oracle|
|
finite cores; bit-identical 1x1, 2x2 and 4x2 splits; max |G - FP64 oracle|
|
||||||
@@ -11,9 +11,8 @@ Mass/momentum drift is reported, not gated.
|
|||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import resource
|
import resource
|
||||||
from threading import RLock
|
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock, Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pycuda.driver as cuda
|
import pycuda.driver as cuda
|
||||||
@@ -21,10 +20,10 @@ import pycuda.driver as cuda
|
|||||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||||
from CelerisLab.body.objects import SimObject
|
from CelerisLab.body.objects import SimObject
|
||||||
from CelerisLab.config import LBMConfig
|
from CelerisLab.config import LBMConfig
|
||||||
from CelerisLab.cuda import compiler_v2 as compiler
|
from CelerisLab.cuda import compiler
|
||||||
from CelerisLab.lbm import block_stepper as implementation
|
from CelerisLab.lbm import refinement_stepper as implementation
|
||||||
from CelerisLab.lbm.blocks import NO_DONOR, BlockLayout2D
|
from CelerisLab.lbm.blocks import NO_DONOR, BlockLayout2D, static_circle_tables
|
||||||
from CelerisLab.lbm.block_stepper import PeriodicBlockStepper
|
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||||
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[3]
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
@@ -112,72 +111,78 @@ def totals(g):
|
|||||||
return g.sum(), (CX[:, None, None] * g).sum(), (CY[:, None, None] * g).sum()
|
return g.sum(), (CX[:, None, None] * g).sum(), (CY[:, None, None] * g).sum()
|
||||||
|
|
||||||
|
|
||||||
|
def single(cfg=None, *, context, block=(12, 16), objects=(), **kw):
|
||||||
|
"""One periodic blocked level (the former PeriodicBlockStepper capability)."""
|
||||||
|
return FixedPatchStepper(cfg or config(), context=context, refinements=[], level_blocks=[block],
|
||||||
|
level_objects=[tuple(objects)], **kw)
|
||||||
|
|
||||||
|
|
||||||
class CpuContracts(unittest.TestCase):
|
class CpuContracts(unittest.TestCase):
|
||||||
"""No device: driver doubles prove rejection before allocation and lifecycle."""
|
"""No device: driver doubles prove rejection before allocation."""
|
||||||
|
|
||||||
def forbidden_context(self):
|
def forbidden_context(self):
|
||||||
return Mock(activate=Mock(side_effect=AssertionError("CUDA reached")), sm_arch="sm_70")
|
return Mock(activate=Mock(side_effect=AssertionError("CUDA reached")), sm_arch="sm_70")
|
||||||
|
|
||||||
def test_unsupported_inputs_rejected_before_cuda(self):
|
def test_unsupported_inputs_rejected_before_cuda(self):
|
||||||
layout = BlockLayout2D.build(NY, NX, 12, 16)
|
for cfg, kw in ((None, dict(block=None)),
|
||||||
cases = ((config(), BlockLayout2D.build(NY, NX, 12, 16, periodic=(True, False))),
|
(None, dict(coarse_boundary="channel")),
|
||||||
(config(), BlockLayout2D.build(NY, NX * 2, 12, 16)),
|
(None, dict(coarse_boundary="slot_reservoir")),
|
||||||
(config(collision="MRT"), layout),
|
(config(collision="MRT"), {}),
|
||||||
(config(), "layout"))
|
(config(les_enabled=True), {}),
|
||||||
for cfg, lay in cases:
|
(None, dict(explosion="projected_linear")),
|
||||||
with self.subTest(cfg=cfg.collision, layout=getattr(lay, "periodic", lay)):
|
(None, dict(observe_raw_exchange=True)),
|
||||||
with self.assertRaises(ValueError):
|
(None, dict(observe_raw_exchange=True, observe_window_exchange=True)),
|
||||||
PeriodicBlockStepper(cfg, lay, context=self.forbidden_context())
|
(None, dict(level_jets=[(object(),)]))):
|
||||||
|
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
||||||
|
single(cfg, context=self.forbidden_context(), **kw)
|
||||||
moving = circle()
|
moving = circle()
|
||||||
moving.state.vx = .01
|
moving.state.vx = .01
|
||||||
build_links = CircleGeometry.build_cut_links
|
build_links = CircleGeometry.build_cut_links
|
||||||
for objects in ([moving], [circle((1.5, 11.5), 2.0)], [circle(), circle()]):
|
for objects in ([moving], [circle((1.5, 11.5), 2.0)], [circle(), circle()]):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
PeriodicBlockStepper(config(), layout, context=self.forbidden_context(), objects=objects)
|
single(context=self.forbidden_context(), objects=objects)
|
||||||
for center, radius in (((16.0, 12.0), -.1), ((16.0, 12.0), -.5), ((15.5, 11.5), np.nan),
|
for center, radius in (((16.0, 12.0), -.1), ((16.0, 12.0), -.5), ((15.5, 11.5), np.nan),
|
||||||
((np.inf, 11.5), 4.3), ((15.5, np.nan), 4.3)):
|
((np.inf, 11.5), 4.3), ((15.5, np.nan), 4.3)):
|
||||||
with self.subTest(center=center, radius=radius), self.assertRaises(ValueError):
|
with self.subTest(center=center, radius=radius), self.assertRaises(ValueError):
|
||||||
PeriodicBlockStepper(config(), layout, context=self.forbidden_context(),
|
single(context=self.forbidden_context(), objects=[circle(center, radius)])
|
||||||
objects=[circle(center, radius)])
|
with patch.object(implementation, "static_circle_tables") as prepare:
|
||||||
with patch.object(implementation, "_static_circles") as prepare:
|
|
||||||
with self.assertRaisesRegex(AssertionError, "CUDA reached"): # body-free path
|
with self.assertRaisesRegex(AssertionError, "CUDA reached"): # body-free path
|
||||||
PeriodicBlockStepper(config(), layout, context=self.forbidden_context(), objects=iter(()))
|
single(context=self.forbidden_context(), objects=iter(()))
|
||||||
prepare.assert_not_called()
|
prepare.assert_not_called()
|
||||||
|
with self.assertRaisesRegex(AssertionError, "CUDA reached"):
|
||||||
|
single(context=self.forbidden_context(), objects=[circle()])
|
||||||
with patch.object(CircleGeometry, "build_cut_links",
|
with patch.object(CircleGeometry, "build_cut_links",
|
||||||
lambda geometry, *a, **k: build_links(geometry, *a, **k)[1:]):
|
lambda geometry, *a, **k: build_links(geometry, *a, **k)[1:]):
|
||||||
with self.assertRaisesRegex(ValueError, "cover every fluid pull"):
|
with self.assertRaisesRegex(ValueError, "cover every fluid pull"):
|
||||||
PeriodicBlockStepper(config(), layout, context=self.forbidden_context(),
|
single(context=self.forbidden_context(), objects=[circle()])
|
||||||
objects=[circle()])
|
|
||||||
with patch.object(implementation, "MAX_GRID_DIM", 3):
|
with patch.object(implementation, "MAX_GRID_DIM", 3):
|
||||||
with self.assertRaisesRegex(ValueError, "launch limits"):
|
with self.assertRaisesRegex(ValueError, "launch limits"):
|
||||||
PeriodicBlockStepper(config(), layout, context=self.forbidden_context())
|
single(context=self.forbidden_context())
|
||||||
|
|
||||||
def test_memory_and_param_layout_rejected_before_allocation(self):
|
def test_memory_and_param_layout_rejected_before_allocation(self):
|
||||||
layout = BlockLayout2D.build(NY, NX, 12, 16)
|
|
||||||
context = Mock(activate=nullcontext, sm_arch="sm_70")
|
context = Mock(activate=nullcontext, sm_arch="sm_70")
|
||||||
with patch.object(cuda, "mem_get_info", return_value=(1024, 1 << 30)), \
|
with patch.object(cuda, "mem_get_info", return_value=(1024, 1 << 30)), \
|
||||||
patch.object(cuda, "Stream") as stream, patch.object(cuda, "mem_alloc") as alloc:
|
patch.object(cuda, "Stream") as stream, patch.object(cuda, "mem_alloc") as alloc:
|
||||||
with self.assertRaises(MemoryError):
|
with self.assertRaisesRegex(MemoryError, "exceed free device memory"):
|
||||||
PeriodicBlockStepper(config(), layout, context=context)
|
single(context=context, objects=[circle()])
|
||||||
stream.assert_not_called()
|
stream.assert_not_called()
|
||||||
alloc.assert_not_called()
|
alloc.assert_not_called()
|
||||||
module = Mock()
|
module = Mock()
|
||||||
module.get_global.return_value = (Mock(), 16)
|
module.get_global.return_value = (Mock(), 16)
|
||||||
with patch.object(cuda, "mem_get_info", return_value=(1 << 30, 1 << 30)), \
|
with patch.object(cuda, "Stream"), patch.object(cuda, "mem_get_info", return_value=(1 << 40, 1 << 40)), patch.object(compiler, "generate_config"), \
|
||||||
patch.object(cuda, "Stream"), patch.object(compiler, "generate_config"), \
|
|
||||||
patch.object(compiler, "compile_kernel") as compile_kernel, \
|
patch.object(compiler, "compile_kernel") as compile_kernel, \
|
||||||
patch.object(compiler, "cleanup_build_dir"), \
|
patch.object(compiler, "cleanup_build_dir"), \
|
||||||
patch.object(compiler, "load_module", return_value=module), \
|
patch.object(compiler, "load_module", return_value=module), \
|
||||||
patch.object(cuda, "mem_alloc") as alloc, patch.object(cuda, "memcpy_htod") as upload:
|
patch.object(cuda, "mem_alloc") as alloc, patch.object(cuda, "memcpy_htod") as upload:
|
||||||
with self.assertRaisesRegex(RuntimeError, "layout mismatch"):
|
with self.assertRaisesRegex(RuntimeError, "layout mismatch"):
|
||||||
PeriodicBlockStepper(config(), layout, context=context)
|
single(context=context)
|
||||||
self.assertEqual(compile_kernel.call_args.kwargs["source"], "blocks.cu")
|
self.assertEqual(compile_kernel.call_args.kwargs["source"], "blocked.cu")
|
||||||
alloc.assert_not_called()
|
alloc.assert_not_called()
|
||||||
upload.assert_not_called()
|
upload.assert_not_called()
|
||||||
|
|
||||||
def test_static_circle_tables(self):
|
def test_static_circle_tables(self):
|
||||||
layout = BlockLayout2D.build(NY, NX, 12, 16)
|
layout = BlockLayout2D.build(NY, NX, 12, 16)
|
||||||
tables = implementation._static_circles(layout, (circle(),))
|
tables = static_circle_tables(layout, (circle(),))
|
||||||
links = CircleGeometry(*CENTER, RADIUS).build_cut_links(NX, NY, domain_flags=dense_flags())
|
links = CircleGeometry(*CENTER, RADIUS).build_cut_links(NX, NY, domain_flags=dense_flags())
|
||||||
self.assertEqual(len(tables["cell"]), len(links))
|
self.assertEqual(len(tables["cell"]), len(links))
|
||||||
np.testing.assert_array_equal(tables["q"], np.array([l.q for l in links], np.float32))
|
np.testing.assert_array_equal(tables["q"], np.array([l.q for l in links], np.float32))
|
||||||
@@ -185,84 +190,11 @@ class CpuContracts(unittest.TestCase):
|
|||||||
self.assertTrue((tables["flags"][solid_cells] & 2).all())
|
self.assertTrue((tables["flags"][solid_cells] & 2).all())
|
||||||
self.assertEqual(int(np.count_nonzero(tables["flags"] & 2)), int(tables["solid"].sum()))
|
self.assertEqual(int(np.count_nonzero(tables["flags"] & 2)), int(tables["solid"].sum()))
|
||||||
|
|
||||||
def fake_backend(self):
|
|
||||||
backend = PeriodicBlockStepper.__new__(PeriodicBlockStepper)
|
|
||||||
backend.layout = BlockLayout2D.build(NY, NX, 12, 16)
|
|
||||||
backend.ctx = Mock(activate=nullcontext)
|
|
||||||
backend._lock = RLock()
|
|
||||||
backend._status = "ready"
|
|
||||||
backend._step_count = 0
|
|
||||||
backend._stream = Mock()
|
|
||||||
backend._module = Mock()
|
|
||||||
backend._copy = backend._step = Mock()
|
|
||||||
backend.boundary, backend._curved, backend._tables, backend._links = None, None, {}, 0
|
|
||||||
backend._dst, backend._src = Mock(), Mock()
|
|
||||||
backend._block, backend._grid, backend._copy_grid = (32, 1, 1), (1, 1, 1), (1, 1)
|
|
||||||
backend._allocations = [Mock(), Mock()]
|
|
||||||
backend._current, backend._spare = backend._allocations
|
|
||||||
return backend
|
|
||||||
|
|
||||||
def test_initialize_rejects_bad_state_before_upload(self):
|
class SingleLevelGPU(unittest.TestCase):
|
||||||
backend = self.fake_backend()
|
|
||||||
good = initial_state()
|
|
||||||
bad = (good[:, :-1], np.where(np.arange(NX) == 3, np.nan, good), -good)
|
|
||||||
with patch.object(cuda, "memcpy_htod") as upload:
|
|
||||||
for g in bad:
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
backend.initialize(g)
|
|
||||||
upload.assert_not_called()
|
|
||||||
backend.initialize(good)
|
|
||||||
self.assertEqual(upload.call_count, 2)
|
|
||||||
self.assertTrue(np.isnan(upload.call_args_list[1].args[1]).all())
|
|
||||||
backend.close()
|
|
||||||
|
|
||||||
def test_failed_launch_and_download_make_backend_unusable(self):
|
|
||||||
for failure in ("step", "download"):
|
|
||||||
backend = self.fake_backend()
|
|
||||||
if failure == "step":
|
|
||||||
backend._step = Mock(side_effect=RuntimeError("launch failed"))
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "launch failed"):
|
|
||||||
backend.step()
|
|
||||||
else:
|
|
||||||
with patch.object(cuda, "memcpy_dtoh", side_effect=RuntimeError("read failed")):
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "read failed"):
|
|
||||||
backend.download()
|
|
||||||
for operation in (backend.download, backend.step):
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
|
||||||
operation()
|
|
||||||
backend.close()
|
|
||||||
for allocation in backend_allocations(backend):
|
|
||||||
allocation.free.assert_called_once()
|
|
||||||
|
|
||||||
def test_activation_failure_retains_resources_for_retry(self):
|
|
||||||
backend = self.fake_backend()
|
|
||||||
allocations, stream, module = backend._allocations[:], backend._stream, backend._module
|
|
||||||
unavailable = MagicMock()
|
|
||||||
unavailable.__enter__.side_effect = RuntimeError("activation failed")
|
|
||||||
backend.ctx.activate = Mock(side_effect=[unavailable, nullcontext()])
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "activation failed"):
|
|
||||||
backend.close()
|
|
||||||
self.assertEqual(backend._status, "failed")
|
|
||||||
self.assertEqual(backend._allocations, allocations)
|
|
||||||
self.assertIs(backend._stream, stream)
|
|
||||||
self.assertIs(backend._module, module)
|
|
||||||
backend.close()
|
|
||||||
self.assertEqual(backend._status, "closed")
|
|
||||||
self.assertIsNone(backend._module)
|
|
||||||
self.assertIsNone(backend._copy)
|
|
||||||
self.assertIsNone(backend._step)
|
|
||||||
for allocation in allocations:
|
|
||||||
allocation.free.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
def backend_allocations(backend):
|
|
||||||
return (backend._current, backend._spare)
|
|
||||||
|
|
||||||
|
|
||||||
class PeriodicBlockGPU(unittest.TestCase):
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.assertEqual(Path(implementation.__file__).resolve(),
|
self.assertEqual(Path(implementation.__file__).resolve(),
|
||||||
ROOT / "src/CelerisLab/lbm/block_stepper.py")
|
ROOT / "src/CelerisLab/lbm/refinement_stepper.py")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
||||||
@@ -273,13 +205,13 @@ class PeriodicBlockGPU(unittest.TestCase):
|
|||||||
g0 = initial_state()
|
g0 = initial_state()
|
||||||
results = []
|
results = []
|
||||||
with CudaContext(0) as context:
|
with CudaContext(0) as context:
|
||||||
for by, bx in SPLITS:
|
for split in SPLITS:
|
||||||
layout = BlockLayout2D.build(NY, NX, by, bx)
|
with single(context=context, block=split) as backend:
|
||||||
with PeriodicBlockStepper(config(), layout, context=context) as backend:
|
|
||||||
backend.initialize(g0)
|
backend.initialize(g0)
|
||||||
backend.step(STEPS)
|
backend.step(STEPS)
|
||||||
self.assertEqual(backend.step_count, STEPS)
|
self.assertEqual(backend.step_count, STEPS)
|
||||||
results.append(backend.download())
|
(g,) = backend.download()
|
||||||
|
results.append(g)
|
||||||
for (by, bx), g in zip(SPLITS, results):
|
for (by, bx), g in zip(SPLITS, results):
|
||||||
self.assertTrue(np.isfinite(g).all(), (by, bx))
|
self.assertTrue(np.isfinite(g).all(), (by, bx))
|
||||||
np.testing.assert_array_equal(g, results[0], err_msg=f"split {(by, bx)}")
|
np.testing.assert_array_equal(g, results[0], err_msg=f"split {(by, bx)}")
|
||||||
@@ -306,16 +238,17 @@ class PeriodicBlockGPU(unittest.TestCase):
|
|||||||
g0 = initial_state()
|
g0 = initial_state()
|
||||||
results = []
|
results = []
|
||||||
with CudaContext(0) as context:
|
with CudaContext(0) as context:
|
||||||
for by, bx in SPLITS:
|
for split in SPLITS:
|
||||||
layout = BlockLayout2D.build(NY, NX, by, bx)
|
with single(context=context, block=split, objects=[circle()]) as backend:
|
||||||
with PeriodicBlockStepper(config(), layout, context=context, objects=[circle()]) as backend:
|
tables = backend._level_boundaries[0].tables
|
||||||
for key, values, dtype in (("q", [l.q for l in links], np.float32),
|
for key, values, dtype in (("q", [l.q for l in links], np.float32),
|
||||||
("dir", [l.dir for l in links], np.uint8),
|
("dir", [l.dir for l in links], np.uint8),
|
||||||
("fallback", [l.fallback for l in links], np.uint8)):
|
("fallback", [l.fallback for l in links], np.uint8)):
|
||||||
np.testing.assert_array_equal(backend.boundary[key], np.array(values, dtype))
|
np.testing.assert_array_equal(tables[key], np.array(values, dtype))
|
||||||
backend.initialize(g0)
|
backend.initialize(g0)
|
||||||
backend.step(STEPS)
|
backend.step(STEPS)
|
||||||
results.append(backend.download())
|
(g,) = backend.download()
|
||||||
|
results.append(g)
|
||||||
for (by, bx), g in zip(SPLITS, results):
|
for (by, bx), g in zip(SPLITS, results):
|
||||||
self.assertTrue(np.isfinite(g[:, ~solid]).all(), (by, bx))
|
self.assertTrue(np.isfinite(g[:, ~solid]).all(), (by, bx))
|
||||||
self.assertTrue(np.isnan(g[:, solid]).all(), (by, bx))
|
self.assertTrue(np.isnan(g[:, solid]).all(), (by, bx))
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import numpy as np
|
|||||||
import pycuda.driver as cuda
|
import pycuda.driver as cuda
|
||||||
|
|
||||||
from CelerisLab.config import LBMConfig
|
from CelerisLab.config import LBMConfig
|
||||||
from CelerisLab.cuda import compiler_v2 as compiler
|
from CelerisLab.cuda import compiler
|
||||||
from CelerisLab.cuda.context import CudaContext
|
from CelerisLab.cuda.context import CudaContext
|
||||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||||
from CelerisLab.lbm.descriptors import FLUID, SOLID, BC_WALL, BC_INLET, BC_OUTLET
|
from CelerisLab.lbm.descriptors import FLUID, SOLID, BC_WALL, BC_INLET, BC_OUTLET
|
||||||
@@ -27,7 +27,7 @@ def state(ny, nx):
|
|||||||
|
|
||||||
def compile_module(cfg, context, source):
|
def compile_module(cfg, context, source):
|
||||||
cfg.validate()
|
cfg.validate()
|
||||||
build = compiler.generate_config(cfg)
|
build = compiler.generate_config(cfg, block_root_bc=2 if source == "blocked.cu" else 0)
|
||||||
try:
|
try:
|
||||||
module = compiler.load_module(compiler.compile_kernel(context.sm_arch, source=source, build_dir=build))
|
module = compiler.load_module(compiler.compile_kernel(context.sm_arch, source=source, build_dir=build))
|
||||||
finally:
|
finally:
|
||||||
@@ -46,7 +46,7 @@ def run_root(cfg, context, initial, block=None, steps=8):
|
|||||||
try:
|
try:
|
||||||
layout = None if block is None else BlockLayout2D.build(cfg.ny, cfg.nx, *block)
|
layout = None if block is None else BlockLayout2D.build(cfg.ny, cfg.nx, *block)
|
||||||
local = cfg if layout is None else replace(cfg, nx=layout.local_shape[1], ny=layout.local_shape[0])
|
local = cfg if layout is None else replace(cfg, nx=layout.local_shape[1], ny=layout.local_shape[0])
|
||||||
module = compile_module(local, context, "kernel_v2.cu" if layout is None else "blocks.cu")
|
module = compile_module(local, context, "uniform.cu" if layout is None else "blocked.cu")
|
||||||
data = initial if layout is None else layout.to_blocked(initial)
|
data = initial if layout is None else layout.to_blocked(initial)
|
||||||
current, spare = upload(data), upload(np.full_like(data, np.nan))
|
current, spare = upload(data), upload(np.full_like(data, np.nan))
|
||||||
tpb = cfg.threads_per_block
|
tpb = cfg.threads_per_block
|
||||||
@@ -106,9 +106,10 @@ class ChannelRootGPU(unittest.TestCase):
|
|||||||
cfg = LBMConfig(nx=48, ny=32, viscosity=.1, velocity=.015, ddf_shifting=shifted)
|
cfg = LBMConfig(nx=48, ny=32, viscosity=.1, velocity=.015, ddf_shifting=shifted)
|
||||||
reference = None
|
reference = None
|
||||||
for block in ((32,48), (8,8), (4,1)):
|
for block in ((32,48), (8,8), (4,1)):
|
||||||
with FixedPatchStepper(cfg, (8,6,40,26), context=context,
|
with FixedPatchStepper(cfg, context=context, coarse_boundary="channel",
|
||||||
coarse_boundary="channel", base_block=block, fine_block=(8,8),
|
refinements=[(8,6,40,26)], level_blocks=[block, (8,8)],
|
||||||
fine_objects=(body,) if bodies else (), jets=(jet,) if bodies else ()) as backend:
|
level_objects=[(), (body,) if bodies else ()],
|
||||||
|
level_jets=[(), (jet,) if bodies else ()]) as backend:
|
||||||
arrays = [state(*level.shape[1:]) for level in backend._levels]
|
arrays = [state(*level.shape[1:]) for level in backend._levels]
|
||||||
backend.initialize(*arrays)
|
backend.initialize(*arrays)
|
||||||
backend.step(4)
|
backend.step(4)
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ EPS = np.finfo(np.float64).eps
|
|||||||
|
|
||||||
|
|
||||||
def build(m, window=True, raw=True):
|
def build(m, window=True, raw=True):
|
||||||
return FixedPatchStepper(config("periodic"), BOUNDS, context=m["context"], base_block=(3, 4),
|
return FixedPatchStepper(config("periodic"), context=m["context"], refinements=[BOUNDS],
|
||||||
fine_block=(16, 20), fine_objects=[circle()],
|
level_blocks=[(3, 4), (16, 20)], level_objects=[(), (circle(),)],
|
||||||
observe_raw_exchange=raw, observe_window_exchange=window)
|
observe_raw_exchange=raw, observe_window_exchange=window)
|
||||||
|
|
||||||
|
|
||||||
@@ -109,10 +109,11 @@ class ExchangeWindowGPU(unittest.TestCase):
|
|||||||
from test_raw_exchange_gpu import BOUNDS as RB, CFG, circle as rc, jet, state
|
from test_raw_exchange_gpu import BOUNDS as RB, CFG, circle as rc, jet, state
|
||||||
from CelerisLab.cuda.context import CudaContext
|
from CelerisLab.cuda.context import CudaContext
|
||||||
with CudaContext(0) as ctx:
|
with CudaContext(0) as ctx:
|
||||||
kw = dict(context=ctx, fine_block=(16, 20), fine_objects=[rc()], jets=[jet()],
|
kw = dict(context=ctx, refinements=[RB], level_blocks=[(12, 8), (16, 20)],
|
||||||
|
level_objects=[(), (rc(),)], level_jets=[(), (jet(),)],
|
||||||
observe_raw_exchange=True)
|
observe_raw_exchange=True)
|
||||||
with FixedPatchStepper(CFG, RB, observe_window_exchange=True, **kw) as windowed, \
|
with FixedPatchStepper(CFG, observe_window_exchange=True, **kw) as windowed, \
|
||||||
FixedPatchStepper(CFG, RB, **kw) as plain:
|
FixedPatchStepper(CFG, **kw) as plain:
|
||||||
arrays = [state(level.shape) for level in windowed._levels]
|
arrays = [state(level.shape) for level in windowed._levels]
|
||||||
windowed.initialize(*arrays)
|
windowed.initialize(*arrays)
|
||||||
plain.initialize(*arrays)
|
plain.initialize(*arrays)
|
||||||
@@ -200,11 +201,11 @@ class ExchangeWindowShiftedMRTGPU(unittest.TestCase):
|
|||||||
from CelerisLab.cuda.context import CudaContext
|
from CelerisLab.cuda.context import CudaContext
|
||||||
cfg = LBMConfig(nx=32, ny=24, viscosity=.1, collision="MRT", ddf_shifting=True)
|
cfg = LBMConfig(nx=32, ny=24, viscosity=.1, collision="MRT", ddf_shifting=True)
|
||||||
with CudaContext(0) as ctx:
|
with CudaContext(0) as ctx:
|
||||||
kw = dict(context=ctx, base_block=(12, 8), fine_block=(16, 20), fine_objects=[rc()],
|
kw = dict(context=ctx, refinements=[RB], level_blocks=[(12, 8), (16, 20)],
|
||||||
observe_raw_exchange=True)
|
level_objects=[(), (rc(),)], observe_raw_exchange=True)
|
||||||
with FixedPatchStepper(cfg, RB, observe_window_exchange=True, **kw) as windowed, \
|
with FixedPatchStepper(cfg, observe_window_exchange=True, **kw) as windowed, \
|
||||||
FixedPatchStepper(cfg, RB, observe_window_exchange=True, **kw) as split, \
|
FixedPatchStepper(cfg, observe_window_exchange=True, **kw) as split, \
|
||||||
FixedPatchStepper(cfg, RB, **kw) as plain:
|
FixedPatchStepper(cfg, **kw) as plain:
|
||||||
arrays = [state(level.shape) for level in windowed._levels]
|
arrays = [state(level.shape) for level in windowed._levels]
|
||||||
for run in (windowed, split, plain):
|
for run in (windowed, split, plain):
|
||||||
run.initialize(*arrays)
|
run.initialize(*arrays)
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
"""Two coarse/four fine steps: isolated fine circle, no flux-accuracy claim.
|
"""Two coarse/four fine steps: isolated fine circle on blocked storage, no flux-accuracy claim.
|
||||||
|
|
||||||
Run only through the bounded GPU runner after a fresh resource check. The test
|
Run only through the bounded GPU runner after a fresh resource check. The fine
|
||||||
hooks read actual kernel inputs/outputs; they never change the stepping order.
|
step hook reads the actual output buffer; it never changes the stepping order.
|
||||||
|
|
||||||
|
Retired with the dense refinement route (accepted losses, see the D2 commit):
|
||||||
|
the dense-kernel per-hop flux ledger (fine/coarse transfer + boundary budget),
|
||||||
|
the CurvedLinkSoA reflection oracle and the CELERIS_REFINEMENT_AUDIT collision
|
||||||
|
counts. The blocked test keeps the four-hop comparison with the uniform solver,
|
||||||
|
the raw-exchange mass sign and passive == zero actuation; raw exchange covers
|
||||||
|
the boundary part only, not full population flux closure.
|
||||||
"""
|
"""
|
||||||
import importlib.util
|
import importlib.util
|
||||||
from contextlib import nullcontext
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import RLock
|
|
||||||
from types import SimpleNamespace
|
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -18,7 +22,6 @@ import CelerisLab
|
|||||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||||
from CelerisLab.body.objects import SimObject
|
from CelerisLab.body.objects import SimObject
|
||||||
from CelerisLab.config import LBMConfig
|
from CelerisLab.config import LBMConfig
|
||||||
from CelerisLab.cuda import compiler_v2 as compiler
|
|
||||||
from CelerisLab.cuda.context import CudaContext
|
from CelerisLab.cuda.context import CudaContext
|
||||||
from CelerisLab.lbm import refinement_stepper as implementation
|
from CelerisLab.lbm import refinement_stepper as implementation
|
||||||
from CelerisLab.lbm.descriptors import FLUID, SOLID
|
from CelerisLab.lbm.descriptors import FLUID, SOLID
|
||||||
@@ -27,10 +30,8 @@ from CelerisLab.lbm.descriptors import FLUID, SOLID
|
|||||||
ROOT = Path(__file__).resolve().parents[3]
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
CX = np.array([0, 1, -1, 0, 0, 1, -1, 1, -1])
|
CX = np.array([0, 1, -1, 0, 0, 1, -1, 1, -1])
|
||||||
CY = np.array([0, 0, 0, 1, -1, 1, -1, -1, 1])
|
CY = np.array([0, 0, 0, 1, -1, 1, -1, -1, 1])
|
||||||
OPP = np.array([0, 2, 1, 4, 3, 6, 5, 8, 7])
|
|
||||||
W = np.array([4/9]+[1/9]*4+[1/36]*4)
|
W = np.array([4/9]+[1/9]*4+[1/36]*4)
|
||||||
MOMENTS = np.stack((np.ones(9), CX, CY))
|
MOMENTS = np.stack((np.ones(9), CX, CY))
|
||||||
GEOMETRY = ("fluid_idx", "dir", "q", "body_id", "rx", "ry", "rz", "fallback_class")
|
|
||||||
BOUNDS = (4, 4, 20, 20)
|
BOUNDS = (4, 4, 20, 20)
|
||||||
|
|
||||||
|
|
||||||
@@ -53,196 +54,6 @@ def object_spec(speed):
|
|||||||
radius=2.15, surface_actuation=surface(speed))
|
radius=2.15, surface_actuation=surface(speed))
|
||||||
|
|
||||||
|
|
||||||
def audit_compiler(original):
|
|
||||||
def compile_private(*args, **kwargs):
|
|
||||||
entry = Path(kwargs["build_dir"])/"fine_surface_audit.cu"
|
|
||||||
entry.write_text('#define CELERIS_REFINEMENT_AUDIT\n#include "refinement.cu"\n')
|
|
||||||
kwargs["source"] = entry.name
|
|
||||||
return original(*args, **kwargs)
|
|
||||||
return compile_private
|
|
||||||
|
|
||||||
|
|
||||||
def reflection_oracle(before, cl, speed, *, verify_projection=True):
|
|
||||||
"""FP64 BFL from frozen spatial populations, independent arc projection."""
|
|
||||||
flat = before.astype(float).reshape(9, -1)
|
|
||||||
radius = np.hypot(cl.rx.astype(float), cl.ry.astype(float))
|
|
||||||
delta = (np.arctan2(cl.ry.astype(float), cl.rx.astype(float))-.7+np.pi) % (2*np.pi)-np.pi
|
|
||||||
amplitude = 0 if speed is None else speed
|
|
||||||
strength = amplitude*np.maximum(0, 1-(delta/.85)**2)**2
|
|
||||||
velocity = strength[:, None]*np.column_stack((cl.rx, cl.ry))/radius[:, None]
|
|
||||||
if verify_projection and cl.surface_velocity is not None:
|
|
||||||
np.testing.assert_allclose(cl.surface_velocity, velocity, atol=3e-9, rtol=0)
|
|
||||||
incoming = []
|
|
||||||
for i, (k, d, q, fallback) in enumerate(zip(cl.fluid_idx, cl.dir, cl.q, cl.fallback_class)):
|
|
||||||
k, d, q = int(k), int(d), float(q)
|
|
||||||
rho = flat[:, k].sum()
|
|
||||||
dot = CX[d]*velocity[i, 0]+CY[d]*velocity[i, 1]
|
|
||||||
if fallback:
|
|
||||||
value = flat[d, k]-6*W[d]*rho*dot
|
|
||||||
elif q < .5:
|
|
||||||
value = 2*q*flat[d, k]+(1-2*q)*flat[d, k-CX[d]-40*CY[d]]-6*W[d]*rho*dot
|
|
||||||
else:
|
|
||||||
value = flat[d, k]/(2*q)+(1-1/(2*q))*flat[OPP[d], k]-3*W[d]*rho*dot/q
|
|
||||||
incoming.append(value)
|
|
||||||
return np.array(incoming)
|
|
||||||
|
|
||||||
|
|
||||||
def fine_flux(before, staged, active, solid):
|
|
||||||
"""Population ledger across the fine leaf boundary: solid versus halo.
|
|
||||||
|
|
||||||
Internal fluid-to-fluid streaming cancels pairwise; solid and halo packets
|
|
||||||
are counted in their actual direction, before any collision.
|
|
||||||
"""
|
|
||||||
boundary, transfer = np.zeros(9), np.zeros(9)
|
|
||||||
magnitude = 0.0
|
|
||||||
for d, (cx, cy) in enumerate(zip(CX, CY)):
|
|
||||||
incoming = active & ~np.roll(active, (cy, cx), (0, 1))
|
|
||||||
outgoing = active & ~np.roll(active, (-cy, -cx), (0, 1))
|
|
||||||
from_solid = np.roll(solid, (cy, cx), (0, 1))
|
|
||||||
into_solid = np.roll(solid, (-cy, -cx), (0, 1))
|
|
||||||
received = np.roll(staged[d], (cy, cx), (0, 1)).astype(float)
|
|
||||||
sent = before[d].astype(float)
|
|
||||||
for vector, inc, out in ((boundary, incoming & from_solid, outgoing & into_solid),
|
|
||||||
(transfer, incoming & ~from_solid, outgoing & ~into_solid)):
|
|
||||||
assert np.isfinite(received[inc]).all() and np.isfinite(sent[out]).all()
|
|
||||||
vector[d] = received[inc].sum()-sent[out].sum()
|
|
||||||
magnitude += np.abs(received[inc]).sum()+np.abs(sent[out]).sum()
|
|
||||||
return boundary, transfer, magnitude
|
|
||||||
|
|
||||||
|
|
||||||
def coarse_flux(before, fine, active):
|
|
||||||
"""Derive every replacement from physical midpoints, not production tables."""
|
|
||||||
x0, y0, x1, y1 = BOUNDS
|
|
||||||
incoming = np.stack([np.roll(before[d], (CY[d], CX[d]), (0, 1)) for d in range(9)]).astype(float)
|
|
||||||
magnitude = 0.0
|
|
||||||
for y, x in np.argwhere(active):
|
|
||||||
for d in range(1, 9):
|
|
||||||
if x0 <= x+.5-CX[d]/2 <= x1 and y0 <= y+.5-CY[d]/2 <= y1:
|
|
||||||
a, b = 4+2*(x-x0), 4+2*(y-y0)
|
|
||||||
children = fine[d, b:b+2, a:a+2].astype(float)
|
|
||||||
assert children.shape == (2, 2) and np.isfinite(children).all()
|
|
||||||
incoming[d, y, x] = children.sum()/4
|
|
||||||
magnitude += np.abs(children).sum()/4
|
|
||||||
assert np.isfinite(incoming[:, active]).all()
|
|
||||||
# This retains the eight grazing-corner replacements of uncovered donors.
|
|
||||||
flux = (incoming[:, active]-before[:, active].astype(float)).sum(axis=1)
|
|
||||||
magnitude += np.abs(incoming[:, active]).sum()+np.abs(before[:, active]).sum(dtype=float)
|
|
||||||
return flux, magnitude
|
|
||||||
|
|
||||||
|
|
||||||
def run_patch(speed, *, audit=True):
|
|
||||||
cfg = LBMConfig(nx=24, ny=24, viscosity=.05, velocity=0, rho=1)
|
|
||||||
coarse_active = np.ones((24, 24), bool)
|
|
||||||
coarse_active[4:20, 4:20] = False
|
|
||||||
source = object_spec(speed)
|
|
||||||
state = vars(source.state).copy()
|
|
||||||
with CudaContext(0) as context:
|
|
||||||
build = (patch.object(compiler, "compile_kernel", audit_compiler(compiler.compile_kernel))
|
|
||||||
if audit else nullcontext())
|
|
||||||
with build:
|
|
||||||
backend = implementation.FixedPatchStepper(cfg, BOUNDS, context=context, fine_objects=[source])
|
|
||||||
with backend, context.activate():
|
|
||||||
c, f = backend._levels
|
|
||||||
cl = backend._fine_curved
|
|
||||||
geometry = {name: getattr(cl, name).copy() for name in GEOMETRY}
|
|
||||||
flags = backend.fine_boundary.flags.reshape(40, 40)
|
|
||||||
solid = (flags & SOLID) != 0
|
|
||||||
active = np.zeros((40, 40), bool)
|
|
||||||
active[4:-4, 4:-4] = True
|
|
||||||
active &= (flags & FLUID) != 0
|
|
||||||
active &= ~solid
|
|
||||||
local = backend.fine_boundary.objects[0]
|
|
||||||
assert local.center == (20., 20.) and local.radius == 4.3
|
|
||||||
assert source.center == (12.25, 12.25) and source.radius == 2.15
|
|
||||||
yy, xx = np.mgrid[:40, :40]
|
|
||||||
roi = active & ((xx-20)**2+(yy-20)**2 <= 9**2)
|
|
||||||
backend.initialize(rest((24, 24)), rest((40, 40)))
|
|
||||||
if audit:
|
|
||||||
for level in backend._levels:
|
|
||||||
for name in ("collision_counts", "explosion_counts", "incoming_counts"):
|
|
||||||
pointer, size = level.module.get_global(name)
|
|
||||||
cuda.memset_d8(pointer, 0, size)
|
|
||||||
initial = backend.download()
|
|
||||||
initial_pop = initial[0][:, coarse_active].sum(axis=1, dtype=float)
|
|
||||||
initial_pop += .25*initial[1][:, active].sum(axis=1, dtype=float)
|
|
||||||
boundary, fine_transfer, coarse_transfer = np.zeros(9), np.zeros(9), np.zeros(9)
|
|
||||||
budget_sum, reflection_error, drive_mass = 0., 0., 0.
|
|
||||||
traces = []
|
|
||||||
original_curved, original_fine, original_coarse = backend._curved, f.kernel, c.kernel
|
|
||||||
|
|
||||||
def curved(*args, **kwargs):
|
|
||||||
nonlocal boundary, fine_transfer, budget_sum, reflection_error, drive_mass
|
|
||||||
backend._stream.synchronize()
|
|
||||||
before = read(args[0], f.shape)
|
|
||||||
expected = reflection_oracle(before, cl, speed)
|
|
||||||
original_curved(*args, **kwargs)
|
|
||||||
backend._stream.synchronize()
|
|
||||||
staged = read(args[0], f.shape)
|
|
||||||
d, k = cl.dir.astype(int), cl.fluid_idx.astype(int)
|
|
||||||
values = staged.reshape(9, -1)[OPP[d], k+CX[d]+40*CY[d]]
|
|
||||||
passive = reflection_oracle(before, cl, None, verify_projection=False)
|
|
||||||
drive_mass += .25*float(np.sum(values-passive))
|
|
||||||
error = float(np.max(np.abs(values-expected)))
|
|
||||||
reflection_error = max(reflection_error, error)
|
|
||||||
assert error <= 1e-7
|
|
||||||
np.testing.assert_array_equal(staged[:, active], before[:, active])
|
|
||||||
b, transfer, magnitude = fine_flux(before, staged, active, solid)
|
|
||||||
boundary += .25*b
|
|
||||||
fine_transfer += .25*transfer
|
|
||||||
budget_sum += .25*magnitude
|
|
||||||
|
|
||||||
def fine(*args, **kwargs):
|
|
||||||
nonlocal budget_sum
|
|
||||||
backend._stream.synchronize()
|
|
||||||
staged = read(args[0], f.shape)
|
|
||||||
original_fine(*args, **kwargs)
|
|
||||||
backend._stream.synchronize()
|
|
||||||
advanced = read(args[1], f.shape)
|
|
||||||
assert np.isfinite(advanced[:, active]).all()
|
|
||||||
assert np.isnan(advanced[:, solid]).all()
|
|
||||||
# Include actual collision operands in the FP32 rounding budget.
|
|
||||||
budget_sum += .25*np.abs(staged[:, active]).sum(dtype=float)
|
|
||||||
traces.append(advanced[:, roi].copy())
|
|
||||||
|
|
||||||
def coarse(*args, **kwargs):
|
|
||||||
nonlocal coarse_transfer, budget_sum
|
|
||||||
backend._stream.synchronize()
|
|
||||||
before, fine_values = read(args[0], c.shape), read(args[2], f.shape)
|
|
||||||
transfer, magnitude = coarse_flux(before, fine_values, coarse_active)
|
|
||||||
coarse_transfer += transfer
|
|
||||||
budget_sum += magnitude
|
|
||||||
return original_coarse(*args, **kwargs)
|
|
||||||
|
|
||||||
with patch.object(backend, "_curved", curved), patch.object(f, "kernel", fine), \
|
|
||||||
patch.object(c, "kernel", coarse):
|
|
||||||
backend.step(2)
|
|
||||||
actual = backend.download()
|
|
||||||
population = actual[0][:, coarse_active].sum(axis=1, dtype=float)
|
|
||||||
population += .25*actual[1][:, active].sum(axis=1, dtype=float)
|
|
||||||
ledger = boundary+fine_transfer+coarse_transfer
|
|
||||||
error = MOMENTS@(population-initial_pop-ledger)
|
|
||||||
budget = 8*np.finfo(np.float32).eps*budget_sum
|
|
||||||
assert np.isfinite(budget_sum) and budget_sum > 0
|
|
||||||
assert np.max(np.abs(error)) <= budget
|
|
||||||
if speed:
|
|
||||||
assert speed*drive_mass > 0
|
|
||||||
assert len(traces) == 4 and backend.step_count == 2
|
|
||||||
if audit:
|
|
||||||
for level, mask, count in ((c, coarse_active, 2), (f, active, 4)):
|
|
||||||
pointer, size = level.module.get_global("collision_counts")
|
|
||||||
counts = read(pointer, mask.shape, np.uint32)
|
|
||||||
assert size == counts.nbytes
|
|
||||||
np.testing.assert_array_equal(counts, count*mask)
|
|
||||||
for name, values in geometry.items():
|
|
||||||
np.testing.assert_array_equal(getattr(cl, name), values)
|
|
||||||
assert vars(source.state) == state and source.geometry.center == source.center
|
|
||||||
print(f"speed={speed} audit={audit} BFL={reflection_error:.9g} ledger={error.tolist()} "
|
|
||||||
f"budget={budget:.9g} boundary={list(MOMENTS@boundary)} "
|
|
||||||
f"transfer={list(MOMENTS@(fine_transfer+coarse_transfer))} "
|
|
||||||
f"drive_mass={drive_mass:+.9g}; counts={'PASS' if audit else 'normal build'}", flush=True)
|
|
||||||
return traces, roi, actual
|
|
||||||
|
|
||||||
|
|
||||||
def uniform_reference(speed, roi):
|
def uniform_reference(speed, roi):
|
||||||
# Reuse the existing explicit config helper without importing unrelated tests.
|
# Reuse the existing explicit config helper without importing unrelated tests.
|
||||||
spec = importlib.util.spec_from_file_location("fine_surface_cases", ROOT/"tests/integration/_cases.py")
|
spec = importlib.util.spec_from_file_location("fine_surface_cases", ROOT/"tests/integration/_cases.py")
|
||||||
@@ -260,61 +71,59 @@ def uniform_reference(speed, roi):
|
|||||||
return traces
|
return traces
|
||||||
|
|
||||||
|
|
||||||
|
def run_blocked_patch(speed):
|
||||||
|
"""All-blocked run (whole-level tiles) of the same case; fine ROI after each fine hop."""
|
||||||
|
cfg = LBMConfig(nx=24, ny=24, viscosity=.05, velocity=0, rho=1)
|
||||||
|
source = object_spec(speed)
|
||||||
|
with CudaContext(0) as context:
|
||||||
|
with implementation.FixedPatchStepper(cfg, context=context, refinements=[BOUNDS],
|
||||||
|
level_blocks=[(24, 24), (40, 40)],
|
||||||
|
level_objects=[(), (source,)],
|
||||||
|
observe_raw_exchange=True) as backend:
|
||||||
|
c, f = backend._levels
|
||||||
|
flags = backend.fine_boundary.flags.reshape(40, 40)
|
||||||
|
active = np.zeros((40, 40), bool)
|
||||||
|
active[4:-4, 4:-4] = True
|
||||||
|
active &= ((flags & FLUID) != 0) & ((flags & SOLID) == 0)
|
||||||
|
yy, xx = np.mgrid[:40, :40]
|
||||||
|
roi = active & ((xx-20)**2+(yy-20)**2 <= 9**2)
|
||||||
|
backend.initialize(rest((24, 24)), rest((40, 40)))
|
||||||
|
traces, original = [], f.kernel
|
||||||
|
|
||||||
|
def fine(*args, **kwargs):
|
||||||
|
original(*args, **kwargs)
|
||||||
|
backend._stream.synchronize()
|
||||||
|
advanced = f.layout.to_dense(read(args[1], f.layout.blocked_shape))
|
||||||
|
assert np.isfinite(advanced[:, active]).all()
|
||||||
|
assert np.isnan(advanced[:, (flags & SOLID) != 0]).all()
|
||||||
|
traces.append(advanced[:, roi].copy())
|
||||||
|
|
||||||
|
with patch.object(f, "kernel", fine):
|
||||||
|
backend.step(2)
|
||||||
|
assert len(traces) == 4 and backend.step_count == 2
|
||||||
|
mass = backend.read_raw_exchange(0)["total"]["mass_into_fluid"]
|
||||||
|
return traces, roi, backend.download(), float(mass)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("speed", [0., .002, -.002])
|
@pytest.mark.parametrize("speed", [0., .002, -.002])
|
||||||
def test_fine_surface_four_hops(speed):
|
def test_fine_surface_blocked_matches_uniform_four_hops(speed):
|
||||||
assert Path(CelerisLab.__file__).resolve() == ROOT/"src/CelerisLab/__init__.py"
|
"""Blocked storage: fine ROI after each of four hops against the uniform 48^2 solver
|
||||||
assert Path(compiler._KERNEL_DIR).resolve() == ROOT/"src/CelerisLab/lbm/kernels"
|
(same tolerances as the dense test); blowing/suction drives mass with its sign;
|
||||||
assert Path(implementation.__file__).resolve() == ROOT/"src/CelerisLab/lbm/refinement_stepper.py"
|
zero actuation equals the passive circle bit for bit."""
|
||||||
actual, roi, final = run_patch(speed)
|
actual, roi, final, mass = run_blocked_patch(speed)
|
||||||
reference = uniform_reference(speed, roi)
|
reference = uniform_reference(speed, roi)
|
||||||
for got, wanted in zip(actual, reference):
|
for got, wanted in zip(actual, reference):
|
||||||
np.testing.assert_allclose(got, wanted, atol=3e-6, rtol=0)
|
np.testing.assert_allclose(got, wanted, atol=3e-6, rtol=0)
|
||||||
np.testing.assert_allclose(MOMENTS@got.astype(float), MOMENTS@wanted.astype(float),
|
np.testing.assert_allclose(MOMENTS@got.astype(float), MOMENTS@wanted.astype(float),
|
||||||
atol=2e-5, rtol=0)
|
atol=2e-5, rtol=0)
|
||||||
if speed == 0:
|
if speed:
|
||||||
passive, passive_roi, passive_final = run_patch(None, audit=False)
|
assert speed*mass > 0
|
||||||
|
else:
|
||||||
|
passive, passive_roi, passive_final, _ = run_blocked_patch(None)
|
||||||
np.testing.assert_array_equal(passive_roi, roi)
|
np.testing.assert_array_equal(passive_roi, roi)
|
||||||
for got, wanted in zip(actual+list(final), passive+list(passive_final)):
|
for got, wanted in zip(actual+list(final), passive+list(passive_final)):
|
||||||
np.testing.assert_array_equal(got, wanted)
|
np.testing.assert_array_equal(got, wanted)
|
||||||
|
print(f"blocked speed={speed} four hops vs uniform PASS mass_into_fluid={mass:+.9g}", flush=True)
|
||||||
|
|
||||||
@pytest.mark.parametrize("missing", ["surface_velocity_gpu", "link_exchange_gpu"])
|
|
||||||
def test_missing_surface_device_buffer_rejects_before_launch(missing):
|
|
||||||
backend = implementation.FixedPatchStepper.__new__(implementation.FixedPatchStepper)
|
|
||||||
backend.fine_boundary = SimpleNamespace(objects=[object_spec(.002)])
|
|
||||||
backend._fine_curved = SimpleNamespace(fluid_idx_gpu=object(),
|
|
||||||
surface_velocity_gpu=object(), link_exchange_gpu=object())
|
|
||||||
setattr(backend._fine_curved, missing, None)
|
|
||||||
backend._curved = Mock()
|
|
||||||
with pytest.raises(RuntimeError, match="Surface boundary device buffers are missing"):
|
|
||||||
backend._apply_fine_boundary(object())
|
|
||||||
backend._curved.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
def test_close_frees_fine_boundary_after_stream_failure_once():
|
|
||||||
backend = implementation.FixedPatchStepper.__new__(implementation.FixedPatchStepper)
|
|
||||||
backend.ctx = SimpleNamespace(activate=nullcontext)
|
|
||||||
backend._lock = RLock()
|
|
||||||
backend._status = "ready"
|
|
||||||
stream, soa, first, second = Mock(), Mock(), Mock(), Mock()
|
|
||||||
stream.synchronize.side_effect = RuntimeError("stream synchronization failed")
|
|
||||||
backend._stream = stream
|
|
||||||
backend._fine_curved = soa
|
|
||||||
backend._fine_flags = first
|
|
||||||
backend._curved = Mock()
|
|
||||||
backend._levels = [object(), object()]
|
|
||||||
backend._allocations = [first, second]
|
|
||||||
with pytest.raises(RuntimeError, match="stream synchronization failed"):
|
|
||||||
backend.close()
|
|
||||||
assert backend._status == "closed"
|
|
||||||
assert backend._levels == [] and backend._allocations == []
|
|
||||||
assert backend._fine_curved is None and backend._fine_flags is None
|
|
||||||
assert backend._curved is None and backend._stream is None
|
|
||||||
backend.close()
|
|
||||||
stream.synchronize.assert_called_once_with()
|
|
||||||
soa.free.assert_called_once_with()
|
|
||||||
first.free.assert_called_once_with()
|
|
||||||
second.free.assert_called_once_with()
|
|
||||||
|
|
||||||
|
|
||||||
def test_cross_interface_donor_rejects_before_cuda():
|
def test_cross_interface_donor_rejects_before_cuda():
|
||||||
@@ -325,6 +134,8 @@ def test_cross_interface_donor_rejects_before_cuda():
|
|||||||
context.activate.side_effect = AssertionError("must reject before CUDA activation")
|
context.activate.side_effect = AssertionError("must reject before CUDA activation")
|
||||||
with patch.object(cuda, "mem_alloc") as allocate:
|
with patch.object(cuda, "mem_alloc") as allocate:
|
||||||
with pytest.raises(ValueError, match="donor crosses"):
|
with pytest.raises(ValueError, match="donor crosses"):
|
||||||
implementation.FixedPatchStepper(cfg, BOUNDS, context=context, fine_objects=[source])
|
implementation.FixedPatchStepper(cfg, context=context, refinements=[BOUNDS],
|
||||||
|
level_blocks=[(24, 24), (40, 40)],
|
||||||
|
level_objects=[(), (source,)])
|
||||||
context.activate.assert_not_called()
|
context.activate.assert_not_called()
|
||||||
allocate.assert_not_called()
|
allocate.assert_not_called()
|
||||||
|
|||||||
@@ -48,9 +48,8 @@ def make(context):
|
|||||||
for (x, y), amplitude in zip(centers, (.003, -.002))
|
for (x, y), amplitude in zip(centers, (.003, -.002))
|
||||||
)
|
)
|
||||||
run = FixedPatchStepper(
|
run = FixedPatchStepper(
|
||||||
CFG, PARENTS, context=context, base_block=(4, 4), fine_block=(4, 4),
|
CFG, context=context, refinements=[PARENTS, START], level_blocks=[(4, 4)] * 3,
|
||||||
child_block=(4, 4), child_rectangles_in_parent=START,
|
level_objects=[(), (), objects], level_jets=[(), (), jets],
|
||||||
child_objects=objects, child_jets=jets,
|
|
||||||
)
|
)
|
||||||
return run, objects
|
return run, objects
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,8 @@ B = ((16,16,32,40), (60,16,76,40))
|
|||||||
def make(ctx):
|
def make(ctx):
|
||||||
body = SimObject(71, geometry=CircleGeometry(12.,14.,.8),center=(12.,14.),radius=.8)
|
body = SimObject(71, geometry=CircleGeometry(12.,14.,.8),center=(12.,14.),radius=.8)
|
||||||
jet = Jet(Tube((10.5,14.),0.,3.,.3),Parallel(0.),'parabolic',.003)
|
jet = Jet(Tube((10.5,14.),0.,3.,.3),Parallel(0.),'parabolic',.003)
|
||||||
run = FixedPatchStepper(CFG,PARENTS,context=ctx,base_block=(4,4),fine_block=(4,4),
|
run = FixedPatchStepper(CFG,context=ctx,refinements=[PARENTS,A],level_blocks=[(4,4)]*3,
|
||||||
child_block=(4,4),child_rectangles_in_parent=A,
|
level_objects=[(),(),(body,)],level_jets=[(),(),(jet,)])
|
||||||
child_objects=(body,),child_jets=(jet,))
|
|
||||||
return run,(body,)
|
return run,(body,)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -90,14 +90,18 @@ def census(run, index, buffer):
|
|||||||
return totals(dense, leaf), external, independent
|
return totals(dense, leaf), external, independent
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def fine(block):
|
||||||
|
"""Two-level circle+jet construction on BOUNDS, blocked with a (12, 8) root tile."""
|
||||||
|
return dict(refinements=[BOUNDS], level_blocks=[(12, 8), block],
|
||||||
|
level_objects=[(), (circle(),)], level_jets=[(), (jet(),)])
|
||||||
|
|
||||||
class RawExchangeGPU(unittest.TestCase):
|
class RawExchangeGPU(unittest.TestCase):
|
||||||
def test_row_ledger_interval_jet_switch_and_no_population_effect(self):
|
def test_row_ledger_interval_jet_switch_and_no_population_effect(self):
|
||||||
with CudaContext(0) as ctx:
|
with CudaContext(0) as ctx:
|
||||||
for block in ((32, 40), (16, 20), (8, 10)):
|
for block in ((32, 40), (16, 20), (8, 10)):
|
||||||
with FixedPatchStepper(CFG, BOUNDS, context=ctx, fine_block=block,
|
with FixedPatchStepper(CFG, context=ctx, **fine(block), observe_raw_exchange=True) as measured, \
|
||||||
fine_objects=[circle()], jets=[jet()], observe_raw_exchange=True) as measured, \
|
FixedPatchStepper(CFG, context=ctx, **fine(block)) as plain:
|
||||||
FixedPatchStepper(CFG, BOUNDS, context=ctx, fine_block=block,
|
|
||||||
fine_objects=[circle()], jets=[jet()]) as plain:
|
|
||||||
arrays = [state(l.shape) for l in measured._levels]
|
arrays = [state(l.shape) for l in measured._levels]
|
||||||
measured.initialize(*arrays)
|
measured.initialize(*arrays)
|
||||||
plain.initialize(*arrays)
|
plain.initialize(*arrays)
|
||||||
@@ -142,27 +146,6 @@ class RawExchangeGPU(unittest.TestCase):
|
|||||||
np.testing.assert_array_equal(measured.read_raw_exchange(0)['total']['impulse'],
|
np.testing.assert_array_equal(measured.read_raw_exchange(0)['total']['impulse'],
|
||||||
obs['total']['impulse'])
|
obs['total']['impulse'])
|
||||||
|
|
||||||
def test_dense_fine_storage_reference(self):
|
|
||||||
"""Same fine resolution/initial data with dense storage: a numerical
|
|
||||||
reference for cut-link rows, not a uniform-whole-domain convergence study."""
|
|
||||||
with CudaContext(0) as ctx:
|
|
||||||
with FixedPatchStepper(CFG, BOUNDS, context=ctx, fine_objects=[circle()], jets=[jet()]) as dense, \
|
|
||||||
FixedPatchStepper(CFG, BOUNDS, context=ctx, fine_block=(16, 20),
|
|
||||||
fine_objects=[circle()], jets=[jet()], observe_raw_exchange=True) as blocked:
|
|
||||||
arrays = [state(l.shape) for l in blocked._levels]
|
|
||||||
dense.initialize(*arrays)
|
|
||||||
blocked.initialize(*arrays)
|
|
||||||
for speed in (0., .002, 0.):
|
|
||||||
dense.set_jet_amplitude(0, speed)
|
|
||||||
blocked.set_jet_amplitude(0, speed)
|
|
||||||
dense.step(); blocked.step()
|
|
||||||
for a, e in zip(blocked.download(), dense.download()):
|
|
||||||
np.testing.assert_array_equal(a, e)
|
|
||||||
count = dense._fine_curved.count
|
|
||||||
expected = download(dense._fine_curved.link_exchange_gpu, (count, 3))
|
|
||||||
actual = download(blocked._level_boundaries[1].last_raw, (count, 4))
|
|
||||||
np.testing.assert_array_equal(actual[:, :3], expected)
|
|
||||||
|
|
||||||
def test_depth_four_interval_storage_and_sampling(self):
|
def test_depth_four_interval_storage_and_sampling(self):
|
||||||
cfg = LBMConfig(nx=48, ny=48, viscosity=.02)
|
cfg = LBMConfig(nx=48, ny=48, viscosity=.02)
|
||||||
obj = SimObject(0, geometry=CircleGeometry(16., 16., .5), center=(16., 16.), radius=.5)
|
obj = SimObject(0, geometry=CircleGeometry(16., 16., .5), center=(16., 16.), radius=.5)
|
||||||
@@ -221,16 +204,13 @@ class RawExchangeGPU(unittest.TestCase):
|
|||||||
Eight local hops are a short-time equivalence check, not convergence.
|
Eight local hops are a short-time equivalence check, not convergence.
|
||||||
The test adapter only enables the standalone kernel's optional inputs.
|
The test adapter only enables the standalone kernel's optional inputs.
|
||||||
"""
|
"""
|
||||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
|
||||||
from CelerisLab.lbm.block_stepper import PeriodicBlockStepper
|
|
||||||
fine_cfg = replace(CFG, nx=64, ny=48, viscosity=.2)
|
fine_cfg = replace(CFG, nx=64, ny=48, viscosity=.2)
|
||||||
obj = SimObject(0, geometry=CircleGeometry(31.5, 23.5, 4.3),
|
obj = SimObject(0, geometry=CircleGeometry(31.5, 23.5, 4.3),
|
||||||
center=(31.5, 23.5), radius=4.3)
|
center=(31.5, 23.5), radius=4.3)
|
||||||
layout = BlockLayout2D.build(48, 64, 8, 8)
|
|
||||||
with CudaContext(0) as ctx:
|
with CudaContext(0) as ctx:
|
||||||
with PeriodicBlockStepper(fine_cfg, layout, context=ctx, objects=[obj]) as uniform, \
|
with FixedPatchStepper(fine_cfg, context=ctx, refinements=[], level_blocks=[(8, 8)],
|
||||||
FixedPatchStepper(CFG, BOUNDS, context=ctx, fine_block=(16, 20),
|
level_objects=[(obj,)]) as uniform, \
|
||||||
fine_objects=[circle()], jets=[jet()], observe_raw_exchange=True) as amr:
|
FixedPatchStepper(CFG, context=ctx, **fine((16, 20)), observe_raw_exchange=True) as amr:
|
||||||
amr.initialize(*(state(l.shape) for l in amr._levels))
|
amr.initialize(*(state(l.shape) for l in amr._levels))
|
||||||
uniform.initialize(state((9, 48, 64)))
|
uniform.initialize(state((9, 48, 64)))
|
||||||
b = amr._level_boundaries[1]
|
b = amr._level_boundaries[1]
|
||||||
@@ -238,18 +218,21 @@ class RawExchangeGPU(unittest.TestCase):
|
|||||||
links = obj.geometry.build_cut_links(64, 48)
|
links = obj.geometry.build_cut_links(64, 48)
|
||||||
np.testing.assert_array_equal([v.dir for v in links], b.tables['dir'])
|
np.testing.assert_array_equal([v.dir for v in links], b.tables['dir'])
|
||||||
np.testing.assert_allclose([v.q for v in links], b.tables['q'], atol=2e-7, rtol=0)
|
np.testing.assert_allclose([v.q for v in links], b.tables['q'], atol=2e-7, rtol=0)
|
||||||
# Same fine-grid force arms after translation to the same center.
|
# Same fine-grid force arms after translation to the same center: the
|
||||||
rx = uniform._upload(b.tables['rx'])
|
# single level's static BFL gets the AMR Jet column and force arms; the raw
|
||||||
ry = uniform._upload(b.tables['ry'])
|
# output pointer is substituted at launch (no stepper-side accumulation).
|
||||||
|
root = uniform._level_boundaries[0]
|
||||||
velocity = uniform._upload(b.tables['surface_velocity'])
|
velocity = uniform._upload(b.tables['surface_velocity'])
|
||||||
|
root.device.update(surface_velocity=velocity, rx=uniform._upload(b.tables['rx']),
|
||||||
|
ry=uniform._upload(b.tables['ry']))
|
||||||
raw = uniform._allocate(count*16)
|
raw = uniform._allocate(count*16)
|
||||||
original = uniform._curved
|
original = root.kernel
|
||||||
samples = []
|
samples = []
|
||||||
def measured_boundary(*args, **kw):
|
def measured_boundary(*args, **kw):
|
||||||
original(*args[:7], velocity, rx, ry, raw, **kw)
|
original(*args[:10], raw, **kw) # BlockCurvedBoundary raw-exchange output
|
||||||
uniform._stream.synchronize()
|
uniform._stream.synchronize()
|
||||||
samples.append(download(raw, (count, 4)))
|
samples.append(download(raw, (count, 4)))
|
||||||
uniform._curved = measured_boundary
|
root.kernel = measured_boundary
|
||||||
for speed in (0., .002, -.001, 0.):
|
for speed in (0., .002, -.001, 0.):
|
||||||
amr.set_jet_amplitude(0, speed)
|
amr.set_jet_amplitude(0, speed)
|
||||||
cuda.memcpy_htod(velocity, b.tables['surface_velocity'])
|
cuda.memcpy_htod(velocity, b.tables['surface_velocity'])
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
Run CPU and GPU classes in separate processes. GPU callers must first check
|
Run CPU and GPU classes in separate processes. GPU callers must first check
|
||||||
resources/device availability; this file never selects or reserves a GPU.
|
resources/device availability; this file never selects or reserves a GPU.
|
||||||
|
|
||||||
|
Dense refinement storage was removed: every level is blocked and level_blocks
|
||||||
|
is required (TileContractCPU). The retired dense/blocked route parity gates
|
||||||
|
(BlockedBaseGPU exact parity, DenseBlockedRouteParity incl. MRT and two
|
||||||
|
bodies, the route capability matrix) passed and are recorded at 1573942 and
|
||||||
|
2469af7.
|
||||||
"""
|
"""
|
||||||
from contextlib import ExitStack, contextmanager, nullcontext
|
from contextlib import ExitStack, contextmanager, nullcontext
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
@@ -13,7 +19,7 @@ from unittest.mock import Mock, patch
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pycuda.driver as cuda
|
import pycuda.driver as cuda
|
||||||
|
|
||||||
from CelerisLab.cuda import compiler_v2 as compiler
|
from CelerisLab.cuda import compiler
|
||||||
from CelerisLab.lbm import refinement_stepper
|
from CelerisLab.lbm import refinement_stepper
|
||||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||||
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
||||||
@@ -73,6 +79,7 @@ def mocked_backend(mode="periodic", fail_alloc=None):
|
|||||||
stack.enter_context(patch.object(compiler, name))
|
stack.enter_context(patch.object(compiler, name))
|
||||||
stack.enter_context(patch.object(compiler, "load_module", return_value=module))
|
stack.enter_context(patch.object(compiler, "load_module", return_value=module))
|
||||||
stack.enter_context(patch.object(cuda, "Stream", return_value=stream))
|
stack.enter_context(patch.object(cuda, "Stream", return_value=stream))
|
||||||
|
stack.enter_context(patch.object(cuda, "mem_get_info", return_value=(1 << 40, 1 << 40)))
|
||||||
stack.enter_context(patch.object(cuda, "mem_alloc", side_effect=allocate))
|
stack.enter_context(patch.object(cuda, "mem_alloc", side_effect=allocate))
|
||||||
stack.enter_context(patch.object(cuda, "memcpy_htod",
|
stack.enter_context(patch.object(cuda, "memcpy_htod",
|
||||||
side_effect=lambda p, a: uploads.append((p, a))))
|
side_effect=lambda p, a: uploads.append((p, a))))
|
||||||
@@ -83,11 +90,10 @@ def mocked_backend(mode="periodic", fail_alloc=None):
|
|||||||
class BlockedBaseCPU(unittest.TestCase):
|
class BlockedBaseCPU(unittest.TestCase):
|
||||||
def test_reject_before_cuda(self):
|
def test_reject_before_cuda(self):
|
||||||
context = Mock(activate=Mock(side_effect=AssertionError("CUDA reached")))
|
context = Mock(activate=Mock(side_effect=AssertionError("CUDA reached")))
|
||||||
for kwargs in ({"base_block": (12, 8)},
|
for blocks in ([(12, 8), None], [(7, 8), FINE_BLOCK], [(12,), FINE_BLOCK]):
|
||||||
{"base_block": (7, 8), "fine_block": FINE_BLOCK},
|
with self.subTest(blocks=blocks), self.assertRaises(ValueError):
|
||||||
{"base_block": (12,), "fine_block": FINE_BLOCK}):
|
FixedPatchStepper(config("periodic"), context=context, refinements=[BOUNDS],
|
||||||
with self.subTest(kwargs=kwargs), self.assertRaises(ValueError):
|
level_blocks=blocks)
|
||||||
FixedPatchStepper(config("periodic"), BOUNDS, context=context, **kwargs)
|
|
||||||
context.activate.assert_not_called()
|
context.activate.assert_not_called()
|
||||||
|
|
||||||
def test_all_arrival_rows_donors_and_kind_masks(self):
|
def test_all_arrival_rows_donors_and_kind_masks(self):
|
||||||
@@ -193,9 +199,10 @@ class BlockedBaseCPU(unittest.TestCase):
|
|||||||
|
|
||||||
def test_launch_order_stream_wall_constants_and_buffer_swaps(self):
|
def test_launch_order_stream_wall_constants_and_buffer_swaps(self):
|
||||||
with mocked_backend("slot_reservoir") as m:
|
with mocked_backend("slot_reservoir") as m:
|
||||||
backend = FixedPatchStepper(config("slot_reservoir"), BOUNDS, context=m["context"],
|
backend = FixedPatchStepper(config("slot_reservoir"), context=m["context"],
|
||||||
coarse_boundary="slot_reservoir", fine_block=FINE_BLOCK,
|
coarse_boundary="slot_reservoir", refinements=[BOUNDS],
|
||||||
base_block=(3, 4), fine_objects=[circle(actuation(.002))])
|
level_blocks=[(3, 4), FINE_BLOCK],
|
||||||
|
level_objects=[(), (circle(actuation(.002)),)])
|
||||||
try:
|
try:
|
||||||
backend.initialize(state(NY, NX, 1), state(*backend.patch.fine_shape, 2))
|
backend.initialize(state(NY, NX, 1), state(*backend.patch.fine_shape, 2))
|
||||||
coarse, fine = backend._levels
|
coarse, fine = backend._levels
|
||||||
@@ -223,6 +230,8 @@ class BlockedBaseCPU(unittest.TestCase):
|
|||||||
self.assertIs(collide[4], coarse.kind)
|
self.assertIs(collide[4], coarse.kind)
|
||||||
walls = [a for ptr, a in m["uploads"] if isinstance(ptr, str) and ptr == "d_block_wall"]
|
walls = [a for ptr, a in m["uploads"] if isinstance(ptr, str) and ptr == "d_block_wall"]
|
||||||
self.assertEqual(len(walls), 1)
|
self.assertEqual(len(walls), 1)
|
||||||
|
self.assertEqual([call.kwargs["block_root_bc"] for call in
|
||||||
|
compiler.generate_config.call_args_list], [1, 0])
|
||||||
width, height, slot, speed = struct.unpack("=IIIf", walls[0])
|
width, height, slot, speed = struct.unpack("=IIIf", walls[0])
|
||||||
self.assertEqual((width, height, slot), (NX, NY, 8))
|
self.assertEqual((width, height, slot), (NX, NY, 8))
|
||||||
self.assertAlmostEqual(speed, .001)
|
self.assertAlmostEqual(speed, .001)
|
||||||
@@ -236,16 +245,16 @@ class BlockedBaseCPU(unittest.TestCase):
|
|||||||
for fail_at in (1, 6, 10):
|
for fail_at in (1, 6, 10):
|
||||||
with self.subTest(fail_at=fail_at), mocked_backend(fail_alloc=fail_at) as m:
|
with self.subTest(fail_at=fail_at), mocked_backend(fail_alloc=fail_at) as m:
|
||||||
with self.assertRaisesRegex(MemoryError, "injected allocation"):
|
with self.assertRaisesRegex(MemoryError, "injected allocation"):
|
||||||
FixedPatchStepper(config("periodic"), BOUNDS, context=m["context"],
|
FixedPatchStepper(config("periodic"), context=m["context"], refinements=[BOUNDS],
|
||||||
fine_block=FINE_BLOCK, base_block=(3, 4))
|
level_blocks=[(3, 4), FINE_BLOCK])
|
||||||
self.assertEqual(len(m["allocations"]), fail_at)
|
self.assertEqual(len(m["allocations"]), fail_at)
|
||||||
for allocation in m["allocations"]:
|
for allocation in m["allocations"]:
|
||||||
allocation.free.assert_called_once_with()
|
allocation.free.assert_called_once_with()
|
||||||
|
|
||||||
def test_failed_parent_stage_cannot_publish_or_resume(self):
|
def test_failed_parent_stage_cannot_publish_or_resume(self):
|
||||||
with mocked_backend() as m:
|
with mocked_backend() as m:
|
||||||
backend = FixedPatchStepper(config("periodic"), BOUNDS, context=m["context"],
|
backend = FixedPatchStepper(config("periodic"), context=m["context"], refinements=[BOUNDS],
|
||||||
fine_block=FINE_BLOCK, base_block=(3, 4))
|
level_blocks=[(3, 4), FINE_BLOCK])
|
||||||
backend.initialize(state(NY, NX, 1), state(*backend.patch.fine_shape, 2))
|
backend.initialize(state(NY, NX, 1), state(*backend.patch.fine_shape, 2))
|
||||||
m["functions"]["BlockCoalesce"].side_effect = RuntimeError("injected parent stage failure")
|
m["functions"]["BlockCoalesce"].side_effect = RuntimeError("injected parent stage failure")
|
||||||
current = backend._levels[0].current
|
current = backend._levels[0].current
|
||||||
@@ -263,61 +272,33 @@ class BlockedBaseCPU(unittest.TestCase):
|
|||||||
allocation.free.assert_called_once_with()
|
allocation.free.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
class BlockedBaseGPU(unittest.TestCase):
|
class TileContractCPU(unittest.TestCase):
|
||||||
def test_bodyfree_exact_parity_and_raw_invalid_slots(self):
|
"""level_blocks is required: one positive (by, bx) tile per level, checked before CUDA."""
|
||||||
self.compare([(None, ((24, 32), (3, 4), (1, 1)), 8)])
|
|
||||||
|
|
||||||
def test_actuated_circle_exact_parity_and_raw_invalid_slots(self):
|
def test_level_blocks_required_and_validated(self):
|
||||||
self.compare([(.002, ((12, 8),), 8), (-.002, ((12, 8),), 8)])
|
context = Mock(activate=Mock(side_effect=AssertionError("CUDA reached")))
|
||||||
|
cfg = config("periodic")
|
||||||
def test_non_power_two_slot_width_exact_parity(self):
|
for label, kw in (("omitted", {}),
|
||||||
self.compare([(None, ((3, 4),), 7), (None, ((3, 4),), 10)], ("slot_reservoir",))
|
("None list", dict(level_blocks=None)),
|
||||||
|
("too few levels", dict(level_blocks=[(12, 8)])),
|
||||||
def compare(self, cases, modes=("periodic", "slot_reservoir")):
|
("too many levels", dict(level_blocks=[(12, 8), FINE_BLOCK, FINE_BLOCK])),
|
||||||
from CelerisLab.cuda.context import CudaContext
|
("None root", dict(level_blocks=[None, FINE_BLOCK])),
|
||||||
expected_source = Path(__file__).resolve().parents[3] / "src/CelerisLab/lbm/refinement_stepper.py"
|
("None fine", dict(level_blocks=[(12, 8), None])),
|
||||||
self.assertEqual(Path(refinement_stepper.__file__).resolve(), expected_source)
|
("wrong arity", dict(level_blocks=[(12,), FINE_BLOCK])),
|
||||||
with CudaContext(0) as context:
|
("non-integer", dict(level_blocks=[(12, 8.0), FINE_BLOCK])),
|
||||||
for mode in modes:
|
("bool", dict(level_blocks=[(True, 8), FINE_BLOCK])),
|
||||||
for speed, layouts, width in cases:
|
("non-positive", dict(level_blocks=[(0, 8), FINE_BLOCK])),
|
||||||
cfg = config(mode)
|
("non-tuple", dict(level_blocks=[12, FINE_BLOCK])),
|
||||||
if mode == "slot_reservoir":
|
("not dividing", dict(level_blocks=[(7, 8), FINE_BLOCK]))):
|
||||||
cfg = replace(cfg, wall_slot_width=width)
|
with self.subTest(label), self.assertRaises(ValueError):
|
||||||
p = FixedPatch2D.from_config(cfg, BOUNDS)
|
FixedPatchStepper(cfg, context=context, refinements=[BOUNDS], **kw)
|
||||||
arrays = state(NY, NX, 1), state(*p.fine_shape, 2)
|
with self.assertRaisesRegex(ValueError, "whole-level tile"):
|
||||||
reference = None
|
FixedPatchStepper(cfg, context=context, refinements=[BOUNDS])
|
||||||
for base_block in (None, *layouts):
|
context.activate.assert_not_called()
|
||||||
label = f"{mode} width={width} speed={speed} base={base_block}"
|
# The documented whole-level tiles are always valid (construction reaches CUDA).
|
||||||
snapshots = []
|
whole = [(NY, NX), FixedPatch2D.from_config(cfg, BOUNDS).fine_shape]
|
||||||
with FixedPatchStepper(cfg, BOUNDS, context=context, coarse_boundary=mode,
|
with self.assertRaisesRegex(AssertionError, "CUDA reached"):
|
||||||
fine_block=FINE_BLOCK, base_block=base_block,
|
FixedPatchStepper(cfg, context=context, refinements=[BOUNDS], level_blocks=whole)
|
||||||
fine_objects=() if speed is None else [circle(actuation(speed))]) as backend:
|
|
||||||
backend.initialize(*arrays)
|
|
||||||
for step in (0, 1, 2, 8):
|
|
||||||
if step == 1 and base_block is not None:
|
|
||||||
# Blocked staging dirties invalid input slots; its output
|
|
||||||
# must poison them. Dense parent does not stage there.
|
|
||||||
coarse = backend._levels[0]
|
|
||||||
spare = coarse.layout.to_blocked(np.full(coarse.shape, 42, np.float32))
|
|
||||||
cuda.memcpy_htod(coarse.spare, spare)
|
|
||||||
backend.step(step - backend.step_count)
|
|
||||||
public, owner = backend.download(), raw(backend)
|
|
||||||
self.assertEqual(backend.step_count, step)
|
|
||||||
self.assertTrue(np.isnan(owner[0][:, 6:18, 8:24]).all(), label)
|
|
||||||
if mode != "periodic":
|
|
||||||
self.assertTrue(np.isnan(owner[0][:, (0, -1), :]).all(), label)
|
|
||||||
snapshots.append(public + owner)
|
|
||||||
backend.initialize(*arrays)
|
|
||||||
self.assertEqual(backend.step_count, 0)
|
|
||||||
for actual, expected in zip(backend.download() + raw(backend), snapshots[0]):
|
|
||||||
exact(self, actual, expected, label + " reset")
|
|
||||||
if reference is None:
|
|
||||||
reference = snapshots
|
|
||||||
else:
|
|
||||||
for step, actual, expected in zip((0, 1, 2, 8), snapshots, reference):
|
|
||||||
for index, (a, b) in enumerate(zip(actual, expected)):
|
|
||||||
exact(self, a, b, f"{label} step={step} array={index}")
|
|
||||||
print(f"PASS {label}: full finite bits/NaN masks at 0,1,2,8 + reset", flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"""Opt-in blocked fine storage for FixedPatchStepper: CPU tables plus GPU gate.
|
"""Blocked fine storage for FixedPatchStepper: CPU tables, staging and launch contracts.
|
||||||
|
|
||||||
Frozen GPU gate (same stream, 8 coarse steps, nonequilibrium start), in both
|
The former GPU gate (dense fine storage vs 1x1, 2x2 and 4x2 fine blocks,
|
||||||
periodic and slot_reservoir modes: dense fine storage vs 1x1, 2x2 and 4x2
|
bit-identical) was retired with the dense route; its result and the
|
||||||
fine blocks give bit-identical masked downloads AND bit-identical raw coarse
|
dense/blocked route parity are recorded at 1573942 and 2469af7.
|
||||||
and unpacked owner-core fine arrays, including patch-ghost values/NaN masks.
|
|
||||||
"""
|
"""
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
import unittest
|
import unittest
|
||||||
@@ -15,7 +14,7 @@ import pycuda.driver as cuda
|
|||||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||||
from CelerisLab.body.objects import SimObject, SurfaceActuation
|
from CelerisLab.body.objects import SimObject, SurfaceActuation
|
||||||
from CelerisLab.config import LBMConfig
|
from CelerisLab.config import LBMConfig
|
||||||
from CelerisLab.cuda import compiler_v2 as compiler
|
from CelerisLab.cuda import compiler
|
||||||
from CelerisLab.lbm import curved_links
|
from CelerisLab.lbm import curved_links
|
||||||
from CelerisLab.lbm.blocks import NO_DONOR, BlockLayout2D, blocked_link_tables
|
from CelerisLab.lbm.blocks import NO_DONOR, BlockLayout2D, blocked_link_tables
|
||||||
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
||||||
@@ -88,15 +87,13 @@ def seam_cells(layout, mapped):
|
|||||||
|
|
||||||
|
|
||||||
def raw(backend):
|
def raw(backend):
|
||||||
"""Unmasked device arrays; blocked fine storage unpacked from owner cores."""
|
"""Unmasked device arrays of every level; blocked storage unpacked from owner cores."""
|
||||||
coarse, fine = backend._levels
|
out = []
|
||||||
out = [np.empty(coarse.shape, np.float32)]
|
for level in backend._levels:
|
||||||
out.append(np.empty(fine.shape if backend.fine_layout is None
|
array = np.empty(level.shape if level.layout is None else level.layout.blocked_shape,
|
||||||
else backend.fine_layout.blocked_shape, np.float32))
|
np.float32)
|
||||||
cuda.memcpy_dtoh(out[0], coarse.current)
|
cuda.memcpy_dtoh(array, level.current)
|
||||||
cuda.memcpy_dtoh(out[1], fine.current)
|
out.append(array if level.layout is None else level.layout.to_dense(array))
|
||||||
if backend.fine_layout is not None:
|
|
||||||
out[1] = backend.fine_layout.to_dense(out[1])
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -105,9 +102,10 @@ class BlockedFineCPU(unittest.TestCase):
|
|||||||
return Mock(activate=Mock(side_effect=AssertionError("CUDA reached")), sm_arch="sm_70")
|
return Mock(activate=Mock(side_effect=AssertionError("CUDA reached")), sm_arch="sm_70")
|
||||||
|
|
||||||
def test_rejections_before_cuda(self):
|
def test_rejections_before_cuda(self):
|
||||||
for kw in ({"fine_block": (7, 7)}, {"fine_block": (16,)}):
|
for kw in ({"level_blocks": [None, (7, 7)]}, {"level_blocks": [None, (16,)]}):
|
||||||
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
||||||
FixedPatchStepper(config("periodic"), BOUNDS, context=self.forbidden_context(), **kw)
|
FixedPatchStepper(config("periodic"), context=self.forbidden_context(),
|
||||||
|
refinements=[BOUNDS], **kw)
|
||||||
|
|
||||||
def test_single_block_layout_allocates_no_empty_halo_tables(self):
|
def test_single_block_layout_allocates_no_empty_halo_tables(self):
|
||||||
# A 1x1 non-periodic fine layout has zero halo pairs; CUDA rejects mem_alloc(0).
|
# A 1x1 non-periodic fine layout has zero halo pairs; CUDA rejects mem_alloc(0).
|
||||||
@@ -121,12 +119,12 @@ class BlockedFineCPU(unittest.TestCase):
|
|||||||
raise AssertionError("zero-byte device allocation")
|
raise AssertionError("zero-byte device allocation")
|
||||||
sizes.append(size)
|
sizes.append(size)
|
||||||
return Mock()
|
return Mock()
|
||||||
with patch.object(cuda, "Stream"), patch.object(compiler, "generate_config"), \
|
with patch.object(cuda, "Stream"), patch.object(cuda, "mem_get_info", return_value=(1 << 40, 1 << 40)), patch.object(compiler, "generate_config"), \
|
||||||
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
||||||
patch.object(compiler, "load_module", return_value=module), \
|
patch.object(compiler, "load_module", return_value=module), \
|
||||||
patch.object(cuda, "mem_alloc", side_effect=alloc), patch.object(cuda, "memcpy_htod"):
|
patch.object(cuda, "mem_alloc", side_effect=alloc), patch.object(cuda, "memcpy_htod"):
|
||||||
backend = FixedPatchStepper(config("periodic"), BOUNDS, context=context,
|
backend = FixedPatchStepper(config("periodic"), context=context, refinements=[BOUNDS],
|
||||||
fine_block=FINE_BLOCKS[0])
|
level_blocks=[(12, 8), FINE_BLOCKS[0]])
|
||||||
self.assertEqual(len(backend.fine_layout.halo_dst), 0)
|
self.assertEqual(len(backend.fine_layout.halo_dst), 0)
|
||||||
self.assertIsNone(backend._levels[1].halo_dst)
|
self.assertIsNone(backend._levels[1].halo_dst)
|
||||||
backend._gather_halo(backend._levels[1], Mock()) # no launch without pairs
|
backend._gather_halo(backend._levels[1], Mock()) # no launch without pairs
|
||||||
@@ -171,14 +169,15 @@ class BlockedFineCPU(unittest.TestCase):
|
|||||||
return functions.setdefault(name, Mock(side_effect=lambda *a, **k: calls.append((name, a))))
|
return functions.setdefault(name, Mock(side_effect=lambda *a, **k: calls.append((name, a))))
|
||||||
module = Mock(get_function=Mock(side_effect=get_function))
|
module = Mock(get_function=Mock(side_effect=get_function))
|
||||||
module.get_global.return_value = (Mock(), 20)
|
module.get_global.return_value = (Mock(), 20)
|
||||||
with patch.object(cuda, "Stream"), patch.object(compiler, "generate_config"), \
|
with patch.object(cuda, "Stream"), patch.object(cuda, "mem_get_info", return_value=(1 << 40, 1 << 40)), patch.object(compiler, "generate_config"), \
|
||||||
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
||||||
patch.object(compiler, "load_module", return_value=module), \
|
patch.object(compiler, "load_module", return_value=module), \
|
||||||
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
|
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
|
||||||
patch.object(cuda, "memcpy_htod"), \
|
patch.object(cuda, "memcpy_htod"), \
|
||||||
patch.object(curved_links, "CurvedLinkSoA", side_effect=AssertionError("dense SoA built")):
|
patch.object(curved_links, "CurvedLinkSoA", side_effect=AssertionError("dense SoA built")):
|
||||||
backend = FixedPatchStepper(config("periodic"), BOUNDS, context=context,
|
backend = FixedPatchStepper(config("periodic"), context=context, refinements=[BOUNDS],
|
||||||
fine_objects=objects, fine_block=FINE_BLOCKS[1])
|
level_blocks=[(12, 8), FINE_BLOCKS[1]],
|
||||||
|
level_objects=[(), tuple(objects)])
|
||||||
backend.initialize(state(NY, NX, 1), state(*backend.patch.fine_shape, 2))
|
backend.initialize(state(NY, NX, 1), state(*backend.patch.fine_shape, 2))
|
||||||
return backend, calls
|
return backend, calls
|
||||||
|
|
||||||
@@ -187,8 +186,8 @@ class BlockedFineCPU(unittest.TestCase):
|
|||||||
backend, calls = self.mocked_backend([circle(spec)])
|
backend, calls = self.mocked_backend([circle(spec)])
|
||||||
c, f = backend._levels
|
c, f = backend._levels
|
||||||
coarse, cur, spare = c.current, f.current, f.spare
|
coarse, cur, spare = c.current, f.current, f.spare
|
||||||
flags, kind = backend._fine_flags, backend._levels[1].kind
|
flags, kind = backend._level_boundaries[1].flags, backend._levels[1].kind
|
||||||
velocity = backend._link_tables.get("surface_velocity")
|
velocity = backend._level_boundaries[1].device.get("surface_velocity")
|
||||||
self.assertEqual(velocity is None, spec is None)
|
self.assertEqual(velocity is None, spec is None)
|
||||||
backend.step(1)
|
backend.step(1)
|
||||||
backend.close()
|
backend.close()
|
||||||
@@ -196,8 +195,9 @@ class BlockedFineCPU(unittest.TestCase):
|
|||||||
self.assertEqual(order, [("BlockExplode", coarse),
|
self.assertEqual(order, [("BlockExplode", coarse),
|
||||||
("BlockHaloCopy", cur), ("BlockCurvedBoundary", cur), ("BlockStep", cur),
|
("BlockHaloCopy", cur), ("BlockCurvedBoundary", cur), ("BlockStep", cur),
|
||||||
("BlockHaloCopy", spare), ("BlockCurvedBoundary", spare),
|
("BlockHaloCopy", spare), ("BlockCurvedBoundary", spare),
|
||||||
("BlockStep", spare), ("PatchCoarseStep", coarse)])
|
("BlockStep", spare), ("BlockHaloCopy", coarse),
|
||||||
steps = [a for name, a in calls if name == "BlockStep"]
|
("BlockCoalesce", cur), ("BlockStep", coarse)])
|
||||||
|
steps = [a for name, a in calls if name == "BlockStep"][:2] # the two fine hops
|
||||||
self.assertEqual([a[1] for a in steps], [spare, cur])
|
self.assertEqual([a[1] for a in steps], [spare, cur])
|
||||||
for a in steps:
|
for a in steps:
|
||||||
self.assertIs(a[3], flags) # blocked flags, not null
|
self.assertIs(a[3], flags) # blocked flags, not null
|
||||||
@@ -213,7 +213,7 @@ class BlockedFineCPU(unittest.TestCase):
|
|||||||
for speed in (0.0, .002): # any spec, even zero amplitude
|
for speed in (0.0, .002): # any spec, even zero amplitude
|
||||||
backend, calls = self.mocked_backend([circle(actuation(speed))])
|
backend, calls = self.mocked_backend([circle(actuation(speed))])
|
||||||
self.assertIsNotNone(backend._fine_tables["surface_velocity"])
|
self.assertIsNotNone(backend._fine_tables["surface_velocity"])
|
||||||
del backend._link_tables["surface_velocity"]
|
del backend._level_boundaries[1].device["surface_velocity"]
|
||||||
with self.assertRaisesRegex(RuntimeError, "Surface velocity device buffer is missing"):
|
with self.assertRaisesRegex(RuntimeError, "Surface velocity device buffer is missing"):
|
||||||
backend.step(1)
|
backend.step(1)
|
||||||
names = [name for name, _ in calls]
|
names = [name for name, _ in calls]
|
||||||
@@ -279,91 +279,5 @@ class BlockedFineCPU(unittest.TestCase):
|
|||||||
self.assertEqual(set(np.unique(kind).tolist()) - {ACTIVE, INNER_GHOST, OUTER_GHOST}, set())
|
self.assertEqual(set(np.unique(kind).tolist()) - {ACTIVE, INNER_GHOST, OUTER_GHOST}, set())
|
||||||
|
|
||||||
|
|
||||||
class BlockedFineGPU(unittest.TestCase):
|
|
||||||
def test_dense_and_blocked_fine_storage_are_bit_identical(self):
|
|
||||||
from CelerisLab.cuda.context import CudaContext
|
|
||||||
for mode in ("periodic", "slot_reservoir"):
|
|
||||||
cfg = config(mode)
|
|
||||||
patch = FixedPatch2D.from_config(cfg, BOUNDS)
|
|
||||||
g_coarse, g_fine = state(NY, NX, 1), state(*patch.fine_shape, 2)
|
|
||||||
results = []
|
|
||||||
with CudaContext(0) as context:
|
|
||||||
for fine_block in (None, *FINE_BLOCKS):
|
|
||||||
with FixedPatchStepper(cfg, BOUNDS, context=context, coarse_boundary=mode,
|
|
||||||
fine_block=fine_block) as backend:
|
|
||||||
backend.initialize(g_coarse, g_fine)
|
|
||||||
backend.step(STEPS)
|
|
||||||
self.assertEqual(backend.step_count, STEPS)
|
|
||||||
results.append((backend.download(), raw(backend)))
|
|
||||||
(dense_public, dense_raw), blocked = results[0], results[1:]
|
|
||||||
kind = patch.fine_kind
|
|
||||||
self.assertTrue(np.isnan(dense_raw[1][:, kind == OUTER_GHOST]).all(), mode)
|
|
||||||
# INNER populations may be NaN after hop 2 (pulled from poisoned OUTER);
|
|
||||||
# their exact pattern is pinned by the dense-vs-blocked comparison below.
|
|
||||||
self.assertTrue(np.isfinite(dense_raw[1][:, kind == ACTIVE]).all(), mode)
|
|
||||||
for fine_block, (public, raw_arrays) in zip(FINE_BLOCKS, blocked):
|
|
||||||
msg = f"{mode} fine_block={fine_block}"
|
|
||||||
for a, b in zip(public + tuple(raw_arrays), dense_public + tuple(dense_raw)):
|
|
||||||
np.testing.assert_array_equal(a, b, err_msg=msg) # values and NaN masks
|
|
||||||
print(f"{mode}: dense vs {len(blocked)} fine-block layouts bit-identical "
|
|
||||||
f"after {STEPS} coarse steps", flush=True)
|
|
||||||
|
|
||||||
def test_passive_circle_dense_and_blocked_fine_storage_are_bit_identical(self):
|
|
||||||
from CelerisLab.cuda.context import CudaContext
|
|
||||||
for mode in ("periodic", "slot_reservoir"):
|
|
||||||
cfg = config(mode)
|
|
||||||
patch, boundary = fine_circle(mode)
|
|
||||||
solid = (boundary.flags.reshape(patch.fine_shape) & SOLID) != 0
|
|
||||||
g_coarse, g_fine = state(NY, NX, 1), state(*patch.fine_shape, 2)
|
|
||||||
results = []
|
|
||||||
with CudaContext(0) as context:
|
|
||||||
for fine_block in (None, *FINE_BLOCKS):
|
|
||||||
with FixedPatchStepper(cfg, BOUNDS, context=context, coarse_boundary=mode,
|
|
||||||
fine_objects=[circle()], fine_block=fine_block) as backend:
|
|
||||||
backend.initialize(g_coarse, g_fine)
|
|
||||||
backend.step(STEPS)
|
|
||||||
results.append((backend.download(), raw(backend)))
|
|
||||||
(dense_public, dense_raw), blocked = results[0], results[1:]
|
|
||||||
fine = dense_raw[1]
|
|
||||||
self.assertTrue(np.isfinite(fine[:, (patch.fine_kind == ACTIVE) & ~solid]).all(), mode)
|
|
||||||
self.assertTrue(np.isnan(fine[:, solid]).all(), mode)
|
|
||||||
self.assertTrue(np.isnan(fine[:, patch.fine_kind == OUTER_GHOST]).all(), mode)
|
|
||||||
for fine_block, (public, raw_arrays) in zip(FINE_BLOCKS, blocked):
|
|
||||||
msg = f"circle {mode} fine_block={fine_block}"
|
|
||||||
for a, b in zip(public + tuple(raw_arrays), dense_public + tuple(dense_raw)):
|
|
||||||
np.testing.assert_array_equal(a, b, err_msg=msg) # values and NaN masks
|
|
||||||
print(f"circle {mode}: dense vs {len(blocked)} fine-block layouts bit-identical "
|
|
||||||
f"after {STEPS} coarse steps", flush=True)
|
|
||||||
|
|
||||||
def test_actuated_circle_dense_and_blocked_fine_storage_are_bit_identical(self):
|
|
||||||
from CelerisLab.cuda.context import CudaContext
|
|
||||||
for mode in ("periodic", "slot_reservoir"):
|
|
||||||
for speed in (.002, -.002):
|
|
||||||
cfg = config(mode)
|
|
||||||
patch, boundary = fine_circle(mode, actuation(speed))
|
|
||||||
solid = (boundary.flags.reshape(patch.fine_shape) & SOLID) != 0
|
|
||||||
g_coarse, g_fine = state(NY, NX, 1), state(*patch.fine_shape, 2)
|
|
||||||
results = []
|
|
||||||
with CudaContext(0) as context:
|
|
||||||
for fine_block in (None, *FINE_BLOCKS):
|
|
||||||
with FixedPatchStepper(cfg, BOUNDS, context=context, coarse_boundary=mode,
|
|
||||||
fine_objects=[circle(actuation(speed))],
|
|
||||||
fine_block=fine_block) as backend:
|
|
||||||
backend.initialize(g_coarse, g_fine)
|
|
||||||
backend.step(STEPS)
|
|
||||||
results.append((backend.download(), raw(backend)))
|
|
||||||
(dense_public, dense_raw), blocked = results[0], results[1:]
|
|
||||||
fine = dense_raw[1]
|
|
||||||
self.assertTrue(np.isfinite(fine[:, (patch.fine_kind == ACTIVE) & ~solid]).all())
|
|
||||||
self.assertTrue(np.isnan(fine[:, solid]).all())
|
|
||||||
self.assertTrue(np.isnan(fine[:, patch.fine_kind == OUTER_GHOST]).all())
|
|
||||||
for fine_block, (public, raw_arrays) in zip(FINE_BLOCKS, blocked):
|
|
||||||
msg = f"actuated {speed} {mode} fine_block={fine_block}"
|
|
||||||
for a, b in zip(public + tuple(raw_arrays), dense_public + tuple(dense_raw)):
|
|
||||||
np.testing.assert_array_equal(a, b, err_msg=msg) # values and NaN masks
|
|
||||||
print(f"actuated {speed:+.3f} {mode}: dense vs {len(blocked)} fine-block layouts "
|
|
||||||
f"bit-identical after {STEPS} coarse steps", flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import pycuda.driver as cuda
|
|||||||
|
|
||||||
import CelerisLab
|
import CelerisLab
|
||||||
from CelerisLab.lbm import refinement_stepper as implementation
|
from CelerisLab.lbm import refinement_stepper as implementation
|
||||||
|
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||||
from test_refinement_gpu import (ROOT, POP_ATOL, MOMENT_ATOL, ref, postcollision,
|
from test_refinement_gpu import (ROOT, POP_ATOL, MOMENT_ATOL, ref, postcollision,
|
||||||
totals, LBMConfig, CudaContext, compiler,
|
totals, LBMConfig, CudaContext, compiler,
|
||||||
PeriodicPatchStepper)
|
FixedPatchStepper)
|
||||||
|
|
||||||
|
|
||||||
def scalar_interval(coarse, fine, bounds, slot=None):
|
def scalar_interval(coarse, fine, bounds, slot=None):
|
||||||
@@ -83,6 +84,21 @@ def scalar_interval(coarse, fine, bounds, slot=None):
|
|||||||
return result, current
|
return result, current
|
||||||
|
|
||||||
|
|
||||||
|
def whole_tiles(cfg, bounds):
|
||||||
|
"""One whole-level tile per level: (ny, nx) at the root, the fine window below."""
|
||||||
|
return [(cfg.ny, cfg.nx), implementation.FixedPatch2D.from_config(cfg, bounds).fine_shape]
|
||||||
|
|
||||||
|
|
||||||
|
def owner_arrays(backend):
|
||||||
|
"""Raw owner storage of every level as dense (9, ny, nx), before public masking."""
|
||||||
|
arrays = []
|
||||||
|
for level in backend._levels:
|
||||||
|
blocked = np.empty(level.layout.blocked_shape, np.float32)
|
||||||
|
cuda.memcpy_dtoh(blocked, level.current)
|
||||||
|
arrays.append(level.layout.to_dense(blocked))
|
||||||
|
return arrays
|
||||||
|
|
||||||
|
|
||||||
class FaultGuardsCPU(unittest.TestCase):
|
class FaultGuardsCPU(unittest.TestCase):
|
||||||
def test_boundary_mode_and_margin_contract(self):
|
def test_boundary_mode_and_margin_contract(self):
|
||||||
cfg = LBMConfig(nx=32, ny=24, viscosity=.1, velocity=0, wall_normal_velocity=.001,
|
cfg = LBMConfig(nx=32, ny=24, viscosity=.1, velocity=0, wall_normal_velocity=.001,
|
||||||
@@ -94,7 +110,8 @@ class FaultGuardsCPU(unittest.TestCase):
|
|||||||
("slot_reservoir", (12, 4, 20, 21))):
|
("slot_reservoir", (12, 4, 20, 21))):
|
||||||
with self.subTest(mode=mode, bounds=bounds):
|
with self.subTest(mode=mode, bounds=bounds):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
implementation.FixedPatchStepper(cfg, bounds, context=context, coarse_boundary=mode)
|
implementation.FixedPatchStepper(cfg, context=context, coarse_boundary=mode,
|
||||||
|
refinements=[bounds], level_blocks=[(1, 1)] * 2)
|
||||||
context.activate.assert_not_called()
|
context.activate.assert_not_called()
|
||||||
# Both exact channel limits and the original periodic two-cell margin
|
# Both exact channel limits and the original periodic two-cell margin
|
||||||
# must reach context activation, with no allocation or device required.
|
# must reach context activation, with no allocation or device required.
|
||||||
@@ -102,16 +119,20 @@ class FaultGuardsCPU(unittest.TestCase):
|
|||||||
("periodic", (2, 2, 6, 5))):
|
("periodic", (2, 2, 6, 5))):
|
||||||
with self.subTest(mode=mode, bounds=bounds):
|
with self.subTest(mode=mode, bounds=bounds):
|
||||||
with self.assertRaisesRegex(RuntimeError, "validated before CUDA"):
|
with self.assertRaisesRegex(RuntimeError, "validated before CUDA"):
|
||||||
implementation.FixedPatchStepper(cfg, bounds, context=context, coarse_boundary=mode)
|
implementation.FixedPatchStepper(cfg, context=context, coarse_boundary=mode,
|
||||||
|
refinements=[bounds], level_blocks=[(1, 1)] * 2)
|
||||||
|
|
||||||
def fake_backend(self):
|
def fake_backend(self):
|
||||||
backend = PeriodicPatchStepper.__new__(PeriodicPatchStepper)
|
backend = FixedPatchStepper.__new__(FixedPatchStepper)
|
||||||
backend.fine_layout = None # dense fine storage
|
layout = BlockLayout2D.build(1, 1, 1, 1, periodic=(False, False))
|
||||||
|
backend._layouts = [layout, layout] # blocked storage on both levels
|
||||||
|
backend._ddf_shifting = False # unshifted on every level, as _check_storage expects
|
||||||
|
backend._level_cfgs = [LBMConfig(nx=16, ny=16, viscosity=.1)] * 2 # ddf_shifting off
|
||||||
backend.ctx = Mock(activate=nullcontext)
|
backend.ctx = Mock(activate=nullcontext)
|
||||||
backend._lock = RLock()
|
backend._lock = RLock()
|
||||||
backend._status = "ready"
|
backend._status = "ready"
|
||||||
backend._stream = Mock()
|
backend._stream = Mock()
|
||||||
backend._levels = [Mock(shape=(9, 1, 1), layout=None), Mock(shape=(9, 1, 1), layout=None)]
|
backend._levels = [Mock(shape=(9, 1, 1), layout=layout), Mock(shape=(9, 1, 1), layout=layout)]
|
||||||
backend._allocations = [Mock(), Mock()]
|
backend._allocations = [Mock(), Mock()]
|
||||||
return backend
|
return backend
|
||||||
|
|
||||||
@@ -154,20 +175,35 @@ class FaultGuardsCPU(unittest.TestCase):
|
|||||||
context = Mock(activate=nullcontext, sm_arch="sm_70")
|
context = Mock(activate=nullcontext, sm_arch="sm_70")
|
||||||
with patch.object(implementation, "HALO", 2):
|
with patch.object(implementation, "HALO", 2):
|
||||||
with self.assertRaisesRegex(RuntimeError, "four-cell"):
|
with self.assertRaisesRegex(RuntimeError, "four-cell"):
|
||||||
PeriodicPatchStepper(cfg, (4, 4, 12, 12), context=context)
|
FixedPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)],
|
||||||
|
level_blocks=[(16, 16), (24, 24)])
|
||||||
p = implementation.FixedPatch2D.from_config(cfg, (4, 4, 12, 12))
|
p = implementation.FixedPatch2D.from_config(cfg, (4, 4, 12, 12))
|
||||||
with patch.object(cuda, "Stream"), patch.object(implementation.FixedPatch2D, "from_config",
|
from CelerisLab.lbm import hierarchy
|
||||||
return_value=replace(p, tau_coarse=.9)):
|
bad = patch.object(implementation.FixedPatch2D, "from_config",
|
||||||
with self.assertRaisesRegex(RuntimeError, "omega/tau"):
|
return_value=replace(p, tau_coarse=.9))
|
||||||
PeriodicPatchStepper(cfg, p.bounds, context=context)
|
# A pair tau inconsistent with its level config is rejected twice over, both before
|
||||||
|
# any device allocation or upload: first by the hierarchy chain check, and, if that
|
||||||
|
# is bypassed, by the per-level omega/tau check in _build_level.
|
||||||
|
with patch.object(cuda, "Stream"), patch.object(cuda, "mem_get_info", return_value=(1 << 40, 1 << 40)), bad, patch.object(cuda, "mem_alloc") as alloc, \
|
||||||
|
patch.object(cuda, "memcpy_htod") as upload:
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "config chain is inconsistent"):
|
||||||
|
FixedPatchStepper(cfg, context=context, refinements=[p.bounds],
|
||||||
|
level_blocks=[(16, 16), (24, 24)])
|
||||||
|
with patch.object(hierarchy, "_check_chain"), \
|
||||||
|
self.assertRaisesRegex(RuntimeError, "omega/tau"):
|
||||||
|
FixedPatchStepper(cfg, context=context, refinements=[p.bounds],
|
||||||
|
level_blocks=[(16, 16), (24, 24)])
|
||||||
|
alloc.assert_not_called()
|
||||||
|
upload.assert_not_called()
|
||||||
module = Mock()
|
module = Mock()
|
||||||
module.get_global.return_value = (Mock(), 16)
|
module.get_global.return_value = (Mock(), 16)
|
||||||
with patch.object(cuda, "Stream"), patch.object(compiler, "generate_config"), \
|
with patch.object(cuda, "Stream"), patch.object(cuda, "mem_get_info", return_value=(1 << 40, 1 << 40)), patch.object(compiler, "generate_config"), \
|
||||||
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
||||||
patch.object(compiler, "load_module", return_value=module), \
|
patch.object(compiler, "load_module", return_value=module), \
|
||||||
patch.object(cuda, "memcpy_htod") as upload:
|
patch.object(cuda, "memcpy_htod") as upload:
|
||||||
with self.assertRaisesRegex(RuntimeError, "layout mismatch"):
|
with self.assertRaisesRegex(RuntimeError, "layout mismatch"):
|
||||||
PeriodicPatchStepper(cfg, p.bounds, context=context)
|
FixedPatchStepper(cfg, context=context, refinements=[p.bounds],
|
||||||
|
level_blocks=[(16, 16), (24, 24)])
|
||||||
upload.assert_not_called()
|
upload.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@@ -235,23 +271,27 @@ class NormalBuildGPU(unittest.TestCase):
|
|||||||
parts = line.split()
|
parts = line.split()
|
||||||
if len(parts) == 3 and parts[0] == "#define":
|
if len(parts) == 3 and parts[0] == "#define":
|
||||||
definitions[parts[1]] = parts[2]
|
definitions[parts[1]] = parts[2]
|
||||||
generated.append((metadata["config"], definitions))
|
generated.append((metadata["config"], definitions, metadata))
|
||||||
return build
|
return build
|
||||||
|
|
||||||
max_error = max_bc_error = 0.0
|
max_error = max_bc_error = 0.0
|
||||||
with CudaContext(0) as context:
|
with CudaContext(0) as context:
|
||||||
with patch.object(compiler, "generate_config", side_effect=record_config):
|
with patch.object(compiler, "generate_config", side_effect=record_config):
|
||||||
backend = implementation.FixedPatchStepper(cfg, bounds, context=context,
|
backend = implementation.FixedPatchStepper(cfg, context=context, refinements=[bounds],
|
||||||
coarse_boundary="slot_reservoir")
|
coarse_boundary="slot_reservoir",
|
||||||
|
level_blocks=whole_tiles(cfg, bounds))
|
||||||
with backend:
|
with backend:
|
||||||
self.assertEqual(len(generated), 2)
|
self.assertEqual(len(generated), 2)
|
||||||
fine_config, fine_defines = generated[1]
|
fine_config, fine_defines, _ = generated[1]
|
||||||
self.assertEqual((fine_config["nx"], fine_config["ny"]), (fx, fy))
|
# Blocked kernels see one tile window including the 1-cell halo.
|
||||||
|
self.assertEqual((fine_config["nx"], fine_config["ny"]), (fx+2, fy+2))
|
||||||
for name in ("Y_WALL_BC", "TOP_WALL_BC", "WALL_SLOT_WIDTH",
|
for name in ("Y_WALL_BC", "TOP_WALL_BC", "WALL_SLOT_WIDTH",
|
||||||
"WALL_NORMAL_VELOCITY", "TOP_WALL_NORMAL_VELOCITY"):
|
"WALL_NORMAL_VELOCITY", "TOP_WALL_NORMAL_VELOCITY"):
|
||||||
self.assertEqual(float(fine_defines[name].removesuffix("f")), 0, name)
|
self.assertEqual(float(fine_defines[name].removesuffix("f")), 0, name)
|
||||||
self.assertEqual(int(generated[0][1]["Y_WALL_BC"]), 2)
|
# The root's physical slot/reservoir walls come from BLOCK_ROOT_BC=1
|
||||||
self.assertEqual(int(generated[0][1]["TOP_WALL_BC"]), 1)
|
# (module constant d_block_wall); the fine level has none.
|
||||||
|
root_meta, fine_meta = generated[0][2], generated[1][2] # read before cleanup
|
||||||
|
self.assertEqual((root_meta["block_root_bc"], fine_meta["block_root_bc"]), (1, 0))
|
||||||
# Independently derive all target links, including the eight
|
# Independently derive all target links, including the eight
|
||||||
# whose coarse sources are uncovered, from physical midpoints.
|
# whose coarse sources are uncovered, from physical midpoints.
|
||||||
targets, corners = set(), set()
|
targets, corners = set(), set()
|
||||||
@@ -271,9 +311,7 @@ class NormalBuildGPU(unittest.TestCase):
|
|||||||
expected = scalar_interval(*expected, bounds, slot=(amplitude, width))
|
expected = scalar_interval(*expected, bounds, slot=(amplitude, width))
|
||||||
backend.step()
|
backend.step()
|
||||||
self.assertEqual(backend.step_count, step)
|
self.assertEqual(backend.step_count, step)
|
||||||
raw = [np.empty(a.shape, np.float32) for a in initial]
|
raw = owner_arrays(backend)
|
||||||
for level, array in zip(backend._levels, raw):
|
|
||||||
cuda.memcpy_dtoh(array, level.current)
|
|
||||||
# Read raw storage before masking: active NaNs cannot be
|
# Read raw storage before masking: active NaNs cannot be
|
||||||
# hidden by the public download's non-leaf cleanup.
|
# hidden by the public download's non-leaf cleanup.
|
||||||
self.assertTrue(np.isfinite(raw[0][:, coarse_leaf]).all())
|
self.assertTrue(np.isfinite(raw[0][:, coarse_leaf]).all())
|
||||||
@@ -310,8 +348,9 @@ class NormalBuildGPU(unittest.TestCase):
|
|||||||
state = ref.State(*(a * (1 + 1e-3*rng.standard_normal(a.shape))
|
state = ref.State(*(a * (1 + 1e-3*rng.standard_normal(a.shape))
|
||||||
for a in (initial.coarse_f, initial.fine_f)))
|
for a in (initial.coarse_f, initial.fine_f)))
|
||||||
max_pop = max_drift = max_moment = 0.0
|
max_pop = max_drift = max_moment = 0.0
|
||||||
with CudaContext(0) as context, PeriodicPatchStepper(
|
with CudaContext(0) as context, FixedPatchStepper(
|
||||||
LBMConfig(nx=16, ny=16, viscosity=.1), (4, 4, 12, 12), context=context) as backend:
|
LBMConfig(nx=16, ny=16, viscosity=.1), context=context,
|
||||||
|
refinements=[(4, 4, 12, 12)], level_blocks=[(16, 16), (24, 24)]) as backend:
|
||||||
backend.initialize(*postcollision(state, spec, maps))
|
backend.initialize(*postcollision(state, spec, maps))
|
||||||
before = totals(backend.download(), maps)
|
before = totals(backend.download(), maps)
|
||||||
for step in range(1, 9):
|
for step in range(1, 9):
|
||||||
@@ -342,17 +381,17 @@ class NormalBuildGPU(unittest.TestCase):
|
|||||||
fine_mask[4:-4, 4:-4] = True
|
fine_mask[4:-4, 4:-4] = True
|
||||||
initial[1][:, ~fine_mask] = np.nan
|
initial[1][:, ~fine_mask] = np.nan
|
||||||
expected = scalar_interval(*[a.astype(np.float64) for a in initial], bounds)
|
expected = scalar_interval(*[a.astype(np.float64) for a in initial], bounds)
|
||||||
with CudaContext(0) as context, PeriodicPatchStepper(cfg, bounds, context=context) as backend:
|
with CudaContext(0) as context, FixedPatchStepper(cfg, context=context, refinements=[bounds],
|
||||||
|
level_blocks=[(13, 17), (14, 16)]) as backend:
|
||||||
self.assertEqual(backend.patch.fine_shape, (14, 16))
|
self.assertEqual(backend.patch.fine_shape, (14, 16))
|
||||||
p = backend.patch
|
p = backend.patch
|
||||||
allocated = 2 * 9 * (17*13+16*14) * 4 + sum(a.nbytes for a in
|
allocated = 2 * 4 * sum(int(np.prod(level.layout.blocked_shape)) for level in backend._levels) \
|
||||||
(p.ghost_fine, p.ghost_coarse, p.coalesce_coarse, p.coalesce_fine))
|
+ sum(a.nbytes for a in (p.ghost_fine, p.ghost_coarse, p.coalesce_coarse, p.coalesce_fine))
|
||||||
self.assertLess(allocated, 128*1024)
|
self.assertLess(allocated, 128*1024)
|
||||||
backend.initialize(*initial)
|
backend.initialize(*initial)
|
||||||
backend.step()
|
backend.step()
|
||||||
coarse, fine = backend.download()
|
coarse, fine = backend.download()
|
||||||
raw_fine = np.empty((9, 14, 16), np.float32)
|
raw_fine = owner_arrays(backend)[1]
|
||||||
cuda.memcpy_dtoh(raw_fine, backend._levels[1].current)
|
|
||||||
max_error = 0.0
|
max_error = 0.0
|
||||||
for got, want in zip((coarse, raw_fine), expected):
|
for got, want in zip((coarse, raw_fine), expected):
|
||||||
np.testing.assert_array_equal(np.isnan(got), np.isnan(want))
|
np.testing.assert_array_equal(np.isnan(got), np.isnan(want))
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import pycuda.driver as cuda
|
|||||||
|
|
||||||
import CelerisLab
|
import CelerisLab
|
||||||
from CelerisLab.config import LBMConfig
|
from CelerisLab.config import LBMConfig
|
||||||
from CelerisLab.cuda import compiler_v2 as compiler
|
from CelerisLab.cuda import compiler
|
||||||
from CelerisLab.cuda.context import CudaContext
|
from CelerisLab.cuda.context import CudaContext
|
||||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper, PeriodicPatchStepper
|
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[3]
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
POP_ATOL = 3e-6
|
POP_ATOL = 3e-6
|
||||||
@@ -52,12 +52,11 @@ def totals(arrays, maps):
|
|||||||
return np.array([populations.sum(), populations @ ref.CX, populations @ ref.CY])
|
return np.array([populations.sum(), populations @ ref.CX, populations @ ref.CY])
|
||||||
|
|
||||||
|
|
||||||
def global_array(module, name, shape):
|
def level_storage(level):
|
||||||
out = np.empty(shape, dtype=np.uint32)
|
"""Raw device storage of one blocked level as dense (9, ny, nx)."""
|
||||||
ptr, size = module.get_global(name)
|
blocked = np.empty(level.layout.blocked_shape, np.float32)
|
||||||
assert size == out.nbytes
|
cuda.memcpy_dtoh(blocked, level.current)
|
||||||
cuda.memcpy_dtoh(out, ptr)
|
return level.layout.to_dense(blocked)
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
class CoupledGPU(unittest.TestCase):
|
class CoupledGPU(unittest.TestCase):
|
||||||
@@ -82,15 +81,15 @@ class CoupledGPU(unittest.TestCase):
|
|||||||
expected = postcollision(next_state, spec, maps)
|
expected = postcollision(next_state, spec, maps)
|
||||||
arrivals = (next_state.coarse_f, next_state.fine_f)
|
arrivals = (next_state.coarse_f, next_state.fine_f)
|
||||||
|
|
||||||
cases = (("dense_unshifted", False, {}),
|
cases = (("blocked_unshifted", False, dict(level_blocks=[(16, 16), (24, 24)])),
|
||||||
("blocked_shifted", True,
|
("blocked_shifted", True,
|
||||||
dict(base_block=(4, 4), fine_block=(6, 6))))
|
dict(level_blocks=[(4, 4), (6, 6)])))
|
||||||
with CudaContext(0) as context:
|
with CudaContext(0) as context:
|
||||||
for label, shifted, storage in cases:
|
for label, shifted, storage in cases:
|
||||||
with self.subTest(storage=label):
|
with self.subTest(storage=label):
|
||||||
cfg = LBMConfig(nx=16, ny=16, viscosity=.0192,
|
cfg = LBMConfig(nx=16, ny=16, viscosity=.0192,
|
||||||
ddf_shifting=shifted)
|
ddf_shifting=shifted)
|
||||||
with FixedPatchStepper(cfg, (4, 4, 12, 12), context=context,
|
with FixedPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)],
|
||||||
**storage) as backend:
|
**storage) as backend:
|
||||||
self.assertAlmostEqual(backend.patch.tau_coarse, spec.tau_coarse)
|
self.assertAlmostEqual(backend.patch.tau_coarse, spec.tau_coarse)
|
||||||
self.assertAlmostEqual(backend.patch.tau_fine, spec.tau_fine)
|
self.assertAlmostEqual(backend.patch.tau_fine, spec.tau_fine)
|
||||||
@@ -125,7 +124,7 @@ class CoupledGPU(unittest.TestCase):
|
|||||||
"wrong-phase controls PASS", flush=True)
|
"wrong-phase controls PASS", flush=True)
|
||||||
|
|
||||||
def test_cleanup_attempts_every_allocation(self):
|
def test_cleanup_attempts_every_allocation(self):
|
||||||
backend = PeriodicPatchStepper.__new__(PeriodicPatchStepper)
|
backend = FixedPatchStepper.__new__(FixedPatchStepper)
|
||||||
backend.ctx = Mock(activate=nullcontext)
|
backend.ctx = Mock(activate=nullcontext)
|
||||||
backend._lock = RLock()
|
backend._lock = RLock()
|
||||||
backend._status = "failed"
|
backend._status = "failed"
|
||||||
@@ -147,8 +146,9 @@ class CoupledGPU(unittest.TestCase):
|
|||||||
spec = ref.make_spec(collision="srt")
|
spec = ref.make_spec(collision="srt")
|
||||||
maps = ref.build_maps(spec)
|
maps = ref.build_maps(spec)
|
||||||
with CudaContext(0) as context:
|
with CudaContext(0) as context:
|
||||||
with PeriodicPatchStepper(LBMConfig(nx=16, ny=16, viscosity=.1),
|
with FixedPatchStepper(LBMConfig(nx=16, ny=16, viscosity=.1),
|
||||||
(4, 4, 12, 12), context=context) as backend:
|
context=context, refinements=[(4, 4, 12, 12)],
|
||||||
|
level_blocks=[(16, 16), (24, 24)]) as backend:
|
||||||
backend.initialize(*postcollision(ref.make_state(spec), spec, maps))
|
backend.initialize(*postcollision(ref.make_state(spec), spec, maps))
|
||||||
coarse, fine = backend._levels
|
coarse, fine = backend._levels
|
||||||
|
|
||||||
@@ -164,122 +164,75 @@ class CoupledGPU(unittest.TestCase):
|
|||||||
for operation in (backend.download, backend.step):
|
for operation in (backend.download, backend.step):
|
||||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||||
operation()
|
operation()
|
||||||
raw = np.empty(fine.shape, np.float32)
|
raw = level_storage(fine)
|
||||||
cuda.memcpy_dtoh(raw, fine.current)
|
|
||||||
self.assertTrue(np.isfinite(raw.ravel()[backend.patch.coalesce_fine]).all())
|
self.assertTrue(np.isfinite(raw.ravel()[backend.patch.coalesce_fine]).all())
|
||||||
self.assertFalse(context.is_closed)
|
self.assertFalse(context.is_closed)
|
||||||
print("partial failure: two fine hops completed, no coarse step published; cleanup PASS", flush=True)
|
print("partial failure: two fine hops completed, no coarse step published; cleanup PASS", flush=True)
|
||||||
|
|
||||||
def test_bounded_equivalence_and_ownership(self):
|
def eight_steps(self, backend, spec, maps):
|
||||||
self.assertEqual(Path(CelerisLab.__file__).resolve(), ROOT / "src/CelerisLab/__init__.py")
|
"""Rest and seeded nonequilibrium, 8 steps against the scalar reference."""
|
||||||
self.assertEqual(Path(compiler._KERNEL_DIR).resolve(), ROOT / "src/CelerisLab/lbm/kernels")
|
p = backend.patch
|
||||||
|
for case in ("rest", "nonequilibrium_seed1"):
|
||||||
|
with self.subTest(case=case):
|
||||||
|
state = ref.make_state(spec, velocity=(0, 0) if case == "rest" else (.03, -.02))
|
||||||
|
if case != "rest":
|
||||||
|
rng = np.random.default_rng(1)
|
||||||
|
state = ref.State(*(a * (1 + 1e-3 * rng.standard_normal(a.shape))
|
||||||
|
for a in (state.coarse_f, state.fine_f)))
|
||||||
|
initial = postcollision(state, spec, maps)
|
||||||
|
backend.initialize(*initial)
|
||||||
|
before = totals(backend.download(), maps)
|
||||||
|
max_error = max_drift = max_moment_error = max_corner = 0.0
|
||||||
|
for step in range(1, 9):
|
||||||
|
state, diagnostic = ref.advance_coarse_once(spec, maps, state)
|
||||||
|
backend.step()
|
||||||
|
actual = backend.download()
|
||||||
|
expected = postcollision(state, spec, maps)
|
||||||
|
self.assertEqual(backend.step_count, step)
|
||||||
|
p.check_synchronization(step, state.tick, phase="postcollision_g")
|
||||||
|
for got, want in zip(actual, expected):
|
||||||
|
np.testing.assert_array_equal(np.isnan(got), np.isnan(want))
|
||||||
|
error = float(np.nanmax(np.abs(got - want)))
|
||||||
|
self.assertLessEqual(error, POP_ATOL)
|
||||||
|
max_error = max(max_error, error)
|
||||||
|
drift = float(np.max(np.abs(totals(actual, maps) - before)))
|
||||||
|
moment_error = float(np.max(np.abs(totals(actual, maps) - totals(expected, maps))))
|
||||||
|
self.assertLessEqual(drift, MOMENT_ATOL)
|
||||||
|
self.assertLessEqual(moment_error, MOMENT_ATOL)
|
||||||
|
max_drift, max_moment_error = max(max_drift, drift), max(max_moment_error, moment_error)
|
||||||
|
f = backend._levels[1]
|
||||||
|
raw = level_storage(f)
|
||||||
|
np.testing.assert_array_equal(np.isfinite(raw), diagnostic["hop2_valid"])
|
||||||
|
self.assertTrue(np.isfinite(raw.ravel()[p.coalesce_fine]).all())
|
||||||
|
self.assertEqual(len(diagnostic["corner_packets"]), 8)
|
||||||
|
for packet in diagnostic["corner_packets"].values():
|
||||||
|
sx, sy = packet["source_coarse_xy"]
|
||||||
|
self.assertTrue(maps.coarse_active[sy, sx])
|
||||||
|
values = [raw[packet["direction"], y, x]
|
||||||
|
for y, x in packet["children_fine_yx"]]
|
||||||
|
error = float(np.max(np.abs(np.array(values) - packet["packets"])))
|
||||||
|
self.assertLessEqual(error, POP_ATOL)
|
||||||
|
max_corner = max(max_corner, error)
|
||||||
|
print(f"{case}: steps=8 max_population={max_error:.9g} "
|
||||||
|
f"max_leaf_drift={max_drift:.9g} max_leaf_error={max_moment_error:.9g} "
|
||||||
|
f"max_corner_packet={max_corner:.9g}; PASS", flush=True)
|
||||||
|
|
||||||
|
def test_bounded_equivalence_blocked(self):
|
||||||
|
"""The same eight-step reference and launch-failure checks on whole-level tiles."""
|
||||||
spec = ref.make_spec(collision="srt")
|
spec = ref.make_spec(collision="srt")
|
||||||
maps = ref.build_maps(spec)
|
maps = ref.build_maps(spec)
|
||||||
cfg = LBMConfig(nx=16, ny=16, viscosity=.1)
|
cfg = LBMConfig(nx=16, ny=16, viscosity=.1)
|
||||||
compile_original = compiler.compile_kernel
|
|
||||||
|
|
||||||
def audit_compile(*args, **kwargs):
|
|
||||||
# Only this private build's entry differs. Production source unchanged.
|
|
||||||
entry = Path(kwargs["build_dir"]) / "refinement_audit.cu"
|
|
||||||
entry.write_text('#define CELERIS_REFINEMENT_AUDIT\n#include "refinement.cu"\n')
|
|
||||||
kwargs["source"] = entry.name
|
|
||||||
return compile_original(*args, **kwargs)
|
|
||||||
|
|
||||||
with CudaContext(0) as context:
|
with CudaContext(0) as context:
|
||||||
with patch.object(compiler, "compile_kernel", audit_compile):
|
with FixedPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)],
|
||||||
backend = PeriodicPatchStepper(cfg, (4, 4, 12, 12), context=context)
|
level_blocks=[(16, 16), (24, 24)]) as backend:
|
||||||
with backend:
|
self.eight_steps(backend, spec, maps)
|
||||||
# Production allocations: four DDF arrays and four compact tables.
|
|
||||||
device_bytes = 2 * 9 * (16**2 + 24**2) * 4
|
|
||||||
p = backend.patch
|
|
||||||
device_bytes += sum(a.nbytes for a in (p.ghost_fine, p.ghost_coarse,
|
|
||||||
p.coalesce_coarse, p.coalesce_fine))
|
|
||||||
audit_bytes = sum((2 + 9) * np.prod(level.shape[1:]) * 4
|
|
||||||
for level in backend._levels)
|
|
||||||
self.assertLess(device_bytes + audit_bytes, 128 * 1024)
|
|
||||||
print(f"device_arrays={device_bytes} audit_arrays={audit_bytes} bytes", flush=True)
|
|
||||||
for case in ("rest", "nonequilibrium_seed1"):
|
|
||||||
with self.subTest(case=case):
|
|
||||||
state = ref.make_state(spec, velocity=(0, 0) if case == "rest" else (.03, -.02))
|
|
||||||
if case != "rest":
|
|
||||||
rng = np.random.default_rng(1)
|
|
||||||
state = ref.State(*(a * (1 + 1e-3 * rng.standard_normal(a.shape))
|
|
||||||
for a in (state.coarse_f, state.fine_f)))
|
|
||||||
initial = postcollision(state, spec, maps)
|
|
||||||
backend.initialize(*initial)
|
|
||||||
for level in backend._levels:
|
|
||||||
for name in ("collision_counts", "explosion_counts", "incoming_counts"):
|
|
||||||
ptr, size = level.module.get_global(name)
|
|
||||||
cuda.memset_d8(ptr, 0, size)
|
|
||||||
before = totals(backend.download(), maps)
|
|
||||||
max_error = max_drift = max_moment_error = max_corner = 0.0
|
|
||||||
for step in range(1, 9):
|
|
||||||
state, diagnostic = ref.advance_coarse_once(spec, maps, state)
|
|
||||||
backend.step()
|
|
||||||
actual = backend.download()
|
|
||||||
expected = postcollision(state, spec, maps)
|
|
||||||
self.assertEqual(backend.step_count, step)
|
|
||||||
p.check_synchronization(step, state.tick, phase="postcollision_g")
|
|
||||||
for got, want in zip(actual, expected):
|
|
||||||
np.testing.assert_array_equal(np.isnan(got), np.isnan(want))
|
|
||||||
error = float(np.nanmax(np.abs(got - want)))
|
|
||||||
self.assertLessEqual(error, POP_ATOL)
|
|
||||||
max_error = max(max_error, error)
|
|
||||||
drift = float(np.max(np.abs(totals(actual, maps) - before)))
|
|
||||||
moment_error = float(np.max(np.abs(totals(actual, maps) - totals(expected, maps))))
|
|
||||||
self.assertLessEqual(drift, MOMENT_ATOL)
|
|
||||||
self.assertLessEqual(moment_error, MOMENT_ATOL)
|
|
||||||
max_drift, max_moment_error = max(max_drift, drift), max(max_moment_error, moment_error)
|
|
||||||
c, f = backend._levels
|
|
||||||
np.testing.assert_array_equal(global_array(c.module, "collision_counts", (16, 16)),
|
|
||||||
step * maps.coarse_active)
|
|
||||||
np.testing.assert_array_equal(global_array(f.module, "collision_counts", (24, 24)),
|
|
||||||
2 * step * maps.fine_active)
|
|
||||||
np.testing.assert_array_equal(global_array(f.module, "explosion_counts", (24, 24)),
|
|
||||||
step * ~maps.fine_active)
|
|
||||||
owners = np.zeros((9, 16, 16), np.uint32)
|
|
||||||
owners.ravel()[p.coalesce_coarse] = step
|
|
||||||
np.testing.assert_array_equal(global_array(c.module, "incoming_counts", owners.shape), owners)
|
|
||||||
raw = np.empty(f.shape, np.float32)
|
|
||||||
cuda.memcpy_dtoh(raw, f.current)
|
|
||||||
np.testing.assert_array_equal(np.isfinite(raw), diagnostic["hop2_valid"])
|
|
||||||
self.assertTrue(np.isfinite(raw.ravel()[p.coalesce_fine]).all())
|
|
||||||
self.assertEqual(len(diagnostic["corner_packets"]), 8)
|
|
||||||
for packet in diagnostic["corner_packets"].values():
|
|
||||||
sx, sy = packet["source_coarse_xy"]
|
|
||||||
self.assertTrue(maps.coarse_active[sy, sx])
|
|
||||||
values = [raw[packet["direction"], y, x]
|
|
||||||
for y, x in packet["children_fine_yx"]]
|
|
||||||
error = float(np.max(np.abs(np.array(values) - packet["packets"])))
|
|
||||||
self.assertLessEqual(error, POP_ATOL)
|
|
||||||
max_corner = max(max_corner, error)
|
|
||||||
print(f"{case}: steps=8 max_population={max_error:.9g} "
|
|
||||||
f"max_leaf_drift={max_drift:.9g} max_leaf_error={max_moment_error:.9g} "
|
|
||||||
f"max_corner_packet={max_corner:.9g}; ownership/counts PASS", flush=True)
|
|
||||||
|
|
||||||
# Fail before any additional time step; no stale synchronized export.
|
|
||||||
with patch.object(backend._pairs[0], "explode", side_effect=RuntimeError("injected launch failure")):
|
with patch.object(backend._pairs[0], "explode", side_effect=RuntimeError("injected launch failure")):
|
||||||
with self.assertRaisesRegex(RuntimeError, "injected"):
|
with self.assertRaisesRegex(RuntimeError, "injected"):
|
||||||
backend.step()
|
backend.step()
|
||||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
for operation in (backend.download, backend.step):
|
||||||
backend.download()
|
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
operation()
|
||||||
backend.step()
|
|
||||||
self.assertFalse(context.is_closed)
|
self.assertFalse(context.is_closed)
|
||||||
with self.assertRaisesRegex(RuntimeError, "closed"):
|
|
||||||
backend.download()
|
|
||||||
|
|
||||||
# Compile and execute the actual non-audited entry as well: one step.
|
|
||||||
with PeriodicPatchStepper(cfg, (4, 4, 12, 12), context=context) as plain:
|
|
||||||
rest = postcollision(ref.make_state(spec), spec, maps)
|
|
||||||
plain.initialize(*rest)
|
|
||||||
plain.step()
|
|
||||||
for got, expected in zip(plain.download(), rest):
|
|
||||||
np.testing.assert_allclose(got, expected, rtol=0, atol=POP_ATOL, equal_nan=True)
|
|
||||||
print("normal-build rest: steps=1 PASS; source/kernel paths pinned", flush=True)
|
|
||||||
rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
|
||||||
print(f"host_peak_rss={rss:.3f} MiB", flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main(verbosity=2)
|
unittest.main(verbosity=2)
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
Jets use the same coarse cell-edge frame as fine_objects. The equivalent jet
|
Jets use the same coarse cell-edge frame as fine_objects. The equivalent jet
|
||||||
of Astra's actuated fixture is SurfaceActuation(0.7, 0.85, U).as_jet((16, 12),
|
of Astra's actuated fixture is SurfaceActuation(0.7, 0.85, U).as_jet((16, 12),
|
||||||
2.15). Frozen GPU gates (8 coarse steps, both coarse boundary modes):
|
2.15). Frozen GPU gates (8 coarse steps, both coarse boundary modes):
|
||||||
G1 jet run: dense vs 1x1/2x2/4x2 fine blocks bit-identical (values + NaN masks);
|
G1 jet run: 1x1/2x2/4x2 fine blocks (blocked (12, 8) root) bit-identical to the
|
||||||
G2 jet vs legacy actuation (dense): max |dG| over finite fine ACTIVE fluid
|
FINE_BLOCKS[0] reference (values + NaN masks);
|
||||||
|
G2 jet vs legacy actuation (blocked reference): max |dG| over finite fine ACTIVE fluid
|
||||||
populations <= 1e-6 (velocity rows differ only by FP32 rounding; reported);
|
populations <= 1e-6 (velocity rows differ only by FP32 rounding; reported);
|
||||||
G3 amplitude change after 4 steps (+U -> -U), 4 more steps: dense vs blocked
|
G3 amplitude change after 4 steps (+U -> -U), 4 more steps: blocked layouts
|
||||||
bit-identical and the run differs from the unchanged run.
|
bit-identical to the FINE_BLOCKS[0] reference and the run differs from the
|
||||||
|
unchanged run.
|
||||||
"""
|
"""
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
import unittest
|
import unittest
|
||||||
@@ -16,7 +18,7 @@ from unittest.mock import Mock, patch
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pycuda.driver as cuda
|
import pycuda.driver as cuda
|
||||||
|
|
||||||
from CelerisLab.cuda import compiler_v2 as compiler
|
from CelerisLab.cuda import compiler
|
||||||
from CelerisLab.lbm.descriptors import SOLID
|
from CelerisLab.lbm.descriptors import SOLID
|
||||||
from CelerisLab.lbm.refinement import ACTIVE, FixedPatch2D, prepare_fine_boundary
|
from CelerisLab.lbm.refinement import ACTIVE, FixedPatch2D, prepare_fine_boundary
|
||||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||||
@@ -26,6 +28,11 @@ from test_refinement_blocked_fine import (BOUNDS, FINE_BLOCKS, NX, NY, STEPS, ac
|
|||||||
SPEED = .002
|
SPEED = .002
|
||||||
|
|
||||||
|
|
||||||
|
def root_and(fine_block):
|
||||||
|
"""Blocked levels with a (12, 8) root tile."""
|
||||||
|
return [(12, 8), fine_block]
|
||||||
|
|
||||||
|
|
||||||
def jet(speed=SPEED):
|
def jet(speed=SPEED):
|
||||||
return actuation(speed).as_jet((16.0, 12.0), 2.15)
|
return actuation(speed).as_jet((16.0, 12.0), 2.15)
|
||||||
|
|
||||||
@@ -36,13 +43,14 @@ def mocked(objects, jets, fine_block):
|
|||||||
module = Mock(get_function=Mock(return_value=Mock()))
|
module = Mock(get_function=Mock(return_value=Mock()))
|
||||||
module.get_global.return_value = (Mock(), 20)
|
module.get_global.return_value = (Mock(), 20)
|
||||||
htod = Mock()
|
htod = Mock()
|
||||||
with patch.object(cuda, "Stream"), patch.object(compiler, "generate_config"), \
|
with patch.object(cuda, "Stream"), patch.object(cuda, "mem_get_info", return_value=(1 << 40, 1 << 40)), patch.object(compiler, "generate_config"), \
|
||||||
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
||||||
patch.object(compiler, "load_module", return_value=module), \
|
patch.object(compiler, "load_module", return_value=module), \
|
||||||
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
|
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
|
||||||
patch.object(cuda, "memcpy_htod", htod):
|
patch.object(cuda, "memcpy_htod", htod):
|
||||||
backend = FixedPatchStepper(config("periodic"), BOUNDS, context=context,
|
backend = FixedPatchStepper(config("periodic"), context=context, refinements=[BOUNDS],
|
||||||
fine_objects=objects, fine_block=fine_block, jets=jets)
|
level_blocks=root_and(fine_block), level_objects=[(), tuple(objects)],
|
||||||
|
level_jets=[(), tuple(jets)])
|
||||||
return backend, htod
|
return backend, htod
|
||||||
|
|
||||||
|
|
||||||
@@ -99,7 +107,7 @@ class JetStepperCPU(unittest.TestCase):
|
|||||||
def test_blocked_amplitude_update_rewrites_only_velocity_column(self):
|
def test_blocked_amplitude_update_rewrites_only_velocity_column(self):
|
||||||
backend, htod = mocked([circle()], [jet()], FINE_BLOCKS[1])
|
backend, htod = mocked([circle()], [jet()], FINE_BLOCKS[1])
|
||||||
before = backend._fine_tables["surface_velocity"].copy()
|
before = backend._fine_tables["surface_velocity"].copy()
|
||||||
device = backend._link_tables["surface_velocity"]
|
device = backend._level_boundaries[1].device["surface_velocity"]
|
||||||
with patch.object(cuda, "memcpy_htod", htod):
|
with patch.object(cuda, "memcpy_htod", htod):
|
||||||
htod.reset_mock()
|
htod.reset_mock()
|
||||||
backend.set_jet_amplitude(0, -SPEED)
|
backend.set_jet_amplitude(0, -SPEED)
|
||||||
@@ -115,7 +123,7 @@ class JetStepperCPU(unittest.TestCase):
|
|||||||
backend.close()
|
backend.close()
|
||||||
|
|
||||||
def test_copy_failure_marks_failed_and_keeps_host_amplitude(self):
|
def test_copy_failure_marks_failed_and_keeps_host_amplitude(self):
|
||||||
for fine_block in (None, FINE_BLOCKS[1]): # dense SoA and blocked tables
|
for fine_block in (FINE_BLOCKS[1],): # blocked tables
|
||||||
backend, _ = mocked([circle()], [jet()], fine_block)
|
backend, _ = mocked([circle()], [jet()], fine_block)
|
||||||
status = backend._status
|
status = backend._status
|
||||||
for bad, error in ((1.5, ValueError), (float("nan"), ValueError)):
|
for bad, error in ((1.5, ValueError), (float("nan"), ValueError)):
|
||||||
@@ -125,9 +133,7 @@ class JetStepperCPU(unittest.TestCase):
|
|||||||
with self.assertRaises(IndexError):
|
with self.assertRaises(IndexError):
|
||||||
backend.set_jet_amplitude(1, -SPEED)
|
backend.set_jet_amplitude(1, -SPEED)
|
||||||
self.assertEqual(backend._status, status)
|
self.assertEqual(backend._status, status)
|
||||||
dense = backend._fine_curved
|
before = backend._fine_tables["surface_velocity"].copy()
|
||||||
before = (dense.surface_velocity if dense is not None
|
|
||||||
else backend._fine_tables["surface_velocity"]).copy()
|
|
||||||
boom = Mock(side_effect=RuntimeError("copy failed"))
|
boom = Mock(side_effect=RuntimeError("copy failed"))
|
||||||
with patch.object(cuda, "memcpy_htod", boom), patch.object(cuda, "memcpy_htod_async", boom), \
|
with patch.object(cuda, "memcpy_htod", boom), patch.object(cuda, "memcpy_htod_async", boom), \
|
||||||
patch.object(cuda, "pagelocked_empty", side_effect=lambda shape, dtype: np.empty(shape, dtype)):
|
patch.object(cuda, "pagelocked_empty", side_effect=lambda shape, dtype: np.empty(shape, dtype)):
|
||||||
@@ -135,11 +141,7 @@ class JetStepperCPU(unittest.TestCase):
|
|||||||
backend.set_jet_amplitude(0, -SPEED)
|
backend.set_jet_amplitude(0, -SPEED)
|
||||||
self.assertEqual(backend._status, "failed", fine_block)
|
self.assertEqual(backend._status, "failed", fine_block)
|
||||||
self.assertEqual(backend._jets[0].amplitude, SPEED)
|
self.assertEqual(backend._jets[0].amplitude, SPEED)
|
||||||
if dense is not None:
|
np.testing.assert_array_equal(backend._fine_tables["surface_velocity"], before)
|
||||||
self.assertEqual(dense.jets[0].amplitude, SPEED)
|
|
||||||
np.testing.assert_array_equal(dense.surface_velocity, before)
|
|
||||||
else:
|
|
||||||
np.testing.assert_array_equal(backend._fine_tables["surface_velocity"], before)
|
|
||||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||||
backend.step(1)
|
backend.step(1)
|
||||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||||
@@ -149,8 +151,10 @@ class JetStepperCPU(unittest.TestCase):
|
|||||||
def test_jet_overlapping_legacy_aperture_rejected_before_cuda(self):
|
def test_jet_overlapping_legacy_aperture_rejected_before_cuda(self):
|
||||||
with patch.object(cuda, "Stream", side_effect=AssertionError("CUDA touched")):
|
with patch.object(cuda, "Stream", side_effect=AssertionError("CUDA touched")):
|
||||||
with self.assertRaisesRegex(ValueError, "SurfaceActuation"):
|
with self.assertRaisesRegex(ValueError, "SurfaceActuation"):
|
||||||
FixedPatchStepper(config("periodic"), BOUNDS, context=Mock(),
|
FixedPatchStepper(config("periodic"), context=Mock(), refinements=[BOUNDS],
|
||||||
fine_objects=[circle(actuation(SPEED))], jets=[jet()])
|
level_blocks=root_and(FINE_BLOCKS[1]),
|
||||||
|
level_objects=[(), (circle(actuation(SPEED)),)],
|
||||||
|
level_jets=[(), (jet(),)])
|
||||||
|
|
||||||
|
|
||||||
def run(mode, fine_block, *, objects, jets, switch=None):
|
def run(mode, fine_block, *, objects, jets, switch=None):
|
||||||
@@ -158,8 +162,9 @@ def run(mode, fine_block, *, objects, jets, switch=None):
|
|||||||
cfg = config(mode)
|
cfg = config(mode)
|
||||||
patch_ = FixedPatch2D.from_config(cfg, BOUNDS)
|
patch_ = FixedPatch2D.from_config(cfg, BOUNDS)
|
||||||
with CudaContext(0) as context, FixedPatchStepper(
|
with CudaContext(0) as context, FixedPatchStepper(
|
||||||
cfg, BOUNDS, context=context, coarse_boundary=mode, fine_objects=objects,
|
cfg, context=context, coarse_boundary=mode, refinements=[BOUNDS],
|
||||||
fine_block=fine_block, jets=jets) as backend:
|
level_blocks=root_and(fine_block), level_objects=[(), tuple(objects)],
|
||||||
|
level_jets=[(), tuple(jets)]) as backend:
|
||||||
backend.initialize(state(NY, NX, 1), state(*patch_.fine_shape, 2))
|
backend.initialize(state(NY, NX, 1), state(*patch_.fine_shape, 2))
|
||||||
if switch is None:
|
if switch is None:
|
||||||
backend.step(STEPS)
|
backend.step(STEPS)
|
||||||
@@ -176,29 +181,29 @@ class JetStepperGPU(unittest.TestCase):
|
|||||||
patch_, boundary = fine_circle(mode)
|
patch_, boundary = fine_circle(mode)
|
||||||
fluid = (patch_.fine_kind == ACTIVE) & (
|
fluid = (patch_.fine_kind == ACTIVE) & (
|
||||||
(boundary.flags.reshape(patch_.fine_shape) & SOLID) == 0)
|
(boundary.flags.reshape(patch_.fine_shape) & SOLID) == 0)
|
||||||
dense = run(mode, None, objects=[circle()], jets=[jet()])
|
reference = run(mode, FINE_BLOCKS[0], objects=[circle()], jets=[jet()])
|
||||||
self.assertTrue(np.isfinite(dense[1][1][:, fluid]).all(), mode)
|
self.assertTrue(np.isfinite(reference[1][1][:, fluid]).all(), mode)
|
||||||
for fine_block in FINE_BLOCKS:
|
for fine_block in FINE_BLOCKS[1:]:
|
||||||
public, raw_arrays = run(mode, fine_block, objects=[circle()], jets=[jet()])
|
public, raw_arrays = run(mode, fine_block, objects=[circle()], jets=[jet()])
|
||||||
for a, b in zip(public + tuple(raw_arrays), dense[0] + tuple(dense[1])):
|
for a, b in zip(public + tuple(raw_arrays), reference[0] + tuple(reference[1])):
|
||||||
np.testing.assert_array_equal(a, b, err_msg=f"G1 {mode} {fine_block}")
|
np.testing.assert_array_equal(a, b, err_msg=f"G1 {mode} {fine_block}")
|
||||||
legacy = run(mode, None, objects=[circle(actuation(SPEED))], jets=())
|
legacy = run(mode, FINE_BLOCKS[0], objects=[circle(actuation(SPEED))], jets=())
|
||||||
diff = np.abs(dense[1][1][:, fluid] - legacy[1][1][:, fluid]).max()
|
diff = np.abs(reference[1][1][:, fluid] - legacy[1][1][:, fluid]).max()
|
||||||
print(f"G2 {mode}: jet vs legacy fine max |dG| = {diff:.3e}", flush=True)
|
print(f"G2 {mode}: jet vs legacy fine max |dG| = {diff:.3e}", flush=True)
|
||||||
self.assertLessEqual(diff, 1e-6, mode)
|
self.assertLessEqual(diff, 1e-6, mode)
|
||||||
|
|
||||||
def test_g3_amplitude_switch_layouts_identical(self):
|
def test_g3_amplitude_switch_layouts_identical(self):
|
||||||
for mode in ("periodic", "slot_reservoir"):
|
for mode in ("periodic", "slot_reservoir"):
|
||||||
dense = run(mode, None, objects=[circle()], jets=[jet()], switch=-SPEED)
|
reference = run(mode, FINE_BLOCKS[0], objects=[circle()], jets=[jet()], switch=-SPEED)
|
||||||
steady = run(mode, None, objects=[circle()], jets=[jet()])
|
steady = run(mode, FINE_BLOCKS[0], objects=[circle()], jets=[jet()])
|
||||||
self.assertFalse(np.array_equal(dense[1][1], steady[1][1], equal_nan=True), mode)
|
self.assertFalse(np.array_equal(reference[1][1], steady[1][1], equal_nan=True), mode)
|
||||||
for fine_block in FINE_BLOCKS:
|
for fine_block in FINE_BLOCKS[1:]:
|
||||||
public, raw_arrays = run(mode, fine_block, objects=[circle()], jets=[jet()],
|
public, raw_arrays = run(mode, fine_block, objects=[circle()], jets=[jet()],
|
||||||
switch=-SPEED)
|
switch=-SPEED)
|
||||||
for a, b in zip(public + tuple(raw_arrays), dense[0] + tuple(dense[1])):
|
for a, b in zip(public + tuple(raw_arrays), reference[0] + tuple(reference[1])):
|
||||||
np.testing.assert_array_equal(a, b, err_msg=f"G3 {mode} {fine_block}")
|
np.testing.assert_array_equal(a, b, err_msg=f"G3 {mode} {fine_block}")
|
||||||
print(f"G3 {mode}: amplitude switch dense vs {len(FINE_BLOCKS)} layouts identical",
|
print(f"G3 {mode}: amplitude switch, {len(FINE_BLOCKS) - 1} layouts "
|
||||||
flush=True)
|
f"identical to {FINE_BLOCKS[0]}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -42,8 +42,9 @@ class RefinementMRTGPU(unittest.TestCase):
|
|||||||
collision="MRT", ddf_shifting=shifted)
|
collision="MRT", ddf_shifting=shifted)
|
||||||
reference = None
|
reference = None
|
||||||
for block in ((32, 48), (8, 8), (4, 1)):
|
for block in ((32, 48), (8, 8), (4, 1)):
|
||||||
with FixedPatchStepper(cfg, (8, 6, 40, 26), context=context, coarse_boundary=boundary,
|
with FixedPatchStepper(cfg, context=context, coarse_boundary=boundary,
|
||||||
base_block=block, fine_block=(8, 8), fine_objects=(body,)) as backend:
|
refinements=[(8, 6, 40, 26)], level_blocks=[block, (8, 8)],
|
||||||
|
level_objects=[(), (body,)]) as backend:
|
||||||
backend.initialize(*[state(*level.shape[1:]) for level in backend._levels])
|
backend.initialize(*[state(*level.shape[1:]) for level in backend._levels])
|
||||||
backend.step(8)
|
backend.step(8)
|
||||||
raw = backend._download_raw()
|
raw = backend._download_raw()
|
||||||
@@ -53,31 +54,12 @@ class RefinementMRTGPU(unittest.TestCase):
|
|||||||
else:
|
else:
|
||||||
for a, b in zip(reference, raw.values): np.testing.assert_array_equal(a, b)
|
for a, b in zip(reference, raw.values): np.testing.assert_array_equal(a, b)
|
||||||
|
|
||||||
def test_dense_versus_blocked_mrt_two_level(self):
|
|
||||||
"""Unshifted MRT: all-dense PatchCoarseStep/PatchFineStep vs mixed/all-blocked storage."""
|
|
||||||
from test_refinement_blocked_base import BASE_BLOCKS, FINE_BLOCK
|
|
||||||
from test_refinement_blocked_fine import NX, NY, BOUNDS
|
|
||||||
import test_refinement_blocked_fine as fixture
|
|
||||||
cfg = LBMConfig(nx=NX, ny=NY, viscosity=.1, collision="MRT")
|
|
||||||
with CudaContext(0) as context:
|
|
||||||
results = {}
|
|
||||||
layouts = ((None, None), *((base, FINE_BLOCK) for base in (None, *BASE_BLOCKS)))
|
|
||||||
for base_block, fine_block in layouts:
|
|
||||||
with FixedPatchStepper(cfg, BOUNDS, context=context, fine_block=fine_block,
|
|
||||||
base_block=base_block) as backend:
|
|
||||||
backend.initialize(*[fixture.state(*level.shape[1:], seed) for seed, level in enumerate(backend._levels)])
|
|
||||||
backend.step(4)
|
|
||||||
results[base_block, fine_block] = raw(backend)
|
|
||||||
for layout in layouts[1:]:
|
|
||||||
for level, (a, b) in enumerate(zip(results[layout], results[None, None])):
|
|
||||||
exact(self, a, b, f"layout={layout} level={level}")
|
|
||||||
|
|
||||||
def test_shifted_all_blocked_three_level_mrt_smoke(self):
|
def test_shifted_all_blocked_three_level_mrt_smoke(self):
|
||||||
"""Per-level shear rate omega_l = 1/(3*2**l*nu + 1/2); finite ACTIVE leaves."""
|
"""Per-level shear rate omega_l = 1/(3*2**l*nu + 1/2); finite ACTIVE leaves."""
|
||||||
cfg = LBMConfig(nx=16, ny=16, viscosity=.1, collision="MRT", ddf_shifting=True)
|
cfg = LBMConfig(nx=16, ny=16, viscosity=.1, collision="MRT", ddf_shifting=True)
|
||||||
kw = dict(fine_block=(4, 4), base_block=(4, 4), child=(8, 8, 16, 16), child_block=(4, 4))
|
kw = dict(refinements=[(4, 4, 12, 12), (8, 8, 16, 16)], level_blocks=[(4, 4)] * 3)
|
||||||
with CudaContext(0) as context:
|
with CudaContext(0) as context:
|
||||||
with FixedPatchStepper(cfg, (4, 4, 12, 12), context=context, **kw) as backend:
|
with FixedPatchStepper(cfg, context=context, **kw) as backend:
|
||||||
self.assertEqual(len(backend._level_cfgs), 3)
|
self.assertEqual(len(backend._level_cfgs), 3)
|
||||||
for level, level_cfg in enumerate(backend._level_cfgs):
|
for level, level_cfg in enumerate(backend._level_cfgs):
|
||||||
checked = replace(level_cfg)
|
checked = replace(level_cfg)
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ FACE = ((4, 4, 8, 12), (12, 4, 16, 12))
|
|||||||
S1 = ((4, 4, 12, 12), (20, 4, 28, 12))
|
S1 = ((4, 4, 12, 12), (20, 4, 28, 12))
|
||||||
|
|
||||||
|
|
||||||
|
def levels(bounds, blocks=(None, None), objects=(), jets=(), **rest):
|
||||||
|
"""Two-level constructor keywords: one L1 rectangle or set, per-level blocks, L1 bodies/Jets."""
|
||||||
|
return dict(refinements=[bounds], level_blocks=list(blocks),
|
||||||
|
level_objects=[(), tuple(objects)], level_jets=[(), tuple(jets)], **rest)
|
||||||
|
|
||||||
|
|
||||||
def level_fine(patch_set, windows):
|
def level_fine(patch_set, windows):
|
||||||
"""Level-wide fine array with each patch window filled from `windows`."""
|
"""Level-wide fine array with each patch window filled from `windows`."""
|
||||||
out = np.full((9, *patch_set.fine_shape), np.nan, np.float32)
|
out = np.full((9, *patch_set.fine_shape), np.nan, np.float32)
|
||||||
@@ -45,21 +51,21 @@ class MultipatchCPU(unittest.TestCase):
|
|||||||
def test_rejections_before_cuda(self):
|
def test_rejections_before_cuda(self):
|
||||||
cfg = config("periodic")
|
cfg = config("periodic")
|
||||||
context = object() # never activated: every case must fail first
|
context = object() # never activated: every case must fail first
|
||||||
for kw in (dict(), dict(fine_block=(8, 4)), dict(base_block=(4, 4)),
|
for kw in (dict(), dict(blocks=(None, (8, 4))), dict(blocks=((4, 4), None)),
|
||||||
dict(fine_block=(8, 4), base_block=(4, 4), fine_objects=[circle()]),
|
dict(blocks=((4, 4), (8, 4)), objects=[circle()]),
|
||||||
dict(fine_block=(5, 4), base_block=(4, 4))):
|
dict(blocks=((4, 4), (5, 4)))):
|
||||||
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
||||||
FixedPatchStepper(cfg, ((4, 4, 8, 12), (14, 4, 18, 12)), context=context, **kw)
|
FixedPatchStepper(cfg, context=context, **levels(((4, 4, 8, 12), (14, 4, 18, 12)), **kw))
|
||||||
with self.assertRaisesRegex(ValueError, "margin"):
|
with self.assertRaisesRegex(ValueError, "margin"):
|
||||||
FixedPatchStepper(cfg, ((1, 4, 8, 12), (10, 4, 14, 12)), context=context,
|
FixedPatchStepper(cfg, context=context,
|
||||||
fine_block=(8, 4), base_block=(4, 4))
|
**levels(((1, 4, 8, 12), (10, 4, 14, 12)), blocks=((4, 4), (8, 4))))
|
||||||
from CelerisLab.body.jets import Jet, Parallel, Tube
|
from CelerisLab.body.jets import Jet, Parallel, Tube
|
||||||
cfg32 = LBMConfig(nx=32, ny=16, viscosity=.1)
|
cfg32 = LBMConfig(nx=32, ny=16, viscosity=.1)
|
||||||
span = Jet(Tube((5., 8.), 0., 22., .6), Parallel(0.), amplitude=0.)
|
span = Jet(Tube((5., 8.), 0., 22., .6), Parallel(0.), amplitude=0.)
|
||||||
with self.assertRaisesRegex(ValueError, "one jet"): # same obj_id cannot bypass it
|
with self.assertRaisesRegex(ValueError, "one jet"): # same obj_id cannot bypass it
|
||||||
FixedPatchStepper(cfg32, ((2, 2, 14, 14), (18, 2, 30, 14)), context=context,
|
FixedPatchStepper(cfg32, context=context,
|
||||||
fine_block=(4, 4), base_block=(4, 4),
|
**levels(((2, 2, 14, 14), (18, 2, 30, 14)), blocks=((4, 4), (4, 4)),
|
||||||
fine_objects=[body(8., 8.), body(24., 8.)], jets=[span])
|
objects=[body(8., 8.), body(24., 8.)], jets=[span]))
|
||||||
|
|
||||||
|
|
||||||
class MultipatchGPU(unittest.TestCase):
|
class MultipatchGPU(unittest.TestCase):
|
||||||
@@ -67,7 +73,7 @@ class MultipatchGPU(unittest.TestCase):
|
|||||||
|
|
||||||
def run_steps(self, context, cfg, bounds, coarse, fine, **kw):
|
def run_steps(self, context, cfg, bounds, coarse, fine, **kw):
|
||||||
out = []
|
out = []
|
||||||
with FixedPatchStepper(cfg, bounds, context=context, **kw) as backend:
|
with FixedPatchStepper(cfg, context=context, **levels(bounds, **kw)) as backend:
|
||||||
backend.initialize(coarse, fine)
|
backend.initialize(coarse, fine)
|
||||||
for step in self.STEPS:
|
for step in self.STEPS:
|
||||||
backend.step(step - backend.step_count)
|
backend.step(step - backend.step_count)
|
||||||
@@ -82,7 +88,7 @@ class MultipatchGPU(unittest.TestCase):
|
|||||||
p = FixedPatch2D.from_config(cfg, BOUNDS)
|
p = FixedPatch2D.from_config(cfg, BOUNDS)
|
||||||
s = FixedPatchSet2D.from_config(cfg, (BOUNDS,))
|
s = FixedPatchSet2D.from_config(cfg, (BOUNDS,))
|
||||||
coarse, fine = state(NY, NX, 1), state(*p.fine_shape, 2)
|
coarse, fine = state(NY, NX, 1), state(*p.fine_shape, 2)
|
||||||
kw = dict(coarse_boundary=mode, fine_block=(8, 4), base_block=(3, 4))
|
kw = dict(coarse_boundary=mode, blocks=((3, 4), (8, 4)))
|
||||||
old = self.run_steps(context, cfg, BOUNDS, coarse, fine, **kw)
|
old = self.run_steps(context, cfg, BOUNDS, coarse, fine, **kw)
|
||||||
new = self.run_steps(context, cfg, (BOUNDS,), coarse, level_fine(s, [fine]), **kw)
|
new = self.run_steps(context, cfg, (BOUNDS,), coarse, level_fine(s, [fine]), **kw)
|
||||||
for step, a, b in zip(self.STEPS, old, new):
|
for step, a, b in zip(self.STEPS, old, new):
|
||||||
@@ -99,7 +105,7 @@ class MultipatchGPU(unittest.TestCase):
|
|||||||
one = FixedPatchSet2D.from_config(cfg16, (S1[0],))
|
one = FixedPatchSet2D.from_config(cfg16, (S1[0],))
|
||||||
two = FixedPatchSet2D.from_config(cfg32, S1)
|
two = FixedPatchSet2D.from_config(cfg32, S1)
|
||||||
coarse, fine = state(16, 16, 3), state(*one.patches[0].fine_shape, 4)
|
coarse, fine = state(16, 16, 3), state(*one.patches[0].fine_shape, 4)
|
||||||
kw = dict(fine_block=(4, 4), base_block=(4, 4))
|
kw = dict(blocks=((4, 4), (4, 4)))
|
||||||
with CudaContext(0) as context:
|
with CudaContext(0) as context:
|
||||||
ref = self.run_steps(context, cfg16, (S1[0],), coarse, level_fine(one, [fine]), **kw)
|
ref = self.run_steps(context, cfg16, (S1[0],), coarse, level_fine(one, [fine]), **kw)
|
||||||
tiled = np.concatenate([coarse, coarse], axis=2)
|
tiled = np.concatenate([coarse, coarse], axis=2)
|
||||||
@@ -118,7 +124,7 @@ class MultipatchGPU(unittest.TestCase):
|
|||||||
s = FixedPatchSet2D.from_config(cfg, FACE)
|
s = FixedPatchSet2D.from_config(cfg, FACE)
|
||||||
coarse = state(16, 24, 5)
|
coarse = state(16, 24, 5)
|
||||||
fine = level_fine(s, [state(*p.fine_shape, 6 + i) for i, p in enumerate(s.patches)])
|
fine = level_fine(s, [state(*p.fine_shape, 6 + i) for i, p in enumerate(s.patches)])
|
||||||
kw = dict(fine_block=(4, 4), base_block=(4, 4))
|
kw = dict(blocks=((4, 4), (4, 4)))
|
||||||
leaf = s.fine_kind == ACTIVE
|
leaf = s.fine_kind == ACTIVE
|
||||||
support = s.fine_layout((4, 4)).owned
|
support = s.fine_layout((4, 4)).owned
|
||||||
outer = support & (s.fine_kind == OUTER_GHOST)
|
outer = support & (s.fine_kind == OUTER_GHOST)
|
||||||
@@ -164,7 +170,7 @@ class MultipatchBodiesGPU(unittest.TestCase):
|
|||||||
|
|
||||||
def run_steps(self, context, cfg, bounds, coarse, fine, switch=None, **kw):
|
def run_steps(self, context, cfg, bounds, coarse, fine, switch=None, **kw):
|
||||||
out = []
|
out = []
|
||||||
with FixedPatchStepper(cfg, bounds, context=context, **kw) as backend:
|
with FixedPatchStepper(cfg, context=context, **levels(bounds, **kw)) as backend:
|
||||||
backend.initialize(coarse, fine)
|
backend.initialize(coarse, fine)
|
||||||
for step in self.STEPS:
|
for step in self.STEPS:
|
||||||
if switch is not None and step == 8:
|
if switch is not None and step == 8:
|
||||||
@@ -182,8 +188,7 @@ class MultipatchBodiesGPU(unittest.TestCase):
|
|||||||
p = FixedPatch2D.from_config(cfg, BOUNDS)
|
p = FixedPatch2D.from_config(cfg, BOUNDS)
|
||||||
s = FixedPatchSet2D.from_config(cfg, (BOUNDS,))
|
s = FixedPatchSet2D.from_config(cfg, (BOUNDS,))
|
||||||
coarse, fine = state(NY, NX, 1), state(*p.fine_shape, 2)
|
coarse, fine = state(NY, NX, 1), state(*p.fine_shape, 2)
|
||||||
kw = dict(coarse_boundary=mode, fine_block=(8, 4), base_block=(3, 4),
|
kw = dict(coarse_boundary=mode, blocks=((3, 4), (8, 4)), objects=[circle()])
|
||||||
fine_objects=[circle()])
|
|
||||||
old = self.run_steps(context, cfg, BOUNDS, coarse, fine, **kw)
|
old = self.run_steps(context, cfg, BOUNDS, coarse, fine, **kw)
|
||||||
new = self.run_steps(context, cfg, (BOUNDS,), coarse, level_fine(s, [fine]), **kw)
|
new = self.run_steps(context, cfg, (BOUNDS,), coarse, level_fine(s, [fine]), **kw)
|
||||||
for step, a, b in zip(self.STEPS, old, new):
|
for step, a, b in zip(self.STEPS, old, new):
|
||||||
@@ -202,17 +207,17 @@ class MultipatchBodiesGPU(unittest.TestCase):
|
|||||||
two = FixedPatchSet2D.from_config(cfg32, self.PATCHES)
|
two = FixedPatchSet2D.from_config(cfg32, self.PATCHES)
|
||||||
coarse, fine = state(16, 16, 3), state(*one.patches[0].fine_shape, 4)
|
coarse, fine = state(16, 16, 3), state(*one.patches[0].fine_shape, 4)
|
||||||
jet = lambda x: Jet(Tube((x, 8.), 0., 6., .6), Parallel(0.), amplitude=.002)
|
jet = lambda x: Jet(Tube((x, 8.), 0., 6., .6), Parallel(0.), amplitude=.002)
|
||||||
kw = dict(fine_block=(4, 4), base_block=(4, 4))
|
kw = dict(blocks=((4, 4), (4, 4)))
|
||||||
with CudaContext(0) as context:
|
with CudaContext(0) as context:
|
||||||
for jets in ((), (jet(5.),)):
|
for jets in ((), (jet(5.),)):
|
||||||
ref = self.run_steps(context, cfg16, self.PATCHES[:1], coarse, level_fine(one, [fine]),
|
ref = self.run_steps(context, cfg16, self.PATCHES[:1], coarse, level_fine(one, [fine]),
|
||||||
switch=[(0, -.001)] if jets else None,
|
switch=[(0, -.001)] if jets else None,
|
||||||
fine_objects=[body(8., 8.)], jets=jets, **kw)
|
objects=[body(8., 8.)], jets=jets, **kw)
|
||||||
both = jets and (jet(5.), jet(21.))
|
both = jets and (jet(5.), jet(21.))
|
||||||
got = self.run_steps(context, cfg32, self.PATCHES, np.concatenate([coarse, coarse], 2),
|
got = self.run_steps(context, cfg32, self.PATCHES, np.concatenate([coarse, coarse], 2),
|
||||||
level_fine(two, [fine, fine]),
|
level_fine(two, [fine, fine]),
|
||||||
switch=[(0, -.001), (1, -.001)] if jets else None,
|
switch=[(0, -.001), (1, -.001)] if jets else None,
|
||||||
fine_objects=[body(8., 8., 5), body(24., 8., 5)],
|
objects=[body(8., 8., 5), body(24., 8., 5)],
|
||||||
jets=both or (), **kw)
|
jets=both or (), **kw)
|
||||||
for step, a, b in zip(self.STEPS, ref, got):
|
for step, a, b in zip(self.STEPS, ref, got):
|
||||||
label = f"jets={len(jets)} step {step}"
|
label = f"jets={len(jets)} step {step}"
|
||||||
|
|||||||
@@ -27,7 +27,16 @@ from test_refinement_blocked_base import raw
|
|||||||
|
|
||||||
CFG = LBMConfig(nx=16, ny=16, viscosity=.1)
|
CFG = LBMConfig(nx=16, ny=16, viscosity=.1)
|
||||||
L1, L2 = (4, 4, 12, 12), (8, 8, 16, 16)
|
L1, L2 = (4, 4, 12, 12), (8, 8, 16, 16)
|
||||||
KW = dict(fine_block=(4, 4), base_block=(4, 4), child=L2, child_block=(4, 4))
|
B = (4, 4)
|
||||||
|
|
||||||
|
|
||||||
|
def levels(l1=L1, l2=L2, blocks=(B, B, B), objects=((), (), ()), jets=((), (), ())):
|
||||||
|
"""Level-list keywords for the scalar L1 patch with one scalar L2 child."""
|
||||||
|
return dict(refinements=[l1, l2], level_blocks=list(blocks),
|
||||||
|
level_objects=list(objects), level_jets=list(jets))
|
||||||
|
|
||||||
|
|
||||||
|
KW = levels()
|
||||||
|
|
||||||
|
|
||||||
def child_body(spec=None):
|
def child_body(spec=None):
|
||||||
@@ -43,35 +52,38 @@ def child_jet(speed=.002):
|
|||||||
|
|
||||||
|
|
||||||
def mocked_child():
|
def mocked_child():
|
||||||
from CelerisLab.cuda import compiler_v2 as compiler
|
from CelerisLab.cuda import compiler
|
||||||
context = Mock(activate=nullcontext, sm_arch="sm_70")
|
context = Mock(activate=nullcontext, sm_arch="sm_70")
|
||||||
module = Mock(get_function=Mock(side_effect=lambda name: Mock(name=name)))
|
module = Mock(get_function=Mock(side_effect=lambda name: Mock(name=name)))
|
||||||
module.get_global.return_value = (Mock(), 20)
|
module.get_global.return_value = (Mock(), 20)
|
||||||
with patch.object(cuda, "Stream"), patch.object(compiler, "generate_config"), \
|
with patch.object(cuda, "Stream"), patch.object(cuda, "mem_get_info", return_value=(1 << 40, 1 << 40)), patch.object(compiler, "generate_config"), \
|
||||||
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
patch.object(compiler, "compile_kernel"), patch.object(compiler, "cleanup_build_dir"), \
|
||||||
patch.object(compiler, "load_module", return_value=module), \
|
patch.object(compiler, "load_module", return_value=module), \
|
||||||
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
|
patch.object(cuda, "mem_alloc", side_effect=lambda size: Mock()), \
|
||||||
patch.object(cuda, "memcpy_htod"):
|
patch.object(cuda, "memcpy_htod"):
|
||||||
return FixedPatchStepper(CFG, L1, context=context, child_objects=[child_body()],
|
return FixedPatchStepper(CFG, context=context,
|
||||||
child_jets=[child_jet()], **KW)
|
**levels(objects=((), (), (child_body(),)), jets=((), (), (child_jet(),))))
|
||||||
|
|
||||||
|
|
||||||
class ThreeLevelCPU(unittest.TestCase):
|
class ThreeLevelCPU(unittest.TestCase):
|
||||||
def test_rejections_before_cuda(self):
|
def test_rejections_before_cuda(self):
|
||||||
context = object() # never activated: every case must fail first
|
context = object() # never activated: every case must fail first
|
||||||
bad = (dict(KW, child_block=None), dict(KW, fine_block=None),
|
bad = (levels(blocks=(B, B, None)), levels(blocks=(B, None, B)),
|
||||||
dict(KW, fine_objects=[circle()]), dict(KW, child=(4, 8, 16, 16)),
|
levels(objects=((), (circle(),), ())), levels(l2=(4, 8, 16, 16)),
|
||||||
dict(KW, child_block=(5, 4)))
|
levels(blocks=(B, B, (5, 4))))
|
||||||
for kw in bad:
|
for kw in bad:
|
||||||
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
with self.subTest(kw=kw), self.assertRaises(ValueError):
|
||||||
FixedPatchStepper(CFG, L1, context=context, **kw)
|
FixedPatchStepper(CFG, context=context, **kw)
|
||||||
for kw in (dict(child_objects=[child_body()]), dict(child_jets=[child_jet()]),
|
# L2 bodies/Jets without an L2 level (one entry too many), and an L2 Jet without a body.
|
||||||
dict(KW, child_jets=[child_jet()])):
|
two_level = dict(refinements=[L1], level_blocks=[B, B])
|
||||||
|
for kw in (dict(two_level, level_objects=[(), (), (child_body(),)]),
|
||||||
|
dict(two_level, level_jets=[(), (), (child_jet(),)]),
|
||||||
|
levels(jets=((), (), (child_jet(),)))):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
FixedPatchStepper(CFG, L1, context=context, **kw)
|
FixedPatchStepper(CFG, context=context, **kw)
|
||||||
with self.assertRaises(ValueError): # several L1 patches + child: not yet
|
with self.assertRaises(ValueError): # several L1 patches + scalar child: not yet
|
||||||
FixedPatchStepper(LBMConfig(nx=32, ny=16, viscosity=.1), ((4, 4, 12, 12), (20, 4, 28, 12)),
|
FixedPatchStepper(LBMConfig(nx=32, ny=16, viscosity=.1), context=context,
|
||||||
context=context, **KW)
|
**levels(l1=((4, 4, 12, 12), (20, 4, 28, 12))))
|
||||||
|
|
||||||
|
|
||||||
def test_four_halo_bfl_steps_and_fresh_middle_feeds(self):
|
def test_four_halo_bfl_steps_and_fresh_middle_feeds(self):
|
||||||
@@ -80,11 +92,12 @@ class ThreeLevelCPU(unittest.TestCase):
|
|||||||
f, g = run._levels[1:]
|
f, g = run._levels[1:]
|
||||||
b = g.boundary
|
b = g.boundary
|
||||||
events, feeds = [], []
|
events, feeds = [], []
|
||||||
advance, gather = run._advance_child, run._gather_halo
|
advance, gather = run._advance, run._gather_halo
|
||||||
def record_feed(buffer, **kw):
|
def record_feed(index, buffer, **kw): # index 2: L1 hop feeds L2
|
||||||
self.assertIs(buffer, (f.current, f.spare)[len(feeds)])
|
if index == 2:
|
||||||
feeds.append(buffer)
|
self.assertIs(buffer, (f.current, f.spare)[len(feeds)])
|
||||||
advance(buffer, **kw)
|
feeds.append(buffer)
|
||||||
|
advance(index, buffer, **kw)
|
||||||
def record_halo(level, buffer, **kw):
|
def record_halo(level, buffer, **kw):
|
||||||
if level is g:
|
if level is g:
|
||||||
events.append(("halo", buffer))
|
events.append(("halo", buffer))
|
||||||
@@ -95,7 +108,7 @@ class ThreeLevelCPU(unittest.TestCase):
|
|||||||
self.assertIs(kind, g.kind)
|
self.assertIs(kind, g.kind)
|
||||||
events.append(("step", src))
|
events.append(("step", src))
|
||||||
g.kernel.side_effect = record_step
|
g.kernel.side_effect = record_step
|
||||||
with patch.object(run, "_advance_child", side_effect=record_feed), \
|
with patch.object(run, "_advance", side_effect=record_feed), \
|
||||||
patch.object(run, "_gather_halo", side_effect=record_halo):
|
patch.object(run, "_gather_halo", side_effect=record_halo):
|
||||||
run.step()
|
run.step()
|
||||||
self.assertEqual(feeds, [f.current, f.spare])
|
self.assertEqual(feeds, [f.current, f.spare])
|
||||||
@@ -105,8 +118,8 @@ class ThreeLevelCPU(unittest.TestCase):
|
|||||||
self.assertGreater(len(np.unique(b.tables["block"])), 1)
|
self.assertGreater(len(np.unique(b.tables["block"])), 1)
|
||||||
# Negative control: reusing L1's old source at the midpoint must fail.
|
# Negative control: reusing L1's old source at the midpoint must fail.
|
||||||
feeds.clear()
|
feeds.clear()
|
||||||
with patch.object(run, "_advance_child", side_effect=lambda buffer, **kw:
|
with patch.object(run, "_advance", side_effect=lambda index, buffer, **kw:
|
||||||
record_feed(f.current, **kw)):
|
record_feed(index, f.current if index == 2 else buffer, **kw)):
|
||||||
with self.assertRaises(AssertionError):
|
with self.assertRaises(AssertionError):
|
||||||
run.step()
|
run.step()
|
||||||
run.close()
|
run.close()
|
||||||
@@ -155,7 +168,7 @@ class ThreeLevelGPU(unittest.TestCase):
|
|||||||
def test_equilibrium_is_kept_on_every_level(self):
|
def test_equilibrium_is_kept_on_every_level(self):
|
||||||
from CelerisLab.cuda.context import CudaContext
|
from CelerisLab.cuda.context import CudaContext
|
||||||
feq = np.asarray(compute_feq_d2q9(1.0, .03, -.02), np.float32)[:, None, None]
|
feq = np.asarray(compute_feq_d2q9(1.0, .03, -.02), np.float32)[:, None, None]
|
||||||
with CudaContext(0) as ctx, FixedPatchStepper(CFG, L1, context=ctx, **KW) as run:
|
with CudaContext(0) as ctx, FixedPatchStepper(CFG, context=ctx, **KW) as run:
|
||||||
shapes = ((16, 16), run.patch.fine_shape, run.child_patch.fine_shape)
|
shapes = ((16, 16), run.patch.fine_shape, run.child_patch.fine_shape)
|
||||||
run.initialize(*(np.broadcast_to(feq, (9, *s)).copy() for s in shapes))
|
run.initialize(*(np.broadcast_to(feq, (9, *s)).copy() for s in shapes))
|
||||||
run.step(8)
|
run.step(8)
|
||||||
@@ -168,15 +181,16 @@ class ThreeLevelGPU(unittest.TestCase):
|
|||||||
|
|
||||||
def test_nonequilibrium_masks_and_fresh_middle_feeds(self):
|
def test_nonequilibrium_masks_and_fresh_middle_feeds(self):
|
||||||
from CelerisLab.cuda.context import CudaContext
|
from CelerisLab.cuda.context import CudaContext
|
||||||
with CudaContext(0) as ctx, FixedPatchStepper(CFG, L1, context=ctx, **KW) as run:
|
with CudaContext(0) as ctx, FixedPatchStepper(CFG, context=ctx, **KW) as run:
|
||||||
p1, p2 = run.patch, run.child_patch
|
p1, p2 = run.patch, run.child_patch
|
||||||
run.initialize(state(16, 16, 1), state(*p1.fine_shape, 2), state(*p2.fine_shape, 3))
|
run.initialize(state(16, 16, 1), state(*p1.fine_shape, 2), state(*p2.fine_shape, 3))
|
||||||
f = run._levels[1]
|
f = run._levels[1]
|
||||||
feeds, original = [], run._advance_child
|
feeds, original = [], run._advance
|
||||||
def record(buffer, **kw):
|
def record(index, buffer, **kw): # index 2: L1 hop feeds L2
|
||||||
feeds.append(int(buffer))
|
if index == 2:
|
||||||
original(buffer, **kw)
|
feeds.append(int(buffer))
|
||||||
with patch.object(run, "_advance_child", side_effect=record):
|
original(index, buffer, **kw)
|
||||||
|
with patch.object(run, "_advance", side_effect=record):
|
||||||
cur, spare = int(f.current), int(f.spare)
|
cur, spare = int(f.current), int(f.spare)
|
||||||
run.step(1)
|
run.step(1)
|
||||||
self.assertEqual(feeds, [cur, spare]) # current G, then the new G of hop 1
|
self.assertEqual(feeds, [cur, spare]) # current G, then the new G of hop 1
|
||||||
@@ -203,8 +217,9 @@ class ThreeLevelBodyGPU(unittest.TestCase):
|
|||||||
CFG, velocity=0., wall_normal_velocity=.001, wall_slot_width=4,
|
CFG, velocity=0., wall_normal_velocity=.001, wall_slot_width=4,
|
||||||
y_wall_bc="on_site_velocity", top_wall_bc="reservoir")
|
y_wall_bc="on_site_velocity", top_wall_bc="reservoir")
|
||||||
with CudaContext(0) as ctx, FixedPatchStepper(
|
with CudaContext(0) as ctx, FixedPatchStepper(
|
||||||
cfg, L1, context=ctx, coarse_boundary=mode, child_objects=[child_body(spec)],
|
cfg, context=ctx, coarse_boundary=mode,
|
||||||
child_jets=jets, **dict(KW, child_block=block)) as run:
|
**levels(blocks=(B, B, block), objects=((), (), (child_body(spec),)),
|
||||||
|
jets=((), (), tuple(jets)))) as run:
|
||||||
run.initialize(*(state(*level.shape[1:], i + 1) for i, level in enumerate(run._levels)))
|
run.initialize(*(state(*level.shape[1:], i + 1) for i, level in enumerate(run._levels)))
|
||||||
snapshots = []
|
snapshots = []
|
||||||
for stop in (0, 1, 2, 4, 8):
|
for stop in (0, 1, 2, 4, 8):
|
||||||
|
|||||||
@@ -78,10 +78,10 @@ class ThreeLevelMultipatchGPU(unittest.TestCase):
|
|||||||
def run_case(self, ctx, cfg=CFG, parents=PARENTS, children=CHILDREN,
|
def run_case(self, ctx, cfg=CFG, parents=PARENTS, children=CHILDREN,
|
||||||
mode='periodic', blocks=((4, 4), (4, 4)), objects=(), drive=(), switch=False,
|
mode='periodic', blocks=((4, 4), (4, 4)), objects=(), drive=(), switch=False,
|
||||||
scalar_child=None):
|
scalar_child=None):
|
||||||
kwargs = dict(child_rectangles_in_parent=children) if scalar_child is None else dict(child=scalar_child)
|
child = children if scalar_child is None else scalar_child
|
||||||
with FixedPatchStepper(config(cfg, mode), parents, context=ctx, coarse_boundary=mode,
|
with FixedPatchStepper(config(cfg, mode), context=ctx, coarse_boundary=mode,
|
||||||
base_block=(4, 4), fine_block=blocks[0], child_block=blocks[1],
|
refinements=[parents, child], level_blocks=[(4, 4), blocks[0], blocks[1]],
|
||||||
child_objects=objects, child_jets=drive, **kwargs) as run:
|
level_objects=[(), (), objects], level_jets=[(), (), drive]) as run:
|
||||||
frame = frames(run)
|
frame = frames(run)
|
||||||
run.initialize(*(physical_state(level.shape[1:], *f)
|
run.initialize(*(physical_state(level.shape[1:], *f)
|
||||||
for level, f in zip(run._levels, frame)))
|
for level, f in zip(run._levels, frame)))
|
||||||
|
|||||||
@@ -26,9 +26,8 @@ FP64_ATOL = 3e-6
|
|||||||
|
|
||||||
|
|
||||||
def make(ctx):
|
def make(ctx):
|
||||||
return FixedPatchStepper(CFG, PARENTS, context=ctx, base_block=(4, 4),
|
return FixedPatchStepper(CFG, context=ctx, refinements=[PARENTS, A],
|
||||||
fine_block=(4, 4), child_block=(4, 4),
|
level_blocks=[(4, 4)] * 3)
|
||||||
child_rectangles_in_parent=A)
|
|
||||||
|
|
||||||
|
|
||||||
def initial(run, uniform):
|
def initial(run, uniform):
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ class ShiftedJetsGPU(unittest.TestCase):
|
|||||||
Parallel(0.),amplitude=.013)
|
Parallel(0.),amplitude=.013)
|
||||||
for rho in (.99,1.01):
|
for rho in (.99,1.01):
|
||||||
with self.subTest(rho=rho),CudaContext(0) as ctx:
|
with self.subTest(rho=rho),CudaContext(0) as ctx:
|
||||||
with FixedPatchStepper(cfg,(8,6,24,18),context=ctx,base_block=(4,4),fine_block=(4,4),
|
with FixedPatchStepper(cfg,context=ctx,refinements=[(8,6,24,18)],level_blocks=[(4,4)]*2,
|
||||||
fine_objects=(obj,),jets=(whole,),observe_raw_exchange=True) as run:
|
level_objects=[(),(obj,)],level_jets=[(),(whole,)],observe_raw_exchange=True) as run:
|
||||||
run._initialize_raw(initial(run,rho,u=.013,v=0.))
|
run._initialize_raw(initial(run,rho,u=.013,v=0.))
|
||||||
expected=run._download_raw();ledger=Ledger(self,run,1)
|
expected=run._download_raw();ledger=Ledger(self,run,1)
|
||||||
self.assertTrue(run._level_boundaries[1].tables['aperture'].all())
|
self.assertTrue(run._level_boundaries[1].tables['aperture'].all())
|
||||||
@@ -153,7 +153,8 @@ class ShiftedJetsGPU(unittest.TestCase):
|
|||||||
obj=circle((16.,12.),2.15);j=drive((16.,12.),2.15)
|
obj=circle((16.,12.),2.15);j=drive((16.,12.),2.15)
|
||||||
for rho in (.99,1.01):
|
for rho in (.99,1.01):
|
||||||
with self.subTest(rho=rho),CudaContext(0) as ctx,ExitStack() as owned:
|
with self.subTest(rho=rho),CudaContext(0) as ctx,ExitStack() as owned:
|
||||||
settings=dict(bounds=(8,6,24,18),context=ctx,base_block=(4,4),fine_block=(4,4),fine_objects=(obj,),jets=(j,))
|
settings=dict(context=ctx,refinements=[(8,6,24,18)],level_blocks=[(4,4)]*2,
|
||||||
|
level_objects=[(),(obj,)],level_jets=[(),(j,)])
|
||||||
measured=owned.enter_context(FixedPatchStepper(cfg,observe_raw_exchange=True,**settings))
|
measured=owned.enter_context(FixedPatchStepper(cfg,observe_raw_exchange=True,**settings))
|
||||||
plain=owned.enter_context(FixedPatchStepper(cfg,**settings))
|
plain=owned.enter_context(FixedPatchStepper(cfg,**settings))
|
||||||
measured._initialize_raw(initial(measured,rho));plain._initialize_raw(initial(plain,rho))
|
measured._initialize_raw(initial(measured,rho));plain._initialize_raw(initial(plain,rho))
|
||||||
@@ -186,8 +187,9 @@ class ShiftedJetsGPU(unittest.TestCase):
|
|||||||
parents=((4,4,20,28),(24,4,44,28));a=((16,16,32,40),(56,16,72,40));b=(a[0],(60,16,76,40))
|
parents=((4,4,20,28),(24,4,44,28));a=((16,16,32,40),(56,16,72,40));b=(a[0],(60,16,76,40))
|
||||||
obj=circle((12.,14.),.8);j=drive((12.,14.),.8)
|
obj=circle((12.,14.),.8);j=drive((12.,14.),.8)
|
||||||
with CudaContext(0) as ctx,ExitStack() as owned:
|
with CudaContext(0) as ctx,ExitStack() as owned:
|
||||||
old=owned.enter_context(FixedPatchStepper(cfg,parents,context=ctx,base_block=(4,4),fine_block=(4,4),
|
old=owned.enter_context(FixedPatchStepper(cfg,context=ctx,refinements=[parents,a],
|
||||||
child_block=(4,4),child_rectangles_in_parent=a,child_objects=(obj,),child_jets=(j,),observe_raw_exchange=True))
|
level_blocks=[(4,4)]*3,level_objects=[(),(),(obj,)],level_jets=[(),(),(j,)],
|
||||||
|
observe_raw_exchange=True))
|
||||||
old._initialize_raw(initial(old,.99));ledger=Ledger(self,old,2)
|
old._initialize_raw(initial(old,.99));ledger=Ledger(self,old,2)
|
||||||
for speed in (.002,-.001):old.set_jet_amplitude(0,speed,level=2);ledger.step()
|
for speed in (.002,-.001):old.set_jet_amplitude(0,speed,level=2);ledger.step()
|
||||||
before=old._download_raw();count=old.step_count
|
before=old._download_raw();count=old.step_count
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ B=((20,16,36,40),)
|
|||||||
|
|
||||||
|
|
||||||
def make(ctx):
|
def make(ctx):
|
||||||
return FixedPatchStepper(CFG,PARENTS,context=ctx,base_block=(4,4),
|
return FixedPatchStepper(CFG,context=ctx,refinements=[PARENTS,A],
|
||||||
fine_block=(4,4),child_block=(4,4),
|
level_blocks=[(4,4)]*3)
|
||||||
child_rectangles_in_parent=A)
|
|
||||||
|
|
||||||
|
|
||||||
def initial(run):
|
def initial(run):
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user