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.

Files changed (33) hide show
  1. pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.femprj +0 -0
  2. pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.prt +0 -0
  3. pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.py +69 -32
  4. pyfemtet/FemtetPJTSample/gau_ex08_parametric.femprj +0 -0
  5. pyfemtet/FemtetPJTSample/gau_ex08_parametric.py +37 -25
  6. pyfemtet/FemtetPJTSample/her_ex40_parametric.py +57 -35
  7. pyfemtet/FemtetPJTSample/wat_ex14_parallel_parametric.py +62 -0
  8. pyfemtet/FemtetPJTSample/wat_ex14_parametric.femprj +0 -0
  9. pyfemtet/FemtetPJTSample/wat_ex14_parametric.py +61 -0
  10. pyfemtet/__init__.py +1 -1
  11. pyfemtet/opt/_FemtetWithNX/update_model.py +6 -2
  12. pyfemtet/opt/__init__.py +1 -1
  13. pyfemtet/opt/base.py +457 -86
  14. pyfemtet/opt/core.py +77 -17
  15. pyfemtet/opt/interface.py +217 -137
  16. pyfemtet/opt/monitor.py +181 -98
  17. pyfemtet/opt/{_optuna.py → optimizer.py} +70 -30
  18. pyfemtet/tools/DispatchUtils.py +46 -44
  19. {pyfemtet-0.1.12.dist-info → pyfemtet-0.2.1.dist-info}/LICENSE +1 -1
  20. pyfemtet-0.2.1.dist-info/METADATA +42 -0
  21. pyfemtet-0.2.1.dist-info/RECORD +31 -0
  22. pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01 - original.x_t +0 -359
  23. pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.x_t +0 -359
  24. pyfemtet/FemtetPJTSample/fem4 = Femtet(femprj_path=None, model_name=None, connect_method='catch').femprj +0 -0
  25. pyfemtet/FemtetPJTSample/gal_ex11_parametric.femprj +0 -0
  26. pyfemtet/FemtetPJTSample/gal_ex11_parametric.py +0 -54
  27. pyfemtet/FemtetPJTSample/pas_ex1_parametric.femprj +0 -0
  28. pyfemtet/FemtetPJTSample/pas_ex1_parametric.py +0 -66
  29. pyfemtet/FemtetPJTSample/pas_ex1_parametric2.py +0 -68
  30. pyfemtet/tools/FemtetClassConst.py +0 -9
  31. pyfemtet-0.1.12.dist-info/METADATA +0 -205
  32. pyfemtet-0.1.12.dist-info/RECORD +0 -37
  33. {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 .core import InterprocessVariables, UserInterruption
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
- 定義域を負領域に拡張したlog関数です。
24
- 多目的最適化における目的関数同士のスケール差により
25
- 意図しない傾向が生ずることのの軽減策として
26
- 内部でsymlog処理を行います。
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 Exception(message)
59
+ raise ValueError(message)
51
60
  else:
52
- raise Exception(message)
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 Exception(message)
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 _convert(self, value: float):
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
- def __init__(self, history_path, ipv):
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.ipv = ipv
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.data.columns) > 0:
315
+ if len(self.actor_data.columns) > 0:
198
316
  # 読み込んだ columns が生成した columns と違っていればエラー
199
317
  try:
200
- if self.data.columns != self._data_columns:
201
- raise Exception(f'読み込んだ history と問題の設定が異なります. \n\n読み込まれた設定:\n{list(self.data.columns)}\n\n現在の設定:\n{self._data_columns}')
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.data.columns)}\n\n現在の設定:\n{self._data_columns}')
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(0.) # dummy hypervolume
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.ipv.append_history(row)
228
- data = self.ipv.get_history()
229
- self.data = pd.DataFrame(
230
- data,
231
- columns=self._data_columns,
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
- self.data['trial'] = np.arange(len(self.data))
236
- self._calc_non_domi(objectives)
237
- self._calc_hypervolume(objectives)
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.data.to_csv(self.path, index=None)
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.data[self.obj_names].copy()
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._convert)
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
- self.data['non_domi'] = non_domi
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.data['non_domi']
273
- pdf = self.data[idx]
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 np.nan
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._convert(pareto_set[j, i])
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
- # (H+m-1)C(m-1) <= n <= (m-1)C(H+m) になるような H を探す
289
- H = 0
290
- while True:
291
- left = math.comb(H + m - 1, m - 1)
292
- right = math.comb(H + m, m - 1)
293
- if left <= n <= right:
294
- break
295
- else:
296
- H += 1
297
- # H==0 なら r は最大の値
298
- if H == 0:
299
- r = 2
300
- else:
301
- # r を計算
302
- r = 1 + 1. / H
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.data
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_%M.csv')
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, self.ipv)
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 Exception('initial_value を指定してください.')
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
- def get_parameter(self, format='dict'):
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 Exception('get_parameter() got invalid format: {format}')
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._convert(v) for (_, obj), v in zip(self.objectives.items(), self.obj_values)]
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 _main(self, *args, **kwargs):
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 _setup_main(self, *args, **kwargs):
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._setup_main(**setup_kwargs) # 具象クラス固有のメソッド
942
+ self.setup_concrete_main(**setup_kwargs) # 具象クラス固有のメソッド
579
943
 
580
944
  # 計算スレッドとそれを止めるためのイベント
581
- t = Thread(target=self._main)
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
- tm = Thread(target=self.monitor.start_server)
588
- tm.start()
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, _parallel_setting):
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
- ipv=self.ipv,
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._main(_subprocess_idx)
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
- parallel_setting = self.fem.before_parallel_setup(self)
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, parallel_setting)
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()