Compare commits

..
41 changed files with 2445 additions and 694 deletions
-62
View File
@@ -1,62 +0,0 @@
# 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.
@@ -0,0 +1,72 @@
# One bounded bodyless SRT inlet control
Preparation only; review precedes any GPU admission. This test-only opt-in is based on
8e378aa047bb35ae4ec46037eff7256bd2f074e7. Historical first-health evidence and its source
manifest are untouched. The control calls the same pinned D80 config factory and uses
unchanged production `Simulation.initialize()` / `run(1)`; `--without-cylinder` changes
only the single cylinder object list to empty. No profile, wall/source plane, domain,
collision, relaxation, speed, representation, step cap, health limit or field observation
is adjusted. SRT tau=.524 and the existing inlet/outlet remain as in the failed run.
Production already supports zero objects through `ensure_packed_buffers`. The absent
body has no force observable: the control never calls `read_force(0)`, records `force:null`
and `force_observation:not_applicable_no_body`, and does not invent a zero force. Physical
population/macro/Mach/density checks remain intact. Existing per-step diagnostics, first
health crossing, immediately preceding/current raw+decoded state capture, 128 MiB budget,
external watchdog and maximum 320 steps remain unchanged. No extra baseline run is needed.
Removing the cylinder necessarily turns its former solid cells into fluid. Those cells
receive the same production parabolic fluid initializer; shared fluid and boundary-ghost
cells retain the original initializer and configuration. This is not a transplant of the
baseline's solid-cell state into new fluid. Removing the body also removes its cut links
and specializes the existing kernel for zero objects, as ordinary Simulation already does.
The spatial `body_near` category is retained only as the **former-cylinder neighborhood**
for comparing locations. It does not claim an actual body is present; body-solid flag
counts should be zero in the control.
## Explicit identity and source checks
The unchanged manifest still pins numerical source 9805383, as used by the baseline8e
harness. The new test-only HEAD does not pretend to be that historical run. Preflight,
READY and diagnostic/result records include the control label, original baseline harness
commit, new diagnostic checkout revision/dirty status, selected numerical checkout
revision/dirty status, and new diagnostic file hash. Production/helper bytes are verified
against the original manifest before either case. A later development checkout with
changed numerical files is rejected; the old pin is not silently accepted or regenerated.
The default cylinder case remains available for compatibility, but this requested control
uses only `--without-cylinder` and a distinct new output directory.
## Interpretation before seeing outcomes
- Similar early inlet-region failure without a cylinder: cylinder presence is not
necessary for this observed short-time instability under these settings. This supports
investigating inlet/collision/kinetic-state dynamics; it does not prove a specific
boundary defect or exclude other shared boundaries/initialization effects.
- No health violation through 320: removing the body changes the observed onset, consistent
with a body-generated transient or altered amplification interacting with the inlet.
This does not prove the cylinder is erroneous, establish inlet stability beyond320,
or certify SRT generally.
- Different failure location/time: geometry affects the transient; classify the preserved
incoming distributions and moments before assigning any mechanism.
The baseline failed at196 on finite Mach>.2, not nonfinite state. A control can likewise
stop on a finite threshold crossing. Do not describe either as a NaN blow-up unless the
separate diagnostic flags establish that.
CPU-only checks:
```sh
python tests/validation/uniform_srt_diagnostic/test_diagnostic.py
python tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py --without-cylinder --output /tmp/new-bodyless-preflight
```
After review and root admission, from the explicitly recorded test checkout:
```sh
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s 240s python tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py --without-cylinder --execute --gpu-admitted --wall-seconds 180 --output output/new-srt-bodyless-control
```
This patch performs no GPU run or remote push. Original runner gates/results and
production files are unchanged. The extra tests verify removal-only selection without
mutating inputs, reject unexpected object sets, verify absence of force readback, and
retain physical health failures when force is unavailable.
+31
View File
@@ -0,0 +1,31 @@
# Fixed-window inlet history (test-only)
Opt in with `--record-inlet-history`, together with `--without-cylinder` for the requested control. Default execution still retains only the original first-health pair. This followup is based on bodyless diagnostic `66a27410504fef477808d6079ce79ca1ae6b903c`; the numerical source manifest remains unchanged at `9805383f9c50d867e8555471baf5dbda8333a652`. Actual diagnostic/numerical checkout revisions and script SHA256 are recorded separately.
The requested observation window is **completed steps 0 through 196 inclusive**. Recording stops after196, but evolution continues under the original first-health/320-step/wall-time stop behavior. An earlier failure preserves the recorded prefix, including its failing observation. `complete_for_requested_window` means all197 requested frames exist; it never means complete history past196. `diagnostic_terminal_step` records the terminal observed step separately.
`inlet_history.npz` contains:
- `raw_shifted[time,q,y,x]`: unchanged FP32 bits, all9q, all330 rows, x0..3, copied from already-existing active-buffer raw snapshots.
- Static uint16 `flags[y,x]`, global x/y indices, contiguous int64 `steps`.
- `time_s=steps*dt` and `tstar=steps*mean_speed/diameter`, plus requested range and diagnostic terminal step.
- Step zero is the initialized state; subsequent states are completed postcollision active-buffer observations. Ghost/solid flags remain distinct from physical-fluid health checks.
Full configuration, physical parameters and source identity remain in `preflight.json`; `diagnostics.json` adds the history SHA256, size, window and completeness receipt. No additional full-field copies or solver calls are introduced. The new history copies only x0..3 from the baseline's existing raw snapshot. Baseline two-state evidence and health gates remain intact.
The full requested raw payload is9,361,440 bytes (8.928MiB). Flags, time arrays, coordinates and NPZ overhead keep it below12MiB. Initialization checks the fixed-window allocation against that bound with64KiB overhead reserve, and saving checks actual artifact size. This fits inside the existing128MiB total evidence cap with the original pair. The12MiB limit is an artifact-size bound, not peak RAM: the retained strip list and stacked output briefly coexist. There is no variable compression/overflow policy.
CPU checks:
```sh
python tests/validation/uniform_srt_diagnostic/test_diagnostic.py
python tests/validation/uniform_srt_diagnostic/test_history.py
python tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py --without-cylinder --record-inlet-history --output /tmp/new-inlet-history-preflight
```
After independent review and root GPU admission, from this exact test checkout, with a new output directory:
```sh
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s 240s python tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py --without-cylinder --record-inlet-history --execute --gpu-admitted --wall-seconds 180 --output output/new-srt-inlet-history
```
The homogeneous linear surrogate is a separate analysis artifact. This history will allow comparison with actual startup, including initialization mismatch and wall-junction propagation; neither the recorder nor the surrogate changes a boundary or establishes a corrective scheme. No GPU run is included in this commit.
+52
View File
@@ -0,0 +1,52 @@
# First-failure uniform SRT diagnostic (prepared, not executed)
Purpose: locate the first physical failure in the unchanged uniform D80 SRT control that was healthy at executed step 160 and nonfinite at the next observation, executed step 320. The original failure's summary `steps=160` is the last accepted observation, not the last executed step. This diagnostic does not select different numerical parameters, run an AMR case or perform a new benchmark campaign.
## Exact source and configuration
The default source is the containing repository (override with `--source`); the exact pinned production/runtime source is commit `9805383f9c50d867e8555471baf5dbda8333a652`. The manifest pins production files and the helper/runner dependencies directly against the original failure's `source_before.json`. All pinned contents match. Import guards require CelerisLab and the runner from the selected pinned checkout.
The script calls the original `run_refined_dfg.config(helper,80)` factory and unchanged `Simulation(...).initialize()`. Thus it retains D80, U=.01, ν=.008, τ=.524, ω≈1.9084, shifted FP32, SRT, double buffer, parabolic local Zou–He inlet, all-nine pressure-NEQ damping .5, backflow clamp disabled, identical cylinder geometry and initialization. No clipping, equilibrium rewrite or solver patch is added.
The inlet overload in this source may differ by FP32 rounding from older d79 code even where the algebra matches; that is not changed here and is not established as the cause of failure. MRT's outlet branch also differs. Neither prior MRT success nor this diagnostic establishes an SRT/outlet/body attribution.
## Observation and stop contract
- At most 320 actual solver steps, starting from the original fresh initialization. Stop at the first physical health failure (or wall deadline).
- Read the production macroscopic path, its decoded populations, and force after every completed step. Preserve the actual executed count separately from the last observed count.
- Independently report nonfinite physical populations, nonfinite physical macros and nonfinite reaction. Apply the original density [.5,1.5] and Mach≤.2 checks to physical FLUID nodes. A new population-only check may stop earlier than the original macro-only health check; the report explicitly distinguishes it.
- Per-step `Simulation.run(1)` resets/reads the observation buffer for a one-step force window. This changes observation cadence and force accumulation windows, not configuration or fluid updates. A cumulative-force-only overflow from the original 100/60-step batching would not be reproduced identically; that limitation is explicit. Original macro warnings already show nonfinite populations/macros, so this instrument prioritizes locating the physical failure.
- Ghost/inactive/body-solid populations are reported in separate categories; their NaNs do not independently trigger physical failure. Category masks are disjoint and complete. Boundary subtypes are decoded as the nibble, not mistaken for independent bit flags.
- Physical region labels are body-near (within radius+3 cells), outlet/inlet-near (three fluid columns), wall-near (three fluid rows), and remaining bulk. These locate symptoms, not causal mechanisms.
- Initial, preceding and firstbad/final compact reports retain category counts, finite global extrema and positions, the first 16 offending nodes in row-major order, their bad q indices/all populations, and up to four 5×5 population/flag neighborhoods. “First” identifies earliest sampled step; within that step the list is row-major, not an asserted temporal order among GPU threads.
## Bounded evidence
Only the immediately preceding and firstbad/final active-buffer states are retained. `states.npz` contains their exact raw shifted FP32 values, production-decoded FP32 populations and step numbers, plus flags. This is ~81 MiB at the exact D80 shape, with a 128 MiB artifact limit enforced. There are no saved field trajectories. Initial conditions are reproducible from the source/config; only their diagnostic summary is retained.
These are diagnostic snapshots, not full restart checkpoints (spare buffers and observation/control state are not a restart bundle). `diagnostics.json` stores nonfinite numbers as explicit `NaN`, `+Inf`, `-Inf` strings and is strict finite JSON; NPZ preserves actual IEEE values. A force-only failure can have no offending fluid node. A clean cap means no sampled failure through 320, not benchmark acceptance.
## CPU validation
```sh
python tests/validation/uniform_srt_diagnostic/test_diagnostic.py
python tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py --output /tmp/new-srt-preflight
```
Seven CPU-only tests pass: source/config, complete category partition, inactive nonfinite exclusion, independent population/macro/force reasons, health thresholds, bounded two-state artifact writing, and deadline/source guards. `py_compile` and the real-source default preflight pass. No CUDA import is required by preflight/tests and no GPU was executed here.
## Root-only admitted execution
After independent review and root GPU admission:
```sh
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s <hard-budget>s python tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py --source <exact-9805383-checkout> --execute --gpu-admitted --wall-seconds <soft-budget> --output <new-output-directory>
```
The external watchdog is required for a blocked CUDA call; cooperative wall checks cannot interrupt one. Budget should allow per-step downloads and ~81 MiB evidence writing; this diagnostic is not a performance measurement. The step ceiling cannot be increased by a CLI flag. `READY` is emitted after the first completed step and all observation paths. First physical failure is expected diagnostic evidence and returns exit 2; step cap returns 0. `preflight.json` records the pre-execution configuration, not a completion receipt.
Original failure files, production source, original pilot runner and gates are untouched. This test-only handoff is based on exact commit 9805383; added diagnostic files are not part of the historical failure source manifest.
## Historical reporting undercount (not changed here)
The failed pilot executed to step 320 before its second observation failed. Its summary `steps=160` reflects `begin`, which advances only after an accepted observation. This is a reporting undercount, not evidence that the second batch did not execute. The original failure package and runner/gates are preserved unchanged. This diagnostic records both `executed_steps` and `observation_step`, and labels previous-state steps explicitly. Fixing the historical runner's reporting is a separate change.
+6
View File
@@ -32,6 +32,7 @@ INLET_SCHEME_MAP = {
"channel_stabilized": 1,
"equilibrium": 2,
"regularized": 3,
"experimental_regularized_full9": 4,
}
OUTLET_MAP = {"neq_extrap": 0, "zero_gradient": 1, "blended": 2}
Y_WALL_BC_MAP = {"bounce_back": 0, "free_slip": 1, "on_site_velocity": 2}
@@ -162,6 +163,11 @@ class LBMConfig:
raise ValueError("wall_slot_width requires on_site_velocity and velocity=0")
if self.top_wall_bc == "reservoir" and (self.y_wall_bc != "on_site_velocity" or self.velocity != 0):
raise ValueError("reservoir requires on_site_velocity and velocity=0")
if self.inlet_scheme == "experimental_regularized_full9":
if (not self.is_d2q9 or self.collision != "SRT"
or self.streaming != "double_buffer" or self.store_precision != "FP32"
or self.les_enabled or self.y_wall_bc != "bounce_back"):
raise ValueError("experimental_regularized_full9 requires D2Q9/SRT/FP32/double_buffer, bounce_back walls, no LES")
if self.y_wall_bc == "on_site_velocity":
if (not self.is_d2q9 or self.collision != "SRT"
or self.streaming != "double_buffer" or self.store_precision != "FP32"
-33
View File
@@ -60,39 +60,6 @@ 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(
@@ -74,7 +74,7 @@ __device__ __forceinline__ float west_velocity_rho_closure_d3q19(
__device__ __forceinline__ bool inlet_scheme_uses_post_collision_ghost()
{
#if INLET_SCHEME == 0
#if INLET_SCHEME == 0 || INLET_SCHEME == 4
return true;
#else
return false;
@@ -0,0 +1,36 @@
// Experimental D2Q9 west velocity boundary: full-state stress regularization.
// Palabos boundaryTemplates::compute_PiNeq / regularized velocity completion.
// This returns PRE-collision populations. The existing step kernel collides once.
#ifndef CELERIS_EXPERIMENTAL_FULL9_INLET_CUH
#define CELERIS_EXPERIMENTAL_FULL9_INLET_CUH
#if LATTICE_MODEL == LATTICE_D2Q9
__device__ inline void apply_experimental_full9_inlet_d2q9(
float* __restrict__ f, float ux_target, float uy_target)
{
const float rho = west_velocity_rho_closure_d2q9(f, ux_target);
float eq[9];
compute_feq(rho, ux_target, uy_target, eq);
// Only known pulled directions are read; unknown wrapped q1/5/7 are ignored.
// f and eq use the same shifting convention, so weights cancel in K.
const float k2 = f[2] - eq[2];
const float k3 = f[3] - eq[3];
const float k4 = f[4] - eq[4];
const float k6 = f[6] - eq[6];
const float k8 = f[8] - eq[8];
const float pxx = 2.0f * (k2 + k6 + k8);
const float pyy = k3 + k4 + 2.0f * (k6 + k8);
const float pxy = 2.0f * (k6 - k8);
const int cx[9] = {0, 1,-1, 0, 0, 1,-1, 1,-1};
const int cy[9] = {0, 0, 0, 1,-1, 1,-1,-1, 1};
for (int q = 0; q < 9; ++q) {
const float weight = q == 0 ? W0_VAL : (q < 5 ? WS_VAL : WE_VAL);
const float stress = weight * (
4.5f * (((float)(cx[q]*cx[q]) - 1.0f/3.0f) * pxx
+ ((float)(cy[q]*cy[q]) - 1.0f/3.0f) * pyy)
+ 9.0f * (float)(cx[q]*cy[q]) * pxy);
f[q] = eq[q] + stress;
}
}
#endif
#endif
@@ -24,6 +24,7 @@
#include "inlet/channel_stabilized.cuh"
#include "inlet/equilibrium.cuh"
#include "inlet/regularized.cuh"
#include "inlet/experimental_full9.cuh"
#include "outlet/pressure_neq.cuh"
#if DIM == 2
@@ -45,6 +46,8 @@ __device__ __forceinline__ void apply_inlet_pull_d2q9(
#if INLET_SCHEME == 0
apply_zou_he_left_velocity_inlet_d2q9(f, u_target, v_target);
#elif INLET_SCHEME == 4
apply_experimental_full9_inlet_d2q9(f, u_target, v_target);
#elif INLET_SCHEME == 1 || INLET_SCHEME == 3
float f_neb[NQ];
const unsigned long k_neb = linear_index(x + 1u, y);
@@ -143,6 +146,8 @@ __device__ __forceinline__ void apply_inlet_esopull_d2q9(
#if INLET_SCHEME == 0
apply_zou_he_left_velocity_inlet_d2q9(f, u_target, v_target);
#elif INLET_SCHEME == 4
apply_experimental_full9_inlet_d2q9(f, u_target, v_target);
#elif INLET_SCHEME == 1 || INLET_SCHEME == 3
#if INLET_SCHEME == 1
apply_channel_stabilized_inlet_d2q9(f, f_neb, (float)y);
+3 -17
View File
@@ -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, projected_explosion_tables, _prepare_blocked_boundary,
blocked_kind, _prepare_blocked_boundary,
BLOCKED_BOUNDARY_DEVICE_COLUMNS)
from .descriptors import SOLID
from .raw_exchange import summarize_exchange
@@ -41,7 +41,6 @@ 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
@@ -217,7 +216,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, explosion="constant"):
observe_raw_exchange=False):
if HALO != 4:
raise RuntimeError("refinement.cu requires a four-cell fine halo")
if coarse_boundary not in ("periodic", "slot_reservoir", "channel"):
@@ -246,15 +245,6 @@ 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")
@@ -477,10 +467,6 @@ 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:
@@ -745,7 +731,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), *pair.interpolation,
np.uint64(pair.parent_cells),
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)
-34
View File
@@ -66,40 +66,6 @@ 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")
-204
View File
@@ -1,204 +0,0 @@
"""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)
+17
View File
@@ -0,0 +1,17 @@
# Frozen CPU-only cycle-assessor v2 plan
Predeclared before evaluating v2 acceptance on archived flows, 2026-10-18.
Cycle boundaries are every ascending lift Cl=0 crossing, found by linear interpolation between consecutive samples with Cl<0 then Cl>=0. Direction and zero level are fixed. No smoothing, optimized window, fitted phase, or alternative-method selection. Exact sampled zeros count once. Existing period2..5, lift amplitude>=.2, five recent consecutive cycles, period variation<=.005, extrema variation<=.005, mass/flux gates and official-reference tolerances remain unchanged.
Complete cycles are resampled on257 evenly spaced phase points with the existing linear interpolation, extrema and trapezoidal integration. Force timing remains interval midpoint; pressure/mass/flux use their end-sample times. Partial cycles are excluded; implausible periods are rejected, and missing cycles must still fail consecutiveness. No density, collision, geometry, initialization, outlet or flow-data change.
Official reference is rebuilt from the same pinned archive, lv6/dt4 line forces and pressure values, selecting its last complete ascending-crossing cycle. It uses the same v2 phase convention for scalar and waveform comparison. Original trough-phase reference and frozen422e549 assessment remain unchanged.
Qualification order:
1. Independent review of this definition and plan.
2. Synthetic constant-period sinusoidal/harmonic signals sampled at dt*=.02, with fixed deterministic small perturbations confined to extrema; false local extrema must not create cycles. Test exact-zero ties, zero offset phase, genuine period drift above.005, low amplitude, insufficient cycles, invalid/nonfinite/nonmonotone input, and missing/extra crossings. No fitted noise parameters.
3. Reanalyse official source waveform, archived full9 uniform data, and archived MRT uniform/refined data. Report every case and every gate, old and v2 side by side, including failures. No favorable-window selection.
4. Independent code/result review. Write versioned reanalysis receipts with input hashes, assessor hash/version, reference construction and gates. Do not overwrite original results or alter the original failed exit2.
Scope: DFG shedding signals with recurrent sign-changing lift. This does not define a universal cycle detector for signals without zero crossings. New acceptance is a separately identified measurement, never a claim that the historical frozen assessor passed.
+27
View File
@@ -0,0 +1,27 @@
# DFG ascending-zero assessor v2
This CPU-only measurement correction is separate from the frozen assessor. It changes cycle boundaries, not numerical flow data or any gate value. PLAN.md was committed at1ca3bb8 before archived v2 outcomes; the crossing algorithm was committed at2369949 before those evaluations. No flow run is included.
Every consecutive ascending Cl<0 to Cl>=0 crossing defines a candidate cycle. Times are linearly interpolated, with no suppression, smoothing or prominence choices. Fixed period/amplitude plausibility and all existing five-cycle, consecutive, recent, extrema, mass, flux and reference gates are reused. Invalid intervals are not reconnected across gaps. This definition is specific to sign-changing DFG shedding lift; no-zero or poorly sampled signals need another explicitly reviewed measurement, not silent fallback.
Cycle extrema and trapezoidal integrals retain the existing257-point phase grid. Force uses midpoint time, pressure/mass/flux endpoint time. The official raw lv6/dt4 reference is rebuilt in the same ascending phase, using its last complete cycle over the entire supplied shared time coverage. Original trough-phase reference files are untouched. Raw pressure columns7 minus12 are interpolated at force sample times; physical time is multiplied by10. Reference mass/flux placeholders are not measured conservation evidence.
`reanalyse.py` verifies the official archive hash, exact CSV equality to original summaries, and identical frozen gate outcomes. It reports any floating-point difference when replaying old cycle statistics instead of claiming bitwise identity across numerical library environments. All five supplied histories are reported, with their input/summary/code hashes, original identity, old and v2 results. Its new receipt is called reanalysis.json and cannot overwrite an existing output directory. It does not change the old run's exit code or accuracy_qualified field.
Tests include constant and noncommensurate/asymmetric signals, fixed small peak noise, explicit false local extrema, genuine period drift above the unchanged gate, low amplitude, insufficient cycles, zero plateaus, extra/missing crossings, missing sample points, invalid data and coverage. The algorithm is still vulnerable to sign noise and aliasing near a crossing; these tests are bounded validation, not universal robustness proof. Synthetic source conditions and perturbations are specified in the test file, not fitted to flow results.
Run tests:
```
python tests/validation/cycle_v2/test_periodic_v2.py
```
Reanalyse with every archived case named explicitly:
```
python tests/validation/cycle_v2/reanalyse.py --official-archive <pinned-official.zip> --history full9=<full9-history.csv> --history mrt_uniform=<mrt-uniform-history.csv> --history mrt_refined=<mrt-refined-history.csv> --output <new-directory>
```
Each history needs its original summary.json alongside it. Multiple observer variants may be listed separately; duplicate refined histories are not independent physical replications. Historical data and thresholds remain untouched. Any future production-run use requires explicit integration/review; this module does not alter default assessors.
Frozen replay integrity permits at most1e-10 absolute floating difference per stored cycle number, with identical schema, counts and gate decisions. This is a CPU replay check only, not a change to physical accuracy or convergence thresholds. Actual archived uniform MRT differences were at most1.081e-13; other replayed cycle statistics matched exactly.
+102
View File
@@ -0,0 +1,102 @@
"""Versioned ascending-zero DFG measurement; original data and gates unchanged."""
import numpy as np
from periodic import GATES, REFERENCE_GATES, compare
VERSION = "dfg-ascending-zero-v2"
def ascending_crossings(t, y):
t = np.asarray(t, dtype=float); y = np.asarray(y, dtype=float)
if t.ndim != 1 or y.shape != t.shape or not np.isfinite(t).all() or not np.isfinite(y).all():
raise ValueError("Finite one-dimensional time/signal arrays required")
if np.any(np.diff(t) <= 0):
raise ValueError("Strictly increasing times required")
i = np.flatnonzero((y[:-1] < 0) & (y[1:] >= 0))
return (t[i] - y[i] * (t[i+1] - t[i]) / (y[i+1] - y[i])).tolist()
def validate_rows(rows):
keys = ("tstar", "tstar_force", "cd", "cl", "dp", "mass", "flux_in", "flux_out")
values = np.asarray([[row[k] for k in keys] for row in rows], dtype=float)
if not np.isfinite(values).all():
raise ValueError("Nonfinite observation")
if len(rows) and (np.any(np.diff(values[:, 0]) <= 0) or np.any(np.diff(values[:, 1]) <= 0)):
raise ValueError("Strictly increasing observation times required")
def extract(rows, min_time=250.0):
validate_rows(rows)
if len(rows) < 3:
return []
t = np.array([r["tstar_force"] for r in rows])
cl = np.array([r["cl"] for r in rows])
ends = ascending_crossings(t, cl)
cycles = []
for a, b in zip(ends, ends[1:]):
if a < min_time or not GATES["min_period"] < b - a < GATES["max_period"]:
continue
grid = np.linspace(a, b, 257)
c = dict(start=a, end=b, period=b - a, st=1 / (b - a))
for key in ("cd", "cl", "dp", "mass", "flux_in", "flux_out"):
times = np.array(
[r["tstar" if key not in ("cd", "cl") else "tstar_force"] for r in rows]
)
# No endpoint extrapolation: pressure/mass must cover the whole cycle.
if a < times[0] or b > times[-1]:
break
v = np.interp(grid, times, [r[key] for r in rows])
c[key + "_min"] = float(v.min())
c[key + "_max"] = float(v.max())
c[key + "_midrange"] = float((v.min() + v.max()) / 2)
c[key + "_amplitude"] = float(v.max() - v.min())
c[key + "_time_mean"] = float(
np.sum((v[1:] + v[:-1]) * np.diff(grid) / 2) / (b - a)
)
if key in ("cd", "cl", "dp"):
c[key + "_waveform"] = v.tolist()
else:
c["flux_relative"] = abs(
c["flux_in_time_mean"] - c["flux_out_time_mean"]
) / max(abs(c["flux_in_time_mean"]), 1e-30)
cycles.append(c)
return cycles
def assess(rows):
cycles = extract(rows, min_time=GATES["min_tstar"])
tail = cycles[-GATES["cycles"] :]
checks = {}
checks["enough_cycles"] = len(tail) == GATES["cycles"]
if checks["enough_cycles"]:
checks["consecutive"] = all(
abs(a["end"] - b["start"]) < 1e-8 for a, b in zip(tail, tail[1:])
)
checks["recent"] = (
0 <= rows[-1]["tstar_force"] - tail[-1]["end"] <= GATES["max_period"]
)
checks["shedding"] = (
min(c["cl_amplitude"] for c in tail) >= GATES["min_lift_amplitude"]
)
periods = [c["period"] for c in tail]
checks["period"] = (
np.ptp(periods) / np.mean(periods) <= GATES["period_relative_range"]
)
checks["extrema"] = all(
np.ptp([c[k + "_" + e] for c in tail]) <= GATES["extrema_absolute_range"]
for k in ("cd", "cl", "dp")
for e in ("min", "max")
)
mass = [c["mass_time_mean"] for c in tail]
checks["mass"] = (
np.ptp(mass) / abs(np.mean(mass)) <= GATES["mass_relative_range"]
)
checks["flux"] = (
max(c["flux_relative"] for c in tail) <= GATES["cycle_mean_flux_relative"]
)
return dict(
periodic=bool(checks["enough_cycles"] and all(checks.values())),
checks={k: bool(v) for k, v in checks.items()},
cycles=cycles,
)
+112
View File
@@ -0,0 +1,112 @@
"""Versioned read-only DFG reanalysis; never overwrites a flow receipt."""
import argparse
import csv
import hashlib
import io
import json
from pathlib import Path
import sys
import zipfile
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import periodic as frozen
import periodic_v2 as v2
OFFICIAL_SHA256 = '08db59605b2f60efc512d2bbc79628ea5d1cc720c0986938a16920884a0215be'
PREFIX = 'logs/results_cc2d_bench2_referencenew/'
def sha(path): return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def official_reference(path):
if sha(path) != OFFICIAL_SHA256: raise ValueError('Wrong official archive')
with zipfile.ZipFile(path) as z:
force = np.loadtxt(io.BytesIO(z.read(PREFIX+'bdforces_q2_lv6_dt4')))
pressure = np.loadtxt(io.BytesIO(z.read(PREFIX+'pointvalues_q2_lv6_dt4')))
tf=force[:,1]*10; tp=pressure[:,1]*10
if not np.isfinite(force).all() or not np.isfinite(pressure).all():
raise ValueError('Nonfinite official input')
if np.any(np.diff(tf)<=0) or np.any(np.diff(tp)<=0): raise ValueError('Official time order')
# Entire supplied interval, restricted only to shared force/pressure coverage.
mask=(tf>=tp[0])&(tf<=tp[-1]); force=force[mask]; tf=tf[mask]
dp=np.interp(tf,tp,pressure[:,6]-pressure[:,11])
rows=[dict(tstar=float(t),tstar_force=float(t),cd=float(f[3]),cl=float(f[4]),dp=float(p),
mass=1.,flux_in=1.,flux_out=1.) for t,f,p in zip(tf,force,dp)]
cycles=v2.extract(rows,min_time=0.)
if not cycles: raise ValueError('No complete official cycle')
ref=cycles[-1]
return ref,dict(archive_sha256=OFFICIAL_SHA256,selection='lv6/dt4 last complete ascending Cl=0 cycle in entire shared input coverage',
eligible_time=[float(tf[0]),float(tf[-1])],complete_cycles=len(cycles),
pressure='columns7 minus12, interpolated at original force times; physical time multiplied by10',
artificial_fields='mass/flux placeholders only; official archive is not used to certify conservation')
def frozen_replay_difference(current_cycles, archived_cycles):
"""Integrity-only FP replay allowance, not a physical acceptance threshold."""
if len(current_cycles) != len(archived_cycles):
raise ValueError('Frozen cycle count differs from original')
deltas=[]
for current, archived in zip(current_cycles, archived_cycles):
if current.keys()!=archived.keys(): raise ValueError('Frozen cycle schema differs')
for key in current:
x=np.asarray(current[key]); y=np.asarray(archived[key])
if x.shape!=y.shape or not np.isfinite(x).all() or not np.isfinite(y).all():
raise ValueError('Frozen cycle shape or finite-state mismatch')
deltas.append(float(np.max(np.abs(x-y))))
difference=max(deltas,default=0.)
if difference>1e-10: raise ValueError('Frozen replay exceeds FP integrity allowance')
return difference
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--official-archive',type=Path,required=True)
parser.add_argument('--history',action='append',required=True,help='Unique label=CSV path; all cases reported')
parser.add_argument('--output',type=Path,required=True)
a=parser.parse_args()
if a.output.exists(): raise ValueError('Use a new output directory')
cases=[]; labels=set()
reference,reference_info=official_reference(a.official_archive)
old_ref_path=Path(frozen.__file__).parent/'data/dfg_re100/reference_cycle.json'
old_ref=json.loads(old_ref_path.read_text())
for item in a.history:
label,path=item.split('=',1); path=Path(path)
if label in labels: raise ValueError('Duplicate case label')
labels.add(label)
rows=[{k:float(v) for k,v in r.items()} for r in csv.DictReader(path.open())]
source_receipt_path=path.with_name('summary.json')
source_receipt=json.loads(source_receipt_path.read_text())
if rows != source_receipt.get('history'):
raise ValueError('CSV differs from original summary history')
v2.validate_rows(rows)
old=frozen.assess(rows); new=v2.assess(rows)
for key in ('periodic', 'checks'):
if old[key] != source_receipt.get(key):
raise ValueError('Frozen reassessment differs from original summary')
replay_difference=frozen_replay_difference(old['cycles'],source_receipt['cycles'])
old_comparison=frozen.compare(old['cycles'][-1],old_ref) if old['cycles'] else None
new_comparison=v2.compare(new['cycles'][-1],reference) if new['cycles'] else None
# This is assessment of recorded observables, not proof of unrecorded population health.
healthy=bool(rows and all(.5<=r['rho_min']<=r['rho_max']<=1.5 and 0<=r['max_mach']<=.2 for r in rows))
cases.append(dict(label=label,input_csv_sha256=sha(path),
original_summary_sha256=sha(source_receipt_path),
frozen_cycle_max_absolute_replay_difference=replay_difference,
original_identity={k:source_receipt[k] for k in ('case','collision','observation_mode','numerical_source_commit','checkout','checkout_revision','mode','status') if k in source_receipt},
observations=len(rows),
recorded_end_tstar=rows[-1]['tstar'] if rows else None,recorded_observations_healthy=healthy,
frozen=old,frozen_reference=old_comparison,v2=new,v2_reference=new_comparison,
v2_recorded_observable_agreement=bool(healthy and new['periodic'] and new_comparison and new_comparison['reference_agreement'])))
a.output.mkdir(parents=True)
result=dict(assessment_version=v2.VERSION,original_receipts_unchanged=True,
phase='Every ascending Cl<0 to Cl>=0 crossing; linear interpolation, no suppression',
gates=v2.GATES,reference_gates=v2.REFERENCE_GATES,reference=reference,reference_provenance=reference_info,
code_sha256={Path(p).name:sha(p) for p in [__file__,v2.__file__,frozen.__file__]},
frozen_reference_sha256=sha(old_ref_path),cases=cases)
(a.output/'reanalysis.json').write_text(json.dumps(result,indent=2,allow_nan=False)+'\n')
print(json.dumps([dict(case=c['label'],old_periodic=c['frozen']['periodic'],
v2_checks=c['v2']['checks'],v2_reference=c['v2_reference'],
recorded_observable_agreement=c['v2_recorded_observable_agreement']) for c in cases],indent=2))
if __name__=='__main__': main()
@@ -0,0 +1,108 @@
"""Predeclared synthetic measurement tests; no flow outcome is used for tuning."""
from pathlib import Path
import sys
import unittest
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import periodic
import periodic_v2 as v2
def signal(periods=None, noise=0., offset=0., amplitude=1., irregular=False, harmonic=0.):
periods = np.full(20, 3.3) if periods is None else np.asarray(periods)
boundaries = 250.37 + np.r_[0., np.cumsum(periods)]
t = np.arange(boundaries[0]-.1, boundaries[-1]+.1, .02)
if irregular: t = t[np.arange(len(t)) % 7 != 0]
phase = np.interp(t, boundaries, np.arange(len(boundaries))) * 2*np.pi
lift = amplitude*(np.sin(phase) + harmonic*np.sin(2*phase)) + offset
# Fixed seed and fixed peak-local mask; not fitted to any flow dataset.
lift += noise*np.random.default_rng(1729).uniform(-1,1,len(t))*(abs(np.sin(phase))>.9)
return [dict(tstar=x+.01, tstar_force=x, cl=l,
cd=3.2+.03*np.cos(2*p), dp=2.45+.05*np.cos(p),
mass=1., flux_in=1., flux_out=1.) for x,l,p in zip(t,lift,phase)]
def test_constant_period_peak_noise_and_mean_offset():
for noise, offset in [(0.,0.),(.0001,0.),(.001,0.),(.001,.08)]:
result=v2.assess(signal(noise=noise,offset=offset))
assert result['periodic'], result['checks']
periods=np.array([c['period'] for c in result['cycles'][-5:]])
assert max(abs(periods-3.3))<1e-5
def test_noncommensurate_asymmetric_waveform():
result=v2.assess(signal(periods=[3.313]*20,harmonic=.1,offset=.08,noise=.001))
assert result['periodic'], result['checks']
periods=np.array([c['period'] for c in result['cycles'][-5:]])
assert max(abs(periods-3.313))<5e-5
def test_false_local_extrema_do_not_split_cycles():
rows=signal()
y=np.array([r['cl'] for r in rows])
# A fixed .002 dip at every positive peak creates a local minimum while
# preserving all zero crossings. This is the predeclared false-extremum case.
peaks=np.flatnonzero((y[1:-1]>y[:-2]) & (y[1:-1]>=y[2:]) & (y[1:-1]>.9))+1
for i in peaks: rows[i]['cl']-=.002
assert v2.assess(rows)['periodic']
assert not periodic.assess(rows)['periodic']
def test_true_drift_is_rejected():
result=v2.assess(signal(periods=np.arange(20)*.01+3.2))
assert not result['periodic'] and not result['checks']['period']
measured=np.array([c['period'] for c in result['cycles'][-5:]])
assert np.ptp(measured)/np.mean(measured)>.005
def test_insufficient_cycles_and_amplitude():
assert not v2.assess(signal(periods=[3.3]*4))['periodic']
result=v2.assess(signal(amplitude=.05))
assert not result['periodic'] and not result['checks']['shedding']
def test_exact_zero_plateau_and_crossing_noise():
assert v2.ascending_crossings([0,1,2,3,4,5],[-1,0,0,1,-1,0])==[1.,5.]
# All crossings retained, even a short sign reversal: no hidden debounce.
assert len(v2.ascending_crossings([0,1,2,3,4,5],[-1,.001,-.001,.001,1,2]))==2
assert v2.ascending_crossings([0,1,2],[1,.1,2])==[]
def test_extra_and_missing_cycles_not_reconnected():
for last in [[3.3,.2,3.1,3.3],[3.3,6.6,3.3,3.3]]:
result=v2.assess(signal(periods=[3.3]*12+last))
assert not result['periodic'] and not result['checks']['consecutive']
def test_invalid_inputs_and_irregular_sampling():
for t,y in [([0,0],[-1,1]),([1,0],[-1,1]),([0,1],[-1,float('nan')])]:
with unittest.TestCase().assertRaises(ValueError):v2.ascending_crossings(t,y)
rows=signal();rows[1]['dp']=float('inf')
with unittest.TestCase().assertRaises(ValueError):v2.assess(rows)
assert v2.assess(signal(irregular=True))['periodic']
def test_coverage_and_unchanged_gates():
rows=signal()
cycles=v2.extract(rows,min_time=250.)
# Move end-sample coverage late; force remains unchanged. Partial cycles excluded.
delayed=[dict(r,tstar=r['tstar']+10.) for r in rows]
other=v2.extract(delayed,min_time=250.)
assert len(other)<len(cycles) and all(c['start']>=delayed[0]['tstar'] for c in other)
assert v2.GATES is periodic.GATES and v2.REFERENCE_GATES is periodic.REFERENCE_GATES
assert v2.compare is periodic.compare
def test_frozen_replay_integrity_allowance():
from reanalyse import frozen_replay_difference
baseline=[dict(period=3.3,wave=[0.,1.,0.])]
assert frozen_replay_difference(baseline,[dict(period=3.3+1e-13,wave=[0.,1.,0.])])<1e-10
for bad in [[dict(period=3.3+1e-6,wave=[0.,1.,0.])],
[dict(period=float('nan'),wave=[0.,1.,0.])],[]]:
with unittest.TestCase().assertRaises(ValueError): frozen_replay_difference(baseline,bad)
if __name__=='__main__':
result=unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite(
unittest.FunctionTestCase(fn) for name,fn in sorted(globals().items()) if name.startswith('test_')))
raise SystemExit(not result.wasSuccessful())
@@ -0,0 +1,43 @@
# Uniform full-nine DFG qualification
This is a separate full-run entrypoint. The pilot wrapper, pilot manifest, shared observer, reference data, gates and all production files remain byte-identical to c0be841. Numerical source remains 926ca27407c26ca11283282a6e53249925725376; actual execution HEAD is recorded separately. A new full manifest pins the added wrapper and the original pilot manifest.
## Admission and case
Require the completed exact c0be841 uniform SRT/full9 cylinder pilot directory, including result.json, preflight.json and uniform/ready.json. Admission checks finite complete pilot history, collision/inlet/case/observation/source identity, physical configuration, body, parameters, frozen gates, READY, clean checkout and positive timings. Bodyless, MRT, refined, stale-source or malformed receipts cannot admit this run.
Restart from the unchanged original production initialization, not a pilot continuation. Keep D80, lattice mean U=.01, viscosity=.008, tau=.524, shifted FP32 double buffer, cylinder/walls/domain, no LES/force, and pressure-NEQ outlet all-nine damping .5. Only the already tested experimental inlet differs from the historical baseline.
Maximum 2,400,000 steps to t*=300. Reuse the existing observer: t*=.5 intervals before240, then .02 through300. All force batches are included; force time is the interval midpoint, pressure time its endpoint. Early calibration remains accounted in the physical trajectory. Sparse health checks identify first observed failure, not exact onset. Timeout retains progress.
Success requires the full physical cap, healthy completion, frozen periodic convergence AND official-reference agreement. Healthy completion alone or periodicity alone returns failure. The project tolerances remain unchanged; the reference data are official DFG data. This does not qualify a canonical do-nothing outlet, grid convergence, AMR, MRT, LES or other precision paths.
## Cost and bounds
Measured pilot startup: 3.3353 s. Step plus force: 3.6794 s for 40,000 steps. Linear step-work extrapolation is 220.764 s, before additional late-time dense observations and altered batch subdivision. The full observer has 3,480 intervals (480 sparse and 3,000 dense), compared with 10 pilot intervals. Prior same-size MRT full runs took 354–373 s total. Expect several minutes, with timing reported rather than promised.
Internal cap: 720 s maximum. Outer watchdog: 900 s, then 30 s termination grace. The wrapper rejects a larger internal cap. Expiry preserves an incomplete result and does not qualify accuracy. No GPU execution is performed during preparation.
## Commands
CPU tests:
```
python tests/validation/experimental_full9/test_dfg_full.py
python tests/validation/experimental_full9/test_dfg.py
python tests/validation/experimental_full9/test_full9.py
```
Preflight against the actual retained pilot directory:
```
python tests/validation/experimental_full9/run_dfg_full.py --pilot-results <actual-c0be841-pilot-directory> --output <new-preflight-directory>
```
After independent review and explicit full-run admission:
```
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s 900s python tests/validation/experimental_full9/run_dfg_full.py --pilot-results <actual-c0be841-pilot-directory> --execute --gpu-admitted --wall-seconds 720 --output output/new-full9-dfg-full
```
The four admission tests use clearly labeled synthetic receipts. They are host regression fixtures, not measured pilot evidence. Runtime admission uses the actual retained pilot receipts.
@@ -0,0 +1,26 @@
# Experimental uniform DFG pilot
Numerical source remains926ca27407c26ca11283282a6e53249925725376. This test-only followup uses a separate dfg_source_manifest.json, pins the official reference-cycle file, and records actual execution HEAD independently. The historical bodyless manifest is unchanged and correctly rejects this checkout because the shared runner gained optional hooks. Use historical939c10c for that historical entrypoint; do not repin old evidence.
The new run_dfg.py is uniform-only, with the original D80 cylinder, SRT tau.524, shiftedFP32 double buffer, original initialization, noLES/force, pressure-NEQ outlet all-nine damping.5. Only inlet scheme is experimental_regularized_full9. It reuses run_refined_dfg.run_case and the existing pressure, force, physical mass/flux and periodic/reference helpers. Legacy run_case callers retain the original inlet and dense pilot cadence.
This pilot advances40,000steps to t*=5 with sparse observations every4000steps (t*=.5), matching the standard pre240 cadence. Every100-step batch contributes its full momentum impulse; force time is the interval midpoint, pressure time its endpoint. First100steps provide warmup; the next four100-step batches measure warmed step+force cost. All500steps are included in the physical trajectory and observation impulse; no reset or discarded evolution. Calibration durations and total step/force/startup/wall timings are recorded. Batch force download synchronizes GPU work through the existing API.
A first unhealthy sample identifies only the first observed failure, not its onset. Receipts separate executed_steps, last_observed_step and the existing steps field; timeout retains available progress, history and calibration. No per-step field download or new health threshold is introduced. Runtime admission uses180seconds internally, with an external240second watchdog. Later full-run admission must use measured batched timings, not the earlier per-step snapshot diagnostic rate. Same-size historical MRT full campaigns took roughly354–373seconds, but this pilot measures the present SRT/full9 case.
CPU checks from this checkout:
```
python tests/validation/experimental_full9/test_dfg.py
python tests/validation/experimental_full9/test_full9.py
python -m py_compile tests/validation/experimental_full9/run_dfg.py tests/validation/run_refined_dfg.py
python tests/validation/experimental_full9/run_dfg.py --output /tmp/new-full9-dfg-preflight
```
After independent review and explicit GPU admission only:
```
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s 240s python tests/validation/experimental_full9/run_dfg.py --execute --gpu-admitted --wall-seconds 180 --output output/new-full9-dfg-pilot
```
This entrypoint intentionally has no full-run execution option. Existing official-reference assessment is reused and accuracy_pass requires health, periodicity and reference agreement for future qualification; pilot accuracy_qualified is alwaysfalse. A healthy pilot qualifies bounded admission/timing only. No grid convergence, canonical outlet, refined/MRT/other precision, long-time accuracy or general stability claim follows.
@@ -0,0 +1,38 @@
# Isolated experimental full-nine inlet comparison
This is preparation for one separately admitted, at-most320-step bodyless robustness comparison. It is not public support, a default change, proof of root cause, or boundary-accuracy acceptance.
## Source and isolation
Numerical commit: `926ca27407c26ca11283282a6e53249925725376`, based on history diagnostic `e92de3742476ea5afd6b84ff9538992df48548f7`. The new manifest pins numerical files and separately hashes the two runtime harness files. Actual checkout HEAD/dirty state is recorded on execution; --source must resolve to the executing harness checkout. The historical manifest is unchanged and intentionally rejects the new numerical tree. The original Zou–He default and IDs0..3 remain unchanged.
Explicit scheme `experimental_regularized_full9` is ID4. Configuration guards restrict it to D2Q9, SRT, double-buffer, FP32 storage, bounce-back y walls, and no LES; shifted and unshifted storage are tested. The bodyless runner uses exactly the old config factory, changes only inlet scheme, and removes only the original cylinder. Original initialization, profile, boundary planes, outlet/all-nine damping, viscosity, timestep and health gates remain unchanged.
The new helper computes rho from the existing west known-population closure. At the prescribed velocity, known Kq=Fq−Feq gives Pi_xx=2(K2+K6+K8), Pi_yy=K3+K4+2(K6+K8), Pi_xy=2(K6−K8). It reconstructs all nine pre-collision populations from equilibrium plus the second-order stress projection. The existing step kernel then applies SRT exactly once. Old-buffer timing is unchanged. This preserves prescribed physical velocity moments while intentionally changing the boundary's kinetic reconstruction; it is not numerically identical to Zou–He.
Primary mapping: [Palabos stress construction](https://unigespc.gitlab.io/palabos/docs/html/d0/dfa/boundary_templates_8h_source.html), [velocity completion before base collision](https://unigespc.gitlab.io/palabos/docs/html/d2/d68/regularized_boundary_dynamics_8hh_source.html), [Latt–Chopard equations10–12](https://arxiv.org/pdf/physics/0506157). This independently implemented mapping does not use Celeris's differently defined existing incoming-only donor-damped `regularized` option.
Near-wall inlet known inputs are unchanged. This comparison includes their existing interaction; it does not silently move walls or repair initialization. An EsoPull dispatch arm is present because that function is preprocessed even in double-buffer builds; the configuration guard rejects running that streaming variant.
## Tests and bounded analytical check
```sh
python tests/validation/experimental_full9/test_full9.py
python tests/validation/experimental_full9/test_runner.py
OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 python tests/validation/experimental_full9/check_linear_strip.py
python tests/validation/experimental_full9/run_bodyless.py --record-inlet-history --output /tmp/new-full9-preflight
```
The host tests compile the actual helper and canonical macro/SRT headers with g++, in shifted/unshifted modes. They check equilibrium, mass/momentum, prescribed stress, ignored wrapped q1/5/7, exactly-one collision, source ordering, legacy IDs/defaults and rejected configurations. These tests do not certify CUDA compilation.
The fixed homogeneous linear comparison uses the original five k samples, width32, rho1, U=.0028664576821029186, tau=.524 and zero far incoming perturbation. The prior3π/4 Zou–He radius1.0186489 becomes .9802484 with full9; all five sampled full9 radii are below1. This is a finite sampled surrogate result, not a continuous stability proof or a match to the actual dominant≈2π/3 history spectrum. No parameter tuning or new flow is part of this check.
## Exact admitted command
From the final clean reviewed checkout, after explicit GPU admission:
```sh
CUDA_VISIBLE_DEVICES=<admitted-device> timeout --signal=TERM --kill-after=30s 240s python tests/validation/experimental_full9/run_bodyless.py --record-inlet-history --execute --gpu-admitted --wall-seconds 180 --output output/new-experimental-full9-bodyless
```
The shared diagnostic still stops on its unchanged first-health/320-step/wall ceiling. Optional raw history records the fixed0..196 window only, with the existing12MiB bound and completeness receipt; evolution can continue beyond196. Use a new output directory and preserve old evidence.
A healthy320-step result would support only bounded robustness to this exact startup. Failure remains evidence, not a reason to tune the scheme after seeing results. Compare step-count/health, finite fields, density/Mach, inlet target moments and retained modal history; conservation and long-time/cylinder accuracy require later separate tests. No default/dev integration is authorized by this experiment.
@@ -0,0 +1,31 @@
"""Bounded fixed-parameter linear comparison; no nonlinear flow or GPU run."""
import json
from pathlib import Path
import numpy as np
from scipy.linalg import eigvals
cx=np.array([0,1,-1,0,0,1,-1,1,-1]);cy=np.array([0,0,0,1,-1,1,-1,-1,1]);w=np.array([4/9]+[1/9]*4+[1/36]*4)
U=.0028664576821029186;omega=1/.524;cu=cx*U
base=w*(1+3*cu+4.5*cu**2-1.5*U**2)
E=w[:,None]*((1+3*cu+4.5*cu**2-1.5*U*U)[:,None]+(3*cx+9*cu*cx-3*U)[:,None]*(cx-U)[None,:]+(3*cy+9*cu*cy)[:,None]*cy[None,:])
C=(1-omega)*np.eye(9)+omega*E
r=np.array([1,0,2,1,1,0,2,0,2])/(1-U);I=np.eye(9)
B=I.copy();B[1]=I[2]+2/3*U*r;B[5]=I[6]+.5*(I[4]-I[3])+U/6*r;B[7]=I[8]+.5*(I[3]-I[4])+U/6*r
T=np.array([[0,0,2,0,0,0,2,0,2],[0,0,0,1,1,0,2,0,2],[0,0,0,0,0,0,2,0,-2]])
R=w[:,None]*np.array([4.5*(cx*cx-1/3),4.5*(cy*cy-1/3),9*cx*cy]).T
Bfull=base[:,None]*r+R@T@(I-base[:,None]*r)
assert np.max(abs(Bfull[:,[1,5,7]]))<1e-15
assert np.max(abs(np.array([np.ones(9),cx,cy])@Bfull-np.array([r,U*r,np.zeros(9)])))<1e-14
def radius(n,k,b):
A=np.zeros((9*n,9*n),complex)
for x in range(n):
local=C@b if x==0 else C
for q in range(9):
donor=x-int(cx[q])
if 0<=donor<n:A[9*x:9*x+9,9*donor+q]+=local[:,q]*np.exp(-1j*k*cy[q])
return float(np.max(abs(eigvals(A,check_finite=False))))
def main():
rows=[dict(k_over_pi=k/np.pi,zou_he=radius(32,k,B),experimental_full9=radius(32,k,Bfull)) for k in [0,np.pi/4,np.pi/2,3*np.pi/4,np.pi]]
print(json.dumps(dict(scope='Width32, zero incoming far perturbation, five original fixed k samples; negative samples do not prove stability',U=U,tau=.524,rows=rows),indent=2,allow_nan=False))
if __name__=='__main__':main()
@@ -0,0 +1,123 @@
{
"commit": "926ca27407c26ca11283282a6e53249925725376",
"historical_numerical_source_commit": "9805383f9c50d867e8555471baf5dbda8333a652",
"variant": "experimental_regularized_full9",
"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": "215b09829e10453b333aae69811ffc638c7aa26ae418f25f8654a1a20a74f212",
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
"src/CelerisLab/lbm/channel.py": "2d5b5ff43414cb35633ac4ce90ce2c3a3353950b25c168e1c2ab58d498dd8b0f",
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
"src/CelerisLab/lbm/kernels/blocks.cu": "e3b9606f861bfea8c4b0f34e8356bb230081f899c584f67be89178d889099b17",
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "1203739c346cb4986e4ecb3090a70114eecc1688ae886d5abfcdb643be256acb",
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
"src/CelerisLab/lbm/kernels/boundary/inlet/experimental_full9.cuh": "f2475d6322f060b256f37d81e43d6aa3a1918e2eac83c076c32c2599b674f596",
"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": "4955786adc6d0853f2a9703611ea76e58ef045d6d6f4295bd795b05dc2f00dcd",
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
"src/CelerisLab/lbm/kernels/refinement.cu": "129f3d91da93b4324f88a0da73a4da1e5390f72c14cce574ee63306712da9895",
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "befbf6d757e532a59c166d41f920d16d71557cd3a574478e7aa83da737c48cbd",
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
"src/CelerisLab/lbm/raw_exchange.py": "8694202466eba848563e298656353b0f5db94e21a01fa05f51aad6dc2b9fd311",
"src/CelerisLab/lbm/refinement.py": "08c2a0557276a98cbcaa459d9bb7a9735d8bbbf64a6114c57af2fda61481a687",
"src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d",
"src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6",
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c",
"tests/validation/data/refined_dfg/source_manifest.json": "e495948b10f8112c77ca67255122b5e8bc9e32570e8c899802afcb85e608fef4",
"tests/validation/periodic.py": "a2212c0e2705126327a5019e9cafe64eeca398da1b75061ff23630c9843697a6",
"tests/validation/re100_helpers.py": "289c6e01075ff361573f862b90cfd61e5980c9a8efcbc78aef69979b4004fd60",
"tests/validation/run_refined_dfg.py": "992169a38ab16ab20a9b9bb4a29853cafe1d6bc07adba0df435a104be1de7b68",
"tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c"
},
"runtime_sha256": {
"tests/validation/experimental_full9/run_dfg.py": "400f6ff373623e5312c8dfae3cb8dc0172360e40feb2a9780443ec7f7ded6eea",
"tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py": "56b9ef786001c7760abab8d870f6437d719b427c21964f00067188f5fb27caad",
"tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c",
"tests/validation/data/dfg_re100/reference_cycle.json": "073d1bda1629697efebad95667bfe80598e217d708dbffd7d5f6c6999379c55f",
"tests/validation/experimental_full9/run_dfg_full.py": "b82f3fcab73f20ce86394a4de80022022434215b3e07fa9aaf5cbd8118413ef1",
"tests/validation/experimental_full9/dfg_source_manifest.json": "f5b7ebfc136555f5cdfebd95b886bd8263491f27b8abd723bbe76c9b1aeba476"
},
"experiment": "uniform_full9_dfg_full_qualification"
}
@@ -0,0 +1,121 @@
{
"commit": "926ca27407c26ca11283282a6e53249925725376",
"historical_numerical_source_commit": "9805383f9c50d867e8555471baf5dbda8333a652",
"variant": "experimental_regularized_full9",
"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": "215b09829e10453b333aae69811ffc638c7aa26ae418f25f8654a1a20a74f212",
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
"src/CelerisLab/lbm/channel.py": "2d5b5ff43414cb35633ac4ce90ce2c3a3353950b25c168e1c2ab58d498dd8b0f",
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
"src/CelerisLab/lbm/kernels/blocks.cu": "e3b9606f861bfea8c4b0f34e8356bb230081f899c584f67be89178d889099b17",
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "1203739c346cb4986e4ecb3090a70114eecc1688ae886d5abfcdb643be256acb",
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
"src/CelerisLab/lbm/kernels/boundary/inlet/experimental_full9.cuh": "f2475d6322f060b256f37d81e43d6aa3a1918e2eac83c076c32c2599b674f596",
"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": "4955786adc6d0853f2a9703611ea76e58ef045d6d6f4295bd795b05dc2f00dcd",
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
"src/CelerisLab/lbm/kernels/refinement.cu": "129f3d91da93b4324f88a0da73a4da1e5390f72c14cce574ee63306712da9895",
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "befbf6d757e532a59c166d41f920d16d71557cd3a574478e7aa83da737c48cbd",
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
"src/CelerisLab/lbm/raw_exchange.py": "8694202466eba848563e298656353b0f5db94e21a01fa05f51aad6dc2b9fd311",
"src/CelerisLab/lbm/refinement.py": "08c2a0557276a98cbcaa459d9bb7a9735d8bbbf64a6114c57af2fda61481a687",
"src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d",
"src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6",
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c",
"tests/validation/data/refined_dfg/source_manifest.json": "e495948b10f8112c77ca67255122b5e8bc9e32570e8c899802afcb85e608fef4",
"tests/validation/periodic.py": "a2212c0e2705126327a5019e9cafe64eeca398da1b75061ff23630c9843697a6",
"tests/validation/re100_helpers.py": "289c6e01075ff361573f862b90cfd61e5980c9a8efcbc78aef69979b4004fd60",
"tests/validation/run_refined_dfg.py": "992169a38ab16ab20a9b9bb4a29853cafe1d6bc07adba0df435a104be1de7b68",
"tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c"
},
"runtime_sha256": {
"tests/validation/experimental_full9/run_dfg.py": "400f6ff373623e5312c8dfae3cb8dc0172360e40feb2a9780443ec7f7ded6eea",
"tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py": "56b9ef786001c7760abab8d870f6437d719b427c21964f00067188f5fb27caad",
"tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c",
"tests/validation/data/dfg_re100/reference_cycle.json": "073d1bda1629697efebad95667bfe80598e217d708dbffd7d5f6c6999379c55f"
},
"experiment": "uniform_full9_dfg_sparse_pilot"
}
@@ -0,0 +1,58 @@
"""Explicit experimental full9 inlet, exact original bodyless SRT control otherwise.
CPU preflight by default. No acceptance claim; requires separate GPU admission.
"""
import argparse
import hashlib
import math
import os
from pathlib import Path
import sys
HERE=Path(__file__).resolve().parent
ROOT=HERE.parents[2]
sys.path.insert(0,str(HERE.parent/'uniform_srt_diagnostic'))
import diagnose_uniform_srt as diagnostic
VARIANT='experimental_regularized_full9'
def configuration(source):
if source.resolve()!=ROOT.resolve():
raise ValueError('Experimental source must equal the executing harness checkout')
cfg,body,p,manifest=diagnostic.source_config(source,HERE/'source_manifest.json')
if cfg['method']['collision']!='SRT' or cfg['method']['inlet']['scheme']!='zou_he_local':
raise ValueError('Unexpected baseline factory')
cfg['method']['inlet']['scheme']=VARIANT
return cfg,diagnostic.select_body(body,True),p,manifest
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source',type=Path,default=ROOT)
parser.add_argument('--output',type=Path,required=True)
parser.add_argument('--record-inlet-history',action='store_true')
parser.add_argument('--execute',action='store_true')
parser.add_argument('--gpu-admitted',action='store_true')
parser.add_argument('--wall-seconds',type=float)
a=parser.parse_args();a.source=a.source.resolve()
cfg,body,p,manifest=configuration(a.source)
from source_provenance import checkout_provenance
a.identity=dict(experimental_variant=VARIANT,
observation_mode='inlet_raw_history' if a.record_inlet_history else 'first_health_pair',
history_limit_bytes=diagnostic.HISTORY_LIMIT_BYTES if a.record_inlet_history else None,
history_requested_steps=[0,196] if a.record_inlet_history else None,
control='without_cylinder',force_observation='not_applicable_no_body',
baseline_diagnostic_commit='66a27410504fef477808d6079ce79ca1ae6b903c',
numerical_source_commit=manifest['commit'],
diagnostic_checkout=checkout_provenance(ROOT),numerical_checkout=checkout_provenance(a.source),
diagnostic_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
shared_diagnostic_sha256=hashlib.sha256(Path(diagnostic.__file__).read_bytes()).hexdigest())
if a.output.exists() and any(a.output.iterdir()):raise ValueError('Use a new/empty output directory')
if a.execute and (not a.gpu_admitted or not os.environ.get('CUDA_VISIBLE_DEVICES') or
a.wall_seconds is None or not math.isfinite(a.wall_seconds) or a.wall_seconds<=0):
raise ValueError('Explicit root admission, device selection and finite wall ceiling required')
a.output.mkdir(parents=True,exist_ok=True)
diagnostic.dump(a.output/'preflight.json',dict(**a.identity,source=manifest,config=cfg,body=body,parameters=p,
max_steps=320,max_evidence_bytes=diagnostic.LIMIT_BYTES,executed=False))
return diagnostic.execute(a,cfg,body,p) if a.execute else 0
if __name__=='__main__':raise SystemExit(main())
@@ -0,0 +1,113 @@
"""Uniform experimental full9 DFG pilot; CPU preflight unless explicitly admitted."""
import argparse
import hashlib
import json
import math
import os
from pathlib import Path
import sys
import time
HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[2]
sys.path.insert(0, str(HERE.parent / 'uniform_srt_diagnostic'))
import diagnose_uniform_srt as diagnostic
VARIANT = 'experimental_regularized_full9'
OBSERVATION = 'uniform_sparse_pilot_v1'
def configuration(source):
if source.resolve() != ROOT.resolve():
raise ValueError('Source must equal executing harness checkout')
cfg, body, p, manifest = diagnostic.source_config(source, HERE / 'dfg_source_manifest.json')
cfg['method']['inlet']['scheme'] = VARIANT
return cfg, body, p, manifest
def accuracy_pass(result):
"""Standard later qualification predicate; pilot never qualifies accuracy."""
return (result.get('healthy') is True and result.get('periodic') is True
and result.get('reference_comparison', {}).get('reference_agreement') is True)
def identity(manifest):
return dict(experimental_variant=VARIANT, collision='SRT', control='with_cylinder',
observation_mode=OBSERVATION, numerical_source_commit=manifest['commit'],
manifest_sha256=hashlib.sha256((HERE / 'dfg_source_manifest.json').read_bytes()).hexdigest())
def verify_pilot(result, expected):
import run_refined_dfg as runner
runner.ensure_finite(result)
if any(result.get(k) != v for k, v in expected.items()):
raise ValueError('Pilot identity mismatch')
if (result.get('case') != 'uniform' or result.get('mode') != 'pilot'
or result.get('status') != 'pilot_complete' or result.get('healthy') is not True
or result.get('steps') != 40000 or result.get('executed_steps') != 40000):
raise ValueError('Incomplete or mislabeled pilot')
timings = result.get('calibration_batches_seconds', [])
if len(timings) != 5 or any(isinstance(v, bool) or not isinstance(v, (int, float)) or v <= 0 for v in timings):
raise ValueError('Missing positive calibration timings')
rows = result.get('history', [])
if len(rows) != 10:
raise ValueError('Incomplete sparse pilot history')
for i, row in enumerate(rows, 1):
for key in ('cd', 'cl', 'mass', 'flux_in', 'flux_out', 'dp'):
if isinstance(row.get(key), bool) or not isinstance(row.get(key), (int, float)):
raise ValueError('Missing numeric pilot observation')
if (row.get('step') != i * 4000 or row.get('tstar') != i * .5
or row.get('tstar_force') != i * .5 - .25
or not .5 <= row.get('rho_min', 0) <= row.get('rho_max', 2) <= 1.5
or not 0 <= row.get('max_mach', 1) <= .2):
raise ValueError('Invalid sparse pilot observation')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path, default=ROOT)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--execute', action='store_true')
parser.add_argument('--gpu-admitted', action='store_true')
parser.add_argument('--wall-seconds', type=float, default=180.)
a = parser.parse_args()
a.source = a.source.resolve()
cfg, body, p, manifest = configuration(a.source)
import run_refined_dfg as runner
from source_provenance import checkout_provenance
h, periodic = runner.helpers(a.source)
if not math.isfinite(a.wall_seconds) or a.wall_seconds <= 0:
raise ValueError('Finite positive wall ceiling required')
if a.execute and (not a.gpu_admitted or not os.environ.get('CUDA_VISIBLE_DEVICES')):
raise ValueError('Explicit GPU admission and device selection required')
if a.output.exists() and any(a.output.iterdir()):
raise ValueError('Use new/empty output directory')
a.output.mkdir(parents=True, exist_ok=True)
receipt = identity(manifest)
receipt['checkout'] = checkout_provenance(ROOT)
diagnostic.dump(a.output / 'preflight.json', dict(**receipt, config=cfg, body=body,
parameters=p, source=manifest, max_steps=40000, cap_tstar=5.,
observation_width_tstar=.5, force_batch_steps=100,
calibration='First 100 steps warmup; next 400 timed, counted in physical trajectory',
gates=periodic.GATES, reference_gates=periodic.REFERENCE_GATES,
wall_seconds=a.wall_seconds, executed=False, accuracy_qualified=False))
if not a.execute:
return 0
a.mode = 'pilot'
a.deadline = time.monotonic() + a.wall_seconds
progress = {}
try:
result = runner.run_case(a, h, periodic, 'uniform', inlet_scheme=VARIANT,
sparse_pilot=True, receipt_identity=receipt, progress=progress)
except TimeoutError:
diagnostic.dump(a.output / 'result.json', dict(**receipt, status='wall_time_limit',
healthy=False, accuracy_qualified=False, **progress))
return 3
if result['healthy']:
verify_pilot(result, receipt)
diagnostic.dump(a.output / 'result.json', dict(**result, accuracy_qualified=False))
return 0 if result['healthy'] else 2
if __name__ == '__main__':
raise SystemExit(main())
@@ -0,0 +1,115 @@
"""Separately admitted full uniform DFG qualification after the pinned sparse pilot."""
import argparse
import hashlib
import json
import math
import os
from pathlib import Path
import sys
import time
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import run_dfg as pilot
OBSERVATION = 'uniform_standard_full_v1'
def configuration(source):
cfg, body, parameters, pilot_manifest = pilot.configuration(source)
_, _, _, full_manifest = pilot.diagnostic.source_config(source, HERE / 'dfg_full_source_manifest.json')
return cfg, body, parameters, pilot_manifest, full_manifest
def admit_pilot(path, cfg, body, parameters, manifest):
import run_refined_dfg as runner
expected = pilot.identity(manifest)
result = runner.read_finite_json(path / 'result.json')
preflight = runner.read_finite_json(path / 'preflight.json')
ready = runner.read_finite_json(path / 'uniform/ready.json')
pilot.verify_pilot(result, expected)
for name, document in [('preflight', preflight), ('ready', ready)]:
if any(document.get(k) != v for k, v in expected.items()):
raise ValueError('Pilot ' + name + ' identity mismatch')
if (preflight.get('config') != cfg or preflight.get('body') != body
or preflight.get('parameters') != parameters or preflight.get('source') != manifest
or preflight.get('max_steps') != 40000 or preflight.get('cap_tstar') != 5.
or preflight.get('observation_width_tstar') != .5
or preflight.get('gates') != runner.helpers(pilot.ROOT)[1].GATES
or preflight.get('reference_gates') != runner.helpers(pilot.ROOT)[1].REFERENCE_GATES):
raise ValueError('Pilot physical plan or gates mismatch')
if (ready.get('case') != 'uniform' or ready.get('mode') != 'pilot'
or ready.get('step') != 4000 or ready.get('tstar') != .5
or ready.get('runner_sha256') != runner.runner_hash()
or ready.get('all_runtime_and_observation_paths_exercised') is not True
or ready.get('calibration_batches_seconds') != result.get('calibration_batches_seconds')):
raise ValueError('Pilot READY mismatch')
checkout = result.get('checkout')
if (checkout != preflight.get('checkout') or checkout != ready.get('checkout')
or not isinstance(checkout, dict) or checkout.get('checkout_dirty') is not False
or checkout.get('checkout_revision') != 'c0be841b2e6e1ccba3b8343cecd3cee2fe0ed14f'):
raise ValueError('Pilot checkout provenance mismatch')
for key in ['startup_seconds', 'step_seconds', 'wall_seconds']:
value = result.get(key)
if isinstance(value, bool) or not isinstance(value, (float, int)) or value <= 0:
raise ValueError('Invalid pilot timing')
return dict(result_sha256=hashlib.sha256((path/'result.json').read_bytes()).hexdigest(),
checkout=checkout, startup_seconds=result['startup_seconds'],
step_seconds=result['step_seconds'], wall_seconds=result['wall_seconds'],
step_force_scaled_seconds=60 * result['step_seconds'])
def qualified(result):
return (result.get('mode') == 'full' and result.get('case') == 'uniform'
and result.get('steps') == 2400000 and result.get('executed_steps') == 2400000
and pilot.accuracy_pass(result))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source', type=Path, default=pilot.ROOT)
parser.add_argument('--output', type=Path, required=True)
parser.add_argument('--pilot-results', type=Path, required=True)
parser.add_argument('--execute', action='store_true')
parser.add_argument('--gpu-admitted', action='store_true')
parser.add_argument('--wall-seconds', type=float, default=720.)
a = parser.parse_args(); a.source = a.source.resolve()
cfg, body, parameters, pilot_manifest, full_manifest = configuration(a.source)
import run_refined_dfg as runner
from source_provenance import checkout_provenance
h, periodic = runner.helpers(a.source)
admission = admit_pilot(a.pilot_results, cfg, body, parameters, pilot_manifest)
if not math.isfinite(a.wall_seconds) or not 0 < a.wall_seconds <= 720:
raise ValueError('Full-run internal ceiling must be positive and at most720 seconds')
if a.execute and (not a.gpu_admitted or not os.environ.get('CUDA_VISIBLE_DEVICES')):
raise ValueError('Explicit GPU admission and device selection required')
if a.output.exists() and any(a.output.iterdir()):
raise ValueError('Use new/empty output directory')
a.output.mkdir(parents=True, exist_ok=True)
receipt = dict(experimental_variant=pilot.VARIANT, collision='SRT', control='with_cylinder',
observation_mode=OBSERVATION, numerical_source_commit=full_manifest['commit'],
manifest_sha256=hashlib.sha256((HERE/'dfg_full_source_manifest.json').read_bytes()).hexdigest(),
checkout=checkout_provenance(pilot.ROOT), pilot_admission=admission)
pilot.diagnostic.dump(a.output/'preflight.json', dict(**receipt, config=cfg, body=body,
parameters=parameters, source=full_manifest, max_steps=2400000, cap_tstar=300.,
observations='t*=.5 before240; .02 thereafter; forces at interval midpoint, pressure at endpoint',
initialization='Original production initialization; restart, no pilot continuation',
gates=periodic.GATES, reference_gates=periodic.REFERENCE_GATES,
wall_seconds=a.wall_seconds, executed=False, accuracy_qualified=False))
if not a.execute: return 0
a.mode = 'full'; a.deadline = time.monotonic() + a.wall_seconds
progress = {}
try:
result = runner.run_case(a, h, periodic, 'uniform', inlet_scheme=pilot.VARIANT,
sparse_pilot=True, receipt_identity=receipt, progress=progress)
except TimeoutError:
pilot.diagnostic.dump(a.output/'result.json', dict(**receipt, status='wall_time_limit',
healthy=False, accuracy_qualified=False, **progress))
return 3
passed = qualified(result)
pilot.diagnostic.dump(a.output/'result.json', dict(**result, accuracy_qualified=passed))
return 0 if passed else 2
if __name__ == '__main__':
raise SystemExit(main())
@@ -0,0 +1,118 @@
{
"commit": "926ca27407c26ca11283282a6e53249925725376",
"historical_numerical_source_commit": "9805383f9c50d867e8555471baf5dbda8333a652",
"variant": "experimental_regularized_full9",
"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": "215b09829e10453b333aae69811ffc638c7aa26ae418f25f8654a1a20a74f212",
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
"src/CelerisLab/lbm/channel.py": "2d5b5ff43414cb35633ac4ce90ce2c3a3353950b25c168e1c2ab58d498dd8b0f",
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
"src/CelerisLab/lbm/kernels/blocks.cu": "e3b9606f861bfea8c4b0f34e8356bb230081f899c584f67be89178d889099b17",
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "1203739c346cb4986e4ecb3090a70114eecc1688ae886d5abfcdb643be256acb",
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
"src/CelerisLab/lbm/kernels/boundary/inlet/experimental_full9.cuh": "f2475d6322f060b256f37d81e43d6aa3a1918e2eac83c076c32c2599b674f596",
"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": "4955786adc6d0853f2a9703611ea76e58ef045d6d6f4295bd795b05dc2f00dcd",
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
"src/CelerisLab/lbm/kernels/refinement.cu": "129f3d91da93b4324f88a0da73a4da1e5390f72c14cce574ee63306712da9895",
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "befbf6d757e532a59c166d41f920d16d71557cd3a574478e7aa83da737c48cbd",
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
"src/CelerisLab/lbm/raw_exchange.py": "8694202466eba848563e298656353b0f5db94e21a01fa05f51aad6dc2b9fd311",
"src/CelerisLab/lbm/refinement.py": "08c2a0557276a98cbcaa459d9bb7a9735d8bbbf64a6114c57af2fda61481a687",
"src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d",
"src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6",
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c",
"tests/validation/data/refined_dfg/source_manifest.json": "e495948b10f8112c77ca67255122b5e8bc9e32570e8c899802afcb85e608fef4",
"tests/validation/periodic.py": "a2212c0e2705126327a5019e9cafe64eeca398da1b75061ff23630c9843697a6",
"tests/validation/re100_helpers.py": "289c6e01075ff361573f862b90cfd61e5980c9a8efcbc78aef69979b4004fd60",
"tests/validation/run_refined_dfg.py": "256aacd84b3f8412a31ffaba9c4b5635db895c3177686dd34ef8e31f08119414",
"tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c"
},
"runtime_sha256": {
"tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py": "56b9ef786001c7760abab8d870f6437d719b427c21964f00067188f5fb27caad",
"tests/validation/experimental_full9/run_bodyless.py": "840edd524edb66ec06a3b8784c9e561f139a77cf49550125817193f25363c694"
}
}
@@ -0,0 +1,153 @@
"""Focused host checks for the uniform experimental DFG pilot."""
from copy import deepcopy
import importlib.util
import json
from pathlib import Path
import subprocess
import sys
from tempfile import TemporaryDirectory
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parent))
import run_dfg as r
def test_factory_and_default():
cfg, body, p, manifest = r.configuration(r.ROOT)
r.configuration(r.ROOT)
import run_refined_dfg as runner
h, _ = runner.helpers(r.ROOT)
old, old_body, old_p = runner.config(h, 80)
assert old['method']['inlet']['scheme'] == 'zou_he_local'
old['method']['inlet']['scheme'] = r.VARIANT
assert cfg == old and body == old_body and p == old_p
assert len(body['objects']) == 1 and p['diameter'] == 80
assert cfg == runner.config(h, 80, r.VARIANT)[0]
assert manifest['commit'] == '926ca27407c26ca11283282a6e53249925725376'
def receipt():
_, _, _, m = r.configuration(r.ROOT)
expected = r.identity(m)
rows = [dict(step=i*4000, tstar=i*.5, tstar_force=i*.5-.25,
rho_min=.99, rho_max=1.01, max_mach=.03,
cd=3., cl=.1, dp=2., mass=.8, flux_in=.41, flux_out=.41)
for i in range(1, 11)]
return expected, dict(**expected, case='uniform', mode='pilot', status='pilot_complete',
healthy=True, steps=40000, executed_steps=40000, history=rows,
calibration_batches_seconds=[.001]*5)
def test_receipts():
expected, result = receipt()
r.verify_pilot(result, expected)
for key, value in [('collision', 'MRT'), ('experimental_variant', 'zou_he_local'),
('observation_mode', 'first_health_pair'), ('control', 'without_cylinder'),
('numerical_source_commit', 'other'), ('case', 'refined'),
('steps', 39999), ('executed_steps', 39999), ('healthy', False)]:
bad = deepcopy(result); bad[key] = value
with unittest.TestCase().assertRaises(ValueError): r.verify_pilot(bad, expected)
for change in ('nan', 'missing', 'time', 'count'):
bad = deepcopy(result)
if change == 'nan': bad['history'][-1]['tstar'] = float('nan')
if change == 'missing': del bad['history'][0]['cd']
if change == 'time': bad['history'][0]['tstar_force'] = .5
if change == 'count': bad['history'].pop()
with unittest.TestCase().assertRaises(ValueError): r.verify_pilot(bad, expected)
def test_accuracy_requires_reference():
assert not r.accuracy_pass(dict(healthy=True, periodic=True))
assert r.accuracy_pass(dict(healthy=True, periodic=True,
reference_comparison=dict(reference_agreement=True)))
assert not r.accuracy_pass(dict(healthy=True, periodic=False,
reference_comparison=dict(reference_agreement=True)))
def test_preflight_and_source_guards():
with TemporaryDirectory() as temp:
subprocess.run([sys.executable, str(Path(r.__file__)), '--output', temp], check=True)
out = json.loads((Path(temp)/'preflight.json').read_text())
assert out['max_steps'] == 40000 and out['observation_width_tstar'] == .5
assert not out['executed'] and not out['accuracy_qualified']
assert out['wall_seconds'] == 180 and out['control'] == 'with_cylinder'
with unittest.TestCase().assertRaises(ValueError): r.configuration(r.ROOT.parent)
with unittest.TestCase().assertRaisesRegex(ValueError, 'Pinned source mismatch'):
r.diagnostic.source_config(r.ROOT, Path(r.__file__).with_name('source_manifest.json'))
def test_shared_source_semantics():
r.configuration(r.ROOT)
import run_refined_dfg as runner
text = Path(runner.__file__).read_text()
assert 'sparse_pilot=False' in text
assert '(a.mode == "pilot" and not sparse_pilot)' in text
assert 'impulse += sim.read_force(0).astype(np.float64) * batch' in text
assert 'tstar_force=(begin + n / 2) * SPEED / p["diameter"]' in text
assert 'cd=float(impulse[0] / n / den)' in text
assert 'mass = float(rho[fluid].sum()) * p["dx"] ** 2' in text
assert 'executed_steps += batch' in text
assert 'First unhealthy observation; onset between observations is unknown' in text
def test_accounted_calibration_and_failure_progress():
import numpy as np
import types
from unittest.mock import patch
r.configuration(r.ROOT)
import run_refined_dfg as runner
h, periodic = runner.helpers(r.ROOT)
_, _, p = runner.config(h, 80)
count = [0]
fail = [False]
class Simulation:
def __init__(self, *args, **kwargs):
self.field = types.SimpleNamespace(flag=np.ones(p['nx']*p['ny'], np.uint8))
def __enter__(self): return self
def __exit__(self, *args): pass
def initialize(self): count[0] = 0
def run(self, n): count[0] += n
def read_force(self, body): return np.array([.004, .002])
def get_macroscopic(self):
shape = (p['ny'], p['nx'])
return dict(rho=np.full(shape, float('nan') if fail[0] else 1.),
ux=np.full(shape, .01), uy=np.zeros(shape))
modules = {
'CelerisLab.simulation': types.SimpleNamespace(Simulation=Simulation),
'CelerisLab.cuda.context': types.SimpleNamespace(CudaContext=object),
'CelerisLab.lbm.refinement_stepper': types.SimpleNamespace(FixedPatchStepper=object),
}
with patch.dict(sys.modules, modules), patch.object(h, 'pressure_probes', return_value=dict(dp=2.)):
with TemporaryDirectory() as temp:
args = types.SimpleNamespace(source=r.ROOT, output=Path(temp), mode='pilot', deadline=float('inf'))
progress = {}
result = runner.run_case(args, h, periodic, 'uniform', inlet_scheme=r.VARIANT,
sparse_pilot=True, progress=progress)
assert count[0] == 40000 and result['executed_steps'] == 40000
assert len(result['history']) == 10 and len(result['calibration_batches_seconds']) == 5
assert abs(result['history'][0]['cd'] - 1.) < 1e-14
assert result['history'][0]['tstar_force'] == .25
fail[0] = True
with TemporaryDirectory() as temp:
args.output = Path(temp)
result = runner.run_case(args, h, periodic, 'uniform', inlet_scheme=r.VARIANT,
sparse_pilot=True, progress=progress)
assert result['executed_steps'] == 4000 and result['steps'] == 0
assert result['first_observed_failure_step'] == 4000 and result['last_observed_step'] == 0
fail[0] = False
with TemporaryDirectory() as temp:
args.output = Path(temp)
def deadline(_):
if count[0] >= 500: raise TimeoutError('test deadline')
count[0] = 0
with patch.object(runner, 'check_deadline', side_effect=deadline):
with unittest.TestCase().assertRaises(TimeoutError):
runner.run_case(args, h, periodic, 'uniform', inlet_scheme=r.VARIANT,
sparse_pilot=True, progress=progress)
assert progress['executed_steps'] == 500 and progress['last_observed_step'] == 0
assert len(progress['calibration_batches_seconds']) == 5
if __name__ == '__main__':
result = unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite(
unittest.FunctionTestCase(fn) for name, fn in sorted(globals().items()) if name.startswith('test_')))
raise SystemExit(not result.wasSuccessful())
@@ -0,0 +1,97 @@
"""CPU-only full admission and unchanged observer tests; fixture receipts are synthetic."""
from copy import deepcopy
import json
from pathlib import Path
import subprocess
import sys
from tempfile import TemporaryDirectory
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parent))
import run_dfg_full as f
import test_dfg as pilot_tests
def fixture(path):
cfg, body, parameters, manifest, _ = f.configuration(f.pilot.ROOT)
import run_refined_dfg as runner
_, periodic = runner.helpers(f.pilot.ROOT)
expected, result = pilot_tests.receipt()
checkout = dict(checkout_revision='c0be841b2e6e1ccba3b8343cecd3cee2fe0ed14f', checkout_dirty=False)
result.update(checkout=checkout, startup_seconds=3.3353, step_seconds=3.6794, wall_seconds=8.)
preflight = dict(**expected, checkout=checkout, config=cfg, body=body, parameters=parameters,
source=manifest, max_steps=40000, cap_tstar=5., observation_width_tstar=.5,
gates=periodic.GATES, reference_gates=periodic.REFERENCE_GATES)
ready = dict(**expected, checkout=checkout, case='uniform', mode='pilot', step=4000,
tstar=.5, runner_sha256=runner.runner_hash(),
all_runtime_and_observation_paths_exercised=True,
calibration_batches_seconds=result['calibration_batches_seconds'])
(path/'uniform').mkdir(exist_ok=True)
for name, data in [('result.json', result), ('preflight.json', preflight), ('uniform/ready.json', ready)]:
(path/name).write_text(json.dumps(data))
return cfg, body, parameters, manifest
def test_admission_rejects_mismatches():
with TemporaryDirectory() as temp:
path = Path(temp); args = fixture(path)
receipt = f.admit_pilot(path, *args)
assert abs(receipt['step_force_scaled_seconds'] - 220.764) < 1e-10
cases = [('result.json', 'collision', 'MRT'),
('result.json', 'step_seconds', float('nan')),
('result.json', 'healthy', False),
('uniform/ready.json', 'step', 160),
('uniform/ready.json', 'runner_sha256', 'wrong'),
('preflight.json', 'body', {'objects': []}),
('preflight.json', 'gates', {}),
('preflight.json', 'source', {})]
for filename, key, value in cases:
fixture(path)
target = path/filename; data=json.loads(target.read_text()); data[key]=value
target.write_text(json.dumps(data))
with unittest.TestCase().assertRaises(ValueError): f.admit_pilot(path, *args)
fixture(path); (path/'uniform/ready.json').unlink()
with unittest.TestCase().assertRaises(FileNotFoundError): f.admit_pilot(path, *args)
def test_full_requires_health_periodicity_and_reference():
good = dict(case='uniform', mode='full', steps=2400000, executed_steps=2400000,
healthy=True, periodic=True, reference_comparison=dict(reference_agreement=True))
assert f.qualified(good)
for key, value in [('healthy', False), ('periodic', False), ('steps', 40000),
('executed_steps', 2399900), ('mode', 'pilot'), ('case', 'refined'),
('reference_comparison', {}),
('reference_comparison', dict(reference_agreement=False))]:
bad=deepcopy(good); bad[key]=value; assert not f.qualified(bad)
def test_preflight_preserves_physics_and_gates():
with TemporaryDirectory() as temp:
root=Path(temp); incoming=root/'pilot'; incoming.mkdir(); cfg, body, params, _=fixture(incoming)
output=root/'full'
subprocess.run([sys.executable, str(Path(f.__file__)), '--pilot-results', str(incoming),
'--output', str(output)], check=True)
data=json.loads((output/'preflight.json').read_text())
assert data['config']==cfg and data['body']==body and data['parameters']==params
assert data['max_steps']==2400000 and data['cap_tstar']==300 and data['wall_seconds']==720
assert not data['accuracy_qualified'] and not data['executed']
assert data['numerical_source_commit']=='926ca27407c26ca11283282a6e53249925725376'
assert 'restart' in data['initialization']
def test_old_files_and_late_observer_unchanged():
root=f.pilot.ROOT
for name in ['tests/validation/run_refined_dfg.py',
'tests/validation/experimental_full9/run_dfg.py',
'tests/validation/experimental_full9/dfg_source_manifest.json',
'tests/validation/periodic.py']:
old=subprocess.check_output(['git','-C',str(root),'show','c0be841:'+name])
assert old==(root/name).read_bytes()
source=(root/'tests/validation/run_refined_dfg.py').read_text()
assert 'begin * SPEED / p["diameter"] >= 240' in source
assert 'cap = 5.0 if a.mode == "pilot" else 300.0' in source
if __name__ == '__main__':
result=unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite(
unittest.FunctionTestCase(fn) for name,fn in sorted(globals().items()) if name.startswith('test_')))
raise SystemExit(not result.wasSuccessful())
@@ -0,0 +1,105 @@
"""Host-compile the actual numerical helper; no CUDA or flow evolution."""
import ctypes
from pathlib import Path
import subprocess
import sys
from tempfile import TemporaryDirectory
import unittest
import numpy as np
ROOT=Path(__file__).resolve().parents[3]
sys.path.insert(0,str(ROOT/'src'))
from CelerisLab.config import LBMConfig, INLET_SCHEME_MAP
cx=np.array([0,1,-1,0,0,1,-1,1,-1]);cy=np.array([0,0,0,1,-1,1,-1,-1,1]);w=np.array([4/9]+[1/9]*4+[1/36]*4)
M=np.array([np.ones(9),cx,cy]);Q=np.array([cx*cx,cy*cy,cx*cy])
TEMP=TemporaryDirectory();libs=[]
for shifted in [0,1]:
code=f'''
#include <cmath>
#define __device__
#define __forceinline__ inline
#define __fdiv_rn(a,b) ((a)/(b))
#define LATTICE_D2Q9 0
#define LATTICE_MODEL 0
#define NQ 9
#define CS2 (1.0f/3.0f)
#define NY 330
#define U0 .01f
#define W0_VAL (4.0f/9.0f)
#define WS_VAL (1.0f/9.0f)
#define WE_VAL (1.0f/36.0f)
#define USE_DDF_SHIFTING {shifted}
#define INLET_SCHEME 4
#include "operators/macro.cuh"
#include "operators/collision_srt.cuh"
#include "boundary/inlet/common.cuh"
#include "boundary/inlet/experimental_full9.cuh"
extern "C" int apply(float* f,float u,float v,int collision) {{
apply_experimental_full9_inlet_d2q9(f,u,v);
int count=0;
if (collision && inlet_scheme_uses_post_collision_ghost()) {{
float rho,ux,uy,eq[9]; compute_rho_u(f,rho,ux,uy); compute_feq(rho,ux,uy,eq);
collide_srt_no_force(f,eq,1.0f/.524f); ++count;
}}
return count;
}}
'''
src=Path(TEMP.name)/f'test{shifted}.cpp';src.write_text(code);lib=src.with_suffix('.so')
subprocess.run(['g++','-std=c++11','-shared','-fPIC','-O2','-ffp-contract=off','-I',str(ROOT/'src/CelerisLab/lbm/kernels'),str(src),'-o',str(lib)],check=True)
dll=ctypes.CDLL(str(lib));dll.apply.argtypes=[ctypes.POINTER(ctypes.c_float),ctypes.c_float,ctypes.c_float,ctypes.c_int];dll.apply.restype=ctypes.c_int;libs.append(dll)
def eq(rho,u,v):
cu=cx*u+cy*v;return w*rho*(1+3*cu+4.5*cu*cu-1.5*(u*u+v*v))
def reference(f,u,v):
rho=(f[0]+f[3]+f[4]+2*(f[2]+f[6]+f[8]))/(1-u);e=eq(rho,u,v);k=f-e
pi=np.array([2*(k[2]+k[6]+k[8]),k[3]+k[4]+2*(k[6]+k[8]),2*(k[6]-k[8])])
stress=w*(4.5*((cx*cx-1/3)*pi[0]+(cy*cy-1/3)*pi[1])+9*cx*cy*pi[2])
return e,stress,pi
def apply(f,u,v,shifted,collision=0):
raw=np.asarray(f-shifted*w,dtype=np.float32);count=libs[shifted].apply(raw.ctypes.data_as(ctypes.POINTER(ctypes.c_float)),u,v,collision)
return raw.astype(float)+shifted*w,count
def test_moments_stress_and_shift():
rng=np.random.default_rng(92)
for _ in range(20):
f=eq(1.02,.003,0)+rng.normal(0,.001,9);e,s,pi=reference(f,.003,.001)
outputs=[]
for shifted in [0,1]:
out,count=apply(f,.003,.001,shifted);outputs.append(out)
assert count==0
np.testing.assert_allclose(out,e+s,atol=2e-7,rtol=0)
np.testing.assert_allclose(M@out,M@e,atol=3e-7,rtol=0)
np.testing.assert_allclose(Q@(out-e),pi,atol=2e-7,rtol=0)
np.testing.assert_allclose(*outputs,atol=2e-7,rtol=0)
def test_unknown_inputs_ignored_and_equilibrium():
for shifted in [0,1]:
f=eq(1.01,.004,0);out,_=apply(f,.004,0,shifted)
np.testing.assert_allclose(out,f,atol=2e-7,rtol=0)
f[[1,5,7]]=np.nan;ignored,_=apply(f,.004,0,shifted)
assert np.isfinite(ignored).all()
np.testing.assert_array_equal(out,ignored)
def test_exactly_once_srt():
f=eq(1,.003,0)+np.array([.001,0,.002,.003,-.001,0,.002,0,-.002]);e,s,_=reference(f,.003,0)
for shifted in [0,1]:
out,count=apply(f,.003,0,shifted,1)
assert count==1
np.testing.assert_allclose(out,e+(1-1/.524)*s,atol=3e-7,rtol=0)
assert np.max(abs(out-(e+(1-1/.524)**2*s)))>1e-4
source=(ROOT/'src/CelerisLab/lbm/kernels/step/one_step_double.cu').read_text()
part=source.split('#if DIM == 2',2)[-1].split('#elif DIM == 3')[0]
assert part.index('apply_boundary_pull(f,')<part.index('collide_dispatch(f,')<part.index('stream_pull_store(k,')
assert part.count('collide_dispatch(f,')==1
def test_config_guard_and_legacy_ids():
assert {k:INLET_SCHEME_MAP[k] for k in ['zou_he_local','channel_stabilized','equilibrium','regularized']}==dict(zou_he_local=0,channel_stabilized=1,equilibrium=2,regularized=3)
assert LBMConfig().inlet_scheme=='zou_he_local'
for shifted in [False,True]:
c=LBMConfig(inlet_scheme='experimental_regularized_full9',ddf_shifting=shifted);c.validate();assert c.to_macros()['INLET_SCHEME']==4
for change in [dict(collision='MRT'),dict(collision='TRT'),dict(streaming='esopull'),dict(store_precision='FP16S'),dict(les_enabled=True),dict(y_wall_bc='free_slip'),dict(lattice_model='D3Q19')]:
with unittest.TestCase().assertRaises(ValueError):LBMConfig(inlet_scheme='experimental_regularized_full9',**change).validate()
if __name__=='__main__':
result=unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite(unittest.FunctionTestCase(fn) for name,fn in sorted(globals().items()) if name.startswith('test_')))
TEMP.cleanup();raise SystemExit(not result.wasSuccessful())
@@ -0,0 +1,41 @@
"""Experimental provenance and removal-only config checks, CPU only."""
from copy import deepcopy
from pathlib import Path
import sys
import unittest
sys.path.insert(0,str(Path(__file__).resolve().parent))
import run_bodyless as r
def test_config_only_boundary_changes():
cfg,body,p,manifest=r.configuration(r.ROOT)
import run_refined_dfg as runner
h,_=runner.helpers(r.ROOT);baseline,baseline_body,baseline_p=runner.config(h,80)
expected=deepcopy(baseline);expected['method']['inlet']['scheme']=r.VARIANT
assert cfg==expected and p==baseline_p
expected_body=deepcopy(baseline_body);expected_body['objects']=[]
assert body==expected_body
assert manifest['commit']=='926ca27407c26ca11283282a6e53249925725376'
assert cfg['method']['collision']=='SRT' and cfg['method']['outlet']['srt_neq_damp']==.5
def test_historical_manifest_rejects_candidate():
with unittest.TestCase().assertRaisesRegex(ValueError,'Pinned source mismatch'):
r.diagnostic.source_config(r.ROOT)
def test_different_runtime_checkout_rejected():
with unittest.TestCase().assertRaisesRegex(ValueError,'executing harness checkout'):
r.configuration(r.ROOT.parent)
def test_experimental_preflight():
import subprocess,json
from tempfile import TemporaryDirectory
with TemporaryDirectory() as temp:
subprocess.run([sys.executable,str(Path(r.__file__)),'--output',temp,'--record-inlet-history'],check=True,capture_output=True)
result=json.loads((Path(temp)/'preflight.json').read_text())
assert result['experimental_variant']==r.VARIANT
assert result['history_requested_steps']==[0,196] and result['max_steps']==320
assert not result['executed']
assert result['numerical_source_commit']=='926ca27407c26ca11283282a6e53249925725376'
if __name__=='__main__':
result=unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite(unittest.FunctionTestCase(fn) for name,fn in sorted(globals().items()) if name.startswith('test_')))
raise SystemExit(not result.wasSuccessful())
+31 -5
View File
@@ -39,8 +39,8 @@ def helpers(source):
return h, periodic
def config(h, diameter):
cfg, body, p = h.make_config(diameter, SPEED, "SRT", "neq_extrap", "zou_he_local")
def config(h, diameter, inlet_scheme="zou_he_local"):
cfg, body, p = h.make_config(diameter, SPEED, "SRT", "neq_extrap", inlet_scheme)
cfg["method"]["outlet"]["srt_neq_damp"] = 0.5
return cfg, body, p
@@ -159,7 +159,8 @@ def verify_source(source):
return expected
def run_case(a, h, periodic, case):
def run_case(a, h, periodic, case, *, inlet_scheme="zou_he_local",
sparse_pilot=False, receipt_identity=None, progress=None):
from CelerisLab.simulation import Simulation
from CelerisLab.cuda.context import CudaContext
from CelerisLab.lbm.refinement_stepper import FixedPatchStepper
@@ -167,10 +168,18 @@ def run_case(a, h, periodic, case):
from contextlib import ExitStack
refined = case == "refined"
cfgdict, bodydict, p = config(h, 40 if refined else 80)
if refined and inlet_scheme != "zou_he_local":
raise ValueError("Experimental inlet is uniform-only")
cfgdict, bodydict, p = config(h, 40 if refined else 80, inlet_scheme)
identity = {} if receipt_identity is None else dict(receipt_identity)
calibration_batches = []
executed_steps = 0
cap = 5.0 if a.mode == "pilot" else 300.0
max_steps = math.ceil(cap * p["diameter"] / SPEED)
rows = []
if progress is not None:
progress.update(executed_steps=0, last_observed_step=0, history=rows,
calibration_batches_seconds=calibration_batches)
healthy = True
step_seconds = 0.0
started = time.monotonic()
@@ -233,7 +242,7 @@ def run_case(a, h, periodic, case):
check_deadline(a.deadline)
width = (
0.02
if a.mode == "pilot" or begin * SPEED / p["diameter"] >= 240
if (a.mode == "pilot" and not sparse_pilot) or begin * SPEED / p["diameter"] >= 240
else 0.5
)
n = min(max(1, round(width * p["diameter"] / SPEED)), max_steps - begin)
@@ -259,8 +268,14 @@ def run_case(a, h, periodic, case):
while done < n:
check_deadline(a.deadline)
batch = min(100, n - done)
batch_started = time.monotonic() if sparse_pilot else None
sim.run(batch)
executed_steps += batch
if progress is not None:
progress["executed_steps"] = executed_steps
impulse += sim.read_force(0).astype(np.float64) * batch
if sparse_pilot and len(calibration_batches) < 5:
calibration_batches.append(time.monotonic() - batch_started)
done += batch
check_deadline(a.deadline)
step_seconds += time.monotonic() - tick
@@ -320,6 +335,8 @@ def run_case(a, h, periodic, case):
check_deadline(a.deadline)
ensure_finite(row)
rows.append(row)
if progress is not None:
progress["last_observed_step"] = row["step"]
if writer is None:
writer = csv.DictWriter(out, fieldnames=row)
writer.writeheader()
@@ -335,6 +352,9 @@ def run_case(a, h, periodic, case):
break
if len(rows) == 1:
ready = dict(
**identity,
calibration_batches_seconds=calibration_batches,
calibration_note="First 100 steps warmup, next 400 timed; all included in trajectory and force interval",
case=case,
mode=a.mode,
step=begin,
@@ -347,6 +367,12 @@ def run_case(a, h, periodic, case):
)
print("READY " + json.dumps(ready, allow_nan=False), flush=True)
result = dict(
**identity,
calibration_batches_seconds=calibration_batches,
first_observed_failure_step=executed_steps if not healthy and not refined else None,
last_observed_step=rows[-1]["step"] if rows else 0,
failure_timing="First unhealthy observation; onset between observations is unknown" if not healthy else None,
executed_steps=executed_steps if not refined else begin,
case=case,
mode=a.mode,
history=rows,
@@ -1,145 +0,0 @@
"""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()
@@ -1,101 +0,0 @@
{
"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 -7
View File
@@ -1,4 +1,4 @@
"""One bounded GPU case, with explicit opt-in experimental explosion."""
"""One bounded GPU case. No production edits, plots, checkpoints or field dumps."""
import argparse
import hashlib
@@ -75,10 +75,7 @@ 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()
@@ -102,8 +99,6 @@ 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,
@@ -137,7 +132,6 @@ def main():
coarse_boundary="periodic",
refinements=refinements,
level_blocks=[(8, 8)] * args.depth,
explosion=args.explosion,
)
with run:
run.initialize(
@@ -1,85 +0,0 @@
"""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)
@@ -0,0 +1,12 @@
unittest.case.FunctionTestCase (test_bounded_state_evidence) ... ok
unittest.case.FunctionTestCase (test_disjoint_complete_categories) ... ok
unittest.case.FunctionTestCase (test_exact_config_and_source) ... ok
unittest.case.FunctionTestCase (test_inactive_nonfinite_does_not_fail) ... ok
unittest.case.FunctionTestCase (test_original_safety_limits) ... ok
unittest.case.FunctionTestCase (test_population_macro_force_separate) ... ok
unittest.case.FunctionTestCase (test_source_guard_and_deadline) ... ok
----------------------------------------------------------------------
Ran 7 tests in 0.039s
OK
@@ -0,0 +1,312 @@
"""Read-only observations of the exact failed SRT control, at most 320 steps.
Default is CPU preflight. Execution requires root admission and an external watchdog.
No solver or numerical parameter modifications. --without-cylinder removes only the body;
all other geometry and the production initialization remain unchanged.
"""
import argparse
from copy import deepcopy
import hashlib
import importlib
import json
import math
import os
from pathlib import Path
import sys
import time
from tempfile import TemporaryDirectory
import numpy as np
HERE=Path(__file__).resolve().parent
DEFAULT_SOURCE=HERE.parents[2]
LIMIT_BYTES=128*1024**2
HISTORY_LIMIT_BYTES=12*1024**2
HISTORY_RESERVE_BYTES=64*1024
class InletHistory:
"""Optional copies of existing snapshots only; never changes solver/health stop."""
def __init__(self, flags, parameters, limit=HISTORY_LIMIT_BYTES):
self.flags = np.array(flags[:, :4], copy=True)
self.parameters = parameters
self.limit = limit
self.frames = []
self.steps = []
planned_bytes = 197 * (9 * self.flags.shape[0] * 4 * 4 + 24) + self.flags.nbytes
if planned_bytes + HISTORY_RESERVE_BYTES > self.limit:
raise ValueError('Requested history window exceeds 12 MiB budget')
def record(self, state):
if int(state['step']) > 196:
return
raw = state['raw']
if (raw.dtype != np.float32 or raw.ndim != 3 or raw.shape[0] != 9
or raw.shape[1] != self.flags.shape[0] or raw.shape[2] < 4):
raise ValueError('History requires raw shifted FP32 D2Q9 with four inlet columns')
step = int(state['step'])
if step != len(self.steps):
raise ValueError('History requires contiguous completed steps starting at zero')
frame = raw[:, :, :4]
self.frames.append(frame.copy())
self.steps.append(step)
def save(self, output, terminal_step):
steps = np.asarray(self.steps, dtype=np.int64)
raw = np.stack(self.frames) if self.frames else np.empty((0, 9, self.flags.shape[0], 4), np.float32)
path = output / 'inlet_history.npz'
np.savez(path, raw_shifted=raw, flags=self.flags, steps=steps,
time_s=steps * self.parameters['dt'],
tstar=steps * self.parameters['mean_speed'] / self.parameters['diameter'],
x_indices=np.arange(4), y_indices=np.arange(self.flags.shape[0]),
requested_step_range=np.array([0,196],np.int64),
diagnostic_terminal_step=np.array(terminal_step,np.int64))
size = path.stat().st_size
if size > self.limit:
raise RuntimeError('History exceeded its reserved artifact bound')
return dict(mode='inlet_raw_history', requested_first_step=0, requested_last_step=196,
complete_for_requested_window=len(steps)==197,
diagnostic_terminal_step=int(terminal_step), frames=len(steps),
first_step=None if not len(steps) else int(steps[0]),
last_step=None if not len(steps) else int(steps[-1]),
bytes=size, limit_bytes=self.limit,
sha256=hashlib.sha256(path.read_bytes()).hexdigest(),
layout='time,q,y,x; active postcollision raw shifted FP32; step zero is initialization',
window_policy='Recording stops after step196; evolution and health stopping remain unchanged')
def record_history(history, state):
if history is not None:
history.record(state)
def dump(path,value):
path.write_text(json.dumps(value,indent=2,allow_nan=False))
def number(value):
value=float(value)
return value if math.isfinite(value) else ('NaN' if math.isnan(value) else ('+Inf' if value>0 else '-Inf'))
def source_config(source, manifest_path=None):
manifest=json.loads((HERE/'source_manifest.json' if manifest_path is None else manifest_path).read_text())
for name,digest in {**manifest['sha256'],**manifest.get('runtime_sha256',{})}.items():
if hashlib.sha256((source/name).read_bytes()).hexdigest()!=digest:
raise ValueError('Pinned source mismatch: '+name)
sys.path.insert(0,str(source/'src')); sys.path.insert(0,str(source/'tests/validation'))
import CelerisLab
if Path(CelerisLab.__file__).resolve()!=source/'src/CelerisLab/__init__.py':
raise ValueError('Wrong CelerisLab import')
runner=importlib.import_module('run_refined_dfg')
if Path(runner.__file__).resolve()!=source/'tests/validation/run_refined_dfg.py':
raise ValueError('Wrong runner import')
h,_=runner.helpers(source)
cfg,body,p=runner.config(h,80) # Exact original config factory, not a reimplementation.
return cfg,body,p,manifest
def select_body(body, without_cylinder=False):
selected = deepcopy(body)
if without_cylinder:
if len(selected["objects"]) != 1 or selected["objects"][0]["type"] != "cylinder":
raise ValueError("Bodyless control requires exactly the original cylinder")
selected["objects"] = []
return selected
def body_force(sim, body):
# No body means no body-id 0 and no force observable; never fabricate zeros.
return sim.read_force(0).astype(np.float64) if body["objects"] else None
def categories(flags,p):
"""Disjoint physical categories; subtype nibble is not a bitfield."""
from CelerisLab.lbm.descriptors import FLUID,SOLID,OBSTACLE
y,x=np.indices(flags.shape); fluid=(flags&FLUID)!=0
masks={}; left=fluid.copy()
for name,test in (
('body_near',(x-p['center'][0])**2+(y-p['center'][1])**2 <= (p['radius']+3)**2),
('outlet_near',x>=flags.shape[1]-4),('inlet_near',x<=3),
('wall_near',(y<=3)|(y>=flags.shape[0]-4))):
masks[name]=left&test; left &= ~test
masks['bulk']=left
subtype=flags&0xf0
masks['inlet_ghost']=(~fluid)&(subtype==0x20)
masks['outlet_ghost']=(~fluid)&(subtype==0x30)
masks['wall_ghost']=(~fluid)&(subtype==0x10)
assigned=np.logical_or.reduce(list(masks.values()))
masks['body_solid']=(~assigned)&(((flags&OBSTACLE)!=0)|((flags&SOLID)!=0))
assigned|=masks['body_solid']; masks['other_inactive']=~assigned
return fluid,masks
def finite_extrema(a,mask):
good=mask&np.isfinite(a)
if not good.any(): return dict(min=None,max=None)
idx=np.flatnonzero(good); values=a.ravel()[idx]
lo=int(idx[np.argmin(values)]); hi=int(idx[np.argmax(values)])
return dict(min=float(a.ravel()[lo]),max=float(a.ravel()[hi]),
min_yx=list(map(int,np.unravel_index(lo,a.shape))),
max_yx=list(map(int,np.unravel_index(hi,a.shape))))
def diagnose(g,macro,force,flags,p,step):
fluid,masks=categories(flags,p)
badpop=~np.isfinite(g); badnode=badpop.any(axis=0)
badmacro=np.logical_or.reduce([~np.isfinite(macro[k]) for k in ('rho','ux','uy')])
with np.errstate(over='ignore',invalid='ignore'):
mach=np.sqrt(3*(np.asarray(macro['ux'],np.float64)**2+np.asarray(macro['uy'],np.float64)**2))
rho=macro['rho']
# Exactly the original physical macro/reaction conditions, now sampled each step.
nonfinite_macro=bool(np.any(badmacro&fluid))
force_bad=force is not None and not np.isfinite(force).all()
density_bad=bool(np.any(fluid&((rho<.5)|(rho>1.5))))
mach_bad=bool(np.any(fluid&(mach>.2)))
population_bad=bool(np.any(badnode&fluid))
original_failure=nonfinite_macro or force_bad or density_bad or mach_bad
offending=fluid&(badnode|badmacro|(rho<.5)|(rho>1.5)|(mach>.2))
examples=[]
for y,x in np.argwhere(offending)[:16]:
examples.append(dict(y=int(y),x=int(x),physical_xy_m=[float(x*p['dx']),float((y-.5)*p['dx'])],
category=next(name for name,mask in masks.items() if mask[y,x]),flag=int(flags[y,x]),
nonfinite_q=np.flatnonzero(badpop[:,y,x]).tolist(),
populations=[number(v) for v in g[:,y,x]],
rho=number(rho[y,x]),ux=number(macro['ux'][y,x]),uy=number(macro['uy'][y,x])))
neighborhoods=[]
for node in examples[:4]:
y,x=node['y'],node['x']; y0,y1=max(0,y-2),min(flags.shape[0],y+3); x0,x1=max(0,x-2),min(flags.shape[1],x+3)
neighborhoods.append(dict(bounds_xy=[x0,y0,x1,y1],flags=flags[y0:y1,x0:x1].tolist(),
physical_populations=[[[number(v) for v in row] for row in plane] for plane in g[:,y0:y1,x0:x1]]))
return dict(step=step,tstar=step*p['mean_speed']/p['diameter'],
physical_failure=bool(original_failure or population_bad),
original_health_failure=bool(original_failure),
physical_population_nonfinite=population_bad,physical_macro_nonfinite=nonfinite_macro,
force_nonfinite=bool(force_bad),density_limit=density_bad,mach_limit=mach_bad,
force=None if force is None else [number(v) for v in force],
categories={name:dict(nodes=int(mask.sum()),nonfinite_population_nodes=int((mask&badnode).sum()),
nonfinite_populations=int(badpop[:,mask].sum()),nonfinite_macro_nodes=int((mask&badmacro).sum()))
for name,mask in masks.items()},
rho=finite_extrema(rho,fluid),mach=finite_extrema(mach,fluid),
first_physical_examples=examples,local_neighborhoods=neighborhoods)
def snapshot(sim,p):
# Public physical decode and original production macro path. No host reconstruction
# substitutes for the original health check. get_ddf shares its completed-step cache.
with np.errstate(all='ignore'):
macro={k:np.asarray(v).copy() for k,v in sim.get_macroscopic().items()}
g=sim.get_ddf().reshape(9,p['ny'],p['nx'])
import pycuda.driver as cuda
raw=np.empty_like(g,dtype=np.float32)
with sim.ctx.activate():
cuda.memcpy_dtoh(raw,sim.field.ddf_gpu)
return dict(raw=raw,g=g,macro=macro,step=int(sim.stepper.step_count))
def save_states(dest,previous,current,flags,diagnostic):
"""Two states only; raw shifted bits and decoded observations, no trajectory."""
arrays={'flags':flags}
for name,state in (('previous',previous),('current',current)):
if state is None: continue
arrays[name+'_raw_shifted']=state['raw']
arrays[name+'_physical']=state['g']
arrays[name+'_step']=np.array(state['step'],np.int64)
# At D80, raw+physical for two states ~84 MiB; reserve >4 MiB for JSON/NPZ overhead.
if sum(v.nbytes for v in arrays.values())>LIMIT_BYTES-4*1024**2:
raise ValueError('Evidence arrays exceed bounded artifact budget')
np.savez(dest/'states.npz',**arrays)
dump(dest/'diagnostics.json',diagnostic)
size=sum(p.stat().st_size for p in dest.iterdir() if p.is_file())
if size>LIMIT_BYTES: raise RuntimeError('Evidence directory exceeds 128 MiB')
return size
def deadline_check(deadline):
if time.monotonic()>=deadline: raise TimeoutError('Diagnostic wall ceiling reached')
def execute(a,cfg,body,p):
from CelerisLab.simulation import Simulation
started=time.monotonic(); deadline=started+a.wall_seconds
previous=None; prior_report=None; initial_report=None
with TemporaryDirectory() as temp:
temp=Path(temp); dump(temp/'lbm.json',cfg); dump(temp/'body.json',body)
with Simulation(str(temp/'lbm.json'),str(temp/'body.json'),device_id=0) as sim:
sim.initialize() # Exact original production initialization, unchanged.
flags=sim.get_flags().reshape(p['ny'],p['nx'])
current=snapshot(sim,p)
history=InletHistory(flags,p) if a.record_inlet_history else None
record_history(history,current)
initial_report=diagnose(current['g'],current['macro'],None,flags,p,0)
report=initial_report; status='initial_physical_failure' if report['physical_failure'] else 'step_cap_reached'
try:
deadline_check(deadline)
if not report['physical_failure']:
for _ in range(320):
deadline_check(deadline)
previous=current; prior_report=report
sim.run(1) # Same solver; observation buffers reset/read once per step.
force=body_force(sim,body)
current=snapshot(sim,p)
record_history(history,current)
report=diagnose(current['g'],current['macro'],force,flags,p,current['step'])
if current['step']==1:
print('READY '+json.dumps(dict(**a.identity,step=1,all_observation_paths_exercised=True,
physical_healthy=not report['physical_failure']),allow_nan=False),flush=True)
if report['physical_failure']:
status='first_physical_failure'; break
deadline_check(deadline)
except TimeoutError:
status='wall_time_limit'
details=dict(**a.identity,status=status,executed_steps=int(sim.stepper.step_count),
observation_step=current['step'],previous_observation_step=None if previous is None else previous['step'],
initial=initial_report,preceding=prior_report,current=report,
wall_seconds=time.monotonic()-started,
storage='raw_shifted FP32 is exact active-buffer data; physical is production FP32 decoded G. Both are diagnostic snapshots, not complete restart checkpoints.',
health_scope='Physical FLUID nodes and force only; ghost/body categories cannot independently trigger physical failure.',
interpretation='Near-region labels locate symptoms, not the root cause. Per-step inspection changes observation cadence only. No physical acceptance.')
if history is not None:
details['inlet_history']=history.save(a.output,current['step'])
size=save_states(a.output,previous,current,flags,details)
print(json.dumps(dict(**a.identity,status=status,executed_steps=int(sim.stepper.step_count),evidence_bytes=size),allow_nan=False),flush=True)
return 0 if status=='step_cap_reached' else 2
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source',type=Path,default=DEFAULT_SOURCE)
parser.add_argument('--output',type=Path,required=True)
parser.add_argument('--execute',action='store_true')
parser.add_argument('--without-cylinder',action='store_true',
help='Remove only the cylinder for the bounded inlet control')
parser.add_argument('--record-inlet-history',action='store_true',
help='Record bounded raw x0..3 history without changing health stopping')
parser.add_argument('--gpu-admitted',action='store_true')
parser.add_argument('--wall-seconds',type=float)
a=parser.parse_args(); a.source=a.source.resolve()
cfg,body,p,manifest=source_config(a.source)
body=select_body(body,a.without_cylinder)
from source_provenance import checkout_provenance
a.identity=dict(observation_mode='inlet_raw_history' if a.record_inlet_history else 'first_health_pair',
history_limit_bytes=HISTORY_LIMIT_BYTES if a.record_inlet_history else None,
history_requested_steps=[0,196] if a.record_inlet_history else None,
control="without_cylinder" if a.without_cylinder else "baseline_cylinder",
force_observation="not_applicable_no_body" if a.without_cylinder else "per_step_body_reaction",
baseline_diagnostic_commit="8e378aa047bb35ae4ec46037eff7256bd2f074e7",
diagnostic_checkout=checkout_provenance(DEFAULT_SOURCE),
numerical_checkout=checkout_provenance(a.source),
diagnostic_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest())
if a.output.exists() and any(a.output.iterdir()): raise ValueError('Use a new/empty output directory')
if a.execute and (not a.gpu_admitted or not os.environ.get('CUDA_VISIBLE_DEVICES') or
a.wall_seconds is None or not math.isfinite(a.wall_seconds) or a.wall_seconds<=0):
raise ValueError('Explicit root admission, device selection and finite wall ceiling required')
a.output.mkdir(parents=True,exist_ok=True)
dump(a.output/'preflight.json',dict(**a.identity,source=manifest,config=cfg,body=body,parameters=p,
max_steps=320,
max_evidence_bytes=LIMIT_BYTES,executed=False))
if not a.execute: return 0
return execute(a,cfg,body,p)
if __name__=='__main__': raise SystemExit(main())
@@ -0,0 +1,112 @@
{
"commit": "9805383f9c50d867e8555471baf5dbda8333a652",
"failure_source_receipt_sha256": "a77f71550c52d4b8b6e946a3cb98fc5de5db3da1bcbbb3457c6d60167677ce4e",
"sha256": {
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
"src/CelerisLab/lbm/channel.py": "2d5b5ff43414cb35633ac4ce90ce2c3a3353950b25c168e1c2ab58d498dd8b0f",
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
"src/CelerisLab/lbm/kernels/blocks.cu": "e3b9606f861bfea8c4b0f34e8356bb230081f899c584f67be89178d889099b17",
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
"src/CelerisLab/lbm/kernels/refinement.cu": "129f3d91da93b4324f88a0da73a4da1e5390f72c14cce574ee63306712da9895",
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "befbf6d757e532a59c166d41f920d16d71557cd3a574478e7aa83da737c48cbd",
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
"src/CelerisLab/lbm/raw_exchange.py": "8694202466eba848563e298656353b0f5db94e21a01fa05f51aad6dc2b9fd311",
"src/CelerisLab/lbm/refinement.py": "08c2a0557276a98cbcaa459d9bb7a9735d8bbbf64a6114c57af2fda61481a687",
"src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d",
"src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6",
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c",
"tests/validation/data/refined_dfg/source_manifest.json": "e495948b10f8112c77ca67255122b5e8bc9e32570e8c899802afcb85e608fef4",
"tests/validation/periodic.py": "a2212c0e2705126327a5019e9cafe64eeca398da1b75061ff23630c9843697a6",
"tests/validation/re100_helpers.py": "289c6e01075ff361573f862b90cfd61e5980c9a8efcbc78aef69979b4004fd60",
"tests/validation/run_refined_dfg.py": "256aacd84b3f8412a31ffaba9c4b5635db895c3177686dd34ef8e31f08119414",
"tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c"
}
}
@@ -0,0 +1,132 @@
"""CPU-only synthetic diagnostic and source-contract tests; no simulation results."""
import json
from pathlib import Path
import sys
from tempfile import TemporaryDirectory
import unittest
import numpy as np
sys.path.insert(0,str(Path(__file__).resolve().parent))
import diagnose_uniform_srt as d
cfg,body,p,manifest=d.source_config(d.DEFAULT_SOURCE)
def fixture():
flags=np.ones((12,20),np.uint16)
flags[:,0]=2|0x20; flags[:,-1]=2|0x30
flags[[0,-1]]=2|0x10; flags[5,6]=2|0x40|0x200
g=np.full((9,12,20),1/9,np.float32)
macro=dict(rho=np.ones((12,20)),ux=np.zeros((12,20)),uy=np.zeros((12,20)))
params=dict(center=[6,5],radius=1,dx=.1,mean_speed=.01,diameter=80)
return g,macro,flags,params
def test_exact_config_and_source():
assert manifest['commit']=='9805383f9c50d867e8555471baf5dbda8333a652'
assert p['diameter']==80 and p['mean_speed']==.01 and p['nx']==1761 and p['ny']==330
assert cfg['method']['collision']=='SRT' and cfg['method']['ddf_shifting']
assert cfg['method']['outlet']['srt_neq_damp']==.5
assert np.isclose(3*p['viscosity']+.5,.524)
assert body['objects'][0]['center']==[160,160.5]
def test_inactive_nonfinite_does_not_fail():
g,m,f,p=fixture(); fluid,_=d.categories(f,p)
g[:,~fluid]=np.nan
for a in m.values(): a[~fluid]=np.nan
r=d.diagnose(g,m,np.zeros(2),f,p,1)
assert not r['physical_failure']
assert r['categories']['body_solid']['nonfinite_population_nodes']==1
assert r['categories']['outlet_ghost']['nodes']==10
assert r['categories']['inlet_ghost']['nodes']==10
json.dumps(r,allow_nan=False)
def test_population_macro_force_separate():
g,m,f,p=fixture(); g[4,6,17]=np.inf
r=d.diagnose(g,m,np.zeros(2),f,p,27)
assert r['physical_failure'] and r['physical_population_nonfinite']
assert not r['original_health_failure'] and not r['force_nonfinite']
assert r['first_physical_examples'][0]['category']=='outlet_near'
assert r['first_physical_examples'][0]['nonfinite_q']==[4]
assert r['first_physical_examples'][0]['populations'][4]=='+Inf'
json.dumps(r,allow_nan=False)
g,m,f,p=fixture(); m['ux'][6,10]=np.nan
r=d.diagnose(g,m,np.zeros(2),f,p,28)
assert r['physical_macro_nonfinite'] and not r['physical_population_nonfinite']
assert not r['force_nonfinite'] and r['original_health_failure']
g,m,f,p=fixture(); r=d.diagnose(g,m,np.array([np.nan,0]),f,p,29)
assert r['force_nonfinite'] and not r['physical_macro_nonfinite']
assert not r['first_physical_examples']
def test_original_safety_limits():
g,m,f,p=fixture(); m['rho'][5,7]=.49
r=d.diagnose(g,m,[0,0],f,p,1)
assert r['density_limit'] and r['first_physical_examples'][0]['category']=='body_near'
g,m,f,p=fixture(); m['ux'][6,10]=.2
assert d.diagnose(g,m,[0,0],f,p,1)['mach_limit']
def test_disjoint_complete_categories():
_,_,f,p=fixture(); fluid,masks=d.categories(f,p)
np.testing.assert_array_equal(sum(mask.astype(int) for mask in masks.values()),np.ones(f.shape))
physical=sum(masks[k].astype(int) for k in ('body_near','inlet_near','outlet_near','wall_near','bulk'))
np.testing.assert_array_equal(physical,fluid.astype(int))
def test_bounded_state_evidence():
g,m,f,p=fixture(); state=dict(raw=g.copy(),g=g,macro=m,step=1)
with TemporaryDirectory() as tmp:
dest=Path(tmp); size=d.save_states(dest,state,state,f,{'status':'synthetic_test'})
assert size<d.LIMIT_BYTES
with np.load(dest/'states.npz') as saved:
np.testing.assert_array_equal(saved['previous_raw_shifted'],g)
np.testing.assert_array_equal(saved['current_physical'],g)
assert sorted(x.name for x in dest.iterdir())==['diagnostics.json','states.npz']
actual_bytes=4*(9*330*1761*4)+(330*1761*2)+16
assert actual_bytes<d.LIMIT_BYTES-4*1024**2
def test_source_guard_and_deadline():
with TemporaryDirectory() as tmp:
with unittest.TestCase().assertRaises((ValueError,FileNotFoundError)):
d.source_config(Path(tmp))
with unittest.TestCase().assertRaises(TimeoutError): d.deadline_check(0)
def test_bodyless_selection_preserves_baseline():
from copy import deepcopy
baseline_config, baseline_body, baseline_parameters = deepcopy((cfg,body,p))
selected = d.select_body(body,True)
assert selected == {"objects": []}
assert (cfg,body,p) == (baseline_config,baseline_body,baseline_parameters)
assert d.select_body(body) == baseline_body
assert d.select_body(body) is not body
assert p['nx']==1761 and p['ny']==330 and p['mean_speed']==.01
assert np.isclose(3*p['viscosity']+.5,.524)
for invalid in ({"objects":[]},{"objects":body['objects']*2},
{"objects":[{"type":"other"}]}):
with unittest.TestCase().assertRaises(ValueError): d.select_body(invalid,True)
def test_absent_force_is_not_zero_or_read():
class FakeSimulation:
def __init__(self): self.calls=[]
def read_force(self,index):
self.calls.append(index)
return np.array([1.,2.],np.float32)
sim=FakeSimulation()
assert d.body_force(sim,{"objects":[]}) is None and not sim.calls
np.testing.assert_array_equal(d.body_force(sim,body),[1.,2.])
assert sim.calls==[0]
g,m,f,params=fixture()
report=d.diagnose(g,m,None,f,params,1)
assert report['force'] is None and not report['force_nonfinite']
m['ux'][3,1]=.2
assert d.diagnose(g,m,None,f,params,2)['physical_failure']
if __name__=='__main__':
result=unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite(
unittest.FunctionTestCase(fn) for name,fn in sorted(globals().items()) if name.startswith('test_')))
raise SystemExit(not result.wasSuccessful())
@@ -0,0 +1,91 @@
"""CPU-only fixed-window observation tests; no GPU or simulated acceptance."""
from contextlib import redirect_stdout
from io import StringIO
from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace, ModuleType
from unittest.mock import patch
import json
import sys
import unittest
import numpy as np
sys.path.insert(0,str(Path(__file__).resolve().parent))
import diagnose_uniform_srt as d
def test_raw_bits_copy_and_prefix():
flags=np.ones((12,20),np.uint16);p=dict(dt=.25,mean_speed=.01,diameter=80)
h=d.InletHistory(flags,p)
raw=np.arange(9*12*20,dtype=np.uint32).reshape(9,12,20).view(np.float32)
raw[0,0,0]=np.float32(-0.)
expected=raw[:,:,:4].view(np.uint32).copy()
h.record(dict(raw=raw,step=0)); raw.fill(7); flags.fill(0)
h.record(dict(raw=raw,step=1))
with TemporaryDirectory() as temp:
receipt=h.save(Path(temp),1);z=np.load(Path(temp)/'inlet_history.npz')
np.testing.assert_array_equal(z['raw_shifted'][0].view(np.uint32),expected)
assert np.all(z['flags']==1)
np.testing.assert_array_equal(z['steps'],[0,1])
np.testing.assert_array_equal(z['time_s'],[0,.25])
np.testing.assert_array_equal(z['tstar'],[0,.01/80])
assert not receipt['complete_for_requested_window'] and receipt['diagnostic_terminal_step']==1
def test_fixed_window_bound_and_late_stop():
flags=np.ones((330,1761),np.uint16);p=dict(dt=.1,mean_speed=.01,diameter=80)
h=d.InletHistory(flags,p);raw=np.zeros((9,330,4),np.float32)
for step in range(321):h.record(dict(raw=raw,step=step))
assert len(h.frames)==197
with TemporaryDirectory() as temp:
r=h.save(Path(temp),320)
assert r['complete_for_requested_window'] and r['last_step']==196 and r['diagnostic_terminal_step']==320
assert r['bytes']<d.HISTORY_LIMIT_BYTES
with unittest.TestCase().assertRaises(ValueError):d.InletHistory(flags,p,limit=100)
with unittest.TestCase().assertRaises(ValueError):
d.InletHistory(flags,p).record(dict(raw=np.zeros((9,331,4),np.float32),step=0))
def test_off_by_default():
import subprocess
with TemporaryDirectory() as temp:
subprocess.run([sys.executable,str(Path(d.__file__)),'--output',temp],check=True,capture_output=True)
r=json.loads((Path(temp)/'preflight.json').read_text())
assert r['observation_mode']=='first_health_pair' and r['history_requested_steps'] is None
assert not (Path(temp)/'inlet_history.npz').exists()
# Off-path must not inspect or modify observation state.
d.record_history(None,object())
def test_execution_identical_and_failure_included():
class Sim:
def __init__(self,*args,**kwargs):self.stepper=SimpleNamespace(step_count=0);self.calls=[]
def __enter__(self):instances.append(self);return self
def __exit__(self,*args):pass
def initialize(self):self.calls.append('initialize')
def get_flags(self):return np.ones((12,20),np.uint16)
def run(self,n):self.calls.append(('run',n));self.stepper.step_count+=n
module=ModuleType('CelerisLab.simulation');module.Simulation=Sim
def snapshot(sim,p):
raw=np.full((9,12,20),sim.stepper.step_count,np.float32)
return dict(raw=raw,g=raw.copy(),macro={},step=sim.stepper.step_count)
def diagnose(g,m,f,flags,p,step):return dict(physical_failure=step==3)
p=dict(ny=12,nx=20,dt=.1,mean_speed=.01,diameter=80);instances=[];details=[]
def save(output,prev,current,flags,report):details.append((prev['step'],current['step'],report));return 0
with patch.dict(sys.modules,{'CelerisLab.simulation':module}),patch.object(d,'snapshot',snapshot),patch.object(d,'diagnose',diagnose),patch.object(d,'save_states',save),redirect_stdout(StringIO()):
for enabled in [False,True]:
with TemporaryDirectory() as temp:
a=SimpleNamespace(wall_seconds=100,output=Path(temp),identity={},record_inlet_history=enabled)
assert d.execute(a,{},dict(objects=[]),p)==2
if enabled:
z=np.load(Path(temp)/'inlet_history.npz')
np.testing.assert_array_equal(z['steps'],[0,1,2,3])
assert np.all(z['raw_shifted'][-1]==3)
assert details[-1][2]['inlet_history']['diagnostic_terminal_step']==3
assert instances[0].calls==instances[1].calls==['initialize',('run',1),('run',1),('run',1)]
assert [(p,c) for p,c,_ in details]==[(2,3),(2,3)]
assert details[0][2]['status']==details[1][2]['status']=='first_physical_failure'
if __name__=='__main__':
result=unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite(
unittest.FunctionTestCase(fn) for name,fn in sorted(globals().items()) if name.startswith('test_')))
raise SystemExit(not result.wasSuccessful())