Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f287b460fd | ||
|
|
90694e69af | ||
|
|
707afe6bb2 | ||
|
|
26d0d3853c | ||
|
|
e8b8c58995 | ||
|
|
c2496c2103 | ||
|
|
1779cf29f2 | ||
|
|
5dcd3e0dbe | ||
|
|
d61560d98a | ||
|
|
3496a1e3c1 | ||
|
|
b983c1d095 | ||
|
|
fc0e544c0a | ||
|
|
95cb6d2b9f | ||
|
|
53fad76759 | ||
|
|
8403956a9a | ||
|
|
185a919cc5 | ||
|
|
aceea293a3 | ||
|
|
a233c90324 | ||
|
|
f851aecf73 | ||
|
|
51202fc1c3 | ||
|
|
d930323479 | ||
|
|
6c53d2e74a | ||
|
|
724afb607c | ||
|
|
77bab1c4ea | ||
|
|
15fb58e36f | ||
|
|
4d4b9ba00a | ||
|
|
2abc48f0d8 | ||
|
|
3ea131e92c | ||
|
|
00d7a54829 |
@@ -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,34 @@
|
||||
# MRT reuse in blocked/refined stepping (candidate)
|
||||
|
||||
Base `9805383`. Opt-in by `LBMConfig(collision="MRT")`; SRT remains the default and its
|
||||
kernel statements, including the shifted retained-delta/direct-momentum path, are unchanged
|
||||
under the alternate compile branch.
|
||||
|
||||
## What changes
|
||||
- `blocks.cu` (`BlockStep`), `step/block_channel.cu` (`BlockChannelStep`) and `refinement.cu`
|
||||
(`patch_collide`) gain a compile-time `COLLISION_MODEL == 2` branch that repeats the uniform
|
||||
`collide_dispatch` sequence with zero forcing: `compute_rho_u`, omega clamp to
|
||||
`[OMEGA_COLLISION_MIN, OMEGA_COLLISION_MAX]`, `collide_mrt_no_force`.
|
||||
- Kernel and host guards accept SRT or MRT only. TRT, LES, non-FP32 storage, non-double-buffer
|
||||
streaming, and shifted dense/mixed refinement remain rejected.
|
||||
- Transfers, subcycling, BFL, raw exchange ledgers and channel closures are untouched. The
|
||||
existing pressure-NEQ outlet selects its MRT branch (unknowns-only NEQ extrapolation) from
|
||||
`COLLISION_MODEL`, so MRT channel parity must use uniform MRT as reference.
|
||||
|
||||
## Contracts (existing operator, not redefined here)
|
||||
- Shifted storage uses the existing MRT representation (equilibrium moments from `rho - 1`),
|
||||
not the SRT retained-delta/direct-momentum precision. Mass and momentum are conserved
|
||||
moments (relaxation rate 0).
|
||||
- Kinetic rates are the operator's fixed `s_e = s_eps = s_q = 1.2` on every level; the shear
|
||||
rate is the existing per-level omega. Under acoustic 2:1 scaling the physical bulk viscosity
|
||||
therefore halves on each finer level. No rate rescaling is applied.
|
||||
- No LES, no external forcing (`d_params` force fields stay zero).
|
||||
- Shifted Jets stay SRT-only: `body/jets.py` keeps its existing guard. This extension does not
|
||||
provide shifted MRT Jets.
|
||||
- `PeriodicBlockStepper` (uniform blocked stepper) remains SRT-only.
|
||||
|
||||
## Evidence status
|
||||
CPU: `tests/unit/test_refinement_mrt.py` (host guards, source contract). GPU:
|
||||
`tests/validation/multigrid/test_refinement_mrt_gpu.py` (uniform-MRT channel parity, AMR MRT
|
||||
smoke) and unchanged SRT GPU tests. Strict parity may be subject to cross-kernel FMA
|
||||
contraction differences already documented for SRT. Not a DFG/benchmark validation.
|
||||
@@ -0,0 +1,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,71 @@
|
||||
# Matched MRT DFG pilot preparation
|
||||
|
||||
Preparation only: this test-only extension reuses the existing runner and existing MRT
|
||||
operator. GPU admission awaits reviewed MRT capability results; no pilot/full execution
|
||||
is implied by CPU preflight. No production, historical SRT manifest, reference data,
|
||||
periodic gate, geometry, initialization or observation algorithm is changed here.
|
||||
|
||||
## Selection and provenance
|
||||
|
||||
Use `--collision MRT` for **both** uniform D80 and refined root D40/fine D80. There are
|
||||
no independently selectable collision models per case. Default remains SRT.
|
||||
|
||||
MRT verifies `data/refined_dfg/source_manifest_mrt.json`, pinned to reviewed numerical
|
||||
source snapshot `3ea131e92c057843034f11a1c574301a2430dfce`, including the shared geometry,
|
||||
pressure/cycle helpers and official reference. Test-only commits may change HEAD without
|
||||
changing this numerical source pin. Plan, READY, case summaries and comparison separately
|
||||
record `checkout_revision` and `checkout_dirty` using existing source-provenance helper;
|
||||
the runner hash remains separate. Collision is recorded in every receipt, and full-mode
|
||||
validation rejects missing or different collision labels in plan, either summary or either
|
||||
READY receipt, in addition to existing source/runner/health/time checks.
|
||||
|
||||
The historical SRT manifest remains byte-for-byte unchanged. SRT on the MRT candidate
|
||||
checkout deliberately fails that source pin; use a matching historical checkout via
|
||||
`--source` for SRT. No fallback or automatic repinning is permitted. Historical SRT
|
||||
failure/diagnostic evidence remains attached to its original source and runner hashes;
|
||||
it cannot authorize an MRT full run. The existing failed-observation step-reporting
|
||||
undercount is not changed in this narrowly scoped selection patch.
|
||||
|
||||
## Frozen physical comparison
|
||||
|
||||
All settings remain those documented in `refined_dfg.md`: U_lattice=.01, physical mean
|
||||
U=1 m/s, D=.1 m, shear ν=.001 m²/s, fixed physical domain/body/wall planes, patch
|
||||
(40,24,320,140), shifted FP32 double buffers, no LES/forcing/Jets, finest dx=.00125 m,
|
||||
finest dt=.0000125 s, unchanged half-cell frame conversion and axis-specific phase.
|
||||
Force interval accumulation, physical leaf mass, pressure interpolation, flux sampling,
|
||||
safety thresholds, t*=5 pilot and t*=300 cap are unchanged. Existing accuracy/periodicity
|
||||
criteria are imported unchanged. Both MRT cases are fresh runs; old MRT or SRT results
|
||||
are not substituted as controls.
|
||||
|
||||
Both cases use the **existing MRT pressure-NEQ branch**: only unknown directions q2/6/8
|
||||
are extrapolated. This differs from SRT's all-nine .5-damped branch; the retained
|
||||
`srt_neq_damp=.5` configuration value is inactive under MRT. Backflow clamp remains off.
|
||||
The outlet is still noncanonical relative to exact DFG do-nothing conditions.
|
||||
|
||||
The reused MRT operator keeps fixed kinetic rates `s_e=s_eps=s_q=1.2` on all levels;
|
||||
only the shear rate follows existing per-level viscosity. Physical bulk relaxation
|
||||
therefore differs between coarse and fine levels. Transfer operators and subcycling
|
||||
are unchanged. This is the candidate's declared behavior, not a new fully mode-scaled
|
||||
MRT transfer scheme. Do not tune rates, the patch or gates after seeing outcomes.
|
||||
Capability smoke/parity gates do not establish DFG accuracy or speedup.
|
||||
|
||||
## CPU and admission commands
|
||||
|
||||
From this checkout:
|
||||
|
||||
```sh
|
||||
python tests/unit/test_refined_dfg.py
|
||||
python tests/validation/run_refined_dfg.py --collision MRT --mode plan --output /tmp/new-mrt-dfg-plan
|
||||
```
|
||||
|
||||
After root verifies the candidate MRT root/parity/refinement capability results and admits
|
||||
an idle device, the paired pilot may use the existing bounds and external watchdog:
|
||||
|
||||
```sh
|
||||
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s <hard-budget>s python tests/validation/run_refined_dfg.py --collision MRT --mode pilot --gpu-gates-approved --max-wall-seconds <soft-budget> --output <new-mrt-pilot-directory>
|
||||
```
|
||||
|
||||
The flag is caller attestation, not evidence. An unhealthy first case prevents the second.
|
||||
Existing strict pilot receipt validation and calibrated wall-time controls remain; full
|
||||
mode is not being admitted as part of this preparation. Root must review both healthy
|
||||
pilot outcomes and separately decide any full-run admission.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Refined DFG MRT timing harness (measurement only)
|
||||
|
||||
`tests/validation/time_refined_dfg.py` times the matched MRT refined/uniform DFG Re100 setup of
|
||||
`run_refined_dfg.py` (same configuration, geometry and initial state functions). No numerical,
|
||||
physics-gate or production change; no force or accuracy result is produced.
|
||||
|
||||
| Variant | Loop | Force reads |
|
||||
|---|---|---|
|
||||
| A | refined `step(1)` + `read_raw_exchange` per base step | runner-like force-read workload (values discarded) |
|
||||
| B | refined `step(1)` only, no host reads (`observe_raw_exchange` stays on) | timing-only |
|
||||
| C | refined single `step(N)` | timing-only; ledger keeps the final base interval only |
|
||||
| E | uniform `sim.run(<=100)` + `read_force` | runner-like force-read workload (values discarded) |
|
||||
| E' | uniform single `sim.run(2N)` | timing-only; at most a whole-window average, different summation order |
|
||||
|
||||
- No variant retains a valid force history; A/E omit the runner's interval aggregation and
|
||||
validation, and no variant includes macro/probe/CSV costs. Results are not full benchmark
|
||||
throughput.
|
||||
- Status lines: `CONSTRUCTED` (construction/JIT complete) and `WARM_READY` (first warm-up and
|
||||
health check passed) per case.
|
||||
- Equal simulated duration: N refined base steps vs 2N uniform D80 steps.
|
||||
- Every repeat starts from the same fresh state (refined `initialize`, uniform `snapshot/restore`)
|
||||
and the same warm-up; construction/JIT, warm-up, downloads and parity checks are untimed.
|
||||
- The actual stepping stream is synchronized explicitly before and after each timed loop; the
|
||||
wall timer (`time.perf_counter`) is the only source. No CUDA-event span is called kernel time.
|
||||
- `--max-wall-seconds` is required for GPU runs (outer timeout, checked between variants and
|
||||
every 100 steps in per-step loops).
|
||||
- Final populations and NaN masks must be bitwise identical to the first A repeat (A/B/C) or the
|
||||
first E repeat (E/E'); refined and uniform grids are never compared. Leaf/fluid populations
|
||||
must be finite. Physical health (finite leaf/fluid macros, rho in [0.5, 1.5], max Mach <= 0.2,
|
||||
the existing runner limits) is checked after warm-up and after each timed window. Matching NaN
|
||||
is accepted only as a storage mask; any infinity or level-count mismatch fails parity. All
|
||||
parity/health checks are untimed.
|
||||
- Cases run sequentially (uniform, then refined) in one process with startup serialized;
|
||||
variants are interleaved within each repeat. A-B and B-C are measured comparative
|
||||
differences under GPU variability, not exact causal decompositions.
|
||||
|
||||
Usage (root selects one verified idle GPU):
|
||||
|
||||
PYTHONPATH=src python -m unittest tests/unit/test_time_refined_dfg.py
|
||||
python tests/validation/time_refined_dfg.py --output <new> --plan
|
||||
CUDA_VISIBLE_DEVICES=<gpu> python tests/validation/time_refined_dfg.py --output <new> --max-wall-seconds <s>
|
||||
|
||||
Defaults: 2000 base steps, 200 warm-up base steps, 3 repeats. Profiler capture and the
|
||||
`observe_raw_exchange=False` variant are deferred until these measurements exist.
|
||||
|
||||
## A versus observation-window ledger (`tests/validation/time_window_dfg.py`)
|
||||
|
||||
Measurement-only comparison on the same shifted-MRT refined setup: A (per-base `step(1)` +
|
||||
`read_raw_exchange`, summed per 80-base-step interval), W (`start_exchange_window`, `step(80)`,
|
||||
one window read) and W1 (window with 80 x `step(1)`, isolating the read pattern from call
|
||||
batching). Same fresh state, warm-up and simulated duration; populations must be bitwise equal
|
||||
to the first A repeat and per-interval total impulses within the documented engineering
|
||||
tolerance, all outside timing. The candidate source is recorded (checkout revision, dirty flag,
|
||||
file hashes); frozen refined-DFG manifests/results are not used or changed.
|
||||
|
||||
PYTHONPATH=src python -m unittest tests/unit/test_time_window_dfg.py
|
||||
python tests/validation/time_window_dfg.py --output <new> --plan
|
||||
CUDA_VISIBLE_DEVICES=<gpu> python tests/validation/time_window_dfg.py --output <new> --max-wall-seconds <s>
|
||||
@@ -0,0 +1,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.
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Host contract for the blocked root's uniform-SRT channel boundary.
|
||||
"""Host contract for the blocked root's uniform SRT/MRT channel boundary.
|
||||
|
||||
Boundary ghosts are evolved state, but are not physical fluid leaves. Halo
|
||||
ownership remains periodic to reproduce the uniform kernel's wrapped pulls.
|
||||
@@ -29,7 +29,7 @@ def validate_channel_config(cfg, blocks):
|
||||
def validate_channel_transfers(pair, layout, targets):
|
||||
"""Reject actual exchange/closure overlap, not a padded geometric envelope.
|
||||
|
||||
Explosion donors and restriction receivers must use ordinary bulk SRT
|
||||
Explosion donors and restriction receivers must use ordinary bulk collision
|
||||
semantics. Restriction writes receiver-local source slots; these may not
|
||||
overwrite the channel's extra immutable-input reads (pressure donor and
|
||||
same-node halfway-wall corrections). The slot check deliberately respects
|
||||
|
||||
@@ -14,12 +14,15 @@
|
||||
#include "core/obs.cuh"
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#if COLLISION_MODEL == 2
|
||||
#include "operators/collision_mrt.cuh"
|
||||
#endif
|
||||
#include "streaming/pull_double_buffer.cuh"
|
||||
#include "boundary/curved_boundary.cuh"
|
||||
#include "boundary/planar_velocity.cuh"
|
||||
|
||||
#if DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Blocks require FP32 D2Q9 SRT double buffers without LES"
|
||||
#if DIM != 2 || NQ != 9 || (COLLISION_MODEL != 0 && COLLISION_MODEL != 2) || STORE_PRECISION != 0 || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Blocks require FP32 D2Q9 SRT or MRT double buffers without LES"
|
||||
#endif
|
||||
|
||||
// Fixed-patch cell kinds, matching refinement.py ACTIVE/INNER_GHOST/OUTER_GHOST.
|
||||
@@ -141,6 +144,13 @@ extern "C" __global__ void BlockStep(const fpxx* in, fpxx* out, unsigned int hal
|
||||
gx, d_block_wall.nx, d_block_wall.slot_width, d_block_wall.amplitude));
|
||||
else if (gy == d_block_wall.ny - 2) reconstruct_top_reservoir(f, RHO);
|
||||
}
|
||||
#if COLLISION_MODEL == 2
|
||||
// Uniform MRT sequence (collide_dispatch, zero forcing); SRT path below unchanged.
|
||||
float rho, ux, uy;
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
collide_mrt_no_force(f, rho, ux, uy,
|
||||
fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega)));
|
||||
#else
|
||||
float rho, ux, uy, feq[NQ];
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
#if USE_DDF_SHIFTING
|
||||
@@ -153,6 +163,7 @@ extern "C" __global__ void BlockStep(const fpxx* in, fpxx* out, unsigned int hal
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
#endif
|
||||
collide_srt_no_force(f, feq, d_params.omega);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
stream_pull_store(k, f, out + base);
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// nu_t = (C_s * Delta)^2 * |S|, Delta = 1 (lattice unit)
|
||||
// omega_eff = 1 / (3*(nu + nu_t) + 0.5)
|
||||
//
|
||||
// |S| is estimated from non-equilibrium second-order moments using tau0.
|
||||
// |S| = sqrt(2 S_ij S_ij), with Delta = 1 in lattice units.
|
||||
// The stress estimate uses self-consistent tau_eff; tau0 is molecular.
|
||||
// Closed form uses full stress. Iteration uses deviatoric stress and a cap.
|
||||
// ============================================================================
|
||||
|
||||
#ifndef CELERIS_OPERATORS_TURBULENCE_SMAG_CUH
|
||||
@@ -65,12 +67,12 @@ __device__ __forceinline__ float compute_omega_smag(const float* __restrict__ f,
|
||||
|
||||
#if LES_CLOSED_FORM
|
||||
// Closed-form Smagorinsky (no iteration):
|
||||
// ω = 2 / (τ₀ + √(τ₀² + 18·Cs²·√Q / ρ))
|
||||
// ω = 2 / (τ₀ + √(τ₀² + 18·√2·Cs²·√Q / ρ))
|
||||
// where Q = Πneq_ij · Πneq_ij (Frobenius norm squared)
|
||||
const float Q = pixx*pixx + piyy*piyy + 2.0f*pixy*pixy;
|
||||
const float sqrtQ = sqrtf(fmaxf(Q, 0.0f));
|
||||
const float smag_const_sq = LES_CS * LES_CS;
|
||||
const float discriminant = tau0*tau0 + 18.0f * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float discriminant = tau0*tau0 + 18.0f * sqrtf(2.0f) * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float tau_eff = 0.5f * (tau0 + sqrtf(fmaxf(discriminant, tau0*tau0)));
|
||||
return clamp_omega(1.0f / fmaxf(tau_eff, 0.500001f));
|
||||
#else
|
||||
@@ -140,7 +142,7 @@ __device__ __forceinline__ float compute_omega_smag(const float* __restrict__ f,
|
||||
+ 2.0f*(pixy*pixy + pixz*pixz + piyz*piyz);
|
||||
const float sqrtQ = sqrtf(fmaxf(Q, 0.0f));
|
||||
const float smag_const_sq = LES_CS * LES_CS;
|
||||
const float discriminant = tau0*tau0 + 18.0f * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float discriminant = tau0*tau0 + 18.0f * sqrtf(2.0f) * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float tau_eff = 0.5f * (tau0 + sqrtf(fmaxf(discriminant, tau0*tau0)));
|
||||
return clamp_omega(1.0f / fmaxf(tau_eff, 0.500001f));
|
||||
#else
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#include "core/obs.cuh"
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#if COLLISION_MODEL == 2
|
||||
#include "operators/collision_mrt.cuh"
|
||||
#endif
|
||||
#include "streaming/pull_double_buffer.cuh"
|
||||
#include "boundary/planar_velocity.cuh"
|
||||
#include "boundary/curved_boundary.cuh"
|
||||
@@ -19,8 +22,8 @@ extern "C" {
|
||||
#include "step/curved_double.cu"
|
||||
}
|
||||
|
||||
#if DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Refinement requires unshifted FP32 D2Q9 SRT double buffers without LES"
|
||||
#if DIM != 2 || NQ != 9 || (COLLISION_MODEL != 0 && COLLISION_MODEL != 2) || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Refinement requires unshifted FP32 D2Q9 SRT or MRT double buffers without LES"
|
||||
#endif
|
||||
|
||||
// Enabled only in the validation build; no diagnostic arrays in normal builds.
|
||||
@@ -33,10 +36,17 @@ __device__ unsigned int incoming_counts[NQ * TOTAL_CELLS];
|
||||
#endif
|
||||
|
||||
__device__ __forceinline__ void patch_collide(float* f, unsigned long k) {
|
||||
#if COLLISION_MODEL == 2
|
||||
float rho, ux, uy;
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
collide_mrt_no_force(f, rho, ux, uy,
|
||||
fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega)));
|
||||
#else
|
||||
float rho, ux, uy, feq[NQ];
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
collide_srt_no_force(f, feq, d_params.omega);
|
||||
#endif
|
||||
#ifdef CELERIS_REFINEMENT_AUDIT
|
||||
++collision_counts[k];
|
||||
#endif
|
||||
|
||||
@@ -54,12 +54,20 @@ extern "C" __global__ void BlockChannelStep(
|
||||
f[7] = load_ddf(in + base, index_f(k, 8));
|
||||
}
|
||||
if (!wall && !outlet) {
|
||||
#if COLLISION_MODEL == 2
|
||||
// Match uniform MRT arithmetic (collide_dispatch, zero forcing).
|
||||
float rho, ux, uy;
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
collide_mrt_no_force(f, rho, ux, uy,
|
||||
fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega)));
|
||||
#else
|
||||
// Match uniform SRT arithmetic, including its shifted equilibrium path.
|
||||
float rho, ux, uy, feq[NQ], forcing[NQ] = {};
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
const float omega = fminf(OMEGA_COLLISION_MAX, fmaxf(OMEGA_COLLISION_MIN, d_params.omega));
|
||||
collide_srt(f, feq, forcing, omega);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
stream_pull_store(k, f, out + base);
|
||||
|
||||
@@ -7,16 +7,26 @@ import numpy as np
|
||||
|
||||
|
||||
def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
|
||||
time_end, window, reference_center):
|
||||
time_end, window, reference_center, duration=None):
|
||||
"""Reduce FP64 interval integrals or convert FP32 last-substep raw rows.
|
||||
|
||||
dt_l=h_l under acoustic 2:1 refinement. Local momentum j -> base force
|
||||
h*j -> impulse h*dt*j; torque adds one h; mass is h**2*dm.
|
||||
window="window" reduces an FP64 observation-window ledger spanning
|
||||
``duration`` (integer >= 1) base intervals; rates divide by that duration.
|
||||
"""
|
||||
if window not in ("last_base_step", "last_substep"):
|
||||
if window not in ("last_base_step", "last_substep", "window"):
|
||||
raise ValueError("unknown exchange window")
|
||||
if window == "window":
|
||||
if isinstance(duration, bool) or not isinstance(duration, (int, np.integer)) or duration < 1:
|
||||
raise ValueError("window duration must be an integer number of base intervals >= 1")
|
||||
elif duration is not None:
|
||||
raise ValueError("duration applies only to window='window'")
|
||||
values = np.array(rows, dtype=np.float64, copy=True)
|
||||
duration = 1.0 if window == "last_base_step" else h
|
||||
if window == "window":
|
||||
duration = float(duration)
|
||||
else:
|
||||
duration = 1.0 if window == "last_base_step" else h
|
||||
if window == "last_substep":
|
||||
values *= np.array([h*h, h*h, h*h*h, h*h])
|
||||
selected = np.asarray(body_ids) == body_id
|
||||
@@ -31,6 +41,7 @@ def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
|
||||
return dict(wall=reduce(selected & ~aperture), aperture=reduce(selected & aperture),
|
||||
total=reduce(selected), body_id=int(body_id), level=int(level),
|
||||
time_start=time_end-duration, time_end=time_end, dt=h,
|
||||
sample_count=round(1/h) if window == "last_base_step" else 1,
|
||||
sample_count=(round(duration/h) if window == "window"
|
||||
else round(1/h) if window == "last_base_step" else 1),
|
||||
window=window, units="base_lattice_per_unit_depth",
|
||||
definition="raw_boundary_exchange", reference_center=tuple(reference_center), valid=True)
|
||||
|
||||
@@ -52,17 +52,17 @@ class FixedPatch2D:
|
||||
def from_config(cls, cfg: LBMConfig, bounds) -> "FixedPatch2D":
|
||||
"""Prepare maps without changing cfg or allocating coarse-sized tables.
|
||||
|
||||
The initial backend contract is D2Q9/SRT/FP32/double_buffer,
|
||||
The backend contract is D2Q9/SRT or MRT/FP32/double_buffer,
|
||||
without LES. Two coarse cells of margin keep all halo donors in bounds;
|
||||
this does NOT certify that those donors are fluid or periodic.
|
||||
"""
|
||||
checked = replace(cfg)
|
||||
checked.validate()
|
||||
if (not checked.is_d2q9 or checked.collision != "SRT"
|
||||
if (not checked.is_d2q9 or checked.collision not in ("SRT", "MRT")
|
||||
or checked.streaming != "double_buffer"
|
||||
or checked.store_precision != "FP32"
|
||||
or checked.les_enabled):
|
||||
raise ValueError("FixedPatch2D requires D2Q9/SRT/FP32/double_buffer without LES")
|
||||
raise ValueError("FixedPatch2D requires D2Q9/SRT or MRT/FP32/double_buffer without LES")
|
||||
bounds = tuple(bounds)
|
||||
if len(bounds) != 4 or any(isinstance(v, (bool, np.bool_))
|
||||
or not isinstance(v, Integral) for v in bounds):
|
||||
@@ -451,7 +451,7 @@ def prepare_fine_boundary(cfg, patch, objects, *, wall_aware=False, jets=(),
|
||||
checked = replace(cfg)
|
||||
checked.validate()
|
||||
if (patch.coarse_shape != (cfg.ny, cfg.nx) or not checked.is_d2q9
|
||||
or checked.collision != "SRT" or checked.streaming != "double_buffer"
|
||||
or checked.collision not in ("SRT", "MRT") or checked.streaming != "double_buffer"
|
||||
or checked.store_precision != "FP32" or checked.les_enabled
|
||||
or patch.tau_coarse != 3*checked.viscosity+.5):
|
||||
raise ValueError("Fine boundary requires the matching fixed-patch configuration")
|
||||
|
||||
@@ -172,6 +172,9 @@ class FixedPatchStepper:
|
||||
base interval or last substep in base-lattice units per unit depth. This
|
||||
is boundary momentum reaction, not a validated nozzle/stress load. Dense
|
||||
body layouts are not supported by this new diagnostic in this candidate.
|
||||
``observe_window_exchange=True`` (requires observe_raw_exchange) adds a second
|
||||
per-link FP64 ledger accumulated by the same kernel between
|
||||
start_exchange_window() and read_exchange_window(); disabled by default.
|
||||
``jets`` (body.jets.Jet) use the same edge frame as ``fine_objects`` and
|
||||
drive fine cut links selected at their true hits; ``set_jet_amplitude``
|
||||
re-uploads the per-link velocity column only.
|
||||
@@ -216,7 +219,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_raw_exchange=False, observe_window_exchange=False):
|
||||
if HALO != 4:
|
||||
raise RuntimeError("refinement.cu requires a four-cell fine halo")
|
||||
if coarse_boundary not in ("periodic", "slot_reservoir", "channel"):
|
||||
@@ -315,6 +318,13 @@ class FixedPatchStepper:
|
||||
for b, layout in zip(self._boundary_hosts, self._layouts)):
|
||||
raise ValueError("raw exchange currently requires blocked body levels")
|
||||
self._observe_raw_exchange = observe_raw_exchange
|
||||
if not isinstance(observe_window_exchange, bool):
|
||||
raise ValueError("observe_window_exchange must be bool")
|
||||
if observe_window_exchange and not observe_raw_exchange:
|
||||
raise ValueError("observe_window_exchange requires observe_raw_exchange")
|
||||
self._observe_window = observe_window_exchange
|
||||
self._window_valid = False
|
||||
self._window_start = None
|
||||
self._exchange_valid = False
|
||||
self._exchange_spacing = tuple(spacings)
|
||||
self._exchange_centers = tuple(tuple(tuple(o.geometry.center) for o in objects)
|
||||
@@ -365,6 +375,8 @@ class FixedPatchStepper:
|
||||
count = len(b.tables["cell"])
|
||||
b.last_raw = self._allocate(count * 4 * 4)
|
||||
b.interval_integral = self._allocate(count * 4 * 8)
|
||||
if self._observe_window:
|
||||
b.window_integral = self._allocate(count * 4 * 8)
|
||||
b.accumulate = level.module.get_function("BlockAccumulateExchange")
|
||||
b.kernel = level.module.get_function("BlockCurvedBoundary")
|
||||
if self._level_boundaries[1] is not None:
|
||||
@@ -559,6 +571,7 @@ class FixedPatchStepper:
|
||||
cuda.memset_d8(self._fine_curved.link_exchange_gpu, 0, self._fine_curved.count * 12)
|
||||
self._step_count = 0
|
||||
self._exchange_valid = False
|
||||
self._window_valid = False
|
||||
self._status = "ready"
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
@@ -639,7 +652,8 @@ class FixedPatchStepper:
|
||||
fine_block=fine_block, child_block=child_block,
|
||||
child_rectangles_in_parent=new.child.bounds,
|
||||
child_objects=child_objects, child_jets=jets,
|
||||
observe_raw_exchange=self._observe_raw_exchange)
|
||||
observe_raw_exchange=self._observe_raw_exchange,
|
||||
observe_window_exchange=self._observe_window)
|
||||
try:
|
||||
if self._ddf_shifting:
|
||||
candidate._initialize_raw(_StoredPopulations(transferred.g, transferred.shifted))
|
||||
@@ -771,6 +785,10 @@ class FixedPatchStepper:
|
||||
b.accumulate(b.last_raw, b.interval_integral, np.uint32(count),
|
||||
np.float64(h), np.float64(h),
|
||||
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
|
||||
if self._window_valid and b.window_integral is not None:
|
||||
b.accumulate(b.last_raw, b.window_integral, np.uint32(count),
|
||||
np.float64(h), np.float64(h),
|
||||
grid=((count + self._block[0] - 1) // self._block[0], 1), **kw)
|
||||
flags = np.uintp(0) if level.boundary is None else level.boundary.flags
|
||||
level.kernel(src, dst, np.uint32(level.layout.halo), flags, level.kind, grid=level.grid, **kw)
|
||||
|
||||
@@ -804,9 +822,57 @@ class FixedPatchStepper:
|
||||
self._exchange_valid = self._observe_raw_exchange
|
||||
self._status = "ready"
|
||||
except BaseException:
|
||||
self._window_valid = False
|
||||
self._status = "failed"
|
||||
raise
|
||||
|
||||
def start_exchange_window(self):
|
||||
"""Zero the window ledgers and open a window at the current completed step count."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("ready")
|
||||
if not self._observe_window:
|
||||
raise RuntimeError("window exchange observation is disabled")
|
||||
self._window_valid = False
|
||||
try:
|
||||
for b in self._level_boundaries:
|
||||
if b is not None and b.window_integral is not None:
|
||||
cuda.memset_d8(b.window_integral, 0, len(b.tables["cell"]) * 32)
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
self._window_start = self._step_count
|
||||
self._window_valid = True
|
||||
|
||||
def read_exchange_window(self, body_id, *, level=1):
|
||||
"""Non-destructive read of [window start, current step count] in base-lattice units."""
|
||||
with self._lock, self.ctx.activate():
|
||||
self._require("ready")
|
||||
if not self._observe_window:
|
||||
raise RuntimeError("window exchange observation is disabled")
|
||||
if not self._window_valid:
|
||||
raise RuntimeError("no valid exchange window; call start_exchange_window()")
|
||||
duration = self._step_count - self._window_start
|
||||
if duration < 1:
|
||||
raise RuntimeError("exchange window requires at least one completed base interval")
|
||||
if (isinstance(level, bool) or not isinstance(level, Integral)
|
||||
or not 0 <= level < len(self._levels)):
|
||||
raise ValueError("level must name an existing level")
|
||||
b = self._level_boundaries[level]
|
||||
if (b is None or isinstance(body_id, bool) or not isinstance(body_id, Integral)
|
||||
or not 0 <= body_id < len(b.host.objects)):
|
||||
raise ValueError("body_id must name a body on this level")
|
||||
rows = np.empty((len(b.tables["cell"]), 4), np.float64)
|
||||
try:
|
||||
cuda.memcpy_dtoh(rows, b.window_integral)
|
||||
except BaseException:
|
||||
self._status = "failed"
|
||||
raise
|
||||
return summarize_exchange(rows, b.tables["body_id"], b.tables["aperture"],
|
||||
body_id=body_id, level=level, h=self._exchange_spacing[level],
|
||||
time_end=self._step_count, window="window",
|
||||
reference_center=self._exchange_centers[level][body_id],
|
||||
duration=duration)
|
||||
|
||||
def read_raw_exchange(self, body_id, *, level=1, window="last_base_step"):
|
||||
"""Read raw boundary reaction about its geometric center in the root edge frame.
|
||||
|
||||
@@ -885,6 +951,7 @@ class FixedPatchStepper:
|
||||
if b is not None:
|
||||
b.flags = b.device = b.kernel = None
|
||||
b.last_raw = b.interval_integral = b.accumulate = None
|
||||
b.window_integral = None
|
||||
self._levels.clear()
|
||||
self._pairs = []
|
||||
if getattr(self, "_fine_curved", None) is not None:
|
||||
|
||||
@@ -85,6 +85,7 @@ class _BlockedBoundary:
|
||||
last_raw: object = None
|
||||
interval_integral: object = None
|
||||
accumulate: object = None
|
||||
window_integral: object = None # optional observation-window FP64 ledger
|
||||
|
||||
|
||||
def _prepare_blocked_boundary(boundary, layout, cfg, jets, *, _shifted_blocked=False):
|
||||
|
||||
@@ -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,53 @@ 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", "dev8403956")["base_commit"],
|
||||
"8403956a9afc0b0bb89265cec374392013d89016",
|
||||
)
|
||||
|
||||
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_dev8403956.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", "dev8403956")
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "baseline")
|
||||
source_guard(root, "baseline", "dev8403956")
|
||||
target = root / "src/CelerisLab/lbm/kernels/step/one_step_double.cu"
|
||||
target.write_bytes(target.read_bytes() + b"\n// tamper\n")
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "corrected")
|
||||
source_guard(root, "corrected", "dev8403956")
|
||||
|
||||
def test_historical_default_and_manifest_unchanged(self):
|
||||
manifest = HERE / "data/les_flow/source_pins.json"
|
||||
self.assertEqual(hashlib.sha256(manifest.read_bytes()).hexdigest(),
|
||||
"87ca033619da1fc02bee822bfc5681b518a92a155c3c3d1dbbe5265c96066b39")
|
||||
for variant in ("baseline", "corrected"):
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(ROOT, variant)
|
||||
|
||||
def test_named_baseline_accepts_only_reversed_header(self):
|
||||
pins = json.loads((HERE / "data/les_flow/source_pins_dev8403956.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", "dev8403956")
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "corrected", "dev8403956")
|
||||
|
||||
def test_gpu_requires_opt_in_before_import(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Host reduction contract for the optional observation-window exchange ledger."""
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from CelerisLab.lbm.raw_exchange import summarize_exchange
|
||||
|
||||
|
||||
def reduce(rows, window, duration=None, h=.5, time_end=10):
|
||||
return summarize_exchange(rows, np.array([0, 0, 1]), np.array([False, True, False]),
|
||||
body_id=0, level=1, h=h, time_end=time_end, window=window,
|
||||
reference_center=(0., 0.), duration=duration)
|
||||
|
||||
|
||||
class ExchangeWindowReduction(unittest.TestCase):
|
||||
def test_window_duration_scaling(self):
|
||||
rows = np.array([[1., 2., 3., 4.], [.5, -1., 0., 1.], [9., 9., 9., 9.]])
|
||||
out = reduce(rows, "window", duration=4)
|
||||
np.testing.assert_array_equal(out["total"]["impulse"], [1.5, 1.])
|
||||
np.testing.assert_array_equal(out["total"]["force"], [1.5 / 4, 1. / 4])
|
||||
self.assertEqual(out["total"]["torque"], 3. / 4)
|
||||
self.assertEqual(out["total"]["mass_rate"], 5. / 4)
|
||||
np.testing.assert_array_equal(out["aperture"]["impulse"], [.5, -1.])
|
||||
self.assertEqual((out["time_start"], out["time_end"]), (6., 10))
|
||||
self.assertEqual(out["sample_count"], 8) # duration / h fine substeps
|
||||
self.assertEqual(out["window"], "window")
|
||||
|
||||
def test_existing_windows_unchanged(self):
|
||||
rows = np.array([[1., 2., 3., 4.], [0., 0., 0., 0.], [0., 0., 0., 0.]])
|
||||
base = reduce(rows, "last_base_step")
|
||||
self.assertEqual((base["time_start"], base["sample_count"]), (9., 2))
|
||||
np.testing.assert_array_equal(base["total"]["force"], [1., 2.])
|
||||
sub = reduce(rows, "last_substep")
|
||||
self.assertEqual(sub["sample_count"], 1)
|
||||
|
||||
def test_duration_validation(self):
|
||||
rows = np.zeros((3, 4))
|
||||
for bad in (None, 0, -1, 1.5, True):
|
||||
with self.subTest(duration=bad), self.assertRaises(ValueError):
|
||||
reduce(rows, "window", duration=bad)
|
||||
with self.assertRaises(ValueError):
|
||||
reduce(rows, "last_base_step", duration=2)
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -13,7 +13,15 @@ h, periodic = r.helpers(r.DEFAULT_SOURCE)
|
||||
|
||||
|
||||
def test_source_pin():
|
||||
assert r.verify_source(r.DEFAULT_SOURCE)["commit"].startswith("cd3a657")
|
||||
historical = json.loads((r.HERE / "data/refined_dfg/source_manifest.json").read_text())
|
||||
assert historical["commit"].startswith("cd3a657")
|
||||
mrt_history = json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text())
|
||||
assert mrt_history["commit"].startswith("3ea131e")
|
||||
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")
|
||||
|
||||
|
||||
def test_matched_units_and_planes():
|
||||
@@ -135,16 +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))
|
||||
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)
|
||||
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(),
|
||||
)
|
||||
@@ -177,6 +189,8 @@ def pilot_fixture(path):
|
||||
json.dumps(
|
||||
dict(
|
||||
case=case,
|
||||
collision="MRT",
|
||||
observation_mode=observation_mode,
|
||||
mode="pilot",
|
||||
status="pilot_complete",
|
||||
healthy=True,
|
||||
@@ -192,6 +206,8 @@ def pilot_fixture(path):
|
||||
json.dumps(
|
||||
dict(
|
||||
case=case,
|
||||
collision="MRT",
|
||||
observation_mode=observation_mode,
|
||||
mode="pilot",
|
||||
step=interval,
|
||||
runner_sha256=r.runner_hash(),
|
||||
@@ -208,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) == 3606.0
|
||||
assert r.verify_pilot(path, manifest, "MRT", "window") == 3606.0
|
||||
|
||||
|
||||
def test_reject_malformed_pilot_receipts():
|
||||
@@ -238,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)
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
# Exact reviewer regression: previously accepted NaN endpoint.
|
||||
receipt.write_text(
|
||||
json.dumps(
|
||||
@@ -250,11 +266,11 @@ def test_reject_malformed_pilot_receipts():
|
||||
)
|
||||
)
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest)
|
||||
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)
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
|
||||
|
||||
def test_strict_finite_json_and_deadline():
|
||||
@@ -283,6 +299,158 @@ def test_pressure_probes_actual_body_mask():
|
||||
assert h.pressure_probes(np.ones(pair.fine_shape), fluid, p)["dp"] == 0.0
|
||||
|
||||
|
||||
def test_matched_mrt_selection():
|
||||
for diameter in (40, 80):
|
||||
srt, sbody, sp = r.config(h, diameter)
|
||||
mrt, mbody, mp = r.config(h, diameter, "MRT")
|
||||
assert mrt["method"]["collision"] == "MRT"
|
||||
mrt["method"]["collision"] = "SRT"
|
||||
assert mrt == srt and mbody == sbody and mp == sp
|
||||
srt_plan, mrt_plan = r.plan(h), r.plan(h, "MRT")
|
||||
for key in ("root", "uniform", "bounds_root_edge", "finest_dx_m", "finest_dt_s"):
|
||||
assert srt_plan[key] == mrt_plan[key]
|
||||
assert "q2/6/8" in mrt_plan["outlet"] and "all nine" in srt_plan["outlet"]
|
||||
cfg, pair, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT")
|
||||
assert cfg.collision == "MRT" and pair.fine_shape == (240, 568)
|
||||
assert len(host.boundary_cells) > 0
|
||||
|
||||
|
||||
def test_reject_collision_receipts():
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
for rel in ("plan.json", "uniform/summary.json", "refined/summary.json",
|
||||
"uniform/ready.json", "refined/ready.json"):
|
||||
for invalid in ("SRT", None):
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
manifest = pilot_fixture(path)
|
||||
receipt = path / rel
|
||||
record = json.loads(receipt.read_text())
|
||||
if invalid is None:
|
||||
record.pop("collision")
|
||||
else:
|
||||
record["collision"] = invalid
|
||||
receipt.write_text(json.dumps(record))
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
|
||||
|
||||
def test_record_actual_checkout_separately():
|
||||
from source_provenance import checkout_provenance
|
||||
|
||||
actual = checkout_provenance(r.DEFAULT_SOURCE)
|
||||
assert len(actual["checkout_revision"]) == 40
|
||||
assert isinstance(actual["checkout_dirty"], bool)
|
||||
assert 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__":
|
||||
suite = unittest.TestSuite(
|
||||
unittest.FunctionTestCase(fn)
|
||||
|
||||
@@ -142,7 +142,7 @@ class FixedPatchTests(unittest.TestCase):
|
||||
FixedPatch2D.from_config(LBMConfig(nx=nx, ny=ny, viscosity=.1), bounds)
|
||||
|
||||
def test_unsupported_backend_and_fine_relaxation_rejected(self):
|
||||
for kwargs in ({"collision": "TRT"}, {"collision": "MRT"}, {"streaming": "esopull"},
|
||||
for kwargs in ({"collision": "TRT"}, {"streaming": "esopull"},
|
||||
{"les_enabled": True},
|
||||
{"store_precision": "FP16S"}, {"lattice_model": "D3Q19"},
|
||||
{"data_type": "FP64"}, {"omega_min": 1.0}):
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Host guards and source contract for MRT reuse in refined/blocked stepping (no CUDA)."""
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.refinement import FixedPatch2D
|
||||
|
||||
KERNELS = Path(__file__).resolve().parents[2] / "src/CelerisLab/lbm/kernels"
|
||||
BOUNDS = (8, 6, 16, 14)
|
||||
|
||||
|
||||
class RefinementMRTHost(unittest.TestCase):
|
||||
def test_patch_accepts_implemented_models(self):
|
||||
for collision in ("SRT", "MRT"):
|
||||
with self.subTest(collision=collision):
|
||||
FixedPatch2D.from_config(LBMConfig(nx=24, ny=20, viscosity=.1, collision=collision), BOUNDS)
|
||||
|
||||
def test_patch_rejects_unimplemented(self):
|
||||
for options in (dict(collision="TRT"), dict(collision="MRT", les_enabled=True)):
|
||||
with self.subTest(options=options), self.assertRaises(ValueError):
|
||||
FixedPatch2D.from_config(LBMConfig(nx=24, ny=20, viscosity=.1, **options), BOUNDS)
|
||||
|
||||
def test_srt_statements_and_guards_preserved(self):
|
||||
"""MRT is an additive compile-time branch; SRT statements and other guards remain."""
|
||||
blocks = (KERNELS / "blocks.cu").read_text()
|
||||
channel = (KERNELS / "step/block_channel.cu").read_text()
|
||||
refinement = (KERNELS / "refinement.cu").read_text()
|
||||
for text in ("compute_feq_from_momentum(drho, jx, jy, ux, uy, feq);",
|
||||
"compute_feq(rho, ux, uy, feq);",
|
||||
"collide_srt_no_force(f, feq, d_params.omega);"):
|
||||
self.assertIn(text, blocks)
|
||||
self.assertIn("collide_srt(f, feq, forcing, omega);", channel)
|
||||
self.assertIn("collide_srt_no_force(f, feq, d_params.omega);", refinement)
|
||||
for source in (blocks, refinement):
|
||||
self.assertIn("(COLLISION_MODEL != 0 && COLLISION_MODEL != 2)", source)
|
||||
self.assertIn("USE_LES", source)
|
||||
self.assertIn("STORE_PRECISION != 0", source)
|
||||
self.assertIn('#if COLLISION_MODEL == 2\n#include "operators/collision_mrt.cuh"\n#endif', source)
|
||||
self.assertIn("USE_DDF_SHIFTING ||", refinement) # dense/mixed shifted stays rejected
|
||||
for source in (blocks, channel, refinement):
|
||||
self.assertEqual(source.count("collide_mrt_no_force("), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Direct-current-header regression; historical candidate manifests stay frozen."""
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPERS = ROOT / "tests/validation/les"
|
||||
sys.path.insert(0, str(HELPERS))
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"smagorinsky_oracle", HELPERS / "test_candidate.py"
|
||||
)
|
||||
oracle = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(oracle)
|
||||
HEADER = Path("src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh")
|
||||
DESCRIPTORS = Path("src/CelerisLab/lbm/kernels/core/descriptors.cuh")
|
||||
|
||||
|
||||
class SmagorinskyNormalization(unittest.TestCase):
|
||||
def test_current_header_matches_independent_tensor_oracle(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
build_dir = Path(directory)
|
||||
for dim in (2, 3):
|
||||
for cs in (0.0, 0.16):
|
||||
# Compile directly against this checkout, not saved candidate headers.
|
||||
closed = oracle.build(
|
||||
ROOT.parent, ROOT.name, dim, cs, 1, 3, build_dir
|
||||
)
|
||||
converged = oracle.build(
|
||||
ROOT.parent, ROOT.name, dim, cs, 0, 200, build_dir
|
||||
)
|
||||
for rho in (0.7, 1.0, 1.3):
|
||||
for tau0 in (0.56, 0.8):
|
||||
for kind, magnitude in (
|
||||
("equilibrium", 0.0),
|
||||
("shear", -0.02),
|
||||
("shear", 0.02),
|
||||
("extension", -0.02),
|
||||
("extension", 0.02),
|
||||
):
|
||||
stress = oracle.tensor(
|
||||
dim, kind, magnitude * rho, 0, dim - 1
|
||||
)
|
||||
f, eq = oracle.state(dim, rho, stress)
|
||||
expected = 1 / oracle.standard_tau(
|
||||
tau0, cs, oracle.norm(stress), rho
|
||||
)
|
||||
with self.subTest(
|
||||
dim=dim,
|
||||
cs=cs,
|
||||
rho=rho,
|
||||
tau=tau0,
|
||||
kind=kind,
|
||||
sign=magnitude,
|
||||
):
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, rho, tau0), expected, delta=3e-6
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
converged(f, eq, rho, tau0),
|
||||
expected,
|
||||
delta=3e-6,
|
||||
)
|
||||
if cs == 0:
|
||||
f, eq = oracle.state(
|
||||
dim, 1.0, oracle.tensor(dim, "equilibrium", 0.0)
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, 1.0, 0.50001), 1.99, delta=3e-6
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, 1.0, 200.0), 0.01, delta=3e-6
|
||||
)
|
||||
|
||||
def test_removing_sqrt_two_is_detected(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for relative in (HEADER, DESCRIPTORS):
|
||||
target = root / "old_convention" / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / relative, target)
|
||||
target = root / "old_convention" / HEADER
|
||||
text = target.read_text()
|
||||
factor = "18.0f * sqrtf(2.0f) * smag_const_sq"
|
||||
self.assertEqual(text.count(factor), 2)
|
||||
target.write_text(text.replace(factor, "18.0f * smag_const_sq"))
|
||||
for dim in (2, 3):
|
||||
old = oracle.build(root, "old_convention", dim, 0.16, 1, 3, root)
|
||||
stress = oracle.tensor(dim, "shear", 0.02)
|
||||
f, eq = oracle.state(dim, 1.0, stress)
|
||||
expected = 1 / oracle.standard_tau(0.8, 0.16, oracle.norm(stress), 1.0)
|
||||
self.assertGreater(abs(old(f, eq, 1.0, 0.8) - expected), 1e-3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,86 @@
|
||||
"""CPU contract of the measurement-only refined DFG timing harness (no CUDA)."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
||||
import time_refined_dfg as t # noqa: E402
|
||||
|
||||
|
||||
class TimingHarnessContract(unittest.TestCase):
|
||||
def test_equal_simulated_duration(self):
|
||||
s = t.schedule(2000, 200)
|
||||
self.assertEqual(s["refined"], (200, 2000))
|
||||
self.assertEqual(s["uniform"], (400, 4000)) # one refined base step = two D80 steps
|
||||
self.assertAlmostEqual(s["tstar_window"], 2000 * .01 / 40)
|
||||
for bad in ((0, 1), (1, -1)):
|
||||
with self.assertRaises(ValueError):
|
||||
t.schedule(*bad)
|
||||
|
||||
def test_timing_only_labels(self):
|
||||
for name in ("B", "C", "E'"):
|
||||
self.assertFalse(t.VARIANTS[name]["force_history_valid"])
|
||||
self.assertTrue(t.VARIANTS[name]["timing_only"])
|
||||
for name in ("A", "E"):
|
||||
self.assertFalse(t.VARIANTS[name]["force_history_valid"]) # reads discarded
|
||||
self.assertTrue(t.VARIANTS[name]["force_reads"])
|
||||
self.assertIn("runner-like", t.VARIANTS[name]["force_note"])
|
||||
self.assertEqual({v["case"] for v in t.VARIANTS.values()}, {"refined", "uniform"})
|
||||
self.assertIn("final base interval", t.VARIANTS["C"]["force_note"])
|
||||
self.assertIn("whole-window", t.VARIANTS["E'"]["force_note"])
|
||||
|
||||
def test_deadline(self):
|
||||
import time
|
||||
with self.assertRaises(TimeoutError):
|
||||
t.check_deadline(time.monotonic() - 1)
|
||||
t.check_deadline(time.monotonic() + 60)
|
||||
|
||||
def test_parity_detects_bits_and_nan_masks(self):
|
||||
a = np.array([[1., np.nan], [2., 3.]], np.float32)
|
||||
self.assertIsNone(t.same_state([a], [a.copy()]))
|
||||
b = a.copy(); b[1, 1] = np.nextafter(np.float32(3), np.float32(4))
|
||||
self.assertIn("bits", t.same_state([a], [b]))
|
||||
c = a.copy(); c[0, 0] = np.nan
|
||||
self.assertIn("NaN", t.same_state([a], [c]))
|
||||
self.assertIn("level count", t.same_state([a], [a, a]))
|
||||
inf = a.copy(); inf[1, 0] = np.inf
|
||||
self.assertIn("infinite", t.same_state([inf], [inf.copy()])) # matching inf still fails
|
||||
self.assertIn("infinite", t.same_state([a], [inf]))
|
||||
|
||||
def test_flat_uniform_ddf(self):
|
||||
fluid = np.array([[True, False, True], [True, True, False]])
|
||||
flat = np.ones(9 * fluid.size, np.float32) # Simulation.get_ddf() layout
|
||||
self.assertTrue(t.uniform_populations_finite(flat, fluid))
|
||||
flat[1 * fluid.size + 1] = np.nan # q=1, solid cell 1: ignored
|
||||
self.assertTrue(t.uniform_populations_finite(flat, fluid))
|
||||
flat[4 * fluid.size + 0] = np.nan # q=4, fluid cell 0: detected
|
||||
self.assertFalse(t.uniform_populations_finite(flat, fluid))
|
||||
self.assertTrue(t.uniform_populations_finite(np.ones((9,) + fluid.shape), fluid))
|
||||
with self.assertRaises(RuntimeError):
|
||||
t.uniform_populations_finite(np.ones(9 * fluid.size + 1), fluid)
|
||||
|
||||
def test_physical_health_limits(self):
|
||||
ok = (np.array([1., 1.01]), np.array([.01, .02]), np.array([0., .01]))
|
||||
health = t.physical_health([ok])
|
||||
self.assertAlmostEqual(health["rho_max"], 1.01)
|
||||
for bad in ((np.array([1., np.nan]),) + ok[1:],
|
||||
(np.array([.4, 1.]),) + ok[1:],
|
||||
(ok[0], np.array([.2, 0.]), ok[2]),
|
||||
(np.array([]), np.array([]), np.array([]))):
|
||||
with self.assertRaises(RuntimeError):
|
||||
t.physical_health([bad])
|
||||
|
||||
def test_summary_reports_differences_without_claims(self):
|
||||
rows = [dict(variant=v, seconds=s) for v, s in
|
||||
(("A", 3.), ("A", 5.), ("B", 2.), ("C", 1.), ("E", 2.), ("E'", 1.5))]
|
||||
s = t.summarize(rows, .5)
|
||||
self.assertEqual(s["A"]["median_seconds"], 4.)
|
||||
self.assertEqual(s["A_minus_B_seconds"], 2.)
|
||||
self.assertEqual(s["B_minus_C_seconds"], 1.)
|
||||
self.assertEqual(s["A_over_E"], 2.)
|
||||
self.assertIn("not exact causal", s["interpretation"])
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -0,0 +1,38 @@
|
||||
"""CPU contract of the A-versus-window timing harness (no CUDA)."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
||||
import time_window_dfg as w # noqa: E402
|
||||
|
||||
|
||||
class WindowTimingContract(unittest.TestCase):
|
||||
def test_interval_and_variants(self):
|
||||
self.assertEqual(w.INTERVAL, 80)
|
||||
self.assertEqual(w.VARIANTS, ("A", "W", "W1"))
|
||||
|
||||
def test_components(self):
|
||||
obs = dict(total=dict(impulse=np.array([1., 2.]), angular_impulse=3., mass_into_fluid=4.))
|
||||
np.testing.assert_array_equal(w.components(obs), [1., 2., 3., 4.])
|
||||
|
||||
def test_interval_comparison(self):
|
||||
ref = np.array([[1., -1., 0., 0.]])
|
||||
scale = np.array([[10., 10., 1., 1.]])
|
||||
self.assertEqual(w.compare_intervals(ref, scale, ref.copy()), 0.)
|
||||
near = ref + np.array([[5e-9, 0., 0., 0.]])
|
||||
self.assertLessEqual(w.compare_intervals(ref, scale, near), 1.)
|
||||
far = ref + np.array([[1e-7, 0., 0., 0.]])
|
||||
self.assertGreater(w.compare_intervals(ref, scale, far), 1.)
|
||||
zero = np.zeros((1, 4))
|
||||
self.assertGreater(w.compare_intervals(zero, zero, zero + 1e-12), 1.) # floor, no div-by-0
|
||||
|
||||
def test_candidate_identity_records_files(self):
|
||||
identity = w.candidate_identity(Path(__file__).resolve().parents[2])
|
||||
self.assertIn("checkout_revision", identity)
|
||||
self.assertEqual(set(identity["sha256"]), set(w.CANDIDATE_FILES))
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -0,0 +1,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,112 @@
|
||||
{
|
||||
"commit": "3ea131e92c057843034f11a1c574301a2430dfce",
|
||||
"production_origin_commit": "3ea131e92c057843034f11a1c574301a2430dfce",
|
||||
"collision": "MRT",
|
||||
"sha256": {
|
||||
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
|
||||
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
|
||||
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
|
||||
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
|
||||
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
|
||||
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
|
||||
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
|
||||
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
|
||||
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
|
||||
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
|
||||
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
|
||||
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
|
||||
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
|
||||
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
|
||||
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
|
||||
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
|
||||
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
|
||||
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
|
||||
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
|
||||
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
|
||||
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
|
||||
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
|
||||
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
|
||||
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
|
||||
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
|
||||
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
|
||||
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
|
||||
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
|
||||
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
|
||||
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
|
||||
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
|
||||
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
|
||||
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
|
||||
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
|
||||
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
|
||||
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
|
||||
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
|
||||
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
|
||||
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
|
||||
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
|
||||
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
|
||||
"src/CelerisLab/lbm/channel.py": "40d0354e5dc97bcdb7e7b5c1e7f9303a109b7134d8a3af88b8ff4cd75ab5a095",
|
||||
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
|
||||
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
|
||||
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
|
||||
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
|
||||
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
|
||||
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
|
||||
"src/CelerisLab/lbm/kernels/blocks.cu": "2fa894b20a29f006911ac3e3aad0793cc4aaf9ccd7a67d01c6320a0836b1d8ef",
|
||||
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
|
||||
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
|
||||
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
|
||||
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
|
||||
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
|
||||
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
|
||||
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
|
||||
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
|
||||
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
|
||||
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
|
||||
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
|
||||
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
|
||||
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
|
||||
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
|
||||
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
|
||||
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
|
||||
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
|
||||
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
|
||||
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
|
||||
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
|
||||
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
|
||||
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
|
||||
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
|
||||
"src/CelerisLab/lbm/kernels/refinement.cu": "b45aad9c022cecb64f7e968ec3cae2d8385a3cc430ee409700c404a3f4dd8cf8",
|
||||
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
|
||||
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "ee8a31398b8e5233d977c816e6b8e060c1eb2ec71320e594226dbca55593c0d0",
|
||||
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
|
||||
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
|
||||
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
|
||||
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
|
||||
"src/CelerisLab/lbm/raw_exchange.py": "8694202466eba848563e298656353b0f5db94e21a01fa05f51aad6dc2b9fd311",
|
||||
"src/CelerisLab/lbm/refinement.py": "100d5e46ac894f6008b61364cdad5d37c2ca7ed949d8d71405572d6366526417",
|
||||
"src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d",
|
||||
"src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6",
|
||||
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
|
||||
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
|
||||
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c",
|
||||
"tests/validation/re100_helpers.py": "289c6e01075ff361573f862b90cfd61e5980c9a8efcbc78aef69979b4004fd60",
|
||||
"tests/validation/periodic.py": "a2212c0e2705126327a5019e9cafe64eeca398da1b75061ff23630c9843697a6",
|
||||
"tests/validation/data/dfg_re100/reference_cycle.json": "073d1bda1629697efebad95667bfe80598e217d708dbffd7d5f6c6999379c55f",
|
||||
"tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c"
|
||||
}
|
||||
}
|
||||
@@ -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,263 @@
|
||||
"""Four opt-in shifted GPU controls: U64/U128/U256 and refined base128/fine256."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import resource
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import run_spacetime_gpu_n64 as old
|
||||
|
||||
gpu,ref,tg,ROOT = old.gpu,old.ref,old.tg,old.ROOT
|
||||
BASE='26d0d3853c9d4d3cd6ae3f27653dced56f8c5fec'
|
||||
REFERENCE_SHA='9d154c989b087bf288f86d413db0f3067b73767b618c329d4b9e53c4eee00032'
|
||||
CASES=(('uniform64',64,False),('uniform128',128,False),('uniform256',256,False),('refined128',128,True))
|
||||
LIMITS=old.LIMITS
|
||||
SECONDS,RSS_MIB=600.,512.
|
||||
|
||||
|
||||
def setup(n, refined):
|
||||
return dict(base_n=n,finest_n=2*n if refined else n,normalized_domain=[0.,1.,0.,1.],
|
||||
re_L=100,U=.03,mach=.03*np.sqrt(3),viscosity=.0003*n,
|
||||
tau=[.5+.0009*n]+([.5+.0018*n] if refined else []),
|
||||
bounds=[n//8,n//8,7*n//8,3*n//4] if refined else None,
|
||||
normalized_patch=[.125,.125,.875,.75] if refined else None,
|
||||
intervals=16*n,sample_steps=[4*n,8*n,12*n,16*n],sample_times=list(old.TIMES),
|
||||
early_steps=list(range(0,4*n+1,n//16)),early_times=(np.arange(65)*.001875).tolist(),
|
||||
phase='arrival_f',storage='shifted_FP32',collision='SRT',
|
||||
initialization='common analytic FP64 equilibrium at actual cell centers, production shifted codec')
|
||||
|
||||
|
||||
def pressure_projection(arrays,masks,t,n,viscosity):
|
||||
"""One global gauge and a weighted Gram fit, including unequal leaf areas."""
|
||||
rows=[]
|
||||
for level,(a,mask) in enumerate(zip(arrays,masks)):
|
||||
rho=np.asarray(a[:,mask],np.float64).sum(axis=0)
|
||||
x,y=tg.centers(mask.shape,level); x,y=x[mask],y[mask]
|
||||
exact=tg.exact(x,y,t,n,viscosity)[2]
|
||||
angle=4*np.pi/n
|
||||
basis=np.column_stack((np.cos(angle*x),np.sin(angle*x),np.cos(angle*y),np.sin(angle*y)))
|
||||
rows.append(((rho-1)/3,exact,np.full(len(rho),.25**level),basis))
|
||||
p,exact,w,b=[np.concatenate(x) for x in zip(*rows)]
|
||||
volume=w.sum(); p-=w@p/volume; exact-=w@exact/volume; e=p-exact
|
||||
b-=w@b/volume
|
||||
gram=b.T@(w[:,None]*b); rhs=b.T@(w*e)
|
||||
coefficients=np.linalg.solve(gram,rhs); residual=e-b@coefficients
|
||||
norm=float(w@(e*e)); exact_norm=float(w@(exact*exact)); remaining=float(w@(residual*residual))
|
||||
projected=float(coefficients@gram@coefficients)
|
||||
return dict(step=t,tstar=t*.03/n,absolute_rms=np.sqrt(norm/volume),relative_rms=np.sqrt(norm/exact_norm),
|
||||
coefficients=coefficients.tolist(),basis=['cos_4pi_x','sin_4pi_x','cos_4pi_y','sin_4pi_y'],
|
||||
residual_rms=np.sqrt(remaining/volume),residual_relative=np.sqrt(remaining/exact_norm),
|
||||
gram_condition=float(np.linalg.cond(gram)),
|
||||
orthogonality=(b.T@(w*residual)/volume).tolist(),energy_closure=(norm-projected-remaining)/volume)
|
||||
|
||||
|
||||
def load_reference(path):
|
||||
if old.sha(path)!=REFERENCE_SHA:
|
||||
raise ValueError('verified shifted N64 receipt differs')
|
||||
data=json.loads(Path(path).read_text())
|
||||
if (not data['completed'] or not data['passed'] or data['source']['commit']!=BASE
|
||||
or data['setup']['storage']!='shifted_FP32' or data['setup']['bounds']!=[8,8,56,48]
|
||||
or data['setup']['interval_count']!=1024 or len(data['rows'])!=4):
|
||||
raise ValueError('N64 reference identity differs')
|
||||
if tuple(row['tstar'] for row in data['rows'])!=old.TIMES:
|
||||
raise ValueError('N64 reference sample times differ')
|
||||
return data
|
||||
|
||||
|
||||
def source_identity(reference):
|
||||
git=lambda *args:subprocess.check_output(['git',*args],cwd=ROOT,text=True).strip()
|
||||
if git('status','--porcelain'):
|
||||
raise RuntimeError('clean committed checkout required')
|
||||
unchanged=['src','tests/validation/taylor_green/oracle.py']+[f'tests/validation/multigrid/{name}' for name in
|
||||
('spacetime_gpu.cu','spacetime_reference.py','cpu_reference.py','run_spacetime_gpu_n64.py','test_spacetime_gpu.py')]
|
||||
subprocess.check_call(['git','diff','--exit-code',BASE,'--',*unchanged],cwd=ROOT)
|
||||
previous=subprocess.check_output(['git','show',f'{BASE}:tests/validation/multigrid/spacetime_gpu.py'],cwd=ROOT,text=True)
|
||||
# The raw interval schedule remains byte-for-byte unchanged despite the new owner.
|
||||
method=inspect.getsource(gpu.SpaceTimeGPU.step)
|
||||
if method not in previous or np.__version__!=reference['source']['numpy']:
|
||||
raise RuntimeError('raw schedule or NumPy identity changed')
|
||||
files=[Path(__file__),Path(gpu.__file__),Path(old.table_guard.__file__),Path(__file__).with_name('test_spacetime_gpu_ladder.py')]
|
||||
return dict(commit=git('rev-parse','HEAD'),dirty=False,numpy=np.__version__,production_tree=git('rev-parse','HEAD:src'),
|
||||
hashes={p.name:old.sha(p) for p in files},cuda_sha256=old.sha(Path(gpu.__file__).with_suffix('.cu')),
|
||||
raw_schedule_sha256=hashlib.sha256(method.encode()).hexdigest(),unchanged_against=BASE,
|
||||
reference_hashes=gpu.PINS)
|
||||
|
||||
|
||||
def directional(arrays,masks):
|
||||
return sum(.25**level*np.asarray(a[:,m],np.float64).sum(axis=1) for level,(a,m) in enumerate(zip(arrays,masks)))
|
||||
|
||||
|
||||
def populations(run,refined,*,raw=False):
|
||||
state=run._download_stored() if raw else run.download()
|
||||
if not refined:
|
||||
return (state,)
|
||||
if state.phase!='arrival_f' or state.tick!=2*run.step_count:
|
||||
raise AssertionError('refined sample is not synchronized arrival F')
|
||||
return state.coarse_f,state.fine_f
|
||||
|
||||
|
||||
def assess(rows,health):
|
||||
checks={key:all(np.isfinite(row[key]) and 0<=row[key]<=limit for row in rows) for key,limit in LIMITS.items()}
|
||||
checks['population_health']=not any(health)
|
||||
checks['sampled_population_positive']=all(row['minimum_population']>0 for row in rows)
|
||||
return dict(worst={key:max(row[key] for row in rows) for key in LIMITS},campaign_checks=checks,
|
||||
campaign_pass=all(checks.values()),accuracy_comparison_status='descriptive_only_no_new_threshold')
|
||||
|
||||
|
||||
def ratios(a,b,*,trend=False):
|
||||
out=dict(ratio=a/b if b>0 else None,ratio_status='defined' if b>0 else 'zero_denominator')
|
||||
if trend:
|
||||
out.update(two_point_slope=np.log2(a/b) if a>0 and b>0 else None,
|
||||
slope_status='defined' if a>0 and b>0 else 'zero_error_log_undefined')
|
||||
return out
|
||||
|
||||
|
||||
def comparisons(runs,reference):
|
||||
data={r['name']:r for r in runs}; data['refined64']=reference
|
||||
out=[]
|
||||
for refined,base,fine in (('refined64','uniform64','uniform128'),('refined128','uniform128','uniform256')):
|
||||
for control in (base,fine):
|
||||
out.append(dict(candidate=refined,control=control,control_spacing='base' if control==base else 'finest',
|
||||
samples=[dict(tstar=a['tstar'],**{key:dict(candidate=a[key],control=b[key],**ratios(a[key],b[key]))
|
||||
for key in ('velocity_l2','pressure_l2','energy_relative')})
|
||||
for a,b in zip(data[refined]['rows'],data[control]['rows'])]))
|
||||
trends=[]
|
||||
for coarse,fine in (('uniform64','uniform128'),('uniform128','uniform256'),('refined64','refined128')):
|
||||
trends.append(dict(coarse=coarse,fine=fine,samples=[dict(tstar=a['tstar'],**{
|
||||
key:ratios(a[key],b[key],trend=True)
|
||||
for key in ('velocity_l2','pressure_l2','energy_relative')})
|
||||
for a,b in zip(data[coarse]['rows'],data[fine]['rows'])]))
|
||||
return dict(controls=out,trends=trends,caveat='norm differences are not interface error; fixed Mach and two refined sizes do not certify convergence')
|
||||
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--gpu-admitted',action='store_true'); p.add_argument('--reference-n64',type=Path,required=True)
|
||||
p.add_argument('--output',type=Path,required=True); args=p.parse_args()
|
||||
if not args.gpu_admitted or not os.environ.get('CUDA_VISIBLE_DEVICES'):
|
||||
p.error('explicit GPU admission and CUDA_VISIBLE_DEVICES required')
|
||||
if args.output.exists() or args.output.with_suffix(args.output.suffix+'.tmp').exists(): p.error('output or partial receipt exists; preserve it')
|
||||
reference=load_reference(args.reference_n64); source=source_identity(reference); gpu.check_sources()
|
||||
started=time.monotonic()
|
||||
report=dict(completed=False,passed=False,source=source,runs=[],limits=dict(seconds=SECONDS,peak_rss_mib=RSS_MIB),
|
||||
campaign_limits=LIMITS,reference=dict(sha256=REFERENCE_SHA,source=reference['source'],
|
||||
coverage='verified refined base64/fine128 four-time rows only; early pressure trace unavailable'),
|
||||
historical_pressure_gate=dict(scope='reused_refined64_only',**reference['historical_pressure_gate']),
|
||||
n64_experimental_screen=dict(scope='reused_refined64_only',checks=reference['experimental_checks']),
|
||||
frozen_cases=[setup(n,r) for _,n,r in CASES],early_trace_scope='65 predeclared rows per new run, no field snapshots',
|
||||
cuda_visible_devices=os.environ['CUDA_VISIBLE_DEVICES'])
|
||||
run=context=ledger=None
|
||||
case_started=None
|
||||
def save():
|
||||
report['resources']=dict(seconds=time.monotonic()-started,peak_rss_mib=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024,
|
||||
rss_scope='Python process peak; external wrapper monitors process group and wall time')
|
||||
report['resources']['within_limits']=report['resources']['seconds']<=SECONDS and report['resources']['peak_rss_mib']<=RSS_MIB
|
||||
if report['completed'] and not report['resources']['within_limits']: report['passed']=False
|
||||
if run is not None:
|
||||
report['runs'][-1].update(completed_intervals=run.step_count,health_counts=run.health_counts.tolist(),
|
||||
seconds=time.monotonic()-case_started)
|
||||
if ledger is not None:
|
||||
report['runs'][-1].update(ledger=ledger.snapshot(),audited_intervals=ledger.steps)
|
||||
temporary=args.output.with_suffix(args.output.suffix+'.tmp')
|
||||
ref.write_receipt(temporary,report); os.replace(temporary,args.output)
|
||||
def cap():
|
||||
if time.monotonic()-started>SECONDS or resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024>RSS_MIB:
|
||||
raise RuntimeError('frozen aggregate resource cap exceeded')
|
||||
save()
|
||||
try:
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
context=CudaContext(0)
|
||||
if context.name!=reference['device'] or context.sm_arch!=reference['sm_arch']:
|
||||
raise RuntimeError('matched reference device identity differs')
|
||||
report.update(device=context.name,sm_arch=context.sm_arch)
|
||||
for name,n,refined in CASES:
|
||||
cap(); ledger=None; case_started=time.monotonic(); cfg=old.LBMConfig(nx=n,ny=n,viscosity=.0003*n,velocity=0.,ddf_shifting=True)
|
||||
case=dict(name=name,setup=setup(n,refined),completed=False,phase='setup',rows=[],early_pressure=[],prefixes=[],health_counts=[0]*6,
|
||||
ledger_scope='all intervals; fine/transfer components are mathematically absent for uniform runs')
|
||||
report['runs'].append(case); save()
|
||||
run=gpu.SpaceTimeGPU(cfg,tuple(case['setup']['bounds']),context=context,allow_n128=True) if refined else gpu.UniformGPU(cfg,context=context)
|
||||
case.update(build_metadata=run.build_metadata,device_allocations_bytes=run.device_bytes,
|
||||
device_memory_scope='owner allocations; excludes driver/context/compiler')
|
||||
expected_modules=None
|
||||
if not refined and n in (64,128): expected_modules=[reference['build_metadata'][0 if n==64 else 1]]
|
||||
if refined: expected_modules=[report['runs'][1]['build_metadata'][0],report['runs'][2]['build_metadata'][0]]
|
||||
if expected_modules is not None:
|
||||
if run.build_metadata!=expected_modules: raise RuntimeError('matched source/config/PTX metadata differs')
|
||||
case['matched_build_metadata']=True
|
||||
if any(m['flags']!=reference['build_metadata'][0]['flags'] for m in run.build_metadata):
|
||||
raise RuntimeError('compiler flags differ from reference')
|
||||
if refined:
|
||||
case['tables']=old.table_guard.validate_tables(run.plan,run.tables,allow_n128=True)
|
||||
masks=(run.plan.maps.coarse_active,run.plan.maps.fine_active)
|
||||
else: masks=(np.ones((n,n),bool),)
|
||||
analytic=tuple(tg.initial(m.shape,l,n,cfg.viscosity) for l,m in enumerate(masks))
|
||||
initial=ref.cpu.State(*analytic) if refined else analytic[0]
|
||||
physical=lambda:populations(run,refined)
|
||||
stored=lambda:populations(run,refined,raw=True)
|
||||
run.initialize(initial); raw0=stored(); direction0=directional(raw0,masks)
|
||||
case['phase']='warm_reset'; warm_started=time.monotonic()
|
||||
for _ in range(32): cap(); run.step()
|
||||
case['warmup_seconds']=time.monotonic()-warm_started
|
||||
run.initialize(initial)
|
||||
for a,b in zip(raw0,stored()): np.testing.assert_array_equal(a.view(np.uint32),b.view(np.uint32))
|
||||
if run.step_count or run.health_counts.any(): raise AssertionError('warm reset failed')
|
||||
case['raw_reset_verified']=True; arrays=physical()
|
||||
first=tg.metrics(arrays,masks,0,n,cfg.viscosity); mass0=first['mass']; moment0=ref.cpu.MOMENTS.T@directional(arrays,masks)
|
||||
case['initial']=dict(mass=mass0,momentum=moment0[1:].tolist(),minimum_population=min(float(a[:,m].min()) for a,m in zip(arrays,masks)))
|
||||
case['initial_quantization']=[float(abs(a[:,m]-b[:,m]).max()) for a,b,m in zip(arrays,analytic,masks)]
|
||||
case['early_pressure'].append(pressure_projection(arrays,masks,0,n,cfg.viscosity))
|
||||
ledger=old.LedgerTotals(); case['phase']='trajectory'; trajectory_started=time.monotonic()
|
||||
case['all_interval_mass']=dict(maximum_relative=0.,step=0,signed_change_at_maximum=0.,limit=LIMITS['mass_relative'],passed=True)
|
||||
for step in range(1,16*n+1):
|
||||
cap(); measured=run.step(); ledger.add(measured)
|
||||
if run.step_count!=step: raise AssertionError('interval count differs')
|
||||
mass_change=float(ledger.sums[0].sum()); drift=abs(mass_change)/mass0
|
||||
if drift>case['all_interval_mass']['maximum_relative']:
|
||||
case['all_interval_mass'].update(maximum_relative=drift,step=step,signed_change_at_maximum=mass_change,passed=drift<=LIMITS['mass_relative'])
|
||||
if step in case['setup']['early_steps'] or step in case['setup']['sample_steps']:
|
||||
arrays=physical()
|
||||
if step in case['setup']['early_steps']:
|
||||
case['early_pressure'].append(pressure_projection(arrays,masks,step,n,cfg.viscosity)); save()
|
||||
if step in case['setup']['sample_steps']:
|
||||
row=tg.metrics(arrays,masks,step,n,cfg.viscosity,mass0)
|
||||
row.update(step=step,momentum_change=(ref.cpu.MOMENTS.T@directional(arrays,masks)-moment0)[1:].tolist(),
|
||||
mass_change=row['mass']-mass0,minimum_population=min(float(a[:,m].min()) for a,m in zip(arrays,masks)))
|
||||
if not all(np.isfinite(v).all() for v in row.values()): raise FloatingPointError('nonfinite sampled metric')
|
||||
case['rows'].append(row); prefix=ledger.snapshot(directional(stored(),masks)-direction0)
|
||||
prefix['physical_minus_raw_endpoint_moments']=(ref.cpu.MOMENTS.T@directional(arrays,masks)-moment0-ref.cpu.MOMENTS.T@np.array(prefix['endpoint_directional_change'])).tolist()
|
||||
case['prefixes'].append(prefix)
|
||||
if not prefix['closure_pass'] or not prefix['component_split_pass']: raise AssertionError('cumulative budget failed')
|
||||
save(); print(json.dumps(dict(case=name,**row),allow_nan=False),flush=True)
|
||||
case.update(assess(case['rows'],run.health_counts))
|
||||
cap(); case.update(completed=True,phase='completed',health_counts=run.health_counts.tolist(),audited_intervals=ledger.steps,
|
||||
ledger=ledger.snapshot(),trajectory_seconds=time.monotonic()-trajectory_started,seconds=time.monotonic()-case_started)
|
||||
case['passed']=case['campaign_pass'] and case['all_interval_mass']['passed'] and ledger.steps==16*n and len(case['early_pressure'])==65
|
||||
run.close(); run=None; save()
|
||||
cap(); report.update(completed=True,passed=all(c['passed'] for c in report['runs']),comparisons=comparisons(report['runs'],reference))
|
||||
except Exception as exc:
|
||||
report['failure']=repr(exc)
|
||||
raise
|
||||
finally:
|
||||
if run is not None and ledger is not None:
|
||||
current=report['runs'][-1]
|
||||
current['failure_endpoint_status']='unavailable_owner_not_ready'
|
||||
if run.status=='ready':
|
||||
try:
|
||||
current['failure_endpoint']=ledger.snapshot(directional(populations(run,refined,raw=True),masks)-direction0)
|
||||
current['failure_endpoint_status']='measured'
|
||||
except Exception as exc: current['failure_endpoint_status']=repr(exc)
|
||||
for owner in (run,context):
|
||||
if owner is not None:
|
||||
try: owner.close()
|
||||
except Exception as exc: report.setdefault('cleanup_errors',[]).append(repr(exc)); report['passed']=False
|
||||
save()
|
||||
return 0 if report['passed'] else 1
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,392 @@
|
||||
"""Opt-in, frozen N64 space-time GPU trajectory; no convergence certification."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import resource
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0, str(ROOT/'src'))
|
||||
sys.path.insert(0, str(Path(__file__).parents[1]/'taylor_green'))
|
||||
from CelerisLab.config import LBMConfig
|
||||
import oracle as tg
|
||||
import spacetime_gpu as gpu
|
||||
import spacetime_table_guard as table_guard
|
||||
ref = gpu.ref
|
||||
|
||||
CPU_SHA = '180eef90a1c3f43113fe18cc54981ee4723cfc0cc2724334907ff188022fa770'
|
||||
UNIFORM_SHA = '01e7415ae39456f2de7f0c3d331afb971c49d19353b43038650e32e80737f1e7'
|
||||
CUDA_SHA = 'afea11ea4a1c2c3800b2f6a7a8e81f5568989fc9e1cc02fb52d7e1ea3864e003'
|
||||
AUDIT_SHA = 'fba340b1533bec308b666ee84666df0ac9d08e2872be31ee379e2d1447565158'
|
||||
STEPS = (256, 512, 768, 1024)
|
||||
TIMES = (.12, .24, .36, .48)
|
||||
FACTORS = dict(velocity_l2=.8, pressure_l2=1., energy_relative=1.)
|
||||
LIMITS = dict(velocity_l2=.05, pressure_l2=.15, energy_relative=.05, mass_relative=1e-5)
|
||||
HEALTH = ('boundary_pre_nonfinite', 'boundary_pre_nonpositive', 'boundary_stored_nonfinite',
|
||||
'boundary_stored_nonpositive', 'physical_population_nonfinite', 'physical_population_nonpositive')
|
||||
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def load_references(cpu_path, uniform_path):
|
||||
for path, expected in ((cpu_path, CPU_SHA), (uniform_path, UNIFORM_SHA)):
|
||||
if sha(path) != expected:
|
||||
raise ValueError(f'pinned reference receipt differs: {path}')
|
||||
cpu, uniform = [json.loads(Path(p).read_text()) for p in (cpu_path, uniform_path)]
|
||||
assert cpu['completed'] and cpu['experimental_pass'] and not cpu['source']['dirty']
|
||||
assert cpu['source']['commit'] == '07879bc52bfef8d95503146c55d3823c8b6f8d0a'
|
||||
assert [r['mode'] for r in cpu['runs']] == ['constant_ivp', 'spacetime']
|
||||
assert all(tuple(row['tstar'] for row in r['rows']) == TIMES for r in cpu['runs'])
|
||||
assert uniform['completed'] and not uniform['source']['dirty']
|
||||
assert uniform['source']['revision'] == '9805383f9c50d867e8555471baf5dbda8333a652'
|
||||
assert (uniform['n'], uniform['depth'], uniform['viscosity']) == (64, 1, .0192)
|
||||
assert uniform['limits'] == tg.LIMITS == LIMITS
|
||||
return cpu, uniform
|
||||
|
||||
|
||||
def total(state, maps):
|
||||
arrays = [a.astype(np.float64) for a in (state.coarse_f, state.fine_f)]
|
||||
return ref.cpu.MOMENTS.T @ ref.directional(ref.cpu.State(*arrays), maps)
|
||||
|
||||
|
||||
def differing_fields(actual, expected):
|
||||
"""Compare serialized sample numerics exactly, including the sign of zero."""
|
||||
if actual.keys() != expected.keys():
|
||||
return ['field_set']
|
||||
return [key for key in actual if np.asarray(actual[key]).dtype != np.asarray(expected[key]).dtype
|
||||
or np.asarray(actual[key]).shape != np.asarray(expected[key]).shape
|
||||
or np.asarray(actual[key]).tobytes() != np.asarray(expected[key]).tobytes()]
|
||||
|
||||
|
||||
def sample_difference(actual, expected):
|
||||
"""Descriptive comparison only; never an equivalence gate for different codecs."""
|
||||
return {key:dict(reference=value,signed=(np.asarray(actual[key])-value).tolist(),
|
||||
absolute=np.abs(np.asarray(actual[key])-value).tolist()) for key,value in expected.items()}
|
||||
|
||||
|
||||
class LedgerTotals:
|
||||
"""Compensated directional sums; retain absolute step sums to expose cancellation."""
|
||||
names = ('net_change','physical_collision','coarse_collision','fine0_collision','fine1_collision',
|
||||
'child_rounding','child_interpolation','child_cast','register_rounding','receiver_rounding','unexplained')
|
||||
|
||||
def __init__(self):
|
||||
self.sums = np.zeros((len(self.names),9)); self.correction = self.sums.copy()
|
||||
self.absolute = self.sums.copy(); self.absolute_moments = np.zeros((len(self.names),3))
|
||||
self.maxima = self.sums.copy(); self.max_steps = np.zeros(self.sums.shape,np.int64)
|
||||
self.cells = np.zeros((3,4)); self.cell_correction = self.cells.copy()
|
||||
self.steps = 0; self.bound = 0.; self.maximum = 0.
|
||||
|
||||
def add(self, ledger):
|
||||
values = np.array([ledger[name] for name in self.names],np.float64)
|
||||
cells = np.asarray(ledger['collision_cell_mass'],np.float64)
|
||||
if (values.shape != self.sums.shape or not np.isfinite(values).all()
|
||||
or cells.shape != (3,4) or not np.isfinite(cells).all()
|
||||
or not np.isfinite(ledger['unexplained_bound']) or ledger['unexplained_bound'] <= 0):
|
||||
raise AssertionError('invalid diagnostic ledger')
|
||||
delta = values-self.correction
|
||||
updated = self.sums+delta
|
||||
self.correction = (updated-self.sums)-delta
|
||||
self.sums = updated
|
||||
self.absolute += abs(values)
|
||||
self.absolute_moments += abs(values @ ref.cpu.MOMENTS)
|
||||
larger = abs(values) > self.maxima
|
||||
self.max_steps[larger] = self.steps+1; self.maxima = np.maximum(self.maxima,abs(values))
|
||||
delta = cells-self.cell_correction
|
||||
updated = self.cells+delta
|
||||
self.cell_correction = (updated-self.cells)-delta; self.cells = updated
|
||||
self.steps += 1; self.bound += ledger['unexplained_bound']
|
||||
self.maximum = max(self.maximum,float(abs(values[-1]).max()))
|
||||
|
||||
def snapshot(self, endpoint_change=None):
|
||||
values = dict(zip(self.names,self.sums))
|
||||
accounted = sum(values[key] for key in ('physical_collision','child_rounding','register_rounding','receiver_rounding'))
|
||||
closure = None if endpoint_change is None else endpoint_change-accounted
|
||||
telescoping = None if endpoint_change is None else endpoint_change-values['net_change']
|
||||
closure_pass = None if closure is None else max(float(abs(closure).max()),float(abs(telescoping).max())) <= self.bound
|
||||
collision_split = values['physical_collision']-sum(values[key] for key in ('coarse_collision','fine0_collision','fine1_collision'))
|
||||
child_split = values['child_rounding']-values['child_interpolation']-values['child_cast']
|
||||
split_pass = max(float(abs(collision_split).max()),float(abs(child_split).max())) <= self.bound
|
||||
return dict(intervals=self.steps, summed_per_step_bound=self.bound, max_step_unexplained=self.maximum,
|
||||
closure_pass=closure_pass,component_split_pass=split_pass,
|
||||
endpoint_status='not_measured' if endpoint_change is None else 'measured',
|
||||
endpoint_directional_change=None if endpoint_change is None else endpoint_change.tolist(),
|
||||
closure_residual=None if closure is None else closure.tolist(),
|
||||
telescoping_residual=None if telescoping is None else telescoping.tolist(),
|
||||
closure_mass_momentum=None if closure is None else (ref.cpu.MOMENTS.T@closure).tolist(),
|
||||
collision_split_residual=collision_split.tolist(),child_split_residual=child_split.tolist(),
|
||||
collision_cell_mass=dict(levels=['coarse','fine0','fine1'],columns=['net','positive','negative','l1'],
|
||||
applied_volume_weights=[1.,.25,.25],values=self.cells.tolist()),
|
||||
components={key:dict(directional=self.sums[i].tolist(),mass_momentum=(self.sums[i]@ref.cpu.MOMENTS).tolist(),
|
||||
absolute_step_directional_sum=self.absolute[i].tolist(),
|
||||
absolute_step_mass_momentum_sum=self.absolute_moments[i].tolist(),
|
||||
max_absolute_interval=self.maxima[i].tolist(),max_absolute_interval_step=self.max_steps[i].tolist())
|
||||
for i,key in enumerate(self.names)})
|
||||
|
||||
|
||||
def warm_and_reset(run, initial, check_cap, *, audit=False):
|
||||
"""Time 32 synchronized intervals, then restore exact uploaded arrival F."""
|
||||
run.initialize(initial)
|
||||
before = run.download()
|
||||
started = time.monotonic()
|
||||
for _ in range(32):
|
||||
check_cap()
|
||||
run.step(audit=audit)
|
||||
seconds = time.monotonic()-started
|
||||
warm_health = run.health_counts.tolist()
|
||||
run.initialize(initial)
|
||||
after = run.download()
|
||||
for a, b in zip((before.coarse_f, before.fine_f), (after.coarse_f, after.fine_f)):
|
||||
np.testing.assert_array_equal(a, b)
|
||||
if run.step_count != 0 or after.tick != 0 or run.health_counts.any():
|
||||
raise AssertionError('warm-up reset did not restore tick/counters')
|
||||
return after, dict(intervals=32, seconds=seconds, estimated_1024_seconds=32*seconds,
|
||||
health_counts=warm_health, reset_verified=True, ledger_audit=audit)
|
||||
|
||||
|
||||
def assess(rows, cpu, uniform, health_counts):
|
||||
if tuple(row['tstar'] for row in rows) != TIMES:
|
||||
raise ValueError('four frozen sample times required')
|
||||
worst = {key: max(row[key] for row in rows) for key in LIMITS}
|
||||
campaign = {key: all(np.isfinite(row[key]) and 0 <= row[key] <= limit for row in rows)
|
||||
for key, limit in LIMITS.items()}
|
||||
campaign['population_health'] = not any(health_counts)
|
||||
campaign['sampled_positive_finite'] = all(
|
||||
np.isfinite(row[key]) and row[key] > 0 for row in rows
|
||||
for key in ('minimum_population', 'minimum_boundary_pre', 'minimum_boundary_stored'))
|
||||
baseline, candidate = cpu['runs']
|
||||
thresholds = {key: factor*baseline['worst'][key] for key, factor in FACTORS.items()}
|
||||
experimental = {key: all(np.isfinite(row[key]) and row[key] >= 0 for row in rows)
|
||||
and worst[key] <= threshold for key, threshold in thresholds.items()}
|
||||
differences = []
|
||||
for actual, expected in zip(rows, candidate['rows']):
|
||||
delta = {}
|
||||
for key, value in expected.items():
|
||||
d = np.asarray(actual[key])-np.asarray(value)
|
||||
delta[key] = dict(cpu=value, signed=d.tolist(), absolute=np.abs(d).tolist())
|
||||
differences.append(dict(tstar=actual['tstar'], fields=delta))
|
||||
historical_worst = max(row['pressure_l2'] for row in uniform['rows'] if row['tstar'] > 0)
|
||||
historical_limit = max(2*historical_worst, .02)
|
||||
assert historical_limit == .024506347612699403
|
||||
return dict(worst=worst, campaign_checks=campaign, campaign_pass=all(campaign.values()),
|
||||
experimental_thresholds=thresholds, experimental_checks=experimental,
|
||||
experimental_pass=all(experimental.values()),
|
||||
gpu_minus_cpu=dict(status='descriptive_only_no_equivalence_tolerance', rows=differences),
|
||||
historical_pressure_gate=dict(status='historical_shifted_FP32_uniform_vs_unshifted_FP32_candidate',
|
||||
matched_storage=False, uniform_worst=historical_worst, threshold=historical_limit,
|
||||
candidate_worst=worst['pressure_l2'], passed=worst['pressure_l2'] <= historical_limit))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--gpu-admitted', action='store_true')
|
||||
parser.add_argument('--cpu-receipt', type=Path, required=True)
|
||||
parser.add_argument('--uniform-receipt', type=Path, required=True)
|
||||
parser.add_argument('--audit-against', type=Path, help='audit every interval and compare exact saved 1779cf2 samples')
|
||||
parser.add_argument('--shifted', action='store_true', help='opt-in shifted storage with all interval ledgers')
|
||||
parser.add_argument('--unshifted-receipt', type=Path, help='pinned1779 comparison, descriptive only')
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
if not args.gpu_admitted or not os.environ.get('CUDA_VISIBLE_DEVICES'):
|
||||
parser.error('explicit --gpu-admitted and CUDA_VISIBLE_DEVICES required')
|
||||
if args.output.exists():
|
||||
parser.error('output exists; preserve evidence and choose a new path')
|
||||
if args.shifted != (args.unshifted_receipt is not None) or (args.shifted and args.audit_against):
|
||||
parser.error('--shifted requires --unshifted-receipt and cannot use exact --audit-against')
|
||||
exact_repeat = args.audit_against is not None
|
||||
cpu, uniform = load_references(args.cpu_receipt, args.uniform_receipt)
|
||||
previous = None
|
||||
comparison_path = args.audit_against or args.unshifted_receipt
|
||||
if comparison_path is not None:
|
||||
if sha(comparison_path) != AUDIT_SHA:
|
||||
raise ValueError('pinned GPU trajectory receipt differs')
|
||||
previous = json.loads(comparison_path.read_text())
|
||||
if previous['source']['numpy'] != np.__version__:
|
||||
raise RuntimeError('audited repeat requires unchanged NumPy environment')
|
||||
gpu.check_sources()
|
||||
if sha(Path(gpu.__file__).with_suffix('.cu')) != CUDA_SHA:
|
||||
raise RuntimeError('reviewed CUDA source changed')
|
||||
source = dict(commit=subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip(),
|
||||
dirty=bool(subprocess.check_output(['git', 'status', '--porcelain'], cwd=ROOT, text=True)),
|
||||
hashes={p.name: sha(p) for p in (Path(__file__), Path(gpu.__file__), Path(gpu.__file__).with_suffix('.cu'),
|
||||
Path(__file__).with_name('test_spacetime_gpu.py'), Path(table_guard.__file__))},
|
||||
reference_hashes=gpu.PINS, numpy=np.__version__, checkout=str(ROOT))
|
||||
if source['dirty']:
|
||||
raise RuntimeError('clean committed checkout required')
|
||||
started = time.monotonic()
|
||||
report = dict(scope='N64 experimental space-time GPU; no grid-convergence qualification',
|
||||
completed=False, passed=False, source=source, rows=[], health_counts=[0]*6, health_labels=HEALTH,
|
||||
setup=dict(n=64, bounds=[8,8,56,48], re_L=100, U=.03, viscosity=.0192, tau=[.5576,.6152],
|
||||
phase='arrival_f', storage='shifted_FP32' if args.shifted else 'unshifted_FP32', collision='SRT', interval_count=1024,
|
||||
initialization='common analytic FP64 field, encoded using production FP64 weights before FP32 upload' if args.shifted
|
||||
else 'same physical FP64 equilibrium as CPU, rounded to FP32 on upload',
|
||||
cpu_storage='FP64', sample_steps=STEPS, sample_times=TIMES),
|
||||
references=dict(cpu_receipt_sha256=CPU_SHA, cpu_source=cpu['source'],
|
||||
cpu_archive_drive_id='197F9nwDOGf_YROK1a46YpVvNrSldnH_w',
|
||||
cpu_archive_sha256='850ddce3f5203ef38e76e5153e718a9b92e137d1fc388ba7cd12f5d47eab83b4',
|
||||
uniform_receipt_sha256=UNIFORM_SHA, uniform_source=uniform['source'],
|
||||
uniform_storage='historical_shifted_FP32'),
|
||||
frozen_screen=dict(campaign_limits=LIMITS, experimental_factors=FACTORS,
|
||||
gpu_cpu_equivalence_tolerance=None, historical_pressure_threshold=.024506347612699403),
|
||||
boundary_budget_status='pending_measurement' if previous else 'not_measured', max_directional_residual=None,
|
||||
minima_scope='initial and four sampled physical arrival states; boundary minima at four samples',
|
||||
limits=dict(seconds=180., peak_rss_mib=512.), cuda_visible_devices=os.environ['CUDA_VISIBLE_DEVICES'])
|
||||
run = context = None
|
||||
ledger_totals = LedgerTotals() if previous else None
|
||||
if previous:
|
||||
report['diagnostic'] = dict(completed=False, passed=False, reference_sha256=AUDIT_SHA,
|
||||
reference_source=previous['source']['commit'], sample_checks=[], prefixes=[],
|
||||
scope='net drift attribution; exact saved diagnostics only, not full-field trajectory certification' if exact_repeat
|
||||
else 'raw stored-value ledger; unshifted and CPU comparisons descriptive, initial codecs differ')
|
||||
phase = 'setup'
|
||||
|
||||
def save():
|
||||
report['resources'] = dict(seconds=time.monotonic()-started,
|
||||
peak_rss_mib=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024,
|
||||
device_allocations_bytes=None if run is None else run.device_bytes,
|
||||
device_memory_scope='owner allocations; excludes compiler, driver and context')
|
||||
if run is not None:
|
||||
report['health_counts'] = run.health_counts.tolist()
|
||||
report['completed_intervals'] = run.step_count
|
||||
if ledger_totals is not None:
|
||||
report['audited_intervals'] = ledger_totals.steps
|
||||
if ledger_totals.steps:
|
||||
report['boundary_budget_status'] = 'measured' if ledger_totals.steps==1024 else 'measured_prefix'
|
||||
report['diagnostic']['accumulated'] = ledger_totals.snapshot()
|
||||
ref.write_receipt(args.output, report)
|
||||
|
||||
def check_cap():
|
||||
if time.monotonic()-started > 180 or resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024 > 512:
|
||||
raise RuntimeError('frozen resource cap exceeded')
|
||||
|
||||
save()
|
||||
try:
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
context = CudaContext(0)
|
||||
config = LBMConfig(nx=64, ny=64, viscosity=.0192, velocity=0., ddf_shifting=args.shifted)
|
||||
run = gpu.SpaceTimeGPU(config, (8,8,56,48), context=context)
|
||||
report.update(device=context.name, sm_arch=context.sm_arch, build_metadata=run.build_metadata,
|
||||
table_sha256=gpu.table_hash(run.tables))
|
||||
report['table_validation'] = table_guard.validate_tables(run.plan,run.tables)
|
||||
if previous:
|
||||
for key in ('device','sm_arch','table_sha256') + (('build_metadata',) if exact_repeat else ()):
|
||||
if report[key] != previous[key]:
|
||||
raise AssertionError(f'audited repeat identity differs: {key}')
|
||||
analytic = ref.cpu.State(tg.initial((64,64),0,64,.0192), tg.initial((128,128),1,64,.0192))
|
||||
initial = analytic
|
||||
phase = 'warm_reset'
|
||||
initial, report['warmup'] = warm_and_reset(run, initial, check_cap, audit=previous is not None)
|
||||
maps = run.plan.maps
|
||||
masks = (maps.coarse_active, maps.fine_active)
|
||||
arrays = (initial.coarse_f, initial.fine_f)
|
||||
mass0 = tg.metrics(arrays, masks, 0, 64, .0192)['mass']
|
||||
moment0 = total(initial, maps)
|
||||
report['initial'] = dict(mass=mass0, momentum=moment0[1:].tolist(),
|
||||
minimum_population=min(float(a[:,m].min()) for a,m in zip(arrays,masks)))
|
||||
if exact_repeat and differing_fields(report['initial'],previous['initial']):
|
||||
raise AssertionError('audited repeat initial state diagnostics differ')
|
||||
raw_initial = run._download_stored() if args.shifted else initial
|
||||
direction0 = ref.directional(ref.cpu.State(*(a.astype(np.float64) for a in
|
||||
(raw_initial.coarse_f,raw_initial.fine_f))),maps)
|
||||
if args.shifted:
|
||||
errors=[]; unshifted_errors=[]
|
||||
for raw,actual,wanted,mask in zip((raw_initial.coarse_f,raw_initial.fine_f),arrays,
|
||||
(analytic.coarse_f,analytic.fine_f),masks):
|
||||
encoded=gpu._encode_physical(wanted,True)
|
||||
np.testing.assert_array_equal(raw[:,mask].view(np.uint32),encoded[:,mask].view(np.uint32))
|
||||
errors.append(float(abs(actual[:,mask]-wanted[:,mask]).max()))
|
||||
unshifted_errors.append(float(abs(gpu._encode_physical(wanted,False)[:,mask]-wanted[:,mask]).max()))
|
||||
report['warmup']['raw_reset_verified']=True
|
||||
report['initial_quantization']=dict(maximum_population_error_by_level=errors,
|
||||
unshifted_codec_error_by_level=unshifted_errors,
|
||||
moment_change_vs_analytic=(moment0-total(analytic,maps)).tolist(),
|
||||
cpu_start='pinned analytic FP64 trajectory, not reinitialized to decoded GPU upload',
|
||||
matched_cpu_initial_bits=False)
|
||||
report['unshifted_comparison']=dict(status='descriptive_only_codecs_have_different_initial_bits',
|
||||
reference_sha256=AUDIT_SHA,initial=sample_difference(report['initial'],previous['initial']),rows=[])
|
||||
save()
|
||||
check_cap()
|
||||
phase = 'trajectory'
|
||||
trajectory_started = time.monotonic()
|
||||
for step in range(1,1025):
|
||||
check_cap()
|
||||
ledger = run.step(audit=previous is not None)
|
||||
if ledger_totals is not None:
|
||||
ledger_totals.add(ledger)
|
||||
report['max_directional_residual'] = ledger_totals.maximum
|
||||
if step in STEPS:
|
||||
state = run.download()
|
||||
if state.tick != 2*step:
|
||||
raise AssertionError('sample is not synchronized arrival state')
|
||||
arrays = (state.coarse_f, state.fine_f)
|
||||
row = tg.metrics(arrays, masks, step, 64, .0192, mass0)
|
||||
row.update(step=step, momentum_change=(total(state,maps)-moment0)[1:].tolist(),
|
||||
mass_change=row['mass']-mass0,
|
||||
minimum_population=min(float(a[:,m].min()) for a,m in zip(arrays,masks)))
|
||||
if ledger_totals is not None:
|
||||
row['minimum_boundary_pre'] = ledger['minimum_pre']
|
||||
row['minimum_boundary_stored'] = ledger['minimum_stored']
|
||||
else:
|
||||
with context.activate():
|
||||
row['minimum_boundary_pre'] = float(run.read(run.pre,(len(run.plan.groups),4),np.float64).min())
|
||||
row['minimum_boundary_stored'] = float(run.read(run.h,(len(run.plan.groups),4)).min())
|
||||
if not all(np.isfinite(value).all() for value in row.values()):
|
||||
report['failed_sample'] = {key: repr(value) for key,value in row.items()}
|
||||
raise FloatingPointError('nonfinite sample diagnostic')
|
||||
report['rows'].append(row)
|
||||
if previous:
|
||||
expected=previous['rows'][len(report['rows'])-1]
|
||||
changed = differing_fields(row,expected) if exact_repeat else []
|
||||
report['diagnostic']['sample_checks'].append(dict(step=step,
|
||||
status='exact_repeat' if exact_repeat else 'descriptive_only',differing_fields=changed if exact_repeat else None))
|
||||
if args.shifted:
|
||||
report['unshifted_comparison']['rows'].append(dict(step=step,fields=sample_difference(row,expected)))
|
||||
raw_state=run._download_stored() if args.shifted else state
|
||||
endpoint = ref.directional(ref.cpu.State(*(a.astype(np.float64) for a in
|
||||
(raw_state.coarse_f,raw_state.fine_f))),maps)-direction0
|
||||
prefix = ledger_totals.snapshot(endpoint)
|
||||
if args.shifted:
|
||||
prefix['physical_minus_raw_endpoint_moments'] = (
|
||||
total(state,maps)-moment0-ref.cpu.MOMENTS.T@endpoint).tolist()
|
||||
report['diagnostic']['prefixes'].append(prefix)
|
||||
if not prefix['closure_pass'] or not prefix['component_split_pass']:
|
||||
raise AssertionError('cumulative ledger does not close within summed existing per-step bounds')
|
||||
if changed:
|
||||
raise AssertionError(f'audited repeat sample differs: {changed}')
|
||||
save()
|
||||
print(json.dumps(row, allow_nan=False), flush=True)
|
||||
report['trajectory_seconds'] = time.monotonic()-trajectory_started
|
||||
check_cap()
|
||||
report.update(assess(report['rows'], cpu, uniform, run.health_counts))
|
||||
if args.shifted:
|
||||
report['historical_pressure_gate'].update(status='historical_shifted_FP32_uniform_vs_shifted_FP32_candidate',
|
||||
matched_storage=True,matched_control=False)
|
||||
report['completed'] = True
|
||||
report['passed'] = report['campaign_pass'] and report['experimental_pass']
|
||||
if previous:
|
||||
report['diagnostic'].update(completed=True,passed=ledger_totals.steps==1024 and len(report['rows'])==4)
|
||||
except Exception as exc:
|
||||
report['failure'] = dict(phase=phase, error=repr(exc))
|
||||
raise
|
||||
finally:
|
||||
# Preserve the primary failure and every completed sample even if cleanup fails.
|
||||
for owner in (run, context):
|
||||
if owner is not None:
|
||||
try:
|
||||
owner.close()
|
||||
except Exception as exc:
|
||||
report.setdefault('cleanup_errors', []).append(repr(exc))
|
||||
report['passed'] = False
|
||||
save()
|
||||
return 0 if report['passed'] else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,165 @@
|
||||
// Test-only arrival-f space-time coupling. No production scheduler changes.
|
||||
#include <stdint.h>
|
||||
#include <math.h>
|
||||
#include "config.h"
|
||||
#include "core/precision.cuh"
|
||||
#include "core/descriptors.cuh"
|
||||
#include "core/layout.cuh"
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#include "streaming/pull_double_buffer.cuh"
|
||||
|
||||
#if DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Space-time experiment requires FP32 D2Q9 SRT double buffers without LES"
|
||||
#endif
|
||||
|
||||
__device__ __forceinline__ void collide(float* f, float omega) {
|
||||
float rho, ux, uy, feq[NQ];
|
||||
compute_rho_u(f, rho, ux, uy);
|
||||
#if USE_DDF_SHIFTING
|
||||
// Same perturbation-preserving SRT branch as production BlockStep.
|
||||
const float drho = f[0]+f[1]+f[2]+f[3]+f[4]+f[5]+f[6]+f[7]+f[8];
|
||||
const float jx = f[1]-f[2]+f[5]-f[6]+f[7]-f[8];
|
||||
const float jy = f[3]-f[4]+f[5]-f[6]-f[7]+f[8];
|
||||
compute_feq_from_momentum(drho, jx, jy, ux, uy, feq);
|
||||
#else
|
||||
compute_feq(rho, ux, uy, feq);
|
||||
#endif
|
||||
collide_srt_no_force(f, feq, omega);
|
||||
}
|
||||
|
||||
__device__ __forceinline__ double physical(double value, unsigned int q) {
|
||||
#if USE_DDF_SHIFTING
|
||||
// Match population_storage.py's FP64 weights, not promoted FP32 d_w.
|
||||
return value + (q == 0 ? 4.0/9.0 : (q < 5 ? 1.0/9.0 : 1.0/36.0));
|
||||
#else
|
||||
return value;
|
||||
#endif
|
||||
}
|
||||
|
||||
__device__ __forceinline__ void health(float stored, unsigned int q, unsigned int* counts) {
|
||||
const double value = physical((double)stored, q);
|
||||
if (!isfinite(value)) atomicAdd(counts + 4, 1u);
|
||||
if (value <= 0.0f) atomicAdd(counts + 5, 1u);
|
||||
}
|
||||
|
||||
extern "C" __global__ void CoarseStart(
|
||||
const float* coarse, const float* fine, float* out, float omega,
|
||||
unsigned int x0, unsigned int y0, unsigned int x1, unsigned int y1,
|
||||
unsigned int* counts)
|
||||
{
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x >= NX || y >= NY) return;
|
||||
const bool covered = x >= x0 && x < x1 && y >= y0 && y < y1;
|
||||
float f[NQ];
|
||||
for (unsigned int q = 0; q < NQ; ++q) {
|
||||
const unsigned long s = q * 4ul * TOTAL_CELLS + 2ul * y * (2u * NX) + 2u * x;
|
||||
f[q] = covered ? 0.25f * (fine[s] + fine[s+1] + fine[s+2u*NX] + fine[s+2u*NX+1])
|
||||
: coarse[index_f(k, q)];
|
||||
}
|
||||
collide(f, omega);
|
||||
for (int q = 0; q < NQ; ++q) if (!covered) health(f[q], q, counts);
|
||||
stream_pull_store(k, f, out);
|
||||
}
|
||||
|
||||
extern "C" __global__ void Collide(
|
||||
const float* in, float* out, const unsigned char* active, float omega,
|
||||
unsigned int* counts)
|
||||
{
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x >= NX || y >= NY) return;
|
||||
float f[NQ];
|
||||
if (active == nullptr || active[k]) {
|
||||
for (int q = 0; q < NQ; ++q) f[q] = in[index_f(k, q)];
|
||||
collide(f, omega);
|
||||
if (active != nullptr) for (int q = 0; q < NQ; ++q) health(f[q], q, counts);
|
||||
} else {
|
||||
for (int q = 0; q < NQ; ++q) f[q] = NAN;
|
||||
}
|
||||
stream_pull_store(k, f, out);
|
||||
}
|
||||
|
||||
extern "C" __global__ void Pull(
|
||||
const float* in, float* out, const unsigned char* active, unsigned int* counts)
|
||||
{
|
||||
unsigned int x, y; unsigned long k;
|
||||
index_from_thread(x, y, k);
|
||||
if (x >= NX || y >= NY) return;
|
||||
float f[NQ];
|
||||
if (active == nullptr || active[k]) {
|
||||
unsigned long j[NQ];
|
||||
compute_neighbors(x, y, j);
|
||||
stream_pull_load(k, f, in, j);
|
||||
if (active != nullptr) for (int q = 0; q < NQ; ++q) health(f[q], q, counts);
|
||||
} else {
|
||||
for (int q = 0; q < NQ; ++q) f[q] = NAN;
|
||||
}
|
||||
stream_pull_store(k, f, out);
|
||||
}
|
||||
|
||||
extern "C" __global__ void Interpolate(
|
||||
const float* g0, const float* gp, const uint32_t* samples,
|
||||
const uint32_t* origins, const int* sizes, const double* weights,
|
||||
double* pre, float* h, unsigned int groups, unsigned int frozen,
|
||||
unsigned int* counts)
|
||||
{
|
||||
unsigned int group = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (group >= groups) return;
|
||||
const int size = sizes[group];
|
||||
const unsigned int q = origins[group] / TOTAL_CELLS;
|
||||
for (int child = 0; child < 4; ++child) {
|
||||
double value = frozen ? (double)g0[origins[group]] : 0.0;
|
||||
if (!frozen) for (int j = 0; j < size; ++j) {
|
||||
const float* source = j < size/2 ? g0 : gp;
|
||||
value += weights[(4u*group+child)*10u+j] * (double)source[samples[10u*group+j]];
|
||||
}
|
||||
const float stored = __double2float_rn(value);
|
||||
pre[4u*group+child] = value;
|
||||
h[4u*group+child] = stored;
|
||||
if (!isfinite(physical(value, q))) atomicAdd(counts, 1u);
|
||||
if (physical(value, q) <= 0.0) atomicAdd(counts+1, 1u);
|
||||
if (!isfinite(physical((double)stored, q))) atomicAdd(counts+2, 1u);
|
||||
if (physical((double)stored, q) <= 0.0) atomicAdd(counts+3, 1u);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" __global__ void Inject(
|
||||
float* fine, const float* h, const uint32_t* rows, unsigned int count,
|
||||
unsigned int omit_first)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count || (omit_first && row == 0)) return;
|
||||
fine[rows[2u*row]] = h[rows[2u*row+1]];
|
||||
}
|
||||
|
||||
extern "C" __global__ void Register(
|
||||
const float* predicted, const float* fine, const float* h,
|
||||
const uint32_t* targets, const uint32_t* sources, const int* times,
|
||||
double* registers, unsigned int count, int substep, int mutation)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
double value = substep == 0 ? -(double)predicted[targets[row]] : registers[row];
|
||||
for (int j = 0; j < 4; ++j) {
|
||||
const unsigned int slot = 4u*row+j;
|
||||
const int when = times[slot];
|
||||
if (when != substep && !(substep == 0 && when == -1)) continue;
|
||||
if ((mutation == 1 && when == -1) || (mutation == 2 && when == 0)
|
||||
|| (mutation == 3 && when == 1)) continue;
|
||||
value += 0.25 * (double)(when == -1 ? h[sources[slot]] : fine[sources[slot]]);
|
||||
}
|
||||
registers[row] = value;
|
||||
}
|
||||
|
||||
extern "C" __global__ void ApplyRegisters(
|
||||
float* predicted, const double* registers, const uint32_t* targets,
|
||||
unsigned int count, unsigned int* counts)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const unsigned int slot = targets[row];
|
||||
predicted[slot] = __double2float_rn((double)predicted[slot] + registers[row]);
|
||||
health(predicted[slot], slot / TOTAL_CELLS, counts);
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
"""Test-only arrival-f GPU owner; CUDA compilation/execution requires admission."""
|
||||
from dataclasses import replace
|
||||
import hashlib
|
||||
from numbers import Integral
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import spacetime_reference as ref
|
||||
from CelerisLab.lbm.population_storage import _encode_physical, _decode_stored, _WEIGHTS
|
||||
|
||||
PINS = {"spacetime_reference.py": "1fe13242b9a41062a1227db4a6f9b695165aefab7f4c0b3b2adff0c9023552bf",
|
||||
"cpu_reference.py": "bba2a06a9f6517b32079ffff15e4060d8c3b7ed35f16ca134e1877226c9e4e1e",
|
||||
"../taylor_green/oracle.py": "83e4d7425d45538278e9fb181af450a36358bd9a5ed6be57ff611dd9ae5eb35b"}
|
||||
|
||||
|
||||
def check_sources():
|
||||
for name, expected in PINS.items():
|
||||
if hashlib.sha256((Path(__file__).parent/name).read_bytes()).hexdigest() != expected:
|
||||
raise RuntimeError(f"reference source changed: {name}")
|
||||
|
||||
|
||||
def validate_config(config, *, uniform=False, allow_n128=False):
|
||||
if type(uniform) is not bool or type(allow_n128) is not bool:
|
||||
raise ValueError("admission flags must be bool")
|
||||
cfg = replace(config)
|
||||
cfg.validate()
|
||||
if (cfg.nx != cfg.ny or not 8 <= cfg.nx <= (256 if uniform else (128 if allow_n128 else 64)) or cfg.nz != 1
|
||||
or cfg.collision != "SRT" or not cfg.is_d2q9
|
||||
or cfg.streaming != "double_buffer" or cfg.store_precision != "FP32"
|
||||
or cfg.data_type != "FP32" or type(cfg.ddf_shifting) is not bool or cfg.les_enabled
|
||||
or cfg.velocity != 0 or cfg.wall_slot_width or cfg.wall_normal_velocity):
|
||||
raise ValueError("experiment requires periodic unforced square FP32 D2Q9/SRT within admitted size")
|
||||
if uniform:
|
||||
return (cfg,)
|
||||
fine = replace(cfg, nx=2*cfg.nx, ny=2*cfg.ny, viscosity=2*cfg.viscosity)
|
||||
fine.validate()
|
||||
return cfg, fine
|
||||
|
||||
|
||||
def pack_plan(plan):
|
||||
"""Pack reviewed coordinate events; no production transfer builder imports."""
|
||||
n, nf = plan.n, plan.nf
|
||||
groups, receivers = plan.groups, plan.receivers
|
||||
data = dict(samples=np.zeros((len(groups), 10), np.uint32),
|
||||
origins=np.empty(len(groups), np.uint32), sizes=np.empty(len(groups), np.int32),
|
||||
weights=np.zeros((len(groups), 4, 10), np.float64),
|
||||
targets=(receivers[:, 0]*n*n+receivers[:, 1]).astype(np.uint32))
|
||||
lookup = {tuple(row): i for i, row in enumerate(receivers)}
|
||||
packets = [[] for _ in receivers]; incoming = [[], []]
|
||||
for i, g in enumerate(groups):
|
||||
size = len(g.samples)
|
||||
if size not in (6, 10):
|
||||
raise ValueError("unexpected interpolation stencil")
|
||||
data['samples'][i, :size] = g.q*n*n+g.samples
|
||||
data['origins'][i] = g.q*n*n+g.source
|
||||
data['sizes'][i] = size
|
||||
data['weights'][i, :, :size] = g.weights
|
||||
for k, event in enumerate(g.events):
|
||||
if event is None:
|
||||
packets[lookup[(g.q, g.receiver)]].append((-1, 4*i+k))
|
||||
else:
|
||||
m, slot = event
|
||||
incoming[m].append((g.q*nf*nf+slot, 4*i+k))
|
||||
for m, outgoing in enumerate(plan.outgoing):
|
||||
for q, source, target in outgoing:
|
||||
packets[lookup[(int(q), int(target))]].append((m, int(q*nf*nf+source)))
|
||||
if any(len(row) != 4 or len(set(row)) != 4 for row in packets):
|
||||
raise ValueError("each receiver needs four distinct quarter-packets")
|
||||
if len({packet for row in packets for packet in row}) != 4*len(receivers):
|
||||
raise ValueError("packet has multiple receiver owners")
|
||||
data['times'] = np.array([[t for t, _ in row] for row in packets], np.int32)
|
||||
data['sources'] = np.array([[s for _, s in row] for row in packets], np.uint32)
|
||||
for m, rows in enumerate(incoming):
|
||||
data[f'incoming{m}'] = np.array(rows, np.uint32)
|
||||
if len({slot for slot, _ in rows}) != len(rows):
|
||||
raise ValueError("duplicate incoming writer")
|
||||
return {key: np.ascontiguousarray(value) for key, value in data.items()}
|
||||
|
||||
|
||||
def table_hash(data):
|
||||
digest = hashlib.sha256()
|
||||
for key, value in sorted(data.items()):
|
||||
digest.update(f"{key}:{value.dtype}:{value.shape}".encode()+value.tobytes())
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def rounding_ledger(plan, tables, before, after, g0, predicted, fine_phases, pre, h, registers, *, shifted=False):
|
||||
"""Audit actual stored FP32 phases; never call a missing packet 'rounding'."""
|
||||
maps, n = plan.maps, plan.n
|
||||
f64 = lambda a: np.asarray(a, dtype=np.float64)
|
||||
difference = f64(g0[:, maps.coarse_active])-f64(before.coarse_f[:, maps.coarse_active])
|
||||
coarse_collision = difference.sum(axis=1)
|
||||
cell_mass = [difference.sum(axis=0)]
|
||||
collision = coarse_collision.copy(); fine_collision = []
|
||||
for f, g in fine_phases:
|
||||
difference = f64(g[:, maps.fine_active])-f64(f[:, maps.fine_active])
|
||||
fine_collision.append(.25*difference.sum(axis=1))
|
||||
cell_mass.append(.25*difference.sum(axis=0))
|
||||
collision += fine_collision[-1]
|
||||
np.testing.assert_array_equal(h, pre.astype(np.float32))
|
||||
origins = f64(g0).ravel()[tables['origins']]
|
||||
np.testing.assert_allclose(pre.mean(axis=1), origins, rtol=0, atol=2e-14)
|
||||
group = np.bincount(tables['origins']//(n*n), weights=f64(h).mean(axis=1)-origins, minlength=9)
|
||||
interpolation = np.bincount(tables['origins']//(n*n), weights=pre.mean(axis=1)-origins, minlength=9)
|
||||
child_cast = np.bincount(tables['origins']//(n*n), weights=f64(h).mean(axis=1)-pre.mean(axis=1), minlength=9)
|
||||
packet = np.empty(tables['times'].shape, np.float64)
|
||||
for when, source in ((-1, h), (0, fine_phases[0][1]), (1, fine_phases[1][1])):
|
||||
mask = tables['times'] == when
|
||||
packet[mask] = f64(source).ravel()[tables['sources'][mask]]
|
||||
p = f64(predicted).ravel()[tables['targets']]
|
||||
ideal = -p + .25*packet.sum(axis=1)
|
||||
reg_error = registers-ideal
|
||||
bound = 16*np.finfo(float).eps*(abs(p)+.25*abs(packet).sum(axis=1))
|
||||
if np.any(abs(reg_error) > bound):
|
||||
raise AssertionError("receiver packet register mismatch (not roundoff)")
|
||||
actual = after.coarse_f.ravel()[tables['targets']]
|
||||
np.testing.assert_array_equal(actual, (p+registers).astype(np.float32))
|
||||
q = tables['targets']//(n*n)
|
||||
store_error = np.bincount(q, weights=f64(actual)-(p+registers), minlength=9)
|
||||
register_error = np.bincount(q, weights=reg_error, minlength=9)
|
||||
# directional() must sum converted arrays, not accumulate in FP32.
|
||||
total = lambda s: ref.directional(ref.cpu.State(f64(s.coarse_f), f64(s.fine_f)), maps)
|
||||
net = total(after)-total(before)
|
||||
raw = net-collision
|
||||
unexplained = raw-group-store_error-register_error
|
||||
limit = float(64*np.finfo(float).eps*(total(before).sum()+int(shifted)*n*n))
|
||||
np.testing.assert_allclose(unexplained, 0, rtol=0, atol=limit)
|
||||
offset = _WEIGHTS.ravel()[tables['origins']//(n*n),None] if shifted else 0.
|
||||
return dict(raw=raw.tolist(), net_change=net.tolist(), physical_collision=collision.tolist(),
|
||||
coarse_collision=coarse_collision.tolist(), fine0_collision=fine_collision[0].tolist(),
|
||||
fine1_collision=fine_collision[1].tolist(), child_interpolation=interpolation.tolist(),
|
||||
child_cast=child_cast.tolist(),
|
||||
collision_cell_mass=[[float(a.sum()),float(a[a>0].sum()),float(a[a<0].sum()),float(abs(a).sum())]
|
||||
for a in cell_mass],
|
||||
child_rounding=group.tolist(), receiver_rounding=store_error.tolist(),
|
||||
register_rounding=register_error.tolist(), unexplained=unexplained.tolist(),
|
||||
unexplained_bound=limit, minimum_pre=float((pre+offset).min()),
|
||||
minimum_stored=float((f64(h)+offset).min()))
|
||||
|
||||
|
||||
class SpaceTimeGPU:
|
||||
"""Caller-owned context, one stream, explicit synchronized arrival state."""
|
||||
def __init__(self, config, bounds, *, context, mode='spacetime', identity=False, allow_n128=False):
|
||||
check_sources()
|
||||
self.configs = validate_config(config,allow_n128=allow_n128)
|
||||
if mode not in ('spacetime', 'frozen') or type(identity) is not bool:
|
||||
raise ValueError("unsupported interpolation or debug collision mode")
|
||||
self.bounds = tuple(bounds)
|
||||
if len(self.bounds) != 4 or any(isinstance(v, bool) or not isinstance(v, Integral) for v in self.bounds):
|
||||
raise ValueError("rectangle bounds must be four integers")
|
||||
self.plan = ref.Plan(config.nx, self.bounds)
|
||||
self.tables = pack_plan(self.plan)
|
||||
self.ctx, self.mode, self.identity = context, mode, identity
|
||||
self.allocations, self.modules, self.build_metadata = [], [], []
|
||||
self.status, self.step_count, self.device_bytes = 'new', 0, 0
|
||||
self.health_counts = np.zeros(6, np.uint64)
|
||||
import pycuda.driver as cuda
|
||||
from CelerisLab.cuda import compiler_v2 as compiler
|
||||
root = Path(__file__).resolve().parents[3]
|
||||
if Path(compiler._KERNEL_DIR).resolve() != root/'src/CelerisLab/lbm/kernels':
|
||||
raise RuntimeError("wrong production kernel checkout")
|
||||
self.cuda = cuda
|
||||
with context.activate():
|
||||
self.stream = cuda.Stream()
|
||||
try:
|
||||
source = Path(__file__).with_suffix('.cu')
|
||||
for cfg in self.configs:
|
||||
build = compiler.generate_config(cfg)
|
||||
try:
|
||||
Path(build, source.name).write_bytes(source.read_bytes())
|
||||
ptx = compiler.compile_kernel(context.sm_arch, source=source.name, build_dir=build)
|
||||
self.modules.append(compiler.load_module(ptx))
|
||||
import json
|
||||
metadata = json.loads(Path(ptx+'.json').read_text())
|
||||
metadata['test_entry_sha256'] = hashlib.sha256(source.read_bytes()).hexdigest()
|
||||
self.build_metadata.append(metadata)
|
||||
finally:
|
||||
compiler.cleanup_build_dir(build)
|
||||
self.device_tables = {key: self.upload(value) for key, value in self.tables.items()}
|
||||
self.active = self.upload(self.plan.maps.fine_active.astype(np.uint8))
|
||||
self.coarse = [self.allocate(9*config.nx**2*4) for _ in range(4)]
|
||||
self.fine = [self.allocate(9*(2*config.nx)**2*4) for _ in range(3)]
|
||||
self.pre = self.allocate(len(self.plan.groups)*4*8)
|
||||
self.h = self.allocate(len(self.plan.groups)*4*4)
|
||||
self.registers = self.allocate(len(self.plan.receivers)*8)
|
||||
self.counts = self.allocate(6*4)
|
||||
except BaseException:
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
def allocate(self, size):
|
||||
pointer = self.cuda.mem_alloc(size)
|
||||
self.allocations.append(pointer); self.device_bytes += size
|
||||
return pointer
|
||||
|
||||
def upload(self, array):
|
||||
pointer = self.allocate(array.nbytes)
|
||||
self.cuda.memcpy_htod(pointer, array)
|
||||
return pointer
|
||||
|
||||
def read(self, pointer, shape, dtype=np.float32):
|
||||
self.stream.synchronize()
|
||||
out = np.empty(shape, dtype)
|
||||
self.cuda.memcpy_dtoh(out, pointer)
|
||||
return out
|
||||
|
||||
def launch(self, level, name, *args, count=None):
|
||||
cfg = self.configs[level]
|
||||
nt = cfg.threads_per_block
|
||||
grid = ((cfg.nx+nt-1)//nt, cfg.ny) if count is None else ((count+nt-1)//nt, 1)
|
||||
self.modules[level].get_function(name)(*args, block=(nt,1,1), grid=grid, stream=self.stream)
|
||||
|
||||
def initialize(self, state):
|
||||
if (self.status not in ('new', 'ready') or state.phase != 'arrival_f'
|
||||
or isinstance(state.tick, bool) or not isinstance(state.tick, Integral) or state.tick != 0):
|
||||
raise ValueError("initial synchronized arrival state with tick=0 required")
|
||||
shifted = self.configs[0].ddf_shifting
|
||||
arrays = []
|
||||
for values, mask in zip((state.coarse_f,state.fine_f),
|
||||
(self.plan.maps.coarse_active,self.plan.maps.fine_active)):
|
||||
if np.shape(values) != (9,*mask.shape):
|
||||
raise ValueError("positive finite leaf populations and exact shapes required")
|
||||
a = _encode_physical(values,shifted)
|
||||
physical = _decode_stored(a,shifted)
|
||||
if not np.isfinite(physical[:,mask]).all() or (physical[:,mask] <= 0).any():
|
||||
raise ValueError("positive finite leaf populations and exact shapes required")
|
||||
a[:,~mask] = np.nan
|
||||
arrays.append(a)
|
||||
with self.ctx.activate():
|
||||
try:
|
||||
self.cuda.memcpy_htod(self.coarse[0], arrays[0])
|
||||
self.cuda.memcpy_htod(self.fine[0], arrays[1])
|
||||
except BaseException:
|
||||
self.status = 'failed'
|
||||
raise
|
||||
self.status, self.step_count = 'ready', 0
|
||||
self.health_counts.fill(0)
|
||||
|
||||
def _download_stored(self):
|
||||
"""Private audit/reset snapshot; values remain in their storage representation."""
|
||||
if self.status != 'ready':
|
||||
raise RuntimeError(f"GPU owner is {self.status}")
|
||||
try:
|
||||
with self.ctx.activate():
|
||||
arrays = [self.read(self.coarse[0], (9,self.plan.n,self.plan.n)),
|
||||
self.read(self.fine[0], (9,self.plan.nf,self.plan.nf))]
|
||||
except BaseException:
|
||||
self.status = 'failed'
|
||||
raise
|
||||
for a, mask in zip(arrays, (self.plan.maps.coarse_active,self.plan.maps.fine_active)):
|
||||
a[:,~mask] = np.nan
|
||||
return ref.cpu.State(*arrays, tick=2*self.step_count)
|
||||
|
||||
def download(self):
|
||||
"""Physical arrival populations; shifted values are reconstructed in FP64."""
|
||||
stored = self._download_stored()
|
||||
return ref.cpu.State(*(_decode_stored(a,self.configs[0].ddf_shifting)
|
||||
for a in (stored.coarse_f,stored.fine_f)),tick=stored.tick)
|
||||
|
||||
def step(self, *, mutation=0, audit=True):
|
||||
"""One interval; audit=False skips only host ledger snapshots, never health checks."""
|
||||
if (self.status != 'ready' or type(mutation) is not int or mutation not in range(5)
|
||||
or type(audit) is not bool or (mutation and not audit)):
|
||||
raise RuntimeError("ready owner and known test mutation required")
|
||||
before = self._download_stored() if audit else None
|
||||
n, nf, t = self.plan.n, self.plan.nf, self.device_tables
|
||||
ng, nr = len(self.plan.groups), len(self.plan.receivers)
|
||||
u32, null = np.uint32, np.uintp(0)
|
||||
omega = [np.float32(0 if self.identity else cfg.omega) for cfg in self.configs]
|
||||
with self.ctx.activate():
|
||||
try:
|
||||
self.status = 'advancing'
|
||||
self.cuda.memset_d32_async(self.counts, 0, 6, self.stream)
|
||||
fc, g0, predicted, gp = self.coarse
|
||||
self.launch(0, 'CoarseStart', fc,self.fine[0],g0,omega[0],
|
||||
*(u32(v) for v in self.bounds),self.counts)
|
||||
self.launch(0, 'Pull', g0,predicted,null,self.counts)
|
||||
self.launch(0, 'Collide', predicted,gp,null,omega[0],self.counts)
|
||||
self.launch(0, 'Interpolate', g0,gp,t['samples'],t['origins'],t['sizes'],t['weights'],
|
||||
self.pre,self.h,u32(ng),u32(self.mode=='frozen'),self.counts,count=ng)
|
||||
if audit:
|
||||
coarse_g = self.read(g0,(9,n,n)); prediction = self.read(predicted,(9,n,n))
|
||||
fine_phases = []
|
||||
for m in (0,1):
|
||||
f, out, g = self.fine
|
||||
if audit:
|
||||
old_f = self.read(f,(9,nf,nf))
|
||||
self.launch(1, 'Collide', f,g,self.active,omega[1],self.counts)
|
||||
if audit:
|
||||
fine_phases.append((old_f,self.read(g,(9,nf,nf))))
|
||||
self.launch(0, 'Register', predicted,g,self.h,t['targets'],t['sources'],t['times'],
|
||||
self.registers,u32(nr),np.int32(m),np.int32(mutation),count=nr)
|
||||
rows = self.tables[f'incoming{m}']
|
||||
self.launch(1, 'Inject', g,self.h,t[f'incoming{m}'],u32(len(rows)),u32(mutation==4),count=len(rows))
|
||||
self.launch(1, 'Pull', g,out,self.active,self.counts)
|
||||
self.fine[0], self.fine[1] = out, f
|
||||
self.launch(0, 'ApplyRegisters', predicted,self.registers,t['targets'],u32(nr),self.counts,count=nr)
|
||||
counts = self.read(self.counts,(6,),np.uint32)
|
||||
self.health_counts += counts.astype(np.uint64)
|
||||
if counts.any():
|
||||
raise AssertionError(f"boundary/leaf health failed: {counts.tolist()}")
|
||||
ledger = dict(boundary_budget_status='not_measured',unexplained=None)
|
||||
if audit:
|
||||
coarse_after = self.read(predicted,(9,n,n))
|
||||
coarse_after[:,~self.plan.maps.coarse_active] = np.nan
|
||||
after = ref.cpu.State(coarse_after,self.read(self.fine[0],(9,nf,nf)),tick=2*(self.step_count+1))
|
||||
ledger = rounding_ledger(self.plan,self.tables,before,after,coarse_g,prediction,fine_phases,
|
||||
self.read(self.pre,(ng,4),np.float64),self.read(self.h,(ng,4)),
|
||||
self.read(self.registers,(nr,),np.float64),
|
||||
shifted=self.configs[0].ddf_shifting)
|
||||
ledger['boundary_budget_status'] = 'measured'
|
||||
ledger['health_counts'] = counts.tolist()
|
||||
self.coarse[0], self.coarse[2] = predicted, fc
|
||||
self.step_count += 1
|
||||
self.status = 'ready'
|
||||
return ledger
|
||||
except BaseException:
|
||||
self.status = 'failed'
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
if self.status == 'closed':
|
||||
return
|
||||
with self.ctx.activate():
|
||||
error = None
|
||||
try:
|
||||
self.stream.synchronize()
|
||||
except Exception as exc:
|
||||
error = exc
|
||||
failed = []
|
||||
for pointer in self.allocations:
|
||||
try:
|
||||
pointer.free()
|
||||
except Exception as exc:
|
||||
error = error or exc
|
||||
failed.append(pointer)
|
||||
self.allocations = failed
|
||||
self.status = 'cleanup_failed' if failed else 'closed'
|
||||
if not failed:
|
||||
self.modules.clear()
|
||||
if error:
|
||||
raise error
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, kind, value, traceback):
|
||||
try:
|
||||
self.close()
|
||||
except Exception:
|
||||
if kind is None:
|
||||
raise
|
||||
|
||||
|
||||
def uniform_ledger(before, after, collided, *, shifted):
|
||||
"""Periodic streaming permutes each direction; only collision changes its sum."""
|
||||
a,b,g = [np.asarray(x,np.float64) for x in (before,after,collided)]
|
||||
collision=(g-a).sum(axis=(1,2)); net=b.sum(axis=(1,2))-a.sum(axis=(1,2))
|
||||
residual=net-collision
|
||||
bound=float(64*np.finfo(float).eps*(a.sum()+int(shifted)*a.shape[1]*a.shape[2]))
|
||||
np.testing.assert_allclose(residual,0,rtol=0,atol=bound)
|
||||
cells=(g-a).sum(axis=0)
|
||||
out={key:[0.]*9 for key in ('fine0_collision','fine1_collision','child_rounding',
|
||||
'child_interpolation','child_cast','register_rounding','receiver_rounding')}
|
||||
out.update(net_change=net.tolist(),physical_collision=collision.tolist(),coarse_collision=collision.tolist(),
|
||||
unexplained=residual.tolist(),unexplained_bound=bound,
|
||||
collision_cell_mass=[[float(cells.sum()),float(cells[cells>0].sum()),
|
||||
float(cells[cells<0].sum()),float(abs(cells).sum())],[0.]*4,[0.]*4],
|
||||
boundary_budget_status='not_applicable_uniform',inactive_components='fine, child, register, receiver')
|
||||
return out
|
||||
|
||||
|
||||
class UniformGPU(SpaceTimeGPU):
|
||||
"""One explicit arrival-F level; reuse the reviewed kernels and owner helpers."""
|
||||
def __init__(self, config, *, context, identity=False):
|
||||
check_sources()
|
||||
self.configs=validate_config(config,uniform=True)
|
||||
if type(identity) is not bool:
|
||||
raise ValueError('identity must be bool')
|
||||
self.ctx,self.identity=context,identity
|
||||
self.allocations,self.modules,self.build_metadata=[],[],[]
|
||||
self.status,self.step_count,self.device_bytes='new',0,0
|
||||
self.health_counts=np.zeros(6,np.uint64)
|
||||
import pycuda.driver as cuda
|
||||
from CelerisLab.cuda import compiler_v2 as compiler
|
||||
if Path(compiler._KERNEL_DIR).resolve()!=Path(__file__).resolve().parents[3]/'src/CelerisLab/lbm/kernels':
|
||||
raise RuntimeError('wrong production kernel checkout')
|
||||
self.cuda=cuda
|
||||
with context.activate():
|
||||
self.stream=cuda.Stream()
|
||||
try:
|
||||
source=Path(__file__).with_suffix('.cu')
|
||||
build=compiler.generate_config(self.configs[0])
|
||||
try:
|
||||
Path(build,source.name).write_bytes(source.read_bytes())
|
||||
ptx=compiler.compile_kernel(context.sm_arch,source=source.name,build_dir=build)
|
||||
self.modules.append(compiler.load_module(ptx))
|
||||
import json
|
||||
metadata=json.loads(Path(ptx+'.json').read_text())
|
||||
metadata['test_entry_sha256']=hashlib.sha256(source.read_bytes()).hexdigest()
|
||||
self.build_metadata.append(metadata)
|
||||
finally:
|
||||
compiler.cleanup_build_dir(build)
|
||||
n=config.nx
|
||||
self.buffers=[self.allocate(9*n*n*4) for _ in range(3)]
|
||||
self.active=self.upload(np.ones((n,n),np.uint8))
|
||||
self.counts=self.allocate(6*4)
|
||||
except BaseException:
|
||||
try:
|
||||
self.close()
|
||||
except BaseException:
|
||||
pass
|
||||
raise
|
||||
|
||||
def initialize(self, physical):
|
||||
cfg=self.configs[0]
|
||||
if self.status not in ('new','ready') or np.shape(physical)!=(9,cfg.ny,cfg.nx):
|
||||
raise ValueError('ready owner and exact uniform population shape required')
|
||||
stored=_encode_physical(physical,cfg.ddf_shifting)
|
||||
decoded=_decode_stored(stored,cfg.ddf_shifting)
|
||||
if not np.isfinite(decoded).all() or (decoded<=0).any():
|
||||
raise ValueError('positive finite physical populations required')
|
||||
try:
|
||||
with self.ctx.activate():
|
||||
self.cuda.memcpy_htod(self.buffers[0],stored)
|
||||
self.health_counts[:]=0; self.step_count=0; self.status='ready'
|
||||
except BaseException:
|
||||
self.status='failed'
|
||||
raise
|
||||
|
||||
def _download_stored(self):
|
||||
if self.status!='ready':
|
||||
raise RuntimeError('uniform owner is not ready')
|
||||
try:
|
||||
with self.ctx.activate():
|
||||
n=self.configs[0].nx
|
||||
return self.read(self.buffers[0],(9,n,n))
|
||||
except BaseException:
|
||||
self.status='failed'
|
||||
raise
|
||||
|
||||
def download(self):
|
||||
return _decode_stored(self._download_stored(),self.configs[0].ddf_shifting)
|
||||
|
||||
def step(self, *, audit=True):
|
||||
if self.status!='ready' or type(audit) is not bool:
|
||||
raise RuntimeError('ready owner and bool audit required')
|
||||
cfg=self.configs[0]; shape=(9,cfg.ny,cfg.nx)
|
||||
before=self._download_stored() if audit else None
|
||||
with self.ctx.activate():
|
||||
try:
|
||||
self.status='advancing'
|
||||
self.cuda.memset_d32_async(self.counts,0,6,self.stream)
|
||||
f,g,out=self.buffers
|
||||
self.launch(0,'Collide',f,g,self.active,np.float32(0 if self.identity else cfg.omega),self.counts)
|
||||
self.launch(0,'Pull',g,out,self.active,self.counts)
|
||||
counts=self.read(self.counts,(6,),np.uint32)
|
||||
self.health_counts+=counts.astype(np.uint64)
|
||||
if counts.any():
|
||||
raise AssertionError(f'uniform population health failed: {counts.tolist()}')
|
||||
ledger=uniform_ledger(before,self.read(out,shape),self.read(g,shape),shifted=cfg.ddf_shifting) if audit else dict(
|
||||
boundary_budget_status='not_measured',unexplained=None)
|
||||
ledger['health_counts']=counts.tolist()
|
||||
self.buffers[0],self.buffers[2]=out,f
|
||||
self.step_count+=1; self.status='ready'
|
||||
return ledger
|
||||
except BaseException:
|
||||
self.status='failed'
|
||||
raise
|
||||
@@ -0,0 +1,334 @@
|
||||
"""Experimental FP64 BVP coupling: one periodic rectangle, D2Q9, ratio two.
|
||||
|
||||
Guzik et al., https://www.osti.gov/servlets/purl/1226215, sections 3.2--3.5.
|
||||
Coordinate-generated stream events; CPUREF collision only, no production imports.
|
||||
This is a mathematical prototype, not a certified accuracy implementation.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import resource
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import cpu_reference as cpu
|
||||
|
||||
|
||||
class Spec(cpu.Spec):
|
||||
@property
|
||||
def n_fine(self):
|
||||
return 2 * self.n_coarse
|
||||
|
||||
|
||||
def geometry(n, bounds):
|
||||
x0, y0, x1, y1 = bounds
|
||||
if not (2 <= x0 < x1 <= n - 2 and 2 <= y0 < y1 <= n - 2):
|
||||
raise ValueError("one interior rectangle with two coarse cells of clearance required")
|
||||
y, x = np.indices((n, n))
|
||||
covered = (x >= x0) & (x < x1) & (y >= y0) & (y < y1)
|
||||
y, x = np.indices((2*n, 2*n))
|
||||
active = (x >= 2*x0) & (x < 2*x1) & (y >= 2*y0) & (y < 2*y1)
|
||||
inner = (x >= 2*x0-2) & (x < 2*x1+2) & (y >= 2*y0-2) & (y < 2*y1+2) & ~active
|
||||
rows, links = [], []
|
||||
for ky, kx in zip(*np.nonzero(~covered)):
|
||||
for q, (cy, cx) in enumerate(zip(cpu.CY, cpu.CX)):
|
||||
if any(active[2*ky+dy-cy, 2*kx+dx-cx]
|
||||
for dy in (0, 1) for dx in (0, 1)
|
||||
if 0 <= 2*ky+dy-cy < 2*n and 0 <= 2*kx+dx-cx < 2*n):
|
||||
rows.append((q, ky, kx))
|
||||
sy, sx = (ky-cy) % n, (kx-cx) % n
|
||||
if not covered[sy, sx]:
|
||||
links.append(cpu.CornerLink(f"{kx}_{ky}_{q}", q, (sx, sy), (kx, ky)))
|
||||
kind = np.where(active, 0, np.where(inner, 1, 2))
|
||||
return cpu.Maps(~covered, covered, active, inner, kind == 2, kind,
|
||||
np.arange(2*n)//2, (np.arange(2*n)+.5)/2,
|
||||
np.array(rows, dtype=int), tuple(links))
|
||||
|
||||
|
||||
@lru_cache(None)
|
||||
def interpolation(kind, points):
|
||||
"""Return data coordinates, evaluation matrix, and constrained coefficient map."""
|
||||
if kind == "face":
|
||||
samples = np.array([(s, t) for t in (0., 1.) for s in (-1., 0., 1.)])
|
||||
a = np.array([[1, s, t, s*t] for s, t in samples])
|
||||
e = np.array([[1, s, t, s*t] for s, t in points])
|
||||
b = e.mean(axis=0, keepdims=True)
|
||||
d = np.zeros((1, 6)); d[0, 1] = 1
|
||||
else:
|
||||
cross = ((0, 0), (-1, 0), (1, 0), (0, -1), (0, 1))
|
||||
samples = np.array([(x, y, t) for t in (0., 1.) for x, y in cross])
|
||||
a = np.array([[1, x, y, t] for x, y, t in samples])
|
||||
e = np.array([[1, x, y, t] for x, y, t in points])
|
||||
b = np.stack((e.mean(axis=0), [0, 0, 0, 1]))
|
||||
d = np.zeros((2, 10)); d[0, 0] = 1; d[1, 0] = -1; d[1, 5] = 1
|
||||
kkt = np.block([[a.T @ a, b.T], [b, np.zeros((len(b), len(b)))]])
|
||||
coeff = np.linalg.solve(kkt, np.vstack((a.T, d)))[:4]
|
||||
if not np.allclose(b @ coeff, d, rtol=0, atol=2e-14):
|
||||
raise AssertionError("invalid constrained interpolation matrix")
|
||||
return samples, a, e, coeff
|
||||
|
||||
|
||||
@dataclass
|
||||
class Group:
|
||||
q: int
|
||||
source: int
|
||||
receiver: int
|
||||
kind: str
|
||||
points: tuple
|
||||
samples: np.ndarray
|
||||
weights: np.ndarray
|
||||
events: tuple # (substep, fine ghost slot), or None for a leftover
|
||||
|
||||
|
||||
class Plan:
|
||||
def __init__(self, n, bounds):
|
||||
self.n, self.nf = n, 2*n
|
||||
self.maps = geometry(n, bounds)
|
||||
active = self.maps.fine_active
|
||||
lo, hi = np.array(bounds[1::-1]), np.array(bounds[3:1:-1])
|
||||
self.groups = []
|
||||
for iy, ix in zip(*np.nonzero(self.maps.coarse_active)):
|
||||
origin = np.array((iy, ix))
|
||||
for q in range(1, 9):
|
||||
c = np.array((cpu.CY[q], cpu.CX[q]))
|
||||
starts = [2*origin + (dy, dx) for dy in (0, 1) for dx in (0, 1)]
|
||||
events = []
|
||||
for p in starts:
|
||||
event = None
|
||||
for m in (0, 1):
|
||||
old, new = p+m*c, p+(m+1)*c
|
||||
inside = lambda z: bool(np.all(z >= 0) and np.all(z < 2*n) and active[tuple(z)])
|
||||
if not inside(old) and inside(new):
|
||||
event = (m, int(old[0]*2*n+old[1]))
|
||||
break
|
||||
events.append(event)
|
||||
if all(event is None for event in events):
|
||||
continue
|
||||
axes = [j for j in range(2) if origin[j] < lo[j] or origin[j] >= hi[j]]
|
||||
kind = "corner" if len(axes) == 2 else "face"
|
||||
points = []
|
||||
for p in starts:
|
||||
x = (p+.5)/2
|
||||
times = [(float(lo[j] if c[j] > 0 else hi[j])-x[j])/c[j] for j in axes]
|
||||
time = max(times)
|
||||
offset = x+c*time-(origin+.5+c*.5)
|
||||
points.append((float(offset[1]), float(offset[0]), float(time-.5))
|
||||
if kind == "corner" else (float(offset[1-axes[0]]), float(time-.5)))
|
||||
points = tuple(points)
|
||||
samples, _, e, coeff = interpolation(kind, points)
|
||||
indices = []
|
||||
for row in samples:
|
||||
shift = np.array((row[1], row[0]), dtype=int) if kind == "corner" else np.eye(2, dtype=int)[1-axes[0]]*int(row[0])
|
||||
pos = (origin+shift) % n
|
||||
indices.append(int(pos[0]*n+pos[1]))
|
||||
target = (origin+c) % n
|
||||
self.groups.append(Group(q, int(iy*n+ix), int(target[0]*n+target[1]), kind,
|
||||
points, np.array(indices), e @ coeff, tuple(events)))
|
||||
self.outgoing = []
|
||||
# m is zero-based: a substep-0 exit travels two fine spacings by
|
||||
# synchronization; a substep-1 exit travels one.
|
||||
for m in (0, 1):
|
||||
records = []
|
||||
for q in range(1, 9):
|
||||
c = np.array((cpu.CY[q], cpu.CX[q]))
|
||||
for y, x in zip(*np.nonzero(active)):
|
||||
p = np.array((y, x)); endpoint = p+c
|
||||
if not active[tuple(endpoint)]:
|
||||
target = (p+(2-m)*c)//2
|
||||
assert self.maps.coarse_active[tuple(target)]
|
||||
records.append((q, int(y*2*n+x), int(target[0]*n+target[1])))
|
||||
self.outgoing.append(np.array(records, dtype=int))
|
||||
incoming = [(m, g.q, slot) for g in self.groups for event in g.events
|
||||
if event is not None for m, slot in [event]]
|
||||
assert len(incoming) == len(set(incoming)), "duplicate incoming writer"
|
||||
expected = {(m, int(q), int((y-cpu.CY[q])*2*n+x-cpu.CX[q]))
|
||||
for m in (0, 1) for q in range(1, 9)
|
||||
for y, x in zip(*np.nonzero(active)) if not active[y-cpu.CY[q], x-cpu.CX[q]]}
|
||||
assert set(incoming) == expected, "incoming coverage mismatch"
|
||||
receivers = {(int(q), int(target)) for out in self.outgoing for q, _, target in out}
|
||||
receivers |= {(g.q, g.receiver) for g in self.groups if None in g.events}
|
||||
assert receivers == {(int(q), int(y*n+x)) for q, y, x in self.maps.coalesce}
|
||||
self.receivers = np.array(sorted(receivers), dtype=int)
|
||||
|
||||
|
||||
def directional(state, maps):
|
||||
return state.coarse_f[:, maps.coarse_active].sum(axis=1) + .25*state.fine_f[:, maps.fine_active].sum(axis=1)
|
||||
|
||||
|
||||
def advance(spec, plan, state, mode="spacetime", mutation=None):
|
||||
"""One arrival-to-arrival interval. 'frozen' disables space AND time variation."""
|
||||
if mode not in ("frozen", "spacetime"):
|
||||
raise ValueError(mode)
|
||||
n, nf, maps = plan.n, plan.nf, plan.maps
|
||||
if state.phase != "arrival_f" or state.tick % 2:
|
||||
raise ValueError("synchronized arrival state required")
|
||||
fc, ff = state.coarse_f.copy(), state.fine_f.copy()
|
||||
covered_mean = ff.reshape(9, n, 2, n, 2).mean(axis=(2, 4))
|
||||
fc[:, maps.coarse_covered] = covered_mean[:, maps.coarse_covered]
|
||||
all_cells = np.ones((n, n), dtype=bool)
|
||||
def collide(f, mask, tau):
|
||||
return cpu._collide(spec, f, np.isfinite(f), tau, mask, np.zeros(mask.shape, dtype=int))
|
||||
gc = collide(fc, all_cells, spec.tau_coarse)
|
||||
collision = (gc[:, maps.coarse_active]-fc[:, maps.coarse_active]).sum(axis=1)
|
||||
predicted = cpu._stream_periodic(gc)
|
||||
gp = collide(predicted, all_cells, spec.tau_coarse) if mode == "spacetime" else gc
|
||||
registers = np.zeros((9, n*n))
|
||||
q, target = plan.receivers.T
|
||||
registers[q, target] = -predicted.reshape(9, -1)[q, target]
|
||||
incoming = [[], []]
|
||||
group_error = 0.
|
||||
min_boundary = float("inf")
|
||||
negative_boundary = nonfinite_boundary = 0
|
||||
dropped = False
|
||||
for group in plan.groups:
|
||||
q = group.q
|
||||
count = len(group.samples)//2
|
||||
data = np.concatenate((gc.reshape(9, -1)[q, group.samples[:count]], gp.reshape(9, -1)[q, group.samples[count:]]))
|
||||
value = (group.weights @ data if mode == "spacetime"
|
||||
else np.full(4, gc.reshape(9, -1)[q, group.source]))
|
||||
min_boundary = min(min_boundary, float(value.min()))
|
||||
negative_boundary += int(np.count_nonzero(value < 0))
|
||||
nonfinite_boundary += int(np.count_nonzero(~np.isfinite(value)))
|
||||
group_error = max(group_error, abs(value.mean()-gc.reshape(9, -1)[q, group.source]))
|
||||
for k, event in enumerate(group.events):
|
||||
if event is None:
|
||||
if mutation == "omit_leftover" and not dropped:
|
||||
dropped = True
|
||||
else:
|
||||
registers[q, group.receiver] += .25*value[k]
|
||||
else:
|
||||
m, slot = event
|
||||
incoming[m].append((q, slot, value[k]))
|
||||
for m in (0, 1):
|
||||
gf = collide(ff, maps.fine_active, spec.tau_fine)
|
||||
collision += .25*(gf[:, maps.fine_active]-ff[:, maps.fine_active]).sum(axis=1)
|
||||
out = plan.outgoing[m]
|
||||
if mutation != f"omit_outgoing_{m}":
|
||||
np.add.at(registers, (out[:, 0], out[:, 2]), .25*gf.reshape(9, -1)[out[:, 0], out[:, 1]])
|
||||
for q, slot, value in incoming[m]:
|
||||
gf.reshape(9, -1)[q, slot] = value
|
||||
fresh = np.full_like(ff, np.nan)
|
||||
for q in range(9):
|
||||
moved = cpu._shift(gf[q], int(cpu.CY[q]), int(cpu.CX[q]), np.nan)
|
||||
fresh[q, maps.fine_active] = moved[maps.fine_active]
|
||||
if not np.isfinite(fresh[:, maps.fine_active]).all():
|
||||
raise AssertionError("invalid fine incoming population")
|
||||
ff = fresh
|
||||
final_c = predicted + registers.reshape(9, n, n)
|
||||
final_c[:, maps.coarse_covered] = np.nan
|
||||
final = cpu.State(final_c, ff, tick=state.tick+2)
|
||||
residual = directional(final, maps)-directional(state, maps)-collision
|
||||
minimum = min(float(final_c[:, maps.coarse_active].min()), float(ff[:, maps.fine_active].min()))
|
||||
return final, dict(directional_residual=residual, collision=collision,
|
||||
group_error=group_error, min_boundary=min_boundary, min_population=minimum,
|
||||
negative_boundary_count=negative_boundary, nonfinite_boundary_count=nonfinite_boundary)
|
||||
|
||||
|
||||
def write_receipt(path, result):
|
||||
"""Normalize NumPy scalars at the JSON boundary; reject other unknown types."""
|
||||
def scalar(value):
|
||||
if isinstance(value, np.generic):
|
||||
return value.item()
|
||||
raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
|
||||
path.write_text(json.dumps(result, indent=2, allow_nan=False, default=scalar)+"\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--n64", action="store_true", help="explicitly run the frozen 1024-interval experiment")
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--max-seconds",type=float,default=180.,help="wall-time cap, including plan generation")
|
||||
parser.add_argument("--max-rss-mib",type=float,default=256.,help="Linux peak-RSS screening cap")
|
||||
args = parser.parse_args()
|
||||
if not args.n64:
|
||||
parser.error("full trajectory requires --n64")
|
||||
if args.output.exists():
|
||||
parser.error("output exists; preserve prior evidence and choose a new path")
|
||||
if not (np.isfinite(args.max_seconds) and args.max_seconds>0 and np.isfinite(args.max_rss_mib) and args.max_rss_mib>0):
|
||||
parser.error("positive finite resource caps required")
|
||||
started = time.monotonic()
|
||||
sys.path.insert(0, str(Path(__file__).parents[1]/"taylor_green"))
|
||||
import oracle as tg
|
||||
plan = Plan(64, (8, 8, 56, 48))
|
||||
spec = Spec(n_coarse=64, patch_lo=8, halo=16, tau_coarse=.5576, tau_fine=.6152, collision="srt")
|
||||
arrays = [tg.initial((64,64), 0, 64, .0192), tg.initial((128,128), 1, 64, .0192)]
|
||||
for a, mask in zip(arrays, (plan.maps.coarse_active, plan.maps.fine_active)):
|
||||
a[:, ~mask] = np.nan
|
||||
initial = cpu.State(*arrays)
|
||||
mass0 = tg.metrics(arrays, (plan.maps.coarse_active, plan.maps.fine_active), 0, 64, .0192)["mass"]
|
||||
metrics = ("velocity_l2", "pressure_l2", "energy_relative")
|
||||
frozen_baseline = dict(zip(metrics, (.01131134574353476, .03592961319244149, .012514764591008386)))
|
||||
budget_bound = float(64*np.finfo(float).eps*mass0)
|
||||
root = Path(__file__).resolve().parents[3]
|
||||
source = dict(commit=subprocess.check_output(["git", "rev-parse", "HEAD"],cwd=root,text=True).strip(),
|
||||
dirty=bool(subprocess.check_output(["git","status","--porcelain"],cwd=root,text=True)),
|
||||
hashes={p.name:hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
for p in (Path(__file__),Path(cpu.__file__),Path(tg.__file__))},numpy=np.__version__)
|
||||
result = dict(scope="experimental CPU FP64; no GPU/accuracy certification", source=source,
|
||||
screen=dict(velocity_factor=.8, pressure_factor=1., energy_factor=1.,
|
||||
directional_step_bound=budget_bound, mass_relative=1e-11,
|
||||
momentum_absolute=1e-9, baseline_metric_atol=1e-10),
|
||||
limits=dict(seconds=args.max_seconds,rss_mib=args.max_rss_mib),runs=[],completed=False)
|
||||
def save():
|
||||
result["resources"] = dict(seconds=time.monotonic()-started,peak_rss_mib=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024)
|
||||
write_receipt(args.output, result)
|
||||
save()
|
||||
try:
|
||||
for mode in ("constant_ivp", "spacetime"):
|
||||
measured = mode == "spacetime"
|
||||
state = initial; record = dict(mode=mode,rows=[],
|
||||
boundary_budget_status="measured" if measured else "not_measured",
|
||||
max_directional_residual=0. if measured else None,
|
||||
minimum_boundary=None,negative_boundary_count=0 if measured else None,
|
||||
nonfinite_boundary_count=0 if measured else None)
|
||||
result["runs"].append(record)
|
||||
for k in range(1, 1025):
|
||||
if time.monotonic()-started>args.max_seconds or resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024>args.max_rss_mib:
|
||||
raise RuntimeError("experiment resource cap exceeded")
|
||||
if mode == "constant_ivp":
|
||||
state,_ = cpu.advance_coarse_once(spec,plan.maps,state)
|
||||
else:
|
||||
state,diagnostic = advance(spec,plan,state)
|
||||
error = float(np.max(np.abs(diagnostic["directional_residual"])))
|
||||
record["max_directional_residual"] = max(record["max_directional_residual"],error)
|
||||
if np.isfinite(diagnostic["min_boundary"]):
|
||||
record["minimum_boundary"] = (diagnostic["min_boundary"] if record["minimum_boundary"] is None else min(record["minimum_boundary"],diagnostic["min_boundary"]))
|
||||
for name in ("negative_boundary_count","nonfinite_boundary_count"):
|
||||
record[name] += diagnostic[name]
|
||||
if (not np.isfinite(error) or error>budget_bound or diagnostic["group_error"]>2e-14
|
||||
or diagnostic["negative_boundary_count"] or diagnostic["nonfinite_boundary_count"]
|
||||
or not np.isfinite(diagnostic["min_boundary"]) or diagnostic["min_boundary"]<=0
|
||||
or not np.isfinite(diagnostic["min_population"]) or diagnostic["min_population"]<=0):
|
||||
record["failed_diagnostic"] = {name: repr(value) for name,value in diagnostic.items()}
|
||||
raise AssertionError("candidate directional/health invariant failed")
|
||||
if k in (256,512,768,1024):
|
||||
row = tg.metrics((state.coarse_f,state.fine_f),(plan.maps.coarse_active,plan.maps.fine_active),k,64,.0192,mass0)
|
||||
row["momentum_change"] = (cpu.MOMENTS.T@(directional(state,plan.maps)-directional(initial,plan.maps)))[1:].tolist()
|
||||
record["rows"].append(row)
|
||||
save(); print(mode,k,json.dumps(row),flush=True)
|
||||
record["worst"] = {key:max(row[key] for row in record["rows"]) for key in metrics}
|
||||
if mode == "constant_ivp":
|
||||
for key,value in frozen_baseline.items():
|
||||
if abs(record["worst"][key]-value)>1e-10:
|
||||
raise AssertionError(f"frozen baseline mismatch: {key}")
|
||||
baseline,candidate = result["runs"]
|
||||
result["checks"] = {key:candidate["worst"][key]<=factor*baseline["worst"][key]
|
||||
for key,factor in zip(metrics,(.8,1.,1.))}
|
||||
result["checks"]["mass_momentum"] = all(row["mass_relative"]<=1e-11 and max(map(abs,row["momentum_change"]))<=1e-9 for row in candidate["rows"])
|
||||
result["completed"] = True
|
||||
result["experimental_pass"] = all(result["checks"].values())
|
||||
save()
|
||||
return 0 if result["experimental_pass"] else 1
|
||||
except Exception as exc:
|
||||
result["failure"] = dict(mode=mode,step=k,error=repr(exc))
|
||||
save()
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,92 @@
|
||||
"""N64/N128 admission check: exact topology and independently rational interpolation.
|
||||
|
||||
Raw FP64 bytes are diagnostic: different LAPACK builds need not produce them
|
||||
identically. The 2e-14 ceiling is the existing interpolation constraint policy,
|
||||
not a universal LAPACK error theorem. This module never changes runtime weights.
|
||||
"""
|
||||
from fractions import Fraction as F
|
||||
from functools import lru_cache
|
||||
|
||||
import numpy as np
|
||||
from spacetime_gpu import table_hash
|
||||
|
||||
STRUCTURE_SHA = 'cd1842952c4dd4df82da8015454c25da9be12b983270236cc65b4044fa38e6e4'
|
||||
STRUCTURES = {64: (STRUCTURE_SHA, 532),
|
||||
128: ('ee4ecb645674ae04eb87220a5217851405ccc0f45fbb0fe210499dcea5cd93b1', 1060)}
|
||||
ATOL = 2e-14
|
||||
|
||||
|
||||
def _transpose(a):
|
||||
return list(map(list, zip(*a)))
|
||||
|
||||
|
||||
def _multiply(a, b):
|
||||
return [[sum(x*y for x,y in zip(row,column)) for column in zip(*b)] for row in a]
|
||||
|
||||
|
||||
@lru_cache(None)
|
||||
def exact_weights(kind, points):
|
||||
"""Solve the small documented constrained fit using exact rational arithmetic."""
|
||||
if kind == 'face':
|
||||
a = [[F(1),F(s),F(t),F(s*t)] for t in (0,1) for s in (-1,0,1)]
|
||||
e = [[F(1),F(s),F(t),F(s)*F(t)] for s,t in points]
|
||||
b = [[sum(column)/4 for column in zip(*e)]]
|
||||
d = [[F(int(j==1)) for j in range(6)]]
|
||||
elif kind == 'corner':
|
||||
a = [[F(1),F(x),F(y),F(t)] for t in (0,1) for x,y in ((0,0),(-1,0),(1,0),(0,-1),(0,1))]
|
||||
e = [[F(1),F(x),F(y),F(t)] for x,y,t in points]
|
||||
b = [[sum(column)/4 for column in zip(*e)], [F(0),F(0),F(0),F(1)]]
|
||||
d = [[F(int(j==0)) for j in range(10)], [F(int(j==5)-int(j==0)) for j in range(10)]]
|
||||
else:
|
||||
raise ValueError('unsupported interpolation kind')
|
||||
kkt = [r+s for r,s in zip(_multiply(_transpose(a),a),_transpose(b))]
|
||||
kkt += [row+[F(0)]*len(b) for row in b]
|
||||
rhs = _transpose(a)+d
|
||||
n = len(kkt)
|
||||
augmented = [row+data for row,data in zip(kkt,rhs)]
|
||||
for j in range(n):
|
||||
pivot = next(i for i in range(j,n) if augmented[i][j])
|
||||
augmented[j],augmented[pivot] = augmented[pivot],augmented[j]
|
||||
divisor = augmented[j][j]
|
||||
augmented[j] = [v/divisor for v in augmented[j]]
|
||||
for i in range(n):
|
||||
if i != j:
|
||||
factor = augmented[i][j]
|
||||
augmented[i] = [x-factor*y for x,y in zip(augmented[i],augmented[j])]
|
||||
solution = [row[n:] for row in augmented]
|
||||
if _multiply(kkt,solution) != rhs:
|
||||
raise AssertionError('exact interpolation solve failed')
|
||||
return np.array(_multiply(e,solution[:4]), dtype=np.float64)
|
||||
|
||||
|
||||
def validate_tables(plan, tables, *, allow_n128=False):
|
||||
"""Reject topology, layout, padding, operator, or constraint changes separately."""
|
||||
structure = table_hash({key:value for key,value in tables.items() if key != 'weights'})
|
||||
admitted = (64,128) if allow_n128 is True else (64,)
|
||||
if plan.n not in admitted or structure != STRUCTURES[plan.n][0]:
|
||||
raise ValueError('frozen integer topology changed')
|
||||
w = tables['weights']
|
||||
if (w.dtype != np.dtype(np.float64) or w.shape != (STRUCTURES[plan.n][1],4,10) or len(plan.groups) != w.shape[0]
|
||||
or not all(a.flags.c_contiguous for a in tables.values()) or not np.isfinite(w).all()):
|
||||
raise ValueError('weight dtype/shape/layout/finiteness changed')
|
||||
errors = dict(coefficient=0., constant=0., mean=0., temporal=0.)
|
||||
for i,g in enumerate(plan.groups):
|
||||
size = len(g.samples)
|
||||
if size != tables['sizes'][i] or size != {'face':6,'corner':10}.get(g.kind) or len(g.points) != 4:
|
||||
raise ValueError('interpolation group coverage/size changed')
|
||||
if w[i,:,size:].tobytes() != np.zeros((4,10-size),np.float64).tobytes():
|
||||
raise ValueError('weight padding is not exact positive zero')
|
||||
actual = w[i,:,:size]
|
||||
errors['coefficient'] = max(errors['coefficient'],float(abs(actual-exact_weights(g.kind,g.points)).max()))
|
||||
errors['constant'] = max(errors['constant'],float(abs(actual.sum(axis=1)-1).max()))
|
||||
central = np.zeros(size); central[1 if g.kind=='face' else 0] = 1
|
||||
errors['mean'] = max(errors['mean'],float(abs(actual.mean(axis=0)-central).max()))
|
||||
if g.kind == 'corner':
|
||||
early,late = (g.points.index((0.,0.,t)) for t in (-.25,.25))
|
||||
slope = np.zeros(size); slope[0],slope[5] = -1,1
|
||||
errors['temporal'] = max(errors['temporal'],float(abs(2*(actual[late]-actual[early])-slope).max()))
|
||||
if any(error > ATOL for error in errors.values()):
|
||||
raise ValueError(f'exact interpolation/constraint check failed: {errors}; bound={ATOL}')
|
||||
return dict(status='passed', structure_sha256=structure, raw_table_sha256=table_hash(tables),
|
||||
coefficient_oracle='exact rational KKT, rounded once to FP64', atol=ATOL, rtol=0.,
|
||||
maximum_absolute_errors=errors, positive_zero_padding=True)
|
||||
@@ -0,0 +1,239 @@
|
||||
"""Host-only experimental union event contract; no solver or runtime admission.
|
||||
|
||||
Consumes existing level-wide union geometry, never its legacy transfer tables.
|
||||
The frozen 707afe6 rectangle oracle remains independent and unchanged. Topology
|
||||
acceptance proves packet accounting, not interpolation accuracy or GPU support.
|
||||
"""
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import spacetime_reference as ref
|
||||
|
||||
|
||||
class UnresolvedTopology(ValueError):
|
||||
"""An experimental coupling limitation, not a production geometry rejection."""
|
||||
def __init__(self, code, **detail):
|
||||
self.code, self.detail = code, detail
|
||||
super().__init__(f"experimental space-time topology unresolved: {code}: {detail}")
|
||||
|
||||
|
||||
def owned_slots(layout, directions, cells):
|
||||
"""Map dense logical reads/writes to existing owner-core storage, not halos."""
|
||||
cells = np.asarray(cells)
|
||||
if not np.issubdtype(cells.dtype, np.integer) or np.any((cells < 0) | (cells >= np.prod(layout.shape))):
|
||||
raise ValueError('invalid dense cell index')
|
||||
owner = layout.owner.ravel()[cells]
|
||||
if np.any(owner == layout.hole):
|
||||
raise UnresolvedTopology('missing_block_owner', cells=cells[owner == layout.hole].tolist())
|
||||
return layout.slot(owner, directions)
|
||||
|
||||
|
||||
def _first_entry(covered, origin, child, c):
|
||||
"""First continuous union entry of one characteristic, in exact quarter times.
|
||||
|
||||
Probe the cells immediately before/after each crossed grid line using integer
|
||||
eighth-cell coordinates. Return only actual exterior faces of the UNION.
|
||||
"""
|
||||
start4 = 4*origin + 2*child + 1
|
||||
for t4 in (1, 3):
|
||||
before = (2*start4 + c*(2*t4-1))//8
|
||||
after = (2*start4 + c*(2*t4+1))//8
|
||||
if not covered[tuple(before)] and covered[tuple(after)]:
|
||||
axes = []
|
||||
for j in range(2):
|
||||
if before[j] != after[j]:
|
||||
neighbor = after.copy(); neighbor[j] = before[j]
|
||||
if not covered[tuple(neighbor)]: axes.append(j)
|
||||
return t4, axes
|
||||
return None
|
||||
|
||||
|
||||
def _crossing_time(covered, origin, position, c, axes):
|
||||
"""Fig.11 convex T uses the last plane; Fig.13 concave T uses the first.
|
||||
|
||||
Both use the same KKT formulation on different boundary geometry.
|
||||
DOI/primary PDF: https://www.osti.gov/servlets/purl/1226215, §3.5.1.
|
||||
"""
|
||||
times=[(origin[j]+int(c[j]>0)-position[j])/c[j] for j in axes]
|
||||
if len(axes)==1: return times[0]
|
||||
sides=[bool(covered[tuple(origin+np.eye(2,dtype=int)[j]*c[j])]) for j in axes]
|
||||
if sides[0]!=sides[1]:
|
||||
raise UnresolvedTopology('mixed_corner_support',source_yx=origin.tolist())
|
||||
return min(times) if all(sides) else max(times)
|
||||
|
||||
|
||||
class UnionPlan:
|
||||
"""Two-level, level-wide square geometry; all coordinates are level-local.
|
||||
|
||||
Assumes a complete parent domain; does not validate nested parent coverage.
|
||||
``pair`` is existing FixedPatchSet2D geometry, even for one rectangle. Scalar
|
||||
window origins and nested state coverage need their own contract; they are
|
||||
explicitly unadmitted here. No global rectangle-gap rule is imposed.
|
||||
"""
|
||||
phase = 'arrival_f'
|
||||
runtime_supported = False
|
||||
interpolation_accuracy = 'unqualified_for_new_unions'
|
||||
|
||||
def __init__(self, pair):
|
||||
ny, nx = pair.coarse_shape
|
||||
self.origin = tuple(pair.fine_origin)
|
||||
if ny != nx or pair.fine_shape != (2*ny, 2*nx) or self.origin != (0., 0.):
|
||||
raise UnresolvedTopology('requires_level_wide_square_pair',
|
||||
coarse_shape=pair.coarse_shape, fine_shape=pair.fine_shape, origin=self.origin)
|
||||
self.n, self.nf = nx, 2*nx
|
||||
covered = np.array(pair.covered, dtype=bool, copy=True)
|
||||
active = np.kron(covered, np.ones((2, 2), bool))
|
||||
if not np.array_equal(active, pair.fine_kind == 0):
|
||||
raise UnresolvedTopology('non_pair_active_coverage')
|
||||
self.maps = SimpleNamespace(coarse_active=~covered, coarse_covered=covered, fine_active=active)
|
||||
self.outgoing, transit = [[], []], []
|
||||
# Guzik et al. §3.5.4, Fig.19 (https://www.osti.gov/servlets/purl/1226215):
|
||||
# retain substep-0 outgoing G at source, then fill the substep-1 ghost.
|
||||
# Rows are (q, fine source, next ghost); no covered coarse receiver credit.
|
||||
for q in range(1, 9):
|
||||
c = np.array((ref.cpu.CY[q], ref.cpu.CX[q]))
|
||||
boundary = active & ~np.roll(active, tuple(-c), axis=(0, 1))
|
||||
for y, x in zip(*np.nonzero(boundary)):
|
||||
p = np.array((y, x))
|
||||
for m in (0, 1):
|
||||
target = (p+(2-m)*c)//2
|
||||
if covered[tuple(target)]:
|
||||
if m != 0 or not active[tuple(p+2*c)]:
|
||||
raise UnresolvedTopology('unsupported_transit', q=q, substep=m, source_yx=p.tolist())
|
||||
ghost=p+c
|
||||
transit.append((q,int(y*self.nf+x),int(ghost[0]*self.nf+ghost[1])))
|
||||
else:
|
||||
self.outgoing[m].append((q, int(y*self.nf+x), int(target[0]*nx+target[1])))
|
||||
self.outgoing = [np.array(x, dtype=np.int64).reshape(-1, 3) for x in self.outgoing]
|
||||
self.transit = np.array(transit,dtype=np.int64).reshape(-1,3)
|
||||
self.groups = []
|
||||
for iy, ix in zip(*np.nonzero(~covered)):
|
||||
origin = np.array((iy, ix))
|
||||
# Two-cell production margins make all nearby probes in bounds.
|
||||
if not covered[max(0,iy-1):iy+2,max(0,ix-1):ix+2].any(): continue
|
||||
for q in range(1, 9):
|
||||
c = np.array((ref.cpu.CY[q], ref.cpu.CX[q]))
|
||||
children = [np.array((dy, dx)) for dy in (0, 1) for dx in (0, 1)]
|
||||
starts = [2*origin+child for child in children]
|
||||
events = []
|
||||
for p in starts:
|
||||
event = None
|
||||
for m in (0, 1):
|
||||
old, new = p+m*c, p+(m+1)*c
|
||||
if not active[tuple(old)] and active[tuple(new)]:
|
||||
event = (m, int(old[0]*self.nf+old[1])); break
|
||||
events.append(event)
|
||||
if all(e is None for e in events): continue
|
||||
entries = [_first_entry(covered, origin, child, c) for child in children]
|
||||
axes = sorted({j for entry in entries if entry is not None for j in entry[1]})
|
||||
if not axes:
|
||||
raise UnresolvedTopology('missing_exterior_support', source_yx=origin.tolist(), q=q)
|
||||
points = []
|
||||
for p, entry, event in zip(starts, entries, events):
|
||||
position = (p+.5)/2
|
||||
time = _crossing_time(covered,origin,position,c,axes)
|
||||
if entry is not None and (4*time != entry[0] or event is None):
|
||||
raise UnresolvedTopology('nonconvex_entry_support', source_yx=origin.tolist(), q=q,
|
||||
quarter_time=entry[0], support_quarter_time=float(4*time))
|
||||
offset = position+c*time-(origin+.5+c*.5)
|
||||
points.append((float(offset[1]), float(offset[0]), float(time-.5)) if len(axes)==2
|
||||
else (float(offset[1-axes[0]]), float(time-.5)))
|
||||
kind = 'corner' if len(axes)==2 else 'face'; points = tuple(points)
|
||||
samples, _, evaluation, coefficients = ref.interpolation(kind, points)
|
||||
indices = []
|
||||
for row in samples:
|
||||
shift = np.array((row[1],row[0]), int) if kind=='corner' else np.eye(2,dtype=int)[1-axes[0]]*int(row[0])
|
||||
pos = (origin+shift) % nx; indices.append(int(pos[0]*nx+pos[1]))
|
||||
target = (origin+c) % nx
|
||||
self.groups.append(ref.Group(q,int(iy*nx+ix),int(target[0]*nx+target[1]),kind,
|
||||
points,np.array(indices),evaluation@coefficients,tuple(events)))
|
||||
receivers = {(int(q),int(target)) for rows in self.outgoing for q,_,target in rows}
|
||||
receivers |= {(g.q,g.receiver) for g in self.groups if None in g.events}
|
||||
self.receivers = np.array(sorted(receivers),dtype=np.int64).reshape(-1,2)
|
||||
self.proof = validate_ownership(self)
|
||||
|
||||
|
||||
def validate_ownership(plan):
|
||||
"""Independent mask-based event coverage and four-quarter receiver accounting."""
|
||||
n, nf, active = plan.n, plan.nf, plan.maps.fine_active
|
||||
actual = [(m,g.q,slot) for g in plan.groups for e in g.events if e is not None for m,slot in [e]]
|
||||
expected_transit={(q,int(y*nf+x),int((y+ref.cpu.CY[q])*nf+x+ref.cpu.CX[q]))
|
||||
for q in range(1,9) for y,x in zip(*np.nonzero(active))
|
||||
if not active[y+ref.cpu.CY[q],x+ref.cpu.CX[q]]
|
||||
and active[y+2*ref.cpu.CY[q],x+2*ref.cpu.CX[q]]}
|
||||
routes=[tuple(map(int,row)) for row in plan.transit]
|
||||
if len(routes)!=len(set(routes)) or set(routes)!=expected_transit:
|
||||
raise ValueError('transit provenance/uniqueness/coverage failed')
|
||||
actual += [(1,q,ghost) for q,source,ghost in routes]
|
||||
expected = {(m,q,int((y-ref.cpu.CY[q])*nf+x-ref.cpu.CX[q]))
|
||||
for m in (0,1) for q in range(1,9) for y,x in zip(*np.nonzero(active))
|
||||
if not active[y-ref.cpu.CY[q],x-ref.cpu.CX[q]]}
|
||||
if len(actual)!=len(set(actual)) or set(actual)!=expected:
|
||||
raise ValueError('incoming writer uniqueness/coverage failed')
|
||||
packets = {tuple(map(int,row)):[] for row in plan.receivers}
|
||||
if len(packets)!=len(plan.receivers): raise ValueError('duplicate coarse receiver')
|
||||
anchors = set()
|
||||
for i,g in enumerate(plan.groups):
|
||||
if not 1<=g.q<9 or not 0<=g.source<n*n or not plan.maps.coarse_active.ravel()[g.source]:
|
||||
raise ValueError('invalid coarse source anchor')
|
||||
if (g.q,g.source) in anchors: raise ValueError('duplicate coarse source group')
|
||||
anchors.add((g.q,g.source))
|
||||
origin=np.array(divmod(g.source,n)); c=np.array((ref.cpu.CY[g.q],ref.cpu.CX[g.q]))
|
||||
target=(origin+c)%n
|
||||
if g.receiver!=target[0]*n+target[1]: raise ValueError('group receiver provenance failed')
|
||||
ordered=[]; entries=[]
|
||||
for dy,dx in ((0,0),(0,1),(1,0),(1,1)):
|
||||
p=2*origin+(dy,dx); event=None
|
||||
for m in (0,1):
|
||||
old,new=p+m*c,p+(m+1)*c
|
||||
if not active[tuple(old)] and active[tuple(new)]:
|
||||
event=(m,int(old[0]*nf+old[1])); break
|
||||
ordered.append(event)
|
||||
entries.append(_first_entry(plan.maps.coarse_covered,origin,np.array((dy,dx)),c))
|
||||
if tuple(ordered)!=g.events or all(e is None for e in ordered):
|
||||
raise ValueError('ordered child characteristic provenance failed')
|
||||
axes=sorted({j for e in entries if e is not None for j in e[1]})
|
||||
if not axes or g.kind!=('corner' if len(axes)==2 else 'face'):
|
||||
raise ValueError('group supporting axes differ')
|
||||
samples,_,evaluation,coefficients=ref.interpolation(g.kind,g.points)
|
||||
indices=[]
|
||||
for row in samples:
|
||||
shift=np.array((row[1],row[0]),int) if g.kind=='corner' else np.eye(2,dtype=int)[1-axes[0]]*int(row[0])
|
||||
pos=(origin+shift)%n; indices.append(int(pos[0]*n+pos[1]))
|
||||
if not np.array_equal(g.samples,indices): raise ValueError('sample anchor provenance failed')
|
||||
for k,(dy,dx) in enumerate(((0,0),(0,1),(1,0),(1,1))):
|
||||
position=origin+(np.array((dy,dx))+.5)/2
|
||||
time=_crossing_time(plan.maps.coarse_covered,origin,position,c,axes)
|
||||
offset=position+c*time-(origin+.5+c*.5)
|
||||
point=(float(offset[1]),float(offset[0]),float(time-.5)) if g.kind=='corner' else (float(offset[1-axes[0]]),float(time-.5))
|
||||
if g.points[k]!=point: raise ValueError('evaluation point provenance failed')
|
||||
source=np.zeros(len(g.samples)); source[0 if g.kind=='corner' else 1]=1
|
||||
if (not np.allclose(g.weights,evaluation@coefficients,rtol=0,atol=2e-14)
|
||||
or not np.allclose(g.weights.mean(axis=0),source,rtol=0,atol=2e-14)):
|
||||
raise ValueError('group coefficients/directional budget failed')
|
||||
for k,event in enumerate(g.events):
|
||||
if event is None:
|
||||
if (g.q,g.receiver) not in packets: raise ValueError('missing leftover receiver')
|
||||
packets[(g.q,g.receiver)].append(('leftover',i,k))
|
||||
for m,rows in enumerate(plan.outgoing):
|
||||
expected_out = {(q,int(y*nf+x),int(((y+(2-m)*ref.cpu.CY[q])//2)*n+(x+(2-m)*ref.cpu.CX[q])//2))
|
||||
for q in range(1,9) for y,x in zip(*np.nonzero(active))
|
||||
if not active[y+ref.cpu.CY[q],x+ref.cpu.CX[q]]
|
||||
and plan.maps.coarse_active[(y+(2-m)*ref.cpu.CY[q])//2,(x+(2-m)*ref.cpu.CX[q])//2]}
|
||||
actual_out = [tuple(map(int,row)) for row in rows]
|
||||
if len(actual_out)!=len(set(actual_out)) or set(actual_out)!=expected_out:
|
||||
raise ValueError('outgoing uniqueness/coverage failed')
|
||||
for q,slot,target in actual_out: packets[(q,target)].append(('outgoing',m,q,slot))
|
||||
expected_receivers = {(q,int(y*n+x)) for y,x in zip(*np.nonzero(plan.maps.coarse_active))
|
||||
for q in range(1,9) if any(active[2*y+dy-ref.cpu.CY[q],2*x+dx-ref.cpu.CX[q]]
|
||||
for dy in (0,1) for dx in (0,1)
|
||||
if 0<=2*y+dy-ref.cpu.CY[q]<nf and 0<=2*x+dx-ref.cpu.CX[q]<nf)}
|
||||
if set(packets)!=expected_receivers: raise ValueError('coarse receiver coverage failed')
|
||||
tokens=[token for row in packets.values() for token in row]
|
||||
if any(len(row)!=4 for row in packets.values()) or len(tokens)!=len(set(tokens)):
|
||||
raise ValueError('receiver needs four distinct quarter-packets')
|
||||
return dict(topology='supported',incoming_events=len(actual),outgoing_events=sum(map(len,plan.outgoing)),
|
||||
transit_events=len(routes),
|
||||
groups=len(plan.groups),receivers=len(packets),receiver_quarter_packets=len(tokens),
|
||||
interpolation_accuracy=plan.interpolation_accuracy,runtime_supported=False,
|
||||
scope='pair-local ownership/coefficients; complete parent domain assumed, nested coverage unvalidated')
|
||||
@@ -0,0 +1,260 @@
|
||||
"""Optional observation-window exchange ledger: host contract (mocked driver) and GPU gate.
|
||||
|
||||
Run CPU and GPU classes in separate processes; GPU callers select the device.
|
||||
ExchangeWindowGPU uses the SRT/unshifted Jet fixture; ExchangeWindowShiftedMRTGPU covers a
|
||||
shifted MRT, all-blocked, stationary-body case without Jets (no shifted-MRT Jet claim).
|
||||
GPU tolerance (conservative engineering tolerance, not a universal proof): the window ledger
|
||||
sums h*dt*raw per link over fine substeps, while the reference sums per-base interval ledgers
|
||||
over links and then over base steps. Both are FP64 reorderings of the same FP32-exact substep
|
||||
contributions, so the scale is the sum of the ACTUAL absolute substep contributions (read from
|
||||
last_raw after every fine hop), not of per-base totals that could hide substep cancellation:
|
||||
|window - reference| <= 4 * m * eps * sum|substep terms| + 1e-300, m = links * substeps.
|
||||
"""
|
||||
from contextlib import ExitStack
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
import pycuda.driver as cuda
|
||||
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
from test_refinement_blocked_base import mocked_backend
|
||||
from test_refinement_blocked_fine import BOUNDS, circle, config
|
||||
|
||||
EPS = np.finfo(np.float64).eps
|
||||
|
||||
|
||||
def build(m, window=True, raw=True):
|
||||
return FixedPatchStepper(config("periodic"), BOUNDS, context=m["context"], base_block=(3, 4),
|
||||
fine_block=(16, 20), fine_objects=[circle()],
|
||||
observe_raw_exchange=raw, observe_window_exchange=window)
|
||||
|
||||
|
||||
def same_bits(test, a, b):
|
||||
"""Identical NaN masks, no infinities, identical finite FP32 bits (incl. signed zero)."""
|
||||
a, b = np.asarray(a), np.asarray(b)
|
||||
test.assertEqual(a.dtype, b.dtype)
|
||||
test.assertFalse(np.isinf(a).any() or np.isinf(b).any())
|
||||
np.testing.assert_array_equal(np.isnan(a), np.isnan(b))
|
||||
finite = np.isfinite(a)
|
||||
np.testing.assert_array_equal(a[finite].view(np.uint32), b[finite].view(np.uint32))
|
||||
|
||||
|
||||
def accumulations(m):
|
||||
return [c for c in m["calls"] if c[0] == "BlockAccumulateExchange"]
|
||||
|
||||
|
||||
class ExchangeWindowCPU(unittest.TestCase):
|
||||
def test_constructor_validation(self):
|
||||
with mocked_backend() as m:
|
||||
with self.assertRaisesRegex(ValueError, "requires observe_raw_exchange"):
|
||||
build(m, window=True, raw=False)
|
||||
with self.assertRaisesRegex(ValueError, "must be bool"):
|
||||
build(m, window=1)
|
||||
|
||||
def test_state_machine_and_launches(self):
|
||||
with mocked_backend() as m, ExitStack() as stack:
|
||||
stack.enter_context(patch.object(cuda, "memset_d8"))
|
||||
stack.enter_context(patch.object(cuda, "memset_d8_async"))
|
||||
stack.enter_context(patch.object(cuda, "memcpy_dtoh",
|
||||
side_effect=lambda out, src: out.fill(0)))
|
||||
backend = build(m)
|
||||
b = backend._level_boundaries[1]
|
||||
self.assertIsNotNone(b.window_integral)
|
||||
with self.assertRaises(RuntimeError):
|
||||
backend.start_exchange_window() # not initialized
|
||||
arrays = [np.full(level.shape, 1 / 9, np.float32) for level in backend._levels]
|
||||
backend.initialize(*arrays)
|
||||
with self.assertRaisesRegex(RuntimeError, "no valid exchange window"):
|
||||
backend.read_exchange_window(0)
|
||||
backend.step(1)
|
||||
self.assertEqual(len(accumulations(m)), 2) # closed window: interval ledger only
|
||||
backend.start_exchange_window()
|
||||
with self.assertRaisesRegex(RuntimeError, "at least one completed"):
|
||||
backend.read_exchange_window(0)
|
||||
m["calls"].clear()
|
||||
backend.step(2)
|
||||
calls = accumulations(m)
|
||||
self.assertEqual(len(calls), 8) # two fine hops x two ledgers x two base steps
|
||||
self.assertEqual({id(c[1][1]) for c in calls},
|
||||
{id(b.interval_integral), id(b.window_integral)})
|
||||
out = backend.read_exchange_window(0)
|
||||
self.assertEqual((out["time_start"], out["time_end"], out["sample_count"]), (1., 3, 4))
|
||||
self.assertEqual(backend.read_exchange_window(0)["time_end"], 3) # non-destructive
|
||||
backend.initialize(*arrays) # invalidates
|
||||
with self.assertRaisesRegex(RuntimeError, "no valid exchange window"):
|
||||
backend.read_exchange_window(0)
|
||||
backend.start_exchange_window()
|
||||
with patch.object(backend, "_hop", side_effect=RuntimeError("injected")):
|
||||
with self.assertRaises(RuntimeError):
|
||||
backend.step(1)
|
||||
self.assertFalse(backend._window_valid)
|
||||
with self.assertRaises(RuntimeError):
|
||||
backend.read_exchange_window(0) # failed status
|
||||
|
||||
def test_disabled_default_has_no_window_cost(self):
|
||||
with mocked_backend() as m, ExitStack() as stack:
|
||||
stack.enter_context(patch.object(cuda, "memset_d8_async"))
|
||||
backend = build(m, window=False)
|
||||
self.assertIsNone(backend._level_boundaries[1].window_integral)
|
||||
backend.initialize(*[np.full(l.shape, 1 / 9, np.float32) for l in backend._levels])
|
||||
backend.step(1)
|
||||
self.assertEqual(len(accumulations(m)), 2)
|
||||
with self.assertRaisesRegex(RuntimeError, "disabled"):
|
||||
backend.start_exchange_window()
|
||||
|
||||
|
||||
class ExchangeWindowGPU(unittest.TestCase):
|
||||
def test_window_matches_summed_base_reads_and_populations_unchanged(self):
|
||||
from test_raw_exchange_gpu import BOUNDS as RB, CFG, circle as rc, jet, state
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
with CudaContext(0) as ctx:
|
||||
kw = dict(context=ctx, fine_block=(16, 20), fine_objects=[rc()], jets=[jet()],
|
||||
observe_raw_exchange=True)
|
||||
with FixedPatchStepper(CFG, RB, observe_window_exchange=True, **kw) as windowed, \
|
||||
FixedPatchStepper(CFG, RB, **kw) as plain:
|
||||
arrays = [state(level.shape) for level in windowed._levels]
|
||||
windowed.initialize(*arrays)
|
||||
plain.initialize(*arrays)
|
||||
windowed.step(2)
|
||||
plain.step(2)
|
||||
windowed.start_exchange_window()
|
||||
b = windowed._level_boundaries[1]
|
||||
count = len(b.tables["cell"])
|
||||
factors = np.array([.25, .25, .125, .25]) # h*dt, h*h*dt, h*h for level-1 h=.5
|
||||
scale = np.zeros(4)
|
||||
substeps = 0
|
||||
kernel = windowed._levels[1].kernel
|
||||
|
||||
def audited(src, dst, *args, **kw):
|
||||
nonlocal scale, substeps
|
||||
windowed._stream.synchronize() # BFL and both accumulations are complete
|
||||
raw = np.empty((count, 4), np.float32)
|
||||
cuda.memcpy_dtoh(raw, b.last_raw)
|
||||
scale = scale + (np.abs(raw.astype(np.float64)) * factors).sum(axis=0)
|
||||
substeps += 1
|
||||
kernel(src, dst, *args, **kw)
|
||||
windowed._levels[1].kernel = audited
|
||||
keys = ("impulse", "angular_impulse", "mass_into_fluid")
|
||||
summed = {part: [0., 0., 0., 0.] for part in ("total", "wall", "aperture")}
|
||||
steps = 0
|
||||
for speed in (0., .002, .002, -.001, 0., 0.): # amplitude updates stay in-window
|
||||
windowed.set_jet_amplitude(0, speed)
|
||||
plain.set_jet_amplitude(0, speed)
|
||||
windowed.step(1)
|
||||
plain.step(1)
|
||||
steps += 1
|
||||
obs = windowed.read_raw_exchange(0)
|
||||
for part in summed:
|
||||
o = obs[part]
|
||||
summed[part] = [summed[part][0] + o["impulse"][0], summed[part][1] + o["impulse"][1],
|
||||
summed[part][2] + o["angular_impulse"],
|
||||
summed[part][3] + o["mass_into_fluid"]]
|
||||
for a, e in zip(windowed._download_raw().values, plain._download_raw().values):
|
||||
same_bits(self, a, e)
|
||||
out = windowed.read_exchange_window(0)
|
||||
self.assertEqual((out["time_end"] - out["time_start"], out["sample_count"]),
|
||||
(steps, 2 * steps))
|
||||
self.assertEqual(substeps, 2 * steps)
|
||||
bound = 4 * (count * substeps) * EPS * scale + 1e-300
|
||||
for part in summed:
|
||||
o = out[part]
|
||||
got = np.array([o["impulse"][0], o["impulse"][1], o["angular_impulse"],
|
||||
o["mass_into_fluid"]])
|
||||
diff = np.abs(got - np.array(summed[part]))
|
||||
self.assertTrue((diff <= bound).all(), f"{part}: {diff} > {bound}")
|
||||
np.testing.assert_array_equal(o["force"], o["impulse"] / steps)
|
||||
again = windowed.read_exchange_window(0)
|
||||
np.testing.assert_array_equal(again["total"]["impulse"], out["total"]["impulse"])
|
||||
windowed.initialize(*arrays)
|
||||
with self.assertRaises(RuntimeError):
|
||||
windowed.read_exchange_window(0)
|
||||
|
||||
|
||||
def audit_substeps(stepper, b, count, record):
|
||||
"""Wrap the level-1 step kernel: after BFL and accumulation, add |substep terms| to record."""
|
||||
factors = np.array([.25, .25, .125, .25])
|
||||
kernel = stepper._levels[1].kernel
|
||||
|
||||
def audited(src, dst, *args, **kw):
|
||||
stepper._stream.synchronize()
|
||||
raw = np.empty((count, 4), np.float32)
|
||||
cuda.memcpy_dtoh(raw, b.last_raw)
|
||||
record["scale"] = record["scale"] + (np.abs(raw.astype(np.float64)) * factors).sum(axis=0)
|
||||
record["substeps"] += 1
|
||||
kernel(src, dst, *args, **kw)
|
||||
stepper._levels[1].kernel = audited
|
||||
|
||||
|
||||
def components(obs):
|
||||
o = obs["total"]
|
||||
return np.array([o["impulse"][0], o["impulse"][1], o["angular_impulse"], o["mass_into_fluid"]])
|
||||
|
||||
|
||||
class ExchangeWindowShiftedMRTGPU(unittest.TestCase):
|
||||
"""Shifted MRT, all-blocked, stationary body, NO Jet (scoped separately from SRT/Jet)."""
|
||||
|
||||
def test_window_step0_reset_and_split_step(self):
|
||||
from test_raw_exchange_gpu import BOUNDS as RB, circle as rc, state
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
cfg = LBMConfig(nx=32, ny=24, viscosity=.1, collision="MRT", ddf_shifting=True)
|
||||
with CudaContext(0) as ctx:
|
||||
kw = dict(context=ctx, base_block=(12, 8), fine_block=(16, 20), fine_objects=[rc()],
|
||||
observe_raw_exchange=True)
|
||||
with FixedPatchStepper(cfg, RB, observe_window_exchange=True, **kw) as windowed, \
|
||||
FixedPatchStepper(cfg, RB, observe_window_exchange=True, **kw) as split, \
|
||||
FixedPatchStepper(cfg, RB, **kw) as plain:
|
||||
arrays = [state(level.shape) for level in windowed._levels]
|
||||
for run in (windowed, split, plain):
|
||||
run.initialize(*arrays)
|
||||
run.step(2)
|
||||
if run is not plain:
|
||||
run.start_exchange_window()
|
||||
b = windowed._level_boundaries[1]
|
||||
count = len(b.tables["cell"])
|
||||
record = dict(scale=np.zeros(4), substeps=0)
|
||||
audit_substeps(windowed, b, count, record)
|
||||
steps, summed = 5, np.zeros(4)
|
||||
for _ in range(steps):
|
||||
windowed.step(1)
|
||||
plain.step(1)
|
||||
summed += components(windowed.read_raw_exchange(0))
|
||||
for a, e in zip(windowed._download_raw().values, plain._download_raw().values):
|
||||
same_bits(self, a, e)
|
||||
split.step(steps) # one call: same ledger arithmetic as five step(1) calls
|
||||
for a, e in zip(windowed._download_raw().values, split._download_raw().values):
|
||||
same_bits(self, a, e)
|
||||
rows = [np.empty((count, 4), np.float64) for _ in range(2)]
|
||||
cuda.memcpy_dtoh(rows[0], b.window_integral)
|
||||
cuda.memcpy_dtoh(rows[1], split._level_boundaries[1].window_integral)
|
||||
np.testing.assert_array_equal(rows[0].view(np.uint64), rows[1].view(np.uint64))
|
||||
|
||||
out = windowed.read_exchange_window(0)
|
||||
self.assertEqual(record["substeps"], 2 * steps)
|
||||
self.assertEqual((out["time_end"] - out["time_start"], out["sample_count"]),
|
||||
(steps, 2 * steps))
|
||||
bound = 4 * (count * record["substeps"]) * EPS * record["scale"] + 1e-300
|
||||
diff = np.abs(components(out) - summed)
|
||||
self.assertTrue((diff <= bound).all(), f"{diff} > {bound}")
|
||||
np.testing.assert_array_equal(split.read_exchange_window(0)["total"]["impulse"],
|
||||
out["total"]["impulse"])
|
||||
|
||||
windowed.step(0) # no interval: window and its duration unchanged
|
||||
again = windowed.read_exchange_window(0)
|
||||
self.assertEqual(again["time_end"], out["time_end"])
|
||||
np.testing.assert_array_equal(again["total"]["impulse"], out["total"]["impulse"])
|
||||
|
||||
windowed.start_exchange_window() # reset after a window
|
||||
with self.assertRaisesRegex(RuntimeError, "at least one completed"):
|
||||
windowed.read_exchange_window(0)
|
||||
record.update(scale=np.zeros(4), substeps=0)
|
||||
windowed.step(1)
|
||||
single = components(windowed.read_raw_exchange(0))
|
||||
reset = windowed.read_exchange_window(0)
|
||||
self.assertEqual(reset["time_end"] - reset["time_start"], 1)
|
||||
bound = 4 * (count * record["substeps"]) * EPS * record["scale"] + 1e-300
|
||||
self.assertTrue((np.abs(components(reset) - single) <= bound).all())
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""MRT reuse GPU gates. Caller owns GPU admission (CUDA_VISIBLE_DEVICES).
|
||||
|
||||
Run: python -m unittest discover -s tests/validation/multigrid -p test_refinement_mrt_gpu.py
|
||||
Parity compares the blocked MRT channel root with uniform MRT OneStep: the existing
|
||||
pressure-NEQ outlet branch differs by collision model, so SRT is never the reference.
|
||||
AMR checks are finite-evolution/split-invariance smoke tests, not benchmark validation.
|
||||
"""
|
||||
from dataclasses import replace
|
||||
import unittest
|
||||
import numpy as np
|
||||
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
from CelerisLab.lbm.population_storage import _encode_physical
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
from test_channel_root_gpu import run_root, state
|
||||
from test_refinement_blocked_base import raw, exact
|
||||
|
||||
|
||||
class RefinementMRTGPU(unittest.TestCase):
|
||||
def test_uniform_matched_mrt_root(self):
|
||||
with CudaContext(0) as context:
|
||||
for shifted in (False, True):
|
||||
for profile in ("uniform", "parabolic"):
|
||||
cfg = LBMConfig(nx=24, ny=20, viscosity=.1, velocity=.025, collision="MRT",
|
||||
ddf_shifting=shifted, inlet_profile=profile)
|
||||
initial = _encode_physical(state(cfg.ny, cfg.nx), shifted)
|
||||
expected = run_root(cfg, context, initial)
|
||||
self.assertTrue(np.isfinite(expected).all())
|
||||
for block in ((20, 24), (5, 6), (4, 1)):
|
||||
np.testing.assert_array_equal(run_root(cfg, context, initial, block), expected)
|
||||
|
||||
def test_amr_mrt_smoke(self):
|
||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||
from CelerisLab.body.objects import SimObject
|
||||
center = (24., 16.)
|
||||
body = SimObject(0, geometry=CircleGeometry(*center, 2.15), center=center, radius=2.15)
|
||||
with CudaContext(0) as context:
|
||||
for boundary in ("periodic", "channel"):
|
||||
for shifted in (False, True):
|
||||
cfg = LBMConfig(nx=48, ny=32, viscosity=.1, velocity=.015 if boundary == "channel" else 0.,
|
||||
collision="MRT", ddf_shifting=shifted)
|
||||
reference = None
|
||||
for block in ((32, 48), (8, 8), (4, 1)):
|
||||
with FixedPatchStepper(cfg, (8, 6, 40, 26), context=context, coarse_boundary=boundary,
|
||||
base_block=block, fine_block=(8, 8), fine_objects=(body,)) as backend:
|
||||
backend.initialize(*[state(*level.shape[1:]) for level in backend._levels])
|
||||
backend.step(8)
|
||||
raw = backend._download_raw()
|
||||
for a, mask in zip(raw.values, backend._state):
|
||||
self.assertTrue(np.isfinite(a[:, mask]).all())
|
||||
if reference is None: reference = raw.values
|
||||
else:
|
||||
for a, b in zip(reference, raw.values): np.testing.assert_array_equal(a, b)
|
||||
|
||||
def test_dense_versus_blocked_mrt_two_level(self):
|
||||
"""Unshifted MRT: all-dense PatchCoarseStep/PatchFineStep vs mixed/all-blocked storage."""
|
||||
from test_refinement_blocked_base import BASE_BLOCKS, FINE_BLOCK
|
||||
from test_refinement_blocked_fine import NX, NY, BOUNDS
|
||||
import test_refinement_blocked_fine as fixture
|
||||
cfg = LBMConfig(nx=NX, ny=NY, viscosity=.1, collision="MRT")
|
||||
with CudaContext(0) as context:
|
||||
results = {}
|
||||
layouts = ((None, None), *((base, FINE_BLOCK) for base in (None, *BASE_BLOCKS)))
|
||||
for base_block, fine_block in layouts:
|
||||
with FixedPatchStepper(cfg, BOUNDS, context=context, fine_block=fine_block,
|
||||
base_block=base_block) as backend:
|
||||
backend.initialize(*[fixture.state(*level.shape[1:], seed) for seed, level in enumerate(backend._levels)])
|
||||
backend.step(4)
|
||||
results[base_block, fine_block] = raw(backend)
|
||||
for layout in layouts[1:]:
|
||||
for level, (a, b) in enumerate(zip(results[layout], results[None, None])):
|
||||
exact(self, a, b, f"layout={layout} level={level}")
|
||||
|
||||
def test_shifted_all_blocked_three_level_mrt_smoke(self):
|
||||
"""Per-level shear rate omega_l = 1/(3*2**l*nu + 1/2); finite ACTIVE leaves."""
|
||||
cfg = LBMConfig(nx=16, ny=16, viscosity=.1, collision="MRT", ddf_shifting=True)
|
||||
kw = dict(fine_block=(4, 4), base_block=(4, 4), child=(8, 8, 16, 16), child_block=(4, 4))
|
||||
with CudaContext(0) as context:
|
||||
with FixedPatchStepper(cfg, (4, 4, 12, 12), context=context, **kw) as backend:
|
||||
self.assertEqual(len(backend._level_cfgs), 3)
|
||||
for level, level_cfg in enumerate(backend._level_cfgs):
|
||||
checked = replace(level_cfg)
|
||||
checked.validate() # omega is derived only by validate()
|
||||
self.assertEqual(checked.collision, "MRT")
|
||||
self.assertTrue(checked.ddf_shifting)
|
||||
self.assertTrue(np.isclose(checked.omega, 1 / (3 * 2**level * .1 + .5), rtol=1e-14))
|
||||
backend.initialize(*[state(*level.shape[1:]) for level in backend._levels])
|
||||
backend.step(4)
|
||||
values = backend._download_raw().values
|
||||
for a, mask in zip(values, backend._state):
|
||||
self.assertTrue(np.isfinite(a[:, mask]).all())
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -0,0 +1,497 @@
|
||||
"""Explicit host-only checks or admitted tiny GPU audit; never a long trajectory."""
|
||||
import argparse
|
||||
from contextlib import nullcontext
|
||||
from dataclasses import replace
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0, str(ROOT/'src'))
|
||||
from CelerisLab.config import LBMConfig
|
||||
import spacetime_gpu as gpu
|
||||
import run_spacetime_gpu_n64 as trajectory
|
||||
ref = gpu.ref
|
||||
RECORDS = []
|
||||
REPORT = None
|
||||
OUTPUT = None
|
||||
GPU_ADMITTED = False
|
||||
|
||||
|
||||
def record(value):
|
||||
RECORDS.append(value)
|
||||
if OUTPUT is not None:
|
||||
ref.write_receipt(OUTPUT,REPORT)
|
||||
|
||||
|
||||
def config(n, shifted=False):
|
||||
return LBMConfig(nx=n,ny=n,viscosity=.0192,velocity=0.,ddf_shifting=shifted)
|
||||
|
||||
|
||||
def initial(plan, tagged=True):
|
||||
arrays = []
|
||||
for n, mask in ((plan.n,plan.maps.coarse_active),(plan.nf,plan.maps.fine_active)):
|
||||
a = np.broadcast_to(ref.cpu.W[:,None,None], (9,n,n)).copy()
|
||||
if tagged:
|
||||
a *= 1+1e-3*np.random.default_rng(n).standard_normal(a.shape)
|
||||
a[:,~mask] = np.nan
|
||||
arrays.append(a)
|
||||
return ref.cpu.State(*arrays)
|
||||
|
||||
|
||||
class Host(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.plan = ref.Plan(12,(3,3,9,8))
|
||||
|
||||
def test_pins_and_supported_config(self):
|
||||
gpu.check_sources()
|
||||
for n in (12,16,64):
|
||||
c,f = gpu.validate_config(config(n))
|
||||
self.assertAlmostEqual(c.omega,1/.5576)
|
||||
self.assertAlmostEqual(f.omega,1/.6152)
|
||||
for change in (dict(ddf_shifting=1),dict(collision='TRT'),dict(nx=128,ny=128),
|
||||
dict(ny=16),dict(les_enabled=True),dict(velocity=.03)):
|
||||
with self.subTest(change=change), self.assertRaises(ValueError):
|
||||
gpu.validate_config(replace(config(12),**change))
|
||||
|
||||
def test_shifted_codec_and_uploaded_physical_validation(self):
|
||||
for n in (12,64):
|
||||
self.assertTrue(gpu.validate_config(config(n,True))[1].ddf_shifting)
|
||||
physical=gpu._WEIGHTS+2.**-35
|
||||
stored=gpu._encode_physical(physical,True)
|
||||
np.testing.assert_array_equal(stored,np.full((9,1,1),2.**-35,np.float32))
|
||||
np.testing.assert_array_equal(gpu._decode_stored(stored,True),physical)
|
||||
run=gpu.SpaceTimeGPU.__new__(gpu.SpaceTimeGPU)
|
||||
run.status='new'; run.configs=gpu.validate_config(config(12,True)); run.plan=self.plan
|
||||
run.ctx=SimpleNamespace(activate=nullcontext); run.cuda=Mock()
|
||||
run.coarse=['coarse']; run.fine=['fine']; run.health_counts=np.zeros(6,np.uint64)
|
||||
state=initial(self.plan,False)
|
||||
run.initialize(state)
|
||||
for call in run.cuda.memcpy_htod.call_args_list:
|
||||
self.assertTrue((call.args[1][np.isfinite(call.args[1])]==0).all())
|
||||
state.coarse_f[:]-=.01; state.fine_f[:]-=.01
|
||||
run.initialize(state) # Valid negative stored values.
|
||||
with self.assertRaisesRegex(ValueError,'exact shapes'):
|
||||
run.initialize(ref.cpu.State(state.coarse_f[:1],state.fine_f))
|
||||
for invalid in (-.01,np.nan):
|
||||
bad=ref.cpu.State(state.coarse_f.copy(),state.fine_f.copy())
|
||||
bad.coarse_f[0,0,0]=invalid
|
||||
with self.assertRaisesRegex(ValueError,'positive finite'):
|
||||
run.initialize(bad)
|
||||
|
||||
def test_shifted_transfer_translation_and_raw_ledger(self):
|
||||
p=self.plan; t=gpu.pack_plan(p)
|
||||
rng=np.random.default_rng(19)
|
||||
for g in p.groups:
|
||||
values=rng.uniform(.02,.45,len(g.samples)); weight=gpu._WEIGHTS[g.q,0,0]
|
||||
np.testing.assert_allclose(g.weights@(values-weight),g.weights@values-weight,rtol=0,atol=2e-14)
|
||||
# Uniform nonzero stored packets expose register omissions; exact rest would not.
|
||||
arrays=[np.full((9,n,n),-.01,np.float32) for n in (p.n,p.nf)]
|
||||
for a,m in zip(arrays,(p.maps.coarse_active,p.maps.fine_active)):
|
||||
a[:,~m]=np.nan
|
||||
state=ref.cpu.State(*arrays); g0=np.full((9,p.n,p.n),-.01,np.float32)
|
||||
pre=np.repeat(g0.ravel()[t['origins'],None].astype(float),4,axis=1)
|
||||
h=pre.astype(np.float32); registers=np.zeros(len(p.receivers))
|
||||
args=(p,t,state,state,g0,g0,[(state.fine_f,state.fine_f)]*2,pre,h,registers)
|
||||
ledger=gpu.rounding_ledger(*args,shifted=True)
|
||||
self.assertEqual(max(map(abs,ledger['unexplained'])),0.)
|
||||
self.assertGreater(ledger['unexplained_bound'],0.)
|
||||
self.assertGreater(ledger['minimum_stored'],0.)
|
||||
expected_mass=p.n*p.n*(1.+9*float(np.float32(-.01)))
|
||||
self.assertEqual(ledger['unexplained_bound'],64*np.finfo(float).eps*expected_mass)
|
||||
decoded=ref.cpu.State(*(gpu._decode_stored(a,True) for a in arrays))
|
||||
with self.assertRaises(AssertionError):
|
||||
gpu.rounding_ledger(p,t,decoded,*args[3:],shifted=True)
|
||||
registers[0]+=.25*float(np.float32(-.01))
|
||||
with self.assertRaisesRegex(AssertionError,'register mismatch'):
|
||||
gpu.rounding_ledger(*args,shifted=True)
|
||||
|
||||
def test_packed_weights_and_packet_ownership(self):
|
||||
for plan in (self.plan,ref.Plan(16,(4,5,11,12))):
|
||||
t = gpu.pack_plan(plan)
|
||||
self.assertEqual(t['sources'].shape,(len(plan.receivers),4))
|
||||
self.assertEqual(np.count_nonzero(t['times']==-1),24)
|
||||
self.assertEqual(gpu.table_hash(t),gpu.table_hash(gpu.pack_plan(plan)))
|
||||
for i,g in enumerate(plan.groups):
|
||||
np.testing.assert_array_equal(t['weights'][i,:,:len(g.samples)],g.weights)
|
||||
np.testing.assert_allclose(t['weights'][i].sum(axis=1),1,rtol=0,atol=2e-14)
|
||||
np.testing.assert_array_equal(t['samples'][i,:len(g.samples)],g.q*plan.n**2+g.samples)
|
||||
for m in (0,1):
|
||||
self.assertEqual(len(t[f'incoming{m}']),len(plan.outgoing[m]))
|
||||
|
||||
def test_duplicate_writer_is_rejected(self):
|
||||
plan = self.plan
|
||||
groups = list(plan.groups)
|
||||
a,b = next((i,k) for i,g in enumerate(groups) for k,e in enumerate(g.events) if e is not None)
|
||||
event = groups[a].events[b]
|
||||
other = next(i for i,g in enumerate(groups) if i!=a and g.q==groups[a].q and event[0] in [e[0] for e in g.events if e])
|
||||
events = list(groups[other].events)
|
||||
k = next(k for k,e in enumerate(events) if e is not None and e[0]==event[0])
|
||||
events[k] = event; groups[other] = replace(groups[other],events=tuple(events))
|
||||
fake = SimpleNamespace(n=plan.n,nf=plan.nf,groups=groups,receivers=plan.receivers,outgoing=plan.outgoing)
|
||||
with self.assertRaisesRegex(ValueError,'duplicate incoming'):
|
||||
gpu.pack_plan(fake)
|
||||
|
||||
def test_rest_ledger_and_missing_packet_discrimination(self):
|
||||
p,t = self.plan,gpu.pack_plan(self.plan)
|
||||
state = initial(p,False)
|
||||
state = ref.cpu.State(state.coarse_f.astype(np.float32),state.fine_f.astype(np.float32))
|
||||
g0 = np.broadcast_to(ref.cpu.W.astype(np.float32)[:,None,None],(9,p.n,p.n)).copy()
|
||||
pre = np.repeat(g0.ravel()[t['origins'],None].astype(float),4,axis=1)
|
||||
h = pre.astype(np.float32); registers = np.zeros(len(p.receivers))
|
||||
phases = [(state.fine_f,state.fine_f)]*2
|
||||
args = (p,t,state,state,g0,g0,phases,pre,h)
|
||||
ledger = gpu.rounding_ledger(*args,registers)
|
||||
self.assertEqual(max(map(abs,ledger['unexplained'])),0)
|
||||
np.testing.assert_array_equal(ledger['physical_collision'],np.array(ledger['coarse_collision'])
|
||||
+ledger['fine0_collision']+ledger['fine1_collision'])
|
||||
np.testing.assert_array_equal(ledger['child_rounding'],np.array(ledger['child_cast'])+ledger['child_interpolation'])
|
||||
np.testing.assert_array_equal(ledger['collision_cell_mass'],np.zeros((3,4)))
|
||||
registers[0] = .01
|
||||
with self.assertRaisesRegex(AssertionError,'register mismatch'):
|
||||
gpu.rounding_ledger(*args,registers)
|
||||
|
||||
def test_second_substep_characteristic_donor(self):
|
||||
p = ref.Plan(16,(4,4,12,12)); t = gpu.pack_plan(p)
|
||||
row = t['incoming1'][t['incoming1'][:,0]==5*32*32+10*32+7][0]
|
||||
self.assertEqual(t['origins'][row[1]//4],5*16*16+4*16+3)
|
||||
|
||||
def test_failed_free_remains_retryable(self):
|
||||
run=gpu.SpaceTimeGPU.__new__(gpu.SpaceTimeGPU)
|
||||
run.ctx=SimpleNamespace(activate=nullcontext); run.stream=Mock()
|
||||
first,second=Mock(),Mock(); first.free.side_effect=[RuntimeError('free failed'),None]
|
||||
run.allocations=[first,second]; run.modules=[]; run.status='ready'
|
||||
with self.assertRaisesRegex(RuntimeError,'free failed'):
|
||||
run.close()
|
||||
self.assertEqual(run.allocations,[first]); self.assertEqual(run.status,'cleanup_failed')
|
||||
run.close()
|
||||
self.assertEqual(run.status,'closed'); self.assertEqual(run.allocations,[])
|
||||
second.free.assert_called_once()
|
||||
|
||||
def test_unmeasured_schedule_and_health_failure(self):
|
||||
run=gpu.SpaceTimeGPU.__new__(gpu.SpaceTimeGPU)
|
||||
run.status='ready'; run.step_count=0; run.identity=False; run.mode='spacetime'
|
||||
run.ctx=SimpleNamespace(activate=nullcontext); run.cuda=Mock(); run.stream=Mock()
|
||||
run.plan=self.plan; run.bounds=(3,3,9,8)
|
||||
run.configs=gpu.validate_config(config(12)); run.coarse=list('abcd'); run.fine=list('efg')
|
||||
run.tables=gpu.pack_plan(self.plan); run.device_tables={key:key for key in run.tables}
|
||||
run.active='active'; run.pre='pre'; run.h='h'; run.registers='registers'; run.counts='counts'
|
||||
run.health_counts=np.zeros(6,np.uint64)
|
||||
run.launch=Mock(); run.read=Mock(return_value=np.zeros(6,np.uint32)); run.download=Mock()
|
||||
names=['CoarseStart','Pull','Collide','Interpolate','Collide','Register','Inject','Pull',
|
||||
'Collide','Register','Inject','Pull','ApplyRegisters']
|
||||
for _ in range(2):
|
||||
result=run.step(audit=False)
|
||||
self.assertEqual(result['boundary_budget_status'],'not_measured')
|
||||
self.assertIsNone(result['unexplained'])
|
||||
self.assertEqual([call.args[1] for call in run.launch.call_args_list],names*2)
|
||||
self.assertEqual(run.read.call_count,2); run.download.assert_not_called()
|
||||
self.assertEqual(run.step_count,2); self.assertEqual(run.coarse,list('abcd'))
|
||||
run.read.return_value=np.array([0,0,0,0,1,0],np.uint32)
|
||||
with self.assertRaisesRegex(AssertionError,'health failed'):
|
||||
run.step(audit=False)
|
||||
self.assertEqual(run.status,'failed'); self.assertEqual(run.step_count,2)
|
||||
self.assertEqual(run.health_counts.tolist(),[0,0,0,0,1,0])
|
||||
|
||||
def test_warmup_resets_arrivals_and_counters(self):
|
||||
audits=[]
|
||||
class Fake:
|
||||
def initialize(self,state):
|
||||
self.state=ref.cpu.State(state.coarse_f.copy(),state.fine_f.copy())
|
||||
self.step_count=0; self.health_counts=np.zeros(6,np.uint64)
|
||||
def download(self):
|
||||
return ref.cpu.State(self.state.coarse_f.copy(),self.state.fine_f.copy(),tick=2*self.step_count)
|
||||
def step(self,*,audit):
|
||||
audits.append(audit)
|
||||
self.step_count+=1; self.state.coarse_f[:]+=1e-4
|
||||
fake=Fake(); check=Mock(); state=initial(self.plan)
|
||||
actual,warm=trajectory.warm_and_reset(fake,state,check)
|
||||
np.testing.assert_array_equal(actual.coarse_f,state.coarse_f)
|
||||
self.assertEqual(check.call_count,32); self.assertEqual(fake.step_count,0)
|
||||
self.assertTrue(warm['reset_verified']); self.assertEqual(warm['intervals'],32)
|
||||
trajectory.warm_and_reset(fake,state,check,audit=True)
|
||||
self.assertEqual(audits,[False]*32+[True]*32)
|
||||
|
||||
def test_accumulated_drift_ledger_and_exact_samples(self):
|
||||
totals=trajectory.LedgerTotals()
|
||||
for sign in (1.,-1.):
|
||||
ledger={name:np.zeros(9) for name in totals.names}
|
||||
ledger['coarse_collision'][0]=sign*2.**-20
|
||||
ledger['physical_collision']=ledger['coarse_collision'].copy()
|
||||
ledger['child_cast'][5]=sign*2.**-30
|
||||
ledger['child_interpolation'][5]=sign*2.**-50
|
||||
ledger['child_rounding']=ledger['child_cast']+ledger['child_interpolation']
|
||||
ledger['receiver_rounding'][5]=-sign*2.**-32
|
||||
ledger['net_change']=sum(ledger[k] for k in ('physical_collision','child_rounding','receiver_rounding'))
|
||||
mass=ledger['coarse_collision'].sum()
|
||||
ledger.update(unexplained_bound=1e-12,collision_cell_mass=[[mass,max(mass,0.),min(mass,0.),abs(mass)],[0.]*4,[0.]*4])
|
||||
totals.add(ledger)
|
||||
result=totals.snapshot(np.zeros(9))
|
||||
self.assertEqual(result['intervals'],2)
|
||||
self.assertEqual(result['components']['physical_collision']['directional'],[0.]*9)
|
||||
self.assertGreater(result['components']['physical_collision']['absolute_step_mass_momentum_sum'][0],0)
|
||||
self.assertEqual(result['components']['physical_collision']['max_absolute_interval_step'][0],1)
|
||||
self.assertEqual(totals.snapshot()['endpoint_status'],'not_measured')
|
||||
self.assertFalse(totals.snapshot(np.ones(9)*1e-8)['closure_pass'])
|
||||
self.assertEqual(trajectory.differing_fields(dict(a=1.,b=[0.,2.]),dict(a=1.,b=[0.,2.])),[])
|
||||
self.assertEqual(trajectory.differing_fields(dict(a=0.),dict(a=-0.)),['a'])
|
||||
self.assertEqual(trajectory.differing_fields(dict(a=1.),dict(a=np.nextafter(1.,2.))),['a'])
|
||||
|
||||
def test_trajectory_screens_differences_and_json(self):
|
||||
rows=[dict(tstar=t,velocity_l2=np.float64(.01),pressure_l2=.03,energy_relative=.01,
|
||||
mass_relative=1e-7,momentum_change=[1e-6,-2e-6],minimum_population=.02,
|
||||
minimum_boundary_pre=.02,minimum_boundary_stored=.02) for t in trajectory.TIMES]
|
||||
cpu=dict(runs=[dict(worst=dict(velocity_l2=.02,pressure_l2=.04,energy_relative=.02)),
|
||||
dict(rows=[dict(tstar=t,velocity_l2=.009,momentum_change=[0.,0.]) for t in trajectory.TIMES])])
|
||||
uniform=dict(rows=[dict(tstar=.12,pressure_l2=.012253173806349701)])
|
||||
result=trajectory.assess(rows,cpu,uniform,[0]*6)
|
||||
self.assertTrue(result['campaign_pass']); self.assertTrue(result['experimental_pass'])
|
||||
self.assertFalse(result['historical_pressure_gate']['passed'])
|
||||
self.assertFalse(result['historical_pressure_gate']['matched_storage'])
|
||||
self.assertAlmostEqual(result['gpu_minus_cpu']['rows'][0]['fields']['velocity_l2']['signed'],.001)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path=Path(directory)/'receipt.json'
|
||||
ref.write_receipt(path,dict(completed=True,**result))
|
||||
self.assertTrue(json.loads(path.read_text())['completed'])
|
||||
rows[0]['velocity_l2']=.06
|
||||
failed=trajectory.assess(rows,cpu,uniform,[0,1,0,0,0,0])
|
||||
ref.write_receipt(path,dict(completed=True,**failed))
|
||||
saved=json.loads(path.read_text())
|
||||
self.assertFalse(saved['campaign_pass']); self.assertFalse(saved['experimental_pass'])
|
||||
for invalid in (float('nan'),-.01):
|
||||
rows[0]['velocity_l2']=.01; rows[-1]['velocity_l2']=invalid
|
||||
failed=trajectory.assess(rows,cpu,uniform,[0]*6)
|
||||
self.assertFalse(failed['campaign_pass']); self.assertFalse(failed['experimental_pass'])
|
||||
path.write_text('changed reference')
|
||||
with self.assertRaisesRegex(ValueError,'pinned reference'):
|
||||
trajectory.load_references(path,path)
|
||||
|
||||
def test_shifted_descriptive_comparison_and_runner_pin(self):
|
||||
expected=dict(mass=4096.,momentum=[0.,-1e-4])
|
||||
actual=dict(mass=4096.+2.**-20,momentum=[-1e-6,-2e-4])
|
||||
difference=trajectory.sample_difference(actual,expected)
|
||||
self.assertEqual(difference['mass']['signed'],2.**-20)
|
||||
self.assertEqual(difference['momentum']['absolute'],[1e-6,1e-4])
|
||||
self.assertEqual(trajectory.sha(Path(gpu.__file__).with_suffix('.cu')),trajectory.CUDA_SHA)
|
||||
self.assertEqual(trajectory.LIMITS,dict(velocity_l2=.05,pressure_l2=.15,energy_relative=.05,mass_relative=1e-5))
|
||||
self.assertEqual(trajectory.FACTORS,dict(velocity_l2=.8,pressure_l2=1.,energy_relative=1.))
|
||||
|
||||
def test_n64_table_guard_and_balanced_mutation(self):
|
||||
guard=trajectory.table_guard
|
||||
plan=ref.Plan(64,(8,8,56,48)); tables=gpu.pack_plan(plan)
|
||||
self.assertEqual(guard.validate_tables(plan,tables)['status'],'passed')
|
||||
face=next(i for i,g in enumerate(plan.groups) if g.kind=='face')
|
||||
changed={key:value.copy() for key,value in tables.items()}
|
||||
changed['weights'][face,0,0]=np.nextafter(changed['weights'][face,0,0],np.inf)
|
||||
self.assertEqual(guard.validate_tables(plan,changed)['status'],'passed')
|
||||
for mutation in ('coordinate','order','dtype','shape','layout','padding','negative_zero','nan','inf','balanced'):
|
||||
changed={key:value.copy() for key,value in tables.items()}
|
||||
w=changed['weights']
|
||||
if mutation=='coordinate': changed['incoming1'][0,0]^=1
|
||||
elif mutation=='order': changed['origins']=changed['origins'][::-1].copy()
|
||||
elif mutation=='dtype': changed['weights']=w.astype(np.float32)
|
||||
elif mutation=='shape': changed['weights']=w.reshape(532,10,4)
|
||||
elif mutation=='layout': changed['weights']=np.asfortranarray(w)
|
||||
elif mutation=='padding': w[face,0,9]=1e-30
|
||||
elif mutation=='negative_zero': w[face,0,9]=-0.
|
||||
elif mutation=='nan': w[face,0,0]=np.nan
|
||||
elif mutation=='inf': w[face,0,0]=np.inf
|
||||
else:
|
||||
w[face,:2,:2] += 2.**-30*np.array([[1.,-1.],[-1.,1.]])
|
||||
np.testing.assert_array_equal(w[face].sum(axis=1),tables['weights'][face].sum(axis=1))
|
||||
np.testing.assert_array_equal(w[face].mean(axis=0),tables['weights'][face].mean(axis=0))
|
||||
with self.subTest(mutation=mutation), self.assertRaises(ValueError):
|
||||
guard.validate_tables(plan,changed)
|
||||
changed={key:value.copy() for key,value in tables.items()}
|
||||
changed['weights'][-1,0,0]+=1.
|
||||
with self.assertRaises(ValueError):
|
||||
guard.validate_tables(SimpleNamespace(n=64,groups=plan.groups[:-1]),changed)
|
||||
groups=list(plan.groups); groups[0]=replace(groups[0],samples=groups[0].samples[:-1])
|
||||
with self.assertRaisesRegex(ValueError,'coverage/size'):
|
||||
guard.validate_tables(SimpleNamespace(n=64,groups=groups),tables)
|
||||
|
||||
|
||||
class Device(unittest.TestCase):
|
||||
shifted = False
|
||||
|
||||
def config(self,n):
|
||||
return config(n,self.shifted)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not GPU_ADMITTED:
|
||||
raise unittest.SkipTest('CUDA compilation/execution requires explicit tiny-job admission')
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
cls.context = CudaContext(0)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.context.close()
|
||||
|
||||
def test_tiny_equivalence_budgets_and_phase(self):
|
||||
for mode,identity in (('spacetime',False),('spacetime',True),('frozen',False),('frozen',True)):
|
||||
with self.subTest(mode=mode,identity=identity), gpu.SpaceTimeGPU(
|
||||
self.config(12),(3,3,9,8),context=self.context,mode=mode,identity=identity) as run:
|
||||
spec = ref.Spec(n_coarse=12,patch_lo=1,halo=2,tau_coarse=.5576,tau_fine=.6152,
|
||||
collision='identity' if identity else 'srt')
|
||||
for tagged,steps in ((False,2),(True,8)):
|
||||
expected = initial(run.plan,tagged); run.initialize(expected)
|
||||
before = run.download()
|
||||
if self.shifted:
|
||||
expected = before
|
||||
for k in range(1,steps+1):
|
||||
expected,_ = ref.advance(spec,run.plan,expected,mode)
|
||||
ledger = run.step(); actual = run.download()
|
||||
self.assertEqual(actual.tick,2*k); self.assertEqual(run.step_count,k)
|
||||
for a,b in zip((actual.coarse_f,actual.fine_f),(expected.coarse_f,expected.fine_f)):
|
||||
np.testing.assert_array_equal(np.isnan(a),np.isnan(b))
|
||||
np.testing.assert_allclose(a,b,rtol=0,atol=3e-6,equal_nan=True)
|
||||
total = lambda s: ref.cpu.MOMENTS.T @ ref.directional(
|
||||
ref.cpu.State(s.coarse_f.astype(float),s.fine_f.astype(float)),run.plan.maps)
|
||||
np.testing.assert_allclose(total(actual),total(before),rtol=0,atol=2e-4)
|
||||
if tagged and not identity and k==1:
|
||||
for a,b,tau,mask in zip((actual.coarse_f,actual.fine_f),
|
||||
(expected.coarse_f,expected.fine_f),(.5576,.6152),
|
||||
(run.plan.maps.coarse_active,run.plan.maps.fine_active)):
|
||||
wrong = ref.cpu._collide(spec,b,np.isfinite(b),tau,mask,np.zeros(mask.shape,int))
|
||||
self.assertGreater(float(np.nanmax(abs(a-wrong))),3e-6)
|
||||
if k in (1,2,8):
|
||||
record(dict(case='tiny',shifted=self.shifted,mode=mode,identity=identity,tagged=tagged,
|
||||
step=k,device_bytes=run.device_bytes,ledger=ledger))
|
||||
|
||||
def test_omitted_packets_and_ghosts_fail(self):
|
||||
for mutation in (1,2,3,4):
|
||||
with self.subTest(mutation=mutation), gpu.SpaceTimeGPU(
|
||||
self.config(12),(3,3,9,8),context=self.context,identity=self.shifted) as run:
|
||||
state=initial(run.plan,not self.shifted)
|
||||
if self.shifted:
|
||||
state.coarse_f[:]+=1e-4; state.fine_f[:]+=1e-4
|
||||
for a,mask in zip((state.coarse_f,state.fine_f),(run.plan.maps.coarse_active,run.plan.maps.fine_active)):
|
||||
self.assertTrue((gpu._encode_physical(a,True)[:,mask]>0).all())
|
||||
run.initialize(state)
|
||||
with self.assertRaises(AssertionError):
|
||||
run.step(mutation=mutation)
|
||||
self.assertEqual(run.step_count,0)
|
||||
self.assertEqual(run.status,'failed')
|
||||
with self.assertRaises(RuntimeError):
|
||||
run.download()
|
||||
|
||||
def test_frozen_donor_trap(self):
|
||||
with gpu.SpaceTimeGPU(self.config(16),(4,4,12,12),context=self.context,mode='frozen',identity=True) as run:
|
||||
state = initial(run.plan,False)
|
||||
state.coarse_f[5,4,3]=1.; state.coarse_f[5,5,3]=2.
|
||||
run.initialize(state); donor=run.download().coarse_f[5,4,3]; run.step()
|
||||
self.assertEqual(run.download().fine_f[5,11,8],donor)
|
||||
# Negative stored values can be physical in shifted mode; never clip either case.
|
||||
with self.context.activate():
|
||||
for physical_negative in ((False,True) if self.shifted else (True,)):
|
||||
data=np.full((9,16,16),-.01,np.float32)
|
||||
if self.shifted and physical_negative:
|
||||
data=np.broadcast_to(-gpu._WEIGHTS-.01,(9,16,16)).astype(np.float32).copy()
|
||||
run.cuda.memcpy_htod(run.coarse[1],data); run.cuda.memcpy_htod(run.coarse[3],data)
|
||||
run.cuda.memset_d32_async(run.counts,0,6,run.stream)
|
||||
t=run.device_tables; ng=len(run.plan.groups)
|
||||
run.launch(0,'Interpolate',run.coarse[1],run.coarse[3],t['samples'],t['origins'],
|
||||
t['sizes'],t['weights'],run.pre,run.h,np.uint32(ng),np.uint32(0),run.counts,count=ng)
|
||||
counts=run.read(run.counts,(6,),np.uint32)
|
||||
self.assertEqual(counts[1],4*ng*physical_negative)
|
||||
self.assertEqual(counts[3],4*ng*physical_negative)
|
||||
self.assertTrue((run.read(run.h,(ng,4))<0).all())
|
||||
|
||||
def test_n64_one_interval_only(self):
|
||||
sys.path.insert(0,str(Path(__file__).parents[1]/'taylor_green'))
|
||||
import oracle as tg
|
||||
with gpu.SpaceTimeGPU(self.config(64),(8,8,56,48),context=self.context) as run:
|
||||
arrays = [tg.initial((64,64),0,64,.0192),tg.initial((128,128),1,64,.0192)]
|
||||
for a,mask in zip(arrays,(run.plan.maps.coarse_active,run.plan.maps.fine_active)):
|
||||
a[:,~mask]=np.nan
|
||||
state = ref.cpu.State(*arrays); run.initialize(state)
|
||||
spec = ref.Spec(n_coarse=64,patch_lo=8,halo=16,tau_coarse=.5576,tau_fine=.6152,collision='srt')
|
||||
expected,_ = ref.advance(spec,run.plan,run.download() if self.shifted else state)
|
||||
started=time.monotonic(); ledger=run.step(); seconds=time.monotonic()-started
|
||||
actual=run.download()
|
||||
errors=[]
|
||||
for a,b in zip((actual.coarse_f,actual.fine_f),(expected.coarse_f,expected.fine_f)):
|
||||
np.testing.assert_array_equal(np.isnan(a),np.isnan(b))
|
||||
np.testing.assert_allclose(a,b,rtol=0,atol=3e-6,equal_nan=True)
|
||||
errors.append(float(np.nanmax(abs(a-b))))
|
||||
record(dict(case='N64_one_interval',shifted=self.shifted,errors=errors,ledger=ledger,seconds=seconds,
|
||||
device_bytes=run.device_bytes,table_sha256=gpu.table_hash(run.tables),
|
||||
build_metadata=run.build_metadata,device=self.context.name))
|
||||
|
||||
def test_unmeasured_path_and_warm_reset(self):
|
||||
with gpu.SpaceTimeGPU(self.config(12),(3,3,9,8),context=self.context) as run:
|
||||
state=initial(run.plan)
|
||||
run.initialize(state)
|
||||
raw_initial=run._download_stored()
|
||||
for _ in range(2):
|
||||
run.step()
|
||||
expected=run._download_stored()
|
||||
run.initialize(state)
|
||||
for _ in range(2):
|
||||
result=run.step(audit=False)
|
||||
self.assertEqual(result['boundary_budget_status'],'not_measured')
|
||||
actual=run._download_stored()
|
||||
for a,b in zip((actual.coarse_f,actual.fine_f),(expected.coarse_f,expected.fine_f)):
|
||||
np.testing.assert_array_equal(a.view(np.uint32),b.view(np.uint32))
|
||||
reset,warm=trajectory.warm_and_reset(run,state,lambda:None)
|
||||
raw_reset=run._download_stored()
|
||||
for a,b in zip((raw_reset.coarse_f,raw_reset.fine_f),(raw_initial.coarse_f,raw_initial.fine_f)):
|
||||
np.testing.assert_array_equal(a.view(np.uint32),b.view(np.uint32))
|
||||
record(dict(case='unmeasured_bitwise_and_reset',shifted=self.shifted,warmup=warm,health_counts=run.health_counts.tolist()))
|
||||
|
||||
|
||||
class ShiftedDevice(Device):
|
||||
shifted = True
|
||||
|
||||
|
||||
def main():
|
||||
global REPORT,OUTPUT,GPU_ADMITTED
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
mode=parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument('--host-only',action='store_true')
|
||||
mode.add_argument('--gpu-admitted',action='store_true')
|
||||
parser.add_argument('--output',type=Path)
|
||||
args=parser.parse_args()
|
||||
GPU_ADMITTED=args.gpu_admitted
|
||||
if args.gpu_admitted and (not os.environ.get('CUDA_VISIBLE_DEVICES') or args.output is None):
|
||||
parser.error('GPU job requires explicit CUDA_VISIBLE_DEVICES and --output')
|
||||
if args.output is not None and args.output.exists():
|
||||
parser.error('output exists; retain evidence and choose a new path')
|
||||
started=time.monotonic()
|
||||
report=dict(scope='tiny GPU equivalence and rounding audit; no long trajectory',completed=False,
|
||||
requested_mode='admitted_tiny_gpu' if args.gpu_admitted else 'host_only',
|
||||
commit=subprocess.check_output(['git','rev-parse','HEAD'],cwd=ROOT,text=True).strip(),
|
||||
dirty=bool(subprocess.check_output(['git','status','--porcelain'],cwd=ROOT,text=True)),
|
||||
hashes={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in
|
||||
(Path(__file__),Path(gpu.__file__),Path(gpu.__file__).with_suffix('.cu'))},
|
||||
reference_hashes=gpu.PINS,cuda_visible_devices=os.environ.get('CUDA_VISIBLE_DEVICES'),records=RECORDS)
|
||||
REPORT,OUTPUT=report,args.output
|
||||
if args.output:
|
||||
ref.write_receipt(args.output,report)
|
||||
suite=unittest.TestLoader().loadTestsFromTestCase(Host)
|
||||
if args.gpu_admitted:
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(Device))
|
||||
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(ShiftedDevice))
|
||||
result=unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
report.update(completed=True,passed=result.wasSuccessful(),tests=result.testsRun,
|
||||
failures=[dict(test=str(test),error=error) for test,error in result.failures+result.errors],
|
||||
seconds=time.monotonic()-started)
|
||||
if args.output:
|
||||
ref.write_receipt(args.output,report)
|
||||
return 0 if result.wasSuccessful() else 1
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,205 @@
|
||||
"""Host checks or separately admitted tiny GPU ladder capability; no trajectories."""
|
||||
import argparse
|
||||
from contextlib import nullcontext
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
import numpy as np
|
||||
import run_spacetime_gpu_ladder as ladder
|
||||
|
||||
gpu,ref,tg=ladder.gpu,ladder.ref,ladder.tg
|
||||
RECORDS=[]
|
||||
ADMITTED=False
|
||||
|
||||
|
||||
def config(n):
|
||||
return ladder.old.LBMConfig(nx=n,ny=n,viscosity=.0003*n,velocity=0.,ddf_shifting=True)
|
||||
|
||||
|
||||
class Host(unittest.TestCase):
|
||||
def test_scaling_and_admission(self):
|
||||
for _,n,refined in ladder.CASES:
|
||||
s=ladder.setup(n,refined)
|
||||
self.assertEqual(s['finest_n'],n*(2 if refined else 1))
|
||||
self.assertEqual(len(s['early_steps']),65)
|
||||
np.testing.assert_allclose(np.array(s['early_steps'])*.03/n,s['early_times'],rtol=0,atol=3e-17)
|
||||
self.assertEqual(s['intervals']*.03/n,.48)
|
||||
gpu.validate_config(config(n),uniform=not refined,allow_n128=refined)
|
||||
with self.assertRaises(ValueError): gpu.validate_config(config(128))
|
||||
with self.assertRaises(ValueError): gpu.validate_config(config(256),allow_n128=True)
|
||||
with self.assertRaises(ValueError): gpu.validate_config(config(512),uniform=True)
|
||||
|
||||
def test_n128_tables_and_mutations(self):
|
||||
plan=ref.Plan(128,(16,16,112,96)); tables=gpu.pack_plan(plan)
|
||||
out=ladder.old.table_guard.validate_tables(plan,tables,allow_n128=True)
|
||||
self.assertEqual(out['structure_sha256'],'ee4ecb645674ae04eb87220a5217851405ccc0f45fbb0fe210499dcea5cd93b1')
|
||||
self.assertEqual(len(plan.groups),1060)
|
||||
with self.assertRaises(ValueError): ladder.old.table_guard.validate_tables(plan,tables)
|
||||
for kind in ('topology','balanced_weights'):
|
||||
bad={k:v.copy() for k,v in tables.items()}
|
||||
if kind=='topology': bad['sources'][0,0]+=1
|
||||
else: bad['weights'][0,:2,:2]+=1e-5*np.array([[1,-1],[-1,1]])
|
||||
with self.assertRaises(ValueError): ladder.old.table_guard.validate_tables(plan,bad,allow_n128=True)
|
||||
|
||||
def test_projection_manufactured_nonuniform_leaves(self):
|
||||
n=16; plan=ref.Plan(n,(3,3,13,12)); masks=(plan.maps.coarse_active,plan.maps.fine_active)
|
||||
coords=[tg.centers(m.shape,l) for l,m in enumerate(masks)]
|
||||
x=np.concatenate([v[0][m] for v,m in zip(coords,masks)])
|
||||
y=np.concatenate([v[1][m] for v,m in zip(coords,masks)])
|
||||
w=np.concatenate([np.full(m.sum(),.25**l) for l,m in enumerate(masks)])
|
||||
b=np.column_stack((np.cos(4*np.pi*x/n),np.sin(4*np.pi*x/n),np.cos(4*np.pi*y/n),np.sin(4*np.pi*y/n)))
|
||||
b-=w@b/w.sum(); gram=b.T@(w[:,None]*b)
|
||||
residual=np.cos(6*np.pi*x/n)+np.sin(6*np.pi*y/n); residual-=w@residual/w.sum()
|
||||
residual-=b@np.linalg.solve(gram,b.T@(w*residual)); residual*=1e-5
|
||||
coefficients=np.array([2,-3,4,-1])*1e-5
|
||||
pressure=tg.exact(x,y,0,n,.0003*n)[2]+b@coefficients+residual+.017
|
||||
arrays=[]; offset=0
|
||||
for m in masks:
|
||||
a=np.full((9,*m.shape),np.nan); count=m.sum()
|
||||
a[:,m]=ref.cpu.W[:,None]*(1+3*pressure[offset:offset+count]); arrays.append(a); offset+=count
|
||||
result=ladder.pressure_projection(arrays,masks,0,n,.0003*n)
|
||||
np.testing.assert_allclose(result['coefficients'],coefficients,rtol=0,atol=3e-17)
|
||||
self.assertAlmostEqual(result['residual_rms'],np.sqrt(w@(residual*residual)/w.sum()),places=15)
|
||||
np.testing.assert_allclose(result['orthogonality'],0,rtol=0,atol=1e-18)
|
||||
self.assertLess(abs(result['energy_closure']),1e-20)
|
||||
expected=tg.metrics(arrays,masks,0,n,.0003*n)['pressure_l2']
|
||||
self.assertAlmostEqual(result['relative_rms'],expected,places=12)
|
||||
|
||||
def test_uniform_budget_and_negative_control(self):
|
||||
rng=np.random.default_rng(12); a=rng.normal(0,.001,(9,12,12)).astype(np.float32)
|
||||
g=(a*.99).astype(np.float32)
|
||||
b=np.stack([np.roll(g[q],(int(ref.cpu.CY[q]),int(ref.cpu.CX[q])),axis=(0,1)) for q in range(9)])
|
||||
ledger=gpu.uniform_ledger(a,b,g,shifted=True)
|
||||
self.assertLess(max(abs(np.array(ledger['unexplained']))),ledger['unexplained_bound'])
|
||||
b[1,0,0]+=1e-4
|
||||
with self.assertRaises(AssertionError): gpu.uniform_ledger(a,b,g,shifted=True)
|
||||
|
||||
def test_uniform_failed_io_poisoning(self):
|
||||
run=object.__new__(gpu.UniformGPU); run.configs=(config(12),); run.status='ready'
|
||||
run.ctx=SimpleNamespace(activate=nullcontext); run.buffers=['f']; run.cuda=SimpleNamespace(memcpy_htod=Mock(side_effect=RuntimeError('copy')))
|
||||
with self.assertRaisesRegex(RuntimeError,'copy'): run.initialize(np.broadcast_to(ref.cpu.W[:,None,None],(9,12,12)))
|
||||
self.assertEqual(run.status,'failed'); run.status='ready'; run.read=Mock(side_effect=RuntimeError('read'))
|
||||
with self.assertRaisesRegex(RuntimeError,'read'): run._download_stored()
|
||||
self.assertEqual(run.status,'failed')
|
||||
|
||||
def test_uniform_health_mask_reaches_both_kernels(self):
|
||||
run=object.__new__(gpu.UniformGPU); run.configs=(config(16),); run.status='ready'; run.identity=False
|
||||
run.ctx=SimpleNamespace(activate=nullcontext); run.buffers=['f','g','out']; run.active=object(); run.counts='counts'
|
||||
run.stream=object(); run.cuda=SimpleNamespace(memset_d32_async=Mock()); run.launch=Mock()
|
||||
run.read=Mock(return_value=np.zeros(6,np.uint32)); run.health_counts=np.zeros(6,np.uint64); run.step_count=0
|
||||
run.step(audit=False)
|
||||
self.assertEqual([call.args[1] for call in run.launch.call_args_list],['Collide','Pull'])
|
||||
self.assertTrue(all(call.args[4] is run.active for call in run.launch.call_args_list))
|
||||
|
||||
def test_zero_error_comparisons(self):
|
||||
rows=[dict(tstar=t,velocity_l2=0.,pressure_l2=0.,energy_relative=0.) for t in ladder.old.TIMES]
|
||||
result=ladder.comparisons([dict(name=name,rows=rows) for name,_,_ in ladder.CASES],dict(rows=rows))
|
||||
self.assertIsNone(result['trends'][0]['samples'][0]['pressure_l2']['ratio'])
|
||||
self.assertNotIn('two_point_slope',result['controls'][0]['samples'][0]['pressure_l2'])
|
||||
self.assertIn('two_point_slope',result['trends'][0]['samples'][0]['pressure_l2'])
|
||||
json.dumps(result,allow_nan=False)
|
||||
|
||||
def test_frozen_screen_and_json_receipt(self):
|
||||
rows=[dict(tstar=t,minimum_population=.02,**{key:np.float64(limit*.5) for key,limit in ladder.LIMITS.items()}) for t in ladder.old.TIMES]
|
||||
self.assertTrue(ladder.assess(rows,[0]*6)['campaign_pass'])
|
||||
rows[0]['mass_relative']=2e-5
|
||||
failed=ladder.assess(rows,[0]*6); self.assertFalse(failed['campaign_pass'])
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path=Path(tmp)/'receipt.json'; ref.write_receipt(path,dict(completed=True,passed=np.bool_(False),assessment=failed))
|
||||
saved=json.loads(path.read_text()); self.assertFalse(saved['passed']); self.assertTrue(saved['completed'])
|
||||
|
||||
|
||||
class Device(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not ADMITTED: raise unittest.SkipTest('explicit tiny GPU admission required')
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
cls.context=CudaContext(0)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls): cls.context.close()
|
||||
|
||||
def test_uniform_actual_upload_oracle_and_reset(self):
|
||||
for n in (16,64,128,256):
|
||||
with gpu.UniformGPU(config(n),context=self.context) as run:
|
||||
analytic=tg.initial((n,n),0,n,.0003*n); run.initialize(analytic); start=run._download_stored()
|
||||
actual_start=run.download(); mask=np.ones((n,n),bool)
|
||||
spec=ref.Spec(n_coarse=n,patch_lo=1,halo=2,tau_coarse=.5+.0009*n,tau_fine=.5+.0018*n,collision='srt')
|
||||
collided=ref.cpu._collide(spec,actual_start,np.isfinite(actual_start),spec.tau_coarse,mask,np.zeros((n,n),int))
|
||||
expected=np.stack([np.roll(collided[q],(int(ref.cpu.CY[q]),int(ref.cpu.CX[q])),axis=(0,1)) for q in range(9)])
|
||||
ledger=run.step(); actual=run.download()
|
||||
np.testing.assert_allclose(actual,expected,rtol=0,atol=3e-6)
|
||||
audited=run._download_stored(); run.initialize(analytic); run.step(audit=False)
|
||||
np.testing.assert_array_equal(audited.view(np.uint32),run._download_stored().view(np.uint32))
|
||||
run.initialize(analytic); np.testing.assert_array_equal(start.view(np.uint32),run._download_stored().view(np.uint32))
|
||||
self.assertEqual(run.step_count,0); self.assertFalse(run.health_counts.any())
|
||||
RECORDS.append(dict(case=f'uniform{n}_one_interval',error=float(abs(actual-expected).max()),ledger=ledger,
|
||||
build_metadata=run.build_metadata,device_bytes=run.device_bytes,raw_reset_verified=True))
|
||||
|
||||
def test_uniform_tagged_phase_and_health_controls(self):
|
||||
n=16; rng=np.random.default_rng(n)
|
||||
analytic=np.broadcast_to(ref.cpu.W[:,None,None],(9,n,n))*(1+1e-3*rng.standard_normal((9,n,n)))
|
||||
for identity in (True,False):
|
||||
with gpu.UniformGPU(config(n),context=self.context,identity=identity) as run:
|
||||
run.initialize(analytic); start=run.download(); raw_start=run._download_stored(); mask=np.ones((n,n),bool)
|
||||
spec=ref.Spec(tau_coarse=.5+.0009*n,collision='identity' if identity else 'srt')
|
||||
collided=ref.cpu._collide(spec,start,np.isfinite(start),spec.tau_coarse,mask,np.zeros((n,n),int))
|
||||
expected=np.stack([np.roll(collided[q],(int(ref.cpu.CY[q]),int(ref.cpu.CX[q])),axis=(0,1)) for q in range(9)])
|
||||
ledger=run.step(); actual=run.download(); np.testing.assert_allclose(actual,expected,rtol=0,atol=3e-6)
|
||||
if identity:
|
||||
rolled=np.stack([np.roll(raw_start[q],(int(ref.cpu.CY[q]),int(ref.cpu.CX[q])),axis=(0,1)) for q in range(9)])
|
||||
np.testing.assert_array_equal(run._download_stored().view(np.uint32),rolled.view(np.uint32))
|
||||
if not identity:
|
||||
wrong=ref.cpu._collide(spec,expected,np.isfinite(expected),spec.tau_coarse,mask,np.zeros((n,n),int))
|
||||
self.assertGreater(float(abs(actual-wrong).max()),3e-6)
|
||||
RECORDS.append(dict(case='uniform_tagged',identity=identity,error=float(abs(actual-expected).max()),ledger=ledger))
|
||||
with gpu.UniformGPU(config(n),context=self.context) as run:
|
||||
run.initialize(analytic)
|
||||
with self.context.activate():
|
||||
bad=np.broadcast_to(-gpu._WEIGHTS-.01,(9,n,n)).astype(np.float32).copy()
|
||||
run.cuda.memcpy_htod(run.buffers[0],bad)
|
||||
with self.assertRaises(AssertionError): run.step()
|
||||
self.assertTrue(run.health_counts.any()); self.assertEqual(run.status,'failed')
|
||||
RECORDS.append(dict(case='uniform_health_negative_control',health_counts=run.health_counts.tolist()))
|
||||
|
||||
def test_refined_n128_actual_upload_oracle(self):
|
||||
with gpu.SpaceTimeGPU(config(128),(16,16,112,96),context=self.context,allow_n128=True) as run:
|
||||
tables=ladder.old.table_guard.validate_tables(run.plan,run.tables,allow_n128=True)
|
||||
run.initialize(ref.cpu.State(tg.initial((128,128),0,128,.0384),tg.initial((256,256),1,128,.0384)))
|
||||
spec=ref.Spec(n_coarse=128,patch_lo=16,halo=32,tau_coarse=.6152,tau_fine=.7304,collision='srt')
|
||||
expected,_=ref.advance(spec,run.plan,run.download()); ledger=run.step(); actual=run.download(); errors=[]
|
||||
for a,b in zip((actual.coarse_f,actual.fine_f),(expected.coarse_f,expected.fine_f)):
|
||||
np.testing.assert_allclose(a,b,rtol=0,atol=3e-6,equal_nan=True); errors.append(float(np.nanmax(abs(a-b))))
|
||||
RECORDS.append(dict(case='refined128_one_interval',errors=errors,ledger=ledger,tables=tables,
|
||||
build_metadata=run.build_metadata,device_bytes=run.device_bytes))
|
||||
|
||||
|
||||
def main():
|
||||
global ADMITTED
|
||||
p=argparse.ArgumentParser(description=__doc__); modes=p.add_mutually_exclusive_group(required=True)
|
||||
modes.add_argument('--host-only',action='store_true'); modes.add_argument('--gpu-admitted',action='store_true')
|
||||
p.add_argument('--output',type=Path,required=True); args=p.parse_args(); ADMITTED=args.gpu_admitted
|
||||
if args.output.exists(): p.error('output exists; preserve evidence')
|
||||
if ADMITTED and not os.environ.get('CUDA_VISIBLE_DEVICES'): p.error('explicit CUDA_VISIBLE_DEVICES required')
|
||||
paths=(Path(__file__),Path(ladder.__file__),Path(gpu.__file__),Path(gpu.__file__).with_suffix('.cu'),Path(ladder.old.table_guard.__file__))
|
||||
report=dict(completed=False,passed=False,mode='tiny_gpu' if ADMITTED else 'host_only',records=RECORDS,
|
||||
source=dict(commit=subprocess.check_output(['git','rev-parse','HEAD'],cwd=ladder.ROOT,text=True).strip(),
|
||||
dirty=bool(subprocess.check_output(['git','status','--porcelain'],cwd=ladder.ROOT,text=True)),
|
||||
hashes={p.name:ladder.old.sha(p) for p in paths}))
|
||||
ref.write_receipt(args.output,report); started=time.monotonic()
|
||||
suite=unittest.TestLoader().loadTestsFromTestCase(Host)
|
||||
if ADMITTED: suite.addTests(unittest.TestLoader().loadTestsFromTestCase(Device))
|
||||
result=unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
report.update(completed=True,passed=result.wasSuccessful(),tests=result.testsRun,seconds=time.monotonic()-started,
|
||||
failures=[dict(test=str(t),error=e) for t,e in result.errors+result.failures])
|
||||
ref.write_receipt(args.output,report); return 0 if report['passed'] else 1
|
||||
|
||||
|
||||
if __name__=='__main__': raise SystemExit(main())
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Tiny CPU algebra/event tests; no full trajectory or GPU execution."""
|
||||
from dataclasses import replace
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
import numpy as np
|
||||
import spacetime_reference as st
|
||||
|
||||
|
||||
class SpaceTime(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.plan = st.Plan(12, (3, 3, 9, 8))
|
||||
cls.spec = st.Spec(n_coarse=12, patch_lo=1, halo=2,
|
||||
tau_coarse=.5576, tau_fine=.6152, collision="srt")
|
||||
|
||||
def state(self, tagged=False):
|
||||
arrays = []
|
||||
for n, mask in ((12,self.plan.maps.coarse_active),(24,self.plan.maps.fine_active)):
|
||||
q, y, x = np.indices((9,n,n))
|
||||
a = np.broadcast_to(st.cpu.W[:,None,None], (9,n,n)).copy()
|
||||
if tagged:
|
||||
a *= 1+1e-3*np.random.default_rng(n).normal(size=a.shape)
|
||||
a[:,~mask] = np.nan
|
||||
arrays.append(a)
|
||||
return st.cpu.State(*arrays)
|
||||
|
||||
def test_events_and_corner_leftovers(self):
|
||||
groups = [g for g in self.plan.groups if None in g.events]
|
||||
self.assertEqual(len(groups), 8)
|
||||
self.assertTrue(all(g.events.count(None)==3 for g in groups))
|
||||
for m in (0,1):
|
||||
events = list(map(tuple,self.plan.outgoing[m]))
|
||||
self.assertEqual(len(events),len(set(events)))
|
||||
st.Plan(16,(4,5,11,12))
|
||||
|
||||
def test_interpolation(self):
|
||||
for g in self.plan.groups:
|
||||
samples, a, e, coeff = st.interpolation(g.kind,g.points)
|
||||
np.testing.assert_allclose(g.weights@np.ones(len(samples)),1,rtol=0,atol=2e-14)
|
||||
np.testing.assert_allclose(g.weights.mean(axis=0), np.eye(len(samples))[1 if g.kind=='face' else 0],rtol=0,atol=2e-14)
|
||||
if g.kind=='face':
|
||||
beta=np.array([1.,.03,.02,0.])
|
||||
else:
|
||||
cx,cy=st.cpu.CX[g.q],st.cpu.CY[g.q]
|
||||
beta=np.array([1.,.03,.02,-cx*.03-cy*.02])
|
||||
steady=a@np.array([1.,.03,.02,0.])
|
||||
self.assertLess(abs((coeff@steady)[3]),2e-14)
|
||||
np.testing.assert_allclose(g.weights@(a@beta),e@beta,rtol=0,atol=2e-14)
|
||||
|
||||
def test_rest(self):
|
||||
initial=self.state()
|
||||
final,d=st.advance(self.spec,self.plan,initial)
|
||||
for a,b in zip((initial.coarse_f,initial.fine_f),(final.coarse_f,final.fine_f)):
|
||||
np.testing.assert_allclose(a,b,rtol=0,atol=2e-14,equal_nan=True)
|
||||
|
||||
def test_frozen_recovers_ivp(self):
|
||||
for collision in ('identity','srt'):
|
||||
spec=replace(self.spec,collision=collision)
|
||||
a=b=self.state(True)
|
||||
for _ in range(2):
|
||||
a,_=st.advance(spec,self.plan,a,'frozen')
|
||||
b,_=st.cpu.advance_coarse_once(spec,self.plan.maps,b)
|
||||
for aa,bb in zip((a.coarse_f,a.fine_f),(b.coarse_f,b.fine_f)):
|
||||
np.testing.assert_allclose(aa,bb,rtol=0,atol=2e-14,equal_nan=True)
|
||||
|
||||
def test_second_substep_uses_characteristic_origin(self):
|
||||
# Review counterexample in (x,y): ghost (7,10), m=1 traces back
|
||||
# to (6,9), parent (3,4), not its current parent (3,5).
|
||||
plan=st.Plan(16,(4,4,12,12))
|
||||
owners=[g for g in plan.groups if g.q==5 and (1,10*32+7) in g.events]
|
||||
self.assertEqual(len(owners),1)
|
||||
self.assertEqual(owners[0].source,4*16+3)
|
||||
spec=st.Spec(n_coarse=16,patch_lo=2,halo=4,collision='identity')
|
||||
arrays=[]
|
||||
for n,mask in ((16,plan.maps.coarse_active),(32,plan.maps.fine_active)):
|
||||
a=np.broadcast_to(st.cpu.W[:,None,None],(9,n,n)).copy()
|
||||
a[:,~mask]=np.nan
|
||||
arrays.append(a)
|
||||
arrays[0][5,4,3]=1.; arrays[0][5,5,3]=2.
|
||||
final,_=st.advance(spec,plan,st.cpu.State(*arrays),'frozen')
|
||||
self.assertEqual(final.fine_f[5,11,8],1.)
|
||||
self.assertNotEqual(final.fine_f[5,11,8],arrays[0][5,5,3])
|
||||
|
||||
def test_corner_affine_tradeoff_is_explicit(self):
|
||||
group=next(g for g in self.plan.groups if g.kind=='corner')
|
||||
_,a,e,coeff=st.interpolation(group.kind,group.points)
|
||||
linear_x=np.array([0.,1.,0.,0.])
|
||||
# The two paper constraints are consistent. Arbitrary affine exactness
|
||||
# is the incompatible additional demand.
|
||||
self.assertEqual(abs(float((e@linear_x).mean())),.125)
|
||||
fitted=coeff@(a@linear_x)
|
||||
self.assertLess(abs(fitted[3]),2e-14)
|
||||
self.assertLess(abs(float((e@fitted).mean())),2e-14)
|
||||
self.assertGreater(np.max(np.abs(e@fitted-e@linear_x)),.01)
|
||||
|
||||
def test_directional_budgets(self):
|
||||
for collision in ('identity','srt'):
|
||||
spec=replace(self.spec,collision=collision)
|
||||
a=self.state(True)
|
||||
for _ in range(2):
|
||||
a,d=st.advance(spec,self.plan,a)
|
||||
np.testing.assert_allclose(d['directional_residual'],0,rtol=0,atol=2e-12)
|
||||
self.assertLess(d['group_error'],2e-14)
|
||||
|
||||
def test_missing_packets_fail(self):
|
||||
for mutation in ('omit_leftover','omit_outgoing_0','omit_outgoing_1'):
|
||||
_,d=st.advance(self.spec,self.plan,self.state(True),mutation=mutation)
|
||||
self.assertGreater(np.max(np.abs(d['directional_residual'])),1e-5)
|
||||
|
||||
def test_negative_interpolation_is_reported_not_clipped(self):
|
||||
initial=self.state()
|
||||
group=self.plan.groups[0]
|
||||
initial.coarse_f.reshape(9,-1)[group.q,group.source]=-.01
|
||||
spec=replace(self.spec,collision='identity')
|
||||
_,d=st.advance(spec,self.plan,initial,'frozen')
|
||||
self.assertGreaterEqual(d['negative_boundary_count'],4)
|
||||
self.assertEqual(d['min_boundary'],-.01)
|
||||
self.assertEqual(d['nonfinite_boundary_count'],0)
|
||||
|
||||
|
||||
class Receipts(unittest.TestCase):
|
||||
def test_completed_and_failed_screen_receipts(self):
|
||||
for passed in (True, False):
|
||||
with self.subTest(passed=passed), tempfile.TemporaryDirectory() as directory:
|
||||
# Same NumPy comparison type produced by the final metric screen.
|
||||
checks = {'velocity_l2': np.float64(.5 if passed else 1.) <= .8,
|
||||
'mass_momentum': True}
|
||||
self.assertIsInstance(checks['velocity_l2'], np.bool_)
|
||||
result = dict(completed=True, checks=checks,
|
||||
experimental_pass=all(checks.values()),
|
||||
runs=[dict(rows=[dict(step=np.int64(1024),
|
||||
mass_relative=np.float32(0.))])])
|
||||
path = Path(directory)/'receipt.json'
|
||||
st.write_receipt(path, result)
|
||||
saved = json.loads(path.read_text())
|
||||
self.assertIs(saved['completed'], True)
|
||||
self.assertIs(saved['experimental_pass'], passed)
|
||||
self.assertIs(saved['checks']['velocity_l2'], passed)
|
||||
self.assertEqual(saved['runs'][0]['rows'][0],
|
||||
dict(step=1024, mass_relative=0.))
|
||||
|
||||
def test_receipts_still_reject_nonfinite_and_unknown_values(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory)/'receipt.json'
|
||||
with self.assertRaises(ValueError):
|
||||
st.write_receipt(path, {'invalid': np.float32('nan')})
|
||||
with self.assertRaises(TypeError):
|
||||
st.write_receipt(path, {'invalid': object()})
|
||||
self.assertFalse(path.exists())
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
unittest.main(verbosity=2)
|
||||
@@ -0,0 +1,287 @@
|
||||
"""Host-only ownership/geometry tests, never a GPU or accuracy admission."""
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
from CelerisLab.lbm.refinement import FixedPatch2D, FixedPatchSet2D
|
||||
from spacetime_union_plan import UnionPlan, UnresolvedTopology, owned_slots, validate_ownership
|
||||
import spacetime_reference as ref
|
||||
import spacetime_gpu as frozen # pack_plan only: no CUDA import/owner construction
|
||||
from spacetime_table_guard import exact_weights
|
||||
|
||||
MATRIX=[]
|
||||
ENUMERATION={}
|
||||
CASES={
|
||||
'rectangle': ([(4,4,12,12)],True),
|
||||
'two_separated': ([(3,3,6,6),(9,9,12,12)],True),
|
||||
'one_cell_face_gap': ([(3,3,6,10),(7,3,10,10)],True),
|
||||
'one_cell_diagonal_gap': ([(3,3,6,6),(7,7,10,10)],True),
|
||||
'staggered_gap': ([(3,3,6,8),(7,5,11,10)],True),
|
||||
'thin_rectangle': ([(4,4,5,11)],True),
|
||||
'concave_L': ([(4,4,5,7),(4,5,7,7)],True),
|
||||
'corner_contact': ([(3,3,6,6),(6,6,9,9)],True),
|
||||
'cross': ([(3,6,13,9),(6,3,9,13)],True),
|
||||
'narrow_notch': ([(3,3,6,11),(7,3,10,11),(3,9,10,11)],True),
|
||||
'hole': ([(3,3,13,5),(3,11,13,13),(3,5,5,11),(11,5,13,11)],True),
|
||||
}
|
||||
|
||||
|
||||
def pair(rects,n=16,ny=None):
|
||||
cfg=LBMConfig(nx=n,ny=n if ny is None else ny,viscosity=.0192,velocity=0.)
|
||||
return FixedPatchSet2D.from_config(cfg,rects)
|
||||
|
||||
|
||||
def assert_same(test,a,b):
|
||||
# Includes event-child order, zero padding, integer dtype/shape and FP64 bits.
|
||||
aa,bb=frozen.pack_plan(a),frozen.pack_plan(b)
|
||||
test.assertEqual(aa.keys(),bb.keys())
|
||||
for key in aa:
|
||||
test.assertEqual((aa[key].dtype,aa[key].shape,aa[key].tobytes()),
|
||||
(bb[key].dtype,bb[key].shape,bb[key].tobytes()),key)
|
||||
for x,y in zip(a.groups,b.groups):
|
||||
test.assertEqual((x.q,x.source,x.receiver,x.kind,x.points,x.events),
|
||||
(y.q,y.source,y.receiver,y.kind,y.points,y.events))
|
||||
# Frozen pack_plan has no transit column and is NOT a union runtime format.
|
||||
np.testing.assert_array_equal(getattr(a,'transit',np.empty((0,3),int)),
|
||||
getattr(b,'transit',np.empty((0,3),int)))
|
||||
|
||||
|
||||
class UnionContract(unittest.TestCase):
|
||||
def test_single_rectangle_exact_707afe6(self):
|
||||
frozen.check_sources()
|
||||
for n,b in ((12,(3,3,9,8)),(16,(4,5,11,12)),(16,(4,4,5,7)),(64,(8,8,56,48))):
|
||||
with self.subTest(n=n,bounds=b):
|
||||
assert_same(self,UnionPlan(pair([b],n)),ref.Plan(n,b))
|
||||
self.assertNotIn('pycuda.driver',sys.modules)
|
||||
|
||||
def test_union_decomposition_and_permutation_invariance(self):
|
||||
baseline=UnionPlan(pair([(4,4,12,12)]))
|
||||
decompositions=[[(4,4,8,12),(8,4,12,12)],[(4,4,10,12),(6,4,12,12)],
|
||||
[(4,4,12,8),(4,8,12,12)],[(4,4,12,12),(4,4,12,12)]]
|
||||
for rectangles in decompositions:
|
||||
for order in (rectangles,list(reversed(rectangles))):
|
||||
assert_same(self,baseline,UnionPlan(pair(order)))
|
||||
two=CASES['two_separated'][0]
|
||||
assert_same(self,UnionPlan(pair(two)),UnionPlan(pair(list(reversed(two)))))
|
||||
lshape=CASES['concave_L'][0]
|
||||
for decomposition in (list(reversed(lshape)),[(4,4,5,5),(4,5,7,7)],lshape+lshape):
|
||||
assert_same(self,UnionPlan(pair(lshape)),UnionPlan(pair(decomposition)))
|
||||
|
||||
def test_supported_unresolved_matrix(self):
|
||||
for name,(rectangles,supported) in CASES.items():
|
||||
geometry=pair(rectangles) # Existing production accepts EVERY fixture.
|
||||
row=dict(case=name,rectangles=rectangles,production_geometry_accepted=True,
|
||||
runtime_supported=False,interpolation_accuracy='unqualified_for_new_unions')
|
||||
if supported:
|
||||
plan=UnionPlan(geometry); row.update(plan.proof)
|
||||
self.assertEqual(plan.proof['receiver_quarter_packets'],4*len(plan.receivers))
|
||||
else:
|
||||
with self.assertRaises(UnresolvedTopology) as caught: UnionPlan(geometry)
|
||||
row.update(topology='unresolved',code=caught.exception.code,witness=caught.exception.detail)
|
||||
MATRIX.append(row)
|
||||
|
||||
def test_L_witness_and_reflections(self):
|
||||
rectangles=CASES['concave_L'][0]
|
||||
plan=UnionPlan(pair(rectangles))
|
||||
self.assertIn((5,8*32+9,9*32+10),map(tuple,plan.transit))
|
||||
directions=set()
|
||||
for reflect in (False,True):
|
||||
transformed=[(16-x1,y0,16-x0,y1) if reflect else (x0,y0,x1,y1) for x0,y0,x1,y1 in rectangles]
|
||||
for _ in range(4):
|
||||
plan=UnionPlan(pair(transformed)); self.assertEqual(len(plan.transit),2)
|
||||
for q,source,ghost in plan.transit:
|
||||
directions.add(q); p=np.array(divmod(source,32)); c=np.array((ref.cpu.CY[q],ref.cpu.CX[q]))
|
||||
self.assertEqual(ghost,(p+c)[0]*32+(p+c)[1])
|
||||
self.assertEqual([bool(plan.maps.fine_active[tuple(p+j*c)]) for j in range(3)],[True,False,True])
|
||||
transformed=[(16-y1,x0,16-y0,x1) for x0,y0,x1,y1 in transformed]
|
||||
self.assertEqual(directions,{5,6,7,8})
|
||||
|
||||
def test_Fig19_two_hop_transport_and_quarter_budget(self):
|
||||
# Guzik §3.5.4 Fig.19: https://www.osti.gov/servlets/purl/1226215 .
|
||||
# Isolated tagged transport only: no collision law or union evolution.
|
||||
for name in ('concave_L','corner_contact','hole'):
|
||||
p=UnionPlan(pair(CASES[name][0])); q,source,ghost=p.transit.T
|
||||
stride=ref.cpu.CY[q]*p.nf+ref.cpu.CX[q]; target=ghost+stride
|
||||
tags=(np.arange(len(q),dtype=np.float32)+1)/256
|
||||
active=p.maps.fine_active
|
||||
def run(shifted,mutation=None):
|
||||
scratch=np.full((9,p.nf,p.nf),np.nan,np.float32)
|
||||
scratch.reshape(9,-1)[q,source]=tags
|
||||
def moment(field):
|
||||
value=field.astype(float)+(ref.cpu.W[:,None,None] if shifted else 0)
|
||||
return .25*np.where(np.isfinite(field)&active,value,0).sum(axis=(1,2))
|
||||
def pull(field):
|
||||
moved=np.stack([np.roll(field[j],(ref.cpu.CY[j],ref.cpu.CX[j]),axis=(0,1)) for j in range(9)])
|
||||
moved[:,~active]=np.nan
|
||||
return moved
|
||||
initial=moment(scratch)
|
||||
carry=[(int(j),int(h),v.copy()) for j,h,v in zip(q,ghost,scratch.reshape(9,-1)[q,source])]
|
||||
if mutation=='drop': carry.pop()
|
||||
if mutation=='duplicate': carry.append(carry[0])
|
||||
first=pull(scratch) # m0 ghost arrivals are outside the ACTIVE mask.
|
||||
inventory=np.zeros(9)
|
||||
for j,h,value in carry: inventory[j]+=.25*(float(value)+(ref.cpu.W[j] if shifted else 0))
|
||||
np.testing.assert_allclose(moment(first)+inventory,initial,rtol=0,atol=2e-14)
|
||||
if mutation=='early':
|
||||
for j,h,value in carry: scratch.reshape(9,-1)[j,h]=value
|
||||
scratch[:]=np.nan # m1 collision scratch reuse destroys an early injection.
|
||||
while carry:
|
||||
j,h,value=carry.pop()
|
||||
if mutation!='early': scratch.reshape(9,-1)[j,h]=value
|
||||
final=pull(scratch)
|
||||
self.assertEqual(carry,[]) # Consumed route inventory, not subtraction of identical sums.
|
||||
np.testing.assert_allclose(moment(final),initial,rtol=0,atol=2e-14)
|
||||
arrivals=final.reshape(9,-1)[q,target]
|
||||
np.testing.assert_array_equal(arrivals.view(np.uint32),tags.view(np.uint32))
|
||||
for shifted in (False,True):
|
||||
run(shifted)
|
||||
for mutation in ('drop','duplicate','early'):
|
||||
with self.assertRaises(AssertionError): run(shifted,mutation)
|
||||
for direction,slot,_ in p.transit:
|
||||
self.assertNotIn((direction,slot),{tuple(row[:2]) for row in p.outgoing[0]})
|
||||
for change in (lambda b:setattr(b,'transit',b.transit[1:]),
|
||||
lambda b:setattr(b,'transit',np.vstack((b.transit,b.transit[0]))),
|
||||
lambda b:b.transit.__setitem__((0,2),b.transit[0,2]+1),
|
||||
lambda b:b.transit.__setitem__((0,0),1)):
|
||||
bad=copy.deepcopy(p); change(bad)
|
||||
with self.assertRaises(ValueError): validate_ownership(bad)
|
||||
bad=copy.deepcopy(p); direction,slot,_=p.transit[0]
|
||||
y,x=divmod(slot,p.nf); cy,cx=ref.cpu.CY[direction],ref.cpu.CX[direction]
|
||||
covered_target=((y+2*cy)//2)*p.n+(x+2*cx)//2
|
||||
bad.outgoing[0]=np.vstack((bad.outgoing[0],[direction,slot,covered_target]))
|
||||
with self.assertRaises(ValueError): validate_ownership(bad)
|
||||
|
||||
def test_Fig13_first_entry_and_wrong_crossing(self):
|
||||
p=UnionPlan(pair(CASES['concave_L'][0]))
|
||||
g=next(g for g in p.groups if g.source==4*16+5 and g.q==8)
|
||||
self.assertEqual(g.kind,'corner')
|
||||
self.assertEqual(sorted(point[-1] for point in g.points),[-.25,-.25,-.25,.25])
|
||||
# For a concave T first entry is min, not the frozen convex max.
|
||||
damaged=copy.deepcopy(p); wrong=next(x for x in damaged.groups if x.source==g.source and x.q==g.q)
|
||||
origin=np.array((4,5)); c=np.array((ref.cpu.CY[g.q],ref.cpu.CX[g.q])); points=[]
|
||||
for child in ((0,0),(0,1),(1,0),(1,1)):
|
||||
position=origin+(np.array(child)+.5)/2
|
||||
time=max((origin+ (c>0)-position)/c); offset=position+c*time-(origin+.5+c*.5)
|
||||
points.append((float(offset[1]),float(offset[0]),float(time-.5)))
|
||||
wrong.points=tuple(points)
|
||||
with self.assertRaises(ValueError): validate_ownership(damaged)
|
||||
|
||||
def test_all_511_small_masks(self):
|
||||
counts=dict(masks=0,with_transit=0,transit_paths=0)
|
||||
for bits in range(1,512):
|
||||
rectangles=[(4+x,4+y,5+x,5+y) for y in range(3) for x in range(3) if bits & (1<<(3*y+x))]
|
||||
p=UnionPlan(pair(rectangles)); counts['masks']+=1
|
||||
counts['with_transit']+=bool(len(p.transit)); counts['transit_paths']+=len(p.transit)
|
||||
# Independent first-entry check: fine-cell membership before/after
|
||||
# each quarter crossing, without the plan's plane min/max helper.
|
||||
for g in p.groups:
|
||||
c=np.array((ref.cpu.CY[g.q],ref.cpu.CX[g.q])); origin=np.array(divmod(g.source,p.n))
|
||||
for k,child in enumerate(((0,0),(0,1),(1,0),(1,1))):
|
||||
start=2*origin+child; hits=[]
|
||||
for t4 in (1,3):
|
||||
before=(4*start+2+c*(2*t4-1))//4
|
||||
after=(4*start+2+c*(2*t4+1))//4
|
||||
if not p.maps.fine_active[tuple(before)] and p.maps.fine_active[tuple(after)]: hits.append(t4/4)
|
||||
if hits: self.assertEqual(g.points[k][-1]+.5,hits[0])
|
||||
self.assertEqual(counts,dict(masks=511,with_transit=384,transit_paths=2048)); ENUMERATION.update(counts)
|
||||
|
||||
def test_ordered_provenance_and_packet_mutations_fail(self):
|
||||
baseline=UnionPlan(pair(CASES['two_separated'][0]))
|
||||
def reject(change):
|
||||
bad=copy.deepcopy(baseline); change(bad)
|
||||
with self.assertRaises((ValueError,KeyError)): validate_ownership(bad)
|
||||
reject(lambda p:setattr(p.groups[0],'source',p.groups[0].source+1))
|
||||
reject(lambda p:setattr(p.groups[0],'samples',p.groups[0].samples+1))
|
||||
reject(lambda p:setattr(p.groups[0],'events',tuple(reversed(p.groups[0].events))))
|
||||
reject(lambda p:setattr(p.groups[0],'receiver',p.groups[0].receiver+1))
|
||||
reject(lambda p:p.outgoing.__setitem__(0,p.outgoing[0][1:]))
|
||||
reject(lambda p:p.outgoing.__setitem__(1,np.vstack((p.outgoing[1],p.outgoing[1][0]))))
|
||||
reject(lambda p:setattr(p,'receivers',p.receivers[1:]))
|
||||
def balanced(p):
|
||||
p.groups[0].weights[:2,:2]+=1e-5*np.array([[1,-1],[-1,1]])
|
||||
reject(balanced) # Preserves row/column sums, corrupts the actual operator.
|
||||
|
||||
def test_coefficients_and_directional_transfer_budget(self):
|
||||
rng=np.random.default_rng(31)
|
||||
for name in CASES:
|
||||
p=UnionPlan(pair(CASES[name][0])); total=np.zeros(9)
|
||||
receiver={tuple(row):[] for row in p.receivers}; first_leftover=None
|
||||
for g in p.groups:
|
||||
samples,a,e,coeff=ref.interpolation(g.kind,g.points)
|
||||
np.testing.assert_allclose(g.weights,exact_weights(g.kind,g.points),rtol=0,atol=2e-14)
|
||||
np.testing.assert_allclose(g.weights@np.ones(len(samples)),1,rtol=0,atol=2e-14)
|
||||
if g.kind=='face': beta=np.array([1.,.03,.02,0.])
|
||||
else:
|
||||
beta=np.array([1.,.03,.02,-ref.cpu.CX[g.q]*.03-ref.cpu.CY[g.q]*.02])
|
||||
self.assertLess(abs((coeff@(a@np.array([1.,.03,.02,0.])))[3]),2e-14)
|
||||
np.testing.assert_allclose(g.weights@(a@beta),e@beta,rtol=0,atol=2e-14)
|
||||
values=rng.normal(size=len(samples)); h=g.weights@values
|
||||
origin=values[0 if g.kind=='corner' else 1]
|
||||
# Fine incoming plus coarse leftovers must exactly split one
|
||||
# coarse directional source budget, regardless of sample values.
|
||||
incoming=sum(h[k] for k,event in enumerate(g.events) if event is not None)
|
||||
leftover=sum(h[k] for k,event in enumerate(g.events) if event is None)
|
||||
self.assertLess(abs(.25*(incoming+leftover)-origin),2e-14)
|
||||
total[g.q]+=.25*incoming-origin
|
||||
for k,event in enumerate(g.events):
|
||||
if event is None:
|
||||
receiver[(g.q,g.receiver)].append(h[k])
|
||||
if first_leftover is None: first_leftover=(g.q,h[k])
|
||||
# Tagged collision-free boundary-flux ledger, not a solver trajectory:
|
||||
# every outgoing fine debit is credited to its actual receiver.
|
||||
for rows in p.outgoing:
|
||||
for q,slot,target in rows:
|
||||
value=float(rng.normal()); total[q]-=.25*value
|
||||
receiver[(q,target)].append(value)
|
||||
for (q,target),values in receiver.items():
|
||||
self.assertEqual(len(values),4); total[q]+=.25*sum(values)
|
||||
np.testing.assert_allclose(total,0,rtol=0,atol=2e-14)
|
||||
damaged=total.copy(); q,value=first_leftover; damaged[q]-=.25*value
|
||||
self.assertGreater(float(abs(damaged).max()),1e-5)
|
||||
self.assertEqual(validate_ownership(p)['topology'],'supported')
|
||||
|
||||
def test_existing_block_addressing_and_sparse_holes(self):
|
||||
geometry=pair(CASES['two_separated'][0]); p=UnionPlan(geometry)
|
||||
layouts=(BlockLayout2D.build(16,16,4,4),geometry.fine_layout((4,4)))
|
||||
for fine,layout in enumerate(layouts):
|
||||
cells=layout.shape[0]*layout.shape[1]
|
||||
dense=np.arange(9*cells,dtype=np.float64).reshape(9,*layout.shape)
|
||||
blocked=layout.to_blocked(dense).ravel()
|
||||
refs=[]
|
||||
if not fine:
|
||||
refs=[(g.q,int(k)) for g in p.groups for k in [g.source,g.receiver,*g.samples]]
|
||||
else:
|
||||
refs=[(g.q,slot) for g in p.groups for event in g.events if event is not None for _,slot in [event]]
|
||||
refs += [(int(q),int(slot)) for rows in p.outgoing for q,slot,_ in rows]
|
||||
q,k=np.array(refs).T
|
||||
np.testing.assert_array_equal(blocked[owned_slots(layout,q,k)],dense.reshape(9,-1)[q,k])
|
||||
holes=np.flatnonzero(~layouts[1].owned)
|
||||
self.assertGreater(len(holes),0)
|
||||
with self.assertRaises(UnresolvedTopology): owned_slots(layouts[1],np.array([1]),holes[:1])
|
||||
|
||||
def test_deferred_layouts_and_phase_are_explicit(self):
|
||||
with self.assertRaises(UnresolvedTopology) as caught: UnionPlan(pair([(3,3,8,8)],16,12))
|
||||
self.assertEqual(caught.exception.code,'requires_level_wide_square_pair')
|
||||
cfg=LBMConfig(nx=16,ny=16,viscosity=.0192,velocity=0.)
|
||||
scalar=FixedPatch2D.from_config(cfg,(4,4,8,8))
|
||||
with self.assertRaises(UnresolvedTopology): UnionPlan(scalar)
|
||||
self.assertEqual(UnionPlan.phase,'arrival_f'); self.assertFalse(UnionPlan.runtime_supported)
|
||||
# Restriction and collision do not commute, even at omega=1. This is
|
||||
# why postcollision G storage cannot be relabeled/inverted into this F contract.
|
||||
w=ref.cpu.W; cx,cy=ref.cpu.CX,ref.cpu.CY
|
||||
def equilibrium(u):
|
||||
cu=cx*u;return w*(1+3*cu+4.5*cu*cu-1.5*u*u)
|
||||
children=np.array([equilibrium(u) for u in (.03,-.03,.03,-.03)])
|
||||
self.assertAlmostEqual(float(abs(w-children.mean(axis=0)).max()),.0006,places=14)
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
result=unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(UnionContract))
|
||||
print(json.dumps(dict(passed=result.wasSuccessful(),tests=result.testsRun,
|
||||
scope='host-only topology/provenance/coefficient checks; no runtime or accuracy admission',matrix=MATRIX,
|
||||
enumeration=ENUMERATION),indent=2))
|
||||
sys.exit(not result.wasSuccessful())
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Frozen matched SRT DFG comparison. GPU execution requires explicit root admission."""
|
||||
"""Frozen matched SRT/MRT DFG comparison. GPU execution requires explicit root admission."""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
@@ -39,20 +39,22 @@ def helpers(source):
|
||||
return h, periodic
|
||||
|
||||
|
||||
def config(h, diameter):
|
||||
cfg, body, p = h.make_config(diameter, SPEED, "SRT", "neq_extrap", "zou_he_local")
|
||||
def config(h, diameter, collision="SRT"):
|
||||
cfg, body, p = h.make_config(diameter, SPEED, collision, "neq_extrap", "zou_he_local")
|
||||
cfg["method"]["outlet"]["srt_neq_damp"] = 0.5
|
||||
return cfg, body, p
|
||||
|
||||
|
||||
def plan(h):
|
||||
_, _, c = config(h, 40)
|
||||
_, _, f = config(h, 80)
|
||||
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
|
||||
fine_active = 4 * (x1 - x0) * (y1 - y0)
|
||||
coarse_uncovered = c["nx"] * c["ny"] - fine_active // 4
|
||||
return dict(
|
||||
status="prepared_not_run",
|
||||
collision=collision,
|
||||
observation_mode=observation_mode,
|
||||
root_gpu_gates_required=True,
|
||||
root=c,
|
||||
uniform=f,
|
||||
@@ -82,7 +84,11 @@ def plan(h):
|
||||
/ (2 * f["nx"] * f["ny"]),
|
||||
cost_caveat="Geometry-only estimates include ghosts/solids; exclude block halos, transfers, observation and launch overhead. Not measured speedup.",
|
||||
sampling_offset="For origin (38,22), fine physical x=(j+75.5)*dx80 versus uniform i*dx80: half-fine-cell x offset. Fine physical y=(k+42.5)*dx80 equals uniform (l-.5)*dx80 at l=k+43: y samples align. Body, wall and source planes coincide.",
|
||||
outlet="SRT pressure NEQ, all nine fneq damped by .5, no backflow clamp; noncanonical DFG outlet",
|
||||
outlet=(
|
||||
"SRT pressure NEQ, all nine fneq damped by .5, no backflow clamp; noncanonical DFG outlet"
|
||||
if collision == "SRT" else
|
||||
"MRT pressure NEQ, unknowns q2/6/8 extrapolated; no backflow clamp; noncanonical DFG outlet"
|
||||
),
|
||||
modes={"pilot_tstar": 5.0, "full_tstar": 300.0},
|
||||
observation_dense_tstar=0.02,
|
||||
)
|
||||
@@ -120,7 +126,7 @@ def initial_state(shape, origin, spacing, height=164.0, speed=SPEED, root=False)
|
||||
return w * (1 + 3 * cu + 4.5 * cu * cu - 1.5 * velocity * velocity)
|
||||
|
||||
|
||||
def host_geometry(source, h):
|
||||
def host_geometry(source, h, collision="SRT"):
|
||||
"""Production donor/transfer and complete cut-link checks, without CUDA."""
|
||||
from CelerisLab.config import load_lbm_config
|
||||
from CelerisLab.lbm.refinement import FixedPatch2D, prepare_fine_boundary
|
||||
@@ -133,7 +139,7 @@ def host_geometry(source, h):
|
||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||
from CelerisLab.body.objects import SimObject
|
||||
|
||||
cfgdict, _, _ = config(h, 40)
|
||||
cfgdict, _, _ = config(h, 40, collision)
|
||||
with TemporaryDirectory() as tmp:
|
||||
p = Path(tmp) / "cfg.json"
|
||||
p.write_text(json.dumps(cfgdict))
|
||||
@@ -151,14 +157,60 @@ def host_geometry(source, h):
|
||||
return cfg, pair, body, host
|
||||
|
||||
|
||||
def verify_source(source):
|
||||
expected = json.loads((HERE / "data/refined_dfg/source_manifest.json").read_text())
|
||||
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")
|
||||
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:
|
||||
raise RuntimeError("Source mismatch: " + name)
|
||||
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
|
||||
@@ -167,7 +219,7 @@ def run_case(a, h, periodic, case):
|
||||
from contextlib import ExitStack
|
||||
|
||||
refined = case == "refined"
|
||||
cfgdict, bodydict, p = config(h, 40 if refined else 80)
|
||||
cfgdict, bodydict, p = config(h, 40 if refined else 80, a.collision)
|
||||
cap = 5.0 if a.mode == "pilot" else 300.0
|
||||
max_steps = math.ceil(cap * p["diameter"] / SPEED)
|
||||
rows = []
|
||||
@@ -179,7 +231,7 @@ def run_case(a, h, periodic, case):
|
||||
dest.mkdir()
|
||||
with ExitStack() as stack:
|
||||
if refined:
|
||||
cfg, pair, body, host = host_geometry(a.source, h)
|
||||
cfg, pair, body, host = host_geometry(a.source, h, a.collision)
|
||||
context = stack.enter_context(CudaContext(0))
|
||||
backend = stack.enter_context(
|
||||
FixedPatchStepper(
|
||||
@@ -191,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
|
||||
@@ -240,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:
|
||||
@@ -336,6 +378,9 @@ def run_case(a, h, periodic, case):
|
||||
if len(rows) == 1:
|
||||
ready = dict(
|
||||
case=case,
|
||||
collision=a.collision,
|
||||
observation_mode=a.observation_mode,
|
||||
**a.provenance,
|
||||
mode=a.mode,
|
||||
step=begin,
|
||||
tstar=row["tstar"],
|
||||
@@ -348,6 +393,9 @@ def run_case(a, h, periodic, case):
|
||||
print("READY " + json.dumps(ready, allow_nan=False), flush=True)
|
||||
result = dict(
|
||||
case=case,
|
||||
collision=a.collision,
|
||||
observation_mode=a.observation_mode,
|
||||
**a.provenance,
|
||||
mode=a.mode,
|
||||
history=rows,
|
||||
healthy=bool(healthy),
|
||||
@@ -416,12 +464,16 @@ def check_deadline(deadline):
|
||||
)
|
||||
|
||||
|
||||
def verify_pilot(path, manifest):
|
||||
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(
|
||||
BOUNDS
|
||||
):
|
||||
@@ -432,7 +484,9 @@ def verify_pilot(path, manifest):
|
||||
for case, diameter in (("uniform", 80), ("refined", 40)):
|
||||
summary = read_finite_json(path / case / "summary.json")
|
||||
if (
|
||||
summary.get("case") != case
|
||||
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"
|
||||
or summary.get("healthy") is not True
|
||||
@@ -483,7 +537,9 @@ def verify_pilot(path, manifest):
|
||||
raise ValueError("Unhealthy pilot observations")
|
||||
ready = read_finite_json(path / case / "ready.json")
|
||||
if (
|
||||
ready.get("case") != case
|
||||
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
|
||||
or ready.get("runner_sha256") != runner_hash()
|
||||
@@ -507,6 +563,9 @@ def verify_pilot(path, manifest):
|
||||
|
||||
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)
|
||||
parser.add_argument("--mode", choices=["plan", "pilot", "full"], default="plan")
|
||||
@@ -519,9 +578,16 @@ def main():
|
||||
)
|
||||
a = parser.parse_args()
|
||||
a.source = a.source.resolve()
|
||||
manifest = verify_source(a.source)
|
||||
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)
|
||||
calibration = verify_pilot(a.pilot_results, manifest) if a.mode == "full" else None
|
||||
from source_provenance import checkout_provenance
|
||||
|
||||
a.provenance = checkout_provenance(a.source)
|
||||
calibration = (
|
||||
verify_pilot(a.pilot_results, manifest, a.collision, a.observation_mode)
|
||||
if a.mode == "full" else None
|
||||
)
|
||||
if a.mode != "plan" and (
|
||||
a.max_wall_seconds is None
|
||||
or not math.isfinite(a.max_wall_seconds)
|
||||
@@ -536,16 +602,17 @@ 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)
|
||||
specification = plan(h, a.collision, a.observation_mode)
|
||||
specification.update(
|
||||
source_manifest=manifest,
|
||||
**a.provenance,
|
||||
gates=periodic.GATES,
|
||||
reference_gates=periodic.REFERENCE_GATES,
|
||||
runner_sha256=runner_hash(),
|
||||
wall_time_budget_seconds=budget,
|
||||
full_pilot_calibration_seconds=calibration,
|
||||
)
|
||||
host_geometry(a.source, h)
|
||||
host_geometry(a.source, h, a.collision)
|
||||
(a.output / "plan.json").write_text(json.dumps(specification, indent=2))
|
||||
if a.mode == "plan":
|
||||
return 0
|
||||
@@ -561,6 +628,9 @@ def main():
|
||||
except TimeoutError as exc:
|
||||
failure = dict(
|
||||
status="wall_time_limit",
|
||||
collision=a.collision,
|
||||
observation_mode=a.observation_mode,
|
||||
**a.provenance,
|
||||
case=case,
|
||||
matched_case_pass=False,
|
||||
error=str(exc),
|
||||
@@ -574,6 +644,9 @@ def main():
|
||||
if not result["healthy"]:
|
||||
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,8 +59,15 @@ 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",
|
||||
}
|
||||
|
||||
|
||||
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"]
|
||||
@@ -69,7 +76,7 @@ def source_guard(root, variant):
|
||||
)
|
||||
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,326 @@
|
||||
"""Measurement-only wall timing for the matched MRT refined DFG case. No numerical edits.
|
||||
|
||||
Variants over EQUAL simulated duration (one refined base step = two uniform D80 steps):
|
||||
A refined: step(1) + read_raw_exchange per base step -- runner-like force-read workload
|
||||
B refined: step(1) only, no host reads -- timing-only, no force history
|
||||
C refined: one step(N) call -- timing-only; ledger keeps final base interval only
|
||||
E uniform: sim.run(<=100) + read_force -- runner-like force-read workload
|
||||
E' uniform: one sim.run(2N) call -- timing-only; at most a whole-window force
|
||||
average (different summation order), no history
|
||||
A/E read forces but discard them: no retained force history, no interval aggregation/validation,
|
||||
and no macro/probe/CSV cost, so they are not full benchmark throughput.
|
||||
observe_raw_exchange stays True for A/B/C: B/C omit host reads only.
|
||||
Status lines: CONSTRUCTED = backend/simulation built (JIT done); WARM_READY = first warm-up and
|
||||
health check passed for that case.
|
||||
Parity compares A/B/C with each other and E/E' with each other, never uniform with refined.
|
||||
A-B and B-C are measured comparative differences, not exact causal decompositions.
|
||||
Each repeat starts from the same fresh initial state with the same warm-up; construction,
|
||||
compilation, parity and health checks are outside timed regions. The actual stepping stream is
|
||||
synchronized explicitly immediately before the timer starts and after the timed loop. Wall time
|
||||
(perf_counter) is the only timing source; no CUDA-event span is reported as kernel time. An outer
|
||||
--max-wall-seconds deadline is checked between variants and inside per-step loops.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import numpy as np
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
import run_refined_dfg as r # noqa: E402 (published runner: same setup functions)
|
||||
|
||||
VARIANTS = {
|
||||
"A": dict(case="refined", force_history_valid=False, force_reads=True,
|
||||
force_note="runner-like force-read workload; values discarded, no aggregation"),
|
||||
"B": dict(case="refined", force_history_valid=False, timing_only=True,
|
||||
force_note="no host reads"),
|
||||
"C": dict(case="refined", force_history_valid=False, timing_only=True,
|
||||
force_note="raw ledger retains only the final base interval"),
|
||||
"E": dict(case="uniform", force_history_valid=False, force_reads=True,
|
||||
force_note="runner-like force-read workload; values discarded, no aggregation"),
|
||||
"E'": dict(case="uniform", force_history_valid=False, timing_only=True,
|
||||
force_note="at most a whole-window accumulated average; different summation order"),
|
||||
}
|
||||
|
||||
|
||||
def schedule(n_base, warmup_base):
|
||||
"""Steps per case for equal simulated duration (refined dt_root = 2 dt_uniform)."""
|
||||
if n_base <= 0 or warmup_base < 0:
|
||||
raise ValueError("n_base must be positive and warmup_base nonnegative")
|
||||
return dict(refined=(warmup_base, n_base), uniform=(2 * warmup_base, 2 * n_base),
|
||||
tstar_window=n_base * r.SPEED / 40.0)
|
||||
|
||||
|
||||
# Existing run_refined_dfg safety limits (rho in [.5, 1.5], sampled max Mach <= .2).
|
||||
RHO_LIMITS, MAX_MACH = (0.5, 1.5), 0.2
|
||||
|
||||
|
||||
def same_state(reference, values):
|
||||
"""Bitwise populations and identical NaN masks; returns a short description or None.
|
||||
|
||||
Matching NaN is accepted only as a storage mask (inactive/ghost/solid slots); physical
|
||||
leaf health is checked separately. Any infinity in either state is a mismatch.
|
||||
"""
|
||||
reference, values = list(reference), list(values)
|
||||
if len(reference) != len(values):
|
||||
return f"level count {len(reference)} != {len(values)}"
|
||||
for level, (a, b) in enumerate(zip(reference, values)):
|
||||
a, b = np.asarray(a), np.asarray(b)
|
||||
if a.shape != b.shape:
|
||||
return f"level {level}: shape {a.shape} != {b.shape}"
|
||||
if np.isinf(a).any() or np.isinf(b).any():
|
||||
return f"level {level}: infinite population"
|
||||
if not np.array_equal(np.isnan(a), np.isnan(b)):
|
||||
return f"level {level}: NaN mask differs"
|
||||
finite = np.isfinite(a)
|
||||
if a.dtype != b.dtype or not np.array_equal(a[finite].view(np.uint8), b[finite].view(np.uint8)):
|
||||
return f"level {level}: finite bits differ"
|
||||
return None
|
||||
|
||||
|
||||
def uniform_populations_finite(ddf, fluid):
|
||||
"""Simulation.get_ddf() may be flat (9*ny*nx); view it as (Q, cells) before masking."""
|
||||
ddf, fluid = np.asarray(ddf), np.asarray(fluid, bool).ravel()
|
||||
if fluid.size == 0 or ddf.size % fluid.size:
|
||||
raise RuntimeError(f"DDF size {ddf.size} incompatible with {fluid.size} cells")
|
||||
return bool(np.isfinite(ddf.reshape(ddf.size // fluid.size, fluid.size)[:, fluid]).all())
|
||||
|
||||
|
||||
def physical_health(fields):
|
||||
"""fields: (rho, ux, uy) arrays already restricted to physical leaves/fluid."""
|
||||
if not fields or any(not np.isfinite(v).all() or v.size == 0 for f in fields for v in f):
|
||||
raise RuntimeError("nonfinite or empty physical macroscopic field")
|
||||
rho_min = float(min(f[0].min() for f in fields))
|
||||
rho_max = float(max(f[0].max() for f in fields))
|
||||
max_mach = float(max(np.sqrt(3 * (f[1] ** 2 + f[2] ** 2)).max() for f in fields))
|
||||
if not (RHO_LIMITS[0] <= rho_min <= rho_max <= RHO_LIMITS[1] and max_mach <= MAX_MACH):
|
||||
raise RuntimeError(f"health limits violated: rho [{rho_min}, {rho_max}], Mach {max_mach}")
|
||||
return dict(rho_min=rho_min, rho_max=rho_max, max_mach=max_mach)
|
||||
|
||||
|
||||
def refined_health(backend, masks):
|
||||
arrays = backend.download() # physical FP64 populations; non-leaves NaN by contract
|
||||
if any(not np.isfinite(g[:, m]).all() for g, m in zip(arrays, masks)):
|
||||
raise RuntimeError("nonfinite physical leaf population")
|
||||
return physical_health([tuple(v[m] for v in r.macros(g)) for g, m in zip(arrays, masks)])
|
||||
|
||||
|
||||
def uniform_health(sim, fluid):
|
||||
m = sim.get_macroscopic()
|
||||
return physical_health([tuple(np.asarray(m[k], np.float64).reshape(fluid.shape)[fluid]
|
||||
for k in ("rho", "ux", "uy"))])
|
||||
|
||||
|
||||
def check_deadline(deadline):
|
||||
if time.monotonic() >= deadline:
|
||||
raise TimeoutError("timing harness exceeded --max-wall-seconds")
|
||||
|
||||
|
||||
def sync_refined(backend):
|
||||
with backend.ctx.activate():
|
||||
backend._stream.synchronize()
|
||||
|
||||
|
||||
def sync_uniform(sim):
|
||||
with sim.ctx.activate():
|
||||
sim.stream.synchronize()
|
||||
|
||||
|
||||
def refined_loop(backend, variant, n, deadline):
|
||||
if variant == "C":
|
||||
backend.step(n)
|
||||
return
|
||||
for i in range(n):
|
||||
backend.step(1)
|
||||
if variant == "A":
|
||||
backend.read_raw_exchange(0, level=1, window="last_base_step")
|
||||
if i % 100 == 99:
|
||||
check_deadline(deadline)
|
||||
|
||||
|
||||
def uniform_loop(sim, variant, n, deadline):
|
||||
if variant == "E'":
|
||||
sim.run(n)
|
||||
return
|
||||
done = 0
|
||||
while done < n:
|
||||
batch = min(100, n - done)
|
||||
sim.run(batch)
|
||||
sim.read_force(0)
|
||||
done += batch
|
||||
check_deadline(deadline)
|
||||
|
||||
|
||||
def time_refined(a, h, sched):
|
||||
from contextlib import ExitStack
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
from CelerisLab.lbm.channel import channel_masks
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
from CelerisLab.lbm.descriptors import SOLID
|
||||
|
||||
warm, n = sched["refined"]
|
||||
rows, reference = [], {}
|
||||
with ExitStack() as stack:
|
||||
cfg, pair, body, host = r.host_geometry(a.source, h, "MRT")
|
||||
context = stack.enter_context(CudaContext(0))
|
||||
backend = stack.enter_context(FixedPatchStepper(
|
||||
cfg, r.BOUNDS, context=context, coarse_boundary="channel", base_block=r.BASE_BLOCK,
|
||||
fine_block=r.FINE_BLOCK, fine_objects=(body,), observe_raw_exchange=True))
|
||||
masks = [channel_masks(pair.coarse_shape)[0] & ~pair.covered, # as run_refined_dfg
|
||||
(pair.fine_kind == ACTIVE) & ((host.flags.reshape(pair.fine_shape) & SOLID) == 0)]
|
||||
fresh = (r.initial_state(pair.coarse_shape, (0.0, 0.0), 1.0, root=True),
|
||||
r.initial_state(pair.fine_shape, pair.fine_origin, 0.5))
|
||||
print(json.dumps(dict(status="CONSTRUCTED", case="refined")), flush=True)
|
||||
for repeat in range(a.repeats):
|
||||
for variant in ("A", "B", "C"):
|
||||
check_deadline(a.deadline)
|
||||
backend.initialize(*fresh) # resets step count; same fresh state every time
|
||||
refined_loop(backend, variant, warm, a.deadline)
|
||||
warm_health = refined_health(backend, masks) # untimed
|
||||
if repeat == 0 and variant == "A":
|
||||
print(json.dumps(dict(status="WARM_READY", case="refined")), flush=True)
|
||||
sync_refined(backend)
|
||||
start = time.perf_counter()
|
||||
refined_loop(backend, variant, n, a.deadline)
|
||||
sync_refined(backend)
|
||||
seconds = time.perf_counter() - start
|
||||
end_health = refined_health(backend, masks) # untimed
|
||||
values = [np.array(v, copy=True) for v in backend._download_raw().values]
|
||||
if not all(np.isfinite(v[:, m]).all() for v, m in zip(values, backend._state)):
|
||||
raise RuntimeError(f"refined {variant} repeat {repeat}: nonfinite leaf populations")
|
||||
mismatch = None
|
||||
if repeat == 0 and variant == "A":
|
||||
reference["A"] = values
|
||||
else:
|
||||
mismatch = same_state(reference["A"], values)
|
||||
rows.append(dict(variant=variant, repeat=repeat, seconds=seconds,
|
||||
base_steps=n, step_count=backend.step_count,
|
||||
health_after_warmup=warm_health, health_after_window=end_health,
|
||||
parity_vs_A=mismatch or "identical", **VARIANTS[variant]))
|
||||
if mismatch:
|
||||
raise RuntimeError(f"refined {variant} repeat {repeat}: {mismatch}")
|
||||
return rows
|
||||
|
||||
|
||||
def time_uniform(a, h, sched):
|
||||
from CelerisLab.simulation import Simulation
|
||||
from CelerisLab.lbm.descriptors import FLUID
|
||||
|
||||
warm, n = sched["uniform"]
|
||||
rows, reference = [], {}
|
||||
cfgdict, bodydict, params = r.config(h, 80, "MRT")
|
||||
with TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
(tmp / "lbm.json").write_text(json.dumps(cfgdict))
|
||||
(tmp / "body.json").write_text(json.dumps(bodydict))
|
||||
with Simulation(str(tmp / "lbm.json"), str(tmp / "body.json"), device_id=0) as sim:
|
||||
sim.initialize()
|
||||
sim.snapshot() # fresh state; restore keeps clock and streaming parity
|
||||
fluid = (sim.get_flags() & FLUID) != 0
|
||||
ny, nx = params["ny"], params["nx"]
|
||||
fluid = fluid.reshape(ny, nx)
|
||||
print(json.dumps(dict(status="CONSTRUCTED", case="uniform")), flush=True)
|
||||
for repeat in range(a.repeats):
|
||||
for variant in ("E", "E'"):
|
||||
check_deadline(a.deadline)
|
||||
sim.restore()
|
||||
if sim.stepper.step_count % 2:
|
||||
raise RuntimeError("uniform restore parity changed between variants")
|
||||
uniform_loop(sim, variant, warm, a.deadline)
|
||||
warm_health = uniform_health(sim, fluid) # untimed
|
||||
if repeat == 0 and variant == "E":
|
||||
print(json.dumps(dict(status="WARM_READY", case="uniform")), flush=True)
|
||||
sync_uniform(sim)
|
||||
start = time.perf_counter()
|
||||
uniform_loop(sim, variant, n, a.deadline)
|
||||
sync_uniform(sim)
|
||||
seconds = time.perf_counter() - start
|
||||
end_health = uniform_health(sim, fluid) # untimed
|
||||
values = [sim.get_ddf()]
|
||||
if not uniform_populations_finite(values[0], fluid):
|
||||
raise RuntimeError(f"uniform {variant} repeat {repeat}: nonfinite fluid populations")
|
||||
mismatch = None
|
||||
if repeat == 0 and variant == "E":
|
||||
reference["E"] = values
|
||||
else:
|
||||
mismatch = same_state(reference["E"], values)
|
||||
rows.append(dict(variant=variant, repeat=repeat, seconds=seconds,
|
||||
uniform_steps=n, parity_vs_E=mismatch or "identical",
|
||||
health_after_warmup=warm_health, health_after_window=end_health,
|
||||
**VARIANTS[variant]))
|
||||
if mismatch:
|
||||
raise RuntimeError(f"uniform {variant} repeat {repeat}: {mismatch}")
|
||||
return rows
|
||||
|
||||
|
||||
def summarize(rows, tstar_window):
|
||||
summary = {}
|
||||
for variant in VARIANTS:
|
||||
seconds = [row["seconds"] for row in rows if row["variant"] == variant]
|
||||
if seconds:
|
||||
median = statistics.median(seconds)
|
||||
summary[variant] = dict(median_seconds=median, seconds=seconds,
|
||||
seconds_per_tstar=median / tstar_window,
|
||||
**VARIANTS[variant])
|
||||
if {"A", "B", "C"} <= summary.keys():
|
||||
summary["A_minus_B_seconds"] = summary["A"]["median_seconds"] - summary["B"]["median_seconds"]
|
||||
summary["B_minus_C_seconds"] = summary["B"]["median_seconds"] - summary["C"]["median_seconds"]
|
||||
if {"A", "E"} <= summary.keys():
|
||||
summary["A_over_E"] = summary["A"]["median_seconds"] / summary["E"]["median_seconds"]
|
||||
if {"C", "E'"} <= summary.keys():
|
||||
summary["C_over_Eprime"] = summary["C"]["median_seconds"] / summary["E'"]["median_seconds"]
|
||||
summary["interpretation"] = ("Measured comparative differences under GPU variability; "
|
||||
"not exact causal decompositions or performance claims.")
|
||||
return summary
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--source", type=Path, default=r.DEFAULT_SOURCE)
|
||||
p.add_argument("--output", type=Path, required=True)
|
||||
p.add_argument("--base-steps", type=int, default=2000)
|
||||
p.add_argument("--warmup-base-steps", type=int, default=200)
|
||||
p.add_argument("--repeats", type=int, default=3)
|
||||
p.add_argument("--max-wall-seconds", type=float, help="Required outer timeout for GPU runs")
|
||||
p.add_argument("--plan", action="store_true")
|
||||
a = p.parse_args()
|
||||
if a.repeats < 1 or a.base_steps > 20000:
|
||||
p.error("repeats must be >= 1 and base-steps <= 20000")
|
||||
a.source = a.source.resolve()
|
||||
manifest = r.verify_source(a.source, "MRT")
|
||||
sched = schedule(a.base_steps, a.warmup_base_steps)
|
||||
plan = dict(collision="MRT", manifest_commit=manifest["commit"], schedule=sched,
|
||||
repeats=a.repeats, variants=VARIANTS, timer="time.perf_counter wall",
|
||||
excluded="construction/JIT, warm-up, state download and parity checks")
|
||||
if a.plan:
|
||||
print(json.dumps(plan, indent=2))
|
||||
return 0
|
||||
if a.max_wall_seconds is None or not math.isfinite(a.max_wall_seconds) or a.max_wall_seconds <= 0:
|
||||
p.error("GPU timing requires a finite positive --max-wall-seconds")
|
||||
a.deadline = time.monotonic() + a.max_wall_seconds
|
||||
if not os.environ.get("CUDA_VISIBLE_DEVICES") or "," in os.environ["CUDA_VISIBLE_DEVICES"]:
|
||||
p.error("Root must select exactly one verified idle GPU via CUDA_VISIBLE_DEVICES")
|
||||
if a.output.exists() and any(a.output.iterdir()):
|
||||
p.error("Output must be empty/new")
|
||||
a.output.mkdir(parents=True, exist_ok=True)
|
||||
h, _ = r.helpers(a.source)
|
||||
rows = time_uniform(a, h, sched) + time_refined(a, h, sched)
|
||||
result = dict(plan=plan, rows=rows, summary=summarize(rows, sched["tstar_window"]))
|
||||
if not all(math.isfinite(row["seconds"]) for row in rows):
|
||||
raise RuntimeError("Nonfinite timing")
|
||||
(a.output / "timing.json").write_text(json.dumps(result, indent=2))
|
||||
print(json.dumps(result["summary"], indent=2), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,183 @@
|
||||
"""Measurement-only A-versus-window timing for the matched shifted-MRT refined DFG setup.
|
||||
|
||||
Candidate source (window ledger) is recorded by checkout revision, dirty flag and file hashes;
|
||||
the frozen refined-DFG manifests/results are NOT used or modified (they pin pre-window source).
|
||||
Equal simulated duration, same fresh state and warm-up per variant, one observation interval =
|
||||
80 root base steps (the runner's dense-phase interval):
|
||||
A per base step: step(1) + read_raw_exchange, summed per interval (current runner workload)
|
||||
W per interval: start_exchange_window(); step(80); read_exchange_window()
|
||||
W1 per interval: start_exchange_window(); 80 x step(1); read_exchange_window()
|
||||
All variants keep observe_raw_exchange=True; the backend is built once with
|
||||
observe_window_exchange=True (a closed window adds no launch). Populations (bitwise) and
|
||||
per-interval total impulses are compared OUTSIDE timing; the impulse tolerance is a
|
||||
conservative engineering tolerance (see INTERVAL_RTOL); the tighter substep-scaled engineering
|
||||
tolerance is exercised by the capability test tests/validation/multigrid/test_exchange_window.py.
|
||||
Wall time (perf_counter) with explicit stream synchronization is the only timing source.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
import time_refined_dfg as t # noqa: E402 (reuses run_refined_dfg setup via t.r)
|
||||
|
||||
r = t.r
|
||||
INTERVAL = 80
|
||||
VARIANTS = ("A", "W", "W1")
|
||||
# Interval impulses differ only by FP64 summation order (per link over time vs per step over
|
||||
# links). Engineering tolerance: 1e-9 of the interval's summed absolute per-step totals.
|
||||
INTERVAL_RTOL = 1e-9
|
||||
CANDIDATE_FILES = ("src/CelerisLab/lbm/refinement_stepper.py", "src/CelerisLab/lbm/raw_exchange.py",
|
||||
"src/CelerisLab/lbm/refinement_tables.py", "src/CelerisLab/lbm/kernels/blocks.cu")
|
||||
|
||||
|
||||
def components(obs):
|
||||
o = obs["total"]
|
||||
return np.array([o["impulse"][0], o["impulse"][1], o["angular_impulse"], o["mass_into_fluid"]])
|
||||
|
||||
|
||||
def candidate_identity(source):
|
||||
from source_provenance import checkout_provenance
|
||||
return dict(checkout_provenance(source),
|
||||
sha256={f: hashlib.sha256((source / f).read_bytes()).hexdigest()
|
||||
for f in CANDIDATE_FILES})
|
||||
|
||||
|
||||
def run_variant(backend, variant, n_intervals, deadline):
|
||||
"""Returns per-interval totals and, for A, per-interval summed |per-step totals|."""
|
||||
totals, scales = [], []
|
||||
for _ in range(n_intervals):
|
||||
if variant == "A":
|
||||
impulse, scale = np.zeros(4), np.zeros(4)
|
||||
for _ in range(INTERVAL):
|
||||
backend.step(1)
|
||||
c = components(backend.read_raw_exchange(0, level=1, window="last_base_step"))
|
||||
impulse += c
|
||||
scale += np.abs(c)
|
||||
totals.append(impulse)
|
||||
scales.append(scale)
|
||||
else:
|
||||
backend.start_exchange_window()
|
||||
if variant == "W":
|
||||
backend.step(INTERVAL)
|
||||
else:
|
||||
for _ in range(INTERVAL):
|
||||
backend.step(1)
|
||||
totals.append(components(backend.read_exchange_window(0)))
|
||||
t.check_deadline(deadline)
|
||||
return np.array(totals), (np.array(scales) if scales else None)
|
||||
|
||||
|
||||
def compare_intervals(reference, scales, totals):
|
||||
"""Max violation ratio; <= 1 passes. Scale includes a floor so zero cancels cannot divide."""
|
||||
bound = INTERVAL_RTOL * scales + 1e-300
|
||||
return float(np.max(np.abs(totals - reference) / bound))
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--source", type=Path, default=r.DEFAULT_SOURCE)
|
||||
p.add_argument("--output", type=Path, required=True)
|
||||
p.add_argument("--base-steps", type=int, default=2000)
|
||||
p.add_argument("--warmup-base-steps", type=int, default=200)
|
||||
p.add_argument("--repeats", type=int, default=3)
|
||||
p.add_argument("--max-wall-seconds", type=float)
|
||||
p.add_argument("--plan", action="store_true")
|
||||
a = p.parse_args()
|
||||
if a.base_steps <= 0 or a.base_steps % INTERVAL or a.base_steps > 20000 or a.repeats < 1:
|
||||
p.error(f"base-steps must be a positive multiple of {INTERVAL} (<= 20000); repeats >= 1")
|
||||
a.source = a.source.resolve()
|
||||
identity = candidate_identity(a.source)
|
||||
plan = dict(collision="MRT", shifted_storage="as run_refined_dfg MRT config",
|
||||
candidate_source=identity, frozen_manifests_used=False, interval_base_steps=INTERVAL,
|
||||
base_steps=a.base_steps, warmup_base_steps=a.warmup_base_steps, repeats=a.repeats,
|
||||
variants=VARIANTS, tstar_window=a.base_steps * r.SPEED / 40.0,
|
||||
timer="time.perf_counter wall with explicit stream sync",
|
||||
excluded="construction/JIT, warm-up, health, downloads, parity/impulse comparison")
|
||||
if a.plan:
|
||||
print(json.dumps(plan, indent=2))
|
||||
return 0
|
||||
if a.max_wall_seconds is None or not math.isfinite(a.max_wall_seconds) or a.max_wall_seconds <= 0:
|
||||
p.error("GPU timing requires a finite positive --max-wall-seconds")
|
||||
if not os.environ.get("CUDA_VISIBLE_DEVICES") or "," in os.environ["CUDA_VISIBLE_DEVICES"]:
|
||||
p.error("Root must select exactly one verified idle GPU via CUDA_VISIBLE_DEVICES")
|
||||
if a.output.exists() and any(a.output.iterdir()):
|
||||
p.error("Output must be empty/new")
|
||||
a.output.mkdir(parents=True, exist_ok=True)
|
||||
deadline = time.monotonic() + a.max_wall_seconds
|
||||
h, _ = r.helpers(a.source)
|
||||
|
||||
from contextlib import ExitStack
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
from CelerisLab.lbm.channel import channel_masks
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
from CelerisLab.lbm.descriptors import SOLID
|
||||
|
||||
rows, ref = [], {}
|
||||
n_intervals = a.base_steps // INTERVAL
|
||||
with ExitStack() as stack:
|
||||
cfg, pair, body, host = r.host_geometry(a.source, h, "MRT")
|
||||
context = stack.enter_context(CudaContext(0))
|
||||
backend = stack.enter_context(FixedPatchStepper(
|
||||
cfg, r.BOUNDS, context=context, coarse_boundary="channel", base_block=r.BASE_BLOCK,
|
||||
fine_block=r.FINE_BLOCK, fine_objects=(body,), observe_raw_exchange=True,
|
||||
observe_window_exchange=True))
|
||||
masks = [channel_masks(pair.coarse_shape)[0] & ~pair.covered,
|
||||
(pair.fine_kind == ACTIVE) & ((host.flags.reshape(pair.fine_shape) & SOLID) == 0)]
|
||||
fresh = (r.initial_state(pair.coarse_shape, (0.0, 0.0), 1.0, root=True),
|
||||
r.initial_state(pair.fine_shape, pair.fine_origin, 0.5))
|
||||
print(json.dumps(dict(status="CONSTRUCTED", case="refined")), flush=True)
|
||||
for repeat in range(a.repeats):
|
||||
for variant in VARIANTS:
|
||||
t.check_deadline(deadline)
|
||||
backend.initialize(*fresh)
|
||||
for _ in range(a.warmup_base_steps):
|
||||
backend.step(1)
|
||||
warm = t.refined_health(backend, masks)
|
||||
if repeat == 0 and variant == "A":
|
||||
print(json.dumps(dict(status="WARM_READY", case="refined")), flush=True)
|
||||
t.sync_refined(backend)
|
||||
start = time.perf_counter()
|
||||
totals, scales = run_variant(backend, variant, n_intervals, deadline)
|
||||
t.sync_refined(backend)
|
||||
seconds = time.perf_counter() - start
|
||||
end = t.refined_health(backend, masks)
|
||||
values = [np.array(v, copy=True) for v in backend._download_raw().values]
|
||||
if variant == "A" and repeat == 0:
|
||||
ref.update(values=values, totals=totals, scales=scales)
|
||||
parity, ratio = "identical", 0.0
|
||||
else:
|
||||
parity = t.same_state(ref["values"], values) or "identical"
|
||||
ratio = compare_intervals(ref["totals"], ref["scales"], totals)
|
||||
rows.append(dict(variant=variant, repeat=repeat, seconds=seconds,
|
||||
intervals=n_intervals, population_parity_vs_A=parity,
|
||||
interval_impulse_violation_ratio=ratio,
|
||||
health_after_warmup=warm, health_after_window=end))
|
||||
if parity != "identical" or not ratio <= 1:
|
||||
raise RuntimeError(f"{variant} repeat {repeat}: parity {parity}, impulse ratio {ratio}")
|
||||
summary = {v: dict(median_seconds=statistics.median(x["seconds"] for x in rows if x["variant"] == v),
|
||||
seconds=[x["seconds"] for x in rows if x["variant"] == v]) for v in VARIANTS}
|
||||
summary.update(W_over_A=summary["W"]["median_seconds"] / summary["A"]["median_seconds"],
|
||||
W1_over_A=summary["W1"]["median_seconds"] / summary["A"]["median_seconds"],
|
||||
interpretation="Measured comparative timings under GPU variability; W also "
|
||||
"batches step calls, W1 isolates the read pattern. Not a "
|
||||
"benchmark-throughput or causal claim.")
|
||||
(a.output / "window_timing.json").write_text(json.dumps(dict(plan=plan, rows=rows, summary=summary),
|
||||
indent=2))
|
||||
print(json.dumps(summary, indent=2), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user