Parcourir la source

chore(repo): rebuild git after object corruption + thermal simulation work

Original .git objects were lost when an interrupted git gc (triggered by git stash) deleted packs and loose objects; remote is unreachable. Reinitialized git and committed the full working tree (verified intact).

Includes:
- Thermal sim: scripts/run_thermal.py (steady + coupled modes)
- Fix P5-M6 thermal APIs (do_steady_state_analysis, export_results SteadyState)
- metrics.py thermal field aliases (measured)
- Executor enable_thermal plumbing (adapter/executor/config)
- All prior project code, docs, models, references
carlin il y a 22 heures
commit
b8dab74ec7
100 fichiers modifiés avec 58816 ajouts et 0 suppressions
  1. 31 0
      .dockerignore
  2. 69 0
      .gitignore
  3. 207 0
      AGENTS.md
  4. 403 0
      CHANGELOG.md
  5. 1372 0
      DRSS V16.html
  6. 34 0
      Dockerfile
  7. 9080 0
      MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot
  8. 250 0
      MotorCAD软件教程及故障处理防范/MotorCAD脚本自动化仿真参考资料.md
  9. 427 0
      PCB轴向磁通电机Motor-CAD仿真策略评审与实施建议.md
  10. 1003 0
      PCB轴向磁通电机自动化仿真系统设计方案介绍.md
  11. 671 0
      PCB轴向磁通电机自动化仿真系统设计方案简介.html
  12. 267 0
      README.md
  13. 610 0
      ai-collab-dev-playbook-v2.md
  14. 12 0
      axial_mag_pull-master/axial_mag_pull/.gitignore
  15. 41 0
      axial_mag_pull-master/axial_mag_pull/AGENTS.md
  16. 9 0
      axial_mag_pull-master/axial_mag_pull/CLAUDE.md
  17. 9080 0
      axial_mag_pull-master/axial_mag_pull/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot
  18. 9081 0
      axial_mag_pull-master/axial_mag_pull/MARS-12S10P_SSSR_D76-C150_V5.0-0819local.mot
  19. 37 0
      axial_mag_pull-master/axial_mag_pull/PLAN.md
  20. 45 0
      axial_mag_pull-master/axial_mag_pull/README.md
  21. 74 0
      axial_mag_pull-master/axial_mag_pull/REPRODUCE.md
  22. 72 0
      axial_mag_pull-master/axial_mag_pull/RESULTS.md
  23. 158 0
      axial_mag_pull-master/axial_mag_pull/axial_compare.py
  24. 241 0
      axial_mag_pull-master/axial_mag_pull/axial_force_final.py
  25. 221 0
      axial_mag_pull-master/axial_mag_pull/axial_force_run.py
  26. 123 0
      axial_mag_pull-master/axial_mag_pull/axial_probe.py
  27. 136 0
      axial_mag_pull-master/axial_mag_pull/axial_probe2.py
  28. 150 0
      axial_mag_pull-master/axial_mag_pull/axial_probe3.py
  29. 189 0
      axial_mag_pull-master/axial_mag_pull/axial_probe4.py
  30. 94 0
      axial_mag_pull-master/axial_mag_pull/docs/COMPARISON_V3.md
  31. 272 0
      axial_mag_pull-master/axial_mag_pull/docs/CONVERSATION_LOG.md
  32. 105 0
      axial_mag_pull-master/axial_mag_pull/docs/GUI_GUIDE.md
  33. 143 0
      axial_mag_pull-master/axial_mag_pull/docs/KNOWLEDGE_BASE.md
  34. 319 0
      axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V1.html
  35. BIN
      axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V1.pdf
  36. 388 0
      axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V2.html
  37. BIN
      axial_mag_pull-master/axial_mag_pull/docs/report_axialforce_V2.pdf
  38. 72 0
      axial_mag_pull-master/axial_mag_pull/gui_app/README.md
  39. 11 0
      axial_mag_pull-master/axial_mag_pull/gui_app/build.bat
  40. 790 0
      axial_mag_pull-master/axial_mag_pull/gui_app/main.py
  41. 3 0
      axial_mag_pull-master/axial_mag_pull/gui_app/requirements.txt
  42. 318 0
      axial_mag_pull-master/axial_mag_pull/gui_app/solver.py
  43. 25 0
      axial_mag_pull-master/axial_mag_pull/show_motorcad.py
  44. BIN
      axial_mag_pull-master/axial_mag_pull/轴向磁通电机轴向磁拉力计算与轴承选型校核报告V3.0-20260826.pdf
  45. 127 0
      deploy.ps1
  46. 38 0
      docker-compose.yml
  47. 176 0
      docs/CODE_REVIEW_RESPONSE.md
  48. 1058 0
      docs/CONVERSATION_LOG.md
  49. 144 0
      docs/HANDOFF.md
  50. 325 0
      docs/KNOWLEDGE_BASE.md
  51. 545 0
      docs/P1-P5交付总结与上手指南.md
  52. 673 0
      docs/PAPER_KNOWLEDGE_BASE.md
  53. 155 0
      docs/PLATFORM_DESIGN_V2.md
  54. 2183 0
      docs/TEST_RECORDS.md
  55. 286 0
      docs/archive/P1-P4回顾与P5规划.md
  56. 258 0
      docs/archive/P3-评审响应与更新计划.md
  57. 254 0
      docs/archive/P3_IMPLEMENTATION_PLAN.md
  58. 142 0
      docs/archive/P4_IMPLEMENTATION_PLAN.md
  59. 11 0
      docs/archive/README.md
  60. 148 0
      docs/前端界面优化建议_V1.md
  61. 11 0
      executor_config.json
  62. 9080 0
      models/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot
  63. 44 0
      nginx.conf
  64. 28 0
      scripts/build_executable.ps1
  65. 193 0
      scripts/check_machine_paths.py
  66. 219 0
      scripts/executor_config.py
  67. 834 0
      scripts/robust_motorcad.py
  68. 11 0
      scripts/run_gui.py
  69. 205 0
      scripts/run_scan.py
  70. 159 0
      scripts/run_single.py
  71. 168 0
      scripts/run_task_executor.py
  72. 75 0
      scripts/run_task_executor_parallel.py
  73. 204 0
      scripts/run_thermal.py
  74. 12 0
      scripts/scan_airgap.json
  75. 598 0
      scripts/task_executor.py
  76. 214 0
      scripts/test_adapters.py
  77. 33 0
      scripts/test_api_client.py
  78. 222 0
      scripts/test_executor_config.py
  79. 78 0
      scripts/test_executor_m3.py
  80. 122 0
      scripts/test_executor_p5m2.py
  81. 241 0
      scripts/test_metrics_extension.py
  82. 159 0
      scripts/test_p3_adaptive_execution.py
  83. 91 0
      scripts/test_p3_checkpoint.py
  84. 153 0
      scripts/test_p3_closed_loop.py
  85. 109 0
      scripts/test_p3_concurrency.py
  86. 86 0
      scripts/test_p3_m4_contract.py
  87. 134 0
      scripts/test_p3_orchestrator.py
  88. 154 0
      scripts/test_p3_unit_edge.py
  89. 223 0
      scripts/test_p4_acceptance.py
  90. 80 0
      scripts/test_p4_m4_convergence.py
  91. 91 0
      scripts/test_p4_m5_l0.py
  92. 153 0
      scripts/test_p4_schema.py
  93. 209 0
      scripts/test_plan_schema.py
  94. 199 0
      scripts/test_platform_registry.py
  95. 189 0
      scripts/test_robust_solver.py
  96. 147 0
      scripts/test_search_state_summary.py
  97. 199 0
      scripts/test_strategy_morris.py
  98. 255 0
      scripts/test_strategy_surrogate.py
  99. 277 0
      scripts/test_topology_variable_map.py
  100. 1 0
      src/__init__.py

+ 31 - 0
.dockerignore

@@ -0,0 +1,31 @@
+# E1 fix: prevent secrets and unnecessary files from entering Docker image
+.env
+.env.*
+!.env.example
+.git
+.gitignore
+node_modules
+output/
+runs/
+build/
+dist/
+__pycache__/
+*.pyc
+*.pyo
+*.log
+*.db
+*.db-journal
+*.spec
+*.mot
+docs/
+第三方评审/
+axial_mag_pull-master/
+torqrippswap-master/
+MotorCAD软件教程及故障处理防范/
+书籍与论文/
+DRSS V16.html
+web/frontend/node_modules
+web/frontend/dist
+web/backend/output
+web/backend/*.db
+web/backend/data

+ 69 - 0
.gitignore

@@ -0,0 +1,69 @@
+# === 生成物(不入库) ===
+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
+
+# === Web端 ===
+web/frontend/node_modules/
+web/frontend/dist/
+web/backend/*.db
+web/backend/*.db-journal
+web/backend/app/*.db
+web/backend/__pycache__/
+web/backend/app/__pycache__/
+web/backend/app/**/__pycache__/
+
+# === IDE / OS ===
+.vscode/
+.idea/
+.DS_Store
+Thumbs.db
+
+# === 环境 ===
+.env
+.venv/
+venv/
+
+# === 临时文件 ===
+*.tmp
+*.bak
+*~
+
+# === git 损坏备份(恢复过程临时目录,不入库) ===
+.git.corrupted.bak/
+
+# === WorkBuddy 项目数据(不入库) ===
+.workbuddy/
+
+# === 用户个人资料(截图 / 外部评审文档,不入库) ===
+*.png
+*.docx
+第三方评审/
+
+# === 文件名超长(Windows 路径 260 字符限制),git 无法索引 ===
+书籍与论文/相关论文-Chulaee*/

+ 207 - 0
AGENTS.md

@@ -0,0 +1,207 @@
+# AGENTS.md — AI 工具工作说明
+
+> 本文件面向 Claude Code / Codex / Cursor / 豆包等 AI 编程工具。
+> 接到任何任务前,**先读 [docs/KNOWLEDGE_BASE.md](docs/KNOWLEDGE_BASE.md)**。
+> 方法论框架:`ai-collab-dev-playbook-v2.md`(五支柱 + 五步启动法,新项目可复用)。
+> 接续入口:换人/换机/新会话先读 `docs/HANDOFF.md`。
+
+## 项目定位
+
+PCB轴向磁通电机自动化仿真系统 — 双系统解耦架构:
+- **系统一(Web端)**:方案生成与优化(FastAPI + Vue3,AI 闭环 / 自适应搜索 / 经验库)
+- **系统二(本地EXE)**:仿真执行(Motor-CAD / Maxwell / JMAG 适配器,批量调度)
+- **共享核心层** `src/afmcore/`:指标 / 拓扑 / 适配器 / 策略 / L0 预筛选的单一事实源
+
+当前状态:**P1~P5 全部完成,P6 前端体验优化进行中**(P6-M1 已完成,P6-M2/M3 待办)。
+进度、阻塞点、待办的唯一权威来源:`docs/HANDOFF.md` 第 3 节。
+
+## 开始工作前必须阅读
+
+**最小必读集**(新会话至少读这些):
+1. `docs/HANDOFF.md` — 当前进度、阻塞点、待办、接续提示词(含环境恢复步骤)
+2. `docs/KNOWLEDGE_BASE.md` — 核心知识库(环境事实、参数语义、探测技术、SOP、已踩的坑),重点 §1 环境事实与 §3 参数语义
+
+**按需查阅**:
+3. `README.md` — 项目说明、目录结构、快速开始(历史更新见 `CHANGELOG.md`)
+4. `PCB轴向磁通电机自动化仿真系统设计方案介绍.md` — 完整设计方案V2.0(架构、接口、算法选型)
+5. `docs/P1-P5交付总结与上手指南.md` — 新人上手与模块定位
+6. 参考案例的知识库:
+   - `axial_mag_pull-master/axial_mag_pull/docs/KNOWLEDGE_BASE.md`
+   - `torqrippswap-master/torqrippswap/MOTORCAD_SCAN_KNOWLEDGE_BASE.md`
+7. 已完结的历史计划/评审文档见 `docs/archive/`
+
+## 环境体检(换机/新会话第一条命令)
+
+```powershell
+python scripts/check_machine_paths.py        # 只读核对环境,缺项会给修复建议
+python scripts/check_machine_paths.py --fix  # 打印精确修复命令(不自动执行)
+```
+
+## 参考案例代码(可直接复用/改造)
+
+| 功能 | 参考文件 | 说明 |
+|---|---|---|
+| 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` 连接已有实例
+
+## 项目纪律(违者返工)
+
+### 1. 每次阶段/里程碑完成必须更新 README.md
+- 每个 Phase(P1/P2/P3/P4...)或每个 Milestone(M1/M2/M3...)完成后,**必须**立即更新 `README.md`,记录:
+  - 完成的功能点
+  - 新增的文件/模块
+  - 关键技术决策
+  - 已知问题和后续计划
+- 不允许"代码提交了但 README 没更新"的情况
+- README 更新应与代码提交在同一个 commit 中,或紧随其后
+
+### 2. 每次测试必须工作留痕
+- 每次实际运行 Motor-CAD 仿真、API 测试、集成测试后,**必须**记录到 `docs/TEST_RECORDS.md`(含索引表 + 详细记录)
+- 每次对话(含关键决策、技术选择、问题排查)**必须**记录到 `docs/CONVERSATION_LOG.md`,格式:`## YYYY-MM-DD — 主题` → 用户要求 → 本次完成 → 遗留问题
+- 记录内容包括:测试日期、测试环境、测试目的、测试步骤、测试结果(成功/失败)、关键数据、发现的问题、修复措施
+- 测试输出文件(CSV/JSON/log)保留在 `output/` 目录中,不入库但在记录中注明路径
+- 失败的测试也要记录,包括失败原因和后续修复
+
+### 3. Git 提交信息规范
+- Commit message 格式:`type(scope): description`
+  - type: feat/fix/docs/refactor/test/chore/enhance
+  - scope: 模块名(如 robust_motorcad, frontend, backend, p4-m3)
+- 重大变更在 commit message 中详细说明,不允许只写 "update" 或 "fix bug"
+
+### 4. 生成物不入库
+- `output/`、`runs/`、`build/`、`dist/`、`*.log`、`*.spec`、`__pycache__/` 不入库
+- 关键数值转录进入库的文档(TEST_RECORDS.md / RESULTS.md / 报告)
+
+## 反模式自查(交付前对照,命中即返工)
+
+| 反模式 | 对策 |
+|---|---|
+| 不读文档直接开工 | 先读 HANDOFF/KNOWLEDGE_BASE,先报告理解再动手 |
+| 验收凭"看起来对" | 量化基准 + 独立验证(校验方式≠产出方式) |
+| 踩坑不记录 | 坑立刻追加到 KNOWLEDGE_BASE / AGENTS 铁律 |
+| 环境假设不检测 | 换机先跑 check_machine_paths.py |
+| 上下文只留在对话里 | 写 CONVERSATION_LOG 持久化 |
+| 多处定义同一概念 | 走 src/afmcore 单一事实源 |
+| 编造未验证的数值/接口 | 禁臆测 + 标注"待验证" |
+| 阶段完成不更新文档 | 每 P/M 完成立即更新 README |
+
+## 接续与交接
+
+- 换人/换机/新会话:先读 `docs/HANDOFF.md`(进度/阻塞/待办/接续提示词),再跑 `python scripts/check_machine_paths.py`
+- 阶段末(每 Phase/Milestone):更新 HANDOFF.md 的「当前进度/阻塞点/待办」+ README「最近更新」
+- 让下一个 AI 会话 10 分钟接上:整段粘贴 HANDOFF.md 第 4 节「接续提示词」
+
+## 工程规范 — 严格执行
+
+> 适用于所有代码、测试与交付,优先级高于"完成速度"。任何输出在交付前必须过一遍本节。
+
+### 1. 禁止臆测
+- 不得编造任何未实际验证的结果、数值、接口行为或"应该能跑"的结论。
+- 若无法运行或测试某段代码/场景,必须明确说明:**"我无法执行此测试,以下是我的推理/建议"**,并给出理由与降级方案,不得冒充已验证。
+
+### 2. 测试完备
+- 每段交付代码必须附带**可运行的测试用例**,至少覆盖:
+  - 正常路径(happy path)
+  - 边界条件(极值、上下限、最大/最小)
+  - 异常输入(非法值、缺字段、类型错误)
+  - 空值/零值场景
+- 测试脚本统一放 `scripts/test_*.py`,命名与被测模块对应;`python scripts/test_*.py` 可独立运行,exit 0 = PASS。
+- 无法自动化的场景(如真实 Motor-CAD 求解、真实 AI 调用)必须给出可复现的手动测试步骤,或在 TEST_RECORDS.md 标注"待验证"。
+
+### 3. 代码规范
+- 遵循语言标准规范(Python 遵循 PEP8;前端遵循项目既有风格)。
+- 关键逻辑必须有注释;复杂函数/类必须有 docstring(函数用途、参数、返回值、异常)。
+- 沿用 ASCII 约束(见"硬性工程约束 §1"),docstring 用英文。
+
+### 4. 自查清单(交付前逐项确认,回复中标注 ✅/❌)
+- [ ] 代码已通读一遍,无语法错误和明显逻辑漏洞
+- [ ] 所有测试用例已列出,且能描述预期输入与输出
+- [ ] 已考虑边界情况(空值、极值、并发、超时、资源耗尽等)
+- [ ] 已考虑错误处理路径(异常捕获、回滚、降级)
+- [ ] 如果涉及多模块,已确认接口契约和数据流向
+- [ ] 如果无法实际运行测试,已明确告知用户
+
+### 5. 交付声明
+- 只有完成上述自查并确认无误后,才能说"已完成/已通过"。
+- 否则必须使用"**草案待验证**"或"**需要您协助测试**",并说明缺口。
+
+### 6. 迭代修正
+- 若用户反馈测试失败,必须:复现问题 → 定位根因 → 修复 → **重新走一遍自查清单** → 再回复。
+- 不得仅口头致歉后跳过复现与修复。

+ 403 - 0
CHANGELOG.md

@@ -0,0 +1,403 @@
+# CHANGELOG — PCB轴向磁通电机自动化仿真系统
+
+> 本文件承接 README.md 的全部历史更新记录(按时间倒序)。
+> README 只保留最近更新的摘要表;**当前项目状态以 [docs/HANDOFF.md](docs/HANDOFF.md) 第 3 节为唯一权威来源**。
+> 记录规范:新条目加在最上方,格式 `## YYYY-MM-DD — 主题`,里程碑明细另见 `docs/P1-P5交付总结与上手指南.md`。
+
+---
+
+## 2026-09-01 — AI 协作方法论框架升级(Playbook V2)
+
+**背景**:项目已沉淀完整工程纪律(AGENTS.md 硬约束 + TEST_RECORDS + 单一事实源 + 里程碑管理),但缺 playbook 的“接续/换机/上下文延续”机制。本次融合本项目框架与 ai-collab-dev-playbook.md(MARS 电机热仿真方法论)产出 V2 框架,并落地本项目。
+
+**新增/修改**:
+1. 新增 `ai-collab-dev-playbook-v2.md`:五支柱文件体系(README/AGENTS/HANDOFF/KNOWLEDGE_BASE/留痕双件套)+ 五步启动法 V2 + 全套模板(含里程碑管理/测试纪律/单一事实源),下个项目可直接复用
+2. 新增 `docs/HANDOFF.md`:环境要求、恢复步骤、当前进度(P1~P5+P6-M1 完成)、阻塞点(5 个未验证变量/热求解/多工具 mock/Kriging 降级)、待办(P6-M2/M3)+ 接续提示词
+3. 新增 `scripts/check_machine_paths.py`:只读环境体检(Python 版本/环境变量/Motor-CAD exe/Python 包/Git 状态/仓库资产/node),`--fix` 打印修复命令;含项目 venv 探测提示
+4. `AGENTS.md` 接入新框架:补 HANDOFF/playbook-v2 引用、环境体检命令、会话日志强制、反模式自查表、接续与交接节
+5. `docs/CONVERSATION_LOG.md`:头部补统一模板说明 + 追加本次会话记录
+
+**验证**:check_machine_paths.py 本机运行通过(py_compile + 纯 ASCII + 体检输出正常,详见 TEST_RECORDS TEST-024)。
+
+**技术决策**:check_machine_paths.py 采用“默认只读 + --fix 仅打印命令不自动执行”(避免脚本擅自改系统环境);会话日志沿用现有 CONVERSATION_LOG(不新建 session_log,避免双日志漂移)。
+
+**已知问题/后续**:新项目可直接复用 playbook-v2;PCB 项目对照框架第 8 节落地清单逐项对齐。
+- **V2.1 迭代**(本次):playbook 已通用化重构——主文不绑定任何项目,新增「版本更新记录」;MARS/PCB 降级为附录案例,可直接复制到任何新项目初始化。
+
+---
+
+## 2026-08-30(晚)— P5-M2 拓扑感知变量名映射与执行前校验
+
+### 背景
+Plan 23 的 80 个扫描点全部失败,根因是扫描变量使用了径向磁通电机(RFM)的变量名 `Stator_Lam_Outer_Dia` / `Stator_Lam_Inner_Dia`,而当前模型是轴向磁通电机(AFM/SSSR),Motor-CAD 中不存在这些变量。
+
+### 修复内容
+1. **新增拓扑感知变量名映射模块** `web/backend/app/services/topology_variable_map.py`
+   - 定义 SSSR / AFIR / RFM 三种拓扑的已知变量集合(41 个 AFM 变量)
+   - RFM→AFM 别名映射(`Stator_Lam_Outer_Dia` → `Stator_Outer_Diameter` 等)
+   - 模板逻辑名→Motor-CAD 实际名映射(`Number_of_Slots` → `Slot_Number` 等)
+   - 变量名校验 + 未知变量建议功能
+
+2. **修复参数展开函数** `_expand_plan_to_parameters()`
+   - 扫描变量现在经过模板 motorcad_var + 拓扑别名映射,不再直接用原始 name
+   - fixed_params 中不在模板里的参数也经过拓扑别名解析
+
+3. **新增执行前变量名校验** `start_simulation`
+   - 启动仿真前校验所有参数名,未知变量直接返回 400 错误并给出建议名
+   - 防止静默的 Motor-CAD "Could not find variable" 失败
+
+4. **补充模板缺失的 motorcad_var**
+   - 为 8 个几何/电气参数补充了 motorcad_var(`Stator_Outer_Diameter`、`CurrentDefinition` 等)
+   - 仅剩 `Current_Density` 和 `Insulation_Class` 合理留空(非直接 Motor-CAD 变量)
+
+5. **新增变量目录 API** `GET /api/plans/variable-catalog?topology=SSSR`
+   - 返回指定拓扑的模板参数(含 motorcad_var)和已知变量集合
+   - 前端用于填充扫描变量下拉列表
+
+6. **前端优化** PlanDetail.vue
+   - 扫描变量从后端 API 获取拓扑感知的变量目录
+   - 下拉列表显示中文名 + 逻辑名 + Motor-CAD 实际变量名
+   - 不再依赖本地硬编码模板
+
+### 测试
+- 单元测试 `scripts/test_topology_variable_map.py`:8/8 PASS
+- 包含 plan 23 回归测试:验证 RFM 变量名被正确映射,80 个点全部已知
+
+---
+
+## 2026-08-30 — P6-M1:Web 前端 UI/UX 全面重构(专业仿真工具风格)
+
+**背景**:P5 完成后功能完备,但前端存在信息过载(PlanDetail 1129 行长卷)、导航 14 项平铺无分组、参数三套命名口径、监控页功能重叠、stat-card 四处重复手写等问题。参考 SimScale / Ansys 等在线仿真工具的设计语言(工作流树式导航、三级可见性、高数据墨水比、卡片式分组、上下文操作就近放置),完成 B1(信息架构)+ B2(PlanDetail 分层)两批次改造。
+
+**全局设计系统**:
+- 新建 `src/style.css` 设计令牌(CSS 变量):专业深蓝主色 `#2563eb`、中性灰阶、8px 网格间距、统一圆角/阴影/过渡
+- 新建 3 个公共组件:`StatCard.vue`(统计卡片,消除 4 处重复手写)、`SectionCard.vue`(内容区块)、`PageHeader.vue`(页面头部)
+- Element Plus 主题变量覆盖(按钮/卡片/表格/标签/Tabs 统一为设计令牌色)
+
+**B1 信息架构重构**(`MainLayout.vue`):
+- 侧边栏由 7 项平铺改为 5 组工作流导航:工作台 / 执行 / 分析 / AI 智能 / 知识
+- AI 高级功能(L0预筛选、多保真度校准、经验库AI增强)收进「高级功能」可折叠子组
+- 面包屑升级为 项目→方案→任务 层级链(原仅两级)
+- 顶栏新增全局任务状态条(运行中任务数,10s 轮询,点击跳转任务管理)
+- 侧边栏支持折叠(240px ↔ 64px)
+- 页面切换淡入过渡动画
+
+**B2 PlanDetail 分层重构**(`PlanDetail.vue`):
+- 单页长卷改为 4 Tab:概览 / 方案参数 / 仿真结果 / AI 闭环
+- 概览 Tab:统计卡 + AI 设计思路 + 验收标准 + 结果摘要(前5行)+ 快捷操作卡片
+- 方案参数 Tab:扫描变量(主)+ 边界条件 + 固定参数(默认只显示"已修改"项,其余按分类折叠)
+- 仿真结果 Tab:完整结果表 + 导出
+- AI 闭环 Tab:步骤指示器 + AI分析/生成迭代/提取经验 三步向导
+- 运行中进度横幅(始终可见,含预计剩余时间)
+- 预估耗时由硬编码 `points×3min` 改为 `points×2.5min`(贴近真实 90~150s/点)
+- 固定参数"需确认"变量名高亮标签(README 已知的 5 个未验证变量)
+
+**其他页面优化**:
+- `ProjectList.vue`:PageHeader + StatCard + SectionCard + 搜索/拓扑筛选 + 创建弹窗边界条件双列布局
+- `Dashboard.vue`:PageHeader + StatCard + ECharts 趋势/Pareto 图 + 结果表
+- `TaskManager.vue`:PageHeader + 6 项状态统计卡 + 状态筛选 + 创建任务从方案下拉选择(JSON 降为高级折叠项)+ 详情抽屉优化
+
+**验证**:`npm run build` 全绿(vue-tsc 0 错误,vite 11.62s 构建完成,2283 模块)。详见 TEST_RECORDS TEST-023。
+
+**后续批次**:B3(参数目录单一事实源,前后端协同)、B4(流程引导+仿真前检查清单+耗时校准)待实施。
+
+---
+
+## 2026-08-30 — 文档卫生修复 + 前端分包优化(外部工程审核整改)
+
+**背景**:外部系统专家审核发现 3 处文档污染与 1 处前端性能问题,本次全部修复。
+
+1. **README.md 控制字符清理**:清除 2 个粘贴带入的控制字符(0x07/0x08),修复 `daptive→adaptive`、`atch_scheduler.py→batch_scheduler.py` 两处被吞字符的文本
+2. **docs/TEST_RECORDS.md 修复**:清除 3 个控制字符(`fmcore→afmcore` 等);重建"测试记录索引"表(TEST-004~022 原本 5 列错位、内容串行,现恢复为 22 条完整 5 列表格)
+3. **前端 chunk 分包**:`vite.config.ts` 增加 `manualChunks`(vendor-vue / vendor-echarts / vendor-element 三包拆分),业务代码与第三方库解耦,改善缓存与首屏加载;重新 build 验证 EXIT=0(vue-tsc 0 错误)
+
+**验证**:README / TEST_RECORDS / .gitignore 三文件控制字符清零;`npm run build` 全绿;业务 chunk 由 1MB 级降为 13~54kB(vendor 库独立成 3 包)。
+
+---
+
+## 2026-08-30 — P5-M6:多物理场 L2 接入(热网络+结构指标 + 报告模板化)
+
+**背景**:P5 路线最后一批。metrics.py 仅 25 个电磁指标,报告为单一 metrics 表。P5-M6 扩加热网络+结构指标(自动生效),robust_motorcad 加热求解开关,报告按物理域分组。
+
+**metrics.py 扩项**(单一事实源,extract_all_metrics 自动生效):
+- 热网络 6 项(domain=thermal):winding_hotspot_temp_c / magnet_temp_c / stator_temp_c / bearing_temp_c / temp_rise_c / thermal_resistance_k_w
+- 结构 4 项(domain=structural):axial_force_n / radial_force_n / max_stress_mpa / deformation_mm
+- 均 required=False,含英文+中文别名;总指标 25→35
+
+**robust_motorcad 热求解**:`enable_thermal` 开关(__init__ + run_single_point),默认关;电磁求解后尽力而为 do_thermal_calculation + Thermal 导出合并,失败不中断电磁结果。需要模型热网络配置(环境依赖)。
+
+**报告模板化**:report_generator Results Summary 按域分组(Electromagnetic / Thermal / Structural),每域子标题+表格,空域跳过;从 afmcore.metrics 取 domain/label/unit;JSON fallback 含 metrics_by_domain。
+
+**验证**:test_metrics_extension.py 20/20 PASS(定义/提取/中文别名/必需校验/域分组/报告JSON/robust参数);全量回归 21/21。真实热求解待模型配置+实际运行。详见 TEST_RECORDS TEST-022。
+
+**P5 路线全部完成**(M1-M6)。
+
+---
+
+## 2026-08-30 — P5-M5:多工具适配器(Maxwell/JMAG mock + 执行器 tool 参数化)
+
+**背景**:适配器层仅有 MotorCADAdapter,执行器硬编码 import motorcad。P5-M5 新增 MaxwellAdapter/JMAGAdapter(mock 实现,真实接入标注环境依赖),执行器根据 `tool` 动态选择适配器。
+
+**新增适配器**:
+- `MaxwellAdapter`(tool=`maxwell`):mock 实现,capability_domains=(electromagnetic, thermal),确定性合成指标(含 winding_temp_c)
+- `JMAGAdapter`(tool=`jmag`):mock 实现,capability_domains=(electromagnetic,),指标系数与 Maxwell 不同以区分工具
+- 两者均实现 SimulationAdapter 协议(connect/disconnect/load_model/set_parameter/run_simulation/extract_metrics),set_parameter 含回读校验,mock=False 时 connect() 抛 RuntimeError 标注环境依赖
+
+**执行器改造**:`MotorCADTaskExecutor._ensure_adapter()` 从硬编码 import 改为根据 `self.tool` 动态 import(motorcad/maxwell/jmag);`executor_config.json` 的 `tool` 字段现在可切换工具。
+
+**验证**:test_adapters.py 22/22 PASS(注册/全链路/工具区分/回读校验/边界/异常/执行器动态 import);全量回归 20/20。真实 Maxwell/JMAG 接入待目标机环境(需 Ansys Maxwell + PyAEDT / JMAG + jmagpy)。详见 TEST_RECORDS TEST-021。
+
+---
+
+## 2026-08-30 — P5-M4:策略层高级管线(Morris 灵敏度 + IDW 代理 + 预算自适应)
+
+**背景**:蓝图 §6 设计四阶段优化管线(Morris 筛选 → LHS 采样 → Kriging 代理 → NSGA-II),但当前策略层仅有 full_factorial/lhs/adaptive。P5-M4 新增 Morris 灵敏度筛选和代理模型引导策略,补齐蓝图前两阶段。
+
+**依赖降级**:环境无 numpy/scipy/sklearn,项目纪律要求纯 stdlib。代理模型用 **IDW(反距离加权)** 替代 Kriging——预测 + 最近邻距离不确定性,支持 UCB explore-exploit 权衡。后续升级 Kriging 须引入 scipy 并在同接口下替换。
+
+**新增策略**:
+- `MorrisStrategy`(kind=`morris`):纯 stdlib Morris OAT 灵敏度筛选,state() 返回 sensitivity_ranking(mu/mu_star/sigma)+ key_parameters
+- `SurrogateGuidedStrategy`(kind=`surrogate_guided`):初始 LHS → IDW 代理 + UCB 采集 + 预算自适应批次大小,state() 返回 phase/budget/surrogate 诊断(loo_rmse/mean_uncertainty)
+
+**验证**:test_strategy_morris.py 15/15 + test_strategy_surrogate.py 17/17;Morris 线性函数灵敏度排名正确(a=2.0 > b=0.5);SurrogateGuided bowl 函数收敛 best y=0.007(理想 0.0);全量回归 19/19。详见 TEST_RECORDS TEST-020。
+
+---
+
+## 2026-08-30 — P5-M3:adaptive 可视化补全(批次状态 + L0 摘要 + 运行期轮询)
+
+**背景**:P4-M4 上线收敛曲线后,adaptive 优化页面缺少批次级状态可见性(每批多少点成功/失败/不可行)、L0 预筛选结果展示(采样可行率/不可行原因),且运行期只能手动刷新。P5-M3 补齐三视图。
+
+**后端**:`feasibility_search.py` `get_state_summary()` 增加 `batch_summary`(每批进度/状态分布/批内最优)、`l0_summary`(采样可行率/不可行原因 Top N)、`infeasible_points`、`failed_points`;`search.py` SearchStateResponse 同步增加字段(向后兼容)。
+
+**前端**:`AdaptiveOptimize.vue` 增加①运行期 3s 自动轮询(非 searching 状态自动停止)②L0 预筛选摘要卡片(可行率进度条 + 不可行原因 tag)③批次状态总览卡片(每批进度条 + 状态分布 tag + 批内最优)。
+
+**验证**:`test_search_state_summary.py` 8/8 PASS;vue-tsc && vite build 零类型错误;独立后端端到端 create/state 新字段正常。详见 TEST_RECORDS TEST-019。
+
+---
+
+## 2026-08-30 — P5-M2:EXE 配置化 config.json + mock 分支修复
+
+**背景**:本地执行器 EXE 此前只能靠环境变量(MOTORCAD_MODEL / WEB_BASE_URL)配置,部署到目标机需改环境。P5-M2 引入 `executor_config.json` 侧车配置:web 地址 / 模型路径 / 日志 / 实例数全部可改,无需重新打包。
+
+**改动**:
+1. `scripts/executor_config.py`(新增):配置加载器。优先级:`--config` > `$EXECUTOR_CONFIG` > `<EXE目录>/executor_config.json` > `<仓库根>/executor_config.json` > 内置默认;单字段仍可被环境变量覆盖;相对路径按 EXE 目录/仓库根解析;数值/枚举/类型校验。
+2. `executor_config.json`(新增,仓库根模板):web_base_url / model_path / poll_interval / instances / log_dir / log_level / tool / enable_mock。
+3. `scripts/run_task_executor.py`:argparse 支持 `--config / --instances / --interval / --mock / --log-dir / --log-level`;logging 落盘;支持单入口多实例(instances>1)。
+4. `scripts/run_task_executor_parallel.py`:复用共享配置加载(保留原 CLI 兼容)。
+5. `scripts/task_executor.py`(修复 P4-M3 遗留):`MotorCADTaskExecutor._run_simulation_point` 此前忽略 `enable_mock` 无条件走真实 adapter;现 `enable_mock=True` 时走基类 mock(结果带 `source="mock"`),不启动 Motor-CAD、不要求 model_path。
+
+**测试**:`scripts/test_executor_config.py`(20 用例:默认/文件合并/环境覆盖/优先级/边界/异常/空值)+ `scripts/test_executor_p5m2.py`(6 用例:mock 分支正常/多点/非法路径仍 mock/默认关闭/真实模式缺模型路径报错/空路径),全部 EXIT=0;全量回归 16/16 PASS。
+
+**EXE 端到端验证(mock)**:重新打包 `dist/PCB-AFM-Executor.exe`(1.1.0);独立后端(8010 + 临时 DB)→ EXE(`--config` 侧车,enable_mock=true)认领 3 点任务 → mock 求解 → 回传 Web → 任务 completed、3/3 成功、指标入库(tavg 34.56~42.05 N·m、eff 87.95~89.73%)。点级结果带 `source="mock"` 防混淆。
+
+**真实 Motor-CAD 端到端**:✅ 本机已验证单点全链路(EXE → 真实 Motor-CAD → Web,duration 167.5s,tavg=0.52187 / eff=86.06% / losses=41.945W / back_emf=11.15V,与 TEST-010 完全一致,见 TEST-018)。过程中修复两个真实链路 bug:业务名 `airgap_mm→Airgap` 映射 + `point_id` 元数据键排除。真实短扫描/多实例可在目标机验证。
+
+---
+
+## 2026-08-30 — P5-M1:前端全量 build 类型错误清零(backlog B1)
+
+**背景**:`npm run build`(vue-tsc && vite build)历史遗留 71 处类型错误(TS2339/TS2345/TS7006,6 个 .vue)。根因:`src/api/index.ts` 响应拦截器运行时已 unwrap 为 `.data`,但 TS 类型上 `api.get/post` 仍声明为 `Promise<AxiosResponse>`,导致调用处直接访问响应字段报 TS2339;PlanDetail 中 `task.status` 被误判为 HTTP 状态码 number 报 TS2345。
+
+**改动**:
+1. `web/frontend/src/api/index.ts`:axios 实例类型改写为 `UnwrappedApi` 接口(get/post/put/delete 返回 `Promise<T>`,默认 any),类型声明与运行时行为对齐;拦截器逻辑原样保留(实例改名 instance 后 cast)。
+2. `web/frontend/src/views/PlanDetail.vue`:`@selection-change` 回调参数 `sel` 显式标注 `any[]`(消除 TS7006)。
+
+**验证**:`npm run build` 全绿(vue-tsc 0 错误 + vite 2274 modules 构建成功,真实退出码 0);后端 health 200 + `test_api_client.py` 真实链路 PASSED;前端 vite dev + API 代理 200;14 个 Python 回归脚本全绿。详见 docs/TEST_RECORDS.md TEST-016。
+
+---
+
+## 2026-08-29 — 方案详情页三栏职责重构(P2-M2 增强)
+
+**背景**:边界条件栏与固定参数栏数量不平衡。根因:边界条件从模板全量渲染(27项),而固定参数只加载AI方案实际写入的少量参数;且边界条件栏混入了大量设计实现参数。
+
+**改动内容**:
+
+1. **边界条件栏瘦身**(27 → 14项):只保留需求/约束型参数
+   - 性能规格:功率/转速/转矩/电流/母线电压/效率/脉动/损耗
+   - 几何约束:最大外径/轴向长度;热约束:最大温升
+   - 需求描述:拓扑/防护等级/应用场景
+   - 移出(归入固定参数):气隙、内径、槽数、极对数、匝数、并联支路、电流密度、磁钢材料、硅钢片、剩磁、冷却方式、环境温度、绝缘等级
+
+2. **固定参数栏扩容**(模板驱动,36项全量展示)
+   - 后端 `app/services/fixed_params_template.py` 扩充为36个参数,按电机工程习惯分8类
+   - `plan_generator.py` 固定参数改为**模板全量驱动**:AI不再需要输出固定参数,系统按边界条件推断全部参数值
+   - 前端 `PlanDetail.vue` 加载方案时按模板自动补齐缺失固定参数
+   - 每个固定参数加**来源标记**(AI/用户),用户修改后自动切换为用户
+   - 移除"添加固定参数"按钮与弹窗(参数已全量展示,后续扩展往模板里加)
+
+3. **AI方案思路中文简短化**
+   - `plan_generator.py` 默认提示词改造:reasoning 用中文、约200字、只讲关键设计决策与核心参数
+   - AI输出固定模板(plan_name/topology/scan_variables/search_strategy/acceptance_criteria/reasoning),固定参数由系统模板生成
+
+4. **Bug修复**:固定参数含字符串值(磁钢材料/硅钢片/冷却方式等)时一键启动仿真500错误
+   - `plans.py` `_expand_plan_to_parameters`:数值转float、字符串保留原样
+
+**涉及文件**:
+- `web/backend/app/services/fixed_params_template.py`(扩充36参数+8类中文)
+- `web/backend/app/services/plan_generator.py`(模板驱动+中文prompt)
+- `web/backend/app/routers/plans.py`(字符串参数启动修复)
+- `web/frontend/src/views/PlanDetail.vue`(三栏重构+来源标记+去添加按钮)
+
+**已知问题**:
+- 新增参数中 `Max_Speed/Winding_Connection/Current_Density/Magnet_Remanence/Insulation_Class` 的 Motor-CAD 变量名未验证,description 已标注"变量名需确认",执行时可能标记FAILED,需在.mot中确认后修正
+
+---
+
+## 2026-08-29 二期 — 本地执行器真实仿真全链路打通(P2 关键里程碑)
+
+**问题**:前端"一键启动仿真"后一直"仿真中"无进度,Motor-CAD 未打开。
+
+**根因链**:本地执行器未运行 -> 认领逻辑矛盾(只拉pending但任务已dispatched)
+-> 列表接口不含parameters -> 字符串参数float崩溃 -> 模板变量名非真实Motor-CAD变量
+-> AFM_D_Rotor改外径破坏线性几何。
+
+**修复**:
+- 新增 `scripts/run_task_executor.py` 执行器启动入口(后台常驻,认领pending+dispatched任务)
+- `scripts/task_executor.py`:`_hydrate_task` 拉取完整参数集;心跳注册(在线/进度)
+- `scripts/robust_motorcad.py`:跳过非数值参数写入
+- `fixed_params_template.py`:模板加 `motorcad_var`(真实Motor-CAD变量名,从.mot提取);
+  默认值对齐基线模型;AFM_D_Rotor 不写入(外径作约束)
+- `plans.py` `_expand_plan_to_parameters`:模板为骨架合并用户修改,仅写真实变量名
+
+**验证**:plan 21 扫描 Airgap 1->2mm 2点全部OK,Motor-CAD打开、进度推进、结果回传,
+物理规律正确(气隙↑->反电动势↓)。
+
+**涉及文件**:scripts/run_task_executor.py(新)、scripts/task_executor.py、scripts/robust_motorcad.py、
+web/backend/app/services/fixed_params_template.py、web/backend/app/routers/plans.py
+
+**已知问题**:字符串参数(材料/冷却/绝缘)暂不写入,用基线默认;几何改型需Motor-CAD内操作。
+
+---
+
+## 2026-08-29 三期 — 平台化改造第一批:共享核心层 + 适配器抽象
+
+**背景**:按平台化升级设计方案(docs/PLATFORM_DESIGN_V2.md),消除"指标定义三处漂移"与"解析器两套实现"两大平台性短板,为多工具/多拓扑/多策略扩展铺路。
+
+**改动内容**:
+1. **新建共享核心层 `src/afmcore/`**(单一事实源)
+   - `metrics.py`:25 项指标定义(key/label/unit/direction/required/aliases) + 归一化解析器
+   - `adapters/`:SimulationAdapter 接口 + 注册表 + MotorCADAdapter 实现
+2. **三个消费端接入共享层**:solver_core / robust_motorcad / metrics_constants 全部改为从 afmcore.metrics 导入
+3. **修复 tavg_nm/ripple_pct 解析缺口(TEST-003 遗留)**:根因为 robust_motorcad 无归一化精确匹配,已统一为归一化匹配(全角括号→半角、去空白、小写)
+4. **命名规避冲突**:共享包从 platform 改名 afmcore(避免与标准库同名)
+
+**验证**:单元验证全部 PASS(含全角字符解析/中文别名/% 守卫/三端导入/78 文件编译/适配器协议),详见 docs/TEST_RECORDS.md TEST-004。
+
+**进展路线(后续批次)**:P2 拓扑注册表 + task_executor 切换适配器;P3 执行策略接入(adaptive);P4 方案 Schema 统一 + 文档同步 + EXE 打包。
+
+---
+
+## 2026-08-29 四期 — 平台化改造第二批:拓扑注册表 + 执行器切换适配器
+
+**背景**:按平台化设计方案 P2,消除“拓扑是裸字符串”与“求解器硬编码”两个平台性短板。
+
+**改动内容**:
+1. **新建拓扑注册表 `src/afmcore/topology.py`**:SSSR 完整参数体系(8 组 37 项)注册,DRSS/SDSR 预留;提供拓扑验证/参数校验/UI 序列化
+2. **`plan_schema.validate()` 集成拓扑校验**:未知拓扑拦截,新增拓扑只做注册
+3. **`task_executor` 切换 `get_adapter()`**:从硬编码 RobustMotorCADSolver 改为统一适配器入口;修复结果嵌套导致的 _compute_metrics 聚合空的缺口
+
+**验证**:新增可重复运行回归测试 `scripts/test_platform_registry.py`(36 项断言全 PASS);79 个 .py 编译 0 失败;三端导入回归通过。详见 TEST_RECORDS.md TEST-005。
+
+---
+
+## 2026-08-29 五期 — P3 平台化改造(第一批:策略抽象层 + 自适应编排器)
+
+**背景**:按 P3 实施计划(docs/archive/P3_IMPLEMENTATION_PLAN.md),把 Web 端已验收的 adaptive 搜索能力接到本地执行器,打通「方案 -> 批次任务 -> 本地执行 -> 回填搜索 -> 续批/收敛」闭环。
+
+**M1 执行策略抽象层(src/afmcore/strategies/)**:
+- 新增 SimulationStrategy ABC + STRATEGY_REGISTRY,执行器改为向策略要批次,不再硬编码全因子
+- full_factorial(笛卡尔积,自包含)/ lhs(纯 Python 拉丁超立方)/ adaptive(backend 注入桥接,共享层不依赖 web)三种策略注册可用
+- plan_schema 的 SearchStrategy.method 归一化(legacy active_learning/constrained -> adaptive)并经策略注册表校验
+
+**M2 任务模型扩展 + 自适应编排器**:
+- Task 模型新增 task_type / loop_id / batch_id / point_ids / dynamic 字段(旧库自动迁移 ADD COLUMN)
+- 新建 strategy_orchestrator.py:AdaptiveOrchestrator(start_loop / advance_loop / get_loop_status),桥 FeasibilityFirstSearch <-> 任务系统 <-> 本地执行器,loop 状态落盘 output/adaptive_loops/
+- 修复现存 bug:task_manager.report_results 未定义 plan_id(应取 task.plan_id)
+
+**验证**:闭环回归脚本 scripts/test_p3_orchestrator.py(fake 执行器,temp DB 隔离),exit 0;详细记录见 docs/TEST_RECORDS.md TEST-006。
+
+**技术决策**:orchestrator 采用 pull 驱动(advance_loop 由调用方/路由/调度器触发),与既有执行器轮询哲学一致;L0 可行性预筛依赖参数名对齐(airgap_mm/current_a),真实 AI plan 生成即对齐。
+
+**已知问题/后续**:路由/定时器接入与真实 Motor-CAD 烟雾测试放 M5;adaptive 前端视图放 P4。
+
+---
+
+## 2026-08-29 六期 — P3 平台化改造(第二批:执行器批次化 + 多实例)
+
+**背景**:P3-M3,让本地执行器原生支持 adaptive_batch 任务与多实例并行,配合 M2 编排器形成批次闭环。
+
+**改动(scripts/task_executor.py)**:
+- point_id 回传:逐点结果顶层带 point_id(OK/FAILED 均保留),编排器可精确回填搜索
+- 认领原子化:dispatch 失败(被其他实例认领)即跳过该任务,多实例不重复仿真
+- executor_id 唯一化 + 修复 report_results 本地模式 on_complete 参数个数不一致 bug
+
+**新增**:scripts/run_task_executor_parallel.py(--instances N 并行启动,--mock 供测试)
+
+**验证**:scripts/test_executor_m3.py(PASS),P2 回归 36/36 不破坏;见 docs/TEST_RECORDS.md TEST-007。
+
+**技术决策**:多实例并行复用 Web 端 dispatch 的幂等语义(pending->dispatched 原子迁移)防重复,执行器侧无需额外锁。
+
+---
+
+## 2026-08-29 七期 — P3 平台化改造(第三批:调度契约统一)
+
+**背景**:P3-M4,统一两套调度状态词汇(TaskManager 的 pending/dispatched 与 BatchScheduler 的 queued),并让调度器任务字段与 Task ORM 对齐。
+
+**改动**:
+- 新建 web/backend/app/services/task_contract.py(无依赖契约层):规范状态常量 + STATUS_ALIASES 归一(queued->pending、canceled->cancelled、completed_with_errors->completed)+ normalize_status/is_terminal/merge_adaptive_fields
+- batch_scheduler.py:add_task 支持 task_type/loop_id/batch_id/point_ids/dynamic 透传,_summary 输出新字段(状态词 queued 向后兼容)
+
+**验证**:scripts/test_p3_m4_contract.py(PASS),M2 闭环回归不破坏;见 docs/TEST_RECORDS.md TEST-008。
+
+**技术决策**:用契约层声明统一词汇/字段而非重构两个既有服务,消除漂移同时保持向后兼容;状态归一采用「别名映射 + 未知词透传」(能报错而非静默)。
+
+---
+
+## 2026-08-29 九期 — P3 平台化改造(第四批:HTTP 全链路闭环验证)
+
+> 注:历史记录中无“八期”编号,沿用原始记录。
+
+**背景**:P3-M5,验证「orchestrator -> 任务系统 -> 本地执行器(HTTP) -> 仿真 -> point_id 回填 -> 续批 -> 收敛」全链路真实闭环。
+
+**改动**:
+- strategy_orchestrator.py:AI 分析按 KIMI_API_KEY 门控(无 key 时保持 quantitative,不空打 AI 日志)
+- 新增 scripts/test_p3_closed_loop.py:真实 FastAPI(temp DB)+ orchestrator + mock 执行器全链路闭环回归,exit 0
+
+**验证**:闭环 n_results=8 budget_exhausted;P2 36 项 + M2/M3/M4 全量回归 PASS;91 个 .py 编译 0 失败;详见 docs/TEST_RECORDS.md TEST-009。
+
+**技术决策**:AI 分析为可选项——无 key 环境自动降级为纯定量结果,配置 key 后自动启用;避免每次批次空调 AI 的日志噪音与延迟。
+
+**真实 Motor-CAD 烟雾**:已通过(TEST-010)——连接/基线加载/求解 1 点/解析 21 项指标全 OK,back_emf=11.15V 与历史 TR-01 一致;同时发现并修复 MotorCADAdapter.run_point 忽略 model_path 的真实 bug。
+
+### P3 平台化改造(第五批:Web 端执行桥 + 路由整合)
+
+**背景**:P3-M6。Web 端已有 AdaptiveLoop 闭环(AI 方案 -> L0 -> FeasibilityFirstSearch -> 选批 -> report-results 回填 -> AI 分析 -> 经验库 -> 收敛),但「本地执行器执行」环节缺失;平台层已具备任务系统 + 执行器(批次 Task、point_id 回填)。本批打通两套体系。
+
+**改动**:
+- adaptive_loop.py:新增 submit_batch_to_executor() —— 把当前 pending 批次包装为 adaptive_batch Task(复用 task_manager 原语),交由本地执行器认领执行
+- routers/adaptive.py:新增 POST /api/adaptive/loops/{loop_id}/submit-batch 端点
+- 撤销误建的 adaptive_orchestrator 路由(与既有 adaptive.py 路径冲突),main.py 还原
+- 新增 scripts/test_p3_adaptive_execution.py:Web 端 AdaptiveLoop + 执行桥全闭环集成回归(temp DB + fake executor + 无 AI)
+
+**验证**:M6 集成测试 PASS(fake plan -> 初始 3 点 -> submit-batch -> 3 批 -> budget_exhausted,8 点/8 预算;Task 落库含 loop_id/batch_id/point_ids/dynamic;幂等性 OK;HTTP 端点 404/200 正常);P2 36 项 + M2/M3/M4/M5/M6 全量回归 PASS;详见 docs/TEST_RECORDS.md TEST-011。
+
+**技术决策**:采用「Web 智能层 + 平台执行层」整合而非新建第二套循环路由——给 AdaptiveLoop 补执行桥(只加方法/端点,不改既有流程),复用已验证的任务系统/执行器,避免两套 /api/adaptive 循环并存。
+
+### P3 收尾遗留处理 + P4 平台化第四批(十期)
+
+**背景**:用户要求"先处理遗留问题 → 写 P4 详细计划 → 直接做 P4"。P4 按 `docs/archive/P4_IMPLEMENTATION_PLAN.md` 五件套实施,全部完成。
+
+**P3 遗留处理(4 commit)**:
+- 并发原子认领:`task_manager.dispatch_task` 改 SQLAlchemy 条件 UPDATE(8 线程竞争恰 1 win)
+- 断点恢复:`FeasibilityFirstSearch.import_state()` + `AdaptiveLoop.export/restore` + `/loops/{id}/export`、`/loops/import` 端点
+- ASCII 纪律:plans.py + fixed_params_template.py 中文串转 `\uXXXX`(运行解码正确)
+- 环境依赖测试:`test_api_client.py` 真实链路;未动用户常驻服务
+
+**P4 实施(M1~M5,5 commit)**:
+- **M1 Schema 单一权威**:`src/plan_schema.py` 增 parse/validate 入口 + 别名容错 + require_model_path 分级;web 端 main.py 注入 repo root,plans/ai_plan 接入校验(400/422)
+- **M2 文档 V1.1→V2.0**:设计方案追加"附录 B 实现现状对照"(蓝图 vs 实现逐项映射)
+- **M3 EXE 打包**:`scripts/build_executable.ps1` + PyInstaller onefile → `dist/PCB-AFM-Executor.exe`(12.6MB,--version/--self-test 通过)
+- **M4 前端收敛曲线**:search state 增 points_history(逐评估点数据)+ AdaptiveOptimize.vue echarts 收敛曲线
+- **M5 L0 上提共享核心**:`L0PreScreeningEngine` 迁 `src/afmcore/l0/prescreening.py`(唯一实现,纯 stdlib),web 薄 re-export 兼容 6 处调用点
+
+**验证**:新增 test_p4_schema / test_p4_m4_convergence / test_p4_m5_l0 全过;P2 36 + M6 + closed_loop + checkpoint + concurrency 全量回归绿;全量 py_compile 0 失败;ASCII 0 违规;vue-tsc 对 AdaptiveOptimize.vue 0 错误(全量 build 仍有既有类型错误属 backlog)。详见 docs/TEST_RECORDS.md TEST-013~015。

+ 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>

+ 34 - 0
Dockerfile

@@ -0,0 +1,34 @@
+# PCB AFM Simulation System - Backend Dockerfile
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y --no-install-recommends \
+    gcc \
+    && rm -rf /var/lib/apt/lists/*
+
+# Copy and install Python dependencies
+COPY web/backend/requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy application code
+COPY web/backend/ .
+
+# Create output directories
+RUN mkdir -p /app/output/tasks /app/output/reports /app/output/scheduler_state
+
+# Environment variables
+ENV DATABASE_URL=sqlite:///./afm_sim.db
+ENV MAX_PARALLEL_TASKS=2
+ENV PYTHONUNBUFFERED=1
+
+# Expose port
+EXPOSE 8000
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
+    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')" || exit 1
+
+# Start application
+CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Fichier diff supprimé car celui-ci est trop grand
+ 9080 - 0
MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot


+ 250 - 0
MotorCAD软件教程及故障处理防范/MotorCAD脚本自动化仿真参考资料.md

@@ -0,0 +1,250 @@
+# Motor-CAD 脚本自动化仿真参考资料汇总
+
+> 用途:作为仿真系统编程设计的参考资料
+> 整理日期:2026-08-27
+> 说明:Motor-CAD 的自动化接口在 2022 年后已从旧的 ActiveX(COM)体系迁移到 **PyMotorCAD**(基于 JSON-RPC 的 Python 接口,属 PyAnsys 生态),因此本资料以 PyMotorCAD 为主、ActiveX 为辅。文中所有链接均来自官方(Ansys / PyAnsys)或公开的社区与组织分享,并标注了来源性质。
+
+---
+
+## 一、官方核心资料(优先阅读)
+
+### 1.1 PyMotorCAD 官方文档站(最重要,一站式入口)
+
+- **PyMotorCAD 官方文档(含版本切换)**
+  https://motorcad.docs.pyansys.com/
+  - 稳定版(stable)与开发版(dev)及历史版本可在页面右上角切换。
+  - 内容结构:Getting Started / User Guide / API Reference(Methods)/ Examples / Contributing。
+  - 官方支持邮箱:pyansys.core@ansys.com;Bug 与功能请求走 GitHub Issues;问答走 Ansys Developer 论坛 Discuss 区。
+
+- **PyMotorCAD Cheat Sheet(一页速查表 PDF)**
+  https://cheatsheets.docs.pyansys.com/pymotorcad_cheat_sheet.pdf
+  - 一页涵盖:启动/退出实例、几何与绕组参数设置、材料赋值、MotorLAB 模型构建、E-Magnetic 性能曲线提取、MATLAB 中调用 PyMotorCAD、ActiveX 旧脚本迁移写法。
+
+- **API 参考(MotorCAD 对象全部方法)**
+  https://motorcad.docs.pyansys.com/version/stable/methods/index.html
+  - 包括:`MotorCAD` 主对象、`MotorCADCompatibility`(兼容旧 ActiveX 脚本)、几何对象与函数(`geometry` / `geometry_tree` / `geometry_shapes` / `geometry_drawing` / `geometry_fitting`)、工具函数、以及 **`MotorCADError` 错误/异常类型**(错误处理入口,见第三节)。
+
+### 1.2 官方 GitHub 仓库:ansys/pymotorcad
+
+- 仓库地址:https://github.com/ansys/pymotorcad
+- PyPI 包:`ansys-motorcad-core`,安装命令:
+
+  ```bash
+  python -m pip install -U pip
+  python -m pip install ansys-motorcad-core
+  ```
+
+- 许可证:MIT(注意:PyMotorCAD 本身开源,但交互控制 Motor-CAD 仍需要合法授权的 Motor-CAD 软件)。
+- **examples/ 目录就是一套现成的自动化脚本样例库**,与文档站的 Examples 页一一对应(见 1.4)。
+- Issues 页面(https://github.com/ansys/pymotorcad/issues)本身就是一份很好的"故障处理案例库",可按 bug 标签检索:https://github.com/ansys/pymotorcad/labels/bug
+- 发布记录(各版本修复内容,可用于排查版本相关问题):https://github.com/ansys/pymotorcad/releases
+
+### 1.3 User Guide 关键章节(操作说明)
+
+| 章节 | 链接 | 内容要点 |
+|---|---|---|
+| User Guide 总览 | https://motorcad.docs.pyansys.com/version/stable/user_guide/index.html | 内部脚本、外部脚本、MATLAB、自适应模板、旧脚本兼容的整体框架 |
+| 内部 Scripting 选项卡 | https://motorcad.docs.pyansys.com/version/stable/user_guide/internal_scripting.html | Motor-CAD 内置 Python 解释器 + Scripting 选项卡;`main()`、`thermal_steady` 等类的 initial/main/final 钩子结构;`MessageDisplayState` 弹窗控制 |
+| 外部脚本 / 加入自己的 Python | https://motorcad.docs.pyansys.com/version/stable/user_guide/external_scripting.html | 安装模式(user / developer)、tox 测试、开发者贡献流程 |
+| MATLAB 脚本 | https://motorcad.docs.pyansys.com/version/stable/user_guide/matlab_scripting.html | 通过 `py.importlib.import_module('ansys.motorcad.core')` 在 MATLAB 里直接用 PyMotorCAD 做完整 E-Magnetic 自动化(含图形数据逐点读取的 try/break 技巧) |
+| 自适应模板脚本(Adaptive Templates) | https://motorcad.docs.pyansys.com/version/stable/user_guide/adaptive_templates.html | 自定义几何:Region/Line/Arc 对象、自适应参数、`reset_adaptive_geometry()`、外部 IDE 调试、`draw_objects()` 几何绘图调试、DXF 导入、圆角/槽口修改最佳实践 |
+| 旧脚本向后兼容 | https://motorcad.docs.pyansys.com/version/stable/user_guide/backwards_compatibility.html | ActiveX → PyMotorCAD 的迁移规则:函数名改 snake_case;旧 `success` 返回值被**异常机制**取代(失败即抛 `MotorCADError`,必须 try/except);`MotorCADCompatibility` 对象可最小改动运行旧脚本 |
+
+### 1.4 官方 Examples(可直接运行的自动化脚本样例)
+
+- 示例总入口:https://motorcad.docs.pyansys.com/version/stable/examples/index.html
+  - 每个示例都可下载为 `.py` 或 Jupyter Notebook。
+- 分类:
+  - **Basic examples(基础)**:如 E-magnetic 基础脚本(建模→计算→导出 CSV→读图数据,含 `MotorCADError` 处理示范)https://motorcad.docs.pyansys.com/version/stable/examples/basics/emag_basics.html
+  - **Internal scripting(内部脚本)**:
+    - 热稳态 https://motorcad.docs.pyansys.com/version/stable/examples/internal_scripting/thermal_steady_state.html
+    - 热瞬态(占空比中改冷却流量)https://motorcad.docs.pyansys.com/version/stable/examples/internal_scripting/thermal_transient.html
+    - 电磁(计算前参数检查与修正)https://motorcad.docs.pyansys.com/version/stable/examples/internal_scripting/emag.html
+    - 机械应力 https://motorcad.docs.pyansys.com/version/stable/examples/internal_scripting/mechanical_stress.html
+    - 机械力/NVH https://motorcad.docs.pyansys.com/version/stable/examples/internal_scripting/mechanical_force.html
+  - **Adaptive templates library(自适应模板库)**:梯形转子风道、圆弧槽底、转子缺口等 https://motorcad.docs.pyansys.com/version/stable/examples/adaptive_library/TrapezoidalDuct.html
+  - **Linking(与其他 Ansys 产品耦合)**:Motor-CAD → Twin Builder 的 ECE 等效电路导出完整流程(JSON 配置驱动 + 错误捕获)https://motorcad.docs.pyansys.com/version/stable/_sources/examples/links/ece_export_for_twinbuilder.rst.txt
+  - **Samples 仓库(dev 版)**:应力采样、区域边界峰值应力、Ansys Motion 力导出、SYNC 电机参数扫描等 https://motorcad.docs.pyansys.com/version/dev/samples/index.html
+
+### 1.5 随软件安装的官方教程(本地,不要忽略)
+
+Motor-CAD 安装目录下的 Tutorials 文件夹自带自动化教程 PDF 与示例(CADFEM 技术日上 Ansys 官方推荐的入口):
+
+- `C:\ANSYS_Motor-CAD\<版本>\Tutorials\ActiveX_Scripting.pdf` —— 通用脚本教程(含 Automation 教程的 section 2.iii,Scripting 选项卡官方文档也指向它)
+- `C:\ANSYS_Motor-CAD\<版本>\Tutorials\FEA_Geometry_Scripting` —— FEA 几何脚本
+- `C:\ANSYS_Motor-CAD\<版本>\Tutorials\Ansys_Optislang\Advance IPM` —— optiSLang 联合优化
+- `C:\ANSYS_Motor-CAD\<版本>\Tutorials\Scripting_Control_In_Duty_Cycle` —— 占空比中的脚本控制
+- `TwinBuilder_ECE_Tutorial` —— Twin Builder ECE 模型导出
+
+出处(CADFEM 2021 技术日官方合作演讲):https://www.cadfem.net/fileadmin/user_upload/05-cadfem-informs/resource-library/2021_siehr_CADFEM_Techday2_scripting_and_parallelization_for_motorcad.pdf
+该 PDF 同时给出 ActiveX 三命令核心模式(SetVariable / DoXxxAnalysis / GetVariable)与 MATLAB、VBS 的最小示例,以及用 Blackbox Solver 做参数研究/优化并行化的思路。
+
+### 1.6 Ansys 官方培训课程(Innovation Space)
+
+- **Running Parallel Ansys Motor-CAD Calculations Through Scripting**(并行计算脚本化,基于 2026 R1)
+  https://innovationspace.ansys.com/certifications/courses/running-parallel-ansys-motor-cad-calculations-through-scripting/
+  - 用 Python multiprocessing 跑多个 Motor-CAD 实例做并行参数研究;MATLAB 则间接通过 Python 调 PyMotorCAD。
+- **Induction Motor Design using Ansys Motor-CAD Sensitivity Analysis**(免费课,含 Python 要求)
+  https://innovationspace.ansys.com/product/induction-motor-design-using-ansys-motor-cad-sensitivity-analysis/
+
+---
+
+## 二、自动化仿真的关键操作模式(编程设计要点)
+
+以下内容提炼自官方文档,是设计仿真系统时的"骨架代码"模式。
+
+### 2.1 连接方式与生命周期
+
+```python
+import ansys.motorcad.core as pymotorcad
+
+# 外部脚本:启动新实例(脚本结束后自动关闭)
+mc = pymotorcad.MotorCAD()
+
+# 外部脚本:启动新实例并保持打开(调试时有用)
+mc = pymotorcad.MotorCAD(keep_instance_open=True)
+
+# 连接已运行的实例 / 内部脚本环境
+mc = pymotorcad.MotorCAD(open_new_instance=False)
+
+# 官方示例推荐的"内外兼容"写法(Adaptive Templates 脚本通用):
+if pymotorcad.is_running_in_internal_scripting():
+    mc = pymotorcad.MotorCAD(open_new_instance=False)
+else:
+    mc = pymotorcad.MotorCAD(keep_instance_open=True)
+```
+
+- 通信机制:Motor-CAD 启动 RPC 服务器,PyMotorCAD 通过 `http://localhost:<端口>/jsonrpc` 与之通信,也支持跨机 HTTP 远程连接。
+- 无头运行:外部脚本支持 **BlackBox 模式**(不显示 GUI),适合服务器批量仿真。
+- 多实例并行:一个外部脚本可同时驱动多个 Motor-CAD 实例(配合 Ansys optiSLang 做优化)。
+
+### 2.2 自动化三件套(所有工作流的最小闭环)
+
+```python
+mc.set_variable("Tooth_Width", 6)          # 1. 设参数
+mc.do_magnetic_calculation()                # 2. 跑计算(do_steady_state_analysis / do_transient_analysis / do_magnetic_calculation ...)
+torque = mc.get_variable("ShaftTorque")     # 3. 取结果
+```
+
+配套常用方法:`load_template("e8")` / `load_from_file()` / `save_to_file()` / `save_results()` / `export_results("EMagnetic", path)` / `get_magnetic_graph()` / `get_fea_graph()` / `quit()`。
+
+### 2.3 弹窗与消息控制(批处理必备)
+
+```python
+mc.set_variable("MessageDisplayState", 2)   # 消息进独立窗口,禁用弹窗
+# ...批量计算...
+mc.set_variable("MessageDisplayState", 0)   # 结束后恢复
+```
+
+> 官方警告:该设置会禁用关键弹窗(含保存提示、覆盖确认),脚本结束前务必恢复;且它**不能**替代异常处理。
+
+### 2.4 内部脚本钩子结构(Scripting 选项卡 / Run During Analysis)
+
+Motor-CAD 在计算前、每个迭代步、计算后分别调用用户类的 `initial()` / `main()` / `final()`。按求解类型定义类:`thermal_steady`、`thermal_transient`、`emagnetic`、`mechanical_stress`、`mechanical_forces`。典型用途:计算中动态修改边界条件(如热瞬态中按时间关断冷却流量)、计算前参数合法性检查与自动修正、计算后自动保存并退出(`save_and_close()` 模式,见 GitHub issue #741 的官方示例代码:https://github.com/ansys/pymotorcad/issues/741)。
+
+### 2.5 MATLAB 集成
+
+- 旧方式:`mcad = actxserver('MotorCAD.AppAutomation')`(ActiveX/COM)。
+- 新方式(官方推荐):MATLAB 内直接加载 Python 包
+  `pymotorcad = py.importlib.import_module('ansys.motorcad.core')`,之后调用与 Python 完全一致;读 FEA 图数据时用 try/catch + break 的循环探测结束点(官方示例见 1.3 MATLAB 章节)。
+
+### 2.6 几何自定义(Adaptive Templates)
+
+- 版本要求:Motor-CAD ≥ 2024 R1 Update(v2024.1.2)且 PyMotorCAD ≥ 0.4.1;Motor-CAD 内置 PyMotorCAD 可通过 Scripting → Settings → PyMotorCAD updates 升级。
+- 关键约束:Region 的实体(Line/Arc)必须**逆时针**顺序且闭合,否则几何/FEA 计算会失败(先用 `Region.is_closed()` 检查);修改几何前先 `mc.reset_adaptive_geometry()`。
+- 调试工具:`geometry_drawing.draw_objects()` 可在脚本里画出区域用于可视化调试;建议在外部 IDE(PyCharm/VSCode)+ 断点中开发,而不是 Motor-CAD 内置编辑器。
+
+---
+
+## 三、故障处理与常见问题汇总(错误处理机制 + 真实故障案例)
+
+### 3.1 官方错误处理机制(编程设计时必须实现)
+
+1. **`MotorCADError` 异常类型**:PyMotorCAD 与旧 ActiveX 的本质区别——API 调用失败时**直接抛异常**,不再有静默失败的 `success` 返回值。设计仿真系统时应对所有 Motor-CAD 调用做 try/except:
+
+   ```python
+   import ansys.motorcad.core as pymotorcad
+   from ansys.motorcad.core import MotorCADError
+
+   try:
+       mc.do_magnetic_calculation()
+   except pymotorcad.MotorCADError as e:
+       print("Calculation failed: " + str(e))
+
+   # 结果导出同样要捕获
+   try:
+       mc.export_results("EMagnetic", "Export_EMag_Results.csv")
+   except pymotorcad.MotorCADError as e:
+       print("Results failed to export due to Motor-CAD Error: " + str(e))
+   ```
+
+   出处:官方 E-Magnetic 基础示例 https://motorcad.docs.pyansys.com/version/stable/examples/basics/emag_basics.html 与向后兼容章节 https://motorcad.docs.pyansys.com/version/stable/user_guide/backwards_compatibility.html
+
+2. **读图数据的"越界即结束"惯用法**:读取 graph 点到末尾会抛 `MotorCADError`,官方用 while + try/except 循环作为序列结束判断(MotorCAD API 只暴露最近显示的曲线;曲线名称和数据类型在 Motor-CAD 界面 Help → Graph Viewer 里查)。
+
+3. **`MotorCADCompatibility` 对象**:旧 ActiveX 脚本可几乎不改地运行,方便过渡期对照排查问题。
+
+### 3.2 真实故障案例库(论坛 + GitHub Issues,含现象/原因/对策)
+
+| # | 故障现象 | 环境 | 原因与处理 | 来源 |
+|---|---|---|---|---|
+| 1 | `Failed to connect to Motor-CAD instance: port=58016, Url=http://localhost:58016/jsonrpc`,旧版本脚本突然全部失效 | 同机安装 Motor-CAD 2024.1.3 后又装 2024.2.3.1 | **多版本共存导致自动化注册/端口冲突**。检查 Defaults → Automation 里注册的版本;重装/修复安装使注册版本与实际调用版本一致;换到未混装的机器正常即印证是安装态问题 | Ansys 社区 https://discuss.ansys.com/discussion/4499/python-failed-to-connect-to-motor-cad-instance |
+| 2 | `pymotorcad.MotorCAD()` 无法启动新实例 | Windows,Motor-CAD 设置里勾选了 "Hide command Window" | `activex.bat`(`C:\Ansys_Motor-CAD\Shared Files\`)内容格式变化导致 `_find_motor_cad_exe()` 找不到 exe。属已记录的已知 bug,升级 PyMotorCAD 或避免该选项 | GitHub Issue #140 https://github.com/ansys/pymotorcad/issues/140 |
+| 3 | MATLAB `actxserver('motorcad.AppAutomation')` 报 "Server Creation Failed" | MATLAB + Motor-CAD(曾经可用,后失效) | ActiveX/COM 注册问题,多与安装/重装相关。对策:重新注册(Motor-CAD 首次安装后需**重启电脑**完成 ActiveX 注册)、检查注册表 ProgID、DCOM 配置与权限;Ansys 员工建议参考 MathWorks 对同类 COM 错误的 8 种修复方案;更根本的办法是迁移到 PyMotorCAD | Ansys 论坛 https://innovationspace.ansys.com/forum/forums/topic/problems-with-motorcad-automation/ ;GitHub Issue #495 https://github.com/ansys/pymotorcad/issues/495 |
+| 4 | Python `win32com.client.Dispatch("MotorCAD.AppAutomation")` 报 `pywintypes.com_error: (-2146959355, '服务器运行失败')` | 中文用户,集成到自研软件时失败(单独运行正常) | DCOM 配置/权限/防火墙问题。对策:`dcomcnfg` 中配置 Motor-CAD Application Automation 的安全权限、以管理员运行、检查防火墙、确认 ProgID 拼写与安装路径 | 阿里云开发者社区 https://developer.aliyun.com/ask/551036 |
+| 5 | Motor-CAD 报 "Unable to run FE module" | 模型本身无问题(官方复现可算) | 环境/权限问题。对策三步:跑默认模板模型排除模型问题 → **以管理员身份运行 Motor-CAD**(本案例即此解决)→ 确认装在默认目录 `C:\ANSYS_Motor-CAD`,否则重装到默认路径 | Ansys 论坛 https://innovationspace.ansys.com/forum/forums/topic/ansys-motor-cad-unable-to-run-fe-module/ |
+| 6 | 自适应模板旋转转子极区域后 FEA 求解器识别不了绕组区域 | Motor-CAD 2025 R1 + PyMotorCAD 0.7 | 旋转后绕组区域被移到 template-other 节点下,区域树关系丢失。属 adaptive geometry 已知问题,跟踪 Issue | GitHub Issue #473 https://github.com/ansys/pymotorcad/issues/473 |
+| 7 | 模型求解报几何错误,曾正常 | — | **重复的转子几何区域**(自定义几何/FEA Editor 脚本编辑产生)。删除重复区域即恢复。启示:脚本改几何后要做区域树校验 | Ansys 论坛 https://innovationspace.ansys.com/forum/forums/topic/ansys-motorcad-2/ |
+| 8 | 启动时"无法获取许可证" | — | 检查许可证服务器网络连通性(ping)、许可证文件路径与有效期、Ansys License Manager 服务重启、防火墙放行 Ansys 许可端口、许可数量是否占满 | CSDN 文库 https://wenku.csdn.net/answer/7d9nmw0zdbw5 |
+| 9 | 旧 ActiveX 示例中 `MagWindingType` 等参数名失效 | PyMotorCAD 示例与版本演进 | 参数名随版本改名(如 `MagWindingType` → `MagneticWindingType`)。设计系统时参数名应做成可配置映射表,勿硬编码 | GitHub Issue #319 https://github.com/ansys/pymotorcad/issues/319 |
+
+### 3.3 从案例中提炼的排障清单(建议写入仿真系统的自检模块)
+
+1. **连接层**:`pymotorcad.MotorCAD()` 失败 → 检查是否同机混装多版本、Automation 注册版本(Defaults → Automation)、端口被占用/防火墙拦截 localhost、是否勾选了 "Hide command Window"。
+2. **权限层**:COM/FE 模块类错误 → 管理员身份运行、默认路径 `C:\ANSYS_Motor-CAD` 安装、首次安装后重启完成 ActiveX 注册。
+3. **许可层**:拿不到 license → License Manager 服务、端口、文件有效期、并发数。
+4. **模型层**:几何类失败 → 区域闭合性与逆时针顺序(`is_closed()`)、重复区域、Adaptive 脚本先 `reset_adaptive_geometry()`。
+5. **脚本层**:所有 API 调用包 try/except `MotorCADError`;参数名做版本适配;批处理前关弹窗(`MessageDisplayState=2`)并在 finally 中恢复;结果导出校验文件实际生成。
+6. **日志层**:内部脚本用 `mc.show_message()` 写入 Motor-CAD 消息窗口(带时间戳,天然形成运行日志);外部脚本自行记录每次 set/calc/get 的参数与返回,便于复现。
+
+---
+
+## 四、GitHub 上的 Motor-CAD 自动化项目/工具盘点
+
+> 结论先说:**GitHub 上真正可用、持续维护的 Motor-CAD 自动化项目基本只有 Ansys 官方的 `ansys/pymotorcad`**;没有形成规模的第三方"Motor-CAD 自动化 Skill/框架"。检索 `motor-cad`、`pymotorcad` 两个 topic 均显示"尚无公开仓库使用",第三方内容以零散示例为主,且搜索中出现的"破解版/激活版"仓库(如 `Alez1704/ansys-motorcad-15-2-2-unlocked-edition`)**为盗版资源,务必避开,不要引入任何工程环境**。
+
+| 项目 | 地址 | 性质 | 说明 |
+|---|---|---|---|
+| **ansys/pymotorcad** | https://github.com/ansys/pymotorcad | 官方、MIT、持续维护(~30 star,2026 年仍在活跃发版) | 核心库 + `examples/` 完整样例(基础、内部脚本、自适应模板库、Twin Builder/Motion 耦合、参数扫描)。**做仿真系统的首选参考实现**:源码里 `rpc_client_core.py` 展示了实例发现、连接、错误封装;issues/PR 是最好的故障案例库 |
+| DeepWiki 对 ansys/pymotorcad 的结构化解读 | https://deepwiki.com/ansys/pymotorcad/5.1-adaptive-templates | 第三方(AI 生成的代码库导读) | 适合快速理解代码架构:几何对象体系、自适应参数、内外脚本执行上下文(`is_running_in_internal_scripting()` 分支) |
+| Motor-CAD 热模型导入 Simulink/Simscape 示例 | GitHub 搜索 "motorcad" 可见(mathworks 相关仓库) | 组织分享 | Motor-CAD Thermal → Simulink/Simscape 的模型导入示例,做系统级联合仿真可参考 |
+| 零散的 "motorCAD automation" 个人仓库 | GitHub 搜索可得(个位数 star) | 个人分享 | 质量参差,仅作灵感参考 |
+| Ansys Innovation Space 并行计算课程配套脚本 | https://innovationspace.ansys.com/certifications/courses/running-parallel-ansys-motor-cad-calculations-through-scripting/ | 官方 | Python multiprocessing 并行驱动多 Motor-CAD 实例的完整示例(任务调度、排队、结果归集) |
+
+如果你的目标是"给仿真系统找一个现成的自动化框架级 Skill"——目前公开生态里没有,可行路线是:**以 `ansys/pymotorcad` 为底座,自行封装一层任务队列 + 异常重试 + 日志的调度层**(并行模式直接参考官方并行课程与 optiSLang 集成方案)。
+
+---
+
+## 五、社区与第三方分享(组织/个人)
+
+- **Ansys 官方社区(两个都要收藏)**
+  - Ansys Developer Discussions(PyMotorCAD 官方答疑区):https://discuss.ansys.com/
+  - Ansys Innovation Space 论坛(Motor-CAD 板块):https://innovationspace.ansys.com/forum/
+- **CADFEM(Ansys 渠道合作伙伴,组织分享)**
+  - 2021 技术日《Scripting and Parallelization for Motor-CAD》PDF:ActiveX 三命令模式、MATLAB/VBS 最小示例、Blackbox 并行化:https://www.cadfem.net/fileadmin/user_upload/05-cadfem-informs/resource-library/2021_siehr_CADFEM_Techday2_scripting_and_parallelization_for_motorcad.pdf
+  - 2023 R1 更新要点(PyMotorCAD 取代 ActiveX 的官方背景说明):https://www.cadfem.net/fileadmin/user_upload/CADFEM_CH/2023/PUB-CADFEM_Update_2023R1_LF_WBNR-FR.pdf
+- **SimuTech Group(Ansys 精英渠道商,组织分享)**:用 Stochos 贝叶斯优化 + PyMotorCAD 做电机设计优化的完整工作流(Evaluator 函数封装模式值得借鉴):
+  https://simutechgroup.com/resources/blog/bayesian-optimization-electric-motor-design-using-stochos-and-motor-cad/
+- **Ansys Developer Blog**:PyMotorCAD Cheat Sheet 发布说明:https://developer.ansys.com/blog/pymotorcad-cheat-sheet
+- **中文社区排障帖**(个人分享,可作线索、注意甄别):阿里云开发者社区 DCOM 排障 https://developer.aliyun.com/ask/551036 ;CSDN 文库许可证问题 https://wenku.csdn.net/answer/7d9nmw0zdbw5
+
+---
+
+## 六、给仿真系统编程设计的建议(基于以上资料)
+
+1. **接口选型**:新系统一律基于 PyMotorCAD(JSON-RPC),不要再写 ActiveX;需要兼容存量脚本时用 `MotorCADCompatibility`。
+2. **架构参考**:`ansys/pymotorcad` 源码本身是最好的"仿真软件自动化封装"参考——单类 API、异常即错误的语义、内外脚本双上下文判断(`is_running_in_internal_scripting()`)、实例生命周期参数(`open_new_instance` / `keep_instance_open`)。
+3. **健壮性设计**:全链路 try/except `MotorCADError` + 重试;弹窗状态用 try/finally 恢复;参数名做版本映射表;每个任务落盘运行日志(时间戳 + 输入参数 + 结果/异常)。
+4. **并行与调度**:参考官方并行课程用 Python multiprocessing 驱动多实例;批量任务前确保许可数量(每实例占用 license)并规划 BlackBox 无头模式。
+5. **自检模块**:按 3.3 的五层清单(连接/权限/许可/模型/脚本)做启动自检,可显著降低现场排障成本。
+6. **合规提醒**:PyMotorCAD 是 MIT 开源,但驱动 Motor-CAD 必须有合法授权;GitHub 上的"激活版/破解版"仓库一律不要碰。

+ 427 - 0
PCB轴向磁通电机Motor-CAD仿真策略评审与实施建议.md

@@ -0,0 +1,427 @@
+# PCB轴向磁通电机自动仿真系统 — Motor-CAD仿真策略评审与实施建议
+
+> **文档性质**:评审建议稿(供方案系统与执行程序开发评审使用)
+> **版本**:V1.0
+> **日期**:2026-08-27
+> **评审对象**:《PCB轴向磁通电机自动化仿真系统设计方案介绍》V1.1(2026-08-26,Car.Lin)
+> **关键词**:多保真度 · 可行性优先 · 批量自适应闭环 · Motor-CAD · Maxwell/JMAG · PCB轴向磁通电机
+
+---
+
+## 文档信息
+
+| 项目 | 内容 |
+| --- | --- |
+| 文档目的 | 对现有自动仿真系统方案进行技术评审,明确仿真策略、工具职责边界、接口改造和开发优先级。 |
+| 目标读者 | 方案系统开发工程师、本地执行程序开发工程师、电磁/热/结构仿真工程师、项目负责人。 |
+| 评审范围 | 仿真路径、参数搜索策略、Motor-CAD与Maxwell/JMAG协同方式、JSON接口、收敛判据、执行模式与风险控制。 |
+| 评审结论 | 总体架构方向正确,建议有条件通过;仿真策略和接口契约需完成本文所列P0修改后再进入下一阶段开发。 |
+| 核心原则 | Motor-CAD用于快速收敛主要参数并做初步验证;Maxwell或JMAG用于PCB绕组和三维轴向效应的高保真校核。 |
+
+---
+
+## 一、评审结论与修改优先级
+
+> **评审结论**
+>
+> 现有方案的双系统解耦、标准化JSON接口、人在回路、断点续跑和知识沉淀机制均应保留。需要调整的核心不是系统架构,而是仿真策略:当前方案偏向固定批量DoE和代理模型全局优化,尚不足以保证在给定边界条件下快速找到工程可行解,也没有充分约束Motor-CAD近似PCB模型与Maxwell/JMAG高保真模型之间的职责边界。
+
+建议将总体评审结论定为"有条件通过":系统架构和阶段规划可继续推进,但在进入算法增强和工具扩展开发前,必须完成本文列出的P0修改。尤其应把仿真策略从单一"参数扫描方案"升级为"多保真度模型路径+可行性优先搜索+批量自适应闭环"。
+
+结合已确认的工程事实,Motor-CAD只能以近似方式表达PCB绕组,但足以承担主要参数快速收敛、关键工况筛选和初步验证;Maxwell或JMAG必须承担PCB局部电流分布、交流损耗、三维轴向磁路和最终性能的高保真校核。**任何只经过Motor-CAD计算而未经过高保真复核的方案,不应被标记为最终可行设计。**
+
+### 1.1 必须修改项(P0)
+
+1. 在方案生成前增加L0解析与规则预筛选,先排除几何、电气、热和制造上明显不可行的区域。
+2. 把"扫描策略"拆分为"搜索策略"和"保真度策略",避免把所有问题都表达成固定参数队列。
+3. 新增可行性优先模式,默认采用约束贝叶斯、主动学习或信任域局部搜索;LHS+Kriging+NSGA-II保留为全局Pareto模式。
+4. 明确Motor-CAD、Maxwell/JMAG的分层职责和升级条件,建立低保真—高保真偏差校准机制。
+5. 将收敛判据拆分为求解器收敛、硬约束收敛、优化收敛、代理模型可信、高保真一致性和鲁棒性六类。
+6. 扩展结果数据模型,记录模型版本、保真度等级、求解设置、约束裕量、校准系数和置信等级。
+
+### 1.2 建议保留项
+
+| 原方案设计 | 评审意见 | 处理建议 |
+| --- | --- | --- |
+| 双系统解耦 | 方向正确,有利于Web端与本地执行环境独立迭代。 | 保留,并增加批量自适应通信模式。 |
+| 标准化JSON契约 | 是系统可扩展的核心。 | 保留,但必须扩展搜索、保真度、收敛和校准字段。 |
+| 人在回路 | 适合仿真方案审核和知识入库审核。 | 保留;不要把所有人工确认都放在每一次选点上,否则会破坏闭环效率。 |
+| 结果校验规则 | 非常必要,是结果可信度的第一道防线。 | 保留,并增加跨工具一致性和模型置信度规则。 |
+| 断点续跑与错误恢复 | 符合长时仿真场景。 | 保留,并改为基于参数哈希和结果完整性的缓存机制。 |
+| 知识自动沉淀 | 具备长期价值。 | 保留,但沉淀内容必须绑定模型版本和保真度,避免错误经验扩散。 |
+
+### 1.3 一句话给开发团队
+
+> **实施导向**
+>
+> 不要先开发一个"能跑很多参数组合"的执行器,再反过来思考如何收敛;应先定义什么叫可行、什么叫收敛、什么结果必须由Maxwell/JMAG确认,再让执行器围绕这些判据选择下一批仿真点。
+
+---
+
+## 二、现有方案的专家评审
+
+### 2.1 总体判断
+
+附件中的V1.1方案已经具备较完整的平台思维,特别是经验库、规则引擎、AI推理、人工确认、执行程序、结果校验和知识库的闭环设计。其主要短板在于:把"优化算法流程"当成了完整的"仿真路径",而没有把模型可信度、工具职责、PCB建模近似和跨工具校准作为一等公民。
+
+对PCB轴向磁通电机而言,仿真路径的有效性不仅取决于Morris、LHS、Kriging或NSGA-II,还取决于每个参数组合究竟用什么模型计算、计算到哪个精度、在哪些工况下复核、什么时候必须升级到3D FEA。**Motor-CAD与Maxwell/JMAG不是替代关系,而是分层协同关系。**
+
+### 2.2 当前方案的优点
+
+- **系统边界清楚**:方案生成与执行程序通过JSON解耦,符合企业内网和本地仿真环境的实际约束。
+- **工程可控性较强**:方案预览、人工确认、日志、断点续跑和错误恢复机制均符合仿真工程师工作习惯。
+- **工具扩展方向合理**:先Motor-CAD,后Maxwell、JMAG、Flux,符合风险和成本递增的实施顺序。
+- **结果数据结构已有雏形**:输入、输出、校验状态、耗时和错误信息均已考虑。
+- **算法基础方向正确**:Morris、LHS、Kriging和NSGA-II均是电机代理优化中常见且可解释的组合。
+
+### 2.3 关键问题诊断
+
+| 问题 | 影响 | 严重度 | 专家建议 |
+| --- | --- | --- | --- |
+| 把固定DoE流程作为默认主路径 | 前期会浪费大量仿真预算在非可行区域,不能保证快速收敛到可行解。 | 高 | 默认改为可行性优先的约束自适应搜索;固定DoE仅用于全局探索。 |
+| 缺少保真度分级 | 无法判断Motor-CAD结果能否作为最终依据,也无法控制Maxwell/JMAG调用成本。 | 高 | 建立L0~L4模型层级和升级规则。 |
+| PCB绕组近似边界未固化 | 可能把近似模型误差误认为真实优化收益。 | 高 | 把PCB建模假设、近似参数和校准系数写入方案与结果。 |
+| JSON契约偏静态 | 无法支持主动学习、贝叶斯优化和自适应加点。 | 高 | 增加fixed_plan、batch_adaptive、local_closed_loop三种执行模式。 |
+| 停止条件概念混杂 | 求解器收敛、目标达成、代理模型稳定和跨工具一致被混在一起。 | 中高 | 拆分六类收敛判据并分别记录状态。 |
+| 边界条件不完整 | 容易收敛到仿真可行但工程不可制造的方案。 | 中高 | 补充母线电压、电流限制、冷却边界、PCB工艺、公差和材料温度属性。 |
+| 样本量估算偏粗 | Morris和LHS成本可能被低估,导致排期失真。 | 中 | 按参数维度、轨迹数和并行资源重新估算预算。 |
+
+### 2.4 对算法章节的专项意见
+
+附件第6章的"四阶段分层优化策略"建议改名为**"全局探索模式"**。它适合做设计空间理解、敏感性分析和Pareto前沿,不适合作为所有项目的默认快速求解路径。原因是该流程需要先投入Morris和LHS训练样本,再构建代理模型并执行NSGA-II;若用户目标只是尽快得到一个满足边界条件的方案,大量样本可能集中在远离可行域的位置。
+
+Morris样本量应按 **N = r(k + 1)** 重新估算,其中 k 为参数数、r 为轨迹数。若初筛参数达到8个、轨迹数取10,则需要约90次评估,而不是文档中笼统估计的10~50次。Kriging在维度较高或样本不足时也会迅速失去稳定性,因此应将"参数降维"和"可行域裁剪"前置。
+
+---
+
+## 三、仿真路径方法全景与选择
+
+仿真路径应同时回答两个问题:**下一批参数为什么值得仿真**,以及**这批参数应该用哪个保真度模型计算**。只讨论优化算法而忽略模型层级,会导致路径表面上智能、工程上失真。
+
+### 3.1 方法对比矩阵
+
+| 路径方法 | 适用目标 | 效率 | 主要风险 | 建议定位 |
+| --- | --- | --- | --- | --- |
+| 经验初值+人工扫描 | 模型调试、异常诊断、专家复核 | 低—中 | 依赖个人经验,易陷入局部最优 | 保留为专家模式,不作默认主路径 |
+| 规则+解析预筛选 | 排除明显不可行区域 | 极高 | 解析模型精度有限 | 所有路径的L0入口 |
+| 全因子/网格扫描 | 2~3个变量的局部响应面 | 低 | 维度灾难 | 仅用于最终候选局部复核 |
+| Morris/OAT筛选 | 高维参数初筛 | 高 | 对强交互和多峰问题不完整 | 用于降维,不用于直接定案 |
+| LHS+Kriging+NSGA-II | 全局探索、Pareto前沿 | 中高 | 前期样本成本高,代理模型可能失准 | 作为全局探索模式 |
+| 约束贝叶斯/主动学习 | 快速找到满足约束的可行解 | 高 | 多目标和离散变量实现复杂 | **推荐默认模式** |
+| 直接GA/PSO调用求解器 | 小维度、单次仿真很快的场景 | 低 | 仿真次数失控 | 不建议默认使用 |
+| optiSLang MOP/AMOP | 工业级DoE、代理模型和鲁棒性分析 | 高 | 许可与系统集成成本 | 可作为商业基准或高级后端 |
+| 多保真度+跨工具校准 | 最终工程可信设计 | 综合最高 | 流程与数据管理复杂 | **作为系统主骨架** |
+| 鲁棒性/可靠性优化 | 量产、公差、材料波动 | 中 | 计算预算增加 | 作为最终验收前必经步骤 |
+
+### 3.2 高效路径的选择结论
+
+若目标是"快速得到一个满足边界条件的方案",推荐路径是:**解析预筛选+少量初始样本+约束贝叶斯/主动学习+局部信任域收敛+高保真验证**。该路径把计算预算集中到可行域附近,通常比先完整训练全局代理模型更节省仿真次数。
+
+Ansys官方电机优化资料也将工业流程归纳为灵敏度分析、代理模型、优化和最终验证四个环节;这说明附件的代理优化框架方向正确,但该框架更应被定位为完整设计空间探索,而不是所有项目的唯一默认路径。[1]
+
+对于PCB轴向磁通电机,2026年发表的双转子PCB-AH-PMSM研究采用了LHS采样、全局灵敏度分析、Kriging响应面、PSO多目标优化和FEA对比验证,说明LHS+代理模型+智能优化在该类电机上具有直接可参考性。但该论文目标偏向多目标性能改进,并不等同于本系统所需的快速可行性收敛。[2]
+
+贝叶斯优化的优势在于用少量初始点建立概率代理模型,通过采集函数平衡"利用已知优区"和"探索高不确定区",每次真实仿真后更新模型。该机制非常适合Motor-CAD这类有一定求解成本、又需要快速收敛到可行区域的工程场景。[3]
+
+---
+
+## 四、推荐的多保真度闭环仿真路径
+
+建议将系统的仿真主路径定义为**"多保真度、可行性优先、批量自适应闭环"**。其中,Motor-CAD负责快速收敛主要参数并完成初步验证,Maxwell或JMAG负责对少量候选方案做高保真校核和模型校准。
+
+推荐路径示意(流程结构):
+
+```
+L0 解析与规则预筛选
+   ↓
+L1 Motor-CAD 快速收敛(主要参数 + 关键工况)
+   ↓
+L2 Motor-CAD Lab/Therm/Mech 多物理场复核
+   ↓
+L3 Maxwell/JMAG 高保真校验(仅 Top 1~3 候选)
+
+自适应搜索闭环:
+少量初始样本 → 约束代理模型 → 批量选点执行 → 更新模型 → (循环)
+
+收敛出口:
+硬约束满足 + 优化稳定 + 代理模型可信 + 高保真偏差受控 + 制造扰动可接受
+```
+
+### 4.1 保真度分级
+
+| 层级 | 模型/工具 | 主要任务 | 典型输出 | 是否可定案 |
+| --- | --- | --- | --- | --- |
+| L0 | 解析公式+规则引擎 | 几何、电气、热、制造可行性预筛选 | 可行/不可行、风险项、初始参数范围 | 否 |
+| L1 | Motor-CAD快速电磁模型 | 主要参数收敛、关键工况筛选、初步性能验证 | 转矩、反电势、损耗、轴向力初值 | 否 |
+| L2 | Motor-CAD Lab/Therm/Mech | 候选方案多物理场复核、效率图和温升初评 | 效率图、温升、应力、工况边界 | 仅作工程候选 |
+| L3 | Maxwell 3D / JMAG | PCB绕组、3D磁路、端部效应、局部损耗和轴向力校验 | 高保真电磁性能、损耗、力、场分布 | **是,需通过验收阈值** |
+| L4 | 扰动/公差/样机数据 | 制造鲁棒性和模型持续校准 | 最差工况、概率合格率、校准系数 | 用于最终放行 |
+
+Ansys在2026年的轴向磁通电机工作流中也强调:AFM具有天然三维磁路,完整3D FEA精度高但早期设计成本高,因此建议用Motor-CAD快速2D等效线性模型进行概念设计和拓扑筛选,再用Maxwell 3D进行验证、校准和效率图生成。这一官方路径与本文建议一致。[4]
+
+Motor-CAD本身包含EM、Therm、Lab、Mech四个集成模块,适合快速多物理场迭代;2026 R1还增强了AFM热模块、Lab支持、Maxwell-Lab联动、NVH力输出和多静态损耗分析等能力,但部分AFM能力仍属于Beta,需要在项目中做稳定性验证后再固化到默认流程。[5]
+
+### 4.2 三种运行模式
+
+| 模式 | 触发条件 | 核心算法 | 典型预算 | 输出 |
+| --- | --- | --- | --- | --- |
+| 快速可行模式(默认) | 用户要求尽快获得满足边界条件的方案 | L0预筛选+少量LHS/历史种子+约束贝叶斯/主动学习 | 低到中等 | 1~3个可行候选及置信度 |
+| 全局探索模式 | 需要比较效率、功率密度、成本等权衡 | Morris+LHS+Kriging/MOP+NSGA-II | 中到高 | Pareto前沿和参数敏感性 |
+| 高保真校核模式 | 候选方案进入设计冻结或工程评审 | Motor-CAD L2复核+Maxwell/JMAG L3验证+公差扰动 | 集中在少数候选 | 最终验证报告和校准系数 |
+
+### 4.3 默认快速可行模式(八步)
+
+1. **边界条件标准化**:统一单位、工况、温度、约束类型和目标方向,明确硬约束与软目标。
+2. **L0解析预筛选**:根据轴向长度、内外径、气隙、电流密度、PCB工艺和冷却能力裁剪可行域。
+3. **生成初始种子**:优先使用相似案例和专家模板,再用少量LHS补足空间覆盖。
+4. **Motor-CAD快速电磁计算**:仅计算额定、峰值、最高转速和热边界等关键工况,不做全效率图。
+5. **建立可行性概率模型**:模型同时预测性能均值、约束违反概率和不确定性。
+6. **批量自适应选点**:每轮推荐4~8个仿真点,优先选择可行概率高且信息增益大的组合。
+7. **局部信任域收敛**:发现稳定可行区后,在当前最优点附近做小范围精细搜索。
+8. **高保真复核**:对1~3个候选方案进入Motor-CAD L2和Maxwell/JMAG L3验证。
+
+### 4.4 全局Pareto模式
+
+当用户明确要求比较多目标权衡时,才进入Morris、LHS、Kriging和NSGA-II组成的完整全局探索模式。该模式不应以"找到第一个可行点"为停止条件,而应以代理模型质量、Pareto前沿稳定性和高保真复核一致性作为收敛依据。
+
+建议把Pareto模式的结果定位为**"设计空间地图"**,而不是直接输出唯一最优解。最终推荐方案仍需回到高保真校核模式,由Maxwell/JMAG和制造扰动验证。
+
+### 4.5 高保真验证与校准
+
+Motor-CAD和Maxwell/JMAG之间必须建立显式校准关系。至少应记录每个候选方案在相同工况下的转矩、反电势、铜损、铁损、磁钢涡流损耗、轴向力和效率偏差。初期可采用加法或乘法修正系数;数据积累后可升级为Co-Kriging或多保真代理模型。
+
+**关键警告**:如果Motor-CAD低估了PCB交流铜损或磁钢涡流损耗,而系统仍然把Motor-CAD效率作为最终优化目标,搜索方向会被系统性带偏。因此,高保真校准系数应反哺到代理模型和目标函数,而不是只写在验证报告中。
+
+---
+
+## 五、Motor-CAD与Maxwell/JMAG职责边界
+
+### 5.1 Motor-CAD应承担的任务
+
+- 快速比较SSSR、DRSS、SDSR等拓扑在尺寸边界下的可行性。
+- 收敛主要电磁参数,包括气隙、磁钢厚度、极槽配合、有效半径、等效绕组参数和电流密度。
+- 计算关键工况下的转矩、反电势、损耗、轴向力和效率初值。
+- 在候选缩小后调用Lab、热和机械模块,形成初步多物理场筛查。
+- 为Maxwell/JMAG提供参数化候选、工况范围和需要重点验证的指标。
+
+### 5.2 Maxwell/JMAG必须承担的任务
+
+- PCB走线、多层铜箔、过孔、端部连接和局部电流分布的高保真电磁计算。
+- 三维轴向磁路、边缘效应、漏磁、局部饱和和磁钢涡流损耗校验。
+- 轴向磁拉力、转矩脉动和高阶空间谐波的高保真复核。
+- 最终效率、温升输入和退磁风险的高保真验证。
+- 对Motor-CAD近似模型进行偏差评估和校准系数更新。
+
+### 5.3 工具分工原则
+
+> **原则**
+>
+> Motor-CAD决定"大方向是否对",Maxwell/JMAG决定"工程结果是否真"。前者用于快速收敛主要参数,后者用于确认PCB和三维效应对关键指标的影响。两者缺一不可,但不能在同一层级上重复计算。
+
+### 5.4 PCB绕组建模边界
+
+Motor-CAD中的PCB近似模型必须在模型模板中显式记录等效假设,包括:铜厚、线宽、线距、层数、并联支路、过孔电阻、FR4热导率、绝缘厚度、端部连接方式和交流损耗修正系数。每次仿真结果都应绑定这些假设,否则经验库会把不同等效模型的结果混在一起。
+
+建议为PCB绕组增加独立的**"等效模型字典"**,不要直接把PCB参数伪装成常规圆线绕组参数。经验库检索时,应同时匹配拓扑、功率段和PCB等效模型版本。
+
+---
+
+## 六、系统接口与执行引擎修改建议
+
+### 6.1 JSON Schema必须扩展的字段
+
+现有 `simulation_plan.json` 已能表达固定参数扫描,但不足以表达自适应闭环、多保真度升级和跨工具校准。建议增加以下顶层字段:
+
+| 字段 | 内容 |
+| --- | --- |
+| `strategy_mode` | fast_feasible、pareto_exploration、high_fidelity_validation、robustness_check |
+| `fidelity_strategy` | L0~L4模型层级、升级条件、各层级允许的最大求解成本 |
+| `search_strategy` | 初始采样、主动学习、批量大小、局部信任域、约束处理方式 |
+| `calibration_policy` | Motor-CAD与Maxwell/JMAG偏差记录方式、修正系数、更新规则 |
+| `acceptance_criteria` | 硬约束、目标阈值、代理模型可信度、跨工具偏差和鲁棒性要求 |
+| `parallel_execution` | 并行实例数、模型副本策略、许可证失败处理和任务队列策略 |
+
+### 6.2 执行程序应支持三种模式
+
+| 模式 | 工作方式 | 适用阶段 | 对系统二的要求 |
+| --- | --- | --- | --- |
+| 固定计划模式 | 一次性接收全部仿真点并顺序执行 | Phase 1最小闭环 | 当前设计基本满足 |
+| 批量自适应模式 | 每轮接收4~8个点,执行后回传,方案系统计算下一轮 | Phase 3算法增强 | 需要支持批次ID、增量结果上传和方案版本追踪 |
+| 本地闭环模式 | 执行程序内部运行确定性优化器,根据结果动态选点 | 无人值守或网络受限场景 | 需要嵌入数值优化器;不需要内置LLM |
+
+本地闭环模式并不违背"执行程序无需联网/AI"的原则。这里的AI主要指LLM推理;约束贝叶斯、信任域和NSGA-II属于确定性或随机数值优化算法,可以打包在本地EXE中运行。
+
+### 6.3 并行与缓存
+
+- Motor-CAD可以通过PyMotorCAD由外部Python脚本进行并行计算。Ansys 2026 R1培训资料明确覆盖了Python多进程、任务调度、排队和结果汇总,这意味着系统二不应只设计成单实例串行执行器,而应预留并行任务池。[6]
+- 每个并行任务必须使用独立模型副本和独立工作目录,禁止多个实例同时写同一个 `.mot` 文件。
+- 断点续跑应以"参数哈希+模型版本+工况ID+求解器版本+保真度等级"为缓存键,而不是只检查迭代序号。
+- 结果文件应原子写入,先写临时文件再重命名,避免程序中断后被误判为已完成。
+- 许可证失效、求解器崩溃和求解不收敛应分别进入不同的重试与降级策略。
+
+### 6.4 结果数据模型
+
+每条结果建议增加以下字段:
+
+| 字段 | 内容 |
+| --- | --- |
+| `fidelity_level` | L0~L4 |
+| `model_template_version` | Motor-CAD模板、Maxwell/JMAG模型和PCB等效模型版本 |
+| `solver_settings_hash` | 网格、周期数、求解类型、时间步和对称设置等关键配置摘要 |
+| `constraint_margins` | 各硬约束的绝对裕量和百分比裕量 |
+| `surrogate_prediction` | 代理模型预测值、不确定度和实际值偏差 |
+| `cross_validation` | Motor-CAD与Maxwell/JMAG的同工况偏差 |
+| `confidence_grade` | A/B/C/D置信等级 |
+
+---
+
+## 七、收敛判据与验收标准
+
+### 7.1 六类收敛
+
+| 收敛类型 | 判断对象 | 建议判据 | 输出状态 |
+| --- | --- | --- | --- |
+| 求解器收敛 | 单次Motor-CAD或Maxwell/JMAG求解 | 求解完成、关键结果完整、日志无致命错误 | SOLVER_PASS / SOLVER_FAIL |
+| 硬约束收敛 | 候选方案是否满足边界条件 | 转矩、温度、电压、电流、尺寸、轴向力、成本均满足 | FEASIBLE / INFEASIBLE |
+| 优化收敛 | 搜索过程是否继续产生收益 | 连续若干轮最优目标改进小于阈值,或信任域半径低于下限 | CONVERGED / STALLED |
+| 代理模型可信 | 代理模型是否可用于推荐 | 交叉验证误差和候选点不确定度低于阈值 | MODEL_TRUSTED / MODEL_UNCERTAIN |
+| 跨工具一致 | Motor-CAD与Maxwell/JMAG是否一致 | 同工况关键指标偏差在可接受范围内 | HF_PASS / HF_FAIL |
+| 鲁棒性收敛 | 制造和材料扰动下是否仍满足要求 | 最差工况或指定置信度下仍满足硬约束 | ROBUST / FRAGILE |
+
+### 7.2 初始验收阈值建议
+
+以下阈值仅作为项目初始配置,必须通过首批实际模型和样机数据校准,不能直接固化为行业标准:
+
+| 指标 | Motor-CAD内部初筛 | Motor-CAD vs Maxwell/JMAG | 说明 |
+| --- | --- | --- | --- |
+| 平均转矩 | 满足目标并保留建议裕量 | 建议初始控制在±5%以内 | 若偏差系统性存在,应建立修正系数 |
+| 效率 | 满足目标并记录损耗分解 | 建议初始控制在±0.5~1.0个百分点 | PCB交流损耗是重点风险 |
+| 最高温度 | 低于限值并保留热裕量 | 建议初始控制在±5~10 ℃ | 热模型需绑定冷却边界 |
+| 轴向力 | 识别方向与量级 | 建议初始控制在±10%以内 | SSSR和装配偏心场景需重点验证 |
+| 转矩脉动 | 作为筛选指标 | 按项目目标单独定义 | 对控制、NVH和PCB局部效应敏感 |
+
+### 7.3 结果置信等级
+
+| 等级 | 含义 | 允许的用途 |
+| --- | --- | --- |
+| A | 完成Motor-CAD多物理场、Maxwell/JMAG高保真和扰动复核,关键指标均满足。 | 设计冻结、工程评审、样机投入 |
+| B | 完成Motor-CAD和至少一次高保真复核,主要指标一致,鲁棒性待补充。 | 方案候选、供应商沟通、详细设计输入 |
+| C | 只完成Motor-CAD初步验证,未完成Maxwell/JMAG校核。 | 参数筛选、内部讨论,不可对外承诺 |
+| D | 只完成解析或代理模型预测,缺少真实仿真。 | 方案生成参考,不可作为工程结论 |
+
+---
+
+## 八、开发实施路线与验证用例
+
+### 8.1 建议开发优先级
+
+| 优先级 | 开发内容 | 完成判据 |
+| --- | --- | --- |
+| P0 | 扩展JSON Schema;增加L0规则预筛选;增加保真度等级;增加约束裕量和结果置信等级。 | 同一个SSSR案例可按新Schema完整执行并回传结构化结果。 |
+| P0 | 将固定参数队列执行器升级为支持批次ID和增量结果的执行器。 | 中断后重启不重复计算;结果可追溯模型版本。 |
+| P1 | 实现快速可行模式:少量初始样本+约束贝叶斯/主动学习+批量下发。 | 在相同预算下比固定LHS更快找到可行点。 |
+| P1 | 接入Motor-CAD Lab/Therm关键工况复核。 | 候选方案能输出效率图摘要和温升结果。 |
+| P2 | 实现Maxwell或JMAG高保真适配器和偏差校准表。 | 同一候选方案可自动对比低/高保真关键指标。 |
+| P2 | 支持Motor-CAD多实例并行和缓存键。 | 并行结果与串行结果一致,且无模型文件污染。 |
+| P3 | 加入制造扰动和鲁棒性评估。 | 输出最差工况和A/B/C/D置信等级。 |
+
+### 8.2 最小验证用例
+
+建议使用现有SSSR基准模型做三组对比验证:
+
+- **路径A**:固定LHS+Kriging+NSGA-II,验证现有第6章流程。
+- **路径B**:L0预筛选+约束贝叶斯/主动学习,验证快速可行模式。
+- **路径C**:路径B选出的Top 1~3候选进入Maxwell/JMAG,验证跨工具一致性和校准机制。
+
+对比指标不应只包括最终性能,还应包括:总仿真次数、墙钟时间、可行点出现时间、代理模型误差、高保真偏差和工程师人工干预次数。
+
+### 8.3 开发交付物
+
+1. 《仿真策略与保真度分级规范》
+2. 《simulation_plan.json Schema V1.1》
+3. 《simulation_results.json Schema V1.1》
+4. Motor-CAD适配器V0.2和并行任务池
+5. Maxwell/JMAG高保真校核适配器原型
+6. 三条路径的对比验证报告
+7. PCB等效模型字典和跨工具校准表模板
+
+---
+
+## 九、主要风险与待确认问题
+
+### 9.1 主要风险
+
+| 风险 | 表现 | 等级 | 控制措施 |
+| --- | --- | --- | --- |
+| PCB近似模型系统性偏差 | Motor-CAD效率、温升或损耗持续偏离Maxwell/JMAG。 | 高 | 建立校准系数;关键指标必须高保真复核;偏差反哺代理模型。 |
+| 轴向磁通3D效应 | 漏磁、边缘效应和局部饱和导致转矩或轴向力偏差。 | 高 | Motor-CAD只用于筛选;最终候选必须做3D FEA。 |
+| 自适应算法不可解释 | 工程师不理解为什么推荐某个点。 | 中高 | 每个推荐点输出来源、预测值、不确定度和约束概率。 |
+| 经验库冷启动污染 | 不同模型版本或错误结果被当成可复用经验。 | 中高 | 所有经验绑定模型版本、保真度和置信等级;专家审核后入库。 |
+| 并行许可和文件冲突 | 多实例启动失败、模型文件互相覆盖。 | 中 | 独立工作目录、模型副本、许可检测和任务队列。 |
+| 优化目标定义不完整 | 收敛到名义最优但工程不可制造。 | 高 | 补齐母线电压、电流、冷却、公差、PCB工艺和材料边界。 |
+| 停止条件过松 | 系统把第一个可行点误认为全局可靠解。 | 中 | 拆分收敛类型,增加置信等级和鲁棒性出口。 |
+
+### 9.2 开发前必须确认的问题
+
+1. PCB电机结构究竟是无铁芯PCB定子、PCB绕组+铁芯,还是混合结构?
+2. SSSR、DRSS、SDSR三类拓扑是否都需要支持Halbach、背铁和分段磁钢?
+3. Motor-CAD当前模板对PCB铜厚、层数、过孔、端部和FR4热属性的近似方式是什么?
+4. Maxwell和JMAG哪一个作为第一优先级高保真工具?两者的模型转换和结果映射由谁负责?
+5. 默认业务目标是快速可行、完整Pareto,还是量产鲁棒设计?
+6. 有几套Motor-CAD/Maxwell/JMAG许可?是否允许无GUI和多实例并行?
+7. 额定、峰值和高效区工况的持续时间、温度边界和电流限制如何定义?
+8. 气隙、PCB线宽线距、铜厚、磁钢Br和装配偏心的制造公差是多少?
+9. 最终验收阈值由哪个专家团队签发?是否已有样机或测试数据可用于初始校准?
+10. 系统二是否允许内置数值优化器?如果可以,批量自适应模式和本地闭环模式的边界需要重新定义。
+
+---
+
+## 十、附录
+
+### 10.1 推荐JSON扩展示意
+
+```json
+{
+  "strategy_mode": "fast_feasible",
+  "fidelity_strategy": {
+    "levels": ["L0_analytic", "L1_motorcad_emag", "L2_motorcad_lab_therm", "L3_maxwell_3d"],
+    "upgrade_rule": "top_candidates_only",
+    "max_candidates_for_l3": 3
+  },
+  "search_strategy": {
+    "method": "constrained_bayesian",
+    "initial_samples": 16,
+    "batch_size": 4,
+    "max_solver_calls": 80,
+    "local_trust_region": true
+  },
+  "acceptance_criteria": {
+    "hard_constraints": ["torque_nm >= 10", "temperature_c <= 120"],
+    "cross_tool_tolerance": { "torque_pct": 5, "efficiency_point": 1.0 },
+    "surrogate_max_uncertainty": 0.05,
+    "robustness_required": true
+  }
+}
+```
+
+### 10.2 术语
+
+| 术语 | 说明 |
+| --- | --- |
+| 可行性优先 | 先让候选方案满足硬约束,再追求性能最优。 |
+| 多保真度 | 用不同成本和精度的模型分层完成筛选、优化、验证和校准。 |
+| 主动学习 | 根据当前代理模型的不确定性和预期收益,动态选择下一批仿真点。 |
+| 约束贝叶斯优化 | 在贝叶斯优化中显式建模硬约束满足概率,优先搜索可行区域。 |
+| 信任域 | 在当前最优点附近限制搜索范围,逐步扩大或收缩,用于局部稳定收敛。 |
+| 跨工具校准 | 比较Motor-CAD与Maxwell/JMAG在相同工况下的结果差异,并把差异用于修正后续预测。 |
+| 置信等级 | 按验证深度把结果分为A/B/C/D,避免低保真结果被误用为最终结论。 |
+
+### 10.3 参考资料
+
+1. Ansys, *How to Efficiently Optimize Electric Motor Design* — https://ansys.synopsys.com/blog/how-to-efficiently-optimize-electric-motor-design
+2. 《基于Kriging-PSO算法的双转子PCB轴向磁通电机优化设计与分析》,电机工程学报,DOI: 10.11985/JEE.260677
+3. SimuTech Group, *Bayesian Optimization for Electric Motor Design Using Stochos and Ansys Motor-CAD*, 2026 — https://simutechgroup.com/resources/blog/bayesian-optimization-electric-motor-design-using-stochos-and-motor-cad/
+4. Ansys Innovation Space, *Accelerating Multiphysics Optimization Workflows of Axial Flux Motor Design*, 2026 — https://innovationspace.ansys.com/product/accelerating-multiphysics-optimization-workflows-of-axial-flux-motor-design/
+5. Ansys Motor-CAD 产品页(2026 R1 轴向磁通、热、Lab、Maxwell联动与多物理场能力说明)— https://www.ansys.com/products/electronics/ansys-motor-cad
+6. Ansys Innovation Space, *Running Parallel Ansys Motor-CAD Calculations Through Scripting*, 2026 R1 — https://innovationspace.ansys.com/certifications/courses/running-parallel-ansys-motor-cad-calculations-through-scripting/
+7. Ansys, *New Adaptive Templates in Ansys Motor-CAD Make Motor Design Faster, Easier, and More Scalable* — https://www.ansys.com/blog/new-adaptive-templates-ansys-motor-cad-make-motor-design-faster-easier-more-scalable

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

@@ -0,0 +1,1003 @@
+# PCB轴向磁通电机自动化仿真系统设计方案介绍
+
+| 项目 | 内容 |
+|---|---|
+| 文档版本 | V2.0(实现现状同步) |
+| 作者 | Car.Lin |
+| 编制日期 | 2026-08-26 |
+| 文档状态 | 已实施同步(2026-08-29,P1~P3 平台化落地) |
+| 参考原型 | axial_mag_pull(Motor-CAD 轴向磁拉力仿真项目) |
+| 参考原型 | 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模型;执行程序目标用户为仿真工程师;经验库冷启动策略调整为运行中自动积累;仿真耗时基准按现有案例参考、后续日志自动修正。
+
+
+---
+
+## 附录 B:实现现状对照(V2 增补,2026-08-29)
+
+> 本附录由 V1.1 设计蓝图出发,对照 2026-08-29 已实施的 P1~P3 平台化批次(及 P4 进行中项),
+> 如实记录"设计与实现"的差距,消除文档漂移。设计蓝图章节保持原文不变,以下为增量对照。
+
+### B.1 版本记录
+
+| 版本 | 日期 | 内容 |
+|---|---|---|
+| V1.1 | 2026-08-26 | 设计蓝图初稿(架构/接口/算法选型/路线图) |
+| V2.0 | 2026-08-29 | 增补实现现状对照:afmcore 共享核心层、方案 Schema 单一权威、双系统解耦落地、执行策略实际实现、adaptive 闭环、可靠性增强、已知限制 |
+
+### B.2 共享核心层(src/afmcore)
+
+V1.1 设想"通用可扩展平台";P1~P3 已把可复用能力沉淀到共享核心层,Web 端与本地 EXE 双端接入:
+
+| 模块 | 职责 | 实现状态 |
+|---|---|---|
+| `src/afmcore/metrics.py` | 25 项指标定义 + 归一化解析器(单一事实源) | ✅ 已实施(P1) |
+| `src/afmcore/topology.py` | 拓扑注册表(SSSR 8 组 37 项参数体系,DRSS/SDSR 预留) | ✅ 已实施(P2) |
+| `src/afmcore/adapters/` | 仿真适配器抽象(SimulationAdapter 注册表 + MotorCADAdapter/FakeAdapter) | ✅ 已实施(P1) |
+| `src/afmcore/strategies/` | 执行策略注册表(full_factorial / lhs / adaptive 三实现) | ✅ 已实施(P3-M1) |
+| `src/plan_schema.py` | 方案 JSON Schema(dataclass + parse/validate,单一权威) | ✅ 已实施(P4-M1 增强) |
+
+### B.3 方案 Schema 单一权威(P4-M1)
+
+- **权威**:`src/plan_schema.py`(`parse_plan()` / `validate_plan_dict()` / `SimulationPlan.validate(require_model_path=…)`)。
+- **Web 端接入**:`main.py` 注入仓库根路径;`plans.py` 创建/更新、`ai_plan.py` 生成保存均先校验,非法返回 400/422。
+- **别名容错**:扫描变量 `min_value/max_value` 自动映射 `start/stop`,避免字段名不一导致的静默错误点。
+- **分级校验**:draft 阶段不强制 model_path 存在,执行阶段强制。
+
+### B.4 双系统解耦落地(Web 智能层 + 本地执行层)
+
+V1.1 §2/§5 的接口契约已落地为:
+
+```
+Web 端(方案生成/优化)
+   │  POST /api/plans/{id}/start-simulation  → create_task(task_type=scan|adaptive_batch)
+   │  任务状态机:pending → dispatched → running → completed / failed / cancelled
+   ▼
+本地执行器(scripts/task_executor.py,可多实例 --instances N)
+   │  GET /api/tasks?status=pending → 原子认领(dispatch_task 条件更新,恰好一次)→ 执行 → report_results(point_id 回填)
+   ▼
+Web 端(SimulationResult 入库 → 分析 → 经验库)
+```
+
+- 状态词统一由 `src` 侧契约(`task_contract.py`)保证。
+- 执行器心跳/监控:`/api/executor/heartbeat|status|overview`。
+
+### B.5 执行策略:设计蓝图 vs 实际实现
+
+| V1.1 设计蓝图(§6) | 实际实现(P3) | 说明 |
+|---|---|---|
+| Morris 灵敏度筛选 | `full_factorial`(全因子) | 蓝图作为高级目标;实际先提供确定性策略 |
+| LHS 拉丁超立方采样 | `lhs` 策略 | ✅ 已实现 |
+| Kriging 代理模型 + NSGA-II 多目标 | `adaptive`(可行性优先 + active learning + trust region) | 蓝图的高级全局优化管线未全量落地;`adaptive` 以"L0 预筛选 + 初始采样 + 主动学习选批 + 局部 trust region 细化"实现,收敛判断/预算控制完整 |
+| 约 50~110 次仿真/轮 | `max_solver_calls` 预算控制(默认 80) | 口径一致 |
+
+### B.6 Adaptive 闭环链路(P3-M6,已打通)
+
+```
+AI 方案(Kimi,可降级纯定量)→ L0 预筛选 → 初始 LHS 采样
+  → active learning 选批(search.select_next_batch)
+  → submit_batch_to_executor(打包为 adaptive_batch Task,含 loop_id/batch_id/point_ids)
+  → 本地执行器认领/求解(mock 或真实 Motor-CAD)
+  → report_results 回填(point_id 对齐)→ 分析 → 经验库
+  → 收敛 / 预算耗尽 → 结束
+```
+
+### B.7 可靠性增强(P3 收尾,2026-08-29)
+
+- **并发原子认领**:`task_manager.dispatch_task` 改为条件 UPDATE + rowcount 判定,多执行器竞争同一任务恰好一次成功(`test_p3_concurrency.py`)。
+- **断点恢复**:`FeasibilityFirstSearch.import_state()` + `AdaptiveLoop.export_state()/restore_state()` + `GET /loops/{id}/export`、`POST /loops/import`,进程重启后可恢复搜索状态并继续(`test_p3_checkpoint.py`)。
+- 真实 Motor-CAD 烟雾验证:连接→基线加载→求解→解析 21 指标,back_emf=11.15V 与历史一致(TEST-010)。
+
+### B.8 已知限制与后续(P4 进行中)
+
+| 项 | 状态 |
+|---|---|
+| 本地 EXE 打包(PyInstaller,headless 执行器) | P4-M3 进行中 |
+| 前端 adaptive 视图(批次可视化/收敛曲线) | P4-M4 规划中(当前 UI 为全因子展示) |
+| L0 预筛选上提共享核心层 | P4-M5 规划中(当前在 web 端) |
+| 多物理场(L2 热/结构、L3 Maxwell/JMAG) | 接口预留,未接入执行 |
+| 真实 Motor-CAD 依赖 license server(1055@localhost) | 环境依赖,脚本内置检查与降级 |
+| AI 分析依赖 Kimi API key(无 key 自动降级纯定量) | 已实现门控 |
+
+---
+
+*附录 B 为 V2.0 增量;设计蓝图正文保持 V1.1 原样。*

+ 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>

+ 267 - 0
README.md

@@ -0,0 +1,267 @@
+# PCB轴向磁通电机自动化仿真系统
+
+> 双系统解耦的轴向磁通电机自动化仿真平台:Web端方案生成 + 本地EXE仿真执行
+
+| 项目 | 内容 |
+|---|---|
+| 文档版本 | V2.1(P6-M1 完成:前端 UI/UX 重构) |
+| 作者 | Car.Lin |
+| 启动日期 | 2026-08-27 |
+| 当前状态 | P1~P5 全部完成;**P6 前端体验优化进行中**:P6-M1(UI/UX 全面重构)✅;P6-M2(参数目录单一事实源)🔳 待办;P6-M3(流程引导+检查清单+耗时校准)🔳 待办。进度权威来源:[docs/HANDOFF.md](docs/HANDOFF.md) 第 3 节 |
+| 更新日志 | [CHANGELOG.md](CHANGELOG.md)(全部历史更新,本文件只保留摘要) |
+| 设计方案 | [PC轴向磁通电机自动化仿真系统设计方案介绍.md](PCB轴向磁通电机自动化仿真系统设计方案介绍.md)(V2.0,已同步实现现状) |
+| 论文知识库 | [docs/PAPER_KNOWLEDGE_BASE.md](docs/PAPER_KNOWLEDGE_BASE.md)(无铁心PCB-AFPM 38篇论文精读整理,供软件工程师参考) |
+| 上手指南 | [docs/P1-P5交付总结与上手指南.md](docs/P1-P5交付总结与上手指南.md)(P1-P5 全盘核对 + 目录结构 + 快速开始 + 工程纪律 + 测试体系,新人必读) |
+| 远程仓库 | https://gogsgit.ez4l.com/carlin/pcb-afm-simulation-system |
+
+---
+
+## 项目目标
+
+构建一个通用的、可扩展的、双系统解耦的轴向磁通电机自动化仿真平台:
+
+- **系统一(Web端)**:输入边界条件 → 基于经验库+规则+AI生成仿真方案 → 人工确认 → 下发
+- **系统二(本地EXE)**:读取方案 → 驱动Motor-CAD自动仿真 → 输出结果 → 回传
+- **共享核心层 `src/afmcore/`**:指标/拓扑/适配器/策略/L0预筛选的单一事实源,双系统共用
+- **核心闭环**:边界条件 → 方案 → 仿真 → 结果 → 反馈调整 → 经验库积累
+
+---
+
+## 最近更新(摘要)
+
+> 完整历史记录见 [CHANGELOG.md](CHANGELOG.md);里程碑明细见 [docs/P1-P5交付总结与上手指南.md](docs/P1-P5交付总结与上手指南.md)。
+
+| 日期 | 内容 |
+|---|---|
+| 2026-09-04 | **热仿真首次打通**:Motor-CAD 稳态热仿真实测跑通(MARS:电磁 127.8s + 热 6.0s),修正 P5-M6 两个错误 API(`do_thermal_calculation`→`do_steady_state_analysis`、`export_results("Thermal")`→`"SteadyState"`),热指标别名实测登记(绕组 68°C/热点 74.6°C/磁钢 118°C/后轴承 88.5°C);新增 `scripts/run_thermal.py`。⚠ MARS 模型环境温度 `Ambient_Temperature=125` 异常待修正 |
+| 2026-09-03 | **仿真失败根因修复**:MARS 几何变量名实测修正(RotorOuterDiameter/Stator_Lam_Dia/Stator_Bore/Back_Iron_Thickness/PhaseAdvance/材料名 N42UH)+ 生产链路 3 修复(拓扑预检变量集/执行器 dispatch 状态机/适配器导入路径),**多点扫描全链路实测通过**(3 点 Airgap,趋势符合电磁学) |
+| 2026-09-03 | **P6-M2/M3 落地**:BC 字段目录单一事实源(bc_fields + /api/bc-fields + key 统一 normalize_bc)+ 仿真前检查清单 + 耗时校准(实测 solve_time_s)+ 任务自动展开 |
+| 2026-09-03 | **AI 生成链路修复**:Kimi max_tokens 撞顶修复 + topology/strategy 归一化兜底 + AI 生成摘要对话框 + 扫描变量归一化(注册表 8→13,AI 推荐变量不再被误裁) |
+| 2026-09-03 | **UI 评审修复闭环**:统计卡字段名 Bug + Dashboard 结果加载 + BC 空值折叠 + 失败结果错误信息列 + 监控页合并 + 批量清理入口 + 扫描变量编辑持久化 + 列配置持久化 |
+| 2026-09-02 | AI 方案生成 422 修复(前端拓扑枚举 AFIR→SSSR 对齐单一事实源)+ Web 开发环境联调 |
+| 2026-09-01 | AI 协作方法论框架升级(playbook V2)+ 新增 `scripts/check_machine_paths.py` 环境体检 + `docs/HANDOFF.md` |
+| 2026-08-30 | **P6-M1** 前端 UI/UX 全面重构(设计令牌 + 信息架构 + PlanDetail 分层);文档卫生修复;前端 chunk 分包 |
+| 2026-08-30 | **P5-M2 拓扑感知变量名映射**与执行前校验(修复 plan 23 的 80 点全失败)+ variable-catalog API |
+| 2026-08-30 | **P5-M3~M6**:adaptive 三视图 / Morris+IDW 策略 / Maxwell+JMAG 适配器(mock)/ 多物理场 L2 指标(25→35) |
+| 2026-08-30 | **P5-M1~M2**:前端 build 类型错误清零(71→0)/ EXE 配置化 + 真实 Motor-CAD 端到端验证 |
+| 2026-08-29 | 本地执行器真实仿真全链路打通(P2 关键里程碑)+ 方案详情页三栏重构 |
+| 2026-08-29 | 平台化改造五批(afmcore/拓扑注册表/策略层/调度契约/执行桥)+ P3 闭环 + P4 收尾(Schema 统一/EXE 打包/部署) |
+
+---
+
+## 已完成里程碑(概要)
+
+| 阶段 | 内容 | 状态 |
+|---|---|---|
+| Phase 1 | 最小闭环:环境验证 + 扫描引擎 + 方案JSON/GUI + 经验库雏形(MARS-12S10P 气隙扫描验证,物理趋势符合预期) | ✅ |
+| Phase 2 | Web端方案系统(FastAPI + Vue3 + SQLite + 规则引擎 + 边界条件/方案编辑器) | ✅ |
+| Phase 3 | AI驱动智能仿真闭环(Kimi k3 + L0预筛选 + 可行性优先搜索 + 多保真度 + 自适应闭环,32 个 API 端点) | ✅ |
+| Phase 4 | Web AI集成 + 双系统任务闭环 + 批量调度 + 16项鲁棒性 MotorCAD 核心 + 报告生成 + Docker/EXE 部署 | ✅ |
+| P5 | 平台化增强 M1~M6(build清零 / EXE配置化 / adaptive三视图 / Morris+IDW / 多工具适配器 / 多物理场L2) | ✅ |
+| P6-M1 | 前端 UI/UX 全面重构 | ✅ |
+| P6-M2 | BC 参数目录单一事实源(bc_fields + normalize_bc 统一 key 口径) | ✅ |
+| P6-M3 | 流程引导(仿真前检查清单)+ 耗时校准 + 任务自动展开 | ✅ |
+
+**Phase 3 核心能力**:Kimi k3 大模型集成、L0 解析预筛选(14+ 约束检查)、可行性优先搜索(LHS+主动学习+信任域)、多保真度校准(L0→L4)、置信等级 A-D、六类收敛判据、批量自适应闭环。
+
+**Phase 4 核心能力**:前端 10 个 AI 页面、双系统任务闭环(Web创建→执行器轮询→仿真→回传)、批量调度器(优先级/并行/依赖/断点续跑)、增强版 MotorCAD 核心(16 项鲁棒性措施)、Word/JSON 自动报告、Docker + Windows 一键部署。
+
+**代码评审修复**(第三方评审 V1.0 + 二轮,20+ P0 项):数据完整性 / 稳定性 / AI 逻辑 / 前端 / 部署安全 / 纪律六大类,详见 [docs/CODE_REVIEW_RESPONSE.md](docs/CODE_REVIEW_RESPONSE.md)。
+
+逐里程碑计划 vs 实际交付对照表:[docs/P1-P5交付总结与上手指南.md](docs/P1-P5交付总结与上手指南.md) 第 2 节。
+
+---
+
+## 快速开始
+
+### 环境要求
+
+| 项 | 要求 | 验证 |
+|---|---|---|
+| 系统 | 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"` |
+| Node.js | ≥ 18(Web端前端) | `node --version` |
+| 许可证 | FlexNet `ANSYSLMD_LICENSE_FILE=1055@localhost` | ANSYS License Management Center |
+
+**换机/新会话第一步**:`python scripts/check_machine_paths.py`(只读环境体检,缺项给修复建议;`--fix` 打印修复命令)。
+
+### 系统二(本地仿真执行端)
+
+```bash
+# 安装依赖
+pip install ansys-motorcad-core pyside6 pandas
+
+# 方式一(推荐):任务执行器 —— 连接 Web 端,认领并执行仿真任务
+python scripts/run_task_executor.py --config executor_config.json
+#   - enable_mock=true 时不启动 Motor-CAD,可离线验证全链路
+#   - 多实例并行:python scripts/run_task_executor_parallel.py --instances 2
+#   - 配置项(web_base_url / model_path / tool / instances 等)见 executor_config.json
+
+# 方式二(调试):独立脚本
+python scripts/run_single.py                                # 单工况验证
+python scripts/run_scan.py --plan simulation_plan.json      # 参数扫描
+python scripts/run_gui.py                                   # 本地 PySide6 GUI
+```
+
+### 系统一(Web端方案生成)
+
+```bash
+# 后端
+cd web/backend
+pip install -r requirements.txt
+python run.py
+# 后端运行在 http://127.0.0.1:8000,API文档 http://127.0.0.1:8000/docs
+
+# 前端(另开终端)
+cd web/frontend
+npm install
+npm run dev
+# 前端运行在 http://localhost:5173,自动代理 /api 到后端
+
+# 或 Docker 一键部署(P4-M5)
+docker-compose up -d
+```
+
+### Web端核心API(节选)
+
+> 完整 API 以 Swagger(`/docs`)为准——P3 起 AI/搜索/自适应闭环共 32+ 端点,此处仅列常用入口。
+
+| 方法 | 路径 | 用途 |
+|---|---|---|
+| GET | `/api/health` | 健康检查 |
+| GET/POST | `/api/projects` | 项目列表/创建 |
+| GET | `/api/scan-parameters` | 可扫描参数注册表 |
+| GET | `/api/plans/variable-catalog?topology=SSSR` | 拓扑感知变量目录(模板参数 + Motor-CAD 实际变量名) |
+| POST | `/api/generate-plan` | 规则引擎生成方案 |
+| POST | `/api/projects/{id}/generate-plan` | 为项目生成方案 |
+| GET | `/api/plans/{id}/download` | 下载方案(系统二兼容格式) |
+| POST | `/api/plans/{id}/upload-results` | 上传仿真结果CSV |
+| GET | `/api/analytics/metrics` | 指标定义列表(35 项,单一事实源 `src/afmcore/metrics.py`:电磁25+热6+结构4) |
+| GET | `/api/analytics/experience/stats` | 经验库统计(总数/拓扑分布/指标范围) |
+| POST | `/api/analytics/experience/similar` | 相似案例检索(参数距离匹配) |
+| GET | `/api/analytics/plans/{id}/trend` | 参数-指标趋势数据(散点图) |
+| GET | `/api/analytics/plans/{id}/pareto` | Pareto前沿(效率vs损耗) |
+| GET | `/api/analytics/plans/{id}/sensitivity` | 参数敏感性排名(Pearson相关) |
+| GET | `/api/analytics/projects/{id}/overview` | 项目概览统计 |
+| POST | `/api/adaptive/loops/{loop_id}/submit-batch` | 自适应批次下发本地执行器 |
+
+---
+
+## 项目结构
+
+```
+PCB轴向磁通电机自动化仿真系统/
+├── README.md / CHANGELOG.md / AGENTS.md    # 说明 / 历史更新 / AI工作准则
+├── executor_config.json            # 执行器侧车配置(web地址/模型/实例数/工具/mock开关)
+├── deploy.ps1 / Dockerfile / docker-compose.yml / nginx.conf   # 部署
+├── PCB轴向磁通电机自动化仿真系统设计方案介绍.md   # 设计方案 V2.0
+├── ai-collab-dev-playbook-v2.md    # AI 协作方法论框架(可复用)
+├── docs/
+│   ├── HANDOFF.md                  # 接续指南(进度/阻塞/待办/接续提示词,新会话必读)
+│   ├── KNOWLEDGE_BASE.md           # 核心知识库(环境事实/参数语义/坑/SOP)
+│   ├── TEST_RECORDS.md             # 测试记录(TEST-001~024,含索引)
+│   ├── CONVERSATION_LOG.md         # 会话与决策记录
+│   ├── PAPER_KNOWLEDGE_BASE.md     # 无铁心PCB-AFPM论文知识库(38篇)
+│   ├── PLATFORM_DESIGN_V2.md       # 平台化升级设计方案
+│   ├── P1-P5交付总结与上手指南.md   # 里程碑核对 + 新人上手(必读)
+│   ├── CODE_REVIEW_RESPONSE.md     # 代码评审修复记录
+│   ├── 前端界面优化建议_V1.md       # B1~B4 前端优化批次(B3/B4 待办)
+│   └── archive/                    # 已完结历史计划文档(P3/P4实施计划等)
+├── src/                            # 系统二 + 共享核心
+│   ├── afmcore/                    # 共享核心层(单一事实源)
+│   │   ├── metrics.py              # 指标定义 35 项 + 归一化解析器(唯一权威)
+│   │   ├── topology.py             # 拓扑注册表(SSSR 8组37项,DRSS/SDSR预留)
+│   │   ├── adapters/               # 工具适配器(MotorCAD真实 + Maxwell/JMAG mock)
+│   │   ├── strategies/             # 执行策略(full_factorial/lhs/adaptive/morris/surrogate_guided)
+│   │   └── l0/                     # L0 解析预筛选(纯stdlib)
+│   ├── solver_core.py              # 仿真核心(Motor-CAD连接/参数/求解/结果提取)
+│   ├── scan_engine.py              # 参数扫描引擎
+│   ├── plan_schema.py              # 方案JSON Schema(parse/validate单一权威)
+│   ├── experience_db.py            # 经验库(SQLite)
+│   ├── api_client.py               # Web端API客户端(系统二↔系统一通信)
+│   ├── status_constants.py         # 状态常量("OK"/"FAILED"统一)
+│   └── gui/                        # PySide6 GUI
+├── scripts/                        # 33个脚本
+│   ├── run_task_executor.py        # 本地执行器主入口(读executor_config.json)
+│   ├── run_task_executor_parallel.py   # 多实例并行
+│   ├── run_single.py / run_scan.py / run_gui.py    # 调试入口
+│   ├── check_machine_paths.py      # 环境体检(只读,--fix打印修复命令)
+│   ├── build_executable.ps1        # EXE打包(PyInstaller)
+│   └── test_*.py                   # 回归测试(23个,python scripts/test_*.py独立运行)
+├── web/                            # 系统一(Web端)
+│   ├── backend/app/
+│   │   ├── main.py / config.py / database.py
+│   │   ├── models/ / schemas/      # ORM模型 / Pydantic Schema
+│   │   ├── routers/                # 14个路由(projects/plans/tasks/adaptive/ai/ai_plan/
+│   │   │                           #   search/analysis/analytics/executor_monitor/
+│   │   │                           #   experience/generation/monitor/reports)
+│   │   └── services/               # 规则引擎/AI方案生成/自适应闭环/批量调度/
+│   │                               #   报告生成/拓扑变量映射/固定参数模板等
+│   └── frontend/src/               # Vue3 + Element Plus + ECharts
+│       └── views/                  # 9个主视图 + ai/子目录(AI功能页)
+├── models/                         # 基线模型(只读:MARS-12S10P_SSSR .mot 及配套网格/导出文件)
+├── testcase/                       # 测试模型
+├── dist/                           # 打包产物 PCB-AFM-Executor.exe(不入库)
+├── output/                         # 仿真输出(不入库)
+├── experience/                     # 经验库数据(SQLite)
+├── 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. **每阶段完成后更新 README.md**(版本号/状态/路线图)+ CHANGELOG.md
+3. **结果与报告带时间戳+简要说明并提交**;报告版本化不覆盖
+4. **Motor-CAD前台运行**,跑完保持打开供人工检查
+5. **生成物不入库**(output/、*.log、build/、dist/、node_modules/)
+6. **原始.mot只读**,一切修改在时间戳副本上进行
+7. **参数写入后必须回读校验**,不一致标记FAILED
+8. **每个扫描点重新加载基线模型**,防止参数污染
+9. **对话与决策带时间戳记入 docs/CONVERSATION_LOG.md**
+10. **所有 .py / .ps1 源码纯 ASCII**,中文用 Unicode 转义或放 Markdown
+11. **状态信息单一事实源**:当前进度/阻塞/待办只维护 docs/HANDOFF.md 第 3 节,README 只放链接
+12. **粘贴文本入文档后检查控制字符**(0x07/0x08 等会吞字,历史已发生两次)
+
+---
+
+## 阶段路线图
+
+> 阶段编号说明:P4 期间实施的「平台化改造四/五批」与 P4-M1~M5 合并交付(Schema 统一、EXE 打包均已在 P4 完成);P5 为平台化增强 M1~M6;P6 为前端体验优化。逐里程碑明细见 [docs/P1-P5交付总结与上手指南.md](docs/P1-P5交付总结与上手指南.md)。
+
+| 阶段 | 目标 | 状态 |
+|---|---|---|
+| Phase 1(M1~M4) | 最小闭环:环境验证 + 扫描引擎 + 方案JSON/GUI + 经验库雏形 | ✅ 完成 |
+| Phase 2(P2-M1~M5) | Web端方案系统(FastAPI + Vue3 + SQLite + 规则引擎) | ✅ 完成 |
+| Phase 3 | AI驱动智能仿真闭环(Kimi + 可行性优先搜索 + 自适应) | ✅ 完成 |
+| Phase 4 | Web AI集成 + 双系统闭环 + 批量调度 + Schema 统一 + EXE 打包 + 部署 | ✅ 完成 |
+| P5 平台化增强 | M1 前端build清零 / M2 EXE配置化 / M3 adaptive三视图 / M4 Morris+IDW策略 / M5 多工具适配器 / M6 多物理场L2接入 | ✅ 全部完成 |
+| **P6 前端体验优化** | M1 UI/UX全面重构(设计令牌+信息架构+PlanDetail分层) | ✅ 完成 |
+| P6-M2 | B3 参数目录单一事实源(前后端协同) | 🔳 待办 |
+| P6-M3 | B4 流程引导 + 仿真前检查清单 + 耗时校准 | 🔳 待办 |
+| 路线扩展 | 热求解真实验证 + Maxwell/JMAG 真实接入 + Kriging 升级 + DRSS/SDSR 拓扑 | 🔳 规划中 |

+ 610 - 0
ai-collab-dev-playbook-v2.md

@@ -0,0 +1,610 @@
+# AI 协作开发 Playbook(通用框架)
+
+> 一份**通用的、与 AI 协作开发**的方法论与模板。
+> 本文档从两个真实工程复盘提炼,但**主文不绑定任何具体项目**——MARS 电机热流体仿真工程与 PCB 轴向磁通电机自动化仿真系统仅作为「案例来源 / 参考实现」出现在附录,供理解与对照学习。
+> 目标:在任何新项目上,与 AI 协作时能**快速搭好框架、立刻开始干活**,且过程可复现、可交接、可沉淀。
+
+> **命名说明**:本框架把「给 AI 的行为准则」统一命名为 **`AGENTS.md`**(业界通用标准文件名,Claude Code / Codex / Cursor 等主流 AI 工具默认自动读取)。
+
+---
+
+## 版本更新记录
+
+> 本文档会持续迭代。每次改动在此追加一条:**版本号 / 日期 / 改动内容 / 影响**。
+
+| 版本 | 日期 | 改动内容 | 影响 |
+|---|---|---|---|
+| **V1.0** | 2024(MARS 项目沉淀) | 初版:三支柱文件(README/AGENTS/HANDOFF)+ 会话日志 + 规矩前置 + 接口文件化 + 验收量化 + 铁律带"为什么" + 换机接续 + 五步启动法 + 模板库 | 建立基础方法论,从 MARS 电机热仿真工程实战提炼 |
+| **V2.0** | 2026-09-01 | 融合第二个项目(PCB 自动化仿真系统)实战经验:三支柱扩展为**五支柱**(+KNOWLEDGE_BASE + 留痕双件套);新增里程碑管理、测试纪律、工程规范(禁臆测/自查清单/交付声明)、反模式自查表;新增 KNOWLEDGE_BASE / TEST_RECORDS / 里程碑三套模板 | 从"单项目经验"升级为"多项目通用框架" |
+| **V2.1** | 2026-09-01 | **通用化重构**:主文移除所有具体项目描述(路径/参数/工具名),改为通用占位;两个真实项目降级为「案例来源」移入附录;新增本「版本更新记录」 | 文档成为不绑定项目的通用框架,可直接复制到任何新项目 |
+
+> **后续迭代约定**:新增一条版本记录时,写明「版本号 / 日期 / 改动内容 / 影响」四列;若改动较大,可在「改动内容」里分条说明。
+
+---
+
+## 0. 核心结论(30 秒读完)
+
+AI 协作开发的效率,**不取决于 AI 的能力,而取决于工程的秩序**。
+
+从真实项目复盘得出,真正让开发又快又稳的,是五件事:
+
+1. **五支柱文件** —— README(项目全貌)+ AGENTS.md(给所有 AI 的规矩)+ HANDOFF.md(接续指南)+ KNOWLEDGE_BASE.md(环境事实与踩坑)+ 留痕双件套(session_log + TEST_RECORDS)。AI 每次开工读一遍,就能带着全部上下文干活。
+2. **一条日志线** —— 每次对话都写 session_log,把「目标→动作→结论→踩坑→遗留」沉淀为组织记忆;每次测试写 TEST_RECORDS,把「环境→步骤→结果→问题→修复」落成可追溯记录。AI 永远不会重复踩坑。
+3. **规矩前置 + 铁律带"为什么"** —— 开工前把协作约定写成显式规则;每条血泪教训配"为什么",AI 才知道何时严格遵守、何时可以变通。
+4. **验收量化 + 独立验证** —— 每个里程碑都有可复核的数值基准,且**校验方式 ≠ 产出方式**;AI「看似完成」时能立刻被识别。
+5. **里程碑闭环 + 可交接** —— 用 Phase/Milestone 管理进度,每阶段完成即更新 README;HANDOFF + 接续提示词 + 环境体检脚本,让换人/换机/换会话在 10 分钟内接上。
+
+---
+
+## 1. 通用方法论要点(每个要点后附「案例」供对照学习)
+
+### 1.1 五支柱文件:把"上下文"变成"资产"
+
+五个文件分工,让任何新接手者(人类或 AI)能在 10 分钟内进入工作状态:
+
+| 支柱 | 文件 | 写给谁 | 职责 | 关键内容 |
+|---|---|---|---|---|
+| ① 全貌 | **README.md** | 人类/全局 | 项目是什么、怎么跑、做到哪了 | 目标、目录表、工具链、常用命令、约定、最近更新 |
+| ② 规矩 | **AGENTS.md** | 所有 AI(自动读取) | AI 的行为准则 | 工作约定、关键接口、铁律、已知坑、文档索引、反模式自查 |
+| ③ 接续 | **HANDOFF.md** | 接续者 | 换人/换机/续作入口 | 环境要求、恢复步骤、当前进度、阻塞点、接续提示词 |
+| ④ 知识 | **docs/KNOWLEDGE_BASE.md** | 人 + AI | 环境事实、参数语义、探测技术、SOP、已踩的坑 | 环境验证命令、常见问题表、连接/求解 SOP、踩坑清单 |
+| ⑤ 留痕 | **docs/session_log.md + docs/TEST_RECORDS.md** | 组织记忆 | 对话留痕 + 测试留痕 | 目标→动作→结论→踩坑→遗留;环境→步骤→结果→问题→修复 |
+
+**要点**:
+- README 是「静态全貌 + 滚动更新」,HANDOFF 是「动态状态」,AGENTS 是「行为规则」,KNOWLEDGE_BASE 是「环境与坑的事实库」,留痕双件套是「过程记录」。五者分工明确,不要混在一起,否则更新时互相打架。
+- **session_log 与 TEST_RECORDS 必须分离**:session_log 记"对话做了什么、踩了什么坑、遗留什么";TEST_RECORDS 记"测试怎么跑的、结果如何、修了什么"。混淆会导致追溯困难。
+- AGENTS.md 之所以有效,是因为主流 AI 工具在项目目录里工作时**会自动读取它**。你要做的,是把「期望 AI 怎么干活」全部写进去。
+
+> **案例对照**:①—④ 出自两个真实项目的共同实践;⑤「留痕双件套」在第一个项目只做了会话日志,第二个项目补上了 TEST_RECORDS 测试记录,并验证了二者分离的价值。
+
+### 1.2 会话日志 + 测试记录 = 组织记忆
+
+**会话日志**(统一模板):
+
+```
+## YYYY-MM-DD · 第 N 次对话 —— 一句话目标
+### 用户要求(要点)
+### 本次完成(动作 + 结果)
+### 用户纠偏(如有)—— 纠正:为什么,如何修正
+### 遗留问题 / 待确认
+```
+
+**测试记录**(每条含索引 + 详细):
+
+```
+## TEST-XXX:测试主题
+**日期** / **测试环境** / **测试目的** / **测试脚本** / **输出目录**
+### 测试步骤与结果(表格:步骤 | 内容 | 结果 | 详情)
+### 关键数据 / 发现的问题 / 修复措施
+```
+
+**为什么有效**:
+- 对话是易失的,日志是持久的。AI 换会话、换机器后,靠日志重建上下文。
+- 踩过的坑全部沉淀下来,成为后续 AI 的"避坑清单"。
+- 记录本身是一种**校验**:写完日志,等于把这次对话"归档了",可以安心进入下一目标。
+- **强制约束**:每次对话、每次测试都必须留痕,没有例外。
+
+> **案例对照**:两个项目分别沉淀了 20+ 条避坑记录(API 常量、工具特定行为、环境变量陷阱等),验证了「记录即资产」。
+
+### 1.3 规矩前置:把协作摩擦降到零
+
+开工第 1 天就立好的规矩(可裁剪、可扩充):
+
+- 每次测试前先 `git commit`(可回退)
+- 每次对话写 session_log、每次测试写 TEST_RECORDS(可追溯)
+- 文件名只用 ASCII 且有意义(不许 `1111.prt` 这种)
+- 源码(.py/.ps1)只含 ASCII,中文说明写进 Markdown;脚本用英文注释、报告文档可用中文
+- 生成 PPT/PDF 用专门的 skills
+- 有 GUI 的程序必须前台运行;后台 Python 一律加 `-u`(否则看不到进度)
+- 参考目录只读,不许改;原始模型只读(操作在内存或时间戳副本)
+- 特定工具链必须用特定解释器
+- 生成物不入库(output/、build/、dist/、*.log 等),关键数值转录进文档
+
+**要点**:规矩要具体到「AI 能执行」,不要写"注意规范"这种空话。
+
+### 1.4 接口文件化 + 单一事实源:让 AI 的修改可预期
+
+把"关键数据接口"做成文件,把 AI 修改的边界明确框定:
+
+```
+<project>/data/<interface>.csv   ← 改这一个文件,重跑指定步骤,即可换输入
+<project>/src/<core>/<def>.py    ← 指标/拓扑/参数等定义的唯一权威,多端消费
+<project>/config/<xxx>.json      ← 可执行程序侧车配置,改配置无需重新构建
+```
+
+**为什么有效**:AI 修改的边界被明确框定——「只改这个文件,别动其他东西」。可预期的修改 = 可审查的修改 = 可回退的修改。**凡是被多处消费的定义,必须收敛到单一事实源,禁止多处漂移**(否则会出现"改一处漏两处"的返工)。
+
+> **案例对照**:第一个项目用 CSV 接口文件换输入;第二个项目把指标/拓扑/参数 Schema 各归一个权威定义(单一事实源),并因曾有三处定义漂移而返工。
+
+### 1.5 验收基准 + 测试纪律:防止"看似完成"
+
+每个里程碑都留下**量化验收基准**,并立下规矩:**对不上就别往下走**。
+
+**测试纪律(强烈推荐)**:
+- 每段交付代码必须附带可运行的测试用例,至少覆盖:**正常路径 / 边界条件(极值、上下限)/ 异常输入(非法值、缺字段、类型错误)/ 空值零值场景**
+- 测试脚本统一放 `scripts/test_*.py`,命名与被测模块对应;`python scripts/test_*.py` 可独立运行,exit 0 = PASS
+- 无法自动化的场景(真实求解、真实 AI 调用)必须给出可复现的手动测试步骤,或标注"待验证"
+- 全量回归:每个里程碑结束跑一遍全部 test_*.py,防止回归
+
+**要点**:验收要「可复核、可重跑」,且**校验方式最好与产出方式不是同一条代码路径**(否则不算独立验证),不是"看起来对了"。
+
+### 1.6 铁律沉淀:把血泪教训变成规则
+
+把吃过亏的地方写成「铁律」,并且**每条都带"为什么"**(示例,实际请按你的项目补充):
+
+> 1. 任何几何/数据改动后核对「数量 + 总量」不变量。(曾两次在坏数据上白跑)
+> 2. 检查必须用精确模式,不要用快速模式。(快速模式漏掉大部分问题)
+> 3. 不要用某工具的某操作,改用替代方案。(会留副本、后续修复会碎裂)
+> 4. 先 A 后 B,顺序不能反。(顺序反了会建立依赖链,之后无法再改)
+> 5. 工具实例用独立实例,不连已有实例;启动后设为前台可见。(可能控制错误窗口;脚本模式默认隐藏)
+> 6. 参数写入必须回读校验,不一致标记失败并继续。(静默写入失败会污染整批结果)
+> 7. 结果逐点落盘并 flush。(崩溃不丢已算点;不能等整批)
+> 8. 仿真禁止跑在主线程。(GUI 会卡死,必须子线程)
+> 9. 非登录 shell 可能不继承机器级环境变量 → 脚本内回退。(否则工具找不到/静默退出)
+
+**要点**:规则带"为什么",AI 才知道什么时候该严格遵守、什么时候可以判断变通。
+
+### 1.7 换机接续:环境差异变成可检测项
+
+写一个 `scripts/check_machine_paths.py`,一条命令核对软件路径(可能散落在多个文件)、专用解释器、依赖包、仓库资产、Git 状态,并提供 `--fix` 一键修正(或打印精确修复命令)。
+
+**要点**:把"环境假设"写成可检测的脚本,新机器第一条命令就能确认环境。
+
+> **案例对照**:第二个项目把工具定位、许可证、依赖包、仓库资产、Git 干净度全部纳入体检,并发现"AI shell 解释器与项目运行环境分离"这一常见陷阱。
+
+### 1.8 里程碑管理:Phase/Milestone + 每阶段更新 README
+
+用 **Phase(阶段)/ Milestone(里程碑)** 两级粒度推进,并立下硬纪律:
+
+- 每个 Phase 或 Milestone 完成后,**必须立即更新 README.md**,记录:完成的功能点、新增的文件/模块、关键技术决策、已知问题和后续计划
+- 不允许"代码提交了但 README 没更新"的情况;README 更新应与代码提交在同一 commit 中或紧随其后
+- 每个里程碑规划时写清:计划内容 → 实际交付 → 状态 → Commit(可追溯)
+
+**为什么有效**:里程碑是"可复核的最小单元",每完成一个就闭环一次(记录→验收→提交→更新文档),避免大段工作无人可查。
+
+### 1.9 工程规范:禁臆测 + 自查清单 + 交付声明
+
+工程规范(优先级高于"完成速度"):
+
+1. **禁止臆测**:不得编造任何未实际验证的结果、数值、接口行为或"应该能跑"的结论。若无法运行或测试某段代码/场景,必须明确说明"我无法执行此测试,以下是我的推理/建议",并给出理由与降级方案。
+2. **测试完备**:见 1.5 测试纪律。
+3. **代码规范**:遵循语言标准规范(Python 遵循 PEP8);关键逻辑必须有注释;复杂函数/类必须有 docstring(函数用途、参数、返回值、异常)。
+4. **自查清单(交付前逐项确认,标注 ✅/❌)**:代码已通读无语法错误和明显逻辑漏洞 / 所有测试用例已列出且能描述预期输入输出 / 已考虑边界情况(空值、极值、并发、超时、资源耗尽)/ 已考虑错误处理路径(异常捕获、回滚、降级)/ 多模块时已确认接口契约和数据流向 / 无法实际运行测试时已明确告知用户。
+5. **交付声明**:只有完成上述自查并确认无误后,才能说"已完成/已通过";否则必须使用"草案待验证"或"需要您协助测试",并说明缺口。
+6. **迭代修正**:若用户反馈测试失败,必须:复现问题 → 定位根因 → 修复 → 重新走一遍自查清单 → 再回复。不得仅口头致歉后跳过复现与修复。
+
+### 1.10 反模式自查:把 AI 协作的坑写成表
+
+| 反模式 | 后果 | 对策 |
+|---|---|---|
+| 不读文档直接开工 | 跑偏、重复踩坑 | 接续提示词强制"先读、先报告理解、再动手" |
+| 规矩只写"注意规范" | AI 无法执行 | 规矩写到"可执行、可检查"的颗粒度 |
+| 验收凭"看起来对" | 假完成 | 量化基准 + 独立验证 |
+| 踩坑不记录 | 下次再踩 | 每次坑都追加到 AGENTS.md 铁律 / KNOWLEDGE_BASE |
+| 环境假设不检测 | 换机全崩 | check_machine_paths.py 一键核对 |
+| 上下文只留在对话里 | 换会话即失忆 | session_log 持久化 |
+| 让 AI 多任务并行 | 上下文混乱、互相污染 | 一次一个目标 |
+| 只给结论不给原因 | AI 无法变通 | 规则带"为什么" |
+| 多处定义同一概念 | 三处漂移、改一处漏两处 | 单一事实源(各归一个权威定义) |
+| 编造未验证的数值/接口 | 返工、误导决策 | 禁臆测 + 交付声明 + "待验证"标注 |
+| 只跑功能不跑回归 | 改一处坏一片 | 每个里程碑结束跑全量 test_*.py |
+| 阶段完成不更新文档 | 文档与代码脱节、无人能接手 | 每 Phase/M 完成立即更新 README |
+
+---
+
+## 2. 通用框架:五步启动法
+
+```
+Phase 0  探查    读懂现状/需求,不猜
+Phase 1  立规矩  五支柱文件 + 会话日志 + 约定 + git init
+Phase 2  搭骨架  目录结构 + 接口文件(单一事实源)+ 最小可跑通闭环 + 第一个验收基准
+Phase 3  迭代    一次一个目标 → 记录 → 验收 → 提交 → 更新 README(里程碑粒度)
+Phase 4  交接    写清状态/阻塞/下一步 + 接续提示词 + 环境体检脚本
+```
+
+### Phase 0 · 探查(半天内)
+- 把需求、参考资料、旧代码全部读完,**先理解再动手**。
+- 确认:交付物各部分有来源;计划依赖的事实已拿到;没有靠"应该/大概"支撑的关键步骤。
+- 连续两次读取都不再改变计划,就停止探查、开工。
+
+### Phase 1 · 立规矩(半天内)
+- 建目录结构(见 3.1);写五支柱(README / AGENTS / HANDOFF / KNOWLEDGE_BASE / session_log+TEST_RECORDS);git init 第一次 commit。
+- 把「工作约定」写进 AGENTS.md(提交时机/命名/注释语言/GUI 前台/参考目录只读/专用解释器/生成物不入库)。
+
+### Phase 2 · 搭骨架(1 天内)
+- 把「关键数据接口」文件化(单一事实源)。
+- 打通一条**最小可跑通的端到端闭环**(哪怕结果粗糙)。
+- 写第一个验收基准(哪怕粗),并注明独立校验方式。
+
+### Phase 3 · 迭代(主体过程)
+- 用 **Phase/Milestone** 粒度规划,每次只推进一个目标,做完立即:记录 session_log → 跑验收基准 + 全量回归(对不上就停)→ `git commit` → 更新 README → 下一个。
+- 发现坑 → 立刻把「铁律」追加进 AGENTS.md / KNOWLEDGE_BASE。
+- 每个里程碑完成即是一个"可复核、可回溯、可交付"的闭环。
+
+### Phase 4 · 交接
+- 更新 HANDOFF.md:环境要求、恢复步骤、当前进度、阻塞点、下一步、已知坑速查。
+- 写 `scripts/check_machine_paths.py`(环境体检 + --fix)。
+- 写好给 AI 的接续提示词(见 3.6),让下一个会话/机器/人 10 分钟内接上。
+
+---
+
+## 3. 可直接复制的模板
+
+### 3.1 目录结构模板
+
+```
+<project>/
+├── README.md              # 项目全貌 + 最近更新(人类读)
+├── AGENTS.md              # AI 行为准则(AI 自动读)
+├── HANDOFF.md             # 接续指南(换人/换机/AI 续作)
+├── docs/
+│   ├── KNOWLEDGE_BASE.md  # 环境事实 + 参数语义 + 探测技术 + SOP + 已踩的坑
+│   ├── session_log.md     # 会话日志(组织记忆:目标→动作→结论→踩坑→遗留)
+│   ├── TEST_RECORDS.md    # 测试记录(环境→步骤→结果→问题→修复)
+│   └── figures/           # 图表
+├── data/                  # 机器可读数据 + ★接口文件(改这里即可改输入)
+├── scripts/               # 自动化脚本(英文注释)+ test_*.py 测试
+├── src/ 或 work/          # 实际工作产物(含单一事实源定义层)
+├── reference/             # 参考材料(只读,不许改)
+├── package.json           # 如有 JS 依赖
+└── .gitignore             # 生成物不入库
+```
+
+### 3.2 README.md 模板
+
+```markdown
+# <项目名> —— <一句话定位>
+
+<两句话:这个项目做什么、目标链路是什么>
+
+| 项目 | 内容 |
+|---|---|
+| 文档版本 | V<X>(<最新完成阶段>) |
+| 当前状态 | P1 ✅ / P2 进行中 / ... |
+
+## 目录
+| 路径 | 说明 |
+|---|---|
+| `src/` | ... |
+| `scripts/` | ... |
+| `data/` | ... |
+
+## 工具链
+| 工具 | 版本 | 路径 | 备注 |
+|---|---|---|---|
+| ... | ... | ... | ... |
+
+## 约定
+- 每次测试前先 git commit
+- 每次对话记录到 docs/session_log.md;每次测试记录到 docs/TEST_RECORDS.md
+- 文件名只用 ASCII 且有意义;源码 ASCII,脚本英文注释,报告可用中文
+- <参考目录只读等约束>
+
+## 最近更新(YYYY-MM-DD)
+### <Px-My>:<主题>
+**背景** / **改动内容** / **涉及文件** / **验证**(含 TEST 编号)/ **已知问题**
+
+## 常用命令
+python scripts/<入口>.py
+```
+
+### 3.3 AGENTS.md(给所有 AI 的规矩)模板
+
+```markdown
+# <项目名> AI 协作规矩
+
+**接续请先读 HANDOFF.md。**
+
+## 开始工作前必须阅读(按顺序)
+1. docs/KNOWLEDGE_BASE.md —— 核心知识库(环境事实、参数语义、SOP、已踩的坑)
+2. docs/HANDOFF.md —— 当前进度与接续指南
+3. README.md —— 项目全貌与最近更新
+
+## 工作约定
+- 每次测试前先 git commit
+- 每次对话记录到 docs/session_log.md;每次测试记录到 docs/TEST_RECORDS.md
+- 文件名只用 ASCII 且有意义;源码 ASCII,脚本用英文注释;报告文档可用中文
+- 生成 PPT/PDF 用 skills
+- 有 GUI 的程序必须前台运行;后台 Python 一律加 -u
+- 不要修改 <reference>/ 下任何内容(只读);原始模型只读
+- <特定工具>必须用 <特定解释器> 运行
+- 生成物不入库
+
+## 关键接口(单一事实源)
+- `data/<interface>.csv` 或 `src/<core>.py` —— 改这一个文件即可换输入,然后重跑 <步骤A> + <步骤B>
+- 凡被多处消费的定义,必须收敛到单一事实源,禁止多处漂移
+
+## 铁律(每条带"为什么")
+1. <规则>(<原因>)
+2. ...
+
+## 已知坑
+- <坑> → 表现 → 对策(详见 KNOWLEDGE_BASE.md / session_log.md)
+
+## 反模式自查(交付前对照)
+- <反模式1> → <对策1>;<反模式2> → <对策2>;...
+
+## 工程规范
+- 禁止臆测;测试完备(正常/边界/异常/空值);交付前自查清单 ✅/❌;未过自查只能说"草案待验证"
+
+## 当前状态
+- 已完成:... / 进行中:... / 阻塞:...
+
+## 文档索引
+| 文档 | 内容 |
+|---|---|
+| HANDOFF.md | 接续指南 |
+| docs/KNOWLEDGE_BASE.md | 环境事实与踩坑 |
+| docs/session_log.md | 会话记录 |
+| docs/TEST_RECORDS.md | 测试记录 |
+```
+
+### 3.4 会话日志模板(`docs/session_log.md`)
+
+```markdown
+# 会话记录 / Session Log
+
+## YYYY-MM-DD · 第 N 次对话 —— <一句话目标>
+### 用户要求(要点)
+- <要点>
+### 本次完成
+1. <动作> → <结果>
+### 用户纠偏(如有)
+- <纠正>:<为什么>,<如何修正>
+### 遗留问题 / 待确认
+- <问题>
+```
+
+### 3.5 HANDOFF.md 接续指南模板
+
+```markdown
+# 接续指南 · <项目名>
+
+面向<换人/换机/新会话>后继续工作的场景。
+
+## 1. 环境要求
+| 软件 | 版本 | 用途 | 必需性 |
+|---|---|---|---|
+| ... | ... | ... | ... |
+
+## 2. 恢复步骤(第一条命令)
+python scripts/check_machine_paths.py   # 核对环境
+
+## 3. 当前进度
+### 已完成 | 当前阻塞点 | 待办
+
+## 4. 给 AI 的接续提示词(整段粘贴,见 3.6)
+```
+
+### 3.6 给 AI 的「接续提示词」模板
+
+```text
+这是 <项目名> 项目,<一句话定位>。
+
+【先做这几件事,做完再动任何东西】
+1. 读 HANDOFF.md、docs/KNOWLEDGE_BASE.md、docs/session_log.md、AGENTS.md。
+   KNOWLEDGE_BASE 和 session_log 里记录了大量踩过的坑,请重点看,不要重复踩。
+2. 核对环境:python scripts/check_machine_paths.py
+3. 恢复工作目录/依赖:<具体命令>
+
+【工作约定(必须遵守)】
+- 每次测试前先 git commit;每次对话记录到 docs/session_log.md;每次测试记录到 docs/TEST_RECORDS.md
+- 文件名只用 ASCII 且有意义;源码 ASCII,脚本用英文注释
+- 有 GUI 的程序必须前台运行;后台 Python 加 -u
+- 不要修改 reference/ 目录;<特定工具用特定解释器>;生成物不入库
+
+【铁律(血泪教训)】
+- <规则1> / <规则2> / ...
+
+【当前状态】
+- 已完成:... / 阻塞:... / 待办:...
+
+【接下来做什么】
+- <目标1> / <目标2>
+
+先读文档、恢复环境、跑 check_machine_paths.py,然后告诉我你的理解
+和建议的下一步,不要直接开始改东西。
+```
+
+> 最后一句「先理解、别直接改」很重要——让 AI 先对齐认知,而不是闷头干活跑偏。
+
+### 3.7 验收基准表模板
+
+```markdown
+| 验收项 | 基准 | 校验方式 | 独立于产出路径? |
+|---|---|---|---|
+| <关键输出1> | <数值/逐位一致> | <命令/脚本> | 是/否 |
+| <关键输出2> | 误差 ≤ X | <独立验证路径> | 是 |
+
+对不上就别往下走。
+```
+
+**要点**:校验方式最好与产出方式**不是同一条代码路径**(否则不算独立验证)。
+
+### 3.8 环境检查脚本思路(`scripts/check_machine_paths.py`)
+
+纯只读、不修改任何东西的脚本,检查清单:
+1. 软件安装根目录(可能散落在多个文件里,必须一致)
+2. 环境变量(工具定位、许可证——非登录 shell 可能不继承)
+3. 专用解释器/二进制是否存在
+4. Python(或其他运行时)依赖包是否齐全
+5. 仓库内资产(几何/数据/依赖)是否齐全
+6. Git 仓库干净 + HEAD 有效
+7. 工作目录是否已恢复
+对每个缺失项给出"改法",并提供 `--fix` 一键修正(或打印精确修复命令)。建议探测项目内 venv,提示用其解释器复检(避免"当前 shell 与项目环境分离"误报)。
+
+### 3.9 KNOWLEDGE_BASE 模板(`docs/KNOWLEDGE_BASE.md`)
+
+```markdown
+# 知识库 — <项目名>
+
+> 本文件是项目的核心知识沉淀,供人和任何 AI 工具阅读使用。
+> 全部结论基于 <参考案例/实测> 的验证。
+
+## 1. 环境事实
+| 项 | 值 |
+|---|---|
+| <工具> | <版本> |
+| 定位方式 | <环境变量/路径> |
+| 许可证 | <服务/端口> |
+| 非登录 shell 陷阱 | ... |
+
+### 1.1 环境验证命令
+### 1.2 常见环境问题(现象 | 原因 | 解决)
+
+## 2. <工具> 自动化核心方法
+### 2.1 连接与实例管理
+### 2.2 模型加载与保存
+### 2.3 参数写入(含回读校验)
+### 2.4 结果导出与解析
+
+## 3. 参数语义
+## 4. 已踩的坑(铁律来源)
+## 5. SOP(标准操作流程)
+```
+
+### 3.10 TEST_RECORDS 模板(`docs/TEST_RECORDS.md`)
+
+```markdown
+# 测试记录与结果总结
+
+> 每次测试必须记录在此文档中(工作留痕)。
+> 最后更新:YYYY-MM-DD
+
+## 测试记录索引
+| 编号 | 日期 | 测试类型 | 结果 | 关键发现 |
+|---|---|---|---|---|
+| TEST-001 | ... | ... | ... | ... |
+
+## TEST-001:<主题>
+**日期**:...
+**测试环境**:...
+**测试目的**:...
+**测试脚本**:`scripts/test_xxx.py`
+**输出目录**:`output/.../`
+### 测试步骤与结果
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+### 关键数据 / 发现的问题 / 修复措施
+```
+
+### 3.11 里程碑管理模板(README 或独立里程碑计划文档)
+
+```markdown
+## Px-My:<主题>
+**背景**:<为什么做>
+**改动内容**:<做了什么>
+**涉及文件**:<新增/修改的文件>
+**验证**:<测试脚本 + 结果 + TEST 编号>
+**技术决策**:<关键取舍及理由>
+**已知问题/后续**:<遗留项>
+```
+
+---
+
+## 4. 日常迭代的铁律清单(随项目增长不断追加)
+
+在 AGENTS.md 里维护一份「铁律」,每条格式:**规则 + 为什么**。以下为通用示例,请替换为你的项目的真实血泪教训。
+
+| # | 规则 | 为什么 |
+|---|---|---|
+| 1 | 任何数据/几何改动后核对「数量 + 总量」不变量 | 曾两次在坏数据上白跑,只报"操作成功"会漏掉损坏 |
+| 2 | 关键输入的体积/数值必须保持不变 | 输入按件映射,改了映射就失效 |
+| 3 | 检查必须用精确模式,不要用快速模式 | 快速模式漏掉大部分问题 |
+| 4 | 不要用<某工具>的<某操作>,改用<替代方案> | 会留副本、后续修复会碎裂 |
+| 5 | 先<A>后<B>,顺序不能反 | 顺序反了会建立依赖链,之后无法再改 |
+| 6 | 量关键尺寸不要用包围盒接口 | 对薄壁件虚报数值,要用独立解析 |
+| 7 | 同类错误再次出现时查共同成因 | warning 即使伴随成功也必须处理 |
+| 8 | 工具实例用独立实例,不连已有实例;启动后设为前台可见 | 可能控制错误窗口;脚本模式默认隐藏 |
+| 9 | 参数写入必须回读校验,不一致标记失败并继续 | 静默写入失败会污染整批结果 |
+| 10 | 结果逐点落盘并 flush | 崩溃不丢已算点;不能等整批 |
+| 11 | 仿真/长任务禁止跑在主线程 | GUI 会卡死,必须子线程 |
+| 12 | 非登录 shell 要回退设置环境变量 | 环境变量不继承会导致工具找不到/静默退出 |
+
+---
+
+## 5. 与 AI 协作的沟通技巧(实战心得)
+
+### 5.1 纠正要具体、要给"为什么"
+- ❌ "这个不对,重新做"
+- ✅ "`xxx` 用错了:原因是 <技术事实>,请改用 <方案>。依据:<文档/实测>"
+
+### 5.2 验收必须量化、可独立复核
+- 给 AI 明确基准:逐位一致、误差 ≤ X、收敛阈值。
+- 要求「校验方式 ≠ 产出方式」(独立验证)。
+
+### 5.3 让 AI 区分事实与推测
+- 要求 AI 标注:**已查证 / 估算 / 待确认 / 一方称**。
+- 关键数字必须来自输入、具体信源或可复现计算,否则标为待补充并写明口径。
+
+### 5.4 一次只推一个目标
+- 每个会话/阶段聚焦一件事:做完 → 记录 → 验收 → 提交 → 下一个。
+
+### 5.5 信息不足就反问,不猜
+- 有歧义时让 AI 用「我的理解是 X,但缺少 Y,请确认 A/B」反问。
+
+### 5.6 受阻时换通道,不降级交付
+- 同一动作失败两次就换命令/目录/依赖/实现路径。换的是执行通道,不是交付标准。
+
+### 5.7 异议强制机制
+- 当需求存在技术矛盾、逻辑漏洞、安全隐患或实现风险时,AI 必须打断并指出:"【风险提示】<具体问题>。建议方案:<替代方案>,依据:<技术事实>。"禁止为迎合而执行明显错误的指令。
+
+---
+
+## 6. 常见反模式(AI 协作中的坑)
+
+| 反模式 | 后果 | 对策 |
+|---|---|---|
+| 不读文档直接开工 | 跑偏、重复踩坑 | 接续提示词强制"先读、先报告理解、再动手" |
+| 规矩只写"注意规范" | AI 无法执行 | 规矩写到"可执行、可检查"的颗粒度 |
+| 验收凭"看起来对" | 假完成 | 量化基准 + 独立验证 |
+| 踩坑不记录 | 下次再踩 | 每次坑都追加到 AGENTS.md 铁律 |
+| 环境假设不检测 | 换机全崩 | check_machine_paths.py 一键核对 |
+| 上下文只留在对话里 | 换会话即失忆 | session_log 持久化 |
+| 让 AI 多任务并行 | 上下文混乱、互相污染 | 一次一个目标 |
+| 只给结论不给原因 | AI 无法变通 | 规则带"为什么" |
+| 多处定义同一概念 | 改一处漏两处 | 单一事实源 |
+| 编造未验证的数值 | 误导决策 | 禁臆测 + 交付声明 |
+| 不跑回归就收工 | 改一处坏一片 | 里程碑结束跑全量 test_*.py |
+| 阶段完成不更新文档 | 无人能接手 | 每 Phase/M 完成更新 README |
+
+---
+
+## 7. 快速上手 Checklist(新项目第一天)
+
+- [ ] **Phase 0** 读完所有参考资料/旧代码,确认关键事实有来源
+- [ ] **Phase 1** 建目录结构(README / AGENTS.md / HANDOFF / docs/KNOWLEDGE_BASE / docs/session_log / docs/TEST_RECORDS / data / scripts / reference)
+- [ ] **Phase 1** git init + 第一次 commit
+- [ ] **Phase 1** 把「工作约定」写进 AGENTS.md(提交时机/命名/注释语言/GUI 前台/参考目录只读/专用解释器/生成物不入库)
+- [ ] **Phase 2** 把「关键输入」做成一个接口文件(单一事实源),写清"改这一个文件 + 重跑哪几步"
+- [ ] **Phase 2** 打通最小可跑通的端到端闭环
+- [ ] **Phase 2** 写下第一个验收基准(量化、可独立复核)
+- [ ] **Phase 2** 写第一个 test_*.py(含正常/边界/异常/空值)
+- [ ] **Phase 3** 开始迭代:一次一个目标 → 记录 session_log → 验收 + 全量回归 → commit → 更新 README
+- [ ] **Phase 3** 每踩一个坑,立即追加到 AGENTS.md 铁律 / KNOWLEDGE_BASE(规则 + 为什么)
+- [ ] **Phase 4** 阶段末更新 HANDOFF.md(进度/阻塞/待办 + 接续提示词)
+- [ ] **Phase 4** 写 scripts/check_machine_paths.py(环境体检 + --fix)
+
+---
+
+## 附录:案例来源与参考实现
+
+> 本框架从以下两个真实工程提炼。主文为通用方法论,此附录仅用于说明「每条方法论是从哪种场景验证得来的」,供对照学习。
+
+### 案例 A:MARS 电机热流体仿真工程(框架 V1 来源)
+
+- **场景**:电机热流体仿真,多软件工具链(电磁损耗 → 热仿真 → 交叉校核),50+ 自动化脚本,跨机器迁移,AI 全程参与。
+- **沉淀贡献**:三支柱文件、会话日志、规矩前置、接口文件化、验收量化、铁律带"为什么"、换机接续脚本、五步启动法、接续提示词。
+- **参考实现**:`CLAUDE.md`(AGENTS.md 原名)、`HANDOFF.md`、`docs/session_log.md`、`scripts/check_machine_paths.py`、`data/heat_loads.csv`。
+
+### 案例 B:PCB 轴向磁通电机自动化仿真系统(框架 V2 来源)
+
+- **场景**:双系统解耦的电机自动化仿真平台(Web 方案生成 + 本地 EXE 仿真执行),Motor-CAD 自动化,P1~P6 六个 Phase 迭代,AI 全程参与。
+- **沉淀贡献**:五支柱扩展(KNOWLEDGE_BASE + 留痕双件套)、里程碑管理、测试纪律(test_*.py 全量回归)、工程规范(禁臆测/自查清单/交付声明)、单一事实源、反模式自查表、环境变量陷阱文档化。
+- **参考实现**:`AGENTS.md`、`docs/KNOWLEDGE_BASE.md`、`docs/TEST_RECORDS.md`、`docs/HANDOFF.md`、`scripts/check_machine_paths.py`、`scripts/test_*.py`。
+
+### 如何使用本框架
+
+1. 新项目:直接复制本文档,按第 7 节「快速上手 Checklist」初始化。
+2. 遇到不确定的方法论细节,可回看附录两个案例的参考实现文件。
+3. 本框架随你持续迭代——每次改动在「版本更新记录」追加一条。

+ 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。

Fichier diff supprimé car celui-ci est trop grand
+ 9080 - 0
axial_mag_pull-master/axial_mag_pull/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot


Fichier diff supprimé car celui-ci est trop grand
+ 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


+ 127 - 0
deploy.ps1

@@ -0,0 +1,127 @@
+# PCB AFM Simulation System - Windows Deployment Script
+# Usage: .\deploy.ps1
+# Prerequisites: Python 3.10+, Node.js 18+, Git
+
+param(
+    [switch]$SkipFrontendBuild,
+    [switch]$SkipBackendDeps,
+    [string]$BackendPort = "8000",
+    [string]$FrontendPort = "5173"
+)
+
+$ErrorActionPreference = "Stop"
+$ProjectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
+$BackendDir = Join-Path $ProjectRoot "web\backend"
+$FrontendDir = Join-Path $ProjectRoot "web\frontend"
+
+Write-Host "========================================" -ForegroundColor Cyan
+Write-Host "PCB AFM Simulation System - Deployment" -ForegroundColor Cyan
+Write-Host "========================================" -ForegroundColor Cyan
+Write-Host ""
+
+# Step 1: Check prerequisites
+Write-Host "[1/6] Checking prerequisites..." -ForegroundColor Yellow
+
+try {
+    $pythonVersion = python --version 2>&1
+    Write-Host "  Python: $pythonVersion" -ForegroundColor Green
+} catch {
+    Write-Host "  ERROR: Python not found. Install Python 3.10+" -ForegroundColor Red
+    exit 1
+}
+
+try {
+    $nodeVersion = node --version 2>&1
+    Write-Host "  Node.js: $nodeVersion" -ForegroundColor Green
+} catch {
+    Write-Host "  ERROR: Node.js not found. Install Node.js 18+" -ForegroundColor Red
+    exit 1
+}
+
+# Step 2: Backend dependencies
+if (-not $SkipBackendDeps) {
+    Write-Host ""
+    Write-Host "[2/6] Installing backend dependencies..." -ForegroundColor Yellow
+    Push-Location $BackendDir
+    try {
+        pip install -r requirements.txt
+        Write-Host "  Backend dependencies installed." -ForegroundColor Green
+    } finally {
+        Pop-Location
+    }
+} else {
+    Write-Host ""
+    Write-Host "[2/6] Skipping backend dependencies (--SkipBackendDeps)." -ForegroundColor Yellow
+}
+
+# Step 3: Initialize database
+Write-Host ""
+Write-Host "[3/6] Initializing database..." -ForegroundColor Yellow
+Push-Location $BackendDir
+try {
+    python -c "from app.database import init_db; init_db(); print('Database initialized.')"
+    Write-Host "  Database initialized." -ForegroundColor Green
+} catch {
+    Write-Host "  WARNING: Database init failed: $_" -ForegroundColor Yellow
+} finally {
+    Pop-Location
+}
+
+# Step 4: Frontend build
+if (-not $SkipFrontendBuild) {
+    Write-Host ""
+    Write-Host "[4/6] Building frontend..." -ForegroundColor Yellow
+    Push-Location $FrontendDir
+    try {
+        if (-not (Test-Path "node_modules")) {
+            Write-Host "  Installing npm packages..." -ForegroundColor Gray
+            npm install
+        }
+        Write-Host "  Running build..." -ForegroundColor Gray
+        npm run build
+        Write-Host "  Frontend built successfully." -ForegroundColor Green
+    } finally {
+        Pop-Location
+    }
+} else {
+    Write-Host ""
+    Write-Host "[4/6] Skipping frontend build (--SkipFrontendBuild)." -ForegroundColor Yellow
+}
+
+# Step 5: Environment configuration
+Write-Host ""
+Write-Host "[5/6] Checking environment configuration..." -ForegroundColor Yellow
+$envFile = Join-Path $BackendDir ".env"
+if (-not (Test-Path $envFile)) {
+    Write-Host "  Creating .env file from template..." -ForegroundColor Gray
+    @"
+DATABASE_URL=sqlite:///./afm_sim.db
+KIMI_API_KEY=your_kimi_api_key_here
+KIMI_MODEL=k3
+KIMI_BASE_URL=https://api.kimi.com/coding/v1
+MAX_PARALLEL_TASKS=2
+"@ | Set-Content -Path $envFile -Encoding UTF8
+    Write-Host "  .env created. Please edit with your Kimi API key." -ForegroundColor Yellow
+} else {
+    Write-Host "  .env file exists." -ForegroundColor Green
+}
+
+# Step 6: Start services
+Write-Host ""
+Write-Host "[6/6] Starting services..." -ForegroundColor Yellow
+Write-Host ""
+Write-Host "  Backend:  http://localhost:$BackendPort" -ForegroundColor Cyan
+Write-Host "  Frontend: http://localhost:$FrontendPort" -ForegroundColor Cyan
+Write-Host ""
+Write-Host "  To start backend (in backend dir):" -ForegroundColor Gray
+Write-Host "    python -m uvicorn app.main:app --host 0.0.0.0 --port $BackendPort" -ForegroundColor Gray
+Write-Host ""
+Write-Host "  To start frontend dev server (in frontend dir):" -ForegroundColor Gray
+Write-Host "    npm run dev" -ForegroundColor Gray
+Write-Host ""
+Write-Host "  Or use Docker:" -ForegroundColor Gray
+Write-Host "    docker-compose up -d" -ForegroundColor Gray
+Write-Host ""
+Write-Host "========================================" -ForegroundColor Cyan
+Write-Host "Deployment complete!" -ForegroundColor Green
+Write-Host "========================================" -ForegroundColor Cyan

+ 38 - 0
docker-compose.yml

@@ -0,0 +1,38 @@
+# PCB AFM Simulation System - Docker Compose
+services:
+  backend:
+    build:
+      context: .
+      dockerfile: Dockerfile
+    container_name: afm-backend
+    ports:
+      - "8000:8000"
+    volumes:
+      - ./output:/app/output
+      - ./models:/app/models:ro
+      - ./web/backend/.env:/app/.env:ro
+      # E3 fix: persist SQLite database across container rebuilds
+      - ./web/backend/data:/app/data
+    environment:
+      - DATABASE_URL=sqlite:///./data/afm_sim.db
+      - MAX_PARALLEL_TASKS=2
+      # E2 fix: removed KIMI_API_KEY empty default that shadowed .env value.
+      # The key is loaded from the mounted .env file by config.py.
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
+      interval: 30s
+      timeout: 10s
+      retries: 3
+
+  frontend:
+    image: nginx:alpine
+    container_name: afm-frontend
+    ports:
+      - "5173:80"
+    volumes:
+      - ./web/frontend/dist:/usr/share/nginx/html:ro
+      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
+    depends_on:
+      - backend
+    restart: unless-stopped

+ 176 - 0
docs/CODE_REVIEW_RESPONSE.md

@@ -0,0 +1,176 @@
+# 代码评审报告 V1.0 — 应审总结
+
+| 项 | 内容 |
+|---|---|
+| 评审报告 | `第三方评审/代码评审报告_V1.0_20260828.md` |
+| 应审日期 | 2026-08-28 |
+| 应审人 | 开发团队 |
+| 评审方法 | 逐项代码核验 + 实际 Motor-CAD 测试结果对照 |
+
+---
+
+## 一、总体态度
+
+评审报告质量很高,发现的问题绝大多数属实且有代码位置证据支撑。尤其是三类系统性问题的归纳精准:
+
+1. **核心闭环"通而数据错"** — 双份硬编码漂移、mock 假数据静默上报,确实是最危险的问题
+2. **AI 页面零联调** — `\uXXXX` 乱码和 axios `.data` 遗漏是低级但致命的错误
+3. **长时运行稳定性短板** — BatchScheduler 死锁、GUI 关窗崩溃等必现问题
+
+我们虚心接受,逐项修复。以下为逐项评估。
+
+---
+
+## 二、逐项评估
+
+### A. 核心闭环数据完整性(8 项)
+
+| # | 评审结论 | 核验结果 | 处理决定 |
+|---|---|---|---|
+| A1 | upload-results 指标清单双份硬编码漂移 | **属实**。后端 plans.py 硬编码清单与 solver_core.METRIC_KEYS 不一致 | **修复**:以 solver_core.METRIC_KEYS 为单一事实源 |
+| A2 | 仿真失败静默回退 mock 假数据标 status=ok | **属实**。task_executor.py:288 确认存在 mock 回退 | **修复**:失败标 failed,mock 需显式开关+source 标注 |
+| A3 | MotorCAD 实例管理三铁律全违反 | **属实**。未传 open_new_instance、无 set_visible、无基线重载 | **修复**:复用 RobustMotorCADSolver |
+| A4 | 回读校验被 except pass 架空 | **属实** | **修复**:去掉 try/except,异常上抛 |
+| A5 | get_variable 取结果用了错误变量名 | **属实**。Torque_Avg/Efficiency/Total_Losses 无出处 | **修复**:复用导出文件解析路径 |
+| A6 | 自适应闭环缺本地执行桥 | **属实**,但属功能缺失而非 bug | **P1 后续**:工作量较大(约 1-2 天),本轮先记录,P4 收尾后优先实现 |
+| A7 | GUI self._plan 从不更新 | **属实** | **修复**:_start_scan 中回写 self._plan |
+| A8 | delete_experience 204 空响应抛异常 | **属实**。_request() 无条件 json.loads | **修复**:空 body 返回 None |
+
+### B. 稳定性/崩溃(7 项)
+
+| # | 评审结论 | 核验结果 | 处理决定 |
+|---|---|---|---|
+| B1 | BatchScheduler 必现死锁 | **属实**。第 70/107 行在锁内调 _notify_callbacks,后者第 181 行再获取锁 | **修复**:改用 RLock 或将回调移到锁外 |
+| B2 | 扫描中关窗崩溃 | **属实**。无 closeEvent | **修复**:重写 closeEvent |
+| B3 | 变量表格非数字输入全局崩溃 | **属实**。float() 无 try/except | **修复**:逐行 try/except |
+| B4 | GUI 路径 MotorCAD 进程泄漏 | **属实**。disconnect 不在 finally 中 | **修复**:try/finally |
+| B5 | run_single_point 提前 return 跳过落盘 | **属实**。第 612 行 return 跳过第 680-681 行 append/落盘 | **修复**:改为走正常收尾路径 |
+| B6 | point_timeout 死代码 | **属实**。参数接收但未使用 | **修复**:本轮先修正文档标注为未实现,真实超时列入 P1 |
+| B7 | 本地文件兜底模式无限重复执行 | **属实** | **修复**:执行后重命名为 .done.json |
+
+### C. AI 闭环逻辑错误(5 项)
+
+| # | 评审结论 | 核验结果 | 处理决定 |
+|---|---|---|---|
+| C1 | 转矩目标比较方向写反 | **属实**。第 355 行 "torque" in metric,但 key 是 tavg_nm | **修复**:显式 metric-direction 映射表 |
+| C2 | 六类收敛判据只执行四类 | **属实**。check_optimum_stability 和 check_surrogate_error 无调用方 | **修复**:本轮接入两个判据,真正实现六类 |
+| C3 | L0 预筛选空跑即通过 | **属实**。total_checks=0 时 feasible=True | **修复**:增加最低覆盖判定,0 检查时返回 unknown |
+| C4 | 信任域锚点无视优化方向 | **属实**。恒取 max | **修复**:按 objective_direction 选 max/min |
+| C5 | 信任域中心不随最优点迁移 | **属实** | **修复**:is_better 时同步迁移中心 |
+
+### D. Web 前端致命缺陷(4 项)
+
+| # | 评审结论 | 核验结果 | 处理决定 |
+|---|---|---|---|
+| D1 | Vue 模板 \uXXXX 字面乱码 | **属实**。6 个 AI 页面确认存在 | **修复**:.vue 文件直接写 UTF-8 中文 |
+| D2 | axios 响应未取 .data | **属实**。无响应拦截器 | **修复**:加响应拦截器 r => r.data |
+| D3 | ECharts 动态导入 mod.default 错误 | **属实** | **修复**:改为 mod 或静态 import |
+| D4 | 热力图/收敛轨迹伪造数据 | **属实**。硬编码 P1-P6/Q1-Q5 | **修复**:热力图接真实数据,收敛轨迹标注示意图或接 adaptive 数据 |
+
+### E. 部署/安全(4 项)
+
+| # | 评审结论 | 核验结果 | 处理决定 |
+|---|---|---|---|
+| E1 | .env 会被烤入 Docker 镜像 | **属实**。无 .dockerignore | **修复**:新增 .dockerignore |
+| E2 | KIMI_API_KEY 空值屏蔽 .env 密钥 | **属实** | **修复**:删除 compose 空值行,config.py 跳过空值 |
+| E3 | SQLite 无持久化卷 | **属实** | **修复**:增加卷映射 |
+| E4 | API 无认证 | **属实**,但当前为本地/内网部署 | **P1 后续**:本轮加共享 Token 中间件(简单实现) |
+
+### F. 功能缺陷/纪律冲突(5 项)
+
+| # | 评审结论 | 核验结果 | 处理决定 |
+|---|---|---|---|
+| F1 | 断点续扫是死代码 | **属实**。每次毫秒时间戳生成新目录 | **P1 后续**:工作量大,本轮记录 |
+| F2 | 删除项目无级联 | **属实** | **修复**:cascade + PRAGMA foreign_keys |
+| F3 | 3 个 web 端 .py 内嵌中文 | **属实** | **修复**:中文 prompt 抽取到 prompts/ 模板文件 |
+| F4 | Git preflight 可绕过 | **属实** | **裁决**:改为硬拒绝(与 AGENTS.md 纪律一致) |
+| F5 | GET 请求带写副作用 | **属实** | **修复**:download 只读,状态迁移改 POST |
+
+---
+
+## 三、与实际自测情况的对照说明
+
+评审报告注明"未启动 Motor-CAD、未运行动态仿真"。我们已完成 3 次真实 Motor-CAD 测试(TEST-001/002/003),以下几点需补充说明:
+
+1. **RobustMotorCADSolver 实际可用性已验证**:3 次测试均成功连接、计算(133-174 秒)、导出、解析。弹窗抑制(MessageDisplayState=2)确实生效,TEST-002/003 无任何 GUI 弹窗。评审中关于"弹窗需人工点确定"的问题已在 TEST-002 前修复。
+
+2. **指标解析已部分改善**:METRIC_DEFINITIONS 从 12 个扩展到 20 个后,TEST-003 成功解析 14 个指标(TEST-002 为 7 个)。但 A1 指出的"后端清单与本地端不一致"问题仍然存在,需要修复。
+
+3. **preflight 权限层 FAIL 不阻断功能**:评审第 18 项建议将"非默认安装路径"降级为 warning。实际上我们的 preflight 设计是分层的——permission 层 FAIL 只记录警告,不阻断连接和计算,3 次测试均正常完成。但"非默认安装路径"硬编码 C:\ANSYS_Motor-CAD 确实不合理,将降级为 info。
+
+4. **export_results 签名问题已修复**:评审前我们已发现并修复了 export_results 缺少 solution_type 参数的 bug(commit f3b492a),TEST-002/003 导出均成功。
+
+---
+
+## 四、修复计划
+
+### 本轮修复(P0,立即执行)
+
+按评审第八章 P0 清单顺序,共约 20 项:
+
+| 批次 | 内容 | 预估 |
+|---|---|---|
+| 1 | A2+A3+A4+A5: task_executor 重构(复用 RobustMotorCADSolver) | 0.5h |
+| 2 | A1: 指标清单单一事实源 | 0.5h |
+| 3 | A7+A8: GUI plan 回写 + api_client 204 | 20min |
+| 4 | B1: BatchScheduler 死锁 | 20min |
+| 5 | D1+D2: 前端 6 个 AI 页面中文还原 + axios 拦截器 | 1h |
+| 6 | C1+C2+C3: 转矩方向 + 六类判据 + L0 覆盖率 | 1h |
+| 7 | B3+B4+B5+B7: GUI 输入校验/finally/落盘/重复执行 | 0.5h |
+| 8 | C4+C5+D3+D4: 信任域修复 + ECharts 修复 | 0.5h |
+| 9 | E1+E2+E3+F2+F3+F5: 部署/级联/中文外置/GET 副作用 | 1h |
+| 10 | F4: Git preflight 硬拒绝 + B2 closeEvent | 0.5h |
+
+### 后续迭代(P1)
+
+- A6: 自适应闭环本地执行桥
+- F1: 断点续扫真正实现
+- B6: 真实超时机制
+- E4: API 认证完善
+- 🟡 风险项中的其余问题(Session 管理、N+1 查询、分页上限等)
+- 测试体系重建(离线单元测试)
+
+---
+
+## 五、对评审三个决策问题的回答
+
+1. **Git preflight "拒绝"还是"提醒"?** → **硬拒绝**。与 AGENTS.md 纪律一致,不满足时拒绝启动扫描,manifest 记录 HEAD + dirty 标志。
+2. **两套求解器谁合并谁?** → **以 RobustMotorCADSolver 为基础**,将 solver_core.py 的导出解析/指标定义回流合并,task_executor 直接复用,消除第三套裸调用。
+3. **ASCII 纪律是否豁免 web/backend?** → **不豁免**。中文 prompt 抽取到 `web/backend/prompts/` 模板文件。`.vue` 文件不适用此纪律,直接写 UTF-8 中文。
+
+---
+
+*应审总结完成,立即开始 P0 修复。*
+
+---
+
+## 六、第二轮修复(P1 + 风险项)
+
+第一轮 P0 修复完成后,继续修复 P1 项和高优先级风险项:
+
+| # | 问题 | 修复内容 |
+|---|---|---|
+| 1 | 3.4-34 状态枚举全仓库不统一 | 新建 `src/status_constants.py` 定义 POINT_OK/POINT_FAILED/TASK_* 常量;robust_motorcad.py 和 task_executor.py 中小写 "ok"/"failed" 统一改为大写 "OK"/"FAILED" |
+| 2 | 3.4-25 Session 管理不统一 | task_manager.py 中 7 处 `next(get_db())` 改为 `with SessionLocal() as db:`,确保 Session 自动关闭 |
+| 3 | E4 API 无认证 + max_tokens 无上限 | main.py 添加可选 API Key 中间件(AFM_API_KEY 环境变量,未设置时不启用);ai.py ChatRequest.max_tokens 加 Field(le=8192) 上限 |
+| 4 | 3.1-1 求解无超时 + 无连续失败重连 | solver_core.py MotorCADSolver 添加 point_timeout=600s 和 max_consecutive_failures=3 参数;run_single 用 daemon thread 实现超时,连续失败达阈值后自动 disconnect+connect 重连 |
+| 5 | F1 断点续扫 | scan_engine.py 已有完整的 _load_completed_indices + CSV 追加模式实现,确认功能正常,无需额外修改 |
+| 6 | plan_id 碰撞风险 | plans.py _generate_plan_id 从时间戳改为时间戳+6位随机hex后缀(uuid),避免同秒碰撞 |
+| 7 | 分页参数无上限 | projects.py 和 experience.py 的 list 接口 limit 参数加 Query(le=200) 上限,skip 加 Query(ge=0) |
+| 8 | 经验库无去重 | experience.py create_experience 添加 source_plan_id + params_json 去重检查,重复时返回已存在记录 |
+
+### 验证结果
+
+- 75 个 Python 文件全部通过 py_compile 语法检查
+- 所有 .py 文件纯 ASCII 合规(临时脚本除外)
+- 关键模块导入正常(status_constants / solver_core / metrics_constants)
+- 状态枚举统一:全仓库仿真点状态只用 "OK"/"FAILED"
+
+### 仍为已知限制(P2,后续迭代)
+
+- A6: 自适应闭环本地执行桥(需在 task_executor 中集成 feasibility_search,工作量约 1-2 天)
+- N+1 查询优化(analytics 等服务的批量查询)
+- 测试体系重建(离线单元测试 + CI)
+- 其余 🟡 风险项中的低优先级问题
+
+*第二轮修复完成,提交推送。*

+ 1058 - 0
docs/CONVERSATION_LOG.md

@@ -0,0 +1,1058 @@
+# 对话与决策记录
+
+> 所有关键决策、技术选择、问题排查均带时间戳记录于此。
+> 格式:`## YYYY-MM-DD HH:MM — 主题`
+> 统一模板(V2 框架,见 ai-collab-dev-playbook-v2.md §3.4):
+> `## YYYY-MM-DD · 主题` → 用户要求(要点)→ 本次完成(动作+结果)→ 用户纠偏(如有)→ 遗留问题/待确认
+
+---
+
+## 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:经验库雏形 + 反馈闭环
+
+### 待办
+- [x] 搭建项目规范文档
+- [x] 编写 solver_core.py
+- [x] 编写 run_single.py 并跑通 MARS 模型
+- [x] 初始化 Git 仓库
+- [x] M2: 参数扫描引擎 + 气隙扫描验证
+- [ ] M3: 方案JSON接口 + 本地GUI
+- [ ] M4: 经验库雏形 + 反馈闭环
+
+---
+
+## 2026-08-27 13:55 — M1 完成:单工况仿真跑通
+
+### 成果
+- `src/solver_core.py`:Motor-CAD连接、参数回读校验、电磁求解、16项指标提取
+- `scripts/run_single.py`:单工况验证脚本
+- Git仓库初始化,初始commit `75347b6`
+
+### 验证结果(MARS-12S10P SSSR,模型默认工况)
+| 指标 | 数值 |
+|---|---|
+| 平均转矩 | 0.5219 Nm |
+| 转矩脉动 | 2.8150 % |
+| 系统效率 | 86.06 % |
+| 总损耗 | 41.945 W |
+| 求解耗时 | 139.2 s |
+
+状态:OK,Phase 1 四项必选指标全部提取成功。
+
+---
+
+## 2026-08-27 14:42 — M2 完成:参数扫描引擎 + 气隙扫描验证
+
+### 成果
+- `src/scan_engine.py`:笛卡尔积扫描、每点基线重载、参数回读校验、断点续跑、逐点CSV flush、manifest+log+raw
+- `scripts/run_scan.py`:CLI入口,支持config JSON或命令行参数,内置Git preflight
+- `scripts/scan_airgap.json`:气隙扫描配置(0.6/1.0/1.5mm)
+- commit `a43d971`
+
+### 气隙扫描验证结果(3点全部OK,总耗时约7分钟)
+
+| 气隙(mm) | 平均转矩(Nm) | 转矩脉动(%) | 效率(%) | 总损耗(W) | 铁耗(W) | 磁钢损耗(W) | 反电动势(V) | 空载转速(rpm) |
+|---|---|---|---|---|---|---|---|---|
+| 0.6 | 0.5663 | 5.451 | 84.93 | 49.92 | 3.93 | 0.93 | 8.79 | 5450 |
+| 1.0 | 0.5219 | 2.815 | 86.06 | 41.95 | 3.00 | 0.56 | 7.90 | 6055 |
+| 1.5 | 0.4677 | 1.753 | 86.35 | 36.56 | 2.17 | 0.31 | 6.95 | 6878 |
+
+### 物理趋势验证(全部符合预期)
+1. **转矩随气隙增大而减小**:磁耦合减弱 ✓
+2. **转矩脉动随气隙增大而减小**:磁场更平滑,齿槽效应减弱 ✓
+3. **效率随气隙增大略升**:铁耗+磁钢损耗下降幅度超过转矩下降 ✓
+4. **铜耗恒定16.76W**:电流不变 ✓
+5. **反电动势随气隙增大而减小**:气隙磁密降低 ✓
+6. **空载转速随气隙增大而升高**:弱磁效应 ✓
+
+### 关键验证点
+- 1.0mm点与M1单工况结果**完全一致**(0.5219/2.815/86.06/41.95),确认每点基线重载正常工作,无参数污染
+- 每点求解耗时134-141s,与M1一致
+- 输出目录结构完整:scan_results.csv + program_log.log + run_manifest.json + raw/(3个原始导出)
+- Git preflight正常工作
+
+---
+
+## 2026-08-27 15:15 — M3 完成:方案JSON接口 + PySide6本地GUI
+
+### 成果
+- `src/plan_schema.py`:SimulationPlan / ScanVariable / ScanCase 数据类,JSON序列化/反序列化,验证,笛卡尔积点生成
+- `src/gui/main.py`:PySide6 GUI主窗口(方案编辑器 + 执行监控 + 结果表 + 彩色日志)
+- `src/gui/__init__.py`:GUI包初始化
+- `scripts/run_gui.py`:GUI启动脚本
+- PySide6 6.11.2 已安装
+
+### GUI功能
+- 模型路径选择(Browse按钮)
+- 扫描变量表格编辑器(添加/删除变量,设置name/start/stop/step或显式values)
+- 实时估算总点数和耗时
+- 方案保存/加载(simulation_plan.json)
+- Start/Stop扫描控制
+- 进度条实时更新
+- 结果表格实时刷新(核心指标优先,附加指标随后)
+- 彩色分级日志(INFO/OK/WARN/ERROR)
+- 状态栏
+- Git preflight检查(不干净时提示确认)
+
+### 工程规范
+- 5层闪退防护:全局excepthook、禁止末窗退出、dict安全访问、槽函数try-except、MotorCAD对象keepalive
+- QThread子线程运行仿真,不阻塞UI
+- 信号槽通信(log/progress/result_row/finished/failed)
+- 工业软件风格QSS(浅灰蓝背景、白色卡片、蓝色强调色)
+- 全部源码纯ASCII
+
+### 验证
+- GUI窗口创建测试通过(标题、变量表、估算标签正常)
+- plan_schema保存/加载/点生成/验证测试通过
+- 语法检查全部通过
+- ASCII检查全部通过
+
+### 待M4
+- 经验库雏形(SQLite + JSON)
+- 反馈调整方案(基于结果推荐下一轮参数范围)
+- GUI集成经验库检索
+
+---
+
+## 2026-08-27 15:45 — M4 完成:经验库雏形 + 反馈闭环
+
+### 成果
+- `src/experience_db.py`:SQLite经验库,支持结果自动积累、相似案例检索、基于趋势分析的下一轮参数推荐
+- GUI集成:扫描完成后自动入库,"Recommend Next"按钮显示推荐
+
+### 经验库功能
+1. **自动积累**:每次扫描完成后,OK结果自动存入SQLite(params_json + metrics_json)
+2. **相似检索**:按拓扑+参数相对距离匹配历史案例(find_similar)
+3. **反馈推荐**:基于历史数据线性趋势分析,推荐每个参数的增大/减小方向和建议范围(recommend_next_round)
+4. **统计查询**:总运行数、不同方案数、按拓扑筛选
+
+### 反馈推荐算法
+- 从经验库中检索相似案例(参数相对距离≤tolerance)
+- 对每个参数,收集(param_value, target_metric)数据对
+- 计算线性回归斜率(cov/var)
+- 根据优化方向(maximize/minimize)推荐参数增大或减小
+- 建议范围:当前值偏向推荐方向±20%
+- 数据不足时返回insufficient_data状态
+
+### GUI集成
+- 扫描完成后自动将所有OK结果存入经验库
+- "Recommend Next"按钮(紫色),扫描完成后启用
+- 推荐对话框显示:最佳结果、各参数推荐方向、建议范围、趋势斜率、数据点数
+- 经验库状态可通过状态栏/日志查看
+
+### 验证
+- experience_db.py:插入/查询/相似检索/推荐 全部测试通过
+- GUI:经验库实例创建正常,Recommend按钮初始禁用,扫描后启用
+- 语法检查全部通过
+- ASCII检查全部通过
+
+### Phase 1 全部完成
+- M1: 单工况仿真 ✅
+- M2: 参数扫描引擎 ✅
+- M3: 方案JSON + PySide6 GUI ✅
+- M4: 经验库 + 反馈闭环 ✅
+
+### 下一步(Phase 2 规划)
+- Web端方案系统基础框架(Vue3 + FastAPI + PostgreSQL)
+- 内网API通信(方案下载 + 结果上传)
+- DRSS拓扑支持
+- 更复杂的优化算法(Morris/LHS/Kriging/NSGA-II)
+
+---
+
+## 2026-08-27 16:00 — Phase 2 启动与关键决策
+
+### 参与者
+Car.Lin(项目负责人)
+
+### Phase 2 目标
+搭建系统一(Web端方案生成及优化系统),与Phase 1已完成的本地执行端(系统二)API自动联调,实现完整闭环。
+
+### 关键决策(用户确认)
+
+1. **前端框架**:Vue 3 + TypeScript + Element Plus + ECharts
+   - 理由:数据密集型仪表盘(参数矩阵/结果表/敏感性热力图/Pareto/收敛曲线),Element Plus表格表单组件丰富,中文生态好,ECharts在Vue中集成成熟
+   - 不选React:Ant Design对复杂数据表格支持不如Element Plus灵活,国内工业软件Vue更主流
+
+2. **AI接口**:初期不接外部LLM,用规则引擎+经验库检索生成方案;后期接DeepSeek(用户提供API Key)
+   - Phase 2简化,AI方案生成推迟到Phase 3
+
+3. **数据库**:初期用SQLite(SQLAlchemy ORM抽象层),后期迁移PostgreSQL+pgvector只需改连接字符串
+   - 理由:SQLite零配置,本地测试方便;SQLAlchemy ORM确保迁移平滑
+   - 向量检索功能推迟到PostgreSQL迁移后
+
+4. **部署**:本地裸装运行,不用Docker
+
+5. **P2范围简化**:AI方案生成推迟到Phase 3,P2用规则+经验的简化方案生成
+
+6. **双系统通信**:直接做API自动联调(不做手动文件交换)
+   - 系统一API:GET /api/plans/{id}/download, POST /api/plans/{id}/upload-results
+   - 系统二增加API客户端:自动拉取方案、回传结果
+
+### Phase 2 里程碑
+- P2-M1:Web端基础框架(FastAPI后端 + Vue3前端 + SQLite + 基础CRUD API)
+- P2-M2:边界条件输入 + 方案编辑器(规则引擎生成方案)
+- P2-M3:经验库Web端 + 结果分析仪表盘
+- P2-M4:双系统API联调 + 知识库管理
+- P2-M5:Phase 2验收
+
+### 开发纪律
+- 积累开发经验和踩坑记录到CONVERSATION_LOG.md
+- 后端Python代码纯ASCII
+- 前端TypeScript/Vue代码遵循ESLint规范
+- 每次里程碑完成后Git提交
+- API接口与系统二的simulation_plan.json格式完全一致
+
+---
+
+## 2026-08-27 16:30 — P2-M1 完成:Web端基础框架
+
+### 成果
+- **后端(FastAPI + SQLAlchemy + SQLite)**:
+  - `web/backend/app/main.py`:FastAPI应用入口,CORS中间件,路由注册,健康检查
+  - `web/backend/app/config.py`:应用配置(数据库/服务器/CORS/元数据),环境变量覆盖
+  - `web/backend/app/database.py`:SQLAlchemy引擎/Session/Base,init_db建表
+  - `web/backend/app/models/`:Project, SimulationPlan, SimulationResult, ExperienceCase 四个ORM模型
+  - `web/backend/app/schemas/`:Pydantic v2 请求/响应Schema(创建/更新/响应/列表/下载)
+  - `web/backend/app/routers/`:
+    - `projects.py`:项目CRUD(GET/POST/PUT/DELETE + 列表筛选)
+    - `plans.py`:方案CRUD + 下载接口(按ID/按UUID)+ 结果上传(CSV解析)+ 结果查询
+    - `experience.py`:经验库CRUD + 相似检索
+  - `web/backend/test_api.py`:后端API集成测试脚本(健康检查/项目/方案/下载/列表)
+  - `web/backend/run.py`:uvicorn启动入口
+  - `web/backend/requirements.txt`:fastapi/uvicorn/sqlalchemy/pydantic/python-multipart
+
+- **前端(Vue 3 + TypeScript + Element Plus + ECharts + Pinia + Vue Router)**:
+  - `web/frontend/src/main.ts`:应用入口,Element Plus/Pinia/Router注册
+  - `web/frontend/src/App.vue`:根组件
+  - `web/frontend/src/router/index.ts`:路由配置(项目列表/项目详情/方案详情/经验库)
+  - `web/frontend/src/api/index.ts`:axios封装,项目/方案/经验库API调用
+  - `web/frontend/src/layouts/MainLayout.vue`:主布局(侧边栏导航 + 顶部栏 + 内容区)
+  - `web/frontend/src/views/`:
+    - `ProjectList.vue`:项目列表页(表格/新建/删除/搜索)
+    - `ProjectDetail.vue`:项目详情页(基本信息 + 方案列表)
+    - `PlanDetail.vue`:方案详情页(方案信息 + 变量表 + 结果表)
+    - `ExperienceList.vue`:经验库列表页
+  - `web/frontend/vite.config.ts`:Vite配置(代理/api到后端8000端口)
+  - `web/frontend/tsconfig.json`:TypeScript配置
+  - `web/frontend/package.json`:依赖与脚本
+
+- **系统二API客户端**:
+  - `src/api_client.py`:WebAPIClient类(健康检查/项目/方案下载/结果上传/完整工作流),纯urllib无额外依赖
+  - `scripts/test_api_client.py`:API客户端测试脚本
+
+### 关键API端点(双系统联调用)
+| 方法 | 路径 | 用途 |
+|---|---|---|
+| GET | `/api/health` | 健康检查 |
+| GET | `/api/projects` | 项目列表 |
+| POST | `/api/projects` | 创建项目 |
+| GET | `/api/plans/{id}/download` | 按ID下载方案(系统二拉取) |
+| GET | `/api/plans/by-plan-id/{uuid}/download` | 按plan_id字符串下载方案 |
+| POST | `/api/plans/{id}/upload-results` | 上传scan_results.csv(系统二回传) |
+| GET | `/api/plans/{id}/results` | 查询方案结果 |
+
+### 验证结果
+1. **后端API测试**:全部通过(健康检查/创建项目/列表/创建方案/下载/按UUID下载/列表)
+2. **前端构建**:vue-tsc类型检查通过 + vite build成功(9.79s,1676模块)
+3. **ASCII检查**:全部.py/.ps1文件纯ASCII,无违规
+4. **依赖版本**:fastapi 0.141.1 / sqlalchemy 2.0.52 / pydantic 2.13.4 / Vue 3.3 / Element Plus 2.4
+
+### 踩坑记录
+1. **vue-tsc与TypeScript版本兼容**:
+   - 初始安装TypeScript 5.9.3 + vue-tsc 1.x → 报"Search string not found"错误
+   - 升级vue-tsc@latest(版本号与TS对齐到5.5.4)→ 解决
+   - 教训:vue-tsc版本必须与TypeScript主版本严格匹配,建议锁定typescript@~5.5.4
+
+2. **PowerShell npm退出码误报**:
+   - npm将警告信息输出到stderr,PowerShell误判为失败(exit code 1)
+   - 实际构建成功,需看stdout中的"✓ built"确认
+
+### 工程规范
+- 后端Python代码纯ASCII
+- 前端TypeScript严格模式
+- SQLite数据库文件不入库(.gitignore已配置)
+- node_modules/dist不入库
+- API响应格式与系统二simulation_plan.json完全一致
+
+### P2-M1 验收标准
+- [x] FastAPI后端可启动,健康检查正常
+- [x] 项目/方案/经验库 CRUD API完整
+- [x] 方案下载接口(按ID/按UUID)返回系统二兼容格式
+- [x] 结果上传接口可解析scan_results.csv
+- [x] Vue3前端可构建,路由/布局/页面完整
+- [x] 前端API层对接后端
+- [x] 系统二API客户端可调用后端
+- [x] 全部源码纯ASCII
+- [x] .gitignore配置完整
+
+---
+
+
+---
+
+## 2026-08-27 16:22 — P2-M2 完成:边界条件输入 + 方案编辑器(规则引擎)
+
+### 成果
+
+#### 后端:规则引擎服务
+- `web/backend/app/services/rule_engine.py`(439行):
+  - **ScanParameter注册表**:8个可扫描参数(Airgap/Magnet_Length/Magnet_Thickness/Magnet_Arc/RMSCurrent/Shaft_Speed/Magnet_Temperature/TorquePointsPerCycle),含单位、分类、默认范围、物理上下限
+  - **BoundaryConditions解析器**:从项目JSON解析拓扑/尺寸/转速/电流/温度/目标等边界条件
+  - **recommend_range()**:基于边界条件智能推荐每个参数的扫描范围
+    - Airgap:按外径缩放(0.8%-2% D),高转矩目标偏低
+    - Magnet_Length:按外径缩放(3%-7% D),高速封顶
+    - Magnet_Thickness:按径向深度缩放
+    - RMSCurrent:以指定电流为中心(0.5x-1.5x),损耗约束封顶
+    - Shaft_Speed:以指定转速为中心(0.6x-1.4x)
+    - Magnet_Temperature:冷态到热态扫描(20-120度C)
+  - **generate_plan()**:从边界条件生成完整方案草稿(变量列表+推荐值+估算点数)
+  - **generate_values()**:生成等间距采样值(含浮点精度处理)
+
+#### 后端:方案生成API
+- `web/backend/app/routers/generation.py`(103行):
+  - `GET /api/scan-parameters`:获取可扫描参数注册表(前端下拉用),支持按分类筛选
+  - `POST /api/recommend-range`:单参数范围推荐
+  - `POST /api/generate-plan`:从边界条件生成方案
+  - `POST /api/projects/{id}/generate-plan`:为已有项目生成方案(合并项目BC)
+- `web/backend/app/schemas/generation.py`:Pydantic Schema(ParameterInfo/RangeRecommendation/PlanGenerateRequest/Response)
+- `web/backend/app/main.py`:已注册generation路由
+
+#### 前端:边界条件输入表单
+- 改造 `ProjectList.vue` 新建项目对话框(+99行):
+  - 结构化边界条件输入(8个字段):外径/内径/转速/电流/磁钢温度/目标转矩/目标效率/最大损耗
+  - el-input-number数字输入,带单位后缀
+  - 两列网格布局,Boundary Conditions分区
+  - 提交时自动合并拓扑+非空BC到boundary_conditions JSON
+
+#### 前端:可视化方案编辑器
+- 重写 `ProjectDetail.vue` 新建方案对话框(+370行,900px宽):
+  - **变量表格编辑器**:每行一个扫描变量
+    - 参数选择下拉(从scan-parameters API加载,过滤已用参数)
+    - Range模式:start/stop/step输入,自动生成values
+    - Values模式:显式输入逗号分隔值
+    - 实时显示values列表和点数
+    - 删除按钮
+  - **工具栏**:Add Variable / Generate from Rules(规则引擎一键生成)/ Clear All
+  - **实时估算**:总点数(笛卡尔积)+ 预估时间(3min/点)
+  - 创建时构建系统二兼容的plan_data格式(name/display_name/unit/values)
+
+#### 前端API层扩展
+- `generationApi`:listScanParameters / recommendRange / generatePlan / generatePlanForProject
+
+### Git提交
+- commit `a95c0db`:feat(P2-M2): boundary conditions input + visual plan editor + rule engine
+- 9个文件,+1055行,-37行
+
+### 验证结果
+1. **规则引擎单元测试**:8参数注册、范围推荐、值生成全部通过
+2. **完整集成测试(8项)**:健康检查->创建项目(带BC)->参数注册表->范围推荐->生成方案(120点)->创建方案->下载方案(系统二兼容验证)->列表 全部通过
+3. **前端构建**:vite build成功,ProjectDetail.js从6.32kB增长到13.51kB,ProjectList.js从5.47kB增长到9.18kB
+4. **系统二兼容性**:方案下载接口返回的plan_data格式与src/plan_schema.py完全一致,每个variable含name+display_name+unit+values
+
+### 规则引擎设计要点
+1. **物理缩放规则**:几何参数按外径比例缩放,符合AFM电机设计经验
+2. **中心偏移规则**:电参数以指定工况为中心,向两侧扩展
+3. **约束封顶规则**:损耗/转矩/效率目标会限制参数范围上限
+4. **拓扑过滤**:参数支持topology_supported标记,不适用拓扑自动跳过
+5. **可扩展性**:新增参数只需在SCAN_PARAMETERS字典添加条目,推荐逻辑按参数名分发
+
+### 待优化(后续里程碑)
+- generate_values浮点精度:当stop因step不整除时会额外追加,可能产生非整齐值,用户可在前端手动调整
+- 规则引擎当前基于启发式规则,P2-M3/P3可结合经验库数据做数据驱动推荐
+- 前端边界条件编辑:项目详情页目前显示原始JSON,P2-M3可改为结构化展示+编辑
+
+### P2-M2 验收标准
+- [x] 后端规则引擎服务(参数注册表+范围推荐+方案生成)
+- [x] 方案生成API端点(4个新端点)
+- [x] 前端边界条件结构化输入表单
+- [x] 前端可视化方案编辑器(变量表格+Range/Values模式+实时估算)
+- [x] "Generate from Rules"一键生成功能
+- [x] 完整集成测试通过(8项)
+- [x] 系统二兼容性验证通过
+- [x] 前端构建通过
+- [x] 全部Python源码纯ASCII
+- [x] Git提交完成(a95c0db)
+
+---
+
+## 2026-08-27 17:45 — P2-M3 完成:经验库Web端增强 + 结果分析仪表盘
+
+### 成果
+
+#### 后端:分析服务(analytics.py,~340行)
+- `web/backend/app/services/analytics.py`:
+  - **METRIC_DEFS**:11项指标定义(转矩/脉动/效率/各类损耗/反电动势/功率/转速),含label/unit/higher_is_better
+  - **compute_experience_stats()**:经验库聚合统计(总数/拓扑分布/指标范围min-max-avg/参数覆盖率/平均评分)
+  - **find_similar_cases()**:相似案例检索(归一化欧氏距离,共享参数匹配,相似度评分0-1)
+  - **compute_trend_data()**:参数-指标趋势数据(散点图用,按X排序,含统计摘要)
+  - **compute_pareto_frontier()**:Pareto前沿计算(双指标支配关系,返回全部点+前沿点)
+  - **compute_sensitivity()**:参数敏感性排名(Pearson相关系数,绝对值排序,正负方向标注)
+
+#### 后端:分析API路由(analytics.py,~180行)
+- `web/backend/app/routers/analytics.py`:
+  - `GET /api/analytics/metrics`:指标定义列表
+  - `GET /api/analytics/experience/stats`:经验库统计(支持拓扑筛选)
+  - `POST /api/analytics/experience/similar`:相似案例检索(body: params,query: topology/top_k/tolerance)
+  - `GET /api/analytics/plans/{id}/trend`:方案趋势数据(param_key + metric_key)
+  - `GET /api/analytics/plans/{id}/pareto`:Pareto前沿(x_metric + y_metric)
+  - `GET /api/analytics/plans/{id}/sensitivity`:参数敏感性(metric_key)
+  - `GET /api/analytics/projects/{id}/overview`:项目概览(方案数/结果数/最佳效率转矩)
+- `web/backend/app/main.py`:已注册analytics路由
+
+#### 前端:结果分析仪表盘(Dashboard.vue,~400行)
+- `web/frontend/src/views/Dashboard.vue`:
+  - **项目/方案选择器**:级联选择,切换自动加载数据
+  - **统计卡片**:总方案数/结果数(OK/Failed)/最佳效率/最佳转矩
+  - **参数-指标趋势图**(ECharts散点图):可切换X参数和Y指标,含平均值参考线
+  - **Pareto前沿图**(ECharts散点+折线):可切换X/Y指标,全部点灰色+前沿点红色高亮
+  - **参数敏感性柱状图**(ECharts横向柱状图):正相关绿色/负相关红色,显示相关系数
+  - **结果数据表**:参数列+关键指标列+状态标签,支持滚动
+  - 响应式布局,窗口resize自动调整图表
+
+#### 前端:经验库页面增强(ExperienceList.vue,~450行)
+- 重写 `web/frontend/src/views/ExperienceList.vue`:
+  - **统计卡片**:总案例数/SSSR-DRSS分布/平均评分/跟踪参数数
+  - **多条件筛选**:关键词搜索(结论/标签)/拓扑/标签/最低评分
+  - **详情弹窗**:点击行打开,含参数表/指标表/结论/标签/评分
+  - **相似案例检索弹窗**:输入8个常用参数值,设置拓扑/结果数,返回相似度进度条+共享参数标签
+  - 表格增强:参数chip样式、指标分三行展示、标签彩色、行点击高亮
+
+#### 前端:路由与布局
+- `router/index.ts`:新增 `/dashboard` 路由
+- `layouts/MainLayout.vue`:侧边栏新增Dashboard菜单项(DataAnalysis图标)
+- `api/index.ts`:新增 `analyticsApi`(8个端点)
+
+### Git提交
+- 待提交(本轮完成后统一提交)
+
+### 验证结果
+1. **分析服务单元测试**(test_analytics.py):6项全部通过
+   - 指标定义:11项
+   - 经验库统计:总数/拓扑/评分/参数覆盖/指标范围正确
+   - 相似检索:找到完全匹配案例(相似度1.0)
+   - 趋势数据:4个OK点按X排序,统计摘要正确
+   - Pareto前沿:4个点全部在前沿(测试数据无支配关系)
+   - 参数敏感性:RMSCurrent正相关0.75,Airgap负相关-0.66
+2. **后端导入验证**:`from app.main import app` 成功,analytics路由已注册
+3. **前端构建**:vite build成功,生成Dashboard.js/css和ExperienceList.js/css
+4. **ASCII检查**:所有新增.py文件纯ASCII
+
+### 设计要点
+1. **分析服务纯函数设计**:所有分析函数接收dict列表,不依赖数据库,便于单元测试和复用
+2. **Pareto前沿算法**:O(n^2)支配关系判断,考虑higher_is_better方向,适合百级数据点
+3. **敏感性分析**:Pearson相关系数,要求参数与指标样本数一致,自动跳过不匹配参数
+4. **相似检索归一化**:按参数值域归一化后计算欧氏距离,避免量纲影响,相似度=1-距离
+5. **ECharts按需初始化**:图表在数据加载后初始化,组件卸载时dispose,避免内存泄漏
+6. **前端图表交互**:所有图表支持指标切换,tooltip显示完整信息,颜色编码方向(正绿负红)
+
+### 待优化(后续里程碑)
+- Dashboard当前基于单方案分析,P2-M4可增加跨方案对比
+- 经验库相似检索当前基于参数距离,P3可结合指标相似度做联合检索
+- 图表可增加导出PNG功能
+- 结果数据表可增加排序和导出CSV
+
+### P2-M3 验收标准
+- [x] 后端分析服务(统计/趋势/Pareto/敏感性/相似检索)
+- [x] 分析API路由(8个新端点)
+- [x] 前端结果分析仪表盘(4类ECharts图表+统计卡片+结果表)
+- [x] 前端经验库页面增强(统计/搜索/筛选/详情/相似检索)
+- [x] 路由和侧边栏更新
+- [x] 分析服务单元测试通过(6项)
+- [x] 前端构建通过
+- [x] 全部Python源码纯ASCII
+- [x] README.md更新至V0.4
+
+---
+
+## 2026-08-27 18:30 — P2-M4 完成:双系统API联调 + 知识库管理
+
+### 成果
+
+#### 后端:经验库CRUD增强
+- `web/backend/app/routers/experience.py`(重写,~280行):
+  - **PUT /api/experience/{id}**:更新经验案例(conclusion/tags/rating/params/metrics/topology)
+  - **POST /api/experience/from-plan/{plan_id}**:从方案的所有OK结果批量导入经验库
+    - 自动生成conclusion(基于指标值的描述:转矩/效率/脉动/损耗+性能评估)
+    - 支持自定义tags/rating/auto_conclusion参数
+    - 从plan_json中获取topology/model_path(兼容SimulationPlan模型无此字段)
+  - **POST /api/experience/from-result/{result_id}**:从单个结果导入经验库
+  - **_generate_conclusion()**:自动结论生成器(ASCII-only文本)
+
+#### 系统二:API客户端增强
+- `src/api_client.py`(+180行,从17方法扩展到27方法):
+  - **经验库方法**(10个):list/get/create/update/delete/importFromPlan/importFromResult
+  - **分析方法**(6个):getMetricDefs/getExperienceStats/findSimilarExperience/getPlanTrend/getPlanPareto/getPlanSensitivity/getProjectOverview
+  - **sync_local_experience_to_web()**:本地经验库同步到Web端(去重,按plan_id+params签名)
+  - 系统二可完整调用Web端所有API,实现双向通信
+
+#### 前端:经验库管理页面增强
+- `web/frontend/src/views/ExperienceList.vue`(+200行):
+  - **Import from Plan按钮**:选择方案+tags+rating+auto_conclusion开关,一键批量导入
+  - **编辑功能**:表格操作列Edit按钮,弹窗编辑conclusion/tags(多选可创建)/rating/topology
+  - **删除功能**:操作列Delete按钮,确认弹窗后删除
+  - 导入弹窗支持方案下拉选择(从API加载所有方案)
+- `web/frontend/src/api/index.ts`:experienceApi新增update/importFromPlan/importFromResult
+
+#### 双系统联调集成测试
+- `web/backend/test_integration.py`(~300行,55个测试点):
+  - 完整闭环测试:健康检查→创建项目→创建方案→下载方案→上传结果CSV→查询结果→导入经验库→经验库CRUD→分析API(统计/相似/趋势/Pareto/敏感性/项目概览/指标定义)→清理
+  - 使用FastAPI TestClient,无需真实服务器
+  - 模拟系统二上传scan_results.csv(5行,4OK+1FAILED)
+  - 验证数据一致性和API响应格式
+
+### Git提交
+- 待提交(本轮完成后统一提交)
+
+### 验证结果
+1. **集成测试**:55/55全部通过(16个测试组)
+2. **后端导入验证**:`from app.main import app` 成功,所有路由注册
+3. **前端构建**:vite build成功(22.62s),ExperienceList.js 16.48kB
+4. **系统二API客户端**:27个方法,10经验库+6分析+11原有
+5. **ASCII检查**:所有新增/修改.py文件纯ASCII
+
+### 双系统联调架构
+```
+系统一(Web端)                    系统二(本地EXE)
+┌─────────────┐                    ┌─────────────┐
+│  FastAPI    │◄─── REST API ────► │ api_client  │
+│  + SQLite   │   下载方案/上传结果  │  + MotorCAD │
+│  + Vue3     │   经验库同步/分析    │  + 本地GUI  │
+└─────────────┘                    └─────────────┘
+       │                                    │
+       └────────── 经验库双向同步 ──────────┘
+```
+
+### 关键设计决策
+1. **经验库导入从结果而非方案**:只导入status=OK的结果,避免失败数据污染经验库
+2. **自动结论生成**:基于指标值的简单规则生成,用户可后续编辑完善
+3. **系统二api_client纯urllib**:无第三方依赖,可在任何Python环境运行
+4. **本地→Web同步去重**:按plan_id+params签名去重,避免重复导入
+5. **SimulationPlan兼容**:topology/model_path从plan_json获取,不依赖模型字段
+
+### 待优化(后续里程碑)
+- 经验库导入可增加人工审核步骤(先存为draft,确认后入库)
+- 系统二本地经验库可增加定时自动同步到Web端
+- 经验库可增加版本管理和变更历史
+- P2-M5验收时可增加端到端真实Motor-CAD仿真联调测试
+
+### P2-M4 验收标准
+- [x] 后端经验库CRUD增强(PUT更新 + 从方案/结果导入)
+- [x] 自动结论生成器
+- [x] 系统二api_client增强(经验库+分析API,27方法)
+- [x] 本地经验库同步到Web端功能
+- [x] 前端经验库管理(编辑/导入/删除)
+- [x] 双系统联调集成测试(55测试点全部通过)
+- [x] 前端构建通过
+- [x] 全部Python源码纯ASCII
+- [x] README.md更新至V0.5
+
+---
+
+## 2026-08-27 19:40 — P2-M5 完成:Phase 2 验收(自动化+人工)
+
+### 验收方式
+1. **自动化测试**:后端API验收测试 + 双系统联调集成测试 + 分析服务单元测试 + 前端构建 + ASCII检查
+2. **前端人工验收**:通过浏览器自动化工具操作前端页面,逐项验证交互功能
+
+### 自动化验收结果
+
+| 测试项 | 结果 | 详情 |
+|---|---|---|
+| 后端API验收测试 | ✅ 23/23通过 | 健康检查/项目/方案/经验库CRUD/分析API/规则引擎/API文档 |
+| 双系统联调集成测试 | ✅ 55/55通过 | 完整闭环:创建项目→方案→下载→上传结果→导入经验库→CRUD→分析API |
+| 分析服务单元测试 | ✅ 6/6通过 | 指标定义/统计/相似检索/趋势/Pareto/敏感性 |
+| 前端构建 | ✅ 成功 | vite build 12.80s,无错误 |
+| ASCII源码检查 | ✅ 通过 | 所有.py/.ps1文件纯ASCII |
+
+### 前端人工验收结果(浏览器自动化操作)
+
+| 测试项 | 结果 | 验证详情 |
+|---|---|---|
+| 项目列表页 | ✅ 通过 | 后端连接正常(Backend Connected),6个项目显示,统计卡片(Total/SSSR/Active/Completed) |
+| Experience页面 | ✅ 通过 | 4个统计卡片(总案例/拓扑分布/平均评分/跟踪参数),筛选器(搜索/拓扑/标签/评分),操作按钮(Import/Find Similar/Refresh) |
+| Import from Plan | ✅ 通过 | 弹窗含方案选择/Tags/Rating/Auto Conclusion开关;选择Test Airgap Scan方案→导入4个案例→自动生成结论(如"avg torque 2.800 Nm; efficiency 86.0%; good overall performance")→成功消息"Imported 4 cases (skipped 0)" |
+| Edit经验案例 | ✅ 通过 | 弹窗含拓扑/结论/标签/评分;修改结论为"Manually verified..."→保存→成功消息"Case updated successfully" |
+| Dashboard仪表盘 | ✅ 通过 | 项目选择→方案选择→统计卡片(Total Plans 1, Results 4/5, Best Eff 86.5%, Best Torque 3Nm)→趋势图(Airgap→Average Torque)→Pareto图(Total Losses vs Efficiency)→敏感性图(Average Torque目标)→结果数据表(4个OK点,参数+指标+状态列) |
+| Find Similar检索 | ✅ 通过 | 弹窗含8个参数输入(Airgap/RMSCurrent/MagnetThickness/PolePairs/OuterRadius/InnerRadius/Speed/TurnsPerCoil)+拓扑+结果数;输入Airgap=1.0,RMSCurrent=20→检索→找到1个100%匹配案例→显示相似度进度条+共享参数(Airgap,RMSCurrent)+结论 |
+
+### 验收结论
+**Phase 2 全部功能验收通过。**
+
+- Phase 1(最小闭环):M1-M4全部完成并验证
+- Phase 2(Web端方案系统):P2-M1~P2-M5全部完成并验收
+- 双系统(Web端+本地EXE)API联调验证通过
+- 经验库闭环(仿真结果→自动导入→相似检索→反馈推荐)验证通过
+- 结果分析仪表盘(趋势/Pareto/敏感性/统计)验证通过
+
+### 后续规划(Phase 3)
+- AI驱动的方案优化(结合经验库数据做智能推荐)
+- 真实Motor-CAD端到端联调测试
+- 多用户/权限管理
+- 部署与运维(Docker化、CI/CD)
+- 性能优化(大数据量下的图表渲染和查询)
+
+### P2-M5 验收标准
+- [x] 后端API验收测试通过(23/23)
+- [x] 双系统联调集成测试通过(55/55)
+- [x] 分析服务单元测试通过(6/6)
+- [x] 前端构建通过
+- [x] ASCII源码检查通过
+- [x] 前端人工验收通过(6项功能全部验证)
+- [x] README.md更新至V1.0
+- [x] Phase 2 验收结论明确
+
+---
+
+## 2026-08-30 P5-M1:前端 build 清零(backlog B1)
+
+**背景**:新会话开工 P5,基线 e6563fc(工作区仅用户自建论文目录未跟踪)。全量回归 14/14 绿(test_api_client 需后端属环境依赖)。
+
+**决策**:
+1. 修复策略:`src/api/index.ts` 响应拦截器运行时已 unwrap `.data`,把 axios 实例类型改写为 `UnwrappedApi`(get/post/put/delete 返回 `Promise<T>`,默认 any),一处修复覆盖 69 处 TS2339/TS2345;`PlanDetail.vue` 的 `sel` 显式标注 `any[]` 消除 TS7006。运行时行为不变(纯类型断言)。
+2. 前端运行时验证:后端 uvicorn + `test_api_client.py` PASSED + vite dev + `/api` 代理 200。
+3. **Git 环境坑**:从仓库根目录执行带子目录路径的 git 命令(add/hash-object/diff/checkout)永久挂起(CPU≈0 等 IO,30s 不返回);规避:cd 到目标子目录内用相对文件名执行(秒回)。已记入 KNOWLEDGE_BASE.md。本次提交用「分目录 add」完成。
+
+**产物**:commit `b394c39`(feat(p5-m1));README / TEST_RECORDS(TEST-016)/ 交接文档同步。
+
+
+## 2026-08-30 P5-M2:EXE 配置化(config.json)+ mock 分支修复 + EXE 端到端验证
+**背景**:P5-M1 后 HEAD=f053b50。P5-M2 目标:EXE 配置化(web/model/log/实例数)+ 端到端回传验证。环境确认:用户常驻后端(uvicorn 0.0.0.0:8000,PID 30496)+ 常驻执行器(PID 39420)在跑——端到端验证用独立后端 8010 + 临时 DB,未干扰常驻服务。license server 1055 在跑,但用户常驻执行器占用轮询,真实 COM 端到端标注待目标机。
+**决策**:1. 新增 scripts/executor_config.py(配置加载器:--config > $EXECUTOR_CONFIG > EXE 目录侧车 > 仓库根模板 > 默认;env 单字段覆盖;相对路径按 EXE 目录/仓库根解析;校验)。 2. 仓库根 executor_config.json 模板;run_task_executor.py 支持 --config/--instances/--mock/--log-*(版本 1.1.0);parallel 复用共享逻辑。 3. 修复 P4-M3 遗留 bug:MotorCADTaskExecutor._run_simulation_point 忽略 enable_mock 无条件走真实 adapter → 加 mock 分支(source=mock)。 4. EXE 端到端(mock)全链路验证成功:独立后端 8010 → EXE(--config 侧车)认领 3 点 → mock 求解 → 回传 → completed(tavg 34.56~42.05 N·m、eff 87.95~89.73%)。 5. 重新打包 dist/PCB-AFM-Executor.exe(1.1.0,--version/--self-test 通过)。
+**坑**:EXE(frozen)相对路径按 EXE 目录解析(侧车需绝对路径或相对 EXE);PowerShell 多行 Replace 中文锚点不稳 → 用 Python newline="" 保留 CRLF 精确替换。
+**产物**:commit feat(p5-m2)(executor_config.py / executor_config.json / run_task_executor.py / run_task_executor_parallel.py / task_executor.py / test_executor_config.py / test_executor_p5m2.py / README / TEST_RECORDS TEST-017 / P1-P4回顾与P5规划 / CONVERSATION_LOG)。
+
+## 2026-09-01 · 方法论框架升级(Playbook V2 融合落地)
+**用户要求(要点)**:学习 ai-collab-dev-playbook.md,结合其框架与本项目框架,产出更完整的方法论框架 V2(用于下个项目 + 本项目按新框架执行)。
+**本次完成**:
+1. 新增 ai-collab-dev-playbook-v2.md(五支柱 + 五步启动法 V2,融合两项目经验)→ 根目录
+2. 新增 docs/HANDOFF.md(接续指南 + 接续提示词)→ 补全五支柱之「接续」支柱
+3. 新增 scripts/check_machine_paths.py(环境体检,只读 + --fix 打印修复命令)→ 换机第一条命令
+4. AGENTS.md 接入新框架:加 HANDOFF/playbook-v2 引用、环境体检命令、会话日志强制、反模式自查表、接续与交接节
+5. 本文件(CONVERSATION_LOG)头部补统一模板说明并追加本次记录
+**用户纠偏**:无
+**遗留问题 / 待确认**:
+- README「最近更新」待补本次框架升级记录(下一步)
+- 新项目可直接复用 playbook-v2;PCB 项目对照第 8 节落地清单逐项对齐
+
+
+## 2026-09-01 · Playbook V2.1 通用化重构
+**用户要求(要点)**:把 playbook 做成不带项目信息的通用框架文档(MARS/PCB 仅作案例举例);新增版本更新记录,便于持续迭代。
+**本次完成**:
+1. ai-collab-dev-playbook-v2.md 通用化重构:主文移除具体项目路径/参数/工具名,改为通用占位;新增「版本更新记录」(V1.0/V2.0/V2.1 + 迭代约定);MARS/PCB 降级为附录「案例来源与参考实现」
+2. 主文所有「案例对照」标注来源,铁律/反模式/模板全部通用化
+**用户纠偏**:无
+**遗留问题 / 待确认**:
+- 文档后续迭代请在「版本更新记录」追加(版本号/日期/改动内容/影响)
+
+
+## 2026-09-02 · 文档体系整改(评审 P0/P1/P2 修复)
+**用户要求(要点)**:评审本项目文档并提出改进点;确认不影响程序运行后动手修复。
+**本次完成**:
+1. AGENTS.md:项目定位从"Phase 1 最小闭环"更正为 P6 现状(P1~P5 完成 + P6-M1 完成);阅读清单改为"最小必读集 + 按需查阅"分层,降低新会话上下文负担
+2. README.md 整体重写(714→约290行):头部状态补 P6;路线图修正 P4/P5 编号冲突与过期状态("Schema统一+EXE打包 规划中"实已完成);结构树对齐实际目录(afmcore 五模块/14 路由/33 脚本/models 50 文件);快速开始以 run_task_executor + executor_config 为主入口;API 表指标数 11→35 并标注单一事实源;修复路线图吞字行
+3. 新增根目录 CHANGELOG.md:承接全部历史更新记录(2026-09-01 ~ 2026-08-29 各期,含原 README 底部 P5-M2 拓扑感知节);README 只留摘要表
+4. 新增 docs/archive/:归档 4 份已完结计划文档(P3/P4_IMPLEMENTATION_PLAN、P3-评审响应、P1-P4回顾与P5规划)+ 索引 README;归档前已确认无代码引用(output/ 下一次性脚本除外,不入库)
+5. KNOWLEDGE_BASE §4.1:指标清单改为引用 src/afmcore/metrics.py(35 项单一事实源),消除 11/12/35 三处漂移;保留易错字段名示例
+6. TEST_RECORDS.md:头部日期 2026-08-30→2026-09-01;修复 0x0c 控制字符("full_factorial"吞 f,与历史 0x07/0x08 同类病灶)
+7. HANDOFF.md:文档索引补 CHANGELOG.md 与 docs/archive/ 行;TEST 编号 023→024
+8. 上手指南:目录树修正(plan_schema.py 归属 src/ 而非 afmcore/;补 HANDOFF/CONVERSATION_LOG/archive;TEST-024);修改处失效引用更新为 archive/ 路径
+9. README 项目纪律新增第 11/12 条:状态单一事实源=HANDOFF 第 3 节;粘贴文本入文档后必须检查控制字符
+**验证**:8 个改动文档脚本检查通过(控制字符清零 + UTF-8 有效 + 本地 md 链接无失效)
+**技术决策**:①"当前状态"收敛为 HANDOFF 单一事实源,README/AGENTS 只放链接,防止三处复制漂移复发;②CHANGELOG 只迁不复制(避免双份漂移);③归档而非删除(保留可追溯性)
+**踩坑**:同文件两处并行 Edit 会后写覆盖先写(本次 CHANGELOG 两处路径修改丢了一处),同文件编辑必须串行
+**遗留问题 / 待确认**:
+- 工作区存在前次会话未提交的代码改动(P5-M2 拓扑感知、P6-M1 前端重构相关),本次仅提交文档文件
+- CONVERSATION_LOG(700+行)/TEST_RECORDS(1068行)按月分卷归档为后续可选项,未在本次实施
+
+## 2026-09-02 · AI 一键生成方案报错修复(topology/search_strategy 校验失败)
+**用户要求(要点)**:点击"AI 一键生成方案"报错 `Invalid generated plan: topology not supported: 'AFIR'; search strategy not supported: 'full_factorial_grid'`,找原因并修复。
+**根因**:
+1. 前端 ProjectList.vue 新建/编辑项目的拓扑下拉框提供 AFIR/AFPM/TORUS(默认 AFIR),与单一事实源 src/afmcore/topology.py(SSSR/DRSS/SDSR)漂移 → 项目 topology 存为 AFIR。
+2. ai_plan.py generate_and_save 用 project.topology 覆盖 AI 已归一化的 topology(AI 会把 AFIR 映射回 SSSR),导致 plan_data.topology=AFIR 被 validate_plan_dict 拒绝。
+3. Kimi 模型偶发输出 search_strategy.method="full_factorial_grid"(非注册策略),plan_generator.convert_ai_plan_to_unified 只兜底"缺 method"、未兜底"method 非法",非法值透传致校验失败。
+**本次完成**:
+1. ProjectList.vue:拓扑选项 AFIR/AFPM/TORUS → SSSR/DRSS/SDSR,默认 SSSR(5 处)
+2. plan_generator.py:convert_ai_plan_to_unified 增加 search_strategy.method 白名单归一化(基于 src.afmcore.strategies 单一事实源),非法回退 full_factorial + warning
+3. ai_plan.py:generate_and_save 增加 topology 归一化兜底(基于 src.afmcore.topology),非法回退 SSSR + warning
+**验证**:函数级测试(非法 method 回退 + 合法 lhs 保留)PASS;真实端到端调用 AFIR 项目 generate-and-save 返回 HTTP 200,topology 归一化 SSSR、method 归一化 full_factorial,warnings 正确提示。
+**技术决策**:以 src/afmcore 为单一事实源做归一化,而非在 web 层新增第二份白名单;前端硬编码拓扑选项对齐(P6-M2 完整方案待做 API 驱动)。
+**遗留问题 / 待确认**:
+- 已有 3 个 AFIR 测试项目(id 9/10/11)依赖后端兜底运行;前端已修,新建不会再产生 AFIR。
+- AI 仍会输出非标准变量名(如 Stator_Outer_Diameter_mm、Turns_Per_Coil)触发 warning,属另一问题,未在本次修复。
+
+## 2026-09-03 · Web 优化 P0 落地(Kimi max_tokens + AI 生成摘要对话框)
+**用户要求(要点)**:先给 web 页面优化建议(V2 评审),确认后"你开始吧"。
+**评审结论**:P6-M1(B1/B2)已落实;未落实项集中在 P6-M2(参数目录)/ P6-M3(流程引导);新增 P0 发现 3 个实测问题。
+**本次完成(P0 三项)**:
+1. **P0-2 Kimi max_tokens 撞顶修复**:ai_client.chat() 默认 max_tokens 改用 KIMI_MAX_TOKENS(原 plan_generator 写死 2000,k3 reasoning_content 吃满致 content 空→解析失败,对应用户"超时"截图隐藏根因);plan_generator 两处同步;新增 finish_reason=length+content 空的 logger.warning。
+2. **P0-1 AI 生成摘要对话框**:ProjectDetail.vue 生成成功后弹结构化对话框(方案概要 + 系统调整 warnings 逐条 + AI 设计思路折叠 + 查看方案/留在本页),替代原只显示第一条的 ElMessage——解决"黑盒裁剪工程师不知情"信任问题。
+3. **P0-2b 等待体验**:loading overlay 加"已等待 N 秒"计时(AbortController 取消本已存在)。
+**验证**:py_compile+ASCII 通过;vue-tsc 0 错误;端到端 generate-and-save HTTP 200,最新 ai_call_log completion_tokens=650 未撞顶、content 完整。TEST-026。
+**技术决策**:摘要对话框数据直接用后端 generate-and-save 响应(plan_data/warnings/ai_reasoning),无需后端新字段。
+**遗留问题 / 待确认**:
+- P0-3(仿真前检查清单)/ P0-4(TaskManager 方案下拉)/ P1(参数目录统一 + D6 调用链验证)未做,待用户确认继续。
+- AI 偶发非标准变量名(Slot_Depth 等)被裁剪,独立问题。
+
+## 2026-09-03 · Web 优化 P0-3/P0-4 落地(仿真前检查清单 + 任务自动展开)
+**用户要求(要点)**:继续后续待办("Please continue")。
+**本次完成**:
+1. **P0-3 仿真前检查清单**:plans.py 新增 `GET /{plan_id}/preflight`(5 项:模型文件/变量名确认/扫描变量/数据点规模/执行器在线,fail 阻断 warn 提示;为守 ASCII 约束只返回 key/status,中文 label 前端映射);PlanDetail 启动仿真改为先弹检查清单对话框,fail 禁用启动,确认后才 start-simulation。
+2. **P0-4 TaskManager 任务自动展开**:tasks.py `POST /api/tasks` 的 plan_data/parameters 改可选,仅传 plan_id 时自动加载方案并 `_expand_plan_to_parameters` 展开(复用 start-simulation 同一逻辑);前端 doCreate 未编辑 JSON 时省略字段由后端展开。
+**踩坑**:preflight 初测 model_path fail——`models/xx.mot` 相对后端 cwd(web/backend)解析失败,修复为用 config.PROJECT_ROOT 解析相对路径。
+**验证**:py_compile+ASCII 通过;vue-tsc 0 错误;preflight 复测 ok=true;tasks 自动展开 total_points=3 正确。TEST-027。
+**技术决策**:preflight 检查项后端只返回机器可读 key/status/data,中文 label/message 前端映射(守 .py 纯 ASCII 铁律);executor 离线设 warn 不阻断(任务可排队)。
+**遗留问题 / 待确认**:P1(参数目录单一事实源 + D6 调用链验证 current_a vs rated_current_a,可能数据一致性 Bug)未做。
+
+## 2026-09-03 · D6 边界条件 key 别名桥修复(P1 之 Bug 止血)
+**用户要求(要点)**:继续 P1("Please continue")。
+**D6 调用链验证结论(确认为真 Bug)**:BC key 三套口径漂移——①ProjectDetail 录入 + rule_engine 消费用 current_a/speed_rpm/slots/voltage_v/cooling_type;②fixed_params_template.build_default_fixed_params 读 rated_current_a/rated_speed_rpm/slot_count/dc_link_voltage_v/cooling_method;③PlanDetail BC_TEMPLATE 展示用 rated_* 另一套。导致用户在项目边界表单填的电流/转速/槽数/电压/冷却方式不流入固定参数推断(落模板默认 21A/5000rpm/13.5V 等)。另 Magnet_Temperature/Max_Speed 无 BC 推断。
+**本次完成(止血,非完整 P1-1)**:build_default_fixed_params 入口加 BC key 别名桥(canon 优先、别名补填、拷贝不改调用方 dict),补 Magnet_Temperature/Max_Speed 推断。
+**验证**:函数级 11 断言 PASS(前端 key 流入 + 旧 key 兼容 + 不改调用方 dict);端到端 project 9 generate-and-save,固定参数全部取自 BC(RMSCurrent=20/Shaft_Speed=3000/Slots=12/DC_Link=48/Magnet_Temp=40/Cooling=Natural/Max_Speed=6000)。TEST-028。
+**技术决策**:选"别名桥"而非改 elif 链——最小改动、向后兼容旧 rated_ key、不破坏 rule_engine;以 rated_ 为 canon、current_ 为别名(保持模板内部一致)。
+**遗留问题 / 待确认**:完整参数目录单一事实源(P1-1:PlanDetail BC_TEMPLATE 对齐 + rule_engine + 参数目录 API + 数据迁移)为大重构,需专门设计,本轮未做。
+
+## 2026-09-03 · P1-1 第一阶段:BC 字段目录单一事实源落地
+**用户要求(要点)**:继续 P1-1("Please continue")。
+**确认的事实**:①BC key 三套口径漂移(ProjectDetail/rule_engine=current_a 系、fixed_params_template=rated_ 系、PlanDetail BC_TEMPLATE=第三套 rated_);②所有方案 plan.boundary_conditions 均为 None → PlanDetail"边界条件"展示为模板错位+数据源空的死功能。
+**本次完成(P1-1 第一阶段)**:
+1. 新建 bc_fields.py:BC_FIELD_CATALOG(25 字段,规范 key 对齐录入端/rule_engine,\uXXXX label/unit/category/aliases)+ normalize_bc(别名→规范、规范优先、不改调用方)。
+2. generation.py 新增 `GET /api/bc-fields` 暴露目录。
+3. ai_plan.py(AI 生成)+ plans.py(手动创建):归一化后的 project BC 存入 plan_data.boundary_conditions(修活展示数据源)。
+4. PlanDetail.vue:删硬编码 BC_TEMPLATE,改从 /api/bc-fields 目录渲染(修复展示错位)。
+**踩坑**:Write 工具把 `\uXXXX` 落成实际中文字符(非 ASCII)+ docstring 字面 `\uXXXX` 触发 truncated escape → 用 `encode('ascii','backslashreplace')` 脚本统一转义 + docstring 避字面 `\uXXXX`。
+**验证**:5 个 .py 编译+ASCII 通过;normalize_bc 功能正确;vue-tsc 0 错误;/api/bc-fields 25 字段正确;端到端 plan.boundary_conditions 完整保存 25 规范 key。TEST-029。
+**技术决策**:规范 key 以录入端/rule_engine(current_a 系)为准,rated_ 降级为别名(读取兼容);label 用 \uXXXX 守 ASCII 铁律。
+**遗留问题 / 待确认**:P1-1 第二阶段(前端 ProjectDetail 表单从目录渲染、rule_engine/template key 彻底统一、已存数据迁移、scan+BC 目录合并)未做。
+
+## 2026-09-03 · P1-1 第二阶段:BC key 读取统一走 normalize_bc
+**用户要求(要点)**:继续 P1-1 第二阶段("Please continue")。
+**范围决策**:前端 ProjectDetail 表单 key 已与目录一致(第一阶段设计目录时即对齐 bcFields),无需改;真正残留是后端双重转换(normalize_bc 输出 current_ 系,build_default_fixed_params 又用本地 _ALIASES 桥转 rated_)。
+**本次完成**:
+1. fixed_params_template.build_default_fixed_params:改为 `bc = normalize_bc(...)`,删除本地 rated_ 别名桥,elif 改读规范 key(current_a/speed_rpm/slots/voltage_v/cooling_type)。
+2. rule_engine.BoundaryConditions.from_dict:加 normalize_bc,旧 rated_ 数据正确解析。
+**效果**:BC key 别名处理收敛到 bc_fields.normalize_bc 单一点,固定参数推断与 rule_engine 统一读规范 key,消除双重转换。
+**验证**:3 个 .py 编译+ASCII 通过;统一性 14 断言 PASS(current_ 流入 + rated_ 兼容 + rule_engine 解析 rated_ + 不改调用方);运行时 import 无循环;端到端固定参数推断仍全对。TEST-030。
+**技术决策**:已存 DB 的 rated_ 数据由 normalize_bc 读取时兼容,不物理迁移(避免改用户数据);scan 变量目录与 BC 目录为不同维度,保持分离;前端表单已一致不改。
+**遗留问题 / 待确认**:P1-1 实质完成(目录/API/存 BC/展示/读取统一均已落地);剩余为可选深化(scan+BC 目录是否合并、前端表单完全目录驱动)。
+
+## 2026-09-03 · P2-1 耗时校准 + P2-3 AFIR 非标准拓扑标记
+**用户要求(要点)**:继续 P2("Please continue")。
+**本次完成**:
+1. **P2-1 耗时校准**:analytics.py 新增 `GET /api/analytics/solve-time-stats`(基于 simulation_results.solve_time_s 实测);PlanDetail estimatedTimeMin/estimatedRemaining 改用实测 avg(fallback 2.5 分钟/点),加"基于最近 N 次实测 ~Xs/点"依据。数据:11 条实测 avg=125.9s(硬编码 2.5 分钟/点高估 ~19%)。
+2. **P2-3 AFIR 标记**:ProjectList 非标准拓扑(非 SSSR/DRSS/SDSR)项目 tag 改 warning 色 + 警告图标 tooltip,不改 DB(后端已有 AFIR→SSSR 兜底)。
+**验证**:analytics.py 编译+ASCII 通过;vue-tsc 0 错误;solve-time-stats 返回正确(count=11 avg=125.9);AFIR 项目 id 9/10/11 被识别。TEST-031。
+**遗留问题 / 待确认**:P2-2(步骤条交互)/ P2-4(结果表列配置)价值中低未做;AI 偶发非标准变量名被裁剪(独立问题,每次生成触发 warning/裁剪,建议后续专项)。
+
+## 2026-09-03 · AI 扫描变量归一化 + 注册表扩充
+**用户要求(要点)**:继续("Please continue"),处理 AI 非标准变量名导致维度裁剪问题。
+**调查(ai_call_logs 证据)**:AI 高频推荐但非标准变量 = Turns_per_Coil(9 次)/ Stator_Outer_Diameter(9 次)/ Slot_Depth(3 次)/ Stator_Inner_Diameter / Wire_Diameter,均为 fixed_params_template 有效 Motor-CAD 变量,仅因不在 SCAN_PARAMETERS(原 8 个)被误判裁剪。
+**本次完成**:
+1. rule_engine.SCAN_PARAMETERS 8→13:新增上述 5 个变量,范围锚定 MARS 基线(198/122/7/20/1.63)±20-50%。
+2. plan_generator._VARIABLE_NAME_MAP 补 25 个 AI 变体名映射。
+**验证**:编译+ASCII 通过;归一化 16 断言 PASS(12 变体名归一化 + 不再"not a standard"warning + 变量保留);注册表 8→13;端到端 Stator_Outer_Diameter 保留为扫描维度,"非标准变量"warning 消失。TEST-032。
+**技术决策**:范围锚定 MARS 基线 ±20-50%(非瞎编,注释标注依据);点数超限(MAX_POINTS=200)裁剪是合理保守行为,保留维度需改用 lhs/adaptive 采样(另一独立优化,未做)。
+**遗留问题 / 待确认**:点数超限时切换采样策略(保留维度)作为可选深化;其余优化项已全部完成。
+
+## 2026-09-03 · 结果表列配置(P2-4)
+**用户要求(要点)**:继续("Please continue")。
+**本次完成**:PlanDetail 结果表列配置——默认显示核心 5 列(平均转矩/脉动/效率/总损耗/状态)+ 扫描变量列,35 项指标通过"列设置"checkbox 对话框按需勾选,支持"恢复默认"。解决指标扩展后结果表全部铺开拥挤问题(D14)。
+**验证**:vue-tsc 0 错误;前后端服务正常(纯前端改动,Vite 热更新)。TEST-033。
+**说明**:本轮评估过用 agent-browser 做 UI 实测截图,但需下载 ~500MB Chromium,且用户本机前后端已在运行可直接查看 UI,性价比低,故改为完成 P2-4 这个最后有明确用户价值的剩余项。
+
+## 2026-09-03 · UI 评审实测修复(ui-ux-pro-max + 截图)
+**用户要求(要点)**:用 ui-ux-pro-max 技能评审界面,先给方案批准;批准后 continue。
+**评审方法**:加载 ui-ux-pro-max(Data-Dense Dashboard 框架 + UX 指导);agent-browser 下载 Chromium 超时(googleapis),改用系统 Edge headless + 自写 CDP 截图脚本实测 5 页面,逐张分析。
+**评审发现与修复(批次 A+B+C,全部截图验证)**:
+1. **P0-1 PlanDetail 统计卡全 0**(读 plan.scan_variables 不存在,实为 plan_data.variables)→ 4 处改读 plan_data.variables;**P0-1b** calcPoints 不认 values 数组(plan 1)→ 优先 values.length。
+2. **P1-1 Dashboard**:已选方案却提示"请选择方案";默认选第一个(常无结果);结果加载漏 .items → 文案区分 + 默认选 result_count>0 + 加 .items。
+3. **P1-2** BC 25 字段全铺开 → 空值折叠;**P1-3** ProjectList 表格 size=small。
+4. **P2-2** 面包屑补项目名(MainLayout 加载);**P2-4** 标题区分隔。
+**验证**:截图回归(统计卡 0→1个/3点/6分钟、BC 折叠、面包屑项目名、Dashboard 0→6点);vue-tsc 0 错误。TEST-034。
+**踩坑沉淀**:前后端字段名不匹配是反复出现的问题家族(scan_variables vs plan_data.variables、results 的 .items vs .results、calcPoints 不认 values);agent-browser 国内下载 Chromium 超时,Edge headless+CDP 脚本是可靠替代。
+**遗留**:失败结果无错误信息列;PlanDetail 参数编辑无保存逻辑;P2-1 监控合并 / P2-3 批量清理待确认。
+**遗留问题 / 待确认**:P2-2(步骤条交互)价值低未做;可选深化(点数超限采样、列配置持久化 localStorage)。全部既定优化项(P0/P1/P2 + D6 + AI 变量归一化)已完成。
+
+## 2026-09-03 · 失败结果错误信息列
+**用户要求(要点)**:继续处理遗留项("Please continue")。
+**本次完成**:结果表失败行显示具体错误原因——PlanDetail 结果表 status 列 FAILED 加 el-tooltip(悬停显示 error_message,虚线下划线标识)+ error_message 加入列设置可选列;Dashboard resultColumns 加 error_message 列(直接显示)+ status tooltip。实证 /plans/6/results 失败原因 "Could not find Outer_Rotor_Diameter"(印证 HANDOFF 变量名未验证问题)。
+**验证**:vue-tsc 0 错误;截图 v_dashboard_err.png 确认错误信息列显示完整原因。TEST-035。
+**遗留**:Outer_Rotor_Diameter 等未验证变量名需按 HANDOFF 在 .mot 确认后修正 fixed_params_template(独立于 UI);PlanDetail 参数编辑无保存逻辑;P2-1 监控合并 / P2-3 批量清理待确认。
+
+## 2026-09-03 · MARS 变量名实测排查 + fixed_params_template 几何修正(仿真失败根因)
+**用户要求(要点)**:继续排查变量名根因("Please continue")。
+**根因确认**:仿真点全 FAILED 因 `set_variable: Could not find Outer_Rotor_Diameter`——模板几何变量用径向电机命名,与 MARS(PCB 无铁心轴向磁通 SSSR)不符。
+**方法**:解析 .mot 静态比对 + pymotorcad get_variable 探测 + set_variable 回读校验(KNOWLEDGE_BASE §8 + 铁律)。
+**本次完成**:
+1. 修正 4 个几何 motorcad_var:Outer_Rotor_Diameter→RotorOuterDiameter(130)、Stator_Outer_Diameter→Stator_Lam_Dia(76)、Stator_Inner_Diameter→Stator_Bore(50)、Rotor_Back_Iron_Thickness→Back_Iron_Thickness(5),get/set 均 OK。
+2. Inner_Rotor_Diameter(转子内径,4 候选全 MISS)与 Stator_Yoke_Thickness(定子轭厚,PCB 无铁心)设 motorcad_var=null(不写入不报错)。
+3. 几何默认值从径向模板值更正为 MARS 实测值(130/76/50/5)。
+4. KNOWLEDGE_BASE §3.3 新增 MARS 实测变量名对照表(知识沉淀)。
+**验证**:get 探测 22 名 OK / 模板 12 错误名全 MISS / Magnet_Arc_[ED]=121 OK;set+回读 4 名 ALL WRITABLE;py_compile+ASCII 通过。TEST-036。
+**遗留**:模板修正需重启后端;建议重新生成方案并真实仿真端到端复验(确认不再报 Could not find variable);其他拓扑(DRSS)变量名需另行实测。
+
+## 2026-09-03 · 端到端真实仿真验证(变量名修正终极复验)
+**用户要求(要点)**:协助真实仿真复验("Please continue")。
+**方法**:写 pymotorcad 脚本模拟执行器完整流程(build_default_fixed_params 生成 37 参数 → set 全部可写参数 → do_magnetic_calculation 真实求解 → export 提取指标)。
+**迭代排查(3 轮端到端)**:
+1. 首轮 set 31/33 ok,揪出 2 个补充错误名:`Current_Advance_Angle`、`Material_Stator_Lam_Yoke`(几何 4 个已修正 OK)。
+2. 补修:`Current_Advance_Angle→PhaseAdvance`(实测 OK);`Steel_Grade` 设 null(MARS 无铁心)。二轮 set 32/32 ok 但求解失败——`NdFeB_N42SH` 材料库不存在(材料值错误)。
+3. 材料修正:MARS `.mot` 实测 `Material_Magnet=N42UH`,模板默认改 `N42UH`。三轮端到端全链路成功。
+**最终验证**:32 可写参数 set 全成功(0 failed);do_magnetic_calculation 求解成功(~2 分钟);导出 337 行 CSV,关键指标有效(转矩脉动 2.815%、AC 损耗 0.47W、功率因数 0.995)。
+**结论**:仿真失败根因(变量名 + 材料名)彻底修复,模板所有 motorcad_var 经实测验证。TEST-037。
+**遗留**:建议 Web 端用本地执行器跑完整方案扫描复验生产链路;其他拓扑/模型变量名需另行实测。
+
+## 2026-09-03 · 生产链路复验(Web→任务→执行器真实仿真→回传)
+**用户要求(要点)**:协助生产链路复验("Please continue")。
+**过程(链路逐段排障,3 个新修复)**:
+1. 执行器上线(enable_mock=false)。start-simulation 预检 400——`_KNOWN_VARIABLES[SSSR]` 含错误几何名,误判修正后的正确名 unknown。**修复1**:topology_variable_map 的 `_KNOWN_VARIABLES` + alias map 几何名更正为 MARS 实测名。
+2. 预检通过、任务 dispatched,但执行器 `not claimable`——start-simulation 已 pending→dispatched,执行器 claim 时再 dispatch 被后端拒绝(仅 pending→dispatched)。**修复2**:task_executor 对 dispatched 任务跳过重复 dispatch。
+3. 执行器捡起但点失败 `No module named 'scripts'`——afmcore.adapters.motorcad 的 `from scripts.robust_motorcad import` 缺仓库根 sys.path。**修复3**:motorcad._ensure_solver 导入前确保仓库根在 sys.path。
+**最终验证**:任务 completed,successful_points=1,failed_points=0;tavg_nm=0.5219 Nm、efficiency=86.06%、total_losses=41.945 W(与 pymotorcad 单点一致)。**完整生产链路(Web 生成→预检→下发→执行器真实求解→回传)全部通畅**。TEST-038。
+**遗留**:多点扫描方案生产复验;其他拓扑/模型变量名需实测登记。
+
+## 2026-09-03 · 多点扫描生产复验(3 点 Airgap)
+**用户要求(要点)**:做多点扫描方案最终确认("Please continue")。
+**本次完成**:创建 plan 28(Airgap 0.6/1.0/1.5mm,3 点)→ start-simulation → 执行器逐点真实求解。
+**验证结果**:task c2781acf completed,successful_points=3,failed_points=0;批量调度正常(进度 33.3%→66.7%→100%);逐点落盘。
+**逐点指标(物理趋势验证)**:Airgap 0.6/1.0/1.5 → tavg 0.566/0.522/0.468 Nm、eff 84.9/86.1/86.3%、losses 49.9/41.9/36.6 W。趋势符合电磁学(气隙↑→转矩↓损耗↓效率↑),结果真实有效。
+**结论**:多点扫描生产链路完全通畅,从"仿真失败根因(变量名)"到"多点生产链路全通畅"彻底闭环。TEST-039。
+**遗留**:用户可在前端用 AI 生成多变量方案体验完整 AI→仿真→分析闭环。
+
+## 2026-09-03 — UI 评审遗留项全部修复
+
+**用户要求**:对 UI 评审剩余遗留项"全部开始修复"。
+
+**本次完成**(4 项,commit 待提交,TEST-040):
+1. **PlanDetail 扫描变量编辑持久化**:添加/删除置 dirty,"保存修改"按钮 PUT 完整 plan_data(后端单一事实源校验);修复了此前"编辑只改前端状态、刷新丢失"的缺陷。
+2. **P2-1 监控页合并**:新建 ExecutionMonitor.vue(任务监控/执行器状态双 Tab,v-if 仅活动 tab 轮询);`/executor-monitor` 重定向 `/monitor`;导航"执行"组减为两个入口。
+3. **P2-3 批量清理**:后端 projects/tasks 批量删除 API(逐项容错;任务仅终态可删,活动任务 400 拒绝);前端两列表多选 + 条件批量按钮 + 二次确认;任务行级删除按钮(仅终态)。API 实测终态保护通过。
+4. **P2-4 深化**:结果列配置持久化 localStorage(`afm:plan-result-columns` 全局偏好)。
+
+**踩坑**:前端两处 script Edit 静默未生效(old_string 失配),vue-tsc 类型检查报错发现后补回——类型检查再次拦截静默失效。
+
+**验证**:vue-tsc 0 错误;后端 3 文件编译 + ASCII 通过;批量删除 API curl 实测;Edge CDP 截图回归 /monitor、/tasks、/projects。
+
+**遗留**:无。UI 评审全部项闭环。P2-2 步骤条交互维持"价值低不做"。
+
+## 2026-09-03 — 文档同步与前次会话遗留收尾
+
+**用户要求**:"Please continue"——继续收尾。
+
+**本次完成**:
+1. **前次会话遗留入库**(commit `3dd42b9`):`src/plan_schema.py`(字符串枚举支持)+ `style.css`(P6-M1 设计令牌)+ 两个未跟踪测试脚本。`test_plan_schema.py` 7/7 PASS 直接入库。
+2. **测试脚本断言修正**(TEST-041):`test_topology_variable_map.py` 断言基于旧错误变量名,TEST-036 修正后 6/8 失败(预期);更新断言至 MARS 实测名(Stator_Lam_Dia/Stator_Bore/RotorOuterDiameter),新增"旧错误名判 unknown"反向防回归断言,更新后 8/8 PASS。
+3. **文档同步**:README.md"最近更新"+里程碑表补 9-02/9-03 全部工作(P6-M2/M3 标记完成);HANDOFF.md 第 3 节进度/阻塞/待办全面更新(移除已解决的"5 个未验证变量名"阻塞项,新增变量名根因修复与生产链路通畅记录),第 4 节接续提示词同步。
+
+**踩坑**:Edit 工具对 test_topology_variable_map.py 3 处替换静默失效(报告成功但内容未变),grep 复查 + 重跑测试发现。教训:Edit 后必须 grep 验证。
+
+**遗留**:DRSS/SDSR 拓扑变量名待实测登记;热求解真实验证待模型;前端实机走查待用户回归。
+
+## 2026-09-03 — 用户实测反馈四需求落地
+
+**用户要求**(基于实测截图):①启动仿真预检 model_path 空阻断,要求拓扑基础模型库自动调用(SSSR/DRSS/SDSR)②新建项目弹窗 BC 只 6 项应扩到 25 项全显示、默认未设置,详情页全展开 + 用户指定/AI 补充/已修改来源标签 ③概览验收标准按 BC 自动带出判断条件 ④方案参数页 BC/固定参数默认全展开 + 可编辑保存。
+
+**澄清**:DRSS/SDSR 无基础模型文件(仅 SSSR 有 MARS .mot),未编造——SSSR 自动调用,DRSS/SDSR 待用户提供模型后登记。
+
+**本次完成**(TEST-042):
+1. **拓扑默认模型**:topology.py 加 default_model 字段 + default_model_for();三处回退(AI 生成/手动创建/**start-simulation 运行时兜底回填并持久化**——16 个历史空模型方案直接可启动);预检失败项"使用拓扑默认模型"一键修复按钮。
+2. **全量 BC + 来源标签**:发现并修复 defaultBC 预填默认值隐藏 Bug(用户无法区分自填/默认)→ 改全空仅存非空;bc_fields 目录加 type/options 表单元数据;prompt 加 bc_suggestions(仅补未设置项)+ 扫描变量清单同步 13 项;bc_meta 来源追踪(user/ai/edited);新建弹窗 25 项分组渲染;ProjectDetail/PlanDetail BC 全展开+标签+编辑保存;冷却方式枚举值统一大写。
+3. **验收标准**:AI acceptance_criteria + BC 目标字段推导 7 类判断条件,带来源标签;修复 AI 嵌套 dict 格式丢失(convert 归一化 hard_constraints dict→list);修复 plan.acceptance_criteria/boundary_conditions 字段路径 Bug(实际在 plan_data 下)。
+4. **参数区展开编辑**:固定参数默认全展开 + inline 编辑保存;扫描变量"取值"列修复 values 数组显示(原 min/max/step 列对 AI 方案空白)。
+
+**验证**:5 后端文件编译+ASCII;vue-tsc 0 错误;端到端 model_path 回填/bc_meta/归一化通过;Edge CDP 截图 5 张核验(脚本扩展 js: 表达式点击)。
+
+**遗留**:DRSS/SDSR 基础模型待用户提供;AI bc_suggestions 标签待有未设置项场景实测。
+
+## 2026-09-03 — PROMPTS_DIR 路径 Bug 根因修复(AI prompt 从未生效)
+
+**起因**:bc_suggestions 三次生成均为空,深挖根因。
+
+**根因(重大)**:`web/backend/app/config.py` 的 `BACKEND_DIR = Path(__file__).parent` = `web/backend/app/`,而 `PROMPTS_DIR = BACKEND_DIR / "prompts"` 指向不存在的 `app/prompts/`(实际在 `web/backend/prompts/`)→ **真 prompt(generate.txt)从未被加载,一直走中文兜底 prompt**。这回溯解释了此前全部 AI 行为问题:变量名编造(OuterDia/Stator_Lam_Length)、acceptance_criteria 嵌套 dict、bc_suggestions 不输出——模型从未看到含 13 变量清单的真 prompt。
+
+**修复**:PROMPTS_DIR 改 `BACKEND_DIR.parent / "prompts"`;prompt 措辞改强制输出 bc_suggestions;ai_plan.py 项目上下文 BC 全量展开(未设置字段显式 null——此前未设置 key 直接缺失,模型无从知晓可补哪些);SCAN_PARAM_CN 补 4 个中文名。
+
+**验证(TEST-043)**:端到端 AI 补充 7 项 BC(标 ai,不覆盖 user 3 项);扫描变量全部注册标准名;warnings 干净;截图验证"用户指定/AI 补充"标签渲染。测试产物已清理。
+
+**沉淀**:KNOWLEDGE_BASE §1.2 加坑记录(改 prompt 后必须看 ai_call_logs.prompt_preview 验证实际加载)。
+
+**遗留**:历史方案(prompt 修复前生成)质量参差,建议关键方案重新生成;experience/extract.txt 不存在(既有状态)。
+
+## 2026-09-03 — 新 prompt 质量验证 + extract.txt 补建
+
+**本次完成**(TEST-044):
+1. **新 prompt 质量对比**:project 13 端到端生成——扫描变量全标准名、验收标准规范 list 4 条、warnings 干净(对比修复前:编造名/嵌套 dict/多警告)。
+2. **补建 experience/extract.txt**:该目录原为空,experience_enhancer 一直走单行 fallback;同步修其 max_tokens=3000 撞顶隐患(改 KIMI_MAX_TOKENS)。函数级真实 AI 调用验证(plan 28 三点数据):输出结构完整、规则带量化证据、样本量保守声明正确,PASS。
+3. 途中发现前端 Vite 进程挂了(502),已重启恢复。
+
+**说明**:smart-extract 端点为规则化提取不走 AI;extract.txt 生效路径是 adaptive_loop 自适应闭环(待执行器在线后端到端实测)。
+
+**遗留**:自适应闭环端到端实测(待执行器);历史方案质量参差。
+
+## 2026-09-03 — 自适应闭环集成修复(真 prompt 启用后暴露的断层)
+
+**用户要求**:"继续"——闭环 update-experience 是 extract.txt 的真实生效路径,做端到端验证。
+
+**发现并修复 3 个集成断层**(P3-M5 遗留,真 prompt 启用后才暴露):
+1. generate_plan 存 convert 后 `variables`,initialize_search 读 `scan_variables` → 归一化兼容。
+2. convert 输出 `start/stop/step`,initialize_search 读 `min_value/max_value` → 兼容 + values 数组推导。
+3. 闭环方案缺 topology/model_path → 拓扑归一化 + default_model_for 回填。
+
+**验证(TEST-045)**:generate-plan(topology=SSSR + 模型回填 + 标准变量名)✅;init-search HTTP 200 ✅;转换函数级 3 断言 PASS ✅。
+
+**发现第 4 个断层(未修,待办)**:L0 引擎期望 BC 风格参数名(airgap_mm),闭环传 Motor-CAD 名(Airgap)→ 全判不可行 → 初始批次空。需 Motor-CAD 名→L0 BC 名语义映射层(注意 Magnet_Length 轴向 ≠ magnet_thickness_mm 径向厚度,不能瞎对应)。
+
+**遗留**:L0 参数名口径映射为闭环关键待办;update-experience 闭环集成段草案待验证(extract_insights 本体已 TEST-044 函数级验证)。
+
+## 2026-09-03 — 自适应闭环全链路打通(L0 口径映射 + 3 个闭环 Bug 修复)
+
+**用户要求**:"继续"——修复 L0 口径断层(闭环关键待办)并做全链路实测。
+
+**修复**(TEST-046):
+1. **L0 参数名口径**:`src/afmcore/l0/prescreening.py` 加 `MOTORCAD_TO_L0` 语义映射(6 条,均语义严格一致:Magnet_Length=轴向磁通磁钢厚度方向尺寸→magnet_thickness_mm;Magnet_Thickness 径向深度无 L0 对应项不映射)。evaluate 入口翻译,L0 原生 key 优先。函数级 4 检查全 PASS。
+2. **初始批次不消费**:select_next_batch 开头先消费 pending 初始点。
+3. **report-results 路由缺失**:report_loop_results 无装饰器(P3-M5 遗留),已补。
+4. **export inf→JSON 500**:best_objective_value 导出转 None;import 时 None 保持 inf 哨兵(否则 `value > None` TypeError)。
+
+**端到端实测(全真实数据)**:闭环 2 批 8 点(Airgap×RMSCurrent)→ 执行器真实仿真 8/8 completed → report-results(results_analyzed)→ **update-experience 成功**(experience_updated,AI 提取 4 条带量化证据的设计规则,extract.txt 闭环真实生效)。趋势合理(电流↑→转矩↑效率↓)。export/import 检查点机制顺带实测通过。
+
+**踩坑**:任务构造 motorcad_var=None 参数用 name 回退写入导致 "Could not find"(修正:只写可写参数);next-batch 返回字段是 points 不是 batch。
+
+**遗留**:主动学习后续批次(信任域)未验证;loop 为内存态。
+
+## 2026-09-03 — submit-batch 生产路径验证
+
+**修复**:submit_batch_to_executor 补固定参数合并(原只传裸扫描点,执行器不读 plan_data.fixed_params)。
+**验证**:任务 f19ba592(12 点)参数合并正确(30 固定+扫描+point_id,无错误变量名),执行器捡起跑。✅
+**发现缺口(未修)**:执行器跑完不调闭环 /report-results(P3-M5 设计未实现段),闭环需手动桥接结果。
+
+## 2026-09-03 — 闭环生产路径全跑通 + 经验提取数据完整性修复
+
+**验证**(TEST-048):submit-batch 任务 12/12 全成功;12 点 report 入环(置信度 D→C,best 效率 86.3→89.4);update-experience 第二轮成功。
+**AI 反馈暴露缺陷**:all_results 只合 metrics 无输入参数 → 敏感性分析无法做。修复:①report_results 合入 point params ②_condense_results 经 MOTORCAD_TO_L0 翻译 Motor-CAD 输入名再提取。函数级验证 PASS。
+**验证边界**:修复影响未来闭环;函数级已证,端到端待下次闭环。
+**遗留**:执行器→闭环自动回传缺失(P3-M5 未实现段)。
+
+## 2026-09-04 — 执行器→闭环自动回传(P3-M5 最后缺口闭合)
+
+**修复**:task_executor 加 `_report_to_adaptive_loop`——adaptive_batch 任务完成后自动把结果 POST 到闭环 report-results(point_id/metrics/status 映射,best-effort 不影响任务上报)。
+**验证**(TEST-049):链路级——adaptive_batch 任务 3 点自动入环(n_results 0→3),scan 任务负面对照不触发。执行器已重启加载。
+**验证边界**:链路级连通性验证;全闭环自动流转的长时端到端(数十分钟真实仿真)未做,建议生产观察。
+**至此闭环全自动**:submit-batch→执行器→自动回传→主动学习下一批,无需手动桥接。
+
+## 2026-09-04 — 闭环经验入库(闭环价值闭环)
+
+**发现**:generate_experience_entry 只返回 dict 不入库,update_experience 从不持久化——闭环提取的经验重启即失、经验库页不可见、后续 AI 生成无法复用。
+**修复**:update_experience 加 _persist_experience_case(映射为 ExperienceCase 入库,best-effort)。
+**验证**(TEST-051):真实 12 点数据灌入 + update-experience → experience_case_id=7,experience_cases 6→7 行,/api/experience 可见。✅
+**意义**:闭环价值完整闭环——仿真→AI 提取→经验入库→后续 AI 生成复用。
+
+## 2026-09-04 — 经验库复用断裂修复
+
+**起因**:上轮声称"后续 AI 生成复用经验库"未实际验证,按零猜测原则补验。
+**发现(真实断裂)**:generate-and-save 不传 existing_experience、/generate 只在请求体显式传才用、前端不传 → 经验库的值根本没流入 AI 生成。
+**修复**:_load_experience_cases 按拓扑取最近 5 条;generate-and-save 自动加载;/generate 未显式传时自动加载(补 db 依赖)。
+**验证**(TEST-052):加载函数返回 5 案例;端到端生成 HTTP 200;附带实证真 prompt 生效(system prompt 为英文版)。prompt_preview 仅 500 字符截断在 system prompt——经验在 user message 中,日志不可见属截断非未加载。
+**意义**:经验价值链完整——仿真→提取→入库(TEST-051)→生成自动复用(本次)。
+
+## 2026-09-04 — 批次语义修复 + 全自动闭环真实端到端(零手动桥接)
+
+**发现**:submit-batch 提交全部 24 个 pending 点而非当前批次(select_next_batch 不标记选中点,仍 pending)。
+**修复**:引入 dispatched 状态——select_next_batch 选中点标 dispatched(不再被重复选/提交),submit_batch 只提 dispatched 当前批次,batch_summary 桶加 dispatched。函数级验证:pending 4→选 2 dispatched→pending 减 2→submit 正好 2。
+**真实全自动端到端**(TEST-053,核心):建闭环→AI 生成→选点→submit-batch(5 点)→执行器真实仿真 5/5→**自动回传**(全程零手动 report)→n_results 0→5 自动增加→update-experience 入库 case 8,**敏感性分析正常**(Current strong/Airgap moderate,TEST-048 修复真实生效),量化结论正确。
+**意义**:P3-M5 自适应闭环真正全自动,零手动桥接,完整实证。
+**遗留**:执行器在后端重启期间心跳中断需重启(可自愈优化,非阻断)。
+
+## 2026-09-04 — 执行器心跳独立线程
+
+**问题**:执行器心跳与任务执行同线程串行,长跑点(~2min)期间心跳停止被误判 offline(实测 30216 假死)。
+**修复**:start_polling 拆为独立 heartbeat_loop + poll_loop 两线程。
+**验证**(TEST-054):执行器重启上线且 15s 持续在线,旧执行器正常超时离线。
+
+## 2026-09-04 — 自适应闭环前端页
+
+**背景**:闭环后端已全自动实测通畅但无 UI 入口(adaptiveApi 存在于 ai.ts 但无页面使用,且缺 submitBatch 方法)。
+**实现**:新建 AdaptiveLoop.vue(创建表单+闭环列表+详情+一键自动运行状态机+经验提取),补 adaptiveApi.submitBatch,加路由 /ai/adaptive-loop + 导航"自适应闭环"入口 + 面包屑。
+**验证**(TEST-056):vue-tsc 0 错误;页面 PAGE_ERRORS(0);截图列表页+详情页正常。一键自动运行的各步骤 API 已在 TEST-045~053 单独实测,UI 串接待实机长跑确认。
+
+## 2026-09-04 — prompt_preview 截断修复 + 经验进入 prompt 实证
+
+**问题**:_log_call 存 messages[:3][:500],截断在 system prompt,user message(BC+经验)不可见,TEST-043/052 两次无法从日志确认 AI 实际输入。
+**修复**:prompt_preview 含全部消息、上限 500→8000;response_preview 1000→2000。
+**验证**(TEST-057):重新生成后日志含"参考经验案例(5个)"及完整 params/metrics——经验进入 prompt 实证。价值链最后一环闭合。
+
+## 2026-09-04 — 闭环 UI 端到端实测 + axios 超时根因修复
+
+**问题**:首次 UI 一键自动运行跳"运行中断"——方案已生成但前端报错。根因:axios 默认 30s 超时 < AI 生成约 40s,前端中止后端照跑,状态错位(aiPlanApi 早已设 180s,新 adaptiveApi 漏了)。
+**修复**:adaptiveApi 的 generatePlan/reportResults/updateExperience 补 180s 超时;startAutoRun 改断点续跑(按 phase 跳过已完成步骤)。
+**验证**(TEST-058):UI 驱动全闭环端到端实测——2 批次共 5 点全成功、最优 86.346、预算耗尽后自动提取经验(案例 9 入库)。执行器全程 online。TEST-056 验证边界闭合。
+
+## 2026-09-04 — 首次打通 Motor-CAD 热仿真(电磁→热链路)
+
+**用户需求**:加入 Motor-CAD 热仿真;先用 MARS 模型跑一个自动热仿真看效果;电磁仿真后把损耗用于热仿真一起做,节省时间。
+
+**核实(零猜测)**:先查 pymotorcad 源码(ansys.motorcad.core)确认真实 API,发现 P5-M6 遗留的 enable_thermal 用了两个不存在的接口:
+- `do_thermal_calculation()` 不存在 → 真实为 `do_steady_state_analysis()`(稳态)/ `do_transient_analysis()`(瞬态)/ `do_magnetic_thermal_calculation()`(磁热耦合)
+- `export_results("Thermal", ...)` 的 solution_type 无 "Thermal" → 真实为 "SteadyState"/"Transient"
+
+**实测**(TEST-060):写 `scripts/run_thermal.py`,MARS 模型跑通稳态热仿真——电磁 127.8s + 稳态热 6.0s。
+得到绕组平均 67.95°C、热点 74.59°C、磁钢 118.15°C、后轴承 88.47°C。
+
+**修复**:robust_motorcad.py 改两个 API;metrics.py 7 个热指标补实测字段别名(中英文),
+bearing_temp_c 映射到后轴承(轴向磁通电机热风险侧,前轴承仅 49.2°C)。
+
+**遗留(待用户确认)**:MARS 模型 `Ambient_Temperature=125`(环境 125°C 异常,辐射 40°C 正常),
+导致温升/热阻为负值。热仿真前需把环境温度修正为 25~40°C。
+
+**待办**:磁热耦合 do_magnetic_thermal_calculation 待实测;热仿真接入执行器 enable_thermal 全链路待验证。

+ 144 - 0
docs/HANDOFF.md

@@ -0,0 +1,144 @@
+# 接续指南 · PCB 轴向磁通电机自动化仿真系统
+
+> 面向**换人 / 换机 / 新会话**后继续工作的场景。
+> 配套框架:`ai-collab-dev-playbook-v2.md`(方法论)+ `AGENTS.md`(AI 行为准则)。
+> 最后更新:2026-09-01
+
+---
+
+## 1. 环境要求
+
+| 软件 | 版本 | 用途 | 必需性 |
+|---|---|---|---|
+| Motor-CAD | 2026R1 (v261) | 电磁/热/结构仿真求解 | 必需(仿真) |
+| Python | ≥ 3.10 | 执行器 / 后端 / 测试 | 必需 |
+| ansys-motorcad-core | 0.8.x | pymotorcad 自动化接口 | 必需(仿真) |
+| PySide6 / pandas | 最新稳定 | GUI / 数据处理 | 必需 |
+| fastapi / uvicorn / pydantic | 最新稳定 | Web 后端 | 必需(Web) |
+| Node.js / npm | ≥ 18 | 前端 Vue3 构建 | 必需(Web) |
+| Ansys License Manager | FlexNet | 许可证(lmgrd + ansyslmd) | 必需(仿真) |
+| Ansys Maxwell + PyAEDT | — | Maxwell 适配器(当前 mock) | 可选(待接入) |
+| JMAG + jmagpy | — | JMAG 适配器(当前 mock) | 可选(待接入) |
+| scipy/numpy | — | Kriging 代理(当前 IDW 降级) | 可选(待升级) |
+
+---
+
+## 2. 恢复步骤(第一条命令)
+
+```powershell
+# 1) 核对环境(只读体检,缺项会给修复建议)
+python scripts/check_machine_paths.py
+
+# 2) 验证环境变量(非登录 shell 可能不继承,见 docs/KNOWLEDGE_BASE.md §1)
+echo $env:MOTORCAD_ACTIVEX
+echo $env:ANSYSLMD_LICENSE_FILE
+
+# 3) 前端依赖(如需构建)
+cd web/frontend; npm install
+
+# 4) 后端启动(开发态,两终端)
+python -m uvicorn app.main:app --port 8000   # 在 web/backend 下
+
+# 5) 本地执行器(仿真端,enable_mock=true 时无需 Motor-CAD)
+python scripts/run_task_executor.py --config executor_config.json
+```
+
+---
+
+## 3. 当前进度(2026-09-03)
+
+### 已完成
+- **P1**:环境验证 + 单工况仿真 + 参数扫描引擎 + 方案 JSON 接口 + PySide6 GUI + 经验库雏形
+- **P2**:Web 端方案系统(FastAPI + Vue3)+ 本地执行器真实仿真全链路打通
+- **P3**:AI 驱动智能仿真闭环(策略抽象层 + 自适应编排器 + 批次化 + HTTP 闭环 + 真实烟雾测试)
+- **P4**:Web AI 集成 + 双系统闭环 + 批量调度 + Schema 统一 + EXE 打包(`dist/PCB-AFM-Executor.exe`)
+- **P5**:平台化增强 M1~M6(前端 build 清零 / EXE 配置化 / adaptive 三视图 / Morris+IDW 策略 / 多工具适配器 / 多物理场 L2 热+结构指标)
+- **P6-M1**:Web 前端 UI/UX 全面重构(设计令牌 + 3 公共组件 + B1 信息架构 + B2 PlanDetail 分层)
+- **P6-M2**:BC 参数目录单一事实源(`bc_fields.py` 25 字段 + `/api/bc-fields` + `normalize_bc` 统一 key 口径,消除 current_a/rated_ 双轨)
+- **P6-M3**:仿真前检查清单(`/api/plans/{id}/preflight` 5 项检查)+ 耗时校准(实测 solve_time_s 均值回写)+ 任务创建自动展开方案参数
+- **仿真失败根因修复(2026-09-03,关键)**:MARS 模型变量名经 pymotorcad 实测修正——`RotorOuterDiameter`/`Stator_Lam_Dia`/`Stator_Bore`/`Back_Iron_Thickness`/`PhaseAdvance`(原模板用径向电机命名全错)+ 磁钢材料值 `NdFeB_N42SH`→`N42UH`;详见 KNOWLEDGE_BASE §3.3
+- **生产链路全通畅(2026-09-03)**:修复拓扑预检变量集、执行器 dispatch 状态机冲突、适配器 scripts 导入路径;**多点扫描实测通过**(3 点 Airgap:tavg 0.566/0.522/0.468 Nm,趋势符合电磁学)
+- **AI 生成链路修复**:Kimi max_tokens 撞顶(2000→配置值)+ topology/strategy 枚举归一化兜底 + 生成摘要对话框 + 扫描变量归一化(SCAN_PARAMETERS 8→13)
+- **UI 评审修复**:统计卡字段名 Bug + Dashboard 结果加载(.items)+ BC 空值折叠 + 失败结果错误信息列 + 监控页合并(ExecutionMonitor)+ 批量清理(项目/任务)+ 扫描变量编辑持久化 + 列配置 localStorage
+- **PROMPTS_DIR 根因修复(2026-09-03,重大)**:`config.py` 的 `BACKEND_DIR=app/` 导致 `PROMPTS_DIR` 指向不存在的 `app/prompts/`(实际在 `web/backend/prompts/`)→ **真 prompt(generate.txt/analyze.txt)从未加载,一直走兜底**。修复后 AI 变量名遵循度根治、bc_suggestions 生效、验收标准格式规范。新增 `extract.txt`(经验提取)。详见 KNOWLEDGE_BASE §1.2
+- **用户四需求(2026-09-03)**:拓扑默认模型自动调用(topology registry `default_model` + 三处回退 + 预检一键修复)+ 全量 25 项 BC 表单与来源标签(用户指定/AI 补充/已修改,bc_meta 追踪)+ 验收标准自动带出 + BC/固定参数默认全展开可编辑保存
+- **自适应闭环全链路打通(2026-09-03/04,TEST-045~049)**:修复 6 个集成断层(variables/scan_variables 字段名、start/stop 键名、闭环缺 topology/model_path、**L0 参数名口径**——afmcore/l0 加 MOTORCAD_TO_L0 语义映射、初始批次不消费、report-results 路由缺失、export/import inf 哨兵)+ submit-batch 固定参数合并 + **执行器自动回传闭环**(adaptive_batch 完成自动调 report-results)。端到端实测:8+12 点真实仿真全成功,AI 经验提取 2 轮成功
+- **经验提取数据完整性**:report_results 合入输入 params + condense 经 MOTORCAD_TO_L0 翻译(修复"AI 拿不到输入参数无法做敏感性分析")
+
+### 当前阻塞点
+1. **热仿真稳态已打通(2026-09-04 实测)**:`enable_thermal` 修复后 MARS 稳态热跑通(电磁 127.8s + 热 6s)。剩余:MARS 模型 `Ambient_Temperature=125` 异常待修正(致温升/热阻为负);磁热耦合 `do_magnetic_thermal_calculation` / 瞬态 `do_transient_analysis` 待实测;执行器 enable_thermal 全链路待验证
+2. **Maxwell/JMAG 适配器为 mock**:真实接入待目标机环境(Ansys Maxwell + PyAEDT / JMAG + jmagpy)
+3. **代理模型为 IDW 降级**:Kriging 待引入 scipy 后同接口替换
+4. **中文仓库根 + git 2.52.0.windows.1 子目录命令卡死**:规避 = 先 `cd` 到目标子目录再用相对文件名(见 KNOWLEDGE_BASE §1)
+
+### 待办
+- 修正 MARS 模型环境温度 `Ambient_Temperature`(125→25~40°C)后复跑热仿真,验证温升/热阻转正
+- 实测磁热耦合 `do_magnetic_thermal_calculation`(电磁+热一次算,用户"一起做省时间"诉求)
+- 热仿真接入执行器 enable_thermal 全链路(GUI/扫描点热指标落盘)
+- 其他拓扑(DRSS/SDSR)/其他模型的变量名需按 MARS 同样方法实测后登记到 `topology_variable_map`(当前仅 SSSR/MARS 经实测);DRSS/SDSR 无基础模型文件,待用户提供 .mot 后登记到 topology registry 的 default_model
+- 全闭环自动流转的长时端到端观察(信任域收敛);历史方案(prompt 修复前生成)质量参差,建议关键方案重新生成
+- 前端实机走查:AI 生成 → 检查清单 → 启动仿真 → 结果趋势(用户回归)
+
+---
+
+## 4. 给 AI 的接续提示词(整段粘贴)
+
+```text
+这是 PCB 轴向磁通电机自动化仿真系统项目。双系统解耦的轴向磁通电机自动化仿真
+平台:Web端方案生成(FastAPI+Vue3)+ 本地EXE仿真执行(Motor-CAD),共享核心层
+src/afmcore/ 为单一事实源。当前 P6 阶段,P6-M1 已完成前端 UI/UX 重构。
+
+【先做这几件事,做完再动任何东西】
+1. 读 docs/HANDOFF.md、docs/KNOWLEDGE_BASE.md、docs/TEST_RECORDS.md、AGENTS.md。
+   KNOWLEDGE_BASE 里记录了大量踩过的坑(环境变量陷阱、Git 子目录卡死、
+   CurrentDefinition 语义、export_results 兼容性等),请重点看,不要重复踩。
+2. 核对环境:python scripts/check_machine_paths.py
+3. 确认当前进度与阻塞点(见 HANDOFF.md 第 3 节)。
+
+【工作约定(必须遵守)】
+- 每次测试前先 git commit;每次对话记录到 docs/CONVERSATION_LOG.md;
+  每次测试记录到 docs/TEST_RECORDS.md(含 TEST 编号索引)。
+- 源码(.py/.ps1)只含 ASCII,中文写进 Markdown;脚本用英文注释。
+- Motor-CAD 用 open_new_instance=True 独立实例 + set_visible(True);
+  参数写入必须回读校验;每个扫描点结束立即落盘。
+- 不要修改 models/ 下原始 .mot;参考目录只读;生成物(output/runs/build/dist/*.log)不入库。
+- 仿真禁止跑在主线程(GUI 需 QThread)。
+
+【铁律(血泪教训)】
+- 非登录 shell 可能不继承 MOTORCAD_ACTIVEX / ANSYSLMD_LICENSE_FILE → 脚本内回退
+- Git 子目录路径命令可能永久挂起 → 先 cd 到子目录再用相对文件名
+- 多处定义同一概念会漂移 → 指标/拓扑/Schema 一律走 src/afmcore 单一事实源
+- 交付前过自查清单,未验证只能标注"待验证/草案",禁止编造数值
+
+【当前状态】
+- 已完成:P1~P6 全部(含 P6-M2 参数目录单一事实源、P6-M3 检查清单+耗时校准);
+  2026-09-03 仿真失败根因(MARS 变量名/材料名)已实测修复,多点扫描生产链路全通畅;
+  2026-09-03/04 PROMPTS_DIR 根因修复(真 prompt 首次生效)+ 自适应闭环全链路打通
+  (含执行器自动回传,TEST-045~049 实测)
+- 阻塞:热求解/多工具/Kriging 待环境或真实运行
+- 待办:DRSS/SDSR 拓扑模型与变量名实测登记;热求解真实验证;闭环长时运行观察;前端实机走查
+
+【接下来做什么】
+- <目标1> / <目标2>
+
+先读文档、恢复环境、跑 check_machine_paths.py,然后告诉我你的理解和建议的
+下一步,不要直接开始改东西。
+```
+
+---
+
+## 5. 文档索引
+
+| 文档 | 内容 |
+|---|---|
+| README.md | 项目全貌 + 最近更新摘要(历史更新见 CHANGELOG.md) |
+| CHANGELOG.md | 全部历史更新记录(按时间倒序) |
+| AGENTS.md | AI 行为准则(工程规范 / 铁律 / 反模式) |
+| ai-collab-dev-playbook-v2.md | AI 协作方法论框架 V2(新项目可复用) |
+| docs/KNOWLEDGE_BASE.md | 环境事实 / 参数语义 / 探测技术 / SOP / 已踩的坑 |
+| docs/TEST_RECORDS.md | 测试记录(TEST-001~024,含索引) |
+| docs/CONVERSATION_LOG.md | 会话与决策记录 |
+| docs/P1-P5交付总结与上手指南.md | 里程碑全盘核对 + 新人上手 |
+| docs/PAPER_KNOWLEDGE_BASE.md | 无铁心 PCB-AFPM 论文知识库(38 篇) |
+| docs/PLATFORM_DESIGN_V2.md | 平台化升级设计方案 |
+| docs/archive/ | 已完结历史计划文档(P3/P4 实施计划等,仅供追溯) |

+ 325 - 0
docs/KNOWLEDGE_BASE.md

@@ -0,0 +1,325 @@
+# 知识库 — 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)` |
+| Git 子目录路径卡死(2026-08-30 实测) | 从仓库根目录执行带子目录路径的 git 命令(`git add docs/x` / `hash-object web/x` / `diff` / `checkout`)会**永久挂起**(CPU≈0,等 IO,30s 不返回);git log/status/rev-parse/ls-files 正常。**规避**:先 cd 到目标子目录再用相对文件名执行(`cd docs; git add TEST_RECORDS.md` 秒回)。疑似与中文仓库根路径 + git 2.52.0.windows.1 组合相关 |
+
+### 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` |
+| AI 生成不遵循 prompt(变量名编造/可选字段不输出) | **PROMPTS_DIR 路径错一层**:`web/backend/app/config.py` 中 `BACKEND_DIR=app/`,原 `PROMPTS_DIR=BACKEND_DIR/"prompts"` 指向不存在的 `app/prompts/`,实际在 `web/backend/prompts/` → 一直走 `_default_prompt()` 中文兜底 | 已修为 `BACKEND_DIR.parent/"prompts"`(2026-09-03)。**教训:改 prompt 文件后必须验证后端实际加载的是它(看 ai_call_logs 的 prompt_preview 开头)**;影响面:plan_generation/result_analysis 两个 prompt 此前均未生效 |
+
+---
+
+## 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")
+```
+
+### 2.6 热仿真(稳态 / 瞬态 / 磁热耦合)
+
+**关键事实(2026-09-04 实测,勿再踩坑)**:pymotorcad **没有** `do_thermal_calculation()`
+方法,`export_results` 的 solution_type **没有** `"Thermal"` 这个值。历史 P5-M6 的
+enable_thermal 代码用了这两个错误 API,从未真正跑通(HANDOFF 标注"待真实验证"即因此)。
+
+**真实 API**(来自 ansys.motorcad.core 源码 `rpc_methods_calculations.py` 核实):
+
+| 方法 | 语义 |
+|---|---|
+| `do_steady_state_analysis()` | 稳态热求解 |
+| `do_transient_analysis()` | 瞬态热求解 |
+| `do_magnetic_thermal_calculation()` | 磁热耦合(电磁+热一起算,官方注释 "coupled e-magnetic and thermal") |
+| `do_magnetic_calculation()` | 纯电磁 |
+
+**结果导出 solution_type 合法值**(`export_results(solution_type, file_path)`):
+`'EMagnetic'`(电磁)、`'Lab'`(Lab 工况)、`'SteadyState'`(稳态热)、`'Transient'`(瞬态热)。
+**不是** `"Thermal"`。
+
+**标准流程**(电磁 → 热,损耗作为热源):
+```python
+mc.do_magnetic_calculation()   # 先算电磁,得到损耗(热源)
+mc.do_steady_state_analysis()  # 稳态热求解,MARS 实测约 6 秒
+mc.export_results("SteadyState", r"output\raw\thermal.csv")  # 导出热结果
+```
+
+**MARS 稳态热实测(2026-09-04,TEST-060)**:
+- 电磁 127.8s + 稳态热 6.0s,总约 2.7 分钟
+- 绕组平均 67.95°C、绕组热点 74.59°C、磁钢 active 118.15°C、后轴承 88.47°C、前轴承 49.19°C
+- 稳态热导出 section:温度 / 损耗 / 热传导系数 / 热阻 / 热容 / 端部 / 绕组 / 机壳水道 / Node Temperatures
+
+**热指标字段名(SteadyState 导出,已登记到 metrics.py alias)**:
+- 绕组温度 → `T [Winding (A) Average]` / `T[绕组平均]`
+- 绕组热点 → `T[绕组最高]` / `T [EWdg (Outer) Maximum]`
+- 磁钢温度 → `T [Magnet Active]` / `T [Magnet Average]`
+- 定子温度 → `T[定子轭]` / `T[定子外表面]`
+- 后轴承 → `T[后轴承]`(bearing_temp_c 取后轴承,轴向磁通电机热风险侧;前轴承 `T[前轴承]` 更冷)
+- 温升 → `dT [Winding (Maximum) - Ambient]`
+- 热阻 → `Rt [Winding (Maximum) - Ambient]`
+
+**⚠ MARS 模型热边界条件异常(待用户确认修正)**:
+`[Miscellaneous]` section 中 `T_Ambient=125` / `Ambient_Temperature=125`(环境温度 125°C,
+异常;辐射环境温度 `T_Ambient_Radiation=40` 合理)。导致温升/热阻为负值
+(绕组 74.6°C 比环境 125°C 还低)。热仿真前应将 `Ambient_Temperature` 设为合理值
+(如 25~40°C),否则温升/热阻指标无意义。
+
+---
+
+## 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` **无效且不报错**(首跑踩坑)
+
+### 3.3 MARS 几何变量名(2026-09-03 pymotorcad 实测,勿再用错)
+
+**背景**:fixed_params_template 的几何变量曾用径向电机命名(`Outer_Rotor_Diameter` 等),导致 `set_variable` 报 `Could not find variable`、仿真点全部 FAILED。经解析 `.mot` + pymotorcad `get_variable`/`set_variable` 实测,MARS(PCB 无铁心轴向磁通,SSSR)的真实几何变量名如下。
+
+| 概念 | 模板错误名(勿用) | MARS 正确名 | MARS 值 | 实测 |
+|---|---|---|---|---|
+| 转子外径 | `Outer_Rotor_Diameter` | `RotorOuterDiameter` | 130 | get/set 均 OK |
+| 定子外径 | `Stator_Outer_Diameter` | `Stator_Lam_Dia` | 76 | get/set 均 OK |
+| 定子内径 | `Stator_Inner_Diameter` | `Stator_Bore` | 50 | get/set 均 OK |
+| 转子背铁厚 | `Rotor_Back_Iron_Thickness` | `Back_Iron_Thickness` | 5 | get/set 均 OK |
+| 极弧(电角度) | —(原名即对) | `Magnet_Arc_[ED]` | 121 | get OK |
+
+**MARS 无对应变量(motorcad_var 应设 null,不写入)**:
+- `Inner_Rotor_Diameter`(转子内径):`RotorBore` / `Rotor_Inner_Diameter` / `RotorInnerDiameter` / `InnerRotorDiameter` 全部 MISS —— PCB 单转子无独立内径变量。
+- `Stator_Yoke_Thickness`(定子轭厚):`Stator_Yoke_Thickness` / `StatorYokeThickness` / `Yoke_Thickness` 全部 MISS —— PCB 无铁心结构,无轭。
+
+**已实测确认的错误名(get_variable 全 MISS)**:`Outer_Rotor_Diameter` / `Inner_Rotor_Diameter` / `Stator_Outer_Diameter` / `Stator_Inner_Diameter` / `Number_of_Slots` / `Number_of_Poles` / `Turns_per_Coil` / `Max_Speed` / `Winding_Connection` / `Current_Density` / `Magnet_Remanence` / `Insulation_Class`。注意这些是 fixed_params_template 的**显示名**;其中多数模板已通过 `motorcad_var` 映射到真实名(`Slot_Number`/`Pole_Number`/`ConductorsPerSlot`/`WindageGraph_MaxSpeed`/`WindingConnection`/`Magnet_Br_at_RefTemp`,均实测 OK),仅几何 4 个映射错了。
+
+**默认值同步**:模板几何默认值已从径向模板值(200/198/122/120/10/8)更正为 MARS 实测值(130/76/50/—/—/5)。
+
+**端到端补充修正(2026-09-03 TEST-037,set 全通过 + 真实求解成功)**:
+- `Current_Advance_Angle`(电流超前角)→ 正确名 **`PhaseAdvance`**(实测 get/set 均 OK);原 `Current_Advance_Angle` / `CurrentAdvanceAngle` / `AdvanceAngle` 等候选全 MISS。
+- `Steel_Grade`(硅钢片牌号)`motorcad_var` 由 `Material_Stator_Lam_Yoke` 改 **null**:PCB 无铁心无轭,`Material_Stator_Lam_Yoke` 不存在;`Material_Stator_Lam_Outer` 等虽存在但值为空(无铁心未设材料),不写入。
+- `Magnet_Material` 默认值由 `NdFeB_N42SH` 改 **`N42UH`**:`NdFeB_N42SH` 在 Motor-CAD 材料库(solids database)不存在导致求解失败;MARS `.mot` 实测 `Material_Magnet=N42UH`。注意 Motor-CAD 材料命名是 `N42UH` 这类牌号,不带 `NdFeB` 前缀。
+- 验证结果:37 固定参数 32 可写全部 set 成功(0 failed),`do_magnetic_calculation` 求解成功(~2 分钟),导出结果有效(转矩脉动 2.815%、AC 损耗 0.47W、功率因数 0.995)。
+
+---
+
+## 4. 结果指标提取
+
+### 4.1 指标定义(单一事实源,本节不复制清单)
+
+全平台指标清单的唯一权威定义在 `src/afmcore/metrics.py`:35 项(电磁 25 + 热网络 6 + 结构 4),每项含 key/label/unit/direction/required/aliases(中英文别名)。**本知识库不复制指标清单,防止多处漂移**(历史教训:Phase 1 曾在多处复制 11/12 项清单,扩到 35 项后全部过期)。
+
+查询方式:
+- 代码:`from afmcore.metrics import METRIC_DEFINITIONS`
+- API:`GET /api/analytics/metrics`
+- 扩指标:只改 `src/afmcore/metrics.py`,三个消费端(solver_core / robust_motorcad / web analytics)自动生效
+
+易错字段名示例(完整别名表见 metrics.py):
+
+| 指标 | Motor-CAD 导出字段名 |
+|---|---|
+| 平均转矩 | `Average torque (virtual work)` |
+| 转矩脉动 | `Torque Ripple (VW) [%]` |
+| 系统效率 | `System Efficiency` |
+| 反电动势 | `Back EMF Line-Line Voltage (rms)` |
+
+### 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 主线程

+ 545 - 0
docs/P1-P5交付总结与上手指南.md

@@ -0,0 +1,545 @@
+# PCB 轴向磁通电机自动化仿真系统 — P1-P5 交付总结与上手指南
+
+> **文档版本**:V1.0(2026-08-30,P5 完成时点)
+> **适用读者**:新加入项目的开发者、需要快速定位模块的维护者、接手后续 Phase 的工程师
+> **前置阅读**:`AGENTS.md`(工程纪律)→ `docs/KNOWLEDGE_BASE.md`(环境事实与踩坑记录)→ 本文档
+
+---
+
+## 1. 项目概览
+
+### 1.1 一句话定位
+
+双系统解耦的 PCB 轴向磁通电机(AFPM)自动化仿真平台:
+- **系统一(Web 端)**:FastAPI + Vue3,负责方案生成、AI 优化、经验库、可视化、报告
+- **系统二(本地 EXE)**:PyInstaller 打包的本地执行器,驱动 Motor-CAD 真实仿真
+- **共享核心层** `src/afmcore/`:metrics / topology / adapters / strategies / l0 / plan_schema,单一事实源,双系统共用
+
+### 1.2 当前状态(2026-08-30)
+
+| 维度 | 状态 |
+|---|---|
+| Phase 1(最小闭环) | ✅ 全部完成 |
+| Phase 2(Web 端方案系统) | ✅ 全部完成 |
+| Phase 3(AI 驱动智能仿真闭环) | ✅ 全部完成 |
+| Phase 4(Web AI 集成 + 双系统闭环 + 批量调度 + 部署) | ✅ 全部完成 |
+| Phase 5(平台化增强,M1-M6) | ✅ 全部完成 |
+| Git HEAD | `af2275f` |
+| 测试脚本 | 23 个 `scripts/test_*.py`,全量回归 22/22 PASS(test_api_client 需真实链路) |
+| 指标定义 | 35 项(电磁 25 + 热 6 + 结构 4) |
+| 仿真策略 | 5 种(full_factorial / lhs / adaptive / morris / surrogate_guided) |
+| 工具适配器 | 3 种(motorcad 真实 / maxwell mock / jmag mock) |
+| 前端构建 | vue-tsc 0 错误(P5-M1 清零) |
+| EXE 打包 | `dist/PCB-AFM-Executor.exe`(P4-M3,PyInstaller onefile) |
+
+---
+
+## 2. P1-P5 里程碑全盘核对表
+
+### 2.1 Phase 1 — 最小闭环
+
+| 里程碑 | 计划内容 | 实际交付 | 状态 | Commit |
+|---|---|---|---|---|
+| P1-M1 | 环境验证 + 单工况仿真脚本 | Motor-CAD 前台可见、参数回读校验、连接/计算/导出/解析跑通 | ✅ | `75347b6` |
+| P1-M2 | 参数扫描引擎 | 每点基线重载、逐点落盘、中英文字段别名;气隙扫描 3 点物理趋势符合预期 | ✅ | `a43d971` |
+| P1-M3 | 方案 JSON 接口 + 本地 PySide6 GUI | 方案 Schema、GUI 加载/监控、方案回写 | ✅ | `cbd9402` |
+| P1-M4 | 经验库雏形 + 反馈闭环 | SQLite 经验库、相似检索、反馈闭环 | ✅ | `f20bef9` |
+
+**验证记录**:TEST-001(连接探测,发现 export_results API 兼容问题)→ TEST-002(修复后全流程,磁场计算 138.1s/点)→ TEST-003(指标解析 7→14 项)。
+
+### 2.2 Phase 2 — Web 端方案系统
+
+| 里程碑 | 计划内容 | 实际交付 | 状态 | Commit |
+|---|---|---|---|---|
+| P2-M1 | Web 基础框架 | FastAPI + Vue3 + SQLite + CRUD API + 双系统 API 客户端 | ✅ | `1ea7653` |
+| P2-M2 | 边界条件输入 + 方案编辑器 | 规则引擎(参数注册表/范围推荐/方案生成) | ✅ | `a95c0db` |
+| P2-M3 | 经验库 Web 端 + 分析仪表盘 | ECharts 趋势/Pareto/敏感性 | ✅ | `9885437` |
+| P2-M4 | 双系统 API 联调 + 知识库管理 | 经验库 CRUD/导入、系统二客户端增强 | ✅ | `25ee19f` |
+| P2-M5 | Phase 2 验收 | 自动化测试 + 前端人工验收 + 文档 | ✅ | `0df69f2` |
+| P2-补丁 | 前端全面中文化 + Dashboard 修复 | — | ✅ | `2660df7` |
+
+### 2.3 Phase 3 — AI 驱动智能仿真闭环
+
+> 基于第三方专家评审,仿真策略从"固定批量 DoE"升级为"多保真度 + 可行性优先 + 批量自适应闭环",引入 Kimi k3。
+
+| 里程碑 | 计划内容 | 实际交付 | 状态 |
+|---|---|---|---|
+| P3-M1 | AI 服务层基础架构 | Kimi 客户端 + JSON Schema V2 + 多保真度框架(L0→L4) | ✅ |
+| P3-M2 | L0 解析预筛选 + 可行性优先搜索 | 14+ 约束检查 + LHS 初始采样 + 主动学习批量选点 + 局部信任域精修 | ✅ |
+| P3-M3 | AI 方案生成器 | 自然语言 → 结构化仿真方案 | ✅ |
+| P3-M4 | AI 结果分析师 + 多保真度校准 | 置信等级 A~D + 六类收敛判据 | ✅ |
+| P3-M5 | 经验库 AI 增强 + 批量自适应闭环 | 经验提取 + 闭环验收 | ✅ |
+| P3-M6 | Web 端执行桥 + 路由整合 | AdaptiveLoop 补齐 submit_batch_to_executor() | ✅ `768a883` |
+
+**P3 平台化(afmcore 第一批~第三批)**:
+- 第一批:`metrics.py`(25 项指标+归一化解析器)+ `adapters/`(ABC + 注册表 + MotorCADAdapter)→ TEST-004
+- 第二批:`topology.py`(SSSR 8 组 37 项参数)+ task_executor 切换 get_adapter → TEST-005
+- 第三批:`strategies/`(full_factorial/lhs/adaptive)+ Task 模型扩展 + 调度契约统一 → TEST-006~011
+
+**P3 遗留处理(P4 期初)**:并发原子认领(`fb3a505`)、断点恢复(`4dcc627`)、ASCII 纪律(`3caecd1`)、环境依赖测试(TEST-013)。
+
+### 2.4 Phase 4 — Web AI 集成 + 双系统闭环 + 批量调度 + 部署
+
+| 里程碑 | 计划内容 | 实际交付 | 状态 |
+|---|---|---|---|
+| P4-M1 | 前端 AI 功能集成 | 6 个 AI 页面 + API 封装 + 通用组件 | ✅ |
+| P4-M2 | 双系统任务下发与回传 | Web 创建任务 → 执行器轮询领取 → 执行 → 上报 → 回传 → 存储展示 | ✅ |
+| P4-M3 | 批量调度 + 实时监控 + 增强版 MotorCAD 核心 | 优先级队列(1-10) + 最大并行(2) + 依赖 + 断点续跑;16 项鲁棒性措施 | ✅ |
+| P4-M4 | 高级可视化 + 自动报告 | Pareto/收敛/雷达/热力图 + Word/JSON 报告 | ✅ |
+| P4-M5 | 部署打包 | Docker + docker-compose + nginx + Windows 部署脚本 | ✅ |
+
+**P4 平台化批次(p4-m1..m5)**:
+
+| 批次 | 内容 | 验证 | Commit |
+|---|---|---|---|
+| P4-M1 | 方案 Schema 单一权威(src/plan_schema.py) | test_p4_schema.py 7 组全过 | `223888a` |
+| P4-M2 | 设计文档 V1.1→V2.0(附录 B 实现现状对照) | README 引用同步 | `9d09b3c` |
+| P4-M3 | EXE 打包(PyInstaller onefile,12.6MB) | dist/PCB-AFM-Executor.exe 冒烟通过 | `5dd3559` |
+| P4-M4 | 前端 adaptive 收敛曲线(points_history + echarts) | test_p4_m4_convergence.py 5 组;vue-tsc 0 错误 | `10a34a0` |
+| P4-M5 | L0 上提共享核心(src/afmcore/l0/prescreening.py) | test_p4_m5_l0.py 8 组 | `18ea49e` |
+
+**真实 Motor-CAD 实测**(MARS-12S10P,5000rpm):效率 86.06%、总损耗 41.945W、磁场计算 138.1s/点(TEST-002/003/010)。
+
+**第三方代码评审修复**(V1.4):20+ P0 项,详见 `docs/CODE_REVIEW_RESPONSE.md`。
+
+### 2.5 Phase 5 — 平台化增强(M1-M6,本轮全部完成)
+
+| 里程碑 | Backlog | 计划内容 | 实际交付 | 状态 | Commit |
+|---|---|---|---|---|---|
+| P5-M1 | B1 | 前端全量 build 类型错误清零 | vue-tsc 0 错误,修复 12 处类型问题 | ✅ | `b394c39` |
+| P5-M2 | B2 | 真实 EXE E2E 修复 | executor_config.json 可配置 + airgap_mm→Airgap 映射 + 排除 point_id 元数据 | ✅ | `3638699` + `974750e` |
+| P5-M3 | B3 | Adaptive 三视图可视化 | batch status + L0 summary + auto-polling | ✅ | `b137690` |
+| P5-M4 | B4 | 策略层高级管线 | Morris 灵敏度筛选 + IDW 代理 + UCB + 预算自适应批次 | ✅ | `94647f1` |
+| P5-M5 | B6 | 多工具适配器 | MaxwellAdapter/JMAGAdapter mock + 执行器 tool 动态 import | ✅ | `05df4be` |
+| P5-M6 | B5 | 多物理场 L2 接入 | metrics 扩 10 项(热6+结构4)+ robust 热求解开关 + 报告按域分组 | ✅ | `af2275f` |
+
+**Backlog 覆盖核对**:B1✅ B2✅ B3✅ B4✅ B5✅ B6✅ — 全部 6 项 backlog 清零。
+
+---
+
+## 3. 目录结构速查
+
+```
+PCB轴向磁通电机自动化仿真系统/
+├── AGENTS.md                          # 工程纪律(必读)
+├── README.md                          # 项目说明(V2.0)
+├── executor_config.json               # 本地执行器配置(tool/model_path/output_dir)
+├── deploy.ps1                         # Windows 部署脚本
+├── PCB轴向磁通电机自动化仿真系统设计方案介绍.md  # 完整设计方案 V2.0(含附录 B 实现对照)
+│
+├── docs/
+│   ├── KNOWLEDGE_BASE.md              # 核心知识库(环境事实、参数语义、踩坑记录)
+│   ├── HANDOFF.md                     # 接续指南(进度/阻塞/待办,新会话必读)
+│   ├── TEST_RECORDS.md                # 测试记录(TEST-001~024)
+│   ├── CONVERSATION_LOG.md            # 会话与决策记录
+│   ├── PLATFORM_DESIGN_V2.md          # 平台设计 V2
+│   ├── CODE_REVIEW_RESPONSE.md        # 第三方代码评审修复记录
+│   ├── P1-P5交付总结与上手指南.md      # 本文档
+│   └── archive/                       # 已完结历史计划文档(P3/P4实施计划等)
+│
+├── src/
+│   ├── afmcore/                       # 共享核心层(单一事实源,双系统共用)
+│   │   ├── metrics.py                 # 35 项指标定义 + 归一化解析器
+│   │   ├── topology.py                # 拓扑注册表(SSSR 8 组 37 项参数)
+│   │   ├── adapters/                  # 仿真工具适配器
+│   │   │   ├── __init__.py            # SimulationAdapter ABC + 注册表
+│   │   │   ├── motorcad.py            # Motor-CAD 适配器(真实,wrap RobustMotorCADSolver)
+│   │   │   ├── maxwell.py             # Maxwell 适配器(mock,真实接入待环境)
+│   │   │   └── jmag.py                # JMAG 适配器(mock,真实接入待环境)
+│   │   ├── strategies/                # 仿真策略
+│   │   │   ├── __init__.py            # 策略注册表 + get_strategy
+│   │   │   ├── full_factorial.py      # 全因子
+│   │   │   ├── lhs.py                 # 拉丁超立方
+│   │   │   ├── adaptive.py            # 自适应(可行性优先 + 主动学习)
+│   │   │   ├── morris.py              # Morris 灵敏度筛选
+│   │   │   └── surrogate_guided.py    # IDW 代理 + UCB + 预算自适应
+│   │   └── l0/
+│   │       ├── __init__.py
+│   │       └── prescreening.py        # L0 解析预筛选(14+ 约束,纯 stdlib)
+│   ├── api_client.py                  # Web API 客户端(系统二调用系统一)
+│   ├── experience_db.py               # 经验库(SQLite)
+│   ├── plan_schema.py                 # 方案 Schema(parse/validate 单一权威入口)
+│   ├── scan_engine.py                 # 扫描引擎(legacy)
+│   └── solver_core.py                 # 求解核心(legacy)
+│
+├── scripts/
+│   ├── robust_motorcad.py             # 鲁棒 Motor-CAD 求解器(16 项鲁棒性措施)
+│   ├── task_executor.py               # 任务执行器(adapter 驱动,多实例,point_id 回填)
+│   ├── executor_config.py             # 执行器配置加载
+│   ├── run_gui.py                     # 启动本地 PySide6 GUI
+│   ├── run_scan.py                    # 运行参数扫描
+│   ├── run_single.py                  # 运行单工况
+│   ├── run_task_executor.py           # 启动单实例任务执行器
+│   ├── run_task_executor_parallel.py  # 启动多实例并行执行器
+│   ├── build_executable.ps1           # PyInstaller EXE 打包脚本
+│   ├── scan_airgap.json               # 气隙扫描示例方案
+│   └── test_*.py                      # 23 个单元/集成测试脚本
+│
+├── web/
+│   ├── backend/                        # FastAPI 后端
+│   │   └── app/
+│   │       ├── main.py                # FastAPI 入口
+│   │       ├── routers/               # API 路由(plans/tasks/loops/experience/reports 等)
+│   │       ├── services/              # 业务服务(task_contract/report_generator/adaptive 等)
+│   │       └── models.py              # SQLAlchemy 模型
+│   ├── frontend/                       # Vue3 前端
+│   │   └── src/
+│   │       ├── views/                 # 页面(6 个 AI 页面 + 方案/任务/监控/经验库)
+│   │       ├── components/            # 通用组件
+│   │       └── api/                   # API 封装
+│   └── output/                         # Web 端输出(报告等)
+│
+├── models/                             # Motor-CAD 基线模型(.mot,只读)
+├── output/                             # 运行输出(不入库)
+└── dist/                               # EXE 打包输出(不入库)
+```
+
+---
+
+## 4. 核心层 afmcore 模块详解
+
+### 4.1 metrics.py — 指标单一事实源
+
+**职责**:定义全平台所有仿真输出指标,提供归一化解析器。
+
+**关键设计**:
+- `METRIC_DEFINITIONS`:35 项指标,每项含 `key/label/unit/direction/required/aliases/domain`
+- `aliases`:英文 + 中文(`\uXXXX`),覆盖 Motor-CAD 导出的各种字段名变体
+- `normalize_name()`:全角→半角、去所有空白(含全角空格 U+3000)、小写 — 修复历史 tavg/ripple 解析 bug
+- `parse_export()`:分号分隔 CSV 解析器,多编码 fallback(utf-8-sig/utf-8/gbk/cp1252/latin-1)
+- `pick_metric()`:E-Magnetics 段优先精确匹配 → 全段精确 → 前缀模糊(带 % 防护)
+- `extract_all_metrics()`:遍历 METRIC_DEFINITIONS,**新增指标自动生效,无需改调用方**
+
+**扩项方式**:在 `METRIC_DEFINITIONS` 列表中加一项(含 aliases),所有消费端(robust_motorcad / report_generator / Web 后端)自动生效。
+
+### 4.2 adapters/ — 仿真工具适配器
+
+**职责**:统一不同 FEA 工具(Motor-CAD / Maxwell / JMAG)的接口,执行器只依赖 `SimulationAdapter` ABC。
+
+**接口契约**(`SimulationAdapter` 抽象基类):
+- `connect()` / `disconnect()`:生命周期
+- `load_model(model_path)`:加载基线模型(只读)
+- `set_parameter(name, value)`:写参数 + 回读校验(不一致抛 RuntimeError)
+- `run_simulation(mode)`:运行求解
+- `extract_metrics(output_dir, tag)`:导出原始结果 + 解析指标
+- `run_point(...)`:高层便捷方法(默认编排 connect→load→set→run→extract→disconnect)
+
+**注册表**:`register_adapter(tool_name, cls)` / `get_adapter(tool_name, **kwargs)` / `registered_tools()`
+
+**现有适配器**:
+| tool | 实现 | 状态 | capability_domains |
+|---|---|---|---|
+| motorcad | MotorCADAdapter,wrap RobustMotorCADSolver | ✅ 真实可用 | electromagnetic |
+| maxwell | MaxwellAdapter,内存参数+确定性合成指标 | ⚠️ mock(真实接入需 Ansys Maxwell + PyAEDT) | electromagnetic, thermal |
+| jmag | JMAGAdapter,同上 | ⚠️ mock(真实接入需 JMAG Designer + jmagpy) | electromagnetic |
+
+**执行器集成**:`MotorCADTaskExecutor._ensure_adapter()` 根据 `self.tool` 动态 import 对应适配器模块并注册。
+
+### 4.3 strategies/ — 仿真策略
+
+**职责**:参数空间探索策略,统一 `SimulationStrategy` 接口(select_next / report / next_batch_ready / is_converged / state)。
+
+**现有策略**:
+| kind | 类 | 适用场景 | state() 关键输出 |
+|---|---|---|---|
+| full_factorial | FullFactorialStrategy | 小空间全遍历 | progress |
+| lhs | LHSStrategy | 大空间初始采样 | progress |
+| adaptive | AdaptiveStrategy | 可行性优先 + 主动学习 + 信任域精修 | best_point / convergence |
+| morris | MorrisStrategy | 灵敏度筛选(OAT 初等效应) | sensitivity_ranking / key_parameters |
+| surrogate_guided | SurrogateGuidedStrategy | IDW 代理 + UCB + 预算自适应 | phase / surrogate 诊断 |
+
+**注册表**:`get_strategy(kind, **kwargs)` / `registered_strategies()`
+
+### 4.4 l0/prescreening.py — L0 解析预筛选
+
+**职责**:14+ 约束检查(几何/电气/热/制造),在昂贵 FEA 之前快速排除不可行方案。
+
+**关键特性**:纯 stdlib 实现(无 numpy/scipy),可在 EXE 中无依赖运行。
+
+### 4.5 topology.py — 拓扑注册表
+
+**职责**:SSSR(单定子单转子)8 组 37 项参数体系,DRSS/SDSR 预留。参数语义、范围、默认值的单一事实源。
+
+### 4.6 plan_schema.py — 方案契约 V2
+
+**职责**:仿真方案的 JSON Schema 定义 + parse/validate 入口。Web 端和本地执行器共用,确保方案格式一致。
+
+---
+
+## 5. 快速开始
+
+### 5.1 环境准备
+
+**必需**:
+- Python 3.10+(推荐 3.11)
+- Windows 10/11(Motor-CAD 仅 Windows)
+- Git
+
+**Motor-CAD 仿真(可选,跑真实求解需要)**:
+- Ansys Motor-CAD v261(安装路径 `D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe`)
+- 环境变量:`MOTORCAD_ACTIVEX`、`ANSYSLMD_LICENSE_FILE`(非登录 shell 可能不继承,脚本内有回退)
+- pymotorcad(`pip install ansys-motorcad-core`)
+
+**Web 端(可选)**:
+- Node.js 18+(前端构建)
+- FastAPI + uvicorn(后端,`pip install -r web/backend/requirements.txt`)
+
+**安装依赖**:
+```powershell
+cd C:\Users\admin\Desktop\PCB轴向磁通电机自动化仿真系统
+pip install -r requirements.txt  # 如果存在
+# 核心依赖:ansys-motorcad-core, pyside6(GUI), fastapi, uvicorn, sqlalchemy, python-docx
+```
+
+### 5.2 跑测试(验证环境)
+
+```powershell
+# 全量回归(排除需要真实链路的 test_api_client.py)
+Get-ChildItem scripts\test_*.py | ForEach-Object {
+    if ($_.Name -ne "test_api_client.py") {
+        python $_.FullName
+    }
+}
+# 预期:22/22 PASS
+```
+
+**单个测试**:
+```powershell
+python scripts\test_metrics_extension.py   # 指标扩展
+python scripts\test_adapters.py             # 适配器
+python scripts\test_strategy_morris.py      # Morris 策略
+```
+
+### 5.3 启动本地 GUI(系统二,方案编辑 + 监控)
+
+```powershell
+python scripts\run_gui.py
+```
+
+GUI 功能:加载方案 JSON、编辑参数、启动扫描、实时监控进度、查看结果。
+
+### 5.4 启动任务执行器(系统二,从 Web 端领取任务)
+
+```powershell
+# 单实例
+python scripts\run_task_executor.py
+
+# 多实例并行(2 个)
+python scripts\run_task_executor_parallel.py --instances 2
+```
+
+执行器通过 Web API 轮询领取任务,调用适配器(默认 motorcad)执行仿真,结果上报回 Web 端。
+
+**配置**:编辑 `executor_config.json`:
+```json
+{
+  "web_base_url": "http://127.0.0.1:8000",
+  "model_path": "models/base.mot",
+  "tool": "motorcad",
+  "output_dir": "output",
+  "enable_mock": false,
+  "poll_interval_seconds": 5
+}
+```
+
+`tool` 可选:`motorcad`(真实)/ `maxwell`(mock)/ `jmag`(mock)。
+
+### 5.5 启动 EXE(打包后的执行器)
+
+```powershell
+dist\PCB-AFM-Executor.exe --config executor_config.json
+dist\PCB-AFM-Executor.exe --version
+dist\PCB-AFM-Executor.exe --self-test
+```
+
+### 5.6 启动 Web 端(系统一)
+
+```powershell
+# 后端
+cd web\backend
+pip install -r requirements.txt
+uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
+
+# 前端(另开终端)
+cd web\frontend
+npm install
+npm run dev  # 开发模式,http://localhost:5173
+npm run build  # 生产构建(vue-tsc 类型检查,P5-M1 后 0 错误)
+```
+
+### 5.7 运行单工况仿真(命令行)
+
+```powershell
+python scripts\run_single.py --model models\base.mot --params airgap_mm=1.0,magnet_thickness_mm=5.0
+```
+
+### 5.8 运行参数扫描(命令行)
+
+```powershell
+python scripts\run_scan.py --config scripts\scan_airgap.json
+```
+
+---
+
+## 6. 关键工程约束(硬性纪律,违者返工)
+
+> 完整规则见 `AGENTS.md`,以下为最常踩的 7 条。
+
+### 6.1 源码纯 ASCII
+所有 `.py` 和 `.ps1` 文件**只含 ASCII 字符**。中文说明写 Markdown,中文字段名用 `\uXXXX` 转义。
+```powershell
+# 检查命令
+rg -n "[^\x00-\x7F]" --glob '*.py' --glob '*.ps1' .
+```
+
+### 6.2 运行前 Git 必须干净
+实际启动 Motor-CAD 求解前必须满足:Git 仓库存在且 HEAD 有效、所有已跟踪文件无未提交修改。GUI 内置 Git preflight,不满足时拒绝启动扫描。
+
+### 6.3 Motor-CAD 实例管理
+- 使用 `open_new_instance=True` 创建独立实例(**不要**连接已有实例)
+- 启动后必须 `set_visible(True)`(/SCRIPTING 模式默认隐藏)
+- 每个扫描点开始前 `load_from_file(基线模型)`,结束后也重载基线
+
+### 6.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,保存错误并继续下一点。
+
+### 6.5 结果逐点落盘
+每个点完成后立即写 CSV 并 flush,**不能**等整批完成后一次性保存。失败点记录错误并继续。运行目录结构:`output/<timestamp>_<scan_name>/` 含 manifest.json + scan_results.csv + program_log.log + raw/。
+
+### 6.6 原始模型只读
+原始 `.mot` 文件不修改。所有操作在 Motor-CAD 内存中进行,或另存时间戳副本。`models/` 目录下的模型文件视为只读。
+
+### 6.7 生成物不入库
+`output/`、`runs/`、`build/`、`dist/`、`*.log`、`*.spec`、`__pycache__/` 不入库。关键数值转录进入库的文档(TEST_RECORDS.md / RESULTS.md / 报告)。
+
+---
+
+## 7. 测试体系
+
+### 7.1 测试脚本清单(23 个)
+
+| 测试文件 | 覆盖范围 | 类型 |
+|---|---|---|
+| test_robust_solver.py | RobustMotorCADSolver 核心(回读校验/基线重载/逐点落盘) | 单元 |
+| test_platform_registry.py | afmcore 注册表(adapters/strategies) | 单元 |
+| test_p3_unit_edge.py | P3 边界/异常场景 | 单元 |
+| test_p3_m4_contract.py | P3-M4 任务契约 | 单元 |
+| test_p3_orchestrator.py | P3 编排器 | 单元 |
+| test_p3_adaptive_execution.py | P3 自适应执行 | 集成 |
+| test_p3_closed_loop.py | P3 闭环(mock) | 集成 |
+| test_p3_checkpoint.py | 断点恢复 | 单元 |
+| test_p3_concurrency.py | 并发原子认领 | 集成 |
+| test_p4_schema.py | 方案 Schema 校验 | 单元 |
+| test_p4_acceptance.py | P4 验收 | 集成 |
+| test_p4_m4_convergence.py | P4-M4 收敛曲线 | 单元 |
+| test_p4_m5_l0.py | P4-M5 L0 上提 | 单元 |
+| test_executor_config.py | 执行器配置 | 单元 |
+| test_executor_m3.py | 执行器 M3 功能 | 单元 |
+| test_executor_p5m2.py | P5-M2 EXE E2E 修复 | 单元 |
+| test_search_state_summary.py | 搜索状态摘要 | 单元 |
+| test_strategy_morris.py | Morris 策略(P5-M4) | 单元 |
+| test_strategy_surrogate.py | SurrogateGuided 策略(P5-M4) | 单元 |
+| test_adapters.py | 多工具适配器(P5-M5) | 单元 |
+| test_metrics_extension.py | 指标扩展 + 报告域分组(P5-M6) | 单元 |
+| test_api_client.py | Web API 真实链路(需启动后端) | 集成 |
+
+### 7.2 运行方式
+
+```powershell
+# 全量(排除 test_api_client,需真实后端)
+Get-ChildItem scripts\test_*.py | Where-Object { $_.Name -ne "test_api_client.py" } |
+    ForEach-Object { python $_.FullName }
+
+# 单个
+python scripts\test_<name>.py
+# exit 0 = PASS,非 0 = FAIL
+```
+
+### 7.3 测试记录
+
+所有真实 Motor-CAD 仿真、API 测试、集成测试记录在 `docs/TEST_RECORDS.md`(TEST-001~022),含测试日期、环境、目的、步骤、结果、关键数据、发现的问题、修复措施。
+
+---
+
+## 8. 已知限制与后续方向
+
+### 8.1 当前限制(如实标注)
+
+| 限制 | 说明 | 升级路径 |
+|---|---|---|
+| Maxwell/JMAG 为 mock | 当前环境无 Ansys Maxwell / JMAG 安装,适配器为 mock 实现(确定性合成指标) | 目标机安装对应软件 + Python API(PyAEDT / jmagpy),在同接口下替换为真实实现 |
+| Kriging 代理未实现 | 环境无 numpy/scipy,P5-M4 用 IDW(反距离加权)替代 Kriging | 引入 scipy 后替换为 Kriging,策略接口不变 |
+| 真实热求解未运行 | P5-M6 增加了 enable_thermal 开关和代码路径,但需要模型配置热网络 + 实际启动 Motor-CAD 验证 | 配置热网络后运行 enable_thermal=True 的扫描,验证热指标提取 |
+| 结构指标需外部 FEA | axial_force / stress / deformation 等结构指标需要 Motor-CAD 结构模块或第三方 FEA(如 Ansys Mechanical) | 接入结构求解器,或从 Maxwell 3D 结果中提取力后做后处理 |
+| NSGA-II 多目标优化未实现 | P5 规划中提及,未在 M1-M6 范围内 | 后续 Phase 实现多目标优化策略 |
+| 前端工具选择下拉框 | executor_config.json 已支持 tool 字段,但 Web 端任务创建页尚未加工具选择 UI | 前端加 tool 下拉框,传入 task 创建 API |
+| 原有 winding_temp_c 无 domain | 该指标默认归入 electromagnetic 域 | 可加 domain=thermal 字段,归入热域 |
+
+### 8.2 后续方向建议
+
+1. **Phase 6 — 真实多工具接入**:在目标机安装 Maxwell/JMAG,将 mock 适配器替换为真实实现
+2. **多物理场真实耦合**:电磁→热→结构的单向/双向耦合,L2 多保真度层落地
+3. **多目标优化**:NSGA-II / MOEA-D,支持转矩密度×效率×成本的 Pareto 前沿
+4. **Kriging 代理升级**:引入 scipy,替换 IDW,提升代理精度
+5. **经验库自动积累**:仿真结果自动提取关键参数→指标映射,增量更新经验库
+6. **Web 端工具选择 UI**:任务创建页加 tool 下拉框
+7. **热求解真实验证**:配置热网络后运行 enable_thermal 扫描
+
+---
+
+## 9. 提交历史速查(P1-P5 关键 commit)
+
+| Commit | 内容 | Phase |
+|---|---|---|
+| `75347b6` | P1-M1 环境验证 + 单工况仿真 | P1 |
+| `a43d971` | P1-M2 参数扫描引擎 | P1 |
+| `cbd9402` | P1-M3 方案 JSON + PySide6 GUI | P1 |
+| `f20bef9` | P1-M4 经验库雏形 | P1 |
+| `1ea7653` | P2-M1 Web 基础框架 | P2 |
+| `2cae9b6` | afmcore 共享核心层 + metrics 单一事实源 + adapter 抽象 | P3 |
+| `27ccde6` | 拓扑注册表 + adapter 驱动执行器 | P3 |
+| `2e9adfa` | 执行策略抽象层 | P3 |
+| `fa74834` | Adaptive 编排器 | P3 |
+| `768a883` | Web AdaptiveLoop → 本地执行器桥 | P3-M6 |
+| `fb3a505` | 并发原子认领(dispatch_task 条件 UPDATE) | P3 遗留 |
+| `4dcc627` | 断点恢复(search/loop export-import) | P3 遗留 |
+| `223888a` | P4-M1 方案 Schema 单一权威 | P4 |
+| `5dd3559` | P4-M3 EXE 打包(PyInstaller) | P4 |
+| `10a34a0` | P4-M4 前端 adaptive 收敛曲线 | P4 |
+| `18ea49e` | P4-M5 L0 上提共享核心 | P4 |
+| `e6563fc` | P1-P4 回顾 + P5 规划(交接文档) | P5 起点 |
+| `b394c39` | P5-M1 前端类型错误清零(vue-tsc 0 错误) | P5 |
+| `3638699` | P5-M2 EXE 可配置(executor_config.json) | P5 |
+| `974750e` | P5-M2 真实 EXE E2E 修复(airgap_mm→Airgap + 排除 point_id) | P5 |
+| `b137690` | P5-M3 Adaptive 三视图可视化 | P5 |
+| `94647f1` | P5-M4 Morris + IDW 代理 + 预算自适应 | P5 |
+| `05df4be` | P5-M5 Maxwell/JMAG mock 适配器 + tool 动态 import | P5 |
+| `af2275f` | P5-M6 多物理场 L2(热/结构指标 + 热求解开关 + 报告域分组) | P5 收官 |
+
+---
+
+## 10. 新人上手 Checklist
+
+按顺序完成以下步骤,即可独立维护和开发:
+
+- [ ] **读 AGENTS.md**:理解工程纪律(ASCII / Git preflight / 回读校验 / 逐点落盘)
+- [ ] **读 docs/KNOWLEDGE_BASE.md**:理解环境事实、参数语义、已踩的坑
+- [ ] **读本文档 §3-§4**:理解目录结构和 afmcore 核心层
+- [ ] **跑通测试**:`python scripts\test_robust_solver.py` → 全量回归 22/22
+- [ ] **跑通 mock 执行器**:`executor_config.json` 设 `enable_mock=true`,启动执行器,从 Web 端创建 mock 任务
+- [ ] **理解 metrics 扩项**:在 `METRIC_DEFINITIONS` 加一项,验证 `extract_all_metrics` 自动生效
+- [ ] **理解适配器模式**:读 `adapters/__init__.py` + `motorcad.py`,尝试用 `get_adapter("maxwell")` 跑 mock 链路
+- [ ] **理解策略模式**:读 `strategies/__init__.py`,用 `get_strategy("morris")` 跑灵敏度筛选
+- [ ] **读设计方案附录 B**:`PCB轴向磁通电机自动化仿真系统设计方案介绍.md` 附录 B,理解蓝图 vs 实现的对照
+- [ ] **(可选)跑真实 Motor-CAD**:确认环境变量 + license,用 `run_single.py` 跑单工况,对照 TEST-002 结果
+
+---
+
+> **文档维护**:每次 Phase / Milestone 完成后,更新 `README.md` + `docs/TEST_RECORDS.md`,并在本文档对应章节追加里程碑记录。本文档最后更新:2026-08-30(P5 完成时点,HEAD `af2275f`)。

+ 673 - 0
docs/PAPER_KNOWLEDGE_BASE.md

@@ -0,0 +1,673 @@
+# PCB 轴向磁通电机(无铁心 AFPM)论文知识库
+
+> **用途**:本工程(PCB 轴向磁通电机自动化仿真系统)软件工程师的论文知识参考。
+> **来源**:`书籍与论文/相关论文-Chulaee IEEE IAS Design Optimization High Efficiency Coreless PCB Stator AFPM/` 下的 38 篇论文及其中文翻译稿,已于 2026-08-29 系统精读并提炼。
+> **与 KNOWLEDGE_BASE.md 的关系**:本文档回答"**电机本身有什么规律、损耗怎么算、设计怎么优化、仿真时该设什么**";KNOWLEDGE_BASE.md 记录"**Motor-CAD 环境怎么跑、参数怎么探测、踩过什么坑**"。两者配合使用。
+
+---
+
+## 0. 读法指南
+
+- **你是软件工程师,不是电机设计专家**:优先看每节开头的"**工程要点**"框(可直接落进仿真脚本 / 参数表 / 校验逻辑),再看公式与出处。
+- **38 篇论文速查表**见第 10 节;按主题找论文,按论文回原文。
+- **与 Motor-CAD 仿真的衔接**:第 8 节汇总了可直接指导仿真建模、损耗设置、计算预算、参数校验的清单。
+- **数字可靠性**:本知识库中的数字均摘自论文(含样机实测/仿真),非猜测。个别论文存在笔误(如 Infinitum 双转子论文"110 HP(7.46kW)"实为 10 HP),已在文中注明。
+- **常用符号**:`tw` 迹宽、`th` 迹高(铜厚)、`δ` 趋肤深度、`f` 电频率、`σ` 电导率、`ρ` 电阻率、`τp` 极距、`λ` 内外径比。
+
+---
+
+## 1. 技术全景与拓扑
+
+### 1.1 一句话定位
+
+**PCB 定子无铁心轴向磁通永磁电机(C-AFPM / PCB-AFPM)** = 去掉定子铁芯 + 用印刷电路板铜迹线替代铜线绕组 + 永磁转子。适合需要**高功率/转矩密度、轴向紧凑、高效率、可批量制造**的应用(泵、风机、无人机、电动汽车、航空推进、机器人)。
+
+**为什么去掉铁芯**:
+- 消除定子铁损、齿槽转矩 → 更高效率、更低噪声振动
+- 消除/大幅削弱轴向磁拉力(无铁心时不平衡磁拉力几乎可忽略)
+- 无齿槽 → 零齿槽转矩、转矩平稳
+
+**代价**:
+- 有效气隙增大 → 气隙磁密下降,需更多永磁体(无铁心电机需更显著磁通源)
+- 绕组直接暴露于气隙磁场 → **涡流损耗**;宽磁气隙 → 并联导体**环流损耗**
+- 相电感极低(百 μH 级)→ 电流纹波大、弱磁能力弱,需配高开关频率驱动(WBG)
+
+> 出处:《无铁心轴向磁通永磁电机PCB定子中的环流与涡流损耗》《设计方面、绕组布置与印刷电路板电机的应用:一份全面综述》《轴向磁通永磁电机技术综述》
+
+### 1.2 拓扑分类与选型
+
+| 维度 | 选项 | 工程要点(选型建议) |
+|---|---|---|
+| 磁通方向 | 轴向磁通 AFPM / 径向磁通 RFPM | AFPM:轴向紧凑、转矩密度高;RFPM:工艺成熟。**RFPM 转子长径比 D/L > 12 时 AFPM 转矩更高** |
+| 定转子数 | 单定单转(SSSR)/ 双定单转(DSSR)/ 单定双转(SSDR, TORUS)/ 多盘 | SSDR(双转子夹 PCB 定子)最常见:轴向力平衡、转矩高。SSSR 有轴向力不平衡 |
+| 转子磁体 | 表贴 SPM / 内置 IPM / Halbach 阵列 | SPM 简单;IPM 保护磁体适合高速;**Halbach 无需背铁,转矩密度最高可 +30%**(见 1.4) |
+| 定子结构 | 有槽 / 无槽;有铁心 / 无铁心 | PCB 电机几乎全为**无槽无铁心**;有槽才有高转矩密度但齿槽转矩/铁损 |
+| 绕组布置 | 集中(同心/梯形/螺旋)/ 分布(波绕/径向/弧形/不等宽并联) | 分布绕组减小电阻、THD 更好;波绕在超薄 AFPM 有利(见 3.2) |
+| 相数 | 三相 / 两相 / 多相 / 多三相 | 两相可两片 PCB 错位 90° 电角度叠压;多三相可容错 |
+
+> 出处:《设计方面、绕组布置与印刷电路板电机的应用:一份全面综述》《轴向磁通永磁电机技术综述》《轴向磁通永磁同步电机PCB绕组拓扑比较》
+
+### 1.3 核心尺寸设计方程(可用于初步尺寸脚本)
+
+**AFPM 转矩(YASA/无铁心/单面均适用)**:
+
+```
+T = (π/2) · Bδ · Â · R_om³ · λ(1 − λ²)
+```
+
+- `Bδ`:磁负荷(气隙磁密基波),`Â`:电负荷(与半径有关),`R_om`:转子外径,`λ = R_im/R_om` 内外径比
+- **转矩 ∝ 外径三次方**;功率密集型设计 **λ ∈ [0.65, 0.75]**
+- AFPM vs RFPM 转矩比:`T_afpm/T_rfpm = D_om·λ(1−λ²)/(4L)`(D/L>12 时 AFPM 更优)
+
+**空载气隙磁密基波(表面贴装双转子,固定半径极坐标截面)**:
+
+```
+Bz(θ,z) = Bpk · cosh(πz/τp) · cosθ
+Bpk = (4Br/π) · sinh(π·hp1/τp)/sinh(π·g/(2τp)) · sin(π·wp/(2τp))
+```
+
+**Halbach 转子气隙磁密**:
+
+```
+Bz(θ,z) = 2·Bpk·exp(−αg/2)·cosθ·cosh(αz),   α = π/(w_pm+w_pt)
+Bpk = Br·[1−exp(−α·hp2)]·sin(π/nm)/(π/nm)
+```
+
+**PCB 高速电机线圈磁链(Rome 组)**:
+
+```
+λ = Nt · B̂ · R_ext²(1−k_r²)/p · kw     (k_r=内外径比,kw 绕组系数)
+```
+
+> 出处:《轴向磁通永磁电机技术综述》《采用表面贴装永磁体和Halbach阵列转子的无铁心轴向磁通电机的转矩和功率能力》《高速印刷电路板无铁芯轴向磁通永磁电机的设计》
+
+### 1.4 Halbach 阵列:转矩密度的放大器
+
+- **转矩密度最高 +30%**(同质量/体积下,相比表面贴装 SPM)
+- 一侧磁通显著中和 → **可移除转子背铁** → 减重、消除背铁涡流、更正弦的气隙磁密
+- 高速应用注意:磁体段间吸引力大、需机械强度校核
+- 高速小功率(如 3 万 rpm)用 Halbach + 埋入式磁体 + 非磁钢顶盖(Inconel 625)可让**磁链提高约 57%**
+
+> 出处:《采用表面贴装永磁体和Halbach阵列转子的无铁心轴向磁通电机的转矩和功率能力》《高速无铁心轴向磁通永磁电机及其印刷电路板绕组》
+
+### 1.5 不同导体材料(铜/铝/碳纳米管)
+
+- **转矩与电导率关系**:`T ∝ D_o³·√σ·L`(损耗密度不变时)
+- 同转矩下换材料所需外径比:`D_o2/D_o1 = √(σ1/σ2)`;调整外径比调整轴向长度更划算
+- **CNT**:质量仅铜的 1/6、无趋肤效应、电阻低温系数;但宏观电导率低(约 6×10⁶ S/m,铜约 6×10⁷),需加大体积补偿 → 适合"重量敏感、体积不敏感"(太阳能飞机)
+- **铝**:单位质量电导率是铜 2 倍、比热容 2 倍(过载好),适合重量敏感
+
+> 出处:《无铁心多盘轴向磁通永磁电机:采用碳纳米管绕组》
+
+---
+
+## 2. 损耗机理与计算(本知识库最核心,仿真时损耗设置直接参考)
+
+### 2.1 损耗总览
+
+无铁心 PCB 电机的损耗构成(无定子铁损):
+
+| 损耗 | 成因 | 与运行量的关系 | 备注 |
+|---|---|---|---|
+| 直流铜耗 | 电流 × 电阻 | ∝ I² | PCB 铜量少→电阻大→直流损耗大,是主要设计制约 |
+| 涡流损耗 | 导体暴露于旋转气隙磁场 | ∝ f²·B²(详见 2.3) | PCB 迹线可做窄做薄近似利兹线,但铜量受限 |
+| 环流损耗 | 并联支路感应电压不等 | ∝ (ΔE)²/R,**与转速平方成正比** | 可占额定转矩下总铜耗约 15%(波绕组) |
+| 集肤+邻近 | 高频电流分布 | 频率相关 | 迹线远薄于趋肤深度时可忽略(见 2.5) |
+| 磁钢涡流 | 定子谐波/槽口 | 高频 | 分段可抑制 |
+| 机械/风阻 | 旋转 | ∝ 转速²(浸没式) | 高速或浸油应用不可忽略 |
+| 驱动损耗 | 逆变器开关+导通 | — | WBG 高开关频率时不可忽略 |
+
+> 出处:《无铁心轴向磁通永磁电机PCB定子中的环流与涡流损耗》《Loss- and Thermal-Constrained Design of a PCB Stator Double-Rotor Axial-Flux Motor for Immersed Pump Applications》《永磁无刷电机损耗分离的组合实验与数值方法》
+
+### 2.2 直流铜耗与 PCB 电阻
+
+- PCB 相电阻:`R = ρ·l/A`,`A = w·t`(迹宽×铜厚)。铜厚受限(常规 ≤70 μm,高厚 95 μm,13 oz ≈ 455 μm),迹宽受设计规则限制 → **电阻远大于同体积线绕组**。
+- 多层 PCB 并联:`R_eff = R/(N/2)`(N 为层数,相邻层配对并联)。24 层可把有效电阻降到 R 的 1/12,但**层数过多 → 板厚增加 → 环流损耗上升**(见 2.4)。
+- 例(IIT Bombay 30 krpm 70W 电机):单相电阻约 7 Ω,铜耗达 10.5 W;改 12 层并联后显著下降。
+
+> 出处:《高速无铁心轴向磁通永磁电机及其印刷电路板绕组》《用于高速轴向磁通永磁电机的印刷电路板绕组的多物理场分析》
+
+### 2.3 涡流损耗(PCB 矩形/圆导线公式)
+
+**PCB 矩形导体涡流损耗(Chulaee,波绕组每相)**:
+
+```
+P_ed = π²·Nc·Nt·f²·tw·th·lm / (6ρ) · ( tw²·Bz² + th²·Bφ² )
+```
+
+- `Nc` 线圈数、`Nt` 每线圈匝数、`tw` 迹宽、`th` 迹高、`lm` 平均迹长、`ρ` 铜电阻率
+- **切向磁密 Bφ 比轴向 Bz 小一个数量级** → 加厚迹高(th)对涡流增加很小,是提高铜利用率的关键手段
+- 例:螺旋绕组 1000 rpm 时涡流约 1.1 W;波绕组约 0.5 W
+
+**圆导线涡流损耗(Kamper,电阻受限区)**:
+
+```
+P_e = π·l·d⁴·Bpk²·ω² / (32ρ)          (l 导线长,d 线径)
+P_eddy = π·l·Nc·Nt·Ns·d⁴·Ba²·ω²·σ / 128   (Taran 综述版)
+```
+
+**矩形导体一般式(Taran)**:
+
+```
+P = l·Nc·Nt·Ns·w·h·ω²·σ/24 · ( w²·Baz² + h²·Baφ² )
+```
+
+**端部修正因子**(导体短、端部回流路径短时):
+
+```
+K_s = 1 − tanh(π·Leff/dc) / (π·Leff/dc)
+```
+
+**关键结论**:
+- 无铁心电机中**标准 1D 解析法低估涡流损耗可达 43%**;需多层(轴向分)+ 多片(径向切)2D FE 或 3D FE
+- 3 片(径向切片)2D FE 模型是精度/CPU 的良好平衡
+- 切向磁场分量会带来附加涡流 → 宽铜带导体不利(带状宜窄不宜宽)
+- 涡流损耗与 f²、B² 成正比 → 高速高极数是涡流压力来源
+
+> 出处:《无铁心轴向磁通永磁电机PCB定子中的环流与涡流损耗》《无铁心定子轴向磁通永磁(AFPM)电机中涡流损耗的评估》《轴向磁通永磁电机中交流绕组损耗计算方法综述及一种新的三维有限元与解析混合技术》
+
+### 2.4 环流损耗(无铁心 PCB 电机最大的隐性损耗)
+
+**成因**:PCB 多层/多并联支路处于气隙不同位置 → 感应电压不同 → 并联支路间产生环流。
+
+**公式**:
+
+```
+P_cr = (1/R) · Σ[ Ei − (ΣEi)/n ]²        (n 条并联支路)
+P_c  = Vd² / (R1 + R2 + Rc)               (两层之间,Vd 为两层感应电压差)
+```
+
+**量级数据(Chulaee 实测)**:
+- 波绕组 1000 rpm:涡流 0.5 W,**环流 28.3 W**(单相 420 条迹线)——环流可占额定转矩下总铜耗约 15%
+- 24 层 PCB:顶层与底层感应电压差 Vd ≈ 14 V → 环流损耗约 12 W;**降到 12 层则约 0.5 W**(Neethu 例)
+
+**与转速关系**:环流损耗 ∝ (反电动势差)² ∝ **转速²**。高速必须控制。
+**与制造偏差关系**:
+- 磁体剩磁下降 10% → 环流约 +6 W
+- 0.75 mm 静态偏心 → 环流约 +15 W
+
+**抑制手段(按有效性排序)**:
+1. **层间换位(transposition)**:让并联支路经过所有层位,是首选项(完全换位可将环流压到 ≤1 W,见 2.9 案例)
+2. 圆周方向扩展并联路径(增大 Δr 覆盖)
+3. 轴向贯穿尽可能多的 PCB 层
+4. 加宽迹宽、减少并联数
+5. 绕组重接拆分:把并联支路拆成 4 部分合理并联,环流可降 **110 倍**(切向分组只降 4 倍)
+6. 减小制造不对称(磁体一致性、偏心)
+
+> 出处:《无铁心轴向磁通永磁电机PCB定子中的环流与涡流损耗》《考虑详细 PCB 定子布局的无铁心轴向磁通永磁电机多目标设计优化——将涡流与环流损耗降至最低》《用于高速轴向磁通永磁电机的印刷电路板绕组的多物理场分析》《通过绕组重接最小化无槽无刷直流电机中的环流》
+
+### 2.5 集肤效应与邻近损耗
+
+- **PCB 迹线等效利兹线条件**:`tw << δ`,其中趋肤深度 `δ = 1/√(π·f·μ·σ)`。例:650 Hz 时铜 δ ≈ 2.5 mm,迹宽 0.2 mm(差一个数量级)→ **PCB 天然近似利兹线,集肤效应可忽略**(低速/中速设计)
+- 但**邻近效应**(相邻迹线/层间漏磁)在高速高电流密度下显著,尤其槽内多层堆叠
+- 矩形导体集肤电阻:`R_sk = ρ·l/(w·t_eff)`,`t_eff = δ·(1−e^(−t/δ))`
+- **高速有铁心电机警示**(可迁移到绕组损耗理解):12000 rpm 时交流/直流电阻比 5.58,3000 rpm 时 1.67;并联路径增多损耗激增(8 并联时增加近 10 倍);槽顶导体是热点
+- **利兹线股数最优(Sullivan)**:`F_r = 1 + π²·ω²·μ₀²·N²·n²·d_c⁶·k/(768·ρ_c²·b_c²)`,存在最优股数;设计例 375 kHz 下 130 股 48 AWG 最优,F_r′ ≈ 2.35。股径经验上限:**单线直径 ≤ δ/3**
+
+> 出处:《高速无刷永磁电机中的集肤效应与邻近损耗》《电机中股线涡流损耗的高效计算方法》《利兹线变压器绕组中股数的最优选择》《用于高速轴向磁通永磁电机的印刷电路板绕组的多物理场分析》《设计优化、无铁心轴向磁通永磁电机利兹线与PCB定子绕组》
+
+### 2.6 磁钢涡流损耗
+
+- 无铁心电机无定子铁心,但**转子磁钢/背铁仍会因定子谐波产生涡流**
+- NdFeB 参数(100°C):`μr ≈ 1.05`,`ρ ≈ 1.5×10⁻⁷ Ω·m`(导电!)
+- 抑制:磁钢**周向/轴向分段**;高频时用 CE-FEA(60° 电角度窗口)快速评估
+- 磁钢损耗在总损耗中占比通常较小(极数探索论文中"磁钢损耗占比小,未纳入优化目标"),但高速/大谐波时不可忽略
+
+> 出处:《基于计算高效有限元法的集中绕组永磁同步电机永磁体损耗计算》《系统探索极数对超高效率分数马力轴向磁通永磁电机性能与成本极限的影响》
+
+### 2.7 AC 绕组损耗计算全流程(按精度/成本阶梯)
+
+| 方法 | 精度 | 计算成本 | 适用阶段 |
+|---|---|---|---|
+| 解析闭式(2.3 公式) | 中(可能低估 43%) | 瞬时 | 初筛/趋势 |
+| 2D FE(多层+多片) | 中高 | 分钟级 | 参数化/优化主体 |
+| 2D FE + 解析混合(端部修正 Ks) | 高 | 分钟-小时 | 设计定型 |
+| 3D FE 逐走线(详细) | 最高 | **极高**:2600 万+四面体,64 核 192GB HPC 单电周期 >72h;2265 万单元网格化 12h | 最终验证/样机 |
+| 简化等效 3D FE(超快速,见 4.3) | 高(对低饱和电机) | 大幅降低 | 大规模优化 |
+
+**工程建议**:优化阶段用解析/2D,最终点用 3D 逐走线校核;若 3D 逐走线跑不动,用"等效 3D"(把平面 PCB 迹线等效为简化模型)。
+
+> 出处:《轴向磁通永磁电机中交流绕组损耗计算方法综述及一种新的三维有限元与解析混合技术》《论用于电动飞机推进的无铁心永磁电机设计》《基于波绕PCB定子的无铁芯轴向磁通永磁电机的设计优化与实验研究》
+
+### 2.8 损耗分离实验法(用于验证仿真)
+
+- **最小二乘损耗分离**:把实测总损耗回归到与电流/转速成比例的项(`i²`、`i²ω`、`i²ω²`、`ω²` 等),分离出铜耗、铁耗、机械损耗、杂散损耗。残差 <2%
+- 注意:2D FEA 对磁钢转子涡流可能**高估约 3 倍**(端部/3D 效应),实验验证时留意
+- NASA 飞轮法(真空、无外载荷):**堵转测试**(分离电流损耗,含集肤邻近)→ **空载测试**(旋转损耗)→ **恒流测试**;堵转测试中频率 200→1000 Hz 时电流损耗 +40%
+
+> 出处:《永磁无刷电机损耗分离的组合实验与数值方法》《Experimental Performance Evaluation of a High Speed Permanent Magnet Synchronous Motor and Drive for a Flywheel Application》
+
+### 2.9 损耗工程案例(可直接对标)
+
+| 案例 | 关键数字 | 说明 |
+|---|---|---|
+| Chulaee 波绕/螺旋 | 波绕 1000rpm:涡流 0.5W / 环流 28.3W;螺旋:涡流 1.1W | 螺旋绕法本身环流小,但铜利用率低 |
+| Chulaee 多目标优化样机 | 2100rpm 19N·m,36 极 9 层 PCB,SFF≈0.18-0.20,**完全层间换位**:实测涡流 22.6W(<13%)、环流 ≤1W、机械 30.4W,**效率 95.9%(超 IE4)** | 完全换位是环流杀手锏 |
+| Han 波绕样机 | 3050r/min、300V、16Arms、14Nm、10.8A/mm²;**环流限制转速 ≤1400r/min**(2.75hp) | 不换位时环流直接封顶最高转速 |
+| Sansoni 浸没泵 | 8层3oz 78.9% / 12层2oz 82.6% / 12层3oz 84.4% 效率;解析 vs 实测:效率 80.8 vs 77%、转矩 213 vs 210 mNm | 解析效率误差 ~4.7%,转矩 ~1.4% |
+
+> 出处:见各案例对应论文(Chulaee 两篇、Han ECCE、Sansoni IEEE TIA)
+
+---
+
+## 3. PCB 定子绕组设计与制造约束
+
+### 3.1 PCB 制造参数与成本(仿真参数语义必读)
+
+| 参数 | 典型值 / 范围 | 工程含义 |
+|---|---|---|
+| 铜厚 | 常规 1–2 oz(35–70 μm);高厚 95 μm;13 oz ≈ 455 μm(最大) | 铜厚限制铜量 → 直流损耗制约;加厚 PCB 可显著提转矩(PCB 从 1.5→2mm 厚度,转矩近似成正比提升,其中 75% 来自安匝增加、5% 来自电流层靠近磁体) |
+| 最小线宽 | ≈0.15 mm(IPC-2221 通用) | 决定最小迹宽 |
+| 最小线隙 | ≈0.13 mm(IPC-2221);工程常用 0.2–0.3 mm | 绝缘间隙需满足击穿电压;随铜厚增加需加大 |
+| 层数 | 2–24 层;**>6 层成本急剧上升** | 层数↑→电阻↓ 但环流↑板厚↑;12 层是高速样机常用折中 |
+| 基板 | FR-4(玻璃化 180°C,导热仅 0.2 W/m·K) | FR-4 导热差是主要热瓶颈;PCB 与铜热膨胀系数接近,热应力小 |
+| 铜/板厚约束 | 总 PCB 厚度决定有效气隙 | 层数增加 → 板厚增加 → 磁链下降(Neethu:总额定电压/转矩要求 PCB 厚度 ≤3mm) |
+| 成本规律 | 1000 块单件成本约为 10 块的 **13%**;5→1000 件单板成本可降 **87%** | 批量制造是 PCB 电机核心卖点 |
+| 填充因子 | PCB ≈ 0.30–0.31(波绕 0.2286);利兹线 ≈ 0.386 | PCB 铜占有率低 → 同体积直流损耗高,但可用更高电流密度补偿 |
+
+**铜厚与涡流权衡**:迹高 th 可加厚(Bφ 小),但迹宽 tw 加宽会直接增涡流(∝tw²)→ **宽迹面朝径向减涡流,厚迹增加铜量**是核心策略。
+
+> 出处:《设计方面、绕组布置与印刷电路板电机的应用:一份全面综述》《轴向磁通永磁同步电机PCB绕组拓扑比较》《无铁心轴向磁通永磁电机PCB定子中的环流与涡流损耗》《设计优化、无铁心轴向磁通永磁电机利兹线与PCB定子绕组》《用于集成3D打印换热器的电机PCB绕组》
+
+### 3.2 绕组拓扑比较(数值基准,可写入设计脚本)
+
+同一电机(16 极 2000rpm,外径 92mm 内径 50mm,气隙 1mm,磁体 2mm,Br=1.33T,PCB 2 层 70μm,线宽 0.8mm):
+
+| 绕组拓扑 | 相电阻 | 相对同心 | 感应电压/转矩 | THD | 备注 |
+|---|---|---|---|---|---|
+| 同心 | 1.04 Ω | 基准 | 高 | 中 | 端部浪费 |
+| 径向 | 0.87 Ω | **−20%** | 最高 | **最小** | 相电阻低 20-24%,THD 最优 |
+| 弧形 | 0.85 Ω | −18% | 中高 | — | — |
+| 并联 | 0.79 Ω | −24% | 中 | — | 电阻最小 |
+| 不等宽并联 | 0.59 Ω | **−43%** | 中 | — | 再省铜耗 17% |
+
+- 解析(磁标量势法)气隙磁密基波与 FEA 差 <1%
+- 2000 rpm 时涡流 10–20 mW 可忽略(低中速 PCB 绕组)
+- **结论:径向(并联)绕组是 PCB AFPM 综合最优**;波绕适合超薄/无端部场景
+
+**绕组四种 PCB 拓扑**:分布式 / 螺旋 / 递进波 / 连续波。波绕优点:无端部、每匝磁链相同、反电动势正弦(THD 12%→6%、幅值 45→56V 的实测例)。
+
+**不等宽绕组(UEW)**:有源导体向两侧加宽,减小电阻、增大铜占比、增大散热面积。设计参数:
+- 内外径比 γ = 1.5–2.2(小型 1.5–1.73)
+- 每极每相匝数:`N = π·D_mi / (2·m·p·(w_ie + d_L))`(先定 N 再定内径)
+- 内端线宽按最大电流密度(内端电流密度最大、发热集中)设计
+- 典型:d_L=0.3mm,铜厚 4oz
+
+> 出处:《轴向磁通永磁同步电机PCB绕组拓扑比较》《高速无铁心轴向磁通永磁电机及其印刷电路板绕组》《采用不等宽PCB绕组的轴向磁通永磁发电机的电磁设计与分析》《设计方面、绕组布置与印刷电路板电机的应用:一份全面综述》
+
+### 3.3 降低交流损耗的绕组技术汇总
+
+| 技术 | 机理 | 效果/代价 |
+|---|---|---|
+| 层间换位 | 并联支路经所有层位,感应电压均等 | 环流降到 ≤1W(首选) |
+| 狭缝(slit) | 导体开缝抑制涡流路径 | 供电损耗 α 与涡流 β 权衡;**长狭缝并联连接最佳,交叉连接使 β 翻倍**;最优并联狭缝数固定 |
+| 不等宽(UEW) | 加宽有源导体 | 电阻↓、铜占比↑ |
+| 逆导体(inverse trace) | 韩国 EWP 用逆导体图案 | 既定尺寸下最小化电流密度 |
+| 径向波绕 | 无端部、分布绕组 | 反电动势正弦、转矩好 |
+| 加厚迹高 | Bφ 小,增厚不增涡流 | 提 SFF 与转矩 |
+| 磁体形状/线圈几何优化 | 缩短涡流路径 | 降低涡流损耗 |
+
+> 出处:《狭缝结构对无槽永磁电机 PCB 绕组中涡流与供电电流损耗的影响》《考虑详细 PCB 定子布局的无铁心轴向磁通永磁电机多目标设计优化》《PCB定子电动水泵电机的设计》
+
+### 3.4 利兹线 vs PCB 定子(选型对照)
+
+| 维度 | 利兹线 | PCB |
+|---|---|---|
+| 填充因子 | 0.386 | 0.303 |
+| 交流损耗 | 需股数优化(≤δ/3) | 迹线天然细,等效利兹 |
+| 制造 | 线绕、灌封、定位精度差;**成本高达 300 欧元/kg** | 高重复、高精度、批量降本 87% |
+| 热 | 灌封后一般 | 平面散热好、可贴换热器 |
+| 电磁效率(同案例) | **98.8%(最高)** | 略低但接近 |
+| 适用 | 高转矩密度、一次少量 | 批量、超薄、模块化 |
+
+**注意**:利兹线最大单线直径 ≤ δ/3;100 股 AWG 40 是常用配置(38 AWG 单线亦可)。
+
+> 出处:《设计优化、无铁心轴向磁通永磁电机利兹线与PCB定子绕组》《高速印刷电路板无铁芯轴向磁通永磁电机的设计》
+
+---
+
+## 4. 设计优化方法
+
+### 4.1 核心难点:3D FEA 太贵 → 代理辅助/简化模型
+
+AFPM 磁路三维(径向依赖 + 内外缘边缘磁通),2D 模型精度不足;但逐走线 3D FEA 单点 2600 万单元 / >72h。主流破解:
+
+**① 两级代理辅助优化(2L-SAMODE,Ionel 组)**
+- 外环进化算法(差分进化 DE/MODE),内环 Kriging 代理模型估值,仅最有前景的设计用高保真 3D FEA
+- **效果:同 Pareto 前沿,FEA 评估从 886 次降到 163 次**(上千次 → <200 次)
+- 初始样本池用随机设计(宽空间非线性下优于 DoE)
+- 误差超阈值(如 5%)的设计补充进样本池
+- 适用于 3D FEA 单点 15 分钟–数小时的场景(正是本工程可复用的思路)
+
+**② 超快速/最少解 FEA(Chulaee)**
+- 无铁心电机**无饱和**(转矩-电流线性)、齿槽转矩低 → 用最少瞬态解估算性能
+- 几何对称:**1 极 + 轴向 1/2**(1/26 电机)→ 计算量减半以上
+- 平面 PCB 迹线用"等效 3D 简化模型"(见 4.3)
+- 验证:4.2kW/2100rpm/26 极样机实验吻合
+
+**③ 解析 + 2D 组合**:Marcolini 平均半径 2D 解析电磁 + 并行热模型,初步设计仅 **35 秒**(i7 2015)。
+
+> 出处:《超快速无铁心轴向磁通永磁同步电机有限元分析》《系统探索极数对超高效率分数马力轴向磁通永磁电机性能与成本极限的影响》《无铁心轴向磁通永磁电机的新型多物理场设计方法》《轴向磁通永磁电机技术综述》
+
+### 4.2 多目标优化案例(可直接对标目标函数/变量设计)
+
+**案例 A:Chulaee PCB 定子布局多目标优化**
+- 目标:最小化涡流 + 环流损耗;算法 MODE;480 候选设计
+- 关键变量:SFF(槽填充因子)、迹宽/迹高、层数、是否换位
+- 结果:36 极 9 层 PCB、完全换位 → 效率 95.9%(超 IE4)
+
+**案例 B:Taran 极数探索(SAMODE)**
+- 目标:总损耗 F_l = W_Cu + W_c + W_pm;成本 F_c = m_c + 3·m_Cu + **24·m_pm**(磁钢成本权重 24 倍!)
+- 变量 8 个(比例化):气隙 g、裂比 kds、磁钢厚 kpm、槽宽 ksw、极弧比 kp、轭厚 kry、悬垂比 k_oh(−1~+1,负悬垂省磁钢)
+- 槽/极组合:12/10、24/20、36/30、48/40;目标 1050rpm 5.4Nm
+- 规律:极数↑→铜损↓(端部短)、铁损↑(频率高)→ 存在最优极数;磁钢损耗占比小可忽略
+
+**案例 C:Tokgöz GaN 集成(NSGA-II)**
+- 目标:效率 + 功率密度(kW/L);变量:相电流、迹厚、匝数、磁钢厚、极数、内外径
+- 解析目标函数 0.02 秒/评估 → 可加大变量数;代=种群=100
+- 结果:270W,0.36Nm 时 82%、0.18Nm 时 90%,定子温度 59°C
+
+**通用建议(对自动化仿真系统)**:目标函数优先(损耗/成本/转矩密度/效率);成本函数里磁钢权重大;用解析/2D 粗筛 + 3D 精验的两级流程。
+
+> 出处:《考虑详细 PCB 定子布局的无铁心轴向磁通永磁电机多目标设计优化》《系统探索极数对超高效率分数马力轴向磁通永磁电机性能与成本极限的影响》《采用 GaNFETs 的集成电机驱动系统中优化 PCB 电机的机械与热设计》
+
+### 4.3 平面 PCB 线圈的等效 3D FEA 简化模型
+
+- 逐走线 3D 网格单元以千万计,严重拖慢优化
+- **简化等效模型**:利用无铁心电机线性/低饱和特性,将平面 PCB 绕组几何系统简化为较粗网格仍保持精度
+- 配合对称边界(1/26 + 轴向 1/2),数百候选设计评估成为可能
+- 工程含义:本工程在 Motor-CAD/Ansys 中建模时,可优先用简化绕组 + 对称周期,把单点求解压到分钟级,再对最终点细跑
+
+> 出处:《超快速无铁心轴向磁通永磁同步电机有限元分析》
+
+### 4.4 极数选择经验
+
+- 高极数 → 高转矩密度、端部短(铜损↓)、齿槽转矩小;但频率↑→铁损↑(有铁心)、涡流↑、驱动频率↑
+- 无铁心无齿槽,可放心用高极数(26 极是 Chulaee 多篇样机的常用值)
+- 低速高转矩、低功率高速无刷 PM 电机:高极数是主流(转矩密度可达 2 Nm/kg 级)
+
+> 出处:《系统探索极数对超高效率分数马力轴向磁通永磁电机性能与成本极限的影响》《无铁心轴向磁通永磁电机的新型多物理场设计方法》
+
+### 4.5 无铁心 AFPM 多物理场设计方法(Rome 组,完整闭环)
+
+1. 由 **2p/Nc 比值**确定拓扑族
+2. 平均半径 2D 解析电磁模型(含多层轴向厚线圈)
+3. 热模型**并行**评估热点温度
+4. 迭代收敛 → 输出满足热约束的设计
+- C-AFPM 峰值基波气隙磁密可超 **0.6 T**
+- 设计脚本 35 秒/轮 → 适合自动化探索
+
+> 出处:《无铁心轴向磁通永磁电机的新型多物理场设计方法》
+
+---
+
+## 5. 热管理与机械设计
+
+### 5.1 冷却方式对照(可写进热网络脚本)
+
+| 冷却方式 | 典型换热系数 h (W/m²·K) | 特点 |
+|---|---|---|
+| 自然对流(TENV) | 5–15 | 静音、免维护、功率密度低 |
+| 内部风扇(TEFC) | 20–80 | 中功率首选 |
+| 轴向通风道 | 50–150 | 需转子开槽、风摩损增加 |
+| 机壳水套 | 500–3000 | 高功率密度、电动车主流 |
+| 定子槽内油冷 | 800–4000 | 直接冷却绕组、绝缘兼容难 |
+| 空气 | — | 密度 1.2 kg/m³、热容 1.00 kJ/kg·K、导热 0.026 W/m·K |
+| 矿物油 | — | 导热 0.15 W/m·K、热容 1.67 kJ/kg·K、密度 800 kg/m³ → 散热可达空气 3 倍+ |
+
+- 空气冷却在**定子损耗密度超 1500 kW/m²** 时失效 → 高功率密度(≥2.0 kW/kg)需液冷
+- 旋转端部空气对流经验式:`h_air ≈ 5.6·√(v_tip)` W/(m²·K)(v_tip 转子外径线速度 m/s)
+- 水套螺旋通道:Nu=0.023·Re^0.8·Pr^0.4,流速 0.5–2 m/s 时 h=1000–4000;压降∝v^1.8
+- 辐射:涂黑漆 ε=0.9、铝裸面 ε=0.2;100°C 温升范围内占比<10%(TENV 高温段 20–30%)
+
+> 出处:《系统、方法和装置:用于直接液冷轴向磁通电机,配备 PCB 定子》《多物理场仿真设计:电机、电力电子与驱动》
+
+### 5.2 PCB 电机温度限制(热设计边界)
+
+| 部位 | 温度限值 | 备注 |
+|---|---|---|
+| PCB 层压材料 | ~170°C(FR-4 玻璃化 180°C) | 超过即过早失效 |
+| 覆盖层(solder mask) | ~180°C | — |
+| 永磁体(NdFeB) | ~120°C(连续) | 高温退磁风险 |
+| 绕组温升 | NEMA B 级 80°C;F 级绝缘 105°C | Infinitum 样机:40°C 环境 + 34°C 绕组温升 |
+
+**FR-4 导热差(0.2 W/m·K)是主要热瓶颈**;对策:液冷、3D 打印换热器贴合、油冷、加厚铜层(导热好)。
+
+> 出处:《Loss- and Thermal-Constrained Design of a PCB Stator Double-Rotor Axial-Flux Motor for Immersed Pump Applications》《双转子轴向磁通永磁电机采用 PCB 定子》
+
+### 5.3 PCB 电机热设计案例
+
+**① 直接液冷(Infinitum 专利)**
+- 轴分配液体冷却剂到转子与 PCB 定子之间气隙 → 冷却液流经 PCB 表面直接带走热量
+- 避免冷却液进入窄气隙(拖曳损失);非导电非腐蚀油(矿物油/合成传动油/硅油)
+- 热路径:线圈→PCB 层传导→表面→冷却剂对流;部分经层间传导到壳体
+- 效果:电流密度可达传统液冷电机 **4–5 倍**
+
+**② 3D 打印换热器(WEMPEC)**
+- 刚性 PCB 径向定子 + 槽内 3D 打印换热器(TPU 导热 6 W/m·K 耐 110°C / 尼龙 4 W/m·K 耐 200°C)
+- 冷却液-HX 界面平均换热系数 ≈ **14000 W/m²·K**;0.2 L/min 压降仅 153 Pa
+- 导热环氧粘接(k_epx≈? 见表 I,约 1–3 W/m·K),翅片穿插 PCB 层间
+- 意义:把换热器移到绕组旁,电流密度↑、槽面积↓、体积↓
+
+**③ GaN 集成电机驱动(Tokgöz & Keysan)**
+- 电机+驱动器共壳体(外壳三合一:机械集成/热冷却/EMI 屏蔽)
+- 双转子单定子 + 被动冷却 → 270W 时定子温度仅 59°C
+- PCB 定子走线本身兼作散热/测温通道
+
+> 出处:《系统、方法和装置:用于直接液冷轴向磁通电机,配备 PCB 定子》《用于集成3D打印换热器的电机PCB绕组》《采用 GaNFETs 的集成电机驱动系统中优化 PCB 电机的机械与热设计》
+
+### 5.4 机械设计要点
+
+- **轴向磁拉力**(单面结构):`F_z = B_g²·S/(2μ₀)`,可把轴承/装配吃掉;双转子平衡结构可消除
+- 无铁心电机轴向磁拉力大幅削弱(磁通不过定子铁心),这是无铁心的重要机械优势
+- 高速离心力:`F_c = m·ω·v`,转子半径宜小;磁体用保持套筒或埋入式+非磁钢顶盖(Inconel 625)
+- 转子背铁:双转子同速旋转→背铁无基频涡流,可用**低碳钢(无需叠片)**;背铁厚度超过最小值后对转矩无影响
+- 高速应用需校核转子应力与临界转速(无铁心通常临界转速远高于额定)
+
+> 出处:《双转子轴向磁通永磁电机采用 PCB 定子》《高速无铁心轴向磁通永磁电机及其印刷电路板绕组》《设计方面、绕组布置与印刷电路板电机的应用:一份全面综述》
+
+---
+
+## 6. 高速与驱动控制
+
+### 6.1 高速 PCB 电机设计要点
+
+- 高速无铁心电机:无铁耗、零齿槽转矩、转矩平稳 → 非常适合方波/无感控制
+- 约束:**环流损耗 ∝ 转速²** → 高速必须换位/少并联(Chulaee:不换位 3050rpm 设计被环流限制到 1400rpm)
+- 涡流 ∝ f² → 高极数高速时 PCB 迹线涡流压力大
+- 高速转子:埋入式磁体/Halbach + 套筒,控制线速度
+- 低电感(百 μH)→ 电流纹波大 → 需 50–100 kHz 开关频率(WBG)或外接电感器
+
+> 出处:《设计_of_a_High_Speed_Printed_Circuit_Board_Coreless_Axial_Flux_Permanent_Magnet_Machine》《高速无铁心轴向磁通永磁电机及其印刷电路板绕组》《基于波绕PCB定子的无铁芯轴向磁通永磁电机的设计优化与实验研究》
+
+### 6.2 WBG 驱动与宽速域控制
+
+- **问题**:无铁心相电感极低(例 2.2kW/26 极电机相电感仅 **152 μH**、相电阻 0.85Ω)→ 电流纹波大、弱磁能力弱
+- **对策**:SiC/GaN 高开关频率(50–100 kHz 甚至 1 MHz);GaN 开关频率 1MHz 时集成驱动效率 82–90%
+- **双模控制**(FOC + 方波):低速 FOC 高动态、高速方波充分利用直流母线电压 + 无需高精度编码器(霍尔/无感),拓宽速域
+- 低电感也带来优点:电气时间常数小、动态响应极快、电流调节带宽高
+
+> 出处:《采用宽禁带半导体器件的高极数无铁心轴向磁通永磁电机宽速域灵活控制》《采用 GaNFETs 的集成电机驱动系统中优化 PCB 电机的机械与热设计》
+
+### 6.3 弱磁设计(有铁心通用,供理解极速域)
+
+- 理想无限弱磁条件:**永磁磁链标幺值 = d 轴电感**(Ψ_mo = L_d pu)
+- 凸极比 ξ = L_q/L_d;**ξ<1(L_d≥L_q)非常规结构**转矩/功率因数/控制特性更优
+- 满足理想弱磁的电机额定功率因数均约 0.707–0.721
+- 磁钢/气隙参数:d 轴饱和系数 k_so≈1.1–1.3;极弧比 IPM 0.85–0.95、SPM ≈2/3
+- 无铁心电机弱磁能力弱 → 靠方波/升压扩展速域(见 6.2)
+
+> 出处:《适用于弱磁应用的永磁同步电机设计考虑因素》《论用于电动飞机推进的无铁心永磁电机设计》
+
+---
+
+## 7. 测试与验证方法
+
+### 7.1 电感/参数测试(IEEE Std 1812)
+
+- **开路 + 短路试验**:开路测反电动势 → 永磁磁链 `ψm = E_oc/(2πf)`;短路测 `L_d = √(Z²−R²)/(2πf)`(三相对称短路≈纯 d 轴激励)
+- **q 轴电感**需额外测:静态转矩法 `ψm = 2·T_m/(3·p·I_q)`,再结合 L_d 求 L_q
+- 例(9 槽 6 极 IPM 样机):L_q = 7.5 mH
+- **注意**:短路时磁密低于负载工况 → L_d 可能被高估;饱和/交叉耦合时 L_d=f(I_d)、L_q=f(I_q)、ψm=f(I_q)
+- 对无铁心电机:电感极小且线性(无饱和),测试相对简单,但电流纹波大需高带宽测量
+
+> 出处:《内置式永磁同步电机电感测试——依据新版IEEE Std 1812及典型实验室实践》
+
+### 7.2 解析 vs 仿真 vs 实验的典型误差带(验收依据)
+
+| 对比项 | 论文报告误差 | 说明 |
+|---|---|---|
+| 解析气隙磁密基波 vs 2D FEA | <1% | 磁标量势法 |
+| 解析效率 vs 实测(浸没泵) | ~4.7%(80.8% vs 77%) | 解析偏乐观 |
+| 解析转矩 vs 实测 | ~1.4%(213 vs 210 mNm) | 转矩预测可信 |
+| 2D FEA 磁钢涡流 vs 实测 | 高估约 3 倍 | 端部/3D 效应 |
+| 损耗分离回归残差 | <2% | 最小二乘法 |
+| 空载反电动势(波绕逐走线模型) | 与实测匹配 | 波形正弦、两相平衡 |
+| 电感(ψm)虚拟 vs 实测 | 吻合(0.107 Wb) | Maxwell 虚拟试验 |
+
+**工程含义**:给仿真加 ±5% 效率、±2% 转矩的验收容差是现实的;磁钢涡流要用 3D 或实测校正。
+
+### 7.3 高速/真空电机效率测试(NASA 飞轮法)
+
+- 场景:真空腔+磁悬浮,无法直接加机械负载
+- 三步法:**堵转测试**(电流损耗,含集肤邻近)→ **空载测试**(电流损耗+旋转损耗)→ **恒流测试**
+- 旋转损耗 = 空载功率 − 堵转电流损耗(同频率点)
+- 功率测法:P_DC = V_DC·I_DC;P_M/G = Σ v_a·i_a(瞬时值积分)
+- 数据:3kW、2 极、6–60 krpm、1000 Hz;频率 200→1000 Hz 电流损耗 +40%
+- **对该工程的意义**:本工程做自动化仿真时,堵转/空载/负载三类工况的损耗分离与仿真对账可以复用这套逻辑
+
+> 出处:《Experimental Performance Evaluation of a High Speed Permanent Magnet Synchronous Motor and Drive for a Flywheel Application at Different Frequencies》
+
+---
+
+## 8. 对本工程 Motor-CAD 自动化仿真的工程启示(SOP 建议)
+
+> 本节把前 7 节的论文知识翻译成可直接落到本仿真系统脚本/参数表/校验逻辑的动作。
+
+### 8.1 建模与对称性(时间预算)
+
+- **无铁心 PCB 电机无饱和、线性**:可用 1 极 + 轴向 1/2 对称模型,计算量减半以上
+- 平面 PCB 迹线优先用**简化等效 3D** 或 2D 多层+多片;仅最终点用逐走线
+- 解析/2D 粗筛 → 3D 精验的两级流程,单点求解压到分钟级
+- 参考时间锚点:逐走线 3D 单电周期 >72h(HPC)不可行;简化 3D 数百点可跑
+
+### 8.2 损耗设置清单(仿真必须算全的项)
+
+1. **直流铜耗**:按迹宽/迹高/铜厚实算 R,注意温度系数(铜 α≈0.0039/K)
+2. **涡流损耗**:用矩形导体公式(2.3);Bφ 分量可近似忽略但高速要核
+3. **环流损耗**:**必须评估并联支路/多层感应电压差**;默认给"是否换位"参数;不换位时高速环流会封顶转速
+4. **磁钢涡流**:NdFeB 导电(ρ≈1.5e-7 Ω·m),高频谐波下分段评估
+5. **机械/风阻**:高速或浸油场景按转速²估算
+6. **PCB 基板/覆盖层**:FR-4 损耗角小可忽略,但导热差影响温升
+7. **驱动损耗**:WBG 高开关频率时按开关+导通估算(本工程若含驱动模块)
+
+### 8.3 参数校验与回读(结合本工程回读机制)
+
+- **气隙磁密**:解析基波与 FEA 差 <1% 可作为解析模型的置信判据
+- **反电动势/转矩**:解析 vs 3D 期望差 ≤2%(转矩)、≤5%(效率)
+- **环流损耗**:若仿真出的环流 > 涡流一个量级,先查并联支路是否换位/是否制造偏心假设
+- **电感**:无铁心电机电感小且线性,回读异常先怀疑饱和模型设置
+
+### 8.4 设计变量与约束(推荐纳入自动化参数表)
+
+| 参数 | 推荐范围/取值 | 依据 |
+|---|---|---|
+| 内外径比 λ | 0.65–0.75 | AFPM 综述 |
+| 极数 | 无铁心可用 26 极+ | Chulaee 样机 |
+| PCB 层数 | 6–12(>6 成本升,12 为高速折中) | 多篇 |
+| 迹宽/迹高 | 迹宽>迹高有利(涡流∝tw², 厚不增涡流) | 环流涡流论文 |
+| 线隙 | ≥0.13–0.3 mm | IPC-2221/设计 |
+| SFF | 0.18–0.23 | Chulaee |
+| 电流密度 | 10–24 A/mm²(液冷可高) | 多篇 |
+| 磁钢 | NdFeB Br≈1.3T,μr≈1.05 | 多篇 |
+| 成本函数 | 磁钢权重 24×铜 | Taran |
+
+### 8.5 需明确告知软件工程师的"论文已证明但易错"点
+
+- 标准 1D 解析会低估涡流 43%(用 2.3 的公式或 2D 多片)
+- 环流损耗 ∝ 转速²,优化到高转速必须考虑换位
+- FR-4 导热 0.2 W/m·K → 热模型里 PCB 层间热阻不可忽略
+- 无铁心低电感 → 若做驱动耦合仿真要配高开关频率,否则电流纹波失真
+
+---
+
+## 9. 常用数值速查表
+
+### 9.1 材料参数
+
+| 材料 | 电导率 σ (S/m) | 电阻率 ρ (Ω·m) | 密度 | 备注 |
+|---|---|---|---|---|
+| 铜 | ≈5.8e7 | ≈1.72e-8 (20°C) | 8960 | α≈0.0039/K |
+| 铝 | ≈3.5e7 | 2.7e-8 | 2700 | 单位质量电导为铜 2 倍 |
+| 碳纳米管(宏观) | ≈6e6 | — | 铜的 1/6 | 无趋肤效应 |
+| NdFeB 磁钢 | — | ≈1.5e-7 (100°C) | — | μr≈1.05,导电 |
+
+### 9.2 常用经验值
+
+| 量 | 值 |
+|---|---|
+| 趋肤深度(铜) | 650Hz→2.5mm;10kHz→0.66mm;100kHz→0.21mm |
+| 利兹线股径上限 | δ/3 |
+| C-AFPM 峰值基波气隙磁密 | 可超 0.6 T |
+| 无铁心电机比功率现状 | 0.3–2.3 kW/kg(NASA 目标 13 kW/kg) |
+| AFPM 市场预测 | 2022 1.503 亿 → 2032 3.955 亿美元(CAGR 10.1%) |
+| PCB 成本 | >6 层急剧升;1000 件/10 件单件比 ≈13% |
+
+---
+
+## 10. 论文清单与速查索引(38 篇)
+
+| # | 论文(翻译稿名) | 核心主题 | 最值得记住的 1 句话 |
+|---|---|---|---|
+| 1 | Loss- and Thermal-Constrained Design…Immersed Pump | 损耗+热约束设计 | 解析 vs 实测效率误差 4.7%、转矩 1.4%;FR-4 导热差是热瓶颈 |
+| 2 | 多物理场仿真设计:电机、电力电子与驱动(Wiley) | 通用仿真方法论 | 虚拟样机四步流程 + LPTN + 代理优化;Motor-CAD 生成效率图 |
+| 3 | 轴向磁通永磁电机技术综述(Gadiyar & Severson) | 综述/尺寸设计 | T=(π/2)BδÂRom³λ(1−λ²),λ∈[0.65,0.75],D/L>12 时 AFPM 更优 |
+| 4 | 无铁心轴向磁通永磁电机的新型多物理场设计方法 | 多物理场设计 | 平均半径 2D+热并行,35 秒/轮;C-AFPM 磁密可超 0.6T |
+| 5 | 无铁心多盘轴向磁通永磁电机:采用碳纳米管绕组 | 导体材料 | T∝Do³√σL;CNT 轻 6 倍但电导率低 |
+| 6 | 轴向磁通电机拓扑结构标志着下一代电动机的到来 | 产业/Infinitum | Aircore:可靠性 10×、体积重量 −50%、铜 −66%、电流密度 4–5× |
+| 7 | 系统、方法和装置:用于直接液冷轴向磁通电机(专利) | 液冷 | 轴分配冷却液直接冷却 PCB;>1500kW/m² 空气失效 |
+| 8 | 无铁心定子轴向磁通永磁(AFPM)电机中涡流损耗的评估 | 涡流计算 | 标准解析低估 43%;多层+多片 2D FE 最优;3 片平衡 |
+| 9 | 狭缝结构对无槽永磁电机 PCB 绕组中涡流与供电电流损耗的影响 | 狭缝技术 | 长狭缝并联最佳、交叉连接 β 翻倍 |
+| 10 | 通过绕组重接最小化无槽无刷直流电机中的环流 | 环流抑制 | 拆 4 部分并联环流降 110 倍 |
+| 11 | 无铁心轴向磁通永磁电机PCB定子中的环流与涡流损耗 | **环流/涡流核心** | 环流占铜耗 ~15%、∝转速²;换位是首选抑制 |
+| 12 | 设计方面、绕组布置与印刷电路板电机的应用:一份全面综述 | 综述/拓扑 | PCB 电机全分类;分布式绕组优于集中式 |
+| 13 | 轴向磁通永磁同步电机PCB绕组拓扑比较 | 绕组比较 | 径向/并联绕组电阻最低、THD 最好;涡流 10–20mW 可忽略 |
+| 14 | 采用不等宽PCB绕组的轴向磁通永磁发电机的电磁设计与分析 | 不等宽绕组 | UEW 减阻增铜;γ=1.5–2.2 |
+| 15 | Design_of_a_High_Speed_Printed_Circuit_Board_Coreless_Axial_Flux… | 高速设计 | 1kW/7500rpm;磁矢势场模型+转矩闭式 |
+| 16 | 用于高速轴向磁通永磁电机的印刷电路板绕组的多物理场分析 | 高速多物理场 | 24 层 Vd=14V/环流 12W→12 层 0.5W |
+| 17 | 双转子轴向磁通永磁电机采用 PCB 定子 | Infinitum 样机 | 10HP/1800rpm 效率 93% IE5;磁密 0.55T |
+| 18 | 采用 GaNFETs 的集成电机驱动系统中优化 PCB 电机的机械与热设计 | GaN 集成 | 1MHz 开关、270W、82–90% 效率、59°C |
+| 19 | 设计优化、无铁心轴向磁通永磁电机利兹线与PCB定子绕组 | 利兹线 vs PCB | PCB 填充 0.303 vs 利兹 0.386;利兹最高 98.8% |
+| 20 | 基于波绕PCB定子的无铁芯轴向磁通永磁电机的设计优化与实验研究 | 波绕样机 | 3050rpm 设计被环流限到 1400rpm;2265 万单元 |
+| 21 | 论用于电动飞机推进的无铁心永磁电机设计 | 航空推进 | 2640 万单元>72h;55 层并联达 10.5kW |
+| 22 | 用于集成3D打印换热器的电机PCB绕组 | 热管理 | 槽内 HX,h=14000W/m²·K |
+| 23 | 考虑详细PCB定子布局…多目标设计优化(涡流环流最小) | PCB 布局优化 | 完全换位:涡流 22.6W、环流≤1W、效率 95.9% |
+| 24 | 采用表面贴装永磁体和Halbach阵列转子的…转矩和功率能力 | Halbach | Halbach 转矩密度 +30%、免背铁 |
+| 25 | 适用于弱磁应用的永磁同步电机设计考虑因素 | 弱磁 | 理想弱磁 Ψmo=L_d;ξ<1 结构更优 |
+| 26 | 超快速无铁心轴向磁通永磁同步电机有限元分析 | 超快速 FEA | 1 极+轴向 1/2 对称;最少解;等效 3D |
+| 27 | 系统探索极数对超高效率分数马力…性能与成本极限的影响 | 极数/代理优化 | 2L-SAMODE:886→163 次 FEA;磁钢成本权重 24× |
+| 28 | 轴向磁通永磁电机中交流绕组损耗计算方法综述及一种新的三维… | AC 损耗综述 | 矩形/圆导线涡流公式 + 端部修正 Ks |
+| 29 | 基于计算高效有限元法的集中绕组永磁同步电机永磁体损耗计算 | 磁钢损耗 | CE-FEA;NdFeB ρ=1.5e-7 μr=1.05 |
+| 30 | 高速无铁心轴向磁通永磁电机及其印刷电路板绕组 | 高速 Halbach | 30krpm/70W;Halbach 磁链+57%;波绕 THD 6% |
+| 31 | 内置式永磁同步电机电感测试——依据新版IEEE Std 1812… | 参数测试 | 短路测 Ld、静态转矩测 Lq;Lq=7.5mH |
+| 32 | 永磁无刷电机损耗分离的组合实验与数值方法 | 损耗分离 | 最小二乘分离;残差<2%;2D 磁钢涡流高估 3 倍 |
+| 33 | Experimental Performance Evaluation…Flywheel(NASA) | 高速效率测试 | 堵转/空载/恒流三测试;200→1000Hz 电流损耗+40% |
+| 34 | 利兹线变压器绕组中股数的最优选择 | 利兹线 | 最优股数公式;375kHz 130 股 48AWG 最优 |
+| 35 | 电机中股线涡流损耗的高效计算方法 | 股线涡流 | CE-FEA 快速算;矩形股线公式 |
+| 36 | 高速无刷永磁电机中的集肤效应与邻近损耗 | 集肤/邻近 | 12000rpm 交直电阻比 5.58;并联 8 路损耗×10 |
+| 37 | 采用宽禁带半导体器件的高极数无铁心…宽速域灵活控制 | WBG 控制 | 相电感 152μH;FOC+方波双模;50–100kHz |
+| 38 | PCB定子电动水泵电机的设计 | 韩国 EWP | 12V 分布式绕组;14A/mm²、效率 82.62%;逆导体降 AC 损耗 |
+
+---
+
+## 附:阅读边界说明
+
+- 本知识库基于翻译稿精读 + 关键原文核对整理;**未逐篇逐句验证公式符号**,重要公式在落地进仿真脚本前请回原文核对(翻译稿在 `书籍与论文/相关论文-Chulaee.../翻译稿/`)。
+- 个别样机数字来自论文作者报告(含仿真与实测),非本工程实测。
+- Infinitum 双转子论文正文"110 HP(7.46kW)"与 7.46kW=10HP 矛盾,按 10 HP 理解。
+- 论文中的 3D FEA 时间(72h、12h 等)依赖其具体 HPC 硬件,仅作量级参考。
+
+
+
+
+

+ 155 - 0
docs/PLATFORM_DESIGN_V2.md

@@ -0,0 +1,155 @@
+# PCB轴向磁通电机自动化仿真系统 — 平台化升级设计方案 V2.0
+
+| 项 | 内容 |
+|---|---|
+| 文档版本 | V2.0 |
+| 作者 | Car.Lin / AI 协助 |
+| 日期 | 2026-08-29 |
+| 状态 | 评审稿 + 第一批已落地(2026-08-29:afmcore 共享核心层 + 指标单一事实源 + 适配器抽象) |
+| 关联 | 原《设计方案介绍》V1.1 + 《P3-评审响应与更新计划》 |
+
+---
+
+## 1. 为什么要平台化
+
+### 1.1 现状能力(已实现且扎实的部分)
+
+- 双系统解耦(Web 方案端 + 本地执行端)REST 通信已跑通
+- 方案生成三源融合(规则引擎 + Kimi k3 AI + 经验库检索)已落地
+- 本地执行器真实驱动 Motor-CAD,全链路(Web 下发 → 本地认领 → 真实磁计算 → 回传)已实测通过
+- 工程纪律(回读校验/基线重载/弹窗抑制/逐点落盘/纯 ASCII)执行到位
+- 第三方评审 P0/P1 已修复,测试留痕规范
+
+### 1.2 平台性短板(本次系统性深潜发现)
+
+| # | 短板 | 现状 | 平台性影响 |
+|---|---|---|---|
+| S1 | **指标定义三处漂移** | `src/solver_core.py`(15项) / `scripts/robust_motorcad.py`(18项) / `web/.../metrics_constants.py`(25项) 三份不统一 | 新增指标要改三处,必然漂移 |
+| S2 | **解析器两套实现** | solver_core 带字段归一化(全角括号/空白/大小写),robust_motorcad 是无归一化精确匹配 | robust_motorcad 解析不到 tavg_nm/ripple_pct(实测) |
+| S3 | **求解核心两套重叠** | `MotorCADSolver`(src) 与 `RobustMotorCADSolver`(scripts) 职责重叠,无统一抽象 | 维护成本翻倍,行为易分叉 |
+| S4 | **无工具适配器抽象** | 方案要求的 `SimulationAdapter` ABC 未落地,Motor-CAD 硬编码 | 无法插拔 Maxwell/JMAG |
+| S5 | **拓扑是裸字符串** | topology 字段无配置化注册表 | 无法扩展 DRSS/SDSR 参数体系 |
+| S6 | **方案 Schema 两套** | `src/plan_schema.py` 与 `web/backend/app/schemas/*` 并行 | 契约漂移风险 |
+| S7 | **执行策略硬编码全因子** | feasibility_search 未接入执行器,自适应闭环未通 | P3 优化能力未真正生效 |
+| S8 | **调度逻辑分裂** | Web 端 BatchScheduler(内存)与本地执行器(轮询)两套 | 平台化调度难扩展 |
+
+### 1.3 平台化目标
+
+把系统从"单工具、单拓扑、单策略"的专用工具,升级为:
+
+> **可插拔工具、可配置拓扑、可扩展策略、契约统一、单一事实源**的电机仿真自动化平台。
+
+新增一个工具/拓扑/策略时,只做"注册"和"实现适配器",不改平台核心。
+
+---
+
+## 2. 目标架构
+
+```
+┌──────────────────────────────────────────────────────────────────┐
+│                     共享核心层 src/afmcore/                       │
+│  (单一事实源,纯 Python,无 GUI/无 Web 依赖,三方共同引用)         │
+│                                                                    │
+│  metrics.py      指标定义 + 归一化解析器(唯一权威)                 │
+│  plan_schema.py  方案契约 V2(唯一权威)                            │
+│  topology.py     拓扑注册表(SSSR/DRSS/SDSR 参数体系+模板+规则)      │
+│  adapters/       仿真工具适配器(接口+注册表+实现)                  │
+│  strategies/     执行策略(full_factorial / adaptive)              │
+│  validation.py   结果校验规则集(可插拔判据)                        │
+└───────────────┬────────────────────┬────────────────┬──────────────┘
+                │                    │                │
+        ┌───────▼───────┐   ┌───────▼───────┐   ┌─────▼──────────┐
+        │  系统一 Web 端  │   │  系统二 本地端 │   │  GUI / 其他      │
+        │  backend 引用   │   │  executor 引用 │   │  工具脚本引用     │
+        │  src/afmcore  │   │  src/afmcore │   │  src/afmcore   │
+        └────────────────┘   └───────────────┘   └────────────────┘
+```
+
+### 2.1 分层职责
+
+| 层 | 职责 | 不许做 |
+|---|---|---|
+| 共享核心层 src/afmcore/ | 指标、契约、拓扑、适配器接口、策略接口、校验规则 | 不依赖 Motor-CAD/Web/GUI 具体实现 |
+| 系统一(Web) | 方案生成、AI、分析、知识库、任务调度 | 不直接调 Motor-CAD |
+| 系统二(本地) | 执行任务、驱动工具、落盘、回传 | 不依赖 AI(可离线) |
+| GUI | 加载方案、监控、展示 | 不跑仿真(子线程例外) |
+
+### 2.2 扩展点一览(平台性的落点)
+
+| 扩展点 | 新增方式 | 不改动 |
+|---|---|---|
+| 新仿真工具(Maxwell/JMAG/Flux) | 实现 `SimulationAdapter` + 注册 `tool_name` | 执行器/任务层/解析层 |
+| 新拓扑(DRSS/SDSR) | topology.py 注册参数体系 + 模型模板 + 默认规则 | 引擎核心 |
+| 新执行策略(adaptive) | strategies/ 实现策略接口 | 任务下发层 |
+| 新指标 | metrics.py 加一项(key/label/alias/direction) | 所有消费端自动生效 |
+| 新校验判据 | validation.py 加一个可插拔 rule | 执行器/方案端 |
+| 新物理场 | 适配器实现多物理场提取 + 指标扩展 | 架构 |
+
+---
+
+## 3. 第一批落地:共享核心层(本轮实施)
+
+> 优先解决 S1/S2/S3/S4,这是平台性的地基,也是数据正确性的根因。
+
+### 3.1 src/afmcore/metrics.py(单一事实源)
+
+- 合并三处指标定义,形成唯一 `METRIC_DEFINITIONS`(key/label/aliases/direction/required/unit)
+- 归一化解析器:全角括号→半角、去空白、小写,三段匹配(section 优先级→全 section 精确→前缀模糊)
+- 提供 `parse_export()` / `extract_all_metrics()` / `pick_metric()` 统一入口
+- 兼容导出:保留 `solver_core` / `robust_motorcad` / `metrics_constants` 的旧符号名(薄兼容层),避免一次性大改调用点
+
+### 3.2 src/afmcore/adapters/(工具适配器)
+
+```
+base.py      SimulationAdapter(ABC): connect/load_model/set_parameter/run_simulation/extract/disconnect
+registry.py  ADAPTER_REGISTRY: {tool_name: adapter_class},get_adapter(tool)
+motorcad.py  MotorCADAdapter: 包装 RobustMotorCADSolver,实现统一接口
+```
+
+- 执行器通过 `get_adapter("motorcad")` 获取,不再硬编码
+- 未来 `get_adapter("maxwell")` 只需注册新类
+
+### 3.3 消费端接入
+
+| 消费端 | 接入方式 |
+|---|---|
+| `src/solver_core.py` | 改为 `from .afmcore.metrics import *`,保留旧名导出 |
+| `scripts/robust_motorcad.py` | 删除自带 METRIC_DEFINITIONS/_parse_export,改用共享解析器(修复 tavg_nm/ripple) |
+| `web/backend/app/metrics_constants.py` | 从共享层派生(或保留 key 常量,标注权威源在共享层) |
+
+---
+
+## 4. 后续批次(待本批验证后启动)
+
+| 批次 | 内容 | 解决 |
+|---|---|---|
+| P2 | 拓扑注册表落地:topology.py 注册 SSSR 参数体系,DRSS/SDSR 预留 | S5 |
+| P2 | 方案 Schema 统一:以 src/afmcore/plan_schema.py 为权威,web schemas 转薄兼容层 | S6 |
+| P3 | 执行策略接入:strategies/adaptive 包装 feasibility_search,task_executor 支持 adaptive 模式 | S7 | ✅ 已落地(2026-08-29,M1-M6) |
+| P3 | 调度统一:本地执行器支持多实例并行 + 与 BatchScheduler 对齐契约 | S8 | ✅ 已落地(2026-08-29,M4/M3) |
+| P4 | 文档同步:原《设计方案介绍》升级 V2,消除与实现漂移 | — |
+| P4 | 本地 EXE 打包:PyInstaller 打包执行器 + GUI | 系统二交付物 |
+
+---
+
+## 5. 风险与约束
+
+| 风险 | 应对 |
+|---|---|
+| 重构破坏现有可用闭环 | 本批只做"共享层 + 接入",不改行为语义;接入后跑指标解析单元验证 + 回归导入测试 |
+| 三处接入遗漏 | 用 Grep 确认所有 METRIC_DEFINITIONS/_parse_export 引用点,逐一替换 |
+| web 端部署路径引用 src | Dockerfile/requirements 增加 src 路径映射(本批先保证本地运行,部署调整列入 P4) |
+| 纯 ASCII 纪律 | 新代码全 ASCII,中文用 \\uXXXX |
+
+---
+
+*本文档为设计决策稿,代码实现按 3.x 批次逐步落地并回填进度。*
+
+## 6. 实施进度回填
+
+| 批次 | 内容 | 状态 | 验证 |
+|---|---|---|---|
+| 第一批(2026-08-29) | src/afmcore/metrics.py(25项指标+归一化解析器);adapters/(SimulationAdapter+注册表+MotorCADAdapter);solver_core/robust_motorcad/metrics_constants 三端接入;修复 tavg_nm/ripple_pct 解析缺口 | ✅ 完成 | TEST-004:单元验证全 PASS、78文件编译0失败、三端+7本地+4Web 导入回归、纯ASCII |
+| 第二批(2026-08-29) | 拓扑注册表 topology.py(SSSR 参数体系 8组37项注册,DRSS/SDSR 预留)+ plan_schema 拓扑校验 + task_executor 切换 get_adapter + _compute_metrics 缺口修复 | ✅ 完成 | TEST-005:36 项断言全 PASS、79文件编译 0 失败、三端导入回归、纯ASCII、回归测试脚本 test_platform_registry.py 固化 |
+| 第三批 | 执行策略 strategies(full_factorial/lhs/adaptive)+ task 模型扩展 + AdaptiveOrchestrator + 执行器批次/多实例 + 调度契约统一 + Web 端执行桥(submit-batch) | ✅ 完成 | TEST-006~011,回归脚本 test_p3_*.py 全 PASS |
+| 第四批 | 方案 Schema 统一(plan_schema 单一权威)+ 文档同步(原方案 V1.1 → V2)+ EXE 打包 | 🔲 规划中 | — |

+ 2183 - 0
docs/TEST_RECORDS.md

@@ -0,0 +1,2183 @@
+# 测试记录与结果总结
+
+> 本文档记录 PCB 轴向磁通电机自动化仿真系统的所有测试记录,包括 Motor-CAD 仿真测试、API 测试、集成测试等。
+> 每次测试必须记录在此文档中(工作留痕)。
+> 最后更新:2026-09-01
+
+---
+
+## 测试记录索引
+
+| 编号 | 日期 | 测试类型 | 结果 | 关键发现 |
+|---|---|---|---|---|
+| TEST-001 | 2026-08-28 | Motor-CAD 连接与变量探测 | ⚠️ 部分成功 | 发现 export_results API 兼容性问题、弹窗问题 |
+| TEST-002 | 2026-08-28 | Motor-CAD 全流程验证(修复后) | ✅ 全部成功 | 验证连接/计算/导出/解析全流程,弹窗问题解决 |
+| TEST-003 | 2026-08-28 | 扩展指标解析验证 | ✅ 全部成功 | 指标解析从7个提升到14个,平均转矩/转矩脉动仍需调查 |
+| TEST-004 | 2026-08-29 | 平台化改造单元验证(指标单一事实源+归一化解析+适配器框架) | ✅ 全部成功 | 修复 tavg_nm/ripple_pct 解析缺口;统一指标单一事实源 |
+| TEST-005 | 2026-08-29 | 平台化第二批(拓扑注册表+执行器适配器切换) | ✅ 全部成功 | 36 项断言全 PASS;79 个 .py 编译 0 失败 |
+| TEST-006 | 2026-08-29 | P3-M1/M2 策略抽象层 + 自适应编排器闭环 | ✅ 全部成功 | fake 执行器 + temp DB 隔离回归,exit 0 |
+| TEST-007 | 2026-08-29 | P3-M3 执行器批次化 + 多实例 | ✅ 全部成功 | point_id 回传 + 原子认领;P2 回归 36/36 不破坏 |
+| TEST-008 | 2026-08-29 | P3-M4 调度契约统一 | ✅ 全部成功 | 状态词汇归一(queued→pending 等),向后兼容 |
+| TEST-009 | 2026-08-29 | P3-M5 HTTP 全链路闭环 | ✅ 全部成功 | 闭环 8 点 budget_exhausted;91 个 .py 编译 0 失败 |
+| TEST-010 | 2026-08-29 | P3-M5 真实 Motor-CAD 烟雾 | ✅ 全部成功 | 连接/求解/解析 21 指标;back_emf=11.15V 与历史一致 |
+| TEST-011 | 2026-08-29 | P3-M6 Web 端 AdaptiveLoop 执行桥 | ✅ 全部成功 | fake plan→3 批 8 点;幂等;HTTP 端点 404/200 正常 |
+| TEST-012 | 2026-08-29 | P3 收尾(边界测试+全量回归+规范修复) | ✅ 全部成功 | 工程规范落地,全量回归绿 |
+| TEST-013 | 2026-08-29 | P3 遗留(原子认领+断点恢复+ASCII 纪律) | ✅ 全部成功 | 并发 8 线程恰 1 win;断点可恢复 |
+| TEST-014 | 2026-08-29 | P4-M1~M3(Schema 单一权威+文档 V2+EXE 打包) | ✅ 全部成功 | dist/PCB-AFM-Executor.exe 12.6MB 打包成功 |
+| TEST-015 | 2026-08-29 | P4-M4~M5(收敛曲线+L0 上提共享核心) | ✅ 全部成功 | points_history + L0 唯一实现(纯 stdlib) |
+| TEST-016 | 2026-08-30 | P5-M1 前端全量 build 类型错误清零 | ✅ 全部成功 | vue-tsc 0 错误 + vite build 成功(退出码 0) |
+| TEST-017 | 2026-08-30 | P5-M2 EXE 配置化 + mock 分支修复 + E2E mock | ✅ 全部成功 | executor_config.json 配置化;真实 COM 端到端待目标机 |
+| TEST-018 | 2026-08-30 | P5-M2 补充:真实 EXE 端到端单点验证 | ✅ 全部成功 | airgap_mm→Airgap 映射 + point_id 排除;数值与 TEST-010 一致 |
+| TEST-019 | 2026-08-30 | P5-M3 adaptive 可视化补全 | ✅ 全部成功 | 后端 4 新字段 + 前端 3 视图 + 8 测试 + build 绿 |
+| TEST-020 | 2026-08-30 | P5-M4 策略层高级管线 | ✅ 全部成功 | Morris + IDW 代理 + 预算自适应;Kriging 降级 IDW |
+| TEST-021 | 2026-08-30 | P5-M5 多工具适配器 | ✅ 全部成功 | 2 新适配器注册 + 22 测试;真实接入标注环境依赖 |
+| TEST-022 | 2026-08-30 | P5-M6 多物理场 L2 接入 | ✅ 全部成功 | 指标 25→35(热6+结构4);报告按域分组;20 测试 |
+| TEST-023 | 2026-08-30 | P6-M1 Web 前端 UI/UX 全面重构 | ✅ 全部成功 | 设计令牌+3公共组件+B1信息架构+B2 PlanDetail分层;vue-tsc 0错误 |
+| TEST-024 | 2026-09-01 | 环境体检脚本 check_machine_paths.py 验证 | ✅ 脚本可用(shell 环境报包缺失属预期) | 新增脚本:Python版本/环境变量/Motor-CAD/包/Git/资产全检;未发现项目 venv |
+
+---
+
+## TEST-001:Motor-CAD 连接与变量探测测试
+
+**日期**:2026-08-28 04:49 - 05:08
+**测试环境**:Windows 10,Python 3.x,pymotorcad 0.8.8,Motor-CAD v261
+**测试目的**:验证 RobustMotorCADSolver 的连接、变量探测、参数写入、磁场计算、结果导出功能
+**测试脚本**:`scripts/test_robust_solver.py`
+**输出目录**:`output/test_connect_20260828_044951/`
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | Motor-CAD 连接 | ✅ 成功 | `open_new_instance=True` + `set_visible(True)`,健康检查通过 |
+| 2 | 五层自检 | ✅ 完成 | 连接/许可/模型/脚本层 PASS,权限层 FAIL(非管理员,预期) |
+| 3 | 变量探测 | ⚠️ 11/14 | 3 个变量名不存在(`Stator_Number_Of_Slots`、`Rotor_Number_Of_Poles`、`Peak_Phase_Current`),**触发 GUI 弹窗需人工点确定** |
+| 4 | 参数写入+回读 | ✅ 成功 | TorquePointsPerCycle 30→60,回读 60.0,恢复 30 |
+| 5 | 磁场计算 | ✅ 成功 | 耗时 174.4 秒 |
+| 6 | 结果导出 | ❌ 失败 | `export_results() missing 1 required positional argument: 'file_path'` |
+| 7 | 断开连接 | ✅ 成功 | |
+
+### 探测到的模型参数(MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot)
+
+| 参数 | 值 | 单位 |
+|---|---|---|
+| Motor_Type | 0 | - |
+| Airgap | 1 | mm |
+| Magnet_Arc_[ED] | 121 | EDeg |
+| MagnetCentralArc_HalbachRing | 120 | EDeg |
+| Slot_Opening | 8.5 | mm |
+| Slot_Width | 8.5 | mm |
+| Copper_Width | 3.64 | mm |
+| TorquePointsPerCycle | 30 | points/cycle |
+| AirgapMeshPoints_mesh | 600 | points |
+| AirgapMeshPoints_layers | 600 | points |
+| Shaft_Speed | 5000 | rpm |
+
+### 发现的问题
+
+1. **弹窗问题**:`get_variable` 对不存在的变量名会抛出异常 **并弹出 GUI 错误对话框**,阻塞无人值守批处理
+   - 修复:连接后立即设置 `MessageDisplayState=2` 禁用弹窗
+2. **导出 API 兼容性**:pymotorcad 0.8.8 的 `export_results` 签名是 `(solution_type, file_path)`,之前只传了 `file_path`
+   - 修复:改为 `export_results("EMagnetic", file_path)`
+
+### 修复提交
+
+- `f3b492a` — 修复 export_results API 兼容性
+
+---
+
+## TEST-002:Motor-CAD 全流程验证(修复后)
+
+**日期**:2026-08-28 05:08 - 05:15
+**测试环境**:Windows 10,Python 3.x,pymotorcad 0.8.8,Motor-CAD v261
+**测试目的**:验证 TEST-001 发现的两个问题修复后,全流程是否正常
+**测试脚本**:`scripts/test_robust_solver.py`(已添加弹窗抑制)
+**输出目录**:`output/test_connect_20260828_050834/`
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | Motor-CAD 连接 | ✅ 成功 | `open_new_instance=True` + `set_visible(True)` |
+| 2 | 弹窗抑制 | ✅ 成功 | `MessageDisplayState=2`,变量探测阶段**无弹窗** |
+| 3 | 五层自检 | ✅ 完成 | 连接/许可/模型/脚本层 PASS,权限层 FAIL(非管理员,预期) |
+| 4 | 变量探测 | ✅ 11/14 | 3 个变量名不存在但**无弹窗**(已被抑制) |
+| 5 | 参数写入+回读 | ✅ 成功 | TorquePointsPerCycle 30→60,回读 60.0,恢复 30 |
+| 6 | 磁场计算 | ✅ 成功 | 耗时 **138.1 秒**(比 TEST-001 的 174.4 秒快) |
+| 7 | 结果导出 | ✅ 成功 | 10788 字节,`export_results("EMagnetic", path)` |
+| 8 | 结果解析 | ✅ 成功 | 解析到 7 个指标 |
+| 9 | 断开连接 | ✅ 成功 | 弹窗状态恢复,基线重载 |
+
+### 实际仿真结果
+
+| 指标 | 值 | 单位 |
+|---|---|---|
+| 系统效率 efficiency_pct | **86.06** | % |
+| 总损耗 total_losses_w | 41.945 | W |
+| 输入功率 input_power_w | 300.9 | W |
+| 磁钢损耗 magnet_loss_w | 0.5641 | W |
+| 定子铁耗 iron_loss_w | 3.002 | W |
+| 线间反电动势有效值 back_emf_v | 7.898 | V |
+| 轴转速 shaft_speed_rpm | 5000 | rpm |
+
+### 注意事项
+
+- 本次未解析到 `tavg_nm`(平均转矩)和 `ripple_pct`(转矩脉动),原因是字段名匹配不完整
+- 已在 `6a8bc80` 提交中扩展 `METRIC_DEFINITIONS` 从 12 个指标到 20 个,添加了中文字段名变体
+- 下次测试应能解析到更多指标
+
+### 修复提交
+
+- `6a8bc80` — 扩展指标别名 + 测试脚本弹窗抑制
+
+### 测试结论
+
+**RobustMotorCADSolver 已验证可正常连接 Motor-CAD、执行磁场计算、导出并解析结果,弹窗问题已解决,可支持无人值守批量仿真。**
+
+---
+
+## TEST-003:扩展指标解析验证
+
+**日期**:2026-08-28 06:01 - 06:05
+**测试环境**:Windows 10,Python 3.x,pymotorcad 0.8.8,Motor-CAD v261
+**测试目的**:验证扩展后的 METRIC_DEFINITIONS(12→20个指标)能否解析到更多指标,特别是平均转矩和转矩脉动
+**测试脚本**:`scripts/test_robust_solver.py`
+**输出目录**:`output/test_connect_20260828_060101/`
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | Motor-CAD 连接 | ✅ 成功 | `open_new_instance=True` + `set_visible(True)` |
+| 2 | 弹窗抑制 | ✅ 成功 | `MessageDisplayState=2`,无弹窗 |
+| 3 | 五层自检 | ✅ 完成 | 4/5 PASS(权限层 FAIL,非管理员,预期) |
+| 4 | 变量探测 | ✅ 11/14 | 3 个变量名不存在,无弹窗 |
+| 5 | 参数写入+回读 | ✅ 成功 | TorquePointsPerCycle 30→60→30 |
+| 6 | 磁场计算 | ✅ 成功 | 耗时 **133.5 秒** |
+| 7 | 结果导出 | ✅ 成功 | 10788 字节 |
+| 8 | 结果解析 | ✅ 成功 | 解析到 **14 个指标**(比 TEST-002 的 7 个增加一倍) |
+| 9 | 断开连接 | ✅ 成功 | |
+
+### 实际仿真结果(14个指标)
+
+| 指标 | 值 | 单位 | 备注 |
+|---|---|---|---|
+| 系统效率 efficiency_pct | **86.06** | % | 与 TEST-002 一致 |
+| 总损耗 total_losses_w | 41.945 | W | 与 TEST-002 一致 |
+| 输入功率 input_power_w | 300.9 | W | 与 TEST-002 一致 |
+| 输出功率 output_power_w | 258.95 | W | 🆕 新增 |
+| 电磁功率 em_power_w | 273.29 | W | 🆕 新增 |
+| 磁钢损耗 magnet_loss_w | 0.5641 | W | |
+| 定子铁耗 iron_loss_w | 3.002 | W | |
+| 线间反电动势有效值 back_emf_v | 11.15 | V | 与 TEST-002 不同(7.898→11.15,可能是字段匹配到了不同的字段) |
+| 轴转速 shaft_speed_rpm | 5000 | rpm | |
+| 轴转矩 shaft_torque_nm | 0.49456 | Nm | 🆕 新增 |
+| 堵转转矩 stall_torque_nm | 9.364 | Nm | 🆕 新增 |
+| 转矩常数 torque_constant | 0.01757 | Nm/A | 🆕 新增 |
+| 相电流峰值 phase_current_peak_a | 29.7 | A | 🆕 新增 |
+| 线电流有效值 line_current_rms_a | 21.0 | A | 🆕 新增 |
+
+### 仍未解析到的指标
+
+| 指标 | 预期别名 | 可能原因 |
+|---|---|---|
+| 平均转矩 tavg_nm | `平均转矩 (virtual work)` / `Average torque (virtual work)` | 字段名中可能有不可见字符(全角空格等),需进一步调查 |
+| 转矩脉动 ripple_nm | `Torque Ripple (VW)` | 可能被其他字段先匹配,或字段名有差异 |
+| 转矩脉动百分比 ripple_pct | `Torque Ripple (VW) [%]` | 同上 |
+
+### 测试结论
+
+1. **扩展指标别名有效**:METRIC_DEFINITIONS 从 12 个扩展到 20 个后,解析到的指标从 7 个增加到 14 个,验证了中文字段名别名的有效性
+2. **全流程稳定**:连续三次测试(TEST-001/002/003)均成功连接、计算、导出,系统稳定性良好
+3. **待优化**:平均转矩和转矩脉动的字段名匹配仍需调查,可能需要打印导出文件中这些字段的确切字节内容来确认不可见字符
+
+### 后续行动
+
+- [ ] 调查平均转矩/转矩脉动字段名匹配问题(打印字段字节内容)
+- [ ] 运行多参数扫描测试(TEST-004)
+
+---
+
+## 待办测试项
+
+- [ ] TEST-003:多参数扫描测试(3 个磁钢弧角值),验证 `run_single_point()` 完整流程和逐点落盘
+- [ ] TEST-004:扩展指标解析验证(确认 tavg_nm、ripple_pct 等能正确解析)
+- [ ] TEST-005:批量调度器测试(BatchScheduler 多任务排队)
+- [x] TEST-006:Web 端 API 集成测试(任务创建/下发/进度/结果回传)→ 见 TR-2026-08-29-01
+- [ ] TEST-007:断点续跑测试(中断后恢复)
+- [ ] TEST-008:长时间稳定性测试(50+ 仿真点)
+
+---
+
+## TR-2026-08-29-01 本地执行器全链路联调(Motor-CAD 真实仿真)
+
+- 测试日期:2026-08-29
+- 测试环境:本地 Win10 + Motor-CAD 2023R2 (v261) + pymotorcad 0.8.8
+- 测试目的:验证 Web 一键启动仿真 -> 本地执行器 -> Motor-CAD 真实磁计算 -> 结果回传 全链路
+- 测试方案:plan 21(SSSR_AxialFlux_300W_12V_5000rpm),扫描变量 Airgap 1->2mm,2点
+
+### 发现的问题与修复
+1. **执行器未运行**:本地执行器 `scripts/task_executor.py` 未启动,任务卡在 dispatched 0进度。
+   修复:新增 `scripts/run_task_executor.py` 启动入口,后台运行。
+2. **认领逻辑矛盾**:start-simulation 提前置 dispatched,执行器只拉 pending。
+   修复:`fetch_pending_tasks` 同时认领 pending + dispatched。
+3. **任务参数为空**:列表接口不返回 parameters,执行器拿不到参数集直接 0 点完成。
+   修复:新增 `_hydrate_task`,从 `/api/tasks/{id}/download` 拉取完整 task.json。
+4. **字符串参数强转 float 崩溃**:Magnet_Material/Cooling_Type 等 set_variable float() 报错。
+   修复:`robust_motorcad.run_single_point` 跳过非数值参数。
+5. **变量名不匹配(Could not find Outer_Rotor_Diameter)**:模板变量名非真实 Motor-CAD 变量。
+   修复:模板加 `motorcad_var` 字段(从 .mot 提取真实变量名),`_expand_plan_to_parameters`
+   按模板合并生成参数集,仅写 motorcad_var 非空项。Slot_Depth 等默认值对齐基线模型。
+6. **AFM_D_Rotor 改外径破坏线性几何(aLinearRadius=0)**:D76 基线模型改外径后磁计算失败。
+   修复:Outer_Rotor_Diameter 的 motorcad_var 置 None(外径作设计约束不写入,用基线几何)。
+7. **执行器心跳缺失**:/api/executor/status 显示无执行器。
+   修复:执行器 poll 循环加 `_send_heartbeat` 注册,前端可显示在线/进度。
+
+### 测试结果(最终)
+- 任务 a7123abf:completed,2/2 点 OK,耗时 307s
+- 点0(Airgap=1.0mm):back_emf 11.15V,stall_torque 9.364Nm,em_power 273.3W,input 300.9W,loss 41.9W
+- 点1(Airgap=2.0mm):back_emf 8.71V,stall_torque 7.52Nm,em_power 219.5W,input 240.1W,loss 33.6W
+- 物理规律验证:气隙增大 -> 反电动势/转矩下降(正确)
+- 输出目录:web/backend/output/tasks/20260829_134803_SSSR_AxialFlux_300W_12V_5000rpm_Optimization_run/
+
+### 遗留问题
+- 字符串参数(材料/冷却方式/绝缘等级)暂不写入 Motor-CAD(set_variable 需数值),用基线默认
+- 外径等几何约束需在 Motor-CAD 内调整几何后才可改(当前用基线几何)
+
+---
+
+## TEST-004:平台化改造单元验证(指标单一事实源 + 归一化解析 + 适配器框架)
+
+**日期**:2026-08-29
+**环境**:Windows 10 / 11,Python 3.x,本地工作区
+**目的**:验证平台化改造第一批的正确性与回归安全性
+**依据**:docs/PLATFORM_DESIGN_V2.md(平台化升级设计方案)
+
+### 改造内容
+1. 新建共享核心层 src/afmcore/metrics.py:指标定义单一事实源(25 项) + 归一化解析器(全角括号→半角、去空白、小写)
+2. 三个消费端接入:solver_core / 
+obust_motorcad / metrics_constants 全部改为从共享层导入(消除三处漂移)
+3. 新建适配器抽象:src/afmcore/adapters/ 下 SimulationAdapter 接口 + 注册表 + MotorCADAdapter 实现
+
+### 验证结果(全部 PASS)
+| 项 | 结果 | 详情 |
+|---|---|---|
+| 归一化解析(含全角字符) | ✅ | 模拟 Motor-CAD 导出含全角空格/全角括号/全角[],tavg_nm、ripple_pct、efficiency_pct、total_losses_w、back_emf_v 全部解析成功 |
+| 中文别名匹配 | ✅ | 中文字段(平均转矩/转矩脉动/系统效率)能正确匹配 |
+| % 守卫 | ✅ | [%] 字段不再污染 Nm 值指标(ripple_abs_nm/ripple_nm 不被 ripple_pct 字段污染) |
+| 三端导入回归 | ✅ | solver_core(25)/robust_motorcad(25)/metrics_constants(25) 三端持久化同源,导入成功 |
+| 本地端关键模块 | ✅ | solver_core/scan_engine/experience_db/robust_motorcad/task_executor/run_single/run_scan 7/7 导入 OK |
+| Web 端引用模块 | ✅ | metrics_constants/plans/result_analyst/task_manager 4/4 导入 OK |
+| 全量编译 | ✅ | 78 个 .py 全部 py_compile 通过,0 失败 |
+| 纯 ASCII 约束 | ✅ | 新写代码全部 ASCII,无非 ASCII 行 |
+| 适配器注册表 | ✅ | import 即注册 motorcad;未注册工具报清晰 KeyError |
+| 适配器协议 | ✅ | run_point/extract_metrics 输出 schema 与设计一致 |
+
+### 关键发现(修复 TEST-003 遗留问题)
+1. **tavg_nm/ripple_pct 解析不到的根因:**
+obust_motorcad._parse_export 使用无归一化的精确字符串匹配,Motor-CAD 导出字段名含全角空格/括号时匹配失败。已改为共享层归一化匹配。
+2. **指标定义三处漂移:**solver_core(15)/robust_motorcad(18)/metrics_constants(25) 各持一份,已统一为 afmcore.metrics(25)。
+3. **名命冲突:**platform 与标准库同名,已将共享包更名为 afmcore。
+
+### 遗留事项
+- MotorCADAdapter 已通过协议单元验证(FakeSolver);真实 Motor-CAD 连接回归待下一批(P2)
+- task_executor 切换到 get_adapter 模式待 P2 落地(当前直接使用 RobustMotorCADSolver,已受益于共享解析器)
+- Web 部署环境需确保 src/afmcore 可导入(Dockerfile 调整待 P4)
+
+---
+
+## TEST-005:平台化改造第二批(拓扑注册表 + 执行器适配器切换)
+
+**日期**:2026-08-29
+**环境**:Windows 10 / 11,Python 3.x,本地工作区(无真实 Motor-CAD 启动)
+**目的**:验证 P2 拓扑注册表与执行器通过适配器注册表驱动,消除“拓扑裸字符串”与“求解器硬编码”
+
+### 改造内容
+1. 新建 `src/afmcore/topology.py`:拓扑注册表(SSSR 完整参数体系 8 组 37 参数 + DRSS/SDSR 预留),提供 get_topology / is_supported / is_active / validate_params / to_dict
+2. `src/plan_schema.py`:validate() 集成拓扑注册校验(未知拓扑报错)
+3. `scripts/task_executor.py`:MotorCADTaskExecutor 从硬编码 RobustMotorCADSolver 切换为 `afmcore.adapters.get_adapter(tool)`;结果 metrics 扁平化到顶层,修复 _compute_metrics 取不到嵌套 metrics 的缺口
+
+### 验证结果(全部 PASS,36 项)
+| 项 | 结果 | 详情 |
+|---|---|---|
+| 拓扑注册表 | ✅ 22 项 | 注册/查询(大小写不敏感)/参数体系(SSSR 37参数8组)/序列化/自定义注册幂等覆盖 |
+| 拓扑校验集成 | ✅ 4 项 | plan_schema.validate:SSSR 通过、TORUS 拒绝、DRSS 接受(已注册)、序列化往返 |
+| 适配器注册表 | ✅ 4 项 | motorcad 注册、get_adapter 返回协议实例、未注册工具 KeyError |
+| 执行器适配器路径 | ✅ 6 项 | OK 点扁平化、_compute_metrics 取到 tavg_nm_mean、FAILED 点抛异常、execute_task 3 点全链路、cleanup 断开 |
+| 全量编译 | ✅ | 79 个 .py 全部 py_compile 通过,0 失败 |
+| 纯 ASCII | ✅ | 新增/修改代码全部 ASCII(topology.py / plan_schema.py / task_executor.py / test_platform_registry.py) |
+| 导入回归 | ✅ | afmcore(拓扑/指标/适配器) + src(plan_schema/solver_core) + scripts(robust_motorcad/task_executor) + web(metrics_constants) 全部 OK |
+
+### 关键发现
+1. **拓扑裸字符串 → 注册表**:plan.validate 现在拦截未知拓扑(如 TORUS);DRSS/SDSR 已注册(DRSS planned);扩展新拓扑只做 register_topology。
+2. **_compute_metrics 现存缺口修复**:RobustMotorCADSolver 返回 {status, metrics:{...}} 嵌套结构,而 _compute_metrics 从顶层取 tavg_nm,导致聚合永远为空。已通过适配器层扁平化 metrics 到顶层修复。
+3. **测试资产固化**:新增 scripts/test_platform_registry.py 可重复运行的回归测试(36 项断言),后续批次可在此基础上扩展。
+
+### 遗留事项
+- 真实 Motor-CAD 连接回归(含 DRSS 建模后):需要实际 .mot + 授权环境
+- DRSS 参数体系待建模后补全(当前仅预留几何/转子核心参数)
+- fixed_params_template.py 仍为单一模板(SSSR),按拓扑选模板列入 P4 方案 Schema 统一
+- 执行策略接入(P3 adaptive)+ 调度统一(P3 多实例并行)
+
+---
+
+## TEST-006:P3 平台化改造(M1 策略抽象层 + M2 自适应编排器闭环)
+
+**日期**:2026-08-29
+**环境**:Windows,Python 3.x,本地工作区(无真实 Motor-CAD 启动;闭环用 fake 执行器)
+**目的**:验证 P3 第一批:执行策略抽象层(M1)与自适应编排器全闭环(M2),为「web 方案 -> 批次任务 -> 本地执行 -> 回填搜索 -> 续批/收敛」打通。
+
+### M1:执行策略抽象层(src/afmcore/strategies/)
+1. 新建 SimulationStrategy ABC + STRATEGY_REGISTRY(get_strategy / list_strategy_kinds / is_registered / register_strategy),执行器改为向策略要批次而非硬编码全因子
+2. full_factorial.py:变量值列表笛卡尔积,自包含实现(不跨包 import scan_engine)
+3. lhs.py:纯 Python 拉丁超立方采样(无 numpy),空间填充初始覆盖
+4. adaptive.py:AdaptiveBridgeStrategy,backend 注入协议(generate_initial_batch / select_next_batch / report_result),共享核心层不依赖 web
+5. src/plan_schema.py:SearchStrategy.method 归一化(active_learning/constrained -> adaptive)+ validate() 按策略注册表校验(未知策略如 random_forest 拒绝)
+
+### M2:任务模型扩展 + 自适应编排器
+1. models/task.py 新增 task_type / loop_id / batch_id / point_ids / dynamic 字段;database.py 迁移逻辑为旧 tasks 表 ADD COLUMN(已验证旧库自动补列)
+2. 	ask_manager.py create_task 支持新字段(task.json 负载 + ORM + dict 输出透传)
+3. 新建 strategy_orchestrator.py:AdaptiveOrchestrator(start_loop / advance_loop / get_loop_status / list_loops),桥 FeasibilityFirstSearch <-> 任务系统 <-> 本地执行器;loop 状态落盘 output/adaptive_loops/
+4. **修复现存 bug**:task_manager.report_results 引用未定义 plan_id(应为 task.plan_id),导致带 plan 的任务完成时报 NameError
+
+### 验证结果(全部 PASS)
+| 项目 | 结果 | 详情 |
+|---|---|---|
+| 策略注册表 | 通过 | 冒烟:adaptive/full_factorial/lhs 注册、FF 分批收敛、LHS 采样范围、adaptive 桥回传、无 backend/未知策略报错 |
+| plan_schema 策略校验 | 通过 | 别名 active_learning/constrained -> adaptive;random_forest 拒绝;序列化往返归一化 |
+| 任务模型扩展 | 通过 | 新字段 create_task 透传;_task_to_dict 输出 point_ids/dynamic/task_type |
+| DB 迁移 | 通过 | 旧 tasks 表 init_db 自动 ADD COLUMN 5 字段 |
+| orchestrator 闭环 | 通过 | fake 执行器驱动:首批 3 点 -> 回填 -> 续批 -> budget_exhausted 收敛,n_results=8,状态持久化,重复 loop 拒绝 |
+| 编译/ASCII | 通过 | 新改 4 文件 py_compile 通过、纯 ASCII |
+| 回归脚本 | 通过 | scripts/test_p3_orchestrator.py 入库,exit 0 |
+
+### 关键发现
+1. **L0 参数名对齐**:FeasibilityFirstSearch 的可行性预筛依赖参数名与 L0 引擎期望一致(airgap_mm / current_a 等,web 端既有路径同样直接透传);扫描参数名不匹配会全判 infeasible 导致空批次假收敛。调用方须传 L0 对齐名(真实 AI plan 生成即如此)。
+2. **闭环时序**:orchestrator 采用 pull 驱动(advance_loop 由调用方/路由/调度器触发),与既有执行器轮询哲学一致,task 系统零侵入。
+3. **状态可恢复**:loop 元数据 + search export 落盘 JSON,进程重启可恢复元数据。
+
+### 遗留事项
+- search 路由 / 定时器接入 orchestrator(M5,避免动用户未提交的 main.py)
+- 真实 Motor-CAD 烟雾测试(M5,需授权环境)
+- adaptive 前端视图(P4)
+
+---
+
+## TEST-007:P3 平台化改造(M3 执行器批次化 + 多实例)
+
+**日期**:2026-08-29
+**环境**:Windows,Python 3.x,本地工作区(无真实 Motor-CAD;mock 执行器)
+**目的**:验证执行器支持 adaptive_batch 任务的 point_id 回传、多实例并行的认领原子化、以及独立 executor_id。
+
+### 改动内容(scripts/task_executor.py)
+1. **point_id 回传**:execute_task 逐点跑完后,若参数含 point_id 则透传到结果顶层(成功与 FAILED 分支均保留),供 orchestrator 按 point_id 回填搜索
+2. **认领原子化**:dispatch_task 返回 False(任务已被其他实例认领/网络失败)时跳过该任务,多实例并行不会重复仿真同一任务
+3. **executor_id 唯一化**:默认 pid+随机后缀,支持显式传入(多实例各自唯一)
+4. **修复现存 bug**:report_results 本地文件模式调用 on_complete 传 2 参数,与 execute_task 末尾的 3 参数签名不一致,导致本地模式完成时 TypeError;统一为 3 参数
+
+### 新增
+- scripts/run_task_executor_parallel.py:--instances N 并行启动 N 个 TaskExecutor(各自唯一 executor_id,认领原子化防重复),--mock 供测试
+
+### 验证结果(全部 PASS)
+| 项目 | 结果 | 详情 |
+|---|---|---|
+| point_id 透传 | 通过 | OK 与 FAILED 点均在结果顶层带 point_id |
+| 认领原子化 | 通过 | dispatch 返回 False 时任务被跳过(不执行) |
+| executor_id 唯一 | 通过 | 默认实例各不相同;显式传入生效 |
+| P2 回归 | 通过 | test_platform_registry.py 36/36 不回归 |
+| M3 回归 | 通过 | scripts/test_executor_m3.py 入库,exit 0 |
+| 编译/ASCII | 通过 | 3 文件 py_compile 0 失败、纯 ASCII |
+
+### 关键发现
+1. 多实例并行依赖 Web 端 dispatch 的幂等语义(pending->dispatched 原子迁移),执行器侧只需在 claim 失败时跳过即可防重复。
+2. 本地文件模式与 Web 模式在 on_complete 回调签名上曾不一致(2 vs 3 参数),已统一。
+
+### 遗留事项
+- 真实 Motor-CAD 多实例烟雾测试(M5,需授权环境;注意 license 并发限制)
+- adaptive 前端视图(P4)
+
+---
+
+## TEST-008:P3 平台化改造(M4 调度契约统一)
+
+**日期**:2026-08-29
+**环境**:Windows,Python 3.x,本地工作区(无 web 服务/Motor-CAD)
+**目的**:统一两套调度状态词汇(TaskManager 的 pending/dispatched 与 BatchScheduler 的 queued),并让 BatchScheduler 任务字段与 Task ORM 对齐(M2 新增的 adaptive-batch 字段)。
+
+### 改动内容
+1. 新建 web/backend/app/services/task_contract.py(无依赖,供各服务引用):
+   - 规范状态常量 + STATUS_ALIASES 归一化映射(queued->pending、canceled->cancelled、completed_with_errors->completed 等)
+   - normalize_status / is_terminal / merge_adaptive_fields(task_type/loop_id/batch_id/point_ids/dynamic 默认值注入)
+2. batch_scheduler.py:add_task 增加 task_type/loop_id/batch_id/point_ids/dynamic 参数透传;_summary 输出新字段;状态词保持 queued(向后兼容)但经契约归一
+
+### 验证结果(全部 PASS)
+| 项目 | 结果 | 详情 |
+|---|---|---|
+| 状态归一 | 通过 | queued->pending、canceled->cancelled、completed_with_errors->completed、未知词透传、终态判定 |
+| 字段合并 | 通过 | merge_adaptive_fields 默认值(scan/False/[]/None)与显式值 |
+| scheduler 字段对齐 | 通过 | add_task 新字段透传、_summary 输出、legacy 调用不破坏、queued->running 流转、状态落盘 |
+| 回归 | 通过 | test_p3_orchestrator.py(M2 闭环)不回归;test_p3_m4_contract.py 入库 exit 0 |
+| 编译/ASCII | 通过 | 3 文件 py_compile 0 失败、纯 ASCII |
+
+### 关键发现
+1. G5「调度契约两套」本质:BatchScheduler 只服务调度监控 UI(monitor.py),从未与 TaskManager/执行器流转对接;本次用契约层声明统一词汇与字段,消除漂移,而不重构两个既有服务。
+2. status 归一采用「别名映射 + 未知词透传」,与 plan_schema 策略校验的哲学一致(能报错而非静默)。
+
+### 遗留事项
+- monitor 前端展示 scheduler 的 queued 仍沿用旧词(后续可经 normalize_status 归一展示)
+- 真实 Motor-CAD 烟雾(M5)
+
+---
+
+## TEST-009:P3 平台化改造(M5 HTTP 全链路闭环 + 真实烟雾待办)
+
+**日期**:2026-08-29
+**环境**:Windows,Python 3.x;临时 SQLite DB + uvicorn 起的真实 FastAPI 后端;本地 mock 执行器(无真实 Motor-CAD)
+**目的**:验证 P3 全链路真实 HTTP 闭环:orchestrator -> 任务 -> 本地执行器(HTTP 轮询) -> 仿真 -> 回传(point_id) -> 回填搜索 -> 续批 -> 收敛。
+
+### 测试方案(scripts/test_p3_closed_loop.py,exit 0)
+1. 临时 DB 上启动真实 FastAPI web(uvicorn,端口 8137),等 /api/monitor/health
+2. 测试进程内 AdaptiveOrchestrator.start_loop(airgap_mm/current_a,预算 8 批 4)
+3. 真实 TaskExecutor(enable_mock)后台轮询 web,认领 adaptive_batch 任务
+4. 执行器逐点 mock 仿真 -> report_results(HTTP,含 point_id)
+5. 测试驱动 advance_loop:批次完成 -> 回填 -> 续批 -> 预算耗尽
+6. 验证 + 清理(停执行器/停 web)
+
+### 验证结果(全部 PASS)
+| 项目 | 结果 | 详情 |
+|---|---|---|
+| web 启动 | 通过 | temp DB + uvicorn 健康检查 OK |
+| orchestrator 建任务 | 通过 | task_type=adaptive_batch,首批准 3 点 |
+| 执行器 HTTP 认领 | 通过 | 轮询 pending/dispatched -> dispatch -> 执行 |
+| point_id 回传 | 通过 | report_results 结果带 point_id,搜索按 point_id 回填 |
+| 续批/收敛 | 通过 | 3+4+1 点 -> budget_exhausted,n_results=8 |
+| 全量回归 | 通过 | P2 36 项 + M2 + M3 + M4 全部 PASS;91 个 .py 编译 0 失败 |
+| 退出码 | 通过 | exit 0(无 AI 噪音:orchestrator 无 KIMI_API_KEY 时跳过 AI 分析) |
+
+### 关键发现
+1. **orchestrator AI 分析门控**:无 KIMI_API_KEY 时每次 advance 都调 AI 会打 stderr warning 且浪费;改为配置了 key 才调用,保持 quantitative 结果。真实配置 key 后自动启用 AI 分析。
+2. **SQLite 多进程共享**:测试进程(orchestrator) + web 进程(TaskManager) + 执行器(HTTP) 三方共享同一 temp DB 文件,闭环正常。
+
+### 遗留事项
+- **真实 Motor-CAD 烟雾**(环境已确认:MOTORCAD_ACTIVEX + license + exe + 基线模型均在):计划在提交本批后用最小点数验证 MotorCADAdapter 真实求解通路,结果续记 TEST-010
+
+---
+
+## TEST-010:P3 平台化改造(M5 真实 Motor-CAD 烟雾)
+
+**日期**:2026-08-29
+**环境**:Windows + Motor-CAD v261(MOTORCAD_ACTIVEX 已设、license 1055@localhost、exe 存在);基线模型 MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot
+**目的**:验证 MotorCADAdapter 真实求解通路:连接 -> 基线加载 -> 求解 1 点 -> 导出/解析指标 -> 断开。
+
+### 发现并修复的 bug
+**MotorCADAdapter.run_point 忽略 model_path 参数**:run_point(model_path=...) 内部 _ensure_solver() 用默认空路径创建 RobustMotorCADSolver,且从不把传入 model_path 同步给 solver,导致 load_from_file('') 报 "Motor-CAD File does not exist"。修复:run_point 在 connect 前将 model_path 同步到 solver.model_path。
+
+### 验证结果(PASS)
+| 项目 | 结果 | 详情 |
+|---|---|---|
+| 连接 | 通过 | open_new_instance + set_visible |
+| 基线加载 | 通过 | load_from_file 真实 .mot |
+| 求解 | 通过 | 电磁计算 146.3s(单点) |
+| 指标解析 | 通过 | 21 项指标(tavg_nm/ripple/efficiency/back_emf/...) |
+| 解析正确性 | 通过 | back_emf=11.15V 与 TR-2026-08-29-01 完全一致 |
+| 关键指标 | - | tavg_nm=0.52187、efficiency=86.06%、total_losses=41.95W |
+| 输出 | - | output/smoke_p3_m5/(不入库) |
+
+### 关键发现
+1. adapter 层(MotorCADAdapter)此前仅经 FakeAdapter 协议验证,真实求解通路由本次烟雾打通并暴露 run_point 路径 bug——印证「真实烟雾不可省」。
+2. 解析器归一化(afmcore.metrics)在真实导出上正确工作,与历史 TR-01 数值一致。
+
+### 遗留事项
+- 真实 adaptive 完整循环(多批多实例 + 参数写入)留待后续验证
+- 参数写入的真实变量名映射(Airgap 等)沿用 TR-01 已验证方案
+---
+
+## TEST-011:P3 平台化改造(M6 Web 端 AdaptiveLoop 执行桥 + 集成闭环)
+
+**日期**:2026-08-29
+**环境**:临时 SQLite DB(AFM_DB_PATH)+ KIMI_API_KEY=""(无 AI 后端,纯定量)
+**目的**:把 Web 端已验收的 adaptive 搜索(AdaptiveLoop)接到本地执行器:批次 -> adaptive_batch Task -> 执行器回传 -> report-results 回填 -> 续批 -> 收敛。验证新提交桥 submit_batch_to_executor + 新端点 POST /api/adaptive/loops/{id}/submit-batch。
+
+### 验证结果(PASS)
+| 项目 | 结果 | 详情 |
+|---|---|---|
+| 闭环驱动 | 通过 | fake plan -> initialize_search(3点) -> submit-batch -> 3 批 -> budget_exhausted,8 点 / 8 预算 |
+| Task 落库 | 通过 | 每批 1 个 adaptive_batch Task,含 loop_id/batch_id/point_ids/dynamic=True |
+| point_ids 一致性 | 通过 | 初始批次 ids=[0,1,2] 与 task point_ids 一致 |
+| 幂等性 | 通过 | 无 pending 批次时 submit 返回 task_id=None,不产生重复 Task |
+| HTTP 端点 | 通过 | POST /api/adaptive/loops/{id}/submit-batch:404(未找到) / 200(task_id) |
+| 全量回归 | 通过 | P2 36 项 + M2/M3/M4/M5/M6 全绿;91 .py 编译 0 失败 |
+
+### 架构整合说明
+项目存在两套 adaptive 循环(均为已验收):
+1. **AdaptiveLoop(Web 端,08-27)**:AI 方案 -> L0 -> FeasibilityFirstSearch -> 选批 -> report-results 回填 -> AI 分析 -> 经验库 -> 收敛;缺"本地执行器执行"环节。
+2. **AdaptiveOrchestrator(平台层,08-29)**:参数化驱动,桥搜索 <-> 任务系统 <-> 本地执行器(批次 Task、point_id 回填)。
+本次给 AdaptiveLoop 补 submit_batch_to_executor()(复用 task_manager 原语),打通"Web 智能层 + 本地执行层",未改 AdaptiveLoop 既有方法,未新建冲突路由(已撤销误建的 adaptive_orchestrator 路由)。
+
+### 遗留事项
+- 真实 Motor-CAD 多批 adaptive 循环(submit-batch -> 执行器真实求解 -> report-results)待环境就绪验证
+- 前端 adaptive 循环视图(批次/Task/回填状态展示)属 P4
+---
+
+## TEST-012:P3 收尾(工程规范落地 + 单元边界测试 + 全量回归 + 规范修复)
+
+**日期**:2026-08-29
+**环境**:临时 SQLite DB + 无 AI 后端;P4 验收套件在项目 web 环境内运行
+**目的**:P3 收尾自查——按新工程规范(禁止臆测/测试完备/代码规范/自查清单)核对 P1~P3 平台化批次,补齐边界/异常/空值单元测试,修复验收暴露的规范违规,同步文档状态。
+
+### 新增测试
+| 测试 | 覆盖 | 结果 |
+|---|---|---|
+| scripts/test_p3_unit_edge.py | 策略层(注册/未注册/空kind/坏类/别名归一/空points/分批/收敛/batch_size=0)、orchestrator(空参数/重复loop/缺失loop/批次未完成不推进)、AdaptiveLoop.submit 未初始化 RuntimeError、task_manager(缺失返回None/空参数) | PASS |
+
+### 修复的规范违规
+- **deploy.ps1 UTF-8 BOM(0xfeff)**:P4 验收套件 `deploy.ps1 is ASCII-only` 抓出;去掉 BOM 后 P4 验收复跑 **37 passed / 0 failed**。
+
+### 全量回归结果
+| 套件 | 结果 |
+|---|---|
+| test_platform_registry.py(P2) | PASS 36 项 |
+| test_p3_orchestrator.py(M2) | PASS |
+| test_executor_m3.py(M3) | PASS |
+| test_p3_m4_contract.py(M4) | PASS |
+| test_p3_closed_loop.py(M5) | PASS |
+| test_p3_adaptive_execution.py(M6) | PASS |
+| test_p3_unit_edge.py(新增) | PASS |
+| test_p4_acceptance.py(P4 验收,BOM 修复后) | 37 passed / 0 failed |
+
+---
+
+## TEST-023:P6-M1 Web 前端 UI/UX 全面重构
+
+**日期**:2026-08-30
+**环境**:Windows 10,Node v20.20.2,npm 10.8.2,Vue 3.3 + TypeScript 5.5 + Element Plus 2.4 + Vite 5
+**目的**:参考 SimScale / Ansys 等在线仿真工具设计语言,完成 B1(信息架构)+ B2(PlanDetail 分层)两批次前端重构,解决信息过载、导航混乱、参数命名不一致、视觉重复等问题
+**测试方式**:静态代码审查 + `npm run build`(vue-tsc 类型检查 + vite 生产构建)
+
+### 变更清单
+
+| 类别 | 文件 | 变更内容 |
+|---|---|---|
+| 全局样式 | `src/style.css` | 重写为设计令牌系统(CSS 变量):主色 #2563eb、中性灰阶、8px 网格、统一圆角/阴影/过渡;Element Plus 主题覆盖 |
+| 公共组件 | `src/components/StatCard.vue` | 新建,统一统计卡片(消除 4 处重复手写) |
+| 公共组件 | `src/components/SectionCard.vue` | 新建,统一内容区块卡片 |
+| 公共组件 | `src/components/PageHeader.vue` | 新建,统一页面头部 |
+| B1 布局 | `src/layouts/MainLayout.vue` | 侧边栏 5 组工作流导航 + AI 高级功能折叠 + 面包屑层级链 + 全局任务状态条 + 侧边栏折叠 + 页面过渡动画 |
+| B2 核心页 | `src/views/PlanDetail.vue` | 1129 行长卷重构为 4 Tab(概览/方案参数/仿真结果/AI闭环);固定参数默认折叠只显示修改项;运行中进度横幅;AI 闭环步骤向导 |
+| 页面优化 | `src/views/ProjectList.vue` | PageHeader + StatCard + SectionCard + 搜索/拓扑筛选 + 创建弹窗双列布局 |
+| 页面优化 | `src/views/Dashboard.vue` | PageHeader + StatCard + ECharts 趋势/Pareto 图 + 结果表 + CSV 导出 |
+| 页面优化 | `src/views/TaskManager.vue` | 6 项状态统计卡 + 状态筛选 + 创建任务从方案下拉选择(JSON 降为高级折叠)+ 详情抽屉优化 |
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | 全局设计令牌定义 | ✅ 通过 | CSS 变量覆盖主色/语义色/间距/圆角/阴影/字体/侧边栏/布局 8 大类 |
+| 2 | 公共组件创建 | ✅ 通过 | StatCard/SectionCard/PageHeader 三组件含 props/slots/类型定义 |
+| 3 | MainLayout 重构 | ✅ 通过 | 5 组导航渲染正常;AI 高级功能折叠/展开;面包屑随路由动态生成;全局任务状态 10s 轮询 |
+| 4 | PlanDetail Tabs 重构 | ✅ 通过 | 4 Tab 切换正常;固定参数修改项检测逻辑;分类折叠;AI 闭环步骤指示器 |
+| 5 | vue-tsc 类型检查 | ✅ 通过 | 0 错误(修复了 NavItem 联合类型、fixedCategories unknown[]、p.value string\|number、api.planApi 引用等 6 类类型问题) |
+| 6 | vite 生产构建 | ✅ 通过 | 2283 模块转换,11.62s 构建完成;业务 chunk 13~26kB,vendor 库独立分包 |
+| 7 | 构建产物体积 | ⚠️ 警告 | vendor-echarts 1042kB / vendor-element 948kB 超 500kB 警告(既有问题,非本次引入,建议后续 echarts 按需引入) |
+
+### 关键设计决策
+
+1. **主色选择 #2563eb(blue-600)**:比 Element Plus 默认 #409eff 更深沉专业,符合工程仿真工具调性;侧边栏用 #0f172a(slate-900)深色,与内容区浅灰形成对比
+2. **PlanDetail 固定参数折叠策略**:默认只显示"值与默认模板不同"的参数(modifiedFixedParams),其余按分类折叠;36 项参数中通常只有个位数需要工程师关注
+3. **预估耗时校准**:由硬编码 points×3min 改为 points×2.5min,贴近 README 记录的真实单点 90~150s
+4. **AI 闭环三步向导**:用 el-steps 展示 分析→迭代→提取 进度,每步独立卡片,按钮按依赖关系禁用(无结果不能分析,无分析不能迭代)
+5. **任务创建去 JSON 化**:主流程改为从方案下拉选择自动带出,JSON 输入降为"高级选项"折叠,降低工程师使用门槛
+
+### 遗留事项
+
+- B3(参数目录单一事实源):当前前端仍有 SCAN_PARAM_CN / categoryCnMap / BC_TEMPLATE / ALL_FIXED_PARAM_TEMPLATE 四处手写参数目录,需前后端协同统一为后端权威目录
+- B4(流程引导):ProjectDetail 步骤条可交互化、仿真前检查清单(含 5 个未确认变量名)、真实单点耗时回写校准
+- 前端实际渲染效果未在浏览器人工点检(本次为代码重构 + build 验证),建议 `npm run dev` 后人工走查核心流程
+- echarts / element-plus 全量引入导致 vendor chunk 过大,后续可按需引入优化首屏
+| 全量编译 | 93 .py 0 失败 |
+
+### 环境依赖(未独立运行,如实标注)
+- test_api_client.py:需 web 服务运行于 127.0.0.1:8000
+- test_robust_solver.py:需真实 Motor-CAD 连接与求解
+
+### 已知遗留
+- **ASCII 纪律未完全达标**:web/backend/app/routers/plans.py(164 字符)、services/fixed_params_template.py(44 字符)含中文字符串(用户 08-27 在途文件),建议 P4 前转 \uXXXX 或移入文档(本轮未动,避免改动用户文件引入风险)。
+- 参考案例目录(axial_mag_pull-master / torqrippswap-master)非 ASCII 属第三方代码,不在验收范围。
+- 文档同步已完成:PLATFORM_DESIGN_V2 第三批标记完成、README P5 平台化表拆分(第三批完成/第四批规划)。
+
+## TEST-013:P3 遗留处理(并发原子认领 + 断点恢复 + ASCII 纪律 + 环境依赖测试)
+
+**日期**:2026-08-29 **环境**:Windows / Python / 常驻 uvicorn(8000)+ 常驻执行器在跑(未干扰)
+
+**处理项**:
+
+1. **并发原子认领**:`task_manager.dispatch_task` 由"读-改-写"改为 SQLAlchemy 条件 UPDATE(`WHERE status='pending'` + rowcount 判定),多执行器竞争同一任务恰好一次成功。
+   - 测试 `scripts/test_p3_concurrency.py`:8 线程竞争同一 pending 任务 → 恰 1 win / 7 lost(ValueError);顺序二次认领拒绝;HTTP 级竞争同样恰 1 win。
+2. **断点恢复**:`FeasibilityFirstSearch.import_state()` + `AdaptiveLoop.export_state()/restore_state()` + `GET /loops/{id}/export`、`POST /loops/import` 端点。
+   - 测试 `scripts/test_p3_checkpoint.py`:run_id/used_budget/points/objective 一致,恢复后可继续 select_next_batch。
+3. **ASCII 纪律**:`plans.py`(14 中文串)+ `fixed_params_template.py`(86 中文 label)转 `\uXXXX`;运行解码正确(KIMI 空 key 纯定量降级 200)。
+4. **环境依赖测试**:`test_api_client.py` 用临时 DB + 后台 uvicorn 跑通真实链路;未杀用户常驻服务(PID 30496/39420 原样保留);真实 Motor-CAD 占用 license,robust 未改动不重跑,维持标注。
+
+**验证结果**:全部 PASS;全量回归基线绿(P2 36 / M3 / M4 / closed_loop / adaptive / unit_edge / p4_acceptance 37 / concurrency / checkpoint)。
+
+## TEST-014:P4-M1~M3(方案 Schema 单一权威 + 文档 V1.1→V2 + EXE 打包)
+
+**日期**:2026-08-29
+
+- **M1 Schema 统一**:`src.plan_schema.py` 增强(`parse_plan`/`validate_plan_dict` 入口 + `ScanVariable.from_dict` 容忍 min_value/max_value 别名 + `validate(require_model_path)` 分级);web 端 main.py 注入 repo root,plans/ai_plan 接入校验(400/422 拒绝非法 plan_data)。
+  - 测试 `scripts/test_p4_schema.py`:7 组全过(happy/别名/空值/异常/分级/拓扑策略/web 接入)。
+- **M2 文档 V1.1→V2.0**:设计方案追加"附录 B 实现现状对照"(afmcore 共享核心 / Schema 权威 / 双系统解耦 / 策略实现 vs 蓝图 / adaptive 闭环 / 可靠性 / 已知限制),版本表与 README 引用同步。
+- **M3 EXE 打包**:`run_task_executor.py` 加 `--version`/`--self-test`;`scripts/build_executable.ps1`(PyInstaller onefile,paths=src+root,collect-all ansys.motorcad)。
+  - 产物 `dist/PCB-AFM-Executor.exe`(12.6MB):`--version` 与 `--self-test`(mock 单点 status=OK)均通过。真实 Motor-CAD COM 连接依赖 license,不在打包自检内(标注环境依赖)。
+
+## TEST-015:P4-M4~M5(前端 adaptive 收敛曲线 + L0 上提共享核心层)
+
+**日期**:2026-08-29
+
+- **M4 收敛曲线**:`search.get_state_summary()` 新增 `points_history`(逐评估点 id/batch/params/objective/feasible/status),`SearchStateResponse` 携带该字段;前端 `AdaptiveOptimize.vue` 增收敛曲线(echarts 散点可行/不可行 + 当前最优 step 折线)。
+  - 测试 `scripts/test_p4_m4_convergence.py`:5 组全过(空历史/初始批次/跨批累积/不可行标记/响应模型接线)。
+  - vue-tsc:AdaptiveOptimize.vue 0 错误;全量 build 仍有**既有**类型错误(PlanDetail/ProjectDetail/ProjectList,未触碰,属项目 backlog)。
+- **M5 L0 上提**:`L0PreScreeningEngine` 迁至 `src/afmcore/l0/prescreening.py`(唯一实现,纯 stdlib),web 端薄 re-export 保持 6 处调用点兼容。
+  - 测试 `scripts/test_p4_m5_l0.py`:8 组全过(单一定义/四类约束门/空输入门/filter_feasible/feasibility_search 集成)。
+  - 连带修复:`test_p3_closed_loop.py`/`test_p4_m4_convergence.py` 补 repo root 到 sys.path(L0 re-export 依赖 src 解析)。
+  - 回归:P2 36 / M4 / M5 / M6 / closed_loop / checkpoint / concurrency 全绿;全量 py_compile 0 失败;ASCII 0 违规。
+
+**环境依赖(未自动化,如实标注)**:真实 Motor-CAD 求解(license server)不在本批自动化范围内;EXE 的真实 Motor-CAD COM 连接需在目标机验证。
+
+## TEST-016:P5-M1(前端全量 build 类型错误清零)
+
+**日期**:2026-08-30 **环境**:Windows / Node v20.20.2 / npm 10.8.2 / Python 3.13.13
+
+**背景**:backlog B1——`npm run build`(vue-tsc && vite build)历史遗留 71 处类型错误(TS2339/TS2345/TS7006),涉及 Dashboard/ExecutorMonitor/ExperienceList/PlanDetail/ProjectDetail/ProjectList 6 个 .vue 文件。P4-M4 只保证 AdaptiveOptimize.vue 自身 0 错误,全量 build 一直红。
+
+**根因**:`src/api/index.ts` 的响应拦截器(D2 fix)运行时已把 `AxiosResponse` unwrap 为 `.data`,但 TS 类型上 `api.get()/post()` 仍声明为 `Promise<AxiosResponse>`,导致所有调用处直接访问响应字段(`res.items`/`res.metrics` 等)报 TS2339;PlanDetail 中 `task.status` 被误判为 AxiosResponse.status(HTTP 状态码,number 类型)报 TS2345。
+
+**修复**:
+1. `web/frontend/src/api/index.ts`:axios 实例类型改写为 `UnwrappedApi` 接口(get/post/put/delete 均返回 `Promise<T>`,默认 any)——类型声明与运行时行为对齐;拦截器逻辑原样保留(实例改名 instance,再 cast 到 UnwrappedApi)。一处修复覆盖全部 69 处 TS2339/TS2345。
+2. `web/frontend/src/views/PlanDetail.vue`(418 行):`@selection-change` 回调参数 `sel` 显式标注 `any[]`,消除 TS7006 隐式 any。
+
+**验证结果**:
+- `npm run build`(vue-tsc && vite build):vue-tsc 0 错误;vite 2274 modules 构建成功,真实退出码 0(cmd /c 确认)。
+- 后端启动(uvicorn 8000):`/api/health` 200;`test_api_client.py` 真实链路 PASSED(8 项目/历史结果正常读取)。
+- 前端 vite dev(5173):200;`/api` 代理 health 200。
+- 全量 Python 回归(14 个脚本)EXIT=0 全绿(P2 36 / P3 unit_edge/orchestrator/m4_contract/concurrency/checkpoint/closed_loop/adaptive_execution / P4 acceptance/schema/m4_convergence/m5_l0 / robust_solver / executor_m3)。
+
+**遗留/说明**:
+- `UnwrappedApi` 默认返回 `any`,与既有运行时行为一致;需要类型安全的调用可传泛型(如 `api.get<Project[]>()`)。
+- 两个 >500kB 大 chunk 警告为既有现象,非本批引入,不影响 build 通过。
+- 前端 JS 运行时冒烟:dev server + API 代理 + 后端真实链路通过;未做浏览器自动化点击验证(纯类型断言改动,编译后类型擦除,无运行时代码差异)。
+
+
+## TEST-017:P5-M2(EXE 配置化 config.json + mock 分支修复 + EXE 端到端 mock 验证)
+
+**日期**:2026-08-30 **环境**:Windows / Python 3.13.13 / PyInstaller 6.22.2 / 独立后端 8010 + 临时 DB(未干扰用户常驻 8000 服务)
+
+**目的**:把本地执行器 EXE 配置化(web 地址 / model 路径 / 日志 / 实例数从 `executor_config.json` 读取),并打通"EXE → Web 端到端回传"验收链路(mock 求解先行)。
+
+### 改动内容
+1. **新增 `scripts/executor_config.py`**:配置加载器。优先级:`--config` > `$EXECUTOR_CONFIG` > `<EXE目录>/executor_config.json` > `<仓库根>/executor_config.json` > 内置默认;单字段可被环境变量覆盖(WEB_BASE_URL/MOTORCAD_MODEL/EXECUTOR_INSTANCES 等);相对路径按 EXE 目录/仓库根解析;校验 instances≥1、poll_interval>0、log_level 枚举、web_base_url http(s)、enable_mock bool。
+2. **新增 `executor_config.json`(仓库根模板)**:8 字段侧车配置。
+3. **改造 `scripts/run_task_executor.py`**:`--config/--instances/--interval/--mock/--log-dir/--log-level`;logging 落盘 `output/executor_logs/`;单入口多实例(instances>1);版本号升 1.1.0。
+4. **改造 `scripts/run_task_executor_parallel.py`**:复用共享配置(保留 `--instances/--interval/--mock` CLI 兼容)。
+5. **修复 `scripts/task_executor.py`(P4-M3 遗留)**:`MotorCADTaskExecutor._run_simulation_point` 忽略 `enable_mock` 无条件走真实 adapter;现 mock 分支先于 adapter(mock 结果带 `source="mock"`,不启动 Motor-CAD、不要求 model_path)。
+
+### 测试
+| 测试 | 覆盖 | 结果 |
+|---|---|---|
+| scripts/test_executor_config.py | 20 用例:默认/文件合并/绝对路径/环境覆盖/优先级/边界(instances=1, poll=0.5, 空model)/异常(坏JSON/非对象/坏URL/0实例/坏level/坏mock类型)/空值(空对象/null字段/空env) | ✅ EXIT=0 |
+| scripts/test_executor_p5m2.py | 6 用例:mock 点 source/多点/非法 model_path 仍 mock/默认 mock off/真实模式缺 model_path 报错/空 model_path | ✅ EXIT=0 |
+| 全量回归 | test_*.py 全绿(16 个脚本 EXIT=0;test_api_client 需后端已跳过,由端到端验证替代) | ✅ 16/16 |
+| ASCII/CRLF | 新改 6 个 .py 纯 ASCII、CRLF 与现有文件一致 | ✅ |
+
+### EXE 端到端验证(mock 链路,全部 PASS)
+1. 重新打包 `dist/PCB-AFM-Executor.exe`(PyInstaller onefile,`--version`=1.1.0、`--self-test` OK)。
+2. 独立后端:`AFM_DB_PATH=<临时DB>` + `AFM_PORT=8010` + uvicorn 启动,`/api/health` 200。
+3. EXE 以 `--config <临时侧车>` 启动(web_base_url=8010、enable_mock=true、poll_interval=2)。
+4. `POST /api/tasks` 创建 3 点任务(airgap 0.8/1.0/1.2)→ EXE 轮询认领 → mock 求解 → 回传 Web。
+5. 任务 `completed`、3/3 成功(successful_points=3)、duration 0.05s;点级结果 `source="mock"`、point_id 保留;聚合指标:tavg 34.56~42.05 N·m、eff 87.95~89.73%、losses 29.61~56.77 W、temp 85.1~101.1 ℃。
+
+### 过程中发现并处理的坑
+- **EXE 相对路径基准**:EXE(frozen)模式下相对路径(model_path/log_dir)按 **EXE 所在目录**解析(dist/),而非仓库根——侧车 config 建议用绝对路径或相对 EXE 目录的路径;已在 executor_config.py 注释与 README 说明。
+- **P4-M3 mock 未生效(真 bug)**:`MotorCADTaskExecutor._run_simulation_point` 无条件走真实 adapter,`--mock`/`enable_mock` 从未真正进入 mock 分支;本次修复(见上),并用"非法 model_path + mock 仍成功"用例锁定回归。
+
+### 遗留事项
+- **真实 EXE 内 Motor-CAD COM 端到端(目标机)**:license server 本机在跑(1055@localhost)但用户常驻执行器占用轮询,且真实求解 90~150s/点;验收建议在目标机执行:
+  1. 复制 `dist/PCB-AFM-Executor.exe` + `executor_config.json`(web_base_url 指向实际后端、model_path 填目标机 .mot 绝对路径)到目标机;
+  2. 确认 `MOTORCAD_ACTIVEX` 与 `ANSYSLMD_LICENSE_FILE=1055@<server>` 已设;
+  3. 后端创建单点/短扫描任务;`PCB-AFM-Executor.exe` 启动后轮询认领;
+  4. 观察任务 completed、结果带真实指标(对照 TEST-002/003/010:5000rpm eff≈86.06%、tavg≈0.52 N·m、back_emf≈11.15V)。
+
+
+## TEST-018:P5-M2 补充(真实 EXE 端到端单点验证 + 真实参数链路修复)
+
+**日期**:2026-08-30 **环境**:Windows + Motor-CAD v261(MOTORCAD_ACTIVEX 已设、license 1055@localhost、exe 存在);独立后端 8010 + 临时 DB(未干扰用户常驻 8000 服务)
+
+**目的**:完成 P5-M2 验收点②"真实 EXE 端到端回传 Web 结果"——用打包后的 `dist/PCB-AFM-Executor.exe` 以真实模式(非 mock)驱动 Motor-CAD 求解单点并回传 Web。
+
+### 过程中发现并修复的真实链路 bug(mock 测不到,真实求解才暴露)
+1. **业务参数名未映射**:任务参数用 L0/方案层对齐名 `airgap_mm`,Motor-CAD 实际变量名是 `Airgap`(TEST-002 已探测)。此前 `resolve_variable_name` 直接透传导致 `Could not find airgap_mm`。修复:`scripts/robust_motorcad.py` `VARIABLE_NAME_MAP` 增加业务别名 `"airgap_mm": {"default": "Airgap"}`。
+2. **point_id 元数据被当变量写**:M3 的 `point_id` 透传标记被 `run_single_point` 参数循环当作 Motor-CAD 变量 `set_variable` 导致 `Could not find point_id`。修复:参数写入循环跳过元数据键(`point_index`/`point_label`/`point_id`)。
+
+### 验证结果(PASS)
+| 项目 | 结果 | 详情 |
+|---|---|---|
+| EXE 真实模式 | 通过 | `--config` 侧车(enable_mock=false,model_path 绝对路径) |
+| 认领→求解→回传 | 通过 | 任务 `completed`,duration 167.5s(含 Motor-CAD 启动 + 求解) |
+| 求解时长 | 通过 | solve_time_s=145.22(单点电磁计算) |
+| 指标完整性 | 通过 | 20+ 指标(tavg/ripple/efficiency/losses/back_emf/温度类/电流类/转速) |
+| 数值一致性 | 通过 | tavg_nm=0.52187、eff=86.06%、total_losses=41.945W、back_emf=11.15V —— 与 TEST-010 完全一致 |
+| point_id 保留 | 通过 | 结果点级带 point_id=1 |
+| 回归 | 通过 | 全量 test_*.py 16/16 EXIT=0(含 test_robust_solver) |
+
+### 新增测试
+- `scripts/test_executor_p5m2.py` 扩至 8 用例:新增 `resolve_variable_name("airgap_mm")=="Airgap"` 映射断言 + `point_id` 排除回归断言。
+
+### 遗留事项
+- 真实短扫描(2+ 点)/多实例(instances>1)可复用本链路在目标机验证;本机已用单点打通"EXE→真实 Motor-CAD→Web"全链路。
+- 其余业务参数(current_a 等)的 Motor-CAD 变量名映射待按变量探测逐个补充(AGENTS.md 纪律:不做臆测,逐名探测确认)。
+
+
+## TEST-019:P5-M3 adaptive 可视化补全(批次状态 + L0 摘要 + 运行期轮询)
+
+**日期**:2026-08-30 **环境**:Windows + 独立后端 8011(未干扰用户常驻 8000)+ 前端 vue-tsc build
+
+**目的**:完成 P5-M3 验收点"前端 adaptive 三视图可见"——批次点状态可视化(每批进度/分布)+ L0 预筛选结果前端视图 + adaptive 循环运行期状态推送(轮询增强)。
+
+### 后端改动
+1. `web/backend/app/services/feasibility_search.py` `get_state_summary()` 增加 4 字段:
+   - `infeasible_points` / `failed_points`:状态计数
+   - `batch_summary`:按 batch_id 分组,每批含 total/pending/ok/infeasible/failed/best_objective(respect objective_direction)
+   - `l0_summary`:sampled/feasible/infeasible/pass_rate/top_infeasible_reasons(name/count/category,从不可行点 feasibility_report 聚合)
+   - 新增 3 个 helper:`_count_by_status` / `_build_batch_summary` / `_build_l0_summary`
+2. `web/backend/app/routers/search.py` `SearchStateResponse` 增加对应 4 字段(带默认值,向后兼容)。
+
+### 前端改动(`views/ai/AdaptiveOptimize.vue`)
+1. **运行期自动轮询**:创建 search 后启动 3s 轮询,convergence_status != searching 时自动停止,onBeforeUnmount 清理。
+2. **L0 预筛选摘要卡片**:总采样/L0可行/L0拒绝/可行率进度条 + 主要不可行原因 Top N tag。
+3. **批次状态总览卡片**:每批一行(批次号/点数/完成进度条/状态分布 tag/批内最优值)。
+
+### 验证结果(PASS)
+| 项目 | 结果 |
+|---|---|
+| 后端单元测试 | 8/8 PASS(test_search_state_summary.py:正常/报告后聚合/min-max方向/failed计数/空search边界/infeasible原因结构/未知id容错/批次排序) |
+| 前端类型检查 | vue-tsc && vite build 成功,零类型错误(P5-M1 清零保持) |
+| 端到端 API | 独立后端 8011:POST /search/create 返回 batch_summary(1批,6点全pending) + l0_summary(sampled=6,pass_rate=1.0);GET /search/{id}/state 同样返回新字段 |
+| 全量回归 | 16/16 PASS(含新增 test_search_state_summary) |
+
+### 遗留事项
+- WebSocket 实时推送未做(P5-M3 验收允许轮询增强;当前 3s 轮询已满足运行期状态可见)。
+- L0Prescreen.vue 单点评分页面保持现状(P5-M3 的 L0 视图在 AdaptiveOptimize 内通过 l0_summary 实现)。
+
+
+## TEST-020:P5-M4 策略层高级管线(Morris 灵敏度 + IDW 代理 + 预算自适应)
+
+**日期**:2026-08-30 **环境**:Windows + 纯 stdlib(无 numpy/scipy/sklearn/lightgbm)
+
+**目的**:完成 P5-M4 验收点"蓝图 §6 管线可运行;现有策略回归不破"——新增 Morris 灵敏度筛选策略 + 代理模型引导策略(含预算自适应批次大小),注册到 strategies 注册表。
+
+### 依赖评估与降级决策
+- 环境探测:numpy/scipy/sklearn/lightgbm **全部未安装**。
+- 项目纪律:feasibility_search.py / lhs.py 均标注 "Pure-Python implementation (no numpy/scipy dependency)"。
+- P5 规划 §6 约束:"Kriging/NSGA-II 引入新依赖——P5-M4 先评估,轻量实现优先,避免重依赖"。
+- **决策**:代理模型用纯 stdlib **IDW(反距离加权)** 替代 Kriging。IDW 给出预测值 + 基于最近邻距离的不确定性,支持 explore-exploit 权衡;精度低于 Kriging(尤其非平稳曲面),但满足"预测+不确定性选点"核心需求。后续如需 Kriging 须引入 scipy 并在同接口下替换。
+
+### 新增策略
+1. **`MorrisStrategy`**(`src/afmcore/strategies/morris.py`,kind=`morris`)
+   - 纯 stdlib Morris OAT 灵敏度筛选:n_trajectories 条轨迹,每条 n_params+1 个点
+   - 每步只变一个参数(随机排列 + ±Δ 方向),Δ = n_levels/(2(n_levels-1))
+   - `state()` 返回 `sensitivity_ranking`(每参数 mu/mu_star/sigma/n_effects,按 mu_star 降序)+ `key_parameters`(mu_star>0 的前半)
+   - 奇数 n_levels 自动转偶数;参数值带 step 时自动对齐网格
+2. **`SurrogateGuidedStrategy`**(`src/afmcore/strategies/surrogate_guided.py`,kind=`surrogate_guided`)
+   - 两阶段:初始 LHS 采样(n_initial)→ 代理引导选点(UCB 采集)
+   - IDW 代理:预测 = Σ(y_i/d_i^p) / Σ(1/d_i^p),不确定性 = 最近邻归一化距离
+   - UCB 采集:score = pred + kappa*uncertainty(maximize)或 -pred + kappa*uncertainty(minimize)
+   - 预算自适应批次大小:growth = 1 + mean_uncertainty * remaining_budget_ratio,size ∈ [1, max_batch_size]
+   - `state()` 返回 phase(initial/surrogate_guided/exhausted)、budget 用量、surrogate 诊断(n_train/loo_rmse/mean_uncertainty/last_batch_size)
+   - select_next 时记录 point_id→params 映射,report 时自动关联(基类协议无需改)
+
+### 验证结果(PASS)
+| 项目 | 结果 |
+|---|---|
+| Morris 单元测试 | 15/15 PASS(test_strategy_morris.py:轨迹点数/step0无changed_param/线性函数灵敏度排名/收敛/空参数/单轨迹/奇数n_levels/参数边界/未知id/缺失metric/failed排除/注册/state契约) |
+| Surrogate 单元测试 | 17/17 PASS(test_strategy_surrogate.py:初始LHS/bowl收敛/maximize/空参数/budget耗尽/n_initial>budget/自适应批次边界/未知id/缺失metric/failed排除/注册/state契约/IDW预测/距离计算/归一化往返) |
+| Morris 冒烟 | 线性函数 y=2a+0.5b,灵敏度排名 a(mu_star=2.0) > b(0.5),sigma=0(线性无交互)✅ |
+| Surrogate 冒烟 | bowl 函数 y=(a-0.5)^2+(b-0.5)^2,budget=30,best y=0.007(理想 0.0),LOO RMSE=0.089 ✅ |
+| 策略注册 | list_strategy_kinds() = [adaptive, full_factorial, lhs, morris, surrogate_guided] ✅ |
+| 全量回归 | 19/19 PASS(含 2 个新测试) |
+
+### 遗留事项
+- Kriging 代理未实现(环境无 scipy,项目纪律纯 stdlib);IDW 为降级替代,已在策略 docstring 标注升级路径。
+- NSGA-II 多目标优化不在 P5-M4 范围(蓝图 §6.4,可能 P5-M5+)。
+- 代理模型未接入 Web 端 plan_schema 的 method 字段(当前 strategies 注册表可用,Web 端 method 白名单待扩展)。
+
+
+## TEST-021:P5-M5 多工具适配器(Maxwell/JMAG mock + 执行器 tool 参数化)
+
+**日期**:2026-08-30 **环境**:Windows + 纯 stdlib(无 Ansys Maxwell / JMAG 真实安装)
+
+**目的**:完成 P5-M5 验收点"`get_adapter("maxwell")` 可跑 mock/真实链路"——新增 MaxwellAdapter 和 JMAGAdapter,注册到适配器注册表,执行器 `tool` 参数动态选择适配器。
+
+### 真实接入环境依赖标注
+- **Ansys Maxwell**:真实接入需要 Ansys Maxwell + PyAEDT(ansys-pythonnet),当前环境未安装。mock 实现保持接口稳定,真实接入路径已在适配器 docstring 标注(connect/set_parameter/run_simulation/extract_metrics 对应 PyAEDT 调用)。
+- **JMAG**:真实接入需要 JMAG Designer + Python API(jmagpy),当前环境未安装。mock 实现同理。
+- 与既有 Motor-CAD 同策略:接口 + mock 链路先行,真实接入标注环境依赖(P5 规划 §6 约束)。
+
+### 新增适配器
+1. **`MaxwellAdapter`**(`src/afmcore/adapters/maxwell.py`,tool=`maxwell`)
+   - mock 实现:内存参数记录 + 回读校验 + 确定性合成指标
+   - capability_domains = ("electromagnetic", "thermal")(含 winding_temp_c 热指标)
+   - 指标模型:tavg=0.50*airgap+0.30*magnet+0.10;eff=85.0+0.10*airgap;losses=42.0-0.80*airgap
+   - mock=False 时 connect() 抛 RuntimeError(明确标注环境依赖)
+2. **`JMAGAdapter`**(`src/afmcore/adapters/jmag.py`,tool=`jmag`)
+   - mock 实现,指标系数与 Maxwell 略有不同以区分工具
+   - capability_domains = ("electromagnetic",)
+   - 指标模型:tavg=0.45*airgap+0.32*magnet+0.12;eff=84.5+0.12*airgap;losses=43.0-0.75*airgap
+
+### 执行器改造
+- `scripts/task_executor.py` `MotorCADTaskExecutor._ensure_adapter()`:从硬编码 `import afmcore.adapters.motorcad` 改为根据 `self.tool` 动态 import(motorcad/maxwell/jmag),未知 tool 依赖预注册适配器。
+- `executor_config.json` 的 `tool` 字段(P5-M2 已加)现在可选择 "motorcad"/"maxwell"/"jmag"。
+
+### 验证结果(PASS)
+| 项目 | 结果 |
+|---|---|
+| 适配器注册 | registered_tools() 含 maxwell/jmag ✅ |
+| Maxwell mock 全链路 | connect→load→set→run→extract→run_point,status=OK,tavg=2.10(airgap=1,magnet=5)✅ |
+| JMAG mock 全链路 | 同上,tavg=2.17(系数不同,可区分工具)✅ |
+| 工具区分 | 相同参数 maxwell tavg=2.10 ≠ jmag tavg=2.17 ✅ |
+| set_parameter 回读校验 | 写入后回读一致 ✅ |
+| 边界:空参数 | tavg=0.10(所有参数默认 0)✅ |
+| 边界:未 connect 就 run | 抛 RuntimeError ✅ |
+| 异常:mock=False | connect() 抛 RuntimeError(环境依赖标注)✅ |
+| 异常:未知 tool | get_adapter 抛 KeyError ✅ |
+| 执行器动态 import | tool=maxwell→adapter.tool_name="maxwell";tool=jmag→"jmag";未知 tool→KeyError ✅ |
+| 单元测试 | test_adapters.py 22/22 PASS |
+| 全量回归 | 20/20 PASS(含新测试) |
+
+### 遗留事项
+- 真实 Maxwell/JMAG 接入待目标机环境(需安装对应软件 + Python API)。
+- Maxwell 热域(winding_temp_c)当前为合成值,真实接入后从 Maxwell 热求解器提取。
+- 前端/GUI 工具选择下拉框待扩展(当前 executor_config.json 可配,Web 端任务创建页待加 tool 字段)。
+
+
+## TEST-022:P5-M6 多物理场 L2 接入(热网络+结构指标 + 报告模板化)
+
+**日期**:2026-08-30 **环境**:Windows + 纯 stdlib(无真实 Motor-CAD 热求解运行)
+
+**目的**:完成 P5-M6 验收点"热指标入库与报告展示"——metrics.py 扩项(热网络+结构指标,自动生效)、robust_motorcad 热求解开关、report_generator 按物理域分组模板化。
+
+### 1. metrics.py 扩项(单一事实源,自动生效)
+新增 10 个指标(均 required=False,不影响现有必需指标校验):
+
+**热网络指标(domain=thermal)**:
+| key | label | unit | direction |
+|---|---|---|---|
+| winding_hotspot_temp_c | Winding Hotspot Temp | C | lower |
+| magnet_temp_c | Magnet Temp | C | lower |
+| stator_temp_c | Stator Temp | C | lower |
+| bearing_temp_c | Bearing Temp | C | lower |
+| temp_rise_c | Temperature Rise | C | lower |
+| thermal_resistance_k_w | Thermal Resistance | K/W | lower |
+
+**结构/机械指标(domain=structural)**:
+| key | label | unit | direction |
+|---|---|---|---|
+| axial_force_n | Axial Force | N | lower |
+| radial_force_n | Radial Force | N | lower |
+| max_stress_mpa | Max Stress | MPa | lower |
+| deformation_mm | Max Deformation | mm | lower |
+
+每个指标含英文+中文(\uXXXX)别名。`extract_all_metrics()` 遍历 METRIC_DEFINITIONS,新指标自动被提取,无需调用方修改。总指标数从 25 增至 35。
+
+### 2. robust_motorcad.py 热求解开关
+- `__init__` 新增 `enable_thermal: bool = False`(默认关,保持现有电磁-only 行为)
+- `run_single_point` 新增 `enable_thermal: Optional[bool] = None`(覆盖实例默认)
+- 电磁求解后尽力而为调用 `do_thermal_calculation()`(失败记录警告,不中断电磁结果)
+- 电磁导出后尽力而为导出 Thermal 结果并合并 metrics(失败记录警告)
+- 热求解需要模型配置热网络,标注为环境依赖
+
+### 3. report_generator.py 按物理域分组模板化
+- Results Summary 从单一 metrics 表改为按域分组:Electromagnetic / Thermal / Structural
+- 每个域一个 level=2 子标题 + 表格,空域跳过
+- 从 afmcore.metrics 导入 METRIC_DEFINITIONS 获取 domain/label/unit(单一事实源)
+- `_metric_display()` 格式化 label(含 unit)和 value(float 用 %.4g)
+- JSON fallback report 也包含 `metrics_by_domain` 字段
+
+### 验证结果(PASS)
+| 项目 | 结果 |
+|---|---|
+| 新指标定义 | 热6+结构4,均有 domain 字段,required=False ✅ |
+| 总指标数 | 35(原25+新10)✅ |
+| 热指标提取 | mock CSV 含 Magnet Temperature 等字段,extract_all_metrics 自动提取 ✅ |
+| 结构指标提取 | mock CSV 含 Axial Force 等字段,自动提取 ✅ |
+| 中文别名 | \u6c38\u78c1\u4f53\u6e29\u5ea6 → magnet_temp_c ✅ |
+| 必需指标校验 | 缺热/结构指标不影响 check_required_metrics ✅ |
+| 域分组 | electromagnetic/thermal/structural 正确分组,空域跳过 ✅ |
+| 未知 key | 默认归入 electromagnetic ✅ |
+| 报告 JSON | metrics_by_domain 字段存在,热/结构域正确 ✅ |
+| robust 热参数 | __init__ 和 run_single_point 均有 enable_thermal 参数 ✅ |
+| 单元测试 | test_metrics_extension.py 20/20 PASS |
+| 全量回归 | 21/21 PASS(含新测试) |
+
+### 遗留事项
+- 真实 Motor-CAD 热求解未运行(需要模型配置热网络 + 实际启动 Motor-CAD,当前为代码路径预留)。
+- 结构指标(轴向力/应力/变形)需要 Motor-CAD 结构模块或第三方 FEA 工具,当前为 metrics 定义+报告展示预留。
+- 原有 winding_temp_c 指标无 domain 字段,默认归入 electromagnetic(可后续加 domain=thermal)。
+
+
+---
+
+## 2026-08-30: 拓扑感知变量名映射与执行前校验(P5-M2)
+
+### 测试环境
+- OS: Windows
+- Python: 3.x
+- Motor-CAD: 2026R1 (v261)
+- 后端: FastAPI + SQLite
+- 前端: Vue3 + Element Plus
+
+### 测试目的
+修复 Plan 23 全部 80 个扫描点失败的问题,并建立根本性防护机制,防止 RFM/AFM 变量名不匹配错误再次发生。
+
+### 问题根因
+Plan 23 的扫描变量使用了径向磁通电机(RFM)的变量名:
+- `Stator_Lam_Outer_Dia`(80~100mm,5 档)
+- `Stator_Lam_Inner_Dia`(45~60mm,4 档)
+- `Magnet_Thickness`(2~5mm,4 档)
+
+4 × 5 × 4 = 80 个点,全部在写入第一个参数 `Stator_Lam_Outer_Dia` 时失败:
+```
+RuntimeError: MotorCADError writing Stator_Lam_Outer_Dia:
+pymotorcad: set_variable: Error in SetVariable: Could not find Stator_Lam_Outer_Dia
+```
+
+当前模型 `MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot` 是轴向磁通电机(SSSR),正确的变量名是 `Stator_Outer_Diameter` / `Stator_Inner_Diameter`。
+
+### 修复措施
+1. 新增 `topology_variable_map.py`:拓扑感知变量名映射表(41 个 AFM 已知变量 + RFM→AFM 别名映射 + 模板逻辑名映射)
+2. 修复 `_expand_plan_to_parameters()`:扫描变量经过模板 motorcad_var + 拓扑别名映射
+3. 新增 `start_simulation` 执行前变量名校验:未知变量返回 400 + 建议名
+4. 补充模板 8 个参数的 motorcad_var
+5. 新增 `GET /api/plans/variable-catalog` API
+6. 前端 PlanDetail.vue 从后端获取变量目录
+
+### 测试步骤与结果
+| 测试项 | 结果 |
+|---|---|
+| 拓扑归一化(SSSR/AFIR/RFM/None/别名) | ✅ PASS |
+| RFM→AFM 别名解析(Stator_Lam_Outer_Dia → Stator_Outer_Diameter) | ✅ PASS |
+| 模板逻辑名映射(Number_of_Slots → Slot_Number) | ✅ PASS |
+| 已知变量校验(AFM 变量已知,RFM 变量在 AFM 拓扑未知) | ✅ PASS |
+| 参数批量校验(valid/unknown/aliases_resolved 分类) | ✅ PASS |
+| 未知变量建议(suggest_alternative) | ✅ PASS |
+| 已知变量集合获取(41 个 AFM 变量) | ✅ PASS |
+| **Plan 23 回归测试**(80 点,RFM 名被映射,全部已知) | ✅ PASS |
+
+### 单元测试
+- 文件:`scripts/test_topology_variable_map.py`
+- 结果:8/8 PASS
+- 运行命令:`python scripts/test_topology_variable_map.py`
+
+### 关键数据
+- AFM (SSSR) 已知变量数:41
+- 模板参数总数:37
+- 有 motorcad_var 的模板参数:35(仅剩 Current_Density、Insulation_Class 合理留空)
+- Plan 23 失败点数:80/80(修复前)→ 0/80(修复后,全部可正确映射)
+
+### 遗留事项
+- 后端服务需重启以加载新代码(uvicorn --reload 模式自动重载)
+- 真实 Motor-CAD 运行验证待执行(需要重启后端后重新启动 plan 23 仿真)
+- AFIR 拓扑的特有变量待补充(当前继承 SSSR 变量集合)
+- AI 方案生成端的拓扑感知变量名选用待集成(当前后端映射层已能兜底)
+
+
+---
+
+## 2026-08-30: AI 一键生成方案 422 错误修复(P5-M2 补充)
+
+### 问题现象
+在项目详情页点击"一键 AI 生成方案"按钮,前端报错。后端返回 422:
+```
+Invalid generated plan: plan_data malformed: could not convert string to float: 'Star'
+```
+
+### 根因
+`src/plan_schema.py` 中 `FixedParam.value` 被定义为 `float` 类型,且 `from_dict()` 中强制 `float(d.get("value", 0.0))` 转换。
+
+AI 生成的方案中包含字符串枚举类型参数:
+- `Winding_Connection`: `"Star"`(星形连接)
+- `Cooling_Type`: `"Natural Convection"`(自然冷却)
+- `CurrentDefinition`: `"Peak"`(峰值电流)
+
+这些是 Motor-CAD 合法的枚举/字符串参数,但 `float("Star")` 抛出 `ValueError`,被 `validate_plan_dict()` 捕获后返回 422。
+
+### 修复措施
+修改 `src/plan_schema.py`:
+1. `FixedParam.value` 类型从 `float` 改为 `Any`(支持数字和字符串枚举)
+2. `from_dict()` 中移除 `float()` 强制转换,保持原始值类型
+3. `generate_full_params()` 返回类型注解从 `dict[str, float]` 改为 `dict[str, Any]`
+
+### 测试结果
+| 测试项 | 结果 |
+|---|---|
+| FixedParam 接受字符串枚举值(Winding_Connection="Star") | ✅ PASS |
+| 数字值(int/float)仍正常工作 | ✅ PASS |
+| to_dict -> from_dict 字符串值往返保留 | ✅ PASS |
+| 混合数字/字符串参数的方案校验通过 | ✅ PASS |
+| generate_full_params 保留字符串枚举值 | ✅ PASS |
+| 空 fixed_params 边界情况 | ✅ PASS |
+| AI 生成方案完整回归测试(9 固定参数 + 2 扫描变量 = 9 点) | ✅ PASS |
+
+### 单元测试
+- 文件:`scripts/test_plan_schema.py`
+- 结果:7/7 PASS
+- 运行命令:`python scripts/test_plan_schema.py`
+
+### 影响范围
+- 此修复不影响已有数字参数的行为(int/float 保持原类型)
+- 字符串枚举参数现在可以正常通过校验、保存到数据库、并在仿真执行时传递给 Motor-CAD
+- 后端服务需重启以加载新代码
+
+## TEST-024:环境体检脚本 check_machine_paths.py 验证
+
+**日期**:2026-09-01
+**测试环境**:Windows,Python 3.14.7(AI shell),Motor-CAD v261,git 仓库
+**测试目的**:验证新增的 scripts/check_machine_paths.py(Playbook V2 落地:换机/新会话环境体检)能正确检查并报告环境状态。
+**测试脚本**:`scripts/check_machine_paths.py`
+**输出目录**:无(只读体检,stdout 直接输出)
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | 运行 `python scripts/check_machine_paths.py` | ✅ | 退出码 1(存在缺项时按设计返回 1) |
+| 2 | Python 版本 | ✅ | 3.14.7(≥3.10) |
+| 3 | 环境变量 | ✅ | MOTORCAD_ACTIVEX=...activex.bat;ANSYSLMD_LICENSE_FILE=1055@localhost |
+| 4 | Motor-CAD exe | ✅ | activex.bat + D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe 均存在 |
+| 5 | Python 依赖包 | ⚠️ | 6 包在当前 AI shell 解释器 MISSING(预期:shell 未装项目依赖,需在项目实际解释器下运行或 pip install) |
+| 6 | Git 状态 | ⚠️ | HEAD 有效;工作区有未提交改动(本次框架升级文件,待提交) |
+| 7 | 仓库资产 | ✅ | models/*.mot、experience.db、executor_config.json、web/、src/afmcore 全部就位 |
+| 8 | node/npm | ✅ | 在 PATH 上 |
+| 9 | py_compile + ASCII | ✅ | check_machine_paths.py 编译通过、纯 ASCII(符合硬性约束 §1) |
+
+### 关键数据 / 发现的问题 / 修复措施
+
+- **关键数据**:无项目内 `.venv/.env`(venv 探测未命中)→ 包缺失反映的是当前 shell 解释器视图,非项目实际运行环境;项目真实依赖需按 KNOWLEDGE_BASE §1 安装(`pip install ansys-motorcad-core pyside6 pandas` 等)。
+- **发现的问题**:AI shell 的 Python 3.14 与项目依赖环境分离,`find_spec` 全部 MISSING 属环境差异而非脚本 bug。
+- **修复措施**:脚本已加“项目 venv 探测”提示(若存在 .venv 会提示用其解释器复检);`--fix` 打印精确修复命令(setx / pip install)但不自动执行,避免脚本擅自改系统环境。
+- **验证方式**:同一脚本两条命令(默认 + --fix)运行观察;py_compile 编译;ASCII 字符集检查。
+
+## TEST-025:AI 方案生成 topology/search_strategy 归一化修复验证
+
+**日期**:2026-09-02
+**测试环境**:Windows,系统 Python 3.12.10(uvicorn 0.52.4 / fastapi 0.141.1),Kimi k3,SQLite
+**测试目的**:验证 AI 一键生成方案在 topology='AFIR' 且 search_strategy.method='full_factorial_grid' 时不再 422 报错,正确归一化兜底。
+**相关文件**:web/frontend/src/views/ProjectList.vue、web/backend/app/services/plan_generator.py、web/backend/app/routers/ai_plan.py
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | py_compile 语法检查 | ✅ | 两个改动 .py 编译通过 |
+| 2 | afmcore.topology.is_supported | ✅ | AFIR=False,SSSR/DRSS/SDSR=True |
+| 3 | afmcore.strategies 归一化 | ✅ | full_factorial_grid 未注册;active_learning→adaptive 已注册 |
+| 4 | convert_ai_plan_to_unified 函数级测试 | ✅ | method=full_factorial_grid→full_factorial + warning;method=lhs 保留 |
+| 5 | 真实端到端 API(AFIR 项目 id=9 generate-and-save) | ✅ | HTTP 200,topology→SSSR、method→full_factorial,warnings 正确 |
+
+### 关键数据 / 发现的问题 / 修复措施
+
+- **关键数据**:端到端返回 topology="SSSR"、search_strategy.method="full_factorial",warnings 含 "Unknown topology 'AFIR' - defaulted to SSSR" 与 "Unknown search strategy method 'full_factorial_grid' - defaulting to full_factorial"。
+- **发现的问题**:AI 偶发输出非标准变量名(Stator_Outer_Diameter_mm、Turns_Per_Coil)触发 warning(不影响生成),属独立问题待后续。
+- **修复措施**:三处修复(前端拓扑选项对齐 + 后端 topology/strategy 归一化兜底),测试产物(临时脚本、测试方案)已清理。
+
+## TEST-026:Kimi max_tokens 撞顶修复 + AI 生成摘要对话框验证
+
+**日期**:2026-09-03
+**测试环境**:Windows,系统 Python 3.12.10(uvicorn 0.52.4 / fastapi 0.141.1),Kimi k3,Vue3 + Element Plus
+**测试目的**:验证 P0-2(Kimi max_tokens 撞顶导致 content 空)与 P0-1(AI 生成摘要对话框)修复。
+**相关文件**:web/backend/app/services/ai_client.py、web/backend/app/services/plan_generator.py、web/frontend/src/views/ProjectDetail.vue
+
+### 背景 / 根因
+
+- plan_generator.py 调用 chat_json 写死 `max_tokens=2000`(.env 的 KIMI_MAX_TOKENS=4096 未生效);k3 推理模型的 reasoning_content 会吃满 2000,导致 content 为空 → JSON 解析失败 → "AI 生成失败"(对应用户"超时"截图的一个隐藏根因,ai_call_logs id=40 实测 completion_tokens=2000、content 空)。
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | py_compile + ASCII 检查(2 个 .py) | ✅ | 编译通过、纯 ASCII |
+| 2 | vue-tsc --noEmit 类型检查 | ✅ | 0 错误 |
+| 3 | 端到端 generate-and-save(SSSR 项目 id=1) | ✅ | HTTP 200,topology=SSSR,warnings 完整返回 |
+| 4 | max_tokens 生效核对(最新 ai_call_log id=45) | ✅ | completion_tokens=650(未撞顶)、content 完整 JSON、duration 18.5s、status=success |
+
+### 修复内容
+
+- ai_client.py:chat() 未传 max_tokens 时默认用 KIMI_MAX_TOKENS;新增 finish_reason=length 且 content 空时的 logger.warning。
+- plan_generator.py:两处 max_tokens=2000 → KIMI_MAX_TOKENS。
+- ProjectDetail.vue:AI 生成成功后改为"方案生成摘要"对话框(方案概要 + 系统调整 warnings 逐条 + AI 设计思路折叠 + 查看方案/留在本页),替代原只显示第一条的 ElMessage;loading overlay 增加已等待秒数(AbortController 取消本已存在)。
+
+### 关键数据 / 遗留
+
+- **关键数据**:端到端 warnings=["Slot_Depth 非标准变量", "Removed Slot_Depth (点数裁剪)", "full_factorial_grid→full_factorial"],验证摘要对话框数据链完整。
+- **遗留**:AI 仍偶发输出非标准变量名(Slot_Depth 等)被裁剪,属独立问题;P0-3(仿真前检查清单)/ P0-4(TaskManager 方案下拉)/ P1(参数目录)未做。
+
+## TEST-027:仿真前检查清单 + TaskManager 方案自动展开验证
+
+**日期**:2026-09-03
+**测试环境**:Windows,系统 Python 3.12.10(uvicorn 0.52.4 / fastapi 0.141.1),Vue3 + Element Plus
+**测试目的**:验证 P0-3(PlanDetail 仿真前检查清单)与 P0-4(TaskManager 创建任务从方案自动展开参数)。
+**相关文件**:web/backend/app/routers/plans.py、web/backend/app/routers/tasks.py、web/frontend/src/views/PlanDetail.vue、web/frontend/src/views/TaskManager.vue
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | py_compile + ASCII(plans.py/tasks.py) | ✅ | 编译通过、纯 ASCII(preflight 只返回 key/status,中文 label 在前端) |
+| 2 | vue-tsc --noEmit | ✅ | 0 错误 |
+| 3 | preflight API(plan 1)首次 | ⚠️→✅ | 初测 model_path fail(相对路径 `models/xx.mot` 相对后端 cwd 解析失败);修复为用 PROJECT_ROOT 解析相对路径后 pass |
+| 4 | preflight API 复测 | ✅ | ok=true;model_path pass、scan_vars=1、point_count=3、executor warn(0 在线) |
+| 5 | tasks 自动展开(POST /api/tasks 仅传 plan_id=1) | ✅ | HTTP 200,total_points=3(1 变量 × 3 值自动展开),status=pending |
+
+### 修复内容
+
+- plans.py:新增 `GET /{plan_id}/preflight`(5 项检查:模型文件/变量名确认/扫描变量/数据点规模/执行器在线;fail 阻断、warn 提示);model_path 相对路径用 PROJECT_ROOT 解析。
+- tasks.py:`POST /api/tasks` 的 plan_data/parameters 改为可选;仅传 plan_id 时自动加载方案并调用 `_expand_plan_to_parameters` 展开,显传仍优先(高级覆盖)。
+- PlanDetail.vue:启动仿真改为先调 preflight 弹检查清单对话框(状态图标 + 中文 label 映射 + fail 禁用启动),确认后才调 start-simulation。
+- TaskManager.vue:doCreate 仅在用户编辑高级 JSON 时才传 plan_data/parameters,否则省略由后端自动展开;onPlanSelect 重置高级 JSON;加"默认从方案自动带出参数"提示。
+
+### 关键数据 / 遗留
+
+- **关键数据**:tasks 自动展开 total_points=3 正确;preflight 5 项状态机正确(executor 离线为 warn 不阻断,任务可排队)。
+- **测试产物**:测试任务 7d55dc4f 已 cancel(任务文件在 output/tasks/,不入库)。
+- **遗留**:P1(参数目录单一事实源 + D6 调用链验证)未做。
+
+## TEST-028:D6 边界条件 key 别名桥修复验证
+
+**日期**:2026-09-03
+**测试环境**:Windows,系统 Python 3.12.10(uvicorn 0.52.4 / fastapi 0.141.1),Kimi k3
+**测试目的**:验证 D6 数据一致性 Bug 修复——ProjectDetail 录入的边界条件 key(current_a/speed_rpm 等)能流入 build_default_fixed_params 的固定参数推断。
+**相关文件**:web/backend/app/services/fixed_params_template.py
+
+### 根因(确认)
+
+- `build_default_fixed_params` 读取 BC key 用 `rated_current_a/rated_speed_rpm/slot_count/dc_link_voltage_v/cooling_method`,而 ProjectDetail 录入 + rule_engine 消费用 `current_a/speed_rpm/slots/voltage_v/cooling_type` → 用户在项目边界表单填的电流/转速/槽数/电压/冷却方式**不流入**固定参数推断(落模板默认值)。
+- 另发现 `Magnet_Temperature`/`Max_Speed` 完全无 BC 推断。
+
+### 修复
+
+- build_default_fixed_params 入口加 BC key 别名桥(canon 优先、别名补填、拷贝不改调用方 dict);补 Magnet_Temperature(magnet_temp_c)/ Max_Speed(max_speed_rpm)推断。
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | py_compile + ASCII | ✅ | 通过 |
+| 2 | 函数级验证(11 断言) | ✅ | 前端 7 key 全部正确流入;旧 rated_ key 向后兼容;调用方 dict 不被修改 |
+| 3 | 端到端(project 9 generate-and-save) | ✅ | HTTP 200;RMSCurrent=20(BC)、Shaft_Speed=3000(BC)、Number_of_Slots=12、DC_Link_Voltage=48、Magnet_Temperature=40、Cooling_Method=Natural、Max_Speed=6000,全部来自 BC 而非模板默认 |
+
+### 关键数据 / 遗留
+
+- **关键数据**:端到端固定参数全部取自项目 BC(current_a=20/speed_rpm=3000/slots=12/voltage_v=48/magnet_temp_c=40/cooling_type=Natural/max_speed_rpm=6000),D6 修复生效。
+- **顺带确认**:拓扑兜底(AFIR→SSSR)、策略兜底(grid_search_with_refinement→full_factorial)持续正常。
+- **遗留**:完整参数目录单一事实源(P1-1,含 PlanDetail BC_TEMPLATE 对齐、rule_engine、参数目录 API、数据迁移)为大重构,本轮未做;AI 偶发非标准变量名(Stator_Outer_Diameter/Turns_Per_Coil)被裁剪,独立问题。
+
+## TEST-029:BC 字段目录单一事实源(P1-1 第一阶段)验证
+
+**日期**:2026-09-03
+**测试环境**:Windows,系统 Python 3.12.10(uvicorn 0.52.4 / fastapi 0.141.1),Kimi k3,Vue3
+**测试目的**:验证 BC 字段目录单一事实源落地——后端目录 API、方案保存 BC、PlanDetail BC 展示从目录渲染。
+**相关文件**:web/backend/app/services/bc_fields.py(新建)、routers/generation.py、routers/ai_plan.py、routers/plans.py、web/frontend/src/views/PlanDetail.vue
+
+### 背景(确认的事实)
+
+- BC key 三套口径漂移(ProjectDetail/rule_engine 用 current_a 系、fixed_params_template 用 rated_ 系、PlanDetail BC_TEMPLATE 用第三套 rated_)。
+- 所有方案 plan.boundary_conditions 均为 None → PlanDetail"边界条件"展示区模板错位 + 数据源为空,双重失效(死功能)。
+
+### 修复内容
+
+- 新建 bc_fields.py:BC_FIELD_CATALOG(25 字段,规范 key 对齐录入端/rule_engine,含 \uXXXX label/unit/category/aliases)+ normalize_bc(别名→规范,规范优先,不改调用方)。
+- generation.py:新增 `GET /api/bc-fields` 暴露目录。
+- ai_plan.py(AI 生成)+ plans.py(手动创建):把归一化后的 project BC 存入 plan_data.boundary_conditions。
+- PlanDetail.vue:删除硬编码 BC_TEMPLATE,改为从 /api/bc-fields 目录渲染。
+
+### 踩坑与解决
+
+- Write 工具把 `\uXXXX` 转义直接落成实际中文字符 → 文件非 ASCII 且 docstring 字面 `\uXXXX` 触发 truncated escape。解决:先写中文,再用 `encode('ascii','backslashreplace')` 转换脚本统一转 \uXXXX;docstring 避免字面 `\uXXXX`。
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | py_compile + ASCII(5 个 .py) | ✅ | 通过、纯 ASCII |
+| 2 | normalize_bc 功能 | ✅ | rated_current_a→current_a(规范优先)、slot_count→slots、cooling_method→cooling_type、unknown 透传 |
+| 3 | vue-tsc --noEmit | ✅ | 0 错误 |
+| 4 | /api/bc-fields | ✅ | 25 字段、label 中文正确、aliases 正确 |
+| 5 | 端到端(project 9 generate-and-save) | ✅ | plan.boundary_conditions 完整保存 25 个规范 key 的 BC |
+
+### 遗留
+
+- P1-1 第二阶段未做:前端 ProjectDetail bcFields 从目录渲染(表单)、rule_engine/fixed_params_template key 彻底统一、已存数据迁移、scan 变量目录与 BC 目录合并为统一参数目录。
+- AI 偶发非标准变量名被裁剪,独立问题。
+
+## TEST-030:BC key 读取统一走 normalize_bc(P1-1 第二阶段)验证
+
+**日期**:2026-09-03
+**测试环境**:Windows,系统 Python 3.12.10(uvicorn 0.52.4 / fastapi 0.141.1),Kimi k3
+**测试目的**:消除固定参数推断的双重转换——build_default_fixed_params 与 rule_engine 统一走 bc_fields.normalize_bc 单一事实源,删除本地 rated_ 别名桥。
+**相关文件**:web/backend/app/services/fixed_params_template.py、rule_engine.py、bc_fields.py
+
+### 背景
+
+第一阶段后存在双重转换:normalize_bc 输出 current_ 系,build_default_fixed_params 仍用本地 _ALIASES 桥把 current_ 再转 rated_ 读取。本阶段统一为:BC → normalize_bc(唯一别名处理点)→ 各消费端直接读规范 key。
+
+### 修复内容
+
+- fixed_params_template.build_default_fixed_params:改为 `bc = normalize_bc(...)`,删除本地 _ALIASES 桥,elif 改读规范 key(current_a/speed_rpm/slots/voltage_v/cooling_type)。
+- rule_engine.BoundaryConditions.from_dict:开头加 normalize_bc,旧 rated_ 数据正确解析。
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | py_compile + ASCII(3 个 .py) | ✅ | 通过 |
+| 2 | 统一性验证(14 断言) | ✅ | current_ 规范 key 流入;rated_ 旧 key 经 normalize_bc 兼容;rule_engine 解析 rated_;不改调用方 dict |
+| 3 | 后端运行时 import | ✅ | fixed_params_template→bc_fields、rule_engine→bc_fields 无循环,health OK |
+| 4 | 端到端(project 9 generate-and-save) | ✅ | 固定参数推断仍全对(RMSCurrent=20/Shaft_Speed=3000/Slots=12/DC=48/MagnetTemp=40/Cooling=Natural/MaxSpeed=6000) |
+
+### 遗留
+
+- 已存 DB 数据的 rated_ key(project 3/8)由 normalize_bc 读取时兼容,不做物理迁移(避免改用户数据)。
+- scan 变量目录(rule_engine.SCAN_PARAMETERS)与 BC 目录(bc_fields)为不同维度(Motor-CAD 变量 vs 工程边界概念),保持分离不合并。
+- 前端 ProjectDetail 表单 key 已与目录一致(设计目录时即对齐 bcFields),无需改动。
+
+## TEST-031:仿真耗时校准 + AFIR 非标准拓扑标记(P2-1/P2-3)验证
+
+**日期**:2026-09-03
+**测试环境**:Windows,系统 Python 3.12.10(uvicorn 0.52.4 / fastapi 0.141.1),Vue3
+**测试目的**:验证 P2-1 耗时校准(实测 solve_time_s 替代硬编码)与 P2-3 AFIR 非标准拓扑前端标记。
+**相关文件**:web/backend/app/routers/analytics.py、web/frontend/src/views/PlanDetail.vue、web/frontend/src/views/ProjectList.vue
+
+### 数据基础
+
+- simulation_results.solve_time_s:11 条有效实测,avg=125.9s(≈2.1 分钟/点)、min=118s、max=141.4s、median=121s。证实硬编码 2.5 分钟/点高估约 19% 且不能反映实测。
+
+### 修复内容
+
+- analytics.py:新增 `GET /api/analytics/solve-time-stats`(avg/min/max/median/count)。
+- PlanDetail.vue:perPointMin 用实测 avg_s(fallback 2.5 分钟/点),estimatedTimeMin/estimatedRemaining 改用它;"预计耗时"卡片下加依据("基于最近 N 次实测 ~Xs/点")。
+- ProjectList.vue:非标准拓扑(不在 SSSR/DRSS/SDSR)项目 tag 改 warning 色 + 警告图标 tooltip"非标准拓扑,按 SSSR 处理"(不改 DB,后端已有兜底)。
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | py_compile + ASCII(analytics.py) | ✅ | 通过 |
+| 2 | vue-tsc --noEmit | ✅ | 0 错误 |
+| 3 | /api/analytics/solve-time-stats | ✅ | count=11, avg=125.9, min=118, max=141.4, median=121 |
+| 4 | AFIR 项目识别 | ✅ | id 9/10/11 topology=AFIR 被识别为非标准,前端将标记 |
+
+### 遗留
+
+- P2-2(步骤条交互向导)/ P2-4(结果表列配置)未做,价值中低,留后续。
+- AI 偶发非标准变量名被裁剪,独立问题。
+
+## TEST-032:AI 扫描变量归一化 + 注册表扩充验证
+
+**日期**:2026-09-03
+**测试环境**:Windows,系统 Python 3.12.10(uvicorn 0.52.4 / fastapi 0.141.1),Kimi k3
+**测试目的**:解决 AI 推荐变量被误判"非标准"导致扫描维度被裁剪/警告的问题——扩充 SCAN_PARAMETERS 注册表 + 补变量名归一化映射。
+**相关文件**:web/backend/app/services/rule_engine.py、plan_generator.py
+
+### 调查(基于 ai_call_logs 证据)
+
+AI 高频推荐但非标准的变量:Turns_per_Coil(匝数,9 次)、Stator_Outer_Diameter(定子外径,9 次)、Slot_Depth(3 次)、Stator_Inner_Diameter、Wire_Diameter。均为 fixed_params_template 里的有效 Motor-CAD 变量,仅因不在 SCAN_PARAMETERS(原 8 个)被误判。
+
+### 修复内容
+
+- rule_engine.SCAN_PARAMETERS 8→13:新增 Stator_Outer_Diameter/Stator_Inner_Diameter/Slot_Depth/Turns_per_Coil/Wire_Diameter,范围锚定 MARS 基线(198/122/7/20/1.63)±20-50%。
+- plan_generator._VARIABLE_NAME_MAP 补 25 个 AI 变体名映射(Turns_Per_Coil/coil_turns/Stator_Outer_Dia/Stator_OD/Stator_Lam_Outer_Dia/Stator_Slot_Depth 等)。
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | py_compile + ASCII | ✅ | 通过 |
+| 2 | 归一化验证(16 断言) | ✅ | 12 个变体名归一化到注册名;convert 不再产生"not a standard"warning;变量被保留 |
+| 3 | 注册表扩充 | ✅ | 8→13,5 个新变量范围正确 |
+| 4 | 端到端(project 9) | ✅ | "非标准变量"warning 消失;Stator_Outer_Diameter 保留为扫描维度(values=5) |
+
+### 关键数据 / 遗留
+
+- **关键改善**:修复前 Stator_Outer_Diameter/Turns_per_Coil 被标"非标准"并裁剪;修复后成为标准变量被保留。
+- **遗留(设计决策,非缺陷)**:4 个变量且点数超 MAX_POINTS=200 时仍保守裁剪(本次 Turns_per_Coil 因 240 点超限被裁)。若需保留更多维度,应改为点数超限时切换 lhs/adaptive 采样而非砍变量——属另一独立优化,未实施。
+
+## TEST-033:结果表列配置(P2-4)验证
+
+**日期**:2026-09-03
+**测试环境**:Windows,Vue3 + Element Plus 2.4
+**测试目的**:验证结果表列配置——默认只显示核心指标 + 扫描变量,35 项指标按需勾选显示(解决指标扩展后结果表全部铺开拥挤的问题 D14)。
+**相关文件**:web/frontend/src/views/PlanDetail.vue
+
+### 修复内容
+
+- PlanDetail.vue 结果表新增列设置:defaultColumnKeys(核心 5 列 + 扫描变量列);displayColumns 按 visibleColumnKeys 过滤;新增"列设置"按钮 + checkbox 对话框(全部列按需勾选)+"恢复默认"。表格列从 allResultColumns 改为 displayColumns。
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | vue-tsc --noEmit | ✅ | 0 错误 |
+| 2 | 前后端服务 | ✅ | 前端 5173 HTTP 200,后端 health OK(纯前端改动,Vite 热更新) |
+
+### 遗留
+
+- 全部既定优化项(P0/P1/P2 + D6 + AI 变量归一化)已完成。P2-2(步骤条交互)价值低未做。
+- 可选深化:点数超限切换 lhs/adaptive 采样保留维度;结果表列配置可持久化到 localStorage(当前会话内有效)。
+
+## TEST-034:UI 评审实测修复(P0/P1/P2,截图验证)
+
+**日期**:2026-09-03
+**测试环境**:Windows,Vue3 + Element Plus,系统 Edge headless(CDP 截图,Node 原生 WebSocket)
+**测试目的**:用 ui-ux-pro-max 技能 + 实截图评审界面,修复发现的问题并截图回归验证。
+**方法**:agent-browser 因 googleapis 下载 Chromium 超时,改用系统 Edge headless + 自写 CDP 截图脚本(output/_cdp_shot.mjs)实测 5 个页面。
+
+### 评审发现的问题与修复
+
+| 项 | 问题(截图证据) | 修复 | 级别 |
+|---|---|---|---|
+| P0-1 | PlanDetail 概览"扫描变量数/预计数据点/预计耗时"全 0(读 plan.scan_variables 不存在,实为 plan_data.variables) | scanVariables 及增删改 4 处改读 plan_data.variables | Bug |
+| P0-1b | calcPoints 只认 min/max/step,plan 1 variables 用 values 数组 → 预计数据点 0 | calcPoints 优先 values.length,兼容 min/max/step、start/stop | Bug |
+| P1-1 | Dashboard 已选方案却提示"请选择方案";默认选第一个方案(常无结果);结果加载漏 .items 字段({total,items})致有结果显示 0 | 空状态文案区分已选/未选;默认选 result_count>0 方案;results 读取加 .items | Bug+交互 |
+| P1-2 | ProjectDetail 边界条件 25 字段全铺开(22 个"未设置"占位) | 空值默认折叠,只显示已设置 + 展开全部 | 密度 |
+| P1-3 | ProjectList 表格行高大 | 表格 size=small | 密度 |
+| P2-2 | 面包屑 /plans/:id 缺项目名 | MainLayout 加载所属项目名,面包屑补"项目名"层级 | 导航 |
+| P2-4 | ProjectDetail 标题区"返回"与项目名粘连 | 返回按钮独占一行 | 视觉 |
+
+### 截图回归验证(output/v_*.png)
+
+- 方案详情:扫描变量数 0→**1 个**、预计数据点 0→**3 点**、预计耗时 0→**6 分钟**、面包屑补项目名 ✓
+- 项目详情:边界条件从 7 行空字段折叠为 1 行(3 项已设置 + "展开全部(含 22 项未设置)");标题区分隔 ✓
+- Dashboard:数据点总数 0→**6 点**(字段修复);空状态文案"该方案暂无仿真结果" ✓
+- vue-tsc 0 错误。
+
+### 踩坑与沉淀
+
+- **前后端字段名不匹配是反复出现的问题家族**:plan.scan_variables vs plan_data.variables、results 的 .items vs .results/.data、calcPoints 不认 values 数组。建议前端读取一律用兼容多路径的 fallback(`a?.items || a?.results || a?.data`)。
+- **agent-browser 在国内 googleapis 下载 Chromium 超时**:系统 Edge headless(--remote-debugging-port)+ 自写 CDP 截图脚本是可靠的替代方案(Node 22 原生 WebSocket)。
+
+### 遗留
+
+- "Test Airgap Scan"方案 6 结果全 FAILED(数据本身问题),结果明细表无错误信息列——结果表加 error 信息列可作为后续 UX 优化。
+- PlanDetail 方案参数编辑(添加/删除扫描变量)无保存到后端的逻辑(既有功能完整性问题)。
+- P2-1 监控页合并(实时监控+执行器状态)、P2-3 批量清理入口未做(结构/数据改动,待确认)。
+
+## TEST-035:失败结果错误信息列验证
+
+**日期**:2026-09-03
+**测试环境**:Windows,Vue3 + Element Plus,系统 Edge headless(CDP 截图)
+**测试目的**:结果表失败行显示具体错误原因,便于诊断仿真失败(此前 FAILED 状态无原因可查)。
+**相关文件**:web/frontend/src/views/PlanDetail.vue、web/frontend/src/views/Dashboard.vue
+
+### 背景
+
+- /plans/{id}/results 的结果项含 error_message 字段(如 `RuntimeError: MotorCADError writing Outer_Rotor_Diameter: ... Could not find Outer_Rotor_Diameter`),但结果表只显示 FAILED 状态,失败原因不可见。
+- 该错误信息同时实证了 HANDOFF 记录的"5 个固定参数 Motor-CAD 变量名未验证"问题(Outer_Rotor_Diameter 在该模型中不存在)。
+
+### 修复内容
+
+- PlanDetail.vue:结果表 status 列 FAILED 且含 error_message 时加 el-tooltip(悬停显示完整错误,虚线下划线标识可悬停);labelMap 加 error_message→"错误信息"(列设置可勾选显示整列)。
+- Dashboard.vue:resultColumns 加 error_message 列(宽 220,直接显示失败原因);status 列同加 tooltip;两处加 .status-failed-cell 样式。
+
+### 测试步骤与结果
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | vue-tsc --noEmit | ✅ | 0 错误 |
+| 2 | 截图 Dashboard(v_dashboard_err.png) | ✅ | 错误信息列直接显示 "Could not find Outer_Rotor_Diameter" 完整原因 |
+
+### 遗留
+
+- PlanDetail 参数编辑无保存逻辑;P2-1 监控合并 / P2-3 批量清理待确认。
+- Outer_Rotor_Diameter 等未验证变量名导致仿真失败的问题,需按 HANDOFF 待办在 .mot 中确认正确变量名后修正 fixed_params_template(独立于 UI)。
+
+## TEST-036:MARS 变量名实测排查 + fixed_params_template 几何修正
+
+**日期**:2026-09-03
+**测试环境**:Windows,Motor-CAD 2026R1 (v261),pymotorcad 0.8.8,系统 Python 3.12.10,许可证 lmgrd+ansyslmd 在跑
+**测试目的**:排查仿真失败根因(`set_variable: Could not find Outer_Rotor_Diameter`),实测确认 MARS 真实几何变量名并修正模板。
+**相关文件**:web/backend/app/services/fixed_params_template.py、docs/KNOWLEDGE_BASE.md §3.3
+
+### 背景
+
+- UI 错误信息列(TEST-035)实证:仿真点全 FAILED,原因 `Could not find Outer_Rotor_Diameter`。
+- 模板几何变量用径向电机命名,与 MARS(PCB 无铁心轴向磁通)不符。
+
+### 方法(KNOWLEDGE_BASE §8 探测 + 铁律"不要猜变量名")
+
+1. 解析 `.mot`(INI 文本)静态比对模板变量名存在性。
+2. pymotorcad `get_variable` 实测候选名(独立隐藏实例,只读无求解)。
+3. pymotorcad `set_variable` + 回读校验修正后的变量名可写性(铁律"写入回读校验")。
+
+### 实测结论
+
+- **修正(4 个几何)**:`Outer_Rotor_Diameter→RotorOuterDiameter`(130)、`Stator_Outer_Diameter→Stator_Lam_Dia`(76)、`Stator_Inner_Diameter→Stator_Bore`(50)、`Rotor_Back_Iron_Thickness→Back_Iron_Thickness`(5),get/set 均 OK。
+- **设 null(MARS 无此变量,不写入)**:`Inner_Rotor_Diameter`(转子内径,4 候选全 MISS)、`Stator_Yoke_Thickness`(定子轭厚,3 候选全 MISS,PCB 无铁心)。
+- **`Magnet_Arc_[ED]`=121 实测 OK**(原名即对,不改)。
+- **默认值同步**:几何默认值从径向模板值(200/198/122/120/10/8)更正为 MARS 实测值(130/76/50/—/—/5)。
+
+### 验证
+
+| 步骤 | 内容 | 结果 | 详情 |
+|---|---|---|---|
+| 1 | .mot 静态比对 | ✅ | 4 几何名 MISSING,别名映射多数 OK |
+| 2 | get_variable 探测(22s) | ✅ | 22 个真实名 OK;模板 12 个错误名全 MISS;转子内径/轭厚候选全 MISS;Magnet_Arc_[ED]=121 |
+| 3 | set_variable + 回读(15s) | ✅ | 4 个修正名写入回读一致,ALL WRITABLE |
+| 4 | py_compile + ASCII | ✅ | 模板编译通过、纯 ASCII |
+
+### 知识沉淀
+
+- KNOWLEDGE_BASE §3.3 新增"MARS 几何变量名(2026-09-03 实测)",含错误名/正确名对照、无对应变量清单、实测方法。
+- **教训**:固定参数模板的几何命名必须基于实测 .mot/探测,不能套用径向电机模板;`motorcad_var=null` 是"无对应变量"的安全处理(不写入不报错)。
+
+### 遗留
+
+- 模板修正后需重启后端生效;建议重新 AI 生成一个方案并真实启动一次仿真,确认不再报"Could not find variable"(端到端复验)。
+- 转子内径/轭厚若在后续拓扑(DRSS 等)需要,需另行实测对应模型的变量名。
+
+## TEST-037:端到端真实仿真验证(变量名修正终极复验)
+
+**日期**:2026-09-03
+**测试环境**:Windows,Motor-CAD 2026R1 (v261),pymotorcad 0.8.8,许可证在跑
+**测试目的**:模拟执行器完整流程(生成固定参数 → set 全部 → 真实求解 → 提取指标),端到端验证变量名修正后仿真能成功跑通。
+**相关文件**:web/backend/app/services/fixed_params_template.py、docs/KNOWLEDGE_BASE.md §3.3
+
+### 过程与发现(迭代排查)
+
+1. **首次端到端**:37 固定参数 33 可写,set 31 ok / **2 failed**——`Current_Advance_Angle`、`Material_Stator_Lam_Yoke` 也为错误名(几何 4 个已修正 OK)。
+2. **补充排查**:实测 `Current_Advance_Angle→PhaseAdvance`(get/set OK);`Steel_Grade`(Material_Stator_Lam_Yoke)不存在,MARS 无铁心 → 设 null。
+3. **二次端到端**:32 可写 set 全成功(0 failed),但 `do_magnetic_calculation` 失败——`Could not find magnet material NdFeB_N42SH in solids database`(材料**值**错误,非变量名)。
+4. **材料修正**:MARS `.mot` 实测 `Material_Magnet=N42UH`,模板默认 `NdFeB_N42SH` 改 `N42UH`。
+5. **三次端到端**:✅ 全链路成功。
+
+### 最终验证结果
+
+| 步骤 | 内容 | 结果 |
+|---|---|---|
+| set_variable(32 可写参数) | 全部 set | ✅ 32 ok, 0 failed |
+| do_magnetic_calculation | 真实电磁求解 | ✅ 成功(~2 分钟,符合 90-150s) |
+| export_results | 结果导出 | ✅ 337 行 CSV |
+| 关键指标 | 求解结果有效性 | ✅ 转矩脉动 2.815%、AC 损耗 0.47W、功率因数 0.995 |
+
+### 结论
+
+仿真失败根因(变量名 + 材料名)已彻底修复,端到端仿真能成功跑通并产生有效结果。模板所有 motorcad_var 经实测验证(无猜测)。
+
+### 遗留
+
+- 建议用户在 Web 端用本地执行器跑一次完整方案扫描(多参数点),确认生产链路同样通畅(本次为 pymotorcad 单点验证)。
+- 其他拓扑(DRSS)/其他模型的变量名需按同样方法实测。
+
+## TEST-038:生产链路复验(Web 生成 → 任务下发 → 执行器真实仿真 → 结果回传)
+
+**日期**:2026-09-03
+**测试环境**:Windows,Motor-CAD 2026R1 (v261),pymotorcad 0.8.8,FastAPI 后端 + 本地执行器(enable_mock=false)
+**测试目的**:验证完整生产链路(非 pymotorcad 单点)在变量名修正后能成功跑通一次真实仿真。
+**相关文件**:web/backend/app/services/topology_variable_map.py、scripts/task_executor.py、src/afmcore/adapters/motorcad.py
+
+### 过程(链路逐段排障)
+
+1. **执行器上线**:enable_mock=false,heartbeat 正常,idle 等待任务。
+2. **预检拦截**:start-simulation 报 400——`is_known_variable` 用 `_KNOWN_VARIABLES[SSSR]`(含错误几何名),把修正后的正确名误判 unknown。**修复 1**:topology_variable_map 的 `_KNOWN_VARIABLES` 几何名更正为 MARS 实测名(RotorOuterDiameter/Stator_Lam_Dia/Stator_Bore/Back_Iron_Thickness/PhaseAdvance),alias map 的 SSSR/AFIR 目标名同步更正。
+3. **预检通过**:start-simulation 返回 task_id,dispatched。
+4. **执行器不捡任务**:日志 `not claimable (claimed/network)`——start-simulation 已把任务 pending→dispatched,执行器 claim 时再调 dispatch(后端仅允许 pending→dispatched),状态冲突被拒。**修复 2**:task_executor.execute_task 对 status==dispatched 的任务跳过重复 dispatch(仅 pending 才 claim)。
+5. **执行器捡起但点失败**:`No module named 'scripts'`——afmcore.adapters.motorcad 用 `from scripts.robust_motorcad import`,执行器 sys.path 只有 scripts/ 和 src/(无仓库根)。**修复 3**:motorcad._ensure_solver 导入前确保仓库根在 sys.path。
+6. **生产链路成功**:任务 completed,successful_points=1。
+
+### 最终验证结果
+
+| 检查点 | 结果 |
+|---|---|
+| 预检(_KNOWN_VARIABLES) | ✅ 通过(修正后) |
+| 执行器 claim(dispatched 任务) | ✅ 不再 "not claimable" |
+| 适配器加载(scripts 导入) | ✅ 不再 "No module named 'scripts'" |
+| 展开参数正确性 | ✅ RotorOuterDiameter=130/Stator_Lam_Dia=76/Stator_Bore=50/PhaseAdvance/N42UH 等全部正确 |
+| 任务结果 | ✅ completed,successful_points=1,failed_points=0 |
+| 关键指标 | ✅ tavg_nm=0.5219 Nm、efficiency=86.06%、total_losses=41.945 W(与 pymotorcad 单点验证一致) |
+
+### 结论
+
+完整生产链路(Web 生成方案 → start-simulation 预检 → 任务下发 → 本地执行器真实 Motor-CAD 求解 → 结果回传 Web)**全部通畅**,变量名修正后仿真点成功。同时发现并修复了 3 个链路级问题(拓扑预检变量集、执行器 dispatch 状态机、适配器 scripts 导入路径)。
+
+### 遗留
+
+- 多点扫描方案的生产复验(本次为单点最快验证)。
+- 其他拓扑/模型的变量名需按同样方法实测后登记到 topology_variable_map。
+
+## TEST-039:多点扫描生产复验(3 点 Airgap 扫描)
+
+**日期**:2026-09-03
+**测试环境**:Windows,Motor-CAD 2026R1,FastAPI 后端 + 本地执行器(enable_mock=false)
+**测试目的**:多点扫描生产链路最终确认——批量调度、逐点落盘、进度更新、结果趋势物理正确性。
+**方案**:plan 28(Airgap 0.6/1.0/1.5mm,3 点),task c2781acf。
+
+### 验证结果
+
+| 检查点 | 结果 |
+|---|---|
+| 批量调度 | ✅ 执行器逐点跑,进度 33.3%→66.7%→100% |
+| 任务结果 | ✅ completed,successful_points=3,failed_points=0 |
+| 逐点落盘 | ✅ 每点结果独立保存(params + metrics + solve_time_s) |
+
+### 逐点指标(物理趋势验证)
+
+| Airgap | tavg(Nm) | eff(%) | losses(W) | status |
+|---|---|---|---|---|
+| 0.6mm | 0.56625 | 84.926 | 49.917 | OK |
+| 1.0mm | 0.52187 | 86.060 | 41.945 | OK |
+| 1.5mm | 0.46770 | 86.346 | 36.558 | OK |
+
+**趋势符合电磁学**:气隙↑ → 主磁通↓ → 转矩↓(0.566→0.522→0.468);气隙↑ → 铁耗/杂散损耗↓(49.9→41.9→36.6);损耗下降主导 → 效率略升(84.9→86.1→86.3)。结果真实有效,非随机数。
+
+### 结论
+
+多点扫描生产链路完全通畅。单点耗时 ~134s(符合 90-150s 基线),3 点含 Motor-CAD 启动共约 6 分钟。从"仿真失败根因(变量名)"到"多点生产链路全通畅"彻底闭环。
+
+### 遗留
+
+- 建议用户在前端用 AI 生成一个多变量方案并启动,体验完整 AI→仿真→分析闭环。
+- 失败任务历史脏数据(TEST-035 之前的 FAILED)可在经验库/结果分析中对比新成功数据。
+
+---
+
+## TEST-040:遗留 UI 项全部修复(参数编辑持久化 / 监控合并 / 批量清理 / 列配置持久化)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-03 |
+| 测试环境 | Windows / Python 3.12.10 / Node 22.22.2 / FastAPI :8000 / Vite :5173 / Edge headless CDP 截图 |
+| 测试目的 | 落地 UI 评审全部遗留项:PlanDetail 扫描变量编辑持久化、监控页合并、项目/任务批量清理、结果列配置 localStorage 持久化 |
+
+### 测试步骤与结果
+
+1. **扫描变量编辑持久化(PlanDetail)**:添加/删除扫描变量置 dirty 标记,"保存修改"按钮 PUT `/plans/{id}` 完整 plan_data(后端单一事实源校验),保存后重载。✅
+2. **监控页合并(P2-1)**:新建 `ExecutionMonitor.vue`,el-tabs 嵌入原 MonitorDashboard(任务监控)+ ExecutorMonitor(执行器状态),`v-if` 保证仅活动 tab 轮询。路由 `/monitor` 指向合并页,`/executor-monitor` 301 重定向兼容旧链接;导航"执行"组只留"任务管理/执行监控"两个入口。✅
+3. **批量清理(P2-3)**:
+   - 后端:`POST /api/projects/batch-delete`(逐项容错)、`DELETE /api/tasks/{id}` + `POST /api/tasks/batch-delete`(**仅终态可删**,活动任务 400 拒绝并提示先取消)。
+   - 前端:项目列表/任务列表多选列 + 条件渲染批量按钮 + 二次确认(列明项目名);任务行级加"删除"(仅终态显示)。
+   - API 实测:不存在 ID 返回 errors 不中断;真实删除终态任务 204→404;**pending 任务删除被拒 400 "cancel it before deleting",取消后删除 204**。✅
+4. **列配置持久化(P2-4 深化)**:结果列选择存 `localStorage['afm:plan-result-columns']`(全局偏好),加载时读取,恢复默认时清除;空选择拦截"至少保留一列"。✅
+5. **截图回归**:`/monitor`(单入口 + 双 Tab)、`/tasks`(多选列 + 行级删除)、`/projects`(多选列 + AFIR 警告标记 + small 密度)均符合预期。✅
+
+### 验证方式
+
+- `vue-tsc --noEmit` 0 错误;3 个后端 `.py` 编译通过、纯 ASCII。
+- 批量删除 API curl 实测(容错 / 真实删除 / 终态保护)。
+- Edge headless CDP 截图 3 张人工核验。
+
+### 修复过程发现
+
+- 前端两处 script Edit 曾未生效(文件被后续 Edit 改动导致 old_string 失配但界面显示成功),靠 vue-tsc 报错发现并补回——**类型检查再次拦截了静默失效**。
+
+### 遗留
+
+- 无(UI 评审全部项已闭环)。P2-2 步骤条交互向导维持"价值低不做"结论。
+
+---
+
+## TEST-042:用户四需求落地(拓扑默认模型 / 全量 BC 表单+来源标签 / 验收标准 / 参数区展开编辑)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-03 |
+| 测试环境 | Windows / Python 3.12.10 / Node 22.22.2 / FastAPI :8000 / Vite :5173 / Edge headless CDP 截图(支持 js: 表达式点击) |
+| 测试目的 | 用户实测反馈四需求:①拓扑基础模型自动调用(预检 model_path 空阻断启动)②新建项目弹窗全 25 项 BC + 来源标签 ③验收标准按 BC 自动带出 ④BC/固定参数默认全展开 + 可编辑保存 |
+
+### 需求1:拓扑基础模型自动调用
+
+- **后端**:`afmcore/topology.py` 的 `TopologyDefinition` 加 `default_model` 字段(SSSR→MARS .mot,DRSS/SDSR 暂缺)+ `default_model_for()`;三处回退——AI 生成(ai_plan.py)、手动创建(plans.py create_plan)、**启动仿真运行时兜底**(start-simulation 空则回填并持久化,16 个历史空模型方案无需手工修复直接可启动);preflight 模型失败项带 `fixable/default_model`。
+- **前端**:预检对话框模型失败项显示"使用拓扑默认模型"一键修复按钮(PUT + 重跑预检)。
+- **验证**:端到端 project 13(model_path 空)AI 生成 → 自动回填 MARS 模型 + warning 提示。✅
+
+### 需求2:全量 BC 表单 + 来源标签
+
+- **发现并修复隐藏 Bug**:`defaultBC()` 原给全部 25 字段预填默认值 → 用户无法区分"自填/系统默认"。改为全空,仅保存非空字段(cleanBC)。
+- **后端**:bc_fields 目录加表单元数据(type: number/int/enum + options;枚举值为工程标准选项);AI prompt 加 `bc_suggestions` 输出(仅补用户未设置项)+ 扫描变量清单同步扩充至 13 项;方案存 `bc_meta`(user/ai 来源),手动创建全 user。
+- **前端**:新建项目弹窗目录驱动 25 项分组渲染(默认全"未设置");ProjectDetail BC 默认全展开 + 有值标"用户指定";PlanDetail BC 全展开 + 来源标签(用户指定/AI 补充/已修改)+ 编辑保存(改动标 edited)。冷却方式枚举值统一为 Natural/Forced Air/Water/Oil(原小写值与 Motor-CAD 实测值不一致)。
+- **字段路径 Bug 修复**:`plan.boundary_conditions`/`plan.acceptance_criteria` 顶层不存在(实际在 plan_data 下)→ 兼容修正。
+
+### 需求3:验收标准自动带出
+
+- AI acceptance_criteria(hard_constraints)+ BC 目标类字段推导(转矩/效率/损耗/脉动/轴向力/外径/轴向长度 7 类)合并渲染,带"AI 设定/边界条件"来源标签。
+- **修复 AI 输出嵌套 dict 格式丢失**:convert 归一化加 hard_constraints dict→list 转换(`"key <=110"` → `key <=110` 字符串列表)。
+- **截图验证**(plan 31):概览验收标准自动带出 7 条判断条件 + 来源标签。✅
+
+### 需求4:参数区默认展开 + 编辑保存
+
+- BC/固定参数默认全部展开(showAllFixed=true;分类默认展开语义 `!== false`)。
+- 固定参数 inline 编辑(布尔/数值/字符串分控件)+ 保存(PUT plan_data.fixed_params)+ 已修改高亮。
+- **修复扫描变量"最小值"列空缺**:AI 方案 variables 只有 values 数组,列绑 min/max/step 导致空白 → 改为"取值"列直接显示 values 列表(如 0.8/1.15/1.5)。
+
+### 验证方式
+
+- 后端 5 个 `.py` 编译通过、纯 ASCII;vue-tsc 0 错误(初查报 2 个索引类型错误,修 defaultBC 返回类型标注后清零)。
+- 端到端:model_path 回填 + bc_meta 25 项全 user + acceptance_criteria 归一化。
+- Edge CDP 截图 5 张人工核验(概览验收标准/参数页 BC 标签/扫描变量取值/新建项目弹窗/预检修复按钮)。
+- 测试方案已清理。
+
+### 遗留
+
+- DRSS/SDSR 无基础模型文件,选择时无法自动调用(需用户提供 .mot 后登记到 topology registry)。
+- AI 本次未输出 bc_suggestions(项目 BC 已全填 25 项无可补,行为合理);AI 补充来源标签待有未设置项的生成场景验证。
+
+---
+
+## TEST-043:PROMPTS_DIR 路径 Bug 根因修复(AI prompt 从未生效)+ bc_suggestions 实测
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-03 |
+| 测试环境 | Windows / Python 3.12.10 / FastAPI :8000 / Vite :5173 / Kimi k3 |
+| 测试目的 | 验证 AI 补充 BC(bc_suggestions);三次生成均未被补充 → 深挖根因 |
+
+### 根因(重大发现)
+
+- 三次生成 bc_suggestions 均为空。查 `ai_call_logs.prompt_preview` 发现 system prompt 是 `_default_prompt()` 中文兜底版——**真 prompt(generate.txt)从未被加载**。
+- 根因:`web/backend/app/config.py` 中 `BACKEND_DIR = Path(__file__).parent` = `web/backend/app/`,`PROMPTS_DIR = BACKEND_DIR / "prompts"` 指向不存在的 `app/prompts/`(实际在 `web/backend/prompts/`)→ `prompt_path.exists()` 为 False → 静默走 fallback。
+- **影响面(回溯解释多个历史问题)**:AI 变量名编造(OuterDia/Stator_Lam_Length)、acceptance_criteria 嵌套 dict、bc_suggestions 不输出——全部因为模型从未看到含 13 变量清单与设计原则的真 prompt。result_analysis 的 analyze.txt 同样未生效。
+
+### 修复
+
+- `PROMPTS_DIR = BACKEND_DIR.parent / "prompts"`;附带修复:prompt 措辞改强制(REQUIRED FIELD)、`ai_plan.py` 项目上下文 BC 全量展开(未设置字段显式 null,模型才能看到哪些可补)、`SCAN_PARAM_CN` 补 4 个中文名。
+
+### 验证(修复后实测,全部达标)
+
+- 端到端(project:仅 3 项 BC)→ AI 补充 7 项(target_torque_nm=5.7/target_efficiency_pct=92/target_ripple_pct=5/magnet_temp_c=100/cooling_type=Natural/voltage_v=150/max_losses_w=260),bc_meta 标记 `user:3 + ai:7`,用户值未被覆盖。✅
+- 扫描变量全部标准注册名(Airgap/Magnet_Length/Turns_per_Coil),无编造名。✅
+- warnings 干净(仅 model_path 回填提示)。✅
+- Edge CDP 截图:BC 区"用户指定"(绿)/"AI 补充"(蓝)标签渲染正确。✅
+- 测试产物已清理(plans 32-35 + project 14 删除)。
+
+### 遗留
+
+- 历史方案(prompt 修复前生成)的变量名/验收标准质量参差,建议用新 prompt 重新生成关键方案。
+- experience/extract.txt 不存在(experience_enhancer 仍走其自带 fallback,既有状态未变)。
+
+---
+
+## TEST-041:前次会话遗留测试脚本更新与回归(拓扑变量名断言对齐)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-03 |
+| 测试环境 | Windows / Python 3.12.10 |
+| 测试目的 | 前次会话(P5-M2)未跟踪测试脚本 `test_topology_variable_map.py` 断言基于旧错误变量名(Stator_Outer_Diameter 等),在 TEST-036 修正后 6/8 失败;更新断言至 MARS 实测名并全量回归 |
+
+### 测试步骤与结果
+
+1. `test_plan_schema.py`(plan_schema 字符串枚举支持):7/7 PASS,直接入库。✅
+2. `test_topology_variable_map.py` 初跑 2/8——失败断言正是旧错误名,证实 TEST-036 修正改变了行为。✅(预期失败)
+3. 更新断言:`Stator_Outer_Diameter→Stator_Lam_Dia`、`Stator_Inner_Diameter→Stator_Bore`、`Outer_Rotor_Diameter→RotorOuterDiameter`;新增"旧错误名应被判 unknown"反向断言(防回归);docstring 注明修正来源。✅
+4. 更新后全量回归:**8/8 PASS**(含 plan23 80 点回归,RFM→AFM alias 解析到 MARS 实测名)。✅
+5. 两脚本 + `src/plan_schema.py` + `style.css`(P6-M1 设计令牌遗留)ASCII 检查通过并入库。✅
+
+### 踩坑
+
+- Edit 工具对该文件 3 处替换报告成功但实际未生效(old_string 失配未报错),靠 grep 复查 + 重跑测试发现。**教训:Edit 后必须 grep 验证目标字符串已消失/出现**——与"vue-tsc 拦截静默失效"同类问题,测试驱动再次兜底。
+
+### 结论
+
+前次会话(P5-M2/P6-M1)全部遗留改动已验证入库(commit `3dd42b9`);测试断言与 MARS 实测变量名一致,旧错误名有反向防回归断言。
+
+
+---
+
+## TEST-044:新 prompt 质量对比验证 + experience/extract.txt 补建实测
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-03 |
+| 测试环境 | Windows / Python 3.12.10 / FastAPI :8000 / Kimi k3(真 prompt 首次生效) |
+| 测试目的 | ①新 prompt 生成质量对比(变量名/验收标准格式)②补建 experience/extract.txt 并实测 |
+
+### 1. 新 prompt 生成质量(project 13 端到端)
+
+| 维度 | 修复前(兜底 prompt) | 修复后(真 prompt) |
+|---|---|---|
+| 扫描变量名 | 编造名(OuterDia/Stator_Lam_Length)被裁剪 | 全注册标准名(Airgap/Magnet_Length/Magnet_Arc_[ED]) |
+| acceptance_criteria | 嵌套 dict(前端无法渲染) | 规范 list 判断式 4 条(efficiency_pct >= 90 等) |
+| warnings | 多条裁剪/兜底警告 | 仅 model_path 回填提示 |
+| bc_suggestions | 从不输出 | 正常输出(TEST-043 已证 7 项) |
+
+### 2. experience/extract.txt 补建与实测
+
+- 发现 `prompts/experience/` 为空目录,experience_enhancer 一直走单行 fallback;且其 `max_tokens=3000` 有撞顶隐患(同步改为 KIMI_MAX_TOKENS)。
+- 补建正式 extract.txt(design_rules/failure_patterns/parameter_sensitivity/optimal_region/recommendations 结构 + 证据引用要求)。
+- 函数级真实 AI 调用验证(plan 28 三点真实数据,无需执行器):prompt 从文件加载确认;输出结构完整;规则带量化证据(+21% 转矩/-26.7% 损耗)、正确声明样本量仅 3 的保守性、置信度分级;物理趋势与 TEST-039 实测一致。**PASS**。
+- 注:`smart-extract` 端点为规则化提取(不走 AI);extract.txt 生效路径是 adaptive_loop 自适应闭环。
+
+### 遗留
+
+- 历史方案(plan 1-31)系兜底 prompt 时期生成,建议关键方案重新生成。
+- 自适应闭环端到端(含 AI 经验提取)待执行器在线后实测。
+
+---
+
+## TEST-045:自适应闭环集成修复(真 prompt 启用后暴露的断层)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-03 |
+| 测试环境 | Windows / Python 3.12.10 / FastAPI :8000 / Kimi k3(真 prompt) |
+| 测试目的 | 自适应闭环(adaptive_loop)端到端验证——extract.txt 真实生效路径 |
+
+### 发现的集成断层(P3-M5 遗留,真 prompt 启用后暴露)
+
+1. **字段名不匹配**:`generate_plan` 存 convert 后的 `variables`,`initialize_search` 读 `scan_variables` → "No valid scan variables"。修复:generate_plan 归一化补 `scan_variables`,initialize_search 双名兼容。
+2. **数值键名不匹配**:convert 输出 `start/stop/step`,initialize_search 读 `min_value/max_value` → 参数全丢。修复:`lo = var.get("min_value", var.get("start"))` 兼容 + values 数组推导范围。
+3. **闭环方案缺 topology/model_path**:闭环无项目上下文,plan 缺这两项 → 无法仿真。修复:generate_plan 内拓扑归一化 + `default_model_for` 回填。
+4. **L0 参数名口径断层(未修复,记录为待办)**:L0 引擎期望 BC 风格名(`airgap_mm`/`outer_diameter_mm`),闭环传 Motor-CAD 名(`Airgap`/`Magnet_Length`)→ L0 "No checks were performed" 全判不可行 → 初始 LHS 批次为空。需要 Motor-CAD 名 → L0 BC 名的语义映射层(注意 Magnet_Length(轴向) ≠ magnet_thickness_mm(径向厚度),映射不能瞎对应)。
+
+### 验证结果
+
+- 闭环 generate-plan:phase=plan_generated,topology=SSSR,model_path 自动回填 MARS,变量标准名。✅
+- init-search:HTTP 200,search_initialized。✅
+- 转换逻辑函数级:start/stop、values 数组、无效变量跳过 3 断言全 PASS。✅
+- next-batch:返回空(L0 断层所致,已知待办)。
+- update-experience:extract_insights 已在 TEST-044 函数级验证(相同输入结构与真实数据);闭环集成段(all_results 收集→提取→经验条目)**草案待验证**,待 L0 口径修复后实测。
+
+### 遗留
+
+- **L0 参数名口径映射**(Motor-CAD 名 → L0 BC 名)为闭环搜索层的关键待办。
+- 闭环全流程(选点→执行器仿真→report→update-experience)待 L0 修复后端到端实测。
+
+---
+
+## TEST-046:自适应闭环全链路端到端实测(extract.txt 真实生效路径)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-03 |
+| 测试环境 | Windows / Python 3.12.10 / FastAPI :8000 / Kimi k3 / 本地执行器(真实 Motor-CAD) |
+| 测试目的 | 闭环全链路:create → generate-plan → init-search → next-batch → 执行器真实仿真 → report-results → update-experience |
+
+### 修复的问题(本轮累计 4+2 个)
+
+1. **L0 参数名口径**(TEST-045 遗留):L0 引擎期望 BC 风格名,闭环传 Motor-CAD 名 → 在 `src/afmcore/l0/prescreening.py` 加 `MOTORCAD_TO_L0` 语义映射(Airgap→airgap_mm、Magnet_Length→magnet_thickness_mm[轴向磁通磁钢厚度=轴向尺寸]、RMSCurrent→current_a、Magnet_Temperature→magnet_temp_c、Stator_Outer/Inner_Diameter→outer/inner_diameter_mm),evaluate 入口翻译且 L0 原生 key 优先。函数级验证:4 项检查全 PASS + 原生 key 优先。✅
+2. **初始批次不消费**:`select_next_batch` 从不消费 generate_initial_batch 的 pending 点 → 开头先返回 pending 批次。✅
+3. **report-results 路由缺失**:`report_loop_results` 函数无 `@router.post` 装饰器,结果回传端点不可达(P3-M5 遗留)。已补。✅
+4. **export inf 序列化**:`best_objective_value` 初始 inf → JSON 500。导出转 None。✅
+5. **import 哨兵恢复**:export 的 None 导入后覆盖默认 inf → `value > None` TypeError。导入时 None 保持默认哨兵。✅(export/import 检查点机制顺带实测通过)
+
+### 端到端结果(全部真实数据)
+
+- 闭环选点:2 批共 8 点(Airgap 0.6/0.9/1.2 × RMSCurrent 20/30/40/50 组合)
+- 执行器真实仿真:2 个任务 8 点全部 completed(4/4 + 4/4)
+- 结果趋势合理:电流↑→转矩↑损耗↑效率↓(I=20A: 0.51Nm/85.7%;I=50A: 1.21Nm/83.6%)
+- report-results:phase=results_analyzed,置信度 D(点数少属合理评级)
+- **update-experience**:phase=experience_updated,AI 提取 4 条设计规则含量化证据(如"效率峰值在中低转矩点 0.72Nm/86.3% 而非最低转矩点"),经验条目生成。**extract.txt 在闭环真实生效**。✅
+
+### 踩坑
+
+- 任务构造曾把 motorcad_var=None 的参数(Inner_Rotor_Diameter 等)用 name 回退写入 → 执行器报 "Could not find"。修正:只写 motorcad_var 非 None 的参数。
+- next-batch 返回字段是 `points` 不是 `batch`(查询时误读字段名导致误判为空)。
+
+### 遗留
+
+- 闭环只跑到首批两段;主动学习后续批次(信任域)未验证。
+- 测试任务已清理;闭环 loop 为内存态(重启即失,export/import 已验证可恢复)。
+
+---
+
+## TEST-047:submit-batch 生产路径验证(闭环→任务→执行器)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-03 |
+| 测试目的 | 验证闭环 submit-batch(生产路径:闭环自动创建任务给执行器) |
+
+### 发现与修复
+
+- **submit_batch_to_executor 缺固定参数合并**:原实现只传裸扫描点参数({Airgap, RMSCurrent, point_id}),执行器不读 plan_data.fixed_params → 点会缺 CurrentDefinition/MessageDisplayState 等关键固定参数。已修复:每点合并 plan 中 motorcad_var 非 None 的可写固定参数。
+- **验证**:submit-batch 创建任务 f19ba592(12 点 pending 剩余全部);任务文件每点 33 键(30 固定 + 扫描 + point_id),无错误变量名(Inner_Rotor_Diameter/Steel_Grade 已排除)。执行器秒捡起跑。✅
+
+### 发现的已知缺口(未修)
+
+- **执行器→闭环结果自动回传缺失**:执行器跑完只调通用 `/tasks/{id}/results` 上报,不识别 adaptive_batch 类型、不调闭环 `/adaptive/loops/{id}/report-results`。闭环拿不到结果需手动桥接。这是 P3-M5 设计但未实现段,需执行器加 loop 回传逻辑(或 Web 侧轮询桥接)。
+
+### 遗留
+
+- 任务 f19ba592(12 点)在跑,完成后可手动 report 进闭环做主动学习第二轮验证。
+
+---
+
+## TEST-048:闭环生产路径全跑通 + 经验提取数据完整性修复
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-03 |
+| 测试环境 | Windows / FastAPI :8000 / Kimi k3 / 本地执行器(真实 Motor-CAD) |
+| 测试目的 | submit-batch 生产路径全跑通 + 第二轮主动学习 + 经验提取数据完整性 |
+
+### 结果
+
+- **submit-batch 任务 12/12 全部成功**(f19ba592,~24 分钟真实仿真)——生产路径(闭环自动创建任务→执行器捡起跑)验证通过。
+- **12 点 report 入环**:point_id 映射(submit_batch 嵌入的 point_id 直接用,无需猜匹配);置信度 D→C(点数增多合理提升);best 效率 86.295→89.414(LHS 探索到更优点)。
+- **update-experience 第二轮成功**:experience_updated。
+- **AI 反馈暴露数据缺陷**:"No input parameter values are provided"——report_results 的 all_results 只合 metrics 不含输入参数 → 敏感性分析无法做(sensitivity=unknown)。
+
+### 修复(数据完整性)
+
+1. `adaptive_loop.report_results`:all_results 条目合入点的输入 params(从 search.state.points 按 point_id 取)。
+2. `experience_enhancer._condense_results`:输入参数经 `MOTORCAD_TO_L0`(单一事实源)翻译成 BC 名再提取——否则闭环的 Motor-CAD 名输入不会被提取。
+
+函数级验证:Airgap→airgap_mm、RMSCurrent→current_a 翻译提取 PASS。
+
+### 验证边界声明
+
+两个数据完整性修复影响**未来**闭环(当前 loop 的 12 点旧条目已无参数,无法补救);函数级验证通过,端到端效果待下次闭环运行确认。
+
+### 遗留
+
+- 执行器→闭环自动回传缺失(TEST-047 记录,P3-M5 设计未实现段)。
+- 前端 AdaptiveOptimize 走 /api/search/*(独立于 adaptive loops),其契约与 search 服务一致;adaptive loops 无前端页面。
+
+---
+
+## TEST-049:执行器→闭环自动回传(P3-M5 最后缺口闭合)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试目的 | 闭合 TEST-047 发现的"执行器跑完不调闭环 report-results"缺口 |
+
+### 修复
+
+- `scripts/task_executor.py` 加 `_report_to_adaptive_loop`:`execute_task` 完成后,若 `task_type == "adaptive_batch"` 且有 `loop_id`,把每点结果映射为 `{point_id, metrics, status}` POST 到 `/api/adaptive/loops/{loop_id}/report-results`。best-effort(回传失败不影响任务本身的 results 上报)。
+
+### 验证(链路级,真实闭环端点)
+
+- 构造 adaptive_batch 假任务 + 3 点结果 → 调 `_report_to_adaptive_loop` → 闭环 n_results 0→3。✅
+- 负面对照:普通 scan 任务(task_type=scan)不触发回传,n_results 不变。✅
+- 执行器重启加载新逻辑,在线。✅
+
+### 验证边界声明
+
+链路级验证(回传调用 + 闭环接收),用合成 metrics 测连通性;仿真段真实性已由 TEST-046/048 的真实 Motor-CAD 运行证明。全闭环自动流转(submit-batch→执行器→自动回传→下一批)的端到端长时运行未做(需真实仿真数十分钟),建议生产使用中观察。
+
+### 遗留
+
+- 无阻断项。闭环全链路(含自动回传)已可用。
+
+---
+
+## TEST-050:search 服务 L0 验证 + 指标字段名对齐事实源
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试目的 | ①search 服务(前端自适应优化页后端)在 L0 修复后出点验证 ②修复结果表转矩/脉动列空缺 |
+
+### 结果
+
+1. **search 服务出点验证**:`POST /api/search/create`(Airgap 0.6-1.5)→ initial_points 4、pending 4、infeasible 0(L0 修复前全判 infeasible)。前端自适应优化页后端链路健康。✅
+2. **指标字段名对齐事实源**:走查发现方案详情"最新结果摘要"的平均转矩/转矩脉动列显示"-"——前端列定义读 `average_torque_nm`/`torque_ripple_pct`,而指标单一事实源(afmcore/metrics.py)用 `tavg_nm`/`ripple_pct`。修复:PlanDetail/Dashboard 列定义对齐事实源 + 平铺时旧名别名兼容(老数据 average_torque_nm → tavg_nm)。截图验证:转矩/脉动列正常显示(0.566/5.45 等),趋势符合物理。✅
+
+### 说明
+
+- 这是字段名不匹配问题家族的又一实例(前端旧命名 vs afmcore 事实源)。Dashboard 图表仅用 efficiency/losses(未变字段)不受影响。
+- vue-tsc 0 错误。
+
+---
+
+## TEST-051:闭环经验入库(闭环价值闭环)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试目的 | 验证闭环 AI 提取的经验真正沉淀到经验库(可被后续 AI 生成复用) |
+
+### 发现
+
+- `generate_experience_entry` 只返回 dict(注释自称 "ready for database storage"),`update_experience` 只在 HTTP 响应里返回、**从不入库**——重启即失,经验库页看不到,后续 AI 生成(读经验库 existing_experience)无法复用。闭环价值断裂。
+
+### 修复
+
+- `adaptive_loop.update_experience` 加 `_persist_experience_case`:把 AI 洞察映射为 `ExperienceCase` 入库——best feasible point 的 params/metrics + summary+design_rules 作 conclusion + tags(topology/ai-insights/loop)。best-effort(入库失败不中断闭环),返回 experience_case_id。
+
+### 验证(真实 12 点数据)
+
+- 重建闭环 + 灌入 12 点真实仿真结果(f19ba592)+ update-experience → `experience_case_id: 7`,`experience_cases` 表 6→7 行,结论为 AI 洞察文本,tags 含 ai-insights。✅
+- `/api/experience` 列表可见 case 7(经验库页可读)。✅
+
+### 说明
+
+- 至此闭环价值完整闭环:仿真 → AI 提取 → 经验入库 → 后续 AI 生成复用。
+- 验证用例(case 7)基于真实仿真数据,保留入库(有参考价值)。
+
+---
+
+## TEST-052:经验库复用断裂修复(AI 生成不读经验库)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试目的 | 验证"后续 AI 生成复用经验库"的真实性(上次口头声明未验证) |
+
+### 发现(真实断裂)
+
+- `ai_plan.py` 的 generate-and-save(用户实际用的"AI 一键生成")调 `generator.generate()` 时**不传 existing_experience**;`/generate` 端点也只在请求体显式传了才用;前端两个生成入口都不传。→ **经验库的值根本没流入 AI 生成**,闭环提取的经验入库了但生成时不读,价值链在"复用"环节断裂。
+
+### 修复
+
+- 新增 `_load_experience_cases(db, topology)`:按拓扑从 experience_cases 加载最近 5 条(params/metrics/conclusion/tags)。
+- generate-and-save:调用前自动加载同拓扑经验传给 generate()。
+- /generate:请求未显式传经验时自动加载(显式传则优先),并补 db 依赖。
+
+### 验证
+
+- `_load_experience_cases` 直接调用返回 5 案例(含闭环 case 7 的 AI 洞察)。✅
+- 端到端生成成功(HTTP 200)。✅
+- 附带实证:最新 ai_call_log 的 system prompt 开头已是英文真 prompt("You are an axial flux motor...")——PROMPTS_DIR 修复生效的直接证据。
+- 注:prompt_preview 只存前 500 字符(截断在 system prompt),user message 中的经验案例在日志里不可见——日志截断所致,非未加载;功能链路(加载 5 案例 + generate 对非空经验拼入 user message)已确认。
+
+### 说明
+
+- 至此经验价值链完整:仿真 → AI 提取 → 入库(TEST-051)→ 后续生成自动加载复用(本次)。
+
+---
+
+## TEST-053:批次语义修复 + 全自动闭环真实端到端(零手动桥接)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试环境 | Windows / FastAPI :8000 / Kimi k3 / 本地执行器(真实 Motor-CAD) |
+| 测试目的 | 修复批次语义(submit 全部 pending 问题)+ 真实全自动闭环验证 |
+
+### 发现的批次语义问题
+
+- submit-batch 提交全部 24 个 pending 点(point_ids 0-23)而非"当前批次",破坏分批自适应语义。根因:select_next_batch 返回点前 N 个 pending 但不标记,它们仍 pending → submit 拿全部。
+
+### 修复(引入 dispatched 状态)
+
+- select_next_batch:选中点标 status="dispatched"(初始消费段 + 主动学习段)→ 不再被 get_pending_points 重复返回/提交。
+- submit_batch_to_executor:只提交 status=="dispatched" 的当前批次点。
+- batch_summary 状态桶加 dispatched。
+
+### 函数级验证
+
+初始 pending 4 → batch1 选 2 标 dispatched → pending 减到 2(不重复)→ submit 目标正好 2(修复前是全部)。✅
+
+### 真实全自动端到端(零手动桥接,核心成果)
+
+- 建闭环 → AI 生成 → init → next-batch → submit-batch(5 点 dispatched,AI batch_size 覆盖)→ 执行器真实仿真 5/5 → **执行器自动回传**(全程未手动调 report-results)→ 闭环 n_results 0→5 自动增加、phase 自动推进 results_analyzed。✅
+- update-experience:experience_case_id=8 入库;**敏感性分析正常**(Current strong positive、Airgap moderate negative,不再 unknown——TEST-048 数据完整性修复在真实闭环生效);summary 含量化结论(torque 随电流 15A→37.5A 从 0.330→1.006 Nm ~3x)。✅
+
+### 意义
+
+**P3-M5 自适应闭环至此真正全自动**:submit-batch→执行器真实仿真→自动回传→AI 分析→经验入库→后续生成复用,全程无手动桥接。这是本项目"AI 驱动智能仿真闭环"的完整实证。
+
+### 遗留
+
+- 主动学习的多轮信任域收敛未长时观察(点数规模问题,生产使用验证)。
+- 执行器在后端重启期间心跳会中断(需重启执行器恢复)——可考虑执行器心跳自愈(本轮未做,非阻断)。
+
+---
+
+## TEST-054:执行器心跳独立线程(修复任务执行期间误判 offline)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试目的 | 修复执行器架构弱点:心跳与任务执行同线程串行,长跑任务期间心跳停止被误判 offline |
+
+### 问题
+
+- 执行器 `poll_loop` 单线程串行:`_send_heartbeat()` 与 `execute_task()` 同线程。Motor-CAD 单点求解约 2 分钟,跑点期间心跳完全停止 → 后端按 last_heartbeat 超时误判执行器 offline。本轮实测中执行器 30216 出现"进程活着但 offline"假死。
+
+### 修复
+
+- `start_polling` 拆为两个线程:独立 `heartbeat_loop`(每 interval 秒心跳)+ `poll_loop`(取任务执行)。任务执行期间心跳持续。
+
+### 验证
+
+- 编译 + ASCII 通过;重启执行器上线,15s 后持续在线(心跳持续),旧执行器正常超时离线。✅
+
+### 说明
+
+- 假死的完整根因现场已消失(进程活着但日志停在启动)无法完全复现;HTTP 调用均有 timeout(排除无限阻塞);心跳/执行同线程是确定存在的架构弱点,已修。
+
+---
+
+## TEST-055:前端全路由走查(13 路由无错误)+ /generate 回归
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试环境 | Edge headless CDP(截图 + 控制台/异常捕获)|
+| 测试目的 | /generate 端点回归 + 全前端路由健康走查 |
+
+### 结果
+
+1. /generate 回归:HTTP 200,方案正常生成(变量 Airgap、warnings 干净),db 依赖与经验自动加载未破坏该端点。✅
+2. 全路由走查:CDP 脚本捕获 Runtime.consoleAPICalled(error) + Runtime.exceptionThrown,13 个路由全部 PAGE_ERRORS(0)。✅
+3. 抽查非白屏:L0 预筛选 / 多保真度校准 / 高级可视化截图确认正常渲染。✅
+
+### 说明
+
+- 走查工具 _cdp_shot.mjs 扩展了 js: 表达式点击与错误捕获能力,是可复用的 UI 冒烟手段。
+- 所有用户可见页面无控制台错误、无未捕获异常,前端整体健康。
+
+---
+
+## TEST-056:自适应闭环前端页(AdaptiveLoop.vue)
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试目的 | 闭环后端已全自动实测通畅但无 UI 入口,新建前端页面 |
+
+### 实现
+
+- 新建 `views/ai/AdaptiveLoop.vue`:创建闭环表单(需求/总预算/批次大小)+ 闭环列表 + 详情(阶段状态卡、方案摘要、预算进度条、批次历史表、经验提取区)。
+- **一键自动运行**(前端驱动状态机):generate-plan → init-search → 循环(next-batch → submit-batch → 轮询 n_results 等执行器自动回传 → 检查收敛)→ update-experience。利用执行器自动回传(TEST-049),前端只需轮询 n_results。
+- 补 `adaptiveApi.submitBatch`(api/ai.ts 缺失)。
+- 路由 `/ai/adaptive-loop` + 导航"AI 智能"组加入口 + 面包屑映射。
+
+### 验证
+
+- vue-tsc 0 错误;页面渲染 PAGE_ERRORS(0)。
+- 截图:列表页(闭环列表 + 创建表单)+ 详情页(状态卡"经验已更新/5 点/最优 86.12"+ 预算进度 73% + 批次历史 + 经验提取区)均正常。
+- **验证边界**:一键自动运行的各步骤 API 已在 TEST-045~053 单独实测;UI 串接逻辑(状态机 + 轮询)未做真实长时运行(需数十分钟仿真),属"功能已接、待实机长跑确认"。
+
+---
+
+## TEST-057:prompt_preview 截断修复 + 经验进入 prompt 实证
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试目的 | 修复 AI 调用日志截断导致的可观测性缺陷 + 实证经验案例进入 AI prompt(TEST-052 遗留验证缺口) |
+
+### 问题
+
+- `ai_client._log_call` 存 `json.dumps(messages[:3])[:500]`——500 字符截断在 system prompt 开头,user message(含项目 BC + 经验案例)完全不可见。TEST-043/052 两次因此无法从日志确认 AI 实际收到的内容。
+
+### 修复
+
+- prompt_preview:`messages[:3]` → `messages`(全部),`[:500]` → `[:8000]`;response_preview `[:1000]` → `[:2000]`。
+
+### 验证(实证经验进入 prompt)
+
+- 重新生成方案后,最新日志 prompt_preview 长度 8000,user message 含"参考经验案例(5个):[{topology: SSSR, params: {Airgap, RMSCurrent}, metrics: {tavg_nm...}...}]"。✅
+- 经验价值链最后一环实证:闭环提取的经验(TEST-051 入库)在 AI 生成时真实进入 prompt。✅
+
+### 意义
+
+AI 调用的输入完全可观测,后续调试 prompt 行为(变量名、BC 补充、经验复用)可直接查日志,不再黑盒。
+
+---
+
+## TEST-058:闭环 UI 自动运行端到端实测 + axios 超时根因修复
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试目的 | 实测闭环页"一键自动运行"状态机(TEST-056 遗留:UI 串接未实机跑过) |
+
+### 发现的根因
+
+- 首次 UI 自动运行:方案已生成(phase=plan_generated)却跳"运行中断"。
+- 根因:**axios 默认超时 30s < AI 生成方案约 40s**。前端中止报错进 catch,后端继续跑完——状态错位。手动调 init-search 成功证实后端无恙。
+- 对照:aiPlanApi 的 generate/generateAndSave/refine 早已显式设 180s(前人踩过同一坑),新加的 adaptiveApi 漏了。
+
+### 修复(web/frontend/src)
+
+1. `api/ai.ts`:adaptiveApi 的 generatePlan / reportResults / updateExperience 补 `{ timeout: 180000 }`。
+2. `AdaptiveLoop.vue` startAutoRun 改为**断点续跑**:进入时先刷新 phase,init 才生成方案,plan_generated 才初始化搜索,search_initialized 及以后直接进批次循环。任何中断后重点"一键自动运行"即可从断点继续。
+
+### 端到端实测(UI 驱动全闭环)
+
+- 闭环 loop_20260904_022534 从 search_initialized 续跑:批次0(3点)→ 执行器仿真 → 自动回传 → 分析 → 批次1(2点)→ 回传 → 预算耗尽(5/5)→ **自动提取经验**。
+- 结果:5 点全部成功、0 失败、可行 5、最优 86.346 @ Airgap=1.5;UI 详情页正确展示(预算 100%、批次历史两行、经验提取区)。
+- 经验库新增案例 9(SSSR,"本批次包含5个有效仿真点…气隙是主导权衡变量")——经验入库由 UI 自动触发完成。✅
+- 执行器全程 online(心跳独立线程在真实长跑中经受住考验)。✅
+
+### 结论
+
+闭环"一键自动运行"从 UI 点击到经验入库的全链路首次端到端实测通过。TEST-056 的验证边界闭合。
+
+---
+
+## TEST-059:前端生产构建验证 + 补提交共享组件
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试目的 | dev 服务器正常≠生产构建能过;git status 巡检发现共享组件未跟踪 |
+
+### 结果
+
+1. **生产构建**:vite build 22.8s 通过,exit 0。全部页面 chunk 正常产出(含 AdaptiveLoop 10.18 kB)。仅大 chunk 警告(vendor-element 948 kB / vendor-echarts 1.04 MB),属优化项非错误。✅
+2. **补提交**:PageHeader.vue / SectionCard.vue / StatCard.vue 三个共享组件被 5 个已跟踪页面(Dashboard/ProjectList/PlanDetail/TaskManager/AdaptiveLoop)引用却一直未跟踪——克隆即构建失败。已补提交。✅
+
+### 说明
+
+- 大 chunk 警告可通过 manualChunks 拆分优化,未处理(不影响功能)。
+
+---
+
+## TEST-060:Motor-CAD 稳态热仿真首次实测 + 热指标别名登记
+
+| 项目 | 内容 |
+|---|---|
+| 测试日期 | 2026-09-04 |
+| 测试环境 | Motor-CAD 2026R1 (v261),pymotorcad(motorcad2maxwell/.venv),MARS 模型,1055@localhost 许可证可达 |
+| 测试目的 | 首次真实验证 Motor-CAD 热仿真(此前 HANDOFF 标注"热求解待真实验证") |
+
+### 背景
+
+P5-M6 引入的 `enable_thermal` 开关使用了两个**不存在的 API**,从未真正跑通:
+- `mc.do_thermal_calculation()` —— pymotorcad 无此方法(真实为 `do_steady_state_analysis`)
+- `mc.export_results("Thermal", ...)` —— solution_type 无 "Thermal"(真实为 "SteadyState")
+
+### 实测结果(scripts/run_thermal.py)
+
+| 步骤 | 耗时 | 结果 |
+|---|---|---|
+| 电磁计算 do_magnetic_calculation | 127.8s | 正常,损耗作为热源 |
+| 稳态热 do_steady_state_analysis | 6.0s | 正常 |
+| 导出 export_results("SteadyState") | — | 9 个 section 完整 |
+
+**热指标实测值**:
+
+| 指标 | 值 | 说明 |
+|---|---|---|
+| winding_temp_c 绕组平均 | 67.95°C | `T [Winding (A) Average]` |
+| winding_hotspot_temp_c 绕组热点 | 74.59°C | `T[绕组最高]` = EWdg Outer Max |
+| magnet_temp_c 磁钢 active | 118.15°C | 磁钢最高温,热风险首要关注 |
+| stator_temp_c 定子轭 | 55.49°C | `T[定子轭]` |
+| bearing_temp_c 后轴承 | 88.47°C | 前轴承仅 49.19°C,后轴承是热风险点 |
+| temp_rise_c 温升 | -56.88°C | ⚠ 负值,见下 |
+| thermal_resistance_k_w 热阻 | -11.07 K/W | ⚠ 负值,见下 |
+
+### 发现的问题
+
+1. **热边界条件异常**:MARS 模型 `[Miscellaneous]` 中 `Ambient_Temperature=125`(环境 125°C,
+   而辐射环境 `T_Ambient_Radiation=40` 正常),导致温升/热阻为负值。
+   **待用户确认后修正为 25~40°C**。
+
+### 修复内容
+
+1. `scripts/robust_motorcad.py`:`do_thermal_calculation()` → `do_steady_state_analysis()`;
+   `export_results("Thermal")` → `export_results("SteadyState")`。
+2. `src/afmcore/metrics.py`:7 个热指标补充实测字段别名(中英文),bearing_temp_c 映射到后轴承。
+3. 新增 `scripts/run_thermal.py`:独立热仿真验证脚本(可复用)。
+
+### 验证
+
+- `scripts/test_metrics_extension.py` 20 项全部通过(含热指标、enable_thermal 签名)。
+- 热导出文件回放 `extract_all_metrics` 提取 7 项热指标全部命中。
+
+### 结论
+
+热仿真链路首次真实跑通,API 与字段名已核实登记。剩余阻塞:模型环境温度异常待确认。

+ 286 - 0
docs/archive/P1-P4回顾与P5规划.md

@@ -0,0 +1,286 @@
+# PCB 轴向磁通电机自动化仿真系统 — P1~P4 全历程回顾与 P5 规划
+
+> **交接文档**:供新会话接续开发 P5 使用。本文汇总 P1~P4 各阶段的**规划、设计、验证**事实,并给出 P5 规划与开工指引。
+> 生成日期:2026-08-30 | 基线:git HEAD `11630bf`(工作区仅剩用户自建论文目录未入库)
+
+---
+
+## 0. 如何使用本文档
+
+- **新会话开工 P5 前**:先读本文档「§2 系统全景」「§6 已知遗留」「§7 P5 规划」「§8 开工指引」,再按 §8 的必读清单进入项目。
+- **本项目所有设计/计划/记录的权威来源**(按优先级):
+  | 文档 | 内容 |
+  |---|---|
+  | `AGENTS.md` | AI 工具工作说明 + 硬性工程约束 + 项目纪律(**必读**) |
+  | `docs/KNOWLEDGE_BASE.md` | 核心知识库(环境事实/参数语义/探测技术/SOP/已踩坑) |
+  | `PCB轴向磁通电机自动化仿真系统设计方案介绍.md` | 设计方案 V2.0(含附录 B 实现现状对照) |
+  | `docs/PLATFORM_DESIGN_V2.md` | 平台化升级设计(短板 S1~S8 → 批次) |
+  | `docs/P3_IMPLEMENTATION_PLAN.md` / `docs/P4_IMPLEMENTATION_PLAN.md` | P3/P4 实施计划 |
+  | `docs/TEST_RECORDS.md` | 测试记录 TEST-001~015 |
+  | `README.md` | 里程碑、阶段路线图、快速开始 |
+
+- **命名澄清**(历史批次名有重叠,务必区分):
+  - **Phase 1~4**:原设计方案路线图的四个阶段(P1 最小闭环 / P2 Web 方案系统 / P3 AI 闭环 / P4 Web AI 集成+部署),**已全部完成**。
+  - **平台化批次(P3 期 + P4 期)**:以 `src/afmcore` 共享核心层为主线的升级工作。P3 期(commit 用 `p3-m1..m6`)完成策略层/调度/闭环;P4 期(commit 用 `p4-m1..m5`)完成 Schema 统一/文档/EXE/前端收敛曲线/L0 上提。
+  - **P5**:本文 §7 规划的新一轮开发,尚未开工。
+
+---
+
+## 1. 系统全景
+
+### 1.1 一句话定位
+
+> 面向 PCB 定子轴向磁通电机(AFM)的**双系统解耦、可插拔工具、可配置拓扑、可扩展策略**的自动化仿真平台:Web 端生成/优化方案 → 本地 EXE 执行仿真 → 结果入库 → 经验库反哺,AI 全程辅助。
+
+### 1.2 双系统架构(已落地)
+
+```
+系统一(Web 端,联网/AI)                   系统二(本地 EXE,离线可跑)
+┌──────────────────────────┐             ┌──────────────────────────┐
+│ FastAPI + Vue3 + SQLite   │   REST 契约  │ headless 执行器           │
+│ 方案生成(规则+AI+经验库)  │◄───────────►│ (PyInstaller 打包 EXE)     │
+│ adaptive 闭环 / 分析 / 报告│  task_contract│ MotorCADAdapter → Motor-CAD│
+└────────────┬─────────────┘             └──────────────────────────┘
+             │ 共享核心层 src/afmcore/(单一事实源,纯 Python,双端引用)
+             │ metrics(25项) / topology / adapters / strategies / l0 / plan_schema
+```
+
+### 1.3 当前状态快照(2026-08-30)
+
+| 维度 | 状态 |
+|---|---|
+| P1~P4(Phase 1~4 + 平台化批次) | ✅ 全部完成 |
+| 真实 Motor-CAD 全链路 | ✅ 已实测(连接/计算/导出/解析,TEST-002/003/010) |
+| 平台化共享核心层 | ✅ metrics/topology/adapters/strategies/l0/plan_schema 单一权威 |
+| adaptive 闭环(Web 智能层+执行层) | ✅ 打通(P3-M6 + 本轮 P4) |
+| 本地 EXE 打包 | ✅ `dist/PCB-AFM-Executor.exe`(12.6MB,冒烟通过) |
+| 前端全量 build | ✅ build 全绿(P5-M1 清零,vue-tsc 0 错误,2026-08-30) |
+| EXE 配置化(config.json) | ✅ P5-M2 完成(executor_config.json + mock 分支修复,TEST-017) |
+| 真实 EXE 内 Motor-CAD COM 验证 | ✅ 本机单点已验(TEST-018,167.5s,数值与 TEST-010 一致);短扫描/多实例待目标机 |
+
+---
+
+## 2. P1~P4 阶段回顾
+
+### 2.1 Phase 1(最小闭环)— 全部完成
+
+| 里程碑 | 内容 | 设计要点 | 验证结果 | Commit |
+|---|---|---|---|---|
+| M1 | 环境验证 + 单工况仿真脚本 | Motor-CAD 前台可见、参数回读校验 | 连接/计算/导出/解析跑通 | `75347b6` |
+| M2 | 参数扫描引擎(单/多参数) | 每点基线重载、逐点落盘、中英文字段别名 | 气隙扫描 3 点:转矩 0.4677~0.5663Nm、效率 84.93~86.35%,物理趋势符合预期 | `a43d971` |
+| M3 | 方案 JSON 接口 + 本地 PySide6 GUI | 方案 Schema、GUI 加载/监控 | 方案回写 | `cbd9402` |
+| M4 | 经验库雏形 + 反馈闭环 | SQLite 经验库、相似检索 | 反馈闭环 | `f20bef9` |
+
+**验证记录**:`docs/TEST_RECORDS.md` TEST-001(连接探测,发现 export_results API 兼容问题)→ TEST-002(修复后全流程,磁场计算 138.1s/点)→ TEST-003(指标解析 7→14 项,暴露 tavg_nm/ripple 缺口)。
+
+### 2.2 Phase 2(Web 端方案系统)— 全部完成
+
+| 里程碑 | 内容 | 设计要点 | 验证 | Commit |
+|---|---|---|---|---|
+| P2-M1 | Web 基础框架 | FastAPI + Vue3 + SQLite + CRUD API + 双系统 API 客户端 | 基础 API | `1ea7653` |
+| P2-M2 | 边界条件输入 + 方案编辑器 | 规则引擎(参数注册表/范围推荐/方案生成) | 生成方案 | `a95c0db` |
+| P2-M3 | 经验库 Web 端 + 分析仪表盘 | ECharts 趋势/Pareto/敏感性 | 仪表盘 | `9885437` |
+| P2-M4 | 双系统 API 联调 + 知识库管理 | 经验库 CRUD/导入、系统二客户端增强 | 联调 | `25ee19f` |
+| P2-M5 | Phase 2 验收 | 自动化测试 + 前端人工验收 + 文档 | 验收 | `0df69f2` |
+| P2-补丁 | 前端全面中文化 + Dashboard 修复 | — | — | `2660df7` |
+
+### 2.3 Phase 3(AI 驱动智能仿真闭环)— 全部完成
+
+> 基于第三方专家评审意见,仿真策略从"固定批量 DoE"升级为"多保真度 + 可行性优先 + 批量自适应闭环",引入 Kimi k3。
+
+| 里程碑 | 内容 | 设计要点 |
+|---|---|---|
+| P3-M1 | AI 服务层基础架构 | Kimi 客户端(`https://api.kimi.com/coding/v1`,模型 `k3`)+ JSON Schema V2 + 多保真度框架(L0 解析→L1 磁路→L2 2D FEA→L3 3D FEA→L4 瞬态热耦合) |
+| P3-M2 | L0 解析预筛选 + 可行性优先搜索 | 14+ 约束检查(几何/电气/热/制造)+ LHS 初始采样 + 主动学习批量选点 + 局部信任域精修 |
+| P3-M3 | AI 方案生成器 | 自然语言 → 结构化仿真方案(扫描变量/策略/验收准则) |
+| P3-M4 | AI 结果分析师 + 多保真度校准 | 置信等级 A~D(保真度40%+样本密度25%+收敛性25%-异常惩罚10%)+ 六类收敛判据 |
+| P3-M5 | 经验库 AI 增强 + 批量自适应闭环 | 经验提取 + 闭环验收 |
+| P3-M6 | Web 端执行桥 + 路由整合 | AdaptiveLoop 补齐 `submit_batch_to_executor()`,把批次打包为 Task 交本地执行器(`768a883`) |
+
+**P3 能力**:32 个 P3 API 端点;自然语言→AI 方案→L0 筛选→主动搜索→仿真→AI 分析→经验提取→下一批的完整闭环。
+
+**P3 平台化(src/afmcore 第一批~第三批)**:
+- 第一批:`metrics.py`(25 项指标+归一化解析器,修复 tavg_nm/ripple 缺口)+ `adapters/`(SimulationAdapter ABC + 注册表 + MotorCADAdapter)→ TEST-004
+- 第二批:`topology.py`(SSSR 8 组 37 项参数体系,DRSS/SDSR 预留)+ task_executor 切换 `get_adapter` → TEST-005(36 断言)
+- 第三批:`strategies/`(full_factorial/lhs/adaptive)+ Task 模型扩展 + 调度契约统一 + 执行器批次/多实例 → TEST-006~011
+
+**P3 遗留处理(本轮 P4 期初,4 commit)**:
+- 并发原子认领:`dispatch_task` 改 SQLAlchemy 条件 UPDATE(8 线程竞争恰 1 win)→ `fb3a505` + `d525a4b`
+- 断点恢复:`FeasibilityFirstSearch.import_state()` + `AdaptiveLoop.export/restore` + `/loops/{id}/export`、`/loops/import` → `4dcc627`
+- ASCII 纪律:plans.py + fixed_params_template.py 中文串转 `\uXXXX` → `3caecd1`
+- 环境依赖测试:`test_api_client.py` 真实链路 → TEST-013
+
+### 2.4 Phase 4(Web AI 集成 + 双系统闭环 + 批量调度 + 部署)— 全部完成
+
+| 里程碑 | 内容 | 验证要点 |
+|---|---|---|
+| P4-M1 | 前端 AI 功能集成 | 6 个 AI 页面 + API 封装 + 通用组件 |
+| P4-M2 | 双系统任务下发与回传 | Web 创建任务 → 本地执行器轮询领取 → 执行 → 上报 → 回传 → 存储展示 |
+| P4-M3 | 批量调度 + 实时监控 + 增强版 MotorCAD 核心 | 优先级队列(1-10) + 最大并行(2) + 依赖 + 断点续跑;16 项鲁棒性措施 |
+| P4-M4 | 高级可视化 + 自动报告 | Pareto/收敛/雷达/热力图 + Word/JSON 报告 |
+| P4-M5 | 部署打包 | Docker + docker-compose + nginx + Windows 部署脚本 |
+| P4-补丁1 | export_results 兼容(pymotorcad 0.8.8 需 solution_type) | `f3b492a` |
+| P4-补丁2 | 指标别名扩展(12→20)+ 弹窗抑制 | `6a8bc80` |
+
+**真实 Motor-CAD 实测**(MARS-12S10P,5000rpm):效率 86.06%、总损耗 41.945W、磁场计算 138.1s/点(TEST-002/003/010)。
+
+**第三方代码评审修复**(V1.4):20+ P0 项(指标单一事实源 A1 / mock 假数据禁上报 A2 / BatchScheduler 死锁 B1 / 六类收敛判据全接入 C2 / L0 最低覆盖 C3 / 6 页面 994 处中文还原 D1 / .dockerignore 防密钥 E1 / 全仓库状态统一 "OK"/"FAILED" / 75 文件纯 ASCII)。详见 `docs/CODE_REVIEW_RESPONSE.md`。
+
+### 2.5 P4 平台化批次(本轮,commit `p4-m1..m5`)— 全部完成
+
+> 用户要求:"先处理遗留 → 写 P4 计划 → 直接做 P4"。按 `docs/P4_IMPLEMENTATION_PLAN.md` 五件套实施。
+
+| 批次 | 内容 | 设计要点 | 验证 |
+|---|---|---|---|
+| P4-M1 | 方案 Schema 单一权威 | `src/plan_schema.py` 增 parse/validate 入口 + min_value/max_value 别名容错 + require_model_path 分级;web main.py 注入 repo root,plans/ai_plan 接入校验(400/422) | `test_p4_schema.py` 7 组全过 |
+| P4-M2 | 文档 V1.1→V2.0 | 设计方案追加"附录 B 实现现状对照"(蓝图 vs 实现逐项映射) | README 引用同步 |
+| P4-M3 | EXE 打包 | `build_executable.ps1`(PyInstaller onefile,paths=src+root,collect-all ansys.motorcad);执行器加 `--version`/`--self-test` | `dist/PCB-AFM-Executor.exe` 12.6MB 冒烟通过 |
+| P4-M4 | 前端 adaptive 收敛曲线 | search state 增 `points_history`(逐评估点 id/batch/params/objective/feasible)+ AdaptiveOptimize.vue echarts(可行/不可行散点 + 当前最优 step 折线) | `test_p4_m4_convergence.py` 5 组全过;vue-tsc 0 错误 |
+| P4-M5 | L0 上提共享核心 | `L0PreScreeningEngine` 迁 `src/afmcore/l0/prescreening.py`(唯一实现,纯 stdlib),web 薄 re-export 兼容 6 处调用点 | `test_p4_m5_l0.py` 8 组全过;closed_loop/convergence 补 repo-root path 后全绿 |
+
+**P4 批次验证汇总**:P4 acceptance / schema / m4 / m5 / P2(36) / M6 / checkpoint / concurrency / closed_loop 全量回归绿;全量 py_compile 0 失败;ASCII 0 违规。详见 TEST-014/015。
+
+---
+
+## 3. 核心架构与设计决策(平台化主线)
+
+### 3.1 共享核心层 `src/afmcore/`(单一事实源)
+
+| 模块 | 职责 | 扩展方式 |
+|---|---|---|
+| `metrics.py` | 25 项指标定义 + 归一化解析器(唯一权威) | 加一项(key/label/alias/direction)所有消费端自动生效 |
+| `topology.py` | 拓扑注册表(SSSR 8 组 37 项,DRSS/SDSR 预留) | 注册参数体系 + 模板 + 默认规则 |
+| `adapters/` | SimulationAdapter ABC + 注册表 + MotorCADAdapter | 实现 ABC + 注册 tool_name(Maxwell/JMAG 即插) |
+| `strategies/` | full_factorial / lhs / adaptive 三实现 | 注册新策略类 |
+| `l0/prescreening.py` | L0 解析预筛选(14+ 约束,纯 stdlib) | 加约束检查项 |
+| `plan_schema.py` | 方案契约 V2 单一权威 | 经 parse/validate 入口统一校验 |
+
+**分层约束**:共享层不依赖 Motor-CAD/Web/GUI 具体实现;Web 不直接调 Motor-CAD;本地执行器不依赖 AI。
+
+### 3.2 双系统解耦与任务契约
+
+- 状态机:`pending → dispatched → running → completed / failed / cancelled`(`web/backend/app/services/task_contract.py`)
+- 认领:`dispatch_task` 条件 UPDATE + rowcount(并发原子,多执行器恰好一次)
+- 断点:`/loops/{id}/export` + `/loops/import`(进程重启恢复搜索状态)
+- 执行器:`scripts/task_executor.py`(多实例 `--instances N`,adapter 驱动,point_id 回填)
+
+### 3.3 Adaptive 闭环链路(P3-M6 打通,全链路验证过)
+
+```
+AI 方案(Kimi,空 key 降级纯定量)→ L0 预筛选 → 初始 LHS 采样
+  → active learning 选批(search.select_next_batch,含信任域)
+  → submit_batch_to_executor(打包为 adaptive_batch Task:loop_id/batch_id/point_ids)
+  → 本地执行器认领/求解(mock 或真实 Motor-CAD)
+  → report_results 回填(point_id 对齐)→ 分析 → 经验库 → 收敛/预算耗尽 → 结束
+```
+
+### 3.4 关键工程决策(沉淀为纪律)
+
+1. **纯 ASCII 源码**:`.py/.ps1` 全 ASCII,中文 `\uXXXX` 或进 Markdown(`rg -n "[^\x00-\x7F]" --glob '*.py' --glob '*.ps1' .` 检查)
+2. **运行前 Git 提交 + preflight**:启动 Motor-CAD 求解前仓库必须干净
+3. **Motor-CAD 实例管理**:`open_new_instance=True` + `set_visible(True)`;每点 `load_from_file` 基线重载防污染
+4. **参数回读校验**:`set_variable` 后 `get_variable` 回读,`math.isclose` 不一致标记 FAILED
+5. **结果逐点落盘**:CSV flush + fsync,失败点记录继续
+6. **生成物不入库**:`output/ runs/ build/ dist/ *.log *.spec` 一律 .gitignore
+7. **阶段完成必更新 README + 测试必留痕**(TEST_RECORDS.md)
+
+---
+
+## 4. 验证体系
+
+### 4.1 测试脚本清单(`scripts/test_*.py`,均可独立运行,exit 0 = PASS)
+
+| 脚本 | 覆盖 | 关联批次 |
+|---|---|---|
+| `test_platform_registry.py` | 指标/拓扑/适配器注册表 36 断言 | P2 平台化第二批 |
+| `test_p3_unit_edge.py` | 单元边界(正常/边界/异常/空值) | P3 收尾 |
+| `test_p3_orchestrator.py` | AdaptiveOrchestrator 编排 | P3-M2 |
+| `test_p3_m4_contract.py` | 任务契约状态机 | P3-M4 |
+| `test_p3_concurrency.py` | 并发原子认领(8 线程恰 1 win) | P3 遗留 |
+| `test_p3_checkpoint.py` | 断点导出/恢复/续跑 | P3 遗留 |
+| `test_p3_closed_loop.py` | HTTP 全链路闭环(真实 uvicorn 子进程) | P3-M5 |
+| `test_p3_adaptive_execution.py` | Web AdaptiveLoop + 执行桥全闭环 | P3-M6 |
+| `test_p4_acceptance.py` | P4 验收 37 项 | P4 |
+| `test_p4_schema.py` | plan_schema 单测 + web 接入 | P4-M1 |
+| `test_p4_m4_convergence.py` | points_history 收敛数据源 | P4-M4 |
+| `test_p4_m5_l0.py` | L0 上提一致性 | P4-M5 |
+| `test_robust_solver.py` / `test_executor_m3.py` / `test_api_client.py` | 求解/执行器/API 客户端 | 各期 |
+
+> ⚠️ P4-M5 后注意:脚本若 `from app.xxx import` 且经 l0 re-export 触达 `src`,**必须同时把 repo root 加入 sys.path**(`sys.path.insert(0, _ROOT)`)。
+
+### 4.2 真实 Motor-CAD 验证(非 mock,需 license)
+
+| 记录 | 内容 | 关键数据 |
+|---|---|---|
+| TEST-002 | 全流程修复后 | 磁场计算 138.1s/点,弹窗问题解决 |
+| TEST-003 | 指标解析扩展 | 7→14 项;back_emf 7.898→11.15V(字段匹配修正) |
+| TEST-010 | P3-M5 真实烟雾 | 连接→基线加载→求解→21 指标解析,back_emf=11.15V 与历史一致 |
+
+### 4.3 环境依赖(无法自动化,如实标注)
+
+- 真实 Motor-CAD 求解依赖 license server(`ANSYSLMD_LICENSE_FILE=1055@localhost`)
+- EXE 内真实 Motor-CAD COM 连接需目标机验证(打包自检 `--self-test` 用 mock 单点,只验证依赖打包完整)
+- AI 分析依赖 Kimi API key(无 key 自动降级纯定量,已门控)
+
+---
+
+## 5. 已知遗留 / Backlog(P5 输入)
+
+| # | 项 | 现状 | 影响 |
+|---|---|---|---|
+| B1 | 前端全量 build 类型错误 | `vue-tsc` 报 PlanDetail/ProjectDetail/ProjectList 等 axios `.data` 类型错误(未触碰的历史遗留) | `npm run build` 不过;M4 改动文件本身 0 错误 |
+| B2 | 真实 EXE 验收 | EXE 打包冒烟过,但打包内 Motor-CAD COM 连接未在目标机实测 | 系统二交付物未端到端确认 |
+| B3 | adaptive 批次点可视化 | 收敛曲线已上线(P4-M4);批次点状态可视化、L0 预筛选前端视图未做 | 闭环可视化不完整 |
+| B4 | 设计蓝图高级管线 | 蓝图(§6:Morris 筛选/Kriging 代理/NSGA-II 多目标/精确 FEA 验证)未落地,当前为 full_factorial/lhs/adaptive | 全局多目标优化能力缺失 |
+| B5 | 多物理场 L2 | 接口预留,热/结构未接入执行 | 平台覆盖度 |
+| B6 | 多工具适配器 | SimulationAdapter 接口就绪,仅 MotorCAD 实现 | 无法切 Maxwell/JMAG |
+| B7 | Docker 部署回归 | 部署方案文档有,容器化未在本轮重验 | 交付形态 |
+| B8 | 双系统单机一键启动 | 手动分步启动 | 易用性 |
+
+---
+
+## 6. P5 规划
+
+### 6.1 P5 目标
+
+> **平台化深化 + 真实交付闭环**:把已完成的"平台化骨架"打磨成可交付、可扩展、可验证的正式版本——质量地基清零(前端 build)、真实 EXE 验收、可视化补全、蓝图高级能力落地、多工具/多物理场扩展、一键交付。
+
+### 6.2 批次划分(建议顺序,依赖驱动)
+
+| 批次 | 内容 | 目标 | 关键验收 | 依赖 |
+|---|---|---|---|---|
+| **P5-M1** | 前端 build 清零 | 修复既有 vue-tsc 类型错误(axios `.data`、响应类型统一),建立"build 必须绿"基线 | ✅ `npm run build` 全绿(vue-tsc 0 错误,2026-08-30,TEST-016);CI 可加 | B1 |
+| **P5-M2** | 真实 EXE 验收 | 目标机(license 就绪)用 `dist/PCB-AFM-Executor.exe` 跑通单点+短扫描;EXE 配置化(`config.json`:web 地址/model 路径/日志/实例数) | ✅ EXE 配置化完成(executor_config.json + mock 分支修复,TEST-017);✅ 真实 COM 单点端到端本机已验证(TEST-018,tavg/eff 与 TEST-010 一致);短扫描/多实例待目标机 | B2 |
+| **P5-M3** | adaptive 可视化补全 | 批次点状态可视化(每批进度/分布)+ L0 预筛选结果前端视图 + adaptive 循环运行期状态推送(轮询增强) | ✅ 三视图上线(batch_summary+l0_summary+3s轮询,TEST-019);WebSocket 待后续 | P4-M4 |
+| **P5-M4** | 策略层高级管线 | Morris 灵敏度筛选(纯 stdlib,注册为 `morris`)+ 代理模型引导(IDW 纯 stdlib 替代 Kriging,环境无 scipy,注册为 `surrogate_guided`)+ 预算自适应批次大小 | ✅ 2 新策略注册+32 测试+冒烟收敛(TEST-020);Kriging/NSGA-II 待后续(需 scipy) | B4 |
+| **P5-M5** | 多工具适配器 | `MaxwellAdapter`/`JMAGAdapter` mock 实现 + 注册 + 执行器 `tool` 动态 import;真实接入标注环境依赖(需 Maxwell+PyAEDT / JMAG+jmagpy) | ✅ `get_adapter("maxwell"/"jmag")` mock 链路可跑(TEST-021,22测试);真实接入待目标机 | B6 |
+| **P5-M6** | 多物理场 L2 接入 | metrics.py 扩10项(热6+结构4,自动生效) + robust_motorcad enable_thermal 开关 + report_generator 按域分组模板化 | ✅ 热/结构指标入库与报告展示(TEST-022,20测试);真实热求解待模型配置 | B5 |
+| **P5-M7** | 平台化交付 | Docker/docker-compose 回归 + 双系统单机一键启动脚本 + 配置化(config/)+ 部署文档更新 | 一键启动端到端可用 | B7/B8 |
+
+### 6.3 每批验收与风险
+
+| 风险 | 应对 |
+|---|---|
+| 前端 build 既有错误量大 | P5-M1 先做基线快照(错误清单固化),逐文件修复,不追求一次全清 |
+| 真实 Motor-CAD/license 环境不可用 | P5-M2 用 mock + `--self-test` 先验依赖完整性,真实验证记录为环境依赖项 |
+| Kriging/NSGA-II 引入新依赖 | P5-M4 先评估(scikit-learn 是否已在环境),轻量实现优先,避免重依赖 |
+| 多工具/多物理场真实接入不可行 | 接口 + mock 链路先行,真实接入标注环境依赖(与既有 Motor-CAD 同策略) |
+| 批次命名混乱 | 统一以 `p5-mN` commit,README/计划文档同步回填 |
+
+---
+
+## 7. 新会话 P5 开工指引
+
+1. **先读**:`AGENTS.md` → `docs/KNOWLEDGE_BASE.md` → `README.md` → 本文件 §6/§7 → `PCB轴向磁通电机自动化仿真系统设计方案介绍.md`(附录 B)→ `docs/PLATFORM_DESIGN_V2.md`
+2. **确认基线**:`git status` 干净(除用户自建目录)、HEAD = `11630bf`
+3. **跑一遍回归**:`python scripts/test_*.py` 全量(P2 36 / P4 三件套 / P3 闭环类),确认起点绿
+4. **纪律提醒**:
+   - 新 `.py/.ps1` 纯 ASCII;中文进 `.md`
+   - 每批完成 → 更新 README + TEST_RECORDS + 提交(`type(scope): description`)
+   - 真实 Motor-CAD 求解前 git 必须干净(preflight)
+   - 无法实测的必须标注"无法执行此测试,以下为推理/建议"
+5. **从 P5-M1 开始**(质量地基优先),每批验收清单过完再进入下一批。
+
+---
+
+*本文档基于已验证事实整理(git log + README + TEST_RECORDS + 设计方案 V2 + 平台设计 V2),P5 规划为建议路线,可结合新会话调研调整。*

+ 258 - 0
docs/archive/P3-评审响应与更新计划.md

@@ -0,0 +1,258 @@
+# 第三方评审响应与 Phase 3 更新计划
+
+> **文档版本**: V1.0
+> **日期**: 2026-08-27
+> **评审对象**: 《PCB轴向磁通电机自动化仿真系统设计方案介绍》V1.1
+> **评审文档**: `第三方评审/PCB轴向磁通电机Motor-CAD仿真策略评审与实施建议.md`
+
+---
+
+## 一、评审结论与总体响应
+
+### 1.1 评审核心结论
+
+第三方专家评审结论为**"有条件通过"**:系统架构和阶段规划方向正确,但仿真策略需从"固定批量DoE+代理模型全局优化"升级为**"多保真度模型路径 + 可行性优先搜索 + 批量自适应闭环"**。
+
+### 1.2 我们的总体响应
+
+| 评审意见 | 响应 | 落地阶段 |
+|---|---|---|
+| 增加L0解析与规则预筛选 | 采纳,作为所有仿真路径的入口 | P3-M2 |
+| 扫描策略拆分为搜索策略+保真度策略 | 采纳,JSON Schema V2扩展 | P3-M1 |
+| 可行性优先模式(约束贝叶斯/主动学习) | 采纳,作为默认模式 | P3-M2 |
+| Motor-CAD/Maxwell/JMAG分层职责+偏差校准 | 采纳,建立L0-L4保真度分级 | P3-M1/P3-M4 |
+| 六类收敛判据 | 采纳,结果数据模型扩展 | P3-M1 |
+| 结果数据模型扩展(保真度/模型版本/约束裕量/置信等级) | 采纳,Schema V2 | P3-M1 |
+| 三种运行模式(固定/批量自适应/本地闭环) | 采纳,执行引擎升级 | P3-M5 |
+| 并行与缓存机制 | 采纳,参数哈希缓存键 | P3-M5 |
+| PCB等效模型字典 | 采纳,经验库绑定模型版本 | P3-M4 |
+
+---
+
+## 二、P0 必须修改项(已纳入 P3-M1)
+
+### 2.1 JSON Schema V2 扩展字段
+
+在现有 `simulation_plan.json` 基础上增加以下顶层字段:
+
+```json
+{
+  "schema_version": "2.0",
+  "strategy_mode": "fast_feasible",
+  "fidelity_strategy": {
+    "levels": ["L0_analytic", "L1_motorcad_emag", "L2_motorcad_lab_therm", "L3_maxwell_3d"],
+    "upgrade_rule": "top_candidates_only",
+    "max_candidates_for_l3": 3
+  },
+  "search_strategy": {
+    "method": "constrained_bayesian",
+    "initial_samples": 16,
+    "batch_size": 4,
+    "max_solver_calls": 80,
+    "local_trust_region": true
+  },
+  "calibration_policy": {
+    "cross_tool_metrics": ["torque_nm", "efficiency_pct", "axial_force_n"],
+    "tolerance": {"torque_pct": 5, "efficiency_point": 1.0}
+  },
+  "acceptance_criteria": {
+    "hard_constraints": ["torque_nm >= 10", "temperature_c <= 120"],
+    "surrogate_max_uncertainty": 0.05,
+    "robustness_required": true
+  }
+}
+```
+
+### 2.2 结果数据模型扩展
+
+每条结果增加:
+
+| 字段 | 说明 |
+|---|---|
+| `fidelity_level` | L0/L1/L2/L3/L4 |
+| `model_template_version` | Motor-CAD模板、PCB等效模型版本 |
+| `solver_settings_hash` | 网格、周期数、求解配置摘要 |
+| `constraint_margins` | 各硬约束的绝对裕量和百分比裕量 |
+| `surrogate_prediction` | 代理模型预测值、不确定度、实际偏差 |
+| `cross_validation` | Motor-CAD与Maxwell/JMAG同工况偏差 |
+| `confidence_grade` | A/B/C/D置信等级 |
+
+### 2.3 六类收敛判据
+
+| 收敛类型 | 判据 | 输出状态 |
+|---|---|---|
+| 求解器收敛 | 求解完成、结果完整、无致命错误 | SOLVER_PASS/FAIL |
+| 硬约束收敛 | 转矩/温度/电压/电流/尺寸/轴向力均满足 | FEASIBLE/INFEASIBLE |
+| 优化收敛 | 连续若干轮最优改进<阈值,或信任域半径<下限 | CONVERGED/STALLED |
+| 代理模型可信 | 交叉验证误差和候选点不确定度<阈值 | MODEL_TRUSTED/UNCERTAIN |
+| 跨工具一致 | Motor-CAD与Maxwell/JMAG同工况偏差在接受范围 | HF_PASS/FAIL |
+| 鲁棒性收敛 | 制造和材料扰动下仍满足硬约束 | ROBUST/FRAGILE |
+
+---
+
+## 三、多保真度分级(L0-L4)
+
+| 层级 | 模型/工具 | 主要任务 | 可定案 |
+|---|---|---|---|
+| L0 | 解析公式+规则引擎 | 几何/电气/热/制造可行性预筛选 | 否 |
+| L1 | Motor-CAD快速电磁模型 | 主要参数收敛、关键工况筛选、初步性能验证 | 否 |
+| L2 | Motor-CAD Lab/Therm/Mech | 候选方案多物理场复核、效率图和温升初评 | 仅作工程候选 |
+| L3 | Maxwell 3D / JMAG | PCB绕组、3D磁路、端部效应、局部损耗、轴向力校验 | **是,需通过验收阈值** |
+| L4 | 扰动/公差/样机数据 | 制造鲁棒性和模型持续校准 | 用于最终放行 |
+
+**核心原则**:Motor-CAD决定"大方向是否对",Maxwell/JMAG决定"工程结果是否真"。任何只经过Motor-CAD而未经过高保真复核的方案,不应标记为最终可行设计。
+
+---
+
+## 四、三种运行模式
+
+| 模式 | 工作方式 | 适用阶段 |
+|---|---|---|
+| 固定计划模式 | 一次性接收全部仿真点并顺序执行 | Phase 1最小闭环(当前已实现) |
+| 批量自适应模式 | 每轮接收4-8个点,执行后回传,方案系统计算下一轮 | P3算法增强 |
+| 本地闭环模式 | 执行程序内部运行确定性优化器(约束贝叶斯/信任域),根据结果动态选点 | 无人值守或网络受限场景 |
+
+> 本地闭环模式不违背"执行程序无需联网/AI"原则——约束贝叶斯、信任域和NSGA-II属于确定性数值优化算法,可打包在本地EXE中运行,不需要LLM。
+
+---
+
+## 五、Phase 3 更新后的里程碑计划
+
+### P3-M1:基础设施层(AI服务 + Schema V2 + 保真度框架)
+
+**目标**:搭建Kimi AI服务层,完成JSON Schema V2扩展,建立保真度等级和结果数据模型基础。
+
+| 任务 | 说明 |
+|---|---|
+| Kimi AI客户端封装 | `src/ai_client.py` — OpenAI兼容SDK,支持重试/超时/流式,端点`api.kimi.com/coding/v1`,模型`k3` |
+| API Key安全管理 | `.env`文件(已gitignore),环境变量`KIMI_API_KEY`,启动校验 |
+| Prompt模板管理 | `prompts/`目录,Jinja2模板,按功能模块组织 |
+| 调用日志与计费 | `ai_call_logs`表,记录token/耗时/模型/状态 |
+| JSON Schema V2 | 扩展strategy_mode/fidelity_strategy/search_strategy/calibration_policy/acceptance_criteria |
+| 保真度等级枚举 | L0-L4等级定义,结果模型增加fidelity_level/confidence_grade字段 |
+| 六类收敛状态枚举 | SOLVER_PASS/FEASIBLE/CONVERGED/MODEL_TRUSTED/HF_PASS/ROBUST |
+| AI健康检查接口 | `GET /api/ai/health` — 测试Key有效性,返回模型列表 |
+
+**验收**:AI客户端可正常调用k3模型;Schema V2可序列化/反序列化;数据库迁移完成。
+
+---
+
+### P3-M2:L0预筛选引擎 + 可行性优先搜索框架
+
+**目标**:实现解析预筛选和约束贝叶斯/主动学习的算法框架。
+
+| 任务 | 说明 |
+|---|---|
+| L0解析预筛选规则 | 几何约束(内外径/气隙/轴向长度)、电气约束(电流密度/电压/磁负荷)、热约束(温升/冷却)、制造约束(PCB线宽线距/铜厚/公差) |
+| 可行域裁剪 | 根据边界条件和L0规则,裁剪参数搜索空间,输出可行/不可行区域 |
+| 约束贝叶斯优化框架 | 基于scikit-optimize或自定义GP,建模硬约束满足概率,采集函数平衡利用与探索 |
+| 主动学习选点策略 | 每轮推荐4-8个仿真点,优先选择可行概率高且信息增益大的组合 |
+| 局部信任域收敛 | 发现稳定可行区后,在当前最优点附近做小范围精细搜索 |
+| 初始种子生成 | 优先使用相似案例和专家模板,再用少量LHS补足空间覆盖 |
+| 搜索状态管理 | 记录每轮选点、预测值、不确定度、约束概率,支持断点续跑 |
+
+**验收**:对一个SSSR案例,L0预筛选能排除明显不可行区域;贝叶斯框架能输出下一批推荐点。
+
+---
+
+### P3-M3:AI方案生成器(自然语言 → 智能方案)
+
+**目标**:用户用自然语言描述需求,AI结合L0预筛选和搜索策略,自动生成完整仿真方案。
+
+| 任务 | 说明 |
+|---|---|
+| 需求解析Prompt | 自然语言需求 → 结构化参数(拓扑/尺寸/电流/转速/目标/约束) |
+| AI参数推荐 | 结合经验库历史数据,推荐扫描范围、步长、初始值,给出推荐理由 |
+| 方案生成接口 | `POST /api/ai/generate-plan` — 输入项目ID+自然语言,输出完整plan_data(Schema V2) |
+| 前端AI生成入口 | 项目详情页"AI生成方案"按钮,弹窗输入需求,展示结果,支持人工编辑 |
+| 方案校验流水线 | AI生成 → L0预筛选 → 规则引擎校验 → 点数预估 → 保存 |
+| 多轮优化 | 支持"再优化一下"——用户给反馈,AI迭代调整参数 |
+| 方案可解释性 | 每个推荐参数输出来源(经验库/规则/AI推理)和置信度 |
+
+**验收**:输入自然语言需求,能生成通过L0校验的合法方案,点数预估合理。
+
+---
+
+### P3-M4:AI结果分析师 + 多保真度校准 + 置信等级
+
+**目标**:仿真完成后,AI自动分析结果,生成解读报告;建立多保真度偏差校准机制。
+
+| 任务 | 说明 |
+|---|---|
+| 结果摘要生成 | `POST /api/ai/analyze-results` — AI分析所有扫描点,生成文字摘要(最优值/趋势/异常) |
+| 参数敏感性解读 | 结合敏感性分析数据,AI用自然语言解释参数影响 |
+| 优化建议生成 | AI基于结果和经验库,给出下一步优化方向 |
+| 报告导出 | Markdown/HTML格式分析报告,含图表+文字解读,支持下载 |
+| 前端报告页面 | 方案详情页"AI分析报告"Tab,流式输出展示 |
+| 多保真度校准表 | 记录Motor-CAD与Maxwell/JMAG同工况偏差,支持加法/乘法修正系数 |
+| 置信等级评定 | 根据验证深度自动评定A/B/C/D等级,C级以下不可对外承诺 |
+| PCB等效模型字典 | 记录铜厚/线宽/线距/层数/过孔/FR4热属性等等效假设,经验库绑定模型版本 |
+| 校准系数反哺 | 高保真偏差修正系数反哺到代理模型和目标函数,避免搜索方向被带偏 |
+
+**验收**:对已有结果的方案调用分析接口,能生成引用具体数据的合理解读;校准表可记录跨工具偏差。
+
+---
+
+### P3-M5:经验库AI增强 + 批量自适应闭环 + 端到端验收
+
+**目标**:AI增强经验库检索与推理;实现批量自适应闭环;完成P3全流程验收。
+
+| 任务 | 说明 |
+|---|---|
+| 语义检索 | `POST /api/ai/semantic-search` — 自然语言查询→理解意图→检索经验库 |
+| 案例对比分析 | 选择多个案例,AI自动对比参数差异、指标优劣,给出适配建议 |
+| 经验自动沉淀 | 仿真完成后AI自动提取结论,生成经验草稿,人工确认后入库 |
+| 智能问答(RAG) | `POST /api/ai/qa` — 基于经验库内容的问答,引用具体案例作答 |
+| 经验标签自动生成 | AI自动推荐标签(高效率/低脉动/大气隙等),减少人工标注 |
+| 批量自适应通信协议 | 批次ID、增量结果上传、方案版本追踪、下一轮选点请求 |
+| 执行引擎批量模式 | 系统二支持接收批次点、执行后回传、等待下一轮指令 |
+| 参数哈希缓存机制 | 缓存键=参数哈希+模型版本+工况ID+求解器版本+保真度等级,中断不重复计算 |
+| 并行任务池预留 | Motor-CAD多实例并行框架(独立工作目录+模型副本) |
+| 端到端测试 | 自然语言生成方案→L0筛选→保存→(模拟)上传结果→AI分析→经验沉淀 |
+| AI成本监控 | token消耗统计,Prompt优化,单次调用上限 |
+| 文档更新 | README V1.1,AI功能使用说明,Schema V2文档,评审响应文档 |
+| 验收测试 | `test_ai.py` — AI客户端/方案生成/结果分析/语义检索的单元+集成测试 |
+
+**验收**:所有AI功能可用,批量自适应闭环跑通,文档完善,测试通过。
+
+---
+
+## 六、Kimi API 技术规格(已验证)
+
+| 项目 | 值 |
+|---|---|
+| 端点 | `https://api.kimi.com/coding/v1` |
+| 模型 | `k3`(Kimi K3,1M上下文,支持推理) |
+| 备选模型 | `k3-256k` / `kimi-for-coding` / `kimi-for-coding-highspeed` |
+| API Key | `sk-kimi-xxx`(Kimi Code Plan,订阅制,消耗Code Plan配额) |
+| 兼容格式 | OpenAI Chat Completions API |
+| 特殊能力 | 支持思考模式(reasoning_content)、视觉输入、视频输入 |
+| 验证状态 | 2026-08-27 验证通过,正常返回 |
+
+---
+
+## 七、风险与应对(更新版)
+
+| 风险 | 等级 | 控制措施 |
+|---|---|---|
+| PCB近似模型系统性偏差 | 高 | 建立校准系数;关键指标必须高保真复核;偏差反哺代理模型 |
+| 轴向磁通3D效应 | 高 | Motor-CAD只用于筛选;最终候选必须3D FEA |
+| AI生成方案不合法 | 中高 | L0预筛选+规则引擎强制校验,不合法拒绝保存 |
+| API调用成本 | 中 | 默认k3模型,设置token上限,Prompt精简,结果缓存 |
+| API不稳定 | 中 | 超时重试3次(指数退避),失败优雅降级 |
+| 经验库冷启动污染 | 中高 | 所有经验绑定模型版本+保真度+置信等级,专家审核后入库 |
+| 自适应算法不可解释 | 中 | 每个推荐点输出来源/预测值/不确定度/约束概率 |
+| 并行许可和文件冲突 | 中 | 独立工作目录、模型副本、许可检测、任务队列 |
+
+---
+
+## 八、待确认问题(需与专家/用户确认)
+
+1. PCB电机结构:无铁芯PCB定子 / PCB绕组+铁芯 / 混合结构?
+2. Maxwell和JMAG哪个作为第一优先级高保真工具?
+3. 默认业务目标:快速可行 / 完整Pareto / 量产鲁棒设计?
+4. 有几套Motor-CAD/Maxwell/JMAG许可?是否允许多实例并行?
+5. 气隙/PCB线宽线距/铜厚/磁钢Br/装配偏心的制造公差?
+6. 最终验收阈值由哪个专家团队签发?是否有样机数据可用于初始校准?
+
+> 以上问题不阻塞P3-M1~P3-M3开发,可在P3-M4(高保真校准)前确认。

+ 254 - 0
docs/archive/P3_IMPLEMENTATION_PLAN.md

@@ -0,0 +1,254 @@
+# P3 实施计划 — 执行策略接入 + 自适应闭环打通 + 调度统一
+
+| 项 | 内容 |
+|---|---|
+| 文档版本 | V1.0 |
+| 日期 | 2026-08-29 |
+| 状态 | 已实施完成(2026-08-29,M1-M5 全部落地并提交) |
+| 依据 | `docs/PLATFORM_DESIGN_V2.md` 第 4 节(P3 批次)+ 短板 S7/S8 |
+| 前置 | P1(共享核心层 afmcore)✅、P2(拓扑注册表 + 适配器)✅ 已落地 |
+
+---
+
+## 1. 背景与目标
+
+### 1.1 P3 要解决什么
+
+当前系统实际跑通的是 **"全因子扫描"** 一条链路:方案固定参数列表 → 任务下发 → 本地执行器逐点跑 Motor-CAD → 结果回传。而 Phase 3 承诺的 **"AI 自适应优化闭环"(feasibility-first 搜索)在 web 端算法已实现并通过验收,却从未真正驱动真实仿真**。
+
+具体两个平台性短板(对应设计文档 S7 / S8):
+
+| # | 短板 | 现状 | 后果 |
+|---|---|---|---|
+| S7 | **执行策略硬编码全因子** | `FeasibilityFirstSearch` / `AdaptiveLoop` 只存在于 web 端内存;批次选出的点**没有通道下发到本地执行器**,结果也没回填驱动下一批 | 自适应闭环未真通,P3 优化能力是"摆设" |
+| S8 | **调度逻辑分裂** | web 端 `BatchScheduler`(内存优先级队列)与本地 `task_executor`(轮询认领)两套,契约不一致 | 平台化调度难扩展、难并行 |
+
+### 1.2 目标
+
+> 把"单策略(全因子)、单调度(串行轮询)"升级为 **"可插拔执行策略 + 自适应闭环真实跑通 + 多实例并行调度"**。
+
+验收标准(做到什么算完成):
+1. 方案可选择执行策略(full_factorial / adaptive / lhs),策略在共享核心层注册,新增策略只做注册。
+2. adaptive 策略下,本地执行器真实跑 Motor-CAD 的每批结果回填搜索模型,自动选下一批,直至收敛/预算耗尽。
+3. 本地执行器支持多实例并行,任务状态机与 web 端调度契约统一。
+4. 全程可留痕、可断点恢复、可回归测试(延续 TEST-004/005 纪律)。
+
+---
+
+## 2. 现状盘点(已探查,非假设)
+
+### 2.1 已实现(P3 可复用的资产)
+
+| 模块 | 位置 | 能力 | 状态 |
+|---|---|---|---|
+| FeasibilityFirstSearch | `web/backend/app/services/feasibility_search.py` | LHS 初始化 / 主动学习选批 / 信任域 / 收敛判断 / `report_result(point_id, metrics, status)` / 状态导出 | ✅ 已实现+验收 |
+| AdaptiveLoop 编排器 | `web/backend/app/services/adaptive_loop.py` | generate_plan → initialize_search → get_next_batch → report_results → update_experience → check_completion | ✅ 已实现(执行环节占位) |
+| L0 预筛选 | `web/backend/app/services/l0_prescreening.py` | 排除不可行域 | ✅ |
+| AI 结果分析 | `web/backend/app/services/result_analyst.py` | 趋势/异常/置信度分级 | ✅ |
+| 经验库增强 | `web/backend/app/services/experience_enhancer.py` | 自动抽取设计规则入库 | ✅ |
+| 批量调度器 | `web/backend/app/services/batch_scheduler.py` | 优先级队列/并行上限/checkpoint | ✅(web 内存态) |
+| 本地执行器 | `scripts/task_executor.py` | 轮询认领 / 逐点执行(已接 `afmcore` 适配器)/ 进度回传 | ✅(P2 已适配器化) |
+| 任务管理 | `web/backend/app/services/task_manager.py` | 任务 CRUD / 进度 / 结果 / 心跳 | ✅ |
+| 搜索 API | `web/backend/app/routers/search.py` | create / next-batch / report / export / runs | ✅ |
+
+### 2.2 关键缺口(P3 要做的)
+
+| # | 缺口 | 说明 |
+|---|---|---|
+| G1 | **无执行策略抽象** | 方案里的 `search_strategy.method`(full_factorial/lhs/active_learning)只是配置字符串,执行器只认"固定参数列表",没有策略层 |
+| G2 | **批次 ↔ 任务无桥** | `FeasibilityFirstSearch` 选出的 `SearchPoint[]` 与 `TaskManager.create_task(parameters=[...])` 之间没有转换通道;adaptive 批次是"动态追加"语义,现有任务模型是"一次性固定列表" |
+| G3 | **结果无回填驱动** | 执行器把结果回传到 task,但没有任何环节把结果喂回 `search.report_result()` 并触发下一批 `get_next_batch()` |
+| G4 | **执行器单实例串行** | `run_task_executor.py` 启动单个轮询线程,无并行;多实例需要许可证策略 |
+| G5 | **调度契约不一致** | batch_scheduler 的任务字段与 task_executor 认领的字段不同源;状态机语义未统一(queued/dispatched/running/completed 各说各话) |
+
+---
+
+## 3. 目标架构(数据流)
+
+```
+┌──────────────────────── Web 端 ────────────────────────┐
+│  方案(Plan)                                            │
+│   ├─ search_strategy.method ∈ {full_factorial, adaptive, lhs}   │
+│   └─ 执行策略解析 → get_strategy(method)                │
+│                                                        │
+│   [adaptive 协调器 StrategyOrchestrator](新增)         │
+│     generate_plan → initialize_search → get_next_batch  │
+│        │  batch(N points)                              │
+│        ▼                                               │
+│   TaskManager.create_task(type="adaptive_batch",        │
+│        parameters=points, loop_id, batch_id)            │
+│        │                                               │
+│   ┌────▼──── 调度契约(统一状态机 queued/running/…)────┐ │
+│   │  BatchScheduler(可选队列)  ←→  REST /api/tasks      │ │
+│   └──────────────────────────────────────────────────┘ │
+└────────────────────┬────────────────────────────────────┘
+                     │ HTTP 轮询认领
+┌────────────────────▼──────── 本地执行端 ─────────────────┐
+│  TaskExecutor(多实例并行,每实例独立 Motor-CAD)          │
+│   ├─ 认领 → get_adapter(tool).run_point / 批量           │
+│   ├─ 结果回传 /api/tasks/{id}/results                     │
+│   └─ adaptive 任务:本批完成后不结束,等待下一批            │
+└─────────────────────────────────────────────────────────┘
+                     │ 结果
+                     ▼
+   StrategyOrchestrator: report_results → search.report_result()
+        → analysis → update_experience → 收敛? 否 → get_next_batch()
+        → 是 → 闭环完成,方案标记 converged/预算耗尽
+```
+
+关键决策点:
+- **adaptive 循环的"心跳"放哪**:放 web 端 `StrategyOrchestrator`(新增服务),它负责"取批 → 建任务 → 等结果 → 回填 → 下批"。本地执行器保持无状态(只认领、执行、回传),避免执行器内嵌循环逻辑导致断连后状态丢失。
+- **断点恢复**:循环状态落在 web 端(`search.export_state()` 已有 checkpoint 能力),任务级结果落在 task.json(已有)。
+
+---
+
+## 4. 里程碑分解
+
+### P3-M1 执行策略抽象层(共享核心层)
+
+**任务**:
+1. 新建 `src/afmcore/strategies/__init__.py`:`SimulationStrategy` ABC
+   - `select_next(points_to_run) -> list[params]`(生成/选择下一批点)
+   - `report(point_id, metrics, status)`(回填结果)
+   - `next_batch_ready() -> bool` / `is_converged() -> bool` / `state() -> dict`
+   - `kind`("full_factorial" / "adaptive" / "lhs")
+2. 新建 `strategies/full_factorial.py`:把现有 `plan_schema.SimulationPlan.generate_points()`(笛卡尔积)包装为策略
+3. 新建 `strategies/adaptive.py`:**包装 web 端 `FeasibilityFirstSearch`**——这是"算法已在 web、执行在本地"的桥接层。共享核心层不 import web,因此 adaptive 策略持有一个"搜索后端回调"(注入函数),或将该策略实现放在本地侧(见 P3-M2 说明)
+4. 策略注册表:`register_strategy / get_strategy(method)`
+5. `plan_schema.SearchStrategy.method` 增加校验(只允许已注册方法)
+
+**设计取舍(重要)**:`FeasibilityFirstSearch` 目前依赖 `l0_prescreening`(web 服务)。为不破坏现有验收资产,**第一版策略层做"双实现"**:
+- 共享核心层定义协议 + full_factorial/lhs 纯实现(可直接复用)
+- adaptive 通过 **StrategyOrchestrator(web 端)** 实现协议,内部调用现有 `FeasibilityFirstSearch`,本地执行器只认"批次的 points",不感知 adaptive 逻辑
+
+**产出**:`src/afmcore/strategies/`(协议 + 注册表 + full_factorial + lhs)
+**验收**:单测——策略注册/选择/状态;full_factorial 与 `SimulationPlan.generate_points` 结果一致;未注册方法报错。
+
+---
+
+### P3-M2 任务模型扩展 + adaptive 批次桥接
+
+**任务**:
+1. `task_manager.py` 任务模型扩展(Task 增加字段,向后兼容):
+   - `task_type`: `"scan"`(默认,现有一致)| `"adaptive_batch"`
+   - `loop_id` / `batch_id` / `point_ids[]`(adaptive 批次标识)
+   - `dynamic`: `true`(表示任务完成后可能有后续批次,不触发"完成即终态"的误判)
+2. `TaskManager.create_task` 支持 `parameters` 直接传 `SearchPoint[]`(自动转换 `{point_id, params}`)
+3. 新增 `web/backend/app/services/strategy_orchestrator.py`(核心新增):
+   - `start_adaptive(plan, loop_id)` → 初始化搜索 → 取首批 → 建 `adaptive_batch` 任务
+   - `on_batch_completed(task_id, results)` → 结果按 `point_id` 回填 `search.report_result()` → `result_analyst` 分析 → `update_experience` → 判断收敛/预算 → 未收敛则 `get_next_batch()` 建下一批
+   - `get_loop_status(loop_id)` → 供 monitor/前端轮询
+4. `search.py` 路由增强:暴露 `POST /loops/{id}/start`、`POST /loops/{id}/resume`(断点恢复入口)
+
+**设计取舍**:adaptive 循环状态存 web 端(内存 + `export_state()` 落盘可选),执行器无状态。断连恢复:执行器重启后重新认领 `pending/dispatched` 任务,orchestrator 依据任务状态决定是续批还是重建。
+
+**产出**:task 扩展 + `strategy_orchestrator.py` + 路由
+**验收**:单测(fake 执行器)——任务字段向后兼容;orchestrator 完整跑通"建批→完成→回填→下批";断点恢复逻辑(模拟执行器中途退出)。
+
+---
+
+### P3-M3 执行器 adaptive 批次执行模式
+
+**任务**:
+1. `task_executor.py` 支持 `adaptive_batch` 任务:认领后按 `point_ids` 顺序执行,每个 point 走 `get_adapter().run_point()`(P2 已就绪)
+2. 结果回传扩展:`report_results` 携带 `point_id → params → metrics` 映射(现有 `point_index` 之外增加 `point_id`),供 orchestrator 回填
+3. `run_task_executor.py` 支持多实例:`--instance` 参数(多进程各自独立轮询,天然多实例;实例心跳带 `instance_id` 区分)
+4. 执行器对 `dynamic` 任务不误判终态:本批完成 → `completed`(batch 级),循环是否结束由 orchestrator 决定
+
+**产出**:task_executor 扩展 + 多实例入口
+**验收**:fake adapter 集成——一个 `adaptive_batch` 任务 3 点执行、结果带 point_id 回传;双实例并行认领不同任务不重复。
+
+---
+
+### P3-M4 调度契约统一 + 并行控制
+
+**任务**:
+1. 定义**统一任务状态机**(文档 + 常量):
+   `queued → dispatched → running → completed | failed | cancelled`
+   (`task_manager` 为权威,`batch_scheduler` 与其对齐字段名;明确两者关系:batch_scheduler 是可选的上层优先级队列,本地执行器始终走 REST 认领,二者通过同一 `/api/tasks` 契约衔接,不重复实现调度语义)
+2. `batch_scheduler` 字段对齐 `task_manager.Task`(task_id/task_name/priority/status/parameters/plan_data/wait_for),消除两套字段漂移
+3. 并行控制策略文档化:`MAX_PARALLEL_TASKS`(web 队列)+ `NUM_EXECUTORS`(本地实例数)+ **Motor-CAD 许可证约束说明**(浮点许可池决定本地实例上限)
+4. 心跳/监控完善:`executor_heartbeat` 支持 `instance_id`,monitor 展示多实例状态
+
+**产出**:状态机常量 + batch_scheduler 对齐 + 并行配置文档
+**验收**:状态机单测(非法迁移拒绝);batch_scheduler 字段与 Task 契约一致性断言。
+
+---
+
+### P3-M5 端到端闭环验证 + 文档收尾
+
+**任务**:
+1. **集成测试(fake adapter)**:`plan(adaptive) → orchestrator → 批次 → 执行器 → 回填 → 收敛` 全闭环,断言搜索状态收敛、预算扣减正确、经验库有产出
+2. **真实 Motor-CAD 烟雾测试**:单 adaptive 任务(1-2 批)真实跑通(环境允许时;不允许则记录为待办)
+3. 回归测试扩展:`scripts/test_platform_registry.py` 增加策略层 + orchestrator(fake)用例
+4. 文档:README(五期)、TEST_RECORDS(TEST-006)、PLATFORM_DESIGN_V2(P3 批次标记完成)
+5. Git 提交
+
+**验收**:闭环测试 PASS + 文档留痕 + 提交合规。
+
+---
+
+## 5. 工作量与建议顺序
+
+| 里程碑 | 预计工作量 | 依赖 | 建议 |
+|---|---|---|---|
+| P3-M1 策略抽象 | 1-2 天 | P2 适配器 ✅ | 先做,共享核心层独立可测 |
+| P3-M2 任务扩展+orchestrator | 2-3 天 | M1 | 核心难点,优先 |
+| P3-M3 执行器批次+多实例 | 1-2 天 | M2、P2 | 与 M2 可并行一部分 |
+| P3-M4 调度统一 | 1 天 | M3 | 低风险,文档为主 |
+| P3-M5 闭环验证+文档 | 1-2 天 | M2-M4 | 收尾 |
+
+**总计约 6-10 天**(含验证与文档)。建议一次性连续推进 M1→M5,每里程碑提交一次(延续项目纪律)。
+
+---
+
+## 6. 风险与应对
+
+| 风险 | 影响 | 应对 |
+|---|---|---|
+| adaptive 循环"等批"期间执行器空闲 | 利用率低 | 首批用满 batch_size;循环粒度=批次而非单点;或与普通扫描任务混排 |
+| 执行器断连导致循环悬空 | 闭环卡死 | orchestrator 心跳+超时重派;任务级 checkpoint(task.json 已有) |
+| 多实例并行许可证不足 | 启动失败 | 文档化 NUM_EXECUTORS ≤ 可用许可数;启动时预检许可证(robust_motorcad 已有 check_license_server) |
+| FeasibilityFirstSearch 依赖 web 服务(l0) | 策略层耦合 | M1 双实现方案:共享层协议 + web 端 orchestrator 实现;后续再把 l0 上提共享层 |
+| 前端仍是"全因子"展示 | UI 与 adaptive 语义不符 | 本批先做 API/后端闭环,前端 adaptive 视图列 P4 或单独 UI 批次 |
+| 大量结果回填拖慢搜索 | 性能 | report_result 批量接口(一次回填整批,而非逐点) |
+
+---
+
+## 7. 验证矩阵(本计划验收口径)
+
+| 层 | 验证方式 | 通过标准 |
+|---|---|---|
+| 单元 | strategies 单测 | 注册/选择/状态/未注册报错;full_factorial 与 generate_points 一致 |
+| 单元 | 任务模型扩展 | 旧任务无新字段可解析(向后兼容) |
+| 集成 | fake adapter 全闭环 | 收敛/预算/经验库断言全过 |
+| 集成 | 多实例并行 | 双实例不重复认领、心跳区分 instance_id |
+| 真实 | Motor-CAD 烟雾 | 1-2 批真实跑通(环境允许) |
+| 回归 | test_platform_registry.py 扩展 | 原有 36 项 + 新增全 PASS |
+| 纪律 | ASCII/编译/Git | 79+ 文件编译 0 失败、纯 ASCII、提交规范 |
+
+---
+
+## 8. 遗留/后续(不属于本计划范围)
+
+- 前端 adaptive 视图(批次可视化、收敛曲线)→ P4 / 独立 UI 批次
+- 把 L0 预筛选从 web 上提共享核心层(消除 web 依赖)→ P4 重构项
+- 多物理场(热/结构)策略适配 → 路线扩展
+- Maxwell/JMAG 适配器 → 路线扩展
+
+---
+
+*本文档为计划稿,评审通过后按 M1→M5 实施,每里程碑更新 README / TEST_RECORDS / PLATFORM_DESIGN_V2 并提交。*
+---
+
+## 6. 实施记录(2026-08-29 完成)
+
+| 里程碑 | 提交 | 结果 | 备注 |
+|---|---|---|---|
+| M1 策略抽象层 | 2e9adfa | 通过 | strategies/ 抽象+注册表+full_factorial/lhs/adaptive;plan_schema 策略校验 |
+| M2 任务扩展+orchestrator | fa74834 | 通过 | Task 新字段+迁移;AdaptiveOrchestrator 闭环;修复 report_results plan_id bug |
+| M3 执行器批次+多实例 | d65c6fa | 通过 | point_id 回传+认领原子化+并行入口;修复 on_complete 参数个数 bug |
+| M4 调度契约统一 | 719023e | 通过 | task_contract.py 统一状态词+字段对齐 batch_scheduler |
+| M5 闭环验证+真实烟雾 | 2611c28 | 通过 | HTTP 全链路闭环(fake) + 真实 Motor-CAD 烟雾;修复 run_point 忽略 model_path bug |
+
+测试记录:docs/TEST_RECORDS.md TEST-006~010;回归脚本 scripts/test_p3_orchestrator.py / test_executor_m3.py / test_p3_m4_contract.py / test_p3_closed_loop.py + test_platform_registry.py(36 项)。

+ 142 - 0
docs/archive/P4_IMPLEMENTATION_PLAN.md

@@ -0,0 +1,142 @@
+# P4 实施计划 — 平台化第四批
+
+> 状态:**已批准,实施中**
+> 依据:`docs/PLATFORM_DESIGN_V2.md` §4 第四批 / `docs/P3_IMPLEMENTATION_PLAN.md` §8 遗留 / `README.md` P5 平台化表第四批
+> 原则:沿用工程规范(禁止臆测 / 测试完备 / ASCII / 自查清单 / 文档同步 / commit 规范)
+
+---
+
+## 1. 目标
+
+把平台化推进到"可交付、可扩展、文档不漂移"的收口状态:
+
+1. **方案 Schema 单一权威**(核心):消除 `src/plan_schema.py`(dataclass)与 `web/backend/app/schemas/schema_v2.py`(Pydantic)两套并行方案定义的漂移,让 `src/plan_schema.py` 成为唯一结构权威,web 端转为薄兼容层。
+2. **文档同步 V1.1 → V2**:原《设计方案介绍》升级,消除与实现(P1~P3 平台化)的漂移。
+3. **本地 EXE 打包**:PyInstaller 打包本地执行器,产出系统二交付物。
+4. **前端 adaptive 视图**(P3 遗留):批次可视化、收敛曲线,消除"前端仍是全因子展示"的语义错位。
+5. **L0 预筛选上提共享核心层**(P3 遗留):`L0PreScreeningEngine` 从 web 移到 `src/afmcore`,消除 web 依赖。
+
+---
+
+## 2. 现状(已探查确认,2026-08-29)
+
+| 项 | 现状 | 问题 |
+|---|---|---|
+| 方案结构 | `src/plan_schema.py` 393 行 6 个 dataclass(ScanVariable/ScanCase/FixedParam/SearchStrategy/AcceptanceCriteria/SimulationPlan),已接入 afmcore.topology/strategies;**无顶层校验入口** | 与 web schema_v2.py 双份定义,语义漂移 |
+| web 方案模型 | `web/backend/app/schemas/schema_v2.py` Pydantic V2(StrategyMode/FidelityLevel/SearchMethod/…策略子模型) | 独立于 src/plan_schema,AI 输出与存储格式不一致风险 |
+| 数据流 | ai_plan 生成 unified v2 dict(fixed_params + variables with value lists)→ 存 SimulationPlan.plan_data → plans.py download 给本地 EXE | 格式转换点分散(plan_generator/rule_engine/plans.py),缺统一入口 |
+| 文档 | 《PCB轴向磁通电机自动化仿真系统设计方案介绍》= V1.1 | 与 P1~P3 平台化实现漂移 |
+| 打包 | 无 PyInstaller 产物(PyInstaller 6.22.2 已装) | 无系统二交付物 |
+| 前端 | adaptive 闭环只有 API,UI 仍是全因子展示 | 语义错位 |
+| L0 | `web/backend/app/services/l0_prescreening.py` | web 依赖,本地执行器/策略层无法复用 |
+
+---
+
+## 3. 批次表
+
+| 批次 | 内容 | 验证 |
+|---|---|---|
+| P4-M1 | 方案 Schema 统一:`src/plan_schema.py` 补顶层校验/归一化入口(parse/validate);web 端结构权威收敛到 src/plan_schema;schema_v2.py 转薄兼容层(字段语义对齐,不重复定义) | TEST-013:新 test_p4_schema.py(parse/validate 正常+异常+空值、web dict 归一化、schema_v2 兼容层 round-trip、回归现有 plan 用例) |
+| P4-M2 | 文档同步 V1.1→V2:升级《设计方案介绍》,回填平台化架构/接口/算法选择/已知限制 | 文档评审 + 与实现对照清单 |
+| P4-M3 | EXE 打包:PyInstaller 打包 `scripts/task_executor.py` + `robust_motorcad`(执行器,headless 模式优先);spec/产物不入库 | TEST-014:构建脚本可复现,产物启动冒烟(--help / 心跳 mock) |
+| P4-M4 | 前端 adaptive 视图:批次可视化(当前 batch/点状态)+ 收敛曲线(objective vs batch) | 前端组件单测 + 手工验收截图 |
+| P4-M5 | L0 上提共享核心层:`L0PreScreeningEngine` 迁至 `src/afmcore/l0/`,web 改引用 | TEST-015:迁移后导入回归 + 功能等价单测 |
+
+---
+
+## 4. 实施细节
+
+### P4-M1 方案 Schema 统一(核心)
+
+**目标架构**:
+
+```
+src/plan_schema.py          # 唯一权威:dataclass 结构 + 顶层 parse/validate/归一化
+web/.../schemas/schema_v2.py # 薄兼容层:Pydantic 仅做 HTTP 层类型包装,结构语义委托 src/plan_schema
+web/.../services/plan_generator.py / rule_engine.py / routers/plans.py  # 统一走 src.plan_schema 入口
+```
+
+**步骤**:
+1. `src/plan_schema.py` 新增顶层入口:
+   - `parse_plan(data: dict) -> SimulationPlan`:dict → dataclass,缺省字段用默认值(容错 + 归一化)
+   - `validate_plan(plan) -> ValidationReport`:结构合法 + 拓扑参数合法(调 afmcore.topology)+ 策略方法已注册(调 afmcore.strategies)+ 扫描变量范围/步长/values 一致性 + 固定参数类型
+   - `plan_to_dict(plan) -> dict`:统一序列化(兼容现有 `to_dict`)
+2. web 端收敛:
+   - `plan_generator.py`:生成流程末尾统一调用 `parse_plan` 归一化 + 校验,失败返回结构化错误
+   - `routers/plans.py`:POST /plans 创建时用 src.plan_schema 校验 plan_data(替代/补充 Pydantic 校验)
+   - `rule_engine.py`:输出与 src.plan_schema 契约对齐(已声明兼容)
+3. `schema_v2.py` 薄化:保留枚举与 API 模型(HTTP 入参校验),字段与 src.plan_schema 语义对齐;删除重复的结构推导逻辑(如有)
+4. 回归:现有 plan 创建/下载/执行用例全部保持通过
+
+**涉及文件**:`src/plan_schema.py`、`web/backend/app/schemas/schema_v2.py`、`web/backend/app/services/plan_generator.py`、`web/backend/app/routers/plans.py`、`web/backend/app/services/rule_engine.py`(如涉及)、`scripts/test_p4_schema.py`(新)
+
+### P4-M2 文档同步 V1.1 → V2
+
+1. 读 V1.1《设计方案介绍》全文,建立与实现对照清单(架构/接口/算法选择/模块清单)
+2. 升级为 V2:
+   - 架构图:补 `src/afmcore` 共享核心层、双系统解耦(Web 智能层 + 本地执行层)
+   - 接口:补 `/api/adaptive/*`、`/api/executor/*`、task_contract 状态机、plan_schema 单一权威
+   - 算法:补执行策略(full_factorial/lhs/adaptive)、L0 预筛选、断点恢复
+   - 已知限制:EXE 打包状态、前端 adaptive 视图状态、许可证依赖
+3. README 链接指向 V2
+
+**涉及文件**:`PCB轴向磁通电机自动化仿真系统设计方案介绍.md`(V1.1 → V2)、`README.md`
+
+### P4-M3 EXE 打包
+
+1. 确认打包目标:本地执行器 headless 模式(`scripts/task_executor.py` + `scripts/robust_motorcad` + `src/afmcore`)
+2. 编写 `scripts/build_executable.ps1`(构建脚本,ASCII):
+   - PyInstaller `--onefile`(或 `--onedir`,视依赖)打包执行器入口 `scripts/run_task_executor.py`
+   - hidden-imports:pymotorcad 相关、src/afmcore 包
+3. 产物验证:`dist/` 下 EXE 启动冒烟(`--help` / 短任务 mock 心跳)
+4. 规范:`build/`、`dist/`、`*.spec` 不入库(AGENTS.md 生成物纪律)
+
+**涉及文件**:`scripts/build_executable.ps1`(新)、`scripts/run_task_executor.py`(入口确认)
+
+### P4-M4 前端 adaptive 视图(P3 遗留)
+
+1. 前端现状:adaptive 闭环 API 已通(`/api/adaptive/loops/*`),UI 仍全因子展示
+2. 新增:循环状态面板(当前 phase、批次号、已用/总预算、收敛状态)+ 批次点状态可视化 + 收敛曲线(objective vs batch)
+3. 数据源:`GET /api/adaptive/loops/{id}/status` 现有响应
+
+**涉及文件**:web 前端 Vue 组件(LoopMonitor.vue 或并入现有页面)、API 层确认
+
+### P4-M5 L0 上提共享核心层(P3 遗留)
+
+1. `L0PreScreeningEngine`(`web/backend/app/services/l0_prescreening.py`)迁至 `src/afmcore/l0/prescreening.py`
+2. web 端 `l0_prescreening.py` 改引用 src(薄 re-export,保持现有 import 兼容)
+3. 依赖检查:L0 不得依赖 web 专属模块(如 db/HTTP),若依赖则一并解耦
+4. 回归:feasibility_search(已 import L0)+ web 端调用点
+
+**涉及文件**:`src/afmcore/l0/prescreening.py`(新)、`web/backend/app/services/l0_prescreening.py`(薄化)、引用点
+
+---
+
+## 5. 风险与约束
+
+| 风险 | 应对 |
+|---|---|
+| Schema 统一破坏现有闭环 | M1 先加 parse/validate 入口(纯新增),再逐步收敛调用点;每步跑现有 plan 用例回归 |
+| EXE 打包依赖复杂(pymotorcad COM/license) | 打包 headless 执行器优先;真实 Motor-CAD 连接不做打包内自检(依赖 license server),标注环境依赖 |
+| L0 上提引入循环依赖 | 先做依赖扫描(grep L0 的 import),确认无 web 专属依赖再迁移 |
+| 前端 adaptive 视图工作量不确定 | 先做最小可视图(状态面板 + 收敛曲线),批次点可视化列后续 |
+| ASCII 纪律 | 新 .py/.ps1 全 ASCII,中文进 .md |
+| 文档 V2 篇幅大 | 对照清单驱动,逐节回填,不重写原文 |
+
+---
+
+## 6. 验收清单(交付前逐项确认 ✅/❌)
+
+- [x] M1:src.plan_schema parse/validate 单测覆盖正常/异常/空值;web 创建/更新/ai 生成校验接入(test_p4_schema.py 7 组)
+- [x] M2:V2 文档与实现对照清单完成(附录 B),README 引用更新
+- [x] M3:build_executable.ps1 可复现构建,EXE 冒烟通过(--version/--self-test)
+- [x] M4:前端 adaptive 收敛曲线上线(AdaptiveOptimize.vue + points_history 数据源)
+- [x] M5:L0 迁移 src/afmcore/l0/prescreening.py,web 薄 re-export,6 处调用点兼容回归全绿
+- [x] TEST_RECORDS.md 记录 TEST-013~015
+- [x] README 十期记录回填
+- [x] 全量回归(P2 36 / M4 / M5 / M6 / closed_loop / checkpoint / concurrency)+ ASCII 0 + py_compile 0
+- [x] commit 规范 type(scope): description(M1~M5 共 5 commit + 遗留 4 commit + 文档 1 commit)
+
+---
+
+*本文档为 P4 实施计划,实施进度按批次回填。P4 五件套(M1~M5)已于 2026-08-29 全部完成。*

+ 11 - 0
docs/archive/README.md

@@ -0,0 +1,11 @@
+# docs/archive/ — 已完结历史计划文档
+
+> 本目录存放**已实施完毕**的历史计划/评审文档,仅供追溯,不再维护。
+> 当前有效文档见 `docs/HANDOFF.md` 第 5 节文档索引。
+
+| 文档 |完结时间 | 说明 |
+|---|---|---|
+| P3_IMPLEMENTATION_PLAN.md | 2026-08-29 | P3 平台化改造实施计划(策略抽象层/编排器/批次化/调度契约/HTTP闭环/执行桥),已全部实施 |
+| P4_IMPLEMENTATION_PLAN.md | 2026-08-29 | P4 五件套实施计划(Schema 统一/文档 V2/EXE 打包/收敛曲线/L0 上提),已全部实施 |
+| P3-评审响应与更新计划.md | 2026-08-29 | 第三方评审响应与 P3 升级计划(多保真度+可行性优先),已全部实施 |
+| P1-P4回顾与P5规划.md | 2026-08-30 | P1-P4 复盘与 P5 规划;P5 M1~M6 已全部完成,复盘结论并入《P1-P5交付总结与上手指南.md》 |

+ 148 - 0
docs/前端界面优化建议_V1.md

@@ -0,0 +1,148 @@
+# Web 前端界面优化建议 V1
+
+> 审查人:Car.Lin / AI 工程审查
+> 日期:2026-08-30
+> 目标:让 Web 端更简洁、直观、便于仿真工程师操作。
+> 依据:代码审查(`web/frontend/src` 全量通读)+ README / 设计方案 V2.0 / 交付总结中的开发记录。
+
+---
+
+## 0. 审查范围与方法
+
+通读了以下文件并逐条核对证据:
+
+| 类别 | 文件 |
+|---|---|
+| 路由与布局 | `router/index.ts`(15 条路由)、`layouts/MainLayout.vue`、`style.css` |
+| 核心工作流 | `ProjectList.vue`、`ProjectDetail.vue`、`PlanDetail.vue`(1129 行) |
+| 执行与监控 | `TaskManager.vue`、`MonitorDashboard.vue`、`ExecutorMonitor.vue` |
+| 分析 | `Dashboard.vue`、`AdvancedVisualization.vue`、`ExperienceList.vue` |
+| AI 页面 | `ai/PlanGenerator.vue`、`ai/AdaptiveOptimize.vue`、`ai/L0Prescreen.vue`、`ai/ResultAnalysis.vue`、`ai/FidelityCalibration.vue`、`ai/ExperienceEnhance.vue` |
+| 数据层 | `api/index.ts`、`api/ai.ts` |
+| 后端对照 | `services/fixed_params_template.py`、`services/plan_generator.py`(别名映射) |
+
+---
+
+## 1. 现状诊断(问题 → 证据 → 影响)
+
+| # | 问题 | 代码证据 | 对仿真工程师的影响 |
+|---|---|---|---|
+| D1 | **导航 14+ 项平铺,无工作流分组** | `MainLayout.vue` 侧栏 7 项平铺 + AI 子菜单 6 项 | "我下一步该去哪"不清晰;低频 AI 功能(L0预筛选/多保真度校准/经验库增强)与日常功能并列,制造菜单噪音 |
+| D2 | **监控类页面功能重叠** | `MonitorDashboard` 与 `ExecutorMonitor` 都有统计卡 + 活动任务表 + 5s 自动刷新;`TaskManager` 与之高度重合 | 同一件事三个入口,工程师无法确定"看进度该进哪个" |
+| D3 | **面包屑过弱** | `MainLayout` 只有「首页 / 当前页」两级 | 深链打开方案页(如 `/plans/5`)无法感知所属项目与上下文 |
+| D4 | **PlanDetail 单页信息过载** | `PlanDetail.vue` 1129 行:进度卡 + 4 统计卡 + AI思路 + 边界条件表 + 固定参数表(36项×8类) + 扫描变量表 + 验收标准 + AI分析区 + 结果表,全部纵向堆叠 | 核心编辑页需要长时间滚动;边界条件/固定参数/扫描变量三张表字段列相似,极易混淆 |
+| D5 | **参数三套命名口径并存** | ①`ProjectDetail` 存 `current_a/speed_rpm/magnet_temp_c`;②`PlanDetail` BC 模板用 `rated_current_a/rated_speed_rpm`;③固定参数/扫描变量用 Motor-CAD 名 `RMSCurrent/Shaft_Speed/Magnet_Temperature`;前端 `SCAN_PARAM_CN`、`categoryCnMap`、`BC_TEMPLATE`、`ALL_FIXED_PARAM_TEMPLATE` 四处手写 | 同一物理量在不同页面出现三种写法,工程师需脑内映射;后端 `plan_generator.py` 已有 `_VARIABLE_NAME_MAP` 别名兜底,恰恰说明命名漂移已成现实问题 |
+| D6 | **边界条件与固定参数"同一物理量双份设置"** | 额定电流既在 BC(`rated_current_a`)又在固定参数(`RMSCurrent`);转速、温度、冷却方式同理。后端 `build_default_fixed_params` 从 BC 推断固定参数,但**读取的是 `rated_current_a` 而非 `ProjectDetail` 存储的 `current_a`**(需验证完整调用链) | 工程师可能在两处填了不同值,无法确定哪个真正写入 Motor-CAD |
+| D7 | **固定参数 36 项默认全量铺开** | `PlanDetail.vue` 模板补齐逻辑把 `ALL_FIXED_PARAM_TEMPLATE` 全部 push 进表格 | 90% 参数用默认值即可,却逐项展示为可编辑表,注意力被稀释 |
+| D8 | **工作流步骤条是纯展示** | `ProjectDetail` 的 `el-steps` 只算 `currentStep`,不可点击、无"缺什么"提示 | 无法从步骤条获得可执行引导 |
+| D9 | **"AI分析→迭代→提取经验"三按钮割裂** | `PlanDetail.vue` 三个独立按钮 `runAIAnalysis / generateIteration / extractExperience` | 本是一组串联动作,缺一键闭环 |
+| D10 | **预估时间硬编码** | `PlanDetail.vue` `estimatedTimeMin = points * 3`(3 分钟/点) | 单点真实耗时 90~150s,100 点方案预估偏差 1.5~2 倍(README 已列为待办,前端未落实) |
+| D11 | **创建任务需手填 JSON** | `TaskManager.vue` 创建弹窗要求输入 `plan_data_json` / `parameters_json` | 违背"工程师友好"目标;应改为从方案下拉选择自动带出 |
+| D12 | **公共样式重复手写、轻微漂移** | `stat-card` 在 `ProjectList/PlanDetail/Dashboard/ExecutorMonitor` 四处重复定义,边框/圆角/字号细节不一 | 视觉不统一,改动成本高 |
+| D13 | **轮询间隔不一致** | PlanDetail 5s、AdaptiveOptimize 3s、Monitor 5s | 无统一节奏,维护与体验都不一致 |
+| D14 | **结果表列固定** | `PlanDetail` / `Dashboard` 各一份硬编码列 | 指标已扩到 35 项(P5-M6),无法按需显示会越来越挤 |
+
+---
+
+## 2. 优化建议(按优先级)
+
+### P0 — 影响最大、投入相对可控
+
+#### P0-1 信息架构重排(解决 D1/D2/D3)
+
+**目标**:侧栏按"工程师工作流"分组,一眼定位。
+
+- 侧栏改为 4 组 + 可折叠:
+  - **工作台**:项目管理(列表 / 详情)
+  - **执行**:任务管理(合并 `实时监控` + `执行器监控` 为单页 Tab:任务列表 / 执行器状态 / 汇总图表)
+  - **分析**:结果分析(`Dashboard` 与 `高级可视化` 合并或互为 Tab)
+  - **AI 智能**:仅保留 方案生成 / 自适应优化 / AI结果分析 三个高频项可见;L0预筛选、多保真度校准、经验库AI增强 收进「高级功能」折叠子组
+  - **知识**:经验库
+- 面包屑升级为 **项目 → 方案 → 任务** 层级链;深链进入 `PlanDetail` 时自动带出上级链。
+- 顶部 Header 加**全局任务状态条**:任意页面可见"运行中 N 个任务",点击直达任务管理(复用现有 `/api/tasks`、`/api/executor/status`,纯前端改动)。
+
+#### P0-2 PlanDetail 分层重构(解决 D4/D7/D9/D14)
+
+**目标**:核心编辑页从"一页长卷"改为"分页签,各司其职"。
+
+用 `el-tabs` 拆为 4 个页签:
+
+| 页签 | 内容 | 默认 |
+|---|---|---|
+| **概览** | 统计卡 + AI 设计思路 + 验收标准 + 最新结果摘要 + 主操作按钮(启动/停止仿真) | 是 |
+| **方案参数** | 扫描变量表(主)+ 固定参数(折叠态) | — |
+| **仿真结果** | 结果表 + 分析图表(趋势/Pareto/敏感性) | — |
+| **AI 闭环** | "分析 → 迭代 → 提取经验"一键向导(合并 D9 三按钮),步骤化呈现执行结果摘要 | — |
+
+固定参数表默认态:
+- 仅展示「用户修改过 / 与基线默认值不同」的行;
+- 其余按分类折叠进「更多参数…」,提供"全部展开/全部折叠";
+- 这样 36 项里通常只有个位数参数需要工程师关注。
+
+结果表:
+- 列可配置(Element Plus `show-overflow-tooltip` + 列设置),默认只显示核心指标(平均转矩/效率/脉动/总损耗/状态),其余指标折叠可选。
+
+#### P0-3 参数目录单一事实源(解决 D5/D6)
+
+**目标**:消灭三套命名口径,前端从同一目录渲染。
+
+- 后端(或共享层 `src/afmcore`)暴露一份 **参数目录**:`{ key, motorcad_var, name_cn, unit, category, description }`,覆盖边界条件、固定参数、扫描变量三类;前端统一从该目录渲染,删除 `ProjectDetail.bcFields` / `PlanDetail.BC_TEMPLATE` / `ALL_FIXED_PARAM_TEMPLATE` / `SCAN_PARAM_CN` / `categoryCnMap` 等手写副本。
+- 边界条件与固定参数的**同义关系**(如 `rated_current_a ⇄ RMSCurrent`、`rated_speed_rpm ⇄ Shaft_Speed`)在目录中显式声明;PlanDetail 在"边界条件"栏对同义参数标注"将写入固定参数 RMSCurrent(当前值 21.0A)",并提示以固定参数为最终写入值。
+- 在方案参数页签增加 **"变量写入预览"** 面板:列出本次仿真实际 `set_variable` 的清单(变量名 → 值),让工程师在点"启动"前能核对真正写进 Motor-CAD 的内容。这是当前"双份设置"最直接的兜底。
+- 需先验证 D6 调用链:确认 `ProjectDetail` 保存的 `current_a` 是否会经归一化流入 `build_default_fixed_params`(其目前读取 `rated_current_a`)。若不会,属数据一致性 Bug,应一并修复。
+
+### P1 — 提升效率与信任感
+
+#### P1-1 流程引导(解决 D8/D10)
+
+- `ProjectDetail` 步骤条改为**可交互向导**:每步显示"缺什么 / 下一步做什么"+ 快捷按钮(如无方案 → "AI 一键生成";无执行器在线 → 启动指引)。
+- 新增**仿真前检查清单**(复用已有 API):
+  - 模型路径是否存在;
+  - 固定参数中是否有"变量名需确认"项(README 已知:`Max_Speed / Winding_Connection / Current_Density / Magnet_Remanence / Insulation_Class`),列出并允许工程师确认跳过;
+  - 是否至少 1 个扫描变量;
+  - 本地执行器是否在线。
+  - 全绿才可点"启动仿真",红项点击直接定位到对应位置。
+- **预估时间校准**:用真实单点耗时。任务完成后将 `solve_time_s` 均值回写,前端显示"预计 X~Y 分钟(基于最近 N 次单点 ~Xs/点)",替代硬编码 `points*3`。
+
+#### P1-2 任务体验(解决 D11)
+
+- `TaskManager` 创建任务改为**从方案下拉选择**:选方案 → 自动带出参数与模型,JSON 输入折叠为"高级选项"。
+- 监控页合并后的单页,统一轮询节奏(建议全局 5s,可配置)。
+
+### P2 — 视觉与一致性(解决 D12/D13)
+
+- 抽取全局组件:`StatCard` / `SectionCard` / `PageHeader`,删除四处重复样式。
+- 统一轮询间隔、统一"最后更新"时间显示。
+- 深色侧栏保留,但宽度 220px 固定 → 支持折叠(collapse)适配窄屏。
+- 配色维持现有蓝/白/灰体系即可(工程师场景不需要过度装饰),重点把"任务状态"用更醒目的全局方式呈现。
+
+---
+
+## 3. 建议实施批次(可独立验收)
+
+| 批次 | 内容 | 主要涉及 | 回归要求 |
+|---|---|---|---|
+| **B1 信息架构** | 侧栏分组 + 面包屑层级 + 全局任务条 + 监控页合并 | 纯前端(`MainLayout`、`router`、`MonitorDashboard`/`ExecutorMonitor`/`TaskManager` 合并) | `npm run build` 全绿(vue-tsc 0 错误);手动点检全部路由 |
+| **B2 PlanDetail 重构** | Tabs 化 + 固定参数折叠 + 变量写入预览 + 结果列设置 | 前端为主(`PlanDetail.vue`),需后端小字段(变量写入预览基于现有 plan_data 即可) | build 全绿;对旧方案数据兼容(补齐逻辑已有) |
+| **B3 参数目录统一** | 前后端共享参数目录 + 同义映射 + D6 调用链验证 | 前后端协同 + `src/afmcore` 或 backend service | 需回归:AI 生成方案 → 边界条件 → 固定参数推断链路(可复用现有 `test_*.py` 回归脚本) |
+| **B4 流程引导** | 步骤向导 + 仿真前检查清单 + 耗时校准 | 前端 + 少量后端(单点耗时已有 `solve_time_s` 字段) | build 全绿 + 手动验证检查清单各分支 |
+
+> 建议顺序:B1 → B2 → B3 → B4。每批完成按项目纪律更新 README 与 TEST_RECORDS,代码提交与文档同步。
+
+---
+
+## 4. 项目纪律提醒(实施时遵守)
+
+- 前端改动后 `npm run build`(vue-tsc && vite)必须零错误(P5-M1 已建立基线,勿回退)。
+- `.py/.ps1` 纯 ASCII;中文字段名用 `\uXXXX`(`fixed_params_template.py` 现有写法即规范)。
+- 实际启动 Motor-CAD 求解前先 git commit;`output/`、`dist/` 不入库。
+- 原始 `.mot` 只读;参数写入保持回读校验。
+- 每批测试留痕到 `docs/TEST_RECORDS.md`。
+
+---
+
+## 5. 未验证项(诚实声明)
+
+- D6 中"`ProjectDetail` 的 `current_a` 能否流入 `build_default_fixed_params`(其读取 `rated_current_a`)"完整调用链未逐一追踪,需结合 `ai_plan.py` / `plans.py` 路由确认后定论。
+- 前端页面实际渲染效果未在本机浏览器打开验证(本次为静态代码审查),B 批次落地后需人工点检。
+- 单点耗时 90~150s 为 README/KNOWLEDGE_BASE 记录值,前端校准需以本机实测 `solve_time_s` 为准。

+ 11 - 0
executor_config.json

@@ -0,0 +1,11 @@
+{
+  "web_base_url": "http://127.0.0.1:8000",
+  "model_path": "models/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot",
+  "poll_interval": 5,
+  "instances": 1,
+  "log_dir": "output/executor_logs",
+  "log_level": "INFO",
+  "tool": "motorcad",
+  "enable_mock": false,
+  "enable_thermal": false
+}

Fichier diff supprimé car celui-ci est trop grand
+ 9080 - 0
models/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot


+ 44 - 0
nginx.conf

@@ -0,0 +1,44 @@
+# PCB AFM Simulation System - Nginx Configuration
+server {
+    listen 80;
+    server_name localhost;
+    root /usr/share/nginx/html;
+    index index.html;
+
+    # Gzip compression
+    gzip on;
+    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+    gzip_min_length 1024;
+
+    # API proxy to backend
+    location /api/ {
+        proxy_pass http://backend:8000;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_read_timeout 300s;
+        proxy_connect_timeout 10s;
+    }
+
+    # WebSocket proxy
+    location /ws/ {
+        proxy_pass http://backend:8000;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection "upgrade";
+        proxy_set_header Host $host;
+        proxy_read_timeout 86400s;
+    }
+
+    # SPA routing - fallback to index.html
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+
+    # Cache static assets
+    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+        expires 1y;
+        add_header Cache-Control "public, immutable";
+    }
+}

+ 28 - 0
scripts/build_executable.ps1

@@ -0,0 +1,28 @@
+# Build the local headless executor EXE with PyInstaller (P4-M3).
+# All strings ASCII only. Generated artifacts (build/, dist/, *.spec) are
+# NOT committed (see AGENTS.md "generated artifacts not in repo").
+$ErrorActionPreference = "Stop"
+
+$Root = Split-Path -Parent $PSScriptRoot   # <repo>
+$Src = Join-Path $Root "src"
+$Scripts = Join-Path $Root "scripts"
+$Entry = Join-Path $Scripts "run_task_executor.py"
+
+Write-Host "Root:  $Root"
+Write-Host "Entry: $Entry"
+
+if (-not (Test-Path $Entry)) {
+    Write-Error "Entry point not found: $Entry"
+}
+
+# --collect-all ansys.motorcad pulls the pymotorcad package (COM type libs)
+# which PyInstaller cannot fully infer statically.
+pyinstaller --noconfirm --clean --onefile `
+    --name "PCB-AFM-Executor" `
+    --paths $Src --paths $Root `
+    --hidden-import ansys.motorcad.core `
+    --collect-all ansys.motorcad `
+    $Entry
+
+Write-Host "Build done. Artifact:"
+Write-Host "  $Root\dist\PCB-AFM-Executor.exe"

+ 193 - 0
scripts/check_machine_paths.py

@@ -0,0 +1,193 @@
+#!/usr/bin/env python3
+"""
+check_machine_paths.py - Read-only environment & asset check for PCB AFM project.
+
+Checks:
+  1. Python version (>= 3.10)
+  2. Environment variables: MOTORCAD_ACTIVEX, ANSYSLMD_LICENSE_FILE
+  3. Motor-CAD executable path existence (env var + documented fallback path)
+  4. Python dependencies: ansys-motorcad-core, PySide6, pandas, fastapi, uvicorn, pydantic
+  5. Git repo: .git exists, HEAD valid, working tree clean
+  6. Repo assets: models/*.mot, experience/experience.db, executor_config.json, web dirs, src/afmcore
+  7. node / npm presence (needed for frontend build)
+
+Usage:
+  python scripts/check_machine_paths.py          # read-only check
+  python scripts/check_machine_paths.py --fix    # print exact fix commands (does NOT auto-execute)
+
+Exit code:
+  0 = all checks passed
+  1 = at least one check failed
+
+Note:
+  The script is read-only by default. --fix only PRINTS the exact commands to run
+  (setx / pip install), it never modifies the system by itself.
+  All output is ASCII (project source rule).
+"""
+
+import importlib.util
+import os
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+
+# documented fallback for Motor-CAD when env var is missing
+MOTORCAD_FALLBACK = r"D:\Program Files\ANSYS Inc\v261\motorcad\MotorCAD.exe"
+
+FAILURES = []
+WARNINGS = []
+
+
+def check(name, ok, detail):
+    """Record one check result and print it."""
+    tag = "PASS" if ok else "FAIL"
+    print("[%s] %s: %s" % (tag, name, detail))
+    if not ok:
+        FAILURES.append(name)
+
+
+def check_env_var(name):
+    """Check a required environment variable; return its value or ''."""
+    val = os.environ.get(name, "").strip()
+    check("env:" + name, bool(val), val if val else "MISSING")
+    return val
+
+
+def main():
+    fix_mode = "--fix" in sys.argv[1:]
+    print("=== PCB AFM - machine path check ===")
+    print("repo root: %s" % REPO_ROOT)
+
+    # 1. Python version
+    ver = sys.version_info
+    ok_ver = (ver.major, ver.minor) >= (3, 10)
+    check("python-version", ok_ver, "%d.%d.%d" % (ver.major, ver.minor, ver.micro))
+
+    # 2. required environment variables
+    motorcad_act = check_env_var("MOTORCAD_ACTIVEX")
+    check_env_var("ANSYSLMD_LICENSE_FILE")
+
+    # 3. Motor-CAD executable
+    candidates = []
+    if motorcad_act:
+        candidates.append(Path(motorcad_act))
+    candidates.append(Path(MOTORCAD_FALLBACK))
+    found_exes = [str(p) for p in candidates if p.exists()]
+    check("motorcad-exe", bool(found_exes), "; ".join(found_exes) or "none found")
+
+    # 4. python dependencies
+    deps = ["ansys.motorcad.core", "PySide6", "pandas", "fastapi", "uvicorn", "pydantic"]
+    for d in deps:
+        try:
+            found = importlib.util.find_spec(d) is not None
+        except Exception:
+            found = False
+        check("pkg:" + d, found, "installed" if found else "MISSING")
+
+    # detect a project-local virtualenv (informational: shell python may lack project deps)
+    for venv_name in (".venv", ".env"):
+        vd = REPO_ROOT / venv_name
+        if vd.exists():
+            py = vd / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
+            if py.exists():
+                print("[INFO] project venv found: %s (re-run checks with that python to validate project deps)" % py)
+            break
+
+    # 5. git repo state
+    check("git-dir", (REPO_ROOT / ".git").exists(), str(REPO_ROOT / ".git"))
+    head_ok = False
+    head = ""
+    try:
+        r = subprocess.run(
+            ["git", "rev-parse", "--verify", "HEAD"],
+            cwd=str(REPO_ROOT), capture_output=True, text=True, timeout=10,
+        )
+        head_ok = r.returncode == 0
+        head = r.stdout.strip()[:12] if head_ok else "INVALID"
+    except Exception as exc:  # noqa: BLE001 - report any failure as invalid head
+        head = "error:%s" % exc
+    check("git-head", head_ok, head)
+
+    clean = False
+    try:
+        r = subprocess.run(
+            ["git", "status", "--porcelain"],
+            cwd=str(REPO_ROOT), capture_output=True, text=True, timeout=10,
+        )
+        clean = r.returncode == 0 and r.stdout.strip() == ""
+    except Exception:
+        clean = False
+    check("git-clean", clean, "clean" if clean else "has uncommitted changes")
+
+    # 6. repo assets
+    models_dir = REPO_ROOT / "models"
+    mot_files = list(models_dir.glob("*.mot")) if models_dir.exists() else []
+    check("asset:models-mot", len(mot_files) > 0, "%d .mot file(s)" % len(mot_files))
+
+    exp_db = REPO_ROOT / "experience" / "experience.db"
+    check("asset:experience-db", exp_db.exists(), str(exp_db))
+
+    cfg = REPO_ROOT / "executor_config.json"
+    check("asset:executor-config", cfg.exists(), str(cfg))
+
+    web_f = REPO_ROOT / "web" / "frontend"
+    web_b = REPO_ROOT / "web" / "backend"
+    check("asset:web-frontend", web_f.exists(), str(web_f))
+    check("asset:web-backend", web_b.exists(), str(web_b))
+
+    afmcore = REPO_ROOT / "src" / "afmcore"
+    check("asset:afmcore", afmcore.exists(), str(afmcore))
+
+    # 7. node / npm (warning only, frontend build)
+    node_ok = shutil.which("node") is not None
+    npm_ok = shutil.which("npm") is not None
+    if node_ok and npm_ok:
+        print("[PASS] node/npm: found on PATH")
+    else:
+        WARNINGS.append("node/npm not found on PATH (needed for frontend build)")
+        print("[WARN] node/npm: missing on PATH")
+
+    # 8. summary
+    print("=== summary ===")
+    if FAILURES:
+        print("FAILED (%d): %s" % (len(FAILURES), ", ".join(FAILURES)))
+        for w in WARNINGS:
+            print("WARN: %s" % w)
+        if fix_mode:
+            _print_fix_commands()
+        sys.exit(1)
+    else:
+        print("ALL CHECKS PASSED")
+        for w in WARNINGS:
+            print("WARN: %s" % w)
+        sys.exit(0)
+
+
+def _print_fix_commands():
+    """Print (do not execute) the exact commands to fix each failure."""
+    print("=== suggested fixes (run manually) ===")
+    for name in FAILURES:
+        if name == "env:MOTORCAD_ACTIVEX":
+            print(
+                'setx MOTORCAD_ACTIVEX "%s"  # or let scripts fall back to set_motorcad_exe()'
+                % MOTORCAD_FALLBACK
+            )
+        elif name == "env:ANSYSLMD_LICENSE_FILE":
+            print('setx ANSYSLMD_LICENSE_FILE "1055@localhost"')
+        elif name.startswith("pkg:"):
+            pkg = name.split(":", 1)[1]
+            pip_name = "ansys-motorcad-core" if pkg == "ansys.motorcad.core" else pkg
+            print("pip install %s" % pip_name)
+        elif name == "git-clean":
+            print("git commit (or stash) uncommitted changes before running simulations")
+        elif name == "asset:experience-db":
+            print("run once to bootstrap the experience database (see README)")
+        elif name == "asset:models-mot":
+            print("place a baseline .mot model into models/ (read-only)")
+
+
+if __name__ == "__main__":
+    main()

+ 219 - 0
scripts/executor_config.py

@@ -0,0 +1,219 @@
+"""Executor configuration loader (P5-M2).
+
+Loads executor_config.json for the local headless executor.  The goal is
+to make the packaged EXE configurable without re-building: web address,
+model path, logging, poll interval and instance count all come from a
+JSON sidecar that the operator can edit next to the EXE.
+
+Priority (highest first):
+  1. --config CLI path          (explicit)
+  2. $EXECUTOR_CONFIG env var   (explicit)
+  3. <exe_or_script_dir>/executor_config.json   (sidecar next to EXE)
+  4. <repo_root>/executor_config.json           (source-tree template)
+  5. built-in defaults
+
+Single fields may still be overridden by env vars (highest):
+  WEB_BASE_URL / MOTORCAD_MODEL / EXECUTOR_INSTANCES /
+  EXECUTOR_POLL_INTERVAL / EXECUTOR_LOG_DIR / EXECUTOR_LOG_LEVEL /
+  EXECUTOR_TOOL / EXECUTOR_MOCK
+
+All code in this module is ASCII only (AGENTS.md constraint).
+"""
+import json
+import os
+import sys
+
+DEFAULT_WEB_BASE_URL = "http://127.0.0.1:8000"
+DEFAULT_POLL_INTERVAL = 5
+DEFAULT_INSTANCES = 1
+DEFAULT_LOG_DIR = "output/executor_logs"
+DEFAULT_LOG_LEVEL = "INFO"
+DEFAULT_TOOL = "motorcad"
+DEFAULT_MODEL_REL = "models/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot"
+
+VALID_LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
+
+
+def default_config():
+    """Return the built-in default configuration dict."""
+    return {
+        "web_base_url": DEFAULT_WEB_BASE_URL,
+        "model_path": DEFAULT_MODEL_REL,
+        "poll_interval": DEFAULT_POLL_INTERVAL,
+        "instances": DEFAULT_INSTANCES,
+        "log_dir": DEFAULT_LOG_DIR,
+        "log_level": DEFAULT_LOG_LEVEL,
+        "tool": DEFAULT_TOOL,
+        "enable_mock": False,
+        "enable_thermal": False,
+    }
+
+
+def _is_frozen():
+    """True when running from a PyInstaller onefile bundle."""
+    return bool(getattr(sys, "frozen", False))
+
+
+def _base_dir():
+    """Directory that should host the sidecar config for this runtime.
+
+    - EXE mode: the folder containing the executable.
+    - Script mode: the repository root (parent of the scripts/ folder).
+    """
+    if _is_frozen():
+        return os.path.dirname(os.path.abspath(sys.executable))
+    here = os.path.dirname(os.path.abspath(__file__))
+    return os.path.dirname(here)
+
+
+def repo_root():
+    """Repository root (parent of scripts/) when running from source."""
+    here = os.path.dirname(os.path.abspath(__file__))
+    return os.path.dirname(here)
+
+
+def find_config_path(cli_path=None):
+    """Locate a config file path, or None when none exists.
+
+    Args:
+        cli_path: optional explicit path from --config.
+
+    Returns:
+        str path of the first existing config file, else None.
+    """
+    candidates = []
+    if cli_path:
+        candidates.append(cli_path)
+    env_path = os.environ.get("EXECUTOR_CONFIG")
+    if env_path:
+        candidates.append(env_path)
+    candidates.append(os.path.join(_base_dir(), "executor_config.json"))
+    candidates.append(os.path.join(repo_root(), "executor_config.json"))
+    for cand in candidates:
+        if cand and os.path.isfile(cand):
+            return cand
+    return None
+
+
+def _resolve_model_path(model_path, base_dir):
+    """Turn a possibly-relative model_path into an absolute path.
+
+    Relative paths resolve against the config base dir (EXE dir or repo
+    root).  Empty / None values stay empty (EXE may rely on the operator
+    to set it; script mode fills a default).
+    """
+    if not model_path:
+        return None
+    if os.path.isabs(model_path):
+        return os.path.normpath(model_path)
+    return os.path.normpath(os.path.join(base_dir, model_path))
+
+
+def validate_config(cfg):
+    """Validate a config dict; raise ValueError on any violation.
+
+    Checks: types, ranges and known enum values.  Empty model_path is
+    allowed (EXE may be pointed at a model only at run time).
+    """
+    if not isinstance(cfg, dict):
+        raise ValueError("config must be a dict")
+    web = cfg.get("web_base_url")
+    if not isinstance(web, str) or not web.startswith("http"):
+        raise ValueError("web_base_url must be an http(s) URL string")
+    instances = cfg.get("instances")
+    if not isinstance(instances, int) or instances < 1:
+        raise ValueError("instances must be an int >= 1")
+    interval = cfg.get("poll_interval")
+    if not isinstance(interval, (int, float)) or interval <= 0:
+        raise ValueError("poll_interval must be a positive number")
+    level = cfg.get("log_level")
+    if level not in VALID_LOG_LEVELS:
+        raise ValueError("log_level must be one of %s" % (VALID_LOG_LEVELS,))
+    tool = cfg.get("tool")
+    if not isinstance(tool, str) or not tool:
+        raise ValueError("tool must be a non-empty string")
+    mock = cfg.get("enable_mock")
+    if not isinstance(mock, bool):
+        raise ValueError("enable_mock must be a boolean")
+    thermal = cfg.get("enable_thermal")
+    if not isinstance(thermal, bool):
+        raise ValueError("enable_thermal must be a boolean")
+    return cfg
+
+
+def load_config(cli_path=None, env_overrides=True):
+    """Load and validate the effective configuration.
+
+    Args:
+        cli_path: optional --config path.
+        env_overrides: when True, let env vars override file fields.
+
+    Returns:
+        dict with resolved absolute model_path/log_dir and the source
+        string under key "config_source".
+    """
+    cfg = default_config()
+    source = "defaults"
+
+    cfg_path = find_config_path(cli_path)
+    if cfg_path:
+        try:
+            with open(cfg_path, "r", encoding="utf-8") as fh:
+                file_cfg = json.load(fh)
+            if not isinstance(file_cfg, dict):
+                raise ValueError("config file must contain a JSON object")
+            cfg.update({k: v for k, v in file_cfg.items() if v is not None})
+            source = cfg_path
+        except (OSError, ValueError) as exc:
+            # A malformed explicit config must not be silently ignored:
+            # the operator asked for it, so surface the error.
+            raise ValueError("failed to load config %s: %s" % (cfg_path, exc))
+
+    if env_overrides:
+        env_map = {
+            "WEB_BASE_URL": "web_base_url",
+            "MOTORCAD_MODEL": "model_path",
+            "EXECUTOR_INSTANCES": "instances",
+            "EXECUTOR_POLL_INTERVAL": "poll_interval",
+            "EXECUTOR_LOG_DIR": "log_dir",
+            "EXECUTOR_LOG_LEVEL": "log_level",
+            "EXECUTOR_TOOL": "tool",
+            "EXECUTOR_MOCK": "enable_mock",
+            "EXECUTOR_THERMAL": "enable_thermal",
+        }
+        for env_key, cfg_key in env_map.items():
+            raw = os.environ.get(env_key)
+            if raw is None or raw == "":
+                continue
+            if cfg_key in ("instances", "poll_interval"):
+                try:
+                    cfg[cfg_key] = float(raw) if "." in raw else int(raw)
+                except ValueError:
+                    raise ValueError("env %s must be numeric, got %r" % (env_key, raw))
+            elif cfg_key in ("enable_mock", "enable_thermal"):
+                cfg[cfg_key] = raw.strip().lower() in ("1", "true", "yes", "on")
+            else:
+                cfg[cfg_key] = raw
+
+    # Coerce numeric fields read from JSON (json gives int/float already).
+    if not isinstance(cfg["instances"], int):
+        cfg["instances"] = int(cfg["instances"])
+    if not isinstance(cfg["poll_interval"], (int, float)):
+        cfg["poll_interval"] = float(cfg["poll_interval"])
+
+    base_dir = _base_dir() if _is_frozen() else repo_root()
+    model_path = _resolve_model_path(cfg.get("model_path"), base_dir)
+    log_dir = cfg.get("log_dir")
+    if not log_dir:
+        log_dir = DEFAULT_LOG_DIR
+    if os.path.isabs(log_dir):
+        log_dir = os.path.normpath(log_dir)
+    else:
+        log_dir = os.path.normpath(os.path.join(base_dir, log_dir))
+
+    resolved = dict(cfg)
+    resolved["model_path"] = model_path
+    resolved["log_dir"] = log_dir
+    resolved["config_source"] = source
+    validate_config(resolved)
+    return resolved

+ 834 - 0
scripts/robust_motorcad.py

@@ -0,0 +1,834 @@
+"""Robust Motor-CAD simulation core (P4-M3 + reference doc enhancement).
+
+Integrates all robustness practices from reference projects and
+official Motor-CAD automation reference documentation:
+
+Connection & Lifecycle:
+- open_new_instance=True + set_visible(True) (never connect to existing)
+- BlackBox headless mode support for server batch execution
+- Internal/external scripting context detection (is_running_in_internal_scripting)
+- Environment variable fallback (MOTORCAD_ACTIVEX, ANSYSLMD_LICENSE_FILE)
+
+Error Handling:
+- MotorCADError first-catch (PyMotorCAD throws on failure, no silent success)
+- Per-point timeout + retry (max 3 attempts) + auto-reconnect
+- Instance crash detection and auto-restart
+
+Batch Safety:
+- MessageDisplayState=2 popup suppression with try/finally restore
+- Parameter write-back verification (set then get, mismatch = FAILED)
+- Per-point baseline reload (load_from_file before and after each point)
+- Sampling point / mesh compatibility check (avoid 120pt+840mesh popup)
+- Slot opening / PCB copper width linkage formula
+
+Data Integrity:
+- Result export parsing: semicolon CSV, bilingual field aliases, E-Magnetics priority
+- Per-point dual write (CSV + JSON) with flush + fsync
+- Graph data reading with "out-of-bounds = end" idiom (while + try/except MotorCADError)
+
+Preflight Self-Check (5 layers):
+- Connection layer: multi-version, Automation registration, port/firewall, Hide command Window
+- Permission layer: admin rights, default install path, post-install reboot
+- License layer: License Manager service, port, validity, concurrency
+- Model layer: region closure (is_closed), duplicate regions, adaptive geometry reset
+- Script layer: MotorCADError handling, variable name version mapping, popup state
+
+Variable Name Version Mapping:
+- Configurable mapping table (not hardcoded) for version-specific name changes
+- e.g. MagWindingType -> MagneticWindingType across versions
+
+All source is ASCII only; Chinese field names use \\uXXXX escapes.
+"""
+from __future__ import annotations
+
+import csv
+import json
+import math
+import os
+import platform
+import socket
+import sys
+import time
+import traceback
+from datetime import datetime
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple
+
+# MotorCADError may not be available if pymotorcad is not installed
+try:
+    from ansys.motorcad.core import MotorCADError
+    HAS_MOTORCAD_ERROR = True
+except ImportError:
+    MotorCADError = Exception  # type: ignore
+    HAS_MOTORCAD_ERROR = False
+
+# ---------------------------------------------------------------------------
+# Platform core import (single source of truth for metrics / parsing).
+# This replaces the historical per-file METRIC_DEFINITIONS copies, fixing the
+# drift bug (three inconsistent metric lists) and the tavg_nm / ripple_pct
+# parsing bug (normalized matching handles full-width chars in exports).
+# ---------------------------------------------------------------------------
+_ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+_SRC_DIR = os.path.join(_ROOT_DIR, "src")
+if _SRC_DIR not in sys.path:
+    sys.path.insert(0, _SRC_DIR)
+
+from afmcore.metrics import (  # noqa: E402
+    METRIC_DEFINITIONS,
+    METRIC_KEYS,
+    METRIC_LABELS,
+    REQUIRED_METRICS,
+    extract_all_metrics as _platform_extract_all_metrics,
+    parse_export as _platform_parse_export,
+)
+
+
+# Known incompatible sampling point / mesh combinations that cause popups
+INCOMPATIBLE_SAMPLING_MESH = [
+    (120, 840),  # Motor-CAD warns mesh/time step mismatch, blocks batch
+]
+
+# Recommended compatible combinations
+RECOMMENDED_SAMPLING_MESH = [
+    (30, 840),   # Fast trend scan
+    (120, 960),  # Medium confidence
+    (180, 1680), # High confidence final
+]
+
+# ---------------------------------------------------------------------------
+# Variable name version mapping (not hardcoded, configurable).
+# Reference: GitHub Issue #319 - parameter names change across versions
+# e.g. MagWindingType -> MagneticWindingType
+# ---------------------------------------------------------------------------
+
+VARIABLE_NAME_MAP: Dict[str, Dict[str, str]] = {
+    # canonical_name: {version_range: actual_variable_name}
+    # Business alias -> Motor-CAD actual variable name.
+    # L0 / plan_schema use airgap_mm; Motor-CAD calls it Airgap.
+    # Confirmed by variable probing (TEST-002, KNOWLEDGE_BASE).
+    "airgap_mm": {
+        "default": "Airgap",
+    },
+    "MagneticWindingType": {
+        "default": "MagneticWindingType",
+        "legacy": "MagWindingType",  # pre-2023 versions
+    },
+    "TorquePointsPerCycle": {
+        "default": "TorquePointsPerCycle",
+    },
+    "AirgapMeshPoints_mesh": {
+        "default": "AirgapMeshPoints_mesh",
+    },
+    "AirgapMeshPoints_layers": {
+        "default": "AirgapMeshPoints_layers",
+    },
+    "Slot_Opening": {
+        "default": "Slot_Opening",
+    },
+    "Slot_Width": {
+        "default": "Slot_Width",
+    },
+    "Copper_Width": {
+        "default": "Copper_Width",
+    },
+    "MagnetCentralArc_HalbachRing": {
+        "default": "MagnetCentralArc_HalbachRing",
+    },
+    "Magnet_Arc_[ED]": {
+        "default": "Magnet_Arc_[ED]",
+    },
+    "MessageDisplayState": {
+        "default": "MessageDisplayState",
+    },
+}
+
+
+def resolve_variable_name(canonical_name: str, motorcad_version: Optional[str] = None) -> str:
+    """Resolve canonical variable name to version-specific actual name.
+
+    Args:
+        canonical_name: Canonical parameter name (key in VARIABLE_NAME_MAP)
+        motorcad_version: Motor-CAD version string, e.g. "2024.2.3"
+
+    Returns:
+        Actual variable name for this Motor-CAD version
+    """
+    mapping = VARIABLE_NAME_MAP.get(canonical_name, {})
+    if not mapping:
+        return canonical_name
+    # For now, use default. Version-specific logic can be added here.
+    return mapping.get("default", canonical_name)
+
+
+def ensure_environment() -> None:
+    """Ensure Motor-CAD environment variables are set (non-login shell trap).
+
+    Reference: AGENTS.md environment variable traps.
+    Non-login shell may not inherit machine-level env vars:
+    - MOTORCAD_ACTIVEX empty -> pymotorcad cannot find Motor-CAD
+    - ANSYSLMD_LICENSE_FILE empty -> Motor-CAD silently exits after ~30s
+    """
+    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)
+        except Exception:
+            pass
+    if not os.environ.get("ANSYSLMD_LICENSE_FILE"):
+        os.environ["ANSYSLMD_LICENSE_FILE"] = "1055@localhost"
+
+
+def check_sampling_mesh_compatibility(torque_points: int, airgap_mesh: int) -> Tuple[bool, str]:
+    """Check if sampling point / mesh combination is compatible.
+
+    Returns (compatible, message). Incompatible combinations cause
+    Motor-CAD popups that block unattended batch execution.
+    Reference: MOTORCAD_SCAN_KNOWLEDGE_BASE.md section 6.2
+    """
+    for pts, mesh in INCOMPATIBLE_SAMPLING_MESH:
+        if torque_points == pts and airgap_mesh == mesh:
+            return False, (
+                f"TorquePoints={torque_points} + AirgapMesh={airgap_mesh} "
+                f"causes Motor-CAD popup. Use {RECOMMENDED_SAMPLING_MESH[1]} instead."
+            )
+    return True, "OK"
+
+
+def compute_copper_width(slot_opening_mm: float, clearance_mm: float = 0.2,
+                         conductor_count: int = 1) -> float:
+    """Compute PCB copper width from slot opening (linkage formula).
+
+    Copper_Width = (Slot_Opening - clearance) / 2 / conductor_count
+    Reference: MOTORCAD_SCAN_KNOWLEDGE_BASE.md section 4.3
+    """
+    return round((slot_opening_mm - clearance_mm) / 2.0 / conductor_count, 3)
+
+
+def is_running_as_admin() -> bool:
+    """Check if running with administrator privileges (Windows).
+
+    Reference: Fault case #5 - "Unable to run FE module" solved by
+    running as administrator.
+    """
+    try:
+        if platform.system() == "Windows":
+            import ctypes
+            return ctypes.windll.shell32.IsUserAnAdmin() != 0
+        return os.geteuid() == 0  # type: ignore
+    except Exception:
+        return False
+
+
+def check_license_server(host: str = "localhost", port: int = 1055, timeout: float = 3.0) -> Tuple[bool, str]:
+    """Check if Ansys License Manager server is reachable.
+
+    Reference: Fault case #8 - cannot get license.
+    """
+    try:
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.settimeout(timeout)
+        result = sock.connect_ex((host, port))
+        sock.close()
+        if result == 0:
+            return True, f"License server {host}:{port} reachable"
+        return False, f"License server {host}:{port} not reachable (error code {result})"
+    except Exception as e:
+        return False, f"License server check failed: {e}"
+
+
+class PreflightResult:
+    """Result of 5-layer preflight self-check."""
+
+    def __init__(self):
+        self.layers: Dict[str, Dict[str, Any]] = {
+            "connection": {"passed": True, "checks": [], "warnings": []},
+            "permission": {"passed": True, "checks": [], "warnings": []},
+            "license": {"passed": True, "checks": [], "warnings": []},
+            "model": {"passed": True, "checks": [], "warnings": []},
+            "script": {"passed": True, "checks": [], "warnings": []},
+        }
+
+    def add_check(self, layer: str, name: str, passed: bool, message: str = "") -> None:
+        if layer in self.layers:
+            self.layers[layer]["checks"].append({"name": name, "passed": passed, "message": message})
+            if not passed:
+                self.layers[layer]["passed"] = False
+
+    def add_warning(self, layer: str, message: str) -> None:
+        if layer in self.layers:
+            self.layers[layer]["warnings"].append(message)
+
+    @property
+    def all_passed(self) -> bool:
+        return all(layer["passed"] for layer in self.layers.values())
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {"all_passed": self.all_passed, "layers": self.layers}
+
+    def summary(self) -> str:
+        lines = ["Preflight Self-Check Summary:"]
+        for layer_name, layer in self.layers.items():
+            status = "PASS" if layer["passed"] else "FAIL"
+            lines.append(f"  [{status}] {layer_name} layer")
+            for check in layer["checks"]:
+                cs = "OK" if check["passed"] else "FAIL"
+                lines.append(f"    [{cs}] {check['name']}: {check['message']}")
+            for warning in layer["warnings"]:
+                lines.append(f"    [WARN] {warning}")
+        return "\n".join(lines)
+
+
+class RobustMotorCADSolver:
+    """Robust Motor-CAD simulation solver with all best practices.
+
+    Usage:
+        solver = RobustMotorCADSolver(model_path="base.mot")
+        solver.connect()
+        preflight = solver.run_preflight()
+        if preflight.all_passed:
+            for params in parameter_list:
+                result = solver.run_single_point(params, point_index=0)
+        solver.disconnect()
+    """
+
+    def __init__(self, model_path: str, output_dir: Optional[str] = None,
+                 point_timeout: int = 300, max_retries: int = 3,
+                 headless: bool = False, motorcad_version: Optional[str] = None,
+                 enable_thermal: bool = False):
+        self.model_path = model_path
+        # P5-M6: optional thermal solve (requires model with thermal
+        # network configured; OFF by default to preserve EM-only behavior)
+        self.enable_thermal = bool(enable_thermal)
+        self.output_dir = output_dir or os.path.join(
+            os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+            "output", f"run_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
+        )
+        self.raw_dir = os.path.join(self.output_dir, "raw")
+        os.makedirs(self.raw_dir, exist_ok=True)
+        self.point_timeout = point_timeout
+        self.max_retries = max_retries
+        self.headless = headless
+        self.motorcad_version = motorcad_version
+        self.mc = None
+        self._popup_suppressed = False
+        self._csv_path = os.path.join(self.output_dir, "scan_results.csv")
+        self._json_path = os.path.join(self.output_dir, "scan_results.json")
+        self._log_path = os.path.join(self.output_dir, "program_log.log")
+        self._all_results: List[Dict[str, Any]] = []
+        self._csv_header_written = False
+
+    def connect(self) -> None:
+        """Connect to a new Motor-CAD instance (never connect to existing).
+
+        Supports:
+        - Internal/external scripting context detection
+        - BlackBox headless mode for server batch execution
+        - set_visible(True) for /SCRIPTING mode (default hidden)
+
+        Reference: Official doc section 2.1 connection modes.
+        """
+        ensure_environment()
+        try:
+            from ansys.motorcad.core import MotorCAD, is_running_in_internal_scripting
+
+            # Detect internal vs external scripting context
+            if is_running_in_internal_scripting():
+                self.mc = MotorCAD(open_new_instance=False)
+                self._log("Connected in internal scripting mode")
+            else:
+                # External script: always open new instance
+                if self.headless:
+                    # BlackBox mode: no GUI, suitable for server batch
+                    self.mc = MotorCAD(open_new_instance=True, keep_instance_open=False)
+                    self._log("Connected in BlackBox headless mode")
+                else:
+                    self.mc = MotorCAD(open_new_instance=True, keep_instance_open=False)
+                    self.mc.set_visible(True)
+                    self._log("Connected to new visible Motor-CAD instance")
+
+            time.sleep(2)  # Wait for instance to fully initialize
+
+            # Health check: verify connection is responsive
+            _ = self.mc.get_variable("Motor_Type")
+            self._log("Connection health check passed")
+
+        except MotorCADError as e:
+            self._log(f"MotorCADError during connection: {e}")
+            raise
+        except Exception as e:
+            self._log(f"Connection failed: {e}")
+            raise
+
+    def disconnect(self) -> None:
+        """Disconnect from Motor-CAD instance.
+
+        Always restores popup state before quitting.
+        """
+        if self.mc:
+            # Restore popup state (critical: MessageDisplayState must be restored)
+            self._restore_popup_state()
+            try:
+                # Reload baseline to leave clean state
+                self.mc.load_from_file(self.model_path)
+            except Exception:
+                pass
+            try:
+                self.mc.quit()
+            except Exception:
+                pass
+            self.mc = None
+            self._log("Disconnected from Motor-CAD")
+
+    def _suppress_popups(self) -> None:
+        """Suppress Motor-CAD popups for batch execution.
+
+        MessageDisplayState=2: messages go to independent window, no popups.
+        Reference: Official doc section 2.3 popup control.
+
+        WARNING: This disables critical dialogs (save prompts, overwrite
+        confirmations). Must be restored with _restore_popup_state().
+        """
+        if self.mc and not self._popup_suppressed:
+            try:
+                var_name = resolve_variable_name("MessageDisplayState", self.motorcad_version)
+                self.mc.set_variable(var_name, 2)
+                self._popup_suppressed = True
+                self._log("Popup suppression enabled (MessageDisplayState=2)")
+            except MotorCADError as e:
+                self._log(f"Failed to suppress popups: {e}")
+            except Exception as e:
+                self._log(f"Failed to suppress popups: {e}")
+
+    def _restore_popup_state(self) -> None:
+        """Restore popup state to default (0).
+
+        Must be called in finally blocks to ensure restoration even on error.
+        Reference: Official doc section 2.3 - "script must restore before exit".
+        """
+        if self.mc and self._popup_suppressed:
+            try:
+                var_name = resolve_variable_name("MessageDisplayState", self.motorcad_version)
+                self.mc.set_variable(var_name, 0)
+                self._popup_suppressed = False
+                self._log("Popup state restored (MessageDisplayState=0)")
+            except MotorCADError as e:
+                self._log(f"Failed to restore popup state: {e}")
+            except Exception as e:
+                self._log(f"Failed to restore popup state: {e}")
+
+    def _write_and_verify(self, variable: str, value: float,
+                           rel_tol: float = 1e-8, abs_tol: float = 1e-7) -> float:
+        """Write variable and verify with get_variable. Mismatch raises.
+
+        Reference: AGENTS.md constraint #4 - parameter must be read-back verified.
+        Motor-CAD sometimes silently accepts inapplicable parameters.
+        """
+        # Resolve version-specific variable name
+        actual_var = resolve_variable_name(variable, self.motorcad_version)
+        try:
+            self.mc.set_variable(actual_var, value)
+            applied = float(self.mc.get_variable(actual_var))
+        except MotorCADError as e:
+            raise RuntimeError(f"MotorCADError writing {actual_var}: {e}")
+        if not math.isclose(applied, value, rel_tol=rel_tol, abs_tol=abs_tol):
+            raise RuntimeError(
+                f"Variable {actual_var} write mismatch: applied={applied}, expected={value}"
+            )
+        return applied
+
+    def run_preflight(self) -> PreflightResult:
+        """Run 5-layer preflight self-check before simulation.
+
+        Layers (reference: official doc section 3.3 troubleshooting checklist):
+        1. Connection: multi-version, Automation registration, port/firewall, Hide command Window
+        2. Permission: admin rights, default install path, post-install reboot
+        3. License: License Manager service, port, validity, concurrency
+        4. Model: region closure, duplicate regions, adaptive geometry reset
+        5. Script: MotorCADError handling, variable name mapping, popup state
+
+        Returns:
+            PreflightResult with all layer checks
+        """
+        result = PreflightResult()
+        self._log("Starting 5-layer preflight self-check...")
+
+        # Layer 1: Connection
+        result.add_check("connection", "Motor-CAD instance connected",
+                         self.mc is not None, "Instance is active" if self.mc else "No instance")
+        result.add_check("connection", "Connection responsive",
+                         self._check_connection_responsive(),
+                         "Instance responds to get_variable" if self._check_connection_responsive() else "Instance not responding")
+        # Check for common "Hide command Window" issue (GitHub Issue #140)
+        result.add_warning("connection",
+                           "If connection fails, check Motor-CAD Settings -> 'Hide command Window' is unchecked (known bug #140)")
+
+        # Layer 2: Permission
+        admin = is_running_as_admin()
+        result.add_check("permission", "Running as administrator",
+                         admin, "Admin privileges active" if admin else "Not running as admin (may cause FE module errors)")
+        if not admin:
+            result.add_warning("permission",
+                               "Fault case #5: 'Unable to run FE module' may be solved by running as administrator")
+
+        # Check default install path
+        default_path = r"C:\ANSYS_Motor-CAD"
+        has_default = os.path.exists(default_path)
+        result.add_check("permission", "Default install path exists",
+                         has_default, f"Path {default_path} exists" if has_default else f"Default path {default_path} not found (non-default install may cause issues)")
+
+        # Layer 3: License
+        license_ok, license_msg = check_license_server()
+        result.add_check("license", "License server reachable", license_ok, license_msg)
+        if not license_ok:
+            result.add_warning("license",
+                               "Fault case #8: Check Ansys License Manager service, port 1055, license file validity, and concurrency count")
+
+        # Layer 4: Model
+        model_exists = os.path.exists(self.model_path)
+        result.add_check("model", "Baseline model file exists",
+                         model_exists, f"Model at {self.model_path}" if model_exists else f"Model not found at {self.model_path}")
+        if model_exists and self.mc:
+            try:
+                self.mc.load_from_file(self.model_path)
+                result.add_check("model", "Model loads successfully", True, "Model loaded without error")
+            except MotorCADError as e:
+                result.add_check("model", "Model loads successfully", False, f"MotorCADError: {e}")
+            except Exception as e:
+                result.add_check("model", "Model loads successfully", False, str(e))
+        result.add_warning("model",
+                           "If using adaptive geometry, call reset_adaptive_geometry() before modifications; ensure regions are closed (is_closed()) and counter-clockwise")
+
+        # Layer 5: Script
+        result.add_check("script", "MotorCADError import available",
+                         HAS_MOTORCAD_ERROR,
+                         "ansys.motorcad.core.MotorCADError imported" if HAS_MOTORCAD_ERROR else "MotorCADError not available (using generic Exception fallback)")
+        result.add_check("script", "Variable name mapping configured",
+                         len(VARIABLE_NAME_MAP) > 0,
+                         f"{len(VARIABLE_NAME_MAP)} variables in mapping table")
+        result.add_check("script", "Popup state will be restored on disconnect",
+                         True, "try/finally pattern ensures MessageDisplayState restoration")
+
+        self._log(result.summary())
+        return result
+
+    def _check_connection_responsive(self) -> bool:
+        """Check if Motor-CAD instance is responsive."""
+        if not self.mc:
+            return False
+        try:
+            _ = self.mc.get_variable("Motor_Type")
+            return True
+        except Exception:
+            return False
+
+    def run_single_point(self, params: Dict[str, Any], point_index: int = 0,
+                          point_label: str = "",
+                          enable_thermal: Optional[bool] = None) -> Dict[str, Any]:
+        """Run a single simulation point with full robustness protocol.
+
+        Protocol:
+        1. Suppress popups (MessageDisplayState=2)
+        2. Reload baseline model
+        3. Check sampling/mesh compatibility
+        4. Write all parameters with write-back verification (version-resolved names)
+        5. Handle linked parameters (slot opening -> copper width)
+        6. Run magnetic calculation
+        7. Export and parse results
+        8. Write results to CSV and JSON (flush immediately)
+        9. Reload baseline again
+        10. Restore popup state (in finally)
+
+        All Motor-CAD calls wrapped in try/except MotorCADError.
+        """
+        start_time = time.time()
+        result = {
+            "point_index": point_index,
+            "point_label": point_label,
+            "params": params,
+            "status": "pending",
+            "metrics": {},
+            "error": None,
+            "duration_s": 0,
+        }
+
+        # Suppress popups for batch execution
+        self._suppress_popups()
+
+        try:
+            for attempt in range(self.max_retries):
+                try:
+                    # Step 1: Reload baseline
+                    try:
+                        self.mc.load_from_file(self.model_path)
+                    except MotorCADError as e:
+                        raise RuntimeError(f"Baseline reload failed: {e}")
+
+                    # Step 2: Check sampling/mesh compatibility if present
+                    if "TorquePointsPerCycle" in params and "AirgapMeshPoints_mesh" in params:
+                        compatible, msg = check_sampling_mesh_compatibility(
+                            int(params["TorquePointsPerCycle"]),
+                            int(params["AirgapMeshPoints_mesh"])
+                        )
+                        if not compatible:
+                            self._log(f"WARNING: {msg}")
+                            result["error"] = msg
+                            result["status"] = "FAILED"
+                            # B5 fix: break instead of return so the point
+                            # is appended to results and written to disk
+                            break
+
+                    # Step 3: Write all numeric parameters with verification.
+                    # Non-numeric params (materials, grades, strings) are
+                    # skipped because set_variable expects a number. (C1 fix)
+                    for var, val in params.items():
+                        if var in ("point_index", "point_label", "point_id"):
+                            continue
+                        try:
+                            num = float(val)
+                        except (TypeError, ValueError):
+                            self._log(
+                                f"Skipping non-numeric param {var}={val!r}"
+                            )
+                            continue
+                        self._write_and_verify(var, num)
+
+                    # Step 4: Handle linked parameters
+                    if "Slot_Opening" in params and "Copper_Width" not in params:
+                        copper_w = compute_copper_width(float(params["Slot_Opening"]))
+                        self._write_and_verify("Copper_Width", copper_w)
+
+                    # Step 5: Run magnetic calculation
+                    try:
+                        self.mc.do_magnetic_calculation()
+                    except MotorCADError as e:
+                        raise RuntimeError(f"Magnetic calculation failed: {e}")
+
+                    # Step 5b: Optional thermal calculation (P5-M6)
+                    # Best-effort: thermal solve requires a model with thermal
+                    # network configured; failures are warnings, EM results
+                    # remain valid. enable_thermal param overrides instance default.
+                    _thermal_on = (
+                        enable_thermal if enable_thermal is not None
+                        else self.enable_thermal
+                    )
+                    if _thermal_on:
+                        try:
+                            # P5-M6 fix (2026-09-04): pymotorcad has NO
+                            # do_thermal_calculation() method. The steady-state
+                            # thermal solve is do_steady_state_analysis().
+                            # Verified against ansys.motorcad.core sources.
+                            self.mc.do_steady_state_analysis()
+                            self._log("Steady-state thermal calculation completed")
+                        except Exception as _therr:  # noqa: BLE001
+                            self._log(
+                                f"WARNING: thermal calculation failed "
+                                f"(model may lack thermal network): {_therr}"
+                            )
+
+                    # Step 6: Export and parse
+                    raw_file = os.path.join(
+                        self.raw_dir,
+                        f"result_{point_index:04d}_{point_label or 'point'}_{datetime.now().strftime('%H%M%S')}.csv"
+                    )
+                    try:
+                        self.mc.export_results("EMagnetic", raw_file)
+                    except MotorCADError as e:
+                        raise RuntimeError(f"Results export failed: {e}")
+
+                    # Verify export file actually exists
+                    if not os.path.exists(raw_file):
+                        raise RuntimeError(f"Export file not created: {raw_file}")
+
+                    metrics = self._parse_export(raw_file)
+
+                    # Step 6b: Optional thermal export and metric merge (P5-M6)
+                    # Best-effort: thermal export section name may vary by
+                    # Motor-CAD version; failures do not invalidate EM metrics.
+                    if _thermal_on:
+                        try:
+                            _thermal_file = raw_file.replace(".csv", "_thermal.csv")
+                            # solution_type is "SteadyState" (NOT "Thermal").
+                            # Valid values: EMagnetic / Lab / SteadyState / Transient.
+                            self.mc.export_results("SteadyState", _thermal_file)
+                            if os.path.exists(_thermal_file):
+                                _thermal_metrics = self._parse_export(_thermal_file)
+                                metrics.update(_thermal_metrics)
+                                self._log(
+                                    "Thermal metrics merged: %s"
+                                    % sorted(_thermal_metrics.keys())
+                                )
+                        except Exception as _texerr:  # noqa: BLE001
+                            self._log(
+                                f"WARNING: thermal export/merge failed: {_texerr}"
+                            )
+
+                    result["metrics"] = metrics
+                    result["status"] = "OK"
+                    break
+
+                except MotorCADError as e:
+                    result["error"] = f"MotorCADError: {e}"
+                    self._log(f"Point {point_index} attempt {attempt+1} MotorCADError: {e}")
+                    if attempt < self.max_retries - 1:
+                        self._log(f"Retrying point {point_index}...")
+                        time.sleep(2)
+                        self._reconnect_if_needed()
+                    else:
+                        result["status"] = "FAILED"
+                except Exception as e:
+                    result["error"] = f"{type(e).__name__}: {e}"
+                    self._log(f"Point {point_index} attempt {attempt+1} failed: {e}")
+                    if attempt < self.max_retries - 1:
+                        self._log(f"Retrying point {point_index}...")
+                        time.sleep(2)
+                        self._reconnect_if_needed()
+                    else:
+                        result["status"] = "FAILED"
+
+        finally:
+            # Restore popup state (CRITICAL: must happen even on error)
+            self._restore_popup_state()
+            # Reload baseline to leave clean state
+            try:
+                if self.mc:
+                    self.mc.load_from_file(self.model_path)
+            except Exception:
+                pass
+
+        result["duration_s"] = round(time.time() - start_time, 2)
+        self._all_results.append(result)
+        self._write_result_to_disk(result)
+        return result
+
+    def _reconnect_if_needed(self) -> None:
+        """Check if instance is responsive, reconnect if not."""
+        if not self._check_connection_responsive():
+            self._log("Instance unresponsive, reconnecting...")
+            try:
+                self.disconnect()
+            except Exception:
+                pass
+            try:
+                self.connect()
+                self._suppress_popups()
+            except Exception as e:
+                self._log(f"Reconnection failed: {e}")
+
+    def read_graph_data(self, graph_name: str, max_points: int = 10000) -> List[Tuple[float, float]]:
+        """Read graph data using "out-of-bounds = end" idiom.
+
+        Motor-CAD API only exposes the most recently displayed curve.
+        Reading past the end throws MotorCADError, which we use as
+        the sequence termination signal.
+
+        Reference: Official doc section 3.1 point 2 - graph reading idiom.
+
+        Args:
+            graph_name: Name of the graph to read (check in Motor-CAD Help -> Graph Viewer)
+            max_points: Safety limit to prevent infinite loops
+
+        Returns:
+            List of (x, y) data points
+        """
+        points: List[Tuple[float, float]] = []
+        if not self.mc:
+            return points
+
+        try:
+            i = 0
+            while i < max_points:
+                try:
+                    x = self.mc.get_magnetic_graph_point(graph_name, i)
+                    # get_magnetic_graph_point may return tuple or single value
+                    if isinstance(x, (list, tuple)):
+                        points.append((float(x[0]), float(x[1])))
+                    else:
+                        # Single value return - use index as x
+                        points.append((float(i), float(x)))
+                    i += 1
+                except MotorCADError:
+                    # Out of bounds = end of data (official idiom)
+                    break
+                except Exception:
+                    break
+        except Exception as e:
+            self._log(f"Graph reading error: {e}")
+
+        return points
+
+    def _parse_export(self, filepath: str) -> Dict[str, float]:
+        """Parse Motor-CAD export CSV with normalized bilingual matching.
+
+        Delegates to the platform single source of truth
+        (src/afmcore/metrics.py), which applies full-width -> half-width
+        normalization. This fixes the historical bug where tavg_nm and
+        ripple_pct could not be matched due to invisible full-width chars
+        in exported field names.
+        """
+        if not os.path.exists(filepath):
+            return {}
+        return _platform_extract_all_metrics(_platform_parse_export(filepath))
+
+    def _write_result_to_disk(self, result: Dict[str, Any]) -> None:
+        """Write result to CSV and JSON immediately (flush + fsync)."""
+        if not self._csv_header_written:
+            header = ["point_index", "point_label", "status", "duration_s"]
+            for md in METRIC_DEFINITIONS:
+                header.append(md["key"])
+            if result["params"]:
+                for k in result["params"]:
+                    if k not in ("point_index", "point_label"):
+                        header.append(f"param_{k}")
+            with open(self._csv_path, "w", newline="", encoding="utf-8") as f:
+                writer = csv.writer(f, delimiter=";")
+                writer.writerow(header)
+                f.flush()
+                os.fsync(f.fileno())
+            self._csv_header_written = True
+
+        row = [result["point_index"], result["point_label"],
+               result["status"], result["duration_s"]]
+        for md in METRIC_DEFINITIONS:
+            row.append(result["metrics"].get(md["key"], ""))
+        if result["params"]:
+            for k, v in result["params"].items():
+                if k not in ("point_index", "point_label"):
+                    row.append(v)
+        with open(self._csv_path, "a", newline="", encoding="utf-8") as f:
+            writer = csv.writer(f, delimiter=";")
+            writer.writerow(row)
+            f.flush()
+            os.fsync(f.fileno())
+
+        with open(self._json_path, "w", encoding="utf-8") as f:
+            json.dump({"results": self._all_results}, f, ensure_ascii=False, indent=2)
+            f.flush()
+            os.fsync(f.fileno())
+
+    def _log(self, message: str) -> None:
+        """Write timestamped log message."""
+        ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
+        line = f"[{ts}] {message}\n"
+        try:
+            with open(self._log_path, "a", encoding="utf-8") as f:
+                f.write(line)
+                f.flush()
+        except Exception:
+            pass
+
+    def get_summary(self) -> Dict[str, Any]:
+        """Get run summary."""
+        ok = [r for r in self._all_results if r["status"] == "OK"]
+        failed = [r for r in self._all_results if r["status"] == "FAILED"]
+        return {
+            "total": len(self._all_results),
+            "ok": len(ok),
+            "failed": len(failed),
+            "output_dir": self.output_dir,
+            "csv_path": self._csv_path,
+            "json_path": self._json_path,
+            "log_path": self._log_path,
+            "headless_mode": self.headless,
+        }

+ 11 - 0
scripts/run_gui.py

@@ -0,0 +1,11 @@
+"""Launch the PCB axial flux motor simulation GUI."""
+import sys
+from pathlib import Path
+
+PROJECT_ROOT = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(PROJECT_ROOT))
+
+from src.gui.main import main
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 205 - 0
scripts/run_scan.py

@@ -0,0 +1,205 @@
+"""Command-line entry point for parameter scan.
+
+Usage:
+    python scripts/run_scan.py --config scan_config.json
+    python scripts/run_scan.py --model models/MARS.mot --var Airgap --start 0.6 --stop 1.5 --step 0.3
+
+All source is ASCII.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+# Add project root to sys.path
+PROJECT_ROOT = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(PROJECT_ROOT))
+
+from src.solver_core import MotorCADSolver  # noqa: E402
+from src.scan_engine import (  # noqa: E402
+    ScanEngine,
+    generate_cartesian_points,
+    values_inclusive,
+    estimate_total_time,
+)
+
+DEFAULT_MODEL = PROJECT_ROOT / "models" / "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot"
+DEFAULT_OUTPUT = PROJECT_ROOT / "output"
+
+
+def _log(text: str) -> None:
+    from datetime import datetime
+    stamp = datetime.now().strftime("%H:%M:%S")
+    print(f"[{stamp}] {text}", flush=True)
+
+
+def git_preflight(project_root: Path) -> tuple[bool, str]:
+    """Check that git repo exists, HEAD is valid, and tracked files are clean.
+    Returns (ok, commit_or_error)."""
+    try:
+        safe = f"safe.directory={project_root.as_posix()}"
+        base = ["git", "-c", safe]
+        commit = subprocess.check_output(
+            base + ["rev-parse", "--short", "HEAD"],
+            cwd=project_root, text=True, stderr=subprocess.STDOUT,
+        ).strip()
+        dirty = subprocess.check_output(
+            base + ["status", "--porcelain", "--untracked-files=no"],
+            cwd=project_root, text=True, stderr=subprocess.STDOUT,
+        ).strip()
+        if dirty:
+            return False, f"Tracked files have uncommitted changes:\n{dirty}"
+        return True, commit
+    except (OSError, subprocess.CalledProcessError) as exc:
+        return False, f"Git preflight failed: {exc}"
+
+
+def load_config(config_path: Path) -> dict:
+    """Load scan configuration from JSON file."""
+    with open(config_path, "r", encoding="utf-8") as f:
+        return json.load(f)
+
+
+def build_points_from_config(config: dict) -> tuple[list[dict], list[str]]:
+    """Build scan points from config. Returns (points, var_names)."""
+    variables = []
+    var_names = []
+    for v in config.get("variables", []):
+        name = v["name"]
+        var_names.append(name)
+        if "values" in v:
+            values = [float(x) for x in v["values"]]
+        else:
+            values = values_inclusive(
+                float(v["start"]), float(v["stop"]), float(v["step"])
+            )
+        variables.append({"name": name, "display_name": v.get("display_name", name), "values": values})
+
+    points = generate_cartesian_points(variables)
+    return points, var_names
+
+
+def build_points_from_args(args) -> tuple[list[dict], list[str]]:
+    """Build scan points from CLI arguments (single variable only)."""
+    values = values_inclusive(args.start, args.stop, args.step)
+    variables = [{"name": args.var, "display_name": args.var, "values": values}]
+    points = generate_cartesian_points(variables)
+    return points, [args.var]
+
+
+def main() -> int:
+    parser = argparse.ArgumentParser(description="Motor-CAD parameter scan")
+    parser.add_argument("--config", type=str, default=None,
+                        help="Path to scan config JSON")
+    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="Output directory")
+    parser.add_argument("--scan-name", type=str, default="scan",
+                        help="Name for this scan run")
+    parser.add_argument("--var", type=str, default=None,
+                        help="Variable name to scan (CLI mode, single var)")
+    parser.add_argument("--start", type=float, default=None,
+                        help="Start value (CLI mode)")
+    parser.add_argument("--stop", type=float, default=None,
+                        help="Stop value (CLI mode)")
+    parser.add_argument("--step", type=float, default=None,
+                        help="Step value (CLI mode)")
+    parser.add_argument("--quit", action="store_true",
+                        help="Close Motor-CAD after scan")
+    parser.add_argument("--skip-git-check", action="store_true",
+                        help="Skip git preflight check")
+    args = parser.parse_args()
+
+    # Git preflight
+    if not args.skip_git_check:
+        ok, msg = git_preflight(PROJECT_ROOT)
+        if not ok:
+            print(f"GIT PREFLIGHT FAILED: {msg}")
+            print("Commit all changes first, or use --skip-git-check to bypass.")
+            return 1
+        _log(f"Git preflight passed (commit {msg})")
+
+    # Build scan points
+    if args.config:
+        config = load_config(Path(args.config))
+        model_path = config.get("model_path", args.model)
+        scan_name = config.get("scan_name", args.scan_name)
+        points, var_names = build_points_from_config(config)
+    elif args.var and args.start is not None and args.stop is not None and args.step is not None:
+        model_path = args.model
+        scan_name = args.scan_name
+        points, var_names = build_points_from_args(args)
+    else:
+        print("ERROR: Provide either --config or --var/--start/--stop/--step")
+        return 1
+
+    model_path = Path(model_path)
+    if not model_path.exists():
+        print(f"ERROR: Model file not found: {model_path}")
+        return 1
+
+    # Estimate time
+    est = estimate_total_time(points)
+    _log("=" * 60)
+    _log(f"Scan: {scan_name}")
+    _log(f"Model: {model_path.name}")
+    _log(f"Variables: {var_names}")
+    _log(f"Total points: {est['count']}")
+    _log(f"Estimated time: {est['total_min']} min ({est['total_h']} h)")
+    _log("=" * 60)
+    _log("")
+
+    # Run
+    solver = MotorCADSolver(log_cb=_log)
+    result = {}
+
+    try:
+        solver.connect()
+        _log("")
+
+        engine = ScanEngine(
+            solver=solver,
+            model_path=model_path,
+            output_dir=args.output_dir,
+            scan_name=scan_name,
+            log_cb=_log,
+        )
+        result = engine.run(points, var_names=var_names)
+
+        _log("")
+        _log("=" * 60)
+        _log("SCAN COMPLETE")
+        _log("=" * 60)
+        s = result["summary"]
+        _log(f"OK: {s['ok']}  FAILED: {s['failed']}  SKIPPED: {s['skipped']}")
+        _log(f"Results CSV: {result['csv_path']}")
+        _log(f"Run directory: {result['run_dir']}")
+
+    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 __name__ == "__main__":
+    sys.exit(main())

+ 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())

+ 168 - 0
scripts/run_task_executor.py

@@ -0,0 +1,168 @@
+"""Entry point to start the local Motor-CAD task executor (P4-M2 / P5-M2).
+
+Usage:
+    python scripts/run_task_executor.py [--config path] [--instances N]
+                                        [--interval N] [--mock]
+                                        [--log-dir dir] [--log-level LVL]
+
+Configuration is loaded from executor_config.json (see executor_config.py
+for the lookup order and env-var overrides). This single entry point can
+launch N parallel executor instances via config["instances"] or --instances.
+
+NOTE: All strings must be ASCII only.
+"""
+import argparse
+import logging
+import os
+import sys
+import time
+from datetime import datetime
+
+_SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
+if _SCRIPTS_DIR not in sys.path:
+    sys.path.insert(0, _SCRIPTS_DIR)
+
+import executor_config  # noqa: E402
+from task_executor import MotorCADTaskExecutor  # noqa: E402
+
+
+def setup_logging(log_dir, level_name):
+    """Configure a file + console logger for the executor."""
+    os.makedirs(log_dir, exist_ok=True)
+    log_file = os.path.join(
+        log_dir, "executor_%s.log" % datetime.now().strftime("%Y%m%d_%H%M%S")
+    )
+    logging.basicConfig(
+        level=getattr(logging, level_name, logging.INFO),
+        format="%(asctime)s %(levelname)s %(message)s",
+        handlers=[
+            logging.FileHandler(log_file, encoding="utf-8"),
+            logging.StreamHandler(),
+        ],
+    )
+    return log_file
+
+
+def make_log_callback(kind):
+    """Return an executor callback that logs and echoes to console."""
+    logger = logging.getLogger("executor")
+
+    def _cb(*args):
+        msg = " ".join(str(a) for a in args)
+        if kind == "progress":
+            logger.info("%s", msg)
+        elif kind == "complete":
+            logger.info("%s", msg)
+        else:
+            logger.error("%s", msg)
+        print(msg, flush=True)
+
+    return _cb
+
+
+def build_executors(cfg):
+    """Create cfg['instances'] MotorCADTaskExecutor objects."""
+    multi = cfg["instances"] > 1
+    executors = []
+    for i in range(cfg["instances"]):
+        ex = MotorCADTaskExecutor(
+            web_base_url=cfg["web_base_url"],
+            model_path=cfg["model_path"],
+            enable_mock=cfg["enable_mock"],
+            tool=cfg["tool"],
+            enable_thermal=cfg["enable_thermal"],
+            executor_id=("motorcad-executor-%s" % i) if multi else None,
+            on_progress=make_log_callback("progress"),
+            on_complete=make_log_callback("complete"),
+            on_error=make_log_callback("error"),
+        )
+        executors.append(ex)
+    return executors
+
+
+def main():
+    parser = argparse.ArgumentParser(prog="pcb-afm-executor")
+    parser.add_argument("--version", action="store_true",
+                        help="print version and exit")
+    parser.add_argument("--self-test", action="store_true",
+                        help="run a mock single-point execution and exit "
+                             "(no web backend, no Motor-CAD)")
+    parser.add_argument("--config", default=None,
+                        help="path to executor_config.json")
+    parser.add_argument("--instances", type=int, default=None,
+                        help="parallel executor count (overrides config)")
+    parser.add_argument("--interval", type=int, default=None,
+                        help="poll interval seconds (overrides config)")
+    parser.add_argument("--mock", action="store_true",
+                        help="use mock solver (no Motor-CAD)")
+    parser.add_argument("--log-dir", default=None,
+                        help="log output dir (overrides config)")
+    parser.add_argument("--log-level", default=None,
+                        help="log level (overrides config)")
+    args = parser.parse_args()
+
+    if args.version:
+        print("PCB-AFM Executor 1.1.0 (platform P5-M2, configurable)")
+        return
+    if args.self_test:
+        import tempfile
+        from task_executor import TaskExecutor
+        _ex = TaskExecutor(task_dir=tempfile.mkdtemp(prefix="selftest_"),
+                           enable_mock=True)
+        _ex.dispatch_task = lambda tid: True
+        _captured = {}
+        _ex.on_complete = lambda tid, res, met: _captured.update(
+            {tid: (len(res), res[0].get("status") if res else None)})
+        _ex.execute_task({
+            "task_id": "selftest-1",
+            "parameters": [{"airgap_mm": 1.0, "point_id": 1}],
+        })
+        _n, _st = _captured.get("selftest-1", (0, None))
+        print("self-test OK: %d point(s), status=%s" % (_n, _st))
+        return
+
+    # Load configuration (CLI / env / config file / defaults).
+    cfg = executor_config.load_config(cli_path=args.config)
+    if args.instances is not None:
+        cfg["instances"] = args.instances
+    if args.interval is not None:
+        cfg["poll_interval"] = args.interval
+    if args.mock:
+        cfg["enable_mock"] = True
+    if args.log_dir:
+        cfg["log_dir"] = args.log_dir
+    if args.log_level:
+        cfg["log_level"] = args.log_level
+    executor_config.validate_config(cfg)
+
+    log_file = setup_logging(cfg["log_dir"], cfg["log_level"])
+    logger = logging.getLogger("executor")
+    logger.info("config source: %s", cfg["config_source"])
+    logger.info("model=%s", cfg["model_path"])
+    logger.info("web_base_url=%s", cfg["web_base_url"])
+    logger.info("instances=%s poll_interval=%s tool=%s mock=%s log=%s",
+                cfg["instances"], cfg["poll_interval"], cfg["tool"],
+                cfg["enable_mock"], log_file)
+
+    executors = build_executors(cfg)
+    threads = []
+    for ex in executors:
+        threads.append(ex.start_polling(interval=int(cfg["poll_interval"])))
+        logger.info("executor started: %s", ex.executor_id)
+
+    print("Task executor started. Model=%s" % cfg["model_path"], flush=True)
+    print("Web base URL: %s" % cfg["web_base_url"], flush=True)
+    print("Ctrl+C to stop.", flush=True)
+    try:
+        while any(t.is_alive() for t in threads):
+            time.sleep(1)
+    except KeyboardInterrupt:
+        for ex in executors:
+            ex.stop()
+            ex.cleanup()
+        logger.info("All executors stopped.")
+        print("Executor stopped.", flush=True)
+
+
+if __name__ == "__main__":
+    main()

+ 75 - 0
scripts/run_task_executor_parallel.py

@@ -0,0 +1,75 @@
+"""Entry point to start N local Motor-CAD task executors (P3-M3 / P5-M2).
+
+Configurable via executor_config.json (see executor_config.py). This
+entry is kept for back-compat with the original --instances/--interval/
+--mock CLI and delegates to the shared config loader + executor builder
+in run_task_executor.
+
+Run in background:
+    python scripts/run_task_executor_parallel.py --instances 3 --interval 5
+
+NOTE: All strings must be ASCII only.
+"""
+import argparse
+import os
+import sys
+import time
+
+_SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
+if _SCRIPTS_DIR not in sys.path:
+    sys.path.insert(0, _SCRIPTS_DIR)
+
+import executor_config  # noqa: E402
+from run_task_executor import setup_logging, build_executors  # noqa: E402
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Start N local Motor-CAD executors")
+    parser.add_argument("--instances", type=int, default=None,
+                        help="number of parallel executor instances "
+                             "(overrides config)")
+    parser.add_argument("--interval", type=int, default=None,
+                        help="poll interval in seconds (overrides config)")
+    parser.add_argument("--mock", action="store_true",
+                        help="use mock solver (no Motor-CAD) for testing")
+    parser.add_argument("--config", default=None,
+                        help="path to executor_config.json")
+    args = parser.parse_args()
+
+    cfg = executor_config.load_config(cli_path=args.config)
+    if args.instances is not None:
+        cfg["instances"] = args.instances
+    if args.interval is not None:
+        cfg["poll_interval"] = args.interval
+    if args.mock:
+        cfg["enable_mock"] = True
+    executor_config.validate_config(cfg)
+    if cfg["instances"] < 1:
+        parser.error("--instances must be >= 1")
+
+    log_file = setup_logging(cfg["log_dir"], cfg["log_level"])
+    print("N=%d executors. Model=%s" % (cfg["instances"], cfg["model_path"]),
+          flush=True)
+    print("Web base URL: %s" % cfg["web_base_url"], flush=True)
+    print("Log: %s" % log_file, flush=True)
+    print("Ctrl+C to stop.", flush=True)
+
+    executors = build_executors(cfg)
+    threads = []
+    for ex in executors:
+        threads.append(ex.start_polling(interval=int(cfg["poll_interval"])))
+        print("Executor started: %s" % ex.executor_id, flush=True)
+
+    try:
+        while any(t.is_alive() for t in threads):
+            time.sleep(1)
+    except KeyboardInterrupt:
+        for ex in executors:
+            ex.stop()
+            ex.cleanup()
+        print("All executors stopped.", flush=True)
+
+
+if __name__ == "__main__":
+    main()

+ 204 - 0
scripts/run_thermal.py

@@ -0,0 +1,204 @@
+"""Standalone Motor-CAD steady-state thermal simulation for the MARS model.
+
+Purpose: validate that the Motor-CAD thermal solver runs end-to-end on the
+MARS PCB axial flux motor, and capture the REAL thermal result field names
+so the metric aliases in src/afmcore/metrics.py can be tuned to match.
+
+Flow (mode=steady, default):
+    1. Ensure Motor-CAD environment (license + exe path fallback).
+    2. Connect to a new, visible Motor-CAD instance.
+    3. Load the MARS baseline model.
+    4. Run the electromagnetic calculation (losses are the thermal source).
+    5. Run the steady-state thermal analysis.
+    6. Export EM + thermal results (solution_type="SteadyState") and parse.
+
+Flow (mode=coupled):
+    Steps 1-3, then do_magnetic_thermal_calculation (EM + thermal in one
+    coupled call), then export and parse both EM and thermal results.
+
+Run with the venv python that has ansys-motorcad-core installed, e.g.:
+    <venv>/Scripts/python.exe scripts/run_thermal.py --mode steady
+    <venv>/Scripts/python.exe scripts/run_thermal.py --mode coupled
+
+All source is ASCII only.
+"""
+from __future__ import annotations
+
+import argparse
+import math
+import os
+import sys
+import time
+import traceback
+from datetime import datetime
+from pathlib import Path
+
+# Make the platform core importable (src/afmcore/metrics.py).
+_ROOT = Path(__file__).resolve().parent.parent
+_SRC = _ROOT / "src"
+if str(_SRC) not in sys.path:
+    sys.path.insert(0, str(_SRC))
+
+from afmcore.metrics import parse_export, extract_all_metrics  # noqa: E402
+
+MODEL_PATH = _ROOT / "models" / "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot"
+
+_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",
+]
+
+# Workload for the EM run (matches the MARS baseline documented in
+# KNOWLEDGE_BASE section 3): RMS phase current 21 A, shaft speed 5000 rpm.
+WORKLOAD = {
+    "RMSCurrent": 21.0,
+    "Shaft_Speed": 5000.0,
+}
+
+
+def log(msg: str) -> None:
+    print(msg, flush=True)
+
+
+def ensure_environment() -> None:
+    """Set Motor-CAD env vars (non-login shell trap, see KNOWLEDGE_BASE 1)."""
+    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)
+                    log("MOTORCAD_ACTIVEX unset; fallback to %s" % candidate)
+                    break
+        except Exception:
+            pass
+    if not os.environ.get("ANSYSLMD_LICENSE_FILE"):
+        os.environ["ANSYSLMD_LICENSE_FILE"] = "1055@localhost"
+
+
+def write_and_verify(mc, variable: str, value: float) -> None:
+    """Write a variable and read it back; raise on mismatch (AGENTS rule 4)."""
+    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(
+            "Write verification failed for %s: wrote %s, read %s"
+            % (variable, value, applied)
+        )
+
+
+def main(mode: str = "steady") -> int:
+    ensure_environment()
+
+    import ansys.motorcad.core as pymotorcad
+
+    output_dir = _ROOT / "output" / (
+        "thermal_validation_" + datetime.now().strftime("%Y%m%d_%H%M%S")
+    )
+    raw_dir = output_dir / "raw"
+    raw_dir.mkdir(parents=True, exist_ok=True)
+    log("Mode: %s" % mode)
+    log("Output dir: %s" % output_dir)
+
+    log("Connecting to a new visible 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")
+    time.sleep(2)
+    log("Connected.")
+
+    try:
+        log("Loading model: %s" % MODEL_PATH)
+        mc.load_from_file(str(MODEL_PATH))
+
+        for var, val in WORKLOAD.items():
+            try:
+                write_and_verify(mc, var, val)
+                log("  %s = %s (verified)" % (var, val))
+            except Exception as exc:  # noqa: BLE001
+                log("  WARNING: %s write failed: %s" % (var, exc))
+
+        if mode == "coupled":
+            # Magnetic-thermal coupled solve: EM + thermal in one call.
+            log("Running magnetic-thermal coupled calculation ...")
+            t0 = time.time()
+            mc.do_magnetic_thermal_calculation()
+            log("Coupled solve done in %.1f s" % (time.time() - t0))
+        else:
+            log("Running electromagnetic calculation (losses = thermal source) ...")
+            t0 = time.time()
+            mc.do_magnetic_calculation()
+            log("EM done in %.1f s" % (time.time() - t0))
+
+            log("Running steady-state thermal analysis ...")
+            t0 = time.time()
+            mc.do_steady_state_analysis()
+            log("Thermal steady-state done in %.1f s" % (time.time() - t0))
+
+        em_raw = raw_dir / "emagnetic.csv"
+        mc.export_results("EMagnetic", str(em_raw))
+        log("EM results exported: %s" % em_raw)
+
+        thermal_raw = raw_dir / "thermal_steadystate.csv"
+        mc.export_results("SteadyState", str(thermal_raw))
+        log("Thermal results exported: %s" % thermal_raw)
+
+        parsed = parse_export(thermal_raw)
+        metrics = extract_all_metrics(parsed)
+
+        # Print the key thermal metrics (not the full field dump, which is
+        # only needed when tuning aliases; keep output compact).
+        log("")
+        log("=== Extracted thermal metrics ===")
+        thermal_keys = [
+            "winding_temp_c", "winding_hotspot_temp_c", "magnet_temp_c",
+            "stator_temp_c", "bearing_temp_c", "temp_rise_c",
+            "thermal_resistance_k_w",
+        ]
+        for key in thermal_keys:
+            if key in metrics:
+                log("    %s = %s" % (key, metrics[key]))
+
+        # Also report the EM metrics for coupled-vs-steady comparison.
+        em_parsed = parse_export(em_raw)
+        em_metrics = extract_all_metrics(em_parsed)
+        log("")
+        log("=== Extracted EM metrics (for comparison) ===")
+        for key in ["tavg_nm", "ripple_pct", "total_losses_w", "efficiency_pct"]:
+            if key in em_metrics:
+                log("    %s = %s" % (key, em_metrics[key]))
+
+        log("")
+        log("Thermal validation finished. Output dir: %s" % output_dir)
+        return 0
+    finally:
+        try:
+            mc.load_from_file(str(MODEL_PATH))
+        except Exception:  # noqa: BLE001
+            pass
+        try:
+            mc.quit()
+        except Exception:  # noqa: BLE001
+            pass
+        log("Motor-CAD instance closed.")
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(
+        description="Motor-CAD thermal validation for the MARS model."
+    )
+    parser.add_argument(
+        "--mode",
+        choices=["steady", "coupled"],
+        default="steady",
+        help="steady = EM then steady-state thermal (default); "
+             "coupled = do_magnetic_thermal_calculation (EM+thermal in one).",
+    )
+    args = parser.parse_args()
+    try:
+        sys.exit(main(mode=args.mode))
+    except Exception:
+        traceback.print_exc()
+        sys.exit(1)

+ 12 - 0
scripts/scan_airgap.json

@@ -0,0 +1,12 @@
+{
+  "scan_name": "airgap_validation",
+  "model_path": "models/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot",
+  "variables": [
+    {
+      "name": "Airgap",
+      "display_name": "Airgap",
+      "unit": "mm",
+      "values": [0.6, 1.0, 1.5]
+    }
+  ]
+}

+ 598 - 0
scripts/task_executor.py

@@ -0,0 +1,598 @@
+"""Task executor for local simulation system (P4-M2).
+
+Listens for tasks dispatched from Web backend, executes Motor-CAD
+simulations via RobustMotorCADSolver, reports progress and results.
+
+NOTE: All strings must be ASCII only. Chinese text uses \\uXXXX escapes.
+"""
+import json
+import os
+import sys
+import time
+import uuid
+import threading
+from datetime import datetime
+from pathlib import Path
+from typing import Dict, List, Optional, Any, Callable
+
+try:
+    import requests
+except ImportError:
+    requests = None
+
+# Add scripts directory to path for robust_motorcad import
+_SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
+if _SCRIPTS_DIR not in sys.path:
+    sys.path.insert(0, _SCRIPTS_DIR)
+
+
+class TaskExecutor:
+    """Executes simulation tasks dispatched from Web backend."""
+
+    def __init__(
+        self,
+        web_base_url: str = "http://127.0.0.1:8000",
+        task_dir: Optional[str] = None,
+        on_progress: Optional[Callable] = None,
+        on_complete: Optional[Callable] = None,
+        on_error: Optional[Callable] = None,
+        enable_mock: bool = False,
+        executor_id: Optional[str] = None,
+    ):
+        self.web_base_url = web_base_url.rstrip("/")
+        self.task_dir = task_dir or os.path.join(
+            os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+            "output", "tasks"
+        )
+        os.makedirs(self.task_dir, exist_ok=True)
+        self.on_progress = on_progress
+        self.on_complete = on_complete
+        self.on_error = on_error
+        self.enable_mock = enable_mock
+        self._running = False
+        self._current_task: Optional[Dict[str, Any]] = None
+        self._stop_event = threading.Event()
+        if executor_id is not None:
+            self.executor_id = executor_id
+        else:
+            self.executor_id = "motorcad-executor-%s-%s" % (os.getpid(), uuid.uuid4().hex[:4])
+
+    def fetch_pending_tasks(self) -> List[Dict[str, Any]]:
+        """Fetch claimable tasks from Web backend.
+
+        Web's start-simulation marks tasks as 'dispatched' immediately, while
+        tasks created via the tasks API stay 'pending'. The executor claims
+        BOTH states so every task gets picked up regardless of creation path.
+        """
+        if requests is None:
+            return self._scan_local_task_files()
+        tasks = []
+        for st in ("pending", "dispatched"):
+            try:
+                resp = requests.get(
+                    f"{self.web_base_url}/api/tasks",
+                    params={"status": st, "limit": 20},
+                    timeout=10,
+                )
+                if resp.status_code == 200:
+                    data = resp.json()
+                    tasks.extend(data.get("tasks", []))
+            except Exception as e:
+                if self.on_error:
+                    self.on_error(f"Fetch tasks failed: {str(e)}")
+        # De-duplicate by task_id (keep first occurrence)
+        seen = set()
+        result = []
+        for t in tasks:
+            tid = t.get("task_id")
+            if tid and tid not in seen:
+                seen.add(tid)
+                result.append(t)
+        return result
+
+    def _hydrate_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
+        """Fetch full task payload (parameters + plan_data) from Web backend.
+
+        The list API only returns task metadata; the actual parameter sets
+        live in the task.json file exposed by the download endpoint.
+        """
+        if requests is None:
+            return task
+        tid = task.get("task_id")
+        if not tid:
+            return task
+        try:
+            resp = requests.get(
+                f"{self.web_base_url}/api/tasks/{tid}/download",
+                timeout=10,
+            )
+            if resp.status_code == 200:
+                full = resp.json()
+                if isinstance(full, dict):
+                    if full.get("parameters"):
+                        task = {**task, **full}
+                    elif task.get("_local_file"):
+                        # local file fallback
+                        try:
+                            with open(task["_local_file"], "r", encoding="utf-8") as f:
+                                local = json.load(f)
+                            if local.get("parameters"):
+                                task = {**task, **local}
+                        except Exception:
+                            pass
+        except Exception as e:
+            if self.on_error:
+                self.on_error(f"Hydrate task {tid} failed: {str(e)}")
+        return task
+
+    def _scan_local_task_files(self) -> List[Dict[str, Any]]:
+        """Scan local task directory for task files (fallback mode).
+
+        Only picks up *_task.json files. Completed tasks are renamed
+        to *_task.done.json to prevent infinite re-execution (B7 fix).
+        """
+        tasks = []
+        for fname in os.listdir(self.task_dir):
+            if fname.endswith("_task.json") and not fname.endswith("_task.done.json"):
+                fpath = os.path.join(self.task_dir, fname)
+                try:
+                    with open(fpath, "r", encoding="utf-8") as f:
+                        task = json.load(f)
+                    task["_local_file"] = fpath
+                    tasks.append(task)
+                except Exception:
+                    continue
+        return tasks
+
+    def _mark_local_task_done(self, task: Dict[str, Any]) -> None:
+        """Rename completed local task file to prevent re-execution (B7 fix)."""
+        fpath = task.get("_local_file")
+        if fpath and os.path.exists(fpath):
+            done_path = fpath.replace("_task.json", "_task.done.json")
+            try:
+                os.rename(fpath, done_path)
+            except Exception as e:
+                if self.on_error:
+                    self.on_error(f"Failed to mark task done: {str(e)}")
+
+    def dispatch_task(self, task_id: str) -> bool:
+        """Mark task as dispatched on Web backend."""
+        if requests is None:
+            return True
+        try:
+            resp = requests.post(
+                f"{self.web_base_url}/api/tasks/{task_id}/dispatch",
+                timeout=10,
+            )
+            return resp.status_code in (200, 201)
+        except Exception as e:
+            if self.on_error:
+                self.on_error(f"Dispatch task {task_id} failed: {str(e)}")
+            return False
+
+    def report_progress(
+        self,
+        task_id: str,
+        current_point: int,
+        total_points: int,
+        current_params: Optional[Dict[str, Any]] = None,
+        elapsed_time: Optional[float] = None,
+    ) -> bool:
+        """Report simulation progress to Web backend."""
+        if requests is None:
+            if self.on_progress:
+                self.on_progress(task_id, current_point, total_points)
+            return True
+        try:
+            payload = {
+                "current_point": current_point,
+                "total_points": total_points,
+                "current_params": current_params,
+                "elapsed_time": elapsed_time,
+            }
+            resp = requests.post(
+                f"{self.web_base_url}/api/tasks/{task_id}/progress",
+                json=payload,
+                timeout=10,
+            )
+            return resp.status_code == 200
+        except Exception as e:
+            if self.on_error:
+                self.on_error(f"Report progress failed: {str(e)}")
+            return False
+
+    def report_results(
+        self,
+        task_id: str,
+        results: List[Dict[str, Any]],
+        metrics: Optional[Dict[str, Any]] = None,
+        logs: Optional[str] = None,
+        duration: Optional[float] = None,
+        status: str = "completed",
+    ) -> bool:
+        """Report final results to Web backend."""
+        if requests is None:
+            if self.on_complete:
+                self.on_complete(task_id, results, metrics)
+            return True
+        try:
+            payload = {
+                "results": results,
+                "metrics": metrics,
+                "logs": logs,
+                "duration": duration,
+                "status": status,
+            }
+            resp = requests.post(
+                f"{self.web_base_url}/api/tasks/{task_id}/results",
+                json=payload,
+                timeout=30,
+            )
+            return resp.status_code == 200
+        except Exception as e:
+            if self.on_error:
+                self.on_error(f"Report results failed: {str(e)}")
+            return False
+
+    def _report_to_adaptive_loop(
+        self, task: Dict[str, Any], results: List[Dict[str, Any]]
+    ) -> None:
+        """Feed an adaptive_batch task's results back into its loop.
+
+        Maps each point result to {point_id, metrics, status} and posts to the
+        loop's report-results endpoint. Best-effort: a failure here must not
+        break normal task reporting (results are already stored on the task).
+        """
+        if requests is None:
+            return
+        if task.get("task_type") != "adaptive_batch":
+            return
+        loop_id = task.get("loop_id")
+        if not loop_id:
+            return
+        point_results = []
+        for r in results:
+            pid = r.get("point_id")
+            if pid is None:
+                continue
+            point_results.append({
+                "point_id": pid,
+                "metrics": r.get("metrics") or {},
+                "status": "ok" if r.get("status") == "OK" else "failed",
+            })
+        if not point_results:
+            return
+        try:
+            resp = requests.post(
+                f"{self.web_base_url}/api/adaptive/loops/{loop_id}/report-results",
+                json={"point_results": point_results},
+                timeout=120,
+            )
+            if self.on_progress:
+                self.on_progress(
+                    f"Adaptive loop {loop_id}: reported {len(point_results)} "
+                    f"points (HTTP {resp.status_code})"
+                )
+        except Exception as e:
+            if self.on_error:
+                self.on_error(f"Adaptive loop report failed ({loop_id}): {str(e)}")
+
+    def execute_task(self, task: Dict[str, Any]) -> None:
+        """Execute a single simulation task.
+
+        This is a template method. Override _run_simulation_point in
+        subclasses to implement actual Motor-CAD simulation.
+        """
+        task = self._hydrate_task(task)
+        task_id = task.get("task_id", str(uuid.uuid4())[:8])
+        parameters = task.get("parameters", [])
+        total_points = len(parameters)
+        results = []
+        start_time = time.time()
+
+        self._current_task = task
+        # start-simulation already marks a task 'dispatched' at creation, while
+        # tasks created via the tasks API stay 'pending'. Only claim (dispatch)
+        # a task that is still pending; re-dispatching an already-dispatched
+        # task is rejected by the backend (pending -> dispatched only) and
+        # would otherwise be misreported as "not claimable".
+        claimed = True
+        if task.get("status") == "pending":
+            claimed = self.dispatch_task(task_id)
+        if not claimed:
+            # Another instance already claimed this task; skip it so
+            # parallel executors never duplicate the same simulation.
+            if self.on_error:
+                self.on_error("Task %s not claimable (claimed/network); skip" % task_id)
+            self._current_task = None
+            return
+
+        for idx, params in enumerate(parameters):
+            if self._stop_event.is_set():
+                break
+
+            elapsed = time.time() - start_time
+            self.report_progress(task_id, idx, total_points, params, elapsed)
+
+            try:
+                point_result = self._run_simulation_point(params, idx)
+                point_result["point_index"] = idx
+                if "point_id" in params:
+                    point_result["point_id"] = params["point_id"]
+                point_result["params"] = params
+                results.append(point_result)
+            except Exception as e:
+                # A2 fix: failed points are recorded as failed, NOT mock data
+                failed_result = {
+                    "point_index": idx,
+                    "params": params,
+                    "status": "FAILED",
+                    "error": str(e),
+                }
+                if "point_id" in params:
+                    failed_result["point_id"] = params["point_id"]
+                results.append(failed_result)
+                if self.on_error:
+                    self.on_error(f"Point {idx} failed: {str(e)}")
+
+        duration = time.time() - start_time
+        metrics = self._compute_metrics(results)
+        # Status reflects actual outcome: completed/cancelled/failed
+        if self._stop_event.is_set():
+            status = "cancelled"
+        elif any(r.get("status") == "FAILED" for r in results):
+            status = "completed_with_errors" if any(
+                r.get("status") == "OK" for r in results
+            ) else "failed"
+        else:
+            status = "completed"
+
+        self.report_results(task_id, results, metrics, None, duration, status)
+        self.report_progress(task_id, total_points, total_points, None, duration)
+
+        # Adaptive-loop bridge: an adaptive_batch task belongs to a loop; feed
+        # per-point results back to /adaptive/loops/{loop_id}/report-results so
+        # the search advances without manual intervention (P3-M5 gap closure).
+        self._report_to_adaptive_loop(task, results)
+
+        # B7 fix: mark local task file as done to prevent re-execution
+        if requests is None:
+            self._mark_local_task_done(task)
+
+        self._current_task = None
+        if self.on_complete:
+            self.on_complete(task_id, results, metrics)
+
+    def _run_simulation_point(self, params: Dict[str, Any], index: int) -> Dict[str, Any]:
+        """Run a single simulation point. Override in subclass.
+
+        Mock data is ONLY returned when enable_mock=True (explicit opt-in).
+        Mock results are tagged with source="mock" so they can never be
+        confused with real simulation data (A2 fix).
+        """
+        if not self.enable_mock:
+            raise RuntimeError(
+                "No simulation backend configured. "
+                "Use MotorCADTaskExecutor for real Motor-CAD simulation, "
+                "or set enable_mock=True for testing."
+            )
+        import random
+        rng = random.Random(index + hash(json.dumps(params, sort_keys=True)) % 10000)
+        airgap = params.get("airgap_mm", 1.0)
+        current = params.get("current_a", 15.0)
+        return {
+            "tavg_nm": round(current * 2.5 / (airgap ** 0.5) + rng.gauss(0, 0.3), 4),
+            "efficiency_pct": round(88 + rng.gauss(0, 2), 2),
+            "total_losses_w": round(50 + rng.gauss(0, 10), 2),
+            "winding_temp_c": round(90 + rng.gauss(0, 10), 1),
+            "status": "OK",
+            "source": "mock",
+        }
+
+    def _compute_metrics(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
+        """Compute aggregated metrics from results."""
+        ok_results = [r for r in results if r.get("status") == "OK"]
+        if not ok_results:
+            return {
+                "total_points": len(results),
+                "successful_points": 0,
+                "failed_points": len(results),
+            }
+        metrics = {
+            "total_points": len(results),
+            "successful_points": len(ok_results),
+            "failed_points": len(results) - len(ok_results),
+        }
+        for key in ["tavg_nm", "efficiency_pct", "total_losses_w", "winding_temp_c"]:
+            values = [r[key] for r in ok_results if key in r]
+            if values:
+                metrics[f"{key}_min"] = min(values)
+                metrics[f"{key}_max"] = max(values)
+                metrics[f"{key}_mean"] = round(sum(values) / len(values), 4)
+        return metrics
+
+    def _send_heartbeat(self) -> None:
+        """Register this executor with the Web backend (online status)."""
+        if requests is None:
+            return
+        try:
+            status = "running" if self._current_task is not None else "idle"
+            current_task = None
+            progress = None
+            if self._current_task is not None:
+                current_task = self._current_task.get("task_id")
+                total = self._current_task.get("total_points") or 0
+                done = self._current_task.get("completed_points") or 0
+                progress = {
+                    "completed_points": done,
+                    "total_points": total,
+                }
+            requests.post(
+                f"{self.web_base_url}/api/executor/heartbeat",
+                json={
+                    "executor_id": self.executor_id,
+                    "status": status,
+                    "current_task": current_task,
+                    "progress": progress,
+                },
+                timeout=5,
+            )
+        except Exception:
+            # Heartbeat failures are non-fatal
+            pass
+
+    def start_polling(self, interval: int = 5) -> threading.Thread:
+        """Start background threads: one polls/executes tasks, one heartbeats.
+
+        Heartbeat runs on its own thread so a long-running Motor-CAD point
+        (~2 min each) never starves the heartbeat - otherwise the backend
+        would mark this executor offline mid-task (observed 2026-09-04).
+        """
+        self._running = True
+        self._stop_event.clear()
+
+        def heartbeat_loop():
+            while self._running and not self._stop_event.is_set():
+                self._send_heartbeat()
+                self._stop_event.wait(interval)
+
+        def poll_loop():
+            while self._running and not self._stop_event.is_set():
+                try:
+                    tasks = self.fetch_pending_tasks()
+                    for task in tasks:
+                        if self._stop_event.is_set():
+                            break
+                        self.execute_task(task)
+                except Exception as e:
+                    if self.on_error:
+                        self.on_error(f"Poll loop error: {str(e)}")
+                self._stop_event.wait(interval)
+
+        hb_thread = threading.Thread(target=heartbeat_loop, daemon=True)
+        hb_thread.start()
+        thread = threading.Thread(target=poll_loop, daemon=True)
+        thread.start()
+        return thread
+
+    def stop(self):
+        """Stop the executor."""
+        self._running = False
+        self._stop_event.set()
+
+
+class MotorCADTaskExecutor(TaskExecutor):
+    """Task executor backed by a registered simulation-tool adapter.
+
+    Uses afmcore.adapters.get_adapter(tool) so the executor never
+    hard-codes a specific solver. Default tool "motorcad" wraps
+    RobustMotorCADSolver (open_new_instance, set_visible, baseline
+    reload per point, popup suppression, write-back verification,
+    per-point disk flush).
+
+    Result mapping: adapter returns {metrics, status, error, ...}; the
+    metrics dict is flattened to the point's top level so downstream
+    aggregation (TaskExecutor._compute_metrics) keeps working unchanged.
+    """
+
+    def __init__(self, *args, model_path: Optional[str] = None,
+                         tool: str = "motorcad",
+                         enable_thermal: bool = False, **kwargs):
+        # Mock fallback is disabled by default for real solver adapter.
+        kwargs.setdefault("enable_mock", False)
+        super().__init__(*args, **kwargs)
+        self.model_path = model_path
+        self.tool = tool
+        # P5-M6: pass through to the adapter so each EM point can also run a
+        # steady-state thermal solve and merge thermal metrics.
+        self.enable_thermal = bool(enable_thermal)
+        self._adapter = None
+
+    def _ensure_adapter(self):
+        """Lazily create the tool adapter via the platform registry."""
+        if self._adapter is not None:
+            return self._adapter
+        _root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+        _src = os.path.join(_root, "src")
+        if _src not in sys.path:
+            sys.path.insert(0, _src)
+        from afmcore.adapters import get_adapter
+        # Dynamically import the adapter module matching self.tool so it
+        # self-registers in ADAPTER_REGISTRY. Unknown tools rely on
+        # pre-registered adapters (caller may have imported them).
+        if self.tool == "motorcad":
+            import afmcore.adapters.motorcad  # noqa: F401
+        elif self.tool == "maxwell":
+            import afmcore.adapters.maxwell  # noqa: F401
+        elif self.tool == "jmag":
+            import afmcore.adapters.jmag  # noqa: F401
+        if not self.model_path:
+            raise RuntimeError("model_path is required for MotorCADTaskExecutor")
+        output_dir = os.path.join(
+            _root, "output", "task_%s" % datetime.now().strftime("%Y%m%d_%H%M%S")
+        )
+        self._adapter = get_adapter(
+            self.tool, model_path=self.model_path, output_dir=output_dir,
+            enable_thermal=self.enable_thermal,
+        )
+        self._adapter.connect()
+        return self._adapter
+
+    def _run_simulation_point(self, params: Dict[str, Any], index: int) -> Dict[str, Any]:
+        """Run one point through the adapter and flatten metrics to top level.
+
+        The adapter owns the robust protocol (baseline reload, write-back
+        verification, export parsing). A non-OK point raises so execute_task
+        records status=FAILED (no mock fallback).
+
+        When enable_mock=True the base-class mock implementation is used
+        instead, so no Motor-CAD instance is launched at all (P5-M2).
+        """
+        if self.enable_mock:
+            return super()._run_simulation_point(params, index)
+        adapter = self._ensure_adapter()
+        result = adapter.run_point(
+            self.model_path, params=params,
+            output_dir=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+            tag=str(index),
+        )
+        if result.get("status") != "OK":
+            raise RuntimeError(
+                result.get("error") or ("Simulation failed (adapter status=%s)"
+                % result.get("status"))
+            )
+        metrics = result.get("metrics") or {}
+        point = dict(metrics)
+        point["status"] = "OK"
+        point["metrics"] = metrics
+        point["error"] = result.get("error")
+        point["solve_time_s"] = result.get("solve_time_s")
+        return point
+
+    def cleanup(self):
+        """Disconnect the adapter and release the tool instance."""
+        if self._adapter is not None:
+            try:
+                self._adapter.disconnect()
+            except Exception:
+                pass
+            self._adapter = None
+
+
+if __name__ == "__main__":
+    # Standalone test: run executor with mock data (explicit)
+    executor = TaskExecutor(
+        web_base_url=os.environ.get("WEB_BASE_URL", "http://127.0.0.1:8000"),
+        on_progress=lambda tid, cur, tot: print(f"[{tid}] Progress: {cur}/{tot}"),
+        on_complete=lambda tid, res, met: print(f"[{tid}] Complete: {len(res)} points"),
+        on_error=lambda msg: print(f"ERROR: {msg}"),
+        enable_mock=True,
+    )
+    print("Task executor started (mock mode). Press Ctrl+C to stop.")
+    try:
+        thread = executor.start_polling(interval=5)
+        while thread.is_alive():
+            time.sleep(1)
+    except KeyboardInterrupt:
+        executor.stop()
+        print("Executor stopped.")

+ 214 - 0
scripts/test_adapters.py

@@ -0,0 +1,214 @@
+"""P5-M5: unit tests for simulation tool adapters (maxwell / jmag mock).
+
+Covers: registry (register/get/listed), maxwell mock full pipeline,
+jmag mock full pipeline, tool differentiation (same params -> different
+metrics), set_parameter read-back verification, boundary (empty params /
+unknown tool / run before connect), anomaly (real-mode RuntimeError),
+and executor dynamic adapter import by tool name.
+
+All source is ASCII only. Run: python scripts/test_adapters.py
+exit 0 = PASS.
+"""
+import os
+import sys
+import unittest
+
+_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+_SRC = os.path.join(_ROOT, "src")
+if _SRC not in sys.path:
+    sys.path.insert(0, _SRC)
+
+from afmcore.adapters import (  # noqa: E402
+    SimulationAdapter,
+    get_adapter,
+    register_adapter,
+    registered_tools,
+)
+import afmcore.adapters.maxwell  # noqa: E402,F401  (registers "maxwell")
+import afmcore.adapters.jmag  # noqa: E402,F401  (registers "jmag")
+
+
+class TestAdapterRegistry(unittest.TestCase):
+    def test_maxwell_registered(self):
+        self.assertIn("maxwell", registered_tools())
+
+    def test_jmag_registered(self):
+        self.assertIn("jmag", registered_tools())
+
+    def test_get_adapter_returns_instance(self):
+        a = get_adapter("maxwell", model_path="/tmp/x.mot", mock=True)
+        self.assertIsInstance(a, SimulationAdapter)
+        self.assertEqual(a.tool_name, "maxwell")
+
+    def test_unknown_tool_raises_keyerror(self):
+        with self.assertRaises(KeyError):
+            get_adapter("nonexistent_tool_xyz")
+
+    def test_register_adapter_rejects_non_string(self):
+        with self.assertRaises(ValueError):
+            register_adapter("", SimulationAdapter)
+
+
+class TestMaxwellMockPipeline(unittest.TestCase):
+    def setUp(self):
+        self.adapter = get_adapter(
+            "maxwell", model_path="/tmp/test.mot",
+            output_dir="output/p5m5_test", mock=True,
+        )
+
+    def test_connect_disconnect(self):
+        self.adapter.connect()
+        self.assertTrue(self.adapter._connected)
+        self.adapter.disconnect()
+        self.assertFalse(self.adapter._connected)
+
+    def test_load_model_clears_params(self):
+        self.adapter._params["old"] = 1.0
+        self.adapter.load_model("/tmp/new.mot")
+        self.assertEqual(self.adapter._loaded_model, "/tmp/new.mot")
+        self.assertEqual(self.adapter._params, {})
+
+    def test_set_parameter_readback(self):
+        self.adapter.connect()
+        self.adapter.set_parameter("airgap_mm", 1.5)
+        self.assertEqual(self.adapter._params["airgap_mm"], 1.5)
+
+    def test_run_simulation_before_connect_raises(self):
+        with self.assertRaises(RuntimeError):
+            self.adapter.run_simulation("electromagnetic")
+
+    def test_run_simulation_unsupported_mode_raises(self):
+        self.adapter.connect()
+        with self.assertRaises(ValueError):
+            self.adapter.run_simulation("structural")
+
+    def test_extract_metrics_deterministic(self):
+        self.adapter.connect()
+        self.adapter.set_parameter("airgap_mm", 1.0)
+        self.adapter.set_parameter("magnet_thickness_mm", 5.0)
+        ext = self.adapter.extract_metrics("output/p5m5_test", "t1")
+        self.assertEqual(ext["status"], "OK")
+        # tavg = 0.50*1.0 + 0.30*5.0 + 0.10 = 2.10
+        self.assertAlmostEqual(ext["metrics"]["tavg_nm"], 2.10, places=4)
+        self.assertAlmostEqual(ext["metrics"]["efficiency_pct"], 85.10, places=2)
+        self.assertIn("winding_temp_c", ext["metrics"])  # thermal domain
+
+    def test_run_point_full_pipeline(self):
+        result = self.adapter.run_point(
+            model_path="/tmp/test.mot",
+            params={"airgap_mm": 1.0, "magnet_thickness_mm": 5.0},
+            output_dir="output/p5m5_test",
+            tag="full",
+        )
+        self.assertEqual(result["status"], "OK")
+        self.assertIn("tavg_nm", result["metrics"])
+        self.assertEqual(result["params"]["airgap_mm"], 1.0)
+
+    def test_run_point_empty_params(self):
+        result = self.adapter.run_point(
+            model_path="/tmp/test.mot", params={},
+            output_dir="output/p5m5_test", tag="empty",
+        )
+        self.assertEqual(result["status"], "OK")
+        # all params default to 0 -> tavg = 0.10
+        self.assertAlmostEqual(result["metrics"]["tavg_nm"], 0.10, places=4)
+
+    def test_real_mode_raises(self):
+        a = get_adapter("maxwell", model_path="/tmp/x.mot", mock=False)
+        with self.assertRaises(RuntimeError):
+            a.connect()
+
+
+class TestJMAGMockPipeline(unittest.TestCase):
+    def setUp(self):
+        self.adapter = get_adapter(
+            "jmag", model_path="/tmp/test.jmag",
+            output_dir="output/p5m5_test", mock=True,
+        )
+
+    def test_tool_label_and_domains(self):
+        self.assertEqual(self.adapter.tool_label, "JMAG Designer (mock)")
+        self.assertEqual(self.adapter.capability_domains, ("electromagnetic",))
+
+    def test_extract_metrics_deterministic(self):
+        self.adapter.connect()
+        self.adapter.set_parameter("airgap_mm", 1.0)
+        self.adapter.set_parameter("magnet_thickness_mm", 5.0)
+        ext = self.adapter.extract_metrics("output/p5m5_test", "t1")
+        # tavg = 0.45*1.0 + 0.32*5.0 + 0.12 = 2.17
+        self.assertAlmostEqual(ext["metrics"]["tavg_nm"], 2.17, places=4)
+        self.assertNotIn("winding_temp_c", ext["metrics"])  # jmag has no thermal domain
+
+    def test_run_point_full_pipeline(self):
+        result = self.adapter.run_point(
+            model_path="/tmp/test.jmag",
+            params={"airgap_mm": 2.0, "magnet_thickness_mm": 4.0},
+            output_dir="output/p5m5_test", tag="full",
+        )
+        self.assertEqual(result["status"], "OK")
+        # tavg = 0.45*2.0 + 0.32*4.0 + 0.12 = 0.90+1.28+0.12 = 2.30
+        self.assertAlmostEqual(result["metrics"]["tavg_nm"], 2.30, places=4)
+
+    def test_unsupported_mode_raises(self):
+        self.adapter.connect()
+        with self.assertRaises(ValueError):
+            self.adapter.run_simulation("thermal")
+
+
+class TestToolDifferentiation(unittest.TestCase):
+    """Same input params must produce different metrics for maxwell vs jmag."""
+
+    def test_same_params_different_tavg(self):
+        params = {"airgap_mm": 1.0, "magnet_thickness_mm": 5.0}
+        mx = get_adapter("maxwell", mock=True)
+        jm = get_adapter("jmag", mock=True)
+        r_mx = mx.run_point("/tmp/x.mot", params, "output/p5m5_test", "diff")
+        r_jm = jm.run_point("/tmp/x.jmag", params, "output/p5m5_test", "diff")
+        self.assertNotEqual(r_mx["metrics"]["tavg_nm"], r_jm["metrics"]["tavg_nm"])
+        self.assertNotEqual(r_mx["metrics"]["efficiency_pct"], r_jm["metrics"]["efficiency_pct"])
+
+
+class TestExecutorDynamicImport(unittest.TestCase):
+    """MotorCADTaskExecutor must dynamically import the adapter matching tool."""
+
+    def test_executor_tool_maxwell_creates_maxwell_adapter(self):
+        sys.path.insert(0, _ROOT)
+        from scripts.task_executor import MotorCADTaskExecutor
+        ex = MotorCADTaskExecutor(
+            web_base_url="http://127.0.0.1:9",
+            model_path="/tmp/test.mot",
+            tool="maxwell",
+            enable_mock=False,
+        )
+        adapter = ex._ensure_adapter()
+        self.assertEqual(adapter.tool_name, "maxwell")
+        self.assertIn("maxwell", registered_tools())
+
+    def test_executor_tool_jmag_creates_jmag_adapter(self):
+        sys.path.insert(0, _ROOT)
+        from scripts.task_executor import MotorCADTaskExecutor
+        ex = MotorCADTaskExecutor(
+            web_base_url="http://127.0.0.1:9",
+            model_path="/tmp/test.jmag",
+            tool="jmag",
+            enable_mock=False,
+        )
+        adapter = ex._ensure_adapter()
+        self.assertEqual(adapter.tool_name, "jmag")
+        self.assertIn("jmag", registered_tools())
+
+    def test_executor_tool_unknown_raises_keyerror(self):
+        sys.path.insert(0, _ROOT)
+        from scripts.task_executor import MotorCADTaskExecutor
+        ex = MotorCADTaskExecutor(
+            web_base_url="http://127.0.0.1:9",
+            model_path="/tmp/test.mot",
+            tool="nonexistent_tool_xyz",
+            enable_mock=False,
+        )
+        with self.assertRaises(KeyError):
+            ex._ensure_adapter()
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)

+ 33 - 0
scripts/test_api_client.py

@@ -0,0 +1,33 @@
+"""Quick test for API client."""
+import sys
+sys.path.insert(0, ".")
+from src.api_client import WebAPIClient
+
+client = WebAPIClient("http://127.0.0.1:8000")
+
+print("Health:", client.health_check())
+
+projects = client.list_projects()
+print(f"Projects: {len(projects)}")
+for p in projects:
+    print(f"  #{p['id']}: {p['name']} ({p['topology']})")
+
+plans = client.list_plans(project_id=1)
+print(f"Plans for project 1: {len(plans)}")
+for pl in plans:
+    print(f"  #{pl['id']}: {pl['name']} ({pl['plan_id']}) status={pl['status']}")
+
+if plans:
+    plan = client.download_plan(plans[0]["id"])
+    print(f"Downloaded plan: {plan['plan_id']}")
+    vars_list = [v["name"] for v in plan["plan_data"].get("variables", [])]
+    print(f"  Variables: {vars_list}")
+
+    results = client.get_plan_results(plans[0]["id"])
+    print(f"Results: {len(results)}")
+    for r in results[:2]:
+        tavg = r["metrics"].get("tavg_nm")
+        eff = r["metrics"].get("efficiency_pct")
+        print(f"  #{r['run_index']}: params={r['params']}, tavg={tavg}, eff={eff}")
+
+print("API client test PASSED")

+ 222 - 0
scripts/test_executor_config.py

@@ -0,0 +1,222 @@
+"""Unit tests for scripts/executor_config.py (P5-M2).
+
+Run: python scripts/test_executor_config.py
+Covers happy path / boundary / abnormal / empty-value cases for the
+executor config loader and validator. exit 0 == PASS.
+
+NOTE: All strings in this file are ASCII only.
+"""
+import json
+import os
+import sys
+import tempfile
+import unittest
+from unittest import mock
+
+_SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
+if _SCRIPTS_DIR not in sys.path:
+    sys.path.insert(0, _SCRIPTS_DIR)
+
+import executor_config  # noqa: E402
+
+
+class ExecutorConfigTest(unittest.TestCase):
+    """Test config load priority, resolution and validation."""
+
+    def setUp(self):
+        self._keep = dict(os.environ)
+        for key in ("EXECUTOR_CONFIG", "WEB_BASE_URL", "MOTORCAD_MODEL",
+                    "EXECUTOR_INSTANCES", "EXECUTOR_POLL_INTERVAL",
+                    "EXECUTOR_LOG_DIR", "EXECUTOR_LOG_LEVEL",
+                    "EXECUTOR_TOOL", "EXECUTOR_MOCK"):
+            os.environ.pop(key, None)
+        self._tmp = tempfile.mkdtemp(prefix="exec_cfg_test_")
+
+    def tearDown(self):
+        os.environ.clear()
+        os.environ.update(self._keep)
+        import shutil
+        shutil.rmtree(self._tmp, ignore_errors=True)
+
+    def _isolate(self):
+        """Point _base_dir/repo_root at an empty temp dir so the real
+        repo-root sidecar config cannot influence tests."""
+        return (
+            mock.patch.object(executor_config, "_base_dir",
+                              return_value=self._tmp),
+            mock.patch.object(executor_config, "repo_root",
+                              return_value=self._tmp),
+        )
+
+    def _write(self, name, obj):
+        path = os.path.join(self._tmp, name)
+        with open(path, "w", encoding="utf-8") as fh:
+            json.dump(obj, fh)
+        return path
+
+    # ---------- happy path ----------
+    def test_defaults_when_no_file_and_no_env(self):
+        with self._isolate()[0], self._isolate()[1]:
+            cfg = executor_config.load_config()
+        self.assertEqual(cfg["web_base_url"], "http://127.0.0.1:8000")
+        self.assertEqual(cfg["instances"], 1)
+        self.assertEqual(cfg["poll_interval"], 5)
+        self.assertEqual(cfg["log_level"], "INFO")
+        self.assertEqual(cfg["tool"], "motorcad")
+        self.assertIs(cfg["enable_mock"], False)
+        self.assertEqual(cfg["config_source"], "defaults")
+        self.assertTrue(os.path.isabs(cfg["model_path"]))
+        self.assertTrue(cfg["model_path"].startswith(self._tmp))
+        self.assertTrue(cfg["log_dir"].startswith(self._tmp))
+
+    def test_file_load_merges_and_resolves(self):
+        path = self._write("executor_config.json", {
+            "web_base_url": "http://192.168.1.10:9000",
+            "model_path": "models/custom.mot",
+            "instances": 3,
+            "poll_interval": 2,
+            "log_dir": "logs/exec",
+            "log_level": "DEBUG",
+            "tool": "motorcad",
+            "enable_mock": True,
+        })
+        cfg = executor_config.load_config(cli_path=path)
+        self.assertEqual(cfg["web_base_url"], "http://192.168.1.10:9000")
+        self.assertEqual(cfg["instances"], 3)
+        self.assertEqual(cfg["poll_interval"], 2)
+        self.assertEqual(cfg["log_level"], "DEBUG")
+        self.assertIs(cfg["enable_mock"], True)
+        self.assertEqual(cfg["config_source"], path)
+        root = executor_config.repo_root()
+        expected_model = os.path.normpath(os.path.join("models", "custom.mot"))
+        self.assertTrue(cfg["model_path"].endswith(expected_model))
+        self.assertTrue(cfg["model_path"].startswith(root))
+        self.assertTrue(cfg["log_dir"].startswith(root))
+
+    def test_absolute_paths_kept(self):
+        path = self._write("executor_config.json", {
+            "model_path": "D:/models/abs.mot",
+            "log_dir": "D:/logs",
+        })
+        cfg = executor_config.load_config(cli_path=path)
+        self.assertEqual(cfg["model_path"], os.path.normpath("D:/models/abs.mot"))
+        self.assertEqual(cfg["log_dir"], os.path.normpath("D:/logs"))
+
+    # ---------- env overrides ----------
+    def test_env_overrides_file_and_cli(self):
+        path = self._write("executor_config.json", {
+            "web_base_url": "http://file:8000",
+            "instances": 2,
+            "poll_interval": 7,
+        })
+        os.environ["WEB_BASE_URL"] = "http://env:9999"
+        os.environ["MOTORCAD_MODEL"] = "models/env.mot"
+        os.environ["EXECUTOR_INSTANCES"] = "4"
+        os.environ["EXECUTOR_MOCK"] = "true"
+        cfg = executor_config.load_config(cli_path=path)
+        self.assertEqual(cfg["web_base_url"], "http://env:9999")
+        self.assertEqual(cfg["instances"], 4)
+        self.assertEqual(cfg["poll_interval"], 7)  # file value kept
+        self.assertIs(cfg["enable_mock"], True)
+
+    def test_env_numeric_invalid(self):
+        os.environ["EXECUTOR_INSTANCES"] = "abc"
+        with self.assertRaises(ValueError):
+            executor_config.load_config()
+
+    # ---------- find_config_path priority ----------
+    def test_find_config_path_cli_first(self):
+        a = self._write("a.json", {"instances": 1})
+        b = self._write("b.json", {"instances": 1})
+        os.environ["EXECUTOR_CONFIG"] = b
+        self.assertEqual(executor_config.find_config_path(cli_path=a), a)
+
+    def test_find_config_path_env_fallback(self):
+        b = self._write("b.json", {"instances": 1})
+        os.environ["EXECUTOR_CONFIG"] = b
+        self.assertEqual(executor_config.find_config_path(), b)
+
+    def test_find_config_path_none(self):
+        with self._isolate()[0], self._isolate()[1]:
+            self.assertIsNone(
+                executor_config.find_config_path("no/such/file.json"))
+
+    # ---------- boundary ----------
+    def test_minimal_instances_ok(self):
+        cfg = executor_config.load_config()
+        self.assertGreaterEqual(cfg["instances"], 1)
+
+    def test_float_poll_interval_ok(self):
+        path = self._write("executor_config.json", {"poll_interval": 0.5})
+        cfg = executor_config.load_config(cli_path=path)
+        self.assertEqual(cfg["poll_interval"], 0.5)
+
+    def test_empty_model_path_allowed(self):
+        path = self._write("executor_config.json", {"model_path": ""})
+        cfg = executor_config.load_config(cli_path=path)
+        self.assertIsNone(cfg["model_path"])
+
+    # ---------- abnormal ----------
+    def test_invalid_json_raises(self):
+        path = os.path.join(self._tmp, "bad.json")
+        with open(path, "w", encoding="utf-8") as fh:
+            fh.write("{ not json !!!")
+        with self.assertRaises(ValueError):
+            executor_config.load_config(cli_path=path)
+
+    def test_non_dict_json_raises(self):
+        path = self._write("arr.json", [1, 2, 3])
+        with self.assertRaises(ValueError):
+            executor_config.load_config(cli_path=path)
+
+    def test_validate_bad_url(self):
+        with self.assertRaises(ValueError):
+            executor_config.validate_config({
+                "web_base_url": "ftp://x", "instances": 1,
+                "poll_interval": 1, "log_level": "INFO",
+                "tool": "motorcad", "enable_mock": False})
+
+    def test_validate_zero_instances(self):
+        with self.assertRaises(ValueError):
+            executor_config.validate_config({
+                "web_base_url": "http://x", "instances": 0,
+                "poll_interval": 1, "log_level": "INFO",
+                "tool": "motorcad", "enable_mock": False})
+
+    def test_validate_bad_level(self):
+        with self.assertRaises(ValueError):
+            executor_config.validate_config({
+                "web_base_url": "http://x", "instances": 1,
+                "poll_interval": 1, "log_level": "VERBOSE",
+                "tool": "motorcad", "enable_mock": False})
+
+    def test_validate_bad_mock_type(self):
+        with self.assertRaises(ValueError):
+            executor_config.validate_config({
+                "web_base_url": "http://x", "instances": 1,
+                "poll_interval": 1, "log_level": "INFO",
+                "tool": "motorcad", "enable_mock": "yes"})
+
+    # ---------- empty values ----------
+    def test_empty_file_object_uses_defaults(self):
+        path = self._write("empty.json", {})
+        cfg = executor_config.load_config(cli_path=path)
+        self.assertEqual(cfg["web_base_url"], "http://127.0.0.1:8000")
+        self.assertEqual(cfg["instances"], 1)
+
+    def test_null_file_field_ignored(self):
+        path = self._write("null.json", {"web_base_url": None, "instances": 2})
+        cfg = executor_config.load_config(cli_path=path)
+        self.assertEqual(cfg["web_base_url"], "http://127.0.0.1:8000")
+        self.assertEqual(cfg["instances"], 2)
+
+    def test_empty_env_ignored(self):
+        os.environ["WEB_BASE_URL"] = ""
+        os.environ["EXECUTOR_INSTANCES"] = ""
+        cfg = executor_config.load_config()
+        self.assertEqual(cfg["web_base_url"], "http://127.0.0.1:8000")
+        self.assertEqual(cfg["instances"], 1)
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)

+ 78 - 0
scripts/test_executor_m3.py

@@ -0,0 +1,78 @@
+# One-off: P3-M3 regression - executor point_id passthrough + claim check.
+import json
+import os
+import sys
+import tempfile
+
+_SCRIPTS = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, _SCRIPTS)
+
+import task_executor as te  # noqa: E402
+
+# force local-file mode (no web calls)
+te.requests = None
+
+tmp = tempfile.mkdtemp()
+captured = {}
+
+
+def make_task(tid, params):
+    path = os.path.join(tmp, tid + "_task.json")
+    with open(path, "w", encoding="utf-8") as f:
+        json.dump({"task_id": tid, "task_name": "m3", "parameters": params}, f)
+    return {"task_id": tid, "_local_file": path, "parameters": params}
+
+
+# ---------------- 1) point_id passthrough (claim succeeds) ----------------
+ex = te.TaskExecutor(task_dir=tmp, enable_mock=True)
+ex.dispatch_task = lambda tid: True  # claim ok
+ex.on_complete = lambda tid, res, met: captured.setdefault(tid, res)
+task = make_task("t1", [{"airgap_mm": 1.0, "current_a": 10.0, "point_id": 7},
+                        {"airgap_mm": 2.0, "current_a": 5.0, "point_id": 8}])
+ex.execute_task(task)
+res1 = captured.get("t1")
+assert res1 and len(res1) == 2, res1
+for r in res1:
+    assert r["status"] == "OK", r
+    assert r["point_id"] in (7, 8), r
+    assert r["params"].get("point_id") == r["point_id"], r
+print("[1] point_id passthrough OK:", [(r["point_id"], r["status"]) for r in res1])
+
+# ---------------- 2) FAILED point keeps point_id ----------------
+captured2 = {}
+
+
+def _boom(params, idx):
+    raise RuntimeError("simulation failed")
+
+
+ex2 = te.TaskExecutor(task_dir=tmp, enable_mock=True)
+ex2.dispatch_task = lambda tid: True
+ex2._run_simulation_point = _boom
+ex2.on_complete = lambda tid, res, met: captured2.setdefault(tid, res)
+task2 = make_task("t2", [{"airgap_mm": 1.0, "point_id": 42}])
+ex2.execute_task(task2)
+r2 = captured2["t2"][0]
+assert r2["status"] == "FAILED", r2
+assert r2["point_id"] == 42, r2
+print("[2] FAILED point keeps point_id OK:", r2["point_id"], r2["status"])
+
+# ---------------- 3) claim rejected -> task skipped ----------------
+captured3 = {}
+ex3 = te.TaskExecutor(task_dir=tmp, enable_mock=True)
+ex3.dispatch_task = lambda tid: False  # already claimed by another instance
+ex3.on_complete = lambda tid, res, met: captured3.setdefault(tid, res)
+task3 = make_task("t3", [{"airgap_mm": 1.0, "point_id": 99}])
+ex3.execute_task(task3)
+assert "t3" not in captured3, "claimed task must be skipped"
+print("[3] claim-rejected task skipped OK")
+
+# ---------------- 4) unique executor ids ----------------
+a = te.TaskExecutor(task_dir=tmp)
+b = te.TaskExecutor(task_dir=tmp)
+c = te.TaskExecutor(task_dir=tmp, executor_id="motorcad-executor-9")
+assert a.executor_id != b.executor_id, (a.executor_id, b.executor_id)
+assert c.executor_id == "motorcad-executor-9"
+print("[4] unique executor ids OK:", a.executor_id, "|", b.executor_id, "|", c.executor_id)
+
+print("\nALL P3-M3 EXECUTOR TESTS PASSED")

+ 122 - 0
scripts/test_executor_p5m2.py

@@ -0,0 +1,122 @@
+"""P5-M2 regression: MotorCADTaskExecutor mock-vs-real branch.
+
+Run: python scripts/test_executor_p5m2.py
+Verifies that enable_mock=True routes to the base-class mock solver (no
+Motor-CAD, no model_path required) and that enable_mock=False keeps the
+real-adapter contract (model_path required). exit 0 == PASS.
+
+NOTE: All strings in this file are ASCII only.
+"""
+import json
+import os
+import sys
+import tempfile
+import unittest
+
+_SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
+if _SCRIPTS_DIR not in sys.path:
+    sys.path.insert(0, _SCRIPTS_DIR)
+
+import task_executor as te  # noqa: E402
+
+# local-file mode: no real web calls during the test
+te.requests = None
+
+
+def _make_local_task(tmp, tid, params):
+    path = os.path.join(tmp, tid + "_task.json")
+    with open(path, "w", encoding="utf-8") as fh:
+        json.dump({"task_id": tid, "task_name": "p5m2",
+                   "parameters": params}, fh)
+    return {"task_id": tid, "_local_file": path, "parameters": params}
+
+
+class MotorCADExecutorMockBranchTest(unittest.TestCase):
+    """Exercise the enable_mock branch added in P5-M2."""
+
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp(prefix="p5m2_exec_")
+
+    def tearDown(self):
+        import shutil
+        shutil.rmtree(self.tmp, ignore_errors=True)
+
+    def _run(self, executor, tid, params):
+        executor.dispatch_task = lambda t: True
+        captured = {}
+        executor.on_complete = lambda t, res, met: captured.setdefault(t, res)
+        executor.execute_task(_make_local_task(self.tmp, tid, params))
+        return captured.get(tid)
+
+    # ---------- happy path ----------
+    def test_mock_point_source_and_no_model_required(self):
+        ex = te.MotorCADTaskExecutor(task_dir=self.tmp, enable_mock=True,
+                                     model_path=None)
+        res = self._run(ex, "m1", [{"airgap_mm": 1.0, "point_id": 1}])
+        self.assertEqual(len(res), 1)
+        self.assertEqual(res[0]["status"], "OK")
+        self.assertEqual(res[0]["source"], "mock")
+        self.assertIn("tavg_nm", res[0])
+
+    def test_mock_multi_point(self):
+        ex = te.MotorCADTaskExecutor(task_dir=self.tmp, enable_mock=True,
+                                     model_path=None)
+        res = self._run(ex, "m2", [
+            {"airgap_mm": 0.8, "point_id": 1},
+            {"airgap_mm": 1.0, "point_id": 2},
+            {"airgap_mm": 1.2, "point_id": 3},
+        ])
+        self.assertEqual(len(res), 3)
+        for r in res:
+            self.assertEqual(r["status"], "OK")
+            self.assertEqual(r["source"], "mock")
+
+    # ---------- boundary ----------
+    def test_mock_with_illegal_model_path_still_mocks(self):
+        # mock must win even when model_path points at a nonexistent file,
+        # proving no Motor-CAD instance / adapter is launched.
+        ex = te.MotorCADTaskExecutor(task_dir=self.tmp, enable_mock=True,
+                                     model_path="Z:/nonexistent/model.mot")
+        res = self._run(ex, "m3", [{"airgap_mm": 1.0, "point_id": 1}])
+        self.assertEqual(res[0]["source"], "mock")
+
+    def test_default_mock_off(self):
+        # P4-M3 default: real solver adapter, so enable_mock defaults False
+        ex = te.MotorCADTaskExecutor(task_dir=self.tmp, model_path=None)
+        self.assertIs(ex.enable_mock, False)
+
+    # ---------- abnormal ----------
+    def test_real_mode_requires_model_path(self):
+        ex = te.MotorCADTaskExecutor(task_dir=self.tmp, enable_mock=False,
+                                     model_path=None)
+        with self.assertRaises(RuntimeError):
+            ex._run_simulation_point({"airgap_mm": 1.0}, 0)
+
+    # ---------- metadata key exclusion ----------
+    def test_point_id_excluded_from_motorcad_params(self):
+        # Regression (P5-M2 real E2E): point_id is a passthrough
+        # metadata key and must never be sent to Motor-CAD set_variable.
+        src_path = os.path.join(_SCRIPTS_DIR, "robust_motorcad.py")
+        with open(src_path, "r", encoding="utf-8") as fh:
+            src = fh.read()
+        self.assertIn(
+            'if var in ("point_index", "point_label", "point_id"):',
+            src)
+
+    # ---------- business alias ----------
+    def test_business_alias_airgap_resolves(self):
+        from robust_motorcad import resolve_variable_name
+        self.assertEqual(resolve_variable_name("airgap_mm"), "Airgap")
+        # unknown canonical names pass through untouched
+        self.assertEqual(resolve_variable_name("SomeUnknown"), "SomeUnknown")
+
+    # ---------- empty value ----------
+    def test_mock_empty_model_path_string(self):
+        ex = te.MotorCADTaskExecutor(task_dir=self.tmp, enable_mock=True,
+                                     model_path="")
+        res = self._run(ex, "m4", [{"airgap_mm": 1.0, "point_id": 1}])
+        self.assertEqual(res[0]["source"], "mock")
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)

+ 241 - 0
scripts/test_metrics_extension.py

@@ -0,0 +1,241 @@
+"""P5-M6: unit tests for metrics extension (thermal + structural) and
+domain-grouped report generation.
+
+Covers:
+- New thermal/structural metrics present in METRIC_DEFINITIONS with domain
+- parse_export + extract_all_metrics picks up new metrics automatically
+- check_required_metrics unaffected (new metrics required=False)
+- report_generator._group_metrics_by_domain correct grouping
+- report_generator JSON fallback includes metrics_by_domain
+- Boundary: empty metrics, unknown keys, mixed domains
+- robust_motorcad enable_thermal parameter exists (signature check)
+
+All source is ASCII only. Run: python scripts/test_metrics_extension.py
+exit 0 = PASS.
+"""
+import os
+import sys
+import tempfile
+import unittest
+
+_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+_SRC = os.path.join(_ROOT, "src")
+if _SRC not in sys.path:
+    sys.path.insert(0, _SRC)
+
+from afmcore.metrics import (  # noqa: E402
+    METRIC_DEFINITIONS,
+    METRIC_KEYS,
+    REQUIRED_METRICS,
+    check_required_metrics,
+    extract_all_metrics,
+    parse_export,
+)
+
+# report_generator lives under web/backend; add its dir to path
+_REPORT_DIR = os.path.join(_ROOT, "web", "backend", "app", "services")
+if _REPORT_DIR not in sys.path:
+    sys.path.insert(0, _REPORT_DIR)
+from report_generator import (  # noqa: E402
+    ReportGenerator,
+    _DOMAIN_ORDER,
+    _group_metrics_by_domain,
+    _metric_display,
+)
+
+
+THERMAL_KEYS = [
+    "winding_hotspot_temp_c", "magnet_temp_c", "stator_temp_c",
+    "bearing_temp_c", "temp_rise_c", "thermal_resistance_k_w",
+]
+STRUCTURAL_KEYS = [
+    "axial_force_n", "radial_force_n", "max_stress_mpa", "deformation_mm",
+]
+
+
+class TestMetricDefinitions(unittest.TestCase):
+    def test_thermal_metrics_present(self):
+        keys = {m["key"] for m in METRIC_DEFINITIONS}
+        for k in THERMAL_KEYS:
+            self.assertIn(k, keys, "missing thermal metric: %s" % k)
+
+    def test_structural_metrics_present(self):
+        keys = {m["key"] for m in METRIC_DEFINITIONS}
+        for k in STRUCTURAL_KEYS:
+            self.assertIn(k, keys, "missing structural metric: %s" % k)
+
+    def test_thermal_metrics_have_domain(self):
+        for m in METRIC_DEFINITIONS:
+            if m["key"] in THERMAL_KEYS:
+                self.assertEqual(m.get("domain"), "thermal",
+                                 "%s should have domain=thermal" % m["key"])
+
+    def test_structural_metrics_have_domain(self):
+        for m in METRIC_DEFINITIONS:
+            if m["key"] in STRUCTURAL_KEYS:
+                self.assertEqual(m.get("domain"), "structural",
+                                 "%s should have domain=structural" % m["key"])
+
+    def test_new_metrics_not_required(self):
+        for k in THERMAL_KEYS + STRUCTURAL_KEYS:
+            self.assertNotIn(k, REQUIRED_METRICS,
+                             "%s should be required=False" % k)
+
+    def test_total_metric_count(self):
+        # original 25 + 6 thermal + 4 structural = 35
+        self.assertEqual(len(METRIC_DEFINITIONS), 35)
+
+
+class TestMetricExtraction(unittest.TestCase):
+    """Construct a mock Motor-CAD export CSV with thermal/structural fields
+    and verify extract_all_metrics picks them up automatically."""
+
+    def _make_export(self, fields):
+        """Write a mock semicolon-CSV export and return its path."""
+        lines = ["E-Magnetics"]
+        for field, value in fields:
+            lines.append("%s;%s" % (field, value))
+        fd, path = tempfile.mkstemp(suffix=".csv")
+        with os.fdopen(fd, "w", encoding="utf-8") as f:
+            f.write("\n".join(lines))
+        self.addCleanup(os.unlink, path)
+        return path
+
+    def test_extract_thermal_metrics(self):
+        path = self._make_export([
+            ("Average torque (virtual work)", "1.5"),
+            ("Magnet Temperature", "62.5"),
+            ("Winding Hotspot Temperature", "88.3"),
+            ("Stator Temperature", "70.1"),
+            ("Bearing Temperature", "55.0"),
+            ("Temperature Rise", "48.2"),
+            ("Thermal Resistance", "0.85"),
+        ])
+        parsed = parse_export(path)
+        metrics = extract_all_metrics(parsed)
+        self.assertAlmostEqual(metrics["magnet_temp_c"], 62.5, places=2)
+        self.assertAlmostEqual(metrics["winding_hotspot_temp_c"], 88.3, places=2)
+        self.assertAlmostEqual(metrics["stator_temp_c"], 70.1, places=2)
+        self.assertAlmostEqual(metrics["bearing_temp_c"], 55.0, places=2)
+        self.assertAlmostEqual(metrics["temp_rise_c"], 48.2, places=2)
+        self.assertAlmostEqual(metrics["thermal_resistance_k_w"], 0.85, places=2)
+
+    def test_extract_structural_metrics(self):
+        path = self._make_export([
+            ("Axial Force", "125.5"),
+            ("Radial Force", "45.2"),
+            ("Maximum Stress", "180.3"),
+            ("Max Deformation", "0.12"),
+        ])
+        parsed = parse_export(path)
+        metrics = extract_all_metrics(parsed)
+        self.assertAlmostEqual(metrics["axial_force_n"], 125.5, places=2)
+        self.assertAlmostEqual(metrics["radial_force_n"], 45.2, places=2)
+        self.assertAlmostEqual(metrics["max_stress_mpa"], 180.3, places=2)
+        self.assertAlmostEqual(metrics["deformation_mm"], 0.12, places=2)
+
+    def test_chinese_alias_thermal(self):
+        path = self._make_export([
+            ("\u6c38\u78c1\u4f53\u6e29\u5ea6", "70.0"),  # magnet temp
+            ("\u8f74\u5411\u529b", "200.0"),  # axial force
+        ])
+        parsed = parse_export(path)
+        metrics = extract_all_metrics(parsed)
+        self.assertAlmostEqual(metrics["magnet_temp_c"], 70.0, places=2)
+        self.assertAlmostEqual(metrics["axial_force_n"], 200.0, places=2)
+
+    def test_required_check_unaffected(self):
+        # Only tavg/ripple/efficiency/total_losses are required;
+        # missing thermal/structural metrics must not fail the check.
+        ok, missing = check_required_metrics({"tavg_nm": 1.0, "ripple_pct": 2.0,
+                                                "efficiency_pct": 90.0, "total_losses_w": 10.0})
+        self.assertTrue(ok)
+        self.assertEqual(missing, [])
+
+    def test_required_check_fails_on_missing_core(self):
+        ok, missing = check_required_metrics({"tavg_nm": 1.0})
+        self.assertFalse(ok)
+        self.assertIn("ripple_pct", missing)
+
+
+class TestDomainGrouping(unittest.TestCase):
+    def test_group_by_domain(self):
+        metrics = {
+            "tavg_nm": 1.5, "ripple_pct": 5.0,  # electromagnetic (default)
+            "magnet_temp_c": 60.0, "temp_rise_c": 40.0,  # thermal
+            "axial_force_n": 100.0, "max_stress_mpa": 150.0,  # structural
+        }
+        grouped = _group_metrics_by_domain(metrics)
+        self.assertIn("electromagnetic", grouped)
+        self.assertIn("thermal", grouped)
+        self.assertIn("structural", grouped)
+        self.assertEqual(set(grouped["thermal"].keys()), {"magnet_temp_c", "temp_rise_c"})
+        self.assertEqual(set(grouped["structural"].keys()), {"axial_force_n", "max_stress_mpa"})
+
+    def test_empty_metrics(self):
+        self.assertEqual(_group_metrics_by_domain({}), {})
+
+    def test_unknown_key_defaults_electromagnetic(self):
+        grouped = _group_metrics_by_domain({"unknown_metric_xyz": 42.0})
+        self.assertIn("electromagnetic", grouped)
+        self.assertEqual(grouped["electromagnetic"]["unknown_metric_xyz"], 42.0)
+
+    def test_domain_order(self):
+        self.assertEqual(_DOMAIN_ORDER, ["electromagnetic", "thermal", "structural"])
+
+    def test_metric_display(self):
+        label, value = _metric_display("tavg_nm", 1.5)
+        self.assertIn("Average Torque", label)
+        self.assertEqual(value, "1.5")
+
+    def test_metric_display_float_format(self):
+        _, value = _metric_display("efficiency_pct", 92.3456789)
+        self.assertEqual(value, "92.35")  # %.4g
+
+
+class TestReportJsonFallback(unittest.TestCase):
+    def test_json_report_includes_metrics_by_domain(self):
+        rg = ReportGenerator(output_dir=tempfile.mkdtemp())
+        task_data = {
+            "task_id": "test-001",
+            "task_name": "Test Task",
+            "status": "completed",
+            "result_metrics": {
+                "tavg_nm": 1.5,
+                "magnet_temp_c": 60.0,
+                "axial_force_n": 100.0,
+            },
+        }
+        path = rg._generate_json_report(task_data, None, None)
+        self.addCleanup(os.unlink, path)
+        import json
+        with open(path, "r", encoding="utf-8") as f:
+            report = json.load(f)
+        self.assertIn("metrics_by_domain", report)
+        self.assertIn("thermal", report["metrics_by_domain"])
+        self.assertIn("structural", report["metrics_by_domain"])
+        self.assertEqual(report["metrics_by_domain"]["thermal"]["magnet_temp_c"], 60.0)
+
+
+class TestRobustMotorcadThermalParam(unittest.TestCase):
+    """Verify enable_thermal parameter exists in RobustMotorCADSolver."""
+
+    def test_init_has_enable_thermal(self):
+        sys.path.insert(0, _ROOT)
+        from scripts.robust_motorcad import RobustMotorCADSolver
+        import inspect
+        sig = inspect.signature(RobustMotorCADSolver.__init__)
+        self.assertIn("enable_thermal", sig.parameters)
+        self.assertFalse(sig.parameters["enable_thermal"].default)
+
+    def test_run_single_point_has_enable_thermal(self):
+        sys.path.insert(0, _ROOT)
+        from scripts.robust_motorcad import RobustMotorCADSolver
+        import inspect
+        sig = inspect.signature(RobustMotorCADSolver.run_single_point)
+        self.assertIn("enable_thermal", sig.parameters)
+        self.assertIsNone(sig.parameters["enable_thermal"].default)
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)

+ 159 - 0
scripts/test_p3_adaptive_execution.py

@@ -0,0 +1,159 @@
+"""P3-M6 regression: web AdaptiveLoop closed loop + executor bridge.
+
+Validates the complete adaptive closed loop on the web side (AdaptiveLoop),
+including the new submit_batch_to_executor bridge that wraps the current
+pending batch into an adaptive_batch task for the local executor:
+
+    fake plan -> initialize_search (initial batch)
+    -> submit_batch_to_executor (create task)
+    -> fake executor runs the task and reports results
+    -> report_results feeds the search back
+    -> get_next_batch -> ... -> until budget exhausted / converged
+
+Run:  python scripts/test_p3_adaptive_execution.py   (exit 0 = PASS)
+
+Uses an isolated temp SQLite DB and KIMI_API_KEY="" so no AI backend is
+contacted. No real Motor-CAD is involved.
+"""
+import json
+import os
+import sys
+import tempfile
+
+_TMP = os.path.join(tempfile.mkdtemp(), "test_afm.db")
+os.environ["AFM_DB_PATH"] = _TMP
+os.environ["KIMI_API_KEY"] = ""
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "web", "backend"))
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from app.database import init_db  # noqa: E402
+
+init_db()
+
+from app.services.adaptive_loop import create_loop  # noqa: E402
+from app.services.task_manager import get_task_manager  # noqa: E402
+
+tm = get_task_manager()
+
+# ------------------------------------------------------------------ 1. create
+loop = create_loop(
+    user_requirement="maximize average torque within feasibility constraints",
+    total_budget=8,
+    batch_size=4,
+)
+# Inject a fake plan to bypass AI plan generation (parameters are the L0-known
+# names so the feasibility pre-screening matches).
+loop.plan = {
+    "plan_name": "p3-fake",
+    "topology": "SSSR",
+    "scan_variables": [
+        {"name": "airgap_mm", "min_value": 0.8, "max_value": 2.0, "step": 0.1, "unit": "mm"},
+        {"name": "current_a", "min_value": 5.0, "max_value": 20.0, "step": 0.5, "unit": "A"},
+    ],
+    "search_strategy": {"max_solver_calls": 8, "batch_size": 4, "initial_samples": 4},
+    "acceptance_criteria": {
+        "objective_metric": "tavg_nm",
+        "objective_direction": "maximize",
+        "hard_constraints": [],
+    },
+}
+
+# ---------------------------------------------------------- 2. init search
+res = loop.initialize_search()
+assert res["phase"] == "search_initialized", res
+init_points = res["initial_batch"]
+assert len(init_points) > 0, res
+init_ids = sorted(p["id"] for p in init_points)
+print("[1] initialize_search OK: initial_batch=%d ids=%s" % (len(init_points), init_ids))
+
+# --------------------------------------------- 3. submit-batch (new bridge)
+sub = loop.submit_batch_to_executor()
+tid = sub["task_id"]
+assert tid, sub
+assert sub["n_points"] == len(init_points), sub
+assert loop.phase.value == "simulation_running", loop.phase
+task = tm.get_task(tid)
+assert task is not None, tid
+assert task["task_type"] == "adaptive_batch", task
+assert task["loop_id"] == loop.loop_id, task
+assert sorted(task["point_ids"]) == init_ids, (task["point_ids"], init_ids)
+print("[2] submit_batch_to_executor OK: task=%s points=%d type=%s"
+      % (tid, sub["n_points"], task["task_type"]))
+
+
+def run_batch(tid):
+    """Fake local executor: read the task parameters and fabricate metrics."""
+    task = tm.get_task(tid)
+    with open(task["task_file"], "r", encoding="utf-8") as f:
+        payload = json.load(f)
+    results = []
+    for i, params in enumerate(payload["parameters"]):
+        pid = params.get("point_id")
+        results.append({
+            "point_id": pid,
+            "point_index": i,
+            "params": params,
+            "metrics": {"tavg_nm": round(8.0 + 0.5 * pid, 3), "efficiency_pct": 90.0 + (pid % 5)},
+            "status": "OK",
+        })
+    tm.report_results(tid, results, status="completed")
+    return results
+
+
+# --------------------------------------------------------- 4. drive the loop
+steps = 0
+max_steps = 12
+batches = set()
+all_tids = []
+while steps < max_steps:
+    steps += 1
+    sub = loop.submit_batch_to_executor()
+    if not sub["task_id"]:
+        # nothing pending: ask the search for the next batch
+        nb = loop.get_next_batch()
+        if not nb.get("points"):
+            break
+        sub = loop.submit_batch_to_executor()
+        if not sub["task_id"]:
+            break
+    batches.add(sub["batch_id"])
+    all_tids.append(sub["task_id"])
+    results = run_batch(sub["task_id"])
+    point_results = [
+        {"point_id": r["point_id"], "metrics": r["metrics"], "status": "ok"}
+        for r in results
+    ]
+    loop.report_results(point_results)
+    print("[3] step %d batch=%s n_points=%d phase=%s"
+          % (steps, sub["batch_id"], len(point_results), loop.phase.value))
+    comp = loop.check_completion()
+    if comp["completed"]:
+        break
+
+final_phase = loop.phase.value
+state = loop.search.get_state_summary()
+print("[4] FINAL phase=%s batches=%s completed_points=%s used_budget=%s"
+      % (final_phase, sorted(batches), state.get("completed_points"), state.get("used_budget")))
+assert final_phase in ("budget_exhausted", "converged", "completed"), final_phase
+assert state.get("completed_points", 0) >= 4, state
+assert state.get("used_budget", 0) > 0, state
+assert len(batches) >= 1, batches
+
+# ----------------------------------------------------------------- 5. checks
+# every submitted batch task must be persisted with the adaptive fields
+adaptive_tasks = [tm.get_task(t) for t in all_tids]
+assert len(adaptive_tasks) == len(batches) == len(all_tids), (len(adaptive_tasks), len(batches))
+for t in adaptive_tasks:
+    assert t is not None, "task missing"
+    assert t.get("task_type") == "adaptive_batch", t
+    assert t.get("loop_id") == loop.loop_id, t
+    assert t.get("dynamic") is True, t
+print("[5] %d adaptive_batch tasks persisted with loop_id/dynamic fields" % len(adaptive_tasks))
+
+# submitted-batch bridge must be idempotent: second call with no pending
+# points returns task_id=None instead of creating a duplicate task.
+dup = loop.submit_batch_to_executor()
+assert dup.get("task_id") is None, dup
+print("[6] submit-batch idempotency OK (no duplicate task on no pending batch)")
+
+print("\nALL P3-M6 ADAPTIVE EXECUTION BRIDGE TESTS PASSED")

+ 91 - 0
scripts/test_p3_checkpoint.py

@@ -0,0 +1,91 @@
+"""P3 checkpoint test: search import_state + AdaptiveLoop export/restore.
+
+Verifies the /resume path:
+  1. FeasibilityFirstSearch.export_state -> import_state round-trip keeps
+     run_id / budget / points / objective / convergence and can select the
+     next batch.
+  2. AdaptiveLoop.export_state -> restore_state survives a simulated process
+     restart (registry cleared) and can keep driving batches.
+
+Run:  python scripts/test_p3_checkpoint.py   (exit 0 = PASS)
+
+Isolated temp SQLite DB; no real Motor-CAD involved.
+"""
+import os
+import sys
+import tempfile
+
+_TMP = os.path.join(tempfile.mkdtemp(), "ck.db")
+os.environ["AFM_DB_PATH"] = _TMP
+os.environ["KIMI_API_KEY"] = ""
+_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, os.path.join(_ROOT, "web", "backend"))
+sys.path.insert(0, _ROOT)
+
+from app.database import init_db  # noqa: E402
+init_db()
+
+from app.services.feasibility_search import (  # noqa: E402
+    FeasibilityFirstSearch, ParameterRange, L0PreScreeningEngine,
+)
+
+# ---------------------------------------------------------------- 1. search round-trip
+params = [ParameterRange(name="airgap_mm", min_value=0.8, max_value=2.0,
+                         step=0.1, unit="mm")]
+s1 = FeasibilityFirstSearch(parameters=params, l0_engine=L0PreScreeningEngine(),
+                            total_budget=8, batch_size=4, initial_samples=4,
+                            objective_metric="tavg_nm", objective_direction="maximize")
+batch = s1.generate_initial_batch()
+for i, p in enumerate(batch[:3]):
+    s1.report_result(p.id, {"tavg_nm": 8.0 + 0.5 * p.id}, "ok")
+
+ex = s1.export_state()
+s2 = FeasibilityFirstSearch.import_state(ex, l0_engine=L0PreScreeningEngine())
+assert s2.state.run_id == s1.state.run_id
+assert s2.state.used_budget == s1.state.used_budget == 3
+assert len(s2.state.points) == len(s1.state.points)
+assert s2.objective_metric == "tavg_nm" and s2.objective_direction == "maximize"
+assert s2.state.convergence_status == s1.state.convergence_status
+nb = s2.select_next_batch()
+assert nb, "restored search must be able to select the next batch"
+print("[1] search export->import round-trip OK (next batch: %d pts)" % len(nb))
+
+# ---------------------------------------------------------------- 2. AdaptiveLoop resume
+from app.services.adaptive_loop import (  # noqa: E402
+    AdaptiveLoop, create_loop, _loops,
+)
+
+loop = create_loop(user_requirement="max torque", total_budget=8, batch_size=4)
+loop.plan = {
+    "plan_name": "p", "topology": "SSSR",
+    "scan_variables": [
+        {"name": "airgap_mm", "min_value": 0.8, "max_value": 2.0},
+        {"name": "current_a", "min_value": 5.0, "max_value": 20.0},
+    ],
+    "search_strategy": {"max_solver_calls": 8, "batch_size": 4,
+                        "initial_samples": 4},
+    "acceptance_criteria": {"objective_metric": "tavg_nm",
+                            "objective_direction": "maximize",
+                            "hard_constraints": []},
+}
+loop.initialize_search()
+sub = loop.submit_batch_to_executor()
+assert sub.get("task_id"), sub
+exported = loop.export_state()
+assert exported["search"] is not None
+assert exported["loop_id"] == loop.loop_id
+assert exported["phase"] == loop.phase.value
+
+# simulate process restart: in-memory registry is lost
+_loops.clear()
+assert len(_loops) == 0
+
+loop2 = AdaptiveLoop.restore_state(exported)
+assert loop2.loop_id == loop.loop_id
+assert loop2.phase.value == exported["phase"]
+assert loop2.search is not None
+sub2 = loop2.submit_batch_to_executor()
+assert sub2.get("task_id"), "restored loop must be able to submit a batch"
+print("[2] AdaptiveLoop export->restore after restart OK (batch resubmitted)")
+
+print("\nALL P3 CHECKPOINT TESTS PASSED")

+ 153 - 0
scripts/test_p3_closed_loop.py

@@ -0,0 +1,153 @@
+"""P3-M5 integration: real HTTP closed loop with a mock local executor.
+
+Run:  python scripts/test_p3_closed_loop.py   (exit 0 = PASS)
+
+Spins up the real FastAPI web backend on an isolated temp SQLite DB, drives
+the AdaptiveOrchestrator, and lets a real TaskExecutor (mock solver) poll and
+execute adaptive batches over HTTP. Verifies the full chain:
+
+    orchestrator.start_loop -> task created (HTTP) -> executor claims ->
+    mock simulation -> report_results (point_id) -> orchestrator.advance_loop
+    feeds back -> next batch -> ... -> budget exhausted.
+
+Requires: requests, uvicorn, fastapi. No real Motor-CAD.
+"""
+import json
+import os
+import subprocess
+import sys
+import tempfile
+import time
+import urllib.request
+
+_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+_BACKEND = os.path.join(_ROOT, "web", "backend")
+_SCRIPTS = os.path.join(_ROOT, "scripts")
+_TMP = tempfile.mkdtemp(prefix="p3loop_")
+_DB = os.path.join(_TMP, "web.db")
+_STATE = os.path.join(_TMP, "loops")
+_PORT = 8137
+_BASE = "http://127.0.0.1:%d" % _PORT
+
+os.environ["AFM_DB_PATH"] = _DB
+os.environ["KIMI_API_KEY"] = ""  # keep test hermetic: no AI calls
+sys.path.insert(0, _BACKEND)
+sys.path.insert(0, _SCRIPTS)
+sys.path.insert(0, _ROOT)  # src.* (l0 re-export) resolves from repo root
+
+from app.database import init_db  # noqa: E402
+init_db()
+
+from app.services.strategy_orchestrator import AdaptiveOrchestrator  # noqa: E402
+from app.services.task_manager import get_task_manager  # noqa: E402
+
+WEB_ENV = dict(os.environ, AFM_DB_PATH=_DB, AFM_PORT=str(_PORT))
+
+
+def wait_health(timeout=30):
+    url = _BASE + "/api/monitor/health"
+    deadline = time.time() + timeout
+    while time.time() < deadline:
+        try:
+            with urllib.request.urlopen(url, timeout=3) as r:
+                if r.status == 200:
+                    return True
+        except Exception:
+            time.sleep(0.5)
+    return False
+
+
+# ---- start web ----
+print("[0] starting web backend on :%d (temp DB)" % _PORT, flush=True)
+web = subprocess.Popen(
+    [sys.executable, "-m", "uvicorn", "app.main:app",
+     "--host", "127.0.0.1", "--port", str(_PORT), "--log-level", "warning"],
+    cwd=_BACKEND, env=WEB_ENV,
+    stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+)
+try:
+    assert wait_health(), "web backend did not become healthy"
+    print("[1] web backend healthy", flush=True)
+
+    tm = get_task_manager()
+    orch = AdaptiveOrchestrator(state_dir=_STATE)
+
+    # ---- orchestrator start ----
+    res = orch.start_loop(
+        loop_id="loop-http",
+        parameters=[
+            {"name": "airgap_mm", "min_value": 0.8, "max_value": 2.0, "step": 0.1, "unit": "mm"},
+            {"name": "current_a", "min_value": 5.0, "max_value": 20.0, "step": 0.5},
+        ],
+        total_budget=8, batch_size=4, initial_samples=4,
+        objective_metric="tavg_nm", objective_direction="maximize",
+    )
+    assert res["phase"] == "running" and res["current_task_id"], res
+    print("[2] orchestrator started: task=%s" % res["current_task_id"], flush=True)
+
+    # ---- local executor (mock) over real HTTP ----
+    import task_executor as te_mod
+    te_mod.requests = None if not True else te_mod.requests  # keep real requests
+    from task_executor import TaskExecutor
+
+    ex = TaskExecutor(web_base_url=_BASE, enable_mock=True,
+                      executor_id="motorcad-mock-m5")
+    thread = ex.start_polling(interval=2)
+    print("[3] mock executor polling started", flush=True)
+
+    # ---- drive the loop ----
+    steps = 0
+    max_steps = 12
+    batches = set()
+    while steps < max_steps:
+        steps += 1
+        view = orch.get_loop_status("loop-http")
+        phase = view["phase"]
+        if phase in ("converged", "budget_exhausted", "failed"):
+            print("[4] terminal phase=%s after %d advance steps" % (phase, steps), flush=True)
+            break
+        tid = view.get("current_task_id")
+        if not tid:
+            view = orch.advance_loop("loop-http")
+            continue
+        # wait for the executor to finish this batch over HTTP
+        deadline = time.time() + 90
+        while time.time() < deadline:
+            t = tm.get_task(tid)
+            if t and t["status"] in ("completed", "failed", "cancelled"):
+                break
+            time.sleep(1)
+        else:
+            raise RuntimeError("batch task %s did not finish in time" % tid)
+        view = orch.advance_loop("loop-http")
+        batches.add(view.get("current_batch"))
+        print("[5] advance -> batch=%s task=%s phase=%s n_results=%s"
+              % (view.get("current_batch"), view.get("current_task_id"),
+                 view["phase"], view["n_results"]), flush=True)
+
+    ex.stop()
+    thread.join(timeout=5)
+
+    final = orch.get_loop_status("loop-http")
+    print("[6] FINAL phase=%s batches=%s n_results=%s"
+          % (final["phase"], sorted(batches), final["n_results"]), flush=True)
+    assert final["phase"] in ("converged", "budget_exhausted"), final
+    assert final["n_results"] >= 4, final
+    ss = final.get("search_state") or {}
+    assert ss.get("completed_points", 0) >= 4, ss
+    # every reported point must carry point_id and be fed back
+    assert ss.get("used_budget", 0) > 0, ss
+
+    print("\nALL P3-M5 HTTP CLOSED LOOP TESTS PASSED", flush=True)
+finally:
+    # cleanup
+    try:
+        ex.stop()
+    except Exception:
+        pass
+    web.terminate()
+    try:
+        web.wait(timeout=10)
+    except Exception:
+        web.kill()
+    print("[cleanup] web stopped", flush=True)

+ 109 - 0
scripts/test_p3_concurrency.py

@@ -0,0 +1,109 @@
+"""P3 concurrency test: atomic task claim under parallel executor instances.
+
+Verifies that when N executors race to claim the same pending task, exactly
+one wins; the rest get ValueError (already claimed) or a transient
+SQLite lock error - never a duplicate successful claim (which would cause
+double simulation of the same points).
+
+Run:  python scripts/test_p3_concurrency.py   (exit 0 = PASS)
+
+Isolated temp SQLite DB; no real Motor-CAD involved.
+"""
+import os
+import sys
+import tempfile
+import threading
+from concurrent.futures import ThreadPoolExecutor
+
+_TMP = os.path.join(tempfile.mkdtemp(), "conc.db")
+os.environ["AFM_DB_PATH"] = _TMP
+os.environ["KIMI_API_KEY"] = ""
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "web", "backend"))
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from app.database import init_db  # noqa: E402
+init_db()
+
+from app.services.task_manager import get_task_manager  # noqa: E402
+
+tm = get_task_manager()
+
+
+def make_pending_task():
+    t = tm.create_task(
+        plan_id=None, plan_data={},
+        parameters=[{"airgap_mm": 1.0, "point_id": 1}],
+        task_name="race-task", task_type="scan",
+    )
+    return t["task_id"]
+
+
+# ------------------------------------------------------------------ 1. manager-level race
+N = 8
+tid = make_pending_task()
+success = []
+failures = []
+lock = threading.Lock()
+
+
+def claim():
+    try:
+        tm.dispatch_task(tid)
+        with lock:
+            success.append(1)
+    except Exception as exc:  # ValueError or transient lock error
+        with lock:
+            failures.append(type(exc).__name__)
+
+
+with ThreadPoolExecutor(max_workers=N) as pool:
+    list(pool.map(lambda _: claim(), range(N)))
+
+assert len(success) == 1, "exactly one claim must win, got %d" % len(success)
+assert len(failures) == N - 1, "the rest must fail, got %d failures" % len(failures)
+print("[1] manager-level race: 1 win / %d lost (%s)" % (len(failures), set(failures)))
+
+# final state must be dispatched
+task = tm.get_task(tid)
+assert task["status"] == "dispatched", task
+print("[2] final status = dispatched OK")
+
+# a sequential second claim must be rejected
+try:
+    tm.dispatch_task(tid)
+    raise SystemExit("second claim should fail")
+except ValueError:
+    print("[3] sequential second claim rejected OK")
+
+# ------------------------------------------------------------------ 2. HTTP-level race
+from app.database import SessionLocal  # noqa: E402
+from app.models.task import Task  # noqa: E402
+
+# create a second pending task directly for the HTTP race
+with SessionLocal() as db:
+    task = Task(
+        task_id="http-race-1",
+        task_name="http-race", plan_id=None,
+        status="pending", priority=5, created_by="test",
+    )
+    db.add(task)
+    db.commit()
+
+http_success = []
+
+
+def http_claim():
+    try:
+        tm.dispatch_task("http-race-1")
+        with lock:
+            http_success.append(1)
+    except Exception:
+        pass
+
+
+with ThreadPoolExecutor(max_workers=N) as pool:
+    list(pool.map(lambda _: http_claim(), range(N)))
+assert len(http_success) == 1, "HTTP-level exactly-one claim, got %d" % len(http_success)
+print("[4] HTTP-level race: exactly 1 claim won OK")
+
+print("\nALL P3 CONCURRENCY TESTS PASSED")

+ 86 - 0
scripts/test_p3_m4_contract.py

@@ -0,0 +1,86 @@
+"""P3-M4 regression: unified task contract + batch_scheduler field alignment.
+
+Run:  python scripts/test_p3_m4_contract.py   (exit 0 = PASS)
+
+Uses an isolated temp state file for the BatchScheduler (never touches the
+real output/scheduler_state.json). No web server or Motor-CAD required.
+"""
+import os
+import sys
+import tempfile
+
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "web", "backend"))
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+# ---- 1) task_contract: status normalization ----
+from app.services.task_contract import (  # noqa: E402
+    normalize_status,
+    is_terminal,
+    merge_adaptive_fields,
+    TASK_STATUS_PENDING,
+    TASK_STATUS_COMPLETED,
+)
+
+assert normalize_status("queued") == TASK_STATUS_PENDING, normalize_status("queued")
+assert normalize_status("pending") == TASK_STATUS_PENDING
+assert normalize_status("completed_with_errors") == TASK_STATUS_COMPLETED
+assert normalize_status("canceled") == "cancelled"
+assert normalize_status("mystery") == "mystery"  # pass through
+assert is_terminal("completed") and is_terminal("failed") and is_terminal("cancelled")
+assert not is_terminal("pending") and not is_terminal("queued")
+print("[1] task_contract status normalization OK")
+
+# ---- 2) task_contract: adaptive field merge ----
+merged = merge_adaptive_fields({}, task_type="adaptive_batch", loop_id="L1",
+                               batch_id=2, point_ids=[1, 2, 3], dynamic=True)
+assert merged["task_type"] == "adaptive_batch"
+assert merged["loop_id"] == "L1" and merged["batch_id"] == 2
+assert merged["point_ids"] == [1, 2, 3] and merged["dynamic"] is True
+defaults = merge_adaptive_fields({})
+assert defaults["task_type"] == "scan" and defaults["dynamic"] is False
+assert defaults["point_ids"] == [] and defaults["loop_id"] is None
+print("[2] task_contract adaptive field merge OK")
+
+# ---- 3) BatchScheduler: adaptive fields on add_task + summary ----
+from app.services.batch_scheduler import BatchScheduler  # noqa: E402
+
+tmp_state = os.path.join(tempfile.mkdtemp(), "sched.json")
+sch = BatchScheduler(state_file=tmp_state)
+t = sch.add_task(
+    task_id="batch-1", task_name="adaptive-L1-b0", task_type="adaptive_batch",
+    loop_id="L1", batch_id=0, point_ids=[10, 11], dynamic=True,
+    parameters=[{"airgap_mm": 1.0, "point_id": 10}],
+)
+assert t["task_type"] == "adaptive_batch", t
+assert t["loop_id"] == "L1" and t["batch_id"] == 0, t
+assert t["point_ids"] == [10, 11] and t["dynamic"] is True, t
+assert t["status"] == "queued", t
+
+# legacy call still works (no new args)
+t2 = sch.add_task(task_id="scan-1", task_name="plain")
+assert t2["task_type"] == "scan" and t2["dynamic"] is False, t2
+
+# get_next_task transitions queued -> running
+nxt = sch.get_next_task()
+assert nxt is not None and nxt["task_id"] == "batch-1"
+assert nxt["status"] == "running" and nxt["loop_id"] == "L1"
+sch.complete_task("batch-1")
+
+# get_next_task picks the legacy scan task (wait_for already satisfied)
+nxt2 = sch.get_next_task()
+assert nxt2 is not None and nxt2["task_id"] == "scan-1", nxt2
+sch.complete_task("scan-1")
+
+# statistics summary exposes new fields
+stats = sch.get_statistics()
+summaries = stats["recent_completed"] + stats["running_tasks"] + stats["queued_tasks"]
+assert any(s.get("task_id") == "batch-1" and s.get("task_type") == "adaptive_batch"
+           and s.get("loop_id") == "L1" for s in summaries), stats
+print("[3] BatchScheduler adaptive fields aligned OK")
+
+# ---- 4) persist / reload keeps fields ----
+sch2 = BatchScheduler(state_file=tmp_state)
+assert os.path.exists(tmp_state)
+print("[4] scheduler state file exists:", os.path.exists(tmp_state))
+
+print("\nALL P3-M4 CONTRACT TESTS PASSED")

+ 134 - 0
scripts/test_p3_orchestrator.py

@@ -0,0 +1,134 @@
+"""P3-M2 regression: AdaptiveOrchestrator full closed loop with fake executor.
+
+Run:  python scripts/test_p3_orchestrator.py   (exit 0 = PASS)
+
+Uses an isolated temp SQLite DB and temp loop-state dir, so it never touches
+the real web DB or real loop state. No real Motor-CAD is involved.
+"""
+import json
+import os
+import sys
+import tempfile
+
+_TMP = os.path.join(tempfile.mkdtemp(), "test_afm.db")
+_TMP_STATE = os.path.join(tempfile.mkdtemp(), "loops")
+os.environ["AFM_DB_PATH"] = _TMP
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "web", "backend"))
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from app.database import init_db  # noqa: E402
+
+init_db()
+
+from app.services.strategy_orchestrator import AdaptiveOrchestrator  # noqa: E402
+from app.services.task_manager import get_task_manager  # noqa: E402
+
+tm = get_task_manager()
+orch = AdaptiveOrchestrator(state_dir=_TMP_STATE)
+
+# ------------------------------------------------------------------ 1. start
+res = orch.start_loop(
+    loop_id="loop-p3-test",
+    parameters=[
+        {"name": "airgap_mm", "min_value": 0.8, "max_value": 2.0, "step": 0.1, "unit": "mm"},
+        {"name": "current_a", "min_value": 5.0, "max_value": 20.0, "step": 0.5},
+    ],
+    total_budget=8,
+    batch_size=4,
+    initial_samples=4,
+    objective_metric="tavg_nm",
+    objective_direction="maximize",
+)
+assert res["phase"] == "running", res
+assert res["current_task_id"], res
+assert res["batch_task"]["task_type"] == "adaptive_batch", res
+first_batch_ids = res["batch_task"]["point_ids"]
+assert isinstance(first_batch_ids, list) and len(first_batch_ids) > 0, first_batch_ids
+print("[1] start_loop OK: task=%s first_batch=%d points"
+      % (res["current_task_id"], len(first_batch_ids)))
+
+
+def run_batch(tid):
+    """Fake executor: read task.json parameters, fabricate results, report."""
+    task = tm.get_task(tid)
+    with open(task["task_file"], "r", encoding="utf-8") as f:
+        payload = json.load(f)
+    results = []
+    for i, params in enumerate(payload["parameters"]):
+        pid = params.get("point_id")
+        results.append({
+            "point_id": pid,
+            "point_index": i,
+            "params": params,
+            "metrics": {"tavg_nm": round(8.0 + 0.5 * pid, 3), "efficiency_pct": 90.0 + (pid % 5)},
+            "status": "OK",
+        })
+    tm.report_results(tid, results, status="completed")
+    return results
+
+
+# ---------------------------------------------------------------- 2. drive
+steps = 0
+max_steps = 12
+batches_seen = set()
+while steps < max_steps:
+    steps += 1
+    view = orch.get_loop_status("loop-p3-test")
+    if view["phase"] in ("converged", "budget_exhausted", "failed"):
+        break
+    tid = view["current_task_id"]
+    if tid is None:
+        view = orch.advance_loop("loop-p3-test")
+        continue
+    run_batch(tid)
+    view = orch.advance_loop("loop-p3-test")
+    batches_seen.add(view.get("current_batch"))
+    print("[2] step %d -> batch=%s task=%s phase=%s n_results=%s"
+          % (steps, view.get("current_batch"), view.get("current_task_id"),
+             view["phase"], view["n_results"]))
+
+final = orch.get_loop_status("loop-p3-test")
+print("[3] FINAL phase=%s batches=%s n_results=%s"
+      % (final["phase"], sorted(batches_seen), final["n_results"]))
+assert final["phase"] in ("converged", "budget_exhausted"), final
+assert final["n_results"] >= 4, final
+assert len(batches_seen) >= 1, batches_seen
+# all reported points must be reflected in search state
+search_state = final.get("search_state") or {}
+assert search_state.get("completed_points", 0) >= 4, search_state
+assert search_state.get("used_budget", 0) > 0, search_state
+
+# ------------------------------------------------------------- 3. misc
+loops = orch.list_loops()
+assert len(loops) == 1 and loops[0]["loop_id"] == "loop-p3-test", loops
+
+try:
+    orch.start_loop(loop_id="loop-p3-test",
+                    parameters=[{"name": "airgap_mm", "min_value": 1, "max_value": 2}])
+    raise SystemExit("duplicate loop should fail")
+except ValueError:
+    print("[4] duplicate loop rejected OK")
+
+state_path = os.path.join(_TMP_STATE, "loop-p3-test_loop.json")
+assert os.path.exists(state_path), state_path
+print("[5] loop state persisted OK")
+
+# ------------------------------------------------------------ 4. task model
+# create_task with explicit adaptive-batch fields
+t2 = tm.create_task(
+    plan_id=None,
+    plan_data={"topology": "SSSR"},
+    parameters=[{"airgap_mm": 1.0, "point_id": 0}],
+    task_name="meta-batch",
+    task_type="adaptive_batch",
+    loop_id="loop-x",
+    batch_id=3,
+    point_ids=[0, 1],
+    dynamic=True,
+)
+assert t2["task_type"] == "adaptive_batch", t2
+assert t2["loop_id"] == "loop-x" and t2["batch_id"] == 3, t2
+assert t2["point_ids"] == [0, 1] and t2["dynamic"] is True, t2
+print("[6] task model adaptive-batch fields OK")
+
+print("\nALL P3-M2 ORCHESTRATOR TESTS PASSED")

+ 154 - 0
scripts/test_p3_unit_edge.py

@@ -0,0 +1,154 @@
+"""P3 unit edge/exception/empty-value tests (engineering rules).
+
+Covers boundary, exception-input and empty/zero-value paths for the P3
+platform batch modules, complementing the integration-level tests.
+
+Run:  python scripts/test_p3_unit_edge.py   (exit 0 = PASS)
+
+All behaviour below was confirmed by probing the actual implementation;
+no behaviour is assumed.
+"""
+import os
+import sys
+import tempfile
+
+_BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, _BASE)
+_TMP = os.path.join(tempfile.mkdtemp(), "edge.db")
+os.environ["AFM_DB_PATH"] = _TMP
+os.environ["KIMI_API_KEY"] = ""
+
+# ---------------------------------------------------------------- strategies
+from src.afmcore.strategies import (  # noqa: E402
+    get_strategy, normalize_method, register_strategy, is_registered,
+)
+from src.afmcore.strategies.full_factorial import FullFactorialStrategy  # noqa: E402
+from src.afmcore.strategies.lhs import LHSStrategy  # noqa: E402
+
+# [S1] registry exception paths
+try:
+    register_strategy("", object)
+    raise SystemExit("empty kind should raise ValueError")
+except ValueError:
+    pass
+try:
+    register_strategy("x", 123)
+    raise SystemExit("non-subclass should raise TypeError")
+except TypeError:
+    pass
+try:
+    get_strategy("no-such-strategy")
+    raise SystemExit("unknown kind should raise KeyError")
+except KeyError:
+    pass
+assert is_registered("full_factorial") and is_registered("lhs") and is_registered("adaptive")
+print("[S1] registry exception paths OK")
+
+# [S2] normalize_method
+assert normalize_method(None) == "full_factorial"
+assert normalize_method("") == "full_factorial"
+assert normalize_method("active_learning") == "adaptive"
+assert normalize_method("constrained") == "adaptive"
+assert normalize_method("  ADAPTIVE ") == "adaptive"
+assert normalize_method("ABC") == "abc"  # unknown kept lowercase, reported by caller
+print("[S2] normalize_method OK")
+
+# [S3] FullFactorial empty / batching / converged
+ff_empty = FullFactorialStrategy(points=[])
+assert ff_empty.select_next() == []
+assert ff_empty.is_converged() is True
+assert ff_empty.next_batch_ready() is False
+ff = FullFactorialStrategy(points=[{"x": 1}, {"x": 2}, {"x": 3}, {"x": 4}, {"x": 5}], batch_size=2)
+b1 = ff.select_next()
+b2 = ff.select_next()
+b3 = ff.select_next()
+assert [len(b1), len(b2), len(b3)] == [2, 2, 1], (b1, b2, b3)
+assert ff.is_converged() is True
+assert ff.select_next() == []  # exhausted
+print("[S3] FullFactorial empty/batch/converged OK")
+
+# [S4] LHS empty vs normal
+lhs_empty = LHSStrategy(parameters=[], n_samples=4)
+assert lhs_empty.select_next() == []
+lhs = LHSStrategy(
+    parameters=[
+        {"name": "a", "min_value": 1, "max_value": 2},
+        {"name": "b", "min_value": 10, "max_value": 20},
+    ],
+    n_samples=3,
+)
+pts = lhs.select_next()
+assert len(pts) == 3, pts
+for p in pts:
+    assert p["point_id"] is not None
+    assert set(p["params"].keys()) == {"a", "b"}
+print("[S4] LHS empty/normal OK")
+
+# [S5] batch_size boundary
+ff0 = FullFactorialStrategy(points=[{"x": 1}, {"x": 2}], batch_size=0)
+assert ff0.batch_size == 1  # max(1, 0)
+print("[S5] batch_size boundary OK")
+
+# --------------------------------------------------------------- orchestrator
+sys.path.insert(0, os.path.join(_BASE, "web", "backend"))
+from app.database import init_db  # noqa: E402
+init_db()
+from app.services.strategy_orchestrator import AdaptiveOrchestrator  # noqa: E402
+from app.services.task_manager import get_task_manager  # noqa: E402
+from app.services.adaptive_loop import AdaptiveLoop  # noqa: E402
+
+tm = get_task_manager()
+orch = AdaptiveOrchestrator(state_dir=os.path.join(tempfile.mkdtemp(), "loops"))
+
+# [O1] start_loop empty parameters -> ValueError
+try:
+    orch.start_loop("e1", parameters=[])
+    raise SystemExit("start with empty parameters should raise ValueError")
+except ValueError:
+    pass
+
+# [O2] duplicate loop -> ValueError
+orch.start_loop("dup", parameters=[{"name": "airgap_mm", "min_value": 1, "max_value": 2}])
+try:
+    orch.start_loop("dup", parameters=[{"name": "airgap_mm", "min_value": 1, "max_value": 2}])
+    raise SystemExit("duplicate loop should raise ValueError")
+except ValueError:
+    pass
+
+# [O3] advance/status on missing loop -> ValueError
+for fn in (orch.advance_loop, orch.get_loop_status):
+    try:
+        fn("missing-loop")
+        raise SystemExit("missing loop should raise ValueError")
+    except ValueError:
+        pass
+
+# [O4] advance while batch still running -> no error, stays running
+res = orch.start_loop(
+    "l1",
+    parameters=[{"name": "airgap_mm", "min_value": 1, "max_value": 2}],
+    total_budget=8, batch_size=4, initial_samples=4,
+)
+view = orch.advance_loop("l1")
+assert view["phase"] == "running", view
+assert view.get("message") == "batch still running", view
+print("[O1-O4] orchestrator boundary/exception paths OK")
+
+# [O5] AdaptiveLoop.submit_batch_to_executor before init -> RuntimeError
+loop = AdaptiveLoop(user_requirement="probe")
+try:
+    loop.submit_batch_to_executor()
+    raise SystemExit("submit before init should raise RuntimeError")
+except RuntimeError:
+    pass
+print("[O5] AdaptiveLoop submit-before-init raises RuntimeError OK")
+
+# ----------------------------------------------------------------- task_manager
+# [T1] get_task on missing id -> None
+assert tm.get_task("no-such-task") is None
+# [T2] create_task with empty parameters -> still creates a task (no crash)
+t = tm.create_task(plan_id=None, plan_data={}, parameters=[])
+assert t.get("task_id"), t
+print("[T1-T2] task_manager empty/missing paths OK")
+
+print("\nALL P3 UNIT EDGE/EXCEPTION/EMPTY TESTS PASSED")

+ 223 - 0
scripts/test_p4_acceptance.py

@@ -0,0 +1,223 @@
+"""P4 acceptance test suite.
+
+Tests all P4 milestones: M1 (AI frontend API), M2 (task dispatch),
+M3 (batch scheduler + monitor), M4 (reports + visualization data),
+M5 (deployment config).
+
+Run: python scripts/test_p4_acceptance.py
+"""
+import json
+import os
+import sys
+import time
+from datetime import datetime
+
+# Add project root to path
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+PASS = 0
+FAIL = 0
+RESULTS = []
+
+
+def test(name, condition, detail=""):
+    global PASS, FAIL
+    if condition:
+        PASS += 1
+        RESULTS.append(("PASS", name, detail))
+        print(f"  [PASS] {name}")
+    else:
+        FAIL += 1
+        RESULTS.append(("FAIL", name, detail))
+        print(f"  [FAIL] {name} - {detail}")
+
+
+def test_m1_ai_api():
+    """P4-M1: AI API endpoints and frontend integration."""
+    print("\n[P4-M1] AI Frontend Integration")
+    try:
+        from web.backend.app.routers import ai, ai_plan, analysis, adaptive, search
+        test("AI router modules importable", True)
+    except Exception as e:
+        test("AI router modules importable", False, str(e))
+
+    # Check frontend AI API file
+    ai_api_path = os.path.join("web", "frontend", "src", "api", "ai.ts")
+    test("Frontend AI API file exists", os.path.exists(ai_api_path))
+
+    # Check AI views
+    ai_views = ["PlanGenerator.vue", "L0Prescreen.vue", "AdaptiveOptimize.vue",
+                "ResultAnalysis.vue", "FidelityCalibration.vue", "ExperienceEnhance.vue"]
+    for view in ai_views:
+        path = os.path.join("web", "frontend", "src", "views", "ai", view)
+        test(f"AI view {view} exists", os.path.exists(path))
+
+    # Check components
+    comps = ["ConfidenceBadge.vue", "FeasibilityIndicator.vue"]
+    for comp in comps:
+        path = os.path.join("web", "frontend", "src", "components", "ai", comp)
+        test(f"AI component {comp} exists", os.path.exists(path))
+
+
+def test_m2_task_dispatch():
+    """P4-M2: Task management and dispatch."""
+    print("\n[P4-M2] Task Dispatch and Callback")
+    try:
+        from web.backend.app.models.task import Task
+        test("Task model importable", True)
+    except Exception as e:
+        test("Task model importable", False, str(e))
+
+    try:
+        from web.backend.app.services.task_manager import get_task_manager
+        tm = get_task_manager()
+        test("TaskManager service instantiable", tm is not None)
+    except Exception as e:
+        test("TaskManager service instantiable", False, str(e))
+
+    # Check task router
+    router_path = os.path.join("web", "backend", "app", "routers", "tasks.py")
+    test("Tasks router exists", os.path.exists(router_path))
+
+    # Check frontend task manager
+    tm_path = os.path.join("web", "frontend", "src", "views", "TaskManager.vue")
+    test("Frontend TaskManager exists", os.path.exists(tm_path))
+
+    # Check local executor
+    exec_path = os.path.join("scripts", "task_executor.py")
+    test("Local task executor exists", os.path.exists(exec_path))
+
+
+def test_m3_batch_scheduler():
+    """P4-M3: Batch scheduler and monitoring."""
+    print("\n[P4-M3] Batch Scheduler and Monitoring")
+    try:
+        from web.backend.app.services.batch_scheduler import BatchScheduler, get_scheduler
+        scheduler = get_scheduler()
+        test("BatchScheduler instantiable", scheduler is not None)
+
+        # Test add task
+        task = scheduler.add_task("test-task-1", "Test Task", priority=5,
+                                   parameters=[{"x": 1}, {"x": 2}])
+        test("Add task to scheduler", task["task_id"] == "test-task-1")
+
+        # Test statistics
+        stats = scheduler.get_statistics()
+        test("Scheduler statistics has queued_count", "queued_count" in stats)
+        test("Scheduler statistics has overall_progress", "overall_progress" in stats)
+
+        # Cleanup
+        scheduler.cancel_task("test-task-1")
+    except Exception as e:
+        test("BatchScheduler functional", False, str(e))
+
+    # Check monitor router
+    monitor_path = os.path.join("web", "backend", "app", "routers", "monitor.py")
+    test("Monitor router exists", os.path.exists(monitor_path))
+
+    # Check frontend monitor
+    monitor_vue = os.path.join("web", "frontend", "src", "views", "MonitorDashboard.vue")
+    test("Frontend MonitorDashboard exists", os.path.exists(monitor_vue))
+
+    # Check robust motorcad
+    robust_path = os.path.join("scripts", "robust_motorcad.py")
+    test("Robust MotorCAD core exists", os.path.exists(robust_path))
+
+
+def test_m4_visualization_reports():
+    """P4-M4: Advanced visualization and reports."""
+    print("\n[P4-M4] Visualization and Reports")
+    try:
+        from web.backend.app.services.report_generator import ReportGenerator, get_report_generator
+        rg = get_report_generator()
+        test("ReportGenerator instantiable", rg is not None)
+
+        # Test JSON report generation (fallback mode)
+        task_data = {"task_id": "test-report", "task_name": "Test",
+                      "status": "completed", "plan_data": {"x": 1},
+                      "result_metrics": {"efficiency": 90}}
+        report_path = rg.generate_report(task_data)
+        test("Report generated (JSON fallback)", os.path.exists(report_path))
+        if os.path.exists(report_path):
+            os.remove(report_path)
+    except Exception as e:
+        test("ReportGenerator functional", False, str(e))
+
+    # Check reports router
+    reports_path = os.path.join("web", "backend", "app", "routers", "reports.py")
+    test("Reports router exists", os.path.exists(reports_path))
+
+    # Check frontend visualization
+    viz_path = os.path.join("web", "frontend", "src", "views", "AdvancedVisualization.vue")
+    test("Frontend AdvancedVisualization exists", os.path.exists(viz_path))
+
+
+def test_m5_deployment():
+    """P4-M5: Deployment configuration."""
+    print("\n[P4-M5] Deployment and Packaging")
+    files = {
+        "Dockerfile": os.path.join("Dockerfile"),
+        "docker-compose.yml": os.path.join("docker-compose.yml"),
+        "nginx.conf": os.path.join("nginx.conf"),
+        "deploy.ps1": os.path.join("deploy.ps1"),
+    }
+    for name, path in files.items():
+        test(f"Deployment file {name} exists", os.path.exists(path))
+
+    # Check deploy.ps1 is ASCII only
+    deploy_path = files["deploy.ps1"]
+    if os.path.exists(deploy_path):
+        with open(deploy_path, "r", encoding="utf-8") as f:
+            content = f.read()
+        non_ascii = [c for c in content if ord(c) > 127]
+        test("deploy.ps1 is ASCII-only", len(non_ascii) == 0,
+             f"{len(non_ascii)} non-ASCII chars" if non_ascii else "")
+
+
+def test_router_registration():
+    """Verify all routers are registered in main.py."""
+    print("\n[Integration] Router Registration")
+    main_path = os.path.join("web", "backend", "app", "main.py")
+    if os.path.exists(main_path):
+        with open(main_path, "r", encoding="utf-8") as f:
+            content = f.read()
+        expected_routers = ["tasks", "monitor", "reports"]
+        for router in expected_routers:
+            test(f"Router '{router}' imported in main.py", f"import {router}" in content or f", {router}" in content)
+            test(f"Router '{router}' included in main.py", f"include_router({router}.router)" in content)
+    else:
+        test("main.py exists", False)
+
+
+def main():
+    print("=" * 60)
+    print("PCB AFM Simulation System - P4 Acceptance Test")
+    print(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
+    print("=" * 60)
+
+    os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+    test_m1_ai_api()
+    test_m2_task_dispatch()
+    test_m3_batch_scheduler()
+    test_m4_visualization_reports()
+    test_m5_deployment()
+    test_router_registration()
+
+    print("\n" + "=" * 60)
+    print(f"RESULTS: {PASS} passed, {FAIL} failed, {PASS + FAIL} total")
+    print("=" * 60)
+
+    if FAIL > 0:
+        print("\nFailed tests:")
+        for status, name, detail in RESULTS:
+            if status == "FAIL":
+                print(f"  - {name}: {detail}")
+        sys.exit(1)
+    else:
+        print("\nAll tests passed!")
+        sys.exit(0)
+
+
+if __name__ == "__main__":
+    main()

+ 80 - 0
scripts/test_p4_m4_convergence.py

@@ -0,0 +1,80 @@
+"""P4-M4: convergence-chart data source (search state points_history).
+
+Run:  python scripts/test_p4_m4_convergence.py   (exit 0 = PASS)
+
+Verifies get_state_summary() now exposes per-evaluated-point history
+{id, batch_id, params, objective, feasible} consumed by the frontend
+convergence chart, plus the API response model carries the field.
+"""
+import os
+import sys
+
+_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+_BACKEND = os.path.join(_ROOT, "web", "backend")
+sys.path.insert(0, _BACKEND)
+sys.path.insert(0, _ROOT)  # l0 re-export resolves src.afmcore.l0.prescreening
+
+from app.services.feasibility_search import (  # noqa: E402
+    FeasibilityFirstSearch, ParameterRange,
+)
+from app.services.l0_prescreening import L0PreScreeningEngine  # noqa: E402
+
+search = FeasibilityFirstSearch(
+    parameters=[
+        ParameterRange(name="airgap_mm", min_value=0.5, max_value=2.0),
+        ParameterRange(name="magnet_thickness_mm", min_value=3.0, max_value=8.0),
+    ],
+    l0_engine=L0PreScreeningEngine(),
+    total_budget=40,
+    batch_size=4,
+    initial_samples=8,
+    objective_metric="tavg_nm",
+    objective_direction="maximize",
+    seed=42,
+)
+initial = search.generate_initial_batch()
+assert len(initial) > 0, "initial batch empty"
+
+# 1) empty points_history before results
+sm = search.get_state_summary()
+assert sm["points_history"] == [], sm["points_history"]
+print("[1] points_history empty before any result OK")
+
+# 2) report results for every initial point (batch 0 = initial LHS batch)
+for p in initial:
+    search.report_result(p.id, {"tavg_nm": float(p.id) * 0.1 + 1.0}, "ok")
+sm = search.get_state_summary()
+ph = sm["points_history"]
+assert len(ph) == len(initial), (len(ph), len(initial))
+assert all(h["objective"] is not None for h in ph)
+assert all(h["batch_id"] == 0 for h in ph)
+assert all(h["feasible"] is True for h in ph)
+print("[2] points_history populated after initial-batch results OK (%d pts)" % len(ph))
+
+# 3) second batch keeps history cumulative + batch_id increments to 1
+batch2 = search.select_next_batch()
+for p in batch2:
+    search.report_result(p.id, {"tavg_nm": 3.0}, "ok")
+sm = search.get_state_summary()
+ph2 = sm["points_history"]
+assert len(ph2) == len(initial) + len(batch2)
+assert any(h["batch_id"] == 1 for h in ph2)
+print("[3] history cumulative across batches OK (%d pts, batches=%s)"
+      % (len(ph2), sorted({h["batch_id"] for h in ph2})))
+
+# 4) infeasible point still listed with feasible=False
+batch3 = search.select_next_batch()
+for i, p in enumerate(batch3):
+    search.report_result(p.id, {"tavg_nm": 0.0}, "infeasible")
+sm = search.get_state_summary()
+ph3 = sm["points_history"]
+assert any(h["feasible"] is False for h in ph3), "infeasible point missing"
+print("[4] infeasible points flagged OK")
+
+# 5) response-model wiring carries the field (import validates schema)
+from app.routers.search import SearchStateResponse  # noqa: E402
+resp = SearchStateResponse(**sm)
+assert isinstance(resp.points_history, list) and len(resp.points_history) == len(ph3)
+print("[5] SearchStateResponse carries points_history OK")
+
+print("\nALL P4-M4 CONVERGENCE-DATA TESTS PASSED")

+ 91 - 0
scripts/test_p4_m5_l0.py

@@ -0,0 +1,91 @@
+"""P4-M5: L0 pre-screening promoted to shared core (single implementation).
+
+Run:  python scripts/test_p4_m5_l0.py   (exit 0 = PASS)
+
+Verifies:
+  1. The single implementation lives at src/afmcore/l0/prescreening.py.
+  2. The web re-export references the same classes (no drift).
+  3. Core behavior is unchanged: geometric/electrical/thermal/manufacturing
+     checks, evaluate(), filter_feasible(), empty-input gate.
+  4. Consumers (feasibility_search) still work through the re-export.
+"""
+import os
+import sys
+
+_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+_BACKEND = os.path.join(_ROOT, "web", "backend")
+sys.path.insert(0, _BACKEND)
+sys.path.insert(0, _ROOT)
+
+from src.afmcore.l0.prescreening import (  # noqa: E402
+    L0PreScreeningEngine as SrcEngine,
+    FeasibilityReport, ConstraintResult,
+)
+from app.services.l0_prescreening import L0PreScreeningEngine as WebEngine  # noqa: E402
+
+# 1) single definition, no drift
+assert SrcEngine is WebEngine, "re-export must point at the same class"
+assert SrcEngine.__module__ == "src.afmcore.l0.prescreening"
+print("[1] single implementation in shared core OK (%s)" % SrcEngine.__module__)
+
+eng = SrcEngine()
+
+# 2) geometric: valid vs invalid diameter ratio
+r = eng.evaluate({"outer_diameter_mm": 150, "inner_diameter_mm": 80})
+assert r.feasible
+r_bad = eng.evaluate({"outer_diameter_mm": 150, "inner_diameter_mm": 10})
+assert not r_bad.feasible  # ratio 0.067 < 0.2
+print("[2] geometric diameter-ratio gate OK")
+
+# 3) electrical: current density limit
+r = eng.evaluate({"current_a": 10, "conductor_area_mm2": 2.0})  # 5 A/mm2
+assert r.feasible
+r2 = eng.evaluate({"current_a": 100, "conductor_area_mm2": 2.0})  # 50 A/mm2
+assert not r2.feasible
+print("[3] electrical current-density gate OK")
+
+# 4) thermal: winding temp limit
+r = eng.evaluate({"winding_temp_c": 120})   # below 150
+assert r.feasible
+r2 = eng.evaluate({"winding_temp_c": 160})  # above 150
+assert not r2.feasible
+print("[4] thermal winding-temp gate OK")
+
+# 5) manufacturing: PCB line width
+r = eng.evaluate({"pcb_line_width_mm": 0.5})
+assert r.feasible
+r2 = eng.evaluate({"pcb_line_width_mm": 0.05})
+assert not r2.feasible
+print("[5] manufacturing PCB line-width gate OK")
+
+# 6) empty input -> infeasible with UNKNOWN risk item (C3 gate)
+r = eng.evaluate({})
+assert not r.feasible
+assert any("[UNKNOWN]" in x for x in r.risk_items)
+print("[6] empty-input gate OK")
+
+# 7) filter_feasible splits sets
+sets = [
+    {"outer_diameter_mm": 150, "inner_diameter_mm": 80},
+    {"outer_diameter_mm": 150, "inner_diameter_mm": 10},
+]
+feas, infeas = eng.filter_feasible(sets)
+assert len(feas) == 1 and len(infeas) == 1
+print("[7] filter_feasible split OK")
+
+# 8) consumers still work through the re-export
+from app.services.feasibility_search import (  # noqa: E402
+    FeasibilityFirstSearch, ParameterRange,
+)
+search = FeasibilityFirstSearch(
+    parameters=[ParameterRange(name="outer_diameter_mm", min_value=100, max_value=300),
+                ParameterRange(name="inner_diameter_mm", min_value=50, max_value=150)],
+    l0_engine=WebEngine(),
+    total_budget=20, batch_size=4, initial_samples=6,
+    objective_metric="tavg_nm", objective_direction="maximize", seed=1,
+)
+ini = search.generate_initial_batch()
+assert len(ini) > 0
+print("[8] feasibility_search L0 integration OK (%d initial pts)" % len(ini))
+
+print("\nALL P4-M5 L0 PROMOTION TESTS PASSED")

+ 153 - 0
scripts/test_p4_schema.py

@@ -0,0 +1,153 @@
+"""P4-M1 regression: single-source plan schema (src.plan_schema) + web wiring.
+
+Run:  python scripts/test_p4_schema.py   (exit 0 = PASS)
+
+Covers:
+  1. src.plan_schema.parse_plan / validate_plan_dict:
+       - happy path (values + start/stop/step vars, topology, strategy)
+       - alias tolerance (min_value/max_value -> start/stop)
+       - empty / zero-value inputs
+       - malformed input (non-dict, bad structure) -> structured error
+       - require_model_path stage (draft vs execution)
+       - point generation consistency
+  2. Web wiring: POST /api/plans rejects invalid plan_data (400), accepts
+     valid draft (201); PUT rejects invalid plan_data (400). Uses FastAPI
+     TestClient on an isolated temp SQLite DB. No real Motor-CAD.
+"""
+import os
+import sys
+import tempfile
+
+_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+_BACKEND = os.path.join(_ROOT, "web", "backend")
+_DB = os.path.join(tempfile.mkdtemp(prefix="p4schema_"), "web.db")
+
+os.environ["AFM_DB_PATH"] = _DB
+os.environ["KIMI_API_KEY"] = ""  # hermetic: no AI calls
+sys.path.insert(0, _BACKEND)
+sys.path.insert(0, _ROOT)
+
+from src.plan_schema import (  # noqa: E402
+    parse_plan, validate_plan_dict, SimulationPlan,
+)
+
+VALID_VARS = [
+    {"name": "airgap_mm", "display_name": "Airgap", "unit": "mm",
+     "start": 0.8, "stop": 2.0, "step": 0.1},
+    {"name": "Magnet_Arc_[ED]", "values": [120.0, 130.0, 140.0]},
+]
+VALID_FPS = [{"name": "Outer_Rotor_Diameter", "value": 200.0,
+              "category": "Geometry"}]
+
+
+def make_plan(**over):
+    d = {
+        "plan_version": "2.0",
+        "topology": "SSSR",
+        "model_path": "models/MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot",
+        "fixed_params": VALID_FPS,
+        "variables": VALID_VARS,
+        "cases": [{"id": "default", "name": "Default"}],
+        "search_strategy": {"method": "adaptive", "batch_size": 4,
+                            "max_solver_calls": 80},
+        "acceptance_criteria": {"hard_constraints": ["efficiency_pct >= 92"]},
+    }
+    d.update(over)
+    return d
+
+
+# ---------------- 1) happy path + point generation ----------------
+p = parse_plan(make_plan())
+assert p.topology == "SSSR"
+assert len(p.variables) == 2
+assert p.variables[0].get_values()[0] == 0.8
+assert len(p.variables[0].get_values()) == 13  # 0.8..2.0 step 0.1
+ok, errs = validate_plan_dict(make_plan())
+assert ok, errs
+pts, names = p.generate_points()
+assert len(pts) == 13 * 3 and set(names) == {"airgap_mm", "Magnet_Arc_[ED]"}
+print("[1] happy path + point generation OK (39 points)")
+
+# ---------------- 2) alias tolerance ----------------
+p2 = parse_plan(make_plan(variables=[
+    {"name": "airgap_mm", "min_value": 0.8, "max_value": 2.0, "step": 0.1}]))
+assert p2.variables[0].get_values()[0] == 0.8, p2.variables[0].get_values()
+print("[2] min_value/max_value alias -> start/stop OK")
+
+# ---------------- 3) empty / zero-value inputs ----------------
+ok3, errs3 = validate_plan_dict(make_plan(variables=[]))
+assert not ok3 and any("scan variable" in e for e in errs3), errs3
+ok3b, errs3b = validate_plan_dict({})
+assert not ok3b, errs3b
+ok3c, errs3c = validate_plan_dict(make_plan(variables=[
+    {"name": "x", "start": 5.0, "stop": 1.0, "step": 0.5}]))
+assert not ok3c and any("no values" in e for e in errs3c), errs3c
+print("[3] empty / zero-value inputs rejected OK")
+
+# ---------------- 4) malformed input ----------------
+ok4, errs4 = validate_plan_dict("not-a-dict")
+assert not ok4 and any("malformed" in e for e in errs4), errs4
+ok4b, errs4b = validate_plan_dict(make_plan(variables=[{"no_name": 1}]))
+assert not ok4b, errs4b  # KeyError on name -> malformed
+print("[4] malformed input -> structured error OK")
+
+# ---------------- 5) require_model_path stage ----------------
+ok5, _ = validate_plan_dict(
+    make_plan(model_path=""), require_model_path=False)
+assert ok5, "draft stage must not require a model"
+ok5b, errs5b = validate_plan_dict(make_plan(model_path=""))
+assert not ok5b and any("model_path" in e for e in errs5b)
+print("[5] require_model_path stage OK")
+
+# ---------------- 6) unsupported topology / strategy ----------------
+ok6, errs6 = validate_plan_dict(make_plan(topology="NOPE"))
+assert not ok6 and any("topology" in e for e in errs6), errs6
+ok6b, errs6b = validate_plan_dict(
+    make_plan(search_strategy={"method": "no_such_method"}))
+assert not ok6b and any("strategy" in e for e in errs6b), errs6b
+print("[6] unsupported topology/strategy rejected OK")
+
+# ---------------- 7) web wiring (TestClient) ----------------
+from fastapi.testclient import TestClient  # noqa: E402
+from app.database import init_db as web_init_db  # noqa: E402
+web_init_db()
+
+from app.main import app  # noqa: E402
+client = TestClient(app)
+
+# create project
+r = client.post("/api/projects", json={"name": "t", "topology": "SSSR"})
+assert r.status_code == 201, r.text
+pid = r.json()["id"]
+
+# invalid plan_data -> 400
+bad = make_plan(variables=[])
+r = client.post("/api/plans", json={"project_id": pid, "name": "bad",
+                                    "plan_data": bad})
+assert r.status_code == 400, r.status_code
+assert "Invalid plan_data" in r.json()["detail"], r.text
+
+# valid draft (no model path) -> 201
+good = make_plan(model_path="")
+r = client.post("/api/plans", json={"project_id": pid, "name": "good",
+                                    "plan_data": good})
+assert r.status_code == 201, r.text
+plan_id = r.json()["id"]
+
+# invalid update -> 400
+r = client.put("/api/plans/%d" % plan_id,
+               json={"plan_data": make_plan(variables=[])})
+assert r.status_code == 400, r.status_code
+
+# valid update -> 200
+r = client.put("/api/plans/%d" % plan_id,
+               json={"plan_data": make_plan(model_path="")})
+assert r.status_code == 200, r.text
+
+# download round-trips through the same schema
+r = client.get("/api/plans/%d/download" % plan_id)
+assert r.status_code == 200
+assert r.json()["plan_data"]["variables"]
+print("[7] web create/update validation wired OK (400 on invalid)")
+
+print("\nALL P4-M1 SCHEMA TESTS PASSED")

+ 209 - 0
scripts/test_plan_schema.py

@@ -0,0 +1,209 @@
+"""Tests for plan_schema module - especially string enum parameter support.
+
+Covers:
+- FixedParam with string enum values (Winding_Connection, Cooling_Type, etc.)
+- Validation of plans with mixed numeric/string params
+- generate_full_params preserves string values
+- Boundary cases (empty, int, float, str values)
+
+Run: python scripts/test_plan_schema.py
+Exit 0 = PASS, non-zero = FAIL.
+"""
+import sys
+import os
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "web", "backend"))
+
+from src.plan_schema import (
+    FixedParam,
+    ScanVariable,
+    SimulationPlan,
+    validate_plan_dict,
+    parse_plan,
+)
+
+
+def _make_plan(fixed_params, variables=None):
+    """Helper to create a minimal valid plan dict."""
+    return {
+        "plan_id": "test-plan",
+        "plan_version": "2.0",
+        "created_at": "2026-08-30T12:00:00",
+        "topology": "SSSR",
+        "model_path": "models/test.mot",
+        "fixed_params": fixed_params,
+        "variables": variables or [
+            {"name": "Magnet_Thickness", "values": [2, 3], "unit": "mm"},
+        ],
+        "cases": [{"id": "default", "name": "Default", "params": {}}],
+        "output_metrics": ["tavg_nm", "efficiency_pct"],
+    }
+
+
+def test_fixed_param_string_enum():
+    """Test that FixedParam accepts string enum values.
+
+    Regression test for the AI generate-and-save 422 error:
+    Winding_Connection="Star" caused 'could not convert string to float'.
+    """
+    fp = FixedParam.from_dict({
+        "name": "Winding_Connection",
+        "value": "Star",
+        "unit": "",
+    })
+    assert fp.value == "Star"
+    assert isinstance(fp.value, str)
+    print("PASS: test_fixed_param_string_enum")
+
+
+def test_fixed_param_numeric_values():
+    """Test that numeric values still work correctly."""
+    fp_int = FixedParam.from_dict({"name": "Slot_Number", "value": 12})
+    assert fp_int.value == 12
+    assert isinstance(fp_int.value, int)
+
+    fp_float = FixedParam.from_dict({"name": "Airgap", "value": 1.5})
+    assert fp_float.value == 1.5
+    assert isinstance(fp_float.value, float)
+    print("PASS: test_fixed_param_numeric_values")
+
+
+def test_fixed_param_to_dict_roundtrip():
+    """Test that string values survive to_dict -> from_dict roundtrip."""
+    original = FixedParam(name="Cooling_Type", value="Natural Convection", unit="")
+    d = original.to_dict()
+    assert d["value"] == "Natural Convection"
+    restored = FixedParam.from_dict(d)
+    assert restored.value == "Natural Convection"
+    assert isinstance(restored.value, str)
+    print("PASS: test_fixed_param_to_dict_roundtrip")
+
+
+def test_validate_plan_with_string_enums():
+    """Test that a plan with mixed numeric/string fixed params validates OK."""
+    plan = _make_plan([
+        {"name": "Airgap", "value": 1.0, "unit": "mm"},
+        {"name": "Winding_Connection", "value": "Star", "unit": ""},
+        {"name": "Cooling_Type", "value": "Natural Convection", "unit": ""},
+        {"name": "Slot_Number", "value": 12, "unit": ""},
+    ])
+    ok, errors = validate_plan_dict(plan, require_model_path=False)
+    assert ok, f"Plan should validate OK, got errors: {errors}"
+    assert len(errors) == 0
+    print("PASS: test_validate_plan_with_string_enums")
+
+
+def test_generate_full_params_preserves_strings():
+    """Test that generate_full_params preserves string enum values in all points."""
+    plan = _make_plan([
+        {"name": "Airgap", "value": 1.0, "unit": "mm"},
+        {"name": "Winding_Connection", "value": "Star", "unit": ""},
+    ])
+    parsed = parse_plan(plan)
+    points = parsed.generate_full_params()
+    assert len(points) == 2  # 2 variable values
+    for pt in points:
+        assert pt["Winding_Connection"] == "Star", "String enum must be preserved"
+        assert pt["Airgap"] == 1.0
+        assert "Magnet_Thickness" in pt
+    print("PASS: test_generate_full_params_preserves_strings")
+
+
+def test_empty_fixed_params():
+    """Test plan with no fixed params (boundary case)."""
+    plan = _make_plan([])
+    ok, errors = validate_plan_dict(plan, require_model_path=False)
+    assert ok
+    parsed = parse_plan(plan)
+    points = parsed.generate_full_params()
+    assert len(points) == 2
+    print("PASS: test_empty_fixed_params")
+
+
+def test_ai_generated_plan_regression():
+    """Full regression test simulating the exact AI-generated plan that failed.
+
+    The AI generator produces Winding_Connection as a string enum. This test
+    ensures such plans can be parsed, validated, and expanded without the
+    'could not convert string to float' error.
+    """
+    ai_plan = {
+        "plan_id": "ai-generated-test",
+        "plan_version": "2.0",
+        "created_at": "2026-08-30T12:00:00",
+        "topology": "SSSR",
+        "model_path": "models/MARS-12S10P_SSSR.mot",
+        "fixed_params": [
+            {"name": "Outer_Rotor_Diameter", "value": 100.0, "unit": "mm"},
+            {"name": "Inner_Rotor_Diameter", "value": 40.0, "unit": "mm"},
+            {"name": "Airgap", "value": 1.0, "unit": "mm"},
+            {"name": "Slot_Number", "value": 12, "unit": ""},
+            {"name": "Pole_Number", "value": 10, "unit": ""},
+            {"name": "Winding_Connection", "value": "Star", "unit": ""},
+            {"name": "Cooling_Type", "value": "Natural Convection", "unit": ""},
+            {"name": "CurrentDefinition", "value": "Peak", "unit": ""},
+            {"name": "DCBusVoltage", "value": 48.0, "unit": "V"},
+        ],
+        "variables": [
+            {"name": "Magnet_Thickness", "values": [3, 4, 5], "unit": "mm"},
+            {"name": "Stator_Outer_Diameter", "values": [80, 90, 100], "unit": "mm"},
+        ],
+        "cases": [{"id": "default", "name": "Default", "params": {}}],
+        "output_metrics": ["tavg_nm", "ripple_pct", "efficiency_pct"],
+    }
+
+    # 1. Parse should not raise
+    parsed = parse_plan(ai_plan)
+    assert len(parsed.fixed_params) == 9
+    assert len(parsed.variables) == 2
+
+    # 2. Validate should pass
+    ok, errors = validate_plan_dict(ai_plan, require_model_path=False)
+    assert ok, f"Validation failed: {errors}"
+
+    # 3. Generate points should preserve string enums
+    points = parsed.generate_full_params()
+    assert len(points) == 9  # 3 * 3
+    for pt in points:
+        assert pt["Winding_Connection"] == "Star"
+        assert pt["Cooling_Type"] == "Natural Convection"
+        assert pt["CurrentDefinition"] == "Peak"
+        assert isinstance(pt["Outer_Rotor_Diameter"], float)
+        assert isinstance(pt["Slot_Number"], int)
+
+    print("PASS: test_ai_generated_plan_regression")
+
+
+def main():
+    tests = [
+        test_fixed_param_string_enum,
+        test_fixed_param_numeric_values,
+        test_fixed_param_to_dict_roundtrip,
+        test_validate_plan_with_string_enums,
+        test_generate_full_params_preserves_strings,
+        test_empty_fixed_params,
+        test_ai_generated_plan_regression,
+    ]
+
+    passed = 0
+    failed = 0
+    for test in tests:
+        try:
+            test()
+            passed += 1
+        except Exception as e:
+            print(f"FAIL: {test.__name__}: {e}")
+            failed += 1
+
+    print(f"\n{'='*50}")
+    print(f"Results: {passed} passed, {failed} failed, {len(tests)} total")
+    if failed > 0:
+        sys.exit(1)
+    else:
+        print("ALL TESTS PASSED")
+        sys.exit(0)
+
+
+if __name__ == "__main__":
+    main()

+ 199 - 0
scripts/test_platform_registry.py

@@ -0,0 +1,199 @@
+"""Platform P2 regression tests: topology registry + adapter-driven executor.
+
+Covers (docs/PLATFORM_DESIGN_V2.md P2):
+1. Topology registry: registration, lookup, parameter system, validation.
+2. plan_schema topology validation integration.
+3. Adapter registry + get_adapter("motorcad").
+4. MotorCADTaskExecutor wired through get_adapter (metrics flattened to
+   top level, FAILED points raise, _compute_metrics works) - via a fake
+   adapter, so NO real Motor-CAD is launched.
+
+Run:  python scripts/test_platform_registry.py
+Exit code 0 = all PASS. Pure ASCII source.
+"""
+import os
+import sys
+
+ROOT = os.path.dirname(os.path.abspath(__file__))
+PROJ = os.path.dirname(ROOT)
+sys.path.insert(0, ROOT)
+sys.path.insert(0, PROJ)
+sys.path.insert(0, os.path.join(PROJ, "src"))
+sys.path.insert(0, os.path.join(PROJ, "web", "backend"))
+
+PASS = 0
+FAIL = 0
+
+
+def check(name, cond, detail=""):
+    global PASS, FAIL
+    if cond:
+        PASS += 1
+        print("  [PASS] %s%s" % (name, (" - " + detail) if detail else ""))
+    else:
+        FAIL += 1
+        print("  [FAIL] %s%s" % (name, (" - " + detail) if detail else ""))
+
+
+def section(title):
+    print("\n== %s ==" % title)
+
+
+# ---------------------------------------------------------------------------
+section("1. Topology registry")
+from afmcore.topology import (  # noqa: E402
+    TOPOLOGY_REGISTRY,
+    get_topology,
+    get_topology_or_none,
+    list_topologies,
+    list_topology_codes,
+    is_supported,
+    is_active,
+    param_names_for,
+    validate_params,
+    to_dict,
+    register_topology,
+    TopologyDefinition,
+)
+
+check("registry has 3 topologies", len(TOPOLOGY_REGISTRY) == 3)
+check("codes sorted", list_topology_codes() == ["DRSS", "SDSR", "SSSR"])
+sssr = get_topology("SSSR")
+check("SSSR label_zh", sssr.label_zh == "\u5355\u5b9a\u5b50\u5355\u8f6c\u5b50", sssr.label_zh)
+check("SSSR active", is_active("SSSR"))
+check("SSSR 37 params", len(sssr.all_params()) == 37, str(len(sssr.all_params())))
+check("SSSR 8 groups", len(sssr.param_system) == 8)
+check("SSSR sizing", "Airgap" in sssr.sizing_params and "Magnet_Thickness" in sssr.sizing_params)
+check("SSSR scan vars", set(sssr.default_scan_vars) >= {"Airgap", "Magnet_Thickness", "RMSCurrent"})
+check("SSSR single airgap", sssr.airgap_count == 1 and sssr.stator_count == 1 and sssr.rotor_count == 1)
+
+drss = get_topology("drss")  # case-insensitive lookup
+check("DRSS planned + double airgap",
+      drss.status == "planned" and drss.airgap_count == 2 and drss.rotor_count == 2)
+check("DRSS not active", not is_active("DRSS"))
+check("SDSR planned", get_topology("SDSR").status == "planned")
+
+check("is_supported('XXX') False", not is_supported("XXX"))
+check("get_topology_or_none unknown None", get_topology_or_none("XXX") is None)
+
+r = validate_params("SSSR", {"Airgap": 1.0, "RMSCurrent": 15.0, "Foo": 3})
+check("validate known", set(r["known"]) == {"Airgap", "RMSCurrent"})
+check("validate unknown", r["unknown"] == ["Foo"])
+check("validate supported", r["unsupported"] is False)
+
+r2 = validate_params("XXX", {"Airgap": 1.0})
+check("unknown topology -> unsupported", r2["unsupported"] is True)
+
+snap = to_dict()
+check("to_dict has 3", len(snap) == 3)
+check("to_dict SSSR params", len(snap["SSSR"]["params"]) == 37)
+
+# custom registration (idempotent / override)
+register_topology(TopologyDefinition(code="SSSR", label_zh="x", label_en="y", status="active"))
+check("re-register same code overrides", get_topology("SSSR").label_en == "y")
+register_topology(TopologyDefinition(code="SSSR", label_zh="\u5355\u5b9a\u5b50\u5355\u8f6c\u5b50",
+                                     label_en="Single Stator Single Rotor", status="active",
+                                     param_system=sssr.param_system,
+                                     sizing_params=sssr.sizing_params,
+                                     default_scan_vars=sssr.default_scan_vars))
+check("SSSR restored", get_topology("SSSR").label_en == "Single Stator Single Rotor")
+
+# ---------------------------------------------------------------------------
+section("2. plan_schema topology validation")
+from src.plan_schema import SimulationPlan  # noqa: E402
+
+_, errs = SimulationPlan(topology="SSSR").validate()
+check("SSSR no topology error", all("topology not supported" not in e for e in errs), str(errs))
+_, errs = SimulationPlan(topology="TORUS").validate()
+check("TORUS rejected", any("topology not supported" in e for e in errs))
+_, errs = SimulationPlan(topology="DRSS").validate()
+check("DRSS accepted (registered)", all("topology not supported" not in e for e in errs))
+rt = SimulationPlan.from_dict(SimulationPlan(topology="SSSR").to_dict())
+check("serialization round-trip", rt.topology == "SSSR")
+
+# ---------------------------------------------------------------------------
+section("3. Adapter registry")
+from afmcore.adapters import (  # noqa: E402
+    SimulationAdapter,
+    register_adapter,
+    get_adapter,
+    registered_tools,
+)
+import afmcore.adapters.motorcad  # noqa: F401,E402  (registers "motorcad")
+
+check("motorcad registered", "motorcad" in registered_tools())
+mc = get_adapter("motorcad", model_path="models/x.mot")
+check("get_adapter returns SimulationAdapter", isinstance(mc, SimulationAdapter))
+check("adapter tool meta", mc.tool_name == "motorcad" and mc.tool_label == "Motor-CAD (ANSYS)")
+try:
+    get_adapter("maxwell")
+    check("unknown tool raises", False)
+except KeyError:
+    check("unknown tool raises", True)
+
+# ---------------------------------------------------------------------------
+section("4. MotorCADTaskExecutor through adapter (fake, no Motor-CAD)")
+CALLS = []
+
+
+class FakeAdapter(SimulationAdapter):
+    tool_name = "fake"
+
+    def __init__(self, **kw):
+        super().__init__(**kw)
+        self.points = 0
+
+    def connect(self):
+        CALLS.append("connect")
+
+    def disconnect(self):
+        CALLS.append("disconnect")
+
+    def load_model(self, model_path):
+        CALLS.append("load:" + model_path)
+
+    def set_parameter(self, name, value):
+        CALLS.append("set:%s" % name)
+
+    def run_simulation(self, mode="electromagnetic"):
+        CALLS.append("run")
+
+    def extract_metrics(self, output_dir, tag=""):
+        return {"metrics": {}, "status": "OK", "error": None, "raw_path": ""}
+
+    def run_point(self, model_path, params=None, output_dir="output", tag=""):
+        CALLS.append("run_point:" + str(tag))
+        self.points += 1
+        if tag == "fail":
+            return {"metrics": {}, "status": "FAILED", "error": "boom", "raw_path": ""}
+        return {"metrics": {"tavg_nm": 12.5 + self.points, "efficiency_pct": 93.1},
+                "status": "OK", "error": None, "raw_path": "x", "solve_time_s": 1.5}
+
+
+register_adapter("fake", FakeAdapter)
+
+from task_executor import MotorCADTaskExecutor  # noqa: E402
+
+ex = MotorCADTaskExecutor(model_path="models/x.mot", tool="fake", web_base_url="http://127.0.0.1:1")
+res = ex._run_simulation_point({"Airgap": 1.0}, 0)
+check("OK point flattened", res.get("tavg_nm") == 13.5 and res.get("status") == "OK", str(res))
+computed = ex._compute_metrics([res, res])
+check("compute_metrics mean", computed.get("tavg_nm_mean") == 13.5, str(computed))
+check("compute_metrics counts", computed["successful_points"] == 2 and computed["failed_points"] == 0)
+try:
+    ex._run_simulation_point({"Airgap": 1.0}, "fail")
+    check("FAILED point raises", False)
+except RuntimeError:
+    check("FAILED point raises", True)
+
+# execute_task end-to-end with 3 points (no real backend calls needed; adapter
+# drives the simulation; requests is available so progress reports just 404)
+ex.execute_task({"task_id": "T-P2", "parameters": [{"Airgap": 1.0}, {"Airgap": 1.5}, {"Airgap": 2.0}]})
+check("execute_task created adapter", ex._adapter is not None)
+ex.cleanup()
+check("cleanup disconnected", "disconnect" in CALLS and ex._adapter is None)
+
+# ---------------------------------------------------------------------------
+print("\n================================")
+print("PASS: %d   FAIL: %d" % (PASS, FAIL))
+sys.exit(0 if FAIL == 0 else 1)

+ 189 - 0
scripts/test_robust_solver.py

@@ -0,0 +1,189 @@
+"""Test script for RobustMotorCADSolver - Step 1: Connection and variable probing.
+
+This script tests:
+1. Motor-CAD connection (open_new_instance=True + set_visible=True)
+2. Model loading
+3. Variable probing (get common variable names)
+4. Parameter write-back verification
+5. Single magnetic calculation
+6. Result export and parsing
+
+All source is ASCII only.
+"""
+import os
+import sys
+import time
+import json
+
+# Add project root to path
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from scripts.robust_motorcad import (
+    RobustMotorCADSolver,
+    ensure_environment,
+    is_running_as_admin,
+    check_license_server,
+)
+
+
+def main():
+    print("=" * 60)
+    print("RobustMotorCADSolver Test - Step 1: Connection & Probing")
+    print("=" * 60)
+
+    # Pre-checks
+    print("\n[Pre-checks]")
+    print(f"  Running as admin: {is_running_as_admin()}")
+    license_ok, license_msg = check_license_server()
+    print(f"  License server: {license_msg}")
+
+    ensure_environment()
+    print(f"  MOTORCAD_ACTIVEX: {os.environ.get('MOTORCAD_ACTIVEX', 'not set')}")
+    print(f"  ANSYSLMD_LICENSE_FILE: {os.environ.get('ANSYSLMD_LICENSE_FILE', 'not set')}")
+
+    # Model path
+    model_path = os.path.join(
+        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+        "models", "MARS-12S10P_SSSR_D76-C150_V5.0-0819.mot"
+    )
+    print(f"\n  Model path: {model_path}")
+    print(f"  Model exists: {os.path.exists(model_path)}")
+
+    if not os.path.exists(model_path):
+        print("ERROR: Model file not found!")
+        return 1
+
+    # Create solver
+    print("\n[Creating solver]")
+    solver = RobustMotorCADSolver(
+        model_path=model_path,
+        output_dir=os.path.join(
+            os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+            "output", f"test_connect_{time.strftime('%Y%m%d_%H%M%S')}"
+        ),
+        point_timeout=120,
+        max_retries=2,
+        headless=False,
+    )
+    print(f"  Output dir: {solver.output_dir}")
+
+    # Connect
+    print("\n[Step 1: Connecting to Motor-CAD]")
+    try:
+        solver.connect()
+        print("  Connection SUCCESS")
+        # Suppress popups immediately to prevent GUI dialogs from blocking batch
+        solver._suppress_popups()
+        print("  Popup suppression enabled (MessageDisplayState=2)")
+    except Exception as e:
+        print(f"  Connection FAILED: {e}")
+        return 1
+
+    # Run preflight
+    print("\n[Step 2: Running preflight self-check]")
+    try:
+        preflight = solver.run_preflight()
+        print(preflight.summary())
+    except Exception as e:
+        print(f"  Preflight error: {e}")
+
+    # Probe variables
+    print("\n[Step 3: Probing common variables]")
+    common_vars = [
+        "Motor_Type",
+        "Stator_Number_Of_Slots",
+        "Rotor_Number_Of_Poles",
+        "Airgap",
+        "Magnet_Arc_[ED]",
+        "MagnetCentralArc_HalbachRing",
+        "Slot_Opening",
+        "Slot_Width",
+        "Copper_Width",
+        "TorquePointsPerCycle",
+        "AirgapMeshPoints_mesh",
+        "AirgapMeshPoints_layers",
+        "Shaft_Speed",
+        "Peak_Phase_Current",
+    ]
+
+    probed = {}
+    for var in common_vars:
+        try:
+            value = solver.mc.get_variable(var)
+            probed[var] = value
+            print(f"  {var} = {value}")
+        except Exception as e:
+            print(f"  {var} = ERROR: {e}")
+
+    # Save probed variables
+    probe_file = os.path.join(solver.output_dir, "probed_variables.json")
+    with open(probe_file, "w", encoding="utf-8") as f:
+        json.dump(probed, f, indent=2, default=str)
+    print(f"\n  Probed variables saved to: {probe_file}")
+
+    # Test parameter write-back verification
+    print("\n[Step 4: Testing parameter write-back verification]")
+    test_var = "TorquePointsPerCycle"
+    if test_var in probed:
+        original_value = float(probed[test_var])
+        test_value = 60  # Test with 60 points
+        print(f"  Original {test_var} = {original_value}")
+        print(f"  Testing write {test_var} = {test_value}")
+        try:
+            applied = solver._write_and_verify(test_var, test_value)
+            print(f"  Write-back verification SUCCESS: applied={applied}")
+            # Restore original
+            solver._write_and_verify(test_var, original_value)
+            print(f"  Restored {test_var} = {original_value}")
+        except Exception as e:
+            print(f"  Write-back verification FAILED: {e}")
+    else:
+        print(f"  {test_var} not available, skipping write test")
+
+    # Test single magnetic calculation
+    print("\n[Step 5: Testing single magnetic calculation]")
+    try:
+        start = time.time()
+        solver.mc.do_magnetic_calculation()
+        elapsed = time.time() - start
+        print(f"  Magnetic calculation SUCCESS in {elapsed:.1f}s")
+    except Exception as e:
+        print(f"  Magnetic calculation FAILED: {e}")
+
+    # Test result export
+    print("\n[Step 6: Testing result export]")
+    try:
+        export_file = os.path.join(solver.raw_dir, "test_export.csv")
+        solver.mc.export_results("EMagnetic", export_file)
+        if os.path.exists(export_file):
+            file_size = os.path.getsize(export_file)
+            print(f"  Export SUCCESS: {export_file} ({file_size} bytes)")
+            # Parse and show metrics
+            metrics = solver._parse_export(export_file)
+            print(f"  Parsed {len(metrics)} metrics:")
+            for k, v in metrics.items():
+                print(f"    {k} = {v}")
+        else:
+            print("  Export file not created!")
+    except Exception as e:
+        print(f"  Export FAILED: {e}")
+
+    # Disconnect
+    print("\n[Step 7: Disconnecting]")
+    solver.disconnect()
+    print("  Disconnected")
+
+    # Summary
+    print("\n" + "=" * 60)
+    print("Test Summary")
+    print("=" * 60)
+    print(f"  Output directory: {solver.output_dir}")
+    print(f"  Probed variables: {len(probed)}")
+    print(f"  Log file: {solver._log_path}")
+    print("\n  Test completed successfully!")
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())

+ 147 - 0
scripts/test_search_state_summary.py

@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+"""P5-M3: unit tests for get_state_summary() batch_summary / l0_summary.
+
+Covers: happy path, post-report aggregation, empty search boundary,
+infeasible-point L0 reasons, objective direction (max/min), and
+unknown point_id report resilience.
+
+All source is ASCII only. Run: python scripts/test_search_state_summary.py
+exit 0 = PASS.
+"""
+import os
+import sys
+import unittest
+
+_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+_BACKEND = os.path.join(_ROOT, "web", "backend")
+sys.path.insert(0, _BACKEND)
+sys.path.insert(0, _ROOT)
+
+from app.services.feasibility_search import (  # noqa: E402
+    FeasibilityFirstSearch,
+    ParameterRange,
+)
+
+
+def _make_search(direction="maximize", metric="tavg_nm"):
+    """Build a minimal search with one parameter range."""
+    return FeasibilityFirstSearch(
+        parameters=[
+            ParameterRange(name="airgap_mm", min_value=0.5, max_value=2.0, step=0.1),
+        ],
+        total_budget=20,
+        batch_size=4,
+        initial_samples=8,
+        objective_metric=metric,
+        objective_direction=direction,
+        seed=42,
+    )
+
+
+class TestStateSummaryNewFields(unittest.TestCase):
+    """P5-M3: batch_summary / l0_summary / infeasible / failed fields."""
+
+    def test_initial_batch_has_new_fields(self):
+        """Happy path: after generate_initial_batch, all new fields present."""
+        s = _make_search()
+        s.generate_initial_batch()
+        state = s.get_state_summary()
+        for key in ("infeasible_points", "failed_points", "batch_summary", "l0_summary"):
+            self.assertIn(key, state, "missing key: %s" % key)
+        self.assertIsInstance(state["batch_summary"], list)
+        self.assertGreater(len(state["batch_summary"]), 0, "at least batch 0")
+        b0 = state["batch_summary"][0]
+        for key in ("batch_id", "total", "pending", "ok", "infeasible", "failed", "best_objective"):
+            self.assertIn(key, b0, "batch_summary entry missing: %s" % key)
+        self.assertEqual(b0["total"], b0["pending"] + b0["ok"] + b0["infeasible"] + b0["failed"])
+        l0 = state["l0_summary"]
+        for key in ("sampled", "feasible", "infeasible", "pass_rate", "top_infeasible_reasons"):
+            self.assertIn(key, l0, "l0_summary missing: %s" % key)
+        self.assertEqual(l0["sampled"], state["total_points"])
+        self.assertEqual(l0["feasible"] + l0["infeasible"], l0["sampled"])
+
+    def test_report_results_updates_batch_summary(self):
+        """Report ok results: batch_summary ok count rises, best_objective set."""
+        s = _make_search(direction="maximize")
+        pts = s.generate_initial_batch()
+        pending = [p for p in pts if p.status == "pending"]
+        self.assertGreater(len(pending), 0, "need at least one pending point")
+        target = pending[0]
+        s.report_result(target.id, {"tavg_nm": 1.5}, "ok")
+        state = s.get_state_summary()
+        b0 = state["batch_summary"][0]
+        self.assertEqual(b0["ok"], 1)
+        self.assertEqual(b0["best_objective"], 1.5)
+        self.assertEqual(state["completed_points"], 1)
+
+    def test_minimize_direction_best_objective(self):
+        """Minimize: best_objective is the minimum reported value."""
+        s = _make_search(direction="minimize", metric="total_losses_w")
+        pts = s.generate_initial_batch()
+        pending = [p for p in pts if p.status == "pending"]
+        self.assertGreaterEqual(len(pending), 2, "need 2 pending points")
+        s.report_result(pending[0].id, {"total_losses_w": 50.0}, "ok")
+        s.report_result(pending[1].id, {"total_losses_w": 30.0}, "ok")
+        state = s.get_state_summary()
+        self.assertEqual(state["batch_summary"][0]["best_objective"], 30.0)
+
+    def test_failed_point_counted(self):
+        """Report failed: failed_points increments, batch_summary failed count."""
+        s = _make_search()
+        pts = s.generate_initial_batch()
+        pending = [p for p in pts if p.status == "pending"]
+        self.assertGreater(len(pending), 0)
+        s.report_result(pending[0].id, {}, "failed")
+        state = s.get_state_summary()
+        self.assertEqual(state["failed_points"], 1)
+        self.assertEqual(state["batch_summary"][0]["failed"], 1)
+
+    def test_empty_search_boundary(self):
+        """Boundary: search created but no batch generated -> empty summaries."""
+        s = _make_search()
+        state = s.get_state_summary()
+        self.assertEqual(state["total_points"], 0)
+        self.assertEqual(state["batch_summary"], [])
+        self.assertEqual(state["l0_summary"]["sampled"], 0)
+        self.assertEqual(state["l0_summary"]["pass_rate"], 0.0)
+        self.assertEqual(state["infeasible_points"], 0)
+        self.assertEqual(state["failed_points"], 0)
+
+    def test_infeasible_points_l0_reasons_structure(self):
+        """Infeasible points (if any) carry top_infeasible_reasons with name/count/category."""
+        s = _make_search()
+        s.generate_initial_batch()
+        state = s.get_state_summary()
+        reasons = state["l0_summary"]["top_infeasible_reasons"]
+        self.assertIsInstance(reasons, list)
+        for r in reasons:
+            self.assertIn("name", r)
+            self.assertIn("count", r)
+            self.assertIn("category", r)
+            self.assertGreaterEqual(r["count"], 1)
+
+    def test_unknown_point_id_report_no_crash(self):
+        """Resilience: report_result for unknown point_id does not raise."""
+        s = _make_search()
+        s.generate_initial_batch()
+        # Should not raise; implementation may silently ignore or log.
+        try:
+            s.report_result(999999, {"tavg_nm": 1.0}, "ok")
+        except Exception as exc:
+            self.fail("report_result unknown id raised: %s" % exc)
+        state = s.get_state_summary()
+        # Unknown point must not inflate completed counts.
+        self.assertEqual(state["completed_points"], 0)
+
+    def test_batch_summary_sorted_by_batch_id(self):
+        """batch_summary entries sorted ascending by batch_id."""
+        s = _make_search()
+        s.generate_initial_batch()
+        s.select_next_batch()
+        state = s.get_state_summary()
+        ids = [b["batch_id"] for b in state["batch_summary"]]
+        self.assertEqual(ids, sorted(ids))
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)

+ 199 - 0
scripts/test_strategy_morris.py

@@ -0,0 +1,199 @@
+"""P5-M4: unit tests for MorrisStrategy.
+
+Covers: happy path (sensitivity ranking on linear function), boundary
+(empty params / single trajectory / odd n_levels auto-corrected), anomaly
+(unknown point_id report / missing objective metric), null inputs, registry
+integration, and state() field contract.
+
+All source is ASCII only. Run: python scripts/test_strategy_morris.py
+exit 0 = PASS.
+"""
+import os
+import sys
+import unittest
+
+_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, os.path.join(_ROOT, "src"))
+
+from afmcore.strategies import (  # noqa: E402
+    MorrisStrategy,
+    get_strategy,
+    is_registered,
+    list_strategy_kinds,
+)
+
+
+def _linear_objective(params):
+    """y = 2*a + 0.5*b -> parameter 'a' should rank higher than 'b'."""
+    return 2.0 * params.get("a", 0.0) + 0.5 * params.get("b", 0.0)
+
+
+class TestMorrisHappyPath(unittest.TestCase):
+    def test_trajectory_point_count(self):
+        s = MorrisStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1},
+                        {"name": "b", "min_value": 0, "max_value": 1}],
+            n_trajectories=5, n_levels=4, objective_metric="y", rng_seed=1,
+        )
+        pts = s.select_next(1000)
+        # n_trajectories * (n_params + 1) = 5 * 3 = 15
+        self.assertEqual(len(pts), 15)
+
+    def test_step_zero_has_no_changed_param(self):
+        s = MorrisStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            n_trajectories=2, n_levels=4, objective_metric="y", rng_seed=2,
+        )
+        pts = s.select_next(1000)
+        step0 = [p for p in pts if p["step"] == 0]
+        self.assertEqual(len(step0), 2)
+        for p in step0:
+            self.assertIsNone(p["changed_param"])
+
+    def test_sensitivity_ranking_linear(self):
+        s = MorrisStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1},
+                        {"name": "b", "min_value": 0, "max_value": 1}],
+            n_trajectories=8, n_levels=4, objective_metric="y", rng_seed=3,
+        )
+        pts = s.select_next(1000)
+        for p in pts:
+            s.report(p["point_id"], {"y": _linear_objective(p["params"])}, "ok")
+        st = s.state()
+        ranking = st["sensitivity_ranking"]
+        self.assertEqual(len(ranking), 2)
+        # 'a' should rank first (higher mu_star)
+        self.assertEqual(ranking[0]["parameter"], "a")
+        self.assertGreater(ranking[0]["mu_star"], ranking[1]["mu_star"])
+        # linear function -> sigma near zero
+        self.assertAlmostEqual(ranking[0]["sigma"], 0.0, places=6)
+
+    def test_converged_after_all_reported(self):
+        s = MorrisStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            n_trajectories=3, n_levels=4, objective_metric="y", rng_seed=4,
+        )
+        # take only one point first -> pending still non-empty -> not converged
+        first = s.select_next(1)
+        self.assertEqual(len(first), 1)
+        self.assertFalse(s.is_converged())
+        # take the rest and report all
+        rest = s.select_next(1000)
+        all_pts = first + rest
+        for p in all_pts:
+            s.report(p["point_id"], {"y": 1.0}, "ok")
+        self.assertTrue(s.is_converged())
+
+
+class TestMorrisBoundary(unittest.TestCase):
+    def test_empty_parameters(self):
+        s = MorrisStrategy(parameters=[], n_trajectories=5, objective_metric="y")
+        pts = s.select_next(1000)
+        self.assertEqual(pts, [])
+        self.assertTrue(s.is_converged())
+        st = s.state()
+        self.assertEqual(st["n_parameters"], 0)
+        self.assertEqual(st["sensitivity_ranking"], [])
+
+    def test_none_parameters(self):
+        s = MorrisStrategy(parameters=None, n_trajectories=3, objective_metric="y")
+        self.assertEqual(s.select_next(1000), [])
+
+    def test_single_trajectory(self):
+        s = MorrisStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1},
+                        {"name": "b", "min_value": 0, "max_value": 1}],
+            n_trajectories=1, n_levels=4, objective_metric="y", rng_seed=5,
+        )
+        pts = s.select_next(1000)
+        self.assertEqual(len(pts), 3)  # 1 * (2+1)
+
+    def test_odd_n_levels_auto_corrected(self):
+        s = MorrisStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            n_trajectories=1, n_levels=5, objective_metric="y", rng_seed=6,
+        )
+        self.assertEqual(s.n_levels, 6)  # odd -> next even
+        self.assertAlmostEqual(s._delta, 6 / (2 * 5), places=6)
+
+    def test_params_within_bounds(self):
+        s = MorrisStrategy(
+            parameters=[{"name": "a", "min_value": 0.5, "max_value": 2.0, "step": 0.1},
+                        {"name": "b", "min_value": 10, "max_value": 20}],
+            n_trajectories=4, n_levels=4, objective_metric="y", rng_seed=7,
+        )
+        pts = s.select_next(1000)
+        for p in pts:
+            self.assertGreaterEqual(p["params"]["a"], 0.5)
+            self.assertLessEqual(p["params"]["a"], 2.0)
+            self.assertGreaterEqual(p["params"]["b"], 10)
+            self.assertLessEqual(p["params"]["b"], 20)
+
+
+class TestMorrisAnomaly(unittest.TestCase):
+    def test_report_unknown_point_id_no_crash(self):
+        s = MorrisStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            n_trajectories=2, n_levels=4, objective_metric="y", rng_seed=8,
+        )
+        s.select_next(1000)
+        # should not raise
+        s.report(999999, {"y": 1.0}, "ok")
+        # state() must remain callable and well-formed
+        st = s.state()
+        self.assertIn("sensitivity_ranking", st)
+
+    def test_missing_objective_metric_yields_zero_sensitivity(self):
+        s = MorrisStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            n_trajectories=3, n_levels=4, objective_metric="nonexistent", rng_seed=9,
+        )
+        pts = s.select_next(1000)
+        for p in pts:
+            s.report(p["point_id"], {"y": 1.0}, "ok")  # wrong metric key
+        st = s.state()
+        for r in st["sensitivity_ranking"]:
+            self.assertEqual(r["mu_star"], 0.0)
+            self.assertEqual(r["n_effects"], 0)
+
+    def test_failed_status_points_excluded_from_effects(self):
+        s = MorrisStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            n_trajectories=2, n_levels=4, objective_metric="y", rng_seed=10,
+        )
+        pts = s.select_next(1000)
+        # report first point as failed, rest as ok
+        s.report(pts[0]["point_id"], {"y": 999.0}, "failed")
+        for p in pts[1:]:
+            s.report(p["point_id"], {"y": _linear_objective(p["params"])}, "ok")
+        st = s.state()
+        # failed point's trajectory may be partially excluded; no crash
+        self.assertIn("sensitivity_ranking", st)
+
+
+class TestMorrisRegistry(unittest.TestCase):
+    def test_registered(self):
+        self.assertTrue(is_registered("morris"))
+        self.assertIn("morris", list_strategy_kinds())
+
+    def test_get_strategy_creates_instance(self):
+        s = get_strategy("morris", parameters=[{"name": "x", "min_value": 0, "max_value": 1}],
+                          n_trajectories=2, objective_metric="y", rng_seed=11)
+        self.assertIsInstance(s, MorrisStrategy)
+        self.assertEqual(s.kind, "morris")
+
+    def test_state_field_contract(self):
+        s = MorrisStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            n_trajectories=2, n_levels=4, objective_metric="y", rng_seed=12,
+        )
+        st = s.state()
+        for key in ("kind", "batch_size", "n_parameters", "n_trajectories",
+                     "n_levels", "delta", "objective_metric", "total_points",
+                     "pending", "reported", "sensitivity_ranking", "key_parameters"):
+            self.assertIn(key, st, "missing state field: %s" % key)
+        self.assertEqual(st["kind"], "morris")
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)

+ 255 - 0
scripts/test_strategy_surrogate.py

@@ -0,0 +1,255 @@
+"""P5-M4: unit tests for SurrogateGuidedStrategy.
+
+Covers: happy path (initial LHS + surrogate-guided convergence on bowl
+function), boundary (empty params / budget exhaustion / n_initial > budget),
+anomaly (unknown point_id / missing objective), null inputs, budget-adaptive
+batch sizing, both maximize and minimize directions, registry integration,
+and state() field contract.
+
+All source is ASCII only. Run: python scripts/test_strategy_surrogate.py
+exit 0 = PASS.
+"""
+import os
+import sys
+import unittest
+
+_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+sys.path.insert(0, os.path.join(_ROOT, "src"))
+
+from afmcore.strategies import (  # noqa: E402
+    SurrogateGuidedStrategy,
+    get_strategy,
+    is_registered,
+    list_strategy_kinds,
+)
+
+
+def _bowl(params):
+    """Convex bowl: y = (a-0.5)^2 + (b-0.5)^2, minimum at (0.5, 0.5)."""
+    return (params.get("a", 0.0) - 0.5) ** 2 + (params.get("b", 0.0) - 0.5) ** 2
+
+
+def _hill(params):
+    """Inverse bowl: y = 1 - bowl, maximum at (0.5, 0.5)."""
+    return 1.0 - _bowl(params)
+
+
+class TestSurrogateHappyPath(unittest.TestCase):
+    def test_initial_lhs_batch(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1},
+                        {"name": "b", "min_value": 0, "max_value": 1}],
+            objective_metric="y", objective_direction="minimize",
+            n_initial=8, batch_size=4, budget=20, rng_seed=1,
+        )
+        self.assertEqual(s.state()["phase"], "initial")
+        batch = s.select_next(1000)
+        self.assertEqual(len(batch), 8)
+        for p in batch:
+            self.assertIn("point_id", p)
+            self.assertIn("params", p)
+            self.assertIn("a", p["params"])
+            self.assertIn("b", p["params"])
+
+    def test_convergence_on_bowl_minimize(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1},
+                        {"name": "b", "min_value": 0, "max_value": 1}],
+            objective_metric="y", objective_direction="minimize",
+            n_initial=10, batch_size=3, max_batch_size=5, budget=40,
+            n_candidates=60, rng_seed=2,
+        )
+        # run full budget
+        used = 0
+        while used < 40:
+            batch = s.select_next()
+            if not batch:
+                break
+            for p in batch:
+                s.report(p["point_id"], {"y": _bowl(p["params"])}, "ok")
+                used += 1
+        best = min(r["metrics"]["y"] for r in s._done.values() if r["status"] == "ok")
+        # should find a point reasonably close to the minimum (0.0)
+        self.assertLess(best, 0.05)
+        self.assertEqual(s.state()["phase"], "surrogate_guided")
+
+    def test_maximize_direction(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1},
+                        {"name": "b", "min_value": 0, "max_value": 1}],
+            objective_metric="y", objective_direction="maximize",
+            n_initial=8, batch_size=3, budget=25, n_candidates=50, rng_seed=3,
+        )
+        used = 0
+        while used < 25:
+            batch = s.select_next()
+            if not batch:
+                break
+            for p in batch:
+                s.report(p["point_id"], {"y": _hill(p["params"])}, "ok")
+                used += 1
+        best = max(r["metrics"]["y"] for r in s._done.values() if r["status"] == "ok")
+        # hill maximum is 1.0
+        self.assertGreater(best, 0.95)
+
+
+class TestSurrogateBoundary(unittest.TestCase):
+    def test_empty_parameters(self):
+        s = SurrogateGuidedStrategy(parameters=[], n_initial=5, budget=10, objective_metric="y")
+        batch = s.select_next(1000)
+        self.assertEqual(batch, [])
+        self.assertTrue(s.is_converged())
+
+    def test_none_parameters(self):
+        s = SurrogateGuidedStrategy(parameters=None, n_initial=5, budget=10, objective_metric="y")
+        self.assertEqual(s.select_next(1000), [])
+
+    def test_budget_exhaustion(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            objective_metric="y", objective_direction="minimize",
+            n_initial=3, batch_size=2, budget=5, rng_seed=4,
+        )
+        used = 0
+        while True:
+            batch = s.select_next()
+            if not batch:
+                break
+            for p in batch:
+                s.report(p["point_id"], {"y": p["params"]["a"] ** 2}, "ok")
+                used += 1
+        self.assertLessEqual(used, 5)
+        self.assertTrue(s.is_converged())
+        self.assertEqual(s.state()["phase"], "exhausted")
+
+    def test_n_initial_greater_than_budget(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            objective_metric="y", n_initial=20, batch_size=4, budget=5, rng_seed=5,
+        )
+        batch = s.select_next(1000)
+        # initial pending is 20 but budget is 5; select_next serves pending
+        # regardless (pending points were already generated)
+        self.assertEqual(len(batch), 20)
+
+    def test_adaptive_batch_size_within_bounds(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1},
+                        {"name": "b", "min_value": 0, "max_value": 1}],
+            objective_metric="y", objective_direction="minimize",
+            n_initial=6, batch_size=2, max_batch_size=6, budget=30,
+            n_candidates=40, rng_seed=6,
+        )
+        # initial
+        batch = s.select_next(1000)
+        for p in batch:
+            s.report(p["point_id"], {"y": _bowl(p["params"])}, "ok")
+        # surrogate batches
+        for _ in range(4):
+            b = s.select_next()
+            if not b:
+                break
+            self.assertGreaterEqual(len(b), 1)
+            self.assertLessEqual(len(b), 6)
+            for p in b:
+                s.report(p["point_id"], {"y": _bowl(p["params"])}, "ok")
+
+
+class TestSurrogateAnomaly(unittest.TestCase):
+    def test_report_unknown_point_id_no_crash(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            objective_metric="y", n_initial=3, budget=10, rng_seed=7,
+        )
+        s.select_next(1000)
+        s.report(999999, {"y": 1.0}, "ok")  # should not raise
+        # state() must remain callable and well-formed
+        st = s.state()
+        self.assertIn("surrogate", st)
+
+    def test_missing_objective_metric_excluded_from_training(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            objective_metric="nonexistent", n_initial=3, budget=10, rng_seed=8,
+        )
+        batch = s.select_next(1000)
+        for p in batch:
+            s.report(p["point_id"], {"y": 0.5}, "ok")  # wrong metric
+        # surrogate should have 0 training points (no objective values)
+        train = s._training_data()
+        self.assertEqual(len(train), 0)
+
+    def test_failed_status_excluded_from_training(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            objective_metric="y", n_initial=4, budget=10, rng_seed=9,
+        )
+        batch = s.select_next(1000)
+        s.report(batch[0]["point_id"], {"y": 999.0}, "failed")
+        for p in batch[1:]:
+            s.report(p["point_id"], {"y": 0.5}, "ok")
+        train = s._training_data()
+        self.assertEqual(len(train), 3)  # failed excluded
+
+
+class TestSurrogateRegistry(unittest.TestCase):
+    def test_registered(self):
+        self.assertTrue(is_registered("surrogate_guided"))
+        self.assertIn("surrogate_guided", list_strategy_kinds())
+
+    def test_get_strategy_creates_instance(self):
+        s = get_strategy("surrogate_guided",
+                          parameters=[{"name": "x", "min_value": 0, "max_value": 1}],
+                          objective_metric="y", budget=10, rng_seed=10)
+        self.assertIsInstance(s, SurrogateGuidedStrategy)
+        self.assertEqual(s.kind, "surrogate_guided")
+
+    def test_state_field_contract(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            objective_metric="y", n_initial=3, budget=10, rng_seed=11,
+        )
+        st = s.state()
+        for key in ("kind", "batch_size", "max_batch_size", "budget", "used_budget",
+                     "remaining_budget", "n_initial", "n_parameters", "objective_metric",
+                     "objective_direction", "phase", "pending", "reported",
+                     "last_batch_size", "surrogate", "idw_power", "kappa"):
+            self.assertIn(key, st, "missing state field: %s" % key)
+        self.assertEqual(st["kind"], "surrogate_guided")
+        self.assertIn("n_train", st["surrogate"])
+
+
+class TestSurrogateIDWInternals(unittest.TestCase):
+    def test_idw_prediction_interpolation(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 1}],
+            objective_metric="y", n_initial=0, budget=10, rng_seed=12,
+        )
+        train = [((0.0,), 10.0), ((1.0,), 20.0)]
+        pred, unc = s._idw_predict((0.5,), train)
+        # midpoint should be between 10 and 20
+        self.assertGreater(pred, 10.0)
+        self.assertLess(pred, 20.0)
+        self.assertGreater(unc, 0.0)
+
+    def test_distance_calculation(self):
+        d = SurrogateGuidedStrategy._distance((0.0, 0.0), (3.0, 4.0))
+        self.assertAlmostEqual(d, 5.0, places=6)
+
+    def test_normalize_denormalize_roundtrip(self):
+        s = SurrogateGuidedStrategy(
+            parameters=[{"name": "a", "min_value": 0, "max_value": 10},
+                        {"name": "b", "min_value": -5, "max_value": 5}],
+            objective_metric="y", n_initial=0, budget=10, rng_seed=13,
+        )
+        phys = {"a": 5.0, "b": 0.0}
+        norm = s._normalize(phys)
+        self.assertAlmostEqual(norm[0], 0.5, places=6)
+        self.assertAlmostEqual(norm[1], 0.5, places=6)
+        back = s._denormalize(norm)
+        self.assertAlmostEqual(back["a"], 5.0, places=6)
+        self.assertAlmostEqual(back["b"], 0.0, places=6)
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)

+ 277 - 0
scripts/test_topology_variable_map.py

@@ -0,0 +1,277 @@
+"""Tests for topology-aware Motor-CAD variable name mapping.
+
+Covers:
+- Topology normalization
+- RFM -> AFM alias resolution (the plan 23 bug)
+- Known variable validation
+- Parameter validation with suggestions
+- Boundary cases (empty, unknown topology, unknown variable)
+
+Note (2026-09-03): AFM target names were corrected to the MARS-verified
+names measured live via pymotorcad on MARS-12S10P_SSSR:
+Stator_Outer_Diameter -> Stator_Lam_Dia, Stator_Inner_Diameter -> Stator_Bore,
+Outer_Rotor_Diameter -> RotorOuterDiameter, Rotor_Back_Iron_Thickness ->
+Back_Iron_Thickness. Assertions below use the verified names.
+
+Run: python scripts/test_topology_variable_map.py
+Exit 0 = PASS, non-zero = FAIL.
+"""
+import sys
+import os
+
+# Add backend to path
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "web", "backend"))
+
+from app.services.topology_variable_map import (
+    TOPOLOGY_SSSR,
+    TOPOLOGY_AFIR,
+    TOPOLOGY_RFM,
+    normalize_topology,
+    resolve_variable,
+    is_known_variable,
+    validate_parameters,
+    suggest_alternative,
+    get_known_variables,
+)
+
+
+def test_normalize_topology():
+    """Test topology string normalization."""
+    assert normalize_topology("SSSR") == TOPOLOGY_SSSR
+    assert normalize_topology("sssr") == TOPOLOGY_SSSR
+    assert normalize_topology("AFIR") == TOPOLOGY_AFIR
+    assert normalize_topology("RFM") == TOPOLOGY_RFM
+    assert normalize_topology("AFM") == TOPOLOGY_SSSR
+    assert normalize_topology("axial") == TOPOLOGY_SSSR
+    assert normalize_topology("radial") == TOPOLOGY_RFM
+    assert normalize_topology(None) == TOPOLOGY_SSSR
+    assert normalize_topology("") == TOPOLOGY_SSSR
+    assert normalize_topology("UNKNOWN") == TOPOLOGY_SSSR  # default
+    print("PASS: test_normalize_topology")
+
+
+def test_rfm_to_afm_alias_resolution():
+    """Test that RFM variable names are mapped to AFM names on SSSR topology.
+
+    This is the core fix for plan 23: Stator_Lam_Outer_Dia (RFM) must be
+    resolved to Stator_Lam_Dia (MARS-verified AFM name) when topology is SSSR.
+    """
+    # RFM names on SSSR topology -> AFM names
+    resolved, was_alias = resolve_variable("Stator_Lam_Outer_Dia", TOPOLOGY_SSSR)
+    assert resolved == "Stator_Lam_Dia", f"Expected Stator_Lam_Dia, got {resolved}"
+    assert was_alias is True
+
+    resolved, was_alias = resolve_variable("Stator_Lam_Inner_Dia", TOPOLOGY_SSSR)
+    assert resolved == "Stator_Bore"
+    assert was_alias is True
+
+    resolved, was_alias = resolve_variable("Stator_Yoke_Width", TOPOLOGY_SSSR)
+    assert resolved == "Stator_Yoke_Thickness"
+    assert was_alias is True
+
+    # Same RFM names on RFM topology -> unchanged
+    resolved, was_alias = resolve_variable("Stator_Lam_Outer_Dia", TOPOLOGY_RFM)
+    assert resolved == "Stator_Lam_Outer_Dia"
+    assert was_alias is False
+
+    # AFIR topology also maps to AFM names
+    resolved, was_alias = resolve_variable("Stator_Lam_Outer_Dia", TOPOLOGY_AFIR)
+    assert resolved == "Stator_Lam_Dia"
+    assert was_alias is True
+
+    print("PASS: test_rfm_to_afm_alias_resolution")
+
+
+def test_template_logical_name_mapping():
+    """Test that template logical names map to Motor-CAD actual names."""
+    resolved, was_alias = resolve_variable("Number_of_Slots", TOPOLOGY_SSSR)
+    assert resolved == "Slot_Number"
+    assert was_alias is True
+
+    resolved, was_alias = resolve_variable("Number_of_Poles", TOPOLOGY_SSSR)
+    assert resolved == "Pole_Number"
+    assert was_alias is True
+
+    resolved, was_alias = resolve_variable("DC_Link_Voltage", TOPOLOGY_SSSR)
+    assert resolved == "DCBusVoltage"
+    assert was_alias is True
+
+    resolved, was_alias = resolve_variable("Turns_per_Coil", TOPOLOGY_SSSR)
+    assert resolved == "ConductorsPerSlot"
+    assert was_alias is True
+
+    print("PASS: test_template_logical_name_mapping")
+
+
+def test_known_variable_validation():
+    """Test known variable detection per topology."""
+    # AFM variables known on SSSR (MARS-verified names)
+    assert is_known_variable("Stator_Lam_Dia", TOPOLOGY_SSSR) is True
+    assert is_known_variable("Stator_Bore", TOPOLOGY_SSSR) is True
+    assert is_known_variable("RotorOuterDiameter", TOPOLOGY_SSSR) is True
+    assert is_known_variable("Airgap", TOPOLOGY_SSSR) is True
+    assert is_known_variable("Slot_Number", TOPOLOGY_SSSR) is True
+    assert is_known_variable("Magnet_Thickness", TOPOLOGY_SSSR) is True
+
+    # Deprecated wrong names (radial-template naming) are now UNKNOWN
+    assert is_known_variable("Stator_Outer_Diameter", TOPOLOGY_SSSR) is False
+    assert is_known_variable("Stator_Inner_Diameter", TOPOLOGY_SSSR) is False
+    assert is_known_variable("Outer_Rotor_Diameter", TOPOLOGY_SSSR) is False
+
+    # RFM variable NOT known on SSSR
+    assert is_known_variable("Stator_Lam_Outer_Dia", TOPOLOGY_SSSR) is False
+
+    # Unknown variable
+    assert is_known_variable("NonExistent_Var", TOPOLOGY_SSSR) is False
+
+    # Empty / None
+    assert is_known_variable("", TOPOLOGY_SSSR) is False
+
+    print("PASS: test_known_variable_validation")
+
+
+def test_validate_parameters():
+    """Test full parameter validation with classification."""
+    params = {
+        "Stator_Lam_Dia": 100.0,  # known AFM (MARS-verified)
+        "Airgap": 1.0,  # known
+        "Stator_Lam_Outer_Dia": 80.0,  # RFM alias - should be resolved
+        "NonExistent": 42.0,  # unknown
+    }
+
+    result = validate_parameters(params, TOPOLOGY_SSSR)
+
+    # RFM alias should be resolved to the MARS-verified AFM name
+    resolved_names = [r[0] for r in result["valid"]]
+    assert "Stator_Lam_Dia" in resolved_names
+    assert "Airgap" in resolved_names
+    assert "Stator_Lam_Outer_Dia" not in resolved_names  # should be resolved away
+
+    # Unknown variable should be flagged
+    unknown_names = [r[0] for r in result["unknown"]]
+    assert "NonExistent" in unknown_names
+
+    # resolved_params should have all parameters with resolved names
+    assert "Stator_Lam_Dia" in result["resolved_params"]
+    assert "NonExistent" in result["resolved_params"]
+
+    print("PASS: test_validate_parameters")
+
+
+def test_suggest_alternative():
+    """Test suggestion of close variable names."""
+    suggestion = suggest_alternative("Stator_Lam_Outer_Dia", TOPOLOGY_SSSR)
+    assert suggestion is not None
+    assert "Stator" in suggestion
+    assert "Lam" in suggestion or "Dia" in suggestion
+
+    # Completely unrelated name should return None
+    suggestion = suggest_alternative("xyz_abc_123", TOPOLOGY_SSSR)
+    # May or may not return something, just ensure no crash
+    assert suggestion is None or isinstance(suggestion, str)
+
+    print("PASS: test_suggest_alternative")
+
+
+def test_get_known_variables():
+    """Test retrieval of known variable set."""
+    sssr_vars = get_known_variables(TOPOLOGY_SSSR)
+    assert isinstance(sssr_vars, set)
+    assert len(sssr_vars) > 30  # should have substantial coverage
+    assert "Stator_Lam_Dia" in sssr_vars
+    assert "Airgap" in sssr_vars
+
+    # AFIR should share SSSR variables
+    afir_vars = get_known_variables(TOPOLOGY_AFIR)
+    assert "Stator_Lam_Dia" in afir_vars
+
+    print("PASS: test_get_known_variables")
+
+
+def test_plan23_regression():
+    """Regression test for the exact plan 23 failure scenario.
+
+    Plan 23 had scan variables:
+    - Magnet_Thickness (valid)
+    - Stator_Lam_Outer_Dia (RFM name -> should map to Stator_Lam_Dia)
+    - Stator_Lam_Inner_Dia (RFM name -> should map to Stator_Bore)
+
+    After expansion, NO Stator_Lam_* names should remain, and all variables
+    should be known for SSSR topology.
+    """
+    from app.routers.plans import _expand_plan_to_parameters
+
+    plan_data = {
+        "topology": "SSSR",
+        "fixed_params": [
+            {"name": "Airgap", "value": 1, "source": "user"},
+            {"name": "Number_of_Slots", "value": 12, "source": "user"},
+            {"name": "Number_of_Poles", "value": 10, "source": "user"},
+        ],
+        "variables": [
+            {"name": "Magnet_Thickness", "values": [2, 3, 4, 5]},
+            {"name": "Stator_Lam_Outer_Dia", "values": [80, 85, 90, 95, 100]},
+            {"name": "Stator_Lam_Inner_Dia", "values": [45, 50, 55, 60]},
+        ],
+    }
+
+    parameters = _expand_plan_to_parameters(plan_data)
+    assert len(parameters) == 80  # 4 * 5 * 4
+
+    # Check first point
+    point0 = parameters[0]
+
+    # RFM names should NOT be present
+    assert "Stator_Lam_Outer_Dia" not in point0, "RFM name Stator_Lam_Outer_Dia should be resolved"
+    assert "Stator_Lam_Inner_Dia" not in point0, "RFM name Stator_Lam_Inner_Dia should be resolved"
+
+    # MARS-verified AFM names SHOULD be present
+    assert "Stator_Lam_Dia" in point0, "AFM name Stator_Lam_Dia should be present"
+    assert "Stator_Bore" in point0, "AFM name Stator_Bore should be present"
+
+    # Values should be correct
+    assert point0["Stator_Lam_Dia"] == 80
+    assert point0["Stator_Bore"] == 45
+    assert point0["Magnet_Thickness"] == 2
+
+    # All variables should be known for SSSR
+    from app.services.topology_variable_map import is_known_variable
+    unknown = [k for k in point0.keys() if not is_known_variable(k, "SSSR")]
+    assert len(unknown) == 0, f"Unknown variables found: {unknown}"
+
+    print("PASS: test_plan23_regression")
+
+
+def main():
+    tests = [
+        test_normalize_topology,
+        test_rfm_to_afm_alias_resolution,
+        test_template_logical_name_mapping,
+        test_known_variable_validation,
+        test_validate_parameters,
+        test_suggest_alternative,
+        test_get_known_variables,
+        test_plan23_regression,
+    ]
+
+    passed = 0
+    failed = 0
+    for test in tests:
+        try:
+            test()
+            passed += 1
+        except Exception as e:
+            print(f"FAIL: {test.__name__}: {e}")
+            failed += 1
+
+    print(f"\n{'='*50}")
+    print(f"Results: {passed} passed, {failed} failed, {len(tests)} total")
+    if failed > 0:
+        sys.exit(1)
+    else:
+        print("ALL TESTS PASSED")
+        sys.exit(0)
+
+
+if __name__ == "__main__":
+    main()

+ 1 - 0
src/__init__.py

@@ -0,0 +1 @@
+"""PCB axial flux motor automated simulation system - source package."""

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff