Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3552e6ee92 | ||
|
|
afa8f6aca1 | ||
|
|
70c83b7233 | ||
|
|
be8e6d691d | ||
|
|
1f64f7eb11 | ||
|
|
caa573f978 | ||
|
|
1e188e7419 | ||
|
|
02bde0f751 | ||
|
|
335624c27b | ||
|
|
9748ef91ef | ||
|
|
5fb95f5f4d | ||
|
|
46100e1d53 | ||
|
|
de5b0cc080 | ||
|
|
f337652683 | ||
|
|
3554cb2fde | ||
|
|
10069feadb | ||
|
|
ffff742b69 | ||
|
|
92b8afeeed | ||
|
|
eec4677db1 | ||
|
|
2cf9853952 | ||
|
|
1aa8110ec8 | ||
|
|
a135252ae7 | ||
|
|
ed9d8c937d | ||
|
|
fc0e544c0a | ||
|
|
95cb6d2b9f | ||
|
|
53fad76759 | ||
|
|
8403956a9a | ||
|
|
185a919cc5 |
@@ -123,7 +123,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 can use level lists of arbitrary depth; the
|
||||
legacy two/three-level forms are shown below.
|
||||
- `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)`),
|
||||
@@ -135,16 +137,20 @@ Scope and limits:
|
||||
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
|
||||
`bounds`, with both `fine_block` and `base_block`. 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. `fine_objects`/`jets` 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, use the level lists or legacy child forms below.
|
||||
|
||||
```python
|
||||
cfg = LBMConfig(nx=32, ny=16, viscosity=.1)
|
||||
@@ -158,7 +164,7 @@ Scope and limits:
|
||||
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
|
||||
- Legacy three-level scalar 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
|
||||
@@ -192,14 +198,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: `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.
|
||||
These lists are mutually exclusive with the legacy patch keywords. 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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -1,8 +1,11 @@
|
||||
# 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 the legacy three-level entry point of `FixedPatchStepper`,
|
||||
with rectangle unions on either adjacent pair. The `refinements` level-list
|
||||
entry point also 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.
|
||||
@@ -30,12 +33,15 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,58 @@
|
||||
# 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.
|
||||
@@ -159,3 +159,448 @@ 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.
|
||||
|
||||
## Oblique shear wave (test-only diagnostic, not GPU-qualified)
|
||||
|
||||
`shear_wave.py` reuses the frozen oracle's constants, cell centers and
|
||||
geometry. `oracle.py` is byte-identical, and `test_shear_wave.py` pins its
|
||||
eec4677 SHA-256. `run_case.py --flow shear_wave` selects the shear wave;
|
||||
the default flow and its receipt are the unchanged Taylor–Green path.
|
||||
|
||||
### Convention
|
||||
|
||||
Integer mode (2,1) on the L=N periodic root domain:
|
||||
- the wavevector `k = (2*pi/L)*(2,1)` points 26.565° from +x;
|
||||
- mode (1,2) would point 63.435°, and a wrong-mode test catches a swap.
|
||||
|
||||
Polarization is `E = (-1,2)/sqrt(5)`, perpendicular to k:
|
||||
|
||||
u = U*E*sin(k.x + phi)*exp(-nu*|k|^2*t), phi = 0, p = const, rho0 = 1
|
||||
|
||||
`nu = U*N/100`, the Taylor–Green Re100 viscosity at the same N. This is an
|
||||
exact incompressible Navier–Stokes solution. The initial condition is FP64
|
||||
second-order equilibrium at the actual leaf centers, with no
|
||||
non-equilibrium part.
|
||||
|
||||
### Physical caveats
|
||||
|
||||
1. *Scope of the cubic-term argument.* The second-order D2Q9 equilibrium
|
||||
lacks the cubic third moment `rho*u_a*u_b*u_g` of the Maxwellian (Dellar,
|
||||
https://people.maths.ox.ac.uk/dellar/papers/Galilean.pdf, eq. 3). At
|
||||
Navier–Stokes order this produces a stress error proportional to
|
||||
`d_g(rho u_a u_b u_g)`.
|
||||
|
||||
For the ideal constant-density, zero-background transverse wave, k.E = 0
|
||||
makes that contracted derivative vanish. That is the only claim made.
|
||||
It does not remove other discrete or finite-Mach errors. It is not
|
||||
guaranteed once AMR introduces density, longitudinal or higher-mode
|
||||
contamination. The analytic NS field itself is exact.
|
||||
2. *Startup.* Equilibrium initialization is not the discrete shear
|
||||
eigenmode (Mei et al., https://doi.org/10.1016/j.compfluid.2005.08.008).
|
||||
- The decay rate is a free-intercept least-squares fit of ln R over the
|
||||
samples with t* >= 0.12, where `R = hypot(a_hat, b_hat)`.
|
||||
- The intercept cancels only a constant amplitude prefactor, and only
|
||||
once a single exponential dominates.
|
||||
- Per-interval rates and the maximum log-fit residual are kept, so a
|
||||
lingering transient stays visible.
|
||||
- Late sampling alone does not show that transients have gone.
|
||||
|
||||
### Metrics
|
||||
|
||||
Computed on ACTIVE leaf cells, each weighted by its area `4**-level` in
|
||||
root-cell units. The partition must cover the domain exactly once, and one
|
||||
global pressure gauge is used.
|
||||
|
||||
- `a_hat`, `b_hat`: weighted least-squares fit of `u.E` onto sin/cos, using
|
||||
the 2x2 Gram matrix of the actual composite quadrature. A wrong Fourier
|
||||
mode need not project to exactly zero on this quadrature.
|
||||
- `amplitude_error = R/A_exact - 1`.
|
||||
- `phase_drift = atan2(b_hat, a_hat)`, in radians, reported separately.
|
||||
- Rates are positive (minus the fitted slope).
|
||||
- Per root step they are compared with `nu*|k|^2`. Uniform 2N uses its
|
||||
own `nu = 0.0768` at N=256 and doubled step counts.
|
||||
- In t* they are compared with `5*(2*pi)^2/Re = 1.97392088`.
|
||||
- `rate_error = |rate/exact - 1|`.
|
||||
- `velocity_l2`, `residual_l2` (velocity left after removing the fitted
|
||||
mode) and `longitudinal_l2` (the `u.k_hat` part), all relative to the
|
||||
exact-field norm. A longitudinal perturbation also raises the
|
||||
full-vector residual and the velocity L2.
|
||||
- `spurious_pressure_rms`: the raw gauged rms of `p = (rho-1)/3`.
|
||||
- `spurious_pressure_scaled`: that rms divided by `A_exact(t)^2/4`, a
|
||||
continuum reference scale. It is *not* the discrete, globally gauged
|
||||
Taylor–Green norm on the composite grid. Exact pressure is constant, so
|
||||
no relative-to-exact claim is made. Max and per-level values are kept.
|
||||
|
||||
### Preregistered screens (`shear_wave.assess`)
|
||||
|
||||
Run set: uniform N, uniform 2N, and refined depth-2 runs at root N.
|
||||
|
||||
- `eps(N) = (|k| h_root)^2 = 5*(2*pi/N)^2`: dimensionless, coefficient
|
||||
one, 0.01205 at N=128. This is a chosen screen, not a proven error bound.
|
||||
- Absolute screens:
|
||||
- rate_error, max |phase_drift| (radians) and max residual_l2 must each
|
||||
be <= eps;
|
||||
- velocity_l2 <= 0.05, mass <= 1e-5, spurious_pressure_scaled <= 0.02.
|
||||
- Budgets keep the `assess.py` form, `refined <= max(2*control_N, floor)`:
|
||||
- floors are 0.002 for rate and residual, 0.02 for pressure;
|
||||
- the comparative pressure budget is always redundant:
|
||||
`max(2*control, 0.02)` is never below the 0.02 absolute ceiling;
|
||||
- raw ratios to both the root and finest controls, including raw
|
||||
pressure rms, are always reported.
|
||||
- Every record is validated before any screen is applied:
|
||||
- exact scheduled steps and t*, with no duplicates;
|
||||
- every numeric field finite, including list fields and density;
|
||||
- error quantities nonnegative and rho_min > 0;
|
||||
- signed quantities (amplitudes, `amplitude_error`, `phase_drift`,
|
||||
per-interval rate errors) keep their sign.
|
||||
- Outcomes are kept distinct:
|
||||
- INVALID_CONTROL: a control or refined run is missing, a control is
|
||||
incomplete or invalid, or the records come from mixed sources;
|
||||
- INVALID_RUN: a refined record fails validation;
|
||||
- CONTROL_SCREEN_FAIL: complete, valid controls fail their own screens.
|
||||
Each refined run's `own_screens_pass` is still reported, but
|
||||
`controlled_pass` is false, so it is never a controlled-comparison pass;
|
||||
- PASS/FAIL: every refined run's `controlled_pass` (own screens and
|
||||
budgets against valid, passing controls).
|
||||
- Finite modal extinction (late R = 0) is a physical failure, not
|
||||
malformed data. The run gets `rate_fit_valid=false` with null fit
|
||||
quantities, and its other metrics are kept. Nothing is clipped and no
|
||||
rate is invented. The result is CONTROL_SCREEN_FAIL for a control, or
|
||||
FAIL for a refined run.
|
||||
- Screens are never retuned.
|
||||
|
||||
### Limitations
|
||||
|
||||
One flow, one phase and one rectangle, SRT only. The rectangle's vertical
|
||||
faces span 0.625 tangential periods and its horizontal faces 1.5, so this
|
||||
is not placement robustness. Two resolutions show a trend, not an order.
|
||||
|
||||
### Receipt assessment and runner integrity
|
||||
|
||||
- Fixed four-case N128 contract:
|
||||
`python assess_shear_wave.py U128.json U256.json R128_constant.json R128_projected.json --output report.json --expected-revision <sha>`.
|
||||
It reuses the shared provenance, build, workload and sample-time checks
|
||||
from `assess_projected_pairs.py`, and adds shear metadata checks (flow,
|
||||
mode, polarization, angle, phase, eps, limits, disabled early trace, and
|
||||
pinned `shear_wave.py` hash).
|
||||
- Missing, extra, duplicate or mixed-source records give `INVALID_SET`,
|
||||
as does any failed shared check.
|
||||
- Otherwise the result comes from `shear_wave.assess`, which separates
|
||||
`INVALID_CONTROL` from `CONTROL_SCREEN_FAIL`.
|
||||
- `run_case.verify_source(..., flow="shear_wave")` requires the imported
|
||||
`shear_wave.py` to be the selected harness file with the pinned
|
||||
`SHEAR_WAVE_SHA256`. The default TG flow does not import it.
|
||||
- Sample and READY finiteness use `finite_row`, which accepts scalar and
|
||||
list diagnostics. For TG's all-scalar rows it behaves the same as before.
|
||||
- Merge note: native 92b8afe adds a `scaled_n256` receipt block. When the
|
||||
two are merged, keep both blocks and make `--flow shear_wave` and
|
||||
`scaled_n256` mutually exclusive. Not merged here.
|
||||
|
||||
### Scaled shear diagnostic (opt-in `scaled_shear_n256`)
|
||||
|
||||
Runner: `run_case.py --flow shear_wave --diagnostic scaled_shear_n256 --re 100`
|
||||
with one of these four cases:
|
||||
- `--n 256 --depth 1` (U256)
|
||||
- `--n 512 --depth 1` (U512)
|
||||
- `--n 256 --depth 2 --explosion constant` (constant R256)
|
||||
- `--n 256 --depth 2 --explosion projected_linear` (projected R256)
|
||||
|
||||
Everything physical is unchanged from N128:
|
||||
- `rectangles(256,2)` covers the same physical box;
|
||||
- same t* schedule (16N root steps);
|
||||
- `nu = U*N/100`;
|
||||
- same `eps(N)`, limits and floors.
|
||||
|
||||
`scaled_n256` (Taylor–Green, from native 92b8afe) and `scaled_shear_n256`
|
||||
each require their own flow, so they are mutually exclusive. N512 is
|
||||
accepted only through one of the two diagnostics. The shear diagnostic
|
||||
takes no early-pressure trace.
|
||||
|
||||
Assessment:
|
||||
|
||||
python assess_shear_wave.py U256.json U512.json R256_constant.json R256_projected.json \
|
||||
--diagnostic scaled_shear_n256 --reference-files <four de5b0cc N128 receipts> \
|
||||
--output report.json --expected-revision <sha>
|
||||
|
||||
- The scaled N256/512 case, build and workload checks come from the
|
||||
92b8afe assessor. Its TG-only diagnostic identity and early-sampling
|
||||
checks are not used. Instead every record must carry
|
||||
`diagnostic=scaled_shear_n256`, and N128 records must carry none.
|
||||
- The reference N128 receipts are hash-pinned to the audited de5b0cc
|
||||
evidence (commit cba6bca). They are never relabelled as fresh controls.
|
||||
- The report always includes, for each case:
|
||||
- the raw signed rate bias `rate/exact - 1` and the interval errors;
|
||||
- each refined run's signed excess over *each* uniform control (rate
|
||||
bias, residual, raw pressure rms);
|
||||
- the N128→N256 signed changes and ratios. The ratio is null when the
|
||||
N128 value is zero.
|
||||
- These are descriptive. Passing or failing another floor-controlled
|
||||
screen is not the conclusion, and a two-resolution ratio is not a
|
||||
convergence order.
|
||||
- Portability: the receipt's recomputed `eps = |k|^2` is compared within
|
||||
4 ULPs (`close_ulps`), not by exact equality. A cross-runtime replay
|
||||
differed by one ULP: 0.012047856934923534 on Horsea vs
|
||||
0.012047856934923532 elsewhere. Source and file hashes, case identity,
|
||||
the other metadata and every scientific threshold stay exact.
|
||||
|
||||
### Nested shear preset (opt-in `nested_shear_n128`)
|
||||
|
||||
A fixed three-level, two-region preset, defined in `shear_nested.py`. The
|
||||
wave, phase, Re100/U=0.03, initialization, five sample times, eps(root N),
|
||||
limits and floors are the same as before.
|
||||
|
||||
- Refinements, given as parent-frame `(x0, y0, x1, y1)` rectangles on
|
||||
level-wide, zero-origin unions:
|
||||
- L0→L1: `(16,16,60,112)`, `(68,24,112,120)`
|
||||
- L1→L2: `(44,44,108,212)`, `(148,60,212,228)`
|
||||
- Leaves `[7936, 12288, 86016]`, hops `[2048, 4096, 8192]`, work
|
||||
771227648 per refined case; 3723493376 for all four cases.
|
||||
- Level ν `[.0384, .0768, .1536]`, τ `[.6152, .7304, .9608]`.
|
||||
|
||||
Cases (`run_case.py --flow shear_wave --diagnostic nested_shear_n128 --re 100`):
|
||||
- `--n 128 --depth 1`
|
||||
- `--n 512 --depth 1` — the finest control is 4N, not 2N
|
||||
- `--n 128 --depth 3`
|
||||
- `--n 128 --depth 3 --explosion projected_linear [--final-snapshot]`
|
||||
|
||||
`--final-snapshot` is allowed only for the projected depth-3 case. It writes
|
||||
`<output>.final_macro.npz`, containing:
|
||||
- ACTIVE leaves packed as `level`/`i`/`j`;
|
||||
- `rho`, `ux`, `uy` from the FP64 physical populations at the final root
|
||||
synchronization;
|
||||
- embedded metadata: exact time, phase, mode and config.
|
||||
|
||||
It contains no populations, restart state or time series. Its SHA-256 and
|
||||
size go into the receipt. A file over 8 MiB is removed and reported, never
|
||||
kept. All old paths stay scalar-only.
|
||||
|
||||
Assessment: `assess_shear_wave.py U128.json U512.json R128c.json R128p.json
|
||||
--diagnostic nested_shear_n128 --output report.json --expected-revision <sha>`.
|
||||
- It reuses the case-agnostic provenance checks from the TG assessor, plus
|
||||
exact nested scales, builds, patch, workload, metadata and the pinned
|
||||
`shear_nested.py` hash.
|
||||
- The physics is `shear_wave.assess` generalized only in its depth-3 / 4N
|
||||
shape contract; the metrics are unchanged.
|
||||
- `shear_wave.py` is byte-identical (81d5c8ee…), so historical receipt
|
||||
identities and verdicts are untouched.
|
||||
- This preset changes placement and refined volume as well as depth. It
|
||||
gives no isolated cost of an extra level and no resolution trend.
|
||||
|
||||
### Depth-2 twin of the nested preset (opt-in `twin_shear_n128`)
|
||||
|
||||
`shear_twin.py` uses the same two L0→L1 boxes as `shear_nested.py`, with no
|
||||
L2. Leaves are `[7936, 33792]`, hops `[2048, 4096]`, and work is 154664960 per
|
||||
refined case (611319808 for all four).
|
||||
|
||||
The campaign is four fresh runs:
|
||||
`run_case.py --flow shear_wave --diagnostic twin_shear_n128 --re 100` with
|
||||
- `--n 128 --depth 1`
|
||||
- `--n 256 --depth 1` (the finest control is 2N)
|
||||
- `--n 128 --depth 2`
|
||||
- `--n 128 --depth 2 --explosion projected_linear`
|
||||
|
||||
There is no snapshot option. Older exact controls serve only as consistency
|
||||
checks; they are not reused as references.
|
||||
|
||||
Assessment uses the same preset contract as the nested preset, through
|
||||
`assess_shear_wave.py ... --diagnostic twin_shear_n128`:
|
||||
- the physics is the unchanged depth-2 / 2N `shear_wave.assess`;
|
||||
- receipts must pin `shear_twin.py`, `shear_nested.py` and `shear_wave.py`;
|
||||
- `shear_nested.py` stays byte-identical (fa6cfe87…), so the 335624c nested
|
||||
receipts still validate.
|
||||
|
||||
Labels:
|
||||
- *Twin vs nested* is the effect of adding this nested L2 refinement.
|
||||
Removing L2 also changes the finest spacing and refined volume, so it does
|
||||
not isolate abstract depth.
|
||||
- *Twin vs the single-box depth-2 runs* is a placement/geometry contrast at
|
||||
equal depth.
|
||||
- Neither contrast is additive by construction.
|
||||
- The thin-band/acoustic explanation for the nested L1 pressure is a
|
||||
hypothesis, not established.
|
||||
|
||||
### Signed interface-probe traces (opt-in `--probe-trace`, mechanism diagnostic)
|
||||
|
||||
`shear_probes.py` (pinned) uses native's validated 24-target layout through
|
||||
box 1, in root-128 physical coordinates:
|
||||
- a horizontal line y = 56, crossing the vertical faces x = 16 and 22;
|
||||
- a vertical line x = 38, crossing the horizontal faces y = 16 and 22;
|
||||
- normal coordinates 12.5, 14.5, 15.5, 16.25, 17.25, 19.25, 20.75, 21.75,
|
||||
22.125, 23.125, 24.125, 26.125.
|
||||
|
||||
Each target maps to the nearest ACTIVE leaf center. The deterministic tie
|
||||
rule is: distance, then finer level, then smaller j, then smaller i. This
|
||||
gives 24 unique cells in every case, and nested has 3 L0 + 5 L1 + 4 L2 per
|
||||
transect. Maximum displacement from target to center is 0.625 root cells in
|
||||
U128 (0.177 in U512).
|
||||
|
||||
Allowed cases:
|
||||
- nested U128, nested U512, nested projected depth 3;
|
||||
- twin projected depth 2.
|
||||
|
||||
The option can't be combined with `--final-snapshot`.
|
||||
|
||||
Sampling:
|
||||
- The receipt stores each probe's leaf level, `i`/`j`, actual cell center
|
||||
and displacement from its target, all in root-physical units (N=128).
|
||||
- Grids differ between cases, so centers are *not* collocated. Analytic
|
||||
fields are evaluated at each actual center (`shear_probes.residuals`), and
|
||||
comparisons across cases are between residuals at nearby but distinct
|
||||
centers.
|
||||
- Times are root-physical steps 0–128 and 1920–2048. U512 is sampled every 4
|
||||
lattice steps to match.
|
||||
- Each sample stores `rho`, `ux`, `uy` from the FP64 physical populations,
|
||||
POST-collision G at root synchronization only. No substep content is
|
||||
observed, and grid-scale alternation can alias.
|
||||
|
||||
What's kept and enforced:
|
||||
- The five qualification samples stay unchanged.
|
||||
- No field dumps. The trace is validated for exact schedule, finiteness and
|
||||
a 600 kB cap. If invalid it is dropped and an error is recorded, keeping
|
||||
the scalar receipt.
|
||||
- There are no new pass gates; this is a mechanism diagnostic only.
|
||||
|
||||
### Single x-gap-width contrast (`wide_x_shear_n128`)
|
||||
|
||||
This opt-in admits only Re100/N128/projected depth3 with `--probe-trace`.
|
||||
The outer boxes, three grid spacings, viscosities, initialization, five main
|
||||
sample times and trace windows are unchanged. L1→L2 parent-frame rectangles
|
||||
are `[(56,44,96,212),(160,60,200,228)]`: both x-normal middle gaps widen from
|
||||
6 to 12 root cells, while y gaps remain 6. Active leaves are
|
||||
`[7936,20352,53760]`, hops `[2048,4096,8192]`, work 540,016,640 active updates.
|
||||
This also moves the L2 x faces and reduces its volume; it is not a pure
|
||||
travel-distance experiment. No new qualification matrix, assessor, threshold
|
||||
or snapshot is added. Existing scalar screens and all failures stay visible.
|
||||
|
||||
The same 24 targets and actual-center metadata are retained. The horizontal
|
||||
probes stop before the new inner face x = 28: they do not bracket that interface
|
||||
or sample its fine side. The first 8 horizontal L0/L1 probes keep identical
|
||||
centers, and the y-normal x = 38 transect is unchanged. Their temporal residuals
|
||||
can support a descriptive timing contrast, but cannot establish a crossing
|
||||
or reflection coefficient. Changed-level probes must not be treated as
|
||||
collocated samples. `rho-1` remains raw density deviation; do not substitute
|
||||
the 24-probe mean for the global pressure gauge.
|
||||
|
||||
### POST-G initialization counterfactual (`postg_shear_n128`)
|
||||
|
||||
Exactly three fresh Re100 cases are admitted: U128, U512, and projected R128
|
||||
depth 3 with the original nested rectangles. Each requires `--flow shear_wave
|
||||
--diagnostic postg_shear_n128 --probe-trace`; snapshots and early-pressure mode
|
||||
are excluded. Geometry, physical velocity/density, viscosities, collisions,
|
||||
five main times and the 24 signed probes/windows are unchanged. Total work is
|
||||
2,952,265,728 active updates, with 15 main rows, 774 trace rows and five builds.
|
||||
|
||||
Only initialization changes. At level spacing h, k_level = h k and
|
||||
A_i = 3 w_i U cos(theta) (c_i dot e) (c_i dot k_level). Initialize physical
|
||||
FP64 **POST G = feq - (tau - 1) A**, then use the existing shifted FP32 codec.
|
||||
This is the first-gradient, low-Mach Chapman-Enskog approximation, not an
|
||||
exact discrete eigenstate. The PRE coefficient is -tau; the physical viscous
|
||||
stress coefficient tau - 1/2 must not replace the raw POST-G coefficient.
|
||||
The correction has zero mass/momentum before encoding; FP32 introduces normal
|
||||
roundoff. U512 uses its native k and tau, matching nested L2 physical spacing.
|
||||
|
||||
Receipts pin the new initializer module and identify `postg_first_gradient_v1`,
|
||||
with `qualification_eligible=false`. Existing scalar screens remain visible
|
||||
as descriptive outcomes, including failures; no new pass gate or assessor is
|
||||
introduced. Historical assessors reject the new diagnostic and initialization
|
||||
identities. Compare raw rows and signed traces with the retained equilibrium
|
||||
receipts as a startup-state counterfactual. The host first-interval pressure
|
||||
RMS reduction is not a prediction of long-time improvement, and individual
|
||||
probe errors can increase. All equilibrium paths and historical verdicts stay
|
||||
unchanged.
|
||||
|
||||
### Short square orientation diagnostic (`axis_square_shear_n128`)
|
||||
|
||||
Four fresh cases only: U128, U256, constant R128 and projected R128 depth 2.
|
||||
Use `--flow shear_wave --diagnostic axis_square_shear_n128 --probe-trace`.
|
||||
The supported union is `[[(32,32,96,96)]]`; a full-height periodic fine strip
|
||||
is not supported by the unchanged production geometry. All four cases use
|
||||
POST G = feq - (tau - 1) A, mode (2,0), polarization (0,1), phase pi/4,
|
||||
U=.03 and Re100. Root/fine nu are .0384/.0768 and tau are .6152/.7304.
|
||||
This first-gradient low-Mach initialization is not an exact discrete mode.
|
||||
|
||||
Stop at root step 8: two global rows at 0/8, nine local rows at 0..8;
|
||||
U256 advances two native steps per root step. Refined leaves are [12288,16384],
|
||||
hops [8,16], work 360,448 per refined case. Four-case work is 1,900,544 ACTIVE
|
||||
updates, six build sidecars and 720 native cell-time samples. Existing runtime,
|
||||
build/source guards and receipt/trace caps apply. No snapshots or late fit.
|
||||
`screening_pass=null`, `qualification_eligible=false`: exit zero means valid
|
||||
finite completion, with no physical pass threshold or legacy assessor.
|
||||
|
||||
Eight root-unit-cell groups (i,j): inner L/R/B/T = (32,64)/(95,63)/(64,32)/(63,95),
|
||||
outer L/R/B/T = (31,64)/(96,63)/(64,31)/(63,96). Refined inner groups contain four
|
||||
fine cells, outer groups one coarse cell; U128 uses one and U256 four per group.
|
||||
Retain raw rho/ux/uy, native indices, physical centers/areas and parity. Group
|
||||
residuals are signed means of rho-1 and rho*u-u_exact, evaluated at each actual
|
||||
center; normalization uses that group's mean analytic d(uy)/dx. Only inner
|
||||
L/B and R/T pairs have exactly opposite equal-magnitude gradients. Controls
|
||||
have their own native quadrature. Probe density is ungauged; global pressure
|
||||
RMS uses one domain gauge.
|
||||
|
||||
Actual-table local corridors match tangentially extended straight faces beyond
|
||||
22.5 root cells. The conservative paired-map radius is 2.5 root cells per root
|
||||
step, so these selected groups through step 8 are locally straight-face
|
||||
equivalent. This is not a claim about global corner errors. For projected
|
||||
explosion with actual missing-normal-gradient fallback, the stationary
|
||||
linearized affine reference after the first complete root interval predicts
|
||||
zero conserved-moment error on vertical faces. At the
|
||||
bottom/top inner fine quartets its mean delta jx / d(uy)/dx is
|
||||
-/+ 1/(576*tau_f), with opposite adjacent-coarse redistribution.
|
||||
Keep raw values, step-zero rounding and fresh controls: sinusoidal curvature
|
||||
and finite-Mach effects prevent treating this coefficient as an exact gate.
|
||||
|
||||
### Test-only centered straight-face comparison
|
||||
|
||||
`centered_face_square_n128` reuses the square axis-wave POST-G initializer,
|
||||
geometry, native probes and root steps 0–8. It has four fresh cases: U128,
|
||||
U256, current projected R128, and experimental centered R128. This is a
|
||||
qualification-ineligible diagnostic; there is no legacy assessor or new physics gate.
|
||||
|
||||
Only the candidate's private generated fine-level `blocks.cu` changes. A
|
||||
2,112-byte constant mask selects 1,792 rows (448 complete donor quartets).
|
||||
The diagonal projected correction is doubled on straight faces with exactly
|
||||
one unsupported normal axis and both legal tangent neighbors. Donor centers
|
||||
within four root cells of a corner retain the current rule, as do all other
|
||||
patterns, axial populations and rest. The mask is symmetric under transpose
|
||||
and reflection. Its hash and the actual modified source/PTX hashes are retained;
|
||||
production files, restrictions, collision, scheduling and launch arguments stay fixed.
|
||||
A complete quartet has zero summed offset, so each population's quartet mean
|
||||
is preserved. The perpendicular gradient also retains equal characteristic
|
||||
packets; the existing two-hop conservation routing therefore remains applicable.
|
||||
Host tests additionally check the mixed mask through complete two-hop/collision
|
||||
cycles, including corner grazing, and execute the actual edited kernel via C++.
|
||||
The central probes are more than 20 root cells from mask transitions; the
|
||||
reviewed conservative dependency radius is 2.5 root cells per root interval.
|
||||
|
||||
The separate comparison subtracts each group's step-zero residual and the
|
||||
fresh uniform control at matching quadrature (U256 inner, U128 outer). It retains
|
||||
signed density and both momentum components, actual-center gradient normalization,
|
||||
parities, and sign flips. Step 1 requires bottom-inner cross-momentum magnitude
|
||||
improvement greater than its deadband, and top-inner nonworsening within it.
|
||||
Each horizontal face's step-8 RMS combines its inner and outer group means;
|
||||
both must be nonworse. All four vertical groups must remain unchanged within
|
||||
the component deadbands at every sampled root step. The norm
|
||||
`sqrt(drho²/3 + djx² + djy²)` must not worsen beyond its corresponding deadband.
|
||||
Sampled raw-pressure envelope and step-8 global gauged pressure RMS must not
|
||||
worsen beyond the pressure deadband. Nonfinite/nonpositive sampled density,
|
||||
invalid traces, source/build mismatch, or exceeded observed population envelope
|
||||
reject the comparison. A small top signal or sign reversal remains explicit.
|
||||
|
||||
The predeclared shifted population envelope is 1/32. From per-population oracle
|
||||
allowance `6e-8 + 2e-7*1/32`, pairwise moment deadbands are
|
||||
`[1.1925e-6, 7.95e-7, 7.95e-7]`; pressure uses the density deadband divided by 3.
|
||||
Area averaging cannot enlarge this local allowance. These are practical diagnostic
|
||||
deadbands, not certified accumulated root-8 rounding bounds. The envelope is
|
||||
checked on downloaded synchronized ACTIVE populations, not hidden substeps.
|
||||
Known quadratic fine-cell O(h) terms, corner transitions, repeated coupling and
|
||||
stability remain unresolved. Passing permits review of a candidate, not promotion.
|
||||
|
||||
Run the existing `run_case.py` with `--diagnostic centered_face_square_n128
|
||||
--flow shear_wave --re 100 --probe-trace --test-reconstruction current` for
|
||||
U128/U256 (depth 1, constant) and current R128 (depth 2, projected_linear).
|
||||
For candidate R128 use `--test-reconstruction centered_face_v1`. All require
|
||||
an exact clean `--repo`, `--expected-revision`, fresh `--output`, and either
|
||||
`--dry-run` or explicit single-device `--run-gpu`. Compare ordered receipts with
|
||||
`shear_centered.py --repo REPO --expected-revision SHA --output comparison.json
|
||||
U128.json U256.json current.json candidate.json`. Total work is 1,900,544 ACTIVE
|
||||
updates, six build sidecars, and 720 cell-time samples; scalar cap stays 1 MiB/case.
|
||||
|
||||
@@ -63,6 +63,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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,7 +22,7 @@ from .block_stepper import MAX_GRID_DIM
|
||||
from .hierarchy import build_hierarchy, is_rectangle
|
||||
from .refinement import ACTIVE, HALO, FixedPatch2D, FixedPatchSet2D, prepare_fine_boundary
|
||||
from .refinement_tables import (blocked_base_tables, blocked_fine_tables, # noqa: F401 (re-export)
|
||||
blocked_kind, _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
|
||||
@@ -41,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
|
||||
@@ -206,6 +208,12 @@ class FixedPatchStepper:
|
||||
halo -> BFL -> step four times per base step. Runtime child Jet changes
|
||||
require ``set_jet_amplitude(index, amplitude, level=2)``.
|
||||
|
||||
``explosion="projected_linear"`` selects experimental transverse linear
|
||||
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.
|
||||
|
||||
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,
|
||||
@@ -219,7 +227,7 @@ class FixedPatchStepper:
|
||||
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, observe_window_exchange=False):
|
||||
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", "channel"):
|
||||
@@ -248,6 +256,15 @@ class FixedPatchStepper:
|
||||
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")
|
||||
@@ -479,6 +496,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:
|
||||
@@ -653,7 +675,7 @@ class FixedPatchStepper:
|
||||
child_rectangles_in_parent=new.child.bounds,
|
||||
child_objects=child_objects, child_jets=jets,
|
||||
observe_raw_exchange=self._observe_raw_exchange,
|
||||
observe_window_exchange=self._observe_window)
|
||||
observe_window_exchange=self._observe_window, explosion=self._explosion)
|
||||
try:
|
||||
if self._ddf_shifting:
|
||||
candidate._initialize_raw(_StoredPopulations(transferred.g, transferred.shifted))
|
||||
@@ -746,6 +768,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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -41,45 +41,74 @@ class GuardTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(RuntimeError, "velocity/profile"):
|
||||
decode_macros(invalid, w, cx, cy)
|
||||
|
||||
def test_baseline_pins(self):
|
||||
def test_current_corrected_pins(self):
|
||||
self.assertEqual(
|
||||
source_guard(ROOT, "baseline")["base_commit"],
|
||||
"cd3a657f9d3833e812c3cca173525f165b48710a",
|
||||
source_guard(ROOT, "corrected", "a135252")["base_commit"],
|
||||
"a135252ae78b47b57d7e77d19001700b3bedac8b",
|
||||
)
|
||||
|
||||
def test_corrected_and_tamper_guard(self):
|
||||
pins = json.loads((HERE / "data/les_flow/source_pins.json").read_text())
|
||||
pins = json.loads((HERE / "data/les_flow/source_pins_a135252.json").read_text())
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for name in pins["sources"]:
|
||||
dest = root / name
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / name, dest)
|
||||
# Rebuild the reviewed candidate from actual pinned headers, not fixtures.
|
||||
candidate = root / "header_candidate"
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(HERE / "les/prepare.py"),
|
||||
"--repo",
|
||||
str(ROOT),
|
||||
"--output-dir",
|
||||
str(candidate),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
shutil.copyfile(candidate / "patched" / HEADER, root / HEADER)
|
||||
with patch(
|
||||
"run_uniform_les_flow.subprocess.check_output",
|
||||
return_value="isolated-fixture\n",
|
||||
):
|
||||
source_guard(root, "corrected")
|
||||
source_guard(root, "corrected", "a135252")
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "baseline")
|
||||
target = root / "src/CelerisLab/lbm/kernels/step/one_step_double.cu"
|
||||
target.write_bytes(target.read_bytes() + b"\n// tamper\n")
|
||||
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, "corrected")
|
||||
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())
|
||||
tracked = subprocess.check_output(["git", "-C", str(ROOT), "ls-files", "src"], text=True)
|
||||
self.assertEqual(set(pins["sources"]), set(tracked.splitlines()))
|
||||
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:
|
||||
root = Path(directory)
|
||||
for name in pins["sources"]:
|
||||
dest = root / name
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / 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:
|
||||
|
||||
@@ -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()
|
||||
+126
-10
@@ -15,7 +15,11 @@ 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")
|
||||
assert r.verify_source(r.DEFAULT_SOURCE, "MRT")["commit"].startswith("3ea131e")
|
||||
mrt_history = json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text())
|
||||
assert mrt_history["commit"].startswith("3ea131e")
|
||||
assert r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")["commit"].startswith("a233c90")
|
||||
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
|
||||
r.verify_source(r.DEFAULT_SOURCE, "MRT")
|
||||
with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"):
|
||||
r.verify_source(r.DEFAULT_SOURCE, "SRT")
|
||||
|
||||
@@ -139,17 +143,20 @@ def test_axis_specific_sampling_phase():
|
||||
|
||||
def test_full_requires_completed_pilot():
|
||||
with unittest.TestCase().assertRaisesRegex(ValueError, "pilot-results"):
|
||||
r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE, "MRT"))
|
||||
r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE, "MRT", "window"))
|
||||
|
||||
|
||||
def pilot_fixture(path):
|
||||
def pilot_fixture(path, observation_mode="window"):
|
||||
"""Synthetic contract fixture only; never a physical result or run receipt."""
|
||||
manifest = r.verify_source(r.DEFAULT_SOURCE, "MRT")
|
||||
manifest = (r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")
|
||||
if observation_mode == "window" else
|
||||
json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text()))
|
||||
(path / "plan.json").write_text(
|
||||
json.dumps(
|
||||
dict(
|
||||
source_manifest=manifest,
|
||||
collision="MRT",
|
||||
observation_mode=observation_mode,
|
||||
bounds_root_edge=list(r.BOUNDS),
|
||||
runner_sha256=r.runner_hash(),
|
||||
)
|
||||
@@ -183,6 +190,7 @@ def pilot_fixture(path):
|
||||
dict(
|
||||
case=case,
|
||||
collision="MRT",
|
||||
observation_mode=observation_mode,
|
||||
mode="pilot",
|
||||
status="pilot_complete",
|
||||
healthy=True,
|
||||
@@ -199,6 +207,7 @@ def pilot_fixture(path):
|
||||
dict(
|
||||
case=case,
|
||||
collision="MRT",
|
||||
observation_mode=observation_mode,
|
||||
mode="pilot",
|
||||
step=interval,
|
||||
runner_sha256=r.runner_hash(),
|
||||
@@ -215,7 +224,7 @@ def test_pilot_validation_and_calibration():
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
manifest = pilot_fixture(path)
|
||||
assert r.verify_pilot(path, manifest, "MRT") == 3606.0
|
||||
assert r.verify_pilot(path, manifest, "MRT", "window") == 3606.0
|
||||
|
||||
|
||||
def test_reject_malformed_pilot_receipts():
|
||||
@@ -245,7 +254,7 @@ def test_reject_malformed_pilot_receipts():
|
||||
mutate(bad)
|
||||
receipt.write_text(json.dumps(bad))
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT")
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
# Exact reviewer regression: previously accepted NaN endpoint.
|
||||
receipt.write_text(
|
||||
json.dumps(
|
||||
@@ -257,11 +266,11 @@ def test_reject_malformed_pilot_receipts():
|
||||
)
|
||||
)
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT")
|
||||
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")
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
|
||||
|
||||
def test_strict_finite_json_and_deadline():
|
||||
@@ -323,7 +332,7 @@ def test_reject_collision_receipts():
|
||||
record["collision"] = invalid
|
||||
receipt.write_text(json.dumps(record))
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT")
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
|
||||
|
||||
def test_record_actual_checkout_separately():
|
||||
@@ -332,7 +341,114 @@ def test_record_actual_checkout_separately():
|
||||
actual = checkout_provenance(r.DEFAULT_SOURCE)
|
||||
assert len(actual["checkout_revision"]) == 40
|
||||
assert isinstance(actual["checkout_dirty"], bool)
|
||||
assert r.verify_source(r.DEFAULT_SOURCE, "MRT")["production_origin_commit"].startswith("3ea131e")
|
||||
assert r.verify_source(r.DEFAULT_SOURCE, "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 r.verify_source(r.DEFAULT_SOURCE, 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
|
||||
|
||||
|
||||
if __name__ == "__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()
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"base_commit": "8403956a9afc0b0bb89265cec374392013d89016",
|
||||
"production_origin_commit": "8403956a9afc0b0bb89265cec374392013d89016",
|
||||
"source_experiment": "Explicit dev8403956 uniform SRT LES experiment; historical 9805383/5465832 receipts remain historical. Corrected permits only the reviewed normalization header.",
|
||||
"sources": {
|
||||
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
|
||||
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
|
||||
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
|
||||
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
|
||||
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
|
||||
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
|
||||
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
|
||||
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
|
||||
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
|
||||
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
|
||||
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
|
||||
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
|
||||
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
|
||||
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
|
||||
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
|
||||
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
|
||||
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
|
||||
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
|
||||
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
|
||||
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
|
||||
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
|
||||
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
|
||||
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
|
||||
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
|
||||
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
|
||||
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
|
||||
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
|
||||
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
|
||||
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
|
||||
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
|
||||
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
|
||||
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
|
||||
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
|
||||
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
|
||||
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
|
||||
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
|
||||
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
|
||||
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
|
||||
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
|
||||
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
|
||||
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
|
||||
"src/CelerisLab/lbm/channel.py": "40d0354e5dc97bcdb7e7b5c1e7f9303a109b7134d8a3af88b8ff4cd75ab5a095",
|
||||
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
|
||||
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
|
||||
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
|
||||
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
|
||||
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
|
||||
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
|
||||
"src/CelerisLab/lbm/kernels/blocks.cu": "2fa894b20a29f006911ac3e3aad0793cc4aaf9ccd7a67d01c6320a0836b1d8ef",
|
||||
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
|
||||
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
|
||||
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
|
||||
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
|
||||
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
|
||||
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
|
||||
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
|
||||
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
|
||||
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
|
||||
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
|
||||
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
|
||||
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
|
||||
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
|
||||
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
|
||||
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
|
||||
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
|
||||
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
|
||||
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
|
||||
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
|
||||
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
|
||||
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
|
||||
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
|
||||
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
|
||||
"src/CelerisLab/lbm/kernels/refinement.cu": "b45aad9c022cecb64f7e968ec3cae2d8385a3cc430ee409700c404a3f4dd8cf8",
|
||||
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
|
||||
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "ee8a31398b8e5233d977c816e6b8e060c1eb2ec71320e594226dbca55593c0d0",
|
||||
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
|
||||
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
|
||||
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
|
||||
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
|
||||
"src/CelerisLab/lbm/raw_exchange.py": "9031a196006a0f74dee4028e6f3bb673bc19ef7959f9f04dde9e01f93293aeb3",
|
||||
"src/CelerisLab/lbm/refinement.py": "100d5e46ac894f6008b61364cdad5d37c2ca7ed949d8d71405572d6366526417",
|
||||
"src/CelerisLab/lbm/refinement_stepper.py": "b6b835cf97f5aa045045bb4a47cfde34b89929d3872be410cb155727dfca2d33",
|
||||
"src/CelerisLab/lbm/refinement_tables.py": "6eea593aaf791a2c644f1c74df0a41f6f5e62a2fce9feae76e19cf84b615607a",
|
||||
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
|
||||
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
|
||||
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c"
|
||||
},
|
||||
"candidate_header_sha256": "a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa"
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"commit": "a233c9032487a6006e950d1e327079b00466c410",
|
||||
"production_origin_commit": "d9303234795fccf8591ba6274dfc544a84d956ff",
|
||||
"observation_mode": "window",
|
||||
"supported_collisions": [
|
||||
"SRT",
|
||||
"MRT"
|
||||
],
|
||||
"sha256": {
|
||||
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
|
||||
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
|
||||
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
|
||||
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
|
||||
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
|
||||
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
|
||||
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
|
||||
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
|
||||
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
|
||||
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
|
||||
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
|
||||
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
|
||||
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
|
||||
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
|
||||
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
|
||||
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
|
||||
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
|
||||
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
|
||||
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
|
||||
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
|
||||
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
|
||||
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
|
||||
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
|
||||
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
|
||||
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
|
||||
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
|
||||
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
|
||||
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
|
||||
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
|
||||
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
|
||||
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
|
||||
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
|
||||
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
|
||||
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
|
||||
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
|
||||
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
|
||||
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
|
||||
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
|
||||
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
|
||||
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
|
||||
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
|
||||
"src/CelerisLab/lbm/channel.py": "40d0354e5dc97bcdb7e7b5c1e7f9303a109b7134d8a3af88b8ff4cd75ab5a095",
|
||||
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
|
||||
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
|
||||
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
|
||||
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
|
||||
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
|
||||
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
|
||||
"src/CelerisLab/lbm/kernels/blocks.cu": "2fa894b20a29f006911ac3e3aad0793cc4aaf9ccd7a67d01c6320a0836b1d8ef",
|
||||
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
|
||||
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
|
||||
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
|
||||
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
|
||||
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
|
||||
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
|
||||
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
|
||||
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
|
||||
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
|
||||
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
|
||||
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
|
||||
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
|
||||
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
|
||||
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
|
||||
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
|
||||
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
|
||||
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
|
||||
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
|
||||
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
|
||||
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
|
||||
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
|
||||
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
|
||||
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
|
||||
"src/CelerisLab/lbm/kernels/refinement.cu": "b45aad9c022cecb64f7e968ec3cae2d8385a3cc430ee409700c404a3f4dd8cf8",
|
||||
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
|
||||
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "ee8a31398b8e5233d977c816e6b8e060c1eb2ec71320e594226dbca55593c0d0",
|
||||
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
|
||||
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
|
||||
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
|
||||
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
|
||||
"src/CelerisLab/lbm/raw_exchange.py": "9031a196006a0f74dee4028e6f3bb673bc19ef7959f9f04dde9e01f93293aeb3",
|
||||
"src/CelerisLab/lbm/refinement.py": "100d5e46ac894f6008b61364cdad5d37c2ca7ed949d8d71405572d6366526417",
|
||||
"src/CelerisLab/lbm/refinement_stepper.py": "b6b835cf97f5aa045045bb4a47cfde34b89929d3872be410cb155727dfca2d33",
|
||||
"src/CelerisLab/lbm/refinement_tables.py": "6eea593aaf791a2c644f1c74df0a41f6f5e62a2fce9feae76e19cf84b615607a",
|
||||
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
|
||||
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
|
||||
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c",
|
||||
"tests/validation/data/dfg_re100/reference_cycle.json": "073d1bda1629697efebad95667bfe80598e217d708dbffd7d5f6c6999379c55f",
|
||||
"tests/validation/periodic.py": "a2212c0e2705126327a5019e9cafe64eeca398da1b75061ff23630c9843697a6",
|
||||
"tests/validation/re100_helpers.py": "289c6e01075ff361573f862b90cfd61e5980c9a8efcbc78aef69979b4004fd60",
|
||||
"tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
"""Bounded exact-source CUDA plumbing gate; never a flow-accuracy certificate.
|
||||
|
||||
Run this driver externally against exact dev with --mode baseline first, then
|
||||
against the experimental source with --mode candidate --baseline <receipt>.
|
||||
Only scalar receipts/hashes and actual compiler sidecars are retained. Whole-step
|
||||
mass/momentum are diagnostics: no verified FP32 conservation bound is claimed.
|
||||
"""
|
||||
import argparse
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import replace
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
BASE = 'ed9d8c937d446b854d6963bd92d718bb4fb98b96'
|
||||
PRODUCTION = 'a135252ae78b47b57d7e77d19001700b3bedac8b'
|
||||
STOPS = (0, 1, 2, 8)
|
||||
OPERATOR_ATOL, OPERATOR_RTOL = 6e-8, 2e-7
|
||||
REST_ATOL, REST_RTOL = 3e-7, 1e-6
|
||||
REPLAY_ATOL = 3e-6
|
||||
E = np.array([(0,0),(1,0),(-1,0),(0,1),(0,-1),(1,1),(-1,-1),(1,-1),(-1,1)])
|
||||
W = np.array([4/9]+[1/9]*4+[1/36]*4)
|
||||
|
||||
|
||||
def digest(array):
|
||||
"""All finite bits (including signed zero) and exact NaN mask; no NaN payloads."""
|
||||
array = np.asarray(array)
|
||||
if np.isinf(array).any():
|
||||
raise AssertionError('infinite population')
|
||||
finite = np.isfinite(array)
|
||||
h = hashlib.sha256()
|
||||
h.update(str((array.shape, array.dtype.str)).encode())
|
||||
h.update(np.ascontiguousarray(finite).tobytes())
|
||||
h.update(np.ascontiguousarray(array[finite]).tobytes())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def independent_explosion(coarse, pair, layout, kind):
|
||||
"""Coordinate oracle; never reads the production neighbor/offset tables."""
|
||||
ny, nx = pair.coarse_shape
|
||||
legal = np.zeros((ny,nx), bool)
|
||||
legal.ravel()[pair.ghost_coarse] = True
|
||||
legal &= layout.owned & ~pair.covered & (kind == 0)
|
||||
if not legal.ravel()[pair.ghost_coarse].all():
|
||||
raise AssertionError('illegal center donor')
|
||||
out = np.empty((9,len(pair.ghost_fine)))
|
||||
flat = np.asarray(coarse, np.float64).reshape(9,-1)
|
||||
for row, (target, donor) in enumerate(zip(pair.ghost_fine,pair.ghost_coarse)):
|
||||
y,x = divmod(int(donor),nx)
|
||||
gradient = np.zeros((9,2))
|
||||
for axis, (dx,dy) in enumerate(((1,0),(0,1))):
|
||||
ends = [(x-dx,y-dy),(x+dx,y+dy)]
|
||||
if not layout.periodic[axis] and any(not (0<=xx<nx and 0<=yy<ny) for xx,yy in ends):
|
||||
continue
|
||||
ends = [(xx%nx,yy%ny) for xx,yy in ends]
|
||||
if all(legal[yy,xx] for xx,yy in ends):
|
||||
(xm,ym),(xp,yp) = ends
|
||||
gradient[:,axis] = (flat[:,yp*nx+xp]-flat[:,ym*nx+xm])/2
|
||||
for q in range(1,9):
|
||||
gradient[q] -= E[q]*np.dot(E[q],gradient[q])/np.dot(E[q],E[q])
|
||||
fy,fx = divmod(int(target),pair.fine_shape[1])
|
||||
offset = np.array([pair.fine_origin[0]+(fx+.5)/2-(x+.5),
|
||||
pair.fine_origin[1]+(fy+.5)/2-(y+.5)])
|
||||
out[:,row] = flat[:,int(donor)] + gradient@offset
|
||||
return out
|
||||
|
||||
|
||||
def default_options(selector):
|
||||
# Exact dev has no explosion keyword. Baseline and omitted paths pass none.
|
||||
return {} if selector == 'omitted' else {'explosion':selector}
|
||||
|
||||
|
||||
def snapshot(run, raw):
|
||||
from CelerisLab.lbm.refinement import ACTIVE, OUTER_GHOST
|
||||
stored, public = raw(run), run.download()
|
||||
hashes, totals = [], np.zeros(3)
|
||||
for level,(values,visible,kind) in enumerate(zip(stored,public,run.hierarchy.kinds)):
|
||||
active = kind == ACTIVE
|
||||
if not np.isfinite(visible[:,active]).all() or not np.isnan(visible[:,~active]).all():
|
||||
raise AssertionError('public ACTIVE/invalid mask mismatch')
|
||||
if not np.isnan(values[:,kind==OUTER_GHOST]).all():
|
||||
raise AssertionError('OUTER owner core is not poisoned')
|
||||
population = visible[:,active].sum(axis=1,dtype=np.float64)*(.25**level)
|
||||
totals += np.r_[population.sum(), E.T@population]
|
||||
hashes.append(dict(raw=digest(values), public=digest(visible), active=int(active.sum())))
|
||||
if not np.isfinite(totals).all():
|
||||
raise AssertionError('nonfinite mass/momentum diagnostic')
|
||||
return dict(step=run.step_count, levels=hashes, mass_momentum=totals.tolist())
|
||||
|
||||
|
||||
def initialize(run, rest=False):
|
||||
arrays, origin, spacing = [], np.zeros(2), 1.
|
||||
for index,level in enumerate(run._levels):
|
||||
if index:
|
||||
origin += spacing*np.asarray(run.hierarchy.pairs[index-1].fine_origin)
|
||||
spacing /= 2
|
||||
y,x = np.indices(level.shape[1:],dtype=np.float64)
|
||||
x,y = origin[0]+spacing*(x+.5),origin[1]+spacing*(y+.5)
|
||||
q = np.arange(9)[:,None,None]
|
||||
values = np.broadcast_to(W[:,None,None],level.shape).copy()
|
||||
if not rest:
|
||||
# q-distinct smooth non-equilibrium G; same physical frame on all levels.
|
||||
values += 1e-4*np.sin(.17*x+.23*y+q)
|
||||
arrays.append(values)
|
||||
run.initialize(*arrays)
|
||||
|
||||
|
||||
def operator_case(run, index):
|
||||
import pycuda.driver as cuda
|
||||
parent,child = run._levels[index:index+2]
|
||||
geometry,pair = run.hierarchy.pairs[index],run._pairs[index]
|
||||
c = np.random.default_rng(19+index).normal(0,.001,(9,*geometry.coarse_shape))
|
||||
if not run._ddf_shifting:
|
||||
c += W[:,None,None]
|
||||
c = c.astype(np.float32)
|
||||
legal = np.zeros(geometry.coarse_shape,bool)
|
||||
legal.ravel()[geometry.ghost_coarse] = True
|
||||
c[:,~legal] = np.nan
|
||||
packed = parent.layout.to_blocked(c)
|
||||
output = np.full(child.layout.blocked_shape,np.nan,np.float32)
|
||||
cuda.memcpy_htod(parent.current,packed)
|
||||
cuda.memcpy_htod(child.current,output)
|
||||
calls, hop = [],run._hop
|
||||
try:
|
||||
run._hop = lambda *a,**kw: calls.append(a)
|
||||
run._advance(index+1,parent.current,block=run._block,stream=run._stream)
|
||||
run._stream.synchronize()
|
||||
finally:
|
||||
run._hop = hop
|
||||
if calls != [(index+1,child.current,child.spare),(index+1,child.spare,child.current)]:
|
||||
raise AssertionError('explosion changed two-hop launch contract')
|
||||
cuda.memcpy_dtoh(output,child.current)
|
||||
target = child.layout.owner.ravel()[geometry.ghost_fine]
|
||||
slots = np.array([child.layout.slot(target,q) for q in range(9)])
|
||||
actual = output.ravel()[slots]
|
||||
expected = independent_explosion(c,geometry,parent.layout,run.hierarchy.kinds[index])
|
||||
constant = c.reshape(9,-1)[:,geometry.ghost_coarse]
|
||||
if not np.isfinite(actual).all() or np.max(abs(expected-constant)) <= 1e-5:
|
||||
raise AssertionError('operator must exercise finite nonzero projected slopes')
|
||||
np.testing.assert_allclose(actual,expected,atol=OPERATOR_ATOL,rtol=OPERATOR_RTOL)
|
||||
untouched = np.ones(output.size,bool); untouched[slots.ravel()] = False
|
||||
if not np.isnan(output.ravel()[untouched]).all():
|
||||
raise AssertionError('explosion wrote outside its targets')
|
||||
return dict(pair=index,rows=len(target),parent_stride=pair.parent_cells,
|
||||
child_stride=int(np.prod(child.layout.local_shape)),
|
||||
max_error_by_q=np.max(abs(actual-expected),axis=1).tolist(),
|
||||
nonzero_projection=True,poisoned_nondonors=True,untouched_preserved=True)
|
||||
|
||||
|
||||
def replay_two_hops(values, pair, layout, kind):
|
||||
"""Existing FP64 two-hop algebra, using the independent coordinate oracle."""
|
||||
from tests.unit.test_projected_explosion import collide
|
||||
coarse,fine = (a.copy() for a in values)
|
||||
fine.reshape(9,-1)[:,pair.ghost_fine] = independent_explosion(coarse,pair,layout,kind)
|
||||
for _ in range(2):
|
||||
fine = np.array([np.roll(fine[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
|
||||
fine[:,pair.fine_kind==2] = np.nan
|
||||
active = pair.fine_kind==0
|
||||
fine[:,active] = collide(fine[:,active],pair.tau_fine)
|
||||
out = np.array([np.roll(coarse[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
|
||||
out.reshape(-1)[pair.coalesce_coarse] = fine.reshape(-1)[pair.coalesce_fine].mean(axis=1)
|
||||
out[:,~pair.covered] = collide(out[:,~pair.covered],pair.tau_coarse)
|
||||
out[:,pair.covered] = np.nan
|
||||
return out,fine
|
||||
|
||||
|
||||
def run_case(ctx, case, shifted, selector, raw, projected=False, replay=False):
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
cfg,refs,blocks = case
|
||||
cfg = replace(cfg,velocity=0,ddf_shifting=shifted)
|
||||
with FixedPatchStepper(cfg,context=ctx,refinements=refs,level_blocks=blocks,
|
||||
**default_options(selector)) as run:
|
||||
operator = [operator_case(run,i) for i in range(len(run._pairs))] if projected else []
|
||||
counts = [0]*len(run._levels)
|
||||
for index,level in enumerate(run._levels):
|
||||
def counted(*a, index=index, kernel=level.kernel, **kw):
|
||||
counts[index] += 1
|
||||
return kernel(*a,**kw)
|
||||
level.kernel = counted
|
||||
def check_counts(stop):
|
||||
if counts != [stop*2**index for index in range(len(counts))]:
|
||||
raise AssertionError('actual level hop counts differ from acoustic schedule')
|
||||
rest = []
|
||||
if projected:
|
||||
initialize(run,rest=True)
|
||||
for stop in STOPS:
|
||||
run.step(stop-run.step_count)
|
||||
check_counts(stop)
|
||||
snapshot(run,raw) # actual public and raw invalid masks at every stop
|
||||
maximum = 0.
|
||||
for values,kind in zip(run.download(),run.hierarchy.kinds):
|
||||
active = values[:,kind==0]
|
||||
np.testing.assert_allclose(active,np.broadcast_to(W[:,None],active.shape),
|
||||
atol=REST_ATOL,rtol=REST_RTOL)
|
||||
maximum = max(maximum,float(np.max(abs(active-W[:,None]))))
|
||||
rest.append(dict(step=stop,max_absolute_error=maximum))
|
||||
counts[:] = [0]*len(counts)
|
||||
initialize(run)
|
||||
reference = tuple(a.astype(np.float64) for a in run.download()) if replay else None
|
||||
snapshots,replay_errors,previous = [],[],0
|
||||
for stop in STOPS:
|
||||
run.step(stop-run.step_count)
|
||||
if replay:
|
||||
for _ in range(stop-previous):
|
||||
reference = replay_two_hops(reference,run.hierarchy.pairs[0],
|
||||
run._levels[0].layout,run.hierarchy.kinds[0])
|
||||
errors = []
|
||||
for actual,expected,kind in zip(run.download(),reference,run.hierarchy.kinds):
|
||||
np.testing.assert_allclose(actual[:,kind==0],expected[:,kind==0],
|
||||
atol=REPLAY_ATOL,rtol=0)
|
||||
errors.append(np.max(abs(actual[:,kind==0]-expected[:,kind==0]),axis=1).tolist())
|
||||
replay_errors.append(dict(step=stop,max_error_by_level_q=errors))
|
||||
check_counts(stop)
|
||||
snapshots.append(dict(snapshot(run,raw),observed_level_hops=counts.copy())); previous = stop
|
||||
change = (np.array(snapshots[-1]['mass_momentum'])-
|
||||
np.array(snapshots[0]['mass_momentum'])).tolist()
|
||||
return dict(operator=operator,snapshots=snapshots,rest_fixed_point=rest,
|
||||
replay_max_errors=replay_errors,mass_momentum_change_diagnostic=change,
|
||||
conservation_status='diagnostic_only_no_FP32_bound')
|
||||
|
||||
|
||||
def compare_snapshots(actual,expected):
|
||||
if len(actual) != len(expected):
|
||||
raise AssertionError('snapshot count differs')
|
||||
for got,want in zip(actual,expected):
|
||||
if (got['step'] != want['step'] or got['levels'] != want['levels']
|
||||
or got.get('observed_level_hops') != want.get('observed_level_hops')):
|
||||
raise AssertionError('owner bits, public values or masks differ')
|
||||
|
||||
|
||||
def cases():
|
||||
from CelerisLab.config import LBMConfig
|
||||
from tests.unit.test_hierarchy import CFG, FOUR
|
||||
scalar = (LBMConfig(nx=16,ny=16,viscosity=.02),
|
||||
[(4,4,12,12),(8,8,16,16),(8,8,16,16)],
|
||||
[(4,4),(4,8),(6,4),(4,6)])
|
||||
four = (CFG,FOUR,[(6,8),(8,6),(12,8),(16,12)])
|
||||
concave = (LBMConfig(nx=30,ny=30,viscosity=.1),
|
||||
[[(4,4,5,7),(4,5,7,7)]],[(6,5),(4,6)])
|
||||
return dict(scalar=scalar,four=four,concave=concave)
|
||||
|
||||
|
||||
def baseline_receipt(receipt, driver_hash, keys):
|
||||
if (receipt.get('completed') is not True or receipt.get('dry_run') is not False
|
||||
or receipt.get('mode') != 'baseline' or receipt.get('production_revision') != BASE
|
||||
or receipt.get('source',{}).get('revision') != BASE
|
||||
or receipt.get('source',{}).get('dirty') is not False
|
||||
or receipt.get('driver_sha256') != driver_hash
|
||||
or receipt.get('stops') != list(STOPS)
|
||||
or receipt.get('operator_tolerance') != [OPERATOR_ATOL,OPERATOR_RTOL]
|
||||
or receipt.get('rest_tolerance') != [REST_ATOL,REST_RTOL]
|
||||
or receipt.get('replay_tolerance') != [REPLAY_ATOL,0]
|
||||
or set(receipt.get('default_snapshots',{})) != set(keys)
|
||||
or not receipt.get('actual_build_sidecars')):
|
||||
raise ValueError('baseline receipt does not match the frozen exact-dev gate')
|
||||
return receipt
|
||||
|
||||
|
||||
def remap_case(ctx, shifted, raw):
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
cfg,refs,blocks = cases()['four']
|
||||
with FixedPatchStepper(replace(cfg,velocity=0,ddf_shifting=shifted),context=ctx,
|
||||
refinements=refs[:2],level_blocks=blocks[:3],explosion='projected_linear') as run:
|
||||
initialize(run); run.step(1)
|
||||
before = snapshot(run,raw)
|
||||
with run.remap_child([(22,22,42,42),(72,68,76,76)]) as clone:
|
||||
if clone._explosion != 'projected_linear' or any(p.neighbors is None for p in clone._pairs):
|
||||
raise AssertionError('remap lost projected selection')
|
||||
if clone.step_count != 1: raise AssertionError('remap lost synchronized count')
|
||||
initial = snapshot(clone,raw)
|
||||
clone.step(1)
|
||||
after = snapshot(clone,raw)
|
||||
if clone.step_count != 2: raise AssertionError('remap did not continue count')
|
||||
compare_snapshots([snapshot(run,raw)],[before])
|
||||
return dict(shifted=shifted,source_unchanged=True,initial=initial,continued=after)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def capture_builds(report, save, baseline=None):
|
||||
from CelerisLab.cuda import compiler_v2 as compiler
|
||||
original = compiler.load_module
|
||||
def load(path):
|
||||
path = Path(path)
|
||||
metadata = json.loads(Path(str(path)+'.json').read_text())
|
||||
if metadata['ptx_sha256'] != hashlib.sha256(path.read_bytes()).hexdigest():
|
||||
raise AssertionError('actual PTX and build sidecar disagree')
|
||||
if baseline and (metadata['arch'],metadata['flags']) not in [
|
||||
(m['arch'],m['flags']) for m in baseline['actual_build_sidecars']]:
|
||||
raise AssertionError('baseline build architecture or flags differ')
|
||||
report['actual_build_sidecars'].append(metadata)
|
||||
save() # capture AMR metadata before production cleanup removes scratch
|
||||
return original(str(path))
|
||||
compiler.load_module = load
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
compiler.load_module = original
|
||||
|
||||
|
||||
def preflight(repo, revision, mode):
|
||||
git = lambda *a: subprocess.check_output(['git','-C',str(repo),*a],text=True).strip()
|
||||
if git('rev-parse','HEAD') != revision or git('status','--porcelain'):
|
||||
raise ValueError('requires the exact expected clean checkout')
|
||||
source = BASE if mode == 'baseline' else PRODUCTION
|
||||
if mode == 'baseline' and revision != BASE:
|
||||
raise ValueError('baseline must be exact ed9d8c9')
|
||||
if subprocess.call(['git','-C',str(repo),'diff','--quiet',source,'--','src']):
|
||||
raise ValueError('production source differs from the frozen source revision')
|
||||
return source
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--repo',type=Path,required=True)
|
||||
parser.add_argument('--expected-revision',required=True)
|
||||
parser.add_argument('--mode',choices=('baseline','candidate'),required=True)
|
||||
parser.add_argument('--baseline',type=Path)
|
||||
parser.add_argument('--output',type=Path,required=True)
|
||||
parser.add_argument('--run-gpu',action='store_true')
|
||||
parser.add_argument('--dry-run',action='store_true')
|
||||
args = parser.parse_args()
|
||||
if args.output.exists(): parser.error('output already exists')
|
||||
selected = os.environ.get('CUDA_VISIBLE_DEVICES','')
|
||||
if not args.dry_run and (not args.run_gpu or not selected or ',' in selected or selected=='-1'):
|
||||
parser.error('--run-gpu and exactly one explicit CUDA_VISIBLE_DEVICES are required')
|
||||
repo = args.repo.resolve()
|
||||
source = preflight(repo,args.expected_revision,args.mode)
|
||||
baseline = None
|
||||
if args.mode == 'candidate':
|
||||
if args.baseline is None: parser.error('candidate requires --baseline receipt')
|
||||
baseline = json.loads(args.baseline.read_text())
|
||||
sys.path[:0] = [str(repo/'src'),str(repo),str(repo/'tests/validation/multigrid'),
|
||||
str(repo/'tests/validation/taylor_green')]
|
||||
import CelerisLab
|
||||
if Path(CelerisLab.__file__).resolve() != repo/'src/CelerisLab/__init__.py':
|
||||
raise RuntimeError('wrong imported checkout')
|
||||
from run_case import provenance
|
||||
driver_hash = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
|
||||
keys = [f'{name}:shifted={shifted}' for name in cases() for shifted in (False,True)]
|
||||
if baseline is not None: baseline_receipt(baseline,driver_hash,keys)
|
||||
report = dict(mode=args.mode,production_revision=source,source=provenance(repo),
|
||||
driver_sha256=driver_hash,
|
||||
device_selection=selected,completed=False,dry_run=args.dry_run,
|
||||
operator_tolerance=[OPERATOR_ATOL,OPERATOR_RTOL],
|
||||
rest_tolerance=[REST_ATOL,REST_RTOL],replay_tolerance=[REPLAY_ATOL,0],stops=STOPS,
|
||||
conservation_status='diagnostic_only_no_FP32_bound',
|
||||
actual_build_sidecars=[],default_snapshots={},default_equivalence=[],
|
||||
projected_operator=[],union_multilevel_smoke=[],changed_remap=[])
|
||||
args.output.parent.mkdir(parents=True,exist_ok=True)
|
||||
def save():
|
||||
text = json.dumps(report,indent=2,allow_nan=False)+'\n'
|
||||
if len(text.encode()) > 8*1024*1024: raise RuntimeError('receipt exceeds 8 MiB cap')
|
||||
temporary = args.output.with_suffix(args.output.suffix+'.tmp')
|
||||
temporary.write_text(text); os.replace(temporary,args.output)
|
||||
save()
|
||||
if args.dry_run:
|
||||
print('Source preflight only; no CUDA imports or execution.'); return
|
||||
try:
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
from test_refinement_blocked_base import raw
|
||||
with CudaContext(0) as ctx, capture_builds(report,save,baseline):
|
||||
import pycuda.driver as cuda
|
||||
report['device'] = dict(name=ctx.device.name(),pci_bus_id=ctx.device.pci_bus_id(),
|
||||
capability=list(ctx.device.compute_capability()),
|
||||
driver=cuda.get_driver_version(),runtime=list(cuda.get_version()))
|
||||
nvcc = shutil.which('nvcc') or '/usr/local/cuda/bin/nvcc'
|
||||
report['nvcc_version'] = subprocess.check_output([nvcc,'--version'],text=True).strip()
|
||||
if baseline and (report['device'] != baseline.get('device')
|
||||
or report['nvcc_version'] != baseline.get('nvcc_version')
|
||||
or selected != baseline.get('device_selection')):
|
||||
raise ValueError('baseline GPU/device selection or toolchain differs')
|
||||
for name,case in cases().items():
|
||||
for shifted in (False,True):
|
||||
key = f'{name}:shifted={shifted}'
|
||||
report['active_case'] = f'default:{key}'
|
||||
result = run_case(ctx,case,shifted,'omitted',raw)
|
||||
report['default_snapshots'][key] = result['snapshots']
|
||||
if baseline:
|
||||
compare_snapshots(result['snapshots'],baseline['default_snapshots'][key])
|
||||
explicit = run_case(ctx,case,shifted,'constant',raw)
|
||||
compare_snapshots(explicit['snapshots'],result['snapshots'])
|
||||
report['default_equivalence'].append(dict(case=key,passed=True))
|
||||
save()
|
||||
if baseline:
|
||||
for name,case in cases().items():
|
||||
for shifted in (False,True):
|
||||
report['active_case'] = f'projected:{name}:shifted={shifted}'
|
||||
result = run_case(ctx,case,shifted,'projected_linear',raw,True,name=='concave')
|
||||
cfg,refs,blocks = case
|
||||
if name == 'four':
|
||||
variant_refs = [list(reversed(rects)) for rects in refs]
|
||||
elif name == 'concave':
|
||||
variant_refs = [[(4,5,7,7),(4,4,5,5)]] # same union, disjoint decomposition
|
||||
else:
|
||||
variant_refs = refs
|
||||
variant = (cfg,variant_refs,[(2,2)]*len(blocks))
|
||||
alternate = run_case(ctx,variant,shifted,'projected_linear',raw,True)
|
||||
compare_snapshots(alternate['snapshots'],result['snapshots'])
|
||||
report['projected_operator'].append(dict(case=name,shifted=shifted,rows=result['operator']))
|
||||
report['union_multilevel_smoke'].append(dict(case=name,shifted=shifted,result=result,
|
||||
alternate_bits_identical=True))
|
||||
save()
|
||||
for shifted in (False,True):
|
||||
report['active_case'] = f'changed-remap:shifted={shifted}'
|
||||
report['changed_remap'].append(remap_case(ctx,shifted,raw)); save()
|
||||
expected_builds = {(s['arch'],tuple(s['flags'])) for s in baseline['actual_build_sidecars']}
|
||||
actual_builds = {(s['arch'],tuple(s['flags'])) for s in report['actual_build_sidecars']}
|
||||
if actual_builds != expected_builds:
|
||||
raise AssertionError('baseline build architecture or compiler flags differ')
|
||||
report['completed'] = True
|
||||
save()
|
||||
except Exception as error:
|
||||
report['error'] = f'{type(error).__name__}: {error}'
|
||||
save(); raise
|
||||
print(json.dumps(dict(completed=True,mode=args.mode,receipt=str(args.output))))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -7,6 +7,7 @@ At most eight coarse steps per case. No files, plots or flow arrays are saved.
|
||||
import hashlib
|
||||
import importlib.util
|
||||
from contextlib import nullcontext
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
import resource
|
||||
import sys
|
||||
@@ -21,7 +22,7 @@ import CelerisLab
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.cuda import compiler_v2 as compiler
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
from CelerisLab.lbm.refinement_stepper import PeriodicPatchStepper
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper, PeriodicPatchStepper
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
POP_ATOL = 3e-6
|
||||
@@ -60,6 +61,69 @@ def global_array(module, name, shape):
|
||||
|
||||
|
||||
class CoupledGPU(unittest.TestCase):
|
||||
def test_actual_tg_rates_one_interval(self):
|
||||
"""Algebraic rate/phase check, not a new physical Taylor--Green benchmark.
|
||||
|
||||
The N64/Re100 TG rates are exercised on the existing small patch with
|
||||
a nonequilibrium state. One interval checks g=C(f), including the
|
||||
shifted blocked path; no accuracy gate or production source changes.
|
||||
"""
|
||||
self.assertEqual(Path(CelerisLab.__file__).resolve(), ROOT / "src/CelerisLab/__init__.py")
|
||||
self.assertEqual(Path(compiler._KERNEL_DIR).resolve(), ROOT / "src/CelerisLab/lbm/kernels")
|
||||
spec = replace(ref.make_spec(collision="srt"),
|
||||
tau_coarse=.5576, tau_fine=.6152)
|
||||
maps = ref.build_maps(spec)
|
||||
state = ref.make_state(spec, velocity=(.03, -.02))
|
||||
rng = np.random.default_rng(1)
|
||||
state = ref.State(*(a * (1 + 1e-3 * rng.standard_normal(a.shape))
|
||||
for a in (state.coarse_f, state.fine_f)))
|
||||
initial = postcollision(state, spec, maps)
|
||||
next_state, _ = ref.advance_coarse_once(spec, maps, state)
|
||||
expected = postcollision(next_state, spec, maps)
|
||||
arrivals = (next_state.coarse_f, next_state.fine_f)
|
||||
|
||||
cases = (("dense_unshifted", False, {}),
|
||||
("blocked_shifted", True,
|
||||
dict(base_block=(4, 4), fine_block=(6, 6))))
|
||||
with CudaContext(0) as context:
|
||||
for label, shifted, storage in cases:
|
||||
with self.subTest(storage=label):
|
||||
cfg = LBMConfig(nx=16, ny=16, viscosity=.0192,
|
||||
ddf_shifting=shifted)
|
||||
with FixedPatchStepper(cfg, (4, 4, 12, 12), context=context,
|
||||
**storage) as backend:
|
||||
self.assertAlmostEqual(backend.patch.tau_coarse, spec.tau_coarse)
|
||||
self.assertAlmostEqual(backend.patch.tau_fine, spec.tau_fine)
|
||||
self.assertEqual(tuple(level.shape for level in backend._levels),
|
||||
((9, 16, 16), (9, 24, 24)))
|
||||
# Physical FP64 g is encoded by initialize before FP32 rounding.
|
||||
backend.initialize(*initial)
|
||||
before = totals(backend.download(), maps)
|
||||
backend.step(1)
|
||||
actual = backend.download()
|
||||
self.assertEqual(backend.step_count, 1)
|
||||
backend.patch.check_synchronization(
|
||||
1, next_state.tick, phase="postcollision_g")
|
||||
for got, want, arrival in zip(actual, expected, arrivals):
|
||||
np.testing.assert_array_equal(np.isnan(got), np.isnan(want))
|
||||
np.testing.assert_allclose(got, want, rtol=0, atol=POP_ATOL,
|
||||
equal_nan=True)
|
||||
# A raw arrival-f comparison must fail on BOTH levels.
|
||||
# Equilibrium-only data would make this control vacuous.
|
||||
with self.assertRaises(AssertionError):
|
||||
np.testing.assert_allclose(got, arrival, rtol=0,
|
||||
atol=POP_ATOL, equal_nan=True)
|
||||
np.testing.assert_allclose(totals(actual, maps), before,
|
||||
rtol=0, atol=MOMENT_ATOL)
|
||||
np.testing.assert_allclose(totals(actual, maps),
|
||||
totals(expected, maps),
|
||||
rtol=0, atol=MOMENT_ATOL)
|
||||
error = max(float(np.nanmax(np.abs(a - e)))
|
||||
for a, e in zip(actual, expected))
|
||||
print(f"actual-TG-rate algebraic check: {label} steps=1 "
|
||||
f"tau=.5576/.6152 max_population={error:.9g}; "
|
||||
"wrong-phase controls PASS", flush=True)
|
||||
|
||||
def test_cleanup_attempts_every_allocation(self):
|
||||
backend = PeriodicPatchStepper.__new__(PeriodicPatchStepper)
|
||||
backend.ctx = Mock(activate=nullcontext)
|
||||
|
||||
@@ -45,7 +45,7 @@ def config(h, diameter, collision="SRT"):
|
||||
return cfg, body, p
|
||||
|
||||
|
||||
def plan(h, collision="SRT"):
|
||||
def plan(h, collision="SRT", observation_mode="last_base_step"):
|
||||
_, _, c = config(h, 40, collision)
|
||||
_, _, f = config(h, 80, collision)
|
||||
x0, y0, x1, y1 = BOUNDS
|
||||
@@ -54,6 +54,7 @@ def plan(h, collision="SRT"):
|
||||
return dict(
|
||||
status="prepared_not_run",
|
||||
collision=collision,
|
||||
observation_mode=observation_mode,
|
||||
root_gpu_gates_required=True,
|
||||
root=c,
|
||||
uniform=f,
|
||||
@@ -156,10 +157,13 @@ def host_geometry(source, h, collision="SRT"):
|
||||
return cfg, pair, body, host
|
||||
|
||||
|
||||
def verify_source(source, collision="SRT"):
|
||||
def verify_source(source, collision="SRT", observation_mode="last_base_step"):
|
||||
if collision not in ("SRT", "MRT"):
|
||||
raise ValueError("Matched collision must be SRT or MRT")
|
||||
name = "source_manifest.json" if collision == "SRT" else "source_manifest_mrt.json"
|
||||
if observation_mode not in ("last_base_step", "window"):
|
||||
raise ValueError("Unknown refined observation mode")
|
||||
name = ("source_manifest_window.json" if observation_mode == "window" else
|
||||
"source_manifest.json" if collision == "SRT" else "source_manifest_mrt.json")
|
||||
expected = json.loads((HERE / "data/refined_dfg" / name).read_text())
|
||||
for name, digest in expected["sha256"].items():
|
||||
if hashlib.sha256((source / name).read_bytes()).hexdigest() != digest:
|
||||
@@ -167,6 +171,46 @@ def verify_source(source, collision="SRT"):
|
||||
return expected
|
||||
|
||||
|
||||
def advance_refined_interval(backend, n, deadline, observation_mode):
|
||||
"""Return the same root-unit interval impulse, never an already averaged force."""
|
||||
impulse = np.zeros(2)
|
||||
if observation_mode == "window":
|
||||
begin = backend.step_count
|
||||
check_deadline(deadline)
|
||||
backend.start_exchange_window()
|
||||
backend.step(n)
|
||||
exchange = backend.read_exchange_window(0, level=1)
|
||||
if (
|
||||
exchange["units"] != "base_lattice_per_unit_depth"
|
||||
or exchange["window"] != "window"
|
||||
or exchange["sample_count"] != 2 * n
|
||||
or exchange["time_start"] != begin
|
||||
or exchange["time_end"] != begin + n
|
||||
or exchange["dt"] != 0.5
|
||||
):
|
||||
raise RuntimeError("Unexpected window force observation contract")
|
||||
impulse += exchange["total"]["impulse"]
|
||||
check_deadline(deadline)
|
||||
return impulse
|
||||
if observation_mode != "last_base_step":
|
||||
raise ValueError("Unknown refined observation mode")
|
||||
for _ in range(n):
|
||||
check_deadline(deadline)
|
||||
backend.step(1)
|
||||
exchange = backend.read_raw_exchange(
|
||||
0, level=1, window="last_base_step"
|
||||
)
|
||||
if (
|
||||
exchange["units"] != "base_lattice_per_unit_depth"
|
||||
or exchange["sample_count"] != 2
|
||||
or exchange["time_end"] - exchange["time_start"] != 1
|
||||
):
|
||||
raise RuntimeError("Unexpected force observation contract")
|
||||
impulse += exchange["total"]["impulse"]
|
||||
check_deadline(deadline)
|
||||
return impulse
|
||||
|
||||
|
||||
def run_case(a, h, periodic, case):
|
||||
from CelerisLab.simulation import Simulation
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
@@ -199,6 +243,7 @@ def run_case(a, h, periodic, case):
|
||||
fine_block=FINE_BLOCK,
|
||||
fine_objects=(body,),
|
||||
observe_raw_exchange=True,
|
||||
**({"observe_window_exchange": True} if a.observation_mode == "window" else {}),
|
||||
)
|
||||
)
|
||||
from CelerisLab.lbm.channel import channel_masks
|
||||
@@ -248,20 +293,9 @@ def run_case(a, h, periodic, case):
|
||||
impulse = np.zeros(2)
|
||||
tick = time.monotonic()
|
||||
if refined:
|
||||
for _ in range(n):
|
||||
check_deadline(a.deadline)
|
||||
backend.step(1)
|
||||
exchange = backend.read_raw_exchange(
|
||||
0, level=1, window="last_base_step"
|
||||
)
|
||||
if (
|
||||
exchange["units"] != "base_lattice_per_unit_depth"
|
||||
or exchange["sample_count"] != 2
|
||||
or exchange["time_end"] - exchange["time_start"] != 1
|
||||
):
|
||||
raise RuntimeError("Unexpected force observation contract")
|
||||
impulse += exchange["total"]["impulse"]
|
||||
check_deadline(a.deadline)
|
||||
impulse = advance_refined_interval(
|
||||
backend, n, a.deadline, a.observation_mode
|
||||
)
|
||||
else:
|
||||
done = 0
|
||||
while done < n:
|
||||
@@ -345,6 +379,7 @@ def run_case(a, h, periodic, case):
|
||||
ready = dict(
|
||||
case=case,
|
||||
collision=a.collision,
|
||||
observation_mode=a.observation_mode,
|
||||
**a.provenance,
|
||||
mode=a.mode,
|
||||
step=begin,
|
||||
@@ -359,6 +394,7 @@ def run_case(a, h, periodic, case):
|
||||
result = dict(
|
||||
case=case,
|
||||
collision=a.collision,
|
||||
observation_mode=a.observation_mode,
|
||||
**a.provenance,
|
||||
mode=a.mode,
|
||||
history=rows,
|
||||
@@ -428,12 +464,14 @@ def check_deadline(deadline):
|
||||
)
|
||||
|
||||
|
||||
def verify_pilot(path, manifest, collision="SRT"):
|
||||
def verify_pilot(path, manifest, collision="SRT", observation_mode="last_base_step"):
|
||||
if path is None:
|
||||
raise ValueError(
|
||||
"Full comparison requires --pilot-results from a completed matched pilot"
|
||||
)
|
||||
previous = read_finite_json(path / "plan.json")
|
||||
if previous.get("observation_mode") != observation_mode:
|
||||
raise ValueError("Pilot observation mode differs")
|
||||
if previous.get("collision") != collision:
|
||||
raise ValueError("Pilot collision differs")
|
||||
if previous["source_manifest"] != manifest or previous["bounds_root_edge"] != list(
|
||||
@@ -446,7 +484,8 @@ def verify_pilot(path, manifest, collision="SRT"):
|
||||
for case, diameter in (("uniform", 80), ("refined", 40)):
|
||||
summary = read_finite_json(path / case / "summary.json")
|
||||
if (
|
||||
summary.get("collision") != collision
|
||||
summary.get("observation_mode") != observation_mode
|
||||
or summary.get("collision") != collision
|
||||
or summary.get("case") != case
|
||||
or summary.get("mode") != "pilot"
|
||||
or summary.get("status") != "pilot_complete"
|
||||
@@ -498,7 +537,8 @@ def verify_pilot(path, manifest, collision="SRT"):
|
||||
raise ValueError("Unhealthy pilot observations")
|
||||
ready = read_finite_json(path / case / "ready.json")
|
||||
if (
|
||||
ready.get("collision") != collision
|
||||
ready.get("observation_mode") != observation_mode
|
||||
or ready.get("collision") != collision
|
||||
or ready.get("case") != case
|
||||
or ready.get("mode") != "pilot"
|
||||
or ready.get("step") != interval
|
||||
@@ -523,6 +563,8 @@ def verify_pilot(path, manifest, collision="SRT"):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--exchange-window", action="store_true",
|
||||
help="Opt in to refined window-ledger observations on the pinned candidate")
|
||||
parser.add_argument("--collision", choices=["SRT", "MRT"], default="SRT")
|
||||
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
@@ -536,13 +578,14 @@ def main():
|
||||
)
|
||||
a = parser.parse_args()
|
||||
a.source = a.source.resolve()
|
||||
manifest = verify_source(a.source, a.collision)
|
||||
a.observation_mode = "window" if a.exchange_window else "last_base_step"
|
||||
manifest = verify_source(a.source, a.collision, a.observation_mode)
|
||||
h, periodic = helpers(a.source)
|
||||
from source_provenance import checkout_provenance
|
||||
|
||||
a.provenance = checkout_provenance(a.source)
|
||||
calibration = (
|
||||
verify_pilot(a.pilot_results, manifest, a.collision)
|
||||
verify_pilot(a.pilot_results, manifest, a.collision, a.observation_mode)
|
||||
if a.mode == "full" else None
|
||||
)
|
||||
if a.mode != "plan" and (
|
||||
@@ -559,7 +602,7 @@ def main():
|
||||
if a.output.exists() and any(a.output.iterdir()):
|
||||
raise ValueError("Output must be empty/new")
|
||||
a.output.mkdir(parents=True, exist_ok=True)
|
||||
specification = plan(h, a.collision)
|
||||
specification = plan(h, a.collision, a.observation_mode)
|
||||
specification.update(
|
||||
source_manifest=manifest,
|
||||
**a.provenance,
|
||||
@@ -586,6 +629,7 @@ def main():
|
||||
failure = dict(
|
||||
status="wall_time_limit",
|
||||
collision=a.collision,
|
||||
observation_mode=a.observation_mode,
|
||||
**a.provenance,
|
||||
case=case,
|
||||
matched_case_pass=False,
|
||||
@@ -601,6 +645,7 @@ def main():
|
||||
break
|
||||
comparison = dict(
|
||||
collision=a.collision,
|
||||
observation_mode=a.observation_mode,
|
||||
**a.provenance,
|
||||
status="pilot_only" if a.mode == "pilot" else "incomplete",
|
||||
matched_case_pass=False,
|
||||
|
||||
@@ -59,17 +59,24 @@ def decode_macros(data, w, cx, cy):
|
||||
return profile, *metrics
|
||||
|
||||
|
||||
def source_guard(root, variant):
|
||||
manifest = json.loads((HERE / "data/les_flow/source_pins.json").read_text())
|
||||
EXPERIMENTS = {
|
||||
"historical": "source_pins.json",
|
||||
"dev8403956": "source_pins_dev8403956.json",
|
||||
"a135252": "source_pins_a135252.json",
|
||||
}
|
||||
|
||||
|
||||
def source_guard(root, variant, experiment="historical"):
|
||||
manifest_path = HERE / "data/les_flow" / EXPERIMENTS[experiment]
|
||||
manifest = json.loads(manifest_path.read_text())
|
||||
for name, sha in manifest["sources"].items():
|
||||
expected = (
|
||||
manifest["candidate_header_sha256"]
|
||||
if name == HEADER and variant == "corrected"
|
||||
else sha
|
||||
)
|
||||
expected = sha
|
||||
if name == HEADER:
|
||||
expected = (manifest["candidate_header_sha256"] if variant == "corrected"
|
||||
else manifest.get("baseline_header_sha256", sha))
|
||||
if hashlib.sha256((root / name).read_bytes()).hexdigest() != expected:
|
||||
raise RuntimeError("Source pin mismatch: " + name)
|
||||
return dict(
|
||||
identity = dict(
|
||||
base_commit=manifest["base_commit"],
|
||||
production_origin_commit=manifest["production_origin_commit"],
|
||||
source_experiment=manifest["source_experiment"],
|
||||
@@ -80,12 +87,22 @@ def source_guard(root, variant):
|
||||
header_sha256=hashlib.sha256((root / HEADER).read_bytes()).hexdigest(),
|
||||
variant=variant,
|
||||
)
|
||||
if experiment != "historical":
|
||||
identity.update(
|
||||
experiment=experiment,
|
||||
source_manifest_sha256=hashlib.sha256(manifest_path.read_bytes()).hexdigest(),
|
||||
)
|
||||
return identity
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--root", type=Path, required=True)
|
||||
ap.add_argument("--variant", choices=["baseline", "corrected"], required=True)
|
||||
ap.add_argument(
|
||||
"--experiment", choices=tuple(EXPERIMENTS), default="historical",
|
||||
help="Explicit source contract; historical pins remain the default",
|
||||
)
|
||||
ap.add_argument("--output", type=Path, required=True)
|
||||
ap.add_argument(
|
||||
"--device", type=int, default=0, help="Logical device in CUDA_VISIBLE_DEVICES"
|
||||
@@ -100,7 +117,7 @@ def main():
|
||||
ap.error("Output exists; choose a new evidence path")
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
root = args.root.resolve()
|
||||
pins = source_guard(root, args.variant)
|
||||
pins = source_guard(root, args.variant, args.experiment)
|
||||
# Refuse installed-package shadowing; import only after explicit GPU opt-in.
|
||||
sys.path.insert(0, str(root / "src"))
|
||||
import CelerisLab
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
"""New-source four-case repeat of 11901a7; frozen oracle and gates are unchanged."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from assess import BASELINE_FACTOR, FLOORS
|
||||
from oracle import LIMITS, TIMES, U, rectangles
|
||||
from run_case import (INITIALIZATION, sample_steps, NUMERICAL_REVISION, SOURCE_SHA256,
|
||||
BUILD_CAPTURE_SHA256, REFERENCE_HARNESS, SCALED_DIAGNOSTIC)
|
||||
|
||||
# Host-generated snapshots of a135252 at the frozen local configurations.
|
||||
# These describe expected inputs; actual sidecars are captured before cleanup.
|
||||
BUILD_SOURCES = {
|
||||
128: "803177416b7599ab89cc5afd35cf4a28a673b2f4bb8e8a6a1abc0b6d77610873",
|
||||
256: "17ec5e60f87b840d3d13b8778bc8a3aecf485469c5a061af46c5cc19b73396cf",
|
||||
512: "4e85e38098858ec95c137d1b1e96846a58d9cf3cc242b349a3ad7c8b8e3ce0aa",
|
||||
}
|
||||
CASES = ((100, 128, 1, "constant"), (100, 256, 1, "constant"),
|
||||
(100, 128, 2, "constant"), (100, 128, 2, "projected_linear"))
|
||||
REFERENCE_REVISION = "2cf9853952e52f59bea63cf5c1d3a1cd76e53eed"
|
||||
# Audited 2cf9853 scalar receipts, archived at evidence commit 90849a55cad4a0da9b72747da7bfe6eb75962a44.
|
||||
REFERENCE_RECEIPTS = {
|
||||
"uniform128": "724efafc267eaa59dcc59e8d4c16177b67f6c6a08fd2732a481833237bb19cee",
|
||||
"uniform256": "3c312a99ebf747a135674dd481b77455a3d2c9a406ca388f30524b37dad9bbe9",
|
||||
"constant128": "2ee28efd1e81b2e28b7902b6c4f95bb2c1ea4e5f0ec484638fa5ae83dca34bba",
|
||||
"projected128": "ee67b1d4d90a048c0d3d1a16447c6eab3d89c94d151f791306e373f96dd8b963",
|
||||
}
|
||||
NUMERICS = dict(lattice_model="D2Q9", collision="SRT", store_precision="FP32",
|
||||
ddf_shifting=True, streaming="double_buffer", les_enabled=False,
|
||||
boundary="periodic", block_shape=[8, 8],
|
||||
sample_phase="postcollision G at root synchronization")
|
||||
|
||||
|
||||
def finite(value):
|
||||
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)
|
||||
|
||||
|
||||
def finite_tree(value):
|
||||
if isinstance(value, dict):
|
||||
return all(finite_tree(v) for v in value.values())
|
||||
if isinstance(value, list):
|
||||
return all(finite_tree(v) for v in value)
|
||||
return not isinstance(value, float) or math.isfinite(value)
|
||||
|
||||
|
||||
def at_steps(rows, steps, n):
|
||||
return len(rows) == len(steps) and all(
|
||||
type(r.get("step")) is int and r["step"] == step and finite(r.get("tstar"))
|
||||
and abs(r["tstar"] - step * U / n) <= 3e-17
|
||||
for r, step in zip(rows, steps)
|
||||
)
|
||||
|
||||
|
||||
def maximum(rows, metric):
|
||||
values = [r.get(metric) for r in rows]
|
||||
return max(values) if values and all(finite(v) and v >= 0 for v in values) else None
|
||||
|
||||
|
||||
def valid_builds(record, n, depth):
|
||||
builds = record.get("actual_build_sidecars", [])
|
||||
if not isinstance(builds, list) or len(builds) != depth:
|
||||
return False
|
||||
for level, build in enumerate(builds):
|
||||
if not isinstance(build, dict):
|
||||
return False
|
||||
size = n * 2**level
|
||||
cfg = build.get("config", {})
|
||||
expected = dict(nx=10, ny=10, nz=1, lattice_model="D2Q9", collision="SRT",
|
||||
store_precision="FP32", streaming="double_buffer", ddf_shifting=True,
|
||||
les_enabled=False, velocity=0.0, viscosity=U*size/100, rho=1.0,
|
||||
omega=1/(.5+3*U*size/100), threads_per_block=256)
|
||||
ptx = build.get("ptx_sha256", "")
|
||||
if (build.get("source_sha256") != BUILD_SOURCES[size]
|
||||
or build.get("arch") != "sm_70" or build.get("flags") != ["--use_fast_math"]
|
||||
or build.get("n_objects") != 0 or not isinstance(cfg, dict)
|
||||
or any(cfg.get(k) != value for k, value in expected.items())
|
||||
or not isinstance(ptx, str) or len(ptx) != 64
|
||||
or any(c not in "0123456789abcdef" for c in ptx)):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def assess(records, expected_revision=None, *, diagnostic=None):
|
||||
if diagnostic not in (None, SCALED_DIAGNOSTIC):
|
||||
raise ValueError("Unknown diagnostic")
|
||||
base_n = 256 if diagnostic else 128
|
||||
cases = tuple((re, n * base_n // 128, depth, scheme) for re, n, depth, scheme in CASES)
|
||||
indexed, duplicates = {}, []
|
||||
for record in records:
|
||||
key = tuple(record.get(k) for k in ("re_L", "n", "depth", "explosion"))
|
||||
if key in indexed:
|
||||
duplicates.append(str(key))
|
||||
else:
|
||||
indexed[key] = record
|
||||
missing = [str(k) for k in cases if k not in indexed]
|
||||
checks = dict(complete_matrix=not missing, no_extra_cases=set(indexed) <= set(cases),
|
||||
no_duplicate_cases=not duplicates, finite_receipts=finite_tree(records))
|
||||
if diagnostic or any("diagnostic" in r for r in records):
|
||||
checks["diagnostic_identity"] = all(r.get("diagnostic") == diagnostic for r in records)
|
||||
sources = [r.get("source", {}) for r in records]
|
||||
checks["same_source_and_harness"] = bool(sources) and len({
|
||||
json.dumps(s, sort_keys=True) for s in sources}) == 1
|
||||
checks["clean_a135252_numerical_source"] = bool(sources) and all(
|
||||
s.get("dirty") is False and s.get("source_tree_sha256") == SOURCE_SHA256
|
||||
and bool(s.get("revision")) and bool(s.get("harness_sha256"))
|
||||
and s.get("build_capture_sha256") == BUILD_CAPTURE_SHA256
|
||||
and (expected_revision is None or s.get("revision") == expected_revision) for s in sources)
|
||||
runtimes = [r.get("runtime_identity") for r in records]
|
||||
checks["same_recorded_runtime"] = bool(runtimes) and all(
|
||||
isinstance(v, dict) and all(v.get(k) for k in
|
||||
("device", "nvcc_path", "nvcc_version", "python", "numpy", "pycuda"))
|
||||
and isinstance(v["device"], dict) and v["device"].get("capability") == [7, 0]
|
||||
and all(v["device"].get(k) for k in ("name", "pci_bus_id", "driver", "runtime"))
|
||||
for v in runtimes
|
||||
) and len({json.dumps(v, sort_keys=True) for v in runtimes}) == 1
|
||||
devices = [r.get("cuda_visible_devices") for r in records]
|
||||
checks["same_explicit_device"] = bool(devices) and all(
|
||||
isinstance(d, str) and d and "," not in d and d != "-1" for d in devices
|
||||
) and len(set(devices)) == 1
|
||||
enabled = [r.get("early_pressure", {}).get("enabled") for r in records]
|
||||
checks["same_early_sampling"] = bool(enabled) and all(type(v) is bool for v in enabled) and len(set(enabled)) == 1
|
||||
if diagnostic:
|
||||
checks["diagnostic_early_sampling"] = all(v is True for v in enabled)
|
||||
shared_valid = all(checks.values())
|
||||
complete, case_valid, early = {}, {}, []
|
||||
for key in cases:
|
||||
if key not in indexed:
|
||||
continue
|
||||
r = indexed[key]
|
||||
_, n, depth, scheme = key
|
||||
label = f"{n}/{depth}/{scheme}"
|
||||
rows = r.get("rows", [])
|
||||
main_steps, early_steps = sample_steps(n, True)
|
||||
complete[key] = r.get("completed") is True and at_steps(rows, main_steps, n)
|
||||
checks[label + " complete_times"] = complete[key]
|
||||
checks[label + " scales"] = (
|
||||
r.get("U") == U and finite(r.get("viscosity"))
|
||||
and abs(r["viscosity"] - U * n / 100) < 1e-15
|
||||
and r.get("tau_by_level") == [0.5 + 3 * U * n / 100 * 2**l for l in range(depth)]
|
||||
and r.get("mach") == U * math.sqrt(3)
|
||||
)
|
||||
checks[label + " actual_builds"] = valid_builds(r, n, depth)
|
||||
checks[label + " reference"] = (r.get("numerical_revision") == NUMERICAL_REVISION
|
||||
and r.get("reference_harness") == REFERENCE_HARNESS)
|
||||
checks[label + " numerics"] = r.get("numerics") == NUMERICS and r.get("initialization") == INITIALIZATION
|
||||
runtime = r.get("runtime_source", {})
|
||||
package = Path(runtime.get("checkout", "")) / "src/CelerisLab"
|
||||
checks[label + " runtime_source"] = bool(runtime.get("checkout")) and all(
|
||||
runtime.get(k) == str(package / tail) for k, tail in
|
||||
(("module", "__init__.py"), ("compiler", "cuda/compiler_v2.py"), ("kernel_root", "lbm/kernels")))
|
||||
expected_patch = [] if depth == 1 else rectangles(n, depth)
|
||||
checks[label + " patch"] = json.dumps(r.get("refinements")) == json.dumps(expected_patch)
|
||||
cells = [n*n] if depth == 1 else [17*n*n//32, 15*n*n//8]
|
||||
hops = [16*n * 2**l for l in range(depth)]
|
||||
work = r.get("workload", {})
|
||||
checks[label + " workload"] = (
|
||||
work.get("active_cells_by_level") == cells and work.get("level_hops") == hops
|
||||
and work.get("root_intervals") == 16*n
|
||||
and work.get("active_collision_updates_by_level") == [c*h for c, h in zip(cells, hops)]
|
||||
and work.get("active_collision_updates") == sum(c*h for c, h in zip(cells, hops))
|
||||
)
|
||||
checks[label + " absolute"] = bool(rows) and all(
|
||||
finite(row.get(metric)) and 0 <= row[metric] <= limit
|
||||
for row in rows for metric, limit in LIMITS.items())
|
||||
trace = r.get("early_pressure", {})
|
||||
trace_rows = trace.get("rows", [])
|
||||
peak = maximum(trace_rows, "pressure_l2")
|
||||
trace_complete = at_steps(trace_rows, early_steps, n) and peak is not None
|
||||
checks[label + " early_trace"] = (
|
||||
trace_complete and trace.get("sampled_max_pressure_l2") == peak
|
||||
and len(rows) >= 2 and all(trace_rows[i]["pressure_l2"] == rows[j].get("pressure_l2")
|
||||
for i, j in ((0, 0), (-1, 1)))
|
||||
if trace.get("enabled") is True else
|
||||
trace.get("enabled") is False and trace_rows == [] and trace.get("sampled_max_pressure_l2") is None
|
||||
)
|
||||
early.append(dict(n=n, depth=depth, explosion=scheme, enabled=trace.get("enabled"),
|
||||
samples=len(trace_rows), complete=trace_complete,
|
||||
sampled_max_pressure_l2=peak))
|
||||
case_valid[key] = all(value for name, value in checks.items() if name.startswith(label + " "))
|
||||
|
||||
def worst(key, metric):
|
||||
return maximum(indexed[key]["rows"][1:], metric) if complete.get(key) else None
|
||||
|
||||
comparisons, scheme_results = [], {}
|
||||
for scheme in ("constant", "projected_linear"):
|
||||
key = (100, base_n, 2, scheme)
|
||||
for metric, floor in FLOORS.items():
|
||||
amr, coarse, fine = (worst(k, metric) for k in (key, cases[0], cases[1]))
|
||||
budget = None if coarse is None else max(BASELINE_FACTOR * coarse, floor)
|
||||
budget = budget if finite(budget) else None
|
||||
ratio = None if amr is None or fine is None else amr / max(fine, 1e-30)
|
||||
passed = amr is not None and budget is not None and amr <= budget
|
||||
checks[f"baseline {scheme}/{metric}"] = passed
|
||||
comparisons.append(dict(explosion=scheme, metric=metric, amr=amr,
|
||||
baseline_budget=budget, passed=passed,
|
||||
**{f"uniform{base_n}": coarse, f"uniform{2*base_n}": fine,
|
||||
f"amr_to_uniform{2*base_n}": ratio if finite(ratio) else None}))
|
||||
if diagnostic:
|
||||
comparisons[-1]["floor_dominates"] = None if coarse is None else floor >= BASELINE_FACTOR * coarse
|
||||
baseline_passed = all(checks[f"baseline {scheme}/{metric}"] for metric in FLOORS)
|
||||
valid = shared_valid and all(case_valid.get(k, False) for k in (cases[0], cases[1], key))
|
||||
scheme_results[scheme] = dict(passed=valid and baseline_passed,
|
||||
valid_case_and_controls=valid, baseline_passed=baseline_passed)
|
||||
return dict(passed=all(checks.values()), missing=missing, duplicates=duplicates, checks=checks,
|
||||
comparisons=comparisons, scheme_results=scheme_results, early_pressure=early,
|
||||
numerical_revision=NUMERICAL_REVISION, source_tree_sha256=SOURCE_SHA256,
|
||||
reference_harness=REFERENCE_HARNESS, expected_revision=expected_revision,
|
||||
floors=FLOORS, baseline_factor=BASELINE_FACTOR,
|
||||
main_statistic="maximum of four post-initial samples at t*=.12,.24,.36,.48",
|
||||
early_statistic="sampled pressure maximum only, t*=k*.001875, k=0..64; not an all-step or continuous maximum",
|
||||
finest_control=f"uniform{2*base_n} and AMR/ uniform{2*base_n} ratios are descriptive, not acceptance gates",
|
||||
scope="2D periodic Re100 smooth-decay diagnostic; no new algorithm, convergence-order or general AMR qualification")
|
||||
|
||||
|
||||
def load_reference(paths):
|
||||
"""Use the four audited historical files verbatim, never relabel them fresh controls."""
|
||||
blobs = [path.read_bytes() for path in paths]
|
||||
hashes = [hashlib.sha256(blob).hexdigest() for blob in blobs]
|
||||
if len(hashes) != 4 or set(hashes) != set(REFERENCE_RECEIPTS.values()):
|
||||
raise ValueError("Historical N128 receipt hashes differ from the audited 2cf9853 evidence")
|
||||
return [json.loads(blob) for blob in blobs]
|
||||
|
||||
|
||||
def resolution_trend(records, reference, expected_revision):
|
||||
"""Signed changes are descriptive; they never replace the frozen acceptance budgets."""
|
||||
old = assess(reference, REFERENCE_REVISION)
|
||||
new = assess(records, expected_revision, diagnostic=SCALED_DIAGNOSTIC)
|
||||
# Numerical screen failures still have useful raw errors; only malformed inputs block trends.
|
||||
valid = all(v for result in (old, new) for k, v in result["checks"].items()
|
||||
if not k.startswith("baseline ") and not k.endswith(" absolute"))
|
||||
valid = valid and all(maximum(r["rows"], m) is not None for r in reference + records for m in LIMITS)
|
||||
oracle = reference[0]["source"]["harness_sha256"].get("oracle.py") if valid else None
|
||||
valid = valid and bool(oracle) and records[0]["source"]["harness_sha256"].get("oracle.py") == oracle
|
||||
if not valid:
|
||||
return dict(valid=False, per_time=[], worst_post_initial=[], uniform256_bridge=[])
|
||||
key = lambda r: (r["re_L"], r["n"], r["depth"], r["explosion"])
|
||||
before, after = {key(r): r for r in reference}, {key(r): r for r in records}
|
||||
|
||||
def change(role, metric, low, high, **extra):
|
||||
ratio = high / low if low else None
|
||||
percent = 100*(ratio-1) if ratio is not None else None
|
||||
return dict(role=role, metric=metric, n128_campaign_error=low, n256_campaign_error=high,
|
||||
signed_change=high-low, new_to_old=ratio if finite(ratio) else None,
|
||||
ratio_unavailable_reason=None if finite(ratio) else "zero historical error" if not low else "overflow",
|
||||
change_percent=percent if finite(percent) else None, **extra)
|
||||
|
||||
per_time, worst, bridge = [], [], []
|
||||
for old_key, role in zip(CASES, ("uniform_root", "uniform_fine", "constant", "projected_linear")):
|
||||
re, n, depth, scheme = old_key
|
||||
low, high = before[old_key], after[(re, 2*n, depth, scheme)]
|
||||
for metric in LIMITS:
|
||||
per_time.extend(change(role, metric, a[metric], b[metric], tstar=a["tstar"])
|
||||
for a, b in zip(low["rows"], high["rows"]))
|
||||
worst.append(change(role, metric, maximum(low["rows"][1:], metric),
|
||||
maximum(high["rows"][1:], metric)))
|
||||
for a, b in zip(before[CASES[1]]["rows"], after[CASES[1]]["rows"]):
|
||||
bridge.extend(dict(metric=m, tstar=a["tstar"], historical=a[m], fresh=b[m], signed_change=b[m]-a[m])
|
||||
for m in LIMITS)
|
||||
low, high = before[CASES[1]], after[CASES[1]]
|
||||
bridge_identity = dict(main_rows_identical=low["rows"] == high["rows"],
|
||||
early_pressure_rows_identical=low["early_pressure"]["rows"] == high["early_pressure"]["rows"])
|
||||
same_runtime = reference[0]["runtime_identity"] == records[0]["runtime_identity"]
|
||||
return dict(valid=True, reference_revision=REFERENCE_REVISION, reference_receipt_sha256=REFERENCE_RECEIPTS,
|
||||
reference_scheme_results=old["scheme_results"], per_time=per_time, worst_post_initial=worst,
|
||||
uniform256_bridge=bridge, uniform256_bridge_identity=bridge_identity,
|
||||
same_recorded_runtime=same_runtime,
|
||||
trend_interpretation_ready=all(bridge_identity.values()) and same_runtime,
|
||||
scope="Historical same-production/oracle reference versus fresh controls; same-placement two-resolution trend only, not convergence order or phase robustness")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("files", type=Path, nargs="+")
|
||||
p.add_argument("--output", type=Path, required=True)
|
||||
p.add_argument("--expected-revision", required=True)
|
||||
p.add_argument("--diagnostic", choices=[SCALED_DIAGNOSTIC])
|
||||
p.add_argument("--reference-files", type=Path, nargs=4)
|
||||
args = p.parse_args()
|
||||
if args.output.exists():
|
||||
p.error("Output already exists; choose a new path to retain prior evidence")
|
||||
if bool(args.diagnostic) != bool(args.reference_files):
|
||||
p.error("--diagnostic scaled_n256 requires --reference-files; references are diagnostic-only")
|
||||
records = [json.loads(path.read_text()) for path in args.files]
|
||||
result = assess(records, args.expected_revision, diagnostic=args.diagnostic)
|
||||
if args.diagnostic:
|
||||
try:
|
||||
reference = load_reference(args.reference_files)
|
||||
except ValueError as exc:
|
||||
p.error(str(exc))
|
||||
result["diagnostic"] = args.diagnostic
|
||||
result["resolution_trend"] = resolution_trend(records, reference, args.expected_revision)
|
||||
result["checks"]["valid_historical_reference_and_trend"] = result["resolution_trend"]["valid"]
|
||||
result["passed"] = all(result["checks"].values())
|
||||
args.output.write_text(json.dumps(result, indent=2, allow_nan=False) + "\n")
|
||||
print(json.dumps(result, indent=2, allow_nan=False))
|
||||
raise SystemExit(0 if result["passed"] else 1)
|
||||
@@ -0,0 +1,336 @@
|
||||
"""Fixed four-case shear-wave contracts.
|
||||
|
||||
Default: N128 (U128, U256, constant R128, projected R128). Opt-in
|
||||
--diagnostic scaled_shear_n256: N256 (U256, U512, constant R256, projected
|
||||
R256), same physical rectangle, t* schedule, nu = U N / 100, eps(N), limits
|
||||
and floors, plus a descriptive comparison with the hash-pinned N128 receipts.
|
||||
|
||||
Shared provenance, build, workload and sample-time checks are reused from
|
||||
assess_projected_pairs (same cases, source pins and geometry); only the
|
||||
Taylor-Green-specific checks are replaced by shear-wave metadata and physics.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import assess_projected_pairs as tg
|
||||
import shear_wave as sw
|
||||
import shear_nested as nested
|
||||
import shear_twin as twin
|
||||
from run_case import (NESTED_SHEAR, SCALED_DIAGNOSTIC, SCALED_SHEAR, SHEAR_NESTED_SHA256,
|
||||
SHEAR_TWIN_SHA256, SHEAR_WAVE_SHA256, TWIN_SHEAR, sample_steps)
|
||||
|
||||
# Audited de5b0cc N128 receipts (evidence commit cba6bca, shear-wave-de5b0cc zip).
|
||||
REFERENCE_REVISION = "de5b0cc080794d0ce44d37f845aa8e8dbf8c2016"
|
||||
REFERENCE_RECEIPTS = {
|
||||
"uniform128": "d4a60fb806d04bfa8098a862cceba51e00b49979a6a250fef17a5fc8e3ea3255",
|
||||
"uniform256": "e0b58e734724be5f82ee5d6df84395e3181b4e96e4246dd5a574ea23a8d12386",
|
||||
"constant128": "c70d7cd14b1257a01cc49f8edfcc7685d430e5301e2f2d60ed426936f8b96c51",
|
||||
"projected128": "980161458b9463af09a8e3178063fea0f4bbeb465329e1d3becdb0f224537b54",
|
||||
}
|
||||
ROLES = ("uniform_root", "uniform_fine", "constant", "projected_linear")
|
||||
|
||||
SHARED = ("complete_matrix", "no_extra_cases", "no_duplicate_cases", "finite_receipts",
|
||||
"same_source_and_harness", "clean_a135252_numerical_source",
|
||||
"same_recorded_runtime", "same_explicit_device", "same_early_sampling")
|
||||
PER_CASE = ("complete_times", "scales", "actual_builds", "reference", "runtime_source",
|
||||
"patch", "workload")
|
||||
|
||||
|
||||
METADATA_ULPS = 4
|
||||
|
||||
|
||||
def close_ulps(recorded, expected, ulps=METADATA_ULPS):
|
||||
"""Portable check for recomputed float metadata only (e.g. eps = |k|^2).
|
||||
|
||||
The recorded value comes from another runtime/libm, so exact equality can
|
||||
fail by one ULP (observed: 0.012047856934923534 Horsea vs ...532 here).
|
||||
A few ULPs is far below any meaningful change; hashes, case identity and
|
||||
scientific thresholds stay exact.
|
||||
"""
|
||||
return (isinstance(recorded, float) and math.isfinite(recorded)
|
||||
and abs(recorded - expected) <= ulps * math.ulp(max(abs(recorded), abs(expected))))
|
||||
|
||||
|
||||
def metadata_ok(record, n):
|
||||
return (record.get("flow") == "shear_wave" and record.get("mode") == list(sw.MODE)
|
||||
and record.get("polarization") == sw.E.tolist()
|
||||
and record.get("angle_deg") == sw.ANGLE_DEG and record.get("phase") == sw.PHI
|
||||
and close_ulps(record.get("eps"), sw.eps(n))
|
||||
and record.get("numerics") == tg.NUMERICS
|
||||
and record.get("initialization") == sw.INITIALIZATION
|
||||
and record.get("limits") == sw.LIMITS
|
||||
and record.get("early_pressure") == dict(enabled=False, rows=[],
|
||||
sampled_max_pressure_l2=None)
|
||||
and (record.get("source") or {}).get("shear_wave_sha256") == SHEAR_WAVE_SHA256)
|
||||
|
||||
|
||||
def cases_for(scaled):
|
||||
factor = 2 if scaled else 1
|
||||
return tuple((re, n * factor, depth, scheme) for re, n, depth, scheme in tg.CASES)
|
||||
|
||||
|
||||
def assess(records, expected_revision=None, *, scaled=False):
|
||||
if not isinstance(records, list) or not all(isinstance(r, dict) for r in records):
|
||||
raise ValueError("records must be a list of receipt objects")
|
||||
# The scaled TG assessor supplies N256/512 cases, builds and workload; its
|
||||
# TG-only diagnostic identity and early-sampling checks are not used here.
|
||||
base_checks = tg.assess(records, expected_revision,
|
||||
diagnostic=SCALED_DIAGNOSTIC if scaled else None)["checks"]
|
||||
checks = {name: base_checks.get(name, False) for name in SHARED}
|
||||
checks["shear_diagnostic_identity"] = all(
|
||||
r.get("diagnostic") == (SCALED_SHEAR if scaled else None) for r in records)
|
||||
cases = cases_for(scaled)
|
||||
indexed = {}
|
||||
for r in records:
|
||||
indexed.setdefault(tuple(r.get(k) for k in ("re_L", "n", "depth", "explosion")), r)
|
||||
for key in cases:
|
||||
_, n, depth, scheme = key
|
||||
label = f"{n}/{depth}/{scheme}"
|
||||
for name in PER_CASE:
|
||||
checks[f"{label} {name}"] = base_checks.get(f"{label} {name}", False)
|
||||
checks[f"{label} shear_metadata"] = key in indexed and metadata_ok(indexed[key], n)
|
||||
contract_valid = all(checks.values())
|
||||
result = dict(checks=checks, contract_valid=contract_valid,
|
||||
cases=[list(k) for k in cases], expected_revision=expected_revision,
|
||||
diagnostic=SCALED_SHEAR if scaled else None,
|
||||
scope="2D periodic Re100 oblique (2,1) shear wave; one phase and one "
|
||||
"rectangle; no placement robustness or convergence order")
|
||||
if not contract_valid:
|
||||
result.update(status="INVALID_SET", passed=False)
|
||||
return result
|
||||
try:
|
||||
physics = sw.assess(indexed[cases[0]], indexed[cases[1]],
|
||||
[indexed[cases[2]], indexed[cases[3]]])
|
||||
except (KeyError, ValueError) as error:
|
||||
result.update(status="INVALID_SET", passed=False, reason=str(error))
|
||||
return result
|
||||
result.update(physics=physics, status=physics["status"],
|
||||
passed=physics["status"] == "PASS")
|
||||
try:
|
||||
result["signed"] = signed_summary([indexed[k] for k in cases])
|
||||
except (KeyError, TypeError, ValueError) as error:
|
||||
result["signed_unavailable"] = str(error)
|
||||
return result
|
||||
|
||||
|
||||
CASE_AGNOSTIC = ("finite_receipts", "same_source_and_harness", "clean_a135252_numerical_source",
|
||||
"same_recorded_runtime", "same_explicit_device", "same_early_sampling")
|
||||
|
||||
|
||||
# Fixed multi-box presets sharing one contract; only geometry, controls, pins differ.
|
||||
PRESETS = {
|
||||
NESTED_SHEAR: dict(
|
||||
module=nested, physics=nested.assess, snapshot=True,
|
||||
pins={"shear_nested_sha256": SHEAR_NESTED_SHA256},
|
||||
finest_control="uniform 4N (U512)",
|
||||
scope="2D periodic Re100 oblique (2,1) shear wave; fixed three-level/two-region "
|
||||
"placement; changes placement and refined volume as well as depth, so no "
|
||||
"isolated-level cost or resolution trend"),
|
||||
TWIN_SHEAR: dict(
|
||||
module=twin, physics=sw.assess, snapshot=False,
|
||||
pins={"shear_nested_sha256": SHEAR_NESTED_SHA256,
|
||||
"shear_twin_sha256": SHEAR_TWIN_SHA256},
|
||||
finest_control="uniform 2N (U256)",
|
||||
scope="2D periodic Re100 oblique (2,1) shear wave; depth-2 twin of the nested "
|
||||
"preset (same two L0->L1 boxes, no L2). Twin vs nested is the effect of adding "
|
||||
"this nested refinement; it also changes finest spacing and refined volume, so it "
|
||||
"does not isolate abstract depth"),
|
||||
}
|
||||
|
||||
|
||||
def _nested_case_ok(r, n, depth, diagnostic=NESTED_SHEAR):
|
||||
"""Per-case identity of a fixed multi-box preset (scales, builds, patch, work, pins)."""
|
||||
preset = PRESETS[diagnostic]
|
||||
geometry = preset["module"]
|
||||
runtime = r.get("runtime_source", {})
|
||||
package = Path(runtime.get("checkout", "")) / "src/CelerisLab"
|
||||
cells = [n * n] if depth == 1 else geometry.LEAVES
|
||||
hops = [16 * n * 2**level for level in range(depth)]
|
||||
work = r.get("workload", {})
|
||||
snapshot = r.get("final_snapshot")
|
||||
expected_patch = [] if depth == 1 else geometry.REFINEMENTS
|
||||
return {
|
||||
"complete_times": r.get("completed") is True and tg.at_steps(
|
||||
r.get("rows", []), sample_steps(n)[0], n),
|
||||
"scales": (r.get("U") == sw.U and tg.finite(r.get("viscosity"))
|
||||
and abs(r["viscosity"] - sw.U * n / 100) < 1e-15
|
||||
and r.get("tau_by_level") == [0.5 + 3 * sw.U * n / 100 * 2**l
|
||||
for l in range(depth)]
|
||||
and r.get("mach") == sw.U * math.sqrt(3)),
|
||||
"actual_builds": tg.valid_builds(r, n, depth),
|
||||
"reference": (r.get("numerical_revision") == tg.NUMERICAL_REVISION
|
||||
and r.get("reference_harness") == tg.REFERENCE_HARNESS),
|
||||
"runtime_source": bool(runtime.get("checkout")) and all(
|
||||
runtime.get(k) == str(package / tail) for k, tail in
|
||||
(("module", "__init__.py"), ("compiler", "cuda/compiler_v2.py"),
|
||||
("kernel_root", "lbm/kernels"))),
|
||||
"patch": json.dumps(r.get("refinements")) == json.dumps(expected_patch),
|
||||
"workload": (work.get("active_cells_by_level") == cells and work.get("level_hops") == hops
|
||||
and work.get("root_intervals") == 16 * n
|
||||
and work.get("active_collision_updates_by_level") == [
|
||||
c * h for c, h in zip(cells, hops)]
|
||||
and work.get("active_collision_updates") == sum(
|
||||
c * h for c, h in zip(cells, hops))),
|
||||
"shear_metadata": metadata_ok(r, n),
|
||||
"nested_module_pin": all((r.get("source") or {}).get(key) == pin
|
||||
for key, pin in preset["pins"].items()),
|
||||
# Optional, nested projected depth-3 only; bounded and hashed when present.
|
||||
"snapshot": snapshot is None or (
|
||||
preset["snapshot"] and depth == nested.DEPTH and r.get("explosion") == "projected_linear"
|
||||
and snapshot.get("kept") is True and isinstance(snapshot.get("sha256"), str)
|
||||
and len(snapshot["sha256"]) == 64
|
||||
and 0 < snapshot.get("bytes", 0) <= nested.SNAPSHOT_MAX_BYTES
|
||||
and snapshot.get("leaf_counts") == nested.LEAVES
|
||||
and snapshot.get("step") == sample_steps(n)[0][-1]),
|
||||
}
|
||||
|
||||
|
||||
def assess_nested(records, expected_revision=None, diagnostic=NESTED_SHEAR):
|
||||
"""Fixed four-case preset contract: nested_shear_n128 (U128, U512, constant/projected
|
||||
R128 depth 3) or twin_shear_n128 (U128, U256, constant/projected R128 depth 2)."""
|
||||
preset = PRESETS[diagnostic]
|
||||
geometry = preset["module"]
|
||||
if not isinstance(records, list) or not all(isinstance(r, dict) for r in records):
|
||||
raise ValueError("records must be a list of receipt objects")
|
||||
base_checks = tg.assess(records, expected_revision)["checks"]
|
||||
checks = {name: base_checks.get(name, False) for name in CASE_AGNOSTIC}
|
||||
indexed, duplicates = {}, []
|
||||
for r in records:
|
||||
key = tuple(r.get(k) for k in ("re_L", "n", "depth", "explosion"))
|
||||
(duplicates.append(key) if key in indexed else indexed.__setitem__(key, r))
|
||||
checks.update(complete_matrix=all(k in indexed for k in geometry.CASES),
|
||||
no_extra_cases=set(indexed) <= set(geometry.CASES),
|
||||
no_duplicate_cases=not duplicates,
|
||||
shear_diagnostic_identity=all(r.get("diagnostic") == diagnostic
|
||||
for r in records))
|
||||
for key in geometry.CASES:
|
||||
_, n, depth, scheme = key
|
||||
label = f"{n}/{depth}/{scheme}"
|
||||
case = _nested_case_ok(indexed[key], n, depth, diagnostic) if key in indexed else {}
|
||||
for name in ("complete_times", "scales", "actual_builds", "reference", "runtime_source",
|
||||
"patch", "workload", "shear_metadata", "nested_module_pin", "snapshot"):
|
||||
checks[f"{label} {name}"] = bool(case.get(name, False))
|
||||
contract_valid = all(checks.values())
|
||||
result = dict(checks=checks, contract_valid=contract_valid, diagnostic=diagnostic,
|
||||
cases=[list(k) for k in geometry.CASES], expected_revision=expected_revision,
|
||||
finest_control=preset["finest_control"], scope=preset["scope"])
|
||||
if not contract_valid:
|
||||
result.update(status="INVALID_SET", passed=False)
|
||||
return result
|
||||
try:
|
||||
cases = geometry.CASES
|
||||
physics = preset["physics"](indexed[cases[0]], indexed[cases[1]],
|
||||
[indexed[cases[2]], indexed[cases[3]]])
|
||||
except (KeyError, ValueError) as error:
|
||||
result.update(status="INVALID_SET", passed=False, reason=str(error))
|
||||
return result
|
||||
result.update(physics=physics, status=physics["status"], passed=physics["status"] == "PASS")
|
||||
try:
|
||||
# Role "uniform_fine" is the preset's finest control (4N nested, 2N twin).
|
||||
result["signed"] = signed_summary([indexed[k] for k in geometry.CASES])
|
||||
except (KeyError, TypeError, ValueError) as error:
|
||||
result["signed_unavailable"] = str(error)
|
||||
return result
|
||||
|
||||
|
||||
def _signed(record):
|
||||
"""Raw signed quantities of one valid record (None when the fit is unavailable)."""
|
||||
s = sw.summarize(record)
|
||||
bias = None if not s["rate_fit_valid"] else s["rate"] / s["exact_rate"] - 1
|
||||
return dict(n=record["n"], depth=record["depth"], explosion=record["explosion"],
|
||||
rate_fit_valid=s["rate_fit_valid"], signed_rate_bias=bias,
|
||||
rate_tstar=s["rate_tstar"], exact_rate_tstar=s["exact_rate_tstar"],
|
||||
interval_rate_errors=s["interval_rate_errors"],
|
||||
residual_l2=s["residual_l2"], spurious_pressure_scaled=s["spurious_pressure_scaled"],
|
||||
spurious_pressure_rms=s["spurious_pressure_rms"],
|
||||
final_amplitude_error=record["rows"][-1]["amplitude_error"])
|
||||
|
||||
|
||||
def signed_summary(records):
|
||||
"""Signed bias per case and refined excess over each uniform control."""
|
||||
rows = dict(zip(ROLES, (_signed(r) for r in records)))
|
||||
|
||||
def minus(a, b):
|
||||
return None if a is None or b is None else a - b
|
||||
|
||||
for role in ("constant", "projected_linear"):
|
||||
for control in ("uniform_root", "uniform_fine"):
|
||||
rows[role][f"excess_over_{control}"] = {
|
||||
key: minus(rows[role][key], rows[control][key])
|
||||
for key in ("signed_rate_bias", "residual_l2", "spurious_pressure_rms")}
|
||||
return rows
|
||||
|
||||
|
||||
def load_reference(paths):
|
||||
"""The four audited N128 receipts verbatim; never relabelled as fresh controls."""
|
||||
blobs = [Path(path).read_bytes() for path in paths]
|
||||
hashes = {hashlib.sha256(blob).hexdigest() for blob in blobs}
|
||||
if len(blobs) != 4 or hashes != set(REFERENCE_RECEIPTS.values()):
|
||||
raise ValueError("N128 receipt hashes differ from the audited de5b0cc evidence")
|
||||
return [json.loads(blob) for blob in blobs]
|
||||
|
||||
|
||||
def resolution_trend(new_result, reference, reference_revision=REFERENCE_REVISION):
|
||||
"""Descriptive N128 -> N256 signed changes; never an acceptance gate or an order."""
|
||||
old_result = assess(reference, reference_revision)
|
||||
if "signed" not in old_result or "signed" not in new_result:
|
||||
return dict(valid=False, reason="N128 reference or N256 set lacks a valid signed summary")
|
||||
out = {}
|
||||
for role in ROLES:
|
||||
old, new = old_result["signed"][role], new_result["signed"][role]
|
||||
out[role] = {}
|
||||
for key in ("signed_rate_bias", "residual_l2", "spurious_pressure_rms",
|
||||
*(f"excess_over_{c}" for c in ("uniform_root", "uniform_fine"))):
|
||||
if key not in old:
|
||||
continue
|
||||
pairs = old[key].items() if isinstance(old[key], dict) else [(None, old[key])]
|
||||
for sub, low in pairs:
|
||||
high = new[key][sub] if sub else new[key]
|
||||
ok = low is not None and high is not None
|
||||
out[role][key if sub is None else f"{key}.{sub}"] = dict(
|
||||
n128=low, n256=high, signed_change=high - low if ok else None,
|
||||
n256_to_n128=high / low if ok and low else None)
|
||||
return dict(valid=True, reference_revision=reference_revision,
|
||||
reference_receipt_sha256=REFERENCE_RECEIPTS,
|
||||
reference_status=old_result["status"], changes=out,
|
||||
scope="same-placement two-resolution trend; not a convergence order, "
|
||||
"placement robustness or redesign decision")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("files", type=Path, nargs=4)
|
||||
p.add_argument("--output", type=Path, required=True)
|
||||
p.add_argument("--expected-revision", required=True)
|
||||
p.add_argument("--diagnostic", choices=[SCALED_SHEAR, NESTED_SHEAR, TWIN_SHEAR])
|
||||
p.add_argument("--reference-files", type=Path, nargs=4)
|
||||
args = p.parse_args()
|
||||
if args.output.exists():
|
||||
p.error("Output already exists; choose a new path to retain prior evidence")
|
||||
if args.diagnostic in PRESETS:
|
||||
if args.reference_files:
|
||||
p.error(f"{args.diagnostic} takes no reference files (no resolution trend)")
|
||||
result = assess_nested([json.loads(path.read_text()) for path in args.files],
|
||||
args.expected_revision, args.diagnostic)
|
||||
text = json.dumps(result, indent=2, allow_nan=False) + "\n"
|
||||
args.output.write_text(text)
|
||||
print(text, end="")
|
||||
raise SystemExit(0 if result["passed"] else 1)
|
||||
if bool(args.diagnostic) != bool(args.reference_files):
|
||||
p.error("--diagnostic scaled_shear_n256 requires --reference-files (N128 receipts)")
|
||||
result = assess([json.loads(path.read_text()) for path in args.files], args.expected_revision,
|
||||
scaled=bool(args.diagnostic))
|
||||
if args.diagnostic:
|
||||
try:
|
||||
reference = load_reference(args.reference_files)
|
||||
except ValueError as exc:
|
||||
p.error(str(exc))
|
||||
result["resolution_trend"] = resolution_trend(result, reference)
|
||||
text = json.dumps(result, indent=2, allow_nan=False) + "\n"
|
||||
args.output.write_text(text)
|
||||
print(text, end="")
|
||||
raise SystemExit(0 if result["passed"] else 1)
|
||||
@@ -1,9 +1,11 @@
|
||||
"""One bounded GPU case. No production edits, plots, checkpoints or field dumps."""
|
||||
"""One bounded GPU case, with explicit opt-in experimental explosion."""
|
||||
|
||||
import argparse
|
||||
from contextlib import nullcontext
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -12,6 +14,74 @@ import time
|
||||
import numpy as np
|
||||
from oracle import U, TIMES, LIMITS, initial, metrics, rectangles
|
||||
|
||||
NUMERICAL_REVISION = "a135252ae78b47b57d7e77d19001700b3bedac8b"
|
||||
SOURCE_SHA256 = "a5dbfd804befce868067d79a23e5fd47dce7f505b3f9b1bb2145fb57cb5f2178"
|
||||
BUILD_CAPTURE_SHA256 = "3dbaa6547cb0a4cb1541054f3b64ad8b6e18cc70527ac6166e56b944e9bd464a"
|
||||
REFERENCE_HARNESS = "11901a7f77557a901e017a171eeae1758d584fa5"
|
||||
INITIALIZATION = "physical FP64 equilibrium G at actual cell centers; rho=1+3p"
|
||||
SHEAR_WAVE_SHA256 = "81d5c8ee60864372887cddf9239580c82fb6750d956038f1e3ee9de61aa4e877"
|
||||
SCALED_DIAGNOSTIC = "scaled_n256"
|
||||
SCALED_SHEAR = "scaled_shear_n256" # mutually exclusive with scaled_n256 (one flow each)
|
||||
NESTED_SHEAR = "nested_shear_n128" # fixed three-level/two-region shear preset
|
||||
SHEAR_NESTED_SHA256 = "fa6cfe87fb4df8c6d732c637c885276833c81973b950c7724d734f8fc350bcb0"
|
||||
TWIN_SHEAR = "twin_shear_n128" # depth-2 twin: nested L0->L1 boxes without L2
|
||||
SHEAR_TWIN_SHA256 = "1cbf37f18e89aea577581631c4565f51e68d22a3739b698816ef64acbf2b2bb4"
|
||||
WIDE_X_SHEAR = "wide_x_shear_n128" # one projected x-gap-width contrast
|
||||
SHEAR_WIDE_X_SHA256 = "13d16ba355117e2d074edccd9dc5d694c2fc13c673a0abdfca6401f6ed917733"
|
||||
AXIS_SQUARE = "axis_square_shear_n128" # short four-face orientation diagnostic
|
||||
SHEAR_AXIS_SQUARE_SHA256 = "0baffa6eb99c268ce998d90300c993d45bb3b7b27c524b636874d75808bd0a8c"
|
||||
CENTERED_SQUARE = "centered_face_square_n128" # private-build reconstruction comparison
|
||||
SHEAR_CENTERED_SHA256 = "78bd7360e266e5e28ef4f03496413375551fcdc10ceba61d43bd88c33b4a8f5c"
|
||||
POSTG_SHEAR = "postg_shear_n128" # initialization-only three-case counterfactual
|
||||
SHEAR_POSTG_SHA256 = "9c909fa013c092490ee21532e3125b2a78a4c5102c7a1bdc0e76986d3f0ef903"
|
||||
SHEAR_PROBES_SHA256 = "e2e6490d281cc275f448dc6d6ea7b9b4407cc74a51e9256b86c08c8626b89b76" # signed interface-probe mechanism diagnostic
|
||||
|
||||
|
||||
def flow_functions(flow="taylor_green"):
|
||||
"""(initial, metrics, limits, initialization); the default is the frozen TG oracle."""
|
||||
if flow == "taylor_green":
|
||||
return initial, metrics, LIMITS, INITIALIZATION
|
||||
if flow == "shear_wave":
|
||||
import shear_wave
|
||||
return shear_wave.initial, shear_wave.metrics, shear_wave.LIMITS, shear_wave.INITIALIZATION
|
||||
raise ValueError(f"unknown flow {flow!r}")
|
||||
|
||||
|
||||
def finite_row(row):
|
||||
"""Every scalar or list-valued diagnostic is finite (TG rows are all scalars)."""
|
||||
return all(np.isfinite(np.asarray(value, dtype=float)).all() for value in row.values())
|
||||
|
||||
|
||||
def sample(run, masks, step, n, viscosity, initial_mass, metrics, depth, label="sample"):
|
||||
arrays = (run.download(),) if depth == 1 else run.download()
|
||||
row = metrics(arrays, masks, step, n, viscosity, initial_mass)
|
||||
if not finite_row(row):
|
||||
raise FloatingPointError(f"nonfinite {label} diagnostics")
|
||||
return row
|
||||
|
||||
|
||||
def sample_steps(n, early_pressure=False):
|
||||
"""Root synchronization steps; early ladder matches the 707afe6 diagnostic."""
|
||||
main = [round(tstar * n / U) for tstar in TIMES]
|
||||
early = list(range(0, 4 * n + 1, n // 16)) if early_pressure else []
|
||||
return main, early
|
||||
|
||||
|
||||
def runtime_source(repo):
|
||||
"""Fail before CUDA if Python or the compiler snapshot comes from another tree."""
|
||||
import CelerisLab
|
||||
from CelerisLab.cuda import compiler_v2
|
||||
|
||||
paths = dict(module=Path(CelerisLab.__file__).resolve(),
|
||||
compiler=Path(compiler_v2.__file__).resolve(),
|
||||
kernel_root=Path(compiler_v2.kernel_path("blocks.cu")).resolve().parent)
|
||||
package = repo / "src/CelerisLab"
|
||||
expected = dict(module=package / "__init__.py", compiler=package / "cuda/compiler_v2.py",
|
||||
kernel_root=package / "lbm/kernels")
|
||||
if paths != expected:
|
||||
raise RuntimeError("Wrong imported source or CUDA source/include root")
|
||||
return dict(checkout=str(repo), **{key: str(path) for key, path in paths.items()})
|
||||
|
||||
|
||||
def provenance(repo):
|
||||
git = lambda *a: subprocess.check_output(
|
||||
@@ -34,6 +104,102 @@ def provenance(repo):
|
||||
)
|
||||
|
||||
|
||||
def verify_source(repo, revision, flow="taylor_green", probes=False):
|
||||
"""Exact clean harness commit, fixed production tree, and reviewed capture helper."""
|
||||
import oracle
|
||||
from tests.validation.multigrid import check_projected_pairs as gate
|
||||
|
||||
harness = repo / "tests/validation/taylor_green"
|
||||
if (Path(__file__).resolve() != harness / "run_case.py"
|
||||
or Path(oracle.__file__).resolve() != harness / "oracle.py"):
|
||||
raise RuntimeError("Wrong runner or oracle checkout")
|
||||
helper = repo / "tests/validation/multigrid/check_projected_pairs.py"
|
||||
if (Path(gate.__file__).resolve() != helper
|
||||
or hashlib.sha256(helper.read_bytes()).hexdigest() != BUILD_CAPTURE_SHA256):
|
||||
raise RuntimeError("Wrong build-capture helper")
|
||||
if gate.preflight(repo, revision, "candidate") != NUMERICAL_REVISION:
|
||||
raise RuntimeError("Wrong production revision")
|
||||
result = provenance(repo)
|
||||
if result["source_tree_sha256"] != SOURCE_SHA256:
|
||||
raise RuntimeError("Wrong numerical source fingerprint")
|
||||
result["build_capture_sha256"] = BUILD_CAPTURE_SHA256
|
||||
if probes:
|
||||
import shear_probes
|
||||
module = harness / "shear_probes.py"
|
||||
if (Path(shear_probes.__file__).resolve() != module
|
||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_PROBES_SHA256):
|
||||
raise RuntimeError("Wrong shear_probes module checkout or content")
|
||||
result["shear_probes_sha256"] = SHEAR_PROBES_SHA256
|
||||
if flow == "shear_centered":
|
||||
import shear_centered
|
||||
module = harness / "shear_centered.py"
|
||||
if (Path(shear_centered.__file__).resolve() != module
|
||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_CENTERED_SHA256):
|
||||
raise RuntimeError("Wrong shear_centered module checkout or content")
|
||||
result["shear_centered_sha256"] = SHEAR_CENTERED_SHA256
|
||||
if flow in ("shear_axis_square", "shear_centered"):
|
||||
import shear_axis_square
|
||||
module = harness / "shear_axis_square.py"
|
||||
if (Path(shear_axis_square.__file__).resolve() != module
|
||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_AXIS_SQUARE_SHA256):
|
||||
raise RuntimeError("Wrong shear_axis_square module checkout or content")
|
||||
result["shear_axis_square_sha256"] = SHEAR_AXIS_SQUARE_SHA256
|
||||
if flow == "shear_postg":
|
||||
import shear_postg
|
||||
module = harness / "shear_postg.py"
|
||||
if (Path(shear_postg.__file__).resolve() != module
|
||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_POSTG_SHA256):
|
||||
raise RuntimeError("Wrong shear_postg module checkout or content")
|
||||
result["shear_postg_sha256"] = SHEAR_POSTG_SHA256
|
||||
if flow == "shear_wide_x":
|
||||
import shear_wide_x
|
||||
module = harness / "shear_wide_x.py"
|
||||
if (Path(shear_wide_x.__file__).resolve() != module
|
||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_WIDE_X_SHA256):
|
||||
raise RuntimeError("Wrong shear_wide_x module checkout or content")
|
||||
result["shear_wide_x_sha256"] = SHEAR_WIDE_X_SHA256
|
||||
if flow == "shear_twin":
|
||||
import shear_twin
|
||||
module = harness / "shear_twin.py"
|
||||
if (Path(shear_twin.__file__).resolve() != module
|
||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_TWIN_SHA256):
|
||||
raise RuntimeError("Wrong shear_twin module checkout or content")
|
||||
result["shear_twin_sha256"] = SHEAR_TWIN_SHA256
|
||||
if flow in ("shear_nested", "shear_twin", "shear_wide_x", "shear_postg"):
|
||||
import shear_nested
|
||||
module = harness / "shear_nested.py"
|
||||
if (Path(shear_nested.__file__).resolve() != module
|
||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_NESTED_SHA256):
|
||||
raise RuntimeError("Wrong shear_nested module checkout or content")
|
||||
result["shear_nested_sha256"] = SHEAR_NESTED_SHA256
|
||||
if flow in ("shear_wave", "shear_nested", "shear_twin", "shear_wide_x", "shear_postg", "shear_axis_square", "shear_centered"):
|
||||
import shear_wave
|
||||
module = harness / "shear_wave.py"
|
||||
if (Path(shear_wave.__file__).resolve() != module
|
||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_WAVE_SHA256):
|
||||
raise RuntimeError("Wrong shear_wave module checkout or content")
|
||||
result["shear_wave_sha256"] = SHEAR_WAVE_SHA256
|
||||
return result
|
||||
|
||||
|
||||
def build_capture(report, save):
|
||||
from tests.validation.multigrid.check_projected_pairs import capture_builds
|
||||
return capture_builds(report, save)
|
||||
|
||||
|
||||
def runtime_identity(ctx):
|
||||
import pycuda
|
||||
import pycuda.driver as cuda
|
||||
|
||||
nvcc = shutil.which("nvcc") or "/usr/local/cuda/bin/nvcc"
|
||||
return dict(device=dict(name=ctx.device.name(), pci_bus_id=ctx.device.pci_bus_id(),
|
||||
capability=list(ctx.device.compute_capability()),
|
||||
driver=cuda.get_driver_version(), runtime=list(cuda.get_version())),
|
||||
nvcc_path=str(Path(nvcc).resolve()),
|
||||
nvcc_version=subprocess.check_output([nvcc, "--version"], text=True).strip(),
|
||||
python=sys.version, numpy=np.__version__, pycuda=pycuda.VERSION_TEXT)
|
||||
|
||||
|
||||
def config(n, re):
|
||||
from CelerisLab.config import LBMConfig
|
||||
|
||||
@@ -71,19 +237,123 @@ def workload(masks, base_steps):
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--repo", type=Path, required=True)
|
||||
p.add_argument("--expected-revision", required=True)
|
||||
p.add_argument("--run-gpu", action="store_true")
|
||||
p.add_argument("--dry-run", action="store_true")
|
||||
p.add_argument("--re", type=int, choices=[100, 1000], required=True)
|
||||
p.add_argument("--n", type=int, choices=[32, 64, 128, 256], required=True)
|
||||
p.add_argument("--n", type=int, choices=[32, 64, 128, 256, 512], required=True)
|
||||
p.add_argument("--depth", type=int, choices=[1, 2, 3], required=True)
|
||||
p.add_argument("--output", type=Path, required=True)
|
||||
p.add_argument("--explosion", choices=["constant", "projected_linear"], default="constant")
|
||||
p.add_argument("--diagnostic",
|
||||
choices=[SCALED_DIAGNOSTIC, SCALED_SHEAR, NESTED_SHEAR, TWIN_SHEAR,
|
||||
WIDE_X_SHEAR, POSTG_SHEAR, AXIS_SQUARE, CENTERED_SQUARE],
|
||||
help="explicit four-case N256/fine512 same-placement diagnostic "
|
||||
"(scaled_n256: Taylor-Green; scaled_shear_n256: shear wave) or the "
|
||||
"fixed three-level/two-region shear preset (nested_shear_n128) "
|
||||
"or its depth-2 twin (twin_shear_n128), or the single projected "
|
||||
"x-gap-width contrast (wide_x_shear_n128), or three-case POST-G "
|
||||
"initialization counterfactual (postg_shear_n128), or short square "
|
||||
"orientation diagnostic (axis_square_shear_n128)")
|
||||
p.add_argument("--test-reconstruction", choices=["current", "centered_face_v1"],
|
||||
help="required only for centered_face_square_n128; private test build")
|
||||
p.add_argument("--probe-trace", action="store_true",
|
||||
help="signed interface-probe trace (nested U128/U512/projected d3, "
|
||||
"twin projected d2, wide-x projected d3, POST-G three-case set, "
|
||||
"axis-square four-case set); "
|
||||
"mechanism diagnostic, no gates")
|
||||
p.add_argument("--final-snapshot", action="store_true",
|
||||
help="nested_shear_n128 projected depth-3 only: final macro snapshot")
|
||||
p.add_argument("--early-pressure", action="store_true",
|
||||
help="also save 65 scalar pressure samples at t*=k*.001875, k=0..64")
|
||||
p.add_argument("--flow", choices=["taylor_green", "shear_wave"], default="taylor_green")
|
||||
args = p.parse_args()
|
||||
if not os.environ.get("CUDA_VISIBLE_DEVICES"):
|
||||
p.error("Explicit physical CUDA_VISIBLE_DEVICES selection required")
|
||||
centered = args.diagnostic == CENTERED_SQUARE
|
||||
if centered != (args.test_reconstruction is not None):
|
||||
p.error("--test-reconstruction is required only for centered_face_square_n128")
|
||||
if centered:
|
||||
import shear_centered
|
||||
if (args.n, args.depth, args.test_reconstruction) not in shear_centered.CASES:
|
||||
p.error("centered comparison requires one of its four fixed cases")
|
||||
if args.explosion != ("constant" if args.depth == 1 else "projected_linear"):
|
||||
p.error("centered comparison uses constant uniforms and projected refined cases")
|
||||
axis_square = args.diagnostic in (AXIS_SQUARE, CENTERED_SQUARE)
|
||||
nested = args.diagnostic == NESTED_SHEAR
|
||||
wide_x = args.diagnostic == WIDE_X_SHEAR
|
||||
postg = args.diagnostic == POSTG_SHEAR
|
||||
if (wide_x or postg or axis_square) and not args.probe_trace:
|
||||
p.error(f"{args.diagnostic} requires --probe-trace")
|
||||
if args.flow == "shear_wave" and (args.re != 100 or (args.depth == 3 and not (nested or wide_x or postg))
|
||||
or args.early_pressure):
|
||||
p.error("shear_wave requires --re 100, depth 1 or 2 and no --early-pressure")
|
||||
probe_cases = {(NESTED_SHEAR, 128, 1, "constant"), (NESTED_SHEAR, 512, 1, "constant"),
|
||||
(NESTED_SHEAR, 128, 3, "projected_linear"),
|
||||
(TWIN_SHEAR, 128, 2, "projected_linear"),
|
||||
(WIDE_X_SHEAR, 128, 3, "projected_linear"),
|
||||
(POSTG_SHEAR, 128, 1, "constant"), (POSTG_SHEAR, 512, 1, "constant"),
|
||||
(POSTG_SHEAR, 128, 3, "projected_linear")}
|
||||
if axis_square:
|
||||
import shear_axis_square
|
||||
probe_cases.update((args.diagnostic, n, depth, scheme)
|
||||
for _, n, depth, scheme in shear_axis_square.CASES)
|
||||
if args.probe_trace and (args.final_snapshot or (
|
||||
args.diagnostic, args.n, args.depth, args.explosion) not in probe_cases):
|
||||
p.error("--probe-trace requires a fixed nested, twin, wide-x, POST-G or axis-square case, "
|
||||
"without --final-snapshot")
|
||||
if args.final_snapshot and not (nested and args.depth == 3
|
||||
and args.explosion == "projected_linear"):
|
||||
p.error("--final-snapshot requires nested_shear_n128 projected depth 3")
|
||||
initial, metrics, limits, initialization = flow_functions(args.flow)
|
||||
if args.output.exists():
|
||||
p.error("Output already exists; choose a new path to retain prior evidence")
|
||||
if args.explosion != "constant" and args.depth != 2 and not (nested or wide_x or postg):
|
||||
p.error("projected_linear requires depth 2")
|
||||
twin = args.diagnostic == TWIN_SHEAR
|
||||
if axis_square:
|
||||
preset = shear_axis_square
|
||||
if args.flow != "shear_wave" or (args.re, args.n, args.depth, args.explosion) not in preset.CASES:
|
||||
p.error("axis_square_shear_n128 requires one of its four Re100 shear cases")
|
||||
initial, metrics, limits, initialization = preset.initial, preset.metrics, {}, preset.INITIALIZATION
|
||||
elif nested or twin or wide_x or postg:
|
||||
import shear_nested
|
||||
import shear_twin
|
||||
import shear_wide_x
|
||||
preset = shear_wide_x if wide_x else shear_nested if nested else shear_twin
|
||||
if postg:
|
||||
import shear_postg
|
||||
preset = shear_postg
|
||||
initial, initialization = preset.initial, preset.INITIALIZATION
|
||||
if args.flow != "shear_wave":
|
||||
p.error(f"--diagnostic {args.diagnostic} requires --flow shear_wave")
|
||||
if (args.re, args.n, args.depth, args.explosion) not in preset.CASES:
|
||||
p.error(f"{args.diagnostic} requires one of its fixed Re100 cases")
|
||||
elif args.diagnostic:
|
||||
cases = {(256, 1, "constant"), (512, 1, "constant"),
|
||||
(256, 2, "constant"), (256, 2, "projected_linear")}
|
||||
flow = "shear_wave" if args.diagnostic == SCALED_SHEAR else "taylor_green"
|
||||
if args.flow != flow:
|
||||
p.error(f"--diagnostic {args.diagnostic} requires --flow {flow}")
|
||||
if args.diagnostic == SCALED_DIAGNOSTIC and (
|
||||
args.re != 100 or (args.n, args.depth, args.explosion) not in cases
|
||||
or not args.early_pressure):
|
||||
p.error("scaled_n256 requires one of its four Re100 cases and --early-pressure")
|
||||
if args.diagnostic == SCALED_SHEAR and (args.n, args.depth, args.explosion) not in cases:
|
||||
p.error("scaled_shear_n256 requires one of its four Re100 cases")
|
||||
elif args.n == 512:
|
||||
p.error("N512 requires --diagnostic scaled_n256, scaled_shear_n256 or nested_shear_n128")
|
||||
selected = os.environ.get("CUDA_VISIBLE_DEVICES", "")
|
||||
if not args.dry_run and (not args.run_gpu or not selected or "," in selected or selected == "-1"):
|
||||
p.error("--run-gpu and exactly one explicit CUDA_VISIBLE_DEVICES are required")
|
||||
repo = args.repo.resolve()
|
||||
sys.path.insert(0, str(repo / "src"))
|
||||
import CelerisLab
|
||||
|
||||
if Path(CelerisLab.__file__).resolve() != repo / "src/CelerisLab/__init__.py":
|
||||
raise RuntimeError("Wrong imported source checkout")
|
||||
sys.path[:0] = [str(repo / "src"), str(repo)]
|
||||
source_flow = ("shear_centered" if centered else "shear_axis_square" if axis_square else "shear_postg" if postg else "shear_wide_x" if wide_x else "shear_nested" if nested
|
||||
else "shear_twin" if twin else args.flow)
|
||||
source = verify_source(repo, args.expected_revision, source_flow, probes=args.probe_trace)
|
||||
runtime = runtime_source(repo)
|
||||
if args.dry_run:
|
||||
print(json.dumps(dict(source=source, runtime_source=runtime, dry_run=True,
|
||||
**({"diagnostic": args.diagnostic} if args.diagnostic else {}))))
|
||||
return 0
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
from CelerisLab.lbm.block_stepper import PeriodicBlockStepper
|
||||
@@ -92,13 +362,28 @@ def main():
|
||||
from CelerisLab.lbm.hierarchy import build_hierarchy, leaf_partition
|
||||
|
||||
cfg = config(args.n, args.re)
|
||||
refinements = [] if args.depth == 1 else rectangles(args.n, args.depth)
|
||||
if args.depth == 1:
|
||||
refinements = []
|
||||
elif nested or twin or wide_x or postg or axis_square:
|
||||
refinements = [list(level) for level in preset.REFINEMENTS]
|
||||
else:
|
||||
refinements = rectangles(args.n, args.depth)
|
||||
h = build_hierarchy(cfg, refinements)
|
||||
np.testing.assert_array_equal(leaf_partition(h), np.ones((args.n, args.n)))
|
||||
masks = [k == ACTIVE for k in h.kinds]
|
||||
main_steps, early_steps = sample_steps(args.n, args.early_pressure)
|
||||
if axis_square:
|
||||
main_steps = [0, preset.case_steps(args.n)[-1]]
|
||||
report = dict(
|
||||
schema=1,
|
||||
source=provenance(repo),
|
||||
source=source,
|
||||
numerical_revision=NUMERICAL_REVISION,
|
||||
reference_harness=REFERENCE_HARNESS,
|
||||
actual_build_sidecars=[],
|
||||
runtime_identity=None,
|
||||
runtime_source=runtime,
|
||||
historical_baseline_revision="9805383f9c50d867e8555471baf5dbda8333a652",
|
||||
explosion=args.explosion,
|
||||
n=args.n,
|
||||
re_L=args.re,
|
||||
depth=args.depth,
|
||||
@@ -107,32 +392,67 @@ def main():
|
||||
mach=U * np.sqrt(3),
|
||||
viscosity=cfg.viscosity,
|
||||
tau_by_level=[0.5 + 3 * c.viscosity for c in h.configs],
|
||||
initialization="physical FP64 equilibrium G at actual cell centers; rho=1+3p",
|
||||
cuda_visible_devices=os.environ["CUDA_VISIBLE_DEVICES"],
|
||||
initialization=initialization,
|
||||
numerics=dict(lattice_model=cfg.lattice_model, collision=cfg.collision,
|
||||
store_precision=cfg.store_precision, ddf_shifting=cfg.ddf_shifting,
|
||||
streaming=cfg.streaming, les_enabled=cfg.les_enabled,
|
||||
boundary="periodic", block_shape=[8, 8],
|
||||
sample_phase="postcollision G at root synchronization"),
|
||||
cuda_visible_devices=selected,
|
||||
device_id=0,
|
||||
limits=LIMITS,
|
||||
workload=workload(masks, round(TIMES[-1] * args.n / U)),
|
||||
limits=limits,
|
||||
workload=workload(masks, main_steps[-1]),
|
||||
rows=[],
|
||||
early_pressure=dict(enabled=args.early_pressure, rows=[],
|
||||
sampled_max_pressure_l2=None),
|
||||
completed=False,
|
||||
)
|
||||
if args.flow != "taylor_green":
|
||||
import shear_wave
|
||||
wave = preset if axis_square else shear_wave
|
||||
report.update(flow=args.flow, mode=list(wave.MODE),
|
||||
polarization=wave.E.tolist(), angle_deg=wave.ANGLE_DEG,
|
||||
phase=wave.PHI, eps=wave.eps(args.n))
|
||||
if args.diagnostic:
|
||||
report["diagnostic"] = args.diagnostic
|
||||
if postg:
|
||||
report.update(initialization_id=preset.INITIALIZATION_ID, qualification_eligible=False,
|
||||
screening_scope="descriptive scalar screens only; no qualification assessor")
|
||||
if axis_square:
|
||||
report.update(initialization_id=preset.INITIALIZATION_ID, qualification_eligible=False,
|
||||
reference_harness=None, historical_baseline_revision=None,
|
||||
screening_scope="no numerical gates; short orientation diagnostic")
|
||||
if centered:
|
||||
report.update(test_reconstruction=args.test_reconstruction, observed_shifted_max=0.,
|
||||
reconstruction=dict(variant="current"),
|
||||
screening_scope="predeclared centered-witness diagnostic; separate four-case comparison")
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
save = lambda: args.output.write_text(
|
||||
json.dumps(report, indent=2, allow_nan=False) + "\n"
|
||||
)
|
||||
def save():
|
||||
temporary = args.output.with_suffix(args.output.suffix + ".tmp")
|
||||
text = json.dumps(report, indent=2, allow_nan=False) + "\n"
|
||||
if len(text.encode()) > 1024 * 1024:
|
||||
raise RuntimeError("Scalar receipt exceeds 1 MiB cap")
|
||||
temporary.write_text(text)
|
||||
temporary.replace(args.output)
|
||||
|
||||
save()
|
||||
started = time.monotonic()
|
||||
with CudaContext(0) as ctx:
|
||||
if args.depth == 1:
|
||||
layout = BlockLayout2D.build(args.n, args.n, 8, 8, periodic=(True, True))
|
||||
run = PeriodicBlockStepper(cfg, layout, context=ctx)
|
||||
else:
|
||||
run = FixedPatchStepper(
|
||||
cfg,
|
||||
context=ctx,
|
||||
coarse_boundary="periodic",
|
||||
refinements=refinements,
|
||||
level_blocks=[(8, 8)] * args.depth,
|
||||
)
|
||||
with CudaContext(0) as ctx, build_capture(report, save):
|
||||
report["runtime_identity"] = runtime_identity(ctx)
|
||||
save()
|
||||
with (shear_centered.private_build(h, report, args.test_reconstruction) if centered else nullcontext()):
|
||||
if args.depth == 1:
|
||||
layout = BlockLayout2D.build(args.n, args.n, 8, 8, periodic=(True, True))
|
||||
run = PeriodicBlockStepper(cfg, layout, context=ctx)
|
||||
else:
|
||||
run = FixedPatchStepper(
|
||||
cfg,
|
||||
context=ctx,
|
||||
coarse_boundary="periodic",
|
||||
refinements=refinements,
|
||||
level_blocks=[(8, 8)] * args.depth,
|
||||
explosion=args.explosion,
|
||||
)
|
||||
with run:
|
||||
run.initialize(
|
||||
*[
|
||||
@@ -141,24 +461,61 @@ def main():
|
||||
]
|
||||
)
|
||||
initial_mass = None
|
||||
for tstar in TIMES:
|
||||
step = round(tstar * args.n / U)
|
||||
probe_steps = set()
|
||||
if args.probe_trace:
|
||||
import shear_probes
|
||||
if axis_square:
|
||||
shear_probes = preset
|
||||
probe_root = dict(zip(shear_probes.case_steps(args.n), shear_probes.STEPS))
|
||||
probe_steps = set(probe_root)
|
||||
probes = shear_probes.select(masks, args.n)
|
||||
report["probe_trace"] = shear_probes.new_trace(
|
||||
probes, cfg.viscosity, [args.diagnostic, args.n, args.depth, args.explosion])
|
||||
for step in sorted(set(main_steps + early_steps) | probe_steps):
|
||||
run.step(step - run.step_count)
|
||||
arrays = (run.download(),) if args.depth == 1 else run.download()
|
||||
row = metrics(arrays, masks, step, args.n, cfg.viscosity, initial_mass)
|
||||
if step in probe_steps:
|
||||
trace = report["probe_trace"]
|
||||
if "error" not in trace:
|
||||
arrays = (run.download(),) if args.depth == 1 else run.download()
|
||||
try:
|
||||
if centered:
|
||||
report["observed_shifted_max"] = max(report["observed_shifted_max"], shear_centered.observe(arrays))
|
||||
values = shear_probes.sample(arrays, probes)
|
||||
shear_probes.validate_row(values, len(probes))
|
||||
except (ValueError, ZeroDivisionError) as error:
|
||||
report["probe_trace"] = dict(error=str(error))
|
||||
else:
|
||||
trace["steps"].append(probe_root[step]) # root-physical step
|
||||
trace["values"].append(values)
|
||||
if step not in main_steps and step not in early_steps:
|
||||
continue # probe-only step: no qualification metrics
|
||||
row = sample(run, masks, step, args.n, cfg.viscosity, initial_mass,
|
||||
metrics, args.depth)
|
||||
row["step"] = step
|
||||
if initial_mass is None:
|
||||
initial_mass = row["mass"]
|
||||
report["rows"].append(row)
|
||||
if step in main_steps:
|
||||
report["rows"].append(row)
|
||||
if args.final_snapshot and step == main_steps[-1]:
|
||||
# Optional macro-only snapshot; scalar receipt stays authoritative.
|
||||
report["final_snapshot"] = shear_nested.write_snapshot(
|
||||
args.output.with_name(args.output.stem + ".final_macro.npz"),
|
||||
run.download(), masks,
|
||||
shear_nested.snapshot_meta(step, cfg.viscosity, args.explosion,
|
||||
source.get("revision")))
|
||||
if step in early_steps:
|
||||
trace = report["early_pressure"]
|
||||
trace["rows"].append({key: row[key] for key in ("step", "tstar", "pressure_l2")})
|
||||
trace["sampled_max_pressure_l2"] = max(r["pressure_l2"] for r in trace["rows"])
|
||||
save()
|
||||
print(
|
||||
json.dumps(
|
||||
dict(
|
||||
event="PROGRESS",
|
||||
event="PROGRESS" if step in main_steps else "EARLY_PRESSURE",
|
||||
n=args.n,
|
||||
re_L=args.re,
|
||||
depth=args.depth,
|
||||
**row,
|
||||
**(row if step in main_steps else report["early_pressure"]["rows"][-1]),
|
||||
)
|
||||
),
|
||||
flush=True,
|
||||
@@ -167,8 +524,8 @@ def main():
|
||||
# Exercise real kernels/halos/transfers on every level before READY.
|
||||
# This is the first measured trajectory step, not discarded warmup.
|
||||
run.step(1)
|
||||
probe = (run.download(),) if args.depth == 1 else run.download()
|
||||
metrics(probe, masks, 1, args.n, cfg.viscosity, initial_mass)
|
||||
sample(run, masks, 1, args.n, cfg.viscosity, initial_mass,
|
||||
metrics, args.depth, "READY")
|
||||
report["ready_after_step"] = 1
|
||||
save()
|
||||
print(
|
||||
@@ -184,11 +541,16 @@ def main():
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
if args.probe_trace and "error" not in report["probe_trace"]:
|
||||
try:
|
||||
report["probe_trace"]["bytes"] = shear_probes.validate(report["probe_trace"])
|
||||
except ValueError as error: # keep the scalar receipt; drop an invalid trace
|
||||
report["probe_trace"] = dict(error=str(error))
|
||||
report.update(
|
||||
completed=True,
|
||||
seconds=time.monotonic() - started,
|
||||
screening_pass=all(
|
||||
row[key] <= limit for row in report["rows"] for key, limit in LIMITS.items()
|
||||
screening_pass=None if axis_square else all(
|
||||
row[key] <= limit for row in report["rows"] for key, limit in limits.items()
|
||||
),
|
||||
)
|
||||
save()
|
||||
@@ -198,6 +560,8 @@ def main():
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
if axis_square:
|
||||
return int("error" in report["probe_trace"])
|
||||
return 0 if report["screening_pass"] else 1
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Short axis-shear orientation diagnostic; no physical acceptance gates."""
|
||||
|
||||
import json
|
||||
import numpy as np
|
||||
|
||||
import shear_wave as sw
|
||||
from shear_probes import sample, validate_row, TRACE_MAX_BYTES
|
||||
from oracle import CX, CY, W, U, centers
|
||||
|
||||
ROOT_N = 128
|
||||
MODE, E, PHI, ANGLE_DEG = (2, 0), np.array([0., 1.]), np.pi / 4, 0.
|
||||
REFINEMENTS = [[(32, 32, 96, 96)]]
|
||||
CASES = ((100, 128, 1, "constant"), (100, 256, 1, "constant"),
|
||||
(100, 128, 2, "constant"), (100, 128, 2, "projected_linear"))
|
||||
STEPS = tuple(range(9))
|
||||
INITIALIZATION_ID = "postg_first_gradient_v1"
|
||||
INITIALIZATION = "physical FP64 axis-shear POST G=feq-(tau-1)A; first-gradient low-Mach CE"
|
||||
GROUPS = dict(inner_left=(32, 64), inner_right=(95, 63),
|
||||
inner_bottom=(64, 32), inner_top=(63, 95),
|
||||
outer_left=(31, 64), outer_right=(96, 63),
|
||||
outer_bottom=(64, 31), outer_top=(63, 96))
|
||||
|
||||
|
||||
def eps(n):
|
||||
return (4 * np.pi / n)**2
|
||||
|
||||
|
||||
def exact(x, t, length, viscosity):
|
||||
"""Transverse velocity and physical x derivative in the supplied length units."""
|
||||
k = 4 * np.pi / length
|
||||
amplitude = U * np.exp(-viscosity * k * k * t)
|
||||
return amplitude * np.sin(k * x + PHI), amplitude * k * np.cos(k * x + PHI)
|
||||
|
||||
|
||||
def initial(shape, level, length, viscosity):
|
||||
x, _ = centers(shape, level)
|
||||
uy, gradient = exact(x, 0, length, viscosity)
|
||||
tau = .5 + 3 * viscosity * 2**level
|
||||
a = (3 * W * CX * CY)[:, None, None] * gradient * 2.**-level
|
||||
return sw.equilibrium(np.ones_like(uy), np.zeros_like(uy), uy) - (tau - 1) * a
|
||||
|
||||
|
||||
def metrics(arrays, masks, t, length, viscosity, initial_mass=None):
|
||||
rho, ux, uy, x, _, w, _ = sw._leaves(arrays, masks)
|
||||
if not np.isclose(w.sum(), length**2, rtol=0, atol=1e-10):
|
||||
raise ValueError("leaf partition does not cover domain once")
|
||||
ey, _ = exact(x, t, length, viscosity)
|
||||
p = (rho - 1) / 3
|
||||
p -= w @ p / w.sum() # one domain gauge, never the probe mean
|
||||
mass = float(w @ rho)
|
||||
return dict(tstar=t * U / length, mass=mass,
|
||||
mass_relative=0. if initial_mass is None else abs(mass-initial_mass)/initial_mass,
|
||||
velocity_l2=float(np.sqrt(w @ (ux**2 + (uy-ey)**2) / (w @ ey**2))),
|
||||
spurious_pressure_rms=float(np.sqrt(w @ p**2 / w.sum())),
|
||||
rho_min=float(rho.min()), rho_max=float(rho.max()),
|
||||
max_speed=float(np.hypot(ux, uy).max()), leaf_cells=len(rho))
|
||||
|
||||
|
||||
def case_steps(n):
|
||||
return [r * (n // ROOT_N) for r in STEPS]
|
||||
|
||||
|
||||
def select(masks, n=ROOT_N):
|
||||
"""Exact native cells filling eight unit-area groups; no interpolated samples."""
|
||||
if (n, len(masks)) not in ((128, 1), (256, 1), (128, 2)):
|
||||
raise ValueError("unsupported square probe layout")
|
||||
probes = []
|
||||
for name, (x, y) in GROUPS.items():
|
||||
level = int(len(masks) == 2 and name.startswith("inner"))
|
||||
factor = (n // ROOT_N) * 2**level
|
||||
for dy in range(factor):
|
||||
for dx in range(factor):
|
||||
i, j = factor*x + dx, factor*y + dy
|
||||
if not masks[level][j, i]:
|
||||
raise ValueError("square probe is not ACTIVE")
|
||||
probes.append(dict(group=name, level=level, i=i, j=j,
|
||||
center=[(i+.5)/factor, (j+.5)/factor],
|
||||
width=1/factor, area=1/factor**2, parity=[i % 2, j % 2]))
|
||||
if len({(p["level"], p["i"], p["j"]) for p in probes}) != len(probes):
|
||||
raise ValueError("duplicate square probes")
|
||||
return probes
|
||||
|
||||
|
||||
def new_trace(probes, case_viscosity, case):
|
||||
return dict(schema=1, kind="axis-square orientation diagnostic; no pass gates", case=case,
|
||||
time_unit="root step (N=128)", sample_phase="postcollision G at root synchronization",
|
||||
root_n=ROOT_N, viscosity_root=U*ROOT_N/100, case_viscosity=case_viscosity,
|
||||
mode=list(MODE), phase=PHI, amplitude=U, fields=["rho", "ux", "uy"],
|
||||
probes=probes, steps=[], values=[])
|
||||
|
||||
|
||||
def group_moments(trace):
|
||||
"""Signed unit-area means; normalize by each group's mean actual-center derivative.
|
||||
|
||||
Momentum is rho*u. Raw density is ungauged. Per-cell parity stays in the trace.
|
||||
U128 samples one center, whereas U256/refined-inner use four different centers.
|
||||
"""
|
||||
out = []
|
||||
for step, row in zip(trace["steps"], trace["values"]):
|
||||
values, groups = np.asarray(row).reshape(-1, 3), {}
|
||||
for name in GROUPS:
|
||||
indices = [i for i, p in enumerate(trace["probes"]) if p["group"] == name]
|
||||
probes = [trace["probes"][i] for i in indices]
|
||||
weights = np.array([p["area"] for p in probes])
|
||||
if weights.sum() != 1:
|
||||
raise ValueError("probe group does not have unit area")
|
||||
rho, ux, uy = values[indices].T
|
||||
ey, gradient = exact(np.array([p["center"][0] for p in probes]), step,
|
||||
ROOT_N, trace["viscosity_root"])
|
||||
drho, djx, djy = (float(weights @ v) for v in (rho-1, rho*ux, rho*uy-ey))
|
||||
derivative = float(weights @ gradient)
|
||||
groups[name] = dict(drho=drho, djx=djx, djy=djy, mean_duy_dx=derivative,
|
||||
djx_over_duy_dx=djx/derivative, djy_over_duy_dx=djy/derivative)
|
||||
out.append(dict(root_step=step, groups=groups))
|
||||
return out
|
||||
|
||||
|
||||
def validate(trace):
|
||||
if trace["steps"] != list(STEPS) or len(trace["values"]) != len(STEPS):
|
||||
raise ValueError("square trace schedule mismatch")
|
||||
_, n, depth, scheme = trace["case"]
|
||||
if (100, n, depth, scheme) not in CASES or len(trace["probes"]) != (
|
||||
20 if depth == 2 else 8*(n//ROOT_N)**2):
|
||||
raise ValueError("square trace case/count mismatch")
|
||||
for row in trace["values"]:
|
||||
validate_row(row, len(trace["probes"]))
|
||||
trace["group_moments"] = group_moments(trace)
|
||||
size = 0
|
||||
while True:
|
||||
final = len(json.dumps(dict(trace, bytes=size), allow_nan=False).encode())
|
||||
if final == size:
|
||||
break
|
||||
size = final
|
||||
if size > TRACE_MAX_BYTES:
|
||||
raise ValueError("square trace exceeds byte cap")
|
||||
return size
|
||||
@@ -0,0 +1,229 @@
|
||||
"""Test-only straight-face centered witness; private CUDA snapshot, frozen source intact."""
|
||||
from contextlib import contextmanager
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
import shear_axis_square as axis
|
||||
from oracle import CX, CY, W
|
||||
|
||||
DIAGNOSTIC = "centered_face_square_n128"
|
||||
VARIANT = "centered_face_v1"
|
||||
BUILD_SHA256 = "81e880017495190209bd867d4b523a2c243be496ed4681892e4cd789b7ba1f21"
|
||||
CASES = ((128, 1, "current"), (256, 1, "current"),
|
||||
(128, 2, "current"), (128, 2, VARIANT))
|
||||
# Local arithmetic-derived diagnostic deadbands, NOT accumulated trajectory bounds.
|
||||
POP_ENVELOPE, OP_ATOL, OP_RTOL = 1/32, 6e-8, 2e-7
|
||||
MARGIN = 2 * np.array([9, 6, 6]) * (OP_ATOL + OP_RTOL * POP_ENVELOPE)
|
||||
|
||||
|
||||
def routing(hierarchy):
|
||||
"""Square-only mask: full donor quartets, one missing normal axis, legal tangent."""
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
from CelerisLab.lbm.refinement_tables import projected_explosion_tables
|
||||
if hierarchy.depth != 2 or hierarchy.kinds[0].shape != (128, 128):
|
||||
raise ValueError("centered witness requires the fixed two-level square")
|
||||
pair = hierarchy.pairs[0]
|
||||
covered = np.zeros((128, 128), bool); covered[32:96, 32:96] = True
|
||||
if pair.fine_origin != (0., 0.) or not np.array_equal(pair.covered, covered):
|
||||
raise ValueError("wrong centered-witness geometry")
|
||||
layout = BlockLayout2D.build(128, 128, 8, 8, periodic=(True, True))
|
||||
neighbors, offsets = projected_explosion_tables(pair, layout, hierarchy.kinds[0])
|
||||
donors = layout.owner.ravel()[pair.ghost_coarse]
|
||||
off = neighbors.reshape(-1, 2, 2) == donors[:, None, None]
|
||||
disabled = off.all(axis=2)
|
||||
y, x = np.divmod(pair.ghost_coarse, 128)
|
||||
vertical = np.isin(x, [30, 31, 96, 97]) & (y >= 36) & (y < 92)
|
||||
horizontal = np.isin(y, [30, 31, 96, 97]) & (x >= 36) & (x < 92)
|
||||
mask = ((vertical & disabled[:, 0] & ~off[:, 1].any(axis=1)) |
|
||||
(horizontal & disabled[:, 1] & ~off[:, 0].any(axis=1))).astype(np.uint8)
|
||||
for donor in np.unique(pair.ghost_coarse):
|
||||
rows = pair.ghost_coarse == donor
|
||||
if rows.sum() != 4 or len(set(mask[rows])) != 1 or sorted(map(tuple, offsets[rows])) != [(-.25,-.25), (-.25,.25), (.25,-.25), (.25,.25)]:
|
||||
raise ValueError("routing must preserve complete +/- quarter donor quartets")
|
||||
return mask
|
||||
|
||||
|
||||
def metadata(mask):
|
||||
return dict(variant=VARIANT, mask_sha256=hashlib.sha256(mask.tobytes()).hexdigest(),
|
||||
ghost_rows=len(mask), selected_rows=int(mask.sum()),
|
||||
rule="diagonal projected correction x2; audited straight faces only")
|
||||
|
||||
|
||||
def patch_source(source, mask):
|
||||
"""Change only the projected function in the private generated build directory."""
|
||||
anchor = 'extern "C" __global__ void BlockExplodeProjected('
|
||||
position = " to[index_f(t % TOTAL_CELLS, q)] ="
|
||||
before, kernel = source.split(anchor)
|
||||
body, after = kernel.split("// Targets are receiver-local SOURCE slots", 1)
|
||||
if body.count(position) != 1:
|
||||
raise ValueError("unexpected projected kernel source")
|
||||
flag = "__device__ __constant__ unsigned char centered_face_mask[] = {" + ",".join(map(str, mask)) + "};\n"
|
||||
edit = " if (centered_face_mask[row] && ex != 0.f && ey != 0.f) { gx *= 2.f; gy *= 2.f; }\n"
|
||||
return before + flag + anchor + body.replace(position, edit + position) + "// Targets are receiver-local SOURCE slots" + after
|
||||
|
||||
|
||||
def source_digest(build):
|
||||
digest = hashlib.sha256()
|
||||
for path in sorted(Path(build).rglob("*")):
|
||||
if path.is_file() and path.name != ".celeris-build":
|
||||
digest.update(str(path.relative_to(build)).encode()); digest.update(path.read_bytes())
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def private_build(hierarchy, report, variant):
|
||||
"""No kernel upload/launch monkey-patch; same production function and arguments."""
|
||||
if variant == "current":
|
||||
yield
|
||||
return
|
||||
from CelerisLab.cuda import compiler_v2 as compiler
|
||||
mask = routing(hierarchy)
|
||||
report["reconstruction"] = metadata(mask)
|
||||
original, calls = compiler.generate_config, []
|
||||
def generate(cfg, *args, **kwargs):
|
||||
build = Path(original(cfg, *args, **kwargs))
|
||||
calls.append(cfg.viscosity)
|
||||
if len(calls) == 2:
|
||||
if calls != [.0384, .0768]:
|
||||
raise ValueError("unexpected candidate level configuration")
|
||||
path = build / "blocks.cu"
|
||||
path.write_text(patch_source(path.read_text(), mask))
|
||||
sidecar = build / ".celeris-build"
|
||||
data = json.loads(sidecar.read_text())
|
||||
data.update(source_sha256=source_digest(build), experimental_reconstruction=metadata(mask))
|
||||
sidecar.write_text(json.dumps(data, sort_keys=True))
|
||||
elif len(calls) > 2:
|
||||
raise ValueError("candidate is one adjacent pair only")
|
||||
return str(build)
|
||||
compiler.generate_config = generate
|
||||
try:
|
||||
yield
|
||||
if calls != [.0384, .0768]:
|
||||
raise ValueError("candidate did not build its exact two levels")
|
||||
finally:
|
||||
compiler.generate_config = original
|
||||
|
||||
|
||||
def observe(arrays):
|
||||
"""Bound observed synchronized ACTIVE values only, not intermediate GPU states."""
|
||||
maximum = max(float(np.nanmax(abs(a - W[:, None, None]))) for a in arrays)
|
||||
if not np.isfinite(maximum) or maximum > POP_ENVELOPE:
|
||||
raise ValueError("observed shifted-population envelope exceeded")
|
||||
return maximum
|
||||
|
||||
|
||||
def compare(records, expected_source):
|
||||
"""Four fresh receipts; diagnostic screens only, never a physical qualification."""
|
||||
import run_case as rc
|
||||
from assess_projected_pairs import BUILD_SOURCES, NUMERICS, valid_builds, finite_tree
|
||||
from CelerisLab.lbm.hierarchy import build_hierarchy
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
if not finite_tree(records) or not expected_source.get("revision"):
|
||||
raise ValueError("nonfinite receipt or missing exact source identity")
|
||||
keys = [(r['n'], r['depth'], r.get('test_reconstruction')) for r in records]
|
||||
if keys != list(CASES):
|
||||
raise ValueError("expected ordered fresh U128/U256/current/candidate cases")
|
||||
for r in records:
|
||||
for key in ('source', 'runtime_identity', 'runtime_source', 'cuda_visible_devices'):
|
||||
if not r.get(key) or r[key] != records[0][key]:
|
||||
raise ValueError(f"mixed or missing {key}")
|
||||
if (r['source'] != expected_source or r.get('numerical_revision') != rc.NUMERICAL_REVISION
|
||||
or r.get('numerics') != NUMERICS or r.get('re_L') != 100 or r.get('U') != .03
|
||||
or r.get('polarization') != [0., 1.] or r.get('initialization_id') != axis.INITIALIZATION_ID
|
||||
or r['source'].get('dirty') is not False or r['source'].get('source_tree_sha256') != rc.SOURCE_SHA256
|
||||
or r.get('diagnostic') != DIAGNOSTIC or not r.get('completed')
|
||||
or r.get('qualification_eligible') is not False or r.get('initialization') != axis.INITIALIZATION
|
||||
or r.get('mode') != [2, 0] or r.get('phase') != axis.PHI
|
||||
or r.get('explosion') != ('constant' if r['depth'] == 1 else 'projected_linear')
|
||||
or not 0 <= r.get('observed_shifted_max', np.inf) <= POP_ENVELOPE):
|
||||
raise ValueError("wrong candidate receipt identity or observed envelope")
|
||||
h = build_hierarchy(rc.config(r['n'], 100), [] if r['depth'] == 1 else axis.REFINEMENTS)
|
||||
if r.get('refinements') != ([] if r['depth'] == 1 else [[list(axis.REFINEMENTS[0][0])]]):
|
||||
raise ValueError("wrong geometry")
|
||||
if (r.get('viscosity') != .03*r['n']/100
|
||||
or r.get('tau_by_level') != [.5+3*c.viscosity for c in h.configs]
|
||||
or r.get('workload') != rc.workload([k == ACTIVE for k in h.kinds], 8*(r['n']//128))):
|
||||
raise ValueError("wrong physical scales or workload")
|
||||
builds = r.get('actual_build_sidecars', [])
|
||||
canonical = []
|
||||
for level, build in enumerate(builds):
|
||||
modified = r['test_reconstruction'] == VARIANT and level == 1
|
||||
expected = BUILD_SHA256 if modified else BUILD_SOURCES[r['n']*2**level]
|
||||
if (build.get('source_sha256') != expected
|
||||
or build.get('experimental_reconstruction') != (metadata(routing(h)) if modified else None)):
|
||||
raise ValueError("wrong actual build or reconstruction identity")
|
||||
# Reuse the frozen build-metadata checker after checking the private-source hash above.
|
||||
canonical.append(dict(build, source_sha256=BUILD_SOURCES[r['n']*2**level]))
|
||||
if not valid_builds(dict(actual_build_sidecars=canonical), r['n'], r['depth']):
|
||||
raise ValueError("invalid architecture, flags, config or PTX digest")
|
||||
if r.get('reconstruction') != (metadata(routing(h)) if r['test_reconstruction'] == VARIANT else dict(variant='current')):
|
||||
raise ValueError("wrong receipt mask identity")
|
||||
trace = r['probe_trace']
|
||||
declared = axis.new_trace(trace['probes'], r['viscosity'], [DIAGNOSTIC, r['n'], r['depth'], r['explosion']])
|
||||
if any(trace.get(k) != v for k,v in declared.items() if k not in ('steps','values')):
|
||||
raise ValueError("wrong trace phase, clock or physical identity")
|
||||
if trace['probes'] != axis.select([k == ACTIVE for k in h.kinds], r['n']):
|
||||
raise ValueError("wrong actual probe cells")
|
||||
if trace.get("bytes") != axis.validate(trace):
|
||||
raise ValueError("trace serialized byte count mismatch")
|
||||
if np.min(np.asarray(trace["values"])[:,0::3]) <= 0:
|
||||
raise ValueError("nonpositive sampled density")
|
||||
if [row['step'] for row in r['rows']] != [0, 8*(r['n']//128)] or not all(rc.finite_row(v) for v in r['rows']):
|
||||
raise ValueError("invalid scalar rows")
|
||||
if min(row['rho_min'] for row in r['rows']) <= 0:
|
||||
raise ValueError("nonpositive density")
|
||||
current, candidate = records[2:]
|
||||
if current['probe_trace']['values'][0] != candidate['probe_trace']['values'][0]:
|
||||
raise ValueError("candidate/current initial samples differ; cancellation invalid")
|
||||
vectors = []
|
||||
for r in records:
|
||||
groups = axis.group_moments(r['probe_trace'])
|
||||
vectors.append(np.array([[[row['groups'][name][key] for key in ('drho','djx','djy')]
|
||||
for name in axis.GROUPS] for row in groups]))
|
||||
# Inner quadrature matches U256; outer quadrature matches U128.
|
||||
uniform = vectors[1].copy(); uniform[:, 4:] = vectors[0][:, 4:]
|
||||
residual = [v-v[0]-uniform+uniform[0] for v in vectors[2:]]
|
||||
old, new = residual
|
||||
checks = dict(bottom_step1_clear=abs(old[1,2,1])-abs(new[1,2,1]) > MARGIN[1],
|
||||
top_step1_nonworse=abs(new[1,3,1]) <= abs(old[1,3,1])+MARGIN[1])
|
||||
for face, indices in [('bottom',[2,6]), ('top',[3,7])]:
|
||||
rms = lambda a: float(np.sqrt(np.mean(a[8,indices,1]**2)))
|
||||
checks[face+'_step8_nonworse'] = rms(new) <= rms(old)+MARGIN[1]
|
||||
checks['vertical_null'] = bool(np.all(abs(new[:,[0,1,4,5]]-old[:,[0,1,4,5]]) <= MARGIN))
|
||||
acoustic = lambda a: np.sqrt(a[...,0]**2/3 + a[...,1]**2 + a[...,2]**2)
|
||||
acoustic_margin = float(np.sqrt(MARGIN[0]**2/3 + MARGIN[1]**2 + MARGIN[2]**2))
|
||||
checks['acoustic_nonworse'] = bool(np.all(acoustic(new) <= acoustic(old)+acoustic_margin))
|
||||
raw_pressure = lambda r: float(np.max(abs(np.asarray(r['probe_trace']['values'])[:,0::3]-1))/3)
|
||||
checks['sampled_pressure_nonworse'] = raw_pressure(candidate) <= raw_pressure(current)+MARGIN[0]/3
|
||||
checks['step8_pressure_nonworse'] = candidate['rows'][-1]['spurious_pressure_rms'] <= current['rows'][-1]['spurious_pressure_rms']+MARGIN[0]/3
|
||||
return dict(qualified=False, advance_to_review=all(checks.values()), checks=checks,
|
||||
moment_deadband=MARGIN.tolist(), deadband_scope="local arithmetic-derived diagnostic; no accumulated error guarantee",
|
||||
groups=list(axis.GROUPS), components=['drho','djx','djy'],
|
||||
current_signed_residual=old.tolist(), candidate_signed_residual=new.tolist(),
|
||||
raw_current=vectors[2].tolist(), raw_candidate=vectors[3].tolist(),
|
||||
group_moments=[axis.group_moments(r['probe_trace']) for r in records],
|
||||
significant_sign_flips=((old*new < 0) & (abs(old)>MARGIN) & (abs(new)>MARGIN)).tolist(),
|
||||
top_step1_below_deadband=bool(abs(old[1,3,1]) <= MARGIN[1]),
|
||||
note="Absolute reduction with sign retained; top below deadband may be inconclusive.")
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--repo", required=True, type=Path)
|
||||
p.add_argument("--expected-revision", required=True)
|
||||
p.add_argument("receipts", nargs=4, type=Path, help="U128 U256 current candidate, in order")
|
||||
p.add_argument("--output", required=True, type=Path)
|
||||
args = p.parse_args()
|
||||
import run_case as rc
|
||||
source = rc.verify_source(args.repo.resolve(), args.expected_revision, "shear_centered", probes=True)
|
||||
result = compare([json.loads(path.read_text()) for path in args.receipts], source)
|
||||
with args.output.open("x") as stream:
|
||||
json.dump(result, stream, indent=2, allow_nan=False)
|
||||
return 0 if result["advance_to_review"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,174 @@
|
||||
"""Fixed three-level/two-region shear preset (opt-in nested_shear_n128).
|
||||
|
||||
Four fresh cases: U128, U512 (finest control = 4N), constant R128 depth 3,
|
||||
projected R128 depth 3. Same wave, phase, Re100/U=0.03, initialization, five
|
||||
sample times, eps(root N), limits and floors as shear_wave. This changes
|
||||
placement and refined volume as well as depth: no isolated-level cost or
|
||||
resolution-trend label. shear_wave.py is reused unchanged (historical receipt
|
||||
identities stay valid); only the depth/finest-control contract is generalized
|
||||
here.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
import shear_wave as sw
|
||||
from oracle import CX, CY, U, centers
|
||||
|
||||
ROOT_N = 128
|
||||
DEPTH = 3
|
||||
FINEST_FACTOR = 2 ** (DEPTH - 1) # finest-spacing uniform control is 4N
|
||||
# Parent-frame (x0, y0, x1, y1) rectangles; level-wide, zero-origin unions.
|
||||
REFINEMENTS = [
|
||||
[(16, 16, 60, 112), (68, 24, 112, 120)], # L0 -> L1, root coordinates
|
||||
[(44, 44, 108, 212), (148, 60, 212, 228)], # L1 -> L2, L1 coordinates
|
||||
]
|
||||
CASES = ((100, ROOT_N, 1, "constant"), (100, FINEST_FACTOR * ROOT_N, 1, "constant"),
|
||||
(100, ROOT_N, DEPTH, "constant"), (100, ROOT_N, DEPTH, "projected_linear"))
|
||||
ROOT_STEPS = 16 * ROOT_N
|
||||
LEAVES = [7936, 12288, 86016]
|
||||
HOPS = [ROOT_STEPS * 2**level for level in range(DEPTH)] # [2048, 4096, 8192]
|
||||
WORK = sum(c * h for c, h in zip(LEAVES, HOPS)) # 771227648 per refined case
|
||||
NU = [U * ROOT_N / sw.RE * 2**level for level in range(DEPTH)] # [.0384, .0768, .1536]
|
||||
TAU = [0.5 + 3 * nu for nu in NU] # [.6152, .7304, .9608]
|
||||
SNAPSHOT_MAX_BYTES = 8 * 1024 * 1024
|
||||
SNAPSHOT_KEYS = ("level", "i", "j", "rho", "ux", "uy", "meta")
|
||||
|
||||
|
||||
def leaf_masks():
|
||||
"""Independent numpy leaf masks of the fixed preset (root, L1, L2 level-wide grids)."""
|
||||
masks, covered = [], None
|
||||
for level in range(DEPTH):
|
||||
size = ROOT_N * 2**level
|
||||
inside = np.ones((size, size), bool) if level == 0 else covered
|
||||
if level < DEPTH - 1:
|
||||
child = np.zeros((size, size), bool)
|
||||
for x0, y0, x1, y1 in REFINEMENTS[level]:
|
||||
child[y0:y1, x0:x1] = True
|
||||
masks.append(inside & ~child)
|
||||
covered = np.kron(child, np.ones((2, 2), bool))
|
||||
else:
|
||||
masks.append(inside)
|
||||
return masks
|
||||
|
||||
|
||||
def assess(base_control, finest_control, refined):
|
||||
"""shear_wave.assess generalized to depth 3 with a 4N finest control.
|
||||
|
||||
Same summaries, screens, budgets and outcome semantics; only the
|
||||
(depth, finest-control) shape contract differs.
|
||||
"""
|
||||
if base_control is None or finest_control is None or not refined:
|
||||
return dict(status="INVALID_CONTROL", reason="missing control or refined run")
|
||||
n = base_control["n"]
|
||||
if (base_control["depth"], finest_control["depth"], finest_control["n"]) != (
|
||||
1, 1, FINEST_FACTOR * n):
|
||||
raise ValueError("controls must be uniform N and uniform 4N")
|
||||
if any(r["depth"] != DEPTH or r["n"] != n for r in refined):
|
||||
raise ValueError("refined runs must be depth 3 at root N")
|
||||
if len({r.get("explosion") for r in refined}) != len(refined):
|
||||
raise ValueError("duplicate refined explosion scheme")
|
||||
sources = {repr(r.get("source")) for r in [base_control, finest_control, *refined]}
|
||||
base, fine = sw._usable(base_control), sw._usable(finest_control)
|
||||
if base is None or fine is None or len(sources) != 1:
|
||||
return dict(status="INVALID_CONTROL", reason="incomplete/invalid control or mixed source")
|
||||
controls_pass = base["own_screens_pass"] and fine["own_screens_pass"]
|
||||
report = dict(base_control=base, finest_control=fine, refined={},
|
||||
controls_pass=controls_pass)
|
||||
statuses = []
|
||||
for r in refined:
|
||||
try:
|
||||
s = sw.summarize(r)
|
||||
except (KeyError, TypeError, ValueError) as error:
|
||||
report["refined"][r["explosion"]] = dict(valid=False, reason=str(error))
|
||||
statuses.append("INVALID_RUN")
|
||||
continue
|
||||
s["valid"] = True
|
||||
s["budget"] = {
|
||||
key: (None if base[key] is None else max(sw.BASELINE_FACTOR * base[key], floor))
|
||||
for key, floor in sw.FLOORS.items()
|
||||
}
|
||||
s["budget_pass"] = all(
|
||||
s[key] is not None and s["budget"][key] is not None and s[key] <= s["budget"][key]
|
||||
for key in sw.FLOORS)
|
||||
s["control_ratios"] = {
|
||||
name: {key: (s[key] / c[key] if s[key] is not None and c[key] else None)
|
||||
for key in (*sw.FLOORS, "spurious_pressure_rms")}
|
||||
for name, c in (("root", base), ("finest", fine))
|
||||
}
|
||||
s["controlled_pass"] = controls_pass and s["own_screens_pass"] and s["budget_pass"]
|
||||
report["refined"][r["explosion"]] = s
|
||||
statuses.append("PASS" if s["controlled_pass"] else "FAIL")
|
||||
if "INVALID_RUN" in statuses:
|
||||
report["status"] = "INVALID_RUN"
|
||||
elif not controls_pass:
|
||||
report["status"] = "CONTROL_SCREEN_FAIL"
|
||||
else:
|
||||
report["status"] = "PASS" if all(v == "PASS" for v in statuses) else "FAIL"
|
||||
return report
|
||||
|
||||
|
||||
def write_snapshot(path, arrays, masks, meta):
|
||||
"""Final macro snapshot: packed ACTIVE leaves (level, i, j), rho, ux, uy only.
|
||||
|
||||
No populations, no restart state, no time series. Returns the receipt block;
|
||||
a file over SNAPSHOT_MAX_BYTES is removed and reported, never kept.
|
||||
"""
|
||||
path = Path(path)
|
||||
if path.exists():
|
||||
raise FileExistsError(path)
|
||||
cols = {k: [] for k in ("level", "i", "j", "rho", "ux", "uy")}
|
||||
counts = []
|
||||
for level, (g, mask) in enumerate(zip(arrays, masks)):
|
||||
if g.shape != (9, *mask.shape):
|
||||
raise ValueError("population/mask shape mismatch")
|
||||
values = g[:, mask].astype(float)
|
||||
rho = values.sum(axis=0)
|
||||
j, i = np.nonzero(mask) # row = y index, column = x index
|
||||
cols["level"].append(np.full(rho.size, level, np.uint8))
|
||||
cols["i"].append(i.astype(np.uint16))
|
||||
cols["j"].append(j.astype(np.uint16))
|
||||
cols["rho"].append(rho)
|
||||
cols["ux"].append(CX @ values / rho)
|
||||
cols["uy"].append(CY @ values / rho)
|
||||
counts.append(int(rho.size))
|
||||
packed = {k: np.concatenate(v) for k, v in cols.items()}
|
||||
if not all(np.isfinite(packed[k]).all() for k in ("rho", "ux", "uy")):
|
||||
raise FloatingPointError("nonfinite snapshot macros")
|
||||
meta = dict(meta, leaf_counts=counts, cell_center="h*(index+0.5), h=2**-level",
|
||||
fields="rho, ux, uy from FP64 physical populations at ACTIVE leaves")
|
||||
with path.open("xb") as handle:
|
||||
np.savez_compressed(handle, meta=np.array(json.dumps(meta, sort_keys=True)), **packed)
|
||||
size = path.stat().st_size
|
||||
block = dict(file=path.name, bytes=size, leaf_counts=counts, max_bytes=SNAPSHOT_MAX_BYTES,
|
||||
step=meta.get("step"), tstar=meta.get("tstar"))
|
||||
if size > SNAPSHOT_MAX_BYTES:
|
||||
path.unlink()
|
||||
return dict(block, kept=False, sha256=None, reason="exceeds snapshot byte cap")
|
||||
block.update(kept=True, sha256=hashlib.sha256(path.read_bytes()).hexdigest())
|
||||
return block
|
||||
|
||||
|
||||
def read_snapshot(path):
|
||||
"""Validate a snapshot: exact keys, no populations, consistent packed arrays."""
|
||||
with np.load(path, allow_pickle=False) as data:
|
||||
if tuple(sorted(data.files)) != tuple(sorted(SNAPSHOT_KEYS)):
|
||||
raise ValueError("unexpected snapshot contents")
|
||||
out = {k: data[k] for k in data.files}
|
||||
meta = json.loads(str(out.pop("meta")))
|
||||
n = len(out["rho"])
|
||||
if any(len(v) != n for v in out.values()) or sum(meta["leaf_counts"]) != n:
|
||||
raise ValueError("inconsistent snapshot arrays")
|
||||
return meta, out
|
||||
|
||||
|
||||
def snapshot_meta(step, viscosity, explosion, source_revision):
|
||||
"""Exact time/phase/config identity stored inside the snapshot."""
|
||||
return dict(step=step, tstar=step * U / ROOT_N, phase=sw.PHI, mode=list(sw.MODE),
|
||||
polarization=sw.E.tolist(), amplitude=U, re_L=sw.RE, n=ROOT_N, depth=DEPTH,
|
||||
explosion=explosion, viscosity=viscosity, tau_by_level=TAU,
|
||||
refinements=REFINEMENTS, source_revision=source_revision,
|
||||
origin="level-wide zero-origin grids", time="root synchronization, postcollision")
|
||||
@@ -0,0 +1,34 @@
|
||||
"""First-gradient POST-G initialization counterfactual, not a qualification preset.
|
||||
|
||||
PRE F = feq - tau*A; POST G = feq - (tau - 1)*A. This low-Mach,
|
||||
first-order Chapman-Enskog approximation is not an exact discrete eigenstate.
|
||||
The physical viscous-stress factor tau - 1/2 is not the raw POST-G factor.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
import shear_nested as nested
|
||||
import shear_wave as sw
|
||||
from oracle import CX, CY, W, U, centers
|
||||
|
||||
REFINEMENTS = nested.REFINEMENTS
|
||||
CASES = ((100, 128, 1, "constant"), (100, 512, 1, "constant"),
|
||||
(100, 128, 3, "projected_linear"))
|
||||
INITIALIZATION_ID = "postg_first_gradient_v1"
|
||||
INITIALIZATION = (
|
||||
"physical FP64 POST G=feq-(tau-1)A at actual cell centers; "
|
||||
"A=3*w*U*cos(theta)*(c.e)*(c.k_level); rho=1; first-gradient low-Mach CE"
|
||||
)
|
||||
|
||||
|
||||
def initial(shape, level, length, viscosity):
|
||||
"""Return physical FP64 G; production initialize owns shifted FP32 encoding."""
|
||||
x, y = centers(shape, level)
|
||||
k = sw.wavevector(length)
|
||||
theta = k[0] * x + k[1] * y + sw.PHI
|
||||
k_level = k * 2.0**-level
|
||||
tau = 0.5 + 3 * viscosity * 2**level
|
||||
ce = CX * sw.E[0] + CY * sw.E[1]
|
||||
ck = CX * k_level[0] + CY * k_level[1]
|
||||
correction = (3 * W * ce * ck)[:, None, None] * U * np.cos(theta)
|
||||
return sw.initial(shape, level, length, viscosity) - (tau - 1) * correction
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Signed interface-probe traces for the nested/twin shear presets (mechanism diagnostic).
|
||||
|
||||
No pass gates. Native-validated 24-target layout through box 1, in root-128
|
||||
physical coordinates, crossing L0->L1 and (nested only) L1->L2 away from corners:
|
||||
horizontal line y = 56 across the vertical faces x = 16 and x = 22;
|
||||
vertical line x = 38 across the horizontal faces y = 16 and y = 22;
|
||||
normal coordinates NORMAL_COORDS on both (nested: 3 L0 + 5 L1 + 4 L2 each).
|
||||
Each target maps to the nearest ACTIVE leaf center (tie rule: distance, then
|
||||
finer level, then smaller j, then smaller i). Grids differ between cases
|
||||
(U128, U512, nested, twin), so actual centers, levels and displacements are
|
||||
stored and analytic fields are evaluated at the actual centers.
|
||||
Samples are POST-collision G at root synchronization only (root-step units);
|
||||
no substep content is observed, and grid-scale alternation can alias.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
import shear_wave as sw
|
||||
from oracle import CX, CY, U
|
||||
|
||||
ROOT_N = 128
|
||||
NORMAL_COORDS = (12.5, 14.5, 15.5, 16.25, 17.25, 19.25, 20.75, 21.75,
|
||||
22.125, 23.125, 24.125, 26.125)
|
||||
TRANSECTS = {"horizontal_y56": dict(axis=0, other=56.0), # x varies, y fixed
|
||||
"vertical_x38": dict(axis=1, other=38.0)} # y varies, x fixed
|
||||
STEPS = tuple(range(0, 129)) + tuple(range(1920, 2049)) # root steps, both inclusive
|
||||
TRACE_MAX_BYTES = 600_000
|
||||
|
||||
|
||||
def points():
|
||||
out = []
|
||||
for name, t in TRANSECTS.items():
|
||||
for along in NORMAL_COORDS:
|
||||
xy = (along, t["other"]) if t["axis"] == 0 else (t["other"], along)
|
||||
out.append((name, along, xy))
|
||||
return out
|
||||
|
||||
|
||||
def case_steps(n):
|
||||
"""Case lattice steps at the fixed root-physical sample times (U512: 4 per root step)."""
|
||||
return [s * (n // ROOT_N) for s in STEPS]
|
||||
|
||||
|
||||
def select(masks, n=ROOT_N):
|
||||
"""Nearest ACTIVE leaf center for each target (deterministic tie rule).
|
||||
|
||||
Targets and stored centers are in root-physical units (root cell = 1); a case
|
||||
with root N = n has cell size h = ROOT_N / (n * 2**level) there (U512: 0.25).
|
||||
"""
|
||||
if n % ROOT_N:
|
||||
raise ValueError("case root N must be a multiple of 128")
|
||||
probes = []
|
||||
for name, along, (x, y) in points():
|
||||
best = None
|
||||
for level, mask in enumerate(masks):
|
||||
h = ROOT_N / (n * 2 ** level)
|
||||
i0, j0 = int(math.floor(x / h)), int(math.floor(y / h))
|
||||
for j in range(j0 - 2, j0 + 3):
|
||||
for i in range(i0 - 2, i0 + 3):
|
||||
if 0 <= j < mask.shape[0] and 0 <= i < mask.shape[1] and mask[j, i]:
|
||||
cx, cy = h * (i + 0.5), h * (j + 0.5)
|
||||
key = (math.hypot(cx - x, cy - y), -level, j, i)
|
||||
if best is None or key < best[0]:
|
||||
best = (key, level, i, j, cx, cy)
|
||||
if best is None:
|
||||
raise ValueError(f"no ACTIVE leaf near target {(x, y)}")
|
||||
_, level, i, j, cx, cy = best
|
||||
probes.append(dict(transect=name, normal=along, point=[x, y], level=level, i=i, j=j,
|
||||
center=[cx, cy], displacement=[cx - x, cy - y]))
|
||||
if len({(p["level"], p["i"], p["j"]) for p in probes}) != len(probes):
|
||||
raise ValueError("probe targets map to duplicate cells")
|
||||
return probes
|
||||
|
||||
|
||||
def sample(arrays, probes):
|
||||
"""Flat [rho, ux, uy] per probe from FP64 physical populations."""
|
||||
row = []
|
||||
for p in probes:
|
||||
f = np.asarray(arrays[p["level"]][:, p["j"], p["i"]], dtype=float)
|
||||
rho = float(f.sum())
|
||||
row += [rho, float(CX @ f) / rho, float(CY @ f) / rho]
|
||||
return row
|
||||
|
||||
|
||||
def new_trace(probes, case_viscosity, case):
|
||||
"""Times/coordinates are root-physical (N=128); nu_root = U*128/Re for all cases."""
|
||||
return dict(schema=1, kind="mechanism diagnostic, no pass gates", case=case,
|
||||
time_unit="root step (N=128)", sample_phase="postcollision G at root synchronization",
|
||||
root_n=ROOT_N, viscosity_root=U * ROOT_N / sw.RE, case_viscosity=case_viscosity,
|
||||
mode=list(sw.MODE), phase=sw.PHI,
|
||||
amplitude=U, fields=["rho", "ux", "uy"], probes=probes, steps=[], values=[])
|
||||
|
||||
|
||||
def residuals(trace):
|
||||
"""Signed (drho, dux, duy) per step and probe, analytic field at each actual center."""
|
||||
out = []
|
||||
for step, row in zip(trace["steps"], trace["values"]):
|
||||
res = []
|
||||
for n, p in enumerate(trace["probes"]):
|
||||
x, y = p["center"]
|
||||
ux, uy, _ = sw.exact(x, y, step, trace["root_n"], trace["viscosity_root"])
|
||||
rho, vx, vy = row[3 * n: 3 * n + 3]
|
||||
res.append((rho - 1.0, vx - float(ux), vy - float(uy)))
|
||||
out.append(res)
|
||||
return out
|
||||
|
||||
|
||||
def validate_row(row, probe_count):
|
||||
"""Reject an invalid optional sample before it can enter a scalar receipt."""
|
||||
if len(row) != 3 * probe_count or not all(math.isfinite(v) for v in row):
|
||||
raise ValueError("malformed or nonfinite trace row")
|
||||
|
||||
|
||||
def validate(trace):
|
||||
"""Exact schedule, finite values, fixed probe count and byte cap."""
|
||||
import json
|
||||
if trace.get("steps") != list(STEPS) or len(trace["values"]) != len(STEPS):
|
||||
raise ValueError("trace steps differ from the fixed schedule")
|
||||
if len(trace["probes"]) != len(NORMAL_COORDS) * len(TRANSECTS):
|
||||
raise ValueError("unexpected probe count")
|
||||
for row in trace["values"]:
|
||||
validate_row(row, len(trace["probes"]))
|
||||
# Include the final byte-count field itself, including its number of digits.
|
||||
size = 0
|
||||
while True:
|
||||
final_size = len(json.dumps(dict(trace, bytes=size), allow_nan=False).encode())
|
||||
if final_size == size:
|
||||
break
|
||||
size = final_size
|
||||
if size > TRACE_MAX_BYTES:
|
||||
raise ValueError(f"trace exceeds {TRACE_MAX_BYTES} bytes ({size})")
|
||||
return size
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Depth-2 twin of the nested shear preset (opt-in twin_shear_n128).
|
||||
|
||||
Same two L0->L1 boxes as shear_nested, no L2. Four fresh cases: U128, U256
|
||||
(finest control = 2N), constant R128 depth 2, projected R128 depth 2; same
|
||||
wave, phase, Re100/U=0.03, initialization, times, eps(root N), limits, floors.
|
||||
|
||||
Twin vs nested measures the effect of adding this nested L2 refinement.
|
||||
Removing L2 also changes the finest spacing and refined volume, so the
|
||||
contrast does not isolate abstract depth. Physics is the unchanged depth-2,
|
||||
2N-control shear_wave.assess; shear_nested.py stays byte-identical.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
import shear_nested as nested
|
||||
|
||||
ROOT_N = nested.ROOT_N
|
||||
DEPTH = 2
|
||||
FINEST_FACTOR = 2
|
||||
REFINEMENTS = [nested.REFINEMENTS[0]] # L0 -> L1 only, root coordinates
|
||||
CASES = ((100, ROOT_N, 1, "constant"), (100, FINEST_FACTOR * ROOT_N, 1, "constant"),
|
||||
(100, ROOT_N, DEPTH, "constant"), (100, ROOT_N, DEPTH, "projected_linear"))
|
||||
ROOT_STEPS = 16 * ROOT_N
|
||||
LEAVES = [7936, 33792]
|
||||
HOPS = [ROOT_STEPS * 2**level for level in range(DEPTH)] # [2048, 4096]
|
||||
WORK = sum(c * h for c, h in zip(LEAVES, HOPS)) # 154664960 per refined case
|
||||
NU = nested.NU[:DEPTH]
|
||||
TAU = nested.TAU[:DEPTH]
|
||||
|
||||
|
||||
def leaf_masks():
|
||||
"""Independent numpy leaf masks (root complement, full L1 boxes)."""
|
||||
child = np.zeros((ROOT_N, ROOT_N), bool)
|
||||
for x0, y0, x1, y1 in REFINEMENTS[0]:
|
||||
child[y0:y1, x0:x1] = True
|
||||
return [~child, np.kron(child, np.ones((2, 2), bool))]
|
||||
@@ -0,0 +1,312 @@
|
||||
"""CPU-only oblique shear-wave reference and composite-leaf diagnostics.
|
||||
|
||||
Convention: integer mode MODE=(2,1) on the L x L periodic root domain, so the
|
||||
wavevector k = (2*pi/L)*(2,1) points 26.565 deg from +x (mode (1,2) would be
|
||||
63.435 deg). Velocity is polarized along E = (-1,2)/sqrt(5), perpendicular to k.
|
||||
The frozen Taylor-Green oracle is reused for constants and geometry only.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
from oracle import CX, CY, W, U, TIMES, centers
|
||||
|
||||
MODE = (2, 1)
|
||||
PHI = 0.0
|
||||
E = np.array([-MODE[1], MODE[0]], dtype=float) / math.hypot(*MODE)
|
||||
ANGLE_DEG = math.degrees(math.atan2(MODE[1], MODE[0]))
|
||||
RE = 100 # Re = U L / nu, the same nu as Taylor-Green Re100 at equal N
|
||||
FIT_FROM_TSTAR = 0.12 # late-time samples only; reduces startup sensitivity
|
||||
INITIALIZATION = (
|
||||
"physical FP64 equilibrium G at actual cell centers; rho=1 (exact p const)"
|
||||
)
|
||||
# Per-row screens. phase_drift_abs, residual_l2 and the run-level rate screen
|
||||
# use eps(n) and are preregistered diagnostic choices, not error bounds.
|
||||
LIMITS = dict(velocity_l2=0.05, spurious_pressure_scaled=0.02, mass_relative=1e-5)
|
||||
# The comparative pressure budget max(2*control, 0.02) is never below the 0.02
|
||||
# absolute ceiling, so it is always redundant; raw control ratios are kept.
|
||||
FLOORS = dict(rate_error=0.002, residual_l2=0.002, spurious_pressure_scaled=0.02)
|
||||
BASELINE_FACTOR = 2.0
|
||||
|
||||
|
||||
def wavevector(length):
|
||||
return 2 * np.pi / length * np.array(MODE, dtype=float)
|
||||
|
||||
|
||||
def decay_rate(length, viscosity):
|
||||
"""Exact amplitude decay rate per root step: nu |k|^2."""
|
||||
k = wavevector(length)
|
||||
return viscosity * float(k @ k)
|
||||
|
||||
|
||||
def eps(n):
|
||||
"""(|k| h_root)^2 with h_root = 1: dimensionless, coefficient one."""
|
||||
k = wavevector(n)
|
||||
return float(k @ k)
|
||||
|
||||
|
||||
def exact(x, y, t, length, viscosity, amplitude=U):
|
||||
k = wavevector(length)
|
||||
a = amplitude * np.exp(-decay_rate(length, viscosity) * t)
|
||||
s = np.sin(k[0] * x + k[1] * y + PHI)
|
||||
p = np.zeros_like(s)
|
||||
return a * E[0] * s, a * E[1] * s, p # kinematic pressure, constant
|
||||
|
||||
|
||||
def equilibrium(rho, ux, uy):
|
||||
# Same second-order D2Q9 equilibrium as oracle.initial.
|
||||
cu = CX[:, None, None] * ux + CY[:, None, None] * uy
|
||||
return (
|
||||
W[:, None, None]
|
||||
* rho
|
||||
* (1 + 3 * cu + 4.5 * cu * cu - 1.5 * (ux * ux + uy * uy))
|
||||
)
|
||||
|
||||
|
||||
def initial(shape, level, length, viscosity):
|
||||
x, y = centers(shape, level)
|
||||
ux, uy, p = exact(x, y, 0.0, length, viscosity)
|
||||
return equilibrium(1 + 3 * p, ux, uy)
|
||||
|
||||
|
||||
def _leaves(arrays, masks):
|
||||
accum = []
|
||||
for level, (g, mask) in enumerate(zip(arrays, masks)):
|
||||
if g.shape != (9, *mask.shape):
|
||||
raise ValueError("population/mask shape mismatch")
|
||||
values = g[:, mask].astype(float)
|
||||
if not np.isfinite(values).all():
|
||||
raise FloatingPointError("nonfinite leaf populations")
|
||||
rho = values.sum(axis=0)
|
||||
if (rho <= 0).any():
|
||||
raise FloatingPointError("nonpositive leaf density")
|
||||
x, y = centers(mask.shape, level)
|
||||
area = np.full(rho.size, 0.25**level)
|
||||
lev = np.full(rho.size, level)
|
||||
accum.append((rho, CX @ values / rho, CY @ values / rho, x[mask], y[mask], area, lev))
|
||||
return [np.concatenate(v) for v in zip(*accum)]
|
||||
|
||||
|
||||
def metrics(arrays, masks, t, length, viscosity, initial_mass=None):
|
||||
rho, ux, uy, x, y, w, lev = _leaves(arrays, masks)
|
||||
volume = w.sum()
|
||||
if not np.isclose(volume, length**2, rtol=0, atol=1e-10):
|
||||
raise ValueError("leaf partition does not cover domain once")
|
||||
k = wavevector(length)
|
||||
theta = k[0] * x + k[1] * y + PHI
|
||||
s, c = np.sin(theta), np.cos(theta)
|
||||
v = ux * E[0] + uy * E[1]
|
||||
# Weighted least squares on the actual composite quadrature (no
|
||||
# continuous-orthogonality assumption).
|
||||
gram = np.array([[w @ (s * s), w @ (s * c)], [w @ (s * c), w @ (c * c)]])
|
||||
a_hat, b_hat = np.linalg.solve(gram, [w @ (v * s), w @ (v * c)])
|
||||
ex, ey, _ = exact(x, y, t, length, viscosity)
|
||||
exact_norm = w @ (ex * ex + ey * ey)
|
||||
fit = a_hat * s + b_hat * c
|
||||
longitudinal = (ux * k[0] + uy * k[1]) / math.sqrt(k @ k)
|
||||
amplitude = U * math.exp(-decay_rate(length, viscosity) * t)
|
||||
scale = amplitude * amplitude / 4 # continuum reference scale, not the TG norm
|
||||
modal = math.hypot(a_hat, b_hat)
|
||||
p = (rho - 1) / 3
|
||||
p = p - w @ p / volume # one global gauge
|
||||
mass = float(w @ rho)
|
||||
return dict(
|
||||
tstar=t * U / length,
|
||||
mass=mass,
|
||||
mass_relative=(
|
||||
0.0 if initial_mass is None else abs(mass - initial_mass) / initial_mass
|
||||
),
|
||||
a_hat=float(a_hat),
|
||||
b_hat=float(b_hat),
|
||||
amplitude_exact=amplitude,
|
||||
amplitude_ratio=float(a_hat / amplitude),
|
||||
modal_amplitude=modal,
|
||||
amplitude_error=modal / amplitude - 1,
|
||||
phase_drift=float(math.atan2(b_hat, a_hat)),
|
||||
phase_drift_abs=abs(float(math.atan2(b_hat, a_hat))),
|
||||
velocity_l2=float(
|
||||
np.sqrt(w @ ((ux - ex) ** 2 + (uy - ey) ** 2) / exact_norm)
|
||||
),
|
||||
residual_l2=float(
|
||||
np.sqrt(w @ ((ux - fit * E[0]) ** 2 + (uy - fit * E[1]) ** 2) / exact_norm)
|
||||
),
|
||||
longitudinal_l2=float(np.sqrt(w @ (longitudinal**2) / exact_norm)),
|
||||
spurious_pressure_rms=float(np.sqrt(w @ (p * p) / volume)),
|
||||
spurious_pressure_scaled=float(np.sqrt(w @ (p * p) / volume) / scale),
|
||||
spurious_pressure_max_scaled=float(np.max(np.abs(p)) / scale),
|
||||
spurious_pressure_scaled_by_level=[
|
||||
float(np.sqrt(w[lev == i] @ (p[lev == i] ** 2) / w[lev == i].sum()) / scale)
|
||||
for i in range(len(masks))
|
||||
],
|
||||
rho_min=float(rho.min()),
|
||||
rho_max=float(rho.max()),
|
||||
max_speed=float(np.max(np.hypot(ux, uy))),
|
||||
leaf_cells=int(len(rho)),
|
||||
)
|
||||
|
||||
|
||||
def fit_rate(rows, length, viscosity):
|
||||
"""Free-intercept least squares of ln R, R = hypot(a_hat, b_hat), late samples.
|
||||
|
||||
The intercept absorbs only a constant amplitude prefactor once a single
|
||||
exponential dominates; per-interval rates and the fit residual are kept so
|
||||
a lingering transient stays visible. Rates are per root step of the record
|
||||
(uniform 2N uses its own nu and doubled step counts).
|
||||
"""
|
||||
late = [r for r in rows if r["tstar"] >= FIT_FROM_TSTAR - 1e-12]
|
||||
amp = np.array([math.hypot(r["a_hat"], r["b_hat"]) for r in late])
|
||||
if len(late) < 3 or not np.isfinite(amp).all():
|
||||
raise ValueError("need >= 3 late samples with finite R")
|
||||
exact_rate = decay_rate(length, viscosity)
|
||||
to_tstar = length / U # root steps per unit t*
|
||||
if not (amp > 0).all():
|
||||
# Finite modal extinction: a physical failure, not malformed data. No
|
||||
# clipping and no invented rate; fit quantities are unavailable.
|
||||
return dict(rate_fit_valid=False, rate=None, exact_rate=exact_rate,
|
||||
rate_tstar=None, exact_rate_tstar=exact_rate * to_tstar,
|
||||
rate_error=None, interval_rate_errors=None, log_fit_residual_max=None)
|
||||
t = np.array([r["step"] for r in late], dtype=float)
|
||||
(slope, intercept) = np.polyfit(t, np.log(amp), 1)
|
||||
residual = np.log(amp) - (slope * t + intercept)
|
||||
return dict(
|
||||
rate_fit_valid=True, rate=float(-slope), exact_rate=exact_rate,
|
||||
rate_tstar=float(-slope * to_tstar),
|
||||
exact_rate_tstar=exact_rate * to_tstar, # = 5 (2 pi)^2 / Re
|
||||
rate_error=abs(-slope / exact_rate - 1),
|
||||
interval_rate_errors=[
|
||||
float(-np.log(amp[i + 1] / amp[i]) / (t[i + 1] - t[i]) / exact_rate - 1)
|
||||
for i in range(len(t) - 1)
|
||||
],
|
||||
log_fit_residual_max=float(np.max(np.abs(residual))),
|
||||
)
|
||||
|
||||
|
||||
NONNEGATIVE = ("tstar", "mass", "mass_relative", "amplitude_exact", "modal_amplitude",
|
||||
"phase_drift_abs", "velocity_l2", "residual_l2", "longitudinal_l2",
|
||||
"spurious_pressure_rms", "spurious_pressure_scaled",
|
||||
"spurious_pressure_max_scaled", "rho_min", "rho_max", "max_speed")
|
||||
SIGNED = ("a_hat", "b_hat", "amplitude_ratio", "amplitude_error", "phase_drift")
|
||||
|
||||
|
||||
def _number(value):
|
||||
return (isinstance(value, (int, float)) and not isinstance(value, bool)
|
||||
and math.isfinite(value))
|
||||
|
||||
|
||||
def validate_rows(record):
|
||||
"""Exact schedule, finite numbers (incl. lists) and nonnegative error fields.
|
||||
|
||||
Signed quantities (amplitudes, amplitude/phase errors) keep their sign.
|
||||
Raises ValueError so callers can report an invalid run before any screen.
|
||||
"""
|
||||
n, rows = record["n"], record["rows"]
|
||||
steps = [round(tstar * n / U) for tstar in TIMES]
|
||||
if not isinstance(rows, list) or len(rows) != len(steps):
|
||||
raise ValueError("rows do not match the scheduled samples")
|
||||
for row, step in zip(rows, steps):
|
||||
if type(row.get("step")) is not int or row["step"] != step:
|
||||
raise ValueError(f"unscheduled or duplicate step {row.get('step')!r}")
|
||||
if not _number(row.get("tstar")) or abs(row["tstar"] - step * U / n) > 3e-17:
|
||||
raise ValueError(f"wrong sample time at step {step}")
|
||||
if not all(_number(row.get(key)) for key in (*NONNEGATIVE, *SIGNED)):
|
||||
raise ValueError(f"missing or nonfinite diagnostic at step {step}")
|
||||
if any(row[key] < 0 for key in NONNEGATIVE) or row["rho_min"] <= 0:
|
||||
raise ValueError(f"negative error quantity or density at step {step}")
|
||||
levels = row.get("spurious_pressure_scaled_by_level")
|
||||
if (not isinstance(levels, list) or len(levels) != record["depth"]
|
||||
or not all(_number(v) and v >= 0 for v in levels)):
|
||||
raise ValueError(f"invalid per-level pressure at step {step}")
|
||||
|
||||
|
||||
def summarize(record):
|
||||
validate_rows(record)
|
||||
rows = record["rows"]
|
||||
n = record["n"]
|
||||
out = fit_rate(rows, n, record["viscosity"])
|
||||
out.update(
|
||||
eps=eps(n),
|
||||
complete=record["completed"] is True,
|
||||
residual_l2=max(r["residual_l2"] for r in rows),
|
||||
phase_drift_abs=max(r["phase_drift_abs"] for r in rows),
|
||||
spurious_pressure_scaled=max(r["spurious_pressure_scaled"] for r in rows),
|
||||
spurious_pressure_rms=max(r["spurious_pressure_rms"] for r in rows),
|
||||
amplitude_error_abs=max(abs(r["amplitude_error"]) for r in rows),
|
||||
velocity_l2=max(r["velocity_l2"] for r in rows),
|
||||
mass_relative=max(r["mass_relative"] for r in rows),
|
||||
)
|
||||
if out["rate_fit_valid"] and not all(
|
||||
_number(v) for v in (out["rate"], out["rate_error"], *out["interval_rate_errors"],
|
||||
out["log_fit_residual_max"])):
|
||||
raise ValueError("nonfinite rate fit")
|
||||
out["own_screens_pass"] = out["complete"] and out["rate_fit_valid"] and all(
|
||||
out[key] <= out["eps"] for key in ("rate_error", "phase_drift_abs", "residual_l2")
|
||||
) and all(out[key] <= limit for key, limit in LIMITS.items())
|
||||
return out
|
||||
|
||||
|
||||
def _usable(record):
|
||||
try:
|
||||
out = summarize(record)
|
||||
except (KeyError, TypeError, ValueError):
|
||||
return None
|
||||
return out if out["complete"] else None
|
||||
|
||||
|
||||
def assess(base_control, fine_control, refined):
|
||||
"""Uniform root-spacing and finest-spacing controls versus refined runs.
|
||||
|
||||
INVALID_CONTROL: missing, incomplete, invalid-row or mixed-source controls.
|
||||
INVALID_RUN: a refined record has invalid rows (schedule, NaN, sign).
|
||||
CONTROL_SCREEN_FAIL: complete valid controls that fail their own screens;
|
||||
refined own-screen results are kept, but no controlled comparison passes.
|
||||
PASS/FAIL: every refined run's controlled_pass (own screens and budgets).
|
||||
"""
|
||||
if base_control is None or fine_control is None or not refined:
|
||||
return dict(status="INVALID_CONTROL", reason="missing control or refined run")
|
||||
n = base_control["n"]
|
||||
if (base_control["depth"], fine_control["depth"], fine_control["n"]) != (1, 1, 2 * n):
|
||||
raise ValueError("controls must be uniform N and uniform 2N")
|
||||
if any(r["depth"] != 2 or r["n"] != n for r in refined):
|
||||
raise ValueError("refined runs must be depth 2 at root N")
|
||||
if len({r.get("explosion") for r in refined}) != len(refined):
|
||||
raise ValueError("duplicate refined explosion scheme")
|
||||
sources = {repr(r.get("source")) for r in [base_control, fine_control, *refined]}
|
||||
base, fine = _usable(base_control), _usable(fine_control)
|
||||
if base is None or fine is None or len(sources) != 1:
|
||||
return dict(status="INVALID_CONTROL",
|
||||
reason="incomplete/invalid control or mixed source")
|
||||
controls_pass = base["own_screens_pass"] and fine["own_screens_pass"]
|
||||
report = dict(base_control=base, fine_control=fine, refined={},
|
||||
controls_pass=controls_pass)
|
||||
statuses = []
|
||||
for r in refined:
|
||||
try:
|
||||
s = summarize(r)
|
||||
except (KeyError, TypeError, ValueError) as error:
|
||||
report["refined"][r["explosion"]] = dict(valid=False, reason=str(error))
|
||||
statuses.append("INVALID_RUN")
|
||||
continue
|
||||
s["valid"] = True
|
||||
# rate_error is None when a modal fit is unavailable; never a pass.
|
||||
s["budget"] = {
|
||||
key: (None if base[key] is None else max(BASELINE_FACTOR * base[key], floor))
|
||||
for key, floor in FLOORS.items()
|
||||
}
|
||||
s["budget_pass"] = all(
|
||||
s[key] is not None and s["budget"][key] is not None and s[key] <= s["budget"][key]
|
||||
for key in FLOORS)
|
||||
s["control_ratios"] = {
|
||||
name: {key: (s[key] / c[key] if s[key] is not None and c[key] else None)
|
||||
for key in (*FLOORS, "spurious_pressure_rms")}
|
||||
for name, c in (("root", base), ("finest", fine))
|
||||
}
|
||||
s["controlled_pass"] = controls_pass and s["own_screens_pass"] and s["budget_pass"]
|
||||
report["refined"][r["explosion"]] = s
|
||||
statuses.append("PASS" if s["controlled_pass"] else "FAIL")
|
||||
if "INVALID_RUN" in statuses:
|
||||
report["status"] = "INVALID_RUN"
|
||||
elif not controls_pass:
|
||||
report["status"] = "CONTROL_SCREEN_FAIL"
|
||||
else:
|
||||
report["status"] = "PASS" if all(v == "PASS" for v in statuses) else "FAIL"
|
||||
return report
|
||||
@@ -0,0 +1,20 @@
|
||||
"""One projected x-gap-width diagnostic (opt-in wide_x_shear_n128).
|
||||
|
||||
Keep the nested outer boxes and all three spacings. Shrink only L2 in x,
|
||||
so each x-normal middle gap grows from 6 to 12 root cells; y gaps stay 6.
|
||||
This also reduces L2 volume and moves its x faces. It is a descriptive
|
||||
geometry contrast with no new qualification matrix, assessor or pass gate.
|
||||
"""
|
||||
|
||||
import shear_nested as nested
|
||||
|
||||
ROOT_N = nested.ROOT_N
|
||||
DEPTH = nested.DEPTH
|
||||
REFINEMENTS = [nested.REFINEMENTS[0],
|
||||
[(56, 44, 96, 212), (160, 60, 200, 228)]] # parent L1 coordinates
|
||||
CASES = ((100, ROOT_N, DEPTH, "projected_linear"),)
|
||||
LEAVES = [7936, 20352, 53760]
|
||||
HOPS = nested.HOPS
|
||||
WORK = sum(c * h for c, h in zip(LEAVES, HOPS)) # 540016640 active updates
|
||||
NU = nested.NU
|
||||
TAU = nested.TAU
|
||||
@@ -0,0 +1,172 @@
|
||||
"""Fixed square geometry, native sampling, POST phase and runner admission; CPU only."""
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
import numpy as np
|
||||
import run_case as rc
|
||||
import shear_axis_square as axis
|
||||
import shear_wave as sw
|
||||
import test_shear_wave as legacy
|
||||
from oracle import CX, CY, W, U, centers
|
||||
from CelerisLab.lbm.hierarchy import build_hierarchy, leaf_partition
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def masks(n, depth):
|
||||
h = build_hierarchy(rc.config(n, 100), [] if depth == 1 else axis.REFINEMENTS)
|
||||
np.testing.assert_array_equal(leaf_partition(h), np.ones((n, n)))
|
||||
return [k == ACTIVE for k in h.kinds]
|
||||
|
||||
|
||||
class AxisSquareTests(unittest.TestCase):
|
||||
def test_geometry_native_groups_and_work(self):
|
||||
work = 0
|
||||
for _, n, depth, _ in axis.CASES:
|
||||
m = masks(n, depth)
|
||||
p = axis.select(m, n)
|
||||
self.assertEqual(len(p), 20 if depth == 2 else 8*(n//128)**2)
|
||||
self.assertEqual(len({(v['level'], v['i'], v['j']) for v in p}), len(p))
|
||||
roots = [(32, 64), (95, 63), (64, 32), (63, 95),
|
||||
(31, 64), (96, 63), (64, 31), (63, 96)]
|
||||
expected = []
|
||||
for group, (x, y) in enumerate(roots):
|
||||
level = 1 if depth == 2 and group < 4 else 0
|
||||
factor = (n//128) * 2**level
|
||||
expected += [(level, factor*x+dx, factor*y+dy,
|
||||
[x+(dx+.5)/factor, y+(dy+.5)/factor])
|
||||
for dy in range(factor) for dx in range(factor)]
|
||||
self.assertEqual([(v['level'], v['i'], v['j'], v['center']) for v in p], expected)
|
||||
for name in axis.GROUPS:
|
||||
group = [v for v in p if v['group'] == name]
|
||||
self.assertEqual(sum(v['area'] for v in group), 1)
|
||||
np.testing.assert_array_equal(np.mean([v['center'] for v in group], axis=0),
|
||||
np.array(axis.GROUPS[name])+.5)
|
||||
for v in group:
|
||||
self.assertTrue(m[v['level']][v['j'], v['i']])
|
||||
w = rc.workload(m, axis.case_steps(n)[-1])
|
||||
work += w['active_collision_updates']
|
||||
if depth == 2:
|
||||
self.assertEqual(w['active_cells_by_level'], [12288, 16384])
|
||||
self.assertEqual(w['level_hops'], [8, 16])
|
||||
self.assertEqual(axis.case_steps(n), [r*(n//128) for r in range(9)])
|
||||
self.assertEqual(work, 1900544)
|
||||
|
||||
def test_postg_moments_stress_and_matching_fine_control(self):
|
||||
for level in (0, 1):
|
||||
x, _ = centers((33, 41), level)
|
||||
k, h, tau = 4*np.pi/128, 2.**-level, .5+3*.0384*2**level
|
||||
uy = U*np.sin(k*x+np.pi/4)
|
||||
eq = sw.equilibrium(np.ones_like(x), np.zeros_like(x), uy)
|
||||
g = axis.initial(x.shape, level, 128, .0384)
|
||||
a = 3*W[:, None, None]*CX[:, None, None]*CY[:, None, None]*h*k*U*np.cos(k*x+np.pi/4)
|
||||
np.testing.assert_allclose(g, eq-(tau-1)*a, rtol=0, atol=6e-17)
|
||||
for v in (np.ones(9), CX, CY):
|
||||
np.testing.assert_allclose(np.einsum('q,qyx->yx', v, g-eq), 0, rtol=0, atol=2e-16)
|
||||
np.testing.assert_allclose(np.einsum('q,qyx->yx', CX*CY, g-eq),
|
||||
(1-tau)*h*k*U*np.cos(k*x+np.pi/4)/3, atol=6e-17)
|
||||
self.assertGreater(g.min(), 0)
|
||||
np.testing.assert_array_equal(axis.initial((256, 256), 0, 256, .0768),
|
||||
axis.initial((256, 256), 1, 128, .0384))
|
||||
|
||||
def test_signed_momentum_and_actual_center_time(self):
|
||||
m = masks(128, 2)
|
||||
probes = axis.select(m)
|
||||
trace = axis.new_trace(probes, .0384, [rc.AXIS_SQUARE, 128, 2, 'projected_linear'])
|
||||
for step in axis.STEPS:
|
||||
arrays = []
|
||||
for level, mask in enumerate(m):
|
||||
x, _ = centers(mask.shape, level)
|
||||
uy, _ = axis.exact(x, step, 128, .0384)
|
||||
arrays.append(sw.equilibrium(np.full_like(x, 1.001), np.zeros_like(x), uy))
|
||||
trace['steps'].append(step)
|
||||
trace['values'].append(axis.sample(arrays, probes))
|
||||
trace['bytes'] = axis.validate(trace)
|
||||
self.assertEqual(trace['bytes'], len(json.dumps(trace, allow_nan=False).encode()))
|
||||
for row in trace['group_moments']:
|
||||
for name, v in row['groups'].items():
|
||||
self.assertAlmostEqual(v['drho'], .001, places=14)
|
||||
x = np.array([p['center'][0] for p in probes if p['group'] == name])
|
||||
uy, gradient = axis.exact(x, row['root_step'], 128, .0384)
|
||||
self.assertAlmostEqual(v['djy'], .001*uy.mean(), places=14)
|
||||
self.assertAlmostEqual(v['mean_duy_dx'], gradient.mean(), places=14)
|
||||
self.assertAlmostEqual(row['groups']['inner_left']['mean_duy_dx'],
|
||||
-row['groups']['inner_bottom']['mean_duy_dx'], places=14)
|
||||
with patch.object(axis, 'TRACE_MAX_BYTES', trace['bytes']-1), self.assertRaises(ValueError):
|
||||
axis.validate(trace)
|
||||
for mutate in (lambda t: t['steps'].pop(),
|
||||
lambda t: t['steps'].__setitem__(1, 0),
|
||||
lambda t: t['values'][1].__setitem__(0, float('nan')),
|
||||
lambda t: t['probes'].pop()):
|
||||
bad = deepcopy(trace); mutate(bad)
|
||||
with self.assertRaises(ValueError): axis.validate(bad)
|
||||
|
||||
def test_four_full_mock_schedules_and_old_rejection(self):
|
||||
helper = legacy.ProbeTraceTests()
|
||||
helper.setUp()
|
||||
def arrays(m, n, t):
|
||||
return [axis.initial(v.shape, level, n, U*n/100) for level, v in enumerate(m)]
|
||||
for _, n, depth, scheme in axis.CASES:
|
||||
with patch.object(helper, 'exact_arrays', side_effect=arrays):
|
||||
r, counts, files = helper.run_mocked(n, depth, rc.AXIS_SQUARE, scheme,
|
||||
masks(n, depth), initializer=axis.initial)
|
||||
self.assertEqual(counts[-1], 8*(n//128))
|
||||
self.assertEqual([v['step'] for v in r['rows']], [0, 8*(n//128)])
|
||||
self.assertEqual(r['probe_trace']['steps'], list(range(9)))
|
||||
self.assertEqual(r['probe_trace']['probes'], axis.select(masks(n, depth), n))
|
||||
self.assertEqual(r['mode'], [2, 0])
|
||||
self.assertEqual(r['phase'], np.pi/4)
|
||||
self.assertEqual(r['initialization'], axis.INITIALIZATION)
|
||||
self.assertEqual(r['limits'], {})
|
||||
self.assertIsNone(r['screening_pass'])
|
||||
self.assertFalse(r['qualification_eligible'])
|
||||
self.assertEqual(files, ['probe.json'])
|
||||
old = legacy.NestedPresetTests(); old.setUp()
|
||||
records = old.nested_set(); records[-1]['diagnostic'] = rc.AXIS_SQUARE
|
||||
self.assertEqual(old.mod.assess_nested(records, 'r')['status'], 'INVALID_SET')
|
||||
records = [legacy.receipt(128, 1), legacy.receipt(256, 1), legacy.receipt(128, 2),
|
||||
legacy.receipt(128, 2, 'projected_linear')]
|
||||
records[-1].update(diagnostic=rc.AXIS_SQUARE, initialization=axis.INITIALIZATION)
|
||||
self.assertEqual(old.mod.assess(records, 'r')['status'], 'INVALID_SET')
|
||||
self.assertNotIn(rc.AXIS_SQUARE, old.mod.PRESETS)
|
||||
with patch.object(axis, 'sample', return_value=[float('nan')]*24):
|
||||
r, _, _ = helper.run_mocked(128, 1, rc.AXIS_SQUARE, 'constant', masks(128, 1),
|
||||
initializer=axis.initial)
|
||||
self.assertIn('error', r['probe_trace'])
|
||||
self.assertTrue(r['completed'])
|
||||
self.assertEqual(len(r['rows']), 2)
|
||||
|
||||
def test_admission_and_module_pin(self):
|
||||
from tests.validation.multigrid import check_projected_pairs as gate
|
||||
self.assertEqual(hashlib.sha256(Path(axis.__file__).read_bytes()).hexdigest(), rc.SHEAR_AXIS_SQUARE_SHA256)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base = ['run_case.py', '--repo', str(REPO), '--expected-revision', 'f'*40,
|
||||
'--output', str(Path(tmp)/'case.json'), '--dry-run', '--re', '100',
|
||||
'--flow', 'shear_wave', '--diagnostic', rc.AXIS_SQUARE, '--probe-trace']
|
||||
for n, depth, scheme, valid in [(n, d, s, True) for _, n, d, s in axis.CASES] + [
|
||||
(512, 1, 'constant', False), (256, 2, 'projected_linear', False), (128, 3, 'constant', False)]:
|
||||
for extra in ([], ['--final-snapshot'], ['--early-pressure'], ['--flow', 'taylor_green'], ['--re', '1000']):
|
||||
argv = base + ['--n', str(n), '--depth', str(depth), '--explosion', scheme] + extra
|
||||
with patch.object(sys, 'argv', argv), redirect_stdout(io.StringIO()), patch('sys.stderr', io.StringIO()), \
|
||||
patch.object(rc, 'verify_source', return_value={}) as verify, patch.object(rc, 'runtime_source', return_value={}):
|
||||
if valid and not extra:
|
||||
self.assertEqual(rc.main(), 0)
|
||||
verify.assert_called_once_with(REPO, 'f'*40, 'shear_axis_square', probes=True)
|
||||
else:
|
||||
with self.assertRaises(SystemExit): rc.main()
|
||||
verify.assert_not_called()
|
||||
with patch.object(gate, 'preflight', return_value=rc.NUMERICAL_REVISION), \
|
||||
patch.object(rc, 'provenance', return_value=dict(source_tree_sha256=rc.SOURCE_SHA256)):
|
||||
rc.verify_source(REPO, 'r', 'shear_axis_square', probes=True)
|
||||
for module, key, value in ((axis, '__file__', '/wrong/helper.py'), (rc, 'SHEAR_AXIS_SQUARE_SHA256', 'wrong')):
|
||||
with patch.object(module, key, value), self.assertRaisesRegex(RuntimeError, 'shear_axis_square'):
|
||||
rc.verify_source(REPO, 'r', 'shear_axis_square', probes=True)
|
||||
@@ -0,0 +1,246 @@
|
||||
"""Independent coordinate oracle, exact kernel host shim and closed pair budgets."""
|
||||
import ctypes
|
||||
from contextlib import nullcontext
|
||||
from copy import deepcopy
|
||||
from dataclasses import replace
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
import run_case as rc
|
||||
import shear_axis_square as axis
|
||||
import shear_centered as candidate
|
||||
import test_shear_wave as legacy
|
||||
from test_axis_square import masks
|
||||
from CelerisLab.lbm.hierarchy import build_hierarchy
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
from CelerisLab.lbm.refinement_tables import projected_explosion_tables
|
||||
from CelerisLab.lbm.refinement import ACTIVE, OUTER_GHOST
|
||||
from tests.unit import test_projected_explosion as old
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3]
|
||||
SOURCE = REPO/'src/CelerisLab/lbm/kernels/blocks.cu'
|
||||
|
||||
|
||||
def oracle(c, pair, centered=True):
|
||||
"""Uses only dense coordinates/legal donors, never production neighbor/mask arrays."""
|
||||
out = np.empty((9, len(pair.ghost_fine)))
|
||||
legal = set(map(int, pair.ghost_coarse))
|
||||
for r, (donor, target) in enumerate(zip(pair.ghost_coarse, pair.ghost_fine)):
|
||||
y, x = divmod(int(donor), 128); fy, fx = divmod(int(target), 256)
|
||||
delta = np.array([(fx+.5)/2-x-.5, (fy+.5)/2-y-.5])
|
||||
usable = [(128*(y-dy)+x-dx in legal and 128*(y+dy)+x+dx in legal)
|
||||
for dx,dy in [(1,0),(0,1)]]
|
||||
straight = ((x in (30,31,96,97) and 36 <= y < 92 and usable == [False,True])
|
||||
or (y in (30,31,96,97) and 36 <= x < 92 and usable == [True,False]))
|
||||
for q, e in enumerate(old.E):
|
||||
grad = np.array([(c[q,y+dy,x+dx]-c[q,y-dy,x-dx])/2 if ok else 0.
|
||||
for (dx,dy),ok in zip([(1,0),(0,1)],usable)])
|
||||
if q: grad -= e*(grad@e)/(e@e)
|
||||
out[q,r] = c[q,y,x] + (2 if centered and straight and np.all(e) else 1)*(grad@delta)
|
||||
return out
|
||||
|
||||
|
||||
class CenteredTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.h = build_hierarchy(rc.config(128,100), axis.REFINEMENTS)
|
||||
cls.p = cls.h.pairs[0]
|
||||
cls.mask = candidate.routing(cls.h)
|
||||
cls.layout = BlockLayout2D.build(128,128,8,8,periodic=(True,True))
|
||||
cls.ids, cls.offsets = projected_explosion_tables(cls.p,cls.layout,cls.h.kinds[0])
|
||||
cls.neighbors = old.decode(cls.layout,cls.ids)
|
||||
|
||||
def test_mask_constants_quartets_and_transpose(self):
|
||||
self.assertEqual(candidate.metadata(self.mask), dict(variant=candidate.VARIANT,
|
||||
mask_sha256='112de63ce9bafd2e205a9e7029ef6ce5683fe14407b6ac04444579c84960b7ea',
|
||||
ghost_rows=2112, selected_rows=1792,
|
||||
rule='diagonal projected correction x2; audited straight faces only'))
|
||||
c=np.random.default_rng(47).normal(0,.001,(9,128,128))
|
||||
actual=oracle(c,self.p); baseline=oracle(c,self.p,False)
|
||||
base,g=old.reconstruct(c,self.p,self.neighbors,self.offsets)
|
||||
g[np.ix_(np.flatnonzero(np.all(old.E,axis=1)),np.flatnonzero(self.mask),[0,1])]*=2
|
||||
expected=c.reshape(9,-1)[:,self.p.ghost_coarse]+np.einsum('qra,ra->qr',g,self.offsets)
|
||||
np.testing.assert_allclose(actual,expected,atol=1e-18,rtol=0)
|
||||
np.testing.assert_array_equal(actual[:,~self.mask.astype(bool)],baseline[:,~self.mask.astype(bool)])
|
||||
for d in np.unique(self.p.ghost_coarse):
|
||||
rows=self.p.ghost_coarse==d
|
||||
np.testing.assert_allclose(actual[:,rows].mean(axis=1),c.reshape(9,-1)[:,d],atol=1e-18)
|
||||
const=np.broadcast_to(old.W[:,None,None],c.shape)
|
||||
np.testing.assert_array_equal(oracle(const,self.p),np.broadcast_to(old.W[:,None],actual.shape))
|
||||
qswap=[list(map(tuple,old.E)).index(tuple(e[::-1])) for e in old.E]
|
||||
index={int(t):r for r,t in enumerate(self.p.ghost_fine)}
|
||||
swap=[index[int(t%256)*256+int(t//256)] for t in self.p.ghost_fine]
|
||||
np.testing.assert_array_equal(self.mask,self.mask[swap])
|
||||
np.testing.assert_allclose(oracle(c[qswap].transpose(0,2,1),self.p),actual[qswap][:,swap],atol=1e-18)
|
||||
qreflect=[list(map(tuple,old.E)).index(tuple((-e[0],e[1]))) for e in old.E]
|
||||
reflect=[index[int(t//256)*256+255-int(t%256)] for t in self.p.ghost_fine]
|
||||
np.testing.assert_array_equal(self.mask,self.mask[reflect])
|
||||
np.testing.assert_allclose(oracle(c[qreflect,:,::-1],self.p),actual[qreflect][:,reflect],atol=1e-18)
|
||||
# Central dependency corridors remain in a constant mask regime beyond radius8*2.5.
|
||||
for name,(x,y) in axis.GROUPS.items():
|
||||
tangent = y if name.endswith(('left','right')) else x
|
||||
self.assertGreater(min(tangent+.25-36,92-(tangent+.75)),20)
|
||||
|
||||
def test_actual_generated_kernel_shifted_unshifted(self):
|
||||
child=self.p.fine_layout((8,4)) # unequal owner strides, unused slots poisoned
|
||||
patched=candidate.patch_source(SOURCE.read_text(),self.mask)
|
||||
kernel=patched.split('__device__ __constant__ unsigned char centered_face_mask',1)[1]
|
||||
kernel='unsigned char centered_face_mask'+kernel.split('// Targets are receiver-local SOURCE slots',1)[0]
|
||||
kernel=kernel.replace('extern "C" __global__','extern "C"')
|
||||
code='#include <cstdint>\nusing fpxx=float;\n'
|
||||
code+=f'constexpr unsigned NQ=9,TOTAL_CELLS={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
|
||||
original=SOURCE.read_text().split('extern "C" __global__ void BlockExplodeProjected(',1)[1].split('// Targets are receiver-local SOURCE slots',1)[0]
|
||||
code+='void Original('+original
|
||||
code+='extern "C" void invoke(float*c,float*f,uint64_t*t,uint64_t*d,unsigned n,unsigned long l,uint64_t*b,float*o){for(unsigned r=0;r<n;r++){threadIdx.x=r;BlockExplodeProjected(c,f,t,d,n,l,b,o);}}'
|
||||
code+=code[code.index('extern "C" void invoke'):].replace('void invoke(', 'void invoke_old(').replace('BlockExplodeProjected(c,f,','Original(c,f,')
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src=Path(tmp)/'kernel.cpp'; lib=Path(tmp)/'kernel.so'; src.write_text(code)
|
||||
subprocess.run(['g++','-std=c++17','-O2','-shared','-fPIC',str(src),'-o',str(lib)],check=True,capture_output=True)
|
||||
library=ctypes.CDLL(str(lib));fn=library.invoke;fn_old=library.invoke_old
|
||||
ptr=lambda a:a.ctypes.data_as(ctypes.c_void_p)
|
||||
targets=np.ascontiguousarray(child.owner.ravel()[self.p.ghost_fine],np.uint64)
|
||||
donors=np.ascontiguousarray(self.layout.owner.ravel()[self.p.ghost_coarse],np.uint64)
|
||||
for shifted in (True,False):
|
||||
c=np.random.default_rng(8).normal(0,.001,(9,128,128))
|
||||
if not shifted:c+=old.W[:,None,None]
|
||||
c=c.astype(np.float32); c[:,self.p.covered]=np.nan
|
||||
packed=np.full(self.layout.nblocks*9*np.prod(self.layout.local_shape),np.nan,np.float32)
|
||||
for q in range(9):packed[self.layout.slot(self.layout.owner.ravel(),q)]=c[q].ravel()
|
||||
fine=np.full(child.nblocks*9*np.prod(child.local_shape),np.nan,np.float32)
|
||||
fn(ptr(packed),ptr(fine),ptr(targets),ptr(donors),ctypes.c_uint(len(targets)),ctypes.c_ulong(np.prod(self.layout.local_shape)),ptr(self.ids),ptr(self.offsets))
|
||||
original=np.full_like(fine,np.nan)
|
||||
fn_old(ptr(packed),ptr(original),ptr(targets),ptr(donors),ctypes.c_uint(len(targets)),ctypes.c_ulong(np.prod(self.layout.local_shape)),ptr(self.ids),ptr(self.offsets))
|
||||
output=np.array([fine[child.slot(targets,q)] for q in range(9)])
|
||||
unchanged=np.array([original[child.slot(targets,q)] for q in range(9)])
|
||||
np.testing.assert_array_equal(output[:,~self.mask.astype(bool)],unchanged[:,~self.mask.astype(bool)])
|
||||
np.testing.assert_array_equal(output[:5],unchanged[:5])
|
||||
np.testing.assert_allclose(output,oracle(c.astype(float),self.p),atol=6e-8,rtol=2e-7)
|
||||
|
||||
def test_mixed_mask_full_pair_mass_momentum(self):
|
||||
rng=np.random.default_rng(12); p=self.p
|
||||
c=old.W[:,None,None]*(1+rng.normal(0,.002,(9,128,128)))
|
||||
f=old.W[:,None,None]*(1+rng.normal(0,.002,(9,256,256)))
|
||||
start=old.totals(c,f,p); c[:,p.covered]=np.nan
|
||||
for _ in range(2):
|
||||
f.reshape(9,-1)[:,p.ghost_fine]=oracle(c,p)
|
||||
for _ in range(2):
|
||||
f=np.array([np.roll(f[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(old.E)])
|
||||
f[:,p.fine_kind==OUTER_GHOST]=np.nan
|
||||
f[:,p.fine_kind==ACTIVE]=old.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(old.E)])
|
||||
out.reshape(-1)[p.coalesce_coarse]=f.reshape(-1)[p.coalesce_fine].mean(axis=1)
|
||||
out[:,~p.covered]=old.collide(out[:,~p.covered],p.tau_coarse)
|
||||
out[:,p.covered]=np.nan; c=out
|
||||
np.testing.assert_allclose(old.totals(c,f,p),start,rtol=0,atol=4e-10)
|
||||
|
||||
def test_private_build_identity_cleanup_and_old_path(self):
|
||||
from CelerisLab.cuda import compiler_v2 as compiler
|
||||
from assess_projected_pairs import BUILD_SOURCES
|
||||
original=compiler.generate_config; report={}
|
||||
with candidate.private_build(self.h,report,candidate.VARIANT):
|
||||
for level,cfg in enumerate(self.h.configs):
|
||||
cfg=replace(cfg,nx=10,ny=10);cfg.validate(); build=compiler.generate_config(cfg)
|
||||
try:
|
||||
meta=json.loads((Path(build)/'.celeris-build').read_text())
|
||||
self.assertEqual(meta['source_sha256'],candidate.source_digest(build))
|
||||
self.assertEqual(meta['source_sha256'],candidate.BUILD_SHA256 if level else BUILD_SOURCES[128])
|
||||
if level:self.assertEqual(meta['experimental_reconstruction'],candidate.metadata(self.mask))
|
||||
finally:compiler.cleanup_build_dir(build)
|
||||
self.assertIs(compiler.generate_config,original)
|
||||
with candidate.private_build(None,{},'current'):
|
||||
self.assertIs(compiler.generate_config,original)
|
||||
self.assertEqual(hashlib.sha256(SOURCE.read_bytes()).hexdigest(),'668e5cf14e88bdcfa281c2fa6c75c0f8f8cdbbfb8ba2ca03064ae02ef7ad0057')
|
||||
|
||||
def mock_records(self):
|
||||
import argparse
|
||||
helper=legacy.ProbeTraceTests();helper.setUp()
|
||||
parse=argparse.ArgumentParser.parse_args
|
||||
records=[]
|
||||
for n,depth,variant in candidate.CASES:
|
||||
scheme='constant' if depth==1 else 'projected_linear'
|
||||
def parse_extra(parser,*a,**k):
|
||||
result=parse(parser,*a,**k);result.test_reconstruction=variant;return result
|
||||
def arrays(m,n,t):return [axis.initial(v.shape,l,n,.03*n/100) for l,v in enumerate(m)]
|
||||
with patch.object(argparse.ArgumentParser,'parse_args',parse_extra), \
|
||||
patch.object(helper,'exact_arrays',side_effect=arrays), \
|
||||
patch.object(candidate,'private_build',return_value=nullcontext()) as private:
|
||||
r,counts,_=helper.run_mocked(n,depth,candidate.DIAGNOSTIC,scheme,masks(n,depth),initializer=axis.initial)
|
||||
self.assertEqual(r['test_reconstruction'],variant)
|
||||
self.assertEqual(r['probe_trace']['steps'],list(range(9)))
|
||||
self.assertEqual([v['step'] for v in r['rows']],[0,8*(n//128)])
|
||||
self.assertLess(r['observed_shifted_max'],candidate.POP_ENVELOPE)
|
||||
self.assertEqual(counts[-1],8*(n//128));self.assertEqual(private.call_args.args[2],variant)
|
||||
records.append(r)
|
||||
return records
|
||||
|
||||
def test_four_mock_runs_admission_and_old_diagnostic_unchanged(self):
|
||||
self.assertEqual(len(self.mock_records()),4)
|
||||
self.assertEqual(hashlib.sha256(Path(candidate.__file__).read_bytes()).hexdigest(),rc.SHEAR_CENTERED_SHA256)
|
||||
from tests.validation.multigrid import check_projected_pairs as gate
|
||||
with patch.object(gate,'preflight',return_value=rc.NUMERICAL_REVISION), \
|
||||
patch.object(rc,'provenance',return_value=dict(source_tree_sha256=rc.SOURCE_SHA256)):
|
||||
rc.verify_source(REPO,'r','shear_centered',probes=True)
|
||||
with patch.object(candidate,'__file__','/stale/shear_centered.py'), self.assertRaises(RuntimeError):
|
||||
rc.verify_source(REPO,'r','shear_centered',probes=True)
|
||||
import argparse, io, sys
|
||||
from contextlib import redirect_stdout
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base=['run_case.py','--repo',str(REPO),'--expected-revision','f'*40,
|
||||
'--output',str(Path(tmp)/'r.json'),'--re','100','--n','128','--depth','2',
|
||||
'--flow','shear_wave','--probe-trace','--explosion','projected_linear','--dry-run']
|
||||
cases=[(['--diagnostic',candidate.DIAGNOSTIC],False),
|
||||
(['--diagnostic',rc.AXIS_SQUARE,'--test-reconstruction',candidate.VARIANT],False),
|
||||
(['--diagnostic',candidate.DIAGNOSTIC,'--test-reconstruction',candidate.VARIANT,'--depth','1'],False),
|
||||
(['--diagnostic',candidate.DIAGNOSTIC,'--test-reconstruction',candidate.VARIANT],True)]
|
||||
for extra,valid in cases:
|
||||
with patch.object(sys,'argv',base+extra), patch.object(rc,'verify_source',return_value={}) as verify, \
|
||||
patch.object(rc,'runtime_source',return_value={}), redirect_stdout(io.StringIO()), patch('sys.stderr',io.StringIO()):
|
||||
if valid:self.assertEqual(rc.main(),0)
|
||||
else:
|
||||
with self.assertRaises(SystemExit):rc.main()
|
||||
verify.assert_not_called()
|
||||
|
||||
def test_frozen_comparison_gates_and_corruptions(self):
|
||||
import test_projected_pairs as fixture
|
||||
records=self.mock_records(); source=fixture.source()
|
||||
for r in records:
|
||||
r.update(source=deepcopy(source),runtime_identity=deepcopy(fixture.RUNTIME),
|
||||
runtime_source=rc.runtime_source(REPO),cuda_visible_devices='0',
|
||||
actual_build_sidecars=[fixture.build_metadata(r['n']*2**l) for l in range(r['depth'])])
|
||||
records[3]['reconstruction']=candidate.metadata(self.mask)
|
||||
records[3]['actual_build_sidecars'][1].update(source_sha256=candidate.BUILD_SHA256,
|
||||
experimental_reconstruction=candidate.metadata(self.mask))
|
||||
def change(r,step,group,component,value):
|
||||
for i,p in enumerate(r['probe_trace']['probes']):
|
||||
if p['group']==group:r['probe_trace']['values'][step][3*i+component]=value
|
||||
for r,bottom,top in [(records[2],-1e-5,1e-6),(records[3],-1e-6,8e-7)]:
|
||||
change(r,1,'inner_bottom',1,bottom);change(r,1,'inner_top',1,top)
|
||||
for r in records:r["probe_trace"]["bytes"]=axis.validate(r["probe_trace"])
|
||||
result=candidate.compare(records,source)
|
||||
self.assertTrue(result['advance_to_review']);self.assertFalse(result['qualified'])
|
||||
np.testing.assert_allclose(result['moment_deadband'],[1.1925e-6,7.95e-7,7.95e-7],rtol=2e-16,atol=0)
|
||||
for group,step,value,check in [('inner_bottom',1,-9.8e-6,'bottom_step1_clear'),
|
||||
('inner_top',1,4e-6,'top_step1_nonworse'),('outer_top',8,4e-6,'top_step8_nonworse'),
|
||||
('inner_left',1,4e-6,'vertical_null')]:
|
||||
bad=deepcopy(records);change(bad[3],step,group,1,value)
|
||||
bad[3]['probe_trace']['bytes']=axis.validate(bad[3]['probe_trace'])
|
||||
self.assertFalse(candidate.compare(bad,source)['checks'][check])
|
||||
for mutation in [lambda r:r['source'].update(revision='wrong'),
|
||||
lambda r:r['runtime_identity'].update(device='wrong'),
|
||||
lambda r:r.update(viscosity=.1),lambda r:r.update(refinements=[]),
|
||||
lambda r:r.update(observed_shifted_max=.04),
|
||||
lambda r:r['actual_build_sidecars'][1].update(source_sha256='wrong'),
|
||||
lambda r:r['probe_trace']['values'][0].__setitem__(0,1.01),
|
||||
lambda r:r['probe_trace']['steps'].pop(),
|
||||
lambda r:r['probe_trace']['probes'][0].update(i=1)]:
|
||||
bad=deepcopy(records);mutation(bad[3])
|
||||
with self.assertRaises((ValueError,KeyError)):candidate.compare(bad,source)
|
||||
@@ -0,0 +1,554 @@
|
||||
"""Host geometry, receipt-corruption tests and mocked scheduling; no CUDA/flow."""
|
||||
|
||||
from contextlib import nullcontext, redirect_stdout
|
||||
from copy import deepcopy
|
||||
from dataclasses import asdict, replace
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
import run_case
|
||||
import assess_projected_pairs as assessor
|
||||
from assess_projected_pairs import (BUILD_SOURCES, CASES, NUMERICS, NUMERICAL_REVISION,
|
||||
SOURCE_SHA256, assess)
|
||||
from oracle import LIMITS, TIMES, U, rectangles
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3]
|
||||
OLD_REVISION = "519473a61c7b319183aa54e55b9f097499dde0bf"
|
||||
MOCK_REVISION = "f" * 40
|
||||
RUNTIME = dict(device=dict(name="host mock", pci_bus_id="mock", capability=[7, 0],
|
||||
driver=12040, runtime=[12, 4, 0]), nvcc_path="/mock/nvcc",
|
||||
nvcc_version="mock nvcc", python="mock Python", numpy=np.__version__, pycuda="mock")
|
||||
|
||||
|
||||
def source():
|
||||
return dict(revision=MOCK_REVISION, dirty=False, source_tree_sha256=SOURCE_SHA256,
|
||||
harness_sha256={"run_case.py": "a" * 64, "oracle.py": "c" * 64},
|
||||
build_capture_sha256=run_case.BUILD_CAPTURE_SHA256)
|
||||
|
||||
|
||||
def build_metadata(size):
|
||||
cfg = replace(run_case.config(size, 100), nx=10, ny=10)
|
||||
cfg.validate()
|
||||
return dict(source_sha256=BUILD_SOURCES[size], config=asdict(cfg), n_objects=0,
|
||||
arch="sm_70", flags=["--use_fast_math"], ptx_sha256="b" * 64)
|
||||
|
||||
|
||||
def records(early=False, diagnostic=None):
|
||||
from CelerisLab.lbm.hierarchy import build_hierarchy
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
|
||||
out = []
|
||||
for re, n, depth, scheme in CASES:
|
||||
if diagnostic:
|
||||
n *= 2
|
||||
cfg = run_case.config(n, re)
|
||||
refinements = [] if depth == 1 else rectangles(n, depth)
|
||||
h = build_hierarchy(cfg, refinements)
|
||||
main_steps, early_steps = run_case.sample_steps(n, early)
|
||||
out.append(dict(
|
||||
re_L=re, n=n, depth=depth, explosion=scheme, U=U, mach=float(U*np.sqrt(3)),
|
||||
viscosity=cfg.viscosity, tau_by_level=[0.5+3*c.viscosity for c in h.configs],
|
||||
refinements=refinements, numerics=NUMERICS.copy(), initialization=run_case.INITIALIZATION,
|
||||
source=source(), numerical_revision=NUMERICAL_REVISION,
|
||||
reference_harness=run_case.REFERENCE_HARNESS,
|
||||
actual_build_sidecars=[build_metadata(n * 2**level) for level in range(depth)],
|
||||
runtime_identity=deepcopy(RUNTIME), cuda_visible_devices="0",
|
||||
runtime_source=run_case.runtime_source(REPO), completed=True,
|
||||
workload=run_case.workload([k == ACTIVE for k in h.kinds], 16*n),
|
||||
rows=[dict(step=s, tstar=s*U/n, **{k: 0.0 for k in LIMITS}) for s in main_steps],
|
||||
early_pressure=dict(enabled=early, rows=[
|
||||
dict(step=s, tstar=s*U/n, pressure_l2=.01 if s not in main_steps else 0.0) for s in early_steps],
|
||||
sampled_max_pressure_l2=.01 if early else None)))
|
||||
if diagnostic:
|
||||
out[-1]["diagnostic"] = diagnostic
|
||||
return out
|
||||
|
||||
|
||||
class ProjectedPairsTests(unittest.TestCase):
|
||||
def test_exact_geometry_work_and_samples(self):
|
||||
from CelerisLab.lbm.hierarchy import build_hierarchy, leaf_partition
|
||||
|
||||
data = records(True)
|
||||
self.assertEqual(sum(r["workload"]["active_collision_updates"] for r in data), 589299712)
|
||||
for r in data:
|
||||
n, depth = r["n"], r["depth"]
|
||||
h = build_hierarchy(run_case.config(n, 100), r["refinements"])
|
||||
np.testing.assert_array_equal(leaf_partition(h), np.ones((n, n)))
|
||||
main, early = run_case.sample_steps(n, True)
|
||||
self.assertEqual(main, [0, 4*n, 8*n, 12*n, 16*n])
|
||||
self.assertEqual(early, [k*(n//16) for k in range(65)])
|
||||
np.testing.assert_allclose(np.array(early)*U/n, np.arange(65)*.001875, rtol=0, atol=3e-17)
|
||||
if depth == 2:
|
||||
self.assertEqual(r["refinements"], [[(16, 16, 112, 96)]])
|
||||
self.assertTrue(assess(data)["passed"])
|
||||
self.assertTrue(assess(records())["passed"])
|
||||
|
||||
def test_exact_old519_geometry_and_table_parity(self):
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
from CelerisLab.lbm.hierarchy import build_hierarchy
|
||||
from CelerisLab.lbm.refinement_tables import projected_explosion_tables
|
||||
|
||||
# Frozen from executing exact 519473a61c7b319183aa54e55b9f097499dde0bf
|
||||
# geometry and table definitions, after checking unchanged config,
|
||||
# descriptors and blocks. No historical Git object is needed at runtime.
|
||||
# Historical source SHA256, refinement.py then refinement_tables.py:
|
||||
# 08c2a0557276a98cbcaa459d9bb7a9735d8bbbf64a6114c57af2fda61481a687
|
||||
# dabff948bb30496384d1a52c666b0edbc2085f9c61ed1bbc39f748502629113d
|
||||
frozen = {
|
||||
"covered": ("|b1", (128, 128), "14a1b5be54e3309bea825a00a653105951f0c805622b877a86dd453d65556df3"),
|
||||
"fine_kind": ("|u1", (256, 256), "891ff72c3d60686c62b1f35ffe59d054ac23b8f9d59aeb5782bc941202794cc0"),
|
||||
"ghost_fine": ("<u8", (2880,), "2d0311ac0feb1c0e1e2b6bcf806fe616b8217513de218407267dcd7652287579"),
|
||||
"ghost_coarse": ("<u4", (2880,), "5a3b305a0f841c8d05a13f105fda02b43ef875ab1321f9bca579f2b8ae313233"),
|
||||
"coalesce_coarse": ("<u8", (1060,), "a1d88c60f282153dcf2bb497c59d777968b6db773d061d8fea08163c98155967"),
|
||||
"coalesce_fine": ("<u8", (1060, 4), "08914c5d17c1ec8e2bfd234f581e4b3283de26296ae7d80403385feed7b89532"),
|
||||
"neighbors": ("<u8", (2880, 4), "cfef3a07103241e3dfff24fd65ab967afcf20520f699574b191044d47cf5d1f4"),
|
||||
"offsets": ("<f4", (2880, 2), "c98ba658d5f2cc18605e029fbe1da448ded3fd2436edcaf8ea097fa46aeb1f2d"),
|
||||
}
|
||||
hierarchy = build_hierarchy(run_case.config(128, 100), rectangles(128, 2))
|
||||
pair = hierarchy.pairs[0]
|
||||
self.assertEqual((pair.coarse_shape, pair.fine_shape, pair.fine_origin),
|
||||
((128, 128), (256, 256), (0.0, 0.0)))
|
||||
self.assertEqual(len(np.unique(pair.ghost_coarse)), 720)
|
||||
self.assertTrue((hierarchy.kinds[0].ravel()[pair.ghost_coarse] == 0).all())
|
||||
layout = BlockLayout2D.build(128, 128, 8, 8, periodic=(True, True))
|
||||
neighbors, offsets = projected_explosion_tables(pair, layout, hierarchy.kinds[0])
|
||||
arrays = dict(neighbors=neighbors, offsets=offsets)
|
||||
arrays.update({name: getattr(pair, name) for name in frozen if name not in arrays})
|
||||
for name, array in arrays.items():
|
||||
with self.subTest(array=name):
|
||||
self.assertEqual((array.dtype.str, array.shape, hashlib.sha256(array.tobytes()).hexdigest()),
|
||||
frozen[name])
|
||||
|
||||
def test_actual_build_capture_rejects_tampered_ptx_and_sidecar(self):
|
||||
from CelerisLab.cuda import compiler_v2
|
||||
from tests.validation.multigrid.check_projected_pairs import capture_builds
|
||||
|
||||
for tamper in (None, "ptx", "sidecar"):
|
||||
with self.subTest(tamper=tamper), tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "blocks.ptx"
|
||||
path.write_bytes(b"host-only mock PTX")
|
||||
metadata = dict(ptx_sha256=hashlib.sha256(path.read_bytes()).hexdigest())
|
||||
if tamper == "ptx":
|
||||
path.write_bytes(b"tampered")
|
||||
if tamper == "sidecar":
|
||||
metadata["ptx_sha256"] = "0" * 64
|
||||
Path(str(path) + ".json").write_text(json.dumps(metadata))
|
||||
report = {"actual_build_sidecars": []}
|
||||
with patch.object(compiler_v2, "load_module", return_value="mock-module") as load:
|
||||
with capture_builds(report, lambda: None):
|
||||
if tamper:
|
||||
with self.assertRaisesRegex(AssertionError, "PTX and build sidecar"):
|
||||
compiler_v2.load_module(str(path))
|
||||
load.assert_not_called()
|
||||
self.assertEqual(report["actual_build_sidecars"], [])
|
||||
else:
|
||||
self.assertEqual(compiler_v2.load_module(str(path)), "mock-module")
|
||||
self.assertEqual(report["actual_build_sidecars"], [metadata])
|
||||
self.assertIs(compiler_v2.load_module, load)
|
||||
|
||||
def test_frozen_formula_and_finest_control(self):
|
||||
data = records(True)
|
||||
data[0]["rows"][2]["velocity_l2"] = .002
|
||||
data[0]["rows"][0]["velocity_l2"] = .04 # retained absolute gate, excluded from worst
|
||||
for r in data[2:]:
|
||||
for row in r["rows"][1:]:
|
||||
row.update(velocity_l2=.0039, pressure_l2=.02, energy_relative=.004)
|
||||
r["early_pressure"]["rows"][-1]["pressure_l2"] = .02
|
||||
r["early_pressure"]["sampled_max_pressure_l2"] = .02
|
||||
data[1]["rows"][2]["velocity_l2"] = .00001
|
||||
data[3]["early_pressure"]["rows"][7]["pressure_l2"] = .9
|
||||
data[3]["early_pressure"]["sampled_max_pressure_l2"] = .9
|
||||
result = assess(data)
|
||||
self.assertTrue(result["passed"])
|
||||
velocity = result["comparisons"][0]
|
||||
self.assertEqual(velocity["baseline_budget"], .004)
|
||||
self.assertAlmostEqual(velocity["amr_to_uniform256"], 390)
|
||||
self.assertEqual(result["early_pressure"][-1]["sampled_max_pressure_l2"], .9)
|
||||
data[3]["rows"][-1]["pressure_l2"] = .02001
|
||||
result = assess(data)
|
||||
self.assertFalse(result["passed"])
|
||||
self.assertEqual(result["comparisons"][4]["amr"], .02001)
|
||||
|
||||
def test_scheme_verdicts_are_independent(self):
|
||||
data = records()
|
||||
data[2]["rows"][-1]["pressure_l2"] = .03
|
||||
result = assess(data)
|
||||
self.assertFalse(result["passed"])
|
||||
self.assertFalse(result["scheme_results"]["constant"]["passed"])
|
||||
self.assertTrue(result["scheme_results"]["projected_linear"]["passed"])
|
||||
|
||||
def test_corruptions_cannot_pass(self):
|
||||
good = records(True)
|
||||
corruptions = [
|
||||
lambda r: r.update(completed=False),
|
||||
lambda r: r["rows"].pop(),
|
||||
lambda r: r["rows"][-1].update(pressure_l2=float("nan")),
|
||||
lambda r: r["rows"][-1].update(energy_relative=float("inf")),
|
||||
lambda r: r["rows"][-1].update(pressure_l2=1e308),
|
||||
lambda r: r["rows"][-1].update(velocity_l2=-1),
|
||||
lambda r: r["rows"][-1].update(step=2047),
|
||||
lambda r: r["rows"][-1].update(tstar=.4801),
|
||||
lambda r: r["source"].update(dirty=True),
|
||||
lambda r: r["source"].update(source_tree_sha256="wrong"),
|
||||
lambda r: r["source"].update(revision=OLD_REVISION),
|
||||
lambda r: r["source"].pop("build_capture_sha256"),
|
||||
lambda r: r.update(numerical_revision=OLD_REVISION),
|
||||
lambda r: r.update(reference_harness="wrong"),
|
||||
lambda r: r.pop("runtime_identity"),
|
||||
lambda r: r["runtime_identity"].pop("nvcc_version"),
|
||||
lambda r: r["runtime_identity"].update(device="wrong"),
|
||||
lambda r: r["runtime_identity"]["device"].update(capability=[8, 0]),
|
||||
lambda r: r.update(cuda_visible_devices="0,1"),
|
||||
lambda r: r.pop("actual_build_sidecars"),
|
||||
lambda r: r["actual_build_sidecars"].pop(),
|
||||
lambda r: r["actual_build_sidecars"].reverse(),
|
||||
lambda r: r["actual_build_sidecars"][0].update(source_sha256="c" * 64),
|
||||
lambda r: r["actual_build_sidecars"][0].update(arch="sm_80"),
|
||||
lambda r: r["actual_build_sidecars"][0].update(flags=[]),
|
||||
lambda r: r["actual_build_sidecars"][0].update(ptx_sha256="not-a-hash"),
|
||||
lambda r: r["actual_build_sidecars"][0]["config"].update(ddf_shifting=False),
|
||||
lambda r: r["source"]["harness_sha256"].update(extra="mixed"),
|
||||
lambda r: r.update(viscosity=.01),
|
||||
lambda r: r["tau_by_level"].reverse(),
|
||||
lambda r: r["numerics"].update(ddf_shifting=False),
|
||||
lambda r: r.update(initialization="different equilibrium"),
|
||||
lambda r: r.update(refinements=[[(16, 16, 112, 104)]]),
|
||||
lambda r: r["workload"].update(active_collision_updates=0),
|
||||
lambda r: r["runtime_source"].update(kernel_root="/wrong/installed/tree"),
|
||||
lambda r: r["early_pressure"]["rows"].pop(),
|
||||
lambda r: r["early_pressure"]["rows"][8].update(step=65),
|
||||
lambda r: r["early_pressure"]["rows"][-1].update(pressure_l2=.005),
|
||||
lambda r: r["early_pressure"]["rows"][8].update(pressure_l2=float("nan")),
|
||||
lambda r: r["early_pressure"].update(sampled_max_pressure_l2=.5),
|
||||
lambda r: r["early_pressure"].update(enabled=False),
|
||||
]
|
||||
for mutation in corruptions:
|
||||
bad = deepcopy(good)
|
||||
mutation(bad[-1])
|
||||
with self.subTest(mutation=mutation):
|
||||
result = assess(bad)
|
||||
self.assertFalse(result["passed"])
|
||||
json.dumps(result, allow_nan=False) # failure summary itself remains serializable
|
||||
self.assertTrue(assess(good, expected_revision=MOCK_REVISION)["passed"])
|
||||
self.assertFalse(assess(good, expected_revision=OLD_REVISION)["passed"])
|
||||
old = deepcopy(good)
|
||||
for record in old:
|
||||
record["source"].update(revision=OLD_REVISION,
|
||||
source_tree_sha256="766e173df81d2881d3871007f15d5e321f5c7205ae578b0f80b43d1e7fe3e220")
|
||||
self.assertFalse(assess(old)["passed"])
|
||||
self.assertFalse(assess(good[:-1])["passed"])
|
||||
self.assertFalse(assess(good + [good[-1]])["passed"])
|
||||
self.assertFalse(assess([])["passed"])
|
||||
incomplete = deepcopy(good)
|
||||
incomplete[-1]["completed"] = False
|
||||
self.assertFalse(assess(incomplete)["comparisons"][3]["passed"])
|
||||
|
||||
def test_pinned_build_sources_match_host_generated_inputs(self):
|
||||
from CelerisLab.cuda import compiler_v2
|
||||
|
||||
for size in (128, 256, 512):
|
||||
cfg = replace(run_case.config(size, 100), nx=10, ny=10)
|
||||
cfg.validate()
|
||||
root = compiler_v2.generate_config(cfg)
|
||||
try:
|
||||
actual = json.loads((Path(root) / ".celeris-build").read_text())
|
||||
expected = build_metadata(size)
|
||||
self.assertEqual(actual, {key: expected[key] for key in
|
||||
("source_sha256", "config", "n_objects")})
|
||||
finally:
|
||||
compiler_v2.cleanup_build_dir(root)
|
||||
|
||||
def test_scaled_geometry_work_and_frozen_gates(self):
|
||||
data = records(True, run_case.SCALED_DIAGNOSTIC)
|
||||
self.assertEqual(sum(r["workload"]["active_collision_updates"] for r in data), 4714397696)
|
||||
self.assertEqual([r["viscosity"] for r in data], [.0768, .1536, .0768, .0768])
|
||||
self.assertEqual(data[-1]["tau_by_level"], [.7303999999999999, .9608])
|
||||
for r in data:
|
||||
self.assertEqual([row["tstar"] for row in r["rows"]], TIMES)
|
||||
self.assertEqual(len(r["early_pressure"]["rows"]), 65)
|
||||
if r["depth"] == 2:
|
||||
self.assertEqual(r["refinements"], [[(32, 32, 224, 192)]])
|
||||
self.assertEqual(r["workload"]["active_cells_by_level"], [34816, 122880])
|
||||
self.assertEqual(r["workload"]["level_hops"], [4096, 8192])
|
||||
result = assess(data, MOCK_REVISION, diagnostic=run_case.SCALED_DIAGNOSTIC)
|
||||
self.assertTrue(result["passed"])
|
||||
self.assertEqual(result["floors"], dict(velocity_l2=.002, pressure_l2=.02, energy_relative=.004))
|
||||
self.assertTrue(all(row["floor_dominates"] for row in result["comparisons"]))
|
||||
data[-1]["rows"][-1]["velocity_l2"] = .00200001
|
||||
self.assertFalse(assess(data, diagnostic=run_case.SCALED_DIAGNOSTIC)["passed"])
|
||||
self.assertFalse(assess(data)["passed"])
|
||||
data[-1]["rows"][-1]["velocity_l2"] = 0.0
|
||||
for mutate in (lambda r: r.pop("diagnostic"), lambda r: r.update(diagnostic="wrong"),
|
||||
lambda r: r["source"].update(revision="wrong"),
|
||||
lambda r: r["early_pressure"].update(enabled=False),
|
||||
lambda r: r.update(refinements=[[(32, 32, 224, 200)]])):
|
||||
bad = deepcopy(data)
|
||||
mutate(bad[-1])
|
||||
self.assertFalse(assess(bad, MOCK_REVISION, diagnostic=run_case.SCALED_DIAGNOSTIC)["passed"])
|
||||
|
||||
def test_raw_resolution_changes_and_reference_identity(self):
|
||||
old, new = records(True), records(True, run_case.SCALED_DIAGNOSTIC)
|
||||
for r in old:
|
||||
r["source"]["revision"] = assessor.REFERENCE_REVISION
|
||||
old[-2]["rows"][-1]["velocity_l2"] = .004 # historical constant failure remains usable
|
||||
old[-1]["rows"][-1]["velocity_l2"] = .001
|
||||
new[-1]["rows"][-1]["velocity_l2"] = .0015 # floor pass, but worse raw error
|
||||
result = assessor.resolution_trend(new, old, MOCK_REVISION)
|
||||
self.assertTrue(result["valid"])
|
||||
self.assertTrue(result["trend_interpretation_ready"])
|
||||
self.assertFalse(result["reference_scheme_results"]["constant"]["passed"])
|
||||
self.assertEqual((len(result["per_time"]), len(result["worst_post_initial"]),
|
||||
len(result["uniform256_bridge"])), (80, 16, 20))
|
||||
row = next(r for r in result["per_time"] if r["role"] == "projected_linear"
|
||||
and r["metric"] == "velocity_l2" and r["tstar"] == .48)
|
||||
self.assertEqual((row["signed_change"], row["new_to_old"], row["change_percent"]), (.0005, 1.5, 50.))
|
||||
self.assertTrue(assess(new, diagnostic=run_case.SCALED_DIAGNOSTIC)["passed"])
|
||||
self.assertIsNone(result["per_time"][0]["new_to_old"]) # zero denominator is explicit
|
||||
self.assertEqual(result["per_time"][0]["ratio_unavailable_reason"], "zero historical error")
|
||||
new[-1]["rows"][-1]["velocity_l2"] = .06 # retain raw errors even when absolute screen fails
|
||||
self.assertTrue(assessor.resolution_trend(new, old, MOCK_REVISION)["valid"])
|
||||
self.assertFalse(assess(new, diagnostic=run_case.SCALED_DIAGNOSTIC)["passed"])
|
||||
new[-1]["rows"][-1]["velocity_l2"] = .0015
|
||||
for sample in ("rows", "early_pressure"):
|
||||
bad = deepcopy(new)
|
||||
if sample == "rows":
|
||||
bad[0]["rows"][-1]["velocity_l2"] = .0001
|
||||
else:
|
||||
bad[0]["early_pressure"]["rows"][1]["pressure_l2"] = .009
|
||||
result = assessor.resolution_trend(bad, old, MOCK_REVISION)
|
||||
self.assertTrue(result["valid"])
|
||||
self.assertFalse(result["trend_interpretation_ready"])
|
||||
for mutate in (lambda r: r["source"].update(revision=MOCK_REVISION),
|
||||
lambda r: r["source"].update(source_tree_sha256="wrong"),
|
||||
lambda r: r["source"]["harness_sha256"].update(**{"oracle.py": "wrong"}),
|
||||
lambda r: r.update(diagnostic=run_case.SCALED_DIAGNOSTIC),
|
||||
lambda r: r.update(refinements=[]), lambda r: r["rows"].pop()):
|
||||
bad = deepcopy(old)
|
||||
mutate(bad[-1])
|
||||
self.assertFalse(assessor.resolution_trend(new, bad, MOCK_REVISION)["valid"])
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
paths = [Path(tmp) / f"{i}.json" for i in range(4)]
|
||||
for path, receipt in zip(paths, old):
|
||||
path.write_text(json.dumps(receipt))
|
||||
with self.assertRaisesRegex(ValueError, "receipt hashes"):
|
||||
assessor.load_reference(paths)
|
||||
witnesses = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in paths}
|
||||
with patch.object(assessor, "REFERENCE_RECEIPTS", witnesses):
|
||||
self.assertEqual(assessor.load_reference(paths), json.loads(json.dumps(old)))
|
||||
with self.assertRaises(ValueError):
|
||||
assessor.load_reference(paths[:-1] + paths[:1])
|
||||
paths[-1].write_text(paths[-1].read_text() + " ")
|
||||
with self.assertRaises(ValueError):
|
||||
assessor.load_reference(paths)
|
||||
|
||||
def test_scaled_runner_requires_explicit_bounded_diagnostic(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base = ["run_case.py", "--repo", str(REPO), "--expected-revision", MOCK_REVISION,
|
||||
"--output", str(Path(tmp) / "case.json"), "--dry-run"]
|
||||
diagnostic = ["--diagnostic", run_case.SCALED_DIAGNOSTIC, "--early-pressure"]
|
||||
for n, depth, re, extra, valid in (
|
||||
(512, 1, 100, [], False), (512, 1, 100, diagnostic, True),
|
||||
(256, 2, 100, diagnostic, True), (128, 2, 100, diagnostic, False),
|
||||
(256, 3, 100, diagnostic, False), (256, 2, 1000, diagnostic, False),
|
||||
(256, 2, 100, diagnostic[:2], False), (512, 2, 100, diagnostic, False)):
|
||||
argv = base + ["--n", str(n), "--depth", str(depth), "--re", str(re)] + extra
|
||||
with self.subTest(argv=argv), patch.object(sys, "argv", argv), \
|
||||
patch.object(run_case, "verify_source", return_value=source()) as verify, \
|
||||
redirect_stdout(io.StringIO()), patch("sys.stderr", io.StringIO()):
|
||||
if valid:
|
||||
self.assertEqual(run_case.main(), 0)
|
||||
else:
|
||||
with self.assertRaises(SystemExit) as caught:
|
||||
run_case.main()
|
||||
self.assertEqual(caught.exception.code, 2)
|
||||
verify.assert_not_called()
|
||||
|
||||
def test_source_preflight_requires_clean_exact_revision_and_tree(self):
|
||||
import oracle
|
||||
from tests.validation.multigrid import check_projected_pairs as gate
|
||||
|
||||
for module in (run_case, oracle):
|
||||
with self.subTest(imported_module=module.__name__), \
|
||||
patch.object(module, "__file__", "/wrong/checkout/" + Path(module.__file__).name), \
|
||||
patch.object(gate, "preflight") as preflight:
|
||||
with self.assertRaisesRegex(RuntimeError, "Wrong runner or oracle checkout"):
|
||||
run_case.verify_source(REPO, MOCK_REVISION)
|
||||
preflight.assert_not_called()
|
||||
for expected, status, diff, fingerprint in (
|
||||
(MOCK_REVISION, "", 0, SOURCE_SHA256),
|
||||
(OLD_REVISION, "", 0, SOURCE_SHA256),
|
||||
(MOCK_REVISION, " M tracked.py", 0, SOURCE_SHA256),
|
||||
(MOCK_REVISION, "", 1, SOURCE_SHA256),
|
||||
(MOCK_REVISION, "", 0, "wrong")):
|
||||
provenance = dict(source(), source_tree_sha256=fingerprint)
|
||||
with self.subTest(expected=expected, status=status, diff=diff, fingerprint=fingerprint), \
|
||||
patch.object(gate.subprocess, "check_output", side_effect=[MOCK_REVISION, status]), \
|
||||
patch.object(gate.subprocess, "call", return_value=diff), \
|
||||
patch.object(run_case, "provenance", return_value=provenance):
|
||||
if expected != MOCK_REVISION or status or diff or fingerprint != SOURCE_SHA256:
|
||||
with self.assertRaises((ValueError, RuntimeError)):
|
||||
run_case.verify_source(REPO, expected)
|
||||
else:
|
||||
self.assertEqual(run_case.verify_source(REPO, expected), source())
|
||||
|
||||
def test_explicit_opt_in_selector_and_dry_run_without_cuda(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
output = Path(tmp) / "case.json"
|
||||
base = ["run_case.py", "--repo", str(REPO), "--re", "100", "--n", "128",
|
||||
"--depth", "1", "--output", str(output)]
|
||||
pinned = base + ["--expected-revision", MOCK_REVISION]
|
||||
for argv, device in ((base + ["--run-gpu"], "0"), (pinned, "0"),
|
||||
(pinned + ["--run-gpu"], ""), (pinned + ["--run-gpu"], "0,1"),
|
||||
(pinned + ["--run-gpu"], "-1"),
|
||||
(pinned + ["--run-gpu", "--explosion", "projected"], "0"),
|
||||
(pinned + ["--run-gpu", "--explosion", "projected_linear"], "0")):
|
||||
with self.subTest(argv=argv, device=device), patch.object(sys, "argv", argv), \
|
||||
patch.dict("os.environ", CUDA_VISIBLE_DEVICES=device), \
|
||||
patch.object(run_case, "verify_source") as verify, patch("sys.stderr", io.StringIO()):
|
||||
with self.assertRaises(SystemExit) as caught:
|
||||
run_case.main()
|
||||
self.assertEqual(caught.exception.code, 2)
|
||||
verify.assert_not_called()
|
||||
self.assertFalse(output.exists())
|
||||
stdout = io.StringIO()
|
||||
cuda_modules = {name: None for name in ("pycuda", "CelerisLab.cuda.context",
|
||||
"CelerisLab.lbm.block_stepper", "CelerisLab.lbm.refinement_stepper")}
|
||||
with patch.object(sys, "argv", pinned + ["--dry-run"]), \
|
||||
patch.dict(sys.modules, cuda_modules), patch.dict("os.environ", CUDA_VISIBLE_DEVICES=""), \
|
||||
patch.object(run_case, "verify_source", return_value=source()) as verify, \
|
||||
patch.object(run_case, "runtime_identity", side_effect=AssertionError("CUDA reached")), \
|
||||
redirect_stdout(stdout):
|
||||
self.assertEqual(run_case.main(), 0)
|
||||
verify.assert_called_once_with(REPO, MOCK_REVISION, "taylor_green", probes=False)
|
||||
self.assertTrue(json.loads(stdout.getvalue())["dry_run"])
|
||||
self.assertFalse(output.exists())
|
||||
|
||||
def test_runtime_rejects_installed_kernel_tree(self):
|
||||
from CelerisLab.cuda import compiler_v2
|
||||
with patch.object(compiler_v2, "_KERNEL_DIR", "/other/CelerisLab/lbm/kernels"):
|
||||
with self.assertRaisesRegex(RuntimeError, "CUDA source/include root"):
|
||||
run_case.runtime_source(REPO)
|
||||
|
||||
def test_existing_output_is_never_overwritten(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
output = Path(tmp) / "failed.json"
|
||||
output.write_text('{"completed": false}\n')
|
||||
argv = ["run_case.py", "--repo", str(REPO), "--re", "100", "--n", "128",
|
||||
"--depth", "1", "--output", str(output),
|
||||
"--expected-revision", MOCK_REVISION, "--run-gpu"]
|
||||
with patch.object(sys, "argv", argv), patch("sys.stderr", io.StringIO()):
|
||||
with self.assertRaises(SystemExit) as caught:
|
||||
run_case.main()
|
||||
self.assertEqual(caught.exception.code, 2)
|
||||
self.assertEqual(output.read_text(), '{"completed": false}\n')
|
||||
result = subprocess.run([sys.executable, str(Path(__file__).with_name("assess_projected_pairs.py")),
|
||||
"unused.json", "--expected-revision", MOCK_REVISION,
|
||||
"--output", str(output)], capture_output=True)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertEqual(output.read_text(), '{"completed": false}\n')
|
||||
|
||||
def test_mocked_runner_default_early_and_failure(self):
|
||||
# Replace the GPU context and steppers before import. No CUDA module or flow is used.
|
||||
cases = [(early, depth, fail, 128, None, "projected_linear" if early else "constant")
|
||||
for early, depth, fail in
|
||||
((False, 1, None), (False, 2, None), (True, 2, None),
|
||||
(True, 2, "resource"), (True, 2, "nonfinite"),
|
||||
(True, 2, "probe_nonfinite"))]
|
||||
cases += [(True, depth, None, n, run_case.SCALED_DIAGNOSTIC, scheme)
|
||||
for n, depth, scheme in ((256, 1, "constant"), (512, 1, "constant"),
|
||||
(256, 2, "constant"), (256, 2, "projected_linear"))]
|
||||
for early, depth, fail, n, diagnostic, scheme in cases:
|
||||
calls, selectors = [], []
|
||||
|
||||
class FakeRun:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.step_count = 0
|
||||
if "coarse_boundary" in kwargs:
|
||||
selectors.append(kwargs["explosion"])
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
def initialize(self, *arrays):
|
||||
pass
|
||||
|
||||
def step(self, count):
|
||||
self.assertion = count >= 0
|
||||
if not self.assertion:
|
||||
raise AssertionError("backward step")
|
||||
if fail == "resource" and self.step_count >= 16:
|
||||
raise MemoryError("mock resource failure")
|
||||
self.step_count += count
|
||||
calls.append(self.step_count)
|
||||
|
||||
def download(self):
|
||||
return None
|
||||
|
||||
modules = {
|
||||
"CelerisLab.cuda.context": SimpleNamespace(CudaContext=FakeRun),
|
||||
"CelerisLab.lbm.block_stepper": SimpleNamespace(PeriodicBlockStepper=FakeRun),
|
||||
"CelerisLab.lbm.refinement_stepper": SimpleNamespace(FixedPatchStepper=FakeRun),
|
||||
}
|
||||
|
||||
def metric(arrays, masks, step, n, nu, mass):
|
||||
return dict(step=step, tstar=step*U/n, mass=16384.,
|
||||
**{k: float("nan") if ((fail == "nonfinite" and step == 16)
|
||||
or (fail == "probe_nonfinite" and step == 1))
|
||||
and k == "energy_relative" else 0.0 for k in LIMITS})
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
output = Path(tmp) / "case.json"
|
||||
argv = ["run_case.py", "--repo", str(REPO), "--re", "100", "--n", str(n),
|
||||
"--depth", str(depth), "--output", str(output),
|
||||
"--expected-revision", MOCK_REVISION, "--run-gpu"]
|
||||
argv += ["--explosion", scheme]
|
||||
if early:
|
||||
argv += ["--early-pressure"]
|
||||
if diagnostic:
|
||||
argv += ["--diagnostic", diagnostic]
|
||||
with patch.dict(sys.modules, modules), patch.object(sys, "argv", argv), \
|
||||
patch.dict("os.environ", CUDA_VISIBLE_DEVICES="mock"), \
|
||||
patch.object(run_case, "verify_source", return_value=source()) as verify, \
|
||||
patch.object(run_case, "runtime_identity", return_value=deepcopy(RUNTIME)), \
|
||||
patch.object(run_case, "build_capture", return_value=nullcontext()), \
|
||||
patch.object(run_case, "metrics", side_effect=metric), redirect_stdout(io.StringIO()):
|
||||
if fail:
|
||||
with self.assertRaises(MemoryError if fail == "resource" else FloatingPointError):
|
||||
run_case.main()
|
||||
else:
|
||||
self.assertEqual(run_case.main(), 0)
|
||||
receipt = json.loads(output.read_text())
|
||||
verify.assert_called_once_with(REPO, MOCK_REVISION, "taylor_green", probes=False)
|
||||
self.assertEqual(selectors, [scheme] if depth == 2 else [])
|
||||
self.assertEqual(receipt["source"], source())
|
||||
self.assertEqual(receipt["runtime_identity"], RUNTIME)
|
||||
self.assertEqual(receipt["completed"], not fail)
|
||||
self.assertEqual(receipt.get("diagnostic"), diagnostic)
|
||||
self.assertEqual(calls[:2], [0, 1]) # READY probe is part of this trajectory
|
||||
if not fail:
|
||||
self.assertEqual([r["tstar"] for r in receipt["rows"]], TIMES)
|
||||
self.assertEqual(len(receipt["early_pressure"]["rows"]), 65 if early else 0)
|
||||
self.assertEqual(calls[-1], 16*n)
|
||||
else:
|
||||
self.assertEqual(len(receipt["rows"]), 1)
|
||||
self.assertEqual(len(receipt["early_pressure"]["rows"]),
|
||||
{"resource": 3, "nonfinite": 2, "probe_nonfinite": 1}[fail])
|
||||
if fail == "probe_nonfinite":
|
||||
self.assertNotIn("ready_after_step", receipt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user