来源:轴向磁拉力仿真 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
| 项 | 要求 |
|---|---|
| 系统 | 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 都必须在跑 |
非登录 shell(Kimi/Cursor/豆包等)可能不继承机器级环境变量,导致:
MOTORCAD_ACTIVEX 为空 → pymotorcad 找不到 Motor-CADANSYSLMD_LICENSE_FILE 为空 → Motor-CAD 启动后 ~30s 静默退出,报 psutil.NoSuchProcess解决:运行前 inline 设置,或脚本内回退:
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")
Motor-CAD 对轴向磁通电机 (AFM) 的轴向力没有输出变量、没有 2D 图、帮助文档无记录。唯一入口:
mc.get_magnetic_3d_graph_point(graph_name, section, node, timestep)
# graph_name ∈ {Fr|Ft}_{Rotor|Stator}_{OL|OC}_Lumped
求解前必须开开关(默认关):
mc.set_variable("ElectromagneticForcesCalc_Load", True)
mc.set_variable("ElectromagneticForcesCalc_OC", True)
if x[-1]-x[0]==360: 丢末点| 参数 | 正确语义 | 常见误读 |
|---|---|---|
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](电角度) |
— |
mc = MotorCAD() # 前台启动(非 /SCRIPTING 隐藏模式)
mc.set_visible(True) # 关键:/SCRIPTING 模式默认主窗口隐藏,任务栏有图标点不开
实测:
/SCRIPTING模式下 Motor-CAD 主窗口IsWindowVisible=False,任务栏图标是坐标 (−32000,−32000) 的代理窗口。set_visible(True)后正常显示。这是双机通用的默认行为,不是个别机器问题。
# 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()
| 层 | 选择 | 理由 |
|---|---|---|
| GUI 框架 | PyQt5 | 成熟稳定、控件丰富、QSS 样式表强大、工业软件风 |
| 打包 | PyInstaller --onefile --windowed |
单 exe 双击即用、无控制台黑窗 |
| 计算核心 | 独立 solver.py |
与 GUI 解耦,可单独测试/复用 |
| GUI 主程序 | main.py |
仅负责界面 + 线程调度 + 信号槽 |
project/
├── solver.py # 计算核心:纯函数/类,无 GUI 依赖,可单元测试
├── main.py # PyQt5 GUI:参数面板 + 日志 + 结果展示 + 线程
├── requirements.txt # PyQt5, ansys-motorcad-core, pyinstaller
├── build.bat # 一键打包脚本
└── dist/
└── AppName.exe # 生成物(不入库)
仿真耗时 ~2 分钟,必须在子线程运行,否则 GUI 冻结:
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()
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 子模块多,确保全部收集打包成 --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 时连带关闭子进程 |
全局异常钩子模板:
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
线程清理防竞态:
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))
~/app_error.log 是否有未捕获异常参考 ANSYS、JetBrains IDE、VS Code 的工业工具风格:
| 角色 | 色值 | 用途 |
|---|---|---|
| 背景 | #eef1f5 |
主窗口浅灰蓝 |
| 卡片/面板 | #ffffff |
分组容器 |
| 边框 | #e2e8f0 |
卡片/输入框边框 |
| 强调色 | #2563eb |
按钮、选中态、链接 |
| 主文字 | #1e293b |
标题、重要数值 |
| 次文字 | #64748b |
标签、说明 |
| 弱文字 | #94a3b8 |
占位符、辅助信息 |
| 成功 | #16a34a |
通过判据、完成状态 |
| 警告 | #d97706 |
运行中、警告 |
| 错误 | #dc2626 |
失败、错误 |
| 信息 | #7c3aed |
文件路径、解析值 |
| 元素 | 字号 | 字重 |
|---|---|---|
| 全局基础 | 10pt | Regular |
| 分组标题 | 11pt | 600 |
| 输入框/按钮 | 10pt | 600(按钮) |
| 结果大数字 | 22pt | 700 |
| 结果单位 | 11pt | Regular |
| 结果标签 | 9pt | 500,字间距 1px |
| 日志 | 9.5pt | Consolas 等宽 |
| 状态栏 | 9.5pt | Regular |
关键:结果数字用大字号(20-24pt),让用户一眼看到核心数据;标签用小字号弱化为辅助信息。
#e2e8f0┌─────────────────────┐
│ 空载净轴向力 │ ← 9pt 弱色标签
│ 342.9 N │ ← 22pt 粗体数字 + 11pt 单位
└─────────────────────┘
#f8fafc,文字 #334155#16a34a#dc2626#d97706#2563eb#475569#7c3aed#1e293b,浅色文字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; }
""")
# 1. 先看本地状态,确保无未提交改动
git status
# 2. 拉取
git pull origin master
# 3. 如果有冲突 → 手动解决后 git add + git commit
# 4. 确认同步
git status -sb # 应显示 ## master...origin/master(无 ahead/behind)
Pull 前检查清单:
git branchgit remote -v# 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
<类型>: <简明描述>
类型可选:
新增 - 新功能/新文件
修复 - bug 修复
文档 - 文档/说明更新
重构 - 代码重构,功能不变
结果 - 仿真结果/数据
打包 - exe 打包相关
合并 - merge
示例:
修复: 仿真完成后GUI闪退 - None安全访问+全局异常钩子
新增: PyQt5桌面GUI - 参数面板+结果卡片+彩色日志
文档: README补充exe不入库原因
| 现象 | 原因 | 解决 |
|---|---|---|
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 移除追踪 |
build.bat 入库,同事可自行打包| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 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 |
.gitignoreAGENTS.md / README.md(项目说明 + 纪律 + 快速开始)solver.py(可独立调用)build.bat + requirements.txt + 使用说明CONVERSATION_LOG.md