Forráskód Böngészése

init: Phase 1 M1 - project scaffold + single-point simulation verified

- Project structure: src/, scripts/, docs/, models/, output/, experience/
-规范文档: README.md, AGENTS.md, docs/KNOWLEDGE_BASE.md, docs/CONVERSATION_LOG.md
- Core engine: src/solver_core.py (Motor-CAD connection, param write-back verify, result extraction)
- Verification script: scripts/run_single.py
- M1 verified: MARS-12S10P SSSR model, single-point magnetic calc, all Phase 1 metrics extracted
  - Average Torque: 0.5219 Nm
  - Torque Ripple: 2.8150%
  - System Efficiency: 86.06%
  - Total Losses: 41.945 W
  - Solve time: 139.2s
- Reference cases included: axial_mag_pull, torqrippswap
- Reference books included: Gieras, Deng Qiuling
javen.ye 1 hete
commit
75347b6114
62 módosított fájl, 57397 hozzáadás és 0 törlés
  1. 45 0
      .gitignore
  2. 105 0
      AGENTS.md
  3. 1372 0
      DRSS V16.html
  4. 9080 0
      MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot
  5. 909 0
      PCB轴向磁通电机自动化仿真系统设计方案介绍.md
  6. 671 0
      PCB轴向磁通电机自动化仿真系统设计方案简介.html
  7. 144 0
      README.md
  8. 12 0
      axial_mag_pull-master/axial_mag_pull/.gitignore
  9. 41 0
      axial_mag_pull-master/axial_mag_pull/AGENTS.md
  10. 9 0
      axial_mag_pull-master/axial_mag_pull/CLAUDE.md
  11. 9080 0
      axial_mag_pull-master/axial_mag_pull/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot
  12. 9081 0
      axial_mag_pull-master/axial_mag_pull/MARS-12S10P_SSSR_D76-C150_V5.0-0819local.mot
  13. 37 0
      axial_mag_pull-master/axial_mag_pull/PLAN.md
  14. 45 0
      axial_mag_pull-master/axial_mag_pull/README.md
  15. 74 0
      axial_mag_pull-master/axial_mag_pull/REPRODUCE.md
  16. 72 0
      axial_mag_pull-master/axial_mag_pull/RESULTS.md
  17. 158 0
      axial_mag_pull-master/axial_mag_pull/axial_compare.py
  18. 241 0
      axial_mag_pull-master/axial_mag_pull/axial_force_final.py
  19. 221 0
      axial_mag_pull-master/axial_mag_pull/axial_force_run.py
  20. 123 0
      axial_mag_pull-master/axial_mag_pull/axial_probe.py
  21. 136 0
      axial_mag_pull-master/axial_mag_pull/axial_probe2.py
  22. 150 0
      axial_mag_pull-master/axial_mag_pull/axial_probe3.py
  23. 189 0
      axial_mag_pull-master/axial_mag_pull/axial_probe4.py
  24. 94 0
      axial_mag_pull-master/axial_mag_pull/docs/COMPARISON_V3.md
  25. 272 0
      axial_mag_pull-master/axial_mag_pull/docs/CONVERSATION_LOG.md
  26. 105 0
      axial_mag_pull-master/axial_mag_pull/docs/GUI_GUIDE.md
  27. 143 0
      axial_mag_pull-master/axial_mag_pull/docs/KNOWLEDGE_BASE.md
  28. 319 0
      axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V1.html
  29. BIN
      axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V1.pdf
  30. 388 0
      axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V2.html
  31. BIN
      axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V2.pdf
  32. 72 0
      axial_mag_pull-master/axial_mag_pull/gui_app/README.md
  33. 11 0
      axial_mag_pull-master/axial_mag_pull/gui_app/build.bat
  34. 790 0
      axial_mag_pull-master/axial_mag_pull/gui_app/main.py
  35. 3 0
      axial_mag_pull-master/axial_mag_pull/gui_app/requirements.txt
  36. 318 0
      axial_mag_pull-master/axial_mag_pull/gui_app/solver.py
  37. 25 0
      axial_mag_pull-master/axial_mag_pull/show_motorcad.py
  38. BIN
      axial_mag_pull-master/axial_mag_pull/轴向磁通电机轴向磁拉力计算与轴承选型校核报告V3.0-20260826.pdf
  39. 66 0
      docs/CONVERSATION_LOG.md
  40. 250 0
      docs/KNOWLEDGE_BASE.md
  41. 9080 0
      models/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot
  42. 159 0
      scripts/run_single.py
  43. 489 0
      src/solver_core.py
  44. 449 0
      torqrippswap-master/DESKTOP_APP_WORKFLOW.md
  45. 9080 0
      torqrippswap-master/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot
  46. 13 0
      torqrippswap-master/torqrippswap/.gitignore
  47. 303 0
      torqrippswap-master/torqrippswap/MOTORCAD_SCAN_KNOWLEDGE_BASE.md
  48. 274 0
      torqrippswap-master/torqrippswap/README_AI_TOOLS.md
  49. 61 0
      torqrippswap-master/torqrippswap/README_Central_Mag_Arc扫描.md
  50. 31 0
      torqrippswap-master/torqrippswap/build_gui_exe.ps1
  51. 75 0
      torqrippswap-master/torqrippswap/build_studio_exe.ps1
  52. 1006 0
      torqrippswap-master/torqrippswap/main.py
  53. 546 0
      torqrippswap-master/torqrippswap/motorcad_scan_gui.py
  54. 31 0
      torqrippswap-master/torqrippswap/package_magnet_arc_v2.ps1
  55. 3 0
      torqrippswap-master/torqrippswap/requirements.txt
  56. 32 0
      torqrippswap-master/torqrippswap/run_central_mag_arc_scan.ps1
  57. 240 0
      torqrippswap-master/torqrippswap/scan_central_mag_arc.py
  58. 32 0
      torqrippswap-master/torqrippswap/scan_parameters.json
  59. 11 0
      torqrippswap-master/torqrippswap/scan_parameters_magnet_arc.json
  60. 631 0
      torqrippswap-master/torqrippswap/solver.py
  61. BIN
      书籍与论文/轴向磁场无刷同步电机理论与设计-邓秋玲.pdf
  62. BIN
      书籍与论文/轴向磁通永磁无刷电机(原书第2版), Jacek F. Gieras.pdf.pdf

+ 45 - 0
.gitignore

@@ -0,0 +1,45 @@
+# === 生成物(不入库) ===
+output/
+runs/
+build/
+dist/
+*.log
+*.spec
+__pycache__/
+*.pyc
+*.pyo
+
+# === 仿真工作副本与结果数据 ===
+*_working.mot
+*_local.mot
+output_motorcad/
+FEResultsData/
+Emag/
+feasys/
+MessageLogs/
+*.mes
+
+# === 打包产物 ===
+*.zip
+*.pkg
+*.exe
+
+# === 经验库运行时数据 ===
+experience/*.db
+experience/*.db-journal
+
+# === IDE / OS ===
+.vscode/
+.idea/
+.DS_Store
+Thumbs.db
+
+# === 环境 ===
+.env
+.venv/
+venv/
+
+# === 临时文件 ===
+*.tmp
+*.bak
+*~

+ 105 - 0
AGENTS.md

@@ -0,0 +1,105 @@
+# AGENTS.md — AI 工具工作说明
+
+> 本文件面向 Claude Code / Codex / Cursor / 豆包等 AI 编程工具。
+> 接到任何任务前,**先读 [docs/KNOWLEDGE_BASE.md](docs/KNOWLEDGE_BASE.md)**。
+
+## 项目定位
+
+PCB轴向磁通电机自动化仿真系统 — 双系统解耦架构:
+- **系统一(Web端)**:方案生成与优化(Phase 2+)
+- **系统二(本地EXE)**:仿真执行(当前Phase 1重点)
+
+当前处于 **Phase 1 最小闭环开发**:本地GUI方案编辑 → Motor-CAD自动化仿真 → 结果输出 → 经验库积累。
+
+## 开始工作前必须阅读(按顺序)
+
+1. `docs/KNOWLEDGE_BASE.md` — 核心知识库(环境事实、参数语义、探测技术、SOP、已踩的坑)
+2. `README.md` — 项目说明、目录结构、快速开始
+3. `PCB轴向磁通电机自动化仿真系统设计方案介绍.md` — 完整设计方案V1.1(架构、接口、算法选型)
+4. 参考案例的知识库:
+   - `axial_mag_pull-master/axial_mag_pull/docs/KNOWLEDGE_BASE.md`
+   - `torqrippswap-master/torqrippswap/MOTORCAD_SCAN_KNOWLEDGE_BASE.md`
+
+## 参考案例代码(可直接复用/改造)
+
+| 功能 | 参考文件 | 说明 |
+|---|---|---|
+| Motor-CAD连接与前台可见 | `torqrippswap-master/torqrippswap/solver.py` → `MotorCADSolver.connect()` | `open_new_instance=True` + `set_visible(True)` |
+| 参数写入+回读校验 | `torqrippswap-master/torqrippswap/solver.py` → `_write_and_verify()` | 写入后get_variable回读,不一致抛异常 |
+| 每点基线重载 | `torqrippswap-master/torqrippswap/solver.py` → `run_single_point()` | 每点 `load_from_file` 防止污染 |
+| 结果导出与解析 | `torqrippswap-master/torqrippswap/solver.py` → `parse_export()` / `extract_all_metrics()` | 分号分隔CSV,中英文字段别名匹配 |
+| 扫描执行+逐点落盘 | `torqrippswap-master/torqrippswap/solver.py` → `run_scan()` | manifest+CSV+log+raw,每点flush |
+| Git preflight | `torqrippswap-master/torqrippswap/solver.py` → `git_preflight()` | 运行前检查仓库干净 |
+| AFM轴向力3D力图读取 | `axial_mag_pull-master/axial_mag_pull/axial_force_final.py` | `get_magnetic_3d_graph_point`,Fr=轴向力 |
+| 三判据校验 | `axial_mag_pull-master/axial_mag_pull/axial_force_final.py` | 作用反作用/转矩交叉/解析量级 |
+
+## 硬性工程约束(违者返工)
+
+### 1. 源码字符集
+所有 `.py` 和 `.ps1` 文件**必须只包含 ASCII 字符**。中文说明写在 Markdown 文档中,不能写进脚本注释、字符串、窗口标题。中文字段名用 `\uXXXX` Unicode 转义。
+
+检查命令:
+```powershell
+rg -n "[^\x00-\x7F]" --glob '*.py' --glob '*.ps1' .
+```
+
+### 2. 运行前必须 Git 提交
+实际启动 Motor-CAD 求解前必须满足:
+- Git 仓库存在且 HEAD 有效
+- 所有已跟踪文件无未提交修改
+
+GUI 内置 Git preflight,不满足时拒绝启动扫描。
+
+### 3. Motor-CAD 实例管理
+- 使用 `open_new_instance=True` 创建独立实例,**不要**连接已有实例(可能控制错误窗口)
+- 启动后必须 `set_visible(True)`(/SCRIPTING模式默认隐藏主窗口)
+- 每个扫描点开始前 `load_from_file(基线模型)`,结束后也重载基线
+
+### 4. 参数必须回读校验
+不能只调用 `set_variable`。必须:
+```python
+mc.set_variable(variable, value)
+applied = float(mc.get_variable(variable))
+if not math.isclose(applied, value, rel_tol=1e-8, abs_tol=1e-7):
+    raise RuntimeError(...)
+```
+回读不一致时将该点标记为 FAILED,保存错误并继续下一点。
+
+### 5. 结果逐点落盘
+- 每个点完成后立即写 CSV 并 flush,**不能**等整批完成后一次性保存
+- 失败点记录错误并继续
+- 运行目录结构:`output/<timestamp>_<scan_name>/` 含 manifest.json + scan_results.csv + program_log.log + raw/
+
+### 6. 原始模型只读
+- 原始 `.mot` 文件不修改
+- 所有操作在 Motor-CAD 内存中进行,或另存时间戳副本
+- models/ 目录下的模型文件视为只读
+
+### 7. 生成物不入库
+- `output/`、`runs/`、`build/`、`dist/`、`*.log`、`*.spec` 不入库
+- 关键数值转录进入库的文档(RESULTS.md / 报告)
+
+## 环境变量陷阱(AI shell 常踩)
+
+非登录 shell 可能不继承机器级环境变量:
+- `MOTORCAD_ACTIVEX` 为空 → pymotorcad 找不到 Motor-CAD
+- `ANSYSLMD_LICENSE_FILE` 为空 → Motor-CAD 启动后 ~30s 静默退出
+
+脚本内回退:
+```python
+import os
+if not os.environ.get("MOTORCAD_ACTIVEX"):
+    from ansys.motorcad.core import set_motorcad_exe
+    candidate = r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe"
+    if os.path.exists(candidate):
+        set_motorcad_exe(candidate)
+```
+
+## 不要做的事
+
+- 不要猜 Motor-CAD 变量名 — 先从 .mot、已有参数表或探测结果确认
+- 不要在主线程运行仿真(GUI会卡死)— 必须用 QThread 子线程
+- 不要修改原始 .mot 文件
+- 不要把生成物提交到 Git
+- 不要在 .py 文件中写中文字符(用 Unicode 转义或放 Markdown)
+- 不要用 `open_new_instance=False` 连接已有实例

+ 1372 - 0
DRSS V16.html

@@ -0,0 +1,1372 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>PCB轴向磁通电机设计-双转子单定子(无铁芯) - Stackup Rev</title>
+<style>
+    /* =========================================
+       CSS 样式定义区 (保持原有样式不变)
+       ========================================= */
+    :root {
+        --primary-dark: #1e293b;
+        --primary-blue: #2563eb;
+        --accent-teal: #059669;
+        --danger-red: #dc2626;
+        --bg-color: #f8fafc;
+        --border-light: #cbd5e1;
+        --text-main: #334155;
+        --input-bg: #fff7ed;      
+        --header-height: 80px;
+    }
+
+    body {
+        font-family: "Inter", "Segoe UI", "Microsoft YaHei", sans-serif;
+        background-color: var(--bg-color);
+        color: var(--text-main);
+        margin: 0;
+        display: flex;
+        flex-direction: column;
+        height: 100vh;
+        overflow: hidden;
+    }
+
+    /* 顶部导航栏样式 */
+    .sticky-header {
+        height: var(--header-height);
+        background: #ffffff;
+        border-bottom: 1px solid var(--border-light);
+        display: flex;
+        align-items: center;
+        padding: 0 30px;
+        box-shadow: 0 4px 12px -2px rgba(0, 0, 0, 0.08);
+        flex-shrink: 0;
+        justify-content: space-between;
+        z-index: 100;
+    }
+
+    .app-title { font-size: 24px; font-weight: 800; color: var(--primary-dark); display:flex; align-items:center; gap:12px;}
+    .app-subtitle { font-size: 13px; color: #64748b; margin-top: 4px; font-weight: 500;}
+    .version-tag { background: #dcfce7; color: #166534; padding: 2px 6px; border-radius: 4px; font-size: 11px; margin-left: 8px; vertical-align: middle; }
+
+    /* 自动保存状态指示器 */
+    .save-status {
+        font-size: 11px;
+        color: #059669;
+        background: #ecfdf5;
+        padding: 2px 8px;
+        border-radius: 4px;
+        margin-left: 12px;
+        opacity: 0;
+        transition: opacity 0.3s;
+    }
+    .save-status.show { opacity: 1; }
+    .save-status.saving { color: #d97706; background: #fffbeb; }
+
+    /* HUD */
+    .hud-grid { display: flex; gap: 25px; align-items: center; }
+    .hud-item { text-align: center; }
+    .hud-label { font-size: 12px; color: #94a3b8; text-transform: uppercase; font-weight: 700; margin-bottom: 4px;}
+    .hud-val { font-size: 30px; font-weight: 700; color: var(--primary-dark); font-family: "Consolas", monospace; }
+    .hud-unit { font-size: 12px; color: #cbd5e1; margin-left: 2px; font-weight: 600; }
+    .val-highlight { color: var(--primary-blue); }
+    .val-loss { color: #dc2626; } 
+    .val-cost { color: #d97706; }
+
+    .btn-action {
+        background: var(--primary-blue); color: white; border: none; padding: 12px 24px;
+        border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 15px;
+        box-shadow: 0 4px 6px rgba(37, 99, 235, 0.2); transition: all 0.2s;
+        display: flex; align-items: center; gap: 8px;
+    }
+    .btn-action:hover { background: #1d4ed8; transform: translateY(-1px); }
+
+    /* 主容器布局 */
+    .container { display: flex; flex: 1; overflow: hidden; }
+
+    /* 侧边栏样式 */
+    .sidebar {
+        width: 260px; background: var(--primary-dark); color: #f1f5f9;
+        display: flex; flex-direction: column; flex-shrink: 0; padding-top: 20px;
+    }
+    .nav-group-title { padding: 20px 25px 10px; font-size: 12px; color: #94a3b8; font-weight: 800; letter-spacing: 1px; }
+    .nav-btn {
+        padding: 16px 25px; border: none; background: transparent; color: #cbd5e1;
+        text-align: left; cursor: pointer; border-right: 4px solid transparent;
+        transition: all 0.2s; font-size: 15px; display: flex; align-items: center; gap: 12px;
+    }
+    .nav-btn.active { background: rgba(37, 99, 235, 0.2); color: white; border-right-color: var(--primary-blue); font-weight: 600; }
+    .nav-btn svg { width: 20px; height: 20px; opacity: 0.7; }
+    .nav-btn.active svg { opacity: 1; color: var(--primary-blue); }
+    
+    /* 内容区样式 */
+    .main-content {
+        flex: 1; padding: 30px 50px; overflow-y: auto; scroll-behavior: smooth;
+        background-image: radial-gradient(#cbd5e1 1.5px, transparent 1.5px); background-size: 24px 24px;
+    }
+
+    /* --- 表格核心样式 --- */
+    h2 { 
+        font-size: 22px; color: var(--primary-dark); border-left: 6px solid var(--primary-blue); 
+        padding-left: 15px; margin: 0 0 25px 0; font-weight: 800;
+    }
+    h3 {
+        font-size: 18px; color: #475569; margin: 25px 0 15px 0; font-weight: 700;
+    }
+    
+    .section-card {
+        background: white; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05);
+        padding: 30px; margin-bottom: 30px; border: 1px solid var(--border-light);
+    }
+
+    table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 15px; table-layout: fixed; margin-bottom: 25px;}
+    
+    thead th {
+        background: #f1f5f9; color: #475569; font-weight: 700; text-align: left;
+        padding: 14px 15px; border-bottom: 2px solid #cbd5e1;
+        font-size: 14px; letter-spacing: 0.5px;
+    }
+
+    td { 
+        padding: 14px 15px; border-bottom: 1px solid #e2e8f0; vertical-align: middle; 
+        color: #1e293b; line-height: 1.6;
+    }
+
+    /* 列宽定义 */
+    .col-param { width: 14%; font-weight: 600; font-size: 16px; color: #334155; }
+    .col-sym   { width: 8%;   font-weight: 600; font-size: 16px; color: #334155; }
+    .col-val   { width: 14%; font-weight: 600; font-size: 16px; color: #334155;}
+    .col-unit  { width: 8%;  font-weight: 600; font-size: 16px; color: #334155;}
+    .col-form  { width: 28%;   font-weight: 600; font-size: 16px; color: #334155; }
+    .col-def   { width: 28%; font-weight: 600; font-size: 16px; color: #334155;  }
+
+    /* 变量高亮样式 */
+    .col-def span { color: #000; font-weight: 600; font-family: "Times New Roman", serif; font-style: italic; margin-right: 2px;}
+
+    /* 输入框样式 */
+    input[type="number"], select {
+        width: 100%; padding: 10px 12px; border: 1px solid #cbd5e1; border-radius: 6px;
+        background: var(--input-bg); font-family: "JetBrains Mono", monospace; font-weight: 700; color: #c2410c;
+        font-size: 15px; transition: all 0.2s; box-sizing: border-box;
+    }
+    input[type="number"]:focus { border-color: var(--primary-blue); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); outline: none; }
+    
+    /* 结果数字样式 */
+    .res-val { color: var(--accent-teal); font-weight: 700; font-family: "JetBrains Mono", monospace; font-size: 17px; }
+    .res-bad { color: var(--danger-red); }
+    
+    /* 状态指示样式 */
+    .status-ok { color: var(--accent-teal); background: #ecfdf5; padding: 4px 8px; border-radius: 4px; border: 1px solid #10b981; display: inline-block;}
+    .status-warn { color: var(--danger-red); background: #fef2f2; padding: 4px 8px; border-radius: 4px; border: 1px solid #ef4444; display: inline-block;}
+
+    /* 页面切换动画 */
+    .page-section { display: none; animation: fadeIn 0.2s ease-out; }
+    .page-section.active { display: block; }
+    @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
+
+</style>
+</head>
+<body>
+
+    <header class="sticky-header">
+        <div class="app-brand">
+            <span class="app-title">
+                <svg width="28" height="28" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
+                Axial PCB Motor Design
+                <span class="version-tag">V16</span>
+                <span id="saveStatus" class="save-status">已自动保存</span>
+            </span>
+            <span class="app-subtitle">PCB轴向磁通电机设计 </span>
+        </div>
+        
+        <div class="hud-grid">
+            <div class="hud-item">
+                <div class="hud-label">额定功率</div>
+                <div class="hud-value-group"><span class="hud-val" id="hud_Pnom">0</span><span class="hud-unit">W</span></div>
+            </div>
+
+            <div class="hud-item">
+                <div class="hud-label">额定转矩</div>
+                <div class="hud-value-group"><span class="hud-val val-highlight" id="hud_T">0.00</span><span class="hud-unit">Nm</span></div>
+            </div>
+            
+            <div class="hud-item">
+                <div class="hud-label">电机效率</div>
+                <div class="hud-value-group"><span class="hud-val val-highlight" id="hud_Eff">0.0</span><span class="hud-unit">%</span></div>
+            </div>
+            
+             <div class="hud-item">
+                <div class="hud-label">总损耗</div>
+                <div class="hud-value-group"><span class="hud-val val-loss" id="hud_Loss">0</span><span class="hud-unit">W</span></div>
+            </div>
+
+            <div class="hud-item">
+                <div class="hud-label">预估成本</div>
+                <div class="hud-value-group"><span class="hud-val val-cost" id="hud_Cost">0</span><span class="hud-unit">¥</span></div>
+            </div>
+
+            <div class="hud-item">
+                <div class="hud-label">绕阻温度</div>
+                <div class="hud-value-group"><span class="hud-val" id="hud_Temp">0</span><span class="hud-unit">℃</span></div>
+            </div>
+            
+            <div class="hud-item">
+                <div class="hud-label">载流校验</div>
+                <div class="hud-value-group"><span class="hud-val" style="font-size:24px;" id="hud_IPC">--</span></div>
+            </div>
+        </div>
+
+        <button class="btn-action" onclick="app.ui.exportExcel()">
+            <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
+            导出结果
+        </button>
+    </header>
+
+    <div class="container">
+        <nav class="sidebar">
+            <div class="nav-group-title">DESIGN MODULES</div>
+            <button class="nav-btn active" onclick="app.ui.nav('input')">
+                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
+                1. 规格与几何 (Input)
+            </button>
+            <button class="nav-btn" onclick="app.ui.nav('stator')">
+                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
+                2. 定子参数 (Stator)
+            </button>
+            <button class="nav-btn" onclick="app.ui.nav('mag')">
+                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
+                3. 磁路分析 (Magnetic)
+            </button>
+            
+            <div class="nav-group-title">ANALYSIS MODULES</div>
+            <button class="nav-btn" onclick="app.ui.nav('perf')">
+                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
+                4. 性能指标 (Perf)
+            </button>
+            <button class="nav-btn" onclick="app.ui.nav('loss')">
+                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path></svg>
+                5. 损耗分析 (Loss)
+            </button>
+            <button class="nav-btn" onclick="app.ui.nav('therm')">
+                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.879 16.121A3 3 0 1012.015 11L11 14H9c0 .768.293 1.536.879 2.121z"></path></svg>
+                6. 热分析 (Thermal)
+            </button>
+            <button class="nav-btn" onclick="app.ui.nav('cost')">
+                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
+                7. 成本明细 (Cost)
+            </button>
+            <button class="nav-btn" onclick="app.ui.nav('ipc')">
+                <svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
+                8. 载流校验 (IPC-2152)
+            </button>
+        </nav>
+
+        <main class="main-content">
+            <section id="input" class="page-section active">
+                <div class="section-card">
+                    <h2>1. 设计规格与几何输入 (Input)</h2>
+                    
+                    <h3>A. 性能规格 (Specifications)</h3>
+                    <table>
+                        <thead>
+                            <tr><th class="col-param">参数名称</th><th class="col-sym">符号</th><th class="col-val">输入值</th><th class="col-unit">单位</th><th class="col-form">计算公式 / 说明</th><th class="col-def">公式变量物理含义</th></tr>
+                        </thead>
+                        <tbody>
+                            <tr><td>额定功率</td><td>P<sub>nom</sub></td><td><input type="number" id="P_nom" value="300" oninput="app.calculate()"></td><td>W</td><td>User Input</td><td>设计目标机械输出功率</td></tr>
+                            <tr><td>额定转速</td><td>n<sub>nom</sub></td><td><input type="number" id="n_nom" value="3000" oninput="app.calculate()"></td><td>rpm</td><td>User Input</td><td>额定工作点机械转速</td></tr>
+                            <tr><td>母线电压</td><td>V<sub>dc</sub></td><td><input type="number" id="V_dc" value="48" oninput="app.calculate()"></td><td>V</td><td>User Input</td><td>直流供电电压</td></tr>
+                            <tr><td>极对数</td><td>p</td><td><input type="number" id="p" value="6" oninput="app.calculate()"></td><td>-</td><td>User Input</td><td><span>p</span>: 磁极对数 (总极数=2p)</td></tr>
+                        </tbody>
+                    </table>
+
+                    <h3>B. 几何尺寸 (Geometry)</h3>
+                    <table>
+                        <thead>
+                            <tr><th class="col-param">参数名称</th><th class="col-sym">符号</th><th class="col-val">输入值</th><th class="col-unit">单位</th><th class="col-form">计算公式 / 说明</th><th class="col-def">公式变量物理含义</th></tr>
+                        </thead>
+                        <tbody>
+                            <tr><td>定子外径</td><td>D<sub>out</sub></td><td><input type="number" id="D_out" value="100" oninput="app.calculate()"></td><td>mm</td><td>User Input</td><td>PCB线圈区外边缘直径</td></tr>
+                            <tr><td>定子内径</td><td>D<sub>in</sub></td><td><input type="number" id="D_in" value="40" oninput="app.calculate()"></td><td>mm</td><td>User Input</td><td>PCB线圈区内孔直径</td></tr>
+                            <tr><td>机械气隙</td><td>g</td><td><input type="number" id="g_mech" value="0.5" step="0.1" oninput="app.calculate()"></td><td>mm</td><td>User Input (Single Side)</td><td>PCB表面到转子磁钢表面的距离</td></tr>
+                            <tr><td>磁铁厚度</td><td>h<sub>m</sub></td><td><input type="number" id="h_m" value="3.0" step="0.5" oninput="app.calculate()"></td><td>mm</td><td>User Input</td><td>单侧转子上贴的磁钢厚度</td></tr>
+                        </tbody>
+                    </table>
+
+                    <h3>C. PCB 叠层与绕组 (IPC Stack-up)</h3>
+                    <table>
+                        <thead>
+                            <tr><th class="col-param">参数名称</th><th class="col-sym">符号</th><th class="col-val">输入值</th><th class="col-unit">单位</th><th class="col-form">计算公式 / 说明</th><th class="col-def">公式变量物理含义</th></tr>
+                        </thead>
+                        <tbody>
+                            <tr><td>PCB层数</td><td>N<sub>lyr</sub></td><td><input type="number" id="layers" value="8" step="2" oninput="app.calculate()"></td><td>-</td><td>User Input</td><td>必须为偶数 (4, 6, 8...)</td></tr>
+                            
+                            <tr><td>基底铜厚</td><td>t<sub>oz</sub></td><td><input type="number" id="oz" value="3" oninput="app.calculate()"></td><td>oz</td><td>User Input</td><td>内层物理铜厚 (1oz≈35um)</td></tr>
+                            
+                            <tr><td>外层电镀</td><td>t<sub>plate</sub></td><td><input type="number" id="t_plate" value="25" step="5" oninput="app.calculate()"></td><td>μm</td><td>Process Value</td><td>外层孔金属化增加的厚度</td></tr>
+
+                            <tr><td>芯板厚度</td><td>t<sub>core</sub></td><td><input type="number" id="t_core" value="0.15" step="0.01" oninput="app.calculate()"></td><td>mm</td><td>Material (Core)</td><td>FR4 覆铜板绝缘层厚度</td></tr>
+
+                            <tr><td>PP片厚度</td><td>t<sub>pp</sub></td><td><input type="number" id="t_pp" value="0.10" step="0.01" oninput="app.calculate()"></td><td>mm</td><td>Material (Prepreg)</td><td>层间粘结片压合后厚度</td></tr>
+                            
+                            <tr><td>阻焊厚度</td><td>t<sub>mask</sub></td><td><input type="number" id="t_mask" value="0.02" step="0.005" oninput="app.calculate()"></td><td>mm</td><td>Solder Mask</td><td>单面绿油/黑油厚度</td></tr>
+
+                            <tr><td>每层每相匝数</td><td>N<sub>t/l</sub></td><td><input type="number" id="turns_layer" value="5" oninput="app.calculate()"></td><td>-</td><td>User Input</td><td>单层内一相绕组的圈数</td></tr>
+                            <tr><td>线宽</td><td>w<sub>tr</sub></td><td><input type="number" id="w_trace" value="0.6" step="0.05" oninput="app.calculate()"></td><td>mm</td><td>User Input</td><td>线圈铜导线的宽度</td></tr>
+                            <tr><td>端部长度系数</td><td>k<sub>end</sub></td><td><input type="number" id="k_end" value="1.2" step="0.1" oninput="app.calculate()"></td><td>-</td><td>User Input</td><td>端部绕组延伸比例</td></tr>
+                        </tbody>
+                    </table>
+
+                    <h3>D. 材料属性 (Materials)</h3>
+                    <table>
+                        <thead>
+                            <tr><th class="col-param">参数名称</th><th class="col-sym">符号</th><th class="col-val">输入/选择</th><th class="col-unit">单位</th><th class="col-form">计算公式 / 说明</th><th class="col-def">公式变量物理含义</th></tr>
+                        </thead>
+                        <tbody>
+                            <tr>
+                                <td>磁钢牌号</td><td>Grade</td>
+                                <td>
+                                    <select id="mag_grade" onchange="app.ui.updateMagGrade()">
+                                        <option value="ATMAX50SH" selected>ATMAX50SH (Br=1.42T)</option>
+                                        <option value="N35">N35 (Br=1.17T)</option>
+                                        <option value="N52">N52 (Br=1.45T)</option>
+                                        <option value="custom">-- 自定义 --</option>
+                                    </select>
+                                </td>
+                                <td>-</td><td>Database Selection</td><td>选择牌号自动填入Br</td>
+                            </tr>
+                            <tr><td>剩磁</td><td>B<sub>r</sub></td><td><input type="number" id="Br" value="1.425" step="0.01" oninput="app.calculate()"></td><td>T</td><td>From Material Property</td><td>磁铁材料的剩余磁感应强度</td></tr>
+                            <tr><td>相对磁导率</td><td>μ<sub>r</sub></td><td><input type="number" id="mu_r" value="1.05" step="0.01" oninput="app.calculate()"></td><td>-</td><td>Material Constant</td><td>磁铁相对于真空的导磁率</td></tr>
+                            <tr><td>极弧系数</td><td>α<sub>p</sub></td><td><input type="number" id="alpha_p" value="0.75" step="0.05" oninput="app.calculate()"></td><td>-</td><td>Design Factor</td><td>磁钢宽度占极距的比例(0~1)</td></tr>
+                            <tr><td>磁钢电导率</td><td>σ<sub>mag</sub></td><td><input type="number" id="sigma_mag" value="625000" step="1000" oninput="app.calculate()"></td><td>S/m</td><td>Material Property</td><td>钕铁硼电导率(典型0.6~0.8×10⁶ S/m)</td></tr>
+                            <tr><td>铜电阻率</td><td>ρ<sub>cu</sub></td><td><input type="number" id="rho_cu" value="0.0178" step="0.0001" oninput="app.calculate()"></td><td>Ω·mm²/m</td><td>Physical Constant</td><td>20℃下铜的电阻率</td></tr>
+                            <tr><td>铜温漂系数</td><td>α<sub>cu</sub></td><td><input type="number" id="alpha_cu" value="0.00393" step="0.00001" oninput="app.calculate()"></td><td>1/K</td><td>Physical Constant</td><td>电阻随温度变化的线性系数</td></tr>
+                            <tr><td>磁体密度</td><td>ρ<sub>mag</sub></td><td><input type="number" id="rho_mag" value="7.5" step="0.1" oninput="app.calculate()"></td><td>g/cm³</td><td>Material Property</td><td>用于计算磁钢重量及成本</td></tr>
+                            <tr><td>PCB板材Tg</td><td>T<sub>g</sub></td><td><input type="number" id="Tg" value="130" oninput="app.calculate()"></td><td>°C</td><td>Material Limit</td><td>玻璃化转变温度 (FR4通常130-170)</td></tr>
+                            <tr><td>空气密度</td><td>ρ<sub>air</sub></td><td><input type="number" id="rho_air" value="1.225" step="0.001" oninput="app.calculate()"></td><td>kg/m³</td><td>Standard Atmosphere</td><td>20℃标准大气密度</td></tr>
+                            <tr><td>摩擦系数</td><td>C<sub>f</sub></td><td><input type="number" id="cf_fric" value="0.01" step="0.001" oninput="app.calculate()"></td><td>-</td><td>Empirical Value</td><td>转盘风阻/轴承摩擦综合系数(0.005~0.03)</td></tr>
+                        </tbody>
+                    </table>
+
+                    <h3>E. 修正系数 (Correction Factors)</h3>
+                    <table>
+                        <thead>
+                            <tr><th class="col-param">参数名称</th><th class="col-sym">符号</th><th class="col-val">输入值</th><th class="col-unit">单位</th><th class="col-form">逻辑说明</th><th class="col-def">物理含义</th></tr>
+                        </thead>
+                        <tbody>
+                            <tr>
+                                <td>漏磁系数</td><td>k<sub>leak</sub></td>
+                                <td><input type="number" id="k_leak" value="0.95" step="0.01" oninput="app.calculate()"></td>
+                                <td>-</td>
+                                <td>经验值 (0.9~0.98)</td>
+                                <td>考虑极间漏磁导致的磁通损失</td>
+                            </tr>
+                            <tr>
+                                <td>绕组系数</td><td>k<sub>w</sub></td>
+                                <td><input type="number" id="k_w" value="0.95" step="0.01" oninput="app.calculate()"></td>
+                                <td>-</td>
+                                <td>k<sub>w</sub> = k<sub>p</sub> × k<sub>d</sub></td>
+                                <td>基波绕组系数 (分布效应与短距效应)</td>
+                            </tr>
+                            <tr>
+                                <td>功率因数</td><td>PF</td>
+                                <td><input type="number" id="PF" value="0.95" step="0.01" oninput="app.calculate()" min="0.8" max="1.0"></td>
+                                <td>-</td>
+                                <td>PCB轴向电机典型值 (0.92-0.98)</td>
+                                <td>电压与电流相位差的余弦值</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </section>
+
+            <section id="stator" class="page-section">
+                <div class="section-card">
+                    <h2>2. 定子与电阻计算 (Stator)</h2>
+                    <table>
+                        <thead>
+                            <tr><th class="col-param">参数名称</th><th class="col-sym">符号</th><th class="col-val">计算结果</th><th class="col-unit">单位</th><th class="col-form">计算公式 </th><th class="col-def">公式变量物理含义</th></tr>
+                        </thead>
+                        <tbody>
+                            <tr><td>PCB总厚度</td><td>h<sub>s</sub></td><td><span class="res-val" id="res_hs">0</span></td><td>mm</td><td>h<sub>s</sub> = H<sub>mask</sub> + H<sub>cu</sub> + H<sub>dielectric</sub></td><td>IPC 叠层计算值(含Core/PP/铜/阻焊)</td></tr>
+                            <tr><td>平均半径</td><td>R<sub>avg</sub></td><td><span class="res-val" id="res_Ravg">0</span></td><td>mm</td><td>R<sub>avg</sub> = (D<sub>out</sub> + D<sub>in</sub>) / 4</td><td><span>D<sub>out</sub></span>:定子外径, <span>D<sub>in</sub></span>:定子内径</td></tr>
+                            <tr><td>每相总匝数</td><td>N<sub>ph</sub></td><td><span class="res-val" id="res_Nph">0</span></td><td>-</td><td>N<sub>ph</sub> = N<sub>lyr</sub> × N<sub>t/l</sub></td><td><span>N<sub>lyr</sub></span>:层数, <span>N<sub>t/l</sub></span>:每层每相匝数</td></tr>
+                            <tr><td>绕组总长度</td><td>L<sub>tot</sub></td><td><span class="res-val" id="res_Ltot">0</span></td><td>m</td><td>L<sub>tot</sub> = N<sub>ph</sub> × (2L<sub>cond</sub> + 2k<sub>end</sub>τ<sub>p</sub>)</td><td><span>N<sub>ph</sub></span>:总匝数, <span>k<sub>end</sub></span>:端部系数</td></tr>
+                            <tr><td>相电阻(20℃)</td><td>R<sub>20</sub></td><td><span class="res-val" id="res_R20">0</span></td><td>Ω</td><td>R<sub>20</sub> = Σ (R<sub>inner</sub> + R<sub>outer</sub>)</td><td>考虑内外层铜厚差异 (外层含电镀)</td></tr>
+                            <tr><td>相电阻(热)</td><td>R<sub>ph</sub></td><td><span class="res-val" id="res_Rhot">0</span></td><td>Ω</td><td>R<sub>ph</sub> = R<sub>20</sub> × [1 + α(T-20)]</td><td><span>R<sub>20</sub></span>:冷态电阻, <span>α</span>:铜温漂系数, <span>T</span>:温度</td></tr>
+                        </tbody>
+                    </table>
+                </div>
+            </section>
+
+            <section id="mag" class="page-section">
+                <div class="section-card">
+                    <h2>3. 磁路分析 (Flux Linkage Method)</h2>
+                    <p style="color:#64748b; font-size:14px; margin-bottom:15px; border-left:4px solid #f59e0b; padding-left:10px;">
+                        <b>无铁芯修正 (Coreless Correction):</b> 采用磁通链法计算。磁通量 $\Phi$ 基于有效扇区面积积分计算,而非简单的导线切割长度。
+                    </p>
+                    <table>
+                        <thead>
+                            <tr><th class="col-param">参数名称</th><th class="col-sym">符号</th><th class="col-val">计算结果</th><th class="col-unit">单位</th><th class="col-form">计算公式 </th><th class="col-def">公式变量物理含义</th></tr>
+                        </thead>
+                        <tbody>
+                            <tr><td>有效气隙</td><td>g<sub>eff</sub></td><td><span class="res-val" id="res_geff">0</span></td><td>mm</td><td>g<sub>eff</sub> = h<sub>s</sub> + 2g</td><td><span>h<sub>s</sub></span>:定子PCB厚度, <span>g</span>:单侧机械气隙</td></tr>
+                            <tr><td>磁路负载点</td><td>PC</td><td><span class="res-val" id="res_PC">0</span></td>
+                            <td>-</td>
+                            <td>PC = 2h<sub>m</sub> / (g<sub>eff</sub> × μ<sub>r</sub>)</td>
+                            <td><span>h<sub>m</sub></span>:单块磁铁厚, <span>g<sub>eff</sub></span>:总气隙, <span>μ<sub>r</sub></span>:回复磁导率</td>
+                        </tr>
+                        <tr>
+                            <td>气隙磁密(平台)</td>
+                            <td>B<sub>g</sub></td>
+                            <td><span class="res-val" id="res_Bg">0</span></td>
+                            <td>T</td>
+                            <td>B<sub>g</sub> = [2h<sub>m</sub>/(2h<sub>m</sub>+g<sub>eff</sub>)] × B<sub>r</sub></td>
+                            <td>无铁芯结构的有效气隙磁感应强度</td>
+                        </tr>
+                        <tr>
+                            <td>有效极面积</td>
+                            <td>A<sub>pole</sub></td>
+                            <td><span class="res-val" id="res_Apole">0</span></td>
+                            <td>mm²</td>
+                            <td>A = π(R<sub>o</sub>²-R<sub>i</sub>²)/2p × α<sub>p</sub></td>
+                            <td>单个磁极覆盖的扇形有效面积</td>
+                        </tr>
+                        <tr>
+                            <td>每极磁通</td>
+                            <td>Φ</td>
+                            <td><span class="res-val" id="res_Phi">0</span></td>
+                            <td>Wb</td>
+                            <td>Φ = B<sub>g</sub> × A<sub>pole</sub> × k<sub>leak</sub></td>
+                            <td>穿过定子绕组的有效磁通总量</td>
+                        </tr>
+                    </table>
+                </div>
+            </section>
+
+            <section id="perf" class="page-section">
+                <div class="section-card">
+                    <h2>4. 性能指标 (Performance)</h2>
+                    <table>
+                        <thead>
+                            <tr>
+                                <th class="col-param">参数名称</th>
+                                <th class="col-sym">符号</th>
+                                <th class="col-val">计算结果</th>
+                                <th class="col-unit">单位</th>
+                                <th class="col-form">计算公式</th>
+                                <th class="col-def">公式变量物理含义</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                             <tr>
+                                <td>感应电动势</td>
+                                <td>E<sub>rms</sub></td>
+                                <td><span class="res-val" id="res_Erms">0</span></td>
+                                <td>V</td>
+                                <td>E = √2·π · f · N<sub>ph</sub> · Φ · k<sub>w</sub></td>
+                                <td><span>f</span>:电频率, <span>N<sub>ph</sub></span>:相匝数, <span>k<sub>w</sub></span>:绕组系数</td>
+                            </tr>
+                            <tr>
+                                <td>反电势常数</td>
+                                <td>K<sub>e</sub></td>
+                                <td><span class="res-val" id="res_Ke">0</span></td>
+                                <td>V·s/rad</td>
+                                <td>K<sub>e</sub> = E<sub>rms</sub> / ω</td>
+                                <td><span>E<sub>rms</sub></span>:相电势, <span>ω</span>:机械角速度</td>
+                            </tr>
+                            <tr>
+                                <td>Kv 值</td>
+                                <td>K<sub>v</sub></td>
+                                <td><span class="res-val" id="res_Kv">0</span></td>
+                                <td>rpm/V</td>
+                                <td>K<sub>v</sub> ≈ 9.55 / K<sub>e</sub></td>
+                                <td><span>K<sub>e</sub></span>:反电势常数, 9.55:单位换算常数</td>
+                            </tr>
+                            <tr>
+                                <td>额定力矩</td>
+                                <td>T<sub>nom</sub></td>
+                                <td><span class="res-val" id="res_Tnom">0</span></td>
+                                <td>Nm</td>
+                                <td>T<sub>nom</sub> = P<sub>nom</sub> / ω</td>
+                                <td><span>P<sub>nom</sub></span>:额定功率, <span>ω</span>:机械角速度(rad/s)</td>
+                            </tr>
+                            <tr>
+                                <td>电磁转矩常数</td>
+                                <td>K<sub>t</sub></td>
+                                <td><span class="res-val" id="res_Kt">0</span></td>
+                                <td>Nm/A</td>
+                                <td>K<sub>t</sub> = 3 · p · Ψ / √2</td>
+                                <td><span>p</span>:极对数, <span>Ψ</span>:磁链</td>
+                            </tr>
+                            <tr>
+                                <td>输入电功率</td>
+                                <td>P<sub>in</sub></td>
+                                <td><span class="res-val" id="res_Pin">0</span></td>
+                                <td>W</td>
+                                <td>P<sub>in</sub> = P<sub>nom</sub> / η</td>
+                                <td><span>P<sub>nom</sub></span>:输出功率, <span>η</span>:电机效率</td>
+                            </tr>
+                            <tr>
+                                <td>相电流(RMS)</td>
+                                <td>I<sub>rms</sub></td>
+                                <td><span class="res-val" id="res_Irms">0</span></td>
+                                <td>A</td>
+                                <td>I<sub>rms</sub> = P<sub>in</sub> / (3 · E<sub>rms</sub> · PF)</td>
+                                <td>基于能量守恒,考虑效率和功率因数</td>
+                            </tr>
+                            <tr>
+                                <td>电流密度</td>
+                                <td>J</td>
+                                <td><span class="res-val" id="res_J">0</span></td>
+                                <td>A/mm²</td>
+                                <td>J = I<sub>rms</sub> / A<sub>cu</sub></td>
+                                <td><span>I<sub>rms</sub></span>:相电流RMS, <span>A<sub>cu</sub></span>:铜线截面积</td>
+                            </tr>
+                            <tr>
+                                <td>功率因数</td>
+                                <td>PF</td>
+                                <td><span class="res-val" id="res_PF">0</span></td>
+                                <td>-</td>
+                                <td>用户输入值,PCB轴向电机典型0.92-0.98</td>
+                                <td>电压与电流相位差的余弦值</td>
+                            </tr>
+                            <tr>
+                                <td>电机效率</td>
+                                <td>η</td>
+                                <td><span class="res-val" id="res_Eff_perf">0</span></td>
+                                <td>%</td>
+                                <td>η = P<sub>nom</sub> / P<sub>in</sub> × 100%</td>
+                                <td>输出机械功率与输入电功率之比</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </section>
+
+            <section id="loss" class="page-section">
+                <div class="section-card">
+                    <h2>5. 损耗分析 (High-Fidelity Loss Analysis)</h2>
+                    <p style="color:#64748b; font-size:14px; margin-bottom:20px;">
+                        包含集肤/邻近效应产生的交流铜损(AC Loss)、磁钢涡流损耗(谐波感应)以及工程杂散损耗(Stray Loss)。
+                    </p>
+                    <table>
+                        <thead>
+                            <tr>
+                                <th class="col-param">参数名称</th>
+                                <th class="col-sym">符号</th>
+                                <th class="col-val">计算结果</th>
+                                <th class="col-unit">单位</th>
+                                <th class="col-form">计算公式 / 说明</th>
+                                <th class="col-def">公式变量物理含义</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <tr>
+                                <td>PCB 直流铜损</td>
+                                <td>P<sub>cu,dc</sub></td>
+                                <td><span class="res-val" id="res_Pcu_dc">0</span></td>
+                                <td>W</td>
+                                <td>P<sub>cu,dc</sub> = 3 × I<sub>rms</sub>² × R<sub>ph</sub></td>
+                                <td><span>I<sub>rms</sub></span>:相电流, <span>R<sub>ph</sub></span>:热态相电阻</td>
+                            </tr>
+                            <tr>
+                                <td>PCB 交流铜损</td>
+                                <td>P<sub>cu,ac</sub></td>
+                                <td><span class="res-val" id="res_Pcu_ac">0</span></td>
+                                <td>W</td>
+                                <td>P<sub>cu,ac</sub> = P<sub>cu,dc</sub> × (k<sub>ac</sub> - 1)</td>
+                                <td><span>k<sub>ac</sub></span>:交流电阻系数(与频率 f² 相关)</td>
+                            </tr>
+                            <tr>
+                                <td>PCB 涡流损耗</td>
+                                <td>P<sub>eddy</sub></td>
+                                <td><span class="res-val" id="res_Peddy">0</span></td>
+                                <td>W</td>
+                                <td>P = (π²·f²·B²·w²·Vol) / (12·ρ)</td>
+                                <td><span>w</span>:线宽, <span>Vol</span>:铜体积, <span>ρ</span>:电阻率</td>
+                            </tr>
+                            <tr>
+                                <td>磁钢涡流损耗</td>
+                                <td>P<sub>mag</sub></td>
+                                <td><span class="res-val" id="res_Pmag_eddy">0</span></td>
+                                <td>W</td>
+                                <td>P = (π²·f²·B²·t²·σ·Vol) / 6</td>
+                                <td><span>t</span>:磁钢厚, <span>σ</span>:电导率, <span>Vol</span>:磁钢体积</td>
+                            </tr>
+                            <tr>
+                                <td>机械损耗</td>
+                                <td>P<sub>mech</sub></td>
+                                <td><span class="res-val" id="res_Pmech">0</span></td>
+                                <td>W</td>
+                                <td>P = 0.5·C<sub>f</sub>·ρ·ω³·(R<sub>o</sub>⁵-R<sub>i</sub>⁵)</td>
+                                <td><span>C<sub>f</sub></span>:摩擦系数, <span>ρ</span>:空气密度, <span>ω</span>:角速度</td>
+                            </tr>
+                            <tr>
+                                <td>杂散损耗</td>
+                                <td>P<sub>stray</sub></td>
+                                <td><span class="res-val" id="res_Pstray">0</span></td>
+                                <td>W</td>
+                                <td>P<sub>stray</sub> ≈ 0.008 × P<sub>nom</sub></td>
+                                <td><span>P<sub>nom</sub></span>:额定功率 (工程经验值),包含结构件涡流、PCB介质损耗、轴承密封圈/润滑脂的额外阻力</td>
+                            </tr>
+                            <tr style="background-color:#fef2f2;">
+                                <td style="font-weight:800">总损耗</td>
+                                <td>P<sub>loss</sub></td>
+                                <td><span class="res-val res-bad" id="res_Ploss">0</span></td>
+                                <td>W</td>
+                                <td>P<sub>loss</sub> = Σ (P<sub>cu</sub> + P<sub>mag</sub> + P<sub>mech</sub>...)</td>
+                                <td>所有损耗分量之和</td>
+                            </tr>
+                            <tr>
+                                <td>总效率</td>
+                                <td>η</td>
+                                <td><span class="res-val" id="res_Eff">0</span></td>
+                                <td>%</td>
+                                <td>η = P<sub>nom</sub> / (P<sub>nom</sub> + P<sub>loss</sub>)</td>
+                                <td><span>P<sub>nom</sub></span>:输出功率, <span>P<sub>loss</sub></span>:总损耗</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </section>
+
+            <section id="therm" class="page-section">
+                <div class="section-card">
+                    <h2>6. 热分析 (Thermal Analysis)</h2>
+                    
+                    <h3>A. 边界条件</h3>
+                    <table>
+                        <thead>
+                            <tr>
+                                <th class="col-param">参数</th>
+                                <th class="col-sym">符号</th>
+                                <th class="col-val">输入/结果</th>
+                                <th class="col-unit">单位</th>
+                                <th class="col-form">说明</th>
+                                <th class="col-def">物理含义</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <tr>
+                                <td>换热系数</td>
+                                <td>h</td>
+                                <td><input type="number" id="h_conv" value="50" oninput="app.calculate()"></td>
+                                <td>W/m²K</td>
+                                <td>User Input</td>
+                                <td>表面对流散热系数(自然:10, 强迫:50+)</td>
+                            </tr>
+                            <tr>
+                                <td>环境温度</td>
+                                <td>T<sub>amb</sub></td>
+                                <td><input type="number" id="T_amb" value="25" oninput="app.calculate()"></td>
+                                <td>°C</td>
+                                <td>User Input</td>
+                                <td>电机所处的环境空气温度</td>
+                            </tr>
+                        </tbody>
+                    </table>
+
+                    <h3>B. 稳态温升</h3>
+                    <table>
+                        <thead>
+                            <tr>
+                                <th class="col-param">参数</th>
+                                <th class="col-sym">符号</th>
+                                <th class="col-val">结果</th>
+                                <th class="col-unit">单位</th>
+                                <th class="col-form">计算公式</th>
+                                <th class="col-def">公式变量物理含义</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <tr>
+                                <td>定子表面积</td>
+                                <td>A<sub>surf</sub></td>
+                                <td><span class="res-val" id="res_Asurf">0</span></td>
+                                <td>m²</td>
+                                <td>A<sub>surf</sub> = 2 × π × (R<sub>out</sub>² - R<sub>in</sub>²)</td>
+                                <td><span>R<sub>out/in</sub></span>: 定子有效区域外/内半径</td>
+                            </tr>
+                            <tr>
+                                <td>温升</td>
+                                <td>ΔT</td>
+                                <td><span class="res-val" id="res_dT">0</span></td>
+                                <td>K</td>
+                                <td>ΔT = P<sub>loss</sub> / (h × A<sub>surf</sub>)</td>
+                                <td><span>P<sub>loss</sub></span>:总热损耗, <span>h</span>:换热系数, <span>A</span>:面积</td>
+                            </tr>
+                            <tr>
+                                <td>最终温度</td>
+                                <td>T<sub>coil</sub></td>
+                                <td><span class="res-val" id="res_Tfinal">0</span></td>
+                                <td>°C</td>
+                                <td>T<sub>coil</sub> = T<sub>amb</sub> + ΔT</td>
+                                <td><span>T<sub>amb</sub></span>:环境温度, <span>ΔT</span>:计算温升</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </section>
+
+            <section id="cost" class="page-section">
+                <div class="section-card">
+                    <h2>7. 成本明细 (Cost Estimation)</h2>
+                    <table class="cost-table">
+                        <thead>
+                            <tr>
+                                <th style="width:25%;font-weight: 600; font-size: 16px; color: #334155;">组件名称</th>
+                                <th style="width:20%;font-weight: 600; font-size: 16px; color: #334155;">单价输入 (¥)</th>
+                                <th style="width:25%;font-weight: 600; font-size: 16px; color: #334155;">用量计算 / 备注</th>
+                                <th style="width:10%;font-weight: 600; font-size: 16px; color: #334155;">单位</th>
+                                <th style="width:20%;font-weight: 600; font-size: 16px; color: #334155;">小计 (¥)</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <tr class="cost-row">
+                                <td><b>钕铁硼磁铁</b></td>
+                                <td><input type="number" class="cost-input" id="price_mag" value="400" oninput="app.calculate()"></td>
+                                <td>用量: <span id="res_Mmag" style="font-weight:bold;">0</span> kg</td>
+                                <td>元/kg</td>
+                                <td><span class="res-val" id="sub_mag">0</span></td>
+                            </tr>
+                            <tr class="cost-row">
+                                <td><b>PCB定子</b></td>
+                                <td><input type="number" class="cost-input" id="price_pcb_unit" value="60" oninput="app.calculate()"></td>
+                                <td>按片计价</td>
+                                <td>元/pcs</td>
+                                <td><span class="res-val" id="sub_pcb">0</span></td>
+                            </tr>
+                            <tr class="cost-row">
+                                <td><b>轴承</b></td>
+                                <td><input type="number" class="cost-input" id="price_bearing" value="8" oninput="app.calculate()"></td>
+                                <td>用量:2</td>
+                                <td>元/个</td>
+                                <td><span class="res-val" id="sub_bearing">0</span></td>
+                            </tr>
+                            <tr class="cost-row">
+                                <td><b>机加件(轴/壳)</b></td>
+                                <td><input type="number" class="cost-input" id="price_mech" value="50" oninput="app.calculate()"></td>
+                                <td>CNC/压铸/3D打印</td>
+                                <td>元/套</td>
+                                <td><span class="res-val" id="sub_mech">0</span></td>
+                            </tr>
+                            <tr class="cost-row">
+                                <td><b>辅材/人工</b></td>
+                                <td><input type="number" class="cost-input" id="price_labor" value="25" oninput="app.calculate()"></td>
+                                <td>组装费</td>
+                                <td>元/台</td>
+                                <td><span class="res-val" id="sub_labor">0</span></td>
+                            </tr>
+                            <tr class="cost-total">
+                                <td colspan="4" style="text-align:right; padding-right:20px;">整机 BOM 预估总成本:</td>
+                                <td><span class="res-val" id="res_CostTotal">0</span> ¥</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </section>
+
+            <section id="ipc" class="page-section">
+                <div class="section-card">
+                    <h2>8. 载流校验与安全 (IPC-2152 Validation)</h2>
+                    <p style="color:#64748b; font-size:14px; margin-bottom:20px;">基于 IPC-2221/IPC-2152 标准,针对内层导体(最恶劣散热条件)计算载流能力。</p>
+                    
+                    <table>
+                        <thead>
+                            <tr>
+                                <th class="col-param">校验项目</th>
+                                <th class="col-sym">符号</th>
+                                <th class="col-val">结果/状态</th>
+                                <th class="col-unit">单位</th>
+                                <th class="col-form">阈值与逻辑</th>
+                                <th class="col-def">说明</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <tr>
+                                <td>IPC最大载流(10℃温升)</td>
+                                <td>I<sub>IPC10</sub></td>
+                                <td><span class="res-val" id="res_Iipc10">0</span></td>
+                                <td>A</td>
+                                <td>I = k · ΔT<sup>0.44</sup> · A<sup>0.725</sup></td>
+                                <td>保守运行条件下的最大电流 (k=0.024)</td>
+                            </tr>
+                            <tr>
+                                <td>IPC最大载流(40℃温升)</td>
+                                <td>I<sub>IPC40</sub></td>
+                                <td><span class="res-val" id="res_Iipc40">0</span></td>
+                                <td>A</td>
+                                <td>I = k · ΔT<sup>0.44</sup> · A<sup>0.725</sup></td>
+                                <td>常规设计温升上限对应的电流</td>
+                            </tr>
+                            <tr>
+                                <td>当前相电流</td>
+                                <td>I<sub>rms</sub></td>
+                                <td><span class="res-val" id="res_CheckIrms">0</span></td>
+                                <td>A</td>
+                                <td>Ref from Performance</td>
+                                <td>实际工作电流</td>
+                            </tr>
+                            <tr>
+                                <td><b>电流安全裕度</b></td>
+                                <td>Margin</td>
+                                <td><span id="res_IPCMargin">--</span></td>
+                                <td>-</td>
+                                <td>Compare I<sub>rms</sub> vs I<sub>IPC40</sub></td>
+                                <td>PASS: I<sub>rms</sub> < I<sub>IPC40</sub></td>
+                            </tr>
+                            <tr>
+                                <td><b>Tg 热失效风险</b></td>
+                                <td>Risk</td>
+                                <td><span id="res_TgRisk">--</span></td>
+                                <td>-</td>
+                                <td>T<sub>final</sub> vs (T<sub>g</sub> - 20)</td>
+                                <td>确保线圈温度低于材料Tg点至少20度</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </section>
+
+        </main>
+    </div>
+
+<script>
+/**
+     * @namespace App
+     * @description Core namespace for the Motor Design Application.
+     */
+    const App = (() => {
+
+        const Config = {
+            storageKey: 'drss_v10_design_data',
+            magnets: {
+                "ATMAX50SH": 1.425,
+                "N35": 1.17,
+                "N52": 1.45
+            },
+            physics: {
+                k_ipc: 0.024,
+                eddy_correction: 1.2,
+                mu0: 4 * Math.PI * 1e-7,  // 真空磁导率 H/m
+                k_ind_geom: 0.45          // PCB绕组几何形状修正系数 (扁平线圈非理想螺线管)
+            }
+        };
+
+        class PhysicsEngine {
+            
+            static calcGeometry(inputs) {
+                // 1. 基础参数准备
+                const t_base_mm = inputs.oz * 0.035; // 1oz = 0.035mm
+                const t_plate_mm = inputs.t_plate_um / 1000;
+                
+                // 2. 复杂叠层厚度计算 (Stack-up Calculation Logic)
+                let h_s = 0;
+                let h_copper_total = 0; 
+                let h_mask_total = 2 * inputs.t_mask;
+
+                // Resistance Calculation Variables
+                const R_out = inputs.D_out / 2;
+                const R_in = inputs.D_in / 2;
+                const R_avg = (R_out + R_in) / 2;
+                const tau_p = (Math.PI * 2 * R_avg / 1000) / (2 * inputs.p);
+                const L_cond = (R_out - R_in) / 1000;
+                
+                // 假设所有层串联
+                const N_ph = inputs.layers * inputs.turns;
+                const L_turn = 2 * L_cond + 2 * (tau_p * inputs.k_end);
+                const L_total = N_ph * L_turn;
+                
+                // 单层绕组长度
+                const L_per_layer_total = inputs.turns * L_turn; 
+
+                // 电阻计算截面积
+                // 外层:基铜 + 电镀
+                const A_outer = inputs.w_tr * (t_base_mm + t_plate_mm);
+                // 内层:仅基铜
+                const A_inner = inputs.w_tr * t_base_mm;
+
+                let R_20 = 0;
+
+                if (inputs.layers <= 2) {
+                    // 双面板结构 (Mask - OutCu - Core - OutCu - Mask)
+                    const h_cu_outer = 2 * (t_base_mm + t_plate_mm);
+                    h_s = h_mask_total + h_cu_outer + inputs.t_core;
+                    h_copper_total = h_cu_outer; 
+                    
+                    // 电阻计算 (全按外层算)
+                    R_20 = (inputs.rho_cu * L_total) / A_outer;
+                } else {
+                    // 多层板结构 (N >= 4)
+                    // 结构:[Mask-OuterCu-PP] - [Core-InnerCu]... - [PP-OuterCu-Mask]
+                    
+                    const h_cu_outer = 2 * (t_base_mm + t_plate_mm);
+                    const h_cu_inner = (inputs.layers - 2) * t_base_mm;
+                    
+                    // 绝缘介质层: Core数 = N/2 - 1, PP数 = N/2
+                    const num_cores = (inputs.layers / 2) - 1;
+                    const num_pp = inputs.layers / 2;
+                    const h_dielectric = (num_cores * inputs.t_core) + (num_pp * inputs.t_pp);
+                    
+                    // 总厚度
+                    h_s = h_mask_total + h_cu_outer + h_cu_inner + h_dielectric;
+                    
+                    h_copper_total = h_cu_outer + h_cu_inner;
+
+                    // 精确电阻计算:分别计算外层和内层电阻然后叠加
+                    const R_outer_layer = (inputs.rho_cu * L_per_layer_total) / A_outer;
+                    const R_inner_layer = (inputs.rho_cu * L_per_layer_total) / A_inner;
+                    
+                    // 2层外层 + (N-2)层内层
+                    R_20 = (2 * R_outer_layer) + ((inputs.layers - 2) * R_inner_layer);
+                }
+                
+                // 用于 IPC 计算的单层截面积 (取最恶劣情况:内层)
+                const A_cu_worst = inputs.w_tr * t_base_mm; 
+
+                return { 
+                    t_base_mm, t_plate_mm, h_s, 
+                    R_out, R_in, R_avg, tau_p, L_cond, N_ph, L_turn, L_total, 
+                    A_cu: A_cu_worst, // 导出给 IPC 模块使用
+                    R_20 
+                };
+            }
+
+            static calcMagnetic(inputs, geom) {
+                // --- 核心定义:有效气隙 (Effective Gap) ---
+                // 在DRSS结构中,磁通穿过两个机械气隙和定子厚度
+                const g_eff = geom.h_s + 2 * inputs.g;
+                
+                const h_mag_tot = 2 * inputs.h_m; 
+                const PC = h_mag_tot / (g_eff * inputs.mu_r);
+                
+                // Airgap Flux Density
+                const B_g = (h_mag_tot / (h_mag_tot + g_eff)) * inputs.Br;
+
+                const R_out_m = geom.R_out / 1000;
+                const R_in_m = geom.R_in / 1000;
+                
+                // Flux Linkage Area
+                const A_annulus = Math.PI * (Math.pow(R_out_m, 2) - Math.pow(R_in_m, 2));
+                const A_pole = (A_annulus / (2 * inputs.p)) * inputs.alpha_p; 
+                const A_pole_mm2 = A_pole * 1e6; 
+
+                // Flux per pole
+                const Phi = B_g * A_pole * inputs.k_leak;
+                
+                return { g_eff, h_mag_tot, PC, B_g, A_pole, A_pole_mm2, Phi, A_annulus };
+            }
+
+            static calcPerformance(inputs, geom, mag) {
+                const omega = inputs.n_nom * 2 * Math.PI / 60;
+                const f_elec = inputs.n_nom * inputs.p / 60;
+                
+                // 1. Back-EMF (Standard RMS Calculation)
+                // Correct Physics: E_rms = (2*PI/sqrt(2)) * f * N * Phi * kw
+                // sqrt(2)*PI approx 4.44
+                const E_rms = (Math.SQRT2 * Math.PI) * f_elec * geom.N_ph * mag.Phi * inputs.k_w;
+
+                const Ke = E_rms / omega; 
+                const Kv = 9.55 / Ke; 
+
+                const T_nom = inputs.P_nom / omega;
+                
+                // 2. 电磁转矩常数 Kt = 3 * p * Ψ / √2,其中 Ψ = N_ph * Φ * k_w / √2
+                const Psi = geom.N_ph * mag.Phi * inputs.k_w / Math.SQRT2;
+                const Kt = 3 * inputs.p * Psi / Math.SQRT2;
+                
+                // 3. 耦合迭代计算电流、损耗和效率
+                const iterativeResult = this.calcIterativePerformance(inputs, geom, mag, E_rms, T_nom, omega, f_elec);
+                
+                const I_rms = iterativeResult.I_rms;
+                const J = I_rms / geom.A_cu;
+
+                // --- DETAILED INDUCTANCE CALCULATION ---
+                
+                // 1. Magnetic Path Length for Inductance (一致性修正)
+                // 直接引用 Section 3 计算的 mag.g_eff
+                const g_total_m = mag.g_eff / 1000;
+
+                // 2. Phase Area (Approx 1/3 of annulus)
+                const A_phase_m2 = mag.A_annulus / 3;
+                const A_phase_mm2 = A_phase_m2 * 1e6;
+
+                // 3. Inductance Calculation
+                const L_ph_H = (Config.physics.k_ind_geom * Config.physics.mu0 * Math.pow(geom.N_ph, 2) * A_phase_m2) / g_total_m;
+                const L_ph_uH = L_ph_H * 1e6; 
+
+                // Electrical Time Constant
+                const Tau_e_s = L_ph_H / geom.R_20;
+                const Tau_e_us = Tau_e_s * 1e6;
+
+                return { 
+                    omega, f_elec, E_rms, Ke, Kv, T_nom, Kt,
+                    I_rms, J, 
+                    L_ph_uH, Tau_e_us, 
+                    A_phase_mm2,
+                    N_ph_sq: Math.pow(geom.N_ph, 2),
+                    // 迭代计算结果
+                    Pin: iterativeResult.Pin,
+                    PF: inputs.PF,
+                    eta: iterativeResult.eta * 100
+                };
+            }
+
+            static calcIterativePerformance(inputs, geom, mag, E_rms, T_nom, omega, f_elec) {
+                // 初始假设效率 85%
+                let eta_guess = 0.85;
+                let iterCount = 0;
+                let I_rms, Pin, eta_final;
+                
+                do {
+                    // 输入电功率 = 输出机械功率 / 效率
+                    Pin = inputs.P_nom / eta_guess;
+                    
+                    // 相电流 (考虑功率因数)
+                    const PF = inputs.PF; // PCB轴向电机功率因数估计
+                    I_rms = Pin / (3 * E_rms * PF);
+                    
+                    // 计算此电流下的损耗(简化版本,实际应调用完整损耗模型)
+                    const T_coil_guess = inputs.T_amb + 40;
+                    const R_hot = geom.R_20 * (1 + inputs.alpha_cu * (T_coil_guess - 20));
+                    const P_cu = 3 * I_rms * I_rms * R_hot;
+                    
+                    // 估算其他损耗(简化处理)
+                    const w_m = inputs.w_tr / 1000;
+                    const Vol_cu_total = (geom.L_total * 3 * inputs.w_tr * geom.t_base_mm) / 1e6;
+                    const P_eddy_tr = (Math.pow(Math.PI,2) * Math.pow(f_elec,2) * Math.pow(mag.B_g,2) * Math.pow(w_m,2) * Vol_cu_total * Config.physics.eddy_correction) / (12 * 1.68e-8);
+                    
+                    // 磁钢涡流损耗
+                    const t_mag_m = inputs.h_m / 1000;
+                    const Vol_mag = 2 * mag.A_pole * (2 * inputs.p) * t_mag_m; 
+                    const k_harm = 0.04;
+                    const n_harm = 5;
+                    const B_ac = mag.B_g * k_harm;
+                    const f_eff = f_elec * n_harm;
+                    let P_mag_calc = (Math.pow(Math.PI,2)/6) * Math.pow(f_eff,2) * Math.pow(B_ac,2) * Math.pow(t_mag_m,2) * inputs.sigma_mag * Vol_mag;
+                    const P_mag_eddy = Math.max(0, Math.min(P_mag_calc, inputs.P_nom * 0.02));
+                    
+                    // 机械损耗
+                    const R_out_m = geom.R_out / 1000;
+                    const R_in_m = geom.R_in / 1000;
+                    const P_mech = 0.5 * inputs.cf_fric * inputs.rho_air * Math.pow(omega, 3) * (Math.pow(R_out_m, 5) - Math.pow(R_in_m, 5));
+                    
+                    const P_stray = inputs.P_nom * 0.008;
+                    
+                    const P_loss_total = P_cu + P_eddy_tr + P_mag_eddy + P_mech + P_stray;
+                    
+                    // 计算实际效率
+                    eta_final = inputs.P_nom / (inputs.P_nom + P_loss_total);
+                    
+                    // 更新效率猜测
+                    eta_guess = eta_guess * 0.7 + eta_final * 0.3; // 加权更新
+                    
+                    iterCount++;
+                } while (Math.abs(eta_guess - eta_final) > 0.001 && iterCount < 20);
+                
+                // 最终电流计算使用最终效率
+                Pin = inputs.P_nom / eta_final;
+                I_rms = Pin / (3 * E_rms * inputs.PF);
+                
+                return { I_rms, Pin, eta: eta_final };
+            }
+
+            static calcLosses(inputs, geom, mag, perf) {
+                let T_coil = inputs.T_amb + 40; 
+                let R_hot = geom.R_20;
+                let P_cu_dc = 0, P_cu_ac = 0, P_eddy_tr = 0, P_mag_eddy = 0;
+                let P_mech = 0, P_stray = 0, P_loss = 0;
+
+                for(let iter = 0; iter < 4; iter++) {
+                    R_hot = geom.R_20 * (1 + inputs.alpha_cu * (T_coil - 20));
+                    P_cu_dc = 3 * Math.pow(perf.I_rms, 2) * R_hot;
+                    
+                    const k_ac_factor = 1 + Math.pow(perf.f_elec / 1200, 2);
+                    P_cu_ac = P_cu_dc * (k_ac_factor - 1);
+                    
+                    // --- PCB Eddy Current Loss ---
+                    const w_m = inputs.w_tr / 1000;
+                    const Vol_cu_total = (geom.L_total * 3 * inputs.w_tr * geom.t_base_mm) / 1e6;
+                    P_eddy_tr = (Math.pow(Math.PI,2) * Math.pow(perf.f_elec,2) * Math.pow(mag.B_g,2) * Math.pow(w_m,2) * Vol_cu_total * Config.physics.eddy_correction) / (12 * 1.68e-8);
+                    
+                    // --- Magnet Eddy Current Loss ---
+                    const t_mag_m = inputs.h_m / 1000;
+                    const Vol_mag = 2 * mag.A_pole * (2 * inputs.p) * t_mag_m; 
+                    const k_harm = 0.04;
+                    const n_harm = 5;
+                    const B_ac = mag.B_g * k_harm;
+                    const f_eff = perf.f_elec * n_harm;
+                    let P_mag_calc = (Math.pow(Math.PI,2)/6) * Math.pow(f_eff,2) * Math.pow(B_ac,2) * Math.pow(t_mag_m,2) * inputs.sigma_mag * Vol_mag;
+                    P_mag_eddy = Math.max(0, Math.min(P_mag_calc, inputs.P_nom * 0.02));
+                    
+                    // --- Mechanical Loss ---
+                    const R_out_m = geom.R_out / 1000;
+                    const R_in_m = geom.R_in / 1000;
+                    P_mech = 0.5 * inputs.cf_fric * inputs.rho_air * Math.pow(perf.omega, 3) * (Math.pow(R_out_m, 5) - Math.pow(R_in_m, 5));
+                    
+                    P_stray = inputs.P_nom * 0.008;
+                    
+                    P_loss = P_cu_dc + P_cu_ac + P_eddy_tr + P_mag_eddy + P_mech + P_stray;
+                    
+                    const A_surf = 2 * Math.PI * Math.pow(geom.R_out/1000, 2);
+                    const dT = P_loss / (inputs.h_conv * A_surf);
+                    T_coil = inputs.T_amb + dT;
+                }
+                
+                const Eff = (inputs.P_nom / (inputs.P_nom + P_loss)) * 100;
+                return { R_hot, P_cu_dc, P_cu_ac, P_eddy_tr, P_mag_eddy, P_mech, P_stray, P_loss, Eff, T_coil };
+            }
+
+            static calcThermal(inputs, losses, geom) {
+                const A_surf = 2 * Math.PI * (Math.pow(geom.R_out/1000, 2) - Math.pow(geom.R_in/1000, 2));
+                const dT = losses.P_loss / (inputs.h_conv * A_surf);
+                const T_final = inputs.T_amb + dT;
+                return { A_surf, dT, T_final };
+            }
+
+            static calcCost(inputs, geom) {
+                const Area_ring_cm2 = Math.PI * (Math.pow(geom.R_out/10,2) - Math.pow(geom.R_in/10,2));
+                const Vol_mag_cm3 = (2 * Area_ring_cm2 * inputs.alpha_p * inputs.h_m) / 10;
+                const M_mag = (Vol_mag_cm3 * inputs.rho_mag) / 1000;
+                const Cost_Mag = M_mag * inputs.price_mag;
+                const Cost_PCB = inputs.price_pcb;
+                const Cost_Bear = inputs.price_bearing * 2;
+                const Cost_Mech = inputs.price_mech;
+                const Cost_Labor = inputs.price_labor;
+                const Cost_Total = Cost_Mag + Cost_PCB + Cost_Bear + Cost_Mech + Cost_Labor;
+                return { M_mag, Cost_Mag, Cost_PCB, Cost_Bear, Cost_Mech, Cost_Labor, Cost_Total };
+            }
+
+            static calcIPC(inputs, geom, perf, thermal) {
+                const A_mils = geom.A_cu * 1550.0;
+                const k = Config.physics.k_ipc;
+                const I_ipc10 = k * Math.pow(10, 0.44) * Math.pow(A_mils, 0.725);
+                const I_ipc40 = k * Math.pow(40, 0.44) * Math.pow(A_mils, 0.725);
+                let ipcStatus = perf.I_rms < I_ipc40 ? "PASS" : "WARNING";
+                let ipcClass = perf.I_rms < I_ipc40 ? "status-ok" : "status-warn";
+                let tgStatus = (inputs.Tg - thermal.T_final > 20) ? "OK" : "RISK";
+                let tgClass = (inputs.Tg - thermal.T_final > 20) ? "status-ok" : "status-warn";
+                return { I_ipc10, I_ipc40, ipcStatus, ipcClass, tgStatus, tgClass };
+            }
+        }
+
+        class Store {
+            static getInputs() {
+                const v = (id) => parseFloat(document.getElementById(id).value) || 0;
+                return {
+                    P_nom: v('P_nom'), n_nom: v('n_nom'), V_dc: v('V_dc'), p: v('p'),
+                    D_out: v('D_out'), D_in: v('D_in'), g: v('g_mech'), h_m: v('h_m'),
+                    
+                    // --- PCB Stackup ---
+                    layers: v('layers'), 
+                    oz: v('oz'), 
+                    t_plate_um: v('t_plate'),
+                    t_core: v('t_core'),
+                    t_pp: v('t_pp'),
+                    t_mask: v('t_mask'),
+                    turns: v('turns_layer'),
+                    w_tr: v('w_trace'), 
+                    k_end: v('k_end'),
+                    // -----------------------------
+
+                    Br: v('Br'), mu_r: v('mu_r'), alpha_p: v('alpha_p'),
+                    sigma_mag: v('sigma_mag'), rho_cu: v('rho_cu'), alpha_cu: v('alpha_cu'), 
+                    rho_mag: v('rho_mag'), rho_air: v('rho_air'), cf_fric: v('cf_fric'), Tg: v('Tg'),
+                    k_leak: v('k_leak'), k_w: v('k_w'), PF: v('PF'), // 新增功率因数
+                    h_conv: v('h_conv'), T_amb: v('T_amb'),
+                    price_mag: v('price_mag'), price_pcb: v('price_pcb_unit'),
+                    price_bearing: v('price_bearing'), price_mech: v('price_mech'), price_labor: v('price_labor'),
+                    mag_grade: document.getElementById('mag_grade').value
+                };
+            }
+            static save(inputs) { localStorage.setItem(Config.storageKey, JSON.stringify(inputs)); }
+            static load() {
+                try {
+                    const saved = localStorage.getItem(Config.storageKey);
+                    if (!saved) return false;
+                    const data = JSON.parse(saved);
+                    Object.keys(data).forEach(key => {
+                        const el = document.getElementById(key);
+                        if (el) el.value = data[key];
+                    });
+                    return true;
+                } catch (e) { return false; }
+            }
+        }
+
+        class UIManager {
+            constructor() { this.exportData = []; }
+            
+            updateView(data) {
+                const { inputs, geom, mag, perf, losses, thermal, cost, ipc } = data;
+                const set = (id, val, d=2) => { const el = document.getElementById(id); if(el) el.innerText = val.toFixed(d); };
+                const setHTML = (id, html) => { const el = document.getElementById(id); if(el) el.innerHTML = html; };
+
+                // HUD
+                set('hud_Pnom', inputs.P_nom, 0); set('hud_T', perf.T_nom, 2); set('hud_Eff', perf.eta, 1); 
+                set('hud_Loss', losses.P_loss, 1); set('hud_Cost', cost.Cost_Total, 1); set('hud_Temp', thermal.T_final, 0); 
+                setHTML('hud_IPC', `<span class="${ipc.ipcClass}">${ipc.ipcStatus}</span>`);
+
+                // Main Sections
+                set('res_hs', geom.h_s, 2); set('res_Ravg', geom.R_avg, 1); set('res_Nph', geom.N_ph, 0);
+                set('res_Ltot', geom.L_total, 2); set('res_R20', geom.R_20, 4); set('res_Rhot', losses.R_hot, 4);
+
+                set('res_geff', mag.g_eff, 2); set('res_PC', mag.PC, 3); set('res_Bg', mag.B_g, 3);
+                set('res_Apole', mag.A_pole_mm2, 0); set('res_Phi', mag.Phi, 6);
+
+                set('res_Erms', perf.E_rms, 2); set('res_Ke', perf.Ke, 4); set('res_Kv', perf.Kv, 0); 
+                set('res_Tnom', perf.T_nom, 2); set('res_Kt', perf.Kt, 3);
+                set('res_Pin', perf.Pin, 1); set('res_Irms', perf.I_rms, 2); set('res_J', perf.J, 1);
+                set('res_PF', perf.PF, 2); set('res_Eff_perf', perf.eta, 1);
+
+                // 电感相关行
+                this.updateOrInsertRow('perf', '<b>相电感', 'L<sub>ph</sub>', perf.L_ph_uH, 'μH', 
+                    `L ≈ k·μ₀·N<sub>ph</sub>²·A<sub>ph</sub> / g<sub>eff</sub>`, 
+                    'res_Lph', 
+                    `<span>k</span>:0.45(形状修正), <span>μ₀</span>:真空磁导率, <span>N<sub>ph</sub></span>:总匝数, <span>A<sub>ph</sub></span>:相面积`
+                );
+                
+                this.updateOrInsertRow('perf', '└─ 线圈磁路气隙', 'g<sub>eff</sub>', mag.g_eff, 'mm', 
+                    `g<sub>eff</sub> = h<sub>s</sub> + 2g`, 
+                    'res_g_coil', 
+                    `<span>h<sub>s</sub></span>:定子厚, <span>g</span>:单侧气隙`
+                );
+
+                this.updateOrInsertRow('perf', '└─ 单相有效面积', 'A<sub>ph</sub>', perf.A_phase_mm2, 'mm²', 
+                    `A<sub>ph</sub> ≈ A<sub>ring</sub> / 3`, 
+                    'res_Aph', 
+                    `<span>A<sub>ring</sub></span>:定子圆环总面积 (假设三相均分)`
+                );
+
+                this.updateOrInsertRow('perf', '<b>电气时间常数', 'τ<sub>e</sub>', perf.Tau_e_us, 'μs', 
+                    `τ<sub>e</sub> = L<sub>ph</sub> / R<sub>20</sub>`, 
+                    'res_Taue', 
+                    `<span>L<sub>ph</sub></span>:相电感, <span>R<sub>20</sub></span>:冷态电阻`
+                );
+
+                set('res_Pcu_dc', losses.P_cu_dc, 1); set('res_Pcu_ac', losses.P_cu_ac, 1); 
+                set('res_Peddy', losses.P_eddy_tr, 1); set('res_Pmag_eddy', losses.P_mag_eddy, 2);
+                set('res_Pmech', losses.P_mech, 1); set('res_Pstray', losses.P_stray, 1);
+                set('res_Ploss', losses.P_loss, 1); set('res_Eff', losses.Eff, 1);
+
+                set('res_Asurf', thermal.A_surf, 4); set('res_dT', thermal.dT, 1); set('res_Tfinal', thermal.T_final, 1);
+
+                set('res_Mmag', cost.M_mag, 3); set('sub_mag', cost.Cost_Mag, 1); set('sub_pcb', cost.Cost_PCB, 1);
+                set('sub_bearing', cost.Cost_Bear, 1); set('sub_mech', cost.Cost_Mech, 1);
+                set('sub_labor', cost.Cost_Labor, 1); set('res_CostTotal', cost.Cost_Total, 1);
+
+                set('res_Iipc10', ipc.I_ipc10, 2); set('res_Iipc40', ipc.I_ipc40, 2); set('res_CheckIrms', perf.I_rms, 2);
+                setHTML('res_IPCMargin', `<span class="${ipc.ipcClass}">${ipc.ipcStatus}</span>`);
+                setHTML('res_TgRisk', `<span class="${ipc.tgClass}">${ipc.tgStatus}</span>`);
+
+                this.exportData = [
+                    {cat:"Input", name:"额定功率", sym:"P_nom", val:inputs.P_nom, unit:"W"},
+                    {cat:"Performance", name:"输入电功率", sym:"P_in", val:perf.Pin, unit:"W"},
+                    {cat:"Performance", name:"电机效率", sym:"eta", val:perf.eta, unit:"%"},
+                    {cat:"Performance", name:"功率因数", sym:"PF", val:perf.PF, unit:""},
+                    {cat:"Magnetic", name:"气隙磁密Bg", sym:"B_g", val:mag.B_g, unit:"T"},
+                    {cat:"Performance", name:"相电动势RMS", sym:"E_rms", val:perf.E_rms, unit:"V"},
+                    {cat:"Performance", name:"相电流RMS", sym:"I_rms", val:perf.I_rms, unit:"A"},
+                    {cat:"Performance", name:"转矩常数", sym:"K_t", val:perf.Kt, unit:"Nm/A"},
+                    {cat:"Dynamics", name:"相电感", sym:"L_ph", val:perf.L_ph_uH, unit:"uH"},
+                    {cat:"Dynamics", name:"有效气隙", sym:"g_eff", val:mag.g_eff, unit:"mm"},
+                    {cat:"Dynamics", name:"电气时间常数", sym:"Tau_e", val:perf.Tau_e_us, unit:"us"},
+                    {cat:"Loss", name:"直流铜损", sym:"P_cu_dc", val:losses.P_cu_dc, unit:"W"},
+                    {cat:"Loss", name:"总损耗", sym:"P_loss", val:losses.P_loss, unit:"W"},
+                    {cat:"Cost", name:"总成本", sym:"Total", val:cost.Cost_Total, unit:"RMB"}
+                ];
+                this.animateSaveStatus();
+            }
+
+            // Enhanced helper to inject detailed rows
+            updateOrInsertRow(sectionId, name, sym, val, unit, formula, valId, defText) {
+                let el = document.getElementById(valId);
+                // If row doesn't exist, create it dynamically
+                if (!el) {
+                    const tbody = document.querySelector(`#${sectionId} tbody`);
+                    const tr = document.createElement('tr');
+                    // Add subtle background for sub-items
+                    if(name.includes('└─')) tr.style.backgroundColor = "#f8fafc";
+                    
+                    tr.innerHTML = `
+                        <td>${name}</td>
+                        <td class="col-sym">${sym}</td>
+                        <td><span class="res-val" id="${valId}">0</span></td>
+                        <td>${unit}</td>
+                        <td class="col-form" style="font-size:13px;">${formula}</td>
+                        <td class="col-def" style="font-size:12px; color:#64748b;">${defText || ''}</td>
+                    `;
+                    tbody.appendChild(tr);
+                    el = document.getElementById(valId);
+                }
+                // Update value
+                el.innerText = val.toFixed(1);
+            }
+
+            animateSaveStatus() {
+                const statusEl = document.getElementById('saveStatus');
+                statusEl.innerText = "保存中...";
+                statusEl.className = "save-status show saving";
+                setTimeout(() => {
+                    statusEl.innerText = "已自动保存";
+                    statusEl.className = "save-status show";
+                    setTimeout(() => { statusEl.classList.remove('show'); }, 2000);
+                }, 300);
+            }
+            updateMagGrade() {
+                const sel = document.getElementById("mag_grade");
+                const val = sel.value;
+                const brInput = document.getElementById("Br");
+                if (val === "custom") {
+                    brInput.readOnly = false;
+                    brInput.style.backgroundColor = "#fffbf0"; 
+                } else {
+                    if (Config.magnets[val]) { brInput.value = Config.magnets[val]; app.calculate(); }
+                    brInput.readOnly = true; brInput.style.backgroundColor = "#f1f5f9";
+                }
+            }
+            nav(pageId) {
+                document.querySelectorAll('.page-section').forEach(el => el.classList.remove('active'));
+                document.getElementById(pageId).classList.add('active');
+                document.querySelectorAll('.nav-btn').forEach(el => el.classList.remove('active'));
+                const buttons = document.querySelectorAll('.nav-btn');
+                for (let btn of buttons) { if(btn.getAttribute('onclick').includes(pageId)) btn.classList.add('active'); }
+            }
+            exportExcel() {
+                let csvContent = "data:text/csv;charset=utf-8,\uFEFF";
+                csvContent += "Category,Parameter,Symbol,Value,Unit\n";
+                this.exportData.forEach(row => {
+                    const v = (typeof row.val === 'number') ? row.val.toFixed(4) : row.val;
+                    csvContent += `${row.cat},${row.name},${row.sym},${v},${row.unit}\n`;
+                });
+                const link = document.createElement("a");
+                link.setAttribute("href", encodeURI(csvContent));
+                link.setAttribute("download", "Motor_Design_Report.csv");
+                document.body.appendChild(link); link.click(); document.body.removeChild(link);
+            }
+        }
+
+        class Application {
+            constructor() { this.ui = new UIManager(); }
+            init() { Store.load(); this.ui.updateMagGrade(); this.calculate(); }
+            calculate() {
+                const inputs = Store.getInputs();
+                const geom = PhysicsEngine.calcGeometry(inputs);
+                const mag = PhysicsEngine.calcMagnetic(inputs, geom);
+                const perf = PhysicsEngine.calcPerformance(inputs, geom, mag);
+                const losses = PhysicsEngine.calcLosses(inputs, geom, mag, perf);
+                const thermal = PhysicsEngine.calcThermal(inputs, losses, geom);
+                const cost = PhysicsEngine.calcCost(inputs, geom);
+                const ipc = PhysicsEngine.calcIPC(inputs, geom, perf, thermal);
+                this.ui.updateView({ inputs, geom, mag, perf, losses, thermal, cost, ipc });
+                Store.save(inputs);
+            }
+        }
+        return new Application();
+    })();
+    window.onload = () => App.init();
+    window.app = App;
+
+</script>
+</body>
+</html>

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 9080 - 0
MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot


+ 909 - 0
PCB轴向磁通电机自动化仿真系统设计方案介绍.md

@@ -0,0 +1,909 @@
+# PCB轴向磁通电机自动化仿真系统设计方案介绍
+
+| 项目 | 内容 |
+|---|---|
+| 文档版本 | V1.1 |
+| 作者 | Car.Lin |
+| 编制日期 | 2026-08-26 |
+| 文档状态 | 初稿 / 待评审 |
+| 参考原型 | axial_mag_pull(Motor-CAD 轴向磁拉力仿真项目) |
+
+---
+
+## 1. 项目背景与目标
+
+### 1.1 背景
+
+轴向磁通电机(AFM)因其高功率密度、短轴向尺寸的优势,在电动汽车、航空航天、工业驱动等领域得到广泛应用。但其设计过程涉及多拓扑(SSSR / DRSS / SDSR)、多物理场(电磁 / 热 / 结构 / NVH)、多目标(效率 / 功率密度 / 扭矩 / 成本)的耦合优化,传统的"人工调参 → 单次仿真 → 人工分析"模式效率极低,且高度依赖工程师个人经验。
+
+现有 `axial_mag_pull` 项目已验证了"知识库驱动 + 脚本化执行 + 结果校验"的微缩闭环可行性,但仅覆盖单一目标(轴向磁拉力)、单一拓扑(SSSR)、单一工具(Motor-CAD)。本项目在此基础上,构建一个**通用的、可扩展的、双系统解耦的轴向磁通电机自动化仿真平台**。
+
+### 1.2 核心目标
+
+1. **方案生成智能化**:用户输入边界条件后,系统基于经验库 + 知识库 + AI推理,自动生成可执行的仿真方案(参数扫描范围、扫描顺序、工况列表、校验规则)。
+2. **仿真执行自动化**:执行程序读取方案后,自动驱动仿真工具完成全部迭代,记录每次结果,输出汇总数据表。
+3. **双系统解耦**:方案系统(网页端、需联网/AI)与执行程序(本地exe、无需联网/AI)通过标准化接口通信,可独立迭代。
+4. **知识自动沉淀**:每次仿真的方案、结果、结论自动结构化存储,反哺经验库和知识库,实现系统自我进化。
+5. **人在回路**:AI推荐方案 → 人工确认 → 执行,关键决策点保留人工审核权。
+
+---
+
+## 2. 系统总体架构
+
+### 2.1 双系统解耦架构
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│                    企业内网(需登录 / 权限控制)                     │
+│                                                                   │
+│  ┌─────────────────────────────────────────────────────────┐    │
+│  │           系统一:仿真方案生成及优化系统(Web端)            │    │
+│  │                                                           │    │
+│  │  ┌──────────┐  ┌──────────┐  ┌──────────────────────┐  │    │
+│  │  │ 前端UI    │  │ 后端服务  │  │ 数据层               │  │    │
+│  │  │ (Vue/React)│→│ (FastAPI) │→│ PostgreSQL + 向量库   │  │    │
+│  │  └──────────┘  └────┬─────┘  └──────────────────────┘  │    │
+│  │                      │                                     │    │
+│  │         ┌────────────┼────────────┐                       │    │
+│  │         ▼            ▼            ▼                       │    │
+│  │   ┌──────────┐ ┌──────────┐ ┌──────────┐                │    │
+│  │   │方案生成引擎│ │结果分析引擎│ │知识库管理 │                │    │
+│  │   │(规则+AI)  │ │(敏感性+Pareto)│ │(经验+知识)│                │    │
+│  │   └──────────┘ └──────────┘ └──────────┘                │    │
+│  └─────────────────────────────────────────────────────────┘    │
+│                              │                                    │
+│                              │ 接口:simulation_plan.json         │
+│                              │ (手动导出 / 内网API)                │
+│                              ▼                                    │
+│  ┌─────────────────────────────────────────────────────────┐    │
+│  │           系统二:仿真执行程序(本地 EXE)                   │    │
+│  │                                                           │    │
+│  │  ┌─────────────────────────────────────────────────┐    │    │
+│  │  │  用户界面 (PyQt/PySide)                            │    │    │
+│  │  │  方案加载 → 执行监控 → 结果导出 → 日志查看          │    │    │
+│  │  └──────────────────────┬──────────────────────────┘    │    │
+│  │                         │                                 │    │
+│  │         ┌───────────────┼───────────────┐                 │    │
+│  │         ▼               ▼               ▼                 │    │
+│  │  ┌───────────┐  ┌───────────┐  ┌───────────┐            │    │
+│  │  │执行引擎    │  │仿真适配器  │  │结果记录器  │            │    │
+│  │  │(循环+断点) │  │(Motor-CAD/│  │(CSV+JSON+ │            │    │
+│  │  │            │  │ Maxwell/  │  │ 日志)      │            │    │
+│  │  │            │  │ JMAG/Flux)│  │            │            │    │
+│  │  └───────────┘  └───────────┘  └───────────┘            │    │
+│  └─────────────────────────────────────────────────────────┘    │
+│                              │                                    │
+│                              │ 结果:simulation_results.csv       │
+│                              │ (手动导入 / 内网API)                │
+│                              ▼                                    │
+│                    反馈到系统一进行分析迭代                          │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+### 2.2 设计原则
+
+| 原则 | 说明 |
+|---|---|
+| **解耦优先** | 方案生成与仿真执行完全独立,通过标准化JSON接口通信,任一侧升级不影响另一侧 |
+| **工具无关** | 仿真工具通过"适配器"模式抽象,新增工具只需实现适配器接口 |
+| **拓扑可扩展** | 电机拓扑以配置化方式管理,新增拓扑只需定义参数体系和仿真模板 |
+| **人在回路** | AI生成的方案和知识库迭代内容均需人工确认后才生效 |
+| **知识驱动** | 所有方案生成均基于经验库检索 + 物理规则约束 + AI推理,三者交叉验证 |
+| **结果可校验** | 每次仿真结果自动通过可插拔的校验规则集,不通过的标记为异常 |
+
+---
+
+## 3. 系统一:仿真方案生成及优化系统(Web端)
+
+### 3.1 前端用户界面
+
+前端采用单页应用(SPA),分为四个核心工作区:
+
+#### 3.1.1 边界条件输入区
+
+| 输入类别 | 字段示例 | 说明 |
+|---|---|---|
+| 电机拓扑 | SSSR / DRSS / SDSR(单选,带拓扑示意图) | 初期支持SSSR和DRSS,SDSR后续扩展 |
+| 轴向尺寸 | 总轴向长度上限(mm) | 约束条件 |
+| 径向尺寸 | 外径上限 / 内径下限(mm) | 约束条件 |
+| 性能目标 | 输出功率(kW)、额定扭矩(Nm)、峰值扭矩(Nm) | 目标值 |
+| 效率要求 | 额定点效率目标(%)、高效区范围 | 目标值 |
+| 转速范围 | 额定转速 / 最高转速(rpm) | 工况定义 |
+| 冷却方式 | 自然冷却 / 水冷 / 油冷 | 影响热仿真边界 |
+| 约束条件 | 温升上限、磁钢最高工作温度、成本上限 | 硬约束 |
+
+#### 3.1.2 方案预览与编辑区
+
+- AI生成的方案以**可视化形式**展示:
+  - 参数扫描矩阵(变量 × 范围 × 步长 × 优先级)
+  - 扫描顺序甘特图(先扫哪些参数、后扫哪些)
+  - 预估总耗时(基于历史单次仿真时间 × 迭代次数;**初期按现有案例数据参考(单次电磁仿真约88~151秒),后续根据实际仿真结果和运行日志不断自动修正耗时基准**)
+  - 工况列表(空载 / 负载 / 不同温度点)
+- 支持**人工微调**:修改参数范围、调整优先级、增删工况
+- 每个参数建议附带**依据标签**(历史案例 / 物理约束 / 经验公式 / AI推理)
+
+#### 3.1.3 结果分析仪表盘
+
+- **迭代结果数据表**:每次迭代的输入参数 + 输出指标 + 校验状态
+- **参数敏感性热力图**:各参数对各目标的影响程度(Morris/Sobol指数)
+- **Pareto前沿图**:多目标优化的非支配解集(如效率 vs 功率密度)
+- **收敛曲线**:最优值随迭代次数的变化趋势
+- **异常点标记**:校验不通过的迭代点红色标记,可查看错误详情
+
+#### 3.1.4 知识库管理区
+
+- **经验案例库**:按拓扑 / 功率段 / 应用场景分类浏览
+- **相似案例检索**:输入新需求,自动检索Top-N最相似历史案例
+- **知识条目审核**:AI自动生成的知识条目进入待审核队列,专家确认后入库
+- **知识库版本管理**:每条知识有版本历史,可追溯修改记录
+
+### 3.2 后端核心模块
+
+#### 3.2.1 方案生成引擎
+
+方案生成采用**三源融合**策略:
+
+```
+用户边界条件
+     │
+     ├──→ ① 经验库检索:找Top-N相似历史案例,提取其参数范围和扫描策略
+     │
+     ├──→ ② 规则引擎:基于物理约束确定参数可行域(如磁钢厚度受轴向尺寸约束、
+     │                 气隙受加工精度约束、电流密度受冷却约束)
+     │
+     └──→ ③ AI推理:基于经验和规则,推荐扫描顺序、采样方法、优化算法
+              │
+              ▼
+         三源交叉验证 → 生成候选方案 → 人工确认 → 输出 simulation_plan.json
+```
+
+- **经验库检索**:使用向量相似度检索(边界条件向量化后在经验库中匹配),返回最相似的3-5个历史案例,其参数范围和扫描策略作为方案初值
+- **规则引擎**:内置电机设计的物理约束规则集(如 `磁钢厚度 ≤ 轴向长度 × 0.4`、`气隙 ≥ 0.3mm(加工精度下限)`、`电流密度 ≤ 12 A/mm²(自然冷却)`),自动裁剪参数可行域
+- **AI推理**:在经验初值和规则约束的基础上,AI推荐扫描策略(全因子 / LHS / 贝叶斯)、参数优先级、采样密度
+
+#### 3.2.2 结果分析引擎
+
+执行程序返回结果后,结果分析引擎自动完成:
+
+1. **数据清洗**:剔除校验不通过的异常点,标记缺失值
+2. **敏感性分析**:
+   - 初步筛选:Morris方法(计算量小,区分关键/非关键参数)
+   - 精确分析:Sobol方法(对关键参数做全局方差分解,量化主效应和交互效应)
+3. **代理模型构建**:对关键参数构建Kriging / RSM代理模型,用于快速预测
+4. **多目标优化**:在代理模型上运行NSGA-II / MOPSO,搜索Pareto前沿
+5. **迭代方案生成**:基于分析结果,自动生成下一轮仿真方案(如缩小参数范围、加密采样、聚焦Pareto最优区域)
+
+#### 3.2.3 知识库管理模块
+
+- **经验库**(结构化案例数据):
+  - 每条案例包含:边界条件、仿真方案、迭代结果、最优解、结论标签
+  - 支持向量检索(按边界条件相似度匹配)
+  - 支持按拓扑 / 功率段 / 应用场景筛选
+  - **冷启动策略**:初期不做历史数据批量导入(暂无结构化历史数据),经验库从空白开始,**重点在于在实际案例测试、仿真结果输出、运行日志记录的过程中自动积累经验**。每完成一次仿真,系统自动提取关键参数组合与结果对应关系,结构化存入经验库;随着测试案例增多,经验库质量自然提升
+- **知识库**(方法论文档):
+  - 参数语义字典(各拓扑下各参数的物理含义、取值范围、易错点)
+  - 仿真方法SOP(各工具的操作流程、已知坑、探测技术)
+  - 校验规则集(可插拔的结果可信度判据)
+  - 物理公式库(用于解析量级校验、参数初值估算)
+- **自动迭代机制**:
+  - 每次仿真完成后,AI自动提取"可沉淀的知识"(如"XX拓扑下,气隙对效率的影响显著高于磁钢厚度")
+  - 生成知识条目草稿,进入待审核队列
+  - 专家审核通过后正式入库,并关联到对应案例
+
+### 3.3 AI协同的定位
+
+AI在系统中扮演**"检索 + 推理 + 解释"的助手**角色,而非黑盒决策者:
+
+| AI能力 | 应用场景 | 人工审核点 |
+|---|---|---|
+| 相似案例检索 | 新需求进来,自动匹配历史案例 | 无(检索结果仅供参考) |
+| 方案生成 | 基于经验+规则+AI,生成仿真方案草稿 | **必须人工确认后才下发执行** |
+| 结果分析 | 自动计算敏感性、构建代理模型、生成Pareto前沿 | 无(分析结果客观呈现) |
+| 迭代方案推荐 | 基于分析结果,推荐下一轮扫描方案 | **必须人工确认后才下发执行** |
+| 知识提取 | 从仿真结果中自动提取可沉淀的经验 | **必须专家审核后才入库** |
+
+---
+
+## 4. 系统二:仿真执行程序(本地 EXE)
+
+### 4.1 用户界面
+
+基于PyQt/PySide的桌面应用,封装为独立exe(PyInstaller打包)。**目标用户为仿真工程师**,可接受较专业的界面,注重参数透明度、过程可控性和调试信息的完整性。界面分为四个区域:
+
+| 区域 | 功能 |
+|---|---|
+| **方案加载区** | 导入 `simulation_plan.json`,解析并展示执行计划(总迭代次数、参数组合列表、预估耗时) |
+| **执行监控区** | 实时显示:当前第N次/共M次、当前参数组合、已用时间/预计剩余时间、Motor-CAD/Maxwell窗口状态 |
+| **结果展示区** | 已完成迭代的结果数据表(输入参数 + 输出指标 + 校验状态),支持实时刷新 |
+| **日志查看区** | 分级日志(INFO / WARNING / ERROR),仿真工具原始输出归档,支持按级别筛选和导出 |
+
+操作按钮:`开始仿真` / `暂停` / `继续` / `停止` / `导出结果` / `导出日志`
+
+### 4.2 执行引擎
+
+执行引擎是一个**通用的方案解释器**,核心逻辑:
+
+```
+加载 simulation_plan.json
+     │
+     ▼
+解析 scan_strategy → 生成参数组合队列(按优先级排序)
+     │
+     ▼
+┌─── 循环:对每个参数组合 ──────────────────────────────┐
+│                                                         │
+│  1. 检查断点:如果该组合已完成(结果文件存在且校验通过),跳过 │
+│  2. 调用仿真适配器:设置参数 → 运行仿真 → 提取结果          │
+│  3. 运行校验规则集:对结果做可信度校验                       │
+│  4. 记录结果:写入CSV + JSON + 日志                         │
+│  5. 更新进度:刷新UI显示                                    │
+│                                                         │
+│  异常处理:                                                │
+│  - 仿真工具崩溃 → 自动重启,重试最多3次 → 仍失败则标记跳过   │
+│  - 许可证失效 → 暂停执行,提示用户检查许可证                  │
+│  - 求解不收敛 → 记录警告,标记结果为uncertain,继续执行      │
+│                                                         │
+└─────────────────────────────────────────────────────────┘
+     │
+     ▼
+全部完成 → 输出汇总数据表 simulation_results.csv + 执行报告
+```
+
+#### 关键特性:
+
+- **断点续跑**:每次迭代结果独立存储,程序中断后重启可从上次未完成的点继续(通过检查结果文件完整性判断)
+- **错误恢复**:仿真工具崩溃自动重启重试,许可证失效暂停等待,求解不收敛标记后继续
+- **无人值守模式**:可选模式,遇到非致命错误自动跳过继续,致命错误才暂停
+- **前台运行**:仿真工具默认前台弹出(方便人工检查),可切换为后台模式
+
+### 4.3 仿真适配器(多工具兼容)
+
+采用**适配器模式**抽象仿真工具,每个工具实现统一接口:
+
+```python
+class SimulationAdapter(ABC):
+    @abstractmethod
+    def connect(self) -> bool: ...        # 连接/启动仿真工具
+
+    @abstractmethod
+    def load_model(self, model_path: str) -> bool: ...  # 加载模型
+
+    @abstractmethod
+    def set_parameter(self, name: str, value: float) -> bool: ...  # 设置参数
+
+    @abstractmethod
+    def run_simulation(self, case: dict) -> bool: ...  # 运行仿真
+
+    @abstractmethod
+    def extract_result(self, metric: str) -> float: ...  # 提取结果
+
+    @abstractmethod
+    def disconnect(self) -> None: ...  # 断开/关闭
+```
+
+#### 适配器规划:
+
+| 优先级 | 仿真工具 | 自动化接口 | 物理场 | 状态 |
+|---|---|---|---|---|
+| P0 | Motor-CAD | PyMotorCAD (Python API) | 电磁(初期) | 已有验证基础(axial_mag_pull项目) |
+| P1 | Ansys Maxwell 3D | PyAEDT (Python API) / VBScript | 电磁 / 结构 | 规划中 |
+| P2 | JMAG | JSAT / Python API | 电磁 / 热 / 结构 | 规划中 |
+| P3 | Flux | Python API / 脚本 | 电磁 / 热 | 规划中 |
+
+> **初期重点**:先完成Motor-CAD电磁仿真适配器,验证全流程闭环后再扩展其他工具。
+
+### 4.4 结果记录与日志
+
+#### 输出文件结构:
+
+```
+output/
+├── simulation_results.csv       # 汇总数据表(所有迭代的输入+输出+校验状态)
+├── simulation_results.json      # 完整结果(含波形数据、校验详情)
+├── iteration_001/               # 每次迭代的独立目录
+│   ├── params.json              # 该次的输入参数
+│   ├── results.json             # 该次的输出结果
+│   ├── model_working.mot        # 工作模型副本(原模型不污染)
+│   └── raw_output.log           # 仿真工具原始输出
+├── execution_log.txt            # 执行程序日志(分级)
+└── execution_report.md          # 执行报告(汇总统计、异常列表、耗时分析)
+```
+
+#### 汇总数据表字段(CSV):
+
+| 字段 | 说明 |
+|---|---|
+| iteration_id | 迭代序号 |
+| timestamp | 执行时间戳 |
+| topology | 电机拓扑 |
+| param_1, param_2, ... | 各扫描参数的取值(动态列) |
+| case_id | 工况ID |
+| metric_1, metric_2, ... | 各输出指标的数值(动态列) |
+| validation_status | 校验状态(PASS / FAIL / UNCERTAIN) |
+| validation_details | 校验详情(各判据的偏差值) |
+| solve_time_s | 本次求解耗时 |
+| error_message | 错误信息(如有) |
+
+---
+
+## 5. 接口设计(双系统通信契约)
+
+### 5.1 仿真方案 JSON Schema
+
+两个系统之间的核心契约是 `simulation_plan.json`,结构定义如下:
+
+```json
+{
+  "plan_id": "SP-20260826-001",
+  "plan_version": "1.0",
+  "created_at": "2026-08-26T22:00:00+08:00",
+  "created_by": "engineer_001",
+  "status": "draft",
+
+  "motor_spec": {
+    "topology": "SSSR",
+    "model_template": "MARS-12S10P_SSSR.mot",
+    "axial_length_max_mm": 30,
+    "outer_diameter_max_mm": 150,
+    "inner_diameter_min_mm": 60,
+    "output_power_kw": 5,
+    "rated_torque_nm": 10,
+    "peak_torque_nm": 20,
+    "rated_speed_rpm": 5000,
+    "max_speed_rpm": 10000,
+    "target_efficiency": 0.92,
+    "cooling_method": "water",
+    "constraints": {
+      "max_temperature_c": 120,
+      "max_magnet_temp_c": 150,
+      "cost_max_rmb": 500
+    }
+  },
+
+  "scan_strategy": {
+    "method": "sequential_screening",
+    "description": "先Morris筛选关键参数,再LHS采样构建代理模型,最后NSGA-II多目标优化",
+    "max_total_iterations": 100,
+    "variables": [
+      {
+        "name": "Airgap",
+        "display_name": "气隙",
+        "unit": "mm",
+        "range": [0.6, 1.5],
+        "step": 0.3,
+        "priority": 1,
+        "screening_phase": "morris",
+        "basis": "历史案例SP-20260801-003显示气隙对轴向力影响最大(磁负刚度~207N/mm)"
+      },
+      {
+        "name": "Magnet_Length",
+        "display_name": "磁钢轴向厚度",
+        "unit": "mm",
+        "range": [2, 5],
+        "step": 1,
+        "priority": 2,
+        "screening_phase": "morris",
+        "basis": "物理约束:磁钢厚度 ≤ 轴向长度 × 0.4"
+      }
+    ],
+    "sampling": {
+      "method": "latin_hypercube",
+      "n_samples": 30,
+      "seed": 42
+    },
+    "optimization": {
+      "method": "NSGA-II",
+      "population_size": 50,
+      "max_generations": 30,
+      "objectives": [
+        {"metric": "efficiency", "direction": "maximize"},
+        {"metric": "power_density", "direction": "maximize"}
+      ],
+      "constraints": [
+        {"metric": "torque_nm", "operator": ">=", "value": 10},
+        {"metric": "temperature_c", "operator": "<=", "value": 120}
+      ]
+    }
+  },
+
+  "cases": [
+    {
+      "id": "OC_100C",
+      "name": "空载热态",
+      "type": "no_load",
+      "magnet_temp_c": 100,
+      "rms_current_a": 0
+    },
+    {
+      "id": "OL_rated",
+      "name": "额定负载",
+      "type": "load",
+      "magnet_temp_c": 100,
+      "rms_current_a": 21,
+      "speed_rpm": 5000
+    }
+  ],
+
+  "output_metrics": [
+    {"name": "axial_force_n", "display_name": "轴向磁拉力", "unit": "N"},
+    {"name": "torque_nm", "display_name": "输出扭矩", "unit": "Nm"},
+    {"name": "efficiency", "display_name": "效率", "unit": "%"},
+    {"name": "core_loss_w", "display_name": "铁耗", "unit": "W"},
+    {"name": "copper_loss_w", "display_name": "铜耗", "unit": "W"}
+  ],
+
+  "validation_rules": [
+    {
+      "rule_id": "action_reaction",
+      "name": "作用-反作用校验",
+      "description": "定转子合力反号,偏差<5%",
+      "threshold_pct": 5,
+      "enabled": true
+    },
+    {
+      "rule_id": "analytic_magnitude",
+      "name": "解析量级校验",
+      "description": "FEA结果与解析估算同量级(比值0.5~2.0)",
+      "ratio_range": [0.5, 2.0],
+      "enabled": true
+    },
+    {
+      "rule_id": "torque_crosscheck",
+      "name": "转矩交叉校验",
+      "description": "Σ(Ft×r)与转矩图偏差<10%",
+      "threshold_pct": 10,
+      "enabled": true
+    }
+  ],
+
+  "stop_criteria": [
+    {"type": "objective_met", "condition": "efficiency >= 0.92 AND torque_nm >= 10"},
+    {"type": "max_iterations", "value": 100},
+    {"type": "convergence", "metric": "best_efficiency", "tolerance": 0.001, "patience": 10}
+  ],
+
+  "execution_config": {
+    "simulation_tool": "MotorCAD",
+    "tool_version": "2026R1",
+    "model_path": "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot",
+    "foreground": true,
+    "max_retries": 3,
+    "timeout_per_iteration_s": 600,
+    "checkpoint_enabled": true
+  }
+}
+```
+
+### 5.2 结果数据格式
+
+执行程序输出 `simulation_results.csv`(汇总表)和 `simulation_results.json`(完整数据),JSON结构:
+
+```json
+{
+  "plan_id": "SP-20260826-001",
+  "executed_at": "2026-08-26T23:00:00+08:00",
+  "execution_duration_s": 7200,
+  "total_iterations": 45,
+  "successful": 42,
+  "failed": 3,
+  "iterations": [
+    {
+      "iteration_id": 1,
+      "timestamp": "2026-08-26T23:01:00+08:00",
+      "parameters": {"Airgap": 0.6, "Magnet_Length": 2},
+      "case_id": "OC_100C",
+      "results": {
+        "axial_force_n": 513.1,
+        "torque_nm": null,
+        "efficiency": null
+      },
+      "validation": {
+        "status": "PASS",
+        "checks": {
+          "action_reaction": {"deviation_pct": 1.7, "passed": true},
+          "analytic_magnitude": {"ratio": 1.41, "passed": true}
+        }
+      },
+      "solve_time_s": 151,
+      "error": null
+    }
+  ],
+  "summary": {
+    "best_efficiency": 0.935,
+    "best_torque_nm": 12.5,
+    "pareto_front": [{"efficiency": 0.935, "power_density": 4.2}, ...]
+  }
+}
+```
+
+### 5.3 通信方式
+
+| 阶段 | 方式 | 说明 |
+|---|---|---|
+| **初期(手动)** | 文件交换 | 方案系统导出 `simulation_plan.json` → U盘/共享文件夹拷贝 → 执行程序导入;执行完成后导出 `simulation_results.csv` → 拷贝回方案系统导入 |
+| **中期(内网API)** | REST API | 方案系统提供内网API:`POST /api/plan/{id}/download`(下载方案)、`POST /api/plan/{id}/upload-results`(上传结果);执行程序可配置API地址自动拉取方案和回传结果 |
+| **远期(全自动)** | 消息队列 | 方案系统下发任务到消息队列,执行程序监听队列自动领取任务、执行、回传结果;支持多执行节点并行 |
+
+> 两种通信方式的**数据格式完全一致**(JSON/CSV),只是传输通道不同,确保平滑升级。
+
+---
+
+## 6. 方案生成引擎算法选型
+
+基于检索到的电机优化领域论文和专利(Kriging-PSO、SVM多目标优化、Morris/Sobol灵敏度分析、LHS采样等),结合轴向磁通电机的设计特点,方案生成引擎采用**四阶段分层优化策略**:
+
+### 6.1 第一阶段:参数筛选(Morris灵敏度分析)
+
+**目标**:从众多设计参数中快速筛选出对目标影响显著的关键参数,降低后续优化维度。
+
+**方法**:Morris初等效应法
+- 属于全局灵敏度分析的One-Factor-At-a-Time(OAT)方法
+- 计算每个参数的初等效应均值(μ)和标准差(σ)
+- μ大 → 参数主效应显著;σ大 → 参数交互效应或非线性强
+- 计算量小(通常k个参数需要10~50次评估),适合初步筛选
+
+**输出**:参数分类
+- **核心参数**(μ大、σ大):进入第二阶段精细优化
+- **敏感参数**(μ大、σ小):线性影响显著,可固定为最优值或粗扫
+- **普通参数**(μ小):影响不显著,固定为经验值,不参与扫描
+
+**参考依据**:《基于代理模型的电机多学科优化关键技术综述》(电工技术学报)指出Morris方法逻辑清晰、易于实现,适合参数维度高时的初步筛选;IEEE综述《Review of Sensitivity Analysis Methods for Optimal Design Parameters of Motors》确认Sobol方法可信度高但计算量大,Morris方法适合预筛选。
+
+### 6.2 第二阶段:实验设计与采样(LHS拉丁超立方)
+
+**目标**:在关键参数的设计空间内均匀采样,为代理模型构建提供高质量训练数据。
+
+**方法**:拉丁超立方采样(Latin Hypercube Sampling, LHS)
+- 在每个维度上分层采样,确保边缘分布均匀
+- 比全因子设计和随机采样更高效,能以较少样本覆盖设计空间
+- 适合构建代理模型的训练数据
+
+**样本量**:通常为参数维度的5~10倍(如5个关键参数,取30~50个样本)
+
+**参考依据**:《基于Kriging-PSO算法的双转子PCB轴向磁通电机优化设计与分析》(电机工程学报)采用LHS方法采样优化变量并进行全局敏感性分析;MDPI论文《Design, Optimization, and Validation of a Dual Three-Phase YASA Axial Flux Machine》采用LHS+RSM+GA的代理模型优化框架。
+
+### 6.3 第三阶段:代理模型构建(Kriging)
+
+**目标**:用代理模型近似仿真输入-输出关系,大幅加速后续优化搜索。
+
+**方法**:Kriging(克里金)代理模型
+- 基于高斯过程的插值模型,能给出预测值和预测不确定性
+- 对非线性、多峰的电机设计空间拟合精度高
+- 预测不确定性可用于指导后续采样(加点准则)
+
+**备选**:响应面法(RSM)—— 适合强线性关系,计算更简单;神经网络 —— 适合极高维,但需要更多训练数据
+
+**验证**:用留一法交叉验证(LOOCV)或R²指标评估代理模型精度,R²>0.9才用于优化
+
+**参考依据**:Kriging-PSO是国内电机优化论文的主流方法组合(电机工程学报多篇论文采用);安世亚太CAE+AI融合方案也采用"自动仿真生成数据→训练代理模型→AI优化算法搜索"的标准化流程。
+
+### 6.4 第四阶段:多目标优化(NSGA-II)
+
+**目标**:在代理模型上搜索多目标Pareto最优解集。
+
+**方法**:NSGA-II(非支配排序遗传算法)
+- 最经典的多目标进化算法,收敛性和分布性均衡
+- 输出Pareto前沿(非支配解集),供决策者权衡选择
+- 适合电机设计中"效率 vs 功率密度 vs 成本"的多目标权衡
+
+**备选**:MOPSO(多目标粒子群)—— 收敛更快,但容易早熟;SMS-EMOA —— 注重解集分布性
+
+**输出**:Pareto前沿解集,每个解包含完整参数组合和预测性能
+
+**参考依据**:PMC论文《Multi-objective optimization of dual-stator permanent magnet motor based on composite algorithm》采用"灵敏度分析分类→Taguchi优化显著变量→Kriging+GA优化非显著变量"的复合算法;IET论文采用多级优化框架。
+
+### 6.5 第五阶段:精确验证与迭代
+
+**目标**:对Pareto最优解做精确FEA验证,确认代理模型预测的准确性。
+
+**方法**:
+1. 从Pareto前沿选取3~5个代表性候选解(如最高效率、最高功率密度、折中解)
+2. 用真实仿真工具精确计算(不通过代理模型)
+3. 对比代理模型预测值与FEA真实值,偏差>5%的标记为代理模型失准
+4. 将验证结果加入训练集,更新代理模型,必要时重新优化
+
+**闭环**:如果验证结果不满足目标,或代理模型精度不足,自动生成下一轮仿真方案(在失准区域加密采样),进入下一轮迭代。
+
+### 6.6 算法选型总结
+
+```
+用户边界条件
+     │
+     ▼
+┌─────────────────────────────────────────────┐
+│ 阶段1: Morris筛选 (10~50次仿真)              │
+│ 输出: 参数分类 (核心/敏感/普通)               │
+└──────────────────┬──────────────────────────┘
+                   ▼
+┌─────────────────────────────────────────────┐
+│ 阶段2: LHS采样 (30~50次仿真)                 │
+│ 输出: 训练数据集                               │
+└──────────────────┬──────────────────────────┘
+                   ▼
+┌─────────────────────────────────────────────┐
+│ 阶段3: Kriging代理模型构建 (0次仿真, 纯计算)  │
+│ 输出: 代理模型 + 精度验证(R²)                 │
+└──────────────────┬──────────────────────────┘
+                   ▼
+┌─────────────────────────────────────────────┐
+│ 阶段4: NSGA-II多目标优化 (0次仿真, 纯计算)    │
+│ 输出: Pareto前沿解集                           │
+└──────────────────┬──────────────────────────┘
+                   ▼
+┌─────────────────────────────────────────────┐
+│ 阶段5: 精确FEA验证 (3~5次仿真)                │
+│ 输出: 验证后的最优解                           │
+└──────────────────┬──────────────────────────┘
+                   ▼
+          不满足 → 生成下一轮方案 → 循环
+          满足 → 输出最终设计方案
+```
+
+**总仿真次数估算**:约50~110次/轮(相比全因子扫描的数百上千次,效率提升5~10倍)
+
+---
+
+## 7. 多物理场仿真规划
+
+### 7.1 分阶段实施
+
+| 阶段 | 物理场 | 仿真工具 | 目标 | 优先级 |
+|---|---|---|---|---|
+| **第一期** | 电磁 | Motor-CAD | 电磁性能(扭矩、效率、损耗、轴向力) | P0 |
+| **第二期** | 热 | Motor-CAD热模块 / Flux热 | 温升分布、热点温度、冷却效果 | P1 |
+| **第三期** | 结构力学 | Maxwell结构 / Workbench | 转子应力、变形、模态分析 | P2 |
+| **第四期** | NVH | Maxwell + Workbench谐响应 | 电磁噪声、振动 | P3 |
+
+### 7.2 多物理场耦合策略
+
+- **单向耦合**(初期):电磁仿真输出损耗 → 作为热仿真的热源 → 热仿真输出温度 → 修正电磁仿真的材料属性(磁钢Br温度系数、绕组电阻温度系数)
+- **双向耦合**(远期):电磁-热-结构全耦合迭代,直到温度和应力收敛
+
+### 7.3 接口预留
+
+- `simulation_plan.json` 中 `cases` 字段支持多物理场工况定义
+- 仿真适配器接口支持多物理场结果提取
+- 校验规则集支持多物理场专属判据(如热仿真的温升限值、结构仿真的应力限值)
+
+---
+
+## 8. 电机拓扑覆盖规划
+
+### 8.1 拓扑参数体系
+
+| 拓扑 | 缩写 | 特点 | 关键参数 | 优先级 |
+|---|---|---|---|---|
+| 单定子单转子 | SSSR | 结构简单,有不平衡轴向力 | 气隙、磁钢厚度、定子参数 | P0 |
+| 双转子单定子 | DRSS | 轴向力对消,功率密度高 | 双气隙、双磁钢厚度、定子参数 | P0 |
+| 双定子单转子 | SDSR | 转子居中,散热好 | 双气隙、双定子参数、磁钢厚度 | P1 |
+
+### 8.2 拓扑管理方式
+
+- 每种拓扑定义独立的**参数语义字典**(参数名、物理含义、取值范围、与其他拓扑的对应关系)
+- 每种拓扑有独立的**仿真模板**(基础.mot模型、默认工况、默认校验规则)
+- 方案生成引擎根据拓扑选择对应的参数体系和仿真模板
+- 新增拓扑只需:定义参数字典 + 提供基础模型模板 + 配置默认规则,无需修改引擎核心代码
+
+### 8.3 初期重点
+
+- **SSSR**:基于现有 `axial_mag_pull` 项目直接迁移,**默认模板采用 `MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot`**,最快验证全流程闭环
+- **DRSS**:轴向磁通电机的主流高功率密度拓扑,**基础模型模板后续补充**,待SSSR流程验证稳定后启动开发
+
+---
+
+## 9. 技术栈选型
+
+### 9.1 系统一(Web端)
+
+| 层级 | 技术选型 | 说明 |
+|---|---|---|
+| 前端框架 | Vue 3 + TypeScript + Element Plus | 企业级UI组件丰富,适合数据密集型仪表盘 |
+| 可视化 | ECharts + D3.js | ECharts做常规图表(热力图、Pareto图、收敛曲线),D3.js做自定义架构图/拓扑图 |
+| 后端框架 | Python FastAPI | 异步高性能,自动生成API文档,与仿真脚本同语言便于维护 |
+| 关系数据库 | PostgreSQL | 存储用户、项目、方案、结果等结构化数据 |
+| 向量数据库 | pgvector(PostgreSQL扩展) | 存储经验案例的向量表示,支持相似度检索;无需额外部署独立向量库 |
+| AI推理 | 可插拔LLM接口(API-Key / 内网部署) | **初期**:通过API-Key调用大模型(数据经内网代理网关,敏感字段脱敏);**第二期**:迁移到企业内网部署的大模型(Ollama + 本地模型 / 私有API网关),数据完全不出内网 |
+| 优化算法库 | scikit-learn + pymoo + SALib | scikit-learn做代理模型,pymoo做NSGA-II多目标优化,SALib做Morris/Sobol灵敏度分析 |
+| 任务队列 | Celery + Redis | 异步处理方案生成、结果分析、知识提取等耗时任务 |
+| 部署 | Docker + Nginx | 容器化部署,内网服务器运行 |
+
+### 9.2 系统二(本地EXE)
+
+| 层级 | 技术选型 | 说明 |
+|---|---|---|
+| GUI框架 | PySide6 (Qt for Python) | LGPL许可,比PyQt5更宽松;Qt Designer可视化设计界面 |
+| 仿真驱动 | PyMotorCAD / PyAEDT / 各工具Python API | 适配器模式封装 |
+| 数据处理 | pandas + openpyxl | 结果数据表处理,支持导出Excel |
+| 打包 | PyInstaller | 打包为单文件exe,约40~80MB |
+| 日志 | loguru | 比标准logging更易用,支持分级、轮转、彩色输出 |
+| 配置 | YAML / JSON | 执行程序配置(工具路径、API地址等) |
+
+### 9.3 共用组件
+
+| 组件 | 说明 |
+|---|---|
+| 方案JSON Schema | 两个系统共用的接口契约定义(JSON Schema格式) |
+| 校验规则引擎 | 可插拔的结果可信度校验框架,两个系统共用(执行端实时校验,方案端批量复核) |
+| 参数语义字典 | 各拓扑的参数定义,两个系统共用 |
+
+---
+
+## 10. 数据安全与权限管理
+
+### 10.1 部署环境
+
+- 系统一部署在**企业内网服务器**,不暴露到公网
+- 数据库仅在内网访问,设置防火墙规则
+- 执行程序在工程师本地电脑运行,通过内网API与系统一通信(或纯文件交换)
+
+### 10.2 用户权限管理
+
+| 角色 | 权限 |
+|---|---|
+| **工程师** | 创建/编辑自己的项目、输入边界条件、确认方案、查看自己项目的结果、导出方案/结果 |
+| **专家** | 工程师全部权限 + 审核知识库条目 + 查看所有项目(只读) + 管理校验规则集 |
+| **管理员** | 全部权限 + 用户管理 + 系统配置 + 数据库备份/恢复 |
+
+- 项目级权限:每个项目有创建者和共享成员列表,非成员不可见
+- 操作审计:所有关键操作(方案确认、知识审核、数据导出)记录审计日志
+
+### 10.3 数据加密
+
+- 传输加密:内网HTTPS
+- 存储加密:敏感数据(电机设计参数)数据库加密存储
+- 导出控制:数据导出操作记录审计,可配置导出审批流程
+
+---
+
+## 11. 实施路线图
+
+### 11.1 总体节奏
+
+```
+月份:  1    2    3    4    5    6    7    8    9    10   11   12
+       ├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
+Phase1 │████████████│                                              │
+  最小闭环  接口定义+执行程序(Motor-CAD电磁)+SSSR拓扑+手动文件交换   │
+       │    ├─────────────────┤                                    │
+Phase2 │    │█████████████████████████│                            │
+  方案系统  Web端基础框架+经验库+规则引擎+方案生成+结果分析+人在回路  │
+       │         ├─────────────────────────────┤                   │
+Phase3 │         │██████████████████████████████████│              │
+  算法增强  Morris筛选+LHS+Kriging+NSGA-II+DRSS拓扑+内网API通信    │
+       │              ├──────────────────────────────────────────┤│
+Phase4 │              │██████████████████████████████████████████││
+  扩展完善  热/结构仿真+SDSR拓扑+Maxwell/JMAG适配器+知识库自动迭代  │
+```
+
+### 11.2 各阶段详细目标
+
+#### Phase 1:最小闭环验证(第1-2月)
+
+**目标**:验证双系统解耦架构和接口契约的可行性
+
+- [ ] 定义 `simulation_plan.json` 和 `simulation_results.csv` 的完整Schema
+- [ ] 开发执行程序核心框架(PySide6 UI + 方案解析 + 执行引擎 + 结果记录)
+- [ ] 实现Motor-CAD电磁仿真适配器(基于axial_mag_pull项目迁移)
+- [ ] 支持SSSR拓扑
+- [ ] 支持手动文件交换(导出JSON → 执行 → 导入CSV)
+- [ ] 用现有轴向磁拉力案例做端到端验证
+
+**交付物**:执行程序V0.1(exe)、接口规范文档V1.0、验证报告
+
+#### Phase 2:方案系统基础(第2-5月)
+
+**目标**:Web端方案系统可用,实现人在回路的方案生成和结果分析
+
+- [ ] Web端基础框架(Vue3 + FastAPI + PostgreSQL)
+- [ ] 用户管理和权限系统
+- [ ] 项目管理(创建/编辑/共享)
+- [ ] 边界条件输入界面
+- [ ] 经验库(结构化存储 + 向量检索)
+- [ ] 规则引擎(物理约束裁剪参数可行域)
+- [ ] 方案生成(经验检索 + 规则 + AI推荐,人工确认)
+- [ ] 方案预览与编辑界面
+- [ ] 结果导入与分析仪表盘(数据表 + 基础图表)
+- [ ] 知识库管理(手动录入 + 审核流程)
+
+**交付物**:方案系统V0.5(内网可访问)、使用手册
+
+#### Phase 3:算法增强与拓扑扩展(第4-8月)
+
+**目标**:实现完整的四阶段优化算法,支持DRSS拓扑,内网API通信
+
+- [ ] Morris灵敏度分析模块
+- [ ] LHS拉丁超立方采样模块
+- [ ] Kriging代理模型构建与验证
+- [ ] NSGA-II多目标优化模块
+- [ ] 四阶段分层优化策略整合
+- [ ] 参数敏感性热力图
+- [ ] Pareto前沿可视化
+- [ ] 收敛曲线
+- [ ] DRSS拓扑支持(参数字典 + 仿真模板)
+- [ ] 内网API通信(方案下载 + 结果上传)
+- [ ] 迭代方案自动推荐(结果分析 → 下一轮方案)
+
+**交付物**:方案系统V1.0、执行程序V1.0、算法验证报告
+
+#### Phase 4:多物理场与工具扩展(第6-12月)
+
+**目标**:扩展物理场和仿真工具,完善知识库自动迭代
+
+- [ ] 热仿真模块(Motor-CAD热模块 / Flux)
+- [ ] 结构力学仿真模块(Maxwell结构 / Workbench)
+- [ ] SDSR拓扑支持
+- [ ] Ansys Maxwell 3D仿真适配器
+- [ ] JMAG仿真适配器(可选)
+- [ ] 知识库自动提取与迭代(AI从仿真结果中提取经验,专家审核入库)
+- [ ] 多物理场耦合(电磁-热单向耦合)
+- [ ] NVH仿真(可选)
+
+**交付物**:方案系统V2.0、执行程序V2.0、多物理场验证报告
+
+---
+
+## 12. 风险与应对
+
+| 风险 | 影响 | 概率 | 应对措施 |
+|---|---|---|---|
+| **仿真工具API不稳定** | 执行程序频繁出错 | 高 | 适配器模式隔离变化;内置重试和错误恢复;保留手动操作兜底 |
+| **代理模型精度不足** | 优化结果偏离真实最优 | 中 | 严格的R²验证门槛;Pareto解必须FEA验证;失准区域自动加密采样 |
+| **经验库冷启动质量差** | 初期方案生成不可靠 | 高 | 初期以规则引擎为主,AI推荐为辅;手动录入标杆案例;每轮仿真后人工审核知识提取 |
+| **多拓扑参数体系差异大** | 方案生成引擎难以通用 | 中 | 拓扑配置化管理,参数字典独立定义;引擎核心不依赖具体拓扑 |
+| **仿真耗时长,迭代周期慢** | 用户体验差 | 高 | 四阶段优化策略减少总仿真次数;断点续跑;远期支持多节点并行 |
+| **企业内网部署限制** | AI模型无法调用公网API | 中 | 支持内网部署的大模型;初期可用API网关代理,后期迁移内网模型 |
+| **数据安全合规** | 电机设计数据泄露 | 低 | 内网部署 + 权限管理 + 操作审计 + 导出控制 |
+| **多物理场耦合复杂** | 后期扩展困难 | 中 | 接口预留多物理场字段;先做单向耦合,双向耦合作为远期目标 |
+
+---
+
+## 13. 参考资料
+
+### 13.1 学术论文
+
+1. 《基于Kriging-PSO算法的双转子PCB轴向磁通电机优化设计与分析》— 电机工程学报,采用LHS采样+全局敏感性分析+Kriging响应面+PSO优化
+2. 《基于代理模型的电机多学科优化关键技术综述》— 电工技术学报,2022,系统综述Morris/Sobol灵敏度分析、代理模型、优化算法
+3. *Design, Optimization, and Validation of a Dual Three-Phase YASA Axial Flux Machine* — MDPI Energies, 2025,采用LHS+RSM+GA的代理模型优化框架
+4. *A General SVM-Based Multi-Objective Optimization Methodology for Axial Flux Motor Design* — ResearchGate,基于SVM的多目标优化方法
+5. *Review of Sensitivity Analysis Methods for Optimal Design Parameters of Motors* — IEEE Xplore,电机优化参数灵敏度分析方法综述
+6. *Multi-objective optimization of dual-stator permanent magnet motor based on composite algorithm* — PMC, 2024,灵敏度分析分类+Taguchi+Kriging+GA复合算法
+
+### 13.2 专利与行业方案
+
+1. CN202511697293 — 电机系统多学科协同设计与工程数字化平台(NLP+多物理场仿真+自动化工艺生成)
+2. CN107944666A — 一种电机云设计平台(云基础服务+设计资源库+核心设计云服务)
+3. 基于AI辅助的多物理场耦合电机优化设计方法及系统(2026专利)— LHS采样+AI代理模型+LPTN热网络双向耦合
+4. Ansys Maxwell + OptiSlang — 商业电机优化方案(OptiSlang做敏感性分析和优化,Maxwell做仿真)
+5. 安世亚太CAE+AI融合方案 — 标准化AI仿真研发流程(参数化模型→自动仿真→代理模型→AI优化→验证)
+
+### 13.3 内部参考
+
+1. `axial_mag_pull` 项目 — Motor-CAD轴向磁拉力仿真(本系统的原型验证)
+2. `docs/KNOWLEDGE_BASE.md` — 知识库原型(方法/参数语义/探测技术/SOP)
+3. `axial_force_final.py` — 仿真脚本原型(PyMotorCAD驱动+三判据校验)
+
+---
+
+## 附录A:术语表
+
+| 术语 | 全称 | 说明 |
+|---|---|---|
+| AFM | Axial Flux Motor | 轴向磁通电机 |
+| SSSR | Single Stator Single Rotor | 单定子单转子 |
+| DRSS | Dual Rotor Single Stator | 双转子单定子 |
+| SDSR | Single Rotor Dual Stator | 双定子单转子 |
+| LHS | Latin Hypercube Sampling | 拉丁超立方采样 |
+| RSM | Response Surface Methodology | 响应面法 |
+| GA | Genetic Algorithm | 遗传算法 |
+| PSO | Particle Swarm Optimization | 粒子群优化 |
+| NSGA-II | Non-dominated Sorting Genetic Algorithm II | 非支配排序遗传算法第二代 |
+| MOPSO | Multi-Objective Particle Swarm Optimization | 多目标粒子群优化 |
+| DOE | Design of Experiments | 实验设计 |
+| FEA | Finite Element Analysis | 有限元分析 |
+| YASA | Yokeless And Segmented Armature | 无轭分段电枢(轴向磁通电机拓扑) |
+
+---
+
+*文档结束 — V1.1,作者 Car.Lin,2026-08-26*
+
+> **V1.1 更新说明**:明确AI模型部署策略(初期API-Key,第二期内网部署);SSSR默认模板采用MARS-12S10P模型;执行程序目标用户为仿真工程师;经验库冷启动策略调整为运行中自动积累;仿真耗时基准按现有案例参考、后续日志自动修正。

+ 671 - 0
PCB轴向磁通电机自动化仿真系统设计方案简介.html

@@ -0,0 +1,671 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>PCB轴向磁通电机自动化仿真系统</title>
+<style>
+  * { margin: 0; padding: 0; box-sizing: border-box; }
+
+  :root {
+    --bg: #f7f8fa;
+    --surface: #ffffff;
+    --text: #1a1f36;
+    --text-2: #5a6478;
+    --text-3: #94a0b4;
+    --border: #e8ecf2;
+    --blue: #2563eb;
+    --blue-light: #eff4ff;
+    --green: #16a34a;
+    --green-light: #ecfdf3;
+    --purple: #7c3aed;
+    --purple-light: #f5f0ff;
+    --orange: #ea580c;
+    --orange-light: #fff4ed;
+    --cyan: #0891b2;
+    --cyan-light: #ecfeff;
+    --pink: #db2777;
+    --pink-light: #fdf2f8;
+  }
+
+  body {
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC',
+                 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
+    background: var(--bg);
+    color: var(--text);
+    line-height: 1.7;
+    -webkit-font-smoothing: antialiased;
+  }
+
+  .wrap { max-width: 1080px; margin: 0 auto; padding: 0 32px; }
+
+  /* ===== 顶部栏 ===== */
+  .topbar {
+    padding: 20px 0;
+    border-bottom: 1px solid var(--border);
+    background: var(--surface);
+    position: sticky;
+    top: 0;
+    z-index: 100;
+    backdrop-filter: blur(10px);
+    background: rgba(255,255,255,0.85);
+  }
+  .topbar-inner {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+  }
+  .brand {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    font-weight: 700;
+    font-size: 15px;
+  }
+  .brand-logo {
+    width: 32px; height: 32px;
+    background: linear-gradient(135deg, var(--blue), var(--cyan));
+    border-radius: 8px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: white;
+    font-size: 16px;
+    font-weight: 800;
+  }
+  .topbar-meta { font-size: 13px; color: var(--text-3); }
+
+  /* ===== Hero ===== */
+  .hero {
+    padding: 80px 0 60px;
+    text-align: center;
+  }
+  .hero-tag {
+    display: inline-block;
+    padding: 5px 14px;
+    background: var(--blue-light);
+    color: var(--blue);
+    border-radius: 100px;
+    font-size: 12px;
+    font-weight: 600;
+    margin-bottom: 24px;
+    letter-spacing: 0.02em;
+  }
+  .hero h1 {
+    font-size: clamp(30px, 4.5vw, 48px);
+    font-weight: 800;
+    line-height: 1.25;
+    letter-spacing: -0.02em;
+    margin-bottom: 20px;
+  }
+  .hero h1 .accent { color: var(--blue); }
+  .hero-sub {
+    font-size: clamp(15px, 1.8vw, 18px);
+    color: var(--text-2);
+    max-width: 640px;
+    margin: 0 auto 40px;
+  }
+  .hero-keywords {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: center;
+    gap: 10px;
+  }
+  .kw {
+    padding: 7px 16px;
+    background: var(--surface);
+    border: 1px solid var(--border);
+    border-radius: 8px;
+    font-size: 13px;
+    color: var(--text-2);
+    font-weight: 500;
+  }
+
+  /* ===== Section 通用 ===== */
+  section { padding: 64px 0; }
+  .sec-head {
+    text-align: center;
+    margin-bottom: 48px;
+  }
+  .sec-label {
+    font-size: 12px;
+    font-weight: 700;
+    color: var(--blue);
+    letter-spacing: 0.12em;
+    text-transform: uppercase;
+    margin-bottom: 10px;
+  }
+  .sec-title {
+    font-size: clamp(22px, 3vw, 32px);
+    font-weight: 700;
+    margin-bottom: 10px;
+    letter-spacing: -0.01em;
+  }
+  .sec-desc {
+    font-size: 15px;
+    color: var(--text-2);
+    max-width: 560px;
+    margin: 0 auto;
+  }
+
+  /* ===== 解耦核心理念 ===== */
+  .decouple {
+    background: var(--surface);
+    border-top: 1px solid var(--border);
+    border-bottom: 1px solid var(--border);
+  }
+  .decouple-grid {
+    display: grid;
+    grid-template-columns: 1fr 80px 1fr;
+    gap: 0;
+    align-items: stretch;
+  }
+  .decouple-side {
+    padding: 36px 32px;
+    border-radius: 16px;
+  }
+  .decouple-side.think {
+    background: linear-gradient(135deg, #f0f5ff 0%, #e8f4fd 100%);
+    border: 1px solid #dbeafe;
+  }
+  .decouple-side.do {
+    background: linear-gradient(135deg, #f5f3ff 0%, #fdf2f8 100%);
+    border: 1px solid #ede9fe;
+  }
+  .decouple-icon {
+    width: 44px; height: 44px;
+    border-radius: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 22px;
+    margin-bottom: 16px;
+  }
+  .think .decouple-icon { background: var(--blue); color: white; }
+  .do .decouple-icon { background: var(--purple); color: white; }
+
+  .decouple-title {
+    font-size: 18px;
+    font-weight: 700;
+    margin-bottom: 4px;
+  }
+  .decouple-sub {
+    font-size: 12px;
+    color: var(--text-3);
+    margin-bottom: 20px;
+    font-weight: 500;
+  }
+  .decouple-list {
+    list-style: none;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+  }
+  .decouple-list li {
+    font-size: 13.5px;
+    color: var(--text-2);
+    padding-left: 22px;
+    position: relative;
+    line-height: 1.5;
+  }
+  .decouple-list li::before {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 7px;
+    width: 14px;
+    height: 14px;
+    border-radius: 4px;
+  }
+  .think .decouple-list li::before {
+    background: var(--blue);
+    content: '✦';
+    color: white;
+    font-size: 9px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  .do .decouple-list li::before {
+    background: var(--purple);
+    content: '▸';
+    color: white;
+    font-size: 9px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .decouple-bridge {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    gap: 12px;
+  }
+  .bridge-arrow {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 4px;
+  }
+  .bridge-arrow svg { width: 48px; height: 20px; }
+  .bridge-label {
+    font-size: 10px;
+    font-weight: 700;
+    color: var(--orange);
+    background: var(--orange-light);
+    padding: 3px 8px;
+    border-radius: 4px;
+    white-space: nowrap;
+    letter-spacing: 0.02em;
+  }
+
+  .decouple-bottom {
+    margin-top: 28px;
+    text-align: center;
+    font-size: 13px;
+    color: var(--text-2);
+    padding: 16px 24px;
+    background: var(--bg);
+    border-radius: 10px;
+    border: 1px dashed var(--border);
+  }
+  .decouple-bottom strong { color: var(--text); }
+
+  /* ===== 六大价值 ===== */
+  .values-grid {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 20px;
+  }
+  .value-card {
+    background: var(--surface);
+    border: 1px solid var(--border);
+    border-radius: 14px;
+    padding: 28px 24px;
+    transition: all 0.25s ease;
+  }
+  .value-card:hover {
+    transform: translateY(-3px);
+    box-shadow: 0 8px 24px rgba(0,0,0,0.06);
+    border-color: transparent;
+  }
+  .value-num {
+    font-size: 11px;
+    font-weight: 700;
+    color: var(--text-3);
+    letter-spacing: 0.1em;
+    margin-bottom: 12px;
+  }
+  .value-icon {
+    width: 40px; height: 40px;
+    border-radius: 10px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 20px;
+    margin-bottom: 16px;
+  }
+  .value-card:nth-child(1) .value-icon { background: var(--blue-light); }
+  .value-card:nth-child(2) .value-icon { background: var(--green-light); }
+  .value-card:nth-child(3) .value-icon { background: var(--purple-light); }
+  .value-card:nth-child(4) .value-icon { background: var(--orange-light); }
+  .value-card:nth-child(5) .value-icon { background: var(--cyan-light); }
+  .value-card:nth-child(6) .value-icon { background: var(--pink-light); }
+
+  .value-title {
+    font-size: 16px;
+    font-weight: 700;
+    margin-bottom: 8px;
+  }
+  .value-desc {
+    font-size: 13px;
+    color: var(--text-2);
+    line-height: 1.65;
+  }
+
+  /* ===== 闭环流程 ===== */
+  .loop {
+    background: var(--surface);
+    border-top: 1px solid var(--border);
+    border-bottom: 1px solid var(--border);
+  }
+  .loop-steps {
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    gap: 0;
+    position: relative;
+  }
+  .loop-step {
+    text-align: center;
+    padding: 0 16px;
+    position: relative;
+  }
+  .step-circle {
+    width: 52px; height: 52px;
+    border-radius: 50%;
+    background: var(--surface);
+    border: 2px solid var(--blue);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    font-size: 18px;
+    font-weight: 700;
+    color: var(--blue);
+    margin: 0 auto 16px;
+    position: relative;
+    z-index: 2;
+  }
+  .loop-step:not(:last-child)::after {
+    content: '';
+    position: absolute;
+    top: 26px;
+    right: -50%;
+    width: 100%;
+    height: 2px;
+    background: var(--border);
+    z-index: 1;
+  }
+  .step-name {
+    font-size: 14px;
+    font-weight: 700;
+    margin-bottom: 4px;
+  }
+  .step-desc {
+    font-size: 12px;
+    color: var(--text-3);
+    line-height: 1.5;
+  }
+
+  .loop-note {
+    margin-top: 40px;
+    text-align: center;
+    font-size: 13px;
+    color: var(--text-2);
+  }
+  .loop-note .highlight {
+    color: var(--blue);
+    font-weight: 600;
+  }
+
+  /* ===== 数据亮点 ===== */
+  .stats-row {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 20px;
+    margin-top: 48px;
+  }
+  .stat-card {
+    text-align: center;
+    padding: 28px 20px;
+    background: var(--surface);
+    border: 1px solid var(--border);
+    border-radius: 14px;
+  }
+  .stat-num {
+    font-size: 36px;
+    font-weight: 800;
+    color: var(--blue);
+    line-height: 1;
+    margin-bottom: 8px;
+  }
+  .stat-num .unit { font-size: 18px; }
+  .stat-label {
+    font-size: 13px;
+    color: var(--text-2);
+    font-weight: 500;
+  }
+
+  /* ===== 页脚 ===== */
+  footer {
+    padding: 40px 0;
+    text-align: center;
+    border-top: 1px solid var(--border);
+    background: var(--surface);
+  }
+  .footer-title {
+    font-size: 14px;
+    font-weight: 700;
+    margin-bottom: 4px;
+  }
+  .footer-meta {
+    font-size: 12px;
+    color: var(--text-3);
+  }
+
+  /* 响应式 */
+  @media (max-width: 860px) {
+    .decouple-grid { grid-template-columns: 1fr; gap: 20px; }
+    .decouple-bridge { flex-direction: row; min-height: 50px; }
+    .values-grid { grid-template-columns: repeat(2, 1fr); }
+    .loop-steps { grid-template-columns: repeat(2, 1fr); gap: 32px 0; }
+    .loop-step:nth-child(2)::after, .loop-step:nth-child(4)::after { display: none; }
+    .stats-row { grid-template-columns: 1fr; }
+  }
+  @media (max-width: 520px) {
+    .wrap { padding: 0 20px; }
+    .values-grid { grid-template-columns: 1fr; }
+    .hero { padding: 48px 0 36px; }
+    section { padding: 44px 0; }
+  }
+</style>
+</head>
+<body>
+
+<!-- 顶部栏 -->
+<div class="topbar">
+  <div class="wrap topbar-inner">
+    <div class="brand">
+      <div class="brand-logo">P</div>
+      PCB AFM 自动化仿真系统
+    </div>
+    <div class="topbar-meta">设计方案介绍 · Car.Lin</div>
+  </div>
+</div>
+
+<!-- Hero -->
+<div class="wrap">
+  <div class="hero">
+    <div class="hero-tag">系统设计方案 V1.1</div>
+    <h1>让电机仿真从<span class="accent">人工调参</span><br>走向<span class="accent">知识驱动的自动化迭代</span></h1>
+    <p class="hero-sub">
+      双系统解耦架构 — AI 负责思考出方案,程序负责执行跑仿真。<br>
+      每一次运行都在沉淀经验,系统越用越聪明。
+    </p>
+    <div class="hero-keywords">
+      <span class="kw">🧠 思考与执行解耦</span>
+      <span class="kw">🤖 AI 在环</span>
+      <span class="kw">🔄 系统自动迭代</span>
+      <span class="kw">⚡ 提高仿真效率</span>
+      <span class="kw">💾 节省算力</span>
+      <span class="kw">🔒 内网隔离</span>
+    </div>
+  </div>
+</div>
+
+<!-- 解耦核心理念 -->
+<section class="decouple">
+  <div class="wrap">
+    <div class="sec-head">
+      <div class="sec-label">Core Concept</div>
+      <h2 class="sec-title">思考与执行解耦</h2>
+      <p class="sec-desc">出方案、分析结果、优化策略这些脑力活动交给 AI;跑仿真、记数据、处理错误这些重复流程交给程序</p>
+    </div>
+
+    <div class="decouple-grid">
+      <!-- 思考层 -->
+      <div class="decouple-side think">
+        <div class="decouple-icon">🧠</div>
+        <div class="decouple-title">思考层 — AI 方案系统</div>
+        <div class="decouple-sub">网页端 · 需要联网/AI · 企业内网</div>
+        <ul class="decouple-list">
+          <li>根据边界条件生成仿真方案(参数范围、扫描顺序、工况)</li>
+          <li>分析仿真结果,计算参数敏感性,构建代理模型</li>
+          <li>多目标优化,搜索 Pareto 最优解</li>
+          <li>从结果和日志中提取经验,自动迭代知识库</li>
+          <li>推荐下一轮仿真方案(人工确认后下发)</li>
+        </ul>
+      </div>
+
+      <!-- 中间桥 -->
+      <div class="decouple-bridge">
+        <div class="bridge-arrow">
+          <svg viewBox="0 0 48 20" fill="none">
+            <path d="M0 10 L36 10" stroke="#ea580c" stroke-width="2" stroke-dasharray="3 2"/>
+            <path d="M30 4 L42 10 L30 16" stroke="#ea580c" stroke-width="2" fill="none"/>
+          </svg>
+          <span class="bridge-label">方案 JSON</span>
+        </div>
+        <div class="bridge-arrow">
+          <svg viewBox="0 0 48 20" fill="none">
+            <path d="M48 10 L12 10" stroke="#16a34a" stroke-width="2" stroke-dasharray="3 2"/>
+            <path d="M18 4 L6 10 L18 16" stroke="#16a34a" stroke-width="2" fill="none"/>
+          </svg>
+          <span class="bridge-label" style="color:var(--green);background:var(--green-light)">结果 CSV</span>
+        </div>
+      </div>
+
+      <!-- 执行层 -->
+      <div class="decouple-side do">
+        <div class="decouple-icon">⚙️</div>
+        <div class="decouple-title">执行层 — 仿真执行程序</div>
+        <div class="decouple-sub">本地 EXE · 无需联网 · 无需 AI</div>
+        <ul class="decouple-list">
+          <li>读取方案 JSON,解析参数扫描计划</li>
+          <li>自动驱动 Motor-CAD / Maxwell 等工具逐次仿真</li>
+          <li>断点续跑,崩溃自动重试,错误恢复</li>
+          <li>实时记录每次迭代的输入参数和输出结果</li>
+          <li>输出汇总数据表、仿真日志、错误信息</li>
+        </ul>
+      </div>
+    </div>
+
+    <div class="decouple-bottom">
+      两层通过 <strong>标准化 JSON / CSV 接口</strong>通信,可独立迭代、独立升级 —
+      换 AI 模型不影响执行程序,更新仿真适配器不影响方案系统。
+    </div>
+  </div>
+</section>
+
+<!-- 六大价值 -->
+<section>
+  <div class="wrap">
+    <div class="sec-head">
+      <div class="sec-label">Key Values</div>
+      <h2 class="sec-title">六大核心价值</h2>
+      <p class="sec-desc">每个设计决策都针对电机仿真的实际痛点</p>
+    </div>
+
+    <div class="values-grid">
+      <div class="value-card">
+        <div class="value-num">VALUE 01</div>
+        <div class="value-icon">🧠</div>
+        <div class="value-title">思考与执行解耦</div>
+        <div class="value-desc">AI 专注出方案和分析,程序专注跑仿真和记数据。脑力活动与重复劳动分离,各自最优,互不阻塞。</div>
+      </div>
+
+      <div class="value-card">
+        <div class="value-num">VALUE 02</div>
+        <div class="value-icon">🤖</div>
+        <div class="value-title">AI 在环</div>
+        <div class="value-desc">AI 不是黑盒决策者,而是"检索 + 推理 + 解释"的助手。每个参数建议都附带依据,关键方案人工确认后才执行。</div>
+      </div>
+
+      <div class="value-card">
+        <div class="value-num">VALUE 03</div>
+        <div class="value-icon">🔄</div>
+        <div class="value-title">系统自动迭代</div>
+        <div class="value-desc">每次仿真的方案、结果、日志都自动结构化沉淀。经验库和知识库越用越丰富,系统自我进化,不依赖个人经验传承。</div>
+      </div>
+
+      <div class="value-card">
+        <div class="value-num">VALUE 04</div>
+        <div class="value-icon">⚡</div>
+        <div class="value-title">提高仿真效率</div>
+        <div class="value-desc">五阶段分层优化(Morris 筛选 → LHS 采样 → Kriging 代理模型 → NSGA-II 优化 → FEA 验证),将全因子扫描的数百次压缩到约 110 次/轮。</div>
+      </div>
+
+      <div class="value-card">
+        <div class="value-num">VALUE 05</div>
+        <div class="value-icon">💾</div>
+        <div class="value-title">节省算力</div>
+        <div class="value-desc">代理模型用纯计算替代真实仿真,优化搜索阶段零仿真次数。只对 Pareto 最优解做精确 FEA 验证,算力用在刀刃上。</div>
+      </div>
+
+      <div class="value-card">
+        <div class="value-num">VALUE 06</div>
+        <div class="value-icon">🔒</div>
+        <div class="value-title">内网隔离</div>
+        <div class="value-desc">方案系统部署在企业内网,执行程序本地运行无需联网。初期 AI 经 API-Key 调用(内网代理网关),第二期迁移内网部署大模型,电机设计数据完全不出内网。</div>
+      </div>
+    </div>
+
+    <!-- 数据亮点 -->
+    <div class="stats-row">
+      <div class="stat-card">
+        <div class="stat-num">~110<span class="unit">次</span></div>
+        <div class="stat-label">每轮仿真次数(vs 全因子数百次)</div>
+      </div>
+      <div class="stat-card">
+        <div class="stat-num">5-10<span class="unit">×</span></div>
+        <div class="stat-label">仿真效率提升</div>
+      </div>
+      <div class="stat-card">
+        <div class="stat-num">0<span class="unit">次</span></div>
+        <div class="stat-label">优化搜索阶段真实仿真(代理模型替代)</div>
+      </div>
+    </div>
+  </div>
+</section>
+
+<!-- 闭环流程 -->
+<section class="loop">
+  <div class="wrap">
+    <div class="sec-head">
+      <div class="sec-label">Closed Loop</div>
+      <h2 class="sec-title">自动化迭代闭环</h2>
+      <p class="sec-desc">从边界条件到最优解,每一轮都在让系统变得更聪明</p>
+    </div>
+
+    <div class="loop-steps">
+      <div class="loop-step">
+        <div class="step-circle">1</div>
+        <div class="step-name">输入边界条件</div>
+        <div class="step-desc">拓扑、尺寸、功率、扭矩、效率目标、约束条件</div>
+      </div>
+      <div class="loop-step">
+        <div class="step-circle">2</div>
+        <div class="step-name">AI 生成方案</div>
+        <div class="step-desc">经验检索 + 规则约束 + AI 推理,人工确认后下发</div>
+      </div>
+      <div class="loop-step">
+        <div class="step-circle">3</div>
+        <div class="step-name">程序自动仿真</div>
+        <div class="step-desc">执行程序逐次迭代,断点续跑,记录每次结果</div>
+      </div>
+      <div class="loop-step">
+        <div class="step-circle">4</div>
+        <div class="step-name">分析 + 沉淀 + 迭代</div>
+        <div class="step-desc">结果分析、知识提取、经验入库,生成下一轮方案</div>
+      </div>
+    </div>
+
+    <div class="loop-note">
+      每完成一轮,<span class="highlight">经验库增加新案例</span>、
+      <span class="highlight">知识库沉淀新规律</span>、
+      <span class="highlight">耗时基准更精准</span> — 系统持续自我进化。
+    </div>
+  </div>
+</section>
+
+<!-- 页脚 -->
+<footer>
+  <div class="wrap">
+    <div class="footer-title">PCB 轴向磁通电机自动化仿真系统</div>
+    <div class="footer-meta">设计方案介绍 V1.1 · 作者 Car.Lin · 2026-08-26</div>
+  </div>
+</footer>
+
+</body>
+</html>

+ 144 - 0
README.md

@@ -0,0 +1,144 @@
+# PCB轴向磁通电机自动化仿真系统
+
+> 双系统解耦的轴向磁通电机自动化仿真平台:Web端方案生成 + 本地EXE仿真执行
+
+| 项目 | 内容 |
+|---|---|
+| 文档版本 | V0.1(Phase 1 最小闭环) |
+| 作者 | Car.Lin |
+| 启动日期 | 2026-08-27 |
+| 状态 | Phase 1 开发中 |
+| 设计方案 | [PCB轴向磁通电机自动化仿真系统设计方案介绍.md](PCB轴向磁通电机自动化仿真系统设计方案介绍.md) |
+
+---
+
+## 项目目标
+
+构建一个通用的、可扩展的、双系统解耦的轴向磁通电机自动化仿真平台:
+
+- **系统一(Web端)**:输入边界条件 → 基于经验库+规则+AI生成仿真方案 → 人工确认 → 下发
+- **系统二(本地EXE)**:读取方案 → 驱动Motor-CAD自动仿真 → 输出结果 → 回传
+- **核心闭环**:边界条件 → 方案 → 仿真 → 结果 → 反馈调整 → 经验库积累
+
+### Phase 1 目标(当前)
+
+跑通最小闭环:**本地GUI方案编辑 → Motor-CAD自动化仿真 → 结果输出 → 经验库积累**
+
+- 仿真工具:Motor-CAD 2026R1(电磁仿真)
+- 拓扑:SSSR(单定子单转子)
+- 调试模型:`MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot`
+- 输出指标:平均转矩、转矩脉动(%)、系统效率、总损耗
+- 通信方式:本地文件交换(simulation_plan.json → simulation_results.csv)
+
+---
+
+## 快速开始
+
+### 环境要求
+
+| 项 | 要求 | 验证 |
+|---|---|---|
+| 系统 | Windows 10/11 | — |
+| Motor-CAD | 2026R1 (v261) | `echo %MOTORCAD_ACTIVEX%` |
+| Python | ≥ 3.10 | `python --version` |
+| PyMotorCAD | `pip install ansys-motorcad-core` | `python -c "import ansys.motorcad.core"` |
+| 许可证 | FlexNet `ANSYSLMD_LICENSE_FILE=1055@localhost` | ANSYS License Management Center |
+
+### 安装依赖
+
+```bash
+pip install ansys-motorcad-core pyside6 pandas
+```
+
+### 运行单工况验证
+
+```bash
+python scripts/run_single.py
+```
+
+Motor-CAD会前台弹出,求解约2分钟,结束后输出转矩、效率、损耗等指标。
+
+### 运行参数扫描
+
+```bash
+python scripts/run_scan.py --plan simulation_plan.json
+```
+
+---
+
+## 项目结构
+
+```
+PCB轴向磁通电机自动化仿真系统/
+├── README.md                          # 本文件
+├── AGENTS.md                          # AI工具工作说明(必读)
+├── .gitignore
+├── PCB轴向磁通电机自动化仿真系统设计方案介绍.md  # 完整设计方案V1.1
+├── docs/
+│   ├── KNOWLEDGE_BASE.md              # 核心知识库(方法/参数语义/坑/SOP)
+│   └── CONVERSATION_LOG.md            # 对话与决策记录(带时间戳)
+├── src/
+│   ├── solver_core.py                 # 仿真核心(Motor-CAD连接/参数/求解/结果提取)
+│   ├── scan_engine.py                 # 参数扫描引擎
+│   ├── plan_schema.py                 # 方案JSON Schema定义
+│   ├── experience_db.py               # 经验库(SQLite)
+│   └── gui/
+│       └── main.py                    # PySide6 GUI主程序
+├── scripts/
+│   ├── run_single.py                  # 单工况快速验证
+│   └── run_scan.py                    # 命令行扫描入口
+├── models/
+│   └── MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot  # 调试模型(只读)
+├── output/                            # 仿真输出(不入库)
+├── experience/                        # 经验库数据
+├── axial_mag_pull-master/            # 参考案例1:轴向磁拉力仿真
+├── torqrippswap-master/              # 参考案例2:转矩脉动参数扫描+GUI
+└── 书籍与论文/                         # 理论参考资料
+```
+
+---
+
+## 参考案例
+
+本项目基于两个已验证的参考案例构建:
+
+| 案例 | 路径 | 核心价值 |
+|---|---|---|
+| axial_mag_pull | [axial_mag_pull-master/](axial_mag_pull-master/) | 单一目标微缩闭环验证、三判据校验、AFM轴向力数据口 |
+| torqrippswap | [torqrippswap-master/](torqrippswap-master/) | 参数扫描引擎、PyQt5 GUI+EXE打包、工程化规范 |
+
+## 理论参考
+
+轴向磁通电机基础理论知识参见 [书籍与论文/](书籍与论文/) 目录:
+
+- 《轴向磁通永磁无刷电机(原书第2版)》Jacek F. Gieras — 国际权威,主要参考
+- 《轴向磁场无刷同步电机理论与设计》邓秋玲 — 国内工程实践,补充参考
+
+当方案生成、参数初值估算、物理约束规则等需要理论支撑时,优先查阅上述书籍。
+
+---
+
+## 项目纪律(硬性)
+
+1. **每次运行仿真前 git commit**(脚本改动先入库再跑)
+2. **结果与报告带时间戳+简要说明并提交**;报告版本化不覆盖
+3. **Motor-CAD前台运行**,跑完保持打开供人工检查
+4. **生成物不入库**(output/、*.log、build/、dist/)
+5. **原始.mot只读**,一切修改在时间戳副本上进行
+6. **参数写入后必须回读校验**,不一致标记FAILED
+7. **每个扫描点重新加载基线模型**,防止参数污染
+8. **对话与决策带时间戳记入 docs/CONVERSATION_LOG.md**
+
+---
+
+## 阶段路线图
+
+| 阶段 | 目标 | 状态 |
+|---|---|---|
+| **M1** | 环境验证 + 单工况仿真脚本 | 进行中 |
+| **M2** | 参数扫描引擎(单参数/多参数) | 待开始 |
+| **M3** | 方案JSON接口 + 本地GUI(方案编辑+执行监控) | 待开始 |
+| **M4** | 经验库雏形 + 反馈闭环 | 待开始 |
+| Phase 2 | Web端方案系统基础框架 | 规划中 |
+| Phase 3 | 算法增强(Morris/LHS/Kriging/NSGA-II)+ DRSS拓扑 | 规划中 |
+| Phase 4 | 多物理场(热/结构)+ 多工具扩展 | 规划中 |

+ 12 - 0
axial_mag_pull-master/axial_mag_pull/.gitignore

@@ -0,0 +1,12 @@
+# 生成物不入库 (沿用 maxcalculator 项目原则)
+output_motorcad/
+*.log
+__pycache__/
+*.pyc
+# Motor-CAD 求解临时/结果目录 (与 .mot 同名文件夹)
+MARS-*/
+# GUI 打包中间文件 (exe 在 dist/ 下, 按需保留; 不入库)
+gui_app/build/
+gui_app/dist/
+gui_app/*.spec
+gui_app/__pycache__/

+ 41 - 0
axial_mag_pull-master/axial_mag_pull/AGENTS.md

@@ -0,0 +1,41 @@
+# AGENTS.md — AI 工具工作说明 (Claude Code / Codex / Cursor 等通用)
+
+本仓库: Motor-CAD 轴向磁通电机 (AFM) 轴向磁拉力仿真项目。
+接到任何任务前, **先读 [docs/KNOWLEDGE_BASE.md](docs/KNOWLEDGE_BASE.md)**
+—— 它包含环境事实、唯一可行的数据口 (3D lumped 力图, Fr=轴向力)、.mot
+参数语义陷阱 (Magnet_Length 才是磁钢厚度、RMSCurrent 才是电流入口、磁钢
+默认 100°C)、新模型 SOP 与全部已踩的坑。不读它会重复 5 轮探测的弯路。
+
+## 快速执行: 对新 .mot 模型跑轴向磁拉力
+
+```bash
+pip install ansys-motorcad-core     # 前置: Windows + Motor-CAD 2026R1
+# 1) 新模型入库并提交 (纪律: 运行前必须 git commit)
+# 2) 改 axial_force_final.py 顶部 MOT_SRC 与 SEC_RADII_MM (见 KB §5)
+python -X utf8 axial_force_final.py  # 前台弹 Motor-CAD, ~2分钟, --quit 自动关
+# 3) 三判据 (作用反作用/转矩交叉/解析量级) 全过才采信 → 记录+提交
+```
+
+## 硬性纪律 (违者返工)
+
+1. 每次运行仿真前 git commit。
+2. 结果与报告带**时间戳 + 简要说明**并 git 提交; 报告文件名带版本号
+   (V1/V2...), 出新版**保留旧版不覆盖**。
+3. Motor-CAD **前台**运行, 跑完保持打开供人工检查。
+4. 生成物 (output_motorcad/, *.log) 不入库, 关键数值转录进入库文档。
+5. 对话/决策带时间戳记入 docs/CONVERSATION_LOG.md; 原始 .mot 只读。
+
+## 文件地图
+
+| 文件 | 作用 |
+|---|---|
+| docs/KNOWLEDGE_BASE.md | **核心知识库** (方法/坑/SOP/成果, 先读) |
+| axial_force_final.py | 正式计算脚本 (唯一需要运行的) |
+| axial_compare.py | 气隙扫描 + 磁负刚度 |
+| axial_probe*.py, axial_force_run.py | 探测历史存档, 勿运行 |
+| REPRODUCE.md | 人类复现指南; docs/GUI_GUIDE.md 手动 GUI 操作 |
+| RESULTS.md / docs/COMPARISON_V3.md | 已有结果 / 与解析报告 V3.0 的对照 |
+| docs/report_axialforce_V*.html/.pdf | 版本化报告 (V2 最新) |
+| docs/CONVERSATION_LOG.md | 全过程时间戳记录 |
+| MARS-12S10P_*.mot | 原始模型 (只读) |
+| 轴向磁通电机…V3.0-20260826.pdf | 对标的解析法报告 |

+ 9 - 0
axial_mag_pull-master/axial_mag_pull/CLAUDE.md

@@ -0,0 +1,9 @@
+# CLAUDE.md
+
+每个新 session 先读 [AGENTS.md](AGENTS.md) 与
+[docs/KNOWLEDGE_BASE.md](docs/KNOWLEDGE_BASE.md),再动手。
+
+要点速记 (详见知识库):
+- AFM 轴向力唯一数据口: `get_magnetic_3d_graph_point("Fr_{Rotor|Stator}_{OL|OC}_Lumped", 切片, 节点, 时间步)`; 求解前开 `ElectromagneticForcesCalc_Load/OC`。
+- .mot 陷阱: `Magnet_Length`=磁钢轴向厚度 (`Magnet_Thickness` 是径向深度); 电流走 `RMSCurrent`; 磁钢默认 100°C。
+- 纪律: 运行前 git commit; 结果/报告带时间戳+说明并提交; 报告版本化不覆盖; Motor-CAD 前台; 生成物不入库; 对话记入 docs/CONVERSATION_LOG.md。

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 9080 - 0
axial_mag_pull-master/axial_mag_pull/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 9081 - 0
axial_mag_pull-master/axial_mag_pull/MARS-12S10P_SSSR_D76-C150_V5.0-0819local.mot


+ 37 - 0
axial_mag_pull-master/axial_mag_pull/PLAN.md

@@ -0,0 +1,37 @@
+# 轴向磁拉力仿真方案 (Motor-CAD)
+
+## 背景
+- 模型: `MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot` — Axial Flux BPM (YASA 模板),
+  12槽10极, **单定子单转子 (SSSR)**, Airgap=1mm, 磁体厚 13mm, 5000rpm, 峰值电流 29.7A。
+- SSSR 拓扑定转子间存在固有的不平衡轴向磁拉力 (双转子结构才对消), 该力决定轴承
+  选型与结构刚度, 是本次仿真目标。
+
+## 方法
+用系统 Python 3.12 + PyMotorCAD 驱动 Motor-CAD **前台**运行 (参照
+`pss\maxcalculator\motorcad_export.py` 的做法: 候选变量名逐个尝试、结果如实入 JSON)。
+
+脚本 `axial_force_run.py`, 步骤:
+1. 载入原 .mot, 立即另存带时间戳副本到 `output_motorcad/` (不污染原模型)。
+2. 探测并确认 E-Mag 计算设置: TorqueCalculation 已开; 探测轴向力相关开关
+   (候选: ElectromagneticForcesCalc_Load / ForceCalculation / AxialForceCalculation)。
+3. 两个工况分别 `do_magnetic_calculation()`:
+   - **空载** (电流=0): 磁钢对定子铁芯的静态轴向吸力, SSSR 主要成分;
+   - **负载** (模型自带 29.7A 峰值): 看电枢反应对轴向力的影响。
+4. 读取轴向力: 输出变量候选 [AxialForce, ForceAxial, NetAxialForce, Axial_Force,
+   AFMAxialForce]; 波形用 `get_magnetic_graph` 系列读 (均值 + 峰峰值纹波)。
+   若候选全失败 → 在前台 GUI 的 Output Data 人工确认变量名后回填脚本。
+5. **交叉校核**: 解析估算 F ≈ B_g²·A_gap/(2μ₀), B_g 取 Motor-CAD 气隙磁密结果;
+   与 FEA 值量级对比, 偏差 >2x 标记 uncertain 并排查口径。
+6. 输出: `output_motorcad/results_<时间戳>.json` + 波形 CSV; 结论写入对话记录。
+
+## 已知风险
+- Motor-CAD 对 AFM 的轴向力输出变量名未实测, 步骤 4 是探测式的; 若该版本根本不输出
+  轴向力, 备选: (a) 导出气隙磁密自行做 Maxwell 应力张量积分; (b) 转 Maxwell 3D。
+  先探测再定, 不预设。
+- .mot 中 NumAxialSlices=1 — AFM 用多切片 2.5D FEA, 切片数影响径向分辨率与力的
+  精度, 首跑后在 GUI 核对切片设置, 必要时加密复跑对比。
+
+## 纪律
+- 每次运行测试前 `git commit`; 生成物 (output_motorcad/, *.log) 不入库。
+- Motor-CAD 前台运行, 跑完保持打开供人工检查 (显式 `--quit` 才关闭)。
+- 对话与结论带时间戳记入 `docs/CONVERSATION_LOG.md`。

+ 45 - 0
axial_mag_pull-master/axial_mag_pull/README.md

@@ -0,0 +1,45 @@
+# MARS 轴向磁通电机 轴向磁拉力仿真项目
+
+用 Motor-CAD 2026R1 (PyMotorCAD 前台驱动) 对单定子单转子 (SSSR) 轴向磁通
+电机计算轴向磁拉力, 并与解析法报告对标。全过程 git 管理、带时间戳记录。
+
+## 核心结果 (模型 MARS-12S10P, 12槽10极, 气隙 1mm, 21A RMS)
+
+| 工况 | 转子净轴向力 (指向定子) |
+|---|---|
+| 磁钢 100°C 热态, 空载/负载 | 342.9 / 343.2 N (电流影响 +0.1%, 纹波 ~1%) |
+| 磁钢 20°C 冷态, 空载 | 416.5 N |
+| 气隙 0.6 / 1.5 mm (20°C) | 513.1 / 326.7 N (磁负刚度 ~207 N/mm) |
+
+与解析报告 V3.0 对照: 同温度基准下 FEA/解析 = 0.85~0.86, **对得上**;
+轴承结论链 (708AC 不满足 → 推荐 7004AC) 双方互证。
+
+## 快速开始
+
+- **同事复现本结果**: 读 [REPRODUCE.md](REPRODUCE.md) — 一条命令 ~2 分钟。
+- **用 AI 工具跑新模型**: 把仓库交给 AI (Claude Code / Codex / Cursor 等),
+  它会读 [AGENTS.md](AGENTS.md) → [docs/KNOWLEDGE_BASE.md](docs/KNOWLEDGE_BASE.md)
+  (方法、坑、SOP 全在里面), 然后按 SOP 改 `axial_force_final.py` 执行。
+- **手动 GUI 操作**: [docs/GUI_GUIDE.md](docs/GUI_GUIDE.md)。
+
+## 文档索引
+
+| 文档 | 内容 |
+|---|---|
+| [docs/KNOWLEDGE_BASE.md](docs/KNOWLEDGE_BASE.md) | 知识库: 方法/参数语义/探测技术/SOP/纪律 |
+| [RESULTS.md](RESULTS.md) | 结果与方法要点 |
+| [docs/COMPARISON_V3.md](docs/COMPARISON_V3.md) | 与解析报告 V3.0 的逐项对照 |
+| [docs/report_axialforce_V2.html/.pdf](docs/report_axialforce_V2.pdf) | 图文报告 (V2 最新, V1 保留) |
+| [docs/CONVERSATION_LOG.md](docs/CONVERSATION_LOG.md) | 全过程带时间戳对话/决策记录 |
+| [docs/GUI_GUIDE.md](docs/GUI_GUIDE.md) | Motor-CAD 手动操作 7 步指南 |
+| [REPRODUCE.md](REPRODUCE.md) | 复现指南 (环境/运行/判定) |
+
+## 脚本
+
+- `axial_force_final.py` — 正式计算 (唯一需运行); `axial_compare.py` — 气隙扫描
+- `axial_probe*.py` / `axial_force_run.py` — 5 轮探测历史存档, 勿运行
+
+## 纪律 (详见 AGENTS.md)
+
+运行前 git commit · 结果/报告带时间戳+说明并提交 · 报告版本化不覆盖 ·
+Motor-CAD 前台 · 生成物不入库 · 原始 .mot 只读。

+ 74 - 0
axial_mag_pull-master/axial_mag_pull/REPRODUCE.md

@@ -0,0 +1,74 @@
+# 复现指南 — MARS SSSR 轴向磁拉力仿真
+
+交给同事跑之前请通读一遍;整个流程一条命令,约 2 分钟,原始 .mot 不会被修改。
+
+## 1. 环境要求
+
+| 项 | 要求 | 验证方法 |
+|---|---|---|
+| 系统 | Windows | — |
+| Motor-CAD | 2026R1 (v261),安装器会设 `MOTORCAD_ACTIVEX` 环境变量 | `echo %MOTORCAD_ACTIVEX%` 应指向 activex.bat |
+| Python | ≥ 3.10 | `python --version` |
+| PyMotorCAD | `pip install ansys-motorcad-core` | `python -c "import ansys.motorcad.core"` |
+
+## 2. 获取项目
+
+复制整个 `motionpushpull` 目录(含 `.git`,git 历史里有全部踩坑记录)。
+关键文件:
+
+- `MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot` — 原始模型(只读使用)
+- `axial_force_final.py` — **唯一需要运行的脚本**
+- `axial_probe*.py` / `axial_force_run.py` — 探测数据口的历史过程,无需运行
+- `RESULTS.md` — 上次运行的结果与方法要点
+- `docs/CONVERSATION_LOG.md` — 完整过程记录
+
+## 3. 运行
+
+```bash
+python -X utf8 axial_force_final.py
+```
+
+- Motor-CAD 会**前台弹出**(保持前台是本项目约定),求解约 88 s;
+- 结束后 Motor-CAD 保持打开供人工检查;要自动关闭加 `--quit`;
+- 按项目纪律:改动脚本后、运行前先 `git commit`。
+
+## 4. 读结果
+
+控制台末尾两行 `[结论]`,例如:
+
+```text
+[结论] 负载(RMS 21A) 转子净轴向力: 均值 343.2 N, 纹波峰峰 3.87 N
+[结论] 空载 转子净轴向力: 均值 342.9 N, 纹波峰峰 3.53 N
+```
+
+文件输出(`output_motorcad/`,git 不入库):
+
+- `axialforce_final_<时间戳>.json` — 全部波形 + 校核数据
+- `axial_force_<时间戳>.csv` — 净轴向力时间序列(31 步/电周期)
+- `MARS_SSSR_axialF_<时间戳>.mot` — 求解工作副本
+
+## 5. 结果判定(三项都过才采信)
+
+1. **作用–反作用**:`checks.action_reaction_*` 中 `imbalance_pct` < 5%;
+2. **转矩交叉**:`sum_Ft_x_r_Nm_t0` 与 `torque_graph_mean_Nm` 偏差 < 10%;
+3. **解析量级**:`analytic.F_est_N` 与 FEA 值同量级(解析式系统性偏大约 1.4×属正常)。
+
+## 6. 已知坑(都踩过,勿重复)
+
+- **电流是 RMS 口径**(`CurrentDefinition=1`):改电流设 `RMSCurrent`,
+  改 `PeakCurrent` 无效且不报错。
+- 轴向力**没有输出变量/2D 图/文档**,唯一入口是 3D 集中节点力图
+  `Fr_{Rotor|Stator}_{OL|OC}_Lumped`(AFM 2.5D 展开模型中 Fr=轴向力);
+  脚本已封装,无需手动处理。
+- 求解前必须打开 `ElectromagneticForcesCalc_Load/OC`(脚本已做)。
+- 节点首尾(0°/360°)重复,求和须去重(脚本已做)。
+- ~~Motor-CAD 残留锁文件问题~~:Motor-CAD 无此问题(那是 AEDT 的坑);
+  python 退出后 Motor-CAD 进程可能随之退出,属正常。
+
+## 7. 换其它模型
+
+- 改脚本顶部 `MOT_SRC` 指向新 .mot;
+- 若径向切片数量/半径不同:在 .mot 里搜 `AFM_SectionCentreRadius_Array`,
+  更新脚本的 `SEC_RADII_MM`(转矩交叉校核用);
+- 节点数会自动探测(转子=极数、定子=槽数),无需改;
+- 首次跑新模型建议核对:转子/定子合力是否反号、转矩交叉是否对得上。

+ 72 - 0
axial_mag_pull-master/axial_mag_pull/RESULTS.md

@@ -0,0 +1,72 @@
+# MARS-12S10P SSSR 轴向磁拉力仿真结果
+
+**模型**: `MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot` (Axial Flux BPM, 12槽10极
+单定子单转子, 气隙 1mm, 磁钢轴向厚 3mm/径向深 13mm/极弧 67.2%, 5000rpm,
+RMS 相电流 21A; 注意 .mot 中 Magnet_Length=3 才是轴向厚度,
+Magnet_Thickness=13 是径向深度, 磁钢温度默认 100°C)
+**工具**: Motor-CAD 2026R1 (v261) E-Magnetic, 2.5D 多切片 FEA (2 个径向切片,
+截面中心半径 28.25 / 34.75 mm), PyMotorCAD 前台驱动
+**运行**: 2026-08-25 21:25 (commit deb6b65, results/axialforce_final_0825_212518)
+
+## 结论
+
+| 工况 | 转子净轴向力 (均值) | 纹波 (峰峰) |
+|---|---|---|
+| 空载 (开路) | **342.9 N** | 3.5 N (~1.0%) |
+| 负载 (RMS 21A, 相位角 0°) | **343.2 N** | 3.9 N (~1.1%) |
+
+- 方向: 转子被**吸向定子** (SSSR 单边结构的固有不平衡磁拉力)。
+- 负载电流对轴向力几乎无影响 (+0.35 N, +0.1%): 该力由磁钢-铁芯吸力主导,
+  q 轴电流主要产生切向力。轴承选型按 **~345 N 静态轴向预载** 考虑即可,
+  纹波仅 ~4 N (12/10 齿槽配合, 波动频率高但幅值小)。
+
+## 校核 (全部通过)
+
+1. **作用-反作用**: 定子净轴向力 -337.4 N vs 转子 +343.2 N, 偏差 1.7%
+   (定转子节点离散不同: 10 vs 12 节点, 数值误差范围内)。
+2. **转矩交叉核对**: Σ(Ft×r) = 0.505 Nm vs 电磁转矩图均值 0.522 Nm (3%)。
+3. **解析量级**: F ≈ A·mean(B²)/(2μ0) = 485 N, 与 FEA 343 N 同量级
+   (解析式按全环面积+气隙 B² 均值, 系统性偏大, 比值 0.71 合理)。
+   等效气隙磁压 343N/25.7cm² ≈ 133 kPa → B_eff ≈ 0.58 T, 物理自洽。
+
+## 数据文件 (output_motorcad/, 不入库)
+
+- `axialforce_final_0825_212518.json` — 全部波形+校核数据
+- `axial_force_0825_212518.csv` — 净轴向力时间序列 (31 时间步/电周期)
+- `MARS_SSSR_axialF_0825_212518.mot` — 求解工作副本 (原模型未动)
+
+## 复现运行 (2026-08-26 13:03, 第二台机器, commit b448fc6)
+
+另一台机器按 REPRODUCE.md 原样重跑 (Motor-CAD 2026R1 @ D: 盘, 自带 Python
+pymotorcad 0.8.4, 求解 150.9 s; 数据 `axialforce_final_0826_130316.json`):
+
+| 工况 | 转子净轴向力 (均值) | 纹波 (峰峰) | 对比 08-25 定版 |
+|---|---|---|---|
+| 空载 (开路) | 342.9 N | 3.53 N | 一致 |
+| 负载 (RMS 21A) | 343.2 N | 3.87 N | 一致 |
+
+三判据全过 (作用-反作用 1.70%/1.69%, 转矩交叉 3.2%, 解析 485 N 同量级)。
+**跨机复现成功, 数值与定版完全一致。**
+
+复现机新增环境要点 (详见 KNOWLEDGE_BASE.md §1):
+- AI 工具 shell 不继承机器级环境变量, 需 inline export `MOTORCAD_ACTIVEX`
+  (=%APPDATA%\Ansys\v261\motorcad\activex.bat) 与
+  `ANSYSLMD_LICENSE_FILE` (=1055@localhost);
+- ansyslmd vendor daemon 未运行时 Motor-CAD ~30s 静默退出, 表现为
+  psutil.NoSuchProcess, 需先在 ANSYS License Management Center 启动许可;
+- pymotorcad 0.8.4 无 MOTORCAD_ACTIVEX 时直接报错, 脚本已加
+  `set_motorcad_exe()` 回退。
+
+## 方法要点 (5 轮探测得出, 供复用)
+
+- Motor-CAD 对 AFM 的轴向力**无输出变量、无 2D 图、无文档** (chm 全文无
+  "Axial Force"); 数据在 **3D lumped 力图**:
+  `get_magnetic_3d_graph_point("Fr_{Rotor|Stator}_{OL|OC}_Lumped", 切片, 节点, 时间步)`
+- AFM 2.5D 展开模型沿用径向机命名: **Fr(法向) 即轴向力**, Ft 为切向力。
+- 需先开 `ElectromagneticForcesCalc_Load/OC` 再求解; 一次求解 OC/OL 全出。
+- 节点: 转子 10 (36°步), 定子 12 (30°步), 首尾 (0°/360°) 重复需去重;
+  单位 N/节点/切片, 对节点+切片求和得净力。
+- 该模型 `CurrentDefinition=1` (RMS 口径): 改电流要设 `RMSCurrent`,
+  改 `PeakCurrent` 无效 (首跑踩坑)。
+- 图名探测法: `get_magnetic_graph_point(名, 0)` 报错文案区分
+  "Graph name does not exist" (不存在) vs "No points exist" (存在未求解)。

+ 158 - 0
axial_mag_pull-master/axial_mag_pull/axial_compare.py

@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+"""
+对标 V3.0 解析报告的 FEA 验证扫描
+==================================
+参照《轴向磁通电机轴向磁拉力计算与轴承选型校核报告V3.0-20260826.pdf》:
+  - 其基准: 磁钢 20°C, 空载 Fz=483 N (中值口径 500 N)
+  - 其表3-3: g=0.6/1.0/1.5 mm -> 601/483/378 N, kneg(1mm)=250 N/mm
+本模型 .mot 磁钢温度默认 100°C (Br -0.12%/K), 首先归一到 20°C 再扫气隙。
+
+工况: 磁钢 20°C x 气隙 {0.6, 1.0, 1.5} mm, 各求解一次, 读 OC/OL 净轴向力
+及气隙磁密; 有限差分求磁负刚度 kneg。
+
+用法: python axial_compare.py [--quit]
+"""
+import json
+import os
+import sys
+import time
+
+BASE = os.path.dirname(os.path.abspath(__file__))
+MOT_SRC = os.path.join(BASE, "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot")
+OUT_DIR = os.path.join(BASE, "output_motorcad")
+
+GAPS = [0.6, 1.0, 1.5]
+MAGNET_TEMP_C = 20.0
+MAX_TSTEPS = 64
+MAX_NODES = 40
+
+
+def stats(ys):
+    if not ys:
+        return None
+    return {"mean": sum(ys) / len(ys), "min": min(ys), "max": max(ys),
+            "pk2pk": max(ys) - min(ys), "n": len(ys)}
+
+
+def read_nodes(mc, graph, sec, tstep):
+    xs, ys = [], []
+    for i in range(MAX_NODES):
+        try:
+            x, y = mc.get_magnetic_3d_graph_point(graph, sec, i, tstep)
+        except Exception:
+            break
+        xs.append(x)
+        ys.append(y)
+    return xs, ys
+
+
+def net_force_series(mc, graph):
+    series = []
+    for tstep in range(MAX_TSTEPS):
+        total, got = 0.0, False
+        for sec in (1, 2):
+            xs, ys = read_nodes(mc, graph, sec, tstep)
+            if not ys:
+                continue
+            got = True
+            nu = len(ys) - 1 if (len(xs) > 1 and
+                                 abs(xs[-1] - xs[0] - 360.0) < 1e-6) else len(ys)
+            total += sum(ys[:nu])
+        if not got:
+            break
+        series.append(total)
+    return series
+
+
+def read_2d(mc, graph, maxpts=64):
+    ys = []
+    for i in range(maxpts):
+        try:
+            _, y = mc.get_magnetic_graph_point(graph, i)
+        except Exception:
+            break
+        ys.append(y)
+    return ys
+
+
+def main(argv):
+    quit_after = "--quit" in argv
+    os.makedirs(OUT_DIR, exist_ok=True)
+    ts = time.strftime("%m%d_%H%M%S")
+
+    from ansys.motorcad.core import MotorCAD
+    print("启动 Motor-CAD (前台) ...")
+    mc = MotorCAD()
+    try:
+        mc.set_visible(True)   # /SCRIPTING 模式部分机器窗口不显示, 强制可见
+    except Exception:
+        pass
+    results = {"when": ts, "magnet_temp_C": MAGNET_TEMP_C,
+               "reference": "V3.0 报告表3-1/3-3: 20°C, g=0.6/1.0/1.5 -> "
+                            "601/483/378 N, kneg(1mm)=250 N/mm",
+               "cases": []}
+    try:
+        mc.load_from_file(MOT_SRC)
+        out_mot = os.path.join(OUT_DIR, "MARS_SSSR_compare_%s.mot" % ts)
+        mc.save_to_file(out_mot)
+        results["work_mot"] = out_mot
+
+        t_before = mc.get_variable("Magnet_Temperature")
+        results["magnet_temp_before_C"] = t_before
+        mc.set_variable("Magnet_Temperature", MAGNET_TEMP_C)
+        print("磁钢温度: %s -> %s °C" % (t_before, MAGNET_TEMP_C))
+        for var in ["ElectromagneticForcesCalc_Load",
+                    "ElectromagneticForcesCalc_OC"]:
+            mc.set_variable(var, True)
+
+        for g in GAPS:
+            mc.set_variable("Airgap", g)
+            back = mc.get_variable("Airgap")
+            print("== 气隙 %.1f mm (回读 %s), 求解 ..." % (g, back))
+            t0 = time.time()
+            mc.do_magnetic_calculation()
+            dt = time.time() - t0
+            case = {"airgap_mm": back, "solve_seconds": dt}
+            for graph, key in [("Fr_Rotor_OC_Lumped", "F_OC"),
+                               ("Fr_Rotor_OL_Lumped", "F_OL")]:
+                s = net_force_series(mc, graph)
+                case[key] = stats(s)
+            bys = read_2d(mc, "FluxDensityAirgap")
+            if bys:
+                case["B2_mean_T2"] = sum(b * b for b in bys) / len(bys)
+            print("   F_OC=%.1f N, F_OL=%.1f N, mean(B²)=%.3f (耗时 %.0fs)"
+                  % (case["F_OC"]["mean"], case["F_OL"]["mean"],
+                     case.get("B2_mean_T2", -1), dt))
+            results["cases"].append(case)
+
+        # ---- 磁负刚度 (有限差分, OC 口径) ----
+        cs = results["cases"]
+        if len(cs) == 3:
+            f = [c["F_OC"]["mean"] for c in cs]
+            g0, g1, g2 = [c["airgap_mm"] for c in cs]
+            k_low = -(f[1] - f[0]) / (g1 - g0)     # 0.6~1.0 段
+            k_high = -(f[2] - f[1]) / (g2 - g1)    # 1.0~1.5 段
+            k_mid = -(f[2] - f[0]) / (g2 - g0)     # 全段中心差分
+            results["kneg_N_per_mm"] = {"seg_0.6_1.0": k_low,
+                                        "seg_1.0_1.5": k_high,
+                                        "central_at_1.0": k_mid}
+            print("kneg: 0.6~1.0段 %.0f, 1.0~1.5段 %.0f, 中心差分 %.0f N/mm "
+                  "(报告解析值 250)" % (k_low, k_high, k_mid))
+
+        res_path = os.path.join(OUT_DIR, "compare_results_%s.json" % ts)
+        with open(res_path, "w", encoding="utf-8") as fjson:
+            json.dump(results, fjson, ensure_ascii=False, indent=2)
+        print("RESULTS: %s" % res_path)
+        return 0
+    finally:
+        if quit_after:
+            try:
+                mc.quit()
+            except Exception:
+                pass
+        else:
+            print("[提示] Motor-CAD 保持前台打开供检查。")
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))

+ 241 - 0
axial_mag_pull-master/axial_mag_pull/axial_force_final.py

@@ -0,0 +1,241 @@
+# -*- coding: utf-8 -*-
+"""
+MARS SSSR 轴向磁拉力 — 正式计算
+=================================
+第5轮探测确认: AFM 力数据在 3D lumped 力图, 命名沿用径向机惯例, 其 "Fr"
+(法向力) 在 AFM 2.5D 展开模型中即轴向力:
+  Fr_Rotor_OL_Lumped / Fr_Rotor_OC_Lumped (转子, 负载/空载)
+  Fr_Stator_OL_Lumped / Fr_Stator_OC_Lumped (定子, 反作用)
+节点: 转子 10 (36°步, 首尾重复共11点), 定子 12 (30°步, 共13点); 单位 N。
+两个径向切片 (sec1 r=28.25mm, sec2 r=34.75mm) 分别读, 节点求和+切片求和
+得净轴向力; 按时间步扫描得波形。
+
+校核: (a) 定子合力 ≈ -转子合力; (b) Σ(Ft×r) ≈ 电磁转矩图;
+      (c) 解析 F ≈ A/(2μ0)·mean(B²)。
+
+用法: python axial_force_final.py [--quit]
+"""
+import json
+import math
+import os
+import sys
+import time
+
+BASE = os.path.dirname(os.path.abspath(__file__))
+MOT_SRC = os.path.join(BASE, "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot")
+OUT_DIR = os.path.join(BASE, "output_motorcad")
+
+MU0 = 4e-7 * math.pi
+SEC_RADII_MM = [28.25, 34.75]      # AFM_SectionCentreRadius_Array
+MAX_TSTEPS = 64
+MAX_NODES = 40
+
+
+def stats(ys):
+    if not ys:
+        return None
+    return {"mean": sum(ys) / len(ys), "min": min(ys), "max": max(ys),
+            "pk2pk": max(ys) - min(ys), "n": len(ys)}
+
+
+def read_nodes(mc, graph, sec, tstep):
+    """读某时间步的全部节点 (x=角度, y=力N); 首尾重复点保留由调用方处理。"""
+    xs, ys = [], []
+    for i in range(MAX_NODES):
+        try:
+            x, y = mc.get_magnetic_3d_graph_point(graph, sec, i, tstep)
+        except Exception:
+            break
+        xs.append(x)
+        ys.append(y)
+    return xs, ys
+
+
+def net_force_series(mc, graph):
+    """净力时间序列: 对两切片、去重节点求和; 返回 (series, meta)。"""
+    series = []
+    meta = {"sections": {}}
+    for tstep in range(MAX_TSTEPS):
+        total = 0.0
+        got = False
+        for sec in (1, 2):
+            xs, ys = read_nodes(mc, graph, sec, tstep)
+            if not ys:
+                continue
+            got = True
+            # 首尾重复 (0°与360°同一节点) 则去掉末点
+            n_unique = len(ys) - 1 if (len(xs) > 1 and
+                                       abs(xs[-1] - xs[0] - 360.0) < 1e-6) \
+                else len(ys)
+            total += sum(ys[:n_unique])
+            if tstep == 0:
+                meta["sections"][sec] = {"n_points": len(ys),
+                                         "n_unique": n_unique, "x": xs}
+        if not got:
+            break
+        series.append(total)
+    return series, meta
+
+
+def torque_from_ft(mc, graph):
+    """t=0 时刻 Σ(Ft×r) 粗校核 (Nm)。"""
+    tq = 0.0
+    for sec, r_mm in zip((1, 2), SEC_RADII_MM):
+        xs, ys = read_nodes(mc, graph, sec, 0)
+        if not ys:
+            return None
+        n_unique = len(ys) - 1 if (len(xs) > 1 and
+                                   abs(xs[-1] - xs[0] - 360.0) < 1e-6) \
+            else len(ys)
+        tq += sum(ys[:n_unique]) * (r_mm * 1e-3)
+    return tq
+
+
+def read_2d(mc, graph, maxpts=64):
+    xs, ys = [], []
+    for i in range(maxpts):
+        try:
+            x, y = mc.get_magnetic_graph_point(graph, i)
+        except Exception:
+            break
+        xs.append(x)
+        ys.append(y)
+    return xs, ys
+
+
+def main(argv):
+    quit_after = "--quit" in argv
+    os.makedirs(OUT_DIR, exist_ok=True)
+    ts = time.strftime("%m%d_%H%M%S")
+
+    from ansys.motorcad.core import MotorCAD, set_motorcad_exe
+    # Motor-CAD 2026R1 新装机器可能未注册 MOTORCAD_ACTIVEX (pymotorcad 0.8.x
+    # 仍依赖它); 此时显式定位 exe, 可用环境变量 MOTORCAD_EXE 覆盖路径。
+    if not os.environ.get("MOTORCAD_ACTIVEX"):
+        exe = os.environ.get(
+            "MOTORCAD_EXE",
+            r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe")
+        if os.path.isfile(exe):
+            set_motorcad_exe(exe)
+    print("启动 Motor-CAD (前台) ...")
+    mc = MotorCAD()
+    # /SCRIPTING 模式在部分机器上窗口创建但不显示 (任务栏有图标点不开),
+    # 强制可见; 已可见时无副作用 (2026-08-26 同事复现机实测该问题)
+    try:
+        mc.set_visible(True)
+    except Exception as e:
+        print("  [提示] set_visible 失败 (不影响计算): %s" % e)
+    results = {"when": ts, "source_mot": os.path.basename(MOT_SRC),
+               "convention_note": ("AFM 2.5D 展开模型中 Fr(法向)=轴向力; "
+                                   "OL=负载(RMS 21A), OC=空载开路")}
+    try:
+        mc.load_from_file(MOT_SRC)
+        out_mot = os.path.join(OUT_DIR, "MARS_SSSR_axialF_%s.mot" % ts)
+        mc.save_to_file(out_mot)
+        results["work_mot"] = out_mot
+
+        for key, names in [("RMSCurrent_A", ["RMSCurrent"]),
+                           ("ShaftSpeed_rpm", ["ShaftSpeed"]),
+                           ("Airgap_mm", ["Airgap"]),
+                           ("Stator_Lam_Dia_mm", ["Stator_Lam_Dia"]),
+                           ("Stator_Bore_mm", ["Stator_Bore"])]:
+            try:
+                results[key] = mc.get_variable(names[0])
+            except Exception:
+                results[key] = None
+        for var in ["ElectromagneticForcesCalc_Load",
+                    "ElectromagneticForcesCalc_OC"]:
+            mc.set_variable(var, True)
+
+        print("求解 (负载点 RMS %sA, OC+OL 力同算) ..." % results["RMSCurrent_A"])
+        t0 = time.time()
+        mc.do_magnetic_calculation()
+        results["solve_seconds"] = time.time() - t0
+        print("  耗时 %.1f s" % results["solve_seconds"])
+
+        # ---- 净轴向力: 转子/定子 x OL/OC ----
+        forces = {}
+        for graph in ["Fr_Rotor_OL_Lumped", "Fr_Stator_OL_Lumped",
+                      "Fr_Rotor_OC_Lumped", "Fr_Stator_OC_Lumped"]:
+            series, meta = net_force_series(mc, graph)
+            if series:
+                forces[graph] = {"series_N": series, "stats": stats(series),
+                                 "meta": meta}
+                print("  %s: %s" % (graph, stats(series)))
+            else:
+                forces[graph] = None
+                print("  [如实] %s 无数据" % graph)
+        results["axial_forces"] = forces
+
+        # ---- 校核 a: 定转子合力反号 ----
+        checks = {}
+        for case in ("OL", "OC"):
+            fr = forces.get("Fr_Rotor_%s_Lumped" % case)
+            fs = forces.get("Fr_Stator_%s_Lumped" % case)
+            if fr and fs:
+                mr, ms = fr["stats"]["mean"], fs["stats"]["mean"]
+                checks["action_reaction_%s" % case] = {
+                    "rotor_mean_N": mr, "stator_mean_N": ms,
+                    "imbalance_pct": abs(mr + ms) / max(abs(mr), 1e-9) * 100}
+        # ---- 校核 b: Σ(Ft×r) vs 转矩 ----
+        tq_ft = torque_from_ft(mc, "Ft_Rotor_OL_Lumped")
+        _, tq_graph = None, None
+        txs, tys = read_2d(mc, 17)          # id17 = 总转矩 (第4轮已辨认)
+        tq_graph = stats(tys)["mean"] if tys else None
+        checks["torque_crosscheck"] = {"sum_Ft_x_r_Nm_t0": tq_ft,
+                                       "torque_graph_mean_Nm": tq_graph}
+        # ---- 校核 c: 解析 F ≈ A/(2μ0)·mean(B²), B 取气隙磁密图 ----
+        bxs, bys = read_2d(mc, "FluxDensityAirgap")
+        if bys:
+            b2 = sum(b * b for b in bys) / len(bys)
+            d_out = float(results["Stator_Lam_Dia_mm"]) * 1e-3
+            d_in = float(results["Stator_Bore_mm"]) * 1e-3
+            area = math.pi / 4.0 * (d_out ** 2 - d_in ** 2)
+            checks["analytic"] = {"mean_B2_T2": b2, "area_m2": area,
+                                  "F_est_N": area / (2 * MU0) * b2}
+        results["checks"] = checks
+        print("校核: %s" % json.dumps(checks, ensure_ascii=False, indent=1))
+
+        # ---- CSV 波形 ----
+        csv_path = os.path.join(OUT_DIR, "axial_force_%s.csv" % ts)
+        with open(csv_path, "w", encoding="utf-8") as f:
+            f.write("tstep,Fr_Rotor_OL_N,Fr_Stator_OL_N,"
+                    "Fr_Rotor_OC_N,Fr_Stator_OC_N\n")
+            nmax = max(len(v["series_N"]) if v else 0
+                       for v in forces.values())
+            for i in range(nmax):
+                row = [str(i)]
+                for g in ["Fr_Rotor_OL_Lumped", "Fr_Stator_OL_Lumped",
+                          "Fr_Rotor_OC_Lumped", "Fr_Stator_OC_Lumped"]:
+                    v = forces.get(g)
+                    row.append("%.4f" % v["series_N"][i]
+                               if v and i < len(v["series_N"]) else "")
+                f.write(",".join(row) + "\n")
+        results["csv"] = csv_path
+
+        res_path = os.path.join(OUT_DIR, "axialforce_final_%s.json" % ts)
+        with open(res_path, "w", encoding="utf-8") as f:
+            json.dump(results, f, ensure_ascii=False, indent=2)
+        print("RESULTS: %s" % res_path)
+        print("CSV: %s" % csv_path)
+
+        # ---- 结论摘要 ----
+        for case, label in (("OL", "负载(RMS 21A)"), ("OC", "空载")):
+            v = forces.get("Fr_Rotor_%s_Lumped" % case)
+            if v:
+                s = v["stats"]
+                print("[结论] %s 转子净轴向力: 均值 %.1f N, 纹波峰峰 %.2f N"
+                      % (label, s["mean"], s["pk2pk"]))
+        return 0
+    finally:
+        if quit_after:
+            try:
+                mc.quit()
+            except Exception:
+                pass
+        else:
+            print("[提示] Motor-CAD 保持前台打开供检查。")
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))

+ 221 - 0
axial_mag_pull-master/axial_mag_pull/axial_force_run.py

@@ -0,0 +1,221 @@
+# -*- coding: utf-8 -*-
+"""
+MARS-12S10P SSSR 轴向磁拉力仿真 (Motor-CAD 前台, PyMotorCAD 驱动)
+================================================================
+参照 pss\\maxcalculator\\motorcad_export.py 的做法: 变量名候选列表逐个尝试,
+探测结果如实记入 results JSON (失败也记, 不掩盖)。
+
+流程: 载入原 .mot → 另存时间戳副本 → 开力计算开关 → 空载(I=0)求解 →
+      负载(原电流)求解 → 探测轴向力输出(变量+波形) → 解析交叉校核 → JSON。
+
+用法:
+  python axial_force_run.py [--quit] [--skip-noload]
+    --quit         完成后关闭 Motor-CAD (默认保持前台打开供人工检查)
+    --skip-noload  只跑负载工况
+"""
+import json
+import math
+import os
+import sys
+import time
+
+BASE = os.path.dirname(os.path.abspath(__file__))
+MOT_SRC = os.path.join(BASE, "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot")
+OUT_DIR = os.path.join(BASE, "output_motorcad")
+
+MU0 = 4e-7 * math.pi
+
+# 轴向力输出变量候选 (未实测, 探测式; Units_Force=kN 需留意单位)
+FORCE_VARS = [
+    "AxialForce", "Axial_Force", "ForceAxial", "Force_Axial",
+    "NetAxialForce", "AFMAxialForce", "RotorAxialForce", "Rotor_Axial_Force",
+    "StatorAxialForce", "AxialForceMean", "MeanAxialForce",
+    "AxialForceAverage", "AxialForce_Load", "AxialForce_OC",
+]
+# 轴向力波形图候选 (get_magnetic_graph_point)
+FORCE_GRAPHS = [
+    "AxialForceVsAngle", "AxialForce", "ForceAxial", "Axial Force",
+    "Fz", "ForceZ", "Force (Axial)",
+]
+# 气隙磁密波形候选 (解析校核用)
+BG_GRAPHS = [
+    "AirgapFluxDensity", "Airgap Flux Density", "AirgapFluxDensityOC",
+    "BAirgap", "FluxDensityAirgap",
+]
+
+
+def probe_var(mc, names):
+    """按候选名读变量, 返回 (名, 值); 全失败 (None, None)。"""
+    for n in names:
+        try:
+            return n, mc.get_variable(n)
+        except Exception:
+            continue
+    return None, None
+
+
+def probe_graph(mc, names, npoints):
+    """按候选名读波形 (逐点), 返回 {name, x, y} 或 None。"""
+    for n in names:
+        try:
+            x0, y0 = mc.get_magnetic_graph_point(n, 0)
+        except Exception:
+            continue
+        xs, ys = [x0], [y0]
+        for i in range(1, npoints):
+            try:
+                x, y = mc.get_magnetic_graph_point(n, i)
+            except Exception:
+                break
+            xs.append(x)
+            ys.append(y)
+        return {"name": n, "x": xs, "y": ys}
+    return None
+
+
+def stats(ys):
+    if not ys:
+        return None
+    return {"mean": sum(ys) / len(ys), "min": min(ys), "max": max(ys),
+            "pk2pk": max(ys) - min(ys), "n": len(ys)}
+
+
+def run_case(mc, tag, results, npoints):
+    print("== 工况 [%s]: do_magnetic_calculation ..." % tag)
+    t0 = time.time()
+    mc.do_magnetic_calculation()
+    dt = time.time() - t0
+    print("   求解耗时 %.1f s" % dt)
+    case = {"solve_seconds": dt}
+
+    n, v = probe_var(mc, ["TorqueValueAveragePerCycle", "AverageTorque",
+                          "MeanTorque", "ShaftTorque"])
+    case["avg_torque"] = {"variable": n, "value": v}
+    print("   平均转矩: %s = %s" % (n, v))
+
+    n, v = probe_var(mc, FORCE_VARS)
+    case["axial_force_var"] = {"variable": n, "value": v}
+    print("   轴向力变量: %s = %s" % (n, v))
+
+    g = probe_graph(mc, FORCE_GRAPHS, npoints)
+    if g:
+        case["axial_force_graph"] = {"name": g["name"], "stats": stats(g["y"]),
+                                     "x": g["x"], "y": g["y"]}
+        print("   轴向力波形 [%s]: %s" % (g["name"], stats(g["y"])))
+    else:
+        case["axial_force_graph"] = None
+        print("   [警告] 轴向力波形候选全部失败")
+
+    g = probe_graph(mc, BG_GRAPHS, npoints)
+    if g:
+        ys = g["y"]
+        b2_mean = sum(b * b for b in ys) / len(ys)
+        case["airgap_B_graph"] = {"name": g["name"], "stats": stats(ys),
+                                  "B2_mean": b2_mean}
+        print("   气隙磁密波形 [%s]: %s, mean(B^2)=%.4f"
+              % (g["name"], stats(ys), b2_mean))
+    else:
+        case["airgap_B_graph"] = None
+        print("   [提示] 气隙磁密波形候选失败, 解析校核转 GUI 人工读数")
+
+    results["case_" + tag] = case
+    return case
+
+
+def main(argv):
+    quit_after = "--quit" in argv
+    skip_noload = "--skip-noload" in argv
+    os.makedirs(OUT_DIR, exist_ok=True)
+    ts = time.strftime("%m%d_%H%M%S")
+
+    from ansys.motorcad.core import MotorCAD
+    print("启动 Motor-CAD (前台) ...")
+    mc = MotorCAD()
+    results = {"when": ts, "source_mot": os.path.basename(MOT_SRC)}
+    try:
+        mc.load_from_file(MOT_SRC)
+        out_mot = os.path.join(OUT_DIR, "MARS_SSSR_axialforce_%s.mot" % ts)
+        mc.save_to_file(out_mot)
+        print("工作副本: %s" % out_mot)
+        results["work_mot"] = out_mot
+
+        # ---- 模型关键参数回读 (如实入档) ----
+        params = {}
+        for label, names in [
+                ("Slot_Number", ["Slot_Number"]),
+                ("Pole_Number", ["Pole_Number"]),
+                ("Airgap_mm", ["Airgap"]),
+                ("Magnet_Thickness_mm", ["Magnet_Thickness"]),
+                ("ShaftSpeed_rpm", ["ShaftSpeed", "Shaft_Speed"]),
+                ("PeakCurrent_A", ["PeakCurrent", "Peak_Current"]),
+                ("PhaseAdvance_deg", ["PhaseAdvance", "Phase_Advance"]),
+                ("Stator_Lam_Dia_mm", ["Stator_Lam_Dia"]),
+                ("Stator_Bore_mm", ["Stator_Bore"]),
+                ("AFM_D_Rotor_mm", ["AFM_D_Rotor"]),
+                ("TorquePointsPerCycle", ["TorquePointsPerCycle"]),
+                ("Units_Force", ["Units_Force"]),
+        ]:
+            n, v = probe_var(mc, names)
+            params[label] = v
+            print("  %s: %s = %s" % (label, n, v))
+        results["params"] = params
+        i_load = float(params["PeakCurrent_A"] or 0.0)
+        npoints = int(params["TorquePointsPerCycle"] or 30) + 1
+
+        # ---- 打开电磁力计算开关 ----
+        for var in ["ElectromagneticForcesCalc_Load",
+                    "ElectromagneticForcesCalc_OC"]:
+            try:
+                mc.set_variable(var, True)
+                print("  [OK] %s = True" % var)
+            except Exception as e:
+                print("  [警告] %s 设置失败: %s" % (var, e))
+
+        # ---- 工况 A: 空载 (I=0, 磁钢对定子铁芯的静态轴向吸力) ----
+        if not skip_noload:
+            name_i, _ = probe_var(mc, ["PeakCurrent", "Peak_Current"])
+            mc.set_variable(name_i, 0.0)
+            print("  %s -> 0 (空载)" % name_i)
+            run_case(mc, "noload", results, npoints)
+            mc.set_variable(name_i, i_load)
+            print("  %s 恢复 %.3f A" % (name_i, i_load))
+
+        # ---- 工况 B: 负载 (模型自带电流) ----
+        run_case(mc, "load", results, npoints)
+
+        # ---- 解析交叉校核: F ≈ A_gap/(2μ0) · mean(B²) ----
+        try:
+            d_out = float(params["AFM_D_Rotor_mm"]) * 1e-3
+            d_in = float(params["Stator_Bore_mm"]) * 1e-3
+            area = math.pi / 4.0 * (d_out ** 2 - d_in ** 2)
+            results["analytic"] = {"area_m2": area,
+                                   "note": "F=A/(2mu0)*mean(B^2), B 取仿真气隙磁密"}
+            for tag in ("noload", "load"):
+                case = results.get("case_" + tag) or {}
+                bg = case.get("airgap_B_graph")
+                if bg and bg.get("B2_mean"):
+                    f_est = area / (2.0 * MU0) * bg["B2_mean"]
+                    results["analytic"]["F_est_%s_N" % tag] = f_est
+                    print("  解析估算 F_%s ≈ %.1f N (A=%.5f m², mean(B²)=%.4f)"
+                          % (tag, f_est, area, bg["B2_mean"]))
+        except Exception as e:
+            results["analytic"] = {"failed": str(e)}
+
+        mc.save_to_file(out_mot)
+        res_path = os.path.join(OUT_DIR, "results_axialforce_%s.json" % ts)
+        with open(res_path, "w", encoding="utf-8") as f:
+            json.dump(results, f, ensure_ascii=False, indent=2)
+        print("RESULTS: %s" % res_path)
+        return 0
+    finally:
+        if quit_after:
+            try:
+                mc.quit()
+            except Exception:
+                pass
+        else:
+            print("[提示] Motor-CAD 保持前台打开供检查; 自动化场景加 --quit。")
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))

+ 123 - 0
axial_mag_pull-master/axial_mag_pull/axial_probe.py

@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+"""
+探测 Motor-CAD AFM 轴向力图名/变量名 (第2轮)
+============================================
+依据: MotorCAD.exe (v261) UTF-16 字符串中有 "_Axial_Force_Rotor" /
+"_Axial_Force_Stator" 后缀 (前缀运行时拼接), 以及 GUI 显示名 "Axial Force"。
+一次求解后穷举前缀组合, 用 get_magnetic_graph / get_variable 逐个试,
+命中与否全部如实记录。
+
+用法: python axial_probe.py [--quit]
+"""
+import json
+import os
+import sys
+import time
+
+BASE = os.path.dirname(os.path.abspath(__file__))
+MOT_SRC = os.path.join(BASE, "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot")
+OUT_DIR = os.path.join(BASE, "output_motorcad")
+
+PREFIXES = ["", "OC", "OC_", "OC ", "OL", "OL_", "OL ", "Load", "Load_",
+            "OnLoad_", "OnLoad", "OpenCircuit_", "OpenCircuit", "NoLoad_",
+            "Th1_", "Th1", "1_", "Transient_", "Static_"]
+SUFFIXES = ["Axial_Force_Rotor", "Axial_Force_Stator"]
+EXTRA_GRAPHS = [
+    "Axial_Force", "Axial Force", "Axial Force Rotor", "Axial Force Stator",
+    "Axial Force (Rotor)", "Axial Force (Stator)", "AxialForceRotor",
+    "AxialForceStator", "Fz_Rotor_OL_Lumped", "Fz_Stator_OL_Lumped",
+    "Fz_Rotor_OC_Lumped", "Fz_Stator_OC_Lumped", "Fa_Rotor_OL_Lumped",
+    "Ft_Rotor_OL_Lumped",  # 已知径向机存在的命名, 作对照验证探测方法本身
+    "TorqueVsAngle",       # 已知图, 验证 get_magnetic_graph 可用
+]
+VAR_CANDS = (["AxialForceRotor", "AxialForceStator"] +
+             [p + s for p in ("", "OC_", "OL_", "Load_") for s in SUFFIXES])
+
+
+def stats(ys):
+    if not ys:
+        return None
+    return {"mean": sum(ys) / len(ys), "min": min(ys), "max": max(ys),
+            "pk2pk": max(ys) - min(ys), "n": len(ys)}
+
+
+def main(argv):
+    quit_after = "--quit" in argv
+    os.makedirs(OUT_DIR, exist_ok=True)
+    ts = time.strftime("%m%d_%H%M%S")
+
+    from ansys.motorcad.core import MotorCAD
+    print("启动 Motor-CAD (前台) ...")
+    mc = MotorCAD()
+    results = {"when": ts, "probe_round": 2}
+    try:
+        mc.load_from_file(MOT_SRC)
+        out_mot = os.path.join(OUT_DIR, "MARS_SSSR_probe_%s.mot" % ts)
+        mc.save_to_file(out_mot)
+        results["work_mot"] = out_mot
+
+        # 电流口径侦察 (上轮空载疑似未生效: CurrentDefinition=1)
+        cur = {}
+        for n in ["CurrentDefinition", "PeakCurrent", "RMSCurrent",
+                  "Imax", "Irms", "RMS_Current", "LineCurrent"]:
+            try:
+                cur[n] = mc.get_variable(n)
+            except Exception:
+                cur[n] = "<not found>"
+        results["current_vars"] = cur
+        print("电流相关变量: %s" % json.dumps(cur, ensure_ascii=False))
+
+        for var in ["ElectromagneticForcesCalc_Load",
+                    "ElectromagneticForcesCalc_OC"]:
+            mc.set_variable(var, True)
+
+        print("单次求解 (负载点, OC/Load 力同时计算) ...")
+        t0 = time.time()
+        mc.do_magnetic_calculation()
+        print("  耗时 %.1f s" % (time.time() - t0))
+
+        # ---- 图名穷举 ----
+        graph_names = ([p + s for p in PREFIXES for s in SUFFIXES]
+                       + EXTRA_GRAPHS)
+        hits, misses = {}, []
+        for g in graph_names:
+            try:
+                x, y = mc.get_magnetic_graph(g)
+                hits[g] = {"stats": stats(list(y)), "x0": x[0], "x_end": x[-1],
+                           "x": list(x), "y": list(y)}
+                print("  [命中] %s: %s" % (g, hits[g]["stats"]))
+            except Exception:
+                misses.append(g)
+        results["graph_hits"] = {k: {kk: vv for kk, vv in v.items()
+                                     if kk != "x"} for k, v in hits.items()}
+        results["graph_hits_full"] = hits
+        results["graph_misses"] = misses
+        print("图名: 命中 %d / 未中 %d" % (len(hits), len(misses)))
+
+        # ---- 输出变量穷举 ----
+        var_hits = {}
+        for n in VAR_CANDS:
+            try:
+                var_hits[n] = mc.get_variable(n)
+                print("  [变量命中] %s = %s" % (n, var_hits[n]))
+            except Exception:
+                pass
+        results["variable_hits"] = var_hits
+
+        res_path = os.path.join(OUT_DIR, "probe_results_%s.json" % ts)
+        with open(res_path, "w", encoding="utf-8") as f:
+            json.dump(results, f, ensure_ascii=False, indent=2)
+        print("RESULTS: %s" % res_path)
+        return 0
+    finally:
+        if quit_after:
+            try:
+                mc.quit()
+            except Exception:
+                pass
+        else:
+            print("[提示] Motor-CAD 保持前台打开供检查。")
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))

+ 136 - 0
axial_mag_pull-master/axial_mag_pull/axial_probe2.py

@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+"""
+第3轮: 无求解快速图名筛查
+==========================
+原理: get_magnetic_graph_point 对"图名不存在"与"图存在但无结果/点号越界"
+应返回不同报错文案。先用已知图名(TorqueVsAngle/FluxDensityAirgap)与伪名
+(Bogus_XYZ)标定两类文案, 再穷举前缀x后缀组合, 秒级筛出真实存在的轴向力图名。
+命中后单次求解并读取全波形。
+
+用法: python axial_probe2.py [--quit] [--no-solve]
+"""
+import json
+import os
+import sys
+import time
+
+BASE = os.path.dirname(os.path.abspath(__file__))
+MOT_SRC = os.path.join(BASE, "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot")
+OUT_DIR = os.path.join(BASE, "output_motorcad")
+
+PREFIXES = ["", "OC", "OC_", "OC ", "OL", "OL_", "OL ", "Load", "Load_",
+            "Load ", "OnLoad_", "OnLoad", "On Load ", "OpenCircuit_",
+            "OpenCircuit", "Open Circuit ", "NoLoad_", "No Load ", "Th1_",
+            "Th1", "1_", "Transient_", "Static_", "Rotor_", "Stator_"]
+SUFFIXES = ["Axial_Force_Rotor", "Axial_Force_Stator",
+            "Axial Force Rotor", "Axial Force Stator"]
+EXTRA = ["Axial_Force", "Axial Force", "Axial Force (Rotor)",
+         "Axial Force (Stator)", "AxialForceRotor", "AxialForceStator",
+         "Axial_Force_Rotor_OL", "Axial_Force_Rotor_OC",
+         "Axial_Force_Stator_OL", "Axial_Force_Stator_OC",
+         "Fz_Rotor_OL_Lumped", "Fz_Stator_OL_Lumped",
+         "Ft_Rotor_OL_Lumped", "Fr_Stator_OL_Lumped"]
+CONTROLS_GOOD = ["TorqueVsAngle", "FluxDensityAirgap"]
+CONTROLS_BAD = ["Bogus_XYZ_NotAGraph"]
+
+
+def err_of(mc, name):
+    try:
+        x, y = mc.get_magnetic_graph_point(name, 0)
+        return ("OK", (x, y))
+    except Exception as e:
+        return ("ERR", str(e))
+
+
+def stats(ys):
+    if not ys:
+        return None
+    return {"mean": sum(ys) / len(ys), "min": min(ys), "max": max(ys),
+            "pk2pk": max(ys) - min(ys), "n": len(ys)}
+
+
+def read_graph(mc, name, maxpts=200):
+    xs, ys = [], []
+    for i in range(maxpts):
+        try:
+            x, y = mc.get_magnetic_graph_point(name, i)
+        except Exception:
+            break
+        xs.append(x)
+        ys.append(y)
+    return xs, ys
+
+
+def main(argv):
+    quit_after = "--quit" in argv
+    no_solve = "--no-solve" in argv
+    os.makedirs(OUT_DIR, exist_ok=True)
+    ts = time.strftime("%m%d_%H%M%S")
+
+    from ansys.motorcad.core import MotorCAD
+    print("启动 Motor-CAD (前台) ...")
+    mc = MotorCAD()
+    results = {"when": ts, "probe_round": 3}
+    try:
+        mc.load_from_file(MOT_SRC)
+
+        # ---- 标定两类报错文案 ----
+        calib = {}
+        for n in CONTROLS_GOOD + CONTROLS_BAD:
+            calib[n] = err_of(mc, n)
+            print("  标定 %s -> %s" % (n, calib[n]))
+        results["calibration"] = {k: list(v) for k, v in calib.items()}
+        bad_msg = calib[CONTROLS_BAD[0]][1]
+
+        # ---- 穷举筛查 (无求解, 快) ----
+        cands = [p + s for p in PREFIXES for s in SUFFIXES] + EXTRA
+        exists, not_exists, odd = [], [], {}
+        for n in cands:
+            kind, payload = err_of(mc, n)
+            if kind == "OK":
+                exists.append(n)
+            elif payload == bad_msg:
+                not_exists.append(n)
+            else:
+                odd[n] = payload   # 报错文案不同于"不存在" => 图可能存在
+        results["screen"] = {"exists_ok": exists, "odd_errors": odd,
+                             "n_not_exists": len(not_exists)}
+        print("筛查: 直接OK %s; 异样报错 %s; 不存在 %d 个"
+              % (exists, json.dumps(odd, ensure_ascii=False), len(not_exists)))
+
+        promising = exists + list(odd.keys())
+        if promising and not no_solve:
+            for var in ["ElectromagneticForcesCalc_Load",
+                        "ElectromagneticForcesCalc_OC"]:
+                mc.set_variable(var, True)
+            print("有候选, 单次求解后读全波形 ...")
+            t0 = time.time()
+            mc.do_magnetic_calculation()
+            print("  耗时 %.1f s" % (time.time() - t0))
+            waves = {}
+            for n in promising + CONTROLS_GOOD:
+                xs, ys = read_graph(mc, n)
+                if ys:
+                    waves[n] = {"stats": stats(ys), "x": xs, "y": ys}
+                    print("  [波形] %s: %s" % (n, stats(ys)))
+            results["waveforms"] = waves
+        elif not promising:
+            print("[如实] 全部候选均为'不存在', 需换思路 (数值ID枚举或GUI人工查图名)")
+
+        res_path = os.path.join(OUT_DIR, "probe2_results_%s.json" % ts)
+        with open(res_path, "w", encoding="utf-8") as f:
+            json.dump(results, f, ensure_ascii=False, indent=2)
+        print("RESULTS: %s" % res_path)
+        return 0
+    finally:
+        if quit_after:
+            try:
+                mc.quit()
+            except Exception:
+                pass
+        else:
+            print("[提示] Motor-CAD 保持前台打开供检查。")
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))

+ 150 - 0
axial_mag_pull-master/axial_mag_pull/axial_probe3.py

@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+"""
+第4轮: 图 ID 枚举 + 图名 RPC 试探
+==================================
+第3轮已标定: "Graph name does not exist" = 图不存在; "No points exist" =
+图存在但未求解。get_magnetic_graph_point 的 graph 参数可传数字 ID (variant),
+故枚举 ID 0..N 找出全部存在的图; 再试几个未封装的 RPC 方法名拿 ID→名字映射;
+拿不到名字就求解后读全部波形, 按量级特征辨认轴向力。
+
+用法: python axial_probe3.py [--quit] [--max-id N]
+"""
+import json
+import os
+import sys
+import time
+
+BASE = os.path.dirname(os.path.abspath(__file__))
+MOT_SRC = os.path.join(BASE, "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot")
+OUT_DIR = os.path.join(BASE, "output_motorcad")
+
+NAME_RPCS = ["GetMagneticGraphName", "GetMagneticGraphTitle", "GetGraphName",
+             "GetMagneticGraphInfo", "GetMagneticGraphCount",
+             "GetMagneticGraphNames", "GetMagneticGraphYAxisTitle"]
+
+
+def classify(mc, graph):
+    try:
+        x, y = mc.get_magnetic_graph_point(graph, 0)
+        return "OK", (x, y)
+    except Exception as e:
+        msg = str(e)
+        if "does not exist" in msg:
+            return "ABSENT", None
+        if "No points exist" in msg:
+            return "EXISTS", None
+        return "ODD", msg
+
+
+def stats(ys):
+    if not ys:
+        return None
+    return {"mean": sum(ys) / len(ys), "min": min(ys), "max": max(ys),
+            "pk2pk": max(ys) - min(ys), "n": len(ys)}
+
+
+def read_graph(mc, graph, maxpts=128):
+    xs, ys = [], []
+    for i in range(maxpts):
+        try:
+            x, y = mc.get_magnetic_graph_point(graph, i)
+        except Exception:
+            break
+        xs.append(x)
+        ys.append(y)
+    return xs, ys
+
+
+def main(argv):
+    quit_after = "--quit" in argv
+    max_id = 400
+    if "--max-id" in argv:
+        max_id = int(argv[argv.index("--max-id") + 1])
+    os.makedirs(OUT_DIR, exist_ok=True)
+    ts = time.strftime("%m%d_%H%M%S")
+
+    from ansys.motorcad.core import MotorCAD
+    print("启动 Motor-CAD (前台) ...")
+    mc = MotorCAD()
+    results = {"when": ts, "probe_round": 4}
+    try:
+        mc.load_from_file(MOT_SRC)
+
+        # ---- ID 枚举 (无求解) ----
+        exist_ids, odd = [], {}
+        for gid in range(max_id + 1):
+            kind, payload = classify(mc, gid)
+            if kind in ("EXISTS", "OK"):
+                exist_ids.append(gid)
+            elif kind == "ODD":
+                odd[gid] = payload
+        print("存在的图 ID (%d 个): %s" % (len(exist_ids), exist_ids))
+        if odd:
+            print("异样报错: %s" % json.dumps(odd, ensure_ascii=False))
+        results["exist_ids"] = exist_ids
+        results["odd"] = odd
+
+        # ---- 图名 RPC 试探 ----
+        rpc_found = {}
+        probe_id = exist_ids[0] if exist_ids else 0
+        for meth in NAME_RPCS:
+            try:
+                r = mc.connection.send_and_receive(meth, [probe_id])
+                rpc_found[meth] = r
+                print("  [RPC 可用] %s(%s) = %s" % (meth, probe_id, r))
+            except Exception as e:
+                print("  [RPC 不可用] %s: %s" % (meth, str(e)[:80]))
+        results["name_rpcs"] = rpc_found
+
+        id_names = {}
+        name_rpc = next(iter(rpc_found), None)
+        if name_rpc and rpc_found[name_rpc] not in (None, ""):
+            for gid in exist_ids:
+                try:
+                    id_names[gid] = mc.connection.send_and_receive(
+                        name_rpc, [gid])
+                except Exception:
+                    id_names[gid] = None
+            results["id_names"] = id_names
+            print("ID->图名: %s" % json.dumps(id_names, ensure_ascii=False))
+
+        # ---- 求解一次, 读全部存在图的波形 ----
+        for var in ["ElectromagneticForcesCalc_Load",
+                    "ElectromagneticForcesCalc_OC"]:
+            mc.set_variable(var, True)
+        print("求解 (负载点, OC/Load 力已开) ...")
+        t0 = time.time()
+        mc.do_magnetic_calculation()
+        print("  耗时 %.1f s" % (time.time() - t0))
+
+        waves = {}
+        for gid in exist_ids:
+            xs, ys = read_graph(mc, gid)
+            if ys:
+                waves[str(gid)] = {"name": id_names.get(gid),
+                                   "stats": stats(ys),
+                                   "x0": xs[0], "x_end": xs[-1],
+                                   "x": xs, "y": ys}
+        results["waveforms"] = waves
+        print("有数据的图 %d 个:" % len(waves))
+        for gid, w in waves.items():
+            print("  id=%s name=%s x:[%.3g..%.3g] %s"
+                  % (gid, w["name"], w["x0"], w["x_end"], w["stats"]))
+
+        res_path = os.path.join(OUT_DIR, "probe3_results_%s.json" % ts)
+        with open(res_path, "w", encoding="utf-8") as f:
+            json.dump(results, f, ensure_ascii=False, indent=2)
+        print("RESULTS: %s" % res_path)
+        return 0
+    finally:
+        if quit_after:
+            try:
+                mc.quit()
+            except Exception:
+                pass
+        else:
+            print("[提示] Motor-CAD 保持前台打开供检查。")
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))

+ 189 - 0
axial_mag_pull-master/axial_mag_pull/axial_probe4.py

@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+"""
+第5轮: 力开关打开后筛 2D/3D 图名
+=================================
+修正第3轮漏洞: 图名可能在 ElectromagneticForcesCalc_* 打开后才注册, 先开
+开关再筛。同时筛 3D 力图 (get_magnetic_3d_graph_point, 空间x时间), 并用
+已知图 (转矩/气隙磁密) 反推 Graph Viewer 命名惯例。
+
+用法: python axial_probe4.py [--quit]
+"""
+import json
+import os
+import sys
+import time
+
+BASE = os.path.dirname(os.path.abspath(__file__))
+MOT_SRC = os.path.join(BASE, "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot")
+OUT_DIR = os.path.join(BASE, "output_motorcad")
+
+# 已知图命名惯例侦察 (id17=转矩0.522, id0-2=相电流, FluxDensityAirgap 已知)
+KNOWN_PROBE = ["Torque", "Torque OL", "Torque (OL)", "TorqueOL", "Torque_OL",
+               "Torque vs Angle", "TorqueVsAngle", "Cogging Torque",
+               "Cogging Torque OC", "CoggingTorque", "Phase Current",
+               "Phase Current OL", "Current", "CurrentOL", "Back EMF",
+               "Phase EMF", "FluxDensityAirgap", "Airgap Flux Density"]
+
+BASES = ["Axial Force", "Axial_Force", "AxialForce", "Fz", "Fa"]
+ENTS = ["", " Rotor", " Stator", "_Rotor", "_Stator", "Rotor", "Stator"]
+CASES = ["", " OL", " OC", "_OL", "_OC", " (OL)", " (OC)", " Load",
+         " Open Circuit", "_OL_Lumped", "_OC_Lumped", "_Lumped"]
+
+D3_EXTRA = ["Ft_Rotor_OL_Lumped", "Fr_Rotor_OL_Lumped",
+            "Ft_Stator_OL_Lumped", "Fr_Stator_OL_Lumped",
+            "OL_Axial_Force_Rotor", "OC_Axial_Force_Rotor",
+            "OL_Axial_Force_Stator", "OC_Axial_Force_Stator",
+            "Load_Axial_Force_Rotor", "Load_Axial_Force_Stator"]
+
+
+def build_force_names():
+    out = []
+    for b in BASES:
+        for e in ENTS:
+            for c in CASES:
+                n = b + e + c
+                if n not in out:
+                    out.append(n)
+    for n in D3_EXTRA:
+        if n not in out:
+            out.append(n)
+    return out
+
+
+def classify2d(mc, g):
+    try:
+        mc.get_magnetic_graph_point(g, 0)
+        return "OK"
+    except Exception as e:
+        m = str(e)
+        if "does not exist" in m:
+            return "ABSENT"
+        if "No points exist" in m:
+            return "EXISTS"
+        return "ODD:" + m
+
+
+def classify3d(mc, g):
+    try:
+        mc.get_magnetic_3d_graph_point(g, 1, 0, 0)
+        return "OK"
+    except Exception as e:
+        m = str(e)
+        if "does not exist" in m:
+            return "ABSENT"
+        if "No points exist" in m or "no points" in m.lower():
+            return "EXISTS"
+        return "ODD:" + m
+
+
+def stats(ys):
+    if not ys:
+        return None
+    return {"mean": sum(ys) / len(ys), "min": min(ys), "max": max(ys),
+            "pk2pk": max(ys) - min(ys), "n": len(ys)}
+
+
+def read2d(mc, g, maxpts=128):
+    xs, ys = [], []
+    for i in range(maxpts):
+        try:
+            x, y = mc.get_magnetic_graph_point(g, i)
+        except Exception:
+            break
+        xs.append(x)
+        ys.append(y)
+    return xs, ys
+
+
+def read3d(mc, g, section, maxpts=256, tstep=0):
+    xs, ys = [], []
+    for i in range(maxpts):
+        try:
+            x, y = mc.get_magnetic_3d_graph_point(g, section, i, tstep)
+        except Exception:
+            break
+        xs.append(x)
+        ys.append(y)
+    return xs, ys
+
+
+def main(argv):
+    quit_after = "--quit" in argv
+    os.makedirs(OUT_DIR, exist_ok=True)
+    ts = time.strftime("%m%d_%H%M%S")
+
+    from ansys.motorcad.core import MotorCAD
+    print("启动 Motor-CAD (前台) ...")
+    mc = MotorCAD()
+    results = {"when": ts, "probe_round": 5}
+    try:
+        mc.load_from_file(MOT_SRC)
+        for var in ["ElectromagneticForcesCalc_Load",
+                    "ElectromagneticForcesCalc_OC"]:
+            mc.set_variable(var, True)
+        print("力开关已开, 开始筛名 (无求解) ...")
+
+        known = {n: classify2d(mc, n) for n in KNOWN_PROBE}
+        results["known_probe"] = known
+        print("已知图命名侦察: %s" % json.dumps(
+            {k: v for k, v in known.items() if v != "ABSENT"},
+            ensure_ascii=False))
+
+        force_names = build_force_names()
+        hits2d = {}
+        for n in force_names:
+            k = classify2d(mc, n)
+            if k != "ABSENT":
+                hits2d[n] = k
+        results["force_2d_hits"] = hits2d
+        print("2D 力图命中: %s" % json.dumps(hits2d, ensure_ascii=False))
+
+        hits3d = {}
+        for n in force_names:
+            k = classify3d(mc, n)
+            if k != "ABSENT":
+                hits3d[n] = k
+        results["force_3d_hits"] = hits3d
+        print("3D 力图命中: %s" % json.dumps(hits3d, ensure_ascii=False))
+
+        promising2d = [n for n, k in hits2d.items()]
+        promising3d = [n for n, k in hits3d.items()]
+        if promising2d or promising3d:
+            print("求解一次后读波形 ...")
+            t0 = time.time()
+            mc.do_magnetic_calculation()
+            print("  耗时 %.1f s" % (time.time() - t0))
+            waves = {}
+            for n in promising2d:
+                xs, ys = read2d(mc, n)
+                if ys:
+                    waves["2D:" + n] = {"stats": stats(ys), "x": xs, "y": ys}
+                    print("  [2D] %s: %s" % (n, stats(ys)))
+            for n in promising3d:
+                for sec in (1, 2):
+                    xs, ys = read3d(mc, n, sec)
+                    if ys:
+                        key = "3D:%s:sec%d" % (n, sec)
+                        waves[key] = {"stats": stats(ys), "x": xs, "y": ys}
+                        print("  [3D] %s sec%d: %s" % (n, sec, stats(ys)))
+            results["waveforms"] = waves
+        else:
+            print("[如实] 力开关打开后仍无任何命中")
+
+        res_path = os.path.join(OUT_DIR, "probe4_results_%s.json" % ts)
+        with open(res_path, "w", encoding="utf-8") as f:
+            json.dump(results, f, ensure_ascii=False, indent=2)
+        print("RESULTS: %s" % res_path)
+        return 0
+    finally:
+        if quit_after:
+            try:
+                mc.quit()
+            except Exception:
+                pass
+        else:
+            print("[提示] Motor-CAD 保持前台打开供检查。")
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))

+ 94 - 0
axial_mag_pull-master/axial_mag_pull/docs/COMPARISON_V3.md

@@ -0,0 +1,94 @@
+# 对标分析:Motor-CAD FEA vs《轴向磁拉力计算与轴承选型校核报告 V3.0-20260826》
+
+对标对象: 解析法报告 (磁路模型 + Maxwell 应力, 20°C 基准, 空载 Fz=483 N/中值 500 N)。
+FEA 验证运行: 2026-08-25 21:54 (commit 33eb8cf, compare_results_0825_215456)。
+
+## 一、总体结论:**对得上**
+
+方法、趋势、轴承结论全部一致;数值上解析法在同温度基准下系统性偏高约 16%
+(处于磁路法忽略齿槽/边缘/侧漏的正常误差量级),最大的表观差异 (343 vs 483 N)
+主要是**磁钢温度基准不同**造成的,不是矛盾。
+
+## 二、逐项数值对比
+
+### 1. 空载轴向力 (归一到磁钢 20°C 后)
+
+| 气隙 | 报告解析 (表3-1/3-3) | FEA (本次实测) | FEA/解析 |
+|---|---|---|---|
+| 0.6 mm | 601 N | 513.1 N | 0.85 |
+| **1.0 mm (基准)** | **483 N** | **416.5 N** | **0.86** |
+| 1.5 mm | 378 N | 326.7 N | 0.86 |
+
+三个气隙下比值稳定在 0.85~0.86 —— 解析法系统性高 ~16%,来源于集中参数磁路
+不计定子开槽 (Maxwell 应力只作用在齿面铁上)、极缘边缘效应与厚磁钢侧向漏磁
+(其 σ=1.15 只覆盖了一部分)。**FEA 更可信**;报告第 7 节自己也建议用 3D FEA
+复核 —— 本次 Motor-CAD 2.5D FEA 即该复核。
+
+### 2. 磁钢温度的影响 (报告 3.5 节)
+
+| 项 | 报告 | FEA |
+|---|---|---|
+| 温度系数 | α(Br)≈−0.11 %/K, F∝Br² | 模型 Br 系数 −0.12 %/K |
+| 验证 | 80°C 时 −15% | 100°C 实测 343.2 N vs 由 20°C 值按 Br² 折算 340 N —— **偏差 <1%, 定律精确成立** |
+
+**关键提醒:.mot 模型默认 `Magnet_Temperature=100°C`**,此前正式结果 343 N
+是 100°C 热态值;报告 483 N 是 20°C 冷态解析值。同基准比较才有意义:
+- 20°C: FEA 416.5 N vs 解析 483 N (差 16%)
+- 100°C: FEA 343.2 N vs 解析折算 ~395 N (差同量级)
+
+### 3. 磁负刚度 kneg (报告 3.6 节, SS-SR 标志性风险)
+
+| 段 | 报告解析 | FEA 有限差分 |
+|---|---|---|
+| 0.6~1.0 mm | ~290 N/mm (内插) | 241 N/mm |
+| 1.0 mm 处 | 250 N/mm | 207 N/mm (中心差分) |
+| 1.0~1.5 mm | ~210 N/mm (内插) | 180 N/mm |
+
+比值同样 ~0.85 (kneg 随 F 等比)。**负刚度概念与量级成立**;ks>10·kneg 判据下
+708AC 刚度比 ~12~15 倍,"临界"的结论不变。
+
+### 4. 负载电枢反应 (报告 3.7 节)
+
+报告按经验取瞬时峰值比空载高 10~20%,得设计载荷 600 N。
+**FEA 实测: 负载(21A RMS)均值仅比空载高 +0.07%,纹波峰峰 ~1%** ——
+表贴磁钢 + q 轴电流工况下电枢反应对轴向力几乎无影响,+20% 的经验放大在
+机理上不成立。但 600 N 作为设计包络仍然稳健,因为真正的载荷上浮来自:
+低温 (−20°C 约 +10%) 与气隙公差 (0.6 mm 时 +23%)。最恶劣组合
+(−20°C + 0.6 mm) FEA 推算 ≈ 563 N < 600 N,**设计载荷 600 N 恰好覆盖**。
+
+### 5. 轴承校核结论的稳健性 (报告第 4 章)
+
+以 FEA 载荷代入报告公式复核 708AC:
+- 名义 20°C: P=0.87×417≈363 N → L10h≈1 730 h,仍远小于 20 000 h
+  (差距由 35 倍缩小为 ~12 倍,**"疲劳寿命不满足"结论不变**);
+- 按设计载荷 600 N 则报告的 577 h 原样成立;
+- 静载 s0、刚度比"临界"、方案 A (7004AC) 推荐 —— 结论全部不受影响,
+  且 FEA 载荷更低意味着方案 A 裕度比报告估计的更大。
+
+## 三、输入核对 (2026-08-25 已闭环: 完全一致)
+
+初核时曾疑 .mot 的 `Magnet_Thickness=13` 与报告 lm=3 mm 矛盾。经用户澄清
+并查证 .mot 确认: **AFM 模板中 `Magnet_Length=3` 才是磁钢轴向厚度 (3 mm)**,
+`Magnet_Thickness=13` 是磁钢环径向深度 = (76−50)/2 = 13 mm;极弧参数为
+`Magnet_Arc_[ED]=121` (电角度) = 24.2° 机械角 = 覆盖率 67.2%
+(此前误读的 `Pole_Arc=150` 属其它转子类型参数,本模型不生效)。
+
+| 参数 | 报告 (表2-1) | .mot 模型 | 核对 |
+|---|---|---|---|
+| 磁钢轴向厚度 lm | 3 mm | Magnet_Length=3 | **一致** |
+| 磁钢径向深度 | (76−50)/2=13 mm (隐含) | Magnet_Thickness=13 | **一致** |
+| 极弧覆盖 | 24.2°/极 (67.2%) | Magnet_Arc_[ED]=121°elec = 24.2°mech | **一致** |
+| Br / μr | 1.30 / 1.05 | 1.31 / 1.05 | 一致 |
+| 内外径/气隙/极数/转速/电流 | 50/76 mm, 1 mm, 10, 5000 rpm, 21 A | 同 | 一致 |
+
+即模型与报告描述**同一套磁钢几何**,二者的 0.85~0.86 比值是纯粹的方法差异
+(磁路法不计齿槽/边缘/侧漏,系统性偏高 ~15%),不存在误差抵消。
+(初版本节曾警示"误差抵消",系误读 Magnet_Thickness 所致,特此更正并留痕。)
+
+## 四、结论一句话
+
+输入完全对齐、同一温度基准下,FEA 与报告解析值比值稳定在 0.85~0.86,气隙
+趋势、温度定律、负刚度量级、轴承"不满足→换 7004AC"的结论链全部相互印证 ——
+**对得上**。唯一建议修正: 设计载荷 600 N 的依据从"电枢反应 +20%"(FEA 实测
+仅 +0.07%,机理不成立) 改为"低温 + 气隙公差包络"(最恶劣 −20 ℃ + 0.6 mm
+≈ 563 N,数值几乎相同、机理正确)。

+ 272 - 0
axial_mag_pull-master/axial_mag_pull/docs/CONVERSATION_LOG.md

@@ -0,0 +1,272 @@
+# 对话记录 (如实, 带时间戳)
+
+## 2026-08-25
+
+### [2026-08-25 20:53] 用户
+> 我们需要对本目录下的模型 `MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot`,用 Motor-CAD
+> 仿真给出轴向磁拉力。请给出方案先。用 git 管理本项目文件,如实记录带时间戳的我们的
+> 对话,关于工具的使用请参照 `E:\aitestlocal\claude\pss\maxcalculator`,Motor-CAD 要
+> 前台运行,每次运行测试前要 git 提交。
+
+### [2026-08-25 20:57] Claude
+- 查看了参考项目 `pss\maxcalculator`:其 `motorcad_export.py` 用系统 Python 3.12 +
+  PyMotorCAD (`ansys.motorcad.core`) 驱动 Motor-CAD,默认前台保持打开(用户偏好),
+  变量名用候选列表逐个尝试(防版本改名)。本机已装 PyMotorCAD,可用。
+- 检查了 .mot 文件:Axial Flux BPM(基于 YASA LifeCar 模板),12槽10极 SSSR(单定子
+  单转子),Airgap=1mm,Magnet_Thickness=13mm,ShaftSpeed=5000rpm,
+  PeakCurrent=29.7A,TorquePointsPerCycle=30。
+- 初始化了 git 仓库,创建 .gitignore(生成物不入库)与本对话记录。
+- 给出方案(见 PLAN.md),等待用户确认后再跑 Motor-CAD。
+
+### [2026-08-25 21:05] 用户
+> 开始
+
+### [2026-08-25 21:08] Claude
+- 补充侦察 .mot:力计算开关 `ElectromagneticForcesCalc_Load/OC` 存在且为 False
+  (脚本将打开);AFM 为 2 个径向切片(截面中心半径 28.25/34.75mm);
+  `Units_Force=kN`(读数时注意单位);内嵌 Python 脚本只是模板样板,无变量名线索。
+- 编写 `axial_force_run.py`:前台启动 Motor-CAD → 另存时间戳副本 → 开力开关 →
+  空载(I=0)/负载(29.7A) 两工况求解 → 探测式读取轴向力(变量候选+波形候选)→
+  气隙磁密解析校核 F≈A·mean(B²)/(2μ₀) → results JSON。
+- 按纪律:运行前 git 提交,随后执行首跑。
+
+### [2026-08-25 21:10] Claude — 首跑复盘 (如实)
+- 首跑 (commit dc56046) 完成,每工况 FEA 87s。Motor-CAD 消息日志确认力计算已执行
+  ("Open Circuit Force Calculation" + 力谐波 2D FFT)。
+- **问题1**:14 个轴向力变量名 + 7 个图名候选全部未命中。
+- **问题2**:空载(设 PeakCurrent=0)与负载读数逐位相同 (ShaftTorque=0.4946,
+  气隙磁密同)——疑因 .mot 中 CurrentDefinition=1 (RMS 口径) 使 PeakCurrent 改动
+  不生效;且 ShaftTorque 可能是输入回显。空载判定作废。
+- 溯源:Motor-CAD 2026R1 装在 E:\Program Files\ANSYS Inc\v261\motorcad\。
+  从 MotorCAD.exe 的 UTF-16 字符串中实证找到后缀 "_Axial_Force_Rotor" /
+  "_Axial_Force_Stator" (前缀运行时拼接),以及 GUI 显示名 "Axial Force"。
+  PyMotorCAD 无图名枚举 API;FEA 自定义路径的力测量对 AFM 不可用 (exe 内错误
+  文案证实)。
+- 第 2 轮 (commit b94ff89):单次求解 (OC/负载力同算),穷举 19 前缀 × 2 后缀
+  + 16 个补充图名;同时侦察 CurrentDefinition/RMSCurrent 等电流口径变量。
+
+### [2026-08-25 21:20] Claude — 第 2~4 轮结果 (如实)
+- 第 2 轮:0/53 全未中,连对照组 TorqueVsAngle 也未中 → get_magnetic_graph
+  (整线 API) 的探测方法本身失效,非名字问题。确认 CurrentDefinition=1,
+  RMSCurrent=21A 是真正的电流入口 (首跑改 PeakCurrent=0 无效的根因)。
+- 反编译 Motor-CAD.chm (110MB, 3175 文件):全文无 "Axial Force" —— 2026R1
+  新功能无文档,只能实测。
+- 第 3 轮 (ed6bb8f):报错文案筛查法标定成功 ("Graph name does not exist" vs
+  "No points exist");114 个候选名全部"不存在";TorqueVsAngle 在 AFM 模板下
+  确实不存在 (解释第 2 轮)。
+- 第 4 轮 (de620f1):图 ID 0..400 枚举 + 一次求解 → 51 条有数据波形。辨认出:
+  id0-2 相电流 ±29.66A、id16 RMS 电流常量 21A、id17 总转矩均值 0.522 Nm
+  (= id18 + id19 两径向切片分量)、id33-35 反电动势 ±6.4V 等。
+  **无任何波形量级像轴向力** (预期 ~130N 或 0.13kN) → 力波形不在
+  magnetic graph 序列, 或需力开关打开后图名才注册 (第 3 轮筛时未开力开关,
+  是漏洞)。图名查询 RPC (GetMagneticGraphName 等 7 个) 全部不存在。
+- 第 5 轮 (22658e4):先开力开关再筛 2D+3D (get_magnetic_3d_graph_point)
+  ~500 个组合名;用已知图反推 Graph Viewer 命名惯例。
+
+### [2026-08-25 21:25] Claude — 第 5 轮突破 + 正式结果
+- 第 5 轮命中:力数据在 **3D lumped 力图** `Fr/Ft_{Rotor,Stator}_{OL,OC}_Lumped`
+  (get_magnetic_3d_graph_point)。AFM 2.5D 展开模型沿用径向机命名,
+  **Fr(法向) 即轴向力**。转子 10 节点(36°步)全正、定子 12 节点(30°步)全负
+  (反作用),两侧量级一致 → 判定可信。
+- 正式计算 (deb6b65, 求解 88s):
+  - **空载净轴向力 342.9 N (纹波峰峰 3.5 N);负载(RMS 21A) 343.2 N (3.9 N)**,
+    转子被吸向定子。电流影响 +0.1% —— 磁钢主导。
+  - 校核全过:定转子反号偏差 1.7%;Σ(Ft×r)=0.505 vs 转矩图 0.522 Nm (3%);
+    解析 B²A/2μ0=485 N 同量级 (解析系统性偏大, 合理)。
+  - 详见 RESULTS.md;波形 CSV/JSON 在 output_motorcad/ (不入库)。
+
+### [2026-08-25 21:33] 用户
+> 有图形或完整的报告吗?如果交给同事去跑应该怎么样做?
+
+### [2026-08-25 21:40] Claude
+- 生成完整 HTML 报告 `docs/report_axialforce.html`(汇总卡 + 净轴向力波形图
+  + 节点力分布图 + 校核表 + 方法说明 + 复现指南),已发布为 Artifact:
+  https://claude.ai/code/artifact/987ef477-718e-41aa-908d-12d62c29f17e
+  (默认私有,可从页面分享给同事)。
+- 新增 `REPRODUCE.md` 同事复现指南:环境要求、一条命令运行
+  (`python -X utf8 axial_force_final.py`)、结果判定三判据、全部已知坑
+  (RMS 电流口径、3D 力图入口、节点去重等)、换模型注意事项。
+
+### [2026-08-25 21:41] 用户
+> 报告生成一个图文并茂的pdf
+
+### [2026-08-25 21:43] Claude
+- HTML 报告加打印样式后用 Edge 无头 `--print-to-pdf` 排版成 4 页 PDF:
+  `docs/report_axialforce.pdf` (846KB),已逐页目检。
+- 顺手修了两处图形瑕疵:柱状图改统一角度轴 (0–360°,转子/定子同角对齐,
+  解决标签重叠);折线图右端"空载"标签避让曲线。Artifact 同步更新 (同 URL)。
+- 清理了首次 Edge 异步写入的过时中文名 PDF。提交 6619b22 / d28988f。
+
+### [2026-08-25 21:50] 用户
+> 参照"轴向磁通电机轴向磁拉力计算与轴承选型校核报告V3.0-20260826.pdf"
+> 里面的计算和分析,看看能不能对的上
+
+### [2026-08-25 21:58] Claude — 对标结论:对得上
+- 读取 V3.0 解析报告 (磁路+Maxwell 应力, 20°C 基准 Fz=483N/中值 500N,
+  708AC 疲劳寿命不足→推荐 7004AC)。
+- 发现输入分歧:模型磁钢温度 100°C (报告 20°C)、磁钢厚 13mm (报告 3mm)、
+  极弧 83.3% (报告 67.2%);Br/μr/几何/转速/电流一致。
+- 验证扫描 (33eb8cf):磁钢归一 20°C, 气隙 0.6/1.0/1.5mm 三次求解:
+  FEA 513/416/327 N vs 报告解析 601/483/378 N —— **比值稳定 0.85~0.86**
+  (磁路法不计齿槽/边缘/侧漏的正常偏高);kneg FEA 207 N/mm@1mm vs 解析 250;
+  温度定律 F∝Br² 实测偏差 <1% (100°C 343N ↔ 20°C 416N)。
+- 与报告不同的机理性发现:负载电枢反应对轴向力仅 +0.07% (报告经验取
+  +10~20%)——600N 设计载荷仍然稳健, 但依据应改为低温+气隙公差包络
+  (最恶劣 −20°C+0.6mm ≈ 563N < 600N)。
+- 如实警示:报告解析与 FEA 吻合含误差抵消——若按模型真实 13mm/83% 代入
+  其磁路公式得 ~950N (失效);须设计侧确认磁钢厚度/极弧哪个是实物。
+- 详见 docs/COMPARISON_V3.md。轴承结论链 (708AC 不满足→7004AC) 不受影响。
+
+### [2026-08-25 22:05] 用户
+> 出V2版
+> (追加澄清) 磁钢厚度应该是3mm, 模型中的13mm可能是径向磁钢的长度
+
+### [2026-08-25 22:12] Claude — 报告 V2 + 输入分歧闭环
+- 用户澄清正确, 查证 .mot 实锤: **Magnet_Length=3 才是磁钢轴向厚度 (3mm)**,
+  Magnet_Thickness=13 是磁钢环径向深度 =(76−50)/2=13mm; 且极弧实为
+  Magnet_Arc_[ED]=121°elec = 24.2°mech = 67.2%, 与解析报告表 2-1 完全一致
+  (此前误读的 Pole_Arc=150 属其它转子类型参数)。**输入分歧全部消除**,
+  0.85~0.86 比值系纯方法差异, 撤回"误差抵消"警示 (COMPARISON_V3.md 已
+  更正并留痕)。
+- 报告 V2: 汇总卡改为 100°C/20°C 双基准 + 负载影响 + kneg 四卡; 新增
+  "对标 V3.0" 章节 (气隙扫描双线图 + 六项对比表 + 输入核对说明);
+  RESULTS.md 磁钢参数勘误。PDF 重出 (6 页) 已逐页目检, Artifact 同 URL
+  更新 (版本标签 V2-with-comparison)。
+
+### [2026-08-25 22:16] 用户
+> 新生成的文件把上一版的覆盖了,文件名改成V2吧,原来的要保留
+
+### [2026-08-25 22:18] Claude
+- 从 git 历史 (6619b22) 恢复 V1: `report_axialforce_V1.html/.pdf` (4 页);
+  当前版改名 `report_axialforce_V2.html/.pdf` (6 页)。
+- Artifact 用 url 参数指向新路径重发, 链接保持不变。
+- 后续版本约定: 文件名带版本号, 旧版保留不覆盖。
+
+### [2026-08-25 22:22] 用户
+> 如何用motorcad一步一步操作求解和看结果
+
+### [2026-08-25 22:24] Claude
+- 编写 docs/GUI_GUIDE.md:7 步手动操作指南(打开模型/另存副本 → 核对几何
+  → 设工况(RMS 电流、磁钢温度) → 勾力计算开关 → 求解 → 看结果(Output
+  Data/Graphs/Forces/FEA/Graph Viewer) → 气隙敏感性),含轴向力查看的
+  四个关键须知(Fr=轴向力、每节点集中力、净合力需手工求和、定子反号)
+  与已验证参考结果速查表。
+
+### [2026-08-25 22:28] 用户
+> 以后结果和报告应该带上时间戳和简要说明,并进行git提交。将本项目的过程
+> 进行总结和建立知识库,希望其它AI工具也能读取和使用已有的知识和工具、
+> 成果、对照等。总之其它同事git把仓库拉下来之后,可以使用AI工具执行新的
+> 模型的仿真。
+
+### [2026-08-25 22:33] Claude — 知识库建立
+- 新建 **docs/KNOWLEDGE_BASE.md** (核心知识库, 8 节): 环境事实 / AFM 轴向力
+  唯一数据口 (3D lumped 力图, Fr=轴向力) / .mot 参数语义陷阱表 / 图名探测
+  技术 (报错文案筛查、ID 枚举、exe 字符串挖掘等 6 法) / 新模型 SOP 七步 /
+  已有成果与对照定版 / 硬性纪律 / 5 轮探测过程档案。
+- 新建 **AGENTS.md** (跨 AI 工具通用入口: Claude Code/Codex/Cursor 均识别)
+  与 **CLAUDE.md** (Claude 入口, 指向前者) 与 **README.md** (人类入口,
+  含结果速查/文档索引/快速开始)。
+- 纪律固化 (写入 AGENTS.md/KB §7): 结果与报告带时间戳+简要说明并 git
+  提交; 报告版本化不覆盖; 运行前提交; Motor-CAD 前台; 生成物不入库。
+- 同事使用路径: git clone → 交给任意 AI 工具 → 读 AGENTS.md →
+  KNOWLEDGE_BASE.md → 按 SOP 改 axial_force_final.py 跑新模型。
+
+### [2026-08-25 22:36] 用户
+> 执行 git remote add origin https://gogsgit.ez4l.com/horacezhang/axial_mag_pull.git 并推到远端仓库
+
+### [2026-08-25 22:37] Claude
+- 已添加 remote origin 并推送 master 到
+  https://gogsgit.ez4l.com/horacezhang/axial_mag_pull.git (新分支,
+  已设 upstream)。同事从该地址 clone 即可。
+
+### [2026-08-25 22:42] 用户
+> (截图 Calculation 页) 没有看到 open circuit 选项
+
+### [2026-08-25 22:44] Claude
+- 截图确认实际布局: Calculation 页右侧 Performance Tests 栏中,
+  "Electromagnetic Forces" 复选框在 **Open Circuit 分组**与 **On Load
+  分组**里各出现一次 (用户已勾 OC 的, 漏勾 On Load 的) —— 两个都要勾,
+  没有单独叫 "Open Circuit" 的选项。轴向力结果在顶部 **Forces** 标签页。
+- GUI_GUIDE.md 第 4/6 步已按真实界面核对更正并推送 (7348f51)。
+
+### [2026-08-25 22:48] 用户
+> (截图 Forces 页 Time Harmonics 1D, Stator (Axial) 力密度波形+谐波)
+> 哪部分?还是别的tab?
+
+### [2026-08-25 22:50] Claude
+- 确认就是 Forces 页; 当前子页显示的是单点力密度 (N/m²) 时间波形+谐波
+  (NVH 口径), 不是合力。Force Type 选 Rotor (Axial) 即转子轴向力,
+  Calculation 下拉切 OL/OC; Time Domain 2D 是脚本读的空间×时间数据源。
+- GUI 不直接给净合力标量, 须节点求和 (脚本的职责)。快速核对: 0 阶 DC
+  幅值 2.2e5 N/m² × 齿面有效面积 ≈ 400N, 与 20°C 的 416.5N 对上。
+- 单点 2 阶脉动与 DC 同量级但沿圆周相位错开、求和抵消 → 净力纹波仅 ~1%;
+  轴承看净力, 定子盘 NVH 看 2 阶分布激励。已补入 GUI_GUIDE.md 读图指南。
+
+### [2026-08-26 12:45] 用户 (新机器, Kimi Work)
+> 通读仓库文档, 在这台电脑重跑仿真, 更新知识库, 确认后推送到
+> https://gogsgit.ez4l.com/horacezhang/axial_mag_pull.git
+
+### [2026-08-26 12:45] Kimi — 可行性评估
+- 通读 KNOWLEDGE_BASE.md / REPRODUCE.md / axial_force_final.py 等全部关键文件。
+- 环境探测: Motor-CAD 2026R1 在 **D:** 盘 (KB 记的是 E:); 系统 Python 无
+  ansys 包, 但 Motor-CAD **自带 Python 3.13 + pymotorcad 0.8.4** 可直接用;
+  目录是纯文件拷贝 (.git 丢失), 需 git init; 远程 gogsgit 可达 (401 需认证)。
+- 用户确认开始。
+
+### [2026-08-26 12:47] Kimi — git init + 环境适配
+- `git init -b main`, 首次提交 4836a40 (25 文件)。
+- 首跑失败: pymotorcad 0.8.4 仍依赖 MOTORCAD_ACTIVEX (本机未设)。
+  脚本加 `set_motorcad_exe()` 回退 (MOTORCAD_EXE 可覆盖), 提交 b448fc6。
+- 二跑失败: Motor-CAD 启动后 ~30s 静默退出, 报 psutil.NoSuchProcess。
+  排查: lmgrd 监听 1055 但 **ansyslmd vendor daemon 未运行** → 无法 checkout。
+
+### [2026-08-26 13:00] 用户
+> 用 ANSYS License Management Center 把 License 修好了, 再试
+
+### [2026-08-26 13:02] Kimi — 定位环境变量继承问题
+- ansyslmd 已运行, 但重跑仍失败。查注册表: 机器级环境变量其实齐全
+  (ANSYSLMD_LICENSE_FILE=1055@localhost, MOTORCAD_ACTIVEX=%APPDATA%\Ansys\
+  v261\motorcad\activex.bat —— 用户已做 Update to Current Version 注册)。
+- **根因: AI 工具的 shell 未继承机器级环境变量** (Kimi 桌面进程启动早于
+  设置, 或非登录 shell)。inline export 两个变量后运行成功。
+
+### [2026-08-26 13:03] Kimi — 复现成功, 数值与定版完全一致
+- 求解 150.9 s (原机 88~112 s)。结果:
+  空载 342.9 N (纹波 3.53 N) / 负载 21A 343.2 N (纹波 3.87 N) ——
+  与 08-25 定版**完全一致**; 三判据全过 (作用-反作用 1.70%/1.69%,
+  转矩交叉 3.2%, 解析 485 N ≈ 1.41×)。
+- 数据: output_motorcad/axialforce_final_0826_130316.json (不入库),
+  关键数值已转录 RESULTS.md 与 KB §6 (新增"复现验证"小节);
+  KB §1 环境表重写 (双机路径/自带 Python/许可陷阱/非登录 shell 陷阱)。
+- 待用户确认后推送远程。
+
+### [2026-08-26 (原机)] 用户
+> 1. 远端同事推送了他的改动,pull 下来 merge。
+> 2. 同事反应虽然声明了前台运行 motorcad, windows11 任务栏上有 motorcad
+>    但是看不到也点不开真正的 motorcad gui 界面
+
+### [2026-08-26 (原机)] Claude — pull 合并 + 窗口不可见修复
+- git pull 成功 (fast-forward 至 4dd3a8c, 无冲突)。同事 (Kimi 机) 的复现
+  342.9/343.2N 与定版完全一致, 环境坑已入 KB, 很好。
+- 窗口不可见问题定位: pymotorcad 以 `MotorCAD.exe /PORT=n /SCRIPTING`
+  启动 (rpc_client_core.py L455 实证), 部分机器上该模式**窗口创建但不显示**
+  (任务栏有图标、点击无反应 —— 非最小化/非屏幕外)。修复三件套:
+  1. axial_force_final.py / axial_compare.py 启动后调 `mc.set_visible(True)`
+     强制可见 (已可见无副作用);
+  2. 新增 `show_motorcad.py`: `MotorCAD(open_new_instance=False)` attach
+     已运行的隐形实例召回窗口, 不新开实例、不打断求解;
+  3. KB §1 增补"窗口不可见陷阱"条目 (含真·屏幕外情形的 Win+↑ 补救)。
+
+### [2026-08-26 (原机)] 用户
+> 我们这边试一下看看 motorcad 在不在前台 gui 上吧 → (测试后) 看见了。
+
+### [2026-08-26 (原机)] Claude — 可见性修复实测通过
+- Win32 API 逐窗口实测 (test_visible.py, 三时点):
+  - set_visible 前: 主窗口 IsWindowVisible=**False** (隐藏), 任务栏图标是
+    坐标 (−32000,−32000) 的 "Motor-CAD" 代理窗口 —— **本机也复现了同事
+    现象**, 说明 /SCRIPTING 模式默认主窗口隐藏, 不是个别机器问题;
+  - set_visible(True) 后: 主窗口可见且在屏内 (256,72,1280,840) ✓;
+  - 载入模型后标题正常、保持可见 ✓。用户肉眼确认"看见了"。
+- 如实更正: 此前本机历次仿真 Motor-CAD 其实也是隐藏运行的 (用户另开了
+  手动 GUI 实例才没察觉); "前台运行"自本修复起才真正成立。
+- KB 措辞由"部分机器"更正为"该模式默认主窗口隐藏 (双机实测)"。

+ 105 - 0
axial_mag_pull-master/axial_mag_pull/docs/GUI_GUIDE.md

@@ -0,0 +1,105 @@
+# Motor-CAD 手动操作指南 — 轴向磁拉力求解与看结果
+
+适用: Motor-CAD 2026R1 (v261), 模型 MARS-12S10P (Axial Flux BPM, SSSR)。
+说明: 核心开关与数据位置均经脚本实测 (变量名在括号中注明); 个别 GUI 菜单
+措辞可能因版本略有出入, 以实际界面为准。
+
+## 第 1 步 启动与打开模型
+
+1. 启动: 开始菜单 "Ansys Motor-CAD 2026 R1",或直接运行
+   `E:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe`。
+2. File → Open,选 `MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot`。
+3. **立刻 File → Save As 另存副本** (如 output_motorcad/ 下带日期的文件名),
+   避免污染原模型 —— 项目纪律。
+4. 确认左上模型类型为 BPM / Axial Flux (基于 YASA 模板)。
+
+## 第 2 步 核对几何 (Geometry 页)
+
+- Radial / Axial 两个视图切换查看。
+- 关键值: 气隙 Airgap=1mm; 磁钢轴向厚 **Magnet Length=3mm**
+  (注意 Magnet Thickness=13mm 是磁钢环径向深度, 别改错); 极弧
+  Magnet Arc [ED]=121° (=67.2% 覆盖率); 定子内/外径 50/76mm。
+
+## 第 3 步 设置工况 (E-Magnetics 界面 → Input Data)
+
+1. **电流**: 本模型电流定义是 RMS 口径 (CurrentDefinition=1) ——
+   改电流填 "RMS Current" 框 (当前 21A);**填 Peak Current 无效**,这是
+   实测踩过的坑。相位角 Phase Advance = 0。转速 5000 rpm。
+2. **温度**: Temperatures 页 → Magnet Temperature。模型默认 **100°C**
+   (热态, 轴向力 343N);要对标解析报告的冷态就改 20°C (轴向力 417N)。
+   F ∝ Br², Br 温度系数 −0.12%/K。
+
+## 第 4 步 打开力计算 (Calculation 页, 已按 2026R1 实际界面核对)
+
+Calculation 页右侧 **Performance Tests** 栏里, "Electromagnetic Forces"
+复选框出现两次, 分别在两个分组里, **两个都要勾**:
+- **Open Circuit 分组** → ✅ Electromagnetic Forces
+  (=ElectromagneticForcesCalc_OC, 空载力)
+- **On Load 分组** → ✅ Electromagnetic Forces
+  (=ElectromagneticForcesCalc_Load, 负载力)
+- On Load 分组的 Torque 保持勾选。
+
+注意没有单独叫 "Open Circuit" 的选项 —— 是 OC/On Load 两个分组各有一个
+同名复选框, 容易只勾到一个。不勾就没有对应工况的力结果; 两个都勾则一次
+求解空载/负载力全出。
+
+同页顺带核对: 左侧 Drive 栏 Line Current Definition=RMS、RMS Current=21、
+Phase Advance=0、Shaft Speed=5000; 中间 Temperatures 栏 Magnet
+Temperature (100=热态基线 / 20=对标解析报告的冷态); Skew 栏 Rotor
+slices=2 (即两个径向切片)。
+
+## 第 5 步 求解
+
+点 **Solve E-Magnetic Model**。本机实测约 90~110 秒。底部状态栏会依次
+显示: Open Circuit Force Calculation → Back EMF → Transient Torque →
+2D FFT (力谐波后处理) → Solving Completed。
+
+## 第 6 步 看结果
+
+| 想看什么 | 位置 | 说明 |
+|---|---|---|
+| 转矩/电压/损耗汇总 | Output Data 页 (顶部标签) | 平均转矩约 0.52 Nm @21A |
+| 转矩/反电动势/气隙磁密波形 | Graphs 页 (顶部标签) | 气隙磁密峰值 ~0.85T (20°C) |
+| **轴向力 (本项目主角)** | **Forces 页 (顶部标签, Sensitivity 左边)** | 图名含 "Axial Force"; 按 Rotor/Stator、OL/OC、切片选择 |
+| 磁密云图/饱和检查 | FEA 页 | 逐时间步查看 |
+| 全部图名清单 (脚本用) | Help → Graph Viewer | 自动化图名从这里查 |
+| 全部变量名 (脚本用) | Help → Automation Parameter Names (F2) | |
+
+**Forces 页读图指南 (2026R1 实际界面, 已实测核对)**:
+- 左上 Force Type: **Rotor (Axial)** = 转子轴向力 (报告主口径);
+  Calculation 下拉切 On Load / Open Circuit; Angle 下拉选圆周位置。
+- 子页: Time Harmonics 1D = 单点力密度(N/m²)时间波形+时间谐波;
+  Space Harmonics 1D = 空间分布+空间谐波; **Time Domain 2D = 空间×时间
+  全矩阵 (脚本读的数据源)**; Frequency Domain 2D = 2D FFT; Polar Plot。
+- **GUI 不直接显示净合力标量** (343/416N 这类数), 须对节点求和 —— 用
+  `axial_force_final.py`。快速核对: Time Harmonics 的 0 阶(DC)幅值 ≈ 平均
+  轴向力密度, ×定子齿面有效面积 (~全环 2573mm²×0.7) ≈ 净力量级。
+- 单点力密度的 2 阶(2倍电频)脉动幅值可与 DC 同量级, 但各节点相位沿圆周
+  错开、求和后抵消 → 净合力纹波仅 ~1%。轴承看净力 (平稳); 定子盘
+  局部振动/NVH 看这里的 2 阶分布激励。
+
+**看轴向力的关键须知**:
+1. AFM 的 2.5D 展开模型沿用径向机命名 —— 界面上的 **"Radial/法向" 力
+   (Fr) 就是轴向力**, Tangential (Ft) 是切向力。
+2. GUI 显示的是**每节点集中力** (Lumped, 单位 N): 转子 10 节点 (36° 步距)、
+   定子 12 节点 (30° 步距), 分两个径向切片 (r=28.25 / 34.75 mm)。
+3. **GUI 不直接给净合力** —— 净轴向力 = 两切片所有节点求和 (0°/360° 是同
+   一节点, 只算一次)。手工核对: 转子切片1 每节点均值 ~14.9N ×10 + 切片2
+   ~20.8N ×10 ≈ 357N (t=0 快照); 精确合力用脚本
+   `python -X utf8 axial_force_final.py` (自动求和+校核, 见 REPRODUCE.md)。
+4. 定子侧数值为负 (反作用力), 与转子合力大小近似相等 (差 ~1.7%, 离散误差)。
+
+## 第 7 步 (可选) 气隙敏感性 / 磁负刚度
+
+改 Geometry 页 Airgap (如 0.6 / 1.0 / 1.5mm) 分别重解, 记录净轴向力:
+20°C 实测 513 / 417 / 327 N, 中心差分得 kneg ≈ 207 N/mm @1mm ——
+气隙越小吸力越大 (正反馈), 轴承/结构轴向刚度须 >10×kneg。
+
+## 参考结果速查 (本项目已验证)
+
+| 工况 | 净轴向力 (转子, 指向定子) |
+|---|---|
+| 100°C 热态, 1.0mm, 空载 | 342.9 N (纹波峰峰 3.5N) |
+| 100°C 热态, 1.0mm, 负载 21A | 343.2 N (电流影响 +0.1%) |
+| 20°C 冷态, 1.0mm, 空载 | 416.5 N |
+| 20°C 冷态, 0.6 / 1.5mm | 513.1 / 326.7 N |

+ 143 - 0
axial_mag_pull-master/axial_mag_pull/docs/KNOWLEDGE_BASE.md

@@ -0,0 +1,143 @@
+# 知识库 — Motor-CAD 轴向磁通电机轴向磁拉力仿真
+
+> 本文件是项目的核心知识沉淀,供人和任何 AI 工具阅读使用。
+> 全部结论均经实测验证 (2026-08-25, Motor-CAD 2026R1);每条坑都真实踩过。
+> 入口文件: 仓库根 AGENTS.md (AI) / README.md (人)。
+
+## 1. 环境事实 (本机实测)
+
+| 项 | 值 |
+|---|---|
+| Motor-CAD | 2026R1 (v261); 原机: `E:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe`; 复现机(2026-08-26): `D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe` |
+| 定位方式 | 环境变量 `MOTORCAD_ACTIVEX` → activex.bat → exe 路径 (PyMotorCAD 自动发现); activex.bat 在 GUI 内 Defaults→Automation→Update to Current Version 后生成于 `%APPDATA%\Ansys\v261\motorcad\activex.bat`; 未注册时脚本内用 `set_motorcad_exe()` 回退 (axial_force_final.py 已内置, `MOTORCAD_EXE` 可覆盖) |
+| Python | 3.12 (系统), `pip install ansys-motorcad-core`; **或直接用 Motor-CAD 自带 Python** (含 pymotorcad 0.8.4, 免安装): `<安装根>\v261\motorcad\Python\Python\python.exe` |
+| 许可证 | FlexNet: `ANSYSLMD_LICENSE_FILE=1055@localhost` (机器级环境变量), lmgrd + **ansyslmd vendor daemon 都必须在跑**; 无许可时 Motor-CAD 启动后 ~30s 静默退出, pymotorcad 报 psutil.NoSuchProcess —— 见此报错先查 ANSYS License Management Center (http://localhost:1084) |
+| 非登录 shell 陷阱 | Kimi/Cursor 等 AI 工具的 shell 可能**不继承机器级环境变量** (MOTORCAD_ACTIVEX / ANSYSLMD_LICENSE_FILE 为空); 运行前 `export MOTORCAD_ACTIVEX="%APPDATA%\Ansys\v261\motorcad\activex.bat"` 和 `export ANSYSLMD_LICENSE_FILE=1055@localhost` |
+| 单次电磁求解耗时 | 88~112 s (原机) / 151 s (2026-08-26 复现机) |
+| PDF 排版 | Edge 无头: `msedge.exe --headless=new --print-to-pdf=...` (中文文件名会失败, 用 ASCII 名; 用 cmd /c 包住避免 PS 5.1 stderr 包装) |
+| PDF 目检 | `pip install pypdfium2 pillow`, 渲染 PNG 后人工/AI 看图 |
+| 注意 | python 脚本退出后 Motor-CAD 进程可能随之退出, 属正常 |
+| 窗口不可见陷阱 | pymotorcad 用 `MotorCAD.exe /PORT=n /SCRIPTING` 启动, 该模式**默认主窗口隐藏** (2026-08-26 双机 Win32 API 实测: 主窗口 IsWindowVisible=False, 任务栏图标是坐标 −32000 的代理窗口, 故"有图标但点不开")。脚本已在启动后调 `mc.set_visible(True)` 强制可见 (实测有效: 调后主窗口可见且在屏内); 对已在运行的隐形实例跑 `python show_motorcad.py` 召回 (attach 不新开实例、不打断求解)。若可见后仍找不到 = 真·挪到屏幕外: 点任务栏图标 → Win+↑ 最大化拉回 |
+
+## 2. 核心方法: AFM 轴向力的唯一数据口
+
+Motor-CAD 2026R1 对轴向磁通电机 (AFM) 的轴向力**没有输出变量、没有 2D
+结果图、chm 帮助全文无 "Axial Force"** (110MB chm 反编译验证过)。唯一入口:
+
+```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=空载开路 —— **一次求解两者全出**, 无需改电流跑两遍。
+- 单位: N/节点/切片。节点数 = 转子极数 / 定子槽数 (本模型 10/12),
+  x 为圆周角, **0° 与 360° 是同一节点, 求和须去重** (判据:
+  `x[-1]-x[0]==360` 则丢末点)。
+- **净轴向力 = 对全部去重节点求和, 再对全部径向切片求和**。
+  切片中心半径在 .mot 的 `AFM_SectionCentreRadius_Array[i]`。
+- 求解前必须打开开关 (默认关, 关了没有任何力结果):
+  `ElectromagneticForcesCalc_Load=True`, `ElectromagneticForcesCalc_OC=True`
+- 求解: `mc.do_magnetic_calculation()`。
+
+## 3. .mot 参数语义 (AFM 模板, 易错!)
+
+| 参数 | 语义 | 本模型值 |
+|---|---|---|
+| `Magnet_Length` | **磁钢轴向厚度** | 3 mm |
+| `Magnet_Thickness` | 磁钢环**径向深度** (=(D_out−D_in)/2), 不是厚度! | 13 mm |
+| `Magnet_Arc_[ED]` | 极弧 (电角度) | 121° (=24.2°mech, 67.2% 覆盖) |
+| `Pole_Arc` | 其它转子类型参数, **本模型不生效** | 150 (勿误读) |
+| `CurrentDefinition` | 1=RMS 口径 → **改电流设 `RMSCurrent`**, 改 `PeakCurrent` 无效且不报错 | 1 |
+| `Magnet_Temperature` | 磁钢温度, **模型默认 100°C (热态)** | 100 |
+| `Magnet_Br_at_20` / `Magnet_TBr_Coeff` | Br 及温度系数 | 1.31 T / −0.12 %/K |
+| `AFM_SectionCentreRadius_Array` | 径向切片中心半径 | 28.25 / 34.75 mm |
+
+温度定律 F∝Br² 经实测精确成立 (100°C 实测 343N ↔ 由 20°C 折算 340N, <1%)。
+
+## 4. 图名/变量名探测技术 (通用, 遇到未知输出时用)
+
+1. **报错文案筛查** (无需求解, 秒级): `get_magnetic_graph_point(名, 0)` —
+   "Graph name does not exist" = 图不存在; "No points exist" = 存在但未求解。
+   注意: 某些图 (力图) 需先打开对应计算开关才注册。
+2. **图 ID 枚举**: graph 参数可传 int; 求解后逐 ID 读波形按量级辨认
+   (本模型 id17=总转矩, id16=RMS 电流常量, id0-2=相电流)。
+3. **权威清单**: GUI 内 Help → Graph Viewer (全部图名);
+   Help → Automation Parameter Names / F2 (全部变量名)。
+4. **exe 字符串挖掘**: MotorCAD.exe 的 UTF-16 字符串含图名后缀 (如
+   `_Axial_Force_Rotor`)、GUI 文案, PowerShell 正则可提取。
+5. Motor-CAD 消息日志 (`<模型名>\MessageLogs\*.txt`) 记录每次 pymotorcad
+   调用, 但**重复错误行会被抑制**, 缺行不代表没调用。
+6. pymotorcad 的 `get_magnetic_graph`(整线 API) 在 v261 上不可靠,
+   **用 `get_magnetic_graph_point` 逐点读**。
+
+## 5. 新模型仿真工作流 (SOP)
+
+1. 新模型 .mot 放仓库根, `git add` + **运行前先 commit**。
+2. 复制/修改 `axial_force_final.py`: 改 `MOT_SRC`; 从新 .mot 里 grep
+   `AFM_SectionCentreRadius_Array` 更新 `SEC_RADII_MM`; 节点数自动探测无需改。
+3. 确认工况: 磁钢温度 (热态 100°C / 冷态 20°C, 按报告口径选)、RMS 电流、气隙。
+4. 运行: `python -X utf8 axial_force_final.py` (Motor-CAD **前台**弹出,
+   ~2 分钟; `--quit` 自动关闭)。
+5. **三判据全过才采信** (脚本自动输出到 results JSON):
+   - 作用-反作用: 定/转子合力反号, 偏差 <5%
+   - 转矩交叉: Σ(Ft×r) vs 转矩图, 偏差 <10%
+   - 解析量级: B²A/2μ0 与 FEA 同量级 (解析偏高 ~1.15~1.2× 属正常)
+6. 结果记录纪律 (见 §7): 时间戳 + 简要说明 + git 提交。
+7. 需要气隙敏感性/磁负刚度: 参照 `axial_compare.py` (扫气隙+有限差分)。
+8. 报告: 参照 `docs/report_axialforce_V2.html` 改数据数组 →
+   Edge 无头出 PDF → 文件名带新版本号, 旧版保留。
+
+## 6. 已有成果与对照 (2026-08-25 定版)
+
+模型 `MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot` (12槽10极 SSSR, 气隙1mm,
+磁钢 3mm/67.2%, 5000rpm, 21A RMS):
+
+| 工况 | 转子净轴向力 (指向定子) | 数据文件 |
+|---|---|---|
+| 100°C, 1.0mm, 空载 | 342.9 N (纹波峰峰 3.5N) | axialforce_final_0825_212518.json |
+| 100°C, 1.0mm, 负载 21A | 343.2 N (+0.1%) | 同上 |
+| 20°C, 0.6/1.0/1.5mm 空载 | 513.1 / 416.5 / 326.7 N | compare_results_0825_215456.json |
+| 磁负刚度 @1mm, 20°C | ~207 N/mm (中心差分) | 同上 |
+
+对照《轴向磁拉力计算与轴承选型校核报告 V3.0-20260826》(解析法, 仓库根):
+**同温度基准下 FEA/解析 = 0.85~0.86 (三气隙稳定)** —— 磁路法系统性偏高
+~16% (不计齿槽/边缘/侧漏), 结论"对得上"; 轴承结论链 (708AC 疲劳寿命不足
+→ 推荐 7004AC) 双方互证。唯一修正建议: 600N 设计载荷依据应为
+"低温+气隙公差包络 (最恶劣 −20°C+0.6mm ≈563N)", 而非"电枢反应+20%"
+(FEA 实测仅 +0.07%)。详见 docs/COMPARISON_V3.md。
+
+### 复现验证 (2026-08-26, 第二台机器)
+
+另一台机器 (Motor-CAD 装于 D: 盘, 无 MOTORCAD_ACTIVEX 初始注册, 需先修许可)
+按 SOP 原样重跑 `axial_force_final.py`, 求解 150.9 s:
+
+| 工况 | 转子净轴向力 | 与 08-25 定版比对 |
+|---|---|---|
+| 100°C, 1.0mm, 空载 | 342.9 N (纹波峰峰 3.53 N) | **完全一致** |
+| 100°C, 1.0mm, 负载 21A | 343.2 N (纹波峰峰 3.87 N) | **完全一致** |
+
+三判据全过: 作用-反作用偏差 1.70%/1.69% (<5%), 转矩交叉 0.505 vs
+0.522 Nm (3.2%, <10%), 解析 485 N ≈ 1.41× FEA (同量级)。
+数据: axialforce_final_0826_130316.json。**结论: 方法与结果跨机可复现。**
+
+## 7. 项目纪律 (硬性)
+
+1. **每次运行仿真前 git commit** (脚本改动先入库再跑)。
+2. **结果与报告必须带时间戳 + 简要说明, 并 git 提交**: 结果 JSON/CSV 文件
+   名含时间戳 (脚本自动); 提交信息写清"什么工况、什么结果、为什么跑";
+   报告文件名带版本号 (V1/V2...), **出新版保留旧版不覆盖**。
+3. Motor-CAD **前台运行**, 跑完保持打开供人工检查 (`--quit` 显式关闭)。
+4. 生成物 (output_motorcad/, *.log) 不入库; 结果 JSON 的关键数值要转录进
+   RESULTS.md / 报告 (入库的文档) 里。
+5. 对话与决策带时间戳记入 docs/CONVERSATION_LOG.md。
+6. 原始 .mot 只读; 一切修改在另存的时间戳副本上进行。
+
+## 8. 探索过程档案 (为什么是这个方法)
+
+5 轮探测的完整过程见 git 历史 (dc56046→22658e4) 与
+docs/CONVERSATION_LOG.md。速览: 猜变量名(败) → 报错文案筛查法(建立) →
+114 图名全不存在 → 图 ID 枚举锁定 51 条波形无一像力 → 开力开关后按径向机
+命名惯例猜 3D 图 → `Fr_Rotor_OL_Lumped` 命中。教训: 未知输出先建立
+"存在性探测"手段, 再穷举; 对照组 (已知图) 必不可少。

+ 319 - 0
axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V1.html

@@ -0,0 +1,319 @@
+<title>MARS 轴向磁拉力报告</title>
+<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500;700&display=swap">
+<style>
+:root{
+  color-scheme:light;
+  --bg:#fafbfc; --surface:#ffffff; --line:#dde3e9; --grid:#eceff3;
+  --ink:#12181f; --ink-2:#4b5663; --ink-3:#7b8794;
+  --accent:#2a78d6; --s-oc:#2a78d6; --s-ol:#eb6834;
+  --sec1:#2a78d6; --sec2:#8fb8e8; --sec1n:#eb6834; --sec2n:#f4ac8b;
+  --ok:#008300; --tile:#f2f5f9;
+}
+@media (prefers-color-scheme: dark){
+  :root:where(:not([data-theme="light"])){
+    color-scheme:dark;
+    --bg:#15181c; --surface:#1c2026; --line:#333a43; --grid:#262c34;
+    --ink:#f2f4f7; --ink-2:#b6bec8; --ink-3:#87909b;
+    --accent:#3987e5; --s-oc:#3987e5; --s-ol:#d95926;
+    --sec1:#3987e5; --sec2:#6ea3dd; --sec1n:#d95926; --sec2n:#c98a6b;
+    --ok:#41a341; --tile:#232830;
+  }
+}
+:root[data-theme="dark"]{
+  color-scheme:dark;
+  --bg:#15181c; --surface:#1c2026; --line:#333a43; --grid:#262c34;
+  --ink:#f2f4f7; --ink-2:#b6bec8; --ink-3:#87909b;
+  --accent:#3987e5; --s-oc:#3987e5; --s-ol:#d95926;
+  --sec1:#3987e5; --sec2:#6ea3dd; --sec1n:#d95926; --sec2n:#c98a6b;
+  --ok:#41a341; --tile:#232830;
+}
+*{box-sizing:border-box}
+body{margin:0;background:var(--bg);color:var(--ink);
+  font:16px/1.65 "IBM Plex Sans","Noto Sans SC",system-ui,sans-serif;}
+.wrap{max-width:960px;margin:0 auto;padding:40px 24px 72px;display:flex;
+  flex-direction:column;gap:36px}
+header .eyebrow{font:500 12px/1 "IBM Plex Mono",monospace;color:var(--accent);
+  letter-spacing:.14em;text-transform:uppercase;margin-bottom:10px}
+h1{font:700 30px/1.25 "Noto Sans SC","IBM Plex Sans",sans-serif;margin:0 0 10px;
+  text-wrap:balance}
+h2{font:700 19px/1.3 "Noto Sans SC",sans-serif;margin:0 0 4px}
+.sub{color:var(--ink-2);margin:0;max-width:64ch}
+.meta{display:flex;flex-wrap:wrap;gap:8px 20px;margin-top:14px;
+  font:400 13px/1.5 "IBM Plex Mono",monospace;color:var(--ink-3)}
+.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px}
+.tile{background:var(--tile);border:1px solid var(--line);border-radius:8px;
+  padding:16px 18px}
+.tile .k{font:500 12.5px/1.4 "Noto Sans SC",sans-serif;color:var(--ink-2)}
+.tile .v{font:600 28px/1.15 "IBM Plex Sans",sans-serif;
+  font-variant-numeric:tabular-nums;margin-top:6px}
+.tile .v small{font-size:15px;font-weight:500;color:var(--ink-2);margin-left:2px}
+.tile .n{font-size:12.5px;color:var(--ink-3);margin-top:4px}
+.card{background:var(--surface);border:1px solid var(--line);border-radius:10px;
+  padding:22px 24px;display:flex;flex-direction:column;gap:12px}
+.card p{margin:0;color:var(--ink-2);max-width:70ch}
+.legend{display:flex;gap:18px;flex-wrap:wrap;font-size:13.5px;color:var(--ink-2)}
+.legend span{display:inline-flex;align-items:center;gap:7px}
+.sw{width:14px;height:4px;border-radius:2px;display:inline-block}
+.chartbox{position:relative;overflow-x:auto}
+svg text{font:11.5px "IBM Plex Mono",monospace;fill:var(--ink-3)}
+svg .axis-t{font:12px "Noto Sans SC",sans-serif;fill:var(--ink-2)}
+.tip{position:absolute;pointer-events:none;background:var(--surface);
+  border:1px solid var(--line);border-radius:6px;padding:7px 10px;
+  font:12.5px/1.5 "IBM Plex Mono",monospace;color:var(--ink);
+  box-shadow:0 4px 14px rgba(0,0,0,.12);display:none;white-space:nowrap;z-index:3}
+table{border-collapse:collapse;width:100%;font-size:14.5px}
+th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--grid);
+  vertical-align:top}
+th{font:600 12.5px/1.4 "Noto Sans SC",sans-serif;color:var(--ink-3);
+  letter-spacing:.05em}
+td.num{font-family:"IBM Plex Mono",monospace;font-variant-numeric:tabular-nums;
+  white-space:nowrap}
+.pass{color:var(--ok);font-weight:600}
+ol,ul{margin:0;padding-left:22px;color:var(--ink-2)}
+li{margin:5px 0}
+code,pre{font:13px/1.6 "IBM Plex Mono",monospace}
+code{background:var(--tile);border:1px solid var(--line);border-radius:4px;
+  padding:1px 6px}
+pre{background:var(--tile);border:1px solid var(--line);border-radius:8px;
+  padding:14px 16px;overflow-x:auto;margin:0}
+pre code{background:none;border:none;padding:0}
+.note{font-size:13px;color:var(--ink-3)}
+footer{font:12.5px/1.7 "IBM Plex Mono",monospace;color:var(--ink-3);
+  border-top:1px solid var(--line);padding-top:16px}
+@media print{
+  :root{color-scheme:light}
+  body{background:#ffffff}
+  .wrap{max-width:none;padding:0 0 24px;gap:22px}
+  .card,.tiles,header{break-inside:avoid}
+  .card{border-color:#c9d1d9}
+  .chartbox{overflow:visible}
+  .tip{display:none !important}
+  svg{max-width:100%;height:auto}
+}
+</style>
+<div class="wrap">
+
+<header>
+  <div class="eyebrow">Motor-CAD 2026R1 · E-Magnetic 2.5D FEA</div>
+  <h1>MARS-12S10P 单定子单转子电机 轴向磁拉力仿真报告</h1>
+  <p class="sub">单边轴向磁通结构(SSSR)定转子之间存在固有的不平衡轴向吸力。本报告给出该力的大小、纹波与空间分布,供轴承选型与结构设计使用。</p>
+  <div class="meta">
+    <span>模型 MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot</span>
+    <span>12槽 / 10极 · 气隙 1 mm · 磁体 13 mm</span>
+    <span>5000 rpm · 相电流 21 A(RMS) · 相位角 0°</span>
+    <span>仿真日期 2026-08-25</span>
+  </div>
+</header>
+
+<section class="tiles">
+  <div class="tile"><div class="k">空载净轴向力(开路)</div>
+    <div class="v">342.9<small>N</small></div>
+    <div class="n">转子被吸向定子</div></div>
+  <div class="tile"><div class="k">负载净轴向力(21 A RMS)</div>
+    <div class="v">343.2<small>N</small></div>
+    <div class="n">电流影响仅 +0.1%</div></div>
+  <div class="tile"><div class="k">力纹波(峰峰值)</div>
+    <div class="v">3.9<small>N</small></div>
+    <div class="n">约为均值的 1.1%</div></div>
+  <div class="tile"><div class="k">等效气隙磁压</div>
+    <div class="v">133<small>kPa</small></div>
+    <div class="n">对应 B<sub>eff</sub> ≈ 0.58 T,物理自洽</div></div>
+</section>
+
+<section class="card">
+  <div>
+    <h2>净轴向力随转子位置的变化</h2>
+    <p>一个电周期(31 个时间步)内转子所受净轴向力。空载与负载曲线几乎重合——该力由磁钢对定子铁芯的吸力主导,电枢电流主要产生切向力。</p>
+  </div>
+  <div class="legend">
+    <span><i class="sw" style="background:var(--s-oc)"></i>空载(开路)</span>
+    <span><i class="sw" style="background:var(--s-ol)"></i>负载 21 A RMS</span>
+  </div>
+  <div class="chartbox"><svg id="lineChart" width="880" height="300"
+    viewBox="0 0 880 300" role="img"
+    aria-label="净轴向力波形:空载均值342.9牛,负载均值343.2牛"></svg>
+    <div class="tip" id="lineTip"></div></div>
+  <p class="note">注:纵轴为 338–346 N 局部放大以显示纹波;力的绝对量级见上方汇总卡。轴承按 ~345 N 静态轴向预载考虑即可,纹波幅值很小(12/10 齿槽配合)。</p>
+</section>
+
+<section class="card">
+  <div>
+    <h2>节点轴向力分布(t = 0)</h2>
+    <p>力在圆周方向的分布均匀(各极/各齿量级一致、无单边偏载),两径向切片中外侧切片(r = 34.75 mm)承担更大份额——面积更大、线速度更高,符合预期。定子侧为反作用力,方向相反。</p>
+  </div>
+  <div class="legend">
+    <span><i class="sw" style="background:var(--sec1);height:10px;border-radius:3px"></i>切片 1(r = 28.25 mm)</span>
+    <span><i class="sw" style="background:var(--sec2);height:10px;border-radius:3px"></i>切片 2(r = 34.75 mm)</span>
+  </div>
+  <div class="chartbox"><svg id="barChart" width="880" height="360"
+    viewBox="0 0 880 360" role="img"
+    aria-label="转子10个节点与定子12个节点的轴向力分布,转子向上、定子向下"></svg>
+    <div class="tip" id="barTip"></div></div>
+</section>
+
+<section class="card">
+  <div><h2>结果校核(三项全部通过)</h2></div>
+  <table>
+    <thead><tr><th>校核项</th><th>数值</th><th>判据与结论</th></tr></thead>
+    <tbody>
+      <tr><td>作用力–反作用力</td>
+        <td class="num">转子 +343.2 N ↔ 定子 −337.4 N</td>
+        <td>偏差 1.7%(定/转子节点离散数不同:12 vs 10),<span class="pass">通过</span></td></tr>
+      <tr><td>转矩交叉核对</td>
+        <td class="num">Σ(F<sub>t</sub>·r) = 0.505 N·m ↔ 转矩图 0.522 N·m</td>
+        <td>偏差 3%,切向力与转矩输出互洽,<span class="pass">通过</span></td></tr>
+      <tr><td>解析量级核对</td>
+        <td class="num">B²A/2μ₀ = 485 N ↔ FEA 343 N</td>
+        <td>同量级(解析式取全环面积与气隙 B² 均值,系统性偏大,比值 0.71 合理),<span class="pass">通过</span></td></tr>
+    </tbody>
+  </table>
+</section>
+
+<section class="card">
+  <div>
+    <h2>方法说明(关键结论,供复用)</h2>
+    <p>Motor-CAD 2026R1 对轴向磁通电机(AFM)的轴向力<strong style="color:var(--ink)">没有输出变量、没有 2D 结果图、帮助文档未收录</strong>。数据入口是 3D 集中节点力图:</p>
+  </div>
+  <pre><code>get_magnetic_3d_graph_point("Fr_{Rotor|Stator}_{OL|OC}_Lumped", 切片号, 节点号, 时间步)</code></pre>
+  <ul>
+    <li>AFM 的 2.5D 展开模型沿用径向电机命名:<strong style="color:var(--ink)">Fr(法向力)即轴向力</strong>,Ft 为切向力;单位 N/节点/切片。</li>
+    <li>求解前须打开 <code>ElectromagneticForcesCalc_Load</code> 与 <code>_OC</code>;一次求解同时得到空载(OC)与负载(OL)。</li>
+    <li>节点:转子 10 个(36° 步距)、定子 12 个(30° 步距),首尾(0°/360°)为同一节点须去重;对节点与两切片求和得净力。</li>
+    <li>本模型电流为 RMS 口径(<code>CurrentDefinition=1</code>):改电流要设 <code>RMSCurrent</code>,改 <code>PeakCurrent</code> 无效。</li>
+  </ul>
+</section>
+
+<section class="card">
+  <div>
+    <h2>同事复现指南</h2>
+    <p>整个项目由 git 管理,脚本一条命令跑完(约 2 分钟),原始 .mot 不会被修改。</p>
+  </div>
+  <ol>
+    <li><strong style="color:var(--ink)">环境</strong>:Windows + Motor-CAD 2026R1(安装器会设好 <code>MOTORCAD_ACTIVEX</code> 环境变量);Python ≥ 3.10,执行 <code>pip install ansys-motorcad-core</code>。</li>
+    <li><strong style="color:var(--ink)">获取项目</strong>:复制整个 <code>motionpushpull</code> 目录(含 .git)。</li>
+    <li><strong style="color:var(--ink)">运行</strong>:<code>python -X utf8 axial_force_final.py</code>——Motor-CAD 前台弹出,求解约 88 s,结束后保持打开供检查(加 <code>--quit</code> 自动关闭)。</li>
+    <li><strong style="color:var(--ink)">读结果</strong>:控制台 [结论] 两行;波形与校核在 <code>output_motorcad/axialforce_final_*.json</code> 与 <code>axial_force_*.csv</code>。</li>
+    <li><strong style="color:var(--ink)">判定</strong>:作用–反作用偏差 &lt;5%、转矩交叉 &lt;10%、解析同量级,三项都过才采信。</li>
+  </ol>
+  <p class="note">换其它 AFM 模型:改脚本顶部 <code>MOT_SRC</code>;若切片数/半径不同,按 .mot 中 <code>AFM_SectionCentreRadius_Array</code> 更新 <code>SEC_RADII_MM</code>。探测脚本 axial_probe*.py 是找数据口的历史过程,复现时无需运行。详见仓库 <code>REPRODUCE.md</code>。</p>
+</section>
+
+<footer>
+  数据:output_motorcad/axialforce_final_0825_212518.json · git: deb6b65(脚本)/ 4cee88c(结果)· 工具链:PyMotorCAD + Motor-CAD 2026R1(前台)
+</footer>
+</div>
+
+<script>
+(function(){
+"use strict";
+var OC=[344.30,342.75,341.97,341.22,340.77,341.67,341.11,341.92,342.56,342.97,344.21,343.47,343.62,343.40,343.03,343.52,342.36,342.56,342.48,342.48,343.48,342.96,343.40,343.62,343.45,344.22,343.17,343.13,342.91,342.65,343.51];
+var OL=[344.91,343.31,342.28,341.62,341.04,342.06,341.49,342.09,342.82,343.25,344.64,343.78,343.70,343.55,343.06,343.77,342.65,342.72,342.82,342.80,343.97,343.47,343.74,344.03,343.84,344.77,343.70,343.49,343.39,343.08,344.03];
+var css=function(n){return getComputedStyle(document.documentElement).getPropertyValue(n).trim();};
+var NS="http://www.w3.org/2000/svg";
+function el(p,t,a){var e=document.createElementNS(NS,t);for(var k in a)e.setAttribute(k,a[k]);p.appendChild(e);return e;}
+
+/* ---- 折线图 ---- */
+function drawLine(){
+  var svg=document.getElementById("lineChart");if(!svg)return;
+  svg.innerHTML="";
+  var W=880,H=300,L=64,R=16,T=14,B=40;
+  var y0=338,y1=346;
+  var X=function(i){return L+(W-L-R)*i/30;};
+  var Y=function(v){return T+(H-T-B)*(1-(v-y0)/(y1-y0));};
+  for(var g=y0;g<=y1;g+=2){
+    el(svg,"line",{x1:L,x2:W-R,y1:Y(g),y2:Y(g),stroke:css("--grid"),"stroke-width":1});
+    el(svg,"text",{x:L-10,y:Y(g)+4,"text-anchor":"end"}).textContent=g;
+  }
+  for(var d=0;d<=360;d+=90){
+    var x=L+(W-L-R)*d/360;
+    el(svg,"text",{x:x,y:H-B+20,"text-anchor":"middle"}).textContent=d+"°";
+  }
+  el(svg,"text",{x:L-46,y:T+((H-T-B)/2),transform:"rotate(-90 "+(L-46)+" "+(T+(H-T-B)/2)+")","text-anchor":"middle","class":"axis-t"}).textContent="净轴向力 (N)";
+  el(svg,"text",{x:L+(W-L-R)/2,y:H-6,"text-anchor":"middle","class":"axis-t"}).textContent="转子位置(电角度)";
+  function poly(data,color){
+    var pts=data.map(function(v,i){return X(i)+","+Y(v);}).join(" ");
+    el(svg,"polyline",{points:pts,fill:"none",stroke:color,"stroke-width":2,"stroke-linejoin":"round","stroke-linecap":"round"});
+  }
+  poly(OC,css("--s-oc"));poly(OL,css("--s-ol"));
+  el(svg,"text",{x:W-R-4,y:Y(OL[30])-12,"text-anchor":"end",fill:css("--s-ol"),"font-weight":"500"}).textContent="负载 343.2 N";
+  el(svg,"text",{x:W-R-4,y:Y(OC[30])+26,"text-anchor":"end",fill:css("--s-oc"),"font-weight":"500"}).textContent="空载 342.9 N";
+  var cross=el(svg,"line",{y1:T,y2:H-B,stroke:css("--ink-3"),"stroke-width":1,"stroke-dasharray":"3 3",visibility:"hidden"});
+  var m1=el(svg,"circle",{r:4,fill:css("--s-oc"),stroke:css("--surface"),"stroke-width":2,visibility:"hidden"});
+  var m2=el(svg,"circle",{r:4,fill:css("--s-ol"),stroke:css("--surface"),"stroke-width":2,visibility:"hidden"});
+  var tip=document.getElementById("lineTip");
+  svg.addEventListener("mousemove",function(ev){
+    var r=svg.getBoundingClientRect();
+    var px=(ev.clientX-r.left)*(W/r.width);
+    if(px<L||px>W-R){hide();return;}
+    var i=Math.round((px-L)/(W-L-R)*30);i=Math.max(0,Math.min(30,i));
+    var x=X(i);
+    cross.setAttribute("x1",x);cross.setAttribute("x2",x);cross.setAttribute("visibility","visible");
+    m1.setAttribute("cx",x);m1.setAttribute("cy",Y(OC[i]));m1.setAttribute("visibility","visible");
+    m2.setAttribute("cx",x);m2.setAttribute("cy",Y(OL[i]));m2.setAttribute("visibility","visible");
+    tip.style.display="block";
+    tip.innerHTML=(i*12)+"°<br>负载 "+OL[i].toFixed(2)+" N<br>空载 "+OC[i].toFixed(2)+" N";
+    var bx=ev.clientX-r.left,by=ev.clientY-r.top;
+    tip.style.left=Math.min(bx+14,r.width-140)+"px";tip.style.top=(by-10)+"px";
+  });
+  function hide(){cross.setAttribute("visibility","hidden");m1.setAttribute("visibility","hidden");m2.setAttribute("visibility","hidden");tip.style.display="none";}
+  svg.addEventListener("mouseleave",hide);
+}
+
+/* ---- 节点分布图 (转子上/定子下) ---- */
+var ROT1=[20.39,13.37,7.03,11.61,19.17,20.36,13.36,7.05,11.61,19.20];
+var ROT2=[26.97,18.64,13.04,16.53,25.69,26.93,18.71,13.05,16.48,25.72];
+var STA1=[-21.12,-18.29,-11.80,-8.18,-10.20,-16.80,-21.05,-18.25,-11.80,-8.17,-10.21,-16.82];
+var STA2=[-20.15,-18.71,-11.04,-5.81,-9.04,-17.28,-20.15,-18.71,-11.05,-5.79,-9.04,-17.27];
+function drawBars(){
+  var svg=document.getElementById("barChart");if(!svg)return;
+  svg.innerHTML="";
+  var W=880,H=360,L=64,R=16,T=16,B=44;
+  var maxV=50,minV=-45;
+  var Y=function(v){return T+(H-T-B)*(maxV-v)/(maxV-minV);};
+  for(var g=-40;g<=40;g+=20){
+    el(svg,"line",{x1:L,x2:W-R,y1:Y(g),y2:Y(g),stroke:g===0?css("--line"):css("--grid"),"stroke-width":1});
+    el(svg,"text",{x:L-10,y:Y(g)+4,"text-anchor":"end"}).textContent=g;
+  }
+  el(svg,"text",{x:L-46,y:T+(H-T-B)/2,transform:"rotate(-90 "+(L-46)+" "+(T+(H-T-B)/2)+")","text-anchor":"middle","class":"axis-t"}).textContent="节点轴向力 (N)";
+  var tip=document.getElementById("barTip");
+  var span=(W-L-R);
+  var AX=function(a){return L+18+(span-36)*a/360;};   /* 统一角度→x 映射 */
+  for(var td=0;td<=360;td+=90){
+    el(svg,"text",{x:AX(td),y:H-B+18,"text-anchor":"middle"}).textContent=td+"°";
+  }
+  function bars(s1,s2,n,step,c1,c2,label){
+    var bw=Math.min(24,(span-36)/12*0.5);
+    for(var i=0;i<n;i++){
+      var cx=AX(i*step);
+      var v1=s1[i],v2=s2[i],tot=v1+v2;
+      var up=tot>=0;
+      var yA=Y(0),h1=Math.abs(Y(v1)-Y(0)),h2=Math.abs(Y(v1+v2)-Y(v1));
+      var r1=el(svg,"rect",{x:cx-bw/2,y:up?Y(v1):yA+2,width:bw,height:Math.max(h1-2,1),fill:c1,rx:0});
+      var r2=el(svg,"rect",{x:cx-bw/2,y:up?Y(v1+v2):Y(v1)+2,width:bw,height:Math.max(h2-2,1),fill:c2,rx:3});
+      (function(idx,total){
+        [r1,r2].forEach(function(rc){
+          rc.style.cursor="default";
+          rc.addEventListener("mousemove",function(ev){
+            var rt=svg.getBoundingClientRect();
+            tip.style.display="block";
+            tip.innerHTML=label+" 节点 "+(idx*step)+"°<br>切片1 "+s1[idx].toFixed(1)+" N · 切片2 "+s2[idx].toFixed(1)+" N<br>合计 "+total.toFixed(1)+" N";
+            tip.style.left=Math.min(ev.clientX-rt.left+14,rt.width-190)+"px";
+            tip.style.top=(ev.clientY-rt.top-10)+"px";
+          });
+          rc.addEventListener("mouseleave",function(){tip.style.display="none";});
+        });
+      })(i,tot);
+    }
+  }
+  bars(ROT1,ROT2,10,36,css("--sec1"),css("--sec2"),"转子");
+  bars(STA1,STA2,12,30,css("--sec1n"),css("--sec2n"),"定子");
+  el(svg,"text",{x:W-R-4,y:T+12,"text-anchor":"end","class":"axis-t",fill:css("--ink-2")}).textContent="↑ 转子(10 节点,指向定子)";
+  el(svg,"text",{x:W-R-4,y:H-B-8,"text-anchor":"end","class":"axis-t",fill:css("--ink-2")}).textContent="↓ 定子(12 节点,反作用)";
+  el(svg,"text",{x:L+(W-L-R)/2,y:H-6,"text-anchor":"middle","class":"axis-t"}).textContent="圆周位置(机械角度)";
+}
+function drawAll(){drawLine();drawBars();}
+drawAll();
+if(window.matchMedia){window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",drawAll);}
+new MutationObserver(drawAll).observe(document.documentElement,{attributes:true,attributeFilter:["data-theme"]});
+})();
+</script>

BIN
axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V1.pdf


+ 388 - 0
axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V2.html

@@ -0,0 +1,388 @@
+<title>MARS 轴向磁拉力报告</title>
+<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&family=Noto+Sans+SC:wght@400;500;700&display=swap">
+<style>
+:root{
+  color-scheme:light;
+  --bg:#fafbfc; --surface:#ffffff; --line:#dde3e9; --grid:#eceff3;
+  --ink:#12181f; --ink-2:#4b5663; --ink-3:#7b8794;
+  --accent:#2a78d6; --s-oc:#2a78d6; --s-ol:#eb6834;
+  --sec1:#2a78d6; --sec2:#8fb8e8; --sec1n:#eb6834; --sec2n:#f4ac8b;
+  --ok:#008300; --tile:#f2f5f9;
+}
+@media (prefers-color-scheme: dark){
+  :root:where(:not([data-theme="light"])){
+    color-scheme:dark;
+    --bg:#15181c; --surface:#1c2026; --line:#333a43; --grid:#262c34;
+    --ink:#f2f4f7; --ink-2:#b6bec8; --ink-3:#87909b;
+    --accent:#3987e5; --s-oc:#3987e5; --s-ol:#d95926;
+    --sec1:#3987e5; --sec2:#6ea3dd; --sec1n:#d95926; --sec2n:#c98a6b;
+    --ok:#41a341; --tile:#232830;
+  }
+}
+:root[data-theme="dark"]{
+  color-scheme:dark;
+  --bg:#15181c; --surface:#1c2026; --line:#333a43; --grid:#262c34;
+  --ink:#f2f4f7; --ink-2:#b6bec8; --ink-3:#87909b;
+  --accent:#3987e5; --s-oc:#3987e5; --s-ol:#d95926;
+  --sec1:#3987e5; --sec2:#6ea3dd; --sec1n:#d95926; --sec2n:#c98a6b;
+  --ok:#41a341; --tile:#232830;
+}
+*{box-sizing:border-box}
+body{margin:0;background:var(--bg);color:var(--ink);
+  font:16px/1.65 "IBM Plex Sans","Noto Sans SC",system-ui,sans-serif;}
+.wrap{max-width:960px;margin:0 auto;padding:40px 24px 72px;display:flex;
+  flex-direction:column;gap:36px}
+header .eyebrow{font:500 12px/1 "IBM Plex Mono",monospace;color:var(--accent);
+  letter-spacing:.14em;text-transform:uppercase;margin-bottom:10px}
+h1{font:700 30px/1.25 "Noto Sans SC","IBM Plex Sans",sans-serif;margin:0 0 10px;
+  text-wrap:balance}
+h2{font:700 19px/1.3 "Noto Sans SC",sans-serif;margin:0 0 4px}
+.sub{color:var(--ink-2);margin:0;max-width:64ch}
+.meta{display:flex;flex-wrap:wrap;gap:8px 20px;margin-top:14px;
+  font:400 13px/1.5 "IBM Plex Mono",monospace;color:var(--ink-3)}
+.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:12px}
+.tile{background:var(--tile);border:1px solid var(--line);border-radius:8px;
+  padding:16px 18px}
+.tile .k{font:500 12.5px/1.4 "Noto Sans SC",sans-serif;color:var(--ink-2)}
+.tile .v{font:600 28px/1.15 "IBM Plex Sans",sans-serif;
+  font-variant-numeric:tabular-nums;margin-top:6px}
+.tile .v small{font-size:15px;font-weight:500;color:var(--ink-2);margin-left:2px}
+.tile .n{font-size:12.5px;color:var(--ink-3);margin-top:4px}
+.card{background:var(--surface);border:1px solid var(--line);border-radius:10px;
+  padding:22px 24px;display:flex;flex-direction:column;gap:12px}
+.card p{margin:0;color:var(--ink-2);max-width:70ch}
+.legend{display:flex;gap:18px;flex-wrap:wrap;font-size:13.5px;color:var(--ink-2)}
+.legend span{display:inline-flex;align-items:center;gap:7px}
+.sw{width:14px;height:4px;border-radius:2px;display:inline-block}
+.chartbox{position:relative;overflow-x:auto}
+svg text{font:11.5px "IBM Plex Mono",monospace;fill:var(--ink-3)}
+svg .axis-t{font:12px "Noto Sans SC",sans-serif;fill:var(--ink-2)}
+.tip{position:absolute;pointer-events:none;background:var(--surface);
+  border:1px solid var(--line);border-radius:6px;padding:7px 10px;
+  font:12.5px/1.5 "IBM Plex Mono",monospace;color:var(--ink);
+  box-shadow:0 4px 14px rgba(0,0,0,.12);display:none;white-space:nowrap;z-index:3}
+table{border-collapse:collapse;width:100%;font-size:14.5px}
+th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--grid);
+  vertical-align:top}
+th{font:600 12.5px/1.4 "Noto Sans SC",sans-serif;color:var(--ink-3);
+  letter-spacing:.05em}
+td.num{font-family:"IBM Plex Mono",monospace;font-variant-numeric:tabular-nums;
+  white-space:nowrap}
+table.cmp th:first-child,table.cmp td:first-child{min-width:150px}
+table.cmp th:last-child,table.cmp td:last-child{min-width:180px}
+table.cmp td.num{white-space:normal}
+.pass{color:var(--ok);font-weight:600}
+ol,ul{margin:0;padding-left:22px;color:var(--ink-2)}
+li{margin:5px 0}
+code,pre{font:13px/1.6 "IBM Plex Mono",monospace}
+code{background:var(--tile);border:1px solid var(--line);border-radius:4px;
+  padding:1px 6px}
+pre{background:var(--tile);border:1px solid var(--line);border-radius:8px;
+  padding:14px 16px;overflow-x:auto;margin:0}
+pre code{background:none;border:none;padding:0}
+.note{font-size:13px;color:var(--ink-3)}
+footer{font:12.5px/1.7 "IBM Plex Mono",monospace;color:var(--ink-3);
+  border-top:1px solid var(--line);padding-top:16px}
+@media print{
+  :root{color-scheme:light}
+  body{background:#ffffff}
+  .wrap{max-width:none;padding:0 0 24px;gap:22px}
+  .card,.tiles,header{break-inside:avoid}
+  .card{border-color:#c9d1d9}
+  .chartbox{overflow:visible}
+  .tip{display:none !important}
+  svg{max-width:100%;height:auto}
+}
+</style>
+<div class="wrap">
+
+<header>
+  <div class="eyebrow">Motor-CAD 2026R1 · E-Magnetic 2.5D FEA</div>
+  <h1>MARS-12S10P 单定子单转子电机 轴向磁拉力仿真报告</h1>
+  <p class="sub">单边轴向磁通结构(SSSR)定转子之间存在固有的不平衡轴向吸力。本报告给出该力的大小、纹波与空间分布,供轴承选型与结构设计使用。</p>
+  <div class="meta">
+    <span>模型 MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot</span>
+    <span>12槽 / 10极 · 气隙 1 mm · 磁钢轴向厚 3 mm(径向深 13 mm)· 极弧 67.2%</span>
+    <span>5000 rpm · 相电流 21 A(RMS) · 相位角 0°</span>
+    <span>报告 V2 · 2026-08-25 · 新增 V3.0 解析报告对标</span>
+  </div>
+</header>
+
+<section class="tiles">
+  <div class="tile"><div class="k">空载净轴向力(磁钢 100 ℃ 热态)</div>
+    <div class="v">342.9<small>N</small></div>
+    <div class="n">转子被吸向定子(模型默认温度)</div></div>
+  <div class="tile"><div class="k">空载净轴向力(磁钢 20 ℃ 冷态)</div>
+    <div class="v">416.5<small>N</small></div>
+    <div class="n">V2 新增;F∝B<sub>r</sub>² 定律实测偏差 &lt;1%</div></div>
+  <div class="tile"><div class="k">负载影响(21 A RMS)</div>
+    <div class="v">+0.1<small>%</small></div>
+    <div class="n">纹波峰峰 ~1.1%,磁钢主导</div></div>
+  <div class="tile"><div class="k">磁负刚度 k<sub>neg</sub>(1 mm 气隙)</div>
+    <div class="v">207<small>N/mm</small></div>
+    <div class="n">V2 新增;气隙减小→吸力增大的正反馈</div></div>
+</section>
+
+<section class="card">
+  <div>
+    <h2>净轴向力随转子位置的变化</h2>
+    <p>一个电周期(31 个时间步)内转子所受净轴向力。空载与负载曲线几乎重合——该力由磁钢对定子铁芯的吸力主导,电枢电流主要产生切向力。</p>
+  </div>
+  <div class="legend">
+    <span><i class="sw" style="background:var(--s-oc)"></i>空载(开路)</span>
+    <span><i class="sw" style="background:var(--s-ol)"></i>负载 21 A RMS</span>
+  </div>
+  <div class="chartbox"><svg id="lineChart" width="880" height="300"
+    viewBox="0 0 880 300" role="img"
+    aria-label="净轴向力波形:空载均值342.9牛,负载均值343.2牛"></svg>
+    <div class="tip" id="lineTip"></div></div>
+  <p class="note">注:本图为磁钢 100 ℃ 热态基线;纵轴为 338–346 N 局部放大以显示纹波,力的绝对量级见上方汇总卡。纹波幅值很小(12/10 齿槽配合)。轴承设计载荷取 600 N 包络(覆盖低温 + 气隙公差最恶劣组合 ≈563 N,见对标章节)。</p>
+</section>
+
+<section class="card">
+  <div>
+    <h2>节点轴向力分布(t = 0)</h2>
+    <p>力在圆周方向的分布均匀(各极/各齿量级一致、无单边偏载),两径向切片中外侧切片(r = 34.75 mm)承担更大份额——面积更大、线速度更高,符合预期。定子侧为反作用力,方向相反。</p>
+  </div>
+  <div class="legend">
+    <span><i class="sw" style="background:var(--sec1);height:10px;border-radius:3px"></i>切片 1(r = 28.25 mm)</span>
+    <span><i class="sw" style="background:var(--sec2);height:10px;border-radius:3px"></i>切片 2(r = 34.75 mm)</span>
+  </div>
+  <div class="chartbox"><svg id="barChart" width="880" height="360"
+    viewBox="0 0 880 360" role="img"
+    aria-label="转子10个节点与定子12个节点的轴向力分布,转子向上、定子向下"></svg>
+    <div class="tip" id="barTip"></div></div>
+</section>
+
+<section class="card">
+  <div>
+    <h2>对标《轴向磁拉力计算与轴承选型校核报告 V3.0》</h2>
+    <p>该解析报告(磁路模型 + Maxwell 应力,磁钢 20 ℃ 基准)给出空载 F<sub>z</sub>=483 N(中值 500 N)。将 FEA 磁钢温度归一到 20 ℃ 并扫描气隙后对比:<strong style="color:var(--ink)">三个气隙下 FEA/解析比值稳定在 0.85~0.86</strong>——解析法系统性偏高约 16%(集中参数磁路不计齿槽、边缘效应与厚磁钢侧漏的正常量级),趋势完全一致,<strong style="color:var(--ink)">对得上</strong>。</p>
+  </div>
+  <div class="legend">
+    <span><i class="sw" style="background:var(--s-oc)"></i>FEA(本报告,20 ℃)</span>
+    <span><i class="sw" style="background:var(--s-ol)"></i>解析报告 V3.0(表 3-1/3-3)</span>
+  </div>
+  <div class="chartbox"><svg id="gapChart" width="880" height="300"
+    viewBox="0 0 880 300" role="img"
+    aria-label="气隙0.6/1.0/1.5毫米下FEA与解析法轴向力对比"></svg></div>
+  <table class="cmp">
+    <thead><tr><th>对比项</th><th>解析报告 V3.0</th><th>FEA(本报告)</th><th>结论</th></tr></thead>
+    <tbody>
+      <tr><td>空载力 @1.0 mm,20 ℃</td><td class="num">483 N(中值 500 N)</td>
+        <td class="num">416.5 N</td><td>比值 0.86,<span class="pass">一致</span>(解析偏高属方法固有)</td></tr>
+      <tr><td>气隙 0.6 / 1.5 mm</td><td class="num">601 / 378 N</td>
+        <td class="num">513.1 / 326.7 N</td><td>比值 0.85 / 0.86,趋势<span class="pass">一致</span></td></tr>
+      <tr><td>磁负刚度 k<sub>neg</sub> @1 mm</td><td class="num">250 N/mm</td>
+        <td class="num">207 N/mm(中心差分)</td><td>同随 F 等比,判据结论<span class="pass">一致</span></td></tr>
+      <tr><td>温度定律 F∝B<sub>r</sub>²</td><td class="num">80 ℃ 时 −15%(推算)</td>
+        <td class="num">100 ℃ 实测 343 N ↔ 折算 340 N</td><td>偏差 &lt;1%,<span class="pass">精确成立</span></td></tr>
+      <tr><td>负载电枢反应</td><td class="num">经验取 +10~20% → 设计 600 N</td>
+        <td class="num">实测 +0.07%</td><td>机理不成立;但 600 N 包络仍稳健(最恶劣 −20 ℃ + 0.6 mm ≈ 563 N),依据应改为<strong style="color:var(--ink)">低温 + 气隙公差</strong></td></tr>
+      <tr><td>轴承结论链</td><td class="num">708AC 寿命 577 h 不满足 → 推荐 7004AC</td>
+        <td class="num">FEA 载荷代入约 1 730 h</td><td>仍远小于 20 000 h,<span class="pass">结论不变</span>,方案 A 裕度更大</td></tr>
+    </tbody>
+  </table>
+  <p class="note"><strong style="color:var(--ink)">输入核对(已闭环)</strong>:.mot 模型与解析报告描述同一套磁钢几何——模型 <code>Magnet_Length=3</code> 即磁钢轴向厚度 3 mm(<code>Magnet_Thickness=13</code> 是磁钢环径向深度 =(76−50)/2);极弧 <code>Magnet_Arc_[ED]=121°elec</code> = 24.2° 机械角 = 覆盖率 67.2%,均与报告表 2-1 一致。因此 0.85~0.86 的比值是纯方法差异(磁路法不计齿槽/边缘/侧漏,系统性偏高 ~15%),不存在参数口径问题。详见仓库 docs/COMPARISON_V3.md。</p>
+</section>
+
+<section class="card">
+  <div><h2>结果校核(三项全部通过)</h2></div>
+  <table>
+    <thead><tr><th>校核项</th><th>数值</th><th>判据与结论</th></tr></thead>
+    <tbody>
+      <tr><td>作用力–反作用力</td>
+        <td class="num">转子 +343.2 N ↔ 定子 −337.4 N</td>
+        <td>偏差 1.7%(定/转子节点离散数不同:12 vs 10),<span class="pass">通过</span></td></tr>
+      <tr><td>转矩交叉核对</td>
+        <td class="num">Σ(F<sub>t</sub>·r) = 0.505 N·m ↔ 转矩图 0.522 N·m</td>
+        <td>偏差 3%,切向力与转矩输出互洽,<span class="pass">通过</span></td></tr>
+      <tr><td>解析量级核对</td>
+        <td class="num">B²A/2μ₀ = 485 N ↔ FEA 343 N</td>
+        <td>同量级(解析式取全环面积与气隙 B² 均值,系统性偏大,比值 0.71 合理),<span class="pass">通过</span></td></tr>
+    </tbody>
+  </table>
+</section>
+
+<section class="card">
+  <div>
+    <h2>方法说明(关键结论,供复用)</h2>
+    <p>Motor-CAD 2026R1 对轴向磁通电机(AFM)的轴向力<strong style="color:var(--ink)">没有输出变量、没有 2D 结果图、帮助文档未收录</strong>。数据入口是 3D 集中节点力图:</p>
+  </div>
+  <pre><code>get_magnetic_3d_graph_point("Fr_{Rotor|Stator}_{OL|OC}_Lumped", 切片号, 节点号, 时间步)</code></pre>
+  <ul>
+    <li>AFM 的 2.5D 展开模型沿用径向电机命名:<strong style="color:var(--ink)">Fr(法向力)即轴向力</strong>,Ft 为切向力;单位 N/节点/切片。</li>
+    <li>求解前须打开 <code>ElectromagneticForcesCalc_Load</code> 与 <code>_OC</code>;一次求解同时得到空载(OC)与负载(OL)。</li>
+    <li>节点:转子 10 个(36° 步距)、定子 12 个(30° 步距),首尾(0°/360°)为同一节点须去重;对节点与两切片求和得净力。</li>
+    <li>本模型电流为 RMS 口径(<code>CurrentDefinition=1</code>):改电流要设 <code>RMSCurrent</code>,改 <code>PeakCurrent</code> 无效。</li>
+  </ul>
+</section>
+
+<section class="card">
+  <div>
+    <h2>同事复现指南</h2>
+    <p>整个项目由 git 管理,脚本一条命令跑完(约 2 分钟),原始 .mot 不会被修改。</p>
+  </div>
+  <ol>
+    <li><strong style="color:var(--ink)">环境</strong>:Windows + Motor-CAD 2026R1(安装器会设好 <code>MOTORCAD_ACTIVEX</code> 环境变量);Python ≥ 3.10,执行 <code>pip install ansys-motorcad-core</code>。</li>
+    <li><strong style="color:var(--ink)">获取项目</strong>:复制整个 <code>motionpushpull</code> 目录(含 .git)。</li>
+    <li><strong style="color:var(--ink)">运行</strong>:<code>python -X utf8 axial_force_final.py</code>——Motor-CAD 前台弹出,求解约 88 s,结束后保持打开供检查(加 <code>--quit</code> 自动关闭)。</li>
+    <li><strong style="color:var(--ink)">读结果</strong>:控制台 [结论] 两行;波形与校核在 <code>output_motorcad/axialforce_final_*.json</code> 与 <code>axial_force_*.csv</code>。</li>
+    <li><strong style="color:var(--ink)">判定</strong>:作用–反作用偏差 &lt;5%、转矩交叉 &lt;10%、解析同量级,三项都过才采信。</li>
+  </ol>
+  <p class="note">换其它 AFM 模型:改脚本顶部 <code>MOT_SRC</code>;若切片数/半径不同,按 .mot 中 <code>AFM_SectionCentreRadius_Array</code> 更新 <code>SEC_RADII_MM</code>。探测脚本 axial_probe*.py 是找数据口的历史过程,复现时无需运行。详见仓库 <code>REPRODUCE.md</code>。</p>
+</section>
+
+<footer>
+  数据:output_motorcad/axialforce_final_0825_212518.json(基线 100 ℃)· compare_results_0825_215456.json(20 ℃ 气隙扫描)· git: deb6b65 / 33eb8cf / 527eb49 · 对标对象:《轴向磁拉力计算与轴承选型校核报告 V3.0-20260826》· 工具链:PyMotorCAD + Motor-CAD 2026R1(前台)
+</footer>
+</div>
+
+<script>
+(function(){
+"use strict";
+var OC=[344.30,342.75,341.97,341.22,340.77,341.67,341.11,341.92,342.56,342.97,344.21,343.47,343.62,343.40,343.03,343.52,342.36,342.56,342.48,342.48,343.48,342.96,343.40,343.62,343.45,344.22,343.17,343.13,342.91,342.65,343.51];
+var OL=[344.91,343.31,342.28,341.62,341.04,342.06,341.49,342.09,342.82,343.25,344.64,343.78,343.70,343.55,343.06,343.77,342.65,342.72,342.82,342.80,343.97,343.47,343.74,344.03,343.84,344.77,343.70,343.49,343.39,343.08,344.03];
+var css=function(n){return getComputedStyle(document.documentElement).getPropertyValue(n).trim();};
+var NS="http://www.w3.org/2000/svg";
+function el(p,t,a){var e=document.createElementNS(NS,t);for(var k in a)e.setAttribute(k,a[k]);p.appendChild(e);return e;}
+
+/* ---- 折线图 ---- */
+function drawLine(){
+  var svg=document.getElementById("lineChart");if(!svg)return;
+  svg.innerHTML="";
+  var W=880,H=300,L=64,R=16,T=14,B=40;
+  var y0=338,y1=346;
+  var X=function(i){return L+(W-L-R)*i/30;};
+  var Y=function(v){return T+(H-T-B)*(1-(v-y0)/(y1-y0));};
+  for(var g=y0;g<=y1;g+=2){
+    el(svg,"line",{x1:L,x2:W-R,y1:Y(g),y2:Y(g),stroke:css("--grid"),"stroke-width":1});
+    el(svg,"text",{x:L-10,y:Y(g)+4,"text-anchor":"end"}).textContent=g;
+  }
+  for(var d=0;d<=360;d+=90){
+    var x=L+(W-L-R)*d/360;
+    el(svg,"text",{x:x,y:H-B+20,"text-anchor":"middle"}).textContent=d+"°";
+  }
+  el(svg,"text",{x:L-46,y:T+((H-T-B)/2),transform:"rotate(-90 "+(L-46)+" "+(T+(H-T-B)/2)+")","text-anchor":"middle","class":"axis-t"}).textContent="净轴向力 (N)";
+  el(svg,"text",{x:L+(W-L-R)/2,y:H-6,"text-anchor":"middle","class":"axis-t"}).textContent="转子位置(电角度)";
+  function poly(data,color){
+    var pts=data.map(function(v,i){return X(i)+","+Y(v);}).join(" ");
+    el(svg,"polyline",{points:pts,fill:"none",stroke:color,"stroke-width":2,"stroke-linejoin":"round","stroke-linecap":"round"});
+  }
+  poly(OC,css("--s-oc"));poly(OL,css("--s-ol"));
+  el(svg,"text",{x:W-R-4,y:Y(OL[30])-12,"text-anchor":"end",fill:css("--s-ol"),"font-weight":"500"}).textContent="负载 343.2 N";
+  el(svg,"text",{x:W-R-4,y:Y(OC[30])+26,"text-anchor":"end",fill:css("--s-oc"),"font-weight":"500"}).textContent="空载 342.9 N";
+  var cross=el(svg,"line",{y1:T,y2:H-B,stroke:css("--ink-3"),"stroke-width":1,"stroke-dasharray":"3 3",visibility:"hidden"});
+  var m1=el(svg,"circle",{r:4,fill:css("--s-oc"),stroke:css("--surface"),"stroke-width":2,visibility:"hidden"});
+  var m2=el(svg,"circle",{r:4,fill:css("--s-ol"),stroke:css("--surface"),"stroke-width":2,visibility:"hidden"});
+  var tip=document.getElementById("lineTip");
+  svg.addEventListener("mousemove",function(ev){
+    var r=svg.getBoundingClientRect();
+    var px=(ev.clientX-r.left)*(W/r.width);
+    if(px<L||px>W-R){hide();return;}
+    var i=Math.round((px-L)/(W-L-R)*30);i=Math.max(0,Math.min(30,i));
+    var x=X(i);
+    cross.setAttribute("x1",x);cross.setAttribute("x2",x);cross.setAttribute("visibility","visible");
+    m1.setAttribute("cx",x);m1.setAttribute("cy",Y(OC[i]));m1.setAttribute("visibility","visible");
+    m2.setAttribute("cx",x);m2.setAttribute("cy",Y(OL[i]));m2.setAttribute("visibility","visible");
+    tip.style.display="block";
+    tip.innerHTML=(i*12)+"°<br>负载 "+OL[i].toFixed(2)+" N<br>空载 "+OC[i].toFixed(2)+" N";
+    var bx=ev.clientX-r.left,by=ev.clientY-r.top;
+    tip.style.left=Math.min(bx+14,r.width-140)+"px";tip.style.top=(by-10)+"px";
+  });
+  function hide(){cross.setAttribute("visibility","hidden");m1.setAttribute("visibility","hidden");m2.setAttribute("visibility","hidden");tip.style.display="none";}
+  svg.addEventListener("mouseleave",hide);
+}
+
+/* ---- 节点分布图 (转子上/定子下) ---- */
+var ROT1=[20.39,13.37,7.03,11.61,19.17,20.36,13.36,7.05,11.61,19.20];
+var ROT2=[26.97,18.64,13.04,16.53,25.69,26.93,18.71,13.05,16.48,25.72];
+var STA1=[-21.12,-18.29,-11.80,-8.18,-10.20,-16.80,-21.05,-18.25,-11.80,-8.17,-10.21,-16.82];
+var STA2=[-20.15,-18.71,-11.04,-5.81,-9.04,-17.28,-20.15,-18.71,-11.05,-5.79,-9.04,-17.27];
+function drawBars(){
+  var svg=document.getElementById("barChart");if(!svg)return;
+  svg.innerHTML="";
+  var W=880,H=360,L=64,R=16,T=16,B=44;
+  var maxV=50,minV=-45;
+  var Y=function(v){return T+(H-T-B)*(maxV-v)/(maxV-minV);};
+  for(var g=-40;g<=40;g+=20){
+    el(svg,"line",{x1:L,x2:W-R,y1:Y(g),y2:Y(g),stroke:g===0?css("--line"):css("--grid"),"stroke-width":1});
+    el(svg,"text",{x:L-10,y:Y(g)+4,"text-anchor":"end"}).textContent=g;
+  }
+  el(svg,"text",{x:L-46,y:T+(H-T-B)/2,transform:"rotate(-90 "+(L-46)+" "+(T+(H-T-B)/2)+")","text-anchor":"middle","class":"axis-t"}).textContent="节点轴向力 (N)";
+  var tip=document.getElementById("barTip");
+  var span=(W-L-R);
+  var AX=function(a){return L+18+(span-36)*a/360;};   /* 统一角度→x 映射 */
+  for(var td=0;td<=360;td+=90){
+    el(svg,"text",{x:AX(td),y:H-B+18,"text-anchor":"middle"}).textContent=td+"°";
+  }
+  function bars(s1,s2,n,step,c1,c2,label){
+    var bw=Math.min(24,(span-36)/12*0.5);
+    for(var i=0;i<n;i++){
+      var cx=AX(i*step);
+      var v1=s1[i],v2=s2[i],tot=v1+v2;
+      var up=tot>=0;
+      var yA=Y(0),h1=Math.abs(Y(v1)-Y(0)),h2=Math.abs(Y(v1+v2)-Y(v1));
+      var r1=el(svg,"rect",{x:cx-bw/2,y:up?Y(v1):yA+2,width:bw,height:Math.max(h1-2,1),fill:c1,rx:0});
+      var r2=el(svg,"rect",{x:cx-bw/2,y:up?Y(v1+v2):Y(v1)+2,width:bw,height:Math.max(h2-2,1),fill:c2,rx:3});
+      (function(idx,total){
+        [r1,r2].forEach(function(rc){
+          rc.style.cursor="default";
+          rc.addEventListener("mousemove",function(ev){
+            var rt=svg.getBoundingClientRect();
+            tip.style.display="block";
+            tip.innerHTML=label+" 节点 "+(idx*step)+"°<br>切片1 "+s1[idx].toFixed(1)+" N · 切片2 "+s2[idx].toFixed(1)+" N<br>合计 "+total.toFixed(1)+" N";
+            tip.style.left=Math.min(ev.clientX-rt.left+14,rt.width-190)+"px";
+            tip.style.top=(ev.clientY-rt.top-10)+"px";
+          });
+          rc.addEventListener("mouseleave",function(){tip.style.display="none";});
+        });
+      })(i,tot);
+    }
+  }
+  bars(ROT1,ROT2,10,36,css("--sec1"),css("--sec2"),"转子");
+  bars(STA1,STA2,12,30,css("--sec1n"),css("--sec2n"),"定子");
+  el(svg,"text",{x:W-R-4,y:T+12,"text-anchor":"end","class":"axis-t",fill:css("--ink-2")}).textContent="↑ 转子(10 节点,指向定子)";
+  el(svg,"text",{x:W-R-4,y:H-B-8,"text-anchor":"end","class":"axis-t",fill:css("--ink-2")}).textContent="↓ 定子(12 节点,反作用)";
+  el(svg,"text",{x:L+(W-L-R)/2,y:H-6,"text-anchor":"middle","class":"axis-t"}).textContent="圆周位置(机械角度)";
+}
+/* ---- 气隙扫描对比图 (V2) ---- */
+var GAPS=[0.6,1.0,1.5];
+var FEA_G=[513.1,416.5,326.7];
+var ANA_G=[601,483,378];
+function drawGap(){
+  var svg=document.getElementById("gapChart");if(!svg)return;
+  svg.innerHTML="";
+  var W=880,H=300,L=64,R=20,T=16,B=44;
+  var y0=0,y1=650,x0=0.5,x1=1.6;
+  var X=function(g){return L+(W-L-R)*(g-x0)/(x1-x0);};
+  var Y=function(v){return T+(H-T-B)*(1-(v-y0)/(y1-y0));};
+  for(var gv=0;gv<=600;gv+=150){
+    el(svg,"line",{x1:L,x2:W-R,y1:Y(gv),y2:Y(gv),stroke:css("--grid"),"stroke-width":1});
+    el(svg,"text",{x:L-10,y:Y(gv)+4,"text-anchor":"end"}).textContent=gv;
+  }
+  GAPS.forEach(function(g){
+    el(svg,"text",{x:X(g),y:H-B+20,"text-anchor":"middle"}).textContent=g.toFixed(1)+" mm";
+  });
+  el(svg,"text",{x:L-46,y:T+(H-T-B)/2,transform:"rotate(-90 "+(L-46)+" "+(T+(H-T-B)/2)+")","text-anchor":"middle","class":"axis-t"}).textContent="空载轴向力 (N)";
+  el(svg,"text",{x:L+(W-L-R)/2,y:H-6,"text-anchor":"middle","class":"axis-t"}).textContent="气隙长度(磁钢 20 ℃)";
+  function series(data,color,below){
+    var pts=data.map(function(v,i){return X(GAPS[i])+","+Y(v);}).join(" ");
+    el(svg,"polyline",{points:pts,fill:"none",stroke:color,"stroke-width":2,"stroke-linejoin":"round"});
+    data.forEach(function(v,i){
+      el(svg,"circle",{cx:X(GAPS[i]),cy:Y(v),r:4.5,fill:color,stroke:css("--surface"),"stroke-width":2});
+      el(svg,"text",{x:X(GAPS[i]),y:Y(v)+(below?20:-12),"text-anchor":"middle",fill:color,"font-weight":"500"}).textContent=Math.round(v)+" N";
+    });
+  }
+  series(ANA_G,css("--s-ol"),false);
+  series(FEA_G,css("--s-oc"),true);
+  GAPS.forEach(function(g,i){
+    el(svg,"text",{x:X(g),y:Y(FEA_G[i])+36,"text-anchor":"middle"}).textContent="×"+(FEA_G[i]/ANA_G[i]).toFixed(2);
+  });
+}
+function drawAll(){drawLine();drawBars();drawGap();}
+drawAll();
+if(window.matchMedia){window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",drawAll);}
+new MutationObserver(drawAll).observe(document.documentElement,{attributes:true,attributeFilter:["data-theme"]});
+})();
+</script>

BIN
axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V2.pdf


+ 72 - 0
axial_mag_pull-master/axial_mag_pull/gui_app/README.md

@@ -0,0 +1,72 @@
+# 轴向磁拉力仿真 GUI 工具
+
+基于 PyQt5 的桌面工具,调整参数后一键运行 Motor-CAD 轴向磁拉力仿真,无需 AI token。
+
+## 快速开始(直接用 exe)
+
+1. 确保本机已安装 **Motor-CAD 2026R1** 且许可证可用
+2. 双击 `dist/AxialForceGUI.exe`
+3. 确认模型文件路径(默认指向仓库内 MARS 模型)
+4. 调整工况参数(RMS 电流 / 转速 / 气隙 / 磁钢温度)
+5. 点击「运行单点仿真」或切换到「气隙扫描」标签页
+6. 等待 ~2 分钟,结果显示在右下方面板,可导出 JSON/CSV
+
+## 两种运行模式
+
+| 模式 | 说明 |
+|---|---|
+| 单点仿真 | 当前参数跑一次求解,输出空载+负载净轴向力、纹波、三判据 |
+| 气隙扫描 | 设置起始/结束气隙和步长,依次求解,输出力-气隙曲线 + 磁负刚度 |
+
+> 气隙扫描点数超过 10 个时会弹窗确认(每点约 2~3 分钟)。
+
+## 可调参数
+
+| 参数 | 默认值 | 对应 .mot 变量 |
+|---|---|---|
+| RMS 电流 | 21 A | `RMSCurrent`(RMS 口径,勿用 PeakCurrent) |
+| 转速 | 5000 rpm | `ShaftSpeed` |
+| 气隙 | 1.0 mm | `Airgap` |
+| 磁钢温度 | 100 °C | `Magnet_Temperature`(热态;冷态对标用 20°C) |
+
+## 开发模式运行
+
+```bash
+pip install -r requirements.txt
+python main.py
+```
+
+## 重新打包 exe
+
+> **为什么仓库里没有 exe?**
+> `dist/AxialForceGUI.exe`(约 40MB)是 PyInstaller 打包的二进制生成物,按项目
+> 纪律"生成物不入库"不纳入 git 版本管理。原因:① 二进制文件不适合 git 差异管理,
+> 每次重新打包都会产生 40MB 的全新 blob,仓库体积会快速膨胀;② exe 与本机 Python
+> 环境/PyQt 版本绑定,源码 + `build.bat` 才是可复现的交付物;③ 同事拿到源码后
+> 运行 `build.bat` 即可在自己机器上一键打包(约 1 分钟),或直接向维护者拷贝 exe。
+
+```bash
+build.bat
+```
+
+或手动执行:
+
+```bash
+pyinstaller --onefile --windowed --name AxialForceGUI ^
+  --hidden-import solver --collect-submodules ansys main.py
+```
+
+生成的 `dist/AxialForceGUI.exe` 可独立运行(约 40MB)。
+
+## 三判据(结果可信的依据)
+
+1. **作用-反作用**:定/转子合力反号,偏差 < 5%
+2. **转矩交叉**:Σ(Ft×r) 与转矩图偏差 < 10%
+3. **解析量级**:B²A/2μ₀ 与 FEA 同量级(解析偏高 ~1.4× 属正常)
+
+## 注意事项
+
+- 仅支持 AFM(轴向磁通)模型;径向机的 Fr 不是轴向力
+- 仿真运行时 Motor-CAD 会前台弹出,求解期间请勿关闭
+- 若 Motor-CAD 窗口不可见(任务栏有图标点不开),工具已内置 `set_visible(True)` 自动修复
+- 结果文件默认输出到仓库 `output_motorcad/`(不入库)

+ 11 - 0
axial_mag_pull-master/axial_mag_pull/gui_app/build.bat

@@ -0,0 +1,11 @@
+@echo off
+REM 打包 AxialForceGUI.exe
+REM 前提: pip install -r requirements.txt
+cd /d "%~dp0"
+pyinstaller --onefile --windowed --name AxialForceGUI ^
+  --hidden-import solver ^
+  --collect-submodules ansys ^
+  main.py
+echo.
+echo 打包完成: dist\AxialForceGUI.exe
+pause

+ 790 - 0
axial_mag_pull-master/axial_mag_pull/gui_app/main.py

@@ -0,0 +1,790 @@
+# -*- coding: utf-8 -*-
+"""
+MARS 轴向磁拉力仿真工具 — PyQt5 GUI (现代工业风 v2)
+====================================================
+双击运行 (或 python main.py), 调整参数后点击运行, 无需 AI token。
+"""
+import os
+import sys
+import json
+import time
+import traceback
+
+from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QSize
+from PyQt5.QtWidgets import (
+    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
+    QGridLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QCheckBox,
+    QTabWidget, QTextEdit, QGroupBox, QDoubleSpinBox, QSpinBox,
+    QTableWidget, QTableWidgetItem, QHeaderView, QMessageBox, QSplitter,
+    QFrame, QSizePolicy, QStatusBar, QScrollArea
+)
+from PyQt5.QtGui import QFont, QColor, QPalette
+
+BASE = os.path.dirname(os.path.abspath(__file__))
+DEFAULT_MOT = os.path.normpath(
+    os.path.join(BASE, "..", "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot"))
+DEFAULT_OUT = os.path.normpath(os.path.join(BASE, "..", "output_motorcad"))
+DEFAULT_MC_EXE = r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe"
+
+# ============================================================
+# 现代工业风 QSS — 浅灰蓝底 + 白色卡片 + 工业蓝强调
+# ============================================================
+STYLE = """
+* { font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 10pt; }
+
+QMainWindow { background: #eef1f5; }
+
+/* --- 卡片式分组 --- */
+QGroupBox {
+    font-weight: 600; font-size: 11pt; color: #1e293b;
+    border: 1px solid #e2e8f0; border-radius: 8px;
+    margin-top: 16px; padding-top: 14px; padding-left: 10px; padding-right: 10px;
+    background: #ffffff;
+}
+QGroupBox::title {
+    subcontrol-origin: margin; left: 14px; padding: 0 8px;
+    background: #ffffff; color: #1e293b;
+}
+
+/* --- 标签 --- */
+QLabel { color: #334155; font-size: 10pt; }
+QLabel#cardValue { font-size: 22pt; font-weight: 700; color: #1e293b; }
+QLabel#cardUnit  { font-size: 11pt; color: #64748b; font-weight: 400; }
+QLabel#cardLabel { font-size: 9pt; color: #94a3b8; font-weight: 500; letter-spacing: 1px; }
+QLabel#sectionTitle { font-size: 11pt; font-weight: 600; color: #1e293b; padding-bottom: 4px; }
+
+/* --- 输入框 --- */
+QLineEdit, QDoubleSpinBox, QSpinBox {
+    padding: 6px 10px; border: 1px solid #cbd5e1; border-radius: 6px;
+    background: #f8fafc; color: #1e293b; font-size: 10pt; min-height: 22px;
+    selection-background-color: #2563eb; selection-color: white;
+}
+QLineEdit:focus, QDoubleSpinBox:focus, QSpinBox:focus {
+    border: 1px solid #2563eb; background: #ffffff;
+}
+QDoubleSpinBox::up-button, QSpinBox::up-button,
+QDoubleSpinBox::down-button, QSpinBox::down-button { width: 18px; }
+
+/* --- 按钮 --- */
+QPushButton {
+    padding: 8px 20px; border-radius: 6px; font-size: 10pt; font-weight: 600;
+    border: 1px solid #2563eb; background: #2563eb; color: white;
+}
+QPushButton:hover { background: #1d4ed8; border-color: #1d4ed8; }
+QPushButton:pressed { background: #1e40af; }
+QPushButton:disabled { background: #94a3b8; border-color: #94a3b8; color: #e2e8f0; }
+QPushButton#secondary { background: #ffffff; color: #2563eb; border: 1px solid #cbd5e1; }
+QPushButton#secondary:hover { background: #f1f5f9; border-color: #2563eb; }
+QPushButton#secondary:disabled { color: #94a3b8; background: #f1f5f9; border-color: #e2e8f0; }
+
+/* --- 日志区 (浅色终端风) --- */
+QTextEdit#logView {
+    background: #f8fafc; color: #334155;
+    font-family: "Consolas", "Cascadia Code", "Courier New", monospace;
+    font-size: 9.5pt; border: 1px solid #e2e8f0; border-radius: 8px;
+    padding: 10px; line-height: 1.5;
+}
+
+/* --- Tab --- */
+QTabWidget::pane { border: 1px solid #e2e8f0; border-radius: 8px; top: -1px; background: #ffffff; }
+QTabBar::tab {
+    padding: 8px 24px; font-size: 10pt; font-weight: 600; color: #64748b;
+    background: #f1f5f9; border: 1px solid #e2e8f0; border-bottom: none;
+    border-top-left-radius: 6px; border-top-right-radius: 6px; margin-right: 4px;
+}
+QTabBar::tab:selected { background: #ffffff; color: #2563eb; border-bottom: 2px solid #2563eb; }
+QTabBar::tab:hover:!selected { background: #e2e8f0; color: #334155; }
+
+/* --- 表格 --- */
+QTableWidget {
+    gridline-color: #f1f5f9; font-size: 10pt; color: #334155;
+    border: 1px solid #e2e8f0; border-radius: 6px; background: #ffffff;
+    alternate-background-color: #f8fafc;
+}
+QTableWidget::item { padding: 6px; }
+QHeaderView::section {
+    background: #1e293b; color: #f1f5f9; padding: 8px;
+    font-weight: 600; font-size: 9.5pt; border: none;
+}
+
+/* --- 状态栏 --- */
+QStatusBar { background: #1e293b; color: #e2e8f0; font-size: 9.5pt; }
+QStatusBar QLabel { color: #e2e8f0; font-size: 9.5pt; padding: 2px 8px; }
+
+/* --- 滚动条 --- */
+QScrollBar:vertical { width: 10px; background: #f1f5f9; border-radius: 5px; }
+QScrollBar::handle:vertical { background: #cbd5e1; border-radius: 5px; min-height: 30px; }
+QScrollBar::handle:vertical:hover { background: #94a3b8; }
+QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }
+QScrollBar:horizontal { height: 10px; background: #f1f5f9; border-radius: 5px; }
+QScrollBar::handle:horizontal { background: #cbd5e1; border-radius: 5px; min-width: 30px; }
+QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal { width: 0; }
+
+/* --- 复选框 --- */
+QCheckBox { spacing: 8px; color: #334155; font-size: 10pt; }
+QCheckBox::indicator { width: 18px; height: 18px; border: 2px solid #cbd5e1; border-radius: 4px; background: #ffffff; }
+QCheckBox::indicator:checked { background: #2563eb; border-color: #2563eb; }
+"""
+
+
+class SolverWorker(QObject):
+    """后台仿真线程, 通过信号与 GUI 通信。"""
+    log = pyqtSignal(str)
+    result_single = pyqtSignal(dict)
+    result_sweep = pyqtSignal(dict)
+    finished = pyqtSignal()
+    error = pyqtSignal(str)
+
+    def __init__(self, mode, params):
+        super().__init__()
+        self.mode = mode
+        self.params = params
+
+    def run(self):
+        try:
+            from solver import Solver
+            p = self.params
+            solver = Solver(
+                mot_path=p["mot_path"], out_dir=p["out_dir"],
+                rms_current=p["rms_current"], speed_rpm=p["speed_rpm"],
+                airgap_mm=p["airgap_mm"], magnet_temp_c=p["magnet_temp_c"],
+                sec_radii_mm=p.get("sec_radii_mm", [28.25, 34.75]),
+                motorcad_exe=p.get("motorcad_exe") or None,
+                keep_open=p.get("keep_open", True),
+                log_cb=lambda msg: self.log.emit(msg),
+            )
+            if self.mode == "single":
+                results = solver.run_single()
+                self.result_single.emit(results)
+            else:
+                results = solver.run_sweep(p["sweep_gaps"])
+                self.result_sweep.emit(results)
+        except Exception as e:
+            tb = traceback.format_exc()
+            self.error.emit("%s\n%s" % (e, tb))
+        finally:
+            self.finished.emit()
+
+
+class ResultCard(QFrame):
+    """结果数值卡片: 大数字 + 标签 + 单位"""
+    def __init__(self, label, unit="N"):
+        super().__init__()
+        self.setFrameShape(QFrame.StyledPanel)
+        self.setStyleSheet("""
+            ResultCard { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 10px; }
+        """)
+        lay = QVBoxLayout(self)
+        lay.setContentsMargins(16, 14, 16, 14)
+        lay.setSpacing(2)
+
+        self.lbl_label = QLabel(label)
+        self.lbl_label.setObjectName("cardLabel")
+        self.lbl_value = QLabel("—")
+        self.lbl_value.setObjectName("cardValue")
+        self.lbl_value.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+        self.lbl_unit = QLabel(unit)
+        self.lbl_unit.setObjectName("cardUnit")
+
+        val_row = QHBoxLayout()
+        val_row.setSpacing(4)
+        val_row.addWidget(self.lbl_value)
+        val_row.addWidget(self.lbl_unit)
+        val_row.addStretch()
+
+        lay.addWidget(self.lbl_label)
+        lay.addLayout(val_row)
+
+    def set_value(self, val, fmt="%.1f"):
+        self.lbl_value.setText(fmt % val if val is not None else "—")
+
+    def set_color(self, color):
+        self.lbl_value.setStyleSheet("color: %s;" % color)
+
+
+class MainWindow(QMainWindow):
+    def __init__(self):
+        super().__init__()
+        self.setWindowTitle("MARS 轴向磁拉力仿真工具")
+        self.resize(1200, 780)
+        self.setMinimumSize(1024, 680)
+        self.worker = None
+        self.thread = None
+        self.last_results = None
+        self.last_mode = None
+        self._build_ui()
+        self._set_status("就绪", "#94a3b8")
+
+    def _build_ui(self):
+        central = QWidget()
+        self.setCentralWidget(central)
+        root = QHBoxLayout(central)
+        root.setContentsMargins(12, 12, 12, 8)
+        root.setSpacing(12)
+
+        # ============ 左侧参数面板 ============
+        left = QVBoxLayout()
+        left.setSpacing(10)
+
+        # 模型与路径
+        gb_file = QGroupBox("模型与路径")
+        gl = QGridLayout(gb_file)
+        gl.setSpacing(8)
+        gl.setVerticalSpacing(10)
+        self.ed_mot = QLineEdit(DEFAULT_MOT)
+        btn_mot = QPushButton("浏览")
+        btn_mot.setObjectName("secondary")
+        btn_mot.setFixedWidth(64)
+        btn_mot.clicked.connect(self._browse_mot)
+        gl.addWidget(QLabel("模型文件"), 0, 0)
+        gl.addWidget(self.ed_mot, 0, 1)
+        gl.addWidget(btn_mot, 0, 2)
+
+        self.ed_out = QLineEdit(DEFAULT_OUT)
+        btn_out = QPushButton("浏览")
+        btn_out.setObjectName("secondary")
+        btn_out.setFixedWidth(64)
+        btn_out.clicked.connect(self._browse_out)
+        gl.addWidget(QLabel("输出目录"), 1, 0)
+        gl.addWidget(self.ed_out, 1, 1)
+        gl.addWidget(btn_out, 1, 2)
+
+        self.ed_mc = QLineEdit(DEFAULT_MC_EXE)
+        gl.addWidget(QLabel("MC exe 路径"), 2, 0)
+        gl.addWidget(self.ed_mc, 2, 1, 1, 2)
+        left.addWidget(gb_file)
+
+        # 工况参数
+        gb_param = QGroupBox("工况参数")
+        gp = QGridLayout(gb_param)
+        gp.setSpacing(8)
+        gp.setVerticalSpacing(10)
+        self.sp_rms = QDoubleSpinBox(); self.sp_rms.setRange(0, 200); self.sp_rms.setValue(21.0); self.sp_rms.setSuffix(" A")
+        self.sp_speed = QSpinBox(); self.sp_speed.setRange(100, 100000); self.sp_speed.setValue(5000); self.sp_speed.setSuffix(" rpm")
+        self.sp_airgap = QDoubleSpinBox(); self.sp_airgap.setRange(0.1, 10); self.sp_airgap.setSingleStep(0.1); self.sp_airgap.setValue(1.0); self.sp_airgap.setSuffix(" mm")
+        self.sp_temp = QDoubleSpinBox(); self.sp_temp.setRange(-40, 200); self.sp_temp.setValue(100.0); self.sp_temp.setSuffix(" °C")
+        gp.addWidget(QLabel("RMS 电流"), 0, 0); gp.addWidget(self.sp_rms, 0, 1)
+        gp.addWidget(QLabel("转速"), 0, 2); gp.addWidget(self.sp_speed, 0, 3)
+        gp.addWidget(QLabel("气隙"), 1, 0); gp.addWidget(self.sp_airgap, 1, 1)
+        gp.addWidget(QLabel("磁钢温度"), 1, 2); gp.addWidget(self.sp_temp, 1, 3)
+        self.cb_keep = QCheckBox("运行后保持 Motor-CAD 打开")
+        self.cb_keep.setChecked(True)
+        gp.addWidget(self.cb_keep, 2, 0, 1, 4)
+        left.addWidget(gb_param)
+
+        # 运行模式 Tab
+        self.tabs = QTabWidget()
+        # 单点
+        tab_single = QWidget()
+        ts = QVBoxLayout(tab_single)
+        ts.setContentsMargins(8, 12, 8, 8)
+        self.btn_run_single = QPushButton("▶  运行单点仿真")
+        self.btn_run_single.setMinimumHeight(42)
+        self.btn_run_single.setStyleSheet("font-size: 11pt;")
+        self.btn_run_single.clicked.connect(lambda: self._start_run("single"))
+        ts.addWidget(self.btn_run_single)
+        ts.addStretch()
+        self.tabs.addTab(tab_single, "  单点仿真  ")
+
+        # 气隙扫描
+        tab_sweep = QWidget()
+        sw = QGridLayout(tab_sweep)
+        sw.setContentsMargins(8, 12, 8, 8)
+        sw.setSpacing(8)
+        self.sp_gap_start = QDoubleSpinBox(); self.sp_gap_start.setRange(0.1, 10); self.sp_gap_start.setSingleStep(0.1); self.sp_gap_start.setValue(0.6); self.sp_gap_start.setSuffix(" mm")
+        self.sp_gap_end = QDoubleSpinBox(); self.sp_gap_end.setRange(0.1, 10); self.sp_gap_end.setSingleStep(0.1); self.sp_gap_end.setValue(1.5); self.sp_gap_end.setSuffix(" mm")
+        self.sp_gap_step = QDoubleSpinBox(); self.sp_gap_step.setRange(0.05, 5); self.sp_gap_step.setSingleStep(0.05); self.sp_gap_step.setValue(0.2); self.sp_gap_step.setSuffix(" mm")
+        sw.addWidget(QLabel("起始气隙"), 0, 0); sw.addWidget(self.sp_gap_start, 0, 1)
+        sw.addWidget(QLabel("结束气隙"), 0, 2); sw.addWidget(self.sp_gap_end, 0, 3)
+        sw.addWidget(QLabel("步长"), 1, 0); sw.addWidget(self.sp_gap_step, 1, 1)
+        self.lbl_sweep_preview = QLabel("")
+        self.lbl_sweep_preview.setStyleSheet("color: #64748b; font-size: 9pt; padding: 4px 0;")
+        self.lbl_sweep_preview.setWordWrap(True)
+        sw.addWidget(self.lbl_sweep_preview, 1, 2, 1, 2)
+        self.btn_run_sweep = QPushButton("▶  运行气隙扫描")
+        self.btn_run_sweep.setMinimumHeight(42)
+        self.btn_run_sweep.setStyleSheet("font-size: 11pt;")
+        self.btn_run_sweep.clicked.connect(lambda: self._start_run("sweep"))
+        sw.addWidget(self.btn_run_sweep, 2, 0, 1, 4)
+        sw.setRowStretch(3, 1)
+        for sp in (self.sp_gap_start, self.sp_gap_end, self.sp_gap_step):
+            sp.valueChanged.connect(self._update_sweep_preview)
+        self._update_sweep_preview()
+        self.tabs.addTab(tab_sweep, "  气隙扫描  ")
+        left.addWidget(self.tabs)
+        left.addStretch()
+
+        left_w = QWidget()
+        left_w.setLayout(left)
+        left_w.setFixedWidth(400)
+
+        # ============ 右侧: 结果 + 日志 ============
+        right = QVBoxLayout()
+        right.setSpacing(10)
+
+        # --- 结果面板 (卡片式) ---
+        gb_result = QGroupBox("运行结果")
+        res_lay = QVBoxLayout(gb_result)
+        res_lay.setSpacing(10)
+
+        # 卡片行
+        card_row = QHBoxLayout()
+        card_row.setSpacing(10)
+        self.card_oc = ResultCard("空载净轴向力", "N")
+        self.card_ol = ResultCard("负载净轴向力", "N")
+        self.card_ripple = ResultCard("纹波峰峰", "N")
+        self.card_kneg = ResultCard("磁负刚度", "N/mm")
+        self.card_kneg.setVisible(False)
+        for c in (self.card_oc, self.card_ol, self.card_ripple, self.card_kneg):
+            c.setMinimumHeight(88)
+            card_row.addWidget(c)
+        res_lay.addLayout(card_row)
+
+        # 判据 + 摘要
+        self.lbl_checks = QLabel("尚未运行仿真。")
+        self.lbl_checks.setWordWrap(True)
+        self.lbl_checks.setStyleSheet("""
+            QLabel { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px;
+                     padding: 10px 14px; color: #64748b; font-size: 9.5pt; }
+        """)
+        res_lay.addWidget(self.lbl_checks)
+
+        # 结果表格
+        self.table_result = QTableWidget(0, 4)
+        self.table_result.setHorizontalHeaderLabels(["工况", "净轴向力 (N)", "纹波峰峰 (N)", "备注"])
+        self.table_result.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
+        self.table_result.verticalHeader().setVisible(False)
+        self.table_result.setAlternatingRowColors(True)
+        self.table_result.setVisible(False)
+        self.table_result.setMaximumHeight(160)
+        res_lay.addWidget(self.table_result)
+
+        # 导出按钮
+        btn_row = QHBoxLayout()
+        self.btn_export_json = QPushButton("导出 JSON")
+        self.btn_export_json.setObjectName("secondary")
+        self.btn_export_json.setEnabled(False)
+        self.btn_export_json.clicked.connect(self._export_json)
+        self.btn_export_csv = QPushButton("导出 CSV")
+        self.btn_export_csv.setObjectName("secondary")
+        self.btn_export_csv.setEnabled(False)
+        self.btn_export_csv.clicked.connect(self._export_csv)
+        btn_row.addWidget(self.btn_export_json)
+        btn_row.addWidget(self.btn_export_csv)
+        btn_row.addStretch()
+        res_lay.addLayout(btn_row)
+        right.addWidget(gb_result, 4)
+
+        # --- 日志区 ---
+        gb_log = QGroupBox("运行日志")
+        log_lay = QVBoxLayout(gb_log)
+        log_lay.setContentsMargins(10, 14, 10, 10)
+        self.txt_log = QTextEdit()
+        self.txt_log.setObjectName("logView")
+        self.txt_log.setReadOnly(True)
+        log_lay.addWidget(self.txt_log)
+        right.addWidget(gb_log, 5)
+
+        right_w = QWidget()
+        right_w.setLayout(right)
+
+        splitter = QSplitter(Qt.Horizontal)
+        splitter.addWidget(left_w)
+        splitter.addWidget(right_w)
+        splitter.setStretchFactor(0, 0)
+        splitter.setStretchFactor(1, 1)
+        splitter.setHandleWidth(2)
+        root.addWidget(splitter)
+
+        # 状态栏
+        self.statusBar = QStatusBar()
+        self.setStatusBar(self.statusBar)
+        self.lbl_status = QLabel("就绪")
+        self.lbl_runtime = QLabel("")
+        self.statusBar.addWidget(self.lbl_status, 1)
+        self.statusBar.addPermanentWidget(self.lbl_runtime)
+
+    # ================================================================
+    # 交互
+    # ================================================================
+    def _set_status(self, text, color="#e2e8f0"):
+        self.lbl_status.setText(text)
+        self.lbl_status.setStyleSheet("color: %s;" % color)
+
+    def _browse_mot(self):
+        path, _ = QFileDialog.getOpenFileName(self, "选择 Motor-CAD 模型", self.ed_mot.text(), "Motor-CAD (*.mot)")
+        if path:
+            self.ed_mot.setText(path)
+
+    def _browse_out(self):
+        path = QFileDialog.getExistingDirectory(self, "选择输出目录", self.ed_out.text())
+        if path:
+            self.ed_out.setText(path)
+
+    def _update_sweep_preview(self):
+        gaps = self._calc_sweep_gaps()
+        if gaps:
+            est = len(gaps) * 2.5
+            self.lbl_sweep_preview.setText("将求解 %d 个气隙点 (%.1f~%.1fmm), 预计约 %.0f 分钟"
+                                            % (len(gaps), gaps[0], gaps[-1], est))
+        else:
+            self.lbl_sweep_preview.setText("参数无效 (起始应 < 结束, 步长 > 0)")
+
+    def _calc_sweep_gaps(self):
+        s, e, step = self.sp_gap_start.value(), self.sp_gap_end.value(), self.sp_gap_step.value()
+        if s >= e or step <= 0:
+            return []
+        gaps = []
+        g = s
+        while g <= e + 1e-9:
+            gaps.append(round(g, 4))
+            g += step
+        return gaps
+
+    def _collect_params(self):
+        return {
+            "mot_path": self.ed_mot.text().strip(),
+            "out_dir": self.ed_out.text().strip(),
+            "rms_current": self.sp_rms.value(),
+            "speed_rpm": self.sp_speed.value(),
+            "airgap_mm": self.sp_airgap.value(),
+            "magnet_temp_c": self.sp_temp.value(),
+            "motorcad_exe": self.ed_mc.text().strip(),
+            "keep_open": self.cb_keep.isChecked(),
+            "sec_radii_mm": [28.25, 34.75],
+        }
+
+    def _start_run(self, mode):
+        if self.worker and self.thread and self.thread.isRunning():
+            QMessageBox.warning(self, "正在运行", "仿真正在进行中, 请等待完成。")
+            return
+        p = self._collect_params()
+        if not os.path.isfile(p["mot_path"]):
+            QMessageBox.critical(self, "错误", "模型文件不存在:\n%s" % p["mot_path"])
+            return
+        if mode == "sweep":
+            gaps = self._calc_sweep_gaps()
+            if not gaps:
+                QMessageBox.critical(self, "错误", "气隙扫描参数无效。")
+                return
+            p["sweep_gaps"] = gaps
+            if len(gaps) > 10:
+                reply = QMessageBox.question(self, "确认",
+                    "将求解 %d 个气隙点, 预计约 %d 分钟, 确认?" % (len(gaps), len(gaps) * 3))
+                if reply != QMessageBox.Yes:
+                    return
+
+        # 重置界面
+        self.last_results = None
+        self.txt_log.clear()
+        self.table_result.setVisible(False)
+        self.card_oc.set_value(None); self.card_oc.set_color("#1e293b")
+        self.card_ol.set_value(None); self.card_ol.set_color("#1e293b")
+        self.card_ripple.set_value(None); self.card_ripple.set_color("#1e293b")
+        self.card_kneg.setVisible(False)
+        self.lbl_checks.setText("仿真运行中...")
+        self.lbl_checks.setStyleSheet("""
+            QLabel { background: #fffbeb; border: 1px solid #fbbf24; border-radius: 6px;
+                     padding: 10px 14px; color: #92400e; font-size: 9.5pt; }
+        """)
+        self.btn_export_json.setEnabled(False)
+        self.btn_export_csv.setEnabled(False)
+        self._set_running(True)
+        self._set_status("● 运行中... Motor-CAD 求解中", "#fbbf24")
+        self.lbl_runtime.setText("")
+
+        # 启动线程
+        self.thread = QThread()
+        self.worker = SolverWorker(mode, p)
+        self.worker.moveToThread(self.thread)
+        self.thread.started.connect(self.worker.run)
+        self.worker.log.connect(self._append_log)
+        self.worker.result_single.connect(self._on_single_result)
+        self.worker.result_sweep.connect(self._on_sweep_result)
+        self.worker.error.connect(self._on_error)
+        self.worker.finished.connect(self._on_finished)
+        self.worker.finished.connect(self.thread.quit)
+        self.worker.finished.connect(self.worker.deleteLater)
+        self.thread.finished.connect(self.thread.deleteLater)
+        self.last_mode = mode
+        self._run_start_time = __import__("time").time()
+        self.thread.start()
+
+    def _set_running(self, running):
+        for btn in (self.btn_run_single, self.btn_run_sweep):
+            btn.setEnabled(not running)
+        if running:
+            self.btn_run_single.setText("⏳  运行中...")
+            self.btn_run_sweep.setText("⏳  运行中...")
+        else:
+            self.btn_run_single.setText("▶  运行单点仿真")
+            self.btn_run_sweep.setText("▶  运行气隙扫描")
+
+    def _append_log(self, msg):
+        """彩色日志: 根据内容前缀着色"""
+        msg = msg.rstrip()
+        if not msg:
+            return
+        # 判断颜色
+        if msg.startswith("[结论]") or "✓" in msg:
+            color = "#16a34a"  # 绿
+            weight = "600"
+        elif msg.startswith("[错误]") or msg.startswith("[ERROR]") or "Traceback" in msg:
+            color = "#dc2626"  # 红
+            weight = "600"
+        elif msg.startswith("[警告]"):
+            color = "#d97706"  # 橙
+            weight = "600"
+        elif msg.startswith("[提示]"):
+            color = "#2563eb"  # 蓝
+            weight = "500"
+        elif msg.startswith("== ") or msg.startswith("启动") or "求解" in msg[:6]:
+            color = "#475569"  # 深灰 (阶段标题)
+            weight = "600"
+        elif "RESULTS:" in msg or "结果文件:" in msg or "扫描结果:" in msg:
+            color = "#7c3aed"  # 紫 (文件路径)
+            weight = "500"
+        else:
+            color = "#334155"  # 默认
+            weight = "400"
+
+        import html
+        escaped = html.escape(msg)
+        self.txt_log.append('<span style="color:%s;font-weight:%s;">%s</span>' % (color, weight, escaped))
+        sb = self.txt_log.verticalScrollBar()
+        sb.setValue(sb.maximum())
+
+    def _on_single_result(self, results):
+        try:
+            self.last_results = results
+            forces = results.get("axial_forces", {}) or {}
+            checks = results.get("checks", {}) or {}
+
+            # 安全访问: forces[graph] 可能为 None
+            oc_data = forces.get("Fr_Rotor_OC_Lumped") or {}
+            ol_data = forces.get("Fr_Rotor_OL_Lumped") or {}
+            oc = oc_data.get("stats", {}) or {}
+            ol = ol_data.get("stats", {}) or {}
+            self.card_oc.set_value(oc.get("mean"))
+            self.card_ol.set_value(ol.get("mean"))
+            self.card_ripple.set_value(oc.get("pk2pk"))
+            self.card_oc.set_color("#16a34a")
+            self.card_ol.set_color("#2563eb")
+            self.card_ripple.set_color("#64748b")
+
+            # 判据文本
+            parts = []
+            ar_ol = (checks.get("action_reaction_OL") or {}).get("imbalance_pct")
+            ar_oc = (checks.get("action_reaction_OC") or {}).get("imbalance_pct")
+            if ar_ol is not None:
+                ok = ar_ol < 5
+                parts.append(("%s 作用反作用 OL=%.1f%%" % ("✓" if ok else "✗", ar_ol),
+                              "#16a34a" if ok else "#dc2626"))
+            if ar_oc is not None:
+                ok = ar_oc < 5
+                parts.append(("%s OC=%.1f%%" % ("✓" if ok else "✗", ar_oc),
+                              "#16a34a" if ok else "#dc2626"))
+            tq = checks.get("torque_crosscheck") or {}
+            if tq.get("sum_Ft_x_r_Nm_t0") and tq.get("torque_graph_mean_Nm"):
+                diff = abs(tq["sum_Ft_x_r_Nm_t0"] - tq["torque_graph_mean_Nm"]) \
+                    / max(abs(tq["torque_graph_mean_Nm"]), 1e-9) * 100
+                ok = diff < 10
+                parts.append(("%s 转矩交叉=%.1f%% (%.3f vs %.3f Nm)"
+                              % ("✓" if ok else "✗", diff,
+                                 tq["sum_Ft_x_r_Nm_t0"], tq["torque_graph_mean_Nm"]),
+                              "#16a34a" if ok else "#dc2626"))
+            an = checks.get("analytic") or {}
+            if an.get("F_est_N"):
+                parts.append(("解析量级=%.0fN (同量级)" % an["F_est_N"], "#7c3aed"))
+
+            html_parts = " &nbsp;&nbsp; ".join(
+                '<span style="color:%s;font-weight:600;">%s</span>' % (c, t) for t, c in parts
+            )
+            self.lbl_checks.setText(html_parts or "无判据数据")
+            self.lbl_checks.setStyleSheet("""
+                QLabel { background: #f0fdf4; border: 1px solid #86efac; border-radius: 6px;
+                         padding: 10px 14px; font-size: 9.5pt; }
+            """)
+
+            # 表格
+            rows = []
+            rms = (results.get("params") or {}).get("rms_current", 0)
+            for case, label in (("OC", "空载 (开路)"), ("OL", "负载 (RMS %.0fA)" % rms)):
+                v = forces.get("Fr_Rotor_%s_Lumped" % case)
+                if v and v.get("stats"):
+                    s = v["stats"]
+                    rows.append((label, "%.1f" % s["mean"], "%.2f" % s["pk2pk"], "指向定子"))
+            self._fill_table(rows)
+            self.btn_export_json.setEnabled(True)
+            self.btn_export_csv.setEnabled(True)
+        except Exception as e:
+            self._append_log("[错误] 结果显示异常: %s" % e)
+            traceback.print_exc()
+
+    def _on_sweep_result(self, results):
+        try:
+            self.last_results = results
+            cases = results.get("cases", []) or []
+            kneg = results.get("kneg_N_per_mm", {}) or {}
+
+            # 卡片: 用第一个和最后一个气隙的力
+            if cases:
+                first = cases[0] or {}
+                last = cases[-1] or {}
+                foc_first = (first.get("F_OC") or {}).get("mean")
+                foc_last = (last.get("F_OC") or {}).get("mean")
+                self.card_oc.set_value(foc_first)
+                self.card_oc.lbl_label.setText(
+                    "起始气隙 %.2fmm 空载力" % (first.get("airgap_mm", 0) or 0))
+                self.card_ol.set_value(foc_last)
+                self.card_ol.lbl_label.setText(
+                    "结束气隙 %.2fmm 空载力" % (last.get("airgap_mm", 0) or 0))
+                # 平均纹波
+                ripples = [(c.get("F_OC") or {}).get("pk2pk", 0) for c in cases
+                           if (c.get("F_OC") or {}).get("pk2pk") is not None]
+                if ripples:
+                    self.card_ripple.set_value(sum(ripples) / len(ripples))
+                self.card_oc.set_color("#16a34a")
+                self.card_ol.set_color("#2563eb")
+                self.card_ripple.set_color("#64748b")
+                # 磁负刚度卡片
+                if kneg:
+                    central = kneg.get("central")
+                    if central is not None:
+                        self.card_kneg.set_value(central)
+                        self.card_kneg.set_color("#7c3aed")
+                        self.card_kneg.setVisible(True)
+
+            kneg_txt = " | ".join("%s=%.0f" % (k, v) for k, v in kneg.items()) if kneg else "—"
+            self.lbl_checks.setText(
+                '<span style="color:#16a34a;font-weight:600;">✓ 扫描完成</span> &nbsp;&nbsp; '
+                '<span style="color:#7c3aed;">磁负刚度 (N/mm): %s</span>' % kneg_txt)
+            self.lbl_checks.setStyleSheet("""
+                QLabel { background: #f0fdf4; border: 1px solid #86efac; border-radius: 6px;
+                         padding: 10px 14px; font-size: 9.5pt; }
+            """)
+
+            rows = []
+            for c in cases:
+                if not c:
+                    continue
+                g = c.get("airgap_mm", 0)
+                foc = (c.get("F_OC") or {}).get("mean", 0)
+                fol = (c.get("F_OL") or {}).get("mean", 0)
+                rip = (c.get("F_OC") or {}).get("pk2pk", 0)
+                rows.append(("气隙 %.2fmm" % g, "OC=%.1f / OL=%.1f" % (foc, fol),
+                             "%.2f" % rip, "耗时%.0fs" % (c.get("solve_seconds", 0) or 0)))
+            self._fill_table(rows)
+            self.btn_export_json.setEnabled(True)
+            self.btn_export_csv.setEnabled(False)
+        except Exception as e:
+            self._append_log("[错误] 扫描结果显示异常: %s" % e)
+            traceback.print_exc()
+
+    def _fill_table(self, rows):
+        self.table_result.setVisible(True)
+        self.table_result.setRowCount(len(rows))
+        for i, (a, b, c, d) in enumerate(rows):
+            for j, val in enumerate((a, b, c, d)):
+                item = QTableWidgetItem(str(val))
+                item.setTextAlignment(Qt.AlignCenter)
+                self.table_result.setItem(i, j, item)
+
+    def _on_error(self, msg):
+        try:
+            self.lbl_checks.setText('<span style="color:#dc2626;font-weight:600;">✗ 运行出错, 详见日志</span>')
+            self.lbl_checks.setStyleSheet("""
+                QLabel { background: #fef2f2; border: 1px solid #fca5a5; border-radius: 6px;
+                         padding: 10px 14px; font-size: 9.5pt; }
+            """)
+            self._append_log("[错误] %s" % msg)
+        except Exception:
+            traceback.print_exc()
+
+    def _on_finished(self):
+        try:
+            elapsed = time.time() - getattr(self, "_run_start_time", time.time())
+            self._set_running(False)
+            # 检查是否有结果 (成功) 还是出错
+            if self.last_results is not None:
+                self._set_status("✓ 运行完成 — 结果已显示在上方", "#86efac")
+                self.lbl_runtime.setText("总耗时 %.0fs" % elapsed)
+            else:
+                self._set_status("✗ 运行出错 — 详见日志", "#fca5a5")
+                self.lbl_runtime.setText("")
+        except Exception as e:
+            traceback.print_exc()
+        finally:
+            # 延迟清理线程引用, 避免与 deleteLater 竞态
+            from PyQt5.QtCore import QTimer
+            QTimer.singleShot(100, self._cleanup_thread)
+
+    def _cleanup_thread(self):
+        self.worker = None
+        self.thread = None
+
+    def _export_json(self):
+        if not self.last_results:
+            return
+        path, _ = QFileDialog.getSaveFileName(self, "导出 JSON", "axialforce_result.json", "JSON (*.json)")
+        if path:
+            with open(path, "w", encoding="utf-8") as f:
+                json.dump(self.last_results, f, ensure_ascii=False, indent=2)
+            self._append_log("[提示] JSON 已导出 -> %s" % path)
+
+    def _export_csv(self):
+        if not self.last_results or self.last_mode != "single":
+            return
+        path, _ = QFileDialog.getSaveFileName(self, "导出 CSV", "axial_force.csv", "CSV (*.csv)")
+        if path:
+            forces = self.last_results.get("axial_forces", {})
+            graphs = ["Fr_Rotor_OL_Lumped", "Fr_Stator_OL_Lumped",
+                      "Fr_Rotor_OC_Lumped", "Fr_Stator_OC_Lumped"]
+            nmax = max(len(v["series_N"]) if v else 0 for v in forces.values())
+            with open(path, "w", encoding="utf-8") as f:
+                f.write("tstep," + ",".join(graphs) + "\n")
+                for i in range(nmax):
+                    row = [str(i)]
+                    for g in graphs:
+                        v = forces.get(g)
+                        row.append("%.4f" % v["series_N"][i] if v and i < len(v["series_N"]) else "")
+                    f.write(",".join(row) + "\n")
+            self._append_log("[提示] CSV 已导出 -> %s" % path)
+
+
+def main():
+    app = QApplication(sys.argv)
+    app.setStyle("Fusion")
+    app.setStyleSheet(STYLE)
+    # 关键: 防止子窗口/对话框关闭时触发整个应用退出
+    app.setQuitOnLastWindowClosed(False)
+    # 全局默认字体
+    font = QFont("Microsoft YaHei", 10)
+    app.setFont(font)
+
+    # 全局异常钩子: 捕获未处理异常, 防止 --windowed 模式下静默闪退
+    def _excepthook(exc_type, exc_value, exc_tb):
+        import traceback
+        msg = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
+        try:
+            with open(os.path.join(os.path.expanduser("~"), "axial_force_gui_error.log"),
+                      "a", encoding="utf-8") as f:
+                f.write("\n=== %s ===\n%s" % (time.strftime("%Y-%m-%d %H:%M:%S"), msg))
+        except Exception:
+            pass
+        # 尝试在 GUI 中显示错误
+        try:
+            QMessageBox.critical(None, "程序异常",
+                                 "发生未捕获异常:\n%s\n\n详情已写入 ~/axial_force_gui_error.log"
+                                 % str(exc_value))
+        except Exception:
+            pass
+    sys.excepthook = _excepthook
+
+    w = MainWindow()
+    w.show()
+    sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+    main()

+ 3 - 0
axial_mag_pull-master/axial_mag_pull/gui_app/requirements.txt

@@ -0,0 +1,3 @@
+PyQt5>=5.15
+ansys-motorcad-core>=0.8.0
+pyinstaller>=6.0

+ 318 - 0
axial_mag_pull-master/axial_mag_pull/gui_app/solver.py

@@ -0,0 +1,318 @@
+# -*- coding: utf-8 -*-
+"""
+轴向磁拉力求解核心 — 从 axial_force_final.py / axial_compare.py 提取,
+封装为可被 GUI 调用的 Solver 类。支持单点仿真与气隙扫描两种模式。
+所有日志通过 log_cb 回调输出到 GUI。
+"""
+import json
+import math
+import os
+import time
+
+MU0 = 4e-7 * math.pi
+MAX_TSTEPS = 64
+MAX_NODES = 40
+
+# 全局 keepalive: keep_open=True 时保存 MotorCAD 对象引用, 防止 solver 被 GC
+# 时连带销毁 MotorCAD → 关闭 Motor-CAD 进程 (GUI 模式下必须保持)
+_KEEPALIVE_MC = None
+
+
+def _stats(ys):
+    if not ys:
+        return None
+    return {"mean": sum(ys) / len(ys), "min": min(ys), "max": max(ys),
+            "pk2pk": max(ys) - min(ys), "n": len(ys)}
+
+
+def _read_nodes(mc, graph, sec, tstep):
+    xs, ys = [], []
+    for i in range(MAX_NODES):
+        try:
+            x, y = mc.get_magnetic_3d_graph_point(graph, sec, i, tstep)
+        except Exception:
+            break
+        xs.append(x)
+        ys.append(y)
+    return xs, ys
+
+
+def _net_force_series(mc, graph):
+    series = []
+    meta = {"sections": {}}
+    for tstep in range(MAX_TSTEPS):
+        total, got = 0.0, False
+        for sec in (1, 2):
+            xs, ys = _read_nodes(mc, graph, sec, tstep)
+            if not ys:
+                continue
+            got = True
+            nu = len(ys) - 1 if (len(xs) > 1 and
+                                 abs(xs[-1] - xs[0] - 360.0) < 1e-6) else len(ys)
+            total += sum(ys[:nu])
+            if tstep == 0:
+                meta["sections"][sec] = {"n_points": len(ys), "n_unique": nu}
+        if not got:
+            break
+        series.append(total)
+    return series, meta
+
+
+def _torque_from_ft(mc, sec_radii_mm):
+    tq = 0.0
+    for sec, r_mm in zip((1, 2), sec_radii_mm):
+        xs, ys = _read_nodes(mc, "Ft_Rotor_OL_Lumped", sec, 0)
+        if not ys:
+            return None
+        nu = len(ys) - 1 if (len(xs) > 1 and
+                             abs(xs[-1] - xs[0] - 360.0) < 1e-6) else len(ys)
+        tq += sum(ys[:nu]) * (r_mm * 1e-3)
+    return tq
+
+
+def _read_2d(mc, graph, maxpts=64):
+    ys = []
+    for i in range(maxpts):
+        try:
+            _, y = mc.get_magnetic_graph_point(graph, i)
+        except Exception:
+            break
+        ys.append(y)
+    return ys
+
+
+class Solver:
+    """轴向磁拉力求解器。参数通过构造函数传入, run_single() / run_sweep() 执行。"""
+
+    def __init__(self, mot_path, out_dir, rms_current=21.0, speed_rpm=5000.0,
+                 airgap_mm=1.0, magnet_temp_c=100.0, sec_radii_mm=None,
+                 motorcad_exe=None, keep_open=True, log_cb=None):
+        self.mot_path = mot_path
+        self.out_dir = out_dir
+        self.rms_current = rms_current
+        self.speed_rpm = speed_rpm
+        self.airgap_mm = airgap_mm
+        self.magnet_temp_c = magnet_temp_c
+        self.sec_radii_mm = sec_radii_mm or [28.25, 34.75]
+        self.motorcad_exe = motorcad_exe
+        self.keep_open = keep_open
+        self.log = log_cb or (lambda msg: None)
+        self.mc = None
+
+    def _start_motorcad(self):
+        from ansys.motorcad.core import MotorCAD, set_motorcad_exe
+        if not os.environ.get("MOTORCAD_ACTIVEX") and self.motorcad_exe:
+            if os.path.isfile(self.motorcad_exe):
+                set_motorcad_exe(self.motorcad_exe)
+                self.log("  未检测到 MOTORCAD_ACTIVEX, 已显式定位 exe")
+        self.log("启动 Motor-CAD (前台) ...")
+        self.mc = MotorCAD()
+        try:
+            self.mc.set_visible(True)
+        except Exception as e:
+            self.log("  [提示] set_visible 失败 (不影响计算): %s" % e)
+
+    def _load_and_configure(self, tag):
+        os.makedirs(self.out_dir, exist_ok=True)
+        ts = time.strftime("%m%d_%H%M%S")
+        self.mc.load_from_file(self.mot_path)
+        out_mot = os.path.join(self.out_dir, "MARS_SSSR_%s_%s.mot" % (tag, ts))
+        self.mc.save_to_file(out_mot)
+
+        # 设置工况参数
+        self.mc.set_variable("RMSCurrent", self.rms_current)
+        self.mc.set_variable("ShaftSpeed", self.speed_rpm)
+        self.mc.set_variable("Airgap", self.airgap_mm)
+        self.mc.set_variable("Magnet_Temperature", self.magnet_temp_c)
+        # 开力计算开关
+        self.mc.set_variable("ElectromagneticForcesCalc_Load", True)
+        self.mc.set_variable("ElectromagneticForcesCalc_OC", True)
+
+        # 回读确认
+        params = {}
+        for var in ["RMSCurrent", "ShaftSpeed", "Airgap", "Magnet_Temperature",
+                    "Stator_Lam_Dia", "Stator_Bore"]:
+            try:
+                params[var] = self.mc.get_variable(var)
+            except Exception:
+                params[var] = None
+        self.log("  工况: RMS=%.1fA, %drpm, 气隙=%.2fmm, 磁钢%.0f°C"
+                 % (params["RMSCurrent"], params["ShaftSpeed"],
+                    params["Airgap"], params["Magnet_Temperature"]))
+        return out_mot, params, ts
+
+    def _solve_once(self, tag="axialF"):
+        """执行一次求解, 返回 (results_dict, csv_path, json_path)"""
+        out_mot, params, ts = self._load_and_configure(tag)
+        self.log("求解中 (OC+OL 力同算) ...")
+        t0 = time.time()
+        self.mc.do_magnetic_calculation()
+        dt = time.time() - t0
+        self.log("  耗时 %.1f s" % dt)
+
+        results = {"when": ts, "source_mot": os.path.basename(self.mot_path),
+                   "work_mot": out_mot, "solve_seconds": dt,
+                   "params": {"rms_current": params["RMSCurrent"],
+                              "speed_rpm": params["ShaftSpeed"],
+                              "airgap_mm": params["Airgap"],
+                              "magnet_temp_c": params["Magnet_Temperature"]},
+                   "convention_note": "AFM 2.5D 中 Fr(法向)=轴向力; OL=负载, OC=空载"}
+
+        # 净轴向力
+        forces = {}
+        for graph in ["Fr_Rotor_OL_Lumped", "Fr_Stator_OL_Lumped",
+                      "Fr_Rotor_OC_Lumped", "Fr_Stator_OC_Lumped"]:
+            series, meta = _net_force_series(self.mc, graph)
+            if series:
+                forces[graph] = {"series_N": series, "stats": _stats(series),
+                                 "meta": meta}
+                s = _stats(series)
+                self.log("  %s: 均值 %.1f N, 纹波 %.2f N"
+                         % (graph, s["mean"], s["pk2pk"]))
+            else:
+                forces[graph] = None
+                self.log("  [警告] %s 无数据" % graph)
+        results["axial_forces"] = forces
+
+        # 三判据
+        checks = {}
+        for case in ("OL", "OC"):
+            fr = forces.get("Fr_Rotor_%s_Lumped" % case)
+            fs = forces.get("Fr_Stator_%s_Lumped" % case)
+            if fr and fs:
+                mr, ms = fr["stats"]["mean"], fs["stats"]["mean"]
+                checks["action_reaction_%s" % case] = {
+                    "rotor_mean_N": mr, "stator_mean_N": ms,
+                    "imbalance_pct": abs(mr + ms) / max(abs(mr), 1e-9) * 100}
+        tq_ft = _torque_from_ft(self.mc, self.sec_radii_mm)
+        tys = _read_2d(self.mc, 17)
+        tq_graph = _stats(tys)["mean"] if tys else None
+        checks["torque_crosscheck"] = {"sum_Ft_x_r_Nm_t0": tq_ft,
+                                       "torque_graph_mean_Nm": tq_graph}
+        bys = _read_2d(self.mc, "FluxDensityAirgap")
+        if bys and params.get("Stator_Lam_Dia") and params.get("Stator_Bore"):
+            b2 = sum(b * b for b in bys) / len(bys)
+            d_out = float(params["Stator_Lam_Dia"]) * 1e-3
+            d_in = float(params["Stator_Bore"]) * 1e-3
+            area = math.pi / 4.0 * (d_out ** 2 - d_in ** 2)
+            checks["analytic"] = {"mean_B2_T2": b2, "area_m2": area,
+                                  "F_est_N": area / (2 * MU0) * b2}
+        results["checks"] = checks
+        self.log("校核: %s" % json.dumps(checks, ensure_ascii=False))
+
+        # CSV
+        csv_path = os.path.join(self.out_dir, "axial_force_%s_%s.csv" % (tag, ts))
+        with open(csv_path, "w", encoding="utf-8") as f:
+            f.write("tstep,Fr_Rotor_OL_N,Fr_Stator_OL_N,"
+                    "Fr_Rotor_OC_N,Fr_Stator_OC_N\n")
+            nmax = max(len(v["series_N"]) if v else 0 for v in forces.values())
+            for i in range(nmax):
+                row = [str(i)]
+                for g in ["Fr_Rotor_OL_Lumped", "Fr_Stator_OL_Lumped",
+                          "Fr_Rotor_OC_Lumped", "Fr_Stator_OC_Lumped"]:
+                    v = forces.get(g)
+                    row.append("%.4f" % v["series_N"][i]
+                               if v and i < len(v["series_N"]) else "")
+                f.write(",".join(row) + "\n")
+        results["csv"] = csv_path
+
+        # JSON
+        json_path = os.path.join(self.out_dir, "axialforce_%s_%s.json" % (tag, ts))
+        with open(json_path, "w", encoding="utf-8") as f:
+            json.dump(results, f, ensure_ascii=False, indent=2)
+
+        # 结论摘要
+        for case, label in (("OL", "负载(RMS %.0fA)" % self.rms_current),
+                            ("OC", "空载")):
+            v = forces.get("Fr_Rotor_%s_Lumped" % case)
+            if v:
+                s = v["stats"]
+                self.log("[结论] %s 转子净轴向力: 均值 %.1f N, 纹波峰峰 %.2f N"
+                         % (label, s["mean"], s["pk2pk"]))
+        return results, csv_path, json_path
+
+    def run_single(self):
+        """单点仿真。返回 results dict。"""
+        self._start_motorcad()
+        try:
+            results, csv_path, json_path = self._solve_once("single")
+            self.log("结果文件: %s" % json_path)
+            return results
+        finally:
+            if not self.keep_open:
+                try:
+                    self.mc.quit()
+                except Exception:
+                    pass
+            else:
+                global _KEEPALIVE_MC
+                _KEEPALIVE_MC = self.mc  # 保持引用, 防止 GC 关闭 Motor-CAD
+                self.log("[提示] Motor-CAD 保持前台打开供检查。")
+
+    def run_sweep(self, gaps):
+        """气隙扫描。gaps: 气隙列表(mm)。返回汇总 dict。"""
+        self._start_motorcad()
+        try:
+            os.makedirs(self.out_dir, exist_ok=True)
+            ts = time.strftime("%m%d_%H%M%S")
+            self.mc.load_from_file(self.mot_path)
+            out_mot = os.path.join(self.out_dir, "MARS_SSSR_sweep_%s.mot" % ts)
+            self.mc.save_to_file(out_mot)
+
+            self.mc.set_variable("RMSCurrent", self.rms_current)
+            self.mc.set_variable("ShaftSpeed", self.speed_rpm)
+            self.mc.set_variable("Magnet_Temperature", self.magnet_temp_c)
+            self.mc.set_variable("ElectromagneticForcesCalc_Load", True)
+            self.mc.set_variable("ElectromagneticForcesCalc_OC", True)
+
+            sweep = {"when": ts, "magnet_temp_C": self.magnet_temp_c,
+                     "rms_current_A": self.rms_current, "work_mot": out_mot,
+                     "cases": []}
+            for g in gaps:
+                self.mc.set_variable("Airgap", g)
+                back = self.mc.get_variable("Airgap")
+                self.log("== 气隙 %.2f mm (回读 %s), 求解 ..." % (g, back))
+                t0 = time.time()
+                self.mc.do_magnetic_calculation()
+                dt = time.time() - t0
+                case = {"airgap_mm": back, "solve_seconds": dt}
+                for graph, key in [("Fr_Rotor_OC_Lumped", "F_OC"),
+                                   ("Fr_Rotor_OL_Lumped", "F_OL")]:
+                    s, _ = _net_force_series(self.mc, graph)
+                    case[key] = _stats(s)
+                bys = _read_2d(self.mc, "FluxDensityAirgap")
+                if bys:
+                    case["B2_mean_T2"] = sum(b * b for b in bys) / len(bys)
+                self.log("   F_OC=%.1f N, F_OL=%.1f N (耗时 %.0fs)"
+                         % (case["F_OC"]["mean"], case["F_OL"]["mean"], dt))
+                sweep["cases"].append(case)
+
+            # 磁负刚度 (有限差分, OC 口径)
+            cs = sweep["cases"]
+            if len(cs) >= 2:
+                f = [c["F_OC"]["mean"] for c in cs]
+                g = [c["airgap_mm"] for c in cs]
+                kneg = {}
+                for i in range(len(cs) - 1):
+                    kneg["seg_%.2f_%.2f" % (g[i], g[i + 1])] = \
+                        -(f[i + 1] - f[i]) / (g[i + 1] - g[i])
+                if len(cs) >= 3:
+                    kneg["central"] = -(f[-1] - f[0]) / (g[-1] - g[0])
+                sweep["kneg_N_per_mm"] = kneg
+                self.log("磁负刚度: %s N/mm" % json.dumps(kneg, ensure_ascii=False))
+
+            json_path = os.path.join(self.out_dir, "sweep_results_%s.json" % ts)
+            with open(json_path, "w", encoding="utf-8") as f:
+                json.dump(sweep, f, ensure_ascii=False, indent=2)
+            self.log("扫描结果: %s" % json_path)
+            return sweep
+        finally:
+            if not self.keep_open:
+                try:
+                    self.mc.quit()
+                except Exception:
+                    pass
+            else:
+                global _KEEPALIVE_MC
+                _KEEPALIVE_MC = self.mc  # 保持引用, 防止 GC 关闭 Motor-CAD
+                self.log("[提示] Motor-CAD 保持前台打开供检查。")

+ 25 - 0
axial_mag_pull-master/axial_mag_pull/show_motorcad.py

@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+"""
+召回隐形的 Motor-CAD 窗口
+==========================
+现象: /SCRIPTING 模式启动的 Motor-CAD 在部分机器上窗口创建但不显示 ——
+任务栏有图标、点击无反应 (不是最小化)。本脚本连接**已在运行**的实例并
+强制窗口可见, 不启动新实例、不打断正在进行的求解。
+
+用法: python show_motorcad.py
+"""
+import sys
+
+
+def main():
+    from ansys.motorcad.core import MotorCAD
+    print("连接已在运行的 Motor-CAD 实例 ...")
+    mc = MotorCAD(open_new_instance=False)
+    mc.set_visible(True)
+    print("已强制窗口可见。若仍看不到, 可能被挪到屏幕外: 点任务栏图标后按 "
+          "Win+↑ 最大化即可拉回。")
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

BIN
axial_mag_pull-master/axial_mag_pull/轴向磁通电机轴向磁拉力计算与轴承选型校核报告V3.0-20260826.pdf


+ 66 - 0
docs/CONVERSATION_LOG.md

@@ -0,0 +1,66 @@
+# 对话与决策记录
+
+> 所有关键决策、技术选择、问题排查均带时间戳记录于此。
+> 格式:`## YYYY-MM-DD HH:MM — 主题`
+
+---
+
+## 2026-08-27 — 项目启动与 Phase 1 范围确认
+
+### 参与者
+Car.Lin(项目负责人)
+
+### 背景
+基于已完成的两个参考案例(axial_mag_pull 轴向磁拉力仿真、torqrippswap 转矩脉动参数扫描),启动 PCB 轴向磁通电机自动化仿真系统的正式开发。
+
+### 关键决策
+
+1. **项目架构**:采用设计方案 V1.1 的双系统解耦架构
+   - 系统一(Web端):方案生成与优化
+   - 系统二(本地EXE):仿真执行
+   - 接口:simulation_plan.json / simulation_results.csv
+
+2. **Phase 1 范围**:最小闭环验证
+   - 仿真工具:Motor-CAD 2026R1(电磁仿真)
+   - 拓扑:SSSR(单定子单转子)
+   - 调试模型:MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot
+   - 输出指标:平均转矩、转矩脉动(%)、系统效率、总损耗(轴向力暂不做)
+   - 方案编辑:本地GUI内做简单方案编辑器(选参数、设范围步长)
+   - 经验库:SQLite + JSON 文件轻量方案
+   - 通信:本地文件交换
+
+3. **参考案例复用策略**
+   - Motor-CAD 连接/参数写入/回读校验/每点重载/结果解析:复用 torqrippswap 的 solver.py
+   - AFM 参数语义/环境陷阱/探测技术:复用 axial_mag_pull 的 KNOWLEDGE_BASE
+   - GUI 架构/闪退防护/打包:参考 torqrippswap 的 DESKTOP_APP_WORKFLOW.md
+   - 三判据校验:轴向力目标时复用 axial_mag_pull;转矩/效率目标需另建判据
+
+4. **工程规范**
+   - 所有 .py / .ps1 源码纯 ASCII
+   - 运行前 Git preflight 强制检查
+   - 参数写入后必须回读校验
+   - 每点重新加载基线模型
+   - 结果逐点落盘(每点 flush)
+   - 原始 .mot 只读
+
+5. **理论参考**
+   - 主要参考:《轴向磁通永磁无刷电机(原书第2版)》Jacek F. Gieras
+   - 补充参考:《轴向磁场无刷同步电机理论与设计》邓秋玲
+   - 知识文档中标记理论参考路径,理论缺乏时优先查书
+
+6. **DRSS V16.html**
+   - 仅参考其交互方式和参数组织形式
+   - 不直接复用计算逻辑
+   - 不深入分析
+
+### 里程碑规划
+- M1:环境验证 + 单工况仿真脚本
+- M2:参数扫描引擎(单参数/多参数)
+- M3:方案JSON接口 + 本地GUI
+- M4:经验库雏形 + 反馈闭环
+
+### 待办
+- [ ] 搭建项目规范文档
+- [ ] 编写 solver_core.py
+- [ ] 编写 run_single.py 并跑通 MARS 模型
+- [ ] 初始化 Git 仓库

+ 250 - 0
docs/KNOWLEDGE_BASE.md

@@ -0,0 +1,250 @@
+# 知识库 — PCB轴向磁通电机自动化仿真系统
+
+> 本文件是项目的核心知识沉淀,供人和任何 AI 工具阅读使用。
+> 全部结论基于参考案例 axial_mag_pull 和 torqrippswap 的实测验证。
+> 入口文件:仓库根 AGENTS.md(AI)/ README.md(人)。
+
+## 1. 环境事实
+
+| 项 | 值 |
+|---|---|
+| Motor-CAD | 2026R1 (v261) |
+| 定位方式 | 环境变量 `MOTORCAD_ACTIVEX` → activex.bat → exe 路径;未注册时脚本内用 `set_motorcad_exe()` 回退 |
+| Python | ≥ 3.10,`pip install ansys-motorcad-core pyside6 pandas` |
+| 许可证 | FlexNet: `ANSYSLMD_LICENSE_FILE=1055@localhost`,lmgrd + ansyslmd 都必须在跑 |
+| 非登录 shell 陷阱 | AI 工具的 shell 可能不继承机器级环境变量,需 inline 设置或脚本内回退 |
+| 单次电磁求解耗时 | 约 90~150 秒(视机器性能和网格设置) |
+| 窗口不可见陷阱 | pymotorcad 用 `/SCRIPTING` 模式启动,默认主窗口隐藏,必须 `set_visible(True)` |
+
+### 1.1 环境验证命令
+
+```powershell
+# 验证 Motor-CAD 自动化注册
+echo $env:MOTORCAD_ACTIVEX
+
+# 验证许可证
+echo $env:ANSYSLMD_LICENSE_FILE
+
+# 验证 Python 包
+python -c "import ansys.motorcad.core; print('pymotorcad OK')"
+```
+
+### 1.2 常见环境问题
+
+| 现象 | 原因 | 解决 |
+|---|---|---|
+| Motor-CAD 启动后 30s 退出 | 许可证 ansyslmd 未运行 | 检查 ANSYS License Management Center (http://localhost:1084) |
+| pymotorcad 报 NoSuchProcess | 同上,或环境变量未继承 | inline export 两个环境变量 |
+| Motor-CAD 窗口看不见 | /SCRIPTING 模式默认隐藏 | `mc.set_visible(True)`;若仍找不到,点任务栏图标 → Win+↑ 最大化 |
+| 改电流无效 | CurrentDefinition=1 时改了 PeakCurrent | 改 `RMSCurrent` |
+
+---
+
+## 2. Motor-CAD 自动化核心方法
+
+### 2.1 连接与实例管理
+
+```python
+import ansys.motorcad.core as pymotorcad
+
+# 创建独立实例(不要连接已有实例,可能控制错误窗口)
+mc = pymotorcad.MotorCAD(open_new_instance=True, keep_instance_open=False)
+mc.set_visible(True)  # 必须,否则窗口隐藏
+mc.set_variable("MessageDisplayState", 2)
+mc.display_screen("Scripting")
+```
+
+### 2.2 模型加载与保存
+
+```python
+# 加载基线模型
+mc.load_from_file(r"models\MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot")
+
+# 另存工作副本(不污染原模型)
+mc.save_to_file(r"output\working_<timestamp>.mot")
+```
+
+### 2.3 参数写入与回读校验
+
+```python
+import math
+
+def write_and_verify(mc, variable, value):
+    mc.set_variable(variable, value)
+    applied = float(mc.get_variable(variable))
+    if not math.isclose(applied, value, rel_tol=1e-8, abs_tol=1e-7):
+        raise RuntimeError(
+            f"Write verification failed for {variable}: "
+            f"wrote {value}, read {applied}"
+        )
+```
+
+**为什么必须回读**:Motor-CAD 对不适用的参数有时会静默接受,不回读就不知道参数是否真的生效了。
+
+### 2.4 求解
+
+```python
+mc.do_magnetic_calculation()  # 电磁求解,约 90-150 秒
+```
+
+### 2.5 结果导出
+
+```python
+# 导出电磁结果(分号分隔的 CSV,不是普通逗号 CSV)
+mc.export_results("EMagnetic", r"output\raw\result_<timestamp>.csv")
+```
+
+---
+
+## 3. .mot 参数语义(AFM 模板,易错!)
+
+| 参数 | 正确语义 | 常见误读 | MARS模型值 |
+|---|---|---|---|
+| `Magnet_Length` | 磁钢**轴向厚度** | 以为是长度 | 3 mm |
+| `Magnet_Thickness` | 磁钢环**径向深度** = (D_out−D_in)/2 | 以为是厚度 | 13 mm |
+| `Magnet_Arc_[ED]` | 极弧(电角度) | — | 121° |
+| `Pole_Arc` | 某些转子类型不生效 | 以为是极弧 | 勿用 |
+| `CurrentDefinition` | 1=RMS口径 → 改电流设 `RMSCurrent` | 改 PeakCurrent 无效且不报错 | 1 |
+| `RMSCurrent` | RMS相电流(CurrentDefinition=1时的电流入口) | — | 21 A |
+| `Magnet_Temperature` | 磁钢温度,**默认100°C热态** | 以为是常温20°C | 100 |
+| `Airgap` | 气隙长度 | — | 1 mm |
+| `Shaft_Speed` | 轴转速 | — | 5000 rpm |
+| `TorquePointsPerCycle` | 每电周期转矩采样点数 | — | 30(模型默认) |
+| `AirgapMeshPoints_mesh` | 气隙内部网格点 | 与 layers 成对设置 | 840 |
+| `AirgapMeshPoints_layers` | 气隙表面网格点 | 与 mesh 成对设置 | 840 |
+
+### 3.1 温度定律
+
+F ∝ Br²,磁钢剩磁 Br 随温度变化。实测 100°C vs 20°C 的轴向力比值与 Br² 比值一致(<1% 偏差)。
+
+### 3.2 电流口径
+
+`CurrentDefinition=1` 表示 RMS 口径。此时:
+- 改 `RMSCurrent` 生效
+- 改 `PeakCurrent` **无效且不报错**(首跑踩坑)
+
+---
+
+## 4. 结果指标提取
+
+### 4.1 核心指标(Phase 1 必选)
+
+| 指标 key | 显示名称 | Motor-CAD 导出字段名 | 单位 |
+|---|---|---|---|
+| `tavg_nm` | 平均转矩 | `Average torque (virtual work)` | Nm |
+| `ripple_pct` | 转矩脉动 | `Torque Ripple (VW) [%]` | % |
+| `ripple_nm` | 转矩脉动绝对值 | `Torque Ripple (VW)` | Nm |
+| `efficiency_pct` | 系统效率 | `System Efficiency` | % |
+| `total_losses_w` | 总损耗 | `Total Losses (on load)` | W |
+| `copper_loss_w` | 铜耗 | `Armature DC Copper Loss (on load)` | W |
+| `iron_loss_w` | 定子铁耗 | `Stator iron Loss [total] (on load)` | W |
+| `magnet_loss_w` | 磁钢损耗 | `Magnet Loss (on load)` | W |
+| `back_emf_v` | 反电动势 | `Back EMF Line-Line Voltage (rms)` | V |
+| `input_power_w` | 输入功率 | `Input Power` | W |
+| `output_power_w` | 输出功率 | `Output Power` | W |
+| `shaft_speed_rpm` | 轴转速 | `Shaft Speed` | rpm |
+
+### 4.2 导出文件格式
+
+Motor-CAD 的 `export_results` 输出是**分号分隔**的文本文件,不是普通逗号 CSV:
+- 第一列是字段名,第二列是数值
+- 按 section 分段(E-Magnetics、Drive、Losses、Materials、Miscellaneous)
+- 同一指标可能在多个 section 重复,优先取 E-Magnetics
+- 编码可能是 UTF-8、cp1252、gbk 或 latin-1,需逐个尝试
+
+### 4.3 字段名匹配策略
+
+1. 先精确匹配(按 section 优先级:E-Magnetics → Drive → Losses → Materials → Miscellaneous)
+2. 再全 section 精确匹配
+3. 最后前缀模糊匹配(避免短字段误匹配,长度 > 3 才匹配)
+
+---
+
+## 5. AFM 轴向力数据口(Phase 1 暂不使用,保留参考)
+
+Motor-CAD 对 AFM 的轴向力**没有输出变量、没有 2D 结果图、帮助文档无记录**。唯一入口是 3D lumped 力图:
+
+```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=空载 — 一次求解两者全出
+- 求解前必须开开关:`ElectromagneticForcesCalc_Load=True`, `ElectromagneticForcesCalc_OC=True`
+- 节点首尾 0°/360° 重复,求和须去重
+- 净力 = 对全部去重节点求和 + 对全部径向切片求和
+
+> Phase 1 不提取轴向力,后续需要时参考 `axial_mag_pull-master/axial_mag_pull/axial_force_final.py`。
+
+---
+
+## 6. 采样点与气隙网格(转矩脉动评估关键)
+
+| 设置 | 用途 | 风险 |
+|---|---|---|
+| 30 点 / 840 网格 | 快速趋势筛选 | 9槽8极模型主要齿槽成分是18次电频谐波,30点低于可靠分辨要求,可能混叠 |
+| 120 点 / 960 网格 | 中等可信度 | 120点搭配840网格会弹出不对齐警告,需用960 |
+| 180 点 / 1680 网格 | 最终候选复算 | 速度慢但精度高 |
+
+**原则**:
+1. 不要随意组合采样点数和网格点数
+2. 先做单点验证,确认无交互弹窗
+3. 所有结果必须记录实际的 Torque points 和 airgap mesh
+4. 快速扫描的最佳点必须用高精度设置复算
+5. 快速扫描结果只用于趋势判断,不直接当作最终脉动真值
+
+---
+
+## 7. 新模型仿真工作流(SOP)
+
+1. 新模型 `.mot` 放 `models/` 目录,`git add` + 运行前先 commit
+2. 确认模型基本信息:拓扑、槽极数、气隙、磁钢厚度、电流口径
+3. 确认工况:磁钢温度(热态100°C / 冷态20°C)、RMS电流、转速
+4. 用 `scripts/run_single.py` 跑一次空载+负载,验证能提取到全部核心指标
+5. 验证通过后,用 `scripts/run_scan.py` 做参数扫描
+6. 结果记录:时间戳 + 简要说明 + git 提交
+7. 关键数值转录进入库文档
+
+---
+
+## 8. 图名/变量名探测技术(遇到未知输出时用)
+
+1. **报错文案筛查**(无需求解,秒级):`get_magnetic_graph_point(名, 0)` — "Graph name does not exist" = 不存在;"No points exist" = 存在但未求解
+2. **图 ID 枚举**:graph 参数可传 int,求解后逐 ID 读波形按量级辨认
+3. **权威清单**:GUI 内 Help → Graph Viewer(全部图名);Help → Automation Parameter Names / F2(全部变量名)
+4. **exe 字符串挖掘**:MotorCAD.exe 的 UTF-16 字符串含图名后缀、GUI 文案
+5. Motor-CAD 消息日志(`<模型名>\MessageLogs\*.txt`)记录每次 pymotorcad 调用,但重复错误行会被抑制
+
+---
+
+## 9. 理论参考资料
+
+当方案生成、参数初值估算、物理约束规则等需要理论支撑时,查阅以下资料:
+
+| 资料 | 路径 | 适用场景 |
+|---|---|---|
+| 《轴向磁通永磁无刷电机(原书第2版)》Jacek F. Gieras | `书籍与论文/轴向磁通永磁无刷电机(原书第2版), Jacek F. Gieras.pdf.pdf` | 拓扑分类、电磁设计基础、热设计、机械设计 — **主要参考** |
+| 《轴向磁场无刷同步电机理论与设计》邓秋玲 | `书籍与论文/轴向磁场无刷同步电机理论与设计-邓秋玲.pdf` | 国内工程实践、公式推导 — **补充参考** |
+| axial_mag_pull 解析报告 | `axial_mag_pull-master/axial_mag_pull/轴向磁通电机轴向磁拉力计算与轴承选型校核报告V3.0-20260826.pdf` | 轴向磁拉力解析计算方法、轴承选型 |
+
+**理论知识缺乏时的处理原则**:
+- 优先在上述书籍中查找对应章节
+- 找不到时,在 `docs/CONVERSATION_LOG.md` 中记录"理论待补充"标记
+- 不基于猜测制定物理约束或参数初值
+
+---
+
+## 10. 项目纪律(硬性)
+
+1. 每次运行仿真前 git commit
+2. 结果与报告带时间戳+简要说明并 git 提交;报告版本化不覆盖
+3. Motor-CAD 前台运行,跑完保持打开供人工检查
+4. 生成物(output/、*.log、build/、dist/)不入库
+5. 原始 .mot 只读,一切修改在时间戳副本上进行
+6. 参数写入后必须回读校验,不一致标记 FAILED
+7. 每个扫描点重新加载基线模型,防止参数污染
+8. 对话与决策带时间戳记入 `docs/CONVERSATION_LOG.md`
+9. 所有 .py / .ps1 源码纯 ASCII,中文用 Unicode 转义或放 Markdown
+10. 仿真在子线程运行,不阻塞 GUI 主线程

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 9080 - 0
models/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot


+ 159 - 0
scripts/run_single.py

@@ -0,0 +1,159 @@
+"""Single-point electromagnetic simulation verification script.
+
+Loads the MARS SSSR baseline model, runs one magnetic calculation
+with the model's default operating point, and extracts Phase 1 core
+metrics: average torque, torque ripple (%), system efficiency,
+total losses.
+
+Usage:
+    python scripts/run_single.py
+    python scripts/run_single.py --model models/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot
+    python scripts/run_single.py --quit  (close Motor-CAD after run)
+
+All source is ASCII.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import sys
+import time
+from datetime import datetime
+from pathlib import Path
+
+# Add project root to sys.path so we can import src.solver_core.
+PROJECT_ROOT = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(PROJECT_ROOT))
+
+from src.solver_core import (  # noqa: E402
+    MotorCADSolver,
+    METRIC_LABELS,
+    REQUIRED_METRICS,
+)
+
+DEFAULT_MODEL = PROJECT_ROOT / "models" / "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot"
+DEFAULT_OUTPUT = PROJECT_ROOT / "output"
+
+
+def _log(text: str) -> None:
+    stamp = datetime.now().strftime("%H:%M:%S")
+    print(f"[{stamp}] {text}", flush=True)
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description="Single-point Motor-CAD simulation")
+    parser.add_argument("--model", type=str, default=str(DEFAULT_MODEL),
+                        help="Path to .mot model file")
+    parser.add_argument("--output-dir", type=str, default=str(DEFAULT_OUTPUT),
+                        help="Directory for raw export and result JSON")
+    parser.add_argument("--quit", action="store_true",
+                        help="Close Motor-CAD after simulation (default: keep open)")
+    args = parser.parse_args()
+
+    model_path = Path(args.model)
+    if not model_path.exists():
+        print(f"ERROR: Model file not found: {model_path}")
+        return 1
+
+    output_dir = Path(args.output_dir)
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    _log("=" * 60)
+    _log("PCB Axial Flux Motor - Single Point Simulation")
+    _log("=" * 60)
+    _log(f"Model: {model_path.name}")
+    _log(f"Output: {output_dir}")
+    _log("")
+
+    solver = MotorCADSolver(log_cb=_log)
+    result = {}
+
+    try:
+        solver.connect()
+        _log("")
+        _log("Running single-point simulation (model default operating point)...")
+        _log("")
+
+        result = solver.run_single(
+            model_path=model_path,
+            params=None,  # use model default operating point
+            output_dir=output_dir,
+            tag="single",
+        )
+
+        _log("")
+        _log("=" * 60)
+        _log("RESULTS")
+        _log("=" * 60)
+        _log(f"Status: {result['status']}")
+        _log(f"Solve time: {result['solve_time_s']}s")
+        if result.get("error"):
+            _log(f"Error: {result['error']}")
+        _log("")
+
+        metrics = result.get("metrics", {})
+
+        # Print Phase 1 required metrics first.
+        _log("--- Phase 1 Core Metrics ---")
+        for key in REQUIRED_METRICS:
+            label = METRIC_LABELS.get(key, key)
+            value = metrics.get(key, "N/A")
+            if isinstance(value, float):
+                _log(f"  {label}: {value:.4f}")
+            else:
+                _log(f"  {label}: {value}")
+        _log("")
+
+        # Print all other extracted metrics.
+        other = {k: v for k, v in metrics.items() if k not in REQUIRED_METRICS}
+        if other:
+            _log("--- Additional Metrics ---")
+            for key, value in other.items():
+                label = METRIC_LABELS.get(key, key)
+                if isinstance(value, float):
+                    _log(f"  {label}: {value:.4f}")
+                else:
+                    _log(f"  {label}: {value}")
+            _log("")
+
+        # Save result JSON.
+        ts = datetime.now().strftime("%Y%m%d_%H%M%S")
+        result_path = output_dir / f"single_result_{ts}.json"
+        with open(result_path, "w", encoding="utf-8") as f:
+            json.dump(result, f, indent=2, ensure_ascii=False)
+        _log(f"Result saved: {result_path}")
+
+        if result["status"] == "OK":
+            _log("")
+            _log("SUCCESS: All Phase 1 required metrics extracted.")
+        elif result["status"] == "UNCERTAIN":
+            _log("")
+            _log("WARNING: Some required metrics missing - check raw export.")
+        else:
+            _log("")
+            _log("FAILED: Simulation or extraction failed.")
+
+    except KeyboardInterrupt:
+        _log("Interrupted by user.")
+    except Exception as exc:
+        _log(f"FATAL: {type(exc).__name__}: {exc}")
+        import traceback
+        _log(traceback.format_exc())
+        return 1
+    finally:
+        if args.quit:
+            solver.disconnect()
+        else:
+            _log("")
+            _log("Motor-CAD kept open for manual inspection.")
+            _log("Use --quit flag to auto-close.")
+
+    _log("")
+    _log("Done.")
+    return 0 if result.get("status") in ("OK", "UNCERTAIN") else 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 489 - 0
src/solver_core.py

@@ -0,0 +1,489 @@
+"""Motor-CAD electromagnetic simulation core for PCB axial flux motor.
+
+Pure computation module, no GUI dependencies.
+Handles Motor-CAD connection, model loading, parameter write-back
+verification, magnetic calculation, result export parsing, and
+metric extraction with bilingual (English/Chinese) field matching.
+
+All source is ASCII; Chinese field names use \\uXXXX escapes.
+
+Phase 1 metrics: average torque, torque ripple (%), system efficiency,
+total losses (plus auxiliary: copper/iron/magnet loss, back EMF,
+input/output power, shaft speed).
+"""
+
+from __future__ import annotations
+
+import csv
+import json
+import math
+import os
+import time
+import traceback
+from datetime import datetime
+from pathlib import Path
+
+
+# ---------------------------------------------------------------------------
+# Metric definitions: key, display label, and aliases (English + Chinese).
+# Chinese aliases use Unicode escapes so this file stays pure ASCII.
+# ---------------------------------------------------------------------------
+
+METRIC_DEFINITIONS = [
+    {
+        "key": "tavg_nm",
+        "label": "Average Torque [Nm]",
+        "aliases": [
+            "Average torque (virtual work)",
+            "\u5e73\u5747\u8f6c\u77e9 (virtual work)",
+            "\u5e73\u5747\u8f6c\u77e9(virtual work)",
+        ],
+    },
+    {
+        "key": "ripple_pct",
+        "label": "Torque Ripple [%]",
+        "aliases": [
+            "Torque Ripple (VW) [%]",
+            "Torque Ripple (VW)[%]",
+        ],
+    },
+    {
+        "key": "ripple_nm",
+        "label": "Torque Ripple [Nm]",
+        "aliases": [
+            "Torque Ripple (VW)",
+        ],
+    },
+    {
+        "key": "efficiency_pct",
+        "label": "System Efficiency [%]",
+        "aliases": [
+            "System Efficiency",
+            "\u7cfb\u7edf\u6548\u7387",
+        ],
+    },
+    {
+        "key": "total_losses_w",
+        "label": "Total Losses [W]",
+        "aliases": [
+            "Total Losses (on load)",
+            "\u603b\u635f\u8017(\u989d\u5b9a)",
+            "\u603b\u635f\u8017 (\u989d\u5b9a)",
+        ],
+    },
+    {
+        "key": "copper_loss_w",
+        "label": "DC Copper Loss [W]",
+        "aliases": [
+            "Armature DC Copper Loss (on load)",
+            "\u7535\u67a2\u76f4\u6d41\u94dc\u8017 (\u5e26\u8f7d)",
+            "\u7535\u67a2\u76f4\u6d41\u94dc\u8017(\u5e26\u8f7d)",
+        ],
+    },
+    {
+        "key": "iron_loss_w",
+        "label": "Stator Iron Loss [W]",
+        "aliases": [
+            "Stator iron Loss [total] (on load)",
+            "\u5b9a\u5b50\u94c1\u635f[\u603b\u635f\u8017](\u989d\u5b9a)",
+            "\u5b9a\u5b50\u94c1\u635f[\u603b\u635f\u8017] (\u989d\u5b9a)",
+        ],
+    },
+    {
+        "key": "magnet_loss_w",
+        "label": "Magnet Loss [W]",
+        "aliases": [
+            "Magnet Loss (on load)",
+            "\u6c38\u78c1\u4f53\u635f\u8017(\u989d\u5b9a)",
+            "\u6c38\u78c1\u4f53\u635f\u8017 (\u989d\u5b9a)",
+        ],
+    },
+    {
+        "key": "back_emf_v",
+        "label": "Back EMF LL rms [V]",
+        "aliases": [
+            "Back EMF Line-Line Voltage (rms)",
+            "\u7ebf\u95f4\u53cd\u5411\u7535\u52a8\u52bf\u6709\u6548\u503c",
+        ],
+    },
+    {
+        "key": "back_emf_thd_pct",
+        "label": "Back EMF THD [%]",
+        "aliases": [
+            "Harmonic Distortion Back EMF Line-Line Voltage",
+            "\u7ebf\u53cd\u5411\u7535\u52a8\u52bf\u8c10\u6ce2",
+            "\u7ebf\u7535\u538b\u8c10\u6ce2",
+        ],
+    },
+    {
+        "key": "input_power_w",
+        "label": "Input Power [W]",
+        "aliases": [
+            "Input Power",
+            "\u8f93\u5165\u529f\u7387",
+        ],
+    },
+    {
+        "key": "output_power_w",
+        "label": "Output Power [W]",
+        "aliases": [
+            "Output Power",
+            "\u8f93\u51fa\u529f\u7387_\u7535\u538b\u9650\u5236\u9644\u8fd1\u5de5\u4f5c\u70b9",
+        ],
+    },
+    {
+        "key": "em_power_w",
+        "label": "EM Power [W]",
+        "aliases": [
+            "Electromagnetic Power",
+            "\u7535\u78c1\u529f\u7387_\u7535\u538b\u9650\u5236\u9644\u8fd1\u5de5\u4f5c\u70b9",
+        ],
+    },
+    {
+        "key": "shaft_speed_rpm",
+        "label": "Shaft Speed [rpm]",
+        "aliases": [
+            "Shaft Speed",
+            "\u8f6c\u901f[RPM]",
+            "\u8f6c\u901f [RPM]",
+        ],
+    },
+    {
+        "key": "no_load_speed_rpm",
+        "label": "No-load Speed [rpm]",
+        "aliases": [
+            "No load speed",
+            "\u7a7a\u8f7d\u8f6c\u901f",
+        ],
+    },
+]
+
+METRIC_KEYS = [m["key"] for m in METRIC_DEFINITIONS]
+METRIC_LABELS = {m["key"]: m["label"] for m in METRIC_DEFINITIONS}
+
+# Phase 1 required metrics (must be present for a valid result).
+REQUIRED_METRICS = ["tavg_nm", "ripple_pct", "efficiency_pct", "total_losses_w"]
+
+# Section name aliases for priority ordering.
+SECTION_PRIORITY = [
+    "E-Magnetics",
+    "\u7535\u78c1",
+    "Drive",
+    "\u9a71\u52a8",
+    "Losses",
+    "\u635f\u8017",
+    "Materials",
+    "\u6750\u6599",
+    "Miscellaneous",
+    "\u6742\u9879",
+]
+
+
+# ---------------------------------------------------------------------------
+# Utility functions
+# ---------------------------------------------------------------------------
+
+def _normalize_name(name: str) -> str:
+    """Normalize a field name for matching: unify brackets, remove
+    whitespace, lowercase."""
+    s = name
+    s = s.replace("\uff08", "(").replace("\uff09", ")")
+    s = "".join(s.split())
+    return s.lower()
+
+
+# Pre-normalize aliases for fast matching.
+_METRIC_ALIAS_MAP: dict[str, str] = {}
+for _m in METRIC_DEFINITIONS:
+    for _alias in _m["aliases"]:
+        _METRIC_ALIAS_MAP[_normalize_name(_alias)] = _m["key"]
+
+
+def parse_export(path: Path) -> dict[str, dict[str, float]]:
+    """Parse a Motor-CAD semicolon-delimited export file.
+
+    Returns a dict of section_name -> {field_name: value}.
+    Handles UTF-8, cp1252, gbk, and latin-1 encodings.
+    """
+    text = None
+    for encoding in ("utf-8-sig", "gbk", "cp1252", "latin-1"):
+        try:
+            text = Path(path).read_text(encoding=encoding)
+            break
+        except UnicodeDecodeError:
+            continue
+    if text is None:
+        return {}
+    result: dict[str, dict[str, float]] = {}
+    section = "(root)"
+    for raw in text.splitlines():
+        line = raw.strip()
+        if not line:
+            continue
+        if ";" not in line:
+            section = line
+            result.setdefault(section, {})
+            continue
+        parts = line.split(";")
+        try:
+            field = parts[0].strip().strip('"')
+            value = float(parts[1])
+            result.setdefault(section, {})[field] = value
+        except (IndexError, ValueError):
+            continue
+    return result
+
+
+def pick_metric(results: dict[str, dict[str, float]], metric_key: str):
+    """Extract a metric value from parsed export results.
+
+    Tries exact alias match first (section priority order), then
+    falls back to prefix-based fuzzy match across all sections.
+    Returns the value (float) or None if not found.
+    """
+    wanted_aliases = set()
+    for m in METRIC_DEFINITIONS:
+        if m["key"] == metric_key:
+            for alias in m["aliases"]:
+                wanted_aliases.add(_normalize_name(alias))
+            break
+    if not wanted_aliases:
+        return None
+
+    # Phase 1: exact match in priority section order.
+    for section_name in SECTION_PRIORITY:
+        section = results.get(section_name)
+        if section is None:
+            continue
+        for field, value in section.items():
+            if _normalize_name(field) in wanted_aliases:
+                return value
+
+    # Phase 2: exact match across all sections.
+    for section in results.values():
+        for field, value in section.items():
+            if _normalize_name(field) in wanted_aliases:
+                return value
+
+    # Phase 3: prefix fuzzy match.
+    for alias_norm in wanted_aliases:
+        for section in results.values():
+            for field, value in section.items():
+                field_norm = _normalize_name(field)
+                if field_norm.startswith(alias_norm) or alias_norm.startswith(field_norm):
+                    if len(field_norm) > 3:
+                        return value
+    return None
+
+
+def extract_all_metrics(results: dict[str, dict[str, float]]) -> dict[str, float]:
+    """Extract all defined metrics from parsed results."""
+    out: dict[str, float] = {}
+    for m in METRIC_DEFINITIONS:
+        val = pick_metric(results, m["key"])
+        if val is not None:
+            out[m["key"]] = val
+    return out
+
+
+def check_required_metrics(metrics: dict[str, float]) -> tuple[bool, list[str]]:
+    """Check that all Phase 1 required metrics are present.
+    Returns (ok, list_of_missing_keys)."""
+    missing = [k for k in REQUIRED_METRICS if k not in metrics]
+    return (len(missing) == 0, missing)
+
+
+# ---------------------------------------------------------------------------
+# Motor-CAD solver
+# ---------------------------------------------------------------------------
+
+# Candidate Motor-CAD exe paths for fallback when MOTORCAD_ACTIVEX is unset.
+_MOTORCAD_EXE_CANDIDATES = [
+    r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe",
+    r"E:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe",
+    r"C:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe",
+]
+
+
+class MotorCADSolver:
+    """Manages a dedicated, foreground-visible Motor-CAD instance and
+    runs electromagnetic simulations with parameter write-back verification
+    and result extraction.
+
+    No GUI dependencies. Callbacks (log_cb) allow the caller to receive
+    log events.
+    """
+
+    def __init__(self, log_cb=None):
+        self.log_cb = log_cb
+        self.mc = None
+        self.model_path = None
+
+    def _log(self, text: str) -> None:
+        if self.log_cb:
+            self.log_cb(text)
+
+    def connect(self) -> None:
+        """Open a dedicated, foreground-visible Motor-CAD instance.
+
+        Falls back to set_motorcad_exe if MOTORCAD_ACTIVEX is not set.
+        """
+        import ansys.motorcad.core as pymotorcad
+
+        if not os.environ.get("MOTORCAD_ACTIVEX"):
+            try:
+                from ansys.motorcad.core import set_motorcad_exe
+                for candidate in _MOTORCAD_EXE_CANDIDATES:
+                    if os.path.exists(candidate):
+                        set_motorcad_exe(candidate)
+                        self._log(f"MOTORCAD_ACTIVEX not set; using {candidate}")
+                        break
+            except (ImportError, Exception):
+                pass
+
+        self._log("Opening a separate, visible Motor-CAD instance")
+        self.mc = pymotorcad.MotorCAD(open_new_instance=True, keep_instance_open=False)
+        self.mc.set_visible(True)
+        self.mc.set_variable("MessageDisplayState", 2)
+        self.mc.display_screen("Scripting")
+        self._log("Motor-CAD connected and visible")
+
+    def disconnect(self) -> None:
+        """Reload the baseline model (if loaded) and close the instance."""
+        if self.mc is not None:
+            try:
+                if self.model_path:
+                    self.mc.load_from_file(str(self.model_path))
+            except Exception:
+                pass
+            try:
+                self.mc.quit()
+            except Exception:
+                pass
+            self.mc = None
+            self._log("Motor-CAD disconnected")
+
+    def load_model(self, model_path: str | Path) -> None:
+        """Load a .mot model file."""
+        self.model_path = str(Path(model_path).resolve())
+        self._log(f"Loading model: {self.model_path}")
+        self.mc.load_from_file(self.model_path)
+        self.mc.set_visible(True)
+        self.mc.display_screen("Scripting")
+        self._log("Model loaded")
+
+    def write_and_verify(self, variable: str, value: float) -> None:
+        """Write a variable and read it back. Raises RuntimeError on
+        mismatch."""
+        self.mc.set_variable(variable, value)
+        applied = float(self.mc.get_variable(variable))
+        if not math.isclose(applied, value, rel_tol=1e-8, abs_tol=1e-7):
+            raise RuntimeError(
+                f"Write verification failed for {variable}: "
+                f"wrote {value}, read {applied}"
+            )
+
+    def write_parameters(self, params: dict[str, float]) -> None:
+        """Write multiple parameters with write-back verification.
+        Params are written in dict order."""
+        for variable, value in params.items():
+            self._log(f"  Setting {variable} = {value}")
+            self.write_and_verify(variable, value)
+
+    def run_magnetic(self) -> None:
+        """Run the electromagnetic calculation."""
+        self._log("Starting magnetic calculation...")
+        start = time.time()
+        self.mc.do_magnetic_calculation()
+        elapsed = time.time() - start
+        self._log(f"Magnetic calculation finished in {elapsed:.1f}s")
+
+    def export_and_extract(self, output_dir: str | Path, tag: str = "") -> dict:
+        """Export EMagnetic results and extract all metrics.
+
+        Args:
+            output_dir: directory to save raw export CSV.
+            tag: optional tag for the filename (e.g. timestamp or case id).
+
+        Returns:
+            dict with metrics, raw_path, status, error.
+        """
+        result = {
+            "metrics": {},
+            "raw_path": "",
+            "status": "FAILED",
+            "error": "",
+        }
+        try:
+            out_dir = Path(output_dir)
+            out_dir.mkdir(parents=True, exist_ok=True)
+            ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
+            filename = f"result_{tag}_{ts}.csv" if tag else f"result_{ts}.csv"
+            raw_path = out_dir / filename
+            self.mc.export_results("EMagnetic", str(raw_path))
+            self._log(f"Raw results exported: {raw_path}")
+
+            parsed = parse_export(raw_path)
+            metrics = extract_all_metrics(parsed)
+            result["metrics"] = metrics
+            result["raw_path"] = str(raw_path)
+
+            ok, missing = check_required_metrics(metrics)
+            if ok:
+                result["status"] = "OK"
+            else:
+                result["status"] = "UNCERTAIN"
+                result["error"] = f"Missing required metrics: {missing}"
+                self._log(f"WARNING: {result['error']}")
+        except Exception as exc:
+            result["status"] = "FAILED"
+            result["error"] = f"{type(exc).__name__}: {exc}"
+            self._log(result["error"])
+            self._log(traceback.format_exc())
+
+        return result
+
+    def run_single(
+        self,
+        model_path: str | Path,
+        params: dict[str, float] | None = None,
+        output_dir: str | Path = "output",
+        tag: str = "",
+    ) -> dict:
+        """Run a complete single-point simulation.
+
+        Steps: load model -> write params -> magnetic calc -> export+extract.
+
+        Args:
+            model_path: path to .mot baseline model.
+            params: dict of variable_name -> value to set (optional).
+            output_dir: directory for raw export.
+            tag: tag for result filename.
+
+        Returns:
+            dict with metrics, status, error, raw_path, solve_time_s.
+        """
+        started = time.time()
+        result = {
+            "metrics": {},
+            "status": "FAILED",
+            "error": "",
+            "raw_path": "",
+            "solve_time_s": 0,
+            "params": params or {},
+        }
+        try:
+            self.load_model(model_path)
+            if params:
+                self.write_parameters(params)
+            self.run_magnetic()
+            ext = self.export_and_extract(output_dir, tag)
+            result.update(ext)
+        except Exception as exc:
+            result["status"] = "FAILED"
+            result["error"] = f"{type(exc).__name__}: {exc}"
+            self._log(result["error"])
+            self._log(traceback.format_exc())
+
+        result["solve_time_s"] = round(time.time() - started, 1)
+        return result

+ 449 - 0
torqrippswap-master/DESKTOP_APP_WORKFLOW.md

@@ -0,0 +1,449 @@
+# 桌面仿真工具开发与协作工作流指南
+
+> 来源:轴向磁拉力仿真 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`

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 9080 - 0
torqrippswap-master/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot


+ 13 - 0
torqrippswap-master/torqrippswap/.gitignore

@@ -0,0 +1,13 @@
+__pycache__/
+*.pyc
+*.mot
+*.bak
+MARS-9S8P_SSSR_Halbach_PCB-V1.0/
+results/
+runs/
+output/
+build/
+dist/
+*.spec
+*.log
+build_info.py

+ 303 - 0
torqrippswap-master/torqrippswap/MOTORCAD_SCAN_KNOWLEDGE_BASE.md

@@ -0,0 +1,303 @@
+# Motor-CAD 参数扫描与寻优知识库
+
+版本:2026-08-26  
+适用目录:`E:\aitestlocal\codex\tr`  
+参考项目:`E:\aitestlocal\claude\motorcadopt`
+
+## 1. 目标与适用范围
+
+本知识库用于指导 Motor-CAD 模型的自动参数扫描、结果采集、GUI 工具维护和后续寻优。
+当前主要模型为:
+
+`MARS-9S8P_SSSR_Halbach_PCB-V1.0.mot`
+
+已经实现或验证的扫描类型包括:
+
+1. Halbach 中央磁块弧角单参数扫描;
+2. 整体磁铁极弧单参数扫描;
+3. 中央磁块弧角与槽口宽度的二维组合扫描;
+4. 槽口变化时 PCB 铜线宽度的公式联动。
+
+## 2. 已确认的 Motor-CAD 参数映射
+
+| GUI 名称 | Motor-CAD 自动化变量 | 单位 | 基线值 | 说明 |
+|---|---|---:|---:|---|
+| Central Mag Arc [ED] | `MagnetCentralArc_HalbachRing` | EDeg | 120 | Halbach 中央磁块弧角,不等于整体极弧 |
+| Magnet Arc [ED] | `Magnet_Arc_[ED]` | EDeg | 180 | 整体磁铁极弧 |
+| Slot Opening | `Slot_Opening` | mm | 8 | 基线等于槽宽,属于完全开口槽 |
+| Slot Width | `Slot_Width` | mm | 8 | Parallel Slot 模型的槽宽 |
+| PCB 铜线宽 | `Copper_Width` | mm | 3.39 | 已与槽口联动公式核对 |
+| 每电周期转矩点数 | `TorquePointsPerCycle` | points/cycle | 30 | 原模型值,速度快但存在混叠风险 |
+| 气隙内部网格点 | `AirgapMeshPoints_mesh` | points | 840 | 与 surface 参数通常成对设置 |
+| 气隙表面网格点 | `AirgapMeshPoints_layers` | points | 840 | 与 mesh 参数通常成对设置 |
+
+关键区分:
+
+- 扫描 `Central Mag Arc [ED]` 时必须写 `MagnetCentralArc_HalbachRing`。
+- 扫描 `Magnet Arc [ED]` 时必须写 `Magnet_Arc_[ED]`。
+- 两者代表不同的几何含义,不能混用。
+
+## 3. 自动化连接与运行约束
+
+### 3.1 Windows 原生环境
+
+Motor-CAD 通过 Windows RPC/COM 风格接口驱动,必须使用 Windows 原生 Python。
+WSL 或 Linux 环境不能连接本机 Motor-CAD 实例。
+
+主要依赖:
+
+```text
+ansys-motorcad-core
+tkinter
+PyInstaller
+```
+
+### 3.2 前台可见
+
+自动求解使用独立 Motor-CAD 实例,并调用:
+
+```python
+mc = pymotorcad.MotorCAD(open_new_instance=True, keep_instance_open=False)
+mc.set_visible(True)
+```
+
+这样可以避免连接到正在被其它程序控制的 Motor-CAD 实例,同时保证用户能看到新实例。
+
+### 3.3 多实例风险
+
+`open_new_instance=False` 会尝试连接现有实例。机器上同时存在多个 Motor-CAD 时,可能连接到错误模型。
+除非已通过进程和 RPC 端口明确识别目标实例,否则不要使用自动连接现有实例的方式运行批处理。
+
+### 3.4 每点重新加载基线
+
+每个扫描组合开始前必须执行:
+
+```python
+mc.load_from_file(model_path)
+```
+
+然后再写本点参数。这样可以避免上一点参数、求解开关或派生状态污染下一点。
+
+扫描结束时也应重新加载基线模型,使 Motor-CAD 不停留在最后一个扫描值。
+
+## 4. 参数写入规则
+
+### 4.1 只修改明确授权的设计变量
+
+单参数扫描只写该参数;求解采样点和网格属于数值设置,不是设计变量,应在结果清单中明确记录。
+
+### 4.2 写入后必须回读
+
+Motor-CAD 对不适用的参数有时会静默接受,因此每次写入都必须用 `get_variable` 回读并比较:
+
+```python
+mc.set_variable(variable, value)
+applied = float(mc.get_variable(variable))
+```
+
+若不一致,当前点应标为 `FAILED`,不能继续把结果当作有效数据。
+
+### 4.3 槽口与 PCB 铜线宽联动
+
+槽口改变时,铜线宽按以下公式计算:
+
+```text
+Copper_Width = (Slot_Opening - clearance) / 2 / conductor_count
+```
+
+当前默认:
+
+- `clearance = 0.2 mm`
+- `conductor_count = 1`
+- `/2` 表示槽两侧分配
+
+默认槽口扫描 5.4–7.4 mm、步长 0.2 mm 时:
+
+| Slot Opening (mm) | Copper Width (mm) |
+|---:|---:|
+| 5.4 | 2.6 |
+| 5.6 | 2.7 |
+| 5.8 | 2.8 |
+| 6.0 | 2.9 |
+| 6.2 | 3.0 |
+| 6.4 | 3.1 |
+| 6.6 | 3.2 |
+| 6.8 | 3.3 |
+| 7.0 | 3.4 |
+| 7.2 | 3.5 |
+| 7.4 | 3.6 |
+
+二维扫描中推荐写入顺序:
+
+1. `Slot_Opening`
+2. `Copper_Width`
+3. 磁铁弧角参数
+4. 逐项回读验证
+5. 执行磁场计算
+
+## 5. 扫描矩阵
+
+### 5.1 单参数扫描
+
+端点均包含在扫描序列中。如果终点无法由整数个步长严格到达,程序会额外加入终点。
+
+### 5.2 二参数扫描
+
+总仿真数为两个参数水平数的乘积:
+
+```text
+N_total = N_parameter_1 x N_parameter_2
+```
+
+示例:
+
+- Central Mag Arc:90–110 EDeg,步长 1,共 21 个水平;
+- Slot Opening:5.4–7.4 mm,步长 0.2,共 11 个水平;
+- 总仿真数:`21 x 11 = 231`。
+
+按 30 点/840 网格的实际速度约 2.5–4.2 分钟/点估算,231 点可能需要约 10–16 小时。
+GUI 必须在启动前显示组合总数和粗略时长,并要求再次确认。
+
+## 6. 采样点与气隙网格
+
+### 6.1 原模型快速设置
+
+```text
+TorquePointsPerCycle = 30
+Airgap mesh/layers = 840
+```
+
+优点:速度快,适合初筛。  
+风险:本机 9 槽 8 极模型的主要齿槽成分是 18 次电频谐波,30 点/电周期低于可靠分辨要求,
+可能发生混叠,因此不能把 30 点扫描的绝对脉动值直接当作最终结论。
+
+### 6.2 推荐设置
+
+- 快速趋势扫描:30 点/840 网格;
+- 中等可信度扫描:120 点,并采用与时间步对齐的气隙网格;
+- 最终候选复算:180 点和更细、已验证收敛的网格。
+
+已知坑:120 点搭配原始 840 网格时,Motor-CAD 会弹出网格与时间步不对齐警告,导致无人值守 RPC
+批处理失败。Motor-CAD 曾建议改为 960;参考项目的高精度研究还使用过 1680。
+
+原则:
+
+1. 不要随意组合采样点数和网格点数;
+2. 先做单点验证,确认无交互弹窗;
+3. 所有结果必须记录实际的 Torque points 和 airgap mesh;
+4. 快速扫描的最佳点必须用高精度设置复算。
+
+## 7. 结果指标
+
+GUI 与 CSV 应尽可能采集以下常用指标:
+
+| 类别 | Motor-CAD 结果名称 |
+|---|---|
+| 转矩脉动 | `Torque Ripple (VW) [%]`、`Torque Ripple (VW)` |
+| 平均转矩 | `Average torque (virtual work)` |
+| 系统效率 | `System Efficiency` |
+| 反电动势 | `Back EMF Line-Line Voltage (rms)` |
+| 反电动势失真 | `Harmonic Distortion Back EMF Line-Line Voltage` |
+| 总损耗 | `Total Losses (on load)` |
+| 铜耗 | `Armature DC Copper Loss (on load)` |
+| 磁钢损耗 | `Magnet Loss (on load)` |
+| 定子铁耗 | `Stator iron Loss [total] (on load)` |
+| 输入功率 | `Input Power` |
+| 输出功率 | `Output Power`,导出名称可能带状态后缀 |
+| 电磁功率 | `Electromagnetic Power`,导出名称可能带状态后缀 |
+| 轴转速 | `Shaft Speed` |
+| 空载转速 | `No load speed` |
+
+结果导出文件使用分号分隔,不是普通逗号 CSV。相同指标可能在多个段落重复,优先取
+`E-Magnetics`,再按 `Drive`、`Losses`、`Materials`、`Miscellaneous` 搜索。
+
+## 8. 时间戳与可恢复性
+
+每次 GUI 运行应创建独立目录:
+
+```text
+runs/YYYYMMDD_HHMMSS_mmm_<scan_name>/
+```
+
+目录内至少包含:
+
+- `run_manifest_<timestamp>.json`:模型、参数范围、完整组合、数值设置和 Git 提交;
+- `scan_results_<timestamp>.csv`:逐点汇总结果;
+- `program_log_<timestamp>.log`:毫秒级时间戳日志;
+- `raw/result_<index>_<values>_<timestamp>.csv`:每点 Motor-CAD 原始导出。
+
+每个点完成后立即写 CSV 并刷新文件,不能等整批完成后一次性保存。失败点记录错误并继续下一点。
+
+## 9. GUI 与 EXE 版本
+
+| 版本/程序 | 功能 |
+|---|---|
+| `MotorCADParameterScanV2` | 单参数扫描,稳定的三行设置布局 |
+| `MotorCADParameterScanV3` | Central Mag Arc × Slot Opening 二维扫描,带 Copper Width 联动 |
+| `MotorCADMagnetArcScan` | V2 单参数布局,扫描整体 `Magnet_Arc_[ED]` |
+
+单参数 V2 布局要求:
+
+1. 第一行:模型路径;
+2. 第二行:参数、Start、Stop、Step;
+3. 第三行:Torque points、Airgap mesh;
+4. 每项必须拥有独立的标签和输入框,不得复用同一 Grid 列导致控件覆盖。
+
+## 10. Git 与源码规则
+
+### 10.1 源码字符集
+
+所有 `.py` 和 `.ps1` 脚本必须只包含 ASCII 字符。中文说明放在 Markdown 文档中,不能写进脚本注释、
+窗口标题、帮助文本或日志模板。
+
+建议检查:
+
+```powershell
+rg -n "[^\x00-\x7F]" --glob '*.py' --glob '*.ps1' .
+```
+
+### 10.2 运行前必须提交
+
+每次实际启动 Motor-CAD 求解前:
+
+1. 必须存在 Git 提交;
+2. 所有已跟踪文件必须无未提交修改;
+3. 运行清单必须记录当前短提交号;
+4. 未跟踪的模型结果目录不应自动加入提交或删除。
+
+GUI 的 Git preflight 若失败,必须拒绝启动求解。
+
+## 11. 常见故障与处理
+
+| 现象 | 原因 | 处理 |
+|---|---|---|
+| 连接成功但控制了错误窗口 | 同时运行多个 Motor-CAD,自动连接命中错误实例 | 新建独立实例并设为可见 |
+| RPC 在计算开始时报 Warning/Error | Motor-CAD 弹出交互警告 | 先做单点验证;修正网格/时间步组合 |
+| 参数写成功但几何不变 | 参数不适用于当前机型,Motor-CAD 静默接受 | 回读只是最低要求;存疑参数还要截图或结果对照验证 |
+| 扫描点之间结果异常累积 | 未重新加载基线 | 每点先 `load_from_file` |
+| GUI 看不到 Step 输入框 | Grid 列发生标签与输入框覆盖 | 使用 V2 三行布局,每组控件放独立 Frame |
+| 长时间任务中途丢失全部结果 | 批次结束才保存 | 每点立即追加 CSV、写日志并 flush |
+| 30 点结果看似更优 | 18 次谐波混叠 | 只用于趋势筛选,高精度复算确认 |
+| Git 报 dubious ownership | 仓库所有者与运行账户不同 | 每条命令使用 `git -c safe.directory=E:/aitestlocal/codex/tr ...` |
+
+## 12. 推荐寻优流程
+
+1. 确认模型、设计变量和真实自动化变量名;
+2. 检查所有脚本为纯 ASCII;
+3. 提交 Git,并确认已跟踪工作区干净;
+4. 用 1–2 个点验证前台实例、参数回读、结果字段和无弹窗运行;
+5. 使用 30 点/840 网格做粗筛;
+6. 根据趋势缩小参数区间,而不是盲目扩大二维全网格;
+7. 在候选区域用更小步长加密;
+8. 用 180 点和收敛网格复算最佳候选;
+9. 同时检查 Torque Ripple、Tavg、效率、损耗、反电动势和转速,不只看单一目标;
+10. 保存参数、原始结果、汇总表、日志、Git 提交和简单分析。
+
+## 13. 当前工程结论
+
+- `Central Mag Arc [ED]` 与 `Magnet Arc [ED]` 是两个不同参数,工具和结果命名必须明确区分。
+- 9 槽 8 极模型的脉动评估对采样点数和气隙网格非常敏感。
+- 原模型 30 点设置适合快速筛选,不适合直接宣称最终脉动真值。
+- 槽口从完全开口状态收窄是重要设计方向,但改变槽口时必须同步处理 PCB 铜线宽几何。
+- 二维全网格很容易达到数百次仿真;应优先粗筛、缩小区域、再加密和高精度复算。
+- 可靠的自动化不仅要“能跑”,还必须具备前台可见、参数回读、逐点落盘、时间戳、失败续跑和 Git 可追溯性。

+ 274 - 0
torqrippswap-master/torqrippswap/README_AI_TOOLS.md

@@ -0,0 +1,274 @@
+# 使用任意 AI 编程工具维护 Motor-CAD 扫描项目
+
+本文面向使用 Claude Code、Codex CLI、Cursor、GitHub Copilot、Kiro、Cline、Continue 或其它
+AI 编程工具的开发者。目标是让新的 AI 助手在不了解历史对话的情况下,也能安全维护、构建和运行
+本项目。
+
+## 1. 项目用途
+
+本项目使用 Ansys Motor-CAD 的 Python 自动化接口进行参数扫描,并提供 Windows GUI/EXE:
+
+- 设置模型文件;
+- 设置参数范围和步长;
+- 启动独立、前台可见的 Motor-CAD 实例;
+- 实时显示进度和仿真结果;
+- 保存带详细时间戳的汇总 CSV、原始结果和程序日志;
+- 在每个扫描点重新加载基线模型,避免参数累积污染。
+
+## 2. AI 开始工作前必须阅读
+
+让 AI 按顺序阅读:
+
+1. `MOTORCAD_SCAN_KNOWLEDGE_BASE.md`
+2. `README_Central_Mag_Arc扫描.md`
+3. `motorcad_scan_gui.py`
+4. `scan_central_mag_arc.py`
+5. 与任务对应的参数配置 JSON
+6. `E:\aitestlocal\claude\motorcadopt\CLAUDE.md`
+7. `E:\aitestlocal\claude\motorcadopt\TORQUE_RIPPLE_ANALYSIS.md`
+
+其中 `MOTORCAD_SCAN_KNOWLEDGE_BASE.md` 是本项目的主要知识入口,包含参数映射、求解设置、
+运行约束、结果指标和已知 Motor-CAD 陷阱。
+
+## 3. 可直接复制给 AI 的首次提示词
+
+```text
+请先完整阅读 MOTORCAD_SCAN_KNOWLEDGE_BASE.md 和 README_AI_TOOLS.md,再检查当前 Git 状态。
+这是 Windows 原生 Motor-CAD 自动化项目。所有 .py 和 .ps1 源码必须只含 ASCII 字符。
+任何实际 Motor-CAD 求解启动前,必须先有 Git 提交,并确保所有已跟踪文件无未提交修改。
+Motor-CAD 必须使用独立、前台可见的实例,不能抢占其它程序正在使用的实例。
+不要猜 Motor-CAD 变量名;先从 .mot、已有参数表或探测结果确认,写入后必须回读校验。
+每个扫描点开始前重新加载基线模型,每点完成后立即保存 CSV 和日志。
+先说明你检查到的事实和计划,再进行修改。
+```
+
+## 4. 环境要求
+
+### 4.1 操作系统
+
+- Windows 10 或 Windows 11;
+- 已安装与模型兼容的 Ansys Motor-CAD;
+- 使用 Windows 原生 Python,不能使用 WSL Python。
+
+### 4.2 Python 依赖
+
+```powershell
+python -m pip install ansys-motorcad-core pyinstaller
+```
+
+Tkinter 通常随 Windows Python 安装。验证:
+
+```powershell
+python -c "import tkinter; import ansys.motorcad.core; print('OK')"
+```
+
+### 4.3 Git
+
+本机可能出现仓库所有权保护错误。不要修改全局 Git 配置,使用每条命令的局部配置:
+
+```powershell
+git -c safe.directory=E:/aitestlocal/codex/tr status
+```
+
+## 5. 硬性工程约束
+
+### 5.1 脚本必须是纯 ASCII
+
+所有 `.py` 和 `.ps1` 文件只能包含 ASCII 字符。中文说明写在 Markdown 文件中。
+
+检查命令:
+
+```powershell
+rg -n "[^\x00-\x7F]" --glob '*.py' --glob '*.ps1' .
+```
+
+如果命令输出任何行,先修正,再构建或运行。
+
+### 5.2 运行前必须提交 Git
+
+实际启动 Motor-CAD 求解前必须满足:
+
+```powershell
+git -c safe.directory=E:/aitestlocal/codex/tr rev-parse --verify HEAD
+git -c safe.directory=E:/aitestlocal/codex/tr status --porcelain --untracked-files=no
+```
+
+第二条命令必须没有输出。GUI 内置 Git preflight,不满足时应拒绝启动扫描。
+
+### 5.3 不影响其它 Motor-CAD 实例
+
+如果其它程序正在使用 Motor-CAD,不要使用模糊的“连接已有实例”方式。扫描程序应创建独立实例:
+
+```python
+mc = pymotorcad.MotorCAD(open_new_instance=True, keep_instance_open=False)
+mc.set_visible(True)
+```
+
+### 5.4 参数必须验证
+
+不能只调用 `set_variable`。必须回读:
+
+```python
+mc.set_variable(variable, value)
+applied = mc.get_variable(variable)
+```
+
+回读不一致时将该点标记为 `FAILED`,保存错误并继续下一点。
+
+## 6. 当前关键参数
+
+| 用户界面名称 | Motor-CAD 变量 | 说明 |
+|---|---|---|
+| Central Mag Arc [ED] | `MagnetCentralArc_HalbachRing` | Halbach 中央磁块弧角 |
+| Magnet Arc [ED] | `Magnet_Arc_[ED]` | 整体磁铁极弧 |
+| Slot Opening | `Slot_Opening` | 槽口宽度 |
+| Copper Width | `Copper_Width` | PCB 铜线宽度 |
+
+`Central Mag Arc [ED]` 和 `Magnet Arc [ED]` 不是同一个参数,禁止混用。
+
+## 7. 主要程序和配置
+
+| 文件 | 用途 |
+|---|---|
+| `motorcad_scan_gui.py` | GUI 主程序 |
+| `scan_central_mag_arc.py` | 单参数扫描、结果解析和分析基础代码 |
+| `scan_parameters.json` | 二参数扫描配置 |
+| `scan_parameters_magnet_arc.json` | Magnet Arc 单参数配置 |
+| `build_gui_exe.ps1` | PyInstaller 通用构建脚本 |
+| `package_magnet_arc_v2.ps1` | Magnet Arc V2 单参数包生成脚本 |
+| `MOTORCAD_SCAN_KNOWLEDGE_BASE.md` | 项目知识库 |
+
+## 8. 构建 EXE
+
+构建前先提交所有已跟踪修改:
+
+```powershell
+git -c safe.directory=E:/aitestlocal/codex/tr add -- <files>
+git -c safe.directory=E:/aitestlocal/codex/tr commit -m "Describe the change"
+```
+
+构建通用 GUI:
+
+```powershell
+powershell.exe -NoLogo -ExecutionPolicy Bypass -File .\build_gui_exe.ps1 -AppName MotorCADParameterScanV3
+```
+
+生成 Magnet Arc 单参数 V2 包:
+
+```powershell
+powershell.exe -NoLogo -ExecutionPolicy Bypass -File .\package_magnet_arc_v2.ps1
+```
+
+输出位于:
+
+```text
+dist/<AppName>/<AppName>.exe
+```
+
+不要覆盖仍在运行的旧版 EXE 目录。需要修改布局或逻辑时使用新的应用名,例如 V4。
+
+## 9. 运行方法
+
+1. 启动对应 EXE;
+2. Browse 选择 `.mot` 模型;
+3. 检查扫描参数、起点、终点和步长;
+4. 检查 Torque points 和 Airgap mesh;
+5. 二参数扫描时确认两个水平数量的乘积;
+6. 点击 Start;
+7. 确认弹窗中的总仿真数和预计耗时;
+8. 观察前台 Motor-CAD 和 GUI 进度;
+9. 需要停止时使用 `Stop after current point`,不要强制结束当前求解。
+
+## 10. 输出文件
+
+每次运行创建:
+
+```text
+runs/YYYYMMDD_HHMMSS_mmm_<scan_name>/
+```
+
+主要文件:
+
+- `run_manifest_<timestamp>.json`:完整运行配置和 Git 提交;
+- `scan_results_<timestamp>.csv`:汇总结果;
+- `program_log_<timestamp>.log`:详细时间戳日志;
+- `raw/result_*.csv`:每个扫描点的原始 Motor-CAD 导出结果。
+
+分析问题时,AI 应先读取 manifest 和 log,再读取汇总 CSV,最后只打开相关的 raw 文件。
+
+## 11. 结果指标
+
+程序当前关注:
+
+- Torque Ripple,百分比和 Nm;
+- Average torque (virtual work);
+- System Efficiency;
+- Back EMF 及 THD;
+- Total Losses;
+- DC copper loss;
+- Magnet loss;
+- Stator iron loss;
+- Input、Output 和 Electromagnetic Power;
+- Shaft speed 和 No-load speed。
+
+Motor-CAD 导出文件使用分号分隔,同一指标可能在多个段落重复或带状态后缀。不要用普通逗号 CSV
+解析器直接读取原始结果。
+
+## 12. 修改或增加扫描参数
+
+AI 不得凭记忆猜变量名。推荐流程:
+
+1. 在 `.mot` 文件中搜索 GUI 名称附近的候选变量;
+2. 查询 `motorcadopt/results/mot_params_relevant.csv`;
+3. 用单点脚本写入并回读;
+4. 必要时保存几何截图,确认参数确实改变模型;
+5. 将已验证映射加入配置和知识库;
+6. 添加范围检查;
+7. 用 1–2 点 dry run 或真实单点验证;
+8. 提交 Git 后才允许完整扫描。
+
+## 13. 给 AI 的具体任务模板
+
+### 增加单参数扫描
+
+```text
+请在不影响现有 EXE 的前提下,增加 <GUI 参数名> 单参数扫描。
+先从 .mot 和已验证参数表确认 Motor-CAD 自动化变量名,不要猜。
+沿用 V2 三行布局:模型;参数与 Start/Stop/Step;Torque points/Airgap mesh。
+所有 .py/.ps1 只能包含 ASCII。写入后回读校验,每点重新加载基线,每点立即保存结果。
+修改完成后运行静态检查,提交 Git,再用新的应用名构建 EXE。不要启动完整仿真。
+```
+
+### 增加二参数扫描
+
+```text
+请将参数 A 和参数 B 做笛卡尔积扫描,总数量必须显示为 NA x NB。
+启动前显示组合总数和预计时长并要求确认。
+每个点重新加载基线,按依赖顺序写参数并逐个回读。
+结果 CSV 必须同时记录 A、B、所有联动值、求解设置和 Git 提交。
+保留旧版 EXE,构建新的版本目录。
+```
+
+### 分析扫描结果
+
+```text
+请先读取最新 runs 目录中的 manifest、log 和 scan_results CSV。
+检查失败点、参数写入、Torque points、airgap mesh 和 Git 提交。
+按 Torque Ripple 排序,同时施加 Tavg、效率、损耗和转速约束。
+快速扫描只用于趋势判断,给出需要用 180 点和收敛网格复算的候选列表。
+```
+
+## 14. 提交前检查清单
+
+- [ ] 阅读知识库;
+- [ ] 确认真实 Motor-CAD 参数名;
+- [ ] 不影响其它 Motor-CAD 实例;
+- [ ] `.py`、`.ps1` 纯 ASCII;
+- [ ] Python 编译检查通过;
+- [ ] JSON 能正常解析;
+- [ ] Git diff 无空白错误;
+- [ ] 已跟踪修改已提交;
+- [ ] EXE 使用新目录构建;
+- [ ] GUI 启动测试不自动开始求解;
+- [ ] 完整扫描前先做单点验证。
+

+ 61 - 0
torqrippswap-master/torqrippswap/README_Central_Mag_Arc扫描.md

@@ -0,0 +1,61 @@
+# Central Mag Arc 扫描脚本
+
+脚本通过 Motor-CAD 的 Windows COM 接口,扫描当前模型的
+`Central Mag Arc [ED]`(自动化名 `MagnetCentralArc_HalbachRing`),并输出
+Torque Ripple 列表与简单分析。
+
+## 使用前
+
+1. 在 Motor-CAD 中打开目标 `.mot`(脚本连接现有实例,不新开实例)。
+2. 使用安装了 `ansys-motorcad-core` 的 Windows 原生 Python。
+3. 关闭或暂停其它会控制同一 Motor-CAD 实例的脚本。
+
+若同时开了多个 Motor-CAD,先按进程 ID 找监听端口,再显式传给脚本:
+
+```powershell
+Get-CimInstance Win32_Process -Filter "Name='MotorCAD.exe'" | Format-List ProcessId,CommandLine
+netstat -ano | Select-String '<目标进程ID>$'
+python .\scan_central_mag_arc.py --port 65479
+```
+
+选择该进程的 IPv4 `LISTENING` 端口;端口号每次启动可能变化,示例值不能长期复用。
+若现有窗口含未保存工作,使用独立实例最安全:
+
+```powershell
+python .\scan_central_mag_arc.py --new-instance
+```
+
+## 命令
+
+```powershell
+python .\scan_central_mag_arc.py
+```
+
+默认:80 到 120 EDeg,步长 1,`TorquePointsPerCycle=120`,气隙网格
+`mesh/layers=960`。120 点与原始 840 网格不对齐,会触发 Motor-CAD 交互弹窗;960 是
+Motor-CAD 提示的兼容值,同时比参考项目最终复算用的 1680 更快。
+
+自定义范围和步长:
+
+```powershell
+python .\scan_central_mag_arc.py --start 90 --stop 110 --step 2
+```
+
+最终候选高精度复算:
+
+```powershell
+python .\scan_central_mag_arc.py --start 96 --stop 104 --step 1 --torque-points 180 --airgap-mesh 1440 --no-resume
+```
+
+若要求连求解采样设置也完全保持模型原值(原模型为 30 点,但会混叠 18 次谐波):
+
+```powershell
+python .\scan_central_mag_arc.py --torque-points 0 --airgap-mesh 0 --no-resume
+```
+
+结果写入 `results/central_mag_arc_scan.csv`,每个点完成后立即落盘;中断后重跑会跳过
+已有成功点。`results/central_mag_arc_analysis.md` 是自动生成的结果表和简要分析。
+
+每个点开始前脚本都会重新加载原始 `.mot`,然后只修改 Central Mag Arc;结束后再次加载
+原模型,因此不会把最后一个扫描值留在当前模型中。默认的 120 点仅改变本次求解的数值采样,
+不改变其它几何、材料、绕组或工况参数。

+ 31 - 0
torqrippswap-master/torqrippswap/build_gui_exe.ps1

@@ -0,0 +1,31 @@
+param(
+  [string]$AppName = 'MotorCADParameterScan'
+)
+
+$ErrorActionPreference = 'Stop'
+Set-Location -LiteralPath $PSScriptRoot
+
+$safeDir = $PSScriptRoot.Replace('\', '/')
+$commit = git -c "safe.directory=$safeDir" rev-parse --short HEAD
+if ($LASTEXITCODE -ne 0) {
+    throw 'No Git commit exists.'
+}
+$dirty = git -c "safe.directory=$safeDir" status --porcelain --untracked-files=no
+if ($dirty) {
+    throw 'Tracked files have uncommitted changes. Commit before building.'
+}
+
+$buildInfo = "BUILD_COMMIT = '$commit'`n"
+[System.IO.File]::WriteAllText((Join-Path $PSScriptRoot 'build_info.py'), $buildInfo, [System.Text.Encoding]::ASCII)
+
+python -m PyInstaller `
+  --noconfirm `
+  --clean `
+  --windowed `
+  --name $AppName `
+  --collect-all ansys.motorcad.core `
+  --add-data "scan_parameters.json;." `
+  motorcad_scan_gui.py
+
+Copy-Item -LiteralPath '.\scan_parameters.json' -Destination ".\dist\$AppName\scan_parameters.json" -Force
+Write-Host "Build complete: $PSScriptRoot\dist\$AppName\$AppName.exe"

+ 75 - 0
torqrippswap-master/torqrippswap/build_studio_exe.ps1

@@ -0,0 +1,75 @@
+# Build Motor-CAD Scan Studio as a single-file Windows executable.
+# Requires: git commit with clean working tree, Python 3.10+, PyQt5,
+# ansys-motorcad-core, pyinstaller.
+#
+# Usage:
+#   powershell -ExecutionPolicy Bypass -File .\build_studio_exe.ps1
+#   powershell -ExecutionPolicy Bypass -File .\build_studio_exe.ps1 -AppName MotorCADScanStudio
+
+param(
+  [string]$AppName = 'MotorCADScanStudio',
+  [string]$PythonExe = 'C:\Users\admin\AppData\Local\Programs\Python\Python312\python.exe'
+)
+
+$ErrorActionPreference = 'Stop'
+Set-Location -LiteralPath $PSScriptRoot
+
+# --- Git preflight: must have a commit and clean tracked files ---
+$safeDir = $PSScriptRoot.Replace('\', '/')
+$commit = git -c "safe.directory=$safeDir" rev-parse --short HEAD
+if ($LASTEXITCODE -ne 0) {
+    throw 'No Git commit exists. Commit before building.'
+}
+$dirty = git -c "safe.directory=$safeDir" status --porcelain --untracked-files=no
+if ($dirty) {
+    throw 'Tracked files have uncommitted changes. Commit before building.'
+}
+Write-Host "Git commit: $commit" -ForegroundColor Cyan
+
+# --- Embed build info ---
+$buildInfo = "BUILD_COMMIT = '$commit'`nAPP_VERSION = '1.0.0'`n"
+[System.IO.File]::WriteAllText(
+    (Join-Path $PSScriptRoot 'build_info.py'),
+    $buildInfo,
+    [System.Text.Encoding]::ASCII
+)
+
+# --- PyInstaller: one-file, windowed ---
+if (-not (Test-Path -LiteralPath $PythonExe)) {
+    throw "Python executable not found: $PythonExe"
+}
+$pyVersion = & $PythonExe --version 2>&1
+Write-Host "Using Python: $PythonExe ($pyVersion)" -ForegroundColor Cyan
+Write-Host 'Building with PyInstaller...' -ForegroundColor Cyan
+& $PythonExe -m PyInstaller `
+  --noconfirm `
+  --clean `
+  --onefile `
+  --windowed `
+  --name $AppName `
+  --hidden-import solver `
+  --collect-submodules ansys `
+  --add-data "scan_parameters.json;." `
+  main.py
+
+if ($LASTEXITCODE -ne 0) {
+    throw 'PyInstaller build failed.'
+}
+
+# --- Copy config next to exe ---
+$distDir = Join-Path $PSScriptRoot "dist\$AppName"
+if (-not (Test-Path -LiteralPath $distDir)) {
+    # onefile mode puts exe directly in dist/
+    $distDir = Join-Path $PSScriptRoot 'dist'
+}
+Copy-Item -LiteralPath '.\scan_parameters.json' -Destination (Join-Path $distDir 'scan_parameters.json') -Force
+
+$exePath = Join-Path $distDir "$AppName.exe"
+if (Test-Path -LiteralPath $exePath) {
+    $sizeMB = [math]::Round((Get-Item -LiteralPath $exePath).Length / 1MB, 1)
+    Write-Host ""
+    Write-Host "Build complete: $exePath" -ForegroundColor Green
+    Write-Host "Size: $sizeMB MB" -ForegroundColor Green
+} else {
+    Write-Host "Build finished but exe not found at $exePath" -ForegroundColor Yellow
+}

+ 1006 - 0
torqrippswap-master/torqrippswap/main.py

@@ -0,0 +1,1006 @@
+"""Motor-CAD Parameter Scan Studio - PyQt5 desktop GUI (Chinese UI).
+
+Modern industrial-style GUI for Motor-CAD torque ripple parameter
+scans. Uses solver.py as the computation core (no GUI dependency).
+Five-layer crash protection, QThread worker, bilingual metric
+extraction, and per-point CSV/log persistence.
+
+All source is ASCII; Chinese UI strings use \\uXXXX escapes.
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import sys
+import time
+import traceback
+from datetime import datetime
+from pathlib import Path
+
+from PyQt5.QtCore import QObject, Qt, QThread, QTimer, pyqtSignal
+from PyQt5.QtGui import QFont, QTextCursor
+from PyQt5.QtWidgets import (
+    QApplication,
+    QComboBox,
+    QDoubleSpinBox,
+    QFileDialog,
+    QFrame,
+    QGridLayout,
+    QGroupBox,
+    QHBoxLayout,
+    QLabel,
+    QLineEdit,
+    QMainWindow,
+    QMessageBox,
+    QProgressBar,
+    QPushButton,
+    QSpinBox,
+    QStatusBar,
+    QTableWidget,
+    QTableWidgetItem,
+    QTextEdit,
+    QVBoxLayout,
+    QWidget,
+)
+
+# Import solver core.
+from solver import (
+    METRIC_KEYS,
+    METRIC_LABELS,
+    MotorCADSolver,
+    git_preflight,
+    values_inclusive,
+)
+
+APP_NAME = "Motor-CAD \u53c2\u6570\u626b\u63cf\u5de5\u4f5c\u5ba4"
+APP_VERSION = "1.0.0"
+PROFILE_FILE = "scan_parameters.json"
+DEFAULT_MODEL_NAME = "MARS-9S8P_SSSR_Halbach_PCB-V1.0.mot"
+
+# Key metrics shown as large result cards.
+CARD_METRICS = [
+    ("ripple_pct", "%"),
+    ("ripple_nm", "Nm"),
+    ("tavg_nm", "Nm"),
+    ("efficiency_pct", "%"),
+    ("total_losses_w", "W"),
+    ("back_emf_v", "V"),
+]
+
+# Chinese labels for metrics (overrides solver.py English labels).
+METRIC_LABELS_ZH = {
+    "ripple_pct": "\u8f6c\u77e9\u8109\u52a8 [%]",
+    "ripple_nm": "\u8f6c\u77e9\u8109\u52a8 [Nm]",
+    "tavg_nm": "\u5e73\u5747\u8f6c\u77e9 [Nm]",
+    "efficiency_pct": "\u7cfb\u7edf\u6548\u7387 [%]",
+    "back_emf_v": "\u53cd\u7535\u52a8\u52bf LL \u6709\u6548\u503c [V]",
+    "back_emf_thd_pct": "\u53cd\u7535\u52a8\u52bf\u8c10\u6ce2 [%]",
+    "total_losses_w": "\u603b\u635f\u8017 [W]",
+    "copper_loss_w": "\u76f4\u6d41\u94dc\u8017 [W]",
+    "magnet_loss_w": "\u78c1\u94a2\u635f\u8017 [W]",
+    "iron_loss_w": "\u5b9a\u5b50\u94c1\u8017 [W]",
+    "input_power_w": "\u8f93\u5165\u529f\u7387 [W]",
+    "output_power_w": "\u8f93\u51fa\u529f\u7387 [W]",
+    "em_power_w": "\u7535\u78c1\u529f\u7387 [W]",
+    "shaft_speed_rpm": "\u8f74\u8f6c\u901f [rpm]",
+    "no_load_speed_rpm": "\u7a7a\u8f7d\u8f6c\u901f [rpm]",
+    "shaft_torque_nm": "\u8f74\u8f6c\u77e9 [Nm]",
+}
+
+# Global reference to keep long-lived objects alive (crash protection layer 5).
+_GLOBAL_SOLVER_REF = None
+_GLOBAL_WORKER_REF = None
+_GLOBAL_THREAD_REF = None
+
+
+# ---------------------------------------------------------------------------
+# Crash protection layer 1: global exception hook
+# ---------------------------------------------------------------------------
+
+def _global_excepthook(exc_type, exc_value, exc_tb):
+    """Write uncaught exceptions to a log file and show a dialog."""
+    msg = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
+    log_path = os.path.join(os.path.expanduser("~"), "motorcad_scan_studio_error.log")
+    try:
+        with open(log_path, "a", encoding="utf-8") as fh:
+            fh.write(f"\n=== {time.strftime('%Y-%m-%d %H:%M:%S')} ===\n{msg}")
+    except Exception:
+        pass
+    try:
+        QMessageBox.critical(None, "\u5e94\u7528\u5f02\u5e38", str(exc_value))
+    except Exception:
+        pass
+
+
+# ---------------------------------------------------------------------------
+# QSS stylesheet - modern industrial style
+# ---------------------------------------------------------------------------
+
+STYLESHEET = """
+QMainWindow, QWidget {
+    background-color: #eef1f5;
+    color: #1e293b;
+    font-family: "Segoe UI", "Microsoft YaHei", sans-serif;
+    font-size: 10pt;
+}
+QFrame#paramPanel {
+    background-color: #ffffff;
+    border: 1px solid #e2e8f0;
+    border-radius: 8px;
+}
+QGroupBox {
+    background-color: #ffffff;
+    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-color: #ffffff;
+    color: #1e293b;
+}
+QPushButton {
+    padding: 8px 20px;
+    border-radius: 6px;
+    background-color: #2563eb;
+    color: #ffffff;
+    border: 1px solid #2563eb;
+    font-weight: 600;
+    min-height: 32px;
+}
+QPushButton:hover { background-color: #1d4ed8; }
+QPushButton:disabled {
+    background-color: #94a3b8;
+    border-color: #94a3b8;
+}
+QPushButton#stopBtn {
+    background-color: #dc2626;
+    border-color: #dc2626;
+}
+QPushButton#stopBtn:hover { background-color: #b91c1c; }
+QPushButton#stopBtn:disabled {
+    background-color: #94a3b8;
+    border-color: #94a3b8;
+}
+QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox {
+    padding: 6px 10px;
+    border: 1px solid #cbd5e1;
+    border-radius: 6px;
+    background-color: #f8fafc;
+    min-height: 20px;
+}
+QLineEdit:focus, QComboBox:focus, QSpinBox:focus, QDoubleSpinBox:focus {
+    border-color: #2563eb;
+    background-color: #ffffff;
+}
+QLabel { color: #1e293b; }
+QLabel#sectionLabel {
+    font-size: 11pt;
+    font-weight: 600;
+    color: #1e293b;
+}
+QLabel#hintLabel {
+    font-size: 9pt;
+    color: #64748b;
+}
+QLabel#totalLabel {
+    font-size: 10pt;
+    font-weight: 600;
+    color: #2563eb;
+}
+QFrame#resultCard {
+    background-color: #ffffff;
+    border: 1px solid #e2e8f0;
+    border-radius: 10px;
+}
+QLabel#cardLabel {
+    font-size: 9pt;
+    color: #64748b;
+    font-weight: 500;
+    letter-spacing: 1px;
+}
+QLabel#cardValue {
+    font-size: 22pt;
+    font-weight: 700;
+    color: #1e293b;
+}
+QLabel#cardUnit {
+    font-size: 11pt;
+    color: #64748b;
+}
+QTextEdit#logView {
+    background-color: #f8fafc;
+    color: #334155;
+    border: 1px solid #e2e8f0;
+    border-radius: 8px;
+    padding: 10px;
+    font-family: Consolas, "Cascadia Code", monospace;
+    font-size: 9.5pt;
+}
+QTableWidget {
+    background-color: #ffffff;
+    border: 1px solid #e2e8f0;
+    border-radius: 8px;
+    gridline-color: #e2e8f0;
+    font-size: 9.5pt;
+}
+QHeaderView::section {
+    background-color: #f1f5f9;
+    color: #475569;
+    font-weight: 600;
+    padding: 6px;
+    border: none;
+    border-bottom: 1px solid #e2e8f0;
+}
+QProgressBar {
+    border: 1px solid #e2e8f0;
+    border-radius: 6px;
+    background-color: #f1f5f9;
+    text-align: center;
+    height: 20px;
+}
+QProgressBar::chunk {
+    background-color: #2563eb;
+    border-radius: 5px;
+}
+QStatusBar {
+    background-color: #1e293b;
+    color: #e2e8f0;
+    font-size: 9.5pt;
+}
+QStatusBar QLabel { color: #e2e8f0; }
+QScrollBar:vertical {
+    width: 10px;
+    background-color: #f1f5f9;
+}
+QScrollBar::handle:vertical {
+    background-color: #cbd5e1;
+    border-radius: 5px;
+}
+QScrollBar::handle:vertical:hover { background-color: #94a3b8; }
+"""
+
+
+# ---------------------------------------------------------------------------
+# Result card widget
+# ---------------------------------------------------------------------------
+
+class ResultCard(QFrame):
+    """A white card showing a metric label, large value, and unit."""
+
+    def __init__(self, label: str, unit: str, parent=None):
+        super().__init__(parent)
+        self.setObjectName("resultCard")
+        self.setFixedHeight(90)
+        layout = QVBoxLayout(self)
+        layout.setContentsMargins(14, 10, 14, 10)
+        layout.setSpacing(2)
+
+        self.label_widget = QLabel(label.upper())
+        self.label_widget.setObjectName("cardLabel")
+        layout.addWidget(self.label_widget)
+
+        value_row = QHBoxLayout()
+        value_row.setSpacing(4)
+        self.value_widget = QLabel("--")
+        self.value_widget.setObjectName("cardValue")
+        value_row.addWidget(self.value_widget)
+        self.unit_widget = QLabel(unit)
+        self.unit_widget.setObjectName("cardUnit")
+        value_row.addWidget(self.unit_widget)
+        value_row.addStretch()
+        layout.addLayout(value_row)
+
+    def set_value(self, value):
+        if value is None or value == "":
+            self.value_widget.setText("--")
+        else:
+            try:
+                self.value_widget.setText(f"{float(value):.4g}")
+            except (ValueError, TypeError):
+                self.value_widget.setText(str(value))
+
+
+# ---------------------------------------------------------------------------
+# Scan worker (QObject for QThread)
+# ---------------------------------------------------------------------------
+
+class ScanWorker(QObject):
+    """Runs a Motor-CAD parameter scan in a background thread."""
+
+    log = pyqtSignal(str)
+    row = pyqtSignal(dict)
+    progress = pyqtSignal(int, int)
+    finished = pyqtSignal(dict)
+    error = pyqtSignal(str)
+
+    def __init__(self, config: dict):
+        super().__init__()
+        self.config = config
+        self._solver = None
+
+    def run(self):
+        try:
+            self._solver = MotorCADSolver(
+                model_path=Path(self.config["model"]),
+                log_cb=lambda m: self.log.emit(m),
+                progress_cb=lambda i, t: self.progress.emit(i, t),
+                row_cb=lambda r: self.row.emit(r),
+            )
+            self._solver.connect()
+
+            result = self._solver.run_scan(
+                points=self.config["points"],
+                output_dir=Path(self.config["output_dir"]),
+                torque_points=self.config.get("torque_points", 0),
+                airgap_mesh=self.config.get("airgap_mesh", 0),
+                scan_name=self.config.get("scan_name", "scan"),
+                extra_csv_fields=self.config.get("extra_csv_fields", []),
+            )
+            self.finished.emit(result)
+        except Exception as exc:
+            self.error.emit(f"{type(exc).__name__}: {exc}\n{traceback.format_exc()}")
+        finally:
+            if self._solver is not None:
+                try:
+                    self._solver.disconnect()
+                except Exception:
+                    pass
+
+    def cancel(self):
+        if self._solver is not None:
+            self._solver.cancel()
+
+
+# ---------------------------------------------------------------------------
+# Main window
+# ---------------------------------------------------------------------------
+
+class MainWindow(QMainWindow):
+    def __init__(self):
+        super().__init__()
+        self.setWindowTitle(f"{APP_NAME} v{APP_VERSION}")
+        self.resize(1400, 900)
+        self.setMinimumSize(1100, 700)
+
+        self.profiles = self._load_profiles()
+        self.worker = None
+        self.thread = None
+        self.run_start_time = None
+
+        self._build_ui()
+        self._apply_profile(next(iter(self.profiles)) if self.profiles else None)
+
+    # -- utility --
+
+    def _app_root(self) -> Path:
+        if getattr(sys, "frozen", False):
+            return Path(sys.executable).resolve().parent
+        return Path(__file__).resolve().parent
+
+    def _load_profiles(self) -> dict:
+        path = self._app_root() / PROFILE_FILE
+        if path.exists():
+            try:
+                return json.loads(path.read_text(encoding="ascii"))
+            except (json.JSONDecodeError, OSError):
+                pass
+        return {}
+
+    def _default_model_path(self) -> str:
+        for base in (self._app_root(), *self._app_root().parents):
+            candidate = base / DEFAULT_MODEL_NAME
+            if candidate.exists():
+                return str(candidate)
+        return str(self._app_root() / DEFAULT_MODEL_NAME)
+
+    # -- UI construction --
+
+    def _build_ui(self):
+        central = QWidget()
+        self.setCentralWidget(central)
+        main_layout = QHBoxLayout(central)
+        main_layout.setContentsMargins(8, 8, 8, 4)
+        main_layout.setSpacing(8)
+
+        # Left: parameter panel.
+        self.param_panel = self._build_param_panel()
+        self.param_panel.setObjectName("paramPanel")
+        self.param_panel.setFixedWidth(380)
+        main_layout.addWidget(self.param_panel)
+
+        # Right: results + log.
+        right = QWidget()
+        right_layout = QVBoxLayout(right)
+        right_layout.setContentsMargins(0, 0, 0, 0)
+        right_layout.setSpacing(8)
+        right_layout.addWidget(self._build_results_area(), stretch=3)
+        right_layout.addWidget(self._build_log_area(), stretch=2)
+        main_layout.addWidget(right, stretch=1)
+
+        # Status bar.
+        self.status_bar = QStatusBar()
+        self.setStatusBar(self.status_bar)
+        self.status_label = QLabel("\u5c31\u7eea")
+        self.commit_label = QLabel("Git: --")
+        self.time_label = QLabel("\u6301\u7eed\u65f6\u95f4: --")
+        self.status_bar.addWidget(self.status_label, 1)
+        self.status_bar.addPermanentWidget(self.commit_label)
+        self.status_bar.addPermanentWidget(self.time_label)
+
+        # Check git on startup.
+        QTimer.singleShot(100, self._refresh_git_status)
+
+    def _build_param_panel(self) -> QWidget:
+        panel = QFrame()
+        layout = QVBoxLayout(panel)
+        layout.setContentsMargins(12, 12, 12, 12)
+        layout.setSpacing(10)
+
+        # Model group.
+        model_group = QGroupBox("\u6a21\u578b")
+        model_layout = QVBoxLayout(model_group)
+        model_row = QHBoxLayout()
+        self.model_edit = QLineEdit(self._default_model_path())
+        model_row.addWidget(self.model_edit, 1)
+        browse_btn = QPushButton("\u6d4f\u89c8")
+        browse_btn.setFixedWidth(70)
+        browse_btn.clicked.connect(self._browse_model)
+        model_row.addWidget(browse_btn)
+        model_layout.addLayout(model_row)
+        layout.addWidget(model_group)
+
+        # Profile group.
+        profile_group = QGroupBox("\u626b\u63cf\u914d\u7f6e")
+        profile_layout = QVBoxLayout(profile_group)
+        self.profile_combo = QComboBox()
+        self.profile_combo.addItems(list(self.profiles.keys()))
+        self.profile_combo.currentTextChanged.connect(self._apply_profile)
+        profile_layout.addWidget(self.profile_combo)
+        layout.addWidget(profile_group)
+
+        # Parameter 1 group.
+        p1_group = QGroupBox("\u53c2\u65701\uff1a\u4e2d\u592e\u78c1\u5757\u5f27\u89d2 [ED]")
+        p1_layout = QGridLayout(p1_group)
+        p1_layout.addWidget(QLabel("\u8d77\u59cb"), 0, 0)
+        self.p1_start = QDoubleSpinBox()
+        self.p1_start.setRange(0, 180)
+        self.p1_start.setDecimals(1)
+        self.p1_start.setSingleStep(1)
+        p1_layout.addWidget(self.p1_start, 0, 1)
+        p1_layout.addWidget(QLabel("\u7ec8\u6b62"), 0, 2)
+        self.p1_stop = QDoubleSpinBox()
+        self.p1_stop.setRange(0, 180)
+        self.p1_stop.setDecimals(1)
+        self.p1_stop.setSingleStep(1)
+        p1_layout.addWidget(self.p1_stop, 0, 3)
+        p1_layout.addWidget(QLabel("\u6b65\u957f"), 1, 0)
+        self.p1_step = QDoubleSpinBox()
+        self.p1_step.setRange(0.1, 50)
+        self.p1_step.setDecimals(1)
+        self.p1_step.setSingleStep(0.5)
+        p1_layout.addWidget(self.p1_step, 1, 1)
+        for spin in (self.p1_start, self.p1_stop, self.p1_step):
+            spin.valueChanged.connect(self._update_total)
+        layout.addWidget(p1_group)
+
+        # Parameter 2 group.
+        p2_group = QGroupBox("\u53c2\u65702\uff1a\u69fd\u53e3\u5bbd\u5ea6 [mm]")
+        p2_layout = QGridLayout(p2_group)
+        p2_layout.addWidget(QLabel("\u8d77\u59cb"), 0, 0)
+        self.p2_start = QDoubleSpinBox()
+        self.p2_start.setRange(0.2, 20)
+        self.p2_start.setDecimals(1)
+        self.p2_start.setSingleStep(0.2)
+        p2_layout.addWidget(self.p2_start, 0, 1)
+        p2_layout.addWidget(QLabel("\u7ec8\u6b62"), 0, 2)
+        self.p2_stop = QDoubleSpinBox()
+        self.p2_stop.setRange(0.2, 20)
+        self.p2_stop.setDecimals(1)
+        self.p2_stop.setSingleStep(0.2)
+        p2_layout.addWidget(self.p2_stop, 0, 3)
+        p2_layout.addWidget(QLabel("\u6b65\u957f"), 1, 0)
+        self.p2_step = QDoubleSpinBox()
+        self.p2_step.setRange(0.1, 10)
+        self.p2_step.setDecimals(1)
+        self.p2_step.setSingleStep(0.2)
+        p2_layout.addWidget(self.p2_step, 1, 1)
+        for spin in (self.p2_start, self.p2_stop, self.p2_step):
+            spin.valueChanged.connect(self._update_total)
+        layout.addWidget(p2_group)
+
+        # Linked copper width group.
+        link_group = QGroupBox("\u8054\u52a8\u94dc\u7ebf\u5bbd\u5ea6")
+        link_layout = QGridLayout(link_group)
+        link_layout.addWidget(QLabel("\u6bcf\u4fa7\u5bfc\u4f53\u6570"), 0, 0)
+        self.conductor_count = QSpinBox()
+        self.conductor_count.setRange(1, 10)
+        link_layout.addWidget(self.conductor_count, 0, 1)
+        link_layout.addWidget(QLabel("\u95f4\u9699 [mm]"), 0, 2)
+        self.clearance = QDoubleSpinBox()
+        self.clearance.setRange(0, 5)
+        self.clearance.setDecimals(2)
+        self.clearance.setSingleStep(0.1)
+        link_layout.addWidget(self.clearance, 0, 3)
+        formula_label = QLabel(
+            "\u94dc\u7ebf\u5bbd = (\u69fd\u53e3\u5bbd - \u95f4\u9699) / 2 / \u5bfc\u4f53\u6570"
+        )
+        formula_label.setObjectName("hintLabel")
+        link_layout.addWidget(formula_label, 1, 0, 1, 4)
+        layout.addWidget(link_group)
+
+        # Solver settings group.
+        solver_group = QGroupBox("\u6c42\u89e3\u8bbe\u7f6e")
+        solver_layout = QGridLayout(solver_group)
+        solver_layout.addWidget(QLabel("\u8f6c\u77e9\u91c7\u6837\u70b9"), 0, 0)
+        self.torque_points = QSpinBox()
+        self.torque_points.setRange(0, 500)
+        self.torque_points.setValue(30)
+        self.torque_points.setSpecialValueText("model default")
+        solver_layout.addWidget(self.torque_points, 0, 1)
+        solver_layout.addWidget(QLabel("\u6c14\u9699\u7f51\u683c"), 0, 2)
+        self.airgap_mesh = QSpinBox()
+        self.airgap_mesh.setRange(0, 5000)
+        self.airgap_mesh.setValue(840)
+        self.airgap_mesh.setSpecialValueText("model default")
+        solver_layout.addWidget(self.airgap_mesh, 0, 3)
+        layout.addWidget(solver_group)
+
+        # Total and buttons.
+        self.total_label = QLabel("\u603b\u7ec4\u5408\u6570: 0")
+        self.total_label.setObjectName("totalLabel")
+        layout.addWidget(self.total_label)
+
+        self.progress_bar = QProgressBar()
+        self.progress_bar.setValue(0)
+        layout.addWidget(self.progress_bar)
+
+        btn_row = QHBoxLayout()
+        self.start_btn = QPushButton("\u5f00\u59cb\u626b\u63cf")
+        self.start_btn.clicked.connect(self._start_scan)
+        btn_row.addWidget(self.start_btn)
+        self.stop_btn = QPushButton("\u5f53\u524d\u70b9\u540e\u505c\u6b62")
+        self.stop_btn.setObjectName("stopBtn")
+        self.stop_btn.setEnabled(False)
+        self.stop_btn.clicked.connect(self._stop_scan)
+        btn_row.addWidget(self.stop_btn)
+        layout.addLayout(btn_row)
+
+        layout.addStretch()
+        return panel
+
+    def _build_results_area(self) -> QWidget:
+        container = QWidget()
+        layout = QVBoxLayout(container)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.setSpacing(8)
+
+        # Result cards.
+        cards_header = QLabel("\u6700\u65b0\u70b9\u7ed3\u679c")
+        cards_header.setObjectName("sectionLabel")
+        layout.addWidget(cards_header)
+
+        cards_widget = QWidget()
+        cards_layout = QGridLayout(cards_widget)
+        cards_layout.setContentsMargins(0, 0, 0, 0)
+        cards_layout.setSpacing(8)
+        self.cards = {}
+        for idx, (key, unit) in enumerate(CARD_METRICS):
+            label = METRIC_LABELS_ZH.get(key, METRIC_LABELS.get(key, key))
+            card = ResultCard(label, unit)
+            row, col = divmod(idx, 3)
+            cards_layout.addWidget(card, row, col)
+            self.cards[key] = card
+        cards_layout.setColumnStretch(0, 1)
+        cards_layout.setColumnStretch(1, 1)
+        cards_layout.setColumnStretch(2, 1)
+        layout.addWidget(cards_widget)
+
+        # Full metrics table.
+        table_header = QLabel("\u5168\u90e8\u6307\u6807")
+        table_header.setObjectName("sectionLabel")
+        layout.addWidget(table_header)
+
+        self.metrics_table = QTableWidget()
+        self.metrics_table.setColumnCount(2)
+        self.metrics_table.setHorizontalHeaderLabels(
+            ["\u6307\u6807", "\u6570\u503c"]
+        )
+        self.metrics_table.verticalHeader().setVisible(False)
+        self.metrics_table.setRowCount(len(METRIC_KEYS))
+        for row_idx, key in enumerate(METRIC_KEYS):
+            label = METRIC_LABELS_ZH.get(key, METRIC_LABELS.get(key, key))
+            self.metrics_table.setItem(row_idx, 0, QTableWidgetItem(label))
+            self.metrics_table.setItem(row_idx, 1, QTableWidgetItem("--"))
+        self.metrics_table.horizontalHeader().setStretchLastSection(True)
+        layout.addWidget(self.metrics_table, stretch=1)
+
+        return container
+
+    def _build_log_area(self) -> QWidget:
+        container = QWidget()
+        layout = QVBoxLayout(container)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.setSpacing(4)
+
+        header = QLabel("\u7a0b\u5e8f\u65e5\u5fd7")
+        header.setObjectName("sectionLabel")
+        layout.addWidget(header)
+
+        self.log_view = QTextEdit()
+        self.log_view.setObjectName("logView")
+        self.log_view.setReadOnly(True)
+        layout.addWidget(self.log_view)
+
+        return container
+
+    # -- log helpers (HTML colored) --
+
+    def _append_log(self, text: str, color: str = "#334155"):
+        try:
+            escaped = text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
+            html = f'<span style="color:{color};">{escaped}</span>'
+            self.log_view.append(html)
+            self.log_view.moveCursor(QTextCursor.End)
+        except Exception:
+            pass
+
+    def _classify_log(self, text: str) -> str:
+        lower = text.lower()
+        if "error" in lower or "failed" in lower or "traceback" in lower:
+            return "#dc2626"
+        if "warning" in lower or "cancel" in lower:
+            return "#d97706"
+        if "complete" in lower or "ok" in lower or "ended" in lower:
+            return "#16a34a"
+        if "opening" in lower or "starting" in lower or "run directory" in lower:
+            return "#475569"
+        if "result" in lower or "csv" in lower or ".mot" in lower or "directory" in lower:
+            return "#7c3aed"
+        return "#334155"
+
+    # -- profile --
+
+    def _apply_profile(self, profile_name: str | None):
+        try:
+            if not profile_name or profile_name not in self.profiles:
+                return
+            profile = self.profiles[profile_name]
+            p1 = profile.get("parameter_1", {})
+            p2 = profile.get("parameter_2", {})
+            linked = profile.get("linked_value", {})
+            self.p1_start.setValue(float(p1.get("default_start", 90)))
+            self.p1_stop.setValue(float(p1.get("default_stop", 110)))
+            self.p1_step.setValue(float(p1.get("default_step", 1)))
+            self.p2_start.setValue(float(p2.get("default_start", 5.4)))
+            self.p2_stop.setValue(float(p2.get("default_stop", 7.4)))
+            self.p2_step.setValue(float(p2.get("default_step", 0.2)))
+            self.conductor_count.setValue(int(linked.get("default_conductor_count", 1)))
+            self.clearance.setValue(float(linked.get("default_clearance", 0.2)))
+            self._update_total()
+        except Exception as exc:
+            self._append_log(f"Profile apply error: {exc}", "#dc2626")
+
+    def _update_total(self):
+        try:
+            p1_vals = values_inclusive(
+                self.p1_start.value(), self.p1_stop.value(), self.p1_step.value()
+            )
+            p2_vals = values_inclusive(
+                self.p2_start.value(), self.p2_stop.value(), self.p2_step.value()
+            )
+            total = len(p1_vals) * len(p2_vals)
+            self.total_label.setText(
+                f"\u603b\u7ec4\u5408\u6570: {len(p1_vals)} x {len(p2_vals)} = {total}"
+            )
+        except ValueError:
+            self.total_label.setText("\u603b\u7ec4\u5408\u6570: invalid range")
+
+    # -- actions --
+
+    def _browse_model(self):
+        try:
+            path, _ = QFileDialog.getOpenFileName(
+                self,
+                "\u9009\u62e9 Motor-CAD \u6a21\u578b",
+                "",
+                "Motor-CAD models (*.mot);;All files (*.*)",
+            )
+            if path:
+                self.model_edit.setText(path)
+        except Exception as exc:
+            self._append_log(f"Browse error: {exc}", "#dc2626")
+
+    def _refresh_git_status(self):
+        try:
+            ok, commit = git_preflight(self._app_root())
+            if ok:
+                self.commit_label.setText(f"Git: {commit}")
+            else:
+                self.commit_label.setText(f"Git: {commit}")
+        except Exception:
+            self.commit_label.setText("Git: unavailable")
+
+    def _start_scan(self):
+        try:
+            # Git preflight.
+            ok, commit = git_preflight(self._app_root())
+            if not ok:
+                QMessageBox.warning(self, "Git \u9884\u68c0\u5931\u8d25", commit)
+                return
+
+            # Validate model.
+            model_path = Path(self.model_edit.text()).resolve()
+            if not model_path.exists():
+                QMessageBox.warning(
+                    self,
+                    "\u6a21\u578b\u672a\u627e\u5230",
+                    f"\u6a21\u578b\u4e0d\u5b58\u5728\uff1a\n{model_path}",
+                )
+                return
+
+            # Build points.
+            p1_vals = values_inclusive(
+                self.p1_start.value(), self.p1_stop.value(), self.p1_step.value()
+            )
+            p2_vals = values_inclusive(
+                self.p2_start.value(), self.p2_stop.value(), self.p2_step.value()
+            )
+            clearance = self.clearance.value()
+            conductors = self.conductor_count.value()
+
+            # Range checks.
+            profile_name = self.profile_combo.currentText()
+            profile = self.profiles.get(profile_name, {})
+            p1_cfg = profile.get("parameter_1", {})
+            p2_cfg = profile.get("parameter_2", {})
+            if p1_cfg:
+                if p1_vals[0] < p1_cfg.get("minimum", 0) or p1_vals[-1] > p1_cfg.get("maximum", 180):
+                    QMessageBox.warning(
+                        self, "\u8303\u56f4\u9519\u8bef",
+                        f"\u53c2\u65701\u5fc5\u987b\u5728\u8303\u56f4\u5185 "
+                        f"{p1_cfg.get('minimum', 0)}..{p1_cfg.get('maximum', 180)}",
+                    )
+                    return
+            if p2_cfg:
+                if p2_vals[0] < p2_cfg.get("minimum", 0.2) or p2_vals[-1] > p2_cfg.get("maximum", 20):
+                    QMessageBox.warning(
+                        self, "\u8303\u56f4\u9519\u8bef",
+                        f"\u53c2\u65702\u5fc5\u987b\u5728\u8303\u56f4\u5185 "
+                        f"{p2_cfg.get('minimum', 0.2)}..{p2_cfg.get('maximum', 20)}",
+                    )
+                    return
+            if clearance >= p2_vals[0]:
+                QMessageBox.warning(
+                    self, "\u8303\u56f4\u9519\u8bef",
+                    "\u95f4\u9699\u5fc5\u987b\u5c0f\u4e8e\u6bcf\u4e2a\u69fd\u53e3\u5bbd\u5ea6",
+                )
+                return
+
+            total = len(p1_vals) * len(p2_vals)
+            est_hours = total * 180.0 / 3600.0
+            confirm = QMessageBox.question(
+                self,
+                "\u786e\u8ba4\u626b\u63cf",
+                f"\u53c2\u65701\u6c34\u5e73\u6570: {len(p1_vals)}\n"
+                f"\u53c2\u65702\u6c34\u5e73\u6570: {len(p2_vals)}\n"
+                f"\u603b\u4eff\u771f\u6570: {total}\n"
+                f"\u9884\u8ba1\u8017\u65f6: {est_hours:.1f} \u5c0f\u65f6\n\n"
+                f"\u5f00\u59cb\u626b\u63cf\uff1f",
+                QMessageBox.Yes | QMessageBox.No,
+            )
+            if confirm != QMessageBox.Yes:
+                return
+
+            # Build point list.
+            points = []
+            for slot_opening in p2_vals:
+                copper_width = round((slot_opening - clearance) / 2.0 / conductors, 10)
+                for magnet_arc in p1_vals:
+                    points.append({
+                        "writes": [
+                            ("Slot_Opening", slot_opening),
+                            ("Copper_Width", copper_width),
+                            ("MagnetCentralArc_HalbachRing", magnet_arc),
+                        ],
+                        "central_mag_arc_ed": magnet_arc,
+                        "slot_opening_mm": slot_opening,
+                        "copper_width_mm": copper_width,
+                    })
+
+            config = {
+                "model": str(model_path),
+                "points": points,
+                "output_dir": str(self._app_root() / "runs"),
+                "torque_points": self.torque_points.value(),
+                "airgap_mesh": self.airgap_mesh.value(),
+                "scan_name": "two_parameter_scan",
+                "extra_csv_fields": ["central_mag_arc_ed", "slot_opening_mm", "copper_width_mm"],
+                "git_commit": commit,
+            }
+
+            # Reset UI.
+            self.log_view.clear()
+            self.progress_bar.setMaximum(total)
+            self.progress_bar.setValue(0)
+            self.start_btn.setEnabled(False)
+            self.stop_btn.setEnabled(True)
+            self.status_label.setText("\u8fd0\u884c\u4e2d...")
+            self.run_start_time = time.time()
+            self._append_log(
+                f"Starting scan: {total} points, commit {commit}", "#2563eb"
+            )
+
+            # Start worker thread.
+            self.thread = QThread()
+            self.worker = ScanWorker(config)
+            self.worker.moveToThread(self.thread)
+            self.thread.started.connect(self.worker.run)
+            self.worker.log.connect(self._on_worker_log)
+            self.worker.row.connect(self._on_worker_row)
+            self.worker.progress.connect(self._on_worker_progress)
+            self.worker.finished.connect(self._on_worker_finished)
+            self.worker.error.connect(self._on_worker_error)
+            self.worker.finished.connect(self.thread.quit)
+            self.worker.finished.connect(self.worker.deleteLater)
+            self.thread.finished.connect(self.thread.deleteLater)
+            self.thread.start()
+
+            # Keep global references (crash protection layer 5).
+            global _GLOBAL_WORKER_REF, _GLOBAL_THREAD_REF
+            _GLOBAL_WORKER_REF = self.worker
+            _GLOBAL_THREAD_REF = self.thread
+
+        except Exception as exc:
+            self._append_log(
+                f"Start error: {exc}\n{traceback.format_exc()}", "#dc2626"
+            )
+            QMessageBox.critical(self, "\u542f\u52a8\u5931\u8d25", str(exc))
+
+    def _stop_scan(self):
+        try:
+            if self.worker is not None:
+                self.worker.cancel()
+                self.status_label.setText(
+                    "\u505c\u6b62\u8bf7\u6c42\uff1a\u7b49\u5f85\u5f53\u524d\u70b9\u5b8c\u6210"
+                )
+                self._append_log(
+                    "Stop requested after current point", "#d97706"
+                )
+        except Exception as exc:
+            self._append_log(f"Stop error: {exc}", "#dc2626")
+
+    # -- worker signal handlers (each wrapped in try-except, layer 4) --
+
+    def _on_worker_log(self, text: str):
+        try:
+            color = self._classify_log(text)
+            self._append_log(text, color)
+        except Exception:
+            pass
+
+    def _on_worker_row(self, row: dict):
+        try:
+            # Update cards.
+            for key, _ in CARD_METRICS:
+                self.cards[key].set_value(row.get(key, ""))
+            # Update table.
+            for row_idx, key in enumerate(METRIC_KEYS):
+                val = row.get(key, "--")
+                if val == "" or val is None:
+                    val = "--"
+                else:
+                    try:
+                        val = f"{float(val):.6g}"
+                    except (ValueError, TypeError):
+                        val = str(val)
+                self.metrics_table.item(row_idx, 1).setText(val)
+        except Exception as exc:
+            self._append_log(f"Row update error: {exc}", "#dc2626")
+
+    def _on_worker_progress(self, current: int, total: int):
+        try:
+            self.progress_bar.setValue(current)
+            elapsed = time.time() - self.run_start_time if self.run_start_time else 0
+            self.time_label.setText(
+                f"\u6301\u7eed\u65f6\u95f4: {elapsed:.0f}s | {current}/{total}"
+            )
+            self.status_label.setText(f"\u8fd0\u884c\u4e2d {current}/{total}")
+        except Exception:
+            pass
+
+    def _on_worker_finished(self, result: dict):
+        try:
+            summary = result.get("summary", {})
+            ok = summary.get("ok", 0)
+            failed = summary.get("failed", 0)
+            elapsed = time.time() - self.run_start_time if self.run_start_time else 0
+            self.status_label.setText(
+                f"\u5b8c\u6210: {ok} OK, {failed} \u5931\u8d25"
+            )
+            self.time_label.setText(f"\u6301\u7eed\u65f6\u95f4: {elapsed:.0f}s")
+            self._append_log(
+                f"Scan complete: {ok} OK, {failed} failed. "
+                f"Results: {result.get('csv_path', '')}",
+                "#16a34a",
+            )
+            QMessageBox.information(
+                self,
+                "\u626b\u63cf\u5b8c\u6210",
+                f"\u7ed3\u679c\u4fdd\u5b58\u5728\uff1a\n{result.get('run_dir', '')}",
+            )
+        except Exception as exc:
+            self._append_log(f"Finish error: {exc}", "#dc2626")
+        finally:
+            self._reset_controls()
+            self._cleanup_thread()
+
+    def _on_worker_error(self, message: str):
+        try:
+            self.status_label.setText("\u5931\u8d25")
+            self._append_log(message, "#dc2626")
+            QMessageBox.critical(self, "\u626b\u63cf\u5931\u8d25", message)
+        except Exception:
+            pass
+        finally:
+            self._reset_controls()
+            self._cleanup_thread()
+
+    def _reset_controls(self):
+        try:
+            self.start_btn.setEnabled(True)
+            self.stop_btn.setEnabled(False)
+        except Exception:
+            pass
+
+    def _cleanup_thread(self):
+        # Delayed cleanup to avoid race with deleteLater (crash protection).
+        try:
+            QTimer.singleShot(200, self._do_cleanup)
+        except Exception:
+            pass
+
+    def _do_cleanup(self):
+        try:
+            global _GLOBAL_WORKER_REF, _GLOBAL_THREAD_REF
+            _GLOBAL_WORKER_REF = None
+            _GLOBAL_THREAD_REF = None
+            self.worker = None
+            self.thread = None
+        except Exception:
+            pass
+
+
+# ---------------------------------------------------------------------------
+# Entry point
+# ---------------------------------------------------------------------------
+
+def main() -> int:
+    # Crash protection layer 1.
+    sys.excepthook = _global_excepthook
+
+    app = QApplication(sys.argv)
+    app.setStyle("Fusion")
+    app.setStyleSheet(STYLESHEET)
+
+    # Crash protection layer 2: don't quit when last window closes.
+    app.setQuitOnLastWindowClosed(False)
+
+    # Default font.
+    font = QFont("Segoe UI", 10)
+    app.setFont(font)
+
+    window = MainWindow()
+    window.show()
+
+    return app.exec_()
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 546 - 0
torqrippswap-master/torqrippswap/motorcad_scan_gui.py

@@ -0,0 +1,546 @@
+"""Tkinter GUI for timestamped Motor-CAD parameter scans."""
+
+from __future__ import annotations
+
+import csv
+import json
+import math
+import os
+import queue
+import subprocess
+import sys
+import threading
+import time
+import traceback
+from datetime import datetime
+from pathlib import Path
+import tkinter as tk
+from tkinter import filedialog, messagebox, ttk
+
+from scan_central_mag_arc import parse_export, pick, values_inclusive
+
+try:
+    from build_info import BUILD_COMMIT
+except ImportError:
+    BUILD_COMMIT = "source-tree"
+
+
+APP_TITLE = "Motor-CAD Parameter Scan"
+DEFAULT_MODEL_NAME = "MARS-9S8P_SSSR_Halbach_PCB-V1.0.mot"
+PROFILE_FILE = "scan_parameters.json"
+METRICS = [
+    ("ripple_pct", "Torque Ripple [%]", "Torque Ripple (VW) [%]"),
+    ("ripple_nm", "Torque Ripple [Nm]", "Torque Ripple (VW)"),
+    ("tavg_nm", "Tavg VW [Nm]", "Average torque (virtual work)"),
+    ("efficiency_pct", "Efficiency [%]", "System Efficiency"),
+    ("back_emf_v", "Back EMF LL rms [V]", "Back EMF Line-Line Voltage (rms)"),
+    ("back_emf_thd_pct", "Back EMF THD [%]", "Harmonic Distortion Back EMF Line-Line Voltage"),
+    ("total_losses_w", "Total losses [W]", "Total Losses (on load)"),
+    ("copper_loss_w", "DC copper loss [W]", "Armature DC Copper Loss (on load)"),
+    ("magnet_loss_w", "Magnet loss [W]", "Magnet Loss (on load)"),
+    ("iron_loss_w", "Stator iron loss [W]", "Stator iron Loss [total] (on load)"),
+    ("input_power_w", "Input power [W]", "Input Power"),
+    ("output_power_w", "Output power [W]", "Output Power"),
+    ("em_power_w", "EM power [W]", "Electromagnetic Power"),
+    ("shaft_speed_rpm", "Shaft speed [rpm]", "Shaft Speed"),
+    ("no_load_speed_rpm", "No-load speed [rpm]", "No load speed"),
+]
+CSV_FIELDS = ["run_index", "parameter_1_label", "parameter_1_variable", "parameter_1_value",
+              "parameter_2_label", "parameter_2_variable", "parameter_2_value",
+              "linked_variable", "linked_value", "conductor_count", "clearance_mm"] + [
+    item[0] for item in METRICS
+] + ["seconds", "status", "error", "timestamp"]
+
+
+def app_root() -> Path:
+    if getattr(sys, "frozen", False):
+        return Path(sys.executable).resolve().parent
+    return Path(__file__).resolve().parent
+
+
+def bundled_root() -> Path:
+    return Path(getattr(sys, "_MEIPASS", app_root()))
+
+
+def default_model_path() -> Path:
+    for base in (app_root(), *app_root().parents):
+        candidate = base / DEFAULT_MODEL_NAME
+        if candidate.exists():
+            return candidate
+    return app_root() / DEFAULT_MODEL_NAME
+
+
+def load_profiles() -> dict:
+    external = app_root() / PROFILE_FILE
+    bundled = bundled_root() / PROFILE_FILE
+    path = external if external.exists() else bundled
+    return json.loads(path.read_text(encoding="ascii"))
+
+
+def find_repo(start: Path) -> Path | None:
+    for candidate in (start, *start.parents):
+        if (candidate / ".git").exists():
+            return candidate
+    return None
+
+
+def git_preflight() -> tuple[bool, str]:
+    repo = find_repo(app_root())
+    if repo is None:
+        if BUILD_COMMIT not in ("", "source-tree", "unknown"):
+            return True, BUILD_COMMIT
+        return False, "No Git repository or embedded build commit was found."
+    safe = f"safe.directory={repo.as_posix()}"
+    base = ["git", "-c", safe]
+    try:
+        commit = subprocess.check_output(base + ["rev-parse", "--short", "HEAD"], cwd=repo,
+                                         text=True, stderr=subprocess.STDOUT).strip()
+        dirty = subprocess.check_output(base + ["status", "--porcelain", "--untracked-files=no"],
+                                        cwd=repo, text=True, stderr=subprocess.STDOUT).strip()
+    except (OSError, subprocess.CalledProcessError) as exc:
+        return False, f"Git preflight failed: {exc}"
+    if dirty:
+        return False, "Tracked files have uncommitted changes. Commit before starting a scan."
+    return True, commit
+
+
+def metric_value(results: dict, key: str):
+    value = pick(results, key)
+    if value != "":
+        return value
+    wanted = key.lower().replace(" ", "")
+    for section in results.values():
+        for name, candidate in section.items():
+            normalized = name.lower().replace(" ", "")
+            if normalized.startswith(wanted):
+                return candidate
+    return ""
+
+
+class ScanWorker(threading.Thread):
+    def __init__(self, config: dict, events: queue.Queue, cancel: threading.Event):
+        super().__init__(daemon=True)
+        self.config = config
+        self.events = events
+        self.cancel = cancel
+
+    def emit(self, kind: str, payload=None) -> None:
+        self.events.put((kind, payload))
+
+    def run(self) -> None:
+        mc = None
+        log_file = None
+        try:
+            import ansys.motorcad.core as pymotorcad
+
+            run_dir = Path(self.config["run_dir"])
+            raw_dir = run_dir / "raw"
+            raw_dir.mkdir(parents=True, exist_ok=True)
+            csv_path = run_dir / f"scan_results_{self.config['timestamp']}.csv"
+            log_path = run_dir / f"program_log_{self.config['timestamp']}.log"
+            manifest_path = run_dir / f"run_manifest_{self.config['timestamp']}.json"
+            manifest_path.write_text(json.dumps(self.config, indent=2), encoding="ascii")
+            log_file = log_path.open("a", encoding="ascii", errors="backslashreplace")
+
+            def log(text: str) -> None:
+                stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
+                line = f"{stamp} {text}"
+                log_file.write(line + "\n")
+                log_file.flush()
+                self.emit("log", line)
+
+            points = self.config["points"]
+            log(f"Run directory: {run_dir}")
+            log(f"Git commit: {self.config['git_commit']}")
+            log("Opening a separate Motor-CAD instance")
+            mc = pymotorcad.MotorCAD(open_new_instance=True, keep_instance_open=False)
+            mc.set_visible(True)
+            mc.set_variable("MessageDisplayState", 2)
+            mc.display_screen("Scripting")
+
+            with csv_path.open("w", newline="", encoding="utf-8-sig") as csv_file:
+                writer = csv.DictWriter(csv_file, fieldnames=CSV_FIELDS)
+                writer.writeheader()
+                csv_file.flush()
+                for index, point in enumerate(points, 1):
+                    if self.cancel.is_set():
+                        log("Cancel requested; stopping before next point")
+                        break
+                    started = time.time()
+                    row = {field: "" for field in CSV_FIELDS}
+                    row.update(
+                        run_index=index,
+                        parameter_1_label=self.config["parameter_1"]["label"],
+                        parameter_1_variable=self.config["parameter_1"]["variable"],
+                        parameter_1_value=point["parameter_1_value"],
+                        parameter_2_label=self.config["parameter_2"]["label"],
+                        parameter_2_variable=self.config["parameter_2"]["variable"],
+                        parameter_2_value=point["parameter_2_value"],
+                        linked_variable=self.config["linked_value"]["variable"],
+                        linked_value=point["linked_value"],
+                        conductor_count=self.config["conductor_count"],
+                        clearance_mm=self.config["clearance"],
+                        status="FAILED",
+                        timestamp=datetime.now().isoformat(timespec="milliseconds"),
+                    )
+                    log(f"[{index}/{len(points)}] Starting "
+                        f"{self.config['parameter_1']['variable']}={point['parameter_1_value']:g}, "
+                        f"{self.config['parameter_2']['variable']}={point['parameter_2_value']:g}, "
+                        f"{self.config['linked_value']['variable']}={point['linked_value']:g}")
+                    try:
+                        mc.load_from_file(self.config["model"])
+                        mc.set_visible(True)
+                        mc.display_screen("Scripting")
+                        if self.config["torque_points"] > 0:
+                            mc.set_variable("TorquePointsPerCycle", self.config["torque_points"])
+                        if self.config["airgap_mesh"] > 0:
+                            mc.set_variable("AirgapMeshPoints_mesh", self.config["airgap_mesh"])
+                            mc.set_variable("AirgapMeshPoints_layers", self.config["airgap_mesh"])
+                        writes = (
+                            (self.config["parameter_2"]["variable"], point["parameter_2_value"]),
+                            (self.config["linked_value"]["variable"], point["linked_value"]),
+                            (self.config["parameter_1"]["variable"], point["parameter_1_value"]),
+                        )
+                        for variable, value in writes:
+                            mc.set_variable(variable, value)
+                        for variable, value in writes:
+                            applied = float(mc.get_variable(variable))
+                            if not math.isclose(applied, value, rel_tol=1e-8, abs_tol=1e-7):
+                                raise RuntimeError(
+                                    f"Write verification failed for {variable}: wrote {value}, read {applied}"
+                                )
+                        mc.do_magnetic_calculation()
+                        raw_path = raw_dir / (
+                            f"result_{index:04d}_p1_{point['parameter_1_value']:g}_"
+                            f"p2_{point['parameter_2_value']:g}_{self.config['timestamp']}.csv"
+                        )
+                        mc.export_results("EMagnetic", str(raw_path))
+                        results = parse_export(raw_path)
+                        for field, _, result_name in METRICS:
+                            row[field] = metric_value(results, result_name)
+                        if row["ripple_pct"] == "":
+                            raise RuntimeError("Torque Ripple (VW) [%] was not found")
+                        row["status"] = "OK"
+                    except Exception as exc:
+                        row["error"] = f"{type(exc).__name__}: {exc}"
+                        log(row["error"])
+                        log(traceback.format_exc())
+                    row["seconds"] = round(time.time() - started, 1)
+                    writer.writerow(row)
+                    csv_file.flush()
+                    self.emit("row", row)
+                    self.emit("progress", (index, len(points)))
+                    log(f"[{index}/{len(points)}] {row['status']} ripple={row['ripple_pct']} "
+                        f"tavg={row['tavg_nm']} seconds={row['seconds']}")
+
+            log(f"Scan ended. Results: {csv_path}")
+            self.emit("done", {"run_dir": str(run_dir), "csv": str(csv_path), "log": str(log_path)})
+        except Exception as exc:
+            self.emit("fatal", f"{type(exc).__name__}: {exc}\n{traceback.format_exc()}")
+        finally:
+            if mc is not None:
+                try:
+                    mc.load_from_file(self.config["model"])
+                except Exception:
+                    pass
+            if log_file is not None:
+                log_file.close()
+
+
+class MotorCADScanApp(tk.Tk):
+    def __init__(self):
+        super().__init__()
+        self.title(APP_TITLE)
+        self.geometry("1500x850")
+        self.minsize(1050, 650)
+        self.profiles = load_profiles()
+        self.events: queue.Queue = queue.Queue()
+        self.cancel_event = threading.Event()
+        self.worker: ScanWorker | None = None
+        self.vars = {
+            "model": tk.StringVar(value=str(default_model_path())),
+            "profile": tk.StringVar(value=next(iter(self.profiles))),
+            "p1_start": tk.StringVar(),
+            "p1_stop": tk.StringVar(),
+            "p1_step": tk.StringVar(),
+            "p2_start": tk.StringVar(),
+            "p2_stop": tk.StringVar(),
+            "p2_step": tk.StringVar(),
+            "conductor_count": tk.StringVar(),
+            "clearance": tk.StringVar(),
+            "total_runs": tk.StringVar(value="Total combinations: 0"),
+            "torque_points": tk.StringVar(value="30"),
+            "airgap_mesh": tk.StringVar(value="840"),
+            "status": tk.StringVar(value="Ready"),
+            "commit": tk.StringVar(value=BUILD_COMMIT),
+        }
+        self.build_ui()
+        self.profile_changed()
+        self.after(150, self.poll_events)
+
+    def build_ui(self) -> None:
+        controls = ttk.LabelFrame(self, text="Scan setup", padding=8)
+        controls.pack(fill="x", padx=8, pady=6)
+        ttk.Label(controls, text="Model").grid(row=0, column=0, sticky="w")
+        ttk.Entry(controls, textvariable=self.vars["model"], width=100).grid(row=0, column=1, sticky="ew", padx=4)
+        ttk.Button(controls, text="Browse", command=self.browse_model).grid(row=0, column=2, padx=4)
+
+        ttk.Label(controls, text="Scan profile").grid(row=1, column=0, sticky="w")
+        combo = ttk.Combobox(controls, textvariable=self.vars["profile"], values=list(self.profiles), state="readonly", width=32)
+        combo.grid(row=1, column=1, sticky="w", padx=4)
+        combo.bind("<<ComboboxSelected>>", lambda _: self.profile_changed())
+
+        p1_frame = ttk.LabelFrame(controls, text="Parameter 1: Central Mag Arc [ED]", padding=4)
+        p1_frame.grid(row=2, column=1, columnspan=2, sticky="w", padx=4, pady=(6, 2))
+        for col, (label, key) in enumerate((("Start", "p1_start"), ("Stop", "p1_stop"), ("Step", "p1_step"))):
+            group = ttk.Frame(p1_frame)
+            group.grid(row=0, column=col, padx=(0, 18), sticky="w")
+            ttk.Label(group, text=label, width=6).pack(side="left")
+            entry = ttk.Entry(group, textvariable=self.vars[key], width=12)
+            entry.pack(side="left")
+            entry.bind("<KeyRelease>", lambda _: self.update_total())
+
+        p2_frame = ttk.LabelFrame(controls, text="Parameter 2: Slot Opening [mm]", padding=4)
+        p2_frame.grid(row=3, column=1, columnspan=2, sticky="w", padx=4, pady=2)
+        for col, (label, key) in enumerate((("Start", "p2_start"), ("Stop", "p2_stop"), ("Step", "p2_step"))):
+            group = ttk.Frame(p2_frame)
+            group.grid(row=0, column=col, padx=(0, 18), sticky="w")
+            ttk.Label(group, text=label, width=6).pack(side="left")
+            entry = ttk.Entry(group, textvariable=self.vars[key], width=12)
+            entry.pack(side="left")
+            entry.bind("<KeyRelease>", lambda _: self.update_total())
+
+        link_frame = ttk.LabelFrame(controls, text="Linked copper width", padding=4)
+        link_frame.grid(row=4, column=1, columnspan=2, sticky="w", padx=4, pady=2)
+        for col, (label, key) in enumerate((("Conductors per side", "conductor_count"),
+                                            ("Clearance [mm]", "clearance"))):
+            group = ttk.Frame(link_frame)
+            group.grid(row=0, column=col, padx=(0, 24), sticky="w")
+            ttk.Label(group, text=label, width=20).pack(side="left")
+            ttk.Entry(group, textvariable=self.vars[key], width=12).pack(side="left")
+        ttk.Label(link_frame, text="Copper Width = (Slot Opening - Clearance) / 2 / Conductors").grid(
+            row=1, column=0, columnspan=2, sticky="w", pady=(5, 0)
+        )
+
+        solver_frame = ttk.Frame(controls)
+        solver_frame.grid(row=5, column=1, columnspan=2, sticky="w", padx=4, pady=(6, 2))
+        for col, (label, key) in enumerate((("Torque points", "torque_points"),
+                                            ("Airgap mesh", "airgap_mesh"))):
+            group = ttk.Frame(solver_frame)
+            group.grid(row=0, column=col, padx=(0, 28), sticky="w")
+            ttk.Label(group, text=label, width=15).pack(side="left")
+            ttk.Entry(group, textvariable=self.vars[key], width=12).pack(side="left")
+        ttk.Label(solver_frame, textvariable=self.vars["total_runs"]).grid(row=1, column=0, columnspan=2, sticky="w", pady=(6, 0))
+        controls.columnconfigure(1, weight=1)
+
+        actions = ttk.Frame(self, padding=(8, 2))
+        actions.pack(fill="x")
+        self.start_button = ttk.Button(actions, text="Start scan", command=self.start_scan)
+        self.start_button.pack(side="left")
+        self.cancel_button = ttk.Button(actions, text="Stop after current point", command=self.cancel_scan, state="disabled")
+        self.cancel_button.pack(side="left", padx=6)
+        ttk.Label(actions, textvariable=self.vars["status"]).pack(side="left", padx=12)
+        git_frame = ttk.Frame(actions)
+        git_frame.pack(side="right")
+        ttk.Label(git_frame, text="Git:").pack(side="left")
+        ttk.Label(git_frame, textvariable=self.vars["commit"]).pack(side="left", padx=4)
+
+        self.progress = ttk.Progressbar(self, mode="determinate")
+        self.progress.pack(fill="x", padx=8, pady=4)
+
+        table_frame = ttk.Frame(self)
+        table_frame.pack(fill="both", expand=True, padx=8, pady=4)
+        columns = ["run_index", "parameter_1_value", "parameter_2_value", "linked_value"] + [item[0] for item in METRICS] + ["seconds", "status"]
+        self.tree = ttk.Treeview(table_frame, columns=columns, show="headings", height=16)
+        headings = {"run_index": "#", "parameter_1_value": "Central Arc [ED]",
+                    "parameter_2_value": "Slot Opening [mm]", "linked_value": "Copper Width [mm]",
+                    "seconds": "Seconds", "status": "Status"}
+        headings.update({field: label for field, label, _ in METRICS})
+        for column in columns:
+            self.tree.heading(column, text=headings[column])
+            self.tree.column(column, width=105, anchor="center", stretch=False)
+        self.tree.column("run_index", width=45)
+        self.tree.column("status", width=70)
+        ybar = ttk.Scrollbar(table_frame, orient="vertical", command=self.tree.yview)
+        xbar = ttk.Scrollbar(table_frame, orient="horizontal", command=self.tree.xview)
+        self.tree.configure(yscrollcommand=ybar.set, xscrollcommand=xbar.set)
+        self.tree.grid(row=0, column=0, sticky="nsew")
+        ybar.grid(row=0, column=1, sticky="ns")
+        xbar.grid(row=1, column=0, sticky="ew")
+        table_frame.rowconfigure(0, weight=1)
+        table_frame.columnconfigure(0, weight=1)
+
+        log_frame = ttk.LabelFrame(self, text="Program log", padding=4)
+        log_frame.pack(fill="both", expand=False, padx=8, pady=(2, 8))
+        self.log_text = tk.Text(log_frame, height=9, wrap="none")
+        self.log_text.pack(fill="both", expand=True)
+
+    def browse_model(self) -> None:
+        path = filedialog.askopenfilename(title="Select Motor-CAD model", filetypes=[("Motor-CAD model", "*.mot"), ("All files", "*.*")])
+        if path:
+            self.vars["model"].set(path)
+
+    def profile_changed(self) -> None:
+        profile = self.profiles[self.vars["profile"].get()]
+        p1 = profile["parameter_1"]
+        p2 = profile["parameter_2"]
+        linked = profile["linked_value"]
+        for prefix, parameter in (("p1", p1), ("p2", p2)):
+            self.vars[f"{prefix}_start"].set(str(parameter["default_start"]))
+            self.vars[f"{prefix}_stop"].set(str(parameter["default_stop"]))
+            self.vars[f"{prefix}_step"].set(str(parameter["default_step"]))
+        self.vars["conductor_count"].set(str(linked["default_conductor_count"]))
+        self.vars["clearance"].set(str(linked["default_clearance"]))
+        self.update_total()
+
+    def update_total(self) -> None:
+        try:
+            p1_values = values_inclusive(float(self.vars["p1_start"].get()),
+                                         float(self.vars["p1_stop"].get()),
+                                         float(self.vars["p1_step"].get()))
+            p2_values = values_inclusive(float(self.vars["p2_start"].get()),
+                                         float(self.vars["p2_stop"].get()),
+                                         float(self.vars["p2_step"].get()))
+            self.vars["total_runs"].set(
+                f"Total combinations: {len(p1_values)} x {len(p2_values)} = {len(p1_values) * len(p2_values)}"
+            )
+        except ValueError:
+            self.vars["total_runs"].set("Total combinations: invalid range")
+
+    def append_log(self, text: str) -> None:
+        self.log_text.insert("end", text + "\n")
+        self.log_text.see("end")
+
+    def start_scan(self) -> None:
+        ok, commit = git_preflight()
+        if not ok:
+            messagebox.showerror("Git preflight failed", commit)
+            return
+        try:
+            model = Path(self.vars["model"].get()).resolve()
+            if not model.exists():
+                raise ValueError(f"Model does not exist: {model}")
+            p1_start = float(self.vars["p1_start"].get())
+            p1_stop = float(self.vars["p1_stop"].get())
+            p1_step = float(self.vars["p1_step"].get())
+            p2_start = float(self.vars["p2_start"].get())
+            p2_stop = float(self.vars["p2_stop"].get())
+            p2_step = float(self.vars["p2_step"].get())
+            conductor_count = int(self.vars["conductor_count"].get())
+            clearance = float(self.vars["clearance"].get())
+            torque_points = int(self.vars["torque_points"].get())
+            airgap_mesh = int(self.vars["airgap_mesh"].get())
+            p1_values = values_inclusive(p1_start, p1_stop, p1_step)
+            p2_values = values_inclusive(p2_start, p2_stop, p2_step)
+            profile = self.profiles[self.vars["profile"].get()]
+            p1 = profile["parameter_1"]
+            p2 = profile["parameter_2"]
+            linked = profile["linked_value"]
+            if p1_values[0] < p1["minimum"] or p1_values[-1] > p1["maximum"]:
+                raise ValueError(f"Parameter 1 must stay within {p1['minimum']}..{p1['maximum']} {p1['unit']}")
+            if p2_values[0] < p2["minimum"] or p2_values[-1] > p2["maximum"]:
+                raise ValueError(f"Parameter 2 must stay within {p2['minimum']}..{p2['maximum']} {p2['unit']}")
+            if conductor_count < 1:
+                raise ValueError("Conductor count must be at least 1")
+            if clearance < 0 or clearance >= p2_values[0]:
+                raise ValueError("Clearance must be non-negative and smaller than every slot opening")
+            points = []
+            for slot_opening in p2_values:
+                copper_width = round((slot_opening - clearance) / 2.0 / conductor_count, 10)
+                for magnet_arc in p1_values:
+                    points.append({
+                        "parameter_1_value": magnet_arc,
+                        "parameter_2_value": slot_opening,
+                        "linked_value": copper_width,
+                    })
+        except ValueError as exc:
+            messagebox.showerror("Invalid settings", str(exc))
+            return
+
+        estimated_hours = len(points) * 180.0 / 3600.0
+        proceed = messagebox.askyesno(
+            "Confirm two-parameter scan",
+            f"Parameter 1 levels: {len(p1_values)}\n"
+            f"Parameter 2 levels: {len(p2_values)}\n"
+            f"Total simulations: {len(points)}\n"
+            f"Rough duration at 3 minutes per point: {estimated_hours:.1f} hours\n\n"
+            "Start the scan?",
+        )
+        if not proceed:
+            return
+
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
+        run_dir = app_root() / "runs" / f"{timestamp}_two_parameter_scan"
+        config = {
+            "timestamp": timestamp,
+            "run_dir": str(run_dir),
+            "model": str(model),
+            "profile": self.vars["profile"].get(),
+            "parameter_1": p1,
+            "parameter_2": p2,
+            "linked_value": linked,
+            "points": points,
+            "conductor_count": conductor_count,
+            "clearance": clearance,
+            "torque_points": torque_points,
+            "airgap_mesh": airgap_mesh,
+            "git_commit": commit,
+        }
+        for item in self.tree.get_children():
+            self.tree.delete(item)
+        self.log_text.delete("1.0", "end")
+        self.progress.configure(maximum=len(points), value=0)
+        self.vars["commit"].set(commit)
+        self.vars["status"].set(f"Running 0/{len(points)}")
+        self.cancel_event.clear()
+        self.start_button.configure(state="disabled")
+        self.cancel_button.configure(state="normal")
+        self.worker = ScanWorker(config, self.events, self.cancel_event)
+        self.worker.start()
+
+    def cancel_scan(self) -> None:
+        self.cancel_event.set()
+        self.cancel_button.configure(state="disabled")
+        self.vars["status"].set("Stop requested; waiting for current point")
+
+    def poll_events(self) -> None:
+        try:
+            while True:
+                kind, payload = self.events.get_nowait()
+                if kind == "log":
+                    self.append_log(payload)
+                elif kind == "row":
+                    columns = ["run_index", "parameter_1_value", "parameter_2_value", "linked_value"] + [
+                        item[0] for item in METRICS
+                    ] + ["seconds", "status"]
+                    self.tree.insert("", "end", values=[payload.get(column, "") for column in columns])
+                    self.tree.yview_moveto(1.0)
+                elif kind == "progress":
+                    current, total = payload
+                    self.progress.configure(value=current)
+                    self.vars["status"].set(f"Running {current}/{total}")
+                elif kind == "done":
+                    self.finish_run(f"Complete: {payload['run_dir']}")
+                    messagebox.showinfo("Scan complete", f"Results saved in:\n{payload['run_dir']}")
+                elif kind == "fatal":
+                    self.finish_run("Failed")
+                    self.append_log(payload)
+                    messagebox.showerror("Scan failed", payload)
+        except queue.Empty:
+            pass
+        self.after(150, self.poll_events)
+
+    def finish_run(self, status: str) -> None:
+        self.vars["status"].set(status)
+        self.start_button.configure(state="normal")
+        self.cancel_button.configure(state="disabled")
+
+
+def main() -> int:
+    app = MotorCADScanApp()
+    app.mainloop()
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 31 - 0
torqrippswap-master/torqrippswap/package_magnet_arc_v2.ps1

@@ -0,0 +1,31 @@
+$ErrorActionPreference = 'Stop'
+Set-Location -LiteralPath $PSScriptRoot
+
+$safeDir = $PSScriptRoot.Replace('\', '/')
+$commit = git -c "safe.directory=$safeDir" rev-parse --short HEAD
+if ($LASTEXITCODE -ne 0) {
+    throw 'No Git commit exists.'
+}
+$dirty = git -c "safe.directory=$safeDir" status --porcelain --untracked-files=no
+if ($dirty) {
+    throw 'Tracked files have uncommitted changes. Commit before packaging.'
+}
+
+$sourceDir = Join-Path $PSScriptRoot 'dist\MotorCADParameterScanV2'
+$targetDir = Join-Path $PSScriptRoot 'dist\MotorCADMagnetArcScan'
+$sourceExe = Join-Path $sourceDir 'MotorCADParameterScanV2.exe'
+$targetExe = Join-Path $targetDir 'MotorCADMagnetArcScan.exe'
+
+if (-not (Test-Path -LiteralPath $sourceExe)) {
+    throw "V2 executable not found: $sourceExe"
+}
+if (Test-Path -LiteralPath $targetDir) {
+    throw "Target already exists: $targetDir"
+}
+
+Copy-Item -LiteralPath $sourceDir -Destination $targetDir -Recurse
+Rename-Item -LiteralPath (Join-Path $targetDir 'MotorCADParameterScanV2.exe') -NewName 'MotorCADMagnetArcScan.exe'
+Copy-Item -LiteralPath '.\scan_parameters_magnet_arc.json' -Destination (Join-Path $targetDir 'scan_parameters.json') -Force
+
+Write-Host "Git commit: $commit"
+Write-Host "Package complete: $targetExe"

+ 3 - 0
torqrippswap-master/torqrippswap/requirements.txt

@@ -0,0 +1,3 @@
+ansys-motorcad-core>=0.8.0
+PyQt5>=5.15.0
+pyinstaller>=5.0

+ 32 - 0
torqrippswap-master/torqrippswap/run_central_mag_arc_scan.ps1

@@ -0,0 +1,32 @@
+$ErrorActionPreference = 'Stop'
+$Host.UI.RawUI.WindowTitle = 'Motor-CAD Central Mag Arc Scan 90-110 EDeg'
+Set-Location -LiteralPath $PSScriptRoot
+
+$safeDir = $PSScriptRoot.Replace('\', '/')
+$null = git -c "safe.directory=$safeDir" rev-parse --verify HEAD 2>$null
+if ($LASTEXITCODE -ne 0) {
+    throw 'No Git commit exists. Commit the scripts before running.'
+}
+$trackedChanges = git -c "safe.directory=$safeDir" status --porcelain --untracked-files=no
+if ($trackedChanges) {
+    throw 'Tracked files have uncommitted changes. Commit them before running.'
+}
+$commit = git -c "safe.directory=$safeDir" rev-parse --short HEAD
+
+$logPath = Join-Path $PSScriptRoot 'results\central_mag_arc_console.log'
+New-Item -ItemType Directory -Force -Path (Split-Path -Parent $logPath) | Out-Null
+
+Write-Host 'Starting a separate, visible Motor-CAD instance.' -ForegroundColor Cyan
+Write-Host "Committed version: $commit" -ForegroundColor Cyan
+Write-Host 'Range 90-110 EDeg, step 1, 21 points; model defaults: 30 points and 840 airgap mesh.' -ForegroundColor Cyan
+Write-Host "Progress log: $logPath" -ForegroundColor Cyan
+
+$pythonExe = 'C:\Users\zhang\AppData\Local\Microsoft\WindowsApps\python.exe'
+& $pythonExe -u .\scan_central_mag_arc.py `
+  --start 90 --stop 110 --step 1 `
+  --torque-points 0 --airgap-mesh 0 `
+  --new-instance --no-resume 2>&1 |
+  Tee-Object -FilePath $logPath
+
+Write-Host ''
+Write-Host 'The scan process has ended. This window remains open for review.' -ForegroundColor Green

+ 240 - 0
torqrippswap-master/torqrippswap/scan_central_mag_arc.py

@@ -0,0 +1,240 @@
+"""Scan Central Mag Arc [ED] and collect Motor-CAD torque ripple results.
+
+Each run reloads the baseline model before changing only
+MagnetCentralArc_HalbachRing. Solver discretization is separately configurable.
+"""
+
+from __future__ import annotations
+
+import argparse
+import csv
+import json
+import math
+import statistics
+import time
+import traceback
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parent
+DEFAULT_MODEL = ROOT / "MARS-9S8P_SSSR_Halbach_PCB-V1.0.mot"
+PARAMETER = "MagnetCentralArc_HalbachRing"
+SECTION_PRIORITY = ("E-Magnetics", "Drive", "Losses", "Materials", "Miscellaneous")
+METRICS = (
+    "Average torque (virtual work)",
+    "Torque Ripple (VW)",
+    "Torque Ripple (VW) [%]",
+    "Cogging Torque Ripple (Vw)",
+    "No load speed",
+)
+
+
+def values_inclusive(start: float, stop: float, step: float) -> list[float]:
+    if step <= 0 or stop < start:
+        raise ValueError("step must be positive and stop must be >= start")
+    count = int(math.floor((stop - start) / step + 1e-9))
+    values = [round(start + i * step, 10) for i in range(count + 1)]
+    if not math.isclose(values[-1], stop, abs_tol=1e-9):
+        values.append(float(stop))
+    return values
+
+
+def norm(name: str) -> str:
+    return name.split("_(")[0].strip().replace(" ", "").lower()
+
+
+def parse_export(path: Path) -> dict[str, dict[str, float]]:
+    text = None
+    for encoding in ("utf-8-sig", "cp1252", "gbk", "latin-1"):
+        try:
+            text = path.read_text(encoding=encoding)
+            break
+        except UnicodeDecodeError:
+            pass
+    if text is None:
+        return {}
+    result: dict[str, dict[str, float]] = {}
+    section = "(root)"
+    for raw in text.splitlines():
+        line = raw.strip()
+        if not line:
+            continue
+        if ";" not in line:
+            section = line
+            result.setdefault(section, {})
+            continue
+        parts = line.split(";")
+        try:
+            result.setdefault(section, {})[norm(parts[0].strip().strip('"'))] = float(parts[1])
+        except (IndexError, ValueError):
+            continue
+    return result
+
+
+def pick(result: dict[str, dict[str, float]], key: str):
+    wanted = norm(key)
+    for section in SECTION_PRIORITY:
+        if wanted in result.get(section, {}):
+            return result[section][wanted]
+    for section in result.values():
+        if wanted in section:
+            return section[wanted]
+    return ""
+
+
+def existing_ok_values(csv_path: Path) -> set[float]:
+    if not csv_path.exists():
+        return set()
+    with csv_path.open("r", encoding="utf-8-sig", newline="") as fh:
+        return {
+            float(row["central_mag_arc_ed"])
+            for row in csv.DictReader(fh)
+            if row.get("status") == "OK"
+        }
+
+
+def write_analysis(csv_path: Path, output: Path) -> None:
+    with csv_path.open("r", encoding="utf-8-sig", newline="") as fh:
+        rows = [r for r in csv.DictReader(fh) if r.get("status") == "OK"]
+    rows.sort(key=lambda r: float(r["central_mag_arc_ed"]))
+    valid = [r for r in rows if r.get("torque_ripple_pct")]
+    if not valid:
+        output.write_text("# Scan analysis\n\nNo successful results to analyze.\n", encoding="utf-8")
+        return
+    best = min(valid, key=lambda r: float(r["torque_ripple_pct"]))
+    baseline = min(valid, key=lambda r: abs(float(r["central_mag_arc_ed"]) - 120.0))
+    ripples = [float(r["torque_ripple_pct"]) for r in valid]
+    trend = "decreased" if ripples[-1] < ripples[0] else "increased"
+    table = [
+        "| Central Mag Arc [ED] | Torque Ripple (VW) [%] | Tavg (VW) [Nm] | Status |",
+        "|---:|---:|---:|:---|",
+    ]
+    table += [
+        f"| {float(r['central_mag_arc_ed']):g} | {float(r['torque_ripple_pct']):.5g} | "
+        f"{float(r['average_torque_vw_nm']):.6g} | {r['status']} |"
+        for r in valid
+    ]
+    text = f"""# Central Mag Arc scan results and analysis
+
+- Successful points: {len(valid)}
+- Minimum Torque Ripple: **{float(best['torque_ripple_pct']):.5g}%** at **{float(best['central_mag_arc_ed']):g} EDeg**
+- Average torque at the best point: {float(best['average_torque_vw_nm']):.6g} Nm
+- Ripple change versus the point nearest 120 EDeg: {float(best['torque_ripple_pct']) - float(baseline['torque_ripple_pct']):+.4g} percentage points
+- Across the scanned interval, Torque Ripple {trend}; population standard deviation is {statistics.pstdev(ripples):.4g} percentage points.
+
+Note: Use this scan for relative comparison. Recheck final candidates with 180
+points per electrical cycle and a finer airgap mesh.
+
+## Results
+
+{chr(10).join(table)}
+"""
+    output.write_text(text, encoding="utf-8")
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("--model", type=Path, default=DEFAULT_MODEL)
+    parser.add_argument("--start", type=float, default=80.0)
+    parser.add_argument("--stop", type=float, default=120.0)
+    parser.add_argument("--step", type=float, default=1.0)
+    parser.add_argument("--torque-points", type=int, default=120,
+                        help="Points per electrical cycle; 0 preserves the model value")
+    parser.add_argument("--airgap-mesh", type=int, default=960,
+                        help="Airgap mesh/layers value; 0 preserves the model value")
+    parser.add_argument("--output-dir", type=Path, default=ROOT / "results")
+    parser.add_argument("--port", type=int, default=-1,
+                        help="Motor-CAD RPC port for connecting to a specific instance")
+    parser.add_argument("--new-instance", action="store_true",
+                        help="Open a separate Motor-CAD instance")
+    parser.add_argument("--no-resume", action="store_true", help="Do not skip successful CSV rows")
+    parser.add_argument("--dry-run", action="store_true")
+    args = parser.parse_args()
+
+    model = args.model.resolve()
+    if not model.exists():
+        raise SystemExit(f"Model does not exist: {model}")
+    values = values_inclusive(args.start, args.stop, args.step)
+    out_dir = args.output_dir.resolve()
+    csv_path = out_dir / "central_mag_arc_scan.csv"
+    raw_dir = out_dir / "raw"
+    analysis_path = out_dir / "central_mag_arc_analysis.md"
+    print(f"Model: {model}", flush=True)
+    print(f"Scan: {values[0]:g}..{values[-1]:g} EDeg, {len(values)} points; "
+          f"TorquePointsPerCycle={args.torque_points or 'model default'}, "
+          f"AirgapMesh={args.airgap_mesh or 'model default'}", flush=True)
+    print(f"Results: {csv_path}", flush=True)
+    if args.dry_run:
+        return 0
+
+    out_dir.mkdir(parents=True, exist_ok=True)
+    raw_dir.mkdir(parents=True, exist_ok=True)
+    completed = set() if args.no_resume else existing_ok_values(csv_path)
+    fields = ["central_mag_arc_ed", "torque_ripple_pct", "torque_ripple_nm",
+              "average_torque_vw_nm", "cogging_ripple_vw_nm", "no_load_speed_rpm",
+              "seconds", "status", "applied_value", "error"]
+    new_file = not csv_path.exists() or args.no_resume
+    mode = "w" if args.no_resume else "a"
+
+    import ansys.motorcad.core as pymotorcad
+    mc = pymotorcad.MotorCAD(port=args.port, open_new_instance=args.new_instance)
+    mc.set_visible(True)
+    mc.set_variable("MessageDisplayState", 2)
+    mc.display_screen("Scripting")
+    with csv_path.open(mode, encoding="utf-8-sig", newline="") as fh:
+        writer = csv.DictWriter(fh, fieldnames=fields)
+        if new_file:
+            writer.writeheader()
+        try:
+            for index, value in enumerate(values, 1):
+                if value in completed:
+                    print(f"[{index}/{len(values)}] {value:g} EDeg already complete; skipped", flush=True)
+                    continue
+                started = time.time()
+                row = {k: "" for k in fields}
+                row.update(central_mag_arc_ed=value, status="FAILED")
+                try:
+                    mc.load_from_file(str(model))
+                    mc.display_screen("Scripting")
+                    if args.torque_points:
+                        mc.set_variable("TorquePointsPerCycle", args.torque_points)
+                    if args.airgap_mesh:
+                        mc.set_variable("AirgapMeshPoints_mesh", args.airgap_mesh)
+                        mc.set_variable("AirgapMeshPoints_layers", args.airgap_mesh)
+                    mc.set_variable(PARAMETER, value)
+                    applied = float(mc.get_variable(PARAMETER))
+                    row["applied_value"] = applied
+                    if not math.isclose(applied, value, abs_tol=1e-6):
+                        raise RuntimeError(f"Parameter verification failed: wrote {value}, read {applied}")
+                    mc.do_magnetic_calculation()
+                    raw_path = raw_dir / f"central_mag_arc_{value:g}.csv"
+                    mc.export_results("EMagnetic", str(raw_path))
+                    result = parse_export(raw_path)
+                    row.update(
+                        torque_ripple_pct=pick(result, "Torque Ripple (VW) [%]"),
+                        torque_ripple_nm=pick(result, "Torque Ripple (VW)"),
+                        average_torque_vw_nm=pick(result, "Average torque (virtual work)"),
+                        cogging_ripple_vw_nm=pick(result, "Cogging Torque Ripple (Vw)"),
+                        no_load_speed_rpm=pick(result, "No load speed"),
+                        status="OK",
+                    )
+                    if row["torque_ripple_pct"] == "":
+                        raise RuntimeError("Torque Ripple (VW) [%] was not found in exported results")
+                except Exception as exc:  # noqa: BLE001
+                    row["status"] = "FAILED"
+                    row["error"] = f"{type(exc).__name__}: {exc}"
+                    traceback.print_exc()
+                row["seconds"] = round(time.time() - started, 1)
+                writer.writerow(row)
+                fh.flush()
+                print(f"[{index}/{len(values)}] {value:g} EDeg {row['status']}  Ripple={row['torque_ripple_pct']}%  Tavg={row['average_torque_vw_nm']} Nm  {row['seconds']}s", flush=True)
+        finally:
+            mc.load_from_file(str(model))
+
+    write_analysis(csv_path, analysis_path)
+    print(f"Complete: {csv_path}\nAnalysis: {analysis_path}", flush=True)
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 32 - 0
torqrippswap-master/torqrippswap/scan_parameters.json

@@ -0,0 +1,32 @@
+{
+  "Central Mag Arc x Slot Opening": {
+    "parameter_1": {
+      "label": "Central Mag Arc [ED]",
+      "variable": "MagnetCentralArc_HalbachRing",
+      "unit": "EDeg",
+      "default_start": 90.0,
+      "default_stop": 110.0,
+      "default_step": 1.0,
+      "minimum": 0.0,
+      "maximum": 180.0
+    },
+    "parameter_2": {
+      "label": "Slot Opening",
+      "variable": "Slot_Opening",
+      "unit": "mm",
+      "default_start": 5.4,
+      "default_stop": 7.4,
+      "default_step": 0.2,
+      "minimum": 0.2,
+      "maximum": 8.0
+    },
+    "linked_value": {
+      "label": "Copper Width",
+      "variable": "Copper_Width",
+      "unit": "mm",
+      "default_clearance": 0.2,
+      "default_conductor_count": 1,
+      "formula": "(parameter_2 - clearance) / 2 / conductor_count"
+    }
+  }
+}

+ 11 - 0
torqrippswap-master/torqrippswap/scan_parameters_magnet_arc.json

@@ -0,0 +1,11 @@
+{
+  "Magnet Arc [ED]": {
+    "variable": "Magnet_Arc_[ED]",
+    "unit": "EDeg",
+    "default_start": 126.0,
+    "default_stop": 180.0,
+    "default_step": 1.0,
+    "minimum": 0.0,
+    "maximum": 180.0
+  }
+}

+ 631 - 0
torqrippswap-master/torqrippswap/solver.py

@@ -0,0 +1,631 @@
+"""Motor-CAD parameter scan solver core.
+
+Pure computation module, no GUI dependencies.
+Handles Motor-CAD connection, model reload, parameter write-back
+verification, magnetic calculation, result export parsing, and
+metric extraction with bilingual (English/Chinese) field matching.
+
+All source is ASCII; Chinese field names use \\uXXXX escapes.
+"""
+
+from __future__ import annotations
+
+import csv
+import json
+import math
+import os
+import subprocess
+import time
+import traceback
+from datetime import datetime
+from pathlib import Path
+
+
+# ---------------------------------------------------------------------------
+# Metric definitions: key, display label, and aliases (English + Chinese).
+# Chinese aliases use Unicode escapes so this file stays pure ASCII.
+# ---------------------------------------------------------------------------
+
+METRIC_DEFINITIONS = [
+    {
+        "key": "ripple_pct",
+        "label": "Torque Ripple [%]",
+        "aliases": [
+            "Torque Ripple (VW) [%]",
+            "Torque Ripple (VW)[%]",
+        ],
+    },
+    {
+        "key": "ripple_nm",
+        "label": "Torque Ripple [Nm]",
+        "aliases": [
+            "Torque Ripple (VW)",
+        ],
+    },
+    {
+        "key": "tavg_nm",
+        "label": "Tavg VW [Nm]",
+        "aliases": [
+            "Average torque (virtual work)",
+            "\u5e73\u5747\u8f6c\u77e9 (virtual work)",
+            "\u5e73\u5747\u8f6c\u77e9(virtual work)",
+        ],
+    },
+    {
+        "key": "efficiency_pct",
+        "label": "Efficiency [%]",
+        "aliases": [
+            "System Efficiency",
+            "\u7cfb\u7edf\u6548\u7387",
+        ],
+    },
+    {
+        "key": "back_emf_v",
+        "label": "Back EMF LL rms [V]",
+        "aliases": [
+            "Back EMF Line-Line Voltage (rms)",
+            "\u7ebf\u95f4\u53cd\u5411\u7535\u52a8\u52bf\u6709\u6548\u503c",
+        ],
+    },
+    {
+        "key": "back_emf_thd_pct",
+        "label": "Back EMF THD [%]",
+        "aliases": [
+            "Harmonic Distortion Back EMF Line-Line Voltage",
+            "\u7ebf\u53cd\u5411\u7535\u52a8\u52bf\u8c10\u6ce2",
+            "\u7ebf\u7535\u538b\u8c10\u6ce2",
+        ],
+    },
+    {
+        "key": "total_losses_w",
+        "label": "Total losses [W]",
+        "aliases": [
+            "Total Losses (on load)",
+            "\u603b\u635f\u8017(\u989d\u5b9a)",
+            "\u603b\u635f\u8017 (\u989d\u5b9a)",
+        ],
+    },
+    {
+        "key": "copper_loss_w",
+        "label": "DC copper loss [W]",
+        "aliases": [
+            "Armature DC Copper Loss (on load)",
+            "\u7535\u67a2\u76f4\u6d41\u94dc\u8017 (\u5e26\u8f7d)",
+            "\u7535\u67a2\u76f4\u6d41\u94dc\u8017(\u5e26\u8f7d)",
+        ],
+    },
+    {
+        "key": "magnet_loss_w",
+        "label": "Magnet loss [W]",
+        "aliases": [
+            "Magnet Loss (on load)",
+            "\u6c38\u78c1\u4f53\u635f\u8017(\u989d\u5b9a)",
+            "\u6c38\u78c1\u4f53\u635f\u8017 (\u989d\u5b9a)",
+        ],
+    },
+    {
+        "key": "iron_loss_w",
+        "label": "Stator iron loss [W]",
+        "aliases": [
+            "Stator iron Loss [total] (on load)",
+            "\u5b9a\u5b50\u94c1\u635f[\u603b\u635f\u8017](\u989d\u5b9a)",
+            "\u5b9a\u5b50\u94c1\u635f[\u603b\u635f\u8017] (\u989d\u5b9a)",
+        ],
+    },
+    {
+        "key": "input_power_w",
+        "label": "Input power [W]",
+        "aliases": [
+            "Input Power",
+            "\u8f93\u5165\u529f\u7387",
+        ],
+    },
+    {
+        "key": "output_power_w",
+        "label": "Output power [W]",
+        "aliases": [
+            "Output Power",
+            "\u8f93\u51fa\u529f\u7387_\u7535\u538b\u9650\u5236\u9644\u8fd1\u5de5\u4f5c\u70b9",
+        ],
+    },
+    {
+        "key": "em_power_w",
+        "label": "EM power [W]",
+        "aliases": [
+            "Electromagnetic Power",
+            "\u7535\u78c1\u529f\u7387_\u7535\u538b\u9650\u5236\u9644\u8fd1\u5de5\u4f5c\u70b9",
+        ],
+    },
+    {
+        "key": "shaft_speed_rpm",
+        "label": "Shaft speed [rpm]",
+        "aliases": [
+            "Shaft Speed",
+            "\u8f6c\u901f[RPM]",
+            "\u8f6c\u901f [RPM]",
+        ],
+    },
+    {
+        "key": "no_load_speed_rpm",
+        "label": "No-load speed [rpm]",
+        "aliases": [
+            "No load speed",
+            "\u7a7a\u8f7d\u8f6c\u901f",
+        ],
+    },
+    {
+        "key": "shaft_torque_nm",
+        "label": "Shaft torque [Nm]",
+        "aliases": [
+            "Shaft Torque",
+            "\u8f74\u8f6c\u77e9",
+        ],
+    },
+]
+
+METRIC_KEYS = [m["key"] for m in METRIC_DEFINITIONS]
+METRIC_LABELS = {m["key"]: m["label"] for m in METRIC_DEFINITIONS}
+
+# Section name aliases for priority ordering.
+SECTION_PRIORITY = [
+    "E-Magnetics",
+    "\u7535\u78c1",
+    "Drive",
+    "\u9a71\u52a8",
+    "Losses",
+    "\u635f\u8017",
+    "Materials",
+    "\u6750\u6599",
+    "Miscellaneous",
+    "\u6742\u9879",
+]
+
+
+# ---------------------------------------------------------------------------
+# Utility functions
+# ---------------------------------------------------------------------------
+
+def values_inclusive(start: float, stop: float, step: float) -> list[float]:
+    """Generate values from start to stop inclusive, appending stop if
+    it is not exactly reachable by integer steps."""
+    if step <= 0 or stop < start:
+        raise ValueError("step must be positive and stop must be >= start")
+    count = int(math.floor((stop - start) / step + 1e-9))
+    values = [round(start + i * step, 10) for i in range(count + 1)]
+    if not math.isclose(values[-1], stop, abs_tol=1e-9):
+        values.append(float(stop))
+    return values
+
+
+def _normalize_name(name: str) -> str:
+    """Normalize a field name for matching: unify brackets, remove
+    whitespace, lowercase."""
+    s = name
+    # Full-width brackets to half-width.
+    s = s.replace("\uff08", "(").replace("\uff09", ")")
+    # Remove all whitespace.
+    s = "".join(s.split())
+    return s.lower()
+
+
+# Pre-normalize aliases for fast matching.
+_METRIC_ALIAS_MAP: dict[str, str] = {}
+for _m in METRIC_DEFINITIONS:
+    for _alias in _m["aliases"]:
+        _METRIC_ALIAS_MAP[_normalize_name(_alias)] = _m["key"]
+
+
+def parse_export(path: Path) -> dict[str, dict[str, float]]:
+    """Parse a Motor-CAD semicolon-delimited export file.
+
+    Returns a dict of section_name -> {field_name: value}.
+    Handles UTF-8, cp1252, gbk, and latin-1 encodings.
+    """
+    text = None
+    for encoding in ("utf-8-sig", "gbk", "cp1252", "latin-1"):
+        try:
+            text = path.read_text(encoding=encoding)
+            break
+        except UnicodeDecodeError:
+            continue
+    if text is None:
+        return {}
+    result: dict[str, dict[str, float]] = {}
+    section = "(root)"
+    for raw in text.splitlines():
+        line = raw.strip()
+        if not line:
+            continue
+        if ";" not in line:
+            section = line
+            result.setdefault(section, {})
+            continue
+        parts = line.split(";")
+        try:
+            field = parts[0].strip().strip('"')
+            value = float(parts[1])
+            result.setdefault(section, {})[field] = value
+        except (IndexError, ValueError):
+            continue
+    return result
+
+
+def pick_metric(results: dict[str, dict[str, float]], metric_key: str):
+    """Extract a metric value from parsed export results.
+
+    Tries exact alias match first (section priority order), then
+    falls back to prefix-based fuzzy match across all sections.
+    Returns the value (float) or "" if not found.
+    """
+    # Build the set of normalized aliases for this metric.
+    wanted_aliases = set()
+    for m in METRIC_DEFINITIONS:
+        if m["key"] == metric_key:
+            for alias in m["aliases"]:
+                wanted_aliases.add(_normalize_name(alias))
+            break
+    if not wanted_aliases:
+        return ""
+
+    # Phase 1: exact match in priority section order.
+    for section_name in SECTION_PRIORITY:
+        section = results.get(section_name)
+        if section is None:
+            continue
+        for field, value in section.items():
+            if _normalize_name(field) in wanted_aliases:
+                return value
+
+    # Phase 2: exact match across all sections.
+    for section in results.values():
+        for field, value in section.items():
+            if _normalize_name(field) in wanted_aliases:
+                return value
+
+    # Phase 3: prefix fuzzy match.
+    for alias_norm in wanted_aliases:
+        for section in results.values():
+            for field, value in section.items():
+                field_norm = _normalize_name(field)
+                if field_norm.startswith(alias_norm) or alias_norm.startswith(field_norm):
+                    if len(field_norm) > 3:  # avoid trivial matches
+                        return value
+    return ""
+
+
+def extract_all_metrics(results: dict[str, dict[str, float]]) -> dict[str, float]:
+    """Extract all defined metrics from parsed results."""
+    out: dict[str, float] = {}
+    for m in METRIC_DEFINITIONS:
+        val = pick_metric(results, m["key"])
+        if val != "":
+            out[m["key"]] = val
+    return out
+
+
+# ---------------------------------------------------------------------------
+# Git preflight
+# ---------------------------------------------------------------------------
+
+def find_repo(start: Path) -> Path | None:
+    """Walk up from start to find a directory containing .git."""
+    for candidate in (start, *start.parents):
+        if (candidate / ".git").exists():
+            return candidate
+    return None
+
+
+def git_preflight(script_dir: Path) -> tuple[bool, str]:
+    """Check that a git repo exists, HEAD is valid, and tracked files
+    are clean. Returns (ok, commit_or_error)."""
+    repo = find_repo(script_dir)
+    if repo is None:
+        return False, "No Git repository found."
+    safe = f"safe.directory={repo.as_posix()}"
+    base = ["git", "-c", safe]
+    try:
+        commit = subprocess.check_output(
+            base + ["rev-parse", "--short", "HEAD"],
+            cwd=repo, text=True, stderr=subprocess.STDOUT,
+        ).strip()
+        dirty = subprocess.check_output(
+            base + ["status", "--porcelain", "--untracked-files=no"],
+            cwd=repo, text=True, stderr=subprocess.STDOUT,
+        ).strip()
+    except (OSError, subprocess.CalledProcessError) as exc:
+        return False, f"Git preflight failed: {exc}"
+    if dirty:
+        return False, "Tracked files have uncommitted changes. Commit before starting a scan."
+    return True, commit
+
+
+# ---------------------------------------------------------------------------
+# Motor-CAD solver
+# ---------------------------------------------------------------------------
+
+class MotorCADSolver:
+    """Manages a dedicated, foreground-visible Motor-CAD instance and
+    runs parameter scans with per-point baseline reload, write-back
+    verification, and immediate CSV/log persistence.
+
+    No GUI dependencies. Callbacks (log_cb, progress_cb, row_cb) allow
+    the caller to receive events.
+    """
+
+    def __init__(
+        self,
+        model_path: Path,
+        log_cb=None,
+        progress_cb=None,
+        row_cb=None,
+    ):
+        self.model_path = Path(model_path).resolve()
+        self.log_cb = log_cb
+        self.progress_cb = progress_cb
+        self.row_cb = row_cb
+        self.mc = None
+        self._cancel = False
+
+    def _log(self, text: str) -> None:
+        if self.log_cb:
+            self.log_cb(text)
+
+    def cancel(self) -> None:
+        """Request cancellation after the current point finishes."""
+        self._cancel = True
+
+    def connect(self) -> None:
+        """Open a dedicated, foreground-visible Motor-CAD instance.
+
+        Falls back to set_motorcad_exe if MOTORCAD_ACTIVEX is not set.
+        """
+        import ansys.motorcad.core as pymotorcad
+
+        if not os.environ.get("MOTORCAD_ACTIVEX"):
+            try:
+                from ansys.motorcad.core import set_motorcad_exe
+                candidate = r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe"
+                if os.path.exists(candidate):
+                    set_motorcad_exe(candidate)
+                    self._log(f"MOTORCAD_ACTIVEX not set; using {candidate}")
+            except (ImportError, Exception):
+                pass
+
+        self._log("Opening a separate, visible Motor-CAD instance")
+        self.mc = pymotorcad.MotorCAD(open_new_instance=True, keep_instance_open=False)
+        self.mc.set_visible(True)
+        self.mc.set_variable("MessageDisplayState", 2)
+        self.mc.display_screen("Scripting")
+
+    def disconnect(self) -> None:
+        """Reload the baseline model and close the instance."""
+        if self.mc is not None:
+            try:
+                self.mc.load_from_file(str(self.model_path))
+            except Exception:
+                pass
+            try:
+                self.mc.quit()
+            except Exception:
+                pass
+            self.mc = None
+
+    def _write_and_verify(self, variable: str, value: float) -> None:
+        """Write a variable and read it back. Raises RuntimeError on
+        mismatch."""
+        self.mc.set_variable(variable, value)
+        applied = float(self.mc.get_variable(variable))
+        if not math.isclose(applied, value, rel_tol=1e-8, abs_tol=1e-7):
+            raise RuntimeError(
+                f"Write verification failed for {variable}: "
+                f"wrote {value}, read {applied}"
+            )
+
+    def run_single_point(
+        self,
+        index: int,
+        total: int,
+        writes: list[tuple[str, float]],
+        torque_points: int = 0,
+        airgap_mesh: int = 0,
+        raw_dir: Path | None = None,
+    ) -> dict:
+        """Run a single simulation point.
+
+        Args:
+            index: 1-based point index.
+            total: total number of points.
+            writes: list of (variable_name, value) to set, in order.
+            torque_points: TorquePointsPerCycle (0 = keep model default).
+            airgap_mesh: Airgap mesh/layers value (0 = keep model default).
+            raw_dir: directory to save raw export CSV.
+
+        Returns:
+            dict with metrics, status, error, seconds.
+        """
+        started = time.time()
+        result: dict = {
+            "index": index,
+            "status": "FAILED",
+            "error": "",
+            "seconds": 0,
+            "metrics": {},
+        }
+        try:
+            # Reload baseline model for every point.
+            self.mc.load_from_file(str(self.model_path))
+            self.mc.set_visible(True)
+            self.mc.display_screen("Scripting")
+
+            # Solver discretization (if specified).
+            if torque_points > 0:
+                self.mc.set_variable("TorquePointsPerCycle", torque_points)
+            if airgap_mesh > 0:
+                self.mc.set_variable("AirgapMeshPoints_mesh", airgap_mesh)
+                self.mc.set_variable("AirgapMeshPoints_layers", airgap_mesh)
+
+            # Write design variables and verify each.
+            for variable, value in writes:
+                self._write_and_verify(variable, value)
+
+            self._log(f"[{index}/{total}] Starting magnetic calculation")
+            self.mc.do_magnetic_calculation()
+
+            # Export raw results.
+            if raw_dir is not None:
+                raw_dir.mkdir(parents=True, exist_ok=True)
+                ts = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
+                val_tag = "_".join(f"{v}_{val:g}" for v, val in writes)
+                raw_path = raw_dir / f"result_{index:04d}_{val_tag}_{ts}.csv"
+                self.mc.export_results("EMagnetic", str(raw_path))
+                parsed = parse_export(raw_path)
+                result["metrics"] = extract_all_metrics(parsed)
+                result["raw_path"] = str(raw_path)
+
+            result["status"] = "OK"
+        except Exception as exc:
+            result["status"] = "FAILED"
+            result["error"] = f"{type(exc).__name__}: {exc}"
+            self._log(result["error"])
+            self._log(traceback.format_exc())
+
+        result["seconds"] = round(time.time() - started, 1)
+        return result
+
+    def run_scan(
+        self,
+        points: list[dict],
+        output_dir: Path,
+        torque_points: int = 0,
+        airgap_mesh: int = 0,
+        scan_name: str = "scan",
+        extra_csv_fields: list[str] | None = None,
+    ) -> dict:
+        """Run a full parameter scan.
+
+        Args:
+            points: list of dicts, each with 'writes' (list of (var, val))
+                    and optional extra fields for CSV.
+            output_dir: base directory for run output.
+            torque_points: solver setting.
+            airgap_mesh: solver setting.
+            scan_name: name for the run subdirectory.
+            extra_csv_fields: additional CSV column names beyond metrics.
+
+        Returns:
+            dict with run_dir, csv_path, log_path, manifest_path, summary.
+        """
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
+        run_dir = Path(output_dir) / f"{timestamp}_{scan_name}"
+        raw_dir = run_dir / "raw"
+        run_dir.mkdir(parents=True, exist_ok=True)
+
+        csv_path = run_dir / f"scan_results_{timestamp}.csv"
+        log_path = run_dir / f"program_log_{timestamp}.log"
+        manifest_path = run_dir / f"run_manifest_{timestamp}.json"
+
+        extra_fields = extra_csv_fields or []
+        csv_fields = ["run_index", "status", "seconds", "error"] + extra_fields + METRIC_KEYS
+
+        log_file = log_path.open("a", encoding="ascii", errors="backslashreplace")
+
+        def log(text: str) -> None:
+            stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
+            line = f"{stamp} {text}"
+            log_file.write(line + "\n")
+            log_file.flush()
+            self._log(line)
+
+        # Write manifest.
+        manifest = {
+            "timestamp": timestamp,
+            "model": str(self.model_path),
+            "scan_name": scan_name,
+            "total_points": len(points),
+            "torque_points": torque_points,
+            "airgap_mesh": airgap_mesh,
+            "points": [
+                {
+                    "writes": [[v, val] for v, val in p.get("writes", [])],
+                    **{k: p[k] for k in p if k != "writes"},
+                }
+                for p in points
+            ],
+        }
+        manifest_path.write_text(json.dumps(manifest, indent=2), encoding="ascii")
+        log(f"Run directory: {run_dir}")
+        log(f"Model: {self.model_path}")
+        log(f"Total points: {len(points)}")
+
+        summary = {"ok": 0, "failed": 0, "results": []}
+
+        try:
+            with csv_path.open("w", newline="", encoding="utf-8-sig") as csv_file:
+                writer = csv.DictWriter(csv_file, fieldnames=csv_fields)
+                writer.writeheader()
+                csv_file.flush()
+
+                for idx, point in enumerate(points, 1):
+                    if self._cancel:
+                        log("Cancel requested; stopping before next point")
+                        break
+
+                    writes = point.get("writes", [])
+                    log(
+                        f"[{idx}/{len(points)}] "
+                        + ", ".join(f"{v}={val:g}" for v, val in writes)
+                    )
+
+                    point_result = self.run_single_point(
+                        index=idx,
+                        total=len(points),
+                        writes=writes,
+                        torque_points=torque_points,
+                        airgap_mesh=airgap_mesh,
+                        raw_dir=raw_dir,
+                    )
+
+                    row = {field: "" for field in csv_fields}
+                    row["run_index"] = idx
+                    row["status"] = point_result["status"]
+                    row["seconds"] = point_result["seconds"]
+                    row["error"] = point_result["error"]
+                    for key in extra_fields:
+                        if key in point:
+                            row[key] = point[key]
+                    for key, value in point_result["metrics"].items():
+                        row[key] = value
+
+                    writer.writerow(row)
+                    csv_file.flush()
+
+                    if point_result["status"] == "OK":
+                        summary["ok"] += 1
+                    else:
+                        summary["failed"] += 1
+                    summary["results"].append(row)
+
+                    if self.row_cb:
+                        self.row_cb(row)
+                    if self.progress_cb:
+                        self.progress_cb(idx, len(points))
+
+                    ripple = row.get("ripple_pct", "")
+                    tavg = row.get("tavg_nm", "")
+                    log(
+                        f"[{idx}/{len(points)}] {row['status']} "
+                        f"ripple={ripple} tavg={tavg} seconds={row['seconds']}"
+                    )
+
+            log(f"Scan ended. OK={summary['ok']} FAILED={summary['failed']}")
+            log(f"Results: {csv_path}")
+        finally:
+            log_file.close()
+
+        return {
+            "run_dir": str(run_dir),
+            "csv_path": str(csv_path),
+            "log_path": str(log_path),
+            "manifest_path": str(manifest_path),
+            "summary": summary,
+        }

BIN
书籍与论文/轴向磁场无刷同步电机理论与设计-邓秋玲.pdf


BIN
书籍与论文/轴向磁通永磁无刷电机(原书第2版), Jacek F. Gieras.pdf.pdf


Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott