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.
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
morphopt/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ # region optcore imports
2
+ from .optcore.controller import Controller
3
+ from .optcore.modelparams import GeometryParams, FEAParams, Materials
4
+ from .optcore.solver import MorphSolver
5
+ from .optcore.updaters.geometry import UpdaterGeometries
6
+ from .optcore.updaters.updaters import Updaters
7
+ from .optcore.modelparams import Params
8
+ from .optcore.objfunc import ObjectiveFunction
9
+ from .optcore.history import History
10
+ from .optcore.baseobject import BaseObject
11
+ from .optcore.utils.plot_history_surface import SurfacesFigurePlotter
12
+ # endregion
13
+
14
+ from .opt_runner import start_optimization, debug_optimization, view_optimization_result
15
+
16
+
17
+
18
+
19
+ controller: Controller = None
20
+
21
+ __all__ = [
22
+ "Controller",
23
+ "GeometryParams",
24
+ "FEAParams",
25
+ "Materials",
26
+ "MorphSolver",
27
+ "UpdaterGeometries",
28
+ "Updaters",
29
+ "Params",
30
+ "ObjectiveFunction",
31
+ ]
32
+
morphopt/opt_runner.py ADDED
@@ -0,0 +1,130 @@
1
+
2
+
3
+
4
+ def start_optimization(device='cpu', restart_per_iteration: int = 20, path_result: str=None, target_iteration=None, no_gui: bool=False):
5
+ """
6
+ Start a new optimization process.
7
+
8
+ Args:
9
+ Controller (type): The controller class to use for the optimization.
10
+ device (str, optional): The device to use for computation. Defaults to 'cpu'.
11
+ restart_per_iteration (int, optional): The number of iterations between restarts. Defaults to 20.
12
+ """
13
+ if path_result is None:
14
+ import __main__
15
+ main_filepath = __main__.__file__
16
+ else:
17
+ main_filepath = path_result + '/scripts/' + 'MAIN_SCRIPT_FOR_RESTART.py'
18
+
19
+ import time
20
+ import multiprocessing as mp
21
+ from .taskoptmization import TaskOptimization
22
+ from .taskui import run_ui
23
+
24
+
25
+ dataqueue = mp.Queue()
26
+ process_optimization = mp.Process(target=TaskOptimization.task_optimization, kwargs={'path_result': path_result,
27
+ 'main_filepath': main_filepath,
28
+ 'device': device,
29
+ 'target_iteration': target_iteration,
30
+ 'restart_per_iteration': restart_per_iteration,
31
+ 'dataqueue': dataqueue})
32
+ process_optimization.start()
33
+
34
+ if no_gui:
35
+ process_optimization.join()
36
+ return
37
+
38
+ process_ui = mp.Process(target=run_ui, args=(dataqueue, main_filepath))
39
+ process_ui.start()
40
+
41
+ process_optimization.join()
42
+ process_ui.join()
43
+
44
+ def view_optimization_result(path_result: str = None):
45
+ """
46
+ View the result of an optimization process.
47
+
48
+ Args:
49
+ path_result (str): The folder path of the optimization result.
50
+ """
51
+ import os
52
+ import multiprocessing as mp
53
+ from .taskui import run_ui
54
+ from .optcore.history import History
55
+
56
+ if path_result is None or not os.path.exists(path_result):
57
+ # Use file dialog to select directory if path is not provided
58
+ # We need a temporary app to show the dialog
59
+ from PyQt6.QtWidgets import QApplication, QFileDialog
60
+ import sys
61
+
62
+ # Check if an app instance already exists
63
+ app = QApplication.instance()
64
+ if not app:
65
+ app = QApplication(sys.argv)
66
+
67
+ path_result = QFileDialog.getExistingDirectory(None, "Select Optimization Result Folder", os.getcwd())
68
+ if not path_result:
69
+ return
70
+
71
+ main_filepath = os.path.join(path_result, 'scripts', 'MAIN_SCRIPT_FOR_RESTART.py')
72
+ if not os.path.exists(main_filepath):
73
+ print(f"Error: {main_filepath} not found.")
74
+ return
75
+
76
+ # Determine the last iteration
77
+ history = History()
78
+ try:
79
+ history.load(foldpath=os.path.join(path_result, 'log'))
80
+ last_iteration = history.iteration
81
+ except:
82
+ last_iteration = 0
83
+
84
+ dataqueue = mp.Queue()
85
+ # Send the initial data to load the result
86
+ dataqueue.put({'iteration': last_iteration, 'path_result': path_result})
87
+
88
+ run_ui(dataqueue, main_filepath)
89
+
90
+ def debug_optimization(device='cpu', restart_per_iteration: int = 20, path_result: str=None, target_iteration=None, ):
91
+ """
92
+ Start a new optimization process.
93
+
94
+ Args:
95
+ Controller (type): The controller class to use for the optimization.
96
+ device (str, optional): The device to use for computation. Defaults to 'cpu'.
97
+ restart_per_iteration (int, optional): The number of iterations between restarts. Defaults to 20.
98
+ """
99
+ import __main__
100
+ from .taskoptmization import TaskOptimization
101
+ import os
102
+ import morphopt
103
+
104
+ if path_result is None:
105
+ import __main__
106
+ main_filepath = __main__.__file__
107
+ else:
108
+ main_filepath = path_result + '/scripts/' + 'MAIN_SCRIPT_FOR_RESTART.py'
109
+
110
+ filename = os.path.splitext(os.path.basename(main_filepath))[0]
111
+ filepath = os.path.dirname(main_filepath)
112
+ os.chdir(filepath)
113
+ import sys
114
+ sys.path.append(os.getcwd())
115
+
116
+ Controller: morphopt.Controller = getattr(__import__(filename), 'ThisController')
117
+
118
+ controller: morphopt.Controller = Controller()
119
+ controller.restart_per_iteration = restart_per_iteration
120
+ controller.optdevice = device
121
+
122
+ TaskOptimization.optmain(device=device,
123
+ path_result=path_result,
124
+ target_iteration=target_iteration,
125
+ restart_per_iteration=restart_per_iteration,
126
+ main_filepath=main_filepath)
127
+
128
+
129
+
130
+
@@ -0,0 +1,48 @@
1
+
2
+
3
+ class BaseObject:
4
+
5
+ def initialize(self, *args, **kwargs) -> None:
6
+ """
7
+ Initialize the class.
8
+
9
+ This method should be implemented in subclasses to initialize specific attributes.
10
+ """
11
+ pass
12
+
13
+ def reinitialize(self, iteration: int, *args, **kwargs) -> None:
14
+ """
15
+ reInitialize the class.
16
+
17
+ This method should be implemented in subclasses to reinitialize specific attributes.
18
+ """
19
+ pass
20
+
21
+ def save(self, foldpath: str, iteration: int) -> None:
22
+ """
23
+ Save the objective function data to a file.
24
+
25
+ Args:
26
+ foldpath (str): The path to save the objective function data.
27
+ iteration (int): The iteration number to save.
28
+ """
29
+ pass
30
+
31
+ def load(self, foldpath: str, iteration: int) -> None:
32
+ """
33
+ Load the objective function data from a file.
34
+
35
+ Args:
36
+ foldpath (str): The path to load the objective function data from.
37
+ iteration (int): The iteration number to load.
38
+ """
39
+ pass
40
+
41
+ def pathlog_required(self) -> list[str]:
42
+ """
43
+ Allocate the path for saving data.
44
+
45
+ Args:
46
+ foldpath (str): The path to allocate.
47
+ """
48
+ return []
@@ -0,0 +1,404 @@
1
+ from __future__ import annotations
2
+ from typing import Optional, TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from .. import Params, MorphSolver, Updaters, ObjectiveFunction
6
+ from .history import History
7
+ import datetime
8
+ import importlib
9
+ import os
10
+ import shutil
11
+ import torch
12
+ import numpy as np
13
+
14
+ import time
15
+ import gc
16
+ import morphopt
17
+ import multiprocessing as mp
18
+
19
+ class Controller:
20
+
21
+ class Params:
22
+ pass
23
+
24
+ class Solver:
25
+ pass
26
+
27
+ class Updater:
28
+ pass
29
+
30
+ class ObjectiveFunction:
31
+ pass
32
+
33
+ def __init__(self, path_result_folder: str, opt_label: str = 'DefaultLabel') -> None:
34
+ """
35
+ Initialize the Controller class.
36
+
37
+ This class is responsible for managing the optimization process.
38
+ It initializes the workflow, generates the model, performs finite element analysis (FEA), and updates the surfaces.
39
+ """
40
+
41
+ self.params: Params
42
+ """
43
+ Params: An instance of the Params class from the ModelParams module.
44
+ """
45
+
46
+ self.solver: MorphSolver
47
+ """
48
+ Solver: An instance of the Solver class from the Solve module.
49
+ """
50
+
51
+ self.updater: Updaters
52
+ """
53
+ Updaters: An instance of the Updaters class from the Update module.
54
+ """
55
+
56
+ self.objfun: ObjectiveFunction
57
+ """
58
+ ObjFun: An instance of the ObjectiveFunction class from the ObjFunc module.
59
+ """
60
+
61
+ self.opt_label: str = opt_label
62
+ """
63
+ The name of the optimization.
64
+ """
65
+
66
+ self.path_result_folder: str = path_result_folder
67
+ """
68
+ The foldpath to the result directory.
69
+ """
70
+
71
+ self.path_result: str = None
72
+ """
73
+ The path to the result directory.
74
+ """
75
+
76
+ self.history = History()
77
+ """
78
+ History: An instance of the History class from the History module to record the optimization history.
79
+ """
80
+
81
+ self.restart_per_iteration: int = 20
82
+ """
83
+ The frequency of restarting the optimization process.
84
+ """
85
+
86
+ self.dataqueue: Optional[mp.Queue] = None
87
+
88
+ self.pools = None
89
+ """
90
+ The multiprocessing pool for parallel computation.
91
+ """
92
+
93
+ self.optdevice: str = 'cpu'
94
+ """
95
+ The device to run the optimization on.
96
+ """
97
+
98
+ def initialize_path(self, main_filepath: str = None) -> None:
99
+ """
100
+ Initialize the workflow by importing necessary modules and setting up the environment.
101
+ """
102
+
103
+ def vendor_package(package_name, target_dir='.'):
104
+ """
105
+ copy the specified package to the target directory.
106
+
107
+ Args:
108
+ package_name: The name of the package to copy.
109
+ target_dir: The target directory, default is the current directory.
110
+ """
111
+ try:
112
+ # import the package
113
+ module = importlib.import_module(package_name)
114
+
115
+ # get the package path
116
+ package_path = os.path.dirname(module.__file__)
117
+
118
+ # construct the target path
119
+ target_path = os.path.join(target_dir, package_name)
120
+
121
+ # if the target directory exists, remove it first
122
+ if os.path.exists(target_path):
123
+ if os.path.isfile(target_path):
124
+ os.remove(target_path)
125
+ else:
126
+ shutil.rmtree(target_path)
127
+
128
+ # copy the package files
129
+ if os.path.isdir(package_path):
130
+ shutil.copytree(package_path, target_path)
131
+ print(f"successfully copied package '{package_name}' to {target_path}")
132
+ else:
133
+ shutil.copy2(package_path, target_path)
134
+ print(f"successfully copied module '{package_name}' to {target_path}")
135
+
136
+ # Check if there are related .pth files or other metadata that need to be handled
137
+ # For pure Python packages, the above steps are usually sufficient
138
+
139
+ except ImportError:
140
+ print(f"Error: Package '{package_name}' not found. Please install it first.")
141
+ except Exception as e:
142
+ print(f"Error occurred during copying: {str(e)}")
143
+
144
+ self.path_result = self.path_result_folder + '/' + self.opt_label + '_' + 'T' + datetime.datetime.now().strftime(
145
+ "%Y%m%d_%H%M%S") + '/'
146
+
147
+ # create the result path if it does not exist
148
+ os.makedirs(self.path_result + '/cache/')
149
+ pathlog_list = []
150
+ pathlog_list += self.params.pathlog_required()
151
+ pathlog_list += self.solver.pathlog_required()
152
+ pathlog_list += self.updater.pathlog_required()
153
+ pathlog_list += self.objfun.pathlog_required()
154
+ for pathlog in pathlog_list:
155
+ os.makedirs(self.path_result + '/log/' + pathlog)
156
+
157
+ os.makedirs(self.path_result + '/fea')
158
+
159
+ os.makedirs(self.path_result + '/scripts/', exist_ok=True)
160
+ if main_filepath is not None:
161
+ shutil.copy(main_filepath, self.path_result + '/scripts/MAIN_SCRIPT_FOR_RESTART.py')
162
+ else:
163
+ import __main__
164
+ shutil.copy(__main__.__file__, self.path_result + '/scripts/MAIN_SCRIPT_FOR_RESTART.py')
165
+
166
+ vendor_package('torchfea', target_dir=self.path_result + '/scripts/')
167
+ vendor_package('cpgeo', target_dir=self.path_result + '/scripts/')
168
+ vendor_package('morphopt', target_dir=self.path_result + '/scripts/')
169
+
170
+
171
+ def initialize(self) -> None:
172
+ """
173
+ Initialize the workflow by initializing the Params, Solver, Updaters, and ObjFun classes.
174
+ """
175
+ morphopt.controller = self
176
+
177
+ self.params = self.Params()
178
+ self.solver = self.Solver(params=self.params)
179
+ self.updater = self.Updater(params=self.params)
180
+ self.objfun = self.ObjectiveFunction()
181
+
182
+
183
+ self.params.initialize()
184
+ self.solver.initialize()
185
+ self.updater.initialize()
186
+ self.objfun.initialize()
187
+ self.history.initialize()
188
+
189
+ self.pools = mp.Pool(processes=self.solver.num_process)
190
+
191
+ def start_optimization(self, main_filepath: str = None) -> None:
192
+
193
+ self.initialize()
194
+ self.initialize_path(main_filepath=main_filepath)
195
+ self.opt_loop()
196
+
197
+ def restart_optimization(self, path_result: str, target_iteration: int = None) -> None:
198
+
199
+ self.path_result = path_result
200
+ self.initialize()
201
+ self.history.load(foldpath=self.path_result + '/log/', iteration=target_iteration)
202
+
203
+ if target_iteration is None:
204
+ target_iteration = self.history.iteration
205
+
206
+ self.params.load(foldpath=self.path_result + '/log/', iteration=target_iteration)
207
+ self.solver.load(foldpath=self.path_result + '/log/', iteration=target_iteration)
208
+ self.updater.load(foldpath=self.path_result + '/log/', iteration=target_iteration)
209
+
210
+ self.history.iteration += 1
211
+
212
+ self.opt_loop()
213
+
214
+ def opt_loop(self) -> None:
215
+ """
216
+ This function runs the optimization loop for a specified number of iterations.
217
+ It calls the opt_step function in each iteration.
218
+ """
219
+
220
+ while True:
221
+
222
+ # Explicitly release large objects to ensure they are collected
223
+ self.clear_cache()
224
+
225
+ seed_size0 = self.params.geometry.fea_seed_size
226
+ # self.params.geometry.fea_seed_size = seed_size0 * np.random.uniform(0.9, 1.0)
227
+
228
+ loss, t0, t1, t2, t3 = self.step()
229
+
230
+ self.params.geometry.fea_seed_size = seed_size0
231
+
232
+ # Record the history of the optimization process
233
+ self.record_history(loss, t0, t1, t2, t3)
234
+
235
+ # Print the information
236
+ self.print_info(t0, t1, t2, t3)
237
+
238
+ # Save the current parameters and plot the figures
239
+ self.save()
240
+
241
+ # if dataqueue is not None, send the data to the queue
242
+ if self.dataqueue is not None:
243
+ self.dataqueue.put({'iteration': self.history.iteration, 'path_result': self.path_result})
244
+
245
+ if self.restart_per_iteration > 0 and (self.history.iteration+1) % self.restart_per_iteration == 0:
246
+ self.pools.close()
247
+ self.pools.join()
248
+ return
249
+
250
+ self.history.iteration += 1
251
+
252
+
253
+ def step(self):
254
+ """
255
+ Perform a single optimization step.
256
+ Returns:
257
+ loss (torch.Tensor): The loss value after the optimization step.
258
+ t0 (float): The start time of the optimization step.
259
+ t1 (float): The time after model generation.
260
+ t2 (float): The time after finite element analysis (FEA).
261
+ t3 (float): The time after updating the surfaces.
262
+ """
263
+ t0 = time.time()
264
+ if os.path.exists(self.path_result + '/cache/TopOptRun.inp'):
265
+ os.remove(self.path_result + '/cache/TopOptRun.inp')
266
+
267
+ # Initialize the workflow
268
+ self.params.reinitialize(iteration = self.history.iteration)
269
+ self.solver.reinitialize(iteration = self.history.iteration)
270
+ self.updater.reinitialize(iteration = self.history.iteration)
271
+ self.objfun.reinitialize(iteration = self.history.iteration)
272
+
273
+ # Perform the optimization step
274
+ # Generate the model
275
+ inp = self.params.geometry.generate()
276
+ self.objfun.inp = inp
277
+ fe = self.params.feamodel.create_fea(inp=inp)
278
+ self.params.materials.set_materials(fe)
279
+ fe.initialize()
280
+ self.objfun.fe = fe
281
+
282
+ t1 = time.time()
283
+
284
+ # Perform finite element analysis (FEA)
285
+ self.objfun.U = self.solver.solve()
286
+
287
+ t2 = time.time()
288
+
289
+ # the adjoint problem
290
+ self.objfun.calculate_adjoint_problem()
291
+
292
+ # Update the surfaces based on the FEA results
293
+ self.updater.update()
294
+ self.updater.update_variables()
295
+
296
+ loss = self.objfun.get_objective()
297
+
298
+ t3 = time.time()
299
+
300
+ return loss, t0, t1, t2, t3
301
+
302
+ def record_history(self, loss: torch.Tensor, t0: float, t1: float, t2: float, t3: float) -> None:
303
+ """
304
+ Record the history of the optimization process.
305
+ """
306
+ if self.objfun.fe.assembly._reference_points.get('RP_head') is None:
307
+ displacement = []
308
+ else:
309
+ GC_start_index = self.objfun.fe.assembly._GC_list_indexStart[self.objfun.fe.assembly.get_reference_point('RP_head')._RGC_index]
310
+ displacement = [self.objfun.U[i][GC_start_index:GC_start_index+6].tolist() for i in range(len(self.objfun.U))]
311
+ self.history.append('deformation', displacement)
312
+ self.history.append('objective', loss.item())
313
+ self.history.append('metrics', self.objfun.get_metrics())
314
+ self.history.append('time', [t1-t0, t2-t1, t3-t2])
315
+ self.history.append('num_elements', self.objfun.fe.assembly.get_instance('final_model').elems['element-0']._elems.shape[0])
316
+ self.history.append('num_nodes', self.objfun.fe.assembly.get_instance('final_model').nodes.shape[0])
317
+
318
+ def print_info(self, t0, t1, t2, t3) -> None:
319
+ """
320
+ Print the current information of the optimization process.
321
+ """
322
+ print(f"Iteration: {self.history.iteration}")
323
+ print(self.objfun)
324
+ print(f"Loss: {self.history.history_objective[-1]:.6f}")
325
+ print("Time Breakdown:")
326
+ print(f" Initialization Time: {t1 - t0:.2f} seconds")
327
+ print(f" FEA Time: {t2 - t1:.2f} seconds")
328
+ print(f" Update Time: {t3 - t2:.2f} seconds")
329
+ print(f" Total Time: {t3 - t0:.2f} seconds")
330
+ print("-" * 50)
331
+
332
+ def save(self) -> None:
333
+ """
334
+ Save the current state of the optimization process.
335
+ """
336
+ self.params.save(foldpath=self.path_result + '/log/', iteration=self.history.iteration)
337
+ self.solver.save(foldpath=self.path_result + '/log/', iteration=self.history.iteration)
338
+ self.updater.save(foldpath=self.path_result + '/log/', iteration=self.history.iteration)
339
+ self.history.save(foldpath=self.path_result + '/log/', iteration=self.history.iteration)
340
+ self.objfun.save(foldpath=self.path_result + '/log/', iteration=self.history.iteration)
341
+
342
+ def clear_cache(self) -> None:
343
+ """
344
+ Clear the cache directory.
345
+ """
346
+ self.objfun.K_sp = []
347
+ self.objfun.K_solver = []
348
+ self.objfun.U = None
349
+ self.objfun.ADJu = None
350
+ self.objfun.fe = None
351
+
352
+ torch.cuda.empty_cache()
353
+ data = gc.collect()
354
+ print(f"Garbage collector: collected {data} objects.")
355
+
356
+ def change_device(self, device: torch.device, obj: object = None) -> None:
357
+ """
358
+ Recursively change the device of the finite element model and all nested objects.
359
+
360
+ Args:
361
+ device (torch.device): The target device.
362
+ obj (object, optional): The object to change the device for. If None, change the device for the Controller instance itself.
363
+
364
+ Returns:
365
+ None
366
+ """
367
+ if obj is not None:
368
+ self._change_device_recursive(obj, device)
369
+ else:
370
+ self._change_device_recursive(self, device)
371
+
372
+ def _change_device_recursive(self, obj, device, visited=None):
373
+ """
374
+ Recursively move tensors to the target device.
375
+ """
376
+ if visited is None:
377
+ visited = set()
378
+
379
+ obj_id = id(obj)
380
+ if obj_id in visited:
381
+ return
382
+ visited.add(obj_id)
383
+
384
+ if isinstance(obj, dict):
385
+ for k, v in obj.items():
386
+ if isinstance(v, torch.Tensor):
387
+ obj[k] = v.to(device)
388
+ else:
389
+ self._change_device_recursive(v, device, visited)
390
+ elif isinstance(obj, list):
391
+ for i, v in enumerate(obj):
392
+ if isinstance(v, torch.Tensor):
393
+ obj[i] = v.to(device)
394
+ else:
395
+ self._change_device_recursive(v, device, visited)
396
+ elif isinstance(obj, tuple):
397
+ for v in obj:
398
+ self._change_device_recursive(v, device, visited)
399
+ elif hasattr(obj, '__dict__'):
400
+ for k, v in list(obj.__dict__.items()):
401
+ if isinstance(v, torch.Tensor):
402
+ setattr(obj, k, v.to(device))
403
+ else:
404
+ self._change_device_recursive(v, device, visited)