pyfemtet 0.6.6__py3-none-any.whl → 0.7.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.

@@ -1,6 +1,7 @@
1
1
  import os
2
2
  from pathlib import Path
3
3
  from time import sleep
4
+ import gc
4
5
 
5
6
  import pandas as pd
6
7
  import numpy as np
@@ -20,29 +21,165 @@ from pyfemtet.core import SolveError
20
21
  from pyfemtet.opt.optimizer.parameter import Parameter
21
22
 
22
23
  from pyfemtet.dispatch_extensions import _get_pid, dispatch_specific_femtet
23
- from pyfemtet.dispatch_extensions._impl import _NestableSpawnProcess
24
24
 
25
25
  from pyfemtet._femtet_config_util.exit import _exit_or_force_terminate
26
- from pyfemtet._femtet_config_util.autosave import _set_autosave_enabled, _get_autosave_enabled
27
26
 
28
27
  from pyfemtet._util.excel_macro_util import watch_excel_macro_error
28
+ from pyfemtet._util.dask_util import lock_or_no_lock
29
29
 
30
30
  from pyfemtet._warning import show_experimental_warning
31
31
 
32
- import logging
33
-
34
- logger = logging.getLogger(__name__)
35
- logger.setLevel(logging.INFO)
32
+ from pyfemtet.opt.interface._base import logger
36
33
 
37
34
 
38
35
  class ExcelInterface(FEMInterface):
