Source code for pyfemtet.opt.interface._excel_interface.excel_interface

from __future__ import annotations

from typing import TYPE_CHECKING

import os
import gc
import tempfile
from time import sleep
from pathlib import Path

import numpy as np

from win32com.client import DispatchEx, Dispatch
from win32com.client.dynamic import CDispatch
# noinspection PyUnresolvedReferences
from pythoncom import CoInitialize, CoUninitialize
# noinspection PyUnresolvedReferences
from pywintypes import com_error
from femtetutils import util

from pyfemtet._util.dask_util import *
from pyfemtet._util.process_util import *
from pyfemtet._util.excel_parse_util import *
from pyfemtet._util.excel_macro_util import *
from pyfemtet._util.femtet_autosave import *
from pyfemtet._i18n import _

from pyfemtet.opt.exceptions import *
from pyfemtet.opt.problem.problem import *
from pyfemtet.opt.interface._base_interface import COMInterface

from pyfemtet.logger import get_module_logger
from pyfemtet._util.helper import float_

if TYPE_CHECKING:
    from pyfemtet.opt.optimizer import AbstractOptimizer
    from pyfemtet.opt.problem.problem import Objective, Constraint

logger = get_module_logger('opt.interface')


class WorkbookNotOpenedError(Exception):
    pass


class WorkSheetNotFoundError(Exception):
    pass


