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,314 @@
1
+ # -*- coding: utf-8 -*-
2
+ """feapy_solver.py -- Pure Python FEA (2D CST + 3D CTE)"""
3
+ import numpy as np
4
+
5
+
6
+ class MaterialPropertyError(ValueError):
7
+ """材料属性缺失/非法(fail-closed:宁可不计算,也不给出错误结论)。"""
8
+
9
+
10
+ def _require_positive(value, name):
11
+ """校验材料参数为【正的有限数】,否则抛 MaterialPropertyError。
12
+
13
+ ── 【PH-01 修复】为什么必须在这里兜底 ────────────────────────────────
14
+ 实测缺陷:swapi.COMMON_MATERIALS 原先【全部材料都缺 poissons_ratio】,
15
+ 于是 nu=None 一路传到 `nu**2` → TypeError:
16
+ unsupported operand type(s) for ** or pow(): 'NoneType' and 'int'
17
+ 整个 FEA 挂掉,SF=N/A、mass=null、status=UNKNOWN,物理防线整体失效。
18
+ 根治是补全材料表(已做),但【不该只靠数据齐全】—— 只要上游任何环节
19
+ 漏传/传 None,就会以同样的方式崩在深处。因此在入口做 fail-closed:
20
+ 参数非法 → 抛明确异常,由调用方转成"不可判定",绝不用 nan/0 顶替。
21
+ """
22
+ if value is None:
23
+ raise MaterialPropertyError(
24
+ "材料参数 %s 缺失(None)—— 无法计算;"
25
+ "请检查材料是否真正赋到零件上(密度=1000/属性缺省都属未赋材)" % name)
26
+ if isinstance(value, bool):
27
+ raise MaterialPropertyError("材料参数 %s 是布尔值,不是数值" % name)
28
+ try:
29
+ v = float(value)
30
+ except Exception:
31
+ raise MaterialPropertyError("材料参数 %s=%r 不是数值" % (name, value))
32
+ if v != v or v in (float("inf"), float("-inf")):
33
+ raise MaterialPropertyError("材料参数 %s=%r 不是有限数" % (name, value))
34
+ if v <= 0:
35
+ raise MaterialPropertyError("材料参数 %s=%r 必须为正数" % (name, value))
36
+ return v
37
+
38
+
39
+ def _check_nu(nu):
40
+ """泊松比专项校验:必须落在 (-1, 0.5) —— 这是各向同性弹性体的物理边界。
41
+
42
+ 0.5 对应不可压缩材料(会导致体积模量无穷大、刚度矩阵奇异);
43
+ 负值超出热力学稳定范围。这里明确拦下,避免"算出结果但结果无意义"。
44
+ """
45
+ # ⚠️ 先做类型/缺失校验:直接写 float(nu) 会在 nu=None 时抛 TypeError,
46
+ # 那是"崩在深处",而不是 fail-closed 的明确拒绝(PH-01 的原始症状)。
47
+ if nu is None:
48
+ raise MaterialPropertyError(
49
+ "材料参数 nu(泊松比)缺失(None)—— 无法组装刚度矩阵;"
50
+ "请检查材料是否真正赋到零件上(未赋材/属性缺省都会导致 None)")
51
+ if isinstance(nu, bool):
52
+ raise MaterialPropertyError("材料参数 nu(泊松比)是布尔值,不是数值")
53
+ try:
54
+ _nv = float(nu)
55
+ except Exception:
56
+ raise MaterialPropertyError("材料参数 nu=%r 不是数值" % (nu,))
57
+ if _nv != _nv or _nv in (float("inf"), float("-inf")):
58
+ raise MaterialPropertyError("材料参数 nu=%r 不是有限数" % (nu,))
59
+ _require_positive(1.0 - 2.0 * _nv, "nu(1-2ν)") # 排除 nu >= 0.5
60
+ _require_positive(1.0 + _nv, "nu(1+ν)") # 排除 nu <= -1
61
+ return _nv
62
+
63
+
64
+ def _D_2d(E, nu, model):
65
+ E = _require_positive(E, "E")
66
+ nu = _check_nu(nu)
67
+ if model == "plane_stress":
68
+ return E / (1 - nu**2) * np.array([[1, nu, 0], [nu, 1, 0], [0, 0, (1 - nu) / 2]])
69
+ elif model == "plane_strain":
70
+ return E / ((1 + nu) * (1 - 2 * nu)) * np.array([[1 - nu, nu, 0], [nu, 1 - nu, 0], [0, 0, (1 - 2 * nu) / 2]])
71
+ raise ValueError(f"Unknown 2D model: {model}")
72
+
73
+
74
+ def _D_3d(E, nu):
75
+ E = _require_positive(E, "E")
76
+ nu = _check_nu(nu)
77
+ la = E * nu / ((1 + nu) * (1 - 2 * nu))
78
+ mu = E / (2 * (1 + nu))
79
+ return np.array([[la + 2*mu, la, la, 0, 0, 0],
80
+ [la, la+2*mu, la, 0, 0, 0],
81
+ [la, la, la+2*mu, 0, 0, 0],
82
+ [0, 0, 0, mu, 0, 0],
83
+ [0, 0, 0, 0, mu, 0],
84
+ [0, 0, 0, 0, 0, mu]])
85
+
86
+
87
+ def _cst_B(x1,y1,x2,y2,x3,y3,area):
88
+ if area < 1e-10: return np.zeros((3,6))
89
+ b1,b2,b3=y2-y3,y3-y1,y1-y2
90
+ c1,c2,c3=x3-x2,x1-x3,x2-x1
91
+ return np.array([[b1,0,b2,0,b3,0],[0,c1,0,c2,0,c3],[c1,b1,c2,b2,c3,b3]])/(2.0*area)
92
+
93
+
94
+ def _cte_B_vol(coords):
95
+ x1,y1,z1=coords[0]; x2,y2,z2=coords[1]
96
+ x3,y3,z3=coords[2]; x4,y4,z4=coords[3]
97
+ vol=(x2-x1)*((y3-y1)*(z4-z1)-(z3-z1)*(y4-y1)) \
98
+ -(y2-y1)*((x3-x1)*(z4-z1)-(z3-z1)*(x4-x1)) \
99
+ +(z2-z1)*((x3-x1)*(y4-y1)-(y3-y1)*(x4-x1))
100
+ vol=abs(vol)/6.0
101
+ if vol<1e-10: return np.zeros((6,12)),0.0
102
+ M=np.array([[1,x1,y1,z1],[1,x2,y2,z2],[1,x3,y3,z3],[1,x4,y4,z4]])
103
+ invM=np.linalg.inv(M)
104
+ bz=[invM[1,0],invM[1,1],invM[1,2],invM[1,3]]
105
+ cz=[invM[2,0],invM[2,1],invM[2,2],invM[2,3]]
106
+ dz=[invM[3,0],invM[3,1],invM[3,2],invM[3,3]]
107
+ B=np.zeros((6,12))
108
+ for i in range(4):
109
+ B[0,i*3]=bz[i]; B[1,i*3+1]=cz[i]; B[2,i*3+2]=dz[i]
110
+ B[3,i*3+1]=dz[i]; B[3,i*3+2]=cz[i]
111
+ B[4,i*3]=dz[i]; B[4,i*3+2]=bz[i]
112
+ B[5,i*3]=cz[i]; B[5,i*3+1]=bz[i]
113
+ return B,vol
114
+
115
+
116
+ def mesh_rect(x0,x1,y0,y1,nx,ny):
117
+ nodes=[]
118
+ for i in range(nx+1):
119
+ for j in range(ny+1):
120
+ nodes.append([x0+i*(x1-x0)/nx, y0+j*(y1-y0)/ny, 0.0])
121
+ nodes=np.array(nodes)
122
+ elems=[]
123
+ for i in range(nx):
124
+ for j in range(ny):
125
+ idx=i*(ny+1)+j
126
+ elems.append([idx,idx+1,idx+(ny+1)])
127
+ elems.append([idx+1,idx+(ny+1)+1,idx+(ny+1)])
128
+ return nodes,np.array(elems)
129
+
130
+
131
+ def mesh_box(x0,x1,y0,y1,z0,z1,nx,ny,nz):
132
+ nodes=[]
133
+ for i in range(nx+1):
134
+ for j in range(ny+1):
135
+ for k in range(nz+1):
136
+ nodes.append([x0+i*(x1-x0)/nx, y0+j*(y1-y0)/ny, z0+k*(z1-z0)/nz])
137
+ nodes=np.array(nodes)
138
+ elems=[]
139
+ for i in range(nx):
140
+ for j in range(ny):
141
+ for k in range(nz):
142
+ idx=i*(ny+1)*(nz+1)+j*(nz+1)+k
143
+ v=[idx,idx+1,idx+(nz+1),idx+(nz+1)+1,
144
+ idx+(ny+1)*(nz+1),idx+(ny+1)*(nz+1)+1,
145
+ idx+(ny+1)*(nz+1)+(nz+1),idx+(ny+1)*(nz+1)+(nz+1)+1]
146
+ elems.append([v[0],v[1],v[2],v[4]])
147
+ elems.append([v[1],v[3],v[2],v[6]])
148
+ elems.append([v[1],v[5],v[3],v[7]])
149
+ elems.append([v[1],v[2],v[3],v[6]])
150
+ elems.append([v[1],v[3],v[6],v[7]])
151
+ elems.append([v[1],v[4],v[5],v[7]])
152
+ return nodes,np.array(elems)
153
+
154
+
155
+ class FEASolver:
156
+ def __init__(self,nodes,elements,model="plane_stress",thickness=1.0):
157
+ self.nodes=np.array(nodes); self.elements=np.array(elements)
158
+ self.model=model; self.N=len(nodes); self.thickness=thickness
159
+ self.nd=3 if model=="3d" else 2
160
+ self.ndof=self.N*self.nd
161
+
162
+ def solve(self,E,nu,fixed=None,loads=None):
163
+ D=_D_3d(E,nu) if self.model=="3d" else _D_2d(E,nu,self.model)
164
+ nd=self.nd; K=np.zeros((self.ndof,self.ndof))
165
+ Bcache={}
166
+ elem_volumes=[]
167
+
168
+ for elem in self.elements:
169
+ nn=[int(elem[m]) for m in range(len(elem))]
170
+ key=tuple(nn); coords=self.nodes[nn]
171
+ if nd==2:
172
+ x1,y1,_=coords[0]; x2,y2,_=coords[1]; x3,y3,_=coords[2]
173
+ area=abs(x1*(y2-y3)+x2*(y3-y1)+x3*(y1-y2))*0.5
174
+ Bcache[key]=_cst_B(x1,y1,x2,y2,x3,y3,area)
175
+ elem_volumes.append(("2d",area))
176
+ else:
177
+ Bcache[key],vol=_cte_B_vol(coords)
178
+ elem_volumes.append(("3d",vol))
179
+
180
+ B=Bcache[key]; val=elem_volumes[-1][1]
181
+ if nd==2:
182
+ Ke=B.T@D@B*val*self.thickness
183
+ else:
184
+ Ke=B.T@D@B*val
185
+ dofs=[]
186
+ for n in nn:
187
+ for d in range(nd):
188
+ dofs.append(n*nd+d)
189
+ for a in range(len(dofs)):
190
+ for bb in range(len(dofs)):
191
+ K[dofs[a],dofs[bb]]+=Ke[a,bb]
192
+
193
+ F=np.zeros(self.ndof)
194
+ if loads:
195
+ for item in loads:
196
+ ni=int(item[0])
197
+ F[ni*nd]+=item[1] if len(item)>1 else 0
198
+ F[ni*nd+1]+=item[2] if len(item)>2 else 0
199
+ if nd==3 and len(item)>3:
200
+ F[ni*nd+2]+=item[3]
201
+ if fixed:
202
+ pen=1e15
203
+ for item in fixed:
204
+ ni,d=int(item[0]),int(item[1])
205
+ K[ni*nd+d,ni*nd+d]+=pen
206
+ U=np.linalg.solve(K,F)
207
+
208
+ # von Mises
209
+ vm=[]
210
+ for idx,elem in enumerate(self.elements):
211
+ nn=[int(elem[m]) for m in range(len(elem))]
212
+ key=tuple(nn); B=Bcache[key]
213
+ etype,vval=elem_volumes[idx]
214
+ if nd==2:
215
+ u_e=[]
216
+ for m in range(3):
217
+ n=nn[m]
218
+ u_e.append(U[n*2]); u_e.append(U[n*2+1])
219
+ strain=B@np.array(u_e); stress=D@strain
220
+ sx,sy,txy=stress
221
+ vm.append(np.sqrt(sx**2-sx*sy+sy**2+3*txy**2))
222
+ else:
223
+ u_e=[]
224
+ for m in range(4):
225
+ n=nn[m]
226
+ u_e.append(U[n*3]); u_e.append(U[n*3+1]); u_e.append(U[n*3+2])
227
+ strain=B@np.array(u_e); stress=D@strain
228
+ sx,sy,sz,tyz,txz,txy=stress
229
+ vm.append(np.sqrt(0.5*((sx-sy)**2+(sy-sz)**2+(sz-sx)**2+6*(tyz**2+txz**2+txy**2))))
230
+
231
+ if nd==2:
232
+ disp=np.sqrt(U[0::2]**2+U[1::2]**2)
233
+ else:
234
+ disp=np.sqrt(U[0::3]**2+U[1::3]**2+U[2::3]**2)
235
+
236
+ return {"max_displacement_mm":float(disp.max()),"max_von_mises_mpa":float(max(vm)) if vm else 0.0,"displacement_field":U,"von_mises_per_element":vm}
237
+
238
+
239
+ def solve_cantilever_2d(L,H,T,F_load,E,nu,nx=40,ny=12):
240
+ nodes,elems=mesh_rect(0,L,0,H,nx,ny)
241
+ s=FEASolver(nodes,elems,model="plane_stress",thickness=T)
242
+ fixed=[]
243
+ for j in range(ny+1):
244
+ n=j; fixed.append((n,0)); fixed.append((n,1))
245
+ loads=[]
246
+ nr=nx*(ny+1)
247
+ for j in range(ny+1):
248
+ n=nr+j; loads.append((n,0.0,-F_load/(ny+1)))
249
+ r=s.solve(E,nu,fixed=fixed,loads=loads)
250
+ I=T*H**3/12; de=F_load*L**3/(3*E*I); se=6*F_load*L/(T*H**2)
251
+ r["analytical"]={"delta_mm":de,"sigma_mpa":se,"disp_error_pct":abs(r["max_displacement_mm"]/de-1)*100 if de else 0,"stress_error_pct":abs(r["max_von_mises_mpa"]/se-1)*100 if se else 0}
252
+ return r
253
+
254
+
255
+ def solve_cantilever_3d(L,H,D,F_load,E,nu,nx=10,ny=5,nz=5):
256
+ nodes,elems=mesh_box(0,L,0,H,0,D,nx,ny,nz)
257
+ s=FEASolver(nodes,elems,model="3d")
258
+ tol=L/(2*nx)
259
+ fixed=[]
260
+ for ii,node in enumerate(nodes):
261
+ if node[0]<tol:
262
+ fixed.append((ii,0)); fixed.append((ii,1)); fixed.append((ii,2))
263
+ rn=[]
264
+ for ii,node in enumerate(nodes):
265
+ if node[0]>L-tol: rn.append(ii)
266
+ rc=len(rn)
267
+ loads=[]
268
+ for ii in rn:
269
+ loads.append((ii,0.0,-F_load/rc))
270
+ r=s.solve(E,nu,fixed=fixed,loads=loads)
271
+ I=D*H**3/12; de=F_load*L**3/(3*E*I); se=6*F_load*L/(D*H**2)
272
+ r["analytical"]={"delta_mm":de,"sigma_mpa":se,"disp_error_pct":abs(r["max_displacement_mm"]/de-1)*100 if de else 0,"stress_error_pct":abs(r["max_von_mises_mpa"]/se-1)*100 if se else 0}
273
+ return r
274
+
275
+
276
+ def solve_cantilever(L, H, W_or_T, F_load, E, nu, nx=10, ny=5, nz=0):
277
+ """兼容接口:自动检测 2D/3D。
278
+
279
+ - 若 nz==0 或 W_or_T 与 H 同量级 → 2D 平面应力
280
+ - 否则 → 3D 四面体
281
+ """
282
+ if nz == 0 or abs(W_or_T - H) < 1e-9:
283
+ return solve_cantilever_2d(L, H, W_or_T, F_load, E, nu, nx=nx, ny=ny)
284
+ else:
285
+ return solve_cantilever_3d(L, H, W_or_T, F_load, E, nu, nx=nx, ny=ny, nz=nz)
286
+
287
+
288
+ if __name__=="__main__":
289
+ import sys; sys.stdout.reconfigure(encoding="utf-8")
290
+ print("="*60); print("2D+3D FEA Test"); print("="*60); print()
291
+ print("[1] 2D Cantilever")
292
+ r2d=solve_cantilever_2d(200,60,60,500,68900,0.33,nx=40,ny=12)
293
+ n2d,e2d=mesh_rect(0,200,0,60,40,12)
294
+ print(f" Mesh: {len(n2d)} nodes, {len(e2d)} elems")
295
+ print(f" Disp: {r2d['max_displacement_mm']:.4f}mm (exact:{r2d['analytical']['delta_mm']:.4f}, err:{r2d['analytical']['disp_error_pct']:.1f}%)")
296
+ print(f" Sigma: {r2d['max_von_mises_mpa']:.2f}MPa (exact:{r2d['analytical']['sigma_mpa']:.2f}, err:{r2d['analytical']['stress_error_pct']:.1f}%)")
297
+ print(f" SF: {276.0/r2d['max_von_mises_mpa']:.2f}")
298
+ print()
299
+ print("[2] 3D Cantilever (10x5x5)")
300
+ r3d=solve_cantilever_3d(200,60,60,500,68900,0.33,nx=10,ny=5,nz=5)
301
+ n3d,e3d=mesh_box(0,200,0,60,0,60,10,5,5)
302
+ print(f" Mesh: {len(n3d)} nodes, {len(e3d)} tets")
303
+ print(f" Disp: {r3d['max_displacement_mm']:.4f}mm (exact:{r3d['analytical']['delta_mm']:.4f}, err:{r3d['analytical']['disp_error_pct']:.1f}%)")
304
+ print(f" Sigma: {r3d['max_von_mises_mpa']:.2f}MPa (exact:{r3d['analytical']['sigma_mpa']:.2f}, err:{r3d['analytical']['stress_error_pct']:.1f}%)")
305
+ print(f" SF: {276.0/r3d['max_von_mises_mpa']:.2f}")
306
+ print()
307
+ print("[3] 3D Convergence")
308
+ for nv in [(3,2,2),(5,3,3),(8,4,4),(10,5,5)]:
309
+ r=solve_cantilever_3d(200,60,60,500,68900,0.33,nx=nv[0],ny=nv[1],nz=nv[2])
310
+ nc,ec=mesh_box(0,200,0,60,0,60,nv[0],nv[1],nv[2])
311
+ sf=276.0/r["max_von_mises_mpa"]
312
+ print(f" {nv[0]}x{nv[1]}x{nv[2]}: {len(nc)}n/{len(ec)}t | disp={r['max_displacement_mm']:.4f}mm ({r['analytical']['disp_error_pct']:+.1f}%) | sigma={r['max_von_mises_mpa']:.2f}MPa ({r['analytical']['stress_error_pct']:+.1f}%) | SF={sf:.2f}")
313
+ print()
314
+ print("="*60); print("All tests passed!"); print("="*60)