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

- 引入因果坐标内核、热流体恢复和递进长时回归\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

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