39
-
40
- input_xlsm_path: Path # 操作対象の xlsm パス
36
+ """Excel を計算コアとして利用するためのクラス。
37
+
38
+ 通常の有限要素法を Excel に
39
+ 置き換えて使用することが可能です。
40
+
41
+ すでに Excel マクロと Femtet を
42
+ 連携させた自動解析システムを
43
+ 構築している場合、このクラスは
44
+ それをラップします。これにより、
45
+ PyFemtet を用いた最適化を
46
+ 行う際に便利な機能を提供します。
47
+
48
+ Args:
49
+ input_xlsm_path (str or Path):
50
+ 設計変数の定義を含む Excel ファイルのパスを指定
51
+ します。
52
+
53
+ input_sheet_name (str):
54
+ 設計変数の定義を含むシートの名前を指定します。
55
+
56
+ output_xlsm_path (str or Path, optional):
57
+ 目的関数の定義を含む Excel ファイルのパスを指定
58
+ します。指定しない場合は ``input_xlsm_path`` と
59
+ 同じと見做します。
60
+
61
+ output_sheet_name (str, optional):
62
+ 目的関数の定義を含む含むシートの名前を指定します。
63
+ 指定しない場合は ``input_sheet_name`` と同じと見
64
+ 做します。
65
+
66
+ procedure_name (str, optional):
67
+ Excel マクロ関数名を指定します。指定しない場合は
68
+ ``FemtetMacro.FemtetMain`` と見做します。
69
+
70
+ procedure_args (list or tuple, optional):
71
+ Excel マクロ関数に渡す引数をリストまたはタプルで
72
+ 指定します。
73
+
74
+ connect_method (str, optional):
75
+ Excel との接続方法を指定します。 'auto' または
76
+ 'new' が利用可能です。デフォルトは 'auto' です。
77
+
78
+ procedure_timeout (float or None, optional):
79
+ Excel マクロ関数のタイムアウト時間を秒単位で指定
80
+ します。 None の場合はタイムアウトなしとなります。
81
+
82
+ setup_xlsm_path (str or Path, optional):
83
+ セットアップ時に呼ぶ関数を含む xlsm のパスです。
84
+ 指定しない場合は ``input_xlsm_path`` と
85
+ 同じと見做します。
86
+
87
+ setup_procedure_name (str, optional):
88
+ セットアップ時に呼ぶマクロ関数名です。
89
+ 指定しない場合、セットアップ時に何もしません。
90
+
91
+ setup_procedure_args (list or tuple, optional):
92
+ セットアップ時に呼ぶマクロ関数の引数です。
93
+
94
+ teardown_xlsm_path (str or Path, optional):
95
+ 終了時に呼ぶ関数を含む xlsm のパスです。
96
+ 指定しない場合は ``input_xlsm_path`` と
97
+ 同じと見做します。
98
+
99
+ teardown_procedure_name (str, optional):
100
+ 終了時に呼ぶマクロ関数名です。
101
+ 指定しない場合、終了時に何もしません。
102
+
103
+ teardown_procedure_args (list or tuple, optional):
104
+ 終了時に呼ぶマクロ関数の引数です。
105
+
106
+ visible (bool):
107
+ excel を可視化するかどうかです。
108
+ ただし、 True を指定した場合でもマクロの実行中は
109
+ 不可視になります。
110
+ デフォルトは False です。
111
+
112
+ display_alerts (bool):
113
+ excel ダイアログを表示するかどうかです。
114
+ デバッグ目的の場合以外は True にしないでください。
115
+ デフォルトは False です。
116
+
117
+ terminate_excel_when_quit (bool):
118
+ 終了時に Excel を終了するかどうかです。
119
+ 指定しない場合、 connect_method が 'new' の場合
120
+ True とふるまい 'auto' の場合 False と振舞います。
121
+
122
+ interactive (bool):
123
+ excel を対話モードにするかどうかです。
124
+ False にすると、 visible == True であっても
125
+ 自動化プロセス中にユーザーが誤って
126
+ Excel 本体を操作できないようにします。
127
+ デフォルトは True です。
128
+
129
+ Attributes:
130
+ input_xlsm_path (Path):
131
+ 設計変数の定義を含む Excel ファイルのパス。
132
+
133
+ input_sheet_name (str):
134
+ 設計変数の定義を含むシートの名前。
135
+
136
+ output_xlsm_path (Path):
137
+ 目的関数の定義を含む Excel ファイルのパス。
138
+
139
+ output_sheet_name (str):
140
+ 目的関数の定義を含む含むシートの名前。
141
+
142
+ procedure_name (str):
143
+ 実行する Excel マクロ関数名。
144
+
145
+ procedure_args (list or tuple):
146
+ Excel マクロ関数に渡す引数のリストまたはタプル。
147
+
148
+ connect_method (str):
149
+ 接続方法。'new' または 'auto'。
150
+
151
+ procedure_timeout (float or None):
152
+ Excel マクロ関数の実行タイムアウト。
153
+ Noneの場合は無制限。
154
+
155
+ terminate_excel_when_quit (bool):
156
+ プログラム終了時に Excel を終了するかどうか。
157
+ connect_method が 'new' の場合 True,
158
+ 'auto' の場合 False。
159
+
160
+ excel (CDispatch):
161
+ Excel の COM オブジェクト。
162
+
163
+ input_sheet (CDispatch):
164
+ 設計変数を含むシートの COM オブジェクト。
165
+
166
+ output_sheet (CDispatch):
167
+ 目的関数を含むシートの COM オブジェクト。
168
+
169
+ input_workbook (CDispatch):
170
+ 設計変数を含む xlsm ファイルの COM オブジェクト。
171
+
172
+ output_workbook (CDispatch):
173
+ 設計変数を含む xlsm ファイルの COM オブジェクト。
174
+
175
+ """
176
+
177
+ input_xlsm_path: str # 操作対象の xlsm パス
41
178
  input_sheet_name: str # 変数セルを定義しているシート名
42
- output_xlsm_path: Path # 操作対象の xlsm パス (指定しない場合、input と同一)
179
+ output_xlsm_path: str # 操作対象の xlsm パス (指定しない場合、input と同一)
43
180
  output_sheet_name: str # 計算結果セルを定義しているシート名 (指定しない場合、input と同一)
44
181
 
45
- # TODO: related_file_paths: dict[Path] を必要なら実装 # 並列時に個別に並列プロセスの space にアップロードする必要のあるパス
182
+ related_file_paths: list[str] # 並列時に個別に並列プロセスの space にアップロードする必要のあるパス
46
183
 
47
184
  procedure_name: str # マクロ関数名(or モジュール名.関数名)
48
185
  procedure_args: list # マクロ関数の引数
@@ -52,15 +189,27 @@ class ExcelInterface(FEMInterface):
52
189
  sh_input: CDispatch # 変数の定義された WorkSheet
53
190
  wb_output: CDispatch # システムを構成する Workbook
54
191
  sh_output: CDispatch # 計算結果の定義された WorkSheet (sh_input と同じでもよい)
192
+ wb_setup: CDispatch # システムを構成する Workbook
193
+ wb_teardown: CDispatch # システムを構成する Workbook
55
194
 
