pyfemtet 0.3.12__py3-none-any.whl → 0.4.2__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.

Files changed (35) hide show
  1. pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.py +61 -32
  2. pyfemtet/FemtetPJTSample/Sldworks_ex01/Sldworks_ex01.py +62 -40
  3. pyfemtet/FemtetPJTSample/gau_ex08_parametric.py +26 -23
  4. pyfemtet/FemtetPJTSample/her_ex40_parametric.femprj +0 -0
  5. pyfemtet/FemtetPJTSample/her_ex40_parametric.py +58 -46
  6. pyfemtet/FemtetPJTSample/wat_ex14_parallel_parametric.py +31 -29
  7. pyfemtet/FemtetPJTSample/wat_ex14_parametric.femprj +0 -0
  8. pyfemtet/FemtetPJTSample/wat_ex14_parametric.py +30 -28
  9. pyfemtet/__init__.py +1 -1
  10. pyfemtet/core.py +14 -0
  11. pyfemtet/dispatch_extensions.py +5 -0
  12. pyfemtet/opt/__init__.py +22 -2
  13. pyfemtet/opt/_femopt.py +544 -0
  14. pyfemtet/opt/_femopt_core.py +732 -0
  15. pyfemtet/opt/interface/__init__.py +15 -0
  16. pyfemtet/opt/interface/_base.py +71 -0
  17. pyfemtet/opt/{interface.py → interface/_femtet.py} +121 -408
  18. pyfemtet/opt/interface/_femtet_with_nx/__init__.py +3 -0
  19. pyfemtet/opt/interface/_femtet_with_nx/_interface.py +128 -0
  20. pyfemtet/opt/interface/_femtet_with_sldworks.py +174 -0
  21. pyfemtet/opt/opt/__init__.py +8 -0
  22. pyfemtet/opt/opt/_base.py +202 -0
  23. pyfemtet/opt/opt/_optuna.py +246 -0
  24. pyfemtet/opt/visualization/__init__.py +7 -0
  25. pyfemtet/opt/visualization/_graphs.py +222 -0
  26. pyfemtet/opt/visualization/_monitor.py +1149 -0
  27. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.2.dist-info}/METADATA +6 -5
  28. pyfemtet-0.4.2.dist-info/RECORD +38 -0
  29. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.2.dist-info}/WHEEL +1 -1
  30. pyfemtet-0.4.2.dist-info/entry_points.txt +3 -0
  31. pyfemtet/opt/base.py +0 -1490
  32. pyfemtet/opt/monitor.py +0 -474
  33. pyfemtet-0.3.12.dist-info/RECORD +0 -26
  34. /pyfemtet/opt/{_FemtetWithNX → interface/_femtet_with_nx}/update_model.py +0 -0
  35. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.2.dist-info}/LICENSE +0 -0
