优化求解器重试并校正AMESim机械端口
This commit is contained in:
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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user