Files
SystemSimulationApp/tests/test_amesim_gas_registry.py

480 lines
16 KiB
Python

from __future__ import annotations
import unittest
from app.main import (
ReactFlowProjectPayload,
build_reactflow_system_xml,
compile_reactflow_network,
compile_system_xml_network,
)
from app.system_xml import validate_system_xml_document
from app.simulation.components.amesim.gases import (
AMESIM_BUILTIN_AIR_GAS_INDEX,
AMESIM_DEFAULT_GAS_INDEX,
AmesimGasDefinition,
AmesimGasRegistry,
default_amesim_gas_registry,
normalize_amesim_defined_gas_index,
normalize_amesim_gas_index,
)
from app.simulation.components.amesim.media import AmesimIdealAirMedium
from app.simulation.core.medium import IdealGasMedium
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
from tests.test_generic_system_xml_simulation import component_node, physical_edge
from tests.test_system_xml_protocol import physical_port
GAS_INDEXED_MODEL_TYPES = (
"amesim_pnch023",
"amesim_pnch012",
"amesim_pnrp17",
"amesim_pnor001",
"amesim_pnvo001_fixed",
"amesim_pnvo001",
"amesim_pnl00r",
"amesim_pnl0001",
"amesim_pnl0002",
"amesim_pnl0003",
)
def pneumatic_pipe_node(
component_id: str,
*,
gi: float,
) -> dict[str, object]:
return component_node(
component_id,
"amesim_pnl00r",
[
physical_port("port_1", "bidirectional", "left"),
physical_port("port_2", "bidirectional", "right"),
],
{"diam": 0.01, "le": 1.0, "rr": 1.0e-5, "gi": gi},
)
def pneumatic_chamber_node(
component_id: str,
*,
gi: float | None = None,
volume: float = 0.01,
pressure: float = 200000.0,
temperature: float = 300.0,
) -> dict[str, object]:
parameters = {
"cvol": volume,
"kth": 0.0,
"sth": 0.1,
"extemp": 293.15,
"p0": pressure,
"T0": temperature,
}
if gi is not None:
parameters["gi"] = gi
return component_node(
component_id,
"amesim_pnch023",
[
physical_port("port_1", "bidirectional", "left"),
physical_port("port_2", "bidirectional", "right"),
],
parameters,
)
def ideal_air_medium_node(
component_id: str,
*,
gi: float,
property_model: float | None = None,
) -> dict[str, object]:
parameters = {"gi": gi}
if property_model is not None:
parameters["property_model"] = property_model
return component_node(
component_id,
"amesim_ideal_air_medium",
[],
parameters,
)
def two_slot_registry() -> tuple[AmesimGasRegistry, IdealGasMedium, IdealGasMedium]:
future_helium = IdealGasMedium(
name="FutureHeliumStub",
R_gas=2077.26439404998,
cp_ref=5193.0,
viscosity_ref=1.96e-5,
sutherland_constant=79.4,
)
registry = AmesimGasRegistry(
(
AmesimGasDefinition(
gi=2,
label="Future helium",
medium=future_helium,
fluid_type=12,
eos_type=6,
),
)
)
air = registry.resolve(AMESIM_BUILTIN_AIR_GAS_INDEX)
return registry, air, future_helium
class AmesimGasRegistryTests(unittest.TestCase):
def test_all_gas_sensitive_models_expose_the_same_gi_contract(self) -> None:
medium = IdealGasMedium()
for model_type in GAS_INDEXED_MODEL_TYPES:
with self.subTest(model_type=model_type):
spec = COMPONENT_MODEL_REGISTRY[model_type]
parameter = spec.parameter_by_name["gi"]
self.assertIs(spec.parameters[0], parameter)
self.assertEqual(parameter.default, 0.0)
self.assertEqual(parameter.minimum, 0.0)
self.assertEqual(parameter.maximum, 99.0)
self.assertEqual(parameter.editor, "amesimGasReference")
component = spec.create(f"{model_type}_1", medium, {})
self.assertEqual(component.gi, AMESIM_DEFAULT_GAS_INDEX)
with self.assertRaisesRegex(ValueError, "gi must be an integer"):
spec.create(f"{model_type}_fractional", medium, {"gi": 1.5})
def test_default_slot_resolves_to_air(self) -> None:
registry = default_amesim_gas_registry()
medium = registry.resolve(AMESIM_DEFAULT_GAS_INDEX)
self.assertIsInstance(medium, AmesimIdealAirMedium)
self.assertEqual(medium.name, "AMESimAirIdealGas")
self.assertEqual(medium.SUBSTANCE_ID, "air")
self.assertEqual(medium.PROPERTY_METHOD_ID, "ideal_gas")
self.assertEqual(medium.R_gas, 287.0)
self.assertEqual(medium.cp_ref, 1005.0)
def test_gi_normalization_rejects_fractional_and_out_of_range_values(self) -> None:
self.assertEqual(normalize_amesim_gas_index(0.0), 0)
self.assertEqual(normalize_amesim_gas_index(1.0), 1)
with self.assertRaisesRegex(ValueError, "integer"):
normalize_amesim_gas_index(1.5)
with self.assertRaisesRegex(ValueError, "between 0 and 99"):
normalize_amesim_gas_index(-1)
with self.assertRaisesRegex(ValueError, "between 0 and 99"):
normalize_amesim_gas_index(100)
with self.assertRaisesRegex(ValueError, "reserved"):
normalize_amesim_defined_gas_index(0)
self.assertEqual(normalize_amesim_defined_gas_index(1), 1)
def test_unknown_slot_is_not_silently_replaced_with_air(self) -> None:
registry = default_amesim_gas_registry()
with self.assertRaisesRegex(ValueError, "gi=2.*not defined"):
registry.resolve(2, component_name="pipe_1")
project = ReactFlowProjectPayload(
projectSchemaVersion=1,
name="undefined-gas",
nodes=[pneumatic_pipe_node("pipe_1", gi=2.0)],
)
with self.assertRaisesRegex(ValueError, "gi=2.*pipe_1.*not defined"):
compile_reactflow_network(project)
def test_custom_registry_can_define_future_helium_slot(self) -> None:
registry, air, future_helium = two_slot_registry()
self.assertIs(registry.resolve(0), air)
self.assertIs(registry.resolve(2), future_helium)
self.assertEqual(registry.definitions[2].fluid_type, 12)
self.assertEqual(registry.definitions[2].eos_type, 6)
def test_builtin_air_slot_cannot_be_overridden(self) -> None:
registry = default_amesim_gas_registry()
with self.assertRaisesRegex(ValueError, "gi=0.*reserved"):
registry.register(
AmesimGasDefinition(
gi=0,
label="Invalid override",
medium=IdealGasMedium(name="NotBuiltinAir"),
)
)
self.assertEqual(registry.resolve(0).name, "AMESimAirIdealGas")
def test_air_medium_definition_is_a_zero_port_compile_time_component(self) -> None:
spec = COMPONENT_MODEL_REGISTRY["amesim_ideal_air_medium"]
self.assertEqual(spec.display.category_id, "media")
self.assertEqual(spec.display.role, "amesimGasMediumDefinition")
self.assertEqual(spec.display.label, "空气介质定义")
self.assertEqual(spec.ports, ())
self.assertEqual(spec.parameters[0].minimum, 1.0)
property_model = spec.parameter_by_name["property_model"]
self.assertIs(spec.parameters[1], property_model)
self.assertEqual(property_model.default, 0.0)
self.assertEqual(property_model.editor, "amesimGasPropertyModel")
self.assertEqual(
[(option.value, option.label) for option in property_model.options],
[(0, "理想气体")],
)
project = ReactFlowProjectPayload(
projectSchemaVersion=1,
name="project-air-slot",
nodes=[
pneumatic_pipe_node("pipe_1", gi=1.0),
ideal_air_medium_node("air_properties", gi=1.0),
],
)
network = compile_reactflow_network(project)
self.assertNotIn("air_properties", network.components)
self.assertEqual(
network.components["pipe_1"].medium.name,
"AMESimAirIdealGas",
)
def test_air_definition_dispatches_the_selected_property_model(self) -> None:
spec = COMPONENT_MODEL_REGISTRY["amesim_ideal_air_medium"]
definition = spec.create(
"air_properties",
IdealGasMedium(),
{"gi": 3.0, "property_model": 0.0},
)
gas_definition = definition.gas_definition()
self.assertEqual(
definition.parameter_values,
{"gi": 3.0, "property_model": 0.0},
)
self.assertEqual(gas_definition.label, "空气(理想气体)")
self.assertEqual(gas_definition.fluid_type, 2)
self.assertEqual(gas_definition.eos_type, 1)
self.assertIsInstance(gas_definition.medium, AmesimIdealAirMedium)
self.assertEqual(
gas_definition.medium.PROPERTY_METHOD_ID,
"ideal_gas",
)
with self.assertRaisesRegex(ValueError, "property_model.*at most 0"):
spec.create(
"unsupported_air_properties",
IdealGasMedium(),
{"gi": 4.0, "property_model": 1.0},
)
def test_project_medium_definition_may_appear_after_its_references(self) -> None:
project = ReactFlowProjectPayload(
projectSchemaVersion=1,
name="two-phase-medium-resolution",
nodes=[
pneumatic_chamber_node("chamber_1", gi=7.0),
ideal_air_medium_node("air_properties", gi=7.0),
],
)
network = compile_reactflow_network(project)
self.assertEqual(
network.components["chamber_1"].medium.name,
"AMESimAirIdealGas",
)
def test_medium_definition_and_reference_survive_system_xml_round_trip(
self,
) -> None:
original = ReactFlowProjectPayload(
projectSchemaVersion=1,
name="medium-xml-round-trip",
nodes=[
ideal_air_medium_node(
"air_properties",
gi=4.0,
property_model=0.0,
),
pneumatic_pipe_node("pipe_1", gi=4.0),
],
)
xml_bytes = build_reactflow_system_xml(original)
report = validate_system_xml_document(xml_bytes)
self.assertTrue(report.valid, report.issues)
self.assertIsNotNone(report.document)
assert report.document is not None
components = {
component.id: component for component in report.document.components
}
self.assertEqual(
components["air_properties"].model_type,
"amesim_ideal_air_medium",
)
self.assertEqual(components["air_properties"].model_version, "0.2.0")
self.assertEqual(
{item.name: item.value for item in components["air_properties"].parameters},
{"gi": 4.0, "property_model": 0.0},
)
self.assertEqual(
{item.name: item.value for item in components["pipe_1"].parameters}[
"gi"
],
4.0,
)
model_components = {
component["id"]: component
for component in report.document.as_model_data()["components"]
}
self.assertEqual(
model_components["air_properties"]["parameters"],
{"gi": 4.0, "property_model": 0.0},
)
self.assertEqual(model_components["pipe_1"]["parameters"]["gi"], 4.0)
network = compile_system_xml_network(report.document)
self.assertNotIn("air_properties", network.components)
self.assertIsInstance(
network.components["pipe_1"].medium,
AmesimIdealAirMedium,
)
def test_duplicate_project_medium_indices_are_rejected(self) -> None:
project = ReactFlowProjectPayload(
projectSchemaVersion=1,
name="duplicate-medium-index",
nodes=[
ideal_air_medium_node("air_properties_1", gi=1.0),
ideal_air_medium_node("air_properties_2", gi=1.0),
],
)
with self.assertRaisesRegex(
ValueError,
"air_properties_2.*gi=1.*already defined",
):
compile_reactflow_network(project)
def test_medium_definition_cannot_claim_builtin_slot_zero(self) -> None:
project = ReactFlowProjectPayload(
projectSchemaVersion=1,
name="reserved-medium-index",
nodes=[ideal_air_medium_node("air_properties", gi=0.0)],
)
with self.assertRaisesRegex(ValueError, "gi.*at least 1"):
compile_reactflow_network(project)
def test_default_compile_selects_air_without_python_state(self) -> None:
project = ReactFlowProjectPayload(
projectSchemaVersion=1,
name="default-air",
nodes=[pneumatic_chamber_node("chamber_1")],
)
network = compile_reactflow_network(project)
chamber = network.components["chamber_1"]
self.assertEqual(chamber.medium.name, "AMESimAirIdealGas")
self.assertFalse(hasattr(chamber, "state"))
def test_gi_is_resolved_before_native_compilation(self) -> None:
registry, _, future_helium = two_slot_registry()
project = ReactFlowProjectPayload(
projectSchemaVersion=1,
name="selected-medium-initialization",
nodes=[pneumatic_chamber_node("chamber_1", gi=2.0)],
)
network = compile_reactflow_network(
project,
amesim_gas_registry=registry,
)
chamber = network.components["chamber_1"]
self.assertIs(chamber.medium, future_helium)
def test_selected_medium_is_injected_into_the_whole_pneumatic_circuit(
self,
) -> None:
registry, _, future_helium = two_slot_registry()
project = ReactFlowProjectPayload(
projectSchemaVersion=1,
name="future-helium-circuit",
nodes=[
pneumatic_pipe_node("pnl_1", gi=2.0),
component_node(
"tank_1",
"tank",
[physical_port("port_a", "inlet", "left")],
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
),
],
edges=[
physical_edge("edge-1", "pnl_1", "port_2", "tank_1", "port_a"),
],
)
network = compile_reactflow_network(
project,
amesim_gas_registry=registry,
)
self.assertIs(network.components["pnl_1"].medium, future_helium)
self.assertIs(network.components["tank_1"].medium, future_helium)
def test_connected_circuit_rejects_conflicting_gas_indices(self) -> None:
registry, _, _ = two_slot_registry()
project = ReactFlowProjectPayload(
projectSchemaVersion=1,
name="conflicting-gases",
nodes=[
pneumatic_pipe_node("air_pipe", gi=0.0),
pneumatic_pipe_node("helium_pipe", gi=2.0),
],
edges=[
physical_edge(
"edge-1",
"air_pipe",
"port_2",
"helium_pipe",
"port_1",
),
],
)
with self.assertRaisesRegex(
ValueError,
"conflicting AMESim gas definitions",
):
compile_reactflow_network(
project,
amesim_gas_registry=registry,
)
def test_disconnected_circuits_may_use_different_gas_indices(self) -> None:
registry, air, future_helium = two_slot_registry()
project = ReactFlowProjectPayload(
projectSchemaVersion=1,
name="separate-gases",
nodes=[
pneumatic_pipe_node("air_pipe", gi=0.0),
pneumatic_pipe_node("helium_pipe", gi=2.0),
],
)
network = compile_reactflow_network(
project,
amesim_gas_registry=registry,
)
self.assertIs(network.components["air_pipe"].medium, air)
self.assertIs(network.components["helium_pipe"].medium, future_helium)
if __name__ == "__main__":
unittest.main()