@@ -0,0 +1,544 @@
1
+ # built-in
2
+ import os
3
+ import datetime
4
+ from time import time, sleep
5
+ from threading import Thread
6
+ import json
7
+
8
+ # 3rd-party
9
+ import numpy as np
10
+ import pandas as pd
11
+ from dask.distributed import LocalCluster, Client
12
+
13
+ # pyfemtet relative
14
+ from pyfemtet.opt.interface import FEMInterface, FemtetInterface
15
+ from pyfemtet.opt.opt import AbstractOptimizer, OptunaOptimizer
16
+ from pyfemtet.opt.visualization._monitor import ProcessMonitorApp
17
+ from pyfemtet.opt._femopt_core import (
18
+ _check_bound,
19
+ _is_access_gogh,
20
+ Objective,
21
+ Constraint,
22
+ History,
23
+ OptimizationStatus,
24
+ logger,
25
+ )
26
+
27
+
28
+ class FEMOpt:
29
+ """Class to control FEM interface and optimizer.
30
+
31
+ Args:
32
+ fem (FEMInterface, optional): The finite element method interface. Defaults to None. If None, automatically set to FemtetInterface.
33
+ opt (AbstractOptimizer):
34
+ 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.
35
+ scheduler_address (str or None): If cluster processing, set this parameter like "tcp://xxx.xxx.xxx.xxx:xxxx".
36
+
37
+ Attributes:
38
+ fem (FEMInterface): The interface of FEM system.
39
+ opt (AbstractOptimizer): The optimizer.
40
+ scheduler_address (str or None): Dask scheduler address. If None, LocalCluster will be used.
41
+ client (Client): Dask client. For detail, see dask documentation.
42
+ status (OptimizationStatus): Entire process status. This contains dask actor.
43
+ history(History): History of optimization process. This contains dask actor.
44
+ history_path (str): The path to the history (.csv) file.
45
+ worker_status_list([OptimizationStatus]): Process status of each dask worker.
46
+ monitor_process_future(Future): Future of monitor server process. This is dask future.
47
+ monitor_server_kwargs(dict): Monitor server parameter. Currently, the valid arguments are hostname and port.
48
+
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ fem: FEMInterface = None,
54
+ opt: AbstractOptimizer = None,
55
+ history_path: str = None,
56
+ scheduler_address: str = None
57
+ ):
58
+ logger.info('Initialize FEMOpt')
59
+
60
+ # 引数の処理
61
+ if history_path is None:
62
+ history_path = datetime.datetime.now().strftime('%Y%m%d_%H%M%S.csv')
63
+ self.history_path = os.path.abspath(history_path)
64
+ self.scheduler_address = scheduler_address
65
+
66
+ if fem is None:
67
+ self.fem = FemtetInterface()
68
+ else:
69
+ self.fem = fem
70
+
71
+ if opt is None:
72
+ self.opt = OptunaOptimizer()
73
+ else:
74
+ self.opt = opt
75
+
76
+ # メンバーの宣言
77
+ self.client = None
78
+ self.status = None # actor
79
+ self.history = None # actor
80
+ self.worker_status_list = None # [actor]
81
+ self.monitor_process_future = None
82
+ self.monitor_server_kwargs = dict()
83
+ self.monitor_process_worker_name = None
84
+
85
+ # multiprocess 時に pickle できないオブジェクト参照の削除
86
+ def __getstate__(self):
87
+ state = self.__dict__.copy()
88
+ del state['fem']
89
+ return state
90
+
91
+ def __setstate__(self, state):
92
+ self.__dict__.update(state)
93
+
94
+ def set_random_seed(self, seed: int):
95
+ """Sets the random seed for reproducibility.
96
+
97
+ Args:
98
+ seed (int): The random seed value to be set.
99
+
100
+ """
101
+ self.opt.seed = seed
102
+
103
+ def add_parameter(
104
+ self,
105
+ name: str,
106
+ initial_value: float or None = None,
107
+ lower_bound: float or None = None,
108
+ upper_bound: float or None = None,
109
+ memo: str = ''
110
+ ):
111
+ """Adds a parameter to the optimization problem.
112
+
113
+ Args:
114
+ name (str): The name of the parameter.
115
+ initial_value (float or None, optional): The initial value of the parameter. Defaults to None. If None, try to get initial value from FEMInterface.
116
+ lower_bound (float or None, optional): The lower bound of the parameter. Defaults to None. However, this argument is required for some algorithms.
117
+ upper_bound (float or None, optional): The upper bound of the parameter. Defaults to None. However, this argument is required for some algorithms.
118
+ memo (str, optional): Additional information about the parameter. Defaults to ''.
119
+ Raises:
120
+ ValueError: If initial_value is not specified and the value for the given name is also not specified.
121
+
122
+ """
123
+
124
+ _check_bound(lower_bound, upper_bound, name)
125
+ value = self.fem.check_param_value(name)
126
+ if initial_value is None:
127
+ if value is not None:
128
+ initial_value = value
129
+ else:
130
+ raise ValueError('initial_value を指定してください.')
131
+
132
+ d = {
133
+ 'name': name,
134
+ 'value': float(initial_value),
135
+ 'lb': float(lower_bound),
136
+ 'ub': float(upper_bound),
137
+ 'memo': memo,
138
+ }
139
+ pdf = pd.DataFrame(d, index=[0], dtype=object)
140
+
141
+ if len(self.opt.parameters) == 0:
142
+ self.opt.parameters = pdf
143
+ else:
144
+ self.opt.parameters = pd.concat([self.opt.parameters, pdf], ignore_index=True)
145
+
146
+ def add_objective(
147
+ self,
148
+ fun,
149
+ name: str or None = None,
150
+ direction: str or float = 'minimize',
151
+ args: tuple or None = None,
152
+ kwargs: dict or None = None
153
+ ):
154
+ """Adds an objective to the optimization problem.
155
+
156
+ Args:
157
+ fun (callable): The objective function.
158
+ name (str or None, optional): The name of the objective. Defaults to None.
159
+ direction (str or float, optional): The optimization direction. Defaults to 'minimize'.
160
+ args (tuple or None, optional): Additional arguments for the objective function. Defaults to None.
161
+ kwargs (dict or None, optional): Additional keyword arguments for the objective function. Defaults to None.
162
+
163
+ Note:
164
+ If the FEMInterface is FemtetInterface, the 1st argument of fun should be Femtet (IPyDispatch) object.
165
+
166
+ Tip:
167
+ If name is None, name is a string with the prefix `"obj_"` followed by a sequential number.
168
+
169
+ """
170
+
171
+ # 引数の処理
172
+ if args is None:
173
+ args = tuple()
174
+ elif not isinstance(args, tuple):
175
+ args = (args,)
176
+ if kwargs is None:
177
+ kwargs = dict()
178
+ if name is None:
179
+ prefix = Objective.default_name
180
+ i = 0
181
+ while True:
182
+ candidate = f'{prefix}_{str(int(i))}'
183
+ is_existing = candidate in list(self.opt.objectives.keys())
184
+ if not is_existing:
185
+ break
186
+ else:
187
+ i += 1
188
+ name = candidate
189
+
190
+ self.opt.objectives[name] = Objective(fun, name, direction, args, kwargs)
191
+
192
+ def add_constraint(
193
+ self,
194
+ fun,
195
+ name: str or None = None,
196
+ lower_bound: float or None = None,
197
+ upper_bound: float or None = None,
198
+ strict: bool = True,
199
+ args: tuple or None = None,
200
+ kwargs: dict or None = None,
201
+ ):
202
+ """Adds a constraint to the optimization problem.
203
+
204
+ Args:
205
+ fun (callable): The constraint function.
206
+ name (str or None, optional): The name of the constraint. Defaults to None.
207
+ lower_bound (float or Non, optional): The lower bound of the constraint. Defaults to None.
208
+ upper_bound (float or Non, optional): The upper bound of the constraint. Defaults to None.
209
+ strict (bool, optional): Flag indicating if it is a strict constraint. Defaults to True.
210
+ args (tuple or None, optional): Additional arguments for the constraint function. Defaults to None.
211
+ kwargs (dict): Additional arguments for the constraint function. Defaults to None.
212
+
213
+ Note:
214
+ If the FEMInterface is FemtetInterface, the 1st argument of fun should be Femtet (IPyDispatch) object.
215
+
216
+ Tip:
217
+ If name is None, name is a string with the prefix `"cns_"` followed by a sequential number.
218
+
219
+ """
220
+
221
+ # 引数の処理
222
+ if args is None:
223
+ args = tuple()
224
+ elif not isinstance(args, tuple):
225
+ args = (args,)
226
+ if kwargs is None:
227
+ kwargs = dict()
228
+ if name is None:
229
+ prefix = Constraint.default_name
230
+ i = 0
231
+ while True:
232
+ candidate = f'{prefix}_{str(int(i))}'
233
+ is_existing = candidate in list(self.opt.constraints.keys())
234
+ if not is_existing:
235
+ break
236
+ else:
237
+ i += 1
238
+ name = candidate
239
+
240
+ # strict constraint の場合、solve 前に評価したいので Gogh へのアクセスを禁ずる
241
+ if strict:
242
+ if _is_access_gogh(fun):
243
+ message = f'関数 {fun.__name__} に Gogh (Femtet 解析結果)へのアクセスがあります.'
244
+ message += 'デフォルトでは constraint は解析前に評価され, 条件を満たさない場合解析を行いません.'
245
+ message += '拘束に解析結果を含めたい場合は, strict=False を設定してください.'
246
+ raise Exception(message)
247
+
248
+ self.opt.constraints[name] = Constraint(fun, name, lower_bound, upper_bound, strict, args, kwargs)
249
+
250
+ def get_parameter(self, format='dict') -> pd.DataFrame or dict or np.ndarray:
251
+ """Returns the parameter in a specified format.
252
+
253
+ Args:
254
+ format (str, optional): The desired output format. Defaults to 'dict'. Valid formats are 'values', 'df' and 'dict'.
255
+
256
+ Returns:
257
+ pd.DataFrame or dict or np.ndarray: The parameter data converted into the specified format.
258
+
259
+ Raises:
260
+ ValueError: If an invalid format is provided.
261
+
262
+ """
263
+ return self.opt.get_parameter(format)
264
+
265
+ def set_monitor_host(self, host=None, port=None):
266
+ """Sets up the monitor server with the specified host and port.
267
+
268
+ Args:
269
+ host (str): The hostname or IP address of the monitor server.
270
+ port (int or None, optional): The port number of the monitor server. If None, ``8080`` will be used. Defaults to None.
271
+
272
+ Tip:
273
+ Specifying host ``0.0.0.0`` allows viewing monitor from all computers on the local network.
274
+
275
+ However, please note that in this case,
276
+ it will be visible to all users on the local network.
277
+
278
+ If no hostname is specified, the monitor server will be hosted on ``localhost``.
279
+
280
+ """
281
+ self.monitor_server_kwargs = dict(
282
+ host=host,
283
+ port=port
284
+ )
285
+
286
+ def optimize(
287
+ self,
288
+ n_trials=None,
289
+ n_parallel=1,
290
+ timeout=None,
291
+ wait_setup=True,
292
+ ):
293
+ """Runs the main optimization process.
294
+
295
+ Args:
296
+ n_trials (int or None, optional): The number of trials. Defaults to None.
297
+ n_parallel (int, optional): The number of parallel processes. Defaults to 1.
298
+ timeout (float or None, optional): The maximum amount of time in seconds that each trial can run. Defaults to None.
299
+ wait_setup (bool, optional): Wait for all workers launching FEM system. Defaults to True.
300
+
301
+ Tip:
302
+ If set_monitor_host() is not executed, a local server for monitoring will be started at localhost:8080.
303
+
304
+ Note:
305
+ If ``n_trials`` and ``timeout`` are both None, it runs forever until interrupting by the user.
306
+
307
+ Note:
308
+ If ``n_parallel`` >= 2, depending on the end timing, ``n_trials`` may be exceeded by up to ``n_parallel-1`` times.
309
+
310
+ Warning:
311
+ If ``n_parallel`` >= 2 and ``fem`` is a subclass of ``FemtetInterface``, the ``strictly_pid_specify`` of subprocess is set to ``False``.
312
+ So **it is recommended to close all other Femtet processes before running.**
313
+
314
+ """
315
+
316
+ # 共通引数
317
+ self.opt.n_trials = n_trials
318
+ self.opt.timeout = timeout
319
+
320
+ # クラスターの設定
321
+ self.opt.is_cluster = self.scheduler_address is not None
322
+ if self.opt.is_cluster:
323
+ # 既存のクラスターに接続
324
+ logger.info('Connecting to existing cluster.')
325
+ self.client = Client(self.scheduler_address)
326
+
327
+ # 最適化タスクを振り分ける worker を指定
328
+ subprocess_indices = list(range(n_parallel))
329
+ worker_addresses = list(self.client.nthreads().keys())
330
+
331
+ # monitor worker の設定
332
+ logger.info('Launching monitor server. This may take a few seconds.')
333
+ self.monitor_process_worker_name = datetime.datetime.now().strftime("Monitor-%Y%m%d-%H%M%S")
334
+ current_n_workers = len(self.client.nthreads().keys())
335
+ from dask.distributed import Worker
336
+ Worker(scheduler_ip=self.client.scheduler.address, nthreads=1, name=self.monitor_process_worker_name)
337
+
338
+ # monitor 用 worker が増えるまで待つ
339
+ self.client.wait_for_workers(n_workers=current_n_workers + 1)
340
+
341
+ else:
342
+ # ローカルクラスターを構築
343
+ logger.info('Launching single machine cluster. This may take tens of seconds.')
344
+ cluster = LocalCluster(processes=True, n_workers=n_parallel,
345
+ threads_per_worker=1) # n_parallel = n_parallel - 1 + 1; main 分減らし、monitor 分増やす
346
+ self.client = Client(cluster, direct_to_workers=False)
347
+ self.scheduler_address = self.client.scheduler.address
348
+
349
+ # 最適化タスクを振り分ける worker を指定
350
+ subprocess_indices = list(range(n_parallel))[1:]
351
+ worker_addresses = list(self.client.nthreads().keys())
352
+
353
+ # monitor worker の設定
354
+ self.monitor_process_worker_name = worker_addresses[0]
355
+ worker_addresses[0] = 'Main'
356
+
357
+ # Femtet 特有の処理
358
+ metadata = None
359
+ if isinstance(self.fem, FemtetInterface):
360
+ metadata = json.dumps(
361
+ dict(
362
+ femprj_path=self.fem.original_femprj_path,
363
+ model_name=self.fem.model_name
364
+ )
365
+ )
366
+
367
+ # actor の設定
368
+ self.status = OptimizationStatus(self.client)
369
+ self.worker_status_list = [OptimizationStatus(self.client, name) for name in worker_addresses] # tqdm 検討
370
+ self.status.set(OptimizationStatus.SETTING_UP)
371
+ self.history = History(
372
+ self.history_path,
373
+ self.opt.parameters['name'].to_list(),
374
+ list(self.opt.objectives.keys()),
375
+ list(self.opt.constraints.keys()),
376
+ self.client,
377
+ metadata,
378
+ )
379
+
380
+ # launch monitor
381
+ self.monitor_process_future = self.client.submit(
382
+ # func
383
+ _start_monitor_server,
384
+ # args
385
+ self.history,
386
+ self.status,
387
+ worker_addresses,
388
+ self.worker_status_list,
389
+ # kwargs
390
+ **self.monitor_server_kwargs,
391
+ # kwargs of submit
392
+ workers=self.monitor_process_worker_name, # if invalid arg,
393
+ allow_other_workers=False
394
+ )
395
+
396
+ # fem
397
+ self.fem._setup_before_parallel(self.client)
398
+
399
+ # opt
400
+ self.opt.fem_class = type(self.fem)
401
+ self.opt.fem_kwargs = self.fem.kwargs
402
+ self.opt.entire_status = self.status
403
+ self.opt.history = self.history
404
+ self.opt._setup_before_parallel()
405
+
406
+ # クラスターでの計算開始
407
+ self.status.set(OptimizationStatus.LAUNCHING_FEM)
408
+ start = time()
409
+ calc_futures = self.client.map(
410
+ self.opt._run,
411
+ subprocess_indices,
412
+ [self.worker_status_list] * len(subprocess_indices),
413
+ [wait_setup] * len(subprocess_indices),
414
+ workers=worker_addresses,
415
+ allow_other_workers=False,
416
+ )
417
+
418
+ t_main = None
419
+ if not self.opt.is_cluster:
420
+ # ローカルプロセスでの計算(opt._main 相当の処理)
421
+ subprocess_idx = 0
422
+
423
+ # set_fem
424
+ self.opt.fem = self.fem
425
+ self.opt._reconstruct_fem(skip_reconstruct=True)
426
+
427
+ t_main = Thread(
428
+ target=self.opt._run,
429
+ args=(
430
+ subprocess_idx,
431
+ self.worker_status_list,
432
+ wait_setup,
433
+ ),
434
+ kwargs=dict(
435
+ skip_set_fem=True,
436
+ )
437
+ )
438
+ t_main.start()
439
+
440
+ # save history
441
+ def save_history():
442
+ while True:
443
+ sleep(2)
444
+ try:
445
+ self.history.save()
446
+ except PermissionError:
447
+ logger.warning(f'{self.history.path} が使用中のため書き込みできません。プログラム終了までにこのファイルを解放してください。履歴データが失われます。')
448
+ if self.status.get() >= OptimizationStatus.TERMINATED:
449
+ break
450
+
451
+ t_save_history = Thread(target=save_history)
452
+ t_save_history.start()
453
+
454
+ # 終了を待つ
455
+ self.client.gather(calc_futures)
456
+ if not self.opt.is_cluster: # 既存の fem を使っているならそれも待つ
457
+ if t_main is not None:
458
+ t_main.join()
459
+ self.status.set(OptimizationStatus.TERMINATED)
460
+ end = time()
461
+
462
+ # 一応
463
+ t_save_history.join()
464
+
465
+ logger.info(f'計算が終了しました. 実行時間は {int(end - start)} 秒でした。ウィンドウを閉じると終了します.')
466
+ logger.info(f'結果は{self.history.path}を確認してください.')
467
+
468
+ def terminate_all(self):
469
+ """Try to terminate all launched processes.
470
+
471
+ If distributed computing, Scheduler and Workers will NOT be terminated.
472
+
473
+ """
474
+
475
+ # monitor が terminated 状態で少なくとも一度更新されなければ running のまま固まる
476
+ sleep(1)
477
+
478
+ # terminate monitor process
479
+ self.status.set(OptimizationStatus.TERMINATE_ALL)
480
+ logger.info(self.monitor_process_future.result())
481
+ sleep(1)
482
+
483
+ # terminate actors
484
+ self.client.cancel(self.history._future, force=True)
485
+ self.client.cancel(self.status._future, force=True)
486
+ for worker_status in self.worker_status_list:
487
+ self.client.cancel(worker_status._future, force=True)
488
+ logger.info('Terminate actors.')
489
+ sleep(1)
490
+
491
+ # これがないと dash app が落ちないとか問題あるの?
492
+
493
+ # terminate monitor worker
494
+ n_workers = len(self.client.nthreads())
495
+
496
+ found_worker_dict = self.client.retire_workers(
497
+ names=[self.monitor_process_worker_name], # name
498
+ close_workers=True,
499
+ remove=True,
500
+ )
501
+
502
+ if len(found_worker_dict) == 0:
503
+ found_worker_dict = self.client.retire_workers(
504
+ workers=[self.monitor_process_worker_name], # address
505
+ close_workers=True,
506
+ remove=True,
507
+ )
508
+
509
+ if len(found_worker_dict) > 0:
510
+ while n_workers == len(self.client.nthreads()):
511
+ sleep(1)
512
+ logger.info('Terminate monitor processes worker.')
513
+ sleep(1)
514
+ else:
515
+ logger.warn('Monitor process worker not found.')
516
+
517
+ # close scheduler, other workers(, cluster)
518
+ self.client.close()
519
+ while self.client.scheduler is not None:
520
+ sleep(1)
521
+ logger.info('Terminate client.')
522
+
523
+ # close FEM (if specified to quit when deconstruct)
524
+ del self.fem
525
+ logger.info('Terminate FEM.')
526
+
527
+ # terminate dask relative processes.
528
+ if not self.opt.is_cluster:
529
+ self.client.shutdown()
530
+ logger.info('Terminate all relative processes.')
531
+ sleep(3)
532
+
533
+
534
+ def _start_monitor_server(
535
+ history,
536
+ status,
537
+ worker_addresses,
538
+ worker_status_list,
539
+ host=None,
540
+ port=None,
541
+ ):
542
+ monitor = ProcessMonitorApp(history, status, worker_addresses, worker_status_list)
543
+ monitor.start_server(host, port)
544
+ return 'Exit monitor server process gracefully'