pyfemtet 0.1.12__py3-none-any.whl → 0.2.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 pyfemtet might be problematic. Click here for more details.
- pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.femprj +0 -0
- pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.prt +0 -0
- pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.py +69 -32
- pyfemtet/FemtetPJTSample/gau_ex08_parametric.femprj +0 -0
- pyfemtet/FemtetPJTSample/gau_ex08_parametric.py +37 -25
- pyfemtet/FemtetPJTSample/her_ex40_parametric.py +57 -35
- pyfemtet/FemtetPJTSample/wat_ex14_parallel_parametric.py +62 -0
- pyfemtet/FemtetPJTSample/wat_ex14_parametric.femprj +0 -0
- pyfemtet/FemtetPJTSample/wat_ex14_parametric.py +61 -0
- pyfemtet/__init__.py +1 -1
- pyfemtet/opt/_FemtetWithNX/update_model.py +6 -2
- pyfemtet/opt/__init__.py +1 -1
- pyfemtet/opt/base.py +457 -86
- pyfemtet/opt/core.py +77 -17
- pyfemtet/opt/interface.py +217 -137
- pyfemtet/opt/monitor.py +181 -98
- pyfemtet/opt/{_optuna.py → optimizer.py} +70 -30
- pyfemtet/tools/DispatchUtils.py +46 -44
- {pyfemtet-0.1.12.dist-info → pyfemtet-0.2.1.dist-info}/LICENSE +1 -1
- pyfemtet-0.2.1.dist-info/METADATA +42 -0
- pyfemtet-0.2.1.dist-info/RECORD +31 -0
- pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01 - original.x_t +0 -359
- pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.x_t +0 -359
- pyfemtet/FemtetPJTSample/fem4 = Femtet(femprj_path=None, model_name=None, connect_method='catch').femprj +0 -0
- pyfemtet/FemtetPJTSample/gal_ex11_parametric.femprj +0 -0
- pyfemtet/FemtetPJTSample/gal_ex11_parametric.py +0 -54
- pyfemtet/FemtetPJTSample/pas_ex1_parametric.femprj +0 -0
- pyfemtet/FemtetPJTSample/pas_ex1_parametric.py +0 -66
- pyfemtet/FemtetPJTSample/pas_ex1_parametric2.py +0 -68
- pyfemtet/tools/FemtetClassConst.py +0 -9
- pyfemtet-0.1.12.dist-info/METADATA +0 -205
- pyfemtet-0.1.12.dist-info/RECORD +0 -37
- {pyfemtet-0.1.12.dist-info → pyfemtet-0.2.1.dist-info}/WHEEL +0 -0
pyfemtet/opt/base.py
CHANGED
|
@@ -13,18 +13,27 @@ import pandas as pd
|
|
|
13
13
|
from optuna._hypervolume import WFG
|
|
14
14
|
import ray
|
|
15
15
|
|
|
16
|
-
from .
|
|
16
|
+
from win32com.client import Constants
|
|
17
|
+
|
|
18
|
+
from .core import InterprocessVariables, UserInterruption, TerminatableThread, Scapegoat, restore_constants_from_scapegoat
|
|
17
19
|
from .interface import FEMInterface, FemtetInterface
|
|
18
20
|
from .monitor import Monitor
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
def symlog(x):
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
def symlog(x: float | np.ndarray):
|
|
24
|
+
"""Log function whose domain is extended to the negative region.
|
|
25
|
+
|
|
26
|
+
Symlog processing is performed internally as a measure to reduce
|
|
27
|
+
unintended trends caused by scale differences
|
|
28
|
+
between objective functions in multi-objective optimization.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
x (float | np.ndarray)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
float
|
|
35
|
+
"""
|
|
36
|
+
|
|
28
37
|
if isinstance(x, np.ndarray):
|
|
29
38
|
ret = np.zeros(x.shape)
|
|
30
39
|
idx = np.where(x >= 0)
|
|
@@ -47,9 +56,9 @@ def _check_direction(direction):
|
|
|
47
56
|
pass
|
|
48
57
|
elif isinstance(direction, str):
|
|
49
58
|
if (direction != 'minimize') and (direction != 'maximize'):
|
|
50
|
-
raise
|
|
59
|
+
raise ValueError(message)
|
|
51
60
|
else:
|
|
52
|
-
raise
|
|
61
|
+
raise ValueError(message)
|
|
53
62
|
|
|
54
63
|
|
|
55
64
|
def _check_lb_ub(lb, ub, name=None):
|
|
@@ -58,7 +67,7 @@ def _check_lb_ub(lb, ub, name=None):
|
|
|
58
67
|
message = f'{name}に対して' + message
|
|
59
68
|
if (lb is not None) and (ub is not None):
|
|
60
69
|
if lb > ub:
|
|
61
|
-
raise
|
|
70
|
+
raise ValueError(message)
|
|
62
71
|
|
|
63
72
|
|
|
64
73
|
def _is_access_gogh(fun):
|
|
@@ -106,14 +115,30 @@ def _ray_are_alive(refs):
|
|
|
106
115
|
|
|
107
116
|
|
|
108
117
|
class Function:
|
|
118
|
+
"""Base class for Objective and Constraint."""
|
|
109
119
|
|
|
110
120
|
def __init__(self, fun, name, args, kwargs):
|
|
121
|
+
# unserializable な COM 定数を parallelize するための処理
|
|
122
|
+
for varname in fun.__globals__:
|
|
123
|
+
if isinstance(fun.__globals__[varname], Constants):
|
|
124
|
+
fun.__globals__[varname] = Scapegoat()
|
|
111
125
|
self.fun = fun
|
|
112
126
|
self.name = name
|
|
113
127
|
self.args = args
|
|
114
128
|
self.kwargs = kwargs
|
|
115
129
|
|
|
116
|
-
def calc(self, fem):
|
|
130
|
+
def calc(self, fem: FEMInterface):
|
|
131
|
+
"""Execute user-defined fun.
|
|
132
|
+
|
|
133
|
+
If fem is a FemtetInterface,
|
|
134
|
+
the 1st argument of fun is set to fem automatically.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
fem (FEMInterface)
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
float
|
|
141
|
+
"""
|
|
117
142
|
args = self.args
|
|
118
143
|
# Femtet 特有の処理
|
|
119
144
|
if isinstance(fem, FemtetInterface):
|
|
@@ -122,15 +147,46 @@ class Function:
|
|
|
122
147
|
|
|
123
148
|
|
|
124
149
|
class Objective(Function):
|
|
150
|
+
"""Class for registering user-defined objective function."""
|
|
125
151
|
|
|
126
152
|
default_name = 'obj'
|
|
127
153
|
|
|
128
154
|
def __init__(self, fun, name, direction, args, kwargs):
|
|
155
|
+
"""Initializes an Objective instance.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
fun: The user-defined objective function.
|
|
159
|
+
name (str): The name of the objective function.
|
|
160
|
+
direction (str or float or int): The direction of optimization.
|
|
161
|
+
args: Additional arguments for the objective function.
|
|
162
|
+
kwargs: Additional keyword arguments for the objective function.
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
ValueError: If the direction is not valid.
|
|
166
|
+
|
|
167
|
+
"""
|
|
129
168
|
_check_direction(direction)
|
|
130
169
|
self.direction = direction
|
|
131
170
|
super().__init__(fun, name, args, kwargs)
|
|
132
171
|
|
|
133
|
-
def
|
|
172
|
+
def convert(self, value: float):
|
|
173
|
+
"""Converts an evaluation value to the value of user-defined objective function based on the specified direction.
|
|
174
|
+
|
|
175
|
+
When direction is `'minimize'`, ``value`` is calculated.
|
|
176
|
+
When direction is `'maximize'`, ``-value`` is calculated.
|
|
177
|
+
When direction is float, ``abs(value - direction)`` is calculated.
|
|
178
|
+
Finally, the calculated value is passed to the symlog function.
|
|
179
|
+
|
|
180
|
+
``value`` is the return value of the user-defined function.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
value (float): The evaluation value to be converted.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
float: The converted objective value.
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
|
|
134
190
|
# 評価関数(direction 任意)を目的関数(minimize, symlog)に変換する
|
|
135
191
|
ret = value
|
|
136
192
|
if isinstance(self.direction, float) or isinstance(self.direction, int):
|
|
@@ -146,10 +202,27 @@ class Objective(Function):
|
|
|
146
202
|
|
|
147
203
|
|
|
148
204
|
class Constraint(Function):
|
|
205
|
+
"""Class for registering user-defined constraint function."""
|
|
149
206
|
|
|
150
207
|
default_name = 'cns'
|
|
151
208
|
|
|
152
209
|
def __init__(self, fun, name, lb, ub, strict, args, kwargs):
|
|
210
|
+
"""Initializes a Constraint instance.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
fun: The user-defined constraint function.
|
|
214
|
+
name (str): The name of the constraint function.
|
|
215
|
+
lb: The lower bound of the constraint.
|
|
216
|
+
ub: The upper bound of the constraint.
|
|
217
|
+
strict (bool): Whether to enforce strict inequality for the bounds.
|
|
218
|
+
args: Additional arguments for the constraint function.
|
|
219
|
+
kwargs: Additional keyword arguments for the constraint function.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
ValueError: If the lower bound is greater than or equal to the upper bound.
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
|
|
153
226
|
_check_lb_ub(lb, ub)
|
|
154
227
|
self.lb = lb
|
|
155
228
|
self.ub = ub
|
|
@@ -157,24 +230,70 @@ class Constraint(Function):
|
|
|
157
230
|
super().__init__(fun, name, args, kwargs)
|
|
158
231
|
|
|
159
232
|
|
|
233
|
+
@ray.remote
|
|
234
|
+
class HistoryDfCore:
|
|
235
|
+
"""Class for managing a DataFrame object in a distributed manner."""
|
|
236
|
+
|
|
237
|
+
def __init__(self, df):
|
|
238
|
+
self.df = df
|
|
239
|
+
|
|
240
|
+
def set_df(self, df):
|
|
241
|
+
self.df = df
|
|
242
|
+
|
|
243
|
+
def get_df(self):
|
|
244
|
+
return self.df
|
|
245
|
+
|
|
246
|
+
|
|
160
247
|
class History:
|
|
248
|
+
"""Class for managing the history of optimization results.
|
|
161
249
|
|
|
162
|
-
|
|
250
|
+
Attributes:
|
|
251
|
+
path (str): The path to the history file.
|
|
252
|
+
_actor_data (HistoryDfCore): The distributed DataFrame object for storing actor data.
|
|
253
|
+
data (pd.DataFrame): The DataFrame object for storing the entire history.
|
|
254
|
+
param_names (list): The names of the parameters in the study.
|
|
255
|
+
obj_names (list): The names of the objectives in the study.
|
|
256
|
+
cns_names (list): The names of the constraints in the study.
|
|
257
|
+
|
|
258
|
+
"""
|
|
259
|
+
def __init__(self, history_path):
|
|
260
|
+
"""Initializes a History instance.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
history_path (str): The path to the history file.
|
|
264
|
+
|
|
265
|
+
"""
|
|
163
266
|
|
|
164
267
|
# 引数の処理
|
|
165
268
|
self.path = history_path # .csv
|
|
166
|
-
self.
|
|
269
|
+
self._actor_data = HistoryDfCore.remote(pd.DataFrame())
|
|
167
270
|
self.data = pd.DataFrame()
|
|
168
271
|
self.param_names = []
|
|
169
272
|
self.obj_names = []
|
|
170
273
|
self.cns_names = []
|
|
171
|
-
self._data_columns = []
|
|
172
274
|
|
|
173
275
|
# path が存在すれば dataframe を読み込む
|
|
174
276
|
if os.path.isfile(self.path):
|
|
277
|
+
self.actor_data = pd.read_csv(self.path)
|
|
175
278
|
self.data = pd.read_csv(self.path)
|
|
176
279
|
|
|
280
|
+
@property
|
|
281
|
+
def actor_data(self):
|
|
282
|
+
return ray.get(self._actor_data.get_df.remote())
|
|
283
|
+
|
|
284
|
+
@actor_data.setter
|
|
285
|
+
def actor_data(self, df):
|
|
286
|
+
self._actor_data.set_df.remote(df)
|
|
287
|
+
|
|
177
288
|
def init(self, param_names, obj_names, cns_names):
|
|
289
|
+
"""Initializes the parameter, objective, and constraint names in the History instance.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
param_names (list): The names of parameters in optimization.
|
|
293
|
+
obj_names (list): The names of objectives in optimization.
|
|
294
|
+
cns_names (list): The names of constraints in optimization.
|
|
295
|
+
|
|
296
|
+
"""
|
|
178
297
|
self.param_names = param_names
|
|
179
298
|
self.obj_names = obj_names
|
|
180
299
|
self.cns_names = cns_names
|
|
@@ -191,21 +310,40 @@ class History:
|
|
|
191
310
|
columns.append('hypervolume')
|
|
192
311
|
columns.append('message')
|
|
193
312
|
columns.append('time')
|
|
194
|
-
self._data_columns = columns
|
|
195
313
|
|
|
196
314
|
# restart ならば前のデータとの整合を確認
|
|
197
|
-
if len(self.
|
|
315
|
+
if len(self.actor_data.columns) > 0:
|
|
198
316
|
# 読み込んだ columns が生成した columns と違っていればエラー
|
|
199
317
|
try:
|
|
200
|
-
if self.
|
|
201
|
-
raise Exception(f'読み込んだ history と問題の設定が異なります. \n\n読み込まれた設定:\n{list(self.
|
|
318
|
+
if list(self.actor_data.columns) != columns:
|
|
319
|
+
raise Exception(f'読み込んだ history と問題の設定が異なります. \n\n読み込まれた設定:\n{list(self.actor_data.columns)}\n\n現在の設定:\n{columns}')
|
|
202
320
|
else:
|
|
203
321
|
# 同じであっても目的と拘束の上下限や direction が違えばエラー
|
|
204
322
|
pass
|
|
205
323
|
except ValueError:
|
|
206
|
-
raise Exception(f'読み込んだ history と問題の設定が異なります. \n\n読み込まれた設定:\n{list(self.
|
|
324
|
+
raise Exception(f'読み込んだ history と問題の設定が異なります. \n\n読み込まれた設定:\n{list(self.actor_data.columns)}\n\n現在の設定:\n{columns}')
|
|
325
|
+
|
|
326
|
+
else:
|
|
327
|
+
# actor_data は actor 経由の getter property なので self.data[column] = ... とやっても
|
|
328
|
+
# actor には変更が反映されない. 以下同様
|
|
329
|
+
tmp = self.actor_data
|
|
330
|
+
for column in columns:
|
|
331
|
+
tmp[column] = None
|
|
332
|
+
self.actor_data = tmp
|
|
333
|
+
self.data = self.actor_data.copy()
|
|
207
334
|
|
|
208
335
|
def record(self, parameters, objectives, constraints, obj_values, cns_values, message):
|
|
336
|
+
"""Records the optimization results in the history.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
parameters (dict): The parameter values.
|
|
340
|
+
objectives (dict): The objective functions.
|
|
341
|
+
constraints (dict): The constraint functions.
|
|
342
|
+
obj_values (list): The objective values.
|
|
343
|
+
cns_values (list): The constraint values.
|
|
344
|
+
message (str): Additional information or messages related to the optimization results.
|
|
345
|
+
|
|
346
|
+
"""
|
|
209
347
|
|
|
210
348
|
# create row
|
|
211
349
|
row = list()
|
|
@@ -219,88 +357,99 @@ class History:
|
|
|
219
357
|
row.extend([cns_value, cns.lb, cns.ub])
|
|
220
358
|
feasible_list.append(_is_feasible(cns_value, cns.lb, cns.ub))
|
|
221
359
|
row.append(all(feasible_list))
|
|
222
|
-
row.append(
|
|
360
|
+
row.append(-1.) # dummy hypervolume
|
|
223
361
|
row.append(message) # message
|
|
224
362
|
row.append(datetime.datetime.now()) # time
|
|
225
363
|
|
|
226
364
|
# append
|
|
227
|
-
self.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
365
|
+
if len(self.actor_data) == 0:
|
|
366
|
+
self.actor_data = pd.DataFrame([row], columns=self.actor_data.columns)
|
|
367
|
+
else:
|
|
368
|
+
tmp = self.actor_data
|
|
369
|
+
tmp.loc[len(tmp)] = row
|
|
370
|
+
self.actor_data = tmp
|
|
233
371
|
|
|
234
372
|
# calc
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
373
|
+
try:
|
|
374
|
+
tmp = self.actor_data
|
|
375
|
+
tmp['trial'] = np.arange(len(tmp)) + 1 # 1 始まり
|
|
376
|
+
self.actor_data = tmp
|
|
377
|
+
self._calc_non_domi(objectives)
|
|
378
|
+
self._calc_hypervolume(objectives)
|
|
379
|
+
except (ValueError, pd.errors.IndexingError): # 計算中に別のプロセスが append した場合、そちらに処理を任せる
|
|
380
|
+
pass
|
|
238
381
|
|
|
239
382
|
# serialize
|
|
240
383
|
try:
|
|
241
|
-
self.
|
|
384
|
+
self.actor_data.to_csv(self.path, index=None)
|
|
242
385
|
except PermissionError:
|
|
243
386
|
print(f'warning: {self.path} がロックされています。データはロック解除後に保存されます。')
|
|
244
387
|
|
|
388
|
+
# unparallelize
|
|
389
|
+
self.data = self.actor_data.copy()
|
|
390
|
+
|
|
245
391
|
def _calc_non_domi(self, objectives):
|
|
246
392
|
|
|
247
393
|
# 目的関数の履歴を取り出してくる
|
|
248
|
-
solution_set = self.
|
|
394
|
+
solution_set = self.actor_data[self.obj_names].copy()
|
|
249
395
|
|
|
250
396
|
# 最小化問題の座標空間に変換する
|
|
251
397
|
for name, objective in objectives.items():
|
|
252
|
-
solution_set[name] = solution_set[name].map(objective.
|
|
398
|
+
solution_set[name] = solution_set[name].map(objective.convert)
|
|
253
399
|
|
|
254
|
-
|
|
400
|
+
# 非劣解の計算
|
|
255
401
|
non_domi = []
|
|
256
402
|
for i, row in solution_set.iterrows():
|
|
257
403
|
non_domi.append((row > solution_set).product(axis=1).sum(axis=0) == 0)
|
|
258
404
|
|
|
259
405
|
# 非劣解の登録
|
|
260
|
-
|
|
406
|
+
tmp = self.actor_data
|
|
407
|
+
tmp['non_domi'] = non_domi
|
|
408
|
+
self.actor_data = tmp
|
|
261
409
|
|
|
262
410
|
del solution_set
|
|
263
411
|
|
|
264
412
|
def _calc_hypervolume(self, objectives):
|
|
265
|
-
"""
|
|
266
|
-
hypervolume 履歴を更新する
|
|
267
|
-
※ reference point が変わるたびに hypervolume を計算しなおす必要がある
|
|
268
|
-
[1]Hisao Ishibuchi et al. "Reference Point Specification in Hypercolume Calculation for Fair Comparison and Efficient Search"
|
|
269
|
-
"""
|
|
270
413
|
#### 前準備
|
|
271
414
|
# パレート集合の抽出
|
|
272
|
-
idx = self.
|
|
273
|
-
pdf = self.
|
|
415
|
+
idx = self.actor_data['non_domi'].values
|
|
416
|
+
pdf = self.actor_data[idx]
|
|
274
417
|
pareto_set = pdf[self.obj_names].values
|
|
275
418
|
n = len(pareto_set) # 集合の要素数
|
|
276
419
|
m = len(pareto_set.T) # 目的変数数
|
|
420
|
+
# 多目的でないと計算できない
|
|
421
|
+
if m <= 1:
|
|
422
|
+
return None
|
|
277
423
|
# 長さが 2 以上でないと計算できない
|
|
278
424
|
if n <= 1:
|
|
279
|
-
return
|
|
425
|
+
return None
|
|
280
426
|
# 最小化問題に convert
|
|
281
427
|
for i, (name, objective) in enumerate(objectives.items()):
|
|
282
428
|
for j in range(n):
|
|
283
|
-
pareto_set[j, i] = objective.
|
|
429
|
+
pareto_set[j, i] = objective.convert(pareto_set[j, i])
|
|
284
430
|
#### reference point の計算[1]
|
|
285
431
|
# 逆正規化のための範囲計算
|
|
286
432
|
maximum = pareto_set.max(axis=0)
|
|
287
433
|
minimum = pareto_set.min(axis=0)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
#
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
434
|
+
|
|
435
|
+
# # [1]Hisao Ishibuchi et al. "Reference Point Specification in Hypercolume Calculation for Fair Comparison and Efficient Search"
|
|
436
|
+
# # (H+m-1)C(m-1) <= n <= (m-1)C(H+m) になるような H を探す[1]
|
|
437
|
+
# H = 0
|
|
438
|
+
# while True:
|
|
439
|
+
# left = math.comb(H + m - 1, m - 1)
|
|
440
|
+
# right = math.comb(H + m, m - 1)
|
|
441
|
+
# if left <= n <= right:
|
|
442
|
+
# break
|
|
443
|
+
# else:
|
|
444
|
+
# H += 1
|
|
445
|
+
# # H==0 なら r は最大の値
|
|
446
|
+
# if H == 0:
|
|
447
|
+
# r = 2
|
|
448
|
+
# else:
|
|
449
|
+
# # r を計算
|
|
450
|
+
# r = 1 + 1. / H
|
|
303
451
|
r = 1.01
|
|
452
|
+
|
|
304
453
|
# r を逆正規化
|
|
305
454
|
reference_point = r * (maximum - minimum) + minimum
|
|
306
455
|
|
|
@@ -314,7 +463,7 @@ class History:
|
|
|
314
463
|
hvs.append(hv)
|
|
315
464
|
|
|
316
465
|
# 計算結果を履歴の一部に割り当て
|
|
317
|
-
df = self.
|
|
466
|
+
df = pd.DataFrame(self.actor_data.to_dict()) # read-only error 回避
|
|
318
467
|
df.loc[idx, 'hypervolume'] = np.array(hvs)
|
|
319
468
|
|
|
320
469
|
# dominated の行に対して、上に見ていって
|
|
@@ -326,19 +475,44 @@ class History:
|
|
|
326
475
|
except IndexError:
|
|
327
476
|
# pass # nan のままにする
|
|
328
477
|
df.loc[i, 'hypervolume'] = 0
|
|
478
|
+
self.actor_data = df
|
|
329
479
|
|
|
330
480
|
|
|
331
481
|
class OptimizerBase(ABC):
|
|
482
|
+
"""Base class for optimization algorithms.
|
|
483
|
+
|
|
484
|
+
Attributes:
|
|
485
|
+
fem (FEMInterface): The finite element method interface.
|
|
486
|
+
history_path (str): The path to the history (.csv) file .
|
|
487
|
+
ipv (InterprocessVariables): The interprocess variables.
|
|
488
|
+
parameters (pd.DataFrame): The DataFrame object for storing the parameters.
|
|
489
|
+
objectives (dict): The dictionary of objective functions.
|
|
490
|
+
constraints (dict): The dictionary of constraint functions.
|
|
491
|
+
history (History): The history of optimization results.
|
|
492
|
+
monitor (Monitor): The monitor object for visualization and monitoring.
|
|
493
|
+
monitor_thread: Thread object for monitor server.
|
|
494
|
+
seed (int or None): The random seed for reproducibility.
|
|
495
|
+
message(str) : Additional information or messages related to the optimization process
|
|
496
|
+
obj_values ([float]): A list to store objective values during optimization
|
|
497
|
+
cns_values ([float]): A list to store constraint values during optimization
|
|
498
|
+
|
|
499
|
+
"""
|
|
332
500
|
|
|
333
501
|
def __init__(self, fem: FEMInterface = None, history_path=None):
|
|
502
|
+
"""Initializes an OptimizerBase instance.
|
|
334
503
|
|
|
504
|
+
Args:
|
|
505
|
+
fem (FEMInterface, optional): The finite element method interface. Defaults to None. If None, automattically set to FemtetInterface.
|
|
506
|
+
history_path (str, optional): The path to the history file. Defaults to None. If None, '%Y_%m_%d_%H_%M_%S.csv' is created in current directory.
|
|
507
|
+
|
|
508
|
+
"""
|
|
335
509
|
print('---initialize---')
|
|
336
510
|
|
|
337
511
|
ray.init(ignore_reinit_error=True)
|
|
338
512
|
|
|
339
513
|
# 引数の処理
|
|
340
514
|
if history_path is None:
|
|
341
|
-
history_path = datetime.datetime.now().strftime('%Y_%m_%d_%H_%
|
|
515
|
+
history_path = datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S.csv')
|
|
342
516
|
self.history_path = os.path.abspath(history_path)
|
|
343
517
|
if fem is None:
|
|
344
518
|
self.fem = FemtetInterface()
|
|
@@ -350,8 +524,10 @@ class OptimizerBase(ABC):
|
|
|
350
524
|
self.parameters = pd.DataFrame()
|
|
351
525
|
self.objectives = dict()
|
|
352
526
|
self.constraints = dict()
|
|
353
|
-
self.history = History(self.history_path
|
|
527
|
+
self.history = History(self.history_path)
|
|
354
528
|
self.monitor: Monitor = None
|
|
529
|
+
self.monitor_thread = None
|
|
530
|
+
self.monitor_server_kwargs = dict()
|
|
355
531
|
self.seed: int or None = None
|
|
356
532
|
self.message = ''
|
|
357
533
|
self.obj_values: [float] = []
|
|
@@ -372,12 +548,19 @@ class OptimizerBase(ABC):
|
|
|
372
548
|
state = self.__dict__.copy()
|
|
373
549
|
del state['fem']
|
|
374
550
|
del state['monitor']
|
|
551
|
+
del state['monitor_thread']
|
|
375
552
|
return state
|
|
376
553
|
|
|
377
554
|
def __setstate__(self, state):
|
|
378
555
|
self.__dict__.update(state)
|
|
379
556
|
|
|
380
557
|
def set_fem(self, **extra_kwargs):
|
|
558
|
+
"""Sets or resets the finite element method interface.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
**extra_kwargs: Additional keyword arguments to be passed to the FEMInterface constructor.
|
|
562
|
+
|
|
563
|
+
"""
|
|
381
564
|
fem_kwargs = self._fem_kwargs.copy()
|
|
382
565
|
fem_kwargs.update(extra_kwargs)
|
|
383
566
|
self.fem = self._fem_class(
|
|
@@ -385,9 +568,21 @@ class OptimizerBase(ABC):
|
|
|
385
568
|
)
|
|
386
569
|
|
|
387
570
|
def set_random_seed(self, seed: int):
|
|
571
|
+
"""Sets the random seed for reproducibility.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
seed (int): The random seed value to be set.
|
|
575
|
+
|
|
576
|
+
"""
|
|
388
577
|
self.seed = seed
|
|
389
578
|
|
|
390
579
|
def get_random_seed(self):
|
|
580
|
+
"""Returns the current random seed value.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
int: The current random seed value.
|
|
584
|
+
|
|
585
|
+
"""
|
|
391
586
|
return self.seed
|
|
392
587
|
|
|
393
588
|
def add_parameter(
|
|
@@ -398,6 +593,18 @@ class OptimizerBase(ABC):
|
|
|
398
593
|
upper_bound: float or None = None,
|
|
399
594
|
memo: str = ''
|
|
400
595
|
):
|
|
596
|
+
"""Adds a parameter to the optimization problem.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
name (str): The name of the parameter.
|
|
600
|
+
initial_value (float or None, optional): The initial value of the parameter. Defaults to None. If None, try to get inittial value from FEMInterface.
|
|
601
|
+
lower_bound (float or None, optional): The lower bound of the parameter. Defaults to None. However, this argument is required for some algorithms.
|
|
602
|
+
upper_bound (float or None, optional): The upper bound of the parameter. Defaults to None. However, this argument is required for some algorithms.
|
|
603
|
+
memo (str, optional): Additional information about the parameter. Defaults to ''.
|
|
604
|
+
Raises:
|
|
605
|
+
ValueError: If initial_value is not specified and the value for the given name is also not specified.
|
|
606
|
+
|
|
607
|
+
"""
|
|
401
608
|
|
|
402
609
|
_check_lb_ub(lower_bound, upper_bound, name)
|
|
403
610
|
value = self.fem.check_param_value(name)
|
|
@@ -405,7 +612,7 @@ class OptimizerBase(ABC):
|
|
|
405
612
|
if value is not None:
|
|
406
613
|
initial_value = value
|
|
407
614
|
else:
|
|
408
|
-
raise
|
|
615
|
+
raise ValueError('initial_value を指定してください.')
|
|
409
616
|
|
|
410
617
|
d = {
|
|
411
618
|
'name': name,
|
|
@@ -429,6 +636,23 @@ class OptimizerBase(ABC):
|
|
|
429
636
|
args: tuple or None = None,
|
|
430
637
|
kwargs: dict or None = None
|
|
431
638
|
):
|
|
639
|
+
"""Adds an objective to the optimization problem.
|
|
640
|
+
|
|
641
|
+
Args:
|
|
642
|
+
fun (callable): The objective function.
|
|
643
|
+
name (str or None, optional): The name of the objective. Defaults to None.
|
|
644
|
+
direction (str or float, optional): The optimization direction. Defaults to 'minimize'.
|
|
645
|
+
args (tuple or None, optional): Additional arguments for the objective function. Defaults to None.
|
|
646
|
+
kwargs (dict or None, optional): Additional keyword arguments for the objective function. Defaults to None.
|
|
647
|
+
|
|
648
|
+
Note:
|
|
649
|
+
If the FEMInterface is FemtetInterface, the 1st argument of fun should be Femtet (IPyDispatch) object.
|
|
650
|
+
|
|
651
|
+
Tip:
|
|
652
|
+
If name is None, name is a string with the prefix `"obj_"` followed by a sequential number.
|
|
653
|
+
|
|
654
|
+
"""
|
|
655
|
+
|
|
432
656
|
# 引数の処理
|
|
433
657
|
if args is None:
|
|
434
658
|
args = tuple()
|
|
@@ -444,6 +668,8 @@ class OptimizerBase(ABC):
|
|
|
444
668
|
is_existing = candidate in list(self.objectives.keys())
|
|
445
669
|
if not is_existing:
|
|
446
670
|
break
|
|
671
|
+
else:
|
|
672
|
+
i += 1
|
|
447
673
|
name = candidate
|
|
448
674
|
|
|
449
675
|
self.objectives[name] = Objective(fun, name, direction, args, kwargs)
|
|
@@ -459,6 +685,24 @@ class OptimizerBase(ABC):
|
|
|
459
685
|
args: tuple or None = None,
|
|
460
686
|
kwargs: dict or None = None,
|
|
461
687
|
):
|
|
688
|
+
"""Adds a constraint to the optimization problem.
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
fun (callable): The constraint function.
|
|
692
|
+
name (str or None, optional): The name of the constraint. Defaults to None.
|
|
693
|
+
lower_bound (float or Non, optional): The lower bound of the constraint. Defaults to None.
|
|
694
|
+
upper_bound (float or Non, optional): The upper bound of the constraint. Defaults to None.
|
|
695
|
+
strict (bool, optional): Flag indicating if it is a strict constraint. Defaults to True.
|
|
696
|
+
args (tuple or None, optional): Additional arguments for the constraint function. Defaults toNone.
|
|
697
|
+
|
|
698
|
+
Note:
|
|
699
|
+
If the FEMInterface is FemtetInterface, the 1st argument of fun should be Femtet (IPyDispatch) object.
|
|
700
|
+
|
|
701
|
+
Tip:
|
|
702
|
+
If name is None, name is a string with the prefix `"cns_"` followed by a sequential number.
|
|
703
|
+
|
|
704
|
+
"""
|
|
705
|
+
|
|
462
706
|
# 引数の処理
|
|
463
707
|
if args is None:
|
|
464
708
|
args = tuple()
|
|
@@ -474,6 +718,8 @@ class OptimizerBase(ABC):
|
|
|
474
718
|
is_existing = candidate in list(self.objectives.keys())
|
|
475
719
|
if not is_existing:
|
|
476
720
|
break
|
|
721
|
+
else:
|
|
722
|
+
i += 1
|
|
477
723
|
name = candidate
|
|
478
724
|
|
|
479
725
|
# strict constraint の場合、solve 前に評価したいので Gogh へのアクセスを禁ずる
|
|
@@ -486,9 +732,19 @@ class OptimizerBase(ABC):
|
|
|
486
732
|
|
|
487
733
|
self.constraints[name] = Constraint(fun, name, lower_bound, upper_bound, strict, args, kwargs)
|
|
488
734
|
|
|
735
|
+
def get_parameter(self, format='dict'):
|
|
736
|
+
"""Returns the parameters in the specified format.
|
|
737
|
+
|
|
738
|
+
Args:
|
|
739
|
+
format (str, optional): The desired format of the parameters. Can be 'df' (DataFrame), 'values', or 'dict'. Defaults to 'dict'.
|
|
489
740
|
|
|
741
|
+
Returns:
|
|
742
|
+
object: The parameters in the specified format.
|
|
490
743
|
|
|
491
|
-
|
|
744
|
+
Raises:
|
|
745
|
+
ValueError: If an invalid format is provided.
|
|
746
|
+
|
|
747
|
+
"""
|
|
492
748
|
if format == 'df':
|
|
493
749
|
return self.parameters
|
|
494
750
|
elif format == 'values' or format == 'value':
|
|
@@ -499,9 +755,19 @@ class OptimizerBase(ABC):
|
|
|
499
755
|
ret[row['name']] = row.value
|
|
500
756
|
return ret
|
|
501
757
|
else:
|
|
502
|
-
raise
|
|
758
|
+
raise ValueError('get_parameter() got invalid format: {format}')
|
|
503
759
|
|
|
504
760
|
def is_calculated(self, x):
|
|
761
|
+
"""Checks if the proposed x is the last calculated value.
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
x (iterable): The proposed x value.
|
|
765
|
+
|
|
766
|
+
Returns:
|
|
767
|
+
bool: True if the proposed x is the last calculated value, False otherwise.
|
|
768
|
+
|
|
769
|
+
"""
|
|
770
|
+
|
|
505
771
|
# 提案された x が最後に計算したものと一致していれば True
|
|
506
772
|
# ただし 1 回目の計算なら False
|
|
507
773
|
# ひとつでも違う 1回目の計算 期待
|
|
@@ -510,7 +776,6 @@ class OptimizerBase(ABC):
|
|
|
510
776
|
# True False False
|
|
511
777
|
# False False True
|
|
512
778
|
|
|
513
|
-
|
|
514
779
|
# 1 回目の計算
|
|
515
780
|
if len(self.history.data) == 0:
|
|
516
781
|
return False
|
|
@@ -526,6 +791,24 @@ class OptimizerBase(ABC):
|
|
|
526
791
|
|
|
527
792
|
|
|
528
793
|
def f(self, x, message=''):
|
|
794
|
+
"""Calculates the objective function values for the given parameter values.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
x (iterable): The parameter values.
|
|
798
|
+
message (str, optional): Additional information about the calculation. Defaults to ''.
|
|
799
|
+
|
|
800
|
+
Raises:
|
|
801
|
+
UserInterruption: If the calculation is interrupted.
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
list: The converted objective function values.
|
|
805
|
+
|
|
806
|
+
Note:
|
|
807
|
+
The return value is not the return value of a user-defined function,
|
|
808
|
+
but the converted value when reconsidering the optimization problem to be minimize problem.
|
|
809
|
+
See :func:`Objective.convert` for detail.
|
|
810
|
+
|
|
811
|
+
"""
|
|
529
812
|
|
|
530
813
|
x = np.array(x)
|
|
531
814
|
|
|
@@ -543,25 +826,106 @@ class OptimizerBase(ABC):
|
|
|
543
826
|
# fem のソルブ
|
|
544
827
|
self.fem.update(self.parameters)
|
|
545
828
|
|
|
829
|
+
# constants への参照を復帰させる
|
|
830
|
+
# parallel_process の中でこれを実行するとメインプロセスで restore されなくなるし、
|
|
831
|
+
# main の中 parallel_process の前にこれを実行すると unserializability に引っかかる
|
|
832
|
+
# メンバー変数の列挙
|
|
833
|
+
for attr_name in dir(self):
|
|
834
|
+
if attr_name.startswith('__'):
|
|
835
|
+
continue
|
|
836
|
+
# メンバー変数の取得
|
|
837
|
+
attr_value = getattr(self, attr_name)
|
|
838
|
+
# メンバー変数が辞書なら
|
|
839
|
+
if isinstance(attr_value, dict):
|
|
840
|
+
for _, value in attr_value.items():
|
|
841
|
+
# 辞書の value が Function なら
|
|
842
|
+
if isinstance(value, Function):
|
|
843
|
+
restore_constants_from_scapegoat(value)
|
|
844
|
+
|
|
546
845
|
# 計算
|
|
547
|
-
self.obj_values = [obj.calc(self.fem) for _, obj in self.objectives.items()]
|
|
548
|
-
self.cns_values = [cns.calc(self.fem) for _, cns in self.constraints.items()]
|
|
846
|
+
self.obj_values = [float(obj.calc(self.fem)) for _, obj in self.objectives.items()]
|
|
847
|
+
self.cns_values = [float(cns.calc(self.fem)) for _, cns in self.constraints.items()]
|
|
549
848
|
|
|
550
849
|
# 記録
|
|
850
|
+
if self.fem.subprocess_idx is not None:
|
|
851
|
+
message = message + f'; by subprocess{self.fem.subprocess_idx}'
|
|
551
852
|
self.history.record(self.parameters, self.objectives, self.constraints, self.obj_values, self.cns_values, message)
|
|
552
853
|
|
|
553
854
|
# minimize
|
|
554
|
-
return [obj.
|
|
855
|
+
return [obj.convert(v) for (_, obj), v in zip(self.objectives.items(), self.obj_values)]
|
|
555
856
|
|
|
556
857
|
|
|
557
858
|
@abstractmethod
|
|
558
|
-
def
|
|
859
|
+
def concrete_main(self, *args, **kwargs):
|
|
860
|
+
"""The main function for the concrete class to implement.
|
|
861
|
+
|
|
862
|
+
if ``n_trials`` >= 2, this method will be called by a parallel process.
|
|
863
|
+
|
|
864
|
+
Args:
|
|
865
|
+
*args: Variable length argument list.
|
|
866
|
+
**kwargs: Arbitrary keyword arguments.
|
|
867
|
+
|
|
868
|
+
"""
|
|
559
869
|
pass
|
|
560
870
|
|
|
561
|
-
def
|
|
871
|
+
def setup_concrete_main(self, *args, **kwargs):
|
|
872
|
+
"""Performs the setup for the concrete class.
|
|
873
|
+
|
|
874
|
+
Args:
|
|
875
|
+
*args: Variable length argument list.
|
|
876
|
+
**kwargs: Arbitrary keyword arguments.
|
|
877
|
+
|
|
878
|
+
"""
|
|
562
879
|
pass
|
|
563
880
|
|
|
881
|
+
def setup_monitor_server(self, host, port=None):
|
|
882
|
+
"""Sets up the monitor server with the specified host and port.
|
|
883
|
+
|
|
884
|
+
Args:
|
|
885
|
+
host (str): The hostname or IP address of the monitor server.
|
|
886
|
+
port (int or None, optional): The port number of the monitor server. If None, ``8080`` will be used. Defaults to None.
|
|
887
|
+
|
|
888
|
+
Tip:
|
|
889
|
+
If host is ``0.0.0.0``, a server will be set up
|
|
890
|
+
that is visible from the local network.
|
|
891
|
+
Start a browser on another machine
|
|
892
|
+
and type ``<ip_address_or_hostname>:<port>`` in the address bar.
|
|
893
|
+
|
|
894
|
+
However, please note that in this case,
|
|
895
|
+
it will be visible to all users on the local network.
|
|
896
|
+
|
|
897
|
+
"""
|
|
898
|
+
self.monitor_server_kwargs = dict(
|
|
899
|
+
host=host,
|
|
900
|
+
port=port
|
|
901
|
+
)
|
|
902
|
+
|
|
564
903
|
def main(self, n_trials=None, n_parallel=1, timeout=None, method='TPE', **setup_kwargs):
|
|
904
|
+
"""Runs the main optimization process.
|
|
905
|
+
|
|
906
|
+
Args:
|
|
907
|
+
n_trials (int or None): The number of trials. Defaults to None.
|
|
908
|
+
n_parallel (int): The number of parallel processes. Defaults to 1.
|
|
909
|
+
timeout (float or None): The maximum amount of time in seconds that each trial can run. Defaults to None.
|
|
910
|
+
method (str): The optimization method to use. Defaults to 'TPE'.
|
|
911
|
+
**setup_kwargs: Additional keyword arguments for setting up the optimization process.
|
|
912
|
+
|
|
913
|
+
Tip:
|
|
914
|
+
If setup_monitor_server() is not executed, a local server for monitoring will be started at localhost:8080.
|
|
915
|
+
|
|
916
|
+
Note:
|
|
917
|
+
If ``n_trials`` and ``timeout`` are both None, it will calculate repeatedly until interrupted by the user.
|
|
918
|
+
|
|
919
|
+
Note:
|
|
920
|
+
If ``n_parallel`` >= 2, depending on the end timing, ``n_trials`` may be exceeded by up to ``n_parallel-1`` times.
|
|
921
|
+
|
|
922
|
+
Note:
|
|
923
|
+
Currently, supported methods are 'TPE' and 'botorch'.
|
|
924
|
+
For detail, see https://optuna.readthedocs.io/en/stable/reference/samplers/index.html.
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
"""
|
|
928
|
+
|
|
565
929
|
# 共通引数
|
|
566
930
|
self.n_trials = n_trials
|
|
567
931
|
self.n_parallel = n_parallel
|
|
@@ -575,34 +939,37 @@ class OptimizerBase(ABC):
|
|
|
575
939
|
list(self.objectives.keys()),
|
|
576
940
|
list(self.constraints.keys()),
|
|
577
941
|
)
|
|
578
|
-
self.
|
|
942
|
+
self.setup_concrete_main(**setup_kwargs) # 具象クラス固有のメソッド
|
|
579
943
|
|
|
580
944
|
# 計算スレッドとそれを止めるためのイベント
|
|
581
|
-
t = Thread(target=self.
|
|
945
|
+
t = Thread(target=self.concrete_main)
|
|
582
946
|
t.start() # Exception が起きてもここでは検出できないし、メインスレッドは落ちない
|
|
947
|
+
|
|
948
|
+
# 計算開始
|
|
583
949
|
self.ipv.set_state('processing')
|
|
584
950
|
|
|
585
951
|
# モニタースレッド
|
|
586
952
|
self.monitor = Monitor(self)
|
|
587
|
-
|
|
588
|
-
|
|
953
|
+
self.monitor_thread = TerminatableThread(
|
|
954
|
+
target=self.monitor.start_server,
|
|
955
|
+
kwargs=self.monitor_server_kwargs
|
|
956
|
+
)
|
|
957
|
+
self.monitor_thread.start()
|
|
589
958
|
|
|
590
959
|
# 追加の計算プロセスが行う処理の定義
|
|
591
960
|
@ray.remote
|
|
592
|
-
def parallel_process(_subprocess_idx,
|
|
961
|
+
def parallel_process(_subprocess_idx, _subprocess_settings):
|
|
593
962
|
print('Start to re-initialize fem object.')
|
|
963
|
+
# プロセス化されたときに del した fem を restore する
|
|
594
964
|
self.set_fem(
|
|
595
965
|
subprocess_idx=_subprocess_idx,
|
|
596
|
-
|
|
597
|
-
pid=_parallel_setting[_subprocess_idx]
|
|
598
|
-
) # プロセス化されたときに monitor と fem を落としている
|
|
599
|
-
print('Start to setup parallel process.')
|
|
600
|
-
self.fem.parallel_setup(
|
|
601
|
-
_subprocess_idx,
|
|
966
|
+
subprocess_settings=_subprocess_settings
|
|
602
967
|
)
|
|
968
|
+
print('Start to setup parallel process.')
|
|
969
|
+
self.fem.parallel_setup()
|
|
603
970
|
print('Start parallel optimization.')
|
|
604
971
|
try:
|
|
605
|
-
self.
|
|
972
|
+
self.concrete_main(_subprocess_idx)
|
|
606
973
|
except UserInterruption:
|
|
607
974
|
pass
|
|
608
975
|
print('Finish parallel optimization.')
|
|
@@ -610,12 +977,12 @@ class OptimizerBase(ABC):
|
|
|
610
977
|
print('Finish parallel process.')
|
|
611
978
|
|
|
612
979
|
# 追加の計算プロセスを立てる前の前処理
|
|
613
|
-
|
|
980
|
+
subprocess_settings = self.fem.settings_before_parallel(self)
|
|
614
981
|
|
|
615
982
|
# 追加の計算プロセス
|
|
616
983
|
obj_refs = []
|
|
617
984
|
for subprocess_idx in range(self.n_parallel-1):
|
|
618
|
-
obj_ref = parallel_process.remote(subprocess_idx,
|
|
985
|
+
obj_ref = parallel_process.remote(subprocess_idx, subprocess_settings)
|
|
619
986
|
obj_refs.append(obj_ref)
|
|
620
987
|
|
|
621
988
|
start = time()
|
|
@@ -642,3 +1009,7 @@ class OptimizerBase(ABC):
|
|
|
642
1009
|
del obj
|
|
643
1010
|
|
|
644
1011
|
ray.shutdown()
|
|
1012
|
+
|
|
1013
|
+
def terminate_monitor(self):
|
|
1014
|
+
"""Forcefully terminates the monitor thread."""
|
|
1015
|
+
self.monitor_thread.force_terminate()
|