pyfemtet 0.5.4__py3-none-any.whl → 0.6.0__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.5.4"
1
+ __version__ = "0.6.0"
@@ -80,7 +80,7 @@ msgstr "モデル更新に失敗しました。"
80
80
 
81
81
  #: pyfemtet/message/messages.py:60
82
82
  msgid "It was detected that the configuration of Femtet python macro constants has not been completed. The configuration was done automatically (python -m win32com.client.makepy FemtetMacro). Please restart the program."
83
- msgstr "Femtet マクロの定数の設定が未完了であることが検出され、設定コマンド(python -m win23com.client.makepy FemtetMacro)が自動実行されました。プログラムを再実行してください。"
83
+ msgstr "Femtet マクロの定数の設定が未完了であることが検出され、設定コマンド(python -m win32com.client.makepy FemtetMacro)が自動実行されました。プログラムを再実行してください。"
84
84
 
85
85
  #: pyfemtet/message/messages.py:61
86
86
  msgid "Failed to connect to Femtet."
pyfemtet/opt/__init__.py CHANGED
@@ -9,6 +9,8 @@ from pyfemtet.opt.optimizer import AbstractOptimizer
9
9
 
10
10
  from pyfemtet.opt._femopt import FEMOpt
11
11
 
12
+ from pyfemtet.opt._femopt_core import History
13
+
12
14
 
13
15
  __all__ = [
14
16
  'FEMOpt',
@@ -21,4 +23,5 @@ __all__ = [
21
23
  'OptunaOptimizer',
22
24
  'ScipyScalarOptimizer',
23
25
  'ScipyOptimizer',
26
+ 'History',
24
27
  ]
pyfemtet/opt/_femopt.py CHANGED
@@ -12,7 +12,7 @@ from traceback import print_exception
12
12
  # 3rd-party
13
13
  import numpy as np
14
14
  import pandas as pd
15
- from dask.distributed import LocalCluster, Client, Worker
15
+ from dask.distributed import LocalCluster, Client
16
16
 
17
17
  # pyfemtet relative
18
18
  from pyfemtet.opt.interface import FEMInterface, FemtetInterface
@@ -32,10 +32,13 @@ from pyfemtet._message import Msg, encoding
32
32
  from pyfemtet.opt.optimizer.parameter import Parameter, Expression
33
33
  from pyfemtet._warning import experimental_feature
34
34
 
35
+ from dask import config as cfg
36
+ cfg.set({'distributed.scheduler.worker-ttl': None})
35
37
 
36
- def add_worker(client, worker_name):
38
+
39
+ def add_worker(client, worker_name, n_workers=1):
37
40
  import sys
38
- from subprocess import Popen, DEVNULL
41
+ from subprocess import Popen, DEVNULL, PIPE
39
42
 
40
43
  current_n_workers = len(client.nthreads().keys())
41
44
 
@@ -43,16 +46,16 @@ def add_worker(client, worker_name):
43
46
  f'{sys.executable} -m dask worker '
44
47
  f'{client.scheduler.address} '
45
48
  f'--nthreads 1 '
46
- f'--nworkers 1 '
47
- f'--name {worker_name} '
48
- f'--no-nanny',
49
+ f'--nworkers {n_workers} '
50
+ f'--name {worker_name} ', # A unique name for this worker like ‘worker-1’. If used with -nworkers then the process number will be appended like name-0, name-1, name-2, …
51
+ # --no-nanny option は --nworkers と併用できない
49
52
  shell=True,
50
53
  stderr=DEVNULL,
51
54
  stdout=DEVNULL,
52
55
  )
53
56
 
54
57
  # worker が増えるまで待つ
55
- client.wait_for_workers(n_workers=current_n_workers + 1)
58
+ client.wait_for_workers(n_workers=current_n_workers + n_workers)
56
59
 
57
60
 
58
61
  class FEMOpt:
@@ -132,6 +135,7 @@ class FEMOpt:
132
135
  self.monitor_process_future = None
