Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0be841b2e | ||
|
|
939c10c867 | ||
|
|
926ca27407 | ||
|
|
e92de37424 | ||
|
|
66a2741050 | ||
|
|
8e378aa047 |
No files matched your search
@@ -113,47 +113,3 @@ These criteria are fixed before the continuation runs. There is no new physical
|
||||
Actual earlier scale-1 pilot cost was 52.3884s launch-to-exit, 46.1414s after READY for800 steps with full-field diagnostics every base step. A naive same-diagnostic3200-step scale-1 extrapolation is about210s; the new876 field samples reduce transfer count but add ray diagnostics and compression. Use conservative planning envelopes210–500s for scale1 and900–3200s for scale2, not timing guarantees. The scale2 conservative envelope accounts for4x cells and4x root steps; sparse sampling mitigates but does not eliminate that cost. Post-READY safety caps are1200s and5400s respectively, replacing the unsuitable fixed1800s assumption. Root owns compilation/startup timeout and serial launch decisions.
|
||||
|
||||
Device-envelope estimates remain128/512MiB for s1/s2, at least1GiB free (or twice estimate). Output estimate is under64MiB per run including five primitive snapshots and876 sample records; no all-time populations are saved. Plan/metadata and sampled files are rewritten at each100-reference-time window to retain bounded partial evidence if interrupted. READY still follows compiled warmup and state/time reset, before measured stepping begins.
|
||||
|
||||
## Dense-root temporal diagnostic: preparation only
|
||||
|
||||
The audited continuation did not resolve time quadrature: all48 audited stride2 RMS changes exceeded their stride1 CV residuals, by factors1.55–11.46. That historical failure and its gates remain unchanged. The following is a measurement-only experiment to test this explanation, not a solver fix, a physics pass, or permission to extend GPU work. The separate periodic global stock-minus-raw mass residual also persists; denser contour quadrature cannot by itself resolve it.
|
||||
|
||||
Future launch, only after the computer is online and root approves a serialized bounded run:
|
||||
|
||||
```sh
|
||||
CUDA_VISIBLE_DEVICES="$VERIFIED_IDLE_GPU" python tests/validation/run_jet_circle_cv.py --repo "$PWD" --output "$NEW_DENSE_S1" --dense-root --steps 800 --scale 1
|
||||
CUDA_VISIBLE_DEVICES="$VERIFIED_IDLE_GPU" python tests/validation/run_jet_circle_cv.py --repo "$PWD" --output "$NEW_DENSE_S2" --dense-root --steps 800 --scale 2
|
||||
python tests/validation/analyze_jet_cv_continuation.py "$NEW_DENSE_S1" "$NEW_DENSE_S2" --output "$DENSE_COMPARISON_JSON"
|
||||
```
|
||||
|
||||
This mode fixes Re100,beta.1 and reference t=0..800 (t*≈.9965), with800/3200 root intervals and801/3201 endpoint records. It is mutually exclusive with --continuation and rejects longer runs or other scales/Re. Physical geometry, source manifest, solver, velocity commands, kernel and raw observation path are unchanged. There are no new finest-hop hooks. Existing GPU population downloads occur every root interval for CPU primitive diagnostics; those distributions are discarded rather than retained.
|
||||
|
||||
Dense scalar records contain CV endpoint inventories, each flux and split momentum terms, density/speed, physical composite mass, exact cumulative raw partitions and cumulative absolute aperture net mass. Rays remain on the exact old reference cadence:0..100 at dt1, then104..800 at dt4,276 samples for each scale. Schema2 maps their smaller arrays through ray_sample_indices into the dense records. Full primitive snapshots occur only at reference0,100,800. Saved data is capped at32MiB per case; dense arrays are scalar diagnostics, not all-time full fields. Schema/unit metadata, file hashes, complete-schedule checks and matched root time intervals remain mandatory.
|
||||
|
||||
The analyzer derives root strides1,2,4 and the exact old cadence from the SAME dense primitive records, always keeping window endpoints and the SAME raw ledger. At scale2 these root strides correspond to reference dt.25,.5,1; the old cadence corresponds to root gaps4/16. Its fourth quadrature result and change from dense are separately identified. Existing residual normalizations and historical files are preserved. No automatic refinement-progress flags are issued for this temporal-only mode. Root-sampled agreement cannot certify finer-than-root time convergence or exclude aliasing; an unresolved outcome stays unresolved, with no automatic longer run.
|
||||
|
||||
Three distinct throughput quantities must remain separate:
|
||||
|
||||
- Prescribed unsigned nominal mass at rho_ref=1:2*Q_command*window_duration.
|
||||
- Observed raw absolute NET aperture mass: sum over root intervals of abs(dm_blowing)+abs(dm_suction), after spatial reduction within each aperture. This retains reversals between root intervals that abs(window-net) loses. Within-aperture or within-root/substep reversals remain unresolved by this ledger. The legacy raw_gross_mass_over_nominal field retains its old abs(window-net)/ONE-aperture-nominal definition; it is not this new quantity.
|
||||
- Sparse reconstructed absolute Q/M: true-arc quadrature of abs(u.n) and abs(rho*u.n), using each existing linear/quadratic ray estimate, followed by old-cadence time integration. This is estimator-dependent measured-field evidence, not exact wall truth, not dense-root quadrature, and not a prescribed nominal flow.
|
||||
|
||||
Global stock-minus-raw mass balance is reported per root interval and summed per window, in reference mass units. Raw total-minus-wall/aperture partition discrepancies are also separate. Neither is bundled into the CV temporal error, and neither is called a proven raw-extractor defect. Source/geometry/state validity, both signed and gross budgets, old failures, and physics_validation_pass=false remain intact.
|
||||
|
||||
Planning envelope from the earlier52-second800-step pilot: s1 60–240s, s2 600–2400s, with conservative post-READY caps1200/5400s. Scale2 has4x cells and4x root observations over the same reference horizon; sparse saving reduces output, not all CPU/PCIe observation cost. These are preparation estimates, not measured timings. Root owns startup/compilation timeout and GPU serialization. No GPU/remote action was taken to prepare this option.
|
||||
|
||||
Budget enforcement preflights the projected retained file sizes before replacing each snapshot/window report or the sample+metadata pair. If32MiB would be exceeded, staged replacements are removed, the previous verified sample/hash-metadata pair remains intact, and execution stops. Temporary compression files need additional short-lived disk space and are distinct from the32MiB retained-evidence cap. This protects budget failures; it is not a filesystem crash-recovery transaction. Both raw partition identities, total−wall−aperture and aperture−blowing−suction, are saved separately from stock/raw and CV residuals.
|
||||
|
||||
## Optional shifted-FP32 representation control
|
||||
|
||||
The dense-root campaign accepts `--shifted-fp32` as an explicit numerical-representation control. Without it, the historical unshifted FP32 default remains unchanged. Use a fresh output directory and append the flag to the reviewed dense command, for example:
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES="$VERIFIED_IDLE_GPU" python tests/validation/run_jet_circle_cv.py --repo "$PWD" --output "$NEW_SHIFTED_DENSE_S1" --dense-root --steps 800 --scale 1 --shifted-fp32
|
||||
```
|
||||
|
||||
Root owns the future GPU launch and scale selection. This option requires the bounded dense campaign; it does not authorize another run. Geometry, Jet profile/amplitude, initial physical seed, viscosity, time horizon, root sampling, sparse rays/fields, normalization, thresholds, budgets and prior failed gates are unchanged. The `case.ddf_shifting` flag and `case.storage_variant` receipt appear in the plan, READY, window report and sample metadata. Preserve the unshifted evidence in its original directory. Compare like-scale records at identical root endpoints, including CV mass/momentum, global stock-minus-raw balance, raw partitions and measured aperture flux. A reduced mass residual would support representation sensitivity; it would not establish physical convergence or prove that every residual is roundoff.
|
||||
|
||||
Both production blocked steppers already support this representation: initialization takes physical populations and encodes `g=f-w` to FP32; public download returns physical `f` decoded in FP64. The runner deliberately retains its historical FP32-rounded physical equilibrium seed for BOTH variants rather than also changing initial precision. Encoding introduces a small additional rounding difference: maximum per-population change 1.035e-10 and density change 3.201e-10 at both scales for this seed. Thus the decoded starting states are not bit-identical. The residual initial density offset is therefore not removed. All existing measurement routines receive physical populations and apply no extra weight shift. Snapshots keep the existing primitive-field precision.
|
||||
|
||||
The existing `BlockCurvedBoundary` optional-input adapter invokes the same shifted-aware BFL helper as the AMR boundary: wall density restores the unit background, raw mass uses reflected-minus-outgoing (opposite weights cancel), and reaction restores twice the direction weight. No host correction is applied to raw rows. The shifted branch selects `compute_feq_from_momentum` rather than unshifted `compute_feq`, so this control changes existing numerical algebra as well as storage representation. It cannot isolate storage precision alone or identify collision as the sole cause. Block collision preserves the density perturbation in its shifted branch; blocked halo copies and AMR transfers already support shifted storage. This option changes no solver/kernel/API or source manifest. Existing shifted-storage/Jet host tests and the new runner codec tests are CPU evidence only; numerical GPU behavior remains to be checked by the reviewed control.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -28,41 +28,6 @@ def eq(rho, u, v):
|
||||
|
||||
|
||||
class Host(unittest.TestCase):
|
||||
def test_shifted_storage_receipt_preserves_physical_case(self):
|
||||
for scale in (1, 2):
|
||||
base = case(scale)
|
||||
shifted = case(scale, ddf_shifting=True)
|
||||
self.assertFalse(base.pop("ddf_shifting"))
|
||||
self.assertTrue(shifted.pop("ddf_shifting"))
|
||||
self.assertEqual(base.pop("storage_variant")["name"], "unshifted_fp32")
|
||||
self.assertEqual(shifted.pop("storage_variant")["name"], "shifted_fp32")
|
||||
self.assertEqual(base, shifted)
|
||||
|
||||
def test_shifted_physical_download_roundtrip(self):
|
||||
from CelerisLab.lbm.population_storage import _encode_physical, _decode_stored
|
||||
from run_jet_circle_cv import moments
|
||||
from jet_cv_measurements import primitives
|
||||
|
||||
for speed in (0.03, 0.015):
|
||||
seed = state((9, 48, 64), speed)
|
||||
self.assertEqual(seed.dtype, np.float32)
|
||||
plain = _decode_stored(_encode_physical(seed, False), False)
|
||||
decoded = _decode_stored(_encode_physical(seed, True), True)
|
||||
self.assertEqual(decoded.dtype, np.float64)
|
||||
np.testing.assert_array_equal(plain, seed)
|
||||
# Same FP32 physical seed; only encoding quantization may differ.
|
||||
np.testing.assert_allclose(decoded, seed, rtol=0, atol=3e-10)
|
||||
rho, momentum, velocity = moments(decoded)
|
||||
rho2, velocity2 = primitives(decoded, CX, CY)
|
||||
np.testing.assert_array_equal(rho, rho2)
|
||||
np.testing.assert_array_equal(velocity, velocity2)
|
||||
np.testing.assert_allclose(momentum, rho * velocity, atol=1e-17)
|
||||
fluid = np.ones(seed.shape[1:], bool)
|
||||
inv, flux = cv(decoded, fluid, (0, 0), (5, 4, 25, 20), 0.0072)
|
||||
expected, _ = cv(seed, fluid, (0, 0), (5, 4, 25, 20), 0.0072)
|
||||
np.testing.assert_allclose(inv, expected, rtol=0, atol=1e-6)
|
||||
np.testing.assert_allclose(flux, 0, atol=1e-14)
|
||||
|
||||
def test_descriptor_and_uniform_closed_flux(self):
|
||||
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
||||
|
||||
|
||||
@@ -31,111 +31,6 @@ from analyze_jet_cv_continuation import integrate, analyze
|
||||
|
||||
|
||||
class Measurements(unittest.TestCase):
|
||||
def test_budget_failure_preserves_verified_pair(self):
|
||||
from jet_cv_measurements import install_evidence
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
data = path / "samples.npz"
|
||||
meta = path / "samples.json"
|
||||
data.write_bytes(b"old-data")
|
||||
meta.write_text("old-meta")
|
||||
staged = path / ".samples.pending.npz"
|
||||
staged_meta = path / ".meta.pending"
|
||||
staged.write_bytes(b"x" * 100)
|
||||
staged_meta.write_text("new-meta")
|
||||
with self.assertRaisesRegex(RuntimeError, "last verified files preserved"):
|
||||
install_evidence({data: staged, meta: staged_meta}, limit=20)
|
||||
self.assertEqual(data.read_bytes(), b"old-data")
|
||||
self.assertEqual(meta.read_text(), "old-meta")
|
||||
self.assertEqual(
|
||||
sorted(p.name for p in path.iterdir()), ["samples.json", "samples.npz"]
|
||||
)
|
||||
|
||||
def test_dense_root_and_exact_old_cadence(self):
|
||||
from jet_cv_measurements import old_sample_steps
|
||||
|
||||
for scale, count in ((1, 801), (2, 3201)):
|
||||
c = case(scale, steps=800)
|
||||
c["dense_root"] = True
|
||||
dense = sample_steps(c)
|
||||
old = old_sample_steps(c)
|
||||
self.assertEqual(len(dense), count)
|
||||
self.assertEqual(len(old), 276)
|
||||
np.testing.assert_array_equal(dense, np.arange(800 * scale**2 + 1))
|
||||
self.assertTrue(np.isin(old, dense).all())
|
||||
np.testing.assert_array_equal(old[:101] / scale**2, np.arange(101))
|
||||
np.testing.assert_array_equal(old[101:] / scale**2, np.arange(104, 801, 4))
|
||||
|
||||
def test_root_interval_absolute_net_mass_reversals(self):
|
||||
from jet_cv_measurements import absolute_aperture_interval_mass
|
||||
|
||||
masks = (np.array([True, False]), np.array([False, True]))
|
||||
rows = np.zeros((2, 4))
|
||||
rows[0, 3] = 1
|
||||
first = absolute_aperture_interval_mass(rows, *masks)
|
||||
rows[0, 3] = -1
|
||||
self.assertEqual(first + absolute_aperture_interval_mass(rows, *masks), 2)
|
||||
self.assertEqual(abs(1 - 1), 0)
|
||||
|
||||
def test_dense_offline_oscillation_preserves_ledger(self):
|
||||
import json
|
||||
from jet_cv_measurements import old_sample_steps
|
||||
|
||||
for s in (1, 2):
|
||||
c = case(s, steps=800)
|
||||
c["dense_root"] = True
|
||||
steps = sample_steps(c)
|
||||
t = steps / s**2
|
||||
n = len(t)
|
||||
ridx = old_sample_steps(c)
|
||||
# Nonzero steady reference fluxes cancel exact stock changes.
|
||||
inv = np.zeros((n, 2, 2, 3))
|
||||
inv[..., 0] = (100 - 0.001 * t[:, None, None]) * s**2
|
||||
inv[..., 1] = -t[:, None, None] * 0.002 * s
|
||||
flux = np.zeros_like(inv)
|
||||
flux[..., 0] = 0.001 + 1e-4 * np.cos(np.pi / 2 * t[:, None, None])
|
||||
flux[..., 1] = 0.002 / s
|
||||
raw = np.zeros((n, 2, 5, 4))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp)
|
||||
(path / f"samples_s{s}.json").write_text(
|
||||
json.dumps(dict(case=c, sample_file="samples.npz"))
|
||||
)
|
||||
np.savez(
|
||||
path / "samples.npz",
|
||||
t_reference=t,
|
||||
cv_inventory=inv,
|
||||
cv_flux=flux,
|
||||
raw_cumulative=raw,
|
||||
ray_sample_indices=ridx,
|
||||
arc_estimates=np.zeros((len(ridx), 2, 2, 2, 2)),
|
||||
arc_absolute_estimates=np.zeros((len(ridx), 2, 2, 2, 2)),
|
||||
raw_absolute_aperture_mass_cumulative=np.broadcast_to(
|
||||
2 * t[:, None] * s**2, (n, 2)
|
||||
),
|
||||
global_mass_reference=np.ones((n, 2)) * 100,
|
||||
level_stats=np.broadcast_to([1, 1, 0, 0.03, 100], (n, 3, 5)),
|
||||
velocity_error=np.zeros(n),
|
||||
)
|
||||
result = analyze(path)
|
||||
w = result["windows"][1]
|
||||
self.assertTrue(result["complete_schedule"])
|
||||
np.testing.assert_allclose(
|
||||
np.array(w["normalized_cv_residual"])[0], 0, atol=1e-11
|
||||
)
|
||||
self.assertGreater(
|
||||
abs(np.array(w["old_cadence_change_from_dense"])[..., 0]).max(),
|
||||
0.005,
|
||||
)
|
||||
np.testing.assert_allclose(
|
||||
w["global_stock_raw_balance_sum_reference"], 0
|
||||
)
|
||||
np.testing.assert_allclose(
|
||||
w["raw_absolute_aperture_net_interval_mass_reference"], 200
|
||||
)
|
||||
np.testing.assert_allclose(w["raw_reaction_coefficient"], 0)
|
||||
|
||||
def test_common_physical_schedule(self):
|
||||
a = sample_steps(case(1, steps=3200))
|
||||
b = sample_steps(case(2, steps=3200))
|
||||
@@ -239,57 +134,6 @@ class Measurements(unittest.TestCase):
|
||||
snap["uniform_velocity_reference"][0][fluid], 0.01, rtol=1e-7
|
||||
)
|
||||
|
||||
def test_dense_recorder_retains_sparse_rays(self):
|
||||
h = 0.5
|
||||
y, x = np.mgrid[:64, :64]
|
||||
x = (x + 0.5) * h
|
||||
y = (y + 0.5) * h
|
||||
fluid = (x - 16) ** 2 + (y - 16) ** 2 > 25
|
||||
fine_leaf = fluid.copy()
|
||||
fine_leaf[:, :4] = False
|
||||
root_leaf = np.zeros((32, 32), bool)
|
||||
root_leaf[:, :2] = True
|
||||
|
||||
def eq(ny, nx):
|
||||
cu = CX[:, None, None] * 0.01
|
||||
return np.broadcast_to(
|
||||
W[:, None, None] * (1 + 3 * cu + 4.5 * cu * cu - 1.5 * 0.01**2),
|
||||
(9, ny, nx),
|
||||
).copy()
|
||||
|
||||
ug = eq(64, 64)
|
||||
levels = [eq(32, 32), ug.copy()]
|
||||
c = dict(
|
||||
scale=1,
|
||||
dense_root=True,
|
||||
steps=800,
|
||||
center=[16, 16],
|
||||
diameter=10,
|
||||
u=0.01,
|
||||
nu=0.0072,
|
||||
contours=[[9, 9, 23, 23], [7, 7, 25, 25]],
|
||||
)
|
||||
data = {
|
||||
name: [cv(ug, mask, (0, 0), r, c["nu"]) for r in c["contours"]]
|
||||
for name, mask in [("uniform_adapter", fluid), ("amr_api", fine_leaf)]
|
||||
}
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
rec = MeasurementRecorder(
|
||||
c, tmp, fluid, [root_leaf, fine_leaf], (0, 0), CX, CY
|
||||
)
|
||||
for step in (0, 101):
|
||||
rec.capture(step, ug, levels, data, {k: np.zeros(2) for k in data}, 0.0)
|
||||
rec.records[-1]["raw_cumulative"] = np.zeros((2, 5, 4))
|
||||
rec.records[-1]["raw_absolute_aperture_mass_cumulative"] = np.zeros(2)
|
||||
rec.flush()
|
||||
a = np.load(Path(tmp) / "samples_s1.npz")
|
||||
np.testing.assert_array_equal(a["step"], [0, 101])
|
||||
np.testing.assert_array_equal(a["ray_sample_indices"], [0])
|
||||
self.assertEqual(a["arc_estimates"].shape, (1, 2, 2, 2, 2))
|
||||
self.assertTrue(
|
||||
np.all(a["arc_absolute_estimates"] >= abs(a["arc_estimates"]) - 1e-14)
|
||||
)
|
||||
|
||||
def test_offline_mass_force_normalization(self):
|
||||
import json
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ def analyze(directory):
|
||||
t = a["t_reference"]
|
||||
if not len(t) or t[0] != 0 or np.any(np.diff(t) <= 0):
|
||||
raise ValueError("Sample reference times must increase strictly from zero")
|
||||
from jet_cv_measurements import sample_steps, old_sample_steps
|
||||
from jet_cv_measurements import sample_steps
|
||||
|
||||
complete_schedule = np.array_equal(t, sample_steps(c) / s**2)
|
||||
records = []
|
||||
@@ -45,15 +45,7 @@ def analyze(directory):
|
||||
) # angular impulse reference = root/s^2 (not used in force tests)
|
||||
q0 = 2 * np.pi / 12 * (c["diameter"] / 2) * c["jet_speed"]
|
||||
f0 = 0.5 * (c["u"] * s) ** 2 * (c["diameter"] / s)
|
||||
dense_root = c.get("dense_root", False)
|
||||
old_times = old_sample_steps(c) / s**2
|
||||
ray_indices = (
|
||||
a["ray_sample_indices"] if "ray_sample_indices" in a else np.arange(len(t))
|
||||
)
|
||||
ray_times = t[ray_indices]
|
||||
if dense_root and complete_schedule and not np.array_equal(ray_times, old_times):
|
||||
raise ValueError("Sparse ray records do not match the declared old cadence")
|
||||
for lo in range(0, c.get("steps", 3200 * s * s) // (s * s), 100):
|
||||
for lo in range(0, 3200, 100):
|
||||
hi = lo + 100
|
||||
ids = np.flatnonzero((t >= lo) & (t <= hi))
|
||||
if len(ids) < 2 or t[ids[0]] != lo or t[ids[-1]] != hi:
|
||||
@@ -62,14 +54,8 @@ def analyze(directory):
|
||||
delta = inv[last] - inv[first]
|
||||
ledger = raw[last] - raw[first]
|
||||
cv_variants = []
|
||||
selections = [np.unique(np.r_[ids[::stride], ids[-1]]) for stride in (1, 2, 4)]
|
||||
if dense_root:
|
||||
selections.append(ids[np.isin(t[ids], old_times)])
|
||||
for selected in selections:
|
||||
if t[selected[0]] != lo or t[selected[-1]] != hi:
|
||||
raise ValueError(
|
||||
"Every quadrature selection must retain both window endpoints"
|
||||
)
|
||||
for stride in (1, 2, 4):
|
||||
selected = np.unique(np.r_[ids[::stride], ids[-1]])
|
||||
total = integrate(t[selected], flux[selected])
|
||||
m = delta[:, :, 0] + total[:, :, 0]
|
||||
j = -delta[:, :, 1:] - total[:, :, 1:]
|
||||
@@ -82,61 +68,16 @@ def analyze(directory):
|
||||
axis=-1,
|
||||
)
|
||||
)
|
||||
ray_ids = np.flatnonzero((ray_times >= lo) & (ray_times <= hi))
|
||||
arc = (
|
||||
integrate(ray_times[ray_ids], a["arc_estimates"][ray_ids]) / 100
|
||||
integrate(t[ids], a["arc_estimates"][ids]) / 100
|
||||
) # Q/M rate conversion is unity
|
||||
# Each estimator, aperture, Q/M; compare against signed prescribed Q (rho_ref=1 for M only).
|
||||
arc_ratio = arc / np.array([q0, -q0])[None, None, :, None]
|
||||
global_delta = (
|
||||
a["global_mass_reference"][last] - a["global_mass_reference"][first]
|
||||
)
|
||||
dense_details = {}
|
||||
if dense_root:
|
||||
absolute = (
|
||||
a["raw_absolute_aperture_mass_cumulative"][last]
|
||||
- a["raw_absolute_aperture_mass_cumulative"][first]
|
||||
) / s**2
|
||||
stock_raw = np.diff(a["global_mass_reference"][ids], axis=0) - np.diff(
|
||||
raw[ids, :, 2, 3], axis=0
|
||||
)
|
||||
partition = ledger[:, 2] - ledger[:, 0] - ledger[:, 1]
|
||||
dense_details = dict(
|
||||
quadrature_names=[
|
||||
"root_stride1",
|
||||
"root_stride2",
|
||||
"root_stride4",
|
||||
"exact_old_cadence",
|
||||
],
|
||||
root_dt_reference=1 / s**2,
|
||||
old_cadence_change_from_dense=(
|
||||
cv_variants[3] - cv_variants[0]
|
||||
).tolist(),
|
||||
root_temporal_sensitivity_max=float(
|
||||
np.max(abs(np.array(cv_variants)[1:3] - cv_variants[0]))
|
||||
),
|
||||
raw_absolute_aperture_net_interval_mass_reference=absolute.tolist(),
|
||||
prescribed_unsigned_nominal_mass_reference=float(2 * q0 * 100),
|
||||
legacy_raw_gross_field_definition="raw_gross_mass_over_nominal retains abs(window net blowing)+abs(window net suction), divided by ONE nominal aperture throughput; it is not root-interval absolute throughput.",
|
||||
raw_absolute_to_prescribed_unsigned_nominal=(
|
||||
absolute / (2 * q0 * 100)
|
||||
).tolist(),
|
||||
global_stock_raw_balance_max_abs_root_interval_reference=np.max(
|
||||
abs(stock_raw), axis=0
|
||||
).tolist(),
|
||||
global_stock_raw_balance_sum_reference=stock_raw.sum(axis=0).tolist(),
|
||||
raw_total_minus_wall_aperture_reference=partition.tolist(),
|
||||
raw_aperture_minus_blowing_suction_reference=(
|
||||
ledger[:, 1] - ledger[:, 3] - ledger[:, 4]
|
||||
).tolist(),
|
||||
measured_ray_absolute_volume_mass_reference=integrate(
|
||||
ray_times[ray_ids], a["arc_absolute_estimates"][ray_ids]
|
||||
).tolist(),
|
||||
measured_ray_cadence="Sparse old cadence; estimator-dependent, not dense-root or exact wall throughput",
|
||||
)
|
||||
records.append(
|
||||
dict(
|
||||
**dense_details,
|
||||
t_reference=[lo, hi],
|
||||
startup=hi <= 800,
|
||||
normalized_cv_residual=np.array(cv_variants).tolist(),
|
||||
@@ -156,12 +97,6 @@ def analyze(directory):
|
||||
)
|
||||
return dict(
|
||||
scale=s,
|
||||
dense_root=dense_root,
|
||||
previous_temporal_failure_retained=(
|
||||
"Prior continuation stride2 changes exceeded baseline residual in all48 audited comparisons; this diagnostic does not retroactively change those gates."
|
||||
if dense_root
|
||||
else None
|
||||
),
|
||||
complete_schedule=bool(complete_schedule),
|
||||
sample_checksum_verified=checksum_verified,
|
||||
windows=records,
|
||||
@@ -187,10 +122,8 @@ def main():
|
||||
results=results,
|
||||
physics_validation_pass=False,
|
||||
)
|
||||
if (
|
||||
not any(x.get("dense_root", False) for x in results)
|
||||
and sorted(x["scale"] for x in results) == [1, 2]
|
||||
and all(x["complete_schedule"] for x in results)
|
||||
if sorted(x["scale"] for x in results) == [1, 2] and all(
|
||||
x["complete_schedule"] for x in results
|
||||
):
|
||||
ordered = sorted(results, key=lambda x: x["scale"])
|
||||
summary = []
|
||||
|
||||
@@ -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,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,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,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())
|
||||
@@ -10,63 +10,13 @@ LINEAR = np.array([2.5, -1.5, 0.0])
|
||||
QUADRATIC = np.array([4.375, -5.25, 1.875])
|
||||
|
||||
|
||||
EVIDENCE_LIMIT = 32 * 1024**2
|
||||
|
||||
|
||||
def install_evidence(replacements, limit=None):
|
||||
"""Preflight retained bytes before replacing files; cap failure preserves old pair.
|
||||
|
||||
Staged compression bytes are temporary disk overhead, not retained evidence.
|
||||
This guards budget failures, not a transactional crash-recovery filesystem.
|
||||
"""
|
||||
temporary = set(replacements.values())
|
||||
try:
|
||||
directory = next(iter(replacements)).parent
|
||||
retained = sum(
|
||||
p.stat().st_size
|
||||
for p in directory.iterdir()
|
||||
if p.is_file() and p not in temporary
|
||||
)
|
||||
projected = retained - sum(p.stat().st_size for p in replacements if p.exists())
|
||||
projected += sum(p.stat().st_size for p in temporary)
|
||||
if limit is not None and projected > limit:
|
||||
raise RuntimeError(
|
||||
"Dense-root 32 MiB retained-evidence budget would be exceeded; last verified files preserved"
|
||||
)
|
||||
for target, staged in replacements.items():
|
||||
staged.replace(target)
|
||||
finally:
|
||||
for p in temporary:
|
||||
if p.exists():
|
||||
p.unlink()
|
||||
|
||||
|
||||
def write_evidence_text(path, text, *, dense_root=False):
|
||||
staged = path.with_name("." + path.name + ".pending")
|
||||
staged.write_text(text)
|
||||
install_evidence({path: staged}, EVIDENCE_LIMIT if dense_root else None)
|
||||
|
||||
|
||||
def sample_steps(c):
|
||||
"""Same reference times at both scales; dense startup retained."""
|
||||
if c.get("dense_root", False):
|
||||
return np.arange(c["steps"] + 1, dtype=int)
|
||||
return old_sample_steps(c)
|
||||
|
||||
|
||||
def old_sample_steps(c):
|
||||
"""Exact previous physical cadence, clipped to this campaign's endpoint."""
|
||||
s2 = c["scale"] ** 2
|
||||
end = c.get("steps", 3200 * s2) // s2
|
||||
reference = sorted(set(range(0, min(100, end) + 1)) | set(range(100, end + 1, 4)))
|
||||
reference = sorted(set(range(0, 101)) | set(range(100, 3201, 4)))
|
||||
return np.array(reference, int) * s2
|
||||
|
||||
|
||||
def absolute_aperture_interval_mass(rows, blowing, suction):
|
||||
"""Sum absolute aperture NET masses for one root interval, not per-link flux."""
|
||||
return float(abs(rows[blowing, 3].sum()) + abs(rows[suction, 3].sum()))
|
||||
|
||||
|
||||
def factors(scale):
|
||||
"""Lref=one s=1 root length, Tref=one s=1 root time, rho_ref=1."""
|
||||
return dict(
|
||||
@@ -155,7 +105,7 @@ def ray_geometry(fluid, origin, center, radius, h=0.5):
|
||||
)
|
||||
|
||||
|
||||
def reconstruct_aperture(rho, vel, geo, *, return_absolute=False):
|
||||
def reconstruct_aperture(rho, vel, geo):
|
||||
field = np.concatenate((rho[None], vel), axis=0)
|
||||
x, y, a, b = geo["ix"], geo["iy"], geo["a"], geo["b"]
|
||||
ray = (
|
||||
@@ -165,24 +115,12 @@ def reconstruct_aperture(rho, vel, geo, *, return_absolute=False):
|
||||
+ field[:, y + 1, x + 1] * a * b
|
||||
)
|
||||
estimates = []
|
||||
absolute = []
|
||||
for coeff in (LINEAR, QUADRATIC):
|
||||
wall = np.einsum("cajk,k->caj", ray, coeff)
|
||||
un = np.einsum("caj,ajc->aj", wall[1:], geo["normal"])
|
||||
q = np.sum(un * geo["ds"], axis=1)
|
||||
mass = np.sum(wall[0] * un * geo["ds"], axis=1)
|
||||
estimates.append(np.stack((q, mass), axis=-1))
|
||||
absolute.append(
|
||||
np.stack(
|
||||
(
|
||||
np.sum(abs(un) * geo["ds"], axis=1),
|
||||
np.sum(abs(wall[0] * un) * geo["ds"], axis=1),
|
||||
),
|
||||
axis=-1,
|
||||
)
|
||||
)
|
||||
if return_absolute:
|
||||
return ray, np.array(estimates), np.array(absolute)
|
||||
return ray, np.array(estimates)
|
||||
|
||||
|
||||
@@ -198,8 +136,6 @@ class MeasurementRecorder:
|
||||
self.cx = cx
|
||||
self.cy = cy
|
||||
self.records = []
|
||||
self.sparse_rays = []
|
||||
self.ray_steps = set(old_sample_steps(c).tolist())
|
||||
self.rays = [
|
||||
ray_geometry(fluid_u, (0.0, 0.0), np.array(c["center"]), c["diameter"] / 2),
|
||||
ray_geometry(leaves[1], origin_a, np.array(c["center"]), c["diameter"] / 2),
|
||||
@@ -243,16 +179,8 @@ class MeasurementRecorder:
|
||||
record["cv_inventory"] = []
|
||||
record["cv_flux"] = []
|
||||
record["momentum_flux_terms"] = []
|
||||
save_ray = not c.get("dense_root", False) or step in self.ray_steps
|
||||
ray_record = dict(
|
||||
sample_index=len(self.records),
|
||||
arc_ray_values=[],
|
||||
arc_estimates=[],
|
||||
arc_absolute_estimates=[],
|
||||
)
|
||||
if not c.get("dense_root", False):
|
||||
record["arc_ray_values"] = []
|
||||
record["arc_estimates"] = []
|
||||
record["arc_ray_values"] = []
|
||||
record["arc_estimates"] = []
|
||||
record["nearest_mass_flux"] = [
|
||||
nearest_flux[k] for k in ("uniform_adapter", "amr_api")
|
||||
]
|
||||
@@ -275,18 +203,9 @@ class MeasurementRecorder:
|
||||
rtol=2e-10,
|
||||
)
|
||||
record["momentum_flux_terms"].append(terms)
|
||||
if save_ray:
|
||||
ray, estimates, absolute = reconstruct_aperture(
|
||||
rho, vel, self.rays[idx], return_absolute=True
|
||||
)
|
||||
ray_record["arc_ray_values"].append(ray)
|
||||
ray_record["arc_estimates"].append(estimates)
|
||||
ray_record["arc_absolute_estimates"].append(absolute)
|
||||
if not c.get("dense_root", False):
|
||||
record["arc_ray_values"].append(ray)
|
||||
record["arc_estimates"].append(estimates)
|
||||
if c.get("dense_root", False) and save_ray:
|
||||
self.sparse_rays.append(ray_record)
|
||||
ray, estimates = reconstruct_aperture(rho, vel, self.rays[idx])
|
||||
record["arc_ray_values"].append(ray)
|
||||
record["arc_estimates"].append(estimates)
|
||||
self.records.append(record)
|
||||
if step // s**2 in (0, 100, 800, 1600, 3200) and step % s**2 == 0:
|
||||
fields = {}
|
||||
@@ -304,11 +223,7 @@ class MeasurementRecorder:
|
||||
fields[name + "_origin_reference"] = np.asarray(origin) / s
|
||||
fields[name + "_h_reference"] = np.array(h / s)
|
||||
file = self.output / f"fields_s{s}_tref{step//s**2}.npz"
|
||||
staged = file.with_name("." + file.name + ".pending.npz")
|
||||
np.savez_compressed(staged, **fields)
|
||||
install_evidence(
|
||||
{file: staged}, EVIDENCE_LIMIT if c.get("dense_root", False) else None
|
||||
)
|
||||
np.savez_compressed(file, **fields)
|
||||
self.snapshots.append(
|
||||
dict(
|
||||
file=file.name,
|
||||
@@ -321,32 +236,19 @@ class MeasurementRecorder:
|
||||
def flush(self):
|
||||
path = self.output / f"samples_s{self.c['scale']}.npz"
|
||||
arrays = {k: np.array([r[k] for r in self.records]) for k in self.records[0]}
|
||||
if self.c.get("dense_root", False):
|
||||
arrays["ray_sample_indices"] = np.array(
|
||||
[r["sample_index"] for r in self.sparse_rays], dtype=int
|
||||
)
|
||||
for key in ("arc_ray_values", "arc_estimates", "arc_absolute_estimates"):
|
||||
arrays[key] = np.array([r[key] for r in self.sparse_rays])
|
||||
for i, g in enumerate(self.rays):
|
||||
for k in ("normal", "ds", "positions"):
|
||||
arrays[f"ray_{i}_{k}"] = g[k]
|
||||
arrays["ray_offsets_fine_cells"] = OFFSETS
|
||||
arrays["linear_coefficients"] = LINEAR
|
||||
arrays["quadratic_coefficients"] = QUADRATIC
|
||||
staged = path.with_name("." + path.name + ".pending.npz")
|
||||
np.savez_compressed(staged, **arrays)
|
||||
np.savez_compressed(path, **arrays)
|
||||
metadata = dict(
|
||||
schema=2 if self.c.get("dense_root", False) else 1,
|
||||
schema=1,
|
||||
case=self.c,
|
||||
dense_root=bool(self.c.get("dense_root", False)),
|
||||
ray_sample_index_key=(
|
||||
"ray_sample_indices" if self.c.get("dense_root", False) else None
|
||||
),
|
||||
raw_absolute_throughput_definition="Cumulative sum of abs(blowing NET dm)+abs(suction NET dm) per root interval; unresolved within-aperture/substep reversals; not abs(window net).",
|
||||
absolute_throughput_definition="Quadrature integral of abs(reconstructed u.n), abs(reconstructed rho*u.n); not abs(signed integral), not prescribed nominal, not exact wall truth.",
|
||||
reference_conversion=factors(self.c["scale"]),
|
||||
sample_file=path.name,
|
||||
sample_sha256=hashlib.sha256(staged.read_bytes()).hexdigest(),
|
||||
sample_sha256=hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||
sample_count=len(self.records),
|
||||
snapshots=self.snapshots,
|
||||
level_stats_columns=[
|
||||
@@ -378,10 +280,6 @@ class MeasurementRecorder:
|
||||
],
|
||||
warning="Extrapolation and nearest-node values are estimator-dependent, not exact wall values. Fields/ray arrays use root-lattice units except explicitly named reference arrays.",
|
||||
)
|
||||
meta_path = self.output / f"samples_s{self.c['scale']}.json"
|
||||
meta_staged = meta_path.with_name("." + meta_path.name + ".pending")
|
||||
meta_staged.write_text(json.dumps(metadata, indent=2) + "\n")
|
||||
install_evidence(
|
||||
{path: staged, meta_path: meta_staged},
|
||||
EVIDENCE_LIMIT if self.c.get("dense_root", False) else None,
|
||||
(self.output / f"samples_s{self.c['scale']}.json").write_text(
|
||||
json.dumps(metadata, indent=2) + "\n"
|
||||
)
|
||||
@@ -18,24 +18,11 @@ CY = np.array([0, 0, 0, 1, -1, 1, -1, -1, 1])
|
||||
W = np.array([4 / 9] + [1 / 9] * 4 + [1 / 36] * 4)
|
||||
|
||||
|
||||
def case(scale=1, re=100, steps=800, ddf_shifting=False):
|
||||
def case(scale=1, re=100, steps=800):
|
||||
u = 0.03 / scale
|
||||
d = 24.084 * scale
|
||||
return dict(
|
||||
scale=scale,
|
||||
ddf_shifting=bool(ddf_shifting),
|
||||
storage_variant=dict(
|
||||
name="shifted_fp32" if ddf_shifting else "unshifted_fp32",
|
||||
device_populations="g=f-w" if ddf_shifting else "f",
|
||||
device_dtype="float32",
|
||||
equilibrium_path=(
|
||||
"compute_feq_from_momentum" if ddf_shifting else "compute_feq"
|
||||
),
|
||||
initial_physical_seed="historical FP32-rounded equilibrium, identical in both variants",
|
||||
downloaded_populations="physical f, decoded by public stepper.download()",
|
||||
downloaded_dtype="float64" if ddf_shifting else "float32",
|
||||
raw_exchange="physical reaction and mass; no host weight correction",
|
||||
),
|
||||
re_d=re,
|
||||
beta=0.1,
|
||||
nx=192 * scale,
|
||||
@@ -239,7 +226,7 @@ def run_case(c, ctx, output, provenance, continuation=False):
|
||||
collision="SRT",
|
||||
streaming="double_buffer",
|
||||
store_precision="FP32",
|
||||
ddf_shifting=c.get("ddf_shifting", False),
|
||||
ddf_shifting=False,
|
||||
les_enabled=False,
|
||||
)
|
||||
fine_cfg = LBMConfig(
|
||||
@@ -250,7 +237,7 @@ def run_case(c, ctx, output, provenance, continuation=False):
|
||||
collision="SRT",
|
||||
streaming="double_buffer",
|
||||
store_precision="FP32",
|
||||
ddf_shifting=c.get("ddf_shifting", False),
|
||||
ddf_shifting=False,
|
||||
les_enabled=False,
|
||||
)
|
||||
obj = SimObject(
|
||||
@@ -363,10 +350,6 @@ def run_case(c, ctx, output, provenance, continuation=False):
|
||||
for name in ("uniform_adapter", "amr_api")
|
||||
}
|
||||
|
||||
from jet_cv_measurements import absolute_aperture_interval_mass
|
||||
|
||||
cumulative_absolute_aperture_mass = np.zeros(2)
|
||||
|
||||
def capture(step):
|
||||
ug = uniform.download()
|
||||
levels = amr.download()
|
||||
@@ -398,10 +381,6 @@ def run_case(c, ctx, output, provenance, continuation=False):
|
||||
for name in ("uniform_adapter", "amr_api")
|
||||
]
|
||||
)
|
||||
if c.get("dense_root", False):
|
||||
recorder.records[-1][
|
||||
"raw_absolute_aperture_mass_cumulative"
|
||||
] = cumulative_absolute_aperture_mass.copy()
|
||||
return data, error, aperture
|
||||
|
||||
previous, _, previous_aperture = capture(0)
|
||||
@@ -448,17 +427,7 @@ def run_case(c, ctx, output, provenance, continuation=False):
|
||||
np.testing.assert_allclose(
|
||||
ar.sum(axis=0)[:2], obs["total"]["impulse"], atol=1e-13, rtol=1e-13
|
||||
)
|
||||
np.testing.assert_allclose(
|
||||
ar[:, 3].sum(), obs["total"]["mass_into_fluid"], atol=1e-13, rtol=1e-13
|
||||
)
|
||||
for backend_index, (name, rr) in enumerate(
|
||||
[("uniform_adapter", ur), ("amr_api", ar)]
|
||||
):
|
||||
cumulative_absolute_aperture_mass[
|
||||
backend_index
|
||||
] += absolute_aperture_interval_mass(
|
||||
rr, masks["blowing"], masks["suction"]
|
||||
)
|
||||
for name, rr in [("uniform_adapter", ur), ("amr_api", ar)]:
|
||||
for k, mask in masks.items():
|
||||
subtotal = rr[mask].sum(axis=0)
|
||||
accumulated[name][k] += subtotal
|
||||
@@ -550,12 +519,8 @@ def run_case(c, ctx, output, provenance, continuation=False):
|
||||
physics_validation_pass=False,
|
||||
precision="FP32 evolution, FP64 host diagnostics",
|
||||
)
|
||||
from jet_cv_measurements import write_evidence_text
|
||||
|
||||
write_evidence_text(
|
||||
output / f"re{c['re_d']}_s{c['scale']}.json",
|
||||
json.dumps(payload, indent=2) + "\n",
|
||||
dense_root=c.get("dense_root", False),
|
||||
(output / f"re{c['re_d']}_s{c['scale']}.json").write_text(
|
||||
json.dumps(payload, indent=2) + "\n"
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
@@ -587,33 +552,16 @@ def main():
|
||||
action="store_true",
|
||||
help="Fixed measurement campaign: Re100, scale1/2, 3200 reference steps",
|
||||
)
|
||||
p.add_argument(
|
||||
"--dense-root",
|
||||
action="store_true",
|
||||
help="Bounded ref0..800 scalar diagnostics at every root interval; sparse rays/fields",
|
||||
)
|
||||
p.add_argument(
|
||||
"--shifted-fp32",
|
||||
action="store_true",
|
||||
help="Opt-in shifted numerical-representation control for --dense-root; historical unshifted default unchanged",
|
||||
)
|
||||
p.add_argument("--plan", action="store_true")
|
||||
a = p.parse_args()
|
||||
if a.shifted_fp32 and not a.dense_root:
|
||||
p.error("--shifted-fp32 requires the bounded --dense-root campaign")
|
||||
if a.steps <= 0 or a.steps > 3200:
|
||||
p.error("steps must be 1..3200")
|
||||
if a.re == 400 and not a.allow_re400:
|
||||
p.error("Re400 requires explicit conditional-campaign opt-in")
|
||||
if a.dense_root:
|
||||
if a.continuation or a.scale not in (1, 2) or a.re != 100 or a.steps != 800:
|
||||
p.error(
|
||||
"--dense-root requires --re 100 --scale 1/2 --steps 800, without --continuation"
|
||||
)
|
||||
if a.continuation:
|
||||
if a.scale not in (1, 2) or a.re != 100 or a.steps != 3200:
|
||||
p.error("--continuation requires --re 100 --scale 1/2 --steps 3200")
|
||||
c = case(a.scale, a.re, a.steps, ddf_shifting=a.shifted_fp32)
|
||||
c = case(a.scale, a.re, a.steps)
|
||||
if a.continuation:
|
||||
from jet_cv_measurements import sample_steps, factors
|
||||
|
||||
@@ -627,28 +575,12 @@ def main():
|
||||
post_ready_budget_seconds=1200 if a.scale == 1 else 5400,
|
||||
runtime_estimate_seconds=[210, 500] if a.scale == 1 else [900, 3200],
|
||||
)
|
||||
if a.dense_root:
|
||||
from jet_cv_measurements import sample_steps, old_sample_steps, factors
|
||||
|
||||
c.update(
|
||||
dense_root=True,
|
||||
measurement_schema=2,
|
||||
reference_conversion=factors(a.scale),
|
||||
post_ready_budget_seconds=1200 if a.scale == 1 else 5400,
|
||||
runtime_estimate_seconds=[60, 240] if a.scale == 1 else [600, 2400],
|
||||
)
|
||||
c.update(
|
||||
sample_count=len(sample_steps(c)),
|
||||
sparse_ray_sample_count=len(old_sample_steps(c)),
|
||||
scalar_sample_dt_reference=1 / a.scale**2,
|
||||
raw_interval_dt_reference=1 / a.scale**2,
|
||||
)
|
||||
plan = dict(
|
||||
case=c,
|
||||
required_revision=PIN,
|
||||
required_free_device_mib=1024,
|
||||
estimated_peak_device_mib=128 * a.scale**2,
|
||||
estimated_output_mib=32 if a.dense_root else (64 if a.continuation else 2),
|
||||
estimated_output_mib=64 if a.continuation else 2,
|
||||
max_steps=c["steps"],
|
||||
gpu_launch_owner="root; this runner does not select or reserve a GPU",
|
||||
)
|
||||
@@ -680,9 +612,7 @@ def main():
|
||||
free, total = cuda.mem_get_info()
|
||||
if free < max(1024, 2 * plan["estimated_peak_device_mib"]) * 1024**2:
|
||||
raise RuntimeError("Insufficient free device memory")
|
||||
run_case(
|
||||
c, ctx, a.output, provenance, continuation=a.continuation or a.dense_root
|
||||
)
|
||||
run_case(c, ctx, a.output, provenance, continuation=a.continuation)
|
||||
print(
|
||||
json.dumps(
|
||||
dict(status="COMPLETE_DIAGNOSTIC_ONLY", physics_validation_pass=False)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
Reference in new issue
Block a user