56
- visible: bool = False # excel を可視化するかどうか
57
- display_alerts: bool = False # ダイアログを表示するかどうか
195
+ visible: bool # excel を可視化するかどうか
196
+ display_alerts: bool # ダイアログを表示するかどうか
197
+ terminate_excel_when_quit: bool # 終了時に Excel を終了するかどうか
198
+ interactive: bool # excel を対話モードにするかどうか
58
199
 
59
- _load_problem_from_me: bool = True # TODO: add_parameter() 等を省略するかどうか。定義するだけでフラグとして機能する。
200
+ _load_problem_from_me: bool = True
60
201
  _excel_pid: int
61
202
  _excel_hwnd: int
203
+ _with_femtet_autosave_setting: bool = True # Femtet の自動保存機能の自動設定を行うかどうか。Femtet がインストールされていない場合はオフにする。クラス変数なので、インスタンス化前に設定する。
62
204
  _femtet_autosave_buffer: bool # Femtet の自動保存機能の一時退避場所。最適化中はオフにする。
63
205
 
206
+ setup_xlsm_path: str
207
+ setup_procedure_name: str
208
+ setup_procedure_args: list or tuple
209
+ teardown_xlsm_path: str
210
+ teardown_procedure_name: str
211
+ teardown_procedure_args: list or tuple
212
+
64
213
  def __init__(
65
214
  self,
66
215
  input_xlsm_path: str or Path,
@@ -71,48 +220,67 @@ class ExcelInterface(FEMInterface):
71
220
  procedure_args: list or tuple = None,
72
221
  connect_method: str = 'auto', # or 'new'
73
222
  procedure_timeout: float or None = None,
223
+ setup_xlsm_path: str or Path = None,
224
+ setup_procedure_name: str = None,
225
+ setup_procedure_args: list or tuple = None,
226
+ teardown_xlsm_path: str or Path = None,
227
+ teardown_procedure_name: str = None,
228
+ teardown_procedure_args: list or tuple = None,
229
+ related_file_paths: list[str or Path] = None,
230
+ visible: bool = False,
231
+ display_alerts: bool = False,
232
+ terminate_excel_when_quit: bool = None,
233
+ interactive: bool = True,
74
234
  ):
235
+
75
236
  show_experimental_warning("ExcelInterface")
76
237
 
77
238
  # 初期化
78
- self.input_xlsm_path = None # あとで取得する
239
+ self.input_xlsm_path = str(input_xlsm_path) # あとで再取得する
79
240
  self.input_sheet_name = input_sheet_name
80
- self.output_xlsm_path = None # あとで取得する
241
+ self.output_xlsm_path = str(input_xlsm_path) if output_xlsm_path is None else str(output_xlsm_path)
81
242
  self.output_sheet_name = output_sheet_name or self.input_sheet_name
82
243
  self.procedure_name = procedure_name or 'FemtetMacro.FemtetMain'
83
244
  self.procedure_args = procedure_args or []
84
245
  assert connect_method in ['new', 'auto']
85
246
  self.connect_method = connect_method
86
- self._femtet_autosave_buffer = _get_autosave_enabled()
87
247
  self.procedure_timeout = procedure_timeout
248
+ if terminate_excel_when_quit is None:
249
+ self.terminate_excel_when_quit = self.connect_method == 'new'
250
+ else:
251
+ self.terminate_excel_when_quit = terminate_excel_when_quit
252
+
253
+ self.setup_xlsm_path = str(input_xlsm_path) if setup_xlsm_path is None else str(setup_xlsm_path) # あとで取得する
254
+ self.setup_procedure_name = setup_procedure_name
255
+ self.setup_procedure_args = setup_procedure_args or []
256
+
257
+ self.teardown_xlsm_path = str(input_xlsm_path) if teardown_xlsm_path is None else str(teardown_xlsm_path) # あとで取得する
258
+ self.teardown_procedure_name = teardown_procedure_name
259
+ self.teardown_procedure_args = teardown_procedure_args or []
260
+
261
+ self.related_file_paths = [str(p) for p in related_file_paths] if related_file_paths is not None else []
262
+
263
+ self.visible = visible
264
+ self.interactive = interactive
265
+ self.display_alerts = display_alerts
88
266
 
89
267
  # dask サブプロセスのときは space 直下の input_xlsm_path を参照する
90
268
  try:
91
269
  worker = get_worker()
