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.
Files changed (46) hide show
  1. ansys/mechanical/core/__init__.py +11 -4
  2. ansys/mechanical/core/_version.py +48 -47
  3. ansys/mechanical/core/embedding/__init__.py +1 -1
  4. ansys/mechanical/core/embedding/addins.py +1 -7
  5. ansys/mechanical/core/embedding/app.py +610 -281
  6. ansys/mechanical/core/embedding/app_libraries.py +24 -5
  7. ansys/mechanical/core/embedding/appdata.py +16 -4
  8. ansys/mechanical/core/embedding/background.py +106 -0
  9. ansys/mechanical/core/embedding/cleanup_gui.py +61 -0
  10. ansys/mechanical/core/embedding/enum_importer.py +2 -2
  11. ansys/mechanical/core/embedding/imports.py +27 -7
  12. ansys/mechanical/core/embedding/initializer.py +105 -53
  13. ansys/mechanical/core/embedding/loader.py +19 -9
  14. ansys/mechanical/core/embedding/logger/__init__.py +219 -216
  15. ansys/mechanical/core/embedding/logger/environ.py +1 -1
  16. ansys/mechanical/core/embedding/logger/linux_api.py +1 -1
  17. ansys/mechanical/core/embedding/logger/sinks.py +1 -1
  18. ansys/mechanical/core/embedding/logger/windows_api.py +2 -2
  19. ansys/mechanical/core/embedding/poster.py +38 -4
  20. ansys/mechanical/core/embedding/resolver.py +41 -44
  21. ansys/mechanical/core/embedding/runtime.py +1 -1
  22. ansys/mechanical/core/embedding/shims.py +9 -8
  23. ansys/mechanical/core/embedding/ui.py +228 -0
  24. ansys/mechanical/core/embedding/utils.py +1 -1
  25. ansys/mechanical/core/embedding/viz/__init__.py +1 -1
  26. ansys/mechanical/core/embedding/viz/{pyvista_plotter.py → embedding_plotter.py} +24 -8
  27. ansys/mechanical/core/embedding/viz/usd_converter.py +59 -25
  28. ansys/mechanical/core/embedding/viz/utils.py +32 -2
  29. ansys/mechanical/core/embedding/warnings.py +1 -1
  30. ansys/mechanical/core/errors.py +2 -1
  31. ansys/mechanical/core/examples/__init__.py +1 -1
  32. ansys/mechanical/core/examples/downloads.py +10 -5
  33. ansys/mechanical/core/feature_flags.py +51 -0
  34. ansys/mechanical/core/ide_config.py +212 -0
  35. ansys/mechanical/core/launcher.py +9 -9
  36. ansys/mechanical/core/logging.py +14 -2
  37. ansys/mechanical/core/mechanical.py +2324 -2237
  38. ansys/mechanical/core/misc.py +176 -176
  39. ansys/mechanical/core/pool.py +712 -712
  40. ansys/mechanical/core/run.py +321 -246
  41. {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/LICENSE +7 -7
  42. {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/METADATA +57 -56
  43. ansys_mechanical_core-0.11.12.dist-info/RECORD +45 -0
  44. {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/WHEEL +1 -1
  45. {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/entry_points.txt +1 -0
  46. ansys_mechanical_core-0.10.10.dist-info/RECORD +0 -40
@@ -1,712 +1,712 @@
1
- # Copyright (C) 2022 - 2024 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/v231/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/v231/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="231")
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 2023R1 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 "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) < 231:
195
- raise VersionError("A local Mechanical pool requires Mechanical 2023 R1 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
- f"To use the keyword argument 'progress_bar', you must have installed "
346
- f"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(f"Stopped instance because running failed.")
390
- try:
391
- obj.exit()
392
- except:
393
- pass
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:231
533
- Software build date:Wed Jul 13 14:29:54 2022
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
- pass
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)