pyfemtet 0.4.10__py3-none-any.whl → 0.4.11__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 (71) hide show
  1. pyfemtet/__init__.py +1 -1
  2. pyfemtet/opt/_femopt.py +9 -3
  3. pyfemtet/opt/interface/_femtet.py +32 -10
  4. pyfemtet/opt/interface/_femtet_parametric.py +5 -25
  5. pyfemtet/opt/opt/_base.py +9 -1
  6. pyfemtet/opt/opt/_optuna.py +4 -0
  7. pyfemtet/opt/visualization/__init__.py +0 -7
  8. pyfemtet/opt/visualization/_create_wrapped_components.py +93 -0
  9. pyfemtet/opt/visualization/base.py +254 -0
  10. pyfemtet/opt/visualization/complex_components/__init__.py +0 -0
  11. pyfemtet/opt/visualization/complex_components/alert_region.py +71 -0
  12. pyfemtet/opt/visualization/complex_components/control_femtet.py +195 -0
  13. pyfemtet/opt/visualization/{_graphs.py → complex_components/main_figure_creator.py} +13 -49
  14. pyfemtet/opt/visualization/complex_components/main_graph.py +263 -0
  15. pyfemtet/opt/visualization/process_monitor/__init__.py +0 -0
  16. pyfemtet/opt/visualization/process_monitor/application.py +201 -0
  17. pyfemtet/opt/visualization/process_monitor/pages.py +276 -0
  18. pyfemtet/opt/visualization/result_viewer/.gitignore +1 -0
  19. pyfemtet/opt/visualization/result_viewer/__init__.py +0 -0
  20. pyfemtet/opt/visualization/result_viewer/application.py +44 -0
  21. pyfemtet/opt/visualization/result_viewer/pages.py +692 -0
  22. pyfemtet/opt/visualization/result_viewer/tutorial/tutorial.csv +18 -0
  23. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14.jpg +0 -0
  24. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14.log +81 -0
  25. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14.pdt +0 -0
  26. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_heatflow.csv +28 -0
  27. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_heatflow_el.csv +22 -0
  28. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial1.jpg +0 -0
  29. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial1.pdt +0 -0
  30. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial10.jpg +0 -0
  31. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial10.pdt +0 -0
  32. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial11.jpg +0 -0
  33. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial11.pdt +0 -0
  34. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial12.jpg +0 -0
  35. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial12.pdt +0 -0
  36. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial13.jpg +0 -0
  37. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial13.pdt +0 -0
  38. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial14.jpg +0 -0
  39. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial14.pdt +0 -0
  40. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial15.jpg +0 -0
  41. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial15.pdt +0 -0
  42. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial2.jpg +0 -0
  43. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial2.pdt +0 -0
  44. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial3.jpg +0 -0
  45. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial3.pdt +0 -0
  46. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial4.jpg +0 -0
  47. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial4.pdt +0 -0
  48. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial5.jpg +0 -0
  49. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial5.pdt +0 -0
  50. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial6.jpg +0 -0
  51. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial6.pdt +0 -0
  52. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial7.jpg +0 -0
  53. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial7.pdt +0 -0
  54. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial8.jpg +0 -0
  55. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial8.pdt +0 -0
  56. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial9.jpg +0 -0
  57. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.Results/Ex14_trial9.pdt +0 -0
  58. pyfemtet/opt/visualization/result_viewer/tutorial/wat_ex14_parametric.femprj +0 -0
  59. pyfemtet/opt/visualization/wrapped_components/__init__.py +0 -0
  60. pyfemtet/opt/visualization/wrapped_components/dbc.py +1518 -0
  61. pyfemtet/opt/visualization/wrapped_components/dcc.py +609 -0
  62. pyfemtet/opt/visualization/wrapped_components/html.py +3570 -0
  63. pyfemtet/opt/visualization/wrapped_components/str_enum.py +43 -0
  64. {pyfemtet-0.4.10.dist-info → pyfemtet-0.4.11.dist-info}/METADATA +1 -1
  65. pyfemtet-0.4.11.dist-info/RECORD +136 -0
  66. {pyfemtet-0.4.10.dist-info → pyfemtet-0.4.11.dist-info}/entry_points.txt +1 -1
  67. pyfemtet/opt/visualization/_monitor.py +0 -1227
  68. pyfemtet/opt/visualization/result_viewer.py +0 -13
  69. pyfemtet-0.4.10.dist-info/RECORD +0 -83
  70. {pyfemtet-0.4.10.dist-info → pyfemtet-0.4.11.dist-info}/LICENSE +0 -0
  71. {pyfemtet-0.4.10.dist-info → pyfemtet-0.4.11.dist-info}/WHEEL +0 -0
