From 2abc48f0d8896e9d21e007c2fa1d76f8ac78f351 Mon Sep 17 00:00:00 2001 From: TRON Date: Sun, 18 Oct 2026 03:49:21 -0700 Subject: [PATCH] test: select matched MRT DFG pilot with separate provenance --- docs/validation/refined_dfg_mrt.md | 71 +++++++++++ tests/unit/test_refined_dfg.py | 66 +++++++++-- .../data/refined_dfg/source_manifest_mrt.json | 112 ++++++++++++++++++ tests/validation/run_refined_dfg.py | 68 +++++++---- 4 files changed, 290 insertions(+), 27 deletions(-) create mode 100644 docs/validation/refined_dfg_mrt.md create mode 100644 tests/validation/data/refined_dfg/source_manifest_mrt.json diff --git a/docs/validation/refined_dfg_mrt.md b/docs/validation/refined_dfg_mrt.md new file mode 100644 index 0000000..df55c78 --- /dev/null +++ b/docs/validation/refined_dfg_mrt.md @@ -0,0 +1,71 @@ +# Matched MRT DFG pilot preparation + +Preparation only: this test-only extension reuses the existing runner and existing MRT +operator. GPU admission awaits reviewed MRT capability results; no pilot/full execution +is implied by CPU preflight. No production, historical SRT manifest, reference data, +periodic gate, geometry, initialization or observation algorithm is changed here. + +## Selection and provenance + +Use `--collision MRT` for **both** uniform D80 and refined root D40/fine D80. There are +no independently selectable collision models per case. Default remains SRT. + +MRT verifies `data/refined_dfg/source_manifest_mrt.json`, pinned to reviewed numerical +source snapshot `3ea131e92c057843034f11a1c574301a2430dfce`, including the shared geometry, +pressure/cycle helpers and official reference. Test-only commits may change HEAD without +changing this numerical source pin. Plan, READY, case summaries and comparison separately +record `checkout_revision` and `checkout_dirty` using existing source-provenance helper; +the runner hash remains separate. Collision is recorded in every receipt, and full-mode +validation rejects missing or different collision labels in plan, either summary or either +READY receipt, in addition to existing source/runner/health/time checks. + +The historical SRT manifest remains byte-for-byte unchanged. SRT on the MRT candidate +checkout deliberately fails that source pin; use a matching historical checkout via +`--source` for SRT. No fallback or automatic repinning is permitted. Historical SRT +failure/diagnostic evidence remains attached to its original source and runner hashes; +it cannot authorize an MRT full run. The existing failed-observation step-reporting +undercount is not changed in this narrowly scoped selection patch. + +## Frozen physical comparison + +All settings remain those documented in `refined_dfg.md`: U_lattice=.01, physical mean +U=1 m/s, D=.1 m, shear ν=.001 m²/s, fixed physical domain/body/wall planes, patch +(40,24,320,140), shifted FP32 double buffers, no LES/forcing/Jets, finest dx=.00125 m, +finest dt=.0000125 s, unchanged half-cell frame conversion and axis-specific phase. +Force interval accumulation, physical leaf mass, pressure interpolation, flux sampling, +safety thresholds, t*=5 pilot and t*=300 cap are unchanged. Existing accuracy/periodicity +criteria are imported unchanged. Both MRT cases are fresh runs; old MRT or SRT results +are not substituted as controls. + +Both cases use the **existing MRT pressure-NEQ branch**: only unknown directions q2/6/8 +are extrapolated. This differs from SRT's all-nine .5-damped branch; the retained +`srt_neq_damp=.5` configuration value is inactive under MRT. Backflow clamp remains off. +The outlet is still noncanonical relative to exact DFG do-nothing conditions. + +The reused MRT operator keeps fixed kinetic rates `s_e=s_eps=s_q=1.2` on all levels; +only the shear rate follows existing per-level viscosity. Physical bulk relaxation +therefore differs between coarse and fine levels. Transfer operators and subcycling +are unchanged. This is the candidate's declared behavior, not a new fully mode-scaled +MRT transfer scheme. Do not tune rates, the patch or gates after seeing outcomes. +Capability smoke/parity gates do not establish DFG accuracy or speedup. + +## CPU and admission commands + +From this checkout: + +```sh +python tests/unit/test_refined_dfg.py +python tests/validation/run_refined_dfg.py --collision MRT --mode plan --output /tmp/new-mrt-dfg-plan +``` + +After root verifies the candidate MRT root/parity/refinement capability results and admits +an idle device, the paired pilot may use the existing bounds and external watchdog: + +```sh +CUDA_VISIBLE_DEVICES= timeout --signal=TERM --kill-after=30s s python tests/validation/run_refined_dfg.py --collision MRT --mode pilot --gpu-gates-approved --max-wall-seconds --output +``` + +The flag is caller attestation, not evidence. An unhealthy first case prevents the second. +Existing strict pilot receipt validation and calibrated wall-time controls remain; full +mode is not being admitted as part of this preparation. Root must review both healthy +pilot outcomes and separately decide any full-run admission. diff --git a/tests/unit/test_refined_dfg.py b/tests/unit/test_refined_dfg.py index a5c6f4e..38c4517 100644 --- a/tests/unit/test_refined_dfg.py +++ b/tests/unit/test_refined_dfg.py @@ -13,7 +13,11 @@ h, periodic = r.helpers(r.DEFAULT_SOURCE) def test_source_pin(): - assert r.verify_source(r.DEFAULT_SOURCE)["commit"].startswith("cd3a657") + historical = json.loads((r.HERE / "data/refined_dfg/source_manifest.json").read_text()) + assert historical["commit"].startswith("cd3a657") + assert r.verify_source(r.DEFAULT_SOURCE, "MRT")["commit"].startswith("3ea131e") + with unittest.TestCase().assertRaisesRegex(RuntimeError, "Source mismatch"): + r.verify_source(r.DEFAULT_SOURCE, "SRT") def test_matched_units_and_planes(): @@ -135,16 +139,17 @@ def test_axis_specific_sampling_phase(): def test_full_requires_completed_pilot(): with unittest.TestCase().assertRaisesRegex(ValueError, "pilot-results"): - r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE)) + r.verify_pilot(None, r.verify_source(r.DEFAULT_SOURCE, "MRT")) def pilot_fixture(path): """Synthetic contract fixture only; never a physical result or run receipt.""" - manifest = r.verify_source(r.DEFAULT_SOURCE) + manifest = r.verify_source(r.DEFAULT_SOURCE, "MRT") (path / "plan.json").write_text( json.dumps( dict( source_manifest=manifest, + collision="MRT", bounds_root_edge=list(r.BOUNDS), runner_sha256=r.runner_hash(), ) @@ -177,6 +182,7 @@ def pilot_fixture(path): json.dumps( dict( case=case, + collision="MRT", mode="pilot", status="pilot_complete", healthy=True, @@ -192,6 +198,7 @@ def pilot_fixture(path): json.dumps( dict( case=case, + collision="MRT", mode="pilot", step=interval, runner_sha256=r.runner_hash(), @@ -208,7 +215,7 @@ def test_pilot_validation_and_calibration(): with TemporaryDirectory() as tmp: path = Path(tmp) manifest = pilot_fixture(path) - assert r.verify_pilot(path, manifest) == 3606.0 + assert r.verify_pilot(path, manifest, "MRT") == 3606.0 def test_reject_malformed_pilot_receipts(): @@ -238,7 +245,7 @@ def test_reject_malformed_pilot_receipts(): mutate(bad) receipt.write_text(json.dumps(bad)) with unittest.TestCase().assertRaises(ValueError): - r.verify_pilot(path, manifest) + r.verify_pilot(path, manifest, "MRT") # Exact reviewer regression: previously accepted NaN endpoint. receipt.write_text( json.dumps( @@ -250,11 +257,11 @@ def test_reject_malformed_pilot_receipts(): ) ) with unittest.TestCase().assertRaises(ValueError): - r.verify_pilot(path, manifest) + r.verify_pilot(path, manifest, "MRT") receipt.write_text(json.dumps(valid)) (path / "uniform/ready.json").write_text("{}") with unittest.TestCase().assertRaises(ValueError): - r.verify_pilot(path, manifest) + r.verify_pilot(path, manifest, "MRT") def test_strict_finite_json_and_deadline(): @@ -283,6 +290,51 @@ def test_pressure_probes_actual_body_mask(): assert h.pressure_probes(np.ones(pair.fine_shape), fluid, p)["dp"] == 0.0 +def test_matched_mrt_selection(): + for diameter in (40, 80): + srt, sbody, sp = r.config(h, diameter) + mrt, mbody, mp = r.config(h, diameter, "MRT") + assert mrt["method"]["collision"] == "MRT" + mrt["method"]["collision"] = "SRT" + assert mrt == srt and mbody == sbody and mp == sp + srt_plan, mrt_plan = r.plan(h), r.plan(h, "MRT") + for key in ("root", "uniform", "bounds_root_edge", "finest_dx_m", "finest_dt_s"): + assert srt_plan[key] == mrt_plan[key] + assert "q2/6/8" in mrt_plan["outlet"] and "all nine" in srt_plan["outlet"] + cfg, pair, _, host = r.host_geometry(r.DEFAULT_SOURCE, h, "MRT") + assert cfg.collision == "MRT" and pair.fine_shape == (240, 568) + assert len(host.boundary_cells) > 0 + + +def test_reject_collision_receipts(): + from tempfile import TemporaryDirectory + + for rel in ("plan.json", "uniform/summary.json", "refined/summary.json", + "uniform/ready.json", "refined/ready.json"): + for invalid in ("SRT", None): + with TemporaryDirectory() as tmp: + path = Path(tmp) + manifest = pilot_fixture(path) + receipt = path / rel + record = json.loads(receipt.read_text()) + if invalid is None: + record.pop("collision") + else: + record["collision"] = invalid + receipt.write_text(json.dumps(record)) + with unittest.TestCase().assertRaises(ValueError): + r.verify_pilot(path, manifest, "MRT") + + +def test_record_actual_checkout_separately(): + from source_provenance import checkout_provenance + + actual = checkout_provenance(r.DEFAULT_SOURCE) + assert len(actual["checkout_revision"]) == 40 + assert isinstance(actual["checkout_dirty"], bool) + assert r.verify_source(r.DEFAULT_SOURCE, "MRT")["production_origin_commit"].startswith("3ea131e") + + if __name__ == "__main__": suite = unittest.TestSuite( unittest.FunctionTestCase(fn) diff --git a/tests/validation/data/refined_dfg/source_manifest_mrt.json b/tests/validation/data/refined_dfg/source_manifest_mrt.json new file mode 100644 index 0000000..70d21e2 --- /dev/null +++ b/tests/validation/data/refined_dfg/source_manifest_mrt.json @@ -0,0 +1,112 @@ +{ + "commit": "3ea131e92c057843034f11a1c574301a2430dfce", + "production_origin_commit": "3ea131e92c057843034f11a1c574301a2430dfce", + "collision": "MRT", + "sha256": { + "src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6", + "src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d", + "src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc", + "src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763", + "src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9", + "src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2", + "src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a", + "src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f", + "src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436", + "src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac", + "src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3", + "src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d", + "src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec", + "src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300", + "src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68", + "src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323", + "src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef", + "src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f", + "src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b", + "src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046", + "src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515", + "src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29", + "src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec", + "src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605", + "src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b", + "src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65", + "src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37", + "src/CelerisLab/config.py": "2f1bba7515f5bc08cb3a8d140bcafe7fa0689e01b3b06494697bb2956a71530e", + "src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a", + "src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1", + "src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865", + "src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79", + "src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56", + "src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817", + "src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea", + "src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1", + "src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9", + "src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2", + "src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b", + "src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7", + "src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd", + "src/CelerisLab/lbm/channel.py": "40d0354e5dc97bcdb7e7b5c1e7f9303a109b7134d8a3af88b8ff4cd75ab5a095", + "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": "2fa894b20a29f006911ac3e3aad0793cc4aaf9ccd7a67d01c6320a0836b1d8ef", + "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/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/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37", + "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": "b45aad9c022cecb64f7e968ec3cae2d8385a3cc430ee409700c404a3f4dd8cf8", + "src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013", + "src/CelerisLab/lbm/kernels/step/block_channel.cu": "ee8a31398b8e5233d977c816e6b8e060c1eb2ec71320e594226dbca55593c0d0", + "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": "100d5e46ac894f6008b61364cdad5d37c2ca7ed949d8d71405572d6366526417", + "src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d", + "src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6", + "src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce", + "src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197", + "src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c", + "tests/validation/re100_helpers.py": "289c6e01075ff361573f862b90cfd61e5980c9a8efcbc78aef69979b4004fd60", + "tests/validation/periodic.py": "a2212c0e2705126327a5019e9cafe64eeca398da1b75061ff23630c9843697a6", + "tests/validation/data/dfg_re100/reference_cycle.json": "073d1bda1629697efebad95667bfe80598e217d708dbffd7d5f6c6999379c55f", + "tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c" + } +} diff --git a/tests/validation/run_refined_dfg.py b/tests/validation/run_refined_dfg.py index cc4acff..1885e9e 100644 --- a/tests/validation/run_refined_dfg.py +++ b/tests/validation/run_refined_dfg.py @@ -1,4 +1,4 @@ -"""Frozen matched SRT DFG comparison. GPU execution requires explicit root admission.""" +"""Frozen matched SRT/MRT DFG comparison. GPU execution requires explicit root admission.""" import argparse import csv @@ -39,20 +39,21 @@ def helpers(source): return h, periodic -def config(h, diameter): - cfg, body, p = h.make_config(diameter, SPEED, "SRT", "neq_extrap", "zou_he_local") +def config(h, diameter, collision="SRT"): + cfg, body, p = h.make_config(diameter, SPEED, collision, "neq_extrap", "zou_he_local") cfg["method"]["outlet"]["srt_neq_damp"] = 0.5 return cfg, body, p -def plan(h): - _, _, c = config(h, 40) - _, _, f = config(h, 80) +def plan(h, collision="SRT"): + _, _, c = config(h, 40, collision) + _, _, f = config(h, 80, collision) x0, y0, x1, y1 = BOUNDS fine_active = 4 * (x1 - x0) * (y1 - y0) coarse_uncovered = c["nx"] * c["ny"] - fine_active // 4 return dict( status="prepared_not_run", + collision=collision, root_gpu_gates_required=True, root=c, uniform=f, @@ -82,7 +83,11 @@ def plan(h): / (2 * f["nx"] * f["ny"]), 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.", - outlet="SRT pressure NEQ, all nine fneq damped by .5, no backflow clamp; noncanonical DFG outlet", + outlet=( + "SRT pressure NEQ, all nine fneq damped by .5, no backflow clamp; noncanonical DFG outlet" + if collision == "SRT" else + "MRT pressure NEQ, unknowns q2/6/8 extrapolated; no backflow clamp; noncanonical DFG outlet" + ), modes={"pilot_tstar": 5.0, "full_tstar": 300.0}, observation_dense_tstar=0.02, ) @@ -120,7 +125,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) -def host_geometry(source, h): +def host_geometry(source, h, collision="SRT"): """Production donor/transfer and complete cut-link checks, without CUDA.""" from CelerisLab.config import load_lbm_config from CelerisLab.lbm.refinement import FixedPatch2D, prepare_fine_boundary @@ -133,7 +138,7 @@ def host_geometry(source, h): from CelerisLab.body.geometry.circle import CircleGeometry from CelerisLab.body.objects import SimObject - cfgdict, _, _ = config(h, 40) + cfgdict, _, _ = config(h, 40, collision) with TemporaryDirectory() as tmp: p = Path(tmp) / "cfg.json" p.write_text(json.dumps(cfgdict)) @@ -151,8 +156,11 @@ def host_geometry(source, h): return cfg, pair, body, host -def verify_source(source): - expected = json.loads((HERE / "data/refined_dfg/source_manifest.json").read_text()) +def verify_source(source, collision="SRT"): + if collision not in ("SRT", "MRT"): + raise ValueError("Matched collision must be SRT or MRT") + name = "source_manifest.json" if collision == "SRT" else "source_manifest_mrt.json" + expected = json.loads((HERE / "data/refined_dfg" / name).read_text()) for name, digest in expected["sha256"].items(): if hashlib.sha256((source / name).read_bytes()).hexdigest() != digest: raise RuntimeError("Source mismatch: " + name) @@ -167,7 +175,7 @@ def run_case(a, h, periodic, case): from contextlib import ExitStack refined = case == "refined" - cfgdict, bodydict, p = config(h, 40 if refined else 80) + cfgdict, bodydict, p = config(h, 40 if refined else 80, a.collision) cap = 5.0 if a.mode == "pilot" else 300.0 max_steps = math.ceil(cap * p["diameter"] / SPEED) rows = [] @@ -179,7 +187,7 @@ def run_case(a, h, periodic, case): dest.mkdir() with ExitStack() as stack: if refined: - cfg, pair, body, host = host_geometry(a.source, h) + cfg, pair, body, host = host_geometry(a.source, h, a.collision) context = stack.enter_context(CudaContext(0)) backend = stack.enter_context( FixedPatchStepper( @@ -336,6 +344,8 @@ def run_case(a, h, periodic, case): if len(rows) == 1: ready = dict( case=case, + collision=a.collision, + **a.provenance, mode=a.mode, step=begin, tstar=row["tstar"], @@ -348,6 +358,8 @@ def run_case(a, h, periodic, case): print("READY " + json.dumps(ready, allow_nan=False), flush=True) result = dict( case=case, + collision=a.collision, + **a.provenance, mode=a.mode, history=rows, healthy=bool(healthy), @@ -416,12 +428,14 @@ def check_deadline(deadline): ) -def verify_pilot(path, manifest): +def verify_pilot(path, manifest, collision="SRT"): if path is None: raise ValueError( "Full comparison requires --pilot-results from a completed matched pilot" ) previous = read_finite_json(path / "plan.json") + if previous.get("collision") != collision: + raise ValueError("Pilot collision differs") if previous["source_manifest"] != manifest or previous["bounds_root_edge"] != list( BOUNDS ): @@ -432,7 +446,8 @@ def verify_pilot(path, manifest): for case, diameter in (("uniform", 80), ("refined", 40)): summary = read_finite_json(path / case / "summary.json") if ( - summary.get("case") != case + summary.get("collision") != collision + or summary.get("case") != case or summary.get("mode") != "pilot" or summary.get("status") != "pilot_complete" or summary.get("healthy") is not True @@ -483,7 +498,8 @@ def verify_pilot(path, manifest): raise ValueError("Unhealthy pilot observations") ready = read_finite_json(path / case / "ready.json") if ( - ready.get("case") != case + ready.get("collision") != collision + or ready.get("case") != case or ready.get("mode") != "pilot" or ready.get("step") != interval or ready.get("runner_sha256") != runner_hash() @@ -507,6 +523,7 @@ def verify_pilot(path, manifest): def main(): parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--collision", choices=["SRT", "MRT"], default="SRT") parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--mode", choices=["plan", "pilot", "full"], default="plan") @@ -519,9 +536,15 @@ def main(): ) a = parser.parse_args() a.source = a.source.resolve() - manifest = verify_source(a.source) + manifest = verify_source(a.source, a.collision) h, periodic = helpers(a.source) - calibration = verify_pilot(a.pilot_results, manifest) if a.mode == "full" else None + from source_provenance import checkout_provenance + + a.provenance = checkout_provenance(a.source) + calibration = ( + verify_pilot(a.pilot_results, manifest, a.collision) + if a.mode == "full" else None + ) if a.mode != "plan" and ( a.max_wall_seconds is None or not math.isfinite(a.max_wall_seconds) @@ -536,16 +559,17 @@ def main(): if a.output.exists() and any(a.output.iterdir()): raise ValueError("Output must be empty/new") a.output.mkdir(parents=True, exist_ok=True) - specification = plan(h) + specification = plan(h, a.collision) specification.update( source_manifest=manifest, + **a.provenance, gates=periodic.GATES, reference_gates=periodic.REFERENCE_GATES, runner_sha256=runner_hash(), wall_time_budget_seconds=budget, full_pilot_calibration_seconds=calibration, ) - host_geometry(a.source, h) + host_geometry(a.source, h, a.collision) (a.output / "plan.json").write_text(json.dumps(specification, indent=2)) if a.mode == "plan": return 0 @@ -561,6 +585,8 @@ def main(): except TimeoutError as exc: failure = dict( status="wall_time_limit", + collision=a.collision, + **a.provenance, case=case, matched_case_pass=False, error=str(exc), @@ -574,6 +600,8 @@ def main(): if not result["healthy"]: break comparison = dict( + collision=a.collision, + **a.provenance, status="pilot_only" if a.mode == "pilot" else "incomplete", matched_case_pass=False, )