PyFemtet 0.3.1__tar.gz → 0.3.3__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 (26) hide show
  1. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/PKG-INFO +2 -1
  2. pyfemtet-0.3.3/pyfemtet/__init__.py +1 -0
  3. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/dispatch_extensions.py +52 -47
  4. pyfemtet-0.3.3/pyfemtet/logger.py +99 -0
  5. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/opt/base.py +46 -33
  6. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/opt/interface.py +22 -18
  7. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyproject.toml +2 -1
  8. pyfemtet-0.3.1/pyfemtet/__init__.py +0 -1
  9. pyfemtet-0.3.1/pyfemtet/opt/_optimizer.py +0 -241
  10. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/LICENSE +0 -0
  11. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/README.md +0 -0
  12. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.femprj +0 -0
  13. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.prt +0 -0
  14. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.py +0 -0
  15. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/FemtetPJTSample/gau_ex08_parametric.femprj +0 -0
  16. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/FemtetPJTSample/gau_ex08_parametric.py +0 -0
  17. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/FemtetPJTSample/her_ex40_parametric.femprj +0 -0
  18. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/FemtetPJTSample/her_ex40_parametric.py +0 -0
  19. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/FemtetPJTSample/wat_ex14_parallel_parametric.py +0 -0
  20. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/FemtetPJTSample/wat_ex14_parametric.femprj +0 -0
  21. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/FemtetPJTSample/wat_ex14_parametric.py +0 -0
  22. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/_old/_SW_Femtet.py +0 -0
  23. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/core.py +0 -0
  24. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/opt/_FemtetWithNX/update_model.py +0 -0
  25. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/opt/__init__.py +0 -0
  26. {pyfemtet-0.3.1 → pyfemtet-0.3.3}/pyfemtet/opt/monitor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyfemtet
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Design parameter optimization using Femtet.
5
5
  Home-page: https://github.com/pyfemtet/pyfemtet
6
6
  License: BSD-3-Clause
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Requires-Dist: botorch (>=0.9.5,<0.10.0) ; python_version < "3.12"
17
+ Requires-Dist: colorlog (>=6.8.0,<7.0.0)
17
18
  Requires-Dist: dash (>=2.14.2,<3.0.0)
18
19
  Requires-Dist: dash-bootstrap-components (>=1.5.0,<2.0.0)
19
20
  Requires-Dist: dask (>=2023.12.1,<2024.0.0)
@@ -0,0 +1 @@
1
+ __version__ = "0.3.3"
@@ -3,8 +3,6 @@ from typing import Tuple
3
3
  import os
4
4
  from time import time, sleep
5
5
  from multiprocessing import current_process
6
- import logging
7
- import warnings
8
6
 
9
7
  import psutil
10
8
  from dask.distributed import Lock
@@ -18,15 +16,13 @@ from femtetutils import util
18
16
  from multiprocessing.context import BaseContext, SpawnProcess, _concrete_contexts
19
17
  from multiprocessing.process import _children, _cleanup
20
18
 
21
-
22
- logger = logging.getLogger('dispatch_log')
23
- handler = logging.StreamHandler()
24
- handler.setFormatter(logging.Formatter('[%(process)d] %(message)s'))
25
- logger.addHandler(handler)
26
- # logger.setLevel(logging.INFO)
19
+ import logging
20
+ from .logger import get_logger
21
+ logger = get_logger('dispatch')
22
+ logger.setLevel(logging.INFO)
27
23
 
28
24
 
29
- DISPATCH_TIMEOUT = 60
25
+ DISPATCH_TIMEOUT = 120
30
26
 
31
27
 
32
28
  class DispatchExtensionException(Exception):
@@ -114,11 +110,6 @@ _concrete_contexts.update(
114
110
  )
115
111
 
116
112
 
117
- def _log_prefix():
118
- prefix = current_process().name + ' : '
119
- return prefix
120
-
121
-
122
113
  def _get_hwnds(pid):
123
114
  """Proces ID から window handle を取得します."""
124
115
  def callback(hwnd, _hwnds):
@@ -160,7 +151,6 @@ def launch_and_dispatch_femtet(timeout=DISPATCH_TIMEOUT) -> Tuple[IFemtet, int]:
160
151
  Returns:
161
152
  Tuple[IFemtet, int]:
162
153
  """
163
-
164
154
  util.execute_femtet()
165
155
  pid = util.get_last_executed_femtet_process_id()
166
156
  logger.debug(f'Target pid is {pid}.')
@@ -169,7 +159,7 @@ def launch_and_dispatch_femtet(timeout=DISPATCH_TIMEOUT) -> Tuple[IFemtet, int]:
169
159
  return Femtet, pid
170
160
 
171
161
 
172
- def dispatch_femtet(timeout=DISPATCH_TIMEOUT) -> Tuple[IFemtet, int]:
162
+ def dispatch_femtet(timeout=DISPATCH_TIMEOUT, subprocess_log_prefix='') -> Tuple[IFemtet, int]:
173
163
  """Connect to existing Femtet process.
174
164
 
175
165
  Args:
@@ -181,11 +171,13 @@ def dispatch_femtet(timeout=DISPATCH_TIMEOUT) -> Tuple[IFemtet, int]:
181
171
  Returns:
182
172
  Tuple[IFemtet, int]:
