1226 lines
42 KiB
Python
1226 lines
42 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from math import expm1
|
|
|
|
from app.simulation.core.base import AlgebraicComponent, DynamicComponent
|
|
from app.simulation.core.catalog import (
|
|
ComponentDisplaySpec,
|
|
ParameterGroupDisplaySpec,
|
|
PortDisplaySpec,
|
|
)
|
|
from app.simulation.core.equations import EquationResidual
|
|
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,))
|
|
_LSTP00A_NUMERICAL_STIFFNESS = ParameterCondition("stiffmode", (1.0,))
|
|
_LSTP00A_GEOMETRICAL_STIFFNESS = ParameterCondition("stiffmode", (2.0,))
|
|
|
|
|
|
class AmesimF000(AlgebraicComponent):
|
|
"""AMESim F000 zero force source."""
|
|
|
|
MODEL_TYPE = "amesim_f000"
|
|
MODEL_VERSION = "0.1.0"
|
|
PORTS = (PortDefinition.mechanical_translational("port_1"),)
|
|
PARAMETERS = ()
|
|
RESULT_VARIABLES = ()
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="F000 零力源",
|
|
library_id="amesim",
|
|
category_id="mechanical",
|
|
symbol="amesim_f000",
|
|
ports=(PortDisplaySpec("port_1", "right", order=10),),
|
|
order=10,
|
|
)
|
|
|
|
def __init__(self, name: str) -> None:
|
|
super().__init__(name=name)
|
|
self.set_parameter_values({})
|
|
self.port_1 = self.register_declared_port("port_1")
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
medium: IdealGasMedium,
|
|
parameters: Mapping[str, float],
|
|
) -> "AmesimF000":
|
|
return cls(name=name)
|
|
|
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
|
return (self.port_1.f,)
|
|
|
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
|
return (
|
|
EquationResidual(
|
|
id=f"{self.name}:zero_force",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="constitutive",
|
|
variables=(f"{self.name}.port_1.f",),
|
|
role="flow",
|
|
value=self.port_1.f,
|
|
),
|
|
)
|
|
|
|
|
|
class AmesimForc(AlgebraicComponent):
|
|
"""AMESim FORC signal-to-force converter."""
|
|
|
|
MODEL_TYPE = "amesim_forc"
|
|
MODEL_VERSION = "0.2.0"
|
|
PORTS = (
|
|
PortDefinition.signal("res", nominal_role="input"),
|
|
PortDefinition.mechanical_translational("port_2"),
|
|
)
|
|
PARAMETERS = (
|
|
ParameterDefinition(
|
|
"direction",
|
|
1.0,
|
|
label="力方向",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
editor="choice",
|
|
options=(
|
|
ParameterOption(1.0, "正向"),
|
|
ParameterOption(-1.0, "反向"),
|
|
),
|
|
description=(
|
|
"显式控制输入信号相对于机械端口正方向的力符号;"
|
|
"图标旋转和镜像不会改变该参数。"
|
|
),
|
|
),
|
|
)
|
|
RESULT_VARIABLES = (
|
|
ResultVariableDefinition("force", "输出力", "force", "N", "signal", 10),
|
|
)
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="FORC 信号转力",
|
|
library_id="amesim",
|
|
category_id="mechanical",
|
|
symbol="amesim_forc",
|
|
ports=(
|
|
PortDisplaySpec("res", "left", order=10),
|
|
PortDisplaySpec("port_2", "right", order=20),
|
|
),
|
|
order=20,
|
|
)
|
|
|
|
def __init__(self, name: str, *, direction: float = 1.0) -> None:
|
|
super().__init__(name=name)
|
|
self.set_parameter_values({"direction": direction})
|
|
self.direction = float(direction)
|
|
self.res = self.register_declared_port("res")
|
|
self.port_2 = self.register_declared_port("port_2")
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
medium: IdealGasMedium,
|
|
parameters: Mapping[str, float],
|
|
) -> "AmesimForc":
|
|
return cls(name=name, direction=parameters["direction"])
|
|
|
|
@property
|
|
def output_force(self) -> float:
|
|
return self.direction * float(self.res.signal)
|
|
|
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
|
return (
|
|
self.port_2.f + self.output_force,
|
|
)
|
|
|
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
|
return (
|
|
EquationResidual(
|
|
id=f"{self.name}:signal_force",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="constitutive",
|
|
variables=(f"{self.name}.port_2.f", f"{self.name}.res.signal"),
|
|
role="flow",
|
|
value=self.port_2.f + self.output_force,
|
|
),
|
|
)
|
|
|
|
def component_result_values(self) -> Mapping[str, float]:
|
|
return {"force": self.output_force}
|
|
|
|
|
|
class AmesimMecmas21(DynamicComponent):
|
|
"""AMESim MECMAS21 first public one-dimensional translational mass."""
|
|
|
|
MODEL_TYPE = "amesim_mecmas21"
|
|
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,
|
|
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",
|
|
2.0,
|
|
label="启用摩擦",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
editor="choice",
|
|
options=(
|
|
ParameterOption(1.0, "否"),
|
|
ParameterOption(2.0, "是"),
|
|
),
|
|
description=(
|
|
"AMESim 原始编码:1 表示不启用摩擦,2 表示启用摩擦。"
|
|
"当前公开模型在启用时实现黏性摩擦、风阻和基础库仑摩擦;"
|
|
"高级静摩擦与 Stribeck 公式尚未实现。"
|
|
),
|
|
),
|
|
ParameterDefinition(
|
|
"stoptype",
|
|
1.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),
|
|
ResultVariableDefinition("v", "速度", "velocity", "m/s", "state", 20),
|
|
ResultVariableDefinition("x", "位移", "length", "m", "state", 30),
|
|
ResultVariableDefinition("Fvisc", "黏性摩擦力", "force", "N", "derived", 40),
|
|
ResultVariableDefinition("Ffric", "干摩擦力", "force", "N", "derived", 50),
|
|
ResultVariableDefinition("Fmin", "下限位力", "force", "N", "derived", 60),
|
|
ResultVariableDefinition("Fmax", "上限位力", "force", "N", "derived", 70),
|
|
)
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="MECMAS21 一维质量",
|
|
library_id="amesim",
|
|
category_id="mechanical",
|
|
symbol="amesim_mecmas21",
|
|
ports=(
|
|
PortDisplaySpec("port_2", "left", order=10),
|
|
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
|
|
|
|
def __init__(self, name: str, medium: IdealGasMedium, **parameters: float) -> None:
|
|
super().__init__(name=name)
|
|
resolved = {definition.name: parameters.get(definition.name, definition.default) for definition in self.PARAMETERS}
|
|
self.set_parameter_values(resolved)
|
|
for name, value in resolved.items():
|
|
setattr(self, name, float(value))
|
|
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)
|
|
self.x = float(self.x0)
|
|
self._constraint_acceleration: float | None = None
|
|
self._constraint_velocity: float | None = None
|
|
self.refresh_thermodynamic_ports()
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
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"):
|
|
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]:
|
|
return [self.v, self.x]
|
|
|
|
def set_state_vector(self, values: list[float]) -> None:
|
|
if len(values) != 2:
|
|
raise ValueError("MECMAS21 state vector requires [v, x].")
|
|
self.v = float(values[0])
|
|
self.x = float(values[1])
|
|
self._constraint_acceleration = None
|
|
self._constraint_velocity = None
|
|
self.refresh_thermodynamic_ports()
|
|
|
|
def refresh_thermodynamic_ports(self) -> None:
|
|
for port in (self.port_1, self.port_2):
|
|
port.x = self.x
|
|
port.v = self.v
|
|
|
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
|
return (
|
|
self.port_1.x - self.x,
|
|
self.port_1.v - self.v,
|
|
self.port_2.x - self.x,
|
|
self.port_2.v - self.v,
|
|
)
|
|
|
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
|
return (
|
|
self._state_residual("port_1", "x", self.port_1.x - self.x),
|
|
self._state_residual("port_1", "v", self.port_1.v - self.v),
|
|
self._state_residual("port_2", "x", self.port_2.x - self.x),
|
|
self._state_residual("port_2", "v", self.port_2.v - self.v),
|
|
)
|
|
|
|
def _state_residual(self, port_name: str, variable: str, value: float) -> EquationResidual:
|
|
return EquationResidual(
|
|
id=f"{self.name}:{port_name}_{variable}_state",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="state",
|
|
variables=(f"{self.name}.{port_name}.{variable}",),
|
|
role="effort",
|
|
value=value,
|
|
)
|
|
|
|
def _viscous_friction_force(self) -> float:
|
|
if not self.use_friction:
|
|
return 0.0
|
|
return -self.rvisc * self.v
|
|
|
|
def _windage_force(self) -> float:
|
|
if not self.use_friction:
|
|
return 0.0
|
|
return -self.wind * self.v * abs(self.v)
|
|
|
|
def _dry_friction_force(self) -> float:
|
|
if not self.use_friction:
|
|
return 0.0
|
|
if self.v > 0.0:
|
|
return -self.fcoul
|
|
if self.v < 0.0:
|
|
return self.fcoul
|
|
return 0.0
|
|
|
|
def _lower_limit_force(self) -> float:
|
|
if int(self.stoptype) != 2:
|
|
return 0.0
|
|
penetration = max(self.xmin - self.x, 0.0)
|
|
if penetration <= 0.0:
|
|
return 0.0
|
|
damping_fraction = (
|
|
min(penetration / self.Pdmin, 1.0)
|
|
if self.Pdmin > 0.0
|
|
else 1.0
|
|
)
|
|
force = (
|
|
self.Kbmin * penetration
|
|
- damping_fraction * self.Dbmin * self.v
|
|
)
|
|
return force if int(self.discContactOption) == 1 else max(force, 0.0)
|
|
|
|
def _upper_limit_force(self) -> float:
|
|
if int(self.stoptype) != 2:
|
|
return 0.0
|
|
penetration = max(self.x - self.xmax, 0.0)
|
|
if penetration <= 0.0:
|
|
return 0.0
|
|
damping_fraction = (
|
|
min(penetration / self.Pdmax, 1.0)
|
|
if self.Pdmax > 0.0
|
|
else 1.0
|
|
)
|
|
force = (
|
|
self.Kbmax * penetration
|
|
+ damping_fraction * self.Dbmax * self.v
|
|
)
|
|
return force if int(self.discContactOption) == 1 else max(force, 0.0)
|
|
|
|
def force_without_endstops(self) -> float:
|
|
return (
|
|
self.port_1.f
|
|
+ self.port_2.f
|
|
+ self._viscous_friction_force()
|
|
+ self._windage_force()
|
|
+ self._dry_friction_force()
|
|
)
|
|
|
|
def unconstrained_acceleration(self) -> float:
|
|
return (
|
|
self.force_without_endstops()
|
|
+ self._lower_limit_force()
|
|
- self._upper_limit_force()
|
|
) / self.mass
|
|
|
|
@property
|
|
def uses_ideal_endstops(self) -> bool:
|
|
return int(self.stoptype) == 1
|
|
|
|
def set_constraint_motion(
|
|
self,
|
|
acceleration: float | None,
|
|
*,
|
|
velocity: float | None = None,
|
|
) -> None:
|
|
self._constraint_acceleration = (
|
|
None if acceleration is None else float(acceleration)
|
|
)
|
|
self._constraint_velocity = None if velocity is None else float(velocity)
|
|
|
|
def acceleration(self) -> float:
|
|
if self._constraint_acceleration is not None:
|
|
return self._constraint_acceleration
|
|
return self.unconstrained_acceleration()
|
|
|
|
def state_derivative_from_ports(self, connected_h: Mapping[str, float]) -> list[float]:
|
|
velocity = (
|
|
self.v
|
|
if self._constraint_velocity is None
|
|
else self._constraint_velocity
|
|
)
|
|
return [self.acceleration(), velocity]
|
|
|
|
def component_result_values(self) -> Mapping[str, float]:
|
|
return {
|
|
"a": self.acceleration(),
|
|
"v": self.v,
|
|
"x": self.x,
|
|
"Fvisc": self._viscous_friction_force(),
|
|
"Ffric": self._dry_friction_force(),
|
|
"Fmin": self._lower_limit_force(),
|
|
"Fmax": self._upper_limit_force(),
|
|
}
|
|
|
|
|
|
class AmesimLstp00a(AlgebraicComponent):
|
|
"""AMESim LSTP00A first public elastic contact component."""
|
|
|
|
MODEL_TYPE = "amesim_lstp00a"
|
|
MODEL_VERSION = "0.2.0"
|
|
PORTS = (
|
|
PortDefinition.mechanical_translational("port_1"),
|
|
PortDefinition.mechanical_translational("port_2"),
|
|
)
|
|
PARAMETERS = (
|
|
ParameterDefinition(
|
|
"na",
|
|
10.0,
|
|
label="有效圈数",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
minimum=0.0,
|
|
minimum_exclusive=True,
|
|
description="几何刚度模式下使用的弹簧有效圈数。",
|
|
visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,),
|
|
),
|
|
ParameterDefinition(
|
|
"gap0",
|
|
0.0,
|
|
label="初始间隙",
|
|
quantity="length",
|
|
unit="m",
|
|
description="两个机械端口开始产生接触力前的初始间隙。",
|
|
),
|
|
ParameterDefinition(
|
|
"kcont",
|
|
1.0e6,
|
|
label="接触刚度",
|
|
quantity="translational_stiffness",
|
|
unit="N/m",
|
|
minimum=0.0,
|
|
description="数值刚度模式下直接指定的接触刚度。",
|
|
visible_when=(_LSTP00A_NUMERICAL_STIFFNESS,),
|
|
),
|
|
ParameterDefinition(
|
|
"G",
|
|
8.57e10,
|
|
label="剪切模量",
|
|
quantity="pressure",
|
|
unit="Pa",
|
|
minimum=0.0,
|
|
description="几何刚度模式下的材料剪切模量。",
|
|
visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,),
|
|
),
|
|
ParameterDefinition(
|
|
"sdiam",
|
|
0.02,
|
|
label="弹簧直径",
|
|
quantity="length",
|
|
unit="m",
|
|
minimum=0.0,
|
|
description="几何刚度模式下的弹簧平均直径。",
|
|
visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,),
|
|
),
|
|
ParameterDefinition(
|
|
"wdiam",
|
|
0.002,
|
|
label="线径",
|
|
quantity="length",
|
|
unit="m",
|
|
minimum=0.0,
|
|
description="几何刚度模式下的弹簧线径。",
|
|
visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,),
|
|
),
|
|
ParameterDefinition(
|
|
"rcont",
|
|
0.0,
|
|
label="接触阻尼",
|
|
quantity="translational_damping",
|
|
unit="N/(m/s)",
|
|
minimum=0.0,
|
|
description="接触穿透过程中使用的最大阻尼系数。",
|
|
),
|
|
ParameterDefinition(
|
|
"Pdis",
|
|
1.0e-7,
|
|
label="满阻尼穿透",
|
|
quantity="length",
|
|
unit="m",
|
|
minimum=0.0,
|
|
description="接触阻尼由零增至全值所需的穿透量。",
|
|
),
|
|
ParameterDefinition(
|
|
"stiffmode",
|
|
1.0,
|
|
label="刚度模式",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
minimum=1.0,
|
|
maximum=2.0,
|
|
editor="choice",
|
|
options=(
|
|
ParameterOption(1.0, "数值刚度"),
|
|
ParameterOption(2.0, "几何参数"),
|
|
),
|
|
description=(
|
|
"AMESim 原始编码:1 直接使用接触刚度,2 使用弹簧几何参数。"
|
|
"当前公开求解器尚未实现由几何参数换算刚度,几何模式仍沿用"
|
|
"已保存的接触刚度值。"
|
|
),
|
|
),
|
|
ParameterDefinition(
|
|
"discContactOption",
|
|
1.0,
|
|
label="允许负接触力",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
minimum=1.0,
|
|
maximum=2.0,
|
|
editor="choice",
|
|
options=(
|
|
ParameterOption(1.0, "允许负接触力"),
|
|
ParameterOption(2.0, "不允许负接触力"),
|
|
),
|
|
description=(
|
|
"AMESim 原始编码:1 保留阻尼项可能产生的负接触力,"
|
|
"2 将接触力限制为非负。"
|
|
),
|
|
),
|
|
)
|
|
RESULT_VARIABLES = (
|
|
ResultVariableDefinition("gap", "间隙", "length", "m", "derived", 10),
|
|
ResultVariableDefinition("penetration", "穿透", "length", "m", "derived", 20),
|
|
ResultVariableDefinition("force", "接触力", "force", "N", "derived", 30),
|
|
)
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="LSTP00A 弹性接触",
|
|
library_id="amesim",
|
|
category_id="mechanical",
|
|
symbol="amesim_lstp00a",
|
|
ports=(
|
|
PortDisplaySpec("port_1", "left", order=10),
|
|
PortDisplaySpec("port_2", "right", order=20),
|
|
),
|
|
order=40,
|
|
parameter_groups=(
|
|
ParameterGroupDisplaySpec(
|
|
id="stiffness",
|
|
label="刚度",
|
|
parameters=("kcont", "G", "sdiam", "wdiam", "na"),
|
|
order=10,
|
|
),
|
|
ParameterGroupDisplaySpec(
|
|
id="contact",
|
|
label="接触",
|
|
parameters=("gap0", "rcont", "Pdis"),
|
|
order=20,
|
|
),
|
|
),
|
|
)
|
|
|
|
def __init__(self, name: str, medium: IdealGasMedium, **parameters: float) -> None:
|
|
super().__init__(name=name)
|
|
resolved = {
|
|
definition.name: parameters.get(definition.name, definition.default)
|
|
for definition in self.PARAMETERS
|
|
}
|
|
self.set_parameter_values(resolved)
|
|
for name, value in resolved.items():
|
|
setattr(self, name, float(value))
|
|
self.port_1 = self.register_declared_port("port_1")
|
|
self.port_2 = self.register_declared_port("port_2")
|
|
self._causal_penetration: float | None = None
|
|
self._causal_contact_force: float | None = None
|
|
self._causal_port_1_x: float | None = None
|
|
self._causal_port_2_x: float | None = None
|
|
self._causal_port_1_v: float | None = None
|
|
self._causal_port_2_v: float | None = None
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
medium: IdealGasMedium,
|
|
parameters: Mapping[str, float],
|
|
) -> "AmesimLstp00a":
|
|
definitions = {definition.name: definition for definition in cls.PARAMETERS}
|
|
for integer_name in ("stiffmode", "discContactOption"):
|
|
value = float(parameters[integer_name])
|
|
if not value.is_integer():
|
|
raise ValueError(f"LSTP00A {integer_name} must be an integer.")
|
|
message = definitions[integer_name].validation_message(value)
|
|
if message is not None:
|
|
raise ValueError(f"LSTP00A {integer_name} {message}.")
|
|
return cls(name=name, medium=medium, **dict(parameters))
|
|
|
|
@property
|
|
def gap(self) -> float:
|
|
if self._causal_penetration is not None:
|
|
assert self._causal_port_1_x is not None
|
|
assert self._causal_port_2_x is not None
|
|
penetration = (
|
|
self._causal_penetration
|
|
+ (self.port_1.x - self._causal_port_1_x)
|
|
- (self.port_2.x - self._causal_port_2_x)
|
|
)
|
|
return -penetration
|
|
return self.gap0 + (self.port_2.x - self.port_1.x)
|
|
|
|
@property
|
|
def penetration(self) -> float:
|
|
return max(-self.gap, 0.0)
|
|
|
|
@property
|
|
def penetration_velocity(self) -> float:
|
|
return self.port_1.v - self.port_2.v
|
|
|
|
@property
|
|
def contact_force(self) -> float:
|
|
if self._causal_contact_force is not None:
|
|
assert self._causal_port_1_x is not None
|
|
assert self._causal_port_2_x is not None
|
|
assert self._causal_port_1_v is not None
|
|
assert self._causal_port_2_v is not None
|
|
if (
|
|
self.port_1.x == self._causal_port_1_x
|
|
and self.port_2.x == self._causal_port_2_x
|
|
and self.port_1.v == self._causal_port_1_v
|
|
and self.port_2.v == self._causal_port_2_v
|
|
):
|
|
return self._causal_contact_force
|
|
return self.contact_force_for_penetration(self.penetration)
|
|
|
|
def contact_force_for_penetration(self, penetration: float) -> float:
|
|
penetration = max(float(penetration), 0.0)
|
|
if penetration <= 0.0:
|
|
return 0.0
|
|
damping_fraction = (
|
|
-expm1(-penetration / self.Pdis)
|
|
if self.Pdis > 0.0
|
|
else 1.0
|
|
)
|
|
force = (
|
|
self.kcont * penetration
|
|
+ damping_fraction * self.rcont * self.penetration_velocity
|
|
)
|
|
return force if int(self.discContactOption) == 1 else max(force, 0.0)
|
|
|
|
def clear_causal_contact(self) -> None:
|
|
self._causal_penetration = None
|
|
self._causal_contact_force = None
|
|
self._causal_port_1_x = None
|
|
self._causal_port_2_x = None
|
|
self._causal_port_1_v = None
|
|
self._causal_port_2_v = None
|
|
|
|
def set_causal_contact(self, *, penetration: float, force: float) -> None:
|
|
"""Retain a locally causalized contact pair without cancellation.
|
|
|
|
A massless contact can require a penetration many orders of magnitude
|
|
smaller than either absolute port coordinate. Reconstructing that
|
|
penetration by subtracting the two coordinates can then lose the
|
|
information entirely. The algebraic solver has already solved this
|
|
constitutive pair, so retain it for the rest of the current closure.
|
|
"""
|
|
|
|
self._causal_penetration = max(float(penetration), 0.0)
|
|
self._causal_contact_force = (
|
|
float(force)
|
|
if int(self.discContactOption) == 1
|
|
else max(float(force), 0.0)
|
|
)
|
|
self._causal_port_1_x = float(self.port_1.x)
|
|
self._causal_port_2_x = float(self.port_2.x)
|
|
self._causal_port_1_v = float(self.port_1.v)
|
|
self._causal_port_2_v = float(self.port_2.v)
|
|
|
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
|
force = self.contact_force
|
|
return (
|
|
self.port_1.f - force,
|
|
self.port_2.f + force,
|
|
)
|
|
|
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
|
force = self.contact_force
|
|
return (
|
|
EquationResidual(
|
|
id=f"{self.name}:port_1_contact_force",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="constitutive",
|
|
variables=(
|
|
f"{self.name}.port_1.f",
|
|
f"{self.name}.port_1.x",
|
|
f"{self.name}.port_1.v",
|
|
f"{self.name}.port_2.x",
|
|
f"{self.name}.port_2.v",
|
|
),
|
|
role="flow",
|
|
value=self.port_1.f - force,
|
|
),
|
|
EquationResidual(
|
|
id=f"{self.name}:port_2_contact_force",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="constitutive",
|
|
variables=(
|
|
f"{self.name}.port_2.f",
|
|
f"{self.name}.port_1.x",
|
|
f"{self.name}.port_1.v",
|
|
f"{self.name}.port_2.x",
|
|
f"{self.name}.port_2.v",
|
|
),
|
|
role="flow",
|
|
value=self.port_2.f + force,
|
|
),
|
|
)
|
|
|
|
def component_result_values(self) -> Mapping[str, float]:
|
|
return {
|
|
"gap": self.gap,
|
|
"penetration": self.penetration,
|
|
"force": self.contact_force,
|
|
}
|
|
|
|
|
|
class AmesimLmechn1(AlgebraicComponent):
|
|
"""AMESim LMECHN1 first public dynamic linear mechanical node."""
|
|
|
|
MODEL_TYPE = "amesim_lmechn1"
|
|
MODEL_VERSION = "0.2.0"
|
|
PORTS = tuple(
|
|
PortDefinition.mechanical_translational(f"port_{index}")
|
|
for index in range(1, 22)
|
|
)
|
|
PARAMETERS = (
|
|
ParameterDefinition(
|
|
"v1",
|
|
2.0,
|
|
label="右侧端口数",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
minimum=1.0,
|
|
maximum=20.0,
|
|
description="设置工作区中显示的右侧机械端口数量,最多 20 个。",
|
|
),
|
|
ParameterDefinition(
|
|
"sum",
|
|
1.0,
|
|
label="节点求和模式",
|
|
quantity="dimensionless",
|
|
unit="",
|
|
editor="choice",
|
|
options=(
|
|
ParameterOption(1.0, "各端口力代数和为零(标准节点)"),
|
|
),
|
|
),
|
|
)
|
|
RESULT_VARIABLES = (
|
|
ResultVariableDefinition("tforce", "节点合力", "force", "N", "derived", 10),
|
|
)
|
|
DISPLAY = ComponentDisplaySpec(
|
|
label="LMECHN1 线性机械节点",
|
|
library_id="amesim",
|
|
category_id="mechanical",
|
|
symbol="amesim_lmechn1",
|
|
ports=tuple(
|
|
[
|
|
PortDisplaySpec(f"port_{index}", "right", order=index * 10)
|
|
for index in range(1, 21)
|
|
]
|
|
+ [PortDisplaySpec("port_21", "left", order=210)]
|
|
),
|
|
order=50,
|
|
)
|
|
|
|
def __init__(self, name: str, medium: IdealGasMedium, *, v1: float = 2.0, sum: float = 1.0) -> None:
|
|
super().__init__(name=name)
|
|
self.set_parameter_values({"v1": v1, "sum": sum})
|
|
self.v1 = int(v1)
|
|
self.sum = int(sum)
|
|
for definition in self.PORTS:
|
|
setattr(self, definition.name, self.register_declared_port(definition.name))
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
medium: IdealGasMedium,
|
|
parameters: Mapping[str, float],
|
|
) -> "AmesimLmechn1":
|
|
for integer_name in ("v1", "sum"):
|
|
if not float(parameters[integer_name]).is_integer():
|
|
raise ValueError(f"LMECHN1 {integer_name} must be an integer.")
|
|
return cls(name=name, medium=medium, v1=parameters["v1"], sum=parameters["sum"])
|
|
|
|
@classmethod
|
|
def active_port_definitions_for_parameters(
|
|
cls,
|
|
parameters: Mapping[str, float],
|
|
) -> tuple[PortDefinition, ...]:
|
|
right_port_count = float(parameters["v1"])
|
|
if not right_port_count.is_integer():
|
|
raise ValueError("LMECHN1 v1 must be an integer.")
|
|
count = int(right_port_count)
|
|
if count < 1 or count > 20:
|
|
raise ValueError("LMECHN1 v1 must be between 1 and 20.")
|
|
return cls.PORTS[: count + 1]
|
|
|
|
@property
|
|
def active_port_definitions(self) -> tuple[PortDefinition, ...]:
|
|
return self.PORTS[: self.v1 + 1]
|
|
|
|
@property
|
|
def active_ports(self) -> tuple[str, ...]:
|
|
return tuple(
|
|
definition.name for definition in self.active_port_definitions
|
|
)
|
|
|
|
@property
|
|
def reference_port_name(self) -> str:
|
|
return f"port_{self.v1 + 1}"
|
|
|
|
@property
|
|
def required_connection_ports(self) -> tuple[str, ...]:
|
|
return self.active_ports
|
|
|
|
@property
|
|
def total_force(self) -> float:
|
|
# AMESim's ``tforce`` is the force transmitted by the summed branch
|
|
# ports (1..v1). The final active port is the balancing/common port and
|
|
# is excluded from that reported value.
|
|
return sum(self.get_port(port_name).f for port_name in self.active_ports[:-1])
|
|
|
|
@property
|
|
def force_balance(self) -> float:
|
|
return self.total_force + self.get_port(self.reference_port_name).f
|
|
|
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
|
reference = self.get_port(self.reference_port_name)
|
|
values: list[float] = []
|
|
for port_name in self.active_ports[:-1]:
|
|
port = self.get_port(port_name)
|
|
values.extend((port.x - reference.x, port.v - reference.v))
|
|
values.append(self.force_balance)
|
|
return tuple(values)
|
|
|
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
|
reference_name = self.reference_port_name
|
|
reference = self.get_port(reference_name)
|
|
residuals: list[EquationResidual] = []
|
|
for port_name in self.active_ports[:-1]:
|
|
port = self.get_port(port_name)
|
|
residuals.append(
|
|
EquationResidual(
|
|
id=f"{self.name}:{port_name}_x_equal",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="equal",
|
|
variables=(f"{self.name}.{port_name}.x", f"{self.name}.{reference_name}.x"),
|
|
role="effort",
|
|
value=port.x - reference.x,
|
|
)
|
|
)
|
|
residuals.append(
|
|
EquationResidual(
|
|
id=f"{self.name}:{port_name}_v_equal",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="equal",
|
|
variables=(f"{self.name}.{port_name}.v", f"{self.name}.{reference_name}.v"),
|
|
role="effort",
|
|
value=port.v - reference.v,
|
|
)
|
|
)
|
|
residuals.append(
|
|
EquationResidual(
|
|
id=f"{self.name}:force_balance",
|
|
owner="component",
|
|
owner_id=self.name,
|
|
relation="sumToZero",
|
|
variables=tuple(f"{self.name}.{port_name}.f" for port_name in self.active_ports),
|
|
role="flow",
|
|
value=self.force_balance,
|
|
)
|
|
)
|
|
return tuple(residuals)
|
|
|
|
def component_result_values(self) -> Mapping[str, float]:
|
|
return {"tforce": self.total_force}
|