pyfemtet 0.3.12__py3-none-any.whl → 0.4.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 (35) hide show
  1. pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.py +1 -1
  2. pyfemtet/FemtetPJTSample/Sldworks_ex01/Sldworks_ex01.py +1 -1
  3. pyfemtet/FemtetPJTSample/gau_ex08_parametric.py +1 -1
  4. pyfemtet/FemtetPJTSample/her_ex40_parametric.femprj +0 -0
  5. pyfemtet/FemtetPJTSample/her_ex40_parametric.py +1 -1
  6. pyfemtet/FemtetPJTSample/wat_ex14_parallel_parametric.py +1 -1
  7. pyfemtet/FemtetPJTSample/wat_ex14_parametric.femprj +0 -0
  8. pyfemtet/FemtetPJTSample/wat_ex14_parametric.py +1 -1
  9. pyfemtet/__init__.py +1 -1
  10. pyfemtet/core.py +14 -0
  11. pyfemtet/dispatch_extensions.py +5 -0
  12. pyfemtet/opt/__init__.py +22 -2
  13. pyfemtet/opt/_femopt.py +544 -0
  14. pyfemtet/opt/_femopt_core.py +730 -0
  15. pyfemtet/opt/interface/__init__.py +15 -0
  16. pyfemtet/opt/interface/_base.py +71 -0
  17. pyfemtet/opt/{interface.py → interface/_femtet.py} +120 -407
  18. pyfemtet/opt/interface/_femtet_with_nx/__init__.py +3 -0
  19. pyfemtet/opt/interface/_femtet_with_nx/_interface.py +128 -0
  20. pyfemtet/opt/interface/_femtet_with_sldworks.py +174 -0
  21. pyfemtet/opt/opt/__init__.py +8 -0
  22. pyfemtet/opt/opt/_base.py +202 -0
  23. pyfemtet/opt/opt/_optuna.py +240 -0
  24. pyfemtet/opt/visualization/__init__.py +7 -0
  25. pyfemtet/opt/visualization/_graphs.py +222 -0
  26. pyfemtet/opt/visualization/_monitor.py +1149 -0
  27. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.1.dist-info}/METADATA +4 -4
  28. pyfemtet-0.4.1.dist-info/RECORD +38 -0
  29. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.1.dist-info}/WHEEL +1 -1
  30. pyfemtet-0.4.1.dist-info/entry_points.txt +3 -0
  31. pyfemtet/opt/base.py +0 -1490
  32. pyfemtet/opt/monitor.py +0 -474
  33. pyfemtet-0.3.12.dist-info/RECORD +0 -26
  34. /pyfemtet/opt/{_FemtetWithNX → interface/_femtet_with_nx}/update_model.py +0 -0
  35. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.1.dist-info}/LICENSE +0 -0