92
- space = worker.local_directory
93
- self.input_xlsm_path = Path(os.path.join(space, os.path.basename(input_xlsm_path))).resolve()
94
- self.output_xlsm_path = Path(os.path.join(space, os.path.basename(output_xlsm_path))).resolve()
270
+ space = os.path.abspath(worker.local_directory)
271
+ self.input_xlsm_path = os.path.join(space, os.path.basename(self.input_xlsm_path))
272
+ self.output_xlsm_path = os.path.join(space, os.path.basename(self.output_xlsm_path))
273
+ self.setup_xlsm_path = os.path.join(space, os.path.basename(self.setup_xlsm_path))
274
+ self.teardown_xlsm_path = os.path.join(space, os.path.basename(self.teardown_xlsm_path))
275
+ self.related_file_paths = [os.path.join(space, os.path.basename(p)) for p in self.related_file_paths]
95
276
 
96
277
  # main プロセスの場合は絶対パスを参照する
97
278
  except ValueError:
98
- self.input_xlsm_path = Path(os.path.abspath(input_xlsm_path)).resolve()
99
- if output_xlsm_path is None:
100
- self.output_xlsm_path = self.input_xlsm_path
101
- else:
102
- self.output_xlsm_path = Path(os.path.abspath(output_xlsm_path)).resolve()
103
-
104
- # FIXME:
105
- # そもそも ExcelInterface なので Femtet 関連のことをやらなくていいと思う。
106
- # FemtetRef が文句なく動けばいいのだが、手元環境ではなぜか動いたり動かなかったりするため
107
- # 仕方なく Femtet を Python 側から動かしている仮実装。
108
- # 先に femtet を起動
109
- util.execute_femtet()
110
-
111
- # 直後の Excel 起動に間に合わない場合があったため
112
- # Femtet が Dispatch 可能になるまで捨てプロセスで待つ
113
- p = _NestableSpawnProcess(target=wait_femtet)
114
- p.start()
115
- p.join()
279
+ self.input_xlsm_path = os.path.abspath(self.input_xlsm_path)
280
+ self.output_xlsm_path = os.path.abspath(self.output_xlsm_path)
281
+ self.setup_xlsm_path = os.path.abspath(self.setup_xlsm_path)
282
+ self.teardown_xlsm_path = os.path.abspath(self.teardown_xlsm_path)
283
+ self.related_file_paths = [os.path.abspath(p) for p in self.related_file_paths]
116
284
 
117
285
  # サブプロセスでの restore のための情報保管
118
286
  kwargs = dict(
@@ -123,25 +291,109 @@ class ExcelInterface(FEMInterface):
123
291
  procedure_name=self.procedure_name,
124
292
  procedure_args=self.procedure_args,
125
293
  connect_method='new', # subprocess で connect する際は new を強制する
294
+ terminate_excel_when_quit=True, # なので終了時は破棄する
126
295
  procedure_timeout=self.procedure_timeout,
296
+ setup_xlsm_path=self.setup_xlsm_path,
297
+ setup_procedure_name=self.setup_procedure_name,
298
+ setup_procedure_args=self.setup_procedure_args,
299
+ teardown_xlsm_path=self.teardown_xlsm_path,
300
+ teardown_procedure_name=self.teardown_procedure_name,
301
+ teardown_procedure_args=self.teardown_procedure_args,
302
+ related_file_paths=self.related_file_paths,
303
+ visible=self.visible,
304
+ interactive=self.interactive,
305
+ display_alerts=self.display_alerts,
127
306
  )
128
307
  FEMInterface.__init__(self, **kwargs)
129
308
 
130
309
  def __del__(self):
131
- try:
132
- _set_autosave_enabled(self._femtet_autosave_buffer)
133
- finally:
134
- pass
310
+ pass
135
311
 
136
312
  def _setup_before_parallel(self, client) -> None:
137
313
  # メインプロセスで、並列プロセスを開始する前に行う前処理
138
314
 
139
- input_xlsm_path: Path = self.kwargs['input_xlsm_path']
140
- output_xlsm_path: Path = self.kwargs['output_xlsm_path']
315
+ client.upload_file(self.input_xlsm_path, False)
316
+
317
+ if not is_same_path(self.input_xlsm_path, self.output_xlsm_path):
318
+ client.upload_file(self.output_xlsm_path, False)
319
+
320
+ if not is_same_path(self.input_xlsm_path, self.setup_xlsm_path):
321
+ client.upload_file(self.setup_xlsm_path, False)
141
322
 
