pyfemtet 0.3.12__py3-none-any.whl → 0.4.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 (35) hide show
  1. pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.py +1 -1
  2. pyfemtet/FemtetPJTSample/Sldworks_ex01/Sldworks_ex01.py +1 -1
  3. pyfemtet/FemtetPJTSample/gau_ex08_parametric.py +1 -1
  4. pyfemtet/FemtetPJTSample/her_ex40_parametric.femprj +0 -0
  5. pyfemtet/FemtetPJTSample/her_ex40_parametric.py +1 -1
  6. pyfemtet/FemtetPJTSample/wat_ex14_parallel_parametric.py +1 -1
  7. pyfemtet/FemtetPJTSample/wat_ex14_parametric.femprj +0 -0
  8. pyfemtet/FemtetPJTSample/wat_ex14_parametric.py +1 -1
  9. pyfemtet/__init__.py +1 -1
  10. pyfemtet/core.py +14 -0
  11. pyfemtet/dispatch_extensions.py +5 -0
  12. pyfemtet/opt/__init__.py +22 -2
  13. pyfemtet/opt/_femopt.py +544 -0
  14. pyfemtet/opt/_femopt_core.py +730 -0
  15. pyfemtet/opt/interface/__init__.py +15 -0
  16. pyfemtet/opt/interface/_base.py +71 -0
  17. pyfemtet/opt/{interface.py → interface/_femtet.py} +120 -407
  18. pyfemtet/opt/interface/_femtet_with_nx/__init__.py +3 -0
  19. pyfemtet/opt/interface/_femtet_with_nx/_interface.py +128 -0
  20. pyfemtet/opt/interface/_femtet_with_sldworks.py +174 -0
  21. pyfemtet/opt/opt/__init__.py +8 -0
  22. pyfemtet/opt/opt/_base.py +202 -0
  23. pyfemtet/opt/opt/_optuna.py +240 -0
  24. pyfemtet/opt/visualization/__init__.py +7 -0
  25. pyfemtet/opt/visualization/_graphs.py +222 -0
  26. pyfemtet/opt/visualization/_monitor.py +1149 -0
  27. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.1.dist-info}/METADATA +4 -4
  28. pyfemtet-0.4.1.dist-info/RECORD +38 -0
  29. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.1.dist-info}/WHEEL +1 -1
  30. pyfemtet-0.4.1.dist-info/entry_points.txt +3 -0
  31. pyfemtet/opt/base.py +0 -1490
  32. pyfemtet/opt/monitor.py +0 -474
  33. pyfemtet-0.3.12.dist-info/RECORD +0 -26
  34. /pyfemtet/opt/{_FemtetWithNX → interface/_femtet_with_nx}/update_model.py +0 -0
  35. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,730 @@
