pyfemtet 0.4.25__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyfemtet might be problematic. Click here for more details.

Files changed (117) hide show
  1. pyfemtet/__init__.py +1 -1
  2. pyfemtet/message/locales/ja/LC_MESSAGES/messages.mo +0 -0
  3. pyfemtet/message/locales/ja/LC_MESSAGES/messages.po +114 -62
  4. pyfemtet/message/locales/messages.pot +114 -62
  5. pyfemtet/message/messages.py +6 -2
  6. pyfemtet/opt/__init__.py +2 -2
  7. pyfemtet/opt/_femopt.py +27 -46
  8. pyfemtet/opt/_femopt_core.py +50 -33
  9. pyfemtet/opt/_test_utils/__init__.py +0 -0
  10. pyfemtet/opt/_test_utils/control_femtet.py +45 -0
  11. pyfemtet/opt/_test_utils/hyper_sphere.py +24 -0
  12. pyfemtet/opt/_test_utils/record_history.py +72 -0
  13. pyfemtet/opt/interface/_femtet.py +39 -1
  14. pyfemtet/opt/optimizer/__init__.py +12 -0
  15. pyfemtet/opt/{opt → optimizer}/_base.py +30 -9
  16. pyfemtet/opt/{opt → optimizer}/_optuna.py +14 -33
  17. pyfemtet/opt/optimizer/_optuna_botorchsampler_parameter_constraint_helper.py +325 -0
  18. pyfemtet/opt/{opt → optimizer}/_scipy.py +10 -6
  19. pyfemtet/opt/{opt → optimizer}/_scipy_scalar.py +24 -12
  20. pyfemtet/opt/samples/femprj_sample/constrained_pipe.femprj +0 -0
  21. pyfemtet/opt/samples/femprj_sample/constrained_pipe.py +97 -0
  22. pyfemtet/opt/samples/femprj_sample/constrained_pipe_test_result.reccsv +13 -0
  23. pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gal_ex58_parametric_test_result.reccsv +1 -1
  24. pyfemtet/opt/{femprj_sample → samples/femprj_sample}/her_ex40_parametric.py +10 -8
  25. pyfemtet/opt/samples/femprj_sample/her_ex40_parametric_test_result.reccsv +18 -0
  26. pyfemtet/opt/samples/femprj_sample_jp/constrained_pipe_jp.py +93 -0
  27. pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/her_ex40_parametric_jp.py +10 -8
  28. pyfemtet/opt/visualization/complex_components/main_figure_creator.py +69 -0
  29. pyfemtet/opt/visualization/complex_components/main_graph.py +299 -14
  30. pyfemtet/opt/visualization/process_monitor/pages.py +1 -1
  31. pyfemtet/opt/visualization/result_viewer/application.py +1 -1
  32. pyfemtet/opt/visualization/result_viewer/pages.py +9 -9
  33. {pyfemtet-0.4.25.dist-info → pyfemtet-0.5.1.dist-info}/METADATA +1 -1
  34. pyfemtet-0.5.1.dist-info/RECORD +115 -0
  35. pyfemtet/_test_util.py +0 -135
  36. pyfemtet/opt/femprj_sample/ParametricIF - True.femprj +0 -0
  37. pyfemtet/opt/femprj_sample/her_ex40_parametric_test_result.reccsv +0 -18
  38. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14.pdt +0 -0
  39. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial1.jpg +0 -0
  40. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial1.pdt +0 -0
  41. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial10.jpg +0 -0
  42. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial10.pdt +0 -0
  43. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial11.jpg +0 -0
  44. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial11.pdt +0 -0
  45. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial12.jpg +0 -0
  46. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial12.pdt +0 -0
  47. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial13.jpg +0 -0
  48. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial13.pdt +0 -0
  49. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial14.jpg +0 -0
  50. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial14.pdt +0 -0
  51. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial15.jpg +0 -0
  52. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial15.pdt +0 -0
  53. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial2.jpg +0 -0
  54. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial2.pdt +0 -0
  55. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial3.jpg +0 -0
  56. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial3.pdt +0 -0
  57. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial4.jpg +0 -0
  58. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial4.pdt +0 -0
  59. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial5.jpg +0 -0
  60. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial5.pdt +0 -0
  61. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial6.jpg +0 -0
  62. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial6.pdt +0 -0
  63. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial7.jpg +0 -0
  64. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial7.pdt +0 -0
  65. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial8.jpg +0 -0
  66. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial8.pdt +0 -0
  67. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial9.jpg +0 -0
  68. pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial9.pdt +0 -0
  69. pyfemtet/opt/opt/__init__.py +0 -12
  70. pyfemtet/opt/opt/_optuna_botorch_helper.py +0 -209
  71. pyfemtet-0.4.25.dist-info/RECORD +0 -140
  72. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/.gitignore +0 -0
  73. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/ParametricIF.femprj +0 -0
  74. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/ParametricIF.py +0 -0
  75. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/ParametricIF_test_result.reccsv +0 -0
  76. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_NX.femprj +0 -0
  77. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_NX.prt +0 -0
  78. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_NX.py +0 -0
  79. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_NX_test_result.reccsv +0 -0
  80. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_SW.SLDPRT +0 -0
  81. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_SW.femprj +0 -0
  82. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_SW.py +0 -0
  83. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_SW_test_result.reccsv +0 -0
  84. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gal_ex58_parametric.femprj +0 -0
  85. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gal_ex58_parametric.py +0 -0
  86. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gau_ex08_parametric.femprj +0 -0
  87. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gau_ex08_parametric.py +0 -0
  88. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gau_ex08_parametric_test_result.reccsv +0 -0
  89. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/her_ex40_parametric.femprj +0 -0
  90. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/paswat_ex1_parametric.femprj +0 -0
  91. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/paswat_ex1_parametric.py +0 -0
  92. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/paswat_ex1_parametric_parallel.py +0 -0
  93. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/paswat_ex1_parametric_test_result.reccsv +0 -0
  94. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/wat_ex14_parametric.femprj +0 -0
  95. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/wat_ex14_parametric.py +0 -0
  96. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/wat_ex14_parametric_parallel.py +0 -0
  97. /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/wat_ex14_parametric_test_result.reccsv +0 -0
  98. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/ParametricIF_jp.femprj +0 -0
  99. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/ParametricIF_jp.py +0 -0
  100. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/cad_ex01_NX_jp.femprj +0 -0
  101. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/cad_ex01_NX_jp.py +0 -0
  102. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/cad_ex01_SW_jp.femprj +0 -0
  103. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/cad_ex01_SW_jp.py +0 -0
  104. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/gal_ex58_parametric_jp.femprj +0 -0
  105. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/gal_ex58_parametric_jp.py +0 -0
  106. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/gau_ex08_parametric_jp.femprj +0 -0
  107. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/gau_ex08_parametric_jp.py +0 -0
  108. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/her_ex40_parametric_jp.femprj +0 -0
  109. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/paswat_ex1_parametric_jp.femprj +0 -0
  110. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/paswat_ex1_parametric_jp.py +0 -0
  111. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/paswat_ex1_parametric_parallel_jp.py +0 -0
  112. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/wat_ex14_parametric_jp.femprj +0 -0
  113. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/wat_ex14_parametric_jp.py +0 -0
  114. /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/wat_ex14_parametric_parallel_jp.py +0 -0
  115. {pyfemtet-0.4.25.dist-info → pyfemtet-0.5.1.dist-info}/LICENSE +0 -0
  116. {pyfemtet-0.4.25.dist-info → pyfemtet-0.5.1.dist-info}/WHEEL +0 -0
  117. {pyfemtet-0.4.25.dist-info → pyfemtet-0.5.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,93 @@
1
+ """拘束付き最適化を実装するサンプル。
2
+
3
+ このセクションでは、拘束の種類と、拘束を必要とするモデルで
4
+ 最適化を実行する手順について説明します。
5
+
6
+ """
7
+
8
+ from optuna_integration import BoTorchSampler
9
+ from pyfemtet.opt import FEMOpt, OptunaOptimizer
10
+
11
+
12
+ def mises_stress(Femtet):
13
+ """フォンミーゼス応力を目的関数として計算します。
14
+
15
+ この関数は、最適化の実行中に FEMOpt オブジェクトによって
16
+ 自動的に呼び出されます。
17
+
18
+ 引数:
19
+ Femtet: PyFemtet を使用して目的関数または拘束関数を
20
+ 定義する場合、最初の引数は Femtet インスタンスを
21
+ 取る必要があります。
22
+
23
+ 戻り値:
24
+ float: 目的または拘束関数は単一の float を返すよう定義してください。
25
+ """
26
+ return Femtet.Gogh.Galileo.GetMaxStress_py()[2]
27
+
28
+
29
+ def radius_diff(Femtet, opt):
30
+ """パイプの外側の半径と内側の半径の差を計算します。
31
+
32
+ この拘束は、最適化の実行中にパイプの内側の半径が
33
+ 外側の半径を超えないようにするために呼び出されます。
34
+
35
+ 注意:
36
+ OptunaOptimizer の BoTorchSampler を使用していて、
37
+ strict な拘束を使用する場合、パラメータを提案するため
38
+ に繰り返し計算が必要になるため、Femtet へのアクセスが
39
+ 非常に遅くなる可能性があることに注意してください。
40
+ この関数の例のように、Femtet にアクセスするのではなく、
41
+ Optimizer オブジェクトを介してパラメータを取得して計算
42
+ を実行することをお勧めします。
43
+
44
+ 非推奨::
45
+
46
+ p = Femtet.GetVariableValue('p')
47
+
48
+ 代わりに::
49
+
50
+ params = opt.get_parameter()
51
+ p = params['p']
52
+
53
+ 引数:
54
+ Femtet: PyFemtet を使用して目的関数または拘束関数を
55
+ 定義する場合、最初の引数は Femtet インスタンスを
56
+ 取る必要があります。
57
+ opt: このオブジェクトを使用すると、Femtet を経由せず
58
+ に外側の半径と内側の半径の値を取得できます。
59
+ """
60
+ params = opt.get_parameter()
61
+ internal_r = params['internal_r']
62
+ external_r = params['external_r']
63
+ return external_r - internal_r
64
+
65
+
66
+ if __name__ == '__main__':
67
+ # 最適化手法のセットアップ
68
+ opt = OptunaOptimizer(
69
+ sampler_class=BoTorchSampler,
70
+ sampler_kwargs=dict(
71
+ n_startup_trials=3, # 最初の 3 回はランダムサンプリングを行います。
72
+ )
73
+ )
74
+ femopt = FEMOpt(opt=opt)
75
+
76
+ # 変数の追加
77
+ femopt.add_parameter("external_r", 10, lower_bound=0.1, upper_bound=10)
78
+ femopt.add_parameter("internal_r", 5, lower_bound=0.1, upper_bound=10)
79
+
80
+ # 最適化の実行中に外側の半径を超えないように strict 拘束を追加します。
81
+ femopt.add_constraint(
82
+ radius_diff, # 拘束関数 (ここでは 外半径 - 内半径).
83
+ name='管厚さ', # 拘束関数にはプログラム上の名前とは別に自由な名前を付与できます.
84
+ lower_bound=1, # 拘束関数の下限 (ここでは管の厚みを最低 1 とする).
85
+ args=(femopt.opt,) # 拘束関数に渡される、Femtet 以外の追加の引数.
86
+ )
87
+
88
+ # 目的関数の追加
89
+ femopt.add_objective(mises_stress, name='ミーゼス応力')
90
+
91
+ # 最適化の実行
92
+ femopt.set_random_seed(42)
93
+ femopt.optimize(n_trials=10)
@@ -80,7 +80,7 @@ class SParameterCalculator:
80
80
  return self.resonance_frequency # 単位: Hz
81
81
 
82
82
 
83
- def antenna_is_smaller_than_substrate(Femtet):
83
+ def antenna_is_smaller_than_substrate(Femtet, opt):
84
84
  """アンテナの大きさと基板の大きさの関係を計算します。
85
85
 
86
86
  この関数は、変数の更新によってモデル形状が破綻しないように
@@ -92,15 +92,17 @@ def antenna_is_smaller_than_substrate(Femtet):
92
92
  Returns:
93
93
  float: 基板エッジとアンテナエッジの間隙。1 mm 以上が必要です。
94
94
  """
95
- r = Femtet.GetVariableValue('antenna_radius')
96
- w = Femtet.GetVariableValue('substrate_w')
95
+ params = opt.get_parameter()
96
+ r = params['antenna_radius']
97
+ w = params['substrate_w']
97
98
  return w / 2 - r # 単位: mm
98
99
 
99
100
 
100
- def port_is_inside_antenna(Femtet):
101
+ def port_is_inside_antenna(Femtet, opt):
101
102
  """給電ポートの位置とアンテナの大きさの関係を計算します。"""
102
- r = Femtet.GetVariableValue('antenna_radius')
103
- x = Femtet.GetVariableValue('port_x')
103
+ params = opt.get_parameter()
104
+ r = params['antenna_radius']
105
+ x = params['port_x']
104
106
  return r - x # 単位: mm。1 mm 以上が必要です。
105
107
 
106
108
 
@@ -125,8 +127,8 @@ if __name__ == '__main__':
125
127
  femopt.add_parameter('port_x', 5, 1, 20)
126
128
 
127
129
  # 拘束関数を最適化問題に追加
128
- femopt.add_constraint(antenna_is_smaller_than_substrate, 'アンテナと基板エッジの間隙', lower_bound=1)
129
- femopt.add_constraint(port_is_inside_antenna, 'アンテナエッジと給電ポートの間隙', lower_bound=1)
130
+ femopt.add_constraint(antenna_is_smaller_than_substrate, 'アンテナと基板エッジの間隙', lower_bound=1, args=(opt,))
131
+ femopt.add_constraint(port_is_inside_antenna, 'アンテナエッジと給電ポートの間隙', lower_bound=1, args=(opt,))
130
132
 
131
133
  # 目的関数を最適化問題に追加
132
134
  # 共振周波数の目標は 3.0 GHz です。
@@ -1,3 +1,4 @@
1
+ import pandas as pd
1
2
  import plotly.graph_objs as go
2
3
  import plotly.express as px
3
4
 
@@ -200,3 +201,71 @@ def _get_multi_objective_pairplot(history, df):
200
201
  )
