初版:实现 AMESim 机械因果化与事件求解
初步支持 MECMAS21 刚性质量状态归并、端止事件、恢复系数,以及 LSTP 接触和压力流量显式因果化。 已知问题:显式传播仍会重复扫描全网方程,长时刚性仿真性能待优化;自适应积分器遇到越出物理域的试探状态时,尚未实现恢复并缩步重试。
This commit is contained in:
1 parent
de265cdde6
commit
971e8f2336
9 files changed
+2808
-169
No files matched your search
@@ -1,6 +1,7 @@
|
||||
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
|
||||
@@ -187,6 +188,8 @@ class AmesimMecmas21(DynamicComponent):
|
||||
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
|
||||
@@ -210,6 +213,8 @@ class AmesimMecmas21(DynamicComponent):
|
||||
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:
|
||||
@@ -256,30 +261,82 @@ class AmesimMecmas21(DynamicComponent):
|
||||
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
|
||||
return self.Kbmin * penetration + max(-self.Dbmin * self.v, 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
|
||||
return self.Kbmax * penetration + max(self.Dbmax * self.v, 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 acceleration(self) -> float:
|
||||
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]:
|
||||
return [self.acceleration(), self.v]
|
||||
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 {
|
||||
@@ -342,6 +399,12 @@ class AmesimLstp00a(AlgebraicComponent):
|
||||
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(
|
||||
@@ -358,6 +421,15 @@ class AmesimLstp00a(AlgebraicComponent):
|
||||
|
||||
@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_2.x - self._causal_port_2_x)
|
||||
- (self.port_1.x - self._causal_port_1_x)
|
||||
)
|
||||
return -penetration
|
||||
return self.gap0 - (self.port_2.x - self.port_1.x)
|
||||
|
||||
@property
|
||||
@@ -370,9 +442,63 @@ class AmesimLstp00a(AlgebraicComponent):
|
||||
|
||||
@property
|
||||
def contact_force(self) -> float:
|
||||
if self.penetration <= 0.0:
|
||||
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
|
||||
return max(self.kcont * self.penetration + self.rcont * self.penetration_velocity, 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
|
||||
@@ -382,7 +508,13 @@ class AmesimLstp00a(AlgebraicComponent):
|
||||
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"),
|
||||
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,
|
||||
),
|
||||
@@ -391,7 +523,13 @@ class AmesimLstp00a(AlgebraicComponent):
|
||||
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"),
|
||||
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,
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user