1
+ # typing
2
+ from typing import List
3
+
4
+ # built-in
5
+ import os
6
+ import datetime
7
+ import inspect
8
+ import ast
9
+ import csv
10
+
11
+ # 3rd-party
12
+ import numpy as np
13
+ import pandas as pd
14
+ from scipy.stats.qmc import LatinHypercube
15
+ from optuna._hypervolume import WFG
16
+ from dask.distributed import Lock
17
+
18
+ # win32com
19
+ from win32com.client import constants, Constants
20
+
21
+ # pyfemtet relative
22
+ from pyfemtet.opt.interface import FEMInterface, FemtetInterface
23
+
24
+ # logger
25
+ import logging
26
+ from pyfemtet.logger import get_logger
27
+ logger = get_logger('femopt')
28
+ logger.setLevel(logging.INFO)
29
+
30
+
31
+ __all__ = [
32
+ 'generate_lhs',
33
+ '_check_bound',
34
+ '_is_access_gogh',
35
+ 'is_feasible',
36
+ 'Objective',
37
+ 'Constraint',
38
+ 'History',
39
+ 'OptimizationStatus',
40
+ 'logger',
41
+ ]
42
+
43
+
44
+ def generate_lhs(bounds: List[List[float]], seed: int or None = None) -> np.ndarray:
45
+ """Latin Hypercube Sampling from given design parameter bounds.
46
+
47
+ If the number of parameters is d,
48
+ sampler returns (N, d) shape ndarray.
49
+ N equals p**2, p is the minimum prime number over d.
50
+ For example, when d=3, then p=5 and N=25.
51
+
52
+ Args:
53
+ bounds (list[list[float]]): List of [lower_bound, upper_bound] of parameters.
54
+ seed (int or None, optional): Random seed. Defaults to None.
55
+
56
+ Returns:
57
+ np.ndarray: (N, d) shape ndarray.
58
+ """
59
+
60
+ d = len(bounds)
61
+
62
+ sampler = LatinHypercube(
63
+ d,
64
+ scramble=False,
65
+ strength=2,
66
+ # optimization='lloyd',
67
+ optimization='random-cd',
68
+ seed=seed,
69
+ )
70
+
71
+ LIMIT = 100
72
+
73
+ def is_prime(p):
74
+ for j in range(2, p):
75
+ if p % j == 0:
76
+ return False
77
+ return True
78
+
79
+ def get_prime(_minimum):
80
+ for p in range(_minimum, LIMIT):
81
+ if is_prime(p):
82
+ return p
83
+
84
+ n = get_prime(d + 1) ** 2
85
+ data = sampler.random(n) # [0,1)
86
+
87
+ for i, (data_range, datum) in enumerate(zip(bounds, data.T)):
88
+ minimum, maximum = data_range
89
+ band = maximum - minimum
90
+ converted_datum = datum * band + minimum
91
+ data[:, i] = converted_datum
92
+
93
+ return data # data.shape = (N, d)
94
+
95
+
96
+ def symlog(x: float or np.ndarray):
97
+ """Log function whose domain is extended to the negative region.
98
+
99
+ Symlog processing is performed internally as a measure to reduce
100
+ unintended trends caused by scale differences
101
+ between objective functions in multi-objective optimization.
102
+
103
+ Args:
104
+ x (float or np.ndarray)
105
+
106
+ Returns:
107
+ float
108
+ """
109
+
110
+ if isinstance(x, np.ndarray):
111
+ ret = np.zeros(x.shape)
112
+ idx = np.where(x >= 0)
113
+ ret[idx] = np.log10(x[idx] + 1)
114
+ idx = np.where(x < 0)
115
+ ret[idx] = -np.log10(1 - x[idx])
116
+ else:
117
+ if x >= 0:
118
+ ret = np.log10(x + 1)
119
+ else:
120
+ ret = -np.log10(1 - x)
121
+
122
+ return ret
123
+
124
+
125
+ def _check_bound(lb, ub, name=None):
126
+ message = f'下限{lb} > 上限{ub} です.'
127
+ if name is not None:
128
+ message = f'{name}に対して' + message
129
+ if (lb is not None) and (ub is not None):
130
+ if lb > ub:
131
+ raise ValueError(message)
132
+
133
+
134
+ def _is_access_gogh(fun):
135
+
136
+ # 関数fのソースコードを取得
137
+ source = inspect.getsource(fun)
138
+
139
+ # ソースコードを抽象構文木(AST)に変換
140
+ tree = ast.parse(source)
141
+
142
+ # 関数定義を見つける
143
+ for node in ast.walk(tree):
144
+ if isinstance(node, ast.FunctionDef):
145
+ # 関数の第一引数の名前を取得
146
+ first_arg_name = node.args.args[0].arg
147
+
148
+ # 関数内の全ての属性アクセスをチェック
149
+ for sub_node in ast.walk(node):
150
+ if isinstance(sub_node, ast.Attribute):
151
+ # 第一引数に対して 'Gogh' へのアクセスがあるかチェック
152
+ if (
153
+ isinstance(sub_node.value, ast.Name)
154
+ and sub_node.value.id == first_arg_name
155
+ and sub_node.attr == 'Gogh'
156
+ ):
157
+ return True
158
+ # ここまできてもなければアクセスしてない
159
+ return False
160
+
161
+
162
+ def is_feasible(value, lb, ub):
163
+ """
164
+ Check if a value is within the specified lower bound and upper bound.
165
+
166
+ Args:
167
+ value (numeric): The value to check.
168
+ lb (optional, numeric): The lower bound. If not specified, there is no lower bound.
169
+ ub (optional, numeric): The upper bound. If not specified, there is no upper bound.
170
+
171
+ Returns:
172
+ bool: True if the value satisfies the bounds; False otherwise.
173
+ """
174
+ if lb is None and ub is not None:
175
+ return value < ub
176
+ elif lb is not None and ub is None:
177
+ return lb < value
178
+ elif lb is not None and ub is not None:
179
+ return lb < value < ub
180
+ else:
181
+ return True
182
+
183
+
184
+ class _Scapegoat:
185
+ """Helper class for parallelize Femtet."""
186
+ # constants を含む関数を並列化するために
187
+ # メイン処理で一時的に constants への参照を
188
+ # このオブジェクトにして、後で restore する
189
+ def __init__(self, ignore=False):
190
+ self._ignore_when_restore_constants = ignore
191
+
192
+
193
+ class Function:
194
+ """Base class for Objective and Constraint."""
195
+
196
+ def __init__(self, fun, name, args, kwargs):
197
+
198
+ # serializable でない COM 定数を parallelize するため
199
+ # COM 定数を一度 _Scapegoat 型のオブジェクトにする
200
+ for varname in fun.__globals__:
201
+ if isinstance(fun.__globals__[varname], Constants):
202
+ fun.__globals__[varname] = _Scapegoat()
203
+
204
+ self.fun = fun
205
+ self.name = name
206
+ self.args = args
207
+ self.kwargs = kwargs
208
+
209
+ def calc(self, fem: FEMInterface):
210
+ """Execute user-defined fun.
211
+
212
+ Args:
213
+ fem (FEMInterface)
214
+
215
+ Returns:
216
+ float
217
+ """
218
+ args = self.args
219
+ # Femtet 特有の処理
220
+ if isinstance(fem, FemtetInterface):
221
+ args = (fem.Femtet, *args)
222
+ return float(self.fun(*args, **self.kwargs))
223
+
224
+ def _restore_constants(self):
225
+ """Helper function for parallelize Femtet."""
226
+ fun = self.fun
227
+ for varname in fun.__globals__:
228
+ if isinstance(fun.__globals__[varname], _Scapegoat):
229
+ if not fun.__globals__[varname]._ignore_when_restore_constants:
230
+ fun.__globals__[varname] = constants
231
+
232
+
233
+ class Objective(Function):
234
+ """Class for registering user-defined objective function."""
235
+
236
+ default_name = 'obj'
237
+
238
+ def __init__(self, fun, name, direction, args, kwargs):
239
+ """Initializes an Objective instance.
240
+
241
+ Args:
242
+ fun: The user-defined objective function.
243
+ name (str): The name of the objective function.
244
+ direction (str or float or int): The direction of optimization.
245
+ args: Additional arguments for the objective function.
246
+ kwargs: Additional keyword arguments for the objective function.
247
+
248
+ Raises:
249
+ ValueError: If the direction is not valid.
250
+
251
+ Note:
252
+ If FEMOpt.fem is a instance of FemtetInterface or its subclass,
253
+ the 1st argument of fun is set to fem automatically.
254
+
255
+
256
+ """
257
+ self._check_direction(direction)
258
+ self.direction = direction
259
+ super().__init__(fun, name, args, kwargs)
260
+
261
+ def convert(self, value: float):
262
+ """Converts an evaluation value to the value of objective function based on the specified direction.
263
+
264
+ When direction is `'minimize'`, ``value`` is calculated.
265
+ When direction is `'maximize'`, ``-value`` is calculated.
266
+ When direction is float, ``abs(value - direction)`` is calculated.
267
+ Finally, the calculated value is passed to the symlog function and returns it.
268
+
269
+ ``value`` is the return value of the user-defined function.
270
+
271
+ Args:
272
+ value (float): The evaluation value to be converted.
273
+
274
+ Returns:
275
+ float: The converted objective value.
276
+
277
+ """
278
+
279
+ # 評価関数(direction 任意)を目的関数(minimize, symlog)に変換する
280
+ ret = value
281
+ if isinstance(self.direction, float) or isinstance(self.direction, int):
282
+ ret = abs(value - self.direction)
283
+ elif self.direction == 'minimize':
284
+ ret = value
285
+ elif self.direction == 'maximize':
286
+ ret = -value
287
+
288
+ ret = symlog(ret)
289
+
290
+ return float(ret)
291
+
292
+ def _check_direction(self, direction):
293
+ message = '評価関数の direction は "minimize", "maximize", 又は数値でなければなりません.'
294
+ message += f'与えられた値は {direction} です.'
295
+ if isinstance(direction, float) or isinstance(direction, int):
296
+ pass
297
+ elif isinstance(direction, str):
298
+ if (direction != 'minimize') and (direction != 'maximize'):
299
+ raise ValueError(message)
300
+ else:
301
+ raise ValueError(message)
302
+
303
+
304
+ class Constraint(Function):
305
+ """Class for registering user-defined constraint function."""
306
+
307
+ default_name = 'cns'
308
+
309
+ def __init__(self, fun, name, lb, ub, strict, args, kwargs):
310
+ """Initializes a Constraint instance.
311
+
312
+ Args:
313
+ fun: The user-defined constraint function.
314
+ name (str): The name of the constraint function.
315
+ lb: The lower bound of the constraint.
316
+ ub: The upper bound of the constraint.
317
+ strict (bool): Whether to enforce strict inequality for the bounds.
318
+ args: Additional arguments for the constraint function.
319
+ kwargs: Additional keyword arguments for the constraint function.
320
+
321
+ Raises:
322
+ ValueError: If the lower bound is greater than or equal to the upper bound.
323
+
324
+ """
325
+
326
+ _check_bound(lb, ub)
327
+ self.lb = lb
328
+ self.ub = ub
329
+ self.strict = strict
330
+ super().__init__(fun, name, args, kwargs)
331
+
332
+
333
+ class _HistoryDfCore:
334
+ """Class for managing a DataFrame object in a distributed manner."""
335
+
336
+ def __init__(self):
337
+ self.df = pd.DataFrame()
338
+
339
+ def set_df(self, df):
340
+ self.df = df
341
+
342
+ def get_df(self):
343
+ return self.df
344
+
345
+
346
+ class History:
347
+ """Class for managing the history of optimization results.
348
+
349
+ Args:
350
+ history_path (str): The path to the csv file.
351
+ prm_names (List[str], optional): The names of parameters. Defaults to None.
352
+ obj_names (List[str], optional): The names of objectives. Defaults to None.
353
+ cns_names (List[str], optional): The names of constraints. Defaults to None.
354
+ client (dask.distributed.Client): Dask client.
355
+ additional_metadata (str, optional): metadata of optimization process.
356
+
357
+ Raises:
358
+ FileNotFoundError: If the csv file is not found.
359
+
360
+ Attributes:
361
+ HEADER_ROW (int): Header row number of csv file. Must be grater than 0. Default to 2.
362
+ ENCODING (str): Encoding of csv file. Default to 'cp932'.
363
+ prm_names (str): User defined names of parameters.
364
+ obj_names (str): User defined names of objectives.
365
+ cns_names (str): User defined names of constraints.
366
+ local_data (pd.DataFrame): Local copy (on memory) of optimization history.
367
+ is_restart (bool): If the optimization process is a continuation of another process or not.
368
+ is_processing (bool): The optimization is running or not.
369
+
370
+ """
371
+
372
+ HEADER_ROW = 2
373
+ ENCODING = 'cp932'
374
+ prm_names = []
375
+ obj_names = []
376
+ cns_names = []
377
+ local_data = pd.DataFrame()
378
+ is_restart = False
379
+ is_processing = False
380
+ _future = None
381
+ _actor_data = None
382
+
383
+ def __init__(
384
+ self,
385
+ history_path,
386
+ prm_names=None,
387
+ obj_names=None,
388
+ cns_names=None,
389
+ client=None,
390
+ additional_metadata=None,
391
+ ):
392
+
393
+ # 引数の処理
394
+ self.path = history_path # .csv
395
+ self.prm_names = prm_names
396
+ self.obj_names = obj_names
397
+ self.cns_names = cns_names
398
+ self.additional_metadata = additional_metadata or ''
399
+
400
+ # 最適化実行中かどうか
401
+ self.is_processing = client is not None
402
+
403
+ # 最適化実行中の process monitor である場合
404
+ if self.is_processing:
405
+
406
+ # actor の生成
407
+ self._future = client.submit(_HistoryDfCore, actor=True)
408
+ self._actor_data = self._future.result()
409
+
410
+ # csv が存在すれば続きからモード
411
+ self.is_restart = os.path.isfile(self.path)
412
+
413
+ # 続きからなら df を読み込んで df にコピー
414
+ if self.is_restart:
415
+ self.load()
416
+
417
+ # そうでなければ df を初期化
418
+ else:
419
+ columns, metadata = self.create_df_columns()
420
+ for c in columns:
421
+ self.local_data[c] = None
422
+ self.metadata = metadata
423
+
424
+ # actor_data の初期化
425
+ self.actor_data = self.local_data
426
+
427
+ # 一時ファイルに書き込みを試み、UnicodeEncodeError が出ないかチェック
428
+ import tempfile
429
+ try:
430
+ with tempfile.TemporaryFile() as f:
431
+ self.save(_f=f)
432
+ except UnicodeEncodeError:
433
+ raise ValueError('変数名、目的名または拘束名にエンコードできない文字が含まれています。環境依存文字は使用しないでください。')
434
+
435
+ # visualization only の場合
436
+ else:
437
+ # csv が存在しなければおかしい
438
+ if not os.path.isfile(self.path):
439
+ raise FileNotFoundError(f'{self.path} が見つかりません。')
440
+
441
+ # csv の local_data へと、names への読み込み
442
+ self.load()
443
+
444
+ def load(self):
445
+ """Load existing result csv."""
446
+
447
+ # df を読み込む
448
+ self.local_data = pd.read_csv(self.path, encoding=self.ENCODING, header=self.HEADER_ROW)
449
+
450
+ # metadata を読み込む
451
+ with open(self.path, mode='r', encoding=self.ENCODING, newline='\n') as f:
452
+ reader = csv.reader(f, delimiter=',')
453
+ self.metadata = reader.__next__()
454
+
455
+ # 最適化問題を読み込む
456
+ columns = self.local_data.columns
457
+ prm_names = [column for i, column in enumerate(columns) if self.metadata[i] == 'prm']
458
+ obj_names = [column for i, column in enumerate(columns) if self.metadata[i] == 'obj']
459
+ cns_names = [column for i, column in enumerate(columns) if self.metadata[i] == 'cns']
460
+
461
+ # is_restart の場合、読み込んだ names と引数の names が一致するか確認しておく
462
+ if self.is_restart:
463
+ if prm_names != self.prm_names: raise ValueError(f'実行中の設定が csv ファイルの設定と一致しません。')
464
+ if obj_names != self.obj_names: raise ValueError(f'実行中の設定が csv ファイルの設定と一致しません。')
465
+ if cns_names != self.cns_names: raise ValueError(f'実行中の設定が csv ファイルの設定と一致しません。')
466
+
467
+ # visualization only の場合、読み込んだ names をも load する
468
+ if not self.is_processing:
469
+ self.prm_names = prm_names
470
+ self.obj_names = obj_names
471
+ self.cns_names = cns_names
472
+
473
+ @property
474
+ def actor_data(self):
475
+ return self._actor_data.get_df().result()
476
+
477
+ @actor_data.setter
478
+ def actor_data(self, df):
479
+ self._actor_data.set_df(df).result()
480
+
481
+ def create_df_columns(self):
482
+ """Create columns of history."""
483
+
484
+ # df として保有するカラムを生成
485
+ columns = list()
486
+
487
+ # columns のメタデータを作成
488
+ metadata = list()
489
+
490
+ # trial
491
+ columns.append('trial') # index
492
+ metadata.append(self.additional_metadata)
493
+
494
+ # parameter
495
+ columns.extend(self.prm_names)
496
+ metadata.extend(['prm'] * len(self.prm_names))
497
+
498
+ # objective relative
499
+ for name in self.obj_names:
500
+ columns.append(name)
501
+ metadata.append('obj')
502
+ columns.append(name + '_direction')
503
+ metadata.append('obj_direction')
504
+ columns.append('non_domi')
505
+ metadata.append('')
506
+
507
+ # constraint relative
508
+ for name in self.cns_names:
509
+ columns.append(name)
510
+ metadata.append('cns')
511
+ columns.append(name + '_lower_bound')
512
+ metadata.append('cns_lb')
513
+ columns.append(name + '_upper_bound')
514
+ metadata.append('cns_ub')
515
+ columns.append('feasible')
516
+ metadata.append('')
517
+
518
+ # the others
519
+ columns.append('hypervolume')
520
+ metadata.append('')
521
+ columns.append('message')
522
+ metadata.append('')
523
+ columns.append('time')
524
+ metadata.append('')
525
+
526
+ return columns, metadata
527
+
528
+ def record(self, parameters, objectives, constraints, obj_values, cns_values, message):
529
+ """Records the optimization results in the history.
530
+
531
+ Record only. NOT save.
532
+
533
+ Args:
534
+ parameters (pd.DataFrame): The parameter values.
535
+ objectives (dict): The objective functions.
536
+ constraints (dict): The constraint functions.
537
+ obj_values (list): The objective values.
538
+ cns_values (list): The constraint values.
539
+ message (str): Additional information or messages related to the optimization results.
540
+
541
+ """
542
+
543
+ # create row
544
+ row = list()
545
+
546
+ # trial(dummy)
547
+ row.append(-1)
548
+
549
+ # parameters
550
+ row.extend(parameters['value'].values)
551
+
552
+ # objectives and their direction
553
+ for (_, obj), obj_value in zip(objectives.items(), obj_values): # objectives, direction
554
+ row.extend([obj_value, obj.direction])
555
+
556
+ # non_domi (dummy)
557
+ row.append(False)
558
+
559
+ # constraints and their lb, ub and calculate each feasibility
560
+ feasible_list = []
561
+ for (_, cns), cns_value in zip(constraints.items(), cns_values): # cns, lb, ub
562
+ row.extend([cns_value, cns.lb, cns.ub])
563
+ feasible_list.append(is_feasible(cns_value, cns.lb, cns.ub))
564
+
565
+ # feasibility
566
+ row.append(all(feasible_list))
567
+
568
+ # the others
569
+ row.append(-1.) # dummy hypervolume
570
+ row.append(message) # message
571
+ row.append(datetime.datetime.now()) # time
572
+
573
+ with Lock('calc-history'):
574
+ # append
575
+ if len(self.actor_data) == 0:
576
+ self.local_data = pd.DataFrame([row], columns=self.actor_data.columns)
577
+ else:
578
+ self.local_data = self.actor_data
579
+ self.local_data.loc[len(self.local_data)] = row
580
+
581
+ # calc
582
+ self.local_data['trial'] = np.arange(len(self.local_data)) + 1 # 1 始まり
583
+ self._calc_non_domi(objectives) # update self.local_data
584
+ self._calc_hypervolume(objectives) # update self.local_data
585
+ self.actor_data = self.local_data
586
+
587
+ def _calc_non_domi(self, objectives):
588
+
589
+ # 目的関数の履歴を取り出してくる
590
+ solution_set = self.local_data[self.obj_names]
591
+
592
+ # 最小化問題の座標空間に変換する
593
+ for obj_column, (_, objective) in zip(self.obj_names, objectives.items()):
594
+ solution_set.loc[:, obj_column] = solution_set[obj_column].map(objective.convert)
595
+
596
+ # 非劣解の計算
597
+ non_domi = []
598
+ for i, row in solution_set.iterrows():
599
+ non_domi.append((row > solution_set).product(axis=1).sum(axis=0) == 0)
600
+
601
+ # 非劣解の登録
602
+ self.local_data['non_domi'] = non_domi
603
+
604
+ def _calc_hypervolume(self, objectives):
605
+
606
+ # タイピングが面倒
607
+ df = self.local_data
608
+
609
+ # パレート集合の抽出
610
+ idx = df['non_domi'].values
611
+ pdf = df[idx]
612
+ pareto_set = pdf[self.obj_names].values
613
+ n = len(pareto_set) # 集合の要素数
614
+ m = len(pareto_set.T) # 目的変数数
615
+ # 多目的でないと計算できない
616
+ if m <= 1:
617
+ return None
618
+ # 長さが 2 以上でないと計算できない
619
+ if n <= 1:
620
+ return None
621
+ # 最小化問題に convert
622
+ for i, (_, objective) in enumerate(objectives.items()):
623
+ for j in range(n):
624
+ pareto_set[j, i] = objective.convert(pareto_set[j, i])
625
+ #### reference point の計算[1]
626
+ # 逆正規化のための範囲計算
627
+ maximum = pareto_set.max(axis=0)
628
+ minimum = pareto_set.min(axis=0)
629
+
630
+ r = 1.01
631
+
632
+ # r を逆正規化
633
+ reference_point = r * (maximum - minimum) + minimum
634
+
635
+ #### hv 履歴の計算
636
+ wfg = WFG()
637
+ hvs = []
638
+ for i in range(n):
639
+ hv = wfg.compute(pareto_set[:i], reference_point)
640
+ if np.isnan(hv):
641
+ hv = 0
642
+ hvs.append(hv)
643
+
644
+ # 計算結果を履歴の一部に割り当て
645
+ df.loc[idx, 'hypervolume'] = np.array(hvs)
646
+
647
+ # dominated の行に対して、上に見ていって
648
+ # 最初に見つけた non-domi 行の hypervolume の値を割り当てます
649
+ for i in range(len(df)):
650
+ if not df.loc[i, 'non_domi']:
651
+ try:
652
+ df.loc[i, 'hypervolume'] = df.loc[:i][df.loc[:i]['non_domi']].iloc[-1]['hypervolume']
653
+ except IndexError:
654
+ df.loc[i, 'hypervolume'] = 0
655
+
656
+ def save(self, _f=None):
657
+ """Save csv file."""
658
+
659
+ if _f is None:
660
+ # save df with columns with prefix
661
+ with open(self.path, 'w', encoding=self.ENCODING) as f:
662
+ writer = csv.writer(f, delimiter=',', lineterminator="\n")
663
+ writer.writerow(self.metadata)
664
+ for i in range(self.HEADER_ROW-1):
665
+ writer.writerow([''] * len(self.metadata))
666
+ self.actor_data.to_csv(f, index=None, encoding=self.ENCODING, lineterminator='\n')
667
+ else: # test
668
+ self.actor_data.to_csv(_f, index=None, encoding=self.ENCODING, lineterminator='\n')
669
+
670
+
671
+ class _OptimizationStatusActor:
672
+ status_int = -1
673
+ status = 'undefined'
674
+
675
+ def set(self, value, text):
676
+ self.status_int = value
677
+ self.status = text
678
+
679
+
680
+ class OptimizationStatus:
681
+ """Optimization status."""
682
+
683
+ UNDEFINED = -1
684
+ INITIALIZING = 0
685
+ SETTING_UP = 10
686
+ LAUNCHING_FEM = 20
687
+ WAIT_OTHER_WORKERS = 22
688
+ # WAIT_1ST = 25
689
+ RUNNING = 30
690
+ INTERRUPTING = 40
691
+ TERMINATED = 50
692
+ TERMINATE_ALL = 60
693
+
694
+ def __init__(self, client, name='entire'):
695
+ self._future = client.submit(_OptimizationStatusActor, actor=True)
696
+ self._actor = self._future.result()
697
+ self.name = name
698
+ self.set(self.INITIALIZING)
699
+
700
+ @classmethod
701
+ def const_to_str(cls, status_const):
702
+ """Convert optimization status integer to message."""
703
+ if status_const == cls.UNDEFINED: return 'Undefined'
704
+ if status_const == cls.INITIALIZING: return 'Initializing'
705
+ if status_const == cls.SETTING_UP: return 'Setting up'
706
+ if status_const == cls.LAUNCHING_FEM: return 'Launching FEM processes'
707
+ if status_const == cls.WAIT_OTHER_WORKERS: return 'Waiting for launching other processes'
708
+ # if status_const == cls.WAIT_1ST: return 'Running and waiting for 1st FEM result.'
709
+ if status_const == cls.RUNNING: return 'Running'
710
+ if status_const == cls.INTERRUPTING: return 'Interrupting'
711
+ if status_const == cls.TERMINATED: return 'Terminated'
712
+ if status_const == cls.TERMINATE_ALL: return 'Terminate_all'
713
+
714
+ def set(self, status_const):
715
+ """Set optimization status."""
716
+ self._actor.set(status_const, self.const_to_str(status_const)).result()
717
+ msg = f'---{self.const_to_str(status_const)}---'
718
+ if (status_const == self.INITIALIZING) and (self.name != 'entire'):
719
+ msg += f' (for Worker {self.name})'
720
+ if self.name == 'entire':
721
+ msg = '(entire) ' + msg
722
+ logger.info(msg)
723
+
724
+ def get(self) -> int:
725
+ """Get optimization status."""
726
+ return self._actor.status_int
727
+
728
+ def get_text(self) -> str:
729
+ """Get optimization status message."""
730
+ return self._actor.status