pyfemtet 0.6.3__py3-none-any.whl → 0.6.5__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.6.3"
1
+ __version__ = "0.6.5"
@@ -4,20 +4,31 @@ from typing import Final
4
4
  # レジストリのパスと値の名前
5
5
  _REGISTRY_PATH: Final[str] = r"SOFTWARE\Murata Software\Femtet2014\Femtet"
6
6
  _VALUE_NAME: Final[str] = "AutoSave"
7
+ _DEFAULT_VALUE: Final[bool] = True
7
8
 
8
9
 
9
10
  def _get_autosave_enabled() -> bool:
10
- with winreg.OpenKey(winreg.HKEY_CURRENT_USER, _REGISTRY_PATH) as key:
11
- value, regtype = winreg.QueryValueEx(key, _VALUE_NAME)
12
- if regtype == winreg.REG_DWORD:
13
- return bool(value)
14
- else:
15
- raise ValueError("Unexpected registry value type.")
11
+ try:
12
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, _REGISTRY_PATH) as key:
13
+ value, regtype = winreg.QueryValueEx(key, _VALUE_NAME)
14
+ if regtype == winreg.REG_DWORD:
15
+ return bool(value)
16
+ else:
17
+ raise ValueError("Unexpected registry value type.")
18
+
19
+ except FileNotFoundError: # [WinError 2] 指定されたファイルが見つかりません。
20
+ __create_registry_key()
21
+ return _get_autosave_enabled()
16
22
 
17
23
 
18
24
  def _set_autosave_enabled(enable: bool) -> None:
19
- with winreg.OpenKey(winreg.HKEY_CURRENT_USER, _REGISTRY_PATH, 0, winreg.KEY_SET_VALUE) as key:
20
- winreg.SetValueEx(key, _VALUE_NAME, 0, winreg.REG_DWORD, int(enable))
25
+ try:
26
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER, _REGISTRY_PATH, 0, winreg.KEY_SET_VALUE) as key:
27
+ winreg.SetValueEx(key, _VALUE_NAME, 0, winreg.REG_DWORD, int(enable))
28
+
29
+ except FileNotFoundError: # [WinError 2] 指定されたファイルが見つかりません。
30
+ __create_registry_key()
31
+ _set_autosave_enabled(enable)
21
32
 
22
33
 
23
34
  def __test_autosave_setting():
@@ -36,3 +47,12 @@ def __test_autosave_setting():
36
47
  print(f"Current AutoSave setting is {'enabled' if after_setting else 'disabled'}.")
37
48
 
38
49
  assert new_setting == after_setting, "レジストリ編集に失敗しました。"
