ansys-mechanical-core 0.10.10__py3-none-any.whl → 0.11.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. ansys/mechanical/core/__init__.py +11 -4
  2. ansys/mechanical/core/_version.py +48 -47
  3. ansys/mechanical/core/embedding/__init__.py +1 -1
  4. ansys/mechanical/core/embedding/addins.py +1 -7
  5. ansys/mechanical/core/embedding/app.py +610 -281
  6. ansys/mechanical/core/embedding/app_libraries.py +24 -5
  7. ansys/mechanical/core/embedding/appdata.py +16 -4
  8. ansys/mechanical/core/embedding/background.py +106 -0
  9. ansys/mechanical/core/embedding/cleanup_gui.py +61 -0
  10. ansys/mechanical/core/embedding/enum_importer.py +2 -2
  11. ansys/mechanical/core/embedding/imports.py +27 -7
  12. ansys/mechanical/core/embedding/initializer.py +105 -53
  13. ansys/mechanical/core/embedding/loader.py +19 -9
  14. ansys/mechanical/core/embedding/logger/__init__.py +219 -216
  15. ansys/mechanical/core/embedding/logger/environ.py +1 -1
  16. ansys/mechanical/core/embedding/logger/linux_api.py +1 -1
  17. ansys/mechanical/core/embedding/logger/sinks.py +1 -1
  18. ansys/mechanical/core/embedding/logger/windows_api.py +2 -2
  19. ansys/mechanical/core/embedding/poster.py +38 -4
  20. ansys/mechanical/core/embedding/resolver.py +41 -44
  21. ansys/mechanical/core/embedding/runtime.py +1 -1
  22. ansys/mechanical/core/embedding/shims.py +9 -8
  23. ansys/mechanical/core/embedding/ui.py +228 -0
  24. ansys/mechanical/core/embedding/utils.py +1 -1
  25. ansys/mechanical/core/embedding/viz/__init__.py +1 -1
  26. ansys/mechanical/core/embedding/viz/{pyvista_plotter.py → embedding_plotter.py} +24 -8
  27. ansys/mechanical/core/embedding/viz/usd_converter.py +59 -25
  28. ansys/mechanical/core/embedding/viz/utils.py +32 -2
  29. ansys/mechanical/core/embedding/warnings.py +1 -1
  30. ansys/mechanical/core/errors.py +2 -1
  31. ansys/mechanical/core/examples/__init__.py +1 -1
  32. ansys/mechanical/core/examples/downloads.py +10 -5
  33. ansys/mechanical/core/feature_flags.py +51 -0
  34. ansys/mechanical/core/ide_config.py +212 -0
  35. ansys/mechanical/core/launcher.py +9 -9
  36. ansys/mechanical/core/logging.py +14 -2
  37. ansys/mechanical/core/mechanical.py +2324 -2237
  38. ansys/mechanical/core/misc.py +176 -176
  39. ansys/mechanical/core/pool.py +712 -712
  40. ansys/mechanical/core/run.py +321 -246
  41. {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/LICENSE +7 -7
  42. {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/METADATA +57 -56
  43. ansys_mechanical_core-0.11.12.dist-info/RECORD +45 -0
  44. {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/WHEEL +1 -1
  45. {ansys_mechanical_core-0.10.10.dist-info → ansys_mechanical_core-0.11.12.dist-info}/entry_points.txt +1 -0
  46. ansys_mechanical_core-0.10.10.dist-info/RECORD +0 -40
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -50,12 +50,31 @@ can be imported with the `import` statement.
50
50
 
51
51
  import os
52
52
  import sys
53
+ import warnings
53
54
 
54
- from ansys.tools.path.path import _get_unified_install_base_for_version
55
+ from ansys.tools.path import get_mechanical_path
55
56
 
57
+ from ansys.mechanical.core.embedding.app import App
56
58
 
57
- def add_mechanical_python_libraries(version: int):
59
+
60
+ def add_mechanical_python_libraries(app_or_version):
58
61
  """Add the Mechanical libraries path to sys.path."""
59
- install, _ = _get_unified_install_base_for_version(version)
60
- location = os.path.join(install, "Addins", "ACT", "libraries", "Mechanical")
62
+ installdir = []
63
+ if isinstance(app_or_version, int):
64
+ warnings.warn(
65
+ "Passing version to add_mechanical_python_libraries() is deprecated."
66
+ "Please pass an instance of App() instead.",
67
+ DeprecationWarning,
68
+ stacklevel=2,
69
+ )
70
+ exe = get_mechanical_path(allow_input=False, version=app_or_version)
71
+ while os.path.basename(exe) != f"v{app_or_version}":
72
+ exe = os.path.dirname(exe)
73
+ installdir.append(exe)
74
+ elif isinstance(app_or_version, App):
75
+ installdir.append(os.environ[f"AWP_ROOT{app_or_version.version}"])
76
+ else:
77
+ raise ValueError("Invalid input: expected an integer (version) or an instance of App().")
78
+
79
+ location = os.path.join(installdir[0], "Addins", "ACT", "libraries", "Mechanical")
61
80
  sys.path.append(location)
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -31,21 +31,33 @@ import warnings
31
31
  class UniqueUserProfile:
32
32
  """Create Unique User Profile (for AppData)."""
33
33
 
34
- def __init__(self, profile_name):
34
+ def __init__(self, profile_name: str, copy_profile: bool = True, dry_run: bool = False):
35
35
  """Initialize UniqueUserProfile class."""
36
36
  self._default_profile = os.path.expanduser("~")
37
37
  self._location = os.path.join(self._default_profile, "PyMechanical-AppData", profile_name)
38
+ self._dry_run = dry_run
39
+ self.copy_profile = copy_profile
38
40
  self.initialize()
39
41
 
40
42
  def initialize(self) -> None:
41
- """Initialize the new profile location."""
43
+ """
44
+ Initialize the new profile location.
45
+
46
+ Args:
47
+ copy_profile (bool): If False, the copy_profile method will be skipped.
48
+ """
49
+ if self._dry_run:
50
+ return
42
51
  if self.exists():
43
52
  self.cleanup()
44
53
  self.mkdirs()
45
- self.copy_profiles()
54
+ if self.copy_profile:
55
+ self.copy_profiles()
46
56
 
47
57
  def cleanup(self) -> None:
48
58
  """Cleanup unique user profile."""
59
+ if self._dry_run:
60
+ return
49
61
  text = "The `private_appdata` option was used, but the following files were not removed: "
50
62
  message = []
51
63
 
@@ -0,0 +1,106 @@
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
+ """Class for running Mechanical on a background thread."""
24
+
25
+ import atexit
26
+ import threading
27
+ import time
28
+ import typing
29
+
30
+ import ansys.mechanical.core as mech
31
+ from ansys.mechanical.core.embedding import initializer
32
+ from ansys.mechanical.core.embedding.poster import Poster
33
+ import ansys.mechanical.core.embedding.utils as utils
34
+
35
+
36
+ def _exit(background_app: "BackgroundApp"):
37
+ """Stop the thread serving the Background App."""
38
+ background_app.stop()
39
+
40
+
41
+ class BackgroundApp:
42
+ """Background App."""
43
+
44
+ __app: mech.App = None
45
+ __app_thread: threading.Thread = None
46
+ __stopped: bool = False
47
+ __stop_signaled: bool = False
48
+ __poster: Poster = None
49
+
50
+ def __init__(self, **kwargs):
51
+ """Construct an instance of BackgroundApp."""
52
+ if BackgroundApp.__app_thread is None:
53
+ initializer.initialize(kwargs.get("version"))
54
+ BackgroundApp.__app_thread = threading.Thread(
55
+ target=self._start_app, kwargs=kwargs, daemon=True
56
+ )
57
+ BackgroundApp.__app_thread.start()
58
+
59
+ while BackgroundApp.__poster is None:
60
+ time.sleep(0.05)
61
+ continue
62
+ else:
63
+ if BackgroundApp.__stopped:
64
+ raise RuntimeError("Cannot initialize a BackgroundApp once it has been stopped!")
65
+
66
+ def new():
67
+ BackgroundApp.__app.new()
68
+
69
+ self.post(new)
70
+
71
+ @property
72
+ def app(self) -> mech.App:
73
+ """Get the App instance of the background thread.
74
+
75
+ It is not meant to be used aside from passing to methods using `post`.
76
+ """
77
+ return BackgroundApp.__app
78
+
79
+ def post(self, callable: typing.Callable):
80
+ """Post callable method to the background app thread."""
81
+ if BackgroundApp.__stopped:
82
+ raise RuntimeError("Cannot use BackgroundApp after stopping it.")
83
+ return BackgroundApp.__poster.post(callable)
84
+
85
+ def stop(self) -> None:
86
+ """Stop the background app thread."""
87
+ if BackgroundApp.__stopped:
88
+ return
89
+ BackgroundApp.__stop_signaled = True
90
+ while True:
91
+ time.sleep(0.05)
92
+ if BackgroundApp.__stopped:
93
+ break
94
+
95
+ def _start_app(self, **kwargs) -> None:
96
+ BackgroundApp.__app = mech.App(**kwargs)
97
+ BackgroundApp.__poster = BackgroundApp.__app.poster
98
+ atexit.register(_exit, self)
99
+ while True:
100
+ if BackgroundApp.__stop_signaled:
101
+ break
102
+ try:
103
+ utils.sleep(40)
104
+ except:
105
+ raise Exception("BackgroundApp cannot sleep.") # pragma: no cover
106
+ BackgroundApp.__stopped = True
@@ -0,0 +1,61 @@
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
+ """Clean up temporary mechdb files after GUI is closed."""
23
+
24
+ from pathlib import Path
25
+ import shutil
26
+ import sys
27
+ import time
28
+
29
+ import psutil
30
+
31
+
32
+ def cleanup_gui(pid, temp_mechdb) -> None:
33
+ """Remove the temporary mechdb file after it is closed.
34
+
35
+ Parameters
36
+ ----------
37
+ pid: int
38
+ The process ID of the open temporary mechdb file.
39
+ temp_mechdb: Path
40
+ The path of the temporary mechdb file.
41
+ """
42
+ # While the pid exists, sleep
43
+ while psutil.pid_exists(pid):
44
+ time.sleep(1)
45
+
46
+ # Delete the temporary mechdb file once the GUI is closed
47
+ temp_mechdb.unlink()
48
+
49
+ # Delete the temporary mechdb Mech_Files folder
50
+ temp_folder_path = temp_mechdb.parent / f"{temp_mechdb.name.split('.')[0]}_Mech_Files"
51
+ shutil.rmtree(temp_folder_path)
52
+
53
+
54
+ if __name__ == "__main__": # pragma: no cover
55
+ """Get the process ID and temporary file path to monitor and delete files after use."""
56
+ # Convert the process id (pid) argument into an integer
57
+ pid = int(sys.argv[1])
58
+ # Convert the temporary mechdb path into a Path
59
+ temp_mechdb_path = Path(sys.argv[2])
60
+ # Remove the temporary mechdb file when the GUI is closed
61
+ cleanup_gui(pid, temp_mechdb_path)
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -33,5 +33,5 @@ clr.AddReference("Ansys.ACT.Interfaces")
33
33
 
34
34
  from Ansys.ACT.Interfaces.Common import * # noqa isort: skip
35
35
  from Ansys.Mechanical.DataModel.Enums import * # noqa isort: skip
36
-
36
+ from Ansys.ACT.Interfaces.Analysis import * # noqa isort: skip
37
37
  import Ansys # noqa isort: skip
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -24,20 +24,28 @@
24
24
  import typing
25
25
 
26
26
 
27
+ def global_entry_points(app: "ansys.mechanical.core.App") -> typing.Dict:
28
+ """Return the global entry points of the application."""
29
+ vars = {}
30
+ vars["ExtAPI"] = app.ExtAPI
31
+ vars["DataModel"] = app.DataModel
32
+ vars["Model"] = app.DataModel.Project.Model
33
+ vars["Tree"] = app.DataModel.Tree
34
+ vars["Graphics"] = app.ExtAPI.Graphics
35
+ return vars
36
+
37
+
27
38
  def global_variables(app: "ansys.mechanical.core.App", enums: bool = False) -> typing.Dict:
28
39
  """Return the Mechanical scripting global variables as a dict.
29
40
 
30
41
  It can be used to add all of these as global variables in python
31
42
  with this command:
32
- `globals().update(global_variables(embedded_app))`
43
+
44
+ ``globals().update(global_variables(embedded_app))``
33
45
 
34
46
  To also import all the enums, set the parameter enums to true.
35
47
  """
36
- vars = {}
37
- vars["ExtAPI"] = app.ExtAPI
38
- vars["DataModel"] = app.DataModel
39
- vars["Model"] = app.DataModel.Project.Model
40
- vars["Tree"] = app.DataModel.Tree
48
+ vars = global_entry_points(app)
41
49
  import clr # isort: skip
42
50
 
43
51
  clr.AddReference("System.Collections")
@@ -45,8 +53,12 @@ def global_variables(app: "ansys.mechanical.core.App", enums: bool = False) -> t
45
53
  clr.AddReference("Ansys.Mechanical.DataModel")
46
54
  # from Ansys.ACT.Mechanical import Transaction
47
55
  # When ansys-pythonnet issue #14 is fixed, uncomment above
56
+ from Ansys.ACT.Core.Math import Point2D, Point3D
57
+ from Ansys.ACT.Math import Vector3D
58
+ from Ansys.ACT.Mechanical.Fields import VariableDefinitionType
48
59
  from Ansys.Core.Units import Quantity
49
60
  from Ansys.Mechanical.DataModel import MechanicalEnums
61
+ from Ansys.Mechanical.Graphics import Point, SectionPlane
50
62
 
51
63
  import System # isort: skip
52
64
  import Ansys # isort: skip
@@ -56,6 +68,14 @@ def global_variables(app: "ansys.mechanical.core.App", enums: bool = False) -> t
56
68
  vars["Ansys"] = Ansys
57
69
  vars["Transaction"] = Transaction
58
70
  vars["MechanicalEnums"] = MechanicalEnums
71
+ # Graphics
72
+ vars["Point"] = Point
73
+ vars["SectionPlane"] = SectionPlane
74
+ # Math
75
+ vars["Point2D"] = Point2D
76
+ vars["Point3D"] = Point3D
77
+ vars["Vector3D"] = Vector3D
78
+ vars["VariableDefinitionType"] = VariableDefinitionType
59
79
 
60
80
  if enums:
61
81
  vars.update(get_all_enums())
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -28,13 +28,19 @@ import platform
28
28
  import sys
29
29
  import warnings
30
30
 
31
- import ansys.tools.path as atp
32
-
33
31
  from ansys.mechanical.core.embedding.loader import load_clr
34
32
  from ansys.mechanical.core.embedding.resolver import resolve
35
33
 
36
34
  INITIALIZED_VERSION = None
37
- SUPPORTED_MECHANICAL_EMBEDDING_VERSIONS_WINDOWS = {241: "2024R1", 232: "2023R2", 231: "2023R1"}
35
+ """Constant for the initialized version."""
36
+
37
+ SUPPORTED_MECHANICAL_EMBEDDING_VERSIONS = {
38
+ 251: "2025R1",
39
+ 242: "2024R2",
40
+ 241: "2024R1",
41
+ 232: "2023R2",
42
+ }
43
+ """Supported Mechanical embedding versions on Windows."""
38
44
 
39
45
 
40
46
  def __add_sys_path(version: int) -> str:
@@ -44,15 +50,6 @@ def __add_sys_path(version: int) -> str:
44
50
  sys.path.append(str(bin_path.resolve()))
45
51
 
46
52
 
47
- def __disable_sec() -> None:
48
- """SEC is part of RSM and is unstable with embedding.
49
-
50
- I'm not going to debug why that is since we are planning to support
51
- DCS/REP in the future instead of RSM.
52
- """
53
- os.environ["ANSYS_MECHANICAL_EMBEDDING_NO_SEC"] = "1"
54
-
55
-
56
53
  def __workaround_material_server(version: int) -> None:
57
54
  """Workaround material server bug in 2024 R1.
58
55
 
@@ -61,53 +58,72 @@ def __workaround_material_server(version: int) -> None:
61
58
  starts by default on a background thread, which may lead to
62
59
  a race condition on shutdown.
63
60
  """
64
- # TODO - remove 242 when that is fixed
65
- if version in [241, 242]:
61
+ if version in [241]:
66
62
  os.environ["ENGRDATA_SERVER_SERIAL"] = "1"
67
63
 
68
64
 
69
- def _get_default_linux_version() -> int:
70
- """Try to get the active linux version from the environment.
65
+ def __check_for_supported_version(version) -> None:
66
+ """Check if Mechanical version is supported with current version of PyMechanical.
71
67
 
72
- On linux, embedding is only possible by setting environment variables before starting python.
73
- The version will then be fixed to a specific version, based on those env vars.
74
- The documented way to set those variables is to run python using the ``mechanical-env`` script,
75
- which can be used after installing the ``ansys-mechanical-env`` package with this command:
76
- ``pip install ansys-mechanical-env``. The script takes user input of a version. If the user
77
- does not provide a version, the ``find_mechanical()`` function from the ``ansys-tools-path``
78
- package is used to find a version of Mechanical.
68
+ If specific environment variable is enabled, then users can overwrite the supported versions.
69
+ However, using unsupported versions may cause issues.
79
70
  """
80
- supported_versions = [232, 241]
81
- awp_roots = {ver: os.environ.get(f"AWP_ROOT{ver}", "") for ver in supported_versions}
82
- installed_versions = {
83
- ver: path for ver, path in awp_roots.items() if path and os.path.isdir(path)
84
- }
85
- assert len(installed_versions) == 1, "multiple AWP_ROOT environment variables found!"
86
- return next(iter(installed_versions))
71
+ allow_old_version = os.getenv("ANSYS_MECHANICAL_EMBEDDING_SUPPORT_OLD_VERSIONS") == "1"
72
+
73
+ # Check if the version is supported
74
+ if not allow_old_version and version < min(SUPPORTED_MECHANICAL_EMBEDDING_VERSIONS):
75
+ raise ValueError(f"Mechanical version {version} is not supported.")
76
+
77
+ return version
78
+
87
79
 
80
+ def _get_latest_default_version() -> int:
81
+ """Try to get the latest Mechanical version from the environment.
88
82
 
89
- def _get_default_version() -> int:
90
- if os.name == "posix":
91
- return _get_default_linux_version()
83
+ Checks if multiple versions of Mechanical found in system.
84
+ For Linux it will be only one since ``mechanical-env`` takes care of that.
85
+ If multiple versions are detected, select the latest one, as no specific version is provided.
86
+ """
87
+ awp_roots = [value for key, value in os.environ.items() if key.startswith("AWP_ROOT")]
92
88
 
93
- if os.name != "nt": # pragma: no cover
94
- raise Exception("Unexpected platform!")
89
+ if not awp_roots:
90
+ raise Exception("No Mechanical installations found.")
95
91
 
96
- _, version = atp.find_mechanical(
97
- supported_versions=SUPPORTED_MECHANICAL_EMBEDDING_VERSIONS_WINDOWS
98
- )
92
+ versions_found = []
93
+ for path in awp_roots:
94
+ folder = os.path.basename(os.path.normpath(path))
95
+ version = folder.split("v")[-1]
96
+ versions_found.append(int(version))
97
+ latest_version = max(versions_found)
99
98
 
100
- # version is of the form 23.2
101
- int_version = int(str(version).replace(".", ""))
102
- return int_version
99
+ if len(awp_roots) > 1:
100
+ warnings.warn(
101
+ f"Multiple versions of Mechanical found! Using latest version {latest_version} ..."
102
+ )
103
103
 
104
+ return latest_version
104
105
 
105
- def __check_python_interpreter_architecture():
106
+
107
+ def __check_python_interpreter_architecture() -> None:
106
108
  """Embedding support only 64 bit architecture."""
107
109
  if platform.architecture()[0] != "64bit":
108
110
  raise Exception("Mechanical Embedding requires a 64-bit Python environment.")
109
111
 
110
112
 
113
+ def __set_environment(version: int) -> None:
114
+ """Set environment variables to configure embedding."""
115
+ if os.name == "nt": # pragma: no cover
116
+ if version < 251:
117
+ os.environ["MECHANICAL_STARTUP_UNOPTIMIZED"] = "1"
118
+
119
+ # Set an environment variable to use the custom CLR host
120
+ # for embedding.
121
+ # In the future (>251), it would always be used.
122
+ if version == 251:
123
+ if "PYMECHANICAL_NO_CLR_HOST_LITE" not in os.environ:
124
+ os.environ["ANSYS_MECHANICAL_EMBEDDING_CLR_HOST"] = "1"
125
+
126
+
111
127
  def __check_for_mechanical_env():
112
128
  """Embedding in linux platform must use mechanical-env."""
113
129
  if platform.system() == "Linux" and os.environ.get("PYMECHANICAL_EMBEDDING") != "TRUE":
@@ -120,22 +136,57 @@ def __check_for_mechanical_env():
120
136
  )
121
137
 
122
138
 
139
+ def __is_lib_loaded(libname: str): # pragma: no cover
140
+ """Return whether a library is loaded."""
141
+ import ctypes
142
+
143
+ RTLD_NOLOAD = 4
144
+ try:
145
+ ctypes.CDLL(libname, RTLD_NOLOAD)
146
+ except OSError:
147
+ return False
148
+ return True
149
+
150
+
151
+ def __check_loaded_libs(version: int = None): # pragma: no cover
152
+ """Ensure that incompatible libraries aren't loaded prior to PyMechanical load."""
153
+ if platform.system() != "Linux":
154
+ return
155
+
156
+ if version < 251:
157
+ return
158
+
159
+ # For 2025 R1, PyMechanical will crash on shutdown if libX11.so is already loaded
160
+ # before starting Mechanical
161
+ if __is_lib_loaded("libX11.so"):
162
+ warnings.warn(
163
+ "libX11.so is loaded prior to initializing the Embedded Instance of Mechanical.\
164
+ Python will crash on shutdown..."
165
+ )
166
+
167
+
123
168
  def initialize(version: int = None):
124
169
  """Initialize Mechanical embedding."""
125
- __check_python_interpreter_architecture() # blocks 32 bit python
126
- __check_for_mechanical_env() # checks for mechanical-env in linux embedding
127
-
128
170
  global INITIALIZED_VERSION
129
- if INITIALIZED_VERSION != None:
130
- assert INITIALIZED_VERSION == version
131
- return
171
+ if version is None:
172
+ version = _get_latest_default_version()
132
173
 
133
- if version == None:
134
- version = _get_default_version()
174
+ version = __check_for_supported_version(version=version)
135
175
 
136
- INITIALIZED_VERSION = version
176
+ if INITIALIZED_VERSION is not None:
177
+ if INITIALIZED_VERSION != version:
178
+ raise ValueError(
179
+ f"Initialized version {INITIALIZED_VERSION} "
180
+ f"does not match the expected version {version}."
181
+ )
182
+ return INITIALIZED_VERSION
137
183
 
138
- __disable_sec()
184
+ __check_python_interpreter_architecture() # blocks 32 bit python
185
+ __check_for_mechanical_env() # checks for mechanical-env in linux embedding
186
+
187
+ __set_environment(version)
188
+
189
+ __check_loaded_libs(version)
139
190
 
140
191
  __workaround_material_server(version)
141
192
 
@@ -164,4 +215,5 @@ def initialize(version: int = None):
164
215
  # attach the resolver
165
216
  resolve(version)
166
217
 
218
+ INITIALIZED_VERSION = version
167
219
  return version
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -22,25 +22,35 @@
22
22
 
23
23
  """clr_loader for pymechanical embedding. This loads the CLR on both windows and linux."""
24
24
  import os
25
+ import warnings
25
26
 
26
27
 
27
28
  def __get_mono(assembly_dir, config_dir):
28
29
  import clr_loader
29
30
 
30
31
  libmono = os.path.join(assembly_dir, "libmonosgen-2.0.so")
31
- mono = clr_loader.get_mono(
32
- set_signal_chaining=True,
33
- libmono=libmono, # TODO: find_mono is broken on clr-loader v0.2.6
34
- assembly_dir=assembly_dir,
35
- config_dir=config_dir,
36
- )
32
+ with warnings.catch_warnings():
33
+ warnings.simplefilter("ignore")
34
+ mono = clr_loader.get_mono(
35
+ set_signal_chaining=True,
36
+ libmono=libmono, # TODO: find_mono is broken on clr-loader v0.2.6
37
+ assembly_dir=assembly_dir,
38
+ config_dir=config_dir,
39
+ )
37
40
  return mono
38
41
 
39
42
 
40
- def load_clr_mono(install_loc):
43
+ def _set_mono_trace(): # pragma: no cover
44
+ if "ANS_MONO_TRACE" in os.environ:
45
+ os.environ["MONO_LOG_LEVEL"] = "message"
46
+ os.environ["MONO_LOG_MASK"] = "all"
47
+
48
+
49
+ def _load_mono(install_loc):
41
50
  """Load the clr using mono that is shipped with the unified install."""
42
51
  from pythonnet import load
43
52
 
53
+ _set_mono_trace()
44
54
  mono_dir = os.path.join(install_loc, "Tools", "mono", "Linux64")
45
55
  assembly_dir = os.path.join(mono_dir, "lib")
46
56
  config_dir = os.path.join(mono_dir, "etc")
@@ -52,4 +62,4 @@ def load_clr(install_loc: str) -> None:
52
62
  """Load the clr, the outcome of this function is that `clr` is usable."""
53
63
  if os.name == "nt": # pragma: no cover
54
64
  return
55
- load_clr_mono(install_loc)
65
+ _load_mono(install_loc)