133
136
  self.monitor_server_kwargs = dict()
134
137
  self.monitor_process_worker_name = None
138
+ self._hv_reference = None
135
139
 
136
140
  # multiprocess 時に pickle できないオブジェクト参照の削除
137
141
  def __getstate__(self):
@@ -385,9 +389,9 @@ class FEMOpt:
385
389
 
386
390
  Warnings:
387
391
 
388
- When ```strict``` == True and using OptunaOptimizer along with BoTorchSampler,
389
- Pyfemtet will solve an optimization subproblem to propose new variables.
390
- During this process, the constraint function fun will be executed at each
392
+ When ```strict``` == True and using OptunaOptimizer along with :class:`PoFBoTorchSampler`,
393
+ PyFemtet will solve an optimization subproblem to propose new variables.
394
+ During this process, the constraint function ```fun``` will be executed at each
391
395
  iteration of the subproblem, which may include time-consuming operations such
392
396
  as retrieving 3D model information via FEMInterface.
393
397
  As a result, it may not be feasible to complete the overall optimization within
@@ -406,7 +410,7 @@ class FEMOpt:
406
410
 
407
411
  Instead, please do the following.
408
412
 
409
- >>> def bottom_area(_, opt): # 第一引数 Femtet にはアクセスしないようにします。 # doctest: +SKIP
413
+ >>> def bottom_area(_, opt): # Not to access the 1st argument. # doctest: +SKIP
410
414
  ... params = opt.get_parameter() # doctest: +SKIP
411
415
  ... w, d = params['width'], params['depth'] # doctest: +SKIP
412
416
  ... return w * d # doctest: +SKIP
@@ -623,14 +627,21 @@ class FEMOpt:
623
627
 
624
628
  else:
625
629
  # ローカルクラスターを構築
626
- logger.info('Launching single machine cluster. This may take tens of seconds.')
630
+ logger.info('Launching single machine cluster... This may take tens of seconds.')
631
+
632
+ # Fixed:
633
+ # Nanny の管理機能は必要ないが、Python API では worker_class を Worker にすると
634
+ # processes 引数が無視されて Thread worker が立てられる。
635
+ # これは CLI の --no-nanny オプションも同様らしい。
636
+
637
+ # クラスターの構築
627
638
  cluster = LocalCluster(
628
639
  processes=True,
629
- n_workers=n_parallel, # n_parallel = n_parallel - 1 + 1; main 分減らし、monitor 分増やす
640
+ n_workers=n_parallel,
630
641
  threads_per_worker=1,
631
- worker_class=Worker,
632
642
  )
633
643
  logger.info('LocalCluster launched successfully.')
644
+
634
645
  self.client = Client(cluster, direct_to_workers=False)
635
646
  self.scheduler_address = self.client.scheduler.address
636
647
  logger.info('Client launched successfully.')
@@ -655,6 +666,7 @@ class FEMOpt:
655
666
  model_name=self.fem.model_name
656
667
  )
657
668
  )
669
+
658
670
  # Femtet の parametric 設定を目的関数に用いるかどうか
659
671
  if self.fem.parametric_output_indexes_use_as_objective is not None:
660
672
  from pyfemtet.opt.interface._femtet_parametric import add_parametric_results_as_objectives
@@ -665,8 +677,14 @@ class FEMOpt:
665
677
  indexes,
666
678
  directions,
667
679
  )
680
+
681
+ # Femtet の confirm_before_exit のセット
682
+ self.fem.confirm_before_exit = confirm_before_exit
683
+ self.fem.kwargs['confirm_before_exit'] = confirm_before_exit
684
+
668
685
  logger.info('Femtet loaded successfully.')
669
686
 
687
+
670
688
  # actor の設定
671
689
  self.status = OptimizationStatus(_client)
672
690
  self.worker_status_list = [OptimizationStatus(_client, name) for name in worker_addresses] # tqdm 検討