50
+
51
+
52
+ def __create_registry_key():
53
+ with winreg.CreateKey(winreg.HKEY_CURRENT_USER, _REGISTRY_PATH) as key:
54
+ winreg.SetValueEx(key, _VALUE_NAME, 0, winreg.REG_DWORD, int(_DEFAULT_VALUE))
55
+
56
+
57
+ if __name__ == '__main__':
58
+ print(_get_autosave_enabled())
File without changes
@@ -0,0 +1,197 @@
1
+ """Excel のエラーダイアログを補足します。"""
2
+ import sys
3
+ from time import sleep
4
+ from threading import Thread
5
+ import logging
6
+ import asyncio # for timeout
7
+ import win32gui
8
+ import win32con
9
+ import win32api
10
+
11
+ logger = logging.getLogger(__name__)
12
+ if __name__ == '__main__':
13
+ formatter = logging.Formatter(logging.BASIC_FORMAT)
14
+ handler = logging.StreamHandler(sys.stdout)
15
+ handler.setFormatter(formatter)
16
+ logger.addHandler(handler)
17
+ logger.setLevel(logging.DEBUG)
18
+
19
+
20
+ class _ExcelDialogProcessor:
21
+
22
+ def __init__(self, excel_, timeout):
23
+ self.excel = excel_
24
+ self.__excel_window_title = f' - Excel' # {basename} - Excel
25
+ self.__error_dialog_title = 'Microsoft Visual Basic'
26
+ self.__vbe_window_title = f'Microsoft Visual Basic for Applications - ' # Microsoft Visual Basic for Applications - {basename}
27
+ self.should_stop = False
28
+ self.timeout = timeout
29
+ self.__timed_out = False
30
+ self.__workbook_paths = [wb.FullName for wb in excel_.Workbooks]
31
+ self.__error_raised = False
32
+ self.__excel_state_stash = dict()
33
+ self.__watch_thread = None
34
+
35
+ async def watch(self):
36
+
37
+ while True:
38
+ if self.should_stop:
39
+ logger.debug('エラーダイアログの監視を終了')
40
+ return
41
+ logger.debug('エラーダイアログを監視中...')
42
+
43
+ win32gui.EnumWindows(self.enum_callback_to_activate, [])
44
+ await asyncio.sleep(0.5)
45
+
46
+ found = []
47
+ win32gui.EnumWindows(self.enum_callback_to_close_dialog, found)
48
+ await asyncio.sleep(0.5)
49
+ if any(found):
50
+ break
51
+
52
+ logger.debug('ブックを閉じます。')
53
+ win32gui.EnumWindows(self.enum_callback_to_close_book, []) # 成功していればこの時点でメイン処理では例外がスローされる
54
+ await asyncio.sleep(1)
55
+
56
+ logger.debug('確認ダイアログがあれば閉じます。')
57
+ win32gui.EnumWindows(self.enum_callback_to_close_confirm_dialog, [])
58
+ await asyncio.sleep(1)
59
+ self.__error_raised = True
60
+
61
+ def enum_callback_to_activate(self, hwnd, _):
62
+ title = win32gui.GetWindowText(hwnd)
63
+ # Excel 本体
64
+ if self.__excel_window_title in title:
65
+ # Visible == True の際、エラーが発生した際、
66
+ # 一度 Excel ウィンドウをアクティブ化しないと dialog が出てこない
67
+ # が、これだけではダメかも。
68
+ win32gui.PostMessage(hwnd, win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0)
69
+
70
+ def enum_callback_to_close_dialog(self, hwnd, found):
71
+ title = win32gui.GetWindowText(hwnd)
72
+ # エラーダイアログ
73
+ if self.__error_dialog_title == title:
74
+ # 何故かこのコマンド以外受け付けず、
75
+ # このコマンドで問答無用でデバッグモードに入る
76
+ logger.debug('エラーダイアログを見つけました。')
77
+ win32api.PostMessage(hwnd, win32con.WM_KEYDOWN, win32con.VK_RETURN, 0)
78
+ win32api.PostMessage(hwnd, win32con.WM_KEYUP, win32con.VK_RETURN, 0)
79
+ logger.debug('エラーダイアログを閉じました。')
80
+ found.append(True)
81
+
82
+ def enum_callback_to_close_confirm_dialog(self, hwnd, _):
83
+ title = win32gui.GetWindowText(hwnd)
84
+ # 確認ダイアログ
85
+ if "Microsoft Excel" in title:
86
+ # DisplayAlerts が False の場合は不要
87
+ win32gui.SendMessage(hwnd, win32con.WM_SYSCOMMAND, win32con.SC_CLOSE, 0)
88
+
89
+ def enum_callback_to_close_book(self, hwnd, _):
90
+ title = win32gui.GetWindowText(hwnd)
91
+ # VBE
92
+ if self.__vbe_window_title in title:
93
+ # 何故かこれで book 本体が閉じる
94
+ win32gui.SendMessage(hwnd, win32con.WM_CLOSE, 0, 0)
95
+
96
+ async def watch_main(self):
97
+ try:
98
+ await asyncio.wait_for(self.watch(), timeout=self.timeout)
99
+ except asyncio.TimeoutError:
100
+ logger.debug('タイムアウトしました。')
101
+ self.should_stop = True
102
+ self.__timed_out = True
103
+
104
+ def __enter__(self):
105
+ logger.debug('Excel を 不可視にします。')
106
+ self.__excel_state_stash['visible'] = self.excel.Visible
107
+ self.__excel_state_stash['display_alerts'] = self.excel.DisplayAlerts
108
+ self.excel.Visible = False
109
+ self.excel.DisplayAlerts = False
110
+
111
+ logger.debug('エラー監視を開始します。')
112
+ self.__watch_thread = Thread(
113
+ target=asyncio.run,
114
+ args=(self.watch_main(),),
115
+ )
116
+ self.__watch_thread.start()
117
+
118
+ def __exit__(self, exc_type, exc_val, exc_tb):
119
+
120
+ logger.debug('Excel の状態を回復します。')
121
+
122
+ self.should_stop = True
123
+ self.__watch_thread.join()
124
+
125
+ self.excel.Visible = self.__excel_state_stash['visible']
126
+ self.excel.DisplayAlerts = self.__excel_state_stash['display_alerts']
127
+
128
+ if self.__timed_out:
129
+ logger.debug('Excel プロセスを強制終了します。')
130
+ logger.error('Excel プロセス強制終了は未実装です。')
131
+ raise TimeoutError('マクロの実行がタイムアウトしました。')
132
+
133
+ # if exc_type is not None:
134
+ # if issubclass(exc_type, com_error) and self.__error_raised:
135
+ if self.__error_raised:
136
+ logger.debug('エラーハンドリングの副作用でブックを閉じているので'
137
+ 'Excel のブックを開きなおします。')
138
+ for wb_path in self.__workbook_paths:
139
+ self.excel.Workbooks.Open(wb_path)
140
+
141
+
142
+ def watch_excel_macro_error(excel_, timeout):
143
+ """Excel のエラーダイアログの出現を監視し、検出されればブックを閉じます。"""
144
+ return _ExcelDialogProcessor(excel_, timeout)
145
+
146
+
147
+ if __name__ == '__main__':
148
+
149
+ import os
150
+ os.chdir(os.path.dirname(__file__))
151
+
152
+ path = os.path.abspath('sample.xlsm')
153
+ path2 = os.path.abspath('sample2.xlsm')
154
+
155
+
156
+ from win32com.client import Dispatch
157
+ from pythoncom import com_error
158
+
159
+ logger.debug('Excel を起動しています。')
160
+ excel = Dispatch('Excel.Application')
161
+ excel.Visible = True
162
+ excel.DisplayAlerts = False
163
+ excel.Interactive = True
164
+
165
+ logger.debug('Workbook を開いています。')
166
+ excel.Workbooks.Open(path)
167
+
168
+ logger.debug('別の Workbook を開いています。')
169
+ excel.Workbooks.Open(path2)
170
+
171
+ logger.debug('Workbook に変更を加えます。')
172
+ excel.Workbooks(1).ActiveSheet.Range('A1').Value = 1.
173
+
174
+ logger.debug('開いている Workbooks 数:')
175
+ logger.debug(excel.Workbooks.Count)
176
+
177
+ try:
178
+ with watch_excel_macro_error(excel, timeout=10):
179
+ sleep(3)
180
+ try:
181
+ excel.Run('raise_error')
182
+ except com_error as e:
183
+ logger.debug('この段階ではまだ Excel 回復機能が働きません。')
184
+ logger.debug('開いている Workbooks 数:')
185
+ logger.debug(excel.Workbooks.Count)
186
+ raise e
187
+
188
+ # excel.Run('no_error')
189
+
190
+ except com_error as e:
191
+ logger.debug('メイン処理でエラーを補足しました。:')
192
+ logger.debug(e)
193
+
194
+ logger.debug('開いている Workbooks 数:')
195
+ logger.debug(excel.Workbooks.Count)
196
+
197
+ logger.debug('保存していない場合、Workbook の変更は失われます。')
pyfemtet/_warning.py CHANGED
@@ -2,15 +2,19 @@ import warnings
2
2
  from functools import wraps
