整合求解器活动监控与步长回归证据

同步远端 PNL0003 诊断和大采样网格能力,语义合并活动感知的 60 秒真停滞判定与旧后端 15 分钟兼容兜底。

纳管热路径优化、15 单元运行证据、浏览器与 API 报告,并补充北京时间更新日志和遗留问题。
This commit is contained in:
lujingze committed 2026-08-19 16:24:31 +00:00
1 parent c19cf77aee
commit e18399c022
46 files changed
+181589 -170

No files matched your search

@@ -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)