655 lines
26 KiB
Python
655 lines
26 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, PortDisplaySpec
|
|
from app.simulation.core.equations import EquationResidual
|
|
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition
|
|
from app.simulation.core.medium import IdealGasMedium
|
|
from app.simulation.core.ports import PortDefinition
|
|
|
|
|
|
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.1.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"),
|
|
)
|
|
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,
|
|
)
|
|
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 = bool(int(self.useFriction))
|
|
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":
|
|
for integer_name in ("useFriction", "stoptype", "discContactOption", "strib", "frictionType"):
|
|
if not float(parameters[integer_name]).is_integer():
|
|
raise ValueError(f"MECMAS21 {integer_name} must be an integer.")
|
|
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}
|