完善MECMAS21参数配置与分组交互
This commit is contained in:
1 parent
32f21f08ff
commit
be54070855
28 files changed
+3418
-202
No files matched your search
+233
-2
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user