Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9660894a1b | ||
|
|
ba3772e3b8 | ||
|
|
b4692e6ec4 | ||
|
|
5731c5171b | ||
|
|
05de02ee60 | ||
|
|
87c3a3165e |
No files matched your search
@@ -0,0 +1,35 @@
|
|||||||
|
# Compatibility and API changes
|
||||||
|
|
||||||
|
Changes that can make a previously accepted setup or call behave differently.
|
||||||
|
Each entry names the commit, the affected entry points and the observable result.
|
||||||
|
|
||||||
|
## 87c3a31: cut-link ownership is validated for every body
|
||||||
|
|
||||||
|
`validate_cut_link_ownership` (`body/preprocess/flag_overlay.py`) now runs for
|
||||||
|
passive bodies as well as Jet/aperture bodies. Every cut link must have an
|
||||||
|
active fluid receiver (FLUID set, SOLID clear), and each `(receiver, direction)`
|
||||||
|
population may have only one writer.
|
||||||
|
|
||||||
|
Rejected with `ValueError`, before flags, populations or device buffers change:
|
||||||
|
|
||||||
|
1. Overlapping or touching passive solids whose cut links write the same
|
||||||
|
population ("Boundary requires unique cut-link writers").
|
||||||
|
2. A body whose cut-link receiver is a non-fluid cell, e.g. a channel wall row
|
||||||
|
or the inlet/outlet column ("Cut links must have active fluid receivers").
|
||||||
|
|
||||||
|
Both were previously accepted silently for passive bodies; the interpolated
|
||||||
|
boundary then had undefined results. The rules apply to the cut links of all
|
||||||
|
bodies together, so a passive body overlapping a Jet body is rejected as well.
|
||||||
|
The check runs in `Simulation.initialize`, runtime body sync (`sync_bodies`),
|
||||||
|
checkpoint save and checkpoint load (`common/checkpoint.py`). Load validates
|
||||||
|
before restoring anything, so a checkpoint whose saved geometry violates either
|
||||||
|
rule no longer restores; rebuild the geometry with separated bodies.
|
||||||
|
|
||||||
|
## 87c3a31: `Simulation.read_surface_exchange` accepts Jet-driven bodies
|
||||||
|
|
||||||
|
`read_surface_exchange(id)` previously required `SurfaceActuation` on the body.
|
||||||
|
It now also accepts bodies that own Jet aperture links, including Jets with zero
|
||||||
|
or negative amplitude (the aperture test is geometric). Bodies with neither
|
||||||
|
`SurfaceActuation` nor aperture links still raise
|
||||||
|
`ValueError("Body has no surface actuation")`. The value is unchanged: the latest
|
||||||
|
step's wall/aperture exchange, not total nozzle force.
|
||||||
@@ -56,3 +56,62 @@ Root must independently review and admit any execution. An admitted paired pilot
|
|||||||
the existing watchdog, device selection, `--gpu-gates-approved`, positive
|
the existing watchdog, device selection, `--gpu-gates-approved`, positive
|
||||||
`--max-wall-seconds` and a new output directory, adding only `--exchange-window` to the
|
`--max-wall-seconds` and a new output directory, adding only `--exchange-window` to the
|
||||||
selected collision's pilot command. No GPU or remote action was performed for this patch.
|
selected collision's pilot command. No GPU or remote action was performed for this patch.
|
||||||
|
|
||||||
|
## Public Jet diagnostic option
|
||||||
|
|
||||||
|
`--collision MRT --exchange-window --jet-scenario zero|fixed|reverse` selects
|
||||||
|
unshifted FP32, no LES, constant transfer on both existing public backends. Default
|
||||||
|
`none` and the historical manifests remain unchanged. `zero` registers both Jets at
|
||||||
|
zero amplitude and retains every existing DFG verdict, including pressure. `fixed`
|
||||||
|
uses outward radial parabolic sectors at pi/2 and 3pi/2, half-width pi/12, peaks
|
||||||
|
(+.001,-.001). Both use the existing D40/D80 geometry and U=.01. Sector origins are
|
||||||
|
(80.5,81) in root edge coordinates and (160,160.5) in uniform node coordinates;
|
||||||
|
radial extent is [0,1.005R]. `reverse` keeps those peaks through t*=300, updates both
|
||||||
|
public setters at that completed boundary, and continues to 305 with opposite signs.
|
||||||
|
|
||||||
|
Fixed diagnostic windows are 250–260, 260–270, 270–280, 280–290 and 290–300; reversal
|
||||||
|
adds 300–305. Histories retain exact interval endpoints and root/uniform lattice
|
||||||
|
impulses. The refined window uses total exchange; uniform uses public `read_force`
|
||||||
|
batch averages. Driven windows report mean/min/max/RMS Cd/Cl without applying DFG
|
||||||
|
reference or periodicity gates. Exit zero means healthy execution, not qualification;
|
||||||
|
`matched_case_pass` remains false. No independent CV or stress-load claim is made.
|
||||||
|
|
||||||
|
This opt-in records current source/helper hashes with the existing manifest format,
|
||||||
|
checkout revision/dirty state and runner hash. Pilot/full must match that snapshot
|
||||||
|
and the exact scenario. It does not relabel current source as historical evidence.
|
||||||
|
Use the existing bounded plan/pilot/full commands with the three option flags above;
|
||||||
|
full requires the matching completed t*=5 paired pilot. `step_seconds` includes force
|
||||||
|
readback; observation/overhead and total wall time are separate, not kernel throughput.
|
||||||
|
|
||||||
|
Add `--observe-cv` to record independent scalar control-volume samples from the
|
||||||
|
already downloaded public macros. The pilot exercises this path at t*=.02..5;
|
||||||
|
full records 501 endpoints at 290..300. Each `cv_history.jsonl` row contains actual
|
||||||
|
root time, preceding interval start/duration/impulse, and two inventories/outward
|
||||||
|
fluxes ordered [mass,Px,Py]. Exclude the first row's preceding impulse: that row
|
||||||
|
supplies the initial inventory. Uniform native impulse converts by .5² to root
|
||||||
|
units; refined ledger impulse is already in root units. Explicit native-aligned
|
||||||
|
contours differ by .25 root cells in x and are recorded in the plan. Cropping
|
||||||
|
retains the two-cell face-gradient stencils. No fields or per-link arrays are saved.
|
||||||
|
`cv_observation_seconds` is the CV portion of observation/overhead time. Helper hash
|
||||||
|
and the observation flag must match pilot/full. Time integration, temporal
|
||||||
|
coarsening and any physical assessment remain separate offline work.
|
||||||
|
|
||||||
|
## Bounded resolution and placement controls
|
||||||
|
|
||||||
|
Defaults remain `--case both --uniform-diameter 80 --placement wide`. Single-case
|
||||||
|
runs use `--case uniform` or `--case refined` and never report a matched-case pass.
|
||||||
|
Their own reference/periodicity or driven-diagnostic results remain available.
|
||||||
|
A single zero-Jet full run exits successfully only when its periodicity and the
|
||||||
|
unchanged complete DFG reference verdict pass; driven completion is not qualification.
|
||||||
|
|
||||||
|
The D160 option requires `--case uniform --uniform-diameter 160` and an explicit
|
||||||
|
public Jet scenario. U and Jet speeds stay .01 and ±.001; viscosity is .016,
|
||||||
|
grid 3521×658, center (320,320.5), radius80. CV uses root spacing/time factor .25,
|
||||||
|
origin (.375,.75), impulse factor .0625, root viscosity .004, and x-face shift .125.
|
||||||
|
|
||||||
|
`--case refined --placement compact` selects root bounds (48,48,128,128), fine
|
||||||
|
168×168 and origin (46,46), with the same D40/fineD80 body/Jet resolution. Its CV
|
||||||
|
contours are (52,52,112,112) and (50,50,120,120); complete face-gradient stencils
|
||||||
|
are clear of both solids and the transfer footprint. Compact is refined-only.
|
||||||
|
CV metadata gives each grid's spacing/origin/contours explicitly. Case, uniform
|
||||||
|
diameter and placement must match the newly completed pilot in every receipt.
|
||||||
@@ -159,303 +159,3 @@ theoretical stress-consistency certification or production-kernel change.
|
|||||||
## Repository integration
|
## Repository integration
|
||||||
|
|
||||||
The four Python harness files remain together under tests/validation/taylor_green, so runtime provenance hashes precisely this harness directory. Formatting changes produce a new harness identity; preparation and integrated receipts must not be mixed. All analytic definitions, numerical floors, assessment gates, sample times, and source-import assertions are unchanged. Production source is unchanged from the cd3 snapshot; these periodic tests do not waive the separate channel GPU gate.
|
The four Python harness files remain together under tests/validation/taylor_green, so runtime provenance hashes precisely this harness directory. Formatting changes produce a new harness identity; preparation and integrated receipts must not be mixed. All analytic definitions, numerical floors, assessment gates, sample times, and source-import assertions are unchanged. Production source is unchanged from the cd3 snapshot; these periodic tests do not waive the separate channel GPU gate.
|
||||||
|
|
||||||
## Oblique shear wave (test-only diagnostic, not GPU-qualified)
|
|
||||||
|
|
||||||
`shear_wave.py` reuses the frozen oracle's constants, cell centers and
|
|
||||||
geometry. `oracle.py` is byte-identical, and `test_shear_wave.py` pins its
|
|
||||||
eec4677 SHA-256. `run_case.py --flow shear_wave` selects the shear wave;
|
|
||||||
the default flow and its receipt are the unchanged Taylor–Green path.
|
|
||||||
|
|
||||||
### Convention
|
|
||||||
|
|
||||||
Integer mode (2,1) on the L=N periodic root domain:
|
|
||||||
- the wavevector `k = (2*pi/L)*(2,1)` points 26.565° from +x;
|
|
||||||
- mode (1,2) would point 63.435°, and a wrong-mode test catches a swap.
|
|
||||||
|
|
||||||
Polarization is `E = (-1,2)/sqrt(5)`, perpendicular to k:
|
|
||||||
|
|
||||||
u = U*E*sin(k.x + phi)*exp(-nu*|k|^2*t), phi = 0, p = const, rho0 = 1
|
|
||||||
|
|
||||||
`nu = U*N/100`, the Taylor–Green Re100 viscosity at the same N. This is an
|
|
||||||
exact incompressible Navier–Stokes solution. The initial condition is FP64
|
|
||||||
second-order equilibrium at the actual leaf centers, with no
|
|
||||||
non-equilibrium part.
|
|
||||||
|
|
||||||
### Physical caveats
|
|
||||||
|
|
||||||
1. *Scope of the cubic-term argument.* The second-order D2Q9 equilibrium
|
|
||||||
lacks the cubic third moment `rho*u_a*u_b*u_g` of the Maxwellian (Dellar,
|
|
||||||
https://people.maths.ox.ac.uk/dellar/papers/Galilean.pdf, eq. 3). At
|
|
||||||
Navier–Stokes order this produces a stress error proportional to
|
|
||||||
`d_g(rho u_a u_b u_g)`.
|
|
||||||
|
|
||||||
For the ideal constant-density, zero-background transverse wave, k.E = 0
|
|
||||||
makes that contracted derivative vanish. That is the only claim made.
|
|
||||||
It does not remove other discrete or finite-Mach errors. It is not
|
|
||||||
guaranteed once AMR introduces density, longitudinal or higher-mode
|
|
||||||
contamination. The analytic NS field itself is exact.
|
|
||||||
2. *Startup.* Equilibrium initialization is not the discrete shear
|
|
||||||
eigenmode (Mei et al., https://doi.org/10.1016/j.compfluid.2005.08.008).
|
|
||||||
- The decay rate is a free-intercept least-squares fit of ln R over the
|
|
||||||
samples with t* >= 0.12, where `R = hypot(a_hat, b_hat)`.
|
|
||||||
- The intercept cancels only a constant amplitude prefactor, and only
|
|
||||||
once a single exponential dominates.
|
|
||||||
- Per-interval rates and the maximum log-fit residual are kept, so a
|
|
||||||
lingering transient stays visible.
|
|
||||||
- Late sampling alone does not show that transients have gone.
|
|
||||||
|
|
||||||
### Metrics
|
|
||||||
|
|
||||||
Computed on ACTIVE leaf cells, each weighted by its area `4**-level` in
|
|
||||||
root-cell units. The partition must cover the domain exactly once, and one
|
|
||||||
global pressure gauge is used.
|
|
||||||
|
|
||||||
- `a_hat`, `b_hat`: weighted least-squares fit of `u.E` onto sin/cos, using
|
|
||||||
the 2x2 Gram matrix of the actual composite quadrature. A wrong Fourier
|
|
||||||
mode need not project to exactly zero on this quadrature.
|
|
||||||
- `amplitude_error = R/A_exact - 1`.
|
|
||||||
- `phase_drift = atan2(b_hat, a_hat)`, in radians, reported separately.
|
|
||||||
- Rates are positive (minus the fitted slope).
|
|
||||||
- Per root step they are compared with `nu*|k|^2`. Uniform 2N uses its
|
|
||||||
own `nu = 0.0768` at N=256 and doubled step counts.
|
|
||||||
- In t* they are compared with `5*(2*pi)^2/Re = 1.97392088`.
|
|
||||||
- `rate_error = |rate/exact - 1|`.
|
|
||||||
- `velocity_l2`, `residual_l2` (velocity left after removing the fitted
|
|
||||||
mode) and `longitudinal_l2` (the `u.k_hat` part), all relative to the
|
|
||||||
exact-field norm. A longitudinal perturbation also raises the
|
|
||||||
full-vector residual and the velocity L2.
|
|
||||||
- `spurious_pressure_rms`: the raw gauged rms of `p = (rho-1)/3`.
|
|
||||||
- `spurious_pressure_scaled`: that rms divided by `A_exact(t)^2/4`, a
|
|
||||||
continuum reference scale. It is *not* the discrete, globally gauged
|
|
||||||
Taylor–Green norm on the composite grid. Exact pressure is constant, so
|
|
||||||
no relative-to-exact claim is made. Max and per-level values are kept.
|
|
||||||
|
|
||||||
### Preregistered screens (`shear_wave.assess`)
|
|
||||||
|
|
||||||
Run set: uniform N, uniform 2N, and refined depth-2 runs at root N.
|
|
||||||
|
|
||||||
- `eps(N) = (|k| h_root)^2 = 5*(2*pi/N)^2`: dimensionless, coefficient
|
|
||||||
one, 0.01205 at N=128. This is a chosen screen, not a proven error bound.
|
|
||||||
- Absolute screens:
|
|
||||||
- rate_error, max |phase_drift| (radians) and max residual_l2 must each
|
|
||||||
be <= eps;
|
|
||||||
- velocity_l2 <= 0.05, mass <= 1e-5, spurious_pressure_scaled <= 0.02.
|
|
||||||
- Budgets keep the `assess.py` form, `refined <= max(2*control_N, floor)`:
|
|
||||||
- floors are 0.002 for rate and residual, 0.02 for pressure;
|
|
||||||
- the comparative pressure budget is always redundant:
|
|
||||||
`max(2*control, 0.02)` is never below the 0.02 absolute ceiling;
|
|
||||||
- raw ratios to both the root and finest controls, including raw
|
|
||||||
pressure rms, are always reported.
|
|
||||||
- Every record is validated before any screen is applied:
|
|
||||||
- exact scheduled steps and t*, with no duplicates;
|
|
||||||
- every numeric field finite, including list fields and density;
|
|
||||||
- error quantities nonnegative and rho_min > 0;
|
|
||||||
- signed quantities (amplitudes, `amplitude_error`, `phase_drift`,
|
|
||||||
per-interval rate errors) keep their sign.
|
|
||||||
- Outcomes are kept distinct:
|
|
||||||
- INVALID_CONTROL: a control or refined run is missing, a control is
|
|
||||||
incomplete or invalid, or the records come from mixed sources;
|
|
||||||
- INVALID_RUN: a refined record fails validation;
|
|
||||||
- CONTROL_SCREEN_FAIL: complete, valid controls fail their own screens.
|
|
||||||
Each refined run's `own_screens_pass` is still reported, but
|
|
||||||
`controlled_pass` is false, so it is never a controlled-comparison pass;
|
|
||||||
- PASS/FAIL: every refined run's `controlled_pass` (own screens and
|
|
||||||
budgets against valid, passing controls).
|
|
||||||
- Finite modal extinction (late R = 0) is a physical failure, not
|
|
||||||
malformed data. The run gets `rate_fit_valid=false` with null fit
|
|
||||||
quantities, and its other metrics are kept. Nothing is clipped and no
|
|
||||||
rate is invented. The result is CONTROL_SCREEN_FAIL for a control, or
|
|
||||||
FAIL for a refined run.
|
|
||||||
- Screens are never retuned.
|
|
||||||
|
|
||||||
### Limitations
|
|
||||||
|
|
||||||
One flow, one phase and one rectangle, SRT only. The rectangle's vertical
|
|
||||||
faces span 0.625 tangential periods and its horizontal faces 1.5, so this
|
|
||||||
is not placement robustness. Two resolutions show a trend, not an order.
|
|
||||||
|
|
||||||
### Receipt assessment and runner integrity
|
|
||||||
|
|
||||||
- Fixed four-case N128 contract:
|
|
||||||
`python assess_shear_wave.py U128.json U256.json R128_constant.json R128_projected.json --output report.json --expected-revision <sha>`.
|
|
||||||
It reuses the shared provenance, build, workload and sample-time checks
|
|
||||||
from `assess_projected_pairs.py`, and adds shear metadata checks (flow,
|
|
||||||
mode, polarization, angle, phase, eps, limits, disabled early trace, and
|
|
||||||
pinned `shear_wave.py` hash).
|
|
||||||
- Missing, extra, duplicate or mixed-source records give `INVALID_SET`,
|
|
||||||
as does any failed shared check.
|
|
||||||
- Otherwise the result comes from `shear_wave.assess`, which separates
|
|
||||||
`INVALID_CONTROL` from `CONTROL_SCREEN_FAIL`.
|
|
||||||
- `run_case.verify_source(..., flow="shear_wave")` requires the imported
|
|
||||||
`shear_wave.py` to be the selected harness file with the pinned
|
|
||||||
`SHEAR_WAVE_SHA256`. The default TG flow does not import it.
|
|
||||||
- Sample and READY finiteness use `finite_row`, which accepts scalar and
|
|
||||||
list diagnostics. For TG's all-scalar rows it behaves the same as before.
|
|
||||||
- Merge note: native 92b8afe adds a `scaled_n256` receipt block. When the
|
|
||||||
two are merged, keep both blocks and make `--flow shear_wave` and
|
|
||||||
`scaled_n256` mutually exclusive. Not merged here.
|
|
||||||
|
|
||||||
### Scaled shear diagnostic (opt-in `scaled_shear_n256`)
|
|
||||||
|
|
||||||
Runner: `run_case.py --flow shear_wave --diagnostic scaled_shear_n256 --re 100`
|
|
||||||
with one of these four cases:
|
|
||||||
- `--n 256 --depth 1` (U256)
|
|
||||||
- `--n 512 --depth 1` (U512)
|
|
||||||
- `--n 256 --depth 2 --explosion constant` (constant R256)
|
|
||||||
- `--n 256 --depth 2 --explosion projected_linear` (projected R256)
|
|
||||||
|
|
||||||
Everything physical is unchanged from N128:
|
|
||||||
- `rectangles(256,2)` covers the same physical box;
|
|
||||||
- same t* schedule (16N root steps);
|
|
||||||
- `nu = U*N/100`;
|
|
||||||
- same `eps(N)`, limits and floors.
|
|
||||||
|
|
||||||
`scaled_n256` (Taylor–Green, from native 92b8afe) and `scaled_shear_n256`
|
|
||||||
each require their own flow, so they are mutually exclusive. N512 is
|
|
||||||
accepted only through one of the two diagnostics. The shear diagnostic
|
|
||||||
takes no early-pressure trace.
|
|
||||||
|
|
||||||
Assessment:
|
|
||||||
|
|
||||||
python assess_shear_wave.py U256.json U512.json R256_constant.json R256_projected.json \
|
|
||||||
--diagnostic scaled_shear_n256 --reference-files <four de5b0cc N128 receipts> \
|
|
||||||
--output report.json --expected-revision <sha>
|
|
||||||
|
|
||||||
- The scaled N256/512 case, build and workload checks come from the
|
|
||||||
92b8afe assessor. Its TG-only diagnostic identity and early-sampling
|
|
||||||
checks are not used. Instead every record must carry
|
|
||||||
`diagnostic=scaled_shear_n256`, and N128 records must carry none.
|
|
||||||
- The reference N128 receipts are hash-pinned to the audited de5b0cc
|
|
||||||
evidence (commit cba6bca). They are never relabelled as fresh controls.
|
|
||||||
- The report always includes, for each case:
|
|
||||||
- the raw signed rate bias `rate/exact - 1` and the interval errors;
|
|
||||||
- each refined run's signed excess over *each* uniform control (rate
|
|
||||||
bias, residual, raw pressure rms);
|
|
||||||
- the N128→N256 signed changes and ratios. The ratio is null when the
|
|
||||||
N128 value is zero.
|
|
||||||
- These are descriptive. Passing or failing another floor-controlled
|
|
||||||
screen is not the conclusion, and a two-resolution ratio is not a
|
|
||||||
convergence order.
|
|
||||||
- Portability: the receipt's recomputed `eps = |k|^2` is compared within
|
|
||||||
4 ULPs (`close_ulps`), not by exact equality. A cross-runtime replay
|
|
||||||
differed by one ULP: 0.012047856934923534 on Horsea vs
|
|
||||||
0.012047856934923532 elsewhere. Source and file hashes, case identity,
|
|
||||||
the other metadata and every scientific threshold stay exact.
|
|
||||||
|
|
||||||
### Nested shear preset (opt-in `nested_shear_n128`)
|
|
||||||
|
|
||||||
A fixed three-level, two-region preset, defined in `shear_nested.py`. The
|
|
||||||
wave, phase, Re100/U=0.03, initialization, five sample times, eps(root N),
|
|
||||||
limits and floors are the same as before.
|
|
||||||
|
|
||||||
- Refinements, given as parent-frame `(x0, y0, x1, y1)` rectangles on
|
|
||||||
level-wide, zero-origin unions:
|
|
||||||
- L0→L1: `(16,16,60,112)`, `(68,24,112,120)`
|
|
||||||
- L1→L2: `(44,44,108,212)`, `(148,60,212,228)`
|
|
||||||
- Leaves `[7936, 12288, 86016]`, hops `[2048, 4096, 8192]`, work
|
|
||||||
771227648 per refined case; 3723493376 for all four cases.
|
|
||||||
- Level ν `[.0384, .0768, .1536]`, τ `[.6152, .7304, .9608]`.
|
|
||||||
|
|
||||||
Cases (`run_case.py --flow shear_wave --diagnostic nested_shear_n128 --re 100`):
|
|
||||||
- `--n 128 --depth 1`
|
|
||||||
- `--n 512 --depth 1` — the finest control is 4N, not 2N
|
|
||||||
- `--n 128 --depth 3`
|
|
||||||
- `--n 128 --depth 3 --explosion projected_linear [--final-snapshot]`
|
|
||||||
|
|
||||||
`--final-snapshot` is allowed only for the projected depth-3 case. It writes
|
|
||||||
`<output>.final_macro.npz`, containing:
|
|
||||||
- ACTIVE leaves packed as `level`/`i`/`j`;
|
|
||||||
- `rho`, `ux`, `uy` from the FP64 physical populations at the final root
|
|
||||||
synchronization;
|
|
||||||
- embedded metadata: exact time, phase, mode and config.
|
|
||||||
|
|
||||||
It contains no populations, restart state or time series. Its SHA-256 and
|
|
||||||
size go into the receipt. A file over 8 MiB is removed and reported, never
|
|
||||||
kept. All old paths stay scalar-only.
|
|
||||||
|
|
||||||
Assessment: `assess_shear_wave.py U128.json U512.json R128c.json R128p.json
|
|
||||||
--diagnostic nested_shear_n128 --output report.json --expected-revision <sha>`.
|
|
||||||
- It reuses the case-agnostic provenance checks from the TG assessor, plus
|
|
||||||
exact nested scales, builds, patch, workload, metadata and the pinned
|
|
||||||
`shear_nested.py` hash.
|
|
||||||
- The physics is `shear_wave.assess` generalized only in its depth-3 / 4N
|
|
||||||
shape contract; the metrics are unchanged.
|
|
||||||
- `shear_wave.py` is byte-identical (81d5c8ee…), so historical receipt
|
|
||||||
identities and verdicts are untouched.
|
|
||||||
- This preset changes placement and refined volume as well as depth. It
|
|
||||||
gives no isolated cost of an extra level and no resolution trend.
|
|
||||||
|
|
||||||
### Depth-2 twin of the nested preset (opt-in `twin_shear_n128`)
|
|
||||||
|
|
||||||
`shear_twin.py` uses the same two L0→L1 boxes as `shear_nested.py`, with no
|
|
||||||
L2. Leaves are `[7936, 33792]`, hops `[2048, 4096]`, and work is 154664960 per
|
|
||||||
refined case (611319808 for all four).
|
|
||||||
|
|
||||||
The campaign is four fresh runs:
|
|
||||||
`run_case.py --flow shear_wave --diagnostic twin_shear_n128 --re 100` with
|
|
||||||
- `--n 128 --depth 1`
|
|
||||||
- `--n 256 --depth 1` (the finest control is 2N)
|
|
||||||
- `--n 128 --depth 2`
|
|
||||||
- `--n 128 --depth 2 --explosion projected_linear`
|
|
||||||
|
|
||||||
There is no snapshot option. Older exact controls serve only as consistency
|
|
||||||
checks; they are not reused as references.
|
|
||||||
|
|
||||||
Assessment uses the same preset contract as the nested preset, through
|
|
||||||
`assess_shear_wave.py ... --diagnostic twin_shear_n128`:
|
|
||||||
- the physics is the unchanged depth-2 / 2N `shear_wave.assess`;
|
|
||||||
- receipts must pin `shear_twin.py`, `shear_nested.py` and `shear_wave.py`;
|
|
||||||
- `shear_nested.py` stays byte-identical (fa6cfe87…), so the 335624c nested
|
|
||||||
receipts still validate.
|
|
||||||
|
|
||||||
Labels:
|
|
||||||
- *Twin vs nested* is the effect of adding this nested L2 refinement.
|
|
||||||
Removing L2 also changes the finest spacing and refined volume, so it does
|
|
||||||
not isolate abstract depth.
|
|
||||||
- *Twin vs the single-box depth-2 runs* is a placement/geometry contrast at
|
|
||||||
equal depth.
|
|
||||||
- Neither contrast is additive by construction.
|
|
||||||
- The thin-band/acoustic explanation for the nested L1 pressure is a
|
|
||||||
hypothesis, not established.
|
|
||||||
|
|
||||||
### Signed interface-probe traces (opt-in `--probe-trace`, mechanism diagnostic)
|
|
||||||
|
|
||||||
`shear_probes.py` (pinned) uses native's validated 24-target layout through
|
|
||||||
box 1, in root-128 physical coordinates:
|
|
||||||
- a horizontal line y = 56, crossing the vertical faces x = 16 and 22;
|
|
||||||
- a vertical line x = 38, crossing the horizontal faces y = 16 and 22;
|
|
||||||
- normal coordinates 12.5, 14.5, 15.5, 16.25, 17.25, 19.25, 20.75, 21.75,
|
|
||||||
22.125, 23.125, 24.125, 26.125.
|
|
||||||
|
|
||||||
Each target maps to the nearest ACTIVE leaf center. The deterministic tie
|
|
||||||
rule is: distance, then finer level, then smaller j, then smaller i. This
|
|
||||||
gives 24 unique cells in every case, and nested has 3 L0 + 5 L1 + 4 L2 per
|
|
||||||
transect. Maximum displacement from target to center is 0.625 root cells in
|
|
||||||
U128 (0.177 in U512).
|
|
||||||
|
|
||||||
Allowed cases:
|
|
||||||
- nested U128, nested U512, nested projected depth 3;
|
|
||||||
- twin projected depth 2.
|
|
||||||
|
|
||||||
The option can't be combined with `--final-snapshot`.
|
|
||||||
|
|
||||||
Sampling:
|
|
||||||
- The receipt stores each probe's leaf level, `i`/`j`, actual cell center
|
|
||||||
and displacement from its target, all in root-physical units (N=128).
|
|
||||||
- Grids differ between cases, so centers are *not* collocated. Analytic
|
|
||||||
fields are evaluated at each actual center (`shear_probes.residuals`), and
|
|
||||||
comparisons across cases are between residuals at nearby but distinct
|
|
||||||
centers.
|
|
||||||
- Times are root-physical steps 0–128 and 1920–2048. U512 is sampled every 4
|
|
||||||
lattice steps to match.
|
|
||||||
- Each sample stores `rho`, `ux`, `uy` from the FP64 physical populations,
|
|
||||||
POST-collision G at root synchronization only. No substep content is
|
|
||||||
observed, and grid-scale alternation can alias.
|
|
||||||
|
|
||||||
What's kept and enforced:
|
|
||||||
- The five qualification samples stay unchanged.
|
|
||||||
- No field dumps. The trace is validated for exact schedule, finiteness and
|
|
||||||
a 600 kB cap. If invalid it is dropped and an error is recorded, keeping
|
|
||||||
the scalar receipt.
|
|
||||||
- There are no new pass gates; this is a mechanism diagnostic only.
|
|
||||||
@@ -216,11 +216,8 @@ def surface_link_velocity(objects, cfg, flags, fluid_idx, dir, body_id, rx, ry,
|
|||||||
enabled = [obj for obj in objects if obj.surface_actuation is not None]
|
enabled = [obj for obj in objects if obj.surface_actuation is not None]
|
||||||
if not enabled and not jets:
|
if not enabled and not jets:
|
||||||
return None, None
|
return None, None
|
||||||
from ..lbm.descriptors import FLUID, SOLID
|
from .preprocess.flag_overlay import validate_cut_link_ownership
|
||||||
if np.any((flags[fluid_idx] & FLUID) == 0) or np.any(flags[fluid_idx] & SOLID):
|
validate_cut_link_ownership(flags, fluid_idx, dir, cfg.nq)
|
||||||
raise ValueError("Surface cut links must have active fluid receivers")
|
|
||||||
if len(np.unique(fluid_idx.astype(np.uint64)*cfg.nq+dir)) != len(dir):
|
|
||||||
raise ValueError("Surface boundary requires unique cut-link writers")
|
|
||||||
surface_velocity = np.zeros((len(dir), 2), np.float32)
|
surface_velocity = np.zeros((len(dir), 2), np.float32)
|
||||||
surface_aperture = np.zeros(len(dir), bool)
|
surface_aperture = np.zeros(len(dir), bool)
|
||||||
for obj in enabled:
|
for obj in enabled:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import pycuda.driver as cuda
|
|||||||
|
|
||||||
from .objects import SimObject
|
from .objects import SimObject
|
||||||
from .registry import BodyRegistry
|
from .registry import BodyRegistry
|
||||||
from .preprocess.flag_overlay import merge_flag_masks
|
from .preprocess.flag_overlay import merge_flag_masks, validate_cut_link_ownership
|
||||||
from ..cuda.compiler_v2 import obs_layout
|
from ..cuda.compiler_v2 import obs_layout
|
||||||
from ..cuda.obs_layout import ObsLayout
|
from ..cuda.obs_layout import ObsLayout
|
||||||
from ..lbm.descriptors import (
|
from ..lbm.descriptors import (
|
||||||
@@ -369,9 +369,9 @@ class ObjectManager:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
objects: List of SimObject instances to process.
|
objects: List of SimObject instances to process.
|
||||||
domain_flags: Optional uint16 flags for donor-cell classification.
|
domain_flags: Optional uint16 flags for receiver/donor classification.
|
||||||
sensor_cell_counts_out: Int array to fill with per-object sensor
|
sensor_cell_counts_out: Int array to fill with per-object sensor
|
||||||
cell counts. If provided, ``fill(0)`` is called first.
|
cell counts after the full geometry has passed validation.
|
||||||
hits_out: List that receives one FP64 (n, 2) array of true hit
|
hits_out: List that receives one FP64 (n, 2) array of true hit
|
||||||
points row-aligned with the cut-link columns (for Jets).
|
points row-aligned with the cut-link columns (for Jets).
|
||||||
|
|
||||||
@@ -394,8 +394,8 @@ class ObjectManager:
|
|||||||
active_fluid = ((domain_flags & FLUID) != 0) & ((domain_flags & SOLID) == 0)
|
active_fluid = ((domain_flags & FLUID) != 0) & ((domain_flags & SOLID) == 0)
|
||||||
force_owners: set[int] = set()
|
force_owners: set[int] = set()
|
||||||
|
|
||||||
if sensor_cell_counts_out is not None:
|
sensor_counts = (np.zeros_like(sensor_cell_counts_out)
|
||||||
sensor_cell_counts_out.fill(0)
|
if sensor_cell_counts_out is not None else None)
|
||||||
|
|
||||||
ez = None
|
ez = None
|
||||||
if self.cfg and self.cfg.is_d3q19:
|
if self.cfg and self.cfg.is_d3q19:
|
||||||
@@ -430,9 +430,9 @@ class ObjectManager:
|
|||||||
if len(cells) > 0:
|
if len(cells) > 0:
|
||||||
s_cells.append(cells)
|
s_cells.append(cells)
|
||||||
s_ids.append(ids)
|
s_ids.append(ids)
|
||||||
if (sensor_cell_counts_out is not None
|
if (sensor_counts is not None
|
||||||
and 0 <= obj.obj_id < sensor_cell_counts_out.size):
|
and 0 <= obj.obj_id < sensor_counts.size):
|
||||||
sensor_cell_counts_out[obj.obj_id] = int(len(cells))
|
sensor_counts[obj.obj_id] = int(len(cells))
|
||||||
if obj.is_force_region and hasattr(obj, 'get_sensor_list'):
|
if obj.is_force_region and hasattr(obj, 'get_sensor_list'):
|
||||||
cells, ids = obj.get_sensor_list(self.nx, self.ny, self.nz)
|
cells, ids = obj.get_sensor_list(self.nx, self.ny, self.nz)
|
||||||
keep = active_fluid[cells]
|
keep = active_fluid[cells]
|
||||||
@@ -461,8 +461,9 @@ class ObjectManager:
|
|||||||
r_fr_ids = np.concatenate(fr_ids) if fr_ids else np.zeros(0, dtype=np.int32)
|
r_fr_ids = np.concatenate(fr_ids) if fr_ids else np.zeros(0, dtype=np.int32)
|
||||||
|
|
||||||
r_hit = np.concatenate(cl_hit) if cl_hit else np.zeros((0, 2))
|
r_hit = np.concatenate(cl_hit) if cl_hit else np.zeros((0, 2))
|
||||||
if hits_out is not None:
|
# Passive walls need the same ownership guarantees as driven walls.
|
||||||
hits_out.append(r_hit)
|
# Overlapping solids are not a supported union-boundary operation.
|
||||||
|
validate_cut_link_ownership(domain_flags, r_idx, r_dir, self.nq)
|
||||||
if self.jets or any(obj.surface_actuation is not None for obj in objects):
|
if self.jets or any(obj.surface_actuation is not None for obj in objects):
|
||||||
# Reject unresolved apertures/ownership before sync or restart mutates state.
|
# Reject unresolved apertures/ownership before sync or restart mutates state.
|
||||||
from ..lbm.curved_links import CurvedLinkSoA
|
from ..lbm.curved_links import CurvedLinkSoA
|
||||||
@@ -470,6 +471,11 @@ class ObjectManager:
|
|||||||
links.assign_host(r_idx, r_dir, r_q, r_bid, r_rx, r_ry, r_rz, r_fallback)
|
links.assign_host(r_idx, r_dir, r_q, r_bid, r_rx, r_ry, r_rz, r_fallback)
|
||||||
links.assign_surface(objects, self.cfg, domain_flags, jets=self.jets, hits=r_hit)
|
links.assign_surface(objects, self.cfg, domain_flags, jets=self.jets, hits=r_hit)
|
||||||
|
|
||||||
|
if sensor_cell_counts_out is not None:
|
||||||
|
sensor_cell_counts_out[:] = sensor_counts
|
||||||
|
if hits_out is not None:
|
||||||
|
hits_out.append(r_hit)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
r_idx, r_dir, r_q, r_bid,
|
r_idx, r_dir, r_q, r_bid,
|
||||||
r_rx, r_ry, r_rz, r_fallback,
|
r_rx, r_ry, r_rz, r_fallback,
|
||||||
@@ -487,8 +493,6 @@ class ObjectManager:
|
|||||||
the caller has already prepared and uploaded the final flag field
|
the caller has already prepared and uploaded the final flag field
|
||||||
for this geometry, such as during initialization.
|
for this geometry, such as during initialization.
|
||||||
"""
|
"""
|
||||||
self._telemetry_field = field
|
|
||||||
|
|
||||||
next_flags = self.build_flags(field.build_channel_flags()) if rebuild_flags else field.flag
|
next_flags = self.build_flags(field.build_channel_flags()) if rebuild_flags else field.flag
|
||||||
hits = []
|
hits = []
|
||||||
|
|
||||||
@@ -499,6 +503,7 @@ class ObjectManager:
|
|||||||
fr_cells, fr_obj_id,
|
fr_cells, fr_obj_id,
|
||||||
) = self.build_compact_lists(domain_flags=next_flags, hits_out=hits)
|
) = self.build_compact_lists(domain_flags=next_flags, hits_out=hits)
|
||||||
|
|
||||||
|
self._telemetry_field = field
|
||||||
if rebuild_flags:
|
if rebuild_flags:
|
||||||
field.flag = next_flags
|
field.flag = next_flags
|
||||||
field.upload_flags()
|
field.upload_flags()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CelerisLab/body/preprocess/flag_overlay.py
|
# CelerisLab/body/preprocess/flag_overlay.py
|
||||||
"""
|
"""
|
||||||
Flag overlay: merge per-object flag masks onto a clean domain base.
|
Flag overlay and cut-link ownership checks against the merged domain.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -12,6 +12,15 @@ import numpy as np
|
|||||||
from ...lbm.descriptors import FLUID, SOLID, SENSOR_FLAG, FRC_REGION
|
from ...lbm.descriptors import FLUID, SOLID, SENSOR_FLAG, FRC_REGION
|
||||||
|
|
||||||
|
|
||||||
|
def validate_cut_link_ownership(flags, fluid_idx, directions, nq):
|
||||||
|
"""Reject solid receivers and multiple writers to one population."""
|
||||||
|
if np.any((flags[fluid_idx] & FLUID) == 0) or np.any(flags[fluid_idx] & SOLID):
|
||||||
|
raise ValueError("Cut links must have active fluid receivers")
|
||||||
|
writers = fluid_idx.astype(np.uint64) * nq + directions
|
||||||
|
if len(np.unique(writers)) != len(directions):
|
||||||
|
raise ValueError("Boundary requires unique cut-link writers")
|
||||||
|
|
||||||
|
|
||||||
def merge_flag_masks(
|
def merge_flag_masks(
|
||||||
base: np.ndarray,
|
base: np.ndarray,
|
||||||
masks: List[np.ndarray],
|
masks: List[np.ndarray],
|
||||||
|
|||||||
@@ -336,13 +336,19 @@ class Simulation:
|
|||||||
def read_surface_exchange(self, id: int):
|
def read_surface_exchange(self, id: int):
|
||||||
"""Latest step's wall/aperture (Fx,Fy,Tz) exchange, not total nozzle force.
|
"""Latest step's wall/aperture (Fx,Fy,Tz) exchange, not total nozzle force.
|
||||||
|
|
||||||
|
Supports SurfaceActuation and Jet-driven bodies, including zero-amplitude
|
||||||
|
apertures. Passive bodies without an aperture are rejected.
|
||||||
Unlike read_body this is not accumulated/time-averaged. It resets after
|
Unlike read_body this is not accumulated/time-averaged. It resets after
|
||||||
geometry sync or checkpoint load; advance before interpreting it.
|
geometry sync or checkpoint load; advance before interpreting it.
|
||||||
"""
|
"""
|
||||||
if self.bodies.get(id).surface_actuation is None:
|
obj = self.bodies.get(id)
|
||||||
|
curved = self.field.curved
|
||||||
|
has_aperture = (curved.aperture is not None
|
||||||
|
and np.any(curved.aperture & (curved.body_id == id)))
|
||||||
|
if obj.surface_actuation is None and not has_aperture:
|
||||||
raise ValueError("Body has no surface actuation")
|
raise ValueError("Body has no surface actuation")
|
||||||
self.stream.synchronize()
|
self.stream.synchronize()
|
||||||
return self.field.curved.surface_exchange(id)
|
return curved.surface_exchange(id)
|
||||||
|
|
||||||
@context_bound
|
@context_bound
|
||||||
def read_body(self, id: int, *,
|
def read_body(self, id: int, *,
|
||||||
@@ -451,18 +457,21 @@ class Simulation:
|
|||||||
@context_bound
|
@context_bound
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""Initialize flow field and sync objects to GPU."""
|
"""Initialize flow field and sync objects to GPU."""
|
||||||
# Recompile if objects were added after construction.
|
|
||||||
self._assert_allocation_contract()
|
self._assert_allocation_contract()
|
||||||
|
# Validate the complete geometry before flags, populations or modules
|
||||||
|
# change. Compact-list construction publishes no outputs on failure.
|
||||||
|
next_flags = self.bodies.build_flags(self.field.build_channel_flags())
|
||||||
|
if self.bodies.jets and self.bodies.count == 0:
|
||||||
|
raise ValueError("Jets need bodies with cut links")
|
||||||
|
self.bodies.build_compact_lists_for(self.bodies.objects, next_flags)
|
||||||
|
|
||||||
|
# Recompile if objects were added after construction.
|
||||||
if self._read_generated_n_objects() != self.bodies.count:
|
if self._read_generated_n_objects() != self.bodies.count:
|
||||||
self.recompile()
|
self.recompile()
|
||||||
|
|
||||||
self.field.flag = self.field.build_channel_flags()
|
self.field.flag = next_flags
|
||||||
if self.bodies.count > 0:
|
|
||||||
self.field.flag = self.bodies.build_flags(self.field.flag)
|
|
||||||
self.field.upload_flags()
|
self.field.upload_flags()
|
||||||
|
|
||||||
if self.bodies.jets and self.bodies.count == 0:
|
|
||||||
raise ValueError("Jets need bodies with cut links")
|
|
||||||
self.stepper.initialize()
|
self.stepper.initialize()
|
||||||
if self.bodies.count > 0:
|
if self.bodies.count > 0:
|
||||||
self.bodies.sync_to_gpu(self.field, rebuild_flags=False)
|
self.bodies.sync_to_gpu(self.field, rebuild_flags=False)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from run_jet_circle_cv import (
|
|||||||
case,
|
case,
|
||||||
state,
|
state,
|
||||||
cv,
|
cv,
|
||||||
|
cv_from_macros,
|
||||||
aperture_probe,
|
aperture_probe,
|
||||||
measured_aperture_flux,
|
measured_aperture_flux,
|
||||||
verify,
|
verify,
|
||||||
@@ -28,6 +29,39 @@ def eq(rho, u, v):
|
|||||||
|
|
||||||
|
|
||||||
class Host(unittest.TestCase):
|
class Host(unittest.TestCase):
|
||||||
|
def test_public_macros_pressure_advection_and_cropped_frames(self):
|
||||||
|
for h, origin, shape, rect in (
|
||||||
|
(.5, (0., 0.), (48, 64), (5., 4., 25., 20.)),
|
||||||
|
(.5, (.25, .5), (48, 64), (5.25, 4., 25.25, 20.)),
|
||||||
|
(.25, (.375, .75), (96, 128), (5.125, 4., 25.125, 20.)),
|
||||||
|
):
|
||||||
|
y, x = np.indices(shape)
|
||||||
|
x = origin[0] + h * (x + .5)
|
||||||
|
k, u, v = 1e-5, .02, -.01
|
||||||
|
rho = 1 + 3 * k * x # p=(rho-1)/3=k*x
|
||||||
|
ux, uy, fluid = np.full(shape, u), np.full(shape, v), np.ones(shape, bool)
|
||||||
|
result = cv_from_macros(rho, ux, uy, fluid, origin, rect, .004, h)
|
||||||
|
expected = 320 * np.array([3*k*u, k + 3*k*u*u, 3*k*u*v])
|
||||||
|
np.testing.assert_allclose(result[1], expected, rtol=1e-11, atol=1e-13)
|
||||||
|
np.testing.assert_allclose(result, cv(eq(rho, u, v), fluid, origin, rect, .004, h), atol=1e-12)
|
||||||
|
x0, y0, x1, y1 = np.rint((np.array(rect)-np.tile(origin, 2))/h).astype(int)
|
||||||
|
selection = np.s_[y0-2:y1+2, x0-2:x1+2]
|
||||||
|
cropped_origin = np.asarray(origin) + h*np.array([x0-2, y0-2])
|
||||||
|
cropped = cv_from_macros(rho[selection], ux[selection], uy[selection],
|
||||||
|
fluid[selection], cropped_origin, rect, .004, h)
|
||||||
|
np.testing.assert_array_equal(cropped, result)
|
||||||
|
|
||||||
|
def test_public_macros_full_normal_viscous_stress(self):
|
||||||
|
y, x = np.indices((48, 64))
|
||||||
|
x = (x + .5) * .5
|
||||||
|
rho, ux, uy, fluid = np.ones_like(x), 1e-4*x*x, np.zeros_like(x), np.ones(x.shape, bool)
|
||||||
|
args = (rho, ux, uy, fluid, (0., 0.), (5., 4., 25., 20.))
|
||||||
|
_, viscous = cv_from_macros(*args, .004)
|
||||||
|
_, inviscid = cv_from_macros(*args, 0.)
|
||||||
|
np.testing.assert_allclose(viscous-inviscid, [0, -4*.004*1e-4*320, 0], atol=1e-13)
|
||||||
|
with self.assertRaisesRegex(ValueError, "matching 2D"):
|
||||||
|
cv_from_macros(rho, ux, uy[:-1], fluid, (0, 0), (5, 4, 25, 20), .004)
|
||||||
|
|
||||||
def test_descriptor_and_uniform_closed_flux(self):
|
def test_descriptor_and_uniform_closed_flux(self):
|
||||||
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,55 @@ def jet_case(device, jets):
|
|||||||
return field, bodies
|
return field, bodies
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_surface_exchange_for_jet_preserves_aperture_at_zero_and_reverse(device):
|
||||||
|
from CelerisLab.simulation import Simulation
|
||||||
|
jet = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER, inward=False), "quartic", 2e-3)
|
||||||
|
field, bodies = jet_case(device, [jet])
|
||||||
|
sim = SimpleNamespace(bodies=bodies, field=field, _initialized=True,
|
||||||
|
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||||
|
cl = field.curved
|
||||||
|
assert bodies.get(0).surface_actuation is None
|
||||||
|
aperture, velocity = cl.aperture, cl.surface_velocity.copy()
|
||||||
|
assert aperture.any() and (~aperture).any()
|
||||||
|
# Synthetic per-link exchanges test readback/splitting, not kernel physics.
|
||||||
|
values = np.arange(cl.count * 3, dtype=np.float32).reshape(-1, 3)
|
||||||
|
device.htod(cl.link_exchange_gpu, values)
|
||||||
|
for amplitude in (2e-3, 0., -1e-3):
|
||||||
|
Simulation.set_jet_amplitude(sim, 0, amplitude)
|
||||||
|
result = Simulation.read_surface_exchange(sim, 0)
|
||||||
|
assert cl.aperture is aperture
|
||||||
|
np.testing.assert_array_equal(aperture, cl.jet_id >= 0)
|
||||||
|
np.testing.assert_allclose(cl.surface_velocity, velocity * (amplitude / 2e-3))
|
||||||
|
for name, mask in (("wall", ~aperture), ("aperture", aperture)):
|
||||||
|
np.testing.assert_array_equal(result[name], values[mask].sum(axis=0, dtype=np.float64))
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_surface_exchange_keeps_legacy_and_passive_guards(device):
|
||||||
|
from CelerisLab.simulation import Simulation
|
||||||
|
field = make_field(device)
|
||||||
|
bodies = manager(field, [circle(SurfaceActuation(0., .6, .002), radius=2.)])
|
||||||
|
bodies.sync_to_gpu(field)
|
||||||
|
sim = SimpleNamespace(bodies=bodies, field=field,
|
||||||
|
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||||
|
for value in Simulation.read_surface_exchange(sim, 0).values():
|
||||||
|
np.testing.assert_array_equal(value, 0.)
|
||||||
|
field.curved.free_gpu_columns()
|
||||||
|
with pytest.raises(RuntimeError, match="diagnostic is allocated"):
|
||||||
|
Simulation.read_surface_exchange(sim, 0)
|
||||||
|
|
||||||
|
# An exchange buffer elsewhere in the scene must not admit a passive body.
|
||||||
|
for jets in ([], [Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER))]):
|
||||||
|
field, bodies = jet_case(device, jets)
|
||||||
|
passive = bodies.add(circle(center=(2., 3.), radius=.6))
|
||||||
|
bodies.sync_to_gpu(field)
|
||||||
|
sim.bodies, sim.field = bodies, field
|
||||||
|
with pytest.raises(ValueError, match="Body has no surface actuation"):
|
||||||
|
Simulation.read_surface_exchange(sim, passive)
|
||||||
|
if not jets:
|
||||||
|
with pytest.raises(ValueError, match="Body has no surface actuation"):
|
||||||
|
Simulation.read_surface_exchange(sim, 0)
|
||||||
|
|
||||||
|
|
||||||
def test_manager_passes_true_hits_and_reuploads_only_velocity(device):
|
def test_manager_passes_true_hits_and_reuploads_only_velocity(device):
|
||||||
# CPU memory double only: checks host/device bookkeeping, not GPU numerics.
|
# CPU memory double only: checks host/device bookkeeping, not GPU numerics.
|
||||||
jet = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER, inward=False), "quartic", 2e-3)
|
jet = Jet(Sector(CENTER, 0., .6, 3.), Radial(CENTER, inward=False), "quartic", 2e-3)
|
||||||
|
|||||||
@@ -146,23 +146,26 @@ def test_full_requires_completed_pilot():
|
|||||||
r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE, "MRT", "window"))
|
r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE, "MRT", "window"))
|
||||||
|
|
||||||
|
|
||||||
def pilot_fixture(path, observation_mode="window"):
|
def pilot_fixture(path, observation_mode="window", jet_scenario="none", case_selection="both", uniform_diameter=80, placement="wide"):
|
||||||
"""Synthetic contract fixture only; never a physical result or run receipt."""
|
"""Synthetic contract fixture only; never a physical result or run receipt."""
|
||||||
manifest = (r.verify_source(r.DEFAULT_SOURCE, "MRT", "window")
|
manifest = (r.verify_source(r.DEFAULT_SOURCE, "MRT", "window", jet_scenario)
|
||||||
if observation_mode == "window" else
|
if observation_mode == "window" else
|
||||||
json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text()))
|
json.loads((r.HERE / "data/refined_dfg/source_manifest_mrt.json").read_text()))
|
||||||
|
selection = dict(case_selection=case_selection, uniform_diameter=uniform_diameter, placement=placement)
|
||||||
(path / "plan.json").write_text(
|
(path / "plan.json").write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
dict(
|
dict(
|
||||||
source_manifest=manifest,
|
source_manifest=manifest,
|
||||||
|
**selection,
|
||||||
collision="MRT",
|
collision="MRT",
|
||||||
|
jet_scenario=jet_scenario,
|
||||||
observation_mode=observation_mode,
|
observation_mode=observation_mode,
|
||||||
bounds_root_edge=list(r.BOUNDS),
|
bounds_root_edge=list(r.PLACEMENTS[placement]),
|
||||||
runner_sha256=r.runner_hash(),
|
runner_sha256=r.runner_hash(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for case, diameter in (("uniform", 80), ("refined", 40)):
|
for case, diameter in r.selected_cases(case_selection, uniform_diameter):
|
||||||
dest = path / case
|
dest = path / case
|
||||||
dest.mkdir()
|
dest.mkdir()
|
||||||
interval = round(0.02 * diameter / r.SPEED)
|
interval = round(0.02 * diameter / r.SPEED)
|
||||||
@@ -189,7 +192,9 @@ def pilot_fixture(path, observation_mode="window"):
|
|||||||
json.dumps(
|
json.dumps(
|
||||||
dict(
|
dict(
|
||||||
case=case,
|
case=case,
|
||||||
|
**selection,
|
||||||
collision="MRT",
|
collision="MRT",
|
||||||
|
jet_scenario=jet_scenario,
|
||||||
observation_mode=observation_mode,
|
observation_mode=observation_mode,
|
||||||
mode="pilot",
|
mode="pilot",
|
||||||
status="pilot_complete",
|
status="pilot_complete",
|
||||||
@@ -206,7 +211,9 @@ def pilot_fixture(path, observation_mode="window"):
|
|||||||
json.dumps(
|
json.dumps(
|
||||||
dict(
|
dict(
|
||||||
case=case,
|
case=case,
|
||||||
|
**selection,
|
||||||
collision="MRT",
|
collision="MRT",
|
||||||
|
jet_scenario=jet_scenario,
|
||||||
observation_mode=observation_mode,
|
observation_mode=observation_mode,
|
||||||
mode="pilot",
|
mode="pilot",
|
||||||
step=interval,
|
step=interval,
|
||||||
@@ -451,6 +458,169 @@ def test_window_contract_rejection():
|
|||||||
assert not backend.events
|
assert not backend.events
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_jet_configuration_and_geometry():
|
||||||
|
for diameter, center, radius in ((40, (80.5, 81), 20), (80, (160, 160.5), 40)):
|
||||||
|
cfg, _, _ = r.config(h, diameter, "MRT", "fixed")
|
||||||
|
assert not cfg["method"]["ddf_shifting"] and not cfg["method"]["les"]["enabled"]
|
||||||
|
jets = r.public_jets(center, radius, "fixed")
|
||||||
|
assert [j.amplitude for j in jets] == [.001, -.001]
|
||||||
|
assert all(j.field.inward is False and j.profile == "parabolic" for j in jets)
|
||||||
|
assert all(j.region.r_max == radius * 1.005 for j in jets)
|
||||||
|
assert all(j.amplitude == 0 for j in r.public_jets(center, radius, "zero"))
|
||||||
|
cfg, _, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT", "fixed")
|
||||||
|
assert not cfg.ddf_shifting and len(host.boundary_cells) > 0
|
||||||
|
assert r.public_jets((0, 0), 1, "none") == ()
|
||||||
|
with unittest.TestCase().assertRaises(ValueError):
|
||||||
|
r.config(h, 40, "SRT", "fixed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_jet_reversal_boundary_and_windows():
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
handle = SimpleNamespace(set_jet_amplitude=lambda i, a: calls.append((i, a)))
|
||||||
|
for diameter in (40, 80):
|
||||||
|
boundary = round(250 * diameter / r.SPEED)
|
||||||
|
assert r.jet_interval(handle, "fixed", boundary - 3, 80, diameter) == (3, None)
|
||||||
|
switch = round(300 * diameter / r.SPEED)
|
||||||
|
assert r.jet_interval(handle, "reverse", switch - 3, 80, diameter) == (3, None)
|
||||||
|
assert not calls
|
||||||
|
n, event = r.jet_interval(handle, "reverse", switch, 80, diameter)
|
||||||
|
assert n == 80 and event["step"] == switch and event["tstar"] == 300
|
||||||
|
assert calls == [(0, -.001), (1, .001)]
|
||||||
|
calls.clear()
|
||||||
|
assert r.jet_interval(handle, "reverse", switch + 80, 80, diameter) == (80, None)
|
||||||
|
assert r.jet_interval(handle, "fixed", switch, 80, diameter) == (80, None)
|
||||||
|
assert not calls
|
||||||
|
rows = [dict(tstar=t, tstar_force=t - .5, cd=2.0, cl=-.1) for t in range(251, 306)]
|
||||||
|
windows = r.jet_diagnostics(rows, "reverse")
|
||||||
|
assert len(windows) == 6 and all(w["complete"] for w in windows)
|
||||||
|
assert all(w["cd"]["mean"] == 2 and np.isclose(w["cl"]["mean"], -.1) for w in windows)
|
||||||
|
assert windows[-1]["covered_tstar"] == 5
|
||||||
|
with unittest.TestCase().assertRaisesRegex(ValueError, "crosses"):
|
||||||
|
r.jet_diagnostics([dict(tstar=250.5, tstar_force=250, cd=1, cl=0)], "fixed")
|
||||||
|
assert r.plan(h, "MRT", "window", "reverse")["modes"]["full_tstar"] == 305
|
||||||
|
|
||||||
|
|
||||||
|
def test_public_jet_source_and_pilot_separation():
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
path = Path(tmp)
|
||||||
|
manifest = pilot_fixture(path, jet_scenario="fixed")
|
||||||
|
assert manifest["source_capture"].startswith("current checkout")
|
||||||
|
assert len(manifest["checkout_revision"]) == 40
|
||||||
|
assert r.verify_pilot(path, manifest, "MRT", "window", "fixed") == 3606.0
|
||||||
|
for scenario in ("none", "zero", "reverse"):
|
||||||
|
with unittest.TestCase().assertRaisesRegex(ValueError, "Jet scenario"):
|
||||||
|
r.verify_pilot(path, manifest, "MRT", "window", scenario)
|
||||||
|
for relative in ("uniform/summary.json", "refined/ready.json"):
|
||||||
|
receipt = path / relative
|
||||||
|
record = json.loads(receipt.read_text())
|
||||||
|
receipt.write_text(json.dumps(dict(record, jet_scenario="zero")))
|
||||||
|
with unittest.TestCase().assertRaises(ValueError):
|
||||||
|
r.verify_pilot(path, manifest, "MRT", "window", "fixed")
|
||||||
|
receipt.write_text(json.dumps(record))
|
||||||
|
with unittest.TestCase().assertRaises(ValueError):
|
||||||
|
r.verify_source(r.DEFAULT_SOURCE, "MRT", "last_base_step", "fixed")
|
||||||
|
|
||||||
|
|
||||||
|
def test_cv_snapshot_common_units_and_window_endpoints():
|
||||||
|
records = []
|
||||||
|
for refined, shape, origin, diameter, impulse in (
|
||||||
|
(True, (240, 568), (38., 22.), 40, [4., -8.]),
|
||||||
|
(False, (330, 1761), (.25, .5), 80, [16., -32.]),
|
||||||
|
(False, (658, 3521), (.375, .75), 160, [64., -128.]),
|
||||||
|
):
|
||||||
|
rho, ux, uy, fluid = np.ones(shape), np.full(shape, .01), np.zeros(shape), np.ones(shape, bool)
|
||||||
|
begin, n = round(290*diameter/r.SPEED), round(.02*diameter/r.SPEED)
|
||||||
|
record = r.cv_snapshot(rho, ux, uy, fluid, origin, refined, begin, n, diameter, impulse)
|
||||||
|
records.append(record)
|
||||||
|
np.testing.assert_allclose(record["outward_flux"], 0, atol=1e-12)
|
||||||
|
assert r.cv_selected(begin, diameter, "full") and not r.cv_selected(begin-n, diameter, "full")
|
||||||
|
end = round(300*diameter/r.SPEED)
|
||||||
|
assert r.cv_selected(end, diameter, "full") and not r.cv_selected(end+n, diameter, "full")
|
||||||
|
assert r.cv_selected(n, diameter, "pilot")
|
||||||
|
assert sum(r.cv_selected(step, diameter, "full") for step in range(begin-n, end+n+1, n)) == 501
|
||||||
|
for key in ("time_root", "interval_start_root", "interval_dt_root", "interval_impulse_root", "inventory"):
|
||||||
|
for record in records[1:]:
|
||||||
|
np.testing.assert_allclose(records[0][key], record[key], rtol=0, atol=1e-10)
|
||||||
|
assert records[0]["interval_dt_root"] == 80 and np.isclose(records[0]["tstar"], 290.02)
|
||||||
|
np.testing.assert_array_equal(records[0]["interval_impulse_root"], [4, -8])
|
||||||
|
|
||||||
|
|
||||||
|
def test_cv_pilot_mode_and_helper_provenance():
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
manifest = r.verify_source(r.DEFAULT_SOURCE, "MRT", "window", "fixed", True)
|
||||||
|
assert "tests/validation/run_jet_circle_cv.py" in manifest["sha256"]
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
path = Path(tmp)
|
||||||
|
old = pilot_fixture(path, jet_scenario="fixed")
|
||||||
|
with unittest.TestCase().assertRaisesRegex(ValueError, "CV observation mode"):
|
||||||
|
r.verify_pilot(path, old, "MRT", "window", "fixed", True)
|
||||||
|
for relative in ("plan.json", "uniform/summary.json", "refined/summary.json",
|
||||||
|
"uniform/ready.json", "refined/ready.json"):
|
||||||
|
file = path / relative
|
||||||
|
record = json.loads(file.read_text())
|
||||||
|
record.update(observe_cv=True, cv_samples=250)
|
||||||
|
if relative == "plan.json":
|
||||||
|
record["source_manifest"] = manifest
|
||||||
|
file.write_text(json.dumps(record))
|
||||||
|
assert r.verify_pilot(path, manifest, "MRT", "window", "fixed", True) == 3606
|
||||||
|
|
||||||
|
|
||||||
|
def test_compact_geometry_and_cv_stencils():
|
||||||
|
from CelerisLab.lbm.refinement import ACTIVE
|
||||||
|
from CelerisLab.lbm.descriptors import SOLID
|
||||||
|
|
||||||
|
_, pair, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT", "fixed", "compact")
|
||||||
|
assert pair.fine_shape == (168, 168) and tuple(pair.fine_origin) == (46, 46)
|
||||||
|
fluid = (pair.fine_kind == ACTIVE) & ((host.flags.reshape(pair.fine_shape) & SOLID) == 0)
|
||||||
|
rho, ux, uy = np.ones(pair.fine_shape), np.full(pair.fine_shape, .01), np.zeros(pair.fine_shape)
|
||||||
|
record = r.cv_snapshot(rho, ux, uy, fluid, pair.fine_origin, True, 1160000, 80, 40, [1, 2], "compact")
|
||||||
|
np.testing.assert_allclose(record["outward_flux"], 0, atol=1e-12)
|
||||||
|
stencil = np.zeros(pair.fine_shape, bool)
|
||||||
|
for rect in r.cv_contours(True, 40, "compact"):
|
||||||
|
x0, y0, x1, y1 = np.rint((rect - np.tile(pair.fine_origin, 2))/.5).astype(int)
|
||||||
|
stencil[y0-2:y1+2, x0-2:x0+2] = stencil[y0-2:y1+2, x1-2:x1+2] = True
|
||||||
|
stencil[y0-2:y0+2, x0-2:x1+2] = stencil[y1-2:y1+2, x0-2:x1+2] = True
|
||||||
|
assert fluid[stencil].all() and not stencil.ravel()[host.transfer_fine_cells].any()
|
||||||
|
|
||||||
|
|
||||||
|
def test_d160_config_and_single_case_receipts():
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
cfg, _, p = r.config(h, 160, "MRT", "fixed")
|
||||||
|
assert (p["nx"], p["ny"], p["center"], p["radius"]) == (3521, 658, [320, 320.5], 80)
|
||||||
|
assert p["viscosity"] == .016 and p["mean_speed"] == .01 and not cfg["method"]["ddf_shifting"]
|
||||||
|
assert r.uniform_origin(160) == (.375, .75)
|
||||||
|
np.testing.assert_array_equal(r.cv_contours(False, 160), r.CV_CONTOURS + [.125, 0, .125, 0])
|
||||||
|
for scenario, expected_exit in (("fixed", 0), ("zero", 2)):
|
||||||
|
with TemporaryDirectory() as tmp:
|
||||||
|
path = Path(tmp)
|
||||||
|
manifest = pilot_fixture(path, jet_scenario=scenario, case_selection="uniform", uniform_diameter=160)
|
||||||
|
assert r.verify_pilot(path, manifest, "MRT", "window", scenario, False, "uniform", 160) == 1803
|
||||||
|
for selection, diameter, placement in (("both", 160, "wide"), ("uniform", 80, "wide"), ("uniform", 160, "compact")):
|
||||||
|
with unittest.TestCase().assertRaises(ValueError):
|
||||||
|
r.verify_pilot(path, manifest, "MRT", "window", scenario, False, selection, diameter, placement)
|
||||||
|
# Synthetic dispatch/result check: no CUDA imports or execution.
|
||||||
|
output = path / "full"
|
||||||
|
argv = ["runner", "--collision", "MRT", "--exchange-window", "--jet-scenario", scenario,
|
||||||
|
"--case", "uniform", "--uniform-diameter", "160", "--mode", "full",
|
||||||
|
"--pilot-results", str(path), "--output", str(output), "--max-wall-seconds", "10", "--gpu-gates-approved"]
|
||||||
|
result = dict(case="uniform", healthy=True, periodic=scenario == "zero", jet_diagnostic_windows=[],
|
||||||
|
status="driven_diagnostics_complete" if scenario == "fixed" else "periodic",
|
||||||
|
reference_comparison=dict(reference_agreement=False))
|
||||||
|
with patch.object(sys, "argv", argv), patch.dict(r.os.environ, CUDA_VISIBLE_DEVICES="mock-no-gpu"), \
|
||||||
|
patch.object(r, "run_case", return_value=result) as run:
|
||||||
|
assert r.main() == expected_exit and run.call_count == 1
|
||||||
|
comparison = json.loads((output / "comparison.json").read_text())
|
||||||
|
assert comparison["matched_case_pass"] is False and "refined_vs_uniform" not in comparison
|
||||||
|
assert comparison["status"].startswith("single_case_")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
suite = unittest.TestSuite(
|
suite = unittest.TestSuite(
|
||||||
unittest.FunctionTestCase(fn)
|
unittest.FunctionTestCase(fn)
|
||||||
|
|||||||
@@ -238,6 +238,81 @@ def test_overlapping_force_regions_rejected(device):
|
|||||||
bodies.build_compact_lists(bodies.build_flags(field.flag))
|
bodies.build_compact_lists(bodies.build_flags(field.flag))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("second_x,error", [(14., "unique cut-link writers"),
|
||||||
|
(17., "active fluid receivers")])
|
||||||
|
def test_passive_overlap_rejected_before_publishing_lists(second_x, error):
|
||||||
|
cfg = LBMConfig(nx=32, ny=24)
|
||||||
|
cfg.validate()
|
||||||
|
bodies = ObjectManager(cfg.nx, cfg.ny, cfg.nz, cfg.nq, cfg)
|
||||||
|
for x in (14., second_x):
|
||||||
|
bodies.add(obj(x=x, y=12., r=4.3))
|
||||||
|
bodies.add(obj(x=4., y=4., r=2., is_sensor=True))
|
||||||
|
counts = np.full(bodies.count, 91, np.int32)
|
||||||
|
hits = []
|
||||||
|
flags = bodies.build_flags(np.full(cfg.nx * cfg.ny, FLUID, np.uint16))
|
||||||
|
for domain_flags in (flags, None):
|
||||||
|
with pytest.raises(ValueError, match=error):
|
||||||
|
bodies.build_compact_lists_for(bodies.objects, domain_flags,
|
||||||
|
sensor_cell_counts_out=counts, hits_out=hits)
|
||||||
|
np.testing.assert_array_equal(counts, 91)
|
||||||
|
assert hits == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("centers", [((10., 12.), (22., 12.)),
|
||||||
|
((10.5, 12.), (19.5, 12.))])
|
||||||
|
def test_separated_passive_bodies_keep_valid_writers(centers):
|
||||||
|
# The second case has a subcell physical gap with a resolved fluid column.
|
||||||
|
cfg = LBMConfig(nx=32, ny=24)
|
||||||
|
cfg.validate()
|
||||||
|
bodies = ObjectManager(cfg.nx, cfg.ny, cfg.nz, cfg.nq, cfg)
|
||||||
|
for x, y in centers:
|
||||||
|
bodies.add(obj(x=x, y=y, r=4.3))
|
||||||
|
flags = bodies.build_flags(np.full(cfg.nx * cfg.ny, FLUID, np.uint16))
|
||||||
|
indices, directions, _, ids, *_ = bodies.build_compact_lists(flags)
|
||||||
|
assert set(ids) == {0, 1}
|
||||||
|
assert np.all(flags[indices] & FLUID) and not np.any(flags[indices] & SOLID)
|
||||||
|
assert len(np.unique(indices.astype(np.uint64) * cfg.nq + directions)) == len(indices)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("entry", ["sync", "initialize", "runtime_edit"])
|
||||||
|
@pytest.mark.parametrize("second_x", [5., 6.])
|
||||||
|
def test_passive_overlap_failure_keeps_live_state(device, entry, second_x):
|
||||||
|
from CelerisLab.simulation import Simulation
|
||||||
|
field = make_field(device)
|
||||||
|
bodies = manager(field, [obj(), obj(x=9., y=3., r=1., is_sensor=True)])
|
||||||
|
bodies.sync_to_gpu(field)
|
||||||
|
overlapping = obj(x=second_x)
|
||||||
|
if entry == "runtime_edit":
|
||||||
|
bodies.stage_add(overlapping)
|
||||||
|
else:
|
||||||
|
bodies.add(overlapping)
|
||||||
|
objects = bodies.objects
|
||||||
|
flags, curved, counts = field.flag.copy(), field.curved.fluid_idx.copy(), bodies.sensor_cell_counts.copy()
|
||||||
|
action = bodies.action.copy()
|
||||||
|
buffers = [(block, bytes(block.data), block.freed) for block in device.blocks]
|
||||||
|
writes = device.writes
|
||||||
|
sim = SimpleNamespace(bodies=bodies, field=field, lbm_cfg=field.cfg,
|
||||||
|
_initialized=entry == "runtime_edit",
|
||||||
|
_assert_allocation_contract=lambda: None,
|
||||||
|
stream=SimpleNamespace(synchronize=device.synchronize))
|
||||||
|
with pytest.raises(ValueError, match="active fluid receivers|unique cut-link writers"):
|
||||||
|
if entry == "sync":
|
||||||
|
bodies.sync_to_gpu(field)
|
||||||
|
elif entry == "initialize":
|
||||||
|
Simulation.initialize(sim)
|
||||||
|
else:
|
||||||
|
Simulation.sync_bodies(sim)
|
||||||
|
assert bodies.objects == objects
|
||||||
|
assert bodies.has_pending_edit() == (entry == "runtime_edit")
|
||||||
|
assert sim._initialized == (entry == "runtime_edit")
|
||||||
|
assert device.writes == writes and len(device.blocks) == len(buffers)
|
||||||
|
assert all(bytes(block.data) == data and block.freed == freed for block, data, freed in buffers)
|
||||||
|
np.testing.assert_array_equal(field.flag, flags)
|
||||||
|
np.testing.assert_array_equal(field.curved.fluid_idx, curved)
|
||||||
|
np.testing.assert_array_equal(bodies.sensor_cell_counts, counts)
|
||||||
|
np.testing.assert_array_equal(bodies.action, action)
|
||||||
|
|
||||||
|
|
||||||
def test_action_upload_and_scratch_are_pinned(device):
|
def test_action_upload_and_scratch_are_pinned(device):
|
||||||
field = make_field(device, streaming="esopull")
|
field = make_field(device, streaming="esopull")
|
||||||
bodies = manager(field, [obj(is_force_region=True)])
|
bodies = manager(field, [obj(is_force_region=True)])
|
||||||
|
|||||||
@@ -74,7 +74,25 @@ def cv(g, fluid, origin, rectangle, nu, h=0.5):
|
|||||||
Cauchy stress from centred velocity gradients, NOT postcollision non-equilibrium.
|
Cauchy stress from centred velocity gradients, NOT postcollision non-equilibrium.
|
||||||
Rectangle faces lie on cell edges; fluid mask excludes the staircase cylinder.
|
Rectangle faces lie on cell edges; fluid mask excludes the staircase cylinder.
|
||||||
"""
|
"""
|
||||||
rho, mom, vel = moments(g)
|
return _cv_quantities(*moments(g), fluid, origin, rectangle, nu, h)
|
||||||
|
|
||||||
|
|
||||||
|
def cv_from_macros(rho, ux, uy, fluid, origin, rectangle, nu, h=0.5):
|
||||||
|
"""Same CV calculation from public macros, in one common length/time frame.
|
||||||
|
|
||||||
|
``origin`` is the lower cell edge, ``h`` the cell width, and ``nu`` uses
|
||||||
|
that frame. Returns [mass, Px, Py] inventory and outward total flux.
|
||||||
|
Crop fields/mask together and translate origin before calling if desired.
|
||||||
|
No boundary-exchange observation enters this independent estimator.
|
||||||
|
"""
|
||||||
|
rho = np.asarray(rho, np.float64)
|
||||||
|
if rho.ndim != 2 or any(np.shape(v) != rho.shape for v in (ux, uy, fluid)):
|
||||||
|
raise ValueError("CV macros and fluid mask must have matching 2D shapes")
|
||||||
|
vel = np.asarray([ux, uy], np.float64)
|
||||||
|
return _cv_quantities(rho, rho[None] * vel, vel, fluid, origin, rectangle, nu, h)
|
||||||
|
|
||||||
|
|
||||||
|
def _cv_quantities(rho, mom, vel, fluid, origin, rectangle, nu, h):
|
||||||
x0, y0, x1, y1 = np.rint((np.array(rectangle) - np.tile(origin, 2)) / h).astype(int)
|
x0, y0, x1, y1 = np.rint((np.array(rectangle) - np.tile(origin, 2)) / h).astype(int)
|
||||||
np.testing.assert_allclose(
|
np.testing.assert_allclose(
|
||||||
np.array([x0, y0, x1, y1]) * h + np.tile(origin, 2), rectangle, atol=1e-12
|
np.array([x0, y0, x1, y1]) * h + np.tile(origin, 2), rectangle, atol=1e-12
|
||||||
|
|||||||
@@ -17,8 +17,109 @@ HERE = Path(__file__).resolve().parent
|
|||||||
DEFAULT_SOURCE = HERE.parents[1]
|
DEFAULT_SOURCE = HERE.parents[1]
|
||||||
BOUNDS = (40, 24, 320, 140)
|
BOUNDS = (40, 24, 320, 140)
|
||||||
BASE_BLOCK = (2, 881) # Exact 166 x 881 domain; no geometric padding.
|
BASE_BLOCK = (2, 881) # Exact 166 x 881 domain; no geometric padding.
|
||||||
FINE_BLOCK = (8, 8) # 240 x 568 including four fine halo cells.
|
FINE_BLOCK = (8, 8) # Wide 240 x 568 or compact 168 x 168, including fine halos.
|
||||||
SPEED = 0.01
|
SPEED = 0.01
|
||||||
|
JET_PEAK = 0.001
|
||||||
|
JET_WINDOWS = [(t, t + 10) for t in range(250, 300, 10)]
|
||||||
|
CV_CONTOURS = np.array([(48., 44., 160., 120.), (44., 36., 180., 128.)])
|
||||||
|
PLACEMENTS = {"wide": BOUNDS, "compact": (48, 48, 128, 128)}
|
||||||
|
COMPACT_CV_CONTOURS = np.array([(52., 52., 112., 112.), (50., 50., 120., 120.)])
|
||||||
|
|
||||||
|
|
||||||
|
def selected_cases(selection="both", uniform_diameter=80):
|
||||||
|
return [(case, diameter) for case, diameter in (("uniform", uniform_diameter), ("refined", 40))
|
||||||
|
if selection in ("both", case)]
|
||||||
|
|
||||||
|
|
||||||
|
def uniform_origin(diameter):
|
||||||
|
h = 40 / diameter
|
||||||
|
return (.5 - .5*h, 1 - h)
|
||||||
|
|
||||||
|
|
||||||
|
def cv_contours(refined, diameter, placement="wide"):
|
||||||
|
contours = CV_CONTOURS if placement == "wide" else COMPACT_CV_CONTOURS
|
||||||
|
return contours if refined else contours + [20/diameter, 0., 20/diameter, 0.]
|
||||||
|
|
||||||
|
|
||||||
|
def cv_selected(step, diameter, mode):
|
||||||
|
return mode == "pilot" or round(290 * diameter / SPEED) <= step <= round(300 * diameter / SPEED)
|
||||||
|
|
||||||
|
|
||||||
|
def cv_snapshot(rho, ux, uy, fluid, origin, refined, begin, n, diameter, impulse, placement="wide"):
|
||||||
|
"""Cropped public macros only; all inventories, fluxes and impulses use root units."""
|
||||||
|
from run_jet_circle_cv import cv_from_macros
|
||||||
|
|
||||||
|
rectangles = cv_contours(refined, diameter, placement)
|
||||||
|
h_cell = .5 if refined else 40 / diameter
|
||||||
|
origin = np.asarray(origin, dtype=float)
|
||||||
|
x0, y0, x1, y1 = np.rint((rectangles[-1] - np.tile(origin, 2)) / h_cell).astype(int)
|
||||||
|
selection = np.s_[y0-2:y1+2, x0-2:x1+2]
|
||||||
|
cropped_origin = origin + h_cell * np.array([x0-2, y0-2])
|
||||||
|
fields = [v[selection] for v in (rho, ux, uy, fluid)]
|
||||||
|
values = [cv_from_macros(*fields, cropped_origin, rect, .004, h=h_cell)
|
||||||
|
for rect in rectangles]
|
||||||
|
h_native = 1.0 if refined else 40 / diameter
|
||||||
|
return dict(step=begin+n, tstar=(begin+n)*SPEED/diameter,
|
||||||
|
time_root=(begin+n)*h_native, interval_start_root=begin*h_native,
|
||||||
|
interval_dt_root=n*h_native,
|
||||||
|
interval_impulse_root=(np.asarray(impulse)*h_native**2).tolist(),
|
||||||
|
inventory=[inventory.tolist() for inventory, _ in values],
|
||||||
|
outward_flux=[flux.tolist() for _, flux in values])
|
||||||
|
|
||||||
|
|
||||||
|
def public_jets(center, radius, scenario):
|
||||||
|
"""Same physical apertures; caller supplies its public geometry frame."""
|
||||||
|
if scenario == "none":
|
||||||
|
return ()
|
||||||
|
from CelerisLab.body.jets import Jet, Sector, Radial
|
||||||
|
|
||||||
|
peak = 0.0 if scenario == "zero" else JET_PEAK
|
||||||
|
return tuple(
|
||||||
|
Jet(Sector(center, angle, math.pi / 12, radius * 1.005),
|
||||||
|
Radial(center, inward=False), "parabolic", amplitude)
|
||||||
|
for angle, amplitude in ((math.pi / 2, peak), (3 * math.pi / 2, -peak))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def jet_interval(handle, scenario, begin, n, diameter):
|
||||||
|
"""Apply a reversal only at a completed interval boundary, before advancing."""
|
||||||
|
if scenario != "none":
|
||||||
|
boundaries = [round(t * diameter / SPEED) for t in (250, 260, 270, 280, 290, 300, 305)]
|
||||||
|
n = min([n] + [step - begin for step in boundaries if step > begin])
|
||||||
|
if scenario != "reverse":
|
||||||
|
return n, None
|
||||||
|
switch = round(300 * diameter / SPEED)
|
||||||
|
if begin < switch:
|
||||||
|
return min(n, switch - begin), None
|
||||||
|
if begin == switch:
|
||||||
|
handle.set_jet_amplitude(0, -JET_PEAK)
|
||||||
|
handle.set_jet_amplitude(1, JET_PEAK)
|
||||||
|
return n, dict(step=begin, tstar=300.0, amplitudes=[-JET_PEAK, JET_PEAK])
|
||||||
|
return n, None
|
||||||
|
|
||||||
|
|
||||||
|
def jet_diagnostics(rows, scenario):
|
||||||
|
"""Raw interval-weighted diagnostics only; no driven-flow qualification gate."""
|
||||||
|
windows = JET_WINDOWS + ([(300, 305)] if scenario == "reverse" else [])
|
||||||
|
result = []
|
||||||
|
for start, end in windows:
|
||||||
|
selected = [r for r in rows if start <= r["tstar_force"] < end]
|
||||||
|
if not selected:
|
||||||
|
continue
|
||||||
|
if any(2 * r["tstar_force"] - r["tstar"] < start - 1e-9
|
||||||
|
or r["tstar"] > end + 1e-9 for r in selected):
|
||||||
|
raise ValueError("Jet diagnostic interval crosses a window boundary")
|
||||||
|
weights = np.array([2 * (r["tstar"] - r["tstar_force"]) for r in selected])
|
||||||
|
record = dict(start_tstar=start, end_tstar=end, samples=len(selected),
|
||||||
|
covered_tstar=float(weights.sum()),
|
||||||
|
complete=bool(np.isclose(weights.sum(), end - start, rtol=0, atol=1e-9)))
|
||||||
|
for key in ("cd", "cl"):
|
||||||
|
values = np.array([r[key] for r in selected])
|
||||||
|
record[key] = dict(mean=float(np.average(values, weights=weights)),
|
||||||
|
min=float(values.min()), max=float(values.max()),
|
||||||
|
rms=float(np.sqrt(np.average(values**2, weights=weights))))
|
||||||
|
result.append(record)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def helpers(source):
|
def helpers(source):
|
||||||
@@ -39,26 +140,34 @@ def helpers(source):
|
|||||||
return h, periodic
|
return h, periodic
|
||||||
|
|
||||||
|
|
||||||
def config(h, diameter, collision="SRT"):
|
def config(h, diameter, collision="SRT", jet_scenario="none"):
|
||||||
cfg, body, p = h.make_config(diameter, SPEED, collision, "neq_extrap", "zou_he_local")
|
cfg, body, p = h.make_config(diameter, SPEED, collision, "neq_extrap", "zou_he_local")
|
||||||
cfg["method"]["outlet"]["srt_neq_damp"] = 0.5
|
cfg["method"]["outlet"]["srt_neq_damp"] = 0.5
|
||||||
|
if jet_scenario != "none":
|
||||||
|
if collision != "MRT":
|
||||||
|
raise ValueError("Public Jet scenarios require MRT")
|
||||||
|
cfg["method"]["ddf_shifting"] = False
|
||||||
return cfg, body, p
|
return cfg, body, p
|
||||||
|
|
||||||
|
|
||||||
def plan(h, collision="SRT", observation_mode="last_base_step"):
|
def plan(h, collision="SRT", observation_mode="last_base_step", jet_scenario="none", uniform_diameter=80, placement="wide"):
|
||||||
_, _, c = config(h, 40, collision)
|
_, _, c = config(h, 40, collision, jet_scenario)
|
||||||
_, _, f = config(h, 80, collision)
|
_, _, f = config(h, uniform_diameter, collision, jet_scenario)
|
||||||
x0, y0, x1, y1 = BOUNDS
|
bounds = PLACEMENTS[placement]
|
||||||
|
x0, y0, x1, y1 = bounds
|
||||||
fine_active = 4 * (x1 - x0) * (y1 - y0)
|
fine_active = 4 * (x1 - x0) * (y1 - y0)
|
||||||
coarse_uncovered = c["nx"] * c["ny"] - fine_active // 4
|
coarse_uncovered = c["nx"] * c["ny"] - fine_active // 4
|
||||||
return dict(
|
return dict(
|
||||||
status="prepared_not_run",
|
status="prepared_not_run",
|
||||||
collision=collision,
|
collision=collision,
|
||||||
|
jet_scenario=jet_scenario,
|
||||||
|
ddf_shifting=jet_scenario == "none",
|
||||||
observation_mode=observation_mode,
|
observation_mode=observation_mode,
|
||||||
root_gpu_gates_required=True,
|
root_gpu_gates_required=True,
|
||||||
root=c,
|
root=c,
|
||||||
uniform=f,
|
uniform=f,
|
||||||
bounds_root_edge=list(BOUNDS),
|
bounds_root_edge=list(bounds),
|
||||||
|
fine_origin_root_edge=[x0 - 2, y0 - 2],
|
||||||
physical_patch_m=[
|
physical_patch_m=[
|
||||||
(x0 - 0.5) * c["dx"],
|
(x0 - 0.5) * c["dx"],
|
||||||
(y0 - 1) * c["dx"],
|
(y0 - 1) * c["dx"],
|
||||||
@@ -78,19 +187,31 @@ def plan(h, collision="SRT", observation_mode="last_base_step"):
|
|||||||
physical_viscosity_m2_s=0.001,
|
physical_viscosity_m2_s=0.001,
|
||||||
finest_dx_m=f["dx"],
|
finest_dx_m=f["dx"],
|
||||||
finest_dt_s=f["dt"],
|
finest_dt_s=f["dt"],
|
||||||
|
refined_finest_dx_m=c["dx"]/2,
|
||||||
|
refined_finest_dt_s=c["dt"]/2,
|
||||||
approximate_leaf_cell_ratio=(coarse_uncovered + fine_active)
|
approximate_leaf_cell_ratio=(coarse_uncovered + fine_active)
|
||||||
/ (f["nx"] * f["ny"]),
|
/ (f["nx"] * f["ny"]),
|
||||||
approximate_collision_work_ratio=(coarse_uncovered + 2 * fine_active)
|
approximate_collision_work_ratio=(coarse_uncovered + 2 * fine_active)
|
||||||
/ (2 * f["nx"] * f["ny"]),
|
/ ((uniform_diameter/40) * f["nx"] * f["ny"]),
|
||||||
cost_caveat="Geometry-only estimates include ghosts/solids; exclude block halos, transfers, observation and launch overhead. Not measured speedup.",
|
cost_caveat="Geometry-only estimates include ghosts/solids; exclude block halos, transfers, observation and launch overhead. Not measured speedup.",
|
||||||
sampling_offset="For origin (38,22), fine physical x=(j+75.5)*dx80 versus uniform i*dx80: half-fine-cell x offset. Fine physical y=(k+42.5)*dx80 equals uniform (l-.5)*dx80 at l=k+43: y samples align. Body, wall and source planes coincide.",
|
sampling_offset="Uniform nodes map to root edges X=.5+i*(40/D), Y=1+(j-.5)*(40/D). Refined centers are fine_origin_root_edge+(index+.5)*.5. Body, wall and source planes coincide; compare observables, not pointwise arrays.",
|
||||||
outlet=(
|
outlet=(
|
||||||
"SRT pressure NEQ, all nine fneq damped by .5, no backflow clamp; noncanonical DFG outlet"
|
"SRT pressure NEQ, all nine fneq damped by .5, no backflow clamp; noncanonical DFG outlet"
|
||||||
if collision == "SRT" else
|
if collision == "SRT" else
|
||||||
"MRT pressure NEQ, unknowns q2/6/8 extrapolated; no backflow clamp; noncanonical DFG outlet"
|
"MRT pressure NEQ, unknowns q2/6/8 extrapolated; no backflow clamp; noncanonical DFG outlet"
|
||||||
),
|
),
|
||||||
modes={"pilot_tstar": 5.0, "full_tstar": 300.0},
|
modes={"pilot_tstar": 5.0, "full_tstar": 305.0 if jet_scenario == "reverse" else 300.0},
|
||||||
observation_dense_tstar=0.02,
|
observation_dense_tstar=0.02,
|
||||||
|
**(dict(jets=dict(
|
||||||
|
uniform_origin_node=f["center"], refined_origin_root_edge=[80.5, 81.0],
|
||||||
|
angles_rad=[math.pi / 2, 3 * math.pi / 2], half_width_rad=math.pi / 12,
|
||||||
|
radial_extent_over_radius=[0.0, 1.005],
|
||||||
|
field="Radial(outward)", profile="parabolic", condition="velocity",
|
||||||
|
initial_amplitudes=[0.0, 0.0] if jet_scenario == "zero" else [JET_PEAK, -JET_PEAK],
|
||||||
|
reversal_tstar=300.0 if jet_scenario == "reverse" else None,
|
||||||
|
diagnostic_windows_tstar=JET_WINDOWS + ([(300, 305)] if jet_scenario == "reverse" else []),
|
||||||
|
transfer="constant", driven_qualification="not assessed",
|
||||||
|
)) if jet_scenario != "none" else {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -126,7 +247,7 @@ def initial_state(shape, origin, spacing, height=164.0, speed=SPEED, root=False)
|
|||||||
return w * (1 + 3 * cu + 4.5 * cu * cu - 1.5 * velocity * velocity)
|
return w * (1 + 3 * cu + 4.5 * cu * cu - 1.5 * velocity * velocity)
|
||||||
|
|
||||||
|
|
||||||
def host_geometry(source, h, collision="SRT"):
|
def host_geometry(source, h, collision="SRT", jet_scenario="none", placement="wide"):
|
||||||
"""Production donor/transfer and complete cut-link checks, without CUDA."""
|
"""Production donor/transfer and complete cut-link checks, without CUDA."""
|
||||||
from CelerisLab.config import load_lbm_config
|
from CelerisLab.config import load_lbm_config
|
||||||
from CelerisLab.lbm.refinement import FixedPatch2D, prepare_fine_boundary
|
from CelerisLab.lbm.refinement import FixedPatch2D, prepare_fine_boundary
|
||||||
@@ -139,12 +260,12 @@ def host_geometry(source, h, collision="SRT"):
|
|||||||
from CelerisLab.body.geometry.circle import CircleGeometry
|
from CelerisLab.body.geometry.circle import CircleGeometry
|
||||||
from CelerisLab.body.objects import SimObject
|
from CelerisLab.body.objects import SimObject
|
||||||
|
|
||||||
cfgdict, _, _ = config(h, 40, collision)
|
cfgdict, _, _ = config(h, 40, collision, jet_scenario)
|
||||||
with TemporaryDirectory() as tmp:
|
with TemporaryDirectory() as tmp:
|
||||||
p = Path(tmp) / "cfg.json"
|
p = Path(tmp) / "cfg.json"
|
||||||
p.write_text(json.dumps(cfgdict))
|
p.write_text(json.dumps(cfgdict))
|
||||||
cfg = load_lbm_config(str(p))
|
cfg = load_lbm_config(str(p))
|
||||||
pair = FixedPatch2D.from_config(cfg, BOUNDS)
|
pair = FixedPatch2D.from_config(cfg, PLACEMENTS[placement])
|
||||||
layout = BlockLayout2D.build(cfg.ny, cfg.nx, *BASE_BLOCK)
|
layout = BlockLayout2D.build(cfg.ny, cfg.nx, *BASE_BLOCK)
|
||||||
validate_channel_config(cfg, [BASE_BLOCK, FINE_BLOCK])
|
validate_channel_config(cfg, [BASE_BLOCK, FINE_BLOCK])
|
||||||
targets = blocked_base_tables(pair, layout, np.argsort(pair.coalesce_coarse))[1]
|
targets = blocked_base_tables(pair, layout, np.argsort(pair.coalesce_coarse))[1]
|
||||||
@@ -153,18 +274,41 @@ def host_geometry(source, h, collision="SRT"):
|
|||||||
body = SimObject(
|
body = SimObject(
|
||||||
0, geometry=CircleGeometry(80.5, 81.0, 20.0), center=(80.5, 81.0), radius=20.0
|
0, geometry=CircleGeometry(80.5, 81.0, 20.0), center=(80.5, 81.0), radius=20.0
|
||||||
)
|
)
|
||||||
host = prepare_fine_boundary(cfg, pair, (body,), _shifted_blocked=True)
|
host = prepare_fine_boundary(
|
||||||
|
cfg, pair, (body,), jets=public_jets((80.5, 81.0), 20.0, jet_scenario),
|
||||||
|
_shifted_blocked=jet_scenario == "none",
|
||||||
|
)
|
||||||
return cfg, pair, body, host
|
return cfg, pair, body, host
|
||||||
|
|
||||||
|
|
||||||
def verify_source(source, collision="SRT", observation_mode="last_base_step"):
|
def verify_source(source, collision="SRT", observation_mode="last_base_step", jet_scenario="none", observe_cv=False):
|
||||||
if collision not in ("SRT", "MRT"):
|
if collision not in ("SRT", "MRT"):
|
||||||
raise ValueError("Matched collision must be SRT or MRT")
|
raise ValueError("Matched collision must be SRT or MRT")
|
||||||
if observation_mode not in ("last_base_step", "window"):
|
if observation_mode not in ("last_base_step", "window"):
|
||||||
raise ValueError("Unknown refined observation mode")
|
raise ValueError("Unknown refined observation mode")
|
||||||
|
if observe_cv and jet_scenario == "none":
|
||||||
|
raise ValueError("--observe-cv requires an explicit public Jet scenario")
|
||||||
name = ("source_manifest_window.json" if observation_mode == "window" else
|
name = ("source_manifest_window.json" if observation_mode == "window" else
|
||||||
"source_manifest.json" if collision == "SRT" else "source_manifest_mrt.json")
|
"source_manifest.json" if collision == "SRT" else "source_manifest_mrt.json")
|
||||||
expected = json.loads((HERE / "data/refined_dfg" / name).read_text())
|
expected = json.loads((HERE / "data/refined_dfg" / name).read_text())
|
||||||
|
if jet_scenario != "none":
|
||||||
|
if collision != "MRT" or observation_mode != "window":
|
||||||
|
raise ValueError("Public Jet scenarios require MRT and --exchange-window")
|
||||||
|
import subprocess
|
||||||
|
sys.path.insert(0, str(HERE))
|
||||||
|
from source_provenance import checkout_provenance
|
||||||
|
|
||||||
|
paths = set(expected["sha256"])
|
||||||
|
if observe_cv:
|
||||||
|
paths.add("tests/validation/run_jet_circle_cv.py")
|
||||||
|
paths.update(subprocess.check_output(
|
||||||
|
["git", "-C", str(source), "ls-files", "src"], text=True).splitlines())
|
||||||
|
return dict(
|
||||||
|
source_capture="current checkout, not historical DFG qualification",
|
||||||
|
**checkout_provenance(source), observation_mode=observation_mode,
|
||||||
|
sha256={name: hashlib.sha256((source / name).read_bytes()).hexdigest()
|
||||||
|
for name in sorted(paths)},
|
||||||
|
)
|
||||||
for name, digest in expected["sha256"].items():
|
for name, digest in expected["sha256"].items():
|
||||||
if hashlib.sha256((source / name).read_bytes()).hexdigest() != digest:
|
if hashlib.sha256((source / name).read_bytes()).hexdigest() != digest:
|
||||||
raise RuntimeError("Source mismatch: " + name)
|
raise RuntimeError("Source mismatch: " + name)
|
||||||
@@ -219,29 +363,33 @@ def run_case(a, h, periodic, case):
|
|||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
|
|
||||||
refined = case == "refined"
|
refined = case == "refined"
|
||||||
cfgdict, bodydict, p = config(h, 40 if refined else 80, a.collision)
|
cfgdict, bodydict, p = config(h, 40 if refined else a.uniform_diameter, a.collision, a.jet_scenario)
|
||||||
cap = 5.0 if a.mode == "pilot" else 300.0
|
cap = 5.0 if a.mode == "pilot" else (305.0 if a.jet_scenario == "reverse" else 300.0)
|
||||||
max_steps = math.ceil(cap * p["diameter"] / SPEED)
|
max_steps = math.ceil(cap * p["diameter"] / SPEED)
|
||||||
rows = []
|
rows = []
|
||||||
healthy = True
|
healthy = True
|
||||||
step_seconds = 0.0
|
step_seconds = 0.0
|
||||||
|
cv_seconds, cv_samples = 0.0, 0
|
||||||
|
amplitude_changes = []
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
check_deadline(a.deadline)
|
check_deadline(a.deadline)
|
||||||
dest = a.output / case
|
dest = a.output / case
|
||||||
dest.mkdir()
|
dest.mkdir()
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
|
cv_out = stack.enter_context((dest / "cv_history.jsonl").open("w")) if a.observe_cv else None
|
||||||
if refined:
|
if refined:
|
||||||
cfg, pair, body, host = host_geometry(a.source, h, a.collision)
|
cfg, pair, body, host = host_geometry(a.source, h, a.collision, a.jet_scenario, a.placement)
|
||||||
context = stack.enter_context(CudaContext(0))
|
context = stack.enter_context(CudaContext(0))
|
||||||
backend = stack.enter_context(
|
backend = stack.enter_context(
|
||||||
FixedPatchStepper(
|
FixedPatchStepper(
|
||||||
cfg,
|
cfg,
|
||||||
BOUNDS,
|
PLACEMENTS[a.placement],
|
||||||
context=context,
|
context=context,
|
||||||
coarse_boundary="channel",
|
coarse_boundary="channel",
|
||||||
base_block=BASE_BLOCK,
|
base_block=BASE_BLOCK,
|
||||||
fine_block=FINE_BLOCK,
|
fine_block=FINE_BLOCK,
|
||||||
fine_objects=(body,),
|
fine_objects=(body,),
|
||||||
|
jets=public_jets((80.5, 81.0), 20.0, a.jet_scenario),
|
||||||
observe_raw_exchange=True,
|
observe_raw_exchange=True,
|
||||||
**({"observe_window_exchange": True} if a.observation_mode == "window" else {}),
|
**({"observe_window_exchange": True} if a.observation_mode == "window" else {}),
|
||||||
)
|
)
|
||||||
@@ -274,6 +422,9 @@ def run_case(a, h, periodic, case):
|
|||||||
sim = stack.enter_context(
|
sim = stack.enter_context(
|
||||||
Simulation(str(tmp / "lbm.json"), str(tmp / "body.json"), device_id=0)
|
Simulation(str(tmp / "lbm.json"), str(tmp / "body.json"), device_id=0)
|
||||||
)
|
)
|
||||||
|
for index, jet in enumerate(public_jets(p["center"], p["radius"], a.jet_scenario)):
|
||||||
|
if sim.add_jet(jet) != index:
|
||||||
|
raise RuntimeError("Unexpected public Jet registration index")
|
||||||
sim.initialize()
|
sim.initialize()
|
||||||
probe_p = p
|
probe_p = p
|
||||||
fluid = (sim.field.flag.reshape(p["ny"], p["nx"]) & FLUID) != 0
|
fluid = (sim.field.flag.reshape(p["ny"], p["nx"]) & FLUID) != 0
|
||||||
@@ -290,6 +441,10 @@ def run_case(a, h, periodic, case):
|
|||||||
else 0.5
|
else 0.5
|
||||||
)
|
)
|
||||||
n = min(max(1, round(width * p["diameter"] / SPEED)), max_steps - begin)
|
n = min(max(1, round(width * p["diameter"] / SPEED)), max_steps - begin)
|
||||||
|
n, change = jet_interval(backend if refined else sim, a.jet_scenario,
|
||||||
|
begin, n, p["diameter"])
|
||||||
|
if change is not None:
|
||||||
|
amplitude_changes.append(change)
|
||||||
impulse = np.zeros(2)
|
impulse = np.zeros(2)
|
||||||
tick = time.monotonic()
|
tick = time.monotonic()
|
||||||
if refined:
|
if refined:
|
||||||
@@ -359,6 +514,19 @@ def run_case(a, h, periodic, case):
|
|||||||
/ SPEED,
|
/ SPEED,
|
||||||
**h.pressure_probes(rho, fluid, probe_p),
|
**h.pressure_probes(rho, fluid, probe_p),
|
||||||
)
|
)
|
||||||
|
if a.jet_scenario != "none":
|
||||||
|
row.update(step_start=begin, tstar_start=begin * SPEED / p["diameter"],
|
||||||
|
interval_steps=n, impulse_x=float(impulse[0]),
|
||||||
|
impulse_y=float(impulse[1]))
|
||||||
|
if a.observe_cv and cv_selected(begin+n, p["diameter"], a.mode):
|
||||||
|
tick = time.monotonic()
|
||||||
|
record = cv_snapshot(rho, ux, uy, fluid,
|
||||||
|
pair.fine_origin if refined else uniform_origin(p["diameter"]),
|
||||||
|
refined, begin, n, p["diameter"], impulse, a.placement)
|
||||||
|
cv_out.write(json.dumps(record, allow_nan=False) + "\n")
|
||||||
|
cv_out.flush()
|
||||||
|
cv_samples += 1
|
||||||
|
cv_seconds += time.monotonic() - tick
|
||||||
check_deadline(a.deadline)
|
check_deadline(a.deadline)
|
||||||
ensure_finite(row)
|
ensure_finite(row)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
@@ -402,14 +570,24 @@ def run_case(a, h, periodic, case):
|
|||||||
steps=begin,
|
steps=begin,
|
||||||
startup_seconds=startup,
|
startup_seconds=startup,
|
||||||
step_seconds=step_seconds,
|
step_seconds=step_seconds,
|
||||||
|
observation_and_overhead_seconds=time.monotonic() - started - startup - step_seconds,
|
||||||
wall_seconds=time.monotonic() - started,
|
wall_seconds=time.monotonic() - started,
|
||||||
force="raw stationary-body link momentum reaction, per-unit-depth; average over entire observation interval; normalized in each base lattice",
|
force="raw stationary-body link momentum reaction, per-unit-depth; average over entire observation interval; normalized in each base lattice",
|
||||||
mass_units="rho_reference * m^2 per unit depth",
|
mass_units="rho_reference * m^2 per unit depth",
|
||||||
flux_units="rho_reference * m^2/s per unit depth",
|
flux_units="rho_reference * m^2/s per unit depth",
|
||||||
pressure="postcollision end-sample; physical pressure difference / (rho_reference * 1 m/s squared)",
|
pressure="postcollision end-sample; physical pressure difference / (rho_reference * 1 m/s squared)",
|
||||||
suite_validated=False,
|
suite_validated=False,
|
||||||
**periodic.assess(rows),
|
**(periodic.assess(rows) if a.jet_scenario in ("none", "zero") else
|
||||||
|
dict(periodic=False, checks={}, cycles=[])),
|
||||||
)
|
)
|
||||||
|
if a.jet_scenario != "none":
|
||||||
|
result.update(jet_diagnostic_windows=jet_diagnostics(rows, a.jet_scenario),
|
||||||
|
amplitude_changes=amplitude_changes,
|
||||||
|
pressure_classification=("DFG reference verdict retained, including pressure"
|
||||||
|
if a.jet_scenario == "zero" else "diagnostic only; no official DFG reference gate"))
|
||||||
|
if a.observe_cv:
|
||||||
|
result.update(cv_observation_seconds=cv_seconds, cv_samples=cv_samples,
|
||||||
|
cv_qualification="not assessed; scalar observations only")
|
||||||
if not healthy:
|
if not healthy:
|
||||||
result["periodic"] = False
|
result["periodic"] = False
|
||||||
reference = json.loads(
|
reference = json.loads(
|
||||||
@@ -425,7 +603,8 @@ def run_case(a, h, periodic, case):
|
|||||||
else (
|
else (
|
||||||
"pilot_complete"
|
"pilot_complete"
|
||||||
if a.mode == "pilot"
|
if a.mode == "pilot"
|
||||||
else ("periodic" if result["periodic"] else "incomplete_periodicity")
|
else ("driven_diagnostics_complete" if a.jet_scenario in ("fixed", "reverse") else
|
||||||
|
"periodic" if result["periodic"] else "incomplete_periodicity")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
(dest / "summary.json").write_text(json.dumps(result, indent=2, allow_nan=False))
|
(dest / "summary.json").write_text(json.dumps(result, indent=2, allow_nan=False))
|
||||||
@@ -464,27 +643,39 @@ def check_deadline(deadline):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def verify_pilot(path, manifest, collision="SRT", observation_mode="last_base_step"):
|
def verify_pilot(path, manifest, collision="SRT", observation_mode="last_base_step", jet_scenario="none", observe_cv=False,
|
||||||
|
case_selection="both", uniform_diameter=80, placement="wide"):
|
||||||
if path is None:
|
if path is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Full comparison requires --pilot-results from a completed matched pilot"
|
"Full comparison requires --pilot-results from a completed matched pilot"
|
||||||
)
|
)
|
||||||
previous = read_finite_json(path / "plan.json")
|
previous = read_finite_json(path / "plan.json")
|
||||||
|
options = dict(case_selection=(case_selection, "both"), uniform_diameter=(uniform_diameter, 80), placement=(placement, "wide"))
|
||||||
|
if any(previous.get(k, default) != expected for k, (expected, default) in options.items()):
|
||||||
|
raise ValueError("Pilot case, resolution or placement differs")
|
||||||
|
if previous.get("observe_cv", False) != observe_cv:
|
||||||
|
raise ValueError("Pilot CV observation mode differs")
|
||||||
|
if previous.get("jet_scenario", "none") != jet_scenario:
|
||||||
|
raise ValueError("Pilot Jet scenario differs")
|
||||||
if previous.get("observation_mode") != observation_mode:
|
if previous.get("observation_mode") != observation_mode:
|
||||||
raise ValueError("Pilot observation mode differs")
|
raise ValueError("Pilot observation mode differs")
|
||||||
if previous.get("collision") != collision:
|
if previous.get("collision") != collision:
|
||||||
raise ValueError("Pilot collision differs")
|
raise ValueError("Pilot collision differs")
|
||||||
if previous["source_manifest"] != manifest or previous["bounds_root_edge"] != list(
|
if previous["source_manifest"] != manifest or previous["bounds_root_edge"] != list(
|
||||||
BOUNDS
|
PLACEMENTS[placement]
|
||||||
):
|
):
|
||||||
raise ValueError("Pilot source or frozen geometry differs")
|
raise ValueError("Pilot source or frozen geometry differs")
|
||||||
if previous.get("runner_sha256") != runner_hash():
|
if previous.get("runner_sha256") != runner_hash():
|
||||||
raise ValueError("Pilot runner differs")
|
raise ValueError("Pilot runner differs")
|
||||||
calibrated_seconds = 0.0
|
calibrated_seconds = 0.0
|
||||||
for case, diameter in (("uniform", 80), ("refined", 40)):
|
for case, diameter in selected_cases(case_selection, uniform_diameter):
|
||||||
summary = read_finite_json(path / case / "summary.json")
|
summary = read_finite_json(path / case / "summary.json")
|
||||||
if (
|
if (
|
||||||
summary.get("observation_mode") != observation_mode
|
any(summary.get(k, default) != expected for k, (expected, default) in options.items())
|
||||||
|
or summary.get("observe_cv", False) != observe_cv
|
||||||
|
or (observe_cv and summary.get("cv_samples") != 250)
|
||||||
|
or summary.get("jet_scenario", "none") != jet_scenario
|
||||||
|
or summary.get("observation_mode") != observation_mode
|
||||||
or summary.get("collision") != collision
|
or summary.get("collision") != collision
|
||||||
or summary.get("case") != case
|
or summary.get("case") != case
|
||||||
or summary.get("mode") != "pilot"
|
or summary.get("mode") != "pilot"
|
||||||
@@ -537,7 +728,10 @@ def verify_pilot(path, manifest, collision="SRT", observation_mode="last_base_st
|
|||||||
raise ValueError("Unhealthy pilot observations")
|
raise ValueError("Unhealthy pilot observations")
|
||||||
ready = read_finite_json(path / case / "ready.json")
|
ready = read_finite_json(path / case / "ready.json")
|
||||||
if (
|
if (
|
||||||
ready.get("observation_mode") != observation_mode
|
any(ready.get(k, default) != expected for k, (expected, default) in options.items())
|
||||||
|
or ready.get("observe_cv", False) != observe_cv
|
||||||
|
or ready.get("jet_scenario", "none") != jet_scenario
|
||||||
|
or ready.get("observation_mode") != observation_mode
|
||||||
or ready.get("collision") != collision
|
or ready.get("collision") != collision
|
||||||
or ready.get("case") != case
|
or ready.get("case") != case
|
||||||
or ready.get("mode") != "pilot"
|
or ready.get("mode") != "pilot"
|
||||||
@@ -555,9 +749,9 @@ def verify_pilot(path, manifest, collision="SRT", observation_mode="last_base_st
|
|||||||
or not 0 < stepping <= wall
|
or not 0 < stepping <= wall
|
||||||
):
|
):
|
||||||
raise ValueError("Invalid pilot calibration timings")
|
raise ValueError("Invalid pilot calibration timings")
|
||||||
# Full physical duration is 60 times pilot; dense pilot sampling makes
|
# Full duration is 60 (or 61 with reversal) times pilot; dense sampling makes
|
||||||
# this conservative for the sparser early full observations. Not a guarantee.
|
# this conservative for the sparser early full observations. Not a guarantee.
|
||||||
calibrated_seconds += startup + 60 * (wall - startup)
|
calibrated_seconds += startup + (61 if jet_scenario == "reverse" else 60) * (wall - startup)
|
||||||
return 3 * calibrated_seconds
|
return 3 * calibrated_seconds
|
||||||
|
|
||||||
|
|
||||||
@@ -566,6 +760,13 @@ def main():
|
|||||||
parser.add_argument("--exchange-window", action="store_true",
|
parser.add_argument("--exchange-window", action="store_true",
|
||||||
help="Opt in to refined window-ledger observations on the pinned candidate")
|
help="Opt in to refined window-ledger observations on the pinned candidate")
|
||||||
parser.add_argument("--collision", choices=["SRT", "MRT"], default="SRT")
|
parser.add_argument("--collision", choices=["SRT", "MRT"], default="SRT")
|
||||||
|
parser.add_argument("--case", choices=["both", "uniform", "refined"], default="both")
|
||||||
|
parser.add_argument("--uniform-diameter", type=int, choices=[80, 160], default=80)
|
||||||
|
parser.add_argument("--placement", choices=list(PLACEMENTS), default="wide")
|
||||||
|
parser.add_argument("--jet-scenario", choices=["none", "zero", "fixed", "reverse"],
|
||||||
|
default="none", help="Opt in to public unshifted MRT Jets; reverse switches at t*=300 and ends at 305")
|
||||||
|
parser.add_argument("--observe-cv", action="store_true",
|
||||||
|
help="Record independent cropped-macro CV samples during pilot or full t*=290..300")
|
||||||
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
|
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
|
||||||
parser.add_argument("--output", type=Path, required=True)
|
parser.add_argument("--output", type=Path, required=True)
|
||||||
parser.add_argument("--mode", choices=["plan", "pilot", "full"], default="plan")
|
parser.add_argument("--mode", choices=["plan", "pilot", "full"], default="plan")
|
||||||
@@ -577,15 +778,26 @@ def main():
|
|||||||
help="Required explicit campaign wall-time ceiling for GPU execution",
|
help="Required explicit campaign wall-time ceiling for GPU execution",
|
||||||
)
|
)
|
||||||
a = parser.parse_args()
|
a = parser.parse_args()
|
||||||
|
if a.uniform_diameter == 160 and a.case != "uniform":
|
||||||
|
raise ValueError("D160 requires --case uniform; no matched-resolution claim")
|
||||||
|
if a.placement == "compact" and a.case != "refined":
|
||||||
|
raise ValueError("Compact placement requires --case refined")
|
||||||
|
if (a.uniform_diameter != 80 or a.placement != "wide") and a.jet_scenario == "none":
|
||||||
|
raise ValueError("Resolution/placement variants require an explicit public Jet scenario")
|
||||||
a.source = a.source.resolve()
|
a.source = a.source.resolve()
|
||||||
a.observation_mode = "window" if a.exchange_window else "last_base_step"
|
a.observation_mode = "window" if a.exchange_window else "last_base_step"
|
||||||
manifest = verify_source(a.source, a.collision, a.observation_mode)
|
manifest = verify_source(a.source, a.collision, a.observation_mode, a.jet_scenario, a.observe_cv)
|
||||||
h, periodic = helpers(a.source)
|
h, periodic = helpers(a.source)
|
||||||
from source_provenance import checkout_provenance
|
from source_provenance import checkout_provenance
|
||||||
|
|
||||||
a.provenance = checkout_provenance(a.source)
|
a.provenance = checkout_provenance(a.source)
|
||||||
|
a.provenance.update(jet_scenario=a.jet_scenario, ddf_shifting=a.jet_scenario == "none")
|
||||||
|
a.provenance.update(case_selection=a.case, uniform_diameter=a.uniform_diameter, placement=a.placement)
|
||||||
|
if a.observe_cv:
|
||||||
|
a.provenance["observe_cv"] = True
|
||||||
calibration = (
|
calibration = (
|
||||||
verify_pilot(a.pilot_results, manifest, a.collision, a.observation_mode)
|
verify_pilot(a.pilot_results, manifest, a.collision, a.observation_mode, a.jet_scenario, a.observe_cv,
|
||||||
|
a.case, a.uniform_diameter, a.placement)
|
||||||
if a.mode == "full" else None
|
if a.mode == "full" else None
|
||||||
)
|
)
|
||||||
if a.mode != "plan" and (
|
if a.mode != "plan" and (
|
||||||
@@ -602,17 +814,31 @@ def main():
|
|||||||
if a.output.exists() and any(a.output.iterdir()):
|
if a.output.exists() and any(a.output.iterdir()):
|
||||||
raise ValueError("Output must be empty/new")
|
raise ValueError("Output must be empty/new")
|
||||||
a.output.mkdir(parents=True, exist_ok=True)
|
a.output.mkdir(parents=True, exist_ok=True)
|
||||||
specification = plan(h, a.collision, a.observation_mode)
|
specification = plan(h, a.collision, a.observation_mode, a.jet_scenario, a.uniform_diameter, a.placement)
|
||||||
specification.update(
|
specification.update(
|
||||||
source_manifest=manifest,
|
source_manifest=manifest,
|
||||||
**a.provenance,
|
**a.provenance,
|
||||||
gates=periodic.GATES,
|
gates=periodic.GATES if a.jet_scenario in ("none", "zero") else None,
|
||||||
reference_gates=periodic.REFERENCE_GATES,
|
reference_gates=periodic.REFERENCE_GATES if a.jet_scenario in ("none", "zero") else None,
|
||||||
runner_sha256=runner_hash(),
|
runner_sha256=runner_hash(),
|
||||||
wall_time_budget_seconds=budget,
|
wall_time_budget_seconds=budget,
|
||||||
full_pilot_calibration_seconds=calibration,
|
full_pilot_calibration_seconds=calibration,
|
||||||
)
|
)
|
||||||
host_geometry(a.source, h, a.collision)
|
if a.observe_cv:
|
||||||
|
specification["cv_observation"] = dict(
|
||||||
|
full_tstar=[290, 300], pilot_tstar=[.02, 5], spacing_tstar=.02,
|
||||||
|
refined_contours_root=cv_contours(True, 40, a.placement).tolist(),
|
||||||
|
uniform_contours_root=cv_contours(False, a.uniform_diameter, a.placement).tolist(),
|
||||||
|
uniform_grid_origin_root=uniform_origin(a.uniform_diameter),
|
||||||
|
refined_grid_origin_root=specification["fine_origin_root_edge"],
|
||||||
|
cell_width_root={"uniform": 40/a.uniform_diameter, "refined": .5},
|
||||||
|
viscosity_root=.004, components=["mass", "Px", "Py"],
|
||||||
|
units="root lattice per unit depth; outward flux per root time",
|
||||||
|
interval="Impulse belongs to the interval ending at each snapshot; first retained endpoint (pilot .02, full 290) supplies initial inventory only: exclude its preceding impulse",
|
||||||
|
qualification="not assessed; trapezoidal integration and time-coarsening are offline",
|
||||||
|
)
|
||||||
|
if a.case != "uniform":
|
||||||
|
host_geometry(a.source, h, a.collision, a.jet_scenario, a.placement)
|
||||||
(a.output / "plan.json").write_text(json.dumps(specification, indent=2))
|
(a.output / "plan.json").write_text(json.dumps(specification, indent=2))
|
||||||
if a.mode == "plan":
|
if a.mode == "plan":
|
||||||
return 0
|
return 0
|
||||||
@@ -621,8 +847,9 @@ def main():
|
|||||||
"GPU execution blocked: root GPU gates approval and explicit device selection required"
|
"GPU execution blocked: root GPU gates approval and explicit device selection required"
|
||||||
)
|
)
|
||||||
results = []
|
results = []
|
||||||
|
cases = selected_cases(a.case, a.uniform_diameter)
|
||||||
a.deadline = time.monotonic() + budget
|
a.deadline = time.monotonic() + budget
|
||||||
for case in ("uniform", "refined"):
|
for case, _ in cases:
|
||||||
try:
|
try:
|
||||||
result = run_case(a, h, periodic, case)
|
result = run_case(a, h, periodic, case)
|
||||||
except TimeoutError as exc:
|
except TimeoutError as exc:
|
||||||
@@ -650,7 +877,12 @@ def main():
|
|||||||
status="pilot_only" if a.mode == "pilot" else "incomplete",
|
status="pilot_only" if a.mode == "pilot" else "incomplete",
|
||||||
matched_case_pass=False,
|
matched_case_pass=False,
|
||||||
)
|
)
|
||||||
if len(results) == 2 and all(r["healthy"] and r["periodic"] for r in results):
|
if a.jet_scenario in ("fixed", "reverse"):
|
||||||
|
comparison.update(status=("pilot_only" if a.mode == "pilot" else "driven_diagnostics_complete")
|
||||||
|
if len(results) == len(cases) and all(r["healthy"] for r in results) else "invalid_state",
|
||||||
|
qualification="not assessed; execution completion is not physical acceptance",
|
||||||
|
jet_diagnostic_windows={r["case"]: r["jet_diagnostic_windows"] for r in results})
|
||||||
|
if a.case == "both" and len(results) == 2 and all(r["healthy"] and r["periodic"] for r in results):
|
||||||
comparison.update(
|
comparison.update(
|
||||||
status="periodic_comparison",
|
status="periodic_comparison",
|
||||||
refined_vs_uniform=periodic.compare(
|
refined_vs_uniform=periodic.compare(
|
||||||
@@ -660,14 +892,20 @@ def main():
|
|||||||
comparison["matched_case_pass"] = comparison["refined_vs_uniform"][
|
comparison["matched_case_pass"] = comparison["refined_vs_uniform"][
|
||||||
"reference_agreement"
|
"reference_agreement"
|
||||||
]
|
]
|
||||||
|
if a.case != "both":
|
||||||
|
comparison.update(status="single_case_" + results[0]["status"],
|
||||||
|
own_reference_comparison=results[0].get("reference_comparison"))
|
||||||
(a.output / "comparison.json").write_text(
|
(a.output / "comparison.json").write_text(
|
||||||
json.dumps(comparison, indent=2, allow_nan=False)
|
json.dumps(comparison, indent=2, allow_nan=False)
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
0
|
0
|
||||||
if len(results) == 2
|
if len(results) == len(cases)
|
||||||
and all(r["healthy"] for r in results)
|
and all(r["healthy"] for r in results)
|
||||||
and (a.mode == "pilot" or comparison["matched_case_pass"])
|
and (a.mode == "pilot" or comparison["matched_case_pass"]
|
||||||
|
or a.jet_scenario in ("fixed", "reverse")
|
||||||
|
or (a.case != "both" and results[0]["periodic"]
|
||||||
|
and results[0].get("reference_comparison", {}).get("reference_agreement", False)))
|
||||||
else 2
|
else 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""New-source four-case repeat of 11901a7; frozen oracle and gates are unchanged."""
|
"""New-source four-case repeat of 11901a7; frozen oracle and gates are unchanged."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
|
||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -9,25 +8,16 @@ from pathlib import Path
|
|||||||
from assess import BASELINE_FACTOR, FLOORS
|
from assess import BASELINE_FACTOR, FLOORS
|
||||||
from oracle import LIMITS, TIMES, U, rectangles
|
from oracle import LIMITS, TIMES, U, rectangles
|
||||||
from run_case import (INITIALIZATION, sample_steps, NUMERICAL_REVISION, SOURCE_SHA256,
|
from run_case import (INITIALIZATION, sample_steps, NUMERICAL_REVISION, SOURCE_SHA256,
|
||||||
BUILD_CAPTURE_SHA256, REFERENCE_HARNESS, SCALED_DIAGNOSTIC)
|
BUILD_CAPTURE_SHA256, REFERENCE_HARNESS)
|
||||||
|
|
||||||
# Host-generated snapshots of a135252 at the frozen local configurations.
|
# Host-generated snapshots of a135252 at the frozen two local configurations.
|
||||||
# These describe expected inputs; actual sidecars are captured before cleanup.
|
# These describe expected inputs; actual sidecars are captured before cleanup.
|
||||||
BUILD_SOURCES = {
|
BUILD_SOURCES = {
|
||||||
128: "803177416b7599ab89cc5afd35cf4a28a673b2f4bb8e8a6a1abc0b6d77610873",
|
128: "803177416b7599ab89cc5afd35cf4a28a673b2f4bb8e8a6a1abc0b6d77610873",
|
||||||
256: "17ec5e60f87b840d3d13b8778bc8a3aecf485469c5a061af46c5cc19b73396cf",
|
256: "17ec5e60f87b840d3d13b8778bc8a3aecf485469c5a061af46c5cc19b73396cf",
|
||||||
512: "4e85e38098858ec95c137d1b1e96846a58d9cf3cc242b349a3ad7c8b8e3ce0aa",
|
|
||||||
}
|
}
|
||||||
CASES = ((100, 128, 1, "constant"), (100, 256, 1, "constant"),
|
CASES = ((100, 128, 1, "constant"), (100, 256, 1, "constant"),
|
||||||
(100, 128, 2, "constant"), (100, 128, 2, "projected_linear"))
|
(100, 128, 2, "constant"), (100, 128, 2, "projected_linear"))
|
||||||
REFERENCE_REVISION = "2cf9853952e52f59bea63cf5c1d3a1cd76e53eed"
|
|
||||||
# Audited 2cf9853 scalar receipts, archived at evidence commit 90849a55cad4a0da9b72747da7bfe6eb75962a44.
|
|
||||||
REFERENCE_RECEIPTS = {
|
|
||||||
"uniform128": "724efafc267eaa59dcc59e8d4c16177b67f6c6a08fd2732a481833237bb19cee",
|
|
||||||
"uniform256": "3c312a99ebf747a135674dd481b77455a3d2c9a406ca388f30524b37dad9bbe9",
|
|
||||||
"constant128": "2ee28efd1e81b2e28b7902b6c4f95bb2c1ea4e5f0ec484638fa5ae83dca34bba",
|
|
||||||
"projected128": "ee67b1d4d90a048c0d3d1a16447c6eab3d89c94d151f791306e373f96dd8b963",
|
|
||||||
}
|
|
||||||
NUMERICS = dict(lattice_model="D2Q9", collision="SRT", store_precision="FP32",
|
NUMERICS = dict(lattice_model="D2Q9", collision="SRT", store_precision="FP32",
|
||||||
ddf_shifting=True, streaming="double_buffer", les_enabled=False,
|
ddf_shifting=True, streaming="double_buffer", les_enabled=False,
|
||||||
boundary="periodic", block_shape=[8, 8],
|
boundary="periodic", block_shape=[8, 8],
|
||||||
@@ -83,11 +73,7 @@ def valid_builds(record, n, depth):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def assess(records, expected_revision=None, *, diagnostic=None):
|
def assess(records, expected_revision=None):
|
||||||
if diagnostic not in (None, SCALED_DIAGNOSTIC):
|
|
||||||
raise ValueError("Unknown diagnostic")
|
|
||||||
base_n = 256 if diagnostic else 128
|
|
||||||
cases = tuple((re, n * base_n // 128, depth, scheme) for re, n, depth, scheme in CASES)
|
|
||||||
indexed, duplicates = {}, []
|
indexed, duplicates = {}, []
|
||||||
for record in records:
|
for record in records:
|
||||||
key = tuple(record.get(k) for k in ("re_L", "n", "depth", "explosion"))
|
key = tuple(record.get(k) for k in ("re_L", "n", "depth", "explosion"))
|
||||||
@@ -95,11 +81,9 @@ def assess(records, expected_revision=None, *, diagnostic=None):
|
|||||||
duplicates.append(str(key))
|
duplicates.append(str(key))
|
||||||
else:
|
else:
|
||||||
indexed[key] = record
|
indexed[key] = record
|
||||||
missing = [str(k) for k in cases if k not in indexed]
|
missing = [str(k) for k in CASES if k not in indexed]
|
||||||
checks = dict(complete_matrix=not missing, no_extra_cases=set(indexed) <= set(cases),
|
checks = dict(complete_matrix=not missing, no_extra_cases=set(indexed) <= set(CASES),
|
||||||
no_duplicate_cases=not duplicates, finite_receipts=finite_tree(records))
|
no_duplicate_cases=not duplicates, finite_receipts=finite_tree(records))
|
||||||
if diagnostic or any("diagnostic" in r for r in records):
|
|
||||||
checks["diagnostic_identity"] = all(r.get("diagnostic") == diagnostic for r in records)
|
|
||||||
sources = [r.get("source", {}) for r in records]
|
sources = [r.get("source", {}) for r in records]
|
||||||
checks["same_source_and_harness"] = bool(sources) and len({
|
checks["same_source_and_harness"] = bool(sources) and len({
|
||||||
json.dumps(s, sort_keys=True) for s in sources}) == 1
|
json.dumps(s, sort_keys=True) for s in sources}) == 1
|
||||||
@@ -122,11 +106,9 @@ def assess(records, expected_revision=None, *, diagnostic=None):
|
|||||||
) and len(set(devices)) == 1
|
) and len(set(devices)) == 1
|
||||||
enabled = [r.get("early_pressure", {}).get("enabled") for r in records]
|
enabled = [r.get("early_pressure", {}).get("enabled") for r in records]
|
||||||
checks["same_early_sampling"] = bool(enabled) and all(type(v) is bool for v in enabled) and len(set(enabled)) == 1
|
checks["same_early_sampling"] = bool(enabled) and all(type(v) is bool for v in enabled) and len(set(enabled)) == 1
|
||||||
if diagnostic:
|
|
||||||
checks["diagnostic_early_sampling"] = all(v is True for v in enabled)
|
|
||||||
shared_valid = all(checks.values())
|
shared_valid = all(checks.values())
|
||||||
complete, case_valid, early = {}, {}, []
|
complete, case_valid, early = {}, {}, []
|
||||||
for key in cases:
|
for key in CASES:
|
||||||
if key not in indexed:
|
if key not in indexed:
|
||||||
continue
|
continue
|
||||||
r = indexed[key]
|
r = indexed[key]
|
||||||
@@ -153,7 +135,7 @@ def assess(records, expected_revision=None, *, diagnostic=None):
|
|||||||
(("module", "__init__.py"), ("compiler", "cuda/compiler_v2.py"), ("kernel_root", "lbm/kernels")))
|
(("module", "__init__.py"), ("compiler", "cuda/compiler_v2.py"), ("kernel_root", "lbm/kernels")))
|
||||||
expected_patch = [] if depth == 1 else rectangles(n, depth)
|
expected_patch = [] if depth == 1 else rectangles(n, depth)
|
||||||
checks[label + " patch"] = json.dumps(r.get("refinements")) == json.dumps(expected_patch)
|
checks[label + " patch"] = json.dumps(r.get("refinements")) == json.dumps(expected_patch)
|
||||||
cells = [n*n] if depth == 1 else [17*n*n//32, 15*n*n//8]
|
cells = [n*n] if depth == 1 else [8704, 30720]
|
||||||
hops = [16*n * 2**l for l in range(depth)]
|
hops = [16*n * 2**l for l in range(depth)]
|
||||||
work = r.get("workload", {})
|
work = r.get("workload", {})
|
||||||
checks[label + " workload"] = (
|
checks[label + " workload"] = (
|
||||||
@@ -186,22 +168,19 @@ def assess(records, expected_revision=None, *, diagnostic=None):
|
|||||||
|
|
||||||
comparisons, scheme_results = [], {}
|
comparisons, scheme_results = [], {}
|
||||||
for scheme in ("constant", "projected_linear"):
|
for scheme in ("constant", "projected_linear"):
|
||||||
key = (100, base_n, 2, scheme)
|
key = (100, 128, 2, scheme)
|
||||||
for metric, floor in FLOORS.items():
|
for metric, floor in FLOORS.items():
|
||||||
amr, coarse, fine = (worst(k, metric) for k in (key, cases[0], cases[1]))
|
amr, coarse, fine = (worst(k, metric) for k in (key, CASES[0], CASES[1]))
|
||||||
budget = None if coarse is None else max(BASELINE_FACTOR * coarse, floor)
|
budget = None if coarse is None else max(BASELINE_FACTOR * coarse, floor)
|
||||||
budget = budget if finite(budget) else None
|
budget = budget if finite(budget) else None
|
||||||
ratio = None if amr is None or fine is None else amr / max(fine, 1e-30)
|
ratio = None if amr is None or fine is None else amr / max(fine, 1e-30)
|
||||||
passed = amr is not None and budget is not None and amr <= budget
|
passed = amr is not None and budget is not None and amr <= budget
|
||||||
checks[f"baseline {scheme}/{metric}"] = passed
|
checks[f"baseline {scheme}/{metric}"] = passed
|
||||||
comparisons.append(dict(explosion=scheme, metric=metric, amr=amr,
|
comparisons.append(dict(explosion=scheme, metric=metric, amr=amr,
|
||||||
baseline_budget=budget, passed=passed,
|
uniform128=coarse, baseline_budget=budget, passed=passed,
|
||||||
**{f"uniform{base_n}": coarse, f"uniform{2*base_n}": fine,
|
uniform256=fine, amr_to_uniform256=ratio if finite(ratio) else None))
|
||||||
f"amr_to_uniform{2*base_n}": ratio if finite(ratio) else None}))
|
|
||||||
if diagnostic:
|
|
||||||
comparisons[-1]["floor_dominates"] = None if coarse is None else floor >= BASELINE_FACTOR * coarse
|
|
||||||
baseline_passed = all(checks[f"baseline {scheme}/{metric}"] for metric in FLOORS)
|
baseline_passed = all(checks[f"baseline {scheme}/{metric}"] for metric in FLOORS)
|
||||||
valid = shared_valid and all(case_valid.get(k, False) for k in (cases[0], cases[1], key))
|
valid = shared_valid and all(case_valid.get(k, False) for k in (CASES[0], CASES[1], key))
|
||||||
scheme_results[scheme] = dict(passed=valid and baseline_passed,
|
scheme_results[scheme] = dict(passed=valid and baseline_passed,
|
||||||
valid_case_and_controls=valid, baseline_passed=baseline_passed)
|
valid_case_and_controls=valid, baseline_passed=baseline_passed)
|
||||||
return dict(passed=all(checks.values()), missing=missing, duplicates=duplicates, checks=checks,
|
return dict(passed=all(checks.values()), missing=missing, duplicates=duplicates, checks=checks,
|
||||||
@@ -211,89 +190,19 @@ def assess(records, expected_revision=None, *, diagnostic=None):
|
|||||||
floors=FLOORS, baseline_factor=BASELINE_FACTOR,
|
floors=FLOORS, baseline_factor=BASELINE_FACTOR,
|
||||||
main_statistic="maximum of four post-initial samples at t*=.12,.24,.36,.48",
|
main_statistic="maximum of four post-initial samples at t*=.12,.24,.36,.48",
|
||||||
early_statistic="sampled pressure maximum only, t*=k*.001875, k=0..64; not an all-step or continuous maximum",
|
early_statistic="sampled pressure maximum only, t*=k*.001875, k=0..64; not an all-step or continuous maximum",
|
||||||
finest_control=f"uniform{2*base_n} and AMR/ uniform{2*base_n} ratios are descriptive, not acceptance gates",
|
finest_control="uniform256 and AMR/ uniform256 ratios are descriptive, not acceptance gates",
|
||||||
scope="2D periodic Re100 smooth-decay diagnostic; no new algorithm, convergence-order or general AMR qualification")
|
scope="2D periodic Re100 smooth-decay diagnostic; no new algorithm, convergence-order or general AMR qualification")
|
||||||
|
|
||||||
|
|
||||||
def load_reference(paths):
|
|
||||||
"""Use the four audited historical files verbatim, never relabel them fresh controls."""
|
|
||||||
blobs = [path.read_bytes() for path in paths]
|
|
||||||
hashes = [hashlib.sha256(blob).hexdigest() for blob in blobs]
|
|
||||||
if len(hashes) != 4 or set(hashes) != set(REFERENCE_RECEIPTS.values()):
|
|
||||||
raise ValueError("Historical N128 receipt hashes differ from the audited 2cf9853 evidence")
|
|
||||||
return [json.loads(blob) for blob in blobs]
|
|
||||||
|
|
||||||
|
|
||||||
def resolution_trend(records, reference, expected_revision):
|
|
||||||
"""Signed changes are descriptive; they never replace the frozen acceptance budgets."""
|
|
||||||
old = assess(reference, REFERENCE_REVISION)
|
|
||||||
new = assess(records, expected_revision, diagnostic=SCALED_DIAGNOSTIC)
|
|
||||||
# Numerical screen failures still have useful raw errors; only malformed inputs block trends.
|
|
||||||
valid = all(v for result in (old, new) for k, v in result["checks"].items()
|
|
||||||
if not k.startswith("baseline ") and not k.endswith(" absolute"))
|
|
||||||
valid = valid and all(maximum(r["rows"], m) is not None for r in reference + records for m in LIMITS)
|
|
||||||
oracle = reference[0]["source"]["harness_sha256"].get("oracle.py") if valid else None
|
|
||||||
valid = valid and bool(oracle) and records[0]["source"]["harness_sha256"].get("oracle.py") == oracle
|
|
||||||
if not valid:
|
|
||||||
return dict(valid=False, per_time=[], worst_post_initial=[], uniform256_bridge=[])
|
|
||||||
key = lambda r: (r["re_L"], r["n"], r["depth"], r["explosion"])
|
|
||||||
before, after = {key(r): r for r in reference}, {key(r): r for r in records}
|
|
||||||
|
|
||||||
def change(role, metric, low, high, **extra):
|
|
||||||
ratio = high / low if low else None
|
|
||||||
percent = 100*(ratio-1) if ratio is not None else None
|
|
||||||
return dict(role=role, metric=metric, n128_campaign_error=low, n256_campaign_error=high,
|
|
||||||
signed_change=high-low, new_to_old=ratio if finite(ratio) else None,
|
|
||||||
ratio_unavailable_reason=None if finite(ratio) else "zero historical error" if not low else "overflow",
|
|
||||||
change_percent=percent if finite(percent) else None, **extra)
|
|
||||||
|
|
||||||
per_time, worst, bridge = [], [], []
|
|
||||||
for old_key, role in zip(CASES, ("uniform_root", "uniform_fine", "constant", "projected_linear")):
|
|
||||||
re, n, depth, scheme = old_key
|
|
||||||
low, high = before[old_key], after[(re, 2*n, depth, scheme)]
|
|
||||||
for metric in LIMITS:
|
|
||||||
per_time.extend(change(role, metric, a[metric], b[metric], tstar=a["tstar"])
|
|
||||||
for a, b in zip(low["rows"], high["rows"]))
|
|
||||||
worst.append(change(role, metric, maximum(low["rows"][1:], metric),
|
|
||||||
maximum(high["rows"][1:], metric)))
|
|
||||||
for a, b in zip(before[CASES[1]]["rows"], after[CASES[1]]["rows"]):
|
|
||||||
bridge.extend(dict(metric=m, tstar=a["tstar"], historical=a[m], fresh=b[m], signed_change=b[m]-a[m])
|
|
||||||
for m in LIMITS)
|
|
||||||
low, high = before[CASES[1]], after[CASES[1]]
|
|
||||||
bridge_identity = dict(main_rows_identical=low["rows"] == high["rows"],
|
|
||||||
early_pressure_rows_identical=low["early_pressure"]["rows"] == high["early_pressure"]["rows"])
|
|
||||||
same_runtime = reference[0]["runtime_identity"] == records[0]["runtime_identity"]
|
|
||||||
return dict(valid=True, reference_revision=REFERENCE_REVISION, reference_receipt_sha256=REFERENCE_RECEIPTS,
|
|
||||||
reference_scheme_results=old["scheme_results"], per_time=per_time, worst_post_initial=worst,
|
|
||||||
uniform256_bridge=bridge, uniform256_bridge_identity=bridge_identity,
|
|
||||||
same_recorded_runtime=same_runtime,
|
|
||||||
trend_interpretation_ready=all(bridge_identity.values()) and same_runtime,
|
|
||||||
scope="Historical same-production/oracle reference versus fresh controls; same-placement two-resolution trend only, not convergence order or phase robustness")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
p = argparse.ArgumentParser(description=__doc__)
|
p = argparse.ArgumentParser(description=__doc__)
|
||||||
p.add_argument("files", type=Path, nargs="+")
|
p.add_argument("files", type=Path, nargs="+")
|
||||||
p.add_argument("--output", type=Path, required=True)
|
p.add_argument("--output", type=Path, required=True)
|
||||||
p.add_argument("--expected-revision", required=True)
|
p.add_argument("--expected-revision", required=True)
|
||||||
p.add_argument("--diagnostic", choices=[SCALED_DIAGNOSTIC])
|
|
||||||
p.add_argument("--reference-files", type=Path, nargs=4)
|
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
if args.output.exists():
|
if args.output.exists():
|
||||||
p.error("Output already exists; choose a new path to retain prior evidence")
|
p.error("Output already exists; choose a new path to retain prior evidence")
|
||||||
if bool(args.diagnostic) != bool(args.reference_files):
|
result = assess([json.loads(path.read_text()) for path in args.files], args.expected_revision)
|
||||||
p.error("--diagnostic scaled_n256 requires --reference-files; references are diagnostic-only")
|
|
||||||
records = [json.loads(path.read_text()) for path in args.files]
|
|
||||||
result = assess(records, args.expected_revision, diagnostic=args.diagnostic)
|
|
||||||
if args.diagnostic:
|
|
||||||
try:
|
|
||||||
reference = load_reference(args.reference_files)
|
|
||||||
except ValueError as exc:
|
|
||||||
p.error(str(exc))
|
|
||||||
result["diagnostic"] = args.diagnostic
|
|
||||||
result["resolution_trend"] = resolution_trend(records, reference, args.expected_revision)
|
|
||||||
result["checks"]["valid_historical_reference_and_trend"] = result["resolution_trend"]["valid"]
|
|
||||||
result["passed"] = all(result["checks"].values())
|
|
||||||
args.output.write_text(json.dumps(result, indent=2, allow_nan=False) + "\n")
|
args.output.write_text(json.dumps(result, indent=2, allow_nan=False) + "\n")
|
||||||
print(json.dumps(result, indent=2, allow_nan=False))
|
print(json.dumps(result, indent=2, allow_nan=False))
|
||||||
raise SystemExit(0 if result["passed"] else 1)
|
raise SystemExit(0 if result["passed"] else 1)
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
"""Fixed four-case shear-wave contracts.
|
|
||||||
|
|
||||||
Default: N128 (U128, U256, constant R128, projected R128). Opt-in
|
|
||||||
--diagnostic scaled_shear_n256: N256 (U256, U512, constant R256, projected
|
|
||||||
R256), same physical rectangle, t* schedule, nu = U N / 100, eps(N), limits
|
|
||||||
and floors, plus a descriptive comparison with the hash-pinned N128 receipts.
|
|
||||||
|
|
||||||
Shared provenance, build, workload and sample-time checks are reused from
|
|
||||||
assess_projected_pairs (same cases, source pins and geometry); only the
|
|
||||||
Taylor-Green-specific checks are replaced by shear-wave metadata and physics.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import math
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import assess_projected_pairs as tg
|
|
||||||
import shear_wave as sw
|
|
||||||
import shear_nested as nested
|
|
||||||
import shear_twin as twin
|
|
||||||
from run_case import (NESTED_SHEAR, SCALED_DIAGNOSTIC, SCALED_SHEAR, SHEAR_NESTED_SHA256,
|
|
||||||
SHEAR_TWIN_SHA256, SHEAR_WAVE_SHA256, TWIN_SHEAR, sample_steps)
|
|
||||||
|
|
||||||
# Audited de5b0cc N128 receipts (evidence commit cba6bca, shear-wave-de5b0cc zip).
|
|
||||||
REFERENCE_REVISION = "de5b0cc080794d0ce44d37f845aa8e8dbf8c2016"
|
|
||||||
REFERENCE_RECEIPTS = {
|
|
||||||
"uniform128": "d4a60fb806d04bfa8098a862cceba51e00b49979a6a250fef17a5fc8e3ea3255",
|
|
||||||
"uniform256": "e0b58e734724be5f82ee5d6df84395e3181b4e96e4246dd5a574ea23a8d12386",
|
|
||||||
"constant128": "c70d7cd14b1257a01cc49f8edfcc7685d430e5301e2f2d60ed426936f8b96c51",
|
|
||||||
"projected128": "980161458b9463af09a8e3178063fea0f4bbeb465329e1d3becdb0f224537b54",
|
|
||||||
}
|
|
||||||
ROLES = ("uniform_root", "uniform_fine", "constant", "projected_linear")
|
|
||||||
|
|
||||||
SHARED = ("complete_matrix", "no_extra_cases", "no_duplicate_cases", "finite_receipts",
|
|
||||||
"same_source_and_harness", "clean_a135252_numerical_source",
|
|
||||||
"same_recorded_runtime", "same_explicit_device", "same_early_sampling")
|
|
||||||
PER_CASE = ("complete_times", "scales", "actual_builds", "reference", "runtime_source",
|
|
||||||
"patch", "workload")
|
|
||||||
|
|
||||||
|
|
||||||
METADATA_ULPS = 4
|
|
||||||
|
|
||||||
|
|
||||||
def close_ulps(recorded, expected, ulps=METADATA_ULPS):
|
|
||||||
"""Portable check for recomputed float metadata only (e.g. eps = |k|^2).
|
|
||||||
|
|
||||||
The recorded value comes from another runtime/libm, so exact equality can
|
|
||||||
fail by one ULP (observed: 0.012047856934923534 Horsea vs ...532 here).
|
|
||||||
A few ULPs is far below any meaningful change; hashes, case identity and
|
|
||||||
scientific thresholds stay exact.
|
|
||||||
"""
|
|
||||||
return (isinstance(recorded, float) and math.isfinite(recorded)
|
|
||||||
and abs(recorded - expected) <= ulps * math.ulp(max(abs(recorded), abs(expected))))
|
|
||||||
|
|
||||||
|
|
||||||
def metadata_ok(record, n):
|
|
||||||
return (record.get("flow") == "shear_wave" and record.get("mode") == list(sw.MODE)
|
|
||||||
and record.get("polarization") == sw.E.tolist()
|
|
||||||
and record.get("angle_deg") == sw.ANGLE_DEG and record.get("phase") == sw.PHI
|
|
||||||
and close_ulps(record.get("eps"), sw.eps(n))
|
|
||||||
and record.get("numerics") == tg.NUMERICS
|
|
||||||
and record.get("initialization") == sw.INITIALIZATION
|
|
||||||
and record.get("limits") == sw.LIMITS
|
|
||||||
and record.get("early_pressure") == dict(enabled=False, rows=[],
|
|
||||||
sampled_max_pressure_l2=None)
|
|
||||||
and (record.get("source") or {}).get("shear_wave_sha256") == SHEAR_WAVE_SHA256)
|
|
||||||
|
|
||||||
|
|
||||||
def cases_for(scaled):
|
|
||||||
factor = 2 if scaled else 1
|
|
||||||
return tuple((re, n * factor, depth, scheme) for re, n, depth, scheme in tg.CASES)
|
|
||||||
|
|
||||||
|
|
||||||
def assess(records, expected_revision=None, *, scaled=False):
|
|
||||||
if not isinstance(records, list) or not all(isinstance(r, dict) for r in records):
|
|
||||||
raise ValueError("records must be a list of receipt objects")
|
|
||||||
# The scaled TG assessor supplies N256/512 cases, builds and workload; its
|
|
||||||
# TG-only diagnostic identity and early-sampling checks are not used here.
|
|
||||||
base_checks = tg.assess(records, expected_revision,
|
|
||||||
diagnostic=SCALED_DIAGNOSTIC if scaled else None)["checks"]
|
|
||||||
checks = {name: base_checks.get(name, False) for name in SHARED}
|
|
||||||
checks["shear_diagnostic_identity"] = all(
|
|
||||||
r.get("diagnostic") == (SCALED_SHEAR if scaled else None) for r in records)
|
|
||||||
cases = cases_for(scaled)
|
|
||||||
indexed = {}
|
|
||||||
for r in records:
|
|
||||||
indexed.setdefault(tuple(r.get(k) for k in ("re_L", "n", "depth", "explosion")), r)
|
|
||||||
for key in cases:
|
|
||||||
_, n, depth, scheme = key
|
|
||||||
label = f"{n}/{depth}/{scheme}"
|
|
||||||
for name in PER_CASE:
|
|
||||||
checks[f"{label} {name}"] = base_checks.get(f"{label} {name}", False)
|
|
||||||
checks[f"{label} shear_metadata"] = key in indexed and metadata_ok(indexed[key], n)
|
|
||||||
contract_valid = all(checks.values())
|
|
||||||
result = dict(checks=checks, contract_valid=contract_valid,
|
|
||||||
cases=[list(k) for k in cases], expected_revision=expected_revision,
|
|
||||||
diagnostic=SCALED_SHEAR if scaled else None,
|
|
||||||
scope="2D periodic Re100 oblique (2,1) shear wave; one phase and one "
|
|
||||||
"rectangle; no placement robustness or convergence order")
|
|
||||||
if not contract_valid:
|
|
||||||
result.update(status="INVALID_SET", passed=False)
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
physics = sw.assess(indexed[cases[0]], indexed[cases[1]],
|
|
||||||
[indexed[cases[2]], indexed[cases[3]]])
|
|
||||||
except (KeyError, ValueError) as error:
|
|
||||||
result.update(status="INVALID_SET", passed=False, reason=str(error))
|
|
||||||
return result
|
|
||||||
result.update(physics=physics, status=physics["status"],
|
|
||||||
passed=physics["status"] == "PASS")
|
|
||||||
try:
|
|
||||||
result["signed"] = signed_summary([indexed[k] for k in cases])
|
|
||||||
except (KeyError, TypeError, ValueError) as error:
|
|
||||||
result["signed_unavailable"] = str(error)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
CASE_AGNOSTIC = ("finite_receipts", "same_source_and_harness", "clean_a135252_numerical_source",
|
|
||||||
"same_recorded_runtime", "same_explicit_device", "same_early_sampling")
|
|
||||||
|
|
||||||
|
|
||||||
# Fixed multi-box presets sharing one contract; only geometry, controls, pins differ.
|
|
||||||
PRESETS = {
|
|
||||||
NESTED_SHEAR: dict(
|
|
||||||
module=nested, physics=nested.assess, snapshot=True,
|
|
||||||
pins={"shear_nested_sha256": SHEAR_NESTED_SHA256},
|
|
||||||
finest_control="uniform 4N (U512)",
|
|
||||||
scope="2D periodic Re100 oblique (2,1) shear wave; fixed three-level/two-region "
|
|
||||||
"placement; changes placement and refined volume as well as depth, so no "
|
|
||||||
"isolated-level cost or resolution trend"),
|
|
||||||
TWIN_SHEAR: dict(
|
|
||||||
module=twin, physics=sw.assess, snapshot=False,
|
|
||||||
pins={"shear_nested_sha256": SHEAR_NESTED_SHA256,
|
|
||||||
"shear_twin_sha256": SHEAR_TWIN_SHA256},
|
|
||||||
finest_control="uniform 2N (U256)",
|
|
||||||
scope="2D periodic Re100 oblique (2,1) shear wave; depth-2 twin of the nested "
|
|
||||||
"preset (same two L0->L1 boxes, no L2). Twin vs nested is the effect of adding "
|
|
||||||
"this nested refinement; it also changes finest spacing and refined volume, so it "
|
|
||||||
"does not isolate abstract depth"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _nested_case_ok(r, n, depth, diagnostic=NESTED_SHEAR):
|
|
||||||
"""Per-case identity of a fixed multi-box preset (scales, builds, patch, work, pins)."""
|
|
||||||
preset = PRESETS[diagnostic]
|
|
||||||
geometry = preset["module"]
|
|
||||||
runtime = r.get("runtime_source", {})
|
|
||||||
package = Path(runtime.get("checkout", "")) / "src/CelerisLab"
|
|
||||||
cells = [n * n] if depth == 1 else geometry.LEAVES
|
|
||||||
hops = [16 * n * 2**level for level in range(depth)]
|
|
||||||
work = r.get("workload", {})
|
|
||||||
snapshot = r.get("final_snapshot")
|
|
||||||
expected_patch = [] if depth == 1 else geometry.REFINEMENTS
|
|
||||||
return {
|
|
||||||
"complete_times": r.get("completed") is True and tg.at_steps(
|
|
||||||
r.get("rows", []), sample_steps(n)[0], n),
|
|
||||||
"scales": (r.get("U") == sw.U and tg.finite(r.get("viscosity"))
|
|
||||||
and abs(r["viscosity"] - sw.U * n / 100) < 1e-15
|
|
||||||
and r.get("tau_by_level") == [0.5 + 3 * sw.U * n / 100 * 2**l
|
|
||||||
for l in range(depth)]
|
|
||||||
and r.get("mach") == sw.U * math.sqrt(3)),
|
|
||||||
"actual_builds": tg.valid_builds(r, n, depth),
|
|
||||||
"reference": (r.get("numerical_revision") == tg.NUMERICAL_REVISION
|
|
||||||
and r.get("reference_harness") == tg.REFERENCE_HARNESS),
|
|
||||||
"runtime_source": bool(runtime.get("checkout")) and all(
|
|
||||||
runtime.get(k) == str(package / tail) for k, tail in
|
|
||||||
(("module", "__init__.py"), ("compiler", "cuda/compiler_v2.py"),
|
|
||||||
("kernel_root", "lbm/kernels"))),
|
|
||||||
"patch": json.dumps(r.get("refinements")) == json.dumps(expected_patch),
|
|
||||||
"workload": (work.get("active_cells_by_level") == cells and work.get("level_hops") == hops
|
|
||||||
and work.get("root_intervals") == 16 * n
|
|
||||||
and work.get("active_collision_updates_by_level") == [
|
|
||||||
c * h for c, h in zip(cells, hops)]
|
|
||||||
and work.get("active_collision_updates") == sum(
|
|
||||||
c * h for c, h in zip(cells, hops))),
|
|
||||||
"shear_metadata": metadata_ok(r, n),
|
|
||||||
"nested_module_pin": all((r.get("source") or {}).get(key) == pin
|
|
||||||
for key, pin in preset["pins"].items()),
|
|
||||||
# Optional, nested projected depth-3 only; bounded and hashed when present.
|
|
||||||
"snapshot": snapshot is None or (
|
|
||||||
preset["snapshot"] and depth == nested.DEPTH and r.get("explosion") == "projected_linear"
|
|
||||||
and snapshot.get("kept") is True and isinstance(snapshot.get("sha256"), str)
|
|
||||||
and len(snapshot["sha256"]) == 64
|
|
||||||
and 0 < snapshot.get("bytes", 0) <= nested.SNAPSHOT_MAX_BYTES
|
|
||||||
and snapshot.get("leaf_counts") == nested.LEAVES
|
|
||||||
and snapshot.get("step") == sample_steps(n)[0][-1]),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def assess_nested(records, expected_revision=None, diagnostic=NESTED_SHEAR):
|
|
||||||
"""Fixed four-case preset contract: nested_shear_n128 (U128, U512, constant/projected
|
|
||||||
R128 depth 3) or twin_shear_n128 (U128, U256, constant/projected R128 depth 2)."""
|
|
||||||
preset = PRESETS[diagnostic]
|
|
||||||
geometry = preset["module"]
|
|
||||||
if not isinstance(records, list) or not all(isinstance(r, dict) for r in records):
|
|
||||||
raise ValueError("records must be a list of receipt objects")
|
|
||||||
base_checks = tg.assess(records, expected_revision)["checks"]
|
|
||||||
checks = {name: base_checks.get(name, False) for name in CASE_AGNOSTIC}
|
|
||||||
indexed, duplicates = {}, []
|
|
||||||
for r in records:
|
|
||||||
key = tuple(r.get(k) for k in ("re_L", "n", "depth", "explosion"))
|
|
||||||
(duplicates.append(key) if key in indexed else indexed.__setitem__(key, r))
|
|
||||||
checks.update(complete_matrix=all(k in indexed for k in geometry.CASES),
|
|
||||||
no_extra_cases=set(indexed) <= set(geometry.CASES),
|
|
||||||
no_duplicate_cases=not duplicates,
|
|
||||||
shear_diagnostic_identity=all(r.get("diagnostic") == diagnostic
|
|
||||||
for r in records))
|
|
||||||
for key in geometry.CASES:
|
|
||||||
_, n, depth, scheme = key
|
|
||||||
label = f"{n}/{depth}/{scheme}"
|
|
||||||
case = _nested_case_ok(indexed[key], n, depth, diagnostic) if key in indexed else {}
|
|
||||||
for name in ("complete_times", "scales", "actual_builds", "reference", "runtime_source",
|
|
||||||
"patch", "workload", "shear_metadata", "nested_module_pin", "snapshot"):
|
|
||||||
checks[f"{label} {name}"] = bool(case.get(name, False))
|
|
||||||
contract_valid = all(checks.values())
|
|
||||||
result = dict(checks=checks, contract_valid=contract_valid, diagnostic=diagnostic,
|
|
||||||
cases=[list(k) for k in geometry.CASES], expected_revision=expected_revision,
|
|
||||||
finest_control=preset["finest_control"], scope=preset["scope"])
|
|
||||||
if not contract_valid:
|
|
||||||
result.update(status="INVALID_SET", passed=False)
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
cases = geometry.CASES
|
|
||||||
physics = preset["physics"](indexed[cases[0]], indexed[cases[1]],
|
|
||||||
[indexed[cases[2]], indexed[cases[3]]])
|
|
||||||
except (KeyError, ValueError) as error:
|
|
||||||
result.update(status="INVALID_SET", passed=False, reason=str(error))
|
|
||||||
return result
|
|
||||||
result.update(physics=physics, status=physics["status"], passed=physics["status"] == "PASS")
|
|
||||||
try:
|
|
||||||
# Role "uniform_fine" is the preset's finest control (4N nested, 2N twin).
|
|
||||||
result["signed"] = signed_summary([indexed[k] for k in geometry.CASES])
|
|
||||||
except (KeyError, TypeError, ValueError) as error:
|
|
||||||
result["signed_unavailable"] = str(error)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _signed(record):
|
|
||||||
"""Raw signed quantities of one valid record (None when the fit is unavailable)."""
|
|
||||||
s = sw.summarize(record)
|
|
||||||
bias = None if not s["rate_fit_valid"] else s["rate"] / s["exact_rate"] - 1
|
|
||||||
return dict(n=record["n"], depth=record["depth"], explosion=record["explosion"],
|
|
||||||
rate_fit_valid=s["rate_fit_valid"], signed_rate_bias=bias,
|
|
||||||
rate_tstar=s["rate_tstar"], exact_rate_tstar=s["exact_rate_tstar"],
|
|
||||||
interval_rate_errors=s["interval_rate_errors"],
|
|
||||||
residual_l2=s["residual_l2"], spurious_pressure_scaled=s["spurious_pressure_scaled"],
|
|
||||||
spurious_pressure_rms=s["spurious_pressure_rms"],
|
|
||||||
final_amplitude_error=record["rows"][-1]["amplitude_error"])
|
|
||||||
|
|
||||||
|
|
||||||
def signed_summary(records):
|
|
||||||
"""Signed bias per case and refined excess over each uniform control."""
|
|
||||||
rows = dict(zip(ROLES, (_signed(r) for r in records)))
|
|
||||||
|
|
||||||
def minus(a, b):
|
|
||||||
return None if a is None or b is None else a - b
|
|
||||||
|
|
||||||
for role in ("constant", "projected_linear"):
|
|
||||||
for control in ("uniform_root", "uniform_fine"):
|
|
||||||
rows[role][f"excess_over_{control}"] = {
|
|
||||||
key: minus(rows[role][key], rows[control][key])
|
|
||||||
for key in ("signed_rate_bias", "residual_l2", "spurious_pressure_rms")}
|
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
def load_reference(paths):
|
|
||||||
"""The four audited N128 receipts verbatim; never relabelled as fresh controls."""
|
|
||||||
blobs = [Path(path).read_bytes() for path in paths]
|
|
||||||
hashes = {hashlib.sha256(blob).hexdigest() for blob in blobs}
|
|
||||||
if len(blobs) != 4 or hashes != set(REFERENCE_RECEIPTS.values()):
|
|
||||||
raise ValueError("N128 receipt hashes differ from the audited de5b0cc evidence")
|
|
||||||
return [json.loads(blob) for blob in blobs]
|
|
||||||
|
|
||||||
|
|
||||||
def resolution_trend(new_result, reference, reference_revision=REFERENCE_REVISION):
|
|
||||||
"""Descriptive N128 -> N256 signed changes; never an acceptance gate or an order."""
|
|
||||||
old_result = assess(reference, reference_revision)
|
|
||||||
if "signed" not in old_result or "signed" not in new_result:
|
|
||||||
return dict(valid=False, reason="N128 reference or N256 set lacks a valid signed summary")
|
|
||||||
out = {}
|
|
||||||
for role in ROLES:
|
|
||||||
old, new = old_result["signed"][role], new_result["signed"][role]
|
|
||||||
out[role] = {}
|
|
||||||
for key in ("signed_rate_bias", "residual_l2", "spurious_pressure_rms",
|
|
||||||
*(f"excess_over_{c}" for c in ("uniform_root", "uniform_fine"))):
|
|
||||||
if key not in old:
|
|
||||||
continue
|
|
||||||
pairs = old[key].items() if isinstance(old[key], dict) else [(None, old[key])]
|
|
||||||
for sub, low in pairs:
|
|
||||||
high = new[key][sub] if sub else new[key]
|
|
||||||
ok = low is not None and high is not None
|
|
||||||
out[role][key if sub is None else f"{key}.{sub}"] = dict(
|
|
||||||
n128=low, n256=high, signed_change=high - low if ok else None,
|
|
||||||
n256_to_n128=high / low if ok and low else None)
|
|
||||||
return dict(valid=True, reference_revision=reference_revision,
|
|
||||||
reference_receipt_sha256=REFERENCE_RECEIPTS,
|
|
||||||
reference_status=old_result["status"], changes=out,
|
|
||||||
scope="same-placement two-resolution trend; not a convergence order, "
|
|
||||||
"placement robustness or redesign decision")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
p = argparse.ArgumentParser(description=__doc__)
|
|
||||||
p.add_argument("files", type=Path, nargs=4)
|
|
||||||
p.add_argument("--output", type=Path, required=True)
|
|
||||||
p.add_argument("--expected-revision", required=True)
|
|
||||||
p.add_argument("--diagnostic", choices=[SCALED_SHEAR, NESTED_SHEAR, TWIN_SHEAR])
|
|
||||||
p.add_argument("--reference-files", type=Path, nargs=4)
|
|
||||||
args = p.parse_args()
|
|
||||||
if args.output.exists():
|
|
||||||
p.error("Output already exists; choose a new path to retain prior evidence")
|
|
||||||
if args.diagnostic in PRESETS:
|
|
||||||
if args.reference_files:
|
|
||||||
p.error(f"{args.diagnostic} takes no reference files (no resolution trend)")
|
|
||||||
result = assess_nested([json.loads(path.read_text()) for path in args.files],
|
|
||||||
args.expected_revision, args.diagnostic)
|
|
||||||
text = json.dumps(result, indent=2, allow_nan=False) + "\n"
|
|
||||||
args.output.write_text(text)
|
|
||||||
print(text, end="")
|
|
||||||
raise SystemExit(0 if result["passed"] else 1)
|
|
||||||
if bool(args.diagnostic) != bool(args.reference_files):
|
|
||||||
p.error("--diagnostic scaled_shear_n256 requires --reference-files (N128 receipts)")
|
|
||||||
result = assess([json.loads(path.read_text()) for path in args.files], args.expected_revision,
|
|
||||||
scaled=bool(args.diagnostic))
|
|
||||||
if args.diagnostic:
|
|
||||||
try:
|
|
||||||
reference = load_reference(args.reference_files)
|
|
||||||
except ValueError as exc:
|
|
||||||
p.error(str(exc))
|
|
||||||
result["resolution_trend"] = resolution_trend(result, reference)
|
|
||||||
text = json.dumps(result, indent=2, allow_nan=False) + "\n"
|
|
||||||
args.output.write_text(text)
|
|
||||||
print(text, end="")
|
|
||||||
raise SystemExit(0 if result["passed"] else 1)
|
|
||||||
@@ -18,37 +18,6 @@ SOURCE_SHA256 = "a5dbfd804befce868067d79a23e5fd47dce7f505b3f9b1bb2145fb57cb5f217
|
|||||||
BUILD_CAPTURE_SHA256 = "3dbaa6547cb0a4cb1541054f3b64ad8b6e18cc70527ac6166e56b944e9bd464a"
|
BUILD_CAPTURE_SHA256 = "3dbaa6547cb0a4cb1541054f3b64ad8b6e18cc70527ac6166e56b944e9bd464a"
|
||||||
REFERENCE_HARNESS = "11901a7f77557a901e017a171eeae1758d584fa5"
|
REFERENCE_HARNESS = "11901a7f77557a901e017a171eeae1758d584fa5"
|
||||||
INITIALIZATION = "physical FP64 equilibrium G at actual cell centers; rho=1+3p"
|
INITIALIZATION = "physical FP64 equilibrium G at actual cell centers; rho=1+3p"
|
||||||
SHEAR_WAVE_SHA256 = "81d5c8ee60864372887cddf9239580c82fb6750d956038f1e3ee9de61aa4e877"
|
|
||||||
SCALED_DIAGNOSTIC = "scaled_n256"
|
|
||||||
SCALED_SHEAR = "scaled_shear_n256" # mutually exclusive with scaled_n256 (one flow each)
|
|
||||||
NESTED_SHEAR = "nested_shear_n128" # fixed three-level/two-region shear preset
|
|
||||||
SHEAR_NESTED_SHA256 = "fa6cfe87fb4df8c6d732c637c885276833c81973b950c7724d734f8fc350bcb0"
|
|
||||||
TWIN_SHEAR = "twin_shear_n128" # depth-2 twin: nested L0->L1 boxes without L2
|
|
||||||
SHEAR_TWIN_SHA256 = "1cbf37f18e89aea577581631c4565f51e68d22a3739b698816ef64acbf2b2bb4"
|
|
||||||
SHEAR_PROBES_SHA256 = "53ba8af8ac9c8078c40f84488c7ade4c11ccc91dd1fc275ff806042e9af30166" # signed interface-probe mechanism diagnostic
|
|
||||||
|
|
||||||
|
|
||||||
def flow_functions(flow="taylor_green"):
|
|
||||||
"""(initial, metrics, limits, initialization); the default is the frozen TG oracle."""
|
|
||||||
if flow == "taylor_green":
|
|
||||||
return initial, metrics, LIMITS, INITIALIZATION
|
|
||||||
if flow == "shear_wave":
|
|
||||||
import shear_wave
|
|
||||||
return shear_wave.initial, shear_wave.metrics, shear_wave.LIMITS, shear_wave.INITIALIZATION
|
|
||||||
raise ValueError(f"unknown flow {flow!r}")
|
|
||||||
|
|
||||||
|
|
||||||
def finite_row(row):
|
|
||||||
"""Every scalar or list-valued diagnostic is finite (TG rows are all scalars)."""
|
|
||||||
return all(np.isfinite(np.asarray(value, dtype=float)).all() for value in row.values())
|
|
||||||
|
|
||||||
|
|
||||||
def sample(run, masks, step, n, viscosity, initial_mass, metrics, depth, label="sample"):
|
|
||||||
arrays = (run.download(),) if depth == 1 else run.download()
|
|
||||||
row = metrics(arrays, masks, step, n, viscosity, initial_mass)
|
|
||||||
if not finite_row(row):
|
|
||||||
raise FloatingPointError(f"nonfinite {label} diagnostics")
|
|
||||||
return row
|
|
||||||
|
|
||||||
|
|
||||||
def sample_steps(n, early_pressure=False):
|
def sample_steps(n, early_pressure=False):
|
||||||
@@ -95,7 +64,7 @@ def provenance(repo):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def verify_source(repo, revision, flow="taylor_green", probes=False):
|
def verify_source(repo, revision):
|
||||||
"""Exact clean harness commit, fixed production tree, and reviewed capture helper."""
|
"""Exact clean harness commit, fixed production tree, and reviewed capture helper."""
|
||||||
import oracle
|
import oracle
|
||||||
from tests.validation.multigrid import check_projected_pairs as gate
|
from tests.validation.multigrid import check_projected_pairs as gate
|
||||||
@@ -114,34 +83,6 @@ def verify_source(repo, revision, flow="taylor_green", probes=False):
|
|||||||
if result["source_tree_sha256"] != SOURCE_SHA256:
|
if result["source_tree_sha256"] != SOURCE_SHA256:
|
||||||
raise RuntimeError("Wrong numerical source fingerprint")
|
raise RuntimeError("Wrong numerical source fingerprint")
|
||||||
result["build_capture_sha256"] = BUILD_CAPTURE_SHA256
|
result["build_capture_sha256"] = BUILD_CAPTURE_SHA256
|
||||||
if probes:
|
|
||||||
import shear_probes
|
|
||||||
module = harness / "shear_probes.py"
|
|
||||||
if (Path(shear_probes.__file__).resolve() != module
|
|
||||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_PROBES_SHA256):
|
|
||||||
raise RuntimeError("Wrong shear_probes module checkout or content")
|
|
||||||
result["shear_probes_sha256"] = SHEAR_PROBES_SHA256
|
|
||||||
if flow == "shear_twin":
|
|
||||||
import shear_twin
|
|
||||||
module = harness / "shear_twin.py"
|
|
||||||
if (Path(shear_twin.__file__).resolve() != module
|
|
||||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_TWIN_SHA256):
|
|
||||||
raise RuntimeError("Wrong shear_twin module checkout or content")
|
|
||||||
result["shear_twin_sha256"] = SHEAR_TWIN_SHA256
|
|
||||||
if flow in ("shear_nested", "shear_twin"): # the twin reuses the nested geometry
|
|
||||||
import shear_nested
|
|
||||||
module = harness / "shear_nested.py"
|
|
||||||
if (Path(shear_nested.__file__).resolve() != module
|
|
||||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_NESTED_SHA256):
|
|
||||||
raise RuntimeError("Wrong shear_nested module checkout or content")
|
|
||||||
result["shear_nested_sha256"] = SHEAR_NESTED_SHA256
|
|
||||||
if flow in ("shear_wave", "shear_nested", "shear_twin"):
|
|
||||||
import shear_wave
|
|
||||||
module = harness / "shear_wave.py"
|
|
||||||
if (Path(shear_wave.__file__).resolve() != module
|
|
||||||
or hashlib.sha256(module.read_bytes()).hexdigest() != SHEAR_WAVE_SHA256):
|
|
||||||
raise RuntimeError("Wrong shear_wave module checkout or content")
|
|
||||||
result["shear_wave_sha256"] = SHEAR_WAVE_SHA256
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -204,79 +145,26 @@ def main():
|
|||||||
p.add_argument("--run-gpu", action="store_true")
|
p.add_argument("--run-gpu", action="store_true")
|
||||||
p.add_argument("--dry-run", action="store_true")
|
p.add_argument("--dry-run", action="store_true")
|
||||||
p.add_argument("--re", type=int, choices=[100, 1000], required=True)
|
p.add_argument("--re", type=int, choices=[100, 1000], required=True)
|
||||||
p.add_argument("--n", type=int, choices=[32, 64, 128, 256, 512], required=True)
|
p.add_argument("--n", type=int, choices=[32, 64, 128, 256], required=True)
|
||||||
p.add_argument("--depth", type=int, choices=[1, 2, 3], required=True)
|
p.add_argument("--depth", type=int, choices=[1, 2, 3], required=True)
|
||||||
p.add_argument("--output", type=Path, required=True)
|
p.add_argument("--output", type=Path, required=True)
|
||||||
p.add_argument("--explosion", choices=["constant", "projected_linear"], default="constant")
|
p.add_argument("--explosion", choices=["constant", "projected_linear"], default="constant")
|
||||||
p.add_argument("--diagnostic",
|
|
||||||
choices=[SCALED_DIAGNOSTIC, SCALED_SHEAR, NESTED_SHEAR, TWIN_SHEAR],
|
|
||||||
help="explicit four-case N256/fine512 same-placement diagnostic "
|
|
||||||
"(scaled_n256: Taylor-Green; scaled_shear_n256: shear wave) or the "
|
|
||||||
"fixed three-level/two-region shear preset (nested_shear_n128) "
|
|
||||||
"or its depth-2 twin (twin_shear_n128)")
|
|
||||||
p.add_argument("--probe-trace", action="store_true",
|
|
||||||
help="signed interface-probe trace (nested U128/U512/projected d3, "
|
|
||||||
"twin projected d2); mechanism diagnostic, no gates")
|
|
||||||
p.add_argument("--final-snapshot", action="store_true",
|
|
||||||
help="nested_shear_n128 projected depth-3 only: final macro snapshot")
|
|
||||||
p.add_argument("--early-pressure", action="store_true",
|
p.add_argument("--early-pressure", action="store_true",
|
||||||
help="also save 65 scalar pressure samples at t*=k*.001875, k=0..64")
|
help="also save 65 scalar pressure samples at t*=k*.001875, k=0..64")
|
||||||
p.add_argument("--flow", choices=["taylor_green", "shear_wave"], default="taylor_green")
|
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
nested = args.diagnostic == NESTED_SHEAR
|
|
||||||
if args.flow == "shear_wave" and (args.re != 100 or (args.depth == 3 and not nested)
|
|
||||||
or args.early_pressure):
|
|
||||||
p.error("shear_wave requires --re 100, depth 1 or 2 and no --early-pressure")
|
|
||||||
probe_cases = {(NESTED_SHEAR, 128, 1, "constant"), (NESTED_SHEAR, 512, 1, "constant"),
|
|
||||||
(NESTED_SHEAR, 128, 3, "projected_linear"),
|
|
||||||
(TWIN_SHEAR, 128, 2, "projected_linear")}
|
|
||||||
if args.probe_trace and (args.final_snapshot or (
|
|
||||||
args.diagnostic, args.n, args.depth, args.explosion) not in probe_cases):
|
|
||||||
p.error("--probe-trace requires nested U128/U512/projected depth 3 or twin projected "
|
|
||||||
"depth 2, without --final-snapshot")
|
|
||||||
if args.final_snapshot and not (nested and args.depth == 3
|
|
||||||
and args.explosion == "projected_linear"):
|
|
||||||
p.error("--final-snapshot requires nested_shear_n128 projected depth 3")
|
|
||||||
initial, metrics, limits, initialization = flow_functions(args.flow)
|
|
||||||
if args.output.exists():
|
if args.output.exists():
|
||||||
p.error("Output already exists; choose a new path to retain prior evidence")
|
p.error("Output already exists; choose a new path to retain prior evidence")
|
||||||
if args.explosion != "constant" and args.depth != 2 and not nested:
|
if args.explosion != "constant" and args.depth != 2:
|
||||||
p.error("projected_linear requires depth 2")
|
p.error("projected_linear requires depth 2")
|
||||||
twin = args.diagnostic == TWIN_SHEAR
|
|
||||||
if nested or twin:
|
|
||||||
import shear_nested
|
|
||||||
import shear_twin
|
|
||||||
preset = shear_nested if nested else shear_twin
|
|
||||||
if args.flow != "shear_wave":
|
|
||||||
p.error(f"--diagnostic {args.diagnostic} requires --flow shear_wave")
|
|
||||||
if (args.re, args.n, args.depth, args.explosion) not in preset.CASES:
|
|
||||||
p.error(f"{args.diagnostic} requires one of its four Re100 cases")
|
|
||||||
elif args.diagnostic:
|
|
||||||
cases = {(256, 1, "constant"), (512, 1, "constant"),
|
|
||||||
(256, 2, "constant"), (256, 2, "projected_linear")}
|
|
||||||
flow = "shear_wave" if args.diagnostic == SCALED_SHEAR else "taylor_green"
|
|
||||||
if args.flow != flow:
|
|
||||||
p.error(f"--diagnostic {args.diagnostic} requires --flow {flow}")
|
|
||||||
if args.diagnostic == SCALED_DIAGNOSTIC and (
|
|
||||||
args.re != 100 or (args.n, args.depth, args.explosion) not in cases
|
|
||||||
or not args.early_pressure):
|
|
||||||
p.error("scaled_n256 requires one of its four Re100 cases and --early-pressure")
|
|
||||||
if args.diagnostic == SCALED_SHEAR and (args.n, args.depth, args.explosion) not in cases:
|
|
||||||
p.error("scaled_shear_n256 requires one of its four Re100 cases")
|
|
||||||
elif args.n == 512:
|
|
||||||
p.error("N512 requires --diagnostic scaled_n256, scaled_shear_n256 or nested_shear_n128")
|
|
||||||
selected = os.environ.get("CUDA_VISIBLE_DEVICES", "")
|
selected = os.environ.get("CUDA_VISIBLE_DEVICES", "")
|
||||||
if not args.dry_run and (not args.run_gpu or not selected or "," in selected or selected == "-1"):
|
if not args.dry_run and (not args.run_gpu or not selected or "," in selected or selected == "-1"):
|
||||||
p.error("--run-gpu and exactly one explicit CUDA_VISIBLE_DEVICES are required")
|
p.error("--run-gpu and exactly one explicit CUDA_VISIBLE_DEVICES are required")
|
||||||
repo = args.repo.resolve()
|
repo = args.repo.resolve()
|
||||||
sys.path[:0] = [str(repo / "src"), str(repo)]
|
sys.path[:0] = [str(repo / "src"), str(repo)]
|
||||||
source = verify_source(repo, args.expected_revision,
|
source = verify_source(repo, args.expected_revision)
|
||||||
"shear_nested" if nested else "shear_twin" if twin else args.flow,
|
|
||||||
probes=args.probe_trace)
|
|
||||||
runtime = runtime_source(repo)
|
runtime = runtime_source(repo)
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
print(json.dumps(dict(source=source, runtime_source=runtime, dry_run=True,
|
print(json.dumps(dict(source=source, runtime_source=runtime, dry_run=True)))
|
||||||
**({"diagnostic": args.diagnostic} if args.diagnostic else {}))))
|
|
||||||
return 0
|
return 0
|
||||||
from CelerisLab.cuda.context import CudaContext
|
from CelerisLab.cuda.context import CudaContext
|
||||||
from CelerisLab.lbm.blocks import BlockLayout2D
|
from CelerisLab.lbm.blocks import BlockLayout2D
|
||||||
@@ -286,12 +174,7 @@ def main():
|
|||||||
from CelerisLab.lbm.hierarchy import build_hierarchy, leaf_partition
|
from CelerisLab.lbm.hierarchy import build_hierarchy, leaf_partition
|
||||||
|
|
||||||
cfg = config(args.n, args.re)
|
cfg = config(args.n, args.re)
|
||||||
if args.depth == 1:
|
refinements = [] if args.depth == 1 else rectangles(args.n, args.depth)
|
||||||
refinements = []
|
|
||||||
elif nested or twin:
|
|
||||||
refinements = [list(level) for level in preset.REFINEMENTS]
|
|
||||||
else:
|
|
||||||
refinements = rectangles(args.n, args.depth)
|
|
||||||
h = build_hierarchy(cfg, refinements)
|
h = build_hierarchy(cfg, refinements)
|
||||||
np.testing.assert_array_equal(leaf_partition(h), np.ones((args.n, args.n)))
|
np.testing.assert_array_equal(leaf_partition(h), np.ones((args.n, args.n)))
|
||||||
masks = [k == ACTIVE for k in h.kinds]
|
masks = [k == ACTIVE for k in h.kinds]
|
||||||
@@ -314,7 +197,7 @@ def main():
|
|||||||
mach=U * np.sqrt(3),
|
mach=U * np.sqrt(3),
|
||||||
viscosity=cfg.viscosity,
|
viscosity=cfg.viscosity,
|
||||||
tau_by_level=[0.5 + 3 * c.viscosity for c in h.configs],
|
tau_by_level=[0.5 + 3 * c.viscosity for c in h.configs],
|
||||||
initialization=initialization,
|
initialization=INITIALIZATION,
|
||||||
numerics=dict(lattice_model=cfg.lattice_model, collision=cfg.collision,
|
numerics=dict(lattice_model=cfg.lattice_model, collision=cfg.collision,
|
||||||
store_precision=cfg.store_precision, ddf_shifting=cfg.ddf_shifting,
|
store_precision=cfg.store_precision, ddf_shifting=cfg.ddf_shifting,
|
||||||
streaming=cfg.streaming, les_enabled=cfg.les_enabled,
|
streaming=cfg.streaming, les_enabled=cfg.les_enabled,
|
||||||
@@ -322,20 +205,13 @@ def main():
|
|||||||
sample_phase="postcollision G at root synchronization"),
|
sample_phase="postcollision G at root synchronization"),
|
||||||
cuda_visible_devices=selected,
|
cuda_visible_devices=selected,
|
||||||
device_id=0,
|
device_id=0,
|
||||||
limits=limits,
|
limits=LIMITS,
|
||||||
workload=workload(masks, round(TIMES[-1] * args.n / U)),
|
workload=workload(masks, round(TIMES[-1] * args.n / U)),
|
||||||
rows=[],
|
rows=[],
|
||||||
early_pressure=dict(enabled=args.early_pressure, rows=[],
|
early_pressure=dict(enabled=args.early_pressure, rows=[],
|
||||||
sampled_max_pressure_l2=None),
|
sampled_max_pressure_l2=None),
|
||||||
completed=False,
|
completed=False,
|
||||||
)
|
)
|
||||||
if args.flow != "taylor_green":
|
|
||||||
import shear_wave
|
|
||||||
report.update(flow=args.flow, mode=list(shear_wave.MODE),
|
|
||||||
polarization=shear_wave.E.tolist(), angle_deg=shear_wave.ANGLE_DEG,
|
|
||||||
phase=shear_wave.PHI, eps=shear_wave.eps(args.n))
|
|
||||||
if args.diagnostic:
|
|
||||||
report["diagnostic"] = args.diagnostic
|
|
||||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
def save():
|
def save():
|
||||||
temporary = args.output.with_suffix(args.output.suffix + ".tmp")
|
temporary = args.output.with_suffix(args.output.suffix + ".tmp")
|
||||||
@@ -370,37 +246,17 @@ def main():
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
initial_mass = None
|
initial_mass = None
|
||||||
probe_steps = set()
|
for step in sorted(set(main_steps + early_steps)):
|
||||||
if args.probe_trace:
|
|
||||||
import shear_probes
|
|
||||||
probe_root = dict(zip(shear_probes.case_steps(args.n), shear_probes.STEPS))
|
|
||||||
probe_steps = set(probe_root)
|
|
||||||
probes = shear_probes.select(masks, args.n)
|
|
||||||
report["probe_trace"] = shear_probes.new_trace(
|
|
||||||
probes, cfg.viscosity, [args.diagnostic, args.n, args.depth, args.explosion])
|
|
||||||
for step in sorted(set(main_steps + early_steps) | probe_steps):
|
|
||||||
run.step(step - run.step_count)
|
run.step(step - run.step_count)
|
||||||
if step in probe_steps:
|
arrays = (run.download(),) if args.depth == 1 else run.download()
|
||||||
trace = report["probe_trace"]
|
row = metrics(arrays, masks, step, args.n, cfg.viscosity, initial_mass)
|
||||||
trace["steps"].append(probe_root[step]) # root-physical step
|
if not all(np.isfinite(value) for value in row.values()):
|
||||||
trace["values"].append(shear_probes.sample(
|
raise FloatingPointError("nonfinite sample diagnostics")
|
||||||
(run.download(),) if args.depth == 1 else run.download(), probes))
|
|
||||||
if step not in main_steps and step not in early_steps:
|
|
||||||
continue # probe-only step: no qualification metrics
|
|
||||||
row = sample(run, masks, step, args.n, cfg.viscosity, initial_mass,
|
|
||||||
metrics, args.depth)
|
|
||||||
row["step"] = step
|
row["step"] = step
|
||||||
if initial_mass is None:
|
if initial_mass is None:
|
||||||
initial_mass = row["mass"]
|
initial_mass = row["mass"]
|
||||||
if step in main_steps:
|
if step in main_steps:
|
||||||
report["rows"].append(row)
|
report["rows"].append(row)
|
||||||
if args.final_snapshot and step == main_steps[-1]:
|
|
||||||
# Optional macro-only snapshot; scalar receipt stays authoritative.
|
|
||||||
report["final_snapshot"] = shear_nested.write_snapshot(
|
|
||||||
args.output.with_name(args.output.stem + ".final_macro.npz"),
|
|
||||||
run.download(), masks,
|
|
||||||
shear_nested.snapshot_meta(step, cfg.viscosity, args.explosion,
|
|
||||||
source.get("revision")))
|
|
||||||
if step in early_steps:
|
if step in early_steps:
|
||||||
trace = report["early_pressure"]
|
trace = report["early_pressure"]
|
||||||
trace["rows"].append({key: row[key] for key in ("step", "tstar", "pressure_l2")})
|
trace["rows"].append({key: row[key] for key in ("step", "tstar", "pressure_l2")})
|
||||||
@@ -422,8 +278,10 @@ def main():
|
|||||||
# Exercise real kernels/halos/transfers on every level before READY.
|
# Exercise real kernels/halos/transfers on every level before READY.
|
||||||
# This is the first measured trajectory step, not discarded warmup.
|
# This is the first measured trajectory step, not discarded warmup.
|
||||||
run.step(1)
|
run.step(1)
|
||||||
sample(run, masks, 1, args.n, cfg.viscosity, initial_mass,
|
probe = (run.download(),) if args.depth == 1 else run.download()
|
||||||
metrics, args.depth, "READY")
|
probe_metrics = metrics(probe, masks, 1, args.n, cfg.viscosity, initial_mass)
|
||||||
|
if not all(np.isfinite(value) for value in probe_metrics.values()):
|
||||||
|
raise FloatingPointError("nonfinite READY diagnostics")
|
||||||
report["ready_after_step"] = 1
|
report["ready_after_step"] = 1
|
||||||
save()
|
save()
|
||||||
print(
|
print(
|
||||||
@@ -439,16 +297,11 @@ def main():
|
|||||||
),
|
),
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
if args.probe_trace:
|
|
||||||
try:
|
|
||||||
report["probe_trace"]["bytes"] = shear_probes.validate(report["probe_trace"])
|
|
||||||
except ValueError as error: # keep the scalar receipt; drop an invalid trace
|
|
||||||
report["probe_trace"] = dict(error=str(error))
|
|
||||||
report.update(
|
report.update(
|
||||||
completed=True,
|
completed=True,
|
||||||
seconds=time.monotonic() - started,
|
seconds=time.monotonic() - started,
|
||||||
screening_pass=all(
|
screening_pass=all(
|
||||||
row[key] <= limit for row in report["rows"] for key, limit in limits.items()
|
row[key] <= limit for row in report["rows"] for key, limit in LIMITS.items()
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
save()
|
save()
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
"""Fixed three-level/two-region shear preset (opt-in nested_shear_n128).
|
|
||||||
|
|
||||||
Four fresh cases: U128, U512 (finest control = 4N), constant R128 depth 3,
|
|
||||||
projected R128 depth 3. Same wave, phase, Re100/U=0.03, initialization, five
|
|
||||||
sample times, eps(root N), limits and floors as shear_wave. This changes
|
|
||||||
placement and refined volume as well as depth: no isolated-level cost or
|
|
||||||
resolution-trend label. shear_wave.py is reused unchanged (historical receipt
|
|
||||||
identities stay valid); only the depth/finest-control contract is generalized
|
|
||||||
here.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
import shear_wave as sw
|
|
||||||
from oracle import CX, CY, U, centers
|
|
||||||
|
|
||||||
ROOT_N = 128
|
|
||||||
DEPTH = 3
|
|
||||||
FINEST_FACTOR = 2 ** (DEPTH - 1) # finest-spacing uniform control is 4N
|
|
||||||
# Parent-frame (x0, y0, x1, y1) rectangles; level-wide, zero-origin unions.
|
|
||||||
REFINEMENTS = [
|
|
||||||
[(16, 16, 60, 112), (68, 24, 112, 120)], # L0 -> L1, root coordinates
|
|
||||||
[(44, 44, 108, 212), (148, 60, 212, 228)], # L1 -> L2, L1 coordinates
|
|
||||||
]
|
|
||||||
CASES = ((100, ROOT_N, 1, "constant"), (100, FINEST_FACTOR * ROOT_N, 1, "constant"),
|
|
||||||
(100, ROOT_N, DEPTH, "constant"), (100, ROOT_N, DEPTH, "projected_linear"))
|
|
||||||
ROOT_STEPS = 16 * ROOT_N
|
|
||||||
LEAVES = [7936, 12288, 86016]
|
|
||||||
HOPS = [ROOT_STEPS * 2**level for level in range(DEPTH)] # [2048, 4096, 8192]
|
|
||||||
WORK = sum(c * h for c, h in zip(LEAVES, HOPS)) # 771227648 per refined case
|
|
||||||
NU = [U * ROOT_N / sw.RE * 2**level for level in range(DEPTH)] # [.0384, .0768, .1536]
|
|
||||||
TAU = [0.5 + 3 * nu for nu in NU] # [.6152, .7304, .9608]
|
|
||||||
SNAPSHOT_MAX_BYTES = 8 * 1024 * 1024
|
|
||||||
SNAPSHOT_KEYS = ("level", "i", "j", "rho", "ux", "uy", "meta")
|
|
||||||
|
|
||||||
|
|
||||||
def leaf_masks():
|
|
||||||
"""Independent numpy leaf masks of the fixed preset (root, L1, L2 level-wide grids)."""
|
|
||||||
masks, covered = [], None
|
|
||||||
for level in range(DEPTH):
|
|
||||||
size = ROOT_N * 2**level
|
|
||||||
inside = np.ones((size, size), bool) if level == 0 else covered
|
|
||||||
if level < DEPTH - 1:
|
|
||||||
child = np.zeros((size, size), bool)
|
|
||||||
for x0, y0, x1, y1 in REFINEMENTS[level]:
|
|
||||||
child[y0:y1, x0:x1] = True
|
|
||||||
masks.append(inside & ~child)
|
|
||||||
covered = np.kron(child, np.ones((2, 2), bool))
|
|
||||||
else:
|
|
||||||
masks.append(inside)
|
|
||||||
return masks
|
|
||||||
|
|
||||||
|
|
||||||
def assess(base_control, finest_control, refined):
|
|
||||||
"""shear_wave.assess generalized to depth 3 with a 4N finest control.
|
|
||||||
|
|
||||||
Same summaries, screens, budgets and outcome semantics; only the
|
|
||||||
(depth, finest-control) shape contract differs.
|
|
||||||
"""
|
|
||||||
if base_control is None or finest_control is None or not refined:
|
|
||||||
return dict(status="INVALID_CONTROL", reason="missing control or refined run")
|
|
||||||
n = base_control["n"]
|
|
||||||
if (base_control["depth"], finest_control["depth"], finest_control["n"]) != (
|
|
||||||
1, 1, FINEST_FACTOR * n):
|
|
||||||
raise ValueError("controls must be uniform N and uniform 4N")
|
|
||||||
if any(r["depth"] != DEPTH or r["n"] != n for r in refined):
|
|
||||||
raise ValueError("refined runs must be depth 3 at root N")
|
|
||||||
if len({r.get("explosion") for r in refined}) != len(refined):
|
|
||||||
raise ValueError("duplicate refined explosion scheme")
|
|
||||||
sources = {repr(r.get("source")) for r in [base_control, finest_control, *refined]}
|
|
||||||
base, fine = sw._usable(base_control), sw._usable(finest_control)
|
|
||||||
if base is None or fine is None or len(sources) != 1:
|
|
||||||
return dict(status="INVALID_CONTROL", reason="incomplete/invalid control or mixed source")
|
|
||||||
controls_pass = base["own_screens_pass"] and fine["own_screens_pass"]
|
|
||||||
report = dict(base_control=base, finest_control=fine, refined={},
|
|
||||||
controls_pass=controls_pass)
|
|
||||||
statuses = []
|
|
||||||
for r in refined:
|
|
||||||
try:
|
|
||||||
s = sw.summarize(r)
|
|
||||||
except (KeyError, TypeError, ValueError) as error:
|
|
||||||
report["refined"][r["explosion"]] = dict(valid=False, reason=str(error))
|
|
||||||
statuses.append("INVALID_RUN")
|
|
||||||
continue
|
|
||||||
s["valid"] = True
|
|
||||||
s["budget"] = {
|
|
||||||
key: (None if base[key] is None else max(sw.BASELINE_FACTOR * base[key], floor))
|
|
||||||
for key, floor in sw.FLOORS.items()
|
|
||||||
}
|
|
||||||
s["budget_pass"] = all(
|
|
||||||
s[key] is not None and s["budget"][key] is not None and s[key] <= s["budget"][key]
|
|
||||||
for key in sw.FLOORS)
|
|
||||||
s["control_ratios"] = {
|
|
||||||
name: {key: (s[key] / c[key] if s[key] is not None and c[key] else None)
|
|
||||||
for key in (*sw.FLOORS, "spurious_pressure_rms")}
|
|
||||||
for name, c in (("root", base), ("finest", fine))
|
|
||||||
}
|
|
||||||
s["controlled_pass"] = controls_pass and s["own_screens_pass"] and s["budget_pass"]
|
|
||||||
report["refined"][r["explosion"]] = s
|
|
||||||
statuses.append("PASS" if s["controlled_pass"] else "FAIL")
|
|
||||||
if "INVALID_RUN" in statuses:
|
|
||||||
report["status"] = "INVALID_RUN"
|
|
||||||
elif not controls_pass:
|
|
||||||
report["status"] = "CONTROL_SCREEN_FAIL"
|
|
||||||
else:
|
|
||||||
report["status"] = "PASS" if all(v == "PASS" for v in statuses) else "FAIL"
|
|
||||||
return report
|
|
||||||
|
|
||||||
|
|
||||||
def write_snapshot(path, arrays, masks, meta):
|
|
||||||
"""Final macro snapshot: packed ACTIVE leaves (level, i, j), rho, ux, uy only.
|
|
||||||
|
|
||||||
No populations, no restart state, no time series. Returns the receipt block;
|
|
||||||
a file over SNAPSHOT_MAX_BYTES is removed and reported, never kept.
|
|
||||||
"""
|
|
||||||
path = Path(path)
|
|
||||||
if path.exists():
|
|
||||||
raise FileExistsError(path)
|
|
||||||
cols = {k: [] for k in ("level", "i", "j", "rho", "ux", "uy")}
|
|
||||||
counts = []
|
|
||||||
for level, (g, mask) in enumerate(zip(arrays, masks)):
|
|
||||||
if g.shape != (9, *mask.shape):
|
|
||||||
raise ValueError("population/mask shape mismatch")
|
|
||||||
values = g[:, mask].astype(float)
|
|
||||||
rho = values.sum(axis=0)
|
|
||||||
j, i = np.nonzero(mask) # row = y index, column = x index
|
|
||||||
cols["level"].append(np.full(rho.size, level, np.uint8))
|
|
||||||
cols["i"].append(i.astype(np.uint16))
|
|
||||||
cols["j"].append(j.astype(np.uint16))
|
|
||||||
cols["rho"].append(rho)
|
|
||||||
cols["ux"].append(CX @ values / rho)
|
|
||||||
cols["uy"].append(CY @ values / rho)
|
|
||||||
counts.append(int(rho.size))
|
|
||||||
packed = {k: np.concatenate(v) for k, v in cols.items()}
|
|
||||||
if not all(np.isfinite(packed[k]).all() for k in ("rho", "ux", "uy")):
|
|
||||||
raise FloatingPointError("nonfinite snapshot macros")
|
|
||||||
meta = dict(meta, leaf_counts=counts, cell_center="h*(index+0.5), h=2**-level",
|
|
||||||
fields="rho, ux, uy from FP64 physical populations at ACTIVE leaves")
|
|
||||||
with path.open("xb") as handle:
|
|
||||||
np.savez_compressed(handle, meta=np.array(json.dumps(meta, sort_keys=True)), **packed)
|
|
||||||
size = path.stat().st_size
|
|
||||||
block = dict(file=path.name, bytes=size, leaf_counts=counts, max_bytes=SNAPSHOT_MAX_BYTES,
|
|
||||||
step=meta.get("step"), tstar=meta.get("tstar"))
|
|
||||||
if size > SNAPSHOT_MAX_BYTES:
|
|
||||||
path.unlink()
|
|
||||||
return dict(block, kept=False, sha256=None, reason="exceeds snapshot byte cap")
|
|
||||||
block.update(kept=True, sha256=hashlib.sha256(path.read_bytes()).hexdigest())
|
|
||||||
return block
|
|
||||||
|
|
||||||
|
|
||||||
def read_snapshot(path):
|
|
||||||
"""Validate a snapshot: exact keys, no populations, consistent packed arrays."""
|
|
||||||
with np.load(path, allow_pickle=False) as data:
|
|
||||||
if tuple(sorted(data.files)) != tuple(sorted(SNAPSHOT_KEYS)):
|
|
||||||
raise ValueError("unexpected snapshot contents")
|
|
||||||
out = {k: data[k] for k in data.files}
|
|
||||||
meta = json.loads(str(out.pop("meta")))
|
|
||||||
n = len(out["rho"])
|
|
||||||
if any(len(v) != n for v in out.values()) or sum(meta["leaf_counts"]) != n:
|
|
||||||
raise ValueError("inconsistent snapshot arrays")
|
|
||||||
return meta, out
|
|
||||||
|
|
||||||
|
|
||||||
def snapshot_meta(step, viscosity, explosion, source_revision):
|
|
||||||
"""Exact time/phase/config identity stored inside the snapshot."""
|
|
||||||
return dict(step=step, tstar=step * U / ROOT_N, phase=sw.PHI, mode=list(sw.MODE),
|
|
||||||
polarization=sw.E.tolist(), amplitude=U, re_L=sw.RE, n=ROOT_N, depth=DEPTH,
|
|
||||||
explosion=explosion, viscosity=viscosity, tau_by_level=TAU,
|
|
||||||
refinements=REFINEMENTS, source_revision=source_revision,
|
|
||||||
origin="level-wide zero-origin grids", time="root synchronization, postcollision")
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
"""Signed interface-probe traces for the nested/twin shear presets (mechanism diagnostic).
|
|
||||||
|
|
||||||
No pass gates. Native-validated 24-target layout through box 1, in root-128
|
|
||||||
physical coordinates, crossing L0->L1 and (nested only) L1->L2 away from corners:
|
|
||||||
horizontal line y = 56 across the vertical faces x = 16 and x = 22;
|
|
||||||
vertical line x = 38 across the horizontal faces y = 16 and y = 22;
|
|
||||||
normal coordinates NORMAL_COORDS on both (nested: 3 L0 + 5 L1 + 4 L2 each).
|
|
||||||
Each target maps to the nearest ACTIVE leaf center (tie rule: distance, then
|
|
||||||
finer level, then smaller j, then smaller i). Grids differ between cases
|
|
||||||
(U128, U512, nested, twin), so actual centers, levels and displacements are
|
|
||||||
stored and analytic fields are evaluated at the actual centers.
|
|
||||||
Samples are POST-collision G at root synchronization only (root-step units);
|
|
||||||
no substep content is observed, and grid-scale alternation can alias.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
import shear_wave as sw
|
|
||||||
from oracle import CX, CY, U
|
|
||||||
|
|
||||||
ROOT_N = 128
|
|
||||||
NORMAL_COORDS = (12.5, 14.5, 15.5, 16.25, 17.25, 19.25, 20.75, 21.75,
|
|
||||||
22.125, 23.125, 24.125, 26.125)
|
|
||||||
TRANSECTS = {"horizontal_y56": dict(axis=0, other=56.0), # x varies, y fixed
|
|
||||||
"vertical_x38": dict(axis=1, other=38.0)} # y varies, x fixed
|
|
||||||
STEPS = tuple(range(0, 129)) + tuple(range(1920, 2049)) # root steps, both inclusive
|
|
||||||
TRACE_MAX_BYTES = 600_000
|
|
||||||
|
|
||||||
|
|
||||||
def points():
|
|
||||||
out = []
|
|
||||||
for name, t in TRANSECTS.items():
|
|
||||||
for along in NORMAL_COORDS:
|
|
||||||
xy = (along, t["other"]) if t["axis"] == 0 else (t["other"], along)
|
|
||||||
out.append((name, along, xy))
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def case_steps(n):
|
|
||||||
"""Case lattice steps at the fixed root-physical sample times (U512: 4 per root step)."""
|
|
||||||
return [s * (n // ROOT_N) for s in STEPS]
|
|
||||||
|
|
||||||
|
|
||||||
def select(masks, n=ROOT_N):
|
|
||||||
"""Nearest ACTIVE leaf center for each target (deterministic tie rule).
|
|
||||||
|
|
||||||
Targets and stored centers are in root-physical units (root cell = 1); a case
|
|
||||||
with root N = n has cell size h = ROOT_N / (n * 2**level) there (U512: 0.25).
|
|
||||||
"""
|
|
||||||
if n % ROOT_N:
|
|
||||||
raise ValueError("case root N must be a multiple of 128")
|
|
||||||
probes = []
|
|
||||||
for name, along, (x, y) in points():
|
|
||||||
best = None
|
|
||||||
for level, mask in enumerate(masks):
|
|
||||||
h = ROOT_N / (n * 2 ** level)
|
|
||||||
i0, j0 = int(math.floor(x / h)), int(math.floor(y / h))
|
|
||||||
for j in range(j0 - 2, j0 + 3):
|
|
||||||
for i in range(i0 - 2, i0 + 3):
|
|
||||||
if 0 <= j < mask.shape[0] and 0 <= i < mask.shape[1] and mask[j, i]:
|
|
||||||
cx, cy = h * (i + 0.5), h * (j + 0.5)
|
|
||||||
key = (math.hypot(cx - x, cy - y), -level, j, i)
|
|
||||||
if best is None or key < best[0]:
|
|
||||||
best = (key, level, i, j, cx, cy)
|
|
||||||
if best is None:
|
|
||||||
raise ValueError(f"no ACTIVE leaf near target {(x, y)}")
|
|
||||||
_, level, i, j, cx, cy = best
|
|
||||||
probes.append(dict(transect=name, normal=along, point=[x, y], level=level, i=i, j=j,
|
|
||||||
center=[cx, cy], displacement=[cx - x, cy - y]))
|
|
||||||
if len({(p["level"], p["i"], p["j"]) for p in probes}) != len(probes):
|
|
||||||
raise ValueError("probe targets map to duplicate cells")
|
|
||||||
return probes
|
|
||||||
|
|
||||||
|
|
||||||
def sample(arrays, probes):
|
|
||||||
"""Flat [rho, ux, uy] per probe from FP64 physical populations."""
|
|
||||||
row = []
|
|
||||||
for p in probes:
|
|
||||||
f = np.asarray(arrays[p["level"]][:, p["j"], p["i"]], dtype=float)
|
|
||||||
rho = float(f.sum())
|
|
||||||
row += [rho, float(CX @ f) / rho, float(CY @ f) / rho]
|
|
||||||
return row
|
|
||||||
|
|
||||||
|
|
||||||
def new_trace(probes, case_viscosity, case):
|
|
||||||
"""Times/coordinates are root-physical (N=128); nu_root = U*128/Re for all cases."""
|
|
||||||
return dict(schema=1, kind="mechanism diagnostic, no pass gates", case=case,
|
|
||||||
time_unit="root step (N=128)", sample_phase="postcollision G at root synchronization",
|
|
||||||
root_n=ROOT_N, viscosity_root=U * ROOT_N / sw.RE, case_viscosity=case_viscosity,
|
|
||||||
mode=list(sw.MODE), phase=sw.PHI,
|
|
||||||
amplitude=U, fields=["rho", "ux", "uy"], probes=probes, steps=[], values=[])
|
|
||||||
|
|
||||||
|
|
||||||
def residuals(trace):
|
|
||||||
"""Signed (drho, dux, duy) per step and probe, analytic field at each actual center."""
|
|
||||||
out = []
|
|
||||||
for step, row in zip(trace["steps"], trace["values"]):
|
|
||||||
res = []
|
|
||||||
for n, p in enumerate(trace["probes"]):
|
|
||||||
x, y = p["center"]
|
|
||||||
ux, uy, _ = sw.exact(x, y, step, trace["root_n"], trace["viscosity_root"])
|
|
||||||
rho, vx, vy = row[3 * n: 3 * n + 3]
|
|
||||||
res.append((rho - 1.0, vx - float(ux), vy - float(uy)))
|
|
||||||
out.append(res)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def validate(trace):
|
|
||||||
"""Exact schedule, finite values, fixed probe count and byte cap."""
|
|
||||||
import json
|
|
||||||
if trace.get("steps") != list(STEPS) or len(trace["values"]) != len(STEPS):
|
|
||||||
raise ValueError("trace steps differ from the fixed schedule")
|
|
||||||
if len(trace["probes"]) != len(NORMAL_COORDS) * len(TRANSECTS):
|
|
||||||
raise ValueError("unexpected probe count")
|
|
||||||
width = 3 * len(trace["probes"])
|
|
||||||
if any(len(r) != width or not all(math.isfinite(v) for v in r) for r in trace["values"]):
|
|
||||||
raise ValueError("malformed or nonfinite trace row")
|
|
||||||
size = len(json.dumps(trace, allow_nan=False).encode())
|
|
||||||
if size > TRACE_MAX_BYTES:
|
|
||||||
raise ValueError(f"trace exceeds {TRACE_MAX_BYTES} bytes ({size})")
|
|
||||||
return size
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
"""Depth-2 twin of the nested shear preset (opt-in twin_shear_n128).
|
|
||||||
|
|
||||||
Same two L0->L1 boxes as shear_nested, no L2. Four fresh cases: U128, U256
|
|
||||||
(finest control = 2N), constant R128 depth 2, projected R128 depth 2; same
|
|
||||||
wave, phase, Re100/U=0.03, initialization, times, eps(root N), limits, floors.
|
|
||||||
|
|
||||||
Twin vs nested measures the effect of adding this nested L2 refinement.
|
|
||||||
Removing L2 also changes the finest spacing and refined volume, so the
|
|
||||||
contrast does not isolate abstract depth. Physics is the unchanged depth-2,
|
|
||||||
2N-control shear_wave.assess; shear_nested.py stays byte-identical.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
import shear_nested as nested
|
|
||||||
|
|
||||||
ROOT_N = nested.ROOT_N
|
|
||||||
DEPTH = 2
|
|
||||||
FINEST_FACTOR = 2
|
|
||||||
REFINEMENTS = [nested.REFINEMENTS[0]] # L0 -> L1 only, root coordinates
|
|
||||||
CASES = ((100, ROOT_N, 1, "constant"), (100, FINEST_FACTOR * ROOT_N, 1, "constant"),
|
|
||||||
(100, ROOT_N, DEPTH, "constant"), (100, ROOT_N, DEPTH, "projected_linear"))
|
|
||||||
ROOT_STEPS = 16 * ROOT_N
|
|
||||||
LEAVES = [7936, 33792]
|
|
||||||
HOPS = [ROOT_STEPS * 2**level for level in range(DEPTH)] # [2048, 4096]
|
|
||||||
WORK = sum(c * h for c, h in zip(LEAVES, HOPS)) # 154664960 per refined case
|
|
||||||
NU = nested.NU[:DEPTH]
|
|
||||||
TAU = nested.TAU[:DEPTH]
|
|
||||||
|
|
||||||
|
|
||||||
def leaf_masks():
|
|
||||||
"""Independent numpy leaf masks (root complement, full L1 boxes)."""
|
|
||||||
child = np.zeros((ROOT_N, ROOT_N), bool)
|
|
||||||
for x0, y0, x1, y1 in REFINEMENTS[0]:
|
|
||||||
child[y0:y1, x0:x1] = True
|
|
||||||
return [~child, np.kron(child, np.ones((2, 2), bool))]
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
"""CPU-only oblique shear-wave reference and composite-leaf diagnostics.
|
|
||||||
|
|
||||||
Convention: integer mode MODE=(2,1) on the L x L periodic root domain, so the
|
|
||||||
wavevector k = (2*pi/L)*(2,1) points 26.565 deg from +x (mode (1,2) would be
|
|
||||||
63.435 deg). Velocity is polarized along E = (-1,2)/sqrt(5), perpendicular to k.
|
|
||||||
The frozen Taylor-Green oracle is reused for constants and geometry only.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from oracle import CX, CY, W, U, TIMES, centers
|
|
||||||
|
|
||||||
MODE = (2, 1)
|
|
||||||
PHI = 0.0
|
|
||||||
E = np.array([-MODE[1], MODE[0]], dtype=float) / math.hypot(*MODE)
|
|
||||||
ANGLE_DEG = math.degrees(math.atan2(MODE[1], MODE[0]))
|
|
||||||
RE = 100 # Re = U L / nu, the same nu as Taylor-Green Re100 at equal N
|
|
||||||
FIT_FROM_TSTAR = 0.12 # late-time samples only; reduces startup sensitivity
|
|
||||||
INITIALIZATION = (
|
|
||||||
"physical FP64 equilibrium G at actual cell centers; rho=1 (exact p const)"
|
|
||||||
)
|
|
||||||
# Per-row screens. phase_drift_abs, residual_l2 and the run-level rate screen
|
|
||||||
# use eps(n) and are preregistered diagnostic choices, not error bounds.
|
|
||||||
LIMITS = dict(velocity_l2=0.05, spurious_pressure_scaled=0.02, mass_relative=1e-5)
|
|
||||||
# The comparative pressure budget max(2*control, 0.02) is never below the 0.02
|
|
||||||
# absolute ceiling, so it is always redundant; raw control ratios are kept.
|
|
||||||
FLOORS = dict(rate_error=0.002, residual_l2=0.002, spurious_pressure_scaled=0.02)
|
|
||||||
BASELINE_FACTOR = 2.0
|
|
||||||
|
|
||||||
|
|
||||||
def wavevector(length):
|
|
||||||
return 2 * np.pi / length * np.array(MODE, dtype=float)
|
|
||||||
|
|
||||||
|
|
||||||
def decay_rate(length, viscosity):
|
|
||||||
"""Exact amplitude decay rate per root step: nu |k|^2."""
|
|
||||||
k = wavevector(length)
|
|
||||||
return viscosity * float(k @ k)
|
|
||||||
|
|
||||||
|
|
||||||
def eps(n):
|
|
||||||
"""(|k| h_root)^2 with h_root = 1: dimensionless, coefficient one."""
|
|
||||||
k = wavevector(n)
|
|
||||||
return float(k @ k)
|
|
||||||
|
|
||||||
|
|
||||||
def exact(x, y, t, length, viscosity, amplitude=U):
|
|
||||||
k = wavevector(length)
|
|
||||||
a = amplitude * np.exp(-decay_rate(length, viscosity) * t)
|
|
||||||
s = np.sin(k[0] * x + k[1] * y + PHI)
|
|
||||||
p = np.zeros_like(s)
|
|
||||||
return a * E[0] * s, a * E[1] * s, p # kinematic pressure, constant
|
|
||||||
|
|
||||||
|
|
||||||
def equilibrium(rho, ux, uy):
|
|
||||||
# Same second-order D2Q9 equilibrium as oracle.initial.
|
|
||||||
cu = CX[:, None, None] * ux + CY[:, None, None] * uy
|
|
||||||
return (
|
|
||||||
W[:, None, None]
|
|
||||||
* rho
|
|
||||||
* (1 + 3 * cu + 4.5 * cu * cu - 1.5 * (ux * ux + uy * uy))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def initial(shape, level, length, viscosity):
|
|
||||||
x, y = centers(shape, level)
|
|
||||||
ux, uy, p = exact(x, y, 0.0, length, viscosity)
|
|
||||||
return equilibrium(1 + 3 * p, ux, uy)
|
|
||||||
|
|
||||||
|
|
||||||
def _leaves(arrays, masks):
|
|
||||||
accum = []
|
|
||||||
for level, (g, mask) in enumerate(zip(arrays, masks)):
|
|
||||||
if g.shape != (9, *mask.shape):
|
|
||||||
raise ValueError("population/mask shape mismatch")
|
|
||||||
values = g[:, mask].astype(float)
|
|
||||||
if not np.isfinite(values).all():
|
|
||||||
raise FloatingPointError("nonfinite leaf populations")
|
|
||||||
rho = values.sum(axis=0)
|
|
||||||
if (rho <= 0).any():
|
|
||||||
raise FloatingPointError("nonpositive leaf density")
|
|
||||||
x, y = centers(mask.shape, level)
|
|
||||||
area = np.full(rho.size, 0.25**level)
|
|
||||||
lev = np.full(rho.size, level)
|
|
||||||
accum.append((rho, CX @ values / rho, CY @ values / rho, x[mask], y[mask], area, lev))
|
|
||||||
return [np.concatenate(v) for v in zip(*accum)]
|
|
||||||
|
|
||||||
|
|
||||||
def metrics(arrays, masks, t, length, viscosity, initial_mass=None):
|
|
||||||
rho, ux, uy, x, y, w, lev = _leaves(arrays, masks)
|
|
||||||
volume = w.sum()
|
|
||||||
if not np.isclose(volume, length**2, rtol=0, atol=1e-10):
|
|
||||||
raise ValueError("leaf partition does not cover domain once")
|
|
||||||
k = wavevector(length)
|
|
||||||
theta = k[0] * x + k[1] * y + PHI
|
|
||||||
s, c = np.sin(theta), np.cos(theta)
|
|
||||||
v = ux * E[0] + uy * E[1]
|
|
||||||
# Weighted least squares on the actual composite quadrature (no
|
|
||||||
# continuous-orthogonality assumption).
|
|
||||||
gram = np.array([[w @ (s * s), w @ (s * c)], [w @ (s * c), w @ (c * c)]])
|
|
||||||
a_hat, b_hat = np.linalg.solve(gram, [w @ (v * s), w @ (v * c)])
|
|
||||||
ex, ey, _ = exact(x, y, t, length, viscosity)
|
|
||||||
exact_norm = w @ (ex * ex + ey * ey)
|
|
||||||
fit = a_hat * s + b_hat * c
|
|
||||||
longitudinal = (ux * k[0] + uy * k[1]) / math.sqrt(k @ k)
|
|
||||||
amplitude = U * math.exp(-decay_rate(length, viscosity) * t)
|
|
||||||
scale = amplitude * amplitude / 4 # continuum reference scale, not the TG norm
|
|
||||||
modal = math.hypot(a_hat, b_hat)
|
|
||||||
p = (rho - 1) / 3
|
|
||||||
p = p - w @ p / volume # one global gauge
|
|
||||||
mass = float(w @ rho)
|
|
||||||
return dict(
|
|
||||||
tstar=t * U / length,
|
|
||||||
mass=mass,
|
|
||||||
mass_relative=(
|
|
||||||
0.0 if initial_mass is None else abs(mass - initial_mass) / initial_mass
|
|
||||||
),
|
|
||||||
a_hat=float(a_hat),
|
|
||||||
b_hat=float(b_hat),
|
|
||||||
amplitude_exact=amplitude,
|
|
||||||
amplitude_ratio=float(a_hat / amplitude),
|
|
||||||
modal_amplitude=modal,
|
|
||||||
amplitude_error=modal / amplitude - 1,
|
|
||||||
phase_drift=float(math.atan2(b_hat, a_hat)),
|
|
||||||
phase_drift_abs=abs(float(math.atan2(b_hat, a_hat))),
|
|
||||||
velocity_l2=float(
|
|
||||||
np.sqrt(w @ ((ux - ex) ** 2 + (uy - ey) ** 2) / exact_norm)
|
|
||||||
),
|
|
||||||
residual_l2=float(
|
|
||||||
np.sqrt(w @ ((ux - fit * E[0]) ** 2 + (uy - fit * E[1]) ** 2) / exact_norm)
|
|
||||||
),
|
|
||||||
longitudinal_l2=float(np.sqrt(w @ (longitudinal**2) / exact_norm)),
|
|
||||||
spurious_pressure_rms=float(np.sqrt(w @ (p * p) / volume)),
|
|
||||||
spurious_pressure_scaled=float(np.sqrt(w @ (p * p) / volume) / scale),
|
|
||||||
spurious_pressure_max_scaled=float(np.max(np.abs(p)) / scale),
|
|
||||||
spurious_pressure_scaled_by_level=[
|
|
||||||
float(np.sqrt(w[lev == i] @ (p[lev == i] ** 2) / w[lev == i].sum()) / scale)
|
|
||||||
for i in range(len(masks))
|
|
||||||
],
|
|
||||||
rho_min=float(rho.min()),
|
|
||||||
rho_max=float(rho.max()),
|
|
||||||
max_speed=float(np.max(np.hypot(ux, uy))),
|
|
||||||
leaf_cells=int(len(rho)),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def fit_rate(rows, length, viscosity):
|
|
||||||
"""Free-intercept least squares of ln R, R = hypot(a_hat, b_hat), late samples.
|
|
||||||
|
|
||||||
The intercept absorbs only a constant amplitude prefactor once a single
|
|
||||||
exponential dominates; per-interval rates and the fit residual are kept so
|
|
||||||
a lingering transient stays visible. Rates are per root step of the record
|
|
||||||
(uniform 2N uses its own nu and doubled step counts).
|
|
||||||
"""
|
|
||||||
late = [r for r in rows if r["tstar"] >= FIT_FROM_TSTAR - 1e-12]
|
|
||||||
amp = np.array([math.hypot(r["a_hat"], r["b_hat"]) for r in late])
|
|
||||||
if len(late) < 3 or not np.isfinite(amp).all():
|
|
||||||
raise ValueError("need >= 3 late samples with finite R")
|
|
||||||
exact_rate = decay_rate(length, viscosity)
|
|
||||||
to_tstar = length / U # root steps per unit t*
|
|
||||||
if not (amp > 0).all():
|
|
||||||
# Finite modal extinction: a physical failure, not malformed data. No
|
|
||||||
# clipping and no invented rate; fit quantities are unavailable.
|
|
||||||
return dict(rate_fit_valid=False, rate=None, exact_rate=exact_rate,
|
|
||||||
rate_tstar=None, exact_rate_tstar=exact_rate * to_tstar,
|
|
||||||
rate_error=None, interval_rate_errors=None, log_fit_residual_max=None)
|
|
||||||
t = np.array([r["step"] for r in late], dtype=float)
|
|
||||||
(slope, intercept) = np.polyfit(t, np.log(amp), 1)
|
|
||||||
residual = np.log(amp) - (slope * t + intercept)
|
|
||||||
return dict(
|
|
||||||
rate_fit_valid=True, rate=float(-slope), exact_rate=exact_rate,
|
|
||||||
rate_tstar=float(-slope * to_tstar),
|
|
||||||
exact_rate_tstar=exact_rate * to_tstar, # = 5 (2 pi)^2 / Re
|
|
||||||
rate_error=abs(-slope / exact_rate - 1),
|
|
||||||
interval_rate_errors=[
|
|
||||||
float(-np.log(amp[i + 1] / amp[i]) / (t[i + 1] - t[i]) / exact_rate - 1)
|
|
||||||
for i in range(len(t) - 1)
|
|
||||||
],
|
|
||||||
log_fit_residual_max=float(np.max(np.abs(residual))),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
NONNEGATIVE = ("tstar", "mass", "mass_relative", "amplitude_exact", "modal_amplitude",
|
|
||||||
"phase_drift_abs", "velocity_l2", "residual_l2", "longitudinal_l2",
|
|
||||||
"spurious_pressure_rms", "spurious_pressure_scaled",
|
|
||||||
"spurious_pressure_max_scaled", "rho_min", "rho_max", "max_speed")
|
|
||||||
SIGNED = ("a_hat", "b_hat", "amplitude_ratio", "amplitude_error", "phase_drift")
|
|
||||||
|
|
||||||
|
|
||||||
def _number(value):
|
|
||||||
return (isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
||||||
and math.isfinite(value))
|
|
||||||
|
|
||||||
|
|
||||||
def validate_rows(record):
|
|
||||||
"""Exact schedule, finite numbers (incl. lists) and nonnegative error fields.
|
|
||||||
|
|
||||||
Signed quantities (amplitudes, amplitude/phase errors) keep their sign.
|
|
||||||
Raises ValueError so callers can report an invalid run before any screen.
|
|
||||||
"""
|
|
||||||
n, rows = record["n"], record["rows"]
|
|
||||||
steps = [round(tstar * n / U) for tstar in TIMES]
|
|
||||||
if not isinstance(rows, list) or len(rows) != len(steps):
|
|
||||||
raise ValueError("rows do not match the scheduled samples")
|
|
||||||
for row, step in zip(rows, steps):
|
|
||||||
if type(row.get("step")) is not int or row["step"] != step:
|
|
||||||
raise ValueError(f"unscheduled or duplicate step {row.get('step')!r}")
|
|
||||||
if not _number(row.get("tstar")) or abs(row["tstar"] - step * U / n) > 3e-17:
|
|
||||||
raise ValueError(f"wrong sample time at step {step}")
|
|
||||||
if not all(_number(row.get(key)) for key in (*NONNEGATIVE, *SIGNED)):
|
|
||||||
raise ValueError(f"missing or nonfinite diagnostic at step {step}")
|
|
||||||
if any(row[key] < 0 for key in NONNEGATIVE) or row["rho_min"] <= 0:
|
|
||||||
raise ValueError(f"negative error quantity or density at step {step}")
|
|
||||||
levels = row.get("spurious_pressure_scaled_by_level")
|
|
||||||
if (not isinstance(levels, list) or len(levels) != record["depth"]
|
|
||||||
or not all(_number(v) and v >= 0 for v in levels)):
|
|
||||||
raise ValueError(f"invalid per-level pressure at step {step}")
|
|
||||||
|
|
||||||
|
|
||||||
def summarize(record):
|
|
||||||
validate_rows(record)
|
|
||||||
rows = record["rows"]
|
|
||||||
n = record["n"]
|
|
||||||
out = fit_rate(rows, n, record["viscosity"])
|
|
||||||
out.update(
|
|
||||||
eps=eps(n),
|
|
||||||
complete=record["completed"] is True,
|
|
||||||
residual_l2=max(r["residual_l2"] for r in rows),
|
|
||||||
phase_drift_abs=max(r["phase_drift_abs"] for r in rows),
|
|
||||||
spurious_pressure_scaled=max(r["spurious_pressure_scaled"] for r in rows),
|
|
||||||
spurious_pressure_rms=max(r["spurious_pressure_rms"] for r in rows),
|
|
||||||
amplitude_error_abs=max(abs(r["amplitude_error"]) for r in rows),
|
|
||||||
velocity_l2=max(r["velocity_l2"] for r in rows),
|
|
||||||
mass_relative=max(r["mass_relative"] for r in rows),
|
|
||||||
)
|
|
||||||
if out["rate_fit_valid"] and not all(
|
|
||||||
_number(v) for v in (out["rate"], out["rate_error"], *out["interval_rate_errors"],
|
|
||||||
out["log_fit_residual_max"])):
|
|
||||||
raise ValueError("nonfinite rate fit")
|
|
||||||
out["own_screens_pass"] = out["complete"] and out["rate_fit_valid"] and all(
|
|
||||||
out[key] <= out["eps"] for key in ("rate_error", "phase_drift_abs", "residual_l2")
|
|
||||||
) and all(out[key] <= limit for key, limit in LIMITS.items())
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _usable(record):
|
|
||||||
try:
|
|
||||||
out = summarize(record)
|
|
||||||
except (KeyError, TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
return out if out["complete"] else None
|
|
||||||
|
|
||||||
|
|
||||||
def assess(base_control, fine_control, refined):
|
|
||||||
"""Uniform root-spacing and finest-spacing controls versus refined runs.
|
|
||||||
|
|
||||||
INVALID_CONTROL: missing, incomplete, invalid-row or mixed-source controls.
|
|
||||||
INVALID_RUN: a refined record has invalid rows (schedule, NaN, sign).
|
|
||||||
CONTROL_SCREEN_FAIL: complete valid controls that fail their own screens;
|
|
||||||
refined own-screen results are kept, but no controlled comparison passes.
|
|
||||||
PASS/FAIL: every refined run's controlled_pass (own screens and budgets).
|
|
||||||
"""
|
|
||||||
if base_control is None or fine_control is None or not refined:
|
|
||||||
return dict(status="INVALID_CONTROL", reason="missing control or refined run")
|
|
||||||
n = base_control["n"]
|
|
||||||
if (base_control["depth"], fine_control["depth"], fine_control["n"]) != (1, 1, 2 * n):
|
|
||||||
raise ValueError("controls must be uniform N and uniform 2N")
|
|
||||||
if any(r["depth"] != 2 or r["n"] != n for r in refined):
|
|
||||||
raise ValueError("refined runs must be depth 2 at root N")
|
|
||||||
if len({r.get("explosion") for r in refined}) != len(refined):
|
|
||||||
raise ValueError("duplicate refined explosion scheme")
|
|
||||||
sources = {repr(r.get("source")) for r in [base_control, fine_control, *refined]}
|
|
||||||
base, fine = _usable(base_control), _usable(fine_control)
|
|
||||||
if base is None or fine is None or len(sources) != 1:
|
|
||||||
return dict(status="INVALID_CONTROL",
|
|
||||||
reason="incomplete/invalid control or mixed source")
|
|
||||||
controls_pass = base["own_screens_pass"] and fine["own_screens_pass"]
|
|
||||||
report = dict(base_control=base, fine_control=fine, refined={},
|
|
||||||
controls_pass=controls_pass)
|
|
||||||
statuses = []
|
|
||||||
for r in refined:
|
|
||||||
try:
|
|
||||||
s = summarize(r)
|
|
||||||
except (KeyError, TypeError, ValueError) as error:
|
|
||||||
report["refined"][r["explosion"]] = dict(valid=False, reason=str(error))
|
|
||||||
statuses.append("INVALID_RUN")
|
|
||||||
continue
|
|
||||||
s["valid"] = True
|
|
||||||
# rate_error is None when a modal fit is unavailable; never a pass.
|
|
||||||
s["budget"] = {
|
|
||||||
key: (None if base[key] is None else max(BASELINE_FACTOR * base[key], floor))
|
|
||||||
for key, floor in FLOORS.items()
|
|
||||||
}
|
|
||||||
s["budget_pass"] = all(
|
|
||||||
s[key] is not None and s["budget"][key] is not None and s[key] <= s["budget"][key]
|
|
||||||
for key in FLOORS)
|
|
||||||
s["control_ratios"] = {
|
|
||||||
name: {key: (s[key] / c[key] if s[key] is not None and c[key] else None)
|
|
||||||
for key in (*FLOORS, "spurious_pressure_rms")}
|
|
||||||
for name, c in (("root", base), ("finest", fine))
|
|
||||||
}
|
|
||||||
s["controlled_pass"] = controls_pass and s["own_screens_pass"] and s["budget_pass"]
|
|
||||||
report["refined"][r["explosion"]] = s
|
|
||||||
statuses.append("PASS" if s["controlled_pass"] else "FAIL")
|
|
||||||
if "INVALID_RUN" in statuses:
|
|
||||||
report["status"] = "INVALID_RUN"
|
|
||||||
elif not controls_pass:
|
|
||||||
report["status"] = "CONTROL_SCREEN_FAIL"
|
|
||||||
else:
|
|
||||||
report["status"] = "PASS" if all(v == "PASS" for v in statuses) else "FAIL"
|
|
||||||
return report
|
|
||||||
@@ -17,7 +17,6 @@ from unittest.mock import patch
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
import run_case
|
import run_case
|
||||||
import assess_projected_pairs as assessor
|
|
||||||
from assess_projected_pairs import (BUILD_SOURCES, CASES, NUMERICS, NUMERICAL_REVISION,
|
from assess_projected_pairs import (BUILD_SOURCES, CASES, NUMERICS, NUMERICAL_REVISION,
|
||||||
SOURCE_SHA256, assess)
|
SOURCE_SHA256, assess)
|
||||||
from oracle import LIMITS, TIMES, U, rectangles
|
from oracle import LIMITS, TIMES, U, rectangles
|
||||||
@@ -32,7 +31,7 @@ RUNTIME = dict(device=dict(name="host mock", pci_bus_id="mock", capability=[7, 0
|
|||||||
|
|
||||||
def source():
|
def source():
|
||||||
return dict(revision=MOCK_REVISION, dirty=False, source_tree_sha256=SOURCE_SHA256,
|
return dict(revision=MOCK_REVISION, dirty=False, source_tree_sha256=SOURCE_SHA256,
|
||||||
harness_sha256={"run_case.py": "a" * 64, "oracle.py": "c" * 64},
|
harness_sha256={"run_case.py": "a" * 64},
|
||||||
build_capture_sha256=run_case.BUILD_CAPTURE_SHA256)
|
build_capture_sha256=run_case.BUILD_CAPTURE_SHA256)
|
||||||
|
|
||||||
|
|
||||||
@@ -43,14 +42,12 @@ def build_metadata(size):
|
|||||||
arch="sm_70", flags=["--use_fast_math"], ptx_sha256="b" * 64)
|
arch="sm_70", flags=["--use_fast_math"], ptx_sha256="b" * 64)
|
||||||
|
|
||||||
|
|
||||||
def records(early=False, diagnostic=None):
|
def records(early=False):
|
||||||
from CelerisLab.lbm.hierarchy import build_hierarchy
|
from CelerisLab.lbm.hierarchy import build_hierarchy
|
||||||
from CelerisLab.lbm.refinement import ACTIVE
|
from CelerisLab.lbm.refinement import ACTIVE
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
for re, n, depth, scheme in CASES:
|
for re, n, depth, scheme in CASES:
|
||||||
if diagnostic:
|
|
||||||
n *= 2
|
|
||||||
cfg = run_case.config(n, re)
|
cfg = run_case.config(n, re)
|
||||||
refinements = [] if depth == 1 else rectangles(n, depth)
|
refinements = [] if depth == 1 else rectangles(n, depth)
|
||||||
h = build_hierarchy(cfg, refinements)
|
h = build_hierarchy(cfg, refinements)
|
||||||
@@ -69,8 +66,6 @@ def records(early=False, diagnostic=None):
|
|||||||
early_pressure=dict(enabled=early, rows=[
|
early_pressure=dict(enabled=early, rows=[
|
||||||
dict(step=s, tstar=s*U/n, pressure_l2=.01 if s not in main_steps else 0.0) for s in early_steps],
|
dict(step=s, tstar=s*U/n, pressure_l2=.01 if s not in main_steps else 0.0) for s in early_steps],
|
||||||
sampled_max_pressure_l2=.01 if early else None)))
|
sampled_max_pressure_l2=.01 if early else None)))
|
||||||
if diagnostic:
|
|
||||||
out[-1]["diagnostic"] = diagnostic
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -256,7 +251,7 @@ class ProjectedPairsTests(unittest.TestCase):
|
|||||||
def test_pinned_build_sources_match_host_generated_inputs(self):
|
def test_pinned_build_sources_match_host_generated_inputs(self):
|
||||||
from CelerisLab.cuda import compiler_v2
|
from CelerisLab.cuda import compiler_v2
|
||||||
|
|
||||||
for size in (128, 256, 512):
|
for size in (128, 256):
|
||||||
cfg = replace(run_case.config(size, 100), nx=10, ny=10)
|
cfg = replace(run_case.config(size, 100), nx=10, ny=10)
|
||||||
cfg.validate()
|
cfg.validate()
|
||||||
root = compiler_v2.generate_config(cfg)
|
root = compiler_v2.generate_config(cfg)
|
||||||
@@ -268,111 +263,6 @@ class ProjectedPairsTests(unittest.TestCase):
|
|||||||
finally:
|
finally:
|
||||||
compiler_v2.cleanup_build_dir(root)
|
compiler_v2.cleanup_build_dir(root)
|
||||||
|
|
||||||
def test_scaled_geometry_work_and_frozen_gates(self):
|
|
||||||
data = records(True, run_case.SCALED_DIAGNOSTIC)
|
|
||||||
self.assertEqual(sum(r["workload"]["active_collision_updates"] for r in data), 4714397696)
|
|
||||||
self.assertEqual([r["viscosity"] for r in data], [.0768, .1536, .0768, .0768])
|
|
||||||
self.assertEqual(data[-1]["tau_by_level"], [.7303999999999999, .9608])
|
|
||||||
for r in data:
|
|
||||||
self.assertEqual([row["tstar"] for row in r["rows"]], TIMES)
|
|
||||||
self.assertEqual(len(r["early_pressure"]["rows"]), 65)
|
|
||||||
if r["depth"] == 2:
|
|
||||||
self.assertEqual(r["refinements"], [[(32, 32, 224, 192)]])
|
|
||||||
self.assertEqual(r["workload"]["active_cells_by_level"], [34816, 122880])
|
|
||||||
self.assertEqual(r["workload"]["level_hops"], [4096, 8192])
|
|
||||||
result = assess(data, MOCK_REVISION, diagnostic=run_case.SCALED_DIAGNOSTIC)
|
|
||||||
self.assertTrue(result["passed"])
|
|
||||||
self.assertEqual(result["floors"], dict(velocity_l2=.002, pressure_l2=.02, energy_relative=.004))
|
|
||||||
self.assertTrue(all(row["floor_dominates"] for row in result["comparisons"]))
|
|
||||||
data[-1]["rows"][-1]["velocity_l2"] = .00200001
|
|
||||||
self.assertFalse(assess(data, diagnostic=run_case.SCALED_DIAGNOSTIC)["passed"])
|
|
||||||
self.assertFalse(assess(data)["passed"])
|
|
||||||
data[-1]["rows"][-1]["velocity_l2"] = 0.0
|
|
||||||
for mutate in (lambda r: r.pop("diagnostic"), lambda r: r.update(diagnostic="wrong"),
|
|
||||||
lambda r: r["source"].update(revision="wrong"),
|
|
||||||
lambda r: r["early_pressure"].update(enabled=False),
|
|
||||||
lambda r: r.update(refinements=[[(32, 32, 224, 200)]])):
|
|
||||||
bad = deepcopy(data)
|
|
||||||
mutate(bad[-1])
|
|
||||||
self.assertFalse(assess(bad, MOCK_REVISION, diagnostic=run_case.SCALED_DIAGNOSTIC)["passed"])
|
|
||||||
|
|
||||||
def test_raw_resolution_changes_and_reference_identity(self):
|
|
||||||
old, new = records(True), records(True, run_case.SCALED_DIAGNOSTIC)
|
|
||||||
for r in old:
|
|
||||||
r["source"]["revision"] = assessor.REFERENCE_REVISION
|
|
||||||
old[-2]["rows"][-1]["velocity_l2"] = .004 # historical constant failure remains usable
|
|
||||||
old[-1]["rows"][-1]["velocity_l2"] = .001
|
|
||||||
new[-1]["rows"][-1]["velocity_l2"] = .0015 # floor pass, but worse raw error
|
|
||||||
result = assessor.resolution_trend(new, old, MOCK_REVISION)
|
|
||||||
self.assertTrue(result["valid"])
|
|
||||||
self.assertTrue(result["trend_interpretation_ready"])
|
|
||||||
self.assertFalse(result["reference_scheme_results"]["constant"]["passed"])
|
|
||||||
self.assertEqual((len(result["per_time"]), len(result["worst_post_initial"]),
|
|
||||||
len(result["uniform256_bridge"])), (80, 16, 20))
|
|
||||||
row = next(r for r in result["per_time"] if r["role"] == "projected_linear"
|
|
||||||
and r["metric"] == "velocity_l2" and r["tstar"] == .48)
|
|
||||||
self.assertEqual((row["signed_change"], row["new_to_old"], row["change_percent"]), (.0005, 1.5, 50.))
|
|
||||||
self.assertTrue(assess(new, diagnostic=run_case.SCALED_DIAGNOSTIC)["passed"])
|
|
||||||
self.assertIsNone(result["per_time"][0]["new_to_old"]) # zero denominator is explicit
|
|
||||||
self.assertEqual(result["per_time"][0]["ratio_unavailable_reason"], "zero historical error")
|
|
||||||
new[-1]["rows"][-1]["velocity_l2"] = .06 # retain raw errors even when absolute screen fails
|
|
||||||
self.assertTrue(assessor.resolution_trend(new, old, MOCK_REVISION)["valid"])
|
|
||||||
self.assertFalse(assess(new, diagnostic=run_case.SCALED_DIAGNOSTIC)["passed"])
|
|
||||||
new[-1]["rows"][-1]["velocity_l2"] = .0015
|
|
||||||
for sample in ("rows", "early_pressure"):
|
|
||||||
bad = deepcopy(new)
|
|
||||||
if sample == "rows":
|
|
||||||
bad[0]["rows"][-1]["velocity_l2"] = .0001
|
|
||||||
else:
|
|
||||||
bad[0]["early_pressure"]["rows"][1]["pressure_l2"] = .009
|
|
||||||
result = assessor.resolution_trend(bad, old, MOCK_REVISION)
|
|
||||||
self.assertTrue(result["valid"])
|
|
||||||
self.assertFalse(result["trend_interpretation_ready"])
|
|
||||||
for mutate in (lambda r: r["source"].update(revision=MOCK_REVISION),
|
|
||||||
lambda r: r["source"].update(source_tree_sha256="wrong"),
|
|
||||||
lambda r: r["source"]["harness_sha256"].update(**{"oracle.py": "wrong"}),
|
|
||||||
lambda r: r.update(diagnostic=run_case.SCALED_DIAGNOSTIC),
|
|
||||||
lambda r: r.update(refinements=[]), lambda r: r["rows"].pop()):
|
|
||||||
bad = deepcopy(old)
|
|
||||||
mutate(bad[-1])
|
|
||||||
self.assertFalse(assessor.resolution_trend(new, bad, MOCK_REVISION)["valid"])
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
|
||||||
paths = [Path(tmp) / f"{i}.json" for i in range(4)]
|
|
||||||
for path, receipt in zip(paths, old):
|
|
||||||
path.write_text(json.dumps(receipt))
|
|
||||||
with self.assertRaisesRegex(ValueError, "receipt hashes"):
|
|
||||||
assessor.load_reference(paths)
|
|
||||||
witnesses = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in paths}
|
|
||||||
with patch.object(assessor, "REFERENCE_RECEIPTS", witnesses):
|
|
||||||
self.assertEqual(assessor.load_reference(paths), json.loads(json.dumps(old)))
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
assessor.load_reference(paths[:-1] + paths[:1])
|
|
||||||
paths[-1].write_text(paths[-1].read_text() + " ")
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
assessor.load_reference(paths)
|
|
||||||
|
|
||||||
def test_scaled_runner_requires_explicit_bounded_diagnostic(self):
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
|
||||||
base = ["run_case.py", "--repo", str(REPO), "--expected-revision", MOCK_REVISION,
|
|
||||||
"--output", str(Path(tmp) / "case.json"), "--dry-run"]
|
|
||||||
diagnostic = ["--diagnostic", run_case.SCALED_DIAGNOSTIC, "--early-pressure"]
|
|
||||||
for n, depth, re, extra, valid in (
|
|
||||||
(512, 1, 100, [], False), (512, 1, 100, diagnostic, True),
|
|
||||||
(256, 2, 100, diagnostic, True), (128, 2, 100, diagnostic, False),
|
|
||||||
(256, 3, 100, diagnostic, False), (256, 2, 1000, diagnostic, False),
|
|
||||||
(256, 2, 100, diagnostic[:2], False), (512, 2, 100, diagnostic, False)):
|
|
||||||
argv = base + ["--n", str(n), "--depth", str(depth), "--re", str(re)] + extra
|
|
||||||
with self.subTest(argv=argv), patch.object(sys, "argv", argv), \
|
|
||||||
patch.object(run_case, "verify_source", return_value=source()) as verify, \
|
|
||||||
redirect_stdout(io.StringIO()), patch("sys.stderr", io.StringIO()):
|
|
||||||
if valid:
|
|
||||||
self.assertEqual(run_case.main(), 0)
|
|
||||||
else:
|
|
||||||
with self.assertRaises(SystemExit) as caught:
|
|
||||||
run_case.main()
|
|
||||||
self.assertEqual(caught.exception.code, 2)
|
|
||||||
verify.assert_not_called()
|
|
||||||
|
|
||||||
def test_source_preflight_requires_clean_exact_revision_and_tree(self):
|
def test_source_preflight_requires_clean_exact_revision_and_tree(self):
|
||||||
import oracle
|
import oracle
|
||||||
from tests.validation.multigrid import check_projected_pairs as gate
|
from tests.validation.multigrid import check_projected_pairs as gate
|
||||||
@@ -429,7 +319,7 @@ class ProjectedPairsTests(unittest.TestCase):
|
|||||||
patch.object(run_case, "runtime_identity", side_effect=AssertionError("CUDA reached")), \
|
patch.object(run_case, "runtime_identity", side_effect=AssertionError("CUDA reached")), \
|
||||||
redirect_stdout(stdout):
|
redirect_stdout(stdout):
|
||||||
self.assertEqual(run_case.main(), 0)
|
self.assertEqual(run_case.main(), 0)
|
||||||
verify.assert_called_once_with(REPO, MOCK_REVISION, "taylor_green")
|
verify.assert_called_once_with(REPO, MOCK_REVISION)
|
||||||
self.assertTrue(json.loads(stdout.getvalue())["dry_run"])
|
self.assertTrue(json.loads(stdout.getvalue())["dry_run"])
|
||||||
self.assertFalse(output.exists())
|
self.assertFalse(output.exists())
|
||||||
|
|
||||||
@@ -459,15 +349,9 @@ class ProjectedPairsTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_mocked_runner_default_early_and_failure(self):
|
def test_mocked_runner_default_early_and_failure(self):
|
||||||
# Replace the GPU context and steppers before import. No CUDA module or flow is used.
|
# Replace the GPU context and steppers before import. No CUDA module or flow is used.
|
||||||
cases = [(early, depth, fail, 128, None, "projected_linear" if early else "constant")
|
for early, depth, fail in ((False, 1, None), (False, 2, None), (True, 2, None),
|
||||||
for early, depth, fail in
|
|
||||||
((False, 1, None), (False, 2, None), (True, 2, None),
|
|
||||||
(True, 2, "resource"), (True, 2, "nonfinite"),
|
(True, 2, "resource"), (True, 2, "nonfinite"),
|
||||||
(True, 2, "probe_nonfinite"))]
|
(True, 2, "probe_nonfinite")):
|
||||||
cases += [(True, depth, None, n, run_case.SCALED_DIAGNOSTIC, scheme)
|
|
||||||
for n, depth, scheme in ((256, 1, "constant"), (512, 1, "constant"),
|
|
||||||
(256, 2, "constant"), (256, 2, "projected_linear"))]
|
|
||||||
for early, depth, fail, n, diagnostic, scheme in cases:
|
|
||||||
calls, selectors = [], []
|
calls, selectors = [], []
|
||||||
|
|
||||||
class FakeRun:
|
class FakeRun:
|
||||||
@@ -511,14 +395,11 @@ class ProjectedPairsTests(unittest.TestCase):
|
|||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
output = Path(tmp) / "case.json"
|
output = Path(tmp) / "case.json"
|
||||||
argv = ["run_case.py", "--repo", str(REPO), "--re", "100", "--n", str(n),
|
argv = ["run_case.py", "--repo", str(REPO), "--re", "100", "--n", "128",
|
||||||
"--depth", str(depth), "--output", str(output),
|
"--depth", str(depth), "--output", str(output),
|
||||||
"--expected-revision", MOCK_REVISION, "--run-gpu"]
|
"--expected-revision", MOCK_REVISION, "--run-gpu"]
|
||||||
argv += ["--explosion", scheme]
|
|
||||||
if early:
|
if early:
|
||||||
argv += ["--early-pressure"]
|
argv += ["--early-pressure", "--explosion", "projected_linear"]
|
||||||
if diagnostic:
|
|
||||||
argv += ["--diagnostic", diagnostic]
|
|
||||||
with patch.dict(sys.modules, modules), patch.object(sys, "argv", argv), \
|
with patch.dict(sys.modules, modules), patch.object(sys, "argv", argv), \
|
||||||
patch.dict("os.environ", CUDA_VISIBLE_DEVICES="mock"), \
|
patch.dict("os.environ", CUDA_VISIBLE_DEVICES="mock"), \
|
||||||
patch.object(run_case, "verify_source", return_value=source()) as verify, \
|
patch.object(run_case, "verify_source", return_value=source()) as verify, \
|
||||||
@@ -531,17 +412,16 @@ class ProjectedPairsTests(unittest.TestCase):
|
|||||||
else:
|
else:
|
||||||
self.assertEqual(run_case.main(), 0)
|
self.assertEqual(run_case.main(), 0)
|
||||||
receipt = json.loads(output.read_text())
|
receipt = json.loads(output.read_text())
|
||||||
verify.assert_called_once_with(REPO, MOCK_REVISION, "taylor_green")
|
verify.assert_called_once_with(REPO, MOCK_REVISION)
|
||||||
self.assertEqual(selectors, [scheme] if depth == 2 else [])
|
self.assertEqual(selectors, ["projected_linear" if early else "constant"] if depth == 2 else [])
|
||||||
self.assertEqual(receipt["source"], source())
|
self.assertEqual(receipt["source"], source())
|
||||||
self.assertEqual(receipt["runtime_identity"], RUNTIME)
|
self.assertEqual(receipt["runtime_identity"], RUNTIME)
|
||||||
self.assertEqual(receipt["completed"], not fail)
|
self.assertEqual(receipt["completed"], not fail)
|
||||||
self.assertEqual(receipt.get("diagnostic"), diagnostic)
|
|
||||||
self.assertEqual(calls[:2], [0, 1]) # READY probe is part of this trajectory
|
self.assertEqual(calls[:2], [0, 1]) # READY probe is part of this trajectory
|
||||||
if not fail:
|
if not fail:
|
||||||
self.assertEqual([r["tstar"] for r in receipt["rows"]], TIMES)
|
self.assertEqual([r["tstar"] for r in receipt["rows"]], TIMES)
|
||||||
self.assertEqual(len(receipt["early_pressure"]["rows"]), 65 if early else 0)
|
self.assertEqual(len(receipt["early_pressure"]["rows"]), 65 if early else 0)
|
||||||
self.assertEqual(calls[-1], 16*n)
|
self.assertEqual(calls[-1], 2048)
|
||||||
else:
|
else:
|
||||||
self.assertEqual(len(receipt["rows"]), 1)
|
self.assertEqual(len(receipt["rows"]), 1)
|
||||||
self.assertEqual(len(receipt["early_pressure"]["rows"]),
|
self.assertEqual(len(receipt["early_pressure"]["rows"]),
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user