ansys-mechanical-core 0.11.11__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.
@@ -40,6 +40,7 @@ except ModuleNotFoundError: # pragma: no cover
40
40
  __version__ = importlib_metadata.version("ansys-mechanical-core")
41
41
 
42
42
  SUPPORTED_MECHANICAL_VERSIONS = {
43
+ 251: "2025R1",
43
44
  242: "2024R2",
44
45
  241: "2024R1",
45
46
  232: "2023R2",
@@ -134,8 +134,7 @@ class App:
134
134
  Create App with Mechanical project file and version:
135
135
 
136
136
  >>> from ansys.mechanical.core import App
137
- >>> app = App(db_file="path/to/file.mechdat", version=241, pri)
138
-
137
+ >>> app = App(db_file="path/to/file.mechdat", version=251)
139
138
 
140
139
  Disable copying the user profile when private appdata is enabled
141
140
 
@@ -182,8 +181,8 @@ class App:
182
181
  profile.update_environment(os.environ)
183
182
  atexit.register(_cleanup_private_appdata, profile)
184
183
 
185
- self._app = _start_application(configuration, self._version, db_file)
186
184
  runtime.initialize(self._version)
185
+ self._app = _start_application(configuration, self._version, db_file)
187
186
  connect_warnings(self)
188
187
  self._poster = None
189
188
 
@@ -76,12 +76,21 @@ class BackgroundApp:
76
76
  """
77
77
  return BackgroundApp.__app
78
78
 
79
- def post(self, callable: typing.Callable):
80
- """Post callable method to the background app thread."""
79
+ def _post(self, callable: typing.Callable, try_post: bool = False):
81
80
  if BackgroundApp.__stopped:
82
81
  raise RuntimeError("Cannot use BackgroundApp after stopping it.")
82
+ if try_post:
83
+ return BackgroundApp.__poster.try_post(callable)
83
84
  return BackgroundApp.__poster.post(callable)
84
85
 
86
+ def post(self, callable: typing.Callable):
87
+ """Post callable method to the background app thread."""
88
+ return self._post(callable)
89
+
90
+ def try_post(self, callable: typing.Callable):
91
+ """Try post callable method to the background app thread."""
92
+ return self._post(callable, try_post=True)
93
+
85
94
  def stop(self) -> None:
86
95
  """Stop the background app thread."""
87
96
  if BackgroundApp.__stopped:
@@ -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
@@ -55,6 +55,7 @@ def global_variables(app: "ansys.mechanical.core.App", enums: bool = False) -> t
55
55
  # When ansys-pythonnet issue #14 is fixed, uncomment above
56
56
  from Ansys.ACT.Core.Math import Point2D, Point3D
57
57
  from Ansys.ACT.Math import Vector3D
58
+ from Ansys.ACT.Mechanical.Fields import VariableDefinitionType
58
59
  from Ansys.Core.Units import Quantity
59
60
  from Ansys.Mechanical.DataModel import MechanicalEnums
60
61
  from Ansys.Mechanical.Graphics import Point, SectionPlane
@@ -74,6 +75,7 @@ def global_variables(app: "ansys.mechanical.core.App", enums: bool = False) -> t
74
75
  vars["Point2D"] = Point2D
75
76
  vars["Point3D"] = Point3D
76
77
  vars["Vector3D"] = Vector3D
78
+ vars["VariableDefinitionType"] = VariableDefinitionType
77
79
 
78
80
  if enums:
79
81
  vars.update(get_all_enums())
@@ -34,7 +34,12 @@ from ansys.mechanical.core.embedding.resolver import resolve
34
34
  INITIALIZED_VERSION = None
35
35
  """Constant for the initialized version."""
36
36
 
37
- SUPPORTED_MECHANICAL_EMBEDDING_VERSIONS = {242: "2024R2", 241: "2024R1", 232: "2023R2"}
37
+ SUPPORTED_MECHANICAL_EMBEDDING_VERSIONS = {
38
+ 251: "2025R1",
39
+ 242: "2024R2",
40
+ 241: "2024R1",
41
+ 232: "2023R2",
42
+ }
38
43
  """Supported Mechanical embedding versions on Windows."""
39
44
 
40
45
 
@@ -37,7 +37,7 @@ Configuring the logger can be done using the :class:`Configuration <ansys.mechan
37
37
  from ansys.mechanical.core.embedding.logger import Configuration, Logger
38
38
 
39
39
  Configuration.configure(level=logging.INFO, to_stdout=True, base_directory=None)
40
- app = mech.App(version=242)
40
+ app = mech.App(version=251)
41
41
 
42
42
  Then, the :class:`Logger <ansys.mechanical.core.embedding.logger.Logger>` class can be used to write messages to the log:
43
43
 
@@ -25,6 +25,19 @@
25
25
  import typing
26
26
 
27
27
 
28
+ class PosterError(Exception):
29
+ """Class which holds errors from the background thread posting system."""
30
+
31
+ def __init__(self, error: Exception):
32
+ """Create an instance to hold the given error."""
33
+ self._error = error
34
+
35
+ @property
36
+ def error(self) -> Exception:
37
+ """Get the underlying exception."""
38
+ return self._error
39
+
40
+
28
41
  class Poster:
29
42
  """Class which can post a python callable function to Mechanical's main thread."""
30
43
 
@@ -37,7 +50,26 @@ class Poster:
37
50
 
38
51
  self._poster = Ans.Common.WB1ManagedUtils.TaskPoster
39
52
 
40
- def post(self, callable: typing.Callable):
53
+ def try_post(self, callable: typing.Callable) -> typing.Any:
54
+ """Post the callable to Mechanical's main thread.
55
+
56
+ This does the same thing as `post` but if `callable`
57
+ raises an exception, try_post will raise the same
58
+ exception to the caller of `try_post`.
59
+ """
60
+
61
+ def wrapped():
62
+ try:
63
+ return callable()
64
+ except Exception as e:
65
+ return PosterError(e)
66
+
67
+ result = self.post(wrapped)
68
+ if isinstance(result, PosterError):
69
+ raise result.error
70
+ return result
71
+
72
+ def post(self, callable: typing.Callable) -> typing.Any:
41
73
  """Post the callable to Mechanical's main thread.
42
74
 
43
75
  The main thread needs to be receiving posted messages
@@ -23,8 +23,8 @@
23
23
  """This is the .NET assembly resolving for embedding Ansys Mechanical.
24
24
 
25
25
  Note that for some Mechanical Addons - additional resolving may be
26
- necessary. A resolve handler is shipped with Ansys Mechanical on windows
27
- starting in version 23.1 and on linux starting in version 23.2
26
+ necessary. A resolve handler is shipped with Ansys Mechanical on Windows
27
+ starting in version 23.1 and on Linux starting in version 23.2
28
28
  """
29
29
 
30
30
 
@@ -36,6 +36,13 @@ def resolve(version):
36
36
  clr.AddReference("Ansys.Mechanical.Embedding")
37
37
  import Ansys # isort: skip
38
38
 
39
- assembly_resolver = Ansys.Mechanical.Embedding.AssemblyResolver
40
- resolve_handler = assembly_resolver.MechanicalResolveEventHandler
41
- System.AppDomain.CurrentDomain.AssemblyResolve += resolve_handler
39
+ try:
40
+ assembly_resolver = Ansys.Mechanical.Embedding.AssemblyResolver
41
+ resolve_handler = assembly_resolver.MechanicalResolveEventHandler
42
+ System.AppDomain.CurrentDomain.AssemblyResolve += resolve_handler
43
+ except AttributeError:
44
+ error_msg = f"""Unable to resolve Mechanical assemblies. Please ensure the following:
45
+ 1. Mechanical is installed.
46
+ 2. A folder with the name "Ansys" does not exist in the same directory as the script being run.
47
+ """
48
+ raise AttributeError(error_msg)
@@ -0,0 +1,36 @@
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
+ """RPC and Mechanical service implementation."""
24
+ from .client import Client
25
+
26
+ # todo - provide an implementation of Server (RemoteMechancial) that installs the below
27
+ # from .default_server import RemoteMechanical
28
+ # and remove them from this import statement
29
+ # todo - combine Server and MechanicalService
30
+ from .server import (
31
+ DefaultServiceMethods,
32
+ MechanicalDefaultServer,
33
+ MechanicalEmbeddedServer,
34
+ MechanicalService,
35
+ )
36
+ from .utils import get_remote_methods, remote_method
@@ -0,0 +1,237 @@
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
+ """Client for Mechanical services."""
23
+
24
+ import os
25
+ import pathlib
26
+ import time
27
+
28
+ import rpyc
29
+
30
+ from ansys.mechanical.core.mechanical import DEFAULT_CHUNK_SIZE
31
+
32
+
33
+ class Client:
34
+ """Client for connecting to Mechanical services."""
35
+
36
+ def __init__(self, host: str, port: int, timeout: float = 120.0):
37
+ """Initialize the client.
38
+
39
+ Parameters
40
+ ----------
41
+ host : str, optional
42
+ IP address to connect to the server. The default is ``None``
43
+ in which case ``localhost`` is used.
44
+ port : int, optional
45
+ Port to connect to the Mecahnical server. The default is ``None``,
46
+ in which case ``10000`` is used.
47
+ timeout : float, optional
48
+ Maximum allowable time for connecting to the Mechanical server.
49
+ The default is ``60.0``.
50
+
51
+ """
52
+ self.host = host
53
+ self.port = port
54
+ self.timeout = timeout
55
+ self.connection = None
56
+ self.root = None
57
+ self._connect()
58
+
59
+ def __getattr__(self, attr):
60
+ """Get attribute from the root object."""
61
+ if hasattr(self.root, attr):
62
+ return getattr(self.root, attr)
63
+ propget_name = f"propget_{attr}"
64
+ if hasattr(self.root, propget_name):
65
+ exposed_fget = getattr(self.root, propget_name)
66
+ return exposed_fget()
67
+ return self.__dict__.items[attr]
68
+
69
+ # TODO - implement setattr
70
+ # def __setattr__(self, attr, value):
71
+ # if hasattr(self.root, attr):
72
+ # inner_prop = getattr(self.root.__class__, attr)
73
+ # if isinstance(inner_prop, property):
74
+ # inner_prop.fset(self.root, value)
75
+ # else:
76
+ # super().__setattr__(attr, value)
77
+
78
+ def _connect(self):
79
+ self._wait_until_ready()
80
+ self.connection = rpyc.connect(self.host, self.port)
81
+ self.root = self.connection.root
82
+ print(f"Connected to {self.host}:{self.port}")
83
+ print(f"Installed methods")
84
+
85
+ def _wait_until_ready(self):
86
+ t_max = time.time() + self.timeout
87
+ while time.time() < t_max:
88
+ try:
89
+ conn = rpyc.connect(self.host, self.port)
90
+ conn.ping() # Simple ping to check if the connection is healthy
91
+ conn.close()
92
+ print("Server is ready to connect")
93
+ break
94
+ except:
95
+ time.sleep(2)
96
+ else:
97
+ raise TimeoutError(
98
+ f"Server at {self.host}:{self.port} not ready within {self.timeout} seconds."
99
+ )
100
+
101
+ def close(self):
102
+ """Close the connection."""
103
+ self.connection.close()
104
+ print(f"Connection to {self.host}:{self.port} closed")
105
+
106
+ def upload(
107
+ self,
108
+ file_name,
109
+ file_location_destination=None,
110
+ chunk_size=DEFAULT_CHUNK_SIZE,
111
+ progress_bar=False,
112
+ ):
113
+ """Upload a file to the server."""
114
+ print(f"arg: {file_name}, {file_location_destination}")
115
+ print()
116
+ if not os.path.exists(file_name):
117
+ print(f"File {file_name} does not exist.")
118
+ return
119
+ file_base_name = os.path.basename(file_name)
120
+ remote_path = os.path.join(file_location_destination, file_base_name)
121
+
122
+ with open(file_name, "rb") as f:
123
+ file_data = f.read()
124
+ self.service_upload(remote_path, file_data)
125
+
126
+ print(f"File {file_name} uploaded to {file_location_destination}")
127
+
128
+ def download(
129
+ self,
130
+ files,
131
+ target_dir=None,
132
+ chunk_size=DEFAULT_CHUNK_SIZE,
133
+ progress_bar=None,
134
+ recursive=False,
135
+ ):
136
+ """Download a file from the server."""
137
+ out_files = []
138
+ os.makedirs(target_dir, exist_ok=True)
139
+
140
+ response = self.service_download(files)
141
+
142
+ if isinstance(response, dict) and response["is_directory"]:
143
+ for relative_file_path in response["files"]:
144
+ full_remote_path = os.path.join(files, relative_file_path)
145
+ local_file_path = os.path.join(target_dir, relative_file_path)
146
+ local_file_dir = os.path.dirname(local_file_path)
147
+ os.makedirs(local_file_dir, exist_ok=True)
148
+
149
+ out_file_path = self._download_file(
150
+ full_remote_path, local_file_path, chunk_size, overwrite=True
151
+ )
152
+ else:
153
+ out_file_path = self._download_file(
154
+ files, os.path.join(target_dir, os.path.basename(files)), chunk_size, overwrite=True
155
+ )
156
+ out_files.append(out_file_path)
157
+
158
+ return out_files
159
+
160
+ def _download_file(self, remote_file_path, local_file_path, chunk_size=1024, overwrite=False):
161
+ if os.path.exists(local_file_path) and not overwrite:
162
+ print(f"File {local_file_path} already exists locally. Skipping download.")
163
+ return
164
+ response = self.service_download(remote_file_path)
165
+ if isinstance(response, dict):
166
+ raise ValueError("Expected a file download, but got a directory response.")
167
+ file_data = response
168
+
169
+ # Write the file data to the local path
170
+ with open(local_file_path, "wb") as f:
171
+ f.write(file_data)
172
+
173
+ print(f"File {remote_file_path} downloaded to {local_file_path}")
174
+
175
+ return local_file_path
176
+
177
+ def download_project(self, extensions=None, target_dir=None, progress_bar=False):
178
+ """Download all project files in the working directory of the Mechanical instance."""
179
+ destination_directory = target_dir.rstrip("\\/")
180
+ if destination_directory:
181
+ path = pathlib.Path(destination_directory)
182
+ path.mkdir(parents=True, exist_ok=True)
183
+ else:
184
+ destination_directory = os.getcwd()
185
+ # relative directory?
186
+ if os.path.isdir(destination_directory):
187
+ if not os.path.isabs(destination_directory):
188
+ # construct full path
189
+ destination_directory = os.path.join(os.getcwd(), destination_directory)
190
+ _project_directory = self.project_directory
191
+ _project_directory = _project_directory.rstrip("\\/")
192
+
193
+ # this is where .mechddb resides
194
+ parent_directory = os.path.dirname(_project_directory)
195
+
196
+ list_of_files = []
197
+
198
+ if not extensions:
199
+ files = self.list_files()
200
+ else:
201
+ files = []
202
+ for each_extension in extensions:
203
+ # mechdb resides one level above project directory
204
+ if "mechdb" == each_extension.lower():
205
+ file_temp = os.path.join(parent_directory, f"*.{each_extension}")
206
+ else:
207
+ file_temp = os.path.join(_project_directory, "**", f"*.{each_extension}")
208
+
209
+ list_files = self._get_files(file_temp, recursive=False)
210
+
211
+ files.extend(list_files)
212
+
213
+ for file in files:
214
+ # create similar hierarchy locally
215
+ new_path = file.replace(parent_directory, destination_directory)
216
+ new_path_dir = os.path.dirname(new_path)
217
+ temp_files = self.download(
218
+ files=file, target_dir=new_path_dir, progress_bar=progress_bar
219
+ )
220
+ list_of_files.extend(temp_files)
221
+ return list_of_files
222
+
223
+ @property
224
+ def is_alive(self):
225
+ """Check if the Mechanical instance is alive."""
226
+ try:
227
+ self.connection.ping()
228
+ return True
229
+ except:
230
+ return False
231
+
232
+ def exit(self):
233
+ """Shuts down the Mechanical instance."""
234
+ print("Requesting server shutdown ...")
235
+ self.root.service_exit()
236
+ self.connection.close()
237
+ print("Disconnected from server")