142
- client.upload_file(str(input_xlsm_path), False)
143
- if input_xlsm_path.resolve() != output_xlsm_path.resolve():
144
- client.upload_file(str(output_xlsm_path), False)
323
+ if not is_same_path(self.input_xlsm_path, self.teardown_xlsm_path):
324
+ client.upload_file(self.setup_xlsm_path, False)
325
+
326
+ for path in self.related_file_paths:
327
+ client.upload_file(path, False)
328
+
329
+ def _setup_after_parallel(self, *args, **kwargs):
330
+ """サブプロセス又はメインプロセスのサブスレッドで、最適化を開始する前の前処理"""
331
+
332
+ # kwargs で space_dir が与えられている場合、そちらを使用する
333
+ # メインプロセスで呼ばれることを想定
334
+ if 'space_dir' in kwargs.keys():
335
+ space = kwargs['space_dir']
336
+ if space is not None:
337
+ self.input_xlsm_path = os.path.join(space, os.path.basename(self.input_xlsm_path))
338
+ self.output_xlsm_path = os.path.join(space, os.path.basename(self.output_xlsm_path))
339
+ self.setup_xlsm_path = os.path.join(space, os.path.basename(self.setup_xlsm_path))
340
+ self.teardown_xlsm_path = os.path.join(space, os.path.basename(self.teardown_xlsm_path))
341
+ self.related_file_paths = [os.path.join(space, os.path.basename(p)) for p in self.related_file_paths]
342
+
343
+ # connect_method が auto でかつ使用中のファイルを開こうとする場合に備えて excel のファイル名を変更
344
+ subprocess_idx = kwargs['opt'].subprocess_idx
345
+
346
+ def proc_path(path, ignore_no_exists):
347
+ exclude_ext, ext = os.path.splitext(path)
348
+ new_path = exclude_ext + f'{subprocess_idx}' + ext
349
+ if os.path.exists(path): # input と output が同じの場合など。input がないのはおかしい
350
+ os.rename(path, new_path)
351
+ elif not ignore_no_exists:
352
+ raise FileNotFoundError(f'{path} が見つかりません。')
353
+ return new_path
354
+
355
+ self.input_xlsm_path = proc_path(self.input_xlsm_path, False)
356
+ self.output_xlsm_path = proc_path(self.output_xlsm_path, True)
357
+ self.setup_xlsm_path = proc_path(self.setup_xlsm_path, True)
358
+ self.teardown_xlsm_path = proc_path(self.teardown_xlsm_path, True)
359
+
360
+ # スレッドが変わっているかもしれないので win32com の初期化
361
+ CoInitialize()
362
+
363
+ # 最適化中は femtet の autosave を無効にする
364
+ if self._with_femtet_autosave_setting:
365
+ from pyfemtet._femtet_config_util.autosave import _set_autosave_enabled, _get_autosave_enabled
366
+ self._femtet_autosave_buffer = _get_autosave_enabled()
367
+ _set_autosave_enabled(False)
368
+
369
+ # excel に繋ぐ
370
+ self.connect_excel(self.connect_method)
371
+
372
+ # load_objective は 1 回目に呼ばれたのが main thread なので
373
+ # subprocess に入った後でもう一度 load objective を行う
374
+ from pyfemtet.opt.optimizer import AbstractOptimizer
375
+ from pyfemtet.opt._femopt_core import Objective
376
+ opt: AbstractOptimizer = kwargs['opt']
377
+ obj: Objective
378
+ for obj_name, obj in opt.objectives.items():
379
+ if isinstance(obj.fun, ScapeGoatObjective):
380
+ opt.objectives[obj_name].fun = self.objective_from_excel
381
+
382
+ # excel の setup 関数を必要なら実行する
383
+ if self.setup_procedure_name is not None:
384
+ with lock_or_no_lock('excel_setup_procedure'):
385
+ try:
386
+ with watch_excel_macro_error(self.excel, timeout=self.procedure_timeout, restore_book=False):
387
+ self.excel.Run(
388
+ f'{self.setup_procedure_name}',
389
+ *self.setup_procedure_args
390
+ )
391
+
392
+ # 再計算
393
+ self.excel.CalculateFull()
394
+
395
+ except com_error as e:
396
+ raise RuntimeError(f'Failed to run macro {self.setup_procedure_args}. The original message is: {e}')
145
397
 
