from __future__ import annotations from dataclasses import dataclass from PythonModels.components.amesim_mechanical import ( AmesimPistonGeometry, circular_area, mm_to_m, ) from PythonModels.reporting.amesim_results import AmesimResults from PythonModels.reporting.test_mql_variables import ( TestMqlVariableCatalog, build_test_mql_variable_catalog, ) from PythonModels.systems.test_mql_config import TestMqlConfig, TestMqlResolvedComponent MM_TO_M = 1.0e-3 N_PER_MM_TO_N_PER_M = 1.0e3 N_PER_MM_PER_S_TO_N_PER_M_PER_S = 1.0e3 @dataclass(frozen=True) class TestMqlPistonSpec: alias: str piston_diameter_m: float rod_diameter_m: float zero_displacement_m: float piston_area_m2: float rod_area_m2: float annulus_area_m2: float data_paths: tuple[str, ...] @dataclass(frozen=True) class TestMqlMassEndstopSpec: alias: str mass_kg: float xmin_m: float xmax_m: float min_stiffness_n_per_m: float max_stiffness_n_per_m: float min_damping_n_per_m_per_s: float max_damping_n_per_m_per_s: float min_penetration_m: float max_penetration_m: float rest_coeff: float data_paths: tuple[str, ...] @dataclass(frozen=True) class TestMqlElasticEndstopSpec: alias: str gap_m: float contact_stiffness_n_per_m: float contact_damping_n_per_m_per_s: float spring_diameter_m: float wire_diameter_m: float data_paths: tuple[str, ...] @dataclass(frozen=True) class TestMqlMechanicalNodeSpec: alias: str port_count: int sum_mode: int data_paths: tuple[str, ...] @dataclass(frozen=True) class TestMqlMechanicalAssembly: pistons: dict[str, TestMqlPistonSpec] masses: dict[str, TestMqlMassEndstopSpec] elastic_endstops: dict[str, TestMqlElasticEndstopSpec] mechanical_nodes: dict[str, TestMqlMechanicalNodeSpec] zero_force_sources: tuple[str, ...] force_connectors: tuple[str, ...] @property def component_count(self) -> int: return ( len(self.pistons) + len(self.masses) + len(self.elastic_endstops) + len(self.mechanical_nodes) + len(self.zero_force_sources) + len(self.force_connectors) ) @property def aliases(self) -> tuple[str, ...]: return tuple( [ *self.pistons, *self.masses, *self.elastic_endstops, *self.mechanical_nodes, *self.zero_force_sources, *self.force_connectors, ] ) def build_test_mql_mechanical_assembly( config: TestMqlConfig | None = None, amesim_results: AmesimResults | None = None, variable_catalog: TestMqlVariableCatalog | None = None, ) -> TestMqlMechanicalAssembly: config = config or TestMqlConfig.from_amesim_specs() if variable_catalog is None and amesim_results is not None: variable_catalog = build_test_mql_variable_catalog(amesim_results) pistons = { component.alias: _build_piston(component, variable_catalog) for component in config.components_by_submodel("PNRP17") } masses = { component.alias: _build_mass(component, variable_catalog) for component in config.components_by_submodel("MECMAS21") } elastic_endstops = { component.alias: _build_elastic_endstop(component, variable_catalog) for component in config.components_by_submodel("LSTP00A") } mechanical_nodes = { component.alias: _build_mechanical_node(component, variable_catalog) for component in config.components_by_submodel("LMECHN1") } zero_force_sources = tuple(component.alias for component in config.components_by_submodel("F000")) force_connectors = tuple(component.alias for component in config.components_by_submodel("FORC")) return TestMqlMechanicalAssembly( pistons=pistons, masses=masses, elastic_endstops=elastic_endstops, mechanical_nodes=mechanical_nodes, zero_force_sources=zero_force_sources, force_connectors=force_connectors, ) def _build_piston( component: TestMqlResolvedComponent, variable_catalog: TestMqlVariableCatalog | None, ) -> TestMqlPistonSpec: geometry = AmesimPistonGeometry( piston_diameter_m=mm_to_m(component.parameter_value("dp")), rod_diameter_m=mm_to_m(component.parameter_value("dr")), zero_length_m=mm_to_m(component.parameter_value("x0")), ) return TestMqlPistonSpec( alias=component.alias, piston_diameter_m=geometry.piston_diameter_m, rod_diameter_m=geometry.rod_diameter_m, zero_displacement_m=geometry.zero_length_m, piston_area_m2=geometry.piston_area_m2, rod_area_m2=geometry.rod_area_m2, annulus_area_m2=geometry.annulus_area_m2, data_paths=_data_paths(variable_catalog, component.alias), ) def _build_mass( component: TestMqlResolvedComponent, variable_catalog: TestMqlVariableCatalog | None, ) -> TestMqlMassEndstopSpec: return TestMqlMassEndstopSpec( alias=component.alias, mass_kg=component.parameter_value("mass"), xmin_m=component.parameter_value("xmin"), xmax_m=component.parameter_value("xmax"), min_stiffness_n_per_m=n_per_mm_to_n_per_m(component.parameter_value("Kbmin")), max_stiffness_n_per_m=n_per_mm_to_n_per_m(component.parameter_value("Kbmax")), min_damping_n_per_m_per_s=n_per_mm_per_s_to_n_per_m_per_s(component.parameter_value("Dbmin")), max_damping_n_per_m_per_s=n_per_mm_per_s_to_n_per_m_per_s(component.parameter_value("Dbmax")), min_penetration_m=mm_to_m(component.parameter_value("Pdmin")), max_penetration_m=mm_to_m(component.parameter_value("Pdmax")), rest_coeff=component.parameter_value("restcoeff"), data_paths=_data_paths(variable_catalog, component.alias), ) def _build_elastic_endstop( component: TestMqlResolvedComponent, variable_catalog: TestMqlVariableCatalog | None, ) -> TestMqlElasticEndstopSpec: return TestMqlElasticEndstopSpec( alias=component.alias, gap_m=mm_to_m(component.parameter_value("gap0")), contact_stiffness_n_per_m=component.parameter_value("kcont"), contact_damping_n_per_m_per_s=component.parameter_value("rcont"), spring_diameter_m=mm_to_m(component.parameter_value("sdiam")), wire_diameter_m=mm_to_m(component.parameter_value("wdiam")), data_paths=_data_paths(variable_catalog, component.alias), ) def _build_mechanical_node( component: TestMqlResolvedComponent, variable_catalog: TestMqlVariableCatalog | None, ) -> TestMqlMechanicalNodeSpec: return TestMqlMechanicalNodeSpec( alias=component.alias, port_count=int(component.parameter_value("v1")), sum_mode=int(component.parameter_value("sum")), data_paths=_data_paths(variable_catalog, component.alias), ) def n_per_mm_to_n_per_m(value: float) -> float: return value * N_PER_MM_TO_N_PER_M def n_per_mm_per_s_to_n_per_m_per_s(value: float) -> float: return value * N_PER_MM_PER_S_TO_N_PER_M_PER_S def _data_paths( variable_catalog: TestMqlVariableCatalog | None, alias: str, ) -> tuple[str, ...]: if variable_catalog is None: return () return variable_catalog.data_paths_for_owner(alias)