From 056b71feadf5fd33fc7fc206a5cc77dfeb4a24af Mon Sep 17 00:00:00 2001 From: huojiarong Date: Wed, 15 Jul 2026 09:05:53 +0000 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85test=5Fmql=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E5=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AmesimModels/test_mql/README.md | 86 ++++++++++++-- PythonModels/systems/test_mql_config.py | 148 ++++++++++++++++++++++++ tests/test_test_mql_config.py | 68 +++++++++++ 3 files changed, 293 insertions(+), 9 deletions(-) create mode 100644 PythonModels/systems/test_mql_config.py create mode 100644 tests/test_test_mql_config.py diff --git a/AmesimModels/test_mql/README.md b/AmesimModels/test_mql/README.md index dacba8c..7455546 100644 --- a/AmesimModels/test_mql/README.md +++ b/AmesimModels/test_mql/README.md @@ -1,16 +1,84 @@ # test_mql -Model name used in this repository: `test_mql`. +本目录记录从 AMESim 模型 `test_mql.ame` 迁移到 `PythonModels` 的当前状态。 -Source AMESim archive: `AmesimModels/test_mql.ame`. +## 模型命名 -Python port files added for this model: +- 仓库内模型名:`test_mql` +- Python 系统类:`TestMqlSystem` +- Python 配置解析类:`TestMqlConfig` +- Python 运行入口:`PythonModels/scripts/run_test_mql.py` + +## 源文件 + +- AMESim 源模型:`AmesimModels/test_mql.ame` +- 该 `.ame` 文件本质是一个 tar 包,内部包含 `test_mql_.cir`、`test_mql_.param`、`test_mql_.modelinfo`、`test_mql_.sim` 等文件。 + +## 当前新增文件 - `PythonModels/systems/test_mql.py` -- `PythonModels/scripts/run_test_mql.py` + - 记录 AMESim 组件清单、全局参数、子模型分布、连接拓扑和 AMESim modelinfo 状态数。 + - 当前是结构化 Python 迁移骨架,不是完整 AMESim 物理方程复刻。 -Current status: structural port scaffold. It records the AMESim component -inventory, global parameters, submodel counts, connection topology, and the -AMESim modelinfo state counts. Existing PythonModels code is not modified. -Detailed physical equations should be filled in incrementally per AMESim -submodel family. +- `PythonModels/systems/test_mql_config.py` + - 提供 `TestMqlConfig.from_amesim_specs()`。 + - 负责把 AMESim 全局参数和组件参数解析成后续可用的 Python 配置对象。 + - 已支持常量、全局参数引用、四则运算、括号和 AMESim 风格的 `^` 指数表达式。 + - 非数值文本会保留为 `None`,后续按具体子模型显式处理。 + +- `PythonModels/scripts/run_test_mql.py` + - 提供独立运行入口。 + - 当前输出结构摘要和基础序列,后续逐步接入真实物理方程。 + +- `tests/test_test_mql_structural.py` + - 保护组件数、连接数、状态数、全局参数和关键子模型计数。 + +- `tests/test_test_mql_config.py` + - 保护全局参数解析、AMESim 表达式解析、按子模型分组和典型组件参数解析。 + +## 当前提取结果 + +- 组件数:117 +- 连接数:84 +- AMESim 连续状态数:132 +- AMESim 离散状态数:24 +- 全局参数:`D1=20`、`D2=20`、`D3=14`、`P0=153`、`Pdq=1`、`V=15`、`cf=0.45` + +主要 AMESim 子模型分布: + +- `F000`: 16 +- `PNPL01`: 16 +- `MECMAS21`: 10 +- `PNRP17`: 8 +- `LSTP00A`: 8 +- `PNCH012`: 8 +- `PNOR001`: 8 +- `PN3NODE2`: 8 +- `P4NODE2`: 8 +- `PNVO001`: 8 +- `STEP0`: 8 +- `PNCH023`: 4 +- `LMECHN1`: 2 +- `FORC`: 2 +- `UD00`: 2 +- `PNGD00`: 1 + +## 验证方式 + +```bash +python3 -m py_compile PythonModels/systems/test_mql.py PythonModels/systems/test_mql_config.py PythonModels/scripts/run_test_mql.py +python3 -m PythonModels.scripts.run_test_mql +python3 -m unittest discover -s tests -t . +``` + +当前测试覆盖的是结构和配置解析,不代表已经完成 AMESim 物理结果复刻。 + +## 后续方向 + +后续继续补模型时,保持只新增或修改 `test_mql` 相关代码,不改旧的 `testmodel`。建议按下面顺序逐步实现真实物理方程: + +1. `PNGD00`:气体属性。 +2. `PNCH023 / PNCH012`:固定气室和变容气室。 +3. `PNOR001 / PNVO001`:固定孔口和可变孔口。 +4. `PNL0001 / PNL0002 / PNL0003 / PNL00R`:管路阻容连接。 +5. `PNRP17 / MECMAS21 / LSTP00A / LMECHN1`:气动活塞、机械负载和端止动。 diff --git a/PythonModels/systems/test_mql_config.py b/PythonModels/systems/test_mql_config.py new file mode 100644 index 0000000..b227752 --- /dev/null +++ b/PythonModels/systems/test_mql_config.py @@ -0,0 +1,148 @@ +from __future__ import annotations + +import ast +import operator +from dataclasses import dataclass +from math import isfinite +from typing import Any + +from PythonModels.systems.test_mql import COMPONENT_SPECS, GLOBAL_PARAMETERS + + +_BINARY_OPERATORS = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.Div: operator.truediv, + ast.Pow: operator.pow, +} +_UNARY_OPERATORS = { + ast.UAdd: operator.pos, + ast.USub: operator.neg, +} + + +class TestMqlExpressionError(ValueError): + """Raised when an AMESim parameter expression cannot be resolved safely.""" + + +@dataclass(frozen=True) +class TestMqlResolvedParameter: + name: str + title: str + raw_value: str + units: str + value: float | None + + @property + def is_numeric(self) -> bool: + return self.value is not None + + +@dataclass(frozen=True) +class TestMqlResolvedComponent: + alias: str + component_name: str + submodel: str + label: str + parameters: dict[str, TestMqlResolvedParameter] + + def parameter_value(self, name: str) -> float: + parameter = self.parameters[name] + if parameter.value is None: + raise KeyError(f"Parameter {name!r} on {self.alias!r} is not numeric") + return parameter.value + + +@dataclass(frozen=True) +class TestMqlConfig: + raw_global_parameters: dict[str, str] + global_parameters: dict[str, float] + components: tuple[TestMqlResolvedComponent, ...] + + @classmethod + def from_amesim_specs(cls) -> "TestMqlConfig": + raw_globals = dict(GLOBAL_PARAMETERS) + numeric_globals = { + name: value + for name, raw in raw_globals.items() + if (value := resolve_numeric_expression(raw, {})) is not None + } + components = tuple( + _resolve_component(spec, numeric_globals) + for spec in COMPONENT_SPECS + ) + return cls( + raw_global_parameters=raw_globals, + global_parameters=numeric_globals, + components=components, + ) + + def component(self, alias: str) -> TestMqlResolvedComponent: + for component in self.components: + if component.alias == alias: + return component + raise KeyError(alias) + + def components_by_submodel(self, submodel: str) -> tuple[TestMqlResolvedComponent, ...]: + return tuple(component for component in self.components if component.submodel == submodel) + + +def _resolve_component( + spec: dict[str, Any], + variables: dict[str, float], +) -> TestMqlResolvedComponent: + parameters = {} + for parameter in spec.get("parameters", []): + name = str(parameter["name"]) + raw_value = str(parameter["value"]) + parameters[name] = TestMqlResolvedParameter( + name=name, + title=str(parameter["title"]), + raw_value=raw_value, + units=str(parameter["units"]), + value=resolve_numeric_expression(raw_value, variables), + ) + return TestMqlResolvedComponent( + alias=str(spec["alias"]), + component_name=str(spec["component_name"]), + submodel=str(spec["submodel"]), + label=str(spec["label"]), + parameters=parameters, + ) + + +def resolve_numeric_expression( + expression: str, + variables: dict[str, float], +) -> float | None: + expression = expression.strip() + if not expression: + return None + normalized = expression.replace("^", "**") + try: + parsed = ast.parse(normalized, mode="eval") + value = float(_eval_node(parsed.body, variables)) + except (SyntaxError, TestMqlExpressionError, ValueError, TypeError, ZeroDivisionError): + return None + return value if isfinite(value) else None + + +def _eval_node(node: ast.AST, variables: dict[str, float]) -> float: + if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): + return float(node.value) + if isinstance(node, ast.Name): + if node.id not in variables: + raise TestMqlExpressionError(f"Unknown variable: {node.id}") + return float(variables[node.id]) + if isinstance(node, ast.BinOp): + operator_type = type(node.op) + if operator_type not in _BINARY_OPERATORS: + raise TestMqlExpressionError(f"Unsupported binary operator: {operator_type}") + return float(_BINARY_OPERATORS[operator_type](_eval_node(node.left, variables), _eval_node(node.right, variables))) + if isinstance(node, ast.UnaryOp): + operator_type = type(node.op) + if operator_type not in _UNARY_OPERATORS: + raise TestMqlExpressionError(f"Unsupported unary operator: {operator_type}") + return float(_UNARY_OPERATORS[operator_type](_eval_node(node.operand, variables))) + raise TestMqlExpressionError(f"Unsupported expression node: {type(node)}") diff --git a/tests/test_test_mql_config.py b/tests/test_test_mql_config.py new file mode 100644 index 0000000..e3d61d9 --- /dev/null +++ b/tests/test_test_mql_config.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import unittest + +from PythonModels.systems.test_mql_config import TestMqlConfig, resolve_numeric_expression + + +class TestMqlConfigTest(unittest.TestCase): + def test_resolves_global_parameters(self) -> None: + config = TestMqlConfig.from_amesim_specs() + + self.assertEqual(config.global_parameters["D1"], 20.0) + self.assertEqual(config.global_parameters["D2"], 20.0) + self.assertEqual(config.global_parameters["D3"], 14.0) + self.assertEqual(config.global_parameters["P0"], 153.0) + self.assertEqual(config.global_parameters["Pdq"], 1.0) + self.assertEqual(config.global_parameters["V"], 15.0) + self.assertEqual(config.global_parameters["cf"], 0.45) + + def test_resolves_amesim_parameter_expressions(self) -> None: + variables = {"D2": 20.0, "P0": 153.0, "cf": 0.45} + + self.assertAlmostEqual(resolve_numeric_expression("3.14*10^2/4", variables), 78.5) + self.assertAlmostEqual(resolve_numeric_expression("0.045/D2", variables), 0.00225) + self.assertAlmostEqual(resolve_numeric_expression("cf", variables), 0.45) + self.assertAlmostEqual( + resolve_numeric_expression("(P0-1.01300000000000e+00)/1.00000000000000e-05", variables), + 15198700.0, + ) + self.assertIsNone(resolve_numeric_expression("predefined nitrogen (N2)", variables)) + + def test_groups_components_by_submodel(self) -> None: + config = TestMqlConfig.from_amesim_specs() + + self.assertEqual(len(config.components_by_submodel("PNRP17")), 8) + self.assertEqual(len(config.components_by_submodel("PNCH012")), 8) + self.assertEqual(len(config.components_by_submodel("PNOR001")), 8) + self.assertEqual(len(config.components_by_submodel("PNVO001")), 8) + self.assertEqual(len(config.components_by_submodel("PNGD00")), 1) + + def test_resolves_representative_component_parameters(self) -> None: + config = TestMqlConfig.from_amesim_specs() + + piston = config.component("pn_brp2_8") + self.assertEqual(piston.submodel, "PNRP17") + self.assertEqual(piston.parameter_value("dp"), 200.0) + self.assertEqual(piston.parameter_value("dr"), 1.0) + + fixed_orifice = config.component("pn_orifice_18") + self.assertEqual(fixed_orifice.submodel, "PNOR001") + self.assertAlmostEqual(fixed_orifice.parameter_value("area"), 78.5) + + variable_orifice = config.component("pn_morifice_11") + self.assertEqual(variable_orifice.submodel, "PNVO001") + self.assertAlmostEqual(variable_orifice.parameter_value("cq"), 0.45) + self.assertAlmostEqual(variable_orifice.parameter_value("area0"), 78.5) + + chamber = config.component("pn_c1_8") + self.assertEqual(chamber.submodel, "PNCH012") + self.assertEqual(chamber.parameter_value("cvol0"), 15.0) + + gas = config.component("pn_gas_data") + self.assertEqual(gas.submodel, "PNGD00") + self.assertAlmostEqual(gas.parameter_value("rGas"), 287.2) + + +if __name__ == "__main__": + unittest.main()