from __future__ import annotations import unittest from app.simulation.components.amesim.mechanical.translational import ( AmesimF000, AmesimForc, AmesimMecmas21, AmesimLstp00a, AmesimLmechn1, ) from app.simulation.core.medium import IdealGasMedium from app.simulation.registry import COMPONENT_MODEL_REGISTRY class AmesimMechanicalPublicComponentTests(unittest.TestCase): def setUp(self) -> None: self.medium = IdealGasMedium() def test_mecmas21_catalog_ports_follow_amesim_icon_sides(self) -> None: self.assertEqual( tuple((port.name, port.side) for port in AmesimMecmas21.DISPLAY.ports), (("port_2", "left"), ("port_1", "right")), ) def test_forc_direction_only_accepts_positive_or_negative_one(self) -> None: definition = AmesimForc.PARAMETERS[0] self.assertEqual(AmesimForc.MODEL_VERSION, "0.2.0") self.assertEqual(definition.name, "direction") self.assertEqual(definition.default, 1.0) self.assertEqual( tuple((option.value, option.label) for option in definition.options), ((1.0, "正向"), (-1.0, "反向")), ) with self.assertRaisesRegex(ValueError, "must be one of 1, -1"): AmesimForc("invalid_force", direction=0.0) def test_mecmas21_choice_metadata_preserves_amesim_codes(self) -> None: definitions = { definition.name: definition for definition in AmesimMecmas21.PARAMETERS } expected_options = { "useFriction": ((1.0, "否"), (2.0, "是")), "stoptype": ( (1.0, "理想限位"), (2.0, "弹性限位"), (3.0, "恢复碰撞"), (4.0, "无限位"), ), "discContactOption": ( (1.0, "允许负接触力"), (2.0, "不允许负接触力"), ), "strib": ((1.0, "否"), (2.0, "是")), "frictionType": ((1.0, "简单"), (2.0, "高级")), } self.assertEqual(definitions["useFriction"].default, 2.0) self.assertEqual(definitions["stoptype"].default, 1.0) for name, options in expected_options.items(): with self.subTest(name=name): definition = definitions[name] self.assertEqual(definition.editor, "choice") self.assertEqual( tuple((option.value, option.label) for option in definition.options), options, ) self.assertIn("尚未实现", definitions["frictionType"].description) self.assertIn("尚未实现", definitions["strib"].description) self.assertEqual(definitions["theta"].label, "倾角(度)") self.assertIn("+90°", definitions["theta"].description) self.assertIn("-90°", definitions["theta"].description) self.assertIn("重力分量", definitions["theta"].description) self.assertEqual(AmesimMecmas21.MODEL_VERSION, "0.2.0") def test_mecmas21_visibility_matches_amesim_parameter_dependencies(self) -> None: definitions = { definition.name: definition for definition in AmesimMecmas21.PARAMETERS } expected_visibility = { "fstick": ( ("stoptype", (1.0, 2.0, 4.0)), ("useFriction", (2.0,)), ), "fcoul": ( ("stoptype", (1.0, 2.0, 4.0)), ("useFriction", (2.0,)), ), "rvisc": (("useFriction", (2.0,)),), "wind": (("useFriction", (2.0,)),), "dvel": ( ("stoptype", (1.0, 2.0, 4.0)), ("useFriction", (2.0,)), ("frictionType", (2.0,)), ), "restdvel": (("stoptype", (3.0,)),), "restcoeff": (("stoptype", (3.0,)),), "astrib": ( ("stoptype", (1.0, 2.0, 4.0)), ("useFriction", (2.0,)), ("frictionType", (2.0,)), ("strib", (2.0,)), ), "xmin": (("stoptype", (1.0, 2.0, 3.0)),), "Kbmin": (("stoptype", (2.0,)),), "Dbmin": (("stoptype", (2.0,)),), "Pdmin": (("stoptype", (2.0,)),), "xmax": (("stoptype", (1.0, 2.0, 3.0)),), "Kbmax": (("stoptype", (2.0,)),), "Dbmax": (("stoptype", (2.0,)),), "Pdmax": (("stoptype", (2.0,)),), "discContactOption": (("stoptype", (2.0,)),), "strib": ( ("stoptype", (1.0, 2.0, 4.0)), ("useFriction", (2.0,)), ("frictionType", (2.0,)), ), "frictionType": ( ("stoptype", (1.0, 2.0, 4.0)), ("useFriction", (2.0,)), ), } for name, expected in expected_visibility.items(): with self.subTest(name=name): self.assertEqual( tuple( (condition.parameter, condition.values) for condition in definitions[name].visible_when ), expected, ) for name in ("mass", "theta", "useFriction", "stoptype", "v0", "x0"): with self.subTest(always_visible=name): self.assertEqual(definitions[name].visible_when, ()) def test_lmechn1_defaults_to_two_right_ports(self) -> None: node = AmesimLmechn1("node_default", self.medium) self.assertEqual(node.v1, 2) self.assertEqual(node.active_ports, ("port_1", "port_2", "port_3")) def test_lmechn1_registry_rejects_fractional_integer_options(self) -> None: with self.assertRaisesRegex(ValueError, "v1 must be an integer"): COMPONENT_MODEL_REGISTRY["amesim_lmechn1"].create( "node_1", self.medium, {"v1": 2.5}, ) def test_lmechn1_supports_twenty_right_ports(self) -> None: node = AmesimLmechn1("node_20", self.medium, v1=20.0, sum=1.0) self.assertEqual( node.active_ports, tuple(f"port_{index}" for index in range(1, 22)), ) with self.assertRaisesRegex(ValueError, "must be at most 20"): AmesimLmechn1("node_21", self.medium, v1=21.0, sum=1.0) def test_mecmas21_registry_rejects_fractional_choice_value(self) -> None: with self.assertRaisesRegex(ValueError, "must be one of"): COMPONENT_MODEL_REGISTRY["amesim_mecmas21"].create( "mass_1", self.medium, {"useFriction": 0.5}, ) def test_mecmas21_registry_rejects_unknown_integer_options(self) -> None: invalid_options = { "useFriction": 0.0, "stoptype": 0.0, "discContactOption": 3.0, "strib": 0.0, "frictionType": 3.0, } for name, value in invalid_options.items(): with self.subTest(name=name): with self.assertRaisesRegex(ValueError, "must be one of"): COMPONENT_MODEL_REGISTRY["amesim_mecmas21"].create( "mass_1", self.medium, {name: value}, ) def test_lstp00a_registry_rejects_invalid_choice_values(self) -> None: invalid_options = { "stiffmode": (0.0, 1.5, 3.0), "discContactOption": (0.0, 1.5, 3.0), } for name, values in invalid_options.items(): for value in values: with self.subTest(name=name, value=value): with self.assertRaisesRegex( ValueError, "must be (?:one of|at least|at most)", ): COMPONENT_MODEL_REGISTRY["amesim_lstp00a"].create( "contact_1", self.medium, {name: value}, ) if __name__ == "__main__": unittest.main()