dsh-engineering-ui 0.0.0-stage → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +772 -3
  3. package/engineering/plugins/dsh-engineering-ui/cordis.patch.yml +5 -0
  4. package/engineering/plugins/dsh-engineering-ui/icon.svg +14 -0
  5. package/engineering/plugins/dsh-engineering-ui/lib/client.js +8259 -0
  6. package/engineering/plugins/dsh-engineering-ui/lib/defense-sign.js +1552 -0
  7. package/engineering/plugins/dsh-engineering-ui/lib/index.js +5633 -0
  8. package/engineering/plugins/dsh-engineering-ui/lib/job-wait-guard.js +236 -0
  9. package/engineering/plugins/dsh-engineering-ui/lib/mem-guard.js +331 -0
  10. package/engineering/plugins/dsh-engineering-ui/lib/webots.js +114 -0
  11. package/engineering/plugins/dsh-engineering-ui/locale/en.json +6 -0
  12. package/engineering/plugins/dsh-engineering-ui/locale/zh.json +6 -0
  13. package/engineering/plugins/dsh-engineering-ui/preset-engineering.patch.yml +601 -0
  14. package/engineering/skills/assembly-orchestration/SKILL.md +651 -0
  15. package/engineering/skills/cad-workflow/SKILL.md +132 -0
  16. package/engineering/skills/mode-selection/SKILL.md +660 -0
  17. package/engineering/skills/physics-in-loop/SKILL.md +238 -0
  18. package/engineering/skills/physics-in-loop/references/feadocs.md +142 -0
  19. package/engineering/skills/solidworks-bridge/SKILL.md +445 -0
  20. package/engineering/skills/sw-design/SKILL.md +112 -0
  21. package/engineering/skills/sw-design/references/autocad-workflow.md +81 -0
  22. package/engineering/skills/sw-design/references/gbt-drafting.md +71 -0
  23. package/engineering/skills/sw-to-cad/SKILL.md +121 -0
  24. package/engineering/skills/webots-simulation/SKILL.md +58 -0
  25. package/engineering/tools/LICENSE.Auto-SolidWorks +21 -0
  26. package/engineering/tools/_plugin_paths.py +93 -0
  27. package/engineering/tools/_root.py +330 -0
  28. package/engineering/tools/_store.py +446 -0
  29. package/engineering/tools/ac_bridge.py +459 -0
  30. package/engineering/tools/ac_rules.json +21 -0
  31. package/engineering/tools/ac_validate.py +699 -0
  32. package/engineering/tools/ask_user.py +887 -0
  33. package/engineering/tools/build_arm1.py +46 -0
  34. package/engineering/tools/build_assembly.py +94 -0
  35. package/engineering/tools/build_base.py +57 -0
  36. package/engineering/tools/build_housing.py +70 -0
  37. package/engineering/tools/build_structural.py +43 -0
  38. package/engineering/tools/build_transmission.py +69 -0
  39. package/engineering/tools/cadx.lsp +128 -0
  40. package/engineering/tools/choice_contract.py +61 -0
  41. package/engineering/tools/conn_state.py +1671 -0
  42. package/engineering/tools/defense_gate.py +3374 -0
  43. package/engineering/tools/drawing_input.py +314 -0
  44. package/engineering/tools/env_check.py +467 -0
  45. package/engineering/tools/examples/build_axis.py +142 -0
  46. package/engineering/tools/examples/build_cup.py +56 -0
  47. package/engineering/tools/examples/build_plate.py +58 -0
  48. package/engineering/tools/examples/build_sphere.py +30 -0
  49. package/engineering/tools/examples/build_transition_duct.py +381 -0
  50. package/engineering/tools/gate_integrity.py +401 -0
  51. package/engineering/tools/mode2_flow.py +3589 -0
  52. package/engineering/tools/mode_checker.py +77 -0
  53. package/engineering/tools/mode_gate.py +8013 -0
  54. package/engineering/tools/part_registry.py +4266 -0
  55. package/engineering/tools/physics/__init__.py +50 -0
  56. package/engineering/tools/physics/_compat.py +12 -0
  57. package/engineering/tools/physics/design_state.py +142 -0
  58. package/engineering/tools/physics/domain_validator.py +751 -0
  59. package/engineering/tools/physics/fatigue.py +489 -0
  60. package/engineering/tools/physics/fea_solver.py +1163 -0
  61. package/engineering/tools/physics/feapy_solver.py +314 -0
  62. package/engineering/tools/physics/gb_materials.json +1228 -0
  63. package/engineering/tools/physics/geometry_gate.py +337 -0
  64. package/engineering/tools/physics/load_case.py +951 -0
  65. package/engineering/tools/physics/material_db.py +874 -0
  66. package/engineering/tools/physics/mesh_adapter.py +321 -0
  67. package/engineering/tools/physics/refine_rules.py +272 -0
  68. package/engineering/tools/physics/rules/gb_base_rules.json +377 -0
  69. package/engineering/tools/physics/rules/gb_drafting_rules.json +57 -0
  70. package/engineering/tools/physics/rules/gb_electrical_rules.json +25 -0
  71. package/engineering/tools/physics/rules/gb_fasteners_rules.json +73 -0
  72. package/engineering/tools/physics/rules/gb_housing_rules.json +41 -0
  73. package/engineering/tools/physics/rules/gb_manufacturing_rules.json +25 -0
  74. package/engineering/tools/physics/rules/gb_material_verify_rules.json +72 -0
  75. package/engineering/tools/physics/rules/gb_materials_rules.json +377 -0
  76. package/engineering/tools/physics/rules/gb_safety_rules.json +89 -0
  77. package/engineering/tools/physics/rules/gb_springs_rules.json +41 -0
  78. package/engineering/tools/physics/rules/gb_standards_registry.json +1646 -0
  79. package/engineering/tools/physics/rules/gb_structural_rules.json +201 -0
  80. package/engineering/tools/physics/rules/gb_surface_rules.json +25 -0
  81. package/engineering/tools/physics/rules/gb_transmission_rules.json +233 -0
  82. package/engineering/tools/physics/rules/housing_rules.json +48 -0
  83. package/engineering/tools/physics/rules/manifest.json +54 -0
  84. package/engineering/tools/physics/rules/mold_rules.json +48 -0
  85. package/engineering/tools/physics/rules/structural_rules.json +56 -0
  86. package/engineering/tools/physics/rules/transmission_rules.json +48 -0
  87. package/engineering/tools/physics/simulation_report.py +829 -0
  88. package/engineering/tools/physics_bridge.py +2685 -0
  89. package/engineering/tools/profile-manifest.ps1 +72 -0
  90. package/engineering/tools/run_regression.py +109 -0
  91. package/engineering/tools/settings.py +419 -0
  92. package/engineering/tools/solidworks-modeling.md +519 -0
  93. package/engineering/tools/sw_bridge.py +8215 -0
  94. package/engineering/tools/sw_guard.py +1066 -0
  95. package/engineering/tools/sw_surface.py +87 -0
  96. package/engineering/tools/swapi.py +14766 -0
  97. package/engineering/tools/verify_sync.py +218 -0
  98. package/engineering/tools/visual_check.py +2589 -0
  99. package/engineering/tools/webots_env.py +121 -0
  100. package/engineering/tools/webots_mcp/__init__.py +3 -0
  101. package/engineering/tools/webots_mcp/__main__.py +3 -0
  102. package/engineering/tools/webots_mcp/assembly.py +391 -0
  103. package/engineering/tools/webots_mcp/bootstrap.py +55 -0
  104. package/engineering/tools/webots_mcp/controller_worker.py +124 -0
  105. package/engineering/tools/webots_mcp/evidence.py +288 -0
  106. package/engineering/tools/webots_mcp/fake_worker.py +69 -0
  107. package/engineering/tools/webots_mcp/fault_resources.py +121 -0
  108. package/engineering/tools/webots_mcp/host.py +45 -0
  109. package/engineering/tools/webots_mcp/ledger.py +73 -0
  110. package/engineering/tools/webots_mcp/owned_job.py +70 -0
  111. package/engineering/tools/webots_mcp/physical_gate.py +187 -0
  112. package/engineering/tools/webots_mcp/physics.py +152 -0
  113. package/engineering/tools/webots_mcp/pipeline.py +108 -0
  114. package/engineering/tools/webots_mcp/recipes.py +129 -0
  115. package/engineering/tools/webots_mcp/repair.py +132 -0
  116. package/engineering/tools/webots_mcp/repair_trial.py +375 -0
  117. package/engineering/tools/webots_mcp/runtime.py +311 -0
  118. package/engineering/tools/webots_mcp/server.py +217 -0
  119. package/engineering/tools/webots_mcp/sw_export.py +509 -0
  120. package/engineering/tools/webots_mcp/transport.py +156 -0
  121. package/engineering/tools/workflow_gate.py +6134 -0
  122. package/package.json +64 -4
