PyFemtet 0.3.3__tar.gz → 0.3.5__tar.gz

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.
Files changed (25) hide show
  1. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/PKG-INFO +1 -1
  2. pyfemtet-0.3.5/pyfemtet/__init__.py +1 -0
  3. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/dispatch_extensions.py +29 -9
  4. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/logger.py +4 -2
  5. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/opt/base.py +195 -139
  6. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/opt/interface.py +9 -4
  7. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyproject.toml +1 -1
  8. pyfemtet-0.3.3/pyfemtet/__init__.py +0 -1
  9. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/LICENSE +0 -0
  10. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/README.md +0 -0
  11. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.femprj +0 -0
  12. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.prt +0 -0
  13. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.py +0 -0
  14. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/FemtetPJTSample/gau_ex08_parametric.femprj +0 -0
  15. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/FemtetPJTSample/gau_ex08_parametric.py +0 -0
  16. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/FemtetPJTSample/her_ex40_parametric.femprj +0 -0
  17. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/FemtetPJTSample/her_ex40_parametric.py +0 -0
  18. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/FemtetPJTSample/wat_ex14_parallel_parametric.py +0 -0
  19. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/FemtetPJTSample/wat_ex14_parametric.femprj +0 -0
  20. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/FemtetPJTSample/wat_ex14_parametric.py +0 -0
  21. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/_old/_SW_Femtet.py +0 -0
  22. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/core.py +0 -0
  23. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/opt/_FemtetWithNX/update_model.py +0 -0
  24. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/opt/__init__.py +0 -0
  25. {pyfemtet-0.3.3 → pyfemtet-0.3.5}/pyfemtet/opt/monitor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyfemtet
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: Design parameter optimization using Femtet.
5
5
  Home-page: https://github.com/pyfemtet/pyfemtet
6
6
  License: BSD-3-Clause
@@ -0,0 +1 @@
1
+ __version__ = "0.3.5"
@@ -4,6 +4,7 @@ import os
4
4
  from time import time, sleep
5
5
  from multiprocessing import current_process
6
6
 
7
+ from tqdm import tqdm
7
8
  import psutil
8
9
  from dask.distributed import Lock
9
10
 
@@ -15,6 +16,7 @@ from femtetutils import util
15
16
 
16
17
  from multiprocessing.context import BaseContext, SpawnProcess, _concrete_contexts
17
18
  from multiprocessing.process import _children, _cleanup
19
+ from multiprocessing.managers import SyncManager
18
20
 
19
21
  import logging
20
22
  from .logger import get_logger
@@ -138,11 +140,16 @@ def _get_pids(process_name):
138
140
  return pids
139
141
 
140
142
 
141
- def launch_and_dispatch_femtet(timeout=DISPATCH_TIMEOUT) -> Tuple[IFemtet, int]:
143
+ def _get_subprocess_log_prefix():
144
+ return f'({current_process().name}) '
145
+
146
+
147
+ def launch_and_dispatch_femtet(timeout=DISPATCH_TIMEOUT, strict_pid_specify=True) -> Tuple[IFemtet, int]:
142
148
  """Launch Femtet by new process and connect to it.
143
149
 
144
150
  Args:
145
151
  timeout (int or float, optional): Seconds to wait for connection. Defaults to DISPATCH_TIMEOUT.
152
+ strict_pid_specify (bool): If True, try to connect the launched femtet process strictly. If False, launch new process but try to connect any connectable femtet.
146
153
 
147
154
  Raises:
148
155
  FemtetNotFoundException: Launched Femtet is not found for some reason (i.e. failed to launch Femtet).
@@ -151,11 +158,26 @@ def launch_and_dispatch_femtet(timeout=DISPATCH_TIMEOUT) -> Tuple[IFemtet, int]:
151
158
  Returns:
152
159
  Tuple[IFemtet, int]:
153
160
  """
161
+ # launch femtet
154
162
  util.execute_femtet()
155
163
  pid = util.get_last_executed_femtet_process_id()
156
164
  logger.debug(f'Target pid is {pid}.')
157
- sleep(5)
158
- Femtet, pid = dispatch_specific_femtet(pid, timeout)
165
+ for _ in tqdm(range(5), 'wait for launch femtet...'):
166
+ sleep(1)
167
+
168
+ # dispatch femtet
169
+ if strict_pid_specify:
170
+ Femtet, pid = dispatch_specific_femtet(pid, timeout)
171
+ else:
172
+ # worker process なら排他処理する
173
+ try:
174
+ with Lock('simply-dispatch-femtet'):
175
+ Femtet, pid = dispatch_femtet()
176
+ except RuntimeError as e:
177
+ if "distributed.lock.Lock" in str(e):
178
+ Femtet, pid = dispatch_femtet()
179
+ else:
180
+ raise e
159
181
  return Femtet, pid
160
182
 
161
183
 
@@ -207,9 +229,6 @@ def dispatch_femtet(timeout=DISPATCH_TIMEOUT, subprocess_log_prefix='') -> Tuple
207
229
  return Femtet, pid
208
230
 
209
231
 
210
- def _get_subprocess_log_prefix():
211
- return f'({current_process().name}) '
212
-
213
232
 