146
398
  def connect_excel(self, connect_method):
147
399
 
@@ -162,6 +414,7 @@ class ExcelInterface(FEMInterface):
162
414
  # 可視性の設定
163
415
  self.excel.Visible = self.visible
164
416
  self.excel.DisplayAlerts = self.display_alerts
417
+ self.excel.Interactive = self.interactive
165
418
 
166
419
  # 開く
167
420
  self.excel.Workbooks.Open(str(self.input_xlsm_path))
@@ -180,11 +433,10 @@ class ExcelInterface(FEMInterface):
180
433
  else:
181
434
  raise RuntimeError(f'Sheet {self.input_sheet_name} does not exist in the book {self.wb_input.Name}.')
182
435
 
183
- if self.input_xlsm_path.resolve() == self.output_xlsm_path.resolve():
436
+ # 開く (output)
437
+ if is_same_path(self.input_xlsm_path, self.output_xlsm_path):
184
438
  self.wb_output = self.wb_input
185
-
186
439
  else:
187
- # 開く (output)
188
440
  self.excel.Workbooks.Open(str(self.output_xlsm_path))
189
441
  for wb in self.excel.Workbooks:
190
442
  if wb.Name == os.path.basename(self.output_xlsm_path):
@@ -201,49 +453,84 @@ class ExcelInterface(FEMInterface):
201
453
  else:
202
454
  raise RuntimeError(f'Sheet {self.output_sheet_name} does not exist in the book {self.wb_output.Name}.')
203
455
 
456
+ # 開く (setup)
457
+ if is_same_path(self.input_xlsm_path, self.setup_xlsm_path):
458
+ self.wb_setup = self.wb_input
459
+ else:
460
+ self.excel.Workbooks.Open(self.setup_xlsm_path)
461
+ for wb in self.excel.Workbooks:
462
+ if wb.Name == os.path.basename(self.setup_xlsm_path):
463
+ self.wb_setup = wb
464
+ break
465
+ else:
466
+ raise RuntimeError(f'Cannot open {self.setup_xlsm_path}')
467
+
468
+ # 開く (teardown)
469
+ if is_same_path(self.input_xlsm_path, self.teardown_xlsm_path):
470
+ self.wb_teardown = self.wb_input
471
+ else:
472
+ self.excel.Workbooks.Open(self.teardown_xlsm_path)
473
+ for wb in self.excel.Workbooks:
474
+ if wb.Name == os.path.basename(self.teardown_xlsm_path):
475
+ self.wb_teardown = wb
476
+ break
477
+ else:
478
+ raise RuntimeError(f'Cannot open {self.teardown_xlsm_path}')
479
+
204
480
  # book に参照設定を追加する
205
481
  self.add_femtet_ref_xla(self.wb_input)
206
482
  self.add_femtet_ref_xla(self.wb_output)
483
+ self.add_femtet_ref_xla(self.wb_setup)
484
+ self.add_femtet_ref_xla(self.wb_teardown)
207
485
 
208
486
  def add_femtet_ref_xla(self, wb):
209
487
 
488
+ # search
489
+ ref_file_1 = r'C:\Program Files\Microsoft Office\root\Office16\XLSTART\FemtetRef.xla'
490
+ if not os.path.exists(ref_file_1):
491
+ # 32bit
492
+ ref_file_1 = r'C:\Program Files (x86)\Microsoft Office\root\Office16\XLSTART\FemtetRef.xla'
493
+ if not os.path.exists(ref_file_1):
494
+ raise FileNotFoundError(f'{ref_file_1} not found. Please check the "Enable Macros" command was fired.')
495
+ contain_1 = False
496
+ for ref in wb.VBProject.References:
497
+ if ref.FullPath is not None:
498
+ if ref.FullPath.lower() == ref_file_1.lower():
499
+ contain_1 = True
500
+ # add
501
+ if not contain_1:
502
+ wb.VBProject.References.AddFromFile(ref_file_1)
503
+
210
504
  # search
211
505
  ref_file_2 = os.path.abspath(util._get_femtetmacro_dllpath())
212
- contain = False
506
+ contain_2 = False
213
507
  for ref in wb.VBProject.References:
214
508
  if ref.Description is not None:
