完善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

@@ -4,13 +4,31 @@ from collections.abc import Mapping
from math import expm1
from app.simulation.core.base import AlgebraicComponent, DynamicComponent
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
from app.simulation.core.catalog import (
ComponentDisplaySpec,
ParameterGroupDisplaySpec,
PortDisplaySpec,
)
from app.simulation.core.equations import EquationResidual
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition
from app.simulation.core.metadata import (
ParameterCondition,
ParameterDefinition,
ParameterOption,
ResultVariableDefinition,
)
from app.simulation.core.medium import IdealGasMedium
from app.simulation.core.ports import PortDefinition
_MECMAS21_FRICTION_ENABLED = ParameterCondition("useFriction", (2.0,))
_MECMAS21_NON_RESTITUTION = ParameterCondition("stoptype", (1.0, 2.0, 4.0))
_MECMAS21_LIMITS_ENABLED = ParameterCondition("stoptype", (1.0, 2.0, 3.0))
_MECMAS21_ELASTIC_STOP = ParameterCondition("stoptype", (2.0,))
_MECMAS21_RESTITUTION_STOP = ParameterCondition("stoptype", (3.0,))
_MECMAS21_ADVANCED_FRICTION = ParameterCondition("frictionType", (2.0,))
_MECMAS21_STRIBECK_ENABLED = ParameterCondition("strib", (2.0,))
class AmesimF000(AlgebraicComponent):
"""AMESim F000 zero force source."""
@@ -133,37 +151,328 @@ class AmesimMecmas21(DynamicComponent):
"""AMESim MECMAS21 first public one-dimensional translational mass."""
MODEL_TYPE = "amesim_mecmas21"
MODEL_VERSION = "0.1.0"
MODEL_VERSION = "0.2.0"
PORTS = (
PortDefinition.mechanical_translational("port_1"),
PortDefinition.mechanical_translational("port_2"),
)
PARAMETERS = (
ParameterDefinition("mass", 1.0, label="质量", quantity="mass", unit="kg", minimum=0.0, minimum_exclusive=True),
ParameterDefinition("fstick", 0.0, label="静摩擦力", quantity="force", unit="N", minimum=0.0),
ParameterDefinition("fcoul", 0.0, label="库仑摩擦力", quantity="force", unit="N", minimum=0.0),
ParameterDefinition("rvisc", 0.0, label="黏性摩擦系数", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
ParameterDefinition("wind", 0.0, label="风阻系数", quantity="windage", unit="N/(m/s)^2", minimum=0.0),
ParameterDefinition("dvel", 1.0e-6, label="粘滞速度阈值", quantity="velocity", unit="m/s", minimum=0.0),
ParameterDefinition("restdvel", 1.0e-6, label="恢复速度阈值", quantity="velocity", unit="m/s", minimum=0.0),
ParameterDefinition("restcoeff", 0.65, label="恢复系数", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
ParameterDefinition("astrib", 1.0e-3, label="Stribeck 常数", quantity="velocity", unit="m/s", minimum=0.0),
ParameterDefinition("xmin", -1.0, label="下位移限位", quantity="length", unit="m"),
ParameterDefinition("Kbmin", 1.0e9, label="下限位刚度", quantity="translational_stiffness", unit="N/m", minimum=0.0),
ParameterDefinition("Dbmin", 1.0e4, label="下限位阻尼", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
ParameterDefinition("Pdmin", 1.0e-4, label="下限位满阻尼穿透", quantity="length", unit="m", minimum=0.0),
ParameterDefinition("xmax", 0.8, label="上位移限位", quantity="length", unit="m"),
ParameterDefinition("Kbmax", 1.0e9, label="上限位刚度", quantity="translational_stiffness", unit="N/m", minimum=0.0),
ParameterDefinition("Dbmax", 1.0e4, label="上限位阻尼", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
ParameterDefinition("Pdmax", 1.0e-4, label="上限位满阻尼穿透", quantity="length", unit="m", minimum=0.0),
ParameterDefinition("theta", 0.0, label="倾角", quantity="dimensionless", unit=""),
ParameterDefinition("useFriction", 1.0, label="启用摩擦", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
ParameterDefinition("stoptype", 4.0, label="限位类型", quantity="dimensionless", unit="", minimum=0.0),
ParameterDefinition("discContactOption", 1.0, label="接触选项", quantity="dimensionless", unit="", minimum=0.0),
ParameterDefinition("strib", 1.0, label="Stribeck 选项", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
ParameterDefinition("frictionType", 1.0, label="摩擦类型", quantity="dimensionless", unit="", minimum=0.0),
ParameterDefinition("v0", 0.0, label="初始速度", quantity="velocity", unit="m/s"),
ParameterDefinition("x0", 0.0, label="初始位移", quantity="length", unit="m"),
ParameterDefinition(
"mass",
1.0,
label="质量",
quantity="mass",
unit="kg",
minimum=0.0,
minimum_exclusive=True,
description="平动质量,必须大于零。",
),
ParameterDefinition(
"fstick",
0.0,
label="静摩擦力",
quantity="force",
unit="N",
minimum=0.0,
description=(
"AMESim 静摩擦阈值。当前公开求解器尚未实现静摩擦保持与脱离逻辑,"
"因此该参数暂不参与力计算。"
),
visible_when=(
_MECMAS21_NON_RESTITUTION,
_MECMAS21_FRICTION_ENABLED,
),
),
ParameterDefinition(
"fcoul",
0.0,
label="库仑摩擦力",
quantity="force",
unit="N",
minimum=0.0,
description="滑动时的库仑摩擦力绝对值,方向与速度相反。",
visible_when=(
_MECMAS21_NON_RESTITUTION,
_MECMAS21_FRICTION_ENABLED,
),
),
ParameterDefinition(
"rvisc",
0.0,
label="黏性摩擦系数",
quantity="translational_damping",
unit="N/(m/s)",
minimum=0.0,
description="与速度成正比的黏性摩擦系数。",
visible_when=(_MECMAS21_FRICTION_ENABLED,),
),
ParameterDefinition(
"wind",
0.0,
label="风阻系数",
quantity="windage",
unit="N/(m/s)^2",
minimum=0.0,
description="与速度平方成正比、方向与速度相反的风阻系数。",
visible_when=(_MECMAS21_FRICTION_ENABLED,),
),
ParameterDefinition(
"dvel",
1.0e-6,
label="粘滞速度阈值",
quantity="velocity",
unit="m/s",
minimum=0.0,
description=(
"高级摩擦模型的低速切换阈值;当前公开求解器尚未实现对应的"
"高级静摩擦公式。"
),
visible_when=(
_MECMAS21_NON_RESTITUTION,
_MECMAS21_FRICTION_ENABLED,
_MECMAS21_ADVANCED_FRICTION,
),
),
ParameterDefinition(
"restdvel",
1.0e-6,
label="恢复速度阈值",
quantity="velocity",
unit="m/s",
minimum=0.0,
description="恢复碰撞低于该入射速度时按无回弹处理。",
visible_when=(_MECMAS21_RESTITUTION_STOP,),
),
ParameterDefinition(
"restcoeff",
0.65,
label="恢复系数",
quantity="dimensionless",
unit="",
minimum=0.0,
maximum=1.0,
description="恢复碰撞后的速度与碰撞前速度绝对值之比。",
visible_when=(_MECMAS21_RESTITUTION_STOP,),
),
ParameterDefinition(
"astrib",
1.0e-3,
label="Stribeck 常数",
quantity="velocity",
unit="m/s",
minimum=0.0,
description=(
"高级 Stribeck 摩擦模型的速度常数;当前公开求解器尚未实现"
"对应的 Stribeck 公式。"
),
visible_when=(
_MECMAS21_NON_RESTITUTION,
_MECMAS21_FRICTION_ENABLED,
_MECMAS21_ADVANCED_FRICTION,
_MECMAS21_STRIBECK_ENABLED,
),
),
ParameterDefinition(
"xmin",
-1.0,
label="下位移限位",
quantity="length",
unit="m",
description="理想、弹性或恢复碰撞限位的下边界位置。",
visible_when=(_MECMAS21_LIMITS_ENABLED,),
),
ParameterDefinition(
"Kbmin",
1.0e9,
label="下限位刚度",
quantity="translational_stiffness",
unit="N/m",
minimum=0.0,
description="弹性下限位的接触刚度。",
visible_when=(_MECMAS21_ELASTIC_STOP,),
),
ParameterDefinition(
"Dbmin",
1.0e4,
label="下限位阻尼",
quantity="translational_damping",
unit="N/(m/s)",
minimum=0.0,
description="弹性下限位的最大接触阻尼。",
visible_when=(_MECMAS21_ELASTIC_STOP,),
),
ParameterDefinition(
"Pdmin",
1.0e-4,
label="下限位满阻尼穿透",
quantity="length",
unit="m",
minimum=0.0,
description="弹性下限位阻尼由零增至全值所需的穿透量。",
visible_when=(_MECMAS21_ELASTIC_STOP,),
),
ParameterDefinition(
"xmax",
0.8,
label="上位移限位",
quantity="length",
unit="m",
description="理想、弹性或恢复碰撞限位的上边界位置。",
visible_when=(_MECMAS21_LIMITS_ENABLED,),
),
ParameterDefinition(
"Kbmax",
1.0e9,
label="上限位刚度",
quantity="translational_stiffness",
unit="N/m",
minimum=0.0,
description="弹性上限位的接触刚度。",
visible_when=(_MECMAS21_ELASTIC_STOP,),
),
ParameterDefinition(
"Dbmax",
1.0e4,
label="上限位阻尼",
quantity="translational_damping",
unit="N/(m/s)",
minimum=0.0,
description="弹性上限位的最大接触阻尼。",
visible_when=(_MECMAS21_ELASTIC_STOP,),
),
ParameterDefinition(
"Pdmax",
1.0e-4,
label="上限位满阻尼穿透",
quantity="length",
unit="m",
minimum=0.0,
description="弹性上限位阻尼由零增至全值所需的穿透量。",
visible_when=(_MECMAS21_ELASTIC_STOP,),
),
ParameterDefinition(
"theta",
0.0,
label="倾角(度)",
quantity="dimensionless",
unit="",
description=(
"源 AMESim 参数以度为单位:+90° 表示端口 1 位于最低端,"
"-90° 表示端口 1 位于最高端。当前一维公开求解器尚未加入"
"由该倾角产生的重力分量。"
),
),
ParameterDefinition(
"useFriction",
1.0,
label="启用摩擦",
quantity="dimensionless",
unit="",
editor="choice",
options=(
ParameterOption(1.0, "否"),
ParameterOption(2.0, "是"),
),
description=(
"AMESim 原始编码:1 表示不启用摩擦,2 表示启用摩擦。"
"当前公开模型在启用时实现黏性摩擦、风阻和基础库仑摩擦;"
"高级静摩擦与 Stribeck 公式尚未实现。"
),
),
ParameterDefinition(
"stoptype",
4.0,
label="限位类型",
quantity="dimensionless",
unit="",
editor="choice",
options=(
ParameterOption(1.0, "理想限位"),
ParameterOption(2.0, "弹性限位"),
ParameterOption(3.0, "恢复碰撞"),
ParameterOption(4.0, "无限位"),
),
description=(
"AMESim 原始编码:1 为理想限位,2 为弹性限位,"
"3 为带恢复系数的碰撞限位,4 为无限位。"
),
),
ParameterDefinition(
"discContactOption",
1.0,
label="允许负接触力",
quantity="dimensionless",
unit="",
editor="choice",
options=(
ParameterOption(1.0, "允许负接触力"),
ParameterOption(2.0, "不允许负接触力"),
),
description=(
"仅在弹性限位中生效。AMESim 原始编码:1 保留阻尼项可能产生的"
"负接触力,2 将接触力限制为非负。"
),
visible_when=(_MECMAS21_ELASTIC_STOP,),
),
ParameterDefinition(
"strib",
1.0,
label="Stribeck 效应",
quantity="dimensionless",
unit="",
editor="choice",
options=(
ParameterOption(1.0, "否"),
ParameterOption(2.0, "是"),
),
description=(
"AMESim 原始编码:1 表示不使用 Stribeck 效应,2 表示使用。"
"该选项目前用于保留模型配置和控制参数显示;"
"高级 Stribeck 摩擦公式尚未实现。"
),
visible_when=(
_MECMAS21_NON_RESTITUTION,
_MECMAS21_FRICTION_ENABLED,
_MECMAS21_ADVANCED_FRICTION,
),
),
ParameterDefinition(
"frictionType",
1.0,
label="摩擦类型",
quantity="dimensionless",
unit="",
editor="choice",
options=(
ParameterOption(1.0, "简单"),
ParameterOption(2.0, "高级"),
),
description=(
"AMESim 原始编码:1 为简单摩擦,2 为高级摩擦。"
"当前公开模型只实现黏性摩擦、风阻和基础库仑摩擦,"
"高级静摩擦与 Stribeck 行为尚未实现。"
),
visible_when=(
_MECMAS21_NON_RESTITUTION,
_MECMAS21_FRICTION_ENABLED,
),
),
ParameterDefinition(
"v0",
0.0,
label="初始速度",
quantity="velocity",
unit="m/s",
description="仿真开始时质量的平动速度。",
),
ParameterDefinition(
"x0",
0.0,
label="初始位移",
quantity="length",
unit="m",
description="仿真开始时质量的平动位置。",
),
)
RESULT_VARIABLES = (
ResultVariableDefinition("a", "加速度", "acceleration", "m/s2", "state", 10),
@@ -184,6 +493,41 @@ class AmesimMecmas21(DynamicComponent):
PortDisplaySpec("port_1", "right", order=20),
),
order=30,
parameter_groups=(
ParameterGroupDisplaySpec(
id="friction",
label="摩擦",
parameters=(
"frictionType",
"strib",
"astrib",
"fstick",
"fcoul",
"rvisc",
"wind",
"dvel",
),
order=10,
),
ParameterGroupDisplaySpec(
id="endstops",
label="限位",
parameters=(
"discContactOption",
"xmax",
"Kbmax",
"Dbmax",
"Pdmax",
"xmin",
"Kbmin",
"Dbmin",
"Pdmin",
"restcoeff",
"restdvel",
),
order=20,
),
),
)
state_size = 2
@@ -193,7 +537,7 @@ class AmesimMecmas21(DynamicComponent):
self.set_parameter_values(resolved)
for name, value in resolved.items():
setattr(self, name, float(value))
self.use_friction = bool(int(self.useFriction))
self.use_friction = int(self.useFriction) == 2
self.port_1 = self.register_declared_port("port_1")
self.port_2 = self.register_declared_port("port_2")
self.v = float(self.v0)
@@ -210,9 +554,14 @@ class AmesimMecmas21(DynamicComponent):
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> "AmesimMecmas21":
definitions = {definition.name: definition for definition in cls.PARAMETERS}
for integer_name in ("useFriction", "stoptype", "discContactOption", "strib", "frictionType"):
if not float(parameters[integer_name]).is_integer():
value = float(parameters[integer_name])
if not value.is_integer():
raise ValueError(f"MECMAS21 {integer_name} must be an integer.")
message = definitions[integer_name].validation_message(value)
if message is not None:
raise ValueError(f"MECMAS21 {integer_name} {message}.")
return cls(name=name, medium=medium, **dict(parameters))
def get_state_vector(self) -> list[float]:
+21
View File
@@ -33,6 +33,26 @@ class PortDisplaySpec:
order: int = 0
@dataclass(frozen=True)
class ParameterGroupDisplaySpec:
"""Ordered, collapsible presentation group for component parameters."""
id: str
label: str
parameters: tuple[str, ...]
order: int = 0
default_expanded: bool = False
def as_catalog_dict(self) -> dict[str, object]:
return {
"id": self.id,
"label": self.label,
"parameters": list(self.parameters),
"order": self.order,
"defaultExpanded": self.default_expanded,
}
@dataclass(frozen=True)
class ComponentDisplaySpec:
"""Frontend metadata co-located with a component implementation."""
@@ -44,6 +64,7 @@ class ComponentDisplaySpec:
ports: tuple[PortDisplaySpec, ...]
order: int = 0
role: ComponentCatalogRole | None = None
parameter_groups: tuple[ParameterGroupDisplaySpec, ...] = ()
@property
def port_by_name(self) -> dict[str, PortDisplaySpec]:
+25 -1
View File
@@ -9,6 +9,7 @@ ResultVariableScope = Literal["component", "port"]
ParameterEditor = Literal[
"amesimGasReference",
"amesimGasPropertyModel",
"choice",
]
@@ -52,9 +53,27 @@ class ParameterOption:
}
@dataclass(frozen=True)
class ParameterCondition:
"""Match when the controlling parameter equals any declared value."""
parameter: str
values: tuple[float, ...]
def as_interface_dict(self) -> dict[str, object]:
return {
"parameter": self.parameter,
"values": list(self.values),
}
@dataclass(frozen=True)
class ParameterDefinition:
"""User-configurable model input expressed in the backend SI contract."""
"""User-configurable model input expressed in the backend SI contract.
Every ``visible_when`` condition must match for the catalog parameter to
be visible; each individual condition matches any one of its ``values``.
"""
name: str
default: float
@@ -67,6 +86,7 @@ class ParameterDefinition:
editor: ParameterEditor | None = None
options: tuple[ParameterOption, ...] = ()
description: str = ""
visible_when: tuple[ParameterCondition, ...] = ()
def validation_message(self, value: float) -> str | None:
if not isfinite(value):
@@ -106,6 +126,10 @@ class ParameterDefinition:
]
if self.description:
payload["description"] = self.description
if self.visible_when:
payload["visibleWhen"] = [
condition.as_interface_dict() for condition in self.visible_when
]
if value is not None:
payload["value"] = value
return payload
@@ -433,7 +433,7 @@ def _build_mass(
reset_velocity_threshold_m_s=component.parameter_value("restdvel"),
rest_coeff=component.parameter_value("restcoeff"),
stribeck_constant_m_s=component.parameter_value("astrib"),
use_friction=bool(int(component.parameter_value("useFriction"))),
use_friction=int(component.parameter_value("useFriction")) == 2,
stop_type=int(component.parameter_value("stoptype")),
initial_velocity_m_s=_initial_value(amesim_results, f"v1@{component.alias}"),
initial_displacement_m=_initial_value(amesim_results, f"x1@{component.alias}"),
+233 -2
View File
@@ -12,10 +12,12 @@ from app.simulation.core.catalog import (
ComponentCategorySpec,
ComponentDisplaySpec,
ComponentLibrarySpec,
ParameterGroupDisplaySpec,
PortDisplaySpec,
)
from app.simulation.core.metadata import (
SI_UNIT_BY_QUANTITY,
ParameterCondition,
ParameterDefinition,
ParameterOption,
ResultVariableDefinition,
@@ -97,6 +99,14 @@ class ComponentModelSpec:
}
if self.display.role is not None:
payload["role"] = self.display.role
if self.display.parameter_groups:
payload["parameterGroups"] = [
group.as_catalog_dict()
for group in sorted(
self.display.parameter_groups,
key=lambda item: (item.order, item.id),
)
]
return payload
def create(
@@ -280,6 +290,81 @@ def _validate_category(
)
def _validate_parameter_groups(
parameter_groups: object,
*,
model_type: str,
parameter_names: Sequence[str],
) -> None:
field = f"Component '{model_type}' display parameter groups"
if not isinstance(parameter_groups, tuple):
raise ValueError(f"{field} must be a tuple.")
group_ids: list[str] = []
grouped_parameter_names: list[str] = []
known_parameter_names = set(parameter_names)
for group in parameter_groups:
if not isinstance(group, ParameterGroupDisplaySpec):
raise ValueError(
f"{field} must use ParameterGroupDisplaySpec."
)
group_id = _validate_machine_id(
group.id,
field=f"{field} id",
)
_validate_label(
group.label,
field=f"{field} '{group_id}' label",
)
_validate_order(
group.order,
field=f"{field} '{group_id}' order",
)
if type(group.default_expanded) is not bool:
raise ValueError(
f"{field} '{group_id}' default_expanded must be a boolean."
)
if not isinstance(group.parameters, tuple) or not group.parameters:
raise ValueError(
f"{field} '{group_id}' parameters must use a non-empty tuple."
)
for parameter_name in group.parameters:
_validate_member_id(
parameter_name,
field=f"{field} '{group_id}' parameter name",
)
_validate_unique_names(
list(group.parameters),
field=f"{field} '{group_id}' parameters",
)
unknown_parameters = sorted(
set(group.parameters) - known_parameter_names
)
if unknown_parameters:
raise ValueError(
f"{field} '{group_id}' references unknown parameters: "
+ ", ".join(unknown_parameters)
+ "."
)
group_ids.append(group_id)
grouped_parameter_names.extend(group.parameters)
_validate_unique_names(group_ids, field=field)
duplicate_memberships = sorted(
{
name
for name in grouped_parameter_names
if grouped_parameter_names.count(name) > 1
}
)
if duplicate_memberships:
raise ValueError(
f"{field} assign parameters to multiple groups: "
+ ", ".join(duplicate_memberships)
+ "."
)
def validate_component_library(library: ComponentLibrarySpec) -> None:
if not isinstance(library, ComponentLibrarySpec):
raise ValueError("Enabled component libraries must use ComponentLibrarySpec.")
@@ -449,6 +534,7 @@ def _validate_parameter(
None,
"amesimGasReference",
"amesimGasPropertyModel",
"choice",
}:
raise ValueError(
f"{field} '{parameter.name}' uses unsupported editor "
@@ -462,10 +548,17 @@ def _validate_parameter(
raise ValueError(
f"{field} '{parameter.name}' property-model editor must declare options."
)
if parameter.options and parameter.editor != "amesimGasPropertyModel":
if parameter.editor == "choice" and not parameter.options:
raise ValueError(
f"{field} '{parameter.name}' choice editor must declare options."
)
if parameter.options and parameter.editor not in {
"amesimGasPropertyModel",
"choice",
}:
raise ValueError(
f"{field} '{parameter.name}' options require the "
"'amesimGasPropertyModel' editor."
"'choice' or 'amesimGasPropertyModel' editor."
)
option_values: list[float] = []
for option in parameter.options:
@@ -513,6 +606,55 @@ def _validate_parameter(
f"{field} '{parameter.name}' contains duplicate option values: "
f"{duplicates}."
)
if not isinstance(parameter.visible_when, tuple):
raise ValueError(
f"{field} '{parameter.name}' visible_when must use a tuple."
)
for condition in parameter.visible_when:
if not isinstance(condition, ParameterCondition):
raise ValueError(
f"{field} '{parameter.name}' visible_when entries must use "
"ParameterCondition."
)
_validate_member_id(
condition.parameter,
field=(
f"{field} '{parameter.name}' visibility condition parameter"
),
)
if not isinstance(condition.values, tuple) or not condition.values:
raise ValueError(
f"{field} '{parameter.name}' visibility condition values "
"must use a non-empty tuple."
)
numeric_condition_values: list[float] = []
for value in condition.values:
if (
isinstance(value, bool)
or not isinstance(value, (int, float))
or not isfinite(value)
):
raise ValueError(
f"{field} '{parameter.name}' visibility condition values "
"must be finite numbers."
)
numeric_condition_values.append(float(value))
duplicate_condition_values = sorted(
{
value
for value in numeric_condition_values
if numeric_condition_values.count(value) > 1
}
)
if duplicate_condition_values:
duplicates = ", ".join(
f"{value:g}" for value in duplicate_condition_values
)
raise ValueError(
f"{field} '{parameter.name}' visibility condition for "
f"'{condition.parameter}' contains duplicate values: "
f"{duplicates}."
)
if (
parameter.minimum is not None
and parameter.maximum is not None
@@ -528,6 +670,89 @@ def _validate_parameter(
)
def _validate_parameter_visibility(
parameters: tuple[ParameterDefinition, ...],
*,
model_type: str,
) -> None:
"""Validate cross-parameter references used by catalog visibility rules."""
parameter_by_name = {parameter.name: parameter for parameter in parameters}
dependencies: dict[str, tuple[str, ...]] = {}
for parameter in parameters:
controllers: list[str] = []
for condition in parameter.visible_when:
controller_name = condition.parameter
controller = parameter_by_name.get(controller_name)
if controller is None:
raise ValueError(
f"Component '{model_type}' parameter '{parameter.name}' "
"visibility condition references unknown parameter "
f"'{controller_name}'."
)
if controller_name == parameter.name:
raise ValueError(
f"Component '{model_type}' parameter '{parameter.name}' "
"visibility condition cannot reference itself."
)
if controller_name in controllers:
raise ValueError(
f"Component '{model_type}' parameter '{parameter.name}' "
"contains duplicate visibility controller "
f"'{controller_name}'."
)
if not controller.options:
raise ValueError(
f"Component '{model_type}' parameter '{parameter.name}' "
f"visibility controller '{controller_name}' must declare "
"options."
)
controller_values = {
float(option.value) for option in controller.options
}
unsupported_values = sorted(
{
float(value)
for value in condition.values
if float(value) not in controller_values
}
)
if unsupported_values:
values = ", ".join(f"{value:g}" for value in unsupported_values)
raise ValueError(
f"Component '{model_type}' parameter '{parameter.name}' "
f"visibility condition values for '{controller_name}' "
f"must belong to its options; unsupported: {values}."
)
controllers.append(controller_name)
dependencies[parameter.name] = tuple(controllers)
states: dict[str, int] = {}
stack: list[str] = []
def visit(parameter_name: str) -> None:
state = states.get(parameter_name, 0)
if state == 2:
return
if state == 1:
cycle_start = stack.index(parameter_name)
cycle = stack[cycle_start:] + [parameter_name]
raise ValueError(
f"Component '{model_type}' parameter visibility dependencies "
f"contain a cycle: {' -> '.join(cycle)}."
)
states[parameter_name] = 1
stack.append(parameter_name)
for controller_name in dependencies[parameter_name]:
visit(controller_name)
stack.pop()
states[parameter_name] = 2
for parameter in parameters:
visit(parameter.name)
def _validate_result_variable(
variable: ResultVariableDefinition,
*,
@@ -676,6 +901,12 @@ def validate_component_model_class(
parameter_names,
field=f"Component '{model_type}' parameters",
)
_validate_parameter_groups(
display.parameter_groups,
model_type=model_type,
parameter_names=parameter_names,
)
_validate_parameter_visibility(parameters, model_type=model_type)
_validate_unique_names(
result_names,
field=f"Component '{model_type}' result variables",