补充test_mql机械观测目录

This commit is contained in:
huojiarong committed 2026-07-16 02:06:10 +00:00
1 parent 6f9e7084dd
commit f6871aefac
2 files changed
+383

No files matched your search

@@ -0,0 +1,259 @@
from __future__ import annotations
from dataclasses import dataclass
from PythonModels.reporting.amesim_results import AmesimResults
from PythonModels.reporting.test_mql_variables import (
TestMqlVariableBinding,
TestMqlVariableCatalog,
build_test_mql_variable_catalog,
)
from PythonModels.systems.test_mql_mechanical import (
TestMqlMechanicalAssembly,
build_test_mql_mechanical_assembly,
)
@dataclass(frozen=True)
class TestMqlPistonObservation:
time: float
chamber_volume_cm3: float
chamber_volume_rate_l_min: float
chamber_length_mm: float
force_port_2_n: float
force_port_3_n: float
displacement_port_2_m: float
velocity_port_2_m_s: float
displacement_port_3_m: float
velocity_port_3_m_s: float
@dataclass(frozen=True)
class TestMqlMassEndstopObservation:
time: float
displacement_m: float
velocity_m_s: float
acceleration_m_s2: float
lower_contact_force_n: float
upper_contact_force_n: float
viscous_friction_force_n: float
dry_friction_force_n: float
stick_flag: float
@dataclass(frozen=True)
class TestMqlElasticEndstopObservation:
time: float
force_n: float
duplicate_force_n: float
gap_mm: float
stiffness_n_m: float
@dataclass(frozen=True)
class TestMqlPistonObservationBinding:
alias: str
volume_path: str
volume_rate_path: str
length_path: str
force_port_2_path: str
force_port_3_path: str
displacement_port_2_path: str
velocity_port_2_path: str
displacement_port_3_path: str
velocity_port_3_path: str
def observation_at(self, results: AmesimResults, index: int) -> TestMqlPistonObservation:
return TestMqlPistonObservation(
time=results.times[index],
chamber_volume_cm3=results.series(self.volume_path)[index],
chamber_volume_rate_l_min=results.series(self.volume_rate_path)[index],
chamber_length_mm=results.series(self.length_path)[index],
force_port_2_n=results.series(self.force_port_2_path)[index],
force_port_3_n=results.series(self.force_port_3_path)[index],
displacement_port_2_m=results.series(self.displacement_port_2_path)[index],
velocity_port_2_m_s=results.series(self.velocity_port_2_path)[index],
displacement_port_3_m=results.series(self.displacement_port_3_path)[index],
velocity_port_3_m_s=results.series(self.velocity_port_3_path)[index],
)
@dataclass(frozen=True)
class TestMqlMassEndstopObservationBinding:
alias: str
displacement_path: str
velocity_path: str
acceleration_path: str
displacement_duplicate_path: str
velocity_duplicate_path: str
acceleration_duplicate_path: str
lower_contact_force_path: str
upper_contact_force_path: str
viscous_friction_force_path: str
dry_friction_force_path: str
stick_flag_path: str
def observation_at(self, results: AmesimResults, index: int) -> TestMqlMassEndstopObservation:
return TestMqlMassEndstopObservation(
time=results.times[index],
displacement_m=results.series(self.displacement_path)[index],
velocity_m_s=results.series(self.velocity_path)[index],
acceleration_m_s2=results.series(self.acceleration_path)[index],
lower_contact_force_n=results.series(self.lower_contact_force_path)[index],
upper_contact_force_n=results.series(self.upper_contact_force_path)[index],
viscous_friction_force_n=results.series(self.viscous_friction_force_path)[index],
dry_friction_force_n=results.series(self.dry_friction_force_path)[index],
stick_flag=results.series(self.stick_flag_path)[index],
)
@dataclass(frozen=True)
class TestMqlElasticEndstopObservationBinding:
alias: str
force_path: str
duplicate_force_path: str
gap_path: str
stiffness_path: str
def observation_at(self, results: AmesimResults, index: int) -> TestMqlElasticEndstopObservation:
return TestMqlElasticEndstopObservation(
time=results.times[index],
force_n=results.series(self.force_path)[index],
duplicate_force_n=results.series(self.duplicate_force_path)[index],
gap_mm=results.series(self.gap_path)[index],
stiffness_n_m=results.series(self.stiffness_path)[index],
)
@dataclass(frozen=True)
class TestMqlMechanicalObservationCatalog:
pistons: dict[str, TestMqlPistonObservationBinding]
masses: dict[str, TestMqlMassEndstopObservationBinding]
elastic_endstops: dict[str, TestMqlElasticEndstopObservationBinding]
@property
def binding_count(self) -> int:
return len(self.pistons) + len(self.masses) + len(self.elastic_endstops)
def build_test_mql_mechanical_observation_catalog(
results: AmesimResults,
*,
variable_catalog: TestMqlVariableCatalog | None = None,
mechanical_assembly: TestMqlMechanicalAssembly | None = None,
) -> TestMqlMechanicalObservationCatalog:
variable_catalog = variable_catalog or build_test_mql_variable_catalog(results)
mechanical_assembly = mechanical_assembly or build_test_mql_mechanical_assembly(
amesim_results=results,
variable_catalog=variable_catalog,
)
return TestMqlMechanicalObservationCatalog(
pistons={
alias: _build_piston_binding(alias, variable_catalog)
for alias in mechanical_assembly.pistons
},
masses={
alias: _build_mass_binding(alias, variable_catalog)
for alias in mechanical_assembly.masses
},
elastic_endstops={
alias: _build_elastic_endstop_binding(alias, variable_catalog)
for alias in mechanical_assembly.elastic_endstops
},
)
def _build_piston_binding(
alias: str,
variable_catalog: TestMqlVariableCatalog,
) -> TestMqlPistonObservationBinding:
variables = _owner_variables(variable_catalog, alias)
return TestMqlPistonObservationBinding(
alias=alias,
volume_path=_required_path(variables, "vol1", expected_units="cm**3"),
volume_rate_path=_required_path(variables, "vvol1", expected_units="L/min"),
length_path=_required_path(variables, "length", expected_units="mm"),
force_port_2_path=_required_path(variables, "f2", expected_units="N"),
force_port_3_path=_required_path(variables, "f3", expected_units="N"),
displacement_port_2_path=_required_path(variables, "x5", expected_units="m"),
velocity_port_2_path=_required_path(variables, "v5", expected_units="m/s"),
displacement_port_3_path=_required_path(variables, "x4", expected_units="m"),
velocity_port_3_path=_required_path(variables, "v4", expected_units="m/s"),
)
def _build_mass_binding(
alias: str,
variable_catalog: TestMqlVariableCatalog,
) -> TestMqlMassEndstopObservationBinding:
variables = _owner_variables(variable_catalog, alias)
return TestMqlMassEndstopObservationBinding(
alias=alias,
displacement_path=_required_path(variables, "x1", expected_units="m"),
velocity_path=_required_path(variables, "v1", expected_units="m/s"),
acceleration_path=_required_path(variables, "acc1", expected_units="m/s/s"),
displacement_duplicate_path=_required_path(variables, "x1dup", expected_units="m"),
velocity_duplicate_path=_required_path(variables, "v1dup", expected_units="m/s"),
acceleration_duplicate_path=_required_path(variables, "acc1dup", expected_units="m/s/s"),
lower_contact_force_path=_required_path(variables, "Fmin", expected_units="N"),
upper_contact_force_path=_required_path(variables, "Fmax", expected_units="N"),
viscous_friction_force_path=_required_path(variables, "Fvisc", expected_units="N"),
dry_friction_force_path=_required_path(variables, "Ffric", expected_units="N"),
stick_flag_path=_required_path(variables, "stick", expected_units=None),
)
def _build_elastic_endstop_binding(
alias: str,
variable_catalog: TestMqlVariableCatalog,
) -> TestMqlElasticEndstopObservationBinding:
variables = _owner_variables(variable_catalog, alias)
return TestMqlElasticEndstopObservationBinding(
alias=alias,
force_path=_required_path(variables, "f1", expected_units="N"),
duplicate_force_path=_required_path(variables, "f2", expected_units="N"),
gap_path=_required_path(variables, "gap", expected_units="mm"),
stiffness_path=_required_path(variables, "kval", expected_units="N/m"),
)
def _owner_variables(
variable_catalog: TestMqlVariableCatalog,
alias: str,
) -> tuple[TestMqlVariableBinding, ...]:
return tuple(
variable
for variable in variable_catalog.variables
if variable.owner_alias == alias
)
def _required_path(
variables: tuple[TestMqlVariableBinding, ...],
signal_name: str,
*,
expected_units: str | None,
) -> str:
variable = _single(
tuple(variable for variable in variables if variable.signal_name == signal_name),
signal_name,
)
_assert_units(variable, expected_units)
return variable.data_path
def _single(
matches: tuple[TestMqlVariableBinding, ...],
description: str,
) -> TestMqlVariableBinding:
if len(matches) != 1:
raise ValueError(f"Expected one {description} variable, found {len(matches)}.")
return matches[0]
def _assert_units(variable: TestMqlVariableBinding, expected_units: str | None) -> None:
if variable.units != expected_units:
raise ValueError(
f"Unexpected units for {variable.data_path}: "
f"{variable.units!r}, expected {expected_units!r}."
)
@@ -0,0 +1,124 @@
from __future__ import annotations
import unittest
from pathlib import Path
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
from PythonModels.reporting.test_mql_mechanical_observations import (
build_test_mql_mechanical_observation_catalog,
)
REPO_ROOT = Path(__file__).resolve().parents[1]
TEST_MQL_AME = REPO_ROOT / "AmesimModels" / "test_mql.ame"
class TestMqlMechanicalObservationCatalogTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.amesim_results = load_test_mql_amesim_results(TEST_MQL_AME)
cls.catalog = build_test_mql_mechanical_observation_catalog(cls.amesim_results)
def test_builds_expected_mechanical_observation_counts(self) -> None:
self.assertEqual(len(self.catalog.pistons), 8)
self.assertEqual(len(self.catalog.masses), 10)
self.assertEqual(len(self.catalog.elastic_endstops), 8)
self.assertEqual(self.catalog.binding_count, 26)
def test_piston_binding_exposes_amesim_data_paths(self) -> None:
binding = self.catalog.pistons["pn_brp2_8"]
self.assertEqual(binding.volume_path, "vol1@pn_brp2_8")
self.assertEqual(binding.volume_rate_path, "vvol1@pn_brp2_8")
self.assertEqual(binding.length_path, "length@pn_brp2_8")
self.assertEqual(binding.force_port_2_path, "f2@pn_brp2_8")
self.assertEqual(binding.force_port_3_path, "f3@pn_brp2_8")
self.assertEqual(binding.displacement_port_2_path, "x5@pn_brp2_8")
self.assertEqual(binding.velocity_port_2_path, "v5@pn_brp2_8")
self.assertEqual(binding.displacement_port_3_path, "x4@pn_brp2_8")
self.assertEqual(binding.velocity_port_3_path, "v4@pn_brp2_8")
def test_piston_observation_matches_amesim_final_sample(self) -> None:
observation = self.catalog.pistons["pn_brp2_8"].observation_at(
self.amesim_results,
-1,
)
self.assertAlmostEqual(observation.time, self.amesim_results.times[-1])
self.assertAlmostEqual(observation.chamber_volume_cm3, 34242.54636512914)
self.assertAlmostEqual(observation.chamber_volume_rate_l_min, 8.360141002531034e-07)
self.assertAlmostEqual(observation.chamber_length_mm, 1090.0013536465842)
self.assertAlmostEqual(observation.displacement_port_2_m, 0.3700013536465843)
self.assertAlmostEqual(observation.velocity_port_2_m_s, 4.435303434246023e-10)
self.assertAlmostEqual(observation.displacement_port_3_m, -0.72)
self.assertAlmostEqual(observation.velocity_port_3_m_s, 0.0)
def test_mass_binding_exposes_primary_and_reversed_duplicate_paths(self) -> None:
binding = self.catalog.masses["mass_friction_endstops_10"]
self.assertEqual(binding.displacement_path, "x1@mass_friction_endstops_10")
self.assertEqual(binding.velocity_path, "v1@mass_friction_endstops_10")
self.assertEqual(binding.acceleration_path, "acc1@mass_friction_endstops_10")
self.assertEqual(binding.displacement_duplicate_path, "x1dup@mass_friction_endstops_10")
self.assertEqual(binding.velocity_duplicate_path, "v1dup@mass_friction_endstops_10")
self.assertEqual(binding.acceleration_duplicate_path, "acc1dup@mass_friction_endstops_10")
self.assertEqual(binding.lower_contact_force_path, "Fmin@mass_friction_endstops_10")
self.assertEqual(binding.upper_contact_force_path, "Fmax@mass_friction_endstops_10")
self.assertEqual(binding.viscous_friction_force_path, "Fvisc@mass_friction_endstops_10")
self.assertEqual(binding.dry_friction_force_path, "Ffric@mass_friction_endstops_10")
self.assertEqual(binding.stick_flag_path, "stick@mass_friction_endstops_10")
def test_mass_observation_matches_amesim_final_sample(self) -> None:
observation = self.catalog.masses["mass_friction_endstops_10"].observation_at(
self.amesim_results,
-1,
)
self.assertAlmostEqual(observation.displacement_m, 0.3700013536465843)
self.assertAlmostEqual(observation.velocity_m_s, 4.435303434246023e-10)
self.assertAlmostEqual(observation.acceleration_m_s2, -3.5662378650158644e-07)
self.assertAlmostEqual(observation.lower_contact_force_n, 0.0)
self.assertAlmostEqual(observation.upper_contact_force_n, 0.0)
self.assertAlmostEqual(observation.viscous_friction_force_n, 0.0)
self.assertAlmostEqual(observation.dry_friction_force_n, 0.0)
self.assertAlmostEqual(observation.stick_flag, 0.0)
def test_mass_duplicate_series_are_sign_reversed(self) -> None:
for binding in self.catalog.masses.values():
with self.subTest(alias=binding.alias):
for primary_path, duplicate_path in (
(binding.displacement_path, binding.displacement_duplicate_path),
(binding.velocity_path, binding.velocity_duplicate_path),
(binding.acceleration_path, binding.acceleration_duplicate_path),
):
primary = self.amesim_results.series(primary_path)
duplicate = self.amesim_results.series(duplicate_path)
self.assertLess(
max(abs(a + b) for a, b in zip(primary, duplicate)),
1.0e-12,
)
def test_elastic_endstop_observation_matches_amesim_final_sample(self) -> None:
observation = self.catalog.elastic_endstops["elasticendstop_8"].observation_at(
self.amesim_results,
-1,
)
self.assertAlmostEqual(observation.force_n, 135409.0113969468)
self.assertAlmostEqual(observation.duplicate_force_n, 135409.0113969468)
self.assertAlmostEqual(observation.gap_mm, -0.0013536465842123313)
self.assertAlmostEqual(observation.stiffness_n_m, 100000000000.0)
def test_elastic_endstop_duplicate_force_matches_primary_series(self) -> None:
for binding in self.catalog.elastic_endstops.values():
force = self.amesim_results.series(binding.force_path)
duplicate = self.amesim_results.series(binding.duplicate_force_path)
with self.subTest(alias=binding.alias):
self.assertLess(
max(abs(a - b) for a, b in zip(force, duplicate)),
1.0e-9,
)
if __name__ == "__main__":
unittest.main()