@@ -0,0 +1,321 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ mesh_adapter.py — STEP 导出与网格数据生成适配层
4
+ =================================================
5
+ 负责将 SolidWorks 模型导出为 STEP,并生成可供 FEA 求解器使用的网格数据。
6
+
7
+ 支持模式:
8
+ - Level 0: 解析解(无需网格,直接计算)
9
+ - Level 1: 杆系网格(2D truss/beam 元素)
10
+ - Level 2: Gmsh STEP → MSH(需安装 gmsh)
11
+ - Level 3: SolidWorks Simulation 原生(需 SW API)
12
+
13
+ 本模块优先使用 Level 0/1,降级到 Level 2/3 仅在可用时。
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import json
19
+ import subprocess
20
+ from typing import Any, Optional
21
+
22
+
23
+ # ==================== Level 0: 解析解(无网格) ====================
24
+
25
+ def analytical_beam_cantilever(
26
+ length_mm: float,
27
+ width_mm: float,
28
+ height_mm: float,
29
+ force_n: float,
30
+ E_mpa: float,
31
+ ) -> dict[str, Any]:
32
+ """悬臂梁解析解:端部集中力。
33
+
34
+ Returns:
35
+ {max_stress_mpa, max_deflection_mm, safety_factor, volume_mm3}
36
+ """
37
+ L = length_mm / 1000.0 # m
38
+ b = width_mm / 1000.0
39
+ h = height_mm / 1000.0
40
+ F = force_n
41
+ E = E_mpa # Pa = N/m²
42
+
43
+ # 截面惯性矩 I = b*h³/12
44
+ I = b * h**3 / 12.0
45
+ # 截面模量 W = b*h²/6
46
+ W = b * h**2 / 6.0
47
+ # 最大弯矩 M = F*L(固定端)
48
+ M = F * L
49
+ # 最大应力 σ = M/W
50
+ sigma_max = M / W # Pa
51
+ # 最大挠度 δ = F*L³/(3EI)
52
+ delta_max = F * L**3 / (3.0 * E * I) # m
53
+
54
+ return {
55
+ "method": "analytical_cantilever_beam",
56
+ "max_stress_mpa": round(sigma_max * 1e-6, 2),
57
+ "max_deflection_mm": round(delta_max * 1000, 4),
58
+ "volume_mm3": round(length_mm * width_mm * height_mm, 2),
59
+ "notes": "first-order Euler-Bernoulli beam theory; ignore stress concentrations",
60
+ }
61
+
62
+
63
+ def analytical_simply_supported_beam(
64
+ span_mm: float,
65
+ width_mm: float,
66
+ height_mm: float,
67
+ force_n: float,
68
+ E_mpa: float,
69
+ ) -> dict[str, Any]:
70
+ """简支梁解析解:跨中集中力。"""
71
+ L = span_mm / 1000.0
72
+ b = width_mm / 1000.0
73
+ h = height_mm / 1000.0
74
+ F = force_n
75
+ E = E_mpa
76
+
77
+ I = b * h**3 / 12.0
78
+ W = b * h**2 / 6.0
79
+ M = F * L / 4.0 # 跨中最大弯矩
80
+ sigma_max = M / W
81
+ delta_max = F * L**3 / (48.0 * E * I)
82
+
83
+ return {
84
+ "method": "analytical_simply_supported_beam",
85
+ "max_stress_mpa": round(sigma_max * 1e-6, 2),
86
+ "max_deflection_mm": round(delta_max * 1000, 4),
87
+ "volume_mm3": round(span_mm * width_mm * height_mm, 2),
88
+ }
89
+
90
+
91
+ # ==================== Level 1: 杆系网格(简单框架) ====================
92
+
93
+ def build_truss_mesh(
94
+ nodes: list[tuple[float, float, float]],
95
+ elements: list[tuple[int, int]],
96
+ E_mpa: float,
97
+ area_mm2: float,
98
+ ) -> dict[str, Any]:
99
+ """构建 2D/3D 杆系网格数据(返回结构矩阵摘要)。
100
+
101
+ 仅用于简单 truss 结构的快速评估,不生成完整 MSH 文件。
102
+
103
+ Returns:
104
+ {num_nodes, num_elements, E_mpa, area_mm2, ready: bool}
105
+ """
106
+ return {
107
+ "method": "truss_mesh_summary",
108
+ "num_nodes": len(nodes),
109
+ "num_elements": len(elements),
110
+ "E_mpa": E_mpa,
111
+ "cross_section_area_mm2": area_mm2,
112
+ "ready": True,
113
+ "note": "truss solver not yet implemented; use this as input for external solver",
114
+ }
115
+
116
+
117
+ # ==================== Level 2: Gmsh STEP → MSH ====================
118
+
119
+ def check_gmsh_available() -> bool:
120
+ """检查 Gmsh 是否已安装。"""
121
+ try:
122
+ result = subprocess.run(
123
+ ["gmsh", "--version"],
124
+ capture_output=True, text=True, timeout=5
125
+ )
126
+ return result.returncode == 0
127
+ except (FileNotFoundError, subprocess.TimeoutExpired):
128
+ return False
129
+
130
+
131
+ def export_step_from_swapi(part_path: str, step_path: str) -> dict[str, Any]:
132
+ """通过 sw_bridge.py 导出 STEP。
133
+
134
+ 依赖:sw_bridge.py run 执行一个临时脚本导出 STEP。
135
+ """
136
+ import sys
137
+ tools_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
138
+ bridge_script = os.path.join(tools_dir, "sw_bridge.py")
139
+
140
+ script_content = f'''
141
+ import sys, os, win32com.client, pythoncom
142
+ sys.path.insert(0, r"{tools_dir}")
143
+ import swapi
144
+ sw = swapi.get_sw()
145
+ # Open part if not already open
146
+ for i in range(sw.GetDocumentCount):
147
+ try:
148
+ d = sw.GetDocument(i)
149
+ if d and d.GetPathName == r"{part_path}":
150
+ break
151
+ except: pass
152
+ else:
153
+ sw.OpenDoc(r"{part_path}", 1)
154
+ doc = sw.ActiveDoc
155
+ step_path = r"{step_path}"
156
+ try:
157
+ rc = doc.SaveAs(step_path)
158
+ print(f"STEP exported: {{step_path}} (rc={{rc}})")
159
+ except Exception as e:
160
+ print(f"STEP export error: {{e}}", file=sys.stderr)
161
+ '''
162
+ script_file = os.path.join(tools_dir, "_tmp_step_export.py")
163
+ try:
164
+ with open(script_file, "w", encoding="utf-8") as f:
165
+ f.write(script_content)
166
+ result = subprocess.run(
167
+ [sys.executable, script_file],
168
+ capture_output=True, text=True, timeout=60
169
+ )
170
+ if result.returncode != 0:
171
+ return {"ok": False, "error": result.stderr or "SW export failed"}
172
+ if os.path.exists(step_path):
173
+ return {"ok": True, "step_path": step_path, "size_bytes": os.path.getsize(step_path)}
174
+ return {"ok": False, "error": "STEP file not created"}
175
+ finally:
176
+ if os.path.exists(script_file):
177
+ os.remove(script_file)
178
+
179
+
180
+ def generate_mesh_gmsh(step_path: str, msh_path: str, order: int = 1) -> dict[str, Any]:
181
+ """使用 Gmsh 生成网格。
182
+
183
+ Args:
184
+ step_path: 输入的 STEP 文件路径
185
+ msh_path: 输出的 MSH 文件路径
186
+ order: 单元阶次(1=线性,2=二次)
187
+
188
+ Returns:
189
+ {ok, num_elements, num_nodes, element_types}
190
+ """
191
+ if not check_gmsh_available():
192
+ return {"ok": False, "error": "Gmsh not found; install from https://gmsh.info"}
193
+
194
+ gmsh_script = f'''
195
+ import gmsh
196
+ import sys
197
+
198
+ gmsh.initialize()
199
+ gmsh.model.add("mesh")
200
+ gmsh.merge(r"{step_path}")
201
+ gmsh.model.mesh.generate(3) # 3D mesh
202
+ gmsh.option.setNumber("Mesh.Order", {order})
203
+ gmsh.write(r"{msh_path}")
204
+ gmsh.finalize()
205
+ print(f"Mesh generated: {{msh_path}}")
206
+ '''
207
+ script_file = os.path.join(tools_dir, "_tmp_gmsh.py") if 'tools_dir' in dir() else None
208
+ # Inline approach: write a temporary Python script
209
+ import tempfile
210
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f:
211
+ f.write(gmsh_script)
212
+ tmp_script = f.name
213
+
214
+ try:
215
+ result = subprocess.run(
216
+ [sys.executable, tmp_script],
217
+ capture_output=True, text=True, timeout=120
218
+ )
219
+ os.unlink(tmp_script)
220
+ if result.returncode != 0:
221
+ return {"ok": False, "error": result.stderr, "stdout": result.stdout}
222
+ if not os.path.exists(msh_path):
223
+ return {"ok": False, "error": "MSH file not created"}
224
+ return {"ok": True, "msh_path": msh_path}
225
+ except Exception as e:
226
+ return {"ok": False, "error": str(e)}
227
+
228
+
229
+ # ==================== Level 3: SolidWorks Simulation API ====================
230
+
231
+ def check_sw_simulation_available(sw_app) -> bool:
232
+ """检查 SolidWorks Simulation 插件是否可用。"""
233
+ try:
234
+ # Simulation 插件 GUID
235
+ sim_guid = "{E3C2B4A0-6A1F-4B5E-9D8F-1A2B3C4D5E6F}" # 占位符,实际需查询
236
+ sw_app.GetAddInClient2(sim_guid)
237
+ return True
238
+ except Exception:
239
+ pass
240
+ # 尝试通过 COM 类名检测
241
+ try:
242
+ sw_app.GetAddInClient2("{3B0FF6F0-4B6E-11D4-B5E3-00C04F68DDB5}")
243
+ return True
244
+ except Exception:
245
+ return False
246
+
247
+
248
+ def create_simulation_study(sw, model, study_name: str, material_id: str,
249
+ boundary_conditions: list, loads: list) -> dict[str, Any]:
250
+ """创建 SolidWorks Simulation 静态分析研究。
251
+
252
+ 注意:此函数需要 SolidWorks Simulation 已安装且授权。
253
+ """
254
+ if not check_sw_simulation_available(sw):
255
+ return {"ok": False, "error": "SolidWorks Simulation not available"}
256
+
257
+ # 实际实现需要大量 COM API 调用,此处返回降级提示
258
+ return {
259
+ "ok": False,
260
+ "error": "SolidWorks Simulation API implementation pending; "
261
+ "use Level 0/1/2 analysis instead",
262
+ "fallback": "analytical or gmsh-based solver",
263
+ }
264
+
265
+
266
+ # ==================== 统一接口 ====================
267
+
268
+ def adapt_mesh(
269
+ part_path: str,
270
+ load_case: dict,
271
+ output_dir: str,
272
+ level: int = 0,
273
+ ) -> dict[str, Any]:
274
+ """统一网格适配入口。
275
+
276
+ Args:
277
+ part_path: SolidWorks 零件路径
278
+ load_case: 载荷工况字典(来自 load_case.py)
279
+ output_dir: 输出目录
280
+ level: 0=解析, 1=杆系, 2=Gmsh, 3=SW Simulation
281
+
282
+ Returns:
283
+ {ok, method, mesh_path, stats}
284
+ """
285
+ os.makedirs(output_dir, exist_ok=True)
286
+ mat = load_case.get("material", {})
287
+ E = mat.get("youngs_modulus_mpa", 200000)
288
+
289
+ if level == 0:
290
+ # 解析解模式
291
+ domain = load_case.get("design_domain", {}).get("bounds", {})
292
+ loads = load_case.get("loads", [])
293
+ total_force = sum(l.get("magnitude_n", 0) for l in loads
294
+ if l.get("type") in ("distributed_force", "concentrated_force"))
295
+ result = analytical_beam_cantilever(
296
+ length_mm=domain.get("x_max", 200),
297
+ width_mm=domain.get("y_max", 60),
298
+ height_mm=domain.get("z_max", 60),
299
+ force_n=total_force,
300
+ E_mpa=E,
301
+ )
302
+ result["load_case"] = load_case.get("meta", {}).get("problem_id", "unknown")
303
+ return {"ok": True, "method": "analytical", "result": result}
304
+
305
+ elif level == 1:
306
+ # 杆系模式(暂不支持自动网格生成)
307
+ return {"ok": False, "error": "truss mesh not yet fully implemented"}
308
+
309
+ elif level == 2:
310
+ # Gmsh 模式
311
+ step_path = os.path.join(output_dir, "model.step")
312
+ msh_path = os.path.join(output_dir, "model.msh")
313
+
314
+ step_result = export_step_from_swapi(part_path, step_path)
315
+ if not step_result.get("ok"):
316
+ return step_result
317
+
318
+ return generate_mesh_gmsh(step_path, msh_path, order=1)
319
+
320
+ else:
321
+ return {"ok": False, "error": f"unknown mesh level: {level}"}
@@ -0,0 +1,272 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ refine_rules.py — 自动修正规则引擎
4
+ ====================================
5
+ 根据仿真反馈,生成可执行的参数修改建议。
6
+
7
+ 策略原则:
8
+ 1. 只修改设计参数白名单中的参数
9
+ 2. 不触碰用户锁定参数(locked_params)
10
+ 3. 优先局部修正,不改变整体拓扑
11
+ 4. 每次修正有明确的物理依据
12
+ 5. 不超过最大迭代次数
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import math
17
+ from typing import Any, Optional
18
+
19
+
20
+ # ── 修正动作类型 ──────────────────────────────────────
21
+ ACTION_INCREASE_THICKNESS = "increase_thickness"
22
+ ACTION_INCREASE_FILLET = "increase_fillet"
23
+ ACTION_ADD_RIB = "add_rib"
24
+ ACTION_REDUCE_VOLUME = "reduce_volume"
25
+ ACTION_CHANGE_SHAPE = "change_shape"
26
+ ACTION_LOCK_PARAM = "lock_param"
27
+ ACTION_FLAG_TBD = "flag_tbd"
28
+
29
+
30
+ def generate_recommendations(
31
+ report: dict,
32
+ design_params: dict,
33
+ locked_params: list[str],
34
+ max_iterations: int = 5,
35
+ ) -> dict[str, Any]:
36
+ """基于仿真报告生成修正建议。
37
+
38
+ Args:
39
+ report: simulation_report.build_report 的输出
40
+ design_params: 当前设计参数字典
41
+ locked_params: 不可修改的参数名列表
42
+ max_iterations: 最大剩余迭代次数
43
+
44
+ Returns:
45
+ {ok, actions, next_params, reason, risk}
46
+ """
47
+ actions = []
48
+ fea = report.get("fea_result", {})
49
+ acc = report.get("acceptance_criteria", {})
50
+ missing_params = {}
51
+
52
+ # ── 安全系数过低 → 增加壁厚/圆角/加筋 ──
53
+ sf = fea.get("safety_factor")
54
+ min_sf = acc.get("min_safety_factor", 2.0)
55
+ max_sf = acc.get("target_safety_factor_max", 5.0)
56
+
57
+ # ══ 【PH-02~06 修复】SF 必须【先校验类型与量级】,再参与判定 ═══════════
58
+ # 实测(对抗测试物理深挖房)暴露 5 个绕过/崩溃点,全部源于"直接拿 sf 比较":
59
+ # PH-02 NaN : `nan < min_sf` 恒为 False → 跳过整个强度分支,
60
+ # 被当成"设计已最优"放行(n_actions=0),绕过强度防线;
61
+ # PH-03 字符串 : `'abc' < 2.0` 抛 TypeError(未捕获)→ 整个 optimize 崩;
62
+ # PH-04 布尔 : `True < 2.0` 为 True(float(True)=1.0)→ 误判强度不足;
63
+ # PH-05 None : 被 `sf is not None` 排除 → 落入"已最优"放行,
64
+ # 掩盖了 FEA 失败的真相;
65
+ # PH-06 极小值 : SF=1e-6 只判 `< min_sf` 就放行,并算 sqrt(min/sf)≈1414
66
+ # 倍盲加厚 —— 实际该值已意味着"结果不可信"。
67
+ # 统一口径:SF 必须是【有限的正数且不低于最低可信下限】。
68
+ # 否则一律 fail-closed(拒绝给出修正建议),并如实说明原因 ——
69
+ # 绝不在非法/不可信的校核结果上继续迭代。
70
+ _SF_MIN_CREDIBLE = 0.5
71
+ _sf_raw = sf
72
+ _sf_num = None
73
+ _sf_problem = None
74
+ if _sf_raw is None:
75
+ _sf_problem = "缺失(None)—— FEA 未产出安全系数,可能是求解失败"
76
+ elif isinstance(_sf_raw, bool):
77
+ _sf_problem = "是布尔值 %r(不是数值)" % (_sf_raw,)
78
+ else:
79
+ try:
80
+ _sf_num = float(_sf_raw)
81
+ except Exception:
82
+ _sf_problem = "不是数值(%r)" % (_sf_raw,)
83
+ else:
84
+ if _sf_num != _sf_num:
85
+ _sf_problem = "是 NaN(非数值)—— 通常意味着除零/无效输入"
86
+ elif _sf_num in (float("inf"), float("-inf")):
87
+ # 正无穷:应力为 0(无载荷/几何异常),交由下方"过度设计"分支处理
88
+ if _sf_num < 0:
89
+ _sf_problem = "是负无穷(非法)"
90
+ elif _sf_num <= 0:
91
+ _sf_problem = "≤ 0(材料已进入破坏区,或载荷输入不合理)"
92
+ elif _sf_num < _SF_MIN_CREDIBLE:
93
+ _sf_problem = ("=%.3g 低于可信下限 %.1f —— 结果不可信,"
94
+ "不应据此给出加厚建议"
95
+ % (_sf_num, _SF_MIN_CREDIBLE))
96
+ if _sf_problem:
97
+ _actions = [{
98
+ "action": "REJECT_UNRELIABLE_SAFETY_FACTOR",
99
+ "param_id": None,
100
+ "from": _sf_raw,
101
+ "to": None,
102
+ "reason": ("安全系数不可信:%s。拒绝在非法/不可信的校核结果上"
103
+ "继续迭代(fail-closed);请先复核载荷数值与单位、"
104
+ "零件几何与材料属性,再重新校核。" % _sf_problem),
105
+ "priority": 0,
106
+ "blocking": True,
107
+ }]
108
+ return {
109
+ "ok": False,
110
+ "actions": _actions,
111
+ "next_params": {},
112
+ "reason": ("安全系数不可用(%s)—— 已 fail-closed,拒绝给出修正建议。"
113
+ % _sf_problem),
114
+ "risk": "high",
115
+ "rejected": True,
116
+ "reject_reason": _sf_problem,
117
+ }
118
+ # 到这里 sf 已是【有限正数且 ≥ 可信下限】,可安全参与后续比较与开方
119
+ sf = _sf_num
120
+
121
+ if sf < min_sf:
122
+ # SF 的类型/量级校验已在函数开头统一完成(PH-02~06 修复):
123
+ # 到这里 sf 必定是【有限正数且 ≥ 可信下限】,可安全开方。
124
+ # 强度不足:增加壁厚
125
+ thickness_param = design_params.get("thickness_mm")
126
+ if thickness_param and thickness_param["id"] not in locked_params:
127
+ current = thickness_param.get("value", 5)
128
+ min_val = thickness_param.get("min", 2)
129
+ # 按应力平方根比例估算所需厚度增量
130
+ scale = math.sqrt(min_sf / sf)
131
+ new_thickness = max(current * scale, current + 1.0, min_val)
132
+ new_thickness = min(new_thickness, thickness_param.get("max", 50))
133
+ actions.append({
134
+ "action": ACTION_INCREASE_THICKNESS,
135
+ "param_id": thickness_param["id"],
136
+ "from": current,
137
+ "to": round(new_thickness, 1),
138
+ "reason": f"safety factor {sf:.2f} < {min_sf}, scale ~{scale:.2f}",
139
+ "priority": 1,
140
+ })
141
+ else:
142
+ # 尝试增加圆角
143
+ fillet_param = design_params.get("fillet_mm")
144
+ if fillet_param and fillet_param.get("id") not in locked_params:
145
+ current_f = fillet_param.get("value", 2)
146
+ new_f = min(current_f * 1.5, fillet_param.get("max", 15))
147
+ actions.append({
148
+ "action": ACTION_INCREASE_FILLET,
149
+ "param_id": fillet_param["id"],
150
+ "from": current_f,
151
+ "to": round(new_f, 1),
152
+ "reason": f"safety factor {sf:.2f} too low; try larger fillet at stress concentration",
153
+ "priority": 2,
154
+ })
155
+
156
+ # ── 安全系数过高 → 减材优化 ──
157
+ if sf is not None and sf > max_sf:
158
+ thickness_param = design_params.get("thickness_mm")
159
+ if thickness_param and thickness_param.get("id") not in locked_params:
160
+ # [Bug7 连带修复] value 可能为 None(真实几何未取得壁厚时)。
161
+ # 上游改为从真实零件取几何后,拿不到壁厚则 value=None;
162
+ # 原实现直接 current * scale 会 TypeError 打崩整个 optimize。
163
+ _cur = thickness_param.get("value")
164
+ try:
165
+ _cur = float(_cur) if _cur is not None else None
166
+ except Exception:
167
+ _cur = None
168
+ _min_t = thickness_param.get("min")
169
+ try:
170
+ _min_t = float(_min_t) if _min_t is not None else 2.0
171
+ except Exception:
172
+ _min_t = 2.0
173
+ if _cur is not None and _cur > 0:
174
+ # 安全系数与厚度的平方近似成反比(梁弯曲)
175
+ scale = math.sqrt(max_sf / sf)
176
+ new_thickness = max(_cur * scale, _min_t)
177
+ new_thickness = min(new_thickness, _cur - 0.5)
178
+ if new_thickness > 0:
179
+ actions.append({
180
+ "action": ACTION_REDUCE_VOLUME,
181
+ "param_id": thickness_param["id"],
182
+ "from": _cur,
183
+ "to": round(new_thickness, 1),
184
+ "reason": f"safety factor {sf:.2f} > {max_sf}, reduce material",
185
+ "priority": 1,
186
+ })
187
+
188
+ # ── 位移超限 → 增大截面惯性矩 ──
189
+ max_disp = fea.get("max_displacement_mm")
190
+ max_disp_req = acc.get("max_displacement_mm")
191
+ if max_disp is not None and max_disp_req is not None and max_disp > max_disp_req:
192
+ height_param = design_params.get("height_mm")
193
+ if height_param and height_param.get("id") not in locked_params:
194
+ current_h = height_param.get("value", 60)
195
+ # 悬臂梁挠度 ∝ 1/h³,所需高度增量 ≈ (disp/ratio)^(1/3)
196
+ scale = (max_disp / max_disp_req) ** (1.0 / 3.0)
197
+ new_h = min(current_h * scale, height_param.get("max", 200))
198
+ actions.append({
199
+ "action": ACTION_CHANGE_SHAPE,
200
+ "param_id": height_param["id"],
201
+ "from": current_h,
202
+ "to": round(new_h, 1),
203
+ "reason": f"displacement {max_disp:.3f}mm > {max_disp_req}mm; increase height by ~{scale:.2f}x",
204
+ "priority": 1,
205
+ })
206
+
207
+ # ── 几何门禁失败 ──
208
+ geom = report.get("geometry_gate", {})
209
+ for gate in geom.get("gates", []):
210
+ if gate.get("status") == "FAIL":
211
+ gid = gate.get("id", "")
212
+ if gid == "DESIGN_SPACE":
213
+ actions.append({
214
+ "action": ACTION_FLAG_TBD,
215
+ "reason": f"design space violation detected: {gate}",
216
+ "priority": 0,
217
+ })
218
+ elif gid == "WALL_THICKNESS":
219
+ actions.append({
220
+ "action": ACTION_INCREASE_THICKNESS,
221
+ "reason": f"wall thickness below minimum: {gate}",
222
+ "priority": 1,
223
+ })
224
+
225
+ # ── 排序并裁剪到最大迭代 ──
226
+ actions.sort(key=lambda a: a.get("priority", 99))
227
+ remaining_iters = max_iterations - report.get("iteration", 1)
228
+ actions = actions[:remaining_iters]
229
+
230
+ # ── 构建 next_params ──
231
+ next_params = dict(design_params)
232
+ for act in actions:
233
+ pid = act.get("param_id")
234
+ if pid and pid in next_params:
235
+ next_params[pid]["value"] = act.get("to", next_params[pid].get("value"))
236
+
237
+ if not actions:
238
+ return {
239
+ "ok": True,
240
+ "actions": [],
241
+ "next_params": next_params,
242
+ "reason": "no actionable recommendations; design may already be optimal or outside parameter space",
243
+ "risk": "NONE",
244
+ }
245
+
246
+ return {
247
+ "ok": True,
248
+ "actions": actions,
249
+ "next_params": next_params,
250
+ "reason": f"{len(actions)} recommendation(s) generated for iteration {report.get('iteration', 1) + 1}",
251
+ "risk": "MEDIUM" if len(actions) > 2 else "LOW",
252
+ }
253
+
254
+
255
+ def validate_action(action: dict, design_params: dict, locked_params: list[str]) -> dict:
256
+ """校验单个修正动作是否合法。"""
257
+ errors = []
258
+ pid = action.get("param_id", "")
259
+ if pid in locked_params:
260
+ errors.append(f"param '{pid}' is locked and cannot be modified")
261
+ if pid not in design_params:
262
+ errors.append(f"param '{pid}' not found in design_params")
263
+ param = design_params.get(pid, {})
264
+ new_val = action.get("to")
265
+ if new_val is not None:
266
+ mn = param.get("min", -float("inf"))
267
+ mx = param.get("max", float("inf"))
268
+ if new_val < mn:
269
+ errors.append(f"new value {new_val} < min {mn}")
270
+ if new_val > mx:
271
+ errors.append(f"new value {new_val} > max {mx}")
272
+ return {"ok": len(errors) == 0, "errors": errors}