# 桌面仿真工具开发与协作工作流指南 > 来源:轴向磁拉力仿真 GUI 项目(Motor-CAD + PyQt5 + PyInstaller) > 适用:任何需要"科学计算内核 + 桌面 GUI + 打包 exe + Git 协作"的工程项目 > 日期:2026-08-27 --- ## 一、项目纪律(先立规矩再干活) | 纪律 | 具体做法 | 为什么 | |---|---|---| | 运行前 commit | 每次启动仿真/打包前 `git add -A && git commit` | 仿真可能改坏模型/脚本,可随时回退 | | 生成物不入库 | `output/`, `build/`, `dist/`, `*.log`, `*.spec` 写入 `.gitignore` | 二进制/临时文件膨胀仓库,diff 无意义 | | 版本化不覆盖 | 报告/结果文件名带版本号 V1/V2,旧版保留 | 出新版可回溯对比,避免误删 | | 时间戳记录 | 结果文件名含 `MMDD_HHMMSS`,对话/决策记入 `CONVERSATION_LOG.md` | 跨机器复现、追溯"什么时候改了什么" | | 原始文件只读 | 原始模型/数据文件不修改,操作在时间戳副本上进行 | 保护输入数据完整性 | ### `.gitignore` 模板 ``` # 生成物 output/ build/ dist/ *.log *.spec __pycache__/ *.pyc ``` --- ## 二、如何跑仿真(Motor-CAD + PyMotorCAD) ### 2.1 环境要求 | 项 | 要求 | |---|---| | 系统 | Windows(Motor-CAD 仅 Windows) | | Motor-CAD | 2026R1 (v261),安装路径如 `D:\Program Files\ANSYS Inc\v261\motorcad\` | | Python | ≥ 3.10,`pip install ansys-motorcad-core` | | 许可证 | FlexNet `ANSYSLMD_LICENSE_FILE=1055@localhost`,lmgrd + ansyslmd 都必须在跑 | ### 2.2 环境变量陷阱(AI 工具 shell 常踩) 非登录 shell(Kimi/Cursor/豆包等)可能**不继承机器级环境变量**,导致: - `MOTORCAD_ACTIVEX` 为空 → pymotorcad 找不到 Motor-CAD - `ANSYSLMD_LICENSE_FILE` 为空 → Motor-CAD 启动后 ~30s 静默退出,报 `psutil.NoSuchProcess` **解决**:运行前 inline 设置,或脚本内回退: ```python import os if not os.environ.get("MOTORCAD_ACTIVEX"): from ansys.motorcad.core import set_motorcad_exe set_motorcad_exe(r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe") ``` ### 2.3 核心方法:AFM 轴向力的唯一数据口 Motor-CAD 对轴向磁通电机 (AFM) 的轴向力**没有输出变量、没有 2D 图、帮助文档无记录**。唯一入口: ```python mc.get_magnetic_3d_graph_point(graph_name, section, node, timestep) # graph_name ∈ {Fr|Ft}_{Rotor|Stator}_{OL|OC}_Lumped ``` - **Fr(法向力)= 轴向力**(AFM 2.5D 展开模型沿用径向机命名) - Ft = 切向力(可由 Σ(Ft×r) 交叉核对转矩) - OL=负载, OC=空载 —— **一次求解两者全出** - 求解前必须开开关(默认关): ```python mc.set_variable("ElectromagneticForcesCalc_Load", True) mc.set_variable("ElectromagneticForcesCalc_OC", True) ``` - 节点首尾 0°/360° 重复,求和须去重:`if x[-1]-x[0]==360: 丢末点` - 净力 = 对全部去重节点求和 + 对全部径向切片求和 ### 2.4 .mot 参数语义陷阱(AFM 模板易错) | 参数 | 正确语义 | 常见误读 | |---|---|---| | `Magnet_Length` | 磁钢**轴向厚度** | 以为是长度 | | `Magnet_Thickness` | 磁钢环**径向深度** = (D_out−D_in)/2 | 以为是厚度 | | `CurrentDefinition=1` | RMS 口径 → 改电流设 `RMSCurrent` | 改 `PeakCurrent` 无效且不报错 | | `Magnet_Temperature` | 默认 **100°C 热态** | 以为是常温 20°C | | `Pole_Arc` | 某些转子类型不生效 | 以为是极弧 | | 极弧实际参数 | `Magnet_Arc_[ED]`(电角度) | — | ### 2.5 前台运行 + 窗口可见性 ```python mc = MotorCAD() # 前台启动(非 /SCRIPTING 隐藏模式) mc.set_visible(True) # 关键:/SCRIPTING 模式默认主窗口隐藏,任务栏有图标点不开 ``` > 实测:`/SCRIPTING` 模式下 Motor-CAD 主窗口 `IsWindowVisible=False`,任务栏图标是坐标 (−32000,−32000) 的代理窗口。`set_visible(True)` 后正常显示。这是**双机通用的默认行为**,不是个别机器问题。 ### 2.6 三判据验证(全过才采信结果) 1. **作用-反作用**:定/转子合力反号,偏差 < 5% 2. **转矩交叉**:Σ(Ft×r) vs 转矩图,偏差 < 10% 3. **解析量级**:F ≈ A·mean(B²)/(2μ₀) 与 FEA 同量级(解析偏高 ~1.4× 属正常) ### 2.7 仿真脚本标准结构 ```python # 1. 启动 Motor-CAD(set_visible 兜底) # 2. load_from_file(原始.mot) → save_to_file(时间戳副本.mot) # 3. set_variable 设置工况(RMSCurrent/ShaftSpeed/Airgap/Magnet_Temperature) # 4. 开力计算开关 # 5. do_magnetic_calculation() (~90-150s) # 6. 读 3D 力图 → 节点去重 → 切片求和 → 净力时间序列 # 7. 三判据计算 # 8. 输出 JSON + CSV(output/ 目录,不入库) # 9. finally: keep_open? 保持打开 : mc.quit() ``` --- ## 三、如何做 exe 应用(PyQt5 + PyInstaller) ### 3.1 技术选型 | 层 | 选择 | 理由 | |---|---|---| | GUI 框架 | **PyQt5** | 成熟稳定、控件丰富、QSS 样式表强大、工业软件风 | | 打包 | **PyInstaller `--onefile --windowed`** | 单 exe 双击即用、无控制台黑窗 | | 计算核心 | 独立 `solver.py` | 与 GUI 解耦,可单独测试/复用 | | GUI 主程序 | `main.py` | 仅负责界面 + 线程调度 + 信号槽 | ### 3.2 代码结构(关键:计算与界面分离) ``` project/ ├── solver.py # 计算核心:纯函数/类,无 GUI 依赖,可单元测试 ├── main.py # PyQt5 GUI:参数面板 + 日志 + 结果展示 + 线程 ├── requirements.txt # PyQt5, ansys-motorcad-core, pyinstaller ├── build.bat # 一键打包脚本 └── dist/ └── AppName.exe # 生成物(不入库) ``` ### 3.3 后台线程模式(避免 GUI 卡死) 仿真耗时 ~2 分钟,必须在子线程运行,否则 GUI 冻结: ```python class SolverWorker(QObject): log = pyqtSignal(str) # 实时日志 result = pyqtSignal(dict) # 结果数据 finished = pyqtSignal() error = pyqtSignal(str) def run(self): try: from solver import Solver s = Solver(..., log_cb=lambda m: self.log.emit(m)) r = s.run_single() self.result.emit(r) except Exception as e: self.error.emit(traceback.format_exc()) finally: self.finished.emit() # GUI 中启动 self.thread = QThread() self.worker = SolverWorker(params) self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.log.connect(self._append_log) self.worker.result.connect(self._on_result) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.thread.start() ``` ### 3.4 打包命令 ```bat pyinstaller --onefile --windowed --name AppName ^ --hidden-import solver ^ --collect-submodules ansys ^ main.py ``` - `--onefile`:单 exe(启动时解压到临时目录,首次 ~3-5s) - `--windowed`:无控制台窗口 - `--hidden-import solver`:solver.py 是动态导入,需显式声明 - `--collect-submodules ansys`:pymotorcad 子模块多,确保全部收集 ### 3.5 闪退防护(5 层,缺一不可) 打包成 `--windowed` 后,**任何未捕获异常都会导致静默闪退**(无控制台可看)。必须层层防护: | 层 | 防护 | 代码 | |---|---|---| | 1 | 全局异常钩子 | `sys.excepthook = custom_handler`(写日志文件 + 弹窗) | | 2 | 禁止末窗退出 | `app.setQuitOnLastWindowClosed(False)` | | 3 | 数据安全访问 | 所有 dict 访问用 `(d.get(k) or {}).get(k2)`,禁止链式 `.get().get()` | | 4 | 槽函数 try-except | 每个 `_on_xxx` 信号槽包裹 try-except,异常写日志不抛出 | | 5 | 计算对象 keepalive | 全局变量持有 MotorCAD 等长生命周期对象,防止 GC 时连带关闭子进程 | **全局异常钩子模板**: ```python def _excepthook(exc_type, exc_value, exc_tb): import traceback, time msg = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) with open(os.path.expanduser("~/app_error.log"), "a", encoding="utf-8") as f: f.write("\n=== %s ===\n%s" % (time.strftime("%Y-%m-%d %H:%M:%S"), msg)) QMessageBox.critical(None, "程序异常", str(exc_value)) sys.excepthook = _excepthook ``` **线程清理防竞态**: ```python def _on_finished(self): # 不要立即 self.worker = None / self.thread = None # 用 QTimer 延迟清理,避免与 deleteLater 竞态 QTimer.singleShot(100, lambda: setattr(self, 'worker', None)) QTimer.singleShot(100, lambda: setattr(self, 'thread', None)) ``` ### 3.6 打包后验证清单 1. 双击 exe,8 秒内不崩溃 → 基本初始化 OK 2. 跑一次完整仿真 → 确认不闪退、结果正常显示 3. 测试边界情况(空结果、部分字段缺失)→ 确认优雅降级 4. 检查 `~/app_error.log` 是否有未捕获异常 --- ## 四、GUI 外观风格要求(现代工业软件风) ### 4.1 设计原则 参考 ANSYS、JetBrains IDE、VS Code 的工业工具风格: - **简洁**:无多余装饰,信息层级清晰 - **比例得当**:字体大小、间距、卡片尺寸协调 - **功能优先**:结果数据醒目,操作路径短 - **无动画**:工程工具不需要花哨动效 ### 4.2 配色方案 | 角色 | 色值 | 用途 | |---|---|---| | 背景 | `#eef1f5` | 主窗口浅灰蓝 | | 卡片/面板 | `#ffffff` | 分组容器 | | 边框 | `#e2e8f0` | 卡片/输入框边框 | | 强调色 | `#2563eb` | 按钮、选中态、链接 | | 主文字 | `#1e293b` | 标题、重要数值 | | 次文字 | `#64748b` | 标签、说明 | | 弱文字 | `#94a3b8` | 占位符、辅助信息 | | 成功 | `#16a34a` | 通过判据、完成状态 | | 警告 | `#d97706` | 运行中、警告 | | 错误 | `#dc2626` | 失败、错误 | | 信息 | `#7c3aed` | 文件路径、解析值 | ### 4.3 字体比例 | 元素 | 字号 | 字重 | |---|---|---| | 全局基础 | 10pt | Regular | | 分组标题 | 11pt | 600 | | 输入框/按钮 | 10pt | 600(按钮) | | 结果大数字 | **22pt** | 700 | | 结果单位 | 11pt | Regular | | 结果标签 | 9pt | 500,字间距 1px | | 日志 | 9.5pt | Consolas 等宽 | | 状态栏 | 9.5pt | Regular | > 关键:结果数字用大字号(20-24pt),让用户一眼看到核心数据;标签用小字号弱化为辅助信息。 ### 4.4 布局规范 - 卡片圆角 **6-8px**,边框 1px `#e2e8f0` - 卡片内边距 **14-16px** - 控件间距 **8-10px** - 输入框最小高度 **22px**,圆角 6px - 按钮最小高度 **38-42px**(主操作按钮),圆角 6px - 左右分栏:左侧参数面板固定宽 **380-400px**,右侧结果+日志自适应 - 结果区在上(优先展示),日志区在下(辅助信息) ### 4.5 结果卡片设计 ``` ┌─────────────────────┐ │ 空载净轴向力 │ ← 9pt 弱色标签 │ 342.9 N │ ← 22pt 粗体数字 + 11pt 单位 └─────────────────────┘ ``` - 白色卡片,圆角 10px,边框 1px - 数字与单位水平排列,单位弱色 - 成功/不同工况用不同颜色区分数字 ### 4.6 日志区美化(拒绝大黑框) - **浅色终端风**:背景 `#f8fafc`,文字 `#334155` - 等宽字体 Consolas/Cascadia Code,行高 1.5 - **彩色分级**(HTML 格式 QTextEdit): - 结论/成功 → 绿色 `#16a34a` - 错误 → 红色 `#dc2626` - 警告 → 橙色 `#d97706` - 提示 → 蓝色 `#2563eb` - 阶段标题 → 深灰加粗 `#475569` - 文件路径 → 紫色 `#7c3aed` - 圆角边框,内边距 10px ### 4.7 状态栏 - 底部状态栏,深色背景 `#1e293b`,浅色文字 - 左侧:运行状态(就绪 / ● 运行中 / ✓ 完成 / ✗ 出错),颜色区分 - 右侧:总耗时等永久信息 ### 4.8 QSS 样式表要点 ```python app.setStyle("Fusion") # 基础风格,再叠加 QSS app.setStyleSheet(""" QGroupBox { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; margin-top: 16px; padding-top: 14px; font-weight: 600; font-size: 11pt; } QGroupBox::title { subcontrol-origin: margin; left: 14px; padding: 0 8px; background: #fff; } QPushButton { padding: 8px 20px; border-radius: 6px; background: #2563eb; color: #fff; border: 1px solid #2563eb; font-weight: 600; } QPushButton:hover { background: #1d4ed8; } QPushButton:disabled { background: #94a3b8; border-color: #94a3b8; } QLineEdit, QDoubleSpinBox, QSpinBox { padding: 6px 10px; border: 1px solid #cbd5e1; border-radius: 6px; background: #f8fafc; } QLineEdit:focus { border-color: #2563eb; background: #fff; } QTextEdit#logView { background: #f8fafc; color: #334155; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px; } QStatusBar { background: #1e293b; color: #e2e8f0; } QScrollBar:vertical { width: 10px; background: #f1f5f9; } QScrollBar::handle:vertical { background: #cbd5e1; border-radius: 5px; } """) ``` --- ## 五、Git 协作流程 ### 5.1 Pull 最新程序 ```bash # 1. 先看本地状态,确保无未提交改动 git status # 2. 拉取 git pull origin master # 3. 如果有冲突 → 手动解决后 git add + git commit # 4. 确认同步 git status -sb # 应显示 ## master...origin/master(无 ahead/behind) ``` **Pull 前检查清单**: - 工作区是否干净?有未提交改动先 commit 或 stash - 当前在哪个分支?`git branch` - 远程地址对不对?`git remote -v` ### 5.2 推送更新 ```bash # 1. 确保本地已 commit git add -A git commit -m "描述: 做了什么, 为什么" # 2. 先 pull 避免冲突(别人可能也推了) git pull origin master # 3. 再 push git push origin master # 4. 确认 git log --oneline -3 git status -sb ``` ### 5.3 Commit 信息规范 ``` <类型>: <简明描述> 类型可选: 新增 - 新功能/新文件 修复 - bug 修复 文档 - 文档/说明更新 重构 - 代码重构,功能不变 结果 - 仿真结果/数据 打包 - exe 打包相关 合并 - merge 示例: 修复: 仿真完成后GUI闪退 - None安全访问+全局异常钩子 新增: PyQt5桌面GUI - 参数面板+结果卡片+彩色日志 文档: README补充exe不入库原因 ``` ### 5.4 常见问题 | 现象 | 原因 | 解决 | |---|---|---| | `push rejected` | 远程有新提交,本地落后 | 先 `git pull`,解决冲突后再 push | | `merge conflict` | 同一文件同一行被两边修改 | 手动编辑冲突文件 → `git add` → `git commit` | | PowerShell 中 git push 报 exit code 1 | git 把进度信息写 stderr,PS 误判为错误 | 看 stdout 是否有 `branch -> branch`,有就是成功了 | | 大文件 push 慢/失败 | exe/二进制入库了 | 加入 `.gitignore`,`git rm --cached` 移除追踪 | ### 5.5 二进制交付物处理 - **exe 不入库**(40MB+ 二进制,git diff 无意义,仓库膨胀快) - 源码 + `build.bat` 入库,同事可自行打包 - 交付 exe 时:直接传文件 / 放网盘 / Release 页面 - 在 README 中明确说明"为什么仓库没有 exe"以及"如何自行打包" --- ## 六、排查速查表 | 现象 | 可能原因 | 排查方向 | |---|---|---| | Motor-CAD 启动后 30s 退出 | 许可证 ansyslmd 未运行 | 检查 ANSYS License Management Center | | pymotorcad 报 NoSuchProcess | 同上,或环境变量未继承 | inline export 两个环境变量 | | 力结果全为 0/空 | 力计算开关未开 | 确认 `ElectromagneticForcesCalc_Load/OC=True` | | 改电流无效 | CurrentDefinition=1 时改了 PeakCurrent | 改 `RMSCurrent` | | 力值比预期小很多 | 磁钢温度 100°C vs 报告 20°C | F∝Br²,按温度系数折算 | | GUI 点运行后卡死 | 仿真在主线程跑了 | 必须用 QThread 子线程 | | exe 双击闪退 | 未捕获异常 + windowed 模式 | 加全局 excepthook,查 `~/app_error.log` | | exe 运行正常但结果不显示 | 结果处理函数中 None 访问异常 | 所有 dict 访问加 `or {}` 安全兜底 | | Motor-CAD 窗口看不见 | /SCRIPTING 模式默认隐藏 | `mc.set_visible(True)` | | 打包后缺模块 | 动态导入未被 PyInstaller 发现 | 加 `--hidden-import` / `--collect-submodules` | --- ## 七、新项目快速启动清单 1. [ ] 初始化 git 仓库,写 `.gitignore` 2. [ ] 写 `AGENTS.md` / `README.md`(项目说明 + 纪律 + 快速开始) 3. [ ] 确认计算环境(软件版本、许可证、Python 包) 4. [ ] 先写**计算核心脚本**(无 GUI),跑通并验证数值 5. [ ] 建立结果验证判据(三判据或等效方法) 6. [ ] 提取计算逻辑为 `solver.py`(可独立调用) 7. [ ] 写 PyQt5 GUI(参数面板 + 日志 + 结果卡片 + 子线程) 8. [ ] 加 5 层闪退防护 9. [ ] 开发模式测试 → 边界测试 → 打包 exe 测试 10. [ ] 写 `build.bat` + `requirements.txt` + 使用说明 11. [ ] commit → 同事 review → push 12. [ ] 对话/决策记录入 `CONVERSATION_LOG.md`