183
173
  """
184
-
185
174
  # Dispatch
186
- logger.debug(f'{_log_prefix()}Try to connect Femtet.')
175
+ if subprocess_log_prefix:
176
+ logger.debug('%s'+'Try to connect Femtet.', subprocess_log_prefix)
177
+ else:
178
+ logger.info('Try to connect Femtet.')
187
179
  Femtet = Dispatch('FemtetMacro.Femtet')
188
- logger.debug(f'{_log_prefix()}Dispatched.')
180
+ logger.debug('%s'+'Dispatch finished.', subprocess_log_prefix)
189
181
 
190
182
  # timeout 秒以内に接続が確立するか
191
183
  start = time()
@@ -194,27 +186,31 @@ def dispatch_femtet(timeout=DISPATCH_TIMEOUT) -> Tuple[IFemtet, int]:
194
186
 
195
187
  # 接続が確立
196
188
  if hwnd > 0:
197
- logger.debug(f'{_log_prefix()}Current hwnd is {hwnd}.')
189
+ logger.debug('%s'+f'Dispatched hwnd is {hwnd} and its pid is {_get_pid(hwnd)}. Connection established.', subprocess_log_prefix)
198
190
  break
199
191
  else:
200
- logger.debug(f'{_log_prefix()}Current hwnd is {hwnd}. Wait for established connection.')
192
+ logger.debug('%s'+f'Dispatched hwnd is {hwnd}. Waiting for establishing connection.', subprocess_log_prefix)
201
193
 
202
194
  # 接続がタイムアウト
203
195
  if time()-start > timeout:
204
- raise FemtetConnectionTimeoutError(f'Connect trial with Femtet is timed out in {timeout} sec')
196
+ raise FemtetConnectionTimeoutError(f'Connection trial with Femtet is timed out in {timeout} sec')
205
197
 
206
198
  sleep(1)
207
199
 
208
200
  # pid を取得
209
201
  pid = _get_pid(hwnd)
210
- logger.info(f'{_log_prefix()}hwnd is {hwnd} and pid is {pid} of connected Femtet.')
211
-
212
- if ('MainProcess' in _log_prefix()) or 'Dask Worker Process' in _log_prefix():
213
- print(f'INFO: Process id of connected Femtet = {pid}')
202
+ if subprocess_log_prefix:
203
+ logger.debug('%s'+f'Successfully connected. The pid of Femtet is {pid}.', subprocess_log_prefix)
204
+ else:
205
+ logger.info(f'Successfully connected. The pid of Femtet is {pid}.')
214
206
 
215
207
  return Femtet, pid
216
208
 
217
209
 
210
+ def _get_subprocess_log_prefix():
211
+ return f'({current_process().name}) '
212
+
213
+
218
214
  def _block_other_femtets(
219
215
  target_pid,
220
216
  subprocess_idx,
@@ -239,9 +235,10 @@ def _block_other_femtets(
239
235
  else:
240
236
  lock_inter_subproc.acquire()
241
237
  try:
242
- logger.debug(f'{_log_prefix()}Try to block Femtet.')
243
- _, my_pid = dispatch_femtet(2)
238
+ logger.debug('%s'+f'Subprocess started.', _get_subprocess_log_prefix())
239
+ _, my_pid = dispatch_femtet(2, _get_subprocess_log_prefix())
244
240
  except DispatchExtensionException:
241
+ logger.debug('%s'+f'Connection failed. The other subprocesses will skip.', _get_subprocess_log_prefix())
245
242
  my_pid = 0
246
243
  # ひとつでもここに来れば、それ以降は探しても無駄なのでスキップフラグを立てる
247
244
  for i in range(len(connection_flags)-1):
@@ -251,8 +248,6 @@ def _block_other_femtets(
251
248
 
252
249
  # # target なら Dispatch の終了を通知する前に main を lock する
253
250
  # if my_pid == target_pid:
254
- # logger.info(f'{_log_prefix()}Connected to {my_pid}. This is the target.')
255
- # logger.info(f'{_log_prefix()}Start to block main process.')
256
251
  # lock_main.acquire()
257
252
 
258
253
  # Dispatch の終了を通知
@@ -266,32 +261,32 @@ def _block_other_femtets(
266
261
  # if all(connection_flags[:-1]):
267
262
  # break
268
263
  # if time()-start > timeout_main_wait_for_us:
269
- # logger.error(f'{_log_prefix()}Timed out while waiting for other subprocesses.')
270
264
  # return -1
271
265
  # sleep(1)
272
- # logger.debug(f'{_log_prefix()}All subprocesses seems to finish connection. Release {my_pid} and main.')
273
- logger.debug(f'{_log_prefix()}Release {my_pid}.')
266
+ logger.debug('%s'+f'Release {my_pid}.', _get_subprocess_log_prefix())
274
267
  # lock_main.release()
275
268
  return 0
276
269
 
277
270
  # my_pid が非正なら何にも関与しないので即刻終了する
278
271
  elif my_pid <= 0:
279
- logger.debug(f'{_log_prefix()}Failed or timed out to connect Femtet. Exit immediately.')
272
+ logger.debug('%s'+'Failed or timed out to connect Femtet.', _get_subprocess_log_prefix())
280
273
  return 1
281
274
 
282
275
  # そうでなければメインプロセスが Dispatch を終えるまで解放しない
283
276
  else:
284
- logger.debug(f'{_log_prefix()}Connected to {my_pid}. Keep connection to block.')
277
+ logger.debug('%s'+f'Connected to {my_pid}. Keep connection to block.', _get_subprocess_log_prefix())
285
278
  start = time()
286
279
  while True:
287
280
  if connection_flags[-1]:
288
- break
281
+ logger.debug('%s'+'Main process seems to connect target Femtet.', _get_subprocess_log_prefix())
282
+ return 1
289
283
  if time()-start > timeout_main_wait_for_us:
290
- logger.error(f'{_log_prefix()}Timed out while waiting for the main process.')
284
+ logger.warning(
285
+ '%s'+f'Timed out to wait for the main process to {timeout_main_wait_for_us}',
286
+ _get_subprocess_log_prefix()
287
+ )
291
288
  return -1
292
289
  sleep(1)
293
- logger.debug(f'{_log_prefix()}Main process seems to connect target Femtet. Exit immediately.')
294
- return 1
295
290
 
296
291
 
297
292
  def dispatch_specific_femtet(pid, timeout=DISPATCH_TIMEOUT) -> Tuple[IFemtet, int]:
@@ -323,7 +318,7 @@ def dispatch_specific_femtet(pid, timeout=DISPATCH_TIMEOUT) -> Tuple[IFemtet, in
323
318
  return dispatch_specific_femtet_core(pid, timeout) # for logger, out of except.
324
319
 
325
320
 
326
- def dispatch_specific_femtet_core(pid, timeout=10) -> Tuple[IFemtet, int]:
321
+ def dispatch_specific_femtet_core(pid, timeout=DISPATCH_TIMEOUT) -> Tuple[IFemtet, int]:
327
322
  """Connect Femtet whose process id is specified.
