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,374 @@
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 cpgeo
12
+
13
+ class CPGEOInterface(CpBasedInterface):
14
+ """
15
+ Class to handle the CPGEO surface interface.
16
+
17
+ CPGEO uses triangular meshes with control points for morphology optimization,
18
+ providing a flexible alternative to B-spline surfaces for complex geometries.
19
+ """
20
+
21
+ def __init__(self, surface: cpgeo.CPGEO, init_size: float, symmetric = [0], MaxC = 1.0):
22
+ super().__init__(surface, symmetric)
23
+
24
+ self.model = surface
25
+ """The CPGEO surface model."""
26
+
27
+ self._cps = torch.from_numpy(self.model.control_points).to(torch.get_default_device())
28
+
29
+ self.MaxC = MaxC
30
+
31
+ self.init_size = init_size
32
+
33
+ self._num_knots: int
34
+ """The number of knot points for the CPGEO model."""
35
+
36
+ def initialize(self):
37
+ """Initialize the CPGEO model and preload knot points for evaluation."""
38
+
39
+ # Initialize CPGEO knots and thresholds
40
+ self.model.initialize()
41
+
42
+ # Load control points into torch tensor
43
+ self._cps = torch.from_numpy(self.model.control_points).to(torch.get_default_device())
44
+
45
+ # Get knot points from the CPGEO model
46
+ # For CPGEO, we use knot points as evaluation points (analogous to UV grid for BSP)
47
+ self._num_knots = self.model._knots.shape[0]
48
+ self._preload_uv = torch.from_numpy(self.model._knots).to(torch.get_default_device())
49
+
50
+ # Precompute weights for knot points (derivative 0, 1, 2)
51
+ # get_weights3 returns: (indices_cps, indices_pts, w) for derivative=0
52
+ # (indices_cps, indices_pts, w), (indices_cps, indices_pts, wdu) for derivative=1
53
+ # (indices_cps, indices_pts, w), (indices_cps, indices_pts, wdu), (indices_cps, indices_pts, wdu2) for derivative=2
54
+ result = self.model.get_weights3(self.model._knots, derivative=2)
55
+ indices_cps, indices_pts, w, wdu, wdu2 = result
56
+
57
+
58
+ indices_cps_t = torch.from_numpy(indices_cps).to(torch.get_default_device())
59
+ indices_pts_t = torch.from_numpy(indices_pts).to(torch.get_default_device())
60
+
61
+ weights_per_query = indices_pts_t[1:] - indices_pts_t[:-1]
62
+ # 使用repeat_interleave创建查询点索引
63
+ indices_pts_t = torch.repeat_interleave(torch.arange(self._preload_uv.shape[0], dtype=torch.long), weights_per_query)
64
+
65
+ # Convert to torch tensors and flatten for compatibility with base class _map method
66
+ self._indices = torch.stack([indices_pts_t, indices_cps_t], dim=0).reshape(2, -1)
67
+
68
+ self._weights = torch.from_numpy(w).to(torch.get_default_device()).flatten()
69
+
70
+ self._weights_du = torch.from_numpy(wdu[0]).to(torch.get_default_device()).flatten()
71
+ self._weights_dv = torch.from_numpy(wdu[1]).to(torch.get_default_device()).flatten()
72
+ self._weights_du2 = torch.from_numpy(wdu2[0, 0]).to(torch.get_default_device()).flatten()
73
+ self._weights_dv2 = torch.from_numpy(wdu2[1, 1]).to(torch.get_default_device()).flatten()
74
+ self._weights_dudv = torch.from_numpy(wdu2[0, 1]).to(torch.get_default_device()).flatten()
75
+
76
+ @staticmethod
77
+ def output_stl_file(vertices, faces, path_output, name_output):
78
+ """Output CPGEO mesh as STL file.
79
+
80
+ Args:
81
+ vertices (np.ndarray): Vertex coordinates, shape (N, 3)
82
+ faces (np.ndarray): Face connectivity, shape (F, 3)
83
+ path_output (str): Output directory path
84
+ name_output (str): Output file name (without extension)
85
+ """
86
+ import pyvista as pv
87
+
88
+ # Create PyVista mesh
89
+ faces_with_count = np.hstack([np.full((faces.shape[0], 1), 3), faces])
90
+ mesh = pv.PolyData(vertices, faces_with_count)
91
+
92
+ # Output STL file
93
+ output_file = path_output + name_output + '.stl'
94
+ mesh.save(output_file)
95
+
96
+ return output_file
97
+
98
+ def output_data(self, path_output, name_output, seed_size=-1, flip=False):
99
+ """Output CPGEO mesh data to STL file.
100
+
101
+ Args:
102
+ path_output (str): Output directory path
103
+ name_output (str): Output file name
104
+ seed_size (float): Optional seed size (unused for CPGEO)
105
+ flip (bool): Optional flip flag (unused for CPGEO)
106
+
107
+ Returns:
108
+ str: Output filename with extension
109
+ """
110
+
111
+ pools = morphopt.controller.pools
112
+ r = self.get_r().detach().cpu().numpy()
113
+ result = pools.apply_async(self.output_stl_file, args=(
114
+ r,
115
+ self.model._cp_faces,
116
+ path_output,
117
+ name_output))
118
+ result.get()
119
+
120
+ return name_output + '.stl'
121
+
122
+ def _geofair_data(self, r: torch.Tensor, rdu: torch.Tensor, rdu2: torch.Tensor):
123
+ """
124
+ To compute the geometric fairness data.
125
+
126
+ Parameters:
127
+ r (torch.Tensor): The surface points.
128
+ rdu (torch.Tensor): The first derivatives of the surface points.
129
+ rdu2 (torch.Tensor): The second derivatives of the surface points.
130
+
131
+ Returns:
132
+ C0 (torch.Tensor): The curvature-based fairness measure.
133
+ FF0 (torch.Tensor): The first fundamental form-based fairness measure.
134
+ RRuu0 (torch.Tensor): The second derivative in u direction-based fairness measure.
135
+ RRuv0 (torch.Tensor): The mixed second derivative-based fairness measure.
136
+ RRvu0 (torch.Tensor): The mixed second derivative-based fairness measure.
137
+ RRvv0 (torch.Tensor): The second derivative in v direction-based fairness measure.
138
+ """
139
+ Normal0 = torch.cross(rdu[:, :, 1], rdu[:, :, 0], dim=1)
140
+ Normal = Normal0 / torch.sqrt(torch.sum(Normal0**2, dim=1, keepdim=True))
141
+
142
+ I = torch.einsum('pim, pin->pmn', rdu, rdu)
143
+ invI = I.inverse()
144
+ II = torch.einsum('pimn, pi->pmn', rdu2, Normal)
145
+
146
+ detI = I[:, 0, 0] * I[:, 1, 1] - I[:, 0, 1] * I[:, 1, 0]
147
+ detII = II[:, 0, 0] * II[:, 1, 1] - II[:, 0, 1] * II[:, 1, 0]
148
+
149
+ H = 0.5 * (invI * II).sum([1, 2])
150
+ K = detII / detI
151
+
152
+ C0 = 4 * H**2 - 2 * K
153
+
154
+ return C0
155
+
156
+ def get_penalty_fairness(self, weight: torch.Tensor, r: torch.Tensor, rdu: torch.Tensor, rdu2: torch.Tensor) -> torch.Tensor:
157
+
158
+ C0 = self._geofair_data(r, rdu, rdu2)
159
+
160
+ indexC, C = self.barrier_function(C0, self.MaxC, 0.8,
161
+ 3)
162
+
163
+ return (weight[indexC] * C).sum()
164
+
165
+ def get_points_weight(self):
166
+ """Compute area-based weights for each knot point in CPGEO mesh.
167
+
168
+ Returns:
169
+ torch.Tensor: Weights based on local surface area around each knot
170
+ """
171
+ R0 = self.get_r().detach()
172
+
173
+ # Get face connectivity
174
+ faces = torch.from_numpy(self.model._cp_faces).to(R0.device)
175
+
176
+ # Compute area for each triangle
177
+ v0 = R0[faces[:, 0]]
178
+ v1 = R0[faces[:, 1]]
179
+ v2 = R0[faces[:, 2]]
180
+
181
+ # Triangle area = 0.5 * ||cross product||
182
+ areas = torch.cross(v1 - v0, v2 - v0, dim=1).norm(dim=1) / 2.0
183
+
184
+ # Distribute area to vertices (each vertex gets 1/3 of incident triangle areas)
185
+ weights = torch.zeros(R0.shape[0], device=R0.device)
186
+ for i in range(3):
187
+ weights.scatter_add_(0, faces[:, i], areas / 3.0)
188
+
189
+ return weights
190
+
191
+ def save(self, filename):
192
+ """Save CPGEO model to file."""
193
+ self.model.control_points = self._cps.detach().cpu().numpy()
194
+ self.model.save(filename)
195
+
196
+ def load(self, filename):
197
+ """Load CPGEO model from file."""
198
+ self.model = cpgeo.CPGEO.load(filename + '.npz')
199
+ self._cps = torch.from_numpy(self.model.control_points).to(torch.get_default_device())
200
+ self.model.refine_surface(seed_size=self.init_size, max_iterations=4)
201
+ self.initialize()
202
+
203
+ 1
204
+
205
+ def get_mesh(self):
206
+ """Get PyVista mesh from CPGEO model.
207
+
208
+ Returns:
209
+ pv.PolyData: PyVista mesh with current control point positions
210
+ """
211
+ import pyvista as pv
212
+
213
+ # Get current control point positions
214
+ vertices = self.get_r().detach().cpu().numpy()
215
+ faces = self.model._cp_faces
216
+
217
+ # Create PyVista mesh (prepend face count for each triangle)
218
+ faces_with_count = np.hstack([np.full((faces.shape[0], 1), 3), faces])
219
+ mesh = pv.PolyData(vertices, faces_with_count)
220
+ mesh.compute_normals(inplace=True)
221
+
222
+ return mesh
223
+
224
+
225
+ @classmethod
226
+ def initialize_cylinder(cls, r0: float, length: float, seed_size: float, flip: bool,
227
+ num_U_ratio: int = 1, num_V_ratio: int = 1, symmetric: list[int] = [0],
228
+ degree = 3, init_location = [0.,0.,0.], maxR = 0.2, maxC = 1., maxFF = 0.2,
229
+ perturbation_L = -1.):
230
+ """
231
+ Initialize the CPGEO surface for cylinder shape optimization.
232
+
233
+ Parameters:
234
+ r0 (float): The radius of the cylinder.
235
+ length (float): The length of the cylinder.
236
+ seed_size (float): The size of the mesh seed.
237
+ flip (bool): Whether to flip the surface normal or not.
238
+ num_U_ratio (int, optional): The ratio for mesh density in circumferential direction. Default is 1.
239
+ num_V_ratio (int, optional): The ratio for mesh density in axial direction. Default is 1.
240
+ symmetric (list[int]): The symmetry of the surface.
241
+ degree (int, optional): Unused for CPGEO. Default is 3.
242
+ init_location (list[float], optional): The initial location of the surface. Default is [0., 0., 0.].
243
+ maxR (float, optional): The maximum curvature derivative constraint. Default is 0.2.
244
+ maxC (float, optional): The maximum curvature constraint. Default is 1.0.
245
+ maxFF (float, optional): The maximum fairness factor. Default is 0.2.
246
+ perturbation_L (float, optional): The perturbation length for initial shape. Default is -1. If < 0, no perturbation is applied.
247
+
248
+ Returns:
249
+ CPGEOInterface: The initialized CPGEO surface object.
250
+ """
251
+ import trimesh
252
+
253
+ # Create cylindrical mesh using trimesh
254
+ # Estimate number of segments based on seed size
255
+ num_segments_circ = max(12, round(r0 * 2 * np.pi / seed_size))
256
+ num_segments_circ = round(num_segments_circ / 12) * 12 # Make it divisible by 12
257
+ num_segments_circ = round(num_segments_circ / num_U_ratio) * num_U_ratio
258
+
259
+ num_segments_axial = max(2, round(length / seed_size))
260
+ num_segments_axial = round(num_segments_axial / num_V_ratio) * num_V_ratio
261
+
262
+ # Generate cylinder vertices
263
+ vertices = []
264
+ for i in range(num_segments_axial + 1):
265
+ z = length * i / num_segments_axial
266
+ for j in range(num_segments_circ):
267
+ theta = 2 * np.pi * j / num_segments_circ
268
+ if flip:
269
+ theta = -theta
270
+
271
+ # Base radius
272
+ r_val = r0
273
+
274
+ # Apply perturbation if specified
275
+ if perturbation_L > 0:
276
+ r_val = r0 * (1 + 0.04 * np.cos(2 * (z / length) * np.pi * (length / perturbation_L)))
277
+
278
+ x = r_val * np.cos(theta) + init_location[0]
279
+ y = r_val * np.sin(theta) + init_location[1]
280
+ z_final = z + init_location[2]
281
+
282
+ vertices.append([x, y, z_final])
283
+
284
+ vertices = np.array(vertices)
285
+
286
+ # Generate faces (triangulate the cylinder surface)
287
+ faces = []
288
+ for i in range(num_segments_axial):
289
+ for j in range(num_segments_circ):
290
+ # Current ring
291
+ v00 = i * num_segments_circ + j
292
+ v01 = i * num_segments_circ + (j + 1) % num_segments_circ
293
+ # Next ring
294
+ v10 = (i + 1) * num_segments_circ + j
295
+ v11 = (i + 1) * num_segments_circ + (j + 1) % num_segments_circ
296
+
297
+ # Two triangles per quad
298
+ if flip:
299
+ faces.append([v00, v10, v01])
300
+ faces.append([v01, v10, v11])
301
+ else:
302
+ faces.append([v00, v01, v10])
303
+ faces.append([v01, v11, v10])
304
+
305
+ faces = np.array(faces)
306
+
307
+ # Create CPGEO model
308
+ cpgeo_model = cpgeo.CPGEO(
309
+ control_points=vertices,
310
+ cp_faces=faces,
311
+ knot_influence_num=20
312
+ )
313
+
314
+ # Create interface
315
+ output = cls(cpgeo_model, init_size=seed_size, symmetric=symmetric,
316
+ MaxR=maxR, MaxC=maxC, MaxFF=maxFF)
317
+ output.flip = flip
318
+
319
+ return output
320
+
321
+ @classmethod
322
+ def initialize_Sphere(cls, r0: float, seed_size: float, flip: bool, symmetric: list[int] = [0], init_location = [0.,0.,0.],
323
+ MaxC = 1.0):
324
+ """
325
+ Initialize the CPGEO surface for sphere shape optimization.
326
+
327
+ Parameters:
328
+ r0 (float): The radius of the sphere.
329
+ seed_size (float): The size of the mesh seed.
330
+ flip (bool): Whether to flip the surface normal or not.
331
+ symmetric (list[int]): The symmetry of the surface.
332
+ init_location (list[float], optional): The initial location of the surface. Default is [0., 0., 0.].
333
+ MaxC (float, optional): The maximum curvature constraint. Default is 1.0
334
+ Returns:
335
+ CPGEOInterface: The initialized CPGEO surface object.
336
+
337
+ """
338
+ import cpgeo.capi
339
+
340
+ # Estimate number of points based on surface area and seed size
341
+ area = 4 * np.pi * r0**2
342
+ num_points = int(area / (seed_size**2))
343
+
344
+ # Generate points using Fibonacci Lattice for uniform spherical distribution
345
+ indices = np.arange(0, num_points, dtype=float) + 0.5
346
+ phi = np.arccos(1 - 2*indices/num_points) + 1e-9 # small offset to avoid north pole singularity
347
+ theta = np.pi * (1 + 5**0.5) * indices
348
+
349
+ x = r0 * np.cos(theta) * np.sin(phi) + init_location[0]
350
+ y = r0 * np.sin(theta) * np.sin(phi) + init_location[1]
351
+ z = r0 * np.cos(phi) + init_location[2]
352
+
353
+ vertices = np.stack((x, y, z), axis=-1)
354
+
355
+ # Get triangulation from CAPI
356
+ # We pass coordinates relative to the center, as spherical triangulation
357
+ # typically operates on directions or convex hull of points
358
+ vertices_relative = (vertices - np.array(init_location)) / r0
359
+ faces = cpgeo.capi.get_sphere_triangulation(vertices_relative.flatten())
360
+
361
+ # Create CPGEO model
362
+ cpgeo_model = cpgeo.CPGEO(
363
+ control_points=vertices,
364
+ cp_faces=faces,
365
+ knot_influence_num=20
366
+ )
367
+
368
+ # Create interface
369
+ output = cls(cpgeo_model, init_size=seed_size, symmetric=symmetric, MaxC=MaxC)
370
+ output.flip = flip
371
+
372
+ return output
373
+
374
+