优化求解器重试并校正AMESim机械端口

This commit is contained in:
huojiarong committed 2026-08-03 09:54:46 +00:00
1 parent 971e8f2336
commit 18d9802f03
15 files changed
+383 -166

No files matched your search

@@ -17,6 +17,12 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase):
def setUp(self) -> None:
self.medium = IdealGasMedium()
def test_mecmas21_catalog_ports_follow_amesim_icon_sides(self) -> None:
self.assertEqual(
tuple((port.name, port.side) for port in AmesimMecmas21.DISPLAY.ports),
(("port_2", "left"), ("port_1", "right")),
)
def test_f000_constrains_mechanical_port_force_to_zero(self) -> None:
source = AmesimF000("zero_1")
source.port_1.f = 12.5
@@ -38,6 +44,14 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase):
self.assertAlmostEqual(residuals[0].value, 0.0)
self.assertEqual(converter.component_result_values(), {"force": 20.0})
converter.apply_layout_transform(rotation=180, mirrored=False)
converter.port_2.f = 20.0
self.assertAlmostEqual(
converter.pressure_flow_equation_residuals()[0].value,
0.0,
)
self.assertEqual(converter.component_result_values(), {"force": 20.0})
def test_mecmas21_acceleration_uses_connected_port_forces(self) -> None:
mass = AmesimMecmas21(
"mass_1",
@@ -96,12 +110,12 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase):
kcont=1000.0,
rcont=10.0,
)
contact.port_1.x = 0.0
contact.port_2.x = 0.002
contact.port_1.v = 0.0
contact.port_2.v = 0.1
contact.port_1.f = -3.0
contact.port_2.f = 3.0
contact.port_1.x = 0.002
contact.port_2.x = 0.0
contact.port_1.v = 0.1
contact.port_2.v = 0.0
contact.port_1.f = 3.0
contact.port_2.f = -3.0
self.assertAlmostEqual(contact.gap, -0.002)
self.assertAlmostEqual(contact.penetration, 0.002)
@@ -116,8 +130,8 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase):
def test_lstp00a_returns_zero_before_contact(self) -> None:
contact = AmesimLstp00a("contact_1", self.medium, gap0=0.001, kcont=1000.0)
contact.port_1.x = 0.0
contact.port_2.x = 0.0005
contact.port_1.x = 0.0005
contact.port_2.x = 0.0
self.assertAlmostEqual(contact.gap, 0.0005)
self.assertAlmostEqual(contact.contact_force, 0.0)
+6 -6
View File
@@ -146,9 +146,9 @@ def signal_force_mass_project() -> ReactFlowProjectPayload:
def elastic_contact_project() -> ReactFlowProjectPayload:
left_parameters = dict(MECMAS21_DEFAULTS)
left_parameters["x0"] = 0.0
left_parameters["x0"] = 0.001
right_parameters = dict(MECMAS21_DEFAULTS)
right_parameters["x0"] = 0.001
right_parameters["x0"] = 0.0
return ReactFlowProjectPayload(
name="amesim-mechanical-elastic-contact-smoke",
nodes=[
@@ -260,10 +260,10 @@ class AmesimMechanicalXmlTests(unittest.TestCase):
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["time"], [0.0, 0.01, 0.02])
self.assertAlmostEqual(result["series"]["contact_1.force"][0], 1.0)
self.assertAlmostEqual(result["series"]["mass_left.a"][0], 0.5)
self.assertAlmostEqual(result["series"]["mass_right.a"][0], -0.5)
self.assertGreater(result["series"]["mass_left.x"][-1], 0.0)
self.assertLess(result["series"]["mass_right.x"][-1], 0.001)
self.assertAlmostEqual(result["series"]["mass_left.a"][0], -0.5)
self.assertAlmostEqual(result["series"]["mass_right.a"][0], 0.5)
self.assertLess(result["series"]["mass_left.x"][-1], 0.001)
self.assertGreater(result["series"]["mass_right.x"][-1], 0.0)
def test_zero_force_mechanical_project_compiles_and_simulates(self) -> None:
xml = build_reactflow_system_xml(zero_force_mass_project())
+14 -2
View File
@@ -79,9 +79,9 @@ def pnrp17_coupled_project() -> ReactFlowProjectPayload:
"amesim_pnrp17",
[
_pneumatic_port("port_1", "left"),
mechanical_port("port_2", "right"),
mechanical_port("port_3", "left"),
mechanical_port("port_4", "left"),
mechanical_port("port_2", "left"),
mechanical_port("port_4", "right"),
mechanical_port("port_5", "right"),
],
{"gi": 0.0, "dp": 0.1, "dr": 0.02, "x0": 0.0},
@@ -131,6 +131,18 @@ def pnrp17_coupled_project() -> ReactFlowProjectPayload:
class AmesimPnrp17Tests(unittest.TestCase):
def test_catalog_ports_follow_amesim_pnrp17_icon_sides(self) -> None:
self.assertEqual(
tuple((port.name, port.side) for port in AmesimPnrp17.DISPLAY.ports),
(
("port_1", "left"),
("port_3", "left"),
("port_2", "left"),
("port_4", "right"),
("port_5", "right"),
),
)
def test_component_equations_match_pnrp17_geometry_and_signs(self) -> None:
medium = IdealGasMedium()
piston = AmesimPnrp17(
+2 -2
View File
@@ -163,7 +163,7 @@ class ComponentCatalogTests(unittest.TestCase):
for parameter in components["amesim_mecmas21"]["parameters"]
}
self.assertEqual(components["amesim_mecmas21"]["category"]["id"], "mechanical")
self.assertEqual([port["name"] for port in components["amesim_mecmas21"]["ports"]], ["port_1", "port_2"])
self.assertEqual([port["name"] for port in components["amesim_mecmas21"]["ports"]], ["port_2", "port_1"])
self.assertEqual(mecmas_parameters["mass"]["unit"], "kg")
self.assertEqual(mecmas_parameters["Kbmin"]["unit"], "N/m")
lstp_parameters = {
@@ -185,7 +185,7 @@ class ComponentCatalogTests(unittest.TestCase):
for parameter in components["amesim_pnrp17"]["parameters"]
}
self.assertEqual(components["amesim_pnrp17"]["category"]["id"], "mechanical")
self.assertEqual([port["name"] for port in components["amesim_pnrp17"]["ports"]], ["port_1", "port_3", "port_4", "port_2", "port_5"])
self.assertEqual([port["name"] for port in components["amesim_pnrp17"]["ports"]], ["port_1", "port_3", "port_2", "port_4", "port_5"])
self.assertEqual(pnrp_parameters["dp"]["unit"], "m")
self.assertEqual(lmechn_parameters["v1"]["maximum"], 8.0)
self.assertEqual(components["amesim_pnvo001"]["category"]["id"], "flow")
+5 -5
View File
@@ -58,7 +58,7 @@ class _PressureCoupledMechanicalLoad(AlgebraicComponent):
f"{self.name}.pneumatic.p",
),
role="flow",
value=self.mechanical.f - self.pneumatic.p,
value=self.mechanical.f + self.pneumatic.p,
),
EquationResidual(
id=f"{self.name}:pressure_closure",
@@ -163,7 +163,7 @@ class ContactSolverCausalizationTests(unittest.TestCase):
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(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)
@@ -172,7 +172,7 @@ class ContactSolverCausalizationTests(unittest.TestCase):
expected_force = 10.0 - 20.0 * (1.0 - exp(-1.0))
load = _PrescribedMechanicalLoad(
"load",
force=expected_force,
force=-expected_force,
displacement=0.0,
velocity=0.0,
)
@@ -191,8 +191,8 @@ class ContactSolverCausalizationTests(unittest.TestCase):
mass=1.0,
useFriction=0.0,
stoptype=4.0,
x0=0.08,
v0=-2.0,
x0=-0.08,
v0=2.0,
)
zero = AmesimF000("zero")
+76
View File
@@ -4,6 +4,7 @@ import types
import unittest
from unittest.mock import patch
from app.simulation.core.errors import RecoverableTrialStateError
from app.simulation.solvers.solver import (
SolveIVPConfig,
StateTransition,
@@ -71,6 +72,81 @@ class IntegrateOdeTests(unittest.TestCase):
self.assertEqual(calls[0]["max_step"], 1.0e-3)
self.assertNotIn("first_step", calls[0])
def test_stepwise_solver_rebuilds_after_recoverable_trial_failure(self) -> None:
import numpy as np
import scipy.integrate
attempted_max_steps: list[float] = []
class RetryBdf:
def __init__(self, fun, t0, y0, t_bound, **kwargs):
self.fun = fun
self.t = float(t0)
self.y = np.asarray(y0, dtype=float)
self.t_bound = float(t_bound)
self.h_abs = float(kwargs["max_step"])
self.status = "running"
attempted_max_steps.append(self.h_abs)
def step(self):
if self.h_abs > 0.25:
raise RecoverableTrialStateError("trial state outside domain")
self.t = self.t_bound
self.status = "finished"
return None
def dense_output(self):
state = self.y.copy()
return lambda _time: state.copy()
with patch.object(scipy.integrate, "BDF", RetryBdf):
result = integrate_ode(
rhs=lambda _time, _state: [0.0],
initial_state=[1.0],
config=SolveIVPConfig(
t_start=0.0,
t_stop=1.0,
method="BDF",
max_step=1.0,
),
t_eval=[0.0, 1.0],
cancel_check=lambda: False,
)
self.assertTrue(result.success, result.message)
self.assertEqual(attempted_max_steps, [1.0, 0.5, 0.25])
self.assertEqual(result.t, [0.0, 1.0])
self.assertEqual(result.y, [[1.0, 1.0]])
def test_stepwise_solver_does_not_retry_ordinary_model_errors(self) -> None:
import numpy as np
import scipy.integrate
attempts = 0
class FailingBdf:
def __init__(self, fun, t0, y0, t_bound, **kwargs):
nonlocal attempts
attempts += 1
self.t = float(t0)
self.y = np.asarray(y0, dtype=float)
self.status = "running"
def step(self):
raise ValueError("structural model error")
with patch.object(scipy.integrate, "BDF", FailingBdf):
result = integrate_ode(
rhs=lambda _time, _state: [0.0],
initial_state=[1.0],
config=SolveIVPConfig(t_start=0.0, t_stop=1.0, method="BDF"),
cancel_check=lambda: False,
)
self.assertFalse(result.success)
self.assertEqual(result.message, "structural model error")
self.assertEqual(attempts, 1)
def test_scipy_stepwise_solver_can_cancel_before_start(self) -> None:
result = integrate_ode(
rhs=lambda _time, state: state,
@@ -144,10 +144,10 @@ class MechanicalSolverCausalizationTests(unittest.TestCase):
Pdis=0.1,
discContactOption=1.0,
)
contact.port_1.x = 0.0
contact.port_2.x = 0.1
contact.port_1.v = 0.0
contact.port_2.v = -2.0
contact.port_1.x = 0.1
contact.port_2.x = 0.0
contact.port_1.v = -2.0
contact.port_2.v = 0.0
expected = 10.0 - 20.0 * (1.0 - exp(-1.0))
self.assertAlmostEqual(contact.contact_force, expected, places=12)
@@ -175,7 +175,7 @@ class MechanicalSolverCausalizationTests(unittest.TestCase):
def test_causal_contact_survives_unrelated_nonlinear_fallback(self) -> None:
medium = IdealGasMedium()
source = AmesimForc("contact_force")
source.res.signal = -40.0
source.res.signal = 40.0
contact = AmesimLstp00a(
"contact",
medium,