From e87df789b2460633bbd23f20c5687627882a3eb4 Mon Sep 17 00:00:00 2001 From: huojiarong Date: Thu, 30 Jul 2026 10:34:29 +0000 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=20AMESim=20=E6=9C=BA?= =?UTF-8?q?=E6=A2=B0=E6=8E=A5=E8=A7=A6=E5=92=8C=E8=8A=82=E7=82=B9=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/simulation/components/amesim/library.py | 2 + .../amesim/mechanical/translational.py | 213 ++++++++++++++++++ docs/amesim-component-migration-matrix.md | 12 +- .../test_amesim_component_migration_matrix.py | 2 - ...est_amesim_mechanical_public_components.py | 66 ++++++ tests/test_amesim_mechanical_xml.py | 134 +++++++++++ tests/test_component_catalog.py | 17 +- tests/test_component_registry.py | 4 + 8 files changed, 441 insertions(+), 9 deletions(-) diff --git a/app/simulation/components/amesim/library.py b/app/simulation/components/amesim/library.py index 1a2f1ed..3c3d63f 100644 --- a/app/simulation/components/amesim/library.py +++ b/app/simulation/components/amesim/library.py @@ -28,6 +28,8 @@ LIBRARY = ComponentLibrarySpec( "app.simulation.components.amesim.mechanical.translational:AmesimF000", "app.simulation.components.amesim.mechanical.translational:AmesimForc", "app.simulation.components.amesim.mechanical.translational:AmesimMecmas21", + "app.simulation.components.amesim.mechanical.translational:AmesimLstp00a", + "app.simulation.components.amesim.mechanical.translational:AmesimLmechn1", "app.simulation.components.amesim.storage.chambers:AmesimPnch023", "app.simulation.components.amesim.storage.chambers:AmesimPnch012", "app.simulation.components.amesim.flow.orifices:AmesimPnor001", diff --git a/app/simulation/components/amesim/mechanical/translational.py b/app/simulation/components/amesim/mechanical/translational.py index 0a2efab..e6437ca 100644 --- a/app/simulation/components/amesim/mechanical/translational.py +++ b/app/simulation/components/amesim/mechanical/translational.py @@ -291,3 +291,216 @@ class AmesimMecmas21(DynamicComponent): "Fmin": self._lower_limit_force(), "Fmax": self._upper_limit_force(), } + + +class AmesimLstp00a(AlgebraicComponent): + """AMESim LSTP00A first public elastic contact component.""" + + MODEL_TYPE = "amesim_lstp00a" + MODEL_VERSION = "0.1.0" + PORTS = ( + PortDefinition.mechanical_translational("port_1"), + PortDefinition.mechanical_translational("port_2"), + ) + PARAMETERS = ( + ParameterDefinition("na", 10.0, label="有效圈数", quantity="dimensionless", unit="", minimum=0.0, minimum_exclusive=True), + ParameterDefinition("gap0", 0.0, label="初始间隙", quantity="length", unit="m"), + ParameterDefinition("kcont", 1.0e6, label="接触刚度", quantity="translational_stiffness", unit="N/m", minimum=0.0), + ParameterDefinition("G", 8.57e10, label="剪切模量", quantity="pressure", unit="Pa", minimum=0.0), + ParameterDefinition("sdiam", 0.02, label="弹簧直径", quantity="length", unit="m", minimum=0.0), + ParameterDefinition("wdiam", 0.002, label="线径", quantity="length", unit="m", minimum=0.0), + ParameterDefinition("rcont", 0.0, label="接触阻尼", quantity="translational_damping", unit="N/(m/s)", minimum=0.0), + ParameterDefinition("Pdis", 1.0e-7, label="满阻尼穿透", quantity="length", unit="m", minimum=0.0), + ParameterDefinition("stiffmode", 1.0, label="刚度模式", quantity="dimensionless", unit="", minimum=0.0), + ParameterDefinition("discContactOption", 1.0, label="接触选项", quantity="dimensionless", unit="", minimum=0.0), + ) + RESULT_VARIABLES = ( + ResultVariableDefinition("gap", "间隙", "length", "m", "derived", 10), + ResultVariableDefinition("penetration", "穿透", "length", "m", "derived", 20), + ResultVariableDefinition("force", "接触力", "force", "N", "derived", 30), + ) + DISPLAY = ComponentDisplaySpec( + label="LSTP00A 弹性接触", + library_id="amesim", + category_id="mechanical", + symbol="generic", + ports=( + PortDisplaySpec("port_1", "left", order=10), + PortDisplaySpec("port_2", "right", order=20), + ), + order=40, + ) + + def __init__(self, name: str, medium: IdealGasMedium, **parameters: float) -> None: + super().__init__(name=name) + resolved = { + definition.name: parameters.get(definition.name, definition.default) + for definition in self.PARAMETERS + } + self.set_parameter_values(resolved) + for name, value in resolved.items(): + setattr(self, name, float(value)) + self.port_1 = self.register_declared_port("port_1") + self.port_2 = self.register_declared_port("port_2") + + @classmethod + def create( + cls, + *, + name: str, + medium: IdealGasMedium, + parameters: Mapping[str, float], + ) -> "AmesimLstp00a": + for integer_name in ("stiffmode", "discContactOption"): + if not float(parameters[integer_name]).is_integer(): + raise ValueError(f"LSTP00A {integer_name} must be an integer.") + return cls(name=name, medium=medium, **dict(parameters)) + + @property + def gap(self) -> float: + return self.gap0 - (self.port_2.x - self.port_1.x) + + @property + def penetration(self) -> float: + return max(-self.gap, 0.0) + + @property + def penetration_velocity(self) -> float: + return self.port_2.v - self.port_1.v + + @property + def contact_force(self) -> float: + if self.penetration <= 0.0: + return 0.0 + return max(self.kcont * self.penetration + self.rcont * self.penetration_velocity, 0.0) + + def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]: + force = self.contact_force + return ( + EquationResidual( + id=f"{self.name}:port_1_contact_force", + owner="component", + owner_id=self.name, + relation="constitutive", + variables=(f"{self.name}.port_1.f", f"{self.name}.port_1.x", f"{self.name}.port_2.x"), + role="flow", + value=self.port_1.f + force, + ), + EquationResidual( + id=f"{self.name}:port_2_contact_force", + owner="component", + owner_id=self.name, + relation="constitutive", + variables=(f"{self.name}.port_2.f", f"{self.name}.port_1.x", f"{self.name}.port_2.x"), + role="flow", + value=self.port_2.f - force, + ), + ) + + def component_result_values(self) -> Mapping[str, float]: + return { + "gap": self.gap, + "penetration": self.penetration, + "force": self.contact_force, + } + + +class AmesimLmechn1(AlgebraicComponent): + """AMESim LMECHN1 first public dynamic linear mechanical node.""" + + MODEL_TYPE = "amesim_lmechn1" + MODEL_VERSION = "0.1.0" + PORTS = tuple( + PortDefinition.mechanical_translational(f"port_{index}") + for index in range(1, 10) + ) + PARAMETERS = ( + ParameterDefinition("v1", 8.0, label="右侧端口数", quantity="dimensionless", unit="", minimum=1.0, maximum=8.0), + ParameterDefinition("sum", 1.0, label="节点求和模式", quantity="dimensionless", unit="", minimum=0.0), + ) + RESULT_VARIABLES = ( + ResultVariableDefinition("tforce", "节点合力", "force", "N", "derived", 10), + ) + DISPLAY = ComponentDisplaySpec( + label="LMECHN1 线性机械节点", + library_id="amesim", + category_id="mechanical", + symbol="junction", + ports=tuple( + [PortDisplaySpec(f"port_{index}", "left", order=index * 10) for index in range(1, 9)] + + [PortDisplaySpec("port_9", "right", order=90)] + ), + order=50, + ) + + def __init__(self, name: str, medium: IdealGasMedium, *, v1: float = 8.0, sum: float = 1.0) -> None: + super().__init__(name=name) + self.set_parameter_values({"v1": v1, "sum": sum}) + self.v1 = int(v1) + self.sum = int(sum) + for definition in self.PORTS: + setattr(self, definition.name, self.register_declared_port(definition.name)) + + @classmethod + def create( + cls, + *, + name: str, + medium: IdealGasMedium, + parameters: Mapping[str, float], + ) -> "AmesimLmechn1": + for integer_name in ("v1", "sum"): + if not float(parameters[integer_name]).is_integer(): + raise ValueError(f"LMECHN1 {integer_name} must be an integer.") + return cls(name=name, medium=medium, v1=parameters["v1"], sum=parameters["sum"]) + + @property + def active_ports(self) -> tuple[str, ...]: + return tuple(f"port_{index}" for index in range(1, self.v1 + 1)) + ("port_9",) + + @property + def total_force(self) -> float: + return sum(self.get_port(port_name).f for port_name in self.active_ports) + + def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]: + reference = self.port_9 + residuals: list[EquationResidual] = [] + for port_name in self.active_ports[:-1]: + port = self.get_port(port_name) + residuals.append( + EquationResidual( + id=f"{self.name}:{port_name}_x_equal", + owner="component", + owner_id=self.name, + relation="equal", + variables=(f"{self.name}.{port_name}.x", f"{self.name}.port_9.x"), + role="effort", + value=port.x - reference.x, + ) + ) + residuals.append( + EquationResidual( + id=f"{self.name}:{port_name}_v_equal", + owner="component", + owner_id=self.name, + relation="equal", + variables=(f"{self.name}.{port_name}.v", f"{self.name}.port_9.v"), + role="effort", + value=port.v - reference.v, + ) + ) + residuals.append( + EquationResidual( + id=f"{self.name}:force_balance", + owner="component", + owner_id=self.name, + relation="sumToZero", + variables=tuple(f"{self.name}.{port_name}.f" for port_name in self.active_ports), + role="flow", + value=self.total_force, + ) + ) + return tuple(residuals) + + def component_result_values(self) -> Mapping[str, float]: + return {"tforce": self.total_force} diff --git a/docs/amesim-component-migration-matrix.md b/docs/amesim-component-migration-matrix.md index 5b2b177..6c9138b 100644 --- a/docs/amesim-component-migration-matrix.md +++ b/docs/amesim-component-migration-matrix.md @@ -44,8 +44,8 @@ | `PNGD00` | 1 | 氦气气体定义 | 暂不公开 | medium 配置,不是画布物理组件 | 应映射为系统/介质设置;不能作为普通可连接组件注册。 | | `PNRP17` | 8 | 气动活塞与移动体耦合 | 暂不公开 | 内部固定算例;后续跨域组件 | 依赖气动端口和机械端口耦合;需要新增机械域、跨域状态和连接规则。 | | `MECMAS21` | 10 | 一维平动质量 | 第一版公开 | `amesim_mecmas21`,`mechanical` | 已接入一维机械端口 `x/v/f`、双端质量状态和基本摩擦/限位项,并跑通零力源与信号力源最小 System XML;完整 AMESim 接触/事件语义仍留后续对齐。 | -| `LMECHN1` | 2 | 动态线性机械节点 | 暂不公开 | 后续机械域组件 | 同上。 | -| `LSTP00A` | 8 | 弹性接触/端止动 | 暂不公开 | 后续机械域组件 | 依赖机械接触和事件/非光滑力模型。 | +| `LMECHN1` | 2 | 动态线性机械节点 | 第一版公开 | `amesim_lmechn1`,`mechanical` | 已接入 9 端一维机械节点、端口位移/速度等值和节点力平衡,并跑通 `FORC -> LMECHN1 -> MECMAS21` 最小 System XML;端口朝向/符号细节留后续 AMESim baseline 对齐。 | +| `LSTP00A` | 8 | 弹性接触/端止动 | 第一版公开 | `amesim_lstp00a`,`mechanical` | 已接入两端一维机械端口、相对位移/速度接触力和最小 System XML 仿真;当前是连续罚函数基础版,完整 AMESim 事件/非光滑接触语义留后续 baseline 对齐。 | | `F000` | 16 | 零力源 | 第一版公开 | `amesim_f000`,`mechanical` | 已作为一端机械零力边界公开,约束端口力为 0。 | | `FORC` | 2 | 信号转力 | 第一版公开 | `amesim_forc`,`mechanical` | 已接入信号输入 `res` 到机械端口力源,可由 `STEP0/UD00` 驱动质量组件。 | | `STEP0` | 8 | 阶跃信号源 | 第一版公开 | `amesim_step0`,`signals` | 已接入标量信号输出端口和求解时信号传播;当前是基础阶跃,不含更复杂事件调度语义。 | @@ -79,14 +79,14 @@ - `STEP0`:已第一版公开。 - `PNVO001`:已第一版公开,保留固定开度变体。 - `UD00`:已第一版公开。 -- `MECMAS21`、`F000`、`FORC`:已第一版公开。 -- `LMECHN1`、`LSTP00A`、`PNRP17` +- `MECMAS21`、`F000`、`FORC`、`LSTP00A`、`LMECHN1`:已第一版公开。 +- `PNRP17` -当前已具备一对一标量信号传播和一维机械端口基础闭合;剩余机械/跨域组件仍需要继续设计多端机械节点、接触事件、气动-机械耦合、XML 协议、前端连线兼容和最小闭合系统测试。 +当前已具备一对一标量信号传播、一维机械端口基础闭合、弹性接触基础件和 9 端机械节点;剩余跨域组件仍需要继续设计气动-机械耦合、XML 协议、前端连线兼容和最小闭合系统测试。 ## 下一步执行建议 1. 先为 AMESim 公开组件建立独立库,例如 `app.simulation.components.amesim`,不要混入 `experimental`。 2. 先只登记一个完成度最高的气动代数组件,例如 `PNOR001` 或 `PNL00R`。 3. 每登记一个模型,都同步补充测试和目录校验,确认 `/api/components/catalog`、System XML 编译和最小仿真都通过。 -4. 动态管路 `PNL0001/2/3`、参数化 `PNCH012`、`STEP0`、`UD00`、信号版 `PNVO001` 和机械基础件 `F000/FORC/MECMAS21` 已完成第一版公开接入;下一步转向 `LMECHN1/LSTP00A/PNRP17` 或完整实时耦合。 +4. 动态管路 `PNL0001/2/3`、参数化 `PNCH012`、`STEP0`、`UD00`、信号版 `PNVO001` 和机械基础件 `F000/FORC/MECMAS21/LSTP00A/LMECHN1` 已完成第一版公开接入;下一步转向 `PNRP17` 或完整实时耦合。 diff --git a/tests/test_amesim_component_migration_matrix.py b/tests/test_amesim_component_migration_matrix.py index 45f09a1..76d2c08 100644 --- a/tests/test_amesim_component_migration_matrix.py +++ b/tests/test_amesim_component_migration_matrix.py @@ -21,8 +21,6 @@ class AmesimComponentMigrationMatrixTests(unittest.TestCase): def test_unsupported_domains_are_not_marked_as_public_candidates(self) -> None: text = MATRIX_PATH.read_text(encoding="utf-8") unsupported = { - "LMECHN1", - "LSTP00A", "PNRP17", } diff --git a/tests/test_amesim_mechanical_public_components.py b/tests/test_amesim_mechanical_public_components.py index 09e581c..3f74bec 100644 --- a/tests/test_amesim_mechanical_public_components.py +++ b/tests/test_amesim_mechanical_public_components.py @@ -6,6 +6,8 @@ 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 @@ -56,6 +58,70 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase): self.assertAlmostEqual(mass.port_1.x, 0.1) self.assertAlmostEqual(mass.port_2.v, 0.2) + + + def test_lmechn1_ties_active_port_kinematics_and_balances_force(self) -> None: + node = AmesimLmechn1("node_1", self.medium, v1=2.0, sum=1.0) + node.port_1.x = 0.2 + node.port_1.v = 0.3 + node.port_1.f = 4.0 + node.port_2.x = 0.2 + node.port_2.v = 0.3 + node.port_2.f = 6.0 + node.port_9.x = 0.2 + node.port_9.v = 0.3 + node.port_9.f = -10.0 + + residuals = node.pressure_flow_equation_residuals() + + self.assertEqual(node.active_ports, ("port_1", "port_2", "port_9")) + self.assertAlmostEqual(node.total_force, 0.0) + self.assertEqual(len(residuals), 5) + self.assertTrue(all(abs(residual.value) <= 1.0e-12 for residual in residuals)) + self.assertEqual(node.component_result_values(), {"tforce": 0.0}) + + 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_lstp00a_generates_opposing_contact_forces(self) -> None: + contact = AmesimLstp00a( + "contact_1", + self.medium, + gap0=0.0, + kcont=1000.0, + rcont=10.0, + ) + contact.port_1.x = 0.0 + contact.port_2.x = 0.002 + contact.port_1.v = 0.0 + contact.port_2.v = 0.1 + contact.port_1.f = -3.0 + contact.port_2.f = 3.0 + + self.assertAlmostEqual(contact.gap, -0.002) + self.assertAlmostEqual(contact.penetration, 0.002) + self.assertAlmostEqual(contact.contact_force, 3.0) + self.assertEqual( + contact.component_result_values(), + {"gap": -0.002, "penetration": 0.002, "force": 3.0}, + ) + residuals = contact.pressure_flow_equation_residuals() + self.assertAlmostEqual(residuals[0].value, 0.0) + self.assertAlmostEqual(residuals[1].value, 0.0) + + def test_lstp00a_returns_zero_before_contact(self) -> None: + contact = AmesimLstp00a("contact_1", self.medium, gap0=0.001, kcont=1000.0) + contact.port_1.x = 0.0 + contact.port_2.x = 0.0005 + + self.assertAlmostEqual(contact.gap, 0.0005) + self.assertAlmostEqual(contact.contact_force, 0.0) + def test_mecmas21_registry_rejects_fractional_integer_options(self) -> None: with self.assertRaisesRegex(ValueError, "useFriction must be an integer"): COMPONENT_MODEL_REGISTRY["amesim_mecmas21"].create( diff --git a/tests/test_amesim_mechanical_xml.py b/tests/test_amesim_mechanical_xml.py index fb03b33..57237c1 100644 --- a/tests/test_amesim_mechanical_xml.py +++ b/tests/test_amesim_mechanical_xml.py @@ -14,6 +14,20 @@ from tests.test_generic_system_xml_simulation import component_node, physical_ed from tests.test_system_xml_protocol import physical_port +LSTP00A_DEFAULTS = { + "na": 10.0, + "gap0": 0.0, + "kcont": 1000.0, + "G": 8.57e10, + "sdiam": 0.02, + "wdiam": 0.002, + "rcont": 0.0, + "Pdis": 1.0e-7, + "stiffmode": 1.0, + "discContactOption": 1.0, +} + + MECMAS21_DEFAULTS = { "mass": 2.0, "fstick": 0.0, @@ -130,7 +144,127 @@ def signal_force_mass_project() -> ReactFlowProjectPayload: ) +def elastic_contact_project() -> ReactFlowProjectPayload: + left_parameters = dict(MECMAS21_DEFAULTS) + left_parameters["x0"] = 0.0 + right_parameters = dict(MECMAS21_DEFAULTS) + right_parameters["x0"] = 0.001 + return ReactFlowProjectPayload( + name="amesim-mechanical-elastic-contact-smoke", + nodes=[ + component_node("zero_left", "amesim_f000", [mechanical_port("port_1", "right")]), + component_node( + "mass_left", + "amesim_mecmas21", + [mechanical_port("port_1", "left"), mechanical_port("port_2", "right")], + left_parameters, + ), + component_node( + "contact_1", + "amesim_lstp00a", + [mechanical_port("port_1", "left"), mechanical_port("port_2", "right")], + LSTP00A_DEFAULTS, + ), + component_node( + "mass_right", + "amesim_mecmas21", + [mechanical_port("port_1", "left"), mechanical_port("port_2", "right")], + right_parameters, + ), + component_node("zero_right", "amesim_f000", [mechanical_port("port_1", "left")]), + ], + edges=[ + physical_edge("edge-1", "zero_left", "port_1", "mass_left", "port_1"), + physical_edge("edge-2", "mass_left", "port_2", "contact_1", "port_1"), + physical_edge("edge-3", "contact_1", "port_2", "mass_right", "port_1"), + physical_edge("edge-4", "mass_right", "port_2", "zero_right", "port_1"), + ], + simulation={"t_start": 0.0, "t_stop": 0.02, "step": 0.01, "max_step": 0.005, "method": "BDF"}, + ) + + +def force_node_mass_project() -> ReactFlowProjectPayload: + node_ports = [mechanical_port(f"port_{index}", "left") for index in range(1, 9)] + node_ports.append(mechanical_port("port_9", "right")) + zero_nodes = [ + component_node(f"zero_{index}", "amesim_f000", [mechanical_port("port_1", "right")]) + for index in range(2, 9) + ] + zero_edges = [ + physical_edge(f"edge-zero-{index}", f"zero_{index}", "port_1", "node_1", f"port_{index}") + for index in range(2, 9) + ] + return ReactFlowProjectPayload( + name="amesim-mechanical-node-smoke", + nodes=[ + component_node( + "step_1", + "amesim_step0", + [signal_port("out", "output", "right")], + {"initial": 10.0, "final": 10.0, "time": 0.0}, + ), + component_node( + "force_1", + "amesim_forc", + [signal_port("res", "input", "left"), mechanical_port("port_2", "right")], + ), + component_node("node_1", "amesim_lmechn1", node_ports, {"v1": 8.0, "sum": 1.0}), + *zero_nodes, + component_node( + "mass_1", + "amesim_mecmas21", + [mechanical_port("port_1", "left"), mechanical_port("port_2", "right")], + MECMAS21_DEFAULTS, + ), + component_node("zero_right", "amesim_f000", [mechanical_port("port_1", "left")]), + ], + edges=[ + signal_edge("signal-1", "step_1", "out", "force_1", "res"), + physical_edge("edge-force", "force_1", "port_2", "node_1", "port_1"), + *zero_edges, + physical_edge("edge-mass", "node_1", "port_9", "mass_1", "port_1"), + physical_edge("edge-right", "mass_1", "port_2", "zero_right", "port_1"), + ], + simulation={"t_start": 0.0, "t_stop": 0.02, "step": 0.01, "max_step": 0.005, "method": "BDF"}, + ) + + class AmesimMechanicalXmlTests(unittest.TestCase): + + + def test_force_node_mass_project_compiles_and_simulates(self) -> None: + xml = build_reactflow_system_xml(force_node_mass_project()) + report = validate_system_xml_document(xml) + + self.assertTrue(report.valid, report.as_dict()) + network = compile_reactflow_network(force_node_mass_project()) + self.assertTrue(network.pressure_flow_structure_dict()["isSquare"]) + result = run_system_xml_simulation(xml) + + self.assertTrue(result["success"], result["message"]) + self.assertEqual(result["series"]["step_1.out.signal"], [10.0, 10.0, 10.0]) + self.assertEqual(result["series"]["force_1.res.signal"], [10.0, 10.0, 10.0]) + self.assertAlmostEqual(result["series"]["mass_1.a"][0], 5.0) + self.assertGreater(result["series"]["mass_1.v"][-1], 0.0) + self.assertGreater(result["series"]["mass_1.x"][-1], 0.0) + + def test_elastic_contact_project_compiles_and_simulates(self) -> None: + xml = build_reactflow_system_xml(elastic_contact_project()) + report = validate_system_xml_document(xml) + + self.assertTrue(report.valid, report.as_dict()) + network = compile_reactflow_network(elastic_contact_project()) + self.assertTrue(network.pressure_flow_structure_dict()["isSquare"]) + result = run_system_xml_simulation(xml) + + self.assertTrue(result["success"], result["message"]) + self.assertEqual(result["series"]["time"], [0.0, 0.01, 0.02]) + self.assertAlmostEqual(result["series"]["contact_1.force"][0], 1.0) + self.assertAlmostEqual(result["series"]["mass_left.a"][0], 0.5) + self.assertAlmostEqual(result["series"]["mass_right.a"][0], -0.5) + self.assertGreater(result["series"]["mass_left.x"][-1], 0.0) + self.assertLess(result["series"]["mass_right.x"][-1], 0.001) + def test_zero_force_mechanical_project_compiles_and_simulates(self) -> None: xml = build_reactflow_system_xml(zero_force_mass_project()) report = validate_system_xml_document(xml) diff --git a/tests/test_component_catalog.py b/tests/test_component_catalog.py index 494ab3b..ce85478 100644 --- a/tests/test_component_catalog.py +++ b/tests/test_component_catalog.py @@ -48,7 +48,7 @@ class ComponentCatalogTests(unittest.TestCase): self.assertEqual(library["label"], "AMESim 组件库") self.assertEqual([category["id"] for category in library["categories"]], ["storage", "flow", "junctions", "boundary", "signals", "mechanical"]) - self.assertEqual(set(components), {"amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"}) + self.assertEqual(set(components), {"amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_lstp00a", "amesim_lmechn1", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"}) self.assertEqual( [port["name"] for port in components["amesim_p4node2"]["ports"]], ["port_1", "port_2", "port_3", "port_4"], @@ -112,6 +112,21 @@ class ComponentCatalogTests(unittest.TestCase): self.assertEqual([port["name"] for port in components["amesim_mecmas21"]["ports"]], ["port_1", "port_2"]) self.assertEqual(mecmas_parameters["mass"]["unit"], "kg") self.assertEqual(mecmas_parameters["Kbmin"]["unit"], "N/m") + lstp_parameters = { + parameter["name"]: parameter + for parameter in components["amesim_lstp00a"]["parameters"] + } + self.assertEqual(components["amesim_lstp00a"]["category"]["id"], "mechanical") + self.assertEqual([port["name"] for port in components["amesim_lstp00a"]["ports"]], ["port_1", "port_2"]) + self.assertEqual(lstp_parameters["gap0"]["unit"], "m") + self.assertEqual(lstp_parameters["kcont"]["unit"], "N/m") + lmechn_parameters = { + parameter["name"]: parameter + for parameter in components["amesim_lmechn1"]["parameters"] + } + self.assertEqual(components["amesim_lmechn1"]["category"]["id"], "mechanical") + self.assertEqual([port["name"] for port in components["amesim_lmechn1"]["ports"]], [f"port_{index}" for index in range(1, 10)]) + self.assertEqual(lmechn_parameters["v1"]["maximum"], 8.0) self.assertEqual(components["amesim_pnvo001"]["category"]["id"], "flow") self.assertEqual([port["name"] for port in components["amesim_pnvo001"]["ports"]], ["res", "port_2", "port_3"]) diff --git a/tests/test_component_registry.py b/tests/test_component_registry.py index 3c506ff..88909e5 100644 --- a/tests/test_component_registry.py +++ b/tests/test_component_registry.py @@ -36,6 +36,8 @@ class ComponentRegistryTests(unittest.TestCase): "amesim_f000", "amesim_forc", "amesim_mecmas21", + "amesim_lstp00a", + "amesim_lmechn1", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", @@ -66,6 +68,8 @@ class ComponentRegistryTests(unittest.TestCase): self.assertEqual(models["amesim_f000"].library.id, "amesim") self.assertEqual(models["amesim_forc"].library.id, "amesim") self.assertEqual(models["amesim_mecmas21"].library.id, "amesim") + self.assertEqual(models["amesim_lstp00a"].library.id, "amesim") + self.assertEqual(models["amesim_lmechn1"].library.id, "amesim") self.assertEqual(models["amesim_pnch023"].library.id, "amesim") self.assertEqual(models["amesim_pnch012"].library.id, "amesim") self.assertEqual(models["amesim_pnor001"].library.id, "amesim")