diff --git a/README.md b/README.md index d8422e6..efd8a6f 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,872 @@ -# mil_sdk +# MIL SDK 文档 -MIL (Model-in-the-Loop) SDK for reading simulation data from Excel files. +MIL SDK 是一个用于处理 MIL(Model-In-the-Loop)仿真数据的 Python 工具包,主要提供 Excel 数据文件的读取、解析、生成和更新功能。 -## Installation +## 目录 + +- [概述](#概述) +- [项目架构](#项目架构) +- [快速开始](#快速开始) +- [数据模型](#数据模型) +- [异常体系](#异常体系) +- [日志模块](#日志模块) +- [Excel 读取模块](#excel-读取模块) +- [Excel 生成模块](#excel-生成模块) +- [Excel 更新模块](#excel-更新模块) +- [API 参考](#api-参考) +- [使用示例](#使用示例) +- [Excel 文件格式约定](#excel-文件格式约定) + +--- + +## 概述 + +### 项目简介 + +MIL SDK 专为 MIL 仿真数据处理场景设计,提供以下核心能力: + +- **数据读取** - 从特定格式的 Excel 文件中解析仿真信号数据 +- **用例管理** - 读取和解析测试用例模板 +- **用例生成** - 根据数据和模板生成测试用例 Excel 文件 +- **数据对比** - 比较新旧仿真数据差异,自动更新信号列 + +### 技术栈 + +| 类别 | 技术选型 | +|------|----------| +| 语言 | Python 3.10+ | +| 主要依赖 | openpyxl >= 3.0.0 | +| 测试框架 | pytest >= 7.0.0 | +| 类型检查 | mypy >= 1.0.0 | +| 代码规范 | ruff >= 0.1.0 | + +### 版本信息 + +- 当前版本:0.1.0 +- 许可证:MIT + +--- + +## 项目架构 + +### 目录结构 + +``` +mil_sdk/ +├── src/ +│ └── core/ # 核心模块 +│ ├── __init__.py # 公共 API 导出 +│ ├── base.py # 核心数据模型 +│ ├── exceptions.py # 自定义异常 +│ ├── logging_config.py # 日志配置 +│ ├── mil_read_data_excel.py # 读取仿真数据 Excel +│ ├── mil_read_case_excel.py # 读取测试用例 Excel +│ ├── mil_create_data_excel.py # 生成测试用例 Excel +│ └── mil_update_excel.py # 更新用例 Excel +├── tests/ # 测试模块 +│ ├── conftest.py # pytest 配置和 fixtures +│ ├── test_base.py # 基础数据模型测试 +│ ├── test_mil_case_excel.py # 用例 Excel 测试 +│ └── test_mil_data_excel.py # 数据 Excel 测试 +├── main.py # 入口文件 +├── pyproject.toml # 项目配置 +├── requirements.txt # 依赖清单 +└── SDK_DOCUMENTATION.md # 本文档 +``` + +### 模块职责 + +| 模块 | 文件 | 职责 | +|------|------|------| +| 数据模型 | base.py | 定义 DataLog、SignalData、ExcelDataResult 等核心数据结构 | +| 异常体系 | exceptions.py | 定义 MILSDKError 及其子类,统一异常处理 | +| 日志模块 | logging_config.py | 提供日志配置和获取功能 | +| 数据读取 | mil_read_data_excel.py | 解析 MIL 仿真数据 Excel,提取信号数据 | +| 用例读取 | mil_read_case_excel.py | 解析测试用例模板 Excel | +| 用例生成 | mil_create_data_excel.py | 根据模板生成测试用例 Excel | +| 用例更新 | mil_update_excel.py | 比较新旧数据,更新信号列 | + +### 依赖关系 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ main.py │ +│ (入口文件,演示 SDK 使用) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ src/core/__init__.py │ +│ (公共 API 统一导出模块) │ +└─────────────────────────────────────────────────────────────────┘ + │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ + ┌────────┐ ┌──────┐ ┌────────┐ ┌──────┐ ┌──────┐ + │ base │ │excep │ │logging│ │ mil_ │ │ mil_ │ + │ │ │tions │ │_conf │ │read_ │ │create│ + │ │ │ │ │ │ │data │ │_data │ + └────────┘ └──────┘ └───────┘ └──────┘ └──────┘ + │ │ + ▼ ▼ + ┌────────┐ ┌────────┐ + │ mil_ │ │ mil_ │ + │read_ │ │update_ │ + │case │ │excel │ + └────────┘ └────────┘ +``` + +--- + +## 快速开始 + +### 安装 ```bash pip install -e . ``` -## Usage - -```python -from src.core import read_excel_data, DataLog - -result = read_excel_data("path/to/simulation.xlsx") -print(result["signal1"]["datalog"]) # List[DataLog] -``` - -## Testing +### 开发依赖安装 ```bash -pytest tests/ -v +pip install -e ".[dev]" ``` -## Dependencies +### 基础使用 -- Python >= 3.10 -- openpyxl >= 3.0.0 \ No newline at end of file +```python +from src.core import setup_logging, read_excel_data, ExcelReaderConfig + +# 初始化日志 +setup_logging() + +# 读取仿真数据 +config = ExcelReaderConfig() +result = read_excel_data("simulation.xlsx", return_object=True, config=config) + +# 获取信号数据 +signal = result.get_signal("signal_name") +if signal: + for log in signal.datalog: + print(f"时间: {log.time}, 值: {log.value}") +``` + +--- + +## 数据模型 + +### DataLog + +仿真数据日志记录,表示单个时间点的数据。 + +```python +@dataclass +class DataLog: + time: float = 0.0 # 时间戳(秒) + value: str = "" # 信号值 +``` + +**属性说明**: + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| time | float | 0.0 | 时间戳,单位为秒 | +| value | str | "" | 信号值,可以是任意类型 | + +**示例**: + +```python +log = DataLog(time=1.5, value="on") +print(f"时间: {log.time}, 值: {log.value}") +``` + +--- + +### SignalData + +信号数据封装,包含信号的元数据和数据日志。 + +```python +@dataclass +class SignalData: + column: int = 0 # 列索引 + attributes: dict[str, str] = field(default_factory=dict) # 信号属性 + datalog: list[DataLog] = field(default_factory=list) # 数据日志列表 +``` + +**属性说明**: + +| 属性 | 类型 | 说明 | +|------|------|------| +| column | int | Excel 列索引 | +| attributes | dict[str, str] | 信号属性字典,键为行号,值为单元格值 | +| datalog | list[DataLog] | 数据日志列表 | + +**方法说明**: + +| 方法 | 返回值 | 说明 | +|------|--------|------| +| to_dict() | dict[str, Any] | 转换为字典格式 | + +**示例**: + +```python +signal = SignalData(column=2, attributes={"type": "boolean"}, datalog=[DataLog(0.0, "off")]) +data = signal.to_dict() +``` + +--- + +### ExcelDataResult + +Excel 数据读取结果封装,提供对 Excel 数据的类型安全访问。 + +```python +@dataclass +class ExcelDataResult: + sheet_name: str = "Scenario1" # 工作表名称 + source_row: int = 0 # Source: Input 所在行号 + signals: dict[str, SignalData] = field(default_factory=dict) # 信号字典 +``` + +**属性说明**: + +| 属性 | 类型 | 说明 | +|------|------|------| +| sheet_name | str | 工作表名称,默认 "Scenario1" | +| source_row | int | "Source: Input" 标记所在行号 | +| signals | dict[str, SignalData] | 信号名称到信号数据的映射 | + +**方法说明**: + +| 方法 | 参数 | 返回值 | 说明 | +|------|------|--------|------| +| get_signal() | name: str | SignalData \| None | 获取指定信号的数据 | +| get_signal_names() | - | list[str] | 获取所有信号名称 | +| to_dict() | - | dict[str, Any] | 转换为字典格式 | + +**示例**: + +```python +result = ExcelDataResult(sheet_name="Scenario1", source_row=3, signals={}) +signal = result.get_signal("signal_name") +names = result.get_signal_names() +data = result.to_dict() +``` + +--- + +### ExcelReaderConfig + +Excel 读取配置类,用于配置 Excel 文件的解析规则。 + +```python +@dataclass +class ExcelReaderConfig: + sheet_name: str = "Scenario1" # 工作表名称 + source_header: str = "Source: Input" # 数据源标记行文本 + time_column: int = 1 # 时间列索引(A 列) + header_row: int = 1 # 信号名称所在行号 + type_row: int = 3 # 信号类型所在行号 + interp_row: int = 6 # 插值类型所在行号 + data_start_row_offset: int = 1 # 数据起始行偏移量 + output_header: str = "Source: Output" # 输出标记行文本 + block_path_row: int = 4 # 块路径所在行号 +``` + +**属性说明**: + +| 属性 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| sheet_name | str | "Scenario1" | 工作表名称 | +| source_header | str | "Source: Input" | 数据源标记行文本 | +| time_column | int | 1 | 时间列索引(A 列) | +| header_row | int | 1 | 信号名称所在行号 | +| type_row | int | 3 | 信号类型所在行号 | +| interp_row | int | 6 | 插值类型所在行号 | +| data_start_row_offset | int | 1 | 相对于 source_row 的数据起始行偏移量 | +| output_header | str | "Source: Output" | 输出标记行文本 | +| block_path_row | int | 4 | 块路径所在行号 | + +**示例**: + +```python +config = ExcelReaderConfig( + sheet_name="Scenario1", + source_header="Source: Input", + time_column=1 +) +``` + +--- + +## 异常体系 + +异常模块位于 [exceptions.py](file:///f:/MyProject/mil_sdk/src/core/exceptions.py)。 + +### 异常层次 + +``` +Exception + └── MILSDKError (基础异常) + ├── ExcelReadError (文件读取错误) + ├── ExcelFormatError (格式错误) + ├── CaseDataError (用例数据错误) + └── ExcelWriteError (文件写入错误) +``` + +### MILSDKError + +基础异常类,所有 SDK 自定义异常的基类。 + +```python +class MILSDKError(Exception): + pass +``` + +### ExcelReadError + +Excel 文件读取错误,当文件不存在或读取失败时抛出。 + +```python +class ExcelReadError(MILSDKError): + """Excel 文件读取错误(文件不存在、权限问题等)""" + pass +``` + +**触发场景**: +- 文件不存在 +- 文件权限问题 +- 文件格式损坏 + +### ExcelFormatError + +Excel 格式错误,当 Excel 格式不符合预期时抛出。 + +```python +class ExcelFormatError(MILSDKError): + """Excel 格式错误(缺少 Sheet、格式不匹配等)""" + pass +``` + +**触发场景**: +- 缺少指定的 Sheet +- 缺少 "Source: Input" 标记 +- 缺少模板版本号 + +### CaseDataError + +用例数据错误,当用例数据异常时抛出。 + +```python +class CaseDataError(MILSDKError): + """用例数据错误(信号不存在、类型错误等)""" + pass +``` + +**触发场景**: +- 信号名称不存在 +- 时间格式错误 +- 缺少用例标题 + +### ExcelWriteError + +Excel 文件写入错误,当文件写入失败时抛出。 + +```python +class ExcelWriteError(MILSDKError): + """Excel 文件写入错误(权限问题、保存失败等)""" + pass +``` + +**触发场景**: +- 文件权限问题 +- 磁盘空间不足 +- 保存失败 + +**异常处理示例**: + +```python +from src.core import ( + MILSDKError, + ExcelReadError, + ExcelFormatError, + CaseDataError, + ExcelWriteError, + read_excel_data, +) + +try: + result = read_excel_data("data.xlsx") +except ExcelReadError as e: + print(f"文件读取失败: {e}") +except ExcelFormatError as e: + print(f"格式错误: {e}") +except MILSDKError as e: + print(f"SDK 错误: {e}") +``` + +--- + +## 日志模块 + +日志模块位于 [logging_config.py](file:///f:/MyProject/mil_sdk/src/core/logging_config.py)。 + +### setup_logging + +配置日志系统,设置日志级别、输出目标等。 + +```python +def setup_logging( + level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO", + log_file: str | Path | None = "logs/mil_sdk.log", + console_output: bool = True, +) -> logging.Logger +``` + +**参数说明**: + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| level | str | "INFO" | 日志级别,可选 "DEBUG"、"INFO"、"WARNING"、"ERROR" | +| log_file | str \| Path \| None | "logs/mil_sdk.log" | 日志文件路径,设为 None 则不写入文件 | +| console_output | bool | True | 是否输出到控制台 | + +**返回值**:根日志记录器 (logging.Logger) + +**日志格式**:`%(asctime)s [%(levelname)s] %(message)s` + +**示例**: + +```python +from src.core import setup_logging + +logger = setup_logging(level="DEBUG", log_file="logs/mil_sdk.log") +``` + +### get_logger + +获取指定名称的日志记录器。 + +```python +def get_logger(name: str) -> logging.Logger +``` + +**参数说明**: + +| 参数 | 类型 | 说明 | +|------|------|------| +| name | str | 日志记录器名称,通常使用 `__name__` | + +**返回值**:日志记录器实例 + +**示例**: + +```python +from src.core import get_logger + +logger = get_logger(__name__) +logger.info("This is an info message") +``` + +--- + +## Excel 读取模块 + +### read_excel_data + +读取 MIL 仿真 Excel 文件并解析信号数据。 + +```python +def read_excel_data( + excel_path: str, + return_object: bool = False, + config: ExcelReaderConfig | None = None +) -> DataDict | ExcelDataResult +``` + +**参数说明**: + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| excel_path | str | - | Excel 文件路径 | +| return_object | bool | False | 是否返回 ExcelDataResult 对象(推荐),False 保持向后兼容 | +| config | ExcelReaderConfig \| None | None | Excel 读取配置,默认使用 ExcelReaderConfig() | + +**返回值**: + +`return_object=False` 时返回包含以下键的字典(向后兼容): +- `wb`: Workbook 对象 +- `sheet`: Worksheet 对象 +- `source_row`: Source: Input 所在行号 +- `<信号名>`: 信号元数据(type, column, datalog) + +`return_object=True` 时返回 ExcelDataResult 对象(推荐): +- `sheet_name`: 工作表名称 +- `source_row`: Source: Input 所在行号 +- `signals`: 信号名称到信号数据的映射 + +**异常**: + +| 异常类型 | 触发条件 | +|----------|----------| +| ExcelReadError | 文件不存在或读取失败 | +| ExcelFormatError | Excel 格式不符合预期 | + +**示例**: + +```python +from src.core import read_excel_data + +# 使用推荐的方式 +result = read_excel_data("simulation.xlsx", return_object=True) +for name in result.get_signal_names(): + signal = result.get_signal(name) + if signal: + print(f"信号: {name}, 数据点: {len(signal.datalog)}") + +# 使用向后兼容的方式 +data = read_excel_data("simulation.xlsx") +wb = data["wb"] +sheet = data["sheet"] +``` + +### read_excel_case + +读取 Excel 用例模板。 + +```python +def read_excel_case( + excel_path: str, + data_dict: dict, + addTimeEn: bool, +) -> CaseDict +``` + +**参数说明**: + +| 参数 | 类型 | 说明 | +|------|------|------| +| excel_path | str | Excel 模板文件路径 | +| data_dict | dict | 信号数据字典 | +| addTimeEn | bool | 是否累加时间 | + +**返回值**:用例字典 + +**异常**: + +| 异常类型 | 触发条件 | +|----------|----------| +| ExcelReadError | 文件读取失败 | +| ExcelFormatError | 格式错误 | +| CaseDataError | 用例数据错误 | + +**用例字典结构**: + +```python +{ + "Sheet名称": { + "用例标题": { + "enable": bool, # 是否启用 + "step": { + "step0": { + "name": str, # 步骤名称 + "time": float, # 时间 + "action": dict # 操作字典 + }, + "step1": {...} + } + } + } +} +``` + +**示例**: + +```python +from src.core import read_excel_case, read_excel_data + +data_dict = read_excel_data("simulation.xlsx", return_object=False) +cases = read_excel_case("template.xlsx", data_dict, addTimeEn=True) + +for sheet_name, sheet_cases in cases.items(): + print(f"Sheet: {sheet_name}") + for case_name, case_data in sheet_cases.items(): + print(f" 用例: {case_name}, 启用: {case_data['enable']}") +``` + +--- + +## Excel 生成模块 + +### create_excel_case + +根据数据和用例模板生成测试用例 Excel 文件。 + +```python +def create_excel_case( + excel_path: str, + data_dict: dict, + sheets_dict: dict, +) -> None +``` + +**参数说明**: + +| 参数 | 类型 | 说明 | +|------|------|------| +| excel_path | str | 输出目录路径 | +| data_dict | dict | 数据字典 | +| sheets_dict | dict | 工作表字典 | + +**异常**: + +| 异常类型 | 触发条件 | +|----------|----------| +| CaseDataError | 数据异常 | +| ExcelWriteError | 文件写入错误 | + +**示例**: + +```python +from src.core import read_excel_data, read_excel_case, create_excel_case + +data_dict = read_excel_data("simulation.xlsx", return_object=False) +sheets_dict = read_excel_case("template.xlsx", data_dict, addTimeEn=True) +create_excel_case("output/", data_dict, sheets_dict) +``` + +--- + +## Excel 更新模块 + +### update_case_excel + +比较新旧数据差异,更新 Excel 文件中的信号列。 + +```python +def update_case_excel( + filename: str, + old_data: dict, + new_data: dict, +) -> None +``` + +**参数说明**: + +| 参数 | 类型 | 说明 | +|------|------|------| +| filename | str | Excel 文件路径 | +| old_data | dict | 旧数据字典 | +| new_data | dict | 新数据字典 | + +**功能说明**: +- 比较新旧数据中的信号差异 +- 在旧文件中添加新数据独有的信号列 +- 从旧文件中删除新数据不存在的信号列 + +**异常**: + +| 异常类型 | 触发条件 | +|----------|----------| +| CaseDataError | 数据类型错误 | + +**示例**: + +```python +from src.core import read_excel_data, update_case_excel + +new_data = read_excel_data("new_data.xlsx", return_object=False) +old_data = read_excel_data("old_data.xlsx", return_object=False) +update_case_excel("updated.xlsx", old_data, new_data) +``` + +--- + +## API 参考 + +### 公共导入 + +所有公共 API 均可通过以下方式导入: + +```python +from src.core import ( + # 数据模型 + DataLog, + SignalData, + ExcelDataResult, + ExcelReaderConfig, + # 核心函数 + read_excel_data, + read_excel_case, + create_excel_case, + update_case_excel, + # 异常 + MILSDKError, + ExcelReadError, + ExcelFormatError, + CaseDataError, + ExcelWriteError, + # 日志 + setup_logging, + get_logger, +) +``` + +### 函数签名汇总 + +| 函数 | 签名 | +|------|------| +| DataLog | `DataLog(time: float = 0.0, value: str = "")` | +| SignalData | `SignalData(column: int = 0, attributes: dict = {}, datalog: list = [])` | +| ExcelDataResult | `ExcelDataResult(sheet_name: str = "Scenario1", source_row: int = 0, signals: dict = {})` | +| ExcelReaderConfig | `ExcelReaderConfig(...)` | +| read_excel_data | `read_excel_data(excel_path: str, return_object: bool = False, config: ExcelReaderConfig \| None = None) -> DataDict \| ExcelDataResult` | +| read_excel_case | `read_excel_case(excel_path: str, data_dict: dict, addTimeEn: bool) -> CaseDict` | +| create_excel_case | `create_excel_case(excel_path: str, data_dict: dict, sheets_dict: dict) -> None` | +| update_case_excel | `update_case_excel(filename: str, old_data: dict, new_data: dict) -> None` | +| setup_logging | `setup_logging(level: str = "INFO", log_file: str \| Path \| None = "logs/mil_sdk.log", console_output: bool = True) -> logging.Logger` | +| get_logger | `get_logger(name: str) -> logging.Logger` | + +--- + +## 使用示例 + +### 基础用法 + +```python +from src.core import setup_logging, read_excel_data + +setup_logging() +result = read_excel_data("simulation.xlsx", return_object=True) + +signal_names = result.get_signal_names() +print(f"共解析 {len(signal_names)} 个信号") + +for name in signal_names: + signal = result.get_signal(name) + if signal: + print(f" {name}: {len(signal.datalog)} 个数据点") +``` + +### 完整工作流 + +```python +from pathlib import Path +from src.core import ( + setup_logging, + read_excel_data, + read_excel_case, + create_excel_case, + update_case_excel, +) + +def main() -> None: + setup_logging(level="INFO") + + excel_path = Path(__file__).parent + + data_dict = read_excel_data("new_data.xlsx", return_object=False) + old_data_dict = read_excel_data("old_data.xlsx", return_object=False) + + update_case_excel("new_case.xlsx", old_data_dict, data_dict) + + +if __name__ == "__main__": + main() +``` + +### 错误处理 + +```python +from src.core import ( + ExcelReadError, + ExcelFormatError, + CaseDataError, + MILSDKError, + read_excel_data, +) + +try: + result = read_excel_data("simulation.xlsx", return_object=True) +except ExcelReadError as e: + print(f"文件读取失败: {e}") + raise +except ExcelFormatError as e: + print(f"格式错误: {e}") + raise +except MILSDKError as e: + print(f"SDK 错误: {e}") + raise +``` + +--- + +## Excel 文件格式约定 + +### 仿真数据文件格式 + +Excel 仿真数据文件遵循以下格式约定: + +| 行号 | 内容 | +|------|------| +| 1 | 信号名称(time, signal1, signal2...) | +| 2 | 预留行(通常为空) | +| 3 | "Source: Input" 标记行,标识数据列开始 | +| 4 | 信号类型信息 | +| 5 | 块路径信息 | +| 6 | 插值类型信息 | +| 7+ | 时间-值数据对 | + +**示例结构**: + +| A | B | C | D | +|---|---|---|---| +| time | signal1 | signal2 | signal3 | +| | | | | +| Source: Input | Source: Input | Source: Input | Source: Input | +| type | boolean | float | boolean | +| BlockPath | /model/signal1 | /model/signal2 | /model/signal3 | +| Interp | const | linear | zero-order hold | +| | off | 0.0 | true | +| 1.0 | on | 1.5 | false | +| 2.0 | off | 2.0 | true | + +**要求**: +- Sheet 名称:`Scenario1` +- 时间列:必须存在且列名为 "time" +- 跳过列:列名为 "Parameter:"、"Value"、"BlockPath" 的列会被跳过 + +### 用例模板文件格式 + +用例模板 Excel 文件遵循以下格式约定: + +| Sheet 名称 | 内容 | +|-----------|------| +| Atech-Hefei | 模板版本号 | +| 其他 Sheet | 测试用例数据 | + +**用例数据格式**: + +| 列号 | 列名 | 说明 | +|------|------|------| +| 1 | TITLE | 用例标题 | +| 2 | STATUS | 状态(完成测试/未完成) | +| 3 | ACTION | 操作描述(signal1=value1; signal2=value2) | +| 4 | NAME | 步骤名称 | +| 5 | TIME | 时间 | + +**操作字符串格式**: +- 多条操作使用分号 `;` 分隔 +- 操作格式:`信号名=值` +- 示例:`signal1=on; signal2=0.5` + +--- + +## 版本历史 + +### v0.1.0 (当前版本) + +- 初始版本发布 +- 支持 Excel 数据文件读取和解析 +- 支持测试用例模板读取和生成 +- 支持新旧数据对比和更新 \ No newline at end of file