457 lines
14 KiB
Python
457 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from app.simulation.components.experimental.library import LIBRARY
|
|
from app.simulation.core.base import AlgebraicComponent
|
|
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
|
from app.simulation.core.metadata import ParameterDefinition
|
|
from app.simulation.core.ports import PortDefinition
|
|
from app.simulation.registry import (
|
|
COMPONENT_MODEL_REGISTRY,
|
|
ComponentModelSpec,
|
|
validate_component_model_class,
|
|
)
|
|
from app.system_xml import validate_system_xml_document
|
|
|
|
|
|
GAS_REFERENCE_PARAMETER = ParameterDefinition(
|
|
"gi",
|
|
0.0,
|
|
label="气体类型索引",
|
|
minimum=0.0,
|
|
maximum=99.0,
|
|
editor="amesimGasReference",
|
|
)
|
|
|
|
|
|
class GasReferenceFixture(AlgebraicComponent):
|
|
MODEL_TYPE = "gas_reference_fixture"
|
|
MODEL_VERSION = "1.0.0"
|
|
PORTS = ()
|
|
PARAMETERS = (GAS_REFERENCE_PARAMETER,)
|
|
RESULT_VARIABLES = ()
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="气体引用测试组件",
|
|
library_id="experimental",
|
|
category_id="flow",
|
|
symbol="generic",
|
|
ports=(),
|
|
)
|
|
|
|
def __init__(self, name: str, gi: float) -> None:
|
|
super().__init__(name)
|
|
self.set_parameter_values({"gi": gi})
|
|
|
|
@classmethod
|
|
def create(cls, *, name, medium, parameters):
|
|
del medium
|
|
return cls(name, parameters["gi"])
|
|
|
|
|
|
class GasMediumDefinitionFixture(AlgebraicComponent):
|
|
MODEL_TYPE = "gas_medium_definition_fixture"
|
|
MODEL_VERSION = "1.0.0"
|
|
PORTS = ()
|
|
PARAMETERS = (
|
|
ParameterDefinition(
|
|
"gi",
|
|
1.0,
|
|
label="介质索引",
|
|
minimum=1.0,
|
|
maximum=99.0,
|
|
),
|
|
)
|
|
RESULT_VARIABLES = ()
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="气体介质定义测试组件",
|
|
library_id="experimental",
|
|
category_id="flow",
|
|
symbol="generic",
|
|
ports=(),
|
|
role="amesimGasMediumDefinition",
|
|
)
|
|
|
|
def __init__(self, name: str, gi: float) -> None:
|
|
super().__init__(name)
|
|
self.set_parameter_values({"gi": gi})
|
|
|
|
@classmethod
|
|
def create(cls, *, name, medium, parameters):
|
|
del medium
|
|
return cls(name, parameters["gi"])
|
|
|
|
|
|
class ConnectedGasReferenceFixture(AlgebraicComponent):
|
|
MODEL_TYPE = "connected_gas_reference_fixture"
|
|
MODEL_VERSION = "1.0.0"
|
|
PORTS = (
|
|
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
|
)
|
|
PARAMETERS = (GAS_REFERENCE_PARAMETER,)
|
|
RESULT_VARIABLES = ()
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="相连气体引用测试组件",
|
|
library_id="experimental",
|
|
category_id="flow",
|
|
symbol="generic",
|
|
ports=(PortDisplaySpec("port_1", "right"),),
|
|
)
|
|
|
|
def __init__(self, name: str, gi: float) -> None:
|
|
super().__init__(name)
|
|
self.register_declared_port("port_1")
|
|
self.set_parameter_values({"gi": gi})
|
|
|
|
@classmethod
|
|
def create(cls, *, name, medium, parameters):
|
|
del medium
|
|
return cls(name, parameters["gi"])
|
|
|
|
|
|
GAS_REFERENCE_SPEC = ComponentModelSpec(
|
|
component_class=GasReferenceFixture,
|
|
library=LIBRARY,
|
|
)
|
|
GAS_MEDIUM_DEFINITION_SPEC = ComponentModelSpec(
|
|
component_class=GasMediumDefinitionFixture,
|
|
library=LIBRARY,
|
|
)
|
|
CONNECTED_GAS_REFERENCE_SPEC = ComponentModelSpec(
|
|
component_class=ConnectedGasReferenceFixture,
|
|
library=LIBRARY,
|
|
)
|
|
|
|
|
|
def system_xml(
|
|
*,
|
|
gi: float | None,
|
|
medium_gas_indices: tuple[float, ...] = (),
|
|
) -> bytes:
|
|
reference_parameter = (
|
|
f'<Parameter name="gi" value="{gi}"/>' if gi is not None else ""
|
|
)
|
|
medium_components = "".join(
|
|
f"""
|
|
<Component id="medium_{index}" type="gas_medium_definition_fixture"
|
|
modelVersion="1.0.0">
|
|
<Parameter name="gi" value="{gas_index}"/>
|
|
</Component>"""
|
|
for index, gas_index in enumerate(medium_gas_indices, start=1)
|
|
)
|
|
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
<System name="medium-contract" schemaVersion="3" unitSystem="SI">
|
|
<Simulation tStart="0" tStop="1" sampleStep="0.1" maxStep="0.01" method="BDF"/>
|
|
<Components>
|
|
<Component id="consumer_1" type="gas_reference_fixture"
|
|
modelVersion="1.0.0">
|
|
{reference_parameter}
|
|
</Component>{medium_components}
|
|
</Components>
|
|
<Connections/>
|
|
</System>
|
|
""".encode()
|
|
|
|
|
|
def connected_system_xml(
|
|
*,
|
|
first_gi: float,
|
|
second_gi: float,
|
|
medium_gas_indices: tuple[float, ...],
|
|
) -> bytes:
|
|
medium_components = "".join(
|
|
f"""
|
|
<Component id="medium_{index}" type="gas_medium_definition_fixture"
|
|
modelVersion="1.0.0">
|
|
<Parameter name="gi" value="{gas_index}"/>
|
|
</Component>"""
|
|
for index, gas_index in enumerate(medium_gas_indices, start=1)
|
|
)
|
|
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
<System name="connected-medium-contract" schemaVersion="3" unitSystem="SI">
|
|
<Simulation tStart="0" tStop="1" sampleStep="0.1" maxStep="0.01" method="BDF"/>
|
|
<Components>
|
|
<Component id="consumer_1" type="connected_gas_reference_fixture"
|
|
modelVersion="1.0.0">
|
|
<Parameter name="gi" value="{first_gi}"/>
|
|
</Component>
|
|
<Component id="consumer_2" type="connected_gas_reference_fixture"
|
|
modelVersion="1.0.0">
|
|
<Parameter name="gi" value="{second_gi}"/>
|
|
</Component>{medium_components}
|
|
</Components>
|
|
<Connections>
|
|
<Connection id="gas_connection">
|
|
<Endpoint component="consumer_1" port="port_1"/>
|
|
<Endpoint component="consumer_2" port="port_1"/>
|
|
</Connection>
|
|
</Connections>
|
|
</System>
|
|
""".encode()
|
|
|
|
|
|
def air_medium_system_xml(*, property_model: float | None) -> bytes:
|
|
property_parameter = (
|
|
f'<Parameter name="property_model" value="{property_model}"/>'
|
|
if property_model is not None
|
|
else ""
|
|
)
|
|
return f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
<System name="air-medium-contract" schemaVersion="3" unitSystem="SI">
|
|
<Simulation tStart="0" tStop="1" sampleStep="0.1" maxStep="0.01" method="BDF"/>
|
|
<Components>
|
|
<Component id="air_1" type="amesim_ideal_air_medium"
|
|
modelVersion="0.2.0">
|
|
<Parameter name="gi" value="1"/>
|
|
{property_parameter}
|
|
</Component>
|
|
</Components>
|
|
<Connections/>
|
|
</System>
|
|
""".encode()
|
|
|
|
|
|
class MediumReferenceCatalogContractTests(unittest.TestCase):
|
|
def test_registry_emits_parameter_editor_and_component_role(self) -> None:
|
|
validate_component_model_class(GasReferenceFixture, library=LIBRARY)
|
|
validate_component_model_class(GasMediumDefinitionFixture, library=LIBRARY)
|
|
|
|
reference_payload = GAS_REFERENCE_SPEC.as_catalog_dict()
|
|
definition_payload = GAS_MEDIUM_DEFINITION_SPEC.as_catalog_dict()
|
|
|
|
self.assertEqual(
|
|
reference_payload["parameters"][0]["editor"],
|
|
"amesimGasReference",
|
|
)
|
|
self.assertEqual(
|
|
definition_payload["role"],
|
|
"amesimGasMediumDefinition",
|
|
)
|
|
|
|
def test_committed_catalog_schema_constrains_the_extension_values(self) -> None:
|
|
schema_path = (
|
|
Path(__file__).resolve().parent.parent
|
|
/ "schemas"
|
|
/ "component-catalog-v1.schema.json"
|
|
)
|
|
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
|
|
self.assertEqual(
|
|
schema["$defs"]["parameter"]["properties"]["editor"]["enum"],
|
|
["amesimGasReference", "amesimGasPropertyModel", "choice"],
|
|
)
|
|
self.assertEqual(
|
|
schema["$defs"]["parameter"]["properties"]["options"]["items"][
|
|
"$ref"
|
|
],
|
|
"#/$defs/parameterOption",
|
|
)
|
|
self.assertEqual(
|
|
schema["$defs"]["component"]["properties"]["role"]["enum"],
|
|
["amesimGasMediumDefinition"],
|
|
)
|
|
|
|
|
|
class MediumReferenceSystemXmlContractTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.registry_patch = patch.dict(
|
|
COMPONENT_MODEL_REGISTRY,
|
|
{
|
|
GasReferenceFixture.MODEL_TYPE: GAS_REFERENCE_SPEC,
|
|
GasMediumDefinitionFixture.MODEL_TYPE: GAS_MEDIUM_DEFINITION_SPEC,
|
|
ConnectedGasReferenceFixture.MODEL_TYPE: CONNECTED_GAS_REFERENCE_SPEC,
|
|
},
|
|
)
|
|
self.registry_patch.start()
|
|
self.addCleanup(self.registry_patch.stop)
|
|
|
|
def test_positive_reference_without_definition_is_not_reinterpreted(self) -> None:
|
|
report = validate_system_xml_document(system_xml(gi=1.0))
|
|
|
|
self.assertFalse(report.valid)
|
|
self.assertEqual(
|
|
{issue.code for issue in report.issues},
|
|
{"AMESIM_GAS_REFERENCE_UNDEFINED"},
|
|
)
|
|
assert report.document is not None
|
|
self.assertEqual(
|
|
report.document.as_model_data()["components"][0]["parameters"]["gi"],
|
|
1.0,
|
|
)
|
|
|
|
def test_missing_gi_is_not_defaulted(self) -> None:
|
|
report = validate_system_xml_document(system_xml(gi=None))
|
|
|
|
self.assertFalse(report.valid)
|
|
self.assertEqual(
|
|
{issue.code for issue in report.issues},
|
|
{"PARAMETER_REQUIRED_MISSING"},
|
|
)
|
|
|
|
def test_builtin_air_reference_zero_is_valid_canonical_data(self) -> None:
|
|
report = validate_system_xml_document(system_xml(gi=0.0))
|
|
|
|
self.assertTrue(report.valid)
|
|
self.assertEqual(report.issues, ())
|
|
assert report.document is not None
|
|
self.assertEqual(
|
|
report.document.as_model_data()["components"][0]["parameters"]["gi"],
|
|
0.0,
|
|
)
|
|
|
|
def test_positive_reference_with_definition_is_preserved(self) -> None:
|
|
report = validate_system_xml_document(
|
|
system_xml(gi=1.0, medium_gas_indices=(1.0,))
|
|
)
|
|
|
|
self.assertTrue(report.valid)
|
|
self.assertEqual(report.issues, ())
|
|
assert report.document is not None
|
|
self.assertEqual(
|
|
report.document.as_model_data()["components"][0]["parameters"]["gi"],
|
|
1.0,
|
|
)
|
|
|
|
def test_positive_reference_accepts_matching_definition(self) -> None:
|
|
report = validate_system_xml_document(
|
|
system_xml(
|
|
gi=2.0,
|
|
medium_gas_indices=(2.0,),
|
|
)
|
|
)
|
|
|
|
self.assertTrue(report.valid)
|
|
self.assertEqual(report.issues, ())
|
|
|
|
def test_missing_property_model_is_not_defaulted(self) -> None:
|
|
report = validate_system_xml_document(
|
|
air_medium_system_xml(property_model=None)
|
|
)
|
|
|
|
self.assertFalse(report.valid)
|
|
self.assertEqual(
|
|
{issue.code for issue in report.issues},
|
|
{"PARAMETER_REQUIRED_MISSING"},
|
|
)
|
|
|
|
def test_explicit_supported_property_model_round_trips_without_warning(self) -> None:
|
|
report = validate_system_xml_document(
|
|
air_medium_system_xml(property_model=0.0)
|
|
)
|
|
|
|
self.assertTrue(report.valid)
|
|
self.assertEqual(report.issues, ())
|
|
|
|
def test_unsupported_property_model_is_rejected_semantically(self) -> None:
|
|
for property_model in (1.0, 99.0):
|
|
with self.subTest(property_model=property_model):
|
|
report = validate_system_xml_document(
|
|
air_medium_system_xml(property_model=property_model)
|
|
)
|
|
|
|
self.assertFalse(report.valid)
|
|
self.assertEqual(
|
|
{issue.code for issue in report.issues},
|
|
{"PARAMETER_VALUE_INVALID"},
|
|
)
|
|
|
|
def test_medium_definition_index_must_be_integer_in_range(self) -> None:
|
|
fractional = validate_system_xml_document(
|
|
system_xml(
|
|
gi=0.0,
|
|
medium_gas_indices=(1.5,),
|
|
)
|
|
)
|
|
zero = validate_system_xml_document(
|
|
system_xml(
|
|
gi=0.0,
|
|
medium_gas_indices=(0.0,),
|
|
)
|
|
)
|
|
|
|
self.assertIn(
|
|
"AMESIM_GAS_MEDIUM_INDEX_INVALID",
|
|
{issue.code for issue in fractional.issues},
|
|
)
|
|
self.assertIn(
|
|
"AMESIM_GAS_MEDIUM_INDEX_INVALID",
|
|
{issue.code for issue in zero.issues},
|
|
)
|
|
|
|
def test_medium_definition_indices_must_be_unique(self) -> None:
|
|
report = validate_system_xml_document(
|
|
system_xml(
|
|
gi=1.0,
|
|
medium_gas_indices=(1.0, 1.0),
|
|
)
|
|
)
|
|
|
|
self.assertFalse(report.valid)
|
|
self.assertIn(
|
|
"AMESIM_GAS_MEDIUM_INDEX_DUPLICATE",
|
|
{issue.code for issue in report.issues},
|
|
)
|
|
|
|
def test_gas_reference_index_must_be_integer_in_range(self) -> None:
|
|
fractional = validate_system_xml_document(
|
|
system_xml(gi=1.5)
|
|
)
|
|
out_of_range = validate_system_xml_document(
|
|
system_xml(gi=100.0)
|
|
)
|
|
|
|
self.assertIn(
|
|
"AMESIM_GAS_REFERENCE_INDEX_INVALID",
|
|
{issue.code for issue in fractional.issues},
|
|
)
|
|
self.assertIn(
|
|
"AMESIM_GAS_REFERENCE_INDEX_INVALID",
|
|
{issue.code for issue in out_of_range.issues},
|
|
)
|
|
|
|
def test_connected_pneumatic_references_cannot_conflict(self) -> None:
|
|
report = validate_system_xml_document(
|
|
connected_system_xml(
|
|
first_gi=1.0,
|
|
second_gi=2.0,
|
|
medium_gas_indices=(1.0, 2.0),
|
|
)
|
|
)
|
|
|
|
self.assertFalse(report.valid)
|
|
self.assertEqual(
|
|
{issue.code for issue in report.issues},
|
|
{"AMESIM_GAS_REFERENCE_CONFLICT"},
|
|
)
|
|
|
|
def test_connected_pneumatic_references_may_share_one_medium(self) -> None:
|
|
report = validate_system_xml_document(
|
|
connected_system_xml(
|
|
first_gi=1.0,
|
|
second_gi=1.0,
|
|
medium_gas_indices=(1.0,),
|
|
)
|
|
)
|
|
|
|
self.assertTrue(report.valid)
|
|
self.assertEqual(report.issues, ())
|
|
|
|
def test_removed_medium_reference_version_is_rejected_by_xsd(self) -> None:
|
|
removed_marker_xml = system_xml(gi=0.0).replace(
|
|
b'<System name="medium-contract"',
|
|
b'<System name="medium-contract" mediumReferenceVersion="1"',
|
|
1,
|
|
)
|
|
report = validate_system_xml_document(removed_marker_xml)
|
|
|
|
self.assertFalse(report.valid)
|
|
self.assertIsNone(report.document)
|
|
self.assertEqual({issue.code for issue in report.issues}, {"XSD_VALIDATION_ERROR"})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|