初版:实现 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

+216
View File
@@ -0,0 +1,216 @@
from __future__ import annotations
from math import exp
import unittest
from app.simulation.components.amesim.mechanical.translational import (
AmesimF000,
AmesimLstp00a,
AmesimMecmas21,
)
from app.simulation.core.base import AlgebraicComponent
from app.simulation.core.equations import EquationResidual
from app.simulation.core.medium import IdealGasMedium
from app.simulation.core.ports import PortDefinition
from app.simulation.solvers.algebraic import PressureFlowSolver
from app.simulation.systems.network import SimulationNetwork
class _PressureCoupledMechanicalLoad(AlgebraicComponent):
PORTS = (
PortDefinition.mechanical_translational("mechanical"),
PortDefinition.pneumatic("pneumatic"),
)
def __init__(
self,
name: str,
*,
initial_force: float,
solved_force: float,
displacement: float,
) -> None:
super().__init__(name=name)
self.solved_force = float(solved_force)
self.mechanical = self.register_declared_port("mechanical")
self.pneumatic = self.register_declared_port("pneumatic")
self.mechanical.x = float(displacement)
self.pneumatic.p = float(initial_force)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
return (
EquationResidual(
id=f"{self.name}:velocity_state",
owner="component",
owner_id=self.name,
relation="state",
variables=(f"{self.name}.mechanical.v",),
role="effort",
value=self.mechanical.v,
),
EquationResidual(
id=f"{self.name}:pressure_force",
owner="component",
owner_id=self.name,
relation="constitutive",
variables=(
f"{self.name}.mechanical.f",
f"{self.name}.pneumatic.p",
),
role="flow",
value=self.mechanical.f - self.pneumatic.p,
),
EquationResidual(
id=f"{self.name}:pressure_closure",
owner="component",
owner_id=self.name,
relation="constitutive",
variables=(f"{self.name}.pneumatic.p",),
role="effort",
value=self.pneumatic.p - self.solved_force,
),
EquationResidual(
id=f"{self.name}:zero_mass_flow",
owner="component",
owner_id=self.name,
relation="constitutive",
variables=(f"{self.name}.pneumatic.m_flow",),
role="flow",
value=self.pneumatic.m_flow,
),
)
class _PrescribedMechanicalLoad(AlgebraicComponent):
PORTS = (PortDefinition.mechanical_translational("port_1"),)
def __init__(
self,
name: str,
*,
force: float,
displacement: float,
velocity: float,
) -> None:
super().__init__(name=name)
self.force = float(force)
self.velocity = float(velocity)
self.port_1 = self.register_declared_port("port_1")
self.port_1.x = float(displacement)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
return (
EquationResidual(
id=f"{self.name}:velocity_state",
owner="component",
owner_id=self.name,
relation="state",
variables=(f"{self.name}.port_1.v",),
role="effort",
value=self.port_1.v - self.velocity,
),
EquationResidual(
id=f"{self.name}:force",
owner="component",
owner_id=self.name,
relation="constitutive",
variables=(f"{self.name}.port_1.f",),
role="flow",
value=self.port_1.f - self.force,
),
)
class ContactSolverCausalizationTests(unittest.TestCase):
def test_nonlinear_binding_tracks_sub_ulp_force_change(self) -> None:
medium = IdealGasMedium()
load = _PressureCoupledMechanicalLoad(
"load",
initial_force=40.0,
solved_force=41.0,
displacement=1.0e9,
)
contact = AmesimLstp00a(
"contact",
medium,
gap0=0.0,
kcont=1.0e11,
rcont=0.0,
Pdis=1.0e-7,
discContactOption=1.0,
)
mass = AmesimMecmas21(
"mass",
medium,
mass=1.0,
useFriction=0.0,
stoptype=4.0,
x0=1.0e9,
v0=0.0,
)
zero = AmesimF000("zero")
network = SimulationNetwork("sub-ulp-dynamic-contact-binding")
for component in (load, contact, mass, zero):
network.add_component(component)
network.connect("load", "mechanical", "contact", "port_1")
network.connect("contact", "port_2", "mass", "port_1")
network.connect("mass", "port_2", "zero", "port_1")
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
diagnostics = PressureFlowSolver(network).solve()
self.assertTrue(diagnostics.success, diagnostics.message)
self.assertGreater(diagnostics.evaluations, 0)
self.assertAlmostEqual(load.pneumatic.p, 41.0, delta=1.0e-3)
self.assertAlmostEqual(load.mechanical.f, 41.0, delta=1.0e-3)
self.assertAlmostEqual(contact.contact_force, 41.0, delta=1.0e-3)
self.assertAlmostEqual(contact.penetration, 4.1e-10, delta=1.0e-14)
def test_negative_contact_binding_uses_nearest_feasible_root(self) -> None:
medium = IdealGasMedium()
expected_force = 10.0 - 20.0 * (1.0 - exp(-1.0))
load = _PrescribedMechanicalLoad(
"load",
force=expected_force,
displacement=0.0,
velocity=0.0,
)
contact = AmesimLstp00a(
"contact",
medium,
gap0=0.0,
kcont=100.0,
rcont=10.0,
Pdis=0.1,
discContactOption=1.0,
)
mass = AmesimMecmas21(
"mass",
medium,
mass=1.0,
useFriction=0.0,
stoptype=4.0,
x0=0.08,
v0=-2.0,
)
zero = AmesimF000("zero")
network = SimulationNetwork("negative-contact-force-binding")
for component in (load, contact, mass, zero):
network.add_component(component)
network.connect("load", "port_1", "contact", "port_1")
network.connect("contact", "port_2", "mass", "port_1")
network.connect("mass", "port_2", "zero", "port_1")
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
diagnostics = PressureFlowSolver(network).solve()
self.assertTrue(diagnostics.success, diagnostics.message)
self.assertLess(contact.contact_force, 0.0)
self.assertAlmostEqual(contact.contact_force, expected_force, places=10)
self.assertAlmostEqual(contact.penetration, 0.1, places=10)
if __name__ == "__main__":
unittest.main()