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.
- ansys/mechanical/core/_version.py +48 -48
- ansys/mechanical/core/embedding/app.py +610 -610
- ansys/mechanical/core/embedding/background.py +11 -2
- ansys/mechanical/core/embedding/logger/__init__.py +219 -219
- ansys/mechanical/core/embedding/resolver.py +48 -41
- ansys/mechanical/core/embedding/rpc/__init__.py +36 -0
- ansys/mechanical/core/embedding/rpc/client.py +237 -0
- ansys/mechanical/core/embedding/rpc/server.py +382 -0
- ansys/mechanical/core/embedding/rpc/utils.py +120 -0
- ansys/mechanical/core/embedding/runtime.py +22 -0
- ansys/mechanical/core/feature_flags.py +1 -0
- ansys/mechanical/core/ide_config.py +212 -212
- ansys/mechanical/core/mechanical.py +2343 -2324
- ansys/mechanical/core/misc.py +176 -176
- ansys/mechanical/core/pool.py +712 -712
- ansys/mechanical/core/run.py +321 -321
- {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.13.dist-info}/METADATA +35 -23
- {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.13.dist-info}/RECORD +21 -17
- {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.13.dist-info}/LICENSE +0 -0
- {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.13.dist-info}/WHEEL +0 -0
- {ansys_mechanical_core-0.11.12.dist-info → ansys_mechanical_core-0.11.13.dist-info}/entry_points.txt +0 -0
@@ -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
|
-
|
185
|
-
|
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)
|