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.
- 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
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)
|