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,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