ansys-mechanical-core 0.10.11__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 +2 -2
- ansys/mechanical/core/embedding/appdata.py +11 -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 +104 -51
- ansys/mechanical/core/embedding/loader.py +1 -1
- 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 +34 -2
- 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 -12
- 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 +1 -1
- 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 -291
- {ansys_mechanical_core-0.10.11.dist-info → ansys_mechanical_core-0.11.12.dist-info}/LICENSE +1 -1
- {ansys_mechanical_core-0.10.11.dist-info → ansys_mechanical_core-0.11.12.dist-info}/METADATA +55 -54
- ansys_mechanical_core-0.11.12.dist-info/RECORD +45 -0
- {ansys_mechanical_core-0.10.11.dist-info → ansys_mechanical_core-0.11.12.dist-info}/WHEEL +1 -1
- {ansys_mechanical_core-0.10.11.dist-info → ansys_mechanical_core-0.11.12.dist-info}/entry_points.txt +1 -0
- ansys_mechanical_core-0.10.11.dist-info/RECORD +0 -41
@@ -1,2237 +1,2324 @@
|
|
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
|
-
"""Connect to Mechanical gRPC server and issues commands."""
|
24
|
-
import atexit
|
25
|
-
from contextlib import closing
|
26
|
-
import datetime
|
27
|
-
import fnmatch
|
28
|
-
from functools import wraps
|
29
|
-
import glob
|
30
|
-
import os
|
31
|
-
import pathlib
|
32
|
-
import socket
|
33
|
-
import threading
|
34
|
-
import time
|
35
|
-
import weakref
|
36
|
-
|
37
|
-
import ansys.api.mechanical.v0.mechanical_pb2 as mechanical_pb2
|
38
|
-
import ansys.api.mechanical.v0.mechanical_pb2_grpc as mechanical_pb2_grpc
|
39
|
-
import ansys.platform.instancemanagement as pypim
|
40
|
-
from ansys.platform.instancemanagement import Instance
|
41
|
-
import ansys.tools.path as atp
|
42
|
-
import grpc
|
43
|
-
|
44
|
-
import ansys.mechanical.core as pymechanical
|
45
|
-
from ansys.mechanical.core import LOG
|
46
|
-
from ansys.mechanical.core.errors import (
|
47
|
-
MechanicalExitedError,
|
48
|
-
MechanicalRuntimeError,
|
49
|
-
VersionError,
|
50
|
-
protect_grpc,
|
51
|
-
)
|
52
|
-
from ansys.mechanical.core.launcher import MechanicalLauncher
|
53
|
-
from ansys.mechanical.core.misc import (
|
54
|
-
check_valid_ip,
|
55
|
-
check_valid_port,
|
56
|
-
check_valid_start_instance,
|
57
|
-
threaded,
|
58
|
-
)
|
59
|
-
|
60
|
-
# Checking if tqdm is installed.
|
61
|
-
# If it is, the default value for progress_bar is true.
|
62
|
-
try:
|
63
|
-
from tqdm import tqdm
|
64
|
-
|
65
|
-
_HAS_TQDM = True
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
"""
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
def
|
159
|
-
"""
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
"""
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
def
|
223
|
-
"""
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
""
|
249
|
-
return
|
250
|
-
|
251
|
-
|
252
|
-
def
|
253
|
-
"""
|
254
|
-
|
255
|
-
Deprecated - use `ansys.tools.path.
|
256
|
-
"""
|
257
|
-
return atp.
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
"
|
262
|
-
|
263
|
-
|
264
|
-
"
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
""
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
``
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
>>> mechanical = pymechanical.Mechanical(
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
self.
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
self.
|
400
|
-
self.
|
401
|
-
|
402
|
-
self.
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
self.
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
self.
|
415
|
-
|
416
|
-
self.
|
417
|
-
self.
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
#
|
444
|
-
self.
|
445
|
-
|
446
|
-
self.
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
self.
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
self.
|
598
|
-
|
599
|
-
|
600
|
-
self.
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
)
|
608
|
-
self.
|
609
|
-
self.
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
self.
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
thread.
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
#
|
733
|
-
#
|
734
|
-
#
|
735
|
-
#
|
736
|
-
#
|
737
|
-
#
|
738
|
-
#
|
739
|
-
#
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
'
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
815
|
-
|
816
|
-
self.
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
f"
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
Return a
|
936
|
-
|
937
|
-
>>> mechanical.run_python_script('
|
938
|
-
'
|
939
|
-
|
940
|
-
Return
|
941
|
-
|
942
|
-
>>> mechanical.run_python_script('ExtAPI.DataModel.Project')
|
943
|
-
''
|
944
|
-
|
945
|
-
Return an empty string
|
946
|
-
|
947
|
-
>>> mechanical.run_python_script('
|
948
|
-
''
|
949
|
-
|
950
|
-
Return
|
951
|
-
|
952
|
-
>>>
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
>>>
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
>>> script
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
'
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
>>> script
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
979
|
-
|
980
|
-
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
|
992
|
-
|
993
|
-
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
1001
|
-
|
1002
|
-
|
1003
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
1007
|
-
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
1042
|
-
|
1043
|
-
|
1044
|
-
|
1045
|
-
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
if
|
1065
|
-
self.
|
1066
|
-
return
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
self.
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1079
|
-
|
1080
|
-
|
1081
|
-
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
|
1089
|
-
|
1090
|
-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1096
|
-
self.log_debug("PyPIM delete has
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
self.log_debug("
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
|
1129
|
-
|
1130
|
-
|
1131
|
-
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
|
1150
|
-
self.
|
1151
|
-
|
1152
|
-
if
|
1153
|
-
|
1154
|
-
|
1155
|
-
self.
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
|
1212
|
-
|
1213
|
-
|
1214
|
-
|
1215
|
-
if
|
1216
|
-
pbar
|
1217
|
-
|
1218
|
-
|
1219
|
-
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1228
|
-
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1241
|
-
|
1242
|
-
|
1243
|
-
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
)
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
|
1269
|
-
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
1288
|
-
|
1289
|
-
|
1290
|
-
|
1291
|
-
|
1292
|
-
|
1293
|
-
|
1294
|
-
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
1301
|
-
|
1302
|
-
|
1303
|
-
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1307
|
-
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
|
1318
|
-
|
1319
|
-
|
1320
|
-
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
current
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
1355
|
-
|
1356
|
-
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
|
1366
|
-
|
1367
|
-
|
1368
|
-
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
1389
|
-
|
1390
|
-
|
1391
|
-
|
1392
|
-
|
1393
|
-
|
1394
|
-
|
1395
|
-
|
1396
|
-
|
1397
|
-
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1401
|
-
|
1402
|
-
|
1403
|
-
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1408
|
-
|
1409
|
-
|
1410
|
-
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1417
|
-
|
1418
|
-
|
1419
|
-
|
1420
|
-
|
1421
|
-
|
1422
|
-
|
1423
|
-
|
1424
|
-
|
1425
|
-
|
1426
|
-
|
1427
|
-
|
1428
|
-
|
1429
|
-
|
1430
|
-
#
|
1431
|
-
|
1432
|
-
|
1433
|
-
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1438
|
-
|
1439
|
-
|
1440
|
-
|
1441
|
-
|
1442
|
-
|
1443
|
-
|
1444
|
-
|
1445
|
-
|
1446
|
-
|
1447
|
-
|
1448
|
-
|
1449
|
-
|
1450
|
-
|
1451
|
-
|
1452
|
-
|
1453
|
-
|
1454
|
-
|
1455
|
-
file.
|
1456
|
-
|
1457
|
-
|
1458
|
-
|
1459
|
-
|
1460
|
-
|
1461
|
-
|
1462
|
-
|
1463
|
-
|
1464
|
-
|
1465
|
-
|
1466
|
-
|
1467
|
-
|
1468
|
-
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1473
|
-
|
1474
|
-
|
1475
|
-
|
1476
|
-
|
1477
|
-
|
1478
|
-
|
1479
|
-
|
1480
|
-
|
1481
|
-
|
1482
|
-
|
1483
|
-
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
|
1491
|
-
"
|
1492
|
-
|
1493
|
-
|
1494
|
-
|
1495
|
-
|
1496
|
-
|
1497
|
-
|
1498
|
-
|
1499
|
-
|
1500
|
-
|
1501
|
-
|
1502
|
-
|
1503
|
-
|
1504
|
-
|
1505
|
-
|
1506
|
-
|
1507
|
-
|
1508
|
-
|
1509
|
-
|
1510
|
-
|
1511
|
-
|
1512
|
-
|
1513
|
-
|
1514
|
-
|
1515
|
-
|
1516
|
-
|
1517
|
-
|
1518
|
-
|
1519
|
-
|
1520
|
-
|
1521
|
-
|
1522
|
-
|
1523
|
-
|
1524
|
-
|
1525
|
-
|
1526
|
-
|
1527
|
-
|
1528
|
-
|
1529
|
-
|
1530
|
-
|
1531
|
-
|
1532
|
-
|
1533
|
-
|
1534
|
-
|
1535
|
-
|
1536
|
-
|
1537
|
-
|
1538
|
-
|
1539
|
-
|
1540
|
-
|
1541
|
-
|
1542
|
-
|
1543
|
-
|
1544
|
-
|
1545
|
-
|
1546
|
-
|
1547
|
-
|
1548
|
-
|
1549
|
-
|
1550
|
-
|
1551
|
-
|
1552
|
-
|
1553
|
-
|
1554
|
-
|
1555
|
-
|
1556
|
-
|
1557
|
-
|
1558
|
-
|
1559
|
-
|
1560
|
-
|
1561
|
-
|
1562
|
-
|
1563
|
-
|
1564
|
-
|
1565
|
-
|
1566
|
-
|
1567
|
-
|
1568
|
-
|
1569
|
-
|
1570
|
-
|
1571
|
-
|
1572
|
-
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1576
|
-
|
1577
|
-
|
1578
|
-
|
1579
|
-
|
1580
|
-
|
1581
|
-
|
1582
|
-
|
1583
|
-
|
1584
|
-
|
1585
|
-
|
1586
|
-
|
1587
|
-
|
1588
|
-
|
1589
|
-
|
1590
|
-
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
1597
|
-
|
1598
|
-
|
1599
|
-
|
1600
|
-
|
1601
|
-
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1606
|
-
|
1607
|
-
|
1608
|
-
|
1609
|
-
|
1610
|
-
|
1611
|
-
|
1612
|
-
|
1613
|
-
|
1614
|
-
|
1615
|
-
|
1616
|
-
|
1617
|
-
|
1618
|
-
|
1619
|
-
|
1620
|
-
|
1621
|
-
|
1622
|
-
|
1623
|
-
|
1624
|
-
|
1625
|
-
|
1626
|
-
|
1627
|
-
|
1628
|
-
|
1629
|
-
|
1630
|
-
)
|
1631
|
-
|
1632
|
-
|
1633
|
-
|
1634
|
-
|
1635
|
-
|
1636
|
-
|
1637
|
-
|
1638
|
-
|
1639
|
-
|
1640
|
-
Clear the database.
|
1641
|
-
|
1642
|
-
|
1643
|
-
|
1644
|
-
|
1645
|
-
|
1646
|
-
|
1647
|
-
|
1648
|
-
|
1649
|
-
|
1650
|
-
|
1651
|
-
|
1652
|
-
|
1653
|
-
|
1654
|
-
self.
|
1655
|
-
|
1656
|
-
|
1657
|
-
|
1658
|
-
|
1659
|
-
|
1660
|
-
|
1661
|
-
|
1662
|
-
|
1663
|
-
#
|
1664
|
-
text_file
|
1665
|
-
|
1666
|
-
|
1667
|
-
|
1668
|
-
|
1669
|
-
|
1670
|
-
|
1671
|
-
|
1672
|
-
|
1673
|
-
|
1674
|
-
|
1675
|
-
|
1676
|
-
|
1677
|
-
|
1678
|
-
|
1679
|
-
|
1680
|
-
|
1681
|
-
|
1682
|
-
|
1683
|
-
|
1684
|
-
|
1685
|
-
|
1686
|
-
|
1687
|
-
|
1688
|
-
|
1689
|
-
|
1690
|
-
|
1691
|
-
|
1692
|
-
|
1693
|
-
|
1694
|
-
|
1695
|
-
|
1696
|
-
request
|
1697
|
-
|
1698
|
-
|
1699
|
-
|
1700
|
-
|
1701
|
-
|
1702
|
-
|
1703
|
-
|
1704
|
-
|
1705
|
-
|
1706
|
-
|
1707
|
-
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
1712
|
-
|
1713
|
-
|
1714
|
-
|
1715
|
-
|
1716
|
-
|
1717
|
-
|
1718
|
-
|
1719
|
-
|
1720
|
-
|
1721
|
-
|
1722
|
-
|
1723
|
-
|
1724
|
-
|
1725
|
-
|
1726
|
-
|
1727
|
-
|
1728
|
-
|
1729
|
-
|
1730
|
-
|
1731
|
-
|
1732
|
-
|
1733
|
-
|
1734
|
-
|
1735
|
-
|
1736
|
-
|
1737
|
-
|
1738
|
-
|
1739
|
-
|
1740
|
-
|
1741
|
-
|
1742
|
-
|
1743
|
-
|
1744
|
-
|
1745
|
-
|
1746
|
-
Log
|
1747
|
-
|
1748
|
-
>>> mechanical.log_message('
|
1749
|
-
|
1750
|
-
|
1751
|
-
|
1752
|
-
|
1753
|
-
|
1754
|
-
|
1755
|
-
|
1756
|
-
self.
|
1757
|
-
elif log_level == "
|
1758
|
-
self.
|
1759
|
-
|
1760
|
-
|
1761
|
-
|
1762
|
-
|
1763
|
-
|
1764
|
-
|
1765
|
-
|
1766
|
-
|
1767
|
-
|
1768
|
-
|
1769
|
-
|
1770
|
-
|
1771
|
-
|
1772
|
-
|
1773
|
-
|
1774
|
-
|
1775
|
-
|
1776
|
-
|
1777
|
-
|
1778
|
-
|
1779
|
-
|
1780
|
-
|
1781
|
-
|
1782
|
-
|
1783
|
-
|
1784
|
-
|
1785
|
-
|
1786
|
-
|
1787
|
-
|
1788
|
-
|
1789
|
-
|
1790
|
-
|
1791
|
-
|
1792
|
-
|
1793
|
-
|
1794
|
-
|
1795
|
-
|
1796
|
-
|
1797
|
-
|
1798
|
-
|
1799
|
-
def
|
1800
|
-
|
1801
|
-
|
1802
|
-
|
1803
|
-
|
1804
|
-
|
1805
|
-
|
1806
|
-
|
1807
|
-
|
1808
|
-
|
1809
|
-
self.
|
1810
|
-
|
1811
|
-
|
1812
|
-
|
1813
|
-
|
1814
|
-
|
1815
|
-
|
1816
|
-
|
1817
|
-
|
1818
|
-
|
1819
|
-
|
1820
|
-
|
1821
|
-
|
1822
|
-
|
1823
|
-
|
1824
|
-
|
1825
|
-
|
1826
|
-
|
1827
|
-
|
1828
|
-
|
1829
|
-
|
1830
|
-
|
1831
|
-
|
1832
|
-
|
1833
|
-
|
1834
|
-
|
1835
|
-
|
1836
|
-
|
1837
|
-
|
1838
|
-
|
1839
|
-
|
1840
|
-
|
1841
|
-
|
1842
|
-
|
1843
|
-
|
1844
|
-
|
1845
|
-
|
1846
|
-
|
1847
|
-
|
1848
|
-
|
1849
|
-
|
1850
|
-
|
1851
|
-
|
1852
|
-
|
1853
|
-
|
1854
|
-
|
1855
|
-
|
1856
|
-
|
1857
|
-
|
1858
|
-
|
1859
|
-
|
1860
|
-
|
1861
|
-
|
1862
|
-
|
1863
|
-
|
1864
|
-
|
1865
|
-
|
1866
|
-
|
1867
|
-
|
1868
|
-
|
1869
|
-
|
1870
|
-
|
1871
|
-
|
1872
|
-
|
1873
|
-
|
1874
|
-
|
1875
|
-
|
1876
|
-
|
1877
|
-
|
1878
|
-
|
1879
|
-
|
1880
|
-
|
1881
|
-
|
1882
|
-
|
1883
|
-
|
1884
|
-
|
1885
|
-
|
1886
|
-
|
1887
|
-
|
1888
|
-
|
1889
|
-
|
1890
|
-
|
1891
|
-
|
1892
|
-
|
1893
|
-
|
1894
|
-
|
1895
|
-
|
1896
|
-
|
1897
|
-
|
1898
|
-
|
1899
|
-
|
1900
|
-
|
1901
|
-
|
1902
|
-
|
1903
|
-
>>>
|
1904
|
-
|
1905
|
-
|
1906
|
-
|
1907
|
-
|
1908
|
-
|
1909
|
-
|
1910
|
-
|
1911
|
-
#
|
1912
|
-
|
1913
|
-
|
1914
|
-
|
1915
|
-
|
1916
|
-
|
1917
|
-
|
1918
|
-
|
1919
|
-
|
1920
|
-
|
1921
|
-
|
1922
|
-
|
1923
|
-
|
1924
|
-
|
1925
|
-
)
|
1926
|
-
|
1927
|
-
|
1928
|
-
|
1929
|
-
|
1930
|
-
|
1931
|
-
|
1932
|
-
|
1933
|
-
|
1934
|
-
|
1935
|
-
|
1936
|
-
|
1937
|
-
|
1938
|
-
|
1939
|
-
|
1940
|
-
|
1941
|
-
|
1942
|
-
|
1943
|
-
|
1944
|
-
|
1945
|
-
|
1946
|
-
|
1947
|
-
|
1948
|
-
|
1949
|
-
|
1950
|
-
|
1951
|
-
|
1952
|
-
|
1953
|
-
|
1954
|
-
|
1955
|
-
|
1956
|
-
|
1957
|
-
|
1958
|
-
|
1959
|
-
|
1960
|
-
|
1961
|
-
|
1962
|
-
|
1963
|
-
|
1964
|
-
|
1965
|
-
|
1966
|
-
|
1967
|
-
|
1968
|
-
|
1969
|
-
|
1970
|
-
|
1971
|
-
|
1972
|
-
|
1973
|
-
|
1974
|
-
|
1975
|
-
|
1976
|
-
|
1977
|
-
|
1978
|
-
|
1979
|
-
|
1980
|
-
|
1981
|
-
|
1982
|
-
|
1983
|
-
|
1984
|
-
|
1985
|
-
|
1986
|
-
|
1987
|
-
|
1988
|
-
|
1989
|
-
|
1990
|
-
|
1991
|
-
|
1992
|
-
|
1993
|
-
|
1994
|
-
|
1995
|
-
|
1996
|
-
|
1997
|
-
|
1998
|
-
|
1999
|
-
|
2000
|
-
|
2001
|
-
|
2002
|
-
|
2003
|
-
|
2004
|
-
|
2005
|
-
|
2006
|
-
|
2007
|
-
|
2008
|
-
|
2009
|
-
|
2010
|
-
|
2011
|
-
|
2012
|
-
|
2013
|
-
|
2014
|
-
|
2015
|
-
|
2016
|
-
|
2017
|
-
|
2018
|
-
|
2019
|
-
|
2020
|
-
|
2021
|
-
|
2022
|
-
|
2023
|
-
|
2024
|
-
|
2025
|
-
The default
|
2026
|
-
|
2027
|
-
|
2028
|
-
|
2029
|
-
|
2030
|
-
|
2031
|
-
|
2032
|
-
|
2033
|
-
|
2034
|
-
default
|
2035
|
-
|
2036
|
-
|
2037
|
-
|
2038
|
-
|
2039
|
-
|
2040
|
-
|
2041
|
-
|
2042
|
-
|
2043
|
-
|
2044
|
-
|
2045
|
-
|
2046
|
-
|
2047
|
-
|
2048
|
-
|
2049
|
-
|
2050
|
-
|
2051
|
-
|
2052
|
-
|
2053
|
-
|
2054
|
-
|
2055
|
-
|
2056
|
-
|
2057
|
-
|
2058
|
-
|
2059
|
-
|
2060
|
-
|
2061
|
-
|
2062
|
-
|
2063
|
-
|
2064
|
-
|
2065
|
-
|
2066
|
-
|
2067
|
-
|
2068
|
-
|
2069
|
-
|
2070
|
-
|
2071
|
-
|
2072
|
-
|
2073
|
-
|
2074
|
-
|
2075
|
-
|
2076
|
-
|
2077
|
-
|
2078
|
-
|
2079
|
-
|
2080
|
-
|
2081
|
-
|
2082
|
-
|
2083
|
-
|
2084
|
-
|
2085
|
-
|
2086
|
-
|
2087
|
-
|
2088
|
-
>>>
|
2089
|
-
|
2090
|
-
|
2091
|
-
|
2092
|
-
|
2093
|
-
|
2094
|
-
|
2095
|
-
|
2096
|
-
|
2097
|
-
|
2098
|
-
|
2099
|
-
|
2100
|
-
|
2101
|
-
|
2102
|
-
|
2103
|
-
|
2104
|
-
|
2105
|
-
|
2106
|
-
|
2107
|
-
|
2108
|
-
|
2109
|
-
|
2110
|
-
|
2111
|
-
|
2112
|
-
|
2113
|
-
|
2114
|
-
|
2115
|
-
|
2116
|
-
|
2117
|
-
|
2118
|
-
|
2119
|
-
|
2120
|
-
|
2121
|
-
|
2122
|
-
|
2123
|
-
|
2124
|
-
|
2125
|
-
|
2126
|
-
|
2127
|
-
|
2128
|
-
|
2129
|
-
|
2130
|
-
|
2131
|
-
|
2132
|
-
|
2133
|
-
|
2134
|
-
# when
|
2135
|
-
#
|
2136
|
-
#
|
2137
|
-
|
2138
|
-
|
2139
|
-
|
2140
|
-
|
2141
|
-
|
2142
|
-
|
2143
|
-
|
2144
|
-
|
2145
|
-
|
2146
|
-
|
2147
|
-
|
2148
|
-
|
2149
|
-
|
2150
|
-
|
2151
|
-
mechanical
|
2152
|
-
|
2153
|
-
|
2154
|
-
|
2155
|
-
|
2156
|
-
|
2157
|
-
|
2158
|
-
|
2159
|
-
|
2160
|
-
|
2161
|
-
|
2162
|
-
|
2163
|
-
|
2164
|
-
|
2165
|
-
|
2166
|
-
mechanical
|
2167
|
-
|
2168
|
-
|
2169
|
-
|
2170
|
-
|
2171
|
-
|
2172
|
-
|
2173
|
-
|
2174
|
-
|
2175
|
-
|
2176
|
-
|
2177
|
-
|
2178
|
-
|
2179
|
-
|
2180
|
-
|
2181
|
-
|
2182
|
-
|
2183
|
-
|
2184
|
-
|
2185
|
-
|
2186
|
-
|
2187
|
-
|
2188
|
-
|
2189
|
-
|
2190
|
-
if
|
2191
|
-
|
2192
|
-
|
2193
|
-
|
2194
|
-
|
2195
|
-
|
2196
|
-
|
2197
|
-
|
2198
|
-
|
2199
|
-
|
2200
|
-
|
2201
|
-
|
2202
|
-
|
2203
|
-
|
2204
|
-
|
2205
|
-
|
2206
|
-
|
2207
|
-
|
2208
|
-
|
2209
|
-
|
2210
|
-
|
2211
|
-
|
2212
|
-
|
2213
|
-
|
2214
|
-
|
2215
|
-
|
2216
|
-
|
2217
|
-
|
2218
|
-
|
2219
|
-
|
2220
|
-
|
2221
|
-
|
2222
|
-
|
2223
|
-
|
2224
|
-
|
2225
|
-
|
2226
|
-
|
2227
|
-
|
2228
|
-
|
2229
|
-
|
2230
|
-
|
2231
|
-
|
2232
|
-
|
2233
|
-
|
2234
|
-
|
2235
|
-
|
2236
|
-
|
2237
|
-
|
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
|
+
"""Connect to Mechanical gRPC server and issues commands."""
|
24
|
+
import atexit
|
25
|
+
from contextlib import closing
|
26
|
+
import datetime
|
27
|
+
import fnmatch
|
28
|
+
from functools import wraps
|
29
|
+
import glob
|
30
|
+
import os
|
31
|
+
import pathlib
|
32
|
+
import socket
|
33
|
+
import threading
|
34
|
+
import time
|
35
|
+
import weakref
|
36
|
+
|
37
|
+
import ansys.api.mechanical.v0.mechanical_pb2 as mechanical_pb2
|
38
|
+
import ansys.api.mechanical.v0.mechanical_pb2_grpc as mechanical_pb2_grpc
|
39
|
+
import ansys.platform.instancemanagement as pypim
|
40
|
+
from ansys.platform.instancemanagement import Instance
|
41
|
+
import ansys.tools.path as atp
|
42
|
+
import grpc
|
43
|
+
|
44
|
+
import ansys.mechanical.core as pymechanical
|
45
|
+
from ansys.mechanical.core import LOG
|
46
|
+
from ansys.mechanical.core.errors import (
|
47
|
+
MechanicalExitedError,
|
48
|
+
MechanicalRuntimeError,
|
49
|
+
VersionError,
|
50
|
+
protect_grpc,
|
51
|
+
)
|
52
|
+
from ansys.mechanical.core.launcher import MechanicalLauncher
|
53
|
+
from ansys.mechanical.core.misc import (
|
54
|
+
check_valid_ip,
|
55
|
+
check_valid_port,
|
56
|
+
check_valid_start_instance,
|
57
|
+
threaded,
|
58
|
+
)
|
59
|
+
|
60
|
+
# Checking if tqdm is installed.
|
61
|
+
# If it is, the default value for progress_bar is true.
|
62
|
+
try:
|
63
|
+
from tqdm import tqdm
|
64
|
+
|
65
|
+
_HAS_TQDM = True
|
66
|
+
"""Whether or not tqdm is installed."""
|
67
|
+
except ModuleNotFoundError: # pragma: no cover
|
68
|
+
_HAS_TQDM = False
|
69
|
+
|
70
|
+
# Default 256 MB message length
|
71
|
+
MAX_MESSAGE_LENGTH = int(os.environ.get("PYMECHANICAL_MAX_MESSAGE_LENGTH", 256 * 1024**2))
|
72
|
+
"""Default message length."""
|
73
|
+
|
74
|
+
# Chunk sizes for streaming and file streaming
|
75
|
+
DEFAULT_CHUNK_SIZE = 256 * 1024 # 256 kB
|
76
|
+
"""Default chunk size."""
|
77
|
+
DEFAULT_FILE_CHUNK_SIZE = 1024 * 1024 # 1MB
|
78
|
+
"""Default file chunk size."""
|
79
|
+
|
80
|
+
|
81
|
+
def setup_logger(loglevel="INFO", log_file=True, mechanical_instance=None):
|
82
|
+
"""Initialize the logger for the given mechanical instance."""
|
83
|
+
# Return existing log if this function has already been called
|
84
|
+
if hasattr(setup_logger, "log"):
|
85
|
+
return setup_logger.log
|
86
|
+
else:
|
87
|
+
setup_logger.log = LOG.add_instance_logger("Mechanical", mechanical_instance)
|
88
|
+
|
89
|
+
setup_logger.log.setLevel(loglevel)
|
90
|
+
|
91
|
+
if log_file:
|
92
|
+
if isinstance(log_file, str):
|
93
|
+
setup_logger.log.log_to_file(filename=log_file, level=loglevel)
|
94
|
+
|
95
|
+
return setup_logger.log
|
96
|
+
|
97
|
+
|
98
|
+
def suppress_logging(func):
|
99
|
+
"""Decorate a function to suppress the logging for a Mechanical instance."""
|
100
|
+
|
101
|
+
@wraps(func)
|
102
|
+
def wrapper(*args, **kwargs):
|
103
|
+
mechanical = args[0]
|
104
|
+
prior_log_level = mechanical.log.level
|
105
|
+
if prior_log_level != "CRITICAL":
|
106
|
+
mechanical.set_log_level("CRITICAL")
|
107
|
+
|
108
|
+
out = func(*args, **kwargs)
|
109
|
+
|
110
|
+
if prior_log_level != "CRITICAL":
|
111
|
+
mechanical.set_log_level(prior_log_level)
|
112
|
+
|
113
|
+
return out
|
114
|
+
|
115
|
+
return wrapper
|
116
|
+
|
117
|
+
|
118
|
+
LOCALHOST = "127.0.0.1"
|
119
|
+
"""Localhost address."""
|
120
|
+
|
121
|
+
MECHANICAL_DEFAULT_PORT = 10000
|
122
|
+
"""Default Mechanical port."""
|
123
|
+
|
124
|
+
GALLERY_INSTANCE = [None]
|
125
|
+
"""List of gallery instances."""
|
126
|
+
|
127
|
+
|
128
|
+
def _cleanup_gallery_instance(): # pragma: no cover
|
129
|
+
"""Clean up any leftover instances of Mechanical from building the gallery."""
|
130
|
+
if GALLERY_INSTANCE[0] is not None:
|
131
|
+
mechanical = Mechanical(
|
132
|
+
ip=GALLERY_INSTANCE[0]["ip"],
|
133
|
+
port=GALLERY_INSTANCE[0]["port"],
|
134
|
+
)
|
135
|
+
mechanical.exit(force=True)
|
136
|
+
|
137
|
+
|
138
|
+
atexit.register(_cleanup_gallery_instance)
|
139
|
+
|
140
|
+
|
141
|
+
def port_in_use(port, host=LOCALHOST):
|
142
|
+
"""Check whether a port is in use at the given host.
|
143
|
+
|
144
|
+
You must actually *bind* the address. Just checking if you can create
|
145
|
+
a socket is insufficient because it is possible to run into permission
|
146
|
+
errors like::
|
147
|
+
|
148
|
+
An attempt was made to access a socket in a way forbidden by its
|
149
|
+
access permissions.
|
150
|
+
"""
|
151
|
+
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
152
|
+
if sock.connect_ex((host, port)) == 0:
|
153
|
+
return True
|
154
|
+
else:
|
155
|
+
return False
|
156
|
+
|
157
|
+
|
158
|
+
def check_ports(port_range, ip="localhost"):
|
159
|
+
"""Check the state of ports in a port range."""
|
160
|
+
ports = {}
|
161
|
+
for port in port_range:
|
162
|
+
ports[port] = port_in_use(port, ip)
|
163
|
+
return ports
|
164
|
+
|
165
|
+
|
166
|
+
def close_all_local_instances(port_range=None, use_thread=True):
|
167
|
+
"""Close all Mechanical instances within a port range.
|
168
|
+
|
169
|
+
You can use this method when cleaning up from a failed pool or
|
170
|
+
batch run.
|
171
|
+
|
172
|
+
Parameters
|
173
|
+
----------
|
174
|
+
port_range : list, optional
|
175
|
+
List of a range of ports to use when cleaning up Mechanical. The
|
176
|
+
default is ``None``, in which case the ports managed by
|
177
|
+
PyMechanical are used.
|
178
|
+
|
179
|
+
use_thread : bool, optional
|
180
|
+
Whether to use threads to close the Mechanical instances.
|
181
|
+
The default is ``True``. So this call will return immediately.
|
182
|
+
|
183
|
+
Examples
|
184
|
+
--------
|
185
|
+
Close all Mechanical instances connected on local ports.
|
186
|
+
|
187
|
+
>>> import ansys.mechanical.core as pymechanical
|
188
|
+
>>> pymechanical.close_all_local_instances()
|
189
|
+
|
190
|
+
"""
|
191
|
+
if port_range is None:
|
192
|
+
port_range = pymechanical.LOCAL_PORTS
|
193
|
+
|
194
|
+
@threaded
|
195
|
+
def close_mechanical_threaded(port, name="Closing Mechanical instance in a thread"):
|
196
|
+
close_mechanical(port, name)
|
197
|
+
|
198
|
+
def close_mechanical(port, name="Closing Mechanical instance"):
|
199
|
+
try:
|
200
|
+
mechanical = Mechanical(port=port)
|
201
|
+
LOG.debug(f"{name}: {mechanical.name}.")
|
202
|
+
mechanical.exit(force=True)
|
203
|
+
except OSError: # pragma: no cover
|
204
|
+
pass
|
205
|
+
|
206
|
+
ports = check_ports(port_range)
|
207
|
+
for port_temp, state in ports.items():
|
208
|
+
if state:
|
209
|
+
if use_thread:
|
210
|
+
close_mechanical_threaded(port_temp)
|
211
|
+
else:
|
212
|
+
close_mechanical(port_temp)
|
213
|
+
|
214
|
+
|
215
|
+
def create_ip_file(ip, path):
|
216
|
+
"""Create the ``mylocal.ip`` file needed to change the IP address of the gRPC server."""
|
217
|
+
file_name = os.path.join(path, "mylocal.ip")
|
218
|
+
with open(file_name, "w", encoding="utf-8") as f:
|
219
|
+
f.write(ip)
|
220
|
+
|
221
|
+
|
222
|
+
def get_mechanical_path(allow_input=True):
|
223
|
+
"""Get path.
|
224
|
+
|
225
|
+
Deprecated - use `ansys.tools.path.get_mechanical_path` instead
|
226
|
+
"""
|
227
|
+
return atp.get_mechanical_path(allow_input)
|
228
|
+
|
229
|
+
|
230
|
+
def check_valid_mechanical():
|
231
|
+
"""Change to see if the default Mechanical path is valid.
|
232
|
+
|
233
|
+
Example (windows)
|
234
|
+
-----------------
|
235
|
+
|
236
|
+
>>> from ansys.mechanical.core import mechanical
|
237
|
+
>>> from ansys.tools.path import change_default_mechanical_path
|
238
|
+
>>> mechanical_path = 'C:/Program Files/ANSYS Inc/v251/aisol/bin/win64/AnsysWBU.exe'
|
239
|
+
>>> change_default_mechanical_path(mechanical_pth)
|
240
|
+
>>> mechanical.check_valid_mechanical()
|
241
|
+
True
|
242
|
+
|
243
|
+
|
244
|
+
"""
|
245
|
+
mechanical_path = atp.get_mechanical_path(False)
|
246
|
+
if mechanical_path is None:
|
247
|
+
return False
|
248
|
+
mechanical_version = atp.version_from_path("mechanical", mechanical_path)
|
249
|
+
return not (mechanical_version < 232 and os.name != "posix")
|
250
|
+
|
251
|
+
|
252
|
+
def change_default_mechanical_path(exe_loc):
|
253
|
+
"""Change default path.
|
254
|
+
|
255
|
+
Deprecated - use `ansys.tools.path.change_default_mechanical_path` instead.
|
256
|
+
"""
|
257
|
+
return atp.change_default_mechanical_path(exe_loc)
|
258
|
+
|
259
|
+
|
260
|
+
def save_mechanical_path(exe_loc=None): # pragma: no cover
|
261
|
+
"""Save path.
|
262
|
+
|
263
|
+
Deprecated - use `ansys.tools.path.save_mechanical_path` instead.
|
264
|
+
"""
|
265
|
+
return atp.save_mechanical_path(exe_loc)
|
266
|
+
|
267
|
+
|
268
|
+
client_to_server_loglevel = {
|
269
|
+
"DEBUG": 1,
|
270
|
+
"INFO": 2,
|
271
|
+
"WARN": 3,
|
272
|
+
"WARNING": 3,
|
273
|
+
"ERROR": 4,
|
274
|
+
"CRITICAL": 5,
|
275
|
+
}
|
276
|
+
|
277
|
+
|
278
|
+
class Mechanical(object):
|
279
|
+
"""Connects to a gRPC Mechanical server and allows commands to be passed."""
|
280
|
+
|
281
|
+
# Required by `_name` method to be defined before __init__ be
|
282
|
+
_ip = None
|
283
|
+
_port = None
|
284
|
+
|
285
|
+
def __init__(
|
286
|
+
self,
|
287
|
+
ip=None,
|
288
|
+
port=None,
|
289
|
+
timeout=60.0,
|
290
|
+
loglevel="WARNING",
|
291
|
+
log_file=False,
|
292
|
+
log_mechanical=None,
|
293
|
+
cleanup_on_exit=False,
|
294
|
+
channel=None,
|
295
|
+
remote_instance=None,
|
296
|
+
keep_connection_alive=True,
|
297
|
+
**kwargs,
|
298
|
+
):
|
299
|
+
"""Initialize the member variable based on the arguments.
|
300
|
+
|
301
|
+
Parameters
|
302
|
+
----------
|
303
|
+
ip : str, optional
|
304
|
+
IP address to connect to the server. The default is ``None``
|
305
|
+
in which case ``localhost`` is used.
|
306
|
+
port : int, optional
|
307
|
+
Port to connect to the Mecahnical server. The default is ``None``,
|
308
|
+
in which case ``10000`` is used.
|
309
|
+
timeout : float, optional
|
310
|
+
Maximum allowable time for connecting to the Mechanical server.
|
311
|
+
The default is ``60.0``.
|
312
|
+
loglevel : str, optional
|
313
|
+
Level of messages to print to the console. The default is ``WARNING``.
|
314
|
+
|
315
|
+
- ``ERROR`` prints only error messages.
|
316
|
+
- ``WARNING`` prints warning and error messages.
|
317
|
+
- ``INFO`` prints info, warning and error messages.
|
318
|
+
- ``DEBUG`` prints debug, info, warning and error messages.
|
319
|
+
|
320
|
+
log_file : bool, optional
|
321
|
+
Whether to copy the messages to a file named ``logs.log``, which is
|
322
|
+
located where the Python script is executed. The default is ``False``.
|
323
|
+
log_mechanical : str, optional
|
324
|
+
Path to the output file on the local disk for writing every script
|
325
|
+
command to. The default is ``None``. However, you might set
|
326
|
+
``"log_mechanical='pymechanical_log.txt"`` to write all commands that are
|
327
|
+
sent to Mechanical via PyMechanical in this file so that you can use them
|
328
|
+
to run a script within Mechanical without PyMechanical.
|
329
|
+
cleanup_on_exit : bool, optional
|
330
|
+
Whether to exit Mechanical when Python exits. The default is ``False``,
|
331
|
+
in which case Mechanical is not exited when the garbage for this Mechanical
|
332
|
+
instance is collected.
|
333
|
+
channel : grpc.Channel, optional
|
334
|
+
gRPC channel to use for the connection. The default is ``None``.
|
335
|
+
You can use this parameter as an alternative to the ``ip`` and ``port``
|
336
|
+
parameters.
|
337
|
+
remote_instance : ansys.platform.instancemanagement.Instance
|
338
|
+
Corresponding remote instance when Mechanical is launched
|
339
|
+
through PyPIM. The default is ``None``. If a remote instance
|
340
|
+
is specified, this instance is deleted when the
|
341
|
+
:func:`mecahnical.exit <ansys.mechanical.core.Mechanical.exit>`
|
342
|
+
function is called.
|
343
|
+
keep_connection_alive : bool, optional
|
344
|
+
Whether to keep the gRPC connection alive by running a background thread
|
345
|
+
and making dummy calls for remote connections. The default is ``True``.
|
346
|
+
|
347
|
+
Examples
|
348
|
+
--------
|
349
|
+
Connect to a Mechanical instance already running on locally on the
|
350
|
+
default port (``10000``).
|
351
|
+
|
352
|
+
>>> from ansys.mechanical import core as pymechanical
|
353
|
+
>>> mechanical = pymechanical.Mechanical()
|
354
|
+
|
355
|
+
Connect to a Mechanical instance running on the LAN on a default port.
|
356
|
+
|
357
|
+
>>> mechanical = pymechanical.Mechanical('192.168.1.101')
|
358
|
+
|
359
|
+
Connect to a Mechanical instance running on the LAN on a non-default port.
|
360
|
+
|
361
|
+
>>> mechanical = pymechanical.Mechanical('192.168.1.101', port=60001)
|
362
|
+
|
363
|
+
If you want to customize the channel, you can connect directly to gRPC channels.
|
364
|
+
For example, if you want to create an insecure channel with a maximum message
|
365
|
+
length of 8 MB, you would run:
|
366
|
+
|
367
|
+
>>> import grpc
|
368
|
+
>>> channel_temp = grpc.insecure_channel(
|
369
|
+
... '127.0.0.1:10000',
|
370
|
+
... options=[
|
371
|
+
... ("grpc.max_receive_message_length", 8*1024**2),
|
372
|
+
... ],
|
373
|
+
... )
|
374
|
+
>>> mechanical = pymechanical.Mechanical(channel=channel_temp)
|
375
|
+
"""
|
376
|
+
self._remote_instance = remote_instance
|
377
|
+
self._channel = channel
|
378
|
+
self._keep_connection_alive = keep_connection_alive
|
379
|
+
|
380
|
+
self._locked = False # being used within MechanicalPool
|
381
|
+
|
382
|
+
# ip could be a machine name. Convert it to an IP address.
|
383
|
+
ip_temp = ip
|
384
|
+
if channel is not None:
|
385
|
+
if ip is not None or port is not None:
|
386
|
+
raise ValueError(
|
387
|
+
"If `channel` is specified, neither `port` nor `ip` can be specified."
|
388
|
+
)
|
389
|
+
elif ip is None:
|
390
|
+
ip_temp = "127.0.0.1"
|
391
|
+
else:
|
392
|
+
ip_temp = socket.gethostbyname(ip) # Converting ip or host name to ip
|
393
|
+
|
394
|
+
self._ip = ip_temp
|
395
|
+
self._port = port
|
396
|
+
|
397
|
+
self._start_parm = kwargs
|
398
|
+
|
399
|
+
self._cleanup_on_exit = cleanup_on_exit
|
400
|
+
self._busy = False # used to check if running a command on the server
|
401
|
+
|
402
|
+
self._local = ip_temp in ["127.0.0.1", "127.0.1.1", "localhost"]
|
403
|
+
if "local" in kwargs: # pragma: no cover # allow this to be overridden
|
404
|
+
self._local = kwargs["local"]
|
405
|
+
|
406
|
+
self._health_response_queue = None
|
407
|
+
self._exiting = False
|
408
|
+
self._exited = None
|
409
|
+
|
410
|
+
self._version = None
|
411
|
+
|
412
|
+
if port is None:
|
413
|
+
port = MECHANICAL_DEFAULT_PORT
|
414
|
+
self._port = port
|
415
|
+
|
416
|
+
self._stub = None
|
417
|
+
self._timeout = timeout
|
418
|
+
|
419
|
+
if channel is None:
|
420
|
+
self._channel = self._create_channel(ip_temp, port)
|
421
|
+
else:
|
422
|
+
self._channel = channel
|
423
|
+
|
424
|
+
self._logLevel = loglevel
|
425
|
+
self._log_file = log_file
|
426
|
+
self._log_mechanical = log_mechanical
|
427
|
+
|
428
|
+
self._log = LOG.add_instance_logger(self.name, self, level=loglevel) # instance logger
|
429
|
+
# adding a file handler to the logger
|
430
|
+
if log_file:
|
431
|
+
if not isinstance(log_file, str):
|
432
|
+
log_file = "instance.log"
|
433
|
+
self._log.log_to_file(filename=log_file, level=loglevel)
|
434
|
+
|
435
|
+
self._log_file_mechanical = log_mechanical
|
436
|
+
if log_mechanical:
|
437
|
+
if not isinstance(log_mechanical, str):
|
438
|
+
self._log_file_mechanical = "pymechanical_log.txt"
|
439
|
+
else:
|
440
|
+
self._log_file_mechanical = log_mechanical
|
441
|
+
|
442
|
+
# temporarily disable logging
|
443
|
+
# useful when we run some dummy calls
|
444
|
+
self._disable_logging = False
|
445
|
+
|
446
|
+
if self._local:
|
447
|
+
self.log_info("Mechanical connection is treated as local.")
|
448
|
+
else:
|
449
|
+
self.log_info("Mechanical connection is treated as remote.")
|
450
|
+
|
451
|
+
# connect and validate to the channel
|
452
|
+
self._multi_connect(timeout=timeout)
|
453
|
+
|
454
|
+
self.log_info("Mechanical is ready to accept grpc calls.")
|
455
|
+
|
456
|
+
def __del__(self): # pragma: no cover
|
457
|
+
"""Clean up on exit."""
|
458
|
+
if self._cleanup_on_exit:
|
459
|
+
try:
|
460
|
+
self.exit(force=True)
|
461
|
+
except grpc.RpcError as e:
|
462
|
+
self.log_error(f"exit: {e}")
|
463
|
+
|
464
|
+
# def _set_log_level(self, level):
|
465
|
+
# """Set an alias for the log level."""
|
466
|
+
# self.set_log_level(level)
|
467
|
+
|
468
|
+
@property
|
469
|
+
def log(self):
|
470
|
+
"""Log associated with the current Mechanical instance."""
|
471
|
+
return self._log
|
472
|
+
|
473
|
+
@property
|
474
|
+
def version(self) -> str:
|
475
|
+
"""Get the Mechanical version based on the instance.
|
476
|
+
|
477
|
+
Examples
|
478
|
+
--------
|
479
|
+
Get the version of the connected Mechanical instance.
|
480
|
+
|
481
|
+
>>> mechanical.version
|
482
|
+
'251'
|
483
|
+
"""
|
484
|
+
if self._version is None:
|
485
|
+
try:
|
486
|
+
self._disable_logging = True
|
487
|
+
script = (
|
488
|
+
'clr.AddReference("Ans.Utilities")\n'
|
489
|
+
"import Ansys\n"
|
490
|
+
"config = Ansys.Utilities.ApplicationConfiguration.DefaultConfiguration\n"
|
491
|
+
"config.VersionInfo.VersionString"
|
492
|
+
)
|
493
|
+
self._version = self.run_python_script(script)
|
494
|
+
except grpc.RpcError: # pragma: no cover
|
495
|
+
raise
|
496
|
+
finally:
|
497
|
+
self._disable_logging = False
|
498
|
+
return self._version
|
499
|
+
|
500
|
+
@property
|
501
|
+
def name(self):
|
502
|
+
"""Name (unique identifier) of the Mechanical instance."""
|
503
|
+
try:
|
504
|
+
if self._channel is not None:
|
505
|
+
if self._remote_instance is not None: # pragma: no cover
|
506
|
+
return f"GRPC_{self._channel._channel._channel.target().decode()}"
|
507
|
+
else:
|
508
|
+
return f"GRPC_{self._channel._channel.target().decode()}"
|
509
|
+
except Exception as e: # pragma: no cover
|
510
|
+
LOG.error(f"Error getting the Mechanical instance name: {str(e)}")
|
511
|
+
|
512
|
+
return f"GRPC_instance_{id(self)}" # pragma: no cover
|
513
|
+
|
514
|
+
@property
|
515
|
+
def busy(self):
|
516
|
+
"""Return True when the Mechanical gRPC server is executing a command."""
|
517
|
+
return self._busy
|
518
|
+
|
519
|
+
@property
|
520
|
+
def locked(self):
|
521
|
+
"""Instance is in use within a pool."""
|
522
|
+
return self._locked
|
523
|
+
|
524
|
+
@locked.setter
|
525
|
+
def locked(self, new_value):
|
526
|
+
"""Instance is in use within a pool."""
|
527
|
+
self._locked = new_value
|
528
|
+
|
529
|
+
def _multi_connect(self, n_attempts=5, timeout=60):
|
530
|
+
"""Try to connect over a series of attempts to the channel.
|
531
|
+
|
532
|
+
Parameters
|
533
|
+
----------
|
534
|
+
n_attempts : int, optional
|
535
|
+
Number of connection attempts. The default is ``5``.
|
536
|
+
timeout : float, optional
|
537
|
+
Maximum allowable time in seconds for establishing a connection.
|
538
|
+
The default is ``60``.
|
539
|
+
"""
|
540
|
+
# This prevents a single failed connection from blocking other attempts
|
541
|
+
connected = False
|
542
|
+
attempt_timeout = timeout / n_attempts
|
543
|
+
self.log_debug(
|
544
|
+
f"timetout:{timeout} n_attempts:{n_attempts} attempt_timeout={attempt_timeout}"
|
545
|
+
)
|
546
|
+
|
547
|
+
max_time = time.time() + timeout
|
548
|
+
i = 1
|
549
|
+
while time.time() < max_time and i <= n_attempts:
|
550
|
+
self.log_debug(f"Connection attempt {i} with attempt timeout {attempt_timeout}s")
|
551
|
+
connected = self._connect(timeout=attempt_timeout)
|
552
|
+
|
553
|
+
if connected:
|
554
|
+
self.log_debug(f"Connection attempt {i} succeeded.")
|
555
|
+
break
|
556
|
+
|
557
|
+
i += 1
|
558
|
+
else: # pragma: no cover
|
559
|
+
self.log_debug(
|
560
|
+
f"Reached either maximum amount of connection attempts "
|
561
|
+
f"({n_attempts}) or timeout ({timeout} s)."
|
562
|
+
)
|
563
|
+
|
564
|
+
if not connected: # pragma: no cover
|
565
|
+
raise IOError(f"Unable to connect to Mechanical instance at {self._channel_str}.")
|
566
|
+
|
567
|
+
@property
|
568
|
+
def _channel_str(self):
|
569
|
+
"""Target string, generally in the form of ``ip:port``, such as ``127.0.0.1:10000``."""
|
570
|
+
if self._channel is not None:
|
571
|
+
if self._remote_instance is not None:
|
572
|
+
return self._channel._channel._channel.target().decode() # pragma: no cover
|
573
|
+
else:
|
574
|
+
return self._channel._channel.target().decode()
|
575
|
+
return "" # pragma: no cover
|
576
|
+
|
577
|
+
def _connect(self, timeout=12, enable_health_check=False):
|
578
|
+
"""Connect a gRPC channel to a remote or local Mechanical instance.
|
579
|
+
|
580
|
+
Parameters
|
581
|
+
----------
|
582
|
+
timeout : float
|
583
|
+
Maximum allowable time in seconds for establishing a connection. The
|
584
|
+
default is ``12``.
|
585
|
+
enable_health_check : bool, optional
|
586
|
+
Whether to enable a check to see if the connection is healthy.
|
587
|
+
The default is ``False``.
|
588
|
+
"""
|
589
|
+
self._state = grpc.channel_ready_future(self._channel)
|
590
|
+
self._stub = mechanical_pb2_grpc.MechanicalServiceStub(self._channel)
|
591
|
+
|
592
|
+
# verify connection
|
593
|
+
time_start = time.time()
|
594
|
+
while ((time.time() - time_start) < timeout) and not self._state._matured:
|
595
|
+
time.sleep(0.01)
|
596
|
+
|
597
|
+
if not self._state._matured: # pragma: no cover
|
598
|
+
return False
|
599
|
+
|
600
|
+
self.log_debug("Established a connection to the Mechanical gRPC server.")
|
601
|
+
|
602
|
+
self.wait_till_mechanical_is_ready(timeout)
|
603
|
+
|
604
|
+
# keeps Mechanical session alive
|
605
|
+
self._timer = None
|
606
|
+
if not self._local and self._keep_connection_alive: # pragma: no cover
|
607
|
+
self._initialised = threading.Event()
|
608
|
+
self._t_trigger = time.time()
|
609
|
+
self._t_delay = 30
|
610
|
+
self._timer = threading.Thread(
|
611
|
+
target=Mechanical._threaded_heartbeat, args=(weakref.proxy(self),)
|
612
|
+
)
|
613
|
+
self._timer.daemon = True
|
614
|
+
self._timer.start()
|
615
|
+
|
616
|
+
# enable health check
|
617
|
+
if enable_health_check: # pragma: no cover
|
618
|
+
self._enable_health_check()
|
619
|
+
|
620
|
+
self.__server_version = None
|
621
|
+
|
622
|
+
return True
|
623
|
+
|
624
|
+
def _enable_health_check(self): # pragma: no cover
|
625
|
+
"""Place the status of the health check in the health response queue."""
|
626
|
+
# lazy imports here to speed up module load
|
627
|
+
from grpc_health.v1 import health_pb2, health_pb2_grpc
|
628
|
+
|
629
|
+
def _consume_responses(response_iterator, response_queue):
|
630
|
+
try:
|
631
|
+
for response in response_iterator:
|
632
|
+
response_queue.put(response)
|
633
|
+
# NOTE: We're doing absolutely nothing with this as
|
634
|
+
# this point since the server-side health check
|
635
|
+
# doesn't change state.
|
636
|
+
except Exception:
|
637
|
+
if self._exiting:
|
638
|
+
return
|
639
|
+
self._exited = True
|
640
|
+
raise MechanicalExitedError(
|
641
|
+
"Lost connection with the Mechanical gRPC server."
|
642
|
+
) from None
|
643
|
+
|
644
|
+
# enable health check
|
645
|
+
from queue import Queue
|
646
|
+
|
647
|
+
request = health_pb2.HealthCheckRequest()
|
648
|
+
self._health_stub = health_pb2_grpc.HealthStub(self._channel)
|
649
|
+
rendezvous = self._health_stub.Watch(request)
|
650
|
+
|
651
|
+
# health check feature implemented after 2023 R1
|
652
|
+
try:
|
653
|
+
status = rendezvous.next()
|
654
|
+
except Exception as err:
|
655
|
+
if err.code().name != "UNIMPLEMENTED":
|
656
|
+
raise err
|
657
|
+
return
|
658
|
+
|
659
|
+
if status.status != health_pb2.HealthCheckResponse.SERVING:
|
660
|
+
raise MechanicalRuntimeError(
|
661
|
+
"Cannot enable health check and/or connect to the Mechanical server."
|
662
|
+
)
|
663
|
+
|
664
|
+
self._health_response_queue = Queue()
|
665
|
+
|
666
|
+
# allow main process to exit by setting daemon to true
|
667
|
+
thread = threading.Thread(
|
668
|
+
target=_consume_responses,
|
669
|
+
args=(rendezvous, self._health_response_queue),
|
670
|
+
daemon=True,
|
671
|
+
)
|
672
|
+
thread.start()
|
673
|
+
|
674
|
+
def _threaded_heartbeat(self): # pragma: no cover
|
675
|
+
"""To call from a thread to verify that a Mechanical instance is alive."""
|
676
|
+
self._initialised.set()
|
677
|
+
while True:
|
678
|
+
if self._exited:
|
679
|
+
break
|
680
|
+
try:
|
681
|
+
time.sleep(self._t_delay)
|
682
|
+
if not self.is_alive:
|
683
|
+
break
|
684
|
+
except ReferenceError:
|
685
|
+
break
|
686
|
+
# except Exception:
|
687
|
+
# continue
|
688
|
+
|
689
|
+
def _create_channel(self, ip, port):
|
690
|
+
"""Create an unsecured gRPC channel."""
|
691
|
+
check_valid_ip(ip)
|
692
|
+
|
693
|
+
# open the channel
|
694
|
+
channel_str = f"{ip}:{port}"
|
695
|
+
LOG.debug(f"Opening insecure channel at {channel_str}.")
|
696
|
+
return grpc.insecure_channel(
|
697
|
+
channel_str,
|
698
|
+
options=[
|
699
|
+
("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH),
|
700
|
+
],
|
701
|
+
)
|
702
|
+
|
703
|
+
@property
|
704
|
+
def is_alive(self) -> bool:
|
705
|
+
"""Whether there is an active connection to the Mechanical gRPC server."""
|
706
|
+
if self._exited:
|
707
|
+
return False
|
708
|
+
|
709
|
+
if self._busy: # pragma: no cover
|
710
|
+
return True
|
711
|
+
|
712
|
+
try: # pragma: no cover
|
713
|
+
self._make_dummy_call()
|
714
|
+
return True
|
715
|
+
except grpc.RpcError:
|
716
|
+
return False
|
717
|
+
|
718
|
+
@staticmethod
|
719
|
+
def set_log_level(loglevel):
|
720
|
+
"""Set the log level.
|
721
|
+
|
722
|
+
Parameters
|
723
|
+
----------
|
724
|
+
loglevel : str, int
|
725
|
+
Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``
|
726
|
+
and ``"ERROR"``.
|
727
|
+
|
728
|
+
Examples
|
729
|
+
--------
|
730
|
+
Set the log level to the ``"DEBUG"`` level.
|
731
|
+
|
732
|
+
# >>> mechanical.set_log_level('DEBUG')
|
733
|
+
#
|
734
|
+
# Set the log level to info
|
735
|
+
#
|
736
|
+
# >>> mechanical.set_log_level('INFO')
|
737
|
+
#
|
738
|
+
# Set the log level to warning
|
739
|
+
#
|
740
|
+
# >>> mechanical.set_log_level('WARNING')
|
741
|
+
#
|
742
|
+
# Set the log level to error
|
743
|
+
#
|
744
|
+
# >>> mechanical.set_log_level('ERROR')
|
745
|
+
"""
|
746
|
+
if isinstance(loglevel, str):
|
747
|
+
loglevel = loglevel.upper()
|
748
|
+
setup_logger(loglevel=loglevel)
|
749
|
+
|
750
|
+
def get_product_info(self):
|
751
|
+
"""Get product information by running a script on the Mechanical gRPC server."""
|
752
|
+
|
753
|
+
def _get_jscript_product_info_command():
|
754
|
+
return (
|
755
|
+
'ExtAPI.Application.ScriptByName("jscript").ExecuteCommand'
|
756
|
+
'("var productInfo = DS.Script.getProductInfo();returnFromScript(productInfo);")'
|
757
|
+
)
|
758
|
+
|
759
|
+
def _get_python_product_info_command():
|
760
|
+
return (
|
761
|
+
'clr.AddReference("Ansys.Mechanical.Application")\n'
|
762
|
+
"Ansys.Mechanical.Application.ProductInfo.ProductInfoAsString"
|
763
|
+
)
|
764
|
+
|
765
|
+
try:
|
766
|
+
self._disable_logging = True
|
767
|
+
if int(self.version) >= 232:
|
768
|
+
script = _get_python_product_info_command()
|
769
|
+
else:
|
770
|
+
script = _get_jscript_product_info_command()
|
771
|
+
return self.run_python_script(script)
|
772
|
+
except grpc.RpcError:
|
773
|
+
raise
|
774
|
+
finally:
|
775
|
+
self._disable_logging = False
|
776
|
+
|
777
|
+
@suppress_logging
|
778
|
+
def __repr__(self):
|
779
|
+
"""Get the user-readable string form of the Mechanical instance."""
|
780
|
+
try:
|
781
|
+
if self._exited:
|
782
|
+
return "Mechanical exited."
|
783
|
+
return self.get_product_info()
|
784
|
+
except grpc.RpcError:
|
785
|
+
return "Error getting product info."
|
786
|
+
|
787
|
+
def launch(self, cleanup_on_exit=True):
|
788
|
+
"""Launch Mechanical in batch or UI mode.
|
789
|
+
|
790
|
+
Parameters
|
791
|
+
----------
|
792
|
+
cleanup_on_exit : bool, optional
|
793
|
+
Whether to exit Mechanical when Python exits. The default is ``True``.
|
794
|
+
When ``False``, Mechanical is not exited when the garbage for this
|
795
|
+
Mechanical instance is collected.
|
796
|
+
"""
|
797
|
+
if not self._local:
|
798
|
+
raise RuntimeError("Can only launch with a local instance of Mechanical.")
|
799
|
+
|
800
|
+
# let us respect the current cleanup behavior
|
801
|
+
if self._cleanup_on_exit:
|
802
|
+
self.exit()
|
803
|
+
|
804
|
+
exec_file = self._start_parm.get("exec_file", get_mechanical_path(allow_input=False))
|
805
|
+
batch = self._start_parm.get("batch", True)
|
806
|
+
additional_switches = self._start_parm.get("additional_switches", None)
|
807
|
+
additional_envs = self._start_parm.get("additional_envs", None)
|
808
|
+
port = launch_grpc(
|
809
|
+
exec_file=exec_file,
|
810
|
+
batch=batch,
|
811
|
+
additional_switches=additional_switches,
|
812
|
+
additional_envs=additional_envs,
|
813
|
+
verbose=True,
|
814
|
+
)
|
815
|
+
# update the new cleanup behavior
|
816
|
+
self._cleanup_on_exit = cleanup_on_exit
|
817
|
+
self._port = port
|
818
|
+
self._channel = self._create_channel(self._ip, port)
|
819
|
+
self._connect(port)
|
820
|
+
|
821
|
+
self.log_info("Mechanical is ready to accept gRPC calls.")
|
822
|
+
|
823
|
+
def wait_till_mechanical_is_ready(self, wait_time=-1):
|
824
|
+
"""Wait until Mechanical is ready.
|
825
|
+
|
826
|
+
Parameters
|
827
|
+
----------
|
828
|
+
wait_time : float, optional
|
829
|
+
Maximum allowable time in seconds for connecting to the Mechanical gRPC server.
|
830
|
+
"""
|
831
|
+
time_1 = datetime.datetime.now()
|
832
|
+
|
833
|
+
sleep_time = 0.5
|
834
|
+
if wait_time == -1: # pragma: no cover
|
835
|
+
self.log_info("Waiting for Mechanical to be ready...")
|
836
|
+
else:
|
837
|
+
self.log_info(f"Waiting for Mechanical to be ready. Maximum wait time: {wait_time}s")
|
838
|
+
|
839
|
+
while not self.__isMechanicalReady():
|
840
|
+
time_2 = datetime.datetime.now()
|
841
|
+
time_interval = time_2 - time_1
|
842
|
+
time_interval_seconds = int(time_interval.total_seconds())
|
843
|
+
|
844
|
+
self.log_debug(
|
845
|
+
f"Mechanical is not ready. You've been waiting for {time_interval_seconds}."
|
846
|
+
)
|
847
|
+
if self._timeout != -1:
|
848
|
+
if time_interval_seconds > wait_time:
|
849
|
+
self.log_debug(
|
850
|
+
f"Allowed wait time {wait_time}s. "
|
851
|
+
f"Waited so for {time_interval_seconds}s, "
|
852
|
+
f"before throwing the error."
|
853
|
+
)
|
854
|
+
raise RuntimeError(
|
855
|
+
f"Couldn't connect to Mechanical. " f"Waited for {time_interval_seconds}s."
|
856
|
+
)
|
857
|
+
|
858
|
+
time.sleep(sleep_time)
|
859
|
+
|
860
|
+
time_2 = datetime.datetime.now()
|
861
|
+
time_interval = time_2 - time_1
|
862
|
+
time_interval_seconds = int(time_interval.total_seconds())
|
863
|
+
|
864
|
+
self.log_info(f"Mechanical is ready. It took {time_interval_seconds} seconds to verify.")
|
865
|
+
|
866
|
+
def __isMechanicalReady(self):
|
867
|
+
"""Whether the Mechanical gRPC server is ready.
|
868
|
+
|
869
|
+
Returns
|
870
|
+
-------
|
871
|
+
bool
|
872
|
+
``True`` if Mechanical is ready, ``False`` otherwise.
|
873
|
+
"""
|
874
|
+
try:
|
875
|
+
script = "ExtAPI.DataModel.Project.ProductVersion"
|
876
|
+
self.run_python_script(script)
|
877
|
+
except grpc.RpcError as error:
|
878
|
+
self.log_debug(f"Mechanical is not ready. Error:{error}.")
|
879
|
+
return False
|
880
|
+
|
881
|
+
return True
|
882
|
+
|
883
|
+
@staticmethod
|
884
|
+
def convert_to_server_log_level(log_level):
|
885
|
+
"""Convert the log level to the server log level.
|
886
|
+
|
887
|
+
Parameters
|
888
|
+
----------
|
889
|
+
log_level : str
|
890
|
+
Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``,
|
891
|
+
``"ERROR"``, and ``"CRITICAL"``.
|
892
|
+
|
893
|
+
Returns
|
894
|
+
-------
|
895
|
+
Converted log level for the server.
|
896
|
+
"""
|
897
|
+
value = client_to_server_loglevel.get(log_level)
|
898
|
+
|
899
|
+
if value is not None:
|
900
|
+
return value
|
901
|
+
|
902
|
+
raise ValueError(
|
903
|
+
f"Log level {log_level} is invalid. Possible values are "
|
904
|
+
f"'DEBUG','INFO', 'WARNING', 'ERROR', and 'CRITICAL'."
|
905
|
+
)
|
906
|
+
|
907
|
+
def run_python_script(
|
908
|
+
self, script_block: str, enable_logging=False, log_level="WARNING", progress_interval=2000
|
909
|
+
):
|
910
|
+
"""Run a Python script block inside Mechanical.
|
911
|
+
|
912
|
+
It returns the string value of the last executed statement. If the value cannot be
|
913
|
+
returned as a string, it will return an empty string.
|
914
|
+
|
915
|
+
Parameters
|
916
|
+
----------
|
917
|
+
script_block : str
|
918
|
+
Script block (one or more lines) to run.
|
919
|
+
enable_logging: bool, optional
|
920
|
+
Whether to enable logging. The default is ``False``.
|
921
|
+
log_level: str
|
922
|
+
Level of logging. The default is ``"WARNING"``. Options are ``"DEBUG"``,
|
923
|
+
``"INFO"``, ``"WARNING"``, and ``"ERROR"``.
|
924
|
+
progress_interval: int, optional
|
925
|
+
Frequency in milliseconds for getting log messages from the server.
|
926
|
+
The default is ``2000``.
|
927
|
+
|
928
|
+
Returns
|
929
|
+
-------
|
930
|
+
str
|
931
|
+
Script result.
|
932
|
+
|
933
|
+
Examples
|
934
|
+
--------
|
935
|
+
Return a value from a simple calculation.
|
936
|
+
|
937
|
+
>>> mechanical.run_python_script('2+3')
|
938
|
+
'5'
|
939
|
+
|
940
|
+
Return a string value from Project object.
|
941
|
+
|
942
|
+
>>> mechanical.run_python_script('ExtAPI.DataModel.Project.ProductVersion')
|
943
|
+
'2025 R1'
|
944
|
+
|
945
|
+
Return an empty string, when you try to return the Project object.
|
946
|
+
|
947
|
+
>>> mechanical.run_python_script('ExtAPI.DataModel.Project')
|
948
|
+
''
|
949
|
+
|
950
|
+
Return an empty string for assignments.
|
951
|
+
|
952
|
+
>>> mechanical.run_python_script('version = ExtAPI.DataModel.Project.ProductVersion')
|
953
|
+
''
|
954
|
+
|
955
|
+
Return value from the last executed statement from a variable.
|
956
|
+
|
957
|
+
>>> script='''
|
958
|
+
addition = 2 + 3
|
959
|
+
multiplication = 3 * 4
|
960
|
+
multiplication
|
961
|
+
'''
|
962
|
+
>>> mechanical.run_python_script(script)
|
963
|
+
'12'
|
964
|
+
|
965
|
+
Return value from last executed statement from a function call.
|
966
|
+
|
967
|
+
>>> script='''
|
968
|
+
import math
|
969
|
+
math.pow(2,3)
|
970
|
+
'''
|
971
|
+
>>> mechanical.run_python_script(script)
|
972
|
+
'8'
|
973
|
+
|
974
|
+
Handle an error scenario.
|
975
|
+
|
976
|
+
>>> script = 'hello_world()'
|
977
|
+
>>> import grpc
|
978
|
+
>>> try:
|
979
|
+
mechanical.run_python_script(script)
|
980
|
+
except grpc.RpcError as error:
|
981
|
+
print(error.details())
|
982
|
+
name 'hello_world' is not defined
|
983
|
+
|
984
|
+
"""
|
985
|
+
self.verify_valid_connection()
|
986
|
+
result_as_string = self.__call_run_python_script(
|
987
|
+
script_block, enable_logging, log_level, progress_interval
|
988
|
+
)
|
989
|
+
return result_as_string
|
990
|
+
|
991
|
+
def run_python_script_from_file(
|
992
|
+
self, file_path, enable_logging=False, log_level="WARNING", progress_interval=2000
|
993
|
+
):
|
994
|
+
"""Run the contents a python file inside Mechanical.
|
995
|
+
|
996
|
+
It returns the string value of the last executed statement. If the value cannot be
|
997
|
+
returned as a string, it will return an empty string.
|
998
|
+
|
999
|
+
Parameters
|
1000
|
+
----------
|
1001
|
+
file_path :
|
1002
|
+
Path for the Python file.
|
1003
|
+
enable_logging: bool, optional
|
1004
|
+
Whether to enable logging. The default is ``False``.
|
1005
|
+
log_level: str
|
1006
|
+
Level of logging. The default is ``"WARNING"``. Options are ``"DEBUG"``,
|
1007
|
+
``"INFO"``, ``"WARNING"``, and ``"ERROR"``.
|
1008
|
+
progress_interval: int, optional
|
1009
|
+
Frequency in milliseconds for getting log messages from the server.
|
1010
|
+
The default is ``2000``.
|
1011
|
+
|
1012
|
+
Returns
|
1013
|
+
-------
|
1014
|
+
str
|
1015
|
+
Script result.
|
1016
|
+
|
1017
|
+
Examples
|
1018
|
+
--------
|
1019
|
+
Return a value from a simple calculation.
|
1020
|
+
|
1021
|
+
Contents of **simple.py** file
|
1022
|
+
|
1023
|
+
2+3
|
1024
|
+
|
1025
|
+
>>> mechanical.run_python_script_from_file('simple.py')
|
1026
|
+
'5'
|
1027
|
+
|
1028
|
+
Return a value from a simple function call.
|
1029
|
+
|
1030
|
+
Contents of **test.py** file
|
1031
|
+
|
1032
|
+
import math
|
1033
|
+
|
1034
|
+
math.pow(2,3)
|
1035
|
+
|
1036
|
+
>>> mechanical.run_python_script_from_file('test.py')
|
1037
|
+
'8'
|
1038
|
+
|
1039
|
+
"""
|
1040
|
+
self.verify_valid_connection()
|
1041
|
+
self.log_debug(f"run_python_script_from_file started")
|
1042
|
+
script_code = Mechanical.__readfile(file_path)
|
1043
|
+
self.log_debug(f"run_python_script_from_file started")
|
1044
|
+
return self.run_python_script(script_code, enable_logging, log_level, progress_interval)
|
1045
|
+
|
1046
|
+
def exit(self, force=False):
|
1047
|
+
"""Exit Mechanical.
|
1048
|
+
|
1049
|
+
Parameters
|
1050
|
+
----------
|
1051
|
+
force : bool, optional
|
1052
|
+
Whether to force Mechanical to exit. The default is ``False``, in which case
|
1053
|
+
only Mechanical in UI mode asks for confirmation. This parameter overrides
|
1054
|
+
any environment variables that may inhibit exiting Mechanical.
|
1055
|
+
|
1056
|
+
Examples
|
1057
|
+
--------
|
1058
|
+
Exit Mechanical.
|
1059
|
+
|
1060
|
+
>>> mechanical.Exit(force=True)
|
1061
|
+
|
1062
|
+
"""
|
1063
|
+
if not force:
|
1064
|
+
if not get_start_instance():
|
1065
|
+
self.log_info("Ignoring exit due to PYMECHANICAL_START_INSTANCE=False")
|
1066
|
+
return
|
1067
|
+
|
1068
|
+
# or building the gallery
|
1069
|
+
if pymechanical.BUILDING_GALLERY:
|
1070
|
+
self._log.info("Ignoring exit due to BUILDING_GALLERY=True")
|
1071
|
+
return
|
1072
|
+
|
1073
|
+
if self._exited:
|
1074
|
+
return
|
1075
|
+
|
1076
|
+
self.verify_valid_connection()
|
1077
|
+
|
1078
|
+
self._exiting = True
|
1079
|
+
|
1080
|
+
self.log_debug("In shutdown.")
|
1081
|
+
request = mechanical_pb2.ShutdownRequest(force_exit=force)
|
1082
|
+
self.log_debug("Shutting down...")
|
1083
|
+
|
1084
|
+
self._busy = True
|
1085
|
+
try:
|
1086
|
+
self._stub.Shutdown(request)
|
1087
|
+
except grpc._channel._InactiveRpcError as error:
|
1088
|
+
self.log_warning("Mechanical exit failed: {str(error}.")
|
1089
|
+
finally:
|
1090
|
+
self._busy = False
|
1091
|
+
|
1092
|
+
self._exited = True
|
1093
|
+
self._stub = None
|
1094
|
+
|
1095
|
+
if self._remote_instance is not None: # pragma: no cover
|
1096
|
+
self.log_debug("PyPIM delete has started.")
|
1097
|
+
try:
|
1098
|
+
self._remote_instance.delete()
|
1099
|
+
except Exception as error:
|
1100
|
+
self.log_warning("Remote instance delete failed: {str(error}.")
|
1101
|
+
self.log_debug("PyPIM delete has finished.")
|
1102
|
+
|
1103
|
+
self._remote_instance = None
|
1104
|
+
self._channel = None
|
1105
|
+
else:
|
1106
|
+
self.log_debug("No PyPIM cleanup is needed.")
|
1107
|
+
|
1108
|
+
local_ports = pymechanical.LOCAL_PORTS
|
1109
|
+
if self._local and self._port in local_ports:
|
1110
|
+
local_ports.remove(self._port)
|
1111
|
+
|
1112
|
+
self.log_info("Shutdown has finished.")
|
1113
|
+
|
1114
|
+
@protect_grpc
|
1115
|
+
def upload(
|
1116
|
+
self,
|
1117
|
+
file_name,
|
1118
|
+
file_location_destination=None,
|
1119
|
+
chunk_size=DEFAULT_FILE_CHUNK_SIZE,
|
1120
|
+
progress_bar=True,
|
1121
|
+
):
|
1122
|
+
"""Upload a file to the Mechanical instance.
|
1123
|
+
|
1124
|
+
Parameters
|
1125
|
+
----------
|
1126
|
+
file_name : str
|
1127
|
+
Local file to upload. Only the file name is needed if the file
|
1128
|
+
is relative to the current working directory. Otherwise, the full path
|
1129
|
+
is needed.
|
1130
|
+
file_location_destination : str, optional
|
1131
|
+
File location on the Mechanical server to upload the file to. The default is
|
1132
|
+
``None``, in which case the project directory is used.
|
1133
|
+
chunk_size : int, optional
|
1134
|
+
Chunk size in bytes. The default is ``1048576``.
|
1135
|
+
progress_bar : bool, optional
|
1136
|
+
Whether to show a progress bar using ``tqdm``. The default is ``True``.
|
1137
|
+
A progress bar is helpful for viewing upload progress.
|
1138
|
+
|
1139
|
+
Returns
|
1140
|
+
-------
|
1141
|
+
str
|
1142
|
+
Base name of the uploaded file.
|
1143
|
+
|
1144
|
+
Examples
|
1145
|
+
--------
|
1146
|
+
Upload the ``hsec.x_t`` file with the progress bar not shown.
|
1147
|
+
|
1148
|
+
>>> mechanical.upload('hsec.x_t', progress_bar=False)
|
1149
|
+
"""
|
1150
|
+
self.verify_valid_connection()
|
1151
|
+
|
1152
|
+
if not os.path.isfile(file_name):
|
1153
|
+
raise FileNotFoundError(f"Unable to locate filename {file_name}.")
|
1154
|
+
|
1155
|
+
self._log.debug(f"Uploading file '{file_name}' to the Mechanical instance.")
|
1156
|
+
|
1157
|
+
if file_location_destination is None:
|
1158
|
+
file_location_destination = self.project_directory
|
1159
|
+
|
1160
|
+
self._busy = True
|
1161
|
+
try:
|
1162
|
+
chunks_generator = self.get_file_chunks(
|
1163
|
+
file_location_destination,
|
1164
|
+
file_name,
|
1165
|
+
chunk_size=chunk_size,
|
1166
|
+
progress_bar=progress_bar,
|
1167
|
+
)
|
1168
|
+
response = self._stub.UploadFile(chunks_generator)
|
1169
|
+
self.log_debug(f"upload_file response is {response.is_ok}.")
|
1170
|
+
finally:
|
1171
|
+
self._busy = False
|
1172
|
+
|
1173
|
+
if not response.is_ok: # pragma: no cover
|
1174
|
+
raise IOError("File failed to upload.")
|
1175
|
+
return os.path.basename(file_name)
|
1176
|
+
|
1177
|
+
def get_file_chunks(self, file_location, file_name, chunk_size, progress_bar):
|
1178
|
+
"""Construct the file upload request for the server.
|
1179
|
+
|
1180
|
+
Parameters
|
1181
|
+
----------
|
1182
|
+
file_location_destination : str, optional
|
1183
|
+
Directory where the file to upload to the server is located.
|
1184
|
+
file_name : str
|
1185
|
+
Name of the file to upload.
|
1186
|
+
chunk_size : int
|
1187
|
+
Chunk size in bytes.
|
1188
|
+
progress_bar : bool
|
1189
|
+
Whether to show a progress bar using ``tqdm``.
|
1190
|
+
"""
|
1191
|
+
pbar = None
|
1192
|
+
if progress_bar:
|
1193
|
+
if not _HAS_TQDM: # pragma: no cover
|
1194
|
+
raise ModuleNotFoundError(
|
1195
|
+
f"To use the keyword argument 'progress_bar', you must have "
|
1196
|
+
f"installed the 'tqdm' package. To avoid this message, you can "
|
1197
|
+
f"set 'progress_bar=False'."
|
1198
|
+
)
|
1199
|
+
|
1200
|
+
n_bytes = os.path.getsize(file_name)
|
1201
|
+
|
1202
|
+
base_name = os.path.basename(file_name)
|
1203
|
+
pbar = tqdm(
|
1204
|
+
total=n_bytes,
|
1205
|
+
desc=f"Uploading {base_name} to {self._channel_str}:{file_location}.",
|
1206
|
+
unit="B",
|
1207
|
+
unit_scale=True,
|
1208
|
+
unit_divisor=1024,
|
1209
|
+
)
|
1210
|
+
|
1211
|
+
with open(file_name, "rb") as f:
|
1212
|
+
while True:
|
1213
|
+
piece = f.read(chunk_size)
|
1214
|
+
length = len(piece)
|
1215
|
+
if length == 0:
|
1216
|
+
if pbar is not None:
|
1217
|
+
pbar.close()
|
1218
|
+
return
|
1219
|
+
|
1220
|
+
if pbar is not None:
|
1221
|
+
pbar.update(length)
|
1222
|
+
|
1223
|
+
chunk = mechanical_pb2.Chunk(payload=piece, size=length)
|
1224
|
+
yield mechanical_pb2.FileUploadRequest(
|
1225
|
+
file_name=os.path.basename(file_name), file_location=file_location, chunk=chunk
|
1226
|
+
)
|
1227
|
+
|
1228
|
+
@property
|
1229
|
+
def project_directory(self):
|
1230
|
+
"""Get the project directory for the currently connected Mechanical instance.
|
1231
|
+
|
1232
|
+
Examples
|
1233
|
+
--------
|
1234
|
+
Get the project directory of the connected Mechanical instance.
|
1235
|
+
|
1236
|
+
>>> mechanical.project_directory
|
1237
|
+
'/tmp/ANSYS.username.1/AnsysMech3F97/Project_Mech_Files/'
|
1238
|
+
|
1239
|
+
"""
|
1240
|
+
return self.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory")
|
1241
|
+
|
1242
|
+
def list_files(self):
|
1243
|
+
"""List the files in the working directory of Mechanical.
|
1244
|
+
|
1245
|
+
Returns
|
1246
|
+
-------
|
1247
|
+
list
|
1248
|
+
List of files in the working directory of Mechanical.
|
1249
|
+
|
1250
|
+
Examples
|
1251
|
+
--------
|
1252
|
+
List the files in the working directory.
|
1253
|
+
|
1254
|
+
>>> files = mechanical.list_files()
|
1255
|
+
>>> for file in files: print(file)
|
1256
|
+
"""
|
1257
|
+
result = self.run_python_script(
|
1258
|
+
"import pymechanical_helpers\npymechanical_helpers.GetAllProjectFiles(ExtAPI)"
|
1259
|
+
)
|
1260
|
+
|
1261
|
+
files_out = result.splitlines()
|
1262
|
+
if not files_out: # pragma: no cover
|
1263
|
+
self.log_warning("No files listed")
|
1264
|
+
return files_out
|
1265
|
+
|
1266
|
+
def _get_files(self, files, recursive=False):
|
1267
|
+
self_files = self.list_files() # to avoid calling it too much
|
1268
|
+
|
1269
|
+
if isinstance(files, str):
|
1270
|
+
if self._local: # pragma: no cover
|
1271
|
+
# in local mode
|
1272
|
+
if os.path.exists(files):
|
1273
|
+
if not os.path.isabs(files):
|
1274
|
+
list_files = [os.path.join(os.getcwd(), files)]
|
1275
|
+
else:
|
1276
|
+
# file exist
|
1277
|
+
list_files = [files]
|
1278
|
+
elif "*" in files:
|
1279
|
+
# using filter
|
1280
|
+
list_files = glob.glob(files, recursive=recursive)
|
1281
|
+
if not list_files:
|
1282
|
+
raise ValueError(
|
1283
|
+
f"The `'files'` parameter ({files}) didn't match any file using "
|
1284
|
+
f"glob expressions in the local client."
|
1285
|
+
)
|
1286
|
+
else:
|
1287
|
+
raise ValueError(
|
1288
|
+
f"The files parameter ('{files}') does not match any file or pattern."
|
1289
|
+
)
|
1290
|
+
else: # Remote or looking into Mechanical working directory
|
1291
|
+
if files in self_files:
|
1292
|
+
list_files = [files]
|
1293
|
+
elif "*" in files:
|
1294
|
+
# try filter on the list_files
|
1295
|
+
if recursive:
|
1296
|
+
self.log_warning(
|
1297
|
+
"Because the 'recursive' keyword argument does not work with "
|
1298
|
+
"remote instances, it is ignored."
|
1299
|
+
)
|
1300
|
+
list_files = fnmatch.filter(self_files, files)
|
1301
|
+
if not list_files:
|
1302
|
+
raise ValueError(
|
1303
|
+
f"The `'files'` parameter ({files}) didn't match any file using "
|
1304
|
+
f"glob expressions in the remote server."
|
1305
|
+
)
|
1306
|
+
else:
|
1307
|
+
raise ValueError(
|
1308
|
+
f"The `'files'` parameter ('{files}') does not match any file or pattern."
|
1309
|
+
)
|
1310
|
+
|
1311
|
+
elif isinstance(files, (list, tuple)):
|
1312
|
+
if not all([isinstance(each, str) for each in files]):
|
1313
|
+
raise ValueError(
|
1314
|
+
"The parameter `'files'` can be a list or tuple, but it "
|
1315
|
+
"should only contain strings."
|
1316
|
+
)
|
1317
|
+
list_files = files
|
1318
|
+
else:
|
1319
|
+
raise ValueError(
|
1320
|
+
f"The `file` parameter type ({type(files)}) is not supported."
|
1321
|
+
"Only strings, tuple of strings, or list of strings are allowed."
|
1322
|
+
)
|
1323
|
+
|
1324
|
+
return list_files
|
1325
|
+
|
1326
|
+
def download(
|
1327
|
+
self,
|
1328
|
+
files,
|
1329
|
+
target_dir=None,
|
1330
|
+
chunk_size=DEFAULT_CHUNK_SIZE,
|
1331
|
+
progress_bar=None,
|
1332
|
+
recursive=False,
|
1333
|
+
): # pragma: no cover
|
1334
|
+
"""Download files from the working directory of the Mechanical instance.
|
1335
|
+
|
1336
|
+
It downloads them from the working directory to the target directory. It returns the list
|
1337
|
+
of local file paths for the downloaded files.
|
1338
|
+
|
1339
|
+
Parameters
|
1340
|
+
----------
|
1341
|
+
files : str, list[str], tuple(str)
|
1342
|
+
One or more files on the Mechanical server to download. The files must be
|
1343
|
+
in the same directory as the Mechanical instance. You can use the
|
1344
|
+
:func:`Mechanical.list_files <ansys.mechanical.core.mechanical.list_files>`
|
1345
|
+
function to list current files. Alternatively, you can specify *glob expressions* to
|
1346
|
+
match file names. For example, you could use ``file*`` to match every file whose
|
1347
|
+
name starts with ``file``.
|
1348
|
+
target_dir: str
|
1349
|
+
Default directory to copy the downloaded files to. The default is ``None`` and
|
1350
|
+
current working directory will be used as target directory.
|
1351
|
+
chunk_size : int, optional
|
1352
|
+
Chunk size in bytes. The default is ``262144``. The value must be less than 4 MB.
|
1353
|
+
progress_bar : bool, optional
|
1354
|
+
Whether to show a progress bar using ``tqdm``. The default is ``None``, in
|
1355
|
+
which case a progress bar is shown. A progress bar is helpful for viewing download
|
1356
|
+
progress.
|
1357
|
+
recursive : bool, optional
|
1358
|
+
Whether to use recursion when using a glob pattern search. The default is ``False``.
|
1359
|
+
|
1360
|
+
Returns
|
1361
|
+
-------
|
1362
|
+
List[str]
|
1363
|
+
List of local file paths.
|
1364
|
+
|
1365
|
+
Notes
|
1366
|
+
-----
|
1367
|
+
There are some considerations to keep in mind when using the ``download()`` method:
|
1368
|
+
|
1369
|
+
* The glob pattern search does not search recursively in remote instances.
|
1370
|
+
* In a remote instance, it is not possible to list or download files in a
|
1371
|
+
location other than the Mechanical working directory.
|
1372
|
+
* If you are connected to a local instance and provide a file path, downloading files
|
1373
|
+
from a different folder is allowed but is not recommended.
|
1374
|
+
|
1375
|
+
Examples
|
1376
|
+
--------
|
1377
|
+
Download a single file.
|
1378
|
+
|
1379
|
+
>>> local_file_path_list = mechanical.download('file.out')
|
1380
|
+
|
1381
|
+
Download all files starting with ``file``.
|
1382
|
+
|
1383
|
+
>>> local_file_path_list = mechanical.download('file*')
|
1384
|
+
|
1385
|
+
Download every file in the Mechanical working directory.
|
1386
|
+
|
1387
|
+
>>> local_file_path_list = mechanical.download('*.*')
|
1388
|
+
|
1389
|
+
Alternatively, the recommended method is to use the
|
1390
|
+
:func:`download_project() <ansys.mechanical.core.mechanical.Mechanical.download_project>`
|
1391
|
+
method to download all files.
|
1392
|
+
|
1393
|
+
>>> local_file_path_list = mechanical.download_project()
|
1394
|
+
|
1395
|
+
"""
|
1396
|
+
self.verify_valid_connection()
|
1397
|
+
|
1398
|
+
if chunk_size > 4 * 1024 * 1024: # 4MB
|
1399
|
+
raise ValueError(
|
1400
|
+
"Chunk sizes bigger than 4 MB can generate unstable behaviour in PyMechanical. "
|
1401
|
+
"Decrease the ``chunk_size`` value."
|
1402
|
+
)
|
1403
|
+
|
1404
|
+
list_files = self._get_files(files, recursive=recursive)
|
1405
|
+
|
1406
|
+
if target_dir:
|
1407
|
+
path = pathlib.Path(target_dir)
|
1408
|
+
path.mkdir(parents=True, exist_ok=True)
|
1409
|
+
else:
|
1410
|
+
target_dir = os.getcwd()
|
1411
|
+
|
1412
|
+
out_files = []
|
1413
|
+
|
1414
|
+
for each_file in list_files:
|
1415
|
+
try:
|
1416
|
+
file_name = os.path.basename(each_file) # Getting only the name of the file.
|
1417
|
+
# We try to avoid that when the full path is supplied. It crashes when trying
|
1418
|
+
# to do `os.path.join(target_dir"os.getcwd()", file_name "full filename path"`
|
1419
|
+
# This produces the file structure to flat out, but it is fine,
|
1420
|
+
# because recursive does not work in remote.
|
1421
|
+
self._busy = True
|
1422
|
+
out_file_path = self._download(
|
1423
|
+
each_file,
|
1424
|
+
out_file_name=os.path.join(target_dir, file_name),
|
1425
|
+
chunk_size=chunk_size,
|
1426
|
+
progress_bar=progress_bar,
|
1427
|
+
)
|
1428
|
+
out_files.append(out_file_path)
|
1429
|
+
except FileNotFoundError:
|
1430
|
+
# So far the gRPC interface returns the size of the file equal
|
1431
|
+
# zero, if the file does not exist, or if its size is zero,
|
1432
|
+
# but they are two different things.
|
1433
|
+
# In theory, since we are obtaining the files name from
|
1434
|
+
# `mechanical.list_files()`, they do exist, so
|
1435
|
+
# if there is any error, it means their size is zero.
|
1436
|
+
pass # This is not the best.
|
1437
|
+
finally:
|
1438
|
+
self._busy = False
|
1439
|
+
|
1440
|
+
return out_files
|
1441
|
+
|
1442
|
+
@protect_grpc
|
1443
|
+
def _download(
|
1444
|
+
self,
|
1445
|
+
target_name,
|
1446
|
+
out_file_name,
|
1447
|
+
chunk_size=DEFAULT_CHUNK_SIZE,
|
1448
|
+
progress_bar=None,
|
1449
|
+
):
|
1450
|
+
"""Download a file from the Mechanical instance.
|
1451
|
+
|
1452
|
+
Parameters
|
1453
|
+
----------
|
1454
|
+
target_name : str
|
1455
|
+
Name of the target file on the server. The file must be in the same
|
1456
|
+
directory as the Mechanical instance. You can use the
|
1457
|
+
``mechanical.list_files()`` function to list current files.
|
1458
|
+
out_file_name : str
|
1459
|
+
Name of the output file if the name is to differ from that for the target
|
1460
|
+
file.
|
1461
|
+
chunk_size : int, optional
|
1462
|
+
Chunk size in bytes. The default is ``"DEFAULT_CHUNK_SIZE"``, in which case
|
1463
|
+
256 kB is used. The value must be less than 4 MB.
|
1464
|
+
progress_bar : bool, optional
|
1465
|
+
Whether to show a progress bar using ``tqdm``. The default is ``None``, in
|
1466
|
+
which case a progress bar is shown. A progress bar is helpful for showing download
|
1467
|
+
progress.
|
1468
|
+
|
1469
|
+
Examples
|
1470
|
+
--------
|
1471
|
+
Download the remote result file "file.rst" as "my_result.rst".
|
1472
|
+
|
1473
|
+
>>> mechanical.download('file.rst', 'my_result.rst')
|
1474
|
+
"""
|
1475
|
+
self.verify_valid_connection()
|
1476
|
+
|
1477
|
+
if not progress_bar and _HAS_TQDM:
|
1478
|
+
progress_bar = True
|
1479
|
+
|
1480
|
+
request = mechanical_pb2.FileDownloadRequest(file_path=target_name, chunk_size=chunk_size)
|
1481
|
+
|
1482
|
+
responses = self._stub.DownloadFile(request)
|
1483
|
+
|
1484
|
+
file_size = self.save_chunks_to_file(
|
1485
|
+
responses, out_file_name, progress_bar=progress_bar, target_name=target_name
|
1486
|
+
)
|
1487
|
+
|
1488
|
+
if not file_size: # pragma: no cover
|
1489
|
+
raise FileNotFoundError(f'File "{out_file_name}" is empty or does not exist')
|
1490
|
+
|
1491
|
+
self.log_info(f"{out_file_name} with size {file_size} has been written.")
|
1492
|
+
|
1493
|
+
return out_file_name
|
1494
|
+
|
1495
|
+
def save_chunks_to_file(self, responses, filename, progress_bar=False, target_name=""):
|
1496
|
+
"""Save chunks to a local file.
|
1497
|
+
|
1498
|
+
Parameters
|
1499
|
+
----------
|
1500
|
+
responses :
|
1501
|
+
filename : str
|
1502
|
+
Name of the local file to save chunks to.
|
1503
|
+
progress_bar : bool, optional
|
1504
|
+
Whether to show a progress bar using ``tqdm``. The default is ``False``.
|
1505
|
+
target_name : str, optional
|
1506
|
+
Name of the target file on the server. The default is ``""``. The file
|
1507
|
+
must be in the same directory as the Mechanical instance. You can use the
|
1508
|
+
``mechanical.list_files()`` function to list current files.
|
1509
|
+
|
1510
|
+
Returns
|
1511
|
+
-------
|
1512
|
+
file_size : int
|
1513
|
+
File size saved in bytes. If ``0`` is returned, no file was written.
|
1514
|
+
"""
|
1515
|
+
pbar = None
|
1516
|
+
if progress_bar:
|
1517
|
+
if not _HAS_TQDM: # pragma: no cover
|
1518
|
+
raise ModuleNotFoundError(
|
1519
|
+
"To use the keyword argument 'progress_bar', you need to have installed "
|
1520
|
+
"the 'tqdm' package.To avoid this message you can set 'progress_bar=False'."
|
1521
|
+
)
|
1522
|
+
|
1523
|
+
file_size = 0
|
1524
|
+
with open(filename, "wb") as f:
|
1525
|
+
for response in responses:
|
1526
|
+
f.write(response.chunk.payload)
|
1527
|
+
payload_size = len(response.chunk.payload)
|
1528
|
+
file_size += payload_size
|
1529
|
+
if pbar is None:
|
1530
|
+
pbar = tqdm(
|
1531
|
+
total=response.file_size,
|
1532
|
+
desc=f"Downloading {self._channel_str}:{target_name} to {filename}",
|
1533
|
+
unit="B",
|
1534
|
+
unit_scale=True,
|
1535
|
+
unit_divisor=1024,
|
1536
|
+
)
|
1537
|
+
pbar.update(payload_size)
|
1538
|
+
else:
|
1539
|
+
pbar.update(payload_size)
|
1540
|
+
|
1541
|
+
if pbar is not None:
|
1542
|
+
pbar.close()
|
1543
|
+
|
1544
|
+
return file_size
|
1545
|
+
|
1546
|
+
def download_project(self, extensions=None, target_dir=None, progress_bar=False):
|
1547
|
+
"""Download all project files in the working directory of the Mechanical instance.
|
1548
|
+
|
1549
|
+
It downloads them from the working directory to the target directory. It returns the list
|
1550
|
+
of local file paths for the downloaded files.
|
1551
|
+
|
1552
|
+
Parameters
|
1553
|
+
----------
|
1554
|
+
extensions : list[str], tuple[str], optional
|
1555
|
+
List of extensions for filtering files before downloading them. The
|
1556
|
+
default is ``None``.
|
1557
|
+
target_dir : str, optional
|
1558
|
+
Path for downloading the files to. The default is ``None``.
|
1559
|
+
progress_bar : bool, optional
|
1560
|
+
Whether to show a progress bar using ``tqdm``. The default is ``False``.
|
1561
|
+
A progress bar is helpful for viewing download progress.
|
1562
|
+
|
1563
|
+
Returns
|
1564
|
+
-------
|
1565
|
+
List[str]
|
1566
|
+
List of local file paths.
|
1567
|
+
|
1568
|
+
Examples
|
1569
|
+
--------
|
1570
|
+
Download all the files in the project.
|
1571
|
+
|
1572
|
+
>>> local_file_path_list = mechanical.download_project()
|
1573
|
+
"""
|
1574
|
+
destination_directory = target_dir.rstrip("\\/")
|
1575
|
+
|
1576
|
+
# let us create the directory, if it doesn't exist
|
1577
|
+
if destination_directory:
|
1578
|
+
path = pathlib.Path(destination_directory)
|
1579
|
+
path.mkdir(parents=True, exist_ok=True)
|
1580
|
+
else:
|
1581
|
+
destination_directory = os.getcwd()
|
1582
|
+
|
1583
|
+
# relative directory?
|
1584
|
+
if os.path.isdir(destination_directory):
|
1585
|
+
if not os.path.isabs(destination_directory):
|
1586
|
+
# construct full path
|
1587
|
+
destination_directory = os.path.join(os.getcwd(), destination_directory)
|
1588
|
+
|
1589
|
+
project_directory = self.project_directory
|
1590
|
+
# remove the trailing slash - server could be windows or linux
|
1591
|
+
project_directory = project_directory.rstrip("\\/")
|
1592
|
+
|
1593
|
+
# this is where .mechddb resides
|
1594
|
+
parent_directory = os.path.dirname(project_directory)
|
1595
|
+
|
1596
|
+
list_of_files = []
|
1597
|
+
|
1598
|
+
if not extensions:
|
1599
|
+
files = self.list_files()
|
1600
|
+
else:
|
1601
|
+
files = []
|
1602
|
+
for each_extension in extensions:
|
1603
|
+
# mechdb resides one level above project directory
|
1604
|
+
if "mechdb" == each_extension.lower():
|
1605
|
+
file_temp = os.path.join(parent_directory, f"*.{each_extension}")
|
1606
|
+
else:
|
1607
|
+
file_temp = os.path.join(project_directory, "**", f"*.{each_extension}")
|
1608
|
+
|
1609
|
+
if self._local:
|
1610
|
+
list_files_expanded = self._get_files(file_temp, recursive=True)
|
1611
|
+
|
1612
|
+
if "mechdb" == each_extension.lower():
|
1613
|
+
# if we have more than one .mechdb in the parent folder
|
1614
|
+
# filter to have only the current mechdb
|
1615
|
+
self_files = self.list_files()
|
1616
|
+
filtered_files = []
|
1617
|
+
for temp_file in list_files_expanded:
|
1618
|
+
if temp_file in self_files:
|
1619
|
+
filtered_files.append(temp_file)
|
1620
|
+
list_files = filtered_files
|
1621
|
+
else:
|
1622
|
+
list_files = list_files_expanded
|
1623
|
+
else:
|
1624
|
+
list_files = self._get_files(file_temp, recursive=False)
|
1625
|
+
|
1626
|
+
files.extend(list_files)
|
1627
|
+
|
1628
|
+
for file in files:
|
1629
|
+
# create similar hierarchy locally
|
1630
|
+
new_path = file.replace(parent_directory, destination_directory)
|
1631
|
+
new_path_dir = os.path.dirname(new_path)
|
1632
|
+
temp_files = self.download(
|
1633
|
+
files=file, target_dir=new_path_dir, progress_bar=progress_bar
|
1634
|
+
)
|
1635
|
+
list_of_files.extend(temp_files)
|
1636
|
+
|
1637
|
+
return list_of_files
|
1638
|
+
|
1639
|
+
def clear(self):
|
1640
|
+
"""Clear the database.
|
1641
|
+
|
1642
|
+
Examples
|
1643
|
+
--------
|
1644
|
+
Clear the database.
|
1645
|
+
|
1646
|
+
>>> mechanical.clear()
|
1647
|
+
|
1648
|
+
"""
|
1649
|
+
self.run_python_script("ExtAPI.DataModel.Project.New()")
|
1650
|
+
|
1651
|
+
def _make_dummy_call(self):
|
1652
|
+
try:
|
1653
|
+
self._disable_logging = True
|
1654
|
+
self.run_python_script("ExtAPI.DataModel.Project.ProjectDirectory")
|
1655
|
+
except grpc.RpcError: # pragma: no cover
|
1656
|
+
raise
|
1657
|
+
finally:
|
1658
|
+
self._disable_logging = False
|
1659
|
+
|
1660
|
+
@staticmethod
|
1661
|
+
def __readfile(file_path):
|
1662
|
+
"""Get the contents of the file as a string."""
|
1663
|
+
# open text file in read mode
|
1664
|
+
text_file = open(file_path, "r", encoding="utf-8")
|
1665
|
+
# read whole file to a string
|
1666
|
+
data = text_file.read()
|
1667
|
+
# close file
|
1668
|
+
text_file.close()
|
1669
|
+
|
1670
|
+
return data
|
1671
|
+
|
1672
|
+
def __call_run_python_script(
|
1673
|
+
self, script_code: str, enable_logging, log_level, progress_interval
|
1674
|
+
):
|
1675
|
+
"""Run the Python script block on the server.
|
1676
|
+
|
1677
|
+
Parameters
|
1678
|
+
----------
|
1679
|
+
script_block : str
|
1680
|
+
Script block (one or more lines) to run.
|
1681
|
+
enable_logging: bool
|
1682
|
+
Whether to enable logging
|
1683
|
+
log_level: str
|
1684
|
+
Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``,
|
1685
|
+
and ``"ERROR"``.
|
1686
|
+
timeout: int, optional
|
1687
|
+
Frequency in milliseconds for getting log messages from the server.
|
1688
|
+
|
1689
|
+
Returns
|
1690
|
+
-------
|
1691
|
+
str
|
1692
|
+
Script result.
|
1693
|
+
|
1694
|
+
"""
|
1695
|
+
log_level_server = self.convert_to_server_log_level(log_level)
|
1696
|
+
request = mechanical_pb2.RunScriptRequest()
|
1697
|
+
request.script_code = script_code
|
1698
|
+
request.enable_logging = enable_logging
|
1699
|
+
request.logger_severity = log_level_server
|
1700
|
+
request.progress_interval = progress_interval
|
1701
|
+
|
1702
|
+
result = ""
|
1703
|
+
self._busy = True
|
1704
|
+
|
1705
|
+
try:
|
1706
|
+
for runscript_response in self._stub.RunPythonScript(request):
|
1707
|
+
if runscript_response.log_info == "__done__":
|
1708
|
+
result = runscript_response.script_result
|
1709
|
+
break
|
1710
|
+
else:
|
1711
|
+
if enable_logging:
|
1712
|
+
self.log_message(log_level, runscript_response.log_info)
|
1713
|
+
except grpc.RpcError as error:
|
1714
|
+
error_info = error.details()
|
1715
|
+
error_info_lower = error_info.lower()
|
1716
|
+
# For the given script, return value cannot be converted to string.
|
1717
|
+
if (
|
1718
|
+
"the expected result" in error_info_lower
|
1719
|
+
and "cannot be return via this API." in error_info
|
1720
|
+
):
|
1721
|
+
if enable_logging:
|
1722
|
+
self.log_debug(f"Ignoring the conversion error.{error_info}")
|
1723
|
+
result = ""
|
1724
|
+
else:
|
1725
|
+
raise
|
1726
|
+
finally:
|
1727
|
+
self._busy = False
|
1728
|
+
|
1729
|
+
self._log_mechanical_script(script_code)
|
1730
|
+
|
1731
|
+
return result
|
1732
|
+
|
1733
|
+
def log_message(self, log_level, message):
|
1734
|
+
"""Log the message using the given log level.
|
1735
|
+
|
1736
|
+
Parameters
|
1737
|
+
----------
|
1738
|
+
log_level: str
|
1739
|
+
Level of logging. Options are ``"DEBUG"``, ``"INFO"``, ``"WARNING"``,
|
1740
|
+
and ``"ERROR"``.
|
1741
|
+
message : str
|
1742
|
+
Message to log.
|
1743
|
+
|
1744
|
+
Examples
|
1745
|
+
--------
|
1746
|
+
Log a debug message.
|
1747
|
+
|
1748
|
+
>>> mechanical.log_message('DEBUG', 'debug message')
|
1749
|
+
|
1750
|
+
Log an info message.
|
1751
|
+
|
1752
|
+
>>> mechanical.log_message('INFO', 'info message')
|
1753
|
+
|
1754
|
+
"""
|
1755
|
+
if log_level == "DEBUG":
|
1756
|
+
self.log_debug(message)
|
1757
|
+
elif log_level == "INFO":
|
1758
|
+
self.log_info(message)
|
1759
|
+
elif log_level == "WARNING":
|
1760
|
+
self.log_warning(message)
|
1761
|
+
elif log_level == "ERROR":
|
1762
|
+
self.log_error(message)
|
1763
|
+
|
1764
|
+
def log_debug(self, message):
|
1765
|
+
"""Log the debug message."""
|
1766
|
+
if self._disable_logging:
|
1767
|
+
return
|
1768
|
+
self._log.debug(message)
|
1769
|
+
|
1770
|
+
def log_info(self, message):
|
1771
|
+
"""Log the info message."""
|
1772
|
+
if self._disable_logging:
|
1773
|
+
return
|
1774
|
+
self._log.info(message)
|
1775
|
+
|
1776
|
+
def log_warning(self, message):
|
1777
|
+
"""Log the warning message."""
|
1778
|
+
if self._disable_logging:
|
1779
|
+
return
|
1780
|
+
self._log.warning(message)
|
1781
|
+
|
1782
|
+
def log_error(self, message):
|
1783
|
+
"""Log the error message."""
|
1784
|
+
if self._disable_logging:
|
1785
|
+
return
|
1786
|
+
self._log.error(message)
|
1787
|
+
|
1788
|
+
def verify_valid_connection(self):
|
1789
|
+
"""Verify whether the connection to Mechanical is valid."""
|
1790
|
+
if self._exited:
|
1791
|
+
raise MechanicalExitedError("Mechanical has already exited.")
|
1792
|
+
|
1793
|
+
if self._stub is None: # pragma: no cover
|
1794
|
+
raise ValueError(
|
1795
|
+
"There is not a valid connection to Mechanical. Launch or connect to it first."
|
1796
|
+
)
|
1797
|
+
|
1798
|
+
@property
|
1799
|
+
def exited(self):
|
1800
|
+
"""Whether Mechanical already exited."""
|
1801
|
+
return self._exited
|
1802
|
+
|
1803
|
+
def _log_mechanical_script(self, script_code):
|
1804
|
+
if self._disable_logging:
|
1805
|
+
return
|
1806
|
+
|
1807
|
+
if self._log_file_mechanical:
|
1808
|
+
try:
|
1809
|
+
with open(self._log_file_mechanical, "a", encoding="utf-8") as file:
|
1810
|
+
file.write(script_code)
|
1811
|
+
file.write("\n")
|
1812
|
+
except IOError as e: # pragma: no cover
|
1813
|
+
self.log_warning(f"I/O error({e.errno}): {e.strerror}")
|
1814
|
+
except Exception as e: # pragma: no cover
|
1815
|
+
self.log_warning("Unexpected error:" + str(e))
|
1816
|
+
|
1817
|
+
|
1818
|
+
def get_start_instance(start_instance_default=True):
|
1819
|
+
"""Check if the ``PYMECHANICAL_START_INSTANCE`` environment variable exists and is valid.
|
1820
|
+
|
1821
|
+
Parameters
|
1822
|
+
----------
|
1823
|
+
start_instance_default : bool, optional
|
1824
|
+
Value to return when ``PYMECHANICAL_START_INSTANCE`` is unset.
|
1825
|
+
|
1826
|
+
Returns
|
1827
|
+
-------
|
1828
|
+
bool
|
1829
|
+
``True`` when the ``PYMECHANICAL_START_INSTANCE`` environment variable exists
|
1830
|
+
and is valid, ``False`` when this environment variable does not exist or is not valid.
|
1831
|
+
If it is unset, ``start_instance_default`` is returned.
|
1832
|
+
|
1833
|
+
Raises
|
1834
|
+
------
|
1835
|
+
OSError
|
1836
|
+
Raised when ``PYMECHANICAL_START_INSTANCE`` is not either ``True`` or ``False``
|
1837
|
+
(case independent).
|
1838
|
+
|
1839
|
+
"""
|
1840
|
+
if "PYMECHANICAL_START_INSTANCE" in os.environ:
|
1841
|
+
if os.environ["PYMECHANICAL_START_INSTANCE"].lower() not in [
|
1842
|
+
"true",
|
1843
|
+
"false",
|
1844
|
+
]: # pragma: no cover
|
1845
|
+
val = os.environ["PYMECHANICAL_START_INSTANCE"]
|
1846
|
+
raise OSError(
|
1847
|
+
f'Invalid value "{val}" for PYMECHANICAL_START_INSTANCE\n'
|
1848
|
+
'PYMECHANICAL_START_INSTANCE should be either "TRUE" or "FALSE"'
|
1849
|
+
)
|
1850
|
+
return os.environ["PYMECHANICAL_START_INSTANCE"].lower() == "true"
|
1851
|
+
return start_instance_default
|
1852
|
+
|
1853
|
+
|
1854
|
+
def launch_grpc(
|
1855
|
+
exec_file="",
|
1856
|
+
batch=True,
|
1857
|
+
port=MECHANICAL_DEFAULT_PORT,
|
1858
|
+
additional_switches=None,
|
1859
|
+
additional_envs=None,
|
1860
|
+
verbose=False,
|
1861
|
+
) -> int:
|
1862
|
+
"""Start Mechanical locally in gRPC mode.
|
1863
|
+
|
1864
|
+
Parameters
|
1865
|
+
----------
|
1866
|
+
exec_file : str, optional
|
1867
|
+
Path for the Mechanical executable file. The default is ``None``, in which
|
1868
|
+
case the cached location is used.
|
1869
|
+
batch : bool, optional
|
1870
|
+
Whether to launch Mechanical in batch mode. The default is ``True``.
|
1871
|
+
When ``False``, Mechanical is launched in UI mode.
|
1872
|
+
port : int, optional
|
1873
|
+
Port to launch the Mechanical instance on. The default is
|
1874
|
+
``MECHANICAL_DEFAULT_PORT``. The final port is the first
|
1875
|
+
port available after (or including) this port.
|
1876
|
+
additional_switches : list, optional
|
1877
|
+
List of additional arguments to pass. The default is ``None``.
|
1878
|
+
additional_envs : dictionary, optional
|
1879
|
+
Dictionary of additional environment variables to pass. The default
|
1880
|
+
is ``None``.
|
1881
|
+
verbose : bool, optional
|
1882
|
+
Whether to print all output when launching and running Mechanical. The
|
1883
|
+
default is ``False``. Printing all output is not recommended unless
|
1884
|
+
you are debugging the startup of Mechanical.
|
1885
|
+
|
1886
|
+
Returns
|
1887
|
+
-------
|
1888
|
+
int
|
1889
|
+
Port number that the Mechanical instance started on.
|
1890
|
+
|
1891
|
+
Notes
|
1892
|
+
-----
|
1893
|
+
If ``PYMECHANICAL_START_INSTANCE`` is set to FALSE, the ``launch_mechanical``
|
1894
|
+
method looks for an existing instance of Mechanical at ``PYMECHANICAL_IP`` on port
|
1895
|
+
``PYMECHANICAL_PORT``, with default to ``127.0.0.1`` and ``10000`` if unset.
|
1896
|
+
This is typically used for automated documentation and testing.
|
1897
|
+
|
1898
|
+
Examples
|
1899
|
+
--------
|
1900
|
+
Launch Mechanical using the default configuration.
|
1901
|
+
|
1902
|
+
>>> from ansys.mechanical.core import launch_mechanical
|
1903
|
+
>>> mechanical = launch_mechanical()
|
1904
|
+
|
1905
|
+
Launch Mechanical using a specified executable file.
|
1906
|
+
|
1907
|
+
>>> exec_file_path = 'C:/Program Files/ANSYS Inc/v251/aisol/bin/win64/AnsysWBU.exe'
|
1908
|
+
>>> mechanical = launch_mechanical(exec_file_path)
|
1909
|
+
|
1910
|
+
"""
|
1911
|
+
# verify version
|
1912
|
+
if atp.version_from_path("mechanical", exec_file) < 232:
|
1913
|
+
raise VersionError("The Mechanical gRPC interface requires Mechanical 2023 R2 or later.")
|
1914
|
+
|
1915
|
+
# get the next available port
|
1916
|
+
local_ports = pymechanical.LOCAL_PORTS
|
1917
|
+
if port is None:
|
1918
|
+
if not local_ports:
|
1919
|
+
port = MECHANICAL_DEFAULT_PORT
|
1920
|
+
else:
|
1921
|
+
port = max(local_ports) + 1
|
1922
|
+
|
1923
|
+
while port_in_use(port) or port in local_ports:
|
1924
|
+
port += 1
|
1925
|
+
local_ports.append(port)
|
1926
|
+
|
1927
|
+
mechanical_launcher = MechanicalLauncher(
|
1928
|
+
batch, port, exec_file, additional_switches, additional_envs, verbose
|
1929
|
+
)
|
1930
|
+
mechanical_launcher.launch()
|
1931
|
+
|
1932
|
+
return port
|
1933
|
+
|
1934
|
+
|
1935
|
+
def launch_remote_mechanical(version=None) -> (grpc.Channel, Instance): # pragma: no cover
|
1936
|
+
"""Start Mechanical remotely using the Product Instance Management (PIM) API.
|
1937
|
+
|
1938
|
+
When calling this method, you must ensure that you are in an environment
|
1939
|
+
where PyPIM is configured. You can use the
|
1940
|
+
:func:`pypim.is_configured <ansys.platform.instancemanagement.is_configured>`
|
1941
|
+
method to verify that PyPIM is configured.
|
1942
|
+
|
1943
|
+
Parameters
|
1944
|
+
----------
|
1945
|
+
version : str, optional
|
1946
|
+
Mechanical version to run in the three-digit format. For example, ``"251"`` to
|
1947
|
+
run 2025 R1. The default is ``None``, in which case the server runs the latest
|
1948
|
+
installed version.
|
1949
|
+
|
1950
|
+
Returns
|
1951
|
+
-------
|
1952
|
+
Tuple containing channel, remote_instance.
|
1953
|
+
"""
|
1954
|
+
pim = pypim.connect()
|
1955
|
+
instance = pim.create_instance(product_name="mechanical", product_version=version)
|
1956
|
+
|
1957
|
+
LOG.info("PyPIM wait for ready has started.")
|
1958
|
+
instance.wait_for_ready()
|
1959
|
+
LOG.info("PyPIM wait for ready has finished.")
|
1960
|
+
|
1961
|
+
channel = instance.build_grpc_channel(
|
1962
|
+
options=[
|
1963
|
+
("grpc.max_receive_message_length", MAX_MESSAGE_LENGTH),
|
1964
|
+
]
|
1965
|
+
)
|
1966
|
+
|
1967
|
+
return channel, instance
|
1968
|
+
|
1969
|
+
|
1970
|
+
def launch_mechanical(
|
1971
|
+
allow_input=True,
|
1972
|
+
exec_file=None,
|
1973
|
+
batch=True,
|
1974
|
+
loglevel="ERROR",
|
1975
|
+
log_file=False,
|
1976
|
+
log_mechanical=None,
|
1977
|
+
additional_switches=None,
|
1978
|
+
additional_envs=None,
|
1979
|
+
start_timeout=120,
|
1980
|
+
port=None,
|
1981
|
+
ip=None,
|
1982
|
+
start_instance=None,
|
1983
|
+
verbose_mechanical=False,
|
1984
|
+
clear_on_connect=False,
|
1985
|
+
cleanup_on_exit=True,
|
1986
|
+
version=None,
|
1987
|
+
keep_connection_alive=True,
|
1988
|
+
) -> Mechanical:
|
1989
|
+
"""Start Mechanical locally.
|
1990
|
+
|
1991
|
+
Parameters
|
1992
|
+
----------
|
1993
|
+
allow_input: bool, optional
|
1994
|
+
Whether to allow user input when discovering the path to the Mechanical
|
1995
|
+
executable file.
|
1996
|
+
exec_file : str, optional
|
1997
|
+
Path for the Mechanical executable file. The default is ``None``,
|
1998
|
+
in which case the cached location is used. If PyPIM is configured
|
1999
|
+
and this parameter is set to ``None``, PyPIM launches Mechanical
|
2000
|
+
using its ``version`` parameter.
|
2001
|
+
batch : bool, optional
|
2002
|
+
Whether to launch Mechanical in batch mode. The default is ``True``.
|
2003
|
+
When ``False``, Mechanical launches in UI mode.
|
2004
|
+
loglevel : str, optional
|
2005
|
+
Level of messages to print to the console.
|
2006
|
+
Options are:
|
2007
|
+
|
2008
|
+
- ``"WARNING"``: Prints only Ansys warning messages.
|
2009
|
+
- ``"ERROR"``: Prints only Ansys error messages.
|
2010
|
+
- ``"INFO"``: Prints all Ansys messages.
|
2011
|
+
|
2012
|
+
The default is ``WARNING``.
|
2013
|
+
log_file : bool, optional
|
2014
|
+
Whether to copy the messages to a file named ``logs.log``, which is
|
2015
|
+
located where the Python script is executed. The default is ``False``.
|
2016
|
+
log_mechanical : str, optional
|
2017
|
+
Path to the output file on the local disk to write every script
|
2018
|
+
command to. The default is ``None``. However, you might set
|
2019
|
+
``"log_mechanical='pymechanical_log.txt'"`` to write all commands that are
|
2020
|
+
sent to Mechanical via PyMechanical to this file. You can then use these
|
2021
|
+
commands to run a script within Mechanical without PyMechanical.
|
2022
|
+
additional_switches : list, optional
|
2023
|
+
Additional switches for Mechanical. The default is ``None``.
|
2024
|
+
additional_envs : dictionary, optional
|
2025
|
+
Dictionary of additional environment variables to pass. The default
|
2026
|
+
is ``None``.
|
2027
|
+
start_timeout : float, optional
|
2028
|
+
Maximum allowable time in seconds to connect to the Mechanical server.
|
2029
|
+
The default is ``120``.
|
2030
|
+
port : int, optional
|
2031
|
+
Port to launch the Mechanical gRPC server on. The default is ``None``,
|
2032
|
+
in which case ``10000`` is used. The final port is the first
|
2033
|
+
port available after (or including) this port. You can override the
|
2034
|
+
default behavior of this parameter with the
|
2035
|
+
``PYMECHANICAL_PORT=<VALID PORT>`` environment variable.
|
2036
|
+
ip : str, optional
|
2037
|
+
IP address to use only when ``start_instance`` is ``False``. The
|
2038
|
+
default is ``None``, in which case ``"127.0.0.1"`` is used. If you
|
2039
|
+
provide an IP address, ``start_instance`` is set to ``False``.
|
2040
|
+
A host name can be provided as an alternative to an IP address.
|
2041
|
+
start_instance : bool, optional
|
2042
|
+
Whether to launch and connect to a new Mechanical instance. The default
|
2043
|
+
is ``None``, in which case an attempt is made to connect to an existing
|
2044
|
+
Mechanical instance at the given ``ip`` and ``port`` parameters, which have
|
2045
|
+
defaults of ``"127.0.0.1"`` and ``10000`` respectively. When ``True``,
|
2046
|
+
a local instance of Mechanical is launched. You can override the default
|
2047
|
+
behavior of this parameter with the ``PYMECHANICAL_START_INSTANCE=FALSE``
|
2048
|
+
environment variable.
|
2049
|
+
verbose_mechanical : bool, optional
|
2050
|
+
Whether to enable printing of all output when launching and running
|
2051
|
+
a Mechanical instance. The default is ``False``. This parameter should be
|
2052
|
+
set to ``True`` for debugging only as output can be tracked within
|
2053
|
+
PyMechanical.
|
2054
|
+
clear_on_connect : bool, optional
|
2055
|
+
When ``start_instance`` is ``False``, whether to clear the environment
|
2056
|
+
when connecting to Mechanical. The default is ``False``. When ``True``,
|
2057
|
+
a fresh environment is provided when you connect to Mechanical.
|
2058
|
+
cleanup_on_exit : bool, optional
|
2059
|
+
Whether to exit Mechanical when Python exits. The default is ``True``.
|
2060
|
+
When ``False``, Mechanical is not exited when the garbage for this Mechanical
|
2061
|
+
instance is collected.
|
2062
|
+
version : str, optional
|
2063
|
+
Mechanical version to run in the three-digit format. For example, ``"251"``
|
2064
|
+
for 2025 R1. The default is ``None``, in which case the server runs the
|
2065
|
+
latest installed version. If PyPIM is configured and ``exce_file=None``,
|
2066
|
+
PyPIM launches Mechanical using its ``version`` parameter.
|
2067
|
+
keep_connection_alive : bool, optional
|
2068
|
+
Whether to keep the gRPC connection alive by running a background thread
|
2069
|
+
and making dummy calls for remote connections. The default is ``True``.
|
2070
|
+
|
2071
|
+
Returns
|
2072
|
+
-------
|
2073
|
+
ansys.mechanical.core.mechanical.Mechanical
|
2074
|
+
Instance of Mechanical.
|
2075
|
+
|
2076
|
+
Notes
|
2077
|
+
-----
|
2078
|
+
If the environment is configured to use `PyPIM <https://pypim.docs.pyansys.com>`_
|
2079
|
+
and ``start_instance=True``, then starting the instance is delegated to PyPIM.
|
2080
|
+
In this case, most of the preceding parameters are ignored because the server-side
|
2081
|
+
configuration is used.
|
2082
|
+
|
2083
|
+
Examples
|
2084
|
+
--------
|
2085
|
+
Launch Mechanical.
|
2086
|
+
|
2087
|
+
>>> from ansys.mechanical.core import launch_mechanical
|
2088
|
+
>>> mech = launch_mechanical()
|
2089
|
+
|
2090
|
+
Launch Mechanical using a specified executable file.
|
2091
|
+
|
2092
|
+
>>> exec_file_path = 'C:/Program Files/ANSYS Inc/v251/aisol/bin/win64/AnsysWBU.exe'
|
2093
|
+
>>> mech = launch_mechanical(exec_file_path)
|
2094
|
+
|
2095
|
+
Connect to an existing Mechanical instance at IP address ``192.168.1.30`` on port
|
2096
|
+
``50001``.
|
2097
|
+
|
2098
|
+
>>> mech = launch_mechanical(start_instance=False, ip='192.168.1.30', port=50001)
|
2099
|
+
"""
|
2100
|
+
# Start Mechanical with PyPIM if the environment is configured for it
|
2101
|
+
# and a directive on how to launch Mechanical was not passed.
|
2102
|
+
if pypim.is_configured() and exec_file is None: # pragma: no cover
|
2103
|
+
LOG.info("Starting Mechanical remotely. The startup configuration will be ignored.")
|
2104
|
+
channel, remote_instance = launch_remote_mechanical(version=version)
|
2105
|
+
return Mechanical(
|
2106
|
+
channel=channel,
|
2107
|
+
remote_instance=remote_instance,
|
2108
|
+
loglevel=loglevel,
|
2109
|
+
log_file=log_file,
|
2110
|
+
log_mechanical=log_mechanical,
|
2111
|
+
timeout=start_timeout,
|
2112
|
+
cleanup_on_exit=cleanup_on_exit,
|
2113
|
+
keep_connection_alive=keep_connection_alive,
|
2114
|
+
)
|
2115
|
+
|
2116
|
+
if ip is None:
|
2117
|
+
ip = os.environ.get("PYMECHANICAL_IP", LOCALHOST)
|
2118
|
+
else: # pragma: no cover
|
2119
|
+
start_instance = False
|
2120
|
+
ip = socket.gethostbyname(ip) # Converting ip or host name to ip
|
2121
|
+
|
2122
|
+
check_valid_ip(ip) # double check
|
2123
|
+
|
2124
|
+
if port is None:
|
2125
|
+
port = int(os.environ.get("PYMECHANICAL_PORT", MECHANICAL_DEFAULT_PORT))
|
2126
|
+
check_valid_port(port)
|
2127
|
+
|
2128
|
+
# connect to an existing instance if enabled
|
2129
|
+
if start_instance is None:
|
2130
|
+
start_instance = check_valid_start_instance(
|
2131
|
+
os.environ.get("PYMECHANICAL_START_INSTANCE", True)
|
2132
|
+
)
|
2133
|
+
|
2134
|
+
# special handling when building the gallery outside of CI. This
|
2135
|
+
# creates an instance of Mechanical the first time if PYMECHANICAL_START_INSTANCE
|
2136
|
+
# is False.
|
2137
|
+
# when you launch, treat it as local.
|
2138
|
+
# when you connect, treat it as remote. We cannot differentiate between
|
2139
|
+
# local vs container scenarios. In the container scenarios, we could be connecting
|
2140
|
+
# to a container using local ip and port
|
2141
|
+
if pymechanical.BUILDING_GALLERY: # pragma: no cover
|
2142
|
+
# launch an instance of PyMechanical if it does not already exist and
|
2143
|
+
# starting instances is allowed
|
2144
|
+
if start_instance and GALLERY_INSTANCE[0] is None:
|
2145
|
+
mechanical = launch_mechanical(
|
2146
|
+
start_instance=True,
|
2147
|
+
cleanup_on_exit=False,
|
2148
|
+
loglevel=loglevel,
|
2149
|
+
)
|
2150
|
+
GALLERY_INSTANCE[0] = {"ip": mechanical._ip, "port": mechanical._port}
|
2151
|
+
return mechanical
|
2152
|
+
|
2153
|
+
# otherwise, connect to the existing gallery instance if available
|
2154
|
+
elif GALLERY_INSTANCE[0] is not None:
|
2155
|
+
mechanical = Mechanical(
|
2156
|
+
ip=GALLERY_INSTANCE[0]["ip"],
|
2157
|
+
port=GALLERY_INSTANCE[0]["port"],
|
2158
|
+
cleanup_on_exit=False,
|
2159
|
+
loglevel=loglevel,
|
2160
|
+
local=False,
|
2161
|
+
)
|
2162
|
+
# we are connecting to the existing gallery instance,
|
2163
|
+
# we need to clear Mechanical.
|
2164
|
+
mechanical.clear()
|
2165
|
+
|
2166
|
+
return mechanical
|
2167
|
+
|
2168
|
+
# finally, if running on CI/CD, connect to the default instance
|
2169
|
+
else:
|
2170
|
+
mechanical = Mechanical(
|
2171
|
+
ip=ip, port=port, cleanup_on_exit=False, loglevel=loglevel, local=False
|
2172
|
+
)
|
2173
|
+
# we are connecting for gallery generation,
|
2174
|
+
# we need to clear Mechanical.
|
2175
|
+
mechanical.clear()
|
2176
|
+
return mechanical
|
2177
|
+
|
2178
|
+
if not start_instance:
|
2179
|
+
mechanical = Mechanical(
|
2180
|
+
ip=ip,
|
2181
|
+
port=port,
|
2182
|
+
loglevel=loglevel,
|
2183
|
+
log_file=log_file,
|
2184
|
+
log_mechanical=log_mechanical,
|
2185
|
+
timeout=start_timeout,
|
2186
|
+
cleanup_on_exit=cleanup_on_exit,
|
2187
|
+
keep_connection_alive=keep_connection_alive,
|
2188
|
+
local=False,
|
2189
|
+
)
|
2190
|
+
if clear_on_connect:
|
2191
|
+
mechanical.clear()
|
2192
|
+
|
2193
|
+
# setting ip for the grpc server
|
2194
|
+
if ip != LOCALHOST: # Default local ip is 127.0.0.1
|
2195
|
+
create_ip_file(ip, os.getcwd())
|
2196
|
+
|
2197
|
+
return mechanical
|
2198
|
+
|
2199
|
+
# verify executable
|
2200
|
+
if exec_file is None:
|
2201
|
+
exec_file = get_mechanical_path(allow_input)
|
2202
|
+
if exec_file is None: # pragma: no cover
|
2203
|
+
raise FileNotFoundError(
|
2204
|
+
"Path to the Mechanical executable file is invalid or cache cannot be loaded. "
|
2205
|
+
"Enter a path manually by specifying a value for the "
|
2206
|
+
"'exec_file' parameter."
|
2207
|
+
)
|
2208
|
+
else: # verify ansys exists at this location
|
2209
|
+
if not os.path.isfile(exec_file):
|
2210
|
+
raise FileNotFoundError(
|
2211
|
+
f'This path for the Mechanical executable is invalid: "{exec_file}"\n'
|
2212
|
+
"Enter a path manually by specifying a value for the "
|
2213
|
+
"'exec_file' parameter."
|
2214
|
+
)
|
2215
|
+
|
2216
|
+
start_parm = {
|
2217
|
+
"exec_file": exec_file,
|
2218
|
+
"batch": batch,
|
2219
|
+
"additional_switches": additional_switches,
|
2220
|
+
"additional_envs": additional_envs,
|
2221
|
+
}
|
2222
|
+
|
2223
|
+
try:
|
2224
|
+
port = launch_grpc(port=port, verbose=verbose_mechanical, **start_parm)
|
2225
|
+
start_parm["local"] = True
|
2226
|
+
mechanical = Mechanical(
|
2227
|
+
ip=ip,
|
2228
|
+
port=port,
|
2229
|
+
loglevel=loglevel,
|
2230
|
+
log_file=log_file,
|
2231
|
+
log_mechanical=log_mechanical,
|
2232
|
+
timeout=start_timeout,
|
2233
|
+
cleanup_on_exit=cleanup_on_exit,
|
2234
|
+
keep_connection_alive=keep_connection_alive,
|
2235
|
+
**start_parm,
|
2236
|
+
)
|
2237
|
+
except Exception as exception: # pragma: no cover
|
2238
|
+
# pass
|
2239
|
+
raise exception
|
2240
|
+
|
2241
|
+
return mechanical
|
2242
|
+
|
2243
|
+
|
2244
|
+
def connect_to_mechanical(
|
2245
|
+
ip=None,
|
2246
|
+
port=None,
|
2247
|
+
loglevel="ERROR",
|
2248
|
+
log_file=False,
|
2249
|
+
log_mechanical=None,
|
2250
|
+
connect_timeout=120,
|
2251
|
+
clear_on_connect=False,
|
2252
|
+
cleanup_on_exit=False,
|
2253
|
+
keep_connection_alive=True,
|
2254
|
+
) -> Mechanical:
|
2255
|
+
"""Connect to an existing Mechanical server instance.
|
2256
|
+
|
2257
|
+
Parameters
|
2258
|
+
----------
|
2259
|
+
ip : str, optional
|
2260
|
+
IP address for connecting to an existing Mechanical instance. The
|
2261
|
+
IP address defaults to ``"127.0.0.1"``.
|
2262
|
+
port : int, optional
|
2263
|
+
Port to listen on for an existing Mechanical instance. The default is ``None``,
|
2264
|
+
in which case ``10000`` is used. You can override the
|
2265
|
+
default behavior of this parameter with the
|
2266
|
+
``PYMECHANICAL_PORT=<VALID PORT>`` environment variable.
|
2267
|
+
loglevel : str, optional
|
2268
|
+
Level of messages to print to the console.
|
2269
|
+
Options are:
|
2270
|
+
|
2271
|
+
- ``"WARNING"``: Prints only Ansys warning messages.
|
2272
|
+
- ``"ERROR"``: Prints only Ansys error messages.
|
2273
|
+
- ``"INFO"``: Prints all Ansys messages.
|
2274
|
+
|
2275
|
+
The default is ``WARNING``.
|
2276
|
+
log_file : bool, optional
|
2277
|
+
Whether to copy the messages to a file named ``logs.log``, which is
|
2278
|
+
located where the Python script is executed. The default is ``False``.
|
2279
|
+
log_mechanical : str, optional
|
2280
|
+
Path to the output file on the local disk to write every script
|
2281
|
+
command to. The default is ``None``. However, you might set
|
2282
|
+
``"log_mechanical='pymechanical_log.txt'"`` to write all commands that are
|
2283
|
+
sent to Mechanical via PyMechanical to this file. You can then use these
|
2284
|
+
commands to run a script within Mechanical without PyMechanical.
|
2285
|
+
connect_timeout : float, optional
|
2286
|
+
Maximum allowable time in seconds to connect to the Mechanical server.
|
2287
|
+
The default is ``120``.
|
2288
|
+
clear_on_connect : bool, optional
|
2289
|
+
Whether to clear the Mechanical instance when connecting. The default is ``False``.
|
2290
|
+
When ``True``, a fresh environment is provided when you connect to Mechanical.
|
2291
|
+
cleanup_on_exit : bool, optional
|
2292
|
+
Whether to exit Mechanical when Python exits. The default is ``False``.
|
2293
|
+
When ``False``, Mechanical is not exited when the garbage for this Mechanical
|
2294
|
+
instance is collected.
|
2295
|
+
keep_connection_alive : bool, optional
|
2296
|
+
Whether to keep the gRPC connection alive by running a background thread
|
2297
|
+
and making dummy calls for remote connections. The default is ``True``.
|
2298
|
+
|
2299
|
+
Returns
|
2300
|
+
-------
|
2301
|
+
ansys.mechanical.core.mechanical.Mechanical
|
2302
|
+
Instance of Mechanical.
|
2303
|
+
|
2304
|
+
Examples
|
2305
|
+
--------
|
2306
|
+
Connect to an existing Mechanical instance at IP address ``192.168.1.30`` on port
|
2307
|
+
``50001``..
|
2308
|
+
|
2309
|
+
|
2310
|
+
>>> from ansys.mechanical.core import connect_to_mechanical
|
2311
|
+
>>> pymech = connect_to_mechanical(ip='192.168.1.30', port=50001)
|
2312
|
+
"""
|
2313
|
+
return launch_mechanical(
|
2314
|
+
start_instance=False,
|
2315
|
+
loglevel=loglevel,
|
2316
|
+
log_file=log_file,
|
2317
|
+
log_mechanical=log_mechanical,
|
2318
|
+
start_timeout=connect_timeout,
|
2319
|
+
port=port,
|
2320
|
+
ip=ip,
|
2321
|
+
clear_on_connect=clear_on_connect,
|
2322
|
+
cleanup_on_exit=cleanup_on_exit,
|
2323
|
+
keep_connection_alive=keep_connection_alive,
|
2324
|
+
)
|