From c0be841b2e6e1ccba3b8343cecd3cee2fe0ed14f Mon Sep 17 00:00:00 2001 From: TRON Date: Sun, 18 Oct 2026 09:41:10 -0700 Subject: [PATCH] test: prepare isolated uniform full9 DFG pilot --- .../experimental_full9/DFG_PILOT.md | 26 +++ .../dfg_source_manifest.json | 121 ++++++++++++++ .../validation/experimental_full9/run_dfg.py | 113 +++++++++++++ .../validation/experimental_full9/test_dfg.py | 153 ++++++++++++++++++ tests/validation/run_refined_dfg.py | 36 ++++- 5 files changed, 444 insertions(+), 5 deletions(-) create mode 100644 tests/validation/experimental_full9/DFG_PILOT.md create mode 100644 tests/validation/experimental_full9/dfg_source_manifest.json create mode 100644 tests/validation/experimental_full9/run_dfg.py create mode 100644 tests/validation/experimental_full9/test_dfg.py diff --git a/tests/validation/experimental_full9/DFG_PILOT.md b/tests/validation/experimental_full9/DFG_PILOT.md new file mode 100644 index 0000000..582d3df --- /dev/null +++ b/tests/validation/experimental_full9/DFG_PILOT.md @@ -0,0 +1,26 @@ +# Experimental uniform DFG pilot + +Numerical source remains926ca27407c26ca11283282a6e53249925725376. This test-only followup uses a separate dfg_source_manifest.json, pins the official reference-cycle file, and records actual execution HEAD independently. The historical bodyless manifest is unchanged and correctly rejects this checkout because the shared runner gained optional hooks. Use historical939c10c for that historical entrypoint; do not repin old evidence. + +The new run_dfg.py is uniform-only, with the original D80 cylinder, SRT tau.524, shiftedFP32 double buffer, original initialization, noLES/force, pressure-NEQ outlet all-nine damping.5. Only inlet scheme is experimental_regularized_full9. It reuses run_refined_dfg.run_case and the existing pressure, force, physical mass/flux and periodic/reference helpers. Legacy run_case callers retain the original inlet and dense pilot cadence. + +This pilot advances40,000steps to t*=5 with sparse observations every4000steps (t*=.5), matching the standard pre240 cadence. Every100-step batch contributes its full momentum impulse; force time is the interval midpoint, pressure time its endpoint. First100steps provide warmup; the next four100-step batches measure warmed step+force cost. All500steps are included in the physical trajectory and observation impulse; no reset or discarded evolution. Calibration durations and total step/force/startup/wall timings are recorded. Batch force download synchronizes GPU work through the existing API. + +A first unhealthy sample identifies only the first observed failure, not its onset. Receipts separate executed_steps, last_observed_step and the existing steps field; timeout retains available progress, history and calibration. No per-step field download or new health threshold is introduced. Runtime admission uses180seconds internally, with an external240second watchdog. Later full-run admission must use measured batched timings, not the earlier per-step snapshot diagnostic rate. Same-size historical MRT full campaigns took roughly354–373seconds, but this pilot measures the present SRT/full9 case. + +CPU checks from this checkout: + +``` +python tests/validation/experimental_full9/test_dfg.py +python tests/validation/experimental_full9/test_full9.py +python -m py_compile tests/validation/experimental_full9/run_dfg.py tests/validation/run_refined_dfg.py +python tests/validation/experimental_full9/run_dfg.py --output /tmp/new-full9-dfg-preflight +``` + +After independent review and explicit GPU admission only: + +``` +CUDA_VISIBLE_DEVICES= timeout --signal=TERM --kill-after=30s 240s python tests/validation/experimental_full9/run_dfg.py --execute --gpu-admitted --wall-seconds 180 --output output/new-full9-dfg-pilot +``` + +This entrypoint intentionally has no full-run execution option. Existing official-reference assessment is reused and accuracy_pass requires health, periodicity and reference agreement for future qualification; pilot accuracy_qualified is alwaysfalse. A healthy pilot qualifies bounded admission/timing only. No grid convergence, canonical outlet, refined/MRT/other precision, long-time accuracy or general stability claim follows. diff --git a/tests/validation/experimental_full9/dfg_source_manifest.json b/tests/validation/experimental_full9/dfg_source_manifest.json new file mode 100644 index 0000000..2ff4edf --- /dev/null +++ b/tests/validation/experimental_full9/dfg_source_manifest.json @@ -0,0 +1,121 @@ +{ + "commit": "926ca27407c26ca11283282a6e53249925725376", + "historical_numerical_source_commit": "9805383f9c50d867e8555471baf5dbda8333a652", + "variant": "experimental_regularized_full9", + "sha256": { + "src/CelerisLab/__init__.py": "21cc469bd6e970a1f9ff5970c8eea4602065e41fc881854759ee565ee55c10c6", + "src/CelerisLab/body/__init__.py": "e4e0e8f0769a94428d5b02fac790443997556cfc5515a5f1ed1595d2c694d69d", + "src/CelerisLab/body/action_smoother.py": "b1a23ee6bbd4850b172cb8738c5c3546ee465a54189e6af9d8044f023d4687dc", + "src/CelerisLab/body/coupling/__init__.py": "c931f7eb318d48408c3e2febcc6917c88c8dcc1697cbf4329b1f340d94c35763", + "src/CelerisLab/body/coupling/soa_packer.py": "8ec26fd55ef74801bbd5ab6bd3d0a7dabc750cbd498aaac2ca4c61a0108ad3c9", + "src/CelerisLab/body/ddf_patch.py": "60fcfb7441e73bf290bcff41ca3a83da3e7ddad765d7247464e8f02e3043b4e2", + "src/CelerisLab/body/geometry/__init__.py": "ada88980ab672959aca9aa3a4c4aade3219524b2ab68110cd2a2f41e3e0ca90a", + "src/CelerisLab/body/geometry/base.py": "cceca14361ca2f34d34a3e43730e1a79b8ca81be00f0dd62e884db32b479419f", + "src/CelerisLab/body/geometry/circle.py": "7605ca7c21103f1d801ac73ab8dd18dffbdac425b2dee7b8858b283628a9e436", + "src/CelerisLab/body/jets.py": "4104c24af4651f4bb4f8d8fd36fb6d88f41c6e4469822f36e22e74c9ede31dac", + "src/CelerisLab/body/manager.py": "01b653d25bd9b4333379c8bfd12383596b686d2029f2e91b1c91b618540ec6b3", + "src/CelerisLab/body/objects.py": "8e63cee8faa9cc6243bcff428be628dfad73a02a9a3b8ece7d78c0a63406a08d", + "src/CelerisLab/body/preprocess/__init__.py": "dfd9d58bc07f626ab23a70053e31b88a6d9165d2e4c76a4e9f48abe674d3d2ec", + "src/CelerisLab/body/preprocess/flag_overlay.py": "831fc9e9736b758847759a085ff133eb49ec91318ace4fda76f85960138ee300", + "src/CelerisLab/body/registry.py": "fbb7327780adbdc6da15ebdb799980daf32df22ee974af0893d7409f177bbb68", + "src/CelerisLab/body/sync_plan.py": "3a15bfe9ebeb91482dbfc68850761ef39c872cc12d2efc795babd0c01e392323", + "src/CelerisLab/common/__init__.py": "d4dbde78bc97fccc9eabac9abae8f0f15c112c10ff8e9b45bd9401d4723df1ef", + "src/CelerisLab/common/_types.py": "aad2ff1a9033d4fd68783ad7123afb47201ddc58785cb6dc0246947037072a5f", + "src/CelerisLab/common/checkpoint.py": "b2f960aecd5ac27a3a7130b809ffad4709c44d522edef0359ab952654625410b", + "src/CelerisLab/common/pathline.py": "a1b6efcc0a076d0f1dc8d49a595779426d7d5626b00072a59bac710ff02ad046", + "src/CelerisLab/common/preprocess.py": "7fc78a08857cf8e98519500b094a1d896af189b6b15658ff9b388ed51bed4515", + "src/CelerisLab/common/render.py": "0e055476257eb2bcd11e733766e52195de02f8fad228f570a3511a7c831bdc29", + "src/CelerisLab/common/streakline/__init__.py": "13ff1cfa416fdd721a08a659e76b5a5a99b8d3201add10a83b7b0bc654bc1eec", + "src/CelerisLab/common/streakline/_config.py": "a9185ef68e9c7b5d5a1dc0b04a28341e2ad91d13c145a7c106636a878cf0a605", + "src/CelerisLab/common/streakline/_integrate.py": "3fbd4368b2c8d0e7fda40dc65aac5df02b085008b4c68acd83cb7ae45d33a13b", + "src/CelerisLab/common/streakline/_render.py": "236cd637414ee318c4d66f9aed32110d8dc42a100d65fde06172f7cd0ba62c65", + "src/CelerisLab/common/streakline/_streakline.py": "1a3e08daa706b5f83cdeb337e9a945a36ddf7583c40af1d0bb9d825ac954df37", + "src/CelerisLab/config.py": "215b09829e10453b333aae69811ffc638c7aa26ae418f25f8654a1a20a74f212", + "src/CelerisLab/configs/CONFIG.md": "41cafdbe525366c08aa9d0416ea026ccf0026ba7123b8a9b65f968a80d928e3a", + "src/CelerisLab/configs/config_body.json": "055f261f0828ca1d4f2bb694bd15b72bbcabe31d552326208f28e37f761adab1", + "src/CelerisLab/configs/config_lbm.json": "17e4311f1f250c6ece59177a8b6ff856b69a016c7c10267e774b030a0a390865", + "src/CelerisLab/configs/config_lbm_flume_mrt_zouhe.json": "8f593321db05a563a42931603a228d61cbcba06dfbe113262d5cd0ad93c0fe79", + "src/CelerisLab/configs/config_lbm_flume_trt_regularized.json": "f4bba8e6a4339fc3c4eb14f6e2f57c4b22993c73703c773d853edfbe34736e56", + "src/CelerisLab/configs/config_lbm_three_cylinder_triangle.json": "eeb8e7149dbe4aa6b3a9562802735aa950750dfbe2d972a0727e788573afa817", + "src/CelerisLab/cuda/__init__.py": "0d01dff9f0399c8baba822aab5ca0122a135f011008ed8a499ebd9f7e5b06aea", + "src/CelerisLab/cuda/compiler_v2.py": "c4cc3ec4445c8d75e6a253dd87963877117a722bc73b9d4811806fc6045d08c1", + "src/CelerisLab/cuda/context.py": "013dd4ec6d01d2fbd11afdfb103eac6a273bd9f798d663d761b851eeb27885e9", + "src/CelerisLab/cuda/obs_layout.py": "5ffb2c3aeeaaaaeb64f2cd8febfbcf4ad9919e5414020ad753cfade0683b21c2", + "src/CelerisLab/lbm/__init__.py": "7f60fb3aaae81501b510d08c2fedff44fcb05657260eb10b6f5cc91e7204306b", + "src/CelerisLab/lbm/block_stepper.py": "00b313daddc6e63d8ab8423c9190ca7e92ad291de20a2f6b80eb53e3f2d85ec7", + "src/CelerisLab/lbm/blocks.py": "1e644c5719993f418185ad4a10b6950a30636fc912befd9eab41ff48b4cf85cd", + "src/CelerisLab/lbm/channel.py": "2d5b5ff43414cb35633ac4ce90ce2c3a3353950b25c168e1c2ab58d498dd8b0f", + "src/CelerisLab/lbm/curved_links.py": "b6957fa13d13f864dba4f7364e93a39905d062dbb3ee9b3f5191add81fff0732", + "src/CelerisLab/lbm/descriptors.py": "c9f025dfc14da43b989047b84cc22bd05d5cc92c9ec695c1da68fdfb788e3f2b", + "src/CelerisLab/lbm/equilibrium.py": "2fb2f2bb7ff326d23abce80fc2a4c0907b17740ea3e29fe6ae079c5044783826", + "src/CelerisLab/lbm/field.py": "0e727eacc3706add4d8d15c418e9b631050871f831698d3ac689dd5f2c701b9b", + "src/CelerisLab/lbm/hierarchy.py": "f234975074f6b6d289d573ff6ea6832feae0e27cb04f80b37fe81792b13a12e7", + "src/CelerisLab/lbm/initializers.py": "dacf9586963e862d6d0a98ef1a066036be6f91e9d423720661d8ef2c1fb2df6a", + "src/CelerisLab/lbm/kernels/blocks.cu": "e3b9606f861bfea8c4b0f34e8356bb230081f899c584f67be89178d889099b17", + "src/CelerisLab/lbm/kernels/boundary/bounce_back.cuh": "aba3748c564ed91b161d268ef8b737b7c7be96a823063d201f3da684dd845233", + "src/CelerisLab/lbm/kernels/boundary/curved_boundary.cuh": "aeb16bfd483514fb03e15a2fed437d673c3b8b2fe68ab83edf4244ef593878ea", + "src/CelerisLab/lbm/kernels/boundary/esopull_curved_helpers.cuh": "b9b474bc4edaff05681c5443235472d04072d0d775fc0eb9c5967faef52b0258", + "src/CelerisLab/lbm/kernels/boundary/ibm_kernels.cuh": "e6909b547dc8d64cdd8a76a90660243a8f5fee535caa161b52b4dd8b7c29934b", + "src/CelerisLab/lbm/kernels/boundary/inlet/channel_stabilized.cuh": "efe077afcbb6b731256a987c2fd79a1dbc470c902f87070d60421ea6cb13d4e2", + "src/CelerisLab/lbm/kernels/boundary/inlet/common.cuh": "1203739c346cb4986e4ecb3090a70114eecc1688ae886d5abfcdb643be256acb", + "src/CelerisLab/lbm/kernels/boundary/inlet/equilibrium.cuh": "f0964c811b2c41a5aaa1c6d99c490b06e850c634eb8749f1f05ef61f93c2de1d", + "src/CelerisLab/lbm/kernels/boundary/inlet/experimental_full9.cuh": "f2475d6322f060b256f37d81e43d6aa3a1918e2eac83c076c32c2599b674f596", + "src/CelerisLab/lbm/kernels/boundary/inlet/regularized.cuh": "943d4a571818bacb89c70bccfaac202691121e3e385803ce6309123a0a03ddaf", + "src/CelerisLab/lbm/kernels/boundary/inlet/zou_he_local.cuh": "e73e66b853b1e4e0e30d8f4185d832aa414ce670a1813ce2b5459f2887f71f89", + "src/CelerisLab/lbm/kernels/boundary/inlet_outlet.cuh": "4955786adc6d0853f2a9703611ea76e58ef045d6d6f4295bd795b05dc2f00dcd", + "src/CelerisLab/lbm/kernels/boundary/outlet/pressure_neq.cuh": "c8b2adc66d12fe602076ddc9693c5c208711fcaddb58aa9a31401a269f51c4c4", + "src/CelerisLab/lbm/kernels/boundary/planar_velocity.cuh": "501b2759d0e2e4280cb6fdd127d66a50fa89f6de48fc2cae9a7eafb11ae90bed", + "src/CelerisLab/lbm/kernels/config.h": "7d1124125e0b354558297934460e64c760fb68cdd7bf6d01a1238bb2eaab8c37", + "src/CelerisLab/lbm/kernels/config/config_grid.h": "1db3e030ebdc116cd7f02362d1fd077daac55e8f0b4329f6285948c224d3ca5d", + "src/CelerisLab/lbm/kernels/config/config_method.h": "4374e04a130f90f97d17cc20c40d2f9ea0ed0dd8603817d07f215d02266e247c", + "src/CelerisLab/lbm/kernels/config/config_objects.h": "db793af052c756561a190a3ff2cb93ee5fe28462ee4f97c6af80882b4dc8b072", + "src/CelerisLab/lbm/kernels/config/config_obs.h": "c11e2249359145b5c4cbaaf0487506fa77c1f4b82c7c169ddc574aa0babcd727", + "src/CelerisLab/lbm/kernels/config/config_physics.h": "8ac0990b168e1058588676cc05c80df8634cfd671dbc92a89246c3c372d83a4e", + "src/CelerisLab/lbm/kernels/core/descriptors.cuh": "8440bc14857da9007982650f828a23608eec9f7cc4bf772e8c8dc1239cb45356", + "src/CelerisLab/lbm/kernels/core/flags.cuh": "0025705f24710330d9d9195f713879bfb650a85a3449baef0e196b831af104ab", + "src/CelerisLab/lbm/kernels/core/layout.cuh": "1fbd820021c89ac3efcde24145996cb0147769e607e498e61f308ed3e0b13706", + "src/CelerisLab/lbm/kernels/core/obs.cuh": "b742b77c9f61e1f020fe9922c3838ae9d855e17b381154958b58f10a22938922", + "src/CelerisLab/lbm/kernels/core/params.cuh": "6e18bf730d936d940e3d85452f0714a2eaae31bf41002f9fd520ec5b372878dd", + "src/CelerisLab/lbm/kernels/core/precision.cuh": "849df14b4cce50af52399e4b8f3e997196f3ae5f1163f68e28df3e6b2507a8c4", + "src/CelerisLab/lbm/kernels/kernel_v2.cu": "c7bd3fb705b77261ba2441c52a24a7e6a7865c165cba088a1b75201c4d29c408", + "src/CelerisLab/lbm/kernels/operators/collision_mrt.cuh": "aee444eb60c47a3fd518dca975d1af05425a0818a0ec6c3fb3f6150f60c2cb44", + "src/CelerisLab/lbm/kernels/operators/collision_srt.cuh": "6b7ff842f392c8f853d2d1a97cc69bfe50f21283f73e659404da07cf6c95f5e1", + "src/CelerisLab/lbm/kernels/operators/collision_trt.cuh": "9725b9840b9ad8b4ec7be61ff5d06d3151e86d1a87585c13f2fbf934d0d2c5f6", + "src/CelerisLab/lbm/kernels/operators/forcing_guo.cuh": "969a430c1af03f32e3bd0df95d805e64f56334717c44dd4ba208ba503062a24b", + "src/CelerisLab/lbm/kernels/operators/helpers.cuh": "0b3a4d95d7d19f5fec09e5423b8f4cc99f8a226b541967425967330da5a051ec", + "src/CelerisLab/lbm/kernels/operators/macro.cuh": "2be20eb2b7d7f5522daaad7f4b57b1ff8cdf883efe4a67846a96783b2fe846a3", + "src/CelerisLab/lbm/kernels/operators/turbulence_smag.cuh": "ff7f000297ee0817e255ad9fcb6a13b647e3ddd5b206518d35f38d2895a9f501", + "src/CelerisLab/lbm/kernels/refinement.cu": "129f3d91da93b4324f88a0da73a4da1e5390f72c14cce574ee63306712da9895", + "src/CelerisLab/lbm/kernels/step/aux_kernels.cu": "578a5b20a157241ee678547059abb1372dd5a589ad06bb2c15b987eb01ca1013", + "src/CelerisLab/lbm/kernels/step/block_channel.cu": "befbf6d757e532a59c166d41f920d16d71557cd3a574478e7aa83da737c48cbd", + "src/CelerisLab/lbm/kernels/step/curved_double.cu": "bfa7a1df1e939020314d49755045f7a85fe4a19cc1c68dbe62653c66c5e7c2b3", + "src/CelerisLab/lbm/kernels/step/esopull_macro.cu": "c73116020ca217ede1e4de27c8269c31f9d91e33433ac8252a21e9b14187c50f", + "src/CelerisLab/lbm/kernels/step/esopull_sync.cu": "c0ff3e72900c3ab89a05dd03684e96d3b740cf1672d565727fc2af165933d087", + "src/CelerisLab/lbm/kernels/step/init_flow.cu": "305be3bb12fc0d039acec1701baabcf0e8a35e683c49c34f2af8ca9867d2b29c", + "src/CelerisLab/lbm/kernels/step/one_step_double.cu": "f9f8242b38e8e0cf8425b75e2b0215bc15363c62eaa0fb43c761b8e0931bfdc9", + "src/CelerisLab/lbm/kernels/step/one_step_esopull.cu": "aa223b2744ae6c9125673df311a96ce588032c1ba58bebb11fd33ecb990f8ed4", + "src/CelerisLab/lbm/kernels/streaming/esopull_semantic_helpers.cuh": "dd5a9091628b079efd97e53fd49e52d09dcee88529235db77a5648e02937af6e", + "src/CelerisLab/lbm/kernels/streaming/esopull_single_buffer.cuh": "77ed4ab9a0ebc5ff62a91a7c39066ef395391ae36e9040a69fb2bd8b1626dc79", + "src/CelerisLab/lbm/kernels/streaming/pull_double_buffer.cuh": "b65d741ed78b7b62f99531ba0c0ce1936663d8dc9640e7d7d16c707f1baf6549", + "src/CelerisLab/lbm/population_storage.py": "a6a86c29c52c892edfe4802fb773c387212e3f4cc86708d80e154986ddf88044", + "src/CelerisLab/lbm/raw_exchange.py": "8694202466eba848563e298656353b0f5db94e21a01fa05f51aad6dc2b9fd311", + "src/CelerisLab/lbm/refinement.py": "08c2a0557276a98cbcaa459d9bb7a9735d8bbbf64a6114c57af2fda61481a687", + "src/CelerisLab/lbm/refinement_stepper.py": "f2bb34fa90805ad21c3e7cf9f9a67d88d3ae3e249b4517d3898567b4bf9e364d", + "src/CelerisLab/lbm/refinement_tables.py": "fdd344024b89df3a83174dc810ca5e32db5e19eb8b1609f8f73b0821f56546b6", + "src/CelerisLab/lbm/stepper.py": "0dcb7cd7ae87e33e3dc67a38b6aa43042fbe673e37d9ad165342ee1d695933ce", + "src/CelerisLab/lbm/synchronized_remap.py": "7400fc4beaffd4649d6a9c524d38ce5fd6fa7eeb32eb5332e4030a4b6648f197", + "src/CelerisLab/simulation.py": "0a03904dc6de88667d7af699005a289adf8b09f0976cb0d099b041f4830a8f4c", + "tests/validation/data/refined_dfg/source_manifest.json": "e495948b10f8112c77ca67255122b5e8bc9e32570e8c899802afcb85e608fef4", + "tests/validation/periodic.py": "a2212c0e2705126327a5019e9cafe64eeca398da1b75061ff23630c9843697a6", + "tests/validation/re100_helpers.py": "289c6e01075ff361573f862b90cfd61e5980c9a8efcbc78aef69979b4004fd60", + "tests/validation/run_refined_dfg.py": "992169a38ab16ab20a9b9bb4a29853cafe1d6bc07adba0df435a104be1de7b68", + "tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c" + }, + "runtime_sha256": { + "tests/validation/experimental_full9/run_dfg.py": "400f6ff373623e5312c8dfae3cb8dc0172360e40feb2a9780443ec7f7ded6eea", + "tests/validation/uniform_srt_diagnostic/diagnose_uniform_srt.py": "56b9ef786001c7760abab8d870f6437d719b427c21964f00067188f5fb27caad", + "tests/validation/source_provenance.py": "6dfe8f2d819dba0e240ba761d31162a5033685d492d09db75eb2b8aaab3a5a4c", + "tests/validation/data/dfg_re100/reference_cycle.json": "073d1bda1629697efebad95667bfe80598e217d708dbffd7d5f6c6999379c55f" + }, + "experiment": "uniform_full9_dfg_sparse_pilot" +} diff --git a/tests/validation/experimental_full9/run_dfg.py b/tests/validation/experimental_full9/run_dfg.py new file mode 100644 index 0000000..9201995 --- /dev/null +++ b/tests/validation/experimental_full9/run_dfg.py @@ -0,0 +1,113 @@ +"""Uniform experimental full9 DFG pilot; CPU preflight unless explicitly admitted.""" +import argparse +import hashlib +import json +import math +import os +from pathlib import Path +import sys +import time + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[2] +sys.path.insert(0, str(HERE.parent / 'uniform_srt_diagnostic')) +import diagnose_uniform_srt as diagnostic + +VARIANT = 'experimental_regularized_full9' +OBSERVATION = 'uniform_sparse_pilot_v1' + + +def configuration(source): + if source.resolve() != ROOT.resolve(): + raise ValueError('Source must equal executing harness checkout') + cfg, body, p, manifest = diagnostic.source_config(source, HERE / 'dfg_source_manifest.json') + cfg['method']['inlet']['scheme'] = VARIANT + return cfg, body, p, manifest + + +def accuracy_pass(result): + """Standard later qualification predicate; pilot never qualifies accuracy.""" + return (result.get('healthy') is True and result.get('periodic') is True + and result.get('reference_comparison', {}).get('reference_agreement') is True) + + +def identity(manifest): + return dict(experimental_variant=VARIANT, collision='SRT', control='with_cylinder', + observation_mode=OBSERVATION, numerical_source_commit=manifest['commit'], + manifest_sha256=hashlib.sha256((HERE / 'dfg_source_manifest.json').read_bytes()).hexdigest()) + + +def verify_pilot(result, expected): + import run_refined_dfg as runner + runner.ensure_finite(result) + if any(result.get(k) != v for k, v in expected.items()): + raise ValueError('Pilot identity mismatch') + if (result.get('case') != 'uniform' or result.get('mode') != 'pilot' + or result.get('status') != 'pilot_complete' or result.get('healthy') is not True + or result.get('steps') != 40000 or result.get('executed_steps') != 40000): + raise ValueError('Incomplete or mislabeled pilot') + timings = result.get('calibration_batches_seconds', []) + if len(timings) != 5 or any(isinstance(v, bool) or not isinstance(v, (int, float)) or v <= 0 for v in timings): + raise ValueError('Missing positive calibration timings') + rows = result.get('history', []) + if len(rows) != 10: + raise ValueError('Incomplete sparse pilot history') + for i, row in enumerate(rows, 1): + for key in ('cd', 'cl', 'mass', 'flux_in', 'flux_out', 'dp'): + if isinstance(row.get(key), bool) or not isinstance(row.get(key), (int, float)): + raise ValueError('Missing numeric pilot observation') + if (row.get('step') != i * 4000 or row.get('tstar') != i * .5 + or row.get('tstar_force') != i * .5 - .25 + or not .5 <= row.get('rho_min', 0) <= row.get('rho_max', 2) <= 1.5 + or not 0 <= row.get('max_mach', 1) <= .2): + raise ValueError('Invalid sparse pilot observation') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--source', type=Path, default=ROOT) + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--execute', action='store_true') + parser.add_argument('--gpu-admitted', action='store_true') + parser.add_argument('--wall-seconds', type=float, default=180.) + a = parser.parse_args() + a.source = a.source.resolve() + cfg, body, p, manifest = configuration(a.source) + import run_refined_dfg as runner + from source_provenance import checkout_provenance + h, periodic = runner.helpers(a.source) + if not math.isfinite(a.wall_seconds) or a.wall_seconds <= 0: + raise ValueError('Finite positive wall ceiling required') + if a.execute and (not a.gpu_admitted or not os.environ.get('CUDA_VISIBLE_DEVICES')): + raise ValueError('Explicit GPU admission and device selection required') + if a.output.exists() and any(a.output.iterdir()): + raise ValueError('Use new/empty output directory') + a.output.mkdir(parents=True, exist_ok=True) + receipt = identity(manifest) + receipt['checkout'] = checkout_provenance(ROOT) + diagnostic.dump(a.output / 'preflight.json', dict(**receipt, config=cfg, body=body, + parameters=p, source=manifest, max_steps=40000, cap_tstar=5., + observation_width_tstar=.5, force_batch_steps=100, + calibration='First 100 steps warmup; next 400 timed, counted in physical trajectory', + gates=periodic.GATES, reference_gates=periodic.REFERENCE_GATES, + wall_seconds=a.wall_seconds, executed=False, accuracy_qualified=False)) + if not a.execute: + return 0 + a.mode = 'pilot' + a.deadline = time.monotonic() + a.wall_seconds + progress = {} + try: + result = runner.run_case(a, h, periodic, 'uniform', inlet_scheme=VARIANT, + sparse_pilot=True, receipt_identity=receipt, progress=progress) + except TimeoutError: + diagnostic.dump(a.output / 'result.json', dict(**receipt, status='wall_time_limit', + healthy=False, accuracy_qualified=False, **progress)) + return 3 + if result['healthy']: + verify_pilot(result, receipt) + diagnostic.dump(a.output / 'result.json', dict(**result, accuracy_qualified=False)) + return 0 if result['healthy'] else 2 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tests/validation/experimental_full9/test_dfg.py b/tests/validation/experimental_full9/test_dfg.py new file mode 100644 index 0000000..19c07bb --- /dev/null +++ b/tests/validation/experimental_full9/test_dfg.py @@ -0,0 +1,153 @@ +"""Focused host checks for the uniform experimental DFG pilot.""" +from copy import deepcopy +import importlib.util +import json +from pathlib import Path +import subprocess +import sys +from tempfile import TemporaryDirectory +import unittest +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import run_dfg as r + + +def test_factory_and_default(): + cfg, body, p, manifest = r.configuration(r.ROOT) + r.configuration(r.ROOT) + import run_refined_dfg as runner + h, _ = runner.helpers(r.ROOT) + old, old_body, old_p = runner.config(h, 80) + assert old['method']['inlet']['scheme'] == 'zou_he_local' + old['method']['inlet']['scheme'] = r.VARIANT + assert cfg == old and body == old_body and p == old_p + assert len(body['objects']) == 1 and p['diameter'] == 80 + assert cfg == runner.config(h, 80, r.VARIANT)[0] + assert manifest['commit'] == '926ca27407c26ca11283282a6e53249925725376' + + +def receipt(): + _, _, _, m = r.configuration(r.ROOT) + expected = r.identity(m) + rows = [dict(step=i*4000, tstar=i*.5, tstar_force=i*.5-.25, + rho_min=.99, rho_max=1.01, max_mach=.03, + cd=3., cl=.1, dp=2., mass=.8, flux_in=.41, flux_out=.41) + for i in range(1, 11)] + return expected, dict(**expected, case='uniform', mode='pilot', status='pilot_complete', + healthy=True, steps=40000, executed_steps=40000, history=rows, + calibration_batches_seconds=[.001]*5) + + +def test_receipts(): + expected, result = receipt() + r.verify_pilot(result, expected) + for key, value in [('collision', 'MRT'), ('experimental_variant', 'zou_he_local'), + ('observation_mode', 'first_health_pair'), ('control', 'without_cylinder'), + ('numerical_source_commit', 'other'), ('case', 'refined'), + ('steps', 39999), ('executed_steps', 39999), ('healthy', False)]: + bad = deepcopy(result); bad[key] = value + with unittest.TestCase().assertRaises(ValueError): r.verify_pilot(bad, expected) + for change in ('nan', 'missing', 'time', 'count'): + bad = deepcopy(result) + if change == 'nan': bad['history'][-1]['tstar'] = float('nan') + if change == 'missing': del bad['history'][0]['cd'] + if change == 'time': bad['history'][0]['tstar_force'] = .5 + if change == 'count': bad['history'].pop() + with unittest.TestCase().assertRaises(ValueError): r.verify_pilot(bad, expected) + + +def test_accuracy_requires_reference(): + assert not r.accuracy_pass(dict(healthy=True, periodic=True)) + assert r.accuracy_pass(dict(healthy=True, periodic=True, + reference_comparison=dict(reference_agreement=True))) + assert not r.accuracy_pass(dict(healthy=True, periodic=False, + reference_comparison=dict(reference_agreement=True))) + + +def test_preflight_and_source_guards(): + with TemporaryDirectory() as temp: + subprocess.run([sys.executable, str(Path(r.__file__)), '--output', temp], check=True) + out = json.loads((Path(temp)/'preflight.json').read_text()) + assert out['max_steps'] == 40000 and out['observation_width_tstar'] == .5 + assert not out['executed'] and not out['accuracy_qualified'] + assert out['wall_seconds'] == 180 and out['control'] == 'with_cylinder' + with unittest.TestCase().assertRaises(ValueError): r.configuration(r.ROOT.parent) + with unittest.TestCase().assertRaisesRegex(ValueError, 'Pinned source mismatch'): + r.diagnostic.source_config(r.ROOT, Path(r.__file__).with_name('source_manifest.json')) + + +def test_shared_source_semantics(): + r.configuration(r.ROOT) + import run_refined_dfg as runner + text = Path(runner.__file__).read_text() + assert 'sparse_pilot=False' in text + assert '(a.mode == "pilot" and not sparse_pilot)' in text + assert 'impulse += sim.read_force(0).astype(np.float64) * batch' in text + assert 'tstar_force=(begin + n / 2) * SPEED / p["diameter"]' in text + assert 'cd=float(impulse[0] / n / den)' in text + assert 'mass = float(rho[fluid].sum()) * p["dx"] ** 2' in text + assert 'executed_steps += batch' in text + assert 'First unhealthy observation; onset between observations is unknown' in text + + +def test_accounted_calibration_and_failure_progress(): + import numpy as np + import types + from unittest.mock import patch + r.configuration(r.ROOT) + import run_refined_dfg as runner + h, periodic = runner.helpers(r.ROOT) + _, _, p = runner.config(h, 80) + count = [0] + fail = [False] + class Simulation: + def __init__(self, *args, **kwargs): + self.field = types.SimpleNamespace(flag=np.ones(p['nx']*p['ny'], np.uint8)) + def __enter__(self): return self + def __exit__(self, *args): pass + def initialize(self): count[0] = 0 + def run(self, n): count[0] += n + def read_force(self, body): return np.array([.004, .002]) + def get_macroscopic(self): + shape = (p['ny'], p['nx']) + return dict(rho=np.full(shape, float('nan') if fail[0] else 1.), + ux=np.full(shape, .01), uy=np.zeros(shape)) + modules = { + 'CelerisLab.simulation': types.SimpleNamespace(Simulation=Simulation), + 'CelerisLab.cuda.context': types.SimpleNamespace(CudaContext=object), + 'CelerisLab.lbm.refinement_stepper': types.SimpleNamespace(FixedPatchStepper=object), + } + with patch.dict(sys.modules, modules), patch.object(h, 'pressure_probes', return_value=dict(dp=2.)): + with TemporaryDirectory() as temp: + args = types.SimpleNamespace(source=r.ROOT, output=Path(temp), mode='pilot', deadline=float('inf')) + progress = {} + result = runner.run_case(args, h, periodic, 'uniform', inlet_scheme=r.VARIANT, + sparse_pilot=True, progress=progress) + assert count[0] == 40000 and result['executed_steps'] == 40000 + assert len(result['history']) == 10 and len(result['calibration_batches_seconds']) == 5 + assert abs(result['history'][0]['cd'] - 1.) < 1e-14 + assert result['history'][0]['tstar_force'] == .25 + fail[0] = True + with TemporaryDirectory() as temp: + args.output = Path(temp) + result = runner.run_case(args, h, periodic, 'uniform', inlet_scheme=r.VARIANT, + sparse_pilot=True, progress=progress) + assert result['executed_steps'] == 4000 and result['steps'] == 0 + assert result['first_observed_failure_step'] == 4000 and result['last_observed_step'] == 0 + fail[0] = False + with TemporaryDirectory() as temp: + args.output = Path(temp) + def deadline(_): + if count[0] >= 500: raise TimeoutError('test deadline') + count[0] = 0 + with patch.object(runner, 'check_deadline', side_effect=deadline): + with unittest.TestCase().assertRaises(TimeoutError): + runner.run_case(args, h, periodic, 'uniform', inlet_scheme=r.VARIANT, + sparse_pilot=True, progress=progress) + assert progress['executed_steps'] == 500 and progress['last_observed_step'] == 0 + assert len(progress['calibration_batches_seconds']) == 5 + + +if __name__ == '__main__': + result = unittest.TextTestRunner(verbosity=2).run(unittest.TestSuite( + unittest.FunctionTestCase(fn) for name, fn in sorted(globals().items()) if name.startswith('test_'))) + raise SystemExit(not result.wasSuccessful()) diff --git a/tests/validation/run_refined_dfg.py b/tests/validation/run_refined_dfg.py index cc4acff..b7b0720 100644 --- a/tests/validation/run_refined_dfg.py +++ b/tests/validation/run_refined_dfg.py @@ -39,8 +39,8 @@ def helpers(source): return h, periodic -def config(h, diameter): - cfg, body, p = h.make_config(diameter, SPEED, "SRT", "neq_extrap", "zou_he_local") +def config(h, diameter, inlet_scheme="zou_he_local"): + cfg, body, p = h.make_config(diameter, SPEED, "SRT", "neq_extrap", inlet_scheme) cfg["method"]["outlet"]["srt_neq_damp"] = 0.5 return cfg, body, p @@ -159,7 +159,8 @@ def verify_source(source): return expected -def run_case(a, h, periodic, case): +def run_case(a, h, periodic, case, *, inlet_scheme="zou_he_local", + sparse_pilot=False, receipt_identity=None, progress=None): from CelerisLab.simulation import Simulation from CelerisLab.cuda.context import CudaContext from CelerisLab.lbm.refinement_stepper import FixedPatchStepper @@ -167,10 +168,18 @@ def run_case(a, h, periodic, case): from contextlib import ExitStack refined = case == "refined" - cfgdict, bodydict, p = config(h, 40 if refined else 80) + if refined and inlet_scheme != "zou_he_local": + raise ValueError("Experimental inlet is uniform-only") + cfgdict, bodydict, p = config(h, 40 if refined else 80, inlet_scheme) + identity = {} if receipt_identity is None else dict(receipt_identity) + calibration_batches = [] + executed_steps = 0 cap = 5.0 if a.mode == "pilot" else 300.0 max_steps = math.ceil(cap * p["diameter"] / SPEED) rows = [] + if progress is not None: + progress.update(executed_steps=0, last_observed_step=0, history=rows, + calibration_batches_seconds=calibration_batches) healthy = True step_seconds = 0.0 started = time.monotonic() @@ -233,7 +242,7 @@ def run_case(a, h, periodic, case): check_deadline(a.deadline) width = ( 0.02 - if a.mode == "pilot" or begin * SPEED / p["diameter"] >= 240 + if (a.mode == "pilot" and not sparse_pilot) or begin * SPEED / p["diameter"] >= 240 else 0.5 ) n = min(max(1, round(width * p["diameter"] / SPEED)), max_steps - begin) @@ -259,8 +268,14 @@ def run_case(a, h, periodic, case): while done < n: check_deadline(a.deadline) batch = min(100, n - done) + batch_started = time.monotonic() if sparse_pilot else None sim.run(batch) + executed_steps += batch + if progress is not None: + progress["executed_steps"] = executed_steps impulse += sim.read_force(0).astype(np.float64) * batch + if sparse_pilot and len(calibration_batches) < 5: + calibration_batches.append(time.monotonic() - batch_started) done += batch check_deadline(a.deadline) step_seconds += time.monotonic() - tick @@ -320,6 +335,8 @@ def run_case(a, h, periodic, case): check_deadline(a.deadline) ensure_finite(row) rows.append(row) + if progress is not None: + progress["last_observed_step"] = row["step"] if writer is None: writer = csv.DictWriter(out, fieldnames=row) writer.writeheader() @@ -335,6 +352,9 @@ def run_case(a, h, periodic, case): break if len(rows) == 1: ready = dict( + **identity, + calibration_batches_seconds=calibration_batches, + calibration_note="First 100 steps warmup, next 400 timed; all included in trajectory and force interval", case=case, mode=a.mode, step=begin, @@ -347,6 +367,12 @@ def run_case(a, h, periodic, case): ) print("READY " + json.dumps(ready, allow_nan=False), flush=True) result = dict( + **identity, + calibration_batches_seconds=calibration_batches, + first_observed_failure_step=executed_steps if not healthy and not refined else None, + last_observed_step=rows[-1]["step"] if rows else 0, + failure_timing="First unhealthy observation; onset between observations is unknown" if not healthy else None, + executed_steps=executed_steps if not refined else begin, case=case, mode=a.mode, history=rows,