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.
- pyfemtet/__init__.py +1 -1
- pyfemtet/message/locales/ja/LC_MESSAGES/messages.mo +0 -0
- pyfemtet/message/locales/ja/LC_MESSAGES/messages.po +114 -62
- pyfemtet/message/locales/messages.pot +114 -62
- pyfemtet/message/messages.py +6 -2
- pyfemtet/opt/__init__.py +2 -2
- pyfemtet/opt/_femopt.py +27 -46
- pyfemtet/opt/_femopt_core.py +50 -33
- pyfemtet/opt/_test_utils/__init__.py +0 -0
- pyfemtet/opt/_test_utils/control_femtet.py +45 -0
- pyfemtet/opt/_test_utils/hyper_sphere.py +24 -0
- pyfemtet/opt/_test_utils/record_history.py +72 -0
- pyfemtet/opt/interface/_femtet.py +39 -1
- pyfemtet/opt/optimizer/__init__.py +12 -0
- pyfemtet/opt/{opt → optimizer}/_base.py +30 -9
- pyfemtet/opt/{opt → optimizer}/_optuna.py +14 -33
- pyfemtet/opt/optimizer/_optuna_botorchsampler_parameter_constraint_helper.py +325 -0
- pyfemtet/opt/{opt → optimizer}/_scipy.py +10 -6
- pyfemtet/opt/{opt → optimizer}/_scipy_scalar.py +24 -12
- pyfemtet/opt/samples/femprj_sample/constrained_pipe.femprj +0 -0
- pyfemtet/opt/samples/femprj_sample/constrained_pipe.py +97 -0
- pyfemtet/opt/samples/femprj_sample/constrained_pipe_test_result.reccsv +13 -0
- pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gal_ex58_parametric_test_result.reccsv +1 -1
- pyfemtet/opt/{femprj_sample → samples/femprj_sample}/her_ex40_parametric.py +10 -8
- pyfemtet/opt/samples/femprj_sample/her_ex40_parametric_test_result.reccsv +18 -0
- pyfemtet/opt/samples/femprj_sample_jp/constrained_pipe_jp.py +93 -0
- pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/her_ex40_parametric_jp.py +10 -8
- pyfemtet/opt/visualization/complex_components/main_figure_creator.py +69 -0
- pyfemtet/opt/visualization/complex_components/main_graph.py +299 -14
- pyfemtet/opt/visualization/process_monitor/pages.py +1 -1
- pyfemtet/opt/visualization/result_viewer/application.py +1 -1
- pyfemtet/opt/visualization/result_viewer/pages.py +9 -9
- {pyfemtet-0.4.25.dist-info → pyfemtet-0.5.1.dist-info}/METADATA +1 -1
- pyfemtet-0.5.1.dist-info/RECORD +115 -0
- pyfemtet/_test_util.py +0 -135
- pyfemtet/opt/femprj_sample/ParametricIF - True.femprj +0 -0
- pyfemtet/opt/femprj_sample/her_ex40_parametric_test_result.reccsv +0 -18
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial1.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial1.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial10.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial10.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial11.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial11.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial12.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial12.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial13.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial13.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial14.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial14.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial15.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial15.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial2.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial2.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial3.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial3.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial4.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial4.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial5.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial5.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial6.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial6.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial7.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial7.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial8.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial8.pdt +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial9.jpg +0 -0
- pyfemtet/opt/femprj_sample/wat_ex14_parametric.Results/Ex14_trial9.pdt +0 -0
- pyfemtet/opt/opt/__init__.py +0 -12
- pyfemtet/opt/opt/_optuna_botorch_helper.py +0 -209
- pyfemtet-0.4.25.dist-info/RECORD +0 -140
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/.gitignore +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/ParametricIF.femprj +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/ParametricIF.py +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/ParametricIF_test_result.reccsv +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_NX.femprj +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_NX.prt +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_NX.py +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_NX_test_result.reccsv +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_SW.SLDPRT +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_SW.femprj +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_SW.py +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/cad_ex01_SW_test_result.reccsv +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gal_ex58_parametric.femprj +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gal_ex58_parametric.py +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gau_ex08_parametric.femprj +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gau_ex08_parametric.py +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/gau_ex08_parametric_test_result.reccsv +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/her_ex40_parametric.femprj +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/paswat_ex1_parametric.femprj +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/paswat_ex1_parametric.py +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/paswat_ex1_parametric_parallel.py +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/paswat_ex1_parametric_test_result.reccsv +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/wat_ex14_parametric.femprj +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/wat_ex14_parametric.py +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/wat_ex14_parametric_parallel.py +0 -0
- /pyfemtet/opt/{femprj_sample → samples/femprj_sample}/wat_ex14_parametric_test_result.reccsv +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/ParametricIF_jp.femprj +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/ParametricIF_jp.py +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/cad_ex01_NX_jp.femprj +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/cad_ex01_NX_jp.py +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/cad_ex01_SW_jp.femprj +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/cad_ex01_SW_jp.py +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/gal_ex58_parametric_jp.femprj +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/gal_ex58_parametric_jp.py +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/gau_ex08_parametric_jp.femprj +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/gau_ex08_parametric_jp.py +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/her_ex40_parametric_jp.femprj +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/paswat_ex1_parametric_jp.femprj +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/paswat_ex1_parametric_jp.py +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/paswat_ex1_parametric_parallel_jp.py +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/wat_ex14_parametric_jp.femprj +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/wat_ex14_parametric_jp.py +0 -0
- /pyfemtet/opt/{femprj_sample_jp → samples/femprj_sample_jp}/wat_ex14_parametric_parallel_jp.py +0 -0
- {pyfemtet-0.4.25.dist-info → pyfemtet-0.5.1.dist-info}/LICENSE +0 -0
- {pyfemtet-0.4.25.dist-info → pyfemtet-0.5.1.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
|
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-
|
|
64
|
-
label=Msg.
|
|
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=
|
|
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
|
-
|
|
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
|
-
# =====
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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:
|
|
@@ -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
|
-
|
|
600
|
-
#
|
|
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
|
-
|
|
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):
|