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.

Files changed (49) hide show
  1. morphopt/__init__.py +32 -0
  2. morphopt/opt_runner.py +130 -0
  3. morphopt/optcore/baseobject.py +48 -0
  4. morphopt/optcore/controller.py +404 -0
  5. morphopt/optcore/history.py +440 -0
  6. morphopt/optcore/modelparams/__init__.py +4 -0
  7. morphopt/optcore/modelparams/base_params.py +97 -0
  8. morphopt/optcore/modelparams/feamodel/__init__.py +0 -0
  9. morphopt/optcore/modelparams/feamodel/feainterface/__init__.py +10 -0
  10. morphopt/optcore/modelparams/feamodel/feainterface/basefeainterface.py +50 -0
  11. morphopt/optcore/modelparams/feamodel/feainterface/bodyforceinterface.py +71 -0
  12. morphopt/optcore/modelparams/feamodel/feainterface/boundaryconditioninterface.py +59 -0
  13. morphopt/optcore/modelparams/feamodel/feainterface/contactinterface.py +113 -0
  14. morphopt/optcore/modelparams/feamodel/feainterface/coupleinterface.py +31 -0
  15. morphopt/optcore/modelparams/feamodel/feainterface/pointinterface.py +79 -0
  16. morphopt/optcore/modelparams/feamodel/feainterface/pressureinterface.py +68 -0
  17. morphopt/optcore/modelparams/feamodel/feainterface/referencepointinterface.py +29 -0
  18. morphopt/optcore/modelparams/feamodel/feainterface/springinterface.py +104 -0
  19. morphopt/optcore/modelparams/feamodel/feaparams.py +264 -0
  20. morphopt/optcore/modelparams/geometry/__init__.py +3 -0
  21. morphopt/optcore/modelparams/geometry/geometryinterfaces/__init__.py +1 -0
  22. morphopt/optcore/modelparams/geometry/geometryinterfaces/basesurfaceinterface.py +763 -0
  23. morphopt/optcore/modelparams/geometry/geometryinterfaces/bspsurfaceinterface.py +723 -0
  24. morphopt/optcore/modelparams/geometry/geometryinterfaces/cpgeosurfaceinterface.py +374 -0
  25. morphopt/optcore/modelparams/geometry/geometryparams.py +715 -0
  26. morphopt/optcore/modelparams/materials/__init__.py +1 -0
  27. morphopt/optcore/modelparams/materials/materialparams.py +110 -0
  28. morphopt/optcore/modelparams/params.py +88 -0
  29. morphopt/optcore/objfunc.py +248 -0
  30. morphopt/optcore/solver.py +159 -0
  31. morphopt/optcore/updaters/__init__.py +2 -0
  32. morphopt/optcore/updaters/base_updater.py +110 -0
  33. morphopt/optcore/updaters/geometry/__init__.py +2 -0
  34. morphopt/optcore/updaters/geometry/objectivefuncs/__init__.py +5 -0
  35. morphopt/optcore/updaters/geometry/objectivefuncs/basefuncs.py +117 -0
  36. morphopt/optcore/updaters/geometry/objectivefuncs/boundarys.py +91 -0
  37. morphopt/optcore/updaters/geometry/objectivefuncs/distancesurface.py +123 -0
  38. morphopt/optcore/updaters/geometry/objectivefuncs/shapederivative.py +179 -0
  39. morphopt/optcore/updaters/geometry/objectivefuncs/surfacefairness.py +40 -0
  40. morphopt/optcore/updaters/geometry/update_geometry.py +347 -0
  41. morphopt/optcore/updaters/optimizer.py +177 -0
  42. morphopt/optcore/updaters/updaters.py +82 -0
  43. morphopt/optcore/utils/plot_history_surface.py +239 -0
  44. morphopt/taskoptmization.py +75 -0
  45. morphopt/taskui.py +458 -0
  46. morphopt-3.1.1.dist-info/METADATA +242 -0
  47. morphopt-3.1.1.dist-info/RECORD +49 -0
  48. morphopt-3.1.1.dist-info/WHEEL +5 -0
  49. 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