396 lines
14 KiB
Python
396 lines
14 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from app.simulation.reporting.amesim_results import AmesimResults
|
|
from app.simulation.reporting.test_mql_variables import (
|
|
TestMqlVariableBinding,
|
|
TestMqlVariableCatalog,
|
|
build_test_mql_variable_catalog,
|
|
)
|
|
from app.simulation.examples.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 TestMqlForceSourceObservation:
|
|
time: float
|
|
force_n: float
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TestMqlForceConnectorObservation:
|
|
time: float
|
|
force_n: float
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TestMqlMechanicalNodeObservation:
|
|
time: float
|
|
velocities_m_s: dict[int, float]
|
|
displacements_m: dict[int, float]
|
|
total_force_n: 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 TestMqlForceSourceObservationBinding:
|
|
alias: str
|
|
force_path: str
|
|
|
|
def observation_at(self, results: AmesimResults, index: int) -> TestMqlForceSourceObservation:
|
|
return TestMqlForceSourceObservation(
|
|
time=results.times[index],
|
|
force_n=results.series(self.force_path)[index],
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TestMqlForceConnectorObservationBinding:
|
|
alias: str
|
|
force_path: str
|
|
|
|
def observation_at(self, results: AmesimResults, index: int) -> TestMqlForceConnectorObservation:
|
|
return TestMqlForceConnectorObservation(
|
|
time=results.times[index],
|
|
force_n=results.series(self.force_path)[index],
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TestMqlMechanicalNodeObservationBinding:
|
|
alias: str
|
|
velocity_paths_by_port: dict[int, str]
|
|
displacement_paths_by_port: dict[int, str]
|
|
total_force_path: str
|
|
|
|
def observation_at(self, results: AmesimResults, index: int) -> TestMqlMechanicalNodeObservation:
|
|
return TestMqlMechanicalNodeObservation(
|
|
time=results.times[index],
|
|
velocities_m_s={
|
|
port: results.series(path)[index]
|
|
for port, path in self.velocity_paths_by_port.items()
|
|
},
|
|
displacements_m={
|
|
port: results.series(path)[index]
|
|
for port, path in self.displacement_paths_by_port.items()
|
|
},
|
|
total_force_n=results.series(self.total_force_path)[index],
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TestMqlMechanicalObservationCatalog:
|
|
pistons: dict[str, TestMqlPistonObservationBinding]
|
|
masses: dict[str, TestMqlMassEndstopObservationBinding]
|
|
elastic_endstops: dict[str, TestMqlElasticEndstopObservationBinding]
|
|
zero_force_sources: dict[str, TestMqlForceSourceObservationBinding]
|
|
force_connectors: dict[str, TestMqlForceConnectorObservationBinding]
|
|
mechanical_nodes: dict[str, TestMqlMechanicalNodeObservationBinding]
|
|
|
|
@property
|
|
def binding_count(self) -> int:
|
|
return (
|
|
len(self.pistons)
|
|
+ len(self.masses)
|
|
+ len(self.elastic_endstops)
|
|
+ len(self.zero_force_sources)
|
|
+ len(self.force_connectors)
|
|
+ len(self.mechanical_nodes)
|
|
)
|
|
|
|
|
|
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
|
|
},
|
|
zero_force_sources={
|
|
alias: _build_zero_force_source_binding(alias, variable_catalog)
|
|
for alias in mechanical_assembly.zero_force_sources
|
|
},
|
|
force_connectors={
|
|
alias: _build_force_connector_binding(alias, variable_catalog)
|
|
for alias in mechanical_assembly.force_connectors
|
|
},
|
|
mechanical_nodes={
|
|
alias: _build_mechanical_node_binding(alias, variable_catalog)
|
|
for alias in mechanical_assembly.mechanical_nodes
|
|
},
|
|
)
|
|
|
|
|
|
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 _build_zero_force_source_binding(
|
|
alias: str,
|
|
variable_catalog: TestMqlVariableCatalog,
|
|
) -> TestMqlForceSourceObservationBinding:
|
|
variables = _owner_variables(variable_catalog, alias)
|
|
return TestMqlForceSourceObservationBinding(
|
|
alias=alias,
|
|
force_path=_required_path(variables, "fzero", expected_units="N"),
|
|
)
|
|
|
|
|
|
def _build_force_connector_binding(
|
|
alias: str,
|
|
variable_catalog: TestMqlVariableCatalog,
|
|
) -> TestMqlForceConnectorObservationBinding:
|
|
variables = _owner_variables(variable_catalog, alias)
|
|
return TestMqlForceConnectorObservationBinding(
|
|
alias=alias,
|
|
force_path=_required_path(variables, "force", expected_units="N"),
|
|
)
|
|
|
|
|
|
def _build_mechanical_node_binding(
|
|
alias: str,
|
|
variable_catalog: TestMqlVariableCatalog,
|
|
) -> TestMqlMechanicalNodeObservationBinding:
|
|
variables = _owner_variables(variable_catalog, alias)
|
|
velocity_paths_by_port = {}
|
|
displacement_paths_by_port = {}
|
|
for port in range(1, 9):
|
|
velocity_paths_by_port[port] = _required_path(
|
|
variables,
|
|
f"p{port}__vt",
|
|
expected_units="m/s",
|
|
)
|
|
displacement_paths_by_port[port] = _required_path(
|
|
variables,
|
|
f"p{port}__xt",
|
|
expected_units="m",
|
|
)
|
|
return TestMqlMechanicalNodeObservationBinding(
|
|
alias=alias,
|
|
velocity_paths_by_port=velocity_paths_by_port,
|
|
displacement_paths_by_port=displacement_paths_by_port,
|
|
total_force_path=_required_path(variables, "tforce", expected_units="N"),
|
|
)
|
|
|
|
|
|
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}."
|
|
)
|