merge/model-development-into-main #2

Merged
lujingze merged 126 commits from merge/model-development-into-main into main 2026-07-31 09:52:44 +08:00
2 changed files with 203 additions and 2 deletions
Showing only changes of commit c84a809f77 - Show all commits

No files matched your search

@@ -50,6 +50,26 @@ class TestMqlElasticEndstopObservation:
stiffness_n_m: 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) @dataclass(frozen=True)
class TestMqlPistonObservationBinding: class TestMqlPistonObservationBinding:
alias: str alias: str
@@ -125,15 +145,71 @@ class TestMqlElasticEndstopObservationBinding:
) )
@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) @dataclass(frozen=True)
class TestMqlMechanicalObservationCatalog: class TestMqlMechanicalObservationCatalog:
pistons: dict[str, TestMqlPistonObservationBinding] pistons: dict[str, TestMqlPistonObservationBinding]
masses: dict[str, TestMqlMassEndstopObservationBinding] masses: dict[str, TestMqlMassEndstopObservationBinding]
elastic_endstops: dict[str, TestMqlElasticEndstopObservationBinding] elastic_endstops: dict[str, TestMqlElasticEndstopObservationBinding]
zero_force_sources: dict[str, TestMqlForceSourceObservationBinding]
force_connectors: dict[str, TestMqlForceConnectorObservationBinding]
mechanical_nodes: dict[str, TestMqlMechanicalNodeObservationBinding]
@property @property
def binding_count(self) -> int: def binding_count(self) -> int:
return len(self.pistons) + len(self.masses) + len(self.elastic_endstops) 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( def build_test_mql_mechanical_observation_catalog(
@@ -160,6 +236,18 @@ def build_test_mql_mechanical_observation_catalog(
alias: _build_elastic_endstop_binding(alias, variable_catalog) alias: _build_elastic_endstop_binding(alias, variable_catalog)
for alias in mechanical_assembly.elastic_endstops 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
},
) )
@@ -217,6 +305,54 @@ def _build_elastic_endstop_binding(
) )
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( def _owner_variables(
variable_catalog: TestMqlVariableCatalog, variable_catalog: TestMqlVariableCatalog,
alias: str, alias: str,
+66 -1
View File
@@ -23,7 +23,10 @@ class TestMqlMechanicalObservationCatalogTests(unittest.TestCase):
self.assertEqual(len(self.catalog.pistons), 8) self.assertEqual(len(self.catalog.pistons), 8)
self.assertEqual(len(self.catalog.masses), 10) self.assertEqual(len(self.catalog.masses), 10)
self.assertEqual(len(self.catalog.elastic_endstops), 8) self.assertEqual(len(self.catalog.elastic_endstops), 8)
self.assertEqual(self.catalog.binding_count, 26) self.assertEqual(len(self.catalog.zero_force_sources), 16)
self.assertEqual(len(self.catalog.force_connectors), 2)
self.assertEqual(len(self.catalog.mechanical_nodes), 2)
self.assertEqual(self.catalog.binding_count, 46)
def test_piston_binding_exposes_amesim_data_paths(self) -> None: def test_piston_binding_exposes_amesim_data_paths(self) -> None:
binding = self.catalog.pistons["pn_brp2_8"] binding = self.catalog.pistons["pn_brp2_8"]
@@ -119,6 +122,68 @@ class TestMqlMechanicalObservationCatalogTests(unittest.TestCase):
1.0e-9, 1.0e-9,
) )
def test_zero_force_source_observations_are_zero(self) -> None:
for binding in self.catalog.zero_force_sources.values():
force = self.amesim_results.series(binding.force_path)
with self.subTest(alias=binding.alias):
self.assertEqual(binding.force_path, f"fzero@{binding.alias}")
self.assertEqual(min(force), 0.0)
self.assertEqual(max(force), 0.0)
self.assertEqual(binding.observation_at(self.amesim_results, -1).force_n, 0.0)
def test_force_connector_observations_match_amesim_samples(self) -> None:
force_1 = self.catalog.force_connectors["forcecon_1"].observation_at(
self.amesim_results,
-1,
)
force_2 = self.catalog.force_connectors["forcecon_2"].observation_at(
self.amesim_results,
-1,
)
self.assertEqual(self.catalog.force_connectors["forcecon_1"].force_path, "force@forcecon_1")
self.assertEqual(self.catalog.force_connectors["forcecon_2"].force_path, "force@forcecon_2")
self.assertAlmostEqual(force_1.force_n, 49000.0)
self.assertAlmostEqual(force_2.force_n, 0.0)
self.assertAlmostEqual(
self.amesim_results.series("force@forcecon_1")[0],
1.0e17,
)
self.assertAlmostEqual(
self.amesim_results.series("force@forcecon_2")[0],
1.0e12,
)
def test_mechanical_node_bindings_expose_all_ports(self) -> None:
node = self.catalog.mechanical_nodes["dynamic_mechanical_node_alternative_2"]
self.assertEqual(sorted(node.velocity_paths_by_port), list(range(1, 9)))
self.assertEqual(sorted(node.displacement_paths_by_port), list(range(1, 9)))
self.assertEqual(node.velocity_paths_by_port[1], "p1__vt@dynamic_mechanical_node_alternative_2")
self.assertEqual(node.displacement_paths_by_port[8], "p8__xt@dynamic_mechanical_node_alternative_2")
self.assertEqual(node.total_force_path, "tforce@dynamic_mechanical_node_alternative_2")
def test_mechanical_node_observations_match_amesim_final_samples(self) -> None:
front = self.catalog.mechanical_nodes["dynamic_mechanical_node_alternative_2"].observation_at(
self.amesim_results,
-1,
)
rear = self.catalog.mechanical_nodes["dynamic_mechanical_node_alternative_3"].observation_at(
self.amesim_results,
-1,
)
self.assertEqual(set(front.velocities_m_s), set(range(1, 9)))
self.assertEqual(set(front.displacements_m), set(range(1, 9)))
self.assertAlmostEqual(front.velocities_m_s[1], -0.0)
self.assertAlmostEqual(front.displacements_m[1], -0.3700000000000001)
self.assertAlmostEqual(front.displacements_m[8], -0.3700000000000001)
self.assertAlmostEqual(front.total_force_n, 1083272.2763054767)
self.assertAlmostEqual(rear.velocities_m_s[1], 0.0)
self.assertAlmostEqual(rear.displacements_m[1], -0.72)
self.assertAlmostEqual(rear.displacements_m[8], -0.72)
self.assertAlmostEqual(rear.total_force_n, 1083272.2762592242)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()