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