342 lines
12 KiB
Python
342 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from PythonModels.components.amesim_mechanical import (
|
|
AmesimElasticEndstop,
|
|
AmesimMassFrictionEndstops,
|
|
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, ...]
|
|
|
|
def geometry(self) -> AmesimPistonGeometry:
|
|
return AmesimPistonGeometry(
|
|
piston_diameter_m=self.piston_diameter_m,
|
|
rod_diameter_m=self.rod_diameter_m,
|
|
zero_length_m=self.zero_displacement_m,
|
|
)
|
|
|
|
|
|
@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
|
|
stiction_force_n: float
|
|
coulomb_friction_n: float
|
|
viscous_friction_n_per_m_per_s: float
|
|
windage_n_per_m2_per_s2: float
|
|
stick_velocity_threshold_m_s: float
|
|
reset_velocity_threshold_m_s: float
|
|
rest_coeff: float
|
|
stribeck_constant_m_s: float
|
|
use_friction: bool
|
|
stop_type: int
|
|
initial_velocity_m_s: float
|
|
initial_displacement_m: float
|
|
data_paths: tuple[str, ...]
|
|
|
|
def endstop(self) -> AmesimMassFrictionEndstops:
|
|
return AmesimMassFrictionEndstops(
|
|
mass_kg=self.mass_kg,
|
|
lower_limit_m=self.xmin_m,
|
|
upper_limit_m=self.xmax_m,
|
|
lower_stiffness_n_per_m=self.min_stiffness_n_per_m,
|
|
upper_stiffness_n_per_m=self.max_stiffness_n_per_m,
|
|
lower_damping_n_per_m_per_s=self.min_damping_n_per_m_per_s,
|
|
upper_damping_n_per_m_per_s=self.max_damping_n_per_m_per_s,
|
|
viscous_friction_n_per_m_per_s=self.viscous_friction_n_per_m_per_s,
|
|
coulomb_friction_n=self.coulomb_friction_n,
|
|
stiction_force_n=self.stiction_force_n,
|
|
windage_n_per_m2_per_s2=self.windage_n_per_m2_per_s2,
|
|
)
|
|
|
|
|
|
@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, ...]
|
|
|
|
def endstop(self) -> AmesimElasticEndstop:
|
|
return AmesimElasticEndstop(
|
|
contact_stiffness_n_per_m=self.contact_stiffness_n_per_m,
|
|
contact_damping_n_per_m_per_s=self.contact_damping_n_per_m_per_s,
|
|
gap0_m=self.gap_m,
|
|
)
|
|
|
|
|
|
@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,
|
|
]
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TestMqlMechanicalMassState:
|
|
alias: str
|
|
velocity_m_s: float
|
|
displacement_m: float
|
|
|
|
def as_vector(self) -> list[float]:
|
|
return [self.velocity_m_s, self.displacement_m]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TestMqlMechanicalMassSnapshot:
|
|
states: tuple[TestMqlMechanicalMassState, ...]
|
|
|
|
@property
|
|
def state_count(self) -> int:
|
|
return 2 * len(self.states)
|
|
|
|
|
|
class TestMqlMechanicalMassClosure:
|
|
def __init__(self, assembly: TestMqlMechanicalAssembly) -> None:
|
|
self.assembly = assembly
|
|
self.mass_aliases = tuple(assembly.masses)
|
|
|
|
def initial_state_vector(self) -> list[float]:
|
|
state: list[float] = []
|
|
for alias in self.mass_aliases:
|
|
spec = self.assembly.masses[alias]
|
|
state.extend([spec.initial_velocity_m_s, spec.initial_displacement_m])
|
|
return state
|
|
|
|
def snapshot(self, state_vector: list[float] | None = None) -> TestMqlMechanicalMassSnapshot:
|
|
values = self.initial_state_vector() if state_vector is None else list(state_vector)
|
|
if len(values) != 2 * len(self.mass_aliases):
|
|
raise ValueError("mechanical mass state vector requires two values per mass")
|
|
states = tuple(
|
|
TestMqlMechanicalMassState(
|
|
alias=alias,
|
|
velocity_m_s=values[2 * index],
|
|
displacement_m=values[2 * index + 1],
|
|
)
|
|
for index, alias in enumerate(self.mass_aliases)
|
|
)
|
|
return TestMqlMechanicalMassSnapshot(states=states)
|
|
|
|
def rhs(self, state_vector: list[float]) -> list[float]:
|
|
snapshot = self.snapshot(state_vector)
|
|
derivatives: list[float] = []
|
|
for state in snapshot.states:
|
|
mass = self.assembly.masses[state.alias].endstop()
|
|
acceleration, velocity = mass.derivatives(
|
|
velocity_m_s=state.velocity_m_s,
|
|
displacement_m=state.displacement_m,
|
|
)
|
|
derivatives.extend([acceleration, velocity])
|
|
return derivatives
|
|
|
|
|
|
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, amesim_results)
|
|
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,
|
|
amesim_results: AmesimResults | 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")),
|
|
stiction_force_n=component.parameter_value("fstick"),
|
|
coulomb_friction_n=component.parameter_value("fcoul"),
|
|
viscous_friction_n_per_m_per_s=component.parameter_value("rvisc"),
|
|
windage_n_per_m2_per_s2=component.parameter_value("wind"),
|
|
stick_velocity_threshold_m_s=component.parameter_value("dvel"),
|
|
reset_velocity_threshold_m_s=component.parameter_value("restdvel"),
|
|
rest_coeff=component.parameter_value("restcoeff"),
|
|
stribeck_constant_m_s=component.parameter_value("astrib"),
|
|
use_friction=bool(int(component.parameter_value("useFriction"))),
|
|
stop_type=int(component.parameter_value("stoptype")),
|
|
initial_velocity_m_s=_initial_value(amesim_results, f"v1@{component.alias}"),
|
|
initial_displacement_m=_initial_value(amesim_results, f"x1@{component.alias}"),
|
|
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 _initial_value(amesim_results: AmesimResults | None, data_path: str) -> float:
|
|
if amesim_results is None:
|
|
return 0.0
|
|
return float(amesim_results.series(data_path)[0])
|
|
|
|
|
|
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)
|