整合求解器活动监控与步长回归证据
同步远端 PNL0003 诊断和大采样网格能力,语义合并活动感知的 60 秒真停滞判定与旧后端 15 分钟兼容兜底。 纳管热路径优化、15 单元运行证据、浏览器与 API 报告,并补充北京时间更新日志和遗留问题。
This commit is contained in:
1 parent
c19cf77aee
commit
e18399c022
46 files changed
+181589
-170
No files matched your search
@@ -257,7 +257,10 @@
|
||||
"environment": {
|
||||
"SIMULATION_CAUSAL_EXECUTOR_V2": "1",
|
||||
"SIMULATION_CAUSAL_COORDINATE_KERNEL": "1",
|
||||
"SIMULATION_CAUSAL_DIRECT_SUM_ASSIGNMENTS": "1",
|
||||
"SIMULATION_CAUSAL_DIRECT_EQUATION_READERS": "1",
|
||||
"SIMULATION_CAUSAL_FAST_PATH": "1",
|
||||
"SIMULATION_MECHANICAL_ATOL_MODE": "legacy",
|
||||
"SIMULATION_ODE_JACOBIAN_MODE": "scipy",
|
||||
"SIMULATIONAPP_PROPERTY_CACHE": "on"
|
||||
},
|
||||
|
||||
+30317
File diff suppressed because it is too large.
Load diff
+38274
File diff suppressed because it is too large.
Load diff
+65793
File diff suppressed because it is too large.
Load diff
+86
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"generatedAt": "2026-08-18T19:06:44Z",
|
||||
"purpose": "Compact evidence for the real streaming API path and activity-aware browser watchdog.",
|
||||
"source": {
|
||||
"xmlPath": "tests/data/test-mql-8.xml",
|
||||
"xmlSha256": "0a2d9331df9eb5974daec25a61c1238ba32b1742d933ffc8b16ce316c5627b0b",
|
||||
"jsonPath": "tests/data/test-mql-8.json",
|
||||
"jsonSha256": "b44bf540ccd1c293fe2af2b9b9052b540abf83961ad955a0f6a4ab40fbe0bb18"
|
||||
},
|
||||
"request": {
|
||||
"endpoint": "/api/system-xml/simulate-stream",
|
||||
"simulationId": "diag-opt00-api-optimized-20260818",
|
||||
"method": "BDF",
|
||||
"tStart": 0.0,
|
||||
"tStop": 0.2,
|
||||
"sampleStep": 0.01,
|
||||
"maxStep": 0.001
|
||||
},
|
||||
"response": {
|
||||
"httpStatus": 200,
|
||||
"contentType": "application/x-ndjson",
|
||||
"phase": "completed",
|
||||
"success": true,
|
||||
"simulatedUntil": 0.2,
|
||||
"sampleCount": 21,
|
||||
"strictlyIncreasingTimes": true,
|
||||
"allNumericSeriesFinite": true,
|
||||
"progressEventCount": 255,
|
||||
"resultEventCount": 1,
|
||||
"integrationSeconds": 146.675315209,
|
||||
"totalSeconds": 147.298753857,
|
||||
"traceFirstProgressToResultSeconds": 147.481,
|
||||
"integrationTotals": {
|
||||
"nfev": 6318,
|
||||
"njev": 331,
|
||||
"nlu": 1060,
|
||||
"acceptedStepCount": 2113,
|
||||
"solverStartCount": 2,
|
||||
"stateTransitionCount": 0,
|
||||
"recoverableRetryCount": 0
|
||||
}
|
||||
},
|
||||
"activityWindow": {
|
||||
"maximumNonHeartbeatProgressGapSeconds": 57.185072,
|
||||
"start": {
|
||||
"wallTimeUtc": "18:46:24.877853",
|
||||
"simulatedTime": 0.048633224495,
|
||||
"activitySequence": 32081,
|
||||
"acceptedStepSequence": 358,
|
||||
"rhsCallCount": 7840
|
||||
},
|
||||
"end": {
|
||||
"wallTimeUtc": "18:47:22.062925",
|
||||
"simulatedTime": 0.049248430694,
|
||||
"activitySequence": 68103,
|
||||
"acceptedStepSequence": 1743,
|
||||
"rhsCallCount": 16153
|
||||
},
|
||||
"interveningHeartbeatCount": 11,
|
||||
"maximumInterEventGapSeconds": 5.074026,
|
||||
"heartbeatActivityAdvanced": true,
|
||||
"acceptedTimeAdvancedInsideThrottledProgressWindow": true,
|
||||
"oldFrontendWouldUseAcceptedProgressOnly": true,
|
||||
"activityAwareFrontendWouldCancel": false
|
||||
},
|
||||
"workerComparison": {
|
||||
"reportPath": "tests/baselines/simulation/test_mql_8/runs/2026-08-18-production-slow-region-exact-v1-0.2.json",
|
||||
"reportSha256": "34268e5864f33709018d7cbe631654d2a835b10dd252886509244f913ae18559",
|
||||
"reportBytes": 437852,
|
||||
"finalValuesEqual": true,
|
||||
"integrationCountersEqual": true,
|
||||
"numericalDiagnosticsEqual": true
|
||||
},
|
||||
"rawCapture": {
|
||||
"retainedOutsideRepository": true,
|
||||
"ndjsonSha256": "52bcca58147e1c78e5137d7134f79d21b314092156e0de0e9c3ebb58b40fd616",
|
||||
"traceSha256": "8a26a349b40556811f9a0f151ebe801d6a126c782c030fe55cfb23c9630a8a90",
|
||||
"headersSha256": "b5baf9b3d5ef1f2c18a9fb4894e78be6422b9e7c8f8821f2e586e92d21348a71"
|
||||
},
|
||||
"limitations": [
|
||||
"The literal interactive browser click path was not captured in this artifact; the same watchdog state machine is covered by Playwright.",
|
||||
"SciPy-internal finite-difference Jacobian builds are reflected by njev but are not classified by the realtime jacobianEvaluationCount field.",
|
||||
"Cooperative cancellation cannot hard-kill a native or Python call that never returns."
|
||||
]
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"kind": "live-browser-stream-verification",
|
||||
"generatedAt": "2026-08-19T15:12:44.536Z",
|
||||
"startedAt": "2026-08-19T15:10:08.400Z",
|
||||
"wallSeconds": 156.136,
|
||||
"workflowCompleted": true,
|
||||
"topology": {
|
||||
"browserOrigin": "http://127.0.0.1:14173",
|
||||
"backendOrigin": "http://127.0.0.1:18181",
|
||||
"servingMode": "isolated-vite-dev-proxy"
|
||||
},
|
||||
"source": {
|
||||
"path": "tests/data/test-mql-8.json",
|
||||
"bytes": 272592,
|
||||
"sha256": "b44bf540ccd1c293fe2af2b9b9052b540abf83961ad955a0f6a4ab40fbe0bb18"
|
||||
},
|
||||
"target": {
|
||||
"tStart": 0,
|
||||
"tStop": 0.2,
|
||||
"sampleStep": 0.01,
|
||||
"maxStep": 0.001,
|
||||
"method": "BDF"
|
||||
},
|
||||
"runtimeEnvironment": {
|
||||
"SIMULATION_CAUSAL_EXECUTOR_V2": "1",
|
||||
"SIMULATION_CAUSAL_COORDINATE_KERNEL": "1",
|
||||
"SIMULATION_CAUSAL_DIRECT_SUM_ASSIGNMENTS": "1",
|
||||
"SIMULATION_CAUSAL_DIRECT_EQUATION_READERS": "1",
|
||||
"SIMULATION_CAUSAL_FAST_PATH": "1",
|
||||
"SIMULATION_MECHANICAL_ATOL_MODE": "legacy",
|
||||
"SIMULATION_ODE_JACOBIAN_MODE": "scipy",
|
||||
"SIMULATIONAPP_PROPERTY_CACHE": "on"
|
||||
},
|
||||
"request": {
|
||||
"simulationId": "e694d041-129d-4b49-af9e-747d85f0056d",
|
||||
"xmlBytes": 104110,
|
||||
"simulationAttributes": {
|
||||
"tStart": "0",
|
||||
"tStop": "0.2",
|
||||
"sampleStep": "0.01",
|
||||
"maxStep": "0.001",
|
||||
"method": "BDF"
|
||||
}
|
||||
},
|
||||
"ndjson": {
|
||||
"lineCount": 256,
|
||||
"parseErrors": [],
|
||||
"streamErrors": [],
|
||||
"result": {
|
||||
"success": true,
|
||||
"status": "completed",
|
||||
"partial": false,
|
||||
"simulatedUntil": 0.2,
|
||||
"requestedStopTime": 0.2,
|
||||
"sampleCount": 21,
|
||||
"timePointCount": 21,
|
||||
"finalTime": 0.2
|
||||
},
|
||||
"activity": {
|
||||
"progressEventCount": 255,
|
||||
"heartbeatCount": 13,
|
||||
"activityTelemetryHeartbeatCount": 13,
|
||||
"firstActivitySequence": 25114,
|
||||
"lastActivitySequence": 66670,
|
||||
"activitySequenceAdvanced": true,
|
||||
"acceptedTransitionCount": 249,
|
||||
"maximumAcceptedPlateau": {
|
||||
"durationMs": 5050.799999237061,
|
||||
"fromAcceptedStepSequence": 1268,
|
||||
"toAcceptedStepSequence": 1404,
|
||||
"fromAcceptedTime": 0.04886080559693986,
|
||||
"toAcceptedTime": 0.048885635713233705,
|
||||
"heartbeatCount": 2,
|
||||
"activitySequenceStart": 57190,
|
||||
"activitySequenceEnd": 60152,
|
||||
"activitySequenceAdvanced": true
|
||||
},
|
||||
"slowRegionEvents": [
|
||||
{
|
||||
"receivedAtMs": 43039.39999961853,
|
||||
"phase": "integrating",
|
||||
"heartbeat": false,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.04807049151065195,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 32061,
|
||||
"activityKind": "accepted_step",
|
||||
"currentTrialTime": 0.04807049151065195,
|
||||
"rhsCallCount": 7836,
|
||||
"acceptedStepSequence": 356,
|
||||
"acceptedTime": 0.04807049151065195
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 43131.29999923706,
|
||||
"phase": "integrating",
|
||||
"heartbeat": false,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 32081,
|
||||
"activityKind": "accepted_step",
|
||||
"currentTrialTime": 0.0486332244950324,
|
||||
"rhsCallCount": 7840,
|
||||
"acceptedStepSequence": 358,
|
||||
"acceptedTime": 0.0486332244950324
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 48131,
|
||||
"phase": "integrating",
|
||||
"heartbeat": true,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 35600,
|
||||
"activityKind": "thermofluid_closure",
|
||||
"currentTrialTime": 0.0486991505296288,
|
||||
"rhsCallCount": 8689,
|
||||
"acceptedStepSequence": 420,
|
||||
"acceptedTime": 0.048699149085530725
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 53157.89999961853,
|
||||
"phase": "integrating",
|
||||
"heartbeat": true,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 38734,
|
||||
"activityKind": "thermofluid_closure",
|
||||
"currentTrialTime": 0.04870448422554062,
|
||||
"rhsCallCount": 9413,
|
||||
"acceptedStepSequence": 539,
|
||||
"acceptedTime": 0.04870448402151976
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 58201.89999961853,
|
||||
"phase": "integrating",
|
||||
"heartbeat": true,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 41712,
|
||||
"activityKind": "thermofluid_closure",
|
||||
"currentTrialTime": 0.04872888403105117,
|
||||
"rhsCallCount": 10076,
|
||||
"acceptedStepSequence": 702,
|
||||
"acceptedTime": 0.048728801929735416
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 63233.59999847412,
|
||||
"phase": "integrating",
|
||||
"heartbeat": true,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 44741,
|
||||
"activityKind": "state_derivatives",
|
||||
"currentTrialTime": 0.04872901764870005,
|
||||
"rhsCallCount": 10765,
|
||||
"acceptedStepSequence": 838,
|
||||
"acceptedTime": 0.04872901502576142
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 68278.29999923706,
|
||||
"phase": "integrating",
|
||||
"heartbeat": true,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 47988,
|
||||
"activityKind": "thermofluid_closure",
|
||||
"currentTrialTime": 0.04880991845348599,
|
||||
"rhsCallCount": 11549,
|
||||
"acceptedStepSequence": 894,
|
||||
"acceptedTime": 0.04880991659730141
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 73322.5,
|
||||
"phase": "integrating",
|
||||
"heartbeat": true,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 50944,
|
||||
"activityKind": "thermofluid_closure",
|
||||
"currentTrialTime": 0.04885091695689501,
|
||||
"rhsCallCount": 12205,
|
||||
"acceptedStepSequence": 1060,
|
||||
"acceptedTime": 0.048849397131310805
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 78344,
|
||||
"phase": "integrating",
|
||||
"heartbeat": true,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 53758,
|
||||
"activityKind": "thermofluid_closure",
|
||||
"currentTrialTime": 0.04885415602948481,
|
||||
"rhsCallCount": 12830,
|
||||
"acceptedStepSequence": 1217,
|
||||
"acceptedTime": 0.048852938260664384
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 83372,
|
||||
"phase": "integrating",
|
||||
"heartbeat": true,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 57190,
|
||||
"activityKind": "solver_step",
|
||||
"currentTrialTime": 0.048860806745699516,
|
||||
"rhsCallCount": 13662,
|
||||
"acceptedStepSequence": 1268,
|
||||
"acceptedTime": 0.04886080559693986
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 88422.79999923706,
|
||||
"phase": "integrating",
|
||||
"heartbeat": true,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 60152,
|
||||
"activityKind": "thermofluid_closure",
|
||||
"currentTrialTime": 0.04888569214870155,
|
||||
"rhsCallCount": 14335,
|
||||
"acceptedStepSequence": 1404,
|
||||
"acceptedTime": 0.048885635713233705
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 93441.29999923706,
|
||||
"phase": "integrating",
|
||||
"heartbeat": true,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 63490,
|
||||
"activityKind": "thermofluid_closure",
|
||||
"currentTrialTime": 0.04888577358403684,
|
||||
"rhsCallCount": 15121,
|
||||
"acceptedStepSequence": 1501,
|
||||
"acceptedTime": 0.048885772226330206
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 98461.89999961853,
|
||||
"phase": "integrating",
|
||||
"heartbeat": true,
|
||||
"progress": 24,
|
||||
"simulatedTime": 0.0486332244950324,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 66670,
|
||||
"activityKind": "solver_step",
|
||||
"currentTrialTime": 0.04888814764518708,
|
||||
"rhsCallCount": 15855,
|
||||
"acceptedStepSequence": 1622,
|
||||
"acceptedTime": 0.048888146224637836
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 101649.59999847412,
|
||||
"phase": "integrating",
|
||||
"heartbeat": false,
|
||||
"progress": 25,
|
||||
"simulatedTime": 0.049248430694378086,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 68103,
|
||||
"activityKind": "accepted_step",
|
||||
"currentTrialTime": 0.049248430694378086,
|
||||
"rhsCallCount": 16153,
|
||||
"acceptedStepSequence": 1743,
|
||||
"acceptedTime": 0.049248430694378086
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 101755.69999885559,
|
||||
"phase": "integrating",
|
||||
"heartbeat": false,
|
||||
"progress": 25,
|
||||
"simulatedTime": 0.049780741149075,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 68133,
|
||||
"activityKind": "accepted_step",
|
||||
"currentTrialTime": 0.049780741149075,
|
||||
"rhsCallCount": 16159,
|
||||
"acceptedStepSequence": 1746,
|
||||
"acceptedTime": 0.049780741149075
|
||||
},
|
||||
{
|
||||
"receivedAtMs": 102123.79999923706,
|
||||
"phase": "integrating",
|
||||
"heartbeat": false,
|
||||
"progress": 25,
|
||||
"simulatedTime": 0.05031324569277993,
|
||||
"totalTime": 0.2,
|
||||
"activitySequence": 68377,
|
||||
"activityKind": "accepted_step",
|
||||
"currentTrialTime": 0.05031324569277993,
|
||||
"rhsCallCount": 16219,
|
||||
"acceptedStepSequence": 1748,
|
||||
"acceptedTime": 0.05031324569277993
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"browser": {
|
||||
"cancellationRequests": [],
|
||||
"pageErrors": [],
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
]
|
||||
},
|
||||
"acceptance": {
|
||||
"completedWith21Samples": true,
|
||||
"noCancellationRequest": true,
|
||||
"noStreamError": true,
|
||||
"noPageError": true,
|
||||
"activityTelemetryAdvanced": true
|
||||
}
|
||||
}
|
||||
+8563
File diff suppressed because it is too large.
Load diff
+8722
File diff suppressed because it is too large.
Load diff
+8517
File diff suppressed because it is too large.
Load diff
+8767
File diff suppressed because it is too large.
Load diff
+8595
File diff suppressed because it is too large.
Load diff
@@ -129,7 +129,10 @@
|
||||
"environment": {
|
||||
"SIMULATION_CAUSAL_EXECUTOR_V2": "1",
|
||||
"SIMULATION_CAUSAL_COORDINATE_KERNEL": "1",
|
||||
"SIMULATION_CAUSAL_DIRECT_SUM_ASSIGNMENTS": "1",
|
||||
"SIMULATION_CAUSAL_DIRECT_EQUATION_READERS": "1",
|
||||
"SIMULATION_CAUSAL_FAST_PATH": "1",
|
||||
"SIMULATION_MECHANICAL_ATOL_MODE": "legacy",
|
||||
"SIMULATION_ODE_JACOBIAN_MODE": "scipy",
|
||||
"SIMULATIONAPP_PROPERTY_CACHE": "on"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ from app.main import (
|
||||
compile_system_xml_network,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from app.simulation.solvers.solver import SolveIVPConfig
|
||||
from app.simulation.systems.generic import GenericFluidSystem
|
||||
from app.system_xml import validate_system_xml_document
|
||||
from tests.test_amesim_pnvo001_signal_xml import signal_edge, signal_port
|
||||
@@ -302,6 +303,140 @@ class AmesimMechanicalXmlTests(unittest.TestCase):
|
||||
self.assertEqual(tolerances, [1.0e-12, 1.0e-12])
|
||||
self.assertEqual(len(tolerances), len(system.initial_state_vector()))
|
||||
|
||||
def test_self_loop_contact_does_not_relax_mass_velocity(self) -> None:
|
||||
contact_parameters = {**LSTP00A_DEFAULTS, "rcont": 1.0e11}
|
||||
project = ReactFlowProjectPayload(
|
||||
projectSchemaVersion=1,
|
||||
name="amesim-mechanical-self-contact",
|
||||
nodes=[
|
||||
component_node(
|
||||
"mass_1",
|
||||
"amesim_mecmas21",
|
||||
[
|
||||
mechanical_port("port_1", "left"),
|
||||
mechanical_port("port_2", "right"),
|
||||
],
|
||||
MECMAS21_DEFAULTS,
|
||||
),
|
||||
component_node(
|
||||
"contact_1",
|
||||
"amesim_lstp00a",
|
||||
[
|
||||
mechanical_port("port_1", "left"),
|
||||
mechanical_port("port_2", "right"),
|
||||
],
|
||||
contact_parameters,
|
||||
),
|
||||
],
|
||||
edges=[
|
||||
physical_edge(
|
||||
"edge-1",
|
||||
"mass_1",
|
||||
"port_1",
|
||||
"contact_1",
|
||||
"port_1",
|
||||
),
|
||||
physical_edge(
|
||||
"edge-2",
|
||||
"mass_1",
|
||||
"port_2",
|
||||
"contact_1",
|
||||
"port_2",
|
||||
),
|
||||
],
|
||||
simulation={
|
||||
"t_start": 0.0,
|
||||
"t_stop": 0.02,
|
||||
"step": 0.01,
|
||||
"max_step": 0.005,
|
||||
"method": "BDF",
|
||||
},
|
||||
)
|
||||
system = GenericFluidSystem(compile_reactflow_network(project))
|
||||
|
||||
plan = system.mechanical_state_reducer.absolute_tolerance_plan(
|
||||
1.0e-8,
|
||||
mode="contact-aware-v1",
|
||||
)
|
||||
|
||||
self.assertEqual(plan.values, (1.0e-12, 1.0e-12))
|
||||
self.assertEqual(plan.groups[0].contacts, ())
|
||||
self.assertFalse(plan.groups[0].eligible)
|
||||
self.assertEqual(plan.groups[0].reason, "noFlexibleContact")
|
||||
|
||||
def test_weak_contact_damping_does_not_relax_velocity(self) -> None:
|
||||
project = elastic_contact_project()
|
||||
project.nodes[2].data.parameters["rcont"] = 1.0e-300
|
||||
system = GenericFluidSystem(compile_reactflow_network(project))
|
||||
|
||||
plan = system.mechanical_state_reducer.absolute_tolerance_plan(
|
||||
1.0e-8,
|
||||
mode="contact-aware-v1",
|
||||
)
|
||||
|
||||
self.assertTrue(plan.groups)
|
||||
self.assertTrue(all(not group.eligible for group in plan.groups))
|
||||
self.assertTrue(
|
||||
all(group.reason == "weakContactDamping" for group in plan.groups)
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
group.velocity_atol == 1.0e-12
|
||||
and group.position_atol == 1.0e-12
|
||||
for group in plan.groups
|
||||
)
|
||||
)
|
||||
|
||||
def test_legacy_tolerance_ignores_extreme_contact_scales(self) -> None:
|
||||
project = elastic_contact_project()
|
||||
project.nodes[2].data.parameters.update(
|
||||
{"kcont": 5.0e-324, "Pdis": 5.0e-324, "rcont": 1.0}
|
||||
)
|
||||
system = GenericFluidSystem(compile_reactflow_network(project))
|
||||
|
||||
legacy = system.mechanical_state_reducer.absolute_tolerance_plan(
|
||||
1.0e-8,
|
||||
mode="legacy",
|
||||
)
|
||||
contact_aware = (
|
||||
system.mechanical_state_reducer.absolute_tolerance_plan(
|
||||
1.0e-8,
|
||||
mode="contact-aware-v1",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertTrue(all(value == 1.0e-12 for value in legacy.values))
|
||||
self.assertTrue(
|
||||
all(group.reason == "legacyMode" for group in legacy.groups)
|
||||
)
|
||||
self.assertTrue(
|
||||
all(
|
||||
group.reason == "invalidContactScale"
|
||||
for group in contact_aware.groups
|
||||
)
|
||||
)
|
||||
|
||||
def test_contact_aware_tolerance_is_limited_to_bdf(self) -> None:
|
||||
system = GenericFluidSystem(
|
||||
compile_reactflow_network(zero_force_mass_project())
|
||||
)
|
||||
|
||||
result = system.simulate(
|
||||
SolveIVPConfig(
|
||||
t_start=0.0,
|
||||
t_stop=0.001,
|
||||
method="RK45",
|
||||
max_step=0.001,
|
||||
),
|
||||
sample_step=0.001,
|
||||
)
|
||||
|
||||
self.assertTrue(result.success)
|
||||
tolerance = result.diagnostics["integration"][
|
||||
"mechanicalAbsoluteTolerance"
|
||||
]
|
||||
self.assertEqual(tolerance["mode"], "legacy")
|
||||
|
||||
|
||||
def test_sparse_reactflow_mecmas_parameters_use_registered_defaults(self) -> None:
|
||||
project = mecmas_project(1.0, 1.0)
|
||||
|
||||
@@ -1,13 +1,42 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from math import sqrt
|
||||
from math import log10, sqrt
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.simulation.components.amesim.flow.pipes import AmesimPnl0001
|
||||
from app.simulation.components.amesim.flow.pipes import (
|
||||
AmesimPnl0001,
|
||||
AmesimPnl0002,
|
||||
AmesimPnl00r,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
||||
|
||||
|
||||
class _ScaledReynoldsPnl0001(AmesimPnl0001):
|
||||
def __init__(self, *args, reynolds_scale: float, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.reynolds_scale = float(reynolds_scale)
|
||||
self.reynolds_calls: list[tuple[float, float]] = []
|
||||
|
||||
def reynolds_number(self, mass_flow: float, temperature: float) -> float:
|
||||
self.reynolds_calls.append((mass_flow, temperature))
|
||||
return self.reynolds_scale * super().reynolds_number(
|
||||
mass_flow,
|
||||
temperature,
|
||||
)
|
||||
|
||||
|
||||
class _DelegatingFrictionPnl0001(AmesimPnl0001):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.friction_reynolds: list[float] = []
|
||||
|
||||
def friction_factor(self, reynolds_number: float) -> float:
|
||||
self.friction_reynolds.append(reynolds_number)
|
||||
return super().friction_factor(reynolds_number)
|
||||
|
||||
|
||||
class AmesimPnl0001ComponentTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.medium = IdealGasMedium()
|
||||
@@ -103,6 +132,176 @@ class AmesimPnl0001ComponentTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(pipe.mass_flow(15.3e6, 15.3e6 + 2.0e-9, 293.15), 0.0)
|
||||
|
||||
def test_overridden_reynolds_number_is_called_on_every_iteration(self) -> None:
|
||||
slow_reynolds = _ScaledReynoldsPnl0001(
|
||||
"slow_reynolds",
|
||||
self.medium,
|
||||
reynolds_scale=0.25,
|
||||
)
|
||||
fast_reynolds = _ScaledReynoldsPnl0001(
|
||||
"fast_reynolds",
|
||||
self.medium,
|
||||
reynolds_scale=4.0,
|
||||
)
|
||||
options = {
|
||||
"upstream_pressure": 2.0e6,
|
||||
"downstream_pressure": 1.0e6,
|
||||
"upstream_temperature": 300.0,
|
||||
"resistance_length": 1.0,
|
||||
"max_iterations": 4,
|
||||
}
|
||||
|
||||
slow_flow = slow_reynolds._one_way_pn2pipefr_mass_flow(**options)
|
||||
fast_flow = fast_reynolds._one_way_pn2pipefr_mass_flow(**options)
|
||||
|
||||
self.assertEqual(len(slow_reynolds.reynolds_calls), 4)
|
||||
self.assertEqual(len(fast_reynolds.reynolds_calls), 4)
|
||||
self.assertTrue(
|
||||
all(
|
||||
temperature == 300.0
|
||||
for _mass_flow, temperature in slow_reynolds.reynolds_calls
|
||||
)
|
||||
)
|
||||
self.assertLess(slow_flow, fast_flow)
|
||||
|
||||
def test_zero_iterations_does_not_evaluate_dynamic_viscosity(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium)
|
||||
original = IdealGasMedium.dynamic_viscosity
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
IdealGasMedium,
|
||||
"dynamic_viscosity",
|
||||
autospec=True,
|
||||
side_effect=original,
|
||||
) as dynamic_viscosity,
|
||||
patch(
|
||||
"app.simulation.components.amesim.flow.pipes.log10",
|
||||
side_effect=log10,
|
||||
) as logarithm,
|
||||
):
|
||||
flow = pipe._one_way_pn2pipefr_mass_flow(
|
||||
upstream_pressure=2.0e6,
|
||||
downstream_pressure=1.0e6,
|
||||
upstream_temperature=300.0,
|
||||
resistance_length=1.0,
|
||||
max_iterations=0,
|
||||
)
|
||||
|
||||
self.assertGreater(flow, 0.0)
|
||||
dynamic_viscosity.assert_not_called()
|
||||
logarithm.assert_not_called()
|
||||
|
||||
def test_builtin_iterations_evaluate_dynamic_viscosity_once(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium)
|
||||
original = IdealGasMedium.dynamic_viscosity
|
||||
|
||||
with patch.object(
|
||||
IdealGasMedium,
|
||||
"dynamic_viscosity",
|
||||
autospec=True,
|
||||
side_effect=original,
|
||||
) as dynamic_viscosity:
|
||||
flow = pipe._one_way_pn2pipefr_mass_flow(
|
||||
upstream_pressure=2.0e6,
|
||||
downstream_pressure=1.0e6,
|
||||
upstream_temperature=300.0,
|
||||
resistance_length=1.0,
|
||||
max_iterations=4,
|
||||
)
|
||||
|
||||
self.assertGreater(flow, 0.0)
|
||||
dynamic_viscosity.assert_called_once_with(self.medium, 300.0)
|
||||
|
||||
def test_builtin_fixed_point_evaluates_fully_rough_term_once(self) -> None:
|
||||
options = {
|
||||
"upstream_pressure": 2.0e6,
|
||||
"downstream_pressure": 1.0e6,
|
||||
"upstream_temperature": 300.0,
|
||||
"resistance_length": 1.0,
|
||||
"max_iterations": 4,
|
||||
}
|
||||
pipes = (
|
||||
AmesimPnl00r("pnl_00r", self.medium, rr=1.0e-5),
|
||||
AmesimPnl0001("pnl_0001", self.medium, rr=1.0e-5),
|
||||
AmesimPnl0002("pnl_0002", self.medium, rr=1.0e-5),
|
||||
)
|
||||
|
||||
for pipe in pipes:
|
||||
with self.subTest(model=type(pipe).__name__):
|
||||
with patch(
|
||||
"app.simulation.components.amesim.flow.pipes.log10",
|
||||
side_effect=log10,
|
||||
) as logarithm:
|
||||
flow = AmesimPnl0001._one_way_pn2pipefr_mass_flow(
|
||||
pipe,
|
||||
**options,
|
||||
)
|
||||
|
||||
self.assertGreater(flow, 0.0)
|
||||
fully_rough_argument = pipe.rr / 3.7
|
||||
self.assertEqual(
|
||||
sum(
|
||||
call.args == (fully_rough_argument,)
|
||||
for call in logarithm.call_args_list
|
||||
),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(logarithm.call_count, 5)
|
||||
|
||||
def test_overridden_friction_factor_keeps_virtual_iteration_path(self) -> None:
|
||||
pipe = _DelegatingFrictionPnl0001(
|
||||
"custom_friction",
|
||||
self.medium,
|
||||
rr=1.0e-5,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"app.simulation.components.amesim.flow.pipes.log10",
|
||||
side_effect=log10,
|
||||
) as logarithm:
|
||||
flow = pipe._one_way_pn2pipefr_mass_flow(
|
||||
upstream_pressure=2.0e6,
|
||||
downstream_pressure=1.0e6,
|
||||
upstream_temperature=300.0,
|
||||
resistance_length=1.0,
|
||||
max_iterations=4,
|
||||
)
|
||||
|
||||
self.assertGreater(flow, 0.0)
|
||||
self.assertEqual(len(pipe.friction_reynolds), 4)
|
||||
self.assertEqual(logarithm.call_count, 8)
|
||||
|
||||
def test_precomputed_fully_rough_path_is_bitwise_identical_by_regime(
|
||||
self,
|
||||
) -> None:
|
||||
for index, downstream_pressure in enumerate(
|
||||
(1_999_999.999, 1_999_990.0, 1_999_900.0, 1.0e6)
|
||||
):
|
||||
with self.subTest(downstream_pressure=downstream_pressure):
|
||||
built_in = AmesimPnl0001(
|
||||
f"built_in_{index}",
|
||||
self.medium,
|
||||
rr=1.0e-5,
|
||||
)
|
||||
virtual = _DelegatingFrictionPnl0001(
|
||||
f"virtual_{index}",
|
||||
self.medium,
|
||||
rr=1.0e-5,
|
||||
)
|
||||
options = {
|
||||
"upstream_pressure": 2.0e6,
|
||||
"downstream_pressure": downstream_pressure,
|
||||
"upstream_temperature": 300.0,
|
||||
"resistance_length": 1.0,
|
||||
"max_iterations": 4,
|
||||
}
|
||||
|
||||
actual = built_in._one_way_pn2pipefr_mass_flow(**options)
|
||||
expected = virtual._one_way_pn2pipefr_mass_flow(**options)
|
||||
|
||||
self.assertEqual(actual.hex(), expected.hex())
|
||||
|
||||
def test_pressure_flow_residuals_do_not_force_storage_mass_balance(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium)
|
||||
pipe.port_1.p = 101000.0
|
||||
|
||||
@@ -672,7 +672,10 @@ class ProgressiveSuiteTests(unittest.TestCase):
|
||||
[{
|
||||
"SIMULATION_CAUSAL_EXECUTOR_V2": "1",
|
||||
"SIMULATION_CAUSAL_COORDINATE_KERNEL": "1",
|
||||
"SIMULATION_CAUSAL_DIRECT_SUM_ASSIGNMENTS": "1",
|
||||
"SIMULATION_CAUSAL_DIRECT_EQUATION_READERS": "1",
|
||||
"SIMULATION_CAUSAL_FAST_PATH": "1",
|
||||
"SIMULATION_MECHANICAL_ATOL_MODE": "legacy",
|
||||
"SIMULATION_ODE_JACOBIAN_MODE": "scipy",
|
||||
"SIMULATIONAPP_PROPERTY_CACHE": "on",
|
||||
}],
|
||||
|
||||
@@ -12,6 +12,7 @@ from app.simulation.solvers.causal_ir import (
|
||||
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_pnl0001_xml import amesim_pnl0001_project
|
||||
from tests.test_amesim_pnvo001_signal_xml import high_pressure_helium_step_project
|
||||
from tests.test_generic_system_xml_simulation import chain_project
|
||||
|
||||
@@ -19,11 +20,102 @@ from tests.test_generic_system_xml_simulation import chain_project
|
||||
TARGET_XML = Path("tests/data/test-mql-8.xml")
|
||||
|
||||
|
||||
class _SyntheticDirectReaderError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _system(project) -> GenericFluidSystem:
|
||||
return GenericFluidSystem(compile_reactflow_network(project))
|
||||
|
||||
|
||||
class CausalNumericIRTests(unittest.TestCase):
|
||||
def _assert_direct_reader_exception_is_controlled(
|
||||
self,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
system = _system(amesim_pnl0001_project())
|
||||
solver = system.pressure_flow_solver
|
||||
compilation = compile_causal_numeric_ir(solver)
|
||||
self.assertTrue(compilation.supported, compilation.fallback_reason)
|
||||
assert compilation.ir is not None
|
||||
ir = compilation.ir
|
||||
direct_evaluation = next(
|
||||
evaluation
|
||||
for stage in ir.program.effort_stages
|
||||
for evaluation in stage.evaluations
|
||||
if evaluation.opcode is CausalIROpcode.EFFORT_DIRECT_RESIDUAL
|
||||
)
|
||||
evaluators = list(ir.bindings.evaluators)
|
||||
victim = solver.unknowns[-1]
|
||||
before = tuple(unknown.read() for unknown in solver.unknowns)
|
||||
|
||||
def failing_reader() -> float:
|
||||
victim.write(victim.read() + 123.0)
|
||||
raise exception
|
||||
|
||||
evaluators[direct_evaluation.evaluator_slot] = failing_reader
|
||||
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,
|
||||
f"effortEvaluationFailed:{type(exception).__name__}",
|
||||
)
|
||||
self.assertTrue(result.rolled_back)
|
||||
self.assertEqual(
|
||||
tuple(unknown.read() for unknown in solver.unknowns),
|
||||
before,
|
||||
)
|
||||
|
||||
def test_direct_reader_key_error_returns_controlled_failure(self) -> None:
|
||||
self._assert_direct_reader_exception_is_controlled(
|
||||
KeyError("synthetic direct-reader key failure")
|
||||
)
|
||||
|
||||
def test_direct_reader_custom_exception_returns_controlled_failure(
|
||||
self,
|
||||
) -> None:
|
||||
self._assert_direct_reader_exception_is_controlled(
|
||||
_SyntheticDirectReaderError("synthetic direct-reader failure")
|
||||
)
|
||||
|
||||
def test_direct_reader_memory_error_remains_fatal(self) -> None:
|
||||
system = _system(amesim_pnl0001_project())
|
||||
compilation = compile_causal_numeric_ir(system.pressure_flow_solver)
|
||||
self.assertTrue(compilation.supported, compilation.fallback_reason)
|
||||
assert compilation.ir is not None
|
||||
ir = compilation.ir
|
||||
direct_evaluation = next(
|
||||
evaluation
|
||||
for stage in ir.program.effort_stages
|
||||
for evaluation in stage.evaluations
|
||||
if evaluation.opcode is CausalIROpcode.EFFORT_DIRECT_RESIDUAL
|
||||
)
|
||||
evaluators = list(ir.bindings.evaluators)
|
||||
|
||||
def failing_reader() -> float:
|
||||
raise MemoryError("synthetic direct-reader allocation failure")
|
||||
|
||||
evaluators[direct_evaluation.evaluator_slot] = failing_reader
|
||||
failing_ir = replace(
|
||||
ir,
|
||||
bindings=replace(ir.bindings, evaluators=tuple(evaluators)),
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(MemoryError, "allocation failure"):
|
||||
failing_ir.execute(
|
||||
failing_ir.create_workspace(),
|
||||
transactional=True,
|
||||
)
|
||||
|
||||
def test_structure_signature_is_stable_and_excludes_bindings(self) -> None:
|
||||
first = compile_causal_numeric_ir(
|
||||
_system(zero_force_mass_project()).pressure_flow_solver
|
||||
@@ -242,18 +334,56 @@ class TargetCausalNumericIRStructureTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(len(pressure_stage.operations), 76)
|
||||
self.assertEqual(len(pressure_stage.evaluations), 40)
|
||||
self.assertTrue(
|
||||
all(
|
||||
self.assertEqual(
|
||||
sum(
|
||||
evaluation.opcode is CausalIROpcode.EFFORT_DIRECT_RESIDUAL
|
||||
for evaluation in pressure_stage.evaluations
|
||||
),
|
||||
20,
|
||||
)
|
||||
self.assertEqual(
|
||||
sum(
|
||||
evaluation.opcode
|
||||
is CausalIROpcode.EFFORT_COMPONENT_RESIDUAL
|
||||
for evaluation in pressure_stage.evaluations
|
||||
)
|
||||
),
|
||||
20,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.system.pressure_flow_solver.causal_execution_diagnostics()[
|
||||
"directEffortAnchorCount"
|
||||
],
|
||||
20,
|
||||
)
|
||||
self.assertEqual(
|
||||
[len(stage.target_slots) for stage in program.flow_stages],
|
||||
[114, 134, 49, 33, 5, 1],
|
||||
)
|
||||
|
||||
def test_target_preserves_exact_direct_sum_assignment_counts(self) -> None:
|
||||
solver = self.system.pressure_flow_solver
|
||||
self.assertEqual(
|
||||
solver.causal_execution_diagnostics()[
|
||||
"directSumFlowAssignmentCount"
|
||||
],
|
||||
38,
|
||||
)
|
||||
component_equation_ids = frozenset(
|
||||
equation.id
|
||||
for evaluation in solver._component_equation_plan
|
||||
for equation in evaluation.templates
|
||||
)
|
||||
secondary = (
|
||||
self.system._thermofluid_closure_plan.secondary_block_solvers[0]
|
||||
)
|
||||
secondary_direct_count = sum(
|
||||
assignment.evaluate is not None
|
||||
and assignment.equation_id in component_equation_ids
|
||||
for stage in secondary._selected_explicit_flow_plan
|
||||
for assignment in stage.assignments
|
||||
)
|
||||
self.assertEqual(secondary_direct_count, 36)
|
||||
|
||||
def test_target_reference_execution_matches_all_compatibility_slots(self) -> None:
|
||||
assert self.compilation.ir is not None
|
||||
solver = self.system.pressure_flow_solver
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import math
|
||||
import sys
|
||||
import threading
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
@@ -8,6 +9,7 @@ from app.simulation.core.errors import RecoverableTrialStateError
|
||||
from app.simulation.solvers.solver import (
|
||||
IntegrationCancelled,
|
||||
SolveIVPConfig,
|
||||
SolverActivityTracker,
|
||||
StateTransition,
|
||||
integrate_ode,
|
||||
)
|
||||
@@ -44,6 +46,164 @@ def _counting_fixed_step_solver(
|
||||
|
||||
|
||||
class IntegrateOdeTests(unittest.TestCase):
|
||||
def test_activity_tracking_preserves_the_exact_trajectory(self) -> None:
|
||||
options = {
|
||||
"rhs": lambda time, state: [
|
||||
-0.5 * float(state[0]) + 0.1 * float(time)
|
||||
],
|
||||
"initial_state": [1.0],
|
||||
"config": SolveIVPConfig(
|
||||
t_start=0.0,
|
||||
t_stop=0.1,
|
||||
method="BDF",
|
||||
max_step=0.01,
|
||||
),
|
||||
"t_eval": [0.0, 0.025, 0.05, 0.075, 0.1],
|
||||
"cancel_check": lambda: False,
|
||||
}
|
||||
|
||||
baseline = integrate_ode(**options)
|
||||
tracked = integrate_ode(
|
||||
**options,
|
||||
activity_tracker=SolverActivityTracker(),
|
||||
)
|
||||
|
||||
self.assertTrue(baseline.success, baseline.message)
|
||||
self.assertTrue(tracked.success, tracked.message)
|
||||
self.assertEqual(tracked.t, baseline.t)
|
||||
self.assertEqual(tracked.y, baseline.y)
|
||||
self.assertEqual(tracked.solver_segments, baseline.solver_segments)
|
||||
|
||||
def test_activity_advances_while_accepted_time_is_unchanged(self) -> None:
|
||||
import numpy as np
|
||||
|
||||
rhs_started = threading.Event()
|
||||
release_rhs = threading.Event()
|
||||
tracker = SolverActivityTracker()
|
||||
results = []
|
||||
errors: list[BaseException] = []
|
||||
|
||||
class BlockingSolver:
|
||||
def __init__(self, fun, t0, y0, t_bound, **_kwargs):
|
||||
self.fun = fun
|
||||
self.t = float(t0)
|
||||
self.y = np.asarray(y0, dtype=float)
|
||||
self.t_bound = float(t_bound)
|
||||
self.status = "running"
|
||||
self.nfev = 0
|
||||
self.njev = 0
|
||||
self.nlu = 0
|
||||
|
||||
def step(self):
|
||||
self.fun(0.25, self.y)
|
||||
self.nfev += 1
|
||||
self.t = self.t_bound
|
||||
self.y = np.asarray([self.t], dtype=float)
|
||||
self.status = "finished"
|
||||
return None
|
||||
|
||||
def rhs(_time, _state):
|
||||
rhs_started.set()
|
||||
if not release_rhs.wait(timeout=2.0):
|
||||
raise TimeoutError("test did not release the blocking RHS")
|
||||
return [1.0]
|
||||
|
||||
def run_integration() -> None:
|
||||
try:
|
||||
results.append(
|
||||
integrate_ode(
|
||||
rhs=rhs,
|
||||
initial_state=[0.0],
|
||||
config=SolveIVPConfig(t_stop=1.0, method="BDF"),
|
||||
cancel_check=lambda: False,
|
||||
activity_tracker=tracker,
|
||||
)
|
||||
)
|
||||
except BaseException as exc: # pragma: no cover - test handoff
|
||||
errors.append(exc)
|
||||
|
||||
with patch("scipy.integrate.BDF", BlockingSolver):
|
||||
worker = threading.Thread(target=run_integration)
|
||||
worker.start()
|
||||
self.assertTrue(rhs_started.wait(timeout=2.0))
|
||||
during_rhs = tracker.snapshot()
|
||||
self.assertEqual(during_rhs.activity_kind, "rhs")
|
||||
self.assertGreater(during_rhs.activity_sequence, 0)
|
||||
self.assertEqual(during_rhs.current_trial_time, 0.25)
|
||||
self.assertEqual(during_rhs.rhs_call_count, 1)
|
||||
self.assertEqual(during_rhs.accepted_step_sequence, 0)
|
||||
self.assertEqual(during_rhs.accepted_time, 0.0)
|
||||
release_rhs.set()
|
||||
worker.join(timeout=2.0)
|
||||
|
||||
self.assertFalse(worker.is_alive())
|
||||
self.assertEqual(errors, [])
|
||||
self.assertTrue(results[0].success, results[0].message)
|
||||
completed = tracker.snapshot()
|
||||
self.assertEqual(completed.accepted_step_sequence, 1)
|
||||
self.assertEqual(completed.accepted_time, 1.0)
|
||||
self.assertEqual(completed.solver_step_sequence, 1)
|
||||
|
||||
def test_activity_distinguishes_blocking_jacobian_evaluation(self) -> None:
|
||||
import numpy as np
|
||||
|
||||
jacobian_started = threading.Event()
|
||||
release_jacobian = threading.Event()
|
||||
tracker = SolverActivityTracker()
|
||||
results = []
|
||||
|
||||
class BlockingSolver:
|
||||
def __init__(self, _fun, t0, y0, t_bound, **options):
|
||||
self.jac = options["jac"]
|
||||
self.t = float(t0)
|
||||
self.y = np.asarray(y0, dtype=float)
|
||||
self.t_bound = float(t_bound)
|
||||
self.status = "running"
|
||||
self.nfev = 0
|
||||
self.njev = 0
|
||||
self.nlu = 0
|
||||
|
||||
def step(self):
|
||||
self.jac(0.5, self.y)
|
||||
self.njev += 1
|
||||
self.t = self.t_bound
|
||||
self.status = "finished"
|
||||
return None
|
||||
|
||||
class BlockingJacobian:
|
||||
def __call__(self, _time, _state):
|
||||
jacobian_started.set()
|
||||
if not release_jacobian.wait(timeout=2.0):
|
||||
raise TimeoutError("test did not release the Jacobian")
|
||||
return np.asarray([[0.0]])
|
||||
|
||||
def run_integration() -> None:
|
||||
results.append(
|
||||
integrate_ode(
|
||||
rhs=lambda _time, _state: [0.0],
|
||||
initial_state=[0.0],
|
||||
config=SolveIVPConfig(t_stop=1.0, method="BDF"),
|
||||
cancel_check=lambda: False,
|
||||
jac=BlockingJacobian(),
|
||||
activity_tracker=tracker,
|
||||
)
|
||||
)
|
||||
|
||||
with patch("scipy.integrate.BDF", BlockingSolver):
|
||||
worker = threading.Thread(target=run_integration)
|
||||
worker.start()
|
||||
self.assertTrue(jacobian_started.wait(timeout=2.0))
|
||||
during_jacobian = tracker.snapshot()
|
||||
self.assertEqual(during_jacobian.activity_kind, "jacobian")
|
||||
self.assertEqual(during_jacobian.jacobian_evaluation_count, 1)
|
||||
self.assertEqual(during_jacobian.rhs_call_count, 0)
|
||||
self.assertEqual(during_jacobian.accepted_step_sequence, 0)
|
||||
release_jacobian.set()
|
||||
worker.join(timeout=2.0)
|
||||
|
||||
self.assertFalse(worker.is_alive())
|
||||
self.assertTrue(results[0].success, results[0].message)
|
||||
|
||||
def test_generic_solver_keeps_canonical_default_tolerance(self) -> None:
|
||||
self.assertEqual(SolveIVPConfig().atol, 1.0e-8)
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ from app.main import (
|
||||
)
|
||||
from app.simulation.components.experimental.flow.resistive_pipe import ResistivePipe
|
||||
from app.simulation.registry import get_component_model_spec
|
||||
from app.simulation.solvers.solver import SolveIVPConfig
|
||||
from app.simulation.solvers.solver import (
|
||||
SolveIVPConfig,
|
||||
SolverActivityTracker,
|
||||
)
|
||||
from app.simulation.systems.generic import (
|
||||
GenericFluidSystem,
|
||||
SimulationPreparationError,
|
||||
@@ -259,7 +262,9 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
|
||||
def test_generic_chain_simulation_conserves_mass_and_moves_pressures(self) -> None:
|
||||
network = compile_reactflow_network(chain_project())
|
||||
progress: list[tuple[float, str]] = []
|
||||
result = GenericFluidSystem(network).simulate(
|
||||
activity_tracker = SolverActivityTracker()
|
||||
system = GenericFluidSystem(network)
|
||||
result = system.simulate(
|
||||
SolveIVPConfig(
|
||||
t_start=0.0,
|
||||
t_stop=0.01,
|
||||
@@ -268,6 +273,7 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
|
||||
),
|
||||
sample_step=0.005,
|
||||
progress_callback=lambda value, phase: progress.append((value, phase)),
|
||||
activity_tracker=activity_tracker,
|
||||
)
|
||||
|
||||
self.assertTrue(result.success)
|
||||
@@ -281,6 +287,23 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
|
||||
)
|
||||
self.assertIn("integrating", {phase for _, phase in progress})
|
||||
self.assertIn("postprocessing", {phase for _, phase in progress})
|
||||
activity = activity_tracker.snapshot()
|
||||
self.assertEqual(activity.activity_kind, "complete")
|
||||
self.assertGreater(activity.rhs_call_count, 0)
|
||||
self.assertGreater(activity.accepted_step_sequence, 0)
|
||||
self.assertEqual(activity.accepted_time, 0.01)
|
||||
self.assertGreater(activity.thermofluid_closure_count, 0)
|
||||
self.assertEqual(
|
||||
result.diagnostics["activity"],
|
||||
activity.as_dict(),
|
||||
)
|
||||
self.assertIsNone(system._activity_tracker)
|
||||
self.assertEqual(
|
||||
result.diagnostics["integration"][
|
||||
"mechanicalAbsoluteTolerance"
|
||||
]["mode"],
|
||||
"legacy",
|
||||
)
|
||||
self.assertLess(result.series["cylinder_1.p"][-1], 500000.0)
|
||||
self.assertGreater(result.series["tank_1.p"][-1], 100000.0)
|
||||
total_mass = [
|
||||
@@ -315,6 +338,33 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
|
||||
pressure_flow["optimizerEvaluationCount"],
|
||||
)
|
||||
|
||||
def test_activity_tracker_scope_is_restored_after_simulation_error(self) -> None:
|
||||
system = GenericFluidSystem(compile_reactflow_network(chain_project()))
|
||||
previous_tracker = SolverActivityTracker()
|
||||
active_tracker = SolverActivityTracker()
|
||||
system._activity_tracker = previous_tracker
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
system,
|
||||
"_simulate",
|
||||
side_effect=RuntimeError("synthetic simulation failure"),
|
||||
),
|
||||
self.assertRaisesRegex(RuntimeError, "synthetic simulation failure"),
|
||||
):
|
||||
system.simulate(
|
||||
SolveIVPConfig(
|
||||
t_start=0.0,
|
||||
t_stop=0.01,
|
||||
method="BDF",
|
||||
max_step=0.001,
|
||||
),
|
||||
sample_step=0.005,
|
||||
activity_tracker=active_tracker,
|
||||
)
|
||||
|
||||
self.assertIs(system._activity_tracker, previous_tracker)
|
||||
|
||||
def test_cancelled_simulation_returns_accepted_partial_samples(self) -> None:
|
||||
cancel_event = threading.Event()
|
||||
|
||||
@@ -569,10 +619,24 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
|
||||
self.assertEqual(snapshot["result"]["status"], "stopped")
|
||||
|
||||
def test_streaming_endpoint_keeps_quiet_solver_connection_alive(self) -> None:
|
||||
def delayed_simulation(_xml_bytes, progress_callback, _cancel_check=None):
|
||||
def delayed_simulation(
|
||||
_xml_bytes,
|
||||
progress_callback,
|
||||
_cancel_check=None,
|
||||
activity_tracker=None,
|
||||
):
|
||||
progress_callback(49, "integrating", "正在进行时间积分与压力流量求解")
|
||||
time.sleep(0.03)
|
||||
return {"success": True}
|
||||
self.assertIsNotNone(activity_tracker)
|
||||
activity_tracker.start_integration(0.0)
|
||||
for trial_time in (0.0487, 0.0488, 0.0489, 0.0490):
|
||||
activity_tracker.record_rhs(trial_time)
|
||||
time.sleep(0.008)
|
||||
return {
|
||||
"success": True,
|
||||
"status": "completed",
|
||||
"simulatedUntil": 0.2,
|
||||
"requestedStopTime": 0.2,
|
||||
}
|
||||
|
||||
with (
|
||||
patch("app.main.SIMULATION_STREAM_HEARTBEAT_SECONDS", 0.005),
|
||||
@@ -590,7 +654,21 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
|
||||
self.assertGreaterEqual(len(heartbeats), 1)
|
||||
self.assertTrue(all(event["progress"] == 49 for event in heartbeats))
|
||||
self.assertTrue(all(event["phase"] == "integrating" for event in heartbeats))
|
||||
self.assertTrue(
|
||||
all(event["acceptedStepSequence"] == 0 for event in heartbeats)
|
||||
)
|
||||
self.assertTrue(all(event["acceptedTime"] == 0.0 for event in heartbeats))
|
||||
self.assertGreater(
|
||||
len({event["activitySequence"] for event in heartbeats}),
|
||||
1,
|
||||
)
|
||||
self.assertTrue(
|
||||
all(event["activityKind"] == "rhs" for event in heartbeats)
|
||||
)
|
||||
self.assertEqual(events[-1]["event"], "result")
|
||||
self.assertEqual(events[-1]["activityKind"], "complete")
|
||||
self.assertEqual(events[-1]["acceptedStepSequence"], 0)
|
||||
self.assertEqual(events[-1]["rhsCallCount"], 4)
|
||||
|
||||
def test_streaming_endpoint_returns_structured_validation_error(self) -> None:
|
||||
events = [
|
||||
|
||||
@@ -166,7 +166,7 @@ class MaxStepMatrixTests(unittest.TestCase):
|
||||
DEFAULT_MANIFEST_PATH,
|
||||
horizon_case_ids=("1s", 2.0),
|
||||
max_steps=(0.01, 0.05),
|
||||
additional_checkpoint_times=(1.0885267285, 1.0997760276),
|
||||
additional_checkpoint_times=(1.08, 1.10),
|
||||
expected_projection_count=2,
|
||||
case_executor=execute,
|
||||
)
|
||||
@@ -180,7 +180,7 @@ class MaxStepMatrixTests(unittest.TestCase):
|
||||
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),
|
||||
(0.0, 0.04, 0.8, 1.0, 1.08, 1.1, 2.0),
|
||||
)
|
||||
self.assertEqual(
|
||||
report["configuration"]["horizons"][1]["horizonCaseId"],
|
||||
@@ -248,6 +248,50 @@ class MaxStepMatrixTests(unittest.TestCase):
|
||||
prefix_comparisons[0]["stateProjection"]["valueMismatchCount"], 0
|
||||
)
|
||||
|
||||
def test_cross_horizon_comparison_excludes_short_terminal(self) -> None:
|
||||
def execute(request: RegressionCaseRequest) -> dict[str, object]:
|
||||
result = _fake_completed(request)
|
||||
if request.stop_time <= 1.0:
|
||||
checkpoints = result["worker"]["summary"]["physicalContract"][
|
||||
"checkpoints"
|
||||
]
|
||||
for checkpoint in checkpoints:
|
||||
if checkpoint["requestedTime"] == request.stop_time:
|
||||
checkpoint["stateValues"]["state.a"] += 1.0
|
||||
return result
|
||||
|
||||
report = run_max_step_matrix(
|
||||
DEFAULT_MANIFEST_PATH,
|
||||
horizon_case_ids=("1s", 2.0),
|
||||
max_steps=(0.01,),
|
||||
expected_projection_count=2,
|
||||
case_executor=execute,
|
||||
)
|
||||
|
||||
comparison = report["comparisons"]["sameMaxStepAcrossHorizons"][0]
|
||||
self.assertTrue(comparison["strictPrefix"])
|
||||
self.assertTrue(comparison["passed"])
|
||||
self.assertNotIn(
|
||||
1.0,
|
||||
comparison["stateProjection"]["commonCheckpointTimes"],
|
||||
)
|
||||
|
||||
def test_same_horizon_comparison_keeps_terminal_checkpoint(self) -> None:
|
||||
report = run_max_step_matrix(
|
||||
DEFAULT_MANIFEST_PATH,
|
||||
horizon_case_ids=("1s",),
|
||||
max_steps=(0.01, 0.05),
|
||||
expected_projection_count=2,
|
||||
case_executor=_fake_completed,
|
||||
)
|
||||
|
||||
comparison = report["comparisons"]["sameHorizonAcrossMaxSteps"][0]
|
||||
self.assertFalse(comparison["strictPrefix"])
|
||||
self.assertIn(
|
||||
1.0,
|
||||
comparison["stateProjection"]["commonCheckpointTimes"],
|
||||
)
|
||||
|
||||
def test_horizons_must_increase_and_timeout_override_is_bounded(self) -> None:
|
||||
with self.assertRaisesRegex(
|
||||
RegressionManifestError, "strictly increasing"
|
||||
@@ -259,6 +303,20 @@ class MaxStepMatrixTests(unittest.TestCase):
|
||||
expected_projection_count=2,
|
||||
case_executor=_fake_completed,
|
||||
)
|
||||
|
||||
def test_off_grid_checkpoint_is_rejected_before_execution(self) -> None:
|
||||
with self.assertRaisesRegex(
|
||||
RegressionManifestError,
|
||||
"not represented by the .* output grid",
|
||||
):
|
||||
run_max_step_matrix(
|
||||
DEFAULT_MANIFEST_PATH,
|
||||
horizon_case_ids=("1s",),
|
||||
max_steps=(0.01,),
|
||||
additional_checkpoint_times=(0.0489,),
|
||||
expected_projection_count=2,
|
||||
case_executor=_fake_completed,
|
||||
)
|
||||
with self.assertRaisesRegex(RegressionManifestError, "must exceed"):
|
||||
run_max_step_matrix(
|
||||
DEFAULT_MANIFEST_PATH,
|
||||
|
||||
@@ -5,24 +5,73 @@ import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.main import compile_reactflow_network
|
||||
from app.simulation.components.amesim.flow.orifices import (
|
||||
AmesimPnvo001SignalOpening,
|
||||
)
|
||||
from app.simulation.components.amesim.flow.pipes import AmesimPnl0001
|
||||
from app.simulation.registry import ComponentModelSpec, get_component_model_spec
|
||||
from app.simulation.solvers.algebraic import (
|
||||
CAUSAL_COORDINATE_KERNEL_ENVIRONMENT_VARIABLE,
|
||||
CAUSAL_DIRECT_EQUATION_READERS_ENVIRONMENT_VARIABLE,
|
||||
CAUSAL_DIRECT_SUM_ASSIGNMENTS_ENVIRONMENT_VARIABLE,
|
||||
CAUSAL_EXECUTOR_V2_ENVIRONMENT_VARIABLE,
|
||||
CAUSAL_FAST_PATH_ENVIRONMENT_VARIABLE,
|
||||
PressureFlowSolver,
|
||||
)
|
||||
from app.simulation.systems.generic import GenericFluidSystem
|
||||
from tests.test_amesim_mechanical_xml import zero_force_mass_project
|
||||
from tests.test_amesim_pnl0001_xml import amesim_pnl0001_project
|
||||
from tests.test_amesim_pnvo001_signal_xml import (
|
||||
high_pressure_helium_step_project,
|
||||
)
|
||||
from tests.test_generic_system_xml_simulation import chain_project
|
||||
|
||||
|
||||
class _UntrustedPnvo001(AmesimPnvo001SignalOpening):
|
||||
# A module-name check alone would incorrectly trust this changed subclass.
|
||||
__module__ = AmesimPnvo001SignalOpening.__module__
|
||||
pressure_flow_callback_count = 0
|
||||
|
||||
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||
self.pressure_flow_callback_count += 1
|
||||
return super().pressure_flow_equation_values()
|
||||
|
||||
|
||||
class _InheritedDirectReaderPnl0001(AmesimPnl0001):
|
||||
"""A changed concrete class that does not repeat the scalar promise."""
|
||||
|
||||
|
||||
class _NonfiniteDirectReaderPnl0001(AmesimPnl0001):
|
||||
__module__ = AmesimPnl0001.__module__
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.full_equation_evaluation_count = 0
|
||||
self.direct_equation_evaluation_count = 0
|
||||
|
||||
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||
self.full_equation_evaluation_count += 1
|
||||
return super().pressure_flow_equation_values()
|
||||
|
||||
def pressure_flow_equation_value_readers(self):
|
||||
def nonfinite_pressure_state_residual() -> float:
|
||||
self.direct_equation_evaluation_count += 1
|
||||
return float("nan")
|
||||
|
||||
return {
|
||||
f"{self.name}:port_2_pressure_state": (
|
||||
nonfinite_pressure_state_residual
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _system(
|
||||
project,
|
||||
*,
|
||||
executor_v2: bool | None = False,
|
||||
coordinate_kernel: bool | None = None,
|
||||
direct_sum_assignments: bool | None = None,
|
||||
direct_equation_readers: bool | None = None,
|
||||
) -> GenericFluidSystem:
|
||||
environment = {}
|
||||
if executor_v2 is not None:
|
||||
@@ -33,6 +82,14 @@ def _system(
|
||||
environment[CAUSAL_COORDINATE_KERNEL_ENVIRONMENT_VARIABLE] = (
|
||||
"1" if coordinate_kernel else "0"
|
||||
)
|
||||
if direct_sum_assignments is not None:
|
||||
environment[CAUSAL_DIRECT_SUM_ASSIGNMENTS_ENVIRONMENT_VARIABLE] = (
|
||||
"1" if direct_sum_assignments else "0"
|
||||
)
|
||||
if direct_equation_readers is not None:
|
||||
environment[CAUSAL_DIRECT_EQUATION_READERS_ENVIRONMENT_VARIABLE] = (
|
||||
"1" if direct_equation_readers else "0"
|
||||
)
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
environment,
|
||||
@@ -44,10 +101,299 @@ def _system(
|
||||
CAUSAL_COORDINATE_KERNEL_ENVIRONMENT_VARIABLE,
|
||||
None,
|
||||
)
|
||||
if direct_sum_assignments is None:
|
||||
os.environ.pop(
|
||||
CAUSAL_DIRECT_SUM_ASSIGNMENTS_ENVIRONMENT_VARIABLE,
|
||||
None,
|
||||
)
|
||||
if direct_equation_readers is None:
|
||||
os.environ.pop(
|
||||
CAUSAL_DIRECT_EQUATION_READERS_ENVIRONMENT_VARIABLE,
|
||||
None,
|
||||
)
|
||||
return GenericFluidSystem(compile_reactflow_network(project))
|
||||
|
||||
|
||||
def _system_with_pnl0001_class(component_class, **options) -> GenericFluidSystem:
|
||||
original_spec = get_component_model_spec("amesim_pnl0001")
|
||||
custom_spec = ComponentModelSpec(
|
||||
component_class=component_class,
|
||||
library=original_spec.library,
|
||||
)
|
||||
|
||||
def custom_spec_lookup(model_type: str):
|
||||
if model_type == "amesim_pnl0001":
|
||||
return custom_spec
|
||||
return get_component_model_spec(model_type)
|
||||
|
||||
with patch(
|
||||
"app.simulation.registry.get_component_model_spec",
|
||||
side_effect=custom_spec_lookup,
|
||||
):
|
||||
return _system(amesim_pnl0001_project(), **options)
|
||||
|
||||
|
||||
class PressureFlowCausalExecutionTests(unittest.TestCase):
|
||||
def test_direct_equation_reader_is_default_on_and_bitwise_optional(
|
||||
self,
|
||||
) -> None:
|
||||
direct = _system(
|
||||
amesim_pnl0001_project(),
|
||||
executor_v2=True,
|
||||
direct_equation_readers=True,
|
||||
)
|
||||
callback = _system(
|
||||
amesim_pnl0001_project(),
|
||||
executor_v2=True,
|
||||
direct_equation_readers=False,
|
||||
)
|
||||
direct_state = direct.initial_state_vector()
|
||||
callback_state = callback.initial_state_vector()
|
||||
|
||||
for time in (0.0, 0.001):
|
||||
self.assertEqual(
|
||||
direct.rhs(time, direct_state),
|
||||
callback.rhs(time, callback_state),
|
||||
)
|
||||
self.assertEqual(
|
||||
tuple(
|
||||
unknown.read()
|
||||
for unknown in direct.pressure_flow_solver.unknowns
|
||||
),
|
||||
tuple(
|
||||
unknown.read()
|
||||
for unknown in callback.pressure_flow_solver.unknowns
|
||||
),
|
||||
)
|
||||
|
||||
direct_diagnostics = (
|
||||
direct.pressure_flow_solver.causal_execution_diagnostics()
|
||||
)
|
||||
callback_diagnostics = (
|
||||
callback.pressure_flow_solver.causal_execution_diagnostics()
|
||||
)
|
||||
self.assertTrue(direct_diagnostics["directEquationReadersConfigured"])
|
||||
self.assertEqual(direct_diagnostics["directEffortAnchorCount"], 1)
|
||||
self.assertFalse(
|
||||
callback_diagnostics["directEquationReadersConfigured"]
|
||||
)
|
||||
self.assertEqual(callback_diagnostics["directEffortAnchorCount"], 0)
|
||||
|
||||
def test_inherited_direct_equation_reader_does_not_opt_in_subclass(
|
||||
self,
|
||||
) -> None:
|
||||
system = _system_with_pnl0001_class(
|
||||
_InheritedDirectReaderPnl0001,
|
||||
direct_equation_readers=True,
|
||||
)
|
||||
pipe = system.network.components["pnl_1"]
|
||||
solver = system.pressure_flow_solver
|
||||
|
||||
self.assertIsInstance(pipe, _InheritedDirectReaderPnl0001)
|
||||
self.assertEqual(
|
||||
solver.causal_execution_diagnostics()["directEffortAnchorCount"],
|
||||
0,
|
||||
)
|
||||
anchor = next(
|
||||
anchor
|
||||
for group in solver._effort_groups["p"]
|
||||
for anchor in group.anchors
|
||||
if anchor.equation_id == "pnl_1:port_2_pressure_state"
|
||||
)
|
||||
self.assertIsNone(anchor.causal_evaluate)
|
||||
|
||||
def test_nonfinite_direct_effort_reader_uses_authoritative_fallback(
|
||||
self,
|
||||
) -> None:
|
||||
system = _system_with_pnl0001_class(
|
||||
_NonfiniteDirectReaderPnl0001,
|
||||
executor_v2=True,
|
||||
direct_equation_readers=True,
|
||||
)
|
||||
pipe = system.network.components["pnl_1"]
|
||||
solver = system.pressure_flow_solver
|
||||
self.assertIsInstance(pipe, _NonfiniteDirectReaderPnl0001)
|
||||
self.assertEqual(
|
||||
solver.causal_execution_diagnostics()["directEffortAnchorCount"],
|
||||
1,
|
||||
)
|
||||
|
||||
derivative = system.rhs(0.0, system.initial_state_vector())
|
||||
|
||||
self.assertTrue(all(value == value for value in derivative))
|
||||
diagnostics = solver.causal_execution_diagnostics()
|
||||
self.assertFalse(diagnostics["enabled"])
|
||||
self.assertEqual(
|
||||
diagnostics["disabledReason"],
|
||||
"nonFiniteCausalEffortAnchor",
|
||||
)
|
||||
self.assertGreaterEqual(diagnostics["legacyFallbackCount"], 1)
|
||||
self.assertGreater(pipe.direct_equation_evaluation_count, 0)
|
||||
self.assertGreater(pipe.full_equation_evaluation_count, 0)
|
||||
|
||||
pipe.direct_equation_evaluation_count = 0
|
||||
pipe.full_equation_evaluation_count = 0
|
||||
solver.propagate_equal_efforts(("p",))
|
||||
|
||||
self.assertEqual(pipe.direct_equation_evaluation_count, 0)
|
||||
self.assertEqual(pipe.full_equation_evaluation_count, 1)
|
||||
|
||||
def test_direct_sum_keeps_same_module_subclass_on_callback_plan(self) -> None:
|
||||
original_spec = get_component_model_spec("amesim_pnvo001")
|
||||
custom_spec = ComponentModelSpec(
|
||||
component_class=_UntrustedPnvo001,
|
||||
library=original_spec.library,
|
||||
)
|
||||
|
||||
def custom_spec_lookup(model_type: str):
|
||||
if model_type == "amesim_pnvo001":
|
||||
return custom_spec
|
||||
return get_component_model_spec(model_type)
|
||||
|
||||
with patch(
|
||||
"app.simulation.registry.get_component_model_spec",
|
||||
side_effect=custom_spec_lookup,
|
||||
):
|
||||
system = _system(
|
||||
high_pressure_helium_step_project(),
|
||||
direct_sum_assignments=True,
|
||||
)
|
||||
|
||||
solver = system.pressure_flow_solver
|
||||
valve = system.network.components["valve_1"]
|
||||
self.assertIsInstance(valve, _UntrustedPnvo001)
|
||||
assignment = next(
|
||||
assignment
|
||||
for stage in solver._explicit_flow_plan
|
||||
for assignment in stage.assignments
|
||||
if assignment.equation_id == "valve_1:mass_flow_balance"
|
||||
)
|
||||
self.assertIsNone(assignment.evaluate)
|
||||
self.assertIs(assignment.component, valve)
|
||||
self.assertEqual(
|
||||
solver.causal_execution_diagnostics()[
|
||||
"directSumFlowAssignmentCount"
|
||||
],
|
||||
0,
|
||||
)
|
||||
|
||||
valve.pressure_flow_callback_count = 0
|
||||
system.rhs(0.041, system.initial_state_vector())
|
||||
self.assertGreater(valve.pressure_flow_callback_count, 0)
|
||||
|
||||
def test_direct_sum_assignments_match_disabled_path_bitwise(self) -> None:
|
||||
direct = _system(
|
||||
high_pressure_helium_step_project(),
|
||||
direct_sum_assignments=True,
|
||||
)
|
||||
callback = _system(
|
||||
high_pressure_helium_step_project(),
|
||||
direct_sum_assignments=False,
|
||||
)
|
||||
direct_state = direct.initial_state_vector()
|
||||
callback_state = callback.initial_state_vector()
|
||||
|
||||
for time in (0.0, 0.041, 0.8):
|
||||
self.assertEqual(
|
||||
direct.rhs(time, direct_state),
|
||||
callback.rhs(time, callback_state),
|
||||
)
|
||||
self.assertEqual(
|
||||
tuple(
|
||||
unknown.read()
|
||||
for unknown in direct.pressure_flow_solver.unknowns
|
||||
),
|
||||
tuple(
|
||||
unknown.read()
|
||||
for unknown in callback.pressure_flow_solver.unknowns
|
||||
),
|
||||
)
|
||||
|
||||
direct_diagnostics = (
|
||||
direct.pressure_flow_solver.causal_execution_diagnostics()
|
||||
)
|
||||
callback_diagnostics = (
|
||||
callback.pressure_flow_solver.causal_execution_diagnostics()
|
||||
)
|
||||
self.assertTrue(
|
||||
direct_diagnostics["directSumAssignmentsConfigured"]
|
||||
)
|
||||
self.assertFalse(
|
||||
callback_diagnostics["directSumAssignmentsConfigured"]
|
||||
)
|
||||
self.assertGreater(
|
||||
direct_diagnostics["directSumFlowAssignmentCount"],
|
||||
0,
|
||||
)
|
||||
self.assertEqual(
|
||||
callback_diagnostics["directSumFlowAssignmentCount"],
|
||||
0,
|
||||
)
|
||||
self.assertEqual(
|
||||
direct_diagnostics["compiledFlowAssignmentCount"],
|
||||
callback_diagnostics["compiledFlowAssignmentCount"],
|
||||
)
|
||||
self.assertLess(
|
||||
direct_diagnostics["directSumFlowAssignmentCount"],
|
||||
direct_diagnostics["compiledFlowAssignmentCount"],
|
||||
)
|
||||
for key in (
|
||||
"fastSolveCount",
|
||||
"fullResidualAuditCount",
|
||||
"auditFailureCount",
|
||||
"legacyFallbackCount",
|
||||
):
|
||||
self.assertEqual(direct_diagnostics[key], callback_diagnostics[key])
|
||||
self.assertGreater(direct_diagnostics["fastSolveCount"], 0)
|
||||
self.assertEqual(direct_diagnostics["auditFailureCount"], 0)
|
||||
self.assertEqual(direct_diagnostics["legacyFallbackCount"], 0)
|
||||
|
||||
def test_direct_sum_reader_rejection_uses_safe_component_fallback(
|
||||
self,
|
||||
) -> None:
|
||||
callback = _system(
|
||||
high_pressure_helium_step_project(),
|
||||
direct_sum_assignments=False,
|
||||
)
|
||||
with patch.object(
|
||||
PressureFlowSolver,
|
||||
"_sum_to_zero_flow_target_reader",
|
||||
side_effect=ValueError("synthetic unsupported sum"),
|
||||
):
|
||||
fallback = _system(
|
||||
high_pressure_helium_step_project(),
|
||||
direct_sum_assignments=True,
|
||||
)
|
||||
|
||||
callback_state = callback.initial_state_vector()
|
||||
fallback_state = fallback.initial_state_vector()
|
||||
for time in (0.0, 0.041, 0.8):
|
||||
self.assertEqual(
|
||||
fallback.rhs(time, fallback_state),
|
||||
callback.rhs(time, callback_state),
|
||||
)
|
||||
self.assertEqual(
|
||||
tuple(
|
||||
unknown.read()
|
||||
for unknown in fallback.pressure_flow_solver.unknowns
|
||||
),
|
||||
tuple(
|
||||
unknown.read()
|
||||
for unknown in callback.pressure_flow_solver.unknowns
|
||||
),
|
||||
)
|
||||
|
||||
diagnostics = (
|
||||
fallback.pressure_flow_solver.causal_execution_diagnostics()
|
||||
)
|
||||
self.assertTrue(diagnostics["directSumAssignmentsConfigured"])
|
||||
self.assertEqual(diagnostics["directSumFlowAssignmentCount"], 0)
|
||||
self.assertTrue(diagnostics["eligible"])
|
||||
self.assertTrue(diagnostics["enabled"])
|
||||
self.assertGreater(diagnostics["fastSolveCount"], 0)
|
||||
self.assertEqual(diagnostics["auditFailureCount"], 0)
|
||||
self.assertEqual(diagnostics["legacyFallbackCount"], 0)
|
||||
|
||||
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)
|
||||
|
||||
@@ -110,6 +110,62 @@ class TestMql8StaticRegressionTests(unittest.TestCase):
|
||||
expected["hasMechanicalStateEvents"],
|
||||
)
|
||||
|
||||
def test_contact_aware_mechanical_tolerance_plan_is_proof_gated(self) -> None:
|
||||
assert self.validation.document is not None
|
||||
system = GenericFluidSystem(
|
||||
compile_system_xml_network(self.validation.document)
|
||||
)
|
||||
reducer = system.mechanical_state_reducer
|
||||
|
||||
plan = reducer.absolute_tolerance_plan(
|
||||
1.0e-8,
|
||||
mode="contact-aware-v1",
|
||||
)
|
||||
diagnostics = plan.as_dict()
|
||||
self.assertEqual(len(plan.values), len(system.initial_state_vector()))
|
||||
self.assertEqual(diagnostics["groupCount"], 10)
|
||||
self.assertEqual(diagnostics["eligibleGroupCount"], 8)
|
||||
self.assertEqual(diagnostics["relaxedVelocityStateCount"], 8)
|
||||
self.assertEqual(diagnostics["relaxedPositionStateCount"], 0)
|
||||
|
||||
by_component = {
|
||||
group.components[0]: group
|
||||
for group in plan.groups
|
||||
}
|
||||
for index in range(1, 9):
|
||||
group = by_component[f"amesim_mecmas21_{index}"]
|
||||
self.assertTrue(group.eligible)
|
||||
self.assertEqual(group.reason, "eligibleFlexibleContact")
|
||||
self.assertAlmostEqual(group.velocity_atol, 1.0e-10)
|
||||
self.assertEqual(group.position_atol, 1.0e-12)
|
||||
self.assertAlmostEqual(
|
||||
group.minimum_damping_strength_ratio,
|
||||
10.0,
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
group.minimum_force_limited_velocity_atol,
|
||||
1.0e-10,
|
||||
)
|
||||
self.assertEqual(
|
||||
group.contacts,
|
||||
(f"amesim_lstp00a_{index}",),
|
||||
)
|
||||
for index in (9, 10):
|
||||
group = by_component[f"amesim_mecmas21_{index}"]
|
||||
self.assertFalse(group.eligible)
|
||||
self.assertEqual(group.reason, "discreteEndstop")
|
||||
self.assertEqual(group.velocity_atol, 1.0e-12)
|
||||
self.assertEqual(group.position_atol, 1.0e-12)
|
||||
|
||||
legacy = reducer.absolute_tolerance_plan(1.0e-8, mode="legacy")
|
||||
self.assertTrue(
|
||||
all(
|
||||
group.velocity_atol == 1.0e-12
|
||||
and group.position_atol == 1.0e-12
|
||||
for group in legacy.groups
|
||||
)
|
||||
)
|
||||
|
||||
def test_signal_event_schedule_is_fixed_for_every_horizon(self) -> None:
|
||||
assert self.validation.document is not None
|
||||
system = GenericFluidSystem(
|
||||
|
||||
Reference in new issue
Block a user