215
509
  if ref.Description == 'FemtetMacro': # FemtetMacro
216
- contain = True
510
+ contain_2 = True
217
511
  # add
218
- if not contain:
512
+ if not contain_2:
219
513
  wb.VBProject.References.AddFromFile(ref_file_2)
220
514
 
221
- def _setup_after_parallel(self, *args, **kwargs):
222
- # サブプロセス又はメインプロセスのサブスレッドで、最適化を開始する前の前処理
223
-
224
- # スレッドが変わっているかもしれないので win32com の初期化
225
- CoInitialize()
226
-
227
- # 最適化中は femtet の autosave を無効にする
228
- _set_autosave_enabled(False)
229
-
230
- # excel に繋ぎなおす
231
- self.connect_excel(self.connect_method)
515
+ def remove_femtet_ref_xla(self, wb):
232
516
 
233
- # load_objective は 1 回目に呼ばれたのが main thread なので
234
- # subprocess に入った後でもう一度 load objective を行う
235
- from pyfemtet.opt.optimizer import AbstractOptimizer
236
- from pyfemtet.opt._femopt_core import Objective
237
- opt: AbstractOptimizer = kwargs['opt']
238
- obj: Objective
239
- for obj_name, obj in opt.objectives.items():
240
- if isinstance(obj.fun, ScapeGoatObjective):
241
- opt.objectives[obj_name].fun = self.objective_from_excel
517
+ # search
518
+ ref_file_1 = r'C:\Program Files\Microsoft Office\root\Office16\XLSTART\FemtetRef.xla'
519
+ if not os.path.exists(ref_file_1):
520
+ # 32bit
521
+ ref_file_1 = r'C:\Program Files (x86)\Microsoft Office\root\Office16\XLSTART\FemtetRef.xla'
522
+ if not os.path.exists(ref_file_1):
523
+ raise FileNotFoundError(f'{ref_file_1} not found. Please check the "Enable Macros" command was fired.')
524
+ for ref in wb.VBProject.References:
525
+ if ref.FullPath is not None:
526
+ if ref.FullPath == ref_file_1: # or ``FemtetMacroを使用するための参照設定を自動で行ないます。``
527
+ wb.VBProject.References.Remove(ref)
242
528
 
243
- # TODO:
244
- # __init__ が作った Femtet 以外を排他処理して
245
- # Excel がそれを使うことを保証するようにする(遅すぎるか?)
246
- # そもそも、excel から Femtet を起動できればそれで済む(?)。
529
+ # search
530
+ for ref in wb.VBProject.References:
531
+ if ref.Description is not None:
532
+ if ref.Description == 'FemtetMacro': # FemtetMacro
533
+ wb.VBProject.References.Remove(ref)
247
534
 
248
535
  def update(self, parameters: pd.DataFrame) -> None:
249
536
 
@@ -273,43 +560,77 @@ class ExcelInterface(FEMInterface):
273
560
  except com_error as e:
274
561
  raise SolveError(f'Failed to run macro {self.procedure_name}. The original message is: {e}')
275
562
 
276
-
277
563
  def quit(self):
278
- logger.info('Excel-Femtet の終了処理を開始します。') # FIXME: message にする
564
+ if self.terminate_excel_when_quit:
565
+
566
+ already_terminated = not hasattr(self, 'excel')
567
+ if already_terminated:
568
+ return
279
569
 
280
- del self.sh_input
281
- del self.sh_output
570
+ logger.info('Excel の終了処理を開始します。')
282
571
 
283
- self.wb_input.Close(SaveChanges := False)
284
- if self.input_xlsm_path.name != self.output_xlsm_path.name:
285
- self.wb_output.Close(SaveChanges := False)
286
- del self.wb_input
287
- del self.wb_output
572
+ # 参照設定解除の前に終了処理を必要なら実施する
573
+ # excel setup 関数を必要なら実行する
574
+ if self.teardown_procedure_name is not None:
575
+ with lock_or_no_lock('excel_setup_procedure'):
576
+ try:
577
+ with watch_excel_macro_error(self.excel, timeout=self.procedure_timeout, restore_book=False):
578
+ self.excel.Run(
579
+ f'{self.teardown_procedure_name}',
580
+ *self.teardown_procedure_args
581
+ )
288
582
 
