507 lines
20 KiB
Python
507 lines
20 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
|
|
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="generic",
|
|
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="signal",
|
|
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")
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
medium: IdealGasMedium,
|
|
parameters: Mapping[str, float],
|
|
) -> "AmesimForc":
|
|
return cls(name=name)
|
|
|
|
@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.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="generic",
|
|
ports=(
|
|
PortDisplaySpec("port_1", "left", order=10),
|
|
PortDisplaySpec("port_2", "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.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.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:
|
|
penetration = max(self.xmin - self.x, 0.0)
|
|
if penetration <= 0.0:
|
|
return 0.0
|
|
return self.Kbmin * penetration + max(-self.Dbmin * self.v, 0.0)
|
|
|
|
def _upper_limit_force(self) -> float:
|
|
penetration = max(self.x - self.xmax, 0.0)
|
|
if penetration <= 0.0:
|
|
return 0.0
|
|
return self.Kbmax * penetration + max(self.Dbmax * self.v, 0.0)
|
|
|
|
def acceleration(self) -> float:
|
|
return (
|
|
self.port_1.f
|
|
+ self.port_2.f
|
|
+ self._viscous_friction_force()
|
|
+ self._windage_force()
|
|
+ self._dry_friction_force()
|
|
+ self._lower_limit_force()
|
|
- self._upper_limit_force()
|
|
) / self.mass
|
|
|
|
def state_derivative_from_ports(self, connected_h: Mapping[str, float]) -> list[float]:
|
|
return [self.acceleration(), self.v]
|
|
|
|
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="generic",
|
|
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")
|
|
|
|
@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:
|
|
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_2.v - self.port_1.v
|
|
|
|
@property
|
|
def contact_force(self) -> float:
|
|
if self.penetration <= 0.0:
|
|
return 0.0
|
|
return max(self.kcont * self.penetration + self.rcont * self.penetration_velocity, 0.0)
|
|
|
|
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_2.x"),
|
|
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_2.x"),
|
|
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="junction",
|
|
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}
|