@@ -0,0 +1,692 @@
1
+ import json
2
+ import os
3
+ import tempfile
4
+ import base64
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import shutil
9
+
10
+ from dash import Output, Input, State, callback_context, no_update
11
+ from dash.exceptions import PreventUpdate
12
+
13
+ from pyfemtet.opt.visualization.wrapped_components import dcc, dbc, html
14
+ from pyfemtet.opt.visualization.base import AbstractPage # , logger
15
+ from pyfemtet.opt.visualization.complex_components.main_graph import MainGraph # , FLEXBOX_STYLE_ALLOW_VERTICAL_FILL
16
+ from pyfemtet.opt.visualization.complex_components.control_femtet import FemtetControl, FemtetState
17
+ from pyfemtet.opt.visualization.complex_components.alert_region import AlertRegion
18
+
19
+ from pyfemtet.opt._femopt_core import History
20
+
21
+
22
+ class HomePage(AbstractPage):
23
+
24
+ def __init__(self, title, rel_url='/'):
25
+ super().__init__(title, rel_url)
26
+
27
+ # noinspection PyAttributeOutsideInit
28
+ def setup_component(self):
29
+
30
+ self.location = dcc.Location(id='result-viewer-location')
31
+
32
+ # control femtet subpage
33
+ self.femtet_control: FemtetControl = FemtetControl()
34
+ self.add_subpage(self.femtet_control)
35
+
36
+ # main graph subpage
37
+ self.main_graph: MainGraph = MainGraph()
38
+ self.add_subpage(self.main_graph)
39
+
40
+ # alert region subpage
41
+ self.alert_region: AlertRegion = AlertRegion()
42
+ self.add_subpage(self.alert_region)
43
+
44
+ # open pdt (or transfer variable to femtet)
45
+ self.open_pdt_button = dbc.Button(
46
+ 'Open Result in Femtet',
47
+ id='open-pdt-button',
48
+ color='primary',
49
+ className="position-relative", # need to show badge
50
+ )
51
+
52
+ # update parameter
53
+ self.update_parameter_button = dbc.Button(
54
+ 'Reconstruct Model',
55
+ id='update-parameter-button',
56
+ color='secondary',
57
+ )
58
+
59
+ # file picker
60
+ self.file_picker_button = dbc.Button(
61
+ 'drag and drop or select files',
62
+ id='file-picker-button',
63
+ color='primary',
64
+ )
65
+ self.file_picker = dcc.Upload(
66
+ id='history-file-picker',
67
+ children=[self.file_picker_button],
68
+ multiple=False,
69
+ style={'display': 'inline'},
70
+ )
71
+
72
+ # tutorial subpage (after setup self components)
73
+ self.tutorial: Tutorial = Tutorial(self, self.main_graph, self.femtet_control)
74
+ self.add_subpage(self.tutorial)
75
+
76
+ # noinspection PyAttributeOutsideInit
77
+ def setup_layout(self):
78
+ """"""
79
+ """
80
+ =======================
81
+ | | ---------------- |
82
+ | | | | |
83
+ | | | Main Graph | |
84
+ | | | | |
85
+ | | ---------------- |
86
+ | | [] ... [] <---- Buttons
87
+ | | ---------------- |
88
+ | | | Alert Region | |
89
+ | ^| ---------------- |
90
+ ==|====================
91
+ |
92
+ SideBar
93
+ """
94
+
95
+ # Uncomment if the plotly's specification if fixed, and we can use dcc.Graph with FlexBox.
96
+ # style = {'height': '95vh'}
97
+ # style.update(FLEXBOX_STYLE_ALLOW_VERTICAL_FILL)
98
+ self.layout = dbc.Container(
99
+ children=[
100
+ dbc.Row([self.location, self.main_graph.layout, self.tutorial.graph_popover, self.tutorial.control_visibility_input_dummy]), # visible / invisible components
101
+ dbc.Row(self.femtet_control.layout), # invisible components
102
+ dbc.Row(
103
+ children=[
104
+ dbc.Col(html.Div([self.tutorial.tutorial_mode_switch], className='d-flex justify-content-center')),
105
+ dbc.Col(html.Div([self.file_picker, self.tutorial.load_sample_csv_div], className='d-flex justify-content-center',),),
106
+ dbc.Col(html.Div([self.femtet_control.connect_femtet_button_spinner, self.tutorial.connect_femtet_popover], className='d-flex justify-content-center')),
107
+ dbc.Col(html.Div([self.open_pdt_button, self.update_parameter_button, self.tutorial.open_pdt_popover], className='d-flex justify-content-center')),
108
+ ],
109
+ justify='evenly',
110
+ ),
111
+ dbc.Row(self.alert_region.layout), # visible / invisible components
112
+ ],
113
+ # style=style,
114
+ fluid=True,
115
+ )
116
+
117
+ def setup_callback(self):
118
+ # setup callback of subpages
119
+ super().setup_callback()
120
+
121
+ app = self.application.app
122
+
123
+ # ===== read history csv file from file picker =====
124
+ @app.callback(
125
+ Output(self.file_picker_button.id, self.file_picker_button.Prop.children),
126
+ Output(self.main_graph.tabs.id, self.main_graph.tabs.Prop.active_tab, allow_duplicate=False),
127
+ Output(self.femtet_control.connect_femtet_button.id, self.femtet_control.connect_femtet_button.Prop.n_clicks), # automatically connect to femtet if the metadata of csv is valid
128
+ Input(self.file_picker.id, self.file_picker.Prop.contents),
129
+ State(self.file_picker.id, self.file_picker.Prop.filename),
130
+ State(self.main_graph.tabs.id, self.main_graph.tabs.Prop.active_tab),
131
+ prevent_initial_call=False,
132
+ )
133
+ def set_history(encoded_file_content: str, file_name: str, active_tab_id: str):
134
+
135
+ # if the history is specified before launch GUI (not implemented), respond it.
136
+ if callback_context.triggered_id is None:
137
+ if self.application.history is not None:
138
+ file_name = os.path.split(self.application.history.path)[1]
139
+ return f'current file: {file_name}', no_update, no_update
140
+
141
+ if encoded_file_content is None:
142
+ raise PreventUpdate
143
+
144
+ if not file_name.lower().endswith('.csv'):
145
+ raise PreventUpdate
146
+
147
+ # create temporary file from file_content(full path is hidden by browser because of the security reason)
148
+ content_type, content_string = encoded_file_content.split(',')
149
+ file_content = base64.b64decode(content_string)
150
+ with tempfile.TemporaryDirectory() as tmp_dir_path:
151
+ csv_path = os.path.join(tmp_dir_path, file_name)
152
+ with open(csv_path, 'wb') as f:
153
+ f.write(file_content)
154
+ self.application.history = History(csv_path)
155
+
156
+ return f'current file: {file_name}', active_tab_id, 1
157
+
158
+ # ===== open pdt =====
159
+ @app.callback(
160
+ Output(self.alert_region.alert_region.id, 'children', allow_duplicate=True),
161
+ Input(self.open_pdt_button.id, self.open_pdt_button.Prop.n_clicks),
162
+ State(self.main_graph.selection_data.id, self.main_graph.selection_data_property),
163
+ State(self.alert_region.alert_region.id, 'children'),
164
+ prevent_initial_call=True,
165
+ )
166
+ def open_pdt(_, selection_data, current_alerts):
167
+ # open_pdt allows to open "解析結果単体" without opening any femprj.
168
+
169
+ # check Femtet state
170
+ connection_state = self.femtet_control.check_femtet_state()
171
+ if connection_state == FemtetState.missing or connection_state == FemtetState.unconnected:
172
+ msg = ('Connection to Femtet is not established. '
173
+ 'Launch Femtet and Open a project.')
174
+ alerts = self.alert_region.create_alerts(msg, color='danger', current_alerts=current_alerts)
175
+ return alerts
176
+
177
+ # check selection
178
+ if selection_data is None:
179
+ msg = 'No result plot is selected.'
180
+ alerts = self.alert_region.create_alerts(msg, color='danger', current_alerts=current_alerts)
181
+ return alerts
182
+
183
+ # get femprj in history csv
184
+ kwargs = self.femtet_control.create_femtet_interface_args()[0] # read metadata from history.
185
+ femprj_path = kwargs['femprj_path']
186
+ model_name = kwargs['model_name']
187
+
188
+ # check metadata is valid
189
+ if femprj_path is None:
190
+ msg = 'The femprj file path in the history csv is not found or valid. '
191
+ alerts = self.alert_region.create_alerts(msg, color='danger')
192
+ return alerts
193
+
194
+ if model_name is None:
195
+ msg = 'The model name in the history csv is not found.'
196
+ alerts = self.alert_region.create_alerts(msg, color='danger')
197
+ return alerts
198
+
199
+ # check pdt_path in selection_data
200
+ pt = selection_data['points'][0]
201
+ trial = pt['customdata'][0]
202
+
203
+ # get pdt path
204
+ pdt_path = self.femtet_control.fem.create_pdt_path(femprj_path, model_name, trial)
205
+
206
+ # check pdt exists
207
+ if not os.path.exists(pdt_path):
208
+ msg = ('.pdt file is not found. '
209
+ 'Please check the .Results folder. '
210
+ 'Note that .pdt file save mode depends on '
211
+ 'the `save_pdt` argument of FemtetInterface in optimization script'
212
+ '(default to `all`).')
213
+ alerts = self.alert_region.create_alerts(msg, color='danger')
214
+ return alerts
215
+
216
+ # OpenPDT(PDTFile As String, bOpenWithGUI As Boolean)
217
+ Femtet = self.femtet_control.fem.Femtet
218
+ succeed = Femtet.OpenPDT(pdt_path, True)
219
+
220
+ if not succeed:
221
+ msg = f'Failed to open {pdt_path}.'
222
+ alerts = self.alert_region.create_alerts(msg, color='danger')
223
+ return alerts
224
+
225
+ return no_update
226
+
227
+ # ===== reconstruct model with updating parameter =====
228
+ @app.callback(
229
+ Output(self.alert_region.alert_region.id, 'children', allow_duplicate=True),
230
+ Input(self.update_parameter_button.id, self.update_parameter_button.Prop.n_clicks),
231
+ State(self.main_graph.selection_data.id, self.main_graph.selection_data_property),
232
+ State(self.alert_region.alert_region.id, 'children'),
233
+ prevent_initial_call=True,
234
+ )
235
+ def update_parameter(_, selection_data, current_alerts):
236
+
237
+ # check Femtet state
238
+ connection_state = self.femtet_control.check_femtet_state()
239
+ if connection_state != FemtetState.connected:
240
+ msg = ('Connection to Femtet is not established. '
241
+ 'Launch Femtet and Open a project.')
242
+ alerts = self.alert_region.create_alerts(msg, color='danger', current_alerts=current_alerts)
243
+ return alerts
244
+
245
+ # check selection
246
+ if selection_data is None:
247
+ msg = 'No result plot is selected.'
248
+ alerts = self.alert_region.create_alerts(msg, color='danger', current_alerts=current_alerts)
249
+ return alerts
250
+
251
+ try:
252
+ Femtet = self.femtet_control.fem.Femtet
253
+
254
+
255
+ # check model to open is included in current project
256
+ if (self.femtet_control.fem.femprj_path != Femtet.Project) \
257
+ or (self.femtet_control.fem.model_name not in Femtet.GetAnalysisModelNames_py()):
258
+ msg = (f'{self.femtet_control.fem.model_name} is not in current project. '
259
+ f'Please check opened project. '
260
+ f'For example, not "analysis model only" but your .femprj file.')
261
+ alerts = self.alert_region.create_alerts(msg, color='danger', current_alerts=current_alerts)
262
+ return alerts
263
+
264
+ # certify opening model
265
+ Femtet.LoadProjectAndAnalysisModel(
266
+ self.femtet_control.fem.femprj_path,
267
+ self.femtet_control.fem.model_name,
268
+ False
269
+ )
270
+
271
+ # copy analysis model or femprj.
272
+ ...
273
+
274
+ except Exception as e:
275
+ msg = ('Unknown error has occurred in analysis model compatibility check. '
276
+ f'exception message is: {e}')
277
+ alerts = self.alert_region.create_alerts(msg, color='danger', current_alerts=current_alerts)
278
+ return alerts
279
+
280
+ try:
281
+ # get nth trial from selection data
282
+ pt = selection_data['points'][0]
283
+ trial = pt['customdata'][0]
284
+
285
+ # get parameter and update model
286
+ df = self.application.history.local_data
287
+ row = df[df['trial'] == trial]
288
+ metadata = np.array(self.application.history.metadata)
289
+ idx = np.where(metadata == 'prm')[0]
290
+
291
+ names = np.array(row.columns)[idx]
292
+ values = np.array(row.iloc[:, idx]).ravel()
293
+
294
+ parameter = pd.DataFrame(
295
+ dict(
296
+ name=names,
297
+ value=values,
298
+ )
299
+ )
300
+
301
+ self.femtet_control.fem.update_model(parameter)
302
+
303
+ Femtet.SaveProject(Femtet.Project, True)
304
+
305
+ return no_update
306
+
307
+ except Exception as e:
308
+ msg = ('Unknown error has occurred in updating model. '
309
+ f'exception message is: {e}')
310
+ alerts = self.alert_region.create_alerts(msg, color='danger', current_alerts=current_alerts)
311
+ return alerts
312
+
313
+ # ===== update alert (chained from launch femtet) =====
314
+ @app.callback(
315
+ Output(self.alert_region.alert_region.id, 'children', allow_duplicate=True),
316
+ Input(self.femtet_control.femtet_state.id, self.femtet_control.femtet_state_prop),
317
+ State(self.alert_region.alert_region.id, 'children'),
318
+ prevent_initial_call=True,
319
+ )
320
+ def add_alert_by_connect_femtet(femtet_state_data, current_alerts):
321
+ if callback_context.triggered_id is None:
322
+ raise PreventUpdate
323
+
324
+ msg = femtet_state_data['alert']
325
+ if msg == '':
326
+ raise PreventUpdate
327
+
328
+ new_alerts = self.alert_region.create_alerts(msg, 'warning', current_alerts)
329
+
330
+ return new_alerts
331
+
332
+ # ===== update alert (chained from read history) =====
333
+ @app.callback(
334
+ Output(self.alert_region.alert_region.id, 'children', allow_duplicate=True),
335
+ Input(self.file_picker_button.id, self.file_picker_button.Prop.children),
336
+ State(self.alert_region.alert_region.id, 'children'),
337
+ prevent_initial_call=True,
338
+ )
339
+ def add_alert_on_history_set(_, current_alerts):
340
+ if callback_context.triggered_id is None:
341
+ raise PreventUpdate
342
+
343
+ if self.application.history is None:
344
+ raise PreventUpdate
345
+
346
+ if self.femtet_control.fem is None:
347
+ raise PreventUpdate
348
+
349
+ # check the corresponding between history and Femtet
350
+ # ├ history-side
351
+ kwargs = self.femtet_control.create_femtet_interface_args()[0] # read metadata from history.
352
+ femprj_path_history_on_history: str or None = kwargs['femprj_path']
353
+ model_name_on_history: str or None = kwargs['model_name']
354
+ # ├ Femtet-side
355
+ Femtet = self.femtet_control.fem.Femtet
356
+ femprj_path: str = Femtet.Project # it can be '解析結果単体.femprj'
357
+ model_name: str = Femtet.AnalysisModelName
358
+ # └ check
359
+ is_same_femprj = (femprj_path == femprj_path_history_on_history) if femprj_path_history_on_history is not None else True
360
+ is_same_model = (model_name == model_name_on_history) if model_name is not None else True
361
+
362
+ # alert
363
+ new_alerts = current_alerts
364
+ if femprj_path_history_on_history is None:
365
+ msg = '.femprj file path of the history csv is invalid. Please certify matching between csv and opening .femprj file.'
366
+ new_alerts = self.alert_region.create_alerts(msg, 'warning', new_alerts)
367
+ else:
368
+ if not is_same_femprj:
369
+ msg = '.femprj file path of the history csv and opened in Femtet is inconsistent. Please certify matching between csv and .femprj file.'
370
+ new_alerts = self.alert_region.create_alerts(msg, 'warning', new_alerts)
371
+
372
+ if model_name_on_history is None:
373
+ msg = 'Analysis model name of the history csv is invalid. Please certify matching between csv and opening analysis model.'
374
+ new_alerts = self.alert_region.create_alerts(msg, 'warning', new_alerts)
375
+ else:
376
+ if not is_same_model:
377
+ msg = 'Analysis model name of the history csv and opened in Femtet is inconsistent. Please certify matching between csv and opening analysis model.'
378
+ new_alerts = self.alert_region.create_alerts(msg, 'warning', new_alerts)
379
+
380
+ if new_alerts == current_alerts:
381
+ raise PreventUpdate
382
+
383
+ return new_alerts
384
+
385
+
386
+ class Tutorial(AbstractPage):
387
+
388
+ # noinspection PyMissingConstructor
389
+ def __init__(self, home_page, main_graph, femtet_control):
390
+ self.home_page: HomePage = home_page
391
+ self.main_graph: MainGraph = main_graph
392
+ self.femtet_control: FemtetControl = femtet_control
393
+
394
+ # noinspection PyAttributeOutsideInit
395
+ def setup_component(self):
396
+
397
+ self.control_visibility_input_dummy = html.Div(hidden=True)
398
+
399
+ self.tutorial_state_prop = 'data-tutorial-state'
400
+ self.tutorial_state = html.Data(
401
+ id='tutorial-state',
402
+ **{self.tutorial_state_prop: dict(
403
+ is_tutorial=False,
404
+ has_history=None,
405
+ point_selected=None,
406
+ )}
407
+ )
408
+
409
+ # switch tutorial mode (always visible)
410
+ self.tutorial_mode_switch = dbc.Checklist(
411
+ ['tutorial mode'],
412
+ id='tutorial-mode-switch',
413
+ switch=True,
414
+ value=False
415
+ )
416
+
417
+ # load sample csv
418
+ self.load_sample_csv_badge = self.create_badge('Click Me!', 'load-sample-csv-badge')
419
+ self.load_sample_csv_button = dbc.Button(
420
+ children=['Load sample csv', self.load_sample_csv_badge],
421
+ id='load-sample-csv-button',
422
+ className="position-relative", # need to show badge
423
+ )
424
+ self.load_sample_csv_popover = dbc.Popover(
425
+ children=[
426
+ dbc.PopoverHeader('Load CSV'),
427
+ dbc.PopoverBody(
428
+ 'Open your optimization result. Then connecting to femtet will start automatically. '
429
+ 'Note that in tutorial mode, this button loads the ready-made sample csv and open sample femprj.'
430
+ ),
431
+ ],
432
+ id='load-sample-csv-popover',
433
+ target=self.load_sample_csv_button.id,
434
+ is_open=False,
435
+ placement='top',
436
+ )
437
+ self.load_sample_csv_div = html.Span(
438
+ children=[self.load_sample_csv_button, self.load_sample_csv_popover],
439
+ id='load-sample-csv-div',
440
+ style={'display': 'none'},
441
+ )
442
+
443
+ # popover and badge of main graph
444
+ self.graph_badge = self.create_badge('Choose a Point!', 'graph-badge')
445
+ self.graph_popover = dbc.Popover(
446
+ children=[
447
+ dbc.PopoverHeader('Main Graph'),
448
+ dbc.PopoverBody(
449
+ children=[
450
+ 'Here the optimization history is shown. '
451
+ 'Each plot represents single FEM result. '
452
+ 'You can pick a result to open the corresponding result in Femtet. ',
453
+ self.graph_badge
454
+ ],
455
+ className="position-relative", # need to show badge
456
+ ),
457
+ ],
458
+ id='graph-popover',
459
+ target=self.main_graph.tabs.id,
460
+ is_open=False,
461
+ placement='bottom',
462
+ )
463
+
464
+ # popover and badge of open pdt
465
+ self.open_pdt_badge = self.create_badge('Click Me!', 'open-pdt-badge')
466
+ self.open_pdt_popover = dbc.Popover(
467
+ children=[
468
+ dbc.PopoverHeader('Open Result'),
469
+ dbc.PopoverBody(
470
+ 'After pick a point in the main graph, '
471
+ 'This button shows the corresponding FEM result in Femtet.'
472
+ ),
473
+ ],
474
+ id='open-pdt-popover',
475
+ target=self.home_page.open_pdt_button.id,
476
+ is_open=False,
477
+ placement='top',
478
+ )
479
+ # popover of connect-femtet
480
+ self.connect_femtet_popover = dbc.Popover(
481
+ children=[
482
+ dbc.PopoverBody('You can re-make connection to Femtet if it misses.'),
483
+ ],
484
+ id='connect-femtet-popover',
485
+ target=self.femtet_control.connect_femtet_button.id,
486
+ is_open=False,
487
+ placement='bottom',
488
+ )
489
+
490
+
491
+ def setup_layout(self):
492
+ pass
493
+
494
+ def setup_callback(self):
495
+ app = self.application.app
496
+
497
+
498
+ @app.callback(
499
+ Output(self.home_page.open_pdt_button, 'children'),
500
+ Input(self.home_page.location, self.home_page.location.Prop.pathname),
501
+ State(self.home_page.open_pdt_button, 'children'),
502
+ prevent_initial_call=False,
503
+ )
504
+ def add_badge_to_button_init(_, current_children):
505
+ children = current_children if isinstance(current_children, list) else [current_children]
506
+ if self.open_pdt_badge not in children:
507
+ children.append(self.open_pdt_badge)
508
+ return children
509
+
510
+ @app.callback(
511
+ Output(self.load_sample_csv_badge, 'style'), # switch visibility
512
+ Output(self.load_sample_csv_popover, 'is_open'), # switch visibility
513
+ Output(self.load_sample_csv_div, 'style'), # switch visibility
514
+ Output(self.home_page.file_picker, 'style'), # switch visibility
515
+ Output(self.graph_badge, 'style'), # switch visibility
516
+ Output(self.graph_popover, 'is_open'), # switch visibility
517
+ Output(self.open_pdt_badge, 'style'), # switch visibility
518
+ Output(self.open_pdt_popover, 'is_open'), # switch visibility
519
+ Output(self.connect_femtet_popover, 'is_open'), # switch visibility
520
+ Input(self.tutorial_mode_switch, 'value'),
521
+ Input(self.load_sample_csv_button, 'n_clicks'), # load button clicked
522
+ Input(self.main_graph.selection_data, self.main_graph.selection_data_property), # selection changed
523
+ Input(self.control_visibility_input_dummy, 'children'),
524
+ State(self.load_sample_csv_badge, 'style'), # switch visibility
525
+ State(self.load_sample_csv_div, 'style'), # switch visibility
526
+ State(self.home_page.file_picker, 'style'), # switch visibility
527
+ State(self.graph_badge, 'style'), # switch visibility
528
+ State(self.open_pdt_badge, 'style'), # switch visibility
529
+ prevent_initial_call=True,
530
+ )
531
+ def control_visibility(
532
+ is_tutorial,
533
+ _1,
534
+ selection_data,
535
+ _2,
536
+ load_sample_csv_badge_current_style,
537
+ load_sample_csv_div_current_style,
538
+ file_picker_current_style,
539
+ graph_badge_current_style,
540
+ open_pdt_badge_current_style,
541
+ ):
542
+ ret = {
543
+ (load_sample_csv_badge_style := 0): no_update,
544
+ (load_sample_csv_popover_visible := 1): no_update,
545
+ (load_sample_csv_div_style := 2): no_update,
546
+ (file_picker_style := 3): no_update,
547
+ (graph_badge_style := 4): no_update,
548
+ (graph_popover_visible := 5): no_update,
549
+ (open_pdt_badge_style := 6): no_update,
550
+ (open_pdt_popover_visible := 7): no_update,
551
+ (connect_femtet_popover_visible := 8): no_update,
552
+ }
553
+
554
+ # prevent unexpected update
555
+ if callback_context.triggered_id is None:
556
+ raise PreventUpdate
557
+
558
+ # ===== initialize =====
559
+ # non-tutorial component
560
+ ret[file_picker_style] = self.control_visibility_by_style(True, file_picker_current_style)
561
+ # tutorial component
562
+ ret[load_sample_csv_badge_style] = self.control_visibility_by_style(False, load_sample_csv_badge_current_style)
563
+ ret[load_sample_csv_popover_visible] = False
564
+ ret[load_sample_csv_div_style] = self.control_visibility_by_style(False, load_sample_csv_div_current_style)
565
+ ret[graph_badge_style] = self.control_visibility_by_style(False, graph_badge_current_style)
566
+ ret[graph_popover_visible] = False
567
+ ret[open_pdt_badge_style] = self.control_visibility_by_style(False, open_pdt_badge_current_style)
568
+ ret[open_pdt_popover_visible] = False
569
+ ret[connect_femtet_popover_visible] = False
570
+
571
+ # if not tutorial, disable all anyway
572
+ if not is_tutorial:
573
+ return tuple(ret.values())
574
+
575
+ # else, visible popover
576
+ else:
577
+ ret[file_picker_style] = self.control_visibility_by_style(False, file_picker_current_style)
578
+ ret[load_sample_csv_div_style] = self.control_visibility_by_style(True, load_sample_csv_div_current_style)
579
+ ret[load_sample_csv_popover_visible] = True
580
+ ret[graph_popover_visible] = True
581
+ ret[connect_femtet_popover_visible] = True
582
+ ret[open_pdt_popover_visible] = True
583
+
584
+ # if history is None, show badge to load csv
585
+ if self.application.history is None:
586
+ ret[load_sample_csv_badge_style] = self.control_visibility_by_style(
587
+ True,
588
+ load_sample_csv_badge_current_style
589
+ )
590
+
591
+ # if history is not None,
592
+ else:
593
+ # if a point is already selected, show badge to open-pdt
594
+ if self.check_point_selected(selection_data):
595
+ ret[open_pdt_badge_style] = self.control_visibility_by_style(
596
+ True,
597
+ open_pdt_badge_current_style,
598
+ )
599
+
600
+ # selection not yet, show badge to main-graph
601
+ else:
602
+ ret[graph_badge_style] = self.control_visibility_by_style(
603
+ True,
604
+ graph_badge_current_style,
605
+ )
606
+
607
+ return tuple(ret.values())
608
+
609
+ # ===== load sample csv =====
610
+ alert_region = self.home_page.alert_region.alert_region
611
+
612
+ @app.callback(
613
+ Output(self.main_graph.tabs, self.main_graph.tabs.Prop.active_tab, allow_duplicate=True),
614
+ Output(self.control_visibility_input_dummy, 'children'),
615
+ Output(self.femtet_control.connect_femtet_button, 'n_clicks', allow_duplicate=True),
616
+ Output(alert_region, 'children', allow_duplicate=True),
617
+ Input(self.load_sample_csv_button, 'n_clicks'),
618
+ State(self.main_graph.tabs, self.main_graph.tabs.Prop.active_tab),
619
+ State(alert_region, 'children'),
620
+ prevent_initial_call=True,
621
+ )
622
+ def load_sample_csv(_, active_tab, current_alerts):
623
+ """Load sample csv"""
624
+ if callback_context.triggered_id is None:
625
+ raise PreventUpdate
626
+
627
+ path = os.path.join(os.path.dirname(__file__), 'tutorial', 'tutorial.csv')
628
+
629
+ if not os.path.exists(path):
630
+ msg = 'Sample csv is not found. Please consider to re-install pyfemtet by `py -m pip install pyfemtet -U --force-reinstall`'
631
+ alerts = self.home_page.alert_region.create_alerts(msg, color='danger', current_alerts=current_alerts)
632
+ return no_update, no_update, no_update, alerts
633
+ self.application.history = History(path)
634
+
635
+ source_file = path.replace('tutorial.csv', 'wat_ex14_parametric.femprj')
636
+ if not os.path.exists(source_file):
637
+ msg = 'Sample femprj file is not found. Please consider to re-install pyfemtet by `py -m pip install pyfemtet -U --force-reinstall`'
638
+ alerts = self.home_page.alert_region.create_alerts(msg, color='danger', current_alerts=current_alerts)
639
+ return no_update, no_update, no_update, alerts
640
+ destination_file = path.replace('tutorial.csv', 'tutorial.femprj')
641
+ shutil.copyfile(source_file, destination_file)
642
+
643
+ source_folder = path.replace('tutorial.csv', 'wat_ex14_parametric.Results')
644
+ if not os.path.exists(source_file):
645
+ msg = 'Sample femprj result folder is not found. Please consider to re-install pyfemtet by `py -m pip install pyfemtet -U --force-reinstall`'
646
+ alerts = self.home_page.alert_region.create_alerts(msg, color='danger', current_alerts=current_alerts)
647
+ return no_update, no_update, no_update, alerts
648
+ destination_folder = path.replace('tutorial.csv', 'tutorial.Results')
649
+ shutil.copytree(source_folder, destination_folder, dirs_exist_ok=True)
650
+
651
+ self.application.history.metadata[0] = json.dumps(
652
+ dict(
653
+ femprj_path=destination_file,
654
+ model_name='Ex14',
655
+ )
656
+ )
657
+
658
+ return active_tab, 1, 1, no_update
659
+
660
+ @staticmethod
661
+ def check_point_selected(data):
662
+ if data is None:
663
+ return False
664
+ if len(data['points']) == 0:
665
+ return False
666
+ return True
667
+
668
+ @staticmethod
669
+ def create_badge(text, id):
670
+ badge = dbc.Badge(
671
+ children=text,
672
+ color="danger",
673
+ pill=True,
674
+ text_color="white",
675
+ style={'display': 'none'},
676
+ className="position-absolute top-0 start-100 translate-middle",
677
+ id=id,
678
+ )
679
+ return badge
680
+
681
+ @staticmethod
682
+ def control_visibility_by_style(visible: bool, current_style: dict):
683
+
684
+ visibility = 'inline' if visible else 'none'
685
+ part = {'display': visibility}
686
+
687
+ if current_style is None:
688
+ return part
689
+
690
+ else:
691
+ current_style.update(part)
692
+ return current_style