201
202
 
202
203
  return fig
204
+
205
+
206
+ def get_objective_plot(history: History, df: pd.DataFrame, obj_names: list[str]) -> go.Figure:
207
+ df = _ls.localize(df)
208
+ df.columns = [c.replace(' / ', '<BR>/ ') for c in df.columns]
209
+ obj_names = [o.replace(' / ', '<BR>/ ') for o in obj_names]
210
+
211
+ common_kwargs = dict(
212
+ color=_ls.non_domi['label'],
213
+ color_discrete_map={
214
+ _ls.non_domi[True]: _cs.non_domi[True],
215
+ _ls.non_domi[False]: _cs.non_domi[False],
216
+ },
217
+ symbol=_ls.feasible['label'],
218
+ symbol_map={
219
+ _ls.feasible[True]: _ss.feasible[True],
220
+ _ls.feasible[False]: _ss.feasible[False],
221
+ },
222
+ custom_data=['trial'],
223
+ category_orders={
224
+ _ls.feasible['label']: (_ls.feasible[False], _ls.feasible[True]),
225
+ _ls.non_domi['label']: (_ls.non_domi[False], _ls.non_domi[True]),
226
+ },
227
+ )
228
+
229
+ if len(obj_names) == 2:
230
+ fig = px.scatter(
231
+ data_frame=df,
232
+ x=obj_names[0],
233
+ y=obj_names[1],
234
+ **common_kwargs,
235
+ )
236
+ fig.update_layout(
237
+ dict(
238
+ xaxis_title=obj_names[0],
239
+ yaxis_title=obj_names[1],
240
+ )
241
+ )
242
+
243
+ elif len(obj_names) == 3:
244
+ fig = px.scatter_3d(
245
+ df,
246
+ x=obj_names[0],
247
+ y=obj_names[1],
248
+ z=obj_names[2],
249
+ **common_kwargs,
250
+ )
251
+ fig.update_layout(
252
+ margin=dict(l=0, r=0, b=0, t=30),
253
+ )
254
+ fig.update_traces(
255
+ marker=dict(
256
+ size=3,
257
+ ),
258
+ )
259
+
260
+ else:
261
+ raise Exception
262
+
263
+ fig.update_layout(
264
+ dict(
265
+ title_text="Objective plot",
266
+ )
267
+ )
268
+
269
+ fig.update_traces(hoverinfo="none", hovertemplate=None)
270
+
271
+ return fig
@@ -2,7 +2,7 @@
2
2
  from dash.development.base_component import Component