3
3
 
4
4
 
5
+ def show_experimental_warning(feature_name):
6
+ warnings.warn(
7
+ f"The function '{feature_name}' is experimental and may change in the future.",
8
+ category=UserWarning,
9
+ stacklevel=2
10
+ )
11
+
12
+
5
13
  def experimental_feature(func):
6
14
 
7
15
  @wraps(func)
8
16
  def wrapper(*args, **kwargs):
9
- warnings.warn(
10
- f"The function '{func.__name__}' is experimental and may change in the future.",
11
- category=UserWarning,
12
- stacklevel=2
13
- )
17
+ show_experimental_warning(func.__name__)
14
18
  return func(*args, **kwargs)
15
19
 
16
20
  return wrapper
@@ -62,6 +66,10 @@ if __name__ == '__main__':
62
66
  print("This is an experimental function.")
63
67
 
64
68
  class Sample:
69
+
70
+ def __init__(self):
71
+ show_experimental_warning("Sample")
72
+
65
73
  @experimental_feature
66
74
  def add(self, a, b):
67
75
  return a + b
@@ -25,7 +25,9 @@ from pyfemtet.dispatch_extensions._impl import _NestableSpawnProcess
25
25
  from pyfemtet._femtet_config_util.exit import _exit_or_force_terminate
26
26
  from pyfemtet._femtet_config_util.autosave import _set_autosave_enabled, _get_autosave_enabled
27
27
 
28
- from pyfemtet._warning import experimental_class
28
+ from pyfemtet._util.excel_macro_util import watch_excel_macro_error
29
+
30
+ from pyfemtet._warning import show_experimental_warning
29
31
 
30
32
  import logging
31
33
 
@@ -33,7 +35,6 @@ logger = logging.getLogger(__name__)
33
35
  logger.setLevel(logging.INFO)
34
36
 
35
37
 
36
- @experimental_class
37
38
  class ExcelInterface(FEMInterface):
38
39
 
39
40
  input_xlsm_path: Path # 操作対象の xlsm パス
@@ -52,8 +53,8 @@ class ExcelInterface(FEMInterface):
52
53
  wb_output: CDispatch # システムを構成する Workbook
53
54
  sh_output: CDispatch # 計算結果の定義された WorkSheet (sh_input と同じでもよい)
54
55
 
55
- visible: bool = True # excel を可視化するかどうか
56
- display_alerts: bool = True # ダイアログを表示するかどうか
56
+ visible: bool = False # excel を可視化するかどうか
57
+ display_alerts: bool = False # ダイアログを表示するかどうか
57
58
 
58
59
  _load_problem_from_me: bool = True # TODO: add_parameter() 等を省略するかどうか。定義するだけでフラグとして機能する。
59
60
  _excel_pid: int
@@ -69,7 +70,9 @@ class ExcelInterface(FEMInterface):
69
70
  procedure_name: str = None,
70
71
  procedure_args: list or tuple = None,
71
72
  connect_method: str = 'auto', # or 'new'
73
+ procedure_timeout: float or None = None,
72
74
  ):
75
+ show_experimental_warning("ExcelInterface")
73
76
 
74
77
  # 初期化
75
78
  self.input_xlsm_path = None # あとで取得する
@@ -81,6 +84,7 @@ class ExcelInterface(FEMInterface):
81
84
  assert connect_method in ['new', 'auto']
82
85
  self.connect_method = connect_method
83
86
  self._femtet_autosave_buffer = _get_autosave_enabled()
87
+ self.procedure_timeout = procedure_timeout
84
88
 
85
89
  # dask サブプロセスのときは space 直下の input_xlsm_path を参照する
86
90
  try:
@@ -119,8 +123,9 @@ class ExcelInterface(FEMInterface):
119
123
  procedure_name=self.procedure_name,
120
124
  procedure_args=self.procedure_args,
121
125
  connect_method='new', # subprocess で connect する際は new を強制する
126
+ procedure_timeout=self.procedure_timeout,
122
127
  )
123
- super().__init__(**kwargs)
128
+ FEMInterface.__init__(self, **kwargs)
124
129
 
125
130
  def __del__(self):
126
131
  try:
@@ -256,20 +261,18 @@ class ExcelInterface(FEMInterface):
256
261
 
257
262
  # マクロ実行
258
263
  try:
259
- self.excel.Run(
260
- f'{self.wb_input.Name}!{self.procedure_name}',
261
- *self.procedure_args
262
- )
264
+ with watch_excel_macro_error(self.excel, timeout=self.procedure_timeout):
265
+ self.excel.Run(
266
+ f'{self.wb_input.Name}!{self.procedure_name}',
267
+ *self.procedure_args
268
+ )
263
269
 
264
- # FIXME: エラーハンドリング
265
- # com_error をキャッチする(solveerror)か、
266
- # sh_out に解析結果を書く(拘束違反)ようにして、
267
- # それが FALSE なら SolveError を raise する。
268
- except ...:
269
- raise SolveError('Excelアップデートに失敗しました')
270
+ # 再計算
271
+ self.excel.CalculateFull()
272
+
273
+ except com_error:
274
+ raise SolveError(f'Failed to run macro {self.wb_input.Name}!{self.procedure_name}')
270
275
 
