merge/model-development-into-main #2

Merged
lujingze merged 126 commits from merge/model-development-into-main into main 2026-07-31 09:52:44 +08:00
3 changed files with 211 additions and 7 deletions
Showing only changes of commit 796433c275 - Show all commits

No files matched your search

+15 -7
View File
@@ -12,7 +12,8 @@
## 源文件
- AMESim 源模型:`AmesimModels/test_mql.ame`
- 该 `.ame` 文件本质是一个 tar 包,内部包含 `test_mql_.cir`、`test_mql_.param`、`test_mql_.modelinfo`、`test_mql_.sim` 等文件。
- 该 `.ame` 文件本质是一个 tar 包,内部包含 `test_mql_.cir`、`test_mql_.param`、`test_mql_.modelinfo`、`test_mql_.sim`、`test_mql_.data`、`test_mql_.results`、`test_mql_.var` 等文件。
- `test_mql_.results` 是 AMESim 主时序结果文件;`test_mql_.data` 是配套结果/数据文件。
## 当前新增文件
@@ -45,7 +46,14 @@
- `PythonModels/components/amesim_pneumatic.py`
- 提供 `test_mql` 后续会用到的 AMESim 气动组件原语。
- 当前包含氦气 PR 压力闭合的气动容腔、标准可压缩孔口流量、单位换算 helper。
- 这层仍是首版近似原语,后续必须通过 AMESim CSV 对齐修正。
- 这层仍是首版近似原语,后续必须通过 AMESim 结果对齐修正。
- `PythonModels/reporting/amesim_results.py`
- 直接解析 `.ame` 包内的 AMESim 二进制结果。
- 当前已确认 `test_mql_.results` 结构为:2 个 int32 头字段、1116 个保存变量映射、1002 行 `time + 1116变量` 的 double 时序矩阵,以及尾部 `time + 1278变量终值`。
- `tests/test_amesim_results.py`
- 保护 `test_mql_.results` 的时间轴、变量数量、Data_Path 映射和典型样本值。
## 物性约定
@@ -86,19 +94,19 @@ AMESim 模型中 `test_mql` 使用氦气,Python 侧当前通过 `HELIUM_PR`
1. 组件别名优先沿用 AMESim 原名,例如 `pn_brp2_8`、`pn_c1_8`、`pn_orifice_18`。
2. 子模型族优先沿用 AMESim 子模型名,例如 `PNRP17`、`PNCH012`、`PNOR001`、`PNVO001`。
3. 输出变量命名要能追溯到 AMESim 的 `Data_Path`,避免 Python 侧改名后无法对比。
4. Python 每完成一批物理方程,都应拿 AMESim 导出的 CSV 结果做误差对比。
5. 当前 `.ame` 包内的 `test_mql_.results` 是 AMESim 二进制结果文件,暂不作为直接解析基准;建议后续从 AMESim 导出 CSV 结果放到本目录下。
6. 在没有 AMESim CSV 基准前,不能把 Python 输出描述为“已经和 AMESim 几乎一致”,只能说明完成了结构、参数或某类组件方程。
4. Python 每完成一批物理方程,都应优先使用 `test_mql_.results` 解析出的 AMESim 原始时序做误差对比。
5. CSV 仍可作为人工检查或外部交换格式,但不是自动对齐的前置条件。
6. 在没有完成逐变量误差对比前,不能把 Python 输出描述为“已经和 AMESim 几乎一致”,只能说明完成了结构、参数、结果读取或某类组件方程。
## 验证方式
```bash
python3 -m py_compile PythonModels/components/amesim_pneumatic.py PythonModels/core/peng_robinson.py PythonModels/systems/test_mql.py PythonModels/systems/test_mql_config.py PythonModels/scripts/run_test_mql.py
python3 -m py_compile PythonModels/components/amesim_pneumatic.py PythonModels/core/peng_robinson.py PythonModels/reporting/amesim_results.py 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 物理结果复刻。
当前测试覆盖的是结构、配置解析、气动原语和 AMESim 结果读取,不代表已经完成 AMESim 物理结果复刻。
## 后续方向
+150
View File
@@ -0,0 +1,150 @@
from __future__ import annotations
import re
import struct
import tarfile
from dataclasses import dataclass
from pathlib import Path
class AmesimResultsError(ValueError):
"""Raised when AMESim result files cannot be parsed consistently."""
@dataclass(frozen=True)
class AmesimVariable:
index: int
label: str
data_path: str | None
param_id: int | None
hidden: bool
@dataclass(frozen=True)
class AmesimResults:
times: tuple[float, ...]
variables: tuple[AmesimVariable, ...]
saved_variable_indices: tuple[int, ...]
series_by_data_path: dict[str, tuple[float, ...]]
final_values_by_data_path: dict[str, float]
@property
def point_count(self) -> int:
return len(self.times)
@property
def saved_variable_count(self) -> int:
return len(self.saved_variable_indices)
def series(self, data_path: str) -> tuple[float, ...]:
return self.series_by_data_path[data_path]
def final_value(self, data_path: str) -> float:
return self.final_values_by_data_path[data_path]
_DATA_PATH_RE = re.compile(r"Data_Path=(\S+)")
_PARAM_ID_RE = re.compile(r"Param_Id=(\d+)")
def load_test_mql_amesim_results(archive_path: str | Path) -> AmesimResults:
return load_amesim_results_from_archive(
archive_path=archive_path,
var_member="test_mql_.var",
results_member="test_mql_.results",
)
def load_amesim_results_from_archive(
*,
archive_path: str | Path,
var_member: str,
results_member: str,
) -> AmesimResults:
with tarfile.open(archive_path) as archive:
var_file = archive.extractfile(var_member)
results_file = archive.extractfile(results_member)
if var_file is None:
raise AmesimResultsError(f"Missing AMESim variable member: {var_member}")
if results_file is None:
raise AmesimResultsError(f"Missing AMESim results member: {results_member}")
var_lines = var_file.read().decode("latin1").splitlines()
results_data = results_file.read()
variables = tuple(_parse_variable_line(index, line) for index, line in enumerate(var_lines))
return parse_amesim_results_bytes(results_data, variables)
def parse_amesim_results_bytes(
results_data: bytes,
variables: tuple[AmesimVariable, ...],
) -> AmesimResults:
if len(results_data) < 8:
raise AmesimResultsError("AMESim results data is too small.")
point_count, encoded_saved_variable_count = struct.unpack_from("<2i", results_data, 0)
saved_variable_count = abs(encoded_saved_variable_count)
if point_count <= 0 or saved_variable_count <= 0:
raise AmesimResultsError("Invalid AMESim results header.")
mapping_offset = 8
mapping_size = saved_variable_count * 4
data_offset = mapping_offset + mapping_size
saved_variable_indices = struct.unpack_from(
f"<{saved_variable_count}i",
results_data,
mapping_offset,
)
if any(index < 0 or index >= len(variables) for index in saved_variable_indices):
raise AmesimResultsError(
"AMESim results variable mapping references unknown .var rows."
)
row_length = 1 + saved_variable_count
main_value_count = point_count * row_length
main_byte_count = main_value_count * 8
main_end = data_offset + main_byte_count
if main_end > len(results_data):
raise AmesimResultsError("AMESim results matrix is truncated.")
main_values = struct.unpack_from(f"<{main_value_count}d", results_data, data_offset)
times = tuple(main_values[row * row_length] for row in range(point_count))
series_by_data_path: dict[str, tuple[float, ...]] = {}
for column, variable_index in enumerate(saved_variable_indices, start=1):
variable = variables[variable_index]
if variable.data_path is None:
continue
series_by_data_path[variable.data_path] = tuple(
main_values[row * row_length + column]
for row in range(point_count)
)
final_values_by_data_path: dict[str, float] = {}
trailing_bytes = len(results_data) - main_end
expected_final_bytes = (1 + len(variables)) * 8
if trailing_bytes >= expected_final_bytes:
final_values = struct.unpack_from(f"<{1 + len(variables)}d", results_data, main_end)
for variable, value in zip(variables, final_values[1:]):
if variable.data_path is not None:
final_values_by_data_path[variable.data_path] = value
return AmesimResults(
times=times,
variables=variables,
saved_variable_indices=tuple(saved_variable_indices),
series_by_data_path=series_by_data_path,
final_values_by_data_path=final_values_by_data_path,
)
def _parse_variable_line(index: int, line: str) -> AmesimVariable:
data_path_match = _DATA_PATH_RE.search(line)
param_id_match = _PARAM_ID_RE.search(line)
label = line
if data_path_match is not None:
label = line[: data_path_match.start()].strip()
return AmesimVariable(
index=index,
label=label,
data_path=data_path_match.group(1) if data_path_match else None,
param_id=int(param_id_match.group(1)) if param_id_match else None,
hidden="HIDDEN" in line,
)
+46
View File
@@ -0,0 +1,46 @@
from __future__ import annotations
import unittest
from pathlib import Path
from PythonModels.reporting.amesim_results import load_test_mql_amesim_results
REPO_ROOT = Path(__file__).resolve().parents[1]
TEST_MQL_AME = REPO_ROOT / "AmesimModels" / "test_mql.ame"
class AmesimResultsTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.results = load_test_mql_amesim_results(TEST_MQL_AME)
def test_reads_binary_results_shape(self) -> None:
self.assertEqual(self.results.point_count, 1002)
self.assertEqual(self.results.saved_variable_count, 1116)
self.assertEqual(len(self.results.variables), 1278)
self.assertEqual(len(self.results.final_values_by_data_path), 1100)
def test_time_axis_matches_amesim_simulation(self) -> None:
self.assertAlmostEqual(self.results.times[0], 0.0)
self.assertAlmostEqual(self.results.times[1], 0.01)
self.assertAlmostEqual(self.results.times[-1], 10.0)
def test_variable_mapping_preserves_data_paths(self) -> None:
self.assertEqual(self.results.saved_variable_indices[:2], (0, 1))
self.assertEqual(self.results.variables[0].data_path, "temp3@pn_c1_8")
self.assertEqual(self.results.variables[1].data_path, "press3@pn_c1_8")
self.assertIn("vvol1@pn_brp2_8", self.results.series_by_data_path)
self.assertIn("vol1@pn_brp2_8", self.results.series_by_data_path)
def test_series_values_match_known_amesim_samples(self) -> None:
temp3 = self.results.series("temp3@pn_c1_8")
press3 = self.results.series("press3@pn_c1_8")
self.assertAlmostEqual(temp3[0], 293.15)
self.assertAlmostEqual(press3[0], -1300.0)
self.assertAlmostEqual(temp3[-1], self.results.final_value("temp3@pn_c1_8"))
self.assertAlmostEqual(press3[-1], self.results.final_value("press3@pn_c1_8"))
if __name__ == "__main__":
unittest.main()