完善通用求解器回归与前端交互

- 引入因果坐标内核、热流体恢复和递进长时回归\n- 完善正交连线、线桥、视图保持与结果曲线缩放\n- 补充依赖约束、CI、测试基线和北京时间更新日志
This commit is contained in:
lujingze committed 2026-08-18 06:42:07 +00:00
1 parent 143e8dd309
commit b435daecf2
65 files changed
+172271 -701

No files matched your search

@@ -0,0 +1,592 @@
{
"schemaVersion": 1,
"id": "test_mql_8-production-0.2s-physical-state-v2",
"caseId": "0.2s",
"lane": "production",
"sourceXmlSha256": "170463d65d074da01f0f9e9dab730b3815c94c1cc80b5190ec2e3fe623da74d3",
"approval": {
"status": "approved",
"approvedAt": "2026-08-17T14:11:10.904892+00:00",
"basis": "Initial P0 bounded production baseline passed finite-series, checkpoint, signal-segment, mechanical-event, residual, audit, and fallback gates."
},
"provenance": {
"sourceReport": {
"path": "tests/baselines/simulation/test_mql_8/runs/2026-08-17-production-v2-0.2.json",
"sha256": "93baac975413703bebe940308ccd0de5dbd753f7c1c4fe015907315f112618a5",
"generatedAt": "2026-08-17T14:11:10.904892+00:00",
"metadataCompatibility": {
"status": "current",
"differences": []
}
}
},
"physicalLayout": {
"projectionCategories": [
"state"
],
"stateKeys": [
"amesim_mecmas21_1.a",
"amesim_mecmas21_1.v",
"amesim_mecmas21_1.x",
"amesim_mecmas21_10.a",
"amesim_mecmas21_10.v",
"amesim_mecmas21_10.x",
"amesim_mecmas21_2.a",
"amesim_mecmas21_2.v",
"amesim_mecmas21_2.x",
"amesim_mecmas21_3.a",
"amesim_mecmas21_3.v",
"amesim_mecmas21_3.x",
"amesim_mecmas21_4.a",
"amesim_mecmas21_4.v",
"amesim_mecmas21_4.x",
"amesim_mecmas21_5.a",
"amesim_mecmas21_5.v",
"amesim_mecmas21_5.x",
"amesim_mecmas21_6.a",
"amesim_mecmas21_6.v",
"amesim_mecmas21_6.x",
"amesim_mecmas21_7.a",
"amesim_mecmas21_7.v",
"amesim_mecmas21_7.x",
"amesim_mecmas21_8.a",
"amesim_mecmas21_8.v",
"amesim_mecmas21_8.x",
"amesim_mecmas21_9.a",
"amesim_mecmas21_9.v",
"amesim_mecmas21_9.x",
"amesim_pnch012_10.U",
"amesim_pnch012_10.m",
"amesim_pnch012_11.U",
"amesim_pnch012_11.m",
"amesim_pnch012_12.U",
"amesim_pnch012_12.m",
"amesim_pnch012_13.U",
"amesim_pnch012_13.m",
"amesim_pnch012_14.U",
"amesim_pnch012_14.m",
"amesim_pnch012_15.U",
"amesim_pnch012_15.m",
"amesim_pnch012_8.U",
"amesim_pnch012_8.m",
"amesim_pnch012_9.U",
"amesim_pnch012_9.m",
"amesim_pnch023_1.U",
"amesim_pnch023_1.m",
"amesim_pnch023_2.U",
"amesim_pnch023_2.m",
"amesim_pnch023_3.U",
"amesim_pnch023_3.m",
"amesim_pnch023_4.U",
"amesim_pnch023_4.m",
"amesim_pnl0001_1.U",
"amesim_pnl0001_1.m",
"amesim_pnl0001_10.U",
"amesim_pnl0001_10.m",
"amesim_pnl0001_11.U",
"amesim_pnl0001_11.m",
"amesim_pnl0001_13.U",
"amesim_pnl0001_13.m",
"amesim_pnl0001_14.U",
"amesim_pnl0001_14.m",
"amesim_pnl0001_15.U",
"amesim_pnl0001_15.m",
"amesim_pnl0001_16.U",
"amesim_pnl0001_16.m",
"amesim_pnl0001_17.U",
"amesim_pnl0001_17.m",
"amesim_pnl0001_18.U",
"amesim_pnl0001_18.m",
"amesim_pnl0001_19.U",
"amesim_pnl0001_19.m",
"amesim_pnl0001_2.U",
"amesim_pnl0001_2.m",
"amesim_pnl0001_20.U",
"amesim_pnl0001_20.m",
"amesim_pnl0001_4.U",
"amesim_pnl0001_4.m",
"amesim_pnl0001_5.U",
"amesim_pnl0001_5.m",
"amesim_pnl0001_7.U",
"amesim_pnl0001_7.m",
"amesim_pnl0001_9.U",
"amesim_pnl0001_9.m",
"amesim_pnl0002_1.U",
"amesim_pnl0002_1.m",
"amesim_pnl0002_2.U",
"amesim_pnl0002_2.m",
"amesim_pnl0002_3.U",
"amesim_pnl0002_3.m",
"amesim_pnl0002_4.U",
"amesim_pnl0002_4.m",
"amesim_pnl0002_5.U",
"amesim_pnl0002_5.m",
"amesim_pnl0002_6.U",
"amesim_pnl0002_6.m",
"amesim_pnl0002_7.U",
"amesim_pnl0002_7.m",
"amesim_pnl0002_8.U",
"amesim_pnl0002_8.m",
"amesim_pnl0003_1.U1",
"amesim_pnl0003_1.U2",
"amesim_pnl0003_1.m1",
"amesim_pnl0003_1.m2",
"amesim_pnl0003_2.U1",
"amesim_pnl0003_2.U2",
"amesim_pnl0003_2.m1",
"amesim_pnl0003_2.m2",
"amesim_pnl0003_3.U1",
"amesim_pnl0003_3.U2",
"amesim_pnl0003_3.m1",
"amesim_pnl0003_3.m2",
"amesim_pnl0003_4.U1",
"amesim_pnl0003_4.U2",
"amesim_pnl0003_4.m1",
"amesim_pnl0003_4.m2",
"amesim_pnl0003_5.U1",
"amesim_pnl0003_5.U2",
"amesim_pnl0003_5.m1",
"amesim_pnl0003_5.m2",
"amesim_pnl0003_6.U1",
"amesim_pnl0003_6.U2",
"amesim_pnl0003_6.m1",
"amesim_pnl0003_6.m2",
"amesim_pnl0003_7.U1",
"amesim_pnl0003_7.U2",
"amesim_pnl0003_7.m1",
"amesim_pnl0003_7.m2",
"amesim_pnl0003_8.U1",
"amesim_pnl0003_8.U2",
"amesim_pnl0003_8.m1",
"amesim_pnl0003_8.m2"
],
"stateKeyLayoutSha256": "12e216303d33639a5f53ef83b6b9fc5851beffb1222a0ed4bb5e1a2432b3d739"
},
"tolerance": {
"relative": 0.0002,
"absolute": 1e-09,
"checkpointTimeAbsoluteSeconds": 1e-12
},
"physicalCheckpoints": [
{
"requestedTime": 0.0,
"values": [
-0.8167936695810981,
0.0,
0.0,
0.0,
0.0,
0.0,
-0.8167936695810981,
0.0,
0.0,
-0.8167936695810981,
0.0,
0.0,
-0.8167936695810981,
0.0,
0.0,
-0.8167936695810981,
0.0,
0.0,
-0.8167936695810981,
0.0,
0.0,
-0.8167936695810981,
0.0,
0.0,
-0.8167936695810981,
0.0,
0.0,
0.0,
0.0,
0.0,
-1563.893548405841,
0.002462855848963059,
-1563.893548405841,
0.002462855848963059,
-1563.893548405841,
0.002462855848963059,
-1563.893548405841,
0.002462855848963059,
-1563.893548405841,
0.002462855848963059,
-1563.893548405841,
0.002462855848963059,
-1563.893548405841,
0.002462855848963059,
-1563.893548405841,
0.002462855848963059,
-886868.3937230138,
1.374510850335198,
-886868.3937230138,
1.374510850335198,
-886868.3937230138,
1.374510850335198,
-886868.3937230138,
1.374510850335198,
-2395.136531414716,
0.0037120966016654976,
-2395.136531414716,
0.0037120966016654976,
-2395.136531414716,
0.0037120966016654976,
-32.754109884455104,
5.158193227968668e-05,
-32.754109884455104,
5.158193227968668e-05,
-32.754109884455104,
5.158193227968668e-05,
-32.754109884455104,
5.158193227968668e-05,
-32.754109884455104,
5.158193227968668e-05,
-32.754109884455104,
5.158193227968668e-05,
-32.754109884455104,
5.158193227968668e-05,
-2395.136531414716,
0.0037120966016654976,
-32.754109884455104,
5.158193227968668e-05,
-2395.136531414716,
0.0037120966016654976,
-2395.136531414716,
0.0037120966016654976,
-2395.136531414716,
0.0037120966016654976,
-2395.136531414716,
0.0037120966016654976,
-65.50821976891021,
0.00010316386455937335,
-65.50821976891021,
0.00010316386455937335,
-65.50821976891021,
0.00010316386455937335,
-65.50821976891021,
0.00010316386455937335,
-65.50821976891021,
0.00010316386455937335,
-65.50821976891021,
0.00010316386455937335,
-65.50821976891021,
0.00010316386455937335,
-65.50821976891021,
0.00010316386455937335,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627
]
},
{
"requestedTime": 0.04,
"values": [
-0.7201778435512728,
-0.03131260841232088,
-0.0006393186923261844,
0.0,
6.421450189245049e-22,
0.0,
-0.7201733833792814,
-0.031312355811718325,
-0.0006393136520185212,
-0.7201718196391171,
-0.03131233882672457,
-0.0006393140197282858,
-0.7201722508603697,
-0.031312278047383596,
-0.0006393124896720936,
-0.7201744791266093,
-0.03131218496519997,
-0.0006393087827851101,
-0.7201783488656359,
-0.031314094334835896,
-0.0006393469119830168,
-0.7201775030345068,
-0.031312389100822,
-0.0006393118191591414,
-0.7201771803079525,
-0.031312911030261764,
-0.0006393266351878673,
0.0,
0.0,
1.120876769981429e-26,
-1563.233367712832,
0.0024627146762544114,
-1563.2343003288415,
0.0024627154840700283,
-1563.2343310045646,
0.002462715544028645,
-1563.234317887853,
0.002462715540851028,
-1563.2342832229026,
0.0024627154834822402,
-1563.234128076657,
0.0024627152649548067,
-1563.2340241256454,
0.0024627151889310217,
-1563.234154194445,
0.0024627153106296135,
-886868.3937230138,
1.374510850335198,
-886868.3937230138,
1.374510850335198,
-886868.3937230138,
1.374510850335198,
-886868.3937230138,
1.374510850335198,
-2395.136531414716,
0.0037120966016654976,
-2395.136531414716,
0.0037120966016654976,
-2395.136531414716,
0.0037120966016654976,
-32.755346551358684,
5.162917551035796e-05,
-32.7553713095164,
5.162919361109966e-05,
-32.755371520495,
5.1629194498694165e-05,
-32.7553740517406,
5.162919592464222e-05,
-32.75549815317228,
5.16292749500248e-05,
-32.7557671078149,
5.1629447239129814e-05,
-32.755537600705786,
5.162929920389579e-05,
-2395.136531414716,
0.0037120966016654976,
-32.7554490658762,
5.1629242163634275e-05,
-2395.136531414716,
0.0037120966016654976,
-2395.136531414716,
0.0037120966016654976,
-2395.136531414716,
0.0037120966016654976,
-2395.136531414716,
0.0037120966016654976,
-65.50992369305769,
0.0001032573549102297,
-65.50924422087368,
0.00010325691856038897,
-65.50924420634506,
0.00010325691908177892,
-65.50924789343988,
0.00010325692011611614,
-65.50952699017678,
0.00010325709663260638,
-65.50953538930851,
0.00010325710204152489,
-65.50932045418728,
0.00010325696357174907,
-65.50995769649337,
0.00010325737519785258,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627,
-733.2050606371581,
-733.2050606371581,
0.0011363561025506627,
0.0011363561025506627
]
},
{
"requestedTime": 0.2,
"values": [
2.6370963314548135e-06,
1.1907436243229137e-06,
1.5914303294371521e-07,
0.0,
6.530827087152731e-22,
6.105198566652894e-27,
2.6364088989794256e-06,
1.1908086349821896e-06,
1.5912507766913015e-07,
2.636304998304695e-06,
1.1908227907789107e-06,
1.5912158717247314e-07,
2.6363827055320143e-06,
1.1908285194387939e-06,
1.5912015990557016e-07,
2.6363664073869586e-06,
1.1908301556228963e-06,
1.5911975015798518e-07,
2.6362371863797307e-06,
1.1908285541103296e-06,
1.5912015181363026e-07,
2.6363282813690604e-06,
1.190822777157153e-06,
1.5912159035091038e-07,
2.636463905218989e-06,
1.1908086605158301e-06,
1.5912507172531535e-07,
0.0,
1.409251022153514e-21,
2.583112155689631e-23,
-21824.884876930104,
0.06638877583716687,
-21824.8394513789,
0.06638874206868575,
-21824.88503639051,
0.06638877594459791,
-21825.04659229254,
0.06638889465525798,
-21825.449220760413,
0.06638918449710254,
-21860.3893940624,
0.06641207557540514,
-21825.44912779144,
0.06638918443617388,
-21825.046674584086,
0.06638889470996256,
-877968.2311707166,
1.2399623843859275,
-877968.2314488764,
1.239962386668905,
-877968.231641228,
1.2399623882475983,
-877968.2313630553,
1.23996238596452,
-2232.5529371119105,
0.0031580856642891347,
-2232.552938579765,
0.003158085669643602,
-2232.552934122746,
0.0031580856635922043,
-1392.944729430734,
0.002032422883230558,
-1391.7199124494437,
0.0020316357664157303,
-1391.7199702457754,
0.002031635646355635,
-1391.7199593465837,
0.00203163554240735,
-1391.7199471015297,
0.0020316355018333084,
-1391.719956464844,
0.0020316355405555213,
-1391.719971723481,
0.0020316356473075348,
-2232.5529379989352,
0.0031580856654872167,
-1391.7199093676563,
0.0020316357644361133,
-2232.552933678534,
0.003158085664480916,
-2232.552938069336,
0.0031580856704486304,
-2232.5529342591235,
0.003158085668637221,
-2232.5529350799816,
0.003158085669751649,
-145.14613577820415,
0.0023684760609916228,
-234.44464213523725,
0.002425822913152506,
-234.44208866732288,
0.0024258210179891326,
-234.43920235483773,
0.0024258190341378137,
-234.43948538748822,
0.0024258192159024635,
-234.4423567512829,
0.0024258211901560865,
-234.44466632040084,
0.002425822928687371,
-145.14616131190638,
0.0023684760773897006,
-665.721080447237,
-664.9052921895398,
0.0009423231603427254,
0.0009411988264117098,
-665.721080812701,
-664.9052927495628,
0.0009423231608531282,
0.0009411988270450505,
-665.7210807465625,
-664.9052926367955,
0.0009423231622138283,
0.0009411988283754829,
-665.7210793863333,
-664.9052912142575,
0.0009423231613560865,
0.0009411988274784932,
-665.7210790083244,
-664.9052908966471,
0.0009423231608560905,
0.0009411988270168083,
-665.721081019834,
-664.9052928469027,
0.0009423231621245239,
0.0009411988282463315,
-665.7210790496954,
-664.9052910102263,
0.0009423231594614548,
0.0009411988256686023,
-665.7210788464794,
-664.9052906188476,
0.0009423231596132578,
0.0009411988257017965
]
}
],
"outputContract": {
"sha256": "3a5cdcf25a6b841f7963044c5ecd1e8a3744fac98000eda53df2956ad0ac6506"
}
}
@@ -0,0 +1,198 @@
{
"schemaVersion": 1,
"id": "test_mql_8-progressive-v2",
"description": "Primary generic-solver regression target for the renamed 0.01 s-grid authoritative inputs, with a short smoke gate before staged extension.",
"source": {
"path": "tests/data/test-mql-8.xml",
"sha256": "170463d65d074da01f0f9e9dab730b3815c94c1cc80b5190ec2e3fe623da74d3",
"bytes": 101013,
"xmlIsAuthoritative": true,
"companionProject": {
"path": "tests/data/test-mql-8.json",
"sha256": "258c50ee4850baa72fb7c2cc24536d0a631fc6a7f1fa6cedb7b6eea7c857cbaa",
"bytes": 264675,
"executionInput": false
},
"simulation": {
"tStart": 0.0,
"tStop": 0.2,
"sampleStep": 0.01,
"maxStep": 0.01,
"method": "BDF"
}
},
"historicalReports": [
{
"path": "tests/baselines/simulation/test_mql_8/runs/2026-08-17-solver-only-v1.json",
"sha256": "ed0e8514c5fc083af6d403270eb0617fc20672ebb96460c355757d28d7059d0f",
"sourceXmlSha256": "42e2d6277d39bd5990cb322243bec8d71c40ff0696b440db3c8623f2ab15c2ee",
"status": "historicalOnly",
"compatibleWithCurrentSource": false,
"reason": "This report used the former spaced filename and 0.002 s sampleStep/maxStep. It is retained only as historical performance evidence and must never seed a v2 golden."
},
{
"path": "tests/baselines/simulation/test_mql_8/runs/2026-08-17-extension-decision.json",
"sha256": "d067bab69418ec21a11481f4a2c1fcb1ba1fc9528faeeb163ea5f466c2e2987f",
"sourceXmlSha256": "42e2d6277d39bd5990cb322243bec8d71c40ff0696b440db3c8623f2ab15c2ee",
"status": "historicalOnly",
"compatibleWithCurrentSource": false,
"reason": "This deferred-stage decision was calculated from the former 0.002 s source report and is not a runtime prediction for the renamed v2 source."
}
],
"structure": {
"componentCount": 152,
"connectionCount": 174,
"dynamicComponentCount": 54,
"stateCount": 124,
"resultVariableCount": 1716,
"pressureFlowUnknownCount": 760,
"pressureFlowEquationCount": 760,
"pressureFlowIsSquare": true,
"logicalEffortCoordinateCount": 112,
"eliminatedEffortAliasCount": 320,
"canonicalCoordinateCount": 440,
"compatibilityScatterCount": 760,
"jacobianNonzeroCount": 3296,
"jacobianColorGroupCount": 52,
"hasMechanicalStateEvents": true
},
"sequence": [
"0.01s-smoke",
"0.2s",
"1s",
"5s",
"10s"
],
"lanes": {
"solver-only": {
"description": "Algorithm iteration and staged-extension lane: same XML and method, with an in-memory 0.02 s output grid and 0.05 s maximum step; signal breakpoints remain explicit solver segments.",
"samplingMode": "fixed",
"sampleStep": 0.02,
"maxStepMode": "fixed",
"maxStep": 0.05,
"instrumentationMode": "standard",
"productionEquivalentOutput": false
},
"production": {
"description": "Acceptance lane: changes only tStop and preserves the source 0.01 s output and internal-step grid.",
"samplingMode": "source",
"maxStepMode": "source",
"instrumentationMode": "standard",
"productionEquivalentOutput": true
}
},
"variants": {
"0.01s-smoke": {
"stopTime": 0.01,
"softTimeoutSeconds": 60.0,
"hardTimeoutSeconds": 90.0,
"useForRuntimePrediction": false,
"checkpointTimes": [
0.0,
0.01
],
"expectedSignalEventTimes": []
},
"0.2s": {
"stopTime": 0.2,
"softTimeoutSeconds": 300.0,
"hardTimeoutSeconds": 360.0,
"checkpointTimes": [
0.0,
0.04,
0.2
],
"expectedSignalEventTimes": [
0.04
],
"expectedMechanicalTransitionTimes": [],
"goldens": {
"production": {
"path": "tests/baselines/simulation/test_mql_8/goldens/production-0.2s-v1.json",
"sha256": "99fa7b3631a89f59f86551847175f3701a1567d8d0bd10d87545734f17515d72"
}
}
},
"1s": {
"stopTime": 1.0,
"softTimeoutSeconds": 1200.0,
"hardTimeoutSeconds": 1260.0,
"checkpointTimes": [
0.0,
0.04,
0.8,
1.0
],
"expectedSignalEventTimes": [
0.04,
0.8
]
},
"5s": {
"stopTime": 5.0,
"softTimeoutSeconds": 2700.0,
"hardTimeoutSeconds": 2760.0,
"checkpointTimes": [
0.0,
0.04,
0.8,
1.0,
2.0,
5.0
],
"expectedSignalEventTimes": [
0.04,
0.8
]
},
"10s": {
"stopTime": 10.0,
"softTimeoutSeconds": 5400.0,
"hardTimeoutSeconds": 5460.0,
"checkpointTimes": [
0.0,
0.04,
0.8,
1.0,
2.0,
5.0,
10.0
],
"expectedSignalEventTimes": [
0.04,
0.8
]
}
},
"correctness": {
"status": "partial",
"maximumScaledResidual": 1e-7,
"requireFiniteSeries": true,
"requireStrictlyIncreasingTimes": true,
"physicalProjectionCategories": [
"state"
],
"stateRelativeTolerance": 0.0002,
"stateAbsoluteTolerance": 1e-9,
"checkpointTimeAbsoluteToleranceSeconds": 1e-12,
"eventTimeAbsoluteToleranceSeconds": 0.00002,
"signalEventTimeAbsoluteToleranceSeconds": 1e-12,
"mechanicalTransitionTimesAvailable": true,
"note": "Physical state checkpoints/events and the value-free output-shape contract are separate. The production 0.2 s variant has an approved state golden; longer horizons and critical algebraic projections remain pending."
},
"execution": {
"causalExecutorV2Default": true,
"environment": {
"SIMULATION_CAUSAL_EXECUTOR_V2": "1",
"SIMULATION_CAUSAL_COORDINATE_KERNEL": "1",
"SIMULATION_CAUSAL_FAST_PATH": "1",
"SIMULATION_ODE_JACOBIAN_MODE": "scipy",
"SIMULATIONAPP_PROPERTY_CACHE": "on"
},
"predictionSafetyFactor": 1.5,
"terminationGraceSeconds": 10.0,
"deferAfterFailureOrTimeout": true,
"deferWhenPredictedWallExceedsSoftTimeout": true,
"longTestEnvironmentVariable": "RUN_TEST_MQL_8_LONG_REGRESSION"
}
}
@@ -0,0 +1,34 @@
{
"schemaVersion": 1,
"kind": "progressive-extension-decision",
"sourceReport": "2026-08-17-solver-only-v1.json",
"sourceReportSha256": "ed0e8514c5fc083af6d403270eb0617fc20672ebb96460c355757d28d7059d0f",
"lane": "solver-only",
"observedCase": {
"caseId": "0.2s",
"outcome": "completed",
"wallSeconds": 132.30459557846189,
"acceptancePassed": true
},
"predictionSafetyFactor": 1.5,
"decisions": [
{
"caseId": "1s",
"outcome": "deferred",
"predictedWallSeconds": 992.2844668384642,
"softTimeoutSeconds": 900.0,
"reason": "predictedWallExceedsSoftBudget"
},
{
"caseId": "5s",
"outcome": "deferred",
"reason": "predecessorDeferred"
},
{
"caseId": "10s",
"outcome": "deferred",
"reason": "predecessorDeferred"
}
],
"simulationWasNotStartedForDeferredCases": true
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,37 @@
{
"schemaVersion": 1,
"kind": "progressive-extension-decision",
"manifestId": "test_mql_8-progressive-v2",
"sourceXmlSha256": "170463d65d074da01f0f9e9dab730b3815c94c1cc80b5190ec2e3fe623da74d3",
"sourceReport": "2026-08-17-production-v2-0.2.json",
"sourceReportSha256": "93baac975413703bebe940308ccd0de5dbd753f7c1c4fe015907315f112618a5",
"lane": "production",
"observedCase": {
"caseId": "0.2s",
"outcome": "completed",
"workerWallSeconds": 135.82402778230608,
"orchestrationWallSeconds": 136.63526012934744,
"acceptancePassed": true
},
"predictionSafetyFactor": 1.5,
"decisions": [
{
"caseId": "1s",
"outcome": "deferred",
"predictedWallSeconds": 1018.6802083672956,
"softTimeoutSeconds": 900.0,
"reason": "predictedWallExceedsSoftBudget"
},
{
"caseId": "5s",
"outcome": "deferred",
"reason": "predecessorDeferred"
},
{
"caseId": "10s",
"outcome": "deferred",
"reason": "predecessorDeferred"
}
],
"simulationWasNotStartedForDeferredCases": true
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,120 @@
{
"schemaVersion": 1,
"id": "test_mql_full_branches-historical-v1",
"description": "Historical slow-region and 2.05 s regression target for the generic solver.",
"source": {
"path": "tests/data/test_mql-full-branches-01-04.xml",
"sha256": "2fb95e65f5de0c85a6a17802aef74ea004087323fd00fd8d01acf0184ff71d48",
"bytes": 58860,
"xmlIsAuthoritative": true,
"simulation": {
"tStart": 0.0,
"tStop": 0.81,
"sampleStep": 0.01,
"maxStep": 0.81,
"method": "BDF"
}
},
"structure": {
"componentCount": 98,
"connectionCount": 106,
"dynamicComponentCount": 33,
"stateCount": 74,
"resultVariableCount": 1021,
"pressureFlowUnknownCount": 472,
"pressureFlowEquationCount": 472,
"pressureFlowIsSquare": true,
"logicalEffortCoordinateCount": 68,
"eliminatedEffortAliasCount": 204,
"canonicalCoordinateCount": 268,
"compatibilityScatterCount": 472,
"jacobianNonzeroCount": 1284,
"jacobianColorGroupCount": 31,
"hasMechanicalStateEvents": true
},
"sequence": [
"0.81s",
"2.10s"
],
"lanes": {
"solver-only": {
"description": "Historical solver regression using the authoritative 0.01 s output grid.",
"samplingMode": "fixed",
"sampleStep": 0.01,
"instrumentationMode": "standard",
"productionEquivalentOutput": true
},
"production": {
"description": "Acceptance lane preserving the authoritative XML output grid.",
"samplingMode": "source",
"instrumentationMode": "standard",
"productionEquivalentOutput": true
}
},
"variants": {
"0.81s": {
"stopTime": 0.81,
"softTimeoutSeconds": 180.0,
"hardTimeoutSeconds": 240.0,
"checkpointTimes": [
0.0,
0.04,
0.69,
0.70,
0.8,
0.81
],
"expectedSignalEventTimes": [
0.04,
0.8
],
"expectedMechanicalTransitionTimes": []
},
"2.10s": {
"stopTime": 2.1,
"softTimeoutSeconds": 300.0,
"hardTimeoutSeconds": 360.0,
"checkpointTimes": [
0.0,
0.04,
0.69,
0.70,
0.8,
1.0,
2.0,
2.05,
2.1
],
"expectedSignalEventTimes": [
0.04,
0.8
]
}
},
"correctness": {
"status": "partial",
"maximumScaledResidual": 1e-7,
"requireFiniteSeries": true,
"requireStrictlyIncreasingTimes": true,
"stateRelativeTolerance": 0.0002,
"eventTimeAbsoluteToleranceSeconds": 0.00002,
"signalEventTimeAbsoluteToleranceSeconds": 1e-12,
"mechanicalTransitionTimesAvailable": true,
"note": "Historical checkpoints are recorded; approved physical-state-v2 golden values remain pending."
},
"execution": {
"causalExecutorV2Default": true,
"environment": {
"SIMULATION_CAUSAL_EXECUTOR_V2": "1",
"SIMULATION_CAUSAL_COORDINATE_KERNEL": "1",
"SIMULATION_CAUSAL_FAST_PATH": "1",
"SIMULATION_ODE_JACOBIAN_MODE": "scipy",
"SIMULATIONAPP_PROPERTY_CACHE": "on"
},
"predictionSafetyFactor": 1.5,
"terminationGraceSeconds": 10.0,
"deferAfterFailureOrTimeout": true,
"deferWhenPredictedWallExceedsSoftTimeout": true,
"longTestEnvironmentVariable": "RUN_TEST_MQL_FULL_BRANCHES_LONG_REGRESSION"
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+258 -2
View File
@@ -3,8 +3,13 @@ from __future__ import annotations
import unittest
from app.simulation.components.amesim.flow.pipes import AmesimPnl00r
from app.simulation.components.amesim.junctions.nodes import AmesimPn3Node2
from app.simulation.components.experimental.storage.cylinder import Cylinder
from app.simulation.components.experimental.storage.tank import Tank
from app.simulation.core.medium import IdealGasMedium
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
from app.simulation.solvers.stream import StreamResolver
from app.simulation.systems.network import SimulationNetwork
class AmesimPnl00rComponentTests(unittest.TestCase):
@@ -32,6 +37,23 @@ class AmesimPnl00rComponentTests(unittest.TestCase):
{"gi": 1.5},
)
def test_initial_flow_temperature_reference_preserves_default_behavior(self) -> None:
pipe = AmesimPnl00r("pnl_1", self.medium)
initial_h = self.medium.specific_enthalpy(self.medium.T_ref)
self.assertEqual(
pipe._connected_h,
{"port_1": initial_h, "port_2": initial_h},
)
self.assertAlmostEqual(pipe._port_temperature("port_1"), self.medium.T_ref)
self.assertAlmostEqual(pipe._port_temperature("port_2"), self.medium.T_ref)
forward = pipe.mass_flow(501000.0, 500000.0)
reverse = pipe.mass_flow(500000.0, 501000.0)
self.assertGreater(forward, 0.0)
self.assertLess(reverse, 0.0)
self.assertAlmostEqual(forward, -reverse, delta=abs(forward) * 0.02)
def test_zero_pressure_drop_has_zero_flow_and_finite_results(self) -> None:
pipe = AmesimPnl00r("pnl_1", self.medium)
pipe.port_1.p = 100000.0
@@ -85,14 +107,24 @@ class AmesimPnl00rComponentTests(unittest.TestCase):
le=1.0,
rr=0.045 / 14.0,
)
pipe.port_1.h_outflow = self.medium.specific_enthalpy(300.0)
pipe.update_flow_temperature_references(
{
"port_1": self.medium.specific_enthalpy(300.0),
"port_2": self.medium.specific_enthalpy(300.0),
}
)
pipe._mass_flow_for_pressure_drop.cache_clear()
first = pipe.mass_flow(501000.0, 500000.0)
after_first = pipe._mass_flow_for_pressure_drop.cache_info()
second = pipe.mass_flow(501000.0, 500000.0)
after_second = pipe._mass_flow_for_pressure_drop.cache_info()
pipe.port_1.h_outflow = self.medium.specific_enthalpy(400.0)
pipe.update_flow_temperature_references(
{
"port_1": self.medium.specific_enthalpy(400.0),
"port_2": self.medium.specific_enthalpy(300.0),
}
)
third = pipe.mass_flow(501000.0, 500000.0)
after_temperature_change = pipe._mass_flow_for_pressure_drop.cache_info()
@@ -105,6 +137,230 @@ class AmesimPnl00rComponentTests(unittest.TestCase):
after_second.misses + 1,
)
def test_stream_outflows_are_crossed_but_flow_temperature_is_same_side(self) -> None:
pipe = AmesimPnl00r("pnl_1", self.medium)
hot_h = self.medium.specific_enthalpy(420.0)
cold_h = self.medium.specific_enthalpy(240.0)
pipe.update_stream_outflows({"port_1": hot_h, "port_2": cold_h})
self.assertEqual(pipe.port_1.h_outflow, cold_h)
self.assertEqual(pipe.port_2.h_outflow, hot_h)
self.assertEqual(pipe._connected_h, {"port_1": hot_h, "port_2": cold_h})
self.assertAlmostEqual(pipe._port_temperature("port_1"), 420.0)
self.assertAlmostEqual(pipe._port_temperature("port_2"), 240.0)
warmer_h = self.medium.specific_enthalpy(460.0)
pipe.update_flow_temperature_references(
{"port_1": warmer_h, "port_2": cold_h}
)
self.assertEqual(
pipe._connected_h,
{"port_1": warmer_h, "port_2": cold_h},
)
self.assertAlmostEqual(pipe._port_temperature("port_1"), 460.0)
# Updating the pressure-flow-only reference must not change the
# zero-volume component's already propagated connector outflows.
self.assertEqual(pipe.port_1.h_outflow, cold_h)
self.assertEqual(pipe.port_2.h_outflow, hot_h)
def test_stream_closure_refreshes_same_side_cache_before_recomputed_flow(self) -> None:
hot_temperature = 420.0
cold_temperature = 240.0
source = Cylinder(
"source",
self.medium,
V=0.02,
p0=501000.0,
T0=hot_temperature,
)
pipe = AmesimPnl00r("pnl_1", self.medium)
sink = Tank(
"sink",
self.medium,
V=0.05,
p0=500000.0,
T0=cold_temperature,
)
network = SimulationNetwork("pnl00r-stream-refresh")
for component in (source, pipe, sink):
network.add_component(component)
network.connect("source", "port_b", "pnl_1", "port_1")
network.connect("pnl_1", "port_2", "sink", "port_a")
diagnostics, connected_h = StreamResolver(network).solve()
self.assertTrue(diagnostics.converged)
self.assertEqual(pipe._connected_h, connected_h["pnl_1"])
self.assertAlmostEqual(pipe._port_temperature("port_1"), hot_temperature)
self.assertAlmostEqual(pipe._port_temperature("port_2"), cold_temperature)
# The pressure-flow layer runs again after this stream closure pass.
# Its first recomputation therefore observes the freshly cached source
# enthalpy, independently of the crossed connector outflow values.
pipe.port_1.p = source.port_b.p
pipe.port_2.p = sink.port_a.p
flow = pipe.mass_flow(pipe.port_1.p, pipe.port_2.p)
self.assertGreater(flow, 0.0)
def test_node_port_2_reference_refresh_does_not_replace_crossed_outflows(
self,
) -> None:
hot = Cylinder(
"hot",
self.medium,
V=0.1,
p0=100_000.0,
T0=400.0,
)
cold = Cylinder(
"cold",
self.medium,
V=0.1,
p0=100_000.0,
T0=200.0,
)
remote = Cylinder(
"remote",
self.medium,
V=0.1,
p0=100_000.0,
T0=300.0,
)
node = AmesimPn3Node2("node")
pipe = AmesimPnl00r("pnl_1", self.medium)
node.port_1.m_flow = 1.0
node.port_2.m_flow = -1.0
node.port_3.m_flow = 0.0
network = SimulationNetwork("pnl00r-node-reference")
for component in (hot, cold, remote, node, pipe):
network.add_component(component)
network.connect("hot", "port_b", "node", "port_1")
network.connect("cold", "port_b", "node", "port_3")
network.connect("node", "port_2", "pnl_1", "port_1")
network.connect("pnl_1", "port_2", "remote", "port_b")
resolver = StreamResolver(network)
diagnostics, connected_h = resolver.solve()
references = resolver.connected_temperature_reference_enthalpies()
outflows_before = (pipe.port_1.h_outflow, pipe.port_2.h_outflow)
self.assertTrue(diagnostics.converged)
self.assertNotEqual(node.port_2.h_outflow, node.temperature_reference_h)
self.assertEqual(pipe._connected_h, connected_h["pnl_1"])
self.assertNotEqual(
connected_h["pnl_1"]["port_1"],
references["pnl_1"]["port_1"],
)
resolver.refresh_flow_temperature_references()
self.assertEqual(pipe._connected_h, references["pnl_1"])
self.assertEqual(
pipe._connected_h["port_1"],
node.temperature_reference_h,
)
self.assertEqual(
(pipe.port_1.h_outflow, pipe.port_2.h_outflow),
outflows_before,
)
def test_hot_and_cold_upstream_flow_results_and_equations_are_consistent(self) -> None:
pipe = AmesimPnl00r(
"pnl_1",
self.medium,
diam=0.014,
le=1.0,
rr=0.045 / 14.0,
)
hot_temperature = 420.0
cold_temperature = 240.0
pipe.update_stream_outflows(
{
"port_1": self.medium.specific_enthalpy(hot_temperature),
"port_2": self.medium.specific_enthalpy(cold_temperature),
}
)
reference_hot = AmesimPnl00r(
"reference_hot",
self.medium,
diam=pipe.diam,
le=pipe.le,
rr=pipe.rr,
)
reference_hot.update_flow_temperature_references(
{
"port_1": self.medium.specific_enthalpy(hot_temperature),
"port_2": self.medium.specific_enthalpy(hot_temperature),
}
)
reference_cold = AmesimPnl00r(
"reference_cold",
self.medium,
diam=pipe.diam,
le=pipe.le,
rr=pipe.rr,
)
reference_cold.update_flow_temperature_references(
{
"port_1": self.medium.specific_enthalpy(cold_temperature),
"port_2": self.medium.specific_enthalpy(cold_temperature),
}
)
cases = (
("forward_hot", 501000.0, 500000.0, hot_temperature, reference_hot),
("reverse_cold", 500000.0, 501000.0, cold_temperature, reference_cold),
)
for label, p_1, p_2, upstream_temperature, reference in cases:
with self.subTest(label=label):
pipe.port_1.p = p_1
pipe.port_2.p = p_2
reference.port_1.p = p_1
reference.port_2.p = p_2
expected_flow = reference.mass_flow(p_1, p_2)
actual_flow = pipe.mass_flow(p_1, p_2)
self.assertAlmostEqual(actual_flow, expected_flow)
pipe.port_1.m_flow = actual_flow
pipe.port_2.m_flow = -actual_flow
equation_values = pipe.pressure_flow_equation_values()
residual_values = tuple(
residual.value
for residual in pipe.pressure_flow_equation_residuals()
)
self.assertEqual(equation_values, (0.0, 0.0))
self.assertEqual(residual_values, equation_values)
results = pipe.component_result_values()
upstream_pressure = max(p_1, p_2)
density = self.medium.density(
upstream_pressure,
upstream_temperature,
)
expected_reynolds = pipe.reynolds_number(
actual_flow,
upstream_temperature,
)
self.assertAlmostEqual(results["re"], expected_reynolds)
self.assertAlmostEqual(
results["v"],
actual_flow / (density * pipe.area),
)
self.assertAlmostEqual(
results["cm"],
abs(actual_flow)
* upstream_temperature**0.5
/ (pipe.area * upstream_pressure),
)
self.assertAlmostEqual(
results["ff"],
min(pipe.friction_factor(expected_reynolds), 64_000_000.0),
)
def test_friction_factor_transitions_from_laminar_to_turbulent(self) -> None:
pipe = AmesimPnl00r("pnl_1", self.medium, rr=1e-5)
+708
View File
@@ -0,0 +1,708 @@
from __future__ import annotations
import hashlib
import json
from pathlib import Path
import sys
import tempfile
import textwrap
import unittest
from unittest.mock import patch
from app.simulation.benchmark_regression import (
DEFAULT_MANIFEST_PATH,
RegressionCaseRequest,
RegressionManifestError,
derive_simulation_xml,
evaluate_regression_golden,
load_regression_manifest,
load_regression_golden,
main,
run_bounded_child_process,
run_regression_suite,
runtime_snapshot,
source_simulation_config,
summarize_simulation_result,
)
def _completed_result(request: RegressionCaseRequest, *, wall_seconds: float = 0.01):
signal_times = (
[0.04, 0.8]
if request.stop_time >= 1.0
else [0.04] if request.stop_time >= 0.04 else []
)
return {
"outcome": "completed",
"orchestrationWallSeconds": wall_seconds,
"worker": {
"outcome": "completed",
"wallSeconds": wall_seconds,
"lastSimulatedTime": request.stop_time,
"summary": {
"success": True,
"status": "completed",
"simulatedUntil": request.stop_time,
"seriesHealth": {
"seriesCount": 1,
"scalarCount": 2,
"nonfiniteCount": 0,
"timeStrictlyIncreasing": True,
"timeStart": 0.0,
"timeEnd": request.stop_time,
},
"checkpoints": [
{
"requestedTime": checkpoint,
"actualTime": checkpoint,
"available": True,
"stateValues": {"state.placeholder": 0.0},
}
for checkpoint in request.checkpoint_times
],
"diagnostics": {
"pressureFlow": {"maxScaledResidual": 1.0e-12},
},
"eventTrace": {
"signalEventTimes": signal_times,
"segments": [
{"startTime": start_time}
for start_time in (0.0, *signal_times)
],
"mechanicalTransitionTimes": [],
"mechanicalTransitionTimesAvailable": True,
},
"physicalContract": {
"schemaVersion": 1,
"projectionCategories": ["state"],
"checkpoints": [
{
"requestedTime": checkpoint,
"actualTime": checkpoint,
"available": True,
"stateValues": {"state.placeholder": 0.0},
}
for checkpoint in request.checkpoint_times
],
"eventTrace": {
"signalEventTimes": signal_times,
"segments": [
{"startTime": start_time}
for start_time in (0.0, *signal_times)
],
"mechanicalTransitionTimes": [],
"mechanicalTransitionTimesAvailable": True,
},
},
"outputContract": {
"schemaVersion": 1,
"sha256": "0" * 64,
"variableCount": 1,
"seriesKeyCount": 1,
"sampleCount": 2,
},
},
},
}
class RegressionManifestTests(unittest.TestCase):
def test_runtime_snapshot_records_repository_identity(self) -> None:
repository = runtime_snapshot()["repository"]
if repository["head"] is not None:
self.assertEqual(len(repository["head"]), 40)
self.assertIn(repository["dirty"], (True, False, None))
self.assertIsInstance(repository["status"], list)
def test_manifest_locks_authoritative_source_and_two_sampling_lanes(self) -> None:
manifest = load_regression_manifest(DEFAULT_MANIFEST_PATH)
self.assertEqual(
manifest["sequence"],
["0.01s-smoke", "0.2s", "1s", "5s", "10s"],
)
self.assertTrue(manifest["source"]["xmlIsAuthoritative"])
self.assertEqual(
manifest["source"]["sha256"],
"170463d65d074da01f0f9e9dab730b3815c94c1cc80b5190ec2e3fe623da74d3",
)
self.assertEqual(Path(manifest["_sourcePath"]).name, "test-mql-8.xml")
self.assertEqual(
manifest["source"]["companionProject"]["sha256"],
"258c50ee4850baa72fb7c2cc24536d0a631fc6a7f1fa6cedb7b6eea7c857cbaa",
)
self.assertFalse(
manifest["source"]["companionProject"]["executionInput"]
)
self.assertEqual(manifest["historicalReports"][0]["status"], "historicalOnly")
self.assertFalse(
manifest["historicalReports"][0]["compatibleWithCurrentSource"]
)
self.assertEqual(manifest["lanes"]["production"]["samplingMode"], "source")
self.assertEqual(manifest["lanes"]["solver-only"]["sampleStep"], 0.02)
self.assertEqual(manifest["lanes"]["solver-only"]["maxStep"], 0.05)
def test_in_memory_derivative_does_not_change_authoritative_xml(self) -> None:
manifest = load_regression_manifest(DEFAULT_MANIFEST_PATH)
source_path = Path(manifest["_sourcePath"])
before = source_path.read_bytes()
derived = derive_simulation_xml(
before,
stop_time=1.0,
sample_step=0.02,
max_step=0.005,
)
self.assertEqual(source_simulation_config(before)["tStop"], 0.2)
self.assertEqual(source_simulation_config(before)["sampleStep"], 0.01)
self.assertEqual(source_simulation_config(before)["maxStep"], 0.01)
self.assertEqual(source_simulation_config(derived)["tStop"], 1.0)
self.assertEqual(source_simulation_config(derived)["sampleStep"], 0.02)
self.assertEqual(source_simulation_config(derived)["maxStep"], 0.005)
self.assertEqual(source_path.read_bytes(), before)
def test_historical_pure_xml_manifest_does_not_require_companion(self) -> None:
historical_manifest_path = (
DEFAULT_MANIFEST_PATH.parent.parent
/ "test_mql_full_branches"
/ "manifest.json"
)
manifest = load_regression_manifest(historical_manifest_path)
self.assertNotIn("companionProject", manifest["source"])
self.assertIsNone(manifest["_companionPath"])
class RegressionGoldenProtocolTests(unittest.TestCase):
def test_reviewed_golden_is_provenanced_and_compared_numerically(self) -> None:
source_xml_sha256 = "1" * 64
output_contract_sha256 = "2" * 64
generated_at = "2026-08-17T00:00:00+00:00"
checkpoints = [{
"requestedTime": 0.0,
"actualTime": 0.0,
"available": True,
"stateValues": {"state.a": 1.0},
}]
summary = {
"physicalContract": {
"schemaVersion": 1,
"projectionCategories": ["state"],
"checkpoints": checkpoints,
"eventTrace": {},
},
"outputContract": {
"schemaVersion": 1,
"sha256": output_contract_sha256,
},
}
report = {
"generatedAt": generated_at,
"source": {"sha256": source_xml_sha256},
"cases": [{
"caseId": "smoke",
"lane": "solver-only",
"worker": {"summary": summary},
}],
}
with tempfile.TemporaryDirectory() as temporary_directory:
repository_root = Path(temporary_directory)
report_path = repository_root / "source-report.json"
report_path.write_text(
json.dumps(report, sort_keys=True), encoding="utf-8"
)
report_sha256 = hashlib.sha256(report_path.read_bytes()).hexdigest()
state_keys = ["state.a"]
state_layout_sha256 = hashlib.sha256(
json.dumps(
state_keys,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
).hexdigest()
golden = {
"schemaVersion": 1,
"id": "synthetic-reviewed-golden",
"caseId": "smoke",
"lane": "solver-only",
"sourceXmlSha256": source_xml_sha256,
"approval": {"status": "approved"},
"provenance": {
"sourceReport": {
"path": "source-report.json",
"sha256": report_sha256,
"generatedAt": generated_at,
"metadataCompatibility": {
"status": "current",
"differences": [],
},
}
},
"physicalLayout": {
"projectionCategories": ["state"],
"stateKeys": state_keys,
"stateKeyLayoutSha256": state_layout_sha256,
},
"tolerance": {
"relative": 0.001,
"absolute": 0.01,
"checkpointTimeAbsoluteSeconds": 1.0e-12,
},
"physicalCheckpoints": [{
"requestedTime": 0.0,
"values": [1.0],
}],
"outputContract": {"sha256": output_contract_sha256},
}
golden_path = repository_root / "golden.json"
golden_path.write_text(
json.dumps(golden, sort_keys=True), encoding="utf-8"
)
golden_sha256 = hashlib.sha256(golden_path.read_bytes()).hexdigest()
loaded = load_regression_golden(
golden_path,
expected_sha256=golden_sha256,
repository_root=repository_root,
)
passing = evaluate_regression_golden(summary, loaded)
self.assertTrue(passing["passed"])
self.assertEqual(passing["comparedValueCount"], 1)
self.assertEqual(passing["issues"], [])
changed_physical = json.loads(json.dumps(summary))
changed_physical["physicalContract"]["checkpoints"][0][
"stateValues"
]["state.a"] = 1.02
physical_failure = evaluate_regression_golden(changed_physical, loaded)
self.assertFalse(physical_failure["passed"])
self.assertIn(
"stateCheckpointGoldenValueMismatch", physical_failure["issues"]
)
changed_output = json.loads(json.dumps(summary))
changed_output["outputContract"]["sha256"] = "3" * 64
output_failure = evaluate_regression_golden(changed_output, loaded)
self.assertFalse(output_failure["passed"])
self.assertIn("outputContractMismatch", output_failure["issues"])
class BoundedChildProcessTests(unittest.TestCase):
def test_short_json_worker_completes_without_timeout(self) -> None:
script = textwrap.dedent(
"""
import json
print(json.dumps({"event": "progress", "simulatedTime": 0.1}), flush=True)
print(json.dumps({
"event": "result",
"outcome": "completed",
"lastSimulatedTime": 0.2,
"wallSeconds": 0.01,
}), flush=True)
"""
)
result = run_bounded_child_process(
[sys.executable, "-u", "-c", script],
soft_timeout_seconds=1.0,
hard_timeout_seconds=2.0,
termination_grace_seconds=0.2,
)
self.assertEqual(result["outcome"], "completed")
self.assertFalse(result["softCancelSent"])
self.assertFalse(result["hardTimeoutReached"])
self.assertEqual(result["lastSimulatedTime"], 0.2)
def test_parent_requests_soft_cancel_before_hard_timeout(self) -> None:
script = textwrap.dedent(
"""
import json
import sys
print(json.dumps({"event": "progress", "simulatedTime": 0.125}), flush=True)
for line in sys.stdin:
if line.strip() == "cancel":
print(json.dumps({
"event": "result",
"outcome": "cancelled",
"lastSimulatedTime": 0.125,
"wallSeconds": 0.1,
}), flush=True)
break
"""
)
result = run_bounded_child_process(
[sys.executable, "-u", "-c", script],
soft_timeout_seconds=0.08,
hard_timeout_seconds=1.0,
termination_grace_seconds=0.2,
)
self.assertEqual(result["outcome"], "soft_timeout")
self.assertTrue(result["softCancelSent"])
self.assertFalse(result["hardTimeoutReached"])
self.assertEqual(result["lastSimulatedTime"], 0.125)
def test_unresponsive_child_is_terminated_at_hard_timeout(self) -> None:
script = textwrap.dedent(
"""
import json
import time
print(json.dumps({"event": "progress", "simulatedTime": 0.05}), flush=True)
time.sleep(5)
"""
)
result = run_bounded_child_process(
[sys.executable, "-u", "-c", script],
soft_timeout_seconds=0.05,
hard_timeout_seconds=0.15,
termination_grace_seconds=0.1,
)
self.assertEqual(result["outcome"], "hard_timeout")
self.assertTrue(result["softCancelSent"])
self.assertTrue(result["hardTimeoutReached"])
self.assertEqual(result["lastSimulatedTime"], 0.05)
def test_child_that_closes_stdin_does_not_break_pipe_cleanup(self) -> None:
script = textwrap.dedent(
"""
import os
import time
os.close(0)
time.sleep(5)
"""
)
result = run_bounded_child_process(
[sys.executable, "-u", "-c", script],
soft_timeout_seconds=0.05,
hard_timeout_seconds=0.15,
termination_grace_seconds=0.1,
)
self.assertEqual(result["outcome"], "hard_timeout")
self.assertTrue(result["softCancelSent"])
self.assertTrue(result["hardTimeoutReached"])
class ProgressiveSuiteTests(unittest.TestCase):
def test_requesting_late_case_also_runs_every_predecessor(self) -> None:
calls: list[str] = []
def complete(request: RegressionCaseRequest) -> dict[str, object]:
calls.append(request.case_id)
return _completed_result(request)
report = run_regression_suite(
DEFAULT_MANIFEST_PATH,
lane="solver-only",
case_ids=["5s"],
case_executor=complete,
)
self.assertEqual(calls, ["0.01s-smoke", "0.2s", "1s", "5s"])
self.assertEqual(
[case["outcome"] for case in report["cases"]],
["completed", "completed", "completed", "completed"],
)
def test_manifest_pins_solver_modes_in_every_request(self) -> None:
seen: list[dict[str, str]] = []
def complete(request: RegressionCaseRequest) -> dict[str, object]:
seen.append(dict(request.environment_overrides))
return _completed_result(request)
run_regression_suite(
DEFAULT_MANIFEST_PATH,
lane="solver-only",
case_ids=["0.01s-smoke"],
case_executor=complete,
)
self.assertEqual(
seen,
[{
"SIMULATION_CAUSAL_EXECUTOR_V2": "1",
"SIMULATION_CAUSAL_COORDINATE_KERNEL": "1",
"SIMULATION_CAUSAL_FAST_PATH": "1",
"SIMULATION_ODE_JACOBIAN_MODE": "scipy",
"SIMULATIONAPP_PROPERTY_CACHE": "on",
}],
)
def test_empty_explicit_case_list_is_rejected(self) -> None:
with self.assertRaisesRegex(
RegressionManifestError,
"No regression variants were selected",
):
run_regression_suite(
DEFAULT_MANIFEST_PATH,
lane="solver-only",
case_ids=[],
case_executor=_completed_result,
)
def test_missing_signal_segment_fails_acceptance(self) -> None:
def missing_segment(request: RegressionCaseRequest) -> dict[str, object]:
result = _completed_result(request)
result["worker"]["summary"]["eventTrace"]["segments"] = [
{"startTime": 0.0}
]
return result
report = run_regression_suite(
DEFAULT_MANIFEST_PATH,
lane="solver-only",
case_ids=["0.2s"],
case_executor=missing_segment,
)
self.assertEqual(report["cases"][1]["outcome"], "correctness_failed")
self.assertIn(
"signalEventSegmentMissing",
report["cases"][1]["acceptance"]["issues"],
)
def test_deferred_suite_returns_nonzero_incomplete_exit_status(self) -> None:
report = {"cases": [
{"outcome": "completed"},
{"outcome": "deferred"},
]}
with patch(
"app.simulation.benchmark_regression.run_regression_suite",
return_value=report,
), patch("builtins.print"):
exit_code = main([])
self.assertEqual(exit_code, 2)
def test_nonfinite_completed_case_fails_acceptance_and_defers_longer_runs(
self,
) -> None:
calls: list[str] = []
def nonfinite_first(request: RegressionCaseRequest) -> dict[str, object]:
calls.append(request.case_id)
result = _completed_result(request)
result["worker"]["summary"]["seriesHealth"]["nonfiniteCount"] = 1
return result
report = run_regression_suite(
DEFAULT_MANIFEST_PATH,
lane="solver-only",
case_executor=nonfinite_first,
)
self.assertEqual(calls, ["0.01s-smoke"])
self.assertEqual(report["cases"][0]["outcome"], "correctness_failed")
self.assertEqual(
report["cases"][0]["acceptance"]["issues"],
["nonfiniteSeries"],
)
self.assertTrue(
all(case["outcome"] == "deferred" for case in report["cases"][1:])
)
def test_failed_predecessor_defers_every_longer_horizon(self) -> None:
calls: list[str] = []
def fail_first(request: RegressionCaseRequest) -> dict[str, object]:
calls.append(request.case_id)
return {"outcome": "error", "worker": None}
report = run_regression_suite(
DEFAULT_MANIFEST_PATH,
lane="solver-only",
case_executor=fail_first,
)
self.assertEqual(calls, ["0.01s-smoke"])
self.assertEqual(report["cases"][0]["outcome"], "error")
self.assertEqual(
[case["outcome"] for case in report["cases"][1:]],
["deferred", "deferred", "deferred", "deferred"],
)
self.assertTrue(
all(
case["reason"] == "predecessorDidNotComplete"
for case in report["cases"][1:]
)
)
def test_prediction_over_budget_defers_before_launching_next_case(self) -> None:
calls: list[str] = []
def expensive_short_case(request: RegressionCaseRequest) -> dict[str, object]:
calls.append(request.case_id)
return _completed_result(request, wall_seconds=200.0)
report = run_regression_suite(
DEFAULT_MANIFEST_PATH,
lane="solver-only",
case_executor=expensive_short_case,
)
self.assertEqual(calls, ["0.01s-smoke", "0.2s"])
self.assertIsNone(report["cases"][1]["predictedWallSeconds"])
third = report["cases"][2]
self.assertEqual(third["outcome"], "deferred")
self.assertEqual(third["reason"], "predictedWallExceedsSoftBudget")
self.assertEqual(third["predictedWallSeconds"], 1500.0)
def test_lanes_pass_distinct_sampling_to_case_executor(self) -> None:
seen: list[tuple[str, float, float]] = []
def complete(request: RegressionCaseRequest) -> dict[str, object]:
seen.append((request.lane, request.sample_step, request.max_step))
return _completed_result(request)
run_regression_suite(
DEFAULT_MANIFEST_PATH,
lane="solver-only",
case_ids=["0.01s-smoke"],
case_executor=complete,
)
run_regression_suite(
DEFAULT_MANIFEST_PATH,
lane="production",
case_ids=["0.01s-smoke"],
case_executor=complete,
)
self.assertEqual(
seen,
[("solver-only", 0.02, 0.05), ("production", 0.01, 0.01)],
)
class ResultSummaryTests(unittest.TestCase):
def test_summary_preserves_diagnostics_and_available_event_trace(self) -> None:
result = {
"success": True,
"status": "completed",
"partial": False,
"message": "done",
"simulatedUntil": 0.2,
"requestedStopTime": 0.2,
"variables": [
{"key": "tank.m", "category": "state"},
{"key": "tank.p", "category": "thermodynamic"},
],
"series": {
"time": [0.0, 0.2],
"tank.m": [1.0, 0.9],
"tank.p": [2.0, 1.5],
},
"final": {"tank.m": 0.9, "tank.p": 1.5},
"diagnostics": {
"stateCount": 1,
"signal": {"eventTimes": [0.04]},
"integration": {
"totals": {
"stateTransitionCount": 2,
"solverStartCount": 3,
},
"segments": [
{
"startTime": 0.0,
"requestedStopTime": 0.2,
"simulatedUntil": 0.2,
"stateTransitionCount": 2,
"stateTransitionTimes": [0.11, 0.17],
"solverStartCount": 3,
"recoverableRetryCount": 0,
}
],
},
},
}
summary = summarize_simulation_result(
result,
checkpoint_times=(0.0, 0.2),
sample_step=0.2,
)
self.assertEqual(summary["diagnostics"], result["diagnostics"])
self.assertEqual(summary["eventTrace"]["signalEventTimes"], [0.04])
self.assertEqual(summary["eventTrace"]["stateTransitionCount"], 2)
self.assertTrue(
summary["eventTrace"]["mechanicalTransitionTimesAvailable"]
)
self.assertEqual(
summary["eventTrace"]["mechanicalTransitionTimes"],
[0.11, 0.17],
)
self.assertEqual(
summary["checkpoints"][1]["stateValues"], {"tank.m": 0.9}
)
self.assertEqual(
summary["physicalContract"]["checkpoints"], summary["checkpoints"]
)
self.assertEqual(
summary["physicalContract"]["eventTrace"], summary["eventTrace"]
)
self.assertFalse(summary["outputContract"]["containsPhysicalValues"])
self.assertEqual(summary["seriesHealth"]["nonfiniteCount"], 0)
changed_values = json.loads(json.dumps(result))
changed_values["series"]["tank.m"] = [99.0, -99.0]
changed_values["series"]["tank.p"] = [-2.0, 1000.0]
value_changed_summary = summarize_simulation_result(
changed_values,
checkpoint_times=(0.0, 0.2),
sample_step=0.2,
)
self.assertEqual(
value_changed_summary["outputContract"]["sha256"],
summary["outputContract"]["sha256"],
)
changed_metadata = json.loads(json.dumps(result))
changed_metadata["variables"][0]["unit"] = "kg"
metadata_changed_summary = summarize_simulation_result(
changed_metadata,
checkpoint_times=(0.0, 0.2),
sample_step=0.2,
)
self.assertNotEqual(
metadata_changed_summary["outputContract"]["sha256"],
summary["outputContract"]["sha256"],
)
def test_missing_mechanical_transition_times_are_reported_unavailable(
self,
) -> None:
summary = summarize_simulation_result(
{
"diagnostics": {
"integration": {
"totals": {"stateTransitionCount": 1},
"segments": [{
"startTime": 0.0,
"stateTransitionCount": 1,
}],
},
},
},
checkpoint_times=(),
sample_step=0.1,
)
self.assertFalse(
summary["eventTrace"]["mechanicalTransitionTimesAvailable"]
)
if __name__ == "__main__":
unittest.main()
+281
View File
@@ -0,0 +1,281 @@
from __future__ import annotations
from dataclasses import replace
from pathlib import Path
import unittest
from app.main import compile_reactflow_network, compile_system_xml_network
from app.simulation.solvers.causal_ir import (
CausalIROpcode,
compile_causal_numeric_ir,
)
from app.simulation.systems.generic import GenericFluidSystem
from app.system_xml import validate_system_xml_document
from tests.test_amesim_mechanical_xml import zero_force_mass_project
from tests.test_amesim_pnvo001_signal_xml import high_pressure_helium_step_project
from tests.test_generic_system_xml_simulation import chain_project
TARGET_XML = Path("tests/data/test-mql-8.xml")
def _system(project) -> GenericFluidSystem:
return GenericFluidSystem(compile_reactflow_network(project))
class CausalNumericIRTests(unittest.TestCase):
def test_structure_signature_is_stable_and_excludes_bindings(self) -> None:
first = compile_causal_numeric_ir(
_system(zero_force_mass_project()).pressure_flow_solver
)
second = compile_causal_numeric_ir(
_system(zero_force_mass_project()).pressure_flow_solver
)
self.assertTrue(first.supported)
self.assertTrue(second.supported)
assert first.ir is not None and second.ir is not None
self.assertEqual(
first.ir.program.structural_signature,
second.ir.program.structural_signature,
)
self.assertEqual(
first.ir.program.structural_signature,
first.ir.program.calculate_structural_signature(),
)
self.assertEqual(len(first.ir.program.structural_signature), 64)
changed_binding = replace(
first.ir,
bindings=replace(
first.ir.bindings,
evaluators=tuple(
(lambda evaluate=evaluate: evaluate())
for evaluate in first.ir.bindings.evaluators
),
),
)
self.assertEqual(
changed_binding.program.structural_signature,
first.ir.program.structural_signature,
)
def test_reference_interpreter_matches_existing_object_plan_bitwise(self) -> None:
system = _system(high_pressure_helium_step_project())
state = system.initial_state_vector()
system.rhs(0.041, state)
solver = system.pressure_flow_solver
compilation = compile_causal_numeric_ir(solver)
self.assertTrue(compilation.supported, compilation.fallback_reason)
assert compilation.ir is not None
before = tuple(unknown.read() for unknown in solver.unknowns)
self.assertTrue(solver._execute_causal_effort_plan(("p",)))
for unknown in solver._explicit_flow_unknowns_by_variables[
frozenset(("f", "m_flow"))
]:
unknown.write(0.0)
expected_flow_stages: list[tuple[float, ...]] = []
for stage in solver._explicit_flow_plan:
values = solver._evaluate_explicit_flow_stage(stage)
expected_flow_stages.append(values)
for assignment, value in zip(stage.assignments, values):
assignment.unknown.write(value)
expected = tuple(unknown.read() for unknown in solver.unknowns)
for unknown, value in zip(solver.unknowns, before):
unknown.write(value)
workspace = compilation.ir.create_workspace()
observed: list[tuple[str, int, tuple[int, ...], tuple[float, ...]]] = []
result = compilation.ir.execute(
workspace,
stage_observer=lambda phase, index, slots, values: observed.append(
(phase, index, slots, values)
),
)
actual = tuple(unknown.read() for unknown in solver.unknowns)
self.assertTrue(result.success, result.fallback_reason)
self.assertEqual(actual, expected)
self.assertEqual(
result.effort_assignment_count,
len(compilation.ir.program.effort_stages[0].operations),
)
self.assertEqual(
result.flow_assignment_count,
compilation.ir.program.flow_assignment_count,
)
self.assertEqual(observed[0][0], "effort:p")
self.assertEqual(
sum(item[0] == "flow" for item in observed),
len(compilation.ir.program.flow_stages),
)
self.assertEqual(
tuple(item[3] for item in observed if item[0] == "flow"),
tuple(expected_flow_stages),
)
def test_workspace_is_reused_and_normal_execution_does_not_snapshot(self) -> None:
system = _system(zero_force_mass_project())
system.rhs(0.0, system.initial_state_vector())
compilation = compile_causal_numeric_ir(system.pressure_flow_solver)
assert compilation.ir is not None
workspace = compilation.ir.create_workspace()
array_ids = (
id(workspace.canonical_values),
id(workspace.effort_residuals),
id(workspace.flow_values),
id(workspace.transaction_values),
)
workspace.transaction_values.fill(float("nan"))
first = compilation.ir.execute(workspace)
second = compilation.ir.execute(workspace)
self.assertTrue(first.success)
self.assertTrue(second.success)
self.assertEqual(
array_ids,
(
id(workspace.canonical_values),
id(workspace.effort_residuals),
id(workspace.flow_values),
id(workspace.transaction_values),
),
)
self.assertTrue(
all(value != value for value in workspace.transaction_values)
)
def test_transactional_audit_rolls_back_partial_flow_failure(self) -> None:
system = _system(zero_force_mass_project())
system.rhs(0.0, system.initial_state_vector())
solver = system.pressure_flow_solver
compilation = compile_causal_numeric_ir(solver)
assert compilation.ir is not None
ir = compilation.ir
before = tuple(unknown.read() for unknown in solver.unknowns)
operation = ir.program.flow_stages[0].operations[0]
evaluators = list(ir.bindings.evaluators)
original = evaluators[operation.evaluator_slot]
if operation.opcode is CausalIROpcode.FLOW_DIRECT:
evaluators[operation.evaluator_slot] = lambda: float("nan")
else:
equation_index = operation.equation_indices[0]
def one_nonfinite_component():
values = list(original())
values[equation_index] = float("nan")
return tuple(values)
evaluators[operation.evaluator_slot] = one_nonfinite_component
failing_ir = replace(
ir,
bindings=replace(ir.bindings, evaluators=tuple(evaluators)),
)
result = failing_ir.execute(
failing_ir.create_workspace(),
transactional=True,
)
self.assertFalse(result.success)
self.assertEqual(result.fallback_reason, "nonFiniteFlowAssignment")
self.assertTrue(result.rolled_back)
self.assertEqual(
tuple(unknown.read() for unknown in solver.unknowns),
before,
)
def test_unsupported_plan_returns_existing_proof_reason(self) -> None:
solver = _system(chain_project()).pressure_flow_solver
compilation = compile_causal_numeric_ir(solver)
self.assertFalse(compilation.supported)
self.assertIsNone(compilation.ir)
self.assertEqual(
compilation.fallback_reason,
solver._causal_fast_path_fallback_reason,
)
class TargetCausalNumericIRStructureTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
report = validate_system_xml_document(TARGET_XML.read_bytes())
assert report.valid and report.document is not None
cls.system = GenericFluidSystem(
compile_system_xml_network(report.document)
)
cls.compilation = compile_causal_numeric_ir(
cls.system.pressure_flow_solver
)
def test_target_has_expected_canonical_and_compatibility_coordinates(
self,
) -> None:
self.assertTrue(
self.compilation.supported,
self.compilation.fallback_reason,
)
assert self.compilation.ir is not None
program = self.compilation.ir.program
self.assertEqual(len(program.compatibility_slots), 760)
self.assertEqual(len(program.canonical_slots), 440)
self.assertEqual(program.effort_group_count, 112)
self.assertEqual(program.flow_assignment_count, 328)
self.assertEqual(program.effort_scatter_count, 432)
self.assertEqual(program.eliminated_effort_replica_count, 320)
self.assertEqual(
tuple(slot.slot for slot in program.canonical_slots),
tuple(range(440)),
)
def test_target_batches_efforts_and_preserves_flow_stage_layout(self) -> None:
assert self.compilation.ir is not None
program = self.compilation.ir.program
pressure_stage = next(
stage for stage in program.effort_stages if stage.variable == "p"
)
self.assertEqual(len(pressure_stage.operations), 72)
self.assertEqual(len(pressure_stage.evaluations), 36)
self.assertTrue(
all(
evaluation.opcode
is CausalIROpcode.EFFORT_COMPONENT_RESIDUAL
for evaluation in pressure_stage.evaluations
)
)
self.assertEqual(
[len(stage.target_slots) for stage in program.flow_stages],
[110, 130, 49, 33, 5, 1],
)
def test_target_reference_execution_matches_all_compatibility_slots(self) -> None:
assert self.compilation.ir is not None
solver = self.system.pressure_flow_solver
self.system.rhs(0.0, self.system.initial_state_vector())
before = tuple(unknown.read() for unknown in solver.unknowns)
self.assertTrue(solver._execute_causal_effort_plan(("p",)))
self.assertIsNone(solver._execute_compiled_causal_flow_plan())
expected = tuple(unknown.read() for unknown in solver.unknowns)
for unknown, value in zip(solver.unknowns, before):
unknown.write(value)
result = self.compilation.ir.execute(
self.compilation.ir.create_workspace()
)
self.assertTrue(result.success, result.fallback_reason)
self.assertEqual(
tuple(unknown.read() for unknown in solver.unknowns),
expected,
)
if __name__ == "__main__":
unittest.main()
+477 -1
View File
@@ -203,6 +203,82 @@ class IntegrateOdeTests(unittest.TestCase):
self.assertEqual(calls[0]["max_step"], 1.0e-3)
self.assertNotIn("first_step", calls[0])
def test_recoverable_trial_retries_default_keeps_direct_scipy_route(
self,
) -> None:
import scipy.integrate
expected = object()
with patch.object(
scipy.integrate,
"solve_ivp",
return_value=expected,
) as direct_solve:
result = integrate_ode(
rhs=lambda _time, state: state,
initial_state=[1.0],
config=SolveIVPConfig(t_start=0.0, t_stop=1.0),
)
self.assertIs(result, expected)
direct_solve.assert_called_once()
def test_recoverable_stepwise_route_matches_direct_scipy_without_failures(
self,
) -> None:
import numpy as np
sample_times = [0.0, 0.025, 0.05, 0.075, 0.1]
def rhs(time, state):
return [-2.0 * float(state[0]) + math.sin(float(time))]
for method in ("RK45", "BDF"):
with self.subTest(method=method):
config = SolveIVPConfig(
t_start=0.0,
t_stop=0.1,
method=method,
rtol=1.0e-9,
atol=1.0e-12,
max_step=0.01,
)
direct = integrate_ode(
rhs=rhs,
initial_state=[1.0],
config=config,
t_eval=sample_times,
)
stepwise = integrate_ode(
rhs=rhs,
initial_state=[1.0],
config=config,
t_eval=sample_times,
recoverable_trial_retries=True,
)
self.assertTrue(direct.success, direct.message)
self.assertTrue(stepwise.success, stepwise.message)
np.testing.assert_array_equal(direct.t, stepwise.t)
np.testing.assert_allclose(
direct.y,
stepwise.y,
rtol=1.0e-12,
atol=1.0e-14,
)
self.assertEqual(
int(direct.nfev),
sum(segment.nfev for segment in stepwise.solver_segments),
)
self.assertEqual(
int(getattr(direct, "njev", 0)),
sum(segment.njev for segment in stepwise.solver_segments),
)
self.assertEqual(
int(getattr(direct, "nlu", 0)),
sum(segment.nlu for segment in stepwise.solver_segments),
)
def test_stepwise_solver_rebuilds_after_recoverable_trial_failure(self) -> None:
import numpy as np
import scipy.integrate
@@ -241,7 +317,7 @@ class IntegrateOdeTests(unittest.TestCase):
max_step=1.0,
),
t_eval=[0.0, 1.0],
cancel_check=lambda: False,
recoverable_trial_retries=True,
)
self.assertTrue(result.success, result.message)
@@ -249,6 +325,392 @@ class IntegrateOdeTests(unittest.TestCase):
self.assertEqual(result.t, [0.0, 1.0])
self.assertEqual(result.y, [[1.0, 1.0]])
def test_stepwise_solver_retries_recoverable_constructor_failure(self) -> None:
import numpy as np
import scipy.integrate
attempts: list[dict[str, object]] = []
class ConstructorRetryBdf:
def __init__(self, _fun, t0, y0, t_bound, **kwargs):
attempts.append(dict(kwargs))
if float(kwargs["max_step"]) > 0.25:
raise RecoverableTrialStateError("constructor trial failed")
self.t = float(t0)
self.y = np.asarray(y0, dtype=float)
self.t_bound = float(t_bound)
self.status = "running"
def step(self):
self.t = self.t_bound
self.status = "finished"
return None
def dense_output(self):
state = self.y.copy()
return lambda _time: state.copy()
with patch.object(scipy.integrate, "BDF", ConstructorRetryBdf):
result = integrate_ode(
rhs=lambda _time, _state: [0.0],
initial_state=[1.0],
config=SolveIVPConfig(
t_start=0.0,
t_stop=1.0,
method="BDF",
max_step=1.0,
),
cancel_check=lambda: False,
)
self.assertTrue(result.success, result.message)
self.assertNotIn("first_step", attempts[0])
self.assertEqual(
[attempt["max_step"] for attempt in attempts],
[1.0, 0.5, 0.25],
)
self.assertEqual(
[attempt.get("first_step") for attempt in attempts],
[None, 0.5, 0.25],
)
segment = result.solver_segments[0]
self.assertEqual(segment.recoverable_retry_count, 2)
self.assertEqual(
[retry.phase for retry in segment.recoverable_retries],
["constructor", "constructor"],
)
self.assertEqual(
segment.as_dict()["recoverableRetries"][0],
{
"phase": "constructor",
"attemptedStep": 1.0,
"reason": "constructor trial failed",
"nextMaxStep": 0.5,
"nextFirstStep": 0.5,
},
)
def test_stepwise_retry_uses_solver_actual_step_not_segment_maximum(
self,
) -> None:
import numpy as np
import scipy.integrate
attempts: list[dict[str, object]] = []
class ActualStepRetryBdf:
def __init__(self, _fun, t0, y0, t_bound, **kwargs):
self.attempt_index = len(attempts)
attempts.append(dict(kwargs))
self.t = float(t0)
self.y = np.asarray(y0, dtype=float)
self.t_bound = float(t_bound)
self.h_abs = min(0.04, float(kwargs["max_step"]))
self.step_size = min(0.03, float(kwargs["max_step"]))
self.status = "running"
def step(self):
if self.attempt_index == 0:
raise RecoverableTrialStateError("small actual trial failed")
self.t = self.t_bound
self.status = "finished"
return None
def dense_output(self):
state = self.y.copy()
return lambda _time: state.copy()
with patch.object(scipy.integrate, "BDF", ActualStepRetryBdf):
result = integrate_ode(
rhs=lambda _time, _state: [0.0],
initial_state=[1.0],
config=SolveIVPConfig(
t_start=0.0,
t_stop=1.0,
method="BDF",
max_step=1.0,
),
cancel_check=lambda: False,
)
self.assertTrue(result.success, result.message)
self.assertNotIn("first_step", attempts[0])
self.assertEqual(attempts[1]["max_step"], 0.02)
self.assertEqual(attempts[1]["first_step"], 0.02)
retry = result.solver_segments[0].recoverable_retries[0]
self.assertEqual(retry.phase, "step")
self.assertEqual(retry.attempted_step, 0.04)
self.assertEqual(retry.next_max_step, 0.02)
def test_stepwise_retry_falls_back_to_previous_step_size_without_h_abs(
self,
) -> None:
import numpy as np
import scipy.integrate
for invalid_h_abs in (None, math.nan):
with self.subTest(h_abs=invalid_h_abs):
attempts: list[dict[str, object]] = []
class StepSizeFallbackBdf:
def __init__(self, _fun, t0, y0, t_bound, **kwargs):
self.attempt_index = len(attempts)
attempts.append(dict(kwargs))
self.t = float(t0)
self.y = np.asarray(y0, dtype=float)
self.t_bound = float(t_bound)
self.h_abs = invalid_h_abs
self.step_size = 0.06
self.status = "running"
def step(self):
if self.attempt_index == 0:
raise RecoverableTrialStateError(
"trial without valid h_abs"
)
self.t = self.t_bound
self.status = "finished"
return None
def dense_output(self):
state = self.y.copy()
return lambda _time: state.copy()
with patch.object(
scipy.integrate,
"BDF",
StepSizeFallbackBdf,
):
result = integrate_ode(
rhs=lambda _time, _state: [0.0],
initial_state=[1.0],
config=SolveIVPConfig(
t_start=0.0,
t_stop=1.0,
method="BDF",
max_step=1.0,
),
cancel_check=lambda: False,
)
self.assertTrue(result.success, result.message)
self.assertEqual(attempts[1]["max_step"], 0.03)
self.assertEqual(attempts[1]["first_step"], 0.03)
retry = result.solver_segments[0].recoverable_retries[0]
self.assertEqual(retry.attempted_step, 0.06)
self.assertEqual(retry.next_max_step, 0.03)
def test_accepted_step_clears_recoverable_retry_state(self) -> None:
import numpy as np
import scipy.integrate
attempts: list[dict[str, object]] = []
caps_after_accepted_retry: list[float] = []
class FailureAfterAcceptedRetryBdf:
def __init__(self, _fun, t0, y0, t_bound, **kwargs):
self.attempt_index = len(attempts)
attempts.append(dict(kwargs))
self.t = float(t0)
self.y = np.asarray(y0, dtype=float)
self.t_bound = float(t_bound)
self.h_abs = min(0.2, float(kwargs["max_step"]))
self.status = "running"
self.step_count = 0
def step(self):
self.step_count += 1
if self.attempt_index == 0:
raise RecoverableTrialStateError("retry once")
if self.step_count == 1:
self.t = 0.25
return None
caps_after_accepted_retry.append(float(self.max_step))
self.status = "failed"
return "ordinary failure after accepted step"
with patch.object(
scipy.integrate,
"BDF",
FailureAfterAcceptedRetryBdf,
):
result = integrate_ode(
rhs=lambda _time, _state: [0.0],
initial_state=[1.0],
config=SolveIVPConfig(
t_start=0.0,
t_stop=1.0,
method="BDF",
max_step=1.0,
),
cancel_check=lambda: False,
)
self.assertFalse(result.success)
self.assertEqual(result.message, "ordinary failure after accepted step")
self.assertEqual(len(attempts), 2)
self.assertEqual(caps_after_accepted_retry, [1.0])
self.assertEqual(result.solver_segments[0].accepted_step_count, 1)
self.assertEqual(result.solver_segments[0].recoverable_retry_count, 1)
def test_transition_restart_does_not_inherit_retry_first_step(self) -> None:
import numpy as np
import scipy.integrate
attempts: list[dict[str, object]] = []
transition_pending = True
class TransitionAfterRetryBdf:
def __init__(self, _fun, t0, y0, t_bound, **kwargs):
self.attempt_index = len(attempts)
attempts.append(dict(kwargs))
self.t = float(t0)
self.y = np.asarray(y0, dtype=float)
self.t_bound = float(t_bound)
self.h_abs = min(0.2, float(kwargs["max_step"]))
self.status = "running"
def step(self):
if self.attempt_index == 0:
raise RecoverableTrialStateError("retry before transition")
self.t = self.t_bound
self.y = np.asarray([self.t], dtype=float)
self.status = "finished"
return None
def dense_output(self):
return lambda time: np.asarray([float(time)], dtype=float)
def transition_handler(
previous_time,
_previous_state,
current_time,
_current_state,
_dense_state,
):
nonlocal transition_pending
if transition_pending and previous_time <= 0.25 <= current_time:
transition_pending = False
return StateTransition(time=0.25, state=[0.25])
return None
with patch.object(scipy.integrate, "BDF", TransitionAfterRetryBdf):
result = integrate_ode(
rhs=lambda _time, _state: [1.0],
initial_state=[0.0],
config=SolveIVPConfig(
t_start=0.0,
t_stop=1.0,
method="BDF",
max_step=1.0,
),
state_transition_handler=transition_handler,
)
self.assertTrue(result.success, result.message)
self.assertEqual(len(attempts), 3)
self.assertNotIn("first_step", attempts[0])
self.assertEqual(attempts[1]["first_step"], 0.1)
self.assertEqual(attempts[1]["max_step"], 0.1)
self.assertNotIn("first_step", attempts[2])
self.assertEqual(attempts[2]["max_step"], 1.0)
def test_recoverable_retry_stops_at_minimum_step(self) -> None:
import numpy as np
import scipy.integrate
minimum_step = 64.0 * math.ulp(1.0)
attempts = 0
class MinimumStepBdf:
def __init__(self, _fun, t0, y0, _t_bound, **_kwargs):
nonlocal attempts
attempts += 1
self.t = float(t0)
self.y = np.asarray(y0, dtype=float)
self.h_abs = 2.0 * minimum_step
self.status = "running"
def step(self):
raise RecoverableTrialStateError("minimum step reached")
with patch.object(scipy.integrate, "BDF", MinimumStepBdf):
result = integrate_ode(
rhs=lambda _time, _state: [0.0],
initial_state=[1.0],
config=SolveIVPConfig(t_stop=1.0, method="BDF", max_step=1.0),
cancel_check=lambda: False,
)
self.assertFalse(result.success)
self.assertEqual(result.message, "minimum step reached")
self.assertEqual(attempts, 1)
retry = result.solver_segments[0].recoverable_retries[0]
self.assertEqual(retry.attempted_step, 2.0 * minimum_step)
self.assertIsNone(retry.next_max_step)
def test_recoverable_retry_has_sixteen_retry_limit(self) -> None:
import scipy.integrate
attempts: list[dict[str, object]] = []
class AlwaysFailingConstructorBdf:
def __init__(self, _fun, _t0, _y0, _t_bound, **kwargs):
attempts.append(dict(kwargs))
raise RecoverableTrialStateError("persistent trial failure")
with patch.object(
scipy.integrate,
"BDF",
AlwaysFailingConstructorBdf,
):
result = integrate_ode(
rhs=lambda _time, _state: [0.0],
initial_state=[1.0],
config=SolveIVPConfig(t_stop=1.0, method="BDF", max_step=1.0),
cancel_check=lambda: False,
)
self.assertFalse(result.success)
self.assertEqual(len(attempts), 17)
diagnostics = result.solver_segments[0].recoverable_retries
self.assertEqual(len(diagnostics), 17)
self.assertEqual(
sum(retry.next_max_step is not None for retry in diagnostics),
16,
)
self.assertIsNone(diagnostics[-1].next_max_step)
def test_recoverable_retry_does_not_swallow_cancellation(self) -> None:
import numpy as np
import scipy.integrate
attempts = 0
class CancellingBdf:
def __init__(self, _fun, t0, y0, _t_bound, **_kwargs):
nonlocal attempts
attempts += 1
self.t = float(t0)
self.y = np.asarray(y0, dtype=float)
self.status = "running"
def step(self):
raise IntegrationCancelled
with patch.object(scipy.integrate, "BDF", CancellingBdf):
result = integrate_ode(
rhs=lambda _time, _state: [0.0],
initial_state=[1.0],
config=SolveIVPConfig(t_stop=1.0, method="BDF", max_step=1.0),
cancel_check=lambda: False,
)
self.assertFalse(result.success)
self.assertEqual(result.status, "cancelled")
self.assertEqual(attempts, 1)
self.assertEqual(result.solver_segments[0].recoverable_retry_count, 0)
def test_stepwise_solver_does_not_retry_ordinary_model_errors(self) -> None:
import numpy as np
import scipy.integrate
@@ -743,6 +1205,20 @@ class IntegrateOdeTests(unittest.TestCase):
self.assertAlmostEqual(result.y[0][1], 0.0, places=12)
self.assertAlmostEqual(result.y[0][2], 0.05, places=8)
self.assertAlmostEqual(result.y[0][-1], 0.65, places=8)
transition_segments = [
segment
for segment in result.solver_segments
if segment.state_transition_count
]
self.assertEqual(len(transition_segments), 1)
self.assertEqual(
transition_segments[0].state_transition_times,
(event_time,),
)
self.assertEqual(
transition_segments[0].as_dict()["stateTransitionTimes"],
[event_time],
)
def test_state_transitions_chain_at_same_time_until_state_repeats(self) -> None:
event_time = 0.25
+105
View File
@@ -0,0 +1,105 @@
from __future__ import annotations
from importlib import metadata
import os
from pathlib import Path
import re
import sys
import unittest
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
REQUIREMENTS_PATH = REPOSITORY_ROOT / "requirements.txt"
CONSTRAINTS_PATH = (
REPOSITORY_ROOT / "constraints" / "python312-direct.txt"
)
PYTHON_VERSION_PATH = REPOSITORY_ROOT / ".python-version"
VERIFY_ENVIRONMENT_VARIABLE = "SYSTEM_SIMULATION_VERIFY_LOCKED_ENV"
REFERENCE_DIRECT_VERSIONS = {
"fastapi": "0.141.1",
"lxml": "6.1.1",
"numpy": "2.5.2",
"pydantic": "2.13.4",
"scipy": "1.18.0",
"uvicorn": "0.52.3",
}
def _active_lines(path: Path) -> tuple[str, ...]:
return tuple(
line
for raw_line in path.read_text(encoding="utf-8").splitlines()
if (line := raw_line.strip()) and not line.startswith("#")
)
def _normalized_name(requirement: str) -> str:
match = re.match(r"[A-Za-z0-9_.-]+", requirement)
if match is None:
raise AssertionError(f"Invalid requirement line: {requirement!r}")
return match.group(0).lower().replace("_", "-")
def _constraint_versions() -> dict[str, str]:
result: dict[str, str] = {}
for line in _active_lines(CONSTRAINTS_PATH):
parts = line.split("==")
if len(parts) != 2 or not all(parts):
raise AssertionError(
f"Direct constraint must be an exact package pin: {line!r}"
)
name, version = parts
normalized = _normalized_name(name)
if normalized in result:
raise AssertionError(f"Duplicate direct constraint: {normalized}")
result[normalized] = version
return result
class DependencyConstraintContractTests(unittest.TestCase):
def test_python_reference_version_is_explicit(self) -> None:
self.assertEqual(
PYTHON_VERSION_PATH.read_text(encoding="utf-8").strip(),
"3.12.3",
)
def test_requirements_remain_ranges_and_declare_every_direct_package(
self,
) -> None:
requirements = _active_lines(REQUIREMENTS_PATH)
names = {_normalized_name(line) for line in requirements}
self.assertEqual(names, set(REFERENCE_DIRECT_VERSIONS))
self.assertTrue(all("==" not in line for line in requirements))
def test_reference_constraints_pin_only_direct_cross_platform_packages(
self,
) -> None:
self.assertEqual(_constraint_versions(), REFERENCE_DIRECT_VERSIONS)
self.assertTrue(
{
"httptools",
"pyyaml",
"uvloop",
"watchfiles",
"websockets",
}.isdisjoint(_constraint_versions())
)
@unittest.skipUnless(
os.getenv(VERIFY_ENVIRONMENT_VARIABLE, "").strip().lower()
in {"1", "true", "yes", "on"},
f"Set {VERIFY_ENVIRONMENT_VARIABLE}=1 to verify installed versions.",
)
def test_installed_environment_matches_reference_constraints(self) -> None:
self.assertEqual(sys.version_info[:3], (3, 12, 3))
installed = {
name: metadata.version(name)
for name in REFERENCE_DIRECT_VERSIONS
}
self.assertEqual(installed, REFERENCE_DIRECT_VERSIONS)
if __name__ == "__main__":
unittest.main()
+275
View File
@@ -0,0 +1,275 @@
from __future__ import annotations
from contextlib import redirect_stdout
from io import StringIO
from pathlib import Path
import unittest
from unittest.mock import patch
from app.simulation.benchmark_regression import (
DEFAULT_MANIFEST_PATH,
RegressionCaseRequest,
RegressionManifestError,
)
from app.simulation.max_step_matrix import main, run_max_step_matrix
def _fake_completed(
request: RegressionCaseRequest,
*,
prefix_bias: float = 0.0,
) -> dict[str, object]:
signal_times = [value for value in (0.04, 0.8) if value <= request.stop_time]
checkpoints = [
{
"requestedTime": checkpoint,
"actualTime": checkpoint,
"available": True,
"stateValues": {
"state.a": checkpoint + prefix_bias,
"state.b": 2.0 * checkpoint,
},
}
for checkpoint in request.checkpoint_times
]
event_trace = {
"signalEventTimes": signal_times,
"stateTransitionCount": 0,
"mechanicalTransitionTimes": [],
"mechanicalTransitionTimesAvailable": True,
}
return {
"outcome": "completed",
"orchestrationWallSeconds": 0.01,
"worker": {
"outcome": "completed",
"wallSeconds": 0.01,
"summary": {
"success": True,
"status": "completed",
"simulatedUntil": request.stop_time,
"checkpoints": checkpoints,
"physicalContract": {
"schemaVersion": 1,
"projectionCategories": ["state"],
"checkpoints": checkpoints,
"eventTrace": event_trace,
},
"eventTrace": event_trace,
"diagnostics": {
"pressureFlow": {"maxScaledResidual": 1.0e-12},
"integration": {
"totals": {
"nfev": int(100 * request.stop_time / request.max_step),
"njev": 4,
"nlu": 8,
"acceptedStepCount": int(
request.stop_time / request.max_step
),
"solverStartCount": len(signal_times) + 1,
"stateTransitionCount": 0,
"recoverableRetryCount": 0,
}
},
},
},
},
}
def _fake_failure(request: RegressionCaseRequest) -> dict[str, object]:
return {
"outcome": "failed",
"worker": {
"outcome": "failed",
"summary": {
"success": False,
"status": "failed",
"simulatedUntil": 0.75 * request.stop_time,
},
},
}
class MaxStepMatrixTests(unittest.TestCase):
def test_single_cell_passes_when_no_comparison_is_required(self) -> None:
report = run_max_step_matrix(
DEFAULT_MANIFEST_PATH,
horizon_case_ids=("1s",),
max_steps=(0.02,),
expected_projection_count=2,
case_executor=_fake_completed,
)
comparisons = report["comparisons"]
self.assertEqual(comparisons["sameHorizonAcrossMaxSteps"], [])
self.assertEqual(comparisons["sameMaxStepAcrossHorizons"], [])
self.assertEqual(comparisons["evaluatedCount"], 0)
self.assertTrue(comparisons["passed"])
self.assertTrue(report["acceptance"]["passed"])
with patch(
"app.simulation.max_step_matrix.run_max_step_matrix",
return_value=report,
), redirect_stdout(StringIO()):
exit_code = main(["--horizon", "1s", "--max-step", "0.02"])
self.assertEqual(exit_code, 0)
def test_multiple_cells_still_fail_when_comparison_is_unavailable(self) -> None:
def execute(request: RegressionCaseRequest) -> dict[str, object]:
if request.max_step == 0.05:
return _fake_failure(request)
return _fake_completed(request)
report = run_max_step_matrix(
DEFAULT_MANIFEST_PATH,
horizon_case_ids=("1s",),
max_steps=(0.01, 0.05),
expected_projection_count=2,
case_executor=execute,
)
comparisons = report["comparisons"]
records = comparisons["sameHorizonAcrossMaxSteps"]
self.assertEqual(len(records), 1)
self.assertFalse(records[0]["evaluated"])
self.assertEqual(records[0]["reason"], "oneOrBothCasesDidNotComplete")
self.assertFalse(comparisons["passed"])
self.assertFalse(report["acceptance"]["passed"])
with patch(
"app.simulation.max_step_matrix.run_max_step_matrix",
return_value=report,
), redirect_stdout(StringIO()):
exit_code = main(
[
"--horizon",
"1s",
"--max-step",
"0.01",
"--max-step",
"0.05",
]
)
self.assertEqual(exit_code, 1)
def test_custom_horizon_and_common_prefix_matrix(self) -> None:
source = Path("tests/data/test-mql-8.xml")
source_before = source.read_bytes()
requests: list[RegressionCaseRequest] = []
def execute(request: RegressionCaseRequest) -> dict[str, object]:
requests.append(request)
return _fake_completed(request)
report = run_max_step_matrix(
DEFAULT_MANIFEST_PATH,
horizon_case_ids=("1s", 2.0),
max_steps=(0.01, 0.05),
additional_checkpoint_times=(1.0885267285, 1.0997760276),
expected_projection_count=2,
case_executor=execute,
)
self.assertEqual(source.read_bytes(), source_before)
self.assertEqual(len(requests), 4)
self.assertEqual(
[(request.stop_time, request.max_step) for request in requests],
[(1.0, 0.01), (1.0, 0.05), (2.0, 0.01), (2.0, 0.05)],
)
custom_requests = [request for request in requests if request.stop_time == 2.0]
self.assertEqual(
custom_requests[0].checkpoint_times,
(0.0, 0.04, 0.8, 1.0, 1.0885267285, 1.0997760276, 2.0),
)
self.assertEqual(
report["configuration"]["horizons"][1]["horizonCaseId"],
"2s-custom",
)
self.assertEqual(
len(report["comparisons"]["sameHorizonAcrossMaxSteps"]), 2
)
self.assertEqual(
len(report["comparisons"]["sameMaxStepAcrossHorizons"]), 2
)
self.assertTrue(report["comparisons"]["passed"])
self.assertTrue(report["acceptance"]["passed"])
def test_failed_tier_defers_every_later_cell(self) -> None:
requests: list[RegressionCaseRequest] = []
def execute(request: RegressionCaseRequest) -> dict[str, object]:
requests.append(request)
if request.max_step == 0.05:
return _fake_failure(request)
return _fake_completed(request)
report = run_max_step_matrix(
DEFAULT_MANIFEST_PATH,
horizon_case_ids=("1s", "5s", "10s"),
max_steps=(0.01, 0.05),
expected_projection_count=2,
case_executor=execute,
)
self.assertEqual(len(requests), 2)
self.assertEqual(report["acceptance"]["deferredCellCount"], 4)
self.assertEqual(
[case["outcome"] for case in report["cases"][2:]],
["deferred"] * 4,
)
self.assertEqual(
report["tierDecisions"][1]["reason"], "previousTierDidNotPass"
)
def test_tstop_dependent_prefix_is_reported(self) -> None:
def execute(request: RegressionCaseRequest) -> dict[str, object]:
return _fake_completed(
request,
prefix_bias=0.1 if request.stop_time > 1.0 else 0.0,
)
report = run_max_step_matrix(
DEFAULT_MANIFEST_PATH,
horizon_case_ids=("1s", 2.0),
max_steps=(0.01, 0.05),
expected_projection_count=2,
stop_after_failed_tier=False,
case_executor=execute,
)
prefix_comparisons = report["comparisons"][
"sameMaxStepAcrossHorizons"
]
self.assertEqual(len(prefix_comparisons), 2)
self.assertTrue(all(item["evaluated"] for item in prefix_comparisons))
self.assertTrue(all(not item["passed"] for item in prefix_comparisons))
self.assertGreater(
prefix_comparisons[0]["stateProjection"]["valueMismatchCount"], 0
)
def test_horizons_must_increase_and_timeout_override_is_bounded(self) -> None:
with self.assertRaisesRegex(
RegressionManifestError, "strictly increasing"
):
run_max_step_matrix(
DEFAULT_MANIFEST_PATH,
horizon_case_ids=(2.0, "1s"),
max_steps=(0.01, 0.05),
expected_projection_count=2,
case_executor=_fake_completed,
)
with self.assertRaisesRegex(RegressionManifestError, "must exceed"):
run_max_step_matrix(
DEFAULT_MANIFEST_PATH,
horizon_case_ids=(2.0,),
max_steps=(0.01, 0.05),
soft_timeout_seconds=20.0,
hard_timeout_seconds=10.0,
expected_projection_count=2,
case_executor=_fake_completed,
)
if __name__ == "__main__":
unittest.main()
+114
View File
@@ -0,0 +1,114 @@
from __future__ import annotations
import hashlib
import os
from pathlib import Path
import unittest
from app.main import compile_system_xml_network
from app.simulation.benchmark_regression import (
load_regression_manifest,
run_regression_suite,
source_simulation_config,
)
from app.simulation.systems.generic import GenericFluidSystem
from app.system_xml import validate_system_xml_document
MANIFEST_PATH = Path(
"tests/baselines/simulation/test_mql_full_branches/manifest.json"
)
LONG_RUN_ENVIRONMENT = "RUN_TEST_MQL_FULL_BRANCHES_LONG_REGRESSION"
class MqlFullBranchesStaticRegressionTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.manifest = load_regression_manifest(MANIFEST_PATH)
cls.source_path = Path(cls.manifest["_sourcePath"])
cls.source_payload = cls.source_path.read_bytes()
cls.validation = validate_system_xml_document(cls.source_payload)
def test_authoritative_xml_hash_and_simulation_settings_are_fixed(self) -> None:
source = self.manifest["source"]
self.assertEqual(
hashlib.sha256(self.source_payload).hexdigest(), source["sha256"]
)
self.assertEqual(len(self.source_payload), source["bytes"])
self.assertEqual(
source_simulation_config(self.source_payload), source["simulation"]
)
def test_structure_snapshot_covers_the_historical_solver_shape(self) -> None:
self.assertTrue(self.validation.valid, self.validation.as_dict())
assert self.validation.document is not None
network = compile_system_xml_network(self.validation.document)
system = GenericFluidSystem(network)
expected = self.manifest["structure"]
pressure_flow = network.pressure_flow_structure_dict()
causal_execution = system.pressure_flow_solver.causal_execution_diagnostics()
jacobian = system.jacobian_sparsity_diagnostics()
self.assertEqual(len(network.components), expected["componentCount"])
self.assertEqual(len(network.connections), expected["connectionCount"])
self.assertEqual(
len(network.dynamic_components()), expected["dynamicComponentCount"]
)
self.assertEqual(
sum(component.state_size for component in network.dynamic_components()),
expected["stateCount"],
)
self.assertEqual(
len(network.result_variable_metadata()), expected["resultVariableCount"]
)
self.assertEqual(
pressure_flow["unknownCount"], expected["pressureFlowUnknownCount"]
)
self.assertEqual(
pressure_flow["equationCount"], expected["pressureFlowEquationCount"]
)
self.assertEqual(pressure_flow["isSquare"], expected["pressureFlowIsSquare"])
for key in (
"logicalEffortCoordinateCount",
"eliminatedEffortAliasCount",
"canonicalCoordinateCount",
"compatibilityScatterCount",
):
self.assertEqual(causal_execution[key], expected[key])
self.assertEqual(jacobian["nonzeroCount"], expected["jacobianNonzeroCount"])
self.assertEqual(
jacobian["colorGroupCount"], expected["jacobianColorGroupCount"]
)
self.assertEqual(
system.mechanical_state_reducer.has_state_events,
expected["hasMechanicalStateEvents"],
)
def test_both_horizons_have_the_expected_signal_schedule(self) -> None:
assert self.validation.document is not None
system = GenericFluidSystem(
compile_system_xml_network(self.validation.document)
)
for case_id in self.manifest["sequence"]:
variant = self.manifest["variants"][case_id]
actual = system.signal_resolver.event_times(
0.0, float(variant["stopTime"])
)
self.assertEqual(actual, tuple(variant["expectedSignalEventTimes"]))
@unittest.skipUnless(
os.getenv(LONG_RUN_ENVIRONMENT, "").strip().lower() in {"1", "true", "yes"},
f"Set {LONG_RUN_ENVIRONMENT}=1 to run bounded 0.81/2.10 s integration.",
)
class MqlFullBranchesLongRegressionTests(unittest.TestCase):
def test_both_horizons_complete_within_their_safety_budgets(self) -> None:
report = run_regression_suite(MANIFEST_PATH, lane="solver-only")
outcomes = [case["outcome"] for case in report["cases"]]
self.assertEqual(outcomes, ["completed", "completed"], report["cases"])
if __name__ == "__main__":
unittest.main()
+268 -2
View File
@@ -6,6 +6,8 @@ from unittest.mock import patch
from app.main import compile_reactflow_network
from app.simulation.solvers.algebraic import (
CAUSAL_COORDINATE_KERNEL_ENVIRONMENT_VARIABLE,
CAUSAL_EXECUTOR_V2_ENVIRONMENT_VARIABLE,
CAUSAL_FAST_PATH_ENVIRONMENT_VARIABLE,
)
from app.simulation.systems.generic import GenericFluidSystem
@@ -16,11 +18,275 @@ from tests.test_amesim_pnvo001_signal_xml import (
from tests.test_generic_system_xml_simulation import chain_project
def _system(project) -> GenericFluidSystem:
return GenericFluidSystem(compile_reactflow_network(project))
def _system(
project,
*,
executor_v2: bool | None = False,
coordinate_kernel: bool | None = None,
) -> GenericFluidSystem:
environment = {}
if executor_v2 is not None:
environment[CAUSAL_EXECUTOR_V2_ENVIRONMENT_VARIABLE] = (
"1" if executor_v2 else "0"
)
if coordinate_kernel is not None:
environment[CAUSAL_COORDINATE_KERNEL_ENVIRONMENT_VARIABLE] = (
"1" if coordinate_kernel else "0"
)
with patch.dict(
os.environ,
environment,
):
if executor_v2 is None:
os.environ.pop(CAUSAL_EXECUTOR_V2_ENVIRONMENT_VARIABLE, None)
if coordinate_kernel is None:
os.environ.pop(
CAUSAL_COORDINATE_KERNEL_ENVIRONMENT_VARIABLE,
None,
)
return GenericFluidSystem(compile_reactflow_network(project))
class PressureFlowCausalExecutionTests(unittest.TestCase):
def test_compiled_v2_is_enabled_by_default_and_can_be_disabled(self) -> None:
default = _system(zero_force_mass_project(), executor_v2=None)
disabled = _system(zero_force_mass_project(), executor_v2=False)
enabled = _system(zero_force_mass_project(), executor_v2=True)
self.assertTrue(
default.pressure_flow_solver.causal_execution_diagnostics()[
"executorV2Configured"
]
)
self.assertFalse(
disabled.pressure_flow_solver.causal_execution_diagnostics()[
"executorV2Configured"
]
)
self.assertTrue(
enabled.pressure_flow_solver.causal_execution_diagnostics()[
"executorV2Configured"
]
)
def test_coordinate_kernel_is_default_on_and_independently_disabled(
self,
) -> None:
default = _system(
zero_force_mass_project(),
executor_v2=True,
)
disabled = _system(
zero_force_mass_project(),
executor_v2=True,
coordinate_kernel=False,
)
default_diagnostics = (
default.pressure_flow_solver.causal_execution_diagnostics()
)
disabled_diagnostics = (
disabled.pressure_flow_solver.causal_execution_diagnostics()
)
self.assertTrue(default_diagnostics["coordinateKernelConfigured"])
self.assertTrue(default_diagnostics["coordinateKernelEnabled"])
self.assertFalse(disabled_diagnostics["coordinateKernelConfigured"])
self.assertFalse(disabled_diagnostics["coordinateKernelEnabled"])
def test_compiled_v2_matches_v1_bitwise_without_target_names(self) -> None:
compiled = _system(
high_pressure_helium_step_project(),
executor_v2=True,
)
v1 = _system(high_pressure_helium_step_project())
compiled_state = compiled.initial_state_vector()
v1_state = v1.initial_state_vector()
for time in (0.0, 0.041, 0.8):
self.assertEqual(
compiled.rhs(time, compiled_state),
v1.rhs(time, v1_state),
)
self.assertEqual(
tuple(
unknown.read()
for unknown in compiled.pressure_flow_solver.unknowns
),
tuple(
unknown.read()
for unknown in v1.pressure_flow_solver.unknowns
),
)
diagnostics = (
compiled.pressure_flow_solver.causal_execution_diagnostics()
)
self.assertGreater(diagnostics["executorV2FastSolveCount"], 0)
self.assertGreater(diagnostics["coordinateKernelFastSolveCount"], 0)
self.assertEqual(
diagnostics["compiledAssignmentCount"],
len(compiled.pressure_flow_solver.unknowns),
)
self.assertEqual(
diagnostics["executorV2RuntimeValidationFailureCount"],
0,
)
self.assertEqual(
diagnostics["canonicalCoordinateCount"],
diagnostics["logicalEffortCoordinateCount"]
+ diagnostics["compiledFlowAssignmentCount"],
)
self.assertEqual(
diagnostics["eliminatedEffortAliasCount"],
diagnostics["compiledEffortUnknownCount"]
- diagnostics["logicalEffortCoordinateCount"],
)
def test_compiled_v2_fast_solve_skips_legacy_seed_scan_and_scales(
self,
) -> None:
system = _system(zero_force_mass_project(), executor_v2=True)
state = system.initial_state_vector()
system.rhs(0.0, state)
solver = system.pressure_flow_solver
with patch.object(
solver,
"_solve_explicit_flow_unknowns",
wraps=solver._solve_explicit_flow_unknowns,
) as legacy_seed, patch.object(
solver,
"_pressure_flow_equation_values",
wraps=solver._pressure_flow_equation_values,
) as residuals, patch.object(
solver,
"_scales",
wraps=solver._scales,
) as scales, patch.object(
solver,
"_evaluate_explicit_flow_stage",
wraps=solver._evaluate_explicit_flow_stage,
) as allocating_stage:
diagnostics = solver.solve(effort_variables=("p",))
legacy_seed.assert_not_called()
residuals.assert_not_called()
scales.assert_not_called()
allocating_stage.assert_not_called()
self.assertIs(
diagnostics,
solver._causal_cached_fast_diagnostics,
)
self.assertTrue(diagnostics.causal_fast_path_used)
def test_compiled_v2_nonfinite_assignment_fuses_and_audits_same_solve(
self,
) -> None:
system = _system(zero_force_mass_project(), executor_v2=True)
state = system.initial_state_vector()
system.rhs(0.0, state)
solver = system.pressure_flow_solver
with patch.object(
solver,
"_execute_causal_coordinate_flow_plan",
return_value="nonFiniteCausalFlowAssignment",
):
diagnostics = solver.solve(effort_variables=("p",))
self.assertTrue(diagnostics.success)
self.assertTrue(diagnostics.residual_verified_this_solve)
execution = solver.causal_execution_diagnostics()
self.assertFalse(execution["enabled"])
self.assertEqual(
execution["disabledReason"],
"nonFiniteCausalFlowAssignment",
)
self.assertEqual(execution["executorV2RuntimeValidationFailureCount"], 1)
self.assertEqual(execution["legacyFallbackCount"], 1)
def test_compiled_v2_rejects_nonfinite_external_mechanical_effort(
self,
) -> None:
system = _system(zero_force_mass_project(), executor_v2=True)
state = system.initial_state_vector()
system.rhs(0.0, state)
solver = system.pressure_flow_solver
velocity = next(
unknown for unknown in solver.unknowns if unknown.variable == "v"
)
velocity.write(float("nan"))
with self.assertRaises(ValueError):
solver.solve(effort_variables=("p",))
execution = solver.causal_execution_diagnostics()
self.assertFalse(execution["enabled"])
self.assertEqual(
execution["disabledReason"],
"nonFiniteCausalExternalEffort",
)
self.assertEqual(execution["executorV2RuntimeValidationFailureCount"], 1)
self.assertEqual(execution["legacyFallbackCount"], 1)
def test_compiled_v2_stage_error_fuses_to_existing_fallback(self) -> None:
system = _system(zero_force_mass_project(), executor_v2=True)
state = system.initial_state_vector()
system.rhs(0.0, state)
solver = system.pressure_flow_solver
with patch.object(
solver,
"_execute_causal_coordinate_flow_plan",
return_value="causalFlowEvaluationFailed:ValueError",
):
diagnostics = solver.solve(effort_variables=("p",))
self.assertTrue(diagnostics.success)
self.assertTrue(diagnostics.residual_verified_this_solve)
execution = solver.causal_execution_diagnostics()
self.assertEqual(
execution["disabledReason"],
"causalFlowEvaluationFailed:ValueError",
)
self.assertEqual(execution["executorV2RuntimeValidationFailureCount"], 1)
def test_compiled_v2_assignment_count_drift_fuses_to_fallback(self) -> None:
system = _system(zero_force_mass_project(), executor_v2=True)
state = system.initial_state_vector()
system.rhs(0.0, state)
solver = system.pressure_flow_solver
with patch.object(
solver,
"_execute_causal_coordinate_flow_plan",
return_value="causalFlowAssignmentCountMismatch",
):
diagnostics = solver.solve(effort_variables=("p",))
self.assertTrue(diagnostics.success)
self.assertTrue(diagnostics.residual_verified_this_solve)
execution = solver.causal_execution_diagnostics()
self.assertEqual(
execution["disabledReason"],
"causalFlowAssignmentCountMismatch",
)
self.assertEqual(execution["executorV2RuntimeValidationFailureCount"], 1)
def test_compiled_v2_keeps_the_sixty_four_solve_audit_boundary(self) -> None:
system = _system(zero_force_mass_project(), executor_v2=True)
state = system.initial_state_vector()
solver = system.pressure_flow_solver
system.rhs(0.0, state)
for _iteration in range(64):
system.rhs(0.0, state)
before = solver.causal_execution_diagnostics()
self.assertEqual(before["executorV2FastSolveCount"], 64)
self.assertEqual(before["fullResidualAuditCount"], 1)
system.rhs(0.0, state)
after = solver.causal_execution_diagnostics()
self.assertEqual(after["executorV2FastSolveCount"], 64)
self.assertEqual(after["fullResidualAuditCount"], 2)
def test_strict_causal_rhs_matches_environment_disabled_legacy_bitwise(
self,
) -> None:
+1 -2
View File
@@ -5,7 +5,6 @@ import unittest
from app.simulation.examples.testmodel.run import prepare_testmodel_run
from app.simulation.paths import (
PROJECT_ROOT,
SIMULATION_BASELINES_DIR,
SIMULATION_RUNS_DIR,
)
@@ -18,7 +17,7 @@ class SimulationPathTests(unittest.TestCase):
self.assertEqual(prepared.output_dir.parent, SIMULATION_RUNS_DIR)
def test_testmodel_baselines_live_with_the_test_assets(self) -> None:
baseline_dir = SIMULATION_BASELINES_DIR / "testmodel"
baseline_dir = PROJECT_ROOT / "tests" / "data" / "testmodel"
self.assertTrue((baseline_dir / "testmodel_primary_series.csv").is_file())
self.assertTrue(
+149 -1
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import os
from types import SimpleNamespace
import unittest
from unittest.mock import patch
@@ -13,6 +14,7 @@ from app.simulation.components.experimental.storage.cylinder import Cylinder
from app.simulation.components.experimental.storage.tank import Tank
from app.simulation.core.medium import IdealGasMedium
from app.simulation.solvers.algebraic import (
CAUSAL_EXECUTOR_V2_ENVIRONMENT_VARIABLE,
AlgebraicSolveDiagnostics,
PressureFlowSolver,
)
@@ -26,6 +28,7 @@ from app.simulation.systems.network import SimulationNetwork
def _pnl0002_solver(
*,
include_unselected_island: bool = False,
executor_v2: bool = False,
) -> tuple[PressureFlowSolver, AmesimPnl0002]:
medium = IdealGasMedium()
left = Cylinder("left", medium, V=0.1, p0=500_000.0)
@@ -51,12 +54,157 @@ def _pnl0002_solver(
for component in network.dynamic_components():
component.refresh_thermodynamic_ports()
solver = PressureFlowSolver(network)
with patch.dict(
os.environ,
{
CAUSAL_EXECUTOR_V2_ENVIRONMENT_VARIABLE: (
"1" if executor_v2 else "0"
)
},
):
solver = PressureFlowSolver(network)
solver.solve()
return solver, pipe
class StreamPressureBlockSolverTests(unittest.TestCase):
def test_compiled_v2_secondary_matches_v1_and_skips_seed_sets(self) -> None:
compiled_solver, _compiled_pipe = _pnl0002_solver(executor_v2=True)
v1_solver, _v1_pipe = _pnl0002_solver()
compiled = StreamPressureBlockSolver(compiled_solver, ("pipe",))
v1 = StreamPressureBlockSolver(v1_solver, ("pipe",))
compiled.solve(scale_context=compiled_solver.scale_context())
v1.solve(scale_context=v1_solver.scale_context())
with patch.object(
compiled,
"_seed_selected_blocks",
wraps=compiled._seed_selected_blocks,
) as legacy_seed, patch.object(
compiled_solver,
"_evaluate_explicit_flow_stage",
wraps=compiled_solver._evaluate_explicit_flow_stage,
) as allocating_stage:
compiled_result = compiled.solve(
scale_context=compiled_solver.scale_context()
)
v1.solve(scale_context=v1_solver.scale_context())
legacy_seed.assert_not_called()
allocating_stage.assert_not_called()
self.assertEqual(
tuple(unknown.read() for unknown in compiled_solver.unknowns),
tuple(unknown.read() for unknown in v1_solver.unknowns),
)
self.assertIs(
compiled_result.diagnostics[0],
compiled._causal_cached_fast_diagnostics,
)
execution = compiled.causal_execution_diagnostics()
self.assertEqual(execution["executorV2FastSolveCount"], 1)
self.assertEqual(execution["coordinateKernelFastSolveCount"], 1)
self.assertEqual(
execution["compiledAssignmentCount"],
len(compiled._selected_unknowns),
)
self.assertEqual(execution["executorV2RuntimeValidationFailureCount"], 0)
def test_compiled_v2_secondary_failure_restores_before_legacy_seed(
self,
) -> None:
solver, _pipe = _pnl0002_solver(executor_v2=True)
block_solver = StreamPressureBlockSolver(solver, ("pipe",))
block_solver.solve(scale_context=solver.scale_context())
expected = tuple(
unknown.read() for unknown in block_solver._selected_flow_unknowns
)
original_seed = block_solver._seed_selected_blocks
def one_nonfinite_assignment():
block_solver._selected_flow_unknowns[0].state.m_flow = float("nan")
return "nonFiniteCausalSecondaryFlowAssignment"
def checked_legacy_seed(entry_values):
self.assertEqual(
tuple(
unknown.read()
for unknown in block_solver._selected_flow_unknowns
),
expected,
)
return original_seed(entry_values)
with patch.object(
block_solver,
"_execute_compiled_secondary_flow_plan",
side_effect=one_nonfinite_assignment,
), patch.object(
block_solver,
"_seed_selected_blocks",
side_effect=checked_legacy_seed,
):
result = block_solver.solve(scale_context=solver.scale_context())
self.assertFalse(result.used_global_fallback)
execution = block_solver.causal_execution_diagnostics()
self.assertFalse(execution["enabled"])
self.assertEqual(
execution["disabledReason"],
"nonFiniteCausalSecondaryFlowAssignment",
)
self.assertEqual(execution["executorV2RuntimeValidationFailureCount"], 1)
self.assertEqual(execution["legacyFallbackCount"], 1)
def test_compiled_v2_secondary_base_error_restores_transaction(self) -> None:
class ForcedFatalError(BaseException):
pass
solver, _pipe = _pnl0002_solver(executor_v2=True)
block_solver = StreamPressureBlockSolver(solver, ("pipe",))
block_solver.solve(scale_context=solver.scale_context())
expected = tuple(
unknown.read() for unknown in block_solver._selected_flow_unknowns
)
def broken_execution() -> None:
block_solver._selected_flow_unknowns[0].write(123_456.0)
raise ForcedFatalError("forced v2 interruption")
with patch.object(
block_solver,
"_execute_compiled_secondary_flow_plan",
side_effect=broken_execution,
):
with self.assertRaises(ForcedFatalError):
block_solver.solve(scale_context=solver.scale_context())
self.assertEqual(
tuple(
unknown.read()
for unknown in block_solver._selected_flow_unknowns
),
expected,
)
def test_compiled_v2_secondary_assignment_count_drift_falls_back(self) -> None:
solver, _pipe = _pnl0002_solver(executor_v2=True)
block_solver = StreamPressureBlockSolver(solver, ("pipe",))
block_solver.solve(scale_context=solver.scale_context())
with patch.object(
block_solver,
"_execute_compiled_secondary_flow_plan",
return_value="causalSecondaryFlowAssignmentCountMismatch",
):
result = block_solver.solve(scale_context=solver.scale_context())
self.assertFalse(result.used_global_fallback)
execution = block_solver.causal_execution_diagnostics()
self.assertEqual(
execution["disabledReason"],
"causalSecondaryFlowAssignmentCountMismatch",
)
self.assertEqual(execution["executorV2RuntimeValidationFailureCount"], 1)
def test_pnl0002_uses_two_blocks_with_one_shared_component(self) -> None:
solver, pipe = _pnl0002_solver()
block_solver = StreamPressureBlockSolver(solver, ("pipe",))
@@ -65,6 +65,25 @@ class _PassThrough(AlgebraicComponent):
self.right.h_outflow = connected_h["left"]
class _ReferenceAwarePassThrough(_PassThrough):
def __init__(
self,
name: str,
update_log: list[str],
reference_log: list[str],
) -> None:
super().__init__(name, update_log)
self.reference_log = reference_log
self.flow_temperature_references: dict[str, float] = {}
def update_flow_temperature_references(
self,
connected_h: Mapping[str, float],
) -> None:
self.reference_log.append(self.name)
self.flow_temperature_references = dict(connected_h)
def _build_chain() -> tuple[
SimulationNetwork,
_CountingDynamicAnchor,
@@ -132,6 +151,53 @@ class StreamResolverExecutionPlanTests(unittest.TestCase):
["first", "second"] * diagnostics.iterations,
)
def test_non_dynamic_flow_reference_hook_does_not_repeat_stream_update(
self,
) -> None:
update_log: list[str] = []
reference_log: list[str] = []
left = _CountingDynamicAnchor(
"left_anchor",
enthalpy=100.0,
temperature_reference_h=1_100.0,
)
middle = _ReferenceAwarePassThrough(
"middle",
update_log,
reference_log,
)
right = _CountingDynamicAnchor(
"right_anchor",
enthalpy=400.0,
temperature_reference_h=1_400.0,
)
network = SimulationNetwork("flow-temperature-reference")
for component in (left, middle, right):
network.add_component(component)
network.connect("left_anchor", "port", "middle", "left")
network.connect("middle", "right", "right_anchor", "port")
resolver = StreamResolver(network)
diagnostics, _connected = resolver.solve()
stream_updates_before = tuple(update_log)
outflows_before = (middle.left.h_outflow, middle.right.h_outflow)
resolver.refresh_flow_temperature_references()
self.assertEqual(
stream_updates_before,
("middle",) * diagnostics.iterations,
)
self.assertEqual(tuple(update_log), stream_updates_before)
self.assertEqual(reference_log, ["middle"])
self.assertEqual(
middle.flow_temperature_references,
{"left": 1_100.0, "right": 1_400.0},
)
self.assertEqual(
(middle.left.h_outflow, middle.right.h_outflow),
outflows_before,
)
def test_precompiled_bindings_preserve_outputs_and_references(self) -> None:
default_network, default_left, default_first, default_second, default_right, _ = (
_build_chain()
+289
View File
@@ -0,0 +1,289 @@
from pathlib import Path
import os
import unittest
from unittest.mock import patch
import numpy as np
from scipy.optimize._numdiff import group_columns
from app.main import compile_system_xml_network
from app.simulation.solvers.jacobian import (
ExactColumnsUnavailable,
SparseSecantJacobian,
)
import app.simulation.solvers.tangent as tangent_module
from app.simulation.solvers.tangent import (
compile_supported_piston_tangent_provider,
)
from app.simulation.systems.generic import GenericFluidSystem
from app.simulation.systems.network import Endpoint
from app.system_xml import validate_system_xml_document
TARGET_XML = Path("tests/data/test-mql-8.xml")
EXPECTED_COLUMNS = tuple(range(104, 120))
EXPECTED_BRANCH_NAMES = (
(
"amesim_mecmas21_1",
"amesim_pnrp17_1",
"amesim_pnch012_15",
"amesim_pnl0001_13",
"amesim_lstp00a_1",
),
(
"amesim_mecmas21_2",
"amesim_pnrp17_2",
"amesim_pnch012_14",
"amesim_pnl0001_14",
"amesim_lstp00a_2",
),
(
"amesim_mecmas21_3",
"amesim_pnrp17_3",
"amesim_pnch012_13",
"amesim_pnl0001_15",
"amesim_lstp00a_3",
),
(
"amesim_mecmas21_4",
"amesim_pnrp17_4",
"amesim_pnch012_12",
"amesim_pnl0001_16",
"amesim_lstp00a_4",
),
(
"amesim_mecmas21_5",
"amesim_pnrp17_5",
"amesim_pnch012_11",
"amesim_pnl0001_17",
"amesim_lstp00a_5",
),
(
"amesim_mecmas21_6",
"amesim_pnrp17_6",
"amesim_pnch012_10",
"amesim_pnl0001_18",
"amesim_lstp00a_6",
),
(
"amesim_mecmas21_7",
"amesim_pnrp17_7",
"amesim_pnch012_9",
"amesim_pnl0001_19",
"amesim_lstp00a_7",
),
(
"amesim_mecmas21_8",
"amesim_pnrp17_8",
"amesim_pnch012_8",
"amesim_pnl0001_20",
"amesim_lstp00a_8",
),
)
class SupportedPistonTangentCompilerTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
report = validate_system_xml_document(TARGET_XML.read_bytes())
assert report.valid and report.document is not None
with patch.dict(os.environ, {"SIMULATION_CAUSAL_FAST_PATH": "1"}):
cls.system = GenericFluidSystem(
compile_system_xml_network(report.document)
)
cls.initial_state = np.asarray(
cls.system.consistent_initial_state_vector(0.0),
dtype=float,
)
def setUp(self) -> None:
self.system.mechanical_state_reducer.reset_constraint_modes()
self.system.apply_state_vector(self.initial_state.tolist())
def _closed_primal(
self,
state: np.ndarray,
) -> tuple[dict[str, dict[str, float]], np.ndarray]:
self.system.apply_state_vector(state.tolist())
connected_h = self.system._close_current_state(0.0)
derivative = np.asarray(
self.system._state_derivatives(connected_h),
dtype=float,
)
return connected_h, derivative
def test_discovers_all_branches_without_legacy_names_or_offsets(self) -> None:
with patch.object(
tangent_module,
"_TARGET_BRANCH_NAMES",
(("not", "a", "real", "branch", "name"),),
):
compilation = compile_supported_piston_tangent_provider(
self.system
)
self.assertTrue(compilation.eligible, compilation.reason)
self.assertEqual(compilation.columns, EXPECTED_COLUMNS)
self.assertEqual(compilation.reached_assignment_count, 84)
provider = compilation.provider
assert provider is not None
self.assertEqual(len(provider.branches), 8)
self.assertEqual(
tuple(
(
branch.mass.name,
branch.piston.name,
branch.chamber.name,
branch.pipe.name,
branch.contact.name,
)
for branch in provider.branches
),
EXPECTED_BRANCH_NAMES,
)
self.assertEqual(
{branch.chamber_connection_port for branch in provider.branches},
{"port_2"},
)
def test_exact_columns_reduce_seed_zero_pattern_to_36_colors(self) -> None:
compilation = compile_supported_piston_tangent_provider(self.system)
self.assertTrue(compilation.eligible, compilation.reason)
provider = compilation.provider
assert provider is not None
pattern = self.system.jacobian_sparsity().tocsc().astype(bool)
columns_only = pattern.tolil(copy=True)
columns_only[:, list(compilation.columns)] = False
columns_only = columns_only.tocsc()
columns_only.eliminate_zeros()
active_columns = np.flatnonzero(
np.asarray(columns_only.getnnz(axis=0)).reshape(-1) > 0
)
groups = group_columns(
columns_only[:, active_columns],
order=0,
)
self.assertEqual(pattern.nnz, 3296)
self.assertEqual(columns_only.nnz, 2464)
self.assertEqual(len(active_columns), 108)
self.assertEqual(int(groups.max(initial=-1)) + 1, 36)
jacobian = SparseSecantJacobian(
lambda _time, state: np.zeros_like(state),
pattern,
1.0e-8,
exact_rows=self.system._exact_ode_jacobian_rows(),
exact_columns=(compilation.columns, provider),
max_consecutive_reuses=0,
)
diagnostics = jacobian.diagnostics()
self.assertEqual(diagnostics["originalColorGroupCount"], 52)
self.assertEqual(diagnostics["remainingColorGroupCount"], 36)
self.assertEqual(diagnostics["exactColumnCount"], 16)
self.assertEqual(diagnostics["finiteDifferenceColumnCount"], 108)
self.assertEqual(
jacobian._remaining_finite_difference_sparsity.nnz,
2176,
)
def test_partial_unsupported_branch_fails_with_stable_reason(self) -> None:
piston_endpoint = Endpoint("amesim_pnrp17_1", "port_5")
connection_index, connection = next(
(index, connection)
for index, connection in enumerate(self.system.network.connections)
if piston_endpoint in connection.endpoints
)
del self.system.network.connections[connection_index]
try:
compilation = compile_supported_piston_tangent_provider(
self.system
)
finally:
self.system.network.connections.insert(
connection_index,
connection,
)
self.assertFalse(compilation.eligible)
self.assertEqual(
compilation.reason,
"unsupportedPistonBranchTopology:contact",
)
def test_initial_contact_boundary_requests_full_numeric_columns(self) -> None:
compilation = compile_supported_piston_tangent_provider(self.system)
self.assertTrue(compilation.eligible, compilation.reason)
provider = compilation.provider
assert provider is not None
connected_h, _derivative = self._closed_primal(
self.initial_state.copy()
)
provider.request_primal_capture()
provider.record_primal(
0.0,
self.initial_state,
connected_h,
)
with self.assertRaises(ExactColumnsUnavailable) as captured:
provider(
0.0,
self.initial_state.copy(),
compilation.columns,
)
self.assertEqual(
captured.exception.reason,
"contactMode:contact_mode_boundary",
)
def test_smooth_sixteen_columns_match_centered_full_rhs(self) -> None:
compilation = compile_supported_piston_tangent_provider(self.system)
self.assertTrue(compilation.eligible, compilation.reason)
provider = compilation.provider
assert provider is not None
state = self.initial_state.copy()
for branch in provider.branches:
chamber_offset, chamber_size = provider.state_offsets[
branch.chamber.name
]
self.assertEqual(chamber_size, 2)
state[chamber_offset] *= 1.01
state[branch.position_index] -= 1.0e-3
connected_h, _base = self._closed_primal(state)
provider.request_primal_capture()
provider.record_primal(0.0, state, connected_h)
exact = provider(0.0, state.copy(), compilation.columns)
numerical = np.empty_like(exact)
for local_column, state_index in enumerate(compilation.columns):
step = 1.0e-7 * max(abs(state[state_index]), 1.0)
lower = state.copy()
upper = state.copy()
lower[state_index] -= step
upper[state_index] += step
lower_rhs = np.asarray(
self.system.rhs(0.0, lower.tolist()),
dtype=float,
)
upper_rhs = np.asarray(
self.system.rhs(0.0, upper.tolist()),
dtype=float,
)
numerical[:, local_column] = (
upper_rhs - lower_rhs
) / (2.0 * step)
np.testing.assert_allclose(
exact,
numerical,
rtol=3.0e-6,
atol=2.0e-5,
)
if __name__ == "__main__":
unittest.main()
+237
View File
@@ -0,0 +1,237 @@
from __future__ import annotations
import hashlib
import json
import os
from pathlib import Path
import unittest
from app.main import compile_system_xml_network
from app.simulation.benchmark_regression import (
DEFAULT_MANIFEST_PATH,
evaluate_regression_golden,
load_regression_golden,
load_regression_manifest,
run_regression_suite,
source_simulation_config,
)
from app.simulation.systems.generic import GenericFluidSystem
from app.system_xml import validate_system_xml_document
LONG_RUN_ENVIRONMENT = "RUN_TEST_MQL_8_LONG_REGRESSION"
class TestMql8StaticRegressionTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.manifest = load_regression_manifest(DEFAULT_MANIFEST_PATH)
cls.source_path = Path(cls.manifest["_sourcePath"])
cls.source_payload = cls.source_path.read_bytes()
cls.validation = validate_system_xml_document(cls.source_payload)
def test_authoritative_xml_hash_and_simulation_settings_are_fixed(self) -> None:
source = self.manifest["source"]
self.assertEqual(
hashlib.sha256(self.source_payload).hexdigest(), source["sha256"]
)
self.assertEqual(len(self.source_payload), source["bytes"])
self.assertEqual(
source_simulation_config(self.source_payload), source["simulation"]
)
self.assertEqual(self.source_path.name, "test-mql-8.xml")
companion = source["companionProject"]
companion_path = Path(self.manifest["_companionPath"])
companion_payload = companion_path.read_bytes()
companion_document = json.loads(companion_payload)
self.assertEqual(companion_path.name, "test-mql-8.json")
self.assertEqual(
hashlib.sha256(companion_payload).hexdigest(), companion["sha256"]
)
self.assertEqual(len(companion_payload), companion["bytes"])
self.assertFalse(companion["executionInput"])
self.assertEqual(
companion_document["simulation"],
{
"t_start": source["simulation"]["tStart"],
"t_stop": source["simulation"]["tStop"],
"step": source["simulation"]["sampleStep"],
"max_step": source["simulation"]["maxStep"],
"method": source["simulation"]["method"],
},
)
def test_authoritative_xml_validates_and_matches_structure_snapshot(self) -> None:
self.assertTrue(self.validation.valid, self.validation.as_dict())
assert self.validation.document is not None
network = compile_system_xml_network(self.validation.document)
system = GenericFluidSystem(network)
expected = self.manifest["structure"]
pressure_flow = network.pressure_flow_structure_dict()
causal_execution = system.pressure_flow_solver.causal_execution_diagnostics()
jacobian = system.jacobian_sparsity_diagnostics()
self.assertEqual(len(network.components), expected["componentCount"])
self.assertEqual(len(network.connections), expected["connectionCount"])
self.assertEqual(
len(network.dynamic_components()), expected["dynamicComponentCount"]
)
self.assertEqual(
sum(component.state_size for component in network.dynamic_components()),
expected["stateCount"],
)
self.assertEqual(
len(network.result_variable_metadata()), expected["resultVariableCount"]
)
self.assertEqual(
pressure_flow["unknownCount"], expected["pressureFlowUnknownCount"]
)
self.assertEqual(
pressure_flow["equationCount"], expected["pressureFlowEquationCount"]
)
self.assertEqual(pressure_flow["isSquare"], expected["pressureFlowIsSquare"])
for key in (
"logicalEffortCoordinateCount",
"eliminatedEffortAliasCount",
"canonicalCoordinateCount",
"compatibilityScatterCount",
):
self.assertEqual(causal_execution[key], expected[key])
self.assertEqual(
jacobian["nonzeroCount"], expected["jacobianNonzeroCount"]
)
self.assertEqual(
jacobian["colorGroupCount"], expected["jacobianColorGroupCount"]
)
self.assertEqual(
system.mechanical_state_reducer.has_state_events,
expected["hasMechanicalStateEvents"],
)
def test_signal_event_schedule_is_fixed_for_every_horizon(self) -> None:
assert self.validation.document is not None
system = GenericFluidSystem(
compile_system_xml_network(self.validation.document)
)
for case_id in self.manifest["sequence"]:
variant = self.manifest["variants"][case_id]
actual = system.signal_resolver.event_times(
0.0, float(variant["stopTime"])
)
self.assertEqual(
actual,
tuple(variant["expectedSignalEventTimes"]),
case_id,
)
def test_approved_production_golden_replays_its_source_report(self) -> None:
repository_root = Path(self.manifest["_repositoryRoot"])
reference = self.manifest["variants"]["0.2s"]["goldens"]["production"]
golden = load_regression_golden(
repository_root / reference["path"],
expected_sha256=reference["sha256"],
repository_root=repository_root,
)
source_report = golden["provenance"]["sourceReport"]
report = json.loads(
(repository_root / source_report["path"]).read_text(encoding="utf-8")
)
case = next(item for item in report["cases"] if item["caseId"] == "0.2s")
audit = evaluate_regression_golden(case["worker"]["summary"], golden)
self.assertTrue(audit["passed"], audit)
self.assertEqual(audit["comparedValueCount"], 402)
self.assertEqual(audit["maxAbsoluteError"], 0.0)
self.assertEqual(audit["maxToleranceRatio"], 0.0)
def test_extension_decision_is_bound_to_the_current_report_and_budget(self) -> None:
repository_root = Path(self.manifest["_repositoryRoot"])
runs = repository_root / "tests/baselines/simulation/test_mql_8/runs"
decision = json.loads(
(runs / "2026-08-17-production-v2-extension-decision.json").read_text(
encoding="utf-8"
)
)
report_path = runs / decision["sourceReport"]
report_payload = report_path.read_bytes()
report = json.loads(report_payload)
source_case = next(
item for item in report["cases"] if item["caseId"] == "0.2s"
)
first_decision = decision["decisions"][0]
self.assertEqual(
decision["sourceXmlSha256"], self.manifest["source"]["sha256"]
)
self.assertEqual(
hashlib.sha256(report_payload).hexdigest(),
decision["sourceReportSha256"],
)
self.assertTrue(source_case["acceptance"]["passed"])
self.assertEqual(
decision["observedCase"]["workerWallSeconds"],
source_case["worker"]["wallSeconds"],
)
expected_prediction = (
source_case["worker"]["wallSeconds"]
* self.manifest["variants"]["1s"]["stopTime"]
/ source_case["stopTime"]
* self.manifest["execution"]["predictionSafetyFactor"]
)
self.assertAlmostEqual(
first_decision["predictedWallSeconds"], expected_prediction
)
self.assertGreater(
first_decision["predictedWallSeconds"],
first_decision["softTimeoutSeconds"],
)
self.assertEqual(first_decision["outcome"], "deferred")
self.assertTrue(decision["simulationWasNotStartedForDeferredCases"])
def test_periodic_main_lane_exercises_the_approved_production_golden(self) -> None:
workflow = (
Path(self.manifest["_repositoryRoot"])
/ ".github/workflows/solver-regression.yml"
).read_text(encoding="utf-8")
self.assertIn("default: production", workflow)
self.assertIn("inputs.lane || 'production'", workflow)
@unittest.skipUnless(
os.getenv(LONG_RUN_ENVIRONMENT, "").strip().lower() in {"1", "true", "yes"},
f"Set {LONG_RUN_ENVIRONMENT}=1 to run bounded 0.01/0.2/1/5/10 s integration.",
)
class TestMql8ProgressiveLongRegressionTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.manifest = load_regression_manifest(DEFAULT_MANIFEST_PATH)
def test_all_horizons_complete_or_stop_at_the_first_bounded_failure(self) -> None:
lane = os.getenv("TEST_MQL_8_REGRESSION_LANE", "production")
report = run_regression_suite(DEFAULT_MANIFEST_PATH, lane=lane)
outcomes = [case["outcome"] for case in report["cases"]]
self.assertEqual(outcomes[0], "completed", report["cases"][0])
if "deferred" in outcomes:
first_deferred = outcomes.index("deferred")
self.assertTrue(
all(outcome == "deferred" for outcome in outcomes[first_deferred:])
)
predecessor = report["cases"][first_deferred - 1]
self.fail(
"Progressive run stopped within its configured safety budget; "
f"optimize before resuming. Predecessor: {predecessor}"
)
self.assertEqual(
outcomes,
["completed"] * len(self.manifest["sequence"]),
report["cases"],
)
if __name__ == "__main__":
unittest.main()
+339
View File
@@ -0,0 +1,339 @@
from __future__ import annotations
from time import perf_counter
import unittest
from unittest.mock import patch
from app.simulation.components.amesim.boundary.sources import AmesimPnpl01
from app.simulation.components.amesim.flow.pipes import AmesimPnl00r
from app.simulation.components.experimental.storage.cylinder import Cylinder
from app.simulation.core.errors import RecoverableTrialStateError
from app.simulation.core.medium import IdealGasMedium
from app.simulation.solvers.algebraic import (
AlgebraicSolveDiagnostics,
AlgebraicSolveError,
)
from app.simulation.solvers.algebraic_blocks import StreamBlockSolveResult
from app.simulation.solvers.solver import SolveIVPConfig
from app.simulation.solvers.stream import StreamSolveDiagnostics, StreamSolveError
from app.simulation.solvers.thermofluid import (
ThermofluidClosureError,
ThermofluidTransactionPlan,
)
from app.simulation.systems.generic import GenericFluidSystem
from app.simulation.systems.network import SimulationNetwork
def _pnl00r_system() -> tuple[GenericFluidSystem, AmesimPnl00r]:
medium = IdealGasMedium()
source = Cylinder(
"source",
medium,
V=0.02,
p0=500_000.0,
T0=310.0,
)
resistance = AmesimPnl00r("resistance", medium)
resistance._causal_test_seed = {"accepted": 7.0}
plug = AmesimPnpl01("plug")
network = SimulationNetwork("thermofluid-rollback")
for component in (source, resistance, plug):
network.add_component(component)
network.connect("source", "port_b", "resistance", "port_1")
network.connect("resistance", "port_2", "plug", "port_1")
return GenericFluidSystem(network), resistance
def _physical_values(system: GenericFluidSystem) -> tuple[float, ...]:
return tuple(
float(getattr(binding.state, binding.variable))
for binding in system._thermofluid_transaction_plan.port_value_bindings
)
class ThermofluidRecoveryTests(unittest.TestCase):
def test_nonconvergence_is_recoverable_diagnostic_and_fully_restored(self) -> None:
system, resistance = _pnl00r_system()
state = system.consistent_initial_state_vector()
system.rhs(0.01, state)
secondary = system._thermofluid_closure_plan.secondary_block_solvers[0]
target = resistance.port_1
expected_ports = _physical_values(system)
expected_connected_h = dict(resistance._connected_h)
expected_causal_seed = dict(resistance._causal_test_seed)
expected_owner_diagnostics = tuple(
owner.last_diagnostics
for owner in system._thermofluid_transaction_plan.diagnostic_owners
)
expected_last_algebraic = system._last_algebraic_diagnostics
expected_last_scope = system._last_algebraic_scope
expected_last_success = (
system._thermofluid_closure_diagnostics.as_dict()["lastSuccess"]
)
fake_diagnostics = AlgebraicSolveDiagnostics(
success=True,
message="forced divergent thermofluid pass",
evaluations=0,
pressure_scale=1.0,
flow_scale=1.0,
max_scaled_residual=0.0,
max_raw_residual=0.0,
)
call_count = 0
def divergent_solve(*, scale_context=None):
del scale_context
nonlocal call_count
call_count += 1
target.m_flow += 1.0
resistance._connected_h = {
"port_1": -float(call_count),
"port_2": -2.0 * float(call_count),
}
resistance._causal_test_seed["polluted"] = float(call_count)
return StreamBlockSolveResult(
diagnostics=(fake_diagnostics,),
scopes=(
system._thermofluid_closure_plan.secondary_component_groups[0],
),
used_global_fallback=False,
)
with patch.object(secondary, "solve", side_effect=divergent_solve):
with self.assertRaises(ThermofluidClosureError) as raised:
system.rhs(0.125, state)
error = raised.exception
self.assertIsInstance(error, RecoverableTrialStateError)
failure = error.diagnostics
self.assertEqual(call_count, 25)
self.assertEqual(failure.failed_rhs_time, 0.125)
self.assertEqual(failure.iterations, 25)
self.assertEqual(failure.failure_count, 1)
self.assertEqual(len(failure.delta_tail), 8)
self.assertEqual(
[item.iteration for item in failure.delta_tail],
list(range(18, 26)),
)
self.assertEqual(failure.max_delta, 1.0)
self.assertEqual(failure.scale, 25.0)
self.assertEqual(failure.tolerance, 25.0e-12)
self.assertIsNotNone(failure.worst_port)
assert failure.worst_port is not None
self.assertEqual(failure.worst_port.component, "resistance")
self.assertEqual(failure.worst_port.port, "port_1")
self.assertEqual(failure.worst_port.signed_delta, 1.0)
self.assertEqual(_physical_values(system), expected_ports)
self.assertEqual(resistance._connected_h, expected_connected_h)
self.assertEqual(resistance._causal_test_seed, expected_causal_seed)
self.assertEqual(
tuple(
owner.last_diagnostics
for owner in system._thermofluid_transaction_plan.diagnostic_owners
),
expected_owner_diagnostics,
)
self.assertIs(system._last_algebraic_diagnostics, expected_last_algebraic)
self.assertEqual(system._last_algebraic_scope, expected_last_scope)
self.assertTrue(system.pressure_flow_solver._causal_audit_required)
self.assertTrue(secondary._causal_audit_required)
outcomes = system._thermofluid_closure_diagnostics.as_dict()
self.assertEqual(outcomes["failureCount"], 1)
self.assertEqual(outcomes["lastSuccess"], expected_last_success)
self.assertEqual(outcomes["lastFailure"], failure.as_dict())
# A successful maintenance/postprocessing closure must not masquerade
# as the last successful integrator RHS.
system._close_current_state(0.5)
self.assertEqual(
system._thermofluid_closure_diagnostics.as_dict()["lastSuccess"],
expected_last_success,
)
# The restored seed is safe to replay at the failed time.
retried = system.rhs(0.125, state)
self.assertEqual(len(retried), len(state))
outcomes = system._thermofluid_closure_diagnostics.as_dict()
self.assertEqual(outcomes["failureCount"], 1)
self.assertEqual(outcomes["lastFailure"], failure.as_dict())
self.assertEqual(outcomes["lastSuccess"]["rhsTime"], 0.125)
def test_simulation_result_exposes_closure_and_transaction_diagnostics(self) -> None:
system, _resistance = _pnl00r_system()
result = system.simulate(
SolveIVPConfig(
t_start=0.0,
t_stop=1.0e-4,
method="RK45",
max_step=1.0e-4,
),
sample_step=1.0e-4,
)
self.assertTrue(result.success)
closure = result.diagnostics["stream"]["thermofluidClosure"]
self.assertEqual(closure["failureCount"], 0)
self.assertIsNone(closure["lastFailure"])
self.assertIsNotNone(closure["lastSuccess"])
self.assertEqual(
closure["transaction"]["physicalPortValueSlotCount"],
20,
)
self.assertEqual(
closure["transaction"]["physicalFlowPortCount"],
4,
)
def test_eventless_generic_simulation_retries_a_recoverable_trial(self) -> None:
system, _resistance = _pnl00r_system()
self.assertFalse(system.mechanical_state_reducer.has_state_events)
self.assertEqual(system.signal_resolver.event_times(0.0, 1.0e-4), ())
original_rhs = system.rhs
failed_once = False
def fail_first_positive_trial(time, state):
nonlocal failed_once
if time > 0.0 and not failed_once:
failed_once = True
raise RecoverableTrialStateError("forced recoverable trial")
return original_rhs(time, state)
with patch.object(system, "rhs", side_effect=fail_first_positive_trial):
result = system.simulate(
SolveIVPConfig(
t_start=0.0,
t_stop=1.0e-4,
method="RK45",
max_step=1.0e-4,
),
sample_step=1.0e-4,
)
self.assertTrue(failed_once)
self.assertTrue(result.success)
totals = result.diagnostics["integration"]["totals"]
self.assertEqual(totals["recoverableRetryCount"], 1)
self.assertEqual(totals["stateTransitionCount"], 0)
segment = result.diagnostics["integration"]["segments"][0]
self.assertEqual(len(segment["recoverableRetries"]), 1)
self.assertEqual(
segment["recoverableRetries"][0]["reason"],
"forced recoverable trial",
)
def test_other_closure_failures_restore_but_remain_nonrecoverable(self) -> None:
fake_algebraic_diagnostics = AlgebraicSolveDiagnostics(
success=False,
message="forced algebraic failure",
evaluations=1,
pressure_scale=1.0,
flow_scale=1.0,
max_scaled_residual=2.0,
max_raw_residual=2.0,
)
for failure_kind in ("stream", "secondaryAlgebraic"):
with self.subTest(failure_kind=failure_kind):
system, resistance = _pnl00r_system()
state = system.consistent_initial_state_vector()
system.rhs(0.01, state)
expected_ports = _physical_values(system)
expected_cache = dict(resistance._connected_h)
expected_success = (
system._thermofluid_closure_diagnostics.as_dict()[
"lastSuccess"
]
)
def pollute() -> None:
resistance.port_1.p = -9.0
resistance.port_1.m_flow = 99.0
resistance.port_1.h_outflow = -999.0
resistance._connected_h = {
"port_1": -1.0,
"port_2": -2.0,
}
if failure_kind == "stream":
def failed_stream(*, dynamic_ports_are_current=False):
del dynamic_ports_are_current
pollute()
diagnostics = StreamSolveDiagnostics(
converged=False,
iterations=100,
max_delta=1.0,
)
raise StreamSolveError("forced stream failure", diagnostics)
context = patch.object(
system.stream_resolver,
"solve",
side_effect=failed_stream,
)
expected_error = StreamSolveError
else:
secondary = (
system._thermofluid_closure_plan.secondary_block_solvers[0]
)
def failed_secondary(*, scale_context=None):
del scale_context
pollute()
raise AlgebraicSolveError(
"forced secondary failure",
fake_algebraic_diagnostics,
scope_kind="physicalIsland",
)
context = patch.object(
secondary,
"solve",
side_effect=failed_secondary,
)
expected_error = AlgebraicSolveError
with context:
with self.assertRaises(expected_error) as raised:
system.rhs(0.25, state)
self.assertNotIsInstance(
raised.exception,
RecoverableTrialStateError,
)
self.assertEqual(_physical_values(system), expected_ports)
self.assertEqual(resistance._connected_h, expected_cache)
outcomes = system._thermofluid_closure_diagnostics.as_dict()
self.assertEqual(outcomes["failureCount"], 0)
self.assertIsNone(outcomes["lastFailure"])
self.assertEqual(outcomes["lastSuccess"], expected_success)
def test_transaction_microbenchmark_stays_scoped_to_numeric_slots(self) -> None:
medium = IdealGasMedium()
network = SimulationNetwork("transaction-microbenchmark")
for index in range(120):
network.add_component(AmesimPnl00r(f"resistance_{index}", medium))
plan = ThermofluidTransactionPlan.compile(network)
diagnostics = plan.diagnostics()
self.assertEqual(diagnostics["physicalPortValueSlotCount"], 1_200)
self.assertEqual(diagnostics["streamAndCausalCacheSlotCount"], 120)
for _ in range(5):
plan.capture().restore()
repetitions = 200
started = perf_counter()
for _ in range(repetitions):
plan.capture().restore()
seconds_per_transaction = (perf_counter() - started) / repetitions
# This deliberately generous guard detects accidental full component/
# network deepcopy while remaining stable on slow CI workers.
self.assertLess(seconds_per_transaction, 0.01)
if __name__ == "__main__":
unittest.main()
+5 -2
View File
@@ -254,8 +254,11 @@ class ThreePistonTangentCompilerTests(unittest.TestCase):
self.assertEqual(runtime["mode"], "semiAnalyticExactColumns")
self.assertEqual(runtime["effectiveMode"], "notEvaluated")
self.assertEqual(runtime["originalColorGroupCount"], 31)
self.assertEqual(runtime["remainingColorGroupCount"], 25)
self.assertEqual(runtime["exactColumnCount"], 6)
# Generic wiring uses topology discovery rather than the legacy
# three-name wrapper. This fixture contains one additional supported
# branch, so all four branches are compiled automatically.
self.assertEqual(runtime["remainingColorGroupCount"], 24)
self.assertEqual(runtime["exactColumnCount"], 8)
def test_generic_simulation_ineligible_path_uses_native_scipy(self) -> None:
report = validate_system_xml_document(TARGET_XML.read_bytes())