morphopt 3.1.1__py3-none-any.whl
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.
Potentially problematic release.
This version of morphopt might be problematic. Click here for more details.
- morphopt/__init__.py +32 -0
- morphopt/opt_runner.py +130 -0
- morphopt/optcore/baseobject.py +48 -0
- morphopt/optcore/controller.py +404 -0
- morphopt/optcore/history.py +440 -0
- morphopt/optcore/modelparams/__init__.py +4 -0
- morphopt/optcore/modelparams/base_params.py +97 -0
- morphopt/optcore/modelparams/feamodel/__init__.py +0 -0
- morphopt/optcore/modelparams/feamodel/feainterface/__init__.py +10 -0
- morphopt/optcore/modelparams/feamodel/feainterface/basefeainterface.py +50 -0
- morphopt/optcore/modelparams/feamodel/feainterface/bodyforceinterface.py +71 -0
- morphopt/optcore/modelparams/feamodel/feainterface/boundaryconditioninterface.py +59 -0
- morphopt/optcore/modelparams/feamodel/feainterface/contactinterface.py +113 -0
- morphopt/optcore/modelparams/feamodel/feainterface/coupleinterface.py +31 -0
- morphopt/optcore/modelparams/feamodel/feainterface/pointinterface.py +79 -0
- morphopt/optcore/modelparams/feamodel/feainterface/pressureinterface.py +68 -0
- morphopt/optcore/modelparams/feamodel/feainterface/referencepointinterface.py +29 -0
- morphopt/optcore/modelparams/feamodel/feainterface/springinterface.py +104 -0
- morphopt/optcore/modelparams/feamodel/feaparams.py +264 -0
- morphopt/optcore/modelparams/geometry/__init__.py +3 -0
- morphopt/optcore/modelparams/geometry/geometryinterfaces/__init__.py +1 -0
- morphopt/optcore/modelparams/geometry/geometryinterfaces/basesurfaceinterface.py +763 -0
- morphopt/optcore/modelparams/geometry/geometryinterfaces/bspsurfaceinterface.py +723 -0
- morphopt/optcore/modelparams/geometry/geometryinterfaces/cpgeosurfaceinterface.py +374 -0
- morphopt/optcore/modelparams/geometry/geometryparams.py +715 -0
- morphopt/optcore/modelparams/materials/__init__.py +1 -0
- morphopt/optcore/modelparams/materials/materialparams.py +110 -0
- morphopt/optcore/modelparams/params.py +88 -0
- morphopt/optcore/objfunc.py +248 -0
- morphopt/optcore/solver.py +159 -0
- morphopt/optcore/updaters/__init__.py +2 -0
- morphopt/optcore/updaters/base_updater.py +110 -0
- morphopt/optcore/updaters/geometry/__init__.py +2 -0
- morphopt/optcore/updaters/geometry/objectivefuncs/__init__.py +5 -0
- morphopt/optcore/updaters/geometry/objectivefuncs/basefuncs.py +117 -0
- morphopt/optcore/updaters/geometry/objectivefuncs/boundarys.py +91 -0
- morphopt/optcore/updaters/geometry/objectivefuncs/distancesurface.py +123 -0
- morphopt/optcore/updaters/geometry/objectivefuncs/shapederivative.py +179 -0
- morphopt/optcore/updaters/geometry/objectivefuncs/surfacefairness.py +40 -0
- morphopt/optcore/updaters/geometry/update_geometry.py +347 -0
- morphopt/optcore/updaters/optimizer.py +177 -0
- morphopt/optcore/updaters/updaters.py +82 -0
- morphopt/optcore/utils/plot_history_surface.py +239 -0
- morphopt/taskoptmization.py +75 -0
- morphopt/taskui.py +458 -0
- morphopt-3.1.1.dist-info/METADATA +242 -0
- morphopt-3.1.1.dist-info/RECORD +49 -0
- morphopt-3.1.1.dist-info/WHEEL +5 -0
- morphopt-3.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import numpy as np
|
|
5
|
+
import torch
|
|
6
|
+
import gmsh
|
|
7
|
+
|
|
8
|
+
import morphopt
|
|
9
|
+
|
|
10
|
+
from .basesurfaceinterface import CpBasedInterface
|
|
11
|
+
import bspmap
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BspInterface(CpBasedInterface):
|
|
18
|
+
"""
|
|
19
|
+
Class to handle the B-spline surface interface.
|
|
20
|
+
"""
|
|
21
|
+
class BSplineSolidGenerator:
|
|
22
|
+
def __init__(self, P0, degree_u=3, degree_v=3):
|
|
23
|
+
"""
|
|
24
|
+
初始化 B-Spline 实体生成器 (使用 Gmsh)
|
|
25
|
+
|
|
26
|
+
参数:
|
|
27
|
+
P0: numpy array, shape (numV, numU, 3).
|
|
28
|
+
代表控制点 (Poles).
|
|
29
|
+
注意: U方向应为非重复的唯一控制点 (Periodic).
|
|
30
|
+
degree_u: U方向次数(多项式最高次数,如degree=3表示三次B样条)
|
|
31
|
+
degree_v: V方向次数(多项式最高次数,如degree=3表示三次B样条)
|
|
32
|
+
"""
|
|
33
|
+
self.P0 = P0
|
|
34
|
+
self.degree_u = degree_u
|
|
35
|
+
self.degree_v = degree_v
|
|
36
|
+
self.volume_tag = None
|
|
37
|
+
|
|
38
|
+
def build(self):
|
|
39
|
+
"""
|
|
40
|
+
构建版本 - 改用 addBSplineSurface + Symmetric Wrapping + Trim
|
|
41
|
+
通过上下游双重延伸(Padding)确保接缝处C2连续性
|
|
42
|
+
"""
|
|
43
|
+
self.volume_tag = None
|
|
44
|
+
|
|
45
|
+
# 初始化 Gmsh
|
|
46
|
+
gmsh.initialize()
|
|
47
|
+
gmsh.option.setNumber("General.Terminal", 0) # Suppress Gmsh output
|
|
48
|
+
gmsh.option.setNumber("General.Verbosity", 0)
|
|
49
|
+
gmsh.model.add("bspline_solid")
|
|
50
|
+
|
|
51
|
+
# 获取维度
|
|
52
|
+
num_poles_v = self.P0.shape[0]
|
|
53
|
+
num_poles_u_raw = self.P0.shape[1]
|
|
54
|
+
du = self.degree_u
|
|
55
|
+
dv = self.degree_v
|
|
56
|
+
|
|
57
|
+
# ----------------------------------------------------
|
|
58
|
+
# 0. Sanitize Input (Check for explicit closure)
|
|
59
|
+
# ----------------------------------------------------
|
|
60
|
+
# 用户可能传入了闭合的控制点序列 (P_start == P_end)
|
|
61
|
+
# B-Spline 周期性数学构造需要唯一控制点序列。
|
|
62
|
+
# 如果发现首尾重合,去掉最后一个。
|
|
63
|
+
|
|
64
|
+
# 检查第一行即可
|
|
65
|
+
p_start = self.P0[0, 0]
|
|
66
|
+
p_end = self.P0[0, -1]
|
|
67
|
+
dist = np.linalg.norm(p_start - p_end)
|
|
68
|
+
|
|
69
|
+
if dist < 1e-6:
|
|
70
|
+
# print("Detecting closed input poles (Start == End). Removing last pole for periodic construction.")
|
|
71
|
+
poles_to_use = self.P0[:, :-1]
|
|
72
|
+
else:
|
|
73
|
+
poles_to_use = self.P0
|
|
74
|
+
|
|
75
|
+
num_poles_u = poles_to_use.shape[1]
|
|
76
|
+
# print(f"Effective Unique Poles U: {num_poles_u}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ----------------------------------------------------
|
|
80
|
+
# 1. Prepare Control Points (Periodic Extension)
|
|
81
|
+
# ----------------------------------------------------
|
|
82
|
+
# B-Spline Periodicity Implementation:
|
|
83
|
+
# 1. Unique Poles: P[0] ... P[n-1] (Count = n)
|
|
84
|
+
# 2. Extended Poles: P[0]...P[n-1] + P[0]...P[d-1] (Count = n + d)
|
|
85
|
+
# 3. Knots: Uniform. Count = (n+d) + d + 1 = n + 2d + 1
|
|
86
|
+
# 4. Valid Period Range: [d, n+d] (Length = n)
|
|
87
|
+
|
|
88
|
+
# Create Point Entities
|
|
89
|
+
point_tags_map = np.zeros((num_poles_v, num_poles_u), dtype=int)
|
|
90
|
+
for j in range(num_poles_v):
|
|
91
|
+
for i in range(num_poles_u):
|
|
92
|
+
pt = poles_to_use[j, i, :]
|
|
93
|
+
t = gmsh.model.occ.addPoint(pt[0], pt[1], pt[2])
|
|
94
|
+
point_tags_map[j, i] = t
|
|
95
|
+
|
|
96
|
+
# Build Extended List (Row by Row)
|
|
97
|
+
# Flattened list for addBSplineSurface
|
|
98
|
+
side_point_tags = []
|
|
99
|
+
|
|
100
|
+
for j in range(num_poles_v):
|
|
101
|
+
# Original: indices 0 ... num_poles_u - 1
|
|
102
|
+
for i in range(num_poles_u):
|
|
103
|
+
side_point_tags.append(point_tags_map[j, i])
|
|
104
|
+
# Wrap Extension: indices 0 ... du - 1
|
|
105
|
+
for k in range(du):
|
|
106
|
+
side_point_tags.append(point_tags_map[j, k])
|
|
107
|
+
|
|
108
|
+
# ----------------------------------------------------
|
|
109
|
+
# 2. Setup Knots (Uniform)
|
|
110
|
+
# ----------------------------------------------------
|
|
111
|
+
num_extended_poles_u = num_poles_u + du
|
|
112
|
+
num_knots_u = num_extended_poles_u + du + 1
|
|
113
|
+
|
|
114
|
+
# Uniform Knots: 0, 1, 2, ...
|
|
115
|
+
# Range of full surface support: [0, num_knots_u - 1] because degree=d
|
|
116
|
+
# Standard domain of B-Spline starts at knot[d] = d
|
|
117
|
+
# Ends at knot[len - 1 - d] = (n+2d) - d = n+d
|
|
118
|
+
# So valid parameter range is [d, n+d]. Length = n. Correct.
|
|
119
|
+
knots_u = [float(i) for i in range(num_knots_u)]
|
|
120
|
+
mults_u = [1] * len(knots_u)
|
|
121
|
+
|
|
122
|
+
# V: Clamped (Standard)
|
|
123
|
+
num_knots_v = num_poles_v - dv + 1
|
|
124
|
+
knots_v = []
|
|
125
|
+
mults_v = []
|
|
126
|
+
for i in range(num_knots_v):
|
|
127
|
+
knots_v.append(float(i))
|
|
128
|
+
if i == 0 or i == num_knots_v - 1:
|
|
129
|
+
mults_v.append(dv + 1)
|
|
130
|
+
else:
|
|
131
|
+
mults_v.append(1)
|
|
132
|
+
|
|
133
|
+
# ----------------------------------------------------
|
|
134
|
+
# 3. Create Base Surface
|
|
135
|
+
# ----------------------------------------------------
|
|
136
|
+
try:
|
|
137
|
+
raw_surface = gmsh.model.occ.addBSplineSurface(
|
|
138
|
+
side_point_tags, num_extended_poles_u, -1, du, dv,
|
|
139
|
+
[], knots_u, knots_v, mults_u, mults_v
|
|
140
|
+
)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
# print(f"BSpline fail: {e}")
|
|
143
|
+
raise e
|
|
144
|
+
|
|
145
|
+
# ----------------------------------------------------
|
|
146
|
+
# 4. Trim Surface (One Period)
|
|
147
|
+
# ----------------------------------------------------
|
|
148
|
+
# Trim range: [du, du + num_poles_u]
|
|
149
|
+
u_min = float(du)
|
|
150
|
+
u_max = float(du + num_poles_u)
|
|
151
|
+
v_min = knots_v[0]
|
|
152
|
+
v_max = knots_v[-1]
|
|
153
|
+
|
|
154
|
+
# Create UV bounding box loop
|
|
155
|
+
p1 = gmsh.model.occ.addPoint(u_min, v_min, 0)
|
|
156
|
+
p2 = gmsh.model.occ.addPoint(u_max, v_min, 0)
|
|
157
|
+
p3 = gmsh.model.occ.addPoint(u_max, v_max, 0)
|
|
158
|
+
p4 = gmsh.model.occ.addPoint(u_min, v_max, 0)
|
|
159
|
+
|
|
160
|
+
l1 = gmsh.model.occ.addLine(p1, p2)
|
|
161
|
+
l2 = gmsh.model.occ.addLine(p2, p3)
|
|
162
|
+
l3 = gmsh.model.occ.addLine(p3, p4)
|
|
163
|
+
l4 = gmsh.model.occ.addLine(p4, p1)
|
|
164
|
+
|
|
165
|
+
loop = gmsh.model.occ.addCurveLoop([l1, l2, l3, l4])
|
|
166
|
+
|
|
167
|
+
side_surface = gmsh.model.occ.addTrimmedSurface(raw_surface, [loop], wire3D=False)
|
|
168
|
+
|
|
169
|
+
gmsh.model.occ.synchronize()
|
|
170
|
+
|
|
171
|
+
# ----------------------------------------------------
|
|
172
|
+
# 5. Create Caps using Common Topology
|
|
173
|
+
# ----------------------------------------------------
|
|
174
|
+
# 直接获取 Trimmed Surface 的边界
|
|
175
|
+
boundaries = gmsh.model.getBoundary([(2, side_surface)], recursive=False)
|
|
176
|
+
|
|
177
|
+
cap_faces = []
|
|
178
|
+
|
|
179
|
+
for dim, tag in boundaries:
|
|
180
|
+
# 检查是否闭合 (Caps)
|
|
181
|
+
status, nodes = gmsh.model.getAdjacencies(1, tag)
|
|
182
|
+
|
|
183
|
+
is_closed_loop = False
|
|
184
|
+
if len(nodes) >= 2:
|
|
185
|
+
# 几何检查
|
|
186
|
+
c1 = gmsh.model.getValue(0, nodes[0], [])
|
|
187
|
+
c2 = gmsh.model.getValue(0, nodes[-1], [])
|
|
188
|
+
dist = np.linalg.norm(np.array(c1)-np.array(c2))
|
|
189
|
+
if dist < 1e-5:
|
|
190
|
+
is_closed_loop = True
|
|
191
|
+
|
|
192
|
+
if is_closed_loop:
|
|
193
|
+
# Cap Face
|
|
194
|
+
w = gmsh.model.occ.addWire([tag])
|
|
195
|
+
try:
|
|
196
|
+
f = gmsh.model.occ.addPlaneSurface([w])
|
|
197
|
+
cap_faces.append(f)
|
|
198
|
+
except:
|
|
199
|
+
try:
|
|
200
|
+
f = gmsh.model.occ.addSurfaceFilling(w)
|
|
201
|
+
cap_faces.append(f)
|
|
202
|
+
except:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ----------------------------------------------------
|
|
207
|
+
# 6. Build Solid
|
|
208
|
+
# ----------------------------------------------------
|
|
209
|
+
surface_tags = [side_surface] + cap_faces
|
|
210
|
+
|
|
211
|
+
# print("Sewing and Healing...")
|
|
212
|
+
|
|
213
|
+
# 尝试直接通过 SurfaceLoop 创建
|
|
214
|
+
try:
|
|
215
|
+
shell_tag = gmsh.model.occ.addSurfaceLoop(surface_tags)
|
|
216
|
+
self.volume_tag = gmsh.model.occ.addVolume([shell_tag])
|
|
217
|
+
# print(f"Created solid via SurfaceLoop: {self.volume_tag}")
|
|
218
|
+
except Exception as e:
|
|
219
|
+
# print(f"Direct SurfaceLoop failed: {e}")
|
|
220
|
+
# 使用 healShapes,重点是 sewFaces=True
|
|
221
|
+
try:
|
|
222
|
+
healed = gmsh.model.occ.healShapes([(2, t) for t in surface_tags],
|
|
223
|
+
tolerance=1e-1,
|
|
224
|
+
fixDegenerated=True,
|
|
225
|
+
fixSmallEdges=False,
|
|
226
|
+
fixSmallFaces=False,
|
|
227
|
+
sewFaces=True,
|
|
228
|
+
makeSolids=True)
|
|
229
|
+
|
|
230
|
+
for dim, tag in healed:
|
|
231
|
+
if dim == 3:
|
|
232
|
+
self.volume_tag = tag
|
|
233
|
+
# print(f"Healed Solid Created: {tag}")
|
|
234
|
+
break
|
|
235
|
+
except Exception as he:
|
|
236
|
+
pass
|
|
237
|
+
# print(f"Heal error: {he}")
|
|
238
|
+
|
|
239
|
+
gmsh.model.occ.synchronize()
|
|
240
|
+
|
|
241
|
+
if self.volume_tag:
|
|
242
|
+
# ----------------------------------------------------
|
|
243
|
+
# 7. Cleanup (Nuclear Option: BRep Isolation)
|
|
244
|
+
# ----------------------------------------------------
|
|
245
|
+
# To ensure ABSOLUTELY NO extra lines, points, or invisible shells
|
|
246
|
+
# are exported, we will:
|
|
247
|
+
# 1. Isolate the Solid in the current model.
|
|
248
|
+
# 2. Write it to a temporary BRep (native OCC format).
|
|
249
|
+
# 3. CLEARS the Gmsh model.
|
|
250
|
+
# 4. Import the BRep back.
|
|
251
|
+
# 5. This guarantees fresh indexing and zero "history".
|
|
252
|
+
|
|
253
|
+
# print("Starting Nuclear Cleanup (BRep Isolation)...")
|
|
254
|
+
|
|
255
|
+
# Remove everything else first (Standard cleanup) to minimize BRep size
|
|
256
|
+
gmsh.model.occ.synchronize()
|
|
257
|
+
keep_entities = {(3, self.volume_tag)}
|
|
258
|
+
all_entities = gmsh.model.getEntities()
|
|
259
|
+
to_remove = [e for e in all_entities if e not in keep_entities and e[0] <= 2] # Remove only < Dim 3 first
|
|
260
|
+
if to_remove:
|
|
261
|
+
gmsh.model.occ.remove(to_remove, recursive=False)
|
|
262
|
+
gmsh.model.occ.synchronize()
|
|
263
|
+
|
|
264
|
+
# Export BRep
|
|
265
|
+
temp_brep = "temp_clean.brep"
|
|
266
|
+
gmsh.write(temp_brep)
|
|
267
|
+
|
|
268
|
+
# RESET GMSH
|
|
269
|
+
# Note: gmsh.clear() clears the current model data but keeps the session
|
|
270
|
+
gmsh.clear()
|
|
271
|
+
|
|
272
|
+
# Re-import
|
|
273
|
+
# print("Reloading clean BRep...")
|
|
274
|
+
gmsh.model.occ.importShapes(temp_brep)
|
|
275
|
+
gmsh.model.occ.synchronize()
|
|
276
|
+
|
|
277
|
+
# Cleanup temp file
|
|
278
|
+
if os.path.exists(temp_brep):
|
|
279
|
+
os.remove(temp_brep)
|
|
280
|
+
|
|
281
|
+
# Get the new volume tag (should be 1)
|
|
282
|
+
vols = gmsh.model.getEntities(3)
|
|
283
|
+
if len(vols) == 1:
|
|
284
|
+
self.volume_tag = vols[0][1]
|
|
285
|
+
# print(f"BRep Isolation Successful. New Volume Tag: {self.volume_tag}")
|
|
286
|
+
|
|
287
|
+
# ----------------------------------------------------
|
|
288
|
+
# Heal AGAIN after re-import to merge tolerances
|
|
289
|
+
# ----------------------------------------------------
|
|
290
|
+
# print("Running Post-Import Heal and Topology Simplification...")
|
|
291
|
+
|
|
292
|
+
# 1. Remove Duplicates (Geometry Level)
|
|
293
|
+
gmsh.model.occ.removeAllDuplicates()
|
|
294
|
+
gmsh.model.occ.synchronize()
|
|
295
|
+
|
|
296
|
+
# 2. Heal
|
|
297
|
+
try:
|
|
298
|
+
healed = gmsh.model.occ.healShapes([(3, self.volume_tag)],
|
|
299
|
+
tolerance=1e-5, # Tighter tolerance
|
|
300
|
+
fixDegenerated=True,
|
|
301
|
+
fixSmallEdges=True,
|
|
302
|
+
fixSmallFaces=True,
|
|
303
|
+
# fixOrientation=True, # Error: Not supported in Python API wrapper?
|
|
304
|
+
sewFaces=True,
|
|
305
|
+
makeSolids=True)
|
|
306
|
+
if healed and healed[0][0] == 3:
|
|
307
|
+
self.volume_tag = healed[0][1]
|
|
308
|
+
# print(f"Post-Import Heal Successful. Tag: {self.volume_tag}")
|
|
309
|
+
except Exception as e:
|
|
310
|
+
pass
|
|
311
|
+
# print(f"Post-Import Heal warning: {e}")
|
|
312
|
+
|
|
313
|
+
gmsh.model.occ.synchronize()
|
|
314
|
+
|
|
315
|
+
# Check Mass
|
|
316
|
+
mass = gmsh.model.occ.getMass(3, self.volume_tag)
|
|
317
|
+
# print(f"Volume Mass: {mass}")
|
|
318
|
+
|
|
319
|
+
# Check Topology
|
|
320
|
+
# Note: getBoundary with recursive=True returns (Dim, Tag) tuples.
|
|
321
|
+
boundaries = gmsh.model.getBoundary([(3, self.volume_tag)], recursive=True)
|
|
322
|
+
edge_tags = [e[1] for e in boundaries if e[0] == 1]
|
|
323
|
+
# print(f"Topology Check: Solid has {len(edge_tags)} edges.")
|
|
324
|
+
if len(edge_tags) > 3:
|
|
325
|
+
pass
|
|
326
|
+
# print("Warning: More than 3 edges (Top, Bottom, Seam). Topology might be split.")
|
|
327
|
+
|
|
328
|
+
# ----------------------------------------------------
|
|
329
|
+
# FINAL CLEANUP: Purge residue from Healing
|
|
330
|
+
# ----------------------------------------------------
|
|
331
|
+
# Healing might create new faces and leave old ones as orphans.
|
|
332
|
+
# We identify ONLY the current solid and its children, delete rest.
|
|
333
|
+
|
|
334
|
+
keep_set = set()
|
|
335
|
+
keep_set.add((3, self.volume_tag))
|
|
336
|
+
|
|
337
|
+
# Recursive descendants (Faces, Edges, Vertices)
|
|
338
|
+
sub_shapes = gmsh.model.getBoundary([(3, self.volume_tag)], recursive=True)
|
|
339
|
+
for dim, tag in sub_shapes:
|
|
340
|
+
keep_set.add((dim, tag))
|
|
341
|
+
|
|
342
|
+
# Get all entities
|
|
343
|
+
candidates = gmsh.model.getEntities()
|
|
344
|
+
|
|
345
|
+
to_delete = []
|
|
346
|
+
for dim, tag in candidates:
|
|
347
|
+
if (dim, tag) not in keep_set:
|
|
348
|
+
to_delete.append((dim, tag))
|
|
349
|
+
|
|
350
|
+
if to_delete:
|
|
351
|
+
# print(f"Post-Heal Cleanup: Removing {len(to_delete)} orphan entities.")
|
|
352
|
+
gmsh.model.occ.remove(to_delete, recursive=False)
|
|
353
|
+
gmsh.model.occ.synchronize()
|
|
354
|
+
|
|
355
|
+
# [REMOVED] Physical Group
|
|
356
|
+
# We do NOT add PhysicalGroup.
|
|
357
|
+
# Experience shows Abaqus treats PhysicalGroups as Sets/Surfaces rather than the native Part.
|
|
358
|
+
# ptag = gmsh.model.addPhysicalGroup(3, [self.volume_tag])
|
|
359
|
+
# gmsh.model.setPhysicalName(3, ptag, "BSplineSolid")
|
|
360
|
+
|
|
361
|
+
else:
|
|
362
|
+
# print(f"Error: Reloaded BRep contains {len(vols)} volumes!")
|
|
363
|
+
self.volume_tag = vols[0][1] if vols else None
|
|
364
|
+
|
|
365
|
+
# Final Sanity Check LOG
|
|
366
|
+
# print("--- Post-Isolation Entity Check ---")
|
|
367
|
+
# for dim in range(4):
|
|
368
|
+
# ents = gmsh.model.getEntities(dim)
|
|
369
|
+
# print(f"Dim {dim}: {len(ents)} entities -> {[e[1] for e in ents]}")
|
|
370
|
+
|
|
371
|
+
return self.volume_tag
|
|
372
|
+
|
|
373
|
+
if self.volume_tag is None:
|
|
374
|
+
print("Warning: Failed to create solid. Returning surface elements.")
|
|
375
|
+
return side_surface # Fallback
|
|
376
|
+
|
|
377
|
+
def export_step(self, filename):
|
|
378
|
+
"""导出 STEP 文件"""
|
|
379
|
+
try:
|
|
380
|
+
gmsh.write(filename)
|
|
381
|
+
print(f"Successfully exported to {filename}")
|
|
382
|
+
return True
|
|
383
|
+
except Exception as e:
|
|
384
|
+
print(f"Export failed: {e}")
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
def finalize(self):
|
|
388
|
+
"""清理 Gmsh 资源"""
|
|
389
|
+
gmsh.finalize()
|
|
390
|
+
|
|
391
|
+
def visualize(self):
|
|
392
|
+
"""启动 Gmsh GUI 进行可视化(可选)"""
|
|
393
|
+
gmsh.fltk.run()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def __init__(self, surface: bspmap.BSP, init_size: float, symmetric = [0], MaxR = 0.2, MaxFF = 0.1, MaxC = 1.0):
|
|
397
|
+
super().__init__(surface, symmetric)
|
|
398
|
+
|
|
399
|
+
self.model = surface
|
|
400
|
+
"""The B-spline surface model."""
|
|
401
|
+
|
|
402
|
+
self._cps = torch.from_numpy(self.model.control_points).to(torch.get_default_device())
|
|
403
|
+
|
|
404
|
+
self.MaxR = MaxR
|
|
405
|
+
self.MaxFF = MaxFF
|
|
406
|
+
self.MaxC = MaxC
|
|
407
|
+
|
|
408
|
+
self.init_size = init_size
|
|
409
|
+
|
|
410
|
+
self.flip: bool = False
|
|
411
|
+
|
|
412
|
+
self.rr_compensation: torch.Tensor
|
|
413
|
+
|
|
414
|
+
self._preload_size: tuple[int, int]
|
|
415
|
+
"""The preloaded size of the UV grid."""
|
|
416
|
+
|
|
417
|
+
def initialize(self):
|
|
418
|
+
|
|
419
|
+
# preload the uv grid
|
|
420
|
+
ratio = 2
|
|
421
|
+
self._preload_size = (self.model.size[0] * ratio, self.model.size[1] * ratio)
|
|
422
|
+
|
|
423
|
+
uvgrids = np.meshgrid(np.linspace(0, 1, self._preload_size[0]), np.linspace(0, 1, self._preload_size[1]+1)[1:], indexing='ij')
|
|
424
|
+
uvgrids_np = np.stack(uvgrids, axis=-1).reshape([-1, 2])
|
|
425
|
+
self._preload_uv = torch.tensor(np.stack([uvgrids[0].reshape([-1]), uvgrids[1].reshape([-1])], axis=1), dtype=torch.float64).to(torch.get_default_device())
|
|
426
|
+
self._weights, indices = self.model.get_weights(uvgrids_np, derivative=[0,0])
|
|
427
|
+
self._weights_du = self.model.get_weights(uvgrids_np, derivative=[1,0])[0]
|
|
428
|
+
self._weights_du2 = self.model.get_weights(uvgrids_np, derivative=[2,0])[0]
|
|
429
|
+
self._weights_dv = self.model.get_weights(uvgrids_np, derivative=[0,1])[0]
|
|
430
|
+
self._weights_dv2 = self.model.get_weights(uvgrids_np, derivative=[0,2])[0]
|
|
431
|
+
self._weights_dudv = self.model.get_weights(uvgrids_np, derivative=[1,1])[0]
|
|
432
|
+
|
|
433
|
+
indices_cps = torch.from_numpy(indices).to(torch.get_default_device()).reshape([self._preload_uv.shape[0], -1])
|
|
434
|
+
indices_pts = torch.arange(self._preload_uv.shape[0], device=torch.get_default_device()).reshape([-1,1]).repeat(1, indices_cps.shape[1])
|
|
435
|
+
self._indices = torch.stack([indices_pts, indices_cps], dim=0).reshape(2, -1)
|
|
436
|
+
|
|
437
|
+
self._weights = torch.from_numpy(self._weights).to(torch.get_default_device()).flatten()
|
|
438
|
+
self._weights_du = torch.from_numpy(self._weights_du).to(torch.get_default_device()).flatten()
|
|
439
|
+
self._weights_du2 = torch.from_numpy(self._weights_du2).to(torch.get_default_device()).flatten()
|
|
440
|
+
self._weights_dv = torch.from_numpy(self._weights_dv).to(torch.get_default_device()).flatten()
|
|
441
|
+
self._weights_dv2 = torch.from_numpy(self._weights_dv2).to(torch.get_default_device()).flatten()
|
|
442
|
+
self._weights_dudv = torch.from_numpy(self._weights_dudv).to(torch.get_default_device()).flatten()
|
|
443
|
+
|
|
444
|
+
# initialize fairness compensation
|
|
445
|
+
RRuu, RRuv, RRvu, RRvv = self._geofair_data(r=self.get_r(),
|
|
446
|
+
rdu=self.get_rdu(),
|
|
447
|
+
rdu2=self.get_rdu2())[-4:]
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
lengthU = (2 * self.model.size[0] * self.init_size)**2
|
|
452
|
+
lengthV = (2 * self.model.size[1] * self.init_size)**2
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
RRuu /= lengthU
|
|
457
|
+
RRuv /= lengthV
|
|
458
|
+
RRvu /= lengthU
|
|
459
|
+
RRvv /= lengthV
|
|
460
|
+
|
|
461
|
+
self.rr_compensation = torch.ones(4, RRuu.shape[0])
|
|
462
|
+
self.rr_compensation[0, RRuu > self.MaxR *
|
|
463
|
+
0.2] = self.MaxR * 0.2 / RRuu[RRuu > self.MaxR * 0.2]
|
|
464
|
+
self.rr_compensation[0] /= lengthU
|
|
465
|
+
self.rr_compensation[1, RRuv > self.MaxR *
|
|
466
|
+
0.2] = self.MaxR * 0.2 / RRuv[RRuv > self.MaxR *
|
|
467
|
+
0.2] / lengthV
|
|
468
|
+
self.rr_compensation[1] /= lengthV
|
|
469
|
+
self.rr_compensation[2, RRvu > self.MaxR *
|
|
470
|
+
0.2] = self.MaxR * 0.2 / RRvu[RRvu > self.MaxR *
|
|
471
|
+
0.2] / lengthU
|
|
472
|
+
self.rr_compensation[2] /= lengthU
|
|
473
|
+
self.rr_compensation[3, RRvv > self.MaxR *
|
|
474
|
+
0.2] = self.MaxR * 0.2 / RRvv[RRvv > self.MaxR *
|
|
475
|
+
0.2] / lengthV
|
|
476
|
+
self.rr_compensation[3] /= lengthV
|
|
477
|
+
|
|
478
|
+
@staticmethod
|
|
479
|
+
def output_stp_file(control_points, degree_u, degree_v, path_output, name_output):
|
|
480
|
+
generator = BspInterface.BSplineSolidGenerator(P0=control_points, degree_u=degree_u, degree_v=degree_v)
|
|
481
|
+
generator.build()
|
|
482
|
+
output_file = path_output + name_output + '.stp'
|
|
483
|
+
generator.export_step(output_file)
|
|
484
|
+
generator.finalize()
|
|
485
|
+
|
|
486
|
+
def output_data(self, path_output, name_output, seed_size=-1, flip=False):
|
|
487
|
+
flip = not flip
|
|
488
|
+
|
|
489
|
+
pools = morphopt.controller.pools
|
|
490
|
+
result = pools.apply_async(self.output_stp_file, args=(self._cps.detach().cpu().numpy().reshape([
|
|
491
|
+
self.model.size[0],
|
|
492
|
+
self.model.size[1],
|
|
493
|
+
3]),
|
|
494
|
+
self.model.degree,
|
|
495
|
+
self.model.degree,
|
|
496
|
+
path_output,
|
|
497
|
+
name_output))
|
|
498
|
+
result.get()
|
|
499
|
+
|
|
500
|
+
return name_output + '.stp'
|
|
501
|
+
|
|
502
|
+
def update_variables(self, x_change):
|
|
503
|
+
|
|
504
|
+
x_change = x_change.reshape([self.model.size[0], self.model.size[1], 3])
|
|
505
|
+
|
|
506
|
+
x_change[:5, :, 2] = 0
|
|
507
|
+
x_change[-5:, :, 2] = 0
|
|
508
|
+
|
|
509
|
+
x_change[0] = 0
|
|
510
|
+
x_change[-1] = 0
|
|
511
|
+
|
|
512
|
+
super().update_variables(x_change)
|
|
513
|
+
|
|
514
|
+
def _geofair_data(self, r: torch.Tensor, rdu: torch.Tensor, rdu2: torch.Tensor):
|
|
515
|
+
"""
|
|
516
|
+
To compute the geometric fairness data.
|
|
517
|
+
|
|
518
|
+
Parameters:
|
|
519
|
+
r (torch.Tensor): The surface points.
|
|
520
|
+
rdu (torch.Tensor): The first derivatives of the surface points.
|
|
521
|
+
rdu2 (torch.Tensor): The second derivatives of the surface points.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
C0 (torch.Tensor): The curvature-based fairness measure.
|
|
525
|
+
FF0 (torch.Tensor): The first fundamental form-based fairness measure.
|
|
526
|
+
RRuu0 (torch.Tensor): The second derivative in u direction-based fairness measure.
|
|
527
|
+
RRuv0 (torch.Tensor): The mixed second derivative-based fairness measure.
|
|
528
|
+
RRvu0 (torch.Tensor): The mixed second derivative-based fairness measure.
|
|
529
|
+
RRvv0 (torch.Tensor): The second derivative in v direction-based fairness measure.
|
|
530
|
+
"""
|
|
531
|
+
Normal0 = torch.cross(rdu[:, :, 1], rdu[:, :, 0], dim=1)
|
|
532
|
+
Normal = Normal0 / torch.sqrt(torch.sum(Normal0**2, dim=1, keepdim=True))
|
|
533
|
+
|
|
534
|
+
I = torch.einsum('pim, pin->pmn', rdu, rdu)
|
|
535
|
+
invI = I.inverse()
|
|
536
|
+
II = torch.einsum('pimn, pi->pmn', rdu2, Normal)
|
|
537
|
+
|
|
538
|
+
detI = I[:, 0, 0] * I[:, 1, 1] - I[:, 0, 1] * I[:, 1, 0]
|
|
539
|
+
detII = II[:, 0, 0] * II[:, 1, 1] - II[:, 0, 1] * II[:, 1, 0]
|
|
540
|
+
|
|
541
|
+
H = 0.5 * (invI * II).sum([1, 2])
|
|
542
|
+
K = detII / detI
|
|
543
|
+
|
|
544
|
+
C0 = 4 * H**2 - 2 * K
|
|
545
|
+
|
|
546
|
+
F = I[:, 0, 1]
|
|
547
|
+
E = I[:, 0, 0]
|
|
548
|
+
G = I[:, 1, 1]
|
|
549
|
+
FF0 = I[:, 0, 1] * I[:, 1, 0] / I[:, 1, 1] / I[:, 0, 0]
|
|
550
|
+
|
|
551
|
+
Iu = torch.einsum('piuw, piv->puvw', rdu2, rdu) + \
|
|
552
|
+
torch.einsum('piu, pivw->puvw', rdu, rdu2)
|
|
553
|
+
|
|
554
|
+
RRuu0 = Iu[:, 0, 0, 0]**2 / E**2
|
|
555
|
+
RRuv0 = Iu[:, 0, 0, 1]**2 / E**2
|
|
556
|
+
RRvu0 = Iu[:, 1, 1, 0]**2 / G**2
|
|
557
|
+
RRvv0 = Iu[:, 1, 1, 1]**2 / G**2
|
|
558
|
+
|
|
559
|
+
return C0, FF0, RRuu0, RRuv0, RRvu0, RRvv0
|
|
560
|
+
|
|
561
|
+
def get_penalty_fairness(self, weight: torch.Tensor, r: torch.Tensor, rdu: torch.Tensor, rdu2: torch.Tensor) -> torch.Tensor:
|
|
562
|
+
|
|
563
|
+
C0, FF0, RRuu0, RRuv0, RRvu0, RRvv0 = self._geofair_data(r, rdu, rdu2)
|
|
564
|
+
|
|
565
|
+
indexC, C = self.barrier_function(C0, self.MaxC, 0.8,
|
|
566
|
+
3)
|
|
567
|
+
|
|
568
|
+
indexFF, FF = self.barrier_function(
|
|
569
|
+
FF0, self.MaxFF, 0.8, 3)
|
|
570
|
+
indexRRuu, RRuu = (self.barrier_function(
|
|
571
|
+
RRuu0 * self.rr_compensation[0], self.MaxR, 0.8,
|
|
572
|
+
3))
|
|
573
|
+
indexRRuv, RRuv = (self.barrier_function(
|
|
574
|
+
RRuv0 * self.rr_compensation[1], self.MaxR, 0.8,
|
|
575
|
+
3))
|
|
576
|
+
indexRRvu, RRvu = (self.barrier_function(
|
|
577
|
+
RRvu0 * self.rr_compensation[2], self.MaxR, 0.8,
|
|
578
|
+
3))
|
|
579
|
+
indexRRvv, RRvv = (self.barrier_function(
|
|
580
|
+
RRvv0 * self.rr_compensation[3], self.MaxR, 0.8,
|
|
581
|
+
3))
|
|
582
|
+
|
|
583
|
+
return (weight[indexC] * C).sum() + \
|
|
584
|
+
(weight[indexFF] * FF).sum() + \
|
|
585
|
+
(weight[indexRRuu] * RRuu).sum() + \
|
|
586
|
+
(weight[indexRRuv] * RRuv).sum() + \
|
|
587
|
+
(weight[indexRRvu] * RRvu).sum() + \
|
|
588
|
+
(weight[indexRRvv] * RRvv).sum()
|
|
589
|
+
|
|
590
|
+
def get_points_weight(self):
|
|
591
|
+
|
|
592
|
+
R0 = self.get_r().detach()
|
|
593
|
+
|
|
594
|
+
R_now = R0.reshape([
|
|
595
|
+
self._preload_size[0], self._preload_size[1],
|
|
596
|
+
3
|
|
597
|
+
])
|
|
598
|
+
R_uplus = R_now.roll(-1, dims=1)
|
|
599
|
+
|
|
600
|
+
R_vplus = R_now[1:]
|
|
601
|
+
R_now = R_now[:-1]
|
|
602
|
+
R_uvplus = R_uplus[1:]
|
|
603
|
+
R_uplus = R_uplus[:-1]
|
|
604
|
+
|
|
605
|
+
area1 = torch.cross(R_vplus - R_now, R_uplus - R_now,
|
|
606
|
+
dim=2).norm(dim=2) / 2
|
|
607
|
+
area2 = torch.cross(R_uvplus - R_uplus,
|
|
608
|
+
R_uvplus - R_vplus,
|
|
609
|
+
dim=2).norm(dim=2) / 2
|
|
610
|
+
|
|
611
|
+
ratio_now = torch.zeros(self._preload_size[0], self._preload_size[1],
|
|
612
|
+
device=R0.device)
|
|
613
|
+
|
|
614
|
+
ratio_now[:-1] += area1 / 3
|
|
615
|
+
ratio_now[1:] += area1 / 3 + area2 / 3
|
|
616
|
+
ratio_now[:-1, (torch.arange(ratio_now.shape[1]) + 1) %
|
|
617
|
+
ratio_now.shape[1]] += area1 / 3 + area2 / 3
|
|
618
|
+
ratio_now[1:, (torch.arange(ratio_now.shape[1]) + 1) %
|
|
619
|
+
ratio_now.shape[1]] += area2 / 3
|
|
620
|
+
|
|
621
|
+
return ratio_now.flatten()
|
|
622
|
+
|
|
623
|
+
def save(self, filename):
|
|
624
|
+
self.model.control_points = self._cps.detach().cpu().numpy()
|
|
625
|
+
self.model.save(filename)
|
|
626
|
+
|
|
627
|
+
def load(self, filename):
|
|
628
|
+
self.model = bspmap.BSP.load(filename + '.npz')
|
|
629
|
+
self._cps = torch.from_numpy(self.model.control_points).to(torch.get_default_device())
|
|
630
|
+
|
|
631
|
+
def get_mesh(self):
|
|
632
|
+
import pyvista as pv
|
|
633
|
+
result = self.get_r().detach().cpu().numpy().reshape([
|
|
634
|
+
self._preload_size[0], self._preload_size[1], 3
|
|
635
|
+
])
|
|
636
|
+
|
|
637
|
+
# Convert to numpy arrays for PyVista
|
|
638
|
+
x = result[:, :, 0]
|
|
639
|
+
y = result[:, :, 1]
|
|
640
|
+
z = result[:, :, 2]
|
|
641
|
+
|
|
642
|
+
# Create structured grid
|
|
643
|
+
grid = pv.StructuredGrid(x, y, z)
|
|
644
|
+
mesh = grid.extract_surface()
|
|
645
|
+
mesh.compute_normals(inplace=True)
|
|
646
|
+
|
|
647
|
+
return mesh
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
@classmethod
|
|
651
|
+
def initialize_cylinder(cls, r0: float, length: float, seed_size: float, flip: bool, num_U_ratio: int = 1, num_V_ratio: int = 1, symmetric: list[int] = [0], degree = 3, init_location = [0.,0.,0.], maxR = 0.2, maxC = 1., maxFF = 0.2, perturbation_L = -1.):
|
|
652
|
+
"""
|
|
653
|
+
Initialize the B-spline surface for the optimization process.
|
|
654
|
+
|
|
655
|
+
Parameters:
|
|
656
|
+
r0 (float): The radius of the cylinder.
|
|
657
|
+
length (float): The length of the cylinder.
|
|
658
|
+
seed_size (float): The size of the seed for the B-spline surface.
|
|
659
|
+
num_U_ratio (int, optional): The ratio for the number of points in the U direction. Default is 1.
|
|
660
|
+
num_V_ratio (int, optional): The ratio for the number of points in the V direction. Default is 1.
|
|
661
|
+
symmetric (list[int]): The symmetry of the surface.
|
|
662
|
+
flip (bool): Whether to flip the surface or not.
|
|
663
|
+
degree (int, optional): The degree of the B-spline surface. Default is 3.
|
|
664
|
+
init_location (list[float], optional): The initial location of the surface. Default is [0., 0., 0.].
|
|
665
|
+
maxR (float, optional): The maximum radius for the pre-loading. Default is 0.2.
|
|
666
|
+
maxC (float, optional): The maximum curvature for the pre-loading. Default is 1.0.
|
|
667
|
+
maxFF (float, optional): The maximum fairness factor for the pre-loading. Default is 0.2.
|
|
668
|
+
perturbation_L (float, optional): The perturbation length for the surface. Default is -1. If < 0, no perturbation is applied.
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
BSP (BSP_Surf): The initialized B-spline surface object.
|
|
672
|
+
surf_type (int): The type of the surface (0 for B-spline surface).
|
|
673
|
+
symmetric (list[int]): The symmetry of the surface.
|
|
674
|
+
"""
|
|
675
|
+
|
|
676
|
+
numU = round(r0 * 2 * np.pi / seed_size)
|
|
677
|
+
numV = round(length / seed_size)
|
|
678
|
+
|
|
679
|
+
numU = round(numU / 12) * 12
|
|
680
|
+
|
|
681
|
+
numU = round(numU / num_U_ratio) * num_U_ratio
|
|
682
|
+
numV = round(numV / num_V_ratio) * num_V_ratio
|
|
683
|
+
|
|
684
|
+
P0 = torch.zeros(numV, numU, 3)
|
|
685
|
+
|
|
686
|
+
x = torch.linspace(0, 1, numU + 1)[:-1]
|
|
687
|
+
y = torch.linspace(0, 1, numV)
|
|
688
|
+
|
|
689
|
+
[y, x] = torch.meshgrid(y, x, indexing='ij')
|
|
690
|
+
theta = 2 * torch.pi * x + (1 / numU) * torch.pi
|
|
691
|
+
|
|
692
|
+
if flip != 0:
|
|
693
|
+
theta = -theta
|
|
694
|
+
|
|
695
|
+
P0[:, :, 0] = torch.cos(theta) * r0
|
|
696
|
+
P0[:, :, 1] = torch.sin(theta) * r0
|
|
697
|
+
P0[:, :, 2] = length * y
|
|
698
|
+
# Apply perturbation if specified
|
|
699
|
+
if perturbation_L > 0:
|
|
700
|
+
r = torch.sqrt(P0[:, :, 0]**2 + P0[:, :, 1]**2)
|
|
701
|
+
r_new = (1 + 0.04*torch.cos(2*(P0[:, :, 2] / length) * np.pi * (length/perturbation_L))) * r0
|
|
702
|
+
P0[:, :, 0] *= r_new / r
|
|
703
|
+
P0[:, :, 1] *= r_new / r
|
|
704
|
+
|
|
705
|
+
basis_u = bspmap.BasisCircular(num_cps=numU, degree=degree)
|
|
706
|
+
basis_v = bspmap.BasisClamped(num_cps=numV, degree=degree)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
P0 += torch.tensor(init_location)
|
|
711
|
+
P0.data[
|
|
712
|
+
0, :, 2] = init_location[2]
|
|
713
|
+
P0.data[-1, :, 2] = init_location[2] + length
|
|
714
|
+
|
|
715
|
+
bsp = bspmap.BSP(basis=[basis_v, basis_u],
|
|
716
|
+
degree=degree,
|
|
717
|
+
size=[numV, numU],
|
|
718
|
+
control_points=P0.cpu().numpy().reshape([-1, 3]))
|
|
719
|
+
|
|
720
|
+
output = cls(bsp, init_size=seed_size, symmetric=symmetric, MaxR=maxR, MaxC=maxC, MaxFF=maxFF)
|
|
721
|
+
output.flip = flip
|
|
722
|
+
|
|
723
|
+
return output
|