test: prepare isolated uniform full9 DFG pilot

This commit is contained in:
2026-10-18 09:41:10 -07:00
parent 939c10c867
commit c0be841b2e
5 changed files with 444 additions and 5 deletions
@@ -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=<admitted-device> 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.
@@ -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"
}
@@ -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())
@@ -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())
+31 -5
View File
@@ -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,