328
323
 
329
324
  Warnings:
@@ -380,7 +375,6 @@ def dispatch_specific_femtet_core(pid, timeout=10) -> Tuple[IFemtet, int]:
380
375
  Returns:
381
376
  Tuple[IFemtet, int]:
382
377
  """
383
-
384
378
  # TODO: 安定性を見て lock_main を復活させるか決める
385
379
 
386
380
  if timeout < 5:
@@ -388,7 +382,7 @@ def dispatch_specific_femtet_core(pid, timeout=10) -> Tuple[IFemtet, int]:
388
382
 
389
383
  # 存在する Femtet プロセスの列挙
390
384
  pids = _get_pids('Femtet.exe')
391
- logger.info(f'{_log_prefix()}PID of existing Femtet processes: {pids}')
385
+ logger.info(f'PID of existing Femtet processes: {pids}')
392
386
  if not (pid in pids):
393
387
  raise FemtetNotFoundException(f"Femtet (pid = {pid}) doesn't exist.")
394
388
 
@@ -406,9 +400,11 @@ def dispatch_specific_femtet_core(pid, timeout=10) -> Tuple[IFemtet, int]:
406
400
  connection_flags.append(False)
407
401
 
408
402
  # 目的以外の Femtet をブロックする子プロセスを開始
403
+ logger.debug('Start subprocess to block Femtet other than target pid.')
409
404
  processes = []
410
405
  for subprocess_id in range(len(pids)):
411
406
  p = _NestableSpawnProcess(
407
+ # p = Process(
412
408
  target=_block_other_femtets,
413
409
  args=(
414
410
  pid,
@@ -427,7 +423,15 @@ def dispatch_specific_femtet_core(pid, timeout=10) -> Tuple[IFemtet, int]:
427
423
  while True:
428
424
  if all(connection_flags[:-1]):
429
425
  break
430
- if time()-start > 10:
426
+ if time()-start > timeout:
427
+ # 子プロセスを終了する
428
+ for p in processes:
429
+ p.terminate()
430
+ p.join()
431
+ try:
432
+ lock_inter_subproc.release()
433
+ except RuntimeError:
434
+ pass
431
435
  raise FemtetConnectionTimeoutError(f'Connect trial with specific Femtet (pid = {pid}) is timed out in {timeout} sec')
432
436
  sleep(1)
433
437
 
@@ -436,7 +440,7 @@ def dispatch_specific_femtet_core(pid, timeout=10) -> Tuple[IFemtet, int]:
436
440
 
437
441
  # 子プロセスによるブロックが終了しているので Dispatch する
438
442
  try:
439
- logger.debug(f'{_log_prefix()}All Femtets except to target seem to be blocked. Try Dispatch.')
443
+ logger.debug('Block process seems to be finished. Try Dispatch.')
440
444
  Femtet, my_pid = dispatch_femtet()
441
445
  except DispatchExtensionException as e:
442
446
  # lock_main.release()
@@ -451,12 +455,13 @@ def dispatch_specific_femtet_core(pid, timeout=10) -> Tuple[IFemtet, int]:
451
455
 
452
456
  if my_pid != pid: # pid の結果が違う場合
453
457
  txt = f'Target pid is {pid}, but connected pid is {my_pid}. '
454
- txt += f'The common reason is that this python process once connected {pid}.'
455
- warnings.warn(txt)
458
+ txt += f'The common reason is that THIS python process once connected {pid}.'
459
+ logger.warn(txt)
460
+ # warnings.warn(txt)
456
461
 
457
462
  return Femtet, my_pid
458
463
 
459
464
 
460
465
  if __name__ == '__main__':
461
- _Femtet, _my_pid = launch_and_dispatch_femtet()
466
+ _Femtet, _my_pid = launch_and_dispatch_femtet(5)
462
467
  # _Femtet, _my_pid = dispatch_specific_femtet(pid=26124)
@@ -0,0 +1,99 @@
1
+ import os
2
+ import sys
3
+ import logging
4
+ from colorlog import ColoredFormatter
5
+ from dask.distributed import get_worker
6
+
7
+
8
+ __all__ = ['get_logger']
9
+
10
+
11
+ def _get_worker_name_as_prefix():
12
+ name = '(Main) '
13
+ try:
14
+ worker = get_worker()
15
+ # address = worker.address
16
+ name = f'(Worker:{worker.name}) '
17
+ except ValueError:
18
+ pass
19
+ return name
20
+
21
+
22
+ class DaskLogRecord(logging.LogRecord):
23
+ """Generate a log message with dask worker name."""
24
+
25
+ # def __init__(self, *args, **kwargs):
26
+ # super().__init__(*args, **kwargs)
27
+ # self.worker = _get_worker_name_as_prefix()
28
+
29
+ def getMessage(self):
30
+ """Add worker name to loggin message.
31
+
32
+ This function is originated from logging.LogRecord.
33
+
34
+ # Copyright (C) 2001-2022 Vinay Sajip. All Rights Reserved.
35
+
36
+ """
37
+ msg = str(self.msg)
38
+ if self.args:
39
+ msg = msg % self.args
40
+ msg = _get_worker_name_as_prefix() + msg
41
+ return msg
42
+
43
+
44
+ logging.setLogRecordFactory(DaskLogRecord) # すべての logging %(message)s の前に prefix を入れる
45
+
46
+
47
+ def _color_supported() -> bool:
48
+ """Detection of color support.
49
+
50
+ This function is originated from optuna.logging.
51
+
52
+ # Copyright (c) 2018 Preferred Networks, Inc.
53
+
54
+ """
55
+
56
+ # NO_COLOR environment variable:
57
+ if os.environ.get("NO_COLOR", None):
58
+ return False
59
+
60
+ if not hasattr(sys.stderr, "isatty") or not sys.stderr.isatty():
61
+ return False
62
+ else:
63
+ return True
64
+
65
+
66
+ def _create_formatter() -> logging.Formatter:
67
+ """Create a formatter."""
68
+ # header = f"[pyfemtet %(name)s] %(levelname).4s %(worker)s]"
69
+ header = f"[pyfemtet %(name)s %(levelname).4s]"
70
+ message = "%(message)s"
71
+
72
+ formatter = ColoredFormatter(
73
+ f"%(log_color)s{header}%(reset)s {message}",
74
+ datefmt=None,
75
+ reset=True,
76
+ log_colors={
77
+ "DEBUG": "purple",
78
+ "INFO": "cyan",
79
+ "WARNING": "yellow",
80
+ "ERROR": "light_red",
81
+ "CRITICAL": "red",
82
+ },
83
+ )
84
+
85
+ return formatter
86
+
87
+
88
+ def get_logger(logger_name):
89
+ """Return a logger with a default ColoredFormatter."""
90
+
91
+ formatter = _create_formatter()
92
+
93
+ logger = logging.getLogger(logger_name)
94
+ handler = logging.StreamHandler()
95
+ handler.setFormatter(formatter)
96
+ logger.addHandler(handler)
97
+ logger.setLevel(logging.DEBUG)
98
+
99
+ return logger
@@ -9,7 +9,6 @@ from time import time, sleep
9
9
  from threading import Thread
10
10
  from subprocess import Popen
11
11
  import warnings
12
- import logging
13
12
 
14
13
  import numpy as np
15
14
  import pandas as pd
@@ -19,7 +18,7 @@ from optuna.study import MaxTrialsCallback
19
18
  from optuna.trial import TrialState
20
19
  from optuna.exceptions import ExperimentalWarning
21
20
  from optuna._hypervolume import WFG
22
- from dask.distributed import LocalCluster, Client, Lock, get_worker
21
+ from dask.distributed import LocalCluster, Client, Lock
23
22
 
24
23
  from win32com.client import constants, Constants
25
24
 
@@ -27,12 +26,10 @@ from ..core import ModelError, MeshError, SolveError
27
26
  from .interface import FEMInterface, FemtetInterface
28
27
  from .monitor import Monitor
29
28
 
30
-
31
- logger = logging.getLogger('optimization-main')
32
- handler = logging.StreamHandler()
33
- handler.setFormatter(logging.Formatter('[%(process)d] %(message)s'))
34
- logger.addHandler(handler)
35
- logger.setLevel(logging.INFO) # TODO: optuna, dask の logger の書式を統一する
29
+ import logging
30
+ from ..logger import get_logger
31
+ logger = get_logger('opt')
32
+ logger.setLevel(logging.INFO)
36
33
 
37
34
 
38
35
  warnings.filterwarnings('ignore', category=ExperimentalWarning)
@@ -350,6 +347,7 @@ class History:
350
347
 
351
348
  # 引数の処理
352
349
  self.path = history_path # .csv
350
+ self.is_restart = False
353
351
  self._future = client.submit(HistoryDfCore, actor=True)
354
352
  self._actor_data = self._future.result()
355
353
  self.param_names = []
@@ -359,6 +357,7 @@ class History:
359
357
  # path が存在すれば dataframe を読み込む
360
358
  if os.path.isfile(self.path):
361
359
  self.actor_data = pd.read_csv(self.path, encoding='shift-jis')
360
+ self.is_restart = True
362
361
 
363
362
  @property
364
363
  def actor_data(self):
@@ -597,7 +596,7 @@ class OptimizationStatus:
597
596
  # self._actor.status_int = status_const
598
597
  # self._actor.status = self.const_to_str(status_const)
599
598
  self._actor.set(status_const, self.const_to_str(status_const)).result()
600
- print(f'---{self.const_to_str(status_const)}---')
599
+ logger.info(f'---{self.const_to_str(status_const)}---')
601
600
 
602
601
  def get(self):
603
602
  return self._actor.status_int
@@ -722,6 +721,7 @@ class OptunaOptimizer(AbstractOptimizer):
722
721
  self.study_name = None
723
722
  self.storage = None
724
723
  self.study = None
724
+ self.optimize_callbacks = []
725
725
  self.sampler_class = optuna.samplers.TPESampler if sampler_class is None else sampler_class
726
726
  self.sampler_kwargs = dict() if sampler_kwargs is None else sampler_kwargs
727
727
  self.additional_initial_parameter = []
@@ -759,7 +759,7 @@ class OptunaOptimizer(AbstractOptimizer):
759
759
  if cns.ub is not None:
760
760
  feasible = feasible and (cns.ub >= cns_value)
761
761
  if not feasible:
762
- print(f'以下の変数で拘束 {cns.name} が満たされませんでした。')
762
+ logger.info(f'以下の変数で拘束 {cns.name} が満たされませんでした。')
763
763
  print(self.get_parameter('dict'))
764
764
  raise optuna.TrialPruned() # set TrialState PRUNED because FAIL causes similar candidate loop.
765
765
 
@@ -767,8 +767,9 @@ class OptunaOptimizer(AbstractOptimizer):
767
767
  try:
768
768
  _, _y, c = self.f(x)
769
769
  except (ModelError, MeshError, SolveError) as e:
770
- print(str(type(e)).split("'")[1].split('.')[-1], e)
771
- print('FEM 解析に失敗しました.')
770
+ logger.info(str(type(e)).split("'")[1].split('.')[-1], e)
771
+ logger.info('以下の変数で FEM 解析に失敗しました。')
772
+ print(self.get_parameter('dict'))
772
773
 
773
774
  # 中断の確認 (解析中に interrupt されている場合対策)
774
775
  if self.status.get() == OptimizationStatus.INTERRUPTING:
@@ -807,8 +808,19 @@ class OptunaOptimizer(AbstractOptimizer):
807
808
  if self.is_cluster: # remote cluster なら scheduler の working dir に保存
808
809
  storage_path = os.path.basename(self.history.path).replace('.csv', '.db')
809
810
 
810
- # create study if storage is not exists
811
- if not os.path.exists(storage_path):
811
+ # callback to terminate
812
+ if self.n_trials is not None:
813
+ n_trials = self.n_trials
814
+
815
+ # restart である場合、追加 N 回と見做す
816
+ if self.history.is_restart:
817
+ n_existing_trials = len(self.history.actor_data)
818
+ n_trials += n_existing_trials
819
+
820
+ self.optimize_callbacks.append(MaxTrialsCallback(n_trials, states=(TrialState.COMPLETE,)))
821
+
822
+ # if not restart, create study if storage is not exists
823
+ if not self.history.is_restart:
812
824
 
813
825
  self.storage = optuna.integration.dask.DaskStorage(
814
826
  f'sqlite:///{storage_path}',
@@ -852,12 +864,17 @@ class OptunaOptimizer(AbstractOptimizer):
852
864
  d, user_attrs={"message": "additional initial (Latin Hypercube Sampling)"}
853
865
  )
854
866
 
867
+ # if is_restart, load study
855
868
  else:
856
-
869
+ if not os.path.exists(storage_path):
870
+ raise FileNotFoundError(f'{storage_path} が見つかりません。.db ファイルは .csv ファイルと同じフォルダに生成されます。クラスター解析の場合は、スケジューラを起動したフォルダに生成されます。')
857
871
  self.storage = optuna.integration.dask.DaskStorage(
858
872
  f'sqlite:///{storage_path}',
859
873
  )
860
874
 
875
+
876
+
877
+
861
878
  def add_init_parameter(
862
879
  self,
863
880
  parameter: dict or Iterable,
@@ -901,18 +918,11 @@ class OptunaOptimizer(AbstractOptimizer):
901
918
  sampler=sampler,
902
919
  )
903
920
 
904
- # callback to terminate
905
- callbacks = []
906
- n_existing_trials = len(self.history.actor_data)
907
- if self.n_trials is not None:
908
- n_trials = n_existing_trials + self.n_trials
909
- callbacks.append(MaxTrialsCallback(n_trials, states=(TrialState.COMPLETE,)))
910
-
911
921
  # run
912
922
  study.optimize(
913
923
  self._objective,
914
924
  timeout=self.timeout,
915
- callbacks=callbacks,
925
+ callbacks=self.optimize_callbacks,
916
926
  )
917
927
 
918
928
  # finalize
@@ -1224,13 +1234,13 @@ class FEMOpt:
1224
1234
 
1225
1235
  else:
1226
1236
  logger.info('Launching single machine cluster. This may take tens of seconds.')
1227
- cluster = LocalCluster(processes=True, threads_per_worker=1)
1237
+ cluster = LocalCluster(processes=True)
1228
1238
  self.client = Client(cluster, direct_to_workers=False)
1229
1239
  self.scheduler_address = self.client.scheduler.address
1230
1240
 
1231
1241
  # monitor 用 worker を起動
1232
1242
  logger.info('Launching monitor server. This may take a few seconds.')
1233
- self.monitor_process_worker_name = datetime.datetime.now().strftime("monitor-server-process-%Y%m%d%H%M%S")
1243
+ self.monitor_process_worker_name = datetime.datetime.now().strftime("Monitor-%Y%m%d-%H%M%S")
1234
1244
  cmd = f'dask worker {self.client.scheduler.address} --name {self.monitor_process_worker_name} --no-nanny'
1235
1245
  worker_addresses = list(self.client.has_what().keys())
1236
1246
  Popen(cmd, shell=True) #, stdout=PIPE) --> cause stream error
@@ -1331,9 +1341,8 @@ class FEMOpt:
1331
1341
  # 一応
1332
1342
  t_save_history.join()
1333
1343
 
1334
- print(f'Optimization finished. Elapsed time is {end - start} sec.')
1335
- print('計算が終了しました. ウィンドウを閉じると終了します.')
1336
- print(f'結果は{self.history.path}を確認してください.')
1344
+ logger.info(f'計算が終了しました. 実行時間は {int(end - start)} 秒でした。ウィンドウを閉じると終了します.')
1345
+ logger.info(f'結果は{self.history.path}を確認してください.')
1337
1346
 
1338
1347
  def terminate_all(self):
1339
1348
 
@@ -1342,13 +1351,13 @@ class FEMOpt:
1342
1351
 
1343
1352
  # terminate monitor process
1344
1353
  self.status.set(OptimizationStatus.TERMINATE_ALL)
1345
- print(self.monitor_process_future.result())
1354
+ logger.info(self.monitor_process_future.result())
1346
1355
  sleep(1)
1347
1356
 
1348
1357
  # terminate actors
1349
1358
  self.client.cancel(self.history._future, force=True)
1350
1359
  self.client.cancel(self.status._future, force=True)
1351
- print('Terminate actors.')
1360
+ logger.info('Terminate actors.')
1352
1361
  sleep(1)
1353
1362
 
1354
1363
  # terminate monitor worker
@@ -1360,19 +1369,23 @@ class FEMOpt:
1360
1369
  )
1361
1370
  while n_workers == len(self.client.nthreads()):
1362
1371
  sleep(1)
1363
- print('Terminate monitor processes worker.')
1372
+ logger.info('Terminate monitor processes worker.')
1364
1373
  sleep(1)
1365
1374
 
1366
1375
  # close scheduler, other workers(, cluster)
1367
1376
  self.client.close()
1368
1377
  while self.client.scheduler is not None:
1369
1378
  sleep(1)
1370
- print('Terminate client.')
1379
+ logger.info('Terminate client.')
1380
+
1381
+ # close FEM (if specified to quit when deconstruct)
1382
+ del self.fem
1383
+ logger.info('Terminate FEM.')
1371
1384
 
1372
1385
  # terminate dask relative processes.
1373
1386
  if not self.opt.is_cluster:
1374
1387
  self.client.shutdown()
1375
- print('Terminate all relative processes.')
1388
+ logger.info('Terminate all relative processes.')
1376
1389
  sleep(3)
1377
1390
 
1378
1391
 
@@ -1,9 +1,9 @@
1
1
  import os
2
2
  import sys
3
- from time import sleep
3
+ from time import sleep, time
4
4
  import json
5
5
  import subprocess
6
- import logging
6
+ import signal
7
7
 
8
8
  import pandas as pd
9
9
  import psutil
@@ -26,14 +26,11 @@ from ..dispatch_extensions import (
26
26
  DispatchExtensionException,
27
27
  )
28
28
 
29
-
30
- logger = logging.getLogger('fem_interface_log')
31
- handler = logging.StreamHandler()
32
- handler.setFormatter(logging.Formatter('[pid %(process)d] %(message)s'))
33
- logger.addHandler(handler)
29
+ import logging
30
+ from ..logger import get_logger
31
+ logger = get_logger('FEM')
34
32
  logger.setLevel(logging.INFO)
35
33
 
36
-
37
34
  here, me = os.path.split(__file__)
38
35
 
39
36
 
@@ -187,7 +184,11 @@ class FemtetInterface(FEMInterface):
187
184
  hwnd = self.Femtet.hWnd
188
185
  pid = _get_pid(hwnd)
189
186
  util.close_femtet(hwnd, 1, True)
187
+ start = time()
190
188
  while psutil.pid_exists(pid):
189
+ if time() - start > 30: # 30 秒経っても存在するのは何かおかしい
190
+ os.kill(pid, signal.SIGKILL)
191
+ break
191
192
  sleep(1)
192
193
  sleep(1)
193
194
 
@@ -299,6 +300,10 @@ class FemtetInterface(FEMInterface):
299
300
  -------
300
301
 
301
302
  """