[docs] class ExcelInterface(COMInterface): """Excel を計算コアとして利用するためのクラス。 通常の有限要素法を Excel に 置き換えて使用することが可能です。 すでに Excel マクロと Femtet を 連携させた自動解析システムを 構築している場合、このクラスは それをラップします。これにより、 PyFemtet を用いた最適化を 行う際に便利な機能を提供します。 Args: input_xlsm_path (str or Path): 設計変数の定義を含む Excel ファイルのパスを指定 します。 input_sheet_name (str): 設計変数の定義を含むシートの名前を指定します。 output_xlsm_path (str or Path, optional): 目的関数の定義を含む Excel ファイルのパスを指定 します。指定しない場合は ``input_xlsm_path`` と 同じと見做します。 output_sheet_name (str, optional): 目的関数の定義を含む含むシートの名前を指定します。 指定しない場合は ``input_sheet_name`` と同じと見 做します。 procedure_xlsm_path (str, optional): 最適化ループ中に呼ぶ Excel マクロ関数を 含む xlsm のパスです。 指定しない場合は ``input_xlsm_path`` と 同じと見做します。 procedure_name (str, optional): Excel マクロ関数名を指定します。指定しない場合は ``FemtetMacro.FemtetMain`` と見做します。 procedure_args (list or tuple, optional): Excel マクロ関数に渡す引数をリストまたはタプルで 指定します。 connect_method (str, optional): Excel との接続方法を指定します。 'auto' または 'new' が利用可能です。デフォルトは 'auto' です。 procedure_timeout (float or None, optional): Excel マクロ関数のタイムアウト時間を秒単位で指定 します。 None の場合はタイムアウトなしとなります。 setup_xlsm_path (str or Path, optional): セットアップ時に呼ぶ関数を含む xlsm のパスです。 指定しない場合は ``input_xlsm_path`` と 同じと見做します。 setup_procedure_name (str, optional): セットアップ時に呼ぶマクロ関数名です。 指定しない場合、セットアップ時に何もしません。 setup_procedure_args (list or tuple, optional): セットアップ時に呼ぶマクロ関数の引数です。 teardown_xlsm_path (str or Path, optional): 終了時に呼ぶ関数を含む xlsm のパスです。 指定しない場合は ``input_xlsm_path`` と 同じと見做します。 teardown_procedure_name (str, optional): 終了時に呼ぶマクロ関数名です。 指定しない場合、終了時に何もしません。 teardown_procedure_args (list or tuple, optional): 終了時に呼ぶマクロ関数の引数です。 visible (bool): excel を可視化するかどうかです。 ただし、 True を指定した場合でもマクロの実行中は 不可視になります。 デフォルトは False です。 display_alerts (bool): excel ダイアログを表示するかどうかです。 デバッグ目的の場合以外は True にしないでください。 デフォルトは False です。 terminate_excel_when_quit (bool): 終了時に Excel を終了するかどうかです。 指定しない場合、 connect_method が 'new' の場合 True とふるまい 'auto' の場合 False と振舞います。 interactive (bool): excel を対話モードにするかどうかです。 False にすると、 visible == True であっても 自動化プロセス中にユーザーが誤って Excel 本体を操作できないようにします。 デフォルトは True です。 Attributes: input_xlsm_path (Path): 設計変数の定義を含む Excel ファイルのパス。 input_sheet_name (str): 設計変数の定義を含むシートの名前。 output_xlsm_path (Path): 目的関数の定義を含む Excel ファイルのパス。 output_sheet_name (str): 目的関数の定義を含む含むシートの名前。 procedure_name (str): 実行する Excel マクロ関数名。 procedure_args (list or tuple): Excel マクロ関数に渡す引数のリストまたはタプル。 excel_connect_method (str): 接続方法。'new' または 'auto'。 procedure_timeout (float or None): Excel マクロ関数の実行タイムアウト。 Noneの場合は無制限。 terminate_excel_when_quit (bool): プログラム終了時に Excel を終了するかどうか。 connect_method が 'new' の場合 True, 'auto' の場合 False。 excel (CDispatch): Excel の COM オブジェクト。 """ input_xlsm_path: str # 操作対象の xlsm パス input_sheet_name: str # 変数セルを定義しているシート名 output_xlsm_path: str # 操作対象の xlsm パス (指定しない場合、input と同一) output_sheet_name: str # 計算結果セルを定義しているシート名 (指定しない場合、input と同一) constraint_xlsm_path: str # 操作対象の xlsm パス (指定しない場合、input と同一) constraint_sheet_name: str # 拘束関数セルを定義しているシート名 (指定しない場合、input と同一) related_file_paths: list[str] # 並列時に個別に並列プロセスの space にアップロードする必要のあるパス procedure_name: str # マクロ関数名(or モジュール名.関数名) procedure_args: list # マクロ関数の引数 excel: CDispatch # Excel Application wb_input: CDispatch # システムを構成する Workbook sh_input: CDispatch # 変数の定義された WorkSheet wb_output: CDispatch # システムを構成する Workbook sh_output: CDispatch # 計算結果の定義された WorkSheet (sh_input と同じでもよい) wb_constraint: CDispatch # システムを構成する Workbook sh_constraint: CDispatch # 計算結果の定義された WorkSheet (sh_input と同じでもよい) wb_procedure: CDispatch # システムを構成する Workbook wb_setup: CDispatch # システムを構成する Workbook wb_teardown: CDispatch # システムを構成する Workbook visible: bool # excel を可視化するかどうか display_alerts: bool # ダイアログを表示するかどうか terminate_excel_when_quit: bool # 終了時に Excel を終了するかどうか interactive: bool # excel を対話モードにするかどうか _excel_pid: int _excel_hwnd: int _with_femtet_autosave_setting: bool = True # Femtet の自動保存機能の自動設定を行うかどうか。Femtet がインストールされていない場合はオフにする。クラス変数なので、インスタンス化前に設定する。 _femtet_autosave_buffer: bool # Femtet の自動保存機能の一時退避場所。最適化中はオフにする。 com_members = {'excel': 'Excel.Application'} setup_xlsm_path: str setup_procedure_name: str setup_procedure_args: list or tuple teardown_xlsm_path: str teardown_procedure_name: str teardown_procedure_args: list or tuple use_named_range: bool # input を定義したシートにおいて input の値を名前付き範囲で指定するかどうか。 force_override_when_load: bool # すでに指定されている parameter 等を excel の load 時に上書きするかどうか。 _tmp_dir: tempfile.TemporaryDirectory def __init__( self, input_xlsm_path: str or Path, input_sheet_name: str, output_xlsm_path: str or Path = None, output_sheet_name: str = None, constraint_xlsm_path: str or Path = None, constraint_sheet_name: str = None, procedure_xlsm_path: str or Path = None, procedure_name: str = None, procedure_args: list or tuple = None, connect_method: str = 'new', # or 'auto' procedure_timeout: float or None = None, setup_xlsm_path: str or Path = None, setup_procedure_name: str = None, setup_procedure_args: list or tuple = None, teardown_xlsm_path: str or Path = None, teardown_procedure_name: str = None, teardown_procedure_args: list or tuple = None, related_file_paths: list[str or Path] = None, visible: bool = False, display_alerts: bool = False, terminate_excel_when_quit: bool = None, interactive: bool = True, use_named_range: bool = True, force_override_when_load: bool = False, ): def proc_path(path_): if path_ is None: return self._original_input_xlsm_path else: ret_ = os.path.abspath(path_) assert os.path.isfile(ret_), f'{ret_} が見つかりません。' return os.path.abspath(path_) # 初期化 self._original_input_xlsm_path = os.path.abspath(str(input_xlsm_path)) assert os.path.isfile(self._original_input_xlsm_path), f'{self._original_input_xlsm_path} が見つかりません。' self._original_output_xlsm_path = proc_path(output_xlsm_path) self._original_constraint_xlsm_path = proc_path(constraint_xlsm_path) self._original_procedure_xlsm_path = proc_path(procedure_xlsm_path) self._original_setup_xlsm_path = proc_path(setup_xlsm_path) self._original_teardown_xlsm_path = proc_path(teardown_xlsm_path) self._original_related_file_paths = \ [proc_path(p) for p in related_file_paths] \ if related_file_paths is not None else [] self.input_xlsm_path = self._original_input_xlsm_path self.output_xlsm_path = self._original_output_xlsm_path self.constraint_xlsm_path = self._original_constraint_xlsm_path self.procedure_xlsm_path = self._original_procedure_xlsm_path self.setup_xlsm_path = self._original_setup_xlsm_path self.teardown_xlsm_path = self._original_teardown_xlsm_path self.related_file_paths = self._original_related_file_paths self.input_sheet_name = input_sheet_name self.output_sheet_name = output_sheet_name if output_sheet_name is not None else input_sheet_name self.constraint_sheet_name = constraint_sheet_name or self.input_sheet_name self.procedure_name = procedure_name self.procedure_args = procedure_args or [] assert connect_method in ['new', 'auto'] self.excel_connect_method = connect_method self.procedure_timeout = procedure_timeout if terminate_excel_when_quit is None: self.terminate_excel_when_quit = self.excel_connect_method == 'new' else: self.terminate_excel_when_quit = terminate_excel_when_quit self.setup_procedure_name = setup_procedure_name self.setup_procedure_args = setup_procedure_args or [] self.teardown_procedure_name = teardown_procedure_name self.teardown_procedure_args = teardown_procedure_args or [] self.visible = visible self.interactive = interactive self.display_alerts = display_alerts self.use_named_range = use_named_range self.force_override_when_load = force_override_when_load @property def object_pass_to_fun(self): """The object pass to the first argument of user-defined objective functions. Returns: excel (CDispatch): COM object of Microsoft Excel. """ return self.excel # ===== setup ===== def _setup_before_parallel(self, scheduler_address=None) -> None: # メインプロセスで、並列プロセスを開始する前に行う前処理 related_files = [self._original_input_xlsm_path] if not _is_same_path(self._original_input_xlsm_path, self._original_output_xlsm_path): related_files.append(self._original_output_xlsm_path) if not _is_same_path(self._original_input_xlsm_path, self._original_constraint_xlsm_path): related_files.append(self._original_constraint_xlsm_path) if not _is_same_path(self._original_input_xlsm_path, self._original_procedure_xlsm_path): related_files.append(self._original_procedure_xlsm_path) if not _is_same_path(self._original_input_xlsm_path, self._original_setup_xlsm_path): related_files.append(self._original_setup_xlsm_path) if not _is_same_path(self._original_input_xlsm_path, self._original_teardown_xlsm_path): related_files.append(self._original_teardown_xlsm_path) related_files.extend(self._original_related_file_paths) # dask worker 向け self._distribute_files(related_files, scheduler_address) def _re_register_paths(self, suffix): # self.hoge_path を dask worker space のファイルに変える # paths を更新 # suffix の付与は connect_method が auto の場合 # 同名ファイルを開かないようにするための配慮 # output 等 == inputの場合、 # 最初に input を rename しているので # すでに rename されている self.input_xlsm_path = self._rename_and_get_path_on_worker_space( self._original_input_xlsm_path, suffix) self.output_xlsm_path = self._rename_and_get_path_on_worker_space( self._original_output_xlsm_path, suffix, True) self.constraint_xlsm_path = self._rename_and_get_path_on_worker_space( self._original_constraint_xlsm_path, suffix, True) self.procedure_xlsm_path = self._rename_and_get_path_on_worker_space( self._original_procedure_xlsm_path, suffix, True) self.setup_xlsm_path = self._rename_and_get_path_on_worker_space( self._original_setup_xlsm_path, suffix, True) self.teardown_xlsm_path = self._rename_and_get_path_on_worker_space( self._original_teardown_xlsm_path, suffix, True) def _setup_after_parallel(self, opt: AbstractOptimizer): """サブプロセス又はメインプロセスのサブスレッドで、最適化を開始する前の前処理""" # 最適化中は femtet の autosave を無効にする if self._with_femtet_autosave_setting: self._femtet_autosave_buffer = _get_autosave_enabled() _set_autosave_enabled(False) # Excel の場合はスレッドが変わっただけでも初期化が必要 CoInitialize() # 元のファイルを保護するため space のファイルを使用 suffix = self._get_file_suffix(opt) self._re_register_paths(suffix) # excel に繋ぐ with Lock('connect-excel'): self.connect_excel(self.excel_connect_method) sleep(1) # load_objective で optimizer に登録した Function は # 少なくともスレッドが異なるので # 現在のスレッドのオブジェクトに参照しなおす obj: Objective for obj_name, obj in opt.objectives.items(): if isinstance(obj.fun, _ScapeGoatObjective): opt.objectives[obj_name].fun = self.objective_from_excel cns: Constraint for cns_name, cns in opt.constraints.items(): if isinstance(cns.fun, _ScapeGoatObjective): opt.constraints[cns_name].fun = self.constraint_from_excel # excel の setup 関数を必要なら実行する if self.setup_procedure_name is not None: logger.info( _( en_message='{procedure_kind} procedure {procedure_name} is running...', jp_message='{procedure_kind} プロシージャ {procedure_name} を実行中です...', procedure_kind='Setup', procedure_name=self.setup_procedure_name ) ) with Lock('excel_setup_procedure'): try: self.wb_setup.Activate() sleep(0.1) with watch_excel_macro_error(self.excel, timeout=self.procedure_timeout, restore_book=False): self.excel.Run( f'{self.setup_procedure_name}', *self.setup_procedure_args ) # 再計算 self.excel.CalculateFull() sleep(1) except com_error as e: raise RuntimeError( _( en_message='Failed to run macro {procedure_name}. The original message is: {exception}', jp_message='マクロ {procedure_name} の実行に失敗しました。エラーメッセージ: {exception}', procedure_name=self.setup_procedure_name, exception=e ) )
[docs] def connect_excel(self, connect_method): # ===== 新しい excel instance を起動 ===== # 起動 logger.info(_( en_message='Launching and connecting to Microsoft Excel...', jp_message='Microsoft Excel を起動して接続しています...', )) if connect_method == 'auto': self.excel = Dispatch('Excel.Application') else: self.excel = DispatchEx('Excel.Application') logger.info(_( en_message='The connection to Excel is established.', jp_message='Excel への接続が確立されました。', )) # FemtetRef.xla を開く self.open_femtet_ref_xla() sleep(0.5) # 起動した excel の pid を記憶する self._excel_hwnd = self.excel.hWnd self._excel_pid = 0 while self._excel_pid == 0: sleep(0.5) self._excel_pid = _get_pid(self.excel.hWnd) # 可視性の設定 self.excel.Visible = self.visible self.excel.DisplayAlerts = self.display_alerts self.excel.Interactive = self.interactive sleep(0.5) # open self.excel.Workbooks.Open(self.input_xlsm_path) sleep(0.1) self.excel.Workbooks.Open(self.output_xlsm_path) sleep(0.1) self.excel.Workbooks.Open(self.constraint_xlsm_path) sleep(0.1) self.excel.Workbooks.Open(self.procedure_xlsm_path) sleep(0.1) self.excel.Workbooks.Open(self.setup_xlsm_path) sleep(0.1) self.excel.Workbooks.Open(self.teardown_xlsm_path) sleep(0.1) # book に参照設定を追加する self.add_femtet_macro_reference(self.wb_input) self.add_femtet_macro_reference(self.wb_output) self.add_femtet_macro_reference(self.wb_setup) self.add_femtet_macro_reference(self.wb_teardown) self.add_femtet_macro_reference(self.wb_constraint)
[docs] def open_femtet_ref_xla(self): # get 64 bit xla_file_path = r'C:\Program Files\Microsoft Office\root\Office16\XLSTART\FemtetRef.xla' # if not exist, get 32bit if not os.path.exists(xla_file_path): xla_file_path = r'C:\Program Files (x86)\Microsoft Office\root\Office16\XLSTART\FemtetRef.xla' # certify if not os.path.exists(xla_file_path): raise FileNotFoundError( _( en_message='Femtet XLA file ({xla_file_path}) not found. ' 'Please run `Enable Macros` command.', jp_message='Femtet XLA ファイル ({xla_file_path}) が見つかりません。' '「マクロ機能の有効化」を実行してください。', xla_file_path=xla_file_path ) ) # self.excel.Workbooks.Add(xla_file_path) self.excel.Workbooks.Open(xla_file_path, ReadOnly=True)
[docs] @staticmethod def add_femtet_macro_reference(wb): # search ref_file_2 = os.path.abspath(util._get_femtetmacro_dllpath()) contain_2 = False for ref in wb.VBProject.References: if ref.Description is not None: if ref.Description == 'FemtetMacro': # FemtetMacro contain_2 = True break # add if not contain_2: wb.VBProject.References.AddFromFile(ref_file_2)
[docs] @staticmethod def remove_femtet_ref_xla(wb): # search for ref in wb.VBProject.References: if ref.Description is not None: if ref.Description == 'FemtetMacro': # FemtetMacro wb.VBProject.References.Remove(ref)
# ===== load =====
[docs] def load_variables(self, opt: AbstractOptimizer, raise_if_no_keyword=True) -> None: df = ParseAsParameter.parse( self.input_xlsm_path, self.input_sheet_name, raise_if_no_keyword, ) for i, row in df.iterrows(): # use(optional) use = True if ParseAsParameter.use in df.columns: _use = row[ParseAsParameter.use] use = False if _is_cell_value_empty(_use) else bool(_use) # bool or NaN # name name = str(row[ParseAsParameter.name]) # if the variable is already added by # add_parameter or add_expression, # use it. if not self.force_override_when_load: if name in opt.variable_manager.get_variables(): continue # value value = float_(row[ParseAsParameter.value]) # if 'choices' column exists and is not empty, use as Categorical. kind = 'Numerical' if ParseAsParameter.choices in df.columns: if row[ParseAsParameter.choices] != _EMPTY_CHOICE: kind = 'Categorical' if kind == 'Numerical': # lb (optional) lb = None if ParseAsParameter.lb in df.columns: lb = row[ParseAsParameter.lb] lb = None if _is_cell_value_empty(lb) else float(lb) # ub (optional) ub = None if ParseAsParameter.ub in df.columns: ub = row[ParseAsParameter.ub] ub = None if _is_cell_value_empty(ub) else float(ub) # step (optional) step = None if ParseAsParameter.step in df.columns: step = row[ParseAsParameter.step] step = None if _is_cell_value_empty(step) else float(step) opt.add_parameter( name=name, initial_value=value, lower_bound=lb, upper_bound=ub, step=step, fix=not use, supress_duplicated_name_check=True, ) elif kind == 'Categorical': # choices choices = row[ParseAsParameter.choices] assert choices != _EMPTY_CHOICE opt.add_categorical_parameter( name=name, initial_value=value, choices=choices, fix=not use, supress_duplicated_name_check=True, ) else: raise NotImplementedError
[docs] def load_objectives(self, opt: AbstractOptimizer, raise_if_no_keyword=True): df = ParseAsObjective.parse( self.output_xlsm_path, self.output_sheet_name, raise_if_no_keyword, ) for i, row in df.iterrows(): # use(optional) use = True if ParseAsObjective.use in df.columns: _use = row[ParseAsObjective.use] use = False if _is_cell_value_empty(_use) else bool(_use) # bool or NaN # name name = str(row[ParseAsObjective.name]) # if the objective is already added by # add_objective, use it. if not self.force_override_when_load: if name in opt.objectives.keys(): continue # direction direction = row[ParseAsObjective.direction] assert not _is_cell_value_empty(direction), 'direction is empty.' try: direction = float(direction) except ValueError: direction = str(direction).lower() assert direction in ['minimize', 'maximize'] if use: # objective を作る opt.add_objective( name=name, direction=direction, fun=_ScapeGoatObjective(), kwargs=dict(name=name), supress_duplicated_name_check=True, )
[docs] def load_constraints(self, opt: AbstractOptimizer, raise_if_no_keyword=False): df = ParseAsConstraint.parse( self.constraint_xlsm_path, self.constraint_sheet_name, raise_if_no_keyword, ) for i, row in df.iterrows(): # use(optional) use = True if ParseAsConstraint.use in df.columns: _use = row[ParseAsConstraint.use] use = False if _is_cell_value_empty(_use) else bool(_use) # bool or NaN # name name = str(row[ParseAsConstraint.name]) # if the constraint is already added by # add_constraint, use it. if not self.force_override_when_load: if name in opt.constraints.keys(): continue # lb (optional) lb = None if ParseAsConstraint.lb in df.columns: lb = row[ParseAsConstraint.lb] lb = None if _is_cell_value_empty(lb) else float(lb) # ub (optional) ub = None if ParseAsConstraint.ub in df.columns: ub = row[ParseAsConstraint.ub] ub = None if _is_cell_value_empty(ub) else float(ub) # strict (optional) strict = True if ParseAsConstraint.strict in df.columns: _strict = row[ParseAsConstraint.strict] strict = True if _is_cell_value_empty(_strict) else bool(_strict) # bool or NaN # using_fem (optional) calc_before_solve = True if ParseAsConstraint.calc_before_solve in df.columns: _calc_before_solve = row[ParseAsConstraint.calc_before_solve] calc_before_solve = True if _is_cell_value_empty(_calc_before_solve) else bool( _calc_before_solve) # bool or NaN if use: opt.add_constraint( name=name, lower_bound=lb, upper_bound=ub, strict=strict, fun=_ScapeGoatObjective(), kwargs=dict(name=name), using_fem=not calc_before_solve, supress_duplicated_name_check=True, )
[docs] def objective_from_excel(self, _, name: str): r = 1 + search_r(self.output_xlsm_path, self.output_sheet_name, name) c = 1 + search_c(self.output_xlsm_path, self.output_sheet_name, ParseAsObjective.value) v = self.sh_output.Cells(r, c).value return float(v)
[docs] def constraint_from_excel(self, _, name: str): r = 1 + search_r(self.constraint_xlsm_path, self.constraint_sheet_name, name) c = 1 + search_c(self.constraint_xlsm_path, self.constraint_sheet_name, ParseAsConstraint.value) v = self.sh_constraint.Cells(r, c).value return float(v)
# ===== Workbook and WorkSheet ===== # unpicklable object を excel だけにするため # これらのオブジェクトは動的に取得 def _get_wb(self, path) -> CDispatch: for wb in self.excel.Workbooks: if wb.Name == os.path.basename(path): return wb else: raise WorkbookNotOpenedError(f'{path} is not opened.') @staticmethod def _get_sh(wb: CDispatch, name) -> CDispatch: for sh in wb.WorkSheets: if sh.Name == name: return sh else: raise WorkSheetNotFoundError( _( en_message='{xla_file_path} not found. Please check the ' '"Enable Macros" command was executed.', jp_message='{xla_file_path}が見つかりません。' '"マクロを有効にする" command が実行' 'されたか確認してください。', xla_file_path=wb.Name ) ) @property def wb_input(self) -> CDispatch: return self._get_wb(self.input_xlsm_path) @property def sh_input(self) -> CDispatch: return self._get_sh(self.wb_input, self.input_sheet_name) @property def wb_output(self) -> CDispatch: return self._get_wb(self.output_xlsm_path) @property def sh_output(self) -> CDispatch: return self._get_sh(self.wb_output, self.output_sheet_name) @property def wb_constraint(self) -> CDispatch: return self._get_wb(self.constraint_xlsm_path) @property def sh_constraint(self) -> CDispatch: return self._get_sh(self.wb_constraint, self.constraint_sheet_name) @property def wb_procedure(self) -> CDispatch: return self._get_wb(self.procedure_xlsm_path) @property def wb_setup(self) -> CDispatch: return self._get_wb(self.setup_xlsm_path) @property def wb_teardown(self) -> CDispatch: return self._get_wb(self.teardown_xlsm_path) # ===== update =====
[docs] def update_parameter(self, x: TrialInput) -> None: COMInterface.update_parameter(self, x) # excel シートの変数更新 if self.use_named_range: for key, variable in self.current_prm_values.items(): try: self.sh_input.Range(key).value = variable.value except com_error: logger.warning( _( en_message='The cell address specification by named range is failed. ' 'The process changes the specification method to table based.', jp_message='名前範囲によるセルアドレス指定に失敗しました。' '処理は指定方法をテーブルに変更します。', ) ) self.use_named_range = False break if not self.use_named_range: # else にしないこと for name, variable in self.current_prm_values.items(): r = 1 + search_r(self.input_xlsm_path, self.input_sheet_name, name) c = 1 + search_c(self.input_xlsm_path, self.input_sheet_name, ParseAsParameter.value) self.sh_input.Cells(r, c).value = variable.value # 再計算 self.excel.CalculateFull()
[docs] def update(self) -> None: if self.procedure_name is None: return # マクロ実行 try: self.wb_procedure.Activate() sleep(0.1) with watch_excel_macro_error(self.excel, timeout=self.procedure_timeout): self.excel.Run( f'{self.procedure_name}', *self.procedure_args ) # 再計算 self.excel.CalculateFull() except com_error as e: raise SolveError( # 変換後(あなたの出力) _( en_message='Failed to run macro {procedure_name}. ' 'The original message is: {exception}', jp_message='マクロ {procedure_name} の実行に失敗しました。 ' 'エラーメッセージ: {exception}', procedure_name=self.procedure_name, exception=e ) )
# ===== close ===== def _close_workbooks(self): # workbook を閉じる with watch_excel_macro_error(self.excel, timeout=10, restore_book=False): self.wb_input.Close(_SaveChanges := False) def close_wb_if_needed(attrib_name_): try: # self.wb_output などとすると # 関数呼び出しの時点でエラーになる wb = getattr(self, attrib_name_) except WorkbookNotOpenedError: pass else: with watch_excel_macro_error(self.excel, timeout=10, restore_book=False): wb.Close(False) close_wb_if_needed('wb_output') close_wb_if_needed('wb_constraint') close_wb_if_needed('wb_procedure') close_wb_if_needed('wb_setup') close_wb_if_needed('wb_teardown')
[docs] def close(self): # 無駄に不具合に遭う可能性があるので # 参照設定は解除しない if not hasattr(self, 'excel'): return if self.excel is None: return # 終了処理を必要なら実施する if self.teardown_procedure_name is not None: logger.info(_( en_message='{procedure_kind} procedure {procedure_name} is running...', jp_message='{procedure_kind} プロシージャ {procedure_name} が実行中...', procedure_kind='Teardown', procedure_name=self.teardown_procedure_name )) with Lock('excel_teardown_procedure'): try: self.wb_teardown.Activate() sleep(0.1) with watch_excel_macro_error(self.excel, timeout=self.procedure_timeout, restore_book=False): self.excel.Run( f'{self.teardown_procedure_name}', *self.teardown_procedure_args ) # 再計算 self.excel.CalculateFull() except com_error as e: raise RuntimeError( _( en_message='Failed to run macro {procedure_name}. ' 'The original message is: {exception}', jp_message='マクロ {procedure_name} の実行に失敗しました。 ' 'エラーメッセージ: {exception}', procedure_name=self.teardown_procedure_name, exception=e ) ) # excel プロセスを終了する if self.terminate_excel_when_quit: logger.info(_( en_message='Terminating Excel process...', jp_message='Excel プロセスを終了しています...', )) already_terminated = not hasattr(self, 'excel') if already_terminated: return # ワークブックを閉じる self._close_workbooks() # excel の終了 with watch_excel_macro_error(self.excel, timeout=10, restore_book=False): self.excel.Quit() del self.excel # ここで Excel のプロセスが残らず落ちる gc.collect() logger.info(_( en_message='Excel process is terminated.', jp_message='Excel プロセスは終了しました。', )) if self._with_femtet_autosave_setting: logger.info(_( en_message='Restore Femtet setting of autosave.', jp_message='Femtet の自動保存設定を復元しています。', )) _set_autosave_enabled(self._femtet_autosave_buffer) # そうでない場合でもブックは閉じる else: self._close_workbooks()
# main thread で作成した excel への参照を含む関数を # 直接 thread や process に渡すと機能しない class _ScapeGoatObjective: # for type hint def __call__(self) -> float: pass @property def __globals__(self): return dict() def _is_same_path(p1, p2): _p1 = os.path.abspath(p1).lower() _p2 = os.path.abspath(p2).lower() return _p1 == _p2 def _is_cell_value_empty(cell_value): if isinstance(cell_value, str): return cell_value == '' elif isinstance(cell_value, int) \ or isinstance(cell_value, float): return np.isnan(cell_value) elif cell_value is None: return True else: return False