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.

@@ -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
- def do_patch(
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, Constraint
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
- **self.sampler_kwargs
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
- # monkey patch
310
- if self._do_monkey_patch:
311
- assert isinstance(sampler, optuna.integration.BoTorchSampler), Msg.ERR_PARAMETER_CONSTRAINT_ONLY_BOTORCH
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
- from pyfemtet.opt.optimizer._optuna_botorchsampler_parameter_constraint_helper import do_patch
358
+ # 既存の trials のうち COMPLETE のものを取得
359
+ existing_trials = study.get_trials(states=(optuna.trial.TrialState.COMPLETE,))
360
+ _study.add_trials(existing_trials)
314
361
 
315
- do_patch(
316
- study,
317
- self.constraints,
318
- self
362
+ # run
363
+ _study.optimize(
364
+ self._objective,
365
+ timeout=self.timeout,
366
+ callbacks=self.optimize_callbacks,
319
367
  )
320
368
 
321
- # run
322
- study.optimize(
323
- self._objective,
324
- timeout=self.timeout,
325
- callbacks=self.optimize_callbacks,
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
+ )