214
233
  def _block_other_femtets(
215
234
  target_pid,
@@ -386,10 +405,11 @@ def dispatch_specific_femtet_core(pid, timeout=DISPATCH_TIMEOUT) -> Tuple[IFemte
386
405
  if not (pid in pids):
387
406
  raise FemtetNotFoundException(f"Femtet (pid = {pid}) doesn't exist.")
388
407
 
408
+ logger.info('Searching specific Femtet...')
409
+
389
410
  # 子プロセスの準備
390
411
  # with Manager() as manager:
391
- from multiprocessing.managers import SyncManager
392
- m = SyncManager(ctx=_NestableSpawnContext())
412
+ m = SyncManager(ctx=_NestableSpawnContext()) # このへんが時間のかかる処理
393
413
  m.start()
394
414
  with m as manager:
395
415
  # フラグの準備
@@ -402,7 +422,7 @@ def dispatch_specific_femtet_core(pid, timeout=DISPATCH_TIMEOUT) -> Tuple[IFemte
402
422
  # 目的以外の Femtet をブロックする子プロセスを開始
403
423
  logger.debug('Start subprocess to block Femtet other than target pid.')
404
424
  processes = []
405
- for subprocess_id in range(len(pids)):
425
+ for subprocess_id in tqdm(range(len(pids)), 'Specifying connection...'):
406
426
  p = _NestableSpawnProcess(
407
427
  # p = Process(
408
428
  target=_block_other_femtets,
@@ -12,8 +12,10 @@ def _get_worker_name_as_prefix():
12
12
  name = '(Main) '
13
13
  try:
14
14
  worker = get_worker()
15
- # address = worker.address
16
- name = f'(Worker:{worker.name}) '
15
+ if isinstance(worker.name, str): # local なら index, cluster なら tcp address
16
+ name = f'({worker.name}) '
17
+ else:
18
+ name = f'(Sub{worker.name}) '
17
19
  except ValueError:
18
20
  pass
19
21
  return name
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Callable, List, Tuple, Iterable, Any
2
+ from typing import List, Iterable
3
3
 
4
4
  import os
5
5
  import datetime
@@ -65,13 +65,13 @@ def generate_lhs(bounds: List[List[float]], seed: int or None = None) -> np.ndar
65
65
  LIMIT = 100
66
66
 
67
67
  def is_prime(p):
68
- for i in range(2, p):
69
- if p % i == 0:
68
+ for j in range(2, p):
69
+ if p % j == 0:
70
70
  return False
71
71
  return True
72
72
 
73
- def get_prime(minimum):
74
- for p in range(minimum, LIMIT):
73
+ def get_prime(_minimum):
74
+ for p in range(_minimum, LIMIT):
75
75
  if is_prime(p):
76
76
  return p
77
77
 
@@ -252,7 +252,7 @@ class Objective(Function):
252
252
  super().__init__(fun, name, args, kwargs)
253
253
 
254
254
  def convert(self, value: float):
255
- """Converts an evaluation value to the value of user-defined objective function based on the specified direction.
255
+ """Converts an evaluation value to the value of objective function based on the specified direction.
256
256
 
257
257
  When direction is `'minimize'`, ``value`` is calculated.
258
258
  When direction is `'maximize'`, ``-value`` is calculated.
@@ -329,9 +329,8 @@ class History:
329
329
  """Class for managing the history of optimization results.
330
330
 
331
331
  Attributes:
332
- path (str): The path to the history file.
333
- _actor_data (HistoryDfCore): The distributed DataFrame object for storing actor data.
334
- data (pd.DataFrame): The DataFrame object for storing the entire history.
332
+ path (str): The path to the history csv file.
333
+ is_restart (bool): The main session is restarted or not.
335
334
  param_names (list): The names of the parameters in the study.
336
335
  obj_names (list): The names of the objectives in the study.
337
336
  cns_names (list): The names of the constraints in the study.
@@ -350,13 +349,15 @@ class History:
350
349
  self.is_restart = False
351
350
  self._future = client.submit(HistoryDfCore, actor=True)
352
351
  self._actor_data = self._future.result()
352
+ self.tmp_data = pd.DataFrame()
353
353
  self.param_names = []
354
354
  self.obj_names = []
355
355
  self.cns_names = []
356
356
 
357
357
  # path が存在すれば dataframe を読み込む
358
358
  if os.path.isfile(self.path):
359
- self.actor_data = pd.read_csv(self.path, encoding='shift-jis')
359
+ self.tmp_data = pd.read_csv(self.path, encoding='shift-jis')
360
+ self.actor_data = self.tmp_data
360
361
  self.is_restart = True
361
362
 
362
363
  @property
@@ -406,6 +407,8 @@ class History:
406
407
  raise Exception(f'読み込んだ history と問題の設定が異なります. \n\n読み込まれた設定:\n{list(self.actor_data.columns)}\n\n現在の設定:\n{columns}')
407
408
 
408
409
  else:
410
+ for column in columns:
411
+ self.tmp_data[column] = None
409
412
  # actor_data は actor 経由の getter property なので self.data[column] = ... とやっても
410
413
  # actor には変更が反映されない. 以下同様
411
414
  tmp = self.actor_data
@@ -445,27 +448,25 @@ class History:
445
448
  with Lock('calc-history'):
446
449
  # append
447
450
  if len(self.actor_data) == 0:
448
- self.actor_data = pd.DataFrame([row], columns=self.actor_data.columns)
451
+ self.tmp_data = pd.DataFrame([row], columns=self.actor_data.columns)
449
452
  else:
450
- tmp = self.actor_data
451
- tmp.loc[len(tmp)] = row
452
- self.actor_data = tmp
453
+ self.tmp_data = self.actor_data
454
+ self.tmp_data.loc[len(self.tmp_data)] = row
453
455
 
454
456
  # calc
455
- tmp = self.actor_data
456
- tmp['trial'] = np.arange(len(tmp)) + 1 # 1 始まり
457
- self.actor_data = tmp
458
- self._calc_non_domi(objectives)
459
- self._calc_hypervolume(objectives)
457
+ self.tmp_data['trial'] = np.arange(len(self.tmp_data)) + 1 # 1 始まり
458
+ self._calc_non_domi(objectives) # update self.tmp_data
459
+ self._calc_hypervolume(objectives) # update self.tmp_data
460
+ self.actor_data = self.tmp_data
460
461
 
461
462
  def _calc_non_domi(self, objectives):
462
463
 
463
464
  # 目的関数の履歴を取り出してくる
464
- solution_set = self.actor_data[self.obj_names].copy()
465
+ solution_set = self.tmp_data[self.obj_names]
465
466
 
466
467
  # 最小化問題の座標空間に変換する
467
468
  for name, objective in objectives.items():
468
- solution_set[name] = solution_set[name].map(objective.convert)
469
+ solution_set.loc[:, name] = solution_set[name].map(objective.convert)
469
470
 
470
471
  # 非劣解の計算
471
472
  non_domi = []
@@ -473,15 +474,11 @@ class History:
473
474
  non_domi.append((row > solution_set).product(axis=1).sum(axis=0) == 0)
474
475
 
475
476
  # 非劣解の登録
476
- tmp = self.actor_data
477
- tmp['non_domi'] = non_domi
478
- self.actor_data = tmp
479
-
480
- del solution_set
477
+ self.tmp_data['non_domi'] = non_domi
481
478
 
482
479
  def _calc_hypervolume(self, objectives):
483
- # プロセス間の競合を避けるためローカルにコピーする
484
- df = self.actor_data.copy()
480
+ # タイピングが面倒
481
+ df = self.tmp_data
485
482
 
486
483
  # パレート集合の抽出
487
484
  idx = df['non_domi'].values
@@ -540,15 +537,12 @@ class History:
540
537
  # dominated の行に対して、上に見ていって
541
538
  # 最初に見つけた non-domi 行の hypervolume の値を割り当てます
542
539
  for i in range(len(df)):
543
- if df.loc[i, 'non_domi'] == False:
540
+ if not df.loc[i, 'non_domi']:
544
541
  try:
545
542
  df.loc[i, 'hypervolume'] = df.loc[:i][df.loc[:i]['non_domi']].iloc[-1]['hypervolume']
546
543
  except IndexError:
547
544
  df.loc[i, 'hypervolume'] = 0
548
545
 
549
- # ローカルの df を worker に戻す
550
- self.actor_data = df
551
-
552
546
 
553
547
  class OptimizationStatusActor:
554
548
  status_int = -1
@@ -568,35 +562,41 @@ class OptimizationStatus:
568
562
  UNDEFINED = -1
569
563
  INITIALIZING = 0
570
564
  SETTING_UP = 10
571
- LAUNCHING = 20
572
- WAIT_1ST = 25
565
+ LAUNCHING_FEM = 20
566
+ WAIT_OTHER_WORKERS = 22
567
+ # WAIT_1ST = 25
573
568
  RUNNING = 30
574
569
  INTERRUPTING = 40
575
570
  TERMINATED = 50
576
571
  TERMINATE_ALL = 60
577
572
 
578
- def __init__(self, client):
573
+ def __init__(self, client, name='entire'):
579
574
  self._future = client.submit(OptimizationStatusActor, actor=True)
580
575
  self._actor = self._future.result()
576
+ self.name = name
581
577
  self.set(self.INITIALIZING)
582
578
 
583
579
  @classmethod
584
- def const_to_str(self, status_const):
585
- if status_const == self.UNDEFINED: return 'Undefined'
586
- if status_const == self.INITIALIZING: return 'Initializing'
587
- if status_const == self.SETTING_UP: return 'Setting up'
588
- if status_const == self.LAUNCHING: return 'Launching FEM processes'
589
- if status_const == self.WAIT_1ST: return 'Running and waiting for 1st FEM result.'
590
- if status_const == self.RUNNING: return 'Running'
591
- if status_const == self.INTERRUPTING: return 'Interrupting'
592
- if status_const == self.TERMINATED: return 'Terminated'
593
- if status_const == self.TERMINATE_ALL: return 'Terminate_all'
580
+ def const_to_str(cls, status_const):
581
+ if status_const == cls.UNDEFINED: return 'Undefined'
582
+ if status_const == cls.INITIALIZING: return 'Initializing'
583
+ if status_const == cls.SETTING_UP: return 'Setting up'
584
+ if status_const == cls.LAUNCHING_FEM: return 'Launching FEM processes'
585
+ if status_const == cls.WAIT_OTHER_WORKERS: return 'Waiting for launching other processes'
586
+ # if status_const == cls.WAIT_1ST: return 'Running and waiting for 1st FEM result.'
587
+ if status_const == cls.RUNNING: return 'Running'
588
+ if status_const == cls.INTERRUPTING: return 'Interrupting'
589
+ if status_const == cls.TERMINATED: return 'Terminated'
590
+ if status_const == cls.TERMINATE_ALL: return 'Terminate_all'
594
591
 
595
592
  def set(self, status_const):
596
- # self._actor.status_int = status_const
597
- # self._actor.status = self.const_to_str(status_const)
598
593
  self._actor.set(status_const, self.const_to_str(status_const)).result()
599
- logger.info(f'---{self.const_to_str(status_const)}---')
594
+ msg = f'---{self.const_to_str(status_const)}---'
595
+ if (status_const == self.INITIALIZING) and (self.name != 'entire'):
596
+ msg += f' (for Worker {self.name})'
597
+ if self.name == 'entire':
598
+ msg = '(entire) ' + msg
599
+ logger.info(msg)
600
600
 
601
601
  def get(self):
602
602
  return self._actor.status_int
@@ -614,8 +614,9 @@ class AbstractOptimizer(ABC):
614
614
  self.parameters = pd.DataFrame()
615
615
  self.objectives = dict()
616
616
  self.constraints = dict()
617
- self.status = None # shared
618
- self.history = None # shared
617
+ self.entire_status = None # actor
618
+ self.history = None # actor
619
+ self.worker_status = None # actor
619
620
  self.message = ''
620
621
  self.seed = None
621
622
  self.timeout = None
@@ -623,9 +624,7 @@ class AbstractOptimizer(ABC):
623
624
  self.is_cluster = False
624
625
 
625
626
  def f(self, x):
626
-
627
- if self.status.get() == OptimizationStatus.LAUNCHING:
628
- self.status.set(OptimizationStatus.WAIT_1ST)
627
+ # interruption の実装は具象クラスに任せる
629
628
 
630
629
  # x の更新
631
630
  self.parameters['value'] = x
@@ -654,14 +653,13 @@ class AbstractOptimizer(ABC):
654
653
  self.message
655
654
  )
656
655
 
657
- if self.status.get() == OptimizationStatus.WAIT_1ST:
658
- self.status.set(OptimizationStatus.RUNNING)
659
-
660
656
  logger.debug('history.record end')
661
657
  return np.array(y), np.array(_y), np.array(c)
662
658
 
663
- def set_fem(self):
664
- self.fem = self.fem_class(**self.fem_kwargs)
659
+ def set_fem(self, skip_reconstruct=False):
660
+ # restore fem
661
+ if not skip_reconstruct:
662
+ self.fem = self.fem_class(**self.fem_kwargs)
665
663
 
666
664
  # COM 定数の restore
667
665
  for obj in self.objectives.values():
@@ -694,12 +692,66 @@ class AbstractOptimizer(ABC):
694
692
  else:
695
693
  raise ValueError('get_parameter() got invalid format: {format}')
696
694
 
697
- def _main(self, subprocess_idx):
698
- self.set_fem()
699
- self.main(subprocess_idx)
695
+ def check_interruption(self):
696
+ if self.entire_status.get() == OptimizationStatus.INTERRUPTING:
697
+ self.worker_status.set(OptimizationStatus.INTERRUPTING)
698
+ self.finalize()
699
+ return True
700
+ else:
701
+ return False
702
+
703
+ def finalize(self):
704
+ del self.fem
705
+ self.worker_status.set(OptimizationStatus.TERMINATED)
706
+
707
+ def _main(
708
+ self,
709
+ subprocess_idx,
710
+ worker_status_list,
711
+ wait_setup,
712
+ skip_set_fem=False,
713
+ ) -> None:
714
+
715
+ # 自分の worker_status の取得
716
+ self.worker_status = worker_status_list[subprocess_idx]
717
+ self.worker_status.set(OptimizationStatus.LAUNCHING_FEM)
718
+
719
+ if self.check_interruption():
720
+ return None
721
+
722
+ # set_fem をはじめ、終了したらそれを示す
723
+ if not skip_set_fem: # なくても動く??
724
+ self.set_fem()
725
+ self.worker_status.set(OptimizationStatus.WAIT_OTHER_WORKERS)
726
+
727
+ # wait_setup or not
728
+ if wait_setup:
729
+ while True:
730
+ if self.check_interruption():
731
+ return None
732
+ # 他のすべての worker_status が wait 以上になったら break
733
+ if all([ws.get() >= OptimizationStatus.WAIT_OTHER_WORKERS for ws in worker_status_list]):
734
+ break
735
+ sleep(1)
736
+ else:
737
+ if self.check_interruption():
738
+ return None
739
+
740
+ # set status running
741
+ if self.entire_status.get() < OptimizationStatus.RUNNING:
742
+ self.entire_status.set(OptimizationStatus.RUNNING)
743
+ self.worker_status.set(OptimizationStatus.RUNNING)
744
+
745
+ # run and finalize
746
+ try:
747
+ self.main(subprocess_idx)
748
+ finally:
749
+ self.finalize()
750
+
751
+ return None
700
752
 
701
753
  @abstractmethod
702
- def main(self, subprocess_idx: int):
754
+ def main(self, subprocess_idx: int = 0) -> None:
703
755
  """Called from dask worker."""
704
756
  pass
705
757
 
@@ -731,7 +783,7 @@ class OptunaOptimizer(AbstractOptimizer):
731
783
  def _objective(self, trial):
732
784
 
733
785
  # 中断の確認 (FAIL loop に陥る対策)
734
- if self.status.get() == OptimizationStatus.INTERRUPTING:
786
+ if self.entire_status.get() == OptimizationStatus.INTERRUPTING:
735
787
  trial.study.stop() # 現在実行中の trial を最後にする
736
788
  return None # set TrialState FAIL
737
789
 
@@ -772,7 +824,7 @@ class OptunaOptimizer(AbstractOptimizer):
772
824
  print(self.get_parameter('dict'))
773
825
 
774
826
  # 中断の確認 (解析中に interrupt されている場合対策)
775
- if self.status.get() == OptimizationStatus.INTERRUPTING:
827
+ if self.entire_status.get() == OptimizationStatus.INTERRUPTING:
776
828
  trial.study.stop() # 現在実行中の trial を最後にする
777
829
  return None # set TrialState FAIL
778
830
 
@@ -789,7 +841,7 @@ class OptunaOptimizer(AbstractOptimizer):
789
841
  trial.set_user_attr('constraint', _c)
790
842
 
791
843
  # 中断の確認 (解析中に interrupt されている場合対策)
792
- if self.status.get() == OptimizationStatus.INTERRUPTING:
844
+ if self.entire_status.get() == OptimizationStatus.INTERRUPTING:
793
845
  trial.study.stop() # 現在実行中の trial を最後にする
794
846
  return None # set TrialState FAIL
795
847
 
@@ -867,14 +919,14 @@ class OptunaOptimizer(AbstractOptimizer):
867
919
  # if is_restart, load study
868
920
  else:
869
921
  if not os.path.exists(storage_path):
870
- raise FileNotFoundError(f'{storage_path} が見つかりません。.db ファイルは .csv ファイルと同じフォルダに生成されます。クラスター解析の場合は、スケジューラを起動したフォルダに生成されます。')
922
+ msg = f'{storage_path} が見つかりません。'
923
+ msg += '.db ファイルは .csv ファイルと同じフォルダに生成されます。'
924
+ msg += 'クラスター解析の場合は、スケジューラを起動したフォルダに生成されます。'
925
+ raise FileNotFoundError(msg)
871
926
  self.storage = optuna.integration.dask.DaskStorage(
872
927
  f'sqlite:///{storage_path}',
873
928
  )
874
929
 
875
-
876
-
877
-
878
930
  def add_init_parameter(
879
931
  self,
880
932
  parameter: dict or Iterable,
@@ -895,7 +947,7 @@ class OptunaOptimizer(AbstractOptimizer):
895
947
  name = f'additional initial ({name})'
896
948
  self.additional_initial_parameter.append([parameter, name])
897
949
 
898
- def main(self, subprocess_idx):
950
+ def main(self, subprocess_idx=0):
899
951
  """Dask worker から呼ばれる関数"""
900
952
 
901
953
  # (re)set random seed
@@ -925,27 +977,20 @@ class OptunaOptimizer(AbstractOptimizer):
925
977
  callbacks=self.optimize_callbacks,
926
978
  )
927
979
 
928
- # finalize
929
- del self.fem
930
-
931
980
 
932
981
  class FEMOpt:
933
982
  """Base class to control FEM interface and optimizer.
934
983
 
935
984
  Attributes:
936
- fem (FEMInterface): The finite element method interface.
937
- history_path (str): The path to the history (.csv) file .
938
- ipv (InterprocessVariables): The interprocess variables.
939
- parameters (pd.DataFrame): The DataFrame object for storing the parameters.
940
- objectives (dict): The dictionary of objective functions.
941
- constraints (dict): The dictionary of constraint functions.
942
- history (History): The history of optimization results.
943
- monitor (Monitor): The monitor object for visualization and monitoring.
944
- monitor_thread: Thread object for monitor server.
945
- seed (int or None): The random seed for reproducibility.
946
- message(str) : Additional information or messages related to the optimization process
947
- obj_values ([float]): A list to store objective values during optimization
948
- cns_values ([float]): A list to store constraint values during optimization
985
+ fem (FEMInterface): The interface of FEM system.
986
+ client (Client): Dask client. For detail, see dask documentation.
987
+ scheduler_address (str or None): Dask scheduler address. If None, LocalCluster will be used.
988
+ status (OptimizationStatus): Entire process status. This contains dask actor.
989
+ history(History): History of optimization process. This contains dask actor.
990
+ history_path (str): The path to the history (.csv) file.
991
+ worker_status_list([OptimizationStatus]): Process status of each dask worker.
992
+ monitor_process_future(Future): Future of monitor server process. This is dask future.
993
+ monitor_server_kwargs(dict): Monitor server parameter. Currently, the valid arguments are hostname and port.
949
994
 
950
995
  """
951
996
 
@@ -959,7 +1004,7 @@ class FEMOpt:
959
1004
  """Initializes an FEMOpt instance.
960
1005
 
961
1006
  Args:
962
- fem (FEMInterface, optional): The finite element method interface. Defaults to None. If None, automattically set to FemtetInterface.
1007
+ fem (FEMInterface, optional): The finite element method interface. Defaults to None. If None, automatically set to FemtetInterface.
963
1008
  opt (AbstractOptimizer):
964
1009
  history_path (str, optional): The path to the history file. Defaults to None. If None, '%Y_%m_%d_%H_%M_%S.csv' is created in current directory.
965
1010
  scheduler_address (str or None): If cluster processing, set this parameter like ``"tcp://xxx.xxx.xxx.xxx:xxxx"``.
@@ -986,10 +1031,12 @@ class FEMOpt:
986
1031
 
987
1032
  # メンバーの宣言
988
1033
  self.client = None
989
- self.status = None
990
- self.history = None
1034
+ self.status = None # actor
1035
+ self.history = None # actor
1036
+ self.worker_status_list = None # [actor]
991
1037
  self.monitor_process_future = None
992
1038
  self.monitor_server_kwargs = dict()
1039
+ self.monitor_process_worker_name = None
993
1040
 
994
1041
  # multiprocess 時に pickle できないオブジェクト参照の削除
995
1042
  def __getstate__(self):
@@ -1021,7 +1068,7 @@ class FEMOpt:
1021
1068
 
1022
1069
  Args:
1023
1070
  name (str): The name of the parameter.
1024
- initial_value (float or None, optional): The initial value of the parameter. Defaults to None. If None, try to get inittial value from FEMInterface.
1071
+ initial_value (float or None, optional): The initial value of the parameter. Defaults to None. If None, try to get initial value from FEMInterface.
1025
1072
  lower_bound (float or None, optional): The lower bound of the parameter. Defaults to None. However, this argument is required for some algorithms.
1026
1073
  upper_bound (float or None, optional): The upper bound of the parameter. Defaults to None. However, this argument is required for some algorithms.
1027
1074
  memo (str, optional): Additional information about the parameter. Defaults to ''.
@@ -1198,16 +1245,16 @@ class FEMOpt:
1198
1245
  n_trials=None,
1199
1246
  n_parallel=1,
1200
1247
  timeout=None,
1248
+ wait_setup=True,
1201
1249
  ):
1202
1250
  """Runs the main optimization process.
1203
1251
 
1204
1252
  Args:
1205
- n_trials (int or None): The number of trials. Defaults to None.
1206
- n_parallel (int): The number of parallel processes. Defaults to 1.
1207
- timeout (float or None): The maximum amount of time in seconds that each trial can run. Defaults to None.
1208
- method (str): The optimization method to use. Defaults to 'TPE'.
1209
- **setup_kwargs: Additional keyword arguments for setting up the optimization process.
1210
-
1253
+ n_trials (int or None, optional): The number of trials. Defaults to None.
1254
+ n_parallel (int, optional): The number of parallel processes. Defaults to 1.
1255
+ timeout (float or None, optional): The maximum amount of time in seconds that each trial can run. Defaults to None.
1256
+ wait_setup (bool, optional): Wait for all workers launching FEM system. Defaults to True.
1257
+
1211
1258
  Tip:
1212
1259
  If setup_monitor_server() is not executed, a local server for monitoring will be started at localhost:8080.
1213
1260
 
@@ -1224,37 +1271,46 @@ class FEMOpt:
1224
1271
 
1225
1272
  """
1226
1273
 
1274
+ # 共通引数
1275
+ self.opt.n_trials = n_trials
1276
+ self.opt.timeout = timeout
1227
1277
 
1228
- # dask メンバーの設定
1278
+ # クラスターの設定
1229
1279
  self.opt.is_cluster = self.scheduler_address is not None
1230
1280
  if self.opt.is_cluster:
1231
1281
  # 既存のクラスターに接続
1232
1282
  logger.info('Connecting to existing cluster.')
1233
1283
  self.client = Client(self.scheduler_address)
1234
-
1235
1284
  else:
1285
+ # ローカルクラスターを構築
1236
1286
  logger.info('Launching single machine cluster. This may take tens of seconds.')
1237
1287
  cluster = LocalCluster(processes=True)
1238
1288
  self.client = Client(cluster, direct_to_workers=False)
1239
1289
  self.scheduler_address = self.client.scheduler.address
1240
1290
 
1291
+ # タスクを振り分ける worker を指定
1292
+ subprocess_indices = list(range(n_parallel))
1293
+ if not self.opt.is_cluster:
1294
+ subprocess_indices = subprocess_indices[1:]
1295
+ worker_addresses = list(self.client.nthreads().keys())
1296
+ assert max(subprocess_indices) <= len(worker_addresses)-1, f'コア数{len(worker_addresses)}は不足しています。'
1297
+ worker_addresses = worker_addresses[:len(range(n_parallel))] # TODO: ノードごとに適度に振り分ける
1298
+ if not self.opt.is_cluster:
1299
+ worker_addresses[0] = 'Main'
1300
+
1241
1301
  # monitor 用 worker を起動
1242
1302
  logger.info('Launching monitor server. This may take a few seconds.')
1243
1303
  self.monitor_process_worker_name = datetime.datetime.now().strftime("Monitor-%Y%m%d-%H%M%S")
1244
1304
  cmd = f'dask worker {self.client.scheduler.address} --name {self.monitor_process_worker_name} --no-nanny'
1245
- worker_addresses = list(self.client.has_what().keys())
1246
- Popen(cmd, shell=True) #, stdout=PIPE) --> cause stream error
1305
+ current_n_workers = len(self.client.nthreads().keys())
1306
+ Popen(cmd, shell=True) # , stdout=PIPE) --> cause stream error
1247
1307
 
1248
1308
  # monitor 用 worker が増えるまで待つ
1249
- while not (len(self.client.has_what()) > len(worker_addresses)):
1250
- sleep(1)
1251
- new_worker_addresses = list(self.client.has_what().keys())
1252
- launched_worker_addresses = [w_adrs for w_adrs in new_worker_addresses if not w_adrs in worker_addresses]
1253
- assert len(launched_worker_addresses) == 1
1254
- launched_worker_address = launched_worker_addresses[0]
1309
+ self.client.wait_for_workers(n_workers=current_n_workers+1)
1255
1310
 
1256
1311
  # actor の設定
1257
1312
  self.status = OptimizationStatus(self.client)
1313
+ self.worker_status_list = [OptimizationStatus(self.client, name) for name in worker_addresses] # tqdm 検討
1258
1314
  self.status.set(OptimizationStatus.SETTING_UP)
1259
1315
  self.history = History(self.history_path, self.client)
1260
1316
  self.history.init(
@@ -1263,17 +1319,13 @@ class FEMOpt:
1263
1319
  list(self.opt.constraints.keys()),
1264
1320
  )
1265
1321
 
1266
- # 共通引数
1267
- self.opt.n_trials = n_trials
1268
- self.opt.timeout = timeout
1269
-
1270
1322
  # fem
1271
1323
  self.fem.setup_before_parallel(self.client)
1272
1324
 
1273
1325
  # opt
1274
1326
  self.opt.fem_class = type(self.fem)
1275
1327
  self.opt.fem_kwargs = self.fem.kwargs
1276
- self.opt.status = self.status
1328
+ self.opt.entire_status = self.status
1277
1329
  self.opt.history = self.history
1278
1330
  self.opt.setup_before_parallel()
1279
1331
 
@@ -1282,39 +1334,42 @@ class FEMOpt:
1282
1334
  start_monitor_server_forever,
1283
1335
  self.history, self.status, # args
1284
1336
  **self.monitor_server_kwargs, # kwargs
1285
- workers=launched_worker_address, # if invalid arg,
1337
+ workers=self.monitor_process_worker_name, # if invalid arg,
1286
1338
  allow_other_workers=False
1287
1339
  )
1288
1340
 
1289
1341
  # クラスターでの計算開始
1290
- self.status.set(OptimizationStatus.LAUNCHING)
1342
+ self.status.set(OptimizationStatus.LAUNCHING_FEM)
1291
1343
  start = time()
1292
- subprocess_indices = list(range(n_parallel))
1344
+ calc_futures = self.client.map(
1345
+ self.opt._main,
1346
+ subprocess_indices,
1347
+ [self.worker_status_list]*len(subprocess_indices),
1348
+ [wait_setup]*len(subprocess_indices),
1349
+ workers=worker_addresses,
1350
+ allow_other_workers=False,
1351
+ )
1293
1352
 
1294
- if self.opt.is_cluster:
1295
- # FIXME: monitor-server-process には割り当てないようにする
1296
- worker_addresses = list(self.client.nthreads().keys())
1297
- worker_addresses.remove(launched_worker_address)
1298
- calc_futures = self.client.map(
1299
- self.opt._main,
1300
- subprocess_indices,
1301
- workers=worker_addresses,
1302
- allow_other_workers=False,
1303
- )
1353
+ t_main = None
1354
+ if not self.opt.is_cluster:
1355
+ # ローカルプロセスでの計算(opt._main 相当の処理)
1356
+ subprocess_idx = 0
1304
1357
 
1305
- else:
1306
- # ローカルなら既存の fem をひとつ使う
1307
- calc_futures = self.client.map(self.opt._main, subprocess_indices[1:])
1308
-
1309
- # ローカルプロセスでの計算開始
1310
- # constants の restore (map の後なら大丈夫)
1311
- # TODO: 最適化中に cluster を scale する場合はここに気を付ける
1312
- for obj in self.opt.objectives.values():
1313
- obj._restore_constants()
1314
- for cns in self.opt.constraints.values():
1315
- cns._restore_constants()
1358
+ # set_fem
1316
1359
  self.opt.fem = self.fem
1317
- t_main = Thread(target=self.opt.main, args=(subprocess_indices[0],))
1360
+ self.opt.set_fem(skip_reconstruct=True)
1361
+
1362
+ t_main = Thread(
1363
+ target=self.opt._main,
1364
+ args=(
1365
+ subprocess_idx,
1366
+ self.worker_status_list,
1367
+ wait_setup,
1368
+ ),
1369
+ kwargs=dict(
1370
+ skip_set_fem=True,
1371
+ )
1372
+ )
1318
1373
  t_main.start()
1319
1374
 
1320
1375
  # save history
@@ -1330,11 +1385,11 @@ class FEMOpt:
1330
1385
  t_save_history = Thread(target=save_history)
1331
1386
  t_save_history.start()
1332
1387
 
1333
-
1334
1388
  # 終了を待つ
1335
1389
  self.client.gather(calc_futures)
1336
1390
  if not self.opt.is_cluster: # 既存の fem を使っているならそれも待つ
1337
- t_main.join()
1391
+ if t_main is not None:
1392
+ t_main.join()
1338
1393
  self.status.set(OptimizationStatus.TERMINATED)
1339
1394
  end = time()
1340
1395
 
@@ -1357,6 +1412,8 @@ class FEMOpt:
1357
1412
  # terminate actors
1358
1413
  self.client.cancel(self.history._future, force=True)
1359
1414
  self.client.cancel(self.status._future, force=True)
1415
+ for worker_status in self.worker_status_list:
1416
+ self.client.cancel(worker_status._future, force=True)
1360
1417
  logger.info('Terminate actors.')
1361
1418
  sleep(1)
1362
1419
 
@@ -1393,4 +1450,3 @@ def start_monitor_server_forever(history, status, host='localhost', port=8080):
1393
1450
  monitor = Monitor(history, status)
1394
1451
  monitor.start_server(host, port)
1395
1452
  return 'Exit monitor server process gracefully'
1396
-
@@ -9,7 +9,7 @@ import pandas as pd
9
9
  import psutil
10
10
  from pywintypes import com_error
11
11
  from pythoncom import CoInitialize, CoUninitialize
12
- from win32com.client import constants
12
+ from win32com.client import constants, Dispatch
13
13
  from dask.distributed import get_worker
14
14
  from femtetutils import util
15
15
 
@@ -119,6 +119,7 @@ class FemtetInterface(FEMInterface):
119
119
  femprj_path=None,
120
120
  model_name=None,
121
121
  connect_method='auto',
122
+ strict_pid_specify=False,
122
123
  **kwargs
123
124
  ):
124
125
  """Initializes the FemtetInterface.
@@ -144,12 +145,13 @@ class FemtetInterface(FEMInterface):
144
145
  self.model_name = model_name
145
146
  self.connect_method = connect_method
146
147
 
147
- # その他のメンバーの宣言や初期化初期化
148
+ # その他のメンバーの宣言や初期化
148
149
  self.Femtet = None
149
150
  self.quit_when_destruct = False
150
151
  self.connected_method = 'unconnected'
151
152
  self.parameters = None
152
153
  self.max_api_retry = 3
154
+ self.strict_pid_specify = strict_pid_specify
153
155
 
154
156
  # dask サブプロセスのときは femprj を更新し connect_method を new にする
155
157
  try:
@@ -198,9 +200,12 @@ class FemtetInterface(FEMInterface):
198
200
 
199
201
  def _connect_new_femtet(self):
200
202
  logger.info('└ Try to launch and connect new Femtet process.')
201
- self.Femtet, _ = launch_and_dispatch_femtet()
203
+
204
+ self.Femtet, _ = launch_and_dispatch_femtet(strict_pid_specify=self.strict_pid_specify)
205
+
202
206
  self.connected_method = 'new'
203
207
 
208
+
204
209
  def _connect_existing_femtet(self, pid: int or None = None):
205
210
  logger.info('└ Try to connect existing Femtet process.')
206
211
  # 既存の Femtet を探して Dispatch する。
@@ -634,7 +639,7 @@ class FemtetWithNXInterface(FemtetInterface):
634
639
  femprj_path=femprj_path,
635
640
  model_name=model_name,
636
641
  connect_method=connect_method,
637
- prt_path = os.path.basename(prt_path) # upload_file でアップロードされたファイルへのパスになる
642
+ prt_path=os.path.basename(prt_path) # upload_file でアップロードされたファイルへのパスになる
638
643
  )
639
644
 
640
645
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyfemtet"
3
- version = "0.3.3" # ignored by versioning plugin
3
+ version = "0.3.5" # ignored by versioning plugin
4
4
  description = "Design parameter optimization using Femtet."
5
5
  authors = ["kazuma.naito <kazuma.naito@murata.com>"]
6
6
  readme = "README.md"
@@ -1 +0,0 @@
1
- __version__ = "0.3.3"
File without changes
File without changes
File without changes