Files
SystemSimulationApp/app/simulation/components/amesim/mechanical/translational.py
T

1004 lines
35 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,))
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_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.1.0"
PORTS = (
PortDefinition.signal("res", nominal_role="input"),
PortDefinition.mechanical_translational("port_2"),
)
PARAMETERS = ()
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) -> None:
super().__init__(name=name)
self.set_parameter_values({})
self.res = self.register_declared_port("res")
self.port_2 = self.register_declared_port("port_2")
self._orientation_sign = 1.0
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> "AmesimForc":
return cls(name=name)
def apply_layout_transform(self, *, rotation: int, mirrored: bool) -> None:
"""Apply the AMESim icon direction to the signed force output."""
normalized_rotation = int(rotation) % 360
if normalized_rotation not in {0, 90, 180, 270}:
raise ValueError("FORC rotation must be a multiple of 90 degrees.")
direction = -1.0 if normalized_rotation in {180, 270} else 1.0
self._orientation_sign = -direction if mirrored else direction
@property
def output_force(self) -> float:
return float(self.res.signal)
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._orientation_sign * 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",
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),
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_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.1.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),
ParameterDefinition("gap0", 0.0, label="初始间隙", quantity="length", unit="m"),
ParameterDefinition("kcont", 1.0e6, label="接触刚度", quantity="translational_stiffness", unit="N/m", minimum=0.0),
ParameterDefinition("G", 8.57e10, label="剪切模量", quantity="pressure", unit="Pa", minimum=0.0),
ParameterDefinition("sdiam", 0.02, label="弹簧直径", quantity="length", unit="m", minimum=0.0),
ParameterDefinition("wdiam", 0.002, label="线径", quantity="length", unit="m", minimum=0.0),
ParameterDefinition("rcont", 0.0, label="接触阻尼", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
ParameterDefinition("Pdis", 1.0e-7, label="满阻尼穿透", quantity="length", unit="m", minimum=0.0),
ParameterDefinition("stiffmode", 1.0, label="刚度模式", quantity="dimensionless", unit="", minimum=0.0),
ParameterDefinition("discContactOption", 1.0, label="接触选项", quantity="dimensionless", unit="", minimum=0.0),
)
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,
)
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":
for integer_name in ("stiffmode", "discContactOption"):
if not float(parameters[integer_name]).is_integer():
raise ValueError(f"LSTP00A {integer_name} must be an integer.")
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_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.1.0"
PORTS = tuple(
PortDefinition.mechanical_translational(f"port_{index}")
for index in range(1, 10)
)
PARAMETERS = (
ParameterDefinition("v1", 8.0, label="右侧端口数", quantity="dimensionless", unit="", minimum=1.0, maximum=8.0),
ParameterDefinition("sum", 1.0, label="节点求和模式", quantity="dimensionless", unit="", minimum=0.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}", "left", order=index * 10) for index in range(1, 9)]
+ [PortDisplaySpec("port_9", "right", order=90)]
),
order=50,
)
def __init__(self, name: str, medium: IdealGasMedium, *, v1: float = 8.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"])
@property
def active_ports(self) -> tuple[str, ...]:
return tuple(f"port_{index}" for index in range(1, self.v1 + 1)) + ("port_9",)
@property
def total_force(self) -> float:
return sum(self.get_port(port_name).f for port_name in self.active_ports)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
reference = self.port_9
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}.port_9.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}.port_9.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.total_force,
)
)
return tuple(residuals)
def component_result_values(self) -> Mapping[str, float]:
return {"tforce": self.total_force}