pyfemtet 0.5.4__py3-none-any.whl → 0.6.0__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/__init__.py +1 -1
- pyfemtet/_message/locales/ja/LC_MESSAGES/messages.po +1 -1
- pyfemtet/opt/__init__.py +3 -0
- pyfemtet/opt/_femopt.py +33 -14
- pyfemtet/opt/_femopt_core.py +99 -63
- pyfemtet/opt/interface/_femtet.py +15 -4
- pyfemtet/opt/optimizer/__init__.py +5 -1
- pyfemtet/opt/optimizer/_base.py +5 -13
- pyfemtet/opt/optimizer/_optuna/__init__.py +0 -0
- pyfemtet/opt/optimizer/_optuna/_botorch_patch/__init__.py +0 -0
- pyfemtet/opt/optimizer/{_optuna_botorchsampler_parameter_constraint_helper.py → _optuna/_botorch_patch/enable_nonlinear_constraint.py} +9 -126
- pyfemtet/opt/optimizer/{_optuna.py → _optuna/_optuna.py} +89 -18
- pyfemtet/opt/optimizer/_optuna/_pof_botorch.py +1833 -0
- pyfemtet/opt/samples/femprj_sample/constrained_pipe.py +2 -2
- pyfemtet/opt/samples/femprj_sample/her_ex40_parametric.py +2 -2
- {pyfemtet-0.5.4.dist-info → pyfemtet-0.6.0.dist-info}/METADATA +1 -1
- {pyfemtet-0.5.4.dist-info → pyfemtet-0.6.0.dist-info}/RECORD +20 -18
- pyfemtet/opt/samples/femprj_sample/.gitignore +0 -2
- {pyfemtet-0.5.4.dist-info → pyfemtet-0.6.0.dist-info}/LICENSE +0 -0
- {pyfemtet-0.5.4.dist-info → pyfemtet-0.6.0.dist-info}/WHEEL +0 -0
- {pyfemtet-0.5.4.dist-info → pyfemtet-0.6.0.dist-info}/entry_points.txt +0 -0
|
@@ -17,46 +17,12 @@ from pyfemtet.opt._femopt_core import Constraint
|
|
|
17
17
|
from pyfemtet.opt.optimizer import OptunaOptimizer, logger
|
|
18
18
|
from pyfemtet._message import Msg
|
|
19
19
|
|
|
20
|
-
from time import time
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
__all__ = ['do_patch']
|
|
24
20
|
|
|
25
21
|
|
|
26
22
|
BotorchConstraint = Callable[[Tensor], Tensor]
|
|
27
23
|
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
study: Study,
|
|
31
|
-
constraints: dict[str, Constraint],
|
|
32
|
-
opt: OptunaOptimizer,
|
|
33
|
-
):
|
|
34
|
-
"""BoTorchSampler の optimize_acqf をパッチし、パラメータ拘束が実施できるようにします。
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
study (Study): Optuna study. Use to calculate bounds.
|
|
38
|
-
constraints (dict[str, Constraint]): Constraints.
|
|
39
|
-
opt (OptunaOptimizer): OptunaOptimizer.
|
|
40
|
-
"""
|
|
41
|
-
import optuna_integration
|
|
42
|
-
|
|
43
|
-
from optuna_integration import version
|
|
44
|
-
if int(version.__version__.split('.')[0]) >= 4:
|
|
45
|
-
target_fun = optuna_integration.botorch.botorch.optimize_acqf
|
|
46
|
-
else:
|
|
47
|
-
target_fun = optuna_integration.botorch.optimize_acqf
|
|
48
|
-
|
|
49
|
-
new_fun: callable = OptimizeReplacedACQF(target_fun)
|
|
50
|
-
new_fun.set_constraints(list(constraints.values()))
|
|
51
|
-
new_fun.set_study(study)
|
|
52
|
-
new_fun.set_opt(opt)
|
|
53
|
-
|
|
54
|
-
if int(version.__version__.split('.')[0]) >= 4:
|
|
55
|
-
optuna_integration.botorch.botorch.optimize_acqf = new_fun
|
|
56
|
-
else:
|
|
57
|
-
optuna_integration.botorch.optimize_acqf = new_fun
|
|
58
|
-
|
|
59
|
-
|
|
25
|
+
# 拘束関数に pytorch の自動微分機能を適用するためのクラス
|
|
60
26
|
class GeneralFunctionWithForwardDifference(torch.autograd.Function):
|
|
61
27
|
"""自作関数を pytorch で自動微分するためのクラスです。
|
|
62
28
|
|
|
@@ -85,6 +51,8 @@ class GeneralFunctionWithForwardDifference(torch.autograd.Function):
|
|
|
85
51
|
return None, diff
|
|
86
52
|
|
|
87
53
|
|
|
54
|
+
# ユーザー定義関数 (pyfemtet.opt.Constraint) を受け取り、
|
|
55
|
+
# botorch で処理できる callable オブジェクトを作成するクラス
|
|
88
56
|
class ConvertedConstraint:
|
|
89
57
|
"""ユーザーが定義した Constraint を botorch で処理できる形式に変換します。
|
|
90
58
|
|
|
@@ -116,6 +84,7 @@ class ConvertedConstraint:
|
|
|
116
84
|
return Tensor([self._constraint.ub - c])
|
|
117
85
|
|
|
118
86
|
|
|
87
|
+
# list[pyfemtet.opt.Constraint] について、正規化された入力に対し、 feasible or not を返す関数
|
|
119
88
|
def is_feasible(study: Study, constraints: list[Constraint], norm_x: np.ndarray, opt: OptunaOptimizer) -> bool:
|
|
120
89
|
feasible = True
|
|
121
90
|
cns: Constraint
|
|
@@ -132,6 +101,7 @@ def is_feasible(study: Study, constraints: list[Constraint], norm_x: np.ndarray,
|
|
|
132
101
|
return feasible
|
|
133
102
|
|
|
134
103
|
|
|
104
|
+
# 正規化された入力を受けて pyfemtet.opt.Constraint を評価する関数
|
|
135
105
|
def evaluate_pyfemtet_cns(study: Study, cns: Constraint, norm_x: np.ndarray, opt: OptunaOptimizer) -> float:
|
|
136
106
|
"""Evaluate given constraint function by given NORMALIZED x.
|
|
137
107
|
|
|
@@ -163,6 +133,9 @@ def evaluate_pyfemtet_cns(study: Study, cns: Constraint, norm_x: np.ndarray, opt
|
|
|
163
133
|
return cns.calc(opt.fem)
|
|
164
134
|
|
|
165
135
|
|
|
136
|
+
# botorch の optimize_acqf で非線形拘束を使えるようにするクラス。以下を備える。
|
|
137
|
+
# - 渡すパラメータ nonlinear_constraints を作成する
|
|
138
|
+
# - gen_initial_conditions で feasible なものを返すラッパー関数
|
|
166
139
|
class NonlinearInequalityConstraints:
|
|
167
140
|
"""botorch の optimize_acqf に parameter constraints を設定するための引数を作成します。"""
|
|
168
141
|
|
|
@@ -191,7 +164,7 @@ class NonlinearInequalityConstraints:
|
|
|
191
164
|
feasible_q_list = []
|
|
192
165
|
for each_q in each_num_restarts:
|
|
193
166
|
norm_x: np.ndarray = each_q.numpy() # normalized parameters
|
|
194
|
-
|
|
167
|
+
|
|
195
168
|
if is_feasible(self._study, self._constraints, norm_x, self._opt):
|
|
196
169
|
feasible_q_list.append(each_q) # Keep only feasible rows
|
|
197
170
|
|
|
@@ -242,93 +215,3 @@ class NonlinearInequalityConstraints:
|
|
|
242
215
|
nonlinear_inequality_constraints=self._nonlinear_inequality_constraints,
|
|
243
216
|
ic_generator=self._generate_feasible_initial_conditions,
|
|
244
217
|
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
class AcquisitionFunctionWithPenalty(AcquisitionFunction):
|
|
248
|
-
"""獲得関数に infeasible 項を追加します。"""
|
|
249
|
-
|
|
250
|
-
# noinspection PyAttributeOutsideInit
|
|
251
|
-
def set_acqf(self, acqf):
|
|
252
|
-
self._acqf = acqf
|
|
253
|
-
|
|
254
|
-
# noinspection PyAttributeOutsideInit
|
|
255
|
-
def set_constraints(self, constraints: list[Constraint]):
|
|
256
|
-
self._constraints: list[Constraint] = constraints
|
|
257
|
-
|
|
258
|
-
# noinspection PyAttributeOutsideInit
|
|
259
|
-
def set_study(self, study: Study):
|
|
260
|
-
self._study: Study = study
|
|
261
|
-
|
|
262
|
-
# noinspection PyAttributeOutsideInit
|
|
263
|
-
def set_opt(self, opt: OptunaOptimizer):
|
|
264
|
-
self._opt = opt
|
|
265
|
-
|
|
266
|
-
def forward(self, X: "Tensor") -> "Tensor":
|
|
267
|
-
"""
|
|
268
|
-
|
|
269
|
-
Args:
|
|
270
|
-
X (Tensor): batch_size x 1 x n_params tensor.
|
|
271
|
-
|
|
272
|
-
Returns:
|
|
273
|
-
Tensor: batch_size tensor.
|
|
274
|
-
|
|
275
|
-
"""
|
|
276
|
-
base = self._acqf.forward(X)
|
|
277
|
-
|
|
278
|
-
norm_x: np.ndarray
|
|
279
|
-
for i, _norm_x in enumerate(X.detach().numpy()):
|
|
280
|
-
|
|
281
|
-
cns: Constraint
|
|
282
|
-
for cns in self._constraints:
|
|
283
|
-
feasible = is_feasible(self._study, [cns], _norm_x[0], self._opt)
|
|
284
|
-
if not feasible:
|
|
285
|
-
base[i] = base[i] * 0. # ペナルティ
|
|
286
|
-
break
|
|
287
|
-
|
|
288
|
-
return base
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
class OptimizeReplacedACQF(partial):
|
|
292
|
-
"""optimize_acqf をこの partial 関数に置き換えます。"""
|
|
293
|
-
|
|
294
|
-
# noinspection PyAttributeOutsideInit
|
|
295
|
-
def set_constraints(self, constraints: list[Constraint]):
|
|
296
|
-
self._constraints: list[Constraint] = constraints
|
|
297
|
-
|
|
298
|
-
# noinspection PyAttributeOutsideInit
|
|
299
|
-
def set_study(self, study: Study):
|
|
300
|
-
self._study: Study = study
|
|
301
|
-
|
|
302
|
-
# noinspection PyAttributeOutsideInit
|
|
303
|
-
def set_opt(self, opt: OptunaOptimizer):
|
|
304
|
-
self._opt = opt
|
|
305
|
-
|
|
306
|
-
def __call__(self, *args, **kwargs):
|
|
307
|
-
"""置き換え先の関数の処理内容です。
|
|
308
|
-
|
|
309
|
-
kwargs を横入りして追記することで拘束を実現します。
|
|
310
|
-
"""
|
|
311
|
-
|
|
312
|
-
logger.info(Msg.START_CANDIDATE_WITH_PARAMETER_CONSTRAINT)
|
|
313
|
-
|
|
314
|
-
# FEM の更新が必要な場合、時間がかかることが多いので警告を出す
|
|
315
|
-
if any([cns.using_fem for cns in self._constraints]):
|
|
316
|
-
logger.warning(Msg.WARN_UPDATE_FEM_PARAMETER_TOOK_A_LONG_TIME)
|
|
317
|
-
|
|
318
|
-
# 獲得関数に infeasible な場合のペナルティ項を追加します。
|
|
319
|
-
acqf = kwargs['acq_function']
|
|
320
|
-
new_acqf = AcquisitionFunctionWithPenalty(...)
|
|
321
|
-
new_acqf.set_acqf(acqf)
|
|
322
|
-
new_acqf.set_constraints(self._constraints)
|
|
323
|
-
new_acqf.set_study(self._study)
|
|
324
|
-
new_acqf.set_opt(self._opt)
|
|
325
|
-
kwargs['acq_function'] = new_acqf
|
|
326
|
-
|
|
327
|
-
# optimize_acqf の探索に parameter constraints を追加します。
|
|
328
|
-
nlic = NonlinearInequalityConstraints(self._study, self._constraints, self._opt)
|
|
329
|
-
kwargs.update(nlic.create_kwargs())
|
|
330
|
-
|
|
331
|
-
# replace other arguments
|
|
332
|
-
...
|
|
333
|
-
|
|
334
|
-
return super().__call__(*args, **kwargs)
|
|
@@ -3,15 +3,15 @@ from typing import Iterable
|
|
|
3
3
|
|
|
4
4
|
# built-in
|
|
5
5
|
import os
|
|
6
|
+
import inspect
|
|
6
7
|
|
|
7
8
|
# 3rd-party
|
|
8
|
-
import numpy as np
|
|
9
9
|
import optuna
|
|
10
10
|
from optuna.trial import TrialState
|
|
11
11
|
from optuna.study import MaxTrialsCallback
|
|
12
12
|
|
|
13
13
|
# pyfemtet relative
|
|
14
|
-
from pyfemtet.opt._femopt_core import OptimizationStatus, generate_lhs
|
|
14
|
+
from pyfemtet.opt._femopt_core import OptimizationStatus, generate_lhs
|
|
15
15
|
from pyfemtet.opt.optimizer import AbstractOptimizer, logger, OptimizationMethodChecker
|
|
16
16
|
from pyfemtet.core import MeshError, ModelError, SolveError
|
|
17
17
|
from pyfemtet._message import Msg
|
|
@@ -86,17 +86,24 @@ class OptunaOptimizer(AbstractOptimizer):
|
|
|
86
86
|
self.additional_initial_parameter = []
|
|
87
87
|
self.additional_initial_methods = add_init_method if hasattr(add_init_method, '__iter__') else [add_init_method]
|
|
88
88
|
self.method_checker = OptunaMethodChecker(self)
|
|
89
|
-
self._do_monkey_patch = False
|
|
90
89
|
|
|
91
90
|
def _objective(self, trial):
|
|
92
91
|
|
|
92
|
+
logger.info('')
|
|
93
|
+
if self._retry_counter == 0:
|
|
94
|
+
logger.info(f'===== trial {1 + len(self.history.get_df())} start =====')
|
|
95
|
+
else:
|
|
96
|
+
logger.info(f'===== trial {1 + len(self.history.get_df())} (retry {self._retry_counter}) start =====')
|
|
97
|
+
|
|
93
98
|
# 中断の確認 (FAIL loop に陥る対策)
|
|
94
99
|
if self.entire_status.get() == OptimizationStatus.INTERRUPTING:
|
|
95
100
|
self.worker_status.set(OptimizationStatus.INTERRUPTING)
|
|
96
101
|
trial.study.stop() # 現在実行中の trial を最後にする
|
|
102
|
+
self._retry_counter = 0
|
|
97
103
|
return None # set TrialState FAIL
|
|
98
104
|
|
|
99
105
|
# candidate x and update parameters
|
|
106
|
+
logger.info('Searching new parameter set...')
|
|
100
107
|
for prm in self.variables.get_variables(format='raw', filter_parameter=True):
|
|
101
108
|
value = trial.suggest_float(
|
|
102
109
|
name=prm.name,
|
|
@@ -126,9 +133,11 @@ class OptunaOptimizer(AbstractOptimizer):
|
|
|
126
133
|
if cns.ub is not None:
|
|
127
134
|
feasible = feasible and (cns.ub >= cns_value)
|
|
128
135
|
if not feasible:
|
|
136
|
+
logger.info('----- Out of constraint! -----')
|
|
129
137
|
logger.info(Msg.INFO_INFEASIBLE)
|
|
130
138
|
logger.info(f'Constraint: {cns.name}')
|
|
131
139
|
logger.info(self.variables.get_variables('dict', filter_parameter=True))
|
|
140
|
+
self._retry_counter += 1
|
|
132
141
|
raise optuna.TrialPruned() # set TrialState PRUNED because FAIL causes similar candidate loop.
|
|
133
142
|
|
|
134
143
|
# 計算
|
|
@@ -142,6 +151,15 @@ class OptunaOptimizer(AbstractOptimizer):
|
|
|
142
151
|
trial.study.stop() # 現在実行中の trial を最後にする
|
|
143
152
|
return None # set TrialState FAIL
|
|
144
153
|
|
|
154
|
+
logger.warning('----- Infeasible! -----')
|
|
155
|
+
logger.warning(Msg.INFO_INFEASIBLE)
|
|
156
|
+
logger.warning(f'Hidden Constraint ({type(e).__name__})')
|
|
157
|
+
logger.warning(self.variables.get_variables('dict', filter_parameter=True))
|
|
158
|
+
logger.warning('Please consider to determine the cause'
|
|
159
|
+
'of the above error and modify the model'
|
|
160
|
+
'or analysis.')
|
|
161
|
+
|
|
162
|
+
self._retry_counter += 1
|
|
145
163
|
raise optuna.TrialPruned() # set TrialState PRUNED because FAIL causes similar candidate loop.
|
|
146
164
|
|
|
147
165
|
# 拘束 attr の更新
|
|
@@ -159,9 +177,11 @@ class OptunaOptimizer(AbstractOptimizer):
|
|
|
159
177
|
if self.entire_status.get() == OptimizationStatus.INTERRUPTING:
|
|
160
178
|
self.worker_status.set(OptimizationStatus.INTERRUPTING)
|
|
161
179
|
trial.study.stop() # 現在実行中の trial を最後にする
|
|
180
|
+
self._retry_counter = 0
|
|
162
181
|
return None # set TrialState FAIL
|
|
163
182
|
|
|
164
183
|
# 結果
|
|
184
|
+
self._retry_counter = 0
|
|
165
185
|
return tuple(_y)
|
|
166
186
|
|
|
167
187
|
def _constraint(self, trial):
|
|
@@ -295,10 +315,20 @@ class OptunaOptimizer(AbstractOptimizer):
|
|
|
295
315
|
self.sampler_kwargs.update(
|
|
296
316
|
seed=seed
|
|
297
317
|
)
|
|
318
|
+
parameters = inspect.signature(self.sampler_class.__init__).parameters
|
|
319
|
+
sampler_kwargs = dict()
|
|
320
|
+
for k, v in self.sampler_kwargs.items():
|
|
321
|
+
if k in parameters.keys():
|
|
322
|
+
sampler_kwargs.update({k: v})
|
|
298
323
|
sampler = self.sampler_class(
|
|
299
|
-
**
|
|
324
|
+
**sampler_kwargs
|
|
300
325
|
)
|
|
301
326
|
|
|
327
|
+
from pyfemtet.opt.optimizer._optuna._pof_botorch import PoFBoTorchSampler
|
|
328
|
+
if isinstance(sampler, PoFBoTorchSampler):
|
|
329
|
+
sampler._pyfemtet_constraints = [cns for cns in self.constraints.values() if cns.strict]
|
|
330
|
+
sampler._pyfemtet_optimizer = self
|
|
331
|
+
|
|
302
332
|
# load study
|
|
303
333
|
study = optuna.load_study(
|
|
304
334
|
study_name=self.study_name,
|
|
@@ -306,21 +336,62 @@ class OptunaOptimizer(AbstractOptimizer):
|
|
|
306
336
|
sampler=sampler,
|
|
307
337
|
)
|
|
308
338
|
|
|
309
|
-
#
|
|
310
|
-
|
|
311
|
-
|
|
339
|
+
# 一時的な実装。
|
|
340
|
+
# TPESampler の場合、リスタート時などの場合で、
|
|
341
|
+
# Pruned が多いとエラーを起こす挙動があるので、
|
|
342
|
+
# Pruned な Trial は remove したい。
|
|
343
|
+
# study.remove_trial がないので、一度ダミー
|
|
344
|
+
# study を作成して最適化終了後に結果をコピーする。
|
|
345
|
+
if isinstance(sampler, optuna.samplers.TPESampler):
|
|
346
|
+
tmp_db = f"tmp{self.subprocess_idx}.db"
|
|
347
|
+
if os.path.exists(tmp_db):
|
|
348
|
+
os.remove(tmp_db)
|
|
349
|
+
|
|
350
|
+
_study = optuna.create_study(
|
|
351
|
+
study_name="tmp",
|
|
352
|
+
storage=f"sqlite:///{tmp_db}",
|
|
353
|
+
sampler=sampler,
|
|
354
|
+
directions=['minimize']*len(self.objectives),
|
|
355
|
+
load_if_exists=False,
|
|
356
|
+
)
|
|
312
357
|
|
|
313
|
-
|
|
358
|
+
# 既存の trials のうち COMPLETE のものを取得
|
|
359
|
+
existing_trials = study.get_trials(states=(optuna.trial.TrialState.COMPLETE,))
|
|
360
|
+
_study.add_trials(existing_trials)
|
|
314
361
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
self.
|
|
318
|
-
self
|
|
362
|
+
# run
|
|
363
|
+
_study.optimize(
|
|
364
|
+
self._objective,
|
|
365
|
+
timeout=self.timeout,
|
|
366
|
+
callbacks=self.optimize_callbacks,
|
|
319
367
|
)
|
|
320
368
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
369
|
+
# trial.number と trial_id は _study への add_trials 時に
|
|
370
|
+
# 振りなおされるため重複したものをフィルタアウトするために
|
|
371
|
+
# datetime_start を利用。
|
|
372
|
+
added_trials = []
|
|
373
|
+
for _trial in _study.get_trials():
|
|
374
|
+
if _trial.datetime_start not in [t.datetime_start for t in existing_trials]:
|
|
375
|
+
added_trials.append(_trial)
|
|
376
|
+
|
|
377
|
+
# Write back added trials to the existing study.
|
|
378
|
+
study.add_trials(added_trials)
|
|
379
|
+
|
|
380
|
+
# clean up
|
|
381
|
+
from optuna.storages import get_storage
|
|
382
|
+
storage = get_storage(f"sqlite:///{tmp_db}")
|
|
383
|
+
storage.remove_session()
|
|
384
|
+
del _study
|
|
385
|
+
del storage
|
|
386
|
+
import gc
|
|
387
|
+
gc.collect()
|
|
388
|
+
if os.path.exists(tmp_db):
|
|
389
|
+
os.remove(tmp_db)
|
|
390
|
+
|
|
391
|
+
else:
|
|
392
|
+
# run
|
|
393
|
+
study.optimize(
|
|
394
|
+
self._objective,
|
|
395
|
+
timeout=self.timeout,
|
|
396
|
+
callbacks=self.optimize_callbacks,
|
|
397
|
+
)
|