test: integrate explicitly repinned uniform LES shear diagnostic
This commit is contained in:
@@ -666,6 +666,7 @@ streak.render("streakline.png")
|
||||
## Validation campaigns
|
||||
|
||||
- [DFG 2D-2 Re100](docs/validation/dfg_re100.md): frozen periodicity and official-reference comparisons, with a declared noncanonical outlet.
|
||||
- [Uniform LES shear decay](docs/validation/uniform_les_flow.md): independent continuum-oracle flow diagnostic, baseline and isolated corrected source variants.
|
||||
- [Smagorinsky helper comparison](docs/validation/les_helpers.md): pinned baseline versus an output-local two-coefficient candidate, without production edits.
|
||||
- [Jet-circle control volume](docs/validation/jet_circle_cv.md): bounded AMR versus uniform-grid conservation diagnostic.
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Bounded uniform-grid LES flow validation: prepared, GPU NOT RUN
|
||||
|
||||
## Status and scope
|
||||
|
||||
Prepared for independent review, then an explicitly authorized Horsea run only. No production files were edited, no normalization patch was applied to a full solver checkout, and no GPU was used. This is a small end-to-end **wall-bounded, x-periodic** shear-decay test, not a fully periodic 2D test. It exercises the actual production Simulation → Stepper → OneStep → Smagorinsky → SRT dispatch and double-buffer pull streaming, plus existing halfway bounce-back walls.
|
||||
|
||||
The original fully periodic-y plan is unsupported at the pinned revision: both `one_step_double.cu:90` and `one_step_esopull.cu:133` replace wall-adjacent incoming populations for every fluid node on y=1 and NY-2 regardless of its neighboring flags. `tests/validation/run_poiseuille.py:1–10` already documents this. Merely changing all flags to FLUID does not remove this behavior. The present test deliberately matches those real walls; it does not modify the kernels or claim a periodic-y public API.
|
||||
|
||||
Only unforced SRT, D2Q9, FP32 shifted storage and double-buffer streaming are included. No MRT/TRT, force correction, EsoPull, AMR, Jet, 3D turbulence or physical LES coefficient calibration is certified. In continuum this unidirectional incompressible shear is trace-free; small finite-Mach/discretization stresses in the LBM are not asserted to vanish exactly. The underlying scalar-helper audit remains the cleaner normalization isolation.
|
||||
|
||||
## Exact source contract
|
||||
|
||||
Baseline is explicitly repinned to the production snapshot in `cd3a657f9d3833e812c3cca173525f165b48710a`, whose reviewed AMR-channel source origin is `47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5`. This new experiment does not inherit historical d79 GPU/flow acceptance. The original preparation remains associated with d79; its results are not rewritten.
|
||||
|
||||
`tests/validation/data/les_flow/source_pins.json` records all 101 tracked production source files from that snapshot. The runner checks every hash before importing CelerisLab or touching a GPU. It accepts only:
|
||||
|
||||
- `--variant baseline`: all baseline hashes, including helper SHA256 `ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501`.
|
||||
- `--variant corrected`: the same source bytes except helper SHA256 `a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa`, from the independently reviewed `tests/validation/les/data/smagorinsky_normalization.patch` artifact.
|
||||
|
||||
The runner records the actual checkout commit and exact imported package. It never applies a patch itself. Run it against separate isolated full checkouts. The patch candidate's minimal header tree is not a runnable solver checkout. A source mismatch is a blocker, not permission to regenerate pins from an arbitrary new checkout.
|
||||
|
||||
Relevant pinned implementation evidence:
|
||||
|
||||
- `tests/integration/test_numerical_gpu.py:24–85`: equilibrium construction, installation of spatial postcollision state, count=1 adapter, raw population observation and x-periodic/walled flags.
|
||||
- `tests/validation/run_poiseuille.py:29–78`: production Simulation use with periodic x, halfway y walls and downloaded populations.
|
||||
- `src/CelerisLab/lbm/kernels/core/layout.cuh:87–104`: actual modulo neighbors.
|
||||
- `src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh:18–38`: incoming populations copied from opposite local input directions.
|
||||
- `src/CelerisLab/lbm/kernels/operators/helpers.cuh:53–61`: LES effective omega feeds actual SRT collision.
|
||||
- `src/CelerisLab/cuda/compiler_v2.py:56–95`: compiler snapshots into private temporary build directories; package sources are not rewritten.
|
||||
|
||||
## Geometry, units and initial state
|
||||
|
||||
8×34 nodes: 8 periodic-x columns, 32 fluid rows, 2 ghost-wall rows. Relative to the bottom physical wall, fluid centers are y=0.5,...,31.5; physical halfway walls are y=0 and 32. Every x edge fluid node is explicitly FLUID; all top/bottom ghost nodes are SOLID|BC_WALL. Thus unused inlet/outlet configuration has no inlet/outlet flags to act on. There are no bodies, Jets or external forces.
|
||||
|
||||
Lattice units: dx_LBM=dt_LBM=1, rho=1, nu=0.02, tau0=0.56, U=0.04, H=32. Initial u_x(y)=U sin(pi y/H), u_y=0. The maximum Mach number is 0.0693. Height-based Re=U H/nu=64; a wavelength-based Re would differ and is not used. No dimensional physical length/time/viscosity is asserted without a user-supplied unit mapping.
|
||||
|
||||
Initialize equilibrium populations, encode shifted FP32, upload both DB buffers and set completed-count=1 following the existing integration-test adapter. That count is storage metadata: physical elapsed time starts at 0 after installation. Observe density and momentum directly from current postcollision populations, adding the descriptor weights in float64; do not mix in cached macro fields. This uses a clearly labeled validation-only raw state/flag adapter, not a promised public `set_periodic` or `set_velocity_profile` API.
|
||||
|
||||
Cases: LES enabled/closed-form with Cs=0 (molecular control), Cs=.16 (ordinary-coefficient sanity), then Cs=1 (amplified normalization diagnostic). Cs=1 is intentionally not a physically recommended SGS coefficient. Initial peak modeled nu_t/nu = Cs² Δ² U(pi/H)/nu = 0.19635; estimated effective tau remains near 0.56–0.5718, far from omega guards. Standard default Cs=.16 yields a much weaker signal at these safe parameters and is not used to make a decisive short-run normalization claim. No defaults are changed.
|
||||
|
||||
Observe at elapsed steps 519, 1038, 2075, approximately 0.1, 0.2, 0.4 molecular decay times. Three cases total 6225 measured advancing launches plus 3 warm-up launches per checkout (6228 total), three snapshots each. Population buffers contain only 272×9 FP32 entries each (about 9.6 KiB), apart from ordinary driver/compiler/context overhead. No frames, trajectories or heavy output. Full result JSON is a few tens of KiB. Runtime depends on compilation/device; no performance claim is made.
|
||||
|
||||
## Independent continuum reference
|
||||
|
||||
For u=(u(y),0), |S|=sqrt(2 S:S)=|du/dy|. Therefore
|
||||
|
||||
du/dt = d/dy [(nu + (Cs Delta)^2 |du/dy|) du/dy],
|
||||
u(0,t)=u(H,t)=0.
|
||||
|
||||
`tests/validation/les_flow_oracle.py` integrates this equation directly with centered finite-volume face fluxes and SSPRK2, at 128 and 256 cells on the same physical H=32 domain. Stability step size uses the nonlinear flux derivative nu+2(Cs Delta)^2|gradient|. Boundary face gradients use the zero Dirichlet value across a half cell. It does not import or reproduce the LBM quadratic/helper. A separate fixed-n=256 test halves the CFL factor from .4 to .2 for Cs=0,.16,1 at every checkpoint, requiring relative changes below 1e-8; measured maximum is 1.8141e-10. This isolates temporal error from the spatial-refinement test.
|
||||
|
||||
Crucially the SGS filter Delta=1 stays fixed as the CPU oracle mesh changes from dy=.25 to .125. Confusing oracle spacing with filter width would change the PDE and invalidate convergence. Future LBM grid refinement also needs an explicit physical-unit/filter-width scaling contract; this artifact does not silently reinterpret Cs or Delta when resizing H.
|
||||
|
||||
Cs=0 is checked directly against U sin(pi y/H) exp[-nu(pi/H)^2 t]. The nonlinear reference includes evolving higher harmonics, rather than assuming it remains a sine. At the final time, host oracle predicts 5.1706% SGS reduction in the fundamental amplitude and 1.5409% full-profile separation between standard and old coefficient conventions. The old convention reference is computed by changing continuum Cs to Cs/2^(1/4), purely as a diagnostic; it does not replace standard acceptance.
|
||||
|
||||
## Predeclared criteria (do not tune after seeing GPU results)
|
||||
|
||||
- Initial decoded profile relative L2 <= 1e-5; otherwise no advancing test.
|
||||
- Molecular Cs=0 profile error against analytic decay <= 0.003 at every checkpoint.
|
||||
- Cs=1 full-profile error against standard nonlinear oracle <= 0.005 at every checkpoint.
|
||||
- Oracle 128/256-grid interpolated relative difference <= 8e-5 at every checkpoint.
|
||||
- Relative error in SGS fundamental-amplitude decrement <= 0.15 at the last two checkpoints. Decrement is measured from actual Cs=0 amplitude minus actual Cs=1 amplitude; reference uses analytic Cs=0 minus the nonlinear oracle. The first checkpoint remains a transient diagnostic for this increment gate.
|
||||
- Fluid mean-density drift <= 5e-5, max |u_y| <= 1e-5 and x-column velocity spread <= 1e-6 at every checkpoint; finite populations, strictly positive finite fluid density and finite decoded velocity/profile/diagnostics required throughout observations, including startup and warm-up.
|
||||
|
||||
Reasons: molecular 0.3% leaves room for finite-lattice dispersion, equilibrium-start kinetic transient and FP32 error at H=32; nonlinear 0.5% is substantially below the predicted 1.54% old-vs-standard separation. The 15% increment tolerance is below the old convention's approximately 29% viscosity-coefficient deficit, while allowing continuum/lattice differences. Directly subtracting the two measured amplitudes reduces common lattice error. Oracle convergence is over 60 times tighter than the nonlinear profile bound. These are declared engineering test gates, not GPU-validated uncertainty estimates; a failure requires diagnosis/refinement and an explicitly revised experiment, never a post-hoc tolerance edit to obtain a pass.
|
||||
|
||||
Both baseline and candidate are judged against the same standard convention. Baseline is expected to fail SGS criteria if the flow reproduces the scalar finding; an exit 1 with valid evidence is useful. The runner reports old-convention profile error but never masks baseline failure. Candidate passing alone is insufficient if baseline does not demonstrate meaningful discrimination. Review both results and source pins together.
|
||||
|
||||
## Host verification and later GPU commands
|
||||
|
||||
From the repository root, using existing Python/NumPy and the existing pytest environment:
|
||||
|
||||
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python -m pytest -q tests/unit/test_les_flow_oracle.py tests/unit/test_les_flow_runner.py
|
||||
|
||||
Nine tests cover analytic decay, independent-oracle mesh convergence, symmetry/positivity/energy decay, filter invariance, fixed-grid timestep halving, signal discrimination, invalid density/velocity rejection, source pins/candidate/tamper rejection and the no-GPU-without-opt-in CLI guard. The integrated tests preserve those checks. No PyCUDA import or GPU run is required for them. Cs=.16 uses the same 0.5% full-profile safety/sanity bound; its SGS increment is not a discriminatory normalization gate.
|
||||
|
||||
The source-guard tests materialize temporary candidate headers with the existing reviewed LES preparation tool; the GPU entrypoint uses `tests/validation/run_uniform_les_flow.py`, `tests/validation/les_flow_oracle.py`, and `tests/validation/data/les_flow/source_pins.json` in that repository layout.
|
||||
|
||||
After review, make isolated full baseline and corrected checkouts of the exact base. Apply only `tests/validation/les/data/smagorinsky_normalization.patch` to the corrected checkout; do not edit the benchmark/production tree. On authorized Horsea, with existing NumPy, PyCUDA, nvcc and driver, run sequentially and select the authorized physical GPU explicitly:
|
||||
|
||||
CUDA_VISIBLE_DEVICES=<physical-GPU> PYTHONDONTWRITEBYTECODE=1 python tests/validation/run_uniform_les_flow.py --run-gpu --device 0 --root <isolated-baseline> --variant baseline --output <new-evidence-dir>/baseline.json
|
||||
CUDA_VISIBLE_DEVICES=<physical-GPU> PYTHONDONTWRITEBYTECODE=1 python tests/validation/run_uniform_les_flow.py --run-gpu --device 0 --root <isolated-corrected> --variant corrected --output <new-evidence-dir>/corrected.json
|
||||
|
||||
`--device 0` is the logical ordinal within CUDA_VISIBLE_DEVICES. Do not use a shell `&&` between these: baseline's expected standard-criterion failure must not suppress the corrected run. Existing output files are refused. A JSON `READY` line is printed separately for each case only after source verification, compilation, initialization, raw-state installation, one actual advancing OneStep launch, post-warm-up macro validation, complete restoration of both DB buffers/flags/count to the initial state, profile readback and CUDA synchronization. It gives source identity, device, Cs, warm-up/reset confirmation, installed-profile error and maximum measured steps. This is the first trustworthy startup checkpoint, not a statement that the physics passed. `DONE` is printed after all three cases and all criteria; failures/compile errors before that remain incomplete runs.
|
||||
|
||||
Deliver both JSON files and stdout/stderr with artifact hashes. Review before any production integration. AMR LES guards remain untouched.
|
||||
|
||||
## Isolated candidate preparation
|
||||
|
||||
Use the current validation commit for two separate full checkouts, preserving the baseline. In the corrected checkout only, apply `tests/validation/les/data/smagorinsky_normalization.patch` with `git apply`; do not commit that experimental production change or update the pins. The runner requires exactly the baseline bytes plus the reviewed helper hash. The header-only scalar diagnostic tree is insufficient for this end-to-end flow. Baseline and corrected commands must run as separate Python processes; package import caching must never combine their implementations. Both reject an imported CelerisLab package outside the explicitly supplied checkout, including an installed DynamisLab copy. Retain the actual checkout commit, variant, exact header hash, and experimental source label with each result.
|
||||
@@ -0,0 +1,70 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
||||
import json
|
||||
import unittest
|
||||
import numpy as np
|
||||
from les_flow_oracle import H, U, NU, TIMES, solve, relative, amplitude
|
||||
|
||||
|
||||
class OracleTests(unittest.TestCase):
|
||||
def test_linear_exact(self):
|
||||
y, states = solve(0, n=256)
|
||||
for t, u in states.items():
|
||||
exact = U * np.sin(np.pi * y / H) * np.exp(-NU * (np.pi / H) ** 2 * t)
|
||||
self.assertLess(relative(u, exact), 1e-5)
|
||||
|
||||
def test_nonlinear_refinement_symmetry_and_dissipation(self):
|
||||
y, states = solve(1, n=256)
|
||||
yc, coarse = solve(1, n=128)
|
||||
energy = U**2 / 2
|
||||
for t, u in states.items():
|
||||
self.assertTrue(np.all(np.isfinite(u)))
|
||||
self.assertGreaterEqual(float(u.min()), 0)
|
||||
self.assertLessEqual(float(u.max()), U)
|
||||
np.testing.assert_allclose(u, u[::-1], atol=1e-14)
|
||||
self.assertLess(float(np.mean(u * u)), energy)
|
||||
energy = float(np.mean(u * u))
|
||||
self.assertLess(relative(np.interp(yc, y, u), coarse[t]), 8e-5)
|
||||
|
||||
def test_filter_is_independent_of_oracle_grid(self):
|
||||
y, a = solve(1, n=128, times=(TIMES[-1],))
|
||||
_, b = solve(0.5, n=128, times=(TIMES[-1],), delta=2)
|
||||
np.testing.assert_array_equal(a[TIMES[-1]], b[TIMES[-1]])
|
||||
|
||||
def test_fixed_grid_timestep_halving(self):
|
||||
errors = []
|
||||
for cs in (0.0, 0.16, 1.0):
|
||||
_, full = solve(cs, n=256, cfl=0.4)
|
||||
_, half = solve(cs, n=256, cfl=0.2)
|
||||
errors.extend(relative(full[t], half[t]) for t in TIMES)
|
||||
self.assertLess(max(errors), 1e-8)
|
||||
print(
|
||||
json.dumps({"fixed_grid_timestep_half_max_relative": max(errors)}),
|
||||
flush=True,
|
||||
)
|
||||
|
||||
def test_signal_and_wrong_convention_separate(self):
|
||||
y, std = solve(1, n=256, times=(TIMES[-1],))
|
||||
_, wrong = solve(2 ** (-0.25), n=256, times=(TIMES[-1],))
|
||||
t = TIMES[-1]
|
||||
base = U * np.exp(-NU * (np.pi / H) ** 2 * t)
|
||||
signal = (base - amplitude(y, std[t])) / base
|
||||
separation = relative(wrong[t], std[t])
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"final_time": t,
|
||||
"standard_sgs_amplitude_signal": signal,
|
||||
"wrong_convention_profile_separation": separation,
|
||||
}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
self.assertGreater(signal, 0.04)
|
||||
self.assertGreater(separation, 0.01)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Host-only entrypoint/source-guard checks; never imports PyCUDA."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
import unittest
|
||||
import numpy as np
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "validation"))
|
||||
from run_uniform_les_flow import source_guard, decode_macros, HERE, HEADER
|
||||
|
||||
ROOT = HERE.parents[1]
|
||||
|
||||
|
||||
class GuardTests(unittest.TestCase):
|
||||
def test_decoded_density_and_velocity_guards(self):
|
||||
w = np.array([4 / 9] + [1 / 9] * 4 + [1 / 36] * 4)
|
||||
cx = np.array([0, 1, -1, 0, 0, 1, -1, -1, 1])
|
||||
cy = np.array([0, 0, 0, 1, -1, 1, -1, 1, -1])
|
||||
data = np.zeros((9, 4, 2))
|
||||
profile, mass, vy, spread = decode_macros(data, w, cx, cy)
|
||||
np.testing.assert_allclose(profile, 0, atol=1e-16)
|
||||
self.assertAlmostEqual(mass, 1)
|
||||
for scale in (1, 2):
|
||||
invalid = np.broadcast_to(-scale * w[:, None, None], data.shape).copy()
|
||||
with self.assertRaisesRegex(RuntimeError, "density"):
|
||||
decode_macros(invalid, w, cx, cy)
|
||||
invalid = data.copy()
|
||||
invalid[0, 1, 0] = np.nan
|
||||
with self.assertRaisesRegex(RuntimeError, "populations"):
|
||||
decode_macros(invalid, w, cx, cy)
|
||||
# Finite density but overflowing moment must not produce an accepted NaN/Inf norm.
|
||||
invalid = data.copy()
|
||||
invalid[1, :, :] = 1e308
|
||||
invalid[2, :, :] = -1e308
|
||||
with self.assertRaisesRegex(RuntimeError, "velocity/profile"):
|
||||
decode_macros(invalid, w, cx, cy)
|
||||
|
||||
def test_baseline_pins(self):
|
||||
self.assertEqual(
|
||||
source_guard(ROOT, "baseline")["base_commit"],
|
||||
"cd3a657f9d3833e812c3cca173525f165b48710a",
|
||||
)
|
||||
|
||||
def test_corrected_and_tamper_guard(self):
|
||||
pins = json.loads((HERE / "data/les_flow/source_pins.json").read_text())
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for name in pins["sources"]:
|
||||
dest = root / name
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / name, dest)
|
||||
# Rebuild the reviewed candidate from actual pinned headers, not fixtures.
|
||||
candidate = root / "header_candidate"
|
||||
subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(HERE / "les/prepare.py"),
|
||||
"--repo",
|
||||
str(ROOT),
|
||||
"--output-dir",
|
||||
str(candidate),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
shutil.copyfile(candidate / "patched" / HEADER, root / HEADER)
|
||||
with patch(
|
||||
"run_uniform_les_flow.subprocess.check_output",
|
||||
return_value="isolated-fixture\n",
|
||||
):
|
||||
source_guard(root, "corrected")
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "baseline")
|
||||
target = root / "src/CelerisLab/lbm/kernels/step/one_step_double.cu"
|
||||
target.write_bytes(target.read_bytes() + b"\n// tamper\n")
|
||||
with self.assertRaisesRegex(RuntimeError, "Source pin mismatch"):
|
||||
source_guard(root, "corrected")
|
||||
|
||||
def test_gpu_requires_opt_in_before_import(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
str(HERE / "run_uniform_les_flow.py"),
|
||||
"--root",
|
||||
str(ROOT),
|
||||
"--variant",
|
||||
"baseline",
|
||||
"--output",
|
||||
str(Path(directory) / "out.json"),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("Explicit --run-gpu is required", result.stderr)
|
||||
self.assertNotIn("pycuda", result.stderr)
|
||||
self.assertFalse((Path(directory) / "out.json").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"base_commit": "cd3a657f9d3833e812c3cca173525f165b48710a",
|
||||
"sources": {
|
||||
"src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6",
|
||||
"src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d",
|
||||
"src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc",
|
||||
"src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763",
|
||||
"src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9",
|
||||
"src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2",
|
||||
"src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a",
|
||||
"src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f",
|
||||
"src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436",
|
||||
"src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac",
|
||||
"src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3",
|
||||
"src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d",
|
||||
"src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec",
|
||||
"src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300",
|
||||
"src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68",
|
||||
"src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323",
|
||||
"src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef",
|
||||
"src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f",
|
||||
"src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b",
|
||||
"src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046",
|
||||
"src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515",
|
||||
"src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29",
|
||||
"src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec",
|
||||
"src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605",
|
||||
"src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b",
|
||||
"src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65",
|
||||
"src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37",
|
||||
"src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e",
|
||||
"src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a",
|
||||
"src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1",
|
||||
"src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865",
|
||||
"src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79",
|
||||
"src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56",
|
||||
"src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817",
|
||||
"src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea",
|
||||
"src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1",
|
||||
"src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9",
|
||||
"src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2",
|
||||
"src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b",
|
||||
"src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7",
|
||||
"src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd",
|
||||
"src/CelerisLab/lbm/channel.py": "2d5b5ff43414cb35633ac4ce90ce2c3a3353950b25c168e1c2ab58d498dd8b0f",
|
||||
"src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732",
|
||||
"src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b",
|
||||
"src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826",
|
||||
"src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b",
|
||||
"src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7",
|
||||
"src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a",
|
||||
"src/CelerisLab/lbm/kernels/blocks.cu": "e3b9606f861bfea8c4b0f34e8356bb230081f899c584f67be89178d889099b17",
|
||||
"src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233",
|
||||
"src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea",
|
||||
"src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258",
|
||||
"src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "59b38a89439878e00de7de16b9c7b03feff443fcc5a421593f7d2536dcbb7ab8",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89",
|
||||
"src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "de8b213a6da42c2e5e9f8966199fc66ea4541b85144a90f3f5dbad3c6ab63b97",
|
||||
"src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4",
|
||||
"src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed",
|
||||
"src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37",
|
||||
"src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d",
|
||||
"src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c",
|
||||
"src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072",
|
||||
"src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727",
|
||||
"src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e",
|
||||
"src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356",
|
||||
"src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab",
|
||||
"src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706",
|
||||
"src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922",
|
||||
"src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd",
|
||||
"src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4",
|
||||
"src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1",
|
||||
"src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6",
|
||||
"src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b",
|
||||
"src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec",
|
||||
"src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3",
|
||||
"src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501",
|
||||
"src/CelerisLab/lbm/kernels/refinement.cu": "129f3d91da93b4324f88a0da73a4da1e5390f72c14cce574ee63306712da9895",
|
||||
"src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013",
|
||||
"src/CelerisLab/lbm/kernels/step/block_channel.cu": "befbf6d757e532a59c166d41f920d16d71557cd3a574478e7aa83da737c48cbd",
|
||||
"src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f",
|
||||
"src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087",
|
||||
"src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9",
|
||||
"src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e",
|
||||
"src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79",
|
||||
"src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549",
|
||||
"src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044",
|
||||
"src/CelerisLab/lbm/raw_exchange.py": "8694202466eba848563e298656353b0f5db94e21a01fa05f51aad6dc2b9fd311",
|
||||
"src/CelerisLab/lbm/refinement.py": "08c2a0557276a98cbcaa459d9bb7a9735d8bbbf64a6114c57af2fda61481a687",
|
||||
"src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d",
|
||||
"src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6",
|
||||
"src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce",
|
||||
"src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197",
|
||||
"src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c"
|
||||
},
|
||||
"candidate_header_sha256": "a3db46c2e7d27530b2d1436e65cdbed0b72cc7e047bfbeef2a640877271b51aa",
|
||||
"production_origin_commit": "47b05bc8abfc957fe876b0c63bdb386e3c6cbaa5",
|
||||
"source_experiment": "Explicit channel47/cd3 source experiment; not inherited d79 GPU acceptance. Corrected variant permits only the reviewed Smagorinsky header patch.",
|
||||
"historical_preparation_commit": "d79e3f6f0a424f5bcc3a1a6d610313e49daf97a2"
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Independent 1D continuum oracle; no LBM closure/header or GPU imports."""
|
||||
|
||||
import numpy as np
|
||||
|
||||
H, U, NU, DELTA = 32.0, 0.04, 0.02, 1.0
|
||||
TIMES = tuple(round(v * H**2 / (np.pi**2 * NU)) for v in (0.1, 0.2, 0.4))
|
||||
|
||||
|
||||
def solve(cs, n=256, times=TIMES, *, h=H, u0=U, nu=NU, delta=DELTA, cfl=0.4):
|
||||
"""FV centered face flux plus SSPRK2, zero Dirichlet at 0,h.
|
||||
|
||||
du/dt = d/dy[(nu + (cs*delta)^2*|du/dy|) du/dy].
|
||||
The oracle refinement dx=h/n MUST NOT replace the LES filter delta.
|
||||
"""
|
||||
if not np.isfinite(cfl) or not 0 < cfl <= 0.4:
|
||||
raise ValueError("cfl must be finite and in (0, .4]")
|
||||
dx = h / n
|
||||
y = (np.arange(n) + 0.5) * dx
|
||||
u = u0 * np.sin(np.pi * y / h)
|
||||
a = (cs * delta) ** 2
|
||||
|
||||
def gradients(v):
|
||||
g = np.empty(n + 1)
|
||||
g[1:-1] = np.diff(v) / dx
|
||||
g[0], g[-1] = 2 * v[0] / dx, -2 * v[-1] / dx
|
||||
return g
|
||||
|
||||
def rhs(v):
|
||||
g = gradients(v)
|
||||
flux = (nu + a * np.abs(g)) * g
|
||||
return np.diff(flux) / dx
|
||||
|
||||
t = 0.0
|
||||
out = {}
|
||||
for target in times:
|
||||
if target < t:
|
||||
raise ValueError("times must be nondecreasing and nonnegative")
|
||||
while t < target:
|
||||
# Nonlinear flux derivative is nu+2*a*abs(gradient).
|
||||
dt = min(
|
||||
target - t, cfl * dx * dx / (nu + 2 * a * np.max(np.abs(gradients(u))))
|
||||
)
|
||||
v = u + dt * rhs(u)
|
||||
u = 0.5 * u + 0.5 * (v + dt * rhs(v))
|
||||
t += dt
|
||||
out[int(target)] = u.copy()
|
||||
return y, out
|
||||
|
||||
|
||||
def amplitude(y, u, h=H):
|
||||
s = np.sin(np.pi * y / h)
|
||||
return float(np.dot(u, s) / np.dot(s, s))
|
||||
|
||||
|
||||
def relative(a, b):
|
||||
return float(np.linalg.norm(a - b) / np.linalg.norm(b))
|
||||
@@ -0,0 +1,321 @@
|
||||
"""Bounded SRT/DB uniform-grid wall-bounded shear validation, Horsea only.
|
||||
|
||||
Uses production Simulation/collision/streaming. Test-only raw population/flag
|
||||
adapter follows tests/integration/test_numerical_gpu.py; no periodic-y API.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from tempfile import TemporaryDirectory
|
||||
import time
|
||||
import numpy as np
|
||||
from les_flow_oracle import H, U, NU, DELTA, TIMES, solve, amplitude, relative
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
HEADER = "src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh"
|
||||
LIMITS = dict(
|
||||
linear_relative_l2=0.003,
|
||||
les_relative_l2=0.005,
|
||||
sgs_increment_relative_error=0.15,
|
||||
oracle_grid_relative=0.00008,
|
||||
mass_drift=0.00005,
|
||||
max_transverse=0.00001,
|
||||
x_invariance=0.000001,
|
||||
initial_relative_l2=0.00001,
|
||||
)
|
||||
|
||||
|
||||
def decode_macros(data, w, cx, cy):
|
||||
"""Reject invalid physical populations/density/macros before reporting norms."""
|
||||
with np.errstate(over="ignore", invalid="ignore", divide="ignore"):
|
||||
f = data.astype(np.float64) + w[:, None, None].astype(np.float64)
|
||||
if not np.isfinite(f).all():
|
||||
raise RuntimeError("Nonfinite populations")
|
||||
f = f[:, 1:-1, :]
|
||||
rho = f.sum(axis=0)
|
||||
if not np.isfinite(rho).all() or np.any(rho <= 0):
|
||||
raise RuntimeError("Nonpositive or nonfinite fluid density")
|
||||
vx = np.einsum("q,qyx->yx", cx, f) / rho
|
||||
vy = np.einsum("q,qyx->yx", cy, f) / rho
|
||||
profile = vx.mean(axis=1)
|
||||
if not (
|
||||
np.isfinite(vx).all()
|
||||
and np.isfinite(vy).all()
|
||||
and np.isfinite(profile).all()
|
||||
):
|
||||
raise RuntimeError("Nonfinite decoded velocity/profile")
|
||||
metrics = (
|
||||
float(rho.mean()),
|
||||
float(np.max(np.abs(vy))),
|
||||
float(np.max(np.ptp(vx, axis=1))),
|
||||
)
|
||||
if not np.isfinite(metrics).all():
|
||||
raise RuntimeError("Nonfinite decoded diagnostics")
|
||||
return profile, *metrics
|
||||
|
||||
|
||||
def source_guard(root, variant):
|
||||
manifest = json.loads((HERE / "data/les_flow/source_pins.json").read_text())
|
||||
for name, sha in manifest["sources"].items():
|
||||
expected = (
|
||||
manifest["candidate_header_sha256"]
|
||||
if name == HEADER and variant == "corrected"
|
||||
else sha
|
||||
)
|
||||
if hashlib.sha256((root / name).read_bytes()).hexdigest() != expected:
|
||||
raise RuntimeError("Source pin mismatch: " + name)
|
||||
return dict(
|
||||
base_commit=manifest["base_commit"],
|
||||
production_origin_commit=manifest["production_origin_commit"],
|
||||
source_experiment=manifest["source_experiment"],
|
||||
checkout_commit=subprocess.check_output(
|
||||
["git", "-C", str(root), "rev-parse", "HEAD"], text=True
|
||||
).strip(),
|
||||
source_file_count=len(manifest["sources"]),
|
||||
header_sha256=hashlib.sha256((root / HEADER).read_bytes()).hexdigest(),
|
||||
variant=variant,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--root", type=Path, required=True)
|
||||
ap.add_argument("--variant", choices=["baseline", "corrected"], required=True)
|
||||
ap.add_argument("--output", type=Path, required=True)
|
||||
ap.add_argument(
|
||||
"--device", type=int, default=0, help="Logical device in CUDA_VISIBLE_DEVICES"
|
||||
)
|
||||
ap.add_argument("--run-gpu", action="store_true")
|
||||
args = ap.parse_args()
|
||||
if not args.run_gpu:
|
||||
ap.error("Explicit --run-gpu is required; authorized Horsea executor only")
|
||||
if not os.environ.get("CUDA_VISIBLE_DEVICES"):
|
||||
ap.error("Explicit CUDA_VISIBLE_DEVICES required")
|
||||
if args.output.exists():
|
||||
ap.error("Output exists; choose a new evidence path")
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
root = args.root.resolve()
|
||||
pins = source_guard(root, args.variant)
|
||||
# Refuse installed-package shadowing; import only after explicit GPU opt-in.
|
||||
sys.path.insert(0, str(root / "src"))
|
||||
import CelerisLab
|
||||
|
||||
if Path(CelerisLab.__file__).resolve() != root / "src/CelerisLab/__init__.py":
|
||||
raise RuntimeError("Wrong CelerisLab import")
|
||||
import pycuda.driver as cuda
|
||||
from CelerisLab.simulation import Simulation
|
||||
from CelerisLab.lbm.descriptors import D2Q9_EX, D2Q9_EY, FLUID, SOLID, BC_WALL
|
||||
|
||||
cx = np.asarray(D2Q9_EX)
|
||||
cy = np.asarray(D2Q9_EY)
|
||||
w = np.asarray([4 / 9] + [1 / 9] * 4 + [1 / 36] * 4, dtype=np.float32)
|
||||
h = int(H)
|
||||
nx = 8
|
||||
ny = h + 2
|
||||
y = np.arange(h) + 0.5
|
||||
report = dict(
|
||||
source=pins,
|
||||
package=CelerisLab.__file__,
|
||||
cuda_visible_devices=os.environ["CUDA_VISIBLE_DEVICES"],
|
||||
logical_device=args.device,
|
||||
geometry=dict(nx=nx, ny=ny, halfway_wall_height=H),
|
||||
parameters=dict(
|
||||
nu=NU,
|
||||
U=U,
|
||||
delta_lattice=DELTA,
|
||||
tau0=0.5 + 3 * NU,
|
||||
re_height=U * H / NU,
|
||||
ma=U * np.sqrt(3),
|
||||
),
|
||||
limits=LIMITS,
|
||||
criterion="standard |S|=sqrt(2 S:S); no baseline-failure masking",
|
||||
cases=[],
|
||||
)
|
||||
|
||||
def save():
|
||||
args.output.write_text(json.dumps(report, indent=2, allow_nan=False) + "\n")
|
||||
|
||||
started = time.monotonic()
|
||||
for cs in (0.0, 0.16, 1.0):
|
||||
cfg = json.loads((root / "src/CelerisLab/configs/config_lbm.json").read_text())
|
||||
cfg["grid"].update(nx=nx, ny=ny, nz=1, lattice_model="D2Q9")
|
||||
cfg["physics"].update(viscosity=NU, velocity=0.0, rho=1.0)
|
||||
cfg["method"].update(
|
||||
collision="SRT",
|
||||
streaming="double_buffer",
|
||||
store_precision="FP32",
|
||||
ddf_shifting=True,
|
||||
y_wall_bc="bounce_back",
|
||||
)
|
||||
cfg["method"]["les"].update(enabled=True, cs=cs, closed_form=True)
|
||||
with TemporaryDirectory(prefix="uniform-les-flow-") as temp:
|
||||
lbm = Path(temp) / "lbm.json"
|
||||
bodies = Path(temp) / "bodies.json"
|
||||
lbm.write_text(json.dumps(cfg))
|
||||
bodies.write_text('{"objects":[]}')
|
||||
sim = Simulation(str(lbm), str(bodies), device_id=args.device)
|
||||
try:
|
||||
sim.initialize()
|
||||
# Exactly the existing integration-test spatial postcollision contract.
|
||||
# y-wall ghosts stay zero velocity; x edge fluid gives modulo x streaming.
|
||||
ux = np.zeros((ny, nx))
|
||||
ux[1:-1] = U * np.sin(np.pi * y / H)[:, None]
|
||||
cu = cx[:, None, None] * ux[None, :, :]
|
||||
physical = w[:, None, None] * (
|
||||
1 + 3 * cu + 4.5 * cu * cu - 1.5 * ux[None, :, :] ** 2
|
||||
)
|
||||
shifted = np.ascontiguousarray(
|
||||
physical - w[:, None, None], dtype=np.float32
|
||||
)
|
||||
|
||||
def install_initial():
|
||||
with sim.ctx.activate():
|
||||
cuda.memcpy_htod(sim.field.ddf_gpu, shifted)
|
||||
cuda.memcpy_htod(sim.field.temp_gpu, shifted)
|
||||
sim.stepper._step_count = (
|
||||
1 # count metadata; physical elapsed time is 0
|
||||
)
|
||||
sim.field.invalidate_host_ddf_cache()
|
||||
flags = sim.field.flag.reshape(ny, nx)
|
||||
flags[:] = FLUID
|
||||
flags[[0, -1], :] = SOLID | BC_WALL
|
||||
sim.field.upload_flags()
|
||||
sim.field.update_params(fx=0.0, fy=0.0, fz=0.0)
|
||||
|
||||
def observe():
|
||||
data = np.empty((9, ny, nx), np.float32)
|
||||
with sim.ctx.activate():
|
||||
cuda.memcpy_dtoh(data, sim.field.ddf_gpu)
|
||||
return decode_macros(data, w, cx, cy)
|
||||
|
||||
install_initial()
|
||||
observe() # reject invalid state before any advancing launch
|
||||
sim.run(1) # exercise the actual advancing kernel before claiming READY
|
||||
observe() # also require valid density/macros after the warm-up launch
|
||||
install_initial() # restore both (possibly swapped) DB buffers, flags and count
|
||||
initial, rho0, _, _ = observe()
|
||||
initial_error = relative(initial, U * np.sin(np.pi * y / H))
|
||||
if (
|
||||
not np.isfinite(initial_error)
|
||||
or initial_error > LIMITS["initial_relative_l2"]
|
||||
):
|
||||
raise RuntimeError("Initial state adapter failed")
|
||||
with sim.ctx.activate():
|
||||
device_name = cuda.Context.get_device().name()
|
||||
cuda.Context.synchronize()
|
||||
print(
|
||||
json.dumps(
|
||||
dict(
|
||||
status="READY",
|
||||
cs=cs,
|
||||
device=device_name,
|
||||
variant=args.variant,
|
||||
source=pins,
|
||||
warmup_steps=1,
|
||||
initial_state_restored=True,
|
||||
initial_relative_l2=initial_error,
|
||||
max_steps=TIMES[-1],
|
||||
mode="SRT double_buffer shifted FP32",
|
||||
)
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
yf, ref = solve(cs, n=256)
|
||||
yc, coarse = solve(cs, n=128)
|
||||
_, wrong = solve(cs * 2 ** (-0.25), n=256) if cs else (yf, ref)
|
||||
case = dict(
|
||||
cs=cs,
|
||||
device=device_name,
|
||||
warmup_steps=1,
|
||||
initial_state_restored=True,
|
||||
configuration=cfg,
|
||||
initial_relative_l2=initial_error,
|
||||
observations=[],
|
||||
)
|
||||
report["cases"].append(case)
|
||||
elapsed = 0
|
||||
for target in TIMES:
|
||||
sim.run(target - elapsed)
|
||||
elapsed = target
|
||||
profile, mass, transverse, xspread = observe()
|
||||
standard = np.interp(y, yf, ref[target])
|
||||
analytic = (
|
||||
U * np.sin(np.pi * y / H) * np.exp(-NU * (np.pi / H) ** 2 * target)
|
||||
)
|
||||
oracle_grid = relative(np.interp(yc, yf, ref[target]), coarse[target])
|
||||
reference = standard if cs else analytic
|
||||
error = relative(profile, reference)
|
||||
obs = dict(
|
||||
elapsed_steps=elapsed,
|
||||
completed_count=sim.stepper.step_count,
|
||||
relative_l2=error,
|
||||
oracle_grid_relative=oracle_grid,
|
||||
mass_drift=abs(mass - rho0),
|
||||
max_transverse=transverse,
|
||||
x_invariance=xspread,
|
||||
amplitude=amplitude(y, profile),
|
||||
reference_amplitude=amplitude(y, reference),
|
||||
wrong_convention_relative_l2=relative(
|
||||
profile, np.interp(y, yf, wrong[target])
|
||||
),
|
||||
profile=profile.tolist(),
|
||||
reference_profile=reference.tolist(),
|
||||
)
|
||||
obs["passed"] = bool(
|
||||
error <= LIMITS["les_relative_l2" if cs else "linear_relative_l2"]
|
||||
and oracle_grid <= LIMITS["oracle_grid_relative"]
|
||||
and abs(mass - rho0) <= LIMITS["mass_drift"]
|
||||
and transverse <= LIMITS["max_transverse"]
|
||||
and xspread <= LIMITS["x_invariance"]
|
||||
)
|
||||
case["observations"].append(obs)
|
||||
save()
|
||||
print(
|
||||
json.dumps(
|
||||
{k: v for k, v in obs.items() if not k.endswith("profile")}
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
finally:
|
||||
sim.close()
|
||||
# Cancel the common molecular lattice error using the actual Cs=0 run.
|
||||
increments = []
|
||||
for zero, les in zip(
|
||||
report["cases"][0]["observations"], report["cases"][-1]["observations"]
|
||||
):
|
||||
measured = zero["amplitude"] - les["amplitude"]
|
||||
expected = zero["reference_amplitude"] - les["reference_amplitude"]
|
||||
increments.append(
|
||||
dict(
|
||||
elapsed_steps=zero["elapsed_steps"],
|
||||
measured=measured,
|
||||
expected=expected,
|
||||
relative_error=abs(measured - expected) / abs(expected),
|
||||
)
|
||||
)
|
||||
report.update(sgs_increments=increments, seconds=time.monotonic() - started)
|
||||
# Last two times are the declared signal gate; earliest is transient diagnostic.
|
||||
report["passed"] = bool(
|
||||
all(o["passed"] for c in report["cases"] for o in c["observations"])
|
||||
and all(
|
||||
v["relative_error"] <= LIMITS["sgs_increment_relative_error"]
|
||||
for v in increments[1:]
|
||||
)
|
||||
)
|
||||
save()
|
||||
print(
|
||||
json.dumps(
|
||||
dict(status="DONE", passed=report["passed"], output=str(args.output))
|
||||
),
|
||||
flush=True,
|
||||
)
|
||||
return 0 if report["passed"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user