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,1149 @@
1
+ import os
2
+ from typing import Optional, List
3
+
4
+ import json
5
+ import webbrowser
6
+ from time import sleep
7
+ from threading import Thread
8
+
9
+ import pandas as pd
10
+ import psutil
11
+ from plotly.graph_objects import Figure
12
+ from dash import Dash, html, dcc, Output, Input, State, callback_context, no_update
13
+ from dash.exceptions import PreventUpdate
14
+ import dash_bootstrap_components as dbc
15
+
16
+ import pyfemtet
17
+ from pyfemtet.opt.interface import FemtetInterface
18
+ from pyfemtet.opt.visualization._graphs import (
19
+ update_default_figure,
20
+ update_hypervolume_plot,
21
+ _CUSTOM_DATA_DICT
22
+ )
23
+
24
+
25
+ import logging
26
+ from pyfemtet.logger import get_logger
27
+ logger = get_logger('viz')
28
+ logger.setLevel(logging.INFO)
29
+
30
+
31
+ DBC_COLUMN_STYLE_CENTER = {
32
+ 'display': 'flex',
33
+ 'justify-content': 'center',
34
+ 'align-items': 'center',
35
+ }
36
+
37
+ DBC_COLUMN_STYLE_RIGHT = {
38
+ 'display': 'flex',
39
+ 'justify-content': 'right',
40
+ 'align-items': 'right',
41
+ }
42
+
43
+
44
+ def _unused_port_number(start=49152):
45
+ # "LISTEN" 状態のポート番号をリスト化
46
+ used_ports = [conn.laddr.port for conn in psutil.net_connections() if conn.status == 'LISTEN']
47
+ port = start
48
+ for port in range(start, 65535 + 1):
49
+ # 未使用のポート番号ならreturn
50
+ if port not in set(used_ports):
51
+ break
52
+ if port != start:
53
+ logger.warn(f'Specified port "{start}" seems to be used. Port "{port}" is used instead.')
54
+ return port
55
+
56
+
57
+ class FemtetControl:
58
+
59
+ # component ID
60
+ ID_LAUNCH_FEMTET_BUTTON = 'femtet-launch-button'
61
+ ID_CONNECT_FEMTET_BUTTON = 'femtet-connect-button'
62
+ ID_CONNECT_FEMTET_DROPDOWN = 'femtet-connect-dropdown'
63
+ ID_FEMTET_STATE_DATA = 'femtet-state-data'
64
+ ID_TRANSFER_PARAMETER_BUTTON = 'femtet-transfer-parameter-button'
65
+ ID_INTERVAL = 'femtet-interval'
66
+ ID_ALERTS = 'femtet-control-alerts'
67
+ ID_CLEAR_ALERTS = 'femtet-control-clear-alerts'
68
+
69
+ # arbitrary attribute name
70
+ ATTR_FEMTET_DATA = 'data-femtet' # attribute of html.Data should start with data-*.
71
+
72
+ # interface
73
+ fem: Optional['pyfemtet.opt.interface.FemtetInterface'] = None
74
+
75
+ @classmethod
76
+ def create_components(cls):
77
+
78
+ interval = dcc.Interval(id=cls.ID_INTERVAL, interval=1000)
79
+ femtet_state = {'pid': 0}
80
+ femtet_state_data = html.Data(id=cls.ID_FEMTET_STATE_DATA, **{cls.ATTR_FEMTET_DATA: femtet_state})
81
+
82
+ launch_new_femtet = dbc.DropdownMenuItem(
83
+ '新しい Femtet を起動して接続',
84
+ id=cls.ID_LAUNCH_FEMTET_BUTTON,
85
+ )
86
+
87
+ connect_existing_femtet = dbc.DropdownMenuItem(
88
+ '既存の Femtet と接続',
89
+ id=cls.ID_CONNECT_FEMTET_BUTTON,
90
+ )
91
+
92
+ launch_femtet_dropdown = dbc.DropdownMenu(
93
+ [
94
+ launch_new_femtet,
95
+ connect_existing_femtet
96
+ ],
97
+ label='Femtet と接続',
98
+ id=cls.ID_CONNECT_FEMTET_DROPDOWN,
99
+ )
100
+
101
+ transfer_parameter_button = dbc.Button('変数を Femtet に転送', id=cls.ID_TRANSFER_PARAMETER_BUTTON, color='primary', disabled=True)
102
+ clear_alert = dbc.Button('警告のクリア', id=cls.ID_CLEAR_ALERTS, color='secondary')
103
+
104
+ buttons = dbc.Row([
105
+ dbc.Col(
106
+ dbc.ButtonGroup([
107
+ launch_femtet_dropdown,
108
+ transfer_parameter_button,
109
+ ])
110
+ ),
111
+ dbc.Col(clear_alert, style=DBC_COLUMN_STYLE_RIGHT),
112
+ ])
113
+
114
+ alerts = dbc.CardBody(children=[], id=cls.ID_ALERTS)
115
+
116
+ control = dbc.Card([
117
+ dbc.CardHeader(buttons),
118
+ alerts,
119
+ ])
120
+
121
+ component = dbc.Container(
122
+ [
123
+ control,
124
+ ]
125
+ )
126
+
127
+ return [interval, femtet_state_data, component]
128
+
129
+ @classmethod
130
+ def add_callback(cls, home: 'HomePageBase'):
131
+
132
+ def launch_femtet_rule():
133
+ # default は手動で開く
134
+ femprj_path = None
135
+ model_name = None
136
+ msg = None
137
+ color = None
138
+
139
+ # metadata の有無を確認
140
+ additional_metadata = home.monitor.history.metadata[0]
141
+ is_invalid = False
142
+
143
+ # json である && femprj_path と model_name が key にある
144
+ prj_data = None
145
+ try:
146
+ prj_data = json.loads(additional_metadata)
147
+ _ = prj_data['femprj_path']
148
+ _ = prj_data['model_name']
149
+ except (TypeError, json.decoder.JSONDecodeError, KeyError):
150
+ is_invalid = True
151
+
152
+ if is_invalid:
153
+ color = 'warning'
154
+ msg = (
155
+ f'{home.monitor.history.path} に'
156
+ '解析プロジェクトファイルを示すデータが存在しないので'
157
+ '解析ファイルを自動で開けません。'
158
+ 'Femtet 起動後、最適化を行ったファイルを'
159
+ '手動で開いてください。'
160
+ )
161
+ logger.warn(msg)
162
+
163
+ else:
164
+
165
+ # femprj が存在しなければ警告
166
+ if not os.path.isfile(prj_data['femprj_path']):
167
+ color = 'warning'
168
+ msg = (
169
+ f'{femprj_path} が見つかりません。'
170
+ 'Femtet 起動後、最適化を行ったファイルを'
171
+ '手動で開いてください。'
172
+ )
173
+
174
+ # femprj はあるがその中に model があるかないかは開かないとわからない
175
+ # そうでなければ自動で開く
176
+ else:
177
+ femprj_path = prj_data['femprj_path']
178
+ model_name = prj_data['model_name']
179
+
180
+ return femprj_path, model_name, msg, color
181
+
182
+ def add_alert(current_alerts, msg, color):
183
+ new_alert = dbc.Alert(
184
+ msg,
185
+ id=f'alert-{len(current_alerts) + 1}',
186
+ dismissable=True,
187
+ color=color,
188
+ )
189
+ current_alerts.append(new_alert)
190
+ return current_alerts
191
+
192
+ #
193
+ # DESCRIPTION:
194
+ # Femtet を起動する際の挙動に従って warning を起こす callback
195
+ @home.monitor.app.callback(
196
+ [
197
+ Output(cls.ID_ALERTS, 'children', allow_duplicate=True),
198
+ ],
199
+ [
200
+ Input(cls.ID_LAUNCH_FEMTET_BUTTON, 'n_clicks'),
201
+ Input(cls.ID_CONNECT_FEMTET_BUTTON, 'n_clicks'),
202
+ ],
203
+ [
204
+ State(cls.ID_ALERTS, 'children'),
205
+ ],
206
+ prevent_initial_call=True,
207
+ )
208
+ def update_alert(_1, _2, current_alerts):
209
+ logger.debug('update-femtet-control-alert')
210
+
211
+ # ボタン経由でないなら無視
212
+ if callback_context.triggered_id is None:
213
+ raise PreventUpdate
214
+
215
+ # 戻り値
216
+ ret = {
217
+ (new_alerts_key := 1): no_update,
218
+ }
219
+
220
+ # 引数の処理
221
+ current_alerts = current_alerts or []
222
+
223
+ _, _, msg, color = launch_femtet_rule()
224
+
225
+ if msg is not None:
226
+ new_alerts = add_alert(current_alerts, msg, color)
227
+ ret[new_alerts_key] = new_alerts
228
+
229
+ return tuple(ret.values())
230
+
231
+ #
232
+ # DESCRIPTION:
233
+ # Femtet を起動し、pid を記憶する callback
234
+ @home.monitor.app.callback(
235
+ [
236
+ Output(cls.ID_FEMTET_STATE_DATA, cls.ATTR_FEMTET_DATA, allow_duplicate=True),
237
+ Output(cls.ID_ALERTS, 'children'),
238
+ ],
239
+ [
240
+ Input(cls.ID_LAUNCH_FEMTET_BUTTON, 'n_clicks'),
241
+ Input(cls.ID_CONNECT_FEMTET_BUTTON, 'n_clicks'),
242
+ ],
243
+ [
244
+ State(cls.ID_ALERTS, 'children'),
245
+ ],
246
+ prevent_initial_call=True,
247
+ )
248
+ def launch_femtet(launch_n_clicks, connect_n_clicks, current_alerts):
249
+ logger.debug(f'launch_femtet')
250
+
251
+ # 戻り値の設定
252
+ ret = {
253
+ (femtet_state_data := 1): no_update,
254
+ (key_new_alerts := 2): no_update,
255
+ }
256
+
257
+ # 引数の処理
258
+ current_alerts = current_alerts or []
259
+
260
+ # ボタン経由でなければ無視
261
+ if callback_context.triggered_id is None:
262
+ raise PreventUpdate
263
+
264
+ # Femtet を起動するための引数をルールに沿って取得
265
+ femprj_path, model_name, _, _ = launch_femtet_rule()
266
+
267
+ # Femtet の起動方法を取得
268
+ method = None
269
+ if callback_context.triggered_id == cls.ID_LAUNCH_FEMTET_BUTTON:
270
+ method = 'new'
271
+ elif callback_context.triggered_id == cls.ID_CONNECT_FEMTET_BUTTON:
272
+ method = 'existing'
273
+
274
+ # Femtet を起動
275
+ try:
276
+ cls.fem = FemtetInterface(
277
+ femprj_path=femprj_path,
278
+ model_name=model_name,
279
+ connect_method=method,
280
+ allow_without_project=True
281
+ )
282
+ cls.fem.quit_when_destruct = False
283
+ ret[femtet_state_data] = {'pid': cls.fem.femtet_pid}
284
+ except Exception as e: # 広めに取る
285
+ msg = e.args[0]
286
+ color = 'danger'
287
+ new_alerts = add_alert(current_alerts, msg, color)
288
+ ret[femtet_state_data] = {'pid': 0} # button の disable を再制御するため
289
+ ret[key_new_alerts] = new_alerts
290
+
291
+ return tuple(ret.values())
292
+
293
+ #
294
+ # DESCRIPTION
295
+ # Femtet の情報をアップデートするなどの監視 callback
296
+ @home.monitor.app.callback(
297
+ [
298
+ Output(cls.ID_FEMTET_STATE_DATA, cls.ATTR_FEMTET_DATA),
299
+ ],
300
+ [
301
+ Input(cls.ID_INTERVAL, 'n_intervals'),
302
+ ],
303
+ [
304
+ State(cls.ID_FEMTET_STATE_DATA, cls.ATTR_FEMTET_DATA)
305
+ ],
306
+ )
307
+ def interval_callback(n_intervals, current_femtet_data):
308
+ logger.debug('interval_callback')
309
+
310
+ # 戻り値
311
+ ret = {(femtet_data := 1): no_update}
312
+
313
+ # 引数の処理
314
+ n_intervals = n_intervals or 0
315
+ current_femtet_data = current_femtet_data or {}
316
+
317
+ # check pid
318
+ if 'pid' in current_femtet_data.keys():
319
+ pid = current_femtet_data['pid']
320
+ if pid > 0:
321
+ # 生きていると主張するなら、死んでいれば更新
322
+ if not psutil.pid_exists(pid):
323
+ ret[femtet_data] = current_femtet_data.update({'pid': 0})
324
+
325
+ return tuple(ret.values())
326
+
327
+ #
328
+ # DESCRIPTION
329
+ # Femtet の生死でボタンを disable にする callback
330
+ @home.monitor.app.callback(
331
+ [
332
+ Output(cls.ID_CONNECT_FEMTET_DROPDOWN, 'disabled', allow_duplicate=True),
333
+ Output(cls.ID_TRANSFER_PARAMETER_BUTTON, 'disabled', allow_duplicate=True),
334
+ ],
335
+ [
336
+ Input(cls.ID_FEMTET_STATE_DATA, cls.ATTR_FEMTET_DATA),
337
+ ],
338
+ prevent_initial_call=True,
339
+ )
340
+ def disable_femtet_button_on_state(femtet_data):
341
+ logger.debug(f'disable_femtet_button')
342
+ ret = {
343
+ (femtet_button_disabled := '1'): no_update,
344
+ (transfer_button_disabled := '3'): no_update,
345
+ }
346
+
347
+ if callback_context.triggered_id is not None:
348
+ femtet_data = femtet_data or {}
349
+ if femtet_data['pid'] > 0:
350
+ ret[femtet_button_disabled] = True
351
+ ret[transfer_button_disabled] = False
352
+ else:
353
+ ret[femtet_button_disabled] = False
354
+ ret[transfer_button_disabled] = True
355
+
356
+ return tuple(ret.values())
357
+
358
+ #
359
+ # DESCRIPTION: Femtet 起動ボタンを押したとき、起動完了を待たずにボタンを disable にする callback
360
+ @home.monitor.app.callback(
361
+ [
362
+ Output(cls.ID_CONNECT_FEMTET_DROPDOWN, 'disabled', allow_duplicate=True),
363
+ Output(cls.ID_TRANSFER_PARAMETER_BUTTON, 'disabled', allow_duplicate=True),
364
+ ],
365
+ [
366
+ Input(cls.ID_LAUNCH_FEMTET_BUTTON, 'n_clicks'),
367
+ Input(cls.ID_CONNECT_FEMTET_BUTTON, 'n_clicks'),
368
+ ],
369
+ prevent_initial_call=True,
370
+ )
371
+ def disable_femtet_button_immediately(launch_n_clicks, connect_n_clicks):
372
+ logger.debug(f'disable_button_on_state')
373
+ ret = {
374
+ (disable_femtet_button := '1'): no_update,
375
+ (disable_transfer_button := '2'): no_update,
376
+ }
377
+
378
+ if callback_context.triggered_id is not None:
379
+ ret[disable_femtet_button] = True
380
+ ret[disable_transfer_button] = True
381
+
382
+ return tuple(ret.values())
383
+
384
+ #
385
+ # DESCRIPTION: 転送ボタンを押すと Femtet にデータを転送する callback
386
+ @home.monitor.app.callback(
387
+ [
388
+ Output(home.ID_DUMMY, 'children', allow_duplicate=True),
389
+ Output(cls.ID_ALERTS, 'children', allow_duplicate=True),
390
+ ],
391
+ [
392
+ Input(cls.ID_TRANSFER_PARAMETER_BUTTON, 'n_clicks'),
393
+ ],
394
+ [
395
+ State(cls.ID_FEMTET_STATE_DATA, cls.ATTR_FEMTET_DATA),
396
+ State(home.ID_SELECTION_DATA, home.ATTR_SELECTION_DATA),
397
+ State(cls.ID_ALERTS, 'children'),
398
+ ],
399
+ prevent_initial_call=True,
400
+ )
401
+ def transfer_parameter_to_femtet(
402
+ n_clicks,
403
+ femtet_data,
404
+ selection_data,
405
+ current_alerts,
406
+ ):
407
+ logger.debug('transfer_parameter_to_femtet')
408
+
409
+ # 戻り値
410
+ ret = {
411
+ (dummy := 1): no_update,
412
+ (key_new_alerts := 2): no_update,
413
+ }
414
+
415
+ # 引数の処理
416
+ femtet_data = femtet_data or {}
417
+ selection_data = selection_data or {}
418
+ alerts = current_alerts or []
419
+
420
+ # Femtet が生きているか
421
+ femtet_alive = False
422
+ logger.debug(femtet_data.keys())
423
+ if 'pid' in femtet_data.keys():
424
+ pid = femtet_data['pid']
425
+ logger.debug(pid)
426
+ if (pid > 0) and (psutil.pid_exists(pid)): # pid_exists(0) == True
427
+ femtet_alive = True
428
+
429
+ additional_alerts = []
430
+
431
+ if not femtet_alive:
432
+ msg = '接続された Femtet が見つかりません。'
433
+ color = 'danger'
434
+ logger.warning(msg)
435
+ additional_alerts = add_alert(additional_alerts, msg, color)
436
+
437
+ if 'points' not in selection_data.keys():
438
+ msg = '何も選択されていません。'
439
+ color = 'danger'
440
+ logger.warning(msg)
441
+ additional_alerts = add_alert(additional_alerts, msg, color)
442
+
443
+ logger.debug(additional_alerts)
444
+ logger.debug(len(additional_alerts) > 0)
445
+
446
+ # この時点で警告(エラー)があれば処理せず終了
447
+ if len(additional_alerts) > 0:
448
+ alerts.extend(additional_alerts)
449
+ ret[key_new_alerts] = alerts
450
+ logger.debug(alerts)
451
+ return tuple(ret.values())
452
+
453
+ # もし手動で開いているなら現在開いているファイルとモデルを記憶させる
454
+ if cls.fem.femprj_path is None:
455
+ cls.fem.femprj_path = cls.fem.Femtet.Project
456
+ cls.fem.model_name = cls.fem.Femtet.AnalysisModelName
457
+
458
+ points_dicts = selection_data['points']
459
+ for points_dict in points_dicts:
460
+ logger.debug(points_dict)
461
+ trial = points_dict['customdata'][_CUSTOM_DATA_DICT['trial']]
462
+ logger.debug(trial)
463
+ index = trial - 1
464
+ names = [name for name in home.monitor.local_df.columns if name.startswith('prm_')]
465
+ values = home.monitor.local_df.iloc[index][names]
466
+
467
+ df = pd.DataFrame(
468
+ dict(
469
+ name=[name[4:] for name in names],
470
+ value=values,
471
+ )
472
+ )
473
+
474
+ try:
475
+ # femprj の保存先を設定
476
+ wo_ext, ext = os.path.splitext(cls.fem.femprj_path)
477
+ new_femprj_path = wo_ext + f'_trial{trial}' + ext
478
+
479
+ # 保存できないエラー
480
+ if os.path.exists(new_femprj_path):
481
+ msg = f'{new_femprj_path} は存在するため、保存はスキップされます。'
482
+ color = 'danger'
483
+ alerts = add_alert(alerts, msg, color)
484
+ ret[key_new_alerts] = alerts
485
+
486
+ else:
487
+ # Femtet に値を転送
488
+ warnings = cls.fem.update_model(df, with_warning=True) # exception の可能性
489
+ for msg in warnings:
490
+ color = 'warning'
491
+ logger.warning(msg)
492
+ alerts = add_alert(alerts, msg, color)
493
+ ret[key_new_alerts] = alerts
494
+
495
+ # 存在する femprj に対して bForce=False で SaveProject すると
496
+ # Exception が発生して except 節に飛んでしまう
497
+ cls.fem.Femtet.SaveProject(
498
+ new_femprj_path, # ProjectFile
499
+ False # bForce
500
+ )
501
+
502
+ except Exception as e: # 広めに取る
503
+ msg = ' '.join([arg for arg in e.args if type(arg) is str]) + 'このエラーが発生する主な理由は、Femtet でプロジェクトが開かれていないことです。'
504
+ color = 'danger'
505
+ alerts = add_alert(alerts, msg, color)
506
+ ret[key_new_alerts] = alerts
507
+
508
+ # 別のファイルを開いているならば元に戻す
509
+ if cls.fem.Femtet.Project != cls.fem.femprj_path:
510
+ try:
511
+ cls.fem.Femtet.LoadProjectAndAnalysisModel(
512
+ cls.fem.femprj_path, # ProjectFile
513
+ cls.fem.model_name, # AnalysisModelName
514
+ True # bForce
515
+ )
516
+ except Exception as e:
517
+ msg = ' '.join([arg for arg in e.args if type(arg) is str]) + '元のファイルを開けません。'
518
+ color = 'danger'
519
+ alerts = add_alert(alerts, msg, color)
520
+ ret[key_new_alerts] = alerts
521
+
522
+ return tuple(ret.values())
523
+
524
+ #
525
+ # DESCRIPTION
526
+ # Alerts を全部消す callback
527
+ @home.monitor.app.callback(
528
+ [
529
+ Output(cls.ID_ALERTS, 'children', allow_duplicate=True),
530
+ ],
531
+ [
532
+ Input(cls.ID_CLEAR_ALERTS, 'n_clicks'),
533
+ ],
534
+ prevent_initial_call=True
535
+ )
536
+ def clear_all_alerts(
537
+ clear_n_clicks
538
+ ):
539
+ ret = {
540
+ (alerts_key := 1): no_update
541
+ }
542
+
543
+ if callback_context.triggered_id is not None:
544
+ ret[alerts_key] = []
545
+
546
+ return tuple(ret.values())
547
+
548
+
549
+ class HomePageBase:
550
+
551
+ layout = None
552
+
553
+ # component id
554
+ ID_DUMMY = 'home-dummy'
555
+ ID_INTERVAL = 'home-interval'
556
+ ID_GRAPH_TABS = 'home-graph-tabs'
557
+ ID_GRAPH_CARD_BODY = 'home-graph-card-body'
558
+ ID_GRAPH = 'home-graph'
559
+ ID_SELECTION_DATA = 'home-selection-data'
560
+
561
+ # selection data attribute
562
+ ATTR_SELECTION_DATA = 'data-selection' # should start with data-*
563
+
564
+ # invisible components
565
+ dummy = html.Div(id=ID_DUMMY)
566
+ interval = dcc.Interval(id=ID_INTERVAL, interval=1000, n_intervals=0)
567
+
568
+ # visible components
569
+ header = html.H1('最適化の結果分析')
570
+ graph_card = dbc.Card()
571
+ contents = dbc.Container(fluid=True)
572
+
573
+ # card + tabs + tab + loading + graph 用
574
+ graphs = {} # {tab_id: {label, fig_func}}
575
+
576
+ def __init__(self, monitor):
577
+ self.monitor = monitor
578
+ self.app: Dash = monitor.app
579
+ self.history = monitor.history
580
+ self.df = monitor.local_df
581
+ self.setup_graph_card()
582
+ self.setup_contents()
583
+ self.setup_layout()
584
+
585
+ def setup_graph_card(self):
586
+
587
+ # graph の追加
588
+ default_tab = 'tab-id-1'
589
+ self.graphs[default_tab] = dict(
590
+ label='目的プロット',
591
+ fig_func=update_default_figure,
592
+ )
593
+
594
+ self.graphs['tab-id-2'] = dict(
595
+ label='ハイパーボリューム',
596
+ fig_func=update_hypervolume_plot,
597
+ )
598
+
599
+ # graphs から tab 作成
600
+ tabs = []
601
+ for tab_id, graph in self.graphs.items():
602
+ tabs.append(dbc.Tab(label=graph['label'], tab_id=tab_id))
603
+
604
+ # tab から tabs 作成
605
+ tabs_component = dbc.Tabs(
606
+ tabs,
607
+ id=self.ID_GRAPH_TABS,
608
+ active_tab=default_tab,
609
+ )
610
+
611
+ # タブ + グラフ本体のコンポーネント作成
612
+ self.graph_card = dbc.Card(
613
+ [
614
+ dbc.CardHeader(tabs_component),
615
+ dbc.CardBody(
616
+ children=[
617
+ # Loading : child が Output である callback について、
618
+ # それが発火してから return するまでの間 Spinner が出てくる
619
+ dcc.Loading(dcc.Graph(id=self.ID_GRAPH)),
620
+ ],
621
+ id=self.ID_GRAPH_CARD_BODY,
622
+ ),
623
+ html.Data(
624
+ id=self.ID_SELECTION_DATA,
625
+ **{self.ATTR_SELECTION_DATA: {}}
626
+ ),
627
+ ],
628
+ )
629
+
630
+ # Loading 表示のためページロード時のみ発火させる callback
631
+ @self.app.callback(
632
+ [
633
+ Output(self.ID_GRAPH, 'figure'),
634
+ ],
635
+ [
636
+ Input(self.ID_GRAPH_CARD_BODY, 'children'),
637
+ ],
638
+ [
639
+ State(self.ID_GRAPH_TABS, 'active_tab'),
640
+ ]
641
+ )
642
+ def on_load(_, active_tab):
643
+ # 1. initial_call または 2. card-body (即ちその中の graph) が変化した時に call される
644
+ # 2 で call された場合は ctx に card_body が格納されるのでそれで判定する
645
+ initial_call = callback_context.triggered_id is None
646
+ if initial_call:
647
+ return [self.get_fig_by_tab_id(active_tab)]
648
+ else:
649
+ raise PreventUpdate
650
+
651
+ # tab によるグラフ切替 callback
652
+ @self.app.callback(
653
+ [Output(self.ID_GRAPH, 'figure', allow_duplicate=True),],
654
+ [Input(self.ID_GRAPH_TABS, 'active_tab'),],
655
+ prevent_initial_call=True,
656
+ )
657
+ def switch_fig_by_tab(active_tab_id):
658
+ logger.debug(f'switch_fig_by_tab: {active_tab_id}')
659
+ if active_tab_id in self.graphs.keys():
660
+ fig_func = self.graphs[active_tab_id]['fig_func']
661
+ fig = fig_func(self.history, self.monitor.local_df)
662
+ else:
663
+ from plotly.graph_objects import Figure
664
+ fig = Figure()
665
+
666
+ return [fig]
667
+
668
+ # 選択したデータを記憶する callback
669
+ @self.monitor.app.callback(
670
+ [
671
+ Output(self.ID_SELECTION_DATA, self.ATTR_SELECTION_DATA),
672
+ ],
673
+ [
674
+ Input(self.ID_GRAPH, 'selectedData')
675
+ ],
676
+ )
677
+ def on_select(selected_data):
678
+ logger.debug(f'on_select: {selected_data}')
679
+ return [selected_data]
680
+
681
+ def get_fig_by_tab_id(self, tab_id):
682
+ if tab_id in self.graphs.keys():
683
+ fig_func = self.graphs[tab_id]['fig_func']
684
+ fig = fig_func(self.history, self.monitor.local_df)
685
+ else:
686
+ fig = Figure()
687
+ return fig
688
+
689
+ def setup_contents(self):
690
+ pass
691
+
692
+ def setup_layout(self):
693
+ # https://dash-bootstrap-components.opensource.faculty.ai/docs/components/accordion/
694
+ self.layout = dbc.Container([
695
+ dbc.Row([dbc.Col(self.dummy), dbc.Col(self.interval)]),
696
+ dbc.Row([dbc.Col(self.header)]),
697
+ dbc.Row([dbc.Col(self.graph_card)]),
698
+ dbc.Row([dbc.Col(self.contents)]),
699
+ ], fluid=True)
700
+
701
+
702
+
703
+ class ResultViewerAppHomePage(HomePageBase):
704
+
705
+ def setup_contents(self):
706
+ # Femtet control
707
+ div = FemtetControl.create_components()
708
+ FemtetControl.add_callback(self)
709
+
710
+ # note
711
+ note = dcc.Markdown(
712
+ '---\n'
713
+ '- 最適化の結果分析画面です。\n'
714
+ '- 凡例をクリックすると、対応する要素の表示/非表示を切り替えます。\n'
715
+ '- ブラウザを使用しますが、ネットワーク通信は行いません。\n'
716
+ '- ブラウザを閉じてもプログラムは終了しません。'
717
+ ' - コマンドプロンプトを閉じるかコマンドプロンプトに `CTRL+C` を入力してプログラムを終了してください。\n'
718
+ )
719
+ self.contents.children = [
720
+ dbc.Row(div),
721
+ dbc.Row(note)
722
+ ]
723
+
724
+
725
+ class ProcessMonitorAppHomePage(HomePageBase):
726
+
727
+ # component id for Monitor
728
+ ID_ENTIRE_PROCESS_STATUS_ALERT = 'home-entire-process-status-alert'
729
+ ID_ENTIRE_PROCESS_STATUS_ALERT_CHILDREN = 'home-entire-process-status-alert-children'
730
+ ID_TOGGLE_INTERVAL_BUTTON = 'home-toggle-interval-button'
731
+ ID_INTERRUPT_PROCESS_BUTTON = 'home-interrupt-process-button'
732
+
733
+ def setup_contents(self):
734
+
735
+ # header
736
+ self.header.children = '最適化の進捗状況'
737
+
738
+ # whole status
739
+ status_alert = dbc.Alert(
740
+ children=html.H4(
741
+ 'Optimization status will be shown here.',
742
+ className='alert-heading',
743
+ id=self.ID_ENTIRE_PROCESS_STATUS_ALERT_CHILDREN,
744
+ ),
745
+ id=self.ID_ENTIRE_PROCESS_STATUS_ALERT,
746
+ color='secondary',
747
+ )
748
+
749
+ # buttons
750
+ toggle_interval_button = dbc.Button('グラフ自動更新を一時停止', id=self.ID_TOGGLE_INTERVAL_BUTTON, color='primary')
751
+ interrupt_button = dbc.Button('最適化を中断', id=self.ID_INTERRUPT_PROCESS_BUTTON, color='danger')
752
+
753
+ note = dcc.Markdown(
754
+ '---\n'
755
+ '- 最適化の結果分析画面です。\n'
756
+ '- ブラウザを使用しますが、ネットワーク通信は行いません。\n'
757
+ '- この画面を閉じても最適化は中断されません。\n'
758
+ f'- この画面を再び開くにはブラウザのアドレスバーに「localhost:{self.monitor.DEFAULT_PORT}」と入力して下さい。\n'
759
+ )
760
+
761
+ self.contents.children = [
762
+ dbc.Row([dbc.Col(status_alert)]),
763
+ dbc.Row([
764
+ dbc.Col(toggle_interval_button, style=DBC_COLUMN_STYLE_CENTER),
765
+ dbc.Col(interrupt_button, style=DBC_COLUMN_STYLE_CENTER)
766
+ ]),
767
+ dbc.Row([dbc.Col(note)]),
768
+ ]
769
+
770
+ self.add_callback()
771
+
772
+ def add_callback(self):
773
+ # 1. interval => figure を更新する
774
+ # 2. btn interrupt => x(status を interrupt にする) and (interrupt を無効)
775
+ # 3. btn toggle => (toggle の children を切替) and (interval を切替)
776
+ # a. status => status-alert を更新
777
+ # b. status terminated => (toggle を無効) and (interrupt を無効) and (interval を無効)
778
+ @self.monitor.app.callback(
779
+ [
780
+ Output(self.ID_GRAPH_CARD_BODY, 'children'), # 1
781
+ Output(self.ID_INTERRUPT_PROCESS_BUTTON, 'disabled'), # 2, b
782
+ Output(self.ID_TOGGLE_INTERVAL_BUTTON, 'children'), # 3
783
+ Output(self.ID_INTERVAL, 'max_intervals'), # 3, b
784
+ Output(self.ID_TOGGLE_INTERVAL_BUTTON, 'disabled'), # b
785
+ Output(self.ID_ENTIRE_PROCESS_STATUS_ALERT_CHILDREN, 'children'), # a
786
+ Output(self.ID_ENTIRE_PROCESS_STATUS_ALERT, 'color'), # a
787
+ ],
788
+ [
789
+ Input(self.ID_INTERVAL, 'n_intervals'), # 1
790
+ Input(self.ID_INTERRUPT_PROCESS_BUTTON, 'n_clicks'), # 2
791
+ Input(self.ID_TOGGLE_INTERVAL_BUTTON, 'n_clicks'), # 3
792
+ ],
793
+ [
794
+ State(self.ID_GRAPH_TABS, "active_tab"),
795
+ ],
796
+ prevent_initial_call=True,
797
+ )
798
+ def monitor_feature(
799
+ _1,
800
+ _2,
801
+ toggle_n_clicks,
802
+ active_tab_id,
803
+ ):
804
+ # 発火確認
805
+ logger.debug(f'monitor_feature: {active_tab_id}')
806
+
807
+ # 引数の処理
808
+ toggle_n_clicks = toggle_n_clicks or 0
809
+ trigger = callback_context.triggered_id or 'implicit trigger'
810
+
811
+ # cls
812
+ from pyfemtet.opt._femopt_core import OptimizationStatus
813
+
814
+ # return 値の default 値(Python >= 3.7 で順番を保持する仕様を利用)
815
+ ret = {
816
+ (card_body := 'card_body'): no_update,
817
+ (disable_interrupt := 'disable_interrupt'): no_update,
818
+ (toggle_btn_msg := 'toggle_btn_msg'): no_update,
819
+ (max_intervals := 'max_intervals'): no_update, # 0:disable, -1:enable
820
+ (disable_toggle := 'disable_toggle'): no_update,
821
+ (status_msg := 'status_children'): no_update,
822
+ (status_color := 'status_color'): no_update,
823
+ }
824
+
825
+ # 2. btn interrupt => x(status を interrupt にする) and (interrupt を無効)
826
+ if trigger == self.ID_INTERRUPT_PROCESS_BUTTON:
827
+ # status を更新
828
+ # component は下流の処理で更新する
829
+ self.monitor.local_entire_status_int = OptimizationStatus.INTERRUPTING
830
+ self.monitor.local_entire_status = OptimizationStatus.const_to_str(OptimizationStatus.INTERRUPTING)
831
+
832
+ # 1. interval => figure を更新する
833
+ if (len(self.monitor.local_df) > 0) and (active_tab_id is not None):
834
+ fig = self.get_fig_by_tab_id(active_tab_id)
835
+ ret[card_body] = dcc.Graph(figure=fig, id=self.ID_GRAPH) # list にせんとダメかも
836
+
837
+ # 3. btn toggle => (toggle の children を切替) and (interval を切替)
838
+ if toggle_n_clicks % 2 == 1:
839
+ ret[max_intervals] = 0 # disable
840
+ ret[toggle_btn_msg] = '自動更新を再開'
841
+ else:
842
+ ret[max_intervals] = -1 # enable
843
+ ret[toggle_btn_msg] = '自動更新を一時停止'
844
+
845
+ # a. status => status-alert を更新
846
+ ret[status_msg] = 'Optimization status: ' + self.monitor.local_entire_status
847
+ if self.monitor.local_entire_status_int == OptimizationStatus.INTERRUPTING:
848
+ ret[status_color] = 'warning'
849
+ elif self.monitor.local_entire_status_int == OptimizationStatus.TERMINATED:
850
+ ret[status_color] = 'dark'
851
+ elif self.monitor.local_entire_status_int == OptimizationStatus.TERMINATE_ALL:
852
+ ret[status_color] = 'dark'
853
+ else:
854
+ ret[status_color] = 'primary'
855
+
856
+ # b. status terminated => (interrupt を無効) and (interval を無効)
857
+ # 中断以降なら中断ボタンを disable にする
858
+ if self.monitor.local_entire_status_int >= OptimizationStatus.INTERRUPTING:
859
+ ret[disable_interrupt] = True
860
+ # 終了以降ならさらに toggle, interval を disable にする
861
+ if self.monitor.local_entire_status_int >= OptimizationStatus.TERMINATED:
862
+ ret[max_intervals] = 0 # disable
863
+ ret[disable_toggle] = True
864
+ ret[toggle_btn_msg] = '更新されません'
865
+
866
+ return tuple(ret.values())
867
+
868
+
869
+ class ProcessMonitorAppWorkerPage:
870
+
871
+ # layout
872
+ layout = dbc.Container()
873
+
874
+ # id
875
+ ID_INTERVAL = 'worker-monitor-interval'
876
+ id_worker_alert_list = []
877
+
878
+ def __init__(self, monitor):
879
+ self.monitor = monitor
880
+ self.setup_layout()
881
+ self.add_callback()
882
+
883
+ def setup_layout(self):
884
+ # common
885
+ dummy = html.Div()
886
+ interval = dcc.Interval(id=self.ID_INTERVAL, interval=1000)
887
+
888
+ rows = [dbc.Row([dbc.Col(dummy), dbc.Col(interval)])]
889
+
890
+ # contents
891
+ worker_status_alerts = []
892
+ for i in range(len(self.monitor.worker_addresses)):
893
+ id_worker_alert = f'worker-status-alert-{i}'
894
+ alert = dbc.Alert('worker status here', id=id_worker_alert, color='dark')
895
+ worker_status_alerts.append(dbc.Row([dbc.Col(alert)]))
896
+ self.id_worker_alert_list.append(id_worker_alert)
897
+
898
+ rows.extend(worker_status_alerts)
899
+
900
+ self.layout = dbc.Container(rows, fluid=True)
901
+
902
+ def add_callback(self):
903
+ # worker_monitor のための callback
904
+ @self.monitor.app.callback(
905
+ [Output(f'{id_worker_alert}', 'children') for id_worker_alert in self.id_worker_alert_list],
906
+ [Output(f'{id_worker_alert}', 'color') for id_worker_alert in self.id_worker_alert_list],
907
+ [Input(self.ID_INTERVAL, 'n_intervals'),]
908
+ )
909
+ def update_worker_state(_):
910
+
911
+ from pyfemtet.opt._femopt_core import OptimizationStatus
912
+
913
+ ret = []
914
+
915
+ for worker_address, worker_status_int in zip(self.monitor.worker_addresses, self.monitor.local_worker_status_int_list):
916
+ worker_status_message = OptimizationStatus.const_to_str(worker_status_int)
917
+ ret.append(f'{worker_address} is {worker_status_message}')
918
+
919
+ colors = []
920
+ for status_int in self.monitor.local_worker_status_int_list:
921
+ if status_int == OptimizationStatus.INTERRUPTING:
922
+ colors.append('warning')
923
+ elif status_int == OptimizationStatus.TERMINATED:
924
+ colors.append('dark')
925
+ elif status_int == OptimizationStatus.TERMINATE_ALL:
926
+ colors.append('dark')
927
+ else:
928
+ colors.append('primary')
929
+
930
+ ret.extend(colors)
931
+
932
+ return tuple(ret)
933
+
934
+
935
+ class AppBase(object):
936
+
937
+ # process_monitor or not
938
+ is_processing = False
939
+
940
+ # port
941
+ DEFAULT_PORT = 49152
942
+
943
+ # members for sidebar application
944
+ SIDEBAR_STYLE = {
945
+ "position": "fixed",
946
+ "top": 0,
947
+ "left": 0,
948
+ "bottom": 0,
949
+ "width": "16rem",
950
+ "padding": "2rem 1rem",
951
+ "background-color": "#f8f9fa",
952
+ }
953
+ CONTENT_STYLE = {
954
+ "margin-left": "18rem",
955
+ "margin-right": "2rem",
956
+ "padding": "2rem 1rem",
957
+ }
958
+ pages = {} # {href: layout}
959
+ nav_links = {} # {order(positive float): NavLink}
960
+
961
+ # members updated by main threads
962
+ local_df = None
963
+
964
+ def __init__(
965
+ self,
966
+ history,
967
+ ):
968
+ # 引数の処理
969
+ self.history = history
970
+
971
+ # df の初期化
972
+ self.local_df = self.history.local_data
973
+
974
+ # app の立上げ
975
+ self.app = Dash(
976
+ __name__,
977
+ external_stylesheets=[dbc.themes.BOOTSTRAP],
978
+ title='PyFemtet Monitor',
979
+ update_title=None,
980
+ )
981
+
982
+ # def setup_some_page(self):
983
+ # href = "/link-url"
984
+ # page = SomePageClass(self)
985
+ # self.pages[href] = page.layout
986
+ # order: int = 1
987
+ # self.nav_links[order] = dbc.NavLink('Some Page', href=href, active="exact")
988
+
989
+ def setup_layout(self):
990
+ # setup sidebar
991
+ # https://dash-bootstrap-components.opensource.faculty.ai/examples/simple-sidebar/
992
+
993
+ # sidebar に表示される順に並び替え
994
+ ordered_items = sorted(self.nav_links.items(), key=lambda x: x[0])
995
+ ordered_links = [value for key, value in ordered_items]
996
+
997
+ # sidebar と contents から app 全体の layout を作成
998
+ sidebar = html.Div(
999
+ [
1000
+ html.H2('PyFemtet Monitor', className='display-4'),
1001
+ html.Hr(),
1002
+ html.P('最適化結果の可視化', className='lead'),
1003
+ dbc.Nav(ordered_links, vertical=True, pills=True),
1004
+ ],
1005
+ style=self.SIDEBAR_STYLE,
1006
+ )
1007
+ content = html.Div(id="page-content", style=self.CONTENT_STYLE)
1008
+ self.app.layout = html.Div([dcc.Location(id="url"), sidebar, content])
1009
+
1010
+ # sidebar によるページ遷移のための callback
1011
+ @self.app.callback(Output("page-content", "children"), [Input("url", "pathname")])
1012
+ def switch_page_content(pathname):
1013
+ if pathname in list(self.pages.keys()):
1014
+ return self.pages[pathname]
1015
+
1016
+ else:
1017
+ return html.Div(
1018
+ [
1019
+ html.H1("404: Not found", className="text-danger"),
1020
+ html.Hr(),
1021
+ html.P(f"The pathname {pathname} was not recognised..."),
1022
+ ],
1023
+ className="p-3 bg-light rounded-3",
1024
+ )
1025
+
1026
+ def run(self, host='localhost', port=None):
1027
+ self.setup_layout()
1028
+ port = port or self.DEFAULT_PORT
1029
+ # port を検証
1030
+ port = _unused_port_number(port)
1031
+ # ブラウザを起動
1032
+ if host == '0.0.0.0':
1033
+ webbrowser.open(f'http://localhost:{str(port)}')
1034
+ else:
1035
+ webbrowser.open(f'http://{host}:{str(port)}')
1036
+ self.app.run(debug=False, host=host, port=port)
1037
+
1038
+
1039
+ class ResultViewerApp(AppBase):
1040
+
1041
+ def __init__(
1042
+ self,
1043
+ history,
1044
+ ):
1045
+
1046
+ # 引数の設定、app の設定
1047
+ super().__init__(history)
1048
+
1049
+ # page 設定
1050
+ self.setup_home()
1051
+
1052
+
1053
+ def setup_home(self):
1054
+ href = "/"
1055
+ page = ResultViewerAppHomePage(self)
1056
+ self.pages[href] = page.layout
1057
+ order = 1
1058
+ self.nav_links[order] = dbc.NavLink("Home", href=href, active="exact")
1059
+
1060
+
1061
+ class ProcessMonitorApp(AppBase):
1062
+
1063
+ DEFAULT_PORT = 8080
1064
+
1065
+ def __init__(
1066
+ self,
1067
+ history,
1068
+ status,
1069
+ worker_addresses: List[str],
1070
+ worker_status_list: List['pyfemtet.opt._core.OptimizationStatus'],
1071
+ ):
1072
+
1073
+ self.status = status
1074
+ self.worker_addresses = worker_addresses
1075
+ self.worker_status_list = worker_status_list
1076
+ self.is_processing = True
1077
+
1078
+ # ログファイルの保存場所
1079
+ if logger.level != logging.DEBUG:
1080
+ log_path = history.path.replace('.csv', '.uilog')
1081
+ monitor_logger = logging.getLogger('werkzeug')
1082
+ monitor_logger.addHandler(logging.FileHandler(log_path))
1083
+
1084
+ # メインスレッドで更新してもらうメンバーを一旦初期化
1085
+ self.local_entire_status = self.status.get_text()
1086
+ self.local_entire_status_int = self.status.get()
1087
+ self.local_worker_status_int_list = [s.get() for s in self.worker_status_list]
1088
+
1089
+ # dash app 設定
1090
+ super().__init__(history)
1091
+
1092
+ # page 設定
1093
+ self.setup_home()
1094
+ self.setup_worker_monitor()
1095
+
1096
+ def setup_home(self):
1097
+ href = "/"
1098
+ page = ProcessMonitorAppHomePage(self)
1099
+ self.pages[href] = page.layout
1100
+ order = 1
1101
+ self.nav_links[order] = dbc.NavLink("Home", href=href, active="exact")
1102
+
1103
+ def setup_worker_monitor(self):
1104
+ href = "/worker-monitor/"
1105
+ page = ProcessMonitorAppWorkerPage(self)
1106
+ self.pages[href] = page.layout
1107
+ order = 2
1108
+ self.nav_links[order] = dbc.NavLink("Workers", href=href, active="exact")
1109
+
1110
+ def start_server(
1111
+ self,
1112
+ host=None,
1113
+ port=None,
1114
+ ):
1115
+ host = host or 'localhost'
1116
+ port = port or self.DEFAULT_PORT
1117
+
1118
+ # dash app server を daemon thread で起動
1119
+ server_thread = Thread(
1120
+ target=self.run,
1121
+ args=(host, port,),
1122
+ daemon=True,
1123
+ )
1124
+ server_thread.start()
1125
+
1126
+ # dash app (=flask server) の callback で dask の actor にアクセスすると
1127
+ # おかしくなることがあるので、ここで必要な情報のみやり取りする
1128
+ from pyfemtet.opt._femopt_core import OptimizationStatus
1129
+ while True:
1130
+ # running 以前に monitor が current status を interrupting にしていれば actor に反映
1131
+ if (
1132
+ (self.status.get() <= OptimizationStatus.RUNNING) # メインプロセスが RUNNING 以前である
1133
+ and
1134
+ (self.local_entire_status_int == OptimizationStatus.INTERRUPTING) # monitor の status が INTERRUPT である
1135
+ ):
1136
+ self.status.set(OptimizationStatus.INTERRUPTING)
1137
+
1138
+ # current status と df を actor から monitor に反映する
1139
+ self.local_entire_status_int = self.status.get()
1140
+ self.local_entire_status = self.status.get_text()
1141
+ self.local_df = self.history.actor_data.copy()
1142
+ self.local_worker_status_int_list = [s.get() for s in self.worker_status_list]
1143
+
1144
+ # terminate_all 指令があれば monitor server をホストするプロセスごと終了する
1145
+ if self.status.get() == OptimizationStatus.TERMINATE_ALL:
1146
+ return 0 # take server down with me
1147
+
1148
+ # interval
1149
+ sleep(1)