Compare commits
90
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
eec4677db1 | ||
|
|
2cf9853952 | ||
|
|
1aa8110ec8 | ||
|
|
a135252ae7 | ||
|
|
ed9d8c937d | ||
|
|
fc0e544c0a | ||
|
|
95cb6d2b9f | ||
|
|
53fad76759 | ||
|
|
8403956a9a | ||
|
|
185a919cc5 | ||
|
|
aceea293a3 | ||
|
|
a233c90324 | ||
|
|
f851aecf73 | ||
|
|
51202fc1c3 | ||
|
|
d930323479 | ||
|
|
6c53d2e74a | ||
|
|
724afb607c | ||
|
|
77bab1c4ea | ||
|
|
15fb58e36f | ||
|
|
4d4b9ba00a | ||
|
|
2abc48f0d8 | ||
|
|
3ea131e92c | ||
|
|
00d7a54829 | ||
|
|
9805383f9c | ||
|
|
1f263b9e46 | ||
|
|
b23cb45adc | ||
|
|
413f7afade | ||
|
|
af984eab37 | ||
|
|
cd3a657f9d | ||
|
|
fa5d0c22a9 | ||
|
|
5b4610441c | ||
|
|
20ae677092 | ||
|
|
69e9b78de9 | ||
|
|
d79e3f6f0a | ||
|
|
9dddf5d812 | ||
|
|
b77f2238dd | ||
|
|
c33caf636d | ||
|
|
aaf35c6505 | ||
|
|
b811b643e7 | ||
|
|
fbe73779c1 |
@@ -112,8 +112,9 @@ from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
cfg = LBMConfig(nx=32, ny=24, viscosity=.1)
|
||||
body = SimObject(0, geometry=CircleGeometry(16.0, 12.0, 2.15), center=(16.0, 12.0), radius=2.15,
|
||||
surface_actuation=SurfaceActuation(0.7, 0.85, 0.002)) # angle, half-width, speed
|
||||
with CudaContext(0) as ctx, FixedPatchStepper(cfg, (8, 6, 24, 18), context=ctx,
|
||||
fine_objects=[body], fine_block=(16, 20)) as run:
|
||||
with CudaContext(0) as ctx, FixedPatchStepper(cfg, context=ctx, refinements=[(8, 6, 24, 18)],
|
||||
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]
|
||||
# (9, ny, nx) postcollision populations per level; non-leaf cells are discarded.
|
||||
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
|
||||
@@ -123,7 +124,9 @@ with CudaContext(0) as ctx, FixedPatchStepper(cfg, (8, 6, 24, 18), context=ctx,
|
||||
```
|
||||
|
||||
Scope and limits:
|
||||
- D2Q9, SRT, FP32, double buffer, unshifted, no LES or forcing; fixed 2:1 patch(es), two levels (or one nested third).
|
||||
- The examples below use D2Q9, SRT, FP32, double buffer, unshifted, no LES or
|
||||
forcing. Fixed 2:1 hierarchies use level lists of arbitrary depth; two- and
|
||||
three-level examples below use that same constructor.
|
||||
- `coarse_boundary="periodic"` (default) or `"slot_reservoir"` (bottom slot, top
|
||||
reservoir; patch needs `y0 >= 4`, `y1 <= ny-4`). Periodic mode ignores the wall/slot config fields.
|
||||
- Object centers/radii use the coarse **cell-edge** frame (cell `I` spans `[I, I+1)`),
|
||||
@@ -131,46 +134,51 @@ Scope and limits:
|
||||
- Bodies must be stationary circles whose whole boundary stencil lies in the fine
|
||||
active region, clear of the coarse/fine transfer cells; violations are rejected
|
||||
before any GPU allocation.
|
||||
- `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
|
||||
four-cell ghost band. It gives results identical to dense storage and is
|
||||
infrastructure for future block AMR, not a speedup.
|
||||
- Several separated patches: pass a sequence of `(x0, y0, x1, y1)`
|
||||
rectangles as `bounds`, at least 4 coarse cells apart, with both `fine_block`
|
||||
and `base_block`. The fine level is then one level-wide grid `(2*ny, 2*nx)`:
|
||||
- Several regions: pass a sequence of `(x0, y0, x1, y1)` rectangles as
|
||||
`refinements[0]`, with both levels blocked (`level_blocks`). Transfers are built once
|
||||
from their union; adjacent, overlapping and concave unions are accepted when
|
||||
their actual dependencies close. There is no fixed rectangle-gap rule.
|
||||
The fine level is then one level-wide grid `(2*ny, 2*nx)`:
|
||||
`run.patch.fine_shape` gives it, `initialize`/`download` use it, and only the
|
||||
patch windows (active region plus 4-cell ghost band, origin `(2*y0-4, 2*x0-4)`)
|
||||
are stored; each window must align with `fine_block`. Order of rectangles does
|
||||
not matter. `fine_objects`/`jets` work as for one patch: each circle (same
|
||||
coarse cell-edge frame) must lie wholly inside one patch's active region with
|
||||
grid-aligned tiles intersecting the union plus its four-cell ghost band are stored.
|
||||
Block sizes must divide the level-wide grid; rectangle edges need not align
|
||||
with tiles. Order of rectangles does not matter. L1 bodies/Jets (`level_objects[1]`/`level_jets[1]`) work
|
||||
as for one patch: each circle (same coarse cell-edge frame) must lie wholly
|
||||
inside the union's active region with
|
||||
its boundary stencil clear of transfer cells; a jet may not select links of two
|
||||
different bodies, even across patches. No wall-touching patches or nesting yet.
|
||||
different bodies, even across regions. Wall-touching patches remain unsupported.
|
||||
For nesting, add further `refinements` entries as shown below.
|
||||
|
||||
```python
|
||||
cfg = LBMConfig(nx=32, ny=16, viscosity=.1)
|
||||
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
||||
with CudaContext(0) as ctx, FixedPatchStepper(
|
||||
cfg, ((4, 4, 12, 12), (16, 4, 24, 12)), context=ctx,
|
||||
fine_block=(4, 4), base_block=(4, 4)) as run:
|
||||
cfg, context=ctx, refinements=[((4, 4, 12, 12), (16, 4, 24, 12))],
|
||||
level_blocks=[(4, 4), (4, 4)]) as run:
|
||||
run.initialize(np.broadcast_to(rest, (9, *run.patch.coarse_shape)),
|
||||
np.broadcast_to(rest, (9, *run.patch.fine_shape))) # (9, 32, 64)
|
||||
run.step(32)
|
||||
coarse_g, fine_g = run.download() # coarse: NaN on every covered cell;
|
||||
# fine: NaN on every non-ACTIVE cell
|
||||
```
|
||||
- Three levels (one nested chain): add `child=(x0, y0, x1, y1)` in
|
||||
**L1 window cells** (the patch's fine grid including its 4-cell ghost band) and
|
||||
`child_block`, with `fine_block` and `base_block`. L2 must sit inside L1's
|
||||
ACTIVE cells, away from L1's ghost band and transfer footprint (checked before
|
||||
any GPU allocation). `initialize` and `download` take/return three arrays; each
|
||||
level is NaN on its non-ACTIVE (and child-covered) cells. One coarse step = 2 L1
|
||||
steps = 4 L2 steps. One L1 patch only. Optional stationary circles and Jets
|
||||
live on L2 via `child_objects` and `child_jets`; `fine_objects`/`jets` retain
|
||||
their L1 meaning and must be empty in this mode. All object and Jet geometry
|
||||
uses the **L0 edge frame**, not the child bounds' L1 window frame. L2 has
|
||||
dx=dt=0.25, so prescribed lattice velocity is unchanged. Each whole body and
|
||||
its BFL stencil must be inside L2 ACTIVE cells, outside the transfer footprint.
|
||||
A Jet may select one physical body across multiple blocks, never two bodies.
|
||||
- Three-level nested windows: pass two scalar rectangles in `refinements` —
|
||||
L1 in L0 cells, L2 in **L1 window cells** (the parent's fine grid including its
|
||||
4-cell ghost band) — plus all three `level_blocks` entries. L2 must sit inside
|
||||
L1's ACTIVE cells, away from L1's ghost band and transfer footprint (checked
|
||||
before any GPU allocation). `initialize` and `download` take/return three
|
||||
arrays; each level is NaN on its non-ACTIVE (and child-covered) cells. One
|
||||
coarse step = 2 L1 steps = 4 L2 steps. One L1 window only in this scalar
|
||||
chain. Optional stationary circles and Jets live on L2 via `level_objects[2]`
|
||||
and `level_jets[2]`; L1 bodies/Jets retain their L1 meaning and must be empty
|
||||
in this mode. All object and Jet geometry uses the **L0 edge frame**, not the
|
||||
child bounds' L1 window frame. L2 has dx=dt=0.25, so prescribed lattice
|
||||
velocity is unchanged. Each whole body and its BFL stencil must be inside L2
|
||||
ACTIVE cells, outside the transfer footprint. A Jet may select one physical
|
||||
body across multiple blocks, never two bodies.
|
||||
|
||||
```python
|
||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||
@@ -182,9 +190,9 @@ Scope and limits:
|
||||
jet = Jet(Tube((6.5, 8.), 0., 3., .3), Parallel(0.), amplitude=.002)
|
||||
rest = np.asarray(compute_feq_d2q9(1.0, 0.0, 0.0), np.float32)[:, None, None]
|
||||
with CudaContext(0) as ctx, FixedPatchStepper(
|
||||
cfg, (4, 4, 12, 12), context=ctx, fine_block=(4, 4), base_block=(4, 4),
|
||||
child=(8, 8, 16, 16), child_block=(4, 4),
|
||||
child_objects=[body], child_jets=[jet]) as run:
|
||||
cfg, context=ctx, refinements=[(4, 4, 12, 12), (8, 8, 16, 16)],
|
||||
level_blocks=[(4, 4)] * 3, level_objects=[(), (), (body,)],
|
||||
level_jets=[(), (), (jet,)]) as run:
|
||||
shapes = (run.patch.coarse_shape, run.patch.fine_shape, run.child_patch.fine_shape)
|
||||
run.initialize(*(np.broadcast_to(rest, (9, *s)) for s in shapes)) # 16², 24², 24²
|
||||
run.step(4)
|
||||
@@ -192,14 +200,30 @@ Scope and limits:
|
||||
run.step(4)
|
||||
coarse_g, l1_g, l2_g = run.download() # solids/covered/nonleaves are NaN
|
||||
```
|
||||
- Not available: per-link surface exchange/force readout in blocked mode,
|
||||
checkpoints, moving bodies, L1 bodies/Jets in a three-level chain,
|
||||
several patches per level with nesting, dynamic refinement, `Simulation`
|
||||
integration. Validation so far is storage/operator equivalence and short runs,
|
||||
not accuracy, convergence or long-time stability. The new L2-body GPU gates
|
||||
(layouts, masks, amplitude reversal, legacy equivalence) and independent
|
||||
every-substep numerical replay remain required acceptance checks; CPU tests
|
||||
alone do not establish GPU numerical correctness.
|
||||
- Level lists (the only constructor): `refinements[l]` describes level `l+1`
|
||||
in allocated level-`l` integer cells; use a rectangle list for a union, or a
|
||||
scalar rectangle for a single window (a scalar child requires a scalar
|
||||
parent). `level_blocks`, `level_objects` and `level_jets` each have one entry
|
||||
per level, including L0. Three or more levels require all-blocked storage and
|
||||
bodies/Jets only on the finest level. Every adjacent pair must preserve
|
||||
inherited ACTIVE coverage, valid donors/receivers, surviving pulls and the
|
||||
upstream transfer footprint. Level `l` takes `2**l` steps per base interval.
|
||||
This describes implemented topology and scheduling, not accuracy or
|
||||
convergence qualification.
|
||||
- Blocked body levels can enable `observe_raw_exchange=True` and call
|
||||
`read_raw_exchange(body_id, level=...)` after a completed base interval.
|
||||
It reduces unique cut-link observations into wall/aperture/total force,
|
||||
torque and mass exchange in base-lattice units per unit depth, for the last
|
||||
base interval (default) or `window="last_substep"`.
|
||||
`observe_window_exchange=True`, alongside `observe_raw_exchange=True`, enables
|
||||
`start_exchange_window()` / `read_exchange_window(body_id, level=...)` across
|
||||
completed base intervals. These are raw boundary-reaction diagnostics, not a general
|
||||
stress or nozzle-load accuracy claim; dense body layouts are unsupported.
|
||||
- Not available: checkpoints, moving bodies, L1 bodies/Jets in a three-level
|
||||
chain, automatic dynamic refinement, `Simulation` integration. Qualification
|
||||
depends on the tested configuration; see [the validation scope](docs/validation/2d_scope.md).
|
||||
CPU topology/schedule tests alone do not establish GPU numerical correctness,
|
||||
general convergence or long-time stability.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -663,6 +687,17 @@ for step in range(steps):
|
||||
streak.render("streakline.png")
|
||||
```
|
||||
|
||||
## Validation campaigns
|
||||
|
||||
- [Periodic Taylor–Green AMR](docs/validation/taylor_green.md): analytic periodic-decay pilot and frozen baseline-relative assessment.
|
||||
- [Matched refined DFG](docs/validation/refined_dfg.md): frozen SRT uniform/refined comparison, requiring channel GPU gates before execution.
|
||||
- [DFG 2D-2 Re100](docs/validation/dfg_re100.md): frozen periodicity and official-reference comparisons, with a declared noncanonical outlet.
|
||||
- [Uniform LES shear decay](docs/validation/uniform_les_flow.md): independent continuum-oracle flow diagnostic, baseline and isolated corrected source variants.
|
||||
- [Smagorinsky helper comparison](docs/validation/les_helpers.md): pinned baseline versus an output-local two-coefficient candidate, without production edits.
|
||||
- [Jet-circle control volume](docs/validation/jet_circle_cv.md): bounded AMR versus uniform-grid conservation diagnostic.
|
||||
|
||||
The runners are in `tests/validation`, their CPU tests in `tests/unit`, and compact reference/source guards in `tests/validation/data`. Current Re100/Jet source guards explicitly pin the reviewed AMR-channel experiment; historical d79 evidence remains tied to its original checkpoint commits. New runs record actual Git HEAD separately from source origin. These campaigns preserve pinned production code; successful CPU checks alone do not establish GPU or physical validation. Keep generated results outside tracked files.
|
||||
|
||||
## Citation
|
||||
|
||||
```bibtex
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Compatibility and API changes
|
||||
|
||||
Changes that can make a previously accepted setup or call behave differently.
|
||||
Each entry names the commit, the affected entry points and the observable result.
|
||||
|
||||
## 87c3a31: cut-link ownership is validated for every body
|
||||
|
||||
`validate_cut_link_ownership` (`body/preprocess/flag_overlay.py`) now runs for
|
||||
passive bodies as well as Jet/aperture bodies. Every cut link must have an
|
||||
active fluid receiver (FLUID set, SOLID clear), and each `(receiver, direction)`
|
||||
population may have only one writer.
|
||||
|
||||
Rejected with `ValueError`, before flags, populations or device buffers change:
|
||||
|
||||
1. Overlapping or touching passive solids whose cut links write the same
|
||||
population ("Boundary requires unique cut-link writers").
|
||||
2. A body whose cut-link receiver is a non-fluid cell, e.g. a channel wall row
|
||||
or the inlet/outlet column ("Cut links must have active fluid receivers").
|
||||
|
||||
Both were previously accepted silently for passive bodies; the interpolated
|
||||
boundary then had undefined results. The rules apply to the cut links of all
|
||||
bodies together, so a passive body overlapping a Jet body is rejected as well.
|
||||
The check runs in `Simulation.initialize`, runtime body sync (`sync_bodies`),
|
||||
checkpoint save and checkpoint load (`common/checkpoint.py`). Load validates
|
||||
before restoring anything, so a checkpoint whose saved geometry violates either
|
||||
rule no longer restores; rebuild the geometry with separated bodies.
|
||||
|
||||
## 87c3a31: `Simulation.read_surface_exchange` accepts Jet-driven bodies
|
||||
|
||||
`read_surface_exchange(id)` previously required `SurfaceActuation` on the body.
|
||||
It now also accepts bodies that own Jet aperture links, including Jets with zero
|
||||
or negative amplitude (the aperture test is geometric). Bodies with neither
|
||||
`SurfaceActuation` nor aperture links still raise
|
||||
`ValueError("Body has no surface actuation")`. The value is unchanged: the latest
|
||||
step's wall/aperture exchange, not total nozzle force.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Selective LES normalization integration on dev8403956
|
||||
|
||||
This change reuses the reviewed correction from 5465832 without merging its
|
||||
branch history. The only production change is the two closed-form coefficients
|
||||
18 -> 18 sqrt(2) and their explanatory comments in turbulence_smag.cuh. Its SHA256
|
||||
is a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa.
|
||||
Defaults, iterative/deviatoric/capped closure, collision dispatch, AMR guards,
|
||||
and all other production source files are unchanged.
|
||||
|
||||
## Separate source contract; unchanged experiment
|
||||
|
||||
The single existing flow runner now accepts `--experiment dev8403956`.
|
||||
Its separate source_pins_dev8403956.json pins all 101 production files from the
|
||||
committed 8403956 baseline; corrected mode permits only the exact reviewed
|
||||
header substitution. Results record the actual checkout commit, import path,
|
||||
experiment name and manifest SHA256. No arbitrary manifest-path override exists.
|
||||
|
||||
Omitting the option still selects the original historical source manifest.
|
||||
That manifest, the original paired receipts, scalar preparation manifests and
|
||||
independent continuum oracle are unchanged. Historical baseline and corrected
|
||||
receipts remain evidence for their own exact sources, not new dev receipts.
|
||||
Historical guards intentionally reject this different dev source tree. Eight
|
||||
production files already differed between the historical source snapshot and
|
||||
8403956; neither the old manifest nor its acceptance is silently regenerated.
|
||||
|
||||
The physical experiment and every numerical acceptance threshold are unchanged:
|
||||
8 x 34 nodes, halfway y walls, periodic x, unforced D2Q9/SRT, shifted FP32,
|
||||
double-buffer streaming, Cs=0/.16/1, checkpoints 519/1038/2075. Each case has one
|
||||
actual advancing warm-up followed by complete initial-state restoration before
|
||||
READY. Total launches are 6,225 measured plus three warm-ups. Cs=1 remains an
|
||||
amplified diagnostic, not a physical coefficient recommendation.
|
||||
|
||||
## Host checks
|
||||
|
||||
PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s tests/unit -p 'test_smagorinsky_normalization.py' -v
|
||||
PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s tests/unit -p 'test_les_flow_*.py' -v
|
||||
|
||||
The direct-current-header test is copied unchanged from reviewed 5465832:
|
||||
120 tensor parameter cases, Cs=0/equilibrium and omega guards, converged
|
||||
trace-free iterative comparison, and mutation detection in both lattice
|
||||
algebras. This is scalar D2/D3 algebra coverage, not 3D-flow qualification.
|
||||
Flow tests retain analytic/reference checks and add current named-source,
|
||||
reverse-patched baseline, variant rejection, unrelated-source tampering and
|
||||
immutable historical-default checks. The temporary baseline is constructed
|
||||
only inside a test directory; production is never reverse-patched.
|
||||
|
||||
## Pending final-commit GPU gate
|
||||
|
||||
After independent review and explicit executor authorization, run from the
|
||||
exact committed candidate checkout, selecting the authorized physical GPU:
|
||||
|
||||
CUDA_VISIBLE_DEVICES=<physical-GPU> PYTHONDONTWRITEBYTECODE=1 python tests/validation/run_uniform_les_flow.py --root "$PWD" --experiment dev8403956 --variant corrected --run-gpu --device 0 --output <new-evidence-directory>/corrected.json
|
||||
|
||||
Retain all source hashes, exact commit/import/device identity, manifest hash,
|
||||
three READY/reset records, complete result JSON, stdout/stderr and actual exit.
|
||||
Require all frozen gates, DONE passed=true and exit 0. Compare with historical
|
||||
corrected evidence without relabeling it as current evidence. The old-vs-new
|
||||
normalization discrimination already exists; no new baseline GPU run is needed
|
||||
unless review finds a changed exercised uniform solver path.
|
||||
|
||||
No GPU result is claimed by this integration preparation. This does not expand
|
||||
LES qualification to MRT, forcing, AMR LES or three-dimensional turbulent flows.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Experimental projected G explosion
|
||||
|
||||
`FixedPatchStepper(..., explosion="projected_linear")` changes only the existing
|
||||
G-to-G explosion reconstruction. The omitted/default `"constant"` selector retains
|
||||
the same kernel, launch arguments and device allocations. Projected mode adds two
|
||||
compact tables per adjacent pair; recursion, collision, BFL, restriction, buffer
|
||||
ownership and synchronized publication are unchanged. `remap_child` preserves it.
|
||||
|
||||
Runtime eligibility is periodic-root, fully blocked D2Q9/SRT/FP32, body/Jet-free,
|
||||
without LES; the fixed-patch backend has no forcing interface. Shifted and unshifted
|
||||
storage use the same reconstruction. One generic builder handles scalar windows,
|
||||
unique union rows and all adjacent hierarchy pairs without depth/rectangle caps.
|
||||
Donors are original explosion donors intersected with owned, uncovered ACTIVE
|
||||
cells. An axis uses centered differences only if both neighbors are legal;
|
||||
otherwise both addresses repeat the center. Nested layout periodicity is explicit.
|
||||
No covered cells, holes, inactive cells or stale halo copies enter the stencil.
|
||||
|
||||
The 33-line kernel is unchanged from our `519473a` experiment and retains its
|
||||
mathematical attribution. Host tests cover union decomposition, concavity, narrow
|
||||
gaps, grazing packet paths, offsets, sparse/nested owner addresses, three/four-level
|
||||
source timing, unchanged constant dispatch, remapping and shifted/unshifted actual
|
||||
kernel arithmetic compiled as host C++. These are not CUDA or flow validation.
|
||||
|
||||
## Reviewed bounded evidence
|
||||
|
||||
The `1aa8110` CUDA gate, with production exactly `a135252`, passed exact-dev
|
||||
omitted/default and explicit-constant equivalence, per-population reconstruction,
|
||||
scalar/union/four-level scheduling, alternate layouts, and changed remapping in
|
||||
both storage modes. Operator maximum error was 2.98023224e-8; the eight-step
|
||||
concave replay maximum was 1.58037614e-7. Whole-step mass/momentum remain
|
||||
finite diagnostics, not an FP32 conservation certificate. Reviewed evidence:
|
||||
`7aa66ff0542175092753ea099f40b903ece0527e`.
|
||||
|
||||
The fresh four-case `2cf9853` N128/256 Taylor–Green repeat retained all `11901a7`
|
||||
settings and gates. All 20 main scalar rows and 260 early-pressure samples matched
|
||||
the older receipts exactly; this is not field-array equality. Projected worst
|
||||
velocity was 0.001995299260844 versus 0.002 (only 0.235% margin), pressure
|
||||
0.012157174904932 and energy 0.001184362645162. Constant velocity
|
||||
0.004418076615223 failed its gate, so the aggregate verdict remains false.
|
||||
The projected early sampled pressure peak was 0.021621453573334; it remains a
|
||||
descriptive sampled maximum. Reviewed evidence: `90849a55cad4a0da9b72747da7bfe6eb75962a44`.
|
||||
|
||||
The old N64 projected failures remain relevant. These results support only this
|
||||
bounded plumbing gate and short periodic Re100 smooth-decay experiment. They do
|
||||
not establish union/multilevel physics accuracy, convergence, long-time stability,
|
||||
body/Jet, boundary, forcing, MRT or LES suitability. Constant remains the default.
|
||||
|
||||
## Separate LES source profile
|
||||
|
||||
The two older LES source manifests and the historical default are unchanged and
|
||||
correctly reject this different source tree. The existing LES runner now offers
|
||||
`--experiment a135252`, pinning all 101 actual committed corrected source files.
|
||||
Its explicit baseline-header override describes only a synthetic isolated reverse
|
||||
control, never the committed a135252 header. The unchanged corrected-only uniform
|
||||
LES regression (Cs=0/.16/1) is the remaining integration gate; no LES result is
|
||||
inherited from the projected evidence. The separate historical jet-CV `channel.py`
|
||||
pin mismatch predates this port and is not addressed here.
|
||||
@@ -0,0 +1,34 @@
|
||||
# MRT reuse in blocked/refined stepping (candidate)
|
||||
|
||||
Base `9805383`. Opt-in by `LBMConfig(collision="MRT")`; SRT remains the default and its
|
||||
kernel statements, including the shifted retained-delta/direct-momentum path, are unchanged
|
||||
under the alternate compile branch.
|
||||
|
||||
## What changes
|
||||
- `blocks.cu` (`BlockStep`), `step/block_channel.cu` (`BlockChannelStep`) and `refinement.cu`
|
||||
(`patch_collide`) gain a compile-time `COLLISION_MODEL == 2` branch that repeats the uniform
|
||||
`collide_dispatch` sequence with zero forcing: `compute_rho_u`, omega clamp to
|
||||
`[OMEGA_COLLISION_MIN, OMEGA_COLLISION_MAX]`, `collide_mrt_no_force`.
|
||||
- Kernel and host guards accept SRT or MRT only. TRT, LES, non-FP32 storage, non-double-buffer
|
||||
streaming, and shifted dense/mixed refinement remain rejected.
|
||||
- Transfers, subcycling, BFL, raw exchange ledgers and channel closures are untouched. The
|
||||
existing pressure-NEQ outlet selects its MRT branch (unknowns-only NEQ extrapolation) from
|
||||
`COLLISION_MODEL`, so MRT channel parity must use uniform MRT as reference.
|
||||
|
||||
## Contracts (existing operator, not redefined here)
|
||||
- Shifted storage uses the existing MRT representation (equilibrium moments from `rho - 1`),
|
||||
not the SRT retained-delta/direct-momentum precision. Mass and momentum are conserved
|
||||
moments (relaxation rate 0).
|
||||
- Kinetic rates are the operator's fixed `s_e = s_eps = s_q = 1.2` on every level; the shear
|
||||
rate is the existing per-level omega. Under acoustic 2:1 scaling the physical bulk viscosity
|
||||
therefore halves on each finer level. No rate rescaling is applied.
|
||||
- No LES, no external forcing (`d_params` force fields stay zero).
|
||||
- Shifted Jets stay SRT-only: `body/jets.py` keeps its existing guard. This extension does not
|
||||
provide shifted MRT Jets.
|
||||
- `PeriodicBlockStepper` (uniform blocked stepper) remains SRT-only.
|
||||
|
||||
## Evidence status
|
||||
CPU: `tests/unit/test_refinement_mrt.py` (host guards, source contract). GPU:
|
||||
`tests/validation/multigrid/test_refinement_mrt_gpu.py` (uniform-MRT channel parity, AMR MRT
|
||||
smoke) and unchanged SRT GPU tests. Strict parity may be subject to cross-kernel FMA
|
||||
contraction differences already documented for SRT. Not a DFG/benchmark validation.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Blocked AMR root channel increment
|
||||
|
||||
Opt in with `FixedPatchStepper(..., coarse_boundary="channel", refinements=[bounds], level_blocks=[(by,bx), (fy,fx)])`. Every level must be blocked. The supported boundary is the existing uniform D2Q9/SRT/FP32 double-buffer channel: local Zou-He west inlet, pressure-NEQ east outlet, stationary halfway bounce-back y walls. Uniform and parabolic inlet profiles and shifted/unshifted storage are supported; the existing configuration guards still reject MRT/LES and incompatible storage. This does not change the external DFG MRT reference.
|
||||
|
||||
`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`:
|
||||
|
||||
- y-edge ghosts (including corners): pull, pair swap, no collision
|
||||
- west ghosts at interior y: Zou-He reconstruction then SRT collision
|
||||
- east ghosts at interior y: pressure NEQ from the immutable left input node, no collision
|
||||
- physical fluid beside y walls: same-node opposite-input correction then collision
|
||||
|
||||
The root uses the uniform SRT equilibrium arithmetic, including the existing shifted rho-minus-one path, to preserve matched-root semantics. Fine `BlockStep` retains its existing shifted direct-momentum/retained-density arithmetic. This increment is not a migration of fine arithmetic.
|
||||
|
||||
Root halo topology stays wrapped like the uniform kernel, solely to reproduce its complete evolved ghost state as well as physical populations. It does not make the physical inlet/outlet periodic. Evolved root state includes boundary ghosts, while public fluid-leaf downloads exclude them. Initialization must provide finite positive-density root ghost states. Private raw snapshots preserve them exactly for same-representation continuation; public leaf-only downloads are not restart states. Channel remapping remains explicitly unsupported.
|
||||
|
||||
`channel.py` checks the actual explosion donors and restriction receivers are uncovered bulk fluid and checks actual receiver-local restriction slots against pressure-donor and wall-correction reads. No arbitrary large padding or block-aligned patch restrictions are added. Keep the physical domain dimensions exact: choose legal root block divisors (including full width) instead of padding a benchmark domain. Stationary circles/Jets retain the existing finest-level and transfer-footprint rules.
|
||||
|
||||
## Gates
|
||||
|
||||
- `tests/unit/test_channel_root.py`: host semantics, minimal footprint, overlap rejection, raw-state/public-leaf separation; actual production uniform and blocked kernels under a CPU launch shim, five non-equilibrium steps, all corners, profiles, representations, y seams and a one-cell-wide block outlet donor.
|
||||
- `tests/validation/multigrid/test_channel_root_gpu.py`: exact uniform-versus-blocked full-state GPU parity; AMR finite-state and block-split invariance with/without a stationary circle and velocity Jet; amplitude update and raw snapshot round-trip.
|
||||
- GPU tests require explicit device admission by the caller. CPU-header algebra is not CUDA execution or external physical validation. The current increment does not establish DFG drag/lift/pressure acceptance or a performance speedup; those need matched SRT controls and a separate benchmark run.
|
||||
@@ -1,23 +1,26 @@
|
||||
# Fixed three-level separated patches
|
||||
# Fixed three-level rectangle unions
|
||||
|
||||
`FixedPatchStepper` supports exactly three static, blocked levels with separated
|
||||
rectangle sets on either adjacent pair. This is not general AMR: there is no
|
||||
per-region timestep, moving geometry, dynamic refinement or L1 body support.
|
||||
This page describes three-level `FixedPatchStepper` setups built from the
|
||||
`refinements` level list, with rectangle unions on either adjacent pair. The
|
||||
same constructor supports arbitrary depth; three or more levels require
|
||||
all-blocked storage and bodies/Jets only on the finest level. This is not
|
||||
general AMR: there is no per-region timestep, moving geometry, automatic
|
||||
dynamic refinement or L1 body support in the three-level form.
|
||||
All regions share one current/spare buffer pair per level. The existing schedule
|
||||
advances L0 once, L1 twice and L2 four times per base step, with aggregate transfers.
|
||||
No CUDA kernel changes are involved.
|
||||
|
||||
## Explicit coordinate frames
|
||||
|
||||
Keep the existing scalar `child=(x0,y0,x1,y1)` for a single scalar parent patch.
|
||||
Its coordinates retain their original meaning: half-open integer indices in the
|
||||
allocated L1 window, including its ghost band.
|
||||
For a single scalar parent patch, pass a scalar rectangle as `refinements[1]`:
|
||||
`(x0,y0,x1,y1)` in half-open integer indices of the allocated L1 window,
|
||||
including its ghost band.
|
||||
|
||||
The new keyword `child_rectangles_in_parent=[...]` is mutually exclusive with
|
||||
`child`. It accepts a scalar or a set parent and interprets every rectangle in
|
||||
the actual allocated L1 array, including ghost bands and holes. It never infers
|
||||
region-local coordinates. The physical root cell edge corresponding to an L1
|
||||
index `(x,y)` is `parent.fine_origin + (x,y)/2`:
|
||||
A rectangle list as `refinements[1]` accepts a scalar or a set parent and
|
||||
interprets every rectangle in the actual allocated L1 array, including ghost
|
||||
bands and holes. It never infers region-local coordinates. The physical root
|
||||
cell edge corresponding to an L1 index `(x,y)` is
|
||||
`parent.fine_origin + (x,y)/2`:
|
||||
|
||||
- A scalar L1 parent has a shifted window origin.
|
||||
- A set L1 parent has origin `(0,0)` and public shape `(2*ny,2*nx)`.
|
||||
@@ -30,20 +33,23 @@ are physically equivalent to parent set `[(4,4,12,12)]` with child set
|
||||
`[(8,8,16,16)]`. Reusing the same numbers after changing the parent frame can
|
||||
move the physical region.
|
||||
|
||||
Both adjacent pairs require a Chebyshev gap of at least four in their own
|
||||
parent-cell units. The whole child union must preserve inherited ACTIVE
|
||||
coverage, uncovered ACTIVE donors/receivers, surviving pulls, and the upstream
|
||||
Both adjacent pairs are built from their unions, without a fixed rectangle-gap
|
||||
rule. Adjacent, overlapping and concave unions are accepted only when their
|
||||
actual transfer dependencies close. The whole child union must preserve
|
||||
inherited ACTIVE coverage, uncovered ACTIVE donors/receivers, surviving pulls, and the upstream
|
||||
restriction footprint. Sparse holes remain unowned; inherited INNER remains
|
||||
stream-only and OUTER remains invalid. Block sizes must align with every
|
||||
support edge, including its four-cell ghost band.
|
||||
stream-only and OUTER remains invalid. Block sizes must divide their allocated
|
||||
level grids. Sparse storage owns every grid-aligned tile intersecting the union
|
||||
plus its four-cell ghost band; rectangle and support edges need not align with
|
||||
tile edges.
|
||||
|
||||
## Example
|
||||
|
||||
```python
|
||||
run = FixedPatchStepper(
|
||||
cfg, [(4, 4, 20, 28), (24, 4, 44, 28)], context=ctx,
|
||||
base_block=(4, 4), fine_block=(4, 4), child_block=(4, 4),
|
||||
child_rectangles_in_parent=[(16, 16, 32, 40), (56, 16, 80, 40)],
|
||||
cfg, context=ctx,
|
||||
refinements=[[(4, 4, 20, 28), (24, 4, 44, 28)], [(16, 16, 32, 40), (56, 16, 80, 40)]],
|
||||
level_blocks=[(4, 4)] * 3,
|
||||
)
|
||||
```
|
||||
|
||||
@@ -51,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
|
||||
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
|
||||
positions, not region IDs or caller `obj_id`. Complete boundary stencils must
|
||||
avoid the transfer footprint. Change a child Jet with
|
||||
@@ -65,6 +71,12 @@ CPU geometry, table, pre-CUDA rejection, schedule and cleanup tests:
|
||||
PYTHONPATH=src python -m unittest discover -s tests/unit -p 'test_refinement*.py' -v
|
||||
```
|
||||
|
||||
`tests/unit/test_union_refinement.py` covers union dependencies and tile
|
||||
ownership; `tests/unit/test_hierarchy.py` covers composed geometry through four
|
||||
levels. `tests/unit/test_raw_exchange.py` checks the four-level host schedule
|
||||
with CUDA mocks. These establish topology/control-flow coverage, not numerical
|
||||
qualification at every depth or interface shape.
|
||||
|
||||
Explicit GPU gates are in
|
||||
`tests/validation/multigrid/test_refinement_three_level_multipatch_gpu.py`.
|
||||
They cover physical-frame equivalence, twins, asymmetric refinement, multiple
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Opt-in shifted blocked candidate
|
||||
|
||||
This candidate is based on fbe7377. The default remains unshifted. Enable with
|
||||
`LBMConfig(ddf_shifting=True)` only on the supported routes below.
|
||||
|
||||
| Route | Candidate support |
|
||||
|---|---|
|
||||
| PeriodicBlockStepper, FP32 D2Q9 SRT double-buffer, no LES | shifted or unshifted |
|
||||
| FixedPatchStepper, every level blocked and same representation | shifted or unshifted |
|
||||
| Passive stationary circular BFL boundary and raw exchange | supported candidate scope |
|
||||
| Synchronized three-level child remap | private raw shifted snapshot, exact overlap/copy |
|
||||
| Dense root/refinement or mixed representation | rejected for shifted |
|
||||
| Slot/reservoir, surface actuation, velocity Jet | existing public shifted exclusion retained |
|
||||
| Other collision/lattice/storage/streaming/LES combinations | existing exclusions retained |
|
||||
|
||||
The D2Q9 equilibrium helper accepts a retained density offset only in shifted
|
||||
compilation. BlockStep computes delta=sum(g) before adding the unit density;
|
||||
velocity uses the existing macro calculation. The existing SRT collision and
|
||||
all unshifted expressions remain. No global precision type or duplicate
|
||||
collision implementation is introduced. Ordinary shifted Simulation callers
|
||||
still use the existing rho−1 overload; this candidate does not change their
|
||||
numerical method.
|
||||
|
||||
Public initialize receives physical populations and performs shifted subtraction
|
||||
in FP64 before the one FP32 storage cast. Public shifted download returns FP64
|
||||
physical populations with exact-rational D2Q9 background weights. Internal raw
|
||||
snapshots retain FP32 g and a representation tag; runtime remap never decodes
|
||||
and re-encodes overlap. Exact mass is interpreted as 1+sum(g), and momentum as
|
||||
sum(c*g); analyses should use raw values for tiny perturbation budgets. The
|
||||
FP32 BFL force constants have their own arithmetic rounding; no exact global
|
||||
conservation claim follows from shifting.
|
||||
|
||||
Planar helper formulas include their shifted reference-density offsets and have
|
||||
actual-header host tests. Their public shifted route remains excluded until a
|
||||
separate test-gated migration. Next velocity-Jet work must validate physical
|
||||
wall density, incoming/outgoing populations, raw force/mass, ownership and
|
||||
transfer interactions before relaxing that restriction.
|
||||
|
||||
Evidence so far: selected-cell CUDA replay favored retained-delta shifting;
|
||||
this is not full-solver validation. Host algebra/storage/remap tests and staged
|
||||
GPU continuation, three-grid cylinder physics, raw budgets, Couette helper
|
||||
stress and measured runtime are separate gates. Original failed experiments
|
||||
remain evidence; no acceptance threshold is relaxed.
|
||||
|
||||
## Direct-input-momentum equilibrium candidate
|
||||
|
||||
The shifted BlockStep now constructs the linear equilibrium term from input
|
||||
momentum j, without reconstructing rho*u. For opposite axis pairs the odd term
|
||||
is j/3; for diagonal pairs it is (jx +/- jy)/12, evaluated with explicit
|
||||
round-to-nearest division. The even term is
|
||||
w*[delta + 4.5*(c dot j)*(c dot u) - 1.5*(j dot u)].
|
||||
This is the same second-order D2Q9 equilibrium in exact arithmetic. Original SRT,
|
||||
storage, boundaries, and transfers are unchanged. No collision residual or force
|
||||
is corrected afterward. Existing four/five-argument equilibrium APIs are retained;
|
||||
only shifted blocked collision calls the new six-argument momentum helper.
|
||||
|
||||
The all-fluid read-only CUDA replay reduced signed x-momentum error by 120–2285x
|
||||
at five sampled states, with lower mass/momentum and second-moment RMS errors.
|
||||
Signed mass bias was larger (~1e-12/cell/hop), so full evolving mass budgets remain
|
||||
mandatory. This local evidence does not establish physical convergence or speed.
|
||||
The unshifted default and its arithmetic path remain unchanged.
|
||||
@@ -0,0 +1,87 @@
|
||||
# Supported settings: 2D Jet + fixed multilevel refinement (candidate c95a7ba)
|
||||
|
||||
Every entry names the guard that enforces it (file:line at c95a7ba) and, where one
|
||||
exists, an existing test that exercises it. A setting not listed here is not
|
||||
claimed. Observations at the end are tied to the exact case in which they were
|
||||
measured; they are not general bounds.
|
||||
|
||||
## Lattice, storage, collision
|
||||
|
||||
| Setting | Supported | Guard |
|
||||
|---|---|---|
|
||||
| Dimension / lattice | 2D D2Q9 only (3D refinement is future work) | `lbm/refinement.py:65`, `lbm/kernels/blocks.cu:25`, `lbm/kernels/refinement.cu:26` |
|
||||
| Precision / streaming | FP32 storage, double buffer | same guards |
|
||||
| Collision on refinement levels | SRT or MRT; TRT and LES rejected | `lbm/refinement.py:65` (re-checked for fine boundaries at `lbm/refinement.py:457`) |
|
||||
| Shifted (ddf_shifting) storage | only when every level is blocked; dense or mixed rejected | `lbm/refinement_stepper.py:198`, `:469`; `lbm/kernels/refinement.cu:26` (dense patch kernel unshifted only) |
|
||||
| Fine relaxation | 1/tau_fine must lie inside the configured omega guards | `lbm/refinement.py:80` |
|
||||
| Uniform blocked backend `PeriodicBlockStepper` | doubly periodic, SRT only, stationary passive circles | `lbm/block_stepper.py:84`, `:91`, `:45`-`:67` |
|
||||
|
||||
Tests: `tests/unit/test_refinement_mrt.py`, `tests/validation/multigrid/test_refinement_mrt_gpu.py`,
|
||||
`tests/unit/test_shifted_storage.py`, `tests/unit/test_shifted_block_operators.py`.
|
||||
|
||||
## Refinement hierarchy (`FixedPatchStepper(cfg, *, context, refinements, ...)`)
|
||||
|
||||
| Setting | Supported | Guard |
|
||||
|---|---|---|
|
||||
| Depth | at least one refinement level; one entry per level in `level_blocks`/`level_objects`/`level_jets` | `lbm/refinement_stepper.py:72`, `:75` |
|
||||
| Single L1 rectangle | two-coarse-cell margin to the domain edge | `lbm/refinement.py:73` |
|
||||
| Several L1 rectangles (union) | require `level_blocks[0]` and `level_blocks[1]`; transfers must close (dependency checks, no fixed gap rule) | `lbm/refinement_stepper.py:84`; `lbm/refinement.py:283`-`:295` |
|
||||
| Nested levels | depth >= 3 must be all blocked; a blocked level requires blocked levels below; a scalar child needs a scalar parent | `lbm/refinement_stepper.py:82`, `:87`; `lbm/hierarchy.py:54` |
|
||||
| Nested transfer support | child only over inherited ACTIVE cells, donors/receivers uncovered ACTIVE, no child over the parent restriction footprint | `lbm/refinement.py:391`-`:407` |
|
||||
| Tile shape | positive `(by, bx)`; the level allocation must be divisible by the tile (no partial tiles yet) | `lbm/refinement_stepper.py:78`, `lbm/refinement.py:246` (shape); `lbm/blocks.py:134` (divisibility) |
|
||||
| Coarse boundary | `periodic`, `slot_reservoir` or `channel` | `lbm/refinement_stepper.py:177` |
|
||||
| `slot_reservoir` | on-site velocity bottom slot, reservoir top, velocity 0, 2 <= slot width < nx, patch rows 4..ny-4 | `lbm/refinement_stepper.py:205`, `:212`; `lbm/refinement.py:546` |
|
||||
| `channel` | all levels blocked; Zou-He inlet, pressure-NEQ outlet, stationary bounce-back walls; transfers away from the closure | `lbm/channel.py:22`, `:26`, `:43`, `:54` |
|
||||
| Explosion | `constant` (default); `projected_linear` only periodic, all-blocked SRT, no bodies/Jets, no LES (experimental) | `lbm/refinement_stepper.py:188`, `:193` |
|
||||
| L2 runtime remap (`remap_child`) | periodic three-level L2 sets only, no L0/L1 bodies, identical pinned boundary | `lbm/refinement_stepper.py:557`, `:567`-`:581` |
|
||||
|
||||
Tests: `tests/unit/test_refinement_three_level_multipatch.py`, `tests/validation/multigrid/test_refinement_multipatch.py`,
|
||||
`test_refinement_three_level.py`, `test_refinement_three_level_multipatch_gpu.py`, `test_refinement_blocked_base.py`,
|
||||
`test_refinement_blocked_fine.py`, `test_channel_root_gpu.py`, `tests/unit/test_channel_root.py`,
|
||||
`tests/unit/test_projected_pair_stepper.py`, `test_runtime_child_remap_gpu.py`, `test_pinned_body_remap_gpu.py`.
|
||||
|
||||
## Bodies and Jets on refined levels
|
||||
|
||||
| Setting | Supported | Guard |
|
||||
|---|---|---|
|
||||
| Bodies | stationary circles without sensor/force-region roles, entirely inside the patch union, fully resolved, disjoint, stencil away from the two-hop transfer footprint | `lbm/refinement.py:474`-`:542` |
|
||||
| Level | not on L0; only on the finest level of a nested hierarchy | `lbm/refinement_stepper.py:80`, `:89` |
|
||||
| Jet condition | velocity only; `pressure` is reserved and raises NotImplementedError | `body/jets.py:148`, `:150` |
|
||||
| Jet geometry | each Jet selects resolved links of exactly one stationary body; no Jet/Jet or Jet/SurfaceActuation overlap; abs(amplitude) < 1 lattice | `body/jets.py:155`, `:175`, `:177`, `:255`, `:261`, `:263` |
|
||||
| Jets with shifted storage | only in the all-blocked stepper with SRT and no LES | `body/jets.py:249` |
|
||||
| SurfaceActuation | unshifted storage only | `body/objects.py:155` |
|
||||
| Raw boundary exchange (force) | blocked body levels only; the window ledger requires `observe_raw_exchange` | `lbm/refinement_stepper.py:264`, `:269` |
|
||||
| Checkpoint with Jets | refused | `simulation.py:687` |
|
||||
|
||||
Raw exchange is the cut-link momentum reaction (wall + aperture), not a validated
|
||||
nozzle thrust or stress load. Jet amplitude is a lattice velocity, not a flow rate.
|
||||
|
||||
Tests: `tests/unit/test_jets.py`, `test_jets_review.py`, `test_shifted_jets.py`, `test_raw_exchange.py`,
|
||||
`tests/validation/multigrid/test_refinement_jets.py`, `test_shifted_jets_gpu.py`, `test_raw_exchange_gpu.py`,
|
||||
`test_exchange_window.py`, `test_fine_surface_gpu.py`.
|
||||
|
||||
## Observations from specific tested cases (not general bounds)
|
||||
|
||||
- SRT channel root vs uniform OneStep, strict-parity test
|
||||
`tests/validation/multigrid/test_channel_root_gpu.py::ChannelRootGPU::test_uniform_matched_root`:
|
||||
in one of its 18 sub-cases (2 storage modes x 3 profile/clamp/damp settings x 3 blocks; which
|
||||
one is not recorded), 1 of the 4320 compared values differs, max abs 2.33e-10, max rel 1.77e-7
|
||||
(about 2 ULP). The test remains failed; its cause and any general bound are unresolved.
|
||||
- MRT root parity test
|
||||
`tests/validation/multigrid/test_refinement_mrt_gpu.py::RefinementMRTGPU::test_uniform_matched_mrt_root`
|
||||
(12 sub-cases: 2 storage modes x 2 profiles x 3 blocks, default outlet settings) passed bitwise
|
||||
in the Horsea focused GPU run at K1 7813334 (2026-10-21).
|
||||
- Periodic Taylor-Green Re 100, measured at 36ffc8b1 (V3 matched pair, N128 depth 2):
|
||||
every per-sample screen passed; the constant explosion's aggregate worst velocity L2,
|
||||
0.004418, exceeds its preregistered 0.002 budget, while projected_linear passed that budget.
|
||||
- Curved-wall pressure (pilot harness, exact-q analytic annulus, tau 0.8): the coarse swirl r1
|
||||
case exceeded its terminal density-gain cap (1.105e-4 vs 1e-4), so the frozen 9-case matrix
|
||||
is NOT_ESTABLISHED. No validated fix exists. This is a pilot harness, not production code.
|
||||
- DFG 2D-1 Re 20 (V4, SRT, t* 60, pressure-NEQ outlet and local Zou-He inlet): refined and uniform
|
||||
runs were NOT_STEADY under the frozen steady rule, so no interval verdict was given.
|
||||
|
||||
## Not supported in this release
|
||||
|
||||
3D, dynamic/automatic AMR (tagging, time-varying hierarchies beyond `remap_child`),
|
||||
moving bodies on refined levels, pressure Jets, Jet checkpoints, LES or TRT on refined
|
||||
levels, partial (non-dividing) tiles.
|
||||
@@ -1,11 +1,15 @@
|
||||
# Synchronized leaf G remap and manual continuation
|
||||
|
||||
This is a CPU state-transfer component, **not a runtime adaptive solver**.
|
||||
It changes only a separated L2 rectangle set in a three-level, body-free,
|
||||
periodic hierarchy. L0/L1 topology, relaxation parameters, direction order,
|
||||
It changes only an L2 rectangle union in a three-level periodic hierarchy.
|
||||
The body-free case is shown first; stationary L2 bodies/Jets follow the bounded
|
||||
support contract below. L0/L1 topology, relaxation parameters, direction order,
|
||||
physical frame and completed time remain unchanged. Existing geometry checks
|
||||
validate child support/nesting and separation. The CPU helper itself adds no CUDA, scheduler, allocator, boundary handling,
|
||||
collision or ghost refill. A bounded runtime adapter is described below.
|
||||
validate child support/nesting and actual transfer dependencies, without a fixed
|
||||
rectangle-gap rule. The three-level remap limit is unchanged by the stepper's
|
||||
arbitrary-depth level-list support. The CPU helper itself adds no CUDA,
|
||||
scheduler, allocator, boundary handling, collision or ghost refill. A bounded
|
||||
runtime adapter is described below.
|
||||
|
||||
```python
|
||||
from CelerisLab.lbm.refinement import FixedPatchSet2D
|
||||
@@ -29,11 +33,13 @@ origin is retained. Physical lower edges are `origin + spacing*(x,y)` on L0,
|
||||
`origin + spacing*(parent.fine_origin + (x,y)/4)` on L2.
|
||||
|
||||
Factory options `coarse_boundary` and `objects` must declare `"periodic"` and
|
||||
an empty body collection (all levels). LBMConfig's channel settings do not
|
||||
select this operator's boundary mode. Geometry metadata is factory-created;
|
||||
an empty collection; L2 bodies use `child_objects` as described below.
|
||||
LBMConfig's channel settings do not select this operator's boundary mode.
|
||||
Geometry metadata is factory-created;
|
||||
manual mutation/forging of metadata is unsupported. Caller certifies the
|
||||
actual state has no bodies/forcing, that all device work and old readers are
|
||||
quiescent, and that inputs are the **current owned-leaf postcollision G**.
|
||||
actual state matches the declared boundary and has no forcing, that all device
|
||||
work and old readers are quiescent, and that inputs are the **current owned-leaf
|
||||
postcollision G**.
|
||||
Count/phase stamps check consistency, not freshness or active-buffer identity.
|
||||
|
||||
`current_g` is three NumPy Q-major arrays `(9,ny,nx)`. For blocked inputs,
|
||||
@@ -73,9 +79,10 @@ PYTHONPATH=src python -m unittest discover -s tests/unit -p 'test_synchronized_r
|
||||
|
||||
`replacement = current.remap_child(rectangles)` returns a new ready
|
||||
`FixedPatchStepper` at the **same absolute step_count**. It supports only
|
||||
body-free periodic three-level runs whose existing L2 uses
|
||||
`child_rectangles_in_parent`. Scalar or union L1 parents are supported; scalar
|
||||
`child=` is deliberately rejected. L0/L1, relaxation and block sizes stay fixed.
|
||||
periodic three-level runs with no L0/L1 bodies and an existing L2
|
||||
`FixedPatchSet2D`; stationary L2 bodies/Jets follow the support contract below.
|
||||
Scalar or union L1 parents are supported; scalar `child=` is deliberately
|
||||
rejected. L0/L1, relaxation and block sizes stay fixed.
|
||||
No tagger, scheduler, in-place replacement, or general rollback is provided.
|
||||
|
||||
The method holds the stepper lock, validates geometry, synchronizes its stream,
|
||||
@@ -113,13 +120,14 @@ Per-population conservation is not an advancement target under collision.
|
||||
### Bounded pinned-body extension
|
||||
|
||||
`replacement = current.remap_child(rectangles, child_objects=original_objects)`
|
||||
also accepts stationary Circles in separated L2 rectangles. Every body’s
|
||||
**complete containing rectangle must remain identical**. Several bodies may
|
||||
share one pinned rectangle; there need not be any fluid-only rectangles.
|
||||
Separated fluid-only rectangles may be added, removed or moved. Existing
|
||||
nesting, four-cell gap, full-circle containment, reverse cut-link coverage and two-hop BFL
|
||||
transfer-footprint checks still apply. This is manual remapping, not adaptive
|
||||
body tracking or runtime body editing.
|
||||
also accepts stationary Circles inside the L2 union. Each body's **physical
|
||||
boundary, complete BFL support and current per-link velocity must remain
|
||||
identical**. Rectangles are not pinned: they may be split, merged or resized,
|
||||
and other fluid coverage may change, including within the same connected union,
|
||||
provided that physical support is unchanged. Existing nesting, union transfer
|
||||
dependencies, full-circle containment, reverse cut-link
|
||||
coverage and two-hop BFL transfer-footprint checks still apply. This is manual
|
||||
remapping, not adaptive body tracking or runtime body editing.
|
||||
|
||||
Supply all original root edge-frame definitions in their construction order.
|
||||
Do not pass `current.child_boundary.objects`: those are projected fine-local copies.
|
||||
@@ -128,8 +136,8 @@ state, remapping reprojects the supplied definition onto the old topology and
|
||||
compares exact ordered circle centers/radii, flags, physical cut links, body
|
||||
IDs, q, lever arms, hits, BFL support,
|
||||
legacy SurfaceActuation specification and current per-link velocity against
|
||||
the existing boundary. Omission, changed geometry/actuation and changed body
|
||||
rectangle fail without device reads or allocations; reordered bodies fail
|
||||
the existing boundary. Omission, changed geometry/actuation or changed physical
|
||||
boundary/BFL support fail without device reads or allocations; reordered bodies fail
|
||||
too. Root object IDs are still assigned by list position, as in the constructor.
|
||||
|
||||
Jets are taken from the live L2 boundary, including the most recent successful
|
||||
@@ -141,12 +149,16 @@ boundary and velocities must match exactly.
|
||||
typed `FineBoundary2D`; it derives L2 fluid leaves as ACTIVE minus SOLID. No
|
||||
caller mask is accepted. Solids, ghosts and holes are never donors and remain
|
||||
NaN, physical-fluid overlap is exact, and exposed L1 still restricts all four
|
||||
old fluid children. The helper checks the old/new body pins and boundary too.
|
||||
Its immutable `pinned_rectangles` is a canonical, deduplicated tuple, empty for body-free layouts; rectangle input
|
||||
order does not affect it. Scalar-parent window offsets remain part of the
|
||||
coordinate conversion.
|
||||
old fluid children. The helper checks old/new physical boundary and velocity
|
||||
equality too. Its immutable `pinned_rectangles` is legacy containment metadata,
|
||||
not a remap eligibility constraint; it may be empty for a seam-spanning body.
|
||||
Scalar-parent window offsets remain part of the coordinate conversion.
|
||||
|
||||
Focused CPU mocks: `tests/unit/test_pinned_body_remap.py`. GPU declarations:
|
||||
Focused CPU mocks: `tests/unit/test_pinned_body_remap.py`.
|
||||
`tests/unit/test_union_body_support.py` also checks seam-spanning circles,
|
||||
rectangle repartition/resize, unchanged physical support and live-Jet
|
||||
preservation. These host checks do not establish GPU evolution accuracy.
|
||||
GPU declarations:
|
||||
`tests/validation/multigrid/test_pinned_body_remap_gpu.py`, covering no-op
|
||||
next-step bitwise identity, moved-fluid A→B→A, current-amplitude preservation,
|
||||
physical boundary/velocity equality, source independence and absolute counts.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Bounded 2D implementation and validation scope
|
||||
|
||||
This inventory starts from dev `8403956a9afc0b0bb89265cec374392013d89016` and
|
||||
integrates only the reviewed LES normalization production correction from
|
||||
`95cb6d2b9f43c1bdd46cbf27f03f0dfc0877cfb0`. The merged production bytes match that
|
||||
tested candidate exactly; the scope-test changes are retained from `53fad767`.
|
||||
GPU receipts remain associated with their exact tested commits, not relabeled as
|
||||
a new merged-commit run. Historical source identities and gates remain unchanged.
|
||||
|
||||
## Implementation boundaries
|
||||
|
||||
- Uniform D2Q9 supports SRT, TRT and MRT. An available collision selector does not
|
||||
qualify every wall, inlet/outlet, forcing, storage or LES combination.
|
||||
- Fixed refinement supports SRT/MRT, FP32 and double-buffer streaming; TRT and LES
|
||||
are rejected. Shifted refinement requires all-blocked storage. Refined channel
|
||||
boundaries have narrower configuration guards than uniform boundaries.
|
||||
- Uniform checkpoints support compatible same-method continuation. Changing
|
||||
collision or LES parameters is a different evolution, not exact continuation.
|
||||
Jet checkpoints and refined checkpoints are unsupported; synchronized remapping
|
||||
is not persistent restart.
|
||||
- Keep these conclusions 2D. Local D3Q19 operator tests do not establish a supported
|
||||
3D Simulation runtime.
|
||||
|
||||
## Evidence and remaining gaps
|
||||
|
||||
| Area | Available evidence | Boundary of the conclusion |
|
||||
|---|---|---|
|
||||
| MRT cylinder flow and observation windows | Full matched uniform/refined MRT DFG and the window-ledger campaign passed their frozen periodicity, reference and matched-comparison gates. The window campaign used `185a919`; its MRT/no-LES numerical path remains unchanged. Current dev also contains independently scoped test/documentation changes and the LES correction, which do not extend MRT qualification. | This does not qualify other collision/boundary combinations or every refinement interface. Timing improvements are not additional accuracy evidence. |
|
||||
| Uniform forced channel | Six Poiseuille GPU cases on `8403956`: SRT/TRT/MRT at wall-normal resolutions H16/H32 passed. | This is the tested analytic channel configuration, not open-boundary or all-layout qualification. |
|
||||
| Uniform restart | Ten SRT/MRT checkpoint GPU tests passed on `8403956`; the separate representative TRT same-method double-buffer continuation GPU case also passed (one test). | This covers the tested same-method continuation configurations. No LES, Jet or refined checkpoint claim follows. |
|
||||
| Taylor–Green refinement | Independent topology checks are available. The actual-rate phase GPU regression passed one test across two layouts, with errors 1.584e-7 and 4.500e-9. | The physical interface-accuracy gap remains open. Passing topology and phase checks does not establish convergence or close that accuracy gate. |
|
||||
| Porous Couette and annulus | Prior campaign evidence exists and must remain associated with its original setup and source. | The reconstructed annulus is a validation helper, not qualification of a public boundary-condition API. Do not infer absence of evidence from a missing current rerun. |
|
||||
| LES normalization | The reviewed correction is integrated. Exact `95cb6d2` GPU validation passed all nine observations and frozen gates, with three warm-up/reset records, verified 101-source/import identity, DONE and exit 0. Last two SGS decrement relative errors were 6.276e-4 and 6.124e-5 against the unchanged 0.15 gate. | Scope is unforced uniform D2Q9/SRT, shifted FP32, double-buffer streaming and the tested wall-bounded x-periodic shear. No TRT/MRT LES, forcing, EsoPull, objects, refinement or 3D turbulence qualification follows. |
|
||||
|
||||
Historical uniform SRT cylinder-startup and inlet-rounding findings remain separate
|
||||
from the passed MRT DFG result. Raw exchange/window consistency is a measurement
|
||||
contract, not a general stress or force-accuracy certification. Existing source-pinned
|
||||
runners may intentionally reject a newer checkout; never silently repin old receipts.
|
||||
|
||||
## Remaining work
|
||||
|
||||
The related host checks passed (67 checks); they do not replace the GPU or
|
||||
physical-accuracy evidence above. Preserve the existing SRT checkpoint cases,
|
||||
historical LES manifests and all frozen numerical criteria.
|
||||
|
||||
Continue the TG accuracy investigation using its actual fine-level rates and
|
||||
the established analytic convention. Keep the physical accuracy gate open until
|
||||
the relevant comparison passes. Review this integration independently before
|
||||
publication; no new merged-commit GPU run is claimed here.
|
||||
|
||||
No repeat of the accepted full MRT DFG or Cartesian expansion across unsupported
|
||||
combinations is required by this document. Main integration remains a separate
|
||||
review of complete, bounded feature scope.
|
||||
@@ -0,0 +1,60 @@
|
||||
# DFG 2D-2 Re100 GPU campaign (validation-only)
|
||||
|
||||
## Current experimental source provenance
|
||||
|
||||
This development branch now includes the reviewed AMR-channel source changes originating in `5338864748ac67fe2544487c059ba149ebf916da` and `47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5`. The current full-source manifest pins the latter reviewed source snapshot, including the new channel files. This is an explicit new experiment; neither source review nor host tests transfer historical d79 GPU/flow acceptance to it. The channel GPU gate has not yet run at integration time.
|
||||
|
||||
New run JSON records the source-origin pin plus `checkout_revision` (actual Git HEAD), `checkout_dirty`, and the experimental-source label. The origin pin is not claimed to be the run's checkout commit. Numerical definitions and thresholds are unchanged. Runs require a Git checkout for this runtime identity; source hash checks remain active.
|
||||
|
||||
Historical d79 Re100/Jet execution stays reproducible at `69e9b78de9483a5ff21e2d0c74311d240bfaf88c`; the d79 LES diagnostic checkpoint is `20ae6770924df15f31305aad56e44a54a3dd4d88`. Their manifests and existing result files are not rewritten. Use those commits to reproduce or interpret old d79 evidence. The current AMR-channel experiment requires its own GPU and applicable flow evidence.
|
||||
|
||||
## Scope and locked definitions
|
||||
|
||||
Explicitly pinned AMR-channel experimental production snapshot (see provenance below). Uniform D2Q9/MRT, shifted FP32, double buffer, no LES, stationary cylinder and no-slip walls. This is not an AMR, jet, or full canonical-outlet certification.
|
||||
|
||||
Physical geometry: [0,2.2] × [0,0.41], center (0.2,0.2), radius .05, rho=1, nu=.001, inlet mean=1 and peak=1.5. D is lattice diameter; dx=.1/D, dt=U_lattice*dx, nu_lattice=U_lattice*D/100. U_lattice=.01 gives peak inlet Mach .02598. Existing local Zou–He inlet is selected in validation config only. Existing pressure-NEQ outlet is unclamped: **it does not impose the canonical vector do-nothing condition nu*d_n(u)-p*n=0**. The existing production channel-flow initial condition is used, not zero velocity. Periodic eligibility is delayed to t*=250 (25 physical seconds), with fixed end t*=300 (30 seconds).
|
||||
|
||||
Official source: https://wwwold.mathematik.tu-dortmund.de/~featflow/en/benchmarks/cfdbenchmarking/flow/dfg_benchmark2_re100.html
|
||||
|
||||
The 20141124 official newer reference archive has higher-order line-integration force data and supersedes the older example table for this comparison. The compact tests/validation/data/dfg_re100/reference_cycle.json preserves the last complete lift-minimum cycle from lv6/dt4. Reference Cd=[3.164266,3.227392], Cl=[-1.021286,.986571], pressure difference=[2.420371,2.507013], St=.301841. These are derived sampled extrema, not exact analytical values. Official mean=(min+max)/2 and amplitude=max-min; separate time averages are also recorded.
|
||||
|
||||
## Exact commands and startup ownership
|
||||
|
||||
Use the existing CUDA/PyCUDA environment; no package installation or production edits required. From the repository root, use a new task-owned output path:
|
||||
|
||||
export PYTHONPATH="$PWD/src${PYTHONPATH:+:$PYTHONPATH}"
|
||||
python -m pytest -q tests/unit/test_re100.py
|
||||
CUDA_VISIBLE_DEVICES=0 python tests/validation/run_dfg_re100.py --diameter 20 --speed .01 --mode pilot --output output/validation/pilot_d20_u001 > pilot.log 2>&1
|
||||
|
||||
Pilot is t*=5 (10,000 steps), with dense .02-t* sampling to calibrate diagnostic cost. It cannot pass periodicity or external benchmark validation. Root launch owner reviews health, memory, timing and stdout before full runs; the full pair is not launched automatically.
|
||||
|
||||
CUDA_VISIBLE_DEVICES=0 python tests/validation/run_dfg_re100.py --diameter 40 --speed .01 --mode full --output output/validation/full_d40_u001 > d40.log 2>&1
|
||||
CUDA_VISIBLE_DEVICES=1 python tests/validation/run_dfg_re100.py --diameter 80 --speed .01 --mode full --output output/validation/full_d80_u001 > d80.log 2>&1
|
||||
|
||||
These are separate invocations, not a parallel shell script. A single root launch owner serializes startup/compilation across all Horsea work. Do not launch the next process until the previous log contains an explicit `READY {...}` line. READY comes after the first physical sample, including Simulation initialize, sim.run, read_force and get_macroscopic, so lazy runtime/diagnostic paths are exercised. That sample remains in the physical trajectory/history. No hidden warmup/reset, no later new diagnostic kernel. Do not treat a process merely existing as READY. Keep separate output directories and retain exit codes/logs. GPU assignment is chosen by root after checking existing workloads; shown indices are examples of physical device selection.
|
||||
|
||||
## Measurement, frozen criteria and outcomes
|
||||
|
||||
Force uses advancing link-momentum exchange, accumulated in batches <=100 steps, combined in FP64; Cd/Cl=2F/(U_lattice² D). Force timestamp is the batch midpoint. Pressure uses fluid-only h/2h extrapolation to cylinder front/back points and physical conversion 1/(3 U_lattice²), at each sample endpoint. For phase comparisons endpoint pressure is interpolated to the same cycle phase. Both raw times are saved.
|
||||
|
||||
Before t*=240 sample width=.5; afterwards .02 (about 166 samples per lift cycle). Pilot uses dense sampling throughout. Extract complete lift-minimum-to-minimum cycles using quadratic local minimum locations, accepting periods 2–5 t*. Latest five complete cycles must be consecutive with the last ending no more than 5 t* before the final observation, and must start at or after t*=250. Eligibility requires lift amplitude >=.2, period range/mean <=.005, Cd/Cl/pressure min and max ranges <=.005 absolute, cycle-mean mass range/mean <=1e-4, and each cycle's mean mass-flux imbalance <=.005. Instantaneous flux equality is not required in this unsteady compressible LBM diagnostic.
|
||||
|
||||
Reference gates: Cd extrema relative errors <=2%, Cl extrema absolute errors <=.03, pressure extrema relative errors <=2%, St relative error <=2%. These are predeclared engineering tolerances, not official uncertainty intervals. Phase-aligned waveform RMS errors, extrema, midpoint means, time means, amplitudes, outlet residual, sampled density bounds and Mach are all reported. Waveform RMS is diagnostic (no retroactive gate). No threshold changes after GPU results. At cap, failed periodicity stays incomplete. Invalid/nonfinite state, rho outside [.5,1.5], or sampled max Mach>.2 causes a safety stop; this is a gross-health guard, not a Mach-independence claim.
|
||||
|
||||
A case_reference_pass requires both periodicity and reference gates. Every raw case keeps suite_validated=false. The two-grid comparison must report each case and errors; matched-Mach D40/D80 is evidence of resolution sensitivity only, not an asymptotic convergence order. If either fails or finer errors increase materially, report the failure instead of adjusting thresholds. No full canonical/AMR/Mach-independent validation claim is allowed.
|
||||
|
||||
## Resource and calibration plan
|
||||
|
||||
D20: 441×84=37,044 nodes, pilot 10,000 steps. D40: 881×166=146,246 nodes, 1,200,000 full steps. D80: 1761×330=581,130 nodes, 2,400,000 full steps. Main double-buffer DDF arrays alone cost 72 bytes/node: 2.7/10.5/41.8 MB decimal. Flags, masks, macro host arrays, body buffers and CUDA context/JIT memory add overhead; root verifies actual free GPU/host memory and disk before launch. Conservative per-process reservation: 1 GiB VRAM and 2 GiB host RAM, excluding shared/compiler overhead; measurements take precedence.
|
||||
|
||||
Historical Re20 D80 U=.01 step throughput was ~6082 MLUPS (45.9 seconds/480k steps). At equal throughput D80 Re100 full advancing steps would take ~229 seconds, excluding dense full-field downloads/reduction and compile overhead. D40 estimate must be calibrated, not assumed proportional. Pilot records startup_seconds, step_seconds, measurement_seconds, mlups. For each full grid, calibrate a same-grid pilot if D20 cannot predict cost adequately. Full has about 3480 records (480 coarse+3000 dense); use measured seconds/step × full steps plus seconds/record ×3480 plus startup. D20-to-D80 extrapolation is only a rough planning upper/lower guide, not an ETA commitment.
|
||||
|
||||
Only geometry.npz, one final_field.npz, history.csv, summary.json and stdout/stderr are persisted. No per-step or repeated full-field snapshots. D80 final float64 rho/ux/uy plus mask are <15 MB uncompressed; history/summary several MB, compact versioned reference/source guards <50 KiB. Budget <100 MB per case and <500 MB whole campaign including logs and optional externally retained archives, safely below 2 GiB; root still checks baseline Horsea usage. Retain sparse evidence only.
|
||||
|
||||
## CPU validation and remaining execution
|
||||
|
||||
Ten tests cover geometry/units/config, pressure scaling, periodic and nonperiodic synthetic histories, immutable production hashes, and self-reference comparison. CUDA execution is not available in the preparation environment and has not been claimed. Run pilot first on Horsea; do not confuse successful CPU tests with GPU readiness.
|
||||
|
||||
## Reference provenance and reproduction
|
||||
|
||||
Only the compact derived cycle is versioned. Its source URL, official ZIP SHA-256, column selection, and cycle-selection description are embedded in reference_cycle.json. Raw archives and GPU results are not repository inputs. To reproduce it, obtain the exact archive identified there, verify its SHA-256, and extract logs/results_cc2d_bench2_referencenew/bdforces_q2_lv6_dt4 and pointvalues_q2_lv6_dt4. Use physical time column 2 multiplied by 10, drag/lift columns 4/5, and pressure columns 7 minus 12 (one-based columns). Build rows with mass=flux_in=flux_out=1, then call periodic.extract(rows, min_time=0) and select the last complete cycle. The source guard is tests/validation/data/dfg_re100/source_manifest.json; it verifies the current reviewed production snapshot rather than equating its origin with the validation branch HEAD.
|
||||
@@ -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`.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Jet-circle closed-control-volume pilot
|
||||
|
||||
## Current experimental source provenance
|
||||
|
||||
This development branch now includes the reviewed AMR-channel source changes originating in `5338864748ac67fe2544487c059ba149ebf916da` and `47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5`. The current full-source manifest pins the latter reviewed source snapshot, including the new channel files. This is an explicit new experiment; neither source review nor host tests transfer historical d79 GPU/flow acceptance to it. The channel GPU gate has not yet run at integration time.
|
||||
|
||||
New run JSON records the source-origin pin plus `checkout_revision` (actual Git HEAD), `checkout_dirty`, and the experimental-source label. The origin pin is not claimed to be the run's checkout commit. Numerical definitions and thresholds are unchanged. Runs require a Git checkout for this runtime identity; source hash checks remain active.
|
||||
|
||||
Historical d79 Re100/Jet execution stays reproducible at `69e9b78de9483a5ff21e2d0c74311d240bfaf88c`; the d79 LES diagnostic checkpoint is `20ae6770924df15f31305aad56e44a54a3dd4d88`. Their manifests and existing result files are not rewritten. Use those commits to reproduce or interpret old d79 evidence. The current AMR-channel experiment requires its own GPU and applicable flow evidence.
|
||||
|
||||
Status: CPU preparation only. No GPU run or production change. Current source origin: reviewed AMR-channel snapshot 47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5; this new experiment has no inherited d79 GPU acceptance. tests/validation/data/jet_circle_cv/source_manifest.json checks all tracked source file hashes before importing GPU code.
|
||||
|
||||
## Execute only after root-owned GPU serialization
|
||||
|
||||
From the repository root, with REPO set to the repository root and an absent output directory:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=src python -m pytest -q tests/unit/test_jet_circle_cv.py
|
||||
python tests/validation/run_jet_circle_cv.py --repo "$REPO" --output "$OUTPUT" --plan
|
||||
CUDA_VISIBLE_DEVICES="$VERIFIED_IDLE_GPU" python tests/validation/run_jet_circle_cv.py --repo "$REPO" --output "$OUTPUT"
|
||||
```
|
||||
|
||||
The runner does not select, reserve, or contact a GPU machine. It requires one explicit CUDA_VISIBLE_DEVICES value, at least 1 GiB free, and refuses an existing output directory. It imports existing PyCUDA; no installation. Default pilot: 800 base steps, 8 output windows, at most 1800 seconds after READY. READY is printed only after both steppers compile, execute one base/two fine warmup hops, exercise download/raw paths, and reset initial state/time. A pre-READY silence may be compilation, not stepping progress. Root owns any overall startup timeout and all launch serialization.
|
||||
|
||||
## Physical/numerical case
|
||||
|
||||
Periodic root box 192 by 128, center (96,64) in root cell-edge coordinates, D=24.084, U0=.03, nu=.0072252, Re_D=100, beta=Vj/U0=.1, rho0=1. Unforced D2Q9 SRT FP32, no DDF shifting, no LES. The slightly noninteger diameter was selected solely to avoid q clipping at all three planned scales. Circle is stationary. Two uniform radial sectors at pi/2 and 3pi/2, half-width pi/12: top blows outward +.003, bottom sucks inward -.003; remaining wall has zero velocity. This periodic transient is NOT DFG, infinite-stream drag, or statistical high-Re validation. End time is t*=t U0/D approximately .9965.
|
||||
|
||||
AMR bounds (48,32,144,96). Finest dx=dt=.5 in root lattice units, nu_fine=2 nu, finest D=48.168. Full-domain fine reference is 384 by 256 with exactly the same finest spacing, timestep, viscosity, collision and storage. Root tau=.5216756, fine tau=.5433512, Mach approximately .05196.
|
||||
|
||||
AMR uses actual public FixedPatchStepper Jet construction and raw observation. The uniform blocked reference reuses the existing test_raw_exchange_gpu.py optional-input validation adapter: production Jet-derived velocity/force-arm arrays feed the unchanged blocked curved kernel. Its normal public PeriodicBlockStepper API is passive-only. This does not claim there is no uniform Jet route anywhere: Simulation.add_jet exists, but the same-blocked-kernel adapter removes a backend/storage confound for this comparison. No new public backend is introduced. Runtime asserts matching directions, q, fallback and true hit coordinates after frame conversion; both sectors must resolve links. Unclamped q/no fallback is required.
|
||||
|
||||
## Independent measurements and signs
|
||||
|
||||
Two closed rectangles: (72,44,120,84) and (64,40,128,88), wholly within the finest patch. Every contour face has two active-fluid stencil layers. Capture physical postcollision populations at t=0 and every completed base interval. Collision conserves density/momentum; these are valid endpoint moments, NOT precollision stress estimates.
|
||||
|
||||
For each contour, M and P sum fluid-cell rho and rho*u times h^2, excluding the staircase solid. Face midpoint quadrature integrates rho*u dot n and [rho*u*u+p*I-mu*(grad u+grad u.T)] dot n. Velocity gradients use centered differences in root-length units, mu=rho*nu, p=(rho-1)/3. Constant pressure subtraction integrates to zero on a closed rectangle. No raw exchange enters these field quantities.
|
||||
|
||||
Trapezoidal time integration samples every base step over the SAME reported [step_start,step_end] as cumulative raw impulses/masses. For each window:
|
||||
|
||||
- mass into fluid through body surface = delta M + integral outer mass flux
|
||||
- raw boundary reaction target = -delta P - integral outer momentum-minus-traction flux
|
||||
|
||||
Raw rows are [Jx,Jy,angular impulse,mass into fluid]; each uniform fine hop scales by [.25,.25,.125,.25], exactly as the AMR interval ledger. Both fine hops are accumulated. AMR observation time endpoints and the public raw reduction are checked. Reports separate total, wall, aperture, blowing and suction raw rows. Coefficients divide force by .5*rho0*U0^2*D. These are momentum reactions including aperture transport; neither raw aperture nor total raw reaction is labelled solid-only hydrodynamic traction.
|
||||
|
||||
Independent aperture mass estimates integrate measured rho*u dot e_r over true arcs using 64-point Gauss quadrature and nearest active-fluid samples. No target velocity or raw row enters this estimate. Reports include maximum sampling distance, actual signed raw mass rates and nominal signed volume flux ±2*(pi/12)*R*Vj. Near-wall nearest-node sampling is only a first-order diagnostic, not certified wall extrapolation. Nominal volume flux is not asserted to equal measured mass flow at finite density variation.
|
||||
|
||||
Inventory near the circle is a staircase-cell approximation; its geometric error and face/time quadrature errors can appear in CV residuals. Two-contour spread and grid refinement are essential. No exact continuum force value, solid traction recovery, jet hardware load, or physics acceptance is claimed. Output status is deliberately PILOT_DIAGNOSTIC_ONLY even on successful execution.
|
||||
|
||||
## Scope, refinement, resources
|
||||
|
||||
Default one pilot only. Estimated device envelope 128 MiB, required free 1 GiB; bounded JSON only, conservatively under 2 MiB, no population snapshots or per-step logs. Every base step downloads both fields for independent quadrature, so PCIe/CPU diagnostic cost is intentional. Six CPU tests cover descriptor/source identity, uniform flux cancellation, affine convective divergence, quadratic viscous sign/coefficient, matched geometry/Jet selection and diffusive scaling.
|
||||
|
||||
Optional --scale 2 or 4 doubles/quadruples all lengths and divides U,V by that scale, holds lattice nu fixed, and scales steps/windows by scale^2: physical Re and t* match, Mach decreases. Source and geometry guards repeat. Device estimate scales as scale^2; memory gate requires twice the estimate or 1 GiB, whichever is greater. Scale 4 is not part of default authorization/readiness. --re 400 requires --allow-re400 and a separate reviewed root launch decision after Re100 evidence; no automatic promotion.
|
||||
|
||||
Compare AMR/reference velocity error, signed reaction coefficients, each contour's independently measured reaction and residual, contour spread, signed mass budgets, and measured aperture flux. Freeze numerical tolerances only after agreeing a scientifically appropriate convergence criterion; this preparation imposes only structural/finite-state guards. A completed pilot cannot establish convergence.
|
||||
|
||||
Follow-on design retained: porous Couette crossflow with u=U*expm1(Re_V*y/H)/expm1(Re_V), v=V and unequal exact wall shear. It requires validation-only planar geometry and is intentionally not implemented here.
|
||||
|
||||
### Amplitude, geometry, and high-Re limits
|
||||
|
||||
Both Jet amplitudes are constant throughout the run and every fine substep; geometry is fixed. There is no runtime switch, per-step forcing schedule, or moving-wall velocity in this campaign. Construction/packing happens once, with identical uploaded amplitudes in the uniform adapter. This avoids any substep timing ambiguity from amplitude changes.
|
||||
|
||||
Every case records actual tau_root and tau_fine. For this fixed s=1 geometry and speed, Re400 would give nu=.0018063, tau_root=.5054189 and tau_fine=.5108378, close to the SRT limit .5. The implemented Re choices are a bounded campaign menu, not a universal physical Reynolds-number ceiling. Higher-Re evidence may require a larger initial resolved geometry or a different justified grid/relaxation design before launch. Diffusive refinement at constant lattice nu improves spatial/Mach resolution but does not move tau farther from .5; simply selecting scale=2 does not cure a poor tau. No all-Re, LES or three-dimensional realism claim follows from this pilot.
|
||||
|
||||
The source guard checks production file content against the current reviewed source origin, not validation-branch HEAD. This runner is validation-only; generated results and frozen execution-package archives are not committed.
|
||||
|
||||
## Measurement-only continuation (frozen design before execution)
|
||||
|
||||
The original 800-step pilot and its audit remain historical evidence, not an acceptance target. The following adds diagnostics only; it neither modifies production kernels nor adjusts velocity commands to improve measured flux. Use the canonical validation Git branch, with the same source-manifest check and checkout provenance record. No standalone replacement source archive is needed.
|
||||
|
||||
Run the pair serially under root-owned GPU allocation, from the checked-out repository:
|
||||
|
||||
```sh
|
||||
python tests/unit/test_jet_circle_cv.py -v
|
||||
python tests/unit/test_jet_cv_measurements.py -v
|
||||
CUDA_VISIBLE_DEVICES="$VERIFIED_IDLE_GPU" python tests/validation/run_jet_circle_cv.py --repo "$PWD" --output "$NEW_OUTPUT_S1" --continuation --steps 3200 --scale 1
|
||||
CUDA_VISIBLE_DEVICES="$VERIFIED_IDLE_GPU" python tests/validation/run_jet_circle_cv.py --repo "$PWD" --output "$NEW_OUTPUT_S2" --continuation --steps 3200 --scale 2
|
||||
python tests/validation/analyze_jet_cv_continuation.py "$NEW_OUTPUT_S1" "$NEW_OUTPUT_S2" --output "$COMPARISON_JSON"
|
||||
```
|
||||
|
||||
Both output directories must be absent. `--plan` prints configuration without CUDA. `--continuation` deliberately accepts only Re100, scale1/2, and 3200 reference steps: a bounded campaign, not a physical Re ceiling. Actual steps are 3200 and 12800, reaching the same t*=3.986048829. Physical circle/box/contours and beta remain fixed; all root lengths grow by s, U and jet speed shrink by s, lattice nu remains .0072252. Both runs retain tau_root=.5216756 and tau_fine=.5433512, with Mach .0519615/s. This diffusive refinement changes the finite-Mach/compressibility error as well as grid resolution; it is not an isolated spatial-error order study.
|
||||
|
||||
### Units, cadence, and reconstructibility
|
||||
|
||||
Reference length/time are one scale-1 root lattice length/time; density reference is one. Convert scale-s root quantities by: length /s, time /s², velocity times s, mass /s², momentum and impulse /s, force times s, angular impulse /s², pressure times s². Volume and mass rates are unchanged. The nominal signed aperture volume rate is ±.0189155293672 in reference units at every scale. A mass flux divided by rho_ref times that rate is a reference-density diagnostic, not an actual-volume-flow ratio. Saved Q and M remain separate.
|
||||
|
||||
Exact raw observations accumulate every completed base interval, including both fine substeps. Field downloads occur only at shared reference times 0..100 at dt=1, then 100..3200 at dt=4: 876 samples per run, compared with 3201/12801 possible every-base samples. Every 100-reference-time window boundary is included. Trapezoidal integration uses the actual variable time gap, with the raw ledger covering exactly the same endpoints. No missing sample is silently treated as unit dt. Sampling is denser during abrupt startup; it is not a guarantee of resolving all high-frequency acoustic modes. Offline coarsening can expose some time-quadrature sensitivity but cannot rule out unresolved-frequency aliasing.
|
||||
|
||||
`samples_s*.npz` stores at every sample: density minima/maxima/max perturbation and max speed for uniform fluid, AMR root leaves and AMR fine leaves; composite full-domain mass; CV endpoint inventory and flux; separate advective, pressure and negative-viscous-traction momentum fluxes; nearest-fluid mass proxy; extrapolation ray primitive values; linear/quadratic reconstructed Q and M; and cumulative raw wall/aperture/total/blowing/suction rows. Associated JSON identifies axes, units, conversion factors, hashes and snapshot metadata. Composite AMR mass sums root leaves at area1 and fine leaves at area.25, then divides by s². Covered cells are excluded. Global periodic mass change minus total raw body mass is reported, so a transfer/interface mass error cannot hide behind the two contours lying wholly within the fine patch.
|
||||
|
||||
Five compressed primitive snapshots at reference times 0,100,800,1600,3200 retain rho, reference velocity, origin and spacing for uniform, AMR root and AMR fine grids. Inactive/solid entries are NaN. These are full primitive grids at sparse times, not downsampled images or distributions. They permit independent spatial quadrature, field inspection, masks and inventory reconstruction. Per-sample split flux/inventory arrays allow independent temporal integration without rerunning GPU work. FP32 saved primitive snapshots introduce their own rounding; FP64 sampled diagnostic/ray arrays remain in the sample file.
|
||||
|
||||
### Independent aperture estimators and limitations
|
||||
|
||||
Thirty-two Gauss points per true aperture arc, with rays at distances 1.5,2.5,3.5 finest-cell widths into the fluid, sample rho,u,v by bilinear interpolation using only active fluid nodes. No commanded wall value or raw population enters reconstruction. Linear and quadratic outward-normal extrapolations separately reconstruct rho and u; integrate Q=integral(u.n ds) and M=integral(rho*u.n ds). Retain ray positions, normals, weights, primitive samples and coefficients, making every estimate reproducible. The nearest-fluid 64-point mass proxy is preserved for continuity with the pilot and remains explicitly a proxy.
|
||||
|
||||
Neither estimator is an exact wall value or validated wall model. Extrapolation amplifies noise and may yield unphysical density; do not clamp it to force agreement. Sector endpoints contain nonsmooth prescribed velocity changes, and interpolation may mix their neighborhood. Linear-versus-quadratic spread, ray density extrema, resolution changes and analytic manufactured tests constrain interpretation, but their agreement is not proof of low bias. Two grids alone cannot establish asymptotic convergence. Nominal geometry/profile flux is exact for the commanded uniform radial profile; comparison with reconstructed Q diagnoses estimator plus numerical-boundary error, not automatically a Jet defect. Raw reaction remains momentum transport plus traction; no nozzle-load or solid-only force claim.
|
||||
|
||||
### Predeclared comparison criteria
|
||||
|
||||
These criteria are fixed before the continuation runs. There is no new physical acceptance threshold and no tolerance fitted to the pilot:
|
||||
|
||||
1. Structural validity: exact source/provenance, matched geometry/parameters, unclamped q/no fallback, both apertures resolved, finite positive active-node density and finite velocity at every saved field sample, complete scheduled samples, contiguous matching raw intervals, and all declared files/hashes. Runtime does not promise positivity between saved samples.
|
||||
2. Report every window's signed and gross quantities, maximum sampled errors, and early (reference t<=800) versus later (t>800) norms separately. Normalize mass residual by rho_ref*abs(Q_command)*window_duration, and reaction residual by .5*rho_ref*U_ref²*D_ref*duration. Avoid dividing by a near-zero net mass or instantaneous force.
|
||||
3. Recompute contour residuals using the complete sampled grid and every second/fourth sample, always retaining endpoints. Save their changes as a temporal-sensitivity proxy, not an error bound. Compare both contours and both backends; show mass, Cx and Cy components separately.
|
||||
4. A directionally resolved reduction in a component's RMS CV residual is flagged only if coarse-minus-fine RMS exceeds twice the largest observed temporal coarsening change. This is an evidence-of-trend flag, never a physics PASS. If a component plateaus or grows, decompose temporal/spatial/staircase/boundary effects before proposing production changes.
|
||||
5. Compare reconstructed Q/command and M/(rho_ref*command) separately, at identical physical windows, retaining both extrapolations. Regard refinement improvement as persuasive only when it exceeds estimator spread and temporal variation. Inspect density and global mass alongside it. Agreement of AMR and uniform ledgers alone cannot validate their shared boundary treatment.
|
||||
6. Four D/U in a periodic unforced domain remains transient consistency evidence. Two scales do not prove order, stable shedding, infinite-stream drag, all-Re capability, or readiness to promote Re400. Formal physical acceptance needs an additional reference/error budget and appropriate time/domain evidence.
|
||||
|
||||
### Runtime and storage budget
|
||||
|
||||
Actual earlier scale-1 pilot cost was 52.3884s launch-to-exit, 46.1414s after READY for800 steps with full-field diagnostics every base step. A naive same-diagnostic3200-step scale-1 extrapolation is about210s; the new876 field samples reduce transfer count but add ray diagnostics and compression. Use conservative planning envelopes210–500s for scale1 and900–3200s for scale2, not timing guarantees. The scale2 conservative envelope accounts for4x cells and4x root steps; sparse sampling mitigates but does not eliminate that cost. Post-READY safety caps are1200s and5400s respectively, replacing the unsuitable fixed1800s assumption. Root owns compilation/startup timeout and serial launch decisions.
|
||||
|
||||
Device-envelope estimates remain128/512MiB for s1/s2, at least1GiB free (or twice estimate). Output estimate is under64MiB per run including five primitive snapshots and876 sample records; no all-time populations are saved. Plan/metadata and sampled files are rewritten at each100-reference-time window to retain bounded partial evidence if interrupted. READY still follows compiled warmup and state/time reset, before measured stepping begins.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Smagorinsky baseline/candidate helper diagnostic
|
||||
|
||||
This validation-only diagnostic compares exact d79 production headers with a temporary candidate. It does not modify production source. The candidate changes only the D2Q9 and D3Q19 closed-form factors from 18 to 18 sqrt(2), plus comments defining |S| = sqrt(2 S:S) and self-consistent effective tau. The small reviewed patch and expected header hashes are in `tests/validation/les/data`; no copied headers, solver trees, raw results, or binary fixtures are versioned.
|
||||
|
||||
## Prepare and verify on the CPU
|
||||
|
||||
From the repository root, choose a new output directory:
|
||||
|
||||
```sh
|
||||
python tests/validation/les/prepare.py --output-dir output/les-host/candidate
|
||||
python tests/validation/les/test_candidate.py --candidate-dir output/les-host/candidate --output output/les-host/host_results.json
|
||||
```
|
||||
|
||||
Preparation checks both actual production header hashes against d79, copies only `descriptors.cuh` and `turbulence_smag.cuh` into output-local baseline/patched include trees, applies the reviewed patch only to the latter, and verifies all four resulting hashes. It refuses an existing candidate directory. The host test verifies candidate hashes again and refuses an existing evidence file. The Git branch HEAD may include validation commits; header content, rather than HEAD identity, is the pin.
|
||||
|
||||
The independent host oracle constructs second-order Hermite populations and solves the independently derived quadratic by bisection:
|
||||
|
||||
tau * (tau - tau0) = 9 Cs^2 ||Pi_neq|| / (sqrt(2) rho)
|
||||
|
||||
There are **736 parameter cases**, covering D2Q9/D3Q19; Cs=0,.08,.16,.24; tau0=.6,.8,1.2; rho=.7,1,1.3; equilibrium, isotropic stress, pure shear, and trace-free diagonal extension; all coordinate planes and both signs; density-scaled Pi and separate fixed-Pi/nonunit-rho probes. The test checks quadratic residuals, closed/default-three/converged-200 iterations, bit-identical unchanged iterative output, and an additional explicit cap-active manufactured probe. Existing g++ and Python stdlib are sufficient; no installation is attempted. Compilation products are temporary. Previously reviewed host maxima were omega error 1.394302e-7 and quadratic residual 1.201202e-7; this is not CUDA fast-math evidence.
|
||||
|
||||
## Authorized GPU diagnostic
|
||||
|
||||
Use the existing working PyCUDA/NumPy environment and an explicitly selected device, through the same serialized GPU launch owner as the other campaigns:
|
||||
|
||||
```sh
|
||||
CUDA_VISIBLE_DEVICES="$VERIFIED_IDLE_GPU" python tests/validation/les/run_gpu.py --run-gpu --device 0 --output-dir output/les-gpu
|
||||
```
|
||||
|
||||
Device 0 is the logical device after CUDA visibility filtering. The output directory must not exist. The runner materializes and verifies the isolated include trees under `output/les-gpu/candidate`, then runs baseline and patched processes sequentially. Each process compiles one SourceModule at a time synchronously and synchronizes each probe. There are no background jobs, worker pools, or parallel compiler calls. No machine contact, device selection, reservation, or environment setup is performed by the runner.
|
||||
|
||||
Each version has exactly **48 scalar GPU cases**, 96 total: D2Q9/D3Q19, Cs=.16/0, equilibrium/shear/isotropic/cap-active manufactured stress, and closed/default-three/converged-200 branches. This is the frozen GPU matrix, not the expanded 736-case host matrix. Outputs `baseline.json` and `patched.json` contain raw scalar omega values and device name. Completion means only the diagnostic executed; an independent comparison against the declared host oracle is still required. A failed run leaves partial evidence for inspection and must not be overwritten.
|
||||
|
||||
## Interpretation and limits
|
||||
|
||||
Full-stress versus deviatoric-stress selection, iterative viscosity cap, iteration count/relaxation, omega guards, collision dispatch, and AMR guards remain unchanged. Closed and iterative paths intentionally remain different for isotropic or cap-active stresses. No settings or Cs defaults change. High stress is a helper diagnostic, not an admissible flow-state claim.
|
||||
|
||||
The corrected scalar formula is justified only under the stated stress-strain estimate. MRT full-trace stress does not generally share one shear relaxation rate, so its bulk coupling remains uncertified. Force-corrected non-equilibrium stress and Guo-force interaction also remain uncertified. These manufactured tests are unforced and do not cover full TRT/MRT collision, streaming modes, shifted storage, walls/Jets, flow solutions, or 3D turbulence. Existing guard statements are preserved, not newly comprehensively tested; the main matrix is guard-inactive. No AMR LES support is added or enabled.
|
||||
|
||||
Production integration and removal of this temporary candidate fixture are separate decisions after GPU and applicable flow evidence. CPU success alone does not approve a production change.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Matched SRT uniform / refined DFG Re100 preparation
|
||||
|
||||
Status: CPU-preflight checked; **not GPU executed or approved to execute**. No physical acceptance or speedup claim. No production source changes. Root owns GPU admission and integration.
|
||||
|
||||
## Frozen setup
|
||||
|
||||
Canonical source snapshot: integrated commit `cd3a657f9d3833e812c3cca173525f165b48710a`. Production origin remains candidate `47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5` (on `5338864`, on `69e9b78`); production files are byte-identical. The integrated Re100 helper header/PIN records that origin explicitly. Both commit identities are retained in the manifest. `tests/validation/data/refined_dfg/source_manifest.json` hashes all production source files plus the existing DFG geometry/pressure helper, periodicity helper and official reference cycle. An integrated checkout may have a different commit but must have identical pinned file contents. No MRT external-reference run is reused as the matched uniform control.
|
||||
|
||||
Both cases: SRT, shifted FP32, double buffer, no LES, no applied force, stationary cylinder, local Zou–He parabolic inlet, halfway y walls, identical pressure-NEQ outlet with **all nine nonequilibrium components damped by 0.5**, backflow clamp disabled. This outlet is not the exact canonical DFG do-nothing condition. Raw cylinder reaction observation is a diagnostic, not applied forcing.
|
||||
|
||||
Physical units use D=0.1 m, mean U=1 m/s, ν=0.001 m²/s, density reference=1; domain 2.2×0.41 m, cylinder center (0.2,0.2) m. Uniform D80: 1761×330, dx=0.00125 m, dt=0.0000125 s, ν_lattice=.008. Refined root D40: 881×166, dx=.0025 m, dt=.000025 s, ν_lattice=.004; fine dx/dt/ν match uniform D80. All lattice mean speeds are .01.
|
||||
|
||||
Fixed half-open patch **(40,24,320,140)** in root edge coordinates maps to x=[.09875,.79875] m, y=[.0575,.3475] m. The body and about six diameters of downstream wake are fine; wall layers and far wake remain coarse. Bounds are frozen before outcomes, not tuned afterward. Geometry-only leaf-count and nominal collision-work ratios to uniform are ~.419 and ~.321. These omit real launch/transfer/halo/measurement costs and are not measured speedups. The narrow root block height is legal but potentially expensive; do not infer speed from leaf count.
|
||||
|
||||
Exact root dimensions are tiled by (by,bx)=(2,881); no padding. Fine allocated shape (240,568), including four-cell halos, uses (8,8). `host_geometry` calls production channel transfer-overlap checks and complete fine cut-link/donor/transfer guards before any CUDA import.
|
||||
|
||||
### Half-cell conversion
|
||||
|
||||
Uniform integer nodes have physical coordinates (i*dx, (j-.5)*dx). Root edge coordinates therefore map to ((X-.5)*dx_root, (Y-1)*dx_root). Body edge center=(80.5,81), radius=20; wall edges Y=1,165; source planes X=.5,880.5. Fine local body nodes are `(body_edge - fine_origin)/.5 - .5`.
|
||||
|
||||
Body and physical wall planes match exactly. The axes have different phases. With fine origin O=(38,22):
|
||||
|
||||
- x_f = [O_x+(j+.5)/2-.5] dx40 = (j+75.5) dx80, versus x_uniform = i dx80. Fine x samples have a half-fine-cell (quarter-root-cell) offset.
|
||||
- y_f = [O_y+(k+.5)/2-1] dx40 = (k+42.5) dx80, versus y_uniform = (l-.5) dx80. The y samples align exactly for l=k+43.
|
||||
|
||||
This difference follows from the inlet's ghost-node plane versus the half-way y-wall planes. The host phase test checks both mappings. Compare interpolated observables, not pointwise arrays or bitwise equality; x-phase error, the different fine collision arithmetic, wall resolution, and coarse/fine transfer effects all contribute to discrepancies. No field-difference acceptance is claimed by this runner. Initial fluid state samples the same analytic parabolic equilibrium as uniform production initialization; root boundary ghosts start at rest. It is not asserted to be bitwise identical across different grids/arithmetic.
|
||||
|
||||
## Observations and gates
|
||||
|
||||
- Every AMR base step is individually followed by `read_raw_exchange(..., window='last_base_step')`: two fine substeps, root-lattice per-unit-depth impulse integrated over [n,n+1]. `step(n)` alone retains only its last base interval, so it cannot support averaging the whole batch.
|
||||
- Uniform `Simulation.run(batch)` / `read_force` gives the mean over that batch; multiply by batch before accumulating. Cd/Cl divide the interval mean reaction by 0.5 U_lattice² D_lattice in each case. Force unit is ρ dx³/dt² per depth, so fine-uniform lattice force is twice root force for the same physical load.
|
||||
- Force timestamps are observation-interval midpoints. Pressure uses physical decoded postcollision density at the interval end, extrapolated from fluid-only points one/two **fine** cells outside the cylinder using existing `pressure_probes`. Pressure difference is normalized by ρ_ref U_physical². Existing periodic helpers align pressure onto force cycles.
|
||||
- Public AMR downloads exclude nonleaves. Fixed geometry masks, not state-dependent finiteness masks, check all fluid populations and sum ρ dx_level²; covered root cells, body, boundaries and fine halos are excluded. Corrupt NaN fluid cannot disappear from the mass check. Mass is per-unit-depth ρ_ref·m². Uniform uses its physical fluid flags and dx². Different sampling of the circular boundary means initial discrete fluid volume can differ slightly; compare temporal stability, not identical absolute mass.
|
||||
- Flux samples root/uniform x-index 2 and -3, summed with dx and physical velocity conversion. Those planes differ by grid-scale offsets; cycle-average inlet/outlet balance is the gate, not direct pointwise flux equality.
|
||||
- Dense measurement width Δt*=.02 in the pilot and for t*≥240, otherwise .5. Pilot ends t*=5; full ends t*=300. Same existing five-cycle periodicity, mass, flux, and reference gates are imported without modification. Both official-reference diagnostics and refined-versus-uniform cycle comparisons are written. `matched_case_pass` refers only to matched SRT agreement, not certification of canonical DFG or the broader suite.
|
||||
- Safety stop: nonfinite state/reaction, Mach>.2, or density outside [.5,1.5]. GPU durations include required exchange reads and synchronization in stepping; full wall time also includes host measurements. Do not report pure-kernel throughput from these numbers.
|
||||
|
||||
## Commands
|
||||
|
||||
From the repository root, CPU only:
|
||||
|
||||
```sh
|
||||
PYTHONPATH=src python tests/unit/test_refined_dfg.py
|
||||
python tests/validation/run_refined_dfg.py --mode plan --output /tmp/refined-dfg-plan-new
|
||||
```
|
||||
|
||||
Fourteen CPU tests are retained in tests/unit/test_refined_dfg.py. Plan JSON is configuration output only; no preparation result files are committed. The initial attempt to use pytest was unavailable in this interpreter; tests use standard-library unittest and NumPy instead.
|
||||
|
||||
Only after root channel GPU gates pass and root explicitly admits an idle physical GPU, root may run:
|
||||
|
||||
```sh
|
||||
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s <caller-hard-budget>s python tests/validation/run_refined_dfg.py --mode pilot --gpu-gates-approved --max-wall-seconds <caller-soft-budget> --output <new-pilot-directory>
|
||||
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s <caller-hard-budget>s python tests/validation/run_refined_dfg.py --mode full --gpu-gates-approved --max-wall-seconds <caller-soft-budget> --pilot-results <completed-pilot-directory> --output <new-full-directory>
|
||||
```
|
||||
|
||||
`--gpu-gates-approved` is a caller attestation, not proof that GPU tests ran. Full mode additionally requires both matched healthy pilots through t*=5 with identical source hashes, bounds and runner hash. No GPU/Mac/remote operation has been performed for this preparation. Pilot/full outputs must be new/empty. Uniform runs first; an unhealthy case stops the campaign. A full run that fails periodic/matched gates exits nonzero.
|
||||
|
||||
## Bounded execution and receipt validation
|
||||
|
||||
No pilot throughput is known yet; the caller must set a finite positive `--max-wall-seconds` rather than treating an invented estimate as calibration. After valid pilots, the full-run calibrated budget is 3 × sum_over_cases[startup_seconds + 60 × (pilot_wall_seconds − startup_seconds)]. The full run uses the smaller of that estimate and the caller's explicit ceiling. This uses total measured pilot evolution/observation cost and the full/pilot physical-time ratio (300/5); 3× is a declared safety margin, not a runtime guarantee. The plan records both budgets.
|
||||
|
||||
The campaign deadline begins before the first case initialization. Cooperative checks surround initialization, every AMR root step or uniform ≤100-step batch, and measurements. On observed expiry, the runner stops, writes a nonpassing `wall_time_limit` receipt, and exits 3. A blocking CUDA call cannot be interrupted by these checks: the external `timeout` watchdog is required, with a hard budget chosen slightly above the soft budget to permit cleanup. Hard termination may leave partial CSV data and no final receipt; that never qualifies as a completed pilot.
|
||||
|
||||
Each case emits `READY` with strict finite JSON and saves `ready.json` only after its first healthy complete observation, after initialization, actual stepping, force readback, macro/pressure/mass/flux paths and CSV flush all succeeded. No early initialization-only READY is emitted.
|
||||
|
||||
Full-run pilot validation rejects nonfinite JSON constants and numeric overflow, wrong case/mode/status, nonboolean health, missing/duplicate/out-of-order rows, wrong step counts or observation times, missing numeric observations, health-limit violations, absent/mismatched READY receipts and invalid timing calibration. It requires exactly 250 observations spanning the full dense t*=5 pilot, source/runner hashes and frozen bounds. Tests include the reviewer's exact NaN-endpoint regression and malformed receipt variants. Synthetic test fixtures exist only in temporary directories and are not simulation results.
|
||||
|
||||
## Integrated source import guard
|
||||
|
||||
The runner's source defaults to this repository root. Before geometry or GPU work, it verifies the pinned source/helper/reference contents and requires the imported CelerisLab package to be exactly `<source>/src/CelerisLab/__init__.py`. An installed DynamisLab copy or another cached checkout is rejected. Integration changes paths/readability and this explicit import assertion; geometry, numerical definitions, gates, and receipt validation remain frozen. Any new path-formatted runner has its own hash, so old preparation pilot receipts cannot authorize a full run of this integrated runner.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Matched MRT DFG pilot preparation
|
||||
|
||||
Preparation only: this test-only extension reuses the existing runner and existing MRT
|
||||
operator. GPU admission awaits reviewed MRT capability results; no pilot/full execution
|
||||
is implied by CPU preflight. No production, historical SRT manifest, reference data,
|
||||
periodic gate, geometry, initialization or observation algorithm is changed here.
|
||||
|
||||
## Selection and provenance
|
||||
|
||||
Use `--collision MRT` for **both** uniform D80 and refined root D40/fine D80. There are
|
||||
no independently selectable collision models per case. Default remains SRT.
|
||||
|
||||
MRT verifies `data/refined_dfg/source_manifest_mrt.json`, pinned to reviewed numerical
|
||||
source snapshot `3ea131e92c057843034f11a1c574301a2430dfce`, including the shared geometry,
|
||||
pressure/cycle helpers and official reference. Test-only commits may change HEAD without
|
||||
changing this numerical source pin. Plan, READY, case summaries and comparison separately
|
||||
record `checkout_revision` and `checkout_dirty` using existing source-provenance helper;
|
||||
the runner hash remains separate. Collision is recorded in every receipt, and full-mode
|
||||
validation rejects missing or different collision labels in plan, either summary or either
|
||||
READY receipt, in addition to existing source/runner/health/time checks.
|
||||
|
||||
The historical SRT manifest remains byte-for-byte unchanged. SRT on the MRT candidate
|
||||
checkout deliberately fails that source pin; use a matching historical checkout via
|
||||
`--source` for SRT. No fallback or automatic repinning is permitted. Historical SRT
|
||||
failure/diagnostic evidence remains attached to its original source and runner hashes;
|
||||
it cannot authorize an MRT full run. The existing failed-observation step-reporting
|
||||
undercount is not changed in this narrowly scoped selection patch.
|
||||
|
||||
## Frozen physical comparison
|
||||
|
||||
All settings remain those documented in `refined_dfg.md`: U_lattice=.01, physical mean
|
||||
U=1 m/s, D=.1 m, shear ν=.001 m²/s, fixed physical domain/body/wall planes, patch
|
||||
(40,24,320,140), shifted FP32 double buffers, no LES/forcing/Jets, finest dx=.00125 m,
|
||||
finest dt=.0000125 s, unchanged half-cell frame conversion and axis-specific phase.
|
||||
Force interval accumulation, physical leaf mass, pressure interpolation, flux sampling,
|
||||
safety thresholds, t*=5 pilot and t*=300 cap are unchanged. Existing accuracy/periodicity
|
||||
criteria are imported unchanged. Both MRT cases are fresh runs; old MRT or SRT results
|
||||
are not substituted as controls.
|
||||
|
||||
Both cases use the **existing MRT pressure-NEQ branch**: only unknown directions q2/6/8
|
||||
are extrapolated. This differs from SRT's all-nine .5-damped branch; the retained
|
||||
`srt_neq_damp=.5` configuration value is inactive under MRT. Backflow clamp remains off.
|
||||
The outlet is still noncanonical relative to exact DFG do-nothing conditions.
|
||||
|
||||
The reused MRT operator keeps fixed kinetic rates `s_e=s_eps=s_q=1.2` on all levels;
|
||||
only the shear rate follows existing per-level viscosity. Physical bulk relaxation
|
||||
therefore differs between coarse and fine levels. Transfer operators and subcycling
|
||||
are unchanged. This is the candidate's declared behavior, not a new fully mode-scaled
|
||||
MRT transfer scheme. Do not tune rates, the patch or gates after seeing outcomes.
|
||||
Capability smoke/parity gates do not establish DFG accuracy or speedup.
|
||||
|
||||
## CPU and admission commands
|
||||
|
||||
From this checkout:
|
||||
|
||||
```sh
|
||||
python tests/unit/test_refined_dfg.py
|
||||
python tests/validation/run_refined_dfg.py --collision MRT --mode plan --output /tmp/new-mrt-dfg-plan
|
||||
```
|
||||
|
||||
After root verifies the candidate MRT root/parity/refinement capability results and admits
|
||||
an idle device, the paired pilot may use the existing bounds and external watchdog:
|
||||
|
||||
```sh
|
||||
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s <hard-budget>s python tests/validation/run_refined_dfg.py --collision MRT --mode pilot --gpu-gates-approved --max-wall-seconds <soft-budget> --output <new-mrt-pilot-directory>
|
||||
```
|
||||
|
||||
The flag is caller attestation, not evidence. An unhealthy first case prevents the second.
|
||||
Existing strict pilot receipt validation and calibrated wall-time controls remain; full
|
||||
mode is not being admitted as part of this preparation. Root must review both healthy
|
||||
pilot outcomes and separately decide any full-run admission.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Refined DFG MRT timing harness (measurement only)
|
||||
|
||||
`tests/validation/time_refined_dfg.py` times the matched MRT refined/uniform DFG Re100 setup of
|
||||
`run_refined_dfg.py` (same configuration, geometry and initial state functions). No numerical,
|
||||
physics-gate or production change; no force or accuracy result is produced.
|
||||
|
||||
| Variant | Loop | Force reads |
|
||||
|---|---|---|
|
||||
| A | refined `step(1)` + `read_raw_exchange` per base step | runner-like force-read workload (values discarded) |
|
||||
| B | refined `step(1)` only, no host reads (`observe_raw_exchange` stays on) | timing-only |
|
||||
| C | refined single `step(N)` | timing-only; ledger keeps the final base interval only |
|
||||
| E | uniform `sim.run(<=100)` + `read_force` | runner-like force-read workload (values discarded) |
|
||||
| E' | uniform single `sim.run(2N)` | timing-only; at most a whole-window average, different summation order |
|
||||
|
||||
- No variant retains a valid force history; A/E omit the runner's interval aggregation and
|
||||
validation, and no variant includes macro/probe/CSV costs. Results are not full benchmark
|
||||
throughput.
|
||||
- Status lines: `CONSTRUCTED` (construction/JIT complete) and `WARM_READY` (first warm-up and
|
||||
health check passed) per case.
|
||||
- Equal simulated duration: N refined base steps vs 2N uniform D80 steps.
|
||||
- Every repeat starts from the same fresh state (refined `initialize`, uniform `snapshot/restore`)
|
||||
and the same warm-up; construction/JIT, warm-up, downloads and parity checks are untimed.
|
||||
- The actual stepping stream is synchronized explicitly before and after each timed loop; the
|
||||
wall timer (`time.perf_counter`) is the only source. No CUDA-event span is called kernel time.
|
||||
- `--max-wall-seconds` is required for GPU runs (outer timeout, checked between variants and
|
||||
every 100 steps in per-step loops).
|
||||
- Final populations and NaN masks must be bitwise identical to the first A repeat (A/B/C) or the
|
||||
first E repeat (E/E'); refined and uniform grids are never compared. Leaf/fluid populations
|
||||
must be finite. Physical health (finite leaf/fluid macros, rho in [0.5, 1.5], max Mach <= 0.2,
|
||||
the existing runner limits) is checked after warm-up and after each timed window. Matching NaN
|
||||
is accepted only as a storage mask; any infinity or level-count mismatch fails parity. All
|
||||
parity/health checks are untimed.
|
||||
- Cases run sequentially (uniform, then refined) in one process with startup serialized;
|
||||
variants are interleaved within each repeat. A-B and B-C are measured comparative
|
||||
differences under GPU variability, not exact causal decompositions.
|
||||
|
||||
Usage (root selects one verified idle GPU):
|
||||
|
||||
PYTHONPATH=src python -m unittest tests/unit/test_time_refined_dfg.py
|
||||
python tests/validation/time_refined_dfg.py --output <new> --plan
|
||||
CUDA_VISIBLE_DEVICES=<gpu> python tests/validation/time_refined_dfg.py --output <new> --max-wall-seconds <s>
|
||||
|
||||
Defaults: 2000 base steps, 200 warm-up base steps, 3 repeats. Profiler capture and the
|
||||
`observe_raw_exchange=False` variant are deferred until these measurements exist.
|
||||
|
||||
## A versus observation-window ledger (`tests/validation/time_window_dfg.py`)
|
||||
|
||||
Measurement-only comparison on the same shifted-MRT refined setup: A (per-base `step(1)` +
|
||||
`read_raw_exchange`, summed per 80-base-step interval), W (`start_exchange_window`, `step(80)`,
|
||||
one window read) and W1 (window with 80 x `step(1)`, isolating the read pattern from call
|
||||
batching). Same fresh state, warm-up and simulated duration; populations must be bitwise equal
|
||||
to the first A repeat and per-interval total impulses within the documented engineering
|
||||
tolerance, all outside timing. The candidate source is recorded (checkout revision, dirty flag,
|
||||
file hashes); frozen refined-DFG manifests/results are not used or changed.
|
||||
|
||||
PYTHONPATH=src python -m unittest tests/unit/test_time_window_dfg.py
|
||||
python tests/validation/time_window_dfg.py --output <new> --plan
|
||||
CUDA_VISIBLE_DEVICES=<gpu> python tests/validation/time_window_dfg.py --output <new> --max-wall-seconds <s>
|
||||
@@ -0,0 +1,117 @@
|
||||
# Opt-in observation-window ledger in the DFG runner
|
||||
|
||||
This test-only integration adds `--exchange-window` to `run_refined_dfg.py`. It uses the
|
||||
existing production ledger; no collision, boundary, transfer, physical setup, reference,
|
||||
periodicity gate or acceptance threshold changes. Default remains the historical
|
||||
per-base-step observation path. Uniform stepping/force accumulation is unchanged.
|
||||
|
||||
For the refined case, each existing observation interval opens a new ledger at completed
|
||||
base step `begin`, advances exactly the same `n` base intervals, and reads the integrated
|
||||
root-lattice impulse for `[begin, begin+n]`. The contract requires root units, `window`
|
||||
kind, `2*n` fine samples, matching endpoints, and fine dt=.5. The existing coefficient
|
||||
calculation still divides this impulse by `n` and `.5*U_lattice**2*D_root`; force timestamps
|
||||
remain `(begin+n/2)*U_lattice/D_root`. It does not divide an already averaged force again.
|
||||
Pressure/macro/physical-leaf mass measurements and safety checks remain at the original
|
||||
interval end. Pilot interval is 80 root steps; early full-run intervals remain 2000, and
|
||||
any final shortened interval uses its actual `n`.
|
||||
|
||||
The opt-in uses one `step(n)` call and one window read per observation, rather than `n`
|
||||
Python calls/readbacks. Internal subcycling and per-base synchronization remain owned by
|
||||
the same backend. Cooperative deadline checks surround that complete interval; unlike
|
||||
the default path they cannot interrupt between base steps inside `step(n)`. The existing
|
||||
external hard watchdog is still required. The full-run cadence/cap is documented here,
|
||||
but this preparation does not admit a full run or a GPU operation.
|
||||
|
||||
## Provenance and receipt separation
|
||||
|
||||
`source_manifest_window.json` pins the reviewed a233c9032487a6006e950d1e327079b00466c410
|
||||
snapshot (production ledger origin d930) plus existing helper/reference files. Both SRT
|
||||
and MRT are supported by this explicit opt-in source pin; collision selection remains
|
||||
independent and shared by the two cases. Existing SRT and MRT manifests are untouched.
|
||||
Default mode continues to reject the changed candidate under those historical pins;
|
||||
use a matching historical `--source` checkout to run its default path. There is no
|
||||
silent repinning, source fallback, or reuse of historical outputs as candidate evidence.
|
||||
|
||||
Plan, READY, summaries, timeout/comparison receipts now record `observation_mode` as
|
||||
`last_base_step` or `window`, alongside collision and actual checkout provenance. On a
|
||||
uniform receipt this labels its **paired campaign's refined observation mode**, not a
|
||||
change to uniform observation. Full-mode pilot validation rejects missing or mismatched
|
||||
mode labels in the plan, either case summary, and either READY receipt. Source/runner
|
||||
hash, collision, completeness and health checks remain. Cross-mode pilot reuse is denied.
|
||||
|
||||
The earlier short A/W/W1 timing/capability evidence is not a full-run speedup or accuracy
|
||||
claim. Mathematical impulse integration/units are unchanged; floating-point reduction
|
||||
order can differ, so bitwise equality of force sums is not promised. Physics acceptance
|
||||
continues to use the existing frozen gates. Historical failures/results are untouched.
|
||||
|
||||
CPU preparation commands, from the repository root:
|
||||
|
||||
```sh
|
||||
python tests/unit/test_refined_dfg.py
|
||||
PYTHONPATH=src python -m unittest tests.unit.test_raw_exchange_window tests.unit.test_time_window_dfg -v
|
||||
python tests/validation/run_refined_dfg.py --collision MRT --exchange-window --mode plan --output /tmp/new-window-dfg-plan
|
||||
```
|
||||
|
||||
Root must independently review and admit any execution. An admitted paired pilot uses
|
||||
the existing watchdog, device selection, `--gpu-gates-approved`, positive
|
||||
`--max-wall-seconds` and a new output directory, adding only `--exchange-window` to the
|
||||
selected collision's pilot command. No GPU or remote action was performed for this patch.
|
||||
|
||||
## Public Jet diagnostic option
|
||||
|
||||
`--collision MRT --exchange-window --jet-scenario zero|fixed|reverse` selects
|
||||
unshifted FP32, no LES, constant transfer on both existing public backends. Default
|
||||
`none` and the historical manifests remain unchanged. `zero` registers both Jets at
|
||||
zero amplitude and retains every existing DFG verdict, including pressure. `fixed`
|
||||
uses outward radial parabolic sectors at pi/2 and 3pi/2, half-width pi/12, peaks
|
||||
(+.001,-.001). Both use the existing D40/D80 geometry and U=.01. Sector origins are
|
||||
(80.5,81) in root edge coordinates and (160,160.5) in uniform node coordinates;
|
||||
radial extent is [0,1.005R]. `reverse` keeps those peaks through t*=300, updates both
|
||||
public setters at that completed boundary, and continues to 305 with opposite signs.
|
||||
|
||||
Fixed diagnostic windows are 250–260, 260–270, 270–280, 280–290 and 290–300; reversal
|
||||
adds 300–305. Histories retain exact interval endpoints and root/uniform lattice
|
||||
impulses. The refined window uses total exchange; uniform uses public `read_force`
|
||||
batch averages. Driven windows report mean/min/max/RMS Cd/Cl without applying DFG
|
||||
reference or periodicity gates. Exit zero means healthy execution, not qualification;
|
||||
`matched_case_pass` remains false. No independent CV or stress-load claim is made.
|
||||
|
||||
This opt-in records current source/helper hashes with the existing manifest format,
|
||||
checkout revision/dirty state and runner hash. Pilot/full must match that snapshot
|
||||
and the exact scenario. It does not relabel current source as historical evidence.
|
||||
Use the existing bounded plan/pilot/full commands with the three option flags above;
|
||||
full requires the matching completed t*=5 paired pilot. `step_seconds` includes force
|
||||
readback; observation/overhead and total wall time are separate, not kernel throughput.
|
||||
|
||||
Add `--observe-cv` to record independent scalar control-volume samples from the
|
||||
already downloaded public macros. The pilot exercises this path at t*=.02..5;
|
||||
full records 501 endpoints at 290..300. Each `cv_history.jsonl` row contains actual
|
||||
root time, preceding interval start/duration/impulse, and two inventories/outward
|
||||
fluxes ordered [mass,Px,Py]. Exclude the first row's preceding impulse: that row
|
||||
supplies the initial inventory. Uniform native impulse converts by .5² to root
|
||||
units; refined ledger impulse is already in root units. Explicit native-aligned
|
||||
contours differ by .25 root cells in x and are recorded in the plan. Cropping
|
||||
retains the two-cell face-gradient stencils. No fields or per-link arrays are saved.
|
||||
`cv_observation_seconds` is the CV portion of observation/overhead time. Helper hash
|
||||
and the observation flag must match pilot/full. Time integration, temporal
|
||||
coarsening and any physical assessment remain separate offline work.
|
||||
|
||||
## Bounded resolution and placement controls
|
||||
|
||||
Defaults remain `--case both --uniform-diameter 80 --placement wide`. Single-case
|
||||
runs use `--case uniform` or `--case refined` and never report a matched-case pass.
|
||||
Their own reference/periodicity or driven-diagnostic results remain available.
|
||||
A single zero-Jet full run exits successfully only when its periodicity and the
|
||||
unchanged complete DFG reference verdict pass; driven completion is not qualification.
|
||||
|
||||
The D160 option requires `--case uniform --uniform-diameter 160` and an explicit
|
||||
public Jet scenario. U and Jet speeds stay .01 and ±.001; viscosity is .016,
|
||||
grid 3521×658, center (320,320.5), radius80. CV uses root spacing/time factor .25,
|
||||
origin (.375,.75), impulse factor .0625, root viscosity .004, and x-face shift .125.
|
||||
|
||||
`--case refined --placement compact` selects root bounds (48,48,128,128), fine
|
||||
168×168 and origin (46,46), with the same D40/fineD80 body/Jet resolution. Its CV
|
||||
contours are (52,52,112,112) and (50,50,120,120); complete face-gradient stencils
|
||||
are clear of both solids and the transfer footprint. Compact is refined-only.
|
||||
CV metadata gives each grid's spacing/origin/contours explicitly. Case, uniform
|
||||
diameter and placement must match the newly completed pilot in every receipt.
|
||||
@@ -0,0 +1,161 @@
|
||||
# Periodic 2D Taylor–Green AMR campaign (prepared, not GPU-qualified)
|
||||
|
||||
This validation harness complements DFG Re100 and Jet/body tests. It measures
|
||||
smooth 2D periodic decay and errors crossing fixed refinement interfaces at
|
||||
Re_L=100 and 1000. It does **not** certify 3D turbulence, LES, Jet performance,
|
||||
wall conditions, dynamic refinement or general high-Re flows.
|
||||
|
||||
## Reference and conventions
|
||||
|
||||
Primary implementation-author documentation, checked 2026-10-18:
|
||||
- TensorMesh, exact velocity and pressure with a consistent phase:
|
||||
https://docs.tensor-mesh.com/example_gallery/fluid/taylor_green.html
|
||||
- AeroSim Nassu, 2D decay, energy and lattice-length Re definition:
|
||||
https://docs.aerosim.io/nassu/validation/analytical/04_taylor_green_vortex_2d/README.html
|
||||
|
||||
We use the TensorMesh phase, rescaled to length L=N root cells:
|
||||
|
||||
k = 2*pi/L; A(t) = U*exp(-2*nu*k*k*t)
|
||||
ux = -A*cos(k*x)*sin(k*y)
|
||||
uy = A*sin(k*x)*cos(k*y)
|
||||
p/rho0 = -A*A/4 * (cos(2*k*x)+cos(2*k*y))
|
||||
E = <|u|^2>/2 = U*U/4 * exp(-4*nu*k*k*t)
|
||||
|
||||
Pressure is kinematic, with rho0=1. A global velocity sign reversal leaves
|
||||
pressure unchanged; exchanging sine/cosine without the corresponding pressure
|
||||
phase can reverse its sign. CPU tests independently check momentum residuals.
|
||||
The nonlinear acceleration is a pressure gradient, **not zero**; the AeroSim
|
||||
page's prose calling advection zero is not adopted. Do not mix phase variants
|
||||
from different references. The formulas above are verified by substitution,
|
||||
not by trusting a copied pressure expression.
|
||||
|
||||
Re_L=U*L/nu, not Re_k=U/(nu*k)=Re_L/(2*pi). U=.03 in lattice units,
|
||||
Ma=U/cs=.0519615, cs^2=1/3, nu_root=.03*N/Re_L. Acoustic refinement uses
|
||||
h_l=dt_l=2^-l: lattice velocity stays U, nu_l=2^l*nu_root and
|
||||
tau_l=.5+3*nu_l. The worst root tau is .50288 (N32/Re1000), omega=1.98855,
|
||||
inside the existing config guard. No viscosity clamping or guard override.
|
||||
|
||||
All initial arrays are physical FP64 equilibrium populations, supplied to the
|
||||
public shifted-FP32 postcollision-G initializer. rho=1+3p at each true cell
|
||||
center. These are valid equilibrium postcollision states, but do not include
|
||||
Chapman–Enskog nonequilibrium stress; startup acoustic/kinetic error remains in
|
||||
the measurements and is shared with uniform baselines. The incompressible
|
||||
reference is not an exact finite-Mach LBM solution. This is why resolution alone
|
||||
need not remove the full error floor.
|
||||
|
||||
## Geometry and workload
|
||||
|
||||
All patches use level-wide union form, so origins are zero, including fine
|
||||
arrays with holes. Coordinates are (index+.5)*2^-level. Metrics integrate only
|
||||
ACTIVE leaves, each weighted by 4^-level. Covered cells and halo cells never
|
||||
enter norms or mass. Geometry tests prove exact single coverage at root-cell
|
||||
resolution. All blocks are 8x8, and both root dimensions are periodic.
|
||||
|
||||
At each root N=32,64:
|
||||
- 2 levels: L1 rectangle (N/8,N/8,7N/8,3N/4), in L0 indices.
|
||||
- 3 levels: same L1 plus L2 rectangle (3N/4,N/2,3N/2,5N/4), in L1 indices.
|
||||
|
||||
These off-axis boundaries cross nonzero flow and gradients rather than sitting
|
||||
only on symmetry or stagnation lines. Whole geometry scales with N.
|
||||
|
||||
16 cases: at each Re, uniform N=32,64,128,256 plus AMR (root32/root64)x(depth2/depth3).
|
||||
Each AMR case therefore has both a same-root uniform baseline and a uniform
|
||||
baseline at its finest spacing. Uniform N is its own lattice length, so nu and
|
||||
step count are scaled to represent the same nondimensional solution.
|
||||
Samples t*=tU/L = 0,.12,.24,.36,.48. End time is 16*N base steps, a meaningful
|
||||
.48 domain turnover or 3.016 vortex-scale turnover times (scale 1/k).
|
||||
At Re1000 energy decays about 7.3%; at Re100 about 53.1%. No plots or full fields;
|
||||
each case writes five diagnostic rows and provenance, incrementally.
|
||||
|
||||
## Running (GPU owner must schedule launches)
|
||||
|
||||
CPU checks, from the repository root:
|
||||
|
||||
PYTHONPATH=src python -m unittest discover -s tests/validation/taylor_green -p 'test_*.py' -v
|
||||
|
||||
One GPU case, on the scheduled worker after checking occupancy:
|
||||
|
||||
CUDA_VISIBLE_DEVICES=<physical-id> python tests/validation/taylor_green/run_case.py --repo /path/to/reviewed/checkout --re 100 --n 32 --depth 1 --output output/taylor_green/re100_n32_d1.json
|
||||
|
||||
Depth 1 is uniform. Run uniform cases before AMR for each Re; inspect baseline
|
||||
screening failures before proceeding. For a complete matrix, repeat the uniform
|
||||
call with N=32,64,128,256 and AMR calls with N=32,64 / depth=2,3, at each Re.
|
||||
Use serial process startup on the parent's assigned Horsea GPUs. This harness
|
||||
does not schedule or acquire GPUs. No GPU was used during preparation.
|
||||
|
||||
python tests/validation/taylor_green/assess.py output/taylor_green/re*_n*_d*.json --output output/taylor_green/assessment.json
|
||||
|
||||
The local API/geometry review used clean staging commit
|
||||
cd3a657f9d3833e812c3cca173525f165b48710a. This is **not** a claim of results on d79
|
||||
or any historical commit. Each runtime output records actual Git HEAD/dirty
|
||||
state, an aggregate source-tree hash, plus SHA256 of all harness Python files. Assessment refuses dirty checkouts and mixed source
|
||||
or harness identities. Keep reports outside the checkout or .gitignore them so
|
||||
new outputs do not change its dirty flag between cases. The source-tree hash excludes bytecode caches; production qualification requires
|
||||
a clean checkout.
|
||||
The runner uses the existing PeriodicBlockStepper / FixedPatchStepper periodic
|
||||
paths only. It does not depend on the new channel-root implementation.
|
||||
|
||||
## Preregistered assessment, before any GPU results
|
||||
|
||||
All sampled times must satisfy finite screening ceilings:
|
||||
- Relative composite velocity L2 <= 5%
|
||||
- Relative globally gauge-adjusted pressure L2 <= 15%
|
||||
- Relative kinetic-energy error <= 5%
|
||||
- Composite mass drift from downloaded t0 <= 1e-5
|
||||
|
||||
These are screening budgets, not literature-certified tolerances. Acceptance
|
||||
also requires all 16 cases, common source/harness, correct nondimensional times
|
||||
and viscosity, and baseline-relative checks on worst post-startup-sample errors:
|
||||
- AMR error <= max(2 * same-root uniform error, declared numerical floor).
|
||||
- Root64 AMR error <= max(.9 * root32 AMR error, floor), for each depth/Re.
|
||||
- Uniform256 error <= max(.9 * uniform32 error, floor).
|
||||
|
||||
Floors: velocity .002, pressure .02, energy .004. They are declared low-Mach /
|
||||
equilibrium-startup budgets, not fitted to observed results. Ma^2=.0027 motivates
|
||||
their scale; it is an asymptotic expectation, not a proof of those coefficients.
|
||||
The coarse baseline sets the AMR budget because much of the domain remains
|
||||
coarse. The finest uniform error and AMR/fine ratio are always reported, with no
|
||||
claim that partial refinement should match a fully fine domain. A pass below a
|
||||
floor is bounded accuracy, not a measured convergence order. Report errors and
|
||||
ratios even for a pass. Failure requires diagnosis or a separately preregistered
|
||||
follow-up, never retroactive tolerance tuning. Higher Re is deliberately absent;
|
||||
Re100/1000 must first be stable, resolved and reviewed.
|
||||
|
||||
Diagnostics use one global pressure gauge, direct cell-center analytic values,
|
||||
kinetic energy per unit reference mass, and actual leaf cell areas. Composite
|
||||
midpoint quadrature introduces a small initial energy integration error, retained
|
||||
rather than subtracted. Mass drift is relative to downloaded t0, so density
|
||||
initialization and mixed-grid quadrature do not masquerade as time drift.
|
||||
|
||||
## Pilot scheduling and transfer-consistency scope
|
||||
|
||||
Start with one uniform case and one matched AMR case; the 16-case matrix is a
|
||||
qualification plan, not authorization to launch every case automatically. The
|
||||
GPU owner reviews the pilot before expanding it. Each process prints a flushed
|
||||
READY only after initialization, a real base step exercising every level's
|
||||
kernel/halo/transfer path, and a finite positive-density download check. That
|
||||
step belongs to the measured trajectory; it is not discarded warmup. PROGRESS
|
||||
messages contain each of the five diagnostic samples. READY alone is not an
|
||||
accuracy pass. Reports include per-level active cells, hops and collision updates.
|
||||
|
||||
The complete matrix costs 768,540,672 active collision updates and 21,504 root
|
||||
intervals, excluding ghost streaming, halos, transfers and diagnostic work.
|
||||
Uniform N256 is the largest case (268,435,456 updates / 4096 intervals per Re).
|
||||
AMR N64/depth3 needs 50,987,008 updates / 1024 intervals. These are work counts,
|
||||
not GPU wall-time estimates. Baseline comparisons remain available on failed
|
||||
screens and partial pilots; absent/nonfinite operands are null, never passing.
|
||||
|
||||
Acoustic viscosity scaling and unchanged lattice velocity do not by themselves
|
||||
prove nonequilibrium-stress consistency across refinement interfaces. A smooth
|
||||
collocated Chapman–Enskog postcollision nonequilibrium has a (tau-1)*h factor;
|
||||
the existing implementation instead uses volumetric piecewise-constant explosion
|
||||
and stream-only ghosts. Whether that actual transfer construction achieves the
|
||||
needed stress accuracy is an unresolved question for this pilot to measure.
|
||||
It is not a proven defect, and a collocated-state scaling factor must not be
|
||||
inserted into this different transfer operator without a separate derivation
|
||||
and validation. This campaign measures the current operator and makes no
|
||||
theoretical stress-consistency certification or production-kernel change.
|
||||
|
||||
## Repository integration
|
||||
|
||||
The four Python harness files remain together under tests/validation/taylor_green, so runtime provenance hashes precisely this harness directory. Formatting changes produce a new harness identity; preparation and integrated receipts must not be mixed. All analytic definitions, numerical floors, assessment gates, sample times, and source-import assertions are unchanged. Production source is unchanged from the cd3 snapshot; these periodic tests do not waive the separate channel GPU gate.
|
||||
@@ -0,0 +1,90 @@
|
||||
# Bounded uniform-grid LES flow validation: prepared, GPU NOT RUN
|
||||
|
||||
## Status and scope
|
||||
|
||||
Prepared for independent review, then an explicitly authorized Horsea run only. No production files were edited, no normalization patch was applied to a full solver checkout, and no GPU was used. This is a small end-to-end **wall-bounded, x-periodic** shear-decay test, not a fully periodic 2D test. It exercises the actual production Simulation → Stepper → OneStep → Smagorinsky → SRT dispatch and double-buffer pull streaming, plus existing halfway bounce-back walls.
|
||||
|
||||
The original fully periodic-y plan is unsupported at the pinned revision: both `one_step_double.cu:90` and `one_step_esopull.cu:133` replace wall-adjacent incoming populations for every fluid node on y=1 and NY-2 regardless of its neighboring flags. `tests/validation/run_poiseuille.py:1–10` already documents this. Merely changing all flags to FLUID does not remove this behavior. The present test deliberately matches those real walls; it does not modify the kernels or claim a periodic-y public API.
|
||||
|
||||
Only unforced SRT, D2Q9, FP32 shifted storage and double-buffer streaming are included. No MRT/TRT, force correction, EsoPull, AMR, Jet, 3D turbulence or physical LES coefficient calibration is certified. In continuum this unidirectional incompressible shear is trace-free; small finite-Mach/discretization stresses in the LBM are not asserted to vanish exactly. The underlying scalar-helper audit remains the cleaner normalization isolation.
|
||||
|
||||
## Exact source contract
|
||||
|
||||
Baseline is explicitly repinned to the production snapshot in `cd3a657f9d3833e812c3cca173525f165b48710a`, whose reviewed AMR-channel source origin is `47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5`. This new experiment does not inherit historical d79 GPU/flow acceptance. The original preparation remains associated with d79; its results are not rewritten.
|
||||
|
||||
`tests/validation/data/les_flow/source_pins.json` records all 101 tracked production source files from that snapshot. The runner checks every hash before importing CelerisLab or touching a GPU. It accepts only:
|
||||
|
||||
- `--variant baseline`: all baseline hashes, including helper SHA256 `ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501`.
|
||||
- `--variant corrected`: the same source bytes except helper SHA256 `a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa`, from the independently reviewed `tests/validation/les/data/smagorinsky_normalization.patch` artifact.
|
||||
|
||||
The runner records the actual checkout commit and exact imported package. It never applies a patch itself. Run it against separate isolated full checkouts. The patch candidate's minimal header tree is not a runnable solver checkout. A source mismatch is a blocker, not permission to regenerate pins from an arbitrary new checkout.
|
||||
|
||||
Relevant pinned implementation evidence:
|
||||
|
||||
- `tests/integration/test_numerical_gpu.py:24–85`: equilibrium construction, installation of spatial postcollision state, count=1 adapter, raw population observation and x-periodic/walled flags.
|
||||
- `tests/validation/run_poiseuille.py:29–78`: production Simulation use with periodic x, halfway y walls and downloaded populations.
|
||||
- `src/CelerisLab/lbm/kernels/core/layout.cuh:87–104`: actual modulo neighbors.
|
||||
- `src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh:18–38`: incoming populations copied from opposite local input directions.
|
||||
- `src/CelerisLab/lbm/kernels/operators/helpers.cuh:53–61`: LES effective omega feeds actual SRT collision.
|
||||
- `src/CelerisLab/cuda/compiler_v2.py:56–95`: compiler snapshots into private temporary build directories; package sources are not rewritten.
|
||||
|
||||
## Geometry, units and initial state
|
||||
|
||||
8×34 nodes: 8 periodic-x columns, 32 fluid rows, 2 ghost-wall rows. Relative to the bottom physical wall, fluid centers are y=0.5,...,31.5; physical halfway walls are y=0 and 32. Every x edge fluid node is explicitly FLUID; all top/bottom ghost nodes are SOLID|BC_WALL. Thus unused inlet/outlet configuration has no inlet/outlet flags to act on. There are no bodies, Jets or external forces.
|
||||
|
||||
Lattice units: dx_LBM=dt_LBM=1, rho=1, nu=0.02, tau0=0.56, U=0.04, H=32. Initial u_x(y)=U sin(pi y/H), u_y=0. The maximum Mach number is 0.0693. Height-based Re=U H/nu=64; a wavelength-based Re would differ and is not used. No dimensional physical length/time/viscosity is asserted without a user-supplied unit mapping.
|
||||
|
||||
Initialize equilibrium populations, encode shifted FP32, upload both DB buffers and set completed-count=1 following the existing integration-test adapter. That count is storage metadata: physical elapsed time starts at 0 after installation. Observe density and momentum directly from current postcollision populations, adding the descriptor weights in float64; do not mix in cached macro fields. This uses a clearly labeled validation-only raw state/flag adapter, not a promised public `set_periodic` or `set_velocity_profile` API.
|
||||
|
||||
Cases: LES enabled/closed-form with Cs=0 (molecular control), Cs=.16 (ordinary-coefficient sanity), then Cs=1 (amplified normalization diagnostic). Cs=1 is intentionally not a physically recommended SGS coefficient. Initial peak modeled nu_t/nu = Cs² Δ² U(pi/H)/nu = 0.19635; estimated effective tau remains near 0.56–0.5718, far from omega guards. Standard default Cs=.16 yields a much weaker signal at these safe parameters and is not used to make a decisive short-run normalization claim. No defaults are changed.
|
||||
|
||||
Observe at elapsed steps 519, 1038, 2075, approximately 0.1, 0.2, 0.4 molecular decay times. Three cases total 6225 measured advancing launches plus 3 warm-up launches per checkout (6228 total), three snapshots each. Population buffers contain only 272×9 FP32 entries each (about 9.6 KiB), apart from ordinary driver/compiler/context overhead. No frames, trajectories or heavy output. Full result JSON is a few tens of KiB. Runtime depends on compilation/device; no performance claim is made.
|
||||
|
||||
## Independent continuum reference
|
||||
|
||||
For u=(u(y),0), |S|=sqrt(2 S:S)=|du/dy|. Therefore
|
||||
|
||||
du/dt = d/dy [(nu + (Cs Delta)^2 |du/dy|) du/dy],
|
||||
u(0,t)=u(H,t)=0.
|
||||
|
||||
`tests/validation/les_flow_oracle.py` integrates this equation directly with centered finite-volume face fluxes and SSPRK2, at 128 and 256 cells on the same physical H=32 domain. Stability step size uses the nonlinear flux derivative nu+2(Cs Delta)^2|gradient|. Boundary face gradients use the zero Dirichlet value across a half cell. It does not import or reproduce the LBM quadratic/helper. A separate fixed-n=256 test halves the CFL factor from .4 to .2 for Cs=0,.16,1 at every checkpoint, requiring relative changes below 1e-8; measured maximum is 1.8141e-10. This isolates temporal error from the spatial-refinement test.
|
||||
|
||||
Crucially the SGS filter Delta=1 stays fixed as the CPU oracle mesh changes from dy=.25 to .125. Confusing oracle spacing with filter width would change the PDE and invalidate convergence. Future LBM grid refinement also needs an explicit physical-unit/filter-width scaling contract; this artifact does not silently reinterpret Cs or Delta when resizing H.
|
||||
|
||||
Cs=0 is checked directly against U sin(pi y/H) exp[-nu(pi/H)^2 t]. The nonlinear reference includes evolving higher harmonics, rather than assuming it remains a sine. At the final time, host oracle predicts 5.1706% SGS reduction in the fundamental amplitude and 1.5409% full-profile separation between standard and old coefficient conventions. The old convention reference is computed by changing continuum Cs to Cs/2^(1/4), purely as a diagnostic; it does not replace standard acceptance.
|
||||
|
||||
## Predeclared criteria (do not tune after seeing GPU results)
|
||||
|
||||
- Initial decoded profile relative L2 <= 1e-5; otherwise no advancing test.
|
||||
- Molecular Cs=0 profile error against analytic decay <= 0.003 at every checkpoint.
|
||||
- Cs=1 full-profile error against standard nonlinear oracle <= 0.005 at every checkpoint.
|
||||
- Oracle 128/256-grid interpolated relative difference <= 8e-5 at every checkpoint.
|
||||
- Relative error in SGS fundamental-amplitude decrement <= 0.15 at the last two checkpoints. Decrement is measured from actual Cs=0 amplitude minus actual Cs=1 amplitude; reference uses analytic Cs=0 minus the nonlinear oracle. The first checkpoint remains a transient diagnostic for this increment gate.
|
||||
- Fluid mean-density drift <= 5e-5, max |u_y| <= 1e-5 and x-column velocity spread <= 1e-6 at every checkpoint; finite populations, strictly positive finite fluid density and finite decoded velocity/profile/diagnostics required throughout observations, including startup and warm-up.
|
||||
|
||||
Reasons: molecular 0.3% leaves room for finite-lattice dispersion, equilibrium-start kinetic transient and FP32 error at H=32; nonlinear 0.5% is substantially below the predicted 1.54% old-vs-standard separation. The 15% increment tolerance is below the old convention's approximately 29% viscosity-coefficient deficit, while allowing continuum/lattice differences. Directly subtracting the two measured amplitudes reduces common lattice error. Oracle convergence is over 60 times tighter than the nonlinear profile bound. These are declared engineering test gates, not GPU-validated uncertainty estimates; a failure requires diagnosis/refinement and an explicitly revised experiment, never a post-hoc tolerance edit to obtain a pass.
|
||||
|
||||
Both baseline and candidate are judged against the same standard convention. Baseline is expected to fail SGS criteria if the flow reproduces the scalar finding; an exit 1 with valid evidence is useful. The runner reports old-convention profile error but never masks baseline failure. Candidate passing alone is insufficient if baseline does not demonstrate meaningful discrimination. Review both results and source pins together.
|
||||
|
||||
## Host verification and later GPU commands
|
||||
|
||||
From the repository root, using existing Python/NumPy and the existing pytest environment:
|
||||
|
||||
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python -m pytest -q tests/unit/test_les_flow_oracle.py tests/unit/test_les_flow_runner.py
|
||||
|
||||
Nine tests cover analytic decay, independent-oracle mesh convergence, symmetry/positivity/energy decay, filter invariance, fixed-grid timestep halving, signal discrimination, invalid density/velocity rejection, source pins/candidate/tamper rejection and the no-GPU-without-opt-in CLI guard. The integrated tests preserve those checks. No PyCUDA import or GPU run is required for them. Cs=.16 uses the same 0.5% full-profile safety/sanity bound; its SGS increment is not a discriminatory normalization gate.
|
||||
|
||||
The source-guard tests materialize temporary candidate headers with the existing reviewed LES preparation tool; the GPU entrypoint uses `tests/validation/run_uniform_les_flow.py`, `tests/validation/les_flow_oracle.py`, and `tests/validation/data/les_flow/source_pins.json` in that repository layout.
|
||||
|
||||
After review, make isolated full baseline and corrected checkouts of the exact base. Apply only `tests/validation/les/data/smagorinsky_normalization.patch` to the corrected checkout; do not edit the benchmark/production tree. On authorized Horsea, with existing NumPy, PyCUDA, nvcc and driver, run sequentially and select the authorized physical GPU explicitly:
|
||||
|
||||
CUDA_VISIBLE_DEVICES=<physical-GPU> PYTHONDONTWRITEBYTECODE=1 python tests/validation/run_uniform_les_flow.py --run-gpu --device 0 --root <isolated-baseline> --variant baseline --output <new-evidence-dir>/baseline.json
|
||||
CUDA_VISIBLE_DEVICES=<physical-GPU> PYTHONDONTWRITEBYTECODE=1 python tests/validation/run_uniform_les_flow.py --run-gpu --device 0 --root <isolated-corrected> --variant corrected --output <new-evidence-dir>/corrected.json
|
||||
|
||||
`--device 0` is the logical ordinal within CUDA_VISIBLE_DEVICES. Do not use a shell `&&` between these: baseline's expected standard-criterion failure must not suppress the corrected run. Existing output files are refused. A JSON `READY` line is printed separately for each case only after source verification, compilation, initialization, raw-state installation, one actual advancing OneStep launch, post-warm-up macro validation, complete restoration of both DB buffers/flags/count to the initial state, profile readback and CUDA synchronization. It gives source identity, device, Cs, warm-up/reset confirmation, installed-profile error and maximum measured steps. This is the first trustworthy startup checkpoint, not a statement that the physics passed. `DONE` is printed after all three cases and all criteria; failures/compile errors before that remain incomplete runs.
|
||||
|
||||
Deliver both JSON files and stdout/stderr with artifact hashes. Review before any production integration. AMR LES guards remain untouched.
|
||||
|
||||
## Isolated candidate preparation
|
||||
|
||||
Use the current validation commit for two separate full checkouts, preserving the baseline. In the corrected checkout only, apply `tests/validation/les/data/smagorinsky_normalization.patch` with `git apply`; do not commit that experimental production change or update the pins. The runner requires exactly the baseline bytes plus the reviewed helper hash. The header-only scalar diagnostic tree is insufficient for this end-to-end flow. Baseline and corrected commands must run as separate Python processes; package import caching must never combine their implementations. Both reject an imported CelerisLab package outside the explicitly supplied checkout, including an installed DynamisLab copy. Retain the actual checkout commit, variant, exact header hash, and experimental source label with each result.
|
||||
@@ -90,8 +90,8 @@ def main():
|
||||
# The caller owns the CUDA context; the stepper must be closed first (the
|
||||
# nested with-statement does that).
|
||||
with CudaContext(0) as context, FixedPatchStepper(
|
||||
cfg, BOUNDS, context=context, coarse_boundary=args.mode,
|
||||
fine_objects=[body], fine_block=fine_block) as run:
|
||||
cfg, context=context, coarse_boundary=args.mode, refinements=[BOUNDS],
|
||||
level_blocks=[None, fine_block], level_objects=[(), (body,)]) as run:
|
||||
p = run.patch
|
||||
# Rest equilibrium is already postcollision G, so no initial collision is
|
||||
# needed. Non-leaf entries (covered, ghost, solid) are discarded by initialize.
|
||||
|
||||
@@ -148,8 +148,8 @@ def main():
|
||||
samples=[], status='running', last_synchronized_coarse_step=0)
|
||||
start, run = time.monotonic(), None
|
||||
try:
|
||||
with CudaContext(0) as context, FixedPatchStepper(cfg, BOUNDS, context=context,
|
||||
coarse_boundary='slot_reservoir') as run:
|
||||
with CudaContext(0) as context, FixedPatchStepper(cfg, context=context,
|
||||
coarse_boundary='slot_reservoir', refinements=[BOUNDS]) as run:
|
||||
p = run.patch
|
||||
masks = [np.ones(p.coarse_shape, bool), p.fine_kind == ACTIVE]
|
||||
masks[0][[0, -1]] = False
|
||||
|
||||
@@ -48,7 +48,7 @@ def main():
|
||||
nx=16, ny=16, viscosity=.1, velocity=1e-3, rho=1.0,
|
||||
lattice_model="D2Q9", collision="SRT", streaming="double_buffer",
|
||||
store_precision="FP32", ddf_shifting=False, les_enabled=False)
|
||||
with CudaContext(0) as context, PeriodicPatchStepper(cfg, (4, 4, 12, 12), context=context) as run:
|
||||
with CudaContext(0) as context, PeriodicPatchStepper(cfg, context=context, refinements=[(4, 4, 12, 12)]) as run:
|
||||
p = run.patch
|
||||
coarse = shear_equilibrium(p.coarse_shape, (0, 0), 1.0, cfg.ny)
|
||||
fine = shear_equilibrium(p.fine_shape, p.fine_origin, .5, cfg.ny)
|
||||
|
||||
+37
-11
@@ -9,6 +9,11 @@ whose links are evaluated; callers convert frames/units before evaluation.
|
||||
One physical link (receiver, direction) may be driven by at most one jet, and
|
||||
one jet may drive the links of only one body (checked at assignment, any amplitude).
|
||||
|
||||
``condition`` names the prescribed boundary type. Only ``"velocity"`` (the
|
||||
default: amplitude is a signed lattice velocity, not a flow rate) is
|
||||
implemented; ``"pressure"`` is reserved and rejected at construction, before
|
||||
any amplitude validation, so it never reaches assignment or a kernel.
|
||||
|
||||
Edge rule: the lateral bound |s| < 1 is exclusive (the profile vanishes
|
||||
there for parabolic/quartic); Sector r_min/r_max and Tube 0 <= along <= length
|
||||
are inclusive. Jets are immutable; an amplitude change replaces the Jet.
|
||||
@@ -25,6 +30,9 @@ from dataclasses import dataclass, replace
|
||||
|
||||
import numpy as np
|
||||
|
||||
CONDITIONS = ("velocity",)
|
||||
RESERVED_CONDITIONS = ("pressure",)
|
||||
|
||||
PROFILES = {
|
||||
"uniform": lambda s: np.ones_like(s),
|
||||
"parabolic": lambda s: 1 - s*s,
|
||||
@@ -122,7 +130,8 @@ class Parallel:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Jet:
|
||||
"""Region (Sector|Tube), field (Radial|Parallel), profile name, signed amplitude.
|
||||
"""Region (Sector|Tube), field (Radial|Parallel), profile name, signed amplitude,
|
||||
prescribed boundary condition (velocity only; pressure reserved).
|
||||
|
||||
Frozen: change amplitude with ``with_amplitude`` (validated, new object);
|
||||
the cached per-link shapes stay valid because region/field/profile cannot change.
|
||||
@@ -131,8 +140,14 @@ class Jet:
|
||||
field: object
|
||||
profile: str = "uniform"
|
||||
amplitude: float = 0.0
|
||||
condition: str = "velocity"
|
||||
|
||||
def __post_init__(self):
|
||||
# The condition decides what amplitude and field mean: check it first.
|
||||
if self.condition in RESERVED_CONDITIONS:
|
||||
raise NotImplementedError(f"{self.condition} jets are reserved but not implemented")
|
||||
if self.condition not in CONDITIONS:
|
||||
raise ValueError(f"Jet condition must be one of {CONDITIONS + RESERVED_CONDITIONS}")
|
||||
if self.profile not in PROFILES:
|
||||
raise ValueError(f"Jet profile must be one of {sorted(PROFILES)}")
|
||||
_finite(self.amplitude)
|
||||
@@ -189,24 +204,33 @@ def edge_to_node_frame(points):
|
||||
|
||||
|
||||
def surface_link_velocity(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
||||
jets=(), hits=None):
|
||||
jets=(), hits=None, *, _shifted_blocked=False):
|
||||
"""Per-link prescribed surface velocity (n, 2) FP32 and aperture mask.
|
||||
|
||||
See _surface_links, which also returns the unit-amplitude jet shapes.
|
||||
"""
|
||||
return _surface_links(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry, jets, hits,
|
||||
_shifted_blocked=_shifted_blocked)[:2]
|
||||
|
||||
|
||||
def _surface_links(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
||||
jets=(), hits=None, *, _shifted_blocked=False):
|
||||
"""(velocity, aperture, jet shape, jet id); the last two are None without jets.
|
||||
|
||||
Host-only and storage-neutral: rows follow the given physical cut-link
|
||||
order. Returns (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
|
||||
not share a link with each other or with a legacy aperture.
|
||||
Shifted storage is privately enabled only by the all-blocked stepper.
|
||||
"""
|
||||
enabled = [obj for obj in objects if obj.surface_actuation is not None]
|
||||
if not enabled and not jets:
|
||||
return None, None
|
||||
from ..lbm.descriptors import FLUID, SOLID
|
||||
if np.any((flags[fluid_idx] & FLUID) == 0) or np.any(flags[fluid_idx] & SOLID):
|
||||
raise ValueError("Surface cut links must have active fluid receivers")
|
||||
if len(np.unique(fluid_idx.astype(np.uint64)*cfg.nq+dir)) != len(dir):
|
||||
raise ValueError("Surface boundary requires unique cut-link writers")
|
||||
return None, None, None, None
|
||||
from .preprocess.flag_overlay import validate_cut_link_ownership
|
||||
validate_cut_link_ownership(flags, fluid_idx, dir, cfg.nq)
|
||||
surface_velocity = np.zeros((len(dir), 2), np.float32)
|
||||
surface_aperture = np.zeros(len(dir), bool)
|
||||
shape = jet_id = None
|
||||
for obj in enabled:
|
||||
obj.validate_surface_actuation(cfg)
|
||||
selected = body_id == obj.obj_id
|
||||
@@ -219,7 +243,9 @@ def surface_link_velocity(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
||||
if hits is None or len(hits) != len(dir):
|
||||
raise ValueError("Jets require true hit points row-aligned with the cut links")
|
||||
if (cfg is None or not cfg.is_d2q9 or cfg.store_precision != "FP32"
|
||||
or cfg.streaming != "double_buffer" or cfg.ddf_shifting):
|
||||
or cfg.streaming != "double_buffer"
|
||||
or (cfg.ddf_shifting and not (_shifted_blocked is True
|
||||
and cfg.collision == "SRT" and not cfg.les_enabled))):
|
||||
raise ValueError("Jets require D2Q9/FP32/unshifted/double_buffer")
|
||||
shape, jet_id = jet_link_shapes(jets, hits)
|
||||
driven = jet_id >= 0
|
||||
@@ -237,4 +263,4 @@ def surface_link_velocity(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
||||
raise ValueError("Jet overlaps a SurfaceActuation aperture")
|
||||
surface_velocity[driven] = jet_link_velocity(jets, shape, jet_id)[driven]
|
||||
surface_aperture |= driven
|
||||
return surface_velocity, surface_aperture
|
||||
return surface_velocity, surface_aperture, shape, jet_id
|
||||
|
||||
@@ -27,7 +27,7 @@ import pycuda.driver as cuda
|
||||
|
||||
from .objects import SimObject
|
||||
from .registry import BodyRegistry
|
||||
from .preprocess.flag_overlay import merge_flag_masks
|
||||
from .preprocess.flag_overlay import merge_flag_masks, validate_cut_link_ownership
|
||||
from ..cuda.compiler_v2 import obs_layout
|
||||
from ..cuda.obs_layout import ObsLayout
|
||||
from ..lbm.descriptors import (
|
||||
@@ -369,9 +369,9 @@ class ObjectManager:
|
||||
|
||||
Args:
|
||||
objects: List of SimObject instances to process.
|
||||
domain_flags: Optional uint16 flags for donor-cell classification.
|
||||
domain_flags: Optional uint16 flags for receiver/donor classification.
|
||||
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
|
||||
points row-aligned with the cut-link columns (for Jets).
|
||||
|
||||
@@ -394,8 +394,8 @@ class ObjectManager:
|
||||
active_fluid = ((domain_flags & FLUID) != 0) & ((domain_flags & SOLID) == 0)
|
||||
force_owners: set[int] = set()
|
||||
|
||||
if sensor_cell_counts_out is not None:
|
||||
sensor_cell_counts_out.fill(0)
|
||||
sensor_counts = (np.zeros_like(sensor_cell_counts_out)
|
||||
if sensor_cell_counts_out is not None else None)
|
||||
|
||||
ez = None
|
||||
if self.cfg and self.cfg.is_d3q19:
|
||||
@@ -430,9 +430,9 @@ class ObjectManager:
|
||||
if len(cells) > 0:
|
||||
s_cells.append(cells)
|
||||
s_ids.append(ids)
|
||||
if (sensor_cell_counts_out is not None
|
||||
and 0 <= obj.obj_id < sensor_cell_counts_out.size):
|
||||
sensor_cell_counts_out[obj.obj_id] = int(len(cells))
|
||||
if (sensor_counts is not None
|
||||
and 0 <= obj.obj_id < sensor_counts.size):
|
||||
sensor_counts[obj.obj_id] = int(len(cells))
|
||||
if obj.is_force_region and hasattr(obj, 'get_sensor_list'):
|
||||
cells, ids = obj.get_sensor_list(self.nx, self.ny, self.nz)
|
||||
keep = active_fluid[cells]
|
||||
@@ -461,8 +461,9 @@ class ObjectManager:
|
||||
r_fr_ids = np.concatenate(fr_ids) if fr_ids else np.zeros(0, dtype=np.int32)
|
||||
|
||||
r_hit = np.concatenate(cl_hit) if cl_hit else np.zeros((0, 2))
|
||||
if hits_out is not None:
|
||||
hits_out.append(r_hit)
|
||||
# Passive walls need the same ownership guarantees as driven walls.
|
||||
# Overlapping solids are not a supported union-boundary operation.
|
||||
validate_cut_link_ownership(domain_flags, r_idx, r_dir, self.nq)
|
||||
if self.jets or any(obj.surface_actuation is not None for obj in objects):
|
||||
# Reject unresolved apertures/ownership before sync or restart mutates state.
|
||||
from ..lbm.curved_links import CurvedLinkSoA
|
||||
@@ -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_surface(objects, self.cfg, domain_flags, jets=self.jets, hits=r_hit)
|
||||
|
||||
if sensor_cell_counts_out is not None:
|
||||
sensor_cell_counts_out[:] = sensor_counts
|
||||
if hits_out is not None:
|
||||
hits_out.append(r_hit)
|
||||
|
||||
return (
|
||||
r_idx, r_dir, r_q, r_bid,
|
||||
r_rx, r_ry, r_rz, r_fallback,
|
||||
@@ -487,8 +493,6 @@ class ObjectManager:
|
||||
the caller has already prepared and uploaded the final flag field
|
||||
for this geometry, such as during initialization.
|
||||
"""
|
||||
self._telemetry_field = field
|
||||
|
||||
next_flags = self.build_flags(field.build_channel_flags()) if rebuild_flags else field.flag
|
||||
hits = []
|
||||
|
||||
@@ -499,6 +503,7 @@ class ObjectManager:
|
||||
fr_cells, fr_obj_id,
|
||||
) = self.build_compact_lists(domain_flags=next_flags, hits_out=hits)
|
||||
|
||||
self._telemetry_field = field
|
||||
if rebuild_flags:
|
||||
field.flag = next_flags
|
||||
field.upload_flags()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CelerisLab/body/preprocess/flag_overlay.py
|
||||
"""
|
||||
Flag overlay: 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
|
||||
@@ -12,6 +12,15 @@ import numpy as np
|
||||
from ...lbm.descriptors import FLUID, SOLID, SENSOR_FLAG, FRC_REGION
|
||||
|
||||
|
||||
def validate_cut_link_ownership(flags, fluid_idx, directions, nq):
|
||||
"""Reject solid receivers and multiple writers to one population."""
|
||||
if np.any((flags[fluid_idx] & FLUID) == 0) or np.any(flags[fluid_idx] & SOLID):
|
||||
raise ValueError("Cut links must have active fluid receivers")
|
||||
writers = fluid_idx.astype(np.uint64) * nq + directions
|
||||
if len(np.unique(writers)) != len(directions):
|
||||
raise ValueError("Boundary requires unique cut-link writers")
|
||||
|
||||
|
||||
def merge_flag_masks(
|
||||
base: np.ndarray,
|
||||
masks: List[np.ndarray],
|
||||
|
||||
@@ -54,14 +54,18 @@ def _write(path: str, content: str):
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Returns its path. Pass it explicitly to compile_kernel for interleaved
|
||||
builds. Legacy generate_config(); compile_kernel() remains context-local.
|
||||
The caller owns cleanup_build_dir() once the loaded module is no longer
|
||||
needed for recompilation/debugging. No package files are changed.
|
||||
block_root_bc specializes a blocked root module; fine/dense builds use 0.
|
||||
"""
|
||||
if (isinstance(block_root_bc, bool) or not isinstance(block_root_bc, int)
|
||||
or block_root_bc not in (0, 1, 2)):
|
||||
raise ValueError("block_root_bc must be an integer mode 0, 1 or 2")
|
||||
macros = cfg.to_macros()
|
||||
if isinstance(n_objects, bool) or not isinstance(n_objects, int) or n_objects < 0:
|
||||
raise ValueError("n_objects must be a nonnegative integer")
|
||||
@@ -71,14 +75,15 @@ def generate_config(cfg: LBMConfig, n_objects: int = 0,
|
||||
try:
|
||||
shutil.copytree(_KERNEL_DIR, build_dir, dirs_exist_ok=True,
|
||||
ignore=shutil.ignore_patterns("*.ptx", "*.cubin", "*.tmp"))
|
||||
_render_config(macros, n_objects, os.path.join(build_dir, "config"))
|
||||
_render_config(macros, n_objects, os.path.join(build_dir, "config"),
|
||||
block_root_bc=block_root_bc)
|
||||
digest = hashlib.sha256()
|
||||
for path in sorted(Path(build_dir).rglob("*")):
|
||||
if path.is_file():
|
||||
digest.update(str(path.relative_to(build_dir)).encode())
|
||||
digest.update(path.read_bytes())
|
||||
metadata = {"source_sha256": digest.hexdigest(), "config": asdict(cfg),
|
||||
"n_objects": n_objects}
|
||||
"n_objects": n_objects, "block_root_bc": block_root_bc}
|
||||
_write(os.path.join(build_dir, ".celeris-build"), json.dumps(metadata, sort_keys=True))
|
||||
except BaseException:
|
||||
shutil.rmtree(build_dir)
|
||||
@@ -101,7 +106,7 @@ def cleanup_build_dir(build_dir: Optional[str]) -> None:
|
||||
_last_ptx_path.set(None)
|
||||
|
||||
|
||||
def _render_config(m: dict, n_objects: int, cdir: str):
|
||||
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}\
|
||||
// Layer: Global / Grid
|
||||
#ifndef CELERIS_CONFIG_GRID_H
|
||||
@@ -165,6 +170,9 @@ def _render_config(m: dict, n_objects: int, cdir: str):
|
||||
#define STORE_PRECISION {m['STORE_PRECISION']}
|
||||
#define USE_DDF_SHIFTING {m['USE_DDF_SHIFTING']}
|
||||
|
||||
// Internal blocked root mode: 0=periodic/fine, 1=slot/reservoir, 2=channel.
|
||||
#define BLOCK_ROOT_BC {block_root_bc}
|
||||
|
||||
#define USE_LES {m['USE_LES']}
|
||||
#define LES_CS {m['LES_CS']}
|
||||
#define LES_CLOSED_FORM {m['LES_CLOSED_FORM']}
|
||||
|
||||
@@ -21,6 +21,7 @@ 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)
|
||||
|
||||
@@ -85,10 +86,10 @@ class PeriodicBlockStepper:
|
||||
checked.validate()
|
||||
if (not checked.is_d2q9 or checked.collision != "SRT"
|
||||
or checked.streaming != "double_buffer" or checked.store_precision != "FP32"
|
||||
or checked.ddf_shifting or checked.les_enabled
|
||||
or checked.les_enabled
|
||||
or (checked.ny, checked.nx) != layout.shape):
|
||||
raise ValueError("PeriodicBlockStepper requires matching D2Q9/SRT/FP32/"
|
||||
"unshifted/double_buffer config without LES")
|
||||
"double_buffer config without LES")
|
||||
ly, lx = layout.local_shape
|
||||
pairs = len(layout.halo_dst)
|
||||
tpb = checked.threads_per_block
|
||||
@@ -99,6 +100,7 @@ class PeriodicBlockStepper:
|
||||
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
|
||||
@@ -177,14 +179,20 @@ class PeriodicBlockStepper:
|
||||
"""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.float32, order="C", copy=True)
|
||||
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.
|
||||
blocked = self.layout.to_blocked(dense)
|
||||
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)
|
||||
@@ -196,11 +204,21 @@ class PeriodicBlockStepper:
|
||||
raise
|
||||
|
||||
def step(self, n=1):
|
||||
"""Advance n steps: halo refresh, then core pull/collide, per step."""
|
||||
"""Advance n steps: halo refresh, then core pull/collide, per step.
|
||||
|
||||
All n steps are enqueued in order on the one stream and the host waits
|
||||
once before returning (same launches and bits as n step(1) calls).
|
||||
Asynchronous device errors surface at that single wait, so they are not
|
||||
attributed to the step that caused them; call step(1) repeatedly to
|
||||
localize one. On any failure the instance is failed and step_count keeps
|
||||
its entry value.
|
||||
"""
|
||||
if isinstance(n, bool) or not isinstance(n, Integral) or n < 0:
|
||||
raise ValueError("n must be a nonnegative integer")
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("ready")
|
||||
if n == 0:
|
||||
return
|
||||
kw = dict(block=self._block, stream=self._stream)
|
||||
try:
|
||||
self._status = "advancing"
|
||||
@@ -216,16 +234,21 @@ class PeriodicBlockStepper:
|
||||
self._step(self._current, self._spare, np.uint32(self.layout.halo),
|
||||
self._tables.get("flags", np.uintp(0)), np.uintp(0), # no kind
|
||||
grid=self._grid, **kw)
|
||||
self._stream.synchronize()
|
||||
self._current, self._spare = self._spare, self._current
|
||||
self._step_count += 1
|
||||
self._current, self._spare = self._spare, self._current # host handles only
|
||||
self._stream.synchronize()
|
||||
self._step_count += n
|
||||
self._status = "ready"
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
|
||||
def download(self):
|
||||
"""Return dense (9, ny, nx) FP32 core populations; no host cache."""
|
||||
"""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)
|
||||
@@ -234,7 +257,7 @@ class PeriodicBlockStepper:
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
return self.layout.to_dense(blocked)
|
||||
return _StoredPopulations((self.layout.to_dense(blocked),), self._ddf_shifting)
|
||||
|
||||
def close(self):
|
||||
"""Release owned resources; retain them for retry if context activation fails."""
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Host contract for the blocked root's uniform SRT/MRT channel boundary.
|
||||
|
||||
Boundary ghosts are evolved state, but are not physical fluid leaves. Halo
|
||||
ownership remains periodic to reproduce the uniform kernel's wrapped pulls.
|
||||
No boundary closure is applied on a fine level.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
|
||||
def channel_masks(shape):
|
||||
"""Physical fluid and bulk-transfer masks in GLOBAL root coordinates."""
|
||||
ny, nx = shape
|
||||
fluid = np.zeros(shape, bool)
|
||||
fluid[1:ny - 1, 1:nx - 1] = True
|
||||
bulk = fluid.copy()
|
||||
bulk[[1, ny - 2]] = False
|
||||
return fluid, bulk
|
||||
|
||||
|
||||
def validate_channel_config(cfg, blocks):
|
||||
if any(block is None for block in blocks):
|
||||
raise ValueError("channel requires all-blocked levels")
|
||||
if (cfg.inlet_scheme != "zou_he_local" or cfg.outlet_mode != "neq_extrap"
|
||||
or cfg.y_wall_bc != "bounce_back" or cfg.top_wall_bc != "velocity"
|
||||
or cfg.wall_slot_width != 0 or cfg.wall_normal_velocity != 0):
|
||||
raise ValueError("channel requires Zou-He inlet, pressure NEQ outlet and stationary bounce-back y walls")
|
||||
|
||||
|
||||
def validate_channel_transfers(pair, layout, targets):
|
||||
"""Reject actual exchange/closure overlap, not a padded geometric envelope.
|
||||
|
||||
Explosion donors and restriction receivers must use ordinary bulk collision
|
||||
semantics. Restriction writes receiver-local source slots; these may not
|
||||
overwrite the channel's extra immutable-input reads (pressure donor and
|
||||
same-node halfway-wall corrections). The slot check deliberately respects
|
||||
block seams, rather than treating different halo images as the same slot.
|
||||
"""
|
||||
ny, nx = pair.coarse_shape
|
||||
_, bulk = channel_masks(pair.coarse_shape)
|
||||
legal = bulk.ravel() & ~pair.covered.ravel()
|
||||
receivers = pair.coalesce_coarse % (ny * nx)
|
||||
if not legal[pair.ghost_coarse].all() or not legal[receivers].all():
|
||||
raise ValueError("channel transfer donors and receivers must be uncovered bulk fluid")
|
||||
owner = layout.owner
|
||||
reads = []
|
||||
# Outlet closes from the full previous-step state at its left neighbor.
|
||||
donor = owner[1:-1, -1].astype(np.int64) - 1
|
||||
for q in range(9):
|
||||
reads.append(layout.slot(donor, q))
|
||||
for y, directions in ((1, (4, 6, 7)), (ny - 2, (3, 5, 8))):
|
||||
for q in directions:
|
||||
reads.append(layout.slot(owner[y, 1:-1], q))
|
||||
if np.intersect1d(targets, np.concatenate(reads)).size:
|
||||
raise ValueError("channel restriction writes overlap immutable boundary closure reads")
|
||||
@@ -94,12 +94,10 @@ class CurvedLinkSoA:
|
||||
"""
|
||||
self.surface_velocity = self.aperture = 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,
|
||||
jets=self.jets, hits=hits)
|
||||
if self.jets:
|
||||
from ..body.jets import jet_link_shapes
|
||||
self.jet_shape, self.jet_id = jet_link_shapes(self.jets, hits)
|
||||
|
||||
def set_jet_amplitude(self, index, amplitude, stream=None):
|
||||
"""Rewrite jet rows from cached shapes; re-upload only the velocity column.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// CelerisLab/lbm/kernels/blocks.cu
|
||||
// One periodic level of fixed-size blocks (lbm/blocks.py layout). Separate
|
||||
// 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).
|
||||
@@ -14,22 +15,31 @@
|
||||
#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"
|
||||
#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 DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Blocks require unshifted FP32 D2Q9 SRT double buffers without LES"
|
||||
#if BLOCK_ROOT_BC == 1
|
||||
#include "boundary/block_wall.cuh"
|
||||
#endif
|
||||
#if BLOCK_ROOT_BC != 2
|
||||
#include "operators/block_collision.cuh"
|
||||
#endif
|
||||
|
||||
#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)
|
||||
@@ -60,6 +70,39 @@ extern "C" __global__ void BlockExplode(
|
||||
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(
|
||||
@@ -111,6 +154,7 @@ extern "C" __global__ void BlockAccumulateExchange(
|
||||
integral[k + 3] += h * h * (double)raw[k + 3];
|
||||
}
|
||||
|
||||
#if BLOCK_ROOT_BC != 2
|
||||
// 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
|
||||
@@ -131,21 +175,18 @@ extern "C" __global__ void BlockStep(const fpxx* in, fpxx* out, unsigned int hal
|
||||
compute_neighbors(x, y, j); // Core neighbors stay inside the window: no wrap.
|
||||
stream_pull_load(k, f, in + base, j);
|
||||
if (cell_kind == KIND_ACTIVE) {
|
||||
if (d_block_wall.nx != 0) {
|
||||
const unsigned int bx = NX - 2 * halo, by = NY - 2 * halo;
|
||||
const unsigned int nbx = d_block_wall.nx / bx;
|
||||
const unsigned int gx = (blockIdx.z % nbx) * bx + x - halo;
|
||||
const unsigned int gy = (blockIdx.z / nbx) * by + y - halo;
|
||||
if (gy == 1)
|
||||
reconstruct_bottom_velocity(f, 0.0f, bottom_wall_normal_velocity(
|
||||
gx, d_block_wall.nx, d_block_wall.slot_width, d_block_wall.amplitude));
|
||||
else if (gy == d_block_wall.ny - 2) reconstruct_top_reservoir(f, RHO);
|
||||
}
|
||||
float rho, ux, uy, feq[NQ];
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
collide_srt_no_force(f, feq, d_params.omega);
|
||||
#if BLOCK_ROOT_BC == 1
|
||||
block_wall(f, x, y, halo);
|
||||
#endif
|
||||
block_collide(f);
|
||||
}
|
||||
}
|
||||
stream_pull_store(k, f, out + base);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// Root channel is a replacement full step, selected once by the host.
|
||||
#if BLOCK_ROOT_BC == 2
|
||||
#include "step/block_channel.cu"
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
@@ -27,18 +27,22 @@
|
||||
#define INLET_REGULARIZED_NEQ_DAMP 0.50f
|
||||
#endif
|
||||
|
||||
__device__ __forceinline__ float inlet_target_u(float y_coord) {
|
||||
__device__ __forceinline__ float inlet_target_u(float y_coord, unsigned int ny, float velocity) {
|
||||
#if INLET_PROFILE == 0
|
||||
return U0;
|
||||
return velocity;
|
||||
#else
|
||||
const float y_clamped = fminf((float)(NY - 2), fmaxf(1.0f, y_coord));
|
||||
const float H = fmaxf((float)(NY - 2), 1.0f);
|
||||
const float y_clamped = fminf((float)(ny - 2), fmaxf(1.0f, y_coord));
|
||||
const float H = fmaxf((float)(ny - 2), 1.0f);
|
||||
const float eta = (y_clamped - 0.5f) / H;
|
||||
const float shape = fmaxf(0.0f, 4.0f * eta * (1.0f - eta));
|
||||
return U0 * 1.5f * shape;
|
||||
return velocity * 1.5f * shape;
|
||||
#endif
|
||||
}
|
||||
|
||||
__device__ __forceinline__ float inlet_target_u(float y_coord) {
|
||||
return inlet_target_u(y_coord, NY, U0);
|
||||
}
|
||||
|
||||
#if LATTICE_MODEL == LATTICE_D2Q9
|
||||
__device__ __forceinline__ float west_velocity_rho_closure_d2q9(
|
||||
const float* __restrict__ f,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// On-site D2Q9 Zou-He velocity closure, paired lattice ordering.
|
||||
// Input is poststream f at the arrival time. Only unknown populations change;
|
||||
// the caller owns collision/store. No force, shifting or off-lattice walls.
|
||||
// the caller owns collision/store. No force or off-lattice walls. Shifted storage uses unit reference density.
|
||||
#ifndef CELERIS_BOUNDARY_PLANAR_VELOCITY_CUH
|
||||
#define CELERIS_BOUNDARY_PLANAR_VELOCITY_CUH
|
||||
#include <math.h>
|
||||
@@ -28,7 +28,11 @@ __device__ __forceinline__ float bottom_wall_normal_velocity(unsigned int x)
|
||||
|
||||
__device__ __forceinline__ void reconstruct_bottom_velocity(float* f, float u, float v)
|
||||
{
|
||||
const float rho = (f[0] + f[1] + f[2] + 2.0f*(f[4] + f[6] + f[7])) / (1.0f-v);
|
||||
const float rho = (
|
||||
#if USE_DDF_SHIFTING
|
||||
1.0f +
|
||||
#endif
|
||||
f[0] + f[1] + f[2] + 2.0f*(f[4] + f[6] + f[7])) / (1.0f-v);
|
||||
f[3] = f[4] + (2.0f/3.0f)*rho*v;
|
||||
f[5] = f[6] + 0.5f*(f[2]-f[1]) + 0.5f*rho*u + (1.0f/6.0f)*rho*v;
|
||||
f[8] = f[7] + 0.5f*(f[1]-f[2]) - 0.5f*rho*u + (1.0f/6.0f)*rho*v;
|
||||
@@ -36,7 +40,11 @@ __device__ __forceinline__ void reconstruct_bottom_velocity(float* f, float u, f
|
||||
|
||||
__device__ __forceinline__ void reconstruct_top_velocity(float* f, float u, float v)
|
||||
{
|
||||
const float rho = (f[0] + f[1] + f[2] + 2.0f*(f[3] + f[5] + f[8])) / (1.0f+v);
|
||||
const float rho = (
|
||||
#if USE_DDF_SHIFTING
|
||||
1.0f +
|
||||
#endif
|
||||
f[0] + f[1] + f[2] + 2.0f*(f[3] + f[5] + f[8])) / (1.0f+v);
|
||||
f[4] = f[3] - (2.0f/3.0f)*rho*v;
|
||||
f[6] = f[5] + 0.5f*(f[1]-f[2]) - 0.5f*rho*u - (1.0f/6.0f)*rho*v;
|
||||
f[7] = f[8] + 0.5f*(f[2]-f[1]) + 0.5f*rho*u - (1.0f/6.0f)*rho*v;
|
||||
@@ -46,7 +54,12 @@ __device__ __forceinline__ void reconstruct_top_velocity(float* f, float u, floa
|
||||
// J_y=rho*v follows from known poststream populations (ordinary D2Q9).
|
||||
__device__ __forceinline__ void reconstruct_top_reservoir(float* f, float rho)
|
||||
{
|
||||
const float jy = f[0]+f[1]+f[2]+2.0f*(f[3]+f[5]+f[8])-rho;
|
||||
#if USE_DDF_SHIFTING
|
||||
const float density_offset = rho - 1.0f;
|
||||
#else
|
||||
const float density_offset = rho;
|
||||
#endif
|
||||
const float jy = f[0]+f[1]+f[2]+2.0f*(f[3]+f[5]+f[8])-density_offset;
|
||||
f[4] = f[3] - (2.0f/3.0f)*jy;
|
||||
f[6] = f[5] + 0.5f*(f[1]-f[2]) - (1.0f/6.0f)*jy;
|
||||
f[7] = f[8] + 0.5f*(f[2]-f[1]) - (1.0f/6.0f)*jy;
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
#define STORE_PRECISION 0
|
||||
#define USE_DDF_SHIFTING 0
|
||||
|
||||
// Internal blocked root mode: 0=periodic/fine, 1=slot/reservoir, 2=channel.
|
||||
#define BLOCK_ROOT_BC 0
|
||||
|
||||
#define USE_LES 0
|
||||
#define LES_CS 0.160000f
|
||||
#define LES_CLOSED_FORM 1
|
||||
|
||||
@@ -53,6 +53,18 @@ __device__ __forceinline__ void index_from_thread(unsigned int& x,
|
||||
k = (unsigned long)y * NX + x;
|
||||
}
|
||||
|
||||
// Global cell of window cell (x, y) in block blockIdx.z of a row-major,
|
||||
// full-domain block layout domain_nx cells wide. NX/NY are the local strides.
|
||||
__device__ __forceinline__ void block_global_xy(unsigned int x, unsigned int y,
|
||||
unsigned int halo, unsigned int domain_nx,
|
||||
unsigned int& gx, unsigned int& gy)
|
||||
{
|
||||
const unsigned int bx = NX - 2 * halo, by = NY - 2 * halo;
|
||||
const unsigned int nbx = domain_nx / bx;
|
||||
gx = (blockIdx.z % nbx) * bx + x - halo;
|
||||
gy = (blockIdx.z / nbx) * by + y - halo;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Neighbor list (periodic BCs via modular arithmetic)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Collision used by periodic/fine and slot/reservoir BlockStep.
|
||||
#ifndef CELERIS_OPERATORS_BLOCK_COLLISION_CUH
|
||||
#define CELERIS_OPERATORS_BLOCK_COLLISION_CUH
|
||||
|
||||
__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
|
||||
@@ -114,6 +114,16 @@ __device__ __forceinline__ void collide_mrt_no_force(float* __restrict__ g,
|
||||
collide_mrt(g, rho, ux, uy, Fin, omega);
|
||||
}
|
||||
|
||||
// Unforced MRT with the clamped device omega, as in collide_dispatch with zero
|
||||
// forcing. Shared by the blocked, channel-root and dense-patch collisions.
|
||||
__device__ __forceinline__ void collide_mrt_unforced(float* __restrict__ g)
|
||||
{
|
||||
float rho, ux, uy;
|
||||
compute_rho_u(g, rho, ux, uy);
|
||||
collide_mrt_no_force(g, rho, ux, uy,
|
||||
fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega)));
|
||||
}
|
||||
|
||||
#elif LATTICE_MODEL == LATTICE_D3Q19
|
||||
|
||||
__device__ __forceinline__ void collide_mrt(float* __restrict__ g,
|
||||
|
||||
@@ -64,11 +64,12 @@ __device__ __forceinline__ void compute_rho_u(const float* __restrict__ f,
|
||||
#if LATTICE_MODEL == LATTICE_D2Q9
|
||||
|
||||
__device__ __forceinline__ void compute_feq(float rho, float ux, float uy,
|
||||
float* __restrict__ feq)
|
||||
{
|
||||
float* __restrict__ feq
|
||||
#if USE_DDF_SHIFTING
|
||||
const float rhom1 = rho - 1.0f; // perturbation density
|
||||
, float rhom1
|
||||
#endif
|
||||
)
|
||||
{
|
||||
const float ux3 = 3.0f * ux;
|
||||
const float uy3 = 3.0f * uy;
|
||||
const float c3 = -1.5f * (ux * ux + uy * uy); // = -|u|²/(2cs²)
|
||||
@@ -112,6 +113,39 @@ __device__ __forceinline__ void compute_feq(float rho, float ux, float uy,
|
||||
#undef FEQ_CORE
|
||||
}
|
||||
|
||||
#if USE_DDF_SHIFTING
|
||||
// Shifted D2Q9 equilibrium using input momentum in the linear term.
|
||||
// Algebraically identical to the usual second-order equilibrium; explicit
|
||||
// rational divisions avoid 3*w rounding and the rho*(j/rho) round trip.
|
||||
__device__ __forceinline__ void compute_feq_from_momentum(
|
||||
float delta, float jx, float jy, float ux, float uy,
|
||||
float* __restrict__ feq)
|
||||
{
|
||||
const float common = fmaf(-1.5f, fmaf(jx, ux, jy * uy), delta);
|
||||
feq[0] = W0_VAL * common;
|
||||
const float jp[4] = {jx, jy, jx + jy, jx - jy};
|
||||
const float up[4] = {ux, uy, ux + uy, ux - uy};
|
||||
#pragma unroll
|
||||
for (int p = 0; p < 4; ++p) {
|
||||
const float weight = p < 2 ? WS_VAL : WE_VAL;
|
||||
const float odd = __fdiv_rn(jp[p], p < 2 ? 3.0f : 12.0f);
|
||||
const float even = weight * fmaf(4.5f * jp[p], up[p], common);
|
||||
feq[1 + 2*p] = even + odd;
|
||||
feq[2 + 2*p] = even - odd;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if USE_DDF_SHIFTING
|
||||
// Existing callers recover the offset from rho; blocked shifted collision
|
||||
// supplies sum(g) directly to the overload above, retaining small perturbations.
|
||||
__device__ __forceinline__ void compute_feq(float rho, float ux, float uy,
|
||||
float* __restrict__ feq)
|
||||
{
|
||||
compute_feq(rho, ux, uy, feq, rho - 1.0f);
|
||||
}
|
||||
#endif
|
||||
|
||||
#elif LATTICE_MODEL == LATTICE_D3Q19
|
||||
|
||||
__device__ __forceinline__ void compute_feq(float rho, float ux, float uy, float uz,
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// nu_t = (C_s * Delta)^2 * |S|, Delta = 1 (lattice unit)
|
||||
// omega_eff = 1 / (3*(nu + nu_t) + 0.5)
|
||||
//
|
||||
// |S| is estimated from non-equilibrium second-order moments using tau0.
|
||||
// |S| = sqrt(2 S_ij S_ij), with Delta = 1 in lattice units.
|
||||
// The stress estimate uses self-consistent tau_eff; tau0 is molecular.
|
||||
// Closed form uses full stress. Iteration uses deviatoric stress and a cap.
|
||||
// ============================================================================
|
||||
|
||||
#ifndef CELERIS_OPERATORS_TURBULENCE_SMAG_CUH
|
||||
@@ -65,12 +67,12 @@ __device__ __forceinline__ float compute_omega_smag(const float* __restrict__ f,
|
||||
|
||||
#if LES_CLOSED_FORM
|
||||
// Closed-form Smagorinsky (no iteration):
|
||||
// ω = 2 / (τ₀ + √(τ₀² + 18·Cs²·√Q / ρ))
|
||||
// ω = 2 / (τ₀ + √(τ₀² + 18·√2·Cs²·√Q / ρ))
|
||||
// where Q = Πneq_ij · Πneq_ij (Frobenius norm squared)
|
||||
const float Q = pixx*pixx + piyy*piyy + 2.0f*pixy*pixy;
|
||||
const float sqrtQ = sqrtf(fmaxf(Q, 0.0f));
|
||||
const float smag_const_sq = LES_CS * LES_CS;
|
||||
const float discriminant = tau0*tau0 + 18.0f * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float discriminant = tau0*tau0 + 18.0f * sqrtf(2.0f) * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float tau_eff = 0.5f * (tau0 + sqrtf(fmaxf(discriminant, tau0*tau0)));
|
||||
return clamp_omega(1.0f / fmaxf(tau_eff, 0.500001f));
|
||||
#else
|
||||
@@ -140,7 +142,7 @@ __device__ __forceinline__ float compute_omega_smag(const float* __restrict__ f,
|
||||
+ 2.0f*(pixy*pixy + pixz*pixz + piyz*piyz);
|
||||
const float sqrtQ = sqrtf(fmaxf(Q, 0.0f));
|
||||
const float smag_const_sq = LES_CS * LES_CS;
|
||||
const float discriminant = tau0*tau0 + 18.0f * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float discriminant = tau0*tau0 + 18.0f * sqrtf(2.0f) * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float tau_eff = 0.5f * (tau0 + sqrtf(fmaxf(discriminant, tau0*tau0)));
|
||||
return clamp_omega(1.0f / fmaxf(tau_eff, 0.500001f));
|
||||
#else
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#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"
|
||||
@@ -19,8 +22,8 @@ extern "C" {
|
||||
#include "step/curved_double.cu"
|
||||
}
|
||||
|
||||
#if DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Refinement requires unshifted FP32 D2Q9 SRT double buffers without LES"
|
||||
#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.
|
||||
@@ -33,10 +36,14 @@ __device__ unsigned int incoming_counts[NQ * TOTAL_CELLS];
|
||||
#endif
|
||||
|
||||
__device__ __forceinline__ void patch_collide(float* f, unsigned long k) {
|
||||
#if COLLISION_MODEL == 2
|
||||
collide_mrt_unforced(f);
|
||||
#else
|
||||
float rho, ux, uy, feq[NQ];
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
collide_srt_no_force(f, feq, d_params.omega);
|
||||
#endif
|
||||
#ifdef CELERIS_REFINEMENT_AUDIT
|
||||
++collision_counts[k];
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#if BLOCK_ROOT_BC != 2
|
||||
#error "BlockChannelStep requires BLOCK_ROOT_BC == 2"
|
||||
#endif
|
||||
|
||||
// Root-only blocked counterpart of OneStep's D2Q9/SRT channel closure.
|
||||
// NX/NY are LOCAL strides; all physical boundary decisions use global coordinates.
|
||||
#include "../boundary/inlet/common.cuh"
|
||||
#include "../boundary/inlet/zou_he_local.cuh"
|
||||
#include "../boundary/outlet/pressure_neq.cuh"
|
||||
|
||||
struct BlockChannel { unsigned int nx, ny; float velocity; };
|
||||
extern "C" { __constant__ BlockChannel d_block_channel; }
|
||||
|
||||
// Same signature as BlockStep so the AMR hop/transfer schedule is unchanged.
|
||||
extern "C" __global__ void BlockChannelStep(
|
||||
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;
|
||||
unsigned int gx, gy;
|
||||
block_global_xy(x, y, halo, d_block_channel.nx, gx, gy);
|
||||
const unsigned long base = (unsigned long)blockIdx.z * NQ * TOTAL_CELLS;
|
||||
const unsigned long cell = (unsigned long)blockIdx.z * TOTAL_CELLS + k;
|
||||
float f[NQ];
|
||||
if (kind != nullptr && kind[cell] == KIND_OUTER) {
|
||||
for (int q = 0; q < NQ; ++q) f[q] = NAN;
|
||||
} else {
|
||||
unsigned long j[NQ];
|
||||
compute_neighbors(x, y, j);
|
||||
stream_pull_load(k, f, in + base, j);
|
||||
const bool wall = gy == 0 || gy == d_block_channel.ny - 1;
|
||||
const bool outlet = gx == d_block_channel.nx - 1;
|
||||
if (wall) {
|
||||
// Wall owns all four corners. Ghosts pull and swap, never collide.
|
||||
for (int q = 1; q < NQ; q += 2) {
|
||||
const float t = f[q]; f[q] = f[q + 1]; f[q + 1] = t;
|
||||
}
|
||||
} else if (gx == 0) {
|
||||
apply_zou_he_left_velocity_inlet_d2q9(
|
||||
f, inlet_target_u((float)gy, d_block_channel.ny, d_block_channel.velocity), 0.0f);
|
||||
} else if (outlet) {
|
||||
// Immutable input donor, receiver-local (also valid across a block seam).
|
||||
float donor[NQ];
|
||||
for (int q = 0; q < NQ; ++q)
|
||||
donor[q] = load_ddf(in + base, index_f(k - 1, q));
|
||||
apply_pressure_outlet_d2q9(f, donor);
|
||||
} else if (gy == 1) {
|
||||
f[3] = load_ddf(in + base, index_f(k, 4));
|
||||
f[5] = load_ddf(in + base, index_f(k, 6));
|
||||
f[8] = load_ddf(in + base, index_f(k, 7));
|
||||
} else if (gy == d_block_channel.ny - 2) {
|
||||
f[4] = load_ddf(in + base, index_f(k, 3));
|
||||
f[6] = load_ddf(in + base, index_f(k, 5));
|
||||
f[7] = load_ddf(in + base, index_f(k, 8));
|
||||
}
|
||||
if (!wall && !outlet) {
|
||||
#if COLLISION_MODEL == 2
|
||||
collide_mrt_unforced(f);
|
||||
#else
|
||||
// Match uniform SRT arithmetic, including its shifted equilibrium path.
|
||||
float rho, ux, uy, feq[NQ], forcing[NQ] = {};
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
const float omega = fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega));
|
||||
collide_srt(f, feq, forcing, omega);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
stream_pull_store(k, f, out + base);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Private D2Q9 FP32 storage boundary for the opt-in blocked steppers."""
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
# Unit-density reference weights in FP64; sum(w)=1 matches rho=1+sum(g).
|
||||
_WEIGHTS = np.array([4/9, 1/9, 1/9, 1/9, 1/9, 1/36, 1/36, 1/36, 1/36],
|
||||
dtype=np.float64)[:, None, None]
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
class _StoredPopulations:
|
||||
values: tuple
|
||||
shifted: bool
|
||||
|
||||
|
||||
def _encode_physical(values, shifted):
|
||||
if not shifted:
|
||||
return np.array(values, dtype=np.float32, order="C", copy=True)
|
||||
return np.ascontiguousarray(np.asarray(values, dtype=np.float64) - _WEIGHTS,
|
||||
dtype=np.float32)
|
||||
|
||||
|
||||
def _decode_stored(values, shifted):
|
||||
return values.astype(np.float64) + _WEIGHTS if shifted else values
|
||||
@@ -7,16 +7,26 @@ import numpy as np
|
||||
|
||||
|
||||
def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
|
||||
time_end, window, reference_center):
|
||||
time_end, window, reference_center, duration=None):
|
||||
"""Reduce FP64 interval integrals or convert FP32 last-substep raw rows.
|
||||
|
||||
dt_l=h_l under acoustic 2:1 refinement. Local momentum j -> base force
|
||||
h*j -> impulse h*dt*j; torque adds one h; mass is h**2*dm.
|
||||
window="window" reduces an FP64 observation-window ledger spanning
|
||||
``duration`` (integer >= 1) base intervals; rates divide by that duration.
|
||||
"""
|
||||
if window not in ("last_base_step", "last_substep"):
|
||||
if window not in ("last_base_step", "last_substep", "window"):
|
||||
raise ValueError("unknown exchange window")
|
||||
if window == "window":
|
||||
if isinstance(duration, bool) or not isinstance(duration, (int, np.integer)) or duration < 1:
|
||||
raise ValueError("window duration must be an integer number of base intervals >= 1")
|
||||
elif duration is not None:
|
||||
raise ValueError("duration applies only to window='window'")
|
||||
values = np.array(rows, dtype=np.float64, copy=True)
|
||||
duration = 1.0 if window == "last_base_step" else h
|
||||
if window == "window":
|
||||
duration = float(duration)
|
||||
else:
|
||||
duration = 1.0 if window == "last_base_step" else h
|
||||
if window == "last_substep":
|
||||
values *= np.array([h*h, h*h, h*h*h, h*h])
|
||||
selected = np.asarray(body_ids) == body_id
|
||||
@@ -31,6 +41,7 @@ def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
|
||||
return dict(wall=reduce(selected & ~aperture), aperture=reduce(selected & aperture),
|
||||
total=reduce(selected), body_id=int(body_id), level=int(level),
|
||||
time_start=time_end-duration, time_end=time_end, dt=h,
|
||||
sample_count=round(1/h) if window == "last_base_step" else 1,
|
||||
sample_count=(round(duration/h) if window == "window"
|
||||
else round(1/h) if window == "last_base_step" else 1),
|
||||
window=window, units="base_lattice_per_unit_depth",
|
||||
definition="raw_boundary_exchange", reference_center=tuple(reference_center), valid=True)
|
||||
|
||||
@@ -52,17 +52,17 @@ class FixedPatch2D:
|
||||
def from_config(cls, cfg: LBMConfig, bounds) -> "FixedPatch2D":
|
||||
"""Prepare maps without changing cfg or allocating coarse-sized tables.
|
||||
|
||||
The initial backend contract is D2Q9/SRT/FP32/unshifted/double_buffer,
|
||||
The backend contract is D2Q9/SRT or MRT/FP32/double_buffer,
|
||||
without LES. Two coarse cells of margin keep all halo donors in bounds;
|
||||
this does NOT certify that those donors are fluid or periodic.
|
||||
"""
|
||||
checked = replace(cfg)
|
||||
checked.validate()
|
||||
if (not checked.is_d2q9 or checked.collision != "SRT"
|
||||
if (not checked.is_d2q9 or checked.collision not in ("SRT", "MRT")
|
||||
or checked.streaming != "double_buffer"
|
||||
or checked.store_precision != "FP32"
|
||||
or checked.ddf_shifting or checked.les_enabled):
|
||||
raise ValueError("FixedPatch2D requires D2Q9/SRT/FP32/unshifted/double_buffer without LES")
|
||||
or checked.les_enabled):
|
||||
raise ValueError("FixedPatch2D requires D2Q9/SRT or MRT/FP32/double_buffer without LES")
|
||||
bounds = tuple(bounds)
|
||||
if len(bounds) != 4 or any(isinstance(v, (bool, np.bool_))
|
||||
or not isinstance(v, Integral) for v in bounds):
|
||||
@@ -151,20 +151,17 @@ class FixedPatch2D:
|
||||
raise ValueError("levels are not synchronized: fine_steps must equal 2*coarse_steps")
|
||||
|
||||
|
||||
MIN_PATCH_GAP = 4 # coarse cells: the full 4-fine-cell supports stay disjoint
|
||||
MIN_PATCH_GAP = 4 # Legacy exported constant; union transfers impose no gap policy.
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
class FixedPatchSet2D:
|
||||
"""Several separated FixedPatch2D rectangles sharing one 2:1 fine level.
|
||||
"""Rectangles describing one 2:1 fine union, with unique transfer ownership.
|
||||
|
||||
The fine level is the whole (2*ny, 2*nx) grid in level coordinates; only
|
||||
the patch supports (active region + HALO ghost band) are stored, by a
|
||||
sparse BlockLayout2D from fine_layout(). Tables have FixedPatch2D meaning
|
||||
and row/child order, re-addressed to level cells: ghost_fine are level
|
||||
fine cells, coalesce_fine are q*N+cell level slots; coarse ids unchanged.
|
||||
Patches are kept in canonical (y0, x0) order, so input order is irrelevant.
|
||||
Active regions must be >= MIN_PATCH_GAP coarse cells apart (Chebyshev).
|
||||
Input patches remain canonical metadata. Numerical kinds and tables are
|
||||
built once from their union; adjacent, overlapping and concave unions are
|
||||
accepted when their actual transfer dependencies close. Sparse storage owns
|
||||
every whole tile intersecting the support, independent of rectangle edges.
|
||||
"""
|
||||
coarse_shape: tuple[int, int]
|
||||
patches: tuple
|
||||
@@ -182,42 +179,43 @@ class FixedPatchSet2D:
|
||||
if not rectangles:
|
||||
raise ValueError("at least one patch rectangle is required")
|
||||
patches = sorted((FixedPatch2D.from_config(cfg, r) for r in rectangles),
|
||||
key=lambda p: (p.bounds[1], p.bounds[0]))
|
||||
for i, a in enumerate(patches):
|
||||
for b in patches[i + 1:]:
|
||||
(ax0, ay0, ax1, ay1), (bx0, by0, bx1, by1) = a.bounds, b.bounds
|
||||
if max(ay0 - by1, by0 - ay1, ax0 - bx1, bx0 - ax1) < MIN_PATCH_GAP:
|
||||
raise ValueError(f"patches {a.bounds} and {b.bounds} are closer "
|
||||
f"than {MIN_PATCH_GAP} coarse cells")
|
||||
key=lambda p: (p.bounds[1], p.bounds[0], p.bounds[3], p.bounds[2]))
|
||||
ny, nx = patches[0].coarse_shape
|
||||
fy, fx = 2 * ny, 2 * nx
|
||||
if fy * fx * 9 > np.iinfo(np.uint64).max // 2:
|
||||
raise ValueError("fine level exceeds 64-bit indexing")
|
||||
kind = np.full((fy, fx), OUTER_GHOST, np.uint8)
|
||||
ghost_fine, coalesce_fine = [], []
|
||||
for p in patches:
|
||||
y0, x0 = p.fine_level_origin
|
||||
ly, lx = p.fine_shape
|
||||
kind[y0:y0 + ly, x0:x0 + lx] = p.fine_kind
|
||||
gy, gx = np.divmod(p.ghost_fine.astype(np.int64), lx)
|
||||
ghost_fine.append((gy + y0) * fx + gx + x0)
|
||||
q, cell = np.divmod(p.coalesce_fine.astype(np.int64), ly * lx)
|
||||
cy, cx = np.divmod(cell, lx)
|
||||
coalesce_fine.append(q * fy * fx + (cy + y0) * fx + cx + x0)
|
||||
ghost_coarse = np.concatenate([p.ghost_coarse for p in patches])
|
||||
coalesce_coarse = np.concatenate([p.coalesce_coarse for p in patches])
|
||||
covered = np.zeros((ny, nx), bool)
|
||||
for p in patches:
|
||||
covered |= p.covered
|
||||
cells = ny * nx
|
||||
if (covered.ravel()[ghost_coarse].any()
|
||||
or covered.ravel()[coalesce_coarse % np.uint64(cells)].any()
|
||||
or len(np.unique(coalesce_coarse)) != len(coalesce_coarse)):
|
||||
raise ValueError("patch transfers overlap another patch")
|
||||
return cls((ny, nx), tuple(patches), patches[0].tau_coarse, patches[0].tau_fine,
|
||||
_readonly(kind, np.uint8), _readonly(np.concatenate(ghost_fine), np.uint64),
|
||||
_readonly(ghost_coarse, np.uint32), _readonly(coalesce_coarse, np.uint64),
|
||||
_readonly(np.concatenate(coalesce_fine), np.uint64).reshape(-1, 4))
|
||||
fy, fx = 2 * ny, 2 * nx
|
||||
if fy * fx * 9 > np.iinfo(np.uint64).max // 2:
|
||||
raise ValueError("fine level exceeds 64-bit indexing")
|
||||
active = np.kron(covered, np.ones((2, 2), bool))
|
||||
inner = _dilate(active, 2)
|
||||
support = _dilate(active, HALO)
|
||||
kind = np.full((fy, fx), OUTER_GHOST, np.uint8)
|
||||
kind[inner] = INNER_GHOST
|
||||
kind[active] = ACTIVE
|
||||
gy, gx = np.nonzero(support & ~active)
|
||||
ghost_fine = (gy * fx + gx).astype(np.uint64)
|
||||
ghost_coarse = ((gy >> 1) * nx + (gx >> 1)).astype(np.uint32)
|
||||
targets, children = [], []
|
||||
ry, rx = np.nonzero(_dilate(covered, 1) & ~covered)
|
||||
for y, x in zip(ry.tolist(), rx.tolist()):
|
||||
for q in range(1, 9):
|
||||
if _closed_union_contains(covered, 2 * x + 1 - D2Q9_EX[q], 2 * y + 1 - D2Q9_EY[q]):
|
||||
targets.append(q * ny * nx + y * nx + x)
|
||||
children.append([q * fy * fx + (2 * y + dy) * fx + 2 * x + dx
|
||||
for dy in (0, 1) for dx in (0, 1)])
|
||||
targets = np.asarray(targets, np.uint64)
|
||||
children = np.asarray(children, np.uint64).reshape(-1, 4)
|
||||
cells = np.asarray(children % np.uint64(fy * fx), np.intp)
|
||||
if not (kind.ravel()[cells] == INNER_GHOST).all():
|
||||
raise ValueError("coalesce children must be INNER ghost cells")
|
||||
pair = cls((ny, nx), tuple(patches), patches[0].tau_coarse, patches[0].tau_fine,
|
||||
_readonly(kind, np.uint8), _readonly(ghost_fine, np.uint64),
|
||||
_readonly(ghost_coarse, np.uint32), _readonly(targets, np.uint64),
|
||||
_readonly(children, np.uint64))
|
||||
_transfer_footprint(pair)
|
||||
_check_parent_pull_closure(pair)
|
||||
return pair
|
||||
|
||||
@property
|
||||
def fine_shape(self) -> tuple[int, int]:
|
||||
@@ -240,22 +238,61 @@ class FixedPatchSet2D:
|
||||
return mask
|
||||
|
||||
def fine_layout(self, block):
|
||||
"""Sparse non-periodic layout storing exactly the patch supports."""
|
||||
"""Sparse non-periodic tiles covering union support; domain divisibility remains."""
|
||||
from .blocks import BlockLayout2D
|
||||
block = tuple(block)
|
||||
if len(block) != 2 or any(isinstance(v, (bool, np.bool_)) or not isinstance(v, Integral)
|
||||
or v <= 0 for v in block):
|
||||
raise ValueError("fine block must be two positive integers (by, bx)")
|
||||
by, bx = map(int, block)
|
||||
origins = set()
|
||||
for p in self.patches:
|
||||
y0, x0 = p.fine_level_origin
|
||||
ly, lx = p.fine_shape
|
||||
if y0 % by or x0 % bx or ly % by or lx % bx:
|
||||
raise ValueError(f"patch {p.bounds} support is not aligned to fine blocks {block}")
|
||||
origins.update((y, x) for y in range(y0, y0 + ly, by) for x in range(x0, x0 + lx, bx))
|
||||
support = _dilate(self.fine_kind == ACTIVE, HALO)
|
||||
ty, tx = np.nonzero(support)
|
||||
origins = sorted({(int(y) // by * by, int(x) // bx * bx) for y, x in zip(ty, tx)})
|
||||
return BlockLayout2D.build(*self.fine_shape, by, bx, periodic=(False, False),
|
||||
origins=sorted(origins))
|
||||
origins=origins)
|
||||
|
||||
|
||||
def _dilate(mask, steps):
|
||||
"""Chebyshev dilation with a zero (non-periodic) exterior."""
|
||||
out = np.asarray(mask, bool)
|
||||
for _ in range(steps):
|
||||
grown = out.copy()
|
||||
grown[1:] |= out[:-1]
|
||||
grown[:-1] |= out[1:]
|
||||
wide = grown.copy()
|
||||
wide[:, 1:] |= grown[:, :-1]
|
||||
wide[:, :-1] |= grown[:, 1:]
|
||||
out = wide
|
||||
return out
|
||||
|
||||
|
||||
def _closed_union_contains(covered, mx, my):
|
||||
"""Does doubled point (mx, my) lie in a closed covered square [2X, 2X+2] x [2Y, 2Y+2]?"""
|
||||
xs = (mx // 2 - 1, mx // 2) if mx % 2 == 0 else ((mx - 1) // 2,)
|
||||
ys = (my // 2 - 1, my // 2) if my % 2 == 0 else ((my - 1) // 2,)
|
||||
ny, nx = covered.shape
|
||||
return any(0 <= y < ny and 0 <= x < nx and covered[y, x] for y in ys for x in xs)
|
||||
|
||||
|
||||
def _check_parent_pull_closure(pair):
|
||||
"""Every uncovered parent cell pulling q from a covered cell needs its (R, q) row; no row
|
||||
may stage into an uncovered parent cell unless its link passes the closed union."""
|
||||
ny, nx = pair.coarse_shape
|
||||
rows = set(map(int, pair.coalesce_coarse))
|
||||
if len(rows) != len(pair.coalesce_coarse):
|
||||
raise ValueError("duplicate coalesce writer (receiver, q)")
|
||||
covered = pair.covered
|
||||
for y, x in zip(*np.nonzero(~covered)):
|
||||
for q in range(1, 9):
|
||||
sy, sx = y - D2Q9_EY[q], x - D2Q9_EX[q]
|
||||
if 0 <= sy < ny and 0 <= sx < nx and covered[sy, sx]:
|
||||
if q * ny * nx + y * nx + x not in rows:
|
||||
raise ValueError("a surviving parent ACTIVE pull lacks its coalesce row")
|
||||
for slot in rows:
|
||||
q, cell = divmod(slot, ny * nx)
|
||||
y, x = divmod(cell, nx)
|
||||
if covered[y, x] or not _closed_union_contains(covered, 2*x + 1 - D2Q9_EX[q], 2*y + 1 - D2Q9_EY[q]):
|
||||
raise ValueError("coalesce row outside the union boundary")
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
@@ -323,14 +360,14 @@ def nest_patch(cfg, parent, child_bounds):
|
||||
|
||||
|
||||
def nest_patch_set(cfg, parent, child_rectangles_in_parent):
|
||||
"""Separated child union in the parent's allocated L1 array coordinates.
|
||||
"""Child rectangle union in the parent's allocated L1 array coordinates.
|
||||
|
||||
The root edge coordinate of L1 index (x, y) is
|
||||
``parent.fine_origin + (x, y)/2``. For a set parent its origin is zero;
|
||||
a scalar parent's window includes its ghost band and has a shifted origin.
|
||||
The child set allocates a dense public extent twice that L1 array, while
|
||||
its blocked storage omits holes. Both adjacent pairs retain gap >= 4 in
|
||||
their respective parent-cell units. Validate support over the whole union.
|
||||
its blocked storage omits holes. Validate the actual adjacent-pair support over the whole union;
|
||||
no rectangle-gap policy is imposed.
|
||||
"""
|
||||
child = FixedPatchSet2D.from_config(
|
||||
_middle_config(cfg, parent), child_rectangles_in_parent)
|
||||
@@ -373,8 +410,19 @@ def _nested_kind(parent, child):
|
||||
return _readonly(composed, np.uint8)
|
||||
|
||||
|
||||
def _circle_in_coverage(covered, cx, cy, radius):
|
||||
"""Exact disk versus uncovered parent squares; no containing-rectangle rule."""
|
||||
ny, nx = covered.shape
|
||||
if cx-radius < 0 or cy-radius < 0 or cx+radius > nx or cy+radius > ny:
|
||||
return False
|
||||
y, x = np.nonzero(~covered)
|
||||
dx = np.maximum(np.maximum(x-cx, cx-(x+1)), 0.)
|
||||
dy = np.maximum(np.maximum(y-cy, cy-(y+1)), 0.)
|
||||
return not np.any(dx*dx + dy*dy < radius*radius)
|
||||
|
||||
|
||||
def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
parent_origin=(0., 0.), parent_spacing=1.):
|
||||
parent_origin=(0., 0.), parent_spacing=1., _shifted_blocked=False):
|
||||
"""Project fixed circles and reject boundary/transfer overlap before CUDA.
|
||||
|
||||
Input centers/radii and Jets are in the root edge frame. ``parent_origin``
|
||||
@@ -386,7 +434,7 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
Original objects are not retained or mutated. No runtime geometry updates.
|
||||
``patch`` may be a FixedPatchSet2D: objects are then projected once onto
|
||||
the level-wide fine grid (fine_origin (0, 0)); each circle must lie inside
|
||||
one patch, body ids are the positions in ``objects`` (never patch ids), and
|
||||
the union, body ids are the positions in ``objects`` (never patch ids), and
|
||||
jets and the (receiver, dir) writer checks run once over the whole level.
|
||||
``jets`` use the same coarse cell-edge frame as the objects; their
|
||||
selection is checked against true fine hits here. Jet amplitudes need no
|
||||
@@ -403,8 +451,8 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
checked = replace(cfg)
|
||||
checked.validate()
|
||||
if (patch.coarse_shape != (cfg.ny, cfg.nx) or not checked.is_d2q9
|
||||
or checked.collision != "SRT" or checked.streaming != "double_buffer"
|
||||
or checked.store_precision != "FP32" or checked.ddf_shifting or checked.les_enabled
|
||||
or checked.collision not in ("SRT", "MRT") or checked.streaming != "double_buffer"
|
||||
or checked.store_precision != "FP32" or checked.les_enabled
|
||||
or patch.tau_coarse != 3*checked.viscosity+.5):
|
||||
raise ValueError("Fine boundary requires the matching fixed-patch configuration")
|
||||
from ..body.geometry.circle import CircleGeometry
|
||||
@@ -416,7 +464,6 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
flags = np.full(nx * ny, FLUID, np.uint16)
|
||||
active = patch.fine_kind.ravel() == ACTIVE
|
||||
local, links = [], []
|
||||
rects = patch.bounds if isinstance(patch, FixedPatchSet2D) else (patch.bounds,)
|
||||
covered = patch.covered
|
||||
dx = dt = .5 * parent_spacing
|
||||
fine_origin = origin + parent_spacing * np.asarray(patch.fine_origin)
|
||||
@@ -430,9 +477,8 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
px, py = (np.asarray((cx, cy)) - origin) / parent_spacing
|
||||
pr = radius / parent_spacing
|
||||
if (not np.isfinite([cx, cy, radius]).all() or radius <= 0
|
||||
or not any(x0 <= px-pr and px+pr <= x1 and y0 <= py-pr
|
||||
and py+pr <= y1 for x0, y0, x1, y1 in rects)):
|
||||
raise ValueError("Entire fine circle must lie inside one physical patch")
|
||||
or not _circle_in_coverage(covered, px, py, pr)):
|
||||
raise ValueError("Entire fine circle must lie inside the physical patch union")
|
||||
center = tuple((v-o)/dx-.5 for v, o in zip((cx, cy), fine_origin))
|
||||
spec = obj.surface_actuation
|
||||
if spec is not None:
|
||||
@@ -509,7 +555,7 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
jet_cfg.validate()
|
||||
fluid_idx, dirs, _, body_id, rx, ry, _, _ = packed
|
||||
surface_link_velocity(local, jet_cfg, flags, fluid_idx, dirs, body_id, rx, ry,
|
||||
jets=jets, hits=hits)
|
||||
jets=jets, hits=hits, _shifted_blocked=_shifted_blocked)
|
||||
return FineBoundary2D(tuple(local), _readonly(flags, np.uint16),
|
||||
tuple(_readonly(a, a.dtype) for a in packed),
|
||||
_readonly(sorted(boundary), np.uint32),
|
||||
|
||||
@@ -16,15 +16,17 @@ import pycuda.driver as cuda
|
||||
|
||||
from ..cuda import compiler_v2 as compiler
|
||||
from .blocks import BlockLayout2D
|
||||
from .channel import channel_masks, validate_channel_config, validate_channel_transfers
|
||||
from ..body.jets import jet_link_velocity
|
||||
from .block_stepper import MAX_GRID_DIM
|
||||
from .hierarchy import build_hierarchy, is_rectangle
|
||||
from .refinement import ACTIVE, HALO, FixedPatch2D, FixedPatchSet2D, prepare_fine_boundary
|
||||
from .refinement_tables import (blocked_base_tables, blocked_fine_tables, # noqa: F401 (re-export)
|
||||
blocked_kind, _prepare_blocked_boundary,
|
||||
blocked_kind, projected_explosion_tables, _prepare_blocked_boundary,
|
||||
BLOCKED_BOUNDARY_DEVICE_COLUMNS)
|
||||
from .descriptors import SOLID
|
||||
from .raw_exchange import summarize_exchange
|
||||
from .population_storage import _StoredPopulations, _encode_physical, _decode_stored
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -39,6 +41,8 @@ class _Pair:
|
||||
n_ghosts: int
|
||||
n_rows: int
|
||||
parent_cells: int # parent block window cells (explode donor stride)
|
||||
neighbors: object = None # projected-only owner-core cells; constant allocates neither
|
||||
offsets: object = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -61,52 +65,8 @@ def _level_name(level):
|
||||
return ("base", "fine", "child")[level] if level < 3 else f"level {level}"
|
||||
|
||||
|
||||
def _legacy_levels(bounds, *, fine_block, base_block, child, child_block, child_rectangles_in_parent,
|
||||
fine_objects, jets, child_objects, child_jets):
|
||||
"""Normalise the original two/three-level keywords to per-level lists (original errors)."""
|
||||
if child is not None and child_rectangles_in_parent is not None:
|
||||
raise ValueError("child and child_rectangles_in_parent are mutually exclusive")
|
||||
nested = child is not None or child_rectangles_in_parent is not None
|
||||
bounds = tuple(bounds)
|
||||
if not is_rectangle(bounds) and (fine_block is None or base_block is None):
|
||||
raise ValueError("several patches require fine_block and base_block")
|
||||
fine_objects, jets = tuple(fine_objects), tuple(jets)
|
||||
child_objects, child_jets = tuple(child_objects), tuple(child_jets)
|
||||
if not nested and (child_objects or child_jets):
|
||||
raise ValueError("child_objects/child_jets require a nested child")
|
||||
if base_block is not None and fine_block is None:
|
||||
raise ValueError("base_block requires fine_block")
|
||||
refinements, blocks = [bounds], [base_block, fine_block]
|
||||
objects, level_jets = [(), fine_objects], [(), jets]
|
||||
if nested:
|
||||
if child is not None and not is_rectangle(bounds):
|
||||
raise ValueError("scalar child requires one parent patch; use child_rectangles_in_parent")
|
||||
if fine_block is None or base_block is None or child_block is None:
|
||||
raise ValueError("a nested child needs fine_block, base_block and child_block")
|
||||
if fine_objects or jets:
|
||||
raise ValueError("a nested child supports bodies/Jets only on L2; use child_objects/child_jets")
|
||||
refinements.append(tuple(child) if child is not None else tuple(child_rectangles_in_parent))
|
||||
blocks.append(child_block)
|
||||
objects.append(child_objects)
|
||||
level_jets.append(child_jets)
|
||||
return refinements, blocks, objects, level_jets
|
||||
|
||||
|
||||
def _initial_levels(level_g, coarse_g, fine_g, child_g):
|
||||
"""One tuple of per-level arrays from initialize()'s positional or legacy named form."""
|
||||
if coarse_g is None and fine_g is None:
|
||||
if child_g is not None and len(level_g) != 2:
|
||||
raise TypeError("child_g needs exactly the coarse and fine arrays before it")
|
||||
return tuple(level_g) + (() if child_g is None else (child_g,))
|
||||
if level_g:
|
||||
raise TypeError("pass level arrays positionally or as coarse_g/fine_g/child_g, not both")
|
||||
if coarse_g is None or fine_g is None:
|
||||
raise TypeError("coarse_g and fine_g are both required in the named form")
|
||||
return (coarse_g, fine_g) + (() if child_g is None else (child_g,))
|
||||
|
||||
|
||||
def _check_levels(refinements, blocks, objects, jets):
|
||||
"""Candidate limits shared by the list and the legacy entry points."""
|
||||
"""Candidate limits of the level lists, checked before any device work."""
|
||||
depth = len(refinements) + 1
|
||||
if depth < 2:
|
||||
raise ValueError("at least one refinement level is required")
|
||||
@@ -115,13 +75,13 @@ def _check_levels(refinements, blocks, objects, jets):
|
||||
raise ValueError(f"{name} needs one entry per level ({depth})")
|
||||
for level, block in enumerate(blocks):
|
||||
if block is not None and len(block) != 2:
|
||||
raise ValueError(f"{_level_name(level)}_block must be (by, bx)")
|
||||
raise ValueError(f"level_blocks[{level}] must be (by, bx)")
|
||||
if objects[0] or jets[0]:
|
||||
raise ValueError("base-level bodies/Jets are not supported")
|
||||
if any(blocks[level - 1] is not None and blocks[level] is None for level in range(1, depth)):
|
||||
raise ValueError("a blocked level requires blocked refined levels below it")
|
||||
if not is_rectangle(refinements[0]) and (blocks[0] is None or blocks[1] is None):
|
||||
raise ValueError("several patches require fine_block and base_block")
|
||||
raise ValueError("several L1 patches require level_blocks[0] and level_blocks[1]")
|
||||
if depth > 2:
|
||||
if any(block is None for block in blocks):
|
||||
raise ValueError("three or more levels must all be blocked")
|
||||
@@ -130,114 +90,115 @@ def _check_levels(refinements, blocks, objects, jets):
|
||||
|
||||
|
||||
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``, ``level_objects`` and ``level_jets``
|
||||
hold one entry per level, L0 included (L0 bodies/Jets are not supported).
|
||||
|
||||
``coarse_boundary`` is ``periodic`` (default) or ``slot_reservoir``:
|
||||
a bottom on-site slot and top fixed-density reservoir from ``cfg``.
|
||||
``channel`` requires all-blocked levels and matches uniform SRT Zou-He
|
||||
inlet, pressure-NEQ outlet and stationary bounce-back y walls. Boundary
|
||||
ghosts evolve internally but public downloads exclude them; initialization
|
||||
must supply their finite positive-density state. Transfers must avoid the
|
||||
actual channel closure footprint. Channel remapping is not supported.
|
||||
Periodic mode does not apply the wall/slot boundary fields in ``cfg``.
|
||||
Outer walls remain coarse; fine halo donors must be ordinary fluid.
|
||||
``fine_objects`` accepts fixed circles in coarse physical coordinates;
|
||||
velocities are in coarse lattice units (dx=dt=1). Independent local copies
|
||||
are projected at fine cell centers. Their complete stencil must be active
|
||||
and disjoint from the actual two-hop transfer footprint.
|
||||
These use the edge/cell-center frame (coarse centers I+.5), whereas ordinary Simulation objects use the integer-node frame.
|
||||
|
||||
``initialize(coarse_g, fine_g)`` accepts physical Q-major (9,ny,nx) arrays
|
||||
already in postcollision phase. It does not collide or build channel flags.
|
||||
Every ``step()`` publishes only after both levels complete on one stream.
|
||||
On a device/launch failure the instance becomes unusable: close and recreate
|
||||
it, rather than pretending the partially advanced fine buffer rolled back.
|
||||
Bodies are fixed circles in root (L0) physical coordinates; velocities are
|
||||
in root lattice units (dx=dt=1). Independent local copies are projected at
|
||||
fine cell centers; their complete stencil must be active and disjoint from
|
||||
the actual two-hop transfer footprint. These use the edge/cell-center frame
|
||||
(coarse centers I+.5), whereas ordinary Simulation objects use the
|
||||
integer-node frame. Jets (body.jets.Jet) use the same frame and drive cut
|
||||
links selected at their true hits; ``set_jet_amplitude(index, amplitude,
|
||||
level=l)`` re-uploads the per-link velocity column only.
|
||||
|
||||
``initialize(*level_g)`` accepts one physical Q-major (9, ny, nx) array per
|
||||
level, already in postcollision phase. It does not collide or build channel
|
||||
flags. Every ``step()`` publishes only after all levels complete on one
|
||||
stream. On a device/launch failure the instance becomes unusable: close and
|
||||
recreate it, rather than pretending a partially advanced buffer rolled back.
|
||||
|
||||
A ``level_blocks`` entry (by, bx) stores that level in blocks.cu storage:
|
||||
the ENTIRE level shape, including the four-cell patch ghost band, is
|
||||
partitioned by a non-periodic BlockLayout2D with one-cell same-level halos.
|
||||
Patch kinds stay per cell (OUTER invalid, INNER stream only, ACTIVE
|
||||
collide); halos are gathered before each hop. Public arrays keep the dense
|
||||
shapes; only the allocation differs. A blocked level requires blocked
|
||||
refined levels below it; several L1 rectangles and depth >= 3 require all
|
||||
levels blocked. Blocked bodies are stationary circles, optionally
|
||||
surface-actuated: prepare_fine_boundary decides legality on the dense grid,
|
||||
then cut links are staged receiver-local per hop: halo -> BFL -> step, same
|
||||
buffer. A blocked parent gathers halos before receiver-local coalescence
|
||||
staging and pull/collision; covered cells are poisoned on every step.
|
||||
|
||||
``fine_block=(by, bx)`` (opt-in) stores the fine level in
|
||||
blocks.cu storage: the ENTIRE fine_shape, including the four-cell patch
|
||||
ghost band, is partitioned by a non-periodic BlockLayout2D with one-cell
|
||||
same-level halos. Patch kinds stay per cell (OUTER invalid, INNER stream
|
||||
only, ACTIVE collide); halos are gathered before each fine hop. Public
|
||||
arrays keep the dense (9, ny, nx) fine shape; only the allocation differs.
|
||||
Blocked fine_objects are stationary circles, optionally surface-actuated:
|
||||
prepare_fine_boundary decides legality on the dense fine grid, then cut
|
||||
links are staged receiver-local per hop: halo -> BFL -> step, same buffer.
|
||||
``base_block=(by, bx)`` also blocks the parent; it requires ``fine_block``.
|
||||
Parent halos precede receiver-local coalescence staging and pull/collision;
|
||||
covered cells are explicitly poisoned on every step. Public shapes stay dense.
|
||||
``observe_raw_exchange=True`` records blocked physical cut-link exchange
|
||||
without changing populations. read_raw_exchange reports the last complete
|
||||
base interval or last substep in base-lattice units per unit depth. This
|
||||
is boundary momentum reaction, not a validated nozzle/stress load. Dense
|
||||
body layouts are not supported by this new diagnostic in this candidate.
|
||||
``jets`` (body.jets.Jet) use the same edge frame as ``fine_objects`` and
|
||||
drive fine cut links selected at their true hits; ``set_jet_amplitude``
|
||||
re-uploads the per-link velocity column only.
|
||||
body levels are not supported by this diagnostic.
|
||||
``observe_window_exchange=True`` (requires observe_raw_exchange) adds a second
|
||||
per-link FP64 ledger accumulated by the same kernel between
|
||||
start_exchange_window() and read_exchange_window(); disabled by default.
|
||||
|
||||
Several patches: pass ``bounds`` as a sequence of (x0, y0, x1, y1)
|
||||
rectangles (FixedPatchSet2D, >= 4 coarse cells apart). This requires
|
||||
fine_block and base_block; fine_objects/jets are projected once onto the
|
||||
level-wide fine grid (each circle inside one patch, body ids = object
|
||||
positions, jet and writer checks level-wide), and
|
||||
uses one level-wide fine grid (2*ny, 2*nx): public fine arrays have that
|
||||
shape, NaN outside the patches' ACTIVE cells. The step sequence is the
|
||||
blocked one above, with every patch's transfers in one launch each.
|
||||
Nested levels: a child set's coordinates index the allocated parent array,
|
||||
including ghost bands and holes: root edge = parent fine_origin + index / 2
|
||||
(zero origin for a parent set; a scalar parent keeps its shifted window
|
||||
origin). A scalar child requires a scalar parent. Rectangle unions impose no
|
||||
fixed gap rule: adjacent, overlapping and concave unions are accepted when
|
||||
their actual transfer dependencies close (FixedPatchSet2D /
|
||||
nest_patch_set). All regions share one buffer pair per level and the fixed
|
||||
schedule of 2**l hops per base step at level l. Bodies/Jets are supported only on the
|
||||
finest level of a nested hierarchy (L2 dx=dt=.25 in root units, unchanged
|
||||
lattice velocity); every whole circle and its BFL stencil must fit strictly
|
||||
away from that level's transfer footprint.
|
||||
|
||||
Three-level separated unions: ``child_rectangles_in_parent=[...]`` is
|
||||
mutually exclusive with scalar ``child`` and accepts either parent form.
|
||||
Coordinates index the allocated L1 array, including ghost bands and holes:
|
||||
root edge = patch.fine_origin + L1 index / 2. A parent set has origin zero;
|
||||
a scalar parent retains its shifted window origin. The child set's public
|
||||
extent is twice the allocated L1 shape, even though storage is sparse.
|
||||
Each pair requires gap >= 4 in its own parent-cell units. All regions share
|
||||
one buffer pair per level and the fixed 1:2:4 schedule; L2-only bodies/Jets
|
||||
below apply unchanged, with object IDs assigned over the whole level.
|
||||
``explosion="projected_linear"`` selects experimental transverse linear
|
||||
reconstruction on each adjacent pair. Default "constant" is unchanged.
|
||||
Projected mode requires periodic root, all-blocked SRT levels without
|
||||
bodies/Jets or LES; this backend supplies no forcing. Geometry is generic,
|
||||
but union/multilevel GPU and matched-physics qualification remain pending.
|
||||
|
||||
Three-level single chain: ``child`` bounds use L1 window cells; all three
|
||||
levels must be blocked. ``child_objects`` and ``child_jets`` use the L0
|
||||
edge frame, with L2 dx=dt=.25 and unchanged lattice velocity. L1 bodies
|
||||
and Jets remain unsupported in a chain. Every whole circle and its BFL
|
||||
stencil must fit strictly away from the L2 transfer footprint. L2 runs
|
||||
halo -> BFL -> step four times per base step. Runtime child Jet changes
|
||||
require ``set_jet_amplitude(index, amplitude, level=2)``.
|
||||
|
||||
Level lists (any depth): ``refinements[l]`` gives level l+1 in level-l
|
||||
integer cells (scalar window or rectangle list, see lbm.hierarchy);
|
||||
``level_blocks``/``level_objects``/``level_jets`` hold one entry per level,
|
||||
L0 included. They are mutually exclusive with the legacy keywords above,
|
||||
which are normalised to the same lists. One recursive hop advances every
|
||||
level: a child explodes from its parent's current hop source, takes two
|
||||
hops, then the parent gathers halos, stages restriction, applies BFL and
|
||||
steps. Depth >= 3 must be fully blocked with bodies/Jets on the finest level.
|
||||
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.
|
||||
"""
|
||||
def __init__(self, cfg, bounds=None, *, context, coarse_boundary="periodic", fine_objects=(),
|
||||
fine_block=None, base_block=None, jets=(), child=None, child_block=None,
|
||||
child_objects=(), child_jets=(), child_rectangles_in_parent=None,
|
||||
refinements=None, level_blocks=None, level_objects=None, level_jets=None,
|
||||
observe_raw_exchange=False):
|
||||
def __init__(self, cfg, *, context, refinements, coarse_boundary="periodic",
|
||||
level_blocks=None, level_objects=None, level_jets=None,
|
||||
observe_raw_exchange=False, observe_window_exchange=False, explosion="constant"):
|
||||
if HALO != 4:
|
||||
raise RuntimeError("refinement.cu requires a four-cell fine halo")
|
||||
if coarse_boundary not in ("periodic", "slot_reservoir"):
|
||||
raise ValueError("coarse_boundary must be periodic or slot_reservoir")
|
||||
if refinements is None:
|
||||
if bounds is None:
|
||||
raise ValueError("bounds or refinements is required")
|
||||
if level_blocks is not None or level_objects is not None or level_jets is not None:
|
||||
raise ValueError("level_blocks/level_objects/level_jets require refinements")
|
||||
refinements, level_blocks, level_objects, level_jets = _legacy_levels(
|
||||
bounds, fine_block=fine_block, base_block=base_block, child=child,
|
||||
child_block=child_block, child_rectangles_in_parent=child_rectangles_in_parent,
|
||||
fine_objects=fine_objects, jets=jets, child_objects=child_objects,
|
||||
child_jets=child_jets)
|
||||
else:
|
||||
if (bounds is not None or child is not None or child_rectangles_in_parent is not None
|
||||
or fine_block is not None or base_block is not None or child_block is not None
|
||||
or tuple(fine_objects) or tuple(jets) or tuple(child_objects) or tuple(child_jets)):
|
||||
raise ValueError("refinements and the legacy patch keywords are mutually exclusive")
|
||||
depth = len(refinements) + 1
|
||||
level_blocks = [None] * depth if level_blocks is None else list(level_blocks)
|
||||
level_objects = [()] * depth if level_objects is None else list(level_objects)
|
||||
level_jets = [()] * depth if level_jets is None else list(level_jets)
|
||||
if coarse_boundary not in ("periodic", "slot_reservoir", "channel"):
|
||||
raise ValueError("coarse_boundary must be periodic, slot_reservoir or channel")
|
||||
depth = len(refinements) + 1
|
||||
level_blocks = [None] * depth if level_blocks is None else list(level_blocks)
|
||||
level_objects = [()] * depth if level_objects is None else list(level_objects)
|
||||
level_jets = [()] * depth if level_jets is None else list(level_jets)
|
||||
level_objects = [tuple(o) for o in level_objects]
|
||||
level_jets = [tuple(j) for j in level_jets]
|
||||
level_blocks = [None if b is None else tuple(b) for b in level_blocks]
|
||||
_check_levels(refinements, level_blocks, level_objects, level_jets)
|
||||
depth = len(refinements) + 1
|
||||
if explosion not in ("constant", "projected_linear"):
|
||||
raise ValueError("explosion must be constant or projected_linear")
|
||||
if explosion == "projected_linear" and (
|
||||
coarse_boundary != "periodic" or any(b is None for b in level_blocks)
|
||||
or any(level_objects) or any(level_jets)
|
||||
or cfg.collision != "SRT" or cfg.les_enabled):
|
||||
raise ValueError("projected explosion requires periodic all-blocked SRT levels "
|
||||
"without bodies/Jets or LES")
|
||||
self._explosion = explosion
|
||||
self._ddf_shifting = bool(cfg.ddf_shifting)
|
||||
if self._ddf_shifting and any(b is None for b in level_blocks):
|
||||
raise ValueError("shifted populations require all-blocked levels; dense/mixed is unsupported")
|
||||
self._channel = coarse_boundary == "channel"
|
||||
if self._channel:
|
||||
validate_channel_config(cfg, level_blocks)
|
||||
self._wall_aware = coarse_boundary == "slot_reservoir"
|
||||
if self._wall_aware and (cfg.y_wall_bc != "on_site_velocity" or cfg.top_wall_bc != "reservoir"
|
||||
or cfg.velocity != 0 or not 2 <= cfg.wall_slot_width < cfg.nx):
|
||||
@@ -264,6 +225,9 @@ class FixedPatchStepper:
|
||||
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)
|
||||
if self._channel:
|
||||
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])
|
||||
# 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],
|
||||
viscosity=2 * cfg.viscosity)
|
||||
@@ -283,11 +247,12 @@ class FixedPatchStepper:
|
||||
pair = pairs[level - 1]
|
||||
if level == 1:
|
||||
host = prepare_fine_boundary(cfg, pair, level_objects[1],
|
||||
wall_aware=self._wall_aware, jets=level_jets[1])
|
||||
wall_aware=self._wall_aware, jets=level_jets[1],
|
||||
_shifted_blocked=self._ddf_shifting)
|
||||
else:
|
||||
host = prepare_fine_boundary(self._level_cfgs[level - 1], pair, level_objects[level],
|
||||
jets=level_jets[level], parent_origin=origin,
|
||||
parent_spacing=spacing)
|
||||
parent_spacing=spacing, _shifted_blocked=self._ddf_shifting)
|
||||
self._boundary_hosts.append(host)
|
||||
origin = tuple(o + spacing * f for o, f in zip(origin, pair.fine_origin))
|
||||
spacing /= 2
|
||||
@@ -298,6 +263,13 @@ class FixedPatchStepper:
|
||||
for b, layout in zip(self._boundary_hosts, self._layouts)):
|
||||
raise ValueError("raw exchange currently requires blocked body levels")
|
||||
self._observe_raw_exchange = observe_raw_exchange
|
||||
if not isinstance(observe_window_exchange, bool):
|
||||
raise ValueError("observe_window_exchange must be bool")
|
||||
if observe_window_exchange and not observe_raw_exchange:
|
||||
raise ValueError("observe_window_exchange requires observe_raw_exchange")
|
||||
self._observe_window = observe_window_exchange
|
||||
self._window_valid = False
|
||||
self._window_start = None
|
||||
self._exchange_valid = False
|
||||
self._exchange_spacing = tuple(spacings)
|
||||
self._exchange_centers = tuple(tuple(tuple(o.geometry.center) for o in objects)
|
||||
@@ -305,13 +277,17 @@ class FixedPatchStepper:
|
||||
self._level_jets = level_jets
|
||||
self._level_boundaries = [None] + [
|
||||
_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)
|
||||
for level in range(1, depth)]
|
||||
self._leaf = [kind == ACTIVE for kind in self.hierarchy.kinds]
|
||||
for level in range(1, depth):
|
||||
host = self._boundary_hosts[level]
|
||||
if host is not None:
|
||||
self._leaf[level] &= (host.flags.reshape(pairs[level - 1].fine_shape) & SOLID) == 0
|
||||
self._state = [mask.copy() for mask in self._leaf]
|
||||
if self._channel:
|
||||
self._leaf[0] &= channel_masks((cfg.ny, cfg.nx))[0]
|
||||
# Private constructor inputs for the bounded manual L2 continuation.
|
||||
self._remap_cfg = replace(cfg)
|
||||
self._level_blocks = level_blocks
|
||||
@@ -344,6 +320,8 @@ class FixedPatchStepper:
|
||||
count = len(b.tables["cell"])
|
||||
b.last_raw = self._allocate(count * 4 * 4)
|
||||
b.interval_integral = self._allocate(count * 4 * 8)
|
||||
if self._observe_window:
|
||||
b.window_integral = self._allocate(count * 4 * 8)
|
||||
b.accumulate = level.module.get_function("BlockAccumulateExchange")
|
||||
b.kernel = level.module.get_function("BlockCurvedBoundary")
|
||||
if self._level_boundaries[1] is not None:
|
||||
@@ -378,13 +356,16 @@ class FixedPatchStepper:
|
||||
config = replace(config, nx=layout.local_shape[1], ny=layout.local_shape[0],
|
||||
y_wall_bc="bounce_back", top_wall_bc="velocity",
|
||||
wall_slot_width=0, wall_normal_velocity=0.0, velocity=0.0)
|
||||
name, source = "BlockStep", "blocks.cu"
|
||||
name, source = ("BlockChannelStep" if index == 0 and self._channel else "BlockStep"), "blocks.cu"
|
||||
config.validate()
|
||||
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)
|
||||
block_root_bc = 0
|
||||
if index == 0 and layout is not None:
|
||||
block_root_bc = 2 if self._channel else 1 if self._wall_aware else 0
|
||||
build = compiler.generate_config(config, n_objects=n_objects, block_root_bc=block_root_bc)
|
||||
try:
|
||||
ptx = compiler.compile_kernel(self.ctx.sm_arch, source=source, build_dir=build)
|
||||
module = compiler.load_module(ptx)
|
||||
@@ -406,6 +387,12 @@ class FixedPatchStepper:
|
||||
if len(layout.halo_dst):
|
||||
level.halo_dst = self._upload(layout.halo_dst)
|
||||
level.halo_src = self._upload(layout.halo_src)
|
||||
if index == 0 and self._channel:
|
||||
cfg = self._remap_cfg
|
||||
ptr, size = module.get_global("d_block_channel")
|
||||
if size != struct.calcsize("=IIf"):
|
||||
raise RuntimeError("Block channel host/device layout mismatch")
|
||||
cuda.memcpy_htod(ptr, struct.pack("=IIf", cfg.nx, cfg.ny, cfg.velocity))
|
||||
if index == 0 and self._wall_aware:
|
||||
cfg = self._remap_cfg
|
||||
ptr, size = module.get_global("d_block_wall")
|
||||
@@ -440,6 +427,11 @@ class FixedPatchStepper:
|
||||
self._upload(children), len(ghosts), len(targets),
|
||||
int(np.prod(pair.coarse_shape if parent.layout is None
|
||||
else parent.layout.local_shape)))
|
||||
if self._explosion == "projected_linear":
|
||||
neighbors, offsets = projected_explosion_tables(
|
||||
pair, parent.layout, self.hierarchy.kinds[index])
|
||||
result.explode = child.module.get_function("BlockExplodeProjected")
|
||||
result.neighbors, result.offsets = self._upload(neighbors), self._upload(offsets)
|
||||
if child.layout is not None:
|
||||
child.kind = self._upload(blocked_kind(child.layout, self.hierarchy.kinds[index + 1]))
|
||||
if index == 0 and base is not None:
|
||||
@@ -469,24 +461,59 @@ class FixedPatchStepper:
|
||||
for array, leaf in zip(arrays, self._leaf):
|
||||
array[:, ~leaf] = np.nan
|
||||
|
||||
def initialize(self, *level_g, coarse_g=None, fine_g=None, child_g=None):
|
||||
"""Upload an explicit G_0, one Q-major array per level; non-leaves are discarded.
|
||||
def _mask_nonstate(self, *arrays):
|
||||
for array, state in zip(arrays, self._state):
|
||||
array[:, ~state] = np.nan
|
||||
|
||||
Pass the level arrays positionally (L0 first), or use the original
|
||||
two/three-level names coarse_g, fine_g[, child_g]; mixing is rejected.
|
||||
def _check_storage(self):
|
||||
if any(bool(c.ddf_shifting) != self._ddf_shifting for c in self._level_cfgs):
|
||||
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):
|
||||
"""Upload physical postcollision G, encoding before any FP32 rounding.
|
||||
|
||||
Shifted input should be physical FP64 to retain small perturbations.
|
||||
Public initialization resets the step count; non-leaves are discarded.
|
||||
"""
|
||||
given = _initial_levels(level_g, coarse_g, fine_g, child_g)
|
||||
given = tuple(level_g)
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("new", "ready")
|
||||
self._check_storage()
|
||||
if len(given) != len(self._levels):
|
||||
raise ValueError("Expected one Q-major array per level")
|
||||
arrays = [np.array(a, dtype=np.float32, order="C", copy=True) for a in given]
|
||||
dtype = np.float64 if self._ddf_shifting else np.float32
|
||||
arrays = [np.array(a, dtype=dtype, order="C", copy=True) for a in given]
|
||||
if any(a.shape != level.shape for a, level in zip(arrays, self._levels)):
|
||||
raise ValueError("Expected Q-major (9,ny,nx) arrays for every level")
|
||||
for array, mask in zip(arrays, self._leaf):
|
||||
for array, mask in zip(arrays, self._state):
|
||||
if not np.isfinite(array[:, mask]).all() or (array[:, mask].sum(axis=0) <= 0).any():
|
||||
raise ValueError("Leaf populations must be finite with positive density")
|
||||
self._mask_nonleaves(*arrays)
|
||||
encoded = tuple(_encode_physical(a, self._ddf_shifting) for a in arrays)
|
||||
self._initialize_raw(_StoredPopulations(encoded, self._ddf_shifting))
|
||||
|
||||
def _initialize_raw(self, snapshot):
|
||||
"""Upload an internally tagged same-representation state without a codec."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("new", "ready")
|
||||
self._check_storage()
|
||||
if (not isinstance(snapshot, _StoredPopulations)
|
||||
or type(snapshot.shifted) is not bool or snapshot.shifted != self._ddf_shifting):
|
||||
raise ValueError("stored population representation mismatch")
|
||||
if len(snapshot.values) != len(self._levels):
|
||||
raise ValueError("Expected one stored array per level")
|
||||
if any(not isinstance(a, np.ndarray) or a.dtype != np.float32 for a in snapshot.values):
|
||||
raise ValueError("stored populations must be FP32")
|
||||
arrays = [np.array(a, order="C", copy=True) for a in snapshot.values]
|
||||
if any(a.shape != level.shape for a, level in zip(arrays, self._levels)):
|
||||
raise ValueError("Expected Q-major (9,ny,nx) arrays for every level")
|
||||
for array, mask in zip(arrays, self._state):
|
||||
values = array[:, mask]
|
||||
if (not np.isfinite(values).all()
|
||||
or (values.sum(axis=0, dtype=np.float64) + int(self._ddf_shifting) <= 0).any()):
|
||||
raise ValueError("Leaf populations must be finite with positive density")
|
||||
self._mask_nonstate(*arrays)
|
||||
arrays = [a if level.layout is None else level.layout.to_blocked(a)
|
||||
for a, level in zip(arrays, self._levels)]
|
||||
try:
|
||||
@@ -497,6 +524,7 @@ class FixedPatchStepper:
|
||||
cuda.memset_d8(self._fine_curved.link_exchange_gpu, 0, self._fine_curved.count * 12)
|
||||
self._step_count = 0
|
||||
self._exchange_valid = False
|
||||
self._window_valid = False
|
||||
self._status = "ready"
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
@@ -507,8 +535,8 @@ class FixedPatchStepper:
|
||||
|
||||
Periodic three-level set children only; L0/L1 and block sizes stay fixed.
|
||||
A body run requires its original root-edge-frame ``child_objects``:
|
||||
stationary circles pinned in unchanged complete L2 rectangles.
|
||||
Separated fluid-only rectangles may be added, moved or removed.
|
||||
stationary circles with identical physical boundary and BFL support.
|
||||
Other fluid coverage may change, even in the same connected union.
|
||||
Current L2 Jets (including updated amplitudes)
|
||||
are preserved. Local projected boundary objects are not valid inputs.
|
||||
The lock and stream synchronization certify
|
||||
@@ -518,13 +546,14 @@ class FixedPatchStepper:
|
||||
construction/upload errors never invalidate the old instance; failure
|
||||
to synchronize/read its device state does, as with step/download.
|
||||
"""
|
||||
from .synchronized_remap import (RemapTopology, remap_synchronized,
|
||||
from .synchronized_remap import (RemapTopology, remap_synchronized, _remap_stored,
|
||||
same_physical_boundary, boundary_velocity)
|
||||
|
||||
with self._lock:
|
||||
self._require("ready")
|
||||
self._check_storage()
|
||||
pairs = self.hierarchy.pairs
|
||||
if (self._wall_aware or self.hierarchy.depth != 3
|
||||
if (self._wall_aware or self._channel or self.hierarchy.depth != 3
|
||||
or not isinstance(pairs[1], FixedPatchSet2D)
|
||||
or self._boundary_hosts[1] is not None
|
||||
or any(b is not None for b in self._level_boundaries[:2])):
|
||||
@@ -534,23 +563,25 @@ class FixedPatchStepper:
|
||||
actual = self._level_boundaries[2]
|
||||
jets = () if actual is None else actual.jets
|
||||
old = RemapTopology.from_config(cfg, pairs[0], pairs[1].bounds,
|
||||
child_objects=child_objects, child_jets=jets)
|
||||
child_objects=child_objects, child_jets=jets,
|
||||
_shifted_blocked=self._ddf_shifting)
|
||||
if (not same_physical_boundary(old.boundary, self._boundary_hosts[2])
|
||||
or (self._boundary_hosts[2] is not None and actual is None)):
|
||||
raise ValueError("child_objects must reproduce the original root-edge-frame boundary")
|
||||
if actual is not None:
|
||||
velocity = actual.tables.get("surface_velocity")
|
||||
if velocity is None:
|
||||
velocity = boundary_velocity(cfg, pairs[1], actual.host, jets)
|
||||
velocity = boundary_velocity(cfg, pairs[1], actual.host, jets,
|
||||
_shifted_blocked=self._ddf_shifting)
|
||||
if (not same_physical_boundary(old.boundary, actual.host)
|
||||
or not np.array_equal(old.surface_velocity, velocity)):
|
||||
raise ValueError("child_objects changed boundary ownership or surface velocity")
|
||||
new = RemapTopology.from_config(cfg, pairs[0], rectangles,
|
||||
child_objects=child_objects, child_jets=jets)
|
||||
if (old.pinned_rectangles != new.pinned_rectangles
|
||||
or not same_physical_boundary(old.boundary, new.boundary)
|
||||
child_objects=child_objects, child_jets=jets,
|
||||
_shifted_blocked=self._ddf_shifting)
|
||||
if (not same_physical_boundary(old.boundary, new.boundary)
|
||||
or not np.array_equal(old.surface_velocity, new.surface_velocity)):
|
||||
raise ValueError("remap requires the identical pinned body rectangles and boundary")
|
||||
raise ValueError("remap requires the identical pinned physical boundary and BFL support")
|
||||
base_block, fine_block, child_block = self._level_blocks
|
||||
# Check sparse block alignment/launch size before any device read/allocation.
|
||||
layout = new.child.fine_layout(child_block)
|
||||
@@ -559,22 +590,27 @@ class FixedPatchStepper:
|
||||
with self.ctx.activate():
|
||||
try:
|
||||
self._stream.synchronize()
|
||||
source = self.download()
|
||||
source = self._download_raw() if self._ddf_shifting else self.download()
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
count = self._step_count
|
||||
transferred = remap_synchronized(
|
||||
transfer = _remap_stored if self._ddf_shifting else remap_synchronized
|
||||
options = dict(shifted=self._ddf_shifting) if self._ddf_shifting else {}
|
||||
transferred = transfer(
|
||||
old, new, source, base_count=count, level_steps=(count, 2*count, 4*count),
|
||||
phase="postcollision_g")
|
||||
phase="postcollision_g", **options)
|
||||
candidate = type(self)(
|
||||
cfg, pairs[0].bounds, context=self.ctx, base_block=base_block,
|
||||
fine_block=fine_block, child_block=child_block,
|
||||
child_rectangles_in_parent=new.child.bounds,
|
||||
child_objects=child_objects, child_jets=jets,
|
||||
observe_raw_exchange=self._observe_raw_exchange)
|
||||
cfg, context=self.ctx, refinements=[pairs[0].bounds, new.child.bounds],
|
||||
level_blocks=[base_block, fine_block, child_block],
|
||||
level_objects=[(), (), child_objects], level_jets=[(), (), jets],
|
||||
observe_raw_exchange=self._observe_raw_exchange,
|
||||
observe_window_exchange=self._observe_window, explosion=self._explosion)
|
||||
try:
|
||||
candidate.initialize(*transferred.g)
|
||||
if self._ddf_shifting:
|
||||
candidate._initialize_raw(_StoredPopulations(transferred.g, transferred.shifted))
|
||||
else:
|
||||
candidate.initialize(*transferred.g)
|
||||
candidate._step_count = count
|
||||
except BaseException:
|
||||
try:
|
||||
@@ -662,6 +698,7 @@ class FixedPatchStepper:
|
||||
pair, level = self._pairs[index - 1], self._levels[index]
|
||||
pair.explode(parent_buffer, level.current, pair.ghosts, pair.donors, np.uint32(pair.n_ghosts),
|
||||
np.uint64(pair.parent_cells),
|
||||
*((pair.neighbors, pair.offsets) if pair.neighbors is not None else ()),
|
||||
grid=((pair.n_ghosts + self._block[0] - 1) // self._block[0], 1), **kw)
|
||||
self._hop(index, level.current, level.spare, **kw)
|
||||
self._hop(index, level.spare, level.current, **kw)
|
||||
@@ -701,6 +738,10 @@ class FixedPatchStepper:
|
||||
b.accumulate(b.last_raw, b.interval_integral, np.uint32(count),
|
||||
np.float64(h), np.float64(h),
|
||||
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
|
||||
if self._window_valid and b.window_integral is not None:
|
||||
b.accumulate(b.last_raw, b.window_integral, np.uint32(count),
|
||||
np.float64(h), np.float64(h),
|
||||
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
|
||||
flags = np.uintp(0) if level.boundary is None else level.boundary.flags
|
||||
level.kernel(src, dst, np.uint32(level.layout.halo), flags, level.kind, grid=level.grid, **kw)
|
||||
|
||||
@@ -711,31 +752,93 @@ class FixedPatchStepper:
|
||||
grid=((pairs + self._block[0] - 1) // self._block[0], 1), **kw)
|
||||
|
||||
def step(self, n=1):
|
||||
"""Advance G→G by n base intervals: level l takes 2**l hops per interval."""
|
||||
"""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:
|
||||
raise ValueError("n must be a nonnegative integer")
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("ready")
|
||||
if n == 0:
|
||||
return
|
||||
root = self._levels[0]
|
||||
kw = dict(block=self._block, stream=self._stream)
|
||||
try:
|
||||
self._status = "advancing"
|
||||
self._exchange_valid = False
|
||||
for _ in range(n):
|
||||
if self._observe_raw_exchange:
|
||||
self._exchange_valid = False
|
||||
if self._observe_raw_exchange: # stream-ordered: holds the last interval only
|
||||
for b in self._level_boundaries:
|
||||
if b is not None:
|
||||
cuda.memset_d8_async(b.interval_integral, 0,
|
||||
len(b.tables["cell"]) * 32, self._stream)
|
||||
self._hop(0, root.current, root.spare, **kw)
|
||||
self._stream.synchronize()
|
||||
root.current, root.spare = root.spare, root.current
|
||||
self._step_count += 1
|
||||
self._exchange_valid = self._observe_raw_exchange
|
||||
root.current, root.spare = root.spare, root.current # host handles only
|
||||
self._stream.synchronize()
|
||||
self._step_count += n
|
||||
self._exchange_valid = self._observe_raw_exchange
|
||||
self._status = "ready"
|
||||
except BaseException:
|
||||
self._window_valid = False
|
||||
self._status = "failed"
|
||||
raise
|
||||
|
||||
def start_exchange_window(self):
|
||||
"""Zero the window ledgers and open a window at the current completed step count."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("ready")
|
||||
if not self._observe_window:
|
||||
raise RuntimeError("window exchange observation is disabled")
|
||||
self._window_valid = False
|
||||
try:
|
||||
for b in self._level_boundaries:
|
||||
if b is not None and b.window_integral is not None:
|
||||
cuda.memset_d8(b.window_integral, 0, len(b.tables["cell"]) * 32)
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
self._window_start = self._step_count
|
||||
self._window_valid = True
|
||||
|
||||
def read_exchange_window(self, body_id, *, level=1):
|
||||
"""Non-destructive read of [window start, current step count] in base-lattice units."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("ready")
|
||||
if not self._observe_window:
|
||||
raise RuntimeError("window exchange observation is disabled")
|
||||
if not self._window_valid:
|
||||
raise RuntimeError("no valid exchange window; call start_exchange_window()")
|
||||
duration = self._step_count - self._window_start
|
||||
if duration < 1:
|
||||
raise RuntimeError("exchange window requires at least one completed base interval")
|
||||
if (isinstance(level, bool) or not isinstance(level, Integral)
|
||||
or not 0 <= level < len(self._levels)):
|
||||
raise ValueError("level must name an existing level")
|
||||
b = self._level_boundaries[level]
|
||||
if (b is None or isinstance(body_id, bool) or not isinstance(body_id, Integral)
|
||||
or not 0 <= body_id < len(b.host.objects)):
|
||||
raise ValueError("body_id must name a body on this level")
|
||||
rows = np.empty((len(b.tables["cell"]), 4), np.float64)
|
||||
try:
|
||||
cuda.memcpy_dtoh(rows, b.window_integral)
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
return summarize_exchange(rows, b.tables["body_id"], b.tables["aperture"],
|
||||
body_id=body_id, level=level, h=self._exchange_spacing[level],
|
||||
time_end=self._step_count, window="window",
|
||||
reference_center=self._exchange_centers[level][body_id],
|
||||
duration=duration)
|
||||
|
||||
def read_raw_exchange(self, body_id, *, level=1, window="last_base_step"):
|
||||
"""Read raw boundary reaction about its geometric center in the root edge frame.
|
||||
@@ -773,9 +876,17 @@ class FixedPatchStepper:
|
||||
reference_center=self._exchange_centers[level][body_id])
|
||||
|
||||
def download(self):
|
||||
"""Return synchronized G arrays (one per level), NaN on all non-leaves; no host cache."""
|
||||
"""Return physical G (FP64 for shifted storage), NaN on non-leaves."""
|
||||
snapshot = self._download_raw()
|
||||
arrays = tuple(_decode_stored(a, snapshot.shifted) for a in snapshot.values)
|
||||
self._mask_nonleaves(*arrays)
|
||||
return arrays
|
||||
|
||||
def _download_raw(self):
|
||||
"""Current owned populations with a representation tag; no add/subtract w."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("ready")
|
||||
self._check_storage()
|
||||
arrays = [np.empty(level.shape if level.layout is None else level.layout.blocked_shape,
|
||||
np.float32) for level in self._levels]
|
||||
try:
|
||||
@@ -786,8 +897,8 @@ class FixedPatchStepper:
|
||||
raise
|
||||
arrays = [a if level.layout is None else level.layout.to_dense(a)
|
||||
for a, level in zip(arrays, self._levels)]
|
||||
self._mask_nonleaves(*arrays)
|
||||
return tuple(arrays)
|
||||
self._mask_nonstate(*arrays)
|
||||
return _StoredPopulations(tuple(arrays), self._ddf_shifting)
|
||||
|
||||
def close(self):
|
||||
"""Release owned resources; retain them for retry if context activation fails."""
|
||||
@@ -807,6 +918,7 @@ class FixedPatchStepper:
|
||||
if b is not None:
|
||||
b.flags = b.device = b.kernel = None
|
||||
b.last_raw = b.interval_integral = b.accumulate = None
|
||||
b.window_integral = None
|
||||
self._levels.clear()
|
||||
self._pairs = []
|
||||
if getattr(self, "_fine_curved", None) is not None:
|
||||
|
||||
@@ -8,7 +8,7 @@ from dataclasses import dataclass, replace
|
||||
import numpy as np
|
||||
|
||||
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 .refinement import ACTIVE, OUTER_GHOST
|
||||
|
||||
@@ -66,6 +66,50 @@ def blocked_base_tables(patch, layout, order, wall_aware=False, *, composed_kind
|
||||
blocked_kind(layout, dense_kind))
|
||||
|
||||
|
||||
def projected_explosion_tables(pair, parent_layout, composed_parent_kind):
|
||||
"""Experimental transverse linear explosion, in the pair's existing row order.
|
||||
|
||||
Schornbaum & Rüde (2016), arXiv:1508.07982, Algorithm 2, following
|
||||
Chen et al., doi:10.1016/j.physa.2005.09.036. Independently implemented
|
||||
from the mathematics: centered gradients, perpendicular projection and
|
||||
child offsets +/-1/4. The kernel applies the projection per population.
|
||||
|
||||
Only original explosion donors that are owned, uncovered ACTIVE cells
|
||||
may supply gradients. Both axis neighbors must be legal; otherwise repeat
|
||||
the center twice for zero slope. Owner-core addresses avoid stale halos.
|
||||
The same rule applies to scalar windows, unions and every adjacent pair;
|
||||
nested layouts retain their own per-axis (usually nonperiodic) topology.
|
||||
"""
|
||||
ny, nx = pair.coarse_shape
|
||||
kind = np.asarray(composed_parent_kind)
|
||||
if parent_layout.shape != pair.coarse_shape or kind.shape != pair.coarse_shape:
|
||||
raise ValueError("projected explosion parent layout/kind has the wrong shape")
|
||||
donor = pair.ghost_coarse.astype(np.intp)
|
||||
legal = np.zeros(ny * nx, bool)
|
||||
legal[donor] = True
|
||||
legal &= (parent_layout.owned.ravel() & ~pair.covered.ravel()
|
||||
& (kind.ravel() == ACTIVE))
|
||||
if not legal[donor].all():
|
||||
raise ValueError("projected explosion donors need owned uncovered parent ACTIVE cells")
|
||||
y, x = np.divmod(donor, nx)
|
||||
neighbors = np.repeat(donor[:, None], 4, axis=1)
|
||||
for axis, (dx, dy) in enumerate(((1, 0), (0, 1))):
|
||||
minus = ((y - dy) % ny) * nx + (x - dx) % nx
|
||||
plus = ((y + dy) % ny) * nx + (x + dx) % nx
|
||||
valid = legal[minus] & legal[plus]
|
||||
if not parent_layout.periodic[axis]:
|
||||
coordinate, extent = (x, nx) if axis == 0 else (y, ny)
|
||||
valid &= (coordinate > 0) & (coordinate < extent - 1)
|
||||
neighbors[valid, 2 * axis] = minus[valid]
|
||||
neighbors[valid, 2 * axis + 1] = plus[valid]
|
||||
fy, fx = np.divmod(pair.ghost_fine, pair.fine_shape[1])
|
||||
ox, oy = pair.fine_origin
|
||||
offsets = np.column_stack((ox + (fx + .5) / 2 - (x + .5),
|
||||
oy + (fy + .5) / 2 - (y + .5)))
|
||||
return (np.ascontiguousarray(parent_layout.owner.ravel()[neighbors], dtype=np.uint64),
|
||||
np.ascontiguousarray(offsets, dtype=np.float32))
|
||||
|
||||
|
||||
# Required device columns plus optional actuation; observation arms are uploaded
|
||||
# separately only when enabled. body_id/aperture remain host reduction metadata.
|
||||
BLOCKED_BOUNDARY_DEVICE_COLUMNS = ("block", "cell", "dir", "q", "fallback", "surface_velocity")
|
||||
@@ -85,9 +129,10 @@ class _BlockedBoundary:
|
||||
last_raw: object = None
|
||||
interval_integral: object = None
|
||||
accumulate: object = None
|
||||
window_integral: object = None # optional observation-window FP64 ledger
|
||||
|
||||
|
||||
def _prepare_blocked_boundary(boundary, layout, cfg, jets):
|
||||
def _prepare_blocked_boundary(boundary, layout, cfg, jets, *, _shifted_blocked=False):
|
||||
if boundary is None or layout is None:
|
||||
return None
|
||||
fluid_idx, dirs, q, body_id, rx, ry, _, fallback = boundary.curved_host
|
||||
@@ -97,11 +142,13 @@ def _prepare_blocked_boundary(boundary, layout, cfg, jets):
|
||||
tables.update(body_id=np.asarray(body_id, np.int32), rx=np.asarray(rx, np.float32),
|
||||
ry=np.asarray(ry, np.float32), aperture=np.zeros(len(dirs), bool))
|
||||
actuated = bool(jets) or any(o.surface_actuation is not None for o in boundary.objects)
|
||||
jet_cache = None
|
||||
if actuated:
|
||||
cfg = replace(cfg)
|
||||
cfg.validate()
|
||||
tables["surface_velocity"], tables["aperture"] = 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,
|
||||
jets=jets, hits=boundary.curved_hits)
|
||||
return _BlockedBoundary(boundary, tables, jets, actuated,
|
||||
jet_link_shapes(jets, boundary.curved_hits) if jets else None)
|
||||
jets=jets, hits=boundary.curved_hits, _shifted_blocked=_shifted_blocked)
|
||||
if jets:
|
||||
jet_cache = (shape, jet_id)
|
||||
return _BlockedBoundary(boundary, tables, jets, actuated, jet_cache)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""CPU-only whole-volume G transfer, not a runtime adaptive solver.
|
||||
|
||||
Periodic three-level grids with fixed L0/L1 topology are supported, including
|
||||
stationary circles pinned inside unchanged L2 rectangles. The caller
|
||||
stationary circles with unchanged physical boundary/BFL support. The caller
|
||||
certifies that current owned leaves are quiescent,
|
||||
unshifted D2Q9 postcollision G at the supplied completed base interval.
|
||||
No device buffers, device boundary state, ghosts, collision or time advance
|
||||
@@ -16,17 +16,19 @@ from .refinement import (ACTIVE, FixedPatch2D, FixedPatchSet2D, FineBoundary2D,
|
||||
nest_patch_set, prepare_fine_boundary)
|
||||
from .descriptors import SOLID
|
||||
from ..body.jets import surface_link_velocity
|
||||
from .population_storage import _StoredPopulations
|
||||
|
||||
|
||||
@dataclass(frozen=True, eq=False)
|
||||
class RemapTopology:
|
||||
"""Validated fixed parent and separated L2 set; use ``from_config``.
|
||||
"""Validated fixed parent and L2 union; use ``from_config``.
|
||||
|
||||
Child rectangles are in the parent's allocated L1 coordinates, including
|
||||
its window offset/halo when the parent is a scalar patch. Dense masks are
|
||||
rebuilt from geometry, never inferred from finite values in input arrays.
|
||||
``pinned_rectangles`` canonically lists each body-containing rectangle once;
|
||||
fluid-only layouts have no pins.
|
||||
``pinned_rectangles`` is legacy metadata for single-rectangle containment.
|
||||
Eligibility pins physical boundary/stencil data, not entire rectangles or
|
||||
connected fluid regions; each topology independently validates transfer closure.
|
||||
"""
|
||||
parent: object
|
||||
child: FixedPatchSet2D
|
||||
@@ -37,7 +39,7 @@ class RemapTopology:
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, cfg, parent, child_rectangles, *, coarse_boundary="periodic", objects=(),
|
||||
child_objects=(), child_jets=()):
|
||||
child_objects=(), child_jets=(), _shifted_blocked=False):
|
||||
if coarse_boundary != "periodic" or tuple(objects) or cfg.nz != 1:
|
||||
raise ValueError("remap requires periodic 2D state; use child_objects for a pinned L2 body")
|
||||
if type(parent) not in (FixedPatch2D, FixedPatchSet2D):
|
||||
@@ -49,7 +51,7 @@ class RemapTopology:
|
||||
viscosity=2 * cfg.viscosity)
|
||||
boundary = prepare_fine_boundary(
|
||||
fine_cfg, child, child_objects, jets=child_jets,
|
||||
parent_origin=parent.fine_origin, parent_spacing=.5)
|
||||
parent_origin=parent.fine_origin, parent_spacing=.5, _shifted_blocked=_shifted_blocked)
|
||||
pinned, velocity = (), None
|
||||
child_leaf = child.fine_kind == ACTIVE
|
||||
if boundary is not None:
|
||||
@@ -59,11 +61,14 @@ class RemapTopology:
|
||||
for obj in child_objects:
|
||||
cx, cy = (np.asarray(obj.geometry.center) - parent.fine_origin) * 2
|
||||
radius = obj.geometry._radius * 2
|
||||
containing.add(next(r for r in child.bounds if
|
||||
r[0] <= cx-radius and cx+radius <= r[2] and
|
||||
r[1] <= cy-radius and cy+radius <= r[3]))
|
||||
match = next((r for r in child.bounds if
|
||||
r[0] <= cx-radius and cx+radius <= r[2] and
|
||||
r[1] <= cy-radius and cy+radius <= r[3]), None)
|
||||
if match is not None:
|
||||
containing.add(match)
|
||||
pinned = tuple(sorted(containing))
|
||||
velocity = boundary_velocity(cfg, child, boundary, child_jets)
|
||||
velocity = boundary_velocity(cfg, child, boundary, child_jets,
|
||||
_shifted_blocked=_shifted_blocked)
|
||||
if velocity is not None:
|
||||
velocity.setflags(write=False)
|
||||
masks = (~parent.covered, middle_kind == ACTIVE, child_leaf)
|
||||
@@ -72,7 +77,7 @@ class RemapTopology:
|
||||
return cls(parent, child, masks, boundary, pinned, velocity)
|
||||
|
||||
|
||||
def boundary_velocity(cfg, child, boundary, jets=()):
|
||||
def boundary_velocity(cfg, child, boundary, jets=(), *, _shifted_blocked=False):
|
||||
"""Physical cut-link order; never blocked addresses or stale Jet snapshots."""
|
||||
local_cfg = replace(cfg, nx=child.fine_shape[1], ny=child.fine_shape[0],
|
||||
viscosity=4 * cfg.viscosity)
|
||||
@@ -80,7 +85,7 @@ def boundary_velocity(cfg, child, boundary, jets=()):
|
||||
fluid, dirs, _, ids, rx, ry, _, _ = boundary.curved_host
|
||||
velocity, _ = surface_link_velocity(
|
||||
boundary.objects, local_cfg, boundary.flags, fluid, dirs, ids, rx, ry,
|
||||
jets=jets, hits=boundary.curved_hits)
|
||||
jets=jets, hits=boundary.curved_hits, _shifted_blocked=_shifted_blocked)
|
||||
return velocity
|
||||
|
||||
|
||||
@@ -115,6 +120,7 @@ class RemapCandidate:
|
||||
phase: str
|
||||
origin: tuple
|
||||
spacing: float
|
||||
shifted: bool = False
|
||||
|
||||
|
||||
def _same_parent(a, b):
|
||||
@@ -123,7 +129,7 @@ def _same_parent(a, b):
|
||||
and a.tau_coarse == b.tau_coarse and a.tau_fine == b.tau_fine)
|
||||
|
||||
|
||||
def _read_leaves(topology, arrays, layouts):
|
||||
def _read_leaves(topology, arrays, layouts, *, shifted=False):
|
||||
"""Unpack only owned cores; nonleaf values are not evidence of validity."""
|
||||
if len(arrays) != 3 or len(layouts) != 3:
|
||||
raise ValueError("expected three G arrays and three layouts")
|
||||
@@ -137,8 +143,10 @@ def _read_leaves(topology, arrays, layouts):
|
||||
a = layout.to_dense(a)
|
||||
if a.shape != (9, *leaf.shape):
|
||||
raise ValueError("G must have Q-major (9, ny, nx) shape")
|
||||
if shifted and a.dtype != np.float32:
|
||||
raise ValueError("shifted stored populations must be FP32")
|
||||
values = a[:, leaf]
|
||||
if not np.isfinite(values).all() or (values.sum(axis=0, dtype=np.float64) <= 0).any():
|
||||
if not np.isfinite(values).all() or (values.sum(axis=0, dtype=np.float64) + int(shifted) <= 0).any():
|
||||
raise ValueError("required leaf donors must be finite with positive density")
|
||||
result.append(a)
|
||||
if len({a.dtype for a in result}) != 1:
|
||||
@@ -163,14 +171,27 @@ def remap_synchronized(old, new, old_g, *, base_count, level_steps, phase,
|
||||
the source dtype. No equilibrium reconstruction or nonequilibrium scaling.
|
||||
All destination nonleaves are NaN. Inputs and their write flags are intact.
|
||||
"""
|
||||
return _remap(old, new, old_g, base_count=base_count, level_steps=level_steps,
|
||||
phase=phase, origin=origin, spacing=spacing, layouts=layouts)
|
||||
|
||||
|
||||
def _remap_stored(old, new, snapshot, *, shifted, **kwargs):
|
||||
"""Private same-representation transfer; public remap remains physical."""
|
||||
if (not isinstance(snapshot, _StoredPopulations) or type(shifted) is not bool
|
||||
or type(snapshot.shifted) is not bool or snapshot.shifted != shifted):
|
||||
raise ValueError("stored population representation mismatch")
|
||||
return _remap(old, new, snapshot.values, shifted=shifted, **kwargs)
|
||||
|
||||
|
||||
def _remap(old, new, old_g, *, base_count, level_steps, phase,
|
||||
origin=(0., 0.), spacing=1., layouts=None, shifted=False):
|
||||
if (not isinstance(old, RemapTopology) or not isinstance(new, RemapTopology)
|
||||
or not _same_parent(old.parent, new.parent)
|
||||
or old.child.tau_fine != new.child.tau_fine):
|
||||
raise ValueError("remap requires unchanged L0/L1 geometry and relaxation")
|
||||
if (old.pinned_rectangles != new.pinned_rectangles
|
||||
or not same_physical_boundary(old.boundary, new.boundary)
|
||||
if (not same_physical_boundary(old.boundary, new.boundary)
|
||||
or not np.array_equal(old.surface_velocity, new.surface_velocity)):
|
||||
raise ValueError("remap requires the identical pinned body rectangles and physical boundary")
|
||||
raise ValueError("remap requires the identical pinned physical boundary and BFL support")
|
||||
if not isinstance(level_steps, (tuple, list)):
|
||||
raise ValueError("level_steps must be three completed integer counts")
|
||||
counts = (base_count, *level_steps)
|
||||
@@ -187,7 +208,7 @@ def remap_synchronized(old, new, old_g, *, base_count, level_steps, phase,
|
||||
if (frame.shape != (2,) or not np.isfinite(frame).all()
|
||||
or not np.isscalar(spacing) or not np.isfinite(spacing) or spacing <= 0):
|
||||
raise ValueError("frame needs a finite (x,y) origin and positive spacing")
|
||||
source = _read_leaves(old, old_g, (None,)*3 if layouts is None else layouts)
|
||||
source = _read_leaves(old, old_g, (None,)*3 if layouts is None else layouts, shifted=shifted)
|
||||
candidate = [np.full((9, *mask.shape), np.nan, dtype=source[0].dtype)
|
||||
for mask in new.leaf_masks]
|
||||
for level, (before, after) in enumerate(zip(old.leaf_masks, new.leaf_masks)):
|
||||
@@ -208,6 +229,6 @@ def remap_synchronized(old, new, old_g, *, base_count, level_steps, phase,
|
||||
# Scale first to avoid an overflowing intermediate sum for finite FP64 G.
|
||||
mean = sum(source[2][:, cy, cx].astype(np.float64) * .25 for cy, cx in children)
|
||||
candidate[1][:, y, x] = mean
|
||||
_read_leaves(new, candidate, (None,)*3)
|
||||
_read_leaves(new, candidate, (None,)*3, shifted=shifted)
|
||||
return RemapCandidate(new, tuple(candidate), int(base_count), tuple(map(int, level_steps)),
|
||||
phase, tuple(map(float, frame)), float(spacing))
|
||||
phase, tuple(map(float, frame)), float(spacing), shifted)
|
||||
|
||||
@@ -336,13 +336,19 @@ class Simulation:
|
||||
def read_surface_exchange(self, id: int):
|
||||
"""Latest step's wall/aperture (Fx,Fy,Tz) exchange, not total nozzle force.
|
||||
|
||||
Supports SurfaceActuation and Jet-driven bodies, including zero-amplitude
|
||||
apertures. Passive bodies without an aperture are rejected.
|
||||
Unlike read_body this is not accumulated/time-averaged. It resets after
|
||||
geometry sync or checkpoint load; advance before interpreting it.
|
||||
"""
|
||||
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")
|
||||
self.stream.synchronize()
|
||||
return self.field.curved.surface_exchange(id)
|
||||
return curved.surface_exchange(id)
|
||||
|
||||
@context_bound
|
||||
def read_body(self, id: int, *,
|
||||
@@ -451,18 +457,21 @@ class Simulation:
|
||||
@context_bound
|
||||
def initialize(self):
|
||||
"""Initialize flow field and sync objects to GPU."""
|
||||
# Recompile if objects were added after construction.
|
||||
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:
|
||||
self.recompile()
|
||||
|
||||
self.field.flag = self.field.build_channel_flags()
|
||||
if self.bodies.count > 0:
|
||||
self.field.flag = self.bodies.build_flags(self.field.flag)
|
||||
self.field.flag = next_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()
|
||||
if self.bodies.count > 0:
|
||||
self.bodies.sync_to_gpu(self.field, rebuild_flags=False)
|
||||
|
||||
@@ -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()
|
||||
@@ -65,9 +65,14 @@ def test_shifted_topology_add_remove_preserves_cache_and_finite_flow(streaming,p
|
||||
assert np.max(macro['rho'][fluid]) < 1.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize('streaming',['double_buffer','esopull'])
|
||||
def test_checkpoint_exact_continuation_and_controls(streaming,tmp_path):
|
||||
with make_sim(nx=48,ny=24,streaming=streaming,shifted=True) as sim:
|
||||
@pytest.mark.parametrize('collision,streaming', [
|
||||
pytest.param('SRT', 'double_buffer', id='double_buffer'),
|
||||
pytest.param('SRT', 'esopull', id='esopull'),
|
||||
pytest.param('TRT', 'double_buffer', id='trt-double_buffer'),
|
||||
])
|
||||
def test_checkpoint_exact_continuation_and_controls(collision,streaming,tmp_path):
|
||||
# Same collision throughout save/load/continue; this is not a method-switch restart.
|
||||
with make_sim(nx=48,ny=24,streaming=streaming,shifted=True,collision=collision) as sim:
|
||||
sim.add_body('circle',center=(14,12),radius=3)
|
||||
sim.add_body('force_region',center=(30,12),radius=2)
|
||||
sim.initialize()
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Actual production step kernels under a CPU launch shim. No CUDA claim.
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#define __device__
|
||||
#define __constant__
|
||||
#define __global__
|
||||
#define __forceinline__ inline
|
||||
#define __launch_bounds__(...)
|
||||
#define LATTICE_D2Q9 0
|
||||
#define LATTICE_D3Q19 1
|
||||
#define LATTICE_MODEL 0
|
||||
#define NQ 9
|
||||
#define DIM 2
|
||||
#define NZ 1
|
||||
#define NT 1
|
||||
#define COLLISION_MODEL 0
|
||||
#define BLOCK_ROOT_BC 2
|
||||
#define USE_LES 0
|
||||
#define RHO 1.f
|
||||
#define U0 .025f
|
||||
#define OMEGA_COLLISION_MIN .01f
|
||||
#define OMEGA_COLLISION_MAX 1.99f
|
||||
#define Y_WALL_BC 0
|
||||
#define OUTLET_MODE 0
|
||||
struct Dim {unsigned int x=0,y=0,z=0;} threadIdx,blockIdx,blockDim;
|
||||
inline float __fdiv_rn(float a,float b){return a/b;}
|
||||
#include "core/precision.cuh"
|
||||
#include "core/descriptors.cuh"
|
||||
#include "core/layout.cuh"
|
||||
#include "core/params.cuh"
|
||||
#include "core/flags.cuh"
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#include "streaming/pull_double_buffer.cuh"
|
||||
#if UNIFORM
|
||||
#include "operators/forcing_guo.cuh"
|
||||
#include "operators/helpers.cuh"
|
||||
#include "boundary/bounce_back.cuh"
|
||||
#include "boundary/inlet_outlet.cuh"
|
||||
#include "step/one_step_double.cu"
|
||||
#else
|
||||
constexpr unsigned char KIND_OUTER=2;
|
||||
#include "step/block_channel.cu"
|
||||
#endif
|
||||
int main(){
|
||||
constexpr int gx=24,gy=20,n=gx*gy;
|
||||
std::vector<float> state(9*n),next(9*n);
|
||||
for(int q=0;q<9;q++)for(int y=0;y<gy;y++)for(int x=0;x<gx;x++)
|
||||
state[q*n+y*gx+x]=d_w[q]*(1.f+.001f*std::sin(float(x+3*y+q)))-USE_DDF_SHIFTING*d_w[q];
|
||||
d_params={1.25f,0,0,0,0};
|
||||
#if UNIFORM
|
||||
std::vector<uint16_t> flags(n,FLAG_FLUID);
|
||||
for(int y=0;y<gy;y++)for(int x=0;x<gx;x++){
|
||||
if(y==0||y==gy-1) flags[y*gx+x]=FLAG_SOLID|FLAG_BC_WALL;
|
||||
else if(x==0) flags[y*gx+x]=FLAG_SOLID|FLAG_BC_INLET;
|
||||
else if(x==gx-1) flags[y*gx+x]=FLAG_SOLID|FLAG_BC_OUTLET;
|
||||
}
|
||||
#else
|
||||
constexpr int bx=NX-2,by=NY-2,nbx=gx/bx,nby=gy/by;
|
||||
std::vector<float> input(nbx*nby*9*TOTAL_CELLS),output(input.size());
|
||||
d_block_channel={gx,gy,U0};
|
||||
#endif
|
||||
for(int step=0;step<5;step++){
|
||||
#if UNIFORM
|
||||
for(int y=0;y<gy;y++)for(int x=0;x<gx;x++){
|
||||
blockIdx={unsigned(x),unsigned(y),0};OneStep(flags.data(),state.data(),next.data());
|
||||
}
|
||||
#else
|
||||
// Populate cores and every wrapped halo from the same dense source.
|
||||
for(int b=0;b<nbx*nby;b++)for(int y=0;y<NY;y++)for(int x=0;x<NX;x++)for(int q=0;q<9;q++){
|
||||
int xx=((b%nbx)*bx+x-1+gx)%gx, yy=((b/nbx)*by+y-1+gy)%gy;
|
||||
input[b*9*TOTAL_CELLS+q*TOTAL_CELLS+y*NX+x]=state[q*n+yy*gx+xx];
|
||||
}
|
||||
for(int b=0;b<nbx*nby;b++)for(int y=1;y<NY-1;y++)for(int x=1;x<NX-1;x++){
|
||||
blockIdx={unsigned(x),unsigned(y),unsigned(b)};
|
||||
BlockChannelStep(input.data(),output.data(),1,nullptr,nullptr);
|
||||
for(int q=0;q<9;q++)next[q*n+((b/nbx)*by+y-1)*gx+(b%nbx)*bx+x-1]=output[b*9*TOTAL_CELLS+q*TOTAL_CELLS+y*NX+x];
|
||||
}
|
||||
#endif
|
||||
state.swap(next);
|
||||
}
|
||||
for(float f:state)printf("%.9g\n",f);
|
||||
}
|
||||
@@ -13,6 +13,8 @@
|
||||
#define OMEGA_COLLISION_MAX 1.99f
|
||||
struct Params {float omega, fx, fy, fz;} d_params;
|
||||
#include "core/descriptors.cuh"
|
||||
// Host round-to-nearest shim; CUDA intrinsic behavior is tested on-device.
|
||||
inline float __fdiv_rn(float a, float b) { return a / b; }
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#include "operators/collision_trt.cuh"
|
||||
|
||||
@@ -34,6 +34,8 @@ inline int obs_torque_index(int,int){return 0;}
|
||||
#include "core/descriptors.cuh"
|
||||
#include "core/precision.cuh"
|
||||
#include "core/layout.cuh"
|
||||
// Host round-to-nearest shim; CUDA intrinsic behavior is tested on-device.
|
||||
inline float __fdiv_rn(float a, float b) { return a / b; }
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#include "operators/collision_trt.cuh"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <initializer_list>
|
||||
#define __device__
|
||||
#define __constant__
|
||||
#define __forceinline__ inline
|
||||
#define LATTICE_D2Q9 0
|
||||
#define LATTICE_MODEL 0
|
||||
#define NQ 9
|
||||
#define DIM 2
|
||||
#define USE_DDF_SHIFTING 1
|
||||
#define WALL_NORMAL_VELOCITY 0.f
|
||||
#define WALL_SLOT_WIDTH 0
|
||||
#define NX 16
|
||||
#include "core/descriptors.cuh"
|
||||
// Host round-to-nearest shim; CUDA intrinsic behavior is tested on-device.
|
||||
inline float __fdiv_rn(float a, float b) { return a / b; }
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#include "boundary/planar_velocity.cuh"
|
||||
void emit(const float* f) {for(int q=0;q<9;q++) printf("%.9g ",f[q]);puts("");}
|
||||
int main() {
|
||||
for(float delta : {0.f, 0x1p-30f, -0x1p-30f, .0002f})
|
||||
for(float u : {0.f,.005f}) {
|
||||
float eq[9]; compute_feq(1.f+delta,u,-.2f*u,eq,delta);
|
||||
printf("E %.9g %.9g %.9g\n",delta,u,-.2f*u);emit(eq);
|
||||
}
|
||||
for(float delta : {0.f, 0x1p-30f, -0x1p-30f, .04f, -.01f})
|
||||
for(float jx : {0.f,.005f,-.02f}) {
|
||||
const float jy = -.2f*jx, rho = 1.f+delta;
|
||||
const float ux = jx/rho, uy = jy/rho;
|
||||
float eq[9];compute_feq_from_momentum(delta,jx,jy,ux,uy,eq);
|
||||
printf("J %.9g %.9g %.9g %.9g %.9g\n",delta,jx,jy,ux,uy);emit(eq);
|
||||
}
|
||||
for(int mode : {0,1,2}) for(float rho : {1.f,1.04f}) {
|
||||
float f[9];const float u=.007f,v=.003f;
|
||||
compute_feq(rho,u,v,f,rho-1.f);
|
||||
for(int q=0;q<9;q++)f[q]+=.00001f*(q-4);
|
||||
printf("B %d %.9g %.9g %.9g\n",mode,rho,u,v);emit(f);
|
||||
if(mode==0)reconstruct_bottom_velocity(f,u,v);
|
||||
else if(mode==1)reconstruct_top_velocity(f,u,v);
|
||||
else reconstruct_top_reservoir(f,rho);
|
||||
emit(f);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Host contract of PeriodicBlockStepper.step(n): one stream wait per call (mocked driver)."""
|
||||
import ast
|
||||
from contextlib import nullcontext
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import types
|
||||
from unittest.mock import Mock
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
|
||||
|
||||
def host_module():
|
||||
"""block_stepper with pycuda/compiler replaced by mocks; no GPU context."""
|
||||
# Locate the source without importing it: importing block_stepper loads pycuda.driver,
|
||||
# which CPU-only import-boundary tests (test_build_contracts) must never see.
|
||||
path = Path(importlib.util.find_spec("CelerisLab.lbm.block_stepper").origin)
|
||||
tree = ast.parse(path.read_text())
|
||||
tree.body = [node for node in tree.body if not (
|
||||
isinstance(node, ast.Import) and any(a.name == "pycuda.driver" for a in node.names)
|
||||
or isinstance(node, ast.ImportFrom) and node.module == "cuda")]
|
||||
module = types.ModuleType("CelerisLab.lbm._test_host_block_stepper")
|
||||
module.__package__ = "CelerisLab.lbm"
|
||||
module.__dict__.update(cuda=Mock(), compiler=Mock())
|
||||
exec(compile(tree, str(path), "exec"), module.__dict__)
|
||||
return module
|
||||
|
||||
|
||||
def backend():
|
||||
host, log = host_module(), []
|
||||
host.cuda.mem_get_info.return_value = (1 << 40, 1 << 40)
|
||||
host.cuda.mem_alloc.side_effect = lambda size: Mock(size=size) # distinct buffers
|
||||
module = Mock()
|
||||
module.get_global.return_value = (Mock(), 20)
|
||||
module.get_function.side_effect = lambda name: Mock(
|
||||
side_effect=lambda *a, **k: log.append((name, a[:2])))
|
||||
host.compiler.load_module.return_value = module
|
||||
cfg = LBMConfig(nx=16, ny=16, viscosity=.1)
|
||||
run = host.PeriodicBlockStepper(cfg, BlockLayout2D.build(16, 16, 8, 8),
|
||||
context=Mock(activate=nullcontext, sm_arch="sm_70"))
|
||||
run._stream.synchronize.side_effect = lambda: log.append(("sync", ()))
|
||||
run.initialize(np.full((9, 16, 16), 1 / 9))
|
||||
log.clear()
|
||||
return run, log
|
||||
|
||||
|
||||
class BlockStepSync(unittest.TestCase):
|
||||
def test_step_n_waits_once_and_matches_n_single_steps(self):
|
||||
many, log_many = backend()
|
||||
many.step(3)
|
||||
single, log_single = backend()
|
||||
for _ in range(3):
|
||||
single.step(1)
|
||||
names = [name for name, _ in log_many]
|
||||
self.assertEqual(names.count("sync"), 1)
|
||||
self.assertEqual(names[-1], "sync")
|
||||
self.assertEqual(names.count("BlockStep"), 3)
|
||||
# Same launch order and buffer roles as three step(1) calls.
|
||||
strip = lambda log: [(n, tuple(id(x) for x in a)) for n, a in log if n != "sync"]
|
||||
roles = lambda run, log: [(n, tuple({id(run._current): "c", id(run._spare): "s"}.get(x, "-")
|
||||
for x in ids)) for n, ids in strip(log)]
|
||||
self.assertEqual([n for n, _ in strip(log_many)], [n for n, _ in strip(log_single)])
|
||||
self.assertEqual(roles(many, log_many), roles(single, log_single))
|
||||
self.assertIsNot(many._current, many._spare)
|
||||
self.assertEqual((many.step_count, single.step_count), (3, 3))
|
||||
|
||||
def test_step_zero_launches_and_waits_for_nothing(self):
|
||||
run, log = backend()
|
||||
run.step(0)
|
||||
self.assertEqual((log, run.step_count), ([], 0))
|
||||
|
||||
def test_failure_keeps_entry_step_count(self):
|
||||
run, log = backend()
|
||||
run.step(1)
|
||||
run._stream.synchronize.side_effect = RuntimeError("device fault")
|
||||
with self.assertRaises(RuntimeError):
|
||||
run.step(2)
|
||||
self.assertEqual((run.step_count, run._status), (1, "failed"))
|
||||
|
||||
def test_launch_failure_mid_batch_fails_closed_without_waiting(self):
|
||||
run, log = backend()
|
||||
calls = []
|
||||
step = run._step
|
||||
|
||||
def failing(*args, **kwargs):
|
||||
calls.append(1)
|
||||
if len(calls) == 2:
|
||||
raise RuntimeError("launch failure")
|
||||
return step(*args, **kwargs)
|
||||
|
||||
run._step = failing
|
||||
with self.assertRaises(RuntimeError):
|
||||
run.step(3)
|
||||
self.assertNotIn("sync", [name for name, _ in log])
|
||||
self.assertEqual((run.step_count, run._status), (0, "failed"))
|
||||
with self.assertRaises(RuntimeError):
|
||||
run.download()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -83,6 +83,32 @@ assert abs(sum(compute_feq_d2q9(1.,0.,0.)) - 1.) < 1e-6
|
||||
|
||||
|
||||
class BuildContracts(unittest.TestCase):
|
||||
def test_block_root_modes_are_rendered_and_recorded(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
for mode in (0, 1, 2):
|
||||
with self.subTest(mode=mode):
|
||||
build = compiler.generate_config(LBMConfig(), build_root=root, block_root_bc=mode)
|
||||
try:
|
||||
method = Path(build, "config/config_method.h").read_text()
|
||||
self.assertIn(f"#define BLOCK_ROOT_BC {mode}\n", method)
|
||||
metadata = json.loads(Path(build, ".celeris-build").read_text())
|
||||
self.assertEqual(metadata["block_root_bc"], mode)
|
||||
self.assertNotIn("block_root_bc", metadata["config"])
|
||||
finally:
|
||||
compiler.cleanup_build_dir(build)
|
||||
build = compiler.generate_config(LBMConfig(), build_root=root)
|
||||
try:
|
||||
self.assertEqual(json.loads(Path(build, ".celeris-build").read_text())["block_root_bc"], 0)
|
||||
finally:
|
||||
compiler.cleanup_build_dir(build)
|
||||
|
||||
def test_invalid_block_root_mode_fails_before_snapshot(self):
|
||||
for mode in (-1, 3, True, False, 1.0, "1", None):
|
||||
with self.subTest(mode=mode), patch.object(compiler.tempfile, "mkdtemp") as allocate:
|
||||
with self.assertRaisesRegex(ValueError, "block_root_bc"):
|
||||
compiler.generate_config(LBMConfig(), block_root_bc=mode)
|
||||
allocate.assert_not_called()
|
||||
|
||||
@staticmethod
|
||||
def fake_compile(command, check):
|
||||
source = Path(command[-3])
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"""CPU-only traceback capture checks; no CUDA or GPU test import."""
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import numpy as np
|
||||
|
||||
PATH = Path(__file__).resolve().parents[1] / 'validation/multigrid/channel_parity_diagnostic/discover.py'
|
||||
spec = importlib.util.spec_from_file_location('channel_discovery', PATH)
|
||||
discovery = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(discovery)
|
||||
|
||||
|
||||
def test_captures_exact_original_frame_and_arrays():
|
||||
def test_uniform_matched_root():
|
||||
cfg = SimpleNamespace(velocity=.025)
|
||||
shifted, profile, clamp, damping, block = True, 'parabolic', False, .8, (4, 1)
|
||||
expected = np.array([.001], np.float32)
|
||||
actual = np.nextafter(expected, np.float32(1))
|
||||
np.testing.assert_array_equal(actual, expected)
|
||||
try:
|
||||
test_uniform_matched_root()
|
||||
except AssertionError as exc:
|
||||
config, expected, actual = discovery.capture_case(exc)
|
||||
assert config['shifted'] == 1 and config['profile'] == 'parabolic'
|
||||
assert config['clamp'] == 0 and config['damping'] == .8
|
||||
assert config['block'] == [4, 1] and config['steps'] == 8
|
||||
np.testing.assert_array_equal(actual, np.nextafter(expected, np.float32(1)))
|
||||
assert expected.flags.owndata and actual.flags.owndata
|
||||
|
||||
|
||||
def test_other_assertion_does_not_guess_case():
|
||||
def test_uniform_matched_root():
|
||||
cfg = SimpleNamespace(velocity=.025)
|
||||
shifted, profile, clamp, damping, block = False, 'uniform', True, .5, (20, 24)
|
||||
expected = actual = np.array([np.nan], np.float32)
|
||||
assert np.isfinite(expected).all()
|
||||
try:
|
||||
test_uniform_matched_root()
|
||||
except AssertionError as exc:
|
||||
assert discovery.capture_case(exc) is None
|
||||
|
||||
|
||||
def test_unrelated_array_assertion_does_not_guess_case():
|
||||
try:
|
||||
np.testing.assert_array_equal([1], [2])
|
||||
except AssertionError as exc:
|
||||
assert discovery.capture_case(exc) is None
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Root-channel host contract and actual-header CPU parity (no CUDA evidence)."""
|
||||
from dataclasses import replace
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
from CelerisLab.lbm.channel import channel_masks, validate_channel_config, validate_channel_transfers
|
||||
from CelerisLab.lbm.refinement import FixedPatch2D
|
||||
from CelerisLab.lbm.refinement_tables import blocked_base_tables
|
||||
from .test_numerical_operators import compile_run
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shift", [0, 1])
|
||||
@pytest.mark.parametrize("profile", [0, 1])
|
||||
@pytest.mark.parametrize("clamp,damping", [(1, ".5f"), (0, ".8f")])
|
||||
def test_actual_kernel_uniform_parity(tmp_path, shift, profile, clamp, damping):
|
||||
"""Nonuniform ghosts, corners, top wall, global profile and outlet halo donor."""
|
||||
defines = dict(USE_DDF_SHIFTING=shift, INLET_PROFILE=profile,
|
||||
OUTLET_BACKFLOW_CLAMP=clamp, OUTLET_SRT_NEQ_DAMP=damping)
|
||||
uniform = np.array(compile_run(tmp_path, "channel_root.cpp", dict(
|
||||
defines, UNIFORM=1, NX=24, NY=20)), np.float32)
|
||||
assert np.isfinite(uniform).all()
|
||||
for by, bx in ((20, 24), (5, 6), (4, 1)):
|
||||
blocked = np.array(compile_run(tmp_path, "channel_root.cpp", dict(
|
||||
defines, UNIFORM=0, NX=bx+2, NY=by+2)), np.float32)
|
||||
np.testing.assert_array_equal(blocked, uniform)
|
||||
|
||||
|
||||
def transfer(bounds, block=(4, 6)):
|
||||
cfg = LBMConfig(nx=24, ny=20, viscosity=.1)
|
||||
pair = FixedPatch2D.from_config(cfg, bounds)
|
||||
layout = BlockLayout2D.build(20, 24, *block)
|
||||
targets = blocked_base_tables(pair, layout, np.argsort(pair.coalesce_coarse))[1]
|
||||
return pair, layout, targets
|
||||
|
||||
|
||||
def test_masks_and_minimal_footprint():
|
||||
fluid, bulk = channel_masks((20, 24))
|
||||
assert fluid.sum() == 18*22 and bulk.sum() == 16*22
|
||||
assert fluid[1, 1] and not bulk[1, 1]
|
||||
assert bulk[2, 1] and not fluid[0, 0]
|
||||
# No tile alignment or artificial large padding: closest legal x donor is x=1.
|
||||
validate_channel_transfers(*transfer((3, 4, 19, 16)))
|
||||
for bounds in ((2, 4, 19, 16), (3, 3, 19, 16), (3, 4, 22, 16), (3, 4, 19, 17)):
|
||||
with pytest.raises(ValueError, match="bulk"):
|
||||
validate_channel_transfers(*transfer(bounds))
|
||||
|
||||
|
||||
def test_actual_closure_read_conflicts():
|
||||
pair, layout, targets = transfer((6, 6, 18, 14))
|
||||
validate_channel_transfers(pair, layout, targets)
|
||||
for target in (layout.slot(int(layout.owner[5, -1]) - 1, 2), # integer under NumPy 1 and 2
|
||||
layout.slot(layout.owner[1, 4], 4),
|
||||
layout.slot(layout.owner[-2, 4], 3)):
|
||||
with pytest.raises(ValueError, match="immutable"):
|
||||
validate_channel_transfers(pair, layout, np.append(targets, target))
|
||||
|
||||
|
||||
def test_channel_scope():
|
||||
cfg = LBMConfig(nx=24, ny=20, viscosity=.1)
|
||||
validate_channel_config(cfg, [(4, 6), (4, 4)])
|
||||
with pytest.raises(ValueError, match="all-blocked"):
|
||||
validate_channel_config(cfg, [None, (4, 4)])
|
||||
for change in (dict(inlet_scheme="equilibrium"), dict(outlet_mode="zero_gradient"),
|
||||
dict(y_wall_bc="free_slip"), dict(wall_slot_width=2)):
|
||||
with pytest.raises(ValueError, match="channel requires"):
|
||||
validate_channel_config(replace(cfg, **change), [(4, 6), (4, 4)])
|
||||
|
||||
|
||||
def test_evolved_ghost_snapshot_and_physical_download():
|
||||
from contextlib import nullcontext
|
||||
from unittest.mock import Mock
|
||||
from .test_refinement_three_level_multipatch import host_module
|
||||
host = host_module()
|
||||
storage = {}
|
||||
host.compiler.load_module.return_value.get_global.side_effect = lambda name: (name, 12 if name == "d_block_channel" else 20)
|
||||
host.cuda.mem_alloc.side_effect = lambda size: Mock()
|
||||
host.cuda.memcpy_htod.side_effect = lambda ptr, a: storage.__setitem__(id(ptr), np.array(a, copy=True))
|
||||
host.cuda.memcpy_dtoh.side_effect = lambda a, ptr: np.copyto(a, storage[id(ptr)])
|
||||
context = Mock(activate=nullcontext, sm_arch="sm_70")
|
||||
cfg = LBMConfig(nx=24, ny=20, viscosity=.1, velocity=.025)
|
||||
backend = host.FixedPatchStepper(cfg, context=context, coarse_boundary="channel",
|
||||
refinements=[(6, 6, 18, 14)], level_blocks=[(4, 6), (4, 4)])
|
||||
try:
|
||||
weights = np.array([4/9]+[1/9]*4+[1/36]*4)[:, None, None]
|
||||
arrays = [np.broadcast_to(weights, level.shape).copy() for level in backend._levels]
|
||||
arrays[0][:, 0, 0] *= 1.001
|
||||
backend.initialize(*arrays)
|
||||
snapshot = backend._download_raw()
|
||||
assert np.isfinite(snapshot.values[0][:, 0, :]).all()
|
||||
assert np.isnan(backend.download()[0][:, 0, :]).all()
|
||||
backend._initialize_raw(snapshot)
|
||||
for a, b in zip(snapshot.values, backend._download_raw().values):
|
||||
np.testing.assert_array_equal(a, b)
|
||||
arrays[0][0, 0, 0] = np.nan
|
||||
with pytest.raises(ValueError, match="finite"):
|
||||
backend.initialize(*arrays)
|
||||
assert backend._levels[0].kernel == host.compiler.load_module.return_value.get_function("BlockChannelStep")
|
||||
assert [call.kwargs["block_root_bc"] for call in host.compiler.generate_config.call_args_list] == [2, 0]
|
||||
finally:
|
||||
backend.close()
|
||||
@@ -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()
|
||||
@@ -50,11 +50,11 @@ class FourLevels(unittest.TestCase):
|
||||
cases = [([L1, [(10, 20, 30, 44)]], "inherited ACTIVE"),
|
||||
([L1, L2[:1] + [(54, 54, 58, 58)]], "inherited ACTIVE"),
|
||||
([L1, L2, [(40, 40, 60, 60)]], "donors/receivers"),
|
||||
([L1, L2, [(41, 41, 60, 60)]], "donors/receivers"),
|
||||
([L1, L2, [(56, 56, 64, 64), (66, 56, 72, 64)]], "closer than")]
|
||||
([L1, L2, [(41, 41, 60, 60)]], "donors/receivers")]
|
||||
for levels, reason in cases:
|
||||
with self.subTest(levels=levels), self.assertRaisesRegex(ValueError, reason):
|
||||
build_hierarchy(CFG, levels)
|
||||
build_hierarchy(CFG, [L1, L2, [(56, 56, 64, 64), (66, 56, 72, 64)]]) # gap 2 closes
|
||||
build_hierarchy(CFG, [L1, L2, [(42, 42, 60, 60)]]) # minimal legal margin
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
"""CPU-only science, geometry, units, and source tests; no PyCUDA import."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
REPO = ROOT
|
||||
sys.path.insert(0, str(ROOT / "tests/validation"))
|
||||
sys.path.insert(0, str(ROOT / "tests/unit"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
from run_jet_circle_cv import (
|
||||
CX,
|
||||
CY,
|
||||
W,
|
||||
case,
|
||||
state,
|
||||
cv,
|
||||
cv_from_macros,
|
||||
aperture_probe,
|
||||
measured_aperture_flux,
|
||||
PIN,
|
||||
verify,
|
||||
)
|
||||
|
||||
|
||||
def eq(rho, u, v):
|
||||
cu = CX[:, None, None] * u + CY[:, None, None] * v
|
||||
return W[:, None, None] * rho * (1 + 3 * cu + 4.5 * cu**2 - 1.5 * (u * u + v * v))
|
||||
|
||||
|
||||
class Host(unittest.TestCase):
|
||||
def test_public_macros_pressure_advection_and_cropped_frames(self):
|
||||
for h, origin, shape, rect in (
|
||||
(.5, (0., 0.), (48, 64), (5., 4., 25., 20.)),
|
||||
(.5, (.25, .5), (48, 64), (5.25, 4., 25.25, 20.)),
|
||||
(.25, (.375, .75), (96, 128), (5.125, 4., 25.125, 20.)),
|
||||
):
|
||||
y, x = np.indices(shape)
|
||||
x = origin[0] + h * (x + .5)
|
||||
k, u, v = 1e-5, .02, -.01
|
||||
rho = 1 + 3 * k * x # p=(rho-1)/3=k*x
|
||||
ux, uy, fluid = np.full(shape, u), np.full(shape, v), np.ones(shape, bool)
|
||||
result = cv_from_macros(rho, ux, uy, fluid, origin, rect, .004, h)
|
||||
expected = 320 * np.array([3*k*u, k + 3*k*u*u, 3*k*u*v])
|
||||
np.testing.assert_allclose(result[1], expected, rtol=1e-11, atol=1e-13)
|
||||
np.testing.assert_allclose(result, cv(eq(rho, u, v), fluid, origin, rect, .004, h), atol=1e-12)
|
||||
x0, y0, x1, y1 = np.rint((np.array(rect)-np.tile(origin, 2))/h).astype(int)
|
||||
selection = np.s_[y0-2:y1+2, x0-2:x1+2]
|
||||
cropped_origin = np.asarray(origin) + h*np.array([x0-2, y0-2])
|
||||
cropped = cv_from_macros(rho[selection], ux[selection], uy[selection],
|
||||
fluid[selection], cropped_origin, rect, .004, h)
|
||||
np.testing.assert_array_equal(cropped, result)
|
||||
|
||||
def test_public_macros_full_normal_viscous_stress(self):
|
||||
y, x = np.indices((48, 64))
|
||||
x = (x + .5) * .5
|
||||
rho, ux, uy, fluid = np.ones_like(x), 1e-4*x*x, np.zeros_like(x), np.ones(x.shape, bool)
|
||||
args = (rho, ux, uy, fluid, (0., 0.), (5., 4., 25., 20.))
|
||||
_, viscous = cv_from_macros(*args, .004)
|
||||
_, inviscid = cv_from_macros(*args, 0.)
|
||||
np.testing.assert_allclose(viscous-inviscid, [0, -4*.004*1e-4*320, 0], atol=1e-13)
|
||||
with self.assertRaisesRegex(ValueError, "matching 2D"):
|
||||
cv_from_macros(rho, ux, uy[:-1], fluid, (0, 0), (5, 4, 25, 20), .004)
|
||||
|
||||
def test_descriptor_and_uniform_closed_flux(self):
|
||||
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
||||
|
||||
np.testing.assert_array_equal(CX, D2Q9_EX)
|
||||
np.testing.assert_array_equal(CY, D2Q9_EY)
|
||||
rho = np.full((48, 64), 1.01)
|
||||
g = eq(rho, 0.023, -0.017)
|
||||
inventory, flux = cv(g, np.ones((48, 64), bool), (0, 0), (5, 4, 25, 20), 0.0072)
|
||||
np.testing.assert_allclose(
|
||||
inventory, 320 * 1.01 * np.array([1, 0.023, -0.017]), atol=1e-12
|
||||
)
|
||||
np.testing.assert_allclose(flux, 0, atol=1e-14)
|
||||
|
||||
def test_affine_velocity_independent_divergence(self):
|
||||
y, x = np.mgrid[:48, :64]
|
||||
x = (x + 0.5) * 0.5
|
||||
y = (y + 0.5) * 0.5
|
||||
a = 0.001
|
||||
g = eq(np.ones_like(x), a * x, -a * y)
|
||||
_, flux = cv(g, np.ones(x.shape, bool), (0, 0), (5, 4, 25, 20), 0.0072)
|
||||
np.testing.assert_allclose(
|
||||
flux, [0, a * a * 320 * 15, a * a * 320 * 12], atol=1e-13
|
||||
)
|
||||
|
||||
def test_quadratic_viscous_flux_sign_and_units(self):
|
||||
y, x = np.mgrid[:48, :64]
|
||||
y = (y + 0.5) * 0.5
|
||||
a = 0.0001
|
||||
nu = 0.0072
|
||||
g = eq(np.ones_like(y), a * y * y, np.zeros_like(y))
|
||||
_, flux = cv(g, np.ones(y.shape, bool), (0, 0), (5, 4, 25, 20), nu)
|
||||
np.testing.assert_allclose(flux, [0, -2 * nu * a * 320, 0], atol=2e-14)
|
||||
|
||||
def test_geometry_adapter_match_and_apertures(self):
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||
from CelerisLab.body.objects import SimObject
|
||||
from CelerisLab.body.jets import Jet, Sector, Radial, jet_link_shapes
|
||||
from CelerisLab.lbm.refinement import FixedPatch2D, prepare_fine_boundary
|
||||
from CelerisLab.lbm.descriptors import SOLID
|
||||
|
||||
c = case()
|
||||
center = np.array(c["center"])
|
||||
r = c["diameter"] / 2
|
||||
cfg = LBMConfig(nx=c["nx"], ny=c["ny"], viscosity=c["nu"])
|
||||
cfg.validate()
|
||||
patch = FixedPatch2D.from_config(cfg, c["bounds"])
|
||||
obj = SimObject(0, CircleGeometry(*center, r), center=tuple(center), radius=r)
|
||||
jets = [
|
||||
Jet(
|
||||
Sector(tuple(center), a, np.pi / 12, r + 0.1),
|
||||
Radial(tuple(center), inward=False),
|
||||
"uniform",
|
||||
s * c["jet_speed"],
|
||||
)
|
||||
for a, s in [(np.pi / 2, 1), (3 * np.pi / 2, -1)]
|
||||
]
|
||||
host = prepare_fine_boundary(cfg, patch, [obj], jets=jets)
|
||||
shape, ids = jet_link_shapes(jets, host.curved_hits)
|
||||
self.assertTrue((ids == 0).any() and (ids == 1).any() and (ids < 0).any())
|
||||
self.assertGreater(host.curved_host[2].min(), 0.010001)
|
||||
self.assertFalse(np.any(host.curved_host[-1]))
|
||||
links = CircleGeometry(*(2 * center - 0.5), 2 * r).build_cut_links(
|
||||
2 * c["nx"], 2 * c["ny"]
|
||||
)
|
||||
np.testing.assert_array_equal([v.dir for v in links], host.curved_host[1])
|
||||
np.testing.assert_allclose(
|
||||
[v.q for v in links], host.curved_host[2], atol=2e-7, rtol=0
|
||||
)
|
||||
np.testing.assert_allclose(
|
||||
np.array([[v.hit_x, v.hit_y] for v in links]) * 0.5 + 0.25,
|
||||
host.curved_hits,
|
||||
atol=2e-12,
|
||||
)
|
||||
fluid = (host.flags.reshape(patch.fine_shape) & SOLID) == 0
|
||||
g = state((9, *patch.fine_shape), c["u"])
|
||||
g[:, ~fluid] = np.nan
|
||||
for contour in c["contours"]:
|
||||
cv(g, fluid, patch.fine_origin, contour, c["nu"])
|
||||
probe = aperture_probe(fluid, patch.fine_origin, center, r)
|
||||
measured = measured_aperture_flux(g, probe)
|
||||
np.testing.assert_allclose(
|
||||
measured, 0, atol=1e-14
|
||||
) # x-directed flow has zero flux on symmetric top/bottom arcs
|
||||
print(
|
||||
"GEOMETRY",
|
||||
dict(
|
||||
links=len(links),
|
||||
aperture_counts=[int((ids == i).sum()) for i in [0, 1]],
|
||||
q_min=float(host.curved_host[2].min()),
|
||||
q_max=float(host.curved_host[2].max()),
|
||||
fallbacks=int(np.count_nonzero(host.curved_host[-1])),
|
||||
sample_distances=[p[-1] for p in probe],
|
||||
),
|
||||
)
|
||||
|
||||
def test_diffusive_scaling(self):
|
||||
a, b = case(1), case(2)
|
||||
self.assertEqual(a["re_d"], b["re_d"])
|
||||
self.assertEqual(a["nu"], b["nu"])
|
||||
self.assertEqual(a["final_t_star"], b["final_t_star"])
|
||||
self.assertEqual(a["steps"] * 4, b["steps"])
|
||||
|
||||
def test_source_pin(self):
|
||||
from tempfile import TemporaryDirectory
|
||||
from historical_tree import historical_tree
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "Source hash mismatch"):
|
||||
verify(REPO)
|
||||
with TemporaryDirectory() as directory:
|
||||
verify(historical_tree(PIN, directory))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Jet boundary-condition hook: velocity implemented, pressure reserved (host only, no CUDA)."""
|
||||
import pytest
|
||||
|
||||
from CelerisLab.body.jets import Jet, Parallel, Tube
|
||||
|
||||
|
||||
def region():
|
||||
return Tube((16., 12.), 0., 3., .7)
|
||||
|
||||
|
||||
def test_default_is_velocity_and_survives_amplitude_change():
|
||||
jet = Jet(region(), Parallel(0.), amplitude=.01)
|
||||
assert jet.condition == "velocity"
|
||||
assert jet.with_amplitude(-.02).condition == "velocity"
|
||||
# positional calls keep their meaning; condition is the new last field
|
||||
assert (Jet(region(), Parallel(0.), "parabolic", .01, "velocity")
|
||||
== Jet(region(), Parallel(0.), "parabolic", .01))
|
||||
|
||||
|
||||
def test_pressure_is_rejected_before_amplitude_validation():
|
||||
# An amplitude that the velocity check would reject must not decide the error.
|
||||
for amplitude in (0., 5., float("nan")):
|
||||
with pytest.raises(NotImplementedError, match="pressure jets are reserved"):
|
||||
Jet(region(), Parallel(0.), amplitude=amplitude, condition="pressure")
|
||||
with pytest.raises(NotImplementedError):
|
||||
Jet(region(), None, profile="no-such-profile", condition="pressure")
|
||||
|
||||
|
||||
def test_unknown_condition_is_a_value_error():
|
||||
for condition in ("Velocity", "flow_rate", "", None):
|
||||
with pytest.raises(ValueError, match="condition"):
|
||||
Jet(region(), Parallel(0.), amplitude=.01, condition=condition)
|
||||
@@ -0,0 +1,182 @@
|
||||
"""Manufactured references for continuation instrumentation, no CUDA."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
||||
from jet_cv_measurements import (
|
||||
sample_steps,
|
||||
factors,
|
||||
ray_geometry,
|
||||
reconstruct_aperture,
|
||||
contour_terms,
|
||||
LINEAR,
|
||||
QUADRATIC,
|
||||
OFFSETS,
|
||||
MeasurementRecorder,
|
||||
)
|
||||
from run_jet_circle_cv import (
|
||||
case,
|
||||
CX,
|
||||
CY,
|
||||
W,
|
||||
cv,
|
||||
aperture_probe,
|
||||
measured_aperture_flux,
|
||||
)
|
||||
from analyze_jet_cv_continuation import integrate, analyze
|
||||
|
||||
|
||||
class Measurements(unittest.TestCase):
|
||||
def test_common_physical_schedule(self):
|
||||
a = sample_steps(case(1, steps=3200))
|
||||
b = sample_steps(case(2, steps=3200))
|
||||
np.testing.assert_array_equal(a, b / 4)
|
||||
self.assertEqual(len(a), 876)
|
||||
self.assertEqual(a[-1], 3200)
|
||||
self.assertTrue(all(t in a for t in range(0, 3201, 100)))
|
||||
self.assertEqual(len(a[a <= 100]), 101)
|
||||
|
||||
def test_reference_factors(self):
|
||||
f = factors(2)
|
||||
self.assertEqual(f["mass"], 0.25)
|
||||
self.assertEqual(f["impulse"], 0.5)
|
||||
self.assertEqual(f["force"], 2)
|
||||
self.assertEqual(f["mass_rate"], 1)
|
||||
|
||||
def test_extrapolation_polynomials(self):
|
||||
self.assertAlmostEqual(LINEAR @ (2 + 3 * OFFSETS), 2)
|
||||
self.assertAlmostEqual(QUADRATIC @ (2 + 3 * OFFSETS + 4 * OFFSETS**2), 2)
|
||||
|
||||
def test_independent_linear_field_volume_and_mass(self):
|
||||
h = 0.5
|
||||
y, x = np.mgrid[:64, :64]
|
||||
x = (x + 0.5) * h
|
||||
y = (y + 0.5) * h
|
||||
rho = 1 + 0.001 * x
|
||||
vel = np.array([0.01 + 0.0001 * x, -0.02 + 0.0002 * y])
|
||||
center = np.array([16.0, 16.0])
|
||||
r = 5
|
||||
fluid = (x - center[0]) ** 2 + (y - center[1]) ** 2 > r * r
|
||||
geo = ray_geometry(fluid, (0.0, 0.0), center, r)
|
||||
ray, est = reconstruct_aperture(rho, vel, geo)
|
||||
points = center + r * geo["normal"]
|
||||
rr = 1 + 0.001 * points[..., 0]
|
||||
uv = np.stack(
|
||||
(0.01 + 0.0001 * points[..., 0], -0.02 + 0.0002 * points[..., 1]), axis=-1
|
||||
)
|
||||
un = np.sum(uv * geo["normal"], axis=-1)
|
||||
exact = np.stack(
|
||||
(np.sum(un * geo["ds"], axis=1), np.sum(rr * un * geo["ds"], axis=1)),
|
||||
axis=-1,
|
||||
)
|
||||
np.testing.assert_allclose(est, np.broadcast_to(exact, est.shape), atol=1e-15)
|
||||
self.assertGreater(np.max(abs(est[:, :, 0] - est[:, :, 1])), 1e-4)
|
||||
|
||||
def test_split_contour_stress(self):
|
||||
y, x = np.mgrid[:48, :64]
|
||||
y = (y + 0.5) * 0.5
|
||||
rho = np.ones_like(y)
|
||||
v = np.array([1e-4 * y * y, np.zeros_like(y)])
|
||||
terms = contour_terms(rho, v, (0, 0), (5, 4, 25, 20), 0.0072)
|
||||
np.testing.assert_allclose(terms[:2], 0, atol=1e-15)
|
||||
np.testing.assert_allclose(terms[2], [-2 * 0.0072 * 1e-4 * 320, 0], atol=1e-15)
|
||||
|
||||
def test_recorder_snapshot_and_reconstructible_arrays(self):
|
||||
h = 0.5
|
||||
y, x = np.mgrid[:64, :64]
|
||||
x = (x + 0.5) * h
|
||||
y = (y + 0.5) * h
|
||||
fluid = (x - 16) ** 2 + (y - 16) ** 2 > 25
|
||||
fine_leaf = fluid.copy()
|
||||
fine_leaf[:, :4] = False
|
||||
root_leaf = np.zeros((32, 32), bool)
|
||||
root_leaf[:, :2] = True
|
||||
|
||||
def equilibrium(ny, nx):
|
||||
cu = CX[:, None, None] * 0.01
|
||||
return np.broadcast_to(
|
||||
W[:, None, None] * (1 + 3 * cu + 4.5 * cu * cu - 1.5 * 0.01**2),
|
||||
(9, ny, nx),
|
||||
).copy()
|
||||
|
||||
ug = equilibrium(64, 64)
|
||||
levels = [equilibrium(32, 32), ug.copy()]
|
||||
c = dict(
|
||||
scale=1,
|
||||
center=[16, 16],
|
||||
diameter=10,
|
||||
u=0.01,
|
||||
nu=0.0072,
|
||||
contours=[[9, 9, 23, 23], [7, 7, 25, 25]],
|
||||
)
|
||||
data = {
|
||||
name: [cv(ug, mask, (0, 0), r, c["nu"]) for r in c["contours"]]
|
||||
for name, mask in [("uniform_adapter", fluid), ("amr_api", fine_leaf)]
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
rec = MeasurementRecorder(
|
||||
c, tmp, fluid, [root_leaf, fine_leaf], (0, 0), CX, CY
|
||||
)
|
||||
rec.capture(0, ug, levels, data, {k: np.zeros(2) for k in data}, 0.0)
|
||||
rec.records[-1]["raw_cumulative"] = np.zeros((2, 5, 4))
|
||||
rec.flush()
|
||||
a = np.load(Path(tmp) / "samples_s1.npz")
|
||||
self.assertEqual(a["arc_ray_values"].shape, (1, 2, 3, 2, 32, 3))
|
||||
np.testing.assert_allclose(
|
||||
a["global_mass_reference"][0, 0], a["global_mass_reference"][0, 1]
|
||||
)
|
||||
snap = np.load(Path(tmp) / "fields_s1_tref0.npz")
|
||||
np.testing.assert_allclose(
|
||||
snap["uniform_velocity_reference"][0][fluid], 0.01, rtol=1e-7
|
||||
)
|
||||
|
||||
def test_offline_mass_force_normalization(self):
|
||||
import json
|
||||
|
||||
c = case(2, steps=3200)
|
||||
s = 2
|
||||
t = np.arange(0, 3201, 100)
|
||||
n = len(t)
|
||||
inv = np.zeros((n, 2, 2, 3))
|
||||
inv[:, :, :, 0] = 100 * s * s
|
||||
inv[:, :, :, 1] = -t[:, None, None] * 0.002 * s
|
||||
raw = np.zeros((n, 2, 5, 4))
|
||||
raw[:, :, 2, 0] = t[:, None] * 0.002 * s
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
(path / "samples_s2.json").write_text(
|
||||
json.dumps(dict(case=c, sample_file="samples_s2.npz"))
|
||||
)
|
||||
np.savez(
|
||||
path / "samples_s2.npz",
|
||||
t_reference=t,
|
||||
cv_inventory=inv,
|
||||
cv_flux=np.zeros_like(inv),
|
||||
raw_cumulative=raw,
|
||||
arc_estimates=np.zeros((n, 2, 2, 2, 2)),
|
||||
global_mass_reference=np.ones((n, 2)) * 100,
|
||||
level_stats=np.broadcast_to([1, 1, 0, 0.03, 100], (n, 3, 5)),
|
||||
velocity_error=np.zeros(n),
|
||||
)
|
||||
result = analyze(path)
|
||||
self.assertEqual(len(result["windows"]), 32)
|
||||
np.testing.assert_allclose(
|
||||
result["windows"][0]["normalized_cv_residual"], 0, atol=1e-14
|
||||
)
|
||||
f0 = 0.5 * 0.03**2 * 24.084
|
||||
np.testing.assert_allclose(
|
||||
result["windows"][0]["raw_reaction_coefficient"], [[0.002 / f0, 0]] * 2
|
||||
)
|
||||
|
||||
def test_nonuniform_trapezoidal_time_units(self):
|
||||
t = np.array([0.0, 1.0, 2.0, 4.0, 8.0])
|
||||
y = np.stack((3 * t + 2, 2 * t - 7), axis=-1)
|
||||
np.testing.assert_allclose(integrate(t, y), [112, 8], atol=1e-15)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -188,6 +188,55 @@ def jet_case(device, jets):
|
||||
return field, bodies
|
||||
|
||||
|
||||
def test_public_surface_exchange_for_jet_preserves_aperture_at_zero_and_reverse(device):
|
||||
from CelerisLab.simulation import Simulation
|
||||
jet = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER, inward=False), "quartic", 2e-3)
|
||||
field, bodies = jet_case(device, [jet])
|
||||
sim = SimpleNamespace(bodies=bodies, field=field, _initialized=True,
|
||||
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||
cl = field.curved
|
||||
assert bodies.get(0).surface_actuation is None
|
||||
aperture, velocity = cl.aperture, cl.surface_velocity.copy()
|
||||
assert aperture.any() and (~aperture).any()
|
||||
# Synthetic per-link exchanges test readback/splitting, not kernel physics.
|
||||
values = np.arange(cl.count * 3, dtype=np.float32).reshape(-1, 3)
|
||||
device.htod(cl.link_exchange_gpu, values)
|
||||
for amplitude in (2e-3, 0., -1e-3):
|
||||
Simulation.set_jet_amplitude(sim, 0, amplitude)
|
||||
result = Simulation.read_surface_exchange(sim, 0)
|
||||
assert cl.aperture is aperture
|
||||
np.testing.assert_array_equal(aperture, cl.jet_id >= 0)
|
||||
np.testing.assert_allclose(cl.surface_velocity, velocity * (amplitude / 2e-3))
|
||||
for name, mask in (("wall", ~aperture), ("aperture", aperture)):
|
||||
np.testing.assert_array_equal(result[name], values[mask].sum(axis=0, dtype=np.float64))
|
||||
|
||||
|
||||
def test_public_surface_exchange_keeps_legacy_and_passive_guards(device):
|
||||
from CelerisLab.simulation import Simulation
|
||||
field = make_field(device)
|
||||
bodies = manager(field, [circle(SurfaceActuation(0., .6, .002), radius=2.)])
|
||||
bodies.sync_to_gpu(field)
|
||||
sim = SimpleNamespace(bodies=bodies, field=field,
|
||||
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||
for value in Simulation.read_surface_exchange(sim, 0).values():
|
||||
np.testing.assert_array_equal(value, 0.)
|
||||
field.curved.free_gpu_columns()
|
||||
with pytest.raises(RuntimeError, match="diagnostic is allocated"):
|
||||
Simulation.read_surface_exchange(sim, 0)
|
||||
|
||||
# An exchange buffer elsewhere in the scene must not admit a passive body.
|
||||
for jets in ([], [Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER))]):
|
||||
field, bodies = jet_case(device, jets)
|
||||
passive = bodies.add(circle(center=(2., 3.), radius=.6))
|
||||
bodies.sync_to_gpu(field)
|
||||
sim.bodies, sim.field = bodies, field
|
||||
with pytest.raises(ValueError, match="Body has no surface actuation"):
|
||||
Simulation.read_surface_exchange(sim, passive)
|
||||
if not jets:
|
||||
with pytest.raises(ValueError, match="Body has no surface actuation"):
|
||||
Simulation.read_surface_exchange(sim, 0)
|
||||
|
||||
|
||||
def test_manager_passes_true_hits_and_reuploads_only_velocity(device):
|
||||
# CPU memory double only: checks host/device bookkeeping, not GPU numerics.
|
||||
jet = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER, inward=False), "quartic", 2e-3)
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
||||
import json
|
||||
import unittest
|
||||
import numpy as np
|
||||
from les_flow_oracle import H, U, NU, TIMES, solve, relative, amplitude
|
||||
|
||||
|
||||
class OracleTests(unittest.TestCase):
|
||||
def test_linear_exact(self):
|
||||
y, states = solve(0, n=256)
|
||||
for t, u in states.items():
|
||||
exact = U * np.sin(np.pi * y / H) * np.exp(-NU * (np.pi / H) ** 2 * t)
|
||||
self.assertLess(relative(u, exact), 1e-5)
|
||||
|
||||
def test_nonlinear_refinement_symmetry_and_dissipation(self):
|
||||
y, states = solve(1, n=256)
|
||||
yc, coarse = solve(1, n=128)
|
||||
energy = U**2 / 2
|
||||
for t, u in states.items():
|
||||
self.assertTrue(np.all(np.isfinite(u)))
|
||||
self.assertGreaterEqual(float(u.min()), 0)
|
||||
self.assertLessEqual(float(u.max()), U)
|
||||
np.testing.assert_allclose(u, u[::-1], atol=1e-14)
|
||||
self.assertLess(float(np.mean(u * u)), energy)
|
||||
energy = float(np.mean(u * u))
|
||||
self.assertLess(relative(np.interp(yc, y, u), coarse[t]), 8e-5)
|
||||
|
||||
def test_filter_is_independent_of_oracle_grid(self):
|
||||
y, a = solve(1, n=128, times=(TIMES[-1],))
|
||||
_, b = solve(0.5, n=128, times=(TIMES[-1],), delta=2)
|
||||
np.testing.assert_array_equal(a[TIMES[-1]], b[TIMES[-1]])
|
||||
|
||||
def test_fixed_grid_timestep_halving(self):
|
||||
errors = []
|
||||
for cs in (0.0, 0.16, 1.0):
|
||||
_, full = solve(cs, n=256, cfl=0.4)
|
||||
_, half = solve(cs, n=256, cfl=0.2)
|
||||
errors.extend(relative(full[t], half[t]) for t in TIMES)
|
||||
self.assertLess(max(errors), 1e-8)
|
||||
print(
|
||||
json.dumps({"fixed_grid_timestep_half_max_relative": max(errors)}),
|
||||
flush=True,
|
||||
)
|
||||
|
||||
def test_signal_and_wrong_convention_separate(self):
|
||||
y, std = solve(1, n=256, times=(TIMES[-1],))
|
||||
_, wrong = solve(2 ** (-0.25), n=256, times=(TIMES[-1],))
|
||||
t = TIMES[-1]
|
||||
base = U * np.exp(-NU * (np.pi / H) ** 2 * t)
|
||||
signal = (base - amplitude(y, std[t])) / base
|
||||
separation = relative(wrong[t], std[t])
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"final_time": t,
|
||||
"standard_sgs_amplitude_signal": signal,
|
||||
"wrong_convention_profile_separation": separation,
|
||||
}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
self.assertGreater(signal, 0.04)
|
||||
self.assertGreater(separation, 0.01)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Host-only entrypoint/source-guard checks; never imports PyCUDA."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
import unittest
|
||||
import numpy as np
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
||||
from run_uniform_les_flow import source_guard, decode_macros, HERE, HEADER
|
||||
|
||||
ROOT = HERE.parents[1]
|
||||
sys.path.insert(0, str(ROOT / "tests/unit"))
|
||||
from historical_tree import historical_tree, tracked
|
||||
|
||||
A135 = "a135252ae78b47b57d7e77d19001700b3bedac8b"
|
||||
|
||||
|
||||
class GuardTests(unittest.TestCase):
|
||||
def test_decoded_density_and_velocity_guards(self):
|
||||
w = np.array([4 / 9] + [1 / 9] * 4 + [1 / 36] * 4)
|
||||
cx = np.array([0, 1, -1, 0, 0, 1, -1, -1, 1])
|
||||
cy = np.array([0, 0, 0, 1, -1, 1, -1, 1, -1])
|
||||
data = np.zeros((9, 4, 2))
|
||||
profile, mass, vy, spread = decode_macros(data, w, cx, cy)
|
||||
np.testing.assert_allclose(profile, 0, atol=1e-16)
|
||||
self.assertAlmostEqual(mass, 1)
|
||||
for scale in (1, 2):
|
||||
invalid = np.broadcast_to(-scale * w[:, None, None], data.shape).copy()
|
||||
with self.assertRaisesRegex(RuntimeError, "density"):
|
||||
decode_macros(invalid, w, cx, cy)
|
||||
invalid = data.copy()
|
||||
invalid[0, 1, 0] = np.nan
|
||||
with self.assertRaisesRegex(RuntimeError, "populations"):
|
||||
decode_macros(invalid, w, cx, cy)
|
||||
# Finite density but overflowing moment must not produce an accepted NaN/Inf norm.
|
||||
invalid = data.copy()
|
||||
invalid[1, :, :] = 1e308
|
||||
invalid[2, :, :] = -1e308
|
||||
with self.assertRaisesRegex(RuntimeError, "velocity/profile"):
|
||||
decode_macros(invalid, w, cx, cy)
|
||||
|
||||
def test_current_corrected_pins(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
root = historical_tree(A135, directory)
|
||||
with patch("run_uniform_les_flow.subprocess.check_output", return_value="historical-tree\n"):
|
||||
identity = source_guard(root, "corrected", "a135252")
|
||||
# base_commit comes from the pins file; the mock only feeds checkout_commit.
|
||||
self.assertEqual(identity["base_commit"], A135)
|
||||
self.assertEqual(identity["checkout_commit"], "historical-tree")
|
||||
|
||||
def test_corrected_and_tamper_guard(self):
|
||||
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
|
||||
with TemporaryDirectory() as directory:
|
||||
origin = historical_tree(A135, Path(directory) / "origin")
|
||||
root = Path(directory) / "fixture"
|
||||
for name in pins["sources"]:
|
||||
dest = root / name
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(origin / name, dest)
|
||||
with patch(
|
||||
"run_uniform_les_flow.subprocess.check_output",
|
||||
return_value="isolated-fixture\n",
|
||||
):
|
||||
source_guard(root, "corrected", "a135252")
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "baseline", "a135252")
|
||||
for name in ("kernels/step/one_step_double.cu", "kernels/blocks.cu",
|
||||
"refinement_stepper.py", "refinement_tables.py"):
|
||||
target = root / "src/CelerisLab/lbm" / name
|
||||
original = target.read_bytes()
|
||||
target.write_bytes(original + b"\n// tamper\n")
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "corrected", "a135252")
|
||||
target.write_bytes(original)
|
||||
|
||||
def test_historical_default_and_manifest_unchanged(self):
|
||||
historical = {
|
||||
"historical": ("source_pins.json", "87ca033619da1fc02bee822bfc5681b518a92a155c3c3d1dbbe5265c96066b39"),
|
||||
"dev8403956": ("source_pins_dev8403956.json", "58187908485f2af7c78677d53de98015878372444265ce9e667683b8ef5d410f"),
|
||||
}
|
||||
for experiment, (name, expected) in historical.items():
|
||||
manifest = HERE / "data/les_flow" / name
|
||||
self.assertEqual(hashlib.sha256(manifest.read_bytes()).hexdigest(), expected)
|
||||
for variant in ("baseline", "corrected"):
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(ROOT, variant, experiment)
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(ROOT, "corrected") # Omission still uses the historical profile.
|
||||
|
||||
def test_current_manifest_pins_actual_corrected_tree(self):
|
||||
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
|
||||
self.assertEqual(pins["base_commit"], A135)
|
||||
self.assertEqual(set(pins["sources"]), tracked(A135))
|
||||
with TemporaryDirectory() as directory:
|
||||
root = historical_tree(A135, directory)
|
||||
for name, expected in pins["sources"].items():
|
||||
self.assertEqual(hashlib.sha256((root / name).read_bytes()).hexdigest(), expected)
|
||||
self.assertEqual(pins["sources"][HEADER], pins["candidate_header_sha256"])
|
||||
self.assertNotEqual(pins["sources"][HEADER], pins["baseline_header_sha256"])
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "baseline", "a135252")
|
||||
|
||||
def test_named_baseline_accepts_only_reversed_header(self):
|
||||
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
|
||||
with TemporaryDirectory() as directory:
|
||||
origin = historical_tree(A135, Path(directory) / "origin")
|
||||
root = Path(directory) / "fixture"
|
||||
for name in pins["sources"]:
|
||||
dest = root / name
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(origin / name, dest)
|
||||
subprocess.run(["git", "apply", "--reverse", str(HERE / "les/data/smagorinsky_normalization.patch")], cwd=root, check=True)
|
||||
with patch("run_uniform_les_flow.subprocess.check_output", return_value="baseline-fixture\n"):
|
||||
source_guard(root, "baseline", "a135252")
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "corrected", "a135252")
|
||||
|
||||
def test_gpu_requires_opt_in_before_import(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(HERE / "run_uniform_les_flow.py"),
|
||||
"--root",
|
||||
str(ROOT),
|
||||
"--variant",
|
||||
"baseline",
|
||||
"--output",
|
||||
str(Path(directory) / "out.json"),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("Explicit --run-gpu is required", result.stderr)
|
||||
self.assertNotIn("pycuda", result.stderr)
|
||||
self.assertFalse((Path(directory) / "out.json").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -81,14 +81,9 @@ class MultiplePinnedRemap(unittest.TestCase):
|
||||
self.assertEqual(old.pinned_rectangles, PINS)
|
||||
self.check_transfer(old, new, fields(old), 2e-13)
|
||||
|
||||
def test_direct_helper_rejects_each_changed_pin_and_body_order(self):
|
||||
def test_direct_helper_rejects_changed_body_order(self):
|
||||
old = mesh(PINS+FLUID, objects())
|
||||
candidates = [mesh(PINS+FLUID, objects()[::-1])]
|
||||
for index in range(2):
|
||||
changed = list(PINS+FLUID)
|
||||
x0, y0, x1, y1 = changed[index]
|
||||
changed[index] = (x0-4, y0, x1, y1)
|
||||
candidates.append(mesh(changed, objects()))
|
||||
for new in candidates:
|
||||
# Invalid donors ensure boundary validation precedes array reads.
|
||||
with self.assertRaisesRegex(ValueError, 'pinned'):
|
||||
@@ -138,6 +133,7 @@ class MultiplePinnedRemap(unittest.TestCase):
|
||||
changed = list(PINS+FLUID)
|
||||
x0, y0, x1, y1 = changed[index]
|
||||
changed[index] = (x0-4, y0, x1, y1)
|
||||
changed[index] = (x0+8, y0, x1, y1) # removes actual body support
|
||||
cases.append((changed, bodies))
|
||||
cases.append((tuple(r for i, r in enumerate(PINS+FLUID) if i != index), bodies))
|
||||
start = len(allocations)
|
||||
|
||||
@@ -83,7 +83,7 @@ class PinnedBodyRemap(unittest.TestCase):
|
||||
run,module,allocations,storage,objects=ready()
|
||||
start=len(allocations)
|
||||
cases=[(B,()),(B,run.child_boundary.objects),(B,(body(center=(12.1,14.)),)),
|
||||
(((20,16,36,40),B[1]),objects),(((16,20,32,40),B[1]),objects),
|
||||
(((24,16,36,40),B[1]),objects),(((16,28,32,40),B[1]),objects),
|
||||
((B[1],),objects)]
|
||||
module.cuda.memcpy_dtoh.reset_mock()
|
||||
for rects,objs in cases:
|
||||
@@ -124,7 +124,9 @@ class PinnedBodyRemap(unittest.TestCase):
|
||||
for a in allocations[len(saved):]: a.free.assert_called_once()
|
||||
run.close()
|
||||
|
||||
def test_direct_helper_rejects_changed_pinned_rectangle(self):
|
||||
def test_direct_helper_allows_changed_rectangle_with_same_boundary(self):
|
||||
old=mesh(A,(body(),))
|
||||
new=mesh(((12,16,32,40),A[1]),(body(),))
|
||||
with self.assertRaisesRegex(ValueError,'pinned'): remap(old,new,fields(old))
|
||||
out = remap(old,new,fields(old))
|
||||
self.assertTrue(same_physical_boundary(old.boundary,new.boundary))
|
||||
self.assertIs(out.topology,new)
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
"""CPU stencil and actual closed-interface packet budgets; no CUDA claims."""
|
||||
import numpy as np
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from dataclasses import replace
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
||||
from CelerisLab.lbm.refinement import FixedPatch2D, FixedPatchSet2D, ACTIVE, OUTER_GHOST
|
||||
from CelerisLab.lbm.refinement_tables import projected_explosion_tables
|
||||
from CelerisLab.lbm.hierarchy import build_hierarchy
|
||||
from tests.unit.test_hierarchy import CFG, FOUR
|
||||
|
||||
E = np.array([D2Q9_EX, D2Q9_EY]).T
|
||||
W = np.array([4/9] + [1/9]*4 + [1/36]*4)
|
||||
|
||||
|
||||
def setup(union=False, bounds=(4, 4, 12, 12)):
|
||||
cfg = LBMConfig(nx=16, ny=16, viscosity=.1)
|
||||
p = (FixedPatchSet2D.from_config(cfg, [bounds]) if union else
|
||||
FixedPatch2D.from_config(cfg, bounds))
|
||||
layout = BlockLayout2D.build(16, 16, 4, 4, periodic=(True, True))
|
||||
neighbors, offsets = projected_explosion_tables(p, layout, np.where(p.covered, OUTER_GHOST, ACTIVE))
|
||||
inverse = np.full(layout.nblocks * np.prod(layout.local_shape), -1, dtype=int)
|
||||
inverse[layout.owner.ravel()] = np.arange(256)
|
||||
return p, inverse[neighbors], offsets
|
||||
|
||||
|
||||
def reconstruct(c, p, neighbors, offsets, project=True):
|
||||
# Independent NumPy evaluation of the documented stencil; neighbors have
|
||||
# been decoded from actual blocked owner IDs, not recomputed geometry.
|
||||
values = c.reshape(9, -1)[:, neighbors]
|
||||
g = .5 * (values[:, :, 1::2] - values[:, :, ::2])
|
||||
if project:
|
||||
g[1:] -= (np.einsum('qra,qa->qr', g[1:], E[1:]) /
|
||||
np.sum(E[1:]**2, axis=1)[:, None])[:, :, None] * E[1:, None, :]
|
||||
out = c.reshape(9, -1)[:, p.ghost_coarse] + np.einsum('qra,ra->qr', g, offsets)
|
||||
return out, g
|
||||
|
||||
|
||||
def check_legal_footprint_mean_and_characteristic(union, bounds):
|
||||
p, neighbors, offsets = setup(union, bounds)
|
||||
assert set(neighbors.ravel()) <= set(p.ghost_coarse)
|
||||
assert not p.covered.ravel()[neighbors].any()
|
||||
assert (neighbors >= 0).all()
|
||||
c = np.random.default_rng(72).normal(size=(9,16,16))
|
||||
c[:, p.covered] = np.nan # poison every forbidden donor
|
||||
out, g = reconstruct(c, p, neighbors, offsets)
|
||||
assert np.isfinite(out).all()
|
||||
np.testing.assert_allclose(np.einsum('qra,qa->qr', g, E), 0, atol=5e-16)
|
||||
for d in np.unique(p.ghost_coarse):
|
||||
rows = p.ghost_coarse == d
|
||||
assert rows.sum() == 4
|
||||
np.testing.assert_allclose(out[:, rows].mean(axis=1), c.reshape(9,-1)[:,d], atol=5e-16)
|
||||
for q in range(1,9):
|
||||
delta = offsets[rows, None, :] - offsets[None, rows, :]
|
||||
# Parallel displacements have zero 2D cross product.
|
||||
parallel = np.isclose(delta[...,0]*E[q,1]-delta[...,1]*E[q,0], 0)
|
||||
diff = out[q, rows][:,None]-out[q, rows][None,:]
|
||||
np.testing.assert_allclose(diff[parallel], 0, atol=5e-16)
|
||||
const = np.broadcast_to(W[:,None,None], (9,16,16)).copy()
|
||||
constant, _ = reconstruct(const,p,neighbors,offsets)
|
||||
np.testing.assert_array_equal(constant, np.broadcast_to(W[:,None], constant.shape))
|
||||
|
||||
|
||||
def collide(f, tau):
|
||||
rho = f.sum(axis=0)
|
||||
u = np.einsum('qa,q...->a...', E, f) / rho
|
||||
cu = np.einsum('qa,a...->q...', E, u)
|
||||
eq = W[:,None] * rho * (1 + 3*cu + 4.5*cu**2 - 1.5*np.sum(u*u,axis=0))
|
||||
return f + (eq-f)/tau
|
||||
|
||||
|
||||
def totals(c, f, p):
|
||||
q = c[:,~p.covered].sum(axis=1)+.25*f[:,p.fine_kind==ACTIVE].sum(axis=1)
|
||||
return np.r_[q.sum(), E.T@q]
|
||||
|
||||
|
||||
def check_full_two_hop_closed_corner_budget(union, collision, bounds):
|
||||
p, neighbors, offsets = setup(union,bounds)
|
||||
rng=np.random.default_rng(17)
|
||||
c=W[:,None,None]*(1+rng.normal(0,.002,(9,16,16)))
|
||||
f=W[:,None,None]*(1+rng.normal(0,.002,(9,*p.fine_shape)))
|
||||
start=totals(c,f,p)
|
||||
# Count exactly the eight coarse diagonal links whose source is also
|
||||
# uncovered, but whose midpoint lies on the closed rectangular interface.
|
||||
q, k=np.divmod(p.coalesce_coarse.astype(np.int64),256)
|
||||
y,x=np.divmod(k,16)
|
||||
grazing=~p.covered[y-E[q,1],x-E[q,0]]
|
||||
assert grazing.sum()==8
|
||||
c[:,p.covered]=np.nan
|
||||
for _ in range(8):
|
||||
f.reshape(9,-1)[:,p.ghost_fine]=reconstruct(c,p,neighbors,offsets)[0]
|
||||
for _ in range(2):
|
||||
f=np.array([np.roll(f[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
|
||||
f[:,p.fine_kind==OUTER_GHOST]=np.nan
|
||||
if collision:
|
||||
f[:,p.fine_kind==ACTIVE]=collide(f[:,p.fine_kind==ACTIVE],p.tau_fine)
|
||||
out=np.array([np.roll(c[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
|
||||
packets=f.reshape(-1)[p.coalesce_fine].mean(axis=1)
|
||||
assert np.isfinite(packets[grazing]).all()
|
||||
out.reshape(-1)[p.coalesce_coarse]=packets
|
||||
if collision:out[:,~p.covered]=collide(out[:,~p.covered],p.tau_coarse)
|
||||
out[:,p.covered]=np.nan
|
||||
c=out
|
||||
np.testing.assert_allclose(totals(c,f,p),start,rtol=0,atol=2e-12)
|
||||
|
||||
|
||||
def check_projection_is_distinguished_by_characteristic_not_global_mass():
|
||||
p,n,o=setup()
|
||||
c=np.random.default_rng(8).normal(size=(9,16,16))
|
||||
_,g=reconstruct(c,p,n,o,project=False)
|
||||
assert np.max(abs(np.einsum('qra,qa->qr',g,E))) > .1
|
||||
|
||||
|
||||
|
||||
|
||||
class ProjectedExplosionTests(unittest.TestCase):
|
||||
def test_stencil(self):
|
||||
for union in (False, True):
|
||||
for bounds in ((4,4,12,12),(2,3,9,10),(2,2,3,3)):
|
||||
with self.subTest(union=union,bounds=bounds):
|
||||
check_legal_footprint_mean_and_characteristic(union,bounds)
|
||||
|
||||
def test_closed_interface(self):
|
||||
for union in (False,True):
|
||||
for collision in (False,True):
|
||||
for bounds in ((4,4,12,12),(2,3,14,12),(2,2,3,3)):
|
||||
with self.subTest(union=union,collision=collision,bounds=bounds):
|
||||
check_full_two_hop_closed_corner_budget(union,collision,bounds)
|
||||
|
||||
def test_projection(self):
|
||||
check_projection_is_distinguished_by_characteristic_not_global_mass()
|
||||
|
||||
def test_affine_and_shift_invariance(self):
|
||||
p,n,o=setup()
|
||||
y,x=np.indices((16,16))
|
||||
c=np.array([.7+q*.01+x*.03+y*.05 for q in range(9)])
|
||||
out,g=reconstruct(c,p,n,o)
|
||||
available=n[:,::2]!=n[:,1::2]
|
||||
gradient=np.array([.03,.05])*available
|
||||
expected=np.broadcast_to(gradient,(9,*gradient.shape)).copy()
|
||||
expected[1:]-=(np.einsum('qra,qa->qr',expected[1:],E[1:])/
|
||||
np.sum(E[1:]**2,axis=1)[:,None])[:,:,None]*E[1:,None,:]
|
||||
np.testing.assert_allclose(g,expected,atol=2e-16)
|
||||
shifted,_=reconstruct(c-W[:,None,None],p,n,o)
|
||||
np.testing.assert_allclose(out-W[:,None],shifted,atol=5e-16)
|
||||
|
||||
|
||||
class ProjectedKernelTests(unittest.TestCase):
|
||||
@unittest.skipUnless(shutil.which("g++"), "g++ is required for actual-kernel CPU arithmetic")
|
||||
def test_actual_kernel_fp32_shifted_and_unshifted(self):
|
||||
p, neighbors, offsets = setup(True)
|
||||
parent = BlockLayout2D.build(16,16,4,4,periodic=(True,True))
|
||||
child = p.fine_layout((8,4)) # deliberately unequal parent/child strides
|
||||
ids, offsets = projected_explosion_tables(p,parent,np.where(p.covered,OUTER_GHOST,ACTIVE))
|
||||
cells = int(np.prod(parent.local_shape))
|
||||
targets = child.owner.ravel()[p.ghost_fine]
|
||||
donors = parent.owner.ravel()[p.ghost_coarse]
|
||||
source = (Path(__file__).resolve().parents[2] /
|
||||
"src/CelerisLab/lbm/kernels/blocks.cu").read_text()
|
||||
kernel = source.split('extern "C" __global__ void BlockExplodeProjected(',1)[1]
|
||||
kernel = 'void BlockExplodeProjected(' + kernel.split('// Targets are receiver-local SOURCE slots',1)[0]
|
||||
def array(name, values, kind):
|
||||
v = np.asarray(values).ravel()
|
||||
text = ','.join(str(int(x)) if kind=='uint64_t' else float(x).hex()+'f' for x in v)
|
||||
return f"{kind} {name}[] = {{{text}}};\n"
|
||||
for shifted in (False,True):
|
||||
c=np.random.default_rng(11).normal(0,.001,(9,16,16))
|
||||
if not shifted:c+=W[:,None,None]
|
||||
c=c.astype(np.float32)
|
||||
packed=np.full(parent.nblocks*9*cells,np.nan,np.float32)
|
||||
for q in range(9):packed[parent.slot(parent.owner.ravel(),q)]=c[q].ravel()
|
||||
# All halo/covered inputs are poisoned; kernel must use legal cores.
|
||||
for q in range(9):packed[parent.slot(parent.owner.ravel()[p.covered.ravel()],q)]=np.nan
|
||||
# Hex initializer needs a portable expression for poisoned NaNs.
|
||||
data=array('coarse',packed,'float').replace('nanf','NAN')
|
||||
code = '#include <cstdint>\n#include <cmath>\n#include <cstdio>\nusing fpxx=float;\n'
|
||||
code += f'constexpr unsigned NQ=9, TOTAL_CELLS={int(np.prod(child.local_shape))};\n'
|
||||
code += 'struct Id {unsigned x;}; Id blockIdx{0},blockDim{1},threadIdx{0};\n'
|
||||
code += 'unsigned index_f(unsigned k,unsigned q){return q*TOTAL_CELLS+k;}\n'
|
||||
code += 'int d_cx[]={0,1,-1,0,0,1,-1,1,-1},d_cy[]={0,0,0,1,-1,1,-1,-1,1};\n'+kernel
|
||||
code += data+array('target',targets,'uint64_t')+array('donor',donors,'uint64_t')
|
||||
code += array('neighbor',ids,'uint64_t')+array('offset',offsets,'float')
|
||||
code += f'float fine[{child.nblocks*9*int(np.prod(child.local_shape))}]={{}};\n'
|
||||
code += f'int main(){{for(unsigned r=0;r<{len(targets)};++r){{threadIdx.x=r;BlockExplodeProjected(coarse,fine,target,donor,{len(targets)},{cells},neighbor,offset);}}'
|
||||
code += f'for(unsigned q=0;q<9;++q)for(unsigned r=0;r<{len(targets)};++r){{auto t=target[r];printf("%.9g\\n",fine[(t/TOTAL_CELLS)*9*TOTAL_CELLS+q*TOTAL_CELLS+t%TOTAL_CELLS]);}}}}'
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
src=Path(folder)/'kernel.cpp'; binary=Path(folder)/'kernel'
|
||||
src.write_text(code)
|
||||
subprocess.run(['g++','-std=c++17','-O2',str(src),'-o',str(binary)],check=True,capture_output=True)
|
||||
result=np.fromstring(subprocess.check_output([str(binary)],text=True),sep=' ').reshape(9,-1)
|
||||
expected,_=reconstruct(c.astype(float),p,neighbors,offsets)
|
||||
np.testing.assert_allclose(result,expected,atol=6e-8,rtol=2e-7)
|
||||
|
||||
|
||||
UNIONS = [
|
||||
[(8, 8, 24, 20)],
|
||||
[(8, 8, 16, 20), (16, 8, 24, 20)], # abutting
|
||||
[(8, 8, 18, 20), (14, 8, 24, 20)], # overlap
|
||||
[(8, 8, 16, 20), (17, 8, 24, 20)], # gap 1
|
||||
[(8, 8, 16, 20), (18, 8, 24, 20)], # gap 2
|
||||
[(8, 8, 16, 20), (19, 8, 24, 20)], # gap 3
|
||||
[(8, 8, 20, 14), (8, 14, 14, 22)], # L concavity
|
||||
[(4, 4, 10, 10), (18, 16, 26, 26)], # separated
|
||||
[(4, 4, 5, 7), (4, 5, 7, 7)], # known concave exit/re-entry path
|
||||
]
|
||||
|
||||
|
||||
def decode(layout, neighbors):
|
||||
inverse = np.full(layout.hole, -1, dtype=int)
|
||||
inverse[layout.owner[layout.owned]] = np.flatnonzero(layout.owned)
|
||||
return inverse[neighbors]
|
||||
|
||||
|
||||
def check_pair_stencil(pair, layout, kind):
|
||||
ids, offsets = projected_explosion_tables(pair, layout, kind)
|
||||
neighbors = decode(layout, ids)
|
||||
legal = set(map(int, pair.ghost_coarse))
|
||||
assert len(np.unique(pair.ghost_fine)) == len(pair.ghost_fine)
|
||||
assert set(neighbors.ravel()) <= legal
|
||||
assert layout.owned.ravel()[neighbors].all()
|
||||
assert (kind.ravel()[neighbors] == ACTIVE).all()
|
||||
assert not pair.covered.ravel()[neighbors].any()
|
||||
ny, nx = pair.coarse_shape
|
||||
for row, donor in enumerate(pair.ghost_coarse):
|
||||
y, x = divmod(int(donor), nx)
|
||||
for axis, (dx, dy) in enumerate(((1, 0), (0, 1))):
|
||||
ends = [(x - dx, y - dy), (x + dx, y + dy)]
|
||||
expected = []
|
||||
for xx, yy in ends:
|
||||
outside = not (0 <= xx < nx and 0 <= yy < ny)
|
||||
if outside and not layout.periodic[axis]:
|
||||
break
|
||||
cell = (yy % ny) * nx + xx % nx
|
||||
if cell not in legal:
|
||||
break
|
||||
expected.append(cell)
|
||||
if len(expected) != 2:
|
||||
expected = [int(donor)] * 2
|
||||
np.testing.assert_array_equal(neighbors[row, 2*axis:2*axis+2], expected)
|
||||
fy, fx = divmod(int(pair.ghost_fine[row]), pair.fine_shape[1])
|
||||
expected = [pair.fine_origin[0]+(fx+.5)/2-(x+.5),
|
||||
pair.fine_origin[1]+(fy+.5)/2-(y+.5)]
|
||||
np.testing.assert_array_equal(offsets[row], expected)
|
||||
assert set(offsets.ravel()) == {-.25, .25}
|
||||
assert ids.dtype == np.uint64 and ids.flags.c_contiguous
|
||||
assert offsets.dtype == np.float32 and offsets.flags.c_contiguous
|
||||
return neighbors, offsets
|
||||
|
||||
|
||||
class GenericProjectedPairs(unittest.TestCase):
|
||||
def test_single_window_and_singleton_union_exact_stencils(self):
|
||||
for bounds in ((4,4,12,12), (2,3,9,10), (2,2,3,3)):
|
||||
a, an, ao = setup(False, bounds)
|
||||
b, bn, bo = setup(True, bounds)
|
||||
np.testing.assert_array_equal(a.ghost_coarse, b.ghost_coarse)
|
||||
np.testing.assert_array_equal(an, bn)
|
||||
np.testing.assert_array_equal(ao, bo)
|
||||
# Coordinates differ between scalar and union arrays; physical rows don't.
|
||||
ay, ax = np.divmod(a.ghost_fine, a.fine_shape[1])
|
||||
by, bx = np.divmod(b.ghost_fine, b.fine_shape[1])
|
||||
np.testing.assert_array_equal(ax + 2*a.fine_origin[0], bx)
|
||||
np.testing.assert_array_equal(ay + 2*a.fine_origin[1], by)
|
||||
|
||||
def test_union_decomposition_permutation_and_topologies(self):
|
||||
cfg = LBMConfig(nx=30, ny=30, viscosity=.1)
|
||||
layout = BlockLayout2D.build(30,30,6,5,periodic=(True,True))
|
||||
stencils = []
|
||||
for rectangles in UNIONS:
|
||||
pair = FixedPatchSet2D.from_config(cfg, rectangles)
|
||||
kind = np.where(pair.covered, OUTER_GHOST, ACTIVE)
|
||||
n, o = check_pair_stencil(pair, layout, kind)
|
||||
other = FixedPatchSet2D.from_config(cfg, rectangles[::-1])
|
||||
on, oo = projected_explosion_tables(other, layout, kind)
|
||||
np.testing.assert_array_equal(n, decode(layout, on))
|
||||
np.testing.assert_array_equal(o, oo)
|
||||
stencils.append((pair.ghost_fine, pair.ghost_coarse, n, o))
|
||||
c = np.random.default_rng(92).normal(size=(9,30,30))
|
||||
c[:, pair.covered] = np.nan
|
||||
values, gradients = reconstruct(c, pair, n, o)
|
||||
self.assertTrue(np.isfinite(values).all())
|
||||
np.testing.assert_allclose(np.einsum('qra,qa->qr', gradients, E),0,atol=1e-15)
|
||||
# q0 keeps its full gradient; no division by zero or forced zero.
|
||||
expected_q0 = .5*(c[0].ravel()[n[:,1::2]]-c[0].ravel()[n[:,::2]])
|
||||
np.testing.assert_array_equal(gradients[0], expected_q0)
|
||||
self.assertGreater(np.max(abs(gradients[0])), .1)
|
||||
for donor in np.unique(pair.ghost_coarse):
|
||||
rows = pair.ghost_coarse == donor
|
||||
self.assertEqual(rows.sum(),4)
|
||||
np.testing.assert_allclose(values[:,rows].mean(axis=1),c.reshape(9,-1)[:,donor],atol=1e-15)
|
||||
for q in range(1,9):
|
||||
delta = o[rows,None,:]-o[None,rows,:]
|
||||
parallel = delta[...,0]*E[q,1] == delta[...,1]*E[q,0]
|
||||
diff = values[q,rows][:,None]-values[q,rows][None,:]
|
||||
np.testing.assert_allclose(diff[parallel],0,atol=1e-15)
|
||||
for split in stencils[1:3]:
|
||||
for expected, actual in zip(stencils[0],split):
|
||||
np.testing.assert_array_equal(actual,expected)
|
||||
|
||||
def test_nested_scalar_origins_sparse_holes_and_unequal_strides(self):
|
||||
cases = [(CFG, FOUR), (LBMConfig(nx=16,ny=16,viscosity=.02),
|
||||
[(4,4,12,12),(8,8,16,16),(8,8,16,16)])]
|
||||
for cfg, refs in cases:
|
||||
hierarchy = build_hierarchy(cfg,refs)
|
||||
for index,pair in enumerate(hierarchy.pairs):
|
||||
if index == 0:
|
||||
layout = BlockLayout2D.build(cfg.ny,cfg.nx,4,4,periodic=(True,True))
|
||||
else:
|
||||
prior = hierarchy.pairs[index-1]
|
||||
layout = (prior.fine_layout((4,8)) if isinstance(prior,FixedPatchSet2D)
|
||||
else BlockLayout2D.build(*prior.fine_shape,4,8,periodic=(False,False)))
|
||||
kind = hierarchy.kinds[index]
|
||||
n,o = check_pair_stencil(pair,layout,kind)
|
||||
c = np.random.default_rng(index).normal(size=(9,*pair.coarse_shape))
|
||||
c[:,~layout.owned | (kind != ACTIVE)] = np.nan
|
||||
self.assertTrue(np.isfinite(reconstruct(c,pair,n,o)[0]).all())
|
||||
if index and isinstance(pair,FixedPatchSet2D):
|
||||
self.assertTrue((~layout.owned).any())
|
||||
self.assertEqual(layout.periodic, (index == 0, index == 0))
|
||||
|
||||
def test_periodicity_is_per_axis_and_missing_pairs_repeat_center(self):
|
||||
cfg=LBMConfig(nx=16,ny=16,viscosity=.1)
|
||||
p=FixedPatch2D.from_config(cfg,(2,2,14,14))
|
||||
kind=np.where(p.covered,OUTER_GHOST,ACTIVE)
|
||||
results=[]
|
||||
for periodic in ((False,False),(True,False),(False,True),(True,True)):
|
||||
layout=BlockLayout2D.build(16,16,4,4,periodic=periodic)
|
||||
n,_=check_pair_stencil(p,layout,kind)
|
||||
results.append(n)
|
||||
y,x=np.divmod(p.ghost_coarse,16)
|
||||
for axis,coordinate in enumerate((x,y)):
|
||||
edge=(coordinate==0)|(coordinate==15)
|
||||
if not periodic[axis]:
|
||||
np.testing.assert_array_equal(n[edge,2*axis:2*axis+2],
|
||||
np.repeat(p.ghost_coarse[edge,None],2,axis=1))
|
||||
self.assertTrue((results[0]!=results[1]).any())
|
||||
self.assertTrue((results[0]!=results[2]).any())
|
||||
self.assertTrue((results[1]!=results[3]).any())
|
||||
|
||||
def test_invalid_parent_donors_reject_before_owner_lookup(self):
|
||||
p,_,_=setup()
|
||||
layout=BlockLayout2D.build(16,16,4,4)
|
||||
kind=np.where(p.covered,OUTER_GHOST,ACTIVE)
|
||||
for nonactive in (1,2,255):
|
||||
bad=kind.copy(); bad.ravel()[p.ghost_coarse[0]]=nonactive
|
||||
with self.assertRaisesRegex(ValueError,'ACTIVE'):
|
||||
projected_explosion_tables(p,layout,bad)
|
||||
with self.assertRaisesRegex(ValueError,'shape'):
|
||||
projected_explosion_tables(p,layout,kind[:1])
|
||||
hole_layout=BlockLayout2D.build(16,16,4,4,origins=[(0,0)])
|
||||
with self.assertRaisesRegex(ValueError,'owned'):
|
||||
projected_explosion_tables(p,hole_layout,kind)
|
||||
|
||||
def test_generic_two_hop_mass_and_momentum_budget(self):
|
||||
cfg=LBMConfig(nx=30,ny=30,viscosity=.1)
|
||||
layout=BlockLayout2D.build(30,30,6,5)
|
||||
grazing_counts=set()
|
||||
for rectangles in UNIONS:
|
||||
pair=FixedPatchSet2D.from_config(cfg,rectangles)
|
||||
ids,o=projected_explosion_tables(pair,layout,np.where(pair.covered,OUTER_GHOST,ACTIVE))
|
||||
n=decode(layout,ids)
|
||||
q,k=np.divmod(pair.coalesce_coarse.astype(np.int64),900)
|
||||
y,x=np.divmod(k,30)
|
||||
grazing=~pair.covered[(y-E[q,1])%30,(x-E[q,0])%30]
|
||||
grazing_counts.add(int(grazing.sum()))
|
||||
self.assertTrue(grazing.any())
|
||||
for collision in (False,True):
|
||||
rng=np.random.default_rng(17)
|
||||
c=W[:,None,None]*(1+rng.normal(0,.002,(9,30,30)))
|
||||
f=W[:,None,None]*(1+rng.normal(0,.002,(9,*pair.fine_shape)))
|
||||
start=totals(c,f,pair)
|
||||
c[:,pair.covered]=np.nan
|
||||
for _ in range(4):
|
||||
f.reshape(9,-1)[:,pair.ghost_fine]=reconstruct(c,pair,n,o)[0]
|
||||
for _ in range(2):
|
||||
f=np.array([np.roll(f[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
|
||||
f[:,pair.fine_kind==OUTER_GHOST]=np.nan
|
||||
if collision:
|
||||
f[:,pair.fine_kind==ACTIVE]=collide(f[:,pair.fine_kind==ACTIVE],pair.tau_fine)
|
||||
out=np.array([np.roll(c[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
|
||||
packets=f.reshape(-1)[pair.coalesce_fine].mean(axis=1)
|
||||
self.assertTrue(np.isfinite(packets[grazing]).all())
|
||||
out.reshape(-1)[pair.coalesce_coarse]=packets
|
||||
if collision:
|
||||
out[:,~pair.covered]=collide(out[:,~pair.covered],pair.tau_coarse)
|
||||
out[:,pair.covered]=np.nan
|
||||
c=out
|
||||
np.testing.assert_allclose(totals(c,f,pair),start,rtol=0,atol=4e-12)
|
||||
self.assertGreater(len(grazing_counts),1) # not a hard-coded eight-corner oracle
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Host-only checks for the opt-in CUDA gate; never imports or compiles CUDA."""
|
||||
from copy import deepcopy
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
from CelerisLab.lbm.hierarchy import build_hierarchy
|
||||
from CelerisLab.lbm.refinement import ACTIVE, OUTER_GHOST, FixedPatch2D, FixedPatchSet2D
|
||||
from CelerisLab.lbm.refinement_tables import projected_explosion_tables
|
||||
from tests.unit.test_hierarchy import CFG, FOUR
|
||||
from tests.unit.test_projected_explosion import decode, reconstruct, totals
|
||||
|
||||
PATH = Path(__file__).resolve().parents[1] / 'validation/multigrid/check_projected_pairs.py'
|
||||
spec = importlib.util.spec_from_file_location('projected_pair_gate', PATH)
|
||||
gate = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(gate)
|
||||
|
||||
|
||||
class ProjectedGateOracle(unittest.TestCase):
|
||||
def check_pair(self, pair, layout, kind):
|
||||
ids, offsets = projected_explosion_tables(pair, layout, kind)
|
||||
neighbors = decode(layout, ids)
|
||||
coarse = np.random.default_rng(731).normal(size=(9, *pair.coarse_shape))
|
||||
legal = np.zeros(pair.coarse_shape, bool)
|
||||
legal.ravel()[pair.ghost_coarse] = True
|
||||
coarse[:, ~legal] = np.nan
|
||||
expected, _ = reconstruct(coarse, pair, neighbors, offsets)
|
||||
# The coordinate oracle must not accidentally share production tables.
|
||||
with patch('CelerisLab.lbm.refinement_tables.projected_explosion_tables',
|
||||
side_effect=AssertionError('oracle used production stencil')):
|
||||
actual = gate.independent_explosion(coarse, pair, layout, kind)
|
||||
np.testing.assert_allclose(actual, expected, rtol=0, atol=2e-15)
|
||||
center = coarse.reshape(9, -1)[:, pair.ghost_coarse]
|
||||
self.assertTrue(np.isfinite(actual).all())
|
||||
self.assertGreater(np.max(abs(actual[0] - center[0])), .01)
|
||||
self.assertGreater(np.max(abs(actual[1:] - center[1:])), .01)
|
||||
return coarse, neighbors, offsets, actual
|
||||
|
||||
def test_scalar_union_nested_sparse_and_unequal_strides(self):
|
||||
def fine_layout(pair, block):
|
||||
return (pair.fine_layout(block) if isinstance(pair, FixedPatchSet2D) else
|
||||
BlockLayout2D.build(*pair.fine_shape, *block, periodic=(False,False)))
|
||||
small = LBMConfig(nx=16, ny=16, viscosity=.02)
|
||||
fixtures = [(small, [(4,4,12,12), (8,8,16,16), (8,8,16,16)]),
|
||||
(CFG, FOUR), (small, [[(4,4,5,7), (4,5,7,7)]])]
|
||||
for cfg, refs in fixtures:
|
||||
hierarchy = build_hierarchy(cfg, refs)
|
||||
for index, pair in enumerate(hierarchy.pairs):
|
||||
block, child_block = ((4,4), (4,8)) if index % 2 == 0 else ((4,8), (4,4))
|
||||
layout = (BlockLayout2D.build(cfg.ny, cfg.nx, *block) if index == 0
|
||||
else fine_layout(hierarchy.pairs[index-1], block))
|
||||
with self.subTest(refs=refs, pair=index):
|
||||
self.assertNotEqual(np.prod(layout.local_shape),
|
||||
np.prod(fine_layout(pair, child_block).local_shape))
|
||||
if index and isinstance(pair, FixedPatchSet2D):
|
||||
self.assertTrue((~layout.owned).any())
|
||||
self.check_pair(pair, layout, hierarchy.kinds[index])
|
||||
|
||||
def test_axis_periodicity_and_mean_preserving_mutants(self):
|
||||
pair = FixedPatch2D.from_config(LBMConfig(nx=16,ny=16,viscosity=.1), (2,2,14,14))
|
||||
kind = np.where(pair.covered, OUTER_GHOST, ACTIVE)
|
||||
outputs = []
|
||||
for periodic in ((False,False), (True,False), (False,True), (True,True)):
|
||||
layout = BlockLayout2D.build(16,16,4,4,periodic=periodic)
|
||||
coarse, neighbors, offsets, actual = self.check_pair(pair, layout, kind)
|
||||
outputs.append(actual)
|
||||
donors, first, inverse = np.unique(pair.ghost_coarse, return_index=True, return_inverse=True)
|
||||
wrong = neighbors[first[np.roll(np.arange(len(donors)), 1)]][inverse]
|
||||
for bad_neighbors, bad_offsets in ((neighbors, -offsets), (wrong, offsets)):
|
||||
mutant, _ = reconstruct(coarse, pair, bad_neighbors, bad_offsets)
|
||||
self.assertGreater(np.max(abs(mutant - actual)), .01)
|
||||
for donor in donors:
|
||||
rows = pair.ghost_coarse == donor
|
||||
self.assertEqual(rows.sum(), 4)
|
||||
np.testing.assert_allclose(mutant[:,rows].mean(axis=1),
|
||||
coarse.reshape(9,-1)[:,donor], rtol=0, atol=1e-15)
|
||||
bad_kind = kind.copy(); bad_kind.ravel()[pair.ghost_coarse[0]] = OUTER_GHOST
|
||||
with self.assertRaisesRegex(AssertionError, 'illegal center donor'):
|
||||
gate.independent_explosion(coarse, pair, layout, bad_kind)
|
||||
for i, j in ((0,1), (0,2), (1,3), (2,3)):
|
||||
self.assertGreater(np.max(abs(outputs[i] - outputs[j])), .01)
|
||||
|
||||
def test_concave_replay_preserves_existing_fp64_budget(self):
|
||||
cfg, refs, blocks = gate.cases()['concave']
|
||||
hierarchy = build_hierarchy(cfg, refs); pair = hierarchy.pairs[0]
|
||||
layout = BlockLayout2D.build(cfg.ny, cfg.nx, *blocks[0])
|
||||
rng = np.random.default_rng(17)
|
||||
values = tuple(gate.W[:,None,None] * (1 + rng.normal(0,.002,(9,*shape)))
|
||||
for shape in (pair.coarse_shape, pair.fine_shape))
|
||||
start = totals(*values, pair)
|
||||
values[0][:,pair.covered] = np.nan
|
||||
for _ in range(8):
|
||||
values = gate.replay_two_hops(values, pair, layout, hierarchy.kinds[0])
|
||||
np.testing.assert_allclose(totals(*values, pair), start, rtol=0, atol=4e-12)
|
||||
self.assertEqual(gate.REPLAY_ATOL, 3e-6) # CUDA replay bound is separate.
|
||||
|
||||
|
||||
class ProjectedGateReceipts(unittest.TestCase):
|
||||
def test_digest_exact_finite_bits_and_nan_masks(self):
|
||||
values = np.array([0., -0., np.nan, 1.], dtype=np.float32)
|
||||
self.assertEqual(gate.digest(values), gate.digest(values.copy()))
|
||||
payload = values.copy(); payload.view(np.uint32)[2] = 0x7fc00001
|
||||
self.assertEqual(gate.digest(values), gate.digest(payload))
|
||||
variants = [np.array([-0., -0., np.nan, 1.], np.float32),
|
||||
np.array([0., np.nan, -0., 1.], np.float32),
|
||||
np.array([0., -0., np.nan, np.nextafter(np.float32(1), np.float32(2))], np.float32),
|
||||
values.reshape(2,2), values.astype(np.float64)]
|
||||
for changed in variants:
|
||||
self.assertNotEqual(gate.digest(values), gate.digest(changed))
|
||||
with self.assertRaisesRegex(AssertionError, 'infinite'):
|
||||
gate.digest(np.array([np.inf]))
|
||||
|
||||
def test_snapshot_negative_controls_and_exact_dev_options(self):
|
||||
snapshot = [dict(step=0, levels=[dict(raw='raw',public='public',active=4)],
|
||||
mass_momentum=[1.,0.,0.],observed_level_hops=[0,0])]
|
||||
gate.compare_snapshots(snapshot, deepcopy(snapshot))
|
||||
diagnostic = deepcopy(snapshot); diagnostic[0]['mass_momentum'][0] = 2.
|
||||
gate.compare_snapshots(snapshot, diagnostic) # Diagnostics are not bitwise gate fields.
|
||||
mutants = [[]]
|
||||
for key, value in (('raw','changed'), ('public','changed'), ('active',3)):
|
||||
changed = deepcopy(snapshot); changed[0]['levels'][0][key] = value
|
||||
mutants.append(changed)
|
||||
changed = deepcopy(snapshot); changed[0]['step'] = 1; mutants.append(changed)
|
||||
changed = deepcopy(snapshot); changed[0]['observed_level_hops'] = [0,1]; mutants.append(changed)
|
||||
for changed in mutants:
|
||||
with self.assertRaises(AssertionError):
|
||||
gate.compare_snapshots(snapshot, changed)
|
||||
self.assertEqual(gate.default_options('omitted'), {})
|
||||
self.assertEqual(gate.default_options('constant'), {'explosion':'constant'})
|
||||
self.assertEqual(gate.default_options('projected_linear'), {'explosion':'projected_linear'})
|
||||
|
||||
def test_baseline_receipt_rejects_provenance_and_contract_mutants(self):
|
||||
keys = [f'{name}:shifted={shifted}' for name in gate.cases() for shifted in (False,True)]
|
||||
good = dict(completed=True,dry_run=False,mode='baseline',production_revision=gate.BASE,
|
||||
source=dict(revision=gate.BASE,dirty=False),driver_sha256='driver',
|
||||
stops=list(gate.STOPS),operator_tolerance=[6e-8,2e-7],
|
||||
rest_tolerance=[3e-7,1e-6],replay_tolerance=[3e-6,0],default_snapshots=dict.fromkeys(keys, []),
|
||||
actual_build_sidecars=[{'ptx_sha256':'captured'}])
|
||||
self.assertIs(gate.baseline_receipt(good, 'driver', keys), good)
|
||||
mutants = dict(completed=False,dry_run=True,mode='candidate',production_revision='wrong',
|
||||
driver_sha256='wrong',stops=[0,1,2],operator_tolerance=[1e-6,2e-7],
|
||||
rest_tolerance=[3e-7,1e-5],replay_tolerance=[3e-6,1e-7],actual_build_sidecars=[],default_snapshots={})
|
||||
for field, value in mutants.items():
|
||||
bad = deepcopy(good); bad[field] = value
|
||||
with self.subTest(field=field), self.assertRaises(ValueError):
|
||||
gate.baseline_receipt(bad, 'driver', keys)
|
||||
for changes in (dict(revision='wrong'), dict(dirty=True), dict(dirty=None)):
|
||||
bad = deepcopy(good); bad['source'].update(changes)
|
||||
with self.assertRaises(ValueError):
|
||||
gate.baseline_receipt(bad, 'driver', keys)
|
||||
bad = deepcopy(good); bad['default_snapshots']['unexpected'] = []
|
||||
with self.assertRaises(ValueError):
|
||||
gate.baseline_receipt(bad, 'driver', keys)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,198 @@
|
||||
"""Production host dispatch with CUDA doubles; no CUDA compilation or flow runs."""
|
||||
from contextlib import nullcontext
|
||||
from dataclasses import replace
|
||||
from unittest.mock import Mock
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from CelerisLab.config import LBMConfig
|
||||
from tests.unit.test_hierarchy import CFG, FOUR
|
||||
from tests.unit.test_refinement_three_level_multipatch import host_module
|
||||
|
||||
|
||||
def backend(**options):
|
||||
host = host_module()
|
||||
calls, allocations, storage = [], [], {}
|
||||
context = Mock(activate=nullcontext, sm_arch='sm_70')
|
||||
host.cuda.mem_alloc.side_effect = lambda size: allocate(size)
|
||||
|
||||
def allocate(size):
|
||||
pointer = Mock(name=f'buffer{len(allocations)}', size=size)
|
||||
allocations.append(pointer)
|
||||
return pointer
|
||||
|
||||
def load(_):
|
||||
module = Mock()
|
||||
module.get_global.return_value = (Mock(), 20)
|
||||
module.get_function.side_effect = lambda name: Mock(
|
||||
name=name, side_effect=lambda *a, **kw: calls.append((name, a, kw)))
|
||||
return module
|
||||
|
||||
host.compiler.load_module.side_effect = load
|
||||
host.cuda.memcpy_htod.side_effect = lambda ptr, a: storage.__setitem__(id(ptr), np.array(a, copy=True))
|
||||
host.cuda.memcpy_dtoh.side_effect = lambda a, ptr: np.copyto(a, storage[id(ptr)])
|
||||
cfg = options.pop('cfg', CFG)
|
||||
refinements = options.pop('refinements', FOUR)
|
||||
blocks = options.pop('level_blocks', [(4, 4)] * (len(refinements) + 1))
|
||||
run = host.FixedPatchStepper(cfg, context=context, refinements=refinements,
|
||||
level_blocks=blocks, **options)
|
||||
run.initialize(*(np.full(level.shape, 1/9, np.float32) for level in run._levels))
|
||||
return run, host, calls, allocations, storage
|
||||
|
||||
|
||||
def trace(run, calls, allocations, storage):
|
||||
"""Stable pointer identities expose allocations/arguments, not just kernel names."""
|
||||
pointer = {id(value): index for index, value in enumerate(allocations)}
|
||||
launches = [(name, tuple(('ptr', pointer[id(a)]) if id(a) in pointer else int(a)
|
||||
for a in args), kw['grid'], kw['block'])
|
||||
for name, args, kw in calls]
|
||||
uploads = [(index, value.dtype.str, value.shape, value.tobytes())
|
||||
for index, ptr in enumerate(allocations) if (value := storage.get(id(ptr))) is not None]
|
||||
return [p.size for p in allocations], uploads, launches
|
||||
|
||||
|
||||
class ProjectedPairDispatch(unittest.TestCase):
|
||||
def test_omitted_and_constant_have_identical_allocations_uploads_and_launches(self):
|
||||
results = []
|
||||
for options in ({}, {'explosion': 'constant'}):
|
||||
run, host, calls, allocations, storage = backend(**options)
|
||||
self.assertEqual(run._explosion, 'constant')
|
||||
self.assertTrue(all(p.neighbors is None and p.offsets is None for p in run._pairs))
|
||||
run.step()
|
||||
explosion = [args for name, args, _ in calls if name == 'BlockExplode']
|
||||
self.assertEqual(len(explosion), 7)
|
||||
self.assertTrue(all(len(args) == 6 for args in explosion))
|
||||
self.assertFalse(any(name == 'BlockExplodeProjected' for name, _, _ in calls))
|
||||
results.append(trace(run, calls, allocations, storage))
|
||||
run.close()
|
||||
self.assertEqual(*results)
|
||||
|
||||
def test_projected_adds_exactly_two_tables_per_adjacent_pair(self):
|
||||
for depth in (2, 3, 4):
|
||||
constant, _, _, ca, _ = backend(refinements=FOUR[:depth-1])
|
||||
projected, _, calls, pa, storage = backend(
|
||||
refinements=FOUR[:depth-1], explosion='projected_linear')
|
||||
self.assertEqual(len(pa)-len(ca), 2*(depth-1))
|
||||
for pair, geometry in zip(projected._pairs, projected.hierarchy.pairs):
|
||||
self.assertEqual(storage[id(pair.neighbors)].shape, (len(geometry.ghost_fine), 4))
|
||||
self.assertEqual(storage[id(pair.offsets)].shape, (len(geometry.ghost_fine), 2))
|
||||
self.assertEqual(pair.neighbors.size, len(geometry.ghost_fine)*4*8)
|
||||
self.assertEqual(pair.offsets.size, len(geometry.ghost_fine)*2*4)
|
||||
projected.step()
|
||||
explosions = [args for name, args, _ in calls if name == 'BlockExplodeProjected']
|
||||
self.assertEqual(len(explosions), 2**(depth-1)-1)
|
||||
self.assertTrue(all(len(args) == 8 for args in explosions))
|
||||
self.assertFalse(any(name == 'BlockExplode' for name, _, _ in calls))
|
||||
for args in explosions:
|
||||
pair = next(p for p in projected._pairs if args[2] is p.ghosts)
|
||||
self.assertIs(args[6], pair.neighbors)
|
||||
self.assertIs(args[7], pair.offsets)
|
||||
self.assertEqual(args[5], pair.parent_cells)
|
||||
constant.close(); projected.close()
|
||||
|
||||
def test_three_and_four_level_schedule_has_fresh_midpoint_sources(self):
|
||||
for depth in (3, 4):
|
||||
for explosion in ('constant', 'projected_linear'):
|
||||
run, host, calls, allocations, storage = backend(
|
||||
refinements=FOUR[:depth-1], explosion=explosion)
|
||||
before = len(allocations)
|
||||
events = []
|
||||
for index, pair in enumerate(run._pairs):
|
||||
pair.explode.side_effect = lambda source, *a, index=index, **kw: events.append(('explode', index, source))
|
||||
pair.coalesce.side_effect = lambda child, source, *a, index=index, **kw: events.append(('restrict', index, source))
|
||||
for index, level in enumerate(run._levels):
|
||||
level.kernel.side_effect = lambda source, *a, index=index, **kw: events.append(('step', index, source))
|
||||
level.halo_copy.side_effect = lambda source, *a, index=index, **kw: events.append(('halo', index, source))
|
||||
expected = []
|
||||
|
||||
def hop(index, source):
|
||||
if index+1 < depth:
|
||||
expected.append(('explode', index, source))
|
||||
child = run._levels[index+1]
|
||||
hop(index+1, child.current)
|
||||
hop(index+1, child.spare)
|
||||
expected.append(('halo', index, source))
|
||||
if index+1 < depth:
|
||||
expected.append(('restrict', index, source))
|
||||
expected.append(('step', index, source))
|
||||
|
||||
hop(0, run._levels[0].current)
|
||||
run.step()
|
||||
self.assertEqual(events, expected)
|
||||
self.assertEqual([level.kernel.call_count for level in run._levels], [2**i for i in range(depth)])
|
||||
self.assertEqual(len(allocations), before)
|
||||
self.assertEqual(run.step_count, 1)
|
||||
for index in range(1, depth-1):
|
||||
feeds = [source for name, pair, source in events if name == 'explode' and pair == index]
|
||||
self.assertEqual(feeds, [run._levels[index].current, run._levels[index].spare] * (2**(index-1)))
|
||||
run.close()
|
||||
|
||||
def test_scalar_windows_follow_the_same_pair_path(self):
|
||||
run, _, _, _, _ = backend(cfg=LBMConfig(nx=16,ny=16,viscosity=.02),
|
||||
refinements=[(4,4,12,12),(8,8,16,16),(8,8,16,16)],
|
||||
level_blocks=[(4,4),(4,8),(8,4),(4,4)], explosion='projected_linear')
|
||||
self.assertTrue(all(pair.neighbors is not None for pair in run._pairs))
|
||||
self.assertEqual([pair.parent_cells for pair in run._pairs], [36,60,60])
|
||||
run.step()
|
||||
self.assertEqual([level.kernel.call_count for level in run._levels], [1,2,4,8])
|
||||
run.close()
|
||||
|
||||
def test_physics_gate_precedes_context_and_allocations(self):
|
||||
host = host_module()
|
||||
context = Mock()
|
||||
options = dict(refinements=FOUR[:1], level_blocks=[(4,4)]*2,
|
||||
explosion='projected_linear', context=context)
|
||||
for cfg, changes in [(CFG, {'explosion':'unknown'}),
|
||||
(CFG, {'coarse_boundary':'slot_reservoir'}),
|
||||
(CFG, {'coarse_boundary':'channel'}),
|
||||
(CFG, {'level_blocks':[None,None], 'refinements':[(6,6,26,26)]}),
|
||||
(CFG, {'level_objects':[(),(object(),)]}),
|
||||
(CFG, {'level_jets':[(),(object(),)]}),
|
||||
(replace(CFG,collision='MRT'), {}),
|
||||
(replace(CFG,les_enabled=True), {})]:
|
||||
with self.subTest(changes=changes, cfg=cfg), self.assertRaisesRegex(ValueError,'explosion'):
|
||||
host.FixedPatchStepper(cfg, **dict(options, **changes))
|
||||
context.activate.assert_not_called()
|
||||
host.cuda.mem_alloc.assert_not_called()
|
||||
# The legacy constant path still allows MRT and dense storage.
|
||||
for options in ({'cfg':replace(CFG,collision='MRT')},
|
||||
{'level_blocks':[None,None], 'refinements':[(6,6,26,26)]}):
|
||||
run, _, _, _, _ = backend(**options)
|
||||
run.close()
|
||||
|
||||
def test_remap_preserves_selector_tables_and_constant_default(self):
|
||||
for explosion in ('constant','projected_linear'):
|
||||
run, host, _, _, storage = backend(refinements=FOUR[:2], explosion=explosion)
|
||||
run._step_count = 7
|
||||
old = run.download()
|
||||
clone = run.remap_child(FOUR[1])
|
||||
self.assertEqual(clone._explosion, explosion)
|
||||
self.assertEqual(clone.step_count, 7)
|
||||
for pair in clone._pairs:
|
||||
self.assertEqual(pair.neighbors is None, explosion == 'constant')
|
||||
for expected, actual in zip(old, clone.download()):
|
||||
np.testing.assert_array_equal(actual,expected)
|
||||
clone.step()
|
||||
self.assertEqual((run.step_count,clone.step_count), (7,8))
|
||||
clone.close(); run.close()
|
||||
|
||||
def test_changed_remap_geometry_keeps_projected_in_both_representations(self):
|
||||
changed = [(22,22,42,42), (72,68,76,76)]
|
||||
for shifted in (False, True):
|
||||
run, _, _, _, _ = backend(cfg=replace(CFG,ddf_shifting=shifted),
|
||||
refinements=FOUR[:2], explosion='projected_linear')
|
||||
run._step_count = 3
|
||||
before = run.download()
|
||||
clone = run.remap_child(changed)
|
||||
self.assertEqual(clone._explosion, 'projected_linear')
|
||||
self.assertEqual(clone.hierarchy.pairs[1].bounds, tuple(changed))
|
||||
self.assertNotEqual(clone._pairs[1].n_ghosts, run._pairs[1].n_ghosts)
|
||||
for old, new in zip(run._pairs, clone._pairs):
|
||||
self.assertIsNot(old.neighbors, new.neighbors)
|
||||
self.assertIsNotNone(new.neighbors)
|
||||
for expected, actual in zip(before, run.download()):
|
||||
np.testing.assert_array_equal(actual, expected)
|
||||
clone.step()
|
||||
self.assertEqual((run.step_count, clone.step_count), (3, 4))
|
||||
clone.close(); run.close()
|
||||
@@ -157,8 +157,9 @@ class RawExchange(unittest.TestCase):
|
||||
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)
|
||||
host.FixedPatchStepper(LBMConfig(nx=32, ny=32, viscosity=.1), context=ctx,
|
||||
refinements=[(4, 4, 28, 28)],
|
||||
level_objects=[(), (body(radius=2.15),)], observe_raw_exchange=True)
|
||||
ctx.activate.assert_not_called()
|
||||
|
||||
def test_all_depth_weights(self):
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Host reduction contract for the optional observation-window exchange ledger."""
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from CelerisLab.lbm.raw_exchange import summarize_exchange
|
||||
|
||||
|
||||
def reduce(rows, window, duration=None, h=.5, time_end=10):
|
||||
return summarize_exchange(rows, np.array([0, 0, 1]), np.array([False, True, False]),
|
||||
body_id=0, level=1, h=h, time_end=time_end, window=window,
|
||||
reference_center=(0., 0.), duration=duration)
|
||||
|
||||
|
||||
class ExchangeWindowReduction(unittest.TestCase):
|
||||
def test_window_duration_scaling(self):
|
||||
rows = np.array([[1., 2., 3., 4.], [.5, -1., 0., 1.], [9., 9., 9., 9.]])
|
||||
out = reduce(rows, "window", duration=4)
|
||||
np.testing.assert_array_equal(out["total"]["impulse"], [1.5, 1.])
|
||||
np.testing.assert_array_equal(out["total"]["force"], [1.5 / 4, 1. / 4])
|
||||
self.assertEqual(out["total"]["torque"], 3. / 4)
|
||||
self.assertEqual(out["total"]["mass_rate"], 5. / 4)
|
||||
np.testing.assert_array_equal(out["aperture"]["impulse"], [.5, -1.])
|
||||
self.assertEqual((out["time_start"], out["time_end"]), (6., 10))
|
||||
self.assertEqual(out["sample_count"], 8) # duration / h fine substeps
|
||||
self.assertEqual(out["window"], "window")
|
||||
|
||||
def test_existing_windows_unchanged(self):
|
||||
rows = np.array([[1., 2., 3., 4.], [0., 0., 0., 0.], [0., 0., 0., 0.]])
|
||||
base = reduce(rows, "last_base_step")
|
||||
self.assertEqual((base["time_start"], base["sample_count"]), (9., 2))
|
||||
np.testing.assert_array_equal(base["total"]["force"], [1., 2.])
|
||||
sub = reduce(rows, "last_substep")
|
||||
self.assertEqual(sub["sample_count"], 1)
|
||||
|
||||
def test_duration_validation(self):
|
||||
rows = np.zeros((3, 4))
|
||||
for bad in (None, 0, -1, 1.5, True):
|
||||
with self.subTest(duration=bad), self.assertRaises(ValueError):
|
||||
reduce(rows, "window", duration=bad)
|
||||
with self.assertRaises(ValueError):
|
||||
reduce(rows, "last_base_step", duration=2)
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -0,0 +1,107 @@
|
||||
import sys, json, hashlib
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "tests/validation"))
|
||||
sys.path.insert(0, str(ROOT / "tests/unit"))
|
||||
import re100_helpers as h
|
||||
import periodic as p
|
||||
|
||||
|
||||
@pytest.mark.parametrize("n", [20, 40, 80])
|
||||
def test_units(n):
|
||||
x = h.parameters(n, 0.01)
|
||||
assert x["viscosity"] * x["dx"] ** 2 / x["dt"] == pytest.approx(0.001)
|
||||
assert x["mean_speed"] * x["dx"] / x["dt"] == pytest.approx(1.0)
|
||||
assert x["mean_speed"] * n / x["viscosity"] == pytest.approx(100)
|
||||
assert x["peak_mach"] == pytest.approx(0.015 * np.sqrt(3))
|
||||
assert (x["top"] - x["bottom"]) * x["dx"] == pytest.approx(0.41)
|
||||
assert (x["nx"] - 1) * x["dx"] == pytest.approx(2.2)
|
||||
assert (np.array(x["center"]) - [0, 0.5]) * x["dx"] == pytest.approx([0.2, 0.2])
|
||||
cfg, _, _ = h.make_config(n, 0.01, "MRT", "neq_extrap", "zou_he_local")
|
||||
assert cfg["method"]["inlet"]["scheme"] == "zou_he_local"
|
||||
|
||||
|
||||
def test_pressure_units():
|
||||
x = h.parameters(40, 0.01)
|
||||
ny, nx = x["ny"], x["nx"]
|
||||
xx = np.arange(nx)[None, :]
|
||||
rho = np.broadcast_to(1 + xx * 1e-6, (ny, nx))
|
||||
fluid = np.ones_like(rho, dtype=bool)
|
||||
assert h.pressure_probes(rho, fluid, x)["dp"] == pytest.approx(
|
||||
-40e-6 / 0.01**2 / 3, abs=1e-10
|
||||
)
|
||||
|
||||
|
||||
def rows(amplitude=1.0, drift=0.0):
|
||||
out = []
|
||||
for t in np.arange(240, 300.001, 0.02):
|
||||
out.append(
|
||||
dict(
|
||||
tstar=t,
|
||||
tstar_force=t,
|
||||
cl=amplitude * np.sin(2 * np.pi * t / 3.3),
|
||||
cd=3.2 + 0.03 * np.cos(4 * np.pi * t / 3.3) + drift * t,
|
||||
dp=2.46 + 0.04 * np.sin(4 * np.pi * t / 3.3),
|
||||
mass=1.0,
|
||||
flux_in=1.0,
|
||||
flux_out=1.0,
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def test_periodic_synthetic():
|
||||
a = p.assess(rows())
|
||||
assert a["periodic"]
|
||||
assert len(a["cycles"]) > 5
|
||||
assert a["cycles"][-1]["st"] == pytest.approx(1 / 3.3, rel=1e-5)
|
||||
assert not p.assess(rows(amplitude=0.01))["periodic"]
|
||||
assert not p.assess(rows(drift=0.01))["periodic"]
|
||||
assert not p.assess(rows()[:100])["periodic"]
|
||||
|
||||
|
||||
def test_production_unmodified(tmp_path):
|
||||
from historical_tree import historical_tree
|
||||
|
||||
m = json.loads(
|
||||
(ROOT / "tests/validation/data/dfg_re100/source_manifest.json").read_text()
|
||||
)
|
||||
assert m["commit"] == h.PIN
|
||||
origin = historical_tree(m["commit"], tmp_path)
|
||||
for name, digest in m["production_sha256"].items():
|
||||
assert hashlib.sha256((origin / name).read_bytes()).hexdigest() == digest, name
|
||||
|
||||
|
||||
def test_reference():
|
||||
r = json.loads(
|
||||
(ROOT / "tests/validation/data/dfg_re100/reference_cycle.json").read_text()
|
||||
)
|
||||
assert 0.301 < r["st"] < 0.303
|
||||
assert 3.22 < r["cd_max"] < 3.23
|
||||
assert 2.5 < r["dp_max"] < 2.51
|
||||
assert p.compare(r, r)["reference_agreement"]
|
||||
|
||||
|
||||
def test_stale_cycles_rejected():
|
||||
r = rows()
|
||||
level = next(x["cl"] for x in r if x["tstar"] >= 284.5)
|
||||
for x in r:
|
||||
if x["tstar"] >= 284.5:
|
||||
x["cl"] = level
|
||||
assert not p.assess(r)["periodic"]
|
||||
|
||||
|
||||
def test_min_time_is_used(monkeypatch):
|
||||
monkeypatch.setitem(p.GATES, "min_tstar", 290.0)
|
||||
assert not p.assess(rows())["periodic"]
|
||||
|
||||
|
||||
def test_nonconsecutive_cycles_rejected(monkeypatch):
|
||||
cycles = p.extract(rows())
|
||||
bad = cycles[-6:]
|
||||
bad.pop(2)
|
||||
monkeypatch.setattr(p, "extract", lambda *a, **k: bad)
|
||||
assert not p.assess(rows())["periodic"]
|
||||
@@ -0,0 +1,678 @@
|
||||
"""CPU-only contract tests. These are not CUDA or DFG acceptance evidence."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import run_refined_dfg as r
|
||||
from historical_tree import historical_tree
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
h, periodic = r.helpers(r.DEFAULT_SOURCE)
|
||||
|
||||
|
||||
def test_source_pin():
|
||||
historical = json.loads((r.HERE / "data/refined_dfg/source_manifest.json").read_text())
|
||||
assert historical["commit"].startswith("cd3a657")
|
||||
mrt_history = json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text())
|
||||
assert mrt_history["commit"].startswith("3ea131e")
|
||||
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
|
||||
r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")
|
||||
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
|
||||
r.verify_source(r.DEFAULT_SOURCE, "MRT")
|
||||
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
|
||||
r.verify_source(r.DEFAULT_SOURCE, "SRT")
|
||||
with TemporaryDirectory() as directory:
|
||||
for collision, mode, prefix in (
|
||||
("SRT", "last_base_step", "cd3a657"),
|
||||
("MRT", "last_base_step", "3ea131e"),
|
||||
("MRT", "window", "a233c90"),
|
||||
):
|
||||
name = {"SRT": "source_manifest.json", "MRT": "source_manifest_mrt.json"}[collision]
|
||||
name = "source_manifest_window.json" if mode == "window" else name
|
||||
commit = json.loads((r.HERE / "data/refined_dfg" / name).read_text())["commit"]
|
||||
assert commit.startswith(prefix)
|
||||
root = historical_tree(commit, Path(directory) / commit)
|
||||
assert r.verify_source(root, collision, mode)["commit"] == commit
|
||||
|
||||
|
||||
def test_matched_units_and_planes():
|
||||
p = r.plan(h)
|
||||
c = p["root"]
|
||||
f = p["uniform"]
|
||||
assert c["nx"] == 881 and c["ny"] == 166
|
||||
assert f["nx"] == 1761 and f["ny"] == 330
|
||||
assert c["dx"] / 2 == f["dx"] and c["dt"] / 2 == f["dt"]
|
||||
assert np.isclose(c["viscosity"] * c["dx"] ** 2 / c["dt"], 0.001)
|
||||
assert np.isclose(f["viscosity"] * f["dx"] ** 2 / f["dt"], 0.001)
|
||||
assert np.isclose((p["body_root_edge"][0] - 0.5) * c["dx"], 0.2)
|
||||
assert np.isclose((p["body_root_edge"][1] - 1) * c["dx"], 0.2)
|
||||
assert np.isclose((165 - 1) * c["dx"], 0.41)
|
||||
assert np.isclose((880.5 - 0.5) * c["dx"], 2.2)
|
||||
assert np.isclose(2 * p["radius_root"] * c["dx"], 0.1)
|
||||
|
||||
|
||||
def test_same_numerics():
|
||||
c, _, _ = r.config(h, 40)
|
||||
f, _, _ = r.config(h, 80)
|
||||
assert c["method"] == f["method"]
|
||||
m = c["method"]
|
||||
assert m["collision"] == "SRT" and m["streaming"] == "double_buffer"
|
||||
assert m["store_precision"] == "FP32" and m["ddf_shifting"]
|
||||
assert not m["les"]["enabled"]
|
||||
assert m["inlet"]["scheme"] == "zou_he_local"
|
||||
assert m["outlet"]["mode"] == "neq_extrap" and m["outlet"]["srt_neq_damp"] == 0.5
|
||||
assert not m["outlet"]["backflow_clamp"]
|
||||
|
||||
|
||||
def test_actual_geometry_guards():
|
||||
cfg, pair, body, host = r.host_geometry(r.DEFAULT_SOURCE, h)
|
||||
assert pair.fine_shape == (240, 568)
|
||||
assert np.allclose(
|
||||
host.objects[0].geometry.center,
|
||||
[
|
||||
(80.5 - pair.fine_origin[0]) / 0.5 - 0.5,
|
||||
(81 - pair.fine_origin[1]) / 0.5 - 0.5,
|
||||
],
|
||||
)
|
||||
assert cfg.nx % r.BASE_BLOCK[1] == 0 and cfg.ny % r.BASE_BLOCK[0] == 0
|
||||
assert len(host.boundary_cells) > 0
|
||||
|
||||
|
||||
def test_physical_leaf_mass_excludes_covered_and_halo():
|
||||
c = np.full((9, 4, 4), 1 / 9)
|
||||
f = np.full((9, 8, 8), 1 / 9)
|
||||
cm = np.ones((4, 4), bool)
|
||||
cm[1:3, 1:3] = False
|
||||
fm = np.zeros((8, 8), bool)
|
||||
fm[2:6, 2:6] = True
|
||||
c[:, ~cm] = np.nan
|
||||
f[:, ~fm] = np.nan
|
||||
assert np.isclose(r.leaf_mass([c, f], 2, [cm, fm]), 64)
|
||||
f[:, 3, 3] = np.nan
|
||||
with unittest.TestCase().assertRaisesRegex(ValueError, "Nonfinite"):
|
||||
r.leaf_mass([c, f], 2, [cm, fm])
|
||||
|
||||
|
||||
def test_force_impulse_interval_scaling():
|
||||
from CelerisLab.lbm.raw_exchange import summarize_exchange
|
||||
|
||||
# Two fine substeps each j=(2,4), h=.5 => root impulse=(1,2).
|
||||
result = summarize_exchange(
|
||||
[[1.0, 2.0, 0.0, 0.0]],
|
||||
[0],
|
||||
[False],
|
||||
body_id=0,
|
||||
level=1,
|
||||
h=0.5,
|
||||
time_end=8,
|
||||
window="last_base_step",
|
||||
reference_center=(80.5, 81.0),
|
||||
)
|
||||
assert result["sample_count"] == 2 and result["time_start"] == 7
|
||||
np.testing.assert_allclose(result["total"]["force"], [1, 2])
|
||||
assert result["units"] == "base_lattice_per_unit_depth"
|
||||
# Force unit scales dx^3/dt^2, thus uniform fine force is twice root.
|
||||
root_cd = 1 / (0.5 * r.SPEED**2 * 40)
|
||||
fine_cd = 2 / (0.5 * r.SPEED**2 * 80)
|
||||
assert root_cd == fine_cd
|
||||
|
||||
|
||||
def test_pressure_local_frame_and_units():
|
||||
_, pair, _, _ = r.host_geometry(r.DEFAULT_SOURCE, h)
|
||||
center = [
|
||||
(80.5 - pair.fine_origin[0]) / 0.5 - 0.5,
|
||||
(81 - pair.fine_origin[1]) / 0.5 - 0.5,
|
||||
]
|
||||
p = {"center": center, "radius": 40.0, "mean_speed": r.SPEED}
|
||||
yy, xx = np.indices(pair.fine_shape)
|
||||
rho = 1 + 1e-7 * xx
|
||||
probes = h.pressure_probes(rho, np.ones(pair.fine_shape, bool), p)
|
||||
assert np.isclose(probes["dp"], -80e-7 / (3 * r.SPEED**2))
|
||||
|
||||
|
||||
def test_initializer_coordinates_and_rest_ghosts():
|
||||
g = r.initial_state((166, 881), (0, 0), 1, root=True)
|
||||
rho, ux, uy = r.macros(g)
|
||||
assert np.max(abs(rho - 1)) < 1e-14
|
||||
assert np.max(abs(uy)) == 0
|
||||
assert np.max(abs(ux[[0, -1]])) == 0
|
||||
assert np.isclose(ux[81, 50], 6 * r.SPEED * (80.5 / 164) * (1 - 80.5 / 164))
|
||||
|
||||
|
||||
def test_axis_specific_sampling_phase():
|
||||
_, pair, _, _ = r.host_geometry(r.DEFAULT_SOURCE, h)
|
||||
assert pair.fine_origin == (38.0, 22.0)
|
||||
j = np.arange(pair.fine_shape[1])
|
||||
k = np.arange(pair.fine_shape[0])
|
||||
fine_x = (pair.fine_origin[0] + (j + 0.5) / 2 - 0.5) * 0.0025
|
||||
fine_y = (pair.fine_origin[1] + (k + 0.5) / 2 - 1) * 0.0025
|
||||
np.testing.assert_allclose(fine_x / 0.00125, j + 75.5, atol=1e-12)
|
||||
np.testing.assert_allclose(fine_y / 0.00125, (k + 43) - 0.5, atol=1e-12)
|
||||
np.testing.assert_allclose(fine_x - (j + 75) * 0.00125, 0.5 * 0.00125, 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):
|
||||
"""verify_source's manifest selection with its working-tree hash comparison neutralised.
|
||||
|
||||
Source identity is owned by test_source_pin; this exercises only which frozen manifest
|
||||
verify_source selects and returns for (collision, observation_mode).
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
||||
class Digest(str):
|
||||
def __ne__(self, other):
|
||||
return False
|
||||
|
||||
class Hash:
|
||||
def __init__(self, data=b""):
|
||||
pass
|
||||
|
||||
def hexdigest(self):
|
||||
return Digest()
|
||||
|
||||
with patch.object(r.hashlib, "sha256", Hash):
|
||||
return r.verify_source(r.DEFAULT_SOURCE, collision, observation_mode)
|
||||
|
||||
|
||||
def test_full_requires_completed_pilot():
|
||||
with unittest.TestCase().assertRaisesRegex(ValueError, "pilot-results"):
|
||||
r.verify_pilot(None, frozen_manifest("window"))
|
||||
|
||||
|
||||
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."""
|
||||
# Receipt-logic fixture: the frozen manifest verify_source returns for a matching tree.
|
||||
# Source identity itself is checked only by test_source_pin.
|
||||
manifest = (r.verify_source(r.DEFAULT_SOURCE, "MRT", "window", jet_scenario)
|
||||
if observation_mode == "window" and jet_scenario != "none" else
|
||||
frozen_manifest(observation_mode))
|
||||
selection = dict(case_selection=case_selection, uniform_diameter=uniform_diameter, placement=placement)
|
||||
(path / "plan.json").write_text(
|
||||
json.dumps(
|
||||
dict(
|
||||
source_manifest=manifest,
|
||||
**selection,
|
||||
collision="MRT",
|
||||
jet_scenario=jet_scenario,
|
||||
observation_mode=observation_mode,
|
||||
bounds_root_edge=list(r.PLACEMENTS[placement]),
|
||||
runner_sha256=r.runner_hash(),
|
||||
)
|
||||
)
|
||||
)
|
||||
for case, diameter in r.selected_cases(case_selection, uniform_diameter):
|
||||
dest = path / case
|
||||
dest.mkdir()
|
||||
interval = round(0.02 * diameter / r.SPEED)
|
||||
rows = [
|
||||
dict(
|
||||
step=(i + 1) * interval,
|
||||
tstar=(i + 1) * 0.02,
|
||||
tstar_force=(i + 0.5) * 0.02,
|
||||
cd=3.2,
|
||||
cl=0.1,
|
||||
dp=2.0,
|
||||
mass=0.8,
|
||||
rho_min=0.99,
|
||||
rho_max=1.01,
|
||||
max_mach=0.03,
|
||||
flux_in=0.4,
|
||||
flux_out=0.4,
|
||||
rho_front=1.0,
|
||||
rho_back=1.0,
|
||||
)
|
||||
for i in range(250)
|
||||
]
|
||||
(dest / "summary.json").write_text(
|
||||
json.dumps(
|
||||
dict(
|
||||
case=case,
|
||||
**selection,
|
||||
collision="MRT",
|
||||
jet_scenario=jet_scenario,
|
||||
observation_mode=observation_mode,
|
||||
mode="pilot",
|
||||
status="pilot_complete",
|
||||
healthy=True,
|
||||
steps=250 * interval,
|
||||
history=rows,
|
||||
startup_seconds=1.0,
|
||||
wall_seconds=11.0,
|
||||
step_seconds=9.0,
|
||||
)
|
||||
)
|
||||
)
|
||||
(dest / "ready.json").write_text(
|
||||
json.dumps(
|
||||
dict(
|
||||
case=case,
|
||||
**selection,
|
||||
collision="MRT",
|
||||
jet_scenario=jet_scenario,
|
||||
observation_mode=observation_mode,
|
||||
mode="pilot",
|
||||
step=interval,
|
||||
runner_sha256=r.runner_hash(),
|
||||
all_runtime_and_observation_paths_exercised=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
return manifest
|
||||
|
||||
|
||||
def test_pilot_validation_and_calibration():
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
manifest = pilot_fixture(path)
|
||||
assert r.verify_pilot(path, manifest, "MRT", "window") == 3606.0
|
||||
|
||||
|
||||
def test_reject_malformed_pilot_receipts():
|
||||
from tempfile import TemporaryDirectory
|
||||
from copy import deepcopy
|
||||
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
manifest = pilot_fixture(path)
|
||||
receipt = path / "uniform/summary.json"
|
||||
valid = json.loads(receipt.read_text())
|
||||
mutations = [
|
||||
lambda s: s.update(case="refined"),
|
||||
lambda s: s.update(mode="full"),
|
||||
lambda s: s.update(healthy="true"),
|
||||
lambda s: s.update(steps=1),
|
||||
lambda s: s["history"].pop(),
|
||||
lambda s: s["history"][1].update(step=160),
|
||||
lambda s: s["history"][-1].update(tstar=float("nan")),
|
||||
lambda s: s["history"][0].update(cd=float("inf")),
|
||||
lambda s: s["history"][0].update(max_mach=0.3),
|
||||
lambda s: s["history"][0].update(tstar_force=0.02),
|
||||
lambda s: s.update(wall_seconds=0),
|
||||
]
|
||||
for mutate in mutations:
|
||||
bad = deepcopy(valid)
|
||||
mutate(bad)
|
||||
receipt.write_text(json.dumps(bad))
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
# Exact reviewer regression: previously accepted NaN endpoint.
|
||||
receipt.write_text(
|
||||
json.dumps(
|
||||
dict(
|
||||
status="pilot_complete",
|
||||
healthy=True,
|
||||
history=[{"tstar": float("nan")}],
|
||||
)
|
||||
)
|
||||
)
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
receipt.write_text(json.dumps(valid))
|
||||
(path / "uniform/ready.json").write_text("{}")
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
|
||||
|
||||
def test_strict_finite_json_and_deadline():
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
with TemporaryDirectory() as tmp:
|
||||
p = Path(tmp) / "bad.json"
|
||||
for data in ('{"x": NaN}', '{"x": Infinity}', '{"x": 1e999}'):
|
||||
p.write_text(data)
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.read_finite_json(p)
|
||||
with unittest.TestCase().assertRaises(TimeoutError):
|
||||
r.check_deadline(0)
|
||||
r.check_deadline(float("inf"))
|
||||
|
||||
|
||||
def test_pressure_probes_actual_body_mask():
|
||||
from CelerisLab.lbm.descriptors import SOLID
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
|
||||
_, pair, _, host = r.host_geometry(r.DEFAULT_SOURCE, h)
|
||||
fluid = (pair.fine_kind == ACTIVE) & (
|
||||
(host.flags.reshape(pair.fine_shape) & SOLID) == 0
|
||||
)
|
||||
p = dict(center=host.objects[0].geometry.center, radius=40.0, mean_speed=r.SPEED)
|
||||
assert h.pressure_probes(np.ones(pair.fine_shape), fluid, p)["dp"] == 0.0
|
||||
|
||||
|
||||
def test_matched_mrt_selection():
|
||||
for diameter in (40, 80):
|
||||
srt, sbody, sp = r.config(h, diameter)
|
||||
mrt, mbody, mp = r.config(h, diameter, "MRT")
|
||||
assert mrt["method"]["collision"] == "MRT"
|
||||
mrt["method"]["collision"] = "SRT"
|
||||
assert mrt == srt and mbody == sbody and mp == sp
|
||||
srt_plan, mrt_plan = r.plan(h), r.plan(h, "MRT")
|
||||
for key in ("root", "uniform", "bounds_root_edge", "finest_dx_m", "finest_dt_s"):
|
||||
assert srt_plan[key] == mrt_plan[key]
|
||||
assert "q2/6/8" in mrt_plan["outlet"] and "all nine" in srt_plan["outlet"]
|
||||
cfg, pair, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT")
|
||||
assert cfg.collision == "MRT" and pair.fine_shape == (240, 568)
|
||||
assert len(host.boundary_cells) > 0
|
||||
|
||||
|
||||
def test_reject_collision_receipts():
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
for rel in ("plan.json", "uniform/summary.json", "refined/summary.json",
|
||||
"uniform/ready.json", "refined/ready.json"):
|
||||
for invalid in ("SRT", None):
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
manifest = pilot_fixture(path)
|
||||
receipt = path / rel
|
||||
record = json.loads(receipt.read_text())
|
||||
if invalid is None:
|
||||
record.pop("collision")
|
||||
else:
|
||||
record["collision"] = invalid
|
||||
receipt.write_text(json.dumps(record))
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
|
||||
|
||||
def test_record_actual_checkout_separately():
|
||||
from source_provenance import checkout_provenance
|
||||
|
||||
actual = checkout_provenance(r.DEFAULT_SOURCE)
|
||||
assert len(actual["checkout_revision"]) == 40
|
||||
assert isinstance(actual["checkout_dirty"], bool)
|
||||
assert selected_manifest("MRT", "window")["production_origin_commit"].startswith("d930")
|
||||
|
||||
|
||||
def test_window_plan_preserves_physics():
|
||||
for collision in ("SRT", "MRT"):
|
||||
baseline = r.plan(h, collision)
|
||||
window = r.plan(h, collision, "window")
|
||||
assert baseline.pop("observation_mode") == "last_base_step"
|
||||
assert window.pop("observation_mode") == "window"
|
||||
assert baseline == window
|
||||
assert selected_manifest(collision, "window")["observation_mode"] == "window"
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_source(r.DEFAULT_SOURCE, "MRT", "unknown")
|
||||
|
||||
|
||||
def test_reject_observation_mode_receipts():
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
for mode, other in (("window", "last_base_step"), ("last_base_step", "window")):
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
manifest = pilot_fixture(path, mode)
|
||||
assert r.verify_pilot(path, manifest, "MRT", mode) == 3606.0
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", other)
|
||||
for rel in ("plan.json", "uniform/summary.json", "refined/summary.json",
|
||||
"uniform/ready.json", "refined/ready.json"):
|
||||
for invalid in (other, None):
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
manifest = pilot_fixture(path, mode)
|
||||
receipt = path / rel
|
||||
record = json.loads(receipt.read_text())
|
||||
if invalid is None:
|
||||
record.pop("observation_mode")
|
||||
else:
|
||||
record["observation_mode"] = invalid
|
||||
receipt.write_text(json.dumps(record))
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", mode)
|
||||
|
||||
|
||||
class ExchangeFixture:
|
||||
"""Synthetic interval ledger for orchestration tests, not a solver or GPU result."""
|
||||
def __init__(self):
|
||||
self.step_count = 11
|
||||
self.events = []
|
||||
self.begin = None
|
||||
self.override = {}
|
||||
|
||||
def start_exchange_window(self):
|
||||
self.begin = self.step_count
|
||||
self.events.append(("start", self.step_count))
|
||||
|
||||
def step(self, n):
|
||||
self.events.append(("step", n))
|
||||
self.step_count += n
|
||||
|
||||
def result(self, start, window):
|
||||
from CelerisLab.lbm.raw_exchange import summarize_exchange
|
||||
|
||||
total = sum(range(start + 1, self.step_count + 1))
|
||||
result = summarize_exchange([[total, -2*total, 0., 0.]], [0], [False],
|
||||
body_id=0, level=1, h=.5, time_end=self.step_count, window=window,
|
||||
reference_center=(80.5, 81.),
|
||||
**({"duration": self.step_count-start} if window == "window" else {}))
|
||||
result.update(self.override)
|
||||
return result
|
||||
|
||||
def read_exchange_window(self, body_id, *, level):
|
||||
assert body_id == 0 and level == 1
|
||||
self.events.append(("read_window", self.step_count))
|
||||
return self.result(self.begin, "window")
|
||||
|
||||
def read_raw_exchange(self, body_id, *, level, window):
|
||||
assert body_id == 0 and level == 1 and window == "last_base_step"
|
||||
self.events.append(("read_raw", self.step_count))
|
||||
return self.result(self.step_count-1, window)
|
||||
|
||||
|
||||
def test_window_interval_impulse_sampling_and_units():
|
||||
base, window = ExchangeFixture(), ExchangeFixture()
|
||||
for n in (80, 2000, 3): # Pilot/dense, early full, and final partial interval.
|
||||
begin = base.step_count
|
||||
a = r.advance_refined_interval(base, n, float("inf"), "last_base_step")
|
||||
w = r.advance_refined_interval(window, n, float("inf"), "window")
|
||||
np.testing.assert_array_equal(w, a)
|
||||
expected = sum(range(begin+1, begin+n+1))
|
||||
np.testing.assert_array_equal(w, [expected, -2*expected])
|
||||
den = .5 * r.SPEED**2 * 40
|
||||
np.testing.assert_array_equal(w/n/den, a/n/den)
|
||||
assert window.step_count == base.step_count == begin+n
|
||||
assert window.events[-3:] == [("start", begin), ("step", n), ("read_window", begin+n)]
|
||||
assert (begin+n/2)*r.SPEED/40 == (window.step_count-n/2)*r.SPEED/40
|
||||
assert not any(event[0] == "read_raw" for event in window.events)
|
||||
|
||||
|
||||
def test_window_contract_rejection():
|
||||
for key, value in (("units", "local"), ("window", "last_base_step"),
|
||||
("sample_count", 2), ("time_start", 0), ("time_end", 0), ("dt", 1.)):
|
||||
backend = ExchangeFixture()
|
||||
backend.override[key] = value
|
||||
with unittest.TestCase().assertRaisesRegex(RuntimeError, "window force"):
|
||||
r.advance_refined_interval(backend, 80, float("inf"), "window")
|
||||
backend = ExchangeFixture()
|
||||
with unittest.TestCase().assertRaises(TimeoutError):
|
||||
r.advance_refined_interval(backend, 80, 0, "window")
|
||||
assert not backend.events
|
||||
|
||||
|
||||
def test_public_jet_configuration_and_geometry():
|
||||
for diameter, center, radius in ((40, (80.5, 81), 20), (80, (160, 160.5), 40)):
|
||||
cfg, _, _ = r.config(h, diameter, "MRT", "fixed")
|
||||
assert not cfg["method"]["ddf_shifting"] and not cfg["method"]["les"]["enabled"]
|
||||
jets = r.public_jets(center, radius, "fixed")
|
||||
assert [j.amplitude for j in jets] == [.001, -.001]
|
||||
assert all(j.field.inward is False and j.profile == "parabolic" for j in jets)
|
||||
assert all(j.region.r_max == radius * 1.005 for j in jets)
|
||||
assert all(j.amplitude == 0 for j in r.public_jets(center, radius, "zero"))
|
||||
cfg, _, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT", "fixed")
|
||||
assert not cfg.ddf_shifting and len(host.boundary_cells) > 0
|
||||
assert r.public_jets((0, 0), 1, "none") == ()
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.config(h, 40, "SRT", "fixed")
|
||||
|
||||
|
||||
def test_public_jet_reversal_boundary_and_windows():
|
||||
from types import SimpleNamespace
|
||||
|
||||
calls = []
|
||||
handle = SimpleNamespace(set_jet_amplitude=lambda i, a: calls.append((i, a)))
|
||||
for diameter in (40, 80):
|
||||
boundary = round(250 * diameter / r.SPEED)
|
||||
assert r.jet_interval(handle, "fixed", boundary - 3, 80, diameter) == (3, None)
|
||||
switch = round(300 * diameter / r.SPEED)
|
||||
assert r.jet_interval(handle, "reverse", switch - 3, 80, diameter) == (3, None)
|
||||
assert not calls
|
||||
n, event = r.jet_interval(handle, "reverse", switch, 80, diameter)
|
||||
assert n == 80 and event["step"] == switch and event["tstar"] == 300
|
||||
assert calls == [(0, -.001), (1, .001)]
|
||||
calls.clear()
|
||||
assert r.jet_interval(handle, "reverse", switch + 80, 80, diameter) == (80, None)
|
||||
assert r.jet_interval(handle, "fixed", switch, 80, diameter) == (80, None)
|
||||
assert not calls
|
||||
rows = [dict(tstar=t, tstar_force=t - .5, cd=2.0, cl=-.1) for t in range(251, 306)]
|
||||
windows = r.jet_diagnostics(rows, "reverse")
|
||||
assert len(windows) == 6 and all(w["complete"] for w in windows)
|
||||
assert all(w["cd"]["mean"] == 2 and np.isclose(w["cl"]["mean"], -.1) for w in windows)
|
||||
assert windows[-1]["covered_tstar"] == 5
|
||||
with unittest.TestCase().assertRaisesRegex(ValueError, "crosses"):
|
||||
r.jet_diagnostics([dict(tstar=250.5, tstar_force=250, cd=1, cl=0)], "fixed")
|
||||
assert r.plan(h, "MRT", "window", "reverse")["modes"]["full_tstar"] == 305
|
||||
|
||||
|
||||
def test_public_jet_source_and_pilot_separation():
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
manifest = pilot_fixture(path, jet_scenario="fixed")
|
||||
assert manifest["source_capture"].startswith("current checkout")
|
||||
assert len(manifest["checkout_revision"]) == 40
|
||||
assert r.verify_pilot(path, manifest, "MRT", "window", "fixed") == 3606.0
|
||||
for scenario in ("none", "zero", "reverse"):
|
||||
with unittest.TestCase().assertRaisesRegex(ValueError, "Jet scenario"):
|
||||
r.verify_pilot(path, manifest, "MRT", "window", scenario)
|
||||
for relative in ("uniform/summary.json", "refined/ready.json"):
|
||||
receipt = path / relative
|
||||
record = json.loads(receipt.read_text())
|
||||
receipt.write_text(json.dumps(dict(record, jet_scenario="zero")))
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", "window", "fixed")
|
||||
receipt.write_text(json.dumps(record))
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_source(r.DEFAULT_SOURCE, "MRT", "last_base_step", "fixed")
|
||||
|
||||
|
||||
def test_cv_snapshot_common_units_and_window_endpoints():
|
||||
records = []
|
||||
for refined, shape, origin, diameter, impulse in (
|
||||
(True, (240, 568), (38., 22.), 40, [4., -8.]),
|
||||
(False, (330, 1761), (.25, .5), 80, [16., -32.]),
|
||||
(False, (658, 3521), (.375, .75), 160, [64., -128.]),
|
||||
):
|
||||
rho, ux, uy, fluid = np.ones(shape), np.full(shape, .01), np.zeros(shape), np.ones(shape, bool)
|
||||
begin, n = round(290*diameter/r.SPEED), round(.02*diameter/r.SPEED)
|
||||
record = r.cv_snapshot(rho, ux, uy, fluid, origin, refined, begin, n, diameter, impulse)
|
||||
records.append(record)
|
||||
np.testing.assert_allclose(record["outward_flux"], 0, atol=1e-12)
|
||||
assert r.cv_selected(begin, diameter, "full") and not r.cv_selected(begin-n, diameter, "full")
|
||||
end = round(300*diameter/r.SPEED)
|
||||
assert r.cv_selected(end, diameter, "full") and not r.cv_selected(end+n, diameter, "full")
|
||||
assert r.cv_selected(n, diameter, "pilot")
|
||||
assert sum(r.cv_selected(step, diameter, "full") for step in range(begin-n, end+n+1, n)) == 501
|
||||
for key in ("time_root", "interval_start_root", "interval_dt_root", "interval_impulse_root", "inventory"):
|
||||
for record in records[1:]:
|
||||
np.testing.assert_allclose(records[0][key], record[key], rtol=0, atol=1e-10)
|
||||
assert records[0]["interval_dt_root"] == 80 and np.isclose(records[0]["tstar"], 290.02)
|
||||
np.testing.assert_array_equal(records[0]["interval_impulse_root"], [4, -8])
|
||||
|
||||
|
||||
def test_cv_pilot_mode_and_helper_provenance():
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
manifest = r.verify_source(r.DEFAULT_SOURCE, "MRT", "window", "fixed", True)
|
||||
assert "tests/validation/run_jet_circle_cv.py" in manifest["sha256"]
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
old = pilot_fixture(path, jet_scenario="fixed")
|
||||
with unittest.TestCase().assertRaisesRegex(ValueError, "CV observation mode"):
|
||||
r.verify_pilot(path, old, "MRT", "window", "fixed", True)
|
||||
for relative in ("plan.json", "uniform/summary.json", "refined/summary.json",
|
||||
"uniform/ready.json", "refined/ready.json"):
|
||||
file = path / relative
|
||||
record = json.loads(file.read_text())
|
||||
record.update(observe_cv=True, cv_samples=250)
|
||||
if relative == "plan.json":
|
||||
record["source_manifest"] = manifest
|
||||
file.write_text(json.dumps(record))
|
||||
assert r.verify_pilot(path, manifest, "MRT", "window", "fixed", True) == 3606
|
||||
|
||||
|
||||
def test_compact_geometry_and_cv_stencils():
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
from CelerisLab.lbm.descriptors import SOLID
|
||||
|
||||
_, pair, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT", "fixed", "compact")
|
||||
assert pair.fine_shape == (168, 168) and tuple(pair.fine_origin) == (46, 46)
|
||||
fluid = (pair.fine_kind == ACTIVE) & ((host.flags.reshape(pair.fine_shape) & SOLID) == 0)
|
||||
rho, ux, uy = np.ones(pair.fine_shape), np.full(pair.fine_shape, .01), np.zeros(pair.fine_shape)
|
||||
record = r.cv_snapshot(rho, ux, uy, fluid, pair.fine_origin, True, 1160000, 80, 40, [1, 2], "compact")
|
||||
np.testing.assert_allclose(record["outward_flux"], 0, atol=1e-12)
|
||||
stencil = np.zeros(pair.fine_shape, bool)
|
||||
for rect in r.cv_contours(True, 40, "compact"):
|
||||
x0, y0, x1, y1 = np.rint((rect - np.tile(pair.fine_origin, 2))/.5).astype(int)
|
||||
stencil[y0-2:y1+2, x0-2:x0+2] = stencil[y0-2:y1+2, x1-2:x1+2] = True
|
||||
stencil[y0-2:y0+2, x0-2:x1+2] = stencil[y1-2:y1+2, x0-2:x1+2] = True
|
||||
assert fluid[stencil].all() and not stencil.ravel()[host.transfer_fine_cells].any()
|
||||
|
||||
|
||||
def test_d160_config_and_single_case_receipts():
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest.mock import patch
|
||||
|
||||
cfg, _, p = r.config(h, 160, "MRT", "fixed")
|
||||
assert (p["nx"], p["ny"], p["center"], p["radius"]) == (3521, 658, [320, 320.5], 80)
|
||||
assert p["viscosity"] == .016 and p["mean_speed"] == .01 and not cfg["method"]["ddf_shifting"]
|
||||
assert r.uniform_origin(160) == (.375, .75)
|
||||
np.testing.assert_array_equal(r.cv_contours(False, 160), r.CV_CONTOURS + [.125, 0, .125, 0])
|
||||
for scenario, expected_exit in (("fixed", 0), ("zero", 2)):
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
manifest = pilot_fixture(path, jet_scenario=scenario, case_selection="uniform", uniform_diameter=160)
|
||||
assert r.verify_pilot(path, manifest, "MRT", "window", scenario, False, "uniform", 160) == 1803
|
||||
for selection, diameter, placement in (("both", 160, "wide"), ("uniform", 80, "wide"), ("uniform", 160, "compact")):
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", "window", scenario, False, selection, diameter, placement)
|
||||
# Synthetic dispatch/result check: no CUDA imports or execution.
|
||||
output = path / "full"
|
||||
argv = ["runner", "--collision", "MRT", "--exchange-window", "--jet-scenario", scenario,
|
||||
"--case", "uniform", "--uniform-diameter", "160", "--mode", "full",
|
||||
"--pilot-results", str(path), "--output", str(output), "--max-wall-seconds", "10", "--gpu-gates-approved"]
|
||||
result = dict(case="uniform", healthy=True, periodic=scenario == "zero", jet_diagnostic_windows=[],
|
||||
status="driven_diagnostics_complete" if scenario == "fixed" else "periodic",
|
||||
reference_comparison=dict(reference_agreement=False))
|
||||
with patch.object(sys, "argv", argv), patch.dict(r.os.environ, CUDA_VISIBLE_DEVICES="mock-no-gpu"), \
|
||||
patch.object(r, "run_case", return_value=result) as run:
|
||||
assert r.main() == expected_exit and run.call_count == 1
|
||||
comparison = json.loads((output / "comparison.json").read_text())
|
||||
assert comparison["matched_case_pass"] is False and "refined_vs_uniform" not in comparison
|
||||
assert comparison["status"].startswith("single_case_")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
suite = unittest.TestSuite(
|
||||
unittest.FunctionTestCase(fn)
|
||||
for name, fn in sorted(globals().items())
|
||||
if name.startswith("test_")
|
||||
)
|
||||
result = unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
raise SystemExit(not result.wasSuccessful())
|
||||
@@ -142,8 +142,8 @@ class FixedPatchTests(unittest.TestCase):
|
||||
FixedPatch2D.from_config(LBMConfig(nx=nx, ny=ny, viscosity=.1), bounds)
|
||||
|
||||
def test_unsupported_backend_and_fine_relaxation_rejected(self):
|
||||
for kwargs in ({"collision": "TRT"}, {"collision": "MRT"}, {"streaming": "esopull"},
|
||||
{"ddf_shifting": True}, {"les_enabled": True},
|
||||
for kwargs in ({"collision": "TRT"}, {"streaming": "esopull"},
|
||||
{"les_enabled": True},
|
||||
{"store_precision": "FP16S"}, {"lattice_model": "D3Q19"},
|
||||
{"data_type": "FP64"}, {"omega_min": 1.0}):
|
||||
cfg = config(**kwargs)
|
||||
@@ -181,14 +181,14 @@ class FixedPatchSetTests(unittest.TestCase):
|
||||
ly, lx = p.fine_shape
|
||||
np.testing.assert_array_equal(s.fine_kind[y0:y0 + ly, x0:x0 + lx], p.fine_kind)
|
||||
gy, gx = np.divmod(p.ghost_fine.astype(np.int64), lx)
|
||||
np.testing.assert_array_equal(s.ghost_fine[cells:cells + len(gy)],
|
||||
(gy + y0) * fx + gx + x0)
|
||||
np.testing.assert_array_equal(s.ghost_coarse[cells:cells + len(gy)], p.ghost_coarse)
|
||||
ghosts = dict(zip(map(int, s.ghost_fine), map(int, s.ghost_coarse)))
|
||||
for f, c in zip((gy + y0) * fx + gx + x0, p.ghost_coarse):
|
||||
self.assertEqual(ghosts[int(f)], int(c))
|
||||
q, c = np.divmod(p.coalesce_fine.astype(np.int64), ly * lx)
|
||||
cy, cx = np.divmod(c, lx)
|
||||
np.testing.assert_array_equal(s.coalesce_fine[rows:rows + len(q)],
|
||||
q * fy * fx + (cy + y0) * fx + cx + x0)
|
||||
np.testing.assert_array_equal(s.coalesce_coarse[rows:rows + len(q)], p.coalesce_coarse)
|
||||
rows_by_target = dict(zip(map(int, s.coalesce_coarse), s.coalesce_fine))
|
||||
for target, children in zip(p.coalesce_coarse, q * fy * fx + (cy + y0) * fx + cx + x0):
|
||||
np.testing.assert_array_equal(rows_by_target[int(target)], children)
|
||||
rows, cells = rows + len(q), cells + len(gy)
|
||||
self.assertEqual((rows, cells), (len(s.coalesce_coarse), len(s.ghost_fine)))
|
||||
self.assertEqual(int((s.fine_kind == ACTIVE).sum()), 4 * int(s.covered.sum()))
|
||||
@@ -207,10 +207,12 @@ class FixedPatchSetTests(unittest.TestCase):
|
||||
np.testing.assert_array_equal(s.coalesce_coarse, p.coalesce_coarse)
|
||||
np.testing.assert_array_equal(s.ghost_coarse, p.ghost_coarse)
|
||||
|
||||
def test_gap_and_overlap_rejected(self):
|
||||
for gap in (0, 2, 3):
|
||||
with self.subTest(gap=gap), self.assertRaisesRegex(ValueError, "closer"):
|
||||
self.build(((4, 4, 8, 12), (8 + gap, 4, 12 + gap, 12)))
|
||||
def test_gap_and_overlap_use_dependency_checks(self):
|
||||
for gap in (-2, 0, 1, 2, 3):
|
||||
with self.subTest(gap=gap):
|
||||
pair = self.build(((4, 4, 8, 12), (8 + gap, 4, 12 + gap, 12)))
|
||||
refinement._transfer_footprint(pair)
|
||||
refinement._check_parent_pull_closure(pair)
|
||||
with self.assertRaises(ValueError):
|
||||
self.build(())
|
||||
with self.assertRaises(ValueError): # FixedPatch2D margin still applies
|
||||
@@ -226,7 +228,7 @@ class FixedPatchSetTests(unittest.TestCase):
|
||||
support[y0:y0 + p.fine_shape[0], x0:x0 + p.fine_shape[1]] = True
|
||||
np.testing.assert_array_equal(layout.owned, support)
|
||||
self.assertTrue(layout.owned.ravel()[s.ghost_fine.astype(np.intp)].all())
|
||||
with self.assertRaisesRegex(ValueError, "aligned"):
|
||||
with self.assertRaisesRegex(ValueError, "divide"):
|
||||
self.build(self.FACE).fine_layout((5, 4))
|
||||
for block in ((0, 4), (4, 0), (-4, 4), (4.0, 4), (True, 4), (4,), (4, 4, 4), (np.bool_(1), 4)):
|
||||
with self.subTest(block=block), self.assertRaisesRegex(ValueError, "positive integers"):
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Host guards and source contract for MRT reuse in refined/blocked stepping (no CUDA)."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.refinement import FixedPatch2D
|
||||
|
||||
KERNELS = Path(__file__).resolve().parents[2] / "src/CelerisLab/lbm/kernels"
|
||||
BOUNDS = (8, 6, 16, 14)
|
||||
|
||||
|
||||
class RefinementMRTHost(unittest.TestCase):
|
||||
def test_patch_accepts_implemented_models(self):
|
||||
for collision in ("SRT", "MRT"):
|
||||
with self.subTest(collision=collision):
|
||||
FixedPatch2D.from_config(LBMConfig(nx=24, ny=20, viscosity=.1, collision=collision), BOUNDS)
|
||||
|
||||
def test_patch_rejects_unimplemented(self):
|
||||
for options in (dict(collision="TRT"), dict(collision="MRT", les_enabled=True)):
|
||||
with self.subTest(options=options), self.assertRaises(ValueError):
|
||||
FixedPatch2D.from_config(LBMConfig(nx=24, ny=20, viscosity=.1, **options), BOUNDS)
|
||||
|
||||
def test_srt_statements_and_guards_preserved(self):
|
||||
"""MRT is an additive compile-time branch; SRT statements and other guards remain."""
|
||||
blocks = (KERNELS / "blocks.cu").read_text()
|
||||
channel = (KERNELS / "step/block_channel.cu").read_text()
|
||||
block_collision = (KERNELS / "operators/block_collision.cuh").read_text()
|
||||
self.assertIn('#include "operators/block_collision.cuh"', blocks)
|
||||
self.assertEqual(blocks.count("block_collide(f);"), 1)
|
||||
refinement = (KERNELS / "refinement.cu").read_text()
|
||||
for text in ("compute_feq_from_momentum(drho, jx, jy, ux, uy, feq);",
|
||||
"compute_feq(rho, ux, uy, feq);",
|
||||
"collide_srt_no_force(f, feq, d_params.omega);"):
|
||||
self.assertIn(text, block_collision)
|
||||
self.assertIn("collide_srt(f, feq, forcing, omega);", channel)
|
||||
self.assertIn("collide_srt_no_force(f, feq, d_params.omega);", refinement)
|
||||
for source in (blocks, refinement):
|
||||
self.assertIn("(COLLISION_MODEL != 0 && COLLISION_MODEL != 2)", source)
|
||||
self.assertIn("USE_LES", source)
|
||||
self.assertIn("STORE_PRECISION != 0", source)
|
||||
self.assertIn('#if COLLISION_MODEL == 2\n#include "operators/collision_mrt.cuh"\n#endif', source)
|
||||
self.assertIn("USE_DDF_SHIFTING ||", refinement) # dense/mixed shifted stays rejected
|
||||
# One shared unforced MRT body; each blocked/patch path calls it exactly once.
|
||||
mrt = (KERNELS / "operators/collision_mrt.cuh").read_text()
|
||||
self.assertEqual(mrt.count("void collide_mrt_unforced("), 1)
|
||||
for source in (blocks + "\n" + block_collision, channel, refinement):
|
||||
self.assertEqual(source.count("collide_mrt_unforced(f);"), 1)
|
||||
self.assertNotIn("collide_mrt_no_force(", source)
|
||||
self.assertNotIn("compute_rho_u(f, rho, ux, uy);\n collide_mrt", source)
|
||||
self.assertNotRegex(source, r"compute_rho_u\(f, rho, ux, uy\);\s*collide_mrt")
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -173,8 +173,10 @@ def bodies_and_jets(speed=.002):
|
||||
return bodies,jets
|
||||
|
||||
|
||||
KW=dict(base_block=(4,4),fine_block=(4,4),child_block=(4,4),
|
||||
child_rectangles_in_parent=CHILDREN)
|
||||
def levels(parents=PARENTS,children=CHILDREN,blocks=((4,4),)*3,objects=((),(),()),jets=((),(),())):
|
||||
"""Level-list constructor keywords for the L0 set, L1 set and L2 set used here."""
|
||||
return dict(refinements=[parents,children],level_blocks=list(blocks),
|
||||
level_objects=list(objects),level_jets=list(jets))
|
||||
|
||||
|
||||
def mock_backend(*, children=CHILDREN, bodies=False, fail_compile=False, fail_upload=False):
|
||||
@@ -201,9 +203,8 @@ def mock_backend(*, children=CHILDREN, bodies=False, fail_compile=False, fail_up
|
||||
module.cuda.memcpy_htod.side_effect=upload
|
||||
objects,jets=bodies_and_jets() if bodies else ((),())
|
||||
try:
|
||||
run=module.FixedPatchStepper(CFG,PARENTS,context=context,
|
||||
child_objects=objects,child_jets=jets,
|
||||
**dict(KW,child_rectangles_in_parent=children))
|
||||
run=module.FixedPatchStepper(CFG,context=context,
|
||||
**levels(children=children,objects=((),(),objects),jets=((),(),jets)))
|
||||
except RuntimeError:
|
||||
for allocation in allocations:
|
||||
allocation.free.assert_called_once_with()
|
||||
@@ -214,28 +215,29 @@ def mock_backend(*, children=CHILDREN, bodies=False, fail_compile=False, fail_up
|
||||
class ConstructorAndSchedule(unittest.TestCase):
|
||||
def test_invalid_inputs_fail_before_context_or_allocations(self):
|
||||
context=Mock()
|
||||
cases=[dict(child=(8,8,16,16)),dict(child_rectangles_in_parent=[]),
|
||||
dict(child_rectangles_in_parent=[(16,16,32)]),
|
||||
dict(child_rectangles_in_parent=[(16.5,16,32,40)]),
|
||||
dict(child_rectangles_in_parent=[(16,16,32,24),(16,27,32,40)]),
|
||||
dict(child_rectangles_in_parent=[(40,16,48,40)]),
|
||||
dict(child_rectangles_in_parent=[(8,16,32,40)]),
|
||||
dict(child_block=None),dict(fine_block=None),dict(base_block=None),
|
||||
dict(child_block=(5,4)),dict(fine_block=(5,4)),
|
||||
dict(child_block=(4,)),dict(child_rectangles_in_parent=(16,16,32,40))]
|
||||
b=(4,4)
|
||||
cases=[dict(children=(8,8,16,16)),dict(children=[]),
|
||||
dict(children=[(16,16,32)]),
|
||||
dict(children=[(16.5,16,32,40)]),
|
||||
dict(children=[(16,16,32,24),(16,27,32,55)]),
|
||||
dict(children=[(40,16,48,40)]),
|
||||
dict(children=[(8,16,32,40)]),
|
||||
dict(blocks=(b,b,None)),dict(blocks=(b,None,b)),dict(blocks=(None,b,b)),
|
||||
dict(blocks=(b,b,(5,4))),dict(blocks=(b,(5,4),b)),
|
||||
dict(blocks=(b,b,(4,))),dict(children=(16,16,32,40))]
|
||||
for changes in cases:
|
||||
with self.subTest(changes=changes),self.assertRaises((ValueError,TypeError)):
|
||||
host.FixedPatchStepper(CFG,PARENTS,context=context,**dict(KW,**changes))
|
||||
with self.assertRaisesRegex(ValueError,'closer'):
|
||||
host.FixedPatchStepper(CFG,[(4,4,20,28),(23,4,44,28)],context=context,**KW)
|
||||
host.FixedPatchStepper(CFG,context=context,**levels(**changes))
|
||||
with self.assertRaisesRegex(ValueError,'margin'):
|
||||
host.FixedPatchStepper(CFG,context=context,**levels(parents=[(1,4,20,28),(23,4,44,28)]))
|
||||
objects,jets=bodies_and_jets()
|
||||
with self.assertRaisesRegex(ValueError,'only on L2'):
|
||||
host.FixedPatchStepper(CFG,PARENTS,context=context,fine_objects=objects,**KW)
|
||||
with self.assertRaisesRegex(ValueError,'only on the finest level'):
|
||||
host.FixedPatchStepper(CFG,context=context,**levels(objects=((),objects,())))
|
||||
from CelerisLab.body.jets import Jet,Tube,Parallel
|
||||
spanning=Jet(Tube((10.5,14.),0.,25.,.3),Parallel(0.),amplitude=0.)
|
||||
with self.assertRaisesRegex(ValueError,'one jet'):
|
||||
host.FixedPatchStepper(CFG,PARENTS,context=context,child_objects=objects,
|
||||
child_jets=[spanning],**KW)
|
||||
host.FixedPatchStepper(CFG,context=context,
|
||||
**levels(objects=((),(),objects),jets=((),(),[spanning])))
|
||||
context.activate.assert_not_called()
|
||||
host.cuda.mem_alloc.assert_not_called()
|
||||
|
||||
@@ -313,21 +315,23 @@ class ConstructorAndSchedule(unittest.TestCase):
|
||||
|
||||
|
||||
|
||||
class InitialLevels(unittest.TestCase):
|
||||
"""initialize() entry adapter: positional per-level arrays or the legacy names, never both."""
|
||||
class RemovedLegacyAPI(unittest.TestCase):
|
||||
"""The pre-level-list constructor/initialize forms are gone and fail loudly."""
|
||||
|
||||
def test_forms(self):
|
||||
a, b, c = object(), object(), object()
|
||||
f = host._initial_levels
|
||||
self.assertEqual(f((a, b), None, None, None), (a, b))
|
||||
self.assertEqual(f((a, b, c), None, None, None), (a, b, c))
|
||||
self.assertEqual(f((a, b), None, None, c), (a, b, c)) # old initialize(c, f, child_g=g)
|
||||
self.assertEqual(f((), a, b, None), (a, b))
|
||||
self.assertEqual(f((), a, b, c), (a, b, c))
|
||||
for args in (((a,), None, None, c), ((a, b, c), None, None, c), ((a,), a, b, None),
|
||||
((), a, None, None), ((), None, b, c)):
|
||||
with self.subTest(args=args), self.assertRaises(TypeError):
|
||||
f(*args)
|
||||
def test_removed_constructor_forms_raise_type_error(self):
|
||||
import inspect
|
||||
context = Mock()
|
||||
with self.assertRaises(TypeError): # positional bounds
|
||||
host.FixedPatchStepper(CFG, PARENTS, context=context)
|
||||
removed = dict(bounds=PARENTS, fine_block=(4, 4), base_block=(4, 4), fine_objects=(),
|
||||
jets=(), child=(8, 8, 16, 16), child_block=(4, 4), child_objects=(),
|
||||
child_jets=(), child_rectangles_in_parent=CHILDREN)
|
||||
for name, value in removed.items():
|
||||
with self.subTest(keyword=name), self.assertRaises(TypeError):
|
||||
host.FixedPatchStepper(CFG, context=context, **levels(), **{name: value})
|
||||
context.activate.assert_not_called()
|
||||
self.assertEqual(list(inspect.signature(host.FixedPatchStepper.initialize).parameters),
|
||||
["self", "level_g"]) # no coarse_g/fine_g/child_g
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
|
||||
@@ -67,7 +67,7 @@ class RuntimeChildRemap(unittest.TestCase):
|
||||
run._status = status
|
||||
with self.assertRaises(RuntimeError): run.remap_child(B)
|
||||
run._status = 'ready'
|
||||
for bad in ([], [(40,16,48,40)], [(16,16,32,24),(16,27,32,40)]):
|
||||
for bad in ([], [(40,16,48,40)], [(16,16,32,24),(16,27,32,55)]):
|
||||
with self.assertRaises(ValueError): run.remap_child(bad)
|
||||
hosts, pairs = run._boundary_hosts, run.hierarchy.pairs
|
||||
for attribute, value in (('_wall_aware', True),
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Actual-header CPU algebra, not CUDA arithmetic/performance evidence."""
|
||||
import numpy as np
|
||||
from .test_numerical_operators import compile_run
|
||||
|
||||
|
||||
def test_retained_density_and_shifted_planar_headers(tmp_path):
|
||||
rows=compile_run(tmp_path,'shifted_block_operators.cpp',{})
|
||||
cx=np.array([0,1,-1,0,0,1,-1,1,-1]);cy=np.array([0,0,0,1,-1,1,-1,-1,1])
|
||||
w=np.array([4/9]+[1/9]*4+[1/36]*4)
|
||||
it=iter(rows)
|
||||
for header in it:
|
||||
kind,*numbers=header.split()
|
||||
a=np.fromstring(next(it),sep=' ',dtype=np.float32).astype(float)
|
||||
if kind=='E':
|
||||
delta,u,v=np.array(numbers,dtype=np.float32).astype(float)
|
||||
rho=float(np.float32(1+delta));cu=3*(cx*u+cy*v)
|
||||
expected=w*(delta+rho*(cu+.5*cu*cu-1.5*(u*u+v*v)))
|
||||
np.testing.assert_allclose(a,expected,rtol=3e-6,atol=1e-10)
|
||||
if u==0:
|
||||
np.testing.assert_allclose(a.sum(),delta,rtol=5e-8,atol=0)
|
||||
if delta: assert np.any(a!=0)
|
||||
elif kind=='J':
|
||||
delta,jx,jy,ux,uy=np.array(numbers,dtype=np.float32).astype(float)
|
||||
jd=cx*jx+cy*jy;ud=cx*ux+cy*uy
|
||||
expected=w*(delta+3*jd+4.5*jd*ud-1.5*(jx*ux+jy*uy))
|
||||
np.testing.assert_allclose(a,expected,rtol=3e-6,atol=2e-9)
|
||||
actual=np.array([a.sum(),a@cx,a@cy,a@(cx*cx),a@(cx*cy),a@(cy*cy)])
|
||||
target=np.array([delta,jx,jy,delta/3+jx*ux,jx*uy,delta/3+jy*uy])
|
||||
np.testing.assert_allclose(actual,target,atol=4e-9,rtol=3e-6)
|
||||
if jx==0 and delta: assert np.any(a!=0)
|
||||
else:
|
||||
mode=int(numbers[0]);rho,u,v=np.array(numbers[1:],dtype=np.float32).astype(float)
|
||||
b=np.fromstring(next(it),sep=' ',dtype=np.float32).astype(float)
|
||||
known=cy!=(1 if mode==0 else -1)
|
||||
np.testing.assert_array_equal(a[known],b[known])
|
||||
physical_rho=1+b.sum()
|
||||
if mode==2:
|
||||
assert abs(physical_rho-rho)<2e-8
|
||||
assert abs(b@cx)<2e-9
|
||||
else:
|
||||
np.testing.assert_allclose([b@cx,b@cy],physical_rho*np.array([u,v]),atol=3e-9,rtol=0)
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Shifted velocity Jets: bounded opt-in and host lifecycle, no CUDA numerics."""
|
||||
from dataclasses import replace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import numpy as np
|
||||
from CelerisLab.body.jets import Jet, Tube, Parallel
|
||||
from CelerisLab.body.objects import SurfaceActuation
|
||||
from CelerisLab.lbm.synchronized_remap import RemapTopology
|
||||
from CelerisLab.lbm.refinement import FixedPatchSet2D
|
||||
from CelerisLab.lbm.population_storage import _StoredPopulations
|
||||
from tests.unit import test_refinement_three_level_multipatch as backend
|
||||
from tests.unit.test_pinned_body_remap import A, B, body, jet, mesh
|
||||
from tests.unit.test_shifted_storage import shifted_fields
|
||||
|
||||
CFG=replace(backend.CFG,ddf_shifting=True)
|
||||
|
||||
|
||||
def topology(objects, jets, *, enabled=False, cfg=CFG):
|
||||
return RemapTopology.from_config(cfg,FixedPatchSet2D.from_config(cfg,backend.PARENTS),A,
|
||||
child_objects=objects,child_jets=jets,_shifted_blocked=enabled)
|
||||
|
||||
|
||||
def ready():
|
||||
objects,jets=(body(),),(jet(),)
|
||||
with patch.object(backend,'CFG',CFG),patch.object(backend,'bodies_and_jets',return_value=(objects,jets)):
|
||||
run,module,allocations=backend.mock_backend(children=A,bodies=True)
|
||||
storage={}
|
||||
module.cuda.memcpy_htod.side_effect=lambda p,a:storage.__setitem__(id(p),np.array(a,copy=True))
|
||||
module.cuda.memcpy_dtoh.side_effect=lambda a,p:np.copyto(a,storage[id(p)])
|
||||
run._initialize_raw(_StoredPopulations(shifted_fields(mesh(A,objects,jets)),True))
|
||||
run._step_count=7
|
||||
return run,module,allocations,storage,objects
|
||||
|
||||
|
||||
class ShiftedJets(unittest.TestCase):
|
||||
def test_default_guard_and_storage_neutral_host_rows(self):
|
||||
with self.assertRaisesRegex(ValueError,'unshifted'):
|
||||
topology((body(),),(jet(),))
|
||||
shifted=topology((body(),),(jet(),),enabled=True)
|
||||
unshifted=topology((body(),),(jet(),),cfg=backend.CFG)
|
||||
np.testing.assert_array_equal(shifted.surface_velocity,unshifted.surface_velocity)
|
||||
self.assertEqual(shifted.surface_velocity.dtype,np.float32)
|
||||
legacy=body(SurfaceActuation(.7,.85,.002))
|
||||
with self.assertRaisesRegex(ValueError,'unshifted'):
|
||||
topology((legacy,),(),enabled=True)
|
||||
|
||||
def test_rejections_before_device_allocation(self):
|
||||
module=backend.host_module()
|
||||
moving=body();moving.state.omega=.01
|
||||
cases=[((moving,),(jet(),)),((body(),),(jet(),jet())),
|
||||
((body(),),(Jet(Tube((100.,100.),0.,1.,.1),Parallel(0.)),)),
|
||||
((body(center=(8.,14.)),),(jet(),)),
|
||||
((body(),body(center=(34.,14.))),
|
||||
(Jet(Tube((10.,14.),0.,26.,.3),Parallel(0.)),))]
|
||||
for objects,jets in cases:
|
||||
with self.subTest(objects=len(objects),jets=len(jets)),self.assertRaises(ValueError):
|
||||
module.FixedPatchStepper(CFG,context=None,refinements=[backend.PARENTS,A],
|
||||
level_blocks=[(4,4)]*3,level_objects=[(),(),objects],level_jets=[(),(),jets])
|
||||
for blocks in ((None,None,None),((4,4),None,(4,4))):
|
||||
with self.assertRaises(ValueError):
|
||||
module.FixedPatchStepper(CFG,context=None,refinements=[backend.PARENTS,A],
|
||||
level_blocks=list(blocks),level_objects=[(),(),(body(),)],
|
||||
level_jets=[(),(),(jet(),)])
|
||||
module.cuda.mem_alloc.assert_not_called()
|
||||
module.compiler.compile_kernel.assert_not_called()
|
||||
|
||||
def test_switch_upload_only_then_raw_remap_keeps_live_velocity(self):
|
||||
run,module,allocations,storage,objects=ready();clone=None
|
||||
try:
|
||||
boundary=run._level_boundaries[2]
|
||||
before=run._download_raw()
|
||||
original=boundary.tables['surface_velocity'].copy()
|
||||
module.cuda.memcpy_htod.reset_mock()
|
||||
run.set_jet_amplitude(0,-jet().amplitude,level=2)
|
||||
module.cuda.memcpy_htod.assert_called_once()
|
||||
self.assertIs(module.cuda.memcpy_htod.call_args.args[0],boundary.device['surface_velocity'])
|
||||
np.testing.assert_array_equal(boundary.tables['surface_velocity'],-original)
|
||||
self.assertEqual(run.step_count,7)
|
||||
for a,b in zip(before.values,run._download_raw().values):self.assertEqual(a.tobytes(),b.tobytes())
|
||||
clone=run.remap_child(B,child_objects=objects)
|
||||
self.assertEqual(clone._level_jets[2][0].amplitude,-jet().amplitude)
|
||||
np.testing.assert_array_equal(clone._level_boundaries[2].tables['surface_velocity'],
|
||||
boundary.tables['surface_velocity'])
|
||||
self.assertEqual(clone.step_count,7)
|
||||
saved=run._download_raw()
|
||||
with self.assertRaises(ValueError):run.remap_child(B,child_objects=(body(center=(12.1,14.)),))
|
||||
for a,b in zip(saved.values,run._download_raw().values):self.assertEqual(a.tobytes(),b.tobytes())
|
||||
finally:
|
||||
if clone is not None:clone.close()
|
||||
run.close()
|
||||
|
||||
def test_invalid_amplitude_does_not_upload_or_commit(self):
|
||||
run,module,_,_,_=ready()
|
||||
try:
|
||||
before=run._level_boundaries[2].tables['surface_velocity'].copy()
|
||||
module.cuda.memcpy_htod.reset_mock()
|
||||
for amplitude in (float('nan'),float('inf'),1.):
|
||||
with self.assertRaises(ValueError):run.set_jet_amplitude(0,amplitude,level=2)
|
||||
module.cuda.memcpy_htod.assert_not_called()
|
||||
np.testing.assert_array_equal(before,run._level_boundaries[2].tables['surface_velocity'])
|
||||
finally:run.close()
|
||||
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -0,0 +1,187 @@
|
||||
"""CPU host contracts; mocked CUDA executes real lifecycle, not numerical kernels."""
|
||||
import ast
|
||||
import types
|
||||
from pathlib import Path
|
||||
from contextlib import nullcontext
|
||||
from dataclasses import replace
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
import numpy as np
|
||||
from CelerisLab.lbm.population_storage import (_StoredPopulations, _WEIGHTS,
|
||||
_encode_physical, _decode_stored)
|
||||
from CelerisLab.lbm.synchronized_remap import _remap_stored, remap_synchronized
|
||||
from tests.unit.test_synchronized_remap import topology, A, B
|
||||
from tests.unit import test_refinement_three_level_multipatch as backend
|
||||
|
||||
|
||||
def shifted_fields(mesh):
|
||||
rng = np.random.default_rng(129)
|
||||
arrays = []
|
||||
for leaf in mesh.leaf_masks:
|
||||
a = rng.normal(0, 1e-7, (9, *leaf.shape)).astype(np.float32)
|
||||
a[:, ~leaf] = np.nan
|
||||
y, x = np.nonzero(leaf)
|
||||
a[0, y[:6], x[:6]] = np.array([0., -0., 2**-80, -2**-80, 2**-140, -2**-140], np.float32)
|
||||
arrays.append(a)
|
||||
return tuple(arrays)
|
||||
|
||||
|
||||
def remap(old, new, arrays, shifted=True):
|
||||
return _remap_stored(old, new, _StoredPopulations(arrays, shifted), shifted=True,
|
||||
base_count=7, level_steps=(7, 14, 28), phase='postcollision_g')
|
||||
|
||||
|
||||
def ready():
|
||||
with patch.object(backend, 'CFG', replace(backend.CFG, ddf_shifting=True)):
|
||||
run, module, allocations = backend.mock_backend(children=A)
|
||||
storage = {}
|
||||
module.cuda.memcpy_htod.side_effect = lambda p, a: storage.__setitem__(id(p), np.array(a, copy=True))
|
||||
module.cuda.memcpy_dtoh.side_effect = lambda a, p: np.copyto(a, storage[id(p)])
|
||||
run._initialize_raw(_StoredPopulations(shifted_fields(topology(A)), True))
|
||||
run._step_count = 7
|
||||
return run, module, allocations, storage
|
||||
|
||||
|
||||
class ShiftedStorage(unittest.TestCase):
|
||||
def test_codec_preserves_sub_fp32_physical_perturbation(self):
|
||||
physical = np.broadcast_to(_WEIGHTS, (9, 2, 2)).copy()
|
||||
physical[1] += 2**-35
|
||||
encoded = _encode_physical(physical, True)
|
||||
self.assertTrue((encoded[1] == np.float32(2**-35)).all())
|
||||
np.testing.assert_array_equal(_decode_stored(encoded, True), physical)
|
||||
self.assertEqual(_decode_stored(encoded, True).dtype, np.float64)
|
||||
np.testing.assert_array_equal(_encode_physical(_WEIGHTS, True), np.zeros((9,1,1), np.float32))
|
||||
self.assertEqual(_encode_physical(physical, False).dtype, np.float32)
|
||||
|
||||
def test_actual_fill_loop_bitwise_copy_and_single_rounding_restriction(self):
|
||||
old, new = topology(A), topology(B)
|
||||
arrays = shifted_fields(old)
|
||||
saved = [a.tobytes() for a in arrays]
|
||||
out = remap(old, new, arrays)
|
||||
self.assertTrue(out.shifted)
|
||||
for a, b, om, nm, before in zip(arrays, out.g, old.leaf_masks, new.leaf_masks, saved):
|
||||
self.assertEqual(a[:, om & nm].tobytes(), b[:, om & nm].tobytes())
|
||||
self.assertTrue(np.isnan(b[:, ~nm]).all())
|
||||
self.assertEqual(a.tobytes(), before)
|
||||
self.assertFalse(np.shares_memory(a,b))
|
||||
y,x = np.nonzero(new.leaf_masks[2] & ~old.leaf_masks[2])
|
||||
self.assertEqual(out.g[2][:,y,x].tobytes(), arrays[1][:,y//2,x//2].tobytes())
|
||||
y,x = np.nonzero(new.leaf_masks[1] & ~old.leaf_masks[1])
|
||||
mean = sum(arrays[2][:,2*y+dy,2*x+dx].astype(np.float64)*.25
|
||||
for dy in (0,1) for dx in (0,1))
|
||||
self.assertEqual(out.g[1][:,y,x].tobytes(), mean.astype(np.float32).tobytes())
|
||||
def integral(mesh, values):
|
||||
return sum(a[:,m].sum(axis=1,dtype=np.float64)/4**l
|
||||
for l,(a,m) in enumerate(zip(values,mesh.leaf_masks)))
|
||||
cast_error = abs(mean.astype(np.float32).astype(np.float64)-mean).sum(axis=1)/4
|
||||
self.assertTrue(np.all(abs(integral(old, arrays)-integral(new,out.g)) <= cast_error+2e-18))
|
||||
|
||||
def test_shifted_density_and_tag_validation(self):
|
||||
old,new = topology(A),topology(B)
|
||||
a = tuple(np.where(m[None], np.float32(-.001), np.float32(np.nan)).repeat(9,axis=0)
|
||||
for m in old.leaf_masks) # FP32 under NumPy 1 and 2 promotion rules
|
||||
remap(old,new,a)
|
||||
with self.assertRaisesRegex(ValueError,'representation'): remap(old,new,a,False)
|
||||
with self.assertRaisesRegex(ValueError,'FP32'): remap(old,new,tuple(x.astype(np.float64) for x in a))
|
||||
a[0][:,old.leaf_masks[0]] = -1
|
||||
with self.assertRaisesRegex(ValueError,'positive density'): remap(old,new,a)
|
||||
rest = tuple(np.where(m[None],np.float32(0),np.float32(np.nan)).repeat(9,axis=0)
|
||||
for m in old.leaf_masks)
|
||||
remap(old,new,rest)
|
||||
with self.assertRaisesRegex(ValueError,'positive density'):
|
||||
remap_synchronized(old,new,rest,base_count=7,level_steps=(7,14,28),phase='postcollision_g')
|
||||
|
||||
def test_runtime_remap_bypasses_public_codec_and_preserves_both_buffers(self):
|
||||
run,module,allocations,storage = ready()
|
||||
clone = None
|
||||
try:
|
||||
before = run._download_raw()
|
||||
expected = remap(topology(A),topology(B),before.values)
|
||||
with patch.object(run,'download',side_effect=AssertionError('public decode used')):
|
||||
clone = run.remap_child(B)
|
||||
self.assertEqual((run.step_count,clone.step_count),(7,7))
|
||||
self.assertFalse(clone._exchange_valid)
|
||||
for level, want, got in zip(clone._levels,expected.g,clone._download_raw().values):
|
||||
self.assertEqual(want.tobytes(),got.tobytes())
|
||||
for pointer in (level.current,level.spare):
|
||||
np.testing.assert_array_equal(storage[id(pointer)],level.layout.to_blocked(want))
|
||||
for a,b in zip(before.values,run._download_raw().values): self.assertEqual(a.tobytes(),b.tobytes())
|
||||
for a in clone.download(): self.assertEqual(a.dtype,np.float64)
|
||||
finally:
|
||||
if clone is not None: clone.close()
|
||||
run.close()
|
||||
|
||||
def test_runtime_public_encode_and_raw_mismatch_fail_before_upload(self):
|
||||
run,module,_,_ = ready()
|
||||
try:
|
||||
physical = tuple(np.broadcast_to(_WEIGHTS, level.shape).copy() for level in run._levels)
|
||||
for a in physical: a[1] += 2**-35
|
||||
run.initialize(*physical)
|
||||
for a,mask in zip(run._download_raw().values,run._leaf):
|
||||
self.assertTrue((a[1,mask] == np.float32(2**-35)).all())
|
||||
self.assertEqual(run.step_count,0)
|
||||
module.cuda.memcpy_htod.reset_mock()
|
||||
with self.assertRaisesRegex(ValueError,'representation'):
|
||||
run._initialize_raw(_StoredPopulations(run._download_raw().values,False))
|
||||
module.cuda.memcpy_htod.assert_not_called()
|
||||
run._level_cfgs[0] = replace(run._level_cfgs[0],ddf_shifting=False)
|
||||
with self.assertRaisesRegex(ValueError,'mixed'): run._download_raw()
|
||||
finally: run.close()
|
||||
|
||||
def test_dense_or_mixed_shifted_rejected_before_compilation(self):
|
||||
module=backend.host_module()
|
||||
cfg=replace(backend.CFG,ddf_shifting=True)
|
||||
for blocks in ([None,None],[(4,4),None],[None,(4,4)]):
|
||||
with self.assertRaisesRegex(ValueError,'all-blocked|blocked refined levels below'):
|
||||
module.FixedPatchStepper(cfg,context=None,refinements=[(4,4,20,28)],
|
||||
level_blocks=blocks)
|
||||
module.compiler.compile_kernel.assert_not_called()
|
||||
|
||||
def test_uniform_block_codec_and_raw_snapshot(self):
|
||||
path = Path(backend.r.__file__).with_name('block_stepper.py')
|
||||
tree = ast.parse(path.read_text())
|
||||
tree.body = [n for n in tree.body if not (
|
||||
isinstance(n, ast.Import) and any(a.name == 'pycuda.driver' for a in n.names)
|
||||
or isinstance(n, ast.ImportFrom) and n.module == 'cuda')]
|
||||
module = types.ModuleType('CelerisLab.lbm._test_block_host')
|
||||
module.__package__ = 'CelerisLab.lbm'
|
||||
cuda, compiler = Mock(), Mock()
|
||||
module.__dict__.update(cuda=cuda,compiler=compiler)
|
||||
exec(compile(tree,str(path),'exec'),module.__dict__)
|
||||
cuda.mem_get_info.return_value=(10**9,10**9)
|
||||
cuda.mem_alloc.side_effect=lambda size: Mock()
|
||||
gpu=Mock();gpu.get_global.return_value=(Mock(),20)
|
||||
compiler.load_module.return_value=gpu
|
||||
context=Mock(activate=nullcontext,sm_arch='sm_70')
|
||||
layout=module.BlockLayout2D.build(16,16,4,4)
|
||||
cfg=replace(backend.CFG,nx=16,ny=16,ddf_shifting=True)
|
||||
run=module.PeriodicBlockStepper(cfg,layout,context=context)
|
||||
storage={}
|
||||
cuda.memcpy_htod.side_effect=lambda p,a: storage.__setitem__(id(p),np.array(a,copy=True))
|
||||
cuda.memcpy_dtoh.side_effect=lambda a,p: np.copyto(a,storage[id(p)])
|
||||
try:
|
||||
physical=np.broadcast_to(_WEIGHTS,(9,16,16)).copy();physical[1]+=2**-35
|
||||
run.initialize(physical)
|
||||
raw=run._download_raw()
|
||||
self.assertTrue(raw.shifted)
|
||||
self.assertEqual(len(raw.values),1)
|
||||
self.assertTrue((raw.values[0][1]==np.float32(2**-35)).all())
|
||||
np.testing.assert_array_equal(run.download(),physical)
|
||||
finally: run.close()
|
||||
|
||||
def test_passive_shifted_body_raw_remap_preserves_support(self):
|
||||
from tests.unit.test_pinned_body_remap import mesh, body, A as rects, B as moved
|
||||
old,new=mesh(rects,(body(),)),mesh(moved,(body(),))
|
||||
result=remap(old,new,shifted_fields(old))
|
||||
self.assertTrue(result.shifted)
|
||||
self.assertTrue(np.isnan(result.g[2][:,~new.leaf_masks[2]]).all())
|
||||
changed=mesh(moved,(body(center=(12.1,14.)),))
|
||||
with self.assertRaisesRegex(ValueError,'boundary'):
|
||||
remap(old,changed,shifted_fields(old))
|
||||
|
||||
def test_fp64_physical_roundtrip_cannot_replace_raw(self):
|
||||
tiny=np.full((9,1,1),np.float32(2**-80))
|
||||
self.assertFalse(np.array_equal(tiny,_encode_physical(_decode_stored(tiny,True),True)))
|
||||
|
||||
|
||||
if __name__=='__main__': unittest.main()
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Direct-current-header regression; historical candidate manifests stay frozen."""
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPERS = ROOT / "tests/validation/les"
|
||||
sys.path.insert(0, str(HELPERS))
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"smagorinsky_oracle", HELPERS / "test_candidate.py"
|
||||
)
|
||||
oracle = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(oracle)
|
||||
HEADER = Path("src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh")
|
||||
DESCRIPTORS = Path("src/CelerisLab/lbm/kernels/core/descriptors.cuh")
|
||||
|
||||
|
||||
class SmagorinskyNormalization(unittest.TestCase):
|
||||
def test_current_header_matches_independent_tensor_oracle(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
build_dir = Path(directory)
|
||||
for dim in (2, 3):
|
||||
for cs in (0.0, 0.16):
|
||||
# Compile directly against this checkout, not saved candidate headers.
|
||||
closed = oracle.build(
|
||||
ROOT.parent, ROOT.name, dim, cs, 1, 3, build_dir
|
||||
)
|
||||
converged = oracle.build(
|
||||
ROOT.parent, ROOT.name, dim, cs, 0, 200, build_dir
|
||||
)
|
||||
for rho in (0.7, 1.0, 1.3):
|
||||
for tau0 in (0.56, 0.8):
|
||||
for kind, magnitude in (
|
||||
("equilibrium", 0.0),
|
||||
("shear", -0.02),
|
||||
("shear", 0.02),
|
||||
("extension", -0.02),
|
||||
("extension", 0.02),
|
||||
):
|
||||
stress = oracle.tensor(
|
||||
dim, kind, magnitude * rho, 0, dim - 1
|
||||
)
|
||||
f, eq = oracle.state(dim, rho, stress)
|
||||
expected = 1 / oracle.standard_tau(
|
||||
tau0, cs, oracle.norm(stress), rho
|
||||
)
|
||||
with self.subTest(
|
||||
dim=dim,
|
||||
cs=cs,
|
||||
rho=rho,
|
||||
tau=tau0,
|
||||
kind=kind,
|
||||
sign=magnitude,
|
||||
):
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, rho, tau0), expected, delta=3e-6
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
converged(f, eq, rho, tau0),
|
||||
expected,
|
||||
delta=3e-6,
|
||||
)
|
||||
if cs == 0:
|
||||
f, eq = oracle.state(
|
||||
dim, 1.0, oracle.tensor(dim, "equilibrium", 0.0)
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, 1.0, 0.50001), 1.99, delta=3e-6
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, 1.0, 200.0), 0.01, delta=3e-6
|
||||
)
|
||||
|
||||
def test_removing_sqrt_two_is_detected(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for relative in (HEADER, DESCRIPTORS):
|
||||
target = root / "old_convention" / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / relative, target)
|
||||
target = root / "old_convention" / HEADER
|
||||
text = target.read_text()
|
||||
factor = "18.0f * sqrtf(2.0f) * smag_const_sq"
|
||||
self.assertEqual(text.count(factor), 2)
|
||||
target.write_text(text.replace(factor, "18.0f * smag_const_sq"))
|
||||
for dim in (2, 3):
|
||||
old = oracle.build(root, "old_convention", dim, 0.16, 1, 3, root)
|
||||
stress = oracle.tensor(dim, "shear", 0.02)
|
||||
f, eq = oracle.state(dim, 1.0, stress)
|
||||
expected = 1 / oracle.standard_tau(0.8, 0.16, oracle.norm(stress), 1.0)
|
||||
self.assertGreater(abs(old(f, eq, 1.0, 0.8) - expected), 1e-3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -238,6 +238,81 @@ def test_overlapping_force_regions_rejected(device):
|
||||
bodies.build_compact_lists(bodies.build_flags(field.flag))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("second_x,error", [(14., "unique cut-link writers"),
|
||||
(17., "active fluid receivers")])
|
||||
def test_passive_overlap_rejected_before_publishing_lists(second_x, error):
|
||||
cfg = LBMConfig(nx=32, ny=24)
|
||||
cfg.validate()
|
||||
bodies = ObjectManager(cfg.nx, cfg.ny, cfg.nz, cfg.nq, cfg)
|
||||
for x in (14., second_x):
|
||||
bodies.add(obj(x=x, y=12., r=4.3))
|
||||
bodies.add(obj(x=4., y=4., r=2., is_sensor=True))
|
||||
counts = np.full(bodies.count, 91, np.int32)
|
||||
hits = []
|
||||
flags = bodies.build_flags(np.full(cfg.nx * cfg.ny, FLUID, np.uint16))
|
||||
for domain_flags in (flags, None):
|
||||
with pytest.raises(ValueError, match=error):
|
||||
bodies.build_compact_lists_for(bodies.objects, domain_flags,
|
||||
sensor_cell_counts_out=counts, hits_out=hits)
|
||||
np.testing.assert_array_equal(counts, 91)
|
||||
assert hits == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("centers", [((10., 12.), (22., 12.)),
|
||||
((10.5, 12.), (19.5, 12.))])
|
||||
def test_separated_passive_bodies_keep_valid_writers(centers):
|
||||
# The second case has a subcell physical gap with a resolved fluid column.
|
||||
cfg = LBMConfig(nx=32, ny=24)
|
||||
cfg.validate()
|
||||
bodies = ObjectManager(cfg.nx, cfg.ny, cfg.nz, cfg.nq, cfg)
|
||||
for x, y in centers:
|
||||
bodies.add(obj(x=x, y=y, r=4.3))
|
||||
flags = bodies.build_flags(np.full(cfg.nx * cfg.ny, FLUID, np.uint16))
|
||||
indices, directions, _, ids, *_ = bodies.build_compact_lists(flags)
|
||||
assert set(ids) == {0, 1}
|
||||
assert np.all(flags[indices] & FLUID) and not np.any(flags[indices] & SOLID)
|
||||
assert len(np.unique(indices.astype(np.uint64) * cfg.nq + directions)) == len(indices)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("entry", ["sync", "initialize", "runtime_edit"])
|
||||
@pytest.mark.parametrize("second_x", [5., 6.])
|
||||
def test_passive_overlap_failure_keeps_live_state(device, entry, second_x):
|
||||
from CelerisLab.simulation import Simulation
|
||||
field = make_field(device)
|
||||
bodies = manager(field, [obj(), obj(x=9., y=3., r=1., is_sensor=True)])
|
||||
bodies.sync_to_gpu(field)
|
||||
overlapping = obj(x=second_x)
|
||||
if entry == "runtime_edit":
|
||||
bodies.stage_add(overlapping)
|
||||
else:
|
||||
bodies.add(overlapping)
|
||||
objects = bodies.objects
|
||||
flags, curved, counts = field.flag.copy(), field.curved.fluid_idx.copy(), bodies.sensor_cell_counts.copy()
|
||||
action = bodies.action.copy()
|
||||
buffers = [(block, bytes(block.data), block.freed) for block in device.blocks]
|
||||
writes = device.writes
|
||||
sim = SimpleNamespace(bodies=bodies, field=field, lbm_cfg=field.cfg,
|
||||
_initialized=entry == "runtime_edit",
|
||||
_assert_allocation_contract=lambda: None,
|
||||
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||
with pytest.raises(ValueError, match="active fluid receivers|unique cut-link writers"):
|
||||
if entry == "sync":
|
||||
bodies.sync_to_gpu(field)
|
||||
elif entry == "initialize":
|
||||
Simulation.initialize(sim)
|
||||
else:
|
||||
Simulation.sync_bodies(sim)
|
||||
assert bodies.objects == objects
|
||||
assert bodies.has_pending_edit() == (entry == "runtime_edit")
|
||||
assert sim._initialized == (entry == "runtime_edit")
|
||||
assert device.writes == writes and len(device.blocks) == len(buffers)
|
||||
assert all(bytes(block.data) == data and block.freed == freed for block, data, freed in buffers)
|
||||
np.testing.assert_array_equal(field.flag, flags)
|
||||
np.testing.assert_array_equal(field.curved.fluid_idx, curved)
|
||||
np.testing.assert_array_equal(bodies.sensor_cell_counts, counts)
|
||||
np.testing.assert_array_equal(bodies.action, action)
|
||||
|
||||
|
||||
def test_action_upload_and_scratch_are_pinned(device):
|
||||
field = make_field(device, streaming="esopull")
|
||||
bodies = manager(field, [obj(is_force_region=True)])
|
||||
|
||||
@@ -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.load_module.return_value = module
|
||||
host.cuda.mem_alloc.side_effect = allocate
|
||||
host.cuda.memcpy_dtoh.side_effect = lambda array, pointer: array.fill(0)
|
||||
host.cuda.memset_d8_async.side_effect = lambda *a: log.append(("memset", a, {}))
|
||||
run = host.FixedPatchStepper(
|
||||
LBMConfig(nx=48, ny=48, viscosity=.02), context=Mock(activate=nullcontext, sm_arch="sm_70"),
|
||||
refinements=[[(6, 6, 26, 26)], [(20, 20, 44, 44)], [(56, 56, 72, 72)]],
|
||||
level_blocks=[(4, 4)] * 4, level_objects=[(), (), (), (body(),)],
|
||||
observe_raw_exchange=True, observe_window_exchange=window)
|
||||
run._stream.synchronize.side_effect = lambda: log.append(("sync", (), {}))
|
||||
run.initialize(*[np.full(level.shape, 1 / 9, np.float32) for level in run._levels])
|
||||
log.clear()
|
||||
return run, log, allocations
|
||||
|
||||
|
||||
def signature(log, allocations):
|
||||
"""Event names with device-pointer arguments replaced by allocation index."""
|
||||
index = {id(a): i for i, a in enumerate(allocations)}
|
||||
return [(name, tuple(index.get(id(a), "-") for a in args)) for name, args, _ in log]
|
||||
|
||||
|
||||
def names(log):
|
||||
return [name for name, _, _ in log]
|
||||
|
||||
|
||||
class StepSync(unittest.TestCase):
|
||||
def test_step1_waits_once_after_all_launches(self):
|
||||
run, log, _ = backend()
|
||||
run.step(1)
|
||||
self.assertEqual(names(log).count("sync"), 1)
|
||||
self.assertEqual(names(log)[-1], "sync")
|
||||
self.assertEqual(names(log)[0], "memset")
|
||||
self.assertEqual(names(log).count("BlockStep"), 1 + 2 + 4 + 8)
|
||||
self.assertEqual(run.step_count, 1)
|
||||
run.close()
|
||||
|
||||
def test_step_n_equals_n_step1_launches_with_one_wait(self):
|
||||
single, single_log, single_alloc = backend()
|
||||
for _ in range(3):
|
||||
single.step(1)
|
||||
batched, batched_log, batched_alloc = backend()
|
||||
batched.step(3)
|
||||
expected = [e for e in signature(single_log, single_alloc) if e[0] != "sync"]
|
||||
got = signature(batched_log, batched_alloc)
|
||||
self.assertEqual(got[-1][0], "sync")
|
||||
self.assertEqual(names(batched_log).count("sync"), 1)
|
||||
self.assertEqual(got[:-1], expected) # same order, same ping-pong buffers
|
||||
self.assertEqual(names(batched_log).count("memset"), 3)
|
||||
self.assertEqual(batched.step_count, single.step_count)
|
||||
index = [{id(a): i for i, a in enumerate(alloc)} for alloc in (single_alloc, batched_alloc)]
|
||||
self.assertEqual(index[1][id(batched._levels[0].current)],
|
||||
index[0][id(single._levels[0].current)])
|
||||
single.close()
|
||||
batched.close()
|
||||
|
||||
def test_step0_launches_and_waits_for_nothing(self):
|
||||
run, log, _ = backend()
|
||||
run.step(2)
|
||||
log.clear()
|
||||
run.step(0)
|
||||
self.assertEqual(log, [])
|
||||
self.assertEqual(run.step_count, 2)
|
||||
value = run.read_raw_exchange(0, level=BODY_LEVEL) # step(0) keeps the ledger valid
|
||||
self.assertEqual((value["time_start"], value["time_end"]), (1, 2))
|
||||
run.close()
|
||||
|
||||
def test_completion_metadata_covers_all_intervals(self):
|
||||
run, _, _ = backend()
|
||||
with self.assertRaisesRegex(RuntimeError, "completed"):
|
||||
run.read_raw_exchange(0, level=BODY_LEVEL)
|
||||
run.step(3)
|
||||
value = run.read_raw_exchange(0, level=BODY_LEVEL)
|
||||
self.assertEqual((value["time_start"], value["time_end"], value["sample_count"]),
|
||||
(2, 3, HOPS)) # step(n) retains only the final base interval
|
||||
run.step(2)
|
||||
self.assertEqual(run.read_raw_exchange(0, level=BODY_LEVEL)["time_end"], 5)
|
||||
run.close()
|
||||
|
||||
def test_window_accounting_across_batched_calls(self):
|
||||
windows = []
|
||||
for calls in ((1, 3), (4,), (1, 1, 1, 1)):
|
||||
run, log, _ = backend(window=True)
|
||||
run.step(1)
|
||||
run.start_exchange_window()
|
||||
log.clear()
|
||||
for n in calls:
|
||||
run.step(n)
|
||||
self.assertEqual(names(log).count("BlockAccumulateExchange"), 2 * 4 * HOPS)
|
||||
self.assertEqual(names(log).count("sync"), len(calls))
|
||||
out = run.read_exchange_window(0, level=BODY_LEVEL)
|
||||
windows.append((out["time_start"], out["time_end"], out["sample_count"]))
|
||||
run.close()
|
||||
self.assertEqual(windows, [(1., 5, 4 * HOPS)] * 3)
|
||||
|
||||
def test_failed_wait_publishes_nothing(self):
|
||||
run, _, _ = backend(window=True)
|
||||
run.step(2)
|
||||
run.start_exchange_window()
|
||||
run._stream.synchronize.side_effect = RuntimeError("device failure")
|
||||
with self.assertRaisesRegex(RuntimeError, "device failure"):
|
||||
run.step(3)
|
||||
self.assertEqual(run.step_count, 2) # value at entry to the failing call
|
||||
self.assertFalse(run._exchange_valid)
|
||||
self.assertFalse(run._window_valid)
|
||||
for read in (lambda: run.read_raw_exchange(0, level=BODY_LEVEL),
|
||||
lambda: run.read_exchange_window(0, level=BODY_LEVEL), run.download,
|
||||
lambda: run.step(1)):
|
||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||
read()
|
||||
run._stream.synchronize.side_effect = None
|
||||
run.close()
|
||||
|
||||
def test_launch_error_mid_batch_skips_wait_and_fails(self):
|
||||
run, log, _ = backend()
|
||||
run.step(1)
|
||||
log.clear()
|
||||
hop, calls = run._hop, []
|
||||
|
||||
def failing(index, *args, **kw):
|
||||
if index == 0:
|
||||
calls.append(1)
|
||||
if len(calls) == 2:
|
||||
raise RuntimeError("launch failure")
|
||||
return hop(index, *args, **kw)
|
||||
run._hop = failing
|
||||
with self.assertRaisesRegex(RuntimeError, "launch failure"):
|
||||
run.step(3)
|
||||
self.assertNotIn("sync", names(log))
|
||||
self.assertEqual(run.step_count, 1)
|
||||
self.assertFalse(run._exchange_valid)
|
||||
with self.assertRaisesRegex(RuntimeError, "failed"):
|
||||
run.read_raw_exchange(0, level=BODY_LEVEL)
|
||||
run.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -175,12 +175,11 @@ class SynchronizedRemapTests(unittest.TestCase):
|
||||
self.assertFalse(np.array_equal(self.g[2][:, lost], returned.g[2][:, lost]))
|
||||
|
||||
def test_invalid_geometry_counts_representation_frame(self):
|
||||
for rectangles in ([], [(2, 2, 10, 10)], [(16, 16, 32, 40)] * 2,
|
||||
[(16, 16, 33.5, 40)], [(16, 16, 32, 40), (33, 16, 36, 40)]):
|
||||
for rectangles in ([], [(2, 2, 10, 10)], [(16, 16, 33.5, 40)]):
|
||||
with self.assertRaises(ValueError):
|
||||
topology(rectangles)
|
||||
parent = self.old.parent
|
||||
for changes in (dict(lattice_model='D3Q19'), dict(ddf_shifting=True),
|
||||
for changes in (dict(lattice_model='D3Q19'),
|
||||
dict(collision='TRT'), dict(les_enabled=True), dict(nz=2)):
|
||||
with self.assertRaises(ValueError):
|
||||
RemapTopology.from_config(replace(CFG, **changes), parent, A)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
"""CPU contract of the measurement-only refined DFG timing harness (no CUDA)."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
||||
import time_refined_dfg as t # noqa: E402
|
||||
|
||||
|
||||
class TimingHarnessContract(unittest.TestCase):
|
||||
def test_equal_simulated_duration(self):
|
||||
s = t.schedule(2000, 200)
|
||||
self.assertEqual(s["refined"], (200, 2000))
|
||||
self.assertEqual(s["uniform"], (400, 4000)) # one refined base step = two D80 steps
|
||||
self.assertAlmostEqual(s["tstar_window"], 2000 * .01 / 40)
|
||||
for bad in ((0, 1), (1, -1)):
|
||||
with self.assertRaises(ValueError):
|
||||
t.schedule(*bad)
|
||||
|
||||
def test_timing_only_labels(self):
|
||||
for name in ("B", "C", "E'"):
|
||||
self.assertFalse(t.VARIANTS[name]["force_history_valid"])
|
||||
self.assertTrue(t.VARIANTS[name]["timing_only"])
|
||||
for name in ("A", "E"):
|
||||
self.assertFalse(t.VARIANTS[name]["force_history_valid"]) # reads discarded
|
||||
self.assertTrue(t.VARIANTS[name]["force_reads"])
|
||||
self.assertIn("runner-like", t.VARIANTS[name]["force_note"])
|
||||
self.assertEqual({v["case"] for v in t.VARIANTS.values()}, {"refined", "uniform"})
|
||||
self.assertIn("final base interval", t.VARIANTS["C"]["force_note"])
|
||||
self.assertIn("whole-window", t.VARIANTS["E'"]["force_note"])
|
||||
|
||||
def test_deadline(self):
|
||||
import time
|
||||
with self.assertRaises(TimeoutError):
|
||||
t.check_deadline(time.monotonic() - 1)
|
||||
t.check_deadline(time.monotonic() + 60)
|
||||
|
||||
def test_parity_detects_bits_and_nan_masks(self):
|
||||
a = np.array([[1., np.nan], [2., 3.]], np.float32)
|
||||
self.assertIsNone(t.same_state([a], [a.copy()]))
|
||||
b = a.copy(); b[1, 1] = np.nextafter(np.float32(3), np.float32(4))
|
||||
self.assertIn("bits", t.same_state([a], [b]))
|
||||
c = a.copy(); c[0, 0] = np.nan
|
||||
self.assertIn("NaN", t.same_state([a], [c]))
|
||||
self.assertIn("level count", t.same_state([a], [a, a]))
|
||||
inf = a.copy(); inf[1, 0] = np.inf
|
||||
self.assertIn("infinite", t.same_state([inf], [inf.copy()])) # matching inf still fails
|
||||
self.assertIn("infinite", t.same_state([a], [inf]))
|
||||
|
||||
def test_flat_uniform_ddf(self):
|
||||
fluid = np.array([[True, False, True], [True, True, False]])
|
||||
flat = np.ones(9 * fluid.size, np.float32) # Simulation.get_ddf() layout
|
||||
self.assertTrue(t.uniform_populations_finite(flat, fluid))
|
||||
flat[1 * fluid.size + 1] = np.nan # q=1, solid cell 1: ignored
|
||||
self.assertTrue(t.uniform_populations_finite(flat, fluid))
|
||||
flat[4 * fluid.size + 0] = np.nan # q=4, fluid cell 0: detected
|
||||
self.assertFalse(t.uniform_populations_finite(flat, fluid))
|
||||
self.assertTrue(t.uniform_populations_finite(np.ones((9,) + fluid.shape), fluid))
|
||||
with self.assertRaises(RuntimeError):
|
||||
t.uniform_populations_finite(np.ones(9 * fluid.size + 1), fluid)
|
||||
|
||||
def test_physical_health_limits(self):
|
||||
ok = (np.array([1., 1.01]), np.array([.01, .02]), np.array([0., .01]))
|
||||
health = t.physical_health([ok])
|
||||
self.assertAlmostEqual(health["rho_max"], 1.01)
|
||||
for bad in ((np.array([1., np.nan]),) + ok[1:],
|
||||
(np.array([.4, 1.]),) + ok[1:],
|
||||
(ok[0], np.array([.2, 0.]), ok[2]),
|
||||
(np.array([]), np.array([]), np.array([]))):
|
||||
with self.assertRaises(RuntimeError):
|
||||
t.physical_health([bad])
|
||||
|
||||
def test_summary_reports_differences_without_claims(self):
|
||||
rows = [dict(variant=v, seconds=s) for v, s in
|
||||
(("A", 3.), ("A", 5.), ("B", 2.), ("C", 1.), ("E", 2.), ("E'", 1.5))]
|
||||
s = t.summarize(rows, .5)
|
||||
self.assertEqual(s["A"]["median_seconds"], 4.)
|
||||
self.assertEqual(s["A_minus_B_seconds"], 2.)
|
||||
self.assertEqual(s["B_minus_C_seconds"], 1.)
|
||||
self.assertEqual(s["A_over_E"], 2.)
|
||||
self.assertIn("not exact causal", s["interpretation"])
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -0,0 +1,38 @@
|
||||
"""CPU contract of the A-versus-window timing harness (no CUDA)."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
||||
import time_window_dfg as w # noqa: E402
|
||||
|
||||
|
||||
class WindowTimingContract(unittest.TestCase):
|
||||
def test_interval_and_variants(self):
|
||||
self.assertEqual(w.INTERVAL, 80)
|
||||
self.assertEqual(w.VARIANTS, ("A", "W", "W1"))
|
||||
|
||||
def test_components(self):
|
||||
obs = dict(total=dict(impulse=np.array([1., 2.]), angular_impulse=3., mass_into_fluid=4.))
|
||||
np.testing.assert_array_equal(w.components(obs), [1., 2., 3., 4.])
|
||||
|
||||
def test_interval_comparison(self):
|
||||
ref = np.array([[1., -1., 0., 0.]])
|
||||
scale = np.array([[10., 10., 1., 1.]])
|
||||
self.assertEqual(w.compare_intervals(ref, scale, ref.copy()), 0.)
|
||||
near = ref + np.array([[5e-9, 0., 0., 0.]])
|
||||
self.assertLessEqual(w.compare_intervals(ref, scale, near), 1.)
|
||||
far = ref + np.array([[1e-7, 0., 0., 0.]])
|
||||
self.assertGreater(w.compare_intervals(ref, scale, far), 1.)
|
||||
zero = np.zeros((1, 4))
|
||||
self.assertGreater(w.compare_intervals(zero, zero, zero + 1e-12), 1.) # floor, no div-by-0
|
||||
|
||||
def test_candidate_identity_records_files(self):
|
||||
identity = w.candidate_identity(Path(__file__).resolve().parents[2])
|
||||
self.assertIn("checkout_revision", identity)
|
||||
self.assertEqual(set(identity["sha256"]), set(w.CANDIDATE_FILES))
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Union seam bodies and actual support pinning; CPU host-only."""
|
||||
import unittest
|
||||
import numpy as np
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.refinement import FixedPatchSet2D,prepare_fine_boundary,_circle_in_coverage
|
||||
from CelerisLab.lbm.synchronized_remap import same_physical_boundary
|
||||
from tests.unit import test_refinement as refinement_tests
|
||||
from tests.unit.test_pinned_body_remap import mesh,body,ready,A
|
||||
from tests.unit.test_synchronized_remap import fields,remap,integrals
|
||||
|
||||
class UnionBodySupport(unittest.TestCase):
|
||||
def test_seam_circle_matches_single_rectangle(self):
|
||||
cfg=LBMConfig(nx=32,ny=16,viscosity=.1)
|
||||
obj=refinement_tests.PatchSetBoundaryTests.body(16.,8.)
|
||||
def b(rects):return prepare_fine_boundary(cfg,FixedPatchSet2D.from_config(cfg,rects),[obj])
|
||||
self.assertTrue(same_physical_boundary(b([(4,2,28,14)]),b([(4,2,16,14),(16,2,28,14)])))
|
||||
with self.assertRaisesRegex(ValueError,'union'):
|
||||
b([(4,2,16,14),(17,2,28,14)])
|
||||
|
||||
def test_continuous_disk_not_only_sampled_solids(self):
|
||||
mask=np.ones((8,8),bool);mask[3,3]=False
|
||||
self.assertFalse(_circle_in_coverage(mask,3.99,3.99,.02))
|
||||
self.assertTrue(_circle_in_coverage(mask,4.1,4.1,.1))
|
||||
|
||||
def test_repartition_and_connected_fluid_changes_do_not_pin_rectangles(self):
|
||||
old=mesh(A,(body(),));initial=fields(old)
|
||||
for rects in (((16,16,24,40),(24,16,32,40),A[1]),((12,16,32,40),A[1]),((20,16,36,40),A[1])):
|
||||
new=mesh(rects,(body(),));self.assertTrue(same_physical_boundary(old.boundary,new.boundary))
|
||||
out=remap(old,new,initial)
|
||||
np.testing.assert_allclose(integrals(old,initial),integrals(new,out.g),rtol=2e-13,atol=2e-12)
|
||||
for a,b,om,nm in zip(initial,out.g,old.leaf_masks,new.leaf_masks):
|
||||
np.testing.assert_array_equal(a[:,om&nm],b[:,om&nm])
|
||||
|
||||
def test_runtime_seam_repartition_preserves_boundary_and_live_jets(self):
|
||||
run,module,allocations,storage,objects=ready()
|
||||
try:
|
||||
run.set_jet_amplitude(0,.017,level=2)
|
||||
clone=run.remap_child(((16,16,24,40),(24,16,32,40),A[1]),child_objects=objects)
|
||||
try:
|
||||
self.assertEqual(clone._boundaries[2].jets[0].amplitude,.017)
|
||||
self.assertTrue(same_physical_boundary(run.child_boundary,clone.child_boundary))
|
||||
for a,b in zip(run.download(),clone.download()):np.testing.assert_array_equal(a,b)
|
||||
finally:clone.close()
|
||||
finally:run.close()
|
||||
@@ -0,0 +1,161 @@
|
||||
"""CPU tests: union tables vs current FixedPatchSet2D; numpy + CelerisLab on PYTHONPATH.
|
||||
Optional numerics: ADAPTER_DIR=<adapter bundle dir> [ADAPTER_FILE=adapter.py or
|
||||
multilevel_numeric_adapter.py (default: whichever exists)]."""
|
||||
from dataclasses import replace
|
||||
import importlib.util
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.refinement import (ACTIVE, OUTER_GHOST, FixedPatchSet2D, _middle_config,
|
||||
_nested_kind, _transfer_footprint)
|
||||
from CelerisLab.lbm.refinement_tables import blocked_kind
|
||||
|
||||
from CelerisLab.lbm.refinement import (FixedPatchSet2D as UnionPatchSet2D,
|
||||
_check_parent_pull_closure as check_parent_pull_closure, _dilate as dilate)
|
||||
|
||||
def table_sets(pair):
|
||||
return (pair.fine_kind.tobytes(), pair.covered.tobytes(),
|
||||
frozenset(zip(map(int,pair.ghost_fine),map(int,pair.ghost_coarse))),
|
||||
frozenset((int(t),tuple(map(int,c))) for t,c in zip(pair.coalesce_coarse,pair.coalesce_fine)))
|
||||
|
||||
CFG = LBMConfig(nx=30, ny=30, viscosity=.1)
|
||||
A, B, U = (8, 8, 16, 20), (16, 8, 24, 20), (8, 8, 24, 20)
|
||||
|
||||
|
||||
def union(*rects, cfg=CFG):
|
||||
return UnionPatchSet2D.from_config(cfg, rects)
|
||||
|
||||
|
||||
class Equivalence(unittest.TestCase):
|
||||
def test_abutting_boxes_equal_their_union_rectangle(self):
|
||||
reference = table_sets(FixedPatchSet2D.from_config(CFG, [U]))
|
||||
for rects in ((A, B), (B, A), (U,)):
|
||||
with self.subTest(rects=rects):
|
||||
self.assertEqual(table_sets(union(*rects)), reference)
|
||||
|
||||
def test_vertical_abutting(self):
|
||||
self.assertEqual(table_sets(union((8, 8, 20, 14), (8, 14, 20, 22))),
|
||||
table_sets(FixedPatchSet2D.from_config(CFG, [(8, 8, 20, 22)])))
|
||||
|
||||
def test_separated_rectangles_reproduce_current_tables(self):
|
||||
rects = [(4, 4, 10, 10), (16, 16, 26, 26)] # gap 6 >= MIN_PATCH_GAP
|
||||
self.assertEqual(table_sets(union(*rects)),
|
||||
table_sets(FixedPatchSet2D.from_config(CFG, rects)))
|
||||
|
||||
def test_layout_tiles_are_free_of_rectangle_alignment(self):
|
||||
pair = union(A, B)
|
||||
support = dilate(pair.fine_kind == ACTIVE, 4)
|
||||
reference = FixedPatchSet2D.from_config(CFG, [U]).fine_layout((4, 4))
|
||||
np.testing.assert_array_equal(pair.fine_layout((4, 4)).owned, reference.owned)
|
||||
layout = pair.fine_layout((6, 10)) # not aligned to the rectangles
|
||||
self.assertTrue(layout.owned[support].all())
|
||||
kind = blocked_kind(layout, pair.fine_kind)
|
||||
owned = layout.owned
|
||||
np.testing.assert_array_equal(kind[layout.owner[owned]], pair.fine_kind[owned])
|
||||
|
||||
|
||||
class NewlyLegal(unittest.TestCase):
|
||||
def check(self, pair):
|
||||
_transfer_footprint(pair)
|
||||
check_parent_pull_closure(pair)
|
||||
ghosts = pair.ghost_fine.astype(np.intp)
|
||||
self.assertFalse((pair.fine_kind.ravel()[ghosts] == ACTIVE).any())
|
||||
self.assertFalse(pair.covered.ravel()[pair.ghost_coarse.astype(np.intp)].any())
|
||||
|
||||
def test_small_gaps_and_l_shape(self):
|
||||
for rects in (((8, 8, 16, 20), (17, 8, 24, 20)), # gap 1
|
||||
((8, 8, 16, 20), (18, 8, 24, 20)), # gap 2
|
||||
((8, 8, 20, 14), (8, 14, 14, 22))): # L-shape
|
||||
with self.subTest(rects=rects):
|
||||
self.check(union(*rects))
|
||||
|
||||
def test_corner_contact_only_by_dependency_checks(self):
|
||||
# Policy: no geometric ban; accepted iff the direction-specific checks pass.
|
||||
try:
|
||||
pair = union((8, 8, 16, 16), (16, 16, 24, 24))
|
||||
except ValueError as error:
|
||||
self.assertRegex(str(error), "outer ghost|allocation|INNER|surviving|coalesce")
|
||||
else:
|
||||
self.check(pair)
|
||||
|
||||
|
||||
class Negatives(unittest.TestCase):
|
||||
def test_inputs_are_validated_before_conversion(self):
|
||||
for bad in ((8.5, 8, 16, 20), ("8", 8, 16, 20), (True, 8, 16, 20), "abcd", (8, 8, 16)):
|
||||
with self.subTest(bad=bad), self.assertRaises(ValueError):
|
||||
union(bad, B)
|
||||
self.assertEqual(union(tuple(np.int64(v) for v in A), B).bounds, (A, B))
|
||||
pair = union(A, B)
|
||||
for block in ((4.0, 4), (True, 4), (0, 4), (4,), ("4", 4)):
|
||||
with self.subTest(block=block), self.assertRaises(ValueError):
|
||||
pair.fine_layout(block)
|
||||
|
||||
def test_missing_donor_at_domain_margin(self):
|
||||
with self.assertRaisesRegex(ValueError, "margin"):
|
||||
union((1, 8, 10, 20), (10, 8, 20, 20))
|
||||
|
||||
def test_missing_or_duplicate_coalesce_row(self):
|
||||
pair = union(A, B)
|
||||
with self.assertRaisesRegex(ValueError, "lacks its coalesce row"):
|
||||
check_parent_pull_closure(replace(pair, coalesce_coarse=pair.coalesce_coarse[1:],
|
||||
coalesce_fine=pair.coalesce_fine[1:]))
|
||||
doubled = replace(pair, coalesce_coarse=np.r_[pair.coalesce_coarse, pair.coalesce_coarse[:1]],
|
||||
coalesce_fine=np.r_[pair.coalesce_fine, pair.coalesce_fine[:1]])
|
||||
with self.assertRaisesRegex(ValueError, "duplicate"):
|
||||
check_parent_pull_closure(doubled)
|
||||
|
||||
def test_direction_specific_dependency_on_outer_cell(self):
|
||||
pair = union(A, B)
|
||||
active = pair.fine_kind == ACTIVE
|
||||
kind = pair.fine_kind.copy()
|
||||
kind[dilate(active, 2) & ~dilate(active, 1)] = OUTER_GHOST # thin the INNER band
|
||||
with self.assertRaisesRegex(ValueError, "outer ghost"):
|
||||
_transfer_footprint(replace(pair, fine_kind=kind))
|
||||
|
||||
def test_nested_union_child_donors_must_be_parent_active(self):
|
||||
parent = FixedPatchSet2D.from_config(CFG, [(6, 6, 24, 24)]) # L1 ACTIVE 12..48
|
||||
middle = _middle_config(CFG, parent)
|
||||
with self.assertRaisesRegex(ValueError, "donors/receivers"):
|
||||
_nested_kind(parent, UnionPatchSet2D.from_config(middle, [(12, 20, 30, 40), (30, 20, 40, 40)]))
|
||||
child = UnionPatchSet2D.from_config(middle, [(16, 20, 30, 40), (30, 20, 40, 40)])
|
||||
reference = FixedPatchSet2D.from_config(middle, [(16, 20, 40, 40)])
|
||||
self.assertEqual(table_sets(child), table_sets(reference))
|
||||
np.testing.assert_array_equal(_nested_kind(parent, child), _nested_kind(parent, reference))
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get("ADAPTER_DIR"), "set ADAPTER_DIR for the CPU D2Q9 adapter run")
|
||||
class AdapterNumerics(unittest.TestCase):
|
||||
def test_eight_steps_bitwise(self):
|
||||
folder = os.environ["ADAPTER_DIR"]
|
||||
name = os.environ.get("ADAPTER_FILE") or next(
|
||||
n for n in ("multilevel_numeric_adapter.py", "adapter.py")
|
||||
if os.path.exists(os.path.join(folder, n)))
|
||||
path = os.path.join(folder, name)
|
||||
spec = importlib.util.spec_from_file_location("multilevel_numeric_adapter", path)
|
||||
adapter = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(adapter)
|
||||
results = []
|
||||
for pair in (union(A, B), FixedPatchSet2D.from_config(CFG, [U])):
|
||||
kinds = [np.where(pair.covered, OUTER_GHOST, ACTIVE).astype(np.uint8), pair.fine_kind]
|
||||
taus = [pair.tau_coarse, pair.tau_fine]
|
||||
initial = []
|
||||
for level, kind in enumerate(kinds):
|
||||
ny, nx = kind.shape
|
||||
y = (np.arange(ny) + .5) / 2 ** level
|
||||
ux = 1e-3 * np.sin(2 * np.pi * y / 30)[:, None] * np.ones((1, nx))
|
||||
cx = np.array([0, 1, -1, 0, 0, 1, -1, 1, -1], float)[:, None, None]
|
||||
w = np.array([4/9] + [1/9]*4 + [1/36]*4)[:, None, None]
|
||||
g = w * (1 + 3*cx*ux + 4.5*(cx*ux)**2 - 1.5*ux*ux)
|
||||
g[:, kind != ACTIVE] = np.nan
|
||||
initial.append(g)
|
||||
run = adapter.make_scheduler(kinds, taus, [pair], initial)
|
||||
run.step(8)
|
||||
results.append([level.current.copy() for level in run.levels])
|
||||
for a, b in zip(*results):
|
||||
np.testing.assert_array_equal(a, b)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Explicit experimental repin and historical-source provenance contracts."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "tests/validation"))
|
||||
from source_provenance import checkout_provenance
|
||||
import re100_helpers
|
||||
import run_jet_circle_cv
|
||||
|
||||
sys.path.insert(0, str(ROOT / "tests/unit"))
|
||||
from historical_tree import historical_tree, tracked
|
||||
|
||||
ORIGIN = "47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5"
|
||||
|
||||
|
||||
def test_manifests_cover_current_source_and_preserve_origin(tmp_path):
|
||||
assert re100_helpers.PIN == run_jet_circle_cv.PIN == ORIGIN
|
||||
manifests = {}
|
||||
for group, hashes_key, revision_key in (
|
||||
("dfg_re100", "production_sha256", "commit"),
|
||||
("jet_circle_cv", "files", "revision"),
|
||||
):
|
||||
manifest = json.loads(
|
||||
(
|
||||
ROOT / "tests/validation/data" / group / "source_manifest.json"
|
||||
).read_text()
|
||||
)
|
||||
assert manifest[revision_key] == ORIGIN
|
||||
assert manifest["source_origin_commits"][-1] == ORIGIN
|
||||
assert (
|
||||
manifest["historical_checkpoints"]["production"]
|
||||
== "d79e3f6f0a424f5bcc3a1a6d610313e49daf97a2"
|
||||
)
|
||||
assert "not inherited from d79" in manifest["experiment"]
|
||||
manifests[group] = manifest[hashes_key]
|
||||
# History-dependent checks last, so a missing commit masks nothing above.
|
||||
names = tracked(ORIGIN)
|
||||
origin = historical_tree(ORIGIN, tmp_path)
|
||||
for group, hashes in manifests.items():
|
||||
assert set(hashes) == set(names), group
|
||||
for name, expected in hashes.items():
|
||||
assert (
|
||||
hashlib.sha256((origin / name).read_bytes()).hexdigest() == expected
|
||||
), name
|
||||
|
||||
|
||||
def test_runtime_checkout_identity_is_separate_from_source_origin(tmp_path):
|
||||
subprocess.run(["git", "init", "-q", str(tmp_path)], check=True)
|
||||
(tmp_path / "test.txt").write_text("tracked\n")
|
||||
subprocess.run(["git", "-C", str(tmp_path), "add", "test.txt"], check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-C",
|
||||
str(tmp_path),
|
||||
"-c",
|
||||
"user.name=Test",
|
||||
"-c",
|
||||
"user.email=test@example.invalid",
|
||||
"commit",
|
||||
"-qm",
|
||||
"fixture",
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
expected = subprocess.check_output(
|
||||
["git", "-C", str(tmp_path), "rev-parse", "HEAD"], text=True
|
||||
).strip()
|
||||
assert checkout_provenance(tmp_path) == dict(
|
||||
checkout_revision=expected, checkout_dirty=False
|
||||
)
|
||||
(tmp_path / "test.txt").write_text("changed\n")
|
||||
assert checkout_provenance(tmp_path) == dict(
|
||||
checkout_revision=expected, checkout_dirty=True
|
||||
)
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline common-unit/sampling sensitivity comparison; never grants physics acceptance."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
|
||||
def integrate(t, y):
|
||||
return np.sum(
|
||||
np.diff(t).reshape((-1,) + (1,) * (y.ndim - 1)) * (y[1:] + y[:-1]) * 0.5, axis=0
|
||||
)
|
||||
|
||||
|
||||
def analyze(directory):
|
||||
meta_path = next(Path(directory).glob("samples_s*.json"))
|
||||
meta = json.loads(meta_path.read_text())
|
||||
c = meta["case"]
|
||||
s = c["scale"]
|
||||
sample_path = meta_path.parent / meta["sample_file"]
|
||||
checksum_verified = False
|
||||
if "sample_sha256" in meta:
|
||||
if (
|
||||
hashlib.sha256(sample_path.read_bytes()).hexdigest()
|
||||
!= meta["sample_sha256"]
|
||||
):
|
||||
raise ValueError("Sample file hash mismatch")
|
||||
checksum_verified = True
|
||||
a = np.load(sample_path)
|
||||
t = a["t_reference"]
|
||||
if not len(t) or t[0] != 0 or np.any(np.diff(t) <= 0):
|
||||
raise ValueError("Sample reference times must increase strictly from zero")
|
||||
from jet_cv_measurements import sample_steps
|
||||
|
||||
complete_schedule = np.array_equal(t, sample_steps(c) / s**2)
|
||||
records = []
|
||||
# Input retains root units; convert before all budgets/comparisons.
|
||||
inv = a["cv_inventory"] * np.array([1 / s**2, 1 / s, 1 / s])
|
||||
flux = a["cv_flux"] * np.array([1.0, s, s])
|
||||
raw = a["raw_cumulative"] * np.array([1 / s, 1 / s, 1.0, 1 / s**2])
|
||||
raw[:, :, :, 2] /= (
|
||||
s**2
|
||||
) # angular impulse reference = root/s^2 (not used in force tests)
|
||||
q0 = 2 * np.pi / 12 * (c["diameter"] / 2) * c["jet_speed"]
|
||||
f0 = 0.5 * (c["u"] * s) ** 2 * (c["diameter"] / s)
|
||||
for lo in range(0, 3200, 100):
|
||||
hi = lo + 100
|
||||
ids = np.flatnonzero((t >= lo) & (t <= hi))
|
||||
if len(ids) < 2 or t[ids[0]] != lo or t[ids[-1]] != hi:
|
||||
continue
|
||||
first, last = ids[0], ids[-1]
|
||||
delta = inv[last] - inv[first]
|
||||
ledger = raw[last] - raw[first]
|
||||
cv_variants = []
|
||||
for stride in (1, 2, 4):
|
||||
selected = np.unique(np.r_[ids[::stride], ids[-1]])
|
||||
total = integrate(t[selected], flux[selected])
|
||||
m = delta[:, :, 0] + total[:, :, 0]
|
||||
j = -delta[:, :, 1:] - total[:, :, 1:]
|
||||
cv_variants.append(
|
||||
np.concatenate(
|
||||
(
|
||||
(m - ledger[:, 2, 3, None])[:, :, None] / (100 * q0),
|
||||
(j - ledger[:, 2, None, :2]) / (100 * f0),
|
||||
),
|
||||
axis=-1,
|
||||
)
|
||||
)
|
||||
arc = (
|
||||
integrate(t[ids], a["arc_estimates"][ids]) / 100
|
||||
) # Q/M rate conversion is unity
|
||||
# Each estimator, aperture, Q/M; compare against signed prescribed Q (rho_ref=1 for M only).
|
||||
arc_ratio = arc / np.array([q0, -q0])[None, None, :, None]
|
||||
global_delta = (
|
||||
a["global_mass_reference"][last] - a["global_mass_reference"][first]
|
||||
)
|
||||
records.append(
|
||||
dict(
|
||||
t_reference=[lo, hi],
|
||||
startup=hi <= 800,
|
||||
normalized_cv_residual=np.array(cv_variants).tolist(),
|
||||
temporal_sensitivity_max=float(
|
||||
np.max(abs(np.array(cv_variants)[1:] - cv_variants[0]))
|
||||
),
|
||||
arc_ratio_to_command_reference_density=arc_ratio.tolist(),
|
||||
global_mass_residual_over_nominal=(
|
||||
(global_delta - ledger[:, 2, 3]) / (100 * q0)
|
||||
).tolist(),
|
||||
raw_reaction_coefficient=(ledger[:, 2, :2] / (100 * f0)).tolist(),
|
||||
raw_gross_mass_over_nominal=(
|
||||
(abs(ledger[:, 3, 3]) + abs(ledger[:, 4, 3])) / (100 * q0)
|
||||
).tolist(),
|
||||
raw_wall_mass_over_nominal=(ledger[:, 0, 3] / (100 * q0)).tolist(),
|
||||
)
|
||||
)
|
||||
return dict(
|
||||
scale=s,
|
||||
complete_schedule=bool(complete_schedule),
|
||||
sample_checksum_verified=checksum_verified,
|
||||
windows=records,
|
||||
sampled_density_range=[
|
||||
float(a["level_stats"][..., 0].min()),
|
||||
float(a["level_stats"][..., 1].max()),
|
||||
],
|
||||
sampled_max_speed_reference=float(a["level_stats"][..., 3].max()),
|
||||
global_mass_initial_final=a["global_mass_reference"][[0, -1]].tolist(),
|
||||
max_sampled_fine_velocity_error=float(a["velocity_error"].max()),
|
||||
physics_validation_pass=False,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("directories", nargs="+", type=Path)
|
||||
p.add_argument("--output", type=Path, required=True)
|
||||
a = p.parse_args()
|
||||
results = [analyze(d) for d in a.directories]
|
||||
payload = dict(
|
||||
status="TRANSIENT_DIAGNOSTIC_COMPARISON",
|
||||
results=results,
|
||||
physics_validation_pass=False,
|
||||
)
|
||||
if sorted(x["scale"] for x in results) == [1, 2] and all(
|
||||
x["complete_schedule"] for x in results
|
||||
):
|
||||
ordered = sorted(results, key=lambda x: x["scale"])
|
||||
summary = []
|
||||
for startup in (True, False):
|
||||
r = [
|
||||
np.array(
|
||||
[
|
||||
w["normalized_cv_residual"][0]
|
||||
for w in item["windows"]
|
||||
if w["startup"] == startup
|
||||
]
|
||||
)
|
||||
for item in ordered
|
||||
]
|
||||
e = [
|
||||
np.array(
|
||||
[
|
||||
w["temporal_sensitivity_max"]
|
||||
for w in item["windows"]
|
||||
if w["startup"] == startup
|
||||
]
|
||||
)
|
||||
for item in ordered
|
||||
]
|
||||
rms = [np.sqrt(np.mean(x * x, axis=(0, 1, 2))) for x in r]
|
||||
unc = max(float(x.max()) for x in e)
|
||||
summary.append(
|
||||
dict(
|
||||
startup=startup,
|
||||
rms_normalized_residual_mass_Cx_Cy=[v.tolist() for v in rms],
|
||||
max_temporal_sensitivity=unc,
|
||||
reduction_exceeds_twice_temporal_proxy=(
|
||||
rms[0] - rms[1] > 2 * unc
|
||||
).tolist(),
|
||||
)
|
||||
)
|
||||
payload["refinement_trend"] = summary
|
||||
a.output.write_text(json.dumps(payload, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,818 @@
|
||||
{
|
||||
"start": 96.60907453162486,
|
||||
"end": 99.92207699254307,
|
||||
"period": 3.313002460918213,
|
||||
"st": 0.301841007302737,
|
||||
"cd_min": 3.164265915836695,
|
||||
"cd_max": 3.2273924509469625,
|
||||
"cd_midrange": 3.1958291833918286,
|
||||
"cd_amplitude": 0.06312653511026767,
|
||||
"cd_time_mean": 3.1958873825129652,
|
||||
"cd_waveform": [
|
||||
3.2192714249444543,
|
||||
3.2202803099943376,
|
||||
3.2212299416471293,
|
||||
3.222118601683268,
|
||||
3.2229440924252426,
|
||||
3.2237043728228487,
|
||||
3.224397563502949,
|
||||
3.225021951759953,
|
||||
3.2255759960759045,
|
||||
3.226058329556411,
|
||||
3.2264677639287034,
|
||||
3.226803291773589,
|
||||
3.2270640893003435,
|
||||
3.227249518358305,
|
||||
3.227359127231524,
|
||||
3.2273924509469625,
|
||||
3.2273485621938347,
|
||||
3.22722863562996,
|
||||
3.227032979281687,
|
||||
3.2267620869419815,
|
||||
3.2264166368706584,
|
||||
3.2259974896059336,
|
||||
3.225505684975984,
|
||||
3.2249424394952255,
|
||||
3.224309142817072,
|
||||
3.2236073535652885,
|
||||
3.222838795285557,
|
||||
3.222005351549716,
|
||||
3.2211090610907003,
|
||||
3.220152110736951,
|
||||
3.21913591342886,
|
||||
3.2180639425025355,
|
||||
3.2169388049876204,
|
||||
3.2157632320581224,
|
||||
3.2145400725256823,
|
||||
3.213272285099224,
|
||||
3.2119629314217737,
|
||||
3.210615168117056,
|
||||
3.209232239485073,
|
||||
3.2078174691606125,
|
||||
3.2063742523121657,
|
||||
3.204906047188526,
|
||||
3.203416367201774,
|
||||
3.201908772307293,
|
||||
3.20038671156229,
|
||||
3.1988540415270714,
|
||||
3.197314448828512,
|
||||
3.1957715956753288,
|
||||
3.194229147910405,
|
||||
3.1926907667041973,
|
||||
3.19116010031245,
|
||||
3.1896407755313154,
|
||||
3.188136389507111,
|
||||
3.1866505018003437,
|
||||
3.18518662592634,
|
||||
3.1837482219382216,
|
||||
3.1823386881615674,
|
||||
3.1809613539268415,
|
||||
3.1796199070610616,
|
||||
3.178317365693067,
|
||||
3.1770566177141566,
|
||||
3.1758406383588778,
|
||||
3.1746722981220814,
|
||||
3.1735543560753845,
|
||||
3.1724894540381894,
|
||||
3.171480110448416,
|
||||
3.1705287147383436,
|
||||
3.169637521817997,
|
||||
3.168808647409722,
|
||||
3.168044062789466,
|
||||
3.16734559035941,
|
||||
3.166714899466848,
|
||||
3.1661541138486857,
|
||||
3.165664557595598,
|
||||
3.1652468672004876,
|
||||
3.1649020564911114,
|
||||
3.164630969966524,
|
||||
3.164434280442736,
|
||||
3.1643124874918267,
|
||||
3.164265915836695,
|
||||
3.164294714155108,
|
||||
3.164398854572734,
|
||||
3.164578132207916,
|
||||
3.164832165115698,
|
||||
3.1651603948311298,
|
||||
3.165562087003554,
|
||||
3.166036763487302,
|
||||
3.166583632347454,
|
||||
3.1672006737836313,
|
||||
3.1678864585242907,
|
||||
3.1686393915089255,
|
||||
3.1694577152711365,
|
||||
3.17033951339357,
|
||||
3.171282714587482,
|
||||
3.1722850969178604,
|
||||
3.1733442928081175,
|
||||
3.17445779387909,
|
||||
3.175622956745662,
|
||||
3.176837008657004,
|
||||
3.178097054026256,
|
||||
3.1794002164290207,
|
||||
3.1807436978583974,
|
||||
3.182123709232136,
|
||||
3.1835369281843153,
|
||||
3.1849799460334114,
|
||||
3.1864492760653347,
|
||||
3.1879413618349814,
|
||||
3.1894525859099345,
|
||||
3.19097927879319,
|
||||
3.1925177278192263,
|
||||
3.1940641867753663,
|
||||
3.195614884812199,
|
||||
3.197166036575152,
|
||||
3.198713851202913,
|
||||
3.2002545337987547,
|
||||
3.2017840862384728,
|
||||
3.2032988764817527,
|
||||
3.2047951919540822,
|
||||
3.2062693641104913,
|
||||
3.207717777711432,
|
||||
3.2091368805426974,
|
||||
3.2105231925781985,
|
||||
3.2118733148571565,
|
||||
3.2131839390335397,
|
||||
3.2144518552407257,
|
||||
3.2156739612615417,
|
||||
3.216847269859638,
|
||||
3.217968917317378,
|
||||
3.219036170327386,
|
||||
3.220045562724451,
|
||||
3.220995334928343,
|
||||
3.2218832986850447,
|
||||
3.2227073170066918,
|
||||
3.2234654153623845,
|
||||
3.224155785778181,
|
||||
3.2247767917974066,
|
||||
3.2253269721252744,
|
||||
3.2258050437055585,
|
||||
3.2262099052701587,
|
||||
3.2265406390922298,
|
||||
3.226796513255351,
|
||||
3.2269769825521033,
|
||||
3.227081689768787,
|
||||
3.22710955220279,
|
||||
3.227061168669443,
|
||||
3.2269370924239436,
|
||||
3.2267377237295465,
|
||||
3.2264636466881744,
|
||||
3.2261156276975886,
|
||||
3.2256946121982577,
|
||||
3.2252017217835,
|
||||
3.224638250833764,
|
||||
3.2240056622744246,
|
||||
3.223305583413101,
|
||||
3.222539801329067,
|
||||
3.2217102575995633,
|
||||
3.2208190428742154,
|
||||
3.219867854353387,
|
||||
3.218859259026148,
|
||||
3.2177961858992568,
|
||||
3.2166812675225214,
|
||||
3.2155172535532928,
|
||||
3.2143070040629325,
|
||||
3.2130534817570826,
|
||||
3.211759744984257,
|
||||
3.210428939916766,
|
||||
3.2090642928856465,
|
||||
3.207669102594001,
|
||||
3.2062467320151002,
|
||||
3.2048006003874154,
|
||||
3.2033341751083237,
|
||||
3.2018508739555798,
|
||||
3.2003542779095873,
|
||||
3.1988481127850616,
|
||||
3.1973359573002664,
|
||||
3.1958213973977445,
|
||||
3.194308017670146,
|
||||
3.19279939346063,
|
||||
3.1912990827323733,
|
||||
3.189810618404239,
|
||||
3.188337500433207,
|
||||
3.1868831881313007,
|
||||
3.185451092560893,
|
||||
3.1840445691293837,
|
||||
3.1826669104651053,
|
||||
3.1813214859380303,
|
||||
3.1800117901097846,
|
||||
3.178740483661364,
|
||||
3.177510533708781,
|
||||
3.1763248101985377,
|
||||
3.1751860802710605,
|
||||
3.1740970016898973,
|
||||
3.173060117741568,
|
||||
3.172077851413086,
|
||||
3.171152500485684,
|
||||
3.1702862322665037,
|
||||
3.169481079259431,
|
||||
3.1687389344676062,
|
||||
3.1680615476209555,
|
||||
3.1674506253282964,
|
||||
3.1669085513260424,
|
||||
3.1664356220663086,
|
||||
3.1660329701689696,
|
||||
3.165701568133256,
|
||||
3.165442226438076,
|
||||
3.165255590934966,
|
||||
3.16514214162142,
|
||||
3.1651021912290123,
|
||||
3.1651358837399672,
|
||||
3.1652431944171915,
|
||||
3.165423928768434,
|
||||
3.165677722868927,
|
||||
3.166004043509568,
|
||||
3.1664021891744154,
|
||||
3.1668723687065503,
|
||||
3.16741250732336,
|
||||
3.168021321234566,
|
||||
3.1686974411192694,
|
||||
3.1694393380619967,
|
||||
3.170245326209458,
|
||||
3.171113566351732,
|
||||
3.172042068956519,
|
||||
3.173028698759894,
|
||||
3.1740711784468334,
|
||||
3.1751670940119223,
|
||||
3.176313898883396,
|
||||
3.177508920042188,
|
||||
3.17874936360477,
|
||||
3.1800328259079818,
|
||||
3.181355841861699,
|
||||
3.1827151303301267,
|
||||
3.1841074729782854,
|
||||
3.1855295642750754,
|
||||
3.186978019262098,
|
||||
3.1884493811374868,
|
||||
3.1899401298427543,
|
||||
3.191446689954596,
|
||||
3.192965439405816,
|
||||
3.1944927184101823,
|
||||
3.1960248382013705,
|
||||
3.1975580901006118,
|
||||
3.199088754756803,
|
||||
3.200613009960425,
|
||||
3.20212708603014,
|
||||
3.2036273295775386,
|
||||
3.205110074007536,
|
||||
3.2065716893821072,
|
||||
3.208008592293136,
|
||||
3.209417254433106,
|
||||
3.210794212466389,
|
||||
3.212136076499309,
|
||||
3.213439539354214,
|
||||
3.214701385159765,
|
||||
3.215918497504247,
|
||||
3.2170878683639637,
|
||||
3.2182066051468428,
|
||||
3.2192715160336944
|
||||
],
|
||||
"cl_min": -1.021285623433455,
|
||||
"cl_max": 0.9865706078511706,
|
||||
"cl_midrange": -0.017357507791142257,
|
||||
"cl_amplitude": 2.0078562312846255,
|
||||
"cl_time_mean": -0.018055778744564696,
|
||||
"cl_waveform": [
|
||||
-1.0212846006341447,
|
||||
-1.0209838590222233,
|
||||
-1.0200727383007768,
|
||||
-1.0185590327265965,
|
||||
-1.0164441167317841,
|
||||
-1.01372972461554,
|
||||
-1.010417946688192,
|
||||
-1.0065112246383783,
|
||||
-1.0020123475697247,
|
||||
-0.9969244470282416,
|
||||
-0.9912509924392147,
|
||||
-0.984995786253974,
|
||||
-0.9781629589771562,
|
||||
-0.9707569640900402,
|
||||
-0.9627825730042762,
|
||||
-0.9542433948273606,
|
||||
-0.9451386764809715,
|
||||
-0.9354819238811002,
|
||||
-0.925279142068636,
|
||||
-0.9145366279059466,
|
||||
-0.9032609650239106,
|
||||
-0.8914590187491076,
|
||||
-0.8791379311317419,
|
||||
-0.8663051160407256,
|
||||
-0.8529682543310683,
|
||||
-0.8391352890960927,
|
||||
-0.8248144210074706,
|
||||
-0.8100141037255332,
|
||||
-0.7947430394336983,
|
||||
-0.7790101644090639,
|
||||
-0.7628173731216689,
|
||||
-0.7461816969118068,
|
||||
-0.7291127993693902,
|
||||
-0.7116205711659391,
|
||||
-0.6937151260047051,
|
||||
-0.6754067966482753,
|
||||
-0.6567061309580705,
|
||||
-0.637623888008147,
|
||||
-0.6181710341714578,
|
||||
-0.5983587392102484,
|
||||
-0.5781983723659854,
|
||||
-0.5577014984612264,
|
||||
-0.5368798738456273,
|
||||
-0.5157454424421091,
|
||||
-0.4943063234695201,
|
||||
-0.4725784485709182,
|
||||
-0.4505750883115271,
|
||||
-0.4283089024165666,
|
||||
-0.405792716370432,
|
||||
-0.38303951669758823,
|
||||
-0.36006244613658056,
|
||||
-0.3368747985998418,
|
||||
-0.3134900140010978,
|
||||
-0.28992167285852,
|
||||
-0.2661834907138685,
|
||||
-0.24228931233131965,
|
||||
-0.21825310567974987,
|
||||
-0.19408895564887746,
|
||||
-0.16980993582635662,
|
||||
-0.14543133426087812,
|
||||
-0.12096814315114233,
|
||||
-0.09643485175457375,
|
||||
-0.0718460310133605,
|
||||
-0.04721632579948139,
|
||||
-0.022560446976959612,
|
||||
0.0021068368194982773,
|
||||
0.02677070764406316,
|
||||
0.05141630692334945,
|
||||
0.07602874438036183,
|
||||
0.10059310719281325,
|
||||
0.1250944693656024,
|
||||
0.1495179012836364,
|
||||
0.17384754771042515,
|
||||
0.19806822588832526,
|
||||
0.22216578848678734,
|
||||
0.2461253934945549,
|
||||
0.2699322466441472,
|
||||
0.2935716120906063,
|
||||
0.3170288231832102,
|
||||
0.34028929328125385,
|
||||
0.36333852677889195,
|
||||
0.3861621301373943,
|
||||
0.40874582307035934,
|
||||
0.43107544954464694,
|
||||
0.4531369891575652,
|
||||
0.47491656822324385,
|
||||
0.4963986071823978,
|
||||
0.5175678949127962,
|
||||
0.5384141735804333,
|
||||
0.5589242775445442,
|
||||
0.5790852440767277,
|
||||
0.5988843242125788,
|
||||
0.6183089934932646,
|
||||
0.6373469625245218,
|
||||
0.6559861873413382,
|
||||
0.674214879706298,
|
||||
0.6920215170336557,
|
||||
0.7093948521976959,
|
||||
0.7263239230232448,
|
||||
0.742798061573706,
|
||||
0.7588052844335239,
|
||||
0.7743307471351503,
|
||||
0.7893708273667098,
|
||||
0.803916135973999,
|
||||
0.8179576199407215,
|
||||
0.8314865695778171,
|
||||
0.8444946252982337,
|
||||
0.8569737840555796,
|
||||
0.8689164052636061,
|
||||
0.8803152163390694,
|
||||
0.8911633177499939,
|
||||
0.9014541876864335,
|
||||
0.9111816861332408,
|
||||
0.9203400586319765,
|
||||
0.9289234860093117,
|
||||
0.936918266184497,
|
||||
0.9443288660701289,
|
||||
0.9511511188545412,
|
||||
0.9573812545471582,
|
||||
0.9630159006719111,
|
||||
0.9680520825648866,
|
||||
0.972487223197885,
|
||||
0.9763191426551175,
|
||||
0.9795460569396854,
|
||||
0.9821665765670635,
|
||||
0.9841797046166384,
|
||||
0.9855848344669915,
|
||||
0.986381747045911,
|
||||
0.9865706078511706,
|
||||
0.9861430751758404,
|
||||
0.9851078843149434,
|
||||
0.9834674674373464,
|
||||
0.9812235079135473,
|
||||
0.978378053362095,
|
||||
0.974933510845897,
|
||||
0.9708926418403944,
|
||||
0.9662585570602749,
|
||||
0.9610347110820011,
|
||||
0.9552248968839854,
|
||||
0.9488332402249186,
|
||||
0.941864193965974,
|
||||
0.9343225322984421,
|
||||
0.9262133449652685,
|
||||
0.9175353367316478,
|
||||
0.9082985884427632,
|
||||
0.8985116225127334,
|
||||
0.888180745849068,
|
||||
0.8773125515396398,
|
||||
0.8659139131624679,
|
||||
0.8539919791562397,
|
||||
0.841554167318266,
|
||||
0.8286081593376501,
|
||||
0.8151618954310706,
|
||||
0.8012235691354387,
|
||||
0.7868016221216644,
|
||||
0.7719047392049175,
|
||||
0.7565418433711996,
|
||||
0.7407178806800201,
|
||||
0.7244435174512682,
|
||||
0.707731607286953,
|
||||
0.6905919887305398,
|
||||
0.6730347151255153,
|
||||
0.6550700503410322,
|
||||
0.6367084643889164,
|
||||
0.617960629252382,
|
||||
0.5988374147135249,
|
||||
0.5793498841758384,
|
||||
0.5595092905790806,
|
||||
0.5393270722633181,
|
||||
0.5188148488816408,
|
||||
0.4979844172354143,
|
||||
0.4768457513845936,
|
||||
0.45541040875158884,
|
||||
0.43369366814580723,
|
||||
0.41170800001396685,
|
||||
0.38946603027749815,
|
||||
0.3669805357724072,
|
||||
0.3442644395643103,
|
||||
0.3213308061849796,
|
||||
0.2981928366787376,
|
||||
0.2748638635506233,
|
||||
0.2513573454944852,
|
||||
0.22768686201881655,
|
||||
0.20386610779915632,
|
||||
0.17990888695762292,
|
||||
0.15582865541317795,
|
||||
0.13163881464575702,
|
||||
0.10735496845190624,
|
||||
0.08299130047733835,
|
||||
0.05856207148298293,
|
||||
0.034081612308419704,
|
||||
0.009564316642990896,
|
||||
-0.014975366427826247,
|
||||
-0.039522940080061256,
|
||||
-0.06406386762101547,
|
||||
-0.08858358056301793,
|
||||
-0.11306748690571801,
|
||||
-0.1375009796129617,
|
||||
-0.16186944527282868,
|
||||
-0.186158127286089,
|
||||
-0.21035090571304071,
|
||||
-0.23443440343427951,
|
||||
-0.2583940736125971,
|
||||
-0.28221540953832974,
|
||||
-0.30588395428926807,
|
||||
-0.3293853105136673,
|
||||
-0.35270515033803884,
|
||||
-0.37582922531739726,
|
||||
-0.39874337658360604,
|
||||
-0.4214335449640186,
|
||||
-0.44388578123531364,
|
||||
-0.4660862563718654,
|
||||
-0.4880212718764244,
|
||||
-0.509677270114379,
|
||||
-0.531036136112504,
|
||||
-0.5520885997128909,
|
||||
-0.5728219120147833,
|
||||
-0.5932231908464959,
|
||||
-0.6132797468099019,
|
||||
-0.6329790933854313,
|
||||
-0.652308956917678,
|
||||
-0.6712572864574565,
|
||||
-0.6898122635200815,
|
||||
-0.7079623116377989,
|
||||
-0.7256961057871241,
|
||||
-0.7430025816000796,
|
||||
-0.7598709443367393,
|
||||
-0.7762906777176882,
|
||||
-0.7922455571624138,
|
||||
-0.8077296357950401,
|
||||
-0.8227349965022713,
|
||||
-0.8372523342423263,
|
||||
-0.8512726627071948,
|
||||
-0.8647873213967461,
|
||||
-0.8777879823088283,
|
||||
-0.8902666562070795,
|
||||
-0.902215698680155,
|
||||
-0.9136278156586564,
|
||||
-0.9244960686140175,
|
||||
-0.9348138793173668,
|
||||
-0.9445750342635408,
|
||||
-0.9537736885083689,
|
||||
-0.9623985419578303,
|
||||
-0.9704465786953729,
|
||||
-0.977916689291491,
|
||||
-0.9848045517658248,
|
||||
-0.99110622613045,
|
||||
-0.9968181555444723,
|
||||
-1.0019371672305317,
|
||||
-1.0064604728414646,
|
||||
-1.0103856687137327,
|
||||
-1.0137107350135544,
|
||||
-1.0164340351998324,
|
||||
-1.01855431505123,
|
||||
-1.0200707006187215,
|
||||
-1.0209826964966897,
|
||||
-1.021285623433455
|
||||
],
|
||||
"dp_min": 2.4203712005726827,
|
||||
"dp_max": 2.507012902468643,
|
||||
"dp_midrange": 2.463692051520663,
|
||||
"dp_amplitude": 0.08664170189596021,
|
||||
"dp_time_mean": 2.4639514695178963,
|
||||
"dp_waveform": [
|
||||
2.485558537878651,
|
||||
2.487354668330821,
|
||||
2.489094038859844,
|
||||
2.4907730268211923,
|
||||
2.4923875927671357,
|
||||
2.493933872161179,
|
||||
2.4954081841697953,
|
||||
2.4968070398569093,
|
||||
2.498127149841111,
|
||||
2.4993654310604727,
|
||||
2.5005190130830766,
|
||||
2.5015852434313093,
|
||||
2.502561692495173,
|
||||
2.50344615769302,
|
||||
2.5042366667973424,
|
||||
2.504931223854115,
|
||||
2.5055272123906085,
|
||||
2.5060247031878675,
|
||||
2.5064226784653965,
|
||||
2.5067203585292273,
|
||||
2.506917201491131,
|
||||
2.507012902468643,
|
||||
2.507007392300157,
|
||||
2.5069008355614204,
|
||||
2.506693628251957,
|
||||
2.5063863949686396,
|
||||
2.5059799860275485,
|
||||
2.505475473290949,
|
||||
2.504874146643661,
|
||||
2.5041775076417783,
|
||||
2.503385760083982,
|
||||
2.5025023981520067,
|
||||
2.501529545753141,
|
||||
2.500469519417466,
|
||||
2.4993248229208986,
|
||||
2.49809814052029,
|
||||
2.4967923311909144,
|
||||
2.495410422113925,
|
||||
2.493955601552369,
|
||||
2.492431212574658,
|
||||
2.490840745393346,
|
||||
2.489187830493204,
|
||||
2.4874762310352,
|
||||
2.4857098352761477,
|
||||
2.483891999686845,
|
||||
2.4820274911095415,
|
||||
2.480120662304518,
|
||||
2.4781758269143728,
|
||||
2.476197383057849,
|
||||
2.474189804509867,
|
||||
2.47215763257422,
|
||||
2.47010546680718,
|
||||
2.4680379564006794,
|
||||
2.465959791179922,
|
||||
2.4638756921667406,
|
||||
2.46179040254991,
|
||||
2.4597086782017046,
|
||||
2.457635278124787,
|
||||
2.455575155334104,
|
||||
2.4535330651884157,
|
||||
2.451513647441801,
|
||||
2.4495215742730494,
|
||||
2.447561471628255,
|
||||
2.4456379085376456,
|
||||
2.443755387861672,
|
||||
2.4419183356738783,
|
||||
2.4401310914614514,
|
||||
2.438397897721916,
|
||||
2.436722890392385,
|
||||
2.4351100884293455,
|
||||
2.4335633840618365,
|
||||
2.432086533067437,
|
||||
2.4306838103185973,
|
||||
2.4293586878640068,
|
||||
2.428113869269998,
|
||||
2.4269524606913686,
|
||||
2.4258773845443202,
|
||||
2.424891371376568,
|
||||
2.423996951340259,
|
||||
2.4231964462177444,
|
||||
2.4224919620804686,
|
||||
2.4218853823843163,
|
||||
2.421378360808599,
|
||||
2.4209723155762863,
|
||||
2.420668423809868,
|
||||
2.4204676161372016,
|
||||
2.4203712005726827,
|
||||
2.420380068704988,
|
||||
2.420493289903554,
|
||||
2.420710759315692,
|
||||
2.4210321100509016,
|
||||
2.421456711924972,
|
||||
2.4219836715842935,
|
||||
2.4226118326498916,
|
||||
2.423339777335187,
|
||||
2.424165828339519,
|
||||
2.4250880518614637,
|
||||
2.4261042616582924,
|
||||
2.4272120230151923,
|
||||
2.4284086587121188,
|
||||
2.4296915368281606,
|
||||
2.4310582510675482,
|
||||
2.432504302921731,
|
||||
2.4340261048245524,
|
||||
2.435619866098322,
|
||||
2.4372816029434006,
|
||||
2.4390071492364305,
|
||||
2.440792167926676,
|
||||
2.442632162995385,
|
||||
2.444522492162555,
|
||||
2.4464583798729533,
|
||||
2.448434930859836,
|
||||
2.450447144296273,
|
||||
2.4524899276311465,
|
||||
2.4545581275894874,
|
||||
2.4566467249172272,
|
||||
2.4587500444816213,
|
||||
2.460862775988026,
|
||||
2.462979593476627,
|
||||
2.4650951703383326,
|
||||
2.4672041949249874,
|
||||
2.469301385189656,
|
||||
2.4713815036357163,
|
||||
2.4734393717724394,
|
||||
2.475469884333746,
|
||||
2.477468023387656,
|
||||
2.4794288713654966,
|
||||
2.4813476246082624,
|
||||
2.483219605457596,
|
||||
2.485039458918412,
|
||||
2.4868033804993956,
|
||||
2.4885072430697144,
|
||||
2.4901469959998734,
|
||||
2.4917187690881257,
|
||||
2.4932188815730543,
|
||||
2.4946438498431105,
|
||||
2.49599039508062,
|
||||
2.4972554500082174,
|
||||
2.498436164504503,
|
||||
2.4995299110592493,
|
||||
2.5005342891380167,
|
||||
2.501447129364351,
|
||||
2.502266495991754,
|
||||
2.5029895274516014,
|
||||
2.5036154604052916,
|
||||
2.5041435094498383,
|
||||
2.5045727041834747,
|
||||
2.504902313989728,
|
||||
2.5051318472519433,
|
||||
2.505261050325141,
|
||||
2.5052899056087403,
|
||||
2.5052186294050998,
|
||||
2.5050476691328045,
|
||||
2.5047776999609392,
|
||||
2.504409621696276,
|
||||
2.5039445544003622,
|
||||
2.503383834013878,
|
||||
2.5027281389852822,
|
||||
2.5019794867872425,
|
||||
2.501140507540692,
|
||||
2.500213360279151,
|
||||
2.4992003931281483,
|
||||
2.498104137409639,
|
||||
2.4969273011438373,
|
||||
2.495672762685901,
|
||||
2.494343563852958,
|
||||
2.492942903234649,
|
||||
2.491474128676318,
|
||||
2.4899407305330525,
|
||||
2.488346334061092,
|
||||
2.486694691617208,
|
||||
2.484989340356667,
|
||||
2.4832342121680133,
|
||||
2.4814339161814924,
|
||||
2.479592639951934,
|
||||
2.4777146583088454,
|
||||
2.4758043245876524,
|
||||
2.473866062448869,
|
||||
2.471904357327426,
|
||||
2.4699237475913485,
|
||||
2.4679288158413883,
|
||||
2.46592417988613,
|
||||
2.4639144839871556,
|
||||
2.461904389322368,
|
||||
2.459898565346157,
|
||||
2.4579017367770137,
|
||||
2.455918764942065,
|
||||
2.453954173148493,
|
||||
2.4520125671214457,
|
||||
2.4500985125535784,
|
||||
2.448216525842054,
|
||||
2.4463710641219887,
|
||||
2.444566515822548,
|
||||
2.4428071908357323,
|
||||
2.4410973107465668,
|
||||
2.439440999504861,
|
||||
2.4378422733498937,
|
||||
2.4363050317847414,
|
||||
2.434833047645694,
|
||||
2.43343006971473,
|
||||
2.432100619437783,
|
||||
2.430846995094142,
|
||||
2.429672363555455,
|
||||
2.428579719706815,
|
||||
2.427571878080977,
|
||||
2.426651464455842,
|
||||
2.4258209081907864,
|
||||
2.425082434921927,
|
||||
2.4244380590290993,
|
||||
2.423889577520045,
|
||||
2.4234385632168207,
|
||||
2.423086359460975,
|
||||
2.4228340742719734,
|
||||
2.4226825762746023,
|
||||
2.4226340587576405,
|
||||
2.4226874437485133,
|
||||
2.422842736063271,
|
||||
2.4230997975159894,
|
||||
2.4234582347462745,
|
||||
2.4239173992440146,
|
||||
2.424476386499562,
|
||||
2.4251340367353267,
|
||||
2.425888936234961,
|
||||
2.4267394193027876,
|
||||
2.427683571153293,
|
||||
2.428719231244257,
|
||||
2.429843998149402,
|
||||
2.431055234079425,
|
||||
2.432351101512926,
|
||||
2.4337276833933763,
|
||||
2.4351813534956155,
|
||||
2.43670858740822,
|
||||
2.4383056624813797,
|
||||
2.4399686671928897,
|
||||
2.4416935114716893,
|
||||
2.4434759376701103,
|
||||
2.4453115320216026,
|
||||
2.447195736733967,
|
||||
2.4491238624275873,
|
||||
2.451091101018784,
|
||||
2.4530925394872574,
|
||||
2.4551231733170757,
|
||||
2.4571781139809588,
|
||||
2.4592520095527344,
|
||||
2.4613395134290164,
|
||||
2.4634353978388575,
|
||||
2.46553441673564,
|
||||
2.467631320555607,
|
||||
2.469720870921841,
|
||||
2.4717978552593056,
|
||||
2.4738571012373916,
|
||||
2.4758934909645545,
|
||||
2.477901974977891,
|
||||
2.479877585675147,
|
||||
2.481815450926397,
|
||||
2.4837108064957207,
|
||||
2.485558618881201
|
||||
],
|
||||
"mass_min": 1.0,
|
||||
"mass_max": 1.0,
|
||||
"mass_midrange": 1.0,
|
||||
"mass_amplitude": 0.0,
|
||||
"mass_time_mean": 1.0,
|
||||
"flux_in_min": 1.0,
|
||||
"flux_in_max": 1.0,
|
||||
"flux_in_midrange": 1.0,
|
||||
"flux_in_amplitude": 0.0,
|
||||
"flux_in_time_mean": 1.0,
|
||||
"flux_out_min": 1.0,
|
||||
"flux_out_max": 1.0,
|
||||
"flux_out_midrange": 1.0,
|
||||
"flux_out_amplitude": 0.0,
|
||||
"flux_out_time_mean": 1.0,
|
||||
"flux_relative": 0.0,
|
||||
"source": "https://wwwold.mathematik.tu-dortmund.de/~featflow/media/dfg_bench2new_2d/20141124_dfg_bench2_official.zip",
|
||||
"archive_sha256": "08db59605b2f60efc512d2bbc79628ea5d1cc720c0986938a16920884a0215be",
|
||||
"selection": "lv6 dt4 last complete lift-minimum-to-minimum cycle; force columns 2,4,5; pressure columns 2,7-12; physical time multiplied by 10",
|
||||
"note": "Newer official high-order line-integration data; do not mix older website table with this reference."
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"commit": "47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5",
|
||||
"production_sha256": {
|
||||
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
|
||||
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
|
||||
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
|
||||
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
|
||||
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
|
||||
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
|
||||
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
|
||||
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
|
||||
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
|
||||
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
|
||||
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
|
||||
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
|
||||
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
|
||||
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
|
||||
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
|
||||
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
|
||||
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
|
||||
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
|
||||
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
|
||||
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
|
||||
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
|
||||
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
|
||||
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
|
||||
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
|
||||
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
|
||||
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
|
||||
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
|
||||
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
|
||||
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
|
||||
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
|
||||
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
|
||||
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
|
||||
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
|
||||
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
|
||||
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
|
||||
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
|
||||
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
|
||||
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
|
||||
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
|
||||
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
|
||||
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
|
||||
"src/CelerisLab/lbm/channel.py": "2d5b5ff43414cb35633ac4ce90ce2c3a3353950b25c168e1c2ab58d498dd8b0f",
|
||||
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
|
||||
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
|
||||
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
|
||||
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
|
||||
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
|
||||
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
|
||||
"src/CelerisLab/lbm/kernels/blocks.cu": "e3b9606f861bfea8c4b0f34e8356bb230081f899c584f67be89178d889099b17",
|
||||
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
|
||||
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
|
||||
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
|
||||
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
|
||||
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
|
||||
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
|
||||
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
|
||||
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
|
||||
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
|
||||
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
|
||||
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
|
||||
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
|
||||
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
|
||||
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
|
||||
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
|
||||
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
|
||||
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
|
||||
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
|
||||
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
|
||||
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
|
||||
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
|
||||
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
|
||||
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
|
||||
"src/CelerisLab/lbm/kernels/refinement.cu": "129f3d91da93b4324f88a0da73a4da1e5390f72c14cce574ee63306712da9895",
|
||||
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
|
||||
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "befbf6d757e532a59c166d41f920d16d71557cd3a574478e7aa83da737c48cbd",
|
||||
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
|
||||
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
|
||||
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
|
||||
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
|
||||
"src/CelerisLab/lbm/raw_exchange.py": "8694202466eba848563e298656353b0f5db94e21a01fa05f51aad6dc2b9fd311",
|
||||
"src/CelerisLab/lbm/refinement.py": "08c2a0557276a98cbcaa459d9bb7a9735d8bbbf64a6114c57af2fda61481a687",
|
||||
"src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d",
|
||||
"src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6",
|
||||
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
|
||||
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
|
||||
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c"
|
||||
},
|
||||
"experiment": "AMR-channel development source; GPU/flow validation not inherited from d79",
|
||||
"source_origin_commits": [
|
||||
"5338864748ac67fe2544487c059ba149ebf916da",
|
||||
"47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5"
|
||||
],
|
||||
"historical_checkpoints": {
|
||||
"production": "d79e3f6f0a424f5bcc3a1a6d610313e49daf97a2",
|
||||
"re100_jet": "69e9b78de9483a5ff21e2d0c74311d240bfaf88c",
|
||||
"les_diagnostic": "20ae6770924df15f31305aad56e44a54a3dd4d88"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"revision": "47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5",
|
||||
"files": {
|
||||
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
|
||||
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
|
||||
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
|
||||
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
|
||||
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
|
||||
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
|
||||
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
|
||||
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
|
||||
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
|
||||
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
|
||||
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
|
||||
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
|
||||
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
|
||||
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
|
||||
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
|
||||
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
|
||||
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
|
||||
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
|
||||
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
|
||||
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
|
||||
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
|
||||
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
|
||||
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
|
||||
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
|
||||
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
|
||||
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
|
||||
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
|
||||
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
|
||||
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
|
||||
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
|
||||
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
|
||||
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
|
||||
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
|
||||
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
|
||||
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
|
||||
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
|
||||
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
|
||||
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
|
||||
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
|
||||
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
|
||||
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
|
||||
"src/CelerisLab/lbm/channel.py": "2d5b5ff43414cb35633ac4ce90ce2c3a3353950b25c168e1c2ab58d498dd8b0f",
|
||||
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
|
||||
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
|
||||
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
|
||||
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
|
||||
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
|
||||
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
|
||||
"src/CelerisLab/lbm/kernels/blocks.cu": "e3b9606f861bfea8c4b0f34e8356bb230081f899c584f67be89178d889099b17",
|
||||
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
|
||||
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
|
||||
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
|
||||
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
|
||||
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
|
||||
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
|
||||
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
|
||||
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
|
||||
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
|
||||
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
|
||||
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
|
||||
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
|
||||
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
|
||||
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
|
||||
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
|
||||
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
|
||||
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
|
||||
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
|
||||
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
|
||||
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
|
||||
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
|
||||
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
|
||||
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
|
||||
"src/CelerisLab/lbm/kernels/refinement.cu": "129f3d91da93b4324f88a0da73a4da1e5390f72c14cce574ee63306712da9895",
|
||||
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
|
||||
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "befbf6d757e532a59c166d41f920d16d71557cd3a574478e7aa83da737c48cbd",
|
||||
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
|
||||
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
|
||||
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
|
||||
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
|
||||
"src/CelerisLab/lbm/raw_exchange.py": "8694202466eba848563e298656353b0f5db94e21a01fa05f51aad6dc2b9fd311",
|
||||
"src/CelerisLab/lbm/refinement.py": "08c2a0557276a98cbcaa459d9bb7a9735d8bbbf64a6114c57af2fda61481a687",
|
||||
"src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d",
|
||||
"src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6",
|
||||
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
|
||||
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
|
||||
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c"
|
||||
},
|
||||
"experiment": "AMR-channel development source; GPU/flow validation not inherited from d79",
|
||||
"source_origin_commits": [
|
||||
"5338864748ac67fe2544487c059ba149ebf916da",
|
||||
"47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5"
|
||||
],
|
||||
"historical_checkpoints": {
|
||||
"production": "d79e3f6f0a424f5bcc3a1a6d610313e49daf97a2",
|
||||
"re100_jet": "69e9b78de9483a5ff21e2d0c74311d240bfaf88c",
|
||||
"les_diagnostic": "20ae6770924df15f31305aad56e44a54a3dd4d88"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"base_commit": "cd3a657f9d3833e812c3cca173525f165b48710a",
|
||||
"sources": {
|
||||
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
|
||||
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
|
||||
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
|
||||
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
|
||||
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
|
||||
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
|
||||
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
|
||||
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
|
||||
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
|
||||
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
|
||||
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
|
||||
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
|
||||
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
|
||||
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
|
||||
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
|
||||
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
|
||||
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
|
||||
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
|
||||
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
|
||||
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
|
||||
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
|
||||
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
|
||||
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
|
||||
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
|
||||
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
|
||||
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
|
||||
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
|
||||
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
|
||||
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
|
||||
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
|
||||
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
|
||||
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
|
||||
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
|
||||
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
|
||||
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
|
||||
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
|
||||
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
|
||||
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
|
||||
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
|
||||
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
|
||||
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
|
||||
"src/CelerisLab/lbm/channel.py": "2d5b5ff43414cb35633ac4ce90ce2c3a3353950b25c168e1c2ab58d498dd8b0f",
|
||||
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
|
||||
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
|
||||
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
|
||||
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
|
||||
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
|
||||
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
|
||||
"src/CelerisLab/lbm/kernels/blocks.cu": "e3b9606f861bfea8c4b0f34e8356bb230081f899c584f67be89178d889099b17",
|
||||
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
|
||||
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
|
||||
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
|
||||
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
|
||||
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
|
||||
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
|
||||
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
|
||||
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
|
||||
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
|
||||
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
|
||||
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
|
||||
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
|
||||
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
|
||||
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
|
||||
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
|
||||
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
|
||||
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
|
||||
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
|
||||
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
|
||||
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
|
||||
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
|
||||
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
|
||||
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
|
||||
"src/CelerisLab/lbm/kernels/refinement.cu": "129f3d91da93b4324f88a0da73a4da1e5390f72c14cce574ee63306712da9895",
|
||||
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
|
||||
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "befbf6d757e532a59c166d41f920d16d71557cd3a574478e7aa83da737c48cbd",
|
||||
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
|
||||
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
|
||||
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
|
||||
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
|
||||
"src/CelerisLab/lbm/raw_exchange.py": "8694202466eba848563e298656353b0f5db94e21a01fa05f51aad6dc2b9fd311",
|
||||
"src/CelerisLab/lbm/refinement.py": "08c2a0557276a98cbcaa459d9bb7a9735d8bbbf64a6114c57af2fda61481a687",
|
||||
"src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d",
|
||||
"src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6",
|
||||
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
|
||||
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
|
||||
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c"
|
||||
},
|
||||
"candidate_header_sha256": "a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa",
|
||||
"production_origin_commit": "47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5",
|
||||
"source_experiment": "Explicit channel47/cd3 source experiment; not inherited d79 GPU acceptance. Corrected variant permits only the reviewed Smagorinsky header patch.",
|
||||
"historical_preparation_commit": "d79e3f6f0a424f5bcc3a1a6d610313e49daf97a2"
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"base_commit": "a135252ae78b47b57d7e77d19001700b3bedac8b",
|
||||
"production_origin_commit": "a135252ae78b47b57d7e77d19001700b3bedac8b",
|
||||
"source_experiment": "Explicit a135252 uniform SRT LES regression. Sources pin the committed corrected tree; baseline is a synthetic isolated reverse-header control, not the committed a135252 source. Historical profiles and receipts remain unchanged.",
|
||||
"sources": {
|
||||
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
|
||||
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
|
||||
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
|
||||
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
|
||||
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
|
||||
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
|
||||
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
|
||||
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
|
||||
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
|
||||
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
|
||||
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
|
||||
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
|
||||
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
|
||||
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
|
||||
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
|
||||
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
|
||||
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
|
||||
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
|
||||
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
|
||||
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
|
||||
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
|
||||
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
|
||||
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
|
||||
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
|
||||
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
|
||||
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
|
||||
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
|
||||
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
|
||||
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
|
||||
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
|
||||
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
|
||||
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
|
||||
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
|
||||
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
|
||||
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
|
||||
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
|
||||
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
|
||||
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
|
||||
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
|
||||
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
|
||||
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
|
||||
"src/CelerisLab/lbm/channel.py": "40d0354e5dc97bcdb7e7b5c1e7f9303a109b7134d8a3af88b8ff4cd75ab5a095",
|
||||
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
|
||||
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
|
||||
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
|
||||
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
|
||||
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
|
||||
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
|
||||
"src/CelerisLab/lbm/kernels/blocks.cu": "668e5cf14e88bdcfa281c2fa6c75c0f8f8cdbbfb8ba2ca03064ae02ef7ad0057",
|
||||
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
|
||||
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
|
||||
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
|
||||
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
|
||||
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
|
||||
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
|
||||
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
|
||||
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
|
||||
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
|
||||
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
|
||||
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
|
||||
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
|
||||
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
|
||||
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
|
||||
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
|
||||
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
|
||||
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
|
||||
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
|
||||
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
|
||||
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
|
||||
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
|
||||
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
|
||||
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa",
|
||||
"src/CelerisLab/lbm/kernels/refinement.cu": "b45aad9c022cecb64f7e968ec3cae2d8385a3cc430ee409700c404a3f4dd8cf8",
|
||||
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
|
||||
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "ee8a31398b8e5233d977c816e6b8e060c1eb2ec71320e594226dbca55593c0d0",
|
||||
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
|
||||
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
|
||||
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
|
||||
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
|
||||
"src/CelerisLab/lbm/raw_exchange.py": "9031a196006a0f74dee4028e6f3bb673bc19ef7959f9f04dde9e01f93293aeb3",
|
||||
"src/CelerisLab/lbm/refinement.py": "100d5e46ac894f6008b61364cdad5d37c2ca7ed949d8d71405572d6366526417",
|
||||
"src/CelerisLab/lbm/refinement_stepper.py": "536cd1d3482591e4ff57c4ae8e888c19e31701687d287aa509129df5b2a43be3",
|
||||
"src/CelerisLab/lbm/refinement_tables.py": "0cbc0f29aabd0ab4ba1f912bd4d0ac2b430246584cb6ddf13dd28e10582accf3",
|
||||
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
|
||||
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
|
||||
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c"
|
||||
},
|
||||
"candidate_header_sha256": "a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa",
|
||||
"baseline_header_sha256": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user