298 lines
13 KiB
Python
298 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
|
|
from PythonModels.core.solver import SolveIVPConfig
|
|
from PythonModels.reporting.test_mql_variables import build_test_mql_variable_catalog
|
|
from PythonModels.systems.test_mql import TestMqlSystem
|
|
from PythonModels.systems.test_mql_mechanical import (
|
|
TestMqlMechanicalMassClosure,
|
|
build_test_mql_mechanical_assembly,
|
|
circular_area,
|
|
mm_to_m,
|
|
n_per_mm_per_s_to_n_per_m_per_s,
|
|
n_per_mm_to_n_per_m,
|
|
)
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
TEST_MQL_AME = REPO_ROOT / "AmesimModels" / "test_mql.ame"
|
|
|
|
|
|
class TestMqlMechanicalAssemblyTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.amesim_results = load_test_mql_amesim_results(TEST_MQL_AME)
|
|
cls.variable_catalog = build_test_mql_variable_catalog(cls.amesim_results)
|
|
cls.assembly = build_test_mql_mechanical_assembly(
|
|
amesim_results=cls.amesim_results,
|
|
variable_catalog=cls.variable_catalog,
|
|
)
|
|
|
|
def test_assembles_expected_mechanical_component_counts(self) -> None:
|
|
self.assertEqual(len(self.assembly.pistons), 8)
|
|
self.assertEqual(len(self.assembly.masses), 10)
|
|
self.assertEqual(len(self.assembly.elastic_endstops), 8)
|
|
self.assertEqual(len(self.assembly.mechanical_nodes), 2)
|
|
self.assertEqual(len(self.assembly.piecewise_signals), 2)
|
|
self.assertEqual(len(self.assembly.force_connectors), 2)
|
|
self.assertEqual(len(self.assembly.zero_force_sources), 16)
|
|
self.assertEqual(self.assembly.component_count, 48)
|
|
|
|
def test_piecewise_force_sources_match_amesim_samples(self) -> None:
|
|
front_signal = self.assembly.piecewise_signals["piecewiselinear_1"]
|
|
rear_signal = self.assembly.piecewise_signals["piecewiselinear"]
|
|
|
|
self.assertAlmostEqual(front_signal.output_at(0.0), 1.0e12)
|
|
self.assertAlmostEqual(front_signal.output_at(0.8000000000000009), 0.0)
|
|
self.assertAlmostEqual(rear_signal.output_at(0.0), 1.0e17)
|
|
self.assertAlmostEqual(rear_signal.output_at(0.8000000000000009), 49000.0)
|
|
self.assertEqual(
|
|
self.assembly.force_connectors["forcecon_2"].target_mass_alias,
|
|
"mass_friction_endstops_18",
|
|
)
|
|
self.assertEqual(
|
|
self.assembly.force_connectors["forcecon_1"].target_mass_alias,
|
|
"mass_friction_endstops_19",
|
|
)
|
|
self.assertAlmostEqual(
|
|
self.assembly.force_connectors["forcecon_2"].force_at(
|
|
0.0,
|
|
self.assembly.piecewise_signals,
|
|
),
|
|
self.amesim_results.series("force@forcecon_2")[0],
|
|
)
|
|
self.assertAlmostEqual(
|
|
self.assembly.force_connectors["forcecon_1"].force_at(
|
|
0.8000000000000009,
|
|
self.assembly.piecewise_signals,
|
|
),
|
|
self.amesim_results.series("force@forcecon_1")[80],
|
|
)
|
|
|
|
def test_mechanical_mass_closure_exposes_all_mecmas21_states(self) -> None:
|
|
closure = TestMqlMechanicalMassClosure(self.assembly)
|
|
state = closure.initial_state_vector()
|
|
snapshot = closure.snapshot(state)
|
|
rhs = closure.rhs(state)
|
|
|
|
self.assertEqual(closure.mass_aliases, tuple(self.assembly.masses))
|
|
self.assertEqual(len(closure.mass_aliases), 10)
|
|
self.assertEqual(len(state), 20)
|
|
self.assertEqual(snapshot.state_count, 20)
|
|
self.assertEqual(len(rhs), 20)
|
|
self.assertEqual(snapshot.states[0].alias, "mass_friction_endstops_10")
|
|
self.assertAlmostEqual(
|
|
snapshot.states[0].velocity_m_s,
|
|
self.amesim_results.series("v1@mass_friction_endstops_10")[0],
|
|
)
|
|
self.assertAlmostEqual(
|
|
snapshot.states[0].displacement_m,
|
|
self.amesim_results.series("x1@mass_friction_endstops_10")[0],
|
|
)
|
|
for index, mass_state in enumerate(snapshot.states):
|
|
self.assertAlmostEqual(rhs[2 * index + 1], mass_state.velocity_m_s)
|
|
|
|
def test_mechanical_mass_snapshot_exposes_lmechan1_node_kinematics(self) -> None:
|
|
closure = TestMqlMechanicalMassClosure(self.assembly)
|
|
state = closure.initial_state_vector()
|
|
state[16] = 0.2
|
|
state[17] = 0.37
|
|
state[18] = -0.3
|
|
state[19] = -0.72
|
|
|
|
snapshot = closure.snapshot(state)
|
|
front = snapshot.node_kinematics_by_alias[
|
|
"dynamic_mechanical_node_alternative_2"
|
|
]
|
|
rear = snapshot.node_kinematics_by_alias[
|
|
"dynamic_mechanical_node_alternative_3"
|
|
]
|
|
|
|
self.assertEqual(set(front.velocities_m_s), set(range(1, 9)))
|
|
self.assertEqual(set(front.displacements_m), set(range(1, 9)))
|
|
self.assertEqual(set(rear.velocities_m_s), set(range(1, 9)))
|
|
self.assertEqual(set(rear.displacements_m), set(range(1, 9)))
|
|
self.assertAlmostEqual(front.velocities_m_s[1], -0.2)
|
|
self.assertAlmostEqual(front.displacements_m[8], -0.37)
|
|
self.assertAlmostEqual(rear.velocities_m_s[1], -0.3)
|
|
self.assertAlmostEqual(rear.displacements_m[8], -0.72)
|
|
|
|
def test_mechanical_mass_snapshot_exposes_piston_kinematics(self) -> None:
|
|
closure = TestMqlMechanicalMassClosure(self.assembly)
|
|
state = closure.initial_state_vector()
|
|
state[0] = 0.4
|
|
state[1] = 0.37
|
|
state[18] = -0.3
|
|
state[19] = -0.72
|
|
|
|
snapshot = closure.snapshot(state)
|
|
piston = snapshot.piston_kinematics_by_alias["pn_brp2_8"]
|
|
|
|
self.assertEqual(set(snapshot.piston_kinematics_by_alias), set(self.assembly.pistons))
|
|
self.assertAlmostEqual(piston.port_2_velocity_m_s, 0.4)
|
|
self.assertAlmostEqual(piston.port_2_displacement_m, 0.37)
|
|
self.assertAlmostEqual(piston.port_3_velocity_m_s, -0.3)
|
|
self.assertAlmostEqual(piston.port_3_displacement_m, -0.72)
|
|
self.assertAlmostEqual(
|
|
self.assembly.pistons["pn_brp2_8"].geometry().chamber_length_mm(
|
|
piston.port_3_displacement_m,
|
|
piston.port_2_displacement_m,
|
|
),
|
|
1090.0,
|
|
)
|
|
|
|
def test_system_simulates_mechanical_mass_state_closure(self) -> None:
|
|
system = TestMqlSystem()
|
|
|
|
solution = system.simulate_mechanical_masses(
|
|
config=SolveIVPConfig(t_start=0.0, t_stop=1.0e-5, max_step=1.0e-6),
|
|
t_eval=[0.0, 1.0e-5],
|
|
)
|
|
|
|
self.assertTrue(solution.success)
|
|
self.assertEqual(len(system.mechanical_state_vector()), 20)
|
|
self.assertEqual(len(solution.t), 2)
|
|
self.assertEqual(len(solution.y), 20)
|
|
self.assertEqual([len(row) for row in solution.y], [2] * 20)
|
|
|
|
def test_piston_geometry_is_converted_to_si_units(self) -> None:
|
|
piston = self.assembly.pistons["pn_brp2_8"]
|
|
|
|
self.assertAlmostEqual(piston.piston_diameter_m, 0.2)
|
|
self.assertAlmostEqual(piston.rod_diameter_m, 0.001)
|
|
self.assertAlmostEqual(piston.zero_displacement_m, 0.0)
|
|
self.assertAlmostEqual(piston.piston_area_m2, circular_area(0.2))
|
|
self.assertAlmostEqual(piston.rod_area_m2, circular_area(0.001))
|
|
self.assertAlmostEqual(piston.annulus_area_m2, piston.piston_area_m2 - piston.rod_area_m2)
|
|
self.assertIn("vvol1@pn_brp2_8", piston.data_paths)
|
|
self.assertIn("f2@pn_brp2_8", piston.data_paths)
|
|
|
|
def test_mass_endstop_parameters_are_converted_to_si_units(self) -> None:
|
|
mass = self.assembly.masses["mass_friction_endstops_10"]
|
|
|
|
self.assertEqual(mass.mass_kg, 50.0)
|
|
self.assertEqual(mass.xmin_m, -1.0)
|
|
self.assertEqual(mass.xmax_m, 0.8)
|
|
self.assertEqual(mass.min_stiffness_n_per_m, 1.0e9)
|
|
self.assertEqual(mass.max_stiffness_n_per_m, 1.0e9)
|
|
self.assertEqual(mass.min_damping_n_per_m_per_s, 1.0e4)
|
|
self.assertEqual(mass.max_damping_n_per_m_per_s, 1.0e4)
|
|
self.assertAlmostEqual(mass.min_penetration_m, 1.0e-4)
|
|
self.assertAlmostEqual(mass.max_penetration_m, 1.0e-4)
|
|
self.assertEqual(mass.stiction_force_n, 0.0)
|
|
self.assertEqual(mass.coulomb_friction_n, 0.0)
|
|
self.assertEqual(mass.viscous_friction_n_per_m_per_s, 0.0)
|
|
self.assertEqual(mass.windage_n_per_m2_per_s2, 0.0)
|
|
self.assertEqual(mass.stick_velocity_threshold_m_s, 1.0e-6)
|
|
self.assertEqual(mass.reset_velocity_threshold_m_s, 1.0e-6)
|
|
self.assertEqual(mass.rest_coeff, 0.65)
|
|
self.assertEqual(mass.stribeck_constant_m_s, 1.0e-3)
|
|
self.assertTrue(mass.use_friction)
|
|
self.assertEqual(mass.stop_type, 4)
|
|
self.assertIn("v1@mass_friction_endstops_10", mass.data_paths)
|
|
self.assertIn("x1@mass_friction_endstops_10", mass.data_paths)
|
|
|
|
def test_large_load_masses_keep_distinct_limits(self) -> None:
|
|
front = self.assembly.masses["mass_friction_endstops_18"]
|
|
rear = self.assembly.masses["mass_friction_endstops_19"]
|
|
|
|
self.assertEqual(front.mass_kg, 170000.0)
|
|
self.assertEqual(front.xmin_m, 0.0)
|
|
self.assertEqual(front.xmax_m, 0.37)
|
|
self.assertEqual(rear.mass_kg, 90000.0)
|
|
self.assertEqual(rear.xmin_m, -0.72)
|
|
self.assertEqual(rear.xmax_m, 0.0)
|
|
|
|
def test_elastic_endstop_parameters_are_converted_to_si_units(self) -> None:
|
|
endstop = self.assembly.elastic_endstops["elasticendstop_8"]
|
|
|
|
self.assertEqual(endstop.gap_m, 0.0)
|
|
self.assertEqual(endstop.contact_stiffness_n_per_m, 1.0e11)
|
|
self.assertEqual(endstop.contact_damping_n_per_m_per_s, 1.0e11)
|
|
self.assertAlmostEqual(endstop.spring_diameter_m, 0.02)
|
|
self.assertAlmostEqual(endstop.wire_diameter_m, 0.002)
|
|
|
|
def test_mechanical_nodes_keep_port_count(self) -> None:
|
|
node = self.assembly.mechanical_nodes["dynamic_mechanical_node_alternative_2"]
|
|
|
|
self.assertEqual(node.port_count, 8)
|
|
self.assertEqual(node.sum_mode, 1)
|
|
self.assertIn("dynamic_mechanical_node_alternative_2", self.assembly.aliases)
|
|
|
|
|
|
def test_all_piston_geometry_observables_match_amesim_final_sample(self) -> None:
|
|
for alias, piston in self.assembly.pistons.items():
|
|
with self.subTest(alias=alias):
|
|
geometry = piston.geometry()
|
|
x4 = self.amesim_results.series(f"x4@{alias}")[-1]
|
|
x5 = self.amesim_results.series(f"x5@{alias}")[-1]
|
|
v4 = self.amesim_results.series(f"v4@{alias}")[-1]
|
|
v5 = self.amesim_results.series(f"v5@{alias}")[-1]
|
|
|
|
self.assertAlmostEqual(
|
|
geometry.chamber_length_mm(x4, x5),
|
|
self.amesim_results.series(f"length@{alias}")[-1],
|
|
)
|
|
self.assertAlmostEqual(
|
|
geometry.chamber_volume_cm3(x4, x5),
|
|
self.amesim_results.series(f"vol1@{alias}")[-1],
|
|
delta=1.0e-6,
|
|
)
|
|
self.assertAlmostEqual(
|
|
geometry.chamber_volume_rate_l_min(v4, v5),
|
|
self.amesim_results.series(f"vvol1@{alias}")[-1],
|
|
delta=1.0e-8,
|
|
)
|
|
|
|
def test_all_elastic_endstop_forces_match_amesim_final_sample(self) -> None:
|
|
for endstop_alias, endstop_spec in self.assembly.elastic_endstops.items():
|
|
piston_alias = endstop_alias.replace("elasticendstop", "pn_brp2")
|
|
with self.subTest(alias=endstop_alias):
|
|
endstop = endstop_spec.endstop()
|
|
gap_mm = self.amesim_results.series(f"gap@{endstop_alias}")[-1]
|
|
penetration_velocity_m_s = self.amesim_results.series(f"v5@{piston_alias}")[-1]
|
|
|
|
self.assertAlmostEqual(
|
|
endstop.contact_force(gap_mm, penetration_velocity_m_s),
|
|
self.amesim_results.series(f"f1@{endstop_alias}")[-1],
|
|
delta=1.0e-3,
|
|
)
|
|
|
|
def test_mass_endstop_second_port_observables_are_opposite_sign(self) -> None:
|
|
for alias in self.assembly.masses:
|
|
with self.subTest(alias=alias):
|
|
for signal in ("x1", "v1", "acc1"):
|
|
primary = self.amesim_results.series(f"{signal}@{alias}")
|
|
second_port = self.amesim_results.series(f"{signal}dup@{alias}")
|
|
for index in (0, len(primary) // 2, -1):
|
|
self.assertAlmostEqual(primary[index], -second_port[index])
|
|
|
|
def test_mass_endstop_force_observables_match_inactive_amesim_contacts(self) -> None:
|
|
for alias, mass_spec in self.assembly.masses.items():
|
|
endstop = mass_spec.endstop()
|
|
with self.subTest(alias=alias):
|
|
x1 = self.amesim_results.series(f"x1@{alias}")[-1]
|
|
v1 = self.amesim_results.series(f"v1@{alias}")[-1]
|
|
|
|
self.assertAlmostEqual(
|
|
endstop.viscous_friction_force(v1),
|
|
self.amesim_results.series(f"Fvisc@{alias}")[-1],
|
|
)
|
|
self.assertAlmostEqual(self.amesim_results.series(f"Ffric@{alias}")[-1], 0.0)
|
|
self.assertAlmostEqual(self.amesim_results.series(f"Fmin@{alias}")[-1], 0.0)
|
|
self.assertAlmostEqual(self.amesim_results.series(f"Fmax@{alias}")[-1], 0.0)
|
|
self.assertLessEqual(endstop.lower_penetration_m(x1), 1.0e-12)
|
|
self.assertLessEqual(endstop.upper_penetration_m(x1), 1.0e-12)
|
|
|
|
def test_unit_helpers(self) -> None:
|
|
self.assertAlmostEqual(mm_to_m(20.0), 0.02)
|
|
self.assertAlmostEqual(n_per_mm_to_n_per_m(1.0e6), 1.0e9)
|
|
self.assertAlmostEqual(n_per_mm_per_s_to_n_per_m_per_s(10.0), 1.0e4)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|