303
+
304
+ # FIXME: Gaudi へのアクセスなど、self.Femtet.Gaudi.somefunc() のような場合、この関数を呼び出す前に Gaudi へのアクセスの時点で com_error が起こる
305
+ # FIXME: => 文字列で渡して eval() すればよい。
306
+
302
307
  if args is None:
303
308
  args = tuple()
304
309
  if kwargs is None:
@@ -309,7 +314,7 @@ class FemtetInterface(FEMInterface):
309
314
  # 2. API 実行時に成功失敗を示す戻り値を返し、ShowLastError で例外にアクセスできる状態になる
310
315
 
311
316
  # Gaudi コマンドなら Gaudi.Activate する
312
- # if self.pp: print(' '*print_indent, 'コマンド', fun.__name__, args, kwargs)
317
+ logger.debug(' '*print_indent + f'Femtet API:{fun.__name__}, args:{args}, kwargs:{kwargs}')
313
318
  if is_Gaudi_method: # Optimizer は Gogh に触らないので全部にこれをつけてもいい気がする
314
319
  try:
315
320
  self._call_femtet_api(
@@ -333,7 +338,7 @@ class FemtetInterface(FEMInterface):
333
338
  returns = ret_if_failed
334
339
  else:
335
340
  returns = [ret_if_failed]*(ret_for_check_idx+1)
336
- # if self.pp: print(' '*print_indent, 'コマンド結果', returns)
341
+ logger.debug(' '*print_indent + f'Femtet API result:{returns}')
337
342
 
338
343
  # チェックすべき値の抽出
339
344
  if ret_for_check_idx is None:
@@ -352,29 +357,28 @@ class FemtetInterface(FEMInterface):
352
357
  if self.femtet_is_alive():
353
358
  # 生きていてもここにきているなら
354
359
  # 指定された Exception を送出する
355
- # if self.pp: print(' '*print_indent, error_message)
360
+ logger.debug(' ' * print_indent + error_message)
356
361
  raise if_error(error_message)
357
362
 
358
363
  # 死んでいるなら再起動
359
364
  else:
360
365
  # 再起動試行回数の上限に達していたら諦める
361
- # if self.pp: print(' '*print_indent, '現在の Femtet 再起動回数:', recourse_depth)
366
+ logger.debug(' ' * print_indent + f'現在の Femtet 再起動回数: {recourse_depth}')
362
367
  if recourse_depth >= self.max_api_retry:
363
368
  raise Exception('Femtet のプロセスが異常終了し、正常に再起動できませんでした.')
364
369
 
365
370
  # 再起動
366
- # if self.pp: print(' '*print_indent, 'Femtet プロセスの異常終了が検知されました. 再起動を試みます.')
367
- print('Femtet プロセスの異常終了が検知されました. 回復を試みます.')
371
+ logger.warn('Femtet プロセスの異常終了が検知されました. 回復を試みます.')
372
+ CoInitialize()
368
373
  self.connect_femtet(connect_method='new')
369
374
  self.open(self.femprj_path, self.model_name)
370
375
 
371
376
  # 状態を復元するために一度変数を渡して解析を行う(fun.__name__がSolveなら2度手間だが)
372
- # if self.pp: print(' '*print_indent, f'再起動されました. コマンド{fun.__name__}の再試行のため、解析を行います.')
377
+ logger.info(' ' * print_indent + f'Femtet が再起動されました。解析を行い、状態回復を試みます。')
373
378
  self.update(self.parameters)
374
379
 
375
380
  # 与えられた API の再帰的再試行
376
- # if self.pp: print(' '*print_indent, f'回復されました. コマンド{fun.__name__}の再試行を行います.')
377
- print('回復されました.')
381
+ logger.info(' ' * print_indent + f'Femtet が回復されました。コマンド {fun.__name__} を再試行します。')
378
382
  return self._call_femtet_api(
379
383
  fun,
380
384
  ret_if_failed,
@@ -507,7 +511,7 @@ class FemtetInterface(FEMInterface):
507
511
  args=(name, value),
508
512
  )
509
513
  else:
510
- print(f'変数 {name} は .femprj に含まれていません。無視されます。')
514
+ logger.warn(f'変数 {name} は .femprj に含まれていません。無視されます。')
511
515
 
512
516
  # ここでは ReExecute しない
513
517
  pass
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyfemtet"
3
- version = "0.3.1" # ignored by versioning plugin
3
+ version = "0.3.3" # 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"
@@ -24,6 +24,7 @@ dash-bootstrap-components = "^1.5.0"
24
24
  dask = "^2023.12.1"
25
25
  distributed = "^2023.12.1"
26
26
  botorch = { version = "^0.9.5", python = "<3.12" }
27
+ colorlog = "^6.8.0"
27
28
 
28
29
  [tool.poetry.group.dev.dependencies]
29
30
  pip-licenses = "^4.3.3"
@@ -1 +0,0 @@
1
- __version__ = "0.3.1"
@@ -1,241 +0,0 @@
1
- # import os
2
- # import gc
3
- # import warnings
4
- #
5
- # import numpy as np
6
- # from scipy.stats.qmc import LatinHypercube
7
- # import optuna
8
- # from optuna.study import MaxTrialsCallback
9
- # from optuna.trial import TrialState
10
- # from optuna.exceptions import ExperimentalWarning
11
- # # optuna.logging.disable_default_handler()
12
- #
13
- # from .core import UserInterruption, ModelError, MeshError, SolveError
14
- # from .base import OptimizerBase
15
- #
16
- #
17
- # warnings.filterwarnings('ignore', category=ExperimentalWarning)
18
- #
19
- #
20
- # def generate_lhs(bounds: list[list[float]], seed: int or None = None) -> np.ndarray:
21
- # """Latin Hypercube Sampling from given design parameter bounds.
22
- #
23
- # If the number of parameters is d,
24
- # sampler returns (N, d) shape ndarray.
25
- # N equals p**2, p is the minimum prime number over d.
26
- # For example, when d=3, then p=5 and N=25.
27
- #
28
- # Args:
29
- # bounds (list[list[float]]): List of [lower_bound, upper_bound] of parameters.
30
- # seed (int or None, optional): Random seed. Defaults to None.
31
- #
32
- # Returns:
33
- # np.ndarray: (N, d) shape ndarray.
34
- # """
35
- #
36
- # d = len(bounds)
37
- #
38
- # sampler = LatinHypercube(
39
- # d,
40
- # scramble=False,
41
- # strength=2,
42
- # # optimization='lloyd',
43
- # optimization='random-cd',
44
- # seed=seed,
45
- # )
46
- #
47
- # LIMIT = 100
48
- #
49
- # def is_prime(p):
50
- # for i in range(2, p):
51
- # if p % i == 0:
52
- # return False
53
- # return True
54
- #
55
- # def get_prime(minimum):
56
- # for p in range(minimum, LIMIT):
57
- # if is_prime(p):
58
- # return p
59
- #
60
- # n = get_prime(d + 1) ** 2
61
- # data = sampler.random(n) # [0,1)
62
- #
63
- # for i, (data_range, datum) in enumerate(zip(bounds, data.T)):
64
- # minimum, maximum = data_range
65
- # band = maximum - minimum
66
- # converted_datum = datum * band + minimum
67
- # data[:, i] = converted_datum
68
- #
69
- # return data # data.shape = (N, d)
70
- #
71
- #
72
- # class OptimizerOptuna(OptimizerBase):
73
- # """Optimizer class using Optuna.
74
- #
75
- # """
76
- #
77
- # def _objective(self, trial):
78
- #
79
- # # 中断指令の処理
80
- # if self.ipv.get_state() == 'interrupting':
81
- # raise UserInterruption
82
- #
83
- # # message の設定
84
- # try:
85
- # message = trial.user_attrs["message"] # あれば
86
- # except (AttributeError, KeyError):
87
- # message = '' # なければ
88
- #
89
- # # x の生成
90
- # x = []
91
- # for i, row in self.parameters.iterrows():
92
- # x.append(trial.suggest_float(row['name'], row['lb'], row['ub']))
93
- # x = np.array(x)
94
- # self.parameters['value'] = x
95
- #
96
- # # strict 拘束の計算で Prune することになったとき
97
- # # constraint attr がないとエラーになるのでダミーを置いておく
98
- # trial.set_user_attr("constraint", (1.,)) # 非正が feasible 扱い
99
- #
100
- # # GetVariableValue 経由で変数にアクセスするなどの場合
101
- # self.fem.update_parameter(self.parameters)
102
- #
103
- # # strict 拘束の計算
104
- # tmp = [[cns.calc(self.fem), cns.lb, cns.ub, name] for name, cns in self.constraints.items() if cns.strict]
105
- # for val, lb, ub, name in tmp:
106
- # if lb is not None:
107
- # if not (lb <= val):
108
- # print(f'拘束 {name} が満たされませんでした。')
109
- # print('変数の組み合わせは以下の通りです。')
110
- # print(self.parameters)
111
- # raise optuna.TrialPruned()
112
- # if ub is not None:
113
- # if not (val <= ub):
114
- # print(f'拘束 {name} が満たされませんでした。')
115
- # print('変数の組み合わせは以下の通りです。')
116
- # print(self.parameters)
117
- # raise optuna.TrialPruned()
118
- #
119
- # # 解析実行
120
- # try:
121
- # obj_values = self.f(x, message) # obj_val と cns_val の更新
122
- # except (ModelError, MeshError, SolveError):
123
- # print('FEM 解析に失敗しました。')
124
- # print('変数の組み合わせは以下の通りです。')
125
- # print(self.parameters)
126
- # raise optuna.TrialPruned()
127
- #
128
- # # 拘束 attr の更新
129
- # _cns_values = [] # 非正なら OK
130
- # for (name, cns), cns_val in zip(self.constraints.items(), self.cns_values):
131
- # lb, ub = cns.lb, cns.ub
132
- # if lb is not None: # fun >= lb <=> lb - fun <= 0
133
- # _cns_values.append(lb - cns_val)
134
- # if ub is not None: # ub >= fun <=> fun - ub <= 0
135
- # _cns_values.append(cns_val - ub)
136
- # trial.set_user_attr("constraint", _cns_values)
137
- #
138
- # # 一応
139
- # gc.collect()
140
- #
141
- # return tuple(obj_values)
142
- #
143
- # def _constraint_function(self, trial):
144
- # return trial.user_attrs["constraint"]
145
- #
146
- # def setup_concrete_main(self, use_lhs_init=True):
147
- # """Performs the setup for the optimization using Optuna.
148
- #
149
- # Do sampler settings, study creation or loading and initial trials settings.
150
- #
151
- # Args:
152
- # use_lhs_init (bool, optional): Flag indicating whether to use Latin Hypercube Sampling for initializing trials. Defaults to True.
153
- #
154
- # """
155
- #
156
- # # sampler の設定
157
- # self.sampler_kwargs = dict(
158
- # # n_startup_trials=5,
159
- # constraints_func=self._constraint_function
160
- # )
161
- # self.sampler_class = optuna.samplers.TPESampler
162
- # if self.method == 'botorch':
163
- # self.sampler_class = optuna.integration.BoTorchSampler
164
- # # self.sampler_kwargs.update(
165
- # # dict(
166
- # # consider_running_trials=True
167
- # # )
168
- # # )
169
- #
170
- # # study name の設定
171
- # self.study_name = os.path.splitext(os.path.basename(self.history.path))[0]
172
- # self.storage_path = os.path.splitext(self.history.path)[0] + '.db'
173
- # self.storage = f"sqlite:///{self.storage_path}"
174
- #
175
- # # storage の設定
176
- # study = optuna.create_study(
177
- # study_name=self.study_name,
178
- # storage=self.storage,
179
- # load_if_exists=True,
180
- # directions=['minimize']*len(self.objectives),
181
- # )
182
- #
183
- # # 初期値の設定
184
- # if len(study.trials) == 0: # リスタートでなければ
185
- # # ユーザーの指定した初期値
186
- # params = self.get_parameter('dict')
187
- # study.enqueue_trial(params, user_attrs={"message": "initial"})
188
- #
189
- # # LHS を初期値にする
190
- # if use_lhs_init:
191
- # names = []
192
- # bounds = []
193
- # for i, row in self.parameters.iterrows():
194
- # names.append(row['name'])
195
- # lb = row['lb']
196
- # ub = row['ub']
197
- # bounds.append([lb, ub])
198
- # data = generate_lhs(bounds, seed=self.seed)
199
- # for datum in data:
200
- # d = {}
201
- # for name, v in zip(names, datum):
202
- # d[name] = v
203
- # study.enqueue_trial(d, user_attrs={"message": "initial Latin Hypercube Sampling"})
204
- #
205
- # def concrete_main(self, subprocess_idx=None):
206
- # """Optimization using Optuna."""
207
- #
208
- # # 乱数シードをプロセス固有にする
209
- # seed = self.seed
210
- # if seed is not None:
211
- # if subprocess_idx is not None:
212
- # seed = seed + (1 + subprocess_idx) # main process と subprocess0 が重複する
213
- #
214
- # # sampler の restore
215
- # sampler = self.sampler_class(
216
- # seed=seed,
217
- # **self.sampler_kwargs
218
- # )
219
- #
220
- # # storage の restore
221
- # study = optuna.load_study(
222
- # study_name=self.study_name,
223
- # storage=self.storage,
224
- # sampler=sampler,
225
- # )
226
- #
227
- # # 最大実行回数の指定
228
- # callbacks = []
229
- # n_existing_trials = len(self.history.actor_data)
230
- # if self.n_trials is not None:
231
- # n_trials = n_existing_trials + self.n_trials
232
- # callbacks.append(MaxTrialsCallback(n_trials, states=(TrialState.COMPLETE,)))
233
- #
234
- # # run
235
- # study.optimize(
236
- # self._objective,
237
- # timeout=self.timeout,
238
- # callbacks=callbacks,
239
- # )
240
- #
241
- # return study
File without changes
File without changes
File without changes