271
- # 再計算
272
- self.excel.CalculateFull()
273
276
 
274
277
  def quit(self):
275
278
  logger.info('Excel-Femtet の終了処理を開始します。') # FIXME: message にする
@@ -3,10 +3,11 @@ import re
3
3
  from time import sleep, time
4
4
 
5
5
  import pandas as pd
6
- from dask.distributed import get_worker
6
+ from dask.distributed import get_worker, Lock
7
7
 
8
8
  from win32com.client import DispatchEx
9
- from pythoncom import CoInitialize, CoUninitialize
9
+ # noinspection PyUnresolvedReferences
10
+ from pythoncom import CoInitialize, CoUninitialize, com_error
10
11
 
11
12
  from pyfemtet.core import ModelError
12
13
  from pyfemtet.opt.interface import FemtetInterface, logger
@@ -47,34 +48,58 @@ class FemtetWithSolidworksInterface(FemtetInterface):
47
48
  def __init__(
48
49
  self,
49
50
  sldprt_path,
51
+ quit_sldworks_on_terminate=False,
50
52
  **kwargs
51
53
  ):
52
54
  # 引数の処理
53
- # dask サブプロセスのときは space 直下の sldprt_path を参照する
54
- try:
55
- worker = get_worker()
56
- space = worker.local_directory
57
- self.sldprt_path = os.path.join(space, os.path.basename(sldprt_path))
58
- except ValueError: # get_worker に失敗した場合
59
- self.sldprt_path = os.path.abspath(sldprt_path)
55
+ self._orig_sldprt_basename = os.path.basename(sldprt_path)
56
+
57
+ # # dask サブプロセスのときは space 直下の sldprt_path を参照する
58
+ # try:
59
+ # worker = get_worker()
60
+ # space = worker.local_directory
61
+ # name_ext = os.path.basename(sldprt_path)
62
+ # name, ext = os.path.splitext(name_ext)
63
+ # self.sldprt_path = os.path.join(space, name_ext)
64
+ #
65
+ # # ただし solidworks は 1 プロセスで同名のファイルを開けないので
66
+ # # 名前を更新する
67
+ # new_sldprt_path = os.path.join(
68
+ # space,
69
+ # f'{name}'
70
+ # f'_{os.path.basename(space)}' # worker に対し一意
71
+ # f'{ext}' # ext は . を含む
72
+ # )
73
+ # os.rename(
74
+ # self.sldprt_path,
75
+ # new_sldprt_path
76
+ # )
77
+ # self.sldprt_path = new_sldprt_path
78
+ #
79
+ # except ValueError: # get_worker に失敗した場合
80
+ # self.sldprt_path = os.path.abspath(sldprt_path)
81
+ self.sldprt_path = os.path.abspath(sldprt_path)
82
+ self.quit_sldworks_on_terminate = quit_sldworks_on_terminate
60
83
 
61
84
  # FemtetInterface の設定 (femprj_path, model_name の更新など)
62
85
  # + restore 情報の上書き
63
86
  super().__init__(
64
87
  sldprt_path=self.sldprt_path,
88
+ quit_sldworks_on_terminate=self.quit_sldworks_on_terminate,
65
89
  **kwargs
66
90
  )
67
91
 
68
92
  def initialize_sldworks_connection(self):
69
93
  # SolidWorks を捕まえ、ファイルを開く
94
+
70
95
  self.swApp = DispatchEx('SLDWORKS.Application')
71
96
  self.swApp.Visible = True
72
97
 
73
- # open model
74
- self.swApp.OpenDoc(self.sldprt_path, self.swDocPART)
75
- self.swModel = self.swApp.ActiveDoc
76
- self.swEqnMgr = self.swModel.GetEquationMgr
77
- self.nEquation = self.swEqnMgr.GetCount
98
+ # solidworks は単一プロセスなので開くファイルはひとつだけ
99
+ try:
100
+ get_worker()
101
+ except ValueError:
102
+ self.swApp.OpenDoc(self.sldprt_path, self.swDocPART)
78
103
 
79
104
  def check_param_value(self, param_name):
