ansys-mechanical-core 0.11.12__py3-none-any.whl → 0.11.13__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.
@@ -1,610 +1,610 @@
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
- """Main application class for embedded Mechanical."""
24
- from __future__ import annotations
25
-
26
- import atexit
27
- import os
28
- from pathlib import Path
29
- import shutil
30
- import typing
31
- import warnings
32
-
33
- from ansys.mechanical.core.embedding import initializer, runtime
34
- from ansys.mechanical.core.embedding.addins import AddinConfiguration
35
- from ansys.mechanical.core.embedding.appdata import UniqueUserProfile
36
- from ansys.mechanical.core.embedding.imports import global_entry_points, global_variables
37
- from ansys.mechanical.core.embedding.poster import Poster
38
- from ansys.mechanical.core.embedding.ui import launch_ui
39
- from ansys.mechanical.core.embedding.warnings import connect_warnings, disconnect_warnings
40
-
41
- if typing.TYPE_CHECKING:
42
- # Make sure to run ``ansys-mechanical-ideconfig`` to add the autocomplete settings to VS Code
43
- # Run ``ansys-mechanical-ideconfig --help`` for more information
44
- import Ansys # pragma: no cover
45
-
46
- try:
47
- import ansys.tools.visualization_interface # noqa: F401
48
-
49
- HAS_ANSYS_VIZ = True
50
- """Whether or not PyVista exists."""
51
- except ImportError:
52
- HAS_ANSYS_VIZ = False
53
-
54
-
55
- def _get_default_addin_configuration() -> AddinConfiguration:
56
- configuration = AddinConfiguration()
57
- return configuration
58
-
59
-
60
- INSTANCES = []
61
- """List of instances."""
62
-
63
-
64
- def _dispose_embedded_app(instances): # pragma: nocover
65
- if len(instances) > 0:
66
- instance = instances[0]
67
- instance._dispose()
68
-
69
-
70
- def _cleanup_private_appdata(profile: UniqueUserProfile):
71
- profile.cleanup()
72
-
73
-
74
- def _start_application(configuration: AddinConfiguration, version, db_file) -> "App":
75
- import clr
76
-
77
- clr.AddReference("Ansys.Mechanical.Embedding")
78
- import Ansys
79
-
80
- if configuration.no_act_addins:
81
- os.environ["ANSYS_MECHANICAL_STANDALONE_NO_ACT_EXTENSIONS"] = "1"
82
-
83
- addin_configuration_name = configuration.addin_configuration
84
- # Starting with version 241 we can pass a configuration name to the constructor
85
- # of Application
86
- if int(version) >= 241:
87
- return Ansys.Mechanical.Embedding.Application(db_file, addin_configuration_name)
88
- else:
89
- return Ansys.Mechanical.Embedding.Application(db_file)
90
-
91
-
92
- class GetterWrapper(object):
93
- """Wrapper class around an attribute of an object."""
94
-
95
- def __init__(self, obj, getter):
96
- """Create a new instance of GetterWrapper."""
97
- # immortal class which provides wrapped object
98
- self.__dict__["_immortal_object"] = obj
99
- # function to get the wrapped object from the immortal class
100
- self.__dict__["_get_wrapped_object"] = getter
101
-
102
- def __getattr__(self, attr):
103
- """Wrap getters to the wrapped object."""
104
- if attr in self.__dict__:
105
- return getattr(self, attr)
106
- return getattr(self._get_wrapped_object(self._immortal_object), attr)
107
-
108
- def __setattr__(self, attr, value):
109
- """Wrap setters to the wrapped object."""
110
- if attr in self.__dict__:
111
- setattr(self, attr, value)
112
- setattr(self._get_wrapped_object(self._immortal_object), attr, value)
113
-
114
-
115
- class App:
116
- """Mechanical embedding Application.
117
-
118
- Parameters
119
- ----------
120
- db_file : str, optional
121
- Path to a mechanical database file (.mechdat or .mechdb).
122
- version : int, optional
123
- Version number of the Mechanical application.
124
- private_appdata : bool, optional
125
- Setting for a temporary AppData directory. Default is False.
126
- Enables running parallel instances of Mechanical.
127
- config : AddinConfiguration, optional
128
- Configuration for addins. By default "Mechanical" is used and ACT Addins are disabled.
129
- copy_profile : bool, optional
130
- Whether to copy the user profile when private_appdata is True. Default is True.
131
-
132
- Examples
133
- --------
134
- Create App with Mechanical project file and version:
135
-
136
- >>> from ansys.mechanical.core import App
137
- >>> app = App(db_file="path/to/file.mechdat", version=251)
138
-
139
- Disable copying the user profile when private appdata is enabled
140
-
141
- >>> app = App(private_appdata=True, copy_profile=False)
142
-
143
- Create App with "Mechanical" configuration and no ACT Addins
144
-
145
- >>> from ansys.mechanical.core.embedding import AddinConfiguration
146
- >>> from ansys.mechanical.core import App
147
- >>> config = AddinConfiguration("Mechanical")
148
- >>> config.no_act_addins = True
149
- >>> app = App(config=config)
150
- """
151
-
152
- def __init__(self, db_file=None, private_appdata=False, **kwargs):
153
- """Construct an instance of the mechanical Application."""
154
- global INSTANCES
155
- from ansys.mechanical.core import BUILDING_GALLERY
156
-
157
- if BUILDING_GALLERY:
158
- if len(INSTANCES) != 0:
159
- instance: App = INSTANCES[0]
160
- instance._share(self)
161
- if db_file is not None:
162
- self.open(db_file)
163
- return
164
- if len(INSTANCES) > 0:
165
- raise Exception("Cannot have more than one embedded mechanical instance!")
166
- version = kwargs.get("version")
167
- if version is not None:
168
- try:
169
- version = int(version)
170
- except ValueError:
171
- raise ValueError(
172
- "The version must be an integer or of type that can be converted to an integer."
173
- )
174
- self._version = initializer.initialize(version)
175
- configuration = kwargs.get("config", _get_default_addin_configuration())
176
-
177
- if private_appdata:
178
- copy_profile = kwargs.get("copy_profile", True)
179
- new_profile_name = f"PyMechanical-{os.getpid()}"
180
- profile = UniqueUserProfile(new_profile_name, copy_profile=copy_profile)
181
- profile.update_environment(os.environ)
182
- atexit.register(_cleanup_private_appdata, profile)
183
-
184
- self._app = _start_application(configuration, self._version, db_file)
185
- runtime.initialize(self._version)
186
- connect_warnings(self)
187
- self._poster = None
188
-
189
- self._disposed = False
190
- atexit.register(_dispose_embedded_app, INSTANCES)
191
- INSTANCES.append(self)
192
- self._updated_scopes: typing.List[typing.Dict[str, typing.Any]] = []
193
- self._subscribe()
194
-
195
- def __repr__(self):
196
- """Get the product info."""
197
- import clr
198
-
199
- clr.AddReference("Ansys.Mechanical.Application")
200
- import Ansys
201
-
202
- return Ansys.Mechanical.Application.ProductInfo.ProductInfoAsString
203
-
204
- def __enter__(self): # pragma: no cover
205
- """Enter the scope."""
206
- return self
207
-
208
- def __exit__(self, exc_type, exc_val, exc_tb): # pragma: no cover
209
- """Exit the scope."""
210
- self._dispose()
211
-
212
- def _dispose(self):
213
- if self._disposed:
214
- return
215
- self._unsubscribe()
216
- disconnect_warnings(self)
217
- self._app.Dispose()
218
- self._disposed = True
219
-
220
- def open(self, db_file, remove_lock=False):
221
- """Open the db file.
222
-
223
- Parameters
224
- ----------
225
- db_file : str
226
- Path to a Mechanical database file (.mechdat or .mechdb).
227
- remove_lock : bool, optional
228
- Whether or not to remove the lock file if it exists before opening the project file.
229
- """
230
- if remove_lock:
231
- lock_file = Path(self.DataModel.Project.ProjectDirectory) / ".mech_lock"
232
- # Remove the lock file if it exists before opening the project file
233
- if lock_file.exists():
234
- warnings.warn(
235
- f"Removing the lock file, {lock_file}, before opening the project. \
236
- This may corrupt the project file.",
237
- UserWarning,
238
- stacklevel=2,
239
- )
240
- lock_file.unlink()
241
-
242
- self.DataModel.Project.Open(db_file)
243
-
244
- def save(self, path=None):
245
- """Save the project."""
246
- if path is not None:
247
- self.DataModel.Project.Save(path)
248
- else:
249
- self.DataModel.Project.Save()
250
-
251
- def save_as(self, path: str, overwrite: bool = False):
252
- """
253
- Save the project as a new file.
254
-
255
- If the `overwrite` flag is enabled, the current saved file is replaced with the new file.
256
-
257
- Parameters
258
- ----------
259
- path : str
260
- The path where the file needs to be saved.
261
- overwrite : bool, optional
262
- Whether the file should be overwritten if it already exists (default is False).
263
-
264
- Raises
265
- ------
266
- Exception
267
- If the file already exists at the specified path and `overwrite` is False.
268
-
269
- Notes
270
- -----
271
- For version 232, if `overwrite` is True, the existing file and its associated directory
272
- (if any) will be removed before saving the new file.
273
- """
274
- if not os.path.exists(path):
275
- self.DataModel.Project.SaveAs(path)
276
- return
277
-
278
- if not overwrite:
279
- raise Exception(
280
- f"File already exists in {path}, Use ``overwrite`` flag to "
281
- "replace the existing file."
282
- )
283
- if self.version < 241: # pragma: no cover
284
- file_name = os.path.basename(path)
285
- file_dir = os.path.dirname(path)
286
- associated_dir = os.path.join(file_dir, os.path.splitext(file_name)[0] + "_Mech_Files")
287
-
288
- # Remove existing files and associated folder
289
- os.remove(path)
290
- if os.path.exists(associated_dir):
291
- shutil.rmtree(associated_dir)
292
- # Save the new file
293
- self.DataModel.Project.SaveAs(path)
294
- else:
295
- self.DataModel.Project.SaveAs(path, overwrite)
296
-
297
- def launch_gui(self, delete_tmp_on_close: bool = True, dry_run: bool = False):
298
- """Launch the GUI."""
299
- launch_ui(self, delete_tmp_on_close, dry_run)
300
-
301
- def new(self):
302
- """Clear to a new application."""
303
- self.DataModel.Project.New()
304
-
305
- def close(self):
306
- """Close the active project."""
307
- # Call New() to remove the lock file of the
308
- # current project on close.
309
- self.DataModel.Project.New()
310
-
311
- def exit(self):
312
- """Exit the application."""
313
- self._unsubscribe()
314
- if self.version < 241:
315
- self.ExtAPI.Application.Close()
316
- else:
317
- self.ExtAPI.Application.Exit()
318
-
319
- def execute_script(self, script: str) -> typing.Any:
320
- """Execute the given script with the internal IronPython engine."""
321
- SCRIPT_SCOPE = "pymechanical-internal"
322
- if not hasattr(self, "script_engine"):
323
- import clr
324
-
325
- clr.AddReference("Ansys.Mechanical.Scripting")
326
- import Ansys
327
-
328
- engine_type = Ansys.Mechanical.Scripting.ScriptEngineType.IronPython
329
- script_engine = Ansys.Mechanical.Scripting.EngineFactory.CreateEngine(engine_type)
330
- empty_scope = False
331
- debug_mode = False
332
- script_engine.CreateScope(SCRIPT_SCOPE, empty_scope, debug_mode)
333
- self.script_engine = script_engine
334
- light_mode = True
335
- args = None
336
- rets = None
337
- script_result = self.script_engine.ExecuteCode(script, SCRIPT_SCOPE, light_mode, args, rets)
338
- error_msg = f"Failed to execute the script"
339
- if script_result is None:
340
- raise Exception(error_msg)
341
- if script_result.Error is not None:
342
- error_msg += f": {script_result.Error.Message}"
343
- raise Exception(error_msg)
344
- return script_result.Value
345
-
346
- def execute_script_from_file(self, file_path=None):
347
- """Execute the given script from file with the internal IronPython engine."""
348
- text_file = open(file_path, "r", encoding="utf-8")
349
- data = text_file.read()
350
- text_file.close()
351
- return self.execute_script(data)
352
-
353
- def plotter(self) -> None:
354
- """Return ``ansys.tools.visualization_interface.Plotter`` object."""
355
- if not HAS_ANSYS_VIZ:
356
- warnings.warn(
357
- "Installation of viz option required! Use pip install ansys-mechanical-core[viz]"
358
- )
359
- return
360
-
361
- if self.version < 242:
362
- warnings.warn("Plotting is only supported with version 2024R2 and later!")
363
- return
364
-
365
- # TODO Check if anything loaded inside app or else show warning and return
366
-
367
- from ansys.mechanical.core.embedding.viz.embedding_plotter import to_plotter
368
-
369
- return to_plotter(self)
370
-
371
- def plot(self) -> None:
372
- """Visualize the model in 3d.
373
-
374
- Requires installation using the viz option. E.g.
375
- pip install ansys-mechanical-core[viz]
376
-
377
- Examples
378
- --------
379
- >>> from ansys.mechanical.core import App
380
- >>> app = App()
381
- >>> app.open("path/to/file.mechdat")
382
- >>> app.plot()
383
- """
384
- _plotter = self.plotter()
385
-
386
- if _plotter is None:
387
- return
388
-
389
- return _plotter.show()
390
-
391
- @property
392
- def poster(self) -> Poster:
393
- """Returns an instance of Poster."""
394
- if self._poster is None:
395
- self._poster = Poster()
396
- return self._poster
397
-
398
- @property
399
- def DataModel(self) -> Ansys.Mechanical.DataModel.Interfaces.DataModelObject:
400
- """Return the DataModel."""
401
- return GetterWrapper(self._app, lambda app: app.DataModel)
402
-
403
- @property
404
- def ExtAPI(self) -> Ansys.ACT.Interfaces.Mechanical.IMechanicalExtAPI:
405
- """Return the ExtAPI object."""
406
- return GetterWrapper(self._app, lambda app: app.ExtAPI)
407
-
408
- @property
409
- def Tree(self) -> Ansys.ACT.Automation.Mechanical.Tree:
410
- """Return the Tree object."""
411
- return GetterWrapper(self._app, lambda app: app.DataModel.Tree)
412
-
413
- @property
414
- def Model(self) -> Ansys.ACT.Automation.Mechanical.Model:
415
- """Return the Model object."""
416
- return GetterWrapper(self._app, lambda app: app.DataModel.Project.Model)
417
-
418
- @property
419
- def Graphics(self) -> Ansys.ACT.Common.Graphics.MechanicalGraphicsWrapper:
420
- """Return the Graphics object."""
421
- return GetterWrapper(self._app, lambda app: app.ExtAPI.Graphics)
422
-
423
- @property
424
- def readonly(self):
425
- """Return whether the Mechanical object is read-only."""
426
- import Ansys
427
-
428
- return Ansys.ACT.Mechanical.MechanicalAPI.Instance.ReadOnlyMode
429
-
430
- @property
431
- def version(self):
432
- """Returns the version of the app."""
433
- return self._version
434
-
435
- @property
436
- def project_directory(self):
437
- """Returns the current project directory."""
438
- return self.DataModel.Project.ProjectDirectory
439
-
440
- def _share(self, other) -> None:
441
- """Shares the state of self with other.
442
-
443
- Other is another instance of App.
444
- This is used when the BUILDING_GALLERY flag is on.
445
- In that mode, multiple instance of App are used, but
446
- they all point to the same underlying application
447
- object. Because of that, special care needs to be
448
- taken to properly share the state. Other will be
449
- a "weak reference", which doesn't own anything.
450
- """
451
- # the other app is not expecting to have a project
452
- # already loaded
453
- self.new()
454
-
455
- # set up the type hint (typing.Self is python3.11+)
456
- other: App = other
457
-
458
- # copy `self` state to other.
459
- other._app = self._app
460
- other._version = self._version
461
- other._poster = self._poster
462
- other._updated_scopes = self._updated_scopes
463
-
464
- # all events will be handled by the original App instance
465
- other._subscribed = False
466
-
467
- # finally, set the other disposed flag to be true
468
- # so that the shutdown sequence isn't duplicated
469
- other._disposed = True
470
-
471
- def _subscribe(self):
472
- try:
473
- # This will throw an error when using pythonnet because
474
- # EventSource isn't defined on the IApplication interface
475
- self.ExtAPI.Application.EventSource.OnWorkbenchReady += self._on_workbench_ready
476
- self._subscribed = True
477
- except:
478
- self._subscribed = False
479
-
480
- def _unsubscribe(self):
481
- if not self._subscribed:
482
- return
483
- self._subscribed = False
484
- self.ExtAPI.Application.EventSource.OnWorkbenchReady -= self._on_workbench_ready
485
-
486
- def _on_workbench_ready(self, sender, args) -> None:
487
- self._update_all_globals()
488
-
489
- def update_globals(
490
- self, globals_dict: typing.Dict[str, typing.Any], enums: bool = True
491
- ) -> None:
492
- """Update global variables.
493
-
494
- When scripting inside Mechanical, the Mechanical UI automatically
495
- sets global variables in Python. PyMechanical cannot do that automatically,
496
- but this method can be used.
497
-
498
- By default, all enums will be imported too. To avoid including enums, set
499
- the `enums` argument to False.
500
-
501
- Examples
502
- --------
503
- >>> from ansys.mechanical.core import App
504
- >>> app = App()
505
- >>> app.update_globals(globals())
506
- """
507
- self._updated_scopes.append(globals_dict)
508
- globals_dict.update(global_variables(self, enums))
509
-
510
- def _update_all_globals(self) -> None:
511
- for scope in self._updated_scopes:
512
- scope.update(global_entry_points(self))
513
-
514
- def _print_tree(self, node, max_lines, lines_count, indentation):
515
- """Recursively print till provided maximum lines limit.
516
-
517
- Each object in the tree is expected to have the following attributes:
518
- - Name: The name of the object.
519
- - Suppressed : Print as suppressed, if object is suppressed.
520
- - Children: Checks if object have children.
521
- Each child node is expected to have the all these attributes.
522
-
523
- Parameters
524
- ----------
525
- lines_count: int, optional
526
- The current count of lines printed. Default is 0.
527
- indentation: str, optional
528
- The indentation string used for printing the tree structure. Default is "".
529
- """
530
- if lines_count >= max_lines and max_lines != -1:
531
- print(f"... truncating after {max_lines} lines")
532
- return lines_count
533
-
534
- if not hasattr(node, "Name"):
535
- raise AttributeError("Object must have a 'Name' attribute")
536
-
537
- node_name = node.Name
538
- if hasattr(node, "Suppressed") and node.Suppressed is True:
539
- node_name += " (Suppressed)"
540
- if hasattr(node, "ObjectState"):
541
- if str(node.ObjectState) == "UnderDefined":
542
- node_name += " (?)"
543
- elif str(node.ObjectState) == "Solved" or str(node.ObjectState) == "FullyDefined":
544
- node_name += " (✓)"
545
- elif str(node.ObjectState) == "NotSolved" or str(node.ObjectState) == "Obsolete":
546
- node_name += " (⚡︎)"
547
- elif str(node.ObjectState) == "SolveFailed":
548
- node_name += " (✕)"
549
- print(f"{indentation}├── {node_name}")
550
- lines_count += 1
551
-
552
- if lines_count >= max_lines and max_lines != -1:
553
- print(f"... truncating after {max_lines} lines")
554
- return lines_count
555
-
556
- if hasattr(node, "Children") and node.Children is not None and node.Children.Count > 0:
557
- for child in node.Children:
558
- lines_count = self._print_tree(child, max_lines, lines_count, indentation + "| ")
559
- if lines_count >= max_lines and max_lines != -1:
560
- break
561
-
562
- return lines_count
563
-
564
- def print_tree(self, node=None, max_lines=80, lines_count=0, indentation=""):
565
- """
566
- Print the hierarchical tree representation of the Mechanical project structure.
567
-
568
- Parameters
569
- ----------
570
- node: DataModel object, optional
571
- The starting object of the tree.
572
- max_lines: int, optional
573
- The maximum number of lines to print. Default is 80. If set to -1, no limit is applied.
574
-
575
- Raises
576
- ------
577
- AttributeError
578
- If the node does not have the required attributes.
579
-
580
- Examples
581
- --------
582
- >>> from ansys.mechanical.core import App
583
- >>> app = App()
584
- >>> app.update_globals(globals())
585
- >>> app.print_tree()
586
- ... ├── Project
587
- ... | ├── Model
588
- ... | | ├── Geometry Imports (⚡︎)
589
- ... | | ├── Geometry (?)
590
- ... | | ├── Materials (✓)
591
- ... | | ├── Coordinate Systems (✓)
592
- ... | | | ├── Global Coordinate System (✓)
593
- ... | | ├── Remote Points (✓)
594
- ... | | ├── Mesh (?)
595
-
596
- >>> app.print_tree(Model, 3)
597
- ... ├── Model
598
- ... | ├── Geometry Imports (⚡︎)
599
- ... | ├── Geometry (?)
600
- ... ... truncating after 3 lines
601
-
602
- >>> app.print_tree(max_lines=2)
603
- ... ├── Project
604
- ... | ├── Model
605
- ... ... truncating after 2 lines
606
- """
607
- if node is None:
608
- node = self.DataModel.Project
609
-
610
- self._print_tree(node, max_lines, lines_count, indentation)
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
+ """Main application class for embedded Mechanical."""
24
+ from __future__ import annotations
25
+
26
+ import atexit
27
+ import os
28
+ from pathlib import Path
29
+ import shutil
30
+ import typing
31
+ import warnings
32
+
33
+ from ansys.mechanical.core.embedding import initializer, runtime
34
+ from ansys.mechanical.core.embedding.addins import AddinConfiguration
35
+ from ansys.mechanical.core.embedding.appdata import UniqueUserProfile
36
+ from ansys.mechanical.core.embedding.imports import global_entry_points, global_variables
37
+ from ansys.mechanical.core.embedding.poster import Poster
38
+ from ansys.mechanical.core.embedding.ui import launch_ui
39
+ from ansys.mechanical.core.embedding.warnings import connect_warnings, disconnect_warnings
40
+
41
+ if typing.TYPE_CHECKING:
42
+ # Make sure to run ``ansys-mechanical-ideconfig`` to add the autocomplete settings to VS Code
43
+ # Run ``ansys-mechanical-ideconfig --help`` for more information
44
+ import Ansys # pragma: no cover
45
+
46
+ try:
47
+ import ansys.tools.visualization_interface # noqa: F401
48
+
49
+ HAS_ANSYS_VIZ = True
50
+ """Whether or not PyVista exists."""
51
+ except ImportError:
52
+ HAS_ANSYS_VIZ = False
53
+
54
+
55
+ def _get_default_addin_configuration() -> AddinConfiguration:
56
+ configuration = AddinConfiguration()
57
+ return configuration
58
+
59
+
60
+ INSTANCES = []
61
+ """List of instances."""
62
+
63
+
64
+ def _dispose_embedded_app(instances): # pragma: nocover
65
+ if len(instances) > 0:
66
+ instance = instances[0]
67
+ instance._dispose()
68
+
69
+
70
+ def _cleanup_private_appdata(profile: UniqueUserProfile):
71
+ profile.cleanup()
72
+
73
+
74
+ def _start_application(configuration: AddinConfiguration, version, db_file) -> "App":
75
+ import clr
76
+
77
+ clr.AddReference("Ansys.Mechanical.Embedding")
78
+ import Ansys
79
+
80
+ if configuration.no_act_addins:
81
+ os.environ["ANSYS_MECHANICAL_STANDALONE_NO_ACT_EXTENSIONS"] = "1"
82
+
83
+ addin_configuration_name = configuration.addin_configuration
84
+ # Starting with version 241 we can pass a configuration name to the constructor
85
+ # of Application
86
+ if int(version) >= 241:
87
+ return Ansys.Mechanical.Embedding.Application(db_file, addin_configuration_name)
88
+ else:
89
+ return Ansys.Mechanical.Embedding.Application(db_file)
90
+
91
+
92
+ class GetterWrapper(object):
93
+ """Wrapper class around an attribute of an object."""
94
+
95
+ def __init__(self, obj, getter):
96
+ """Create a new instance of GetterWrapper."""
97
+ # immortal class which provides wrapped object
98
+ self.__dict__["_immortal_object"] = obj
99
+ # function to get the wrapped object from the immortal class
100
+ self.__dict__["_get_wrapped_object"] = getter
101
+
102
+ def __getattr__(self, attr):
103
+ """Wrap getters to the wrapped object."""
104
+ if attr in self.__dict__:
105
+ return getattr(self, attr)
106
+ return getattr(self._get_wrapped_object(self._immortal_object), attr)
107
+
108
+ def __setattr__(self, attr, value):
109
+ """Wrap setters to the wrapped object."""
110
+ if attr in self.__dict__:
111
+ setattr(self, attr, value)
112
+ setattr(self._get_wrapped_object(self._immortal_object), attr, value)
113
+
114
+
115
+ class App:
116
+ """Mechanical embedding Application.
117
+
118
+ Parameters
119
+ ----------
120
+ db_file : str, optional
121
+ Path to a mechanical database file (.mechdat or .mechdb).
122
+ version : int, optional
123
+ Version number of the Mechanical application.
124
+ private_appdata : bool, optional
125
+ Setting for a temporary AppData directory. Default is False.
126
+ Enables running parallel instances of Mechanical.
127
+ config : AddinConfiguration, optional
128
+ Configuration for addins. By default "Mechanical" is used and ACT Addins are disabled.
129
+ copy_profile : bool, optional
130
+ Whether to copy the user profile when private_appdata is True. Default is True.
131
+
132
+ Examples
133
+ --------
134
+ Create App with Mechanical project file and version:
135
+
136
+ >>> from ansys.mechanical.core import App
137
+ >>> app = App(db_file="path/to/file.mechdat", version=251)
138
+
139
+ Disable copying the user profile when private appdata is enabled
140
+
141
+ >>> app = App(private_appdata=True, copy_profile=False)
142
+
143
+ Create App with "Mechanical" configuration and no ACT Addins
144
+
145
+ >>> from ansys.mechanical.core.embedding import AddinConfiguration
146
+ >>> from ansys.mechanical.core import App
147
+ >>> config = AddinConfiguration("Mechanical")
148
+ >>> config.no_act_addins = True
149
+ >>> app = App(config=config)
150
+ """
151
+
152
+ def __init__(self, db_file=None, private_appdata=False, **kwargs):
153
+ """Construct an instance of the mechanical Application."""
154
+ global INSTANCES
155
+ from ansys.mechanical.core import BUILDING_GALLERY
156
+
157
+ if BUILDING_GALLERY:
158
+ if len(INSTANCES) != 0:
159
+ instance: App = INSTANCES[0]
160
+ instance._share(self)
161
+ if db_file is not None:
162
+ self.open(db_file)
163
+ return
164
+ if len(INSTANCES) > 0:
165
+ raise Exception("Cannot have more than one embedded mechanical instance!")
166
+ version = kwargs.get("version")
167
+ if version is not None:
168
+ try:
169
+ version = int(version)
170
+ except ValueError:
171
+ raise ValueError(
172
+ "The version must be an integer or of type that can be converted to an integer."
173
+ )
174
+ self._version = initializer.initialize(version)
175
+ configuration = kwargs.get("config", _get_default_addin_configuration())
176
+
177
+ if private_appdata:
178
+ copy_profile = kwargs.get("copy_profile", True)
179
+ new_profile_name = f"PyMechanical-{os.getpid()}"
180
+ profile = UniqueUserProfile(new_profile_name, copy_profile=copy_profile)
181
+ profile.update_environment(os.environ)
182
+ atexit.register(_cleanup_private_appdata, profile)
183
+
184
+ runtime.initialize(self._version)
185
+ self._app = _start_application(configuration, self._version, db_file)
186
+ connect_warnings(self)
187
+ self._poster = None
188
+
189
+ self._disposed = False
190
+ atexit.register(_dispose_embedded_app, INSTANCES)
191
+ INSTANCES.append(self)
192
+ self._updated_scopes: typing.List[typing.Dict[str, typing.Any]] = []
193
+ self._subscribe()
194
+
195
+ def __repr__(self):
196
+ """Get the product info."""
197
+ import clr
198
+
199
+ clr.AddReference("Ansys.Mechanical.Application")
200
+ import Ansys
201
+
202
+ return Ansys.Mechanical.Application.ProductInfo.ProductInfoAsString
203
+
204
+ def __enter__(self): # pragma: no cover
205
+ """Enter the scope."""
206
+ return self
207
+
208
+ def __exit__(self, exc_type, exc_val, exc_tb): # pragma: no cover
209
+ """Exit the scope."""
210
+ self._dispose()
211
+
212
+ def _dispose(self):
213
+ if self._disposed:
214
+ return
215
+ self._unsubscribe()
216
+ disconnect_warnings(self)
217
+ self._app.Dispose()
218
+ self._disposed = True
219
+
220
+ def open(self, db_file, remove_lock=False):
221
+ """Open the db file.
222
+
223
+ Parameters
224
+ ----------
225
+ db_file : str
226
+ Path to a Mechanical database file (.mechdat or .mechdb).
227
+ remove_lock : bool, optional
228
+ Whether or not to remove the lock file if it exists before opening the project file.
229
+ """
230
+ if remove_lock:
231
+ lock_file = Path(self.DataModel.Project.ProjectDirectory) / ".mech_lock"
232
+ # Remove the lock file if it exists before opening the project file
233
+ if lock_file.exists():
234
+ warnings.warn(
235
+ f"Removing the lock file, {lock_file}, before opening the project. \
236
+ This may corrupt the project file.",
237
+ UserWarning,
238
+ stacklevel=2,
239
+ )
240
+ lock_file.unlink()
241
+
242
+ self.DataModel.Project.Open(db_file)
243
+
244
+ def save(self, path=None):
245
+ """Save the project."""
246
+ if path is not None:
247
+ self.DataModel.Project.Save(path)
248
+ else:
249
+ self.DataModel.Project.Save()
250
+
251
+ def save_as(self, path: str, overwrite: bool = False):
252
+ """
253
+ Save the project as a new file.
254
+
255
+ If the `overwrite` flag is enabled, the current saved file is replaced with the new file.
256
+
257
+ Parameters
258
+ ----------
259
+ path : str
260
+ The path where the file needs to be saved.
261
+ overwrite : bool, optional
262
+ Whether the file should be overwritten if it already exists (default is False).
263
+
264
+ Raises
265
+ ------
266
+ Exception
267
+ If the file already exists at the specified path and `overwrite` is False.
268
+
269
+ Notes
270
+ -----
271
+ For version 232, if `overwrite` is True, the existing file and its associated directory
272
+ (if any) will be removed before saving the new file.
273
+ """
274
+ if not os.path.exists(path):
275
+ self.DataModel.Project.SaveAs(path)
276
+ return
277
+
278
+ if not overwrite:
279
+ raise Exception(
280
+ f"File already exists in {path}, Use ``overwrite`` flag to "
281
+ "replace the existing file."
282
+ )
283
+ if self.version < 241: # pragma: no cover
284
+ file_name = os.path.basename(path)
285
+ file_dir = os.path.dirname(path)
286
+ associated_dir = os.path.join(file_dir, os.path.splitext(file_name)[0] + "_Mech_Files")
287
+
288
+ # Remove existing files and associated folder
289
+ os.remove(path)
290
+ if os.path.exists(associated_dir):
291
+ shutil.rmtree(associated_dir)
292
+ # Save the new file
293
+ self.DataModel.Project.SaveAs(path)
294
+ else:
295
+ self.DataModel.Project.SaveAs(path, overwrite)
296
+
297
+ def launch_gui(self, delete_tmp_on_close: bool = True, dry_run: bool = False):
298
+ """Launch the GUI."""
299
+ launch_ui(self, delete_tmp_on_close, dry_run)
300
+
301
+ def new(self):
302
+ """Clear to a new application."""
303
+ self.DataModel.Project.New()
304
+
305
+ def close(self):
306
+ """Close the active project."""
307
+ # Call New() to remove the lock file of the
308
+ # current project on close.
309
+ self.DataModel.Project.New()
310
+
311
+ def exit(self):
312
+ """Exit the application."""
313
+ self._unsubscribe()
314
+ if self.version < 241:
315
+ self.ExtAPI.Application.Close()
316
+ else:
317
+ self.ExtAPI.Application.Exit()
318
+
319
+ def execute_script(self, script: str) -> typing.Any:
320
+ """Execute the given script with the internal IronPython engine."""
321
+ SCRIPT_SCOPE = "pymechanical-internal"
322
+ if not hasattr(self, "script_engine"):
323
+ import clr
324
+
325
+ clr.AddReference("Ansys.Mechanical.Scripting")
326
+ import Ansys
327
+
328
+ engine_type = Ansys.Mechanical.Scripting.ScriptEngineType.IronPython
329
+ script_engine = Ansys.Mechanical.Scripting.EngineFactory.CreateEngine(engine_type)
330
+ empty_scope = False
331
+ debug_mode = False
332
+ script_engine.CreateScope(SCRIPT_SCOPE, empty_scope, debug_mode)
333
+ self.script_engine = script_engine
334
+ light_mode = True
335
+ args = None
336
+ rets = None
337
+ script_result = self.script_engine.ExecuteCode(script, SCRIPT_SCOPE, light_mode, args, rets)
338
+ error_msg = f"Failed to execute the script"
339
+ if script_result is None:
340
+ raise Exception(error_msg)
341
+ if script_result.Error is not None:
342
+ error_msg += f": {script_result.Error.Message}"
343
+ raise Exception(error_msg)
344
+ return script_result.Value
345
+
346
+ def execute_script_from_file(self, file_path=None):
347
+ """Execute the given script from file with the internal IronPython engine."""
348
+ text_file = open(file_path, "r", encoding="utf-8")
349
+ data = text_file.read()
350
+ text_file.close()
351
+ return self.execute_script(data)
352
+
353
+ def plotter(self) -> None:
354
+ """Return ``ansys.tools.visualization_interface.Plotter`` object."""
355
+ if not HAS_ANSYS_VIZ:
356
+ warnings.warn(
357
+ "Installation of viz option required! Use pip install ansys-mechanical-core[viz]"
358
+ )
359
+ return
360
+
361
+ if self.version < 242:
362
+ warnings.warn("Plotting is only supported with version 2024R2 and later!")
363
+ return
364
+
365
+ # TODO Check if anything loaded inside app or else show warning and return
366
+
367
+ from ansys.mechanical.core.embedding.viz.embedding_plotter import to_plotter
368
+
369
+ return to_plotter(self)
370
+
371
+ def plot(self) -> None:
372
+ """Visualize the model in 3d.
373
+
374
+ Requires installation using the viz option. E.g.
375
+ pip install ansys-mechanical-core[viz]
376
+
377
+ Examples
378
+ --------
379
+ >>> from ansys.mechanical.core import App
380
+ >>> app = App()
381
+ >>> app.open("path/to/file.mechdat")
382
+ >>> app.plot()
383
+ """
384
+ _plotter = self.plotter()
385
+
386
+ if _plotter is None:
387
+ return
388
+
389
+ return _plotter.show()
390
+
391
+ @property
392
+ def poster(self) -> Poster:
393
+ """Returns an instance of Poster."""
394
+ if self._poster is None:
395
+ self._poster = Poster()
396
+ return self._poster
397
+
398
+ @property
399
+ def DataModel(self) -> Ansys.Mechanical.DataModel.Interfaces.DataModelObject:
400
+ """Return the DataModel."""
401
+ return GetterWrapper(self._app, lambda app: app.DataModel)
402
+
403
+ @property
404
+ def ExtAPI(self) -> Ansys.ACT.Interfaces.Mechanical.IMechanicalExtAPI:
405
+ """Return the ExtAPI object."""
406
+ return GetterWrapper(self._app, lambda app: app.ExtAPI)
407
+
408
+ @property
409
+ def Tree(self) -> Ansys.ACT.Automation.Mechanical.Tree:
410
+ """Return the Tree object."""
411
+ return GetterWrapper(self._app, lambda app: app.DataModel.Tree)
412
+
413
+ @property
414
+ def Model(self) -> Ansys.ACT.Automation.Mechanical.Model:
415
+ """Return the Model object."""
416
+ return GetterWrapper(self._app, lambda app: app.DataModel.Project.Model)
417
+
418
+ @property
419
+ def Graphics(self) -> Ansys.ACT.Common.Graphics.MechanicalGraphicsWrapper:
420
+ """Return the Graphics object."""
421
+ return GetterWrapper(self._app, lambda app: app.ExtAPI.Graphics)
422
+
423
+ @property
424
+ def readonly(self):
425
+ """Return whether the Mechanical object is read-only."""
426
+ import Ansys
427
+
428
+ return Ansys.ACT.Mechanical.MechanicalAPI.Instance.ReadOnlyMode
429
+
430
+ @property
431
+ def version(self):
432
+ """Returns the version of the app."""
433
+ return self._version
434
+
435
+ @property
436
+ def project_directory(self):
437
+ """Returns the current project directory."""
438
+ return self.DataModel.Project.ProjectDirectory
439
+
440
+ def _share(self, other) -> None:
441
+ """Shares the state of self with other.
442
+
443
+ Other is another instance of App.
444
+ This is used when the BUILDING_GALLERY flag is on.
445
+ In that mode, multiple instance of App are used, but
446
+ they all point to the same underlying application
447
+ object. Because of that, special care needs to be
448
+ taken to properly share the state. Other will be
449
+ a "weak reference", which doesn't own anything.
450
+ """
451
+ # the other app is not expecting to have a project
452
+ # already loaded
453
+ self.new()
454
+
455
+ # set up the type hint (typing.Self is python3.11+)
456
+ other: App = other
457
+
458
+ # copy `self` state to other.
459
+ other._app = self._app
460
+ other._version = self._version
461
+ other._poster = self._poster
462
+ other._updated_scopes = self._updated_scopes
463
+
464
+ # all events will be handled by the original App instance
465
+ other._subscribed = False
466
+
467
+ # finally, set the other disposed flag to be true
468
+ # so that the shutdown sequence isn't duplicated
469
+ other._disposed = True
470
+
471
+ def _subscribe(self):
472
+ try:
473
+ # This will throw an error when using pythonnet because
474
+ # EventSource isn't defined on the IApplication interface
475
+ self.ExtAPI.Application.EventSource.OnWorkbenchReady += self._on_workbench_ready
476
+ self._subscribed = True
477
+ except:
478
+ self._subscribed = False
479
+
480
+ def _unsubscribe(self):
481
+ if not self._subscribed:
482
+ return
483
+ self._subscribed = False
484
+ self.ExtAPI.Application.EventSource.OnWorkbenchReady -= self._on_workbench_ready
485
+
486
+ def _on_workbench_ready(self, sender, args) -> None:
487
+ self._update_all_globals()
488
+
489
+ def update_globals(
490
+ self, globals_dict: typing.Dict[str, typing.Any], enums: bool = True
491
+ ) -> None:
492
+ """Update global variables.
493
+
494
+ When scripting inside Mechanical, the Mechanical UI automatically
495
+ sets global variables in Python. PyMechanical cannot do that automatically,
496
+ but this method can be used.
497
+
498
+ By default, all enums will be imported too. To avoid including enums, set
499
+ the `enums` argument to False.
500
+
501
+ Examples
502
+ --------
503
+ >>> from ansys.mechanical.core import App
504
+ >>> app = App()
505
+ >>> app.update_globals(globals())
506
+ """
507
+ self._updated_scopes.append(globals_dict)
508
+ globals_dict.update(global_variables(self, enums))
509
+
510
+ def _update_all_globals(self) -> None:
511
+ for scope in self._updated_scopes:
512
+ scope.update(global_entry_points(self))
513
+
514
+ def _print_tree(self, node, max_lines, lines_count, indentation):
515
+ """Recursively print till provided maximum lines limit.
516
+
517
+ Each object in the tree is expected to have the following attributes:
518
+ - Name: The name of the object.
519
+ - Suppressed : Print as suppressed, if object is suppressed.
520
+ - Children: Checks if object have children.
521
+ Each child node is expected to have the all these attributes.
522
+
523
+ Parameters
524
+ ----------
525
+ lines_count: int, optional
526
+ The current count of lines printed. Default is 0.
527
+ indentation: str, optional
528
+ The indentation string used for printing the tree structure. Default is "".
529
+ """
530
+ if lines_count >= max_lines and max_lines != -1:
531
+ print(f"... truncating after {max_lines} lines")
532
+ return lines_count
533
+
534
+ if not hasattr(node, "Name"):
535
+ raise AttributeError("Object must have a 'Name' attribute")
536
+
537
+ node_name = node.Name
538
+ if hasattr(node, "Suppressed") and node.Suppressed is True:
539
+ node_name += " (Suppressed)"
540
+ if hasattr(node, "ObjectState"):
541
+ if str(node.ObjectState) == "UnderDefined":
542
+ node_name += " (?)"
543
+ elif str(node.ObjectState) == "Solved" or str(node.ObjectState) == "FullyDefined":
544
+ node_name += " (✓)"
545
+ elif str(node.ObjectState) == "NotSolved" or str(node.ObjectState) == "Obsolete":
546
+ node_name += " (⚡︎)"
547
+ elif str(node.ObjectState) == "SolveFailed":
548
+ node_name += " (✕)"
549
+ print(f"{indentation}├── {node_name}")
550
+ lines_count += 1
551
+
552
+ if lines_count >= max_lines and max_lines != -1:
553
+ print(f"... truncating after {max_lines} lines")
554
+ return lines_count
555
+
556
+ if hasattr(node, "Children") and node.Children is not None and node.Children.Count > 0:
557
+ for child in node.Children:
558
+ lines_count = self._print_tree(child, max_lines, lines_count, indentation + "| ")
559
+ if lines_count >= max_lines and max_lines != -1:
560
+ break
561
+
562
+ return lines_count
563
+
564
+ def print_tree(self, node=None, max_lines=80, lines_count=0, indentation=""):
565
+ """
566
+ Print the hierarchical tree representation of the Mechanical project structure.
567
+
568
+ Parameters
569
+ ----------
570
+ node: DataModel object, optional
571
+ The starting object of the tree.
572
+ max_lines: int, optional
573
+ The maximum number of lines to print. Default is 80. If set to -1, no limit is applied.
574
+
575
+ Raises
576
+ ------
577
+ AttributeError
578
+ If the node does not have the required attributes.
579
+
580
+ Examples
581
+ --------
582
+ >>> from ansys.mechanical.core import App
583
+ >>> app = App()
584
+ >>> app.update_globals(globals())
585
+ >>> app.print_tree()
586
+ ... ├── Project
587
+ ... | ├── Model
588
+ ... | | ├── Geometry Imports (⚡︎)
589
+ ... | | ├── Geometry (?)
590
+ ... | | ├── Materials (✓)
591
+ ... | | ├── Coordinate Systems (✓)
592
+ ... | | | ├── Global Coordinate System (✓)
593
+ ... | | ├── Remote Points (✓)
594
+ ... | | ├── Mesh (?)
595
+
596
+ >>> app.print_tree(Model, 3)
597
+ ... ├── Model
598
+ ... | ├── Geometry Imports (⚡︎)
599
+ ... | ├── Geometry (?)
600
+ ... ... truncating after 3 lines
601
+
602
+ >>> app.print_tree(max_lines=2)
603
+ ... ├── Project
604
+ ... | ├── Model
605
+ ... ... truncating after 2 lines
606
+ """
607
+ if node is None:
608
+ node = self.DataModel.Project
609
+
610
+ self._print_tree(node, max_lines, lines_count, indentation)