@@ -0,0 +1,240 @@
1
+ # typing
2
+ from typing import Iterable
3
+
4
+ # built-in
5
+ import os
6
+
7
+ # 3rd-party
8
+ import numpy as np
9
+ import optuna
10
+ from optuna.trial import TrialState
11
+ from optuna.study import MaxTrialsCallback
12
+
13
+ # pyfemtet relative
14
+ from pyfemtet.opt._femopt_core import OptimizationStatus, generate_lhs
15
+ from pyfemtet.opt.opt import AbstractOptimizer, logger
16
+ from pyfemtet.core import MeshError, ModelError, SolveError
17
+
18
+ # filter warnings
19
+ import warnings
20
+ from optuna.exceptions import ExperimentalWarning
21
+ warnings.filterwarnings('ignore', category=ExperimentalWarning)
22
+
23
+
24
+ class OptunaOptimizer(AbstractOptimizer):
25
+
26
+ def __init__(
27
+ self,
28
+ sampler_class: optuna.samplers.BaseSampler or None = None,
29
+ sampler_kwargs: dict or None = None,
30
+ add_init_method: str or Iterable[str] or None = None
31
+ ):
32
+ super().__init__()
33
+ self.study_name = None
34
+ self.storage = None
35
+ self.study = None
36
+ self.optimize_callbacks = []
37
+ self.sampler_class = optuna.samplers.TPESampler if sampler_class is None else sampler_class
38
+ self.sampler_kwargs = dict() if sampler_kwargs is None else sampler_kwargs
39
+ self.additional_initial_parameter = []
40
+ self.additional_initial_methods = add_init_method if hasattr(add_init_method, '__iter__') else [add_init_method]
41
+
42
+ def _objective(self, trial):
43
+
44
+ # 中断の確認 (FAIL loop に陥る対策)
45
+ if self.entire_status.get() == OptimizationStatus.INTERRUPTING:
46
+ self.worker_status.set(OptimizationStatus.INTERRUPTING)
47
+ trial.study.stop() # 現在実行中の trial を最後にする
48
+ return None # set TrialState FAIL
49
+
50
+ # candidate x
51
+ x = []
52
+ for i, row in self.parameters.iterrows():
53
+ v = trial.suggest_float(row['name'], row['lb'], row['ub'])
54
+ x.append(v)
55
+ x = np.array(x).astype(float)
56
+
57
+ # message の設定
58
+ self.message = trial.user_attrs['message'] if 'message' in trial.user_attrs.keys() else ''
59
+
60
+ # fem や opt 経由で変数を取得して constraint を計算する時のためにアップデート
61
+ self.parameters['value'] = x
62
+ self.fem.update_parameter(self.parameters)
63
+
64
+ # strict 拘束
65
+ strict_constraints = [cns for cns in self.constraints.values() if cns.strict]
66
+ for cns in strict_constraints:
67
+ feasible = True
68
+ cns_value = cns.calc(self.fem)
69
+ if cns.lb is not None:
70
+ feasible = feasible and (cns_value >= cns.lb)
71
+ if cns.ub is not None:
72
+ feasible = feasible and (cns.ub >= cns_value)
73
+ if not feasible:
74
+ logger.info(f'以下の変数で拘束 {cns.name} が満たされませんでした。')
75
+ print(self.get_parameter('dict'))
76
+ raise optuna.TrialPruned() # set TrialState PRUNED because FAIL causes similar candidate loop.
77
+
78
+ # 計算
79
+ try:
80
+ _, _y, c = self.f(x)
81
+ except (ModelError, MeshError, SolveError) as e:
82
+ logger.info(e)
83
+ logger.info('以下の変数で FEM 解析に失敗しました。')
84
+ print(self.get_parameter('dict'))
85
+
86
+ # 中断の確認 (解析中に interrupt されている場合対策)
87
+ if self.entire_status.get() == OptimizationStatus.INTERRUPTING:
88
+ self.worker_status.set(OptimizationStatus.INTERRUPTING)
89
+ trial.study.stop() # 現在実行中の trial を最後にする
90
+ return None # set TrialState FAIL
91
+
92
+ raise optuna.TrialPruned() # set TrialState PRUNED because FAIL causes similar candidate loop.
93
+
94
+ # 拘束 attr の更新
95
+ _c = [] # 非正なら OK
96
+ for (name, cns), c_value in zip(self.constraints.items(), c):
97
+ lb, ub = cns.lb, cns.ub
98
+ if lb is not None: # fun >= lb <=> lb - fun <= 0
99
+ _c.append(lb - c_value)
100
+ if ub is not None: # ub >= fun <=> fun - ub <= 0
101
+ _c.append(c_value - ub)
102
+ trial.set_user_attr('constraint', _c)
103
+
104
+ # 中断の確認 (解析中に interrupt されている場合対策)
105
+ if self.entire_status.get() == OptimizationStatus.INTERRUPTING:
106
+ self.worker_status.set(OptimizationStatus.INTERRUPTING)
107
+ trial.study.stop() # 現在実行中の trial を最後にする
108
+ return None # set TrialState FAIL
109
+
110
+ # 結果
111
+ return tuple(_y)
112
+
113
+ def _constraint(self, trial):
114
+ return trial.user_attrs['constraint'] if 'constraint' in trial.user_attrs.keys() else (1,) # infeasible
115
+
116
+ def _setup_before_parallel(self):
117
+ """Create storage, study and set initial parameter."""
118
+
119
+ # create storage
120
+ self.study_name = os.path.basename(self.history.path)
121
+ storage_path = self.history.path.replace('.csv', '.db') # history と同じところに保存
122
+ if self.is_cluster: # remote cluster なら scheduler の working dir に保存
123
+ storage_path = os.path.basename(self.history.path).replace('.csv', '.db')
124
+
125
+ # callback to terminate
126
+ if self.n_trials is not None:
127
+ n_trials = self.n_trials
128
+
129
+ # restart である場合、追加 N 回と見做す
130
+ if self.history.is_restart:
131
+ n_existing_trials = len(self.history.actor_data)
132
+ n_trials += n_existing_trials
133
+
134
+ self.optimize_callbacks.append(MaxTrialsCallback(n_trials, states=(TrialState.COMPLETE,)))
135
+
136
+ # if not restart, create study if storage is not exists
137
+ if not self.history.is_restart:
138
+
139
+ self.storage = optuna.integration.dask.DaskStorage(
140
+ f'sqlite:///{storage_path}',
141
+ )
142
+
143
+ self.study = optuna.create_study(
144
+ study_name=self.study_name,
145
+ storage=self.storage,
146
+ load_if_exists=True,
147
+ directions=['minimize'] * len(self.objectives),
148
+ )
149
+
150
+ # 初期値の設定
151
+ if len(self.study.trials) == 0: # リスタートでなければ
152
+ # ユーザーの指定した初期値
153
+ params = self.get_parameter('dict')
154
+ self.study.enqueue_trial(params, user_attrs={"message": "initial"})
155
+
156
+ # add_initial_parameter で追加された初期値
157
+ for prm, prm_set_name in self.additional_initial_parameter:
158
+ self.study.enqueue_trial(
159
+ prm,
160
+ user_attrs={"message": prm_set_name}
161
+ )
162
+
163
+ # add_init で指定された方法による初期値
164
+ if 'LHS' in self.additional_initial_methods:
165
+ names = []
166
+ bounds = []
167
+ for i, row in self.parameters.iterrows():
168
+ names.append(row['name'])
169
+ lb = row['lb']
170
+ ub = row['ub']
171
+ bounds.append([lb, ub])
172
+ data = generate_lhs(bounds, seed=self.seed)
173
+ for datum in data:
174
+ d = {}
175
+ for name, v in zip(names, datum):
176
+ d[name] = v
177
+ self.study.enqueue_trial(
178
+ d, user_attrs={"message": "additional initial (Latin Hypercube Sampling)"}
179
+ )
180
+
181
+ # if is_restart, load study
182
+ else:
183
+ if not os.path.exists(storage_path):
184
+ msg = f'{storage_path} が見つかりません。'
185
+ msg += '.db ファイルは .csv ファイルと同じフォルダに生成されます。'
186
+ msg += 'クラスター解析の場合は、スケジューラを起動したフォルダに生成されます。'
187
+ raise FileNotFoundError(msg)
188
+ self.storage = optuna.integration.dask.DaskStorage(
189
+ f'sqlite:///{storage_path}',
190
+ )
191
+
192
+ def add_init_parameter(
193
+ self,
194
+ parameter: dict or Iterable,
195
+ name: str or None = None,
196
+ ):
197
+ """Add additional initial parameter for evaluate.
198
+
199
+ The parameter set is ignored if the main() is continued.
200
+
201
+ Args:
202
+ parameter (dict or Iterable): Parameter to evaluate before run optimization algorithm.
203
+ name (str or None): Optional. If specified, the name is saved in the history row. Default to None.
204
+
205
+ """
206
+ if name is None:
207
+ name = 'additional initial'
208
+ else:
209
+ name = f'additional initial ({name})'
210
+ self.additional_initial_parameter.append([parameter, name])
211
+
212
+ def run(self):
213
+ """Set random seed, sampler, study and run study.optimize()."""
214
+
215
+ # (re)set random seed
216
+ seed = self.seed
217
+ if seed is not None:
218
+ if self.subprocess_idx is not None:
219
+ seed += self.subprocess_idx
220
+
221
+ # restore sampler
222
+ sampler = self.sampler_class(
223
+ seed=seed,
224
+ constraints_func=self._constraint,
225
+ **self.sampler_kwargs
226
+ )
227
+
228
+ # load study
229
+ study = optuna.load_study(
230
+ study_name=self.study_name,
231
+ storage=self.storage,
232
+ sampler=sampler,
233
+ )
234
+
235
+ # run
236
+ study.optimize(
237
+ self._objective,
238
+ timeout=self.timeout,
239
+ callbacks=self.optimize_callbacks,
240
+ )
@@ -0,0 +1,7 @@
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,222 @@
1
+ import plotly.graph_objs as go
2
+ import plotly.express as px
3
+
4
+
5
+ _CUSTOM_DATA_DICT = {'trial': 0} # 連番
6
+
7
+
8
+ class _ColorSet:
9
+ non_domi = {True: '#007bff', False: '#6c757d'} # color
10
+
11
+
12
+ class _SymbolSet:
13
+ feasible = {True: 'circle', False: 'circle-open'} # style
14
+
15
+
16
+ class _LanguageSet:
17
+
18
+ feasible = {'label': 'feasible', True: True, False: False}
19
+ non_domi = {'label': 'non_domi', True: True, False: False}
20
+
21
+ def __init__(self, language: str = 'ja'):
22
+ self.lang = language
23
+ if self.lang.lower() == 'ja':
24
+ self.feasible = {'label': '拘束条件', True: '満足', False: '違反'}
25
+ self.non_domi = {'label': '最適性', True: '非劣解', False: '劣解'}
26
+
27
+ def localize(self, df):
28
+ # 元のオブジェクトを変更しないようにコピー
29
+ cdf = df.copy()
30
+
31
+ # feasible, non_domi の localize
32
+ cdf[self.feasible['label']] = [self.feasible[v] for v in cdf['feasible']]
33
+ cdf[self.non_domi['label']] = [self.non_domi[v] for v in cdf['non_domi']]
34
+
35
+ return cdf
36
+
37
+
38
+ _ls = _LanguageSet('ja')
39
+ _cs = _ColorSet()
40
+ _ss = _SymbolSet()
41
+
42
+
43
+ def update_hypervolume_plot(history, df):
44
+ df = _ls.localize(df)
45
+
46
+ # create figure
47
+ fig = px.line(
48
+ df,
49
+ x="trial",
50
+ y="hypervolume",
51
+ markers=True,
52
+ custom_data=_CUSTOM_DATA_DICT.keys(),
53
+ )
54
+
55
+ fig.update_layout(
56
+ dict(
57
+ title_text="ハイパーボリュームプロット",
58
+ )
59
+ )
60
+
61
+ return fig
62
+
63
+
64
+ def update_default_figure(history, df):
65
+
66
+ # data setting
67
+ obj_names = history.obj_names
68
+
69
+ if len(obj_names) == 0:
70
+ return go.Figure()
71
+
72
+ elif len(obj_names) == 1:
73
+ return update_single_objective_plot(history, df)
74
+
75
+ elif len(obj_names) >= 2:
76
+ return update_multi_objective_pairplot(history, df)
77
+
78
+
79
+ def update_single_objective_plot(history, df):
80
+
81
+ df = _ls.localize(df)
82
+ obj_name = history.obj_names[0]
83
+
84
+ fig = px.scatter(
85
+ df,
86
+ x='trial',
87
+ y=obj_name,
88
+ symbol=_ls.feasible['label'],
89
+ symbol_map={
90
+ _ls.feasible[True]: _ss.feasible[True],
91
+ _ls.feasible[False]: _ss.feasible[False],
92
+ },
93
+ hover_data={
94
+ _ls.feasible['label']: False,
95
+ 'trial': True,
96
+ },
97
+ custom_data=_CUSTOM_DATA_DICT.keys(),
98
+ )
99
+
100
+ fig.add_trace(
101
+ go.Scatter(
102
+ x=df['trial'],
103
+ y=df[obj_name],
104
+ mode="lines",
105
+ line=go.scatter.Line(
106
+ width=0.5,
107
+ color='#6c757d',
108
+ ),
109
+ showlegend=False
110
+ )
111
+ )
112
+
113
+ fig.update_layout(
114
+ dict(
115
+ title_text="目的プロット",
116
+ xaxis_title="解析実行回数(回)",
117
+ yaxis_title=obj_name,
118
+ )
119
+ )
120
+
121
+ return fig
122
+
123
+
124
+ def update_multi_objective_pairplot(history, df):
125
+ df = _ls.localize(df)
126
+
127
+ obj_names = history.obj_names
128
+
129
+ common_kwargs = dict(
130
+ color=_ls.non_domi['label'],
131
+ color_discrete_map={
132
+ _ls.non_domi[True]: _cs.non_domi[True],
133
+ _ls.non_domi[False]: _cs.non_domi[False],
134
+ },
135
+ symbol=_ls.feasible['label'],
136
+ symbol_map={
137
+ _ls.feasible[True]: _ss.feasible[True],
138
+ _ls.feasible[False]: _ss.feasible[False],
139
+ },
140
+ hover_data={
141
+ _ls.feasible['label']: False,
142
+ 'trial': True,
143
+ },
144
+ custom_data=_CUSTOM_DATA_DICT.keys(),
145
+ category_orders={
146
+ _ls.feasible['label']: (_ls.feasible[False], _ls.feasible[True]),
147
+ _ls.non_domi['label']: (_ls.non_domi[False], _ls.non_domi[True]),
148
+ },
149
+ )
150
+
151
+ if len(obj_names) == 2:
152
+ fig = px.scatter(
153
+ data_frame=df,
154
+ x=obj_names[0],
155
+ y=obj_names[1],
156
+ **common_kwargs,
157
+ )
158
+ fig.update_layout(
159
+ dict(
160
+ xaxis_title=obj_names[0],
161
+ yaxis_title=obj_names[1],
162
+ )
163
+ )
164
+
165
+ else:
166
+ fig = px.scatter_matrix(
167
+ data_frame=df,
168
+ dimensions=obj_names,
169
+ **common_kwargs,
170
+ )
171
+ fig.update_traces(
172
+ patch={'diagonal.visible': False},
173
+ showupperhalf=False,
174
+ )
175
+
176
+ fig.update_layout(
177
+ dict(
178
+ title_text="多目的ペアプロット",
179
+ )
180
+ )
181
+
182
+ return fig
183
+
184
+
185
+ def _debug():
186
+ import os
187
+
188
+ os.chdir(os.path.dirname(__file__))
189
+ csv_path = 'sample.csv'
190
+
191
+ show_static_monitor(csv_path)
192
+
193
+
194
+ def show_static_monitor(csv_path):
195
+ from pyfemtet.opt._femopt_core import History
196
+ from pyfemtet.opt.visualization._monitor import ResultViewerApp
197
+ _h = History(history_path=csv_path)
198
+ _monitor = ResultViewerApp(history=_h)
199
+ _monitor.run()
200
+
201
+
202
+ def entry_point():
203
+ import argparse
204
+ parser = argparse.ArgumentParser()
205
+
206
+ parser.add_argument('csv_path', help='pyfemtet を実行した結果の csv ファイルのパスを指定してください。', type=str)
207
+
208
+ # parser.add_argument(
209
+ # "-c",
210
+ # "--csv-path",
211
+ # help="pyfemtet.opt による最適化結果 csv ファイルパス",
212
+ # type=str,
213
+ # )
214
+
215
+ args = parser.parse_args()
216
+
217
+ if args.csv_path:
218
+ show_static_monitor(args.csv_path)
219
+
220
+
221
+ if __name__ == '__main__':
222
+ _debug()