完善MECMAS21参数配置与分组交互
This commit is contained in:
1 parent
32f21f08ff
commit
be54070855
28 files changed
+3418
-202
No files matched your search
+108
-5
@@ -38,6 +38,8 @@ FRONTEND_DIST_DIR = Path(__file__).resolve().parent.parent / "frontend" / "dist"
|
||||
PROJECT_STORAGE_DIR = Path(__file__).parent / "data" / "reactflow-projects"
|
||||
SYSTEM_XML_SCHEMA_VERSION = "2"
|
||||
SYSTEM_XML_UNIT_SYSTEM = "SI"
|
||||
AMESIM_PARAMETER_ENCODING_VERSION = 1
|
||||
MECMAS21_LEGACY_BINARY_PARAMETERS = ("useFriction", "strib")
|
||||
|
||||
SimulationProgressEmitter = Callable[
|
||||
[int, str, str, float | None, float | None],
|
||||
@@ -137,6 +139,15 @@ class ReactFlowSimulationConfig(BaseModel):
|
||||
class ReactFlowProjectPayload(BaseModel):
|
||||
name: str = "untitled"
|
||||
mediumReferenceVersion: Literal[1] | None = None
|
||||
# Missing/None is reserved for projects written before canonical AMESim
|
||||
# option codes were introduced. Every current writer emits version 1.
|
||||
amesimParameterEncodingVersion: Literal[1] | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Version 1 is required for current writers; omission is accepted "
|
||||
"only when importing legacy MECMAS21 0/1 option codes."
|
||||
),
|
||||
)
|
||||
nodes: list[ReactFlowNodePayload] = Field(default_factory=list)
|
||||
edges: list[ReactFlowEdgePayload] = Field(default_factory=list)
|
||||
simulation: ReactFlowSimulationConfig = Field(default_factory=ReactFlowSimulationConfig)
|
||||
@@ -168,8 +179,42 @@ class SimulationCancellationPayload(BaseModel):
|
||||
|
||||
def pydantic_to_jsonable(model: BaseModel) -> dict[str, Any]:
|
||||
if hasattr(model, "model_dump"):
|
||||
return model.model_dump(mode="json")
|
||||
return model.dict()
|
||||
return model.model_dump(mode="json", exclude_none=True)
|
||||
return model.dict(exclude_none=True)
|
||||
|
||||
|
||||
def reactflow_project_storage_data(
|
||||
project: ReactFlowProjectPayload,
|
||||
) -> dict[str, Any]:
|
||||
"""Return a current, self-identifying project payload for persistence."""
|
||||
|
||||
data = pydantic_to_jsonable(project)
|
||||
if project.amesimParameterEncodingVersion is None:
|
||||
from app.simulation.registry import get_component_model_spec
|
||||
|
||||
stored_nodes = data["nodes"]
|
||||
for index, node in enumerate(project.nodes):
|
||||
if node.data.modelType != "amesim_mecmas21":
|
||||
continue
|
||||
parameter_definitions = get_component_model_spec(
|
||||
node.data.modelType
|
||||
).parameter_by_name
|
||||
stored_parameters = stored_nodes[index]["data"].setdefault(
|
||||
"parameters",
|
||||
{},
|
||||
)
|
||||
for name in MECMAS21_LEGACY_BINARY_PARAMETERS:
|
||||
stored_parameters[name] = canonical_amesim_parameter_value(
|
||||
project,
|
||||
node,
|
||||
name,
|
||||
node.data.parameters.get(
|
||||
name,
|
||||
parameter_definitions[name].default,
|
||||
),
|
||||
)
|
||||
data["amesimParameterEncodingVersion"] = AMESIM_PARAMETER_ENCODING_VERSION
|
||||
return data
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@@ -367,7 +412,7 @@ def save_reactflow_project(
|
||||
) -> dict[str, object]:
|
||||
path = reactflow_project_path(project_id)
|
||||
PROJECT_STORAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
data = pydantic_to_jsonable(payload)
|
||||
data = reactflow_project_storage_data(payload)
|
||||
data["name"] = payload.name or project_id
|
||||
path.write_text(
|
||||
json.dumps(data, ensure_ascii=False, indent=2),
|
||||
@@ -907,6 +952,9 @@ def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
|
||||
"schemaVersion": SYSTEM_XML_SCHEMA_VERSION,
|
||||
"unitSystem": SYSTEM_XML_UNIT_SYSTEM,
|
||||
"mediumReferenceVersion": "1",
|
||||
"amesimParameterEncodingVersion": str(
|
||||
AMESIM_PARAMETER_ENCODING_VERSION
|
||||
),
|
||||
},
|
||||
)
|
||||
ET.SubElement(
|
||||
@@ -963,11 +1011,30 @@ def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
|
||||
port_definition.positiveFlowDirection or "intoComponent"
|
||||
)
|
||||
ET.SubElement(component_node, "Port", port_attributes)
|
||||
parameter_values = dict(node.data.parameters)
|
||||
parameter_values = {
|
||||
name: canonical_amesim_parameter_value(project, node, name, value)
|
||||
for name, value in node.data.parameters.items()
|
||||
}
|
||||
try:
|
||||
component_spec = get_component_model_spec(node.data.modelType)
|
||||
except ValueError:
|
||||
component_spec = None
|
||||
if (
|
||||
component_spec is not None
|
||||
and project.amesimParameterEncodingVersion is None
|
||||
and node.data.modelType == "amesim_mecmas21"
|
||||
):
|
||||
parameter_definitions = {
|
||||
parameter.name: parameter for parameter in component_spec.parameters
|
||||
}
|
||||
for name in MECMAS21_LEGACY_BINARY_PARAMETERS:
|
||||
if name not in parameter_values:
|
||||
parameter_values[name] = canonical_amesim_parameter_value(
|
||||
project,
|
||||
node,
|
||||
name,
|
||||
parameter_definitions[name].default,
|
||||
)
|
||||
if (
|
||||
component_spec is not None
|
||||
and component_spec.display.role == "amesimGasMediumDefinition"
|
||||
@@ -1140,7 +1207,12 @@ def compile_reactflow_network(
|
||||
for node in project.nodes:
|
||||
spec = get_component_model_spec(node.data.modelType)
|
||||
parameter_values = {
|
||||
parameter.name: parameter_float(node, parameter.name, parameter.default)
|
||||
parameter.name: canonical_amesim_parameter_value(
|
||||
project,
|
||||
node,
|
||||
parameter.name,
|
||||
parameter_float(node, parameter.name, parameter.default),
|
||||
)
|
||||
for parameter in spec.parameters
|
||||
}
|
||||
unknown_parameters = set(node.data.parameters) - set(spec.parameter_by_name)
|
||||
@@ -1436,6 +1508,37 @@ def parameter_float(
|
||||
raise ValueError(f"Parameter '{name}' on component '{node.id}' must be numeric.")
|
||||
|
||||
|
||||
def canonical_amesim_parameter_value(
|
||||
project: ReactFlowProjectPayload,
|
||||
node: ReactFlowNodePayload,
|
||||
name: str,
|
||||
value: Any,
|
||||
) -> Any:
|
||||
"""Translate legacy app-local AMESim codes to the canonical catalog codes.
|
||||
|
||||
Projects created before ``amesimParameterEncodingVersion`` used 0/1 for
|
||||
MECMAS21 yes/no parameters. AMESim itself uses 1/2, so preserve the old
|
||||
project meaning while all newly saved projects and System XML use the
|
||||
canonical codes.
|
||||
"""
|
||||
|
||||
if (
|
||||
project.amesimParameterEncodingVersion is not None
|
||||
or node.data.modelType != "amesim_mecmas21"
|
||||
or name not in MECMAS21_LEGACY_BINARY_PARAMETERS
|
||||
):
|
||||
return value
|
||||
try:
|
||||
numeric_value = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return value
|
||||
if numeric_value == 0.0:
|
||||
return 1.0
|
||||
if numeric_value == 1.0:
|
||||
return 2.0
|
||||
return value
|
||||
|
||||
|
||||
def pipe_config_from_node(node: ReactFlowNodePayload | None, pipe_config_type):
|
||||
return pipe_config_type(
|
||||
length=parameter_float(node, "length", 5.0),
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
+87
-1
@@ -23,6 +23,8 @@ SYSTEM_XML_V2_SCHEMA_PATH = (
|
||||
Path(__file__).resolve().parent.parent / "schemas" / "system-simulation-v2.xsd"
|
||||
)
|
||||
SUPPORTED_SOLVER_METHODS = {"RK45", "RK23", "DOP853", "Radau", "BDF", "LSODA"}
|
||||
AMESIM_PARAMETER_ENCODING_VERSION = "1"
|
||||
MECMAS21_LEGACY_BINARY_PARAMETERS = ("useFriction", "strib")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -139,6 +141,7 @@ class SystemXmlDocument:
|
||||
schema_version: str
|
||||
unit_system: str
|
||||
medium_reference_version: str | None
|
||||
amesim_parameter_encoding_version: str | None
|
||||
simulation: SystemXmlSimulation
|
||||
components: tuple[SystemXmlComponent, ...]
|
||||
connections: tuple[SystemXmlConnection, ...]
|
||||
@@ -153,6 +156,10 @@ class SystemXmlDocument:
|
||||
}
|
||||
if self.medium_reference_version is not None:
|
||||
result["mediumReferenceVersion"] = self.medium_reference_version
|
||||
if self.amesim_parameter_encoding_version is not None:
|
||||
result["amesimParameterEncodingVersion"] = (
|
||||
self.amesim_parameter_encoding_version
|
||||
)
|
||||
return result
|
||||
|
||||
def as_project_data(self) -> dict[str, object]:
|
||||
@@ -176,6 +183,7 @@ class SystemXmlDocument:
|
||||
return {
|
||||
"name": self.name,
|
||||
"mediumReferenceVersion": 1,
|
||||
"amesimParameterEncodingVersion": 1,
|
||||
"nodes": [
|
||||
{
|
||||
"id": component.id,
|
||||
@@ -292,11 +300,19 @@ def validate_system_xml_document(
|
||||
document, property_model_issues = _normalize_amesim_gas_property_models(
|
||||
document
|
||||
)
|
||||
document = replace(document, medium_reference_version="1")
|
||||
document, parameter_encoding_issues = (
|
||||
_normalize_legacy_amesim_parameter_encoding(document)
|
||||
)
|
||||
document = replace(
|
||||
document,
|
||||
medium_reference_version="1",
|
||||
amesim_parameter_encoding_version=AMESIM_PARAMETER_ENCODING_VERSION,
|
||||
)
|
||||
issues = tuple(
|
||||
[
|
||||
*compatibility_issues,
|
||||
*property_model_issues,
|
||||
*parameter_encoding_issues,
|
||||
*_semantic_issues(document),
|
||||
]
|
||||
)
|
||||
@@ -350,6 +366,9 @@ def _parse_validated_root(root: etree._Element) -> SystemXmlDocument:
|
||||
schema_version=str(root.get("schemaVersion")),
|
||||
unit_system=str(root.get("unitSystem")),
|
||||
medium_reference_version=root.get("mediumReferenceVersion"),
|
||||
amesim_parameter_encoding_version=root.get(
|
||||
"amesimParameterEncodingVersion"
|
||||
),
|
||||
simulation=simulation,
|
||||
components=components,
|
||||
connections=connections,
|
||||
@@ -392,6 +411,73 @@ def _parse_component(element: etree._Element) -> SystemXmlComponent:
|
||||
)
|
||||
|
||||
|
||||
def _normalize_legacy_amesim_parameter_encoding(
|
||||
document: SystemXmlDocument,
|
||||
) -> tuple[SystemXmlDocument, tuple[ValidationIssue, ...]]:
|
||||
"""Upgrade legacy app-local MECMAS21 0/1 codes to AMESim 1/2 codes."""
|
||||
|
||||
if document.amesim_parameter_encoding_version is not None:
|
||||
return document, ()
|
||||
|
||||
normalized_components: list[SystemXmlComponent] = []
|
||||
issues: list[ValidationIssue] = []
|
||||
for component in document.components:
|
||||
if component.model_type != "amesim_mecmas21":
|
||||
normalized_components.append(component)
|
||||
continue
|
||||
|
||||
changed_parameters: list[str] = []
|
||||
normalized_parameters: list[SystemXmlParameter] = []
|
||||
parameter_names: set[str] = set()
|
||||
for parameter in component.parameters:
|
||||
parameter_names.add(parameter.name)
|
||||
value = parameter.value
|
||||
if parameter.name in MECMAS21_LEGACY_BINARY_PARAMETERS:
|
||||
if value == 0.0:
|
||||
value = 1.0
|
||||
changed_parameters.append(parameter.name)
|
||||
elif value == 1.0:
|
||||
value = 2.0
|
||||
changed_parameters.append(parameter.name)
|
||||
normalized_parameters.append(replace(parameter, value=value))
|
||||
|
||||
for name in MECMAS21_LEGACY_BINARY_PARAMETERS:
|
||||
if name not in parameter_names:
|
||||
normalized_parameters.append(
|
||||
SystemXmlParameter(name=name, value=2.0, line=component.line)
|
||||
)
|
||||
changed_parameters.append(name)
|
||||
|
||||
normalized_components.append(
|
||||
replace(component, parameters=tuple(normalized_parameters))
|
||||
)
|
||||
if changed_parameters:
|
||||
issues.append(
|
||||
ValidationIssue(
|
||||
layer="semantic",
|
||||
code="AMESIM_PARAMETER_ENCODING_MIGRATED",
|
||||
message=(
|
||||
f"Component '{component.id}' used legacy MECMAS21 "
|
||||
"option semantics; normalized "
|
||||
+ ", ".join(changed_parameters)
|
||||
+ " to canonical AMESim 1/2 codes."
|
||||
),
|
||||
severity="warning",
|
||||
path=f"/System/Components/Component[@id='{component.id}']",
|
||||
line=component.line,
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
replace(
|
||||
document,
|
||||
components=tuple(normalized_components),
|
||||
amesim_parameter_encoding_version=AMESIM_PARAMETER_ENCODING_VERSION,
|
||||
),
|
||||
tuple(issues),
|
||||
)
|
||||
|
||||
|
||||
def _normalize_legacy_amesim_gas_references(
|
||||
document: SystemXmlDocument,
|
||||
) -> tuple[SystemXmlDocument, list[ValidationIssue]]:
|
||||
|
||||
Reference in new issue
Block a user