test: integrate explicitly repinned uniform LES shear diagnostic

This commit is contained in:
TRON committed 2026-10-18 00:15:52 -07:00
1 parent cd3a657f9d
commit af984eab37
7 files changed
+755

No files matched your search

+70
View File
@@ -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()
+107
View File
@@ -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"
}
+56
View File
@@ -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))
+321
View File
@@ -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())