ansys-mechanical-core 0.10.10__py3-none-any.whl → 0.11.12__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.
- ansys/mechanical/core/__init__.py +11 -4
- ansys/mechanical/core/_version.py +48 -47
- ansys/mechanical/core/embedding/__init__.py +1 -1
- ansys/mechanical/core/embedding/addins.py +1 -7
- ansys/mechanical/core/embedding/app.py +610 -281
- ansys/mechanical/core/embedding/app_libraries.py +24 -5
- ansys/mechanical/core/embedding/appdata.py +16 -4
- ansys/mechanical/core/embedding/background.py +106 -0
- ansys/mechanical/core/embedding/cleanup_gui.py +61 -0
- ansys/mechanical/core/embedding/enum_importer.py +2 -2
- ansys/mechanical/core/embedding/imports.py +27 -7
- ansys/mechanical/core/embedding/initializer.py +105 -53
- ansys/mechanical/core/embedding/loader.py +19 -9
- ansys/mechanical/core/embedding/logger/__init__.py +219 -216
- ansys/mechanical/core/embedding/logger/environ.py +1 -1
- ansys/mechanical/core/embedding/logger/linux_api.py +1 -1
- ansys/mechanical/core/embedding/logger/sinks.py +1 -1
- ansys/mechanical/core/embedding/logger/windows_api.py +2 -2
- ansys/mechanical/core/embedding/poster.py +38 -4
- ansys/mechanical/core/embedding/resolver.py +41 -44
- ansys/mechanical/core/embedding/runtime.py +1 -1
- ansys/mechanical/core/embedding/shims.py +9 -8
- ansys/mechanical/core/embedding/ui.py +228 -0
- ansys/mechanical/core/embedding/utils.py +1 -1
- ansys/mechanical/core/embedding/viz/__init__.py +1 -1
- ansys/mechanical/core/embedding/viz/{pyvista_plotter.py → embedding_plotter.py} +24 -8
- ansys/mechanical/core/embedding/viz/usd_converter.py +59 -25
- ansys/mechanical/core/embedding/viz/utils.py +32 -2
- ansys/mechanical/core/embedding/warnings.py +1 -1
- ansys/mechanical/core/errors.py +2 -1
- ansys/mechanical/core/examples/__init__.py +1 -1
- ansys/mechanical/core/examples/downloads.py +10 -5
- ansys/mechanical/core/feature_flags.py +51 -0
- ansys/mechanical/core/ide_config.py +212 -0
- ansys/mechanical/core/launcher.py +9 -9
- ansys/mechanical/core/logging.py +14 -2
- ansys/mechanical/core/mechanical.py +2324 -2237
- ansys/mechanical/core/misc.py +176 -176
- ansys/mechanical/core/pool.py +712 -712
- ansys/mechanical/core/run.py +321 -246
- {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/LICENSE +7 -7
- {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/METADATA +57 -56
- ansys_mechanical_core-0.11.12.dist-info/RECORD +45 -0
- {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/WHEEL +1 -1
- {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/entry_points.txt +1 -0
- ansys_mechanical_core-0.10.10.dist-info/RECORD +0 -40
ansys/mechanical/core/pool.py
CHANGED
@@ -1,712 +1,712 @@
|
|
1
|
-
# Copyright (C) 2022 -
|
2
|
-
# SPDX-License-Identifier: MIT
|
3
|
-
#
|
4
|
-
#
|
5
|
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
# of this software and associated documentation files (the "Software"), to deal
|
7
|
-
# in the Software without restriction, including without limitation the rights
|
8
|
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
# copies of the Software, and to permit persons to whom the Software is
|
10
|
-
# furnished to do so, subject to the following conditions:
|
11
|
-
#
|
12
|
-
# The above copyright notice and this permission notice shall be included in all
|
13
|
-
# copies or substantial portions of the Software.
|
14
|
-
#
|
15
|
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
-
# SOFTWARE.
|
22
|
-
|
23
|
-
"""This module is for threaded implementations of the Mechanical interface."""
|
24
|
-
|
25
|
-
import os
|
26
|
-
import time
|
27
|
-
import warnings
|
28
|
-
|
29
|
-
import ansys.platform.instancemanagement as pypim
|
30
|
-
from ansys.tools.path import version_from_path
|
31
|
-
|
32
|
-
from ansys.mechanical.core.errors import VersionError
|
33
|
-
from ansys.mechanical.core.mechanical import (
|
34
|
-
_HAS_TQDM,
|
35
|
-
LOG,
|
36
|
-
MECHANICAL_DEFAULT_PORT,
|
37
|
-
get_mechanical_path,
|
38
|
-
launch_mechanical,
|
39
|
-
port_in_use,
|
40
|
-
)
|
41
|
-
from ansys.mechanical.core.misc import threaded, threaded_daemon
|
42
|
-
|
43
|
-
if _HAS_TQDM:
|
44
|
-
from tqdm import tqdm
|
45
|
-
|
46
|
-
|
47
|
-
def available_ports(n_ports, starting_port=MECHANICAL_DEFAULT_PORT):
|
48
|
-
"""Get a list of a given number of available ports starting from a specified port number.
|
49
|
-
|
50
|
-
Parameters
|
51
|
-
----------
|
52
|
-
n_ports : int
|
53
|
-
Number of available ports to return.
|
54
|
-
starting_port: int, option
|
55
|
-
Number of the port to start the search from. The default is
|
56
|
-
``MECHANICAL_DEFAULT_PORT``.
|
57
|
-
"""
|
58
|
-
port = starting_port
|
59
|
-
ports = []
|
60
|
-
while port < 65536 and len(ports) < n_ports:
|
61
|
-
if not port_in_use(port):
|
62
|
-
ports.append(port)
|
63
|
-
port += 1
|
64
|
-
|
65
|
-
if len(ports) < n_ports:
|
66
|
-
raise RuntimeError(
|
67
|
-
f"There are not {n_ports} available ports between {starting_port} and 65536."
|
68
|
-
)
|
69
|
-
|
70
|
-
return ports
|
71
|
-
|
72
|
-
|
73
|
-
class LocalMechanicalPool:
|
74
|
-
"""Create a pool of Mechanical instances.
|
75
|
-
|
76
|
-
Parameters
|
77
|
-
----------
|
78
|
-
n_instance : int
|
79
|
-
Number of Mechanical instances to create in the pool.
|
80
|
-
wait : bool, optional
|
81
|
-
Whether to wait for the pool to be initialized. The default is
|
82
|
-
``True``. When ``False``, the pool starts in the background, in
|
83
|
-
which case all resources might not be immediately available.
|
84
|
-
starting_port : int, optional
|
85
|
-
Starting port for the instances. The default is ``10000``.
|
86
|
-
progress_bar : bool, optional
|
87
|
-
Whether to show a progress bar when starting the pool. The default
|
88
|
-
is ``True``, but the progress bar is not shown when ``wait=False``.
|
89
|
-
restart_failed : bool, optional
|
90
|
-
Whether to restart any failed instances in the pool. The default is
|
91
|
-
``True``.
|
92
|
-
**kwargs : dict, optional
|
93
|
-
Additional keyword arguments. For a list of all keyword
|
94
|
-
arguments, use the :func:`ansys.mechanical.core.launch_mechanical`
|
95
|
-
function. If the ``exec_file`` keyword argument is found, it is used to
|
96
|
-
start instances. PyPIM is used to create instances if the following
|
97
|
-
conditions are met:
|
98
|
-
|
99
|
-
- PyPIM is configured.
|
100
|
-
- ``version`` is specified.
|
101
|
-
- ``exec_file`` is not specified.
|
102
|
-
|
103
|
-
|
104
|
-
Examples
|
105
|
-
--------
|
106
|
-
Create a pool of 10 Mechanical instances.
|
107
|
-
|
108
|
-
>>> from ansys.mechanical.core import LocalMechanicalPool
|
109
|
-
>>> pool = LocalMechanicalPool(10)
|
110
|
-
Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s]
|
111
|
-
|
112
|
-
On Windows, create a pool while specifying the Mechanical executable file.
|
113
|
-
|
114
|
-
>>> exec_file = 'C:/Program Files/ANSYS Inc/
|
115
|
-
>>> pool = LocalMechanicalPool(10, exec_file=exec_file)
|
116
|
-
Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s]
|
117
|
-
|
118
|
-
On Linux, create a pool while specifying the Mechanical executable file.
|
119
|
-
|
120
|
-
>>> exec_file = '/ansys_inc/
|
121
|
-
>>> pool = LocalMechanicalPool(10, exec_file=exec_file)
|
122
|
-
Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s]
|
123
|
-
|
124
|
-
In the PyPIM environment, create a pool.
|
125
|
-
|
126
|
-
>>> pool = LocalMechanicalPool(10, version="
|
127
|
-
Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s]
|
128
|
-
|
129
|
-
"""
|
130
|
-
|
131
|
-
def __init__(
|
132
|
-
self,
|
133
|
-
n_instances,
|
134
|
-
wait=True,
|
135
|
-
port=MECHANICAL_DEFAULT_PORT,
|
136
|
-
progress_bar=True,
|
137
|
-
restart_failed=True,
|
138
|
-
**kwargs,
|
139
|
-
):
|
140
|
-
"""Initialize several Mechanical instances.
|
141
|
-
|
142
|
-
Parameters
|
143
|
-
----------
|
144
|
-
n_instance : int
|
145
|
-
Number of Mechanical instances to initialize.
|
146
|
-
wait : bool, optional
|
147
|
-
Whether to wait for the instances to be initialized. The default is
|
148
|
-
``True``. When ``False``, the instances start in the background, in
|
149
|
-
which case all resources might not be immediately available.
|
150
|
-
port : int, optional
|
151
|
-
Port for the first Mechanical instance. The default is
|
152
|
-
``MECHANICAL_DEFAULT_PORT``.
|
153
|
-
progress_bar : bool, optional
|
154
|
-
Whether to display a progress bar when starting the instances. The default
|
155
|
-
is ``True``, but the progress bar is not shown when ``wait=False``.
|
156
|
-
restart_failed : bool, optional
|
157
|
-
Whether to restart any failed instances. The default is ``True``.
|
158
|
-
**kwargs : dict, optional
|
159
|
-
Additional keyword arguments. For a list of all additional keyword
|
160
|
-
arguments, see the :func:`ansys.mechanical.core.launch_mechanical`
|
161
|
-
function. If the ``exec_file`` keyword argument is found, it is used to
|
162
|
-
start instances. Instances are created using PyPIM if the following
|
163
|
-
conditions are met:
|
164
|
-
|
165
|
-
- PyPIM is configured.
|
166
|
-
- Version is specified/
|
167
|
-
- ``exec_file`` is not specified.
|
168
|
-
"""
|
169
|
-
self._instances = []
|
170
|
-
self._spawn_kwargs = kwargs
|
171
|
-
self._remote = False
|
172
|
-
|
173
|
-
#
|
174
|
-
exec_file = None
|
175
|
-
if "exec_file" in kwargs:
|
176
|
-
exec_file = kwargs["exec_file"]
|
177
|
-
else:
|
178
|
-
if pypim.is_configured(): # pragma: no cover
|
179
|
-
if "version" in kwargs:
|
180
|
-
version = kwargs["version"]
|
181
|
-
self._remote = True
|
182
|
-
else:
|
183
|
-
raise "Pypim is configured
|
184
|
-
else: # get default executable
|
185
|
-
exec_file = get_mechanical_path()
|
186
|
-
if exec_file is None: # pragma: no cover
|
187
|
-
raise FileNotFoundError(
|
188
|
-
"Path to Mechanical executable file is invalid or cache cannot be loaded. "
|
189
|
-
"Enter a path manually by specifying a value for the "
|
190
|
-
"'exec_file' parameter."
|
191
|
-
)
|
192
|
-
|
193
|
-
if not self._remote: # pragma: no cover
|
194
|
-
if version_from_path("mechanical", exec_file) <
|
195
|
-
raise VersionError("A local Mechanical pool requires Mechanical 2023
|
196
|
-
|
197
|
-
ports = None
|
198
|
-
|
199
|
-
if not self._remote:
|
200
|
-
# grab available ports
|
201
|
-
ports = available_ports(n_instances, port)
|
202
|
-
|
203
|
-
self._instances = []
|
204
|
-
self._active = True # used by pool monitor
|
205
|
-
|
206
|
-
n_instances = int(n_instances)
|
207
|
-
if n_instances < 2:
|
208
|
-
raise ValueError("You must request at least two instances to create a pool.")
|
209
|
-
|
210
|
-
pbar = None
|
211
|
-
if wait and progress_bar:
|
212
|
-
if not _HAS_TQDM: # pragma: no cover
|
213
|
-
raise ModuleNotFoundError(
|
214
|
-
f"To use the keyword argument 'progress_bar', you must have installed "
|
215
|
-
f"the 'tqdm' package. To avoid this message, you can set 'progress_bar=False'."
|
216
|
-
)
|
217
|
-
|
218
|
-
pbar = tqdm(total=n_instances, desc="Creating Pool")
|
219
|
-
|
220
|
-
# initialize a list of dummy instances
|
221
|
-
self._instances = [None for _ in range(n_instances)]
|
222
|
-
|
223
|
-
if self._remote: # pragma: no cover
|
224
|
-
threads = [
|
225
|
-
self._spawn_mechanical_remote(i, pbar, name=f"Instance {i}")
|
226
|
-
for i in range(n_instances)
|
227
|
-
]
|
228
|
-
else:
|
229
|
-
# threaded spawn
|
230
|
-
threads = [
|
231
|
-
self._spawn_mechanical(i, ports[i], pbar, name=f"Instance {i}")
|
232
|
-
for i in range(n_instances)
|
233
|
-
]
|
234
|
-
if wait:
|
235
|
-
[thread.join() for thread in threads]
|
236
|
-
|
237
|
-
# check if all clients connected have connected
|
238
|
-
if len(self) != n_instances: # pragma: no cover
|
239
|
-
n_connected = len(self)
|
240
|
-
warnings.warn(
|
241
|
-
f"Only {n_connected} clients connected out of {n_instances} requested"
|
242
|
-
)
|
243
|
-
if pbar is not None:
|
244
|
-
pbar.close()
|
245
|
-
|
246
|
-
# monitor pool if requested
|
247
|
-
if restart_failed:
|
248
|
-
self._pool_monitor_thread = self._monitor_pool(name="Monitoring_Thread started")
|
249
|
-
|
250
|
-
if not self._remote:
|
251
|
-
self._verify_unique_ports()
|
252
|
-
|
253
|
-
def _verify_unique_ports(self):
|
254
|
-
if self._remote: # pragma: no cover
|
255
|
-
raise RuntimeError("PyPIM is used. Port information is not available.")
|
256
|
-
|
257
|
-
if len(self.ports) != len(self): # pragma: no cover
|
258
|
-
raise RuntimeError("Mechanical pool has overlapping ports.")
|
259
|
-
|
260
|
-
def map(
|
261
|
-
self,
|
262
|
-
func,
|
263
|
-
iterable=None,
|
264
|
-
clear_at_start=True,
|
265
|
-
progress_bar=True,
|
266
|
-
close_when_finished=False,
|
267
|
-
timeout=None,
|
268
|
-
wait=True,
|
269
|
-
):
|
270
|
-
"""Run a user-defined function on each Mechanical instance in the pool.
|
271
|
-
|
272
|
-
Parameters
|
273
|
-
----------
|
274
|
-
func : function
|
275
|
-
Function with ``mechanical`` as the first argument. The subsequent
|
276
|
-
arguments should match the number of items in each iterable (if any).
|
277
|
-
iterable : list, tuple, optional
|
278
|
-
An iterable containing a set of arguments for the function.
|
279
|
-
The default is ``None``, in which case the function runs
|
280
|
-
once on each instance of Mechanical.
|
281
|
-
clear_at_start : bool, optional
|
282
|
-
Clear Mechanical at the start of execution. The default is
|
283
|
-
``True``. Setting this to ``False`` might lead to instability.
|
284
|
-
progress_bar : bool, optional
|
285
|
-
Whether to show a progress bar when running the batch of input
|
286
|
-
files. The default is ``True``, but the progress bar is not shown
|
287
|
-
when ``wait=False``.
|
288
|
-
close_when_finished : bool, optional
|
289
|
-
Whether to close the instances when the function finishes running
|
290
|
-
on all instances in the pool. The default is ``False``.
|
291
|
-
timeout : float, optional
|
292
|
-
Maximum runtime in seconds for each iteration. The default is
|
293
|
-
``None``, in which case there is no timeout. If you specify a
|
294
|
-
value, each iteration is allowed to run only this number of
|
295
|
-
seconds. Once this value is exceeded, the batch process is
|
296
|
-
stopped and treated as a failure.
|
297
|
-
wait : bool, optional
|
298
|
-
Whether block execution must wait until the batch process is
|
299
|
-
complete. The default is ``True``.
|
300
|
-
|
301
|
-
Returns
|
302
|
-
-------
|
303
|
-
list
|
304
|
-
A list containing the return values for the function.
|
305
|
-
Failed runs do not return an output. Because return values
|
306
|
-
are not necessarily in the same order as the iterable,
|
307
|
-
you might want to add some sort of tracker to the return
|
308
|
-
of your function.
|
309
|
-
|
310
|
-
Examples
|
311
|
-
--------
|
312
|
-
Run several input files while storing the final routine. Note
|
313
|
-
how the function to map must use ``mechanical`` as the first argument.
|
314
|
-
The function can have any number of additional arguments.
|
315
|
-
|
316
|
-
>>> from ansys.mechanical.core import LocalMechanicalPool
|
317
|
-
>>> pool = LocalMechanicalPool(10)
|
318
|
-
>>> completed_indices = []
|
319
|
-
>>> def function(mechanical, name, script):
|
320
|
-
# name, script = args
|
321
|
-
mechanical.clear()
|
322
|
-
output = mechanical.run_python_script(script)
|
323
|
-
return name, output
|
324
|
-
>>> inputs = [("first","2+3"), ("second", "3+4")]
|
325
|
-
>>> output = pool.map(function, inputs, progress_bar=False, wait=True)
|
326
|
-
[('first', '5'), ('second', '7')]
|
327
|
-
"""
|
328
|
-
# check if any instances are available
|
329
|
-
if not len(self): # pragma: no cover
|
330
|
-
# instances could still be spawning...
|
331
|
-
if not all(v is None for v in self._instances):
|
332
|
-
raise RuntimeError("No Mechanical instances available.")
|
333
|
-
|
334
|
-
results = []
|
335
|
-
|
336
|
-
if iterable is not None:
|
337
|
-
jobs_count = len(iterable)
|
338
|
-
else:
|
339
|
-
jobs_count = len(self)
|
340
|
-
|
341
|
-
pbar = None
|
342
|
-
if progress_bar:
|
343
|
-
if not _HAS_TQDM: # pragma: no cover
|
344
|
-
raise ModuleNotFoundError(
|
345
|
-
|
346
|
-
|
347
|
-
)
|
348
|
-
|
349
|
-
pbar = tqdm(total=jobs_count, desc="Mechanical Running")
|
350
|
-
|
351
|
-
@threaded_daemon
|
352
|
-
def func_wrapper(obj, func, clear_at_start, timeout, args=None, name=""):
|
353
|
-
"""Expect obj to be an instance of Mechanical."""
|
354
|
-
LOG.debug(name)
|
355
|
-
complete = [False]
|
356
|
-
|
357
|
-
@threaded_daemon
|
358
|
-
def run(name_local=""):
|
359
|
-
LOG.debug(name_local)
|
360
|
-
|
361
|
-
if clear_at_start:
|
362
|
-
obj.clear()
|
363
|
-
|
364
|
-
if args is not None:
|
365
|
-
if isinstance(args, (tuple, list)):
|
366
|
-
results.append(func(obj, *args))
|
367
|
-
else:
|
368
|
-
results.append(func(obj, args))
|
369
|
-
else:
|
370
|
-
results.append(func(obj))
|
371
|
-
|
372
|
-
complete[0] = True
|
373
|
-
|
374
|
-
run_thread = run(name_local=name)
|
375
|
-
|
376
|
-
if timeout: # pragma: no cover
|
377
|
-
time_start = time.time()
|
378
|
-
while not complete[0]:
|
379
|
-
time.sleep(0.01)
|
380
|
-
if (time.time() - time_start) > timeout:
|
381
|
-
break
|
382
|
-
|
383
|
-
if not complete[0]:
|
384
|
-
LOG.error(f"Stopped instance due to a timeout of {timeout} seconds.")
|
385
|
-
obj.exit()
|
386
|
-
else:
|
387
|
-
run_thread.join()
|
388
|
-
if not complete[0]: # pragma: no cover
|
389
|
-
LOG.error(
|
390
|
-
try:
|
391
|
-
obj.exit()
|
392
|
-
except:
|
393
|
-
|
394
|
-
|
395
|
-
obj.locked = False
|
396
|
-
if pbar:
|
397
|
-
pbar.update(1)
|
398
|
-
|
399
|
-
threads = []
|
400
|
-
if iterable is not None:
|
401
|
-
for args in iterable:
|
402
|
-
# grab the next available instance of mechanical
|
403
|
-
instance, i = self.next_available(return_index=True)
|
404
|
-
instance.locked = True
|
405
|
-
|
406
|
-
threads.append(
|
407
|
-
func_wrapper(
|
408
|
-
instance, func, clear_at_start, timeout, args, name=f"Map_Thread{i}"
|
409
|
-
)
|
410
|
-
)
|
411
|
-
else: # simply apply to all
|
412
|
-
for instance in self._instances:
|
413
|
-
if instance:
|
414
|
-
threads.append(
|
415
|
-
func_wrapper(instance, func, clear_at_start, timeout, name=f"Map_Thread")
|
416
|
-
)
|
417
|
-
|
418
|
-
if close_when_finished: # pragma: no cover
|
419
|
-
# start closing any instances that are not in execution
|
420
|
-
while not all(v is None for v in self._instances):
|
421
|
-
# grab the next available instance of mechanical and close it
|
422
|
-
instance, i = self.next_available(return_index=True)
|
423
|
-
self._instances[i] = None
|
424
|
-
|
425
|
-
try:
|
426
|
-
instance.exit()
|
427
|
-
except Exception as error: # pragma: no cover
|
428
|
-
LOG.error(f"Failed to close instance : str{error}.")
|
429
|
-
else:
|
430
|
-
# wait for all threads to complete
|
431
|
-
if wait:
|
432
|
-
[thread.join() for thread in threads]
|
433
|
-
|
434
|
-
return results
|
435
|
-
|
436
|
-
def run_batch(
|
437
|
-
self,
|
438
|
-
files,
|
439
|
-
clear_at_start=True,
|
440
|
-
progress_bar=True,
|
441
|
-
close_when_finished=False,
|
442
|
-
timeout=None,
|
443
|
-
wait=True,
|
444
|
-
):
|
445
|
-
"""Run a batch of input files on the Mechanical instances in the pool.
|
446
|
-
|
447
|
-
Parameters
|
448
|
-
----------
|
449
|
-
files : list
|
450
|
-
List of input files.
|
451
|
-
clear_at_start : bool, optional
|
452
|
-
Whether to clear Mechanical when execution starts. The default is
|
453
|
-
``True``. Setting this parameter to ``False`` might lead to
|
454
|
-
instability.
|
455
|
-
progress_bar : bool, optional
|
456
|
-
Whether to show a progress bar when running the batch of input
|
457
|
-
files. The default is ``True``, but the progress bar is not shown
|
458
|
-
when ``wait=False``.
|
459
|
-
close_when_finished : bool, optional
|
460
|
-
Whether to close the instances when running the batch
|
461
|
-
of input files is finished. The default is ``False``.
|
462
|
-
timeout : float, optional
|
463
|
-
Maximum runtime in seconds for each iteration. The default is
|
464
|
-
``None``, in which case there is no timeout. If you specify a
|
465
|
-
value, each iteration is allowed to run only this number of
|
466
|
-
seconds. Once this value is exceeded, the batch process is stopped
|
467
|
-
and treated as a failure.
|
468
|
-
wait : bool, optional
|
469
|
-
Whether block execution must wait until the batch process is complete.
|
470
|
-
The default is ``True``.
|
471
|
-
|
472
|
-
Returns
|
473
|
-
-------
|
474
|
-
list
|
475
|
-
List of text outputs from Mechanical for each batch run. The outputs
|
476
|
-
are not necessarily listed in the order of the inputs. Failed runs do
|
477
|
-
not return an output. Because the return outputs are not
|
478
|
-
necessarily in the same order as ``iterable``, you might
|
479
|
-
want to add some sort of tracker or note within the input files.
|
480
|
-
|
481
|
-
Examples
|
482
|
-
--------
|
483
|
-
Run 20 verification files on the pool.
|
484
|
-
|
485
|
-
>>> files = [f"test{index}.py" for index in range(1, 21)]
|
486
|
-
>>> outputs = pool.run_batch(files)
|
487
|
-
>>> len(outputs)
|
488
|
-
20
|
489
|
-
"""
|
490
|
-
# check all files exist before running
|
491
|
-
for filename in files:
|
492
|
-
if not os.path.isfile(filename):
|
493
|
-
raise FileNotFoundError("Unable to locate file %s" % filename)
|
494
|
-
|
495
|
-
def run_file(mechanical, input_file):
|
496
|
-
if clear_at_start:
|
497
|
-
mechanical.clear()
|
498
|
-
return mechanical.run_python_script_from_file(input_file)
|
499
|
-
|
500
|
-
return self.map(
|
501
|
-
run_file,
|
502
|
-
files,
|
503
|
-
progress_bar=progress_bar,
|
504
|
-
close_when_finished=close_when_finished,
|
505
|
-
timeout=timeout,
|
506
|
-
wait=wait,
|
507
|
-
)
|
508
|
-
|
509
|
-
def next_available(self, return_index=False):
|
510
|
-
"""Wait until a Mechanical instance is available and return this instance.
|
511
|
-
|
512
|
-
Parameters
|
513
|
-
----------
|
514
|
-
return_index : bool, optional
|
515
|
-
Whether to return the index along with the instance. The default
|
516
|
-
is ``False``.
|
517
|
-
|
518
|
-
Returns
|
519
|
-
-------
|
520
|
-
pymechanical.Mechanical
|
521
|
-
Instance of Mechanical.
|
522
|
-
|
523
|
-
int
|
524
|
-
Index within the pool of Mechanical instances. This index
|
525
|
-
is not returned by default.
|
526
|
-
|
527
|
-
Examples
|
528
|
-
--------
|
529
|
-
>>> mechanical = pool.next_available()
|
530
|
-
>>> mechanical
|
531
|
-
Ansys Mechanical [Ansys Mechanical Enterprise]
|
532
|
-
Product Version:
|
533
|
-
Software build date:
|
534
|
-
"""
|
535
|
-
# loop until the next instance is available
|
536
|
-
while True:
|
537
|
-
for i, instance in enumerate(self._instances):
|
538
|
-
# if encounter placeholder
|
539
|
-
if not instance: # pragma: no cover
|
540
|
-
continue
|
541
|
-
|
542
|
-
if not instance.locked and not instance._exited:
|
543
|
-
# any instance that is not running or exited
|
544
|
-
# should be available
|
545
|
-
if not instance.busy:
|
546
|
-
# double check that this instance is alive:
|
547
|
-
try:
|
548
|
-
instance._make_dummy_call()
|
549
|
-
except: # pragma: no cover
|
550
|
-
instance.exit()
|
551
|
-
continue
|
552
|
-
|
553
|
-
if return_index:
|
554
|
-
return instance, i
|
555
|
-
else:
|
556
|
-
return instance
|
557
|
-
# review - not needed
|
558
|
-
# else:
|
559
|
-
# instance._exited = True
|
560
|
-
|
561
|
-
def __del__(self):
|
562
|
-
"""Clean up when complete."""
|
563
|
-
print("pool:Automatic clean up.")
|
564
|
-
self.exit()
|
565
|
-
|
566
|
-
def exit(self, block=False):
|
567
|
-
"""Exit all Mechanical instances in the pool.
|
568
|
-
|
569
|
-
Parameters
|
570
|
-
----------
|
571
|
-
block : bool, optional
|
572
|
-
Whether to wait until all processes close before exiting
|
573
|
-
all instances in the pool. The default is ``False``.
|
574
|
-
|
575
|
-
Examples
|
576
|
-
--------
|
577
|
-
>>> pool.exit()
|
578
|
-
"""
|
579
|
-
self._active = False # Stops any active instance restart
|
580
|
-
|
581
|
-
@threaded
|
582
|
-
def threaded_exit(index, instance_local):
|
583
|
-
if instance_local:
|
584
|
-
try:
|
585
|
-
instance_local.exit()
|
586
|
-
except: # pragma: no cover
|
587
|
-
|
588
|
-
self._instances[index] = None
|
589
|
-
LOG.debug(f"Exited instance: {str(instance_local)}")
|
590
|
-
|
591
|
-
threads = []
|
592
|
-
for i, instance in enumerate(self):
|
593
|
-
threads.append(threaded_exit(i, instance))
|
594
|
-
|
595
|
-
if block:
|
596
|
-
[thread.join() for thread in threads]
|
597
|
-
|
598
|
-
def __len__(self):
|
599
|
-
"""Get the number of instances in the pool."""
|
600
|
-
count = 0
|
601
|
-
for instance in self._instances:
|
602
|
-
if instance:
|
603
|
-
if not instance._exited:
|
604
|
-
count += 1
|
605
|
-
return count
|
606
|
-
|
607
|
-
def __getitem__(self, key):
|
608
|
-
"""Get an instance by an index."""
|
609
|
-
return self._instances[key]
|
610
|
-
|
611
|
-
def __iter__(self):
|
612
|
-
"""Iterate through active instances."""
|
613
|
-
for instance in self._instances:
|
614
|
-
if instance:
|
615
|
-
yield instance
|
616
|
-
|
617
|
-
@threaded_daemon
|
618
|
-
def _spawn_mechanical(self, index, port=None, pbar=None, name=""):
|
619
|
-
"""Spawn a Mechanical instance at an index.
|
620
|
-
|
621
|
-
Parameters
|
622
|
-
----------
|
623
|
-
index : int
|
624
|
-
Index to spawn the instance on.
|
625
|
-
port : int, optional
|
626
|
-
Port for the instance. The default is ``None``.
|
627
|
-
pbar :
|
628
|
-
The default is ``None``.
|
629
|
-
name : str, optional
|
630
|
-
Name for the instance. The default is ``""``.
|
631
|
-
"""
|
632
|
-
LOG.debug(name)
|
633
|
-
self._instances[index] = launch_mechanical(port=port, **self._spawn_kwargs)
|
634
|
-
# LOG.debug("Spawned instance %d. Name '%s'", index, name)
|
635
|
-
if pbar is not None:
|
636
|
-
pbar.update(1)
|
637
|
-
|
638
|
-
@threaded_daemon
|
639
|
-
def _spawn_mechanical_remote(self, index, pbar=None, name=""): # pragma: no cover
|
640
|
-
"""Spawn a Mechanical instance at an index.
|
641
|
-
|
642
|
-
Parameters
|
643
|
-
----------
|
644
|
-
index : int
|
645
|
-
Index to spawn the instance on.
|
646
|
-
pbar :
|
647
|
-
The default is ``None``.
|
648
|
-
name : str, optional
|
649
|
-
Name for the instance. The default is ``""``.
|
650
|
-
|
651
|
-
"""
|
652
|
-
LOG.debug(name)
|
653
|
-
self._instances[index] = launch_mechanical(**self._spawn_kwargs)
|
654
|
-
# LOG.debug("Spawned instance %d. Name '%s'", index, name)
|
655
|
-
if pbar is not None:
|
656
|
-
pbar.update(1)
|
657
|
-
|
658
|
-
@threaded_daemon
|
659
|
-
def _monitor_pool(self, refresh=1.0, name=""):
|
660
|
-
"""Check for instances within a pool that have exited (failed) and restart them.
|
661
|
-
|
662
|
-
Parameters
|
663
|
-
----------
|
664
|
-
refresh : float, optional
|
665
|
-
The default is ``1.0``.
|
666
|
-
name : str, optional
|
667
|
-
Name for the instance. The default is ``""``.
|
668
|
-
"""
|
669
|
-
LOG.debug(name)
|
670
|
-
while self._active:
|
671
|
-
for index, instance in enumerate(self._instances):
|
672
|
-
# encountered placeholder
|
673
|
-
if not instance: # pragma: no cover
|
674
|
-
continue
|
675
|
-
if instance._exited: # pragma: no cover
|
676
|
-
try:
|
677
|
-
if self._remote:
|
678
|
-
LOG.debug(
|
679
|
-
f"Restarting a Mechanical remote instance for index : {index}."
|
680
|
-
)
|
681
|
-
self._spawn_mechanical_remote(index, name=f"Instance {index}").join()
|
682
|
-
else:
|
683
|
-
# use the next port after the current available port
|
684
|
-
port = max(self.ports) + 1
|
685
|
-
LOG.debug(
|
686
|
-
f"Restarting a Mechanical instance for index : "
|
687
|
-
f"{index} on port: {port}."
|
688
|
-
)
|
689
|
-
self._spawn_mechanical(
|
690
|
-
index, port=port, name=f"Instance {index}"
|
691
|
-
).join()
|
692
|
-
except Exception as e:
|
693
|
-
LOG.error(e, exc_info=True)
|
694
|
-
time.sleep(refresh)
|
695
|
-
|
696
|
-
@property
|
697
|
-
def ports(self):
|
698
|
-
"""Get a list of the ports that are used.
|
699
|
-
|
700
|
-
Examples
|
701
|
-
--------
|
702
|
-
Get the list of ports used by the pool of Mechanical instances.
|
703
|
-
|
704
|
-
>>> pool.ports
|
705
|
-
[10001, 10002]
|
706
|
-
|
707
|
-
"""
|
708
|
-
return [inst._port for inst in self if inst is not None]
|
709
|
-
|
710
|
-
def __str__(self):
|
711
|
-
"""Get the string representation of this object."""
|
712
|
-
return "Mechanical pool with %d active instances" % len(self)
|
1
|
+
# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
|
2
|
+
# SPDX-License-Identifier: MIT
|
3
|
+
#
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
10
|
+
# furnished to do so, subject to the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
13
|
+
# copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
# SOFTWARE.
|
22
|
+
|
23
|
+
"""This module is for threaded implementations of the Mechanical interface."""
|
24
|
+
|
25
|
+
import os
|
26
|
+
import time
|
27
|
+
import warnings
|
28
|
+
|
29
|
+
import ansys.platform.instancemanagement as pypim
|
30
|
+
from ansys.tools.path import version_from_path
|
31
|
+
|
32
|
+
from ansys.mechanical.core.errors import VersionError
|
33
|
+
from ansys.mechanical.core.mechanical import (
|
34
|
+
_HAS_TQDM,
|
35
|
+
LOG,
|
36
|
+
MECHANICAL_DEFAULT_PORT,
|
37
|
+
get_mechanical_path,
|
38
|
+
launch_mechanical,
|
39
|
+
port_in_use,
|
40
|
+
)
|
41
|
+
from ansys.mechanical.core.misc import threaded, threaded_daemon
|
42
|
+
|
43
|
+
if _HAS_TQDM:
|
44
|
+
from tqdm import tqdm
|
45
|
+
|
46
|
+
|
47
|
+
def available_ports(n_ports, starting_port=MECHANICAL_DEFAULT_PORT):
|
48
|
+
"""Get a list of a given number of available ports starting from a specified port number.
|
49
|
+
|
50
|
+
Parameters
|
51
|
+
----------
|
52
|
+
n_ports : int
|
53
|
+
Number of available ports to return.
|
54
|
+
starting_port: int, option
|
55
|
+
Number of the port to start the search from. The default is
|
56
|
+
``MECHANICAL_DEFAULT_PORT``.
|
57
|
+
"""
|
58
|
+
port = starting_port
|
59
|
+
ports = []
|
60
|
+
while port < 65536 and len(ports) < n_ports:
|
61
|
+
if not port_in_use(port):
|
62
|
+
ports.append(port)
|
63
|
+
port += 1
|
64
|
+
|
65
|
+
if len(ports) < n_ports:
|
66
|
+
raise RuntimeError(
|
67
|
+
f"There are not {n_ports} available ports between {starting_port} and 65536."
|
68
|
+
)
|
69
|
+
|
70
|
+
return ports
|
71
|
+
|
72
|
+
|
73
|
+
class LocalMechanicalPool:
|
74
|
+
"""Create a pool of Mechanical instances.
|
75
|
+
|
76
|
+
Parameters
|
77
|
+
----------
|
78
|
+
n_instance : int
|
79
|
+
Number of Mechanical instances to create in the pool.
|
80
|
+
wait : bool, optional
|
81
|
+
Whether to wait for the pool to be initialized. The default is
|
82
|
+
``True``. When ``False``, the pool starts in the background, in
|
83
|
+
which case all resources might not be immediately available.
|
84
|
+
starting_port : int, optional
|
85
|
+
Starting port for the instances. The default is ``10000``.
|
86
|
+
progress_bar : bool, optional
|
87
|
+
Whether to show a progress bar when starting the pool. The default
|
88
|
+
is ``True``, but the progress bar is not shown when ``wait=False``.
|
89
|
+
restart_failed : bool, optional
|
90
|
+
Whether to restart any failed instances in the pool. The default is
|
91
|
+
``True``.
|
92
|
+
**kwargs : dict, optional
|
93
|
+
Additional keyword arguments. For a list of all keyword
|
94
|
+
arguments, use the :func:`ansys.mechanical.core.launch_mechanical`
|
95
|
+
function. If the ``exec_file`` keyword argument is found, it is used to
|
96
|
+
start instances. PyPIM is used to create instances if the following
|
97
|
+
conditions are met:
|
98
|
+
|
99
|
+
- PyPIM is configured.
|
100
|
+
- ``version`` is specified.
|
101
|
+
- ``exec_file`` is not specified.
|
102
|
+
|
103
|
+
|
104
|
+
Examples
|
105
|
+
--------
|
106
|
+
Create a pool of 10 Mechanical instances.
|
107
|
+
|
108
|
+
>>> from ansys.mechanical.core import LocalMechanicalPool
|
109
|
+
>>> pool = LocalMechanicalPool(10)
|
110
|
+
Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s]
|
111
|
+
|
112
|
+
On Windows, create a pool while specifying the Mechanical executable file.
|
113
|
+
|
114
|
+
>>> exec_file = 'C:/Program Files/ANSYS Inc/v251/aisol/bin/winx64/AnsysWBU.exe'
|
115
|
+
>>> pool = LocalMechanicalPool(10, exec_file=exec_file)
|
116
|
+
Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s]
|
117
|
+
|
118
|
+
On Linux, create a pool while specifying the Mechanical executable file.
|
119
|
+
|
120
|
+
>>> exec_file = '/ansys_inc/v251/aisol/.workbench'
|
121
|
+
>>> pool = LocalMechanicalPool(10, exec_file=exec_file)
|
122
|
+
Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s]
|
123
|
+
|
124
|
+
In the PyPIM environment, create a pool.
|
125
|
+
|
126
|
+
>>> pool = LocalMechanicalPool(10, version="251")
|
127
|
+
Creating Pool: 100%|########| 10/10 [00:01<00:00, 1.43it/s]
|
128
|
+
|
129
|
+
"""
|
130
|
+
|
131
|
+
def __init__(
|
132
|
+
self,
|
133
|
+
n_instances,
|
134
|
+
wait=True,
|
135
|
+
port=MECHANICAL_DEFAULT_PORT,
|
136
|
+
progress_bar=True,
|
137
|
+
restart_failed=True,
|
138
|
+
**kwargs,
|
139
|
+
):
|
140
|
+
"""Initialize several Mechanical instances.
|
141
|
+
|
142
|
+
Parameters
|
143
|
+
----------
|
144
|
+
n_instance : int
|
145
|
+
Number of Mechanical instances to initialize.
|
146
|
+
wait : bool, optional
|
147
|
+
Whether to wait for the instances to be initialized. The default is
|
148
|
+
``True``. When ``False``, the instances start in the background, in
|
149
|
+
which case all resources might not be immediately available.
|
150
|
+
port : int, optional
|
151
|
+
Port for the first Mechanical instance. The default is
|
152
|
+
``MECHANICAL_DEFAULT_PORT``.
|
153
|
+
progress_bar : bool, optional
|
154
|
+
Whether to display a progress bar when starting the instances. The default
|
155
|
+
is ``True``, but the progress bar is not shown when ``wait=False``.
|
156
|
+
restart_failed : bool, optional
|
157
|
+
Whether to restart any failed instances. The default is ``True``.
|
158
|
+
**kwargs : dict, optional
|
159
|
+
Additional keyword arguments. For a list of all additional keyword
|
160
|
+
arguments, see the :func:`ansys.mechanical.core.launch_mechanical`
|
161
|
+
function. If the ``exec_file`` keyword argument is found, it is used to
|
162
|
+
start instances. Instances are created using PyPIM if the following
|
163
|
+
conditions are met:
|
164
|
+
|
165
|
+
- PyPIM is configured.
|
166
|
+
- Version is specified/
|
167
|
+
- ``exec_file`` is not specified.
|
168
|
+
"""
|
169
|
+
self._instances = []
|
170
|
+
self._spawn_kwargs = kwargs
|
171
|
+
self._remote = False
|
172
|
+
|
173
|
+
# Verify that Mechanical is 2023R2 or newer
|
174
|
+
exec_file = None
|
175
|
+
if "exec_file" in kwargs:
|
176
|
+
exec_file = kwargs["exec_file"]
|
177
|
+
else:
|
178
|
+
if pypim.is_configured(): # pragma: no cover
|
179
|
+
if "version" in kwargs:
|
180
|
+
version = kwargs["version"]
|
181
|
+
self._remote = True
|
182
|
+
else:
|
183
|
+
raise ValueError("Pypim is configured, but version is not passed.")
|
184
|
+
else: # get default executable
|
185
|
+
exec_file = get_mechanical_path()
|
186
|
+
if exec_file is None: # pragma: no cover
|
187
|
+
raise FileNotFoundError(
|
188
|
+
"Path to Mechanical executable file is invalid or cache cannot be loaded. "
|
189
|
+
"Enter a path manually by specifying a value for the "
|
190
|
+
"'exec_file' parameter."
|
191
|
+
)
|
192
|
+
|
193
|
+
if not self._remote: # pragma: no cover
|
194
|
+
if version_from_path("mechanical", exec_file) < 232:
|
195
|
+
raise VersionError("A local Mechanical pool requires Mechanical 2023 R2 or later.")
|
196
|
+
|
197
|
+
ports = None
|
198
|
+
|
199
|
+
if not self._remote:
|
200
|
+
# grab available ports
|
201
|
+
ports = available_ports(n_instances, port)
|
202
|
+
|
203
|
+
self._instances = []
|
204
|
+
self._active = True # used by pool monitor
|
205
|
+
|
206
|
+
n_instances = int(n_instances)
|
207
|
+
if n_instances < 2:
|
208
|
+
raise ValueError("You must request at least two instances to create a pool.")
|
209
|
+
|
210
|
+
pbar = None
|
211
|
+
if wait and progress_bar:
|
212
|
+
if not _HAS_TQDM: # pragma: no cover
|
213
|
+
raise ModuleNotFoundError(
|
214
|
+
f"To use the keyword argument 'progress_bar', you must have installed "
|
215
|
+
f"the 'tqdm' package. To avoid this message, you can set 'progress_bar=False'."
|
216
|
+
)
|
217
|
+
|
218
|
+
pbar = tqdm(total=n_instances, desc="Creating Pool")
|
219
|
+
|
220
|
+
# initialize a list of dummy instances
|
221
|
+
self._instances = [None for _ in range(n_instances)]
|
222
|
+
|
223
|
+
if self._remote: # pragma: no cover
|
224
|
+
threads = [
|
225
|
+
self._spawn_mechanical_remote(i, pbar, name=f"Instance {i}")
|
226
|
+
for i in range(n_instances)
|
227
|
+
]
|
228
|
+
else:
|
229
|
+
# threaded spawn
|
230
|
+
threads = [
|
231
|
+
self._spawn_mechanical(i, ports[i], pbar, name=f"Instance {i}")
|
232
|
+
for i in range(n_instances)
|
233
|
+
]
|
234
|
+
if wait:
|
235
|
+
[thread.join() for thread in threads]
|
236
|
+
|
237
|
+
# check if all clients connected have connected
|
238
|
+
if len(self) != n_instances: # pragma: no cover
|
239
|
+
n_connected = len(self)
|
240
|
+
warnings.warn(
|
241
|
+
f"Only {n_connected} clients connected out of {n_instances} requested"
|
242
|
+
)
|
243
|
+
if pbar is not None:
|
244
|
+
pbar.close()
|
245
|
+
|
246
|
+
# monitor pool if requested
|
247
|
+
if restart_failed:
|
248
|
+
self._pool_monitor_thread = self._monitor_pool(name="Monitoring_Thread started")
|
249
|
+
|
250
|
+
if not self._remote:
|
251
|
+
self._verify_unique_ports()
|
252
|
+
|
253
|
+
def _verify_unique_ports(self):
|
254
|
+
if self._remote: # pragma: no cover
|
255
|
+
raise RuntimeError("PyPIM is used. Port information is not available.")
|
256
|
+
|
257
|
+
if len(self.ports) != len(self): # pragma: no cover
|
258
|
+
raise RuntimeError("Mechanical pool has overlapping ports.")
|
259
|
+
|
260
|
+
def map(
|
261
|
+
self,
|
262
|
+
func,
|
263
|
+
iterable=None,
|
264
|
+
clear_at_start=True,
|
265
|
+
progress_bar=True,
|
266
|
+
close_when_finished=False,
|
267
|
+
timeout=None,
|
268
|
+
wait=True,
|
269
|
+
):
|
270
|
+
"""Run a user-defined function on each Mechanical instance in the pool.
|
271
|
+
|
272
|
+
Parameters
|
273
|
+
----------
|
274
|
+
func : function
|
275
|
+
Function with ``mechanical`` as the first argument. The subsequent
|
276
|
+
arguments should match the number of items in each iterable (if any).
|
277
|
+
iterable : list, tuple, optional
|
278
|
+
An iterable containing a set of arguments for the function.
|
279
|
+
The default is ``None``, in which case the function runs
|
280
|
+
once on each instance of Mechanical.
|
281
|
+
clear_at_start : bool, optional
|
282
|
+
Clear Mechanical at the start of execution. The default is
|
283
|
+
``True``. Setting this to ``False`` might lead to instability.
|
284
|
+
progress_bar : bool, optional
|
285
|
+
Whether to show a progress bar when running the batch of input
|
286
|
+
files. The default is ``True``, but the progress bar is not shown
|
287
|
+
when ``wait=False``.
|
288
|
+
close_when_finished : bool, optional
|
289
|
+
Whether to close the instances when the function finishes running
|
290
|
+
on all instances in the pool. The default is ``False``.
|
291
|
+
timeout : float, optional
|
292
|
+
Maximum runtime in seconds for each iteration. The default is
|
293
|
+
``None``, in which case there is no timeout. If you specify a
|
294
|
+
value, each iteration is allowed to run only this number of
|
295
|
+
seconds. Once this value is exceeded, the batch process is
|
296
|
+
stopped and treated as a failure.
|
297
|
+
wait : bool, optional
|
298
|
+
Whether block execution must wait until the batch process is
|
299
|
+
complete. The default is ``True``.
|
300
|
+
|
301
|
+
Returns
|
302
|
+
-------
|
303
|
+
list
|
304
|
+
A list containing the return values for the function.
|
305
|
+
Failed runs do not return an output. Because return values
|
306
|
+
are not necessarily in the same order as the iterable,
|
307
|
+
you might want to add some sort of tracker to the return
|
308
|
+
of your function.
|
309
|
+
|
310
|
+
Examples
|
311
|
+
--------
|
312
|
+
Run several input files while storing the final routine. Note
|
313
|
+
how the function to map must use ``mechanical`` as the first argument.
|
314
|
+
The function can have any number of additional arguments.
|
315
|
+
|
316
|
+
>>> from ansys.mechanical.core import LocalMechanicalPool
|
317
|
+
>>> pool = LocalMechanicalPool(10)
|
318
|
+
>>> completed_indices = []
|
319
|
+
>>> def function(mechanical, name, script):
|
320
|
+
# name, script = args
|
321
|
+
mechanical.clear()
|
322
|
+
output = mechanical.run_python_script(script)
|
323
|
+
return name, output
|
324
|
+
>>> inputs = [("first","2+3"), ("second", "3+4")]
|
325
|
+
>>> output = pool.map(function, inputs, progress_bar=False, wait=True)
|
326
|
+
[('first', '5'), ('second', '7')]
|
327
|
+
"""
|
328
|
+
# check if any instances are available
|
329
|
+
if not len(self): # pragma: no cover
|
330
|
+
# instances could still be spawning...
|
331
|
+
if not all(v is None for v in self._instances):
|
332
|
+
raise RuntimeError("No Mechanical instances available.")
|
333
|
+
|
334
|
+
results = []
|
335
|
+
|
336
|
+
if iterable is not None:
|
337
|
+
jobs_count = len(iterable)
|
338
|
+
else:
|
339
|
+
jobs_count = len(self)
|
340
|
+
|
341
|
+
pbar = None
|
342
|
+
if progress_bar:
|
343
|
+
if not _HAS_TQDM: # pragma: no cover
|
344
|
+
raise ModuleNotFoundError(
|
345
|
+
"To use the keyword argument 'progress_bar', you must have installed "
|
346
|
+
"the 'tqdm' package. To avoid this message, you can set 'progress_bar=False'."
|
347
|
+
)
|
348
|
+
|
349
|
+
pbar = tqdm(total=jobs_count, desc="Mechanical Running")
|
350
|
+
|
351
|
+
@threaded_daemon
|
352
|
+
def func_wrapper(obj, func, clear_at_start, timeout, args=None, name=""):
|
353
|
+
"""Expect obj to be an instance of Mechanical."""
|
354
|
+
LOG.debug(name)
|
355
|
+
complete = [False]
|
356
|
+
|
357
|
+
@threaded_daemon
|
358
|
+
def run(name_local=""):
|
359
|
+
LOG.debug(name_local)
|
360
|
+
|
361
|
+
if clear_at_start:
|
362
|
+
obj.clear()
|
363
|
+
|
364
|
+
if args is not None:
|
365
|
+
if isinstance(args, (tuple, list)):
|
366
|
+
results.append(func(obj, *args))
|
367
|
+
else:
|
368
|
+
results.append(func(obj, args))
|
369
|
+
else:
|
370
|
+
results.append(func(obj))
|
371
|
+
|
372
|
+
complete[0] = True
|
373
|
+
|
374
|
+
run_thread = run(name_local=name)
|
375
|
+
|
376
|
+
if timeout: # pragma: no cover
|
377
|
+
time_start = time.time()
|
378
|
+
while not complete[0]:
|
379
|
+
time.sleep(0.01)
|
380
|
+
if (time.time() - time_start) > timeout:
|
381
|
+
break
|
382
|
+
|
383
|
+
if not complete[0]:
|
384
|
+
LOG.error(f"Stopped instance due to a timeout of {timeout} seconds.")
|
385
|
+
obj.exit()
|
386
|
+
else:
|
387
|
+
run_thread.join()
|
388
|
+
if not complete[0]: # pragma: no cover
|
389
|
+
LOG.error("Stopped instance because running failed.")
|
390
|
+
try:
|
391
|
+
obj.exit()
|
392
|
+
except Exception as e:
|
393
|
+
LOG.error(f"Unexpected error while exiting: {e}")
|
394
|
+
|
395
|
+
obj.locked = False
|
396
|
+
if pbar:
|
397
|
+
pbar.update(1)
|
398
|
+
|
399
|
+
threads = []
|
400
|
+
if iterable is not None:
|
401
|
+
for args in iterable:
|
402
|
+
# grab the next available instance of mechanical
|
403
|
+
instance, i = self.next_available(return_index=True)
|
404
|
+
instance.locked = True
|
405
|
+
|
406
|
+
threads.append(
|
407
|
+
func_wrapper(
|
408
|
+
instance, func, clear_at_start, timeout, args, name=f"Map_Thread{i}"
|
409
|
+
)
|
410
|
+
)
|
411
|
+
else: # simply apply to all
|
412
|
+
for instance in self._instances:
|
413
|
+
if instance:
|
414
|
+
threads.append(
|
415
|
+
func_wrapper(instance, func, clear_at_start, timeout, name=f"Map_Thread")
|
416
|
+
)
|
417
|
+
|
418
|
+
if close_when_finished: # pragma: no cover
|
419
|
+
# start closing any instances that are not in execution
|
420
|
+
while not all(v is None for v in self._instances):
|
421
|
+
# grab the next available instance of mechanical and close it
|
422
|
+
instance, i = self.next_available(return_index=True)
|
423
|
+
self._instances[i] = None
|
424
|
+
|
425
|
+
try:
|
426
|
+
instance.exit()
|
427
|
+
except Exception as error: # pragma: no cover
|
428
|
+
LOG.error(f"Failed to close instance : str{error}.")
|
429
|
+
else:
|
430
|
+
# wait for all threads to complete
|
431
|
+
if wait:
|
432
|
+
[thread.join() for thread in threads]
|
433
|
+
|
434
|
+
return results
|
435
|
+
|
436
|
+
def run_batch(
|
437
|
+
self,
|
438
|
+
files,
|
439
|
+
clear_at_start=True,
|
440
|
+
progress_bar=True,
|
441
|
+
close_when_finished=False,
|
442
|
+
timeout=None,
|
443
|
+
wait=True,
|
444
|
+
):
|
445
|
+
"""Run a batch of input files on the Mechanical instances in the pool.
|
446
|
+
|
447
|
+
Parameters
|
448
|
+
----------
|
449
|
+
files : list
|
450
|
+
List of input files.
|
451
|
+
clear_at_start : bool, optional
|
452
|
+
Whether to clear Mechanical when execution starts. The default is
|
453
|
+
``True``. Setting this parameter to ``False`` might lead to
|
454
|
+
instability.
|
455
|
+
progress_bar : bool, optional
|
456
|
+
Whether to show a progress bar when running the batch of input
|
457
|
+
files. The default is ``True``, but the progress bar is not shown
|
458
|
+
when ``wait=False``.
|
459
|
+
close_when_finished : bool, optional
|
460
|
+
Whether to close the instances when running the batch
|
461
|
+
of input files is finished. The default is ``False``.
|
462
|
+
timeout : float, optional
|
463
|
+
Maximum runtime in seconds for each iteration. The default is
|
464
|
+
``None``, in which case there is no timeout. If you specify a
|
465
|
+
value, each iteration is allowed to run only this number of
|
466
|
+
seconds. Once this value is exceeded, the batch process is stopped
|
467
|
+
and treated as a failure.
|
468
|
+
wait : bool, optional
|
469
|
+
Whether block execution must wait until the batch process is complete.
|
470
|
+
The default is ``True``.
|
471
|
+
|
472
|
+
Returns
|
473
|
+
-------
|
474
|
+
list
|
475
|
+
List of text outputs from Mechanical for each batch run. The outputs
|
476
|
+
are not necessarily listed in the order of the inputs. Failed runs do
|
477
|
+
not return an output. Because the return outputs are not
|
478
|
+
necessarily in the same order as ``iterable``, you might
|
479
|
+
want to add some sort of tracker or note within the input files.
|
480
|
+
|
481
|
+
Examples
|
482
|
+
--------
|
483
|
+
Run 20 verification files on the pool.
|
484
|
+
|
485
|
+
>>> files = [f"test{index}.py" for index in range(1, 21)]
|
486
|
+
>>> outputs = pool.run_batch(files)
|
487
|
+
>>> len(outputs)
|
488
|
+
20
|
489
|
+
"""
|
490
|
+
# check all files exist before running
|
491
|
+
for filename in files:
|
492
|
+
if not os.path.isfile(filename):
|
493
|
+
raise FileNotFoundError("Unable to locate file %s" % filename)
|
494
|
+
|
495
|
+
def run_file(mechanical, input_file):
|
496
|
+
if clear_at_start:
|
497
|
+
mechanical.clear()
|
498
|
+
return mechanical.run_python_script_from_file(input_file)
|
499
|
+
|
500
|
+
return self.map(
|
501
|
+
run_file,
|
502
|
+
files,
|
503
|
+
progress_bar=progress_bar,
|
504
|
+
close_when_finished=close_when_finished,
|
505
|
+
timeout=timeout,
|
506
|
+
wait=wait,
|
507
|
+
)
|
508
|
+
|
509
|
+
def next_available(self, return_index=False):
|
510
|
+
"""Wait until a Mechanical instance is available and return this instance.
|
511
|
+
|
512
|
+
Parameters
|
513
|
+
----------
|
514
|
+
return_index : bool, optional
|
515
|
+
Whether to return the index along with the instance. The default
|
516
|
+
is ``False``.
|
517
|
+
|
518
|
+
Returns
|
519
|
+
-------
|
520
|
+
pymechanical.Mechanical
|
521
|
+
Instance of Mechanical.
|
522
|
+
|
523
|
+
int
|
524
|
+
Index within the pool of Mechanical instances. This index
|
525
|
+
is not returned by default.
|
526
|
+
|
527
|
+
Examples
|
528
|
+
--------
|
529
|
+
>>> mechanical = pool.next_available()
|
530
|
+
>>> mechanical
|
531
|
+
Ansys Mechanical [Ansys Mechanical Enterprise]
|
532
|
+
Product Version:251
|
533
|
+
Software build date: 11/27/2024 09:34:44
|
534
|
+
"""
|
535
|
+
# loop until the next instance is available
|
536
|
+
while True:
|
537
|
+
for i, instance in enumerate(self._instances):
|
538
|
+
# if encounter placeholder
|
539
|
+
if not instance: # pragma: no cover
|
540
|
+
continue
|
541
|
+
|
542
|
+
if not instance.locked and not instance._exited:
|
543
|
+
# any instance that is not running or exited
|
544
|
+
# should be available
|
545
|
+
if not instance.busy:
|
546
|
+
# double check that this instance is alive:
|
547
|
+
try:
|
548
|
+
instance._make_dummy_call()
|
549
|
+
except: # pragma: no cover
|
550
|
+
instance.exit()
|
551
|
+
continue
|
552
|
+
|
553
|
+
if return_index:
|
554
|
+
return instance, i
|
555
|
+
else:
|
556
|
+
return instance
|
557
|
+
# review - not needed
|
558
|
+
# else:
|
559
|
+
# instance._exited = True
|
560
|
+
|
561
|
+
def __del__(self):
|
562
|
+
"""Clean up when complete."""
|
563
|
+
print("pool:Automatic clean up.")
|
564
|
+
self.exit()
|
565
|
+
|
566
|
+
def exit(self, block=False):
|
567
|
+
"""Exit all Mechanical instances in the pool.
|
568
|
+
|
569
|
+
Parameters
|
570
|
+
----------
|
571
|
+
block : bool, optional
|
572
|
+
Whether to wait until all processes close before exiting
|
573
|
+
all instances in the pool. The default is ``False``.
|
574
|
+
|
575
|
+
Examples
|
576
|
+
--------
|
577
|
+
>>> pool.exit()
|
578
|
+
"""
|
579
|
+
self._active = False # Stops any active instance restart
|
580
|
+
|
581
|
+
@threaded
|
582
|
+
def threaded_exit(index, instance_local):
|
583
|
+
if instance_local:
|
584
|
+
try:
|
585
|
+
instance_local.exit()
|
586
|
+
except Exception as e: # pragma: no cover
|
587
|
+
LOG.error(f"Error while exiting instance {str(instance_local)}: {str(e)}")
|
588
|
+
self._instances[index] = None
|
589
|
+
LOG.debug(f"Exited instance: {str(instance_local)}")
|
590
|
+
|
591
|
+
threads = []
|
592
|
+
for i, instance in enumerate(self):
|
593
|
+
threads.append(threaded_exit(i, instance))
|
594
|
+
|
595
|
+
if block:
|
596
|
+
[thread.join() for thread in threads]
|
597
|
+
|
598
|
+
def __len__(self):
|
599
|
+
"""Get the number of instances in the pool."""
|
600
|
+
count = 0
|
601
|
+
for instance in self._instances:
|
602
|
+
if instance:
|
603
|
+
if not instance._exited:
|
604
|
+
count += 1
|
605
|
+
return count
|
606
|
+
|
607
|
+
def __getitem__(self, key):
|
608
|
+
"""Get an instance by an index."""
|
609
|
+
return self._instances[key]
|
610
|
+
|
611
|
+
def __iter__(self):
|
612
|
+
"""Iterate through active instances."""
|
613
|
+
for instance in self._instances:
|
614
|
+
if instance:
|
615
|
+
yield instance
|
616
|
+
|
617
|
+
@threaded_daemon
|
618
|
+
def _spawn_mechanical(self, index, port=None, pbar=None, name=""):
|
619
|
+
"""Spawn a Mechanical instance at an index.
|
620
|
+
|
621
|
+
Parameters
|
622
|
+
----------
|
623
|
+
index : int
|
624
|
+
Index to spawn the instance on.
|
625
|
+
port : int, optional
|
626
|
+
Port for the instance. The default is ``None``.
|
627
|
+
pbar :
|
628
|
+
The default is ``None``.
|
629
|
+
name : str, optional
|
630
|
+
Name for the instance. The default is ``""``.
|
631
|
+
"""
|
632
|
+
LOG.debug(name)
|
633
|
+
self._instances[index] = launch_mechanical(port=port, **self._spawn_kwargs)
|
634
|
+
# LOG.debug("Spawned instance %d. Name '%s'", index, name)
|
635
|
+
if pbar is not None:
|
636
|
+
pbar.update(1)
|
637
|
+
|
638
|
+
@threaded_daemon
|
639
|
+
def _spawn_mechanical_remote(self, index, pbar=None, name=""): # pragma: no cover
|
640
|
+
"""Spawn a Mechanical instance at an index.
|
641
|
+
|
642
|
+
Parameters
|
643
|
+
----------
|
644
|
+
index : int
|
645
|
+
Index to spawn the instance on.
|
646
|
+
pbar :
|
647
|
+
The default is ``None``.
|
648
|
+
name : str, optional
|
649
|
+
Name for the instance. The default is ``""``.
|
650
|
+
|
651
|
+
"""
|
652
|
+
LOG.debug(name)
|
653
|
+
self._instances[index] = launch_mechanical(**self._spawn_kwargs)
|
654
|
+
# LOG.debug("Spawned instance %d. Name '%s'", index, name)
|
655
|
+
if pbar is not None:
|
656
|
+
pbar.update(1)
|
657
|
+
|
658
|
+
@threaded_daemon
|
659
|
+
def _monitor_pool(self, refresh=1.0, name=""):
|
660
|
+
"""Check for instances within a pool that have exited (failed) and restart them.
|
661
|
+
|
662
|
+
Parameters
|
663
|
+
----------
|
664
|
+
refresh : float, optional
|
665
|
+
The default is ``1.0``.
|
666
|
+
name : str, optional
|
667
|
+
Name for the instance. The default is ``""``.
|
668
|
+
"""
|
669
|
+
LOG.debug(name)
|
670
|
+
while self._active:
|
671
|
+
for index, instance in enumerate(self._instances):
|
672
|
+
# encountered placeholder
|
673
|
+
if not instance: # pragma: no cover
|
674
|
+
continue
|
675
|
+
if instance._exited: # pragma: no cover
|
676
|
+
try:
|
677
|
+
if self._remote:
|
678
|
+
LOG.debug(
|
679
|
+
f"Restarting a Mechanical remote instance for index : {index}."
|
680
|
+
)
|
681
|
+
self._spawn_mechanical_remote(index, name=f"Instance {index}").join()
|
682
|
+
else:
|
683
|
+
# use the next port after the current available port
|
684
|
+
port = max(self.ports) + 1
|
685
|
+
LOG.debug(
|
686
|
+
f"Restarting a Mechanical instance for index : "
|
687
|
+
f"{index} on port: {port}."
|
688
|
+
)
|
689
|
+
self._spawn_mechanical(
|
690
|
+
index, port=port, name=f"Instance {index}"
|
691
|
+
).join()
|
692
|
+
except Exception as e:
|
693
|
+
LOG.error(e, exc_info=True)
|
694
|
+
time.sleep(refresh)
|
695
|
+
|
696
|
+
@property
|
697
|
+
def ports(self):
|
698
|
+
"""Get a list of the ports that are used.
|
699
|
+
|
700
|
+
Examples
|
701
|
+
--------
|
702
|
+
Get the list of ports used by the pool of Mechanical instances.
|
703
|
+
|
704
|
+
>>> pool.ports
|
705
|
+
[10001, 10002]
|
706
|
+
|
707
|
+
"""
|
708
|
+
return [inst._port for inst in self if inst is not None]
|
709
|
+
|
710
|
+
def __str__(self):
|
711
|
+
"""Get the string representation of this object."""
|
712
|
+
return "Mechanical pool with %d active instances" % len(self)
|