@@ -678,6 +696,7 @@ class FEMOpt:
678
696
  list(self.opt.constraints.keys()),
679
697
  _client,
680
698
  metadata,
699
+ self._hv_reference
681
700
  )
682
701
  logger.info('Status Actor initialized successfully.')
683
702
 
@@ -453,18 +453,12 @@ class History:
453
453
  cns_names (List[str], optional): The names of constraints. Defaults to None.
454
454
  client (dask.distributed.Client): Dask client.
455
455
  additional_metadata (str, optional): metadata of optimization process.
456
-
457
- Raises:
458
- FileNotFoundError: If the csv file is not found.
459
-
460
- Attributes:
461
- HEADER_ROW (int): Header row number of csv file. Must be grater than 0. Default to 2.
462
- ENCODING (str): Encoding of csv file. Default to 'cp932'.
463
- prm_names (str): User defined names of parameters.
464
- obj_names (str): User defined names of objectives.
465
- cns_names (str): User defined names of constraints.
466
- is_restart (bool): If the optimization process is a continuation of another process or not.
467
- is_processing (bool): The optimization is running or not.
456
+ hv_reference (str or list[float or np.ndarray, optional):
457
+ The method to calculate hypervolume or
458
+ the reference point itself.
459
+ Valid values are 'dynamic-pareto' or
460
+ 'dynamic-nadir' or 'nadir' or 'pareto'
461
+ or fixed point (in objective function space).
468
462
 
469
463
  """
470
464
 
@@ -487,7 +481,10 @@ class History:
487
481
  cns_names=None,
488
482
  client=None,
489
483
  additional_metadata=None,
484
+ hv_reference=None,
490
485
  ):
486
+ # hypervolume 計算メソッド
487
+ self._hv_reference = hv_reference or 'dynamic-pareto'
491
488
 
492
489
  # 引数の処理
493
490
  self.path = history_path # .csv
@@ -739,59 +736,98 @@ class History:
739
736
 
740
737
  def _calc_hypervolume(self, objectives, df):
741
738
 
742
- # filter non-dominated and feasible solutions
743
- idx = df['non_domi'].values
744
- idx = idx * df['feasible'].values # *= を使うと non_domi 列の値が変わる
745
- pdf = df[idx]
746
- pareto_set = pdf[self.obj_names].values
747
- n = len(pareto_set) # 集合の要素数
748
- m = len(pareto_set.T) # 目的変数数
749
- # 多目的でないと計算できない
750
- if m <= 1:
751
- return None
752
- # 長さが 2 以上でないと計算できない
753
- if n <= 1:
754
- return None
755
- # 最小化問題に convert
756
- for i, (_, objective) in enumerate(objectives.items()):
757
- for j in range(n):
758
- pareto_set[j, i] = objective.convert(pareto_set[j, i])
759
- #### reference point の計算[1]
760
- # 逆正規化のための範囲計算
761
- maximum = pareto_set.max(axis=0)
762
- minimum = pareto_set.min(axis=0)
763
-
764
- r = 1.01
765
-
766
- # r を逆正規化
767
- reference_point = r * (maximum - minimum) + minimum
768
-
769
- #### hv 履歴の計算
770
- hvs = []
771
- for i in range(n):
772
- hv = compute_hypervolume(pareto_set[:i], reference_point)
773
- if np.isnan(hv):
774
- hv = 0
775
- hvs.append(hv)
776
-
777
- # 計算結果を履歴の一部に割り当て
778
- # idx: [False, True, False, True, True, False, ...]
779
- # hvs: [ 1, 2, 3, ...]
780
- # want:[ 0, 1, 1, 2, 3, 3, ...]
781
- hvs_index = -1
782
- for i in range(len(df)):
783
-
784
- # process hvs index
785
- if idx[i]:
786
- hvs_index += 1
787
-
788
- # get hv
789
- if hvs_index < 0:
790
- hypervolume = 0.
739
+ if len(objectives) < 2:
740
+ df.loc[len(df) - 1, 'hypervolume'] = 0.
741
+ return
742
+
743
+ # 最小化問題に変換された objective values を取得
744
+ raw_objective_values = df[self.obj_names].values
745
+ objective_values = np.empty_like(raw_objective_values)
746
+ for n_trial in range(len(raw_objective_values)):
747
+ for obj_idx, (_, objective) in enumerate(objectives.items()):
748
+ objective_values[n_trial, obj_idx] = objective.convert(raw_objective_values[n_trial, obj_idx])
749
+
750
+ # pareto front を取得
751
+ def get_pareto(objective_values_, with_partial=False):
752
+ ret = None
753
+ if with_partial:
754
+ ret = []
755
+
756
+ pareto_set_ = np.empty((0, len(self.obj_names)))
757
+ for i in range(len(objective_values_)):
758
+ target = objective_values_[i]
759
+ dominated = False
760
+ # TODO: Array の計算に直して高速化する
761
+ for j in range(len(objective_values_)):
762
+ compare = objective_values_[j]
763
+ if all(target > compare):
764
+ dominated = True
765
+ break
766
+ if not dominated:
767
+ pareto_set_ = np.concatenate([pareto_set_, [target]], axis=0)
768
+
769
+ if ret is not None:
770
+ ret.append(np.array(pareto_set_))
771
+
772
+ if ret is not None:
773
+ return pareto_set_, ret
791
774
  else:
792
- hypervolume = hvs[hvs_index]
775
+ return pareto_set_
776
+
777
+ if self._hv_reference == 'dynamic-pareto':
778
+ pareto_set, pareto_set_list = get_pareto(objective_values, with_partial=True)
779
+ for i, partial_pareto_set in enumerate(pareto_set_list):
780
+ ref_point = pareto_set.max(axis=0) + 1e-8
781
+ hv = compute_hypervolume(partial_pareto_set, ref_point)
782
+ df.loc[i, 'hypervolume'] = hv
783
+ return
784
+
785
+ elif self._hv_reference == 'dynamic-nadir':
786
+ _, pareto_set_list = get_pareto(objective_values, with_partial=True)
787
+ for i, partial_pareto_set in enumerate(pareto_set_list):
788
+ ref_point = objective_values.max(axis=0) + 1e-8
789
+ hv = compute_hypervolume(partial_pareto_set, ref_point)
790
+ df.loc[i, 'hypervolume'] = hv
791
+ return
792
+
793
+ elif self._hv_reference == 'nadir':
794
+ pareto_set = get_pareto(objective_values)
795
+ ref_point = objective_values.max(axis=0) + 1e-8
796
+ hv = compute_hypervolume(pareto_set, ref_point)
797
+ df.loc[len(df) - 1, 'hypervolume'] = hv
798
+ return
799
+
800
+ elif self._hv_reference == 'pareto':
801
+ pareto_set = get_pareto(objective_values)
802
+ ref_point = pareto_set.max(axis=0) + 1e-8
803
+ hv = compute_hypervolume(pareto_set, ref_point)
804
+ df.loc[len(df) - 1, 'hypervolume'] = hv
805
+ return
806
+
807
+ elif (
808
+ isinstance(self._hv_reference, np.ndarray)
809
+ or isinstance(self._hv_reference, list)
810
+ ):
811
+ _buff = np.array(self._hv_reference)
812
+ assert _buff.shape == (len(self.obj_names),)
813
+
814
+ ref_point = np.array(
815
+ [obj.convert(raw_value) for obj, raw_value in zip(objectives.values(), _buff)]
816
+ )
793
817
 
794
- df.loc[i, 'hypervolume'] = hypervolume
818
+ _buff = get_pareto(objective_values)
819
+
820
+ pareto_set = np.empty((0, len(objectives)))
821
+ for pareto_sol in _buff:
822
+ if all(pareto_sol < ref_point):
823
+ pareto_set = np.concatenate([pareto_set, [pareto_sol]], axis=0)
824
+
825
+ hv = compute_hypervolume(pareto_set, ref_point)
826
+ df.loc[len(df) - 1, 'hypervolume'] = hv
827
+ return
828
+
829
+ else:
830
+ raise NotImplementedError(f'Invalid Hypervolume reference point calculation method: {self._hv_reference}')
795
831
 
796
832
  def save(self, _f=None):
797
833
  """Save csv file."""
@@ -78,6 +78,10 @@ class FemtetInterface(FEMInterface):
78
78
  it will be None and no parametric outputs are used
79
79
  as objectives.
80
80
 
81
+ confirm_before_exit (bool):
82
+ Whether to confirm before (abnormal) termination.
83
+ Default is True.
84
+
81
85
  **kwargs: Additional arguments from inherited classes.
82
86
 
83
87
  Warning:
@@ -103,6 +107,7 @@ class FemtetInterface(FEMInterface):
103
107
  allow_without_project: bool = False, # main でのみ True を許容したいので super() の引数にしない。
104
108
  open_result_with_gui: bool = True,
105
109
  parametric_output_indexes_use_as_objective: dict[int, str or float] = None,
110
+ confirm_before_exit: bool = True,
106
111
  **kwargs # 継承されたクラスからの引数
107
112
  ):
108
113
 
@@ -132,6 +137,7 @@ class FemtetInterface(FEMInterface):
132
137
  self.parametric_output_indexes_use_as_objective = parametric_output_indexes_use_as_objective
133
138
  self._original_autosave_enabled = _get_autosave_enabled()
134
139
  _set_autosave_enabled(False)
140
+ self.confirm_before_exit = confirm_before_exit
135
141
 
136
142
  # dask サブプロセスのときは femprj を更新し connect_method を new にする
137
143
  try:
@@ -169,6 +175,7 @@ class FemtetInterface(FEMInterface):
169
175
  open_result_with_gui=self.open_result_with_gui,
170
176
  parametric_output_indexes_use_as_objective=self.parametric_output_indexes_use_as_objective,
171
177
  save_pdt=self.save_pdt,
178
+ confirm_before_exit=self.confirm_before_exit,
172
179
  **kwargs
173
180
  )
174
181
 
@@ -241,7 +248,8 @@ class FemtetInterface(FEMInterface):
241
248
  print('================')
242
249
  print(message)
243
250
  print('================')
244
- input('Press enter to finish...')
251
+ if self.confirm_before_exit:
252
+ input('Press enter to finish...')
245
253
  raise RuntimeError(message)
246
254
 
247
255
  if self.Femtet is None:
@@ -532,7 +540,8 @@ class FemtetInterface(FEMInterface):
532
540
  logger.error(Msg.ERR_CANNOT_ACCESS_API + 'GetVariableNames_py')
533
541
  logger.error(Msg.CERTIFY_MACRO_VERSION)
534
542
  print('================')
535
- input(Msg.ENTER_TO_QUIT)
543
+ if self.confirm_before_exit:
544
+ input(Msg.ENTER_TO_QUIT)
536
545
  raise e
537
546
 
538
547
  if variable_names is not None:
@@ -543,7 +552,8 @@ class FemtetInterface(FEMInterface):
543
552
  logger.error(message)
544
553
  logger.error(f'`{param_name}` not in {variable_names}')
545
554
  print('================')
546
- input(Msg.ENTER_TO_QUIT)
555
+ if self.confirm_before_exit:
556
+ input(Msg.ENTER_TO_QUIT)
547
557
  raise RuntimeError(message)
548
558
  else:
549
559
  return None
@@ -736,7 +746,8 @@ class FemtetInterface(FEMInterface):
736
746
  logger.error(Msg.ERR_CANNOT_ACCESS_API + 'Femtet.Exit()')
737
747
  logger.error(Msg.CERTIFY_MACRO_VERSION)
738
748
  print('================')
739
- input(Msg.ENTER_TO_QUIT)
749
+ if self.confirm_before_exit:
750
+ input(Msg.ENTER_TO_QUIT)
740
751
  raise e
741
752
 
742
753
  else:
@@ -1,12 +1,16 @@
1
1
  from pyfemtet.opt.optimizer._base import AbstractOptimizer, logger, OptimizationMethodChecker
2
- from pyfemtet.opt.optimizer._optuna import OptunaOptimizer
2
+ from pyfemtet.opt.optimizer._optuna._optuna import OptunaOptimizer
3
3
  from pyfemtet.opt.optimizer._scipy import ScipyOptimizer
4
4
  from pyfemtet.opt.optimizer._scipy_scalar import ScipyScalarOptimizer
5
5
 
6
+ from pyfemtet.opt.optimizer._optuna._pof_botorch import PoFBoTorchSampler, PoFConfig
7
+
6
8
  __all__ = [
7
9
  'ScipyScalarOptimizer',
8
10
  'ScipyOptimizer',
9
11
  'OptunaOptimizer',
10
12
  'AbstractOptimizer',
11
13
  'logger',
14
+ 'PoFBoTorchSampler',
15
+ 'PoFConfig',
12
16
  ]
@@ -142,6 +142,7 @@ class AbstractOptimizer(ABC):
142
142
  self.subprocess_idx = None
143
143
  self._exception = None
144
144
  self.method_checker: OptimizationMethodChecker = OptimizationMethodChecker(self)
145
+ self._retry_counter = 0
145
146
 
146
147
  # ===== algorithm specific methods =====
147
148
  @abstractmethod
@@ -176,13 +177,11 @@ class AbstractOptimizer(ABC):
176
177
 
177
178
  # Optimizer の x の更新
178
179
  self.set_parameter_values(x)
179
-
180
- logger.info('---------------------')
181
180
  logger.info(f'input: {x}')
182
181
 
183
182
  # FEM の更新
184
- logger.debug('fem.update() start')
185
183
  try:
184
+ logger.info(f'Solving FEM...')
186
185
  df_to_fem = self.variables.get_variables(
187
186
  format='df',
188
187
  filter_pass_to_fem=True
@@ -193,25 +192,20 @@ class AbstractOptimizer(ABC):
193
192
  logger.info(f'{type(e).__name__} : {e}')
194
193
  logger.info(Msg.INFO_EXCEPTION_DURING_FEM_ANALYSIS)
195
194
  logger.info(x)
196
- raise e # may be just a ModelError, etc.
195
+ raise e # may be just a ModelError, etc. Handling them in Concrete classes.
197
196
 
198
197
  # y, _y, c の更新
199
- logger.debug('calculate y start')
200
198
  y = [obj.calc(self.fem) for obj in self.objectives.values()]
201
199
 
202
- logger.debug('calculate _y start')
203
200
  _y = [obj.convert(value) for obj, value in zip(self.objectives.values(), y)]
204
201
 
205
- logger.debug('calculate c start')
206
202
  c = [cns.calc(self.fem) for cns in self.constraints.values()]
207
203
 
208
- logger.debug('history.record start')
209
-
204
+ # register to history
210
205
  df_to_opt = self.variables.get_variables(
211
206
  format='df',
212
207
  filter_parameter=True,
213
208
  )
214
-
215
209
  self.history.record(
216
210
  df_to_opt,
217
211
  self.objectives,
@@ -223,9 +217,7 @@ class AbstractOptimizer(ABC):
223
217
  postprocess_args=self.fem._create_postprocess_args(),
224
218
  )
225
219
 
226
- logger.debug('history.record end')
227
-
228
- logger.info(f'output: {_y}')
220
+ logger.info(f'output: {y}')
229
221
 
230
222
  return np.array(y), np.array(_y), np.array(c)
231
223
 
File without changes