289
- self.excel.Quit()
290
- del self.excel
583
+ # 再計算
584
+ self.excel.CalculateFull()
291
585
 
292
- import gc
293
- gc.collect()
586
+ except com_error as e:
587
+ raise RuntimeError(f'Failed to run macro {self.teardown_procedure_args}. The original message is: {e}')
294
588
 
295
- # quit した後ならば femtet を終了できる
296
- # excel の process の消滅を待つ
297
- logger.info('Excel の終了を待っています。')
298
- while self._excel_pid == _get_pid(self._excel_hwnd):
299
- sleep(1)
589
+ # 参照設定を解除する(不要な処理かも)
590
+ self.remove_femtet_ref_xla(self.wb_input)
591
+ self.remove_femtet_ref_xla(self.wb_output)
592
+ self.remove_femtet_ref_xla(self.wb_setup)
593
+ self.remove_femtet_ref_xla(self.wb_teardown)
300
594
 
301
- # 正確だが時間がかかるかも
302
- logger.info('終了する Femtet を特定しています。')
303
- femtet_pid = util.get_last_executed_femtet_process_id()
304
- from multiprocessing import Process
305
- p = Process(target=_terminate_femtet, args=(femtet_pid,))
306
- p.start()
595
+ # シートの COM オブジェクト変数を削除する
596
+ del self.sh_input
597
+ del self.sh_output
307
598
 
308
- logger.info('自動保存機能の設定を元に戻しています。')
309
- _set_autosave_enabled(self._femtet_autosave_buffer)
599
+ # workbook を閉じる
600
+ with watch_excel_macro_error(self.excel, timeout=10, restore_book=False):
601
+ self.wb_input.Close(SaveChanges := False)
310
602
 
311
- p.join()
312
- logger.info('Excel-Femtet を終了しました。')
603
+ if not is_same_path(self.input_xlsm_path, self.output_xlsm_path):
604
+ with watch_excel_macro_error(self.excel, timeout=10, restore_book=False):
605
+ self.wb_output.Close(SaveChanges := False)
606
+
607
+ if not is_same_path(self.input_xlsm_path, self.setup_xlsm_path):
608
+ with watch_excel_macro_error(self.excel, timeout=10, restore_book=False):
609
+ self.wb_setup.Close(SaveChanges := False)
610
+
611
+ if not is_same_path(self.input_xlsm_path, self.teardown_xlsm_path):
612
+ with watch_excel_macro_error(self.excel, timeout=10, restore_book=False):
613
+ self.wb_teardown.Close(SaveChanges := False)
614
+
615
+ del self.wb_input
616
+ del self.wb_output
617
+ del self.wb_setup
618
+ del self.wb_teardown
619
+
620
+
621
+ # excel の終了
622
+ with watch_excel_macro_error(self.excel, timeout=10, restore_book=False):
623
+ self.excel.Quit()
624
+ del self.excel
625
+
626
+ # ここで Excel のプロセスが残らず落ちる
627
+ gc.collect()
628
+
629
+ if self._with_femtet_autosave_setting:
630
+ from pyfemtet._femtet_config_util.autosave import _set_autosave_enabled
631
+ logger.info('自動保存機能の設定を元に戻しています。')
632
+ _set_autosave_enabled(self._femtet_autosave_buffer)
633
+ logger.info('自動保存機能の設定を元に戻しました。')
313
634
 
314
635
  # 直接アクセスしてもよいが、ユーザーに易しい名前にするためだけのプロパティ
315
636
  @property
@@ -432,6 +753,7 @@ def _terminate_femtet(femtet_pid_):
432
753
  Femtet, caught_pid = dispatch_specific_femtet(femtet_pid_)
433
754
  _exit_or_force_terminate(timeout=3, Femtet=Femtet, force=True)
434
755
 
756
+
435
757
  # main thread で作成した excel への参照を含む関数を
436
758
  # 直接 thread や process に渡すと機能しない
437
759
  class ScapeGoatObjective:
@@ -443,5 +765,12 @@ class ScapeGoatObjective:
443
765
  return tuple()
444
766
 
445
767
 
768
+ def is_same_path(p1, p2):
769
+ _p1 = os.path.abspath(p1).lower()
770
+ _p2 = os.path.abspath(p2).lower()
771
+ return _p1 == _p2
772
+
773
+
774
+
446
775
  if __name__ == '__main__':
447
776
  ExcelInterface(..., ...)