188 lines
7.7 KiB
Python
188 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from app.main import compile_reactflow_network
|
|
from app.simulation.components.experimental.junctions.tee import Tee
|
|
from app.simulation.components.experimental.storage.cylinder import Cylinder
|
|
from app.simulation.components.experimental.storage.tank import Tank
|
|
from app.simulation.core.base import Component
|
|
from app.simulation.core.medium import IdealGasMedium
|
|
from app.simulation.core.ports import PortDefinition, PortState, PortVariableDefinition
|
|
from app.simulation.examples.testmodel.system import TestModelSystem
|
|
from app.simulation.systems.network import SimulationNetwork
|
|
from tests.test_system_xml_protocol import physical_connection_project
|
|
|
|
|
|
class ComponentInterfaceTests(unittest.TestCase):
|
|
def test_pneumatic_port_exposes_equation_roles_and_signed_flow(self) -> None:
|
|
port = PortState.pneumatic("port_a", nominal_role="bidirectional")
|
|
assert port.definition is not None
|
|
|
|
self.assertEqual(
|
|
[
|
|
(variable.name, variable.role, variable.connection_rule)
|
|
for variable in port.definition.variables
|
|
],
|
|
[
|
|
("p", "effort", "equal"),
|
|
("m_flow", "flow", "sumToZero"),
|
|
("h_outflow", "stream", "streamMix"),
|
|
("volume", "signal", "directed"),
|
|
("volume_flow", "signal", "directed"),
|
|
],
|
|
)
|
|
|
|
port.m_flow = 10.0
|
|
self.assertEqual(port.actual_direction(), "in")
|
|
self.assertEqual(port.inflow_rate, 10.0)
|
|
self.assertEqual(port.outflow_rate, 0.0)
|
|
|
|
port.m_flow = -4.0
|
|
self.assertEqual(port.actual_direction(), "out")
|
|
self.assertEqual(port.inflow_rate, 0.0)
|
|
self.assertEqual(port.outflow_rate, 4.0)
|
|
|
|
def test_physical_connection_is_neutral_and_canonical(self) -> None:
|
|
forward = compile_reactflow_network(physical_connection_project())
|
|
reverse = compile_reactflow_network(
|
|
physical_connection_project(reverse_edge=True)
|
|
)
|
|
|
|
self.assertEqual(
|
|
forward.connections[0].undirected_key,
|
|
reverse.connections[0].undirected_key,
|
|
)
|
|
self.assertEqual(
|
|
forward.connections[0].as_interface_dict(),
|
|
reverse.connections[0].as_interface_dict(),
|
|
)
|
|
self.assertNotIn("source", forward.connections[0].as_interface_dict())
|
|
|
|
def test_compiler_instantiates_parameters_and_actual_edges(self) -> None:
|
|
project = physical_connection_project()
|
|
network = compile_reactflow_network(project)
|
|
|
|
self.assertEqual(set(network.components), {"cylinder_1", "tank_1"})
|
|
self.assertEqual(network.components["cylinder_1"].V, 0.01)
|
|
self.assertEqual(network.components["tank_1"].V, 0.1)
|
|
self.assertEqual(len(network.connections), 1)
|
|
self.assertEqual(network.connections[0].id, "edge-1")
|
|
self.assertEqual(network.connections[0].kind, "physical")
|
|
self.assertEqual(network.connections[0].domain, "pneumatic")
|
|
equation_system = network.as_interface_dict()["pressureFlowSystem"]
|
|
self.assertEqual(equation_system["unknownCount"], 4)
|
|
self.assertEqual(equation_system["equationCount"], 4)
|
|
self.assertTrue(equation_system["isSquare"])
|
|
|
|
def test_physical_port_requires_a_tee_for_branching(self) -> None:
|
|
medium = IdealGasMedium()
|
|
network = SimulationNetwork("branch-check")
|
|
network.add_component(Cylinder("cylinder_1", medium))
|
|
network.add_component(Tank("tank_1", medium))
|
|
network.add_component(Tank("tank_2", medium))
|
|
network.connect("cylinder_1", "port_b", "tank_1", "port_a")
|
|
|
|
with self.assertRaisesRegex(ValueError, "Use a junction component"):
|
|
network.connect("cylinder_1", "port_b", "tank_2", "port_a")
|
|
|
|
def test_connection_requires_the_same_variable_contract(self) -> None:
|
|
medium = IdealGasMedium()
|
|
network = SimulationNetwork("contract-check")
|
|
network.add_component(Cylinder("cylinder_1", medium))
|
|
incompatible = Component("incompatible_1")
|
|
incompatible.register_port(
|
|
PortState(
|
|
definition=PortDefinition(
|
|
name="port_a",
|
|
kind="physical",
|
|
domain="pneumatic",
|
|
nominal_role="bidirectional",
|
|
positive_flow_direction="intoComponent",
|
|
variables=(PortVariableDefinition("p", "effort", "equal"),),
|
|
)
|
|
)
|
|
)
|
|
network.add_component(incompatible)
|
|
|
|
with self.assertRaisesRegex(ValueError, "variable contracts do not match"):
|
|
network.connect("cylinder_1", "port_b", "incompatible_1", "port_a")
|
|
|
|
def test_connection_equations_are_executable_and_endpoint_neutral(self) -> None:
|
|
medium = IdealGasMedium()
|
|
network = SimulationNetwork("residual-check")
|
|
cylinder = Cylinder("cylinder_1", medium)
|
|
tank = Tank("tank_1", medium)
|
|
network.add_component(cylinder)
|
|
network.add_component(tank)
|
|
network.connect("tank_1", "port_a", "cylinder_1", "port_b")
|
|
|
|
cylinder.port_b.p = 2.0e5
|
|
cylinder.port_b.m_flow = -3.0
|
|
tank.port_a.p = 2.0e5
|
|
tank.port_a.m_flow = 3.0
|
|
|
|
residuals = {
|
|
residual.variables[0].rsplit(".", 1)[-1]: residual
|
|
for residual in network.connection_equation_residuals()
|
|
}
|
|
self.assertEqual(set(residuals), {"p", "m_flow"})
|
|
self.assertEqual(residuals["p"].relation, "equal")
|
|
self.assertEqual(residuals["m_flow"].relation, "sumToZero")
|
|
self.assertEqual(residuals["p"].value, 0.0)
|
|
self.assertEqual(residuals["m_flow"].value, 0.0)
|
|
|
|
def test_tee_ports_are_physical_and_bidirectional(self) -> None:
|
|
tee = Tee("tee_1")
|
|
self.assertEqual(
|
|
{definition.name for definition in tee.port_definitions},
|
|
{"port_in", "port_out1", "port_out2"},
|
|
)
|
|
self.assertTrue(
|
|
all(
|
|
definition.kind == "physical"
|
|
and definition.nominal_role == "bidirectional"
|
|
for definition in tee.port_definitions
|
|
)
|
|
)
|
|
|
|
def test_existing_testmodel_topology_still_builds(self) -> None:
|
|
system = TestModelSystem()
|
|
self.assertEqual(len(system.network.components), 8)
|
|
self.assertEqual(len(system.network.connections), 8)
|
|
self.assertTrue(
|
|
all(connection.kind == "physical" for connection in system.network.connections)
|
|
)
|
|
self.assertEqual(len(system.initial_state_vector()), 8)
|
|
|
|
def test_testmodel_snapshot_satisfies_acausal_connection_equations(self) -> None:
|
|
system = TestModelSystem()
|
|
state = system.consistent_initial_state_vector()
|
|
system.snapshot(state)
|
|
|
|
residuals = system.network.pressure_flow_equation_residuals()
|
|
self.assertEqual(len(residuals), 4 * len(system.network.connections))
|
|
self.assertEqual(len(system.network.pressure_flow_unknowns()), len(residuals))
|
|
for residual in residuals:
|
|
tolerance = 1e-6
|
|
self.assertLessEqual(
|
|
abs(residual.value),
|
|
tolerance,
|
|
msg=f"{residual.id} residual is {residual.value}",
|
|
)
|
|
|
|
upstream_tee = system.mytee
|
|
self.assertAlmostEqual(
|
|
upstream_tee.port_in.m_flow
|
|
+ upstream_tee.port_out1.m_flow
|
|
+ upstream_tee.port_out2.m_flow,
|
|
0.0,
|
|
places=12,
|
|
)
|
|
self.assertAlmostEqual(upstream_tee.port_in.p, upstream_tee.port_out1.p)
|
|
self.assertAlmostEqual(upstream_tee.port_in.p, upstream_tee.port_out2.p)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|