Files
SystemSimulationApp/tests/test_component_registry.py
T

654 lines
24 KiB
Python

from __future__ import annotations
import unittest
from app.simulation.components.experimental.library import LIBRARY
from app.simulation.components.amesim.library import LIBRARY as AMESIM_LIBRARY
from app.simulation.core.base import AlgebraicComponent
from app.simulation.core.catalog import (
ComponentDisplaySpec,
ParameterGroupDisplaySpec,
PortDisplaySpec,
)
from app.simulation.core.metadata import (
ParameterCondition,
ParameterDefinition,
ParameterOption,
)
from app.simulation.core.medium import IdealGasMedium
from app.simulation.core.ports import PortDefinition
from app.simulation.registry import (
COMPONENT_LIBRARY_REGISTRY,
COMPONENT_MODEL_REGISTRY,
discover_component_registries,
validate_component_model_class,
)
class ComponentRegistryTests(unittest.TestCase):
def test_manifest_is_the_only_public_model_source(self) -> None:
libraries, models = discover_component_registries()
self.assertEqual(libraries, COMPONENT_LIBRARY_REGISTRY)
self.assertEqual(models, COMPONENT_MODEL_REGISTRY)
self.assertEqual(
tuple(models),
(
"cylinder",
"tank",
"pipe",
"orifice",
"tee",
"amesim_ideal_air_medium",
"amesim_helium_medium",
"amesim_pnpl01",
"amesim_step0",
"amesim_ud00",
"amesim_f000",
"amesim_forc",
"amesim_mecmas21",
"amesim_lstp00a",
"amesim_lmechn1",
"amesim_pnrp17",
"amesim_pnch023",
"amesim_pnch012",
"amesim_pnor001",
"amesim_pnvo001_fixed",
"amesim_pnvo001",
"amesim_pnl00r",
"amesim_pnl0001",
"amesim_pnl0002",
"amesim_pnl0003",
"amesim_pn3node2",
"amesim_p4node2",
),
)
self.assertNotIn(
"app.simulation.components.experimental.flow.pipe:Pipe",
LIBRARY.models,
)
def test_amesim_library_is_enabled_as_separate_public_boundary(self) -> None:
libraries, models = discover_component_registries()
self.assertIn("amesim", libraries)
self.assertEqual(libraries["amesim"], AMESIM_LIBRARY)
self.assertEqual(
models["amesim_ideal_air_medium"].library.id,
"amesim",
)
self.assertEqual(models["amesim_helium_medium"].library.id, "amesim")
self.assertEqual(models["amesim_pnpl01"].library.id, "amesim")
self.assertEqual(models["amesim_step0"].library.id, "amesim")
self.assertEqual(models["amesim_ud00"].library.id, "amesim")
self.assertEqual(models["amesim_f000"].library.id, "amesim")
self.assertEqual(models["amesim_forc"].library.id, "amesim")
self.assertEqual(models["amesim_mecmas21"].library.id, "amesim")
self.assertEqual(models["amesim_lstp00a"].library.id, "amesim")
self.assertEqual(models["amesim_lmechn1"].library.id, "amesim")
self.assertEqual(models["amesim_pnrp17"].library.id, "amesim")
self.assertEqual(models["amesim_pnch023"].library.id, "amesim")
self.assertEqual(models["amesim_pnch012"].library.id, "amesim")
self.assertEqual(models["amesim_pnor001"].library.id, "amesim")
self.assertEqual(models["amesim_pnvo001_fixed"].library.id, "amesim")
self.assertEqual(models["amesim_pnvo001"].library.id, "amesim")
self.assertEqual(models["amesim_pnl00r"].library.id, "amesim")
self.assertEqual(models["amesim_pnl0001"].library.id, "amesim")
self.assertEqual(models["amesim_pnl0002"].library.id, "amesim")
self.assertEqual(models["amesim_pnl0003"].library.id, "amesim")
self.assertEqual(models["amesim_pn3node2"].library.id, "amesim")
self.assertEqual(models["amesim_p4node2"].library.id, "amesim")
def test_every_public_model_owns_its_catalog_contract(self) -> None:
for model_type, spec in COMPONENT_MODEL_REGISTRY.items():
with self.subTest(model_type=model_type):
declarations = spec.component_class.__dict__
self.assertIn("MODEL_VERSION", declarations)
self.assertIn("DISPLAY", declarations)
self.assertIn("create", declarations)
self.assertEqual(spec.display.library_id, spec.library.id)
self.assertIn(
spec.display.category_id,
spec.library.category_by_id,
)
def test_display_ports_must_match_physical_ports(self) -> None:
class BrokenDisplayComponent(AlgebraicComponent):
MODEL_TYPE = "broken_display"
MODEL_VERSION = "1.0.0"
PORTS = (
PortDefinition.pneumatic(
"port_a",
nominal_role="bidirectional",
),
)
PARAMETERS = ()
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="错误显示模型",
library_id="experimental",
category_id="flow",
symbol="generic",
ports=(PortDisplaySpec("port_b", "right"),),
)
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters,
):
return cls(name)
with self.assertRaisesRegex(
ValueError,
"display ports must exactly match",
):
validate_component_model_class(
BrokenDisplayComponent,
library=LIBRARY,
)
def test_parameter_groups_are_strictly_validated(self) -> None:
parameters = (
ParameterDefinition("mode", 1.0, label="模式"),
ParameterDefinition("gain", 2.0, label="增益"),
ParameterDefinition("offset", 0.0, label="偏置"),
)
def validate_groups(parameter_groups: object) -> None:
class GroupedComponent(AlgebraicComponent):
MODEL_TYPE = "grouped_component"
MODEL_VERSION = "1.0.0"
PORTS = ()
PARAMETERS = parameters
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="参数分组测试组件",
library_id="experimental",
category_id="flow",
symbol="generic",
ports=(),
parameter_groups=parameter_groups, # type: ignore[arg-type]
)
@classmethod
def create(cls, *, name, medium, parameters):
del medium, parameters
return cls(name)
validate_component_model_class(GroupedComponent, library=LIBRARY)
valid_groups = (
ParameterGroupDisplaySpec(
id="advanced",
label="高级参数",
parameters=("mode", "gain"),
order=10,
),
)
validate_groups(valid_groups)
self.assertEqual(
valid_groups[0].as_catalog_dict(),
{
"id": "advanced",
"label": "高级参数",
"parameters": ["mode", "gain"],
"order": 10,
"defaultExpanded": False,
},
)
with self.assertRaisesRegex(ValueError, "must be a tuple"):
validate_groups(list(valid_groups))
with self.assertRaisesRegex(ValueError, "ParameterGroupDisplaySpec"):
validate_groups((object(),))
with self.assertRaisesRegex(ValueError, "must start with a lowercase"):
validate_groups(
(
ParameterGroupDisplaySpec(
id="Advanced",
label="高级参数",
parameters=("mode",),
),
)
)
with self.assertRaisesRegex(ValueError, "label must not be empty"):
validate_groups(
(
ParameterGroupDisplaySpec(
id="advanced",
label=" ",
parameters=("mode",),
),
)
)
with self.assertRaisesRegex(ValueError, "order must be an integer"):
validate_groups(
(
ParameterGroupDisplaySpec(
id="advanced",
label="高级参数",
parameters=("mode",),
order=1.5, # type: ignore[arg-type]
),
)
)
with self.assertRaisesRegex(ValueError, "must be a boolean"):
validate_groups(
(
ParameterGroupDisplaySpec(
id="advanced",
label="高级参数",
parameters=("mode",),
default_expanded=1, # type: ignore[arg-type]
),
)
)
with self.assertRaisesRegex(ValueError, "non-empty tuple"):
validate_groups(
(
ParameterGroupDisplaySpec(
id="advanced",
label="高级参数",
parameters=(),
),
)
)
with self.assertRaisesRegex(ValueError, "unknown parameters: missing"):
validate_groups(
(
ParameterGroupDisplaySpec(
id="advanced",
label="高级参数",
parameters=("missing",),
),
)
)
with self.assertRaisesRegex(ValueError, "contains duplicate names: mode"):
validate_groups(
(
ParameterGroupDisplaySpec(
id="advanced",
label="高级参数",
parameters=("mode", "mode"),
),
)
)
with self.assertRaisesRegex(ValueError, "contains duplicate names: advanced"):
validate_groups(
(
ParameterGroupDisplaySpec(
id="advanced",
label="高级参数",
parameters=("mode",),
),
ParameterGroupDisplaySpec(
id="advanced",
label="其他参数",
parameters=("gain",),
),
)
)
with self.assertRaisesRegex(
ValueError,
"assign parameters to multiple groups: mode",
):
validate_groups(
(
ParameterGroupDisplaySpec(
id="advanced",
label="高级参数",
parameters=("mode", "gain"),
),
ParameterGroupDisplaySpec(
id="secondary",
label="其他参数",
parameters=("mode", "offset"),
),
)
)
def test_property_model_parameter_options_are_strictly_validated(self) -> None:
def validate_parameter(parameter: ParameterDefinition) -> None:
class PropertyModelComponent(AlgebraicComponent):
MODEL_TYPE = "property_model_component"
MODEL_VERSION = "1.0.0"
PORTS = ()
PARAMETERS = (parameter,)
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="物性模型测试组件",
library_id="experimental",
category_id="flow",
symbol="generic",
ports=(),
)
@classmethod
def create(cls, *, name, medium, parameters):
del medium, parameters
return cls(name)
validate_component_model_class(
PropertyModelComponent,
library=LIBRARY,
)
def property_parameter(
*,
default: float = 0.0,
minimum: float | None = None,
maximum: float | None = None,
options: tuple[ParameterOption, ...] = (),
description: object = "",
) -> ParameterDefinition:
return ParameterDefinition(
"property_model",
default,
label="物性计算模型",
quantity="dimensionless",
unit="",
minimum=minimum,
maximum=maximum,
editor="amesimGasPropertyModel",
options=options,
description=description, # type: ignore[arg-type]
)
with self.assertRaisesRegex(ValueError, "description must be a string"):
validate_parameter(property_parameter(description=None))
with self.assertRaisesRegex(ValueError, "description must not be blank"):
validate_parameter(property_parameter(description=" "))
with self.assertRaisesRegex(ValueError, "must declare options"):
validate_parameter(property_parameter())
with self.assertRaisesRegex(ValueError, "duplicate option values"):
validate_parameter(
property_parameter(
options=(
ParameterOption(0, "理想气体"),
ParameterOption(0, "重复模型"),
)
)
)
with self.assertRaisesRegex(ValueError, "finite numbers"):
validate_parameter(
property_parameter(
options=(ParameterOption(float("nan"), "非法模型"),)
)
)
with self.assertRaisesRegex(ValueError, "default value must be one of 0"):
validate_parameter(
property_parameter(
default=1.0,
options=(ParameterOption(0, "理想气体"),),
)
)
with self.assertRaisesRegex(ValueError, "option 1 must be at most 0"):
validate_parameter(
property_parameter(
minimum=0.0,
maximum=0.0,
options=(ParameterOption(1, "越界模型"),),
)
)
with self.assertRaisesRegex(ValueError, "option values must be integers"):
validate_parameter(
property_parameter(
default=0.5,
options=(ParameterOption(0.5, "非整数模型"),),
)
)
def test_choice_parameters_and_visibility_conditions_are_strictly_validated(
self,
) -> None:
def validate_parameters(
parameters: tuple[ParameterDefinition, ...],
) -> None:
class ChoiceComponent(AlgebraicComponent):
MODEL_TYPE = "choice_component"
MODEL_VERSION = "1.0.0"
PORTS = ()
PARAMETERS = parameters
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="离散参数测试组件",
library_id="experimental",
category_id="flow",
symbol="generic",
ports=(),
)
@classmethod
def create(cls, *, name, medium, parameters):
del medium, parameters
return cls(name)
validate_component_model_class(ChoiceComponent, library=LIBRARY)
def choice_parameter(
name: str,
default: float = 0.0,
*,
visible_when: tuple[ParameterCondition, ...] = (),
) -> ParameterDefinition:
return ParameterDefinition(
name,
default,
label=name,
editor="choice",
options=(
ParameterOption(0.0, "关闭"),
ParameterOption(1.0, "开启"),
),
visible_when=visible_when,
)
controller = choice_parameter("mode", 1.0)
dependent = ParameterDefinition(
"gain",
2.0,
label="增益",
visible_when=(ParameterCondition("mode", (0.0, 1.0)),),
)
validate_parameters((controller, dependent))
self.assertEqual(
dependent.as_interface_dict()["visibleWhen"],
[{"parameter": "mode", "values": [0.0, 1.0]}],
)
with self.assertRaisesRegex(ValueError, "choice editor must declare options"):
validate_parameters(
(
ParameterDefinition(
"mode",
0.0,
label="模式",
editor="choice",
),
)
)
with self.assertRaisesRegex(
ValueError,
"options require the 'choice' or 'amesimGasPropertyModel' editor",
):
validate_parameters(
(
ParameterDefinition(
"mode",
0.0,
label="模式",
options=(ParameterOption(0.0, "关闭"),),
),
)
)
with self.assertRaisesRegex(ValueError, "unknown parameter 'missing'"):
validate_parameters(
(
controller,
ParameterDefinition(
"gain",
2.0,
label="增益",
visible_when=(
ParameterCondition("missing", (1.0,)),
),
),
)
)
with self.assertRaisesRegex(ValueError, "cannot reference itself"):
validate_parameters(
(
choice_parameter(
"mode",
visible_when=(ParameterCondition("mode", (1.0,)),),
),
)
)
with self.assertRaisesRegex(
ValueError,
"visibility controller 'continuous' must declare options",
):
validate_parameters(
(
ParameterDefinition("continuous", 0.0, label="连续量"),
ParameterDefinition(
"gain",
2.0,
label="增益",
visible_when=(
ParameterCondition("continuous", (0.0,)),
),
),
)
)
with self.assertRaisesRegex(
ValueError,
"duplicate visibility controller 'mode'",
):
validate_parameters(
(
controller,
ParameterDefinition(
"gain",
2.0,
label="增益",
visible_when=(
ParameterCondition("mode", (0.0,)),
ParameterCondition("mode", (1.0,)),
),
),
)
)
with self.assertRaisesRegex(ValueError, "unsupported: 2"):
validate_parameters(
(
controller,
ParameterDefinition(
"gain",
2.0,
label="增益",
visible_when=(ParameterCondition("mode", (2.0,)),),
),
)
)
with self.assertRaisesRegex(ValueError, "contain a cycle"):
validate_parameters(
(
choice_parameter(
"first",
visible_when=(ParameterCondition("second", (1.0,)),),
),
choice_parameter(
"second",
visible_when=(ParameterCondition("first", (1.0,)),),
),
)
)
def test_visibility_condition_value_shape_is_strictly_validated(self) -> None:
def validate_condition(condition: object) -> None:
class VisibilityComponent(AlgebraicComponent):
MODEL_TYPE = "visibility_component"
MODEL_VERSION = "1.0.0"
PORTS = ()
PARAMETERS = (
ParameterDefinition(
"mode",
0.0,
label="模式",
editor="choice",
options=(ParameterOption(0.0, "关闭"),),
),
ParameterDefinition(
"gain",
1.0,
label="增益",
visible_when=(condition,), # type: ignore[arg-type]
),
)
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="可见性测试组件",
library_id="experimental",
category_id="flow",
symbol="generic",
ports=(),
)
@classmethod
def create(cls, *, name, medium, parameters):
del medium, parameters
return cls(name)
validate_component_model_class(VisibilityComponent, library=LIBRARY)
with self.assertRaisesRegex(ValueError, "must use ParameterCondition"):
validate_condition(object())
with self.assertRaisesRegex(ValueError, "must use a non-empty tuple"):
validate_condition(ParameterCondition("mode", ()))
with self.assertRaisesRegex(ValueError, "must be finite numbers"):
validate_condition(ParameterCondition("mode", (float("nan"),)))
with self.assertRaisesRegex(ValueError, "contains duplicate values"):
validate_condition(ParameterCondition("mode", (0.0, 0.0)))
malformed_visibility = ParameterDefinition(
"gain",
1.0,
label="增益",
visible_when=[], # type: ignore[arg-type]
)
class MalformedVisibilityComponent(AlgebraicComponent):
MODEL_TYPE = "malformed_visibility_component"
MODEL_VERSION = "1.0.0"
PORTS = ()
PARAMETERS = (malformed_visibility,)
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="错误可见性测试组件",
library_id="experimental",
category_id="flow",
symbol="generic",
ports=(),
)
@classmethod
def create(cls, *, name, medium, parameters):
del medium, parameters
return cls(name)
with self.assertRaisesRegex(ValueError, "visible_when must use a tuple"):
validate_component_model_class(
MalformedVisibilityComponent,
library=LIBRARY,
)
if __name__ == "__main__":
unittest.main()