3
3
 
4
4
  # callback
5
- from dash import Output, Input, State, no_update, callback_context
5
+ from dash import Output, Input, State, no_update, callback_context, ALL
6
6
  from dash.exceptions import PreventUpdate
7
7
 
8
8
  # components
@@ -21,7 +21,7 @@ import json
21
21
  import numpy as np
22
22
 
23
23
  from pyfemtet.opt.visualization.complex_components import main_figure_creator
24
- from pyfemtet.opt.visualization.base import PyFemtetApplicationBase, AbstractPage, logger
24
+ from pyfemtet.opt.visualization.base import AbstractPage, logger
25
25
  from pyfemtet.message import Msg
26
26
 
27
27
 
@@ -45,12 +45,17 @@ class MainGraph(AbstractPage):
45
45
  | | | graph <-------- ToolTip
46
46
  | | +---------+ | |
47
47
  | +-------------+ |
48
+ +-----------------+
49
+ | | <- CardFooter (depends on tab)
48
50
  +=================+
49
51
 
50
52
  Data(data-selection)
51
53
 
52
54
  """
53
55
 
56
+ TAB_ID_OBJECTIVE_PLOT = 'tab-objectives-plot'
57
+ TAB_ID_HYPERVOLUME_PLOT = 'tab-hypervolume-plot'
58
+
54
59
  def __init__(self):
55
60
  self.setup_figure_creator()
56
61
  super().__init__()
@@ -60,12 +65,17 @@ class MainGraph(AbstractPage):
60
65
  # list[0] is the default tab
61
66
  self.figure_creators = [
62
67
  dict(
63
- tab_id='tab-objective-plot',
64
- label=Msg.TAB_LABEL_OBJECTIVES,
68
+ tab_id='tab-objectives-scatterplot',
69
+ label=Msg.TAB_LABEL_OBJECTIVE_SCATTERPLOT,
65
70
  creator=main_figure_creator.get_default_figure,
66
71
  ),
67
72
  dict(
68
- tab_id='tab-hypervolume-plot',
73
+ tab_id=self.TAB_ID_OBJECTIVE_PLOT,
74
+ label=Msg.TAB_LABEL_OBJECTIVE_PLOT,
75
+ creator=main_figure_creator.get_objective_plot,
76
+ ),
77
+ dict(
78
+ tab_id=self.TAB_ID_HYPERVOLUME_PLOT,
69
79
  label='Hypervolume',
70
80
  creator=main_figure_creator.get_hypervolume_plot,
71
81
  ),
@@ -76,13 +86,65 @@ class MainGraph(AbstractPage):
76
86
  self.location = dcc.Location(id='main-graph-location', refresh=True)
77
87
 
78
88
  # setup header
79
- self.tab_list = [dbc.Tab(label=d['label'], tab_id=d['tab_id']) for d in self.figure_creators]
89
+ tab_list = []
90
+ for d in self.figure_creators:
91
+ is_objective_plot = d['tab_id'] == self.TAB_ID_OBJECTIVE_PLOT # Objective plot tab is only shown in obj_names > 3. Set this via callback.
92
+ is_hypervolume_plot = d['tab_id'] == self.TAB_ID_HYPERVOLUME_PLOT # same above
93
+
94
+ if is_objective_plot or is_hypervolume_plot:
95
+ style = {'display': 'none'}
96
+ else:
97
+ style = None
98
+
99
+ tab_list.append(
100
+ dbc.Tab(
101
+ label=d['label'],
102
+ tab_id=d['tab_id'],
103
+ tab_style=style,
104
+ )
105
+ )
106
+ self.tab_list = tab_list
80
107
  self.tabs = dbc.Tabs(self.tab_list, id='main-graph-tabs')
81
108
  self.card_header = dbc.CardHeader(self.tabs)
82
109
 
83
110
  # setup body
84
111
  self.tooltip = dcc.Tooltip(id='main-graph-tooltip')
85
112
 
113
+ # setup footer components for objective plot
114
+ self.axis1_dropdown = dbc.DropdownMenu()
115
+ self.axis2_dropdown = dbc.DropdownMenu()
116
+ self.axis3_dropdown = dbc.DropdownMenu()
117
+ self.switch_3d = dbc.Checklist(
118
+ options=[
119
+ dict(
120
+ label="3D",
121
+ disabled=False,
122
+ value=False,
123
+ )
124
+ ],
125
+ switch=True,
126
+ value=[],
127
+ )
128
+ self.objective_plot_controller: html.Div = html.Div(
129
+ [
130
+ self.switch_3d,
131
+ dbc.Stack(
132
+ children=['X axis', self.axis1_dropdown],
133
+ direction='horizontal', gap=2),
134
+ dbc.Stack(
135
+ children=['Y axis', self.axis2_dropdown],
136
+ direction='horizontal', gap=2),
137
+ dbc.Stack(
138
+ children=['Z axis', self.axis3_dropdown],
139
+ direction='horizontal', gap=2),
140
+ ]
141
+ )
142
+
143
+ # setup footer
144
+ self.card_footer: dbc.CardFooter = dbc.CardFooter(
145
+ self.objective_plot_controller
146
+ )
147
+
86
148
  # set kwargs of Graph to reconstruct Graph in ProcessMonitor.
87
149
  self.graph_kwargs = dict(
88
150
  id='main-graph',
@@ -127,6 +189,7 @@ class MainGraph(AbstractPage):
127
189
  self.data_length,
128
190
  self.card_header,
129
191
  self.card_body,
192
+ self.card_footer,
130
193
  self.selection_data,
131
194
  ],
132
195
  # style=FLEXBOX_STYLE_ALLOW_VERTICAL_FILL,
@@ -140,15 +203,236 @@ class MainGraph(AbstractPage):
140
203
 
141
204
  app = self.application.app
142
205
 
143
- # ===== Update Graph =====
206
+ # ===== Change visibility of plot tab =====
207
+ objective_plot_tab = [t for t in self.tab_list if t.tab_id == self.TAB_ID_OBJECTIVE_PLOT][0]
208
+ hypervolume_plot_tab = [t for t in self.tab_list if t.tab_id == self.TAB_ID_HYPERVOLUME_PLOT][0]
209
+ @app.callback(
210
+ output=(
211
+ Output(objective_plot_tab, 'tab_style'),
212
+ Output(hypervolume_plot_tab, 'tab_style'),
213
+ ),
214
+ inputs=dict(
215
+ _=(
216
+ Input(self.tabs, 'active_tab'),
217
+ Input(self.location, 'pathname'),
218
+ ),
219
+ current_styles=dict(
220
+ obj=State(objective_plot_tab, 'tab_style'),
221
+ hv=State(hypervolume_plot_tab, 'tab_style'),
222
+ ),
223
+ ),
224
+ prevent_initial_call=True,
225
+ )
226
+ def set_disabled(_, current_styles):
227
+ obj_style: dict = no_update
228
+ hv_style: dict = no_update
229
+
230
+ # load history
231
+ if self.application.history is None:
232
+ raise PreventUpdate
233
+ obj_names = self.application.history.obj_names
234
+
235
+ # show objective plot with 3 or more objectives
236
+ if len(obj_names) < 3:
237
+ if 'display' not in current_styles['obj'].keys():
238
+ obj_style = current_styles['obj']
239
+ obj_style.update({'display': 'none'})
240
+ else:
241
+ if 'display' in current_styles['obj'].keys():
242
+ obj_style = current_styles['obj']
243
+ obj_style.pop('display')
244
+
245
+ # show hypervolume plot with 2 or more objectives
246
+ if len(obj_names) < 2:
247
+ if 'display' not in current_styles['hv'].keys():
248
+ hv_style = current_styles['hv']
249
+ hv_style.update({'display': 'none'})
250
+ else:
251
+ if 'display' in current_styles['hv'].keys():
252
+ hv_style = current_styles['hv']
253
+ hv_style.pop('display')
254
+
255
+ return obj_style, hv_style
256
+
257
+ # ===== Change visibility of dropdown menus =====
258
+ @app.callback(
259
+ Output(self.objective_plot_controller, 'hidden'),
260
+ Input(self.tabs, 'active_tab'),
261
+ prevent_initial_call=True,
262
+ )
263
+ def invisible_controller(active_tab):
264
+ return active_tab != self.TAB_ID_OBJECTIVE_PLOT
265
+
266
+ # ===== Change accessibility of axis3 dropdown =====
267
+ @app.callback(
268
+ Output(self.axis3_dropdown, 'disabled'),
269
+ Input(self.switch_3d, 'value'),
270
+ Input(self.location, 'pathname'),
271
+ prevent_initial_call=True,
272
+ )
273
+ def disable_axis_3_dropdown_menu(is_3d, _):
274
+ return not is_3d
275
+
276
+ # ===== Initialize Dropdown menus =====
277
+ @app.callback(
278
+ output=dict(
279
+ items_list=[
280
+ Output(self.axis1_dropdown, 'children'),
281
+ Output(self.axis2_dropdown, 'children'),
282
+ Output(self.axis3_dropdown, 'children'),
283
+ ],
284
+ labels=[
285
+ Output(self.axis1_dropdown, 'label'),
286
+ Output(self.axis2_dropdown, 'label'),
287
+ Output(self.axis3_dropdown, 'label'),
288
+ ]
289
+ ),
290
+ inputs=[Input(self.location, 'pathname')],
291
+ prevent_initial_call=True,
292
+ )
293
+ def init_dropdown_menus(*_, **__):
294
+ # just in case
295
+ if callback_context.triggered_id is None:
296
+ raise PreventUpdate
297
+
298
+ # load history
299
+ if self.application.history is None:
300
+ logger.error(Msg.ERR_NO_HISTORY_SELECTED)
301
+ raise PreventUpdate
302
+
303
+ # assert 3 or more objectives
304
+ obj_names = self.application.history.obj_names
305
+ if len(obj_names) < 3:
306
+ raise PreventUpdate
307
+
308
+ # add dropdown item to dropdown
309
+ axis1_dropdown_items = []
310
+ axis2_dropdown_items = []
311
+ axis3_dropdown_items = []
312
+
313
+ for i, obj_name in enumerate(obj_names):
314
+ axis1_dropdown_items.append(
315
+ dbc.DropdownMenuItem(
316
+ children=obj_name,
317
+ id={'type': 'objective-axis1-dropdown-menu-item', 'index': obj_name},
318
+ )
319
+ )
320
+
321
+ axis2_dropdown_items.append(
322
+ dbc.DropdownMenuItem(
323
+ children=obj_name,
324
+ id={'type': 'objective-axis2-dropdown-menu-item', 'index': obj_name},
325
+ )
326
+ )
327
+
328
+ axis3_dropdown_items.append(
329
+ dbc.DropdownMenuItem(
330
+ children=obj_name,
331
+ id={'type': 'objective-axis3-dropdown-menu-item', 'index': obj_name},
332
+ )
333
+ )
334
+
335
+ items_list = [axis1_dropdown_items, axis2_dropdown_items, axis3_dropdown_items]
336
+
337
+ ret = dict(
338
+ items_list=items_list,
339
+ labels=obj_names[:3],
340
+ )
341
+
342
+ return ret
343
+
344
+ # ===== Update Dropdown menus =====
345
+ @app.callback(
346
+ output=dict(
347
+ label_1=Output(self.axis1_dropdown, 'label', allow_duplicate=True), # label of dropdown
348
+ label_2=Output(self.axis2_dropdown, 'label', allow_duplicate=True),
349
+ label_3=Output(self.axis3_dropdown, 'label', allow_duplicate=True),
350
+ ),
351
+ inputs=dict(
352
+ _=( # when the dropdown item is clicked
353
+ Input({'type': 'objective-axis1-dropdown-menu-item', 'index': ALL}, 'n_clicks'),
354
+ Input({'type': 'objective-axis2-dropdown-menu-item', 'index': ALL}, 'n_clicks'),
355
+ Input({'type': 'objective-axis3-dropdown-menu-item', 'index': ALL}, 'n_clicks'),
356
+ ),
357
+ current_labels=dict( # for exclusive selection
358
+ label_1=State(self.axis1_dropdown, 'label'),
359
+ label_2=State(self.axis2_dropdown, 'label'),
360
+ label_3=State(self.axis3_dropdown, 'label'),
361
+ ),
362
+ ),
363
+ prevent_initial_call=True,
364
+ )
365
+ def update_dropdowns(_, current_labels):
366
+ # just in case
367
+ if callback_context.triggered_id is None:
368
+ raise PreventUpdate
369
+
370
+ # load history
371
+ if self.application.history is None:
372
+ logger.error(Msg.ERR_NO_HISTORY_SELECTED)
373
+ raise PreventUpdate
374
+ obj_names = self.application.history.obj_names
375
+
376
+ if len(obj_names) < 3:
377
+ raise PreventUpdate
378
+
379
+ # default return values
380
+ ret = dict(
381
+ label_1=no_update,
382
+ label_2=no_update,
383
+ label_3=no_update,
384
+ )
385
+
386
+ # ===== update dropdown label =====
387
+ new_label = callback_context.triggered_id['index']
388
+
389
+ # example of triggerd_id: {'index': 'max_displacement', 'type': 'objective-axis1-dropdown-menu-item'}
390
+ if callback_context.triggered_id['type'] == 'objective-axis1-dropdown-menu-item':
391
+ ret["label_1"] = new_label
392
+ if current_labels["label_2"] == new_label:
393
+ ret["label_2"] = current_labels["label_1"]
394
+ if current_labels["label_3"] == new_label:
395
+ ret["label_3"] = current_labels["label_1"]
396
+ if callback_context.triggered_id['type'] == 'objective-axis2-dropdown-menu-item':
397
+ ret["label_2"] = new_label
398
+ if current_labels["label_3"] == new_label:
399
+ ret["label_3"] = current_labels["label_2"]
400
+ if current_labels["label_1"] == new_label:
401
+ ret["label_1"] = current_labels["label_2"]
402
+ if callback_context.triggered_id['type'] == 'objective-axis3-dropdown-menu-item':
403
+ ret["label_3"] = new_label
404
+ if current_labels["label_1"] == new_label:
405
+ ret["label_1"] = current_labels["label_3"]
406
+ if current_labels["label_2"] == new_label:
407
+ ret["label_2"] = current_labels["label_3"]
408
+
409
+ return ret
410
+
411
+ # ===== Update Graph (by tab selection or dropdown changed) =====
144
412
  @app.callback(
145
413
  Output(self.graph.id, 'figure'),
146
414
  Output(self.data_length.id, self.data_length_prop), # To determine whether Process Monitor should update the graph, the main graph remembers the current amount of data.
147
- Input(self.tabs.id, 'active_tab'),
148
- Input(self.dummy, 'children'),
149
- prevent_initial_call=False,)
150
- def redraw_main_graph(active_tab_id, _):
151
- figure, length = self.get_fig_by_tab_id(active_tab_id, with_length=True)
415
+ inputs=dict(
416
+ active_tab_id=Input(self.tabs.id, 'active_tab'),
417
+ _=Input(self.dummy, 'children'),
418
+ is_3d=Input(self.switch_3d, 'value'),
419
+ obj_names=(
420
+ Input(self.axis1_dropdown, 'label'),
421
+ Input(self.axis2_dropdown, 'label'),
422
+ Input(self.axis3_dropdown, 'label'),
423
+ ),
424
+ ),
425
+ prevent_initial_call=False,
426
+ )
427
+ def redraw_main_graph(active_tab_id, _, is_3d, obj_names):
428
+ kwargs = {}
429
+ if active_tab_id == self.TAB_ID_OBJECTIVE_PLOT:
430
+ if not is_3d:
431
+ obj_names = obj_names[:-1]
432
+ kwargs = dict(
433
+ obj_names=obj_names
434
+ )
435
+ figure, length = self.get_fig_by_tab_id(active_tab_id, with_length=True, kwargs=kwargs)
152
436
  return figure, length
153
437
 
154
438
  # ===== Save Selection =====
@@ -248,7 +532,7 @@ class MainGraph(AbstractPage):
248
532
  img_url = 'data:image/jpeg;base64, ' + encoded_image
249
533
  return html.Img(src=img_url, style={"width": "200px"}) if img_url is not None else html.Div()
250
534
 
251
- def get_fig_by_tab_id(self, tab_id, with_length=False):
535
+ def get_fig_by_tab_id(self, tab_id, with_length=False, kwargs: dict = None):
252
536
  # If the history is not loaded, do nothing
253
537
  if self.application.history is None:
254
538
  raise PreventUpdate
@@ -264,7 +548,8 @@ class MainGraph(AbstractPage):
264
548
 
265
549
  # create figure
266
550
  df = self.data_accessor()
267
- fig = creator(self.application.history, df)
551
+ kwargs = kwargs or {}
552
+ fig = creator(self.application.history, df, **kwargs)
268
553
  if with_length:
269
554
  return fig, len(df)
270
555
  else:
@@ -389,5 +389,5 @@ class OptunaVisualizerPage(AbstractPage):
389
389
  if isinstance(self.application, ProcessMonitorApplication):
390
390
  df = self.application.local_data
391
391
  else:
392
- df = self.application.history.local_data
392
+ df = self.application.history.get_df()
393
393
  return df
@@ -52,4 +52,4 @@ def main():
52
52
 
53
53
 
54
54
  if __name__ == '__main__':
55
- debug()
55
+ main()
@@ -522,6 +522,7 @@ class Tutorial(AbstractPage):
522
522
  Input(self.tutorial_mode_switch, 'value'),
523
523
  Input(self.load_sample_csv_button, 'n_clicks'), # load button clicked
524
524
  Input(self.main_graph.selection_data, self.main_graph.selection_data_property), # selection changed
525
+ Input(self.femtet_control.femtet_state.id, self.femtet_control.femtet_state_prop), # connection established
525
526
  Input(self.control_visibility_input_dummy, 'children'),
526
527
  State(self.load_sample_csv_badge, 'style'), # switch visibility
527
528
  State(self.load_sample_csv_div, 'style'), # switch visibility
@@ -533,9 +534,10 @@ class Tutorial(AbstractPage):
533
534
  )
534
535
  def control_visibility(
535
536
  is_tutorial,
536
- _1,
537
- selection_data,
538
- _2,
537
+ _1, # load_sample clicked
538
+ selection_data, # data selected
539
+ _2, # result of connect_femtet clicked
540
+ _3, # dummy component loaded
539
541
  load_sample_csv_badge_current_style,
540
542
  load_sample_csv_div_current_style,
541
543
  file_picker_current_style,
@@ -596,13 +598,12 @@ class Tutorial(AbstractPage):
596
598
 
597
599
  # if history is not None,
598
600
  else:
599
- print('=====')
600
- # if a point is already selected, show badge to open-pdt or connect-femtet
601
+ # if a point is already selected,
602
+ # show badge to open-pdt or connect-femtet
601
603
  if self.check_point_selected(selection_data):
602
- print(1)
604
+
603
605
  # if femtet is connected, show badge to open-pdt
604
606
  if self.femtet_control.check_femtet_state() is FemtetState.connected:
605
- print(2)
606
607
  ret[open_pdt_badge_style] = self.control_visibility_by_style(
607
608
  True,
608
609
  open_pdt_badge_current_style,
@@ -610,7 +611,6 @@ class Tutorial(AbstractPage):
610
611
 
611
612
  # else, show femtet-connect badge
612
613
  else:
613
- print('3')
614
614
  ret[connect_femtet_badge_style] = self.control_visibility_by_style(
615
615
  True,
616
616
  connect_femtet_badge_current_style,
@@ -646,7 +646,7 @@ class Tutorial(AbstractPage):
646
646
  # get sample file
647
647
  import pyfemtet
648
648
  package_root = os.path.dirname(pyfemtet.__file__)
649
- sample_dir = os.path.join(package_root, 'opt', 'femprj_sample') # FIXME: locale によってパスを変える
649
+ sample_dir = os.path.join(package_root, 'opt', 'samples', 'femprj_sample') # FIXME: locale によってパスを変える
650
650
  path = os.path.join(sample_dir, 'wat_ex14_parametric_test_result.reccsv')
651
651
 
652
652
  if not os.path.exists(path):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyfemtet
3
- Version: 0.4.25
3
+ Version: 0.5.1
4
4
  Summary: Design parameter optimization using Femtet.
5
5
  Home-page: https://github.com/pyfemtet/pyfemtet
6
6
  License: BSD-3-Clause