80
105
  """Override FemtetInterface.check_param_value().
@@ -104,25 +129,39 @@ class FemtetWithSolidworksInterface(FemtetInterface):
104
129
  # Femtet が参照している x_t パスを取得する
105
130
  x_t_path = self.Femtet.Gaudi.LastXTPath
106
131
 
132
+ # dask サブプロセスならば競合しないよう保存先を scratch 直下にしておく
133
+ try:
134
+ get_worker()
135
+ x_t_path = os.path.splitext(self.sldprt_path)[0] + '.x_t'
136
+
137
+ except ValueError: # No worker found
138
+ pass
139
+
107
140
  # 前のが存在するならば消しておく
108
141
  if os.path.isfile(x_t_path):
109
142
  os.remove(x_t_path)
110
143
 
111
144
  # solidworks のモデルの更新
112
- self.update_sw_model(parameters)
145
+ try:
146
+ with Lock('update-model-sldworks'):
147
+ sleep(0.5) # 並列処理でクラッシュすることが多かったため試験的に導入
148
+ self.update_sw_model(parameters, x_t_path)
113
149
 
114
- # export as x_t
115
- self.swModel.SaveAs(x_t_path)
150
+ # femopt を使わない場合
151
+ except RuntimeError: # <class 'distributed.lock.Lock'> object not properly initialized. ...
152
+ self.update_sw_model(parameters, x_t_path)
116
153
 
117
- # 30 秒待っても x_t ができてなければエラー(COM なので)
118
- timeout = 30
119
- start = time()
120
- while True:
121
- if os.path.isfile(x_t_path):
122
- break
123
- if time() - start > timeout:
124
- raise ModelError(Msg.ERR_MODEL_UPDATE_FAILED)
125
- sleep(1)
154
+ # dask サブプロセスならば LastXTPath を更新する
155
+ try:
156
+ get_worker()
157
+ try:
158
+ self.Femtet.Gaudi.LastXTPath = x_t_path
159
+ except (KeyError, AttributeError, com_error):
160
+ raise RuntimeError('This feature is available from Femtet version 2023.2. Please update Femtet.')
161
+
162
+ # dask を使わない場合
163
+ except ValueError: # No worker found
164
+ pass
126
165
 
127
166
  # モデルの再インポート
128
167
  self._call_femtet_api(
@@ -145,40 +184,78 @@ class FemtetWithSolidworksInterface(FemtetInterface):
145
184
  # femprj モデルの変数も更新
146
185
  super().update_model(parameters)
147
186
 
148
- def update_sw_model(self, parameters: pd.DataFrame):
187
+ def update_sw_model(self, parameters: pd.DataFrame, x_t_path):
149
188
  """Update .sldprt"""
189
+
150
190
  # df を dict に変換
151
191
  user_param_dict = {}
152
192
  for i, row in parameters.iterrows():
153
193
  user_param_dict[row['name']] = row['value']
154
194
 
155
- # プロパティを退避
156
- buffer_aso = self.swEqnMgr.AutomaticSolveOrder
157
- buffer_ar = self.swEqnMgr.AutomaticRebuild
158
- self.swEqnMgr.AutomaticSolveOrder = False
159
- self.swEqnMgr.AutomaticRebuild = False
160
-
161
- for i in range(self.nEquation):
162
- # name, equation の取得
163
- current_equation = self.swEqnMgr.Equation(i)
164
- current_name = self._get_name_from_equation(current_equation)
165
- # 対象なら処理
166
- if current_name in list(user_param_dict.keys()):
167
- new_equation = f'"{current_name}" = {user_param_dict[current_name]}'
168
- self.swEqnMgr.Equation(i, new_equation)
169
-
170
- # 式の計算
171
- # noinspection PyStatementEffect
172
- self.swEqnMgr.EvaluateAll # always returns -1
173
-
174
- # プロパティをもとに戻す
175
- self.swEqnMgr.AutomaticSolveOrder = buffer_aso
176
- self.swEqnMgr.AutomaticRebuild = buffer_ar
177
-
178
- # 更新する(ここで失敗はしうる)
179
- result = self.swModel.EditRebuild3 # モデル再構築
180
- if not result:
181
- raise ModelError(Msg.ERR_UPDATE_SOLIDWORKS_MODEL_FAILED)
195
+ while True:
196
+
197
+ try:
198
+
199
+ # ===== model を取得 =====
200
+ swModel = get_model_by_basename(self.swApp, os.path.basename(self.sldprt_path))
201
+
202
+ # ===== equation manager を取得 =====
203
+ swEqnMgr = swModel.GetEquationMgr
204
+ nEquation = swEqnMgr.GetCount
205
+
206
+ # プロパティを退避
207
+ buffer_aso = swEqnMgr.AutomaticSolveOrder
208
+ buffer_ar = swEqnMgr.AutomaticRebuild
209
+ swEqnMgr.AutomaticSolveOrder = False
210
+ swEqnMgr.AutomaticRebuild = False
211
+
212
+ for i in range(nEquation):
213
+ # name, equation の取得
214
+ current_equation = swEqnMgr.Equation(i)
215
+ current_name = self._get_name_from_equation(current_equation)
216
+ # 対象なら処理
217
+ if current_name in list(user_param_dict.keys()):
218
+ new_equation = f'"{current_name}" = {user_param_dict[current_name]}'
219
+ swEqnMgr.Equation(i, new_equation)
220
+
221
+ # 式の計算
222
+ # noinspection PyStatementEffect
223
+ swEqnMgr.EvaluateAll # always returns -1
224
+
225
+ # プロパティをもとに戻す
226
+ swEqnMgr.AutomaticSolveOrder = buffer_aso
227
+ swEqnMgr.AutomaticRebuild = buffer_ar
228
+
229
+ # 更新する(ここで失敗はしうる)
230
+ result = swModel.EditRebuild3 # モデル再構築
231
+ if not result:
232
+ raise ModelError(Msg.ERR_UPDATE_SOLIDWORKS_MODEL_FAILED)
233
+
234
+ # export as x_t
235
+ swModel.SaveAs(x_t_path)
236
+
237
+ # 30 秒待っても x_t ができてなければエラー(COM なのでありうる)
238
+ timeout = 30
239
+ start = time()
240
+ while True:
241
+ if os.path.isfile(x_t_path):
242
+ break
243
+ if time() - start > timeout:
244
+ raise ModelError(Msg.ERR_MODEL_UPDATE_FAILED)
245
+ sleep(1)
246
+
247
+ except AttributeError as e:
248
+ if 'SLDWORKS.Application.' in str(e):
249
+ # re-launch solidworks
250
+ self.swApp = DispatchEx('SLDWORKS.Application')
251
+ self.swApp.Visible = True
252
+ self.swApp.OpenDoc(self.sldprt_path, self.swDocPART)
253
+ continue
254
+
255
+ else:
256
+ raise e
257
+
258
+ break
182
259
 
183
260
  def _get_name_from_equation(self, equation: str):
184
261
  pattern = r'^\s*"(.+?)"\s*$'
@@ -187,3 +264,35 @@ class FemtetWithSolidworksInterface(FemtetInterface):
187
264
  return matched.group(1)
188
265
  else:
189
266
  return None
267
+
268
+ def quit(self, timeout=1, force=True):
269
+ if self.quit_sldworks_on_terminate:
270
+ try:
271
+ get_worker()
272
+ except ValueError:
273
+ try:
274
+ self.swApp.ExitApp()
275
+ except AttributeError:
276
+ pass
277
+
278
+ super().quit(timeout, force)
279
+
280
+
281
+ def get_model_by_basename(swApp, basename):
282
+ swModel = swApp.GetFirstDocument
283
+ while swModel is not None:
284
+ pathname = swModel.GetPathName
285
+ if os.path.basename(pathname) == basename:
286
+ from win32com.client import Dispatch
287
+ # swModel_ = swApp.ActivateDoc3(
288
+ # basename,
289
+ # False,
290
+ # 1, # swRebuildOnActivation_e.swDontRebuildActiveDoc,
291
+ # Dispatch("Scripting.List"),
292
+ # )
293
+ swApp.OpenDoc(pathname, 1)
294
+ swModel_ = swApp.ActiveDoc
295
+ return swModel_
296
+ else:
297
+ swModel = swModel.GetNext
298
+ raise ModuleNotFoundError(f'No model named {basename}')
@@ -7,8 +7,8 @@ import inspect
7
7
 
8
8
  # 3rd-party
9
9
  import optuna
10
- from optuna.trial import TrialState
11
- from optuna.study import MaxTrialsCallback
10
+ from optuna.trial import TrialState, FrozenTrial
11
+ from optuna.study import MaxTrialsCallback, Study
12
12
 
13
13
  # pyfemtet relative
14
14
  from pyfemtet.opt._femopt_core import OptimizationStatus, generate_lhs
@@ -86,6 +86,8 @@ class OptunaOptimizer(AbstractOptimizer):
86
86
  self.additional_initial_parameter = []
87
87
  self.additional_initial_methods = add_init_method if hasattr(add_init_method, '__iter__') else [add_init_method]
88
88
  self.method_checker = OptunaMethodChecker(self)
89
+ self._temporary_storage = None
90
+ self._temporary_storage_path = '_pyfemtet_temporary_file.db'
89
91
 
90
92
  def _objective(self, trial):
91
93
 
@@ -277,6 +279,35 @@ class OptunaOptimizer(AbstractOptimizer):
277
279
  f'sqlite:///{storage_path}',
278
280
  )
279
281
 
282
+ # if TPESampler, create temporary study
283
+ # due to instability in case of many pruned trials.
284
+ from optuna.samplers import TPESampler
285
+ if issubclass(self.sampler_class, TPESampler):
286
+
287
+ if os.path.exists(self._temporary_storage_path):
288
+ os.remove(self._temporary_storage_path)
289
+
290
+ self._temporary_storage = optuna.integration.dask.DaskStorage(
291
+ f'sqlite:///{self._temporary_storage_path}',
292
+ )
293
+
294
+ study = optuna.load_study(
295
+ study_name=None,
296
+ storage=self.storage,
297
+ sampler=None,
298
+ )
299
+
300
+ _study = optuna.create_study(
301
+ study_name='tmp',
302
+ storage=self._temporary_storage,
303
+ load_if_exists=True,
304
+ directions=['minimize'] * len(self.objectives),
305
+ )
306
+
307
+ # Copy COMPLETE trials to temporary study.
308
+ existing_trials = study.get_trials(states=(optuna.trial.TrialState.COMPLETE,))
309
+ _study.add_trials(existing_trials)
310
+
280
311
  def add_init_parameter(
281
312
  self,
282
313
  parameter: dict or Iterable,
@@ -336,62 +367,45 @@ class OptunaOptimizer(AbstractOptimizer):
336
367
  sampler=sampler,
337
368
  )
338
369
 
339
- # 一時的な実装。
340
- # TPESampler の場合、リスタート時などの場合で、
341
- # Pruned が多いとエラーを起こす挙動があるので、
342
- # Pruned な Trial は remove したい。
343
- # study.remove_trial がないので、一度ダミー
344
- # study を作成して最適化終了後に結果をコピーする。
345
- if isinstance(sampler, optuna.samplers.TPESampler):
346
- tmp_db = f"tmp{self.subprocess_idx}.db"
347
- if os.path.exists(tmp_db):
348
- os.remove(tmp_db)
349
-
350
- _study = optuna.create_study(
351
- study_name="tmp",
352
- storage=f"sqlite:///{tmp_db}",
353
- sampler=sampler,
354
- directions=['minimize']*len(self.objectives),
355
- load_if_exists=False,
356
- )
357
-
358
- # 既存の trials のうち COMPLETE のものを取得
359
- existing_trials = study.get_trials(states=(optuna.trial.TrialState.COMPLETE,))
360
- _study.add_trials(existing_trials)
361
-
370
+ # use temporary storage or not
371
+ if self._temporary_storage is None:
362
372
  # run
363
- _study.optimize(
373
+ study.optimize(
364
374
  self._objective,
365
375
  timeout=self.timeout,
366
376
  callbacks=self.optimize_callbacks,
367
377
  )
368
378
 
369
- # trial.number と trial_id は _study への add_trials 時に
370
- # 振りなおされるため重複したものをフィルタアウトするために
371
- # datetime_start を利用。
372
- added_trials = []
373
- for _trial in _study.get_trials():
374
- if _trial.datetime_start not in [t.datetime_start for t in existing_trials]:
375
- added_trials.append(_trial)
376
-
377
- # Write back added trials to the existing study.
378
- study.add_trials(added_trials)
379
+ else:
380
+ # load study
381
+ _study = optuna.load_study(
382
+ study_name="tmp",
383
+ storage=self._temporary_storage,
384
+ sampler=sampler,
385
+ )
379
386
 
380
- # clean up
381
- from optuna.storages import get_storage
382
- storage = get_storage(f"sqlite:///{tmp_db}")
383
- storage.remove_session()
384
- del _study
385
- del storage
386
- import gc
387
- gc.collect()
388
- if os.path.exists(tmp_db):
389
- os.remove(tmp_db)
387
+ # Add callback to coppy back each trial.
388
+ class CopyBack:
389
+ def __call__(self, _: Study, trial: FrozenTrial) -> None:
390
+ # Write back added trials to the existing study.
391
+ study.add_trial(trial)
392
+ self.optimize_callbacks.append(CopyBack())
390
393
 
391
- else:
392
394
  # run
393
- study.optimize(
395
+ _study.optimize(
394
396
  self._objective,
395
397
  timeout=self.timeout,
396
398
  callbacks=self.optimize_callbacks,
397
399
  )
400
+
401
+ # clean up
402
+ try:
403
+ self._temporary_storage.remove_session()
404
+ del self._temporary_storage
405
+ del _study
406
+ import gc
407
+ gc.collect()
408
+ if os.path.exists(self._temporary_storage_path):
409
+ os.remove(self._temporary_storage_path)
410
+ except:
411
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyfemtet
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: Design parameter optimization using Femtet.
5
5
  Home-page: https://github.com/pyfemtet/pyfemtet
6
6
  License: BSD-3-Clause
@@ -1,6 +1,6 @@
1
- pyfemtet/__init__.py,sha256=_SsQ0ZcyZbUqlFFT370nQxs8UER9D0oW_EmCr4Q-hx4,21
1
+ pyfemtet/__init__.py,sha256=_f8whmZJ36ZCTk0Zldzy3KONxDBm0bxyKt7pCSHC3cE,21
2
2
  pyfemtet/_femtet_config_util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- pyfemtet/_femtet_config_util/autosave.py,sha256=e-_GJdiJ5un7iuTet3bK5vwwq4Aqh7B_YVTANDJfZl8,1383
3
+ pyfemtet/_femtet_config_util/autosave.py,sha256=dNirA9XGuFehas8_Jkj2BW9GOzMbPyhnt1WHcH_ObSU,2070
4
4
  pyfemtet/_femtet_config_util/exit.py,sha256=sx8Wcepgi-lL5qJpJl6WvlzbeVheU9_Dtf1f38mmoTo,1822
5
5
  pyfemtet/_message/1. make_pot.bat,sha256=wrTA0YaL7nUfNB0cS8zljOmwq2qgyG6RMwHQbrwjvY4,476
6
6
  pyfemtet/_message/2. make_mo.bat,sha256=6shJ3Yn4BXjDc0hhv_kiGUtVTq4oSRz8-iS4vW29rNE,155
@@ -9,7 +9,9 @@ pyfemtet/_message/babel.cfg,sha256=AQIFCQ7NlAA84PhV0gowHhbIXH41zA55mzhgyROniJk,7
9
9
  pyfemtet/_message/locales/ja/LC_MESSAGES/messages.po,sha256=F2bJGHVMtk086pekjVwY2dluCSl7qeYPgJe1A9CSrxA,24526
10
10
  pyfemtet/_message/locales/messages.pot,sha256=8Yjf462pJdEtxBLySKT34zMG5CH5uLB_8VaJQll_QsY,14493
11
11
  pyfemtet/_message/messages.py,sha256=F8ENLZKoHq5irn-Ag7rqA3aSDsTmRWDyNHvOLY76ROI,13368
12
- pyfemtet/_warning.py,sha256=FaWDmGJVgIF4oDTDSRZNvEEDt3-N5HD9WrHshLSA0zc,2052
12
+ pyfemtet/_util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ pyfemtet/_util/excel_macro_util.py,sha256=Euxz3QmvTnnQCkWnbnPhvX7xXxppRONX69VlFxNCojA,7540
14
+ pyfemtet/_warning.py,sha256=TSOj8mOhuyfOUJB24LsW6GNhTA3IzIEevJw_hLKTrq8,2205
13
15
  pyfemtet/core.py,sha256=3lqfBGJ5IuKz2Nqj5pRo7YQqKwx_0ZDL72u95Ur_1p0,1386
14
16
  pyfemtet/dispatch_extensions/__init__.py,sha256=MI9b6oIS2IXnTNHy8jvZ4QURdTHQd9PN-gifYxqVvk4,272
15
17
  pyfemtet/dispatch_extensions/_impl.py,sha256=HU7rKRAzEe5yYukWrKtdi1aIbUas_kLyaa_KZZGCELE,16244
@@ -24,19 +26,19 @@ pyfemtet/opt/_test_utils/hyper_sphere.py,sha256=nQhw8EIY0DwvcTqrbKhkxiITLZifr4-n
24
26
  pyfemtet/opt/_test_utils/record_history.py,sha256=JCNJLZMCNTpJ6VT7iwEt2DIbwmsuQmgC0ClQSfcatj4,3915
25
27
  pyfemtet/opt/interface/__init__.py,sha256=5hel-mP6tuxzIEJFMZJZWUEWEbFSsskzCWlQ3HORTYI,466
26
28
  pyfemtet/opt/interface/_base.py,sha256=aPJ55dTp4-Q4KMkUZVRlquuBBWWOIOdC6yQsYZR4Jy0,2626
27
- pyfemtet/opt/interface/_excel_interface.py,sha256=ms-o31hZ6dM_gUqlXWoeodRvwZwNMnRuVuM-x6JJh9E,16483
29
+ pyfemtet/opt/interface/_excel_interface.py,sha256=7wJ8SOJaCyl6rVnmjLjw67Yv95bO1gwFBThc-cDLRYE,16649
28
30
  pyfemtet/opt/interface/_femtet.py,sha256=dmKyRG8sWuX2JHjcXpvJ2q632oZh4I94iVo4u7Z7w_M,34742
29
31
  pyfemtet/opt/interface/_femtet_parametric.py,sha256=KDG8SB43AgwuhpCStjvx10G0RzyHhga6k4dfvp0gvYU,2175
30
32
  pyfemtet/opt/interface/_femtet_with_nx/__init__.py,sha256=-6W2g2FDEcKzGHmI5KAKQe-4U5jDpMj0CXuma-GZca0,83
31
33
  pyfemtet/opt/interface/_femtet_with_nx/_interface.py,sha256=BXWdzIFcId1EovpbRD5DmkW0BwqhpDvOuGBv9kdCGy8,5994
32
34
  pyfemtet/opt/interface/_femtet_with_nx/update_model.py,sha256=P7VH0i_o-X9OUe6AGaLF1fACPeHNrMjcrOBCA3MMrI4,3092
33
- pyfemtet/opt/interface/_femtet_with_sldworks.py,sha256=4QvwcHP_L4QMHcwKf_vnuWIV-68uIqmtDh0NNaavJjg,6853
35
+ pyfemtet/opt/interface/_femtet_with_sldworks.py,sha256=A9o-IY8Npq0CHK7yGxBw47wmOtNv5IYR80U2XAabSWM,11019
34
36
  pyfemtet/opt/optimizer/__init__.py,sha256=Ia6viowECkG0IFXtFef0tJ4jDKsoDzJLqMJ9xLFH2LQ,543
35
37
  pyfemtet/opt/optimizer/_base.py,sha256=0jX68VEfLI08Qr01BvfPCHKKlr3Bj6gWQ0T81qpX0y4,12507
36
38
  pyfemtet/opt/optimizer/_optuna/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
39
  pyfemtet/opt/optimizer/_optuna/_botorch_patch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
38
40
  pyfemtet/opt/optimizer/_optuna/_botorch_patch/enable_nonlinear_constraint.py,sha256=2hUP2c8mokkRaSQ8nXxgCCmz8e0JKvEz8R2qIGnTGm0,8863
39
- pyfemtet/opt/optimizer/_optuna/_optuna.py,sha256=kdEi7N-ELWNgciD212h_afWFSO6kiRd960K9JJirVfc,16105
41
+ pyfemtet/opt/optimizer/_optuna/_optuna.py,sha256=nKlEYizSu6BQu8OMhRWRJxk5eXJ0LAPR7h6CQOjbdxE,16460
40
42
  pyfemtet/opt/optimizer/_optuna/_pof_botorch.py,sha256=yVyg1V3trqirSDtbRepgftvS02AEkAhrgjou21JS124,72717
41
43
  pyfemtet/opt/optimizer/_scipy.py,sha256=_2whhMNq6hC1lr5PlYhpZ8Zlh6-DkAjz8SVB5qHIpYg,4766
42
44
  pyfemtet/opt/optimizer/_scipy_scalar.py,sha256=rGvrLjrgfYzxK9GA0-r2Hhoaqt6A0TQsT_1M3moyklc,3615
@@ -116,8 +118,8 @@ pyfemtet/opt/visualization/result_viewer/.gitignore,sha256=ryvb4aqbbsHireHWlPQfx
116
118
  pyfemtet/opt/visualization/result_viewer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
117
119
  pyfemtet/opt/visualization/result_viewer/application.py,sha256=WcHBx_J5eNLKSaprpk9BGifwhO04oN8FiNGYTWorrXA,1691
118
120
  pyfemtet/opt/visualization/result_viewer/pages.py,sha256=laEAKHAtdshCAHxgXo-zMNg3RP6lCxfszO3XwLnF1dU,32156
119
- pyfemtet-0.6.3.dist-info/LICENSE,sha256=sVQBhyoglGJUu65-BP3iR6ujORI6YgEU2Qm-V4fGlOA,1485
120
- pyfemtet-0.6.3.dist-info/METADATA,sha256=l89gTkV_ub72Kqh496W02gf0nmGVwcYC-8w2-Us9zNk,3287
121
- pyfemtet-0.6.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
122
- pyfemtet-0.6.3.dist-info/entry_points.txt,sha256=ZfYqRaoiPtuWqFi2_msccyrVF0LurMn-IHlYamAegZo,104
123
- pyfemtet-0.6.3.dist-info/RECORD,,
121
+ pyfemtet-0.6.5.dist-info/LICENSE,sha256=sVQBhyoglGJUu65-BP3iR6ujORI6YgEU2Qm-V4fGlOA,1485
122
+ pyfemtet-0.6.5.dist-info/METADATA,sha256=04L0xEjP4MQaiIYEV0YZ6rXzf2P1ruOZzyW4hvhsXts,3287
123
+ pyfemtet-0.6.5.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
124
+ pyfemtet-0.6.5.dist-info/entry_points.txt,sha256=ZfYqRaoiPtuWqFi2_msccyrVF0LurMn-IHlYamAegZo,104
125
+ pyfemtet-0.6.5.dist-info/RECORD,,