Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
519473a61c | ||
|
|
1bbe4b46a3 | ||
|
|
43c527b37c |
@@ -1,62 +0,0 @@
|
||||
# Selective LES normalization integration on dev8403956
|
||||
|
||||
This change reuses the reviewed correction from 5465832 without merging its
|
||||
branch history. The only production change is the two closed-form coefficients
|
||||
18 -> 18 sqrt(2) and their explanatory comments in turbulence_smag.cuh. Its SHA256
|
||||
is a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa.
|
||||
Defaults, iterative/deviatoric/capped closure, collision dispatch, AMR guards,
|
||||
and all other production source files are unchanged.
|
||||
|
||||
## Separate source contract; unchanged experiment
|
||||
|
||||
The single existing flow runner now accepts `--experiment dev8403956`.
|
||||
Its separate source_pins_dev8403956.json pins all 101 production files from the
|
||||
committed 8403956 baseline; corrected mode permits only the exact reviewed
|
||||
header substitution. Results record the actual checkout commit, import path,
|
||||
experiment name and manifest SHA256. No arbitrary manifest-path override exists.
|
||||
|
||||
Omitting the option still selects the original historical source manifest.
|
||||
That manifest, the original paired receipts, scalar preparation manifests and
|
||||
independent continuum oracle are unchanged. Historical baseline and corrected
|
||||
receipts remain evidence for their own exact sources, not new dev receipts.
|
||||
Historical guards intentionally reject this different dev source tree. Eight
|
||||
production files already differed between the historical source snapshot and
|
||||
8403956; neither the old manifest nor its acceptance is silently regenerated.
|
||||
|
||||
The physical experiment and every numerical acceptance threshold are unchanged:
|
||||
8 x 34 nodes, halfway y walls, periodic x, unforced D2Q9/SRT, shifted FP32,
|
||||
double-buffer streaming, Cs=0/.16/1, checkpoints 519/1038/2075. Each case has one
|
||||
actual advancing warm-up followed by complete initial-state restoration before
|
||||
READY. Total launches are 6,225 measured plus three warm-ups. Cs=1 remains an
|
||||
amplified diagnostic, not a physical coefficient recommendation.
|
||||
|
||||
## Host checks
|
||||
|
||||
PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s tests/unit -p 'test_smagorinsky_normalization.py' -v
|
||||
PYTHONDONTWRITEBYTECODE=1 python -m unittest discover -s tests/unit -p 'test_les_flow_*.py' -v
|
||||
|
||||
The direct-current-header test is copied unchanged from reviewed 5465832:
|
||||
120 tensor parameter cases, Cs=0/equilibrium and omega guards, converged
|
||||
trace-free iterative comparison, and mutation detection in both lattice
|
||||
algebras. This is scalar D2/D3 algebra coverage, not 3D-flow qualification.
|
||||
Flow tests retain analytic/reference checks and add current named-source,
|
||||
reverse-patched baseline, variant rejection, unrelated-source tampering and
|
||||
immutable historical-default checks. The temporary baseline is constructed
|
||||
only inside a test directory; production is never reverse-patched.
|
||||
|
||||
## Pending final-commit GPU gate
|
||||
|
||||
After independent review and explicit executor authorization, run from the
|
||||
exact committed candidate checkout, selecting the authorized physical GPU:
|
||||
|
||||
CUDA_VISIBLE_DEVICES=<physical-GPU> PYTHONDONTWRITEBYTECODE=1 python tests/validation/run_uniform_les_flow.py --root "$PWD" --experiment dev8403956 --variant corrected --run-gpu --device 0 --output <new-evidence-directory>/corrected.json
|
||||
|
||||
Retain all source hashes, exact commit/import/device identity, manifest hash,
|
||||
three READY/reset records, complete result JSON, stdout/stderr and actual exit.
|
||||
Require all frozen gates, DONE passed=true and exit 0. Compare with historical
|
||||
corrected evidence without relabeling it as current evidence. The old-vs-new
|
||||
normalization discrimination already exists; no new baseline GPU run is needed
|
||||
unless review finds a changed exercised uniform solver path.
|
||||
|
||||
No GPU result is claimed by this integration preparation. This does not expand
|
||||
LES qualification to MRT, forcing, AMR LES or three-dimensional turbulent flows.
|
||||
@@ -1,34 +0,0 @@
|
||||
# MRT reuse in blocked/refined stepping (candidate)
|
||||
|
||||
Base `9805383`. Opt-in by `LBMConfig(collision="MRT")`; SRT remains the default and its
|
||||
kernel statements, including the shifted retained-delta/direct-momentum path, are unchanged
|
||||
under the alternate compile branch.
|
||||
|
||||
## What changes
|
||||
- `blocks.cu` (`BlockStep`), `step/block_channel.cu` (`BlockChannelStep`) and `refinement.cu`
|
||||
(`patch_collide`) gain a compile-time `COLLISION_MODEL == 2` branch that repeats the uniform
|
||||
`collide_dispatch` sequence with zero forcing: `compute_rho_u`, omega clamp to
|
||||
`[OMEGA_COLLISION_MIN, OMEGA_COLLISION_MAX]`, `collide_mrt_no_force`.
|
||||
- Kernel and host guards accept SRT or MRT only. TRT, LES, non-FP32 storage, non-double-buffer
|
||||
streaming, and shifted dense/mixed refinement remain rejected.
|
||||
- Transfers, subcycling, BFL, raw exchange ledgers and channel closures are untouched. The
|
||||
existing pressure-NEQ outlet selects its MRT branch (unknowns-only NEQ extrapolation) from
|
||||
`COLLISION_MODEL`, so MRT channel parity must use uniform MRT as reference.
|
||||
|
||||
## Contracts (existing operator, not redefined here)
|
||||
- Shifted storage uses the existing MRT representation (equilibrium moments from `rho - 1`),
|
||||
not the SRT retained-delta/direct-momentum precision. Mass and momentum are conserved
|
||||
moments (relaxation rate 0).
|
||||
- Kinetic rates are the operator's fixed `s_e = s_eps = s_q = 1.2` on every level; the shear
|
||||
rate is the existing per-level omega. Under acoustic 2:1 scaling the physical bulk viscosity
|
||||
therefore halves on each finer level. No rate rescaling is applied.
|
||||
- No LES, no external forcing (`d_params` force fields stay zero).
|
||||
- Shifted Jets stay SRT-only: `body/jets.py` keeps its existing guard. This extension does not
|
||||
provide shifted MRT Jets.
|
||||
- `PeriodicBlockStepper` (uniform blocked stepper) 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,62 @@
|
||||
# Projected explosion: isolated spatial-interpolation experiment
|
||||
|
||||
Historical baseline: `9805383f9c50d867e8555471baf5dbda8333a652`. This candidate does not replace or relabel its results. The TG runner records the actual candidate revision, dirty state, source hash and harness hashes, plus the historical baseline revision and selected explosion scheme.
|
||||
|
||||
Default remains `constant`. Explicit `explosion="projected_linear"` supports only a periodic, body-free, two-level, fully blocked single rectangle (window or singleton union representation). Dense, multilevel, multi-rectangle and body/Jet cases are rejected. Constructor guards have been statically reviewed; GPU construction is not yet exercised.
|
||||
|
||||
For each population, form a centered gradient only where both axis neighbors are original explosion donors, owned and uncovered. Otherwise set that axis derivative to zero. Project the gradient perpendicular to the population lattice direction (rest population retains its gradient), then evaluate at child offsets ±1/4 coarse cell. Read owner cores, never stale block halos. Repeating the donor for a missing axis avoids invalid reads. All neighbor reads remain within the original coarse transfer footprint.
|
||||
|
||||
Timing, two ghost-only streaming hops, four-child coalescence, relaxation, shifted-storage convention, bulk collision and legacy constant kernel are unchanged. There is no NEQ rescaling, limiter, time interpolation or new framework. Projection commutes with subtracting the constant lattice weights.
|
||||
|
||||
Method attribution: Schornbaum and Rüde, [arXiv:1508.07982](https://arxiv.org/pdf/1508.07982), Algorithm 2; Chen et al., Physica A 362 (2006) 158–167, [DOI 10.1016/j.physa.2005.09.036](https://doi.org/10.1016/j.physa.2005.09.036). Independently implemented mathematical stencil, with no GPL implementation copied. The Chen full text was not directly audited in this experiment.
|
||||
|
||||
## CPU checks
|
||||
|
||||
Run `PYTHONPATH=src python -m unittest discover -s tests/unit -p test_projected_explosion.py -v`.
|
||||
|
||||
- Constant populations, affine available-axis gradients, characteristic constancy, quartet means and shift invariance.
|
||||
- Original donor footprint only; poison covered cells and check finite reconstruction.
|
||||
- Both window and singleton-union layouts, centered/off-axis/minimal near-edge rectangles.
|
||||
- Eight complete intervals, identity and SRT collision: actual fine pulls and coarse restriction tables, all eight grazing corner paths included; composite mass and both momentum components conserved within 2e-12 FP64 absolute tolerance.
|
||||
- Actual CUDA kernel body compiled with a CPU C++ shim: blocked address mapping, FP32 shifted/unshifted inputs, poisoned halo/covered storage, independent NumPy parity. This checks arithmetic and indexing, not CUDA compilation or execution.
|
||||
- Unprojected reconstruction is distinguished by failure of characteristic constancy. It need not fail global mass conservation in this geometry.
|
||||
|
||||
## Next diagnostic, only on authorized Horsea GPU
|
||||
|
||||
Use the same candidate checkout for paired Re100 N64 depth2 runs, differing only in `--explosion constant` versus `--explosion projected_linear`:
|
||||
|
||||
`python tests/validation/taylor_green/run_case.py --repo "$PWD" --re 100 --n 64 --depth 2 --explosion projected_linear --output <new-result-path>`
|
||||
|
||||
Select the physical CUDA device explicitly. Never overwrite historical reports. Compare frozen TG error gates and full trajectories, including mass, energy, pressure and velocity. Amplitude/nonmodal shape decomposition can inform mechanism but cannot substitute for the gates. First establish CUDA compilation and CPU/device parity, then paired end-to-end budgets. The CPU diagnostic below establishes improvement only in its FP64 reference trajectories; it does not establish production suitability or a candidate GPU pass.
|
||||
|
||||
## Local verification limits
|
||||
|
||||
Focused candidate/refinement/block-layout/build-contract checks pass (62 unittest tests). Full unittest discovery is not green: the attempted 200-test run reports 18 missing-dependency/import errors and three historical source-pin refusals (jet-circle and LES guards), plus one skipped test. Those pin refusals correctly reject the changed `blocks.cu`; their immutable fixtures were not weakened or rewritten. Python syntax checks and `git diff --check` pass. No candidate CUDA build, device execution, GPU performance or GPU TG improvement has been established.
|
||||
|
||||
|
||||
## Donor-support audit
|
||||
|
||||
The current support is exactly the original two-coarse-cell-wide explosion ring. Centered normal derivatives therefore vanish along straight faces: the inner donor lacks an uncovered inward neighbor, and the outer donor lacks a third-ring outward neighbor. This is an explicit limitation, not evidence of a missing donor layer. Independent inspection of the authors' [LinearExplosion implementation](https://github.com/lssfau/walberla/blob/master/src/lbm/refinement/LinearExplosion.h) found the same two-cell directional slabs and both-neighbors-valid condition; their union equals all 368 distinct donors in the N64 TG geometry. No upstream code was copied. This does not claim identical block-local tangential fallback at every fine/fine block seam.
|
||||
|
||||
A separate read-only dependency trace compared the ring to all uncovered periodic parent neighbors for four rectangles. Wider support changed some unused outer ghost populations but none consumed by final fine ACTIVE outputs or coarse restriction through the two hops, including full collision dependencies. Widening the support is not part of this candidate.
|
||||
|
||||
## Reproducible CPU Taylor–Green A/B
|
||||
|
||||
Run from this checkout, with NumPy and Python installed:
|
||||
|
||||
`python tests/validation/taylor_green/run_cpu_explosion.py --repo "$PWD" --n 32 --output-dir /tmp/explosion-ab`
|
||||
|
||||
Repeat with `--n 64` for the second retained case. These are the only supported grids. The script reuses unchanged CPUREF `advance_coarse_once`, existing TG initialization/metrics, actual singleton-rectangle transfer tables, and the tested projected reconstruction. The rectangular adapter verifies its coalescence children against the production table and includes all eight grazing-corner links. Only the explosion operation differs within each pair.
|
||||
|
||||
The reference stores physical FP64 arrival populations; production stores shifted FP32 postcollision populations. Collision preserves the macroscopic moments compared here, and initialization is equilibrium. This supports a macro-trajectory comparison, not population-by-population/device equivalence. Outputs record actual checkout/harness provenance and hashes of both imported diagnostic helpers. The compact retained `tests/validation/taylor_green/cpu_explosion_summary.json` records original N32/N64 results and historical GPU report hashes, without importing those reports into this branch.
|
||||
|
||||
At Re100, depth 2, t*=0.48:
|
||||
|
||||
| Root N | Velocity L2, constant → projected | Pressure L2 | Orthogonal velocity residual |
|
||||
|---|---|---|---|
|
||||
| 32 | 3.24942% → 1.52264% | 8.98696% → 4.85601% | 2.28814% → 0.98615% |
|
||||
| 64 | 1.13113% → 0.49304% | 3.55050% → 1.65801% | 0.93699% → 0.40289% |
|
||||
|
||||
Across sampled times, constant CPU versus historical constant GPU differs by at most 6.93e-8 in velocity L2 and 3.58e-6 in pressure L2 for N32; 1.74e-8 and 8.64e-7 for N64. Relative mass drift remains below 1.42e-13 and absolute momentum change below 2.11e-13 in these CPU pairs. Independent N32 rerunning reproduced trajectory/error metrics exactly; recomputed quadrature-factor metadata agrees to roundoff.
|
||||
|
||||
Amplitude is the volume-weighted projection onto the exact velocity field; the orthogonal residual isolates the remaining shape error. The energy decomposition includes the discrete composite-grid exact-energy quadrature factor recorded per row, not simply amplitude squared plus residual squared. These are bounded diagnostic improvements, not proof of complete method fidelity, uniform-grid accuracy equivalence or a validated CUDA implementation. Frozen production validation gates are unchanged.
|
||||
@@ -1,53 +0,0 @@
|
||||
# Bounded 2D implementation and validation scope
|
||||
|
||||
This inventory starts from dev `8403956a9afc0b0bb89265cec374392013d89016` and
|
||||
integrates only the reviewed LES normalization production correction from
|
||||
`95cb6d2b9f43c1bdd46cbf27f03f0dfc0877cfb0`. The merged production bytes match that
|
||||
tested candidate exactly; the scope-test changes are retained from `53fad767`.
|
||||
GPU receipts remain associated with their exact tested commits, not relabeled as
|
||||
a new merged-commit run. Historical source identities and gates remain unchanged.
|
||||
|
||||
## Implementation boundaries
|
||||
|
||||
- Uniform D2Q9 supports SRT, TRT and MRT. An available collision selector does not
|
||||
qualify every wall, inlet/outlet, forcing, storage or LES combination.
|
||||
- Fixed refinement supports SRT/MRT, FP32 and double-buffer streaming; TRT and LES
|
||||
are rejected. Shifted refinement requires all-blocked storage. Refined channel
|
||||
boundaries have narrower configuration guards than uniform boundaries.
|
||||
- Uniform checkpoints support compatible same-method continuation. Changing
|
||||
collision or LES parameters is a different evolution, not exact continuation.
|
||||
Jet checkpoints and refined checkpoints are unsupported; synchronized remapping
|
||||
is not persistent restart.
|
||||
- Keep these conclusions 2D. Local D3Q19 operator tests do not establish a supported
|
||||
3D Simulation runtime.
|
||||
|
||||
## Evidence and remaining gaps
|
||||
|
||||
| Area | Available evidence | Boundary of the conclusion |
|
||||
|---|---|---|
|
||||
| MRT cylinder flow and observation windows | Full matched uniform/refined MRT DFG and the window-ledger campaign passed their frozen periodicity, reference and matched-comparison gates. The window campaign used `185a919`; its MRT/no-LES numerical path remains unchanged. Current dev also contains independently scoped test/documentation changes and the LES correction, which do not extend MRT qualification. | This does not qualify other collision/boundary combinations or every refinement interface. Timing improvements are not additional accuracy evidence. |
|
||||
| Uniform forced channel | Six Poiseuille GPU cases on `8403956`: SRT/TRT/MRT at wall-normal resolutions H16/H32 passed. | This is the tested analytic channel configuration, not open-boundary or all-layout qualification. |
|
||||
| Uniform restart | Ten SRT/MRT checkpoint GPU tests passed on `8403956`; the separate representative TRT same-method double-buffer continuation GPU case also passed (one test). | This covers the tested same-method continuation configurations. No LES, Jet or refined checkpoint claim follows. |
|
||||
| Taylor–Green refinement | Independent topology checks are available. The actual-rate phase GPU regression passed one test across two layouts, with errors 1.584e-7 and 4.500e-9. | The physical interface-accuracy gap remains open. Passing topology and phase checks does not establish convergence or close that accuracy gate. |
|
||||
| Porous Couette and annulus | Prior campaign evidence exists and must remain associated with its original setup and source. | The reconstructed annulus is a validation helper, not qualification of a public boundary-condition API. Do not infer absence of evidence from a missing current rerun. |
|
||||
| LES normalization | The reviewed correction is integrated. Exact `95cb6d2` GPU validation passed all nine observations and frozen gates, with three warm-up/reset records, verified 101-source/import identity, DONE and exit 0. Last two SGS decrement relative errors were 6.276e-4 and 6.124e-5 against the unchanged 0.15 gate. | Scope is unforced uniform D2Q9/SRT, shifted FP32, double-buffer streaming and the tested wall-bounded x-periodic shear. No TRT/MRT LES, forcing, EsoPull, objects, refinement or 3D turbulence qualification follows. |
|
||||
|
||||
Historical uniform SRT cylinder-startup and inlet-rounding findings remain separate
|
||||
from the passed MRT DFG result. Raw exchange/window consistency is a measurement
|
||||
contract, not a general stress or force-accuracy certification. Existing source-pinned
|
||||
runners may intentionally reject a newer checkout; never silently repin old receipts.
|
||||
|
||||
## Remaining work
|
||||
|
||||
The related host checks passed (67 checks); they do not replace the GPU or
|
||||
physical-accuracy evidence above. Preserve the existing SRT checkpoint cases,
|
||||
historical LES manifests and all frozen numerical criteria.
|
||||
|
||||
Continue the TG accuracy investigation using its actual fine-level rates and
|
||||
the established analytic convention. Keep the physical accuracy gate open until
|
||||
the relevant comparison passes. Review this integration independently before
|
||||
publication; no new merged-commit GPU run is claimed here.
|
||||
|
||||
No repeat of the accepted full MRT DFG or Cartesian expansion across unsupported
|
||||
combinations is required by this document. Main integration remains a separate
|
||||
review of complete, bounded feature scope.
|
||||
@@ -1,71 +0,0 @@
|
||||
# Matched MRT DFG pilot preparation
|
||||
|
||||
Preparation only: this test-only extension reuses the existing runner and existing MRT
|
||||
operator. GPU admission awaits reviewed MRT capability results; no pilot/full execution
|
||||
is implied by CPU preflight. No production, historical SRT manifest, reference data,
|
||||
periodic gate, geometry, initialization or observation algorithm is changed here.
|
||||
|
||||
## Selection and provenance
|
||||
|
||||
Use `--collision MRT` for **both** uniform D80 and refined root D40/fine D80. There are
|
||||
no independently selectable collision models per case. Default remains SRT.
|
||||
|
||||
MRT verifies `data/refined_dfg/source_manifest_mrt.json`, pinned to reviewed numerical
|
||||
source snapshot `3ea131e92c057843034f11a1c574301a2430dfce`, including the shared geometry,
|
||||
pressure/cycle helpers and official reference. Test-only commits may change HEAD without
|
||||
changing this numerical source pin. Plan, READY, case summaries and comparison separately
|
||||
record `checkout_revision` and `checkout_dirty` using existing source-provenance helper;
|
||||
the runner hash remains separate. Collision is recorded in every receipt, and full-mode
|
||||
validation rejects missing or different collision labels in plan, either summary or either
|
||||
READY receipt, in addition to existing source/runner/health/time checks.
|
||||
|
||||
The historical SRT manifest remains byte-for-byte unchanged. SRT on the MRT candidate
|
||||
checkout deliberately fails that source pin; use a matching historical checkout via
|
||||
`--source` for SRT. No fallback or automatic repinning is permitted. Historical SRT
|
||||
failure/diagnostic evidence remains attached to its original source and runner hashes;
|
||||
it cannot authorize an MRT full run. The existing failed-observation step-reporting
|
||||
undercount is not changed in this narrowly scoped selection patch.
|
||||
|
||||
## Frozen physical comparison
|
||||
|
||||
All settings remain those documented in `refined_dfg.md`: U_lattice=.01, physical mean
|
||||
U=1 m/s, D=.1 m, shear ν=.001 m²/s, fixed physical domain/body/wall planes, patch
|
||||
(40,24,320,140), shifted FP32 double buffers, no LES/forcing/Jets, finest dx=.00125 m,
|
||||
finest dt=.0000125 s, unchanged half-cell frame conversion and axis-specific phase.
|
||||
Force interval accumulation, physical leaf mass, pressure interpolation, flux sampling,
|
||||
safety thresholds, t*=5 pilot and t*=300 cap are unchanged. Existing accuracy/periodicity
|
||||
criteria are imported unchanged. Both MRT cases are fresh runs; old MRT or SRT results
|
||||
are not substituted as controls.
|
||||
|
||||
Both cases use the **existing MRT pressure-NEQ branch**: only unknown directions q2/6/8
|
||||
are extrapolated. This differs from SRT's all-nine .5-damped branch; the retained
|
||||
`srt_neq_damp=.5` configuration value is inactive under MRT. Backflow clamp remains off.
|
||||
The outlet is still noncanonical relative to exact DFG do-nothing conditions.
|
||||
|
||||
The reused MRT operator keeps fixed kinetic rates `s_e=s_eps=s_q=1.2` on all levels;
|
||||
only the shear rate follows existing per-level viscosity. Physical bulk relaxation
|
||||
therefore differs between coarse and fine levels. Transfer operators and subcycling
|
||||
are unchanged. This is the candidate's declared behavior, not a new fully mode-scaled
|
||||
MRT transfer scheme. Do not tune rates, the patch or gates after seeing outcomes.
|
||||
Capability smoke/parity gates do not establish DFG accuracy or speedup.
|
||||
|
||||
## CPU and admission commands
|
||||
|
||||
From this checkout:
|
||||
|
||||
```sh
|
||||
python tests/unit/test_refined_dfg.py
|
||||
python tests/validation/run_refined_dfg.py --collision MRT --mode plan --output /tmp/new-mrt-dfg-plan
|
||||
```
|
||||
|
||||
After root verifies the candidate MRT root/parity/refinement capability results and admits
|
||||
an idle device, the paired pilot may use the existing bounds and external watchdog:
|
||||
|
||||
```sh
|
||||
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s <hard-budget>s python tests/validation/run_refined_dfg.py --collision MRT --mode pilot --gpu-gates-approved --max-wall-seconds <soft-budget> --output <new-mrt-pilot-directory>
|
||||
```
|
||||
|
||||
The flag is caller attestation, not evidence. An unhealthy first case prevents the second.
|
||||
Existing strict pilot receipt validation and calibrated wall-time controls remain; full
|
||||
mode is not being admitted as part of this preparation. Root must review both healthy
|
||||
pilot outcomes and separately decide any full-run admission.
|
||||
@@ -1,58 +0,0 @@
|
||||
# Refined DFG MRT timing harness (measurement only)
|
||||
|
||||
`tests/validation/time_refined_dfg.py` times the matched MRT refined/uniform DFG Re100 setup of
|
||||
`run_refined_dfg.py` (same configuration, geometry and initial state functions). No numerical,
|
||||
physics-gate or production change; no force or accuracy result is produced.
|
||||
|
||||
| Variant | Loop | Force reads |
|
||||
|---|---|---|
|
||||
| A | refined `step(1)` + `read_raw_exchange` per base step | runner-like force-read workload (values discarded) |
|
||||
| B | refined `step(1)` only, no host reads (`observe_raw_exchange` stays on) | timing-only |
|
||||
| C | refined single `step(N)` | timing-only; ledger keeps the final base interval only |
|
||||
| E | uniform `sim.run(<=100)` + `read_force` | runner-like force-read workload (values discarded) |
|
||||
| E' | uniform single `sim.run(2N)` | timing-only; at most a whole-window average, different summation order |
|
||||
|
||||
- No variant retains a valid force history; A/E omit the runner's interval aggregation and
|
||||
validation, and no variant includes macro/probe/CSV costs. Results are not full benchmark
|
||||
throughput.
|
||||
- Status lines: `CONSTRUCTED` (construction/JIT complete) and `WARM_READY` (first warm-up and
|
||||
health check passed) per case.
|
||||
- Equal simulated duration: N refined base steps vs 2N uniform D80 steps.
|
||||
- Every repeat starts from the same fresh state (refined `initialize`, uniform `snapshot/restore`)
|
||||
and the same warm-up; construction/JIT, warm-up, downloads and parity checks are untimed.
|
||||
- The actual stepping stream is synchronized explicitly before and after each timed loop; the
|
||||
wall timer (`time.perf_counter`) is the only source. No CUDA-event span is called kernel time.
|
||||
- `--max-wall-seconds` is required for GPU runs (outer timeout, checked between variants and
|
||||
every 100 steps in per-step loops).
|
||||
- Final populations and NaN masks must be bitwise identical to the first A repeat (A/B/C) or the
|
||||
first E repeat (E/E'); refined and uniform grids are never compared. Leaf/fluid populations
|
||||
must be finite. Physical health (finite leaf/fluid macros, rho in [0.5, 1.5], max Mach <= 0.2,
|
||||
the existing runner limits) is checked after warm-up and after each timed window. Matching NaN
|
||||
is accepted only as a storage mask; any infinity or level-count mismatch fails parity. All
|
||||
parity/health checks are untimed.
|
||||
- Cases run sequentially (uniform, then refined) in one process with startup serialized;
|
||||
variants are interleaved within each repeat. A-B and B-C are measured comparative
|
||||
differences under GPU variability, not exact causal decompositions.
|
||||
|
||||
Usage (root selects one verified idle GPU):
|
||||
|
||||
PYTHONPATH=src python -m unittest tests/unit/test_time_refined_dfg.py
|
||||
python tests/validation/time_refined_dfg.py --output <new> --plan
|
||||
CUDA_VISIBLE_DEVICES=<gpu> python tests/validation/time_refined_dfg.py --output <new> --max-wall-seconds <s>
|
||||
|
||||
Defaults: 2000 base steps, 200 warm-up base steps, 3 repeats. Profiler capture and the
|
||||
`observe_raw_exchange=False` variant are deferred until these measurements exist.
|
||||
|
||||
## A versus observation-window ledger (`tests/validation/time_window_dfg.py`)
|
||||
|
||||
Measurement-only comparison on the same shifted-MRT refined setup: A (per-base `step(1)` +
|
||||
`read_raw_exchange`, summed per 80-base-step interval), W (`start_exchange_window`, `step(80)`,
|
||||
one window read) and W1 (window with 80 x `step(1)`, isolating the read pattern from call
|
||||
batching). Same fresh state, warm-up and simulated duration; populations must be bitwise equal
|
||||
to the first A repeat and per-interval total impulses within the documented engineering
|
||||
tolerance, all outside timing. The candidate source is recorded (checkout revision, dirty flag,
|
||||
file hashes); frozen refined-DFG manifests/results are not used or changed.
|
||||
|
||||
PYTHONPATH=src python -m unittest tests/unit/test_time_window_dfg.py
|
||||
python tests/validation/time_window_dfg.py --output <new> --plan
|
||||
CUDA_VISIBLE_DEVICES=<gpu> python tests/validation/time_window_dfg.py --output <new> --max-wall-seconds <s>
|
||||
@@ -1,58 +0,0 @@
|
||||
# Opt-in observation-window ledger in the DFG runner
|
||||
|
||||
This test-only integration adds `--exchange-window` to `run_refined_dfg.py`. It uses the
|
||||
existing production ledger; no collision, boundary, transfer, physical setup, reference,
|
||||
periodicity gate or acceptance threshold changes. Default remains the historical
|
||||
per-base-step observation path. Uniform stepping/force accumulation is unchanged.
|
||||
|
||||
For the refined case, each existing observation interval opens a new ledger at completed
|
||||
base step `begin`, advances exactly the same `n` base intervals, and reads the integrated
|
||||
root-lattice impulse for `[begin, begin+n]`. The contract requires root units, `window`
|
||||
kind, `2*n` fine samples, matching endpoints, and fine dt=.5. The existing coefficient
|
||||
calculation still divides this impulse by `n` and `.5*U_lattice**2*D_root`; force timestamps
|
||||
remain `(begin+n/2)*U_lattice/D_root`. It does not divide an already averaged force again.
|
||||
Pressure/macro/physical-leaf mass measurements and safety checks remain at the original
|
||||
interval end. Pilot interval is 80 root steps; early full-run intervals remain 2000, and
|
||||
any final shortened interval uses its actual `n`.
|
||||
|
||||
The opt-in uses one `step(n)` call and one window read per observation, rather than `n`
|
||||
Python calls/readbacks. Internal subcycling and per-base synchronization remain owned by
|
||||
the same backend. Cooperative deadline checks surround that complete interval; unlike
|
||||
the default path they cannot interrupt between base steps inside `step(n)`. The existing
|
||||
external hard watchdog is still required. The full-run cadence/cap is documented here,
|
||||
but this preparation does not admit a full run or a GPU operation.
|
||||
|
||||
## Provenance and receipt separation
|
||||
|
||||
`source_manifest_window.json` pins the reviewed a233c9032487a6006e950d1e327079b00466c410
|
||||
snapshot (production ledger origin d930) plus existing helper/reference files. Both SRT
|
||||
and MRT are supported by this explicit opt-in source pin; collision selection remains
|
||||
independent and shared by the two cases. Existing SRT and MRT manifests are untouched.
|
||||
Default mode continues to reject the changed candidate under those historical pins;
|
||||
use a matching historical `--source` checkout to run its default path. There is no
|
||||
silent repinning, source fallback, or reuse of historical outputs as candidate evidence.
|
||||
|
||||
Plan, READY, summaries, timeout/comparison receipts now record `observation_mode` as
|
||||
`last_base_step` or `window`, alongside collision and actual checkout provenance. On a
|
||||
uniform receipt this labels its **paired campaign's refined observation mode**, not a
|
||||
change to uniform observation. Full-mode pilot validation rejects missing or mismatched
|
||||
mode labels in the plan, either case summary, and either READY receipt. Source/runner
|
||||
hash, collision, completeness and health checks remain. Cross-mode pilot reuse is denied.
|
||||
|
||||
The earlier short A/W/W1 timing/capability evidence is not a full-run speedup or accuracy
|
||||
claim. Mathematical impulse integration/units are unchanged; floating-point reduction
|
||||
order can differ, so bitwise equality of force sums is not promised. Physics acceptance
|
||||
continues to use the existing frozen gates. Historical failures/results are untouched.
|
||||
|
||||
CPU preparation commands, from the repository root:
|
||||
|
||||
```sh
|
||||
python tests/unit/test_refined_dfg.py
|
||||
PYTHONPATH=src python -m unittest tests.unit.test_raw_exchange_window tests.unit.test_time_window_dfg -v
|
||||
python tests/validation/run_refined_dfg.py --collision MRT --exchange-window --mode plan --output /tmp/new-window-dfg-plan
|
||||
```
|
||||
|
||||
Root must independently review and admit any execution. An admitted paired pilot uses
|
||||
the existing watchdog, device selection, `--gpu-gates-approved`, positive
|
||||
`--max-wall-seconds` and a new output directory, adding only `--exchange-window` to the
|
||||
selected collision's pilot command. No GPU or remote action was performed for this patch.
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Host contract for the blocked root's uniform SRT/MRT channel boundary.
|
||||
"""Host contract for the blocked root's uniform-SRT channel boundary.
|
||||
|
||||
Boundary ghosts are evolved state, but are not physical fluid leaves. Halo
|
||||
ownership remains periodic to reproduce the uniform kernel's wrapped pulls.
|
||||
@@ -29,7 +29,7 @@ def validate_channel_config(cfg, blocks):
|
||||
def validate_channel_transfers(pair, layout, targets):
|
||||
"""Reject actual exchange/closure overlap, not a padded geometric envelope.
|
||||
|
||||
Explosion donors and restriction receivers must use ordinary bulk collision
|
||||
Explosion donors and restriction receivers must use ordinary bulk SRT
|
||||
semantics. Restriction writes receiver-local source slots; these may not
|
||||
overwrite the channel's extra immutable-input reads (pressure donor and
|
||||
same-node halfway-wall corrections). The slot check deliberately respects
|
||||
|
||||
@@ -14,15 +14,12 @@
|
||||
#include "core/obs.cuh"
|
||||
#include "operators/macro.cuh"
|
||||
#include "operators/collision_srt.cuh"
|
||||
#if COLLISION_MODEL == 2
|
||||
#include "operators/collision_mrt.cuh"
|
||||
#endif
|
||||
#include "streaming/pull_double_buffer.cuh"
|
||||
#include "boundary/curved_boundary.cuh"
|
||||
#include "boundary/planar_velocity.cuh"
|
||||
|
||||
#if DIM != 2 || NQ != 9 || (COLLISION_MODEL != 0 && COLLISION_MODEL != 2) || STORE_PRECISION != 0 || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Blocks require FP32 D2Q9 SRT or MRT double buffers without LES"
|
||||
#if DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Blocks require FP32 D2Q9 SRT double buffers without LES"
|
||||
#endif
|
||||
|
||||
// Fixed-patch cell kinds, matching refinement.py ACTIVE/INNER_GHOST/OUTER_GHOST.
|
||||
@@ -63,6 +60,39 @@ extern "C" __global__ void BlockExplode(
|
||||
to[index_f(t % TOTAL_CELLS, q)] = from[q * coarse_cells + d % coarse_cells];
|
||||
}
|
||||
|
||||
// Experimental Schornbaum--Ruede (arXiv:1508.07982, Algorithm 2)
|
||||
// and Chen et al. (doi:10.1016/j.physa.2005.09.036) population interpolation.
|
||||
// Independent implementation of the mathematical stencil; no upstream source copied.
|
||||
// Stencil cells are owner cores; a disabled axis repeats its center twice.
|
||||
extern "C" __global__ void BlockExplodeProjected(
|
||||
const fpxx* coarse, fpxx* fine, const uint64_t* target, const uint64_t* donor,
|
||||
unsigned int count, unsigned long coarse_cells,
|
||||
const uint64_t* neighbors, const float* offsets)
|
||||
{
|
||||
unsigned int row = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
if (row >= count) return;
|
||||
const uint64_t t = target[row], d = donor[row];
|
||||
fpxx* to = fine + (t / TOTAL_CELLS) * NQ * TOTAL_CELLS;
|
||||
for (unsigned int q = 0; q < NQ; ++q) {
|
||||
float v[4];
|
||||
for (unsigned int a = 0; a < 4; ++a) {
|
||||
const uint64_t n = neighbors[4ul * row + a];
|
||||
v[a] = coarse[(n / coarse_cells) * NQ * coarse_cells
|
||||
+ q * coarse_cells + n % coarse_cells];
|
||||
}
|
||||
float gx = .5f * (v[1] - v[0]), gy = .5f * (v[3] - v[2]);
|
||||
const float ex = d_cx[q], ey = d_cy[q];
|
||||
if (q != 0) {
|
||||
const float along = (ex * gx + ey * gy) / (ex * ex + ey * ey);
|
||||
gx -= ex * along;
|
||||
gy -= ey * along;
|
||||
}
|
||||
to[index_f(t % TOTAL_CELLS, q)] =
|
||||
coarse[(d / coarse_cells) * NQ * coarse_cells + q * coarse_cells + d % coarse_cells]
|
||||
+ offsets[2ul * row] * gx + offsets[2ul * row + 1] * gy;
|
||||
}
|
||||
}
|
||||
|
||||
// Targets are receiver-local SOURCE slots for the immediately following pull.
|
||||
// Keep child summation order identical to PatchCoarseStep; no collision here.
|
||||
extern "C" __global__ void BlockCoalesce(
|
||||
@@ -144,13 +174,6 @@ 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
|
||||
@@ -163,7 +186,6 @@ 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,9 +5,7 @@
|
||||
// nu_t = (C_s * Delta)^2 * |S|, Delta = 1 (lattice unit)
|
||||
// omega_eff = 1 / (3*(nu + nu_t) + 0.5)
|
||||
//
|
||||
// |S| = sqrt(2 S_ij S_ij), with Delta = 1 in lattice units.
|
||||
// The stress estimate uses self-consistent tau_eff; tau0 is molecular.
|
||||
// Closed form uses full stress. Iteration uses deviatoric stress and a cap.
|
||||
// |S| is estimated from non-equilibrium second-order moments using tau0.
|
||||
// ============================================================================
|
||||
|
||||
#ifndef CELERIS_OPERATORS_TURBULENCE_SMAG_CUH
|
||||
@@ -67,12 +65,12 @@ __device__ __forceinline__ float compute_omega_smag(const float* __restrict__ f,
|
||||
|
||||
#if LES_CLOSED_FORM
|
||||
// Closed-form Smagorinsky (no iteration):
|
||||
// ω = 2 / (τ₀ + √(τ₀² + 18·√2·Cs²·√Q / ρ))
|
||||
// ω = 2 / (τ₀ + √(τ₀² + 18·Cs²·√Q / ρ))
|
||||
// where Q = Πneq_ij · Πneq_ij (Frobenius norm squared)
|
||||
const float Q = pixx*pixx + piyy*piyy + 2.0f*pixy*pixy;
|
||||
const float sqrtQ = sqrtf(fmaxf(Q, 0.0f));
|
||||
const float smag_const_sq = LES_CS * LES_CS;
|
||||
const float discriminant = tau0*tau0 + 18.0f * sqrtf(2.0f) * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float discriminant = tau0*tau0 + 18.0f * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float tau_eff = 0.5f * (tau0 + sqrtf(fmaxf(discriminant, tau0*tau0)));
|
||||
return clamp_omega(1.0f / fmaxf(tau_eff, 0.500001f));
|
||||
#else
|
||||
@@ -142,7 +140,7 @@ __device__ __forceinline__ float compute_omega_smag(const float* __restrict__ f,
|
||||
+ 2.0f*(pixy*pixy + pixz*pixz + piyz*piyz);
|
||||
const float sqrtQ = sqrtf(fmaxf(Q, 0.0f));
|
||||
const float smag_const_sq = LES_CS * LES_CS;
|
||||
const float discriminant = tau0*tau0 + 18.0f * sqrtf(2.0f) * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float discriminant = tau0*tau0 + 18.0f * smag_const_sq * sqrtQ / rho_safe;
|
||||
const float tau_eff = 0.5f * (tau0 + sqrtf(fmaxf(discriminant, tau0*tau0)));
|
||||
return clamp_omega(1.0f / fmaxf(tau_eff, 0.500001f));
|
||||
#else
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
#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"
|
||||
@@ -22,8 +19,8 @@ extern "C" {
|
||||
#include "step/curved_double.cu"
|
||||
}
|
||||
|
||||
#if DIM != 2 || NQ != 9 || (COLLISION_MODEL != 0 && COLLISION_MODEL != 2) || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Refinement requires unshifted FP32 D2Q9 SRT or MRT double buffers without LES"
|
||||
#if DIM != 2 || NQ != 9 || COLLISION_MODEL != 0 || STORE_PRECISION != 0 || USE_DDF_SHIFTING || USE_LES || STREAMING_MODEL != 0
|
||||
#error "Refinement requires unshifted FP32 D2Q9 SRT double buffers without LES"
|
||||
#endif
|
||||
|
||||
// Enabled only in the validation build; no diagnostic arrays in normal builds.
|
||||
@@ -36,17 +33,10 @@ __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,20 +54,12 @@ 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,26 +7,16 @@ import numpy as np
|
||||
|
||||
|
||||
def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
|
||||
time_end, window, reference_center, duration=None):
|
||||
time_end, window, reference_center):
|
||||
"""Reduce FP64 interval integrals or convert FP32 last-substep raw rows.
|
||||
|
||||
dt_l=h_l under acoustic 2:1 refinement. Local momentum j -> base force
|
||||
h*j -> impulse h*dt*j; torque adds one h; mass is h**2*dm.
|
||||
window="window" reduces an FP64 observation-window ledger spanning
|
||||
``duration`` (integer >= 1) base intervals; rates divide by that duration.
|
||||
"""
|
||||
if window not in ("last_base_step", "last_substep", "window"):
|
||||
if window not in ("last_base_step", "last_substep"):
|
||||
raise ValueError("unknown exchange window")
|
||||
if window == "window":
|
||||
if isinstance(duration, bool) or not isinstance(duration, (int, np.integer)) or duration < 1:
|
||||
raise ValueError("window duration must be an integer number of base intervals >= 1")
|
||||
elif duration is not None:
|
||||
raise ValueError("duration applies only to window='window'")
|
||||
values = np.array(rows, dtype=np.float64, copy=True)
|
||||
if window == "window":
|
||||
duration = float(duration)
|
||||
else:
|
||||
duration = 1.0 if window == "last_base_step" else h
|
||||
duration = 1.0 if window == "last_base_step" else h
|
||||
if window == "last_substep":
|
||||
values *= np.array([h*h, h*h, h*h*h, h*h])
|
||||
selected = np.asarray(body_ids) == body_id
|
||||
@@ -41,7 +31,6 @@ def summarize_exchange(rows, body_ids, aperture, *, body_id, level, h,
|
||||
return dict(wall=reduce(selected & ~aperture), aperture=reduce(selected & aperture),
|
||||
total=reduce(selected), body_id=int(body_id), level=int(level),
|
||||
time_start=time_end-duration, time_end=time_end, dt=h,
|
||||
sample_count=(round(duration/h) if window == "window"
|
||||
else round(1/h) if window == "last_base_step" else 1),
|
||||
sample_count=round(1/h) if window == "last_base_step" else 1,
|
||||
window=window, units="base_lattice_per_unit_depth",
|
||||
definition="raw_boundary_exchange", reference_center=tuple(reference_center), valid=True)
|
||||
|
||||
@@ -52,17 +52,17 @@ class FixedPatch2D:
|
||||
def from_config(cls, cfg: LBMConfig, bounds) -> "FixedPatch2D":
|
||||
"""Prepare maps without changing cfg or allocating coarse-sized tables.
|
||||
|
||||
The backend contract is D2Q9/SRT or MRT/FP32/double_buffer,
|
||||
The initial backend contract is D2Q9/SRT/FP32/double_buffer,
|
||||
without LES. Two coarse cells of margin keep all halo donors in bounds;
|
||||
this does NOT certify that those donors are fluid or periodic.
|
||||
"""
|
||||
checked = replace(cfg)
|
||||
checked.validate()
|
||||
if (not checked.is_d2q9 or checked.collision not in ("SRT", "MRT")
|
||||
if (not checked.is_d2q9 or checked.collision != "SRT"
|
||||
or checked.streaming != "double_buffer"
|
||||
or checked.store_precision != "FP32"
|
||||
or checked.les_enabled):
|
||||
raise ValueError("FixedPatch2D requires D2Q9/SRT or MRT/FP32/double_buffer without LES")
|
||||
raise ValueError("FixedPatch2D requires D2Q9/SRT/FP32/double_buffer without LES")
|
||||
bounds = tuple(bounds)
|
||||
if len(bounds) != 4 or any(isinstance(v, (bool, np.bool_))
|
||||
or not isinstance(v, Integral) for v in bounds):
|
||||
@@ -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 not in ("SRT", "MRT") or checked.streaming != "double_buffer"
|
||||
or checked.collision != "SRT" or checked.streaming != "double_buffer"
|
||||
or checked.store_precision != "FP32" or checked.les_enabled
|
||||
or patch.tau_coarse != 3*checked.viscosity+.5):
|
||||
raise ValueError("Fine boundary requires the matching fixed-patch configuration")
|
||||
|
||||
@@ -22,7 +22,7 @@ from .block_stepper import MAX_GRID_DIM
|
||||
from .hierarchy import build_hierarchy, is_rectangle
|
||||
from .refinement import ACTIVE, HALO, FixedPatch2D, FixedPatchSet2D, prepare_fine_boundary
|
||||
from .refinement_tables import (blocked_base_tables, blocked_fine_tables, # noqa: F401 (re-export)
|
||||
blocked_kind, _prepare_blocked_boundary,
|
||||
blocked_kind, projected_explosion_tables, _prepare_blocked_boundary,
|
||||
BLOCKED_BOUNDARY_DEVICE_COLUMNS)
|
||||
from .descriptors import SOLID
|
||||
from .raw_exchange import summarize_exchange
|
||||
@@ -41,6 +41,7 @@ class _Pair:
|
||||
n_ghosts: int
|
||||
n_rows: int
|
||||
parent_cells: int # parent block window cells (explode donor stride)
|
||||
interpolation: tuple = () # optional neighbor/offset device tables
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -172,9 +173,6 @@ 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.
|
||||
@@ -219,7 +217,7 @@ class FixedPatchStepper:
|
||||
fine_block=None, base_block=None, jets=(), child=None, child_block=None,
|
||||
child_objects=(), child_jets=(), child_rectangles_in_parent=None,
|
||||
refinements=None, level_blocks=None, level_objects=None, level_jets=None,
|
||||
observe_raw_exchange=False, observe_window_exchange=False):
|
||||
observe_raw_exchange=False, explosion="constant"):
|
||||
if HALO != 4:
|
||||
raise RuntimeError("refinement.cu requires a four-cell fine halo")
|
||||
if coarse_boundary not in ("periodic", "slot_reservoir", "channel"):
|
||||
@@ -248,6 +246,15 @@ class FixedPatchStepper:
|
||||
level_blocks = [None if b is None else tuple(b) for b in level_blocks]
|
||||
_check_levels(refinements, level_blocks, level_objects, level_jets)
|
||||
depth = len(refinements) + 1
|
||||
if explosion not in ("constant", "projected_linear"):
|
||||
raise ValueError("explosion must be constant or projected_linear")
|
||||
if explosion == "projected_linear" and (
|
||||
depth != 2 or coarse_boundary != "periodic"
|
||||
or any(b is None for b in level_blocks)
|
||||
or any(level_objects) or any(level_jets)
|
||||
or not (is_rectangle(refinements[0]) or len(refinements[0]) == 1)):
|
||||
raise ValueError("projected explosion requires periodic two-level blocked single rectangle without bodies/Jets")
|
||||
self._explosion = explosion
|
||||
self._ddf_shifting = bool(cfg.ddf_shifting)
|
||||
if self._ddf_shifting and any(b is None for b in level_blocks):
|
||||
raise ValueError("shifted populations require all-blocked levels; dense/mixed is unsupported")
|
||||
@@ -318,13 +325,6 @@ 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)
|
||||
@@ -375,8 +375,6 @@ 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:
|
||||
@@ -479,6 +477,10 @@ class FixedPatchStepper:
|
||||
self._upload(children), len(ghosts), len(targets),
|
||||
int(np.prod(pair.coarse_shape if parent.layout is None
|
||||
else parent.layout.local_shape)))
|
||||
if self._explosion == "projected_linear":
|
||||
neighbors, offsets = projected_explosion_tables(pair, parent.layout)
|
||||
result.explode = child.module.get_function("BlockExplodeProjected")
|
||||
result.interpolation = (self._upload(neighbors), self._upload(offsets))
|
||||
if child.layout is not None:
|
||||
child.kind = self._upload(blocked_kind(child.layout, self.hierarchy.kinds[index + 1]))
|
||||
if index == 0 and base is not None:
|
||||
@@ -571,7 +573,6 @@ 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"
|
||||
@@ -652,8 +653,7 @@ 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_window_exchange=self._observe_window)
|
||||
observe_raw_exchange=self._observe_raw_exchange)
|
||||
try:
|
||||
if self._ddf_shifting:
|
||||
candidate._initialize_raw(_StoredPopulations(transferred.g, transferred.shifted))
|
||||
@@ -745,7 +745,7 @@ class FixedPatchStepper:
|
||||
"""Explode from the given parent buffer, then two hops of level ``index``."""
|
||||
pair, level = self._pairs[index - 1], self._levels[index]
|
||||
pair.explode(parent_buffer, level.current, pair.ghosts, pair.donors, np.uint32(pair.n_ghosts),
|
||||
np.uint64(pair.parent_cells),
|
||||
np.uint64(pair.parent_cells), *pair.interpolation,
|
||||
grid=((pair.n_ghosts + self._block[0] - 1) // self._block[0], 1), **kw)
|
||||
self._hop(index, level.current, level.spare, **kw)
|
||||
self._hop(index, level.spare, level.current, **kw)
|
||||
@@ -785,10 +785,6 @@ 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)
|
||||
|
||||
@@ -822,57 +818,9 @@ 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.
|
||||
|
||||
@@ -951,7 +899,6 @@ 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:
|
||||
|
||||
@@ -66,6 +66,40 @@ def blocked_base_tables(patch, layout, order, wall_aware=False, *, composed_kind
|
||||
blocked_kind(layout, dense_kind))
|
||||
|
||||
|
||||
def projected_explosion_tables(patch, layout):
|
||||
"""Experimental periodic single-rectangle stencil, owner-core cell IDs.
|
||||
|
||||
Schornbaum & Rüde (2016), arXiv:1508.07982, Algorithm 2 explosion
|
||||
interpolation, following Chen et al., Physica A 362 (2006), 158-167,
|
||||
doi:10.1016/j.physa.2005.09.036: centered gradients, perpendicular projection,
|
||||
and child offsets +/-1/4. Independently implemented from the mathematics.
|
||||
Only original explosion donors may supply gradients: this preserves the
|
||||
existing coarse transfer footprint. Missing axis pairs use center twice.
|
||||
Caller must restrict use to two periodic, body-free blocked levels.
|
||||
"""
|
||||
if hasattr(patch, "patches") and len(patch.patches) != 1:
|
||||
raise ValueError("projected explosion requires one rectangle")
|
||||
ny, nx = patch.coarse_shape
|
||||
donor = patch.ghost_coarse.astype(np.intp)
|
||||
legal = np.zeros(ny * nx, bool)
|
||||
legal[donor] = True
|
||||
legal &= layout.owned.ravel() & ~patch.covered.ravel()
|
||||
if not legal[donor].all():
|
||||
raise ValueError("projected explosion donors must be owned uncovered cells")
|
||||
y, x = np.divmod(donor, nx)
|
||||
neighbors = np.repeat(donor[:, None], 4, axis=1)
|
||||
for axis, (dx, dy) in enumerate(((1, 0), (0, 1))):
|
||||
minus = ((y - dy) % ny) * nx + (x - dx) % nx
|
||||
plus = ((y + dy) % ny) * nx + (x + dx) % nx
|
||||
valid = legal[minus] & legal[plus]
|
||||
neighbors[valid, 2 * axis] = minus[valid]
|
||||
neighbors[valid, 2 * axis + 1] = plus[valid]
|
||||
fy, fx = np.divmod(patch.ghost_fine, patch.fine_shape[1])
|
||||
offsets = np.column_stack((fx % 2, fy % 2)).astype(np.float32) * .5 - .25
|
||||
return (np.ascontiguousarray(layout.owner.ravel()[neighbors], dtype=np.uint64),
|
||||
np.ascontiguousarray(offsets))
|
||||
|
||||
|
||||
# Required device columns plus optional actuation; observation arms are uploaded
|
||||
# separately only when enabled. body_id/aperture remain host reduction metadata.
|
||||
BLOCKED_BOUNDARY_DEVICE_COLUMNS = ("block", "cell", "dir", "q", "fallback", "surface_velocity")
|
||||
@@ -85,7 +119,6 @@ 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,14 +65,9 @@ def test_shifted_topology_add_remove_preserves_cache_and_finite_flow(streaming,p
|
||||
assert np.max(macro['rho'][fluid]) < 1.5
|
||||
|
||||
|
||||
@pytest.mark.parametrize('collision,streaming', [
|
||||
pytest.param('SRT', 'double_buffer', id='double_buffer'),
|
||||
pytest.param('SRT', 'esopull', id='esopull'),
|
||||
pytest.param('TRT', 'double_buffer', id='trt-double_buffer'),
|
||||
])
|
||||
def test_checkpoint_exact_continuation_and_controls(collision,streaming,tmp_path):
|
||||
# Same collision throughout save/load/continue; this is not a method-switch restart.
|
||||
with make_sim(nx=48,ny=24,streaming=streaming,shifted=True,collision=collision) as sim:
|
||||
@pytest.mark.parametrize('streaming',['double_buffer','esopull'])
|
||||
def test_checkpoint_exact_continuation_and_controls(streaming,tmp_path):
|
||||
with make_sim(nx=48,ny=24,streaming=streaming,shifted=True) as sim:
|
||||
sim.add_body('circle',center=(14,12),radius=3)
|
||||
sim.add_body('force_region',center=(30,12),radius=2)
|
||||
sim.initialize()
|
||||
|
||||
@@ -41,53 +41,45 @@ class GuardTests(unittest.TestCase):
|
||||
with self.assertRaisesRegex(RuntimeError, "velocity/profile"):
|
||||
decode_macros(invalid, w, cx, cy)
|
||||
|
||||
def test_current_corrected_pins(self):
|
||||
def test_baseline_pins(self):
|
||||
self.assertEqual(
|
||||
source_guard(ROOT, "corrected", "dev8403956")["base_commit"],
|
||||
"8403956a9afc0b0bb89265cec374392013d89016",
|
||||
source_guard(ROOT, "baseline")["base_commit"],
|
||||
"cd3a657f9d3833e812c3cca173525f165b48710a",
|
||||
)
|
||||
|
||||
def test_corrected_and_tamper_guard(self):
|
||||
pins = json.loads((HERE / "data/les_flow/source_pins_dev8403956.json").read_text())
|
||||
pins = json.loads((HERE / "data/les_flow/source_pins.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", "dev8403956")
|
||||
source_guard(root, "corrected")
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "baseline", "dev8403956")
|
||||
source_guard(root, "baseline")
|
||||
target = root / "src/CelerisLab/lbm/kernels/step/one_step_double.cu"
|
||||
target.write_bytes(target.read_bytes() + b"\n// tamper\n")
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "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")
|
||||
source_guard(root, "corrected")
|
||||
|
||||
def test_gpu_requires_opt_in_before_import(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
"""CPU stencil and actual closed-interface packet budgets; no CUDA claims."""
|
||||
import numpy as np
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
||||
from CelerisLab.lbm.refinement import FixedPatch2D, FixedPatchSet2D, ACTIVE, OUTER_GHOST
|
||||
from CelerisLab.lbm.refinement_tables import projected_explosion_tables
|
||||
|
||||
E = np.array([D2Q9_EX, D2Q9_EY]).T
|
||||
W = np.array([4/9] + [1/9]*4 + [1/36]*4)
|
||||
|
||||
|
||||
def setup(union=False, bounds=(4, 4, 12, 12)):
|
||||
cfg = LBMConfig(nx=16, ny=16, viscosity=.1)
|
||||
p = (FixedPatchSet2D.from_config(cfg, [bounds]) if union else
|
||||
FixedPatch2D.from_config(cfg, bounds))
|
||||
layout = BlockLayout2D.build(16, 16, 4, 4, periodic=(True, True))
|
||||
neighbors, offsets = projected_explosion_tables(p, layout)
|
||||
inverse = np.full(layout.nblocks * np.prod(layout.local_shape), -1, dtype=int)
|
||||
inverse[layout.owner.ravel()] = np.arange(256)
|
||||
return p, inverse[neighbors], offsets
|
||||
|
||||
|
||||
def reconstruct(c, p, neighbors, offsets, project=True):
|
||||
# Independent NumPy evaluation of the documented stencil; neighbors have
|
||||
# been decoded from actual blocked owner IDs, not recomputed geometry.
|
||||
values = c.reshape(9, -1)[:, neighbors]
|
||||
g = .5 * (values[:, :, 1::2] - values[:, :, ::2])
|
||||
if project:
|
||||
g[1:] -= (np.einsum('qra,qa->qr', g[1:], E[1:]) /
|
||||
np.sum(E[1:]**2, axis=1)[:, None])[:, :, None] * E[1:, None, :]
|
||||
out = c.reshape(9, -1)[:, p.ghost_coarse] + np.einsum('qra,ra->qr', g, offsets)
|
||||
return out, g
|
||||
|
||||
|
||||
def check_legal_footprint_mean_and_characteristic(union, bounds):
|
||||
p, neighbors, offsets = setup(union, bounds)
|
||||
assert set(neighbors.ravel()) <= set(p.ghost_coarse)
|
||||
assert not p.covered.ravel()[neighbors].any()
|
||||
assert (neighbors >= 0).all()
|
||||
c = np.random.default_rng(72).normal(size=(9,16,16))
|
||||
c[:, p.covered] = np.nan # poison every forbidden donor
|
||||
out, g = reconstruct(c, p, neighbors, offsets)
|
||||
assert np.isfinite(out).all()
|
||||
np.testing.assert_allclose(np.einsum('qra,qa->qr', g, E), 0, atol=5e-16)
|
||||
for d in np.unique(p.ghost_coarse):
|
||||
rows = p.ghost_coarse == d
|
||||
assert rows.sum() == 4
|
||||
np.testing.assert_allclose(out[:, rows].mean(axis=1), c.reshape(9,-1)[:,d], atol=5e-16)
|
||||
for q in range(1,9):
|
||||
delta = offsets[rows, None, :] - offsets[None, rows, :]
|
||||
# Parallel displacements have zero 2D cross product.
|
||||
parallel = np.isclose(delta[...,0]*E[q,1]-delta[...,1]*E[q,0], 0)
|
||||
diff = out[q, rows][:,None]-out[q, rows][None,:]
|
||||
np.testing.assert_allclose(diff[parallel], 0, atol=5e-16)
|
||||
const = np.broadcast_to(W[:,None,None], (9,16,16)).copy()
|
||||
constant, _ = reconstruct(const,p,neighbors,offsets)
|
||||
np.testing.assert_array_equal(constant, np.broadcast_to(W[:,None], constant.shape))
|
||||
|
||||
|
||||
def collide(f, tau):
|
||||
rho = f.sum(axis=0)
|
||||
u = np.einsum('qa,q...->a...', E, f) / rho
|
||||
cu = np.einsum('qa,a...->q...', E, u)
|
||||
eq = W[:,None] * rho * (1 + 3*cu + 4.5*cu**2 - 1.5*np.sum(u*u,axis=0))
|
||||
return f + (eq-f)/tau
|
||||
|
||||
|
||||
def totals(c, f, p):
|
||||
q = c[:,~p.covered].sum(axis=1)+.25*f[:,p.fine_kind==ACTIVE].sum(axis=1)
|
||||
return np.r_[q.sum(), E.T@q]
|
||||
|
||||
|
||||
def check_full_two_hop_closed_corner_budget(union, collision, bounds):
|
||||
p, neighbors, offsets = setup(union,bounds)
|
||||
rng=np.random.default_rng(17)
|
||||
c=W[:,None,None]*(1+rng.normal(0,.002,(9,16,16)))
|
||||
f=W[:,None,None]*(1+rng.normal(0,.002,(9,*p.fine_shape)))
|
||||
start=totals(c,f,p)
|
||||
# Count exactly the eight coarse diagonal links whose source is also
|
||||
# uncovered, but whose midpoint lies on the closed rectangular interface.
|
||||
q, k=np.divmod(p.coalesce_coarse.astype(np.int64),256)
|
||||
y,x=np.divmod(k,16)
|
||||
grazing=~p.covered[y-E[q,1],x-E[q,0]]
|
||||
assert grazing.sum()==8
|
||||
c[:,p.covered]=np.nan
|
||||
for _ in range(8):
|
||||
f.reshape(9,-1)[:,p.ghost_fine]=reconstruct(c,p,neighbors,offsets)[0]
|
||||
for _ in range(2):
|
||||
f=np.array([np.roll(f[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
|
||||
f[:,p.fine_kind==OUTER_GHOST]=np.nan
|
||||
if collision:
|
||||
f[:,p.fine_kind==ACTIVE]=collide(f[:,p.fine_kind==ACTIVE],p.tau_fine)
|
||||
out=np.array([np.roll(c[q],(ey,ex),(0,1)) for q,(ex,ey) in enumerate(E)])
|
||||
packets=f.reshape(-1)[p.coalesce_fine].mean(axis=1)
|
||||
assert np.isfinite(packets[grazing]).all()
|
||||
out.reshape(-1)[p.coalesce_coarse]=packets
|
||||
if collision:out[:,~p.covered]=collide(out[:,~p.covered],p.tau_coarse)
|
||||
out[:,p.covered]=np.nan
|
||||
c=out
|
||||
np.testing.assert_allclose(totals(c,f,p),start,rtol=0,atol=2e-12)
|
||||
|
||||
|
||||
def check_projection_is_distinguished_by_characteristic_not_global_mass():
|
||||
p,n,o=setup()
|
||||
c=np.random.default_rng(8).normal(size=(9,16,16))
|
||||
_,g=reconstruct(c,p,n,o,project=False)
|
||||
assert np.max(abs(np.einsum('qra,qa->qr',g,E))) > .1
|
||||
|
||||
|
||||
def check_multiple_rectangles_rejected():
|
||||
cfg=LBMConfig(nx=24,ny=16,viscosity=.1)
|
||||
p=FixedPatchSet2D.from_config(cfg, [(4,4,8,12),(12,4,16,12)])
|
||||
layout=BlockLayout2D.build(16,24,4,4,periodic=(True,True))
|
||||
with unittest.TestCase().assertRaisesRegex(ValueError, 'one rectangle'):
|
||||
projected_explosion_tables(p,layout)
|
||||
|
||||
|
||||
class ProjectedExplosionTests(unittest.TestCase):
|
||||
def test_stencil(self):
|
||||
for union in (False, True):
|
||||
for bounds in ((4,4,12,12),(2,3,9,10),(2,2,3,3)):
|
||||
with self.subTest(union=union,bounds=bounds):
|
||||
check_legal_footprint_mean_and_characteristic(union,bounds)
|
||||
|
||||
def test_closed_interface(self):
|
||||
for union in (False,True):
|
||||
for collision in (False,True):
|
||||
for bounds in ((4,4,12,12),(2,3,14,12),(2,2,3,3)):
|
||||
with self.subTest(union=union,collision=collision,bounds=bounds):
|
||||
check_full_two_hop_closed_corner_budget(union,collision,bounds)
|
||||
|
||||
def test_projection(self):
|
||||
check_projection_is_distinguished_by_characteristic_not_global_mass()
|
||||
|
||||
def test_affine_and_shift_invariance(self):
|
||||
p,n,o=setup()
|
||||
y,x=np.indices((16,16))
|
||||
c=np.array([.7+q*.01+x*.03+y*.05 for q in range(9)])
|
||||
out,g=reconstruct(c,p,n,o)
|
||||
available=n[:,::2]!=n[:,1::2]
|
||||
gradient=np.array([.03,.05])*available
|
||||
expected=np.broadcast_to(gradient,(9,*gradient.shape)).copy()
|
||||
expected[1:]-=(np.einsum('qra,qa->qr',expected[1:],E[1:])/
|
||||
np.sum(E[1:]**2,axis=1)[:,None])[:,:,None]*E[1:,None,:]
|
||||
np.testing.assert_allclose(g,expected,atol=2e-16)
|
||||
shifted,_=reconstruct(c-W[:,None,None],p,n,o)
|
||||
np.testing.assert_allclose(out-W[:,None],shifted,atol=5e-16)
|
||||
|
||||
def test_reject(self):
|
||||
check_multiple_rectangles_rejected()
|
||||
|
||||
|
||||
class ProjectedKernelTests(unittest.TestCase):
|
||||
@unittest.skipUnless(shutil.which("g++"), "g++ is required for actual-kernel CPU arithmetic")
|
||||
def test_actual_kernel_fp32_shifted_and_unshifted(self):
|
||||
p, neighbors, offsets = setup(True)
|
||||
parent = BlockLayout2D.build(16,16,4,4,periodic=(True,True))
|
||||
child = p.fine_layout((8,4)) # deliberately unequal parent/child strides
|
||||
ids, offsets = projected_explosion_tables(p,parent)
|
||||
cells = int(np.prod(parent.local_shape))
|
||||
targets = child.owner.ravel()[p.ghost_fine]
|
||||
donors = parent.owner.ravel()[p.ghost_coarse]
|
||||
source = (Path(__file__).resolve().parents[2] /
|
||||
"src/CelerisLab/lbm/kernels/blocks.cu").read_text()
|
||||
kernel = source.split('extern "C" __global__ void BlockExplodeProjected(',1)[1]
|
||||
kernel = 'void BlockExplodeProjected(' + kernel.split('// Targets are receiver-local SOURCE slots',1)[0]
|
||||
def array(name, values, kind):
|
||||
v = np.asarray(values).ravel()
|
||||
text = ','.join(str(int(x)) if kind=='uint64_t' else float(x).hex()+'f' for x in v)
|
||||
return f"{kind} {name}[] = {{{text}}};\n"
|
||||
for shifted in (False,True):
|
||||
c=np.random.default_rng(11).normal(0,.001,(9,16,16))
|
||||
if not shifted:c+=W[:,None,None]
|
||||
c=c.astype(np.float32)
|
||||
packed=np.full(parent.nblocks*9*cells,np.nan,np.float32)
|
||||
for q in range(9):packed[parent.slot(parent.owner.ravel(),q)]=c[q].ravel()
|
||||
# All halo/covered inputs are poisoned; kernel must use legal cores.
|
||||
for q in range(9):packed[parent.slot(parent.owner.ravel()[p.covered.ravel()],q)]=np.nan
|
||||
# Hex initializer needs a portable expression for poisoned NaNs.
|
||||
data=array('coarse',packed,'float').replace('nanf','NAN')
|
||||
code = '#include <cstdint>\n#include <cmath>\n#include <cstdio>\nusing fpxx=float;\n'
|
||||
code += f'constexpr unsigned NQ=9, TOTAL_CELLS={int(np.prod(child.local_shape))};\n'
|
||||
code += 'struct Id {unsigned x;}; Id blockIdx{0},blockDim{1},threadIdx{0};\n'
|
||||
code += 'unsigned index_f(unsigned k,unsigned q){return q*TOTAL_CELLS+k;}\n'
|
||||
code += 'int d_cx[]={0,1,-1,0,0,1,-1,1,-1},d_cy[]={0,0,0,1,-1,1,-1,-1,1};\n'+kernel
|
||||
code += data+array('target',targets,'uint64_t')+array('donor',donors,'uint64_t')
|
||||
code += array('neighbor',ids,'uint64_t')+array('offset',offsets,'float')
|
||||
code += f'float fine[{child.nblocks*9*int(np.prod(child.local_shape))}]={{}};\n'
|
||||
code += f'int main(){{for(unsigned r=0;r<{len(targets)};++r){{threadIdx.x=r;BlockExplodeProjected(coarse,fine,target,donor,{len(targets)},{cells},neighbor,offset);}}'
|
||||
code += f'for(unsigned q=0;q<9;++q)for(unsigned r=0;r<{len(targets)};++r){{auto t=target[r];printf("%.9g\\n",fine[(t/TOTAL_CELLS)*9*TOTAL_CELLS+q*TOTAL_CELLS+t%TOTAL_CELLS]);}}}}'
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
src=Path(folder)/'kernel.cpp'; binary=Path(folder)/'kernel'
|
||||
src.write_text(code)
|
||||
subprocess.run(['g++','-std=c++17','-O2',str(src),'-o',str(binary)],check=True,capture_output=True)
|
||||
result=np.fromstring(subprocess.check_output([str(binary)],text=True),sep=' ').reshape(9,-1)
|
||||
expected,_=reconstruct(c.astype(float),p,neighbors,offsets)
|
||||
np.testing.assert_allclose(result,expected,atol=6e-8,rtol=2e-7)
|
||||
@@ -1,45 +0,0 @@
|
||||
"""Host reduction contract for the optional observation-window exchange ledger."""
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from CelerisLab.lbm.raw_exchange import summarize_exchange
|
||||
|
||||
|
||||
def reduce(rows, window, duration=None, h=.5, time_end=10):
|
||||
return summarize_exchange(rows, np.array([0, 0, 1]), np.array([False, True, False]),
|
||||
body_id=0, level=1, h=h, time_end=time_end, window=window,
|
||||
reference_center=(0., 0.), duration=duration)
|
||||
|
||||
|
||||
class ExchangeWindowReduction(unittest.TestCase):
|
||||
def test_window_duration_scaling(self):
|
||||
rows = np.array([[1., 2., 3., 4.], [.5, -1., 0., 1.], [9., 9., 9., 9.]])
|
||||
out = reduce(rows, "window", duration=4)
|
||||
np.testing.assert_array_equal(out["total"]["impulse"], [1.5, 1.])
|
||||
np.testing.assert_array_equal(out["total"]["force"], [1.5 / 4, 1. / 4])
|
||||
self.assertEqual(out["total"]["torque"], 3. / 4)
|
||||
self.assertEqual(out["total"]["mass_rate"], 5. / 4)
|
||||
np.testing.assert_array_equal(out["aperture"]["impulse"], [.5, -1.])
|
||||
self.assertEqual((out["time_start"], out["time_end"]), (6., 10))
|
||||
self.assertEqual(out["sample_count"], 8) # duration / h fine substeps
|
||||
self.assertEqual(out["window"], "window")
|
||||
|
||||
def test_existing_windows_unchanged(self):
|
||||
rows = np.array([[1., 2., 3., 4.], [0., 0., 0., 0.], [0., 0., 0., 0.]])
|
||||
base = reduce(rows, "last_base_step")
|
||||
self.assertEqual((base["time_start"], base["sample_count"]), (9., 2))
|
||||
np.testing.assert_array_equal(base["total"]["force"], [1., 2.])
|
||||
sub = reduce(rows, "last_substep")
|
||||
self.assertEqual(sub["sample_count"], 1)
|
||||
|
||||
def test_duration_validation(self):
|
||||
rows = np.zeros((3, 4))
|
||||
for bad in (None, 0, -1, 1.5, True):
|
||||
with self.subTest(duration=bad), self.assertRaises(ValueError):
|
||||
reduce(rows, "window", duration=bad)
|
||||
with self.assertRaises(ValueError):
|
||||
reduce(rows, "last_base_step", duration=2)
|
||||
|
||||
|
||||
if __name__ == "__main__": unittest.main()
|
||||
@@ -13,15 +13,7 @@ h, periodic = r.helpers(r.DEFAULT_SOURCE)
|
||||
|
||||
|
||||
def test_source_pin():
|
||||
historical = json.loads((r.HERE / "data/refined_dfg/source_manifest.json").read_text())
|
||||
assert historical["commit"].startswith("cd3a657")
|
||||
mrt_history = json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text())
|
||||
assert mrt_history["commit"].startswith("3ea131e")
|
||||
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")
|
||||
assert r.verify_source(r.DEFAULT_SOURCE)["commit"].startswith("cd3a657")
|
||||
|
||||
|
||||
def test_matched_units_and_planes():
|
||||
@@ -143,20 +135,16 @@ def test_axis_specific_sampling_phase():
|
||||
|
||||
def test_full_requires_completed_pilot():
|
||||
with unittest.TestCase().assertRaisesRegex(ValueError, "pilot-results"):
|
||||
r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE, "MRT", "window"))
|
||||
r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE))
|
||||
|
||||
|
||||
def pilot_fixture(path, observation_mode="window"):
|
||||
def pilot_fixture(path):
|
||||
"""Synthetic contract fixture only; never a physical result or run receipt."""
|
||||
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()))
|
||||
manifest = r.verify_source(r.DEFAULT_SOURCE)
|
||||
(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(),
|
||||
)
|
||||
@@ -189,8 +177,6 @@ def pilot_fixture(path, observation_mode="window"):
|
||||
json.dumps(
|
||||
dict(
|
||||
case=case,
|
||||
collision="MRT",
|
||||
observation_mode=observation_mode,
|
||||
mode="pilot",
|
||||
status="pilot_complete",
|
||||
healthy=True,
|
||||
@@ -206,8 +192,6 @@ def pilot_fixture(path, observation_mode="window"):
|
||||
json.dumps(
|
||||
dict(
|
||||
case=case,
|
||||
collision="MRT",
|
||||
observation_mode=observation_mode,
|
||||
mode="pilot",
|
||||
step=interval,
|
||||
runner_sha256=r.runner_hash(),
|
||||
@@ -224,7 +208,7 @@ def test_pilot_validation_and_calibration():
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
manifest = pilot_fixture(path)
|
||||
assert r.verify_pilot(path, manifest, "MRT", "window") == 3606.0
|
||||
assert r.verify_pilot(path, manifest) == 3606.0
|
||||
|
||||
|
||||
def test_reject_malformed_pilot_receipts():
|
||||
@@ -254,7 +238,7 @@ def test_reject_malformed_pilot_receipts():
|
||||
mutate(bad)
|
||||
receipt.write_text(json.dumps(bad))
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
r.verify_pilot(path, manifest)
|
||||
# Exact reviewer regression: previously accepted NaN endpoint.
|
||||
receipt.write_text(
|
||||
json.dumps(
|
||||
@@ -266,11 +250,11 @@ def test_reject_malformed_pilot_receipts():
|
||||
)
|
||||
)
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
r.verify_pilot(path, manifest)
|
||||
receipt.write_text(json.dumps(valid))
|
||||
(path / "uniform/ready.json").write_text("{}")
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
r.verify_pilot(path, manifest)
|
||||
|
||||
|
||||
def test_strict_finite_json_and_deadline():
|
||||
@@ -299,158 +283,6 @@ def test_pressure_probes_actual_body_mask():
|
||||
assert h.pressure_probes(np.ones(pair.fine_shape), fluid, p)["dp"] == 0.0
|
||||
|
||||
|
||||
def test_matched_mrt_selection():
|
||||
for diameter in (40, 80):
|
||||
srt, sbody, sp = r.config(h, diameter)
|
||||
mrt, mbody, mp = r.config(h, diameter, "MRT")
|
||||
assert mrt["method"]["collision"] == "MRT"
|
||||
mrt["method"]["collision"] = "SRT"
|
||||
assert mrt == srt and mbody == sbody and mp == sp
|
||||
srt_plan, mrt_plan = r.plan(h), r.plan(h, "MRT")
|
||||
for key in ("root", "uniform", "bounds_root_edge", "finest_dx_m", "finest_dt_s"):
|
||||
assert srt_plan[key] == mrt_plan[key]
|
||||
assert "q2/6/8" in mrt_plan["outlet"] and "all nine" in srt_plan["outlet"]
|
||||
cfg, pair, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT")
|
||||
assert cfg.collision == "MRT" and pair.fine_shape == (240, 568)
|
||||
assert len(host.boundary_cells) > 0
|
||||
|
||||
|
||||
def test_reject_collision_receipts():
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
for rel in ("plan.json", "uniform/summary.json", "refined/summary.json",
|
||||
"uniform/ready.json", "refined/ready.json"):
|
||||
for invalid in ("SRT", None):
|
||||
with TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
manifest = pilot_fixture(path)
|
||||
receipt = path / rel
|
||||
record = json.loads(receipt.read_text())
|
||||
if invalid is None:
|
||||
record.pop("collision")
|
||||
else:
|
||||
record["collision"] = invalid
|
||||
receipt.write_text(json.dumps(record))
|
||||
with unittest.TestCase().assertRaises(ValueError):
|
||||
r.verify_pilot(path, manifest, "MRT", "window")
|
||||
|
||||
|
||||
def test_record_actual_checkout_separately():
|
||||
from source_provenance import checkout_provenance
|
||||
|
||||
actual = checkout_provenance(r.DEFAULT_SOURCE)
|
||||
assert len(actual["checkout_revision"]) == 40
|
||||
assert isinstance(actual["checkout_dirty"], bool)
|
||||
assert 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"}, {"streaming": "esopull"},
|
||||
for kwargs in ({"collision": "TRT"}, {"collision": "MRT"}, {"streaming": "esopull"},
|
||||
{"les_enabled": True},
|
||||
{"store_precision": "FP16S"}, {"lattice_model": "D3Q19"},
|
||||
{"data_type": "FP64"}, {"omega_min": 1.0}):
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,99 +0,0 @@
|
||||
"""Direct-current-header regression; historical candidate manifests stay frozen."""
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPERS = ROOT / "tests/validation/les"
|
||||
sys.path.insert(0, str(HELPERS))
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"smagorinsky_oracle", HELPERS / "test_candidate.py"
|
||||
)
|
||||
oracle = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(oracle)
|
||||
HEADER = Path("src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh")
|
||||
DESCRIPTORS = Path("src/CelerisLab/lbm/kernels/core/descriptors.cuh")
|
||||
|
||||
|
||||
class SmagorinskyNormalization(unittest.TestCase):
|
||||
def test_current_header_matches_independent_tensor_oracle(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
build_dir = Path(directory)
|
||||
for dim in (2, 3):
|
||||
for cs in (0.0, 0.16):
|
||||
# Compile directly against this checkout, not saved candidate headers.
|
||||
closed = oracle.build(
|
||||
ROOT.parent, ROOT.name, dim, cs, 1, 3, build_dir
|
||||
)
|
||||
converged = oracle.build(
|
||||
ROOT.parent, ROOT.name, dim, cs, 0, 200, build_dir
|
||||
)
|
||||
for rho in (0.7, 1.0, 1.3):
|
||||
for tau0 in (0.56, 0.8):
|
||||
for kind, magnitude in (
|
||||
("equilibrium", 0.0),
|
||||
("shear", -0.02),
|
||||
("shear", 0.02),
|
||||
("extension", -0.02),
|
||||
("extension", 0.02),
|
||||
):
|
||||
stress = oracle.tensor(
|
||||
dim, kind, magnitude * rho, 0, dim - 1
|
||||
)
|
||||
f, eq = oracle.state(dim, rho, stress)
|
||||
expected = 1 / oracle.standard_tau(
|
||||
tau0, cs, oracle.norm(stress), rho
|
||||
)
|
||||
with self.subTest(
|
||||
dim=dim,
|
||||
cs=cs,
|
||||
rho=rho,
|
||||
tau=tau0,
|
||||
kind=kind,
|
||||
sign=magnitude,
|
||||
):
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, rho, tau0), expected, delta=3e-6
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
converged(f, eq, rho, tau0),
|
||||
expected,
|
||||
delta=3e-6,
|
||||
)
|
||||
if cs == 0:
|
||||
f, eq = oracle.state(
|
||||
dim, 1.0, oracle.tensor(dim, "equilibrium", 0.0)
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, 1.0, 0.50001), 1.99, delta=3e-6
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
closed(f, eq, 1.0, 200.0), 0.01, delta=3e-6
|
||||
)
|
||||
|
||||
def test_removing_sqrt_two_is_detected(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for relative in (HEADER, DESCRIPTORS):
|
||||
target = root / "old_convention" / relative
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / relative, target)
|
||||
target = root / "old_convention" / HEADER
|
||||
text = target.read_text()
|
||||
factor = "18.0f * sqrtf(2.0f) * smag_const_sq"
|
||||
self.assertEqual(text.count(factor), 2)
|
||||
target.write_text(text.replace(factor, "18.0f * smag_const_sq"))
|
||||
for dim in (2, 3):
|
||||
old = oracle.build(root, "old_convention", dim, 0.16, 1, 3, root)
|
||||
stress = oracle.tensor(dim, "shear", 0.02)
|
||||
f, eq = oracle.state(dim, 1.0, stress)
|
||||
expected = 1 / oracle.standard_tau(0.8, 0.16, oracle.norm(stress), 1.0)
|
||||
self.assertGreater(abs(old(f, eq, 1.0, 0.8) - expected), 1e-3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,86 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,38 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
"""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())
|
||||
@@ -1,392 +0,0 @@
|
||||
"""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())
|
||||
@@ -1,165 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@@ -1,472 +0,0 @@
|
||||
"""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
|
||||
@@ -1,334 +0,0 @@
|
||||
"""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())
|
||||
@@ -1,92 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,239 +0,0 @@
|
||||
"""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')
|
||||
@@ -1,260 +0,0 @@
|
||||
"""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,7 +7,6 @@ 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
|
||||
@@ -22,7 +21,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 FixedPatchStepper, PeriodicPatchStepper
|
||||
from CelerisLab.lbm.refinement_stepper import PeriodicPatchStepper
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
POP_ATOL = 3e-6
|
||||
@@ -61,69 +60,6 @@ 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)
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,497 +0,0 @@
|
||||
"""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())
|
||||
@@ -1,205 +0,0 @@
|
||||
"""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())
|
||||
@@ -1,155 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,287 +0,0 @@
|
||||
"""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/MRT DFG comparison. GPU execution requires explicit root admission."""
|
||||
"""Frozen matched SRT DFG comparison. GPU execution requires explicit root admission."""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
@@ -39,22 +39,20 @@ def helpers(source):
|
||||
return h, periodic
|
||||
|
||||
|
||||
def config(h, diameter, collision="SRT"):
|
||||
cfg, body, p = h.make_config(diameter, SPEED, collision, "neq_extrap", "zou_he_local")
|
||||
def config(h, diameter):
|
||||
cfg, body, p = h.make_config(diameter, SPEED, "SRT", "neq_extrap", "zou_he_local")
|
||||
cfg["method"]["outlet"]["srt_neq_damp"] = 0.5
|
||||
return cfg, body, p
|
||||
|
||||
|
||||
def plan(h, collision="SRT", observation_mode="last_base_step"):
|
||||
_, _, c = config(h, 40, collision)
|
||||
_, _, f = config(h, 80, collision)
|
||||
def plan(h):
|
||||
_, _, c = config(h, 40)
|
||||
_, _, f = config(h, 80)
|
||||
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,
|
||||
@@ -84,11 +82,7 @@ def plan(h, collision="SRT", observation_mode="last_base_step"):
|
||||
/ (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"
|
||||
if collision == "SRT" else
|
||||
"MRT pressure NEQ, unknowns q2/6/8 extrapolated; no backflow clamp; noncanonical DFG outlet"
|
||||
),
|
||||
outlet="SRT pressure NEQ, all nine fneq damped by .5, no backflow clamp; noncanonical DFG outlet",
|
||||
modes={"pilot_tstar": 5.0, "full_tstar": 300.0},
|
||||
observation_dense_tstar=0.02,
|
||||
)
|
||||
@@ -126,7 +120,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, collision="SRT"):
|
||||
def host_geometry(source, h):
|
||||
"""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
|
||||
@@ -139,7 +133,7 @@ def host_geometry(source, h, collision="SRT"):
|
||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||
from CelerisLab.body.objects import SimObject
|
||||
|
||||
cfgdict, _, _ = config(h, 40, collision)
|
||||
cfgdict, _, _ = config(h, 40)
|
||||
with TemporaryDirectory() as tmp:
|
||||
p = Path(tmp) / "cfg.json"
|
||||
p.write_text(json.dumps(cfgdict))
|
||||
@@ -157,60 +151,14 @@ def host_geometry(source, h, collision="SRT"):
|
||||
return cfg, pair, body, host
|
||||
|
||||
|
||||
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())
|
||||
def verify_source(source):
|
||||
expected = json.loads((HERE / "data/refined_dfg/source_manifest.json").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
|
||||
@@ -219,7 +167,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, a.collision)
|
||||
cfgdict, bodydict, p = config(h, 40 if refined else 80)
|
||||
cap = 5.0 if a.mode == "pilot" else 300.0
|
||||
max_steps = math.ceil(cap * p["diameter"] / SPEED)
|
||||
rows = []
|
||||
@@ -231,7 +179,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, a.collision)
|
||||
cfg, pair, body, host = host_geometry(a.source, h)
|
||||
context = stack.enter_context(CudaContext(0))
|
||||
backend = stack.enter_context(
|
||||
FixedPatchStepper(
|
||||
@@ -243,7 +191,6 @@ 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
|
||||
@@ -293,9 +240,20 @@ def run_case(a, h, periodic, case):
|
||||
impulse = np.zeros(2)
|
||||
tick = time.monotonic()
|
||||
if refined:
|
||||
impulse = advance_refined_interval(
|
||||
backend, n, a.deadline, a.observation_mode
|
||||
)
|
||||
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)
|
||||
else:
|
||||
done = 0
|
||||
while done < n:
|
||||
@@ -378,9 +336,6 @@ 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"],
|
||||
@@ -393,9 +348,6 @@ 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),
|
||||
@@ -464,16 +416,12 @@ def check_deadline(deadline):
|
||||
)
|
||||
|
||||
|
||||
def verify_pilot(path, manifest, collision="SRT", observation_mode="last_base_step"):
|
||||
def verify_pilot(path, manifest):
|
||||
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
|
||||
):
|
||||
@@ -484,9 +432,7 @@ def verify_pilot(path, manifest, collision="SRT", observation_mode="last_base_st
|
||||
for case, diameter in (("uniform", 80), ("refined", 40)):
|
||||
summary = read_finite_json(path / case / "summary.json")
|
||||
if (
|
||||
summary.get("observation_mode") != observation_mode
|
||||
or summary.get("collision") != collision
|
||||
or summary.get("case") != case
|
||||
summary.get("case") != case
|
||||
or summary.get("mode") != "pilot"
|
||||
or summary.get("status") != "pilot_complete"
|
||||
or summary.get("healthy") is not True
|
||||
@@ -537,9 +483,7 @@ def verify_pilot(path, manifest, collision="SRT", observation_mode="last_base_st
|
||||
raise ValueError("Unhealthy pilot observations")
|
||||
ready = read_finite_json(path / case / "ready.json")
|
||||
if (
|
||||
ready.get("observation_mode") != observation_mode
|
||||
or ready.get("collision") != collision
|
||||
or ready.get("case") != case
|
||||
ready.get("case") != case
|
||||
or ready.get("mode") != "pilot"
|
||||
or ready.get("step") != interval
|
||||
or ready.get("runner_sha256") != runner_hash()
|
||||
@@ -563,9 +507,6 @@ def verify_pilot(path, manifest, collision="SRT", observation_mode="last_base_st
|
||||
|
||||
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")
|
||||
@@ -578,16 +519,9 @@ def main():
|
||||
)
|
||||
a = parser.parse_args()
|
||||
a.source = a.source.resolve()
|
||||
a.observation_mode = "window" if a.exchange_window else "last_base_step"
|
||||
manifest = verify_source(a.source, a.collision, a.observation_mode)
|
||||
manifest = verify_source(a.source)
|
||||
h, periodic = helpers(a.source)
|
||||
from source_provenance import checkout_provenance
|
||||
|
||||
a.provenance = checkout_provenance(a.source)
|
||||
calibration = (
|
||||
verify_pilot(a.pilot_results, manifest, a.collision, a.observation_mode)
|
||||
if a.mode == "full" else None
|
||||
)
|
||||
calibration = verify_pilot(a.pilot_results, manifest) 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)
|
||||
@@ -602,17 +536,16 @@ def main():
|
||||
if a.output.exists() and any(a.output.iterdir()):
|
||||
raise ValueError("Output must be empty/new")
|
||||
a.output.mkdir(parents=True, exist_ok=True)
|
||||
specification = plan(h, a.collision, a.observation_mode)
|
||||
specification = plan(h)
|
||||
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, a.collision)
|
||||
host_geometry(a.source, h)
|
||||
(a.output / "plan.json").write_text(json.dumps(specification, indent=2))
|
||||
if a.mode == "plan":
|
||||
return 0
|
||||
@@ -628,9 +561,6 @@ 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),
|
||||
@@ -644,9 +574,6 @@ 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,15 +59,8 @@ def decode_macros(data, w, cx, cy):
|
||||
return profile, *metrics
|
||||
|
||||
|
||||
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())
|
||||
def source_guard(root, variant):
|
||||
manifest = json.loads((HERE / "data/les_flow/source_pins.json").read_text())
|
||||
for name, sha in manifest["sources"].items():
|
||||
expected = (
|
||||
manifest["candidate_header_sha256"]
|
||||
@@ -76,7 +69,7 @@ def source_guard(root, variant, experiment="historical"):
|
||||
)
|
||||
if hashlib.sha256((root / name).read_bytes()).hexdigest() != expected:
|
||||
raise RuntimeError("Source pin mismatch: " + name)
|
||||
identity = dict(
|
||||
return dict(
|
||||
base_commit=manifest["base_commit"],
|
||||
production_origin_commit=manifest["production_origin_commit"],
|
||||
source_experiment=manifest["source_experiment"],
|
||||
@@ -87,22 +80,12 @@ def source_guard(root, variant, experiment="historical"):
|
||||
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"
|
||||
@@ -117,7 +100,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, args.experiment)
|
||||
pins = source_guard(root, args.variant)
|
||||
# Refuse installed-package shadowing; import only after explicit GPU opt-in.
|
||||
sys.path.insert(0, str(root / "src"))
|
||||
import CelerisLab
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""Explicit tiny CUDA explosion parity and constructor/one-interval smoke check.
|
||||
|
||||
No installs or device selection defaults. Run only on the authorized GPU host.
|
||||
This is operator/runtime evidence, not Taylor-Green accuracy qualification.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def check_case(ctx, shifted, projected):
|
||||
import pycuda.driver as cuda
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
|
||||
from CelerisLab.lbm.refinement_tables import projected_explosion_tables
|
||||
from CelerisLab.lbm.refinement import ACTIVE
|
||||
from test_projected_explosion import reconstruct, W
|
||||
|
||||
cfg = LBMConfig(nx=16, ny=16, viscosity=.1, velocity=0,
|
||||
ddf_shifting=shifted)
|
||||
cfg.validate()
|
||||
options = {"explosion": "projected_linear"} if projected else {}
|
||||
# Omitting explosion in the constant case tests the production default.
|
||||
with FixedPatchStepper(cfg, context=ctx, refinements=[[(4, 4, 12, 12)]],
|
||||
level_blocks=[(4, 4), (8, 4)], **options) as run:
|
||||
parent, child = run._levels
|
||||
pair = run._pairs[0]
|
||||
patch = run.hierarchy.pairs[0]
|
||||
coarse_cells = int(np.prod(parent.layout.local_shape))
|
||||
fine_cells = int(np.prod(child.layout.local_shape))
|
||||
assert coarse_cells != fine_cells
|
||||
assert pair.parent_cells == coarse_cells
|
||||
assert len(pair.interpolation) == (2 if projected else 0)
|
||||
donor = patch.ghost_coarse.astype(np.intp)
|
||||
# Random variation must exercise the gradient, not just a constant field.
|
||||
c = np.random.default_rng(11).normal(0, .001, (9, 16, 16))
|
||||
if not shifted:
|
||||
c += W[:, None, None]
|
||||
c = c.astype(np.float32)
|
||||
legal = np.zeros(256, bool)
|
||||
legal[donor] = True
|
||||
c.reshape(9, -1)[:, ~legal] = np.nan
|
||||
coarse = np.full(np.prod(parent.layout.blocked_shape), np.nan, np.float32)
|
||||
for q in range(9):
|
||||
coarse[parent.layout.slot(parent.layout.owner.ravel()[legal], q)] = c.reshape(9, -1)[q, legal]
|
||||
fine = np.full(np.prod(child.layout.blocked_shape), np.nan, np.float32)
|
||||
cuda.memcpy_htod(parent.current, coarse)
|
||||
cuda.memcpy_htod(child.current, fine)
|
||||
calls = []
|
||||
real_hop = run._hop
|
||||
try:
|
||||
# Execute the production explosion launch exactly; isolate it from
|
||||
# subsequent streaming/collision without replacing the kernel/tables.
|
||||
run._hop = lambda *args, **kwargs: calls.append(args)
|
||||
run._advance(1, parent.current, block=run._block, stream=run._stream)
|
||||
run._stream.synchronize()
|
||||
finally:
|
||||
run._hop = real_hop
|
||||
assert calls == [(1, child.current, child.spare), (1, child.spare, child.current)]
|
||||
cuda.memcpy_dtoh(fine, child.current)
|
||||
targets = child.layout.owner.ravel()[patch.ghost_fine]
|
||||
slots = np.array([child.layout.slot(targets, q) for q in range(9)])
|
||||
actual = fine[slots]
|
||||
neighbors, offsets = projected_explosion_tables(patch, parent.layout)
|
||||
inverse = np.full(parent.layout.nblocks * coarse_cells, -1, dtype=int)
|
||||
inverse[parent.layout.owner.ravel()] = np.arange(256)
|
||||
constant = c.reshape(9, -1)[:, donor].astype(float)
|
||||
expected = reconstruct(c.astype(float), patch, inverse[neighbors], offsets)[0] if projected else constant
|
||||
assert np.isfinite(actual).all()
|
||||
if projected:
|
||||
assert np.max(np.abs(expected - constant)) > 1e-5
|
||||
np.testing.assert_allclose(actual, expected, atol=6e-8, rtol=2e-7)
|
||||
untouched = np.ones(fine.size, bool)
|
||||
untouched[slots.ravel()] = False
|
||||
assert np.isnan(fine[untouched]).all(), "explosion wrote outside target slots"
|
||||
error = float(np.max(np.abs(actual - expected)))
|
||||
|
||||
# Restore ordinary execution and exercise real constructor wiring,
|
||||
# stream-only ghost hops, restriction, halo copies and collision once.
|
||||
initial = [np.broadcast_to(W[:, None, None], (9, *k.shape)).copy()
|
||||
for k in run.hierarchy.kinds]
|
||||
run.initialize(*initial)
|
||||
run.step(1)
|
||||
for values, kind in zip(run.download(), run.hierarchy.kinds):
|
||||
active = values[:, kind == ACTIVE]
|
||||
assert np.isfinite(active).all()
|
||||
np.testing.assert_allclose(active, np.broadcast_to(W[:, None], active.shape),
|
||||
atol=3e-7, rtol=1e-6)
|
||||
assert run.step_count == 1
|
||||
return dict(shifted=shifted, explosion="projected_linear" if projected else "constant_default",
|
||||
parent_stride=coarse_cells, child_stride=fine_cells,
|
||||
compared_slots=int(actual.size), max_abs_error=error,
|
||||
poisoned_nondonors=True, untouched_slots_preserved=True, equilibrium_step=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--repo", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--run-gpu", action="store_true", help="Explicitly opt into GPU execution")
|
||||
args = parser.parse_args()
|
||||
selected = os.environ.get("CUDA_VISIBLE_DEVICES", "")
|
||||
if not args.run_gpu or not selected or "," in selected or selected == "-1":
|
||||
parser.error("--run-gpu and exactly one explicit physical CUDA_VISIBLE_DEVICES are required")
|
||||
if args.output.exists():
|
||||
parser.error("output already exists; choose a new receipt path")
|
||||
root = args.repo.resolve()
|
||||
sys.path[:0] = [str(root / "src"), str(root / "tests/unit"),
|
||||
str(root / "tests/validation/taylor_green")]
|
||||
import CelerisLab
|
||||
if Path(CelerisLab.__file__).resolve() != root / "src/CelerisLab/__init__.py":
|
||||
raise RuntimeError("Wrong imported source checkout")
|
||||
from run_case import provenance
|
||||
report = dict(source=provenance(root),
|
||||
numpy_oracle_sha256=hashlib.sha256((root / "tests/unit/test_projected_explosion.py").read_bytes()).hexdigest(),
|
||||
cuda_visible_devices=selected,
|
||||
logical_device=0, completed=False, cases=[])
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
def save():
|
||||
args.output.write_text(json.dumps(report, indent=2, allow_nan=False) + "\n")
|
||||
save()
|
||||
try:
|
||||
from CelerisLab.cuda.context import CudaContext
|
||||
with CudaContext(0) as ctx:
|
||||
report["device"] = ctx.device.name()
|
||||
for shifted in (False, True):
|
||||
for projected in (False, True):
|
||||
report["cases"].append(check_case(ctx, shifted, projected))
|
||||
save()
|
||||
report["completed"] = True
|
||||
save()
|
||||
except Exception as exc:
|
||||
report["error"] = f"{type(exc).__name__}: {exc}"
|
||||
save()
|
||||
raise
|
||||
print(json.dumps(report, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"implementation_revision": "43c527b",
|
||||
"historical_baseline_revision": "9805383f9c50d867e8555471baf5dbda8333a652",
|
||||
"scope": "Re100 depth2; FP64 CPU arrival_f macro diagnostics, not candidate CUDA execution or performance",
|
||||
"tstar": 0.48,
|
||||
"cases": [
|
||||
{
|
||||
"n": 32,
|
||||
"historical_gpu_report": "CelerisLab_9805383_tg_matched_re100_n32_d2.json",
|
||||
"historical_gpu_sha256": "6e84837781f2c4eb001721d6f828ea0d927b255fd4c46efdef443a9ae59af730",
|
||||
"constant_max_abs_cpu_gpu_metric_difference": {
|
||||
"velocity_l2": 6.928351422436085e-08,
|
||||
"pressure_l2": 3.5717157327030957e-06,
|
||||
"energy_relative": 2.1743219684455806e-07
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"explosion": "constant",
|
||||
"final": {
|
||||
"velocity_l2": 0.03249421441793122,
|
||||
"pressure_l2": 0.08986961517973119,
|
||||
"energy_relative": 0.044970292392050686,
|
||||
"amplitude_bias": -0.02307193176461997,
|
||||
"orthogonal_residual_l2": 0.022881432107435998,
|
||||
"mass_signed_relative": -7.616129948928574e-14,
|
||||
"momentum_change": [
|
||||
-3.26405569239796e-14,
|
||||
-2.7000103541841014e-14
|
||||
]
|
||||
},
|
||||
"max_abs_mass_relative": 7.616129948928574e-14,
|
||||
"max_abs_momentum_change": 3.608224830031759e-14
|
||||
},
|
||||
{
|
||||
"explosion": "projected_linear",
|
||||
"final": {
|
||||
"velocity_l2": 0.015226371368103809,
|
||||
"pressure_l2": 0.048560123195715434,
|
||||
"energy_relative": 0.02285059695090361,
|
||||
"amplitude_bias": -0.011601431258836059,
|
||||
"orthogonal_residual_l2": 0.009861499773660909,
|
||||
"mass_signed_relative": -7.616129948928574e-14,
|
||||
"momentum_change": [
|
||||
-4.030109579389318e-14,
|
||||
-2.3444787777826548e-15
|
||||
]
|
||||
},
|
||||
"max_abs_mass_relative": 7.616129948928574e-14,
|
||||
"max_abs_momentum_change": 4.030109579389318e-14
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": 64,
|
||||
"historical_gpu_report": "CelerisLab_9805383_tg_re100_n64_d2.json",
|
||||
"historical_gpu_sha256": "251f743d6adc33dff1d829db91fcc5f982c9ac299c177eb2c94e313249b25b9f",
|
||||
"constant_max_abs_cpu_gpu_metric_difference": {
|
||||
"velocity_l2": 1.7330029265794078e-08,
|
||||
"pressure_l2": 8.634212284933107e-07,
|
||||
"energy_relative": 8.74280938097105e-08
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"explosion": "constant",
|
||||
"final": {
|
||||
"velocity_l2": 0.01131134574353476,
|
||||
"pressure_l2": 0.03550495463085949,
|
||||
"energy_relative": 0.012514764591008386,
|
||||
"amplitude_bias": -0.006336459299048802,
|
||||
"orthogonal_residual_l2": 0.009369942693596348,
|
||||
"mass_signed_relative": -1.412203687323199e-13,
|
||||
"momentum_change": [
|
||||
-2.1049828546892968e-13,
|
||||
-1.632548263241773e-14
|
||||
]
|
||||
},
|
||||
"max_abs_mass_relative": 1.412203687323199e-13,
|
||||
"max_abs_momentum_change": 2.1049828546892968e-13
|
||||
},
|
||||
{
|
||||
"explosion": "projected_linear",
|
||||
"final": {
|
||||
"velocity_l2": 0.004930396085095945,
|
||||
"pressure_l2": 0.016580125701009935,
|
||||
"energy_relative": 0.0056291365148441195,
|
||||
"amplitude_bias": -0.0028419317091766816,
|
||||
"orthogonal_residual_l2": 0.004028924138812507,
|
||||
"mass_signed_relative": -1.4099832412739488e-13,
|
||||
"momentum_change": [
|
||||
-1.709743457922741e-13,
|
||||
7.568945470382005e-14
|
||||
]
|
||||
},
|
||||
"max_abs_mass_relative": 1.4099832412739488e-13,
|
||||
"max_abs_momentum_change": 1.709743457922741e-13
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
"""One bounded GPU case. No production edits, plots, checkpoints or field dumps."""
|
||||
"""One bounded GPU case, with explicit opt-in experimental explosion."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
@@ -75,7 +75,10 @@ def main():
|
||||
p.add_argument("--n", type=int, choices=[32, 64, 128, 256], required=True)
|
||||
p.add_argument("--depth", type=int, choices=[1, 2, 3], required=True)
|
||||
p.add_argument("--output", type=Path, required=True)
|
||||
p.add_argument("--explosion", choices=["constant", "projected_linear"], default="constant")
|
||||
args = p.parse_args()
|
||||
if args.explosion != "constant" and args.depth != 2:
|
||||
p.error("projected_linear requires depth 2")
|
||||
if not os.environ.get("CUDA_VISIBLE_DEVICES"):
|
||||
p.error("Explicit physical CUDA_VISIBLE_DEVICES selection required")
|
||||
repo = args.repo.resolve()
|
||||
@@ -99,6 +102,8 @@ def main():
|
||||
report = dict(
|
||||
schema=1,
|
||||
source=provenance(repo),
|
||||
historical_baseline_revision="9805383f9c50d867e8555471baf5dbda8333a652",
|
||||
explosion=args.explosion,
|
||||
n=args.n,
|
||||
re_L=args.re,
|
||||
depth=args.depth,
|
||||
@@ -132,6 +137,7 @@ def main():
|
||||
coarse_boundary="periodic",
|
||||
refinements=refinements,
|
||||
level_blocks=[(8, 8)] * args.depth,
|
||||
explosion=args.explosion,
|
||||
)
|
||||
with run:
|
||||
run.initialize(
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Diagnostic-only rectangular adapter to CPUREF-0.3. No production changes."""
|
||||
import sys,json,time,argparse,hashlib
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--repo',type=Path,required=True,help='Candidate checkout at 43c527b')
|
||||
parser.add_argument('--n',type=int,choices=(32,64),default=32)
|
||||
parser.add_argument('--output-dir',type=Path,required=True)
|
||||
args=parser.parse_args()
|
||||
ROOT=args.repo.resolve()
|
||||
sys.path[:0]=[str(ROOT/'src'),str(ROOT/'tests/validation/multigrid'),str(ROOT/'tests/validation/taylor_green'),str(ROOT/'tests/unit')]
|
||||
import cpu_reference as cpu
|
||||
import oracle
|
||||
from run_case import provenance
|
||||
from test_projected_explosion import reconstruct
|
||||
from CelerisLab.config import LBMConfig
|
||||
from CelerisLab.lbm.refinement import FixedPatchSet2D
|
||||
from CelerisLab.lbm.refinement_tables import projected_explosion_tables
|
||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||
class RectSpec(cpu.Spec):
|
||||
@property
|
||||
def n_fine(self): return 2*self.n_coarse
|
||||
|
||||
def setup(n):
|
||||
nu=oracle.U*n/100
|
||||
cfg=LBMConfig(nx=n,ny=n,viscosity=nu)
|
||||
bounds=oracle.rectangles(n,2)[0]
|
||||
p=FixedPatchSet2D.from_config(cfg,bounds)
|
||||
spec=RectSpec(n_coarse=n,patch_lo=n//8,patch_hi=7*n//8,halo=n//4,tau_coarse=p.tau_coarse,tau_fine=p.tau_fine,collision='srt')
|
||||
q,k=np.divmod(p.coalesce_coarse.astype(int),n*n); y,x=np.divmod(k,n)
|
||||
rows=np.column_stack((q,y,x)); links=[]
|
||||
for qi,yi,xi in rows:
|
||||
sx,sy=xi-cpu.CX[qi],yi-cpu.CY[qi]
|
||||
if not p.covered[sy,sx]: links.append(cpu.CornerLink(f'{xi}_{yi}_q{qi}',int(qi),(int(sx),int(sy)),(int(xi),int(yi))))
|
||||
assert len(links)==8
|
||||
maps=cpu.Maps(~p.covered,p.covered,p.fine_kind==0,p.fine_kind==1,p.fine_kind==2,p.fine_kind,np.arange(2*n)//2,(np.arange(2*n)+.5)/2,rows,tuple(links))
|
||||
# Verify the CPU coalescence child locations equal actual production tables.
|
||||
children=np.array([[qi*(2*n)**2+(2*yi+dy)*2*n+2*xi+dx for dy in (0,1) for dx in (0,1)] for qi,yi,xi in rows])
|
||||
np.testing.assert_array_equal(children,p.coalesce_fine)
|
||||
layout=BlockLayout2D.build(n,n,8,8,periodic=(True,True))
|
||||
neighbors,offsets=projected_explosion_tables(p,layout)
|
||||
inverse=np.full(layout.nblocks*np.prod(layout.local_shape),-1,dtype=int)
|
||||
inverse[layout.owner.ravel()]=np.arange(n*n)
|
||||
neighbors=inverse[neighbors]
|
||||
def explode(gc,cv,gf,fv,m,writes):
|
||||
# Only actual transfer footprint, including inner and outer ghost slabs.
|
||||
gy,gx=np.divmod(p.ghost_fine.astype(int),2*n)
|
||||
if mode[0]=='constant': values=gc.reshape(9,-1)[:,p.ghost_coarse]
|
||||
else: values=reconstruct(gc,p,neighbors,offsets)[0]
|
||||
assert np.isfinite(values).all()
|
||||
gf[:,gy,gx]=values; fv[:,gy,gx]=True; writes[gy,gx]+=1
|
||||
mode=['constant']; cpu._explode=explode
|
||||
return spec,maps,p,mode,nu
|
||||
|
||||
def run(n,which):
|
||||
spec,m,p,mode,nu=setup(n); mode[0]=which
|
||||
masks=[m.coarse_active,m.fine_active]
|
||||
arrays=[oracle.initial(mask.shape,l,n,nu) for l,mask in enumerate(masks)]
|
||||
for a,mask in zip(arrays,masks):a[:,~mask]=np.nan
|
||||
s=cpu.State(*arrays); rows=[]; initmass=None; momentum0=None; start=time.monotonic()
|
||||
for tstar in oracle.TIMES:
|
||||
step=round(tstar*n/oracle.U)
|
||||
while s.tick<2*step: s,_=cpu.advance_coarse_once(spec,m,s)
|
||||
arrays=[s.coarse_f,s.fine_f]
|
||||
row=oracle.metrics(arrays,masks,step,n,nu,initmass)
|
||||
if initmass is None:initmass=row['mass']
|
||||
dot=den=res=0.; parts=[]; momentum=np.zeros(2)
|
||||
for l,(a,mask) in enumerate(zip(arrays,masks)):
|
||||
v=a[:,mask];rho=v.sum(0);u=cpu.CX@v/rho;w=cpu.CY@v/rho
|
||||
x,y=oracle.centers(mask.shape,l);ex,ey,_=oracle.exact(x[mask],y[mask],step,n,nu);vol=.25**l
|
||||
dot+=vol*np.sum(u*ex+w*ey);den+=vol*np.sum(ex*ex+ey*ey)
|
||||
parts.append((u,w,ex,ey,vol));momentum+=vol*np.array([np.sum(cpu.CX@v),np.sum(cpu.CY@v)])
|
||||
amp=dot/den
|
||||
res=sum(vol*np.sum((u-amp*ex)**2+(w-amp*ey)**2) for u,w,ex,ey,vol in parts)
|
||||
if momentum0 is None:momentum0=momentum.copy()
|
||||
row.update(step=step,exact_energy_quadrature_factor=den/(2*n*n*row['exact_energy']),amplitude_ratio=amp,amplitude_bias=amp-1,orthogonal_residual_l2=float(np.sqrt(res/den)),energy_signed_relative=row['energy']/row['exact_energy']-1,mass_signed_relative=row['mass']/initmass-1,momentum=momentum.tolist(),momentum_change=(momentum-momentum0).tolist())
|
||||
rows.append(row)
|
||||
return dict(n=n,explosion=which,seconds=time.monotonic()-start,rows=rows)
|
||||
if __name__=='__main__':
|
||||
a=args
|
||||
a.output_dir.mkdir(parents=True,exist_ok=True)
|
||||
result=dict(implementation_revision='43c527b',source=provenance(ROOT),diagnostic_input_sha256={name:hashlib.sha256((ROOT/name).read_bytes()).hexdigest() for name in ('tests/validation/multigrid/cpu_reference.py','tests/unit/test_projected_explosion.py')},precision='FP64 physical populations',phase='arrival_f; macro moments invariant under collision, initialization equilibrium',runs=[])
|
||||
out=a.output_dir/f'n{a.n}_ab.json'
|
||||
for mode in ('constant','projected_linear'):
|
||||
r=run(a.n,mode);result['runs'].append(r);out.write_text(json.dumps(result,indent=2)+'\n');print(json.dumps(r),flush=True)
|
||||
@@ -1,326 +0,0 @@
|
||||
"""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())
|
||||
@@ -1,183 +0,0 @@
|
||||
"""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