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,715 @@
|
|
|
1
|
+
|
|
2
|
+
import datetime
|
|
3
|
+
import glob
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import gmsh
|
|
8
|
+
|
|
9
|
+
import torchfea
|
|
10
|
+
import numpy as np
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
import morphopt
|
|
14
|
+
from .geometryinterfaces.basesurfaceinterface import BaseInterface
|
|
15
|
+
from ..base_params import BaseParams
|
|
16
|
+
|
|
17
|
+
class MeshGenerator:
|
|
18
|
+
def __init__(self, mesh_size_min=None, mesh_size_max=None):
|
|
19
|
+
# print("Initializing GMSH...")
|
|
20
|
+
gmsh.initialize()
|
|
21
|
+
gmsh.option.setNumber("General.NumThreads", 0) # Use all available cores
|
|
22
|
+
gmsh.option.setNumber("General.Verbosity", 2) # Errors only
|
|
23
|
+
self.files_map = {}
|
|
24
|
+
self.surface_tags_by_index = {}
|
|
25
|
+
self.sorted_indices = []
|
|
26
|
+
|
|
27
|
+
# Set mesh size options if provided
|
|
28
|
+
if mesh_size_min is not None:
|
|
29
|
+
# print(f"Setting Mesh.MeshSizeMin to {mesh_size_min}")
|
|
30
|
+
gmsh.option.setNumber("Mesh.MeshSizeMin", mesh_size_min)
|
|
31
|
+
|
|
32
|
+
if mesh_size_max is not None:
|
|
33
|
+
# print(f"Setting Mesh.MeshSizeMax to {mesh_size_max}")
|
|
34
|
+
gmsh.option.setNumber("Mesh.MeshSizeMax", mesh_size_max)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def scan_directory(self, directory=None):
|
|
38
|
+
if directory is None:
|
|
39
|
+
directory = os.getcwd()
|
|
40
|
+
|
|
41
|
+
# print(f"Scanning directory {directory} for files...")
|
|
42
|
+
# Pattern: __surface-{number}.(stp|stl)
|
|
43
|
+
import re
|
|
44
|
+
pattern = re.compile(r'^__surface-(\d+)\.(stp|stl)$', re.IGNORECASE)
|
|
45
|
+
|
|
46
|
+
self.files_map = {}
|
|
47
|
+
for filename in os.listdir(directory):
|
|
48
|
+
match = pattern.match(filename)
|
|
49
|
+
if match:
|
|
50
|
+
idx = int(match.group(1))
|
|
51
|
+
self.files_map[idx] = os.path.join(directory, filename)
|
|
52
|
+
# print(f" Found: {filename} (Index: {idx})")
|
|
53
|
+
|
|
54
|
+
if 0 not in self.files_map:
|
|
55
|
+
raise FileNotFoundError("Base surface file (Index 0) not found. Need '__surface-0.stp' or '__surface-0.stl'.")
|
|
56
|
+
|
|
57
|
+
self.sorted_indices = sorted(self.files_map.keys())
|
|
58
|
+
# print(f"Processing indices: {self.sorted_indices}")
|
|
59
|
+
|
|
60
|
+
def _get_all_surface_tags(self):
|
|
61
|
+
return set(dim_tag[1] for dim_tag in gmsh.model.getEntities(2))
|
|
62
|
+
|
|
63
|
+
def load_and_process_files(self):
|
|
64
|
+
self.surface_tags_by_index = {}
|
|
65
|
+
|
|
66
|
+
for idx in self.sorted_indices:
|
|
67
|
+
filename = self.files_map[idx]
|
|
68
|
+
# print(f"\n--- Processing Index {idx}: {filename} ---")
|
|
69
|
+
|
|
70
|
+
# Snapshot current surfaces to identify new ones
|
|
71
|
+
pre_surfaces = self._get_all_surface_tags()
|
|
72
|
+
|
|
73
|
+
ext = os.path.splitext(filename)[1].lower()
|
|
74
|
+
|
|
75
|
+
if ext in ['.stp', '.step']:
|
|
76
|
+
# print(" Type: STP (CAD)")
|
|
77
|
+
try:
|
|
78
|
+
# Import OCC
|
|
79
|
+
gmsh.model.occ.importShapes(filename)
|
|
80
|
+
gmsh.model.occ.synchronize()
|
|
81
|
+
|
|
82
|
+
# Remove volumes, keep surfaces
|
|
83
|
+
vols = gmsh.model.getEntities(3)
|
|
84
|
+
if vols:
|
|
85
|
+
# print(f" Found {len(vols)} volume(s) in STP. Removing volume entities, keeping surfaces...")
|
|
86
|
+
gmsh.model.occ.remove(vols, recursive=False)
|
|
87
|
+
gmsh.model.occ.synchronize()
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
raise RuntimeError(f"Error loading STP file {filename}: {e}")
|
|
91
|
+
|
|
92
|
+
elif ext in ['.stl']:
|
|
93
|
+
# print(" Type: STL (Discrete)")
|
|
94
|
+
try:
|
|
95
|
+
gmsh.merge(filename)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise RuntimeError(f"Error loading STL file {filename}: {e}")
|
|
98
|
+
|
|
99
|
+
# Identify newly added surfaces
|
|
100
|
+
post_surfaces = self._get_all_surface_tags()
|
|
101
|
+
new_surfaces = list(post_surfaces - pre_surfaces)
|
|
102
|
+
|
|
103
|
+
if not new_surfaces:
|
|
104
|
+
# print(f" Warning: No surfaces found in {filename}.")
|
|
105
|
+
pass
|
|
106
|
+
else:
|
|
107
|
+
# print(f" Extracted {len(new_surfaces)} surface(s).")
|
|
108
|
+
self.surface_tags_by_index[idx] = new_surfaces
|
|
109
|
+
|
|
110
|
+
def construct_volume(self):
|
|
111
|
+
# print("\n--- Constructing Volume ---")
|
|
112
|
+
|
|
113
|
+
if 0 not in self.surface_tags_by_index or not self.surface_tags_by_index[0]:
|
|
114
|
+
raise RuntimeError("Error: No surfaces available for base (Index 0).")
|
|
115
|
+
|
|
116
|
+
loops = []
|
|
117
|
+
|
|
118
|
+
# Process Base (0) first
|
|
119
|
+
try:
|
|
120
|
+
base_loop = gmsh.model.geo.addSurfaceLoop(self.surface_tags_by_index[0])
|
|
121
|
+
loops.append(base_loop)
|
|
122
|
+
# print(" Added outer surface loop (from Index 0).")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
raise RuntimeError(f"Error creating outer loop: {e}")
|
|
125
|
+
|
|
126
|
+
# Process Cavities (>0)
|
|
127
|
+
for idx in self.sorted_indices:
|
|
128
|
+
if idx == 0: continue
|
|
129
|
+
tags = self.surface_tags_by_index.get(idx)
|
|
130
|
+
if tags:
|
|
131
|
+
try:
|
|
132
|
+
cavity_loop = gmsh.model.geo.addSurfaceLoop(tags)
|
|
133
|
+
loops.append(cavity_loop)
|
|
134
|
+
# print(f" Added cavity loop (from Index {idx}).")
|
|
135
|
+
except Exception as e:
|
|
136
|
+
raise RuntimeError(f"Error creating cavity loop for index {idx}: {e}")
|
|
137
|
+
|
|
138
|
+
# Create Volume
|
|
139
|
+
try:
|
|
140
|
+
vol_tag = gmsh.model.geo.addVolume(loops)
|
|
141
|
+
# print(f" Created Volume Tag: {vol_tag}")
|
|
142
|
+
gmsh.model.geo.synchronize()
|
|
143
|
+
|
|
144
|
+
# Create Physical Volume
|
|
145
|
+
gmsh.model.addPhysicalGroup(3, [vol_tag], name="Volume_All")
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
raise RuntimeError(f"Error creating volume: {e}")
|
|
149
|
+
|
|
150
|
+
def generate_mesh(self, dim=3):
|
|
151
|
+
# print("\n--- Meshing ---")
|
|
152
|
+
try:
|
|
153
|
+
gmsh.model.mesh.generate(dim)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
raise RuntimeError(f"Error during meshing: {e}")
|
|
156
|
+
|
|
157
|
+
def _generate_abaqus_surface_payload(self):
|
|
158
|
+
"""
|
|
159
|
+
Generates the Abaqus SURFACE definition string by mapping 3D element faces
|
|
160
|
+
to the geometric surfaces.
|
|
161
|
+
"""
|
|
162
|
+
# print(" Generating Abaqus surface definitions...")
|
|
163
|
+
|
|
164
|
+
# Get all 3D tetrahedron elements (Type 4 in GMSH)
|
|
165
|
+
try:
|
|
166
|
+
tet_tags, tet_node_tags = gmsh.model.mesh.getElementsByType(4)
|
|
167
|
+
except:
|
|
168
|
+
# print(" No 3D elements found.")
|
|
169
|
+
return ""
|
|
170
|
+
|
|
171
|
+
if len(tet_tags) == 0:
|
|
172
|
+
return ""
|
|
173
|
+
|
|
174
|
+
# Map faces to elements
|
|
175
|
+
# Key: frozenset(3 nodes), Value: (element_tag, abaqus_face_id)
|
|
176
|
+
# Abaqus C3D4 Face Defs (Nodes 1-4):
|
|
177
|
+
# S1: 1, 2, 3
|
|
178
|
+
# S2: 1, 4, 2
|
|
179
|
+
# S3: 2, 4, 3
|
|
180
|
+
# S4: 3, 4, 1
|
|
181
|
+
|
|
182
|
+
# GMSH Tet4 Node Order: 0, 1, 2, 3
|
|
183
|
+
# GMSH flattened check:
|
|
184
|
+
# We need to ensure we use the correct nodes.
|
|
185
|
+
# Assuming compact packing.
|
|
186
|
+
|
|
187
|
+
face_map = {}
|
|
188
|
+
|
|
189
|
+
# Use NumPy for vectorized operations
|
|
190
|
+
tet_nodes = np.array(tet_node_tags).reshape(-1, 4)
|
|
191
|
+
tet_tags_arr = np.array(tet_tags)
|
|
192
|
+
|
|
193
|
+
# Nodes for each face definition
|
|
194
|
+
# S1: (0, 1, 2), S2: (0, 3, 1), S3: (1, 3, 2), S4: (2, 3, 0)
|
|
195
|
+
face_defs = [
|
|
196
|
+
([0, 1, 2], "S1"),
|
|
197
|
+
([0, 3, 1], "S2"),
|
|
198
|
+
([1, 3, 2], "S3"),
|
|
199
|
+
([2, 3, 0], "S4")
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
for col_idx, face_name in face_defs:
|
|
203
|
+
# Extract (N, 3)
|
|
204
|
+
faces = tet_nodes[:, col_idx]
|
|
205
|
+
# Create keys and update map
|
|
206
|
+
for key, tag in zip(map(frozenset, faces), tet_tags_arr):
|
|
207
|
+
face_map[key] = (tag, face_name)
|
|
208
|
+
|
|
209
|
+
payload_lines = []
|
|
210
|
+
|
|
211
|
+
# For each surface index, find which faces belong to it
|
|
212
|
+
for idx, surf_tags in self.surface_tags_by_index.items():
|
|
213
|
+
|
|
214
|
+
# Collect sets of elements for each face type
|
|
215
|
+
sets_data = {
|
|
216
|
+
"S1": [], "S2": [], "S3": [], "S4": []
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
found_count = 0
|
|
220
|
+
|
|
221
|
+
# Set to collect unique node tags for this surface index
|
|
222
|
+
surface_nodes = set()
|
|
223
|
+
|
|
224
|
+
# Iterate over the geometric surfaces for this index
|
|
225
|
+
for s_tag in surf_tags:
|
|
226
|
+
# Get 2D elements (Triangles = Type 2) on this surface
|
|
227
|
+
try:
|
|
228
|
+
tri_tags, tri_node_tags = gmsh.model.mesh.getElementsByType(2, tag=s_tag)
|
|
229
|
+
except:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
n_tris = len(tri_tags)
|
|
233
|
+
if n_tris == 0: continue
|
|
234
|
+
|
|
235
|
+
for t in range(n_tris):
|
|
236
|
+
base = t * 3
|
|
237
|
+
tn0 = tri_node_tags[base]
|
|
238
|
+
tn1 = tri_node_tags[base+1]
|
|
239
|
+
tn2 = tri_node_tags[base+2]
|
|
240
|
+
|
|
241
|
+
# Add nodes to the set for NSET generation
|
|
242
|
+
surface_nodes.add(tn0)
|
|
243
|
+
surface_nodes.add(tn1)
|
|
244
|
+
surface_nodes.add(tn2)
|
|
245
|
+
|
|
246
|
+
key = frozenset((tn0, tn1, tn2))
|
|
247
|
+
|
|
248
|
+
if key in face_map:
|
|
249
|
+
etag, face_id = face_map[key]
|
|
250
|
+
sets_data[face_id].append(etag)
|
|
251
|
+
found_count += 1
|
|
252
|
+
|
|
253
|
+
if found_count > 0:
|
|
254
|
+
# print(f" Mapped {found_count} faces for surface_{idx}_All")
|
|
255
|
+
surf_name = f"surface_{idx}_All"
|
|
256
|
+
|
|
257
|
+
# Create ELSETs for each face type
|
|
258
|
+
active_faces = []
|
|
259
|
+
for face_id, el_list in sets_data.items():
|
|
260
|
+
if el_list:
|
|
261
|
+
set_name = f"_{surf_name}_{face_id}"
|
|
262
|
+
active_faces.append(f"{set_name}, {face_id}")
|
|
263
|
+
|
|
264
|
+
payload_lines.append(f"*ELSET, ELSET={set_name}, INTERNAL")
|
|
265
|
+
# Write IDs, 16 per line max usually, plain csv is fine
|
|
266
|
+
# Join with commas
|
|
267
|
+
# Chunking for niceness
|
|
268
|
+
chunk_size = 16
|
|
269
|
+
for k in range(0, len(el_list), chunk_size):
|
|
270
|
+
chunk = el_list[k:k+chunk_size]
|
|
271
|
+
line = ", ".join(str(e) for e in chunk)
|
|
272
|
+
payload_lines.append(line)
|
|
273
|
+
|
|
274
|
+
# Create SURFACE definition
|
|
275
|
+
payload_lines.append(f"*SURFACE, TYPE=ELEMENT, NAME={surf_name}")
|
|
276
|
+
payload_lines.extend(active_faces)
|
|
277
|
+
|
|
278
|
+
# Create NSET definition
|
|
279
|
+
if surface_nodes:
|
|
280
|
+
payload_lines.append(f"*Nset, nset={surf_name}")
|
|
281
|
+
sorted_nodes = sorted(list(surface_nodes))
|
|
282
|
+
chunk_size = 16
|
|
283
|
+
for k in range(0, len(sorted_nodes), chunk_size):
|
|
284
|
+
chunk = sorted_nodes[k:k+chunk_size]
|
|
285
|
+
line = ", ".join(str(e) for e in chunk)
|
|
286
|
+
payload_lines.append(line)
|
|
287
|
+
|
|
288
|
+
return "\n".join(payload_lines)
|
|
289
|
+
|
|
290
|
+
def export(self, outfile="output.inp"):
|
|
291
|
+
# print(f"\n--- Exporting to {outfile} ---")
|
|
292
|
+
|
|
293
|
+
# 1. Generate the surface definition payload based on the mesh
|
|
294
|
+
surface_payload = self._generate_abaqus_surface_payload()
|
|
295
|
+
|
|
296
|
+
# 2. Write the standard GMSH output (Volume only)
|
|
297
|
+
# Note: We do NOT have Physical Surfaces defined, so they won't be exported as elements.
|
|
298
|
+
gmsh.write(outfile)
|
|
299
|
+
|
|
300
|
+
# 3. Post-process to insert *Part and append surfaces
|
|
301
|
+
# print(" Post-processing INP file...")
|
|
302
|
+
with open(outfile, 'r') as f:
|
|
303
|
+
lines = f.readlines()
|
|
304
|
+
|
|
305
|
+
lines.insert(2, "*Part, name=final_model\n")
|
|
306
|
+
|
|
307
|
+
# Append surface payload
|
|
308
|
+
if surface_payload:
|
|
309
|
+
lines.append("\n")
|
|
310
|
+
lines.append(surface_payload)
|
|
311
|
+
lines.append("\n")
|
|
312
|
+
|
|
313
|
+
# end part
|
|
314
|
+
lines.append("*End Part\n")
|
|
315
|
+
|
|
316
|
+
# Write back
|
|
317
|
+
with open(outfile, 'w') as f:
|
|
318
|
+
f.writelines(lines)
|
|
319
|
+
|
|
320
|
+
def finalize(self):
|
|
321
|
+
gmsh.finalize()
|
|
322
|
+
# print("Done.")
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def run(cls, seed_size: float, output_file="output.inp", directory: str = None):
|
|
326
|
+
generator = cls(mesh_size_max=seed_size*1.4,
|
|
327
|
+
mesh_size_min=seed_size*0.7)
|
|
328
|
+
try:
|
|
329
|
+
generator.scan_directory(directory=directory)
|
|
330
|
+
generator.load_and_process_files()
|
|
331
|
+
generator.construct_volume()
|
|
332
|
+
generator.generate_mesh(3)
|
|
333
|
+
generator.export(output_file)
|
|
334
|
+
except Exception as e:
|
|
335
|
+
print(f"An error occurred: {e}")
|
|
336
|
+
finally:
|
|
337
|
+
generator.finalize()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class GeometryParams(BaseParams):
|
|
341
|
+
"""
|
|
342
|
+
Class to handle the surfaces of the morphable model.
|
|
343
|
+
"""
|
|
344
|
+
from .geometryinterfaces.bspsurfaceinterface import BspInterface as BSP
|
|
345
|
+
from .geometryinterfaces.cpgeosurfaceinterface import CPGEOInterface as CPGEO
|
|
346
|
+
|
|
347
|
+
def __init__(self, fea_seed_size: float, fea_mesh_order: int = 1, reinitialize_per_iter: int = 5, *args, **kwargs) -> None:
|
|
348
|
+
"""
|
|
349
|
+
Initialize the Surfaces class.
|
|
350
|
+
|
|
351
|
+
Parameters:
|
|
352
|
+
thickness (list[float]): The minimum distance between the surfaces.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
self.surface_list: list[BaseInterface] = []
|
|
356
|
+
"""
|
|
357
|
+
List of surface objects.
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
self.reinitialize_per_iter = reinitialize_per_iter
|
|
361
|
+
"""
|
|
362
|
+
The number of iterations after which the surfaces are reinitialized.
|
|
363
|
+
This is useful for ensuring that the surfaces are updated periodically during the optimization process.
|
|
364
|
+
"""
|
|
365
|
+
self.fea_seed_size = fea_seed_size
|
|
366
|
+
"""
|
|
367
|
+
The seed size for the finite element analysis (FEA).
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
self.fea_mesh_order = fea_mesh_order
|
|
371
|
+
"""
|
|
372
|
+
The mesh order for the finite element analysis (FEA).
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
self._surface_node_index: list[np.ndarray] = []
|
|
376
|
+
"""
|
|
377
|
+
The indices of the surface nodes.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
self._iter_since_last_regenerate: int = 0
|
|
381
|
+
"""
|
|
382
|
+
The number of iterations since the last regeneration of the surfaces.
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
self._max_iter_before_regenerate: int = 15
|
|
386
|
+
"""
|
|
387
|
+
The maximum number of iterations before the surfaces are regenerated.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
self._nodes_last_regenerate: np.ndarray = None
|
|
391
|
+
"""
|
|
392
|
+
The node positions at the last regeneration of the surfaces.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
self._max_nodes_change: float = 1.0
|
|
396
|
+
"""
|
|
397
|
+
The maximum allowed change in node positions before the surfaces are regenerated.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
def initialize(self, *args, **kwargs):
|
|
401
|
+
for i in range(self.num_surface):
|
|
402
|
+
self.surface_list[i].initialize(*args, **kwargs)
|
|
403
|
+
|
|
404
|
+
def reinitialize(self, iteration: int):
|
|
405
|
+
"""
|
|
406
|
+
Initialize the surfaces for the optimization process.
|
|
407
|
+
determine which surfaces need to be updated.
|
|
408
|
+
initialize the surfaces.
|
|
409
|
+
"""
|
|
410
|
+
if iteration % self.reinitialize_per_iter == 0:
|
|
411
|
+
for i in range(self.num_surface):
|
|
412
|
+
self.surface_list[i].reinitialize()
|
|
413
|
+
# import copy
|
|
414
|
+
# result = []
|
|
415
|
+
# pools = morphopt.controller.pools
|
|
416
|
+
# for i in range(self.num_surface):
|
|
417
|
+
# surface_now = copy.deepcopy(self.surface_list[i])
|
|
418
|
+
# morphopt.controller.change_device(device='cpu', obj=surface_now)
|
|
419
|
+
# result.append(
|
|
420
|
+
# pools.apply_async(
|
|
421
|
+
# surface_now.reinitialize, kwds={}))
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# get the result
|
|
425
|
+
# for i in range(self.num_surface):
|
|
426
|
+
# result[i].get()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
morphopt.controller.objfun.inp = None
|
|
430
|
+
self.apply_surface_constraints()
|
|
431
|
+
|
|
432
|
+
def add_surface(self, surface_new: BaseInterface) -> None:
|
|
433
|
+
"""
|
|
434
|
+
Add a surface object to the list.
|
|
435
|
+
|
|
436
|
+
Parameters
|
|
437
|
+
----------
|
|
438
|
+
surface : Surface
|
|
439
|
+
The surface object to be added.
|
|
440
|
+
"""
|
|
441
|
+
self.surface_list.append(surface_new)
|
|
442
|
+
|
|
443
|
+
@property
|
|
444
|
+
def num_surface(self) -> int:
|
|
445
|
+
"""
|
|
446
|
+
Get the number of surfaces.
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
length (int) :The number of surfaces.
|
|
450
|
+
"""
|
|
451
|
+
return len(self.surface_list)
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def num_variables_list(self) -> list[int]:
|
|
455
|
+
"""
|
|
456
|
+
Get the number of variables for each surface.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
list[int]: The number of variables for each surface.
|
|
460
|
+
"""
|
|
461
|
+
num_vars = [self.surface_list[i].num_variables for i in range(self.num_surface)]
|
|
462
|
+
return num_vars
|
|
463
|
+
|
|
464
|
+
def pathlog_required(self):
|
|
465
|
+
return ['geometry']
|
|
466
|
+
|
|
467
|
+
def get_geometry_values(self) -> list[torch.Tensor]:
|
|
468
|
+
"""
|
|
469
|
+
Get the geometry values of the surfaces.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
list[tuple]: A tuple containing the geometry values of the surfaces.
|
|
473
|
+
- r (list[torch.Tensor]): The point coordinates of the surfaces.
|
|
474
|
+
- rdu (list[torch.Tensor]): The partial derivatives of the surfaces.
|
|
475
|
+
- rdu2 (list[torch.Tensor]): The second partial derivatives of the surfaces.
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
rlist = [self.surface_list[i].get_geometry_values() for i in range(self.num_surface)]
|
|
479
|
+
r = [rlist[i][0] for i in range(self.num_surface)]
|
|
480
|
+
rdu = [rlist[i][1] for i in range(self.num_surface)]
|
|
481
|
+
rdu2 = [rlist[i][2] for i in range(self.num_surface)]
|
|
482
|
+
return r, rdu, rdu2
|
|
483
|
+
|
|
484
|
+
def get_control_points_list(self) -> list[torch.Tensor]:
|
|
485
|
+
"""
|
|
486
|
+
Get the control points of the surfaces.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
list[torch.Tensor]: The control points of the surfaces.
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
ctrl_pts = [self.surface_list[i].control_points.detach().clone() for i in range(self.num_surface)]
|
|
493
|
+
return ctrl_pts
|
|
494
|
+
|
|
495
|
+
def get_penalty_fairness(self, weight: list[torch.Tensor], r: list[torch.Tensor], rdu: list[torch.Tensor], rdu2: list[torch.Tensor]) -> torch.Tensor:
|
|
496
|
+
"""
|
|
497
|
+
Get the penalty fairness of the surfaces.
|
|
498
|
+
|
|
499
|
+
Parameters:
|
|
500
|
+
weight (list[torch.Tensor]): The weights for the points in the optimization process.
|
|
501
|
+
r (list[torch.Tensor]): The point coordinates of the surfaces.
|
|
502
|
+
rdu (list[torch.Tensor]): The partial derivatives of the surfaces.
|
|
503
|
+
rdu2 (list[torch.Tensor]): The second partial derivatives of the surfaces.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
torch.Tensor: The penalty fairness of the surfaces.
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
penalty = []
|
|
510
|
+
for i in range(self.num_surface):
|
|
511
|
+
penalty.append(self.surface_list[i].get_penalty_fairness(weight[i], r[i], rdu[i], rdu2[i]))
|
|
512
|
+
|
|
513
|
+
return penalty
|
|
514
|
+
|
|
515
|
+
def get_points_weight(self) -> list[torch.Tensor]:
|
|
516
|
+
"""
|
|
517
|
+
Get the weights for the points in the optimization process.
|
|
518
|
+
|
|
519
|
+
Returns:
|
|
520
|
+
list[torch.Tensor]: The weights for the points in the optimization process.
|
|
521
|
+
"""
|
|
522
|
+
|
|
523
|
+
weight = [self.surface_list[i].get_points_weight() for i in range(self.num_surface)]
|
|
524
|
+
return weight
|
|
525
|
+
|
|
526
|
+
def get_parameters(self) -> torch.Tensor:
|
|
527
|
+
"""
|
|
528
|
+
Get the current variables of the surfaces.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
list[torch.Tensor]: The current variables of the surfaces.
|
|
532
|
+
"""
|
|
533
|
+
|
|
534
|
+
xlist = []
|
|
535
|
+
for i in range(self.num_surface):
|
|
536
|
+
xlist.append(self.surface_list[i].get_surface_parameters().flatten().detach().clone())
|
|
537
|
+
return xlist
|
|
538
|
+
|
|
539
|
+
def set_parameters(self, xlist: list[torch.Tensor]) -> None:
|
|
540
|
+
"""
|
|
541
|
+
Set the current variables of the surfaces.
|
|
542
|
+
|
|
543
|
+
Parameters:
|
|
544
|
+
xlist (list[torch.Tensor]): The new variables for the surfaces.
|
|
545
|
+
"""
|
|
546
|
+
for i in range(self.num_surface):
|
|
547
|
+
self.surface_list[i].set_surface_parameters(xlist[i].detach().clone())
|
|
548
|
+
|
|
549
|
+
def get_variables(self) -> torch.Tensor:
|
|
550
|
+
"""
|
|
551
|
+
Get the current variables of the surfaces.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
torch.Tensor: The current variables of the surfaces.
|
|
555
|
+
"""
|
|
556
|
+
xlist = self.get_parameters()
|
|
557
|
+
x_flatten = torch.cat([torch.randn_like(xlist[i].flatten())*1e-6 for i in range(len(xlist))])
|
|
558
|
+
return x_flatten
|
|
559
|
+
|
|
560
|
+
def update_variables(self, x_change: torch.Tensor, max_step_length: list[torch.Tensor]) -> None:
|
|
561
|
+
"""
|
|
562
|
+
Update the surfaces with the new variables.
|
|
563
|
+
|
|
564
|
+
Parameters:
|
|
565
|
+
xlist_change (torch.Tensor): The change of variables for the surfaces.
|
|
566
|
+
"""
|
|
567
|
+
|
|
568
|
+
x_change_list: list[torch.Tensor] = []
|
|
569
|
+
start = 0
|
|
570
|
+
for i in range(self.num_surface):
|
|
571
|
+
end = start + self.surface_list[i].num_variables
|
|
572
|
+
x_change_list.append(x_change[start:end].reshape([-1, 3]))
|
|
573
|
+
start = end
|
|
574
|
+
|
|
575
|
+
for i in range(self.num_surface):
|
|
576
|
+
|
|
577
|
+
r = x_change_list[i].norm(dim=1, keepdim=True)
|
|
578
|
+
|
|
579
|
+
dx = 2/torch.pi * torch.atan(r) * x_change_list[i] / (r + 1e-15) * max_step_length[i].unsqueeze(1)
|
|
580
|
+
|
|
581
|
+
self.surface_list[i].update_variables(dx)
|
|
582
|
+
|
|
583
|
+
self.apply_surface_constraints()
|
|
584
|
+
|
|
585
|
+
def apply_surface_constraints(self) -> None:
|
|
586
|
+
"""
|
|
587
|
+
Apply the constraints (e.g. the symmetric constraint) of the surfaces.
|
|
588
|
+
"""
|
|
589
|
+
pass
|
|
590
|
+
|
|
591
|
+
def save(self, foldpath, iteration) -> None:
|
|
592
|
+
for i in range(self.num_surface):
|
|
593
|
+
self.surface_list[i].save(foldpath + self.pathlog_required()[0] + '/Surface-%d_iter-%d' %
|
|
594
|
+
(i, iteration))
|
|
595
|
+
import pyvista as pv
|
|
596
|
+
|
|
597
|
+
plotter = pv.Plotter(off_screen=True, window_size=(1200, 1200))
|
|
598
|
+
plotter.set_background('white')
|
|
599
|
+
|
|
600
|
+
self.plot(plotter=plotter)
|
|
601
|
+
|
|
602
|
+
# Get all points to determine bounding box
|
|
603
|
+
all_points = []
|
|
604
|
+
for i in range(self.num_surface):
|
|
605
|
+
r, _, _ = self.surface_list[i].get_geometry_values()
|
|
606
|
+
all_points.append(r)
|
|
607
|
+
|
|
608
|
+
all_points = torch.cat(all_points, dim=0)
|
|
609
|
+
x_min, x_max = all_points[:, 0].min().item(), all_points[:, 0].max().item()
|
|
610
|
+
y_min, y_max = all_points[:, 1].min().item(), all_points[:, 1].max().item()
|
|
611
|
+
z_min, z_max = all_points[:, 2].min().item(), all_points[:, 2].max().item()
|
|
612
|
+
|
|
613
|
+
# Add some padding to the bounds
|
|
614
|
+
padding = 0.05 * max(x_max-x_min, y_max-y_min, z_max-z_min)
|
|
615
|
+
|
|
616
|
+
plotter.show_bounds(xtitle='X', ytitle='Y', ztitle='Z', color='black',
|
|
617
|
+
bounds=[x_min-padding, x_max+padding,
|
|
618
|
+
y_min-padding, y_max+padding,
|
|
619
|
+
z_min-padding, z_max+padding])
|
|
620
|
+
|
|
621
|
+
plotter.enable_parallel_projection()
|
|
622
|
+
azimuth = 210
|
|
623
|
+
elevation = 20
|
|
624
|
+
plotter.view_vector((math.cos(math.radians(azimuth)) * math.cos(math.radians(elevation)),
|
|
625
|
+
math.sin(math.radians(azimuth)) * math.cos(math.radians(elevation)),
|
|
626
|
+
math.sin(math.radians(elevation))))
|
|
627
|
+
|
|
628
|
+
plotter.screenshot(foldpath + self.pathlog_required()[0] + '/%d.jpg'%iteration)
|
|
629
|
+
plotter.close()
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def load(self, foldpath, iteration):
|
|
633
|
+
for i in range(self.num_surface):
|
|
634
|
+
self.surface_list[i].load(foldpath + self.pathlog_required()[0] + '/Surface-%d_iter-%d' %
|
|
635
|
+
(i, iteration))
|
|
636
|
+
# self.surface_list[i].initialize()
|
|
637
|
+
|
|
638
|
+
def plot(self, plotter=None):
|
|
639
|
+
if plotter is None:
|
|
640
|
+
import pyvista as pv
|
|
641
|
+
plotter = pv.Plotter()
|
|
642
|
+
|
|
643
|
+
for sf in range(self.num_surface):
|
|
644
|
+
if sf == 0:
|
|
645
|
+
alpha = 0.6
|
|
646
|
+
else:
|
|
647
|
+
alpha = 1
|
|
648
|
+
mesh = self.surface_list[sf].get_mesh()
|
|
649
|
+
plotter.add_mesh(mesh, opacity=alpha, color=(40.0 / 255, 120.0 / 255, 181.0 / 255),
|
|
650
|
+
diffuse=0.8, specular=0.2, ambient=0.3, specular_power=10,
|
|
651
|
+
smooth_shading=True, show_edges=False)
|
|
652
|
+
|
|
653
|
+
def get_meshes(self):
|
|
654
|
+
"""
|
|
655
|
+
Get all meshes for the surfaces.
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
list[pyvista.PolyData]: The mesh objects for all surfaces.
|
|
659
|
+
"""
|
|
660
|
+
mesh_list = []
|
|
661
|
+
for sf in range(self.num_surface):
|
|
662
|
+
mesh = self.surface_list[sf].get_mesh()
|
|
663
|
+
mesh_list.append(mesh)
|
|
664
|
+
return mesh_list
|
|
665
|
+
|
|
666
|
+
def generate(self) -> None:
|
|
667
|
+
"""
|
|
668
|
+
This function generates the geometric model of the soft robot.
|
|
669
|
+
It calls the Rhino application to generate the model and then calls Abaqus for finite element analysis (FEA).
|
|
670
|
+
"""
|
|
671
|
+
|
|
672
|
+
return self._regenerate()
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _regenerate(self) -> None:
|
|
677
|
+
"""
|
|
678
|
+
This function regenerates the geometric model of the soft robot.
|
|
679
|
+
It calls the Rhino application to generate the model and then calls Abaqus for finite element analysis (FEA).
|
|
680
|
+
"""
|
|
681
|
+
path_output = morphopt.controller.path_result + '/Cache/'
|
|
682
|
+
|
|
683
|
+
# export the data
|
|
684
|
+
self._export_data(foldpath=path_output)
|
|
685
|
+
|
|
686
|
+
# call Abaqus for FEA
|
|
687
|
+
inp_path = morphopt.controller.path_result + '/Cache/TopOptRun.inp'
|
|
688
|
+
# self._call_Abaqus(path_output, material_para, self.fea_seed_size, self.fea_mesh_order)
|
|
689
|
+
morphopt.controller.pools.apply_async(MeshGenerator.run, kwds={
|
|
690
|
+
'seed_size': self.fea_seed_size,
|
|
691
|
+
'output_file': inp_path,
|
|
692
|
+
'directory': path_output
|
|
693
|
+
}).get()
|
|
694
|
+
|
|
695
|
+
# read the inp file
|
|
696
|
+
inp = torchfea.FEA_INP()
|
|
697
|
+
inp.read_inp(path=inp_path)
|
|
698
|
+
# inp.read_inp('Z:\\Results\\EXAMPLE_T20260118_100206\\cache\\TopOptRun.inp')
|
|
699
|
+
|
|
700
|
+
return inp
|
|
701
|
+
|
|
702
|
+
def _export_data(self, foldpath: str) -> list[str]:
|
|
703
|
+
"""
|
|
704
|
+
This function export the data of each surfaces
|
|
705
|
+
"""
|
|
706
|
+
|
|
707
|
+
# export each surface with Rhino
|
|
708
|
+
for i in range(self.num_surface):
|
|
709
|
+
surf_name0 = '__surface-%d' % i
|
|
710
|
+
self.surface_list[i].output_data(path_output=foldpath, name_output=surf_name0, flip=(i!=0))
|
|
711
|
+
|
|
712
|
+
files = sorted(glob.glob(foldpath + "__surface-*.stp"))
|
|
713
|
+
if not files:
|
|
714
|
+
print("No __surface-*.stp files found.")
|
|
715
|
+
return
|