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
pyfemtet/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.4.10"
1
+ __version__ = "0.4.11"
pyfemtet/opt/_femopt.py CHANGED
@@ -13,7 +13,7 @@ from dask.distributed import LocalCluster, Client
13
13
  # pyfemtet relative
14
14
  from pyfemtet.opt.interface import FEMInterface, FemtetInterface
15
15
  from pyfemtet.opt.opt import AbstractOptimizer, OptunaOptimizer
16
- from pyfemtet.opt.visualization._monitor import ProcessMonitorApp
16
+ from pyfemtet.opt.visualization.process_monitor.application import main as process_monitor_main
17
17
  from pyfemtet.opt._femopt_core import (
18
18
  _check_bound,
19
19
  _is_access_gogh,
@@ -574,6 +574,12 @@ def _start_monitor_server(
574
574
  host=None,
575
575
  port=None,
576
576
  ):
577
- monitor = ProcessMonitorApp(history, status, worker_addresses, worker_status_list)
578
- monitor.start_server(host, port)
577
+ process_monitor_main(
578
+ history,
579
+ status,
580
+ worker_addresses,
581
+ worker_status_list,
582
+ host,
583
+ port,
584
+ )
579
585
  return 'Exit monitor server process gracefully'
@@ -3,15 +3,19 @@ from typing import Optional, List
3
3
  import os
4
4
  import sys
5
5
  from time import sleep, time
6
- import signal
7
6
 
8
7
  import pandas as pd
9
8
  import psutil
10
9
  from dask.distributed import get_worker
11
10
 
12
- from pywintypes import com_error
11
+ # noinspection PyUnresolvedReferences
12
+ from pywintypes import com_error, error
13
+ # noinspection PyUnresolvedReferences
13
14
  from pythoncom import CoInitialize, CoUninitialize
15
+ # noinspection PyUnresolvedReferences
14
16
  from win32com.client import constants
17
+ import win32con
18
+ import win32gui
15
19
  from femtetutils import util
16
20
 
17
21
  from pyfemtet.core import (
@@ -31,6 +35,10 @@ from pyfemtet.dispatch_extensions import (
31
35
  from pyfemtet.opt.interface import FEMInterface, logger
32
36
 
33
37
 
38
+ def post_activate_message(hwnd):
39
+ win32gui.PostMessage(hwnd, win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0)
40
+
41
+
34
42
  class FemtetInterface(FEMInterface):
35
43
  """Concrete class for the interface with Femtet.
36
44
 
@@ -113,7 +121,7 @@ class FemtetInterface(FEMInterface):
113
121
  if self.original_femprj_path is None:
114
122
  # dask worker でなければ original のはず
115
123
  try:
116
- worker = get_worker()
124
+ _ = get_worker()
117
125
  except ValueError:
118
126
  self.original_femprj_path = self.femprj_path
119
127
 
@@ -252,7 +260,7 @@ class FemtetInterface(FEMInterface):
252
260
 
253
261
  """
254
262
 
255
- # FIXME: Gaudi へのアクセスなど、self.Femtet.Gaudi.somefunc() のような場合、この関数を呼び出す前に Gaudi へのアクセスの時点で com_error が起こる
263
+ # FIXME: Gaudi へのアクセスなど、self.Femtet.Gaudi.SomeFunc() のような場合、この関数を呼び出す前に Gaudi へのアクセスの時点で com_error が起こる
256
264
  # FIXME: => 文字列で渡して eval() すればよい。
257
265
 
258
266
  if args is None:
@@ -282,9 +290,16 @@ class FemtetInterface(FEMInterface):
282
290
 
283
291
  # API を実行
284
292
  try:
285
- returns = fun(*args, **kwargs)
286
- except com_error:
293
+ # 解析結果を開いた状態で Gaudi.Activate して ReExecute する場合、ReExecute の前後にアクティブ化イベントが必要
294
+ if fun.__name__ == 'ReExecute':
295
+ post_activate_message(self.Femtet.hWnd) # can raise pywintypes.error
296
+ returns = fun(*args, **kwargs)
297
+ post_activate_message(self.Femtet.hWnd)
298
+ else:
299
+ returns = fun(*args, **kwargs)
300
+ except (com_error, error):
287
301
  # パターン 2 エラーが生じたことは確定なのでエラーが起こるよう returns を作る
302
+ # com_error ではなく error の場合はおそらく Femtet が落ちている
288
303
  if ret_for_check_idx is None:
289
304
  returns = return_value_if_failed
290
305
  else:
@@ -665,6 +680,13 @@ class FemtetInterface(FEMInterface):
665
680
  )
666
681
  return out
667
682
 
683
+ @staticmethod
684
+ def create_pdt_path(femprj_path, model_name, trial):
685
+ result_dir = femprj_path.replace('.femprj', '.Results')
686
+ pdt_path = os.path.join(result_dir, model_name + f'_trial{trial}.pdt')
687
+ return pdt_path
688
+
689
+ # noinspection PyMethodOverriding
668
690
  @staticmethod
669
691
  def postprocess_func(
670
692
  trial: int,
@@ -676,7 +698,7 @@ class FemtetInterface(FEMInterface):
676
698
  ):
677
699
  result_dir = original_femprj_path.replace('.femprj', '.Results')
678
700
  if pdt_file_content is not None:
679
- pdt_path = os.path.join(result_dir, model_name + f'_trial{trial}.pdt')
701
+ pdt_path = FemtetInterface.create_pdt_path(original_femprj_path, model_name, trial)
680
702
  with open(pdt_path, 'wb') as f:
681
703
  f.write(pdt_file_content)
682
704
 
@@ -710,13 +732,12 @@ class FemtetInterface(FEMInterface):
710
732
  jpg_path = os.path.join(result_dir, self.model_name + '.jpg')
711
733
 
712
734
  # モデル表示画面の設定
713
- self.Femtet.SetWindowSize(200, 200)
735
+ self.Femtet.SetWindowSize(600, 600)
714
736
  self.Femtet.Fit()
715
- self.Femtet.ViewNumeric.SetCoord(1, 1, 1)
716
737
 
717
738
  # ---モデルの画面を保存---
718
739
  self.Femtet.Redraw() # 再描画
719
- succeed = self.Femtet.SavePicture(jpg_path, 200, 200, 80)
740
+ succeed = self.Femtet.SavePicture(jpg_path, 600, 600, 80)
720
741
 
721
742
  self.Femtet.RedrawMode = True # 逐一の描画をオン
722
743
 
@@ -745,6 +766,7 @@ class _UnPicklableNoFEM(FemtetInterface):
745
766
  Femtet = None
746
767
  quit_when_destruct = False
747
768
 
769
+ # noinspection PyMissingConstructor
748
770
  def __init__(self):
749
771
  CoInitialize()
750
772
  self.unpicklable_member = Dispatch('FemtetMacro.Femtet')
@@ -1,36 +1,15 @@
1
+ from femtetutils import util
1
2
  from pyfemtet.dispatch_extensions import _get_pid
2
3
 
3
- import winreg
4
4
  import ctypes
5
5
 
6
6
 
7
- def _get_dll(Femtet):
8
- femtet_major_version = _get_femtet_major_version(Femtet)
9
- dll_path = _get_parametric_dll_path(femtet_major_version)
7
+ def _get_dll():
8
+ femtet_exe_path = util.get_femtet_exe_path()
9
+ dll_path = femtet_exe_path.replace('Femtet.exe', 'ParametricIF.dll')
10
10
  return ctypes.cdll.LoadLibrary(dll_path)
11
11
 
12
12
 
13
- def _get_femtet_major_version(Femtet):
14
- from pyfemtet.core import _version
15
- version_int8 = _version(Femtet=Femtet)
16
- return str(version_int8)[0:4]
17
-
18
-
19
- def _get_parametric_dll_path(femtet_major_version) -> str:
20
- key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\FemtetInfo\InstallVersion\x64')
21
- _, nValues, _ = winreg.QueryInfoKey(key)
22
- for i in range(nValues):
23
- name, data, _ = winreg.EnumValue(key, i)
24
- if name == str(femtet_major_version):
25
- winreg.CloseKey(key)
26
- import os
27
- dll_path = os.path.join(data, 'Program', 'ParametricIF.dll')
28
- return dll_path
29
- # ここまで来ていたら失敗
30
- winreg.CloseKey(key)
31
- raise RuntimeError('パラメトリック解析機能へのアクセスに失敗しました')
32
-
33
-
34
13
  def _get_dll_with_set_femtet(Femtet):
35
14
  dll = _get_dll(Femtet)
36
15
  pid = _get_pid(Femtet.hWnd)
@@ -40,6 +19,7 @@ def _get_dll_with_set_femtet(Femtet):
40
19
 
41
20
 
42
21
  def _get_prm_result_names(Femtet):
22
+ """Used by pyfemtet-opt-gui"""
43
23
  out = []
44
24
 
45
25
  # load dll and set target femtet
pyfemtet/opt/opt/_base.py CHANGED
@@ -65,10 +65,15 @@ class AbstractOptimizer(ABC):
65
65
 
66
66
  # x の更新
67
67
  self.parameters['value'] = x
68
+ logger.info(f'Start calculation with input: {x}')
68
69
 
69
70
  # FEM の更新
70
71
  logger.debug('fem.update() start')
71
- self.fem.update(self.parameters)
72
+ try:
73
+ self.fem.update(self.parameters)
74
+ except Exception as e:
75
+ logger.warning('An exception has occurred during FEM update.')
76
+ raise e
72
77
 
73
78
  # y, _y, c の更新
74
79
  logger.debug('calculate y start')
@@ -93,6 +98,9 @@ class AbstractOptimizer(ABC):
93
98
  )
94
99
 
95
100
  logger.debug('history.record end')
101
+
102
+ logger.info(f'End calculation with output: {_y}')
103
+
96
104
  return np.array(y), np.array(_y), np.array(c)
97
105
 
98
106
  def _reconstruct_fem(self, skip_reconstruct=False):
@@ -18,6 +18,10 @@ from pyfemtet.core import MeshError, ModelError, SolveError
18
18
  # filter warnings
19
19
  import warnings
20
20
  from optuna.exceptions import ExperimentalWarning
21
+
22
+
23
+ optuna.logging.set_verbosity(optuna.logging.ERROR)
24
+
21
25
  warnings.filterwarnings('ignore', category=ExperimentalWarning)
22
26
 
23
27
 
@@ -1,7 +0,0 @@
1
- from pyfemtet.opt.visualization._monitor import ResultViewerApp
2
- from pyfemtet.opt.visualization._monitor import ProcessMonitorApp
3
-
4
- __all__ = [
5
- 'ResultViewerApp',
6
- 'ProcessMonitorApp',
7
- ]
@@ -0,0 +1,93 @@
1
+ """Create autocompletable components"""
2
+ import os
3
+ import inspect
4
+
5
+ from dash.development.base_component import Component
6
+
7
+
8
+ # noinspection PyUnresolvedReferences
9
+ from dash import html, dcc, dash_table
10
+ # noinspection PyUnresolvedReferences
11
+ import dash_bootstrap_components as dbc
12
+
13
+
14
+ here, me = os.path.split(__file__)
15
+ COMPONENT_FILE_DIR = os.path.join(here, 'wrapped_components')
16
+ indent = ' '
17
+
18
+
19
+ def create(module_name: str) -> str:
20
+ header = '''# auto created module
21
+ from pyfemtet.opt.visualization.wrapped_components.str_enum import StrEnum
22
+ # from enum import StrEnum
23
+ import dash
24
+ import dash_bootstrap_components
25
+
26
+
27
+ '''
28
+ path = os.path.join(COMPONENT_FILE_DIR, module_name.replace('.py', '') + '.py')
29
+ with open(path, 'w', newline='\n') as f:
30
+ f.write(header)
31
+ return path
32
+
33
+
34
+ def append(html_class, module_path: str):
35
+ print('==========')
36
+ print(html_class)
37
+
38
+ # get property names
39
+ init_signature = inspect.signature(html_class.__init__)
40
+ props = [param.name for param in init_signature.parameters.values() if (param.name != 'self') and (param.name != 'args') and (param.name != 'kwargs')]
41
+
42
+ # create class definition
43
+ class_definition = f'class {html_class.__name__}({html_class.__module__}):\n' # library specific
44
+
45
+ # create id property
46
+ class_definition += indent + 'def _dummy(self):\n'
47
+ class_definition += indent*2 + '# noinspection PyAttributeOutsideInit\n'
48
+ class_definition += indent*2 + 'self.id = None\n\n'
49
+
50
+ # create Prop attribute
51
+ class_definition += indent + 'class Prop(StrEnum):\n'
52
+ for available_property in props:
53
+ property_definition = f'{available_property} = "{available_property}"'
54
+ try:
55
+ exec(property_definition)
56
+ except (SyntaxError, TypeError):
57
+ continue
58
+ class_definition += indent*2 + f'{property_definition}\n'
59
+
60
+ if class_definition.endswith(':\n'):
61
+ return
62
+
63
+ class_definition += '\n\n'
64
+
65
+ with open(module_path, 'a', newline='\n') as f:
66
+ f.write(class_definition)
67
+
68
+
69
+ def sub(module_name):
70
+ # glob component classes
71
+ module_path = create(module_name)
72
+ class_names = dir(eval(module_name))
73
+ for class_name in class_names:
74
+ cls = eval(f'{module_name}.{class_name}')
75
+ if not inspect.isclass(cls):
76
+ continue
77
+ if issubclass(cls, Component):
78
+ append(cls, module_path)
79
+
80
+
81
+ def main():
82
+ # html
83
+ sub('html')
84
+
85
+ # dcc
86
+ sub('dcc')
87
+
88
+ # dbc
89
+ sub('dbc')
90
+
91
+
92
+ if __name__ == '__main__':
93
+ main()
@@ -0,0 +1,254 @@
1
+ # type hint
2
+ from dash.development.base_component import Component
3
+
4
+ # application
5
+ from dash import Dash
6
+ import webbrowser
7
+
8
+ # callback
9
+ from dash import Output, Input # , State, no_update, callback_context
10
+ # from dash.exceptions import PreventUpdate
11
+
12
+ # components
13
+ # from dash import html, dcc
14
+ import dash_bootstrap_components
15
+
16
+ from pyfemtet.opt._femopt_core import History
17
+ from pyfemtet.opt.visualization.wrapped_components import html, dcc, dbc
18
+
19
+ # the others
20
+ from abc import ABC, abstractmethod
21
+ import logging
22
+ import psutil
23
+ from pyfemtet.logger import get_logger
24
+
25
+
26
+ dash_logger = logging.getLogger('werkzeug')
27
+ dash_logger.setLevel(logging.ERROR)
28
+
29
+ logger = get_logger('viewer')
30
+ logger.setLevel(logging.ERROR)
31
+
32
+
33
+ class AbstractPage(ABC):
34
+ """Define content."""
35
+ """
36
+
37
+ =================
38
+ |~:8080/rel_url | <---- page.rel_url
39
+ =================
40
+ page. | | -------- |
41
+ title -->home| | | |
42
+ | | | <-------- sub_page.layout
43
+ | | -------- |
44
+ | ^ | |<--- page.layout
45
+ ==|==============
46
+ |
47
+ sidebar
48
+
49
+ """
50
+ def __init__(self, title='base-page', rel_url='/', application=None):
51
+ self.layout: Component = None
52
+ self.rel_url = rel_url
53
+ self.title = title
54
+ self.application: PyFemtetApplicationBase = application
55
+ self.subpages = []
56
+ self.setup_component()
57
+ self.setup_layout()
58
+
59
+ def add_subpage(self, subpage: 'AbstractPage'):
60
+ subpage.setup_component()
61
+ subpage.setup_layout()
62
+ self.subpages.append(subpage)
63
+
64
+ def set_application(self, app):
65
+ self.application = app
66
+
67
+ @abstractmethod
68
+ def setup_component(self):
69
+ # noinspection PyAttributeOutsideInit
70
+ self._component = html.Div('This is a abstract page.')
71
+
72
+ @abstractmethod
73
+ def setup_layout(self):
74
+ self.layout = self._component
75
+
76
+ def setup_callback(self):
77
+ # app = self.application.app
78
+
79
+ for subpage in self.subpages:
80
+ subpage.set_application(self.application)
81
+ subpage.setup_callback()
82
+
83
+ # @app.callback(...)
84
+ # def do_something():
85
+ # return ...
86
+
87
+
88
+ def _unused_port_number(start=49152):
89
+ # "LISTEN" 状態のポート番号をリスト化
90
+ used_ports = [conn.laddr.port for conn in psutil.net_connections() if conn.status == 'LISTEN']
91
+ port = start
92
+ for port in range(start, 65535 + 1):
93
+ # 未使用のポート番号ならreturn
94
+ if port not in set(used_ports):
95
+ break
96
+ if port != start:
97
+ logger.warning(f'Specified port "{start}" seems to be used. Port "{port}" is used instead.')
98
+ return port
99
+
100
+
101
+ class SidebarApplicationBase:
102
+ """"""
103
+ """ Define entire layout and callback.
104
+ +------+--------+
105
+ | side | con- |
106
+ | bar | tent |
107
+ +------+--------+
108
+ │ └─ pages (dict(href: str = layout: Component))
109
+ └──────── nav_links (dict(order: float) = NavLink)
110
+ """
111
+
112
+ # port
113
+ DEFAULT_PORT = 49152
114
+
115
+ # members for sidebar application
116
+ SIDEBAR_STYLE = {
117
+ "position": "fixed",
118
+ "top": 0,
119
+ "left": 0,
120
+ "bottom": 0,
121
+ "width": "16rem",
122
+ "padding": "2rem 1rem",
123
+ "background-color": "#f8f9fa",
124
+ }
125
+ CONTENT_STYLE = {
126
+ "margin-left": "18rem",
127
+ "margin-right": "2rem",
128
+ "padding": "2rem 1rem",
129
+ }
130
+
131
+ def __init__(self, title=None, subtitle=None):
132
+ # define app
133
+ self.title = title if title is not None else 'App'
134
+ self.subtitle = subtitle if title is not None else ''
135
+ self.app = Dash(
136
+ __name__,
137
+ external_stylesheets=[dash_bootstrap_components.themes.BOOTSTRAP],
138
+ title=title,
139
+ update_title=None,
140
+ )
141
+ self.pages = dict()
142
+ self.nav_links = dict()
143
+ self.page_objects = []
144
+
145
+ def add_page(self, page: AbstractPage, order: int = None):
146
+ page.set_application(self)
147
+ self.page_objects.append(page)
148
+ self.pages[page.rel_url] = page.layout
149
+ if order is None:
150
+ order = len(self.pages)
151
+ self.nav_links[order] = dbc.NavLink(page.title, href=page.rel_url, active="exact")
152
+
153
+ def setup_callback(self):
154
+ for page in self.page_objects:
155
+ page.setup_callback()
156
+
157
+ def _setup_layout(self):
158
+ # setup sidebar
159
+ # https://dash-bootstrap-components.opensource.faculty.ai/examples/simple-sidebar/
160
+
161
+ # sidebar に表示される順に並び替え
162
+ ordered_items = sorted(self.nav_links.items(), key=lambda x: x[0])
163
+ ordered_links = [value for key, value in ordered_items]
164
+
165
+ # sidebar と contents から app 全体の layout を作成
166
+ sidebar = html.Div(
167
+ [
168
+ html.H2(self.title, className='display-4'),
169
+ html.Hr(),
170
+ html.P(self.subtitle, className='lead'),
171
+ dbc.Nav(ordered_links, vertical=True, pills=True),
172
+ ],
173
+ style=self.SIDEBAR_STYLE,
174
+ )
175
+ content = html.Div(id="page-content", style=self.CONTENT_STYLE)
176
+ self.app.layout = html.Div([dcc.Location(id="url"), sidebar, content])
177
+
178
+ # sidebar によるページ遷移のための callback
179
+ @self.app.callback(Output(content.id, "children"), [Input("url", "pathname")])
180
+ def switch_page_content(pathname):
181
+ if pathname in list(self.pages.keys()):
182
+ return self.pages[pathname]
183
+
184
+ else:
185
+ return html.Div(
186
+ [
187
+ html.H1("404: Not found", className="text-danger"),
188
+ html.Hr(),
189
+ html.P(f"The pathname {pathname} was not recognised..."),
190
+ ],
191
+ className="p-3 bg-light rounded-3",
192
+ )
193
+
194
+ def run(self, host='localhost', port=None, debug=False):
195
+ self._setup_layout()
196
+ port = port or self.DEFAULT_PORT
197
+ # port を検証
198
+ port = _unused_port_number(port)
199
+ # ブラウザを起動
200
+ if host == '0.0.0.0':
201
+ webbrowser.open(f'http://localhost:{str(port)}')
202
+ else:
203
+ webbrowser.open(f'http://{host}:{str(port)}')
204
+ self.app.run(debug=debug, host=host, port=port)
205
+
206
+
207
+ class PyFemtetApplicationBase(SidebarApplicationBase):
208
+ """"""
209
+ """
210
+ +------+--------+
211
+ | side | con- |
212
+ | bar | tent |
213
+ +--^---+--^-----+
214
+ │ └─ pages (dict(href: str = layout: Component))
215
+ └──────── nav_links (dict(order: float) = NavLink)
216
+
217
+ Accessible members:
218
+ - history: History
219
+ └ local_df: pd.DataFrame
220
+ - app: Dash
221
+
222
+ """
223
+
224
+ def __init__(
225
+ self,
226
+ title=None,
227
+ subtitle=None,
228
+ history: History = None,
229
+ ):
230
+ # register arguments
231
+ self.history = history # include actor
232
+ super().__init__(title, subtitle)
233
+
234
+
235
+ def check_page_layout(page_cls: type):
236
+ home_page = page_cls() # required
237
+ application = PyFemtetApplicationBase(title='test-app')
238
+ application.add_page(home_page, 0)
239
+ application.run(debug=True)
240
+
241
+
242
+ if __name__ == '__main__':
243
+
244
+ # template
245
+ g_home_page = AbstractPage('home-page') # required
246
+ g_page1 = AbstractPage('page-1', '/page-1')
247
+
248
+ g_application = SidebarApplicationBase(title='test-app')
249
+ g_application.add_page(g_home_page, 0)
250
+ g_application.add_page(g_page1, 1)
251
+
252
+ # g_application.setup_callback()
253
+
254
+ g_application.run(debug=True)
@@ -0,0 +1,71 @@
1
+ # type hint
2
+ from typing import List
3
+ from dash.development.base_component import Component
4
+
5
+ # callback
6
+ from dash import Output, Input, State, no_update, callback_context
7
+ from dash.exceptions import PreventUpdate
8
+
9
+ # components
10
+ from pyfemtet.opt.visualization.wrapped_components import html, dcc, dbc
11
+
12
+ from pyfemtet.opt.visualization.base import AbstractPage, logger
13
+
14
+
15
+ class AlertRegion(AbstractPage):
16
+
17
+ def setup_component(self):
18
+ # alert
19
+ # noinspection PyAttributeOutsideInit
20
+ self.alert_region = dbc.CardBody(children=[], id='alerts-card-body')
21
+
22
+ # clear alert
23
+ # noinspection PyAttributeOutsideInit
24
+ self.clear_alert_button = dbc.Button(
25
+ children='Clear messages',
26
+ id='clear-messages-button',
27
+ color='secondary',
28
+ outline=True,
29
+ className="position-relative",
30
+ )
31
+
32
+ def setup_layout(self):
33
+ self.layout = dbc.Card(
34
+ [
35
+ dbc.CardHeader(
36
+ children=self.clear_alert_button,
37
+ className='d-flex', # align right
38
+ style={'justify-content': 'end'}, # align right
39
+ ),
40
+ self.alert_region,
41
+ ]
42
+ )
43
+
44
+ def setup_callback(self):
45
+ app = self.application.app
46
+
47
+ # ===== clear alerts ==-==
48
+ @app.callback(
49
+ Output(self.alert_region.id, 'children', allow_duplicate=True),
50
+ Input(self.clear_alert_button.id, self.clear_alert_button.Prop.n_clicks),
51
+ prevent_initial_call=True, # required if allow_duplicate=True
52
+ )
53
+ def clear_alerts(_):
54
+ return []
55
+
56
+ def create_alerts(self, msg, color='secondary', current_alerts=None) -> List[Component]:
57
+
58
+ if current_alerts is None:
59
+ current_alerts = []
60
+
61
+ new_alert = dbc.Alert(
62
+ msg,
63
+ id=f'alert-{len(current_alerts) + 1}',
64
+ dismissable=True,
65
+ color=color,
66
+ )
67
+
68
+ new_alerts = [new_alert]
69
+ new_alerts.extend(current_alerts)
70
+
71
+ return new_alerts