补充test_mql配置解析层

This commit is contained in:
huojiarong committed 2026-07-15 09:05:53 +00:00
1 parent c745fd2a5c
commit 056b71fead
3 files changed
+293 -9

No files matched your search

+77 -9
View File
@@ -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`:气动活塞、机械负载和端止动。
+148
View File
@@ -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)}")
+68
View File
@@ -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()