merge/model-development-into-main #2
No files matched your search
@@ -12,7 +12,8 @@
|
|||||||
## 源文件
|
## 源文件
|
||||||
|
|
||||||
- AMESim 源模型:`AmesimModels/test_mql.ame`
|
- 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`
|
- `PythonModels/components/amesim_pneumatic.py`
|
||||||
- 提供 `test_mql` 后续会用到的 AMESim 气动组件原语。
|
- 提供 `test_mql` 后续会用到的 AMESim 气动组件原语。
|
||||||
- 当前包含氦气 PR 压力闭合的气动容腔、标准可压缩孔口流量、单位换算 helper。
|
- 当前包含氦气 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`。
|
1. 组件别名优先沿用 AMESim 原名,例如 `pn_brp2_8`、`pn_c1_8`、`pn_orifice_18`。
|
||||||
2. 子模型族优先沿用 AMESim 子模型名,例如 `PNRP17`、`PNCH012`、`PNOR001`、`PNVO001`。
|
2. 子模型族优先沿用 AMESim 子模型名,例如 `PNRP17`、`PNCH012`、`PNOR001`、`PNVO001`。
|
||||||
3. 输出变量命名要能追溯到 AMESim 的 `Data_Path`,避免 Python 侧改名后无法对比。
|
3. 输出变量命名要能追溯到 AMESim 的 `Data_Path`,避免 Python 侧改名后无法对比。
|
||||||
4. Python 每完成一批物理方程,都应拿 AMESim 导出的 CSV 结果做误差对比。
|
4. Python 每完成一批物理方程,都应优先使用 `test_mql_.results` 解析出的 AMESim 原始时序做误差对比。
|
||||||
5. 当前 `.ame` 包内的 `test_mql_.results` 是 AMESim 二进制结果文件,暂不作为直接解析基准;建议后续从 AMESim 导出 CSV 结果放到本目录下。
|
5. CSV 仍可作为人工检查或外部交换格式,但不是自动对齐的前置条件。
|
||||||
6. 在没有 AMESim CSV 基准前,不能把 Python 输出描述为“已经和 AMESim 几乎一致”,只能说明完成了结构、参数或某类组件方程。
|
6. 在没有完成逐变量误差对比前,不能把 Python 输出描述为“已经和 AMESim 几乎一致”,只能说明完成了结构、参数、结果读取或某类组件方程。
|
||||||
|
|
||||||
## 验证方式
|
## 验证方式
|
||||||
|
|
||||||
```bash
|
```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 PythonModels.scripts.run_test_mql
|
||||||
python3 -m unittest discover -s tests -t .
|
python3 -m unittest discover -s tests -t .
|
||||||
```
|
```
|
||||||
|
|
||||||
当前测试覆盖的是结构和配置解析,不代表已经完成 AMESim 物理结果复刻。
|
当前测试覆盖的是结构、配置解析、气动原语和 AMESim 结果读取,不代表已经完成 AMESim 物理结果复刻。
|
||||||
|
|
||||||
## 后续方向
|
## 后续方向
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
Reference in new issue
Block a user