完善MECMAS21参数配置与分组交互

This commit is contained in:
ljz committed 2026-08-03 23:51:27 +08:00
1 parent 32f21f08ff
commit be54070855
28 files changed
+3418 -202

No files matched your search

+415 -2
View File
@@ -5,8 +5,16 @@ 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, PortDisplaySpec
from app.simulation.core.metadata import ParameterDefinition, ParameterOption
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 (
@@ -144,6 +152,166 @@ class ComponentRegistryTests(unittest.TestCase):
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):
@@ -235,6 +403,251 @@ class ComponentRegistryTests(unittest.TestCase):
)
)
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()