补充test_mql配置解析层
This commit is contained in:
1 parent
c745fd2a5c
commit
056b71fead
3 files changed
+293
-9
No files matched your search
@@ -1,16 +1,84 @@
|
|||||||
# test_mql
|
# 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/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
|
- `PythonModels/systems/test_mql_config.py`
|
||||||
inventory, global parameters, submodel counts, connection topology, and the
|
- 提供 `TestMqlConfig.from_amesim_specs()`。
|
||||||
AMESim modelinfo state counts. Existing PythonModels code is not modified.
|
- 负责把 AMESim 全局参数和组件参数解析成后续可用的 Python 配置对象。
|
||||||
Detailed physical equations should be filled in incrementally per AMESim
|
- 已支持常量、全局参数引用、四则运算、括号和 AMESim 风格的 `^` 指数表达式。
|
||||||
submodel family.
|
- 非数值文本会保留为 `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`:气动活塞、机械负载和端止动。
|
||||||
@@ -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)}")
|
||||||
@@ -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()
|
||||||
Reference in new issue
Block a user