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

同步远端 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

+201 -2
View File
@@ -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