初版:实现 AMESim 机械因果化与事件求解

初步支持 MECMAS21 刚性质量状态归并、端止事件、恢复系数,以及 LSTP 接触和压力流量显式因果化。

已知问题:显式传播仍会重复扫描全网方程,长时刚性仿真性能待优化;自适应积分器遇到越出物理域的试探状态时,尚未实现恢复并缩步重试。
This commit is contained in:
ljz committed 2026-08-03 15:45:48 +08:00
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,
),