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.
- pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.py +1 -1
- pyfemtet/FemtetPJTSample/Sldworks_ex01/Sldworks_ex01.py +1 -1
- pyfemtet/FemtetPJTSample/gau_ex08_parametric.py +1 -1
- pyfemtet/FemtetPJTSample/her_ex40_parametric.femprj +0 -0
- pyfemtet/FemtetPJTSample/her_ex40_parametric.py +1 -1
- pyfemtet/FemtetPJTSample/wat_ex14_parallel_parametric.py +1 -1
- pyfemtet/FemtetPJTSample/wat_ex14_parametric.femprj +0 -0
- pyfemtet/FemtetPJTSample/wat_ex14_parametric.py +1 -1
- pyfemtet/__init__.py +1 -1
- pyfemtet/core.py +14 -0
- pyfemtet/dispatch_extensions.py +5 -0
- pyfemtet/opt/__init__.py +22 -2
- pyfemtet/opt/_femopt.py +544 -0
- pyfemtet/opt/_femopt_core.py +730 -0
- pyfemtet/opt/interface/__init__.py +15 -0
- pyfemtet/opt/interface/_base.py +71 -0
- pyfemtet/opt/{interface.py → interface/_femtet.py} +120 -407
- pyfemtet/opt/interface/_femtet_with_nx/__init__.py +3 -0
- pyfemtet/opt/interface/_femtet_with_nx/_interface.py +128 -0
- pyfemtet/opt/interface/_femtet_with_sldworks.py +174 -0
- pyfemtet/opt/opt/__init__.py +8 -0
- pyfemtet/opt/opt/_base.py +202 -0
- pyfemtet/opt/opt/_optuna.py +240 -0
- pyfemtet/opt/visualization/__init__.py +7 -0
- pyfemtet/opt/visualization/_graphs.py +222 -0
- pyfemtet/opt/visualization/_monitor.py +1149 -0
- {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.1.dist-info}/METADATA +4 -4
- pyfemtet-0.4.1.dist-info/RECORD +38 -0
- {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.1.dist-info}/WHEEL +1 -1
- pyfemtet-0.4.1.dist-info/entry_points.txt +3 -0
- pyfemtet/opt/base.py +0 -1490
- pyfemtet/opt/monitor.py +0 -474
- pyfemtet-0.3.12.dist-info/RECORD +0 -26
- /pyfemtet/opt/{_FemtetWithNX → interface/_femtet_with_nx}/update_model.py +0 -0
- {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
|