完善AMESim组件界面与仿真求解稳定性
This commit is contained in:
1 parent
e7177ab03e
commit
410ef535e8
34 files changed
+3340
-251
No files matched your search
@@ -0,0 +1,256 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import unittest
|
||||
|
||||
from app.main import (
|
||||
ReactFlowProjectPayload,
|
||||
build_reactflow_system_xml,
|
||||
compile_reactflow_network,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from app.system_xml import validate_system_xml_document
|
||||
from tests.test_generic_system_xml_simulation import component_node, physical_edge
|
||||
from tests.test_system_xml_protocol import physical_port
|
||||
|
||||
|
||||
def _signal_port(name: str, role: str, side: str) -> dict[str, str | None]:
|
||||
return {
|
||||
"name": name,
|
||||
"kind": "signal",
|
||||
"domain": "signal",
|
||||
"nominalRole": role,
|
||||
"positiveFlowDirection": None,
|
||||
"side": side,
|
||||
}
|
||||
|
||||
|
||||
def _signal_edge(
|
||||
edge_id: str,
|
||||
source: str,
|
||||
source_port: str,
|
||||
target: str,
|
||||
target_port: str,
|
||||
) -> dict[str, str]:
|
||||
return {
|
||||
"id": edge_id,
|
||||
"source": source,
|
||||
"sourceHandle": source_port,
|
||||
"target": target,
|
||||
"targetHandle": target_port,
|
||||
}
|
||||
|
||||
|
||||
def _plug(component_id: str) -> dict[str, object]:
|
||||
return component_node(
|
||||
component_id,
|
||||
"amesim_pnpl01",
|
||||
[physical_port("port_1", "bidirectional", "left")],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
def helium_step_equalization_project() -> ReactFlowProjectPayload:
|
||||
"""Programmatic form of the user XML that failed at the 3.04 s sample."""
|
||||
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-helium-step-equalization-regression",
|
||||
nodes=[
|
||||
component_node(
|
||||
"amesim_helium_medium_1",
|
||||
"amesim_helium_medium",
|
||||
[],
|
||||
{"gi": 1.0, "property_model": 0.0},
|
||||
),
|
||||
component_node(
|
||||
"amesim_pnch023_1",
|
||||
"amesim_pnch023",
|
||||
[
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
{
|
||||
"gi": 1.0,
|
||||
"cvol": 0.057,
|
||||
"kth": 0.0,
|
||||
"sth": 0.1,
|
||||
"extemp": 293.15,
|
||||
"p0": 15_300_000.0,
|
||||
"T0": 293.15,
|
||||
},
|
||||
),
|
||||
component_node(
|
||||
"amesim_pnch012_1",
|
||||
"amesim_pnch012",
|
||||
[
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
physical_port("port_3", "bidirectional", "left"),
|
||||
physical_port("port_4", "bidirectional", "right"),
|
||||
],
|
||||
{
|
||||
"gi": 1.0,
|
||||
"cvol0": 0.015,
|
||||
"kth": 1500.0,
|
||||
"sth": 0.7,
|
||||
"extemp": 293.15,
|
||||
"p0": 100_000.0,
|
||||
"T0": 293.15,
|
||||
"vol1": 0.0,
|
||||
"vol2": 0.0,
|
||||
"vol3": 0.0,
|
||||
"vol4": 0.0,
|
||||
"dvol1": 0.0,
|
||||
"dvol2": 0.0,
|
||||
"dvol3": 0.0,
|
||||
"dvol4": 0.0,
|
||||
},
|
||||
),
|
||||
component_node(
|
||||
"amesim_pnvo001_1",
|
||||
"amesim_pnvo001",
|
||||
[
|
||||
_signal_port("res", "input", "left"),
|
||||
physical_port("port_2", "bidirectional", "left"),
|
||||
physical_port("port_3", "bidirectional", "right"),
|
||||
],
|
||||
{
|
||||
"gi": 1.0,
|
||||
"cq": 0.45,
|
||||
"area0": 7.85e-5,
|
||||
"Cv": 0.5,
|
||||
"Kv": 0.4,
|
||||
"flowset": 1.0,
|
||||
"opening0": 1.0,
|
||||
},
|
||||
),
|
||||
component_node(
|
||||
"amesim_step0_1",
|
||||
"amesim_step0",
|
||||
[_signal_port("out", "output", "right")],
|
||||
{"initial": 0.0, "final": 1.0, "time": 0.04},
|
||||
),
|
||||
_plug("amesim_pnpl01_1"),
|
||||
_plug("amesim_pnpl01_2"),
|
||||
_plug("amesim_pnpl01_3"),
|
||||
_plug("amesim_pnpl01_4"),
|
||||
],
|
||||
edges=[
|
||||
physical_edge(
|
||||
"edge-high-plug",
|
||||
"amesim_pnpl01_1",
|
||||
"port_1",
|
||||
"amesim_pnch023_1",
|
||||
"port_1",
|
||||
),
|
||||
physical_edge(
|
||||
"edge-high-valve",
|
||||
"amesim_pnch023_1",
|
||||
"port_2",
|
||||
"amesim_pnvo001_1",
|
||||
"port_2",
|
||||
),
|
||||
physical_edge(
|
||||
"edge-valve-low",
|
||||
"amesim_pnvo001_1",
|
||||
"port_3",
|
||||
"amesim_pnch012_1",
|
||||
"port_3",
|
||||
),
|
||||
physical_edge(
|
||||
"edge-low-port-4",
|
||||
"amesim_pnch012_1",
|
||||
"port_4",
|
||||
"amesim_pnpl01_4",
|
||||
"port_1",
|
||||
),
|
||||
physical_edge(
|
||||
"edge-low-port-2",
|
||||
"amesim_pnch012_1",
|
||||
"port_2",
|
||||
"amesim_pnpl01_3",
|
||||
"port_1",
|
||||
),
|
||||
physical_edge(
|
||||
"edge-low-port-1",
|
||||
"amesim_pnpl01_2",
|
||||
"port_1",
|
||||
"amesim_pnch012_1",
|
||||
"port_1",
|
||||
),
|
||||
_signal_edge(
|
||||
"edge-step-valve",
|
||||
"amesim_step0_1",
|
||||
"out",
|
||||
"amesim_pnvo001_1",
|
||||
"res",
|
||||
),
|
||||
],
|
||||
simulation={
|
||||
"t_start": 0.0,
|
||||
"t_stop": 10.0,
|
||||
"step": 0.02,
|
||||
"max_step": 0.002,
|
||||
"method": "BDF",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class AmesimHeliumStepLongRunTests(unittest.TestCase):
|
||||
def test_helium_step_equalization_completes_beyond_3_04_seconds(self) -> None:
|
||||
project = helium_step_equalization_project()
|
||||
network = compile_reactflow_network(project)
|
||||
self.assertEqual(
|
||||
network.components["amesim_pnch023_1"].medium.name,
|
||||
"AMESimHeliumPengRobinson",
|
||||
)
|
||||
self.assertEqual(
|
||||
network.components["amesim_pnch012_1"].medium.name,
|
||||
"AMESimHeliumPengRobinson",
|
||||
)
|
||||
|
||||
xml = build_reactflow_system_xml(project)
|
||||
report = validate_system_xml_document(xml)
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
|
||||
result = run_system_xml_simulation(xml)
|
||||
|
||||
self.assertTrue(result["success"], result["message"])
|
||||
self.assertEqual(result["status"], "completed")
|
||||
self.assertAlmostEqual(result["simulatedUntil"], 10.0)
|
||||
self.assertEqual(result["diagnostics"]["signal"]["eventTimes"], [0.04])
|
||||
self.assertLessEqual(
|
||||
result["diagnostics"]["pressureFlow"]["maxEvaluationsPerSolve"],
|
||||
5,
|
||||
)
|
||||
self.assertLessEqual(
|
||||
result["diagnostics"]["pressureFlow"]["maxScaledResidual"],
|
||||
1.0e-7,
|
||||
)
|
||||
|
||||
series = result["series"]
|
||||
self.assertIn(3.04, series["time"])
|
||||
self.assertTrue(
|
||||
all(
|
||||
math.isfinite(value)
|
||||
for values in series.values()
|
||||
for value in values
|
||||
)
|
||||
)
|
||||
|
||||
high_pressure = series["amesim_pnch023_1.p"]
|
||||
low_pressure = series["amesim_pnch012_1.p"]
|
||||
initial_pressure_gap = abs(high_pressure[0] - low_pressure[0])
|
||||
final_pressure_gap = abs(high_pressure[-1] - low_pressure[-1])
|
||||
self.assertLess(high_pressure[-1], high_pressure[0])
|
||||
self.assertGreater(low_pressure[-1], low_pressure[0])
|
||||
self.assertLess(final_pressure_gap, initial_pressure_gap)
|
||||
self.assertLess(final_pressure_gap, 10.0)
|
||||
self.assertLess(
|
||||
abs(series["amesim_pnvo001_1.port_2.m_flow"][-1]),
|
||||
1.0e-4,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -84,6 +84,126 @@ def amesim_pnvo001_signal_project() -> ReactFlowProjectPayload:
|
||||
)
|
||||
|
||||
|
||||
def high_pressure_helium_step_project() -> ReactFlowProjectPayload:
|
||||
chamber_ports = [
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
]
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-pnvo001-helium-step-regression",
|
||||
nodes=[
|
||||
component_node(
|
||||
"helium_1",
|
||||
"amesim_helium_medium",
|
||||
[],
|
||||
{"gi": 1.0, "property_model": 0.0},
|
||||
),
|
||||
component_node(
|
||||
"step_1",
|
||||
"amesim_step0",
|
||||
[signal_port("out", "output", "right")],
|
||||
{"initial": 0.0, "final": 1.0, "time": 0.04},
|
||||
),
|
||||
component_node(
|
||||
"high_chamber",
|
||||
"amesim_pnch023",
|
||||
chamber_ports,
|
||||
{
|
||||
"gi": 1.0,
|
||||
"cvol": 0.057,
|
||||
"kth": 0.0,
|
||||
"sth": 0.1,
|
||||
"extemp": 293.15,
|
||||
"p0": 15_300_000.0,
|
||||
"T0": 293.15,
|
||||
},
|
||||
),
|
||||
component_node(
|
||||
"low_chamber",
|
||||
"amesim_pnch023",
|
||||
chamber_ports,
|
||||
{
|
||||
"gi": 1.0,
|
||||
"cvol": 0.015,
|
||||
"kth": 1500.0,
|
||||
"sth": 0.7,
|
||||
"extemp": 293.15,
|
||||
"p0": 100_000.0,
|
||||
"T0": 293.15,
|
||||
},
|
||||
),
|
||||
component_node(
|
||||
"valve_1",
|
||||
"amesim_pnvo001",
|
||||
[
|
||||
signal_port("res", "input", "left"),
|
||||
physical_port("port_2", "bidirectional", "left"),
|
||||
physical_port("port_3", "bidirectional", "right"),
|
||||
],
|
||||
{
|
||||
"gi": 1.0,
|
||||
"cq": 0.45,
|
||||
"area0": 7.85e-5,
|
||||
"Cv": 0.5,
|
||||
"Kv": 0.4,
|
||||
"flowset": 1.0,
|
||||
"opening0": 1.0,
|
||||
},
|
||||
),
|
||||
component_node(
|
||||
"high_plug",
|
||||
"amesim_pnpl01",
|
||||
[physical_port("port_1", "bidirectional", "left")],
|
||||
{},
|
||||
),
|
||||
component_node(
|
||||
"low_plug",
|
||||
"amesim_pnpl01",
|
||||
[physical_port("port_1", "bidirectional", "left")],
|
||||
{},
|
||||
),
|
||||
],
|
||||
edges=[
|
||||
signal_edge("signal-1", "step_1", "out", "valve_1", "res"),
|
||||
physical_edge(
|
||||
"edge-high-plug",
|
||||
"high_plug",
|
||||
"port_1",
|
||||
"high_chamber",
|
||||
"port_1",
|
||||
),
|
||||
physical_edge(
|
||||
"edge-high-valve",
|
||||
"high_chamber",
|
||||
"port_2",
|
||||
"valve_1",
|
||||
"port_2",
|
||||
),
|
||||
physical_edge(
|
||||
"edge-valve-low",
|
||||
"valve_1",
|
||||
"port_3",
|
||||
"low_chamber",
|
||||
"port_1",
|
||||
),
|
||||
physical_edge(
|
||||
"edge-low-plug",
|
||||
"low_chamber",
|
||||
"port_2",
|
||||
"low_plug",
|
||||
"port_1",
|
||||
),
|
||||
],
|
||||
simulation={
|
||||
"t_start": 0.0,
|
||||
"t_stop": 0.042,
|
||||
"step": 0.002,
|
||||
"max_step": 0.002,
|
||||
"method": "BDF",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class AmesimPnvo001SignalXmlTests(unittest.TestCase):
|
||||
def test_signal_project_compiles_with_signal_connection(self) -> None:
|
||||
network = compile_reactflow_network(amesim_pnvo001_signal_project())
|
||||
@@ -107,6 +227,26 @@ class AmesimPnvo001SignalXmlTests(unittest.TestCase):
|
||||
self.assertIn("valve_1.xv", result["series"])
|
||||
self.assertGreater(result["diagnostics"]["signal"]["propagations"], 0)
|
||||
|
||||
def test_high_pressure_helium_step_restarts_solver_at_event(self) -> None:
|
||||
xml = build_reactflow_system_xml(high_pressure_helium_step_project())
|
||||
result = run_system_xml_simulation(xml)
|
||||
|
||||
self.assertTrue(result["success"], result["message"])
|
||||
self.assertEqual(result["simulatedUntil"], 0.042)
|
||||
self.assertEqual(result["diagnostics"]["signal"]["eventTimes"], [0.04])
|
||||
|
||||
times = result["series"]["time"]
|
||||
before_event = times.index(0.038)
|
||||
at_event = times.index(0.04)
|
||||
self.assertEqual(result["series"]["step_1.out.signal"][before_event], 0.0)
|
||||
self.assertEqual(result["series"]["step_1.out.signal"][at_event], 1.0)
|
||||
self.assertEqual(result["series"]["valve_1.xv"][before_event], 0.0)
|
||||
self.assertEqual(result["series"]["valve_1.xv"][at_event], 1.0)
|
||||
self.assertGreater(
|
||||
result["series"]["valve_1.port_2.m_flow"][at_event],
|
||||
0.45,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -20,6 +20,8 @@ class AmesimSignalComponentTests(unittest.TestCase):
|
||||
self.assertEqual(step.output_at(0.49), 0.2)
|
||||
self.assertEqual(step.output_at(0.5), 0.8)
|
||||
self.assertEqual(step.signal_output_values(0.5), {"out": 0.8})
|
||||
self.assertEqual(step.signal_event_times(0.0, 1.0), (0.5,))
|
||||
self.assertEqual(step.signal_event_times(0.5, 1.0), ())
|
||||
|
||||
|
||||
def test_ud00_output_interpolates_piecewise_signal(self) -> None:
|
||||
@@ -38,6 +40,7 @@ class AmesimSignalComponentTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(signal.output_at(2.5), 15.0)
|
||||
self.assertAlmostEqual(signal.output_at(5.0), 35.0)
|
||||
self.assertEqual(signal.signal_output_values(2.5), {"out": 15.0})
|
||||
self.assertEqual(signal.signal_event_times(0.0, 5.0), (0.5, 1.5, 3.5))
|
||||
|
||||
def test_ud00_can_cycle_active_stages(self) -> None:
|
||||
signal = AmesimUd00(
|
||||
@@ -53,6 +56,7 @@ class AmesimSignalComponentTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(signal.output_at(0.25), 2.5)
|
||||
self.assertAlmostEqual(signal.output_at(1.25), 12.5)
|
||||
self.assertAlmostEqual(signal.output_at(2.25), 2.5)
|
||||
self.assertEqual(signal.signal_event_times(0.0, 5.0), (1.0, 2.0, 3.0, 4.0))
|
||||
|
||||
def test_ud00_registry_rejects_fractional_stage_controls(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "nstages must be an integer"):
|
||||
@@ -91,6 +95,24 @@ class AmesimSignalComponentTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(step.out.signal, 0.75)
|
||||
self.assertAlmostEqual(valve.res.signal, 0.75)
|
||||
self.assertAlmostEqual(valve.opening, 0.75)
|
||||
self.assertEqual(resolver.event_times(0.0, 0.2), (0.1,))
|
||||
|
||||
def test_signal_resolver_event_times_are_sorted_and_deduplicated(self) -> None:
|
||||
network = SimulationNetwork("signal-events")
|
||||
network.add_component(
|
||||
AmesimStep0("step_2", self.medium, initial=0.0, final=1.0, time=0.2)
|
||||
)
|
||||
network.add_component(
|
||||
AmesimStep0("step_1", self.medium, initial=1.0, final=0.0, time=0.1)
|
||||
)
|
||||
network.add_component(
|
||||
AmesimStep0("step_3", self.medium, initial=0.0, final=1.0, time=0.2)
|
||||
)
|
||||
|
||||
resolver = SignalResolver(network)
|
||||
|
||||
self.assertEqual(resolver.event_times(0.0, 0.3), (0.1, 0.2))
|
||||
self.assertEqual(resolver.event_times(0.2, 0.3), ())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -50,6 +50,9 @@ class ComponentCatalogTests(unittest.TestCase):
|
||||
self.assertEqual(library["label"], "AMESim 组件库")
|
||||
self.assertEqual([category["id"] for category in library["categories"]], ["media", "storage", "flow", "junctions", "boundary", "signals", "mechanical"])
|
||||
self.assertEqual(set(components), {"amesim_ideal_air_medium", "amesim_helium_medium", "amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_lstp00a", "amesim_lmechn1", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"})
|
||||
for model_type, component in components.items():
|
||||
with self.subTest(model_type=model_type):
|
||||
self.assertEqual(component["symbol"], model_type)
|
||||
self.assertEqual(
|
||||
components["amesim_ideal_air_medium"]["role"],
|
||||
"amesimGasMediumDefinition",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import math
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
@@ -78,6 +79,124 @@ class IntegrateOdeTests(unittest.TestCase):
|
||||
self.assertEqual(result.status, "cancelled")
|
||||
self.assertEqual(result.t, [0.0])
|
||||
|
||||
def test_segmented_bdf_uses_left_limit_and_restarts_at_event(self) -> None:
|
||||
import scipy.integrate
|
||||
|
||||
event_time = 0.5
|
||||
actual_bdf = scipy.integrate.BDF
|
||||
starts: list[float] = []
|
||||
bounds: list[float] = []
|
||||
call_times: list[list[float]] = []
|
||||
|
||||
class RecordingBDF(actual_bdf):
|
||||
def __init__(self, fun, t0, y0, t_bound, **kwargs):
|
||||
starts.append(float(t0))
|
||||
bounds.append(float(t_bound))
|
||||
segment_calls: list[float] = []
|
||||
call_times.append(segment_calls)
|
||||
|
||||
def recording_fun(time, state):
|
||||
segment_calls.append(float(time))
|
||||
return fun(time, state)
|
||||
|
||||
super().__init__(recording_fun, t0, y0, t_bound, **kwargs)
|
||||
|
||||
with patch.object(scipy.integrate, "BDF", RecordingBDF):
|
||||
result = integrate_ode(
|
||||
rhs=lambda time, _state: [1.0 if time < event_time else 2.0],
|
||||
initial_state=[0.0],
|
||||
config=SolveIVPConfig(
|
||||
t_start=0.0,
|
||||
t_stop=1.0,
|
||||
method="BDF",
|
||||
max_step=0.1,
|
||||
first_step=0.8,
|
||||
),
|
||||
t_eval=[0.0, event_time, event_time, 1.0],
|
||||
breakpoints=[event_time],
|
||||
)
|
||||
|
||||
self.assertTrue(result.success, result.message)
|
||||
self.assertEqual(starts, [0.0, event_time])
|
||||
self.assertEqual(bounds[0], math.nextafter(event_time, -math.inf))
|
||||
self.assertEqual(bounds[1], 1.0)
|
||||
self.assertTrue(call_times[0])
|
||||
self.assertTrue(all(time < event_time for time in call_times[0]))
|
||||
self.assertTrue(any(time >= event_time for time in call_times[1]))
|
||||
self.assertEqual(result.t, [0.0, event_time, 1.0])
|
||||
self.assertAlmostEqual(result.y[0][-1], 1.5, places=5)
|
||||
|
||||
def test_segmented_implicit_solvers_merge_samples_and_report_progress(self) -> None:
|
||||
event_time = 0.4
|
||||
|
||||
for method in ("BDF", "Radau"):
|
||||
with self.subTest(method=method):
|
||||
callback_times: list[float] = []
|
||||
result = integrate_ode(
|
||||
rhs=lambda time, _state: [1.0 if time < event_time else 3.0],
|
||||
initial_state=[0.0],
|
||||
config=SolveIVPConfig(
|
||||
t_start=0.0,
|
||||
t_stop=1.0,
|
||||
method=method,
|
||||
max_step=0.05,
|
||||
first_step=0.9,
|
||||
),
|
||||
t_eval=[0.0, event_time, event_time, 0.7, 1.0],
|
||||
accepted_step_callback=callback_times.append,
|
||||
breakpoints=[event_time, event_time],
|
||||
)
|
||||
|
||||
self.assertTrue(result.success, result.message)
|
||||
self.assertEqual(result.t, [0.0, event_time, 0.7, 1.0])
|
||||
self.assertAlmostEqual(result.y[0][-1], 2.2, places=5)
|
||||
self.assertEqual(callback_times.count(event_time), 1)
|
||||
self.assertTrue(
|
||||
all(
|
||||
earlier < later
|
||||
for earlier, later in zip(
|
||||
callback_times,
|
||||
callback_times[1:],
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def test_segmented_solver_can_cancel_after_crossing_a_breakpoint(self) -> None:
|
||||
callback_times: list[float] = []
|
||||
cancellation_requested = False
|
||||
|
||||
def record_progress(time: float) -> None:
|
||||
nonlocal cancellation_requested
|
||||
callback_times.append(time)
|
||||
cancellation_requested = time >= 0.55
|
||||
|
||||
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=0.05,
|
||||
),
|
||||
t_eval=[0.0, 0.2, 0.4, 0.6, 0.8, 1.0],
|
||||
cancel_check=lambda: cancellation_requested,
|
||||
accepted_step_callback=record_progress,
|
||||
breakpoints=[0.4, 0.8],
|
||||
)
|
||||
|
||||
self.assertFalse(result.success)
|
||||
self.assertEqual(result.status, "cancelled")
|
||||
self.assertIn(0.4, callback_times)
|
||||
self.assertGreater(callback_times[-1], 0.4)
|
||||
self.assertTrue(
|
||||
all(
|
||||
earlier < later
|
||||
for earlier, later in zip(callback_times, callback_times[1:])
|
||||
)
|
||||
)
|
||||
self.assertEqual(result.t, sorted(set(result.t)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,198 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.simulation.components.amesim.boundary.sources import AmesimPnpl01
|
||||
from app.simulation.components.amesim.flow.orifices import (
|
||||
AmesimPnvo001SignalOpening,
|
||||
)
|
||||
from app.simulation.components.amesim.media.mediums import (
|
||||
AmesimHeliumPengRobinsonMedium,
|
||||
)
|
||||
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.core.state import VolumeState
|
||||
from app.simulation.solvers.algebraic import AlgebraicSolveError, PressureFlowSolver
|
||||
from app.simulation.systems.network import SimulationNetwork
|
||||
|
||||
|
||||
class PressureFlowSolverInitializationTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def _near_equal_pressure_network() -> tuple[
|
||||
SimulationNetwork,
|
||||
AmesimHeliumPengRobinsonMedium,
|
||||
Cylinder,
|
||||
Tank,
|
||||
AmesimPnvo001SignalOpening,
|
||||
]:
|
||||
medium = AmesimHeliumPengRobinsonMedium()
|
||||
high = Cylinder("high", medium, V=0.057, p0=1.0e5, T0=256.1)
|
||||
low = Tank("low", medium, V=0.015, p0=1.0e5, T0=298.2)
|
||||
valve = AmesimPnvo001SignalOpening(
|
||||
"valve",
|
||||
medium,
|
||||
cq=0.45,
|
||||
area0=7.85e-5,
|
||||
gi=1.0,
|
||||
flowset=1.0,
|
||||
opening0=1.0,
|
||||
)
|
||||
valve.res.signal = 1.0
|
||||
|
||||
network = SimulationNetwork("near-equal-pressure")
|
||||
for component in (high, low, valve):
|
||||
network.add_component(component)
|
||||
network.connect("high", "port_b", "valve", "port_2")
|
||||
network.connect("valve", "port_3", "low", "port_a")
|
||||
return network, medium, high, low, valve
|
||||
|
||||
@staticmethod
|
||||
def _set_pressure_temperature(
|
||||
component: Cylinder | Tank,
|
||||
medium: AmesimHeliumPengRobinsonMedium,
|
||||
pressure: float,
|
||||
temperature: float,
|
||||
) -> None:
|
||||
mass = medium.density(pressure, temperature) * component.V
|
||||
component.state = VolumeState(
|
||||
m=mass,
|
||||
U=mass * medium.specific_internal_energy(temperature),
|
||||
)
|
||||
component.refresh_thermodynamic_ports()
|
||||
|
||||
def test_current_storage_pressure_reseeds_stale_orifice_ports_and_flow(self) -> None:
|
||||
network, medium, high, low, valve = self._near_equal_pressure_network()
|
||||
solver = PressureFlowSolver(network, max_evaluations=10)
|
||||
high_pressure = 10_790_000.0
|
||||
|
||||
self._set_pressure_temperature(high, medium, high_pressure, 256.1)
|
||||
self._set_pressure_temperature(low, medium, high_pressure - 300.0, 298.2)
|
||||
initial = solver.solve()
|
||||
self.assertTrue(initial.success)
|
||||
|
||||
stale_low_pressure = valve.port_3.p
|
||||
stale_flow = valve.port_2.m_flow
|
||||
self._set_pressure_temperature(low, medium, high_pressure - 100.0, 298.2)
|
||||
self.assertNotAlmostEqual(low.port_a.p, stale_low_pressure, places=3)
|
||||
|
||||
updated = solver.solve()
|
||||
|
||||
self.assertTrue(updated.success)
|
||||
self.assertEqual(updated.evaluations, 0)
|
||||
self.assertAlmostEqual(valve.port_2.p, high.port_b.p, places=6)
|
||||
self.assertAlmostEqual(valve.port_3.p, low.port_a.p, places=6)
|
||||
self.assertNotAlmostEqual(valve.port_2.m_flow, stale_flow, places=8)
|
||||
self.assertAlmostEqual(
|
||||
valve.port_2.m_flow,
|
||||
valve.mass_flow(valve.port_2.p, valve.port_3.p),
|
||||
places=10,
|
||||
)
|
||||
self.assertAlmostEqual(high.port_b.m_flow, -valve.port_2.m_flow, places=10)
|
||||
self.assertAlmostEqual(low.port_a.m_flow, -valve.port_3.m_flow, places=10)
|
||||
|
||||
@staticmethod
|
||||
def _closed_boundary_solver() -> PressureFlowSolver:
|
||||
boundary = AmesimPnpl01("closed")
|
||||
boundary.port_1.p = 100_000.0
|
||||
boundary.port_1.m_flow = 1.0
|
||||
network = SimulationNetwork("closed-boundary")
|
||||
network.add_component(boundary)
|
||||
return PressureFlowSolver(network)
|
||||
|
||||
@staticmethod
|
||||
def _least_squares_result(x, *, status: int):
|
||||
return SimpleNamespace(
|
||||
x=x,
|
||||
success=status > 0,
|
||||
status=status,
|
||||
message="test optimizer result",
|
||||
nfev=1,
|
||||
)
|
||||
|
||||
def test_status_zero_is_accepted_only_for_finite_converged_residuals(self) -> None:
|
||||
exact_solver = self._closed_boundary_solver()
|
||||
|
||||
def exact_status_zero(_fun, x0, **_kwargs):
|
||||
values = x0.copy()
|
||||
flow_index = next(
|
||||
index
|
||||
for index, unknown in enumerate(exact_solver.unknowns)
|
||||
if unknown.variable == "m_flow"
|
||||
)
|
||||
values[flow_index] = 0.0
|
||||
return self._least_squares_result(values, status=0)
|
||||
|
||||
with patch.object(
|
||||
PressureFlowSolver,
|
||||
"_seed_explicit_mass_flows",
|
||||
return_value=None,
|
||||
), patch("scipy.optimize.least_squares", side_effect=exact_status_zero):
|
||||
diagnostics = exact_solver.solve()
|
||||
self.assertTrue(diagnostics.success)
|
||||
self.assertEqual(diagnostics.max_scaled_residual, 0.0)
|
||||
|
||||
inaccurate_solver = self._closed_boundary_solver()
|
||||
|
||||
def inaccurate_status_zero(_fun, x0, **_kwargs):
|
||||
values = x0.copy()
|
||||
flow_index = next(
|
||||
index
|
||||
for index, unknown in enumerate(inaccurate_solver.unknowns)
|
||||
if unknown.variable == "m_flow"
|
||||
)
|
||||
values[flow_index] = 1.0
|
||||
return self._least_squares_result(values, status=0)
|
||||
|
||||
with patch.object(
|
||||
PressureFlowSolver,
|
||||
"_seed_explicit_mass_flows",
|
||||
return_value=None,
|
||||
), patch("scipy.optimize.least_squares", side_effect=inaccurate_status_zero):
|
||||
with self.assertRaises(AlgebraicSolveError):
|
||||
inaccurate_solver.solve()
|
||||
|
||||
invalid_status_solver = self._closed_boundary_solver()
|
||||
|
||||
def exact_invalid_status(_fun, x0, **_kwargs):
|
||||
values = x0.copy()
|
||||
flow_index = next(
|
||||
index
|
||||
for index, unknown in enumerate(invalid_status_solver.unknowns)
|
||||
if unknown.variable == "m_flow"
|
||||
)
|
||||
values[flow_index] = 0.0
|
||||
return self._least_squares_result(values, status=-1)
|
||||
|
||||
with patch.object(
|
||||
PressureFlowSolver,
|
||||
"_seed_explicit_mass_flows",
|
||||
return_value=None,
|
||||
), patch("scipy.optimize.least_squares", side_effect=exact_invalid_status):
|
||||
with self.assertRaises(AlgebraicSolveError):
|
||||
invalid_status_solver.solve()
|
||||
|
||||
def test_zero_residual_seed_does_not_bypass_positive_pressure_bound(self) -> None:
|
||||
medium = IdealGasMedium()
|
||||
tank = Tank("tank", medium, V=1.0)
|
||||
plug = AmesimPnpl01("plug")
|
||||
network = SimulationNetwork("invalid-negative-pressure")
|
||||
network.add_component(tank)
|
||||
network.add_component(plug)
|
||||
network.connect("tank", "port_a", "plug", "port_1")
|
||||
|
||||
tank.state = VolumeState(m=1.0, U=-1000.0)
|
||||
tank.refresh_thermodynamic_ports()
|
||||
self.assertLess(tank.port_a.p, 0.0)
|
||||
|
||||
solver = PressureFlowSolver(network, max_evaluations=10)
|
||||
with self.assertRaises(AlgebraicSolveError):
|
||||
solver.solve()
|
||||
self.assertIsNotNone(solver.last_diagnostics)
|
||||
self.assertFalse(solver.last_diagnostics.success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user