ansys-pyensight-core 0.11.0__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/pyensight/core/__init__.py +41 -0
- ansys/pyensight/core/common.py +341 -0
- ansys/pyensight/core/deep_pixel_view.html +98 -0
- ansys/pyensight/core/dockerlauncher.py +1124 -0
- ansys/pyensight/core/dvs.py +872 -0
- ansys/pyensight/core/enscontext.py +345 -0
- ansys/pyensight/core/enshell_grpc.py +641 -0
- ansys/pyensight/core/ensight_grpc.py +874 -0
- ansys/pyensight/core/ensobj.py +515 -0
- ansys/pyensight/core/launch_ensight.py +296 -0
- ansys/pyensight/core/launcher.py +388 -0
- ansys/pyensight/core/libuserd.py +2110 -0
- ansys/pyensight/core/listobj.py +280 -0
- ansys/pyensight/core/locallauncher.py +579 -0
- ansys/pyensight/core/py.typed +0 -0
- ansys/pyensight/core/renderable.py +880 -0
- ansys/pyensight/core/session.py +1923 -0
- ansys/pyensight/core/sgeo_poll.html +24 -0
- ansys/pyensight/core/utils/__init__.py +21 -0
- ansys/pyensight/core/utils/adr.py +111 -0
- ansys/pyensight/core/utils/dsg_server.py +1220 -0
- ansys/pyensight/core/utils/export.py +606 -0
- ansys/pyensight/core/utils/omniverse.py +769 -0
- ansys/pyensight/core/utils/omniverse_cli.py +614 -0
- ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
- ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
- ansys/pyensight/core/utils/parts.py +1221 -0
- ansys/pyensight/core/utils/query.py +487 -0
- ansys/pyensight/core/utils/readers.py +300 -0
- ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
- ansys/pyensight/core/utils/support.py +128 -0
- ansys/pyensight/core/utils/variables.py +2019 -0
- ansys/pyensight/core/utils/views.py +674 -0
- ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
- ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
- ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
- ansys_pyensight_core-0.11.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1923 @@
|
|
|
1
|
+
# Copyright (C) 2022 - 2026 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
|
+
"""Session module.
|
|
24
|
+
|
|
25
|
+
The ``Session`` module allows PyEnSight to control the EnSight session.
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
|
|
29
|
+
>>> from ansys.pyensight.core import LocalLauncher
|
|
30
|
+
>>> session = LocalLauncher().start()
|
|
31
|
+
>>> type(session)
|
|
32
|
+
ansys.pyensight.Session
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
import atexit
|
|
36
|
+
import importlib.util
|
|
37
|
+
from os import listdir
|
|
38
|
+
import os.path
|
|
39
|
+
import platform
|
|
40
|
+
import sys
|
|
41
|
+
import textwrap
|
|
42
|
+
import time
|
|
43
|
+
import types
|
|
44
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union
|
|
45
|
+
from urllib.parse import urlparse
|
|
46
|
+
from urllib.request import url2pathname
|
|
47
|
+
import uuid
|
|
48
|
+
import webbrowser
|
|
49
|
+
|
|
50
|
+
from ansys.pyensight.core.enscontext import EnsContext
|
|
51
|
+
from ansys.pyensight.core.launcher import Launcher
|
|
52
|
+
from ansys.pyensight.core.listobj import ensobjlist
|
|
53
|
+
from ansys.pyensight.core.renderable import (
|
|
54
|
+
RenderableDeepPixel,
|
|
55
|
+
RenderableEVSN,
|
|
56
|
+
RenderableFluidsWebUI,
|
|
57
|
+
RenderableImage,
|
|
58
|
+
RenderableMP4,
|
|
59
|
+
RenderableSGEO,
|
|
60
|
+
RenderableVNC,
|
|
61
|
+
RenderableVNCAngular,
|
|
62
|
+
RenderableWebGL,
|
|
63
|
+
)
|
|
64
|
+
import requests
|
|
65
|
+
|
|
66
|
+
if TYPE_CHECKING:
|
|
67
|
+
from ansys.api.pyensight import ensight_api
|
|
68
|
+
from ansys.pyensight.core import enscontext, ensight_grpc, renderable
|
|
69
|
+
from ansys.pyensight.core.ensobj import ENSOBJ
|
|
70
|
+
from ansys.pyensight.core.utils.dsg_server import DSGSession
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class InvalidEnSightVersion(Exception):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Session:
|
|
78
|
+
"""Provides for accessing an EnSight ``Session`` instance.
|
|
79
|
+
|
|
80
|
+
The ``Session`` object wraps the various connections to an EnSight instance. It includes
|
|
81
|
+
the location of the installation and the gRPC, HTML and WS ports used to talk to the
|
|
82
|
+
EnSight session. In most cases, a ``Session`` instance is created using Launcher
|
|
83
|
+
class methods, but if the EnSight session is already running, an instance can be
|
|
84
|
+
created directly to wrap this running EnSight session.
|
|
85
|
+
|
|
86
|
+
If the ``Session`` object is created via a Launcher ``start()`` method call, when the
|
|
87
|
+
session object is garbage collected, the EnSight instance is automatically stopped.
|
|
88
|
+
To prevent this behavior (and leave the EnSight instance running), set the
|
|
89
|
+
``halt_ensight_on_close`` property to ``False``.
|
|
90
|
+
|
|
91
|
+
A gRPC connection is required to interact with an EnSight session. The host, gRPC
|
|
92
|
+
port number, and secret key must be specified. The HTML and WS ports, which are used to
|
|
93
|
+
enable the :func:`show<ansys.pyensight.core.Session.show>`) method, also require that
|
|
94
|
+
an instance of the websocket server is running.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
host : str, optional
|
|
99
|
+
Name of the host that the EnSight gRPC service is running on.
|
|
100
|
+
The default is ``"127.0.0.1"``, which is the localhost.
|
|
101
|
+
install_path : str, optional
|
|
102
|
+
Path to the CEI directory to launch EnSight from.
|
|
103
|
+
The default is ``None``.
|
|
104
|
+
secret_key : str, optional
|
|
105
|
+
Shared session secret key for validating the gRPC communication.
|
|
106
|
+
The default is ``""``.
|
|
107
|
+
grpc_port : int, optional
|
|
108
|
+
Port number of the EnSight gRPC service. The default is ``12345``.
|
|
109
|
+
grpc_use_tcp_sockets : bool, optional
|
|
110
|
+
If using gRPC, and if True, then allow TCP Socket based connections
|
|
111
|
+
instead of only local connections.
|
|
112
|
+
grpc_allow_network_connections : bool, optional
|
|
113
|
+
If using gRPC and using TCP Socket based connections, listen on all networks.
|
|
114
|
+
grpc_disable_tls : bool, optional
|
|
115
|
+
If using gRPC and using TCP Socket based connections, disable TLS.
|
|
116
|
+
grpc_uds_pathname : str, optional
|
|
117
|
+
If using gRPC and using Unix Domain Socket based connections, explicitly
|
|
118
|
+
set the pathname to the shared UDS file instead of using the default.
|
|
119
|
+
html_host : str, optional
|
|
120
|
+
Optional hostname for html connections if different than host
|
|
121
|
+
Used by Ansys Lab and reverse proxy servers
|
|
122
|
+
html_port : int, optional
|
|
123
|
+
Port number of the websocket server's HTTP server. The default is
|
|
124
|
+
``None``.
|
|
125
|
+
ws_port : int, optional
|
|
126
|
+
Port number of the websocket server's WS server. The default is
|
|
127
|
+
``None``.
|
|
128
|
+
session_directory : str, optional
|
|
129
|
+
Directory on the server for local data storage. The default is
|
|
130
|
+
``None``.
|
|
131
|
+
timeout : float, optional
|
|
132
|
+
Number of seconds to try a gRPC connection before giving up.
|
|
133
|
+
The default is ``120``.
|
|
134
|
+
rest_api : bool, optional
|
|
135
|
+
Whether to enable the EnSight REST API for the remote EnSight instance.
|
|
136
|
+
The default is ``False``.
|
|
137
|
+
sos : bool, optional
|
|
138
|
+
Whether the remote EnSight instance is to use the SOS (Server
|
|
139
|
+
of Servers) feature. The default is ``False``.
|
|
140
|
+
webui_port : int, optional
|
|
141
|
+
Port number of the webui. The default is ``None``.
|
|
142
|
+
disable_grpc_options: bool, optional
|
|
143
|
+
Whether to disable the gRPC options check, and allow to run older
|
|
144
|
+
versions of EnSight
|
|
145
|
+
|
|
146
|
+
Examples
|
|
147
|
+
--------
|
|
148
|
+
|
|
149
|
+
>>> from ansys.pyensight.core import Session
|
|
150
|
+
>>> session = Session(host="127.0.0.1", grpc_port=12345, http_port=8000, ws_port=8100)
|
|
151
|
+
|
|
152
|
+
>>> from ansys.pyensight.core import LocalLauncher
|
|
153
|
+
>>> session = LocalLauncher().start()
|
|
154
|
+
|
|
155
|
+
>>> # Launch an instance of EnSight, then create a second connection to the instance
|
|
156
|
+
>>> from ansys.pyensight.core import LocalLauncher, Session
|
|
157
|
+
>>> launched_session = LocalLauncher().start()
|
|
158
|
+
>>> # Get a string that can be used to create a second connection
|
|
159
|
+
>>> session_string = str(launched_session)
|
|
160
|
+
>>> # Create a second connection to the same EnSight instance
|
|
161
|
+
>>> connected_session = eval(session_string)
|
|
162
|
+
|
|
163
|
+
WARNING:
|
|
164
|
+
Overriding the default values for these options: grpc_use_tcp_sockets, grpc_allow_network_connections,
|
|
165
|
+
and grpc_disable_tls
|
|
166
|
+
can possibly permit control of this computer and any data which resides on it.
|
|
167
|
+
Modification of this configuration is not recommended. Please see the
|
|
168
|
+
documentation for your installed product for additional information.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
host: str = "127.0.0.1",
|
|
174
|
+
install_path: Optional[str] = None,
|
|
175
|
+
secret_key: str = "",
|
|
176
|
+
grpc_port: int = 12345,
|
|
177
|
+
grpc_use_tcp_sockets: Optional[bool] = False,
|
|
178
|
+
grpc_allow_network_connections: Optional[bool] = False,
|
|
179
|
+
grpc_disable_tls: Optional[bool] = False,
|
|
180
|
+
grpc_uds_pathname: Optional[str] = None,
|
|
181
|
+
html_hostname: Optional[str] = None,
|
|
182
|
+
html_port: Optional[int] = None,
|
|
183
|
+
ws_port: Optional[int] = None,
|
|
184
|
+
session_directory: Optional[str] = None,
|
|
185
|
+
timeout: float = 120.0,
|
|
186
|
+
rest_api: bool = False,
|
|
187
|
+
sos: bool = False,
|
|
188
|
+
webui_port: Optional[int] = None,
|
|
189
|
+
disable_grpc_options: bool = False,
|
|
190
|
+
) -> None:
|
|
191
|
+
# every session instance needs a unique name that can be used as a cache key
|
|
192
|
+
self._dsg_session: Optional["DSGSession"] = None
|
|
193
|
+
self._session_name = str(uuid.uuid1())
|
|
194
|
+
# when objects come into play, we can reuse them, so hash ID to instance here
|
|
195
|
+
self._ensobj_hash: Dict[int, "ENSOBJ"] = {}
|
|
196
|
+
self._language = "en"
|
|
197
|
+
self._rest_api_enabled = rest_api
|
|
198
|
+
self._sos_enabled = sos
|
|
199
|
+
self._timeout = timeout
|
|
200
|
+
self._cei_home = ""
|
|
201
|
+
self._cei_suffix = ""
|
|
202
|
+
self._hostname = host
|
|
203
|
+
self._install_path = install_path
|
|
204
|
+
self._launcher = None
|
|
205
|
+
if html_hostname == "" or html_hostname is None:
|
|
206
|
+
# if we weren't given an html host, use the hostname
|
|
207
|
+
self._html_hostname = self._hostname
|
|
208
|
+
else:
|
|
209
|
+
self._html_hostname = html_hostname
|
|
210
|
+
self._html_port = html_port
|
|
211
|
+
self._ws_port = ws_port
|
|
212
|
+
self._secret_key = secret_key
|
|
213
|
+
self._grpc_port = grpc_port
|
|
214
|
+
self._grpc_use_tcp_sockets = grpc_use_tcp_sockets
|
|
215
|
+
self._grpc_allow_network_connections = grpc_allow_network_connections
|
|
216
|
+
self._grpc_disable_tls = grpc_disable_tls
|
|
217
|
+
self._grpc_uds_pathname = grpc_uds_pathname
|
|
218
|
+
self._halt_ensight_on_close = True
|
|
219
|
+
self._callbacks: Dict[str, Tuple[int, Any]] = dict()
|
|
220
|
+
self._webui_port = webui_port
|
|
221
|
+
self._disable_grpc_options = disable_grpc_options
|
|
222
|
+
# if the caller passed a session directory we will assume they are
|
|
223
|
+
# creating effectively a proxy Session and create a (stub) launcher
|
|
224
|
+
if session_directory is not None:
|
|
225
|
+
self._launcher = Launcher()
|
|
226
|
+
self._launcher.session_directory = session_directory
|
|
227
|
+
# The stub will not know about us
|
|
228
|
+
self._halt_ensight_on_close = False
|
|
229
|
+
|
|
230
|
+
# are we in a jupyter notebook?
|
|
231
|
+
try:
|
|
232
|
+
_ = get_ipython() # type: ignore
|
|
233
|
+
self._jupyter_notebook = True # pragma: no cover
|
|
234
|
+
except NameError:
|
|
235
|
+
self._jupyter_notebook = False
|
|
236
|
+
|
|
237
|
+
# Connect to the EnSight instance
|
|
238
|
+
from ansys.api.pyensight import ensight_api # pylint: disable=import-outside-toplevel
|
|
239
|
+
from ansys.pyensight.core import ensight_grpc # pylint: disable=import-outside-toplevel
|
|
240
|
+
|
|
241
|
+
self._ensight = ensight_api.ensight(self)
|
|
242
|
+
self._build_utils_interface()
|
|
243
|
+
self._grpc = ensight_grpc.EnSightGRPC(
|
|
244
|
+
host=self._hostname,
|
|
245
|
+
port=self._grpc_port,
|
|
246
|
+
secret_key=self._secret_key,
|
|
247
|
+
grpc_use_tcp_sockets=self._grpc_use_tcp_sockets,
|
|
248
|
+
grpc_allow_network_connections=self._grpc_allow_network_connections,
|
|
249
|
+
grpc_disable_tls=self._grpc_disable_tls,
|
|
250
|
+
grpc_uds_pathname=self._grpc_uds_pathname,
|
|
251
|
+
disable_grpc_options=self._disable_grpc_options,
|
|
252
|
+
)
|
|
253
|
+
self._grpc.session_name = self._session_name
|
|
254
|
+
|
|
255
|
+
# establish the connection with retry
|
|
256
|
+
self._establish_connection(validate=True)
|
|
257
|
+
|
|
258
|
+
# update the enums to match current EnSight instance
|
|
259
|
+
cmd = "{key: getattr(ensight.objs.enums, key) for key in dir(ensight.objs.enums)}"
|
|
260
|
+
new_enums = self.cmd(cmd)
|
|
261
|
+
for key, value in new_enums.items():
|
|
262
|
+
if key.startswith("__") and (key != "__OBJID__"):
|
|
263
|
+
continue
|
|
264
|
+
setattr(self._ensight.objs.enums, key, value)
|
|
265
|
+
|
|
266
|
+
# create ensight.core
|
|
267
|
+
self._ensight.objs.core = self.cmd("ensight.objs.core")
|
|
268
|
+
|
|
269
|
+
# get the remote Python interpreter version
|
|
270
|
+
self.cmd("import platform", do_eval=False)
|
|
271
|
+
self._ensight_python_version = self.cmd("platform.python_version_tuple()")
|
|
272
|
+
|
|
273
|
+
# Because this session can have allocated significant external resources
|
|
274
|
+
# we very much want a chance to close it up cleanly. It is legal to
|
|
275
|
+
# call close() twice on this class if needed.
|
|
276
|
+
self._already_closed = False
|
|
277
|
+
atexit.register(self.close)
|
|
278
|
+
|
|
279
|
+
# Speed up subtype lookups:
|
|
280
|
+
self._subtype_tables = {}
|
|
281
|
+
part_lookup_dict = dict()
|
|
282
|
+
part_lookup_dict[0] = "ENS_PART_MODEL"
|
|
283
|
+
part_lookup_dict[1] = "ENS_PART_CLIP"
|
|
284
|
+
part_lookup_dict[2] = "ENS_PART_CONTOUR"
|
|
285
|
+
part_lookup_dict[3] = "ENS_PART_DISCRETE_PARTICLE"
|
|
286
|
+
part_lookup_dict[4] = "ENS_PART_FRAME"
|
|
287
|
+
part_lookup_dict[5] = "ENS_PART_ISOSURFACE"
|
|
288
|
+
part_lookup_dict[6] = "ENS_PART_PARTICLE_TRACE"
|
|
289
|
+
part_lookup_dict[7] = "ENS_PART_PROFILE"
|
|
290
|
+
part_lookup_dict[8] = "ENS_PART_VECTOR_ARROW"
|
|
291
|
+
part_lookup_dict[9] = "ENS_PART_ELEVATED_SURFACE"
|
|
292
|
+
part_lookup_dict[10] = "ENS_PART_DEVELOPED_SURFACE"
|
|
293
|
+
part_lookup_dict[15] = "ENS_PART_BUILT_UP"
|
|
294
|
+
part_lookup_dict[16] = "ENS_PART_TENSOR_GLYPH"
|
|
295
|
+
part_lookup_dict[17] = "ENS_PART_FX_VORTEX_CORE"
|
|
296
|
+
part_lookup_dict[18] = "ENS_PART_FX_SHOCK"
|
|
297
|
+
part_lookup_dict[19] = "ENS_PART_FX_SEP_ATT"
|
|
298
|
+
part_lookup_dict[20] = "ENS_PART_MAT_INTERFACE"
|
|
299
|
+
part_lookup_dict[21] = "ENS_PART_POINT"
|
|
300
|
+
part_lookup_dict[22] = "ENS_PART_AXISYMMETRIC"
|
|
301
|
+
part_lookup_dict[24] = "ENS_PART_VOF"
|
|
302
|
+
part_lookup_dict[25] = "ENS_PART_AUX_GEOM"
|
|
303
|
+
part_lookup_dict[26] = "ENS_PART_FILTER"
|
|
304
|
+
self._subtype_tables["ENS_PART"] = part_lookup_dict
|
|
305
|
+
annot_lookup_dict = dict()
|
|
306
|
+
annot_lookup_dict[0] = "ENS_ANNOT_TEXT"
|
|
307
|
+
annot_lookup_dict[1] = "ENS_ANNOT_LINE"
|
|
308
|
+
annot_lookup_dict[2] = "ENS_ANNOT_LOGO"
|
|
309
|
+
annot_lookup_dict[3] = "ENS_ANNOT_LGND"
|
|
310
|
+
annot_lookup_dict[4] = "ENS_ANNOT_MARKER"
|
|
311
|
+
annot_lookup_dict[5] = "ENS_ANNOT_ARROW"
|
|
312
|
+
annot_lookup_dict[6] = "ENS_ANNOT_DIAL"
|
|
313
|
+
annot_lookup_dict[7] = "ENS_ANNOT_GAUGE"
|
|
314
|
+
annot_lookup_dict[8] = "ENS_ANNOT_SHAPE"
|
|
315
|
+
self._subtype_tables["ENS_ANNOT"] = annot_lookup_dict
|
|
316
|
+
tool_lookup_dict = dict()
|
|
317
|
+
tool_lookup_dict[0] = "ENS_TOOL_CURSOR"
|
|
318
|
+
tool_lookup_dict[1] = "ENS_TOOL_LINE"
|
|
319
|
+
tool_lookup_dict[2] = "ENS_TOOL_PLANE"
|
|
320
|
+
tool_lookup_dict[3] = "ENS_TOOL_BOX"
|
|
321
|
+
tool_lookup_dict[4] = "ENS_TOOL_CYLINDER"
|
|
322
|
+
tool_lookup_dict[5] = "ENS_TOOL_CONE"
|
|
323
|
+
tool_lookup_dict[6] = "ENS_TOOL_SPHERE"
|
|
324
|
+
tool_lookup_dict[7] = "ENS_TOOL_REVOLUTION"
|
|
325
|
+
self._subtype_tables["ENS_TOOL"] = tool_lookup_dict
|
|
326
|
+
|
|
327
|
+
def __repr__(self):
|
|
328
|
+
# if this is called from in the ctor, self.launcher might be None.
|
|
329
|
+
session_dir = ""
|
|
330
|
+
if self.launcher:
|
|
331
|
+
session_dir = self.launcher.session_directory
|
|
332
|
+
s = f"Session(host='{self.hostname}', secret_key='{self.secret_key}', "
|
|
333
|
+
s += f"sos={self.sos}, rest_api={self.rest_api}, "
|
|
334
|
+
s += f"html_hostname='{self.html_hostname}', html_port={self.html_port}, "
|
|
335
|
+
s += f"grpc_port={self._grpc_port}, "
|
|
336
|
+
s += f"ws_port={self.ws_port}, session_directory=r'{session_dir}', "
|
|
337
|
+
s += f"webui_port={self._webui_port}, "
|
|
338
|
+
s += f"grpc_use_tcp_sockets={self._grpc_use_tcp_sockets}, "
|
|
339
|
+
s += f"grpc_allow_network_connections={self._grpc_allow_network_connections}, "
|
|
340
|
+
s += f"grpc_disable_tls={self._grpc_disable_tls}, "
|
|
341
|
+
s += f"grpc_uds_pathname='{self._grpc_uds_pathname}')"
|
|
342
|
+
return s
|
|
343
|
+
|
|
344
|
+
def _establish_connection(self, validate: bool = False) -> None:
|
|
345
|
+
"""Establish a gRPC connection to the EnSight instance.
|
|
346
|
+
|
|
347
|
+
Parameters
|
|
348
|
+
----------
|
|
349
|
+
validate : bool
|
|
350
|
+
If true, actually try to communicate with EnSight. By default false.
|
|
351
|
+
"""
|
|
352
|
+
time_start = time.time()
|
|
353
|
+
while time.time() - time_start < self._timeout: # pragma: no cover
|
|
354
|
+
if self._grpc.is_connected():
|
|
355
|
+
try:
|
|
356
|
+
if validate:
|
|
357
|
+
self._cei_home = self.cmd("ensight.version('CEI_HOME')")
|
|
358
|
+
self._cei_suffix = self.cmd("ensight.version('suffix')")
|
|
359
|
+
self._check_rest_connection()
|
|
360
|
+
return
|
|
361
|
+
except OSError: # pragma: no cover
|
|
362
|
+
pass # pragma: no cover
|
|
363
|
+
self._grpc.connect(timeout=self._timeout)
|
|
364
|
+
raise RuntimeError("Unable to establish a gRPC connection to EnSight.") # pragma: no cover
|
|
365
|
+
|
|
366
|
+
def _check_rest_connection(self) -> None:
|
|
367
|
+
"""Validate the REST API connection works
|
|
368
|
+
|
|
369
|
+
Use requests to see if the REST API is up and running (it takes time
|
|
370
|
+
for websocketserver to make a gRPC connection as well).
|
|
371
|
+
|
|
372
|
+
"""
|
|
373
|
+
if not self.rest_api:
|
|
374
|
+
return
|
|
375
|
+
#
|
|
376
|
+
#
|
|
377
|
+
# even when using PIM and a proxy server (Ansys Lab) this connects
|
|
378
|
+
# directly from the python running in the Notebook (the front-end)
|
|
379
|
+
# to the EnSight Docker Container and not the proxy server.
|
|
380
|
+
# Thus, here we use 'http', the private hostname, and the html port
|
|
381
|
+
# (which is the same on the proxy server).
|
|
382
|
+
url = f"http://{self._hostname}:{self.html_port}/ensight/v1/session/exec"
|
|
383
|
+
time_start = time.time()
|
|
384
|
+
while time.time() - time_start < self._timeout:
|
|
385
|
+
try:
|
|
386
|
+
_ = requests.put(
|
|
387
|
+
url,
|
|
388
|
+
json="enscl.rest_test = 30*20",
|
|
389
|
+
headers=dict(Authorization=f"Bearer {self.secret_key}"),
|
|
390
|
+
)
|
|
391
|
+
return
|
|
392
|
+
except Exception:
|
|
393
|
+
pass
|
|
394
|
+
time.sleep(0.5)
|
|
395
|
+
raise RuntimeError("Unable to establish a REST connection to EnSight.") # pragma: no cover
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def name(self) -> str:
|
|
399
|
+
"""The session name is a unique identifier for this Session instance. It
|
|
400
|
+
is used by EnSight to maintain session specific data values within the
|
|
401
|
+
EnSight instance."""
|
|
402
|
+
return self._session_name
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def language(self) -> str:
|
|
406
|
+
"""Current language specification for the EnSight session. Various
|
|
407
|
+
information calls return their information in the target language
|
|
408
|
+
if possible. The default is ``"en"``.
|
|
409
|
+
|
|
410
|
+
Examples
|
|
411
|
+
--------
|
|
412
|
+
|
|
413
|
+
>>> session.language = "en"
|
|
414
|
+
>>> session.ensight.objs.core.attrinfo(session.ensight.objs.enums.PREDEFINEDPALETTES)
|
|
415
|
+
>>> session.language = "zh"
|
|
416
|
+
>>> session.ensight.objs.core.attrinfo(session.ensight.objs.enums.PREDEFINEDPALETTES)
|
|
417
|
+
|
|
418
|
+
"""
|
|
419
|
+
return self._language
|
|
420
|
+
|
|
421
|
+
@language.setter
|
|
422
|
+
def language(self, value: str) -> None:
|
|
423
|
+
self._language = value
|
|
424
|
+
self.cmd(f"ensight.core.tr.changelang(lin='{self._language}')", do_eval=False)
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def halt_ensight_on_close(self) -> bool:
|
|
428
|
+
"""Flag for indicating whether to halt EnSight on close. If this property
|
|
429
|
+
is ``True`` and the session was created via a launcher, when the session
|
|
430
|
+
is closed, the EnSight instance is stopped.
|
|
431
|
+
|
|
432
|
+
.. Note::
|
|
433
|
+
While this flag prevents the :func:`close<ansys.pyensight.core.Session.close>`
|
|
434
|
+
method from shutting down EnSight, depending on how the host Python interpreter is configured,
|
|
435
|
+
the EnSight session may still be halted. For example, this behavior can
|
|
436
|
+
occur in Jupyter Lab.
|
|
437
|
+
"""
|
|
438
|
+
return self._halt_ensight_on_close
|
|
439
|
+
|
|
440
|
+
@halt_ensight_on_close.setter
|
|
441
|
+
def halt_ensight_on_close(self, value: bool) -> None:
|
|
442
|
+
self._halt_ensight_on_close = value
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def timeout(self) -> float:
|
|
446
|
+
"""Amount of time in seconds to try a gRPC connection before giving up."""
|
|
447
|
+
return self._timeout
|
|
448
|
+
|
|
449
|
+
@timeout.setter
|
|
450
|
+
def timeout(self, value: float) -> None:
|
|
451
|
+
self._timeout = value
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def cei_home(self) -> str:
|
|
455
|
+
"""Value of ``CEI_HOME`` for the connected EnSight session."""
|
|
456
|
+
return self._cei_home
|
|
457
|
+
|
|
458
|
+
@property
|
|
459
|
+
def cei_suffix(self) -> str:
|
|
460
|
+
"""Suffix string of the connected EnSight session. For example, ``222``."""
|
|
461
|
+
return self._cei_suffix
|
|
462
|
+
|
|
463
|
+
@property
|
|
464
|
+
def jupyter_notebook(self) -> bool:
|
|
465
|
+
"""Flag indicating if the session is running in a Jupyter notebook and should use
|
|
466
|
+
the display features of that interface.
|
|
467
|
+
|
|
468
|
+
"""
|
|
469
|
+
return self._jupyter_notebook
|
|
470
|
+
|
|
471
|
+
@jupyter_notebook.setter
|
|
472
|
+
def jupyter_notebook(self, value: bool) -> None:
|
|
473
|
+
self._jupyter_notebook = value
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def ensight(self) -> "ensight_api.ensight":
|
|
477
|
+
"""Core EnSight API wrapper."""
|
|
478
|
+
return self._ensight
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def grpc(self) -> "ensight_grpc.EnSightGRPC":
|
|
482
|
+
"""The gRPC wrapper instance used by this session to access EnSight."""
|
|
483
|
+
return self._grpc
|
|
484
|
+
|
|
485
|
+
@property
|
|
486
|
+
def secret_key(self) -> str:
|
|
487
|
+
"""Secret key used for communication validation in the gRPC instance."""
|
|
488
|
+
return self._secret_key
|
|
489
|
+
|
|
490
|
+
@property
|
|
491
|
+
def html_port(self) -> Optional[int]:
|
|
492
|
+
"""Port supporting HTML interaction with EnSight."""
|
|
493
|
+
return self._html_port
|
|
494
|
+
|
|
495
|
+
@property
|
|
496
|
+
def ws_port(self) -> Optional[int]:
|
|
497
|
+
"""Port supporting WS interaction with EnSight."""
|
|
498
|
+
return self._ws_port
|
|
499
|
+
|
|
500
|
+
@property
|
|
501
|
+
def hostname(self) -> str:
|
|
502
|
+
"""Hostname of the system hosting the EnSight instance."""
|
|
503
|
+
return self._hostname
|
|
504
|
+
|
|
505
|
+
@property
|
|
506
|
+
def html_hostname(self) -> str:
|
|
507
|
+
"""Hostname of the system hosting the EnSight web server instance."""
|
|
508
|
+
return self._html_hostname
|
|
509
|
+
|
|
510
|
+
@property
|
|
511
|
+
def launcher(self) -> "Launcher":
|
|
512
|
+
"""Reference to the launcher instance if a launcher was used to instantiate the session."""
|
|
513
|
+
return self._launcher
|
|
514
|
+
|
|
515
|
+
@launcher.setter
|
|
516
|
+
def launcher(self, value: "Launcher"):
|
|
517
|
+
self._launcher = value
|
|
518
|
+
|
|
519
|
+
@property
|
|
520
|
+
def sos(self) -> bool:
|
|
521
|
+
"""
|
|
522
|
+
Flag indicating if the remote EnSight session is running in SOS (Server of Server) mode.
|
|
523
|
+
"""
|
|
524
|
+
return self._sos_enabled
|
|
525
|
+
|
|
526
|
+
@property
|
|
527
|
+
def rest_api(self) -> bool:
|
|
528
|
+
"""
|
|
529
|
+
Flag indicating if the remote EnSight session supports the REST API.
|
|
530
|
+
"""
|
|
531
|
+
return self._rest_api_enabled
|
|
532
|
+
|
|
533
|
+
@staticmethod
|
|
534
|
+
def help():
|
|
535
|
+
"""Open the documentation for PyEnSight in a web browser."""
|
|
536
|
+
url = "https://ensight.docs.pyansys.com/"
|
|
537
|
+
webbrowser.open(url)
|
|
538
|
+
|
|
539
|
+
def copy_to_session(
|
|
540
|
+
self,
|
|
541
|
+
local_prefix: str,
|
|
542
|
+
filelist: List[str],
|
|
543
|
+
remote_prefix: Optional[str] = None,
|
|
544
|
+
progress: bool = False,
|
|
545
|
+
) -> list:
|
|
546
|
+
"""Copy a collection of files into the EnSight session.
|
|
547
|
+
|
|
548
|
+
Copy files from the local filesystem into the filesystem that is hosting
|
|
549
|
+
the EnSight instance.
|
|
550
|
+
|
|
551
|
+
.. note::
|
|
552
|
+
For a :class:`LocalLauncheransys.pyensight.core.LocalLauncher>`
|
|
553
|
+
instance, these are the same filesystems.
|
|
554
|
+
|
|
555
|
+
Parameters
|
|
556
|
+
----------
|
|
557
|
+
local_prefix : str
|
|
558
|
+
URL prefix to use for all files specified for the ``filelist``
|
|
559
|
+
parameter. The only protocol supported is ``'file://'``, which
|
|
560
|
+
is the local filesystem.
|
|
561
|
+
filelist : list
|
|
562
|
+
List of files to copy. These files are prefixed with ``local_prefix``
|
|
563
|
+
and written relative to the ``remote_prefix`` parameter appended to
|
|
564
|
+
``session.launcher.session_directory``.
|
|
565
|
+
remote_prefix : str
|
|
566
|
+
Directory on the remote (EnSight) filesystem, which is the
|
|
567
|
+
destination for the files. This prefix is appended to
|
|
568
|
+
``session.launcher.session_directory``.
|
|
569
|
+
progress : bool, optional
|
|
570
|
+
Whether to show a progress bar. The default is ``False``. If ``True`` and
|
|
571
|
+
the ``tqdm`` module is available, a progress bar is shown.
|
|
572
|
+
|
|
573
|
+
Returns
|
|
574
|
+
-------
|
|
575
|
+
list
|
|
576
|
+
List of the filenames that were copied and their sizes.
|
|
577
|
+
|
|
578
|
+
Examples
|
|
579
|
+
--------
|
|
580
|
+
>>> the_files = ["fluent_data_dir", "ensight_script.py"]
|
|
581
|
+
>>> session.copy_to_session("file:///D:/data", the_files, progress=True)
|
|
582
|
+
|
|
583
|
+
>>> the_files = ["fluent_data_dir", "ensight_script.py"]
|
|
584
|
+
>>> session.copy_to_session("file:///scratch/data", the_files, remote_prefix="data")
|
|
585
|
+
|
|
586
|
+
"""
|
|
587
|
+
uri = urlparse(local_prefix)
|
|
588
|
+
if uri.scheme != "file":
|
|
589
|
+
raise RuntimeError("Only the file:// protocol is supported for the local_prefix")
|
|
590
|
+
localdir = url2pathname(uri.path)
|
|
591
|
+
|
|
592
|
+
remote_functions = textwrap.dedent(
|
|
593
|
+
"""\
|
|
594
|
+
import os
|
|
595
|
+
def copy_write_function__(filename: str, data: bytes) -> None:
|
|
596
|
+
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
|
597
|
+
with open(filename, "ab") as fp:
|
|
598
|
+
fp.write(data)
|
|
599
|
+
"""
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
self.cmd(remote_functions, do_eval=False)
|
|
603
|
+
|
|
604
|
+
out = []
|
|
605
|
+
dirlen = 0
|
|
606
|
+
if localdir: # pragma: no cover
|
|
607
|
+
# we use dirlen + 1 here to remove the '/' inserted by os.path.join()
|
|
608
|
+
dirlen = len(localdir) + 1
|
|
609
|
+
for item in filelist:
|
|
610
|
+
try:
|
|
611
|
+
name = os.path.join(localdir, item)
|
|
612
|
+
if os.path.isfile(name):
|
|
613
|
+
out.append((name[dirlen:], os.stat(name).st_size))
|
|
614
|
+
else:
|
|
615
|
+
for root, _, files in os.walk(name):
|
|
616
|
+
for filename in files:
|
|
617
|
+
fullname = os.path.join(root, filename)
|
|
618
|
+
out.append((fullname[dirlen:], os.stat(fullname).st_size))
|
|
619
|
+
except Exception:
|
|
620
|
+
pass
|
|
621
|
+
if progress: # pragma: no cover
|
|
622
|
+
try:
|
|
623
|
+
from tqdm.auto import tqdm
|
|
624
|
+
except ImportError:
|
|
625
|
+
tqdm = list
|
|
626
|
+
else:
|
|
627
|
+
tqdm = list
|
|
628
|
+
for item in tqdm(out):
|
|
629
|
+
filename = os.path.join(localdir, item[0])
|
|
630
|
+
out_dir = self.launcher.session_directory.replace("\\", "/")
|
|
631
|
+
if remote_prefix:
|
|
632
|
+
out_dir += f"/{remote_prefix}"
|
|
633
|
+
name = out_dir + f"/{item[0]}"
|
|
634
|
+
name = name.replace("\\", "/")
|
|
635
|
+
# Walk the file in chunk size blocks
|
|
636
|
+
chunk_size = 1024 * 1024
|
|
637
|
+
with open(filename, "rb") as fp:
|
|
638
|
+
while True:
|
|
639
|
+
data = fp.read(chunk_size)
|
|
640
|
+
if data == b"":
|
|
641
|
+
break
|
|
642
|
+
self.cmd(
|
|
643
|
+
f"copy_write_function__(r'{name}', {data!r})", do_eval=False
|
|
644
|
+
) # pragma: no cover
|
|
645
|
+
return out
|
|
646
|
+
|
|
647
|
+
def copy_from_session(
|
|
648
|
+
self,
|
|
649
|
+
local_prefix: str,
|
|
650
|
+
filelist: List[str],
|
|
651
|
+
remote_prefix: Optional[str] = None,
|
|
652
|
+
progress: bool = False,
|
|
653
|
+
) -> list:
|
|
654
|
+
"""Copy a collection of files out of the EnSight session.
|
|
655
|
+
|
|
656
|
+
Copy files from the filesystem of the remote EnSight instance to the
|
|
657
|
+
filesystem of the local PyEnsight instance.
|
|
658
|
+
|
|
659
|
+
.. note::
|
|
660
|
+
For a :class:`LocalLauncheransys.pyensight.core.LocalLauncher>`
|
|
661
|
+
instance, these are the same filesystems.
|
|
662
|
+
|
|
663
|
+
Parameters
|
|
664
|
+
----------
|
|
665
|
+
local_prefix : str
|
|
666
|
+
URL prefix of the location to save the files to. The only
|
|
667
|
+
protocol currently supported is ``'file://'``, which is the
|
|
668
|
+
local filesystem.
|
|
669
|
+
filelist : list
|
|
670
|
+
List of the files to copy. These files are prefixed
|
|
671
|
+
with ``session.launcher.session_directory/remote_prefix`` and written
|
|
672
|
+
relative to URL prefix specified for the ``local_prefix`` parameter.
|
|
673
|
+
remote_prefix : str, optional
|
|
674
|
+
Directory on the remote (EnSight) filesystem that is the source
|
|
675
|
+
for the files. This prefix is appended to ``session.launcher.session_directory``.
|
|
676
|
+
progress : bool, optional
|
|
677
|
+
Whether to show a progress bar. The default is ``False``. If ``True`` and
|
|
678
|
+
the ``tqdm`` module is available, a progress bar is shown.
|
|
679
|
+
|
|
680
|
+
Returns
|
|
681
|
+
-------
|
|
682
|
+
list
|
|
683
|
+
List of the files that were copied.
|
|
684
|
+
|
|
685
|
+
Examples
|
|
686
|
+
--------
|
|
687
|
+
>>> the_files = ["fluent_data_dir", "ensight_script.py"]
|
|
688
|
+
>>> session.copy_from_session("file:///D:/restored_data", the_files, progress=True)
|
|
689
|
+
|
|
690
|
+
>>> the_files = ["fluent_data_dir", "ensight_script.py"]
|
|
691
|
+
>>> session.copy_from_session("file:///scratch/restored_data", the_files,
|
|
692
|
+
remote_prefix="data")
|
|
693
|
+
"""
|
|
694
|
+
|
|
695
|
+
uri = urlparse(local_prefix)
|
|
696
|
+
if uri.scheme != "file":
|
|
697
|
+
raise RuntimeError("Only the file:// protocol is supported for the local_prefix")
|
|
698
|
+
localdir = url2pathname(uri.path)
|
|
699
|
+
|
|
700
|
+
remote_functions = textwrap.dedent(
|
|
701
|
+
"""\
|
|
702
|
+
import os
|
|
703
|
+
def copy_walk_function__(remotedir: str, filelist: list) -> None:
|
|
704
|
+
out = []
|
|
705
|
+
dirlen = 0
|
|
706
|
+
if remotedir:
|
|
707
|
+
dirlen = len(remotedir) + 1
|
|
708
|
+
for item in filelist:
|
|
709
|
+
try:
|
|
710
|
+
name = os.path.join(remotedir, item)
|
|
711
|
+
if os.path.isfile(name):
|
|
712
|
+
out.append((name[dirlen:], os.stat(name).st_size))
|
|
713
|
+
else:
|
|
714
|
+
for root, _, files in os.walk(name):
|
|
715
|
+
for filename in files:
|
|
716
|
+
fullname = os.path.join(root, filename)
|
|
717
|
+
out.append((fullname[dirlen:], os.stat(fullname).st_size))
|
|
718
|
+
except Exception:
|
|
719
|
+
pass
|
|
720
|
+
return out
|
|
721
|
+
# (needed for flake8)
|
|
722
|
+
def copy_read_function__(filename: str, offset: int, numbytes: int) -> bytes:
|
|
723
|
+
with open(filename, "rb") as fp:
|
|
724
|
+
fp.seek(offset)
|
|
725
|
+
data = fp.read(numbytes)
|
|
726
|
+
return data
|
|
727
|
+
"""
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
self.cmd(remote_functions, do_eval=False)
|
|
731
|
+
|
|
732
|
+
remote_directory = self.launcher.session_directory
|
|
733
|
+
if remote_prefix:
|
|
734
|
+
remote_directory = f"{remote_directory}/{remote_prefix}"
|
|
735
|
+
remote_directory = remote_directory.replace("\\", "/")
|
|
736
|
+
names = self.cmd(f"copy_walk_function__(r'{remote_directory}', {filelist})", do_eval=True)
|
|
737
|
+
if progress:
|
|
738
|
+
try:
|
|
739
|
+
from tqdm.auto import tqdm
|
|
740
|
+
except ImportError:
|
|
741
|
+
tqdm = list
|
|
742
|
+
else:
|
|
743
|
+
tqdm = list
|
|
744
|
+
for item in tqdm(names):
|
|
745
|
+
name = f"{remote_directory}/{item[0]}".replace("\\", "/")
|
|
746
|
+
full_name = os.path.join(localdir, item[0])
|
|
747
|
+
os.makedirs(os.path.dirname(full_name), exist_ok=True)
|
|
748
|
+
with open(full_name, "wb") as fp:
|
|
749
|
+
offset = 0
|
|
750
|
+
chunk_size = 1024 * 1024
|
|
751
|
+
while True:
|
|
752
|
+
data = self.cmd(
|
|
753
|
+
f"copy_read_function__(r'{name}', {offset}, {chunk_size})", do_eval=True
|
|
754
|
+
)
|
|
755
|
+
if len(data) == 0:
|
|
756
|
+
break
|
|
757
|
+
fp.write(data)
|
|
758
|
+
offset += chunk_size
|
|
759
|
+
return names
|
|
760
|
+
|
|
761
|
+
def run_script(self, filename: str) -> Optional[types.ModuleType]:
|
|
762
|
+
"""Run an EnSight Python script file.
|
|
763
|
+
|
|
764
|
+
In EnSight, there is a notion of a Python *script* that is normally run line by
|
|
765
|
+
line in EnSight. In such scripts, the ``ensight`` module is assumed to be preloaded.
|
|
766
|
+
This method runs such scripts by importing them as modules and running the commands
|
|
767
|
+
through the PyEnSight interface. This is done by installing the PyEnsight ``Session``
|
|
768
|
+
object into the module before it is imported. This makes it possible to use a
|
|
769
|
+
Python debugger with an EnSight Python script, using the PyEnSight interface.
|
|
770
|
+
|
|
771
|
+
.. note::
|
|
772
|
+
|
|
773
|
+
Because the Python script is imported as a module, the script filename must
|
|
774
|
+
have a ``.py`` extension.
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
Parameters
|
|
778
|
+
----------
|
|
779
|
+
filename : str
|
|
780
|
+
Filename of the Python script to run, which is loaded as a module by PyEnSight.
|
|
781
|
+
|
|
782
|
+
Returns
|
|
783
|
+
-------
|
|
784
|
+
types.ModuleType
|
|
785
|
+
Imported module.
|
|
786
|
+
|
|
787
|
+
"""
|
|
788
|
+
dirname = os.path.dirname(filename)
|
|
789
|
+
if not dirname: # pragma: no cover
|
|
790
|
+
dirname = "." # pragma: no cover
|
|
791
|
+
if dirname not in sys.path:
|
|
792
|
+
sys.path.append(dirname)
|
|
793
|
+
module_name, _ = os.path.splitext(os.path.basename(filename))
|
|
794
|
+
# get the module reference
|
|
795
|
+
spec = importlib.util.find_spec(module_name)
|
|
796
|
+
if spec: # pragma: no cover
|
|
797
|
+
module = importlib.util.module_from_spec(spec)
|
|
798
|
+
# insert an ensight interface into the module
|
|
799
|
+
if self.ensight:
|
|
800
|
+
module.ensight = self.ensight # type: ignore
|
|
801
|
+
# load (run) the module
|
|
802
|
+
if spec.loader: # pragma: no cover
|
|
803
|
+
spec.loader.exec_module(module)
|
|
804
|
+
return module
|
|
805
|
+
return None # pragma: no cover
|
|
806
|
+
|
|
807
|
+
def exec(self, function: Callable, *args, remote: bool = False, **kwargs) -> Any:
|
|
808
|
+
"""Run a function containing EnSight API calls locally or in the EnSight interpreter.
|
|
809
|
+
|
|
810
|
+
The function is in this form::
|
|
811
|
+
|
|
812
|
+
def myfunc(ensight, *args, **kwargs):
|
|
813
|
+
...
|
|
814
|
+
return value
|
|
815
|
+
|
|
816
|
+
The ``exec()`` method allows for the function to be executed in the PyEnSight Python
|
|
817
|
+
interpreter or the (remote) EnSight interpreter. Thus, a function making a large
|
|
818
|
+
number of RPC calls can run much faster than if it runs solely in the PyEnSight
|
|
819
|
+
interpreter.
|
|
820
|
+
|
|
821
|
+
These constraints exist on this capability:
|
|
822
|
+
|
|
823
|
+
- The function may only use arguments passed to the ``exec()`` method and can only
|
|
824
|
+
return a single value.
|
|
825
|
+
- The function cannot modify the input arguments.
|
|
826
|
+
- The input arguments must be serializable and the PyEnSight Python interpreter
|
|
827
|
+
version must match the version in EnSight.
|
|
828
|
+
|
|
829
|
+
Parameters
|
|
830
|
+
----------
|
|
831
|
+
remote : bool, optional
|
|
832
|
+
Whether to execute the function in the (remote) EnSight interpreter.
|
|
833
|
+
|
|
834
|
+
Examples
|
|
835
|
+
--------
|
|
836
|
+
>>> from ansys.pyensight.core import LocalLauncher
|
|
837
|
+
>>> session = LocalLauncher().start()
|
|
838
|
+
>>> options = dict()
|
|
839
|
+
>>> options['Verbose mode'] = 'OFF'
|
|
840
|
+
>>> options['Use ghost elements'] = 'OFF'
|
|
841
|
+
>>> options['Long names'] = 'OFF'
|
|
842
|
+
>>> options['Compatibility mode'] = 'ON'
|
|
843
|
+
>>> options['Move Transient Parts'] = 'ON'
|
|
844
|
+
>>> options['Element type'] = 'Tri 3'
|
|
845
|
+
>>> options['Boundary ghosts'] = 'None'
|
|
846
|
+
>>> options['Spread out parts'] = 'Legacy'
|
|
847
|
+
>>> options['Number of spheres'] = 100
|
|
848
|
+
>>> options['Number of cubes'] = 100
|
|
849
|
+
>>> options['Number of planes'] = 0
|
|
850
|
+
>>> options['Number of elements start'] = 1000
|
|
851
|
+
>>> options['Number of elements end'] = 1000
|
|
852
|
+
>>> options['Number of timesteps'] = 1
|
|
853
|
+
>>> options['Part scaling factor'] = 1.000000e+00
|
|
854
|
+
>>> options['Random number seed'] = 0
|
|
855
|
+
>>> options['Number of scalars'] = 3
|
|
856
|
+
>>> options['Number of vectors'] = 3
|
|
857
|
+
>>> options['Number of constants'] = 3
|
|
858
|
+
>>> session.load_data("dummy", file_format="Synthetic", reader_options=options)
|
|
859
|
+
|
|
860
|
+
>>> def count(ensight, attr, value):
|
|
861
|
+
>>> import time
|
|
862
|
+
>>> start = time.time()
|
|
863
|
+
>>> count = 0
|
|
864
|
+
>>> for p in ensight.objs.core.PARTS:
|
|
865
|
+
>>> if p.getattr(attr) == value:
|
|
866
|
+
>>> count += 1
|
|
867
|
+
>>> print(count(session.ensight, "VISIBLE", True))
|
|
868
|
+
>>> print(session.exec(count, "VISIBLE", True))
|
|
869
|
+
>>> print(session.exec(count, "VISIBLE", True, remote=True))
|
|
870
|
+
|
|
871
|
+
"""
|
|
872
|
+
if remote:
|
|
873
|
+
# remote execution only supported in 2023 R1 or later
|
|
874
|
+
if int(self._cei_suffix) < 231:
|
|
875
|
+
raise RuntimeError("Remote function execution only supported in 2023 R1 and later")
|
|
876
|
+
local_python_version = platform.python_version_tuple()
|
|
877
|
+
if self._ensight_python_version[0:2] != local_python_version[0:2]:
|
|
878
|
+
vers = "Local and remote Python versions must match: "
|
|
879
|
+
vers += ".".join(local_python_version)
|
|
880
|
+
vers += " vs "
|
|
881
|
+
vers += ".".join(self._ensight_python_version)
|
|
882
|
+
raise RuntimeError(vers)
|
|
883
|
+
import dill # pylint: disable=import-outside-toplevel
|
|
884
|
+
|
|
885
|
+
# Create a bound object that allows for direct encoding of the args/kwargs params
|
|
886
|
+
# The new function would be bound_function(ensight) where the args are captured
|
|
887
|
+
# in the lambda.
|
|
888
|
+
bound_function = lambda ens: function( # noqa: E731 # pragma: no cover
|
|
889
|
+
ens, *args, **kwargs
|
|
890
|
+
)
|
|
891
|
+
# Serialize the bound function
|
|
892
|
+
serialized_function = dill.dumps(bound_function, recurse=True)
|
|
893
|
+
self.cmd("import dill", do_eval=False)
|
|
894
|
+
# Run it remotely, passing the instance ensight instead of self._ensight
|
|
895
|
+
cmd = f"dill.loads(eval(repr({serialized_function})))(ensight)"
|
|
896
|
+
return self.cmd(cmd)
|
|
897
|
+
else:
|
|
898
|
+
return function(self._ensight, *args, **kwargs)
|
|
899
|
+
|
|
900
|
+
def show(
|
|
901
|
+
self,
|
|
902
|
+
what: str = "image",
|
|
903
|
+
width: Optional[int] = None,
|
|
904
|
+
height: Optional[int] = None,
|
|
905
|
+
temporal: bool = False,
|
|
906
|
+
aa: int = 4,
|
|
907
|
+
fps: float = 30.0,
|
|
908
|
+
num_frames: Optional[int] = None,
|
|
909
|
+
ui: Optional[str] = "simple",
|
|
910
|
+
) -> "renderable.Renderable":
|
|
911
|
+
"""Capture the current EnSight scene or otherwise make it available for
|
|
912
|
+
display in a web browser.
|
|
913
|
+
|
|
914
|
+
This method generates the appropriate visuals and returns the renderable
|
|
915
|
+
object for viewing. If the session is in a Jupyter notebook, the cell
|
|
916
|
+
in which the ``show()`` method is issued is updated with the renderable display.
|
|
917
|
+
|
|
918
|
+
Parameters
|
|
919
|
+
----------
|
|
920
|
+
what : str, optional
|
|
921
|
+
Type of scene display to generate. The default is ``"image"``.
|
|
922
|
+
Options are:
|
|
923
|
+
|
|
924
|
+
* ``image``: Simple rendered PNG image
|
|
925
|
+
* ``deep_pixel``: EnSight deep pixel image
|
|
926
|
+
* ``animation``: MPEG4 movie
|
|
927
|
+
* ``webgl``: Interactive WebGL-based browser viewer
|
|
928
|
+
* ``remote``: Remote rendering-based interactive EnSight viewer
|
|
929
|
+
* ``remote_scene``: Remote rendering-based interactive EnSight viewer
|
|
930
|
+
|
|
931
|
+
width : int, optional
|
|
932
|
+
Width of the rendered entity. The default is ``None``.
|
|
933
|
+
height : int, optional
|
|
934
|
+
Height of the rendered entity. The default is ``None``.
|
|
935
|
+
temporal : bool, optional
|
|
936
|
+
Whether to include all timesteps in WebGL views. The default is ``False``.
|
|
937
|
+
aa : int, optional
|
|
938
|
+
Number of antialiasing passes to use when rendering images. The
|
|
939
|
+
default is ``4``.
|
|
940
|
+
fps : float, optional
|
|
941
|
+
Number of frames per second to use for animation playback. The default
|
|
942
|
+
is ``30``.
|
|
943
|
+
num_frames : int, optional
|
|
944
|
+
Number of frames of static timestep to record for animation playback.
|
|
945
|
+
|
|
946
|
+
Returns
|
|
947
|
+
-------
|
|
948
|
+
renderable.Renderable
|
|
949
|
+
|
|
950
|
+
Raises
|
|
951
|
+
------
|
|
952
|
+
RuntimeError
|
|
953
|
+
If it is not possible to generate the content.
|
|
954
|
+
|
|
955
|
+
Examples
|
|
956
|
+
--------
|
|
957
|
+
Render an image and display it in a browser. Rotate the scene and update the display.
|
|
958
|
+
|
|
959
|
+
>>> image = session.show('image', width=800, height=600)
|
|
960
|
+
>>> image.browser()
|
|
961
|
+
>>> session.ensight.view_transf.rotate(30, 30, 0)
|
|
962
|
+
>>> image.update()
|
|
963
|
+
>>> image.browser()
|
|
964
|
+
|
|
965
|
+
"""
|
|
966
|
+
self._establish_connection()
|
|
967
|
+
if self._html_port is None:
|
|
968
|
+
raise RuntimeError("No websocketserver has been associated with this Session")
|
|
969
|
+
|
|
970
|
+
kwargs: Dict[str, Union[float, int, None, str]] = dict(
|
|
971
|
+
height=height, width=width, temporal=temporal, aa=aa, fps=fps, num_frames=num_frames
|
|
972
|
+
)
|
|
973
|
+
if self._jupyter_notebook: # pragma: no cover
|
|
974
|
+
from IPython.display import display
|
|
975
|
+
|
|
976
|
+
# get the cell DisplayHandle instance
|
|
977
|
+
kwargs["cell_handle"] = display("", display_id=True)
|
|
978
|
+
|
|
979
|
+
render = None
|
|
980
|
+
if what == "image":
|
|
981
|
+
render = RenderableImage(self, **kwargs)
|
|
982
|
+
elif what == "deep_pixel":
|
|
983
|
+
render = RenderableDeepPixel(self, **kwargs)
|
|
984
|
+
elif what == "animation":
|
|
985
|
+
render = RenderableMP4(self, **kwargs)
|
|
986
|
+
elif what == "webgl":
|
|
987
|
+
render = RenderableWebGL(self, **kwargs)
|
|
988
|
+
elif what == "sgeo":
|
|
989
|
+
# the SGEO protocol is only supported in 2023 R1 and higher
|
|
990
|
+
if int(self._cei_suffix) < 231:
|
|
991
|
+
# Use the AVZ viewer in older versions of EnSight
|
|
992
|
+
render = RenderableWebGL(self, **kwargs)
|
|
993
|
+
else:
|
|
994
|
+
render = RenderableSGEO(self, **kwargs)
|
|
995
|
+
elif what == "remote":
|
|
996
|
+
kwargs["ui"] = ui
|
|
997
|
+
render = RenderableVNC(self, **kwargs)
|
|
998
|
+
elif what == "remote_scene":
|
|
999
|
+
render = RenderableEVSN(self, **kwargs)
|
|
1000
|
+
# Undocumented. Available only internally
|
|
1001
|
+
elif what == "webensight":
|
|
1002
|
+
render = RenderableVNCAngular(self, **kwargs)
|
|
1003
|
+
elif what == "webui":
|
|
1004
|
+
render = RenderableFluidsWebUI(self, **kwargs)
|
|
1005
|
+
|
|
1006
|
+
if render is None:
|
|
1007
|
+
raise RuntimeError("Unable to generate requested visualization")
|
|
1008
|
+
|
|
1009
|
+
return render
|
|
1010
|
+
|
|
1011
|
+
def cmd(self, value: str, do_eval: bool = True) -> Any:
|
|
1012
|
+
"""Run a command in EnSight and return the results.
|
|
1013
|
+
|
|
1014
|
+
Parameters
|
|
1015
|
+
----------
|
|
1016
|
+
value : str
|
|
1017
|
+
String of the command to run
|
|
1018
|
+
do_eval : bool, optional
|
|
1019
|
+
Whether to perform an evaluation. The default is ``True``.
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
Returns
|
|
1023
|
+
-------
|
|
1024
|
+
result
|
|
1025
|
+
Result of the string being executed as Python inside EnSight.
|
|
1026
|
+
|
|
1027
|
+
Examples
|
|
1028
|
+
--------
|
|
1029
|
+
|
|
1030
|
+
>>> print(session.cmd("10+4"))
|
|
1031
|
+
14
|
|
1032
|
+
"""
|
|
1033
|
+
if self._dsg_session:
|
|
1034
|
+
self._dsg_session._pyensight_grpc_coming = True
|
|
1035
|
+
self._establish_connection()
|
|
1036
|
+
ret = self._grpc.command(value, do_eval=do_eval)
|
|
1037
|
+
if self._dsg_session:
|
|
1038
|
+
self._dsg_session._pyensight_grpc_coming = False
|
|
1039
|
+
if do_eval:
|
|
1040
|
+
ret = self._convert_ctor(ret)
|
|
1041
|
+
value = eval(ret, dict(session=self, ensobjlist=ensobjlist))
|
|
1042
|
+
return value
|
|
1043
|
+
return ret
|
|
1044
|
+
|
|
1045
|
+
def geometry(self, what: str = "glb") -> bytes:
|
|
1046
|
+
"""Return the current EnSight scene as a geometry file.
|
|
1047
|
+
|
|
1048
|
+
Parameters
|
|
1049
|
+
----------
|
|
1050
|
+
what : str, optional
|
|
1051
|
+
File format to return. The default is ``"glb"``.
|
|
1052
|
+
|
|
1053
|
+
Returns
|
|
1054
|
+
-------
|
|
1055
|
+
obj
|
|
1056
|
+
Generated geometry file as a bytes object.
|
|
1057
|
+
|
|
1058
|
+
Examples
|
|
1059
|
+
--------
|
|
1060
|
+
>>> data = session.geometry()
|
|
1061
|
+
>>> with open("file.glb", "wb") as fp:
|
|
1062
|
+
>>> fp.write(data)
|
|
1063
|
+
|
|
1064
|
+
"""
|
|
1065
|
+
self._establish_connection()
|
|
1066
|
+
return self._grpc.geometry()
|
|
1067
|
+
|
|
1068
|
+
def render(self, width: int, height: int, aa: int = 1) -> bytes:
|
|
1069
|
+
"""Render the current EnSight scene and return a PNG image.
|
|
1070
|
+
|
|
1071
|
+
Parameters
|
|
1072
|
+
----------
|
|
1073
|
+
width : int
|
|
1074
|
+
Width of the rendered image in pixels.
|
|
1075
|
+
height : int
|
|
1076
|
+
Height of the rendered image in pixels.
|
|
1077
|
+
aa : int, optional
|
|
1078
|
+
Number of antialiasing passes to use. The default is ``1``.
|
|
1079
|
+
|
|
1080
|
+
Returns
|
|
1081
|
+
-------
|
|
1082
|
+
obj
|
|
1083
|
+
PNG image as a bytes object.
|
|
1084
|
+
|
|
1085
|
+
Examples
|
|
1086
|
+
--------
|
|
1087
|
+
>>> data = session.render(1920, 1080, aa=4)
|
|
1088
|
+
>>> with open("file.png", "wb") as fp:
|
|
1089
|
+
>>> fp.write(data)
|
|
1090
|
+
|
|
1091
|
+
"""
|
|
1092
|
+
self._establish_connection()
|
|
1093
|
+
return self._grpc.render(width=width, height=height, aa=aa)
|
|
1094
|
+
|
|
1095
|
+
def _release_remote_objects(self, object_id: Optional[int] = None):
|
|
1096
|
+
"""
|
|
1097
|
+
Send a command to the remote EnSight session to drop a specific object
|
|
1098
|
+
or all objects from the remote object cache.
|
|
1099
|
+
|
|
1100
|
+
Parameters
|
|
1101
|
+
----------
|
|
1102
|
+
object_id: int, optional
|
|
1103
|
+
The specific object to drop from the cache. If no objects are specified,
|
|
1104
|
+
then all remote objects associated with this session will be dropped.
|
|
1105
|
+
|
|
1106
|
+
"""
|
|
1107
|
+
obj_str = ""
|
|
1108
|
+
if object_id: # pragma: no cover
|
|
1109
|
+
obj_str = f", id={object_id}" # pragma: no cover
|
|
1110
|
+
cmd = f"ensight.objs.release_id('{self.name}'{obj_str})"
|
|
1111
|
+
_ = self.cmd(cmd, do_eval=False)
|
|
1112
|
+
|
|
1113
|
+
def close(self) -> None:
|
|
1114
|
+
"""Close the session.
|
|
1115
|
+
|
|
1116
|
+
Close the current session and its gRPC connection.
|
|
1117
|
+
"""
|
|
1118
|
+
# if version 242 or higher, free any objects we have cached there
|
|
1119
|
+
if not self._already_closed:
|
|
1120
|
+
if self.cei_suffix >= "242":
|
|
1121
|
+
try:
|
|
1122
|
+
self._release_remote_objects()
|
|
1123
|
+
except RuntimeError: # pragma: no cover
|
|
1124
|
+
# handle some intermediate EnSight builds.
|
|
1125
|
+
pass
|
|
1126
|
+
except IOError: # pragma: no cover
|
|
1127
|
+
# The session might already have been closed via another
|
|
1128
|
+
# session object. If grpc is inactive, there's no sense
|
|
1129
|
+
# in raising an exception since we are closing it anyway
|
|
1130
|
+
pass
|
|
1131
|
+
if self._launcher and self._halt_ensight_on_close:
|
|
1132
|
+
self._launcher.close(self)
|
|
1133
|
+
else:
|
|
1134
|
+
# lightweight shtudown, just close the gRC connection
|
|
1135
|
+
self._grpc.shutdown(stop_ensight=False)
|
|
1136
|
+
self._already_closed = True
|
|
1137
|
+
self._launcher = None
|
|
1138
|
+
|
|
1139
|
+
def _build_utils_interface(self) -> None:
|
|
1140
|
+
"""Build the ``ensight.utils`` interface.
|
|
1141
|
+
|
|
1142
|
+
This method Walk the PY files in the ``utils`` directory, creating instances
|
|
1143
|
+
of the classes in those files and placing them in the
|
|
1144
|
+
``Session.ensight.utils`` namespace.
|
|
1145
|
+
"""
|
|
1146
|
+
self._ensight.utils = types.SimpleNamespace()
|
|
1147
|
+
_utils_dir = os.path.join(os.path.dirname(__file__), "utils")
|
|
1148
|
+
if _utils_dir not in sys.path:
|
|
1149
|
+
sys.path.insert(0, _utils_dir)
|
|
1150
|
+
onlyfiles = [f for f in listdir(_utils_dir) if os.path.isfile(os.path.join(_utils_dir, f))]
|
|
1151
|
+
for _basename in onlyfiles:
|
|
1152
|
+
# skip over any files with the "_server" in their names
|
|
1153
|
+
if "_server" in _basename or "_cli" in _basename:
|
|
1154
|
+
continue
|
|
1155
|
+
_filename = os.path.join(_utils_dir, _basename)
|
|
1156
|
+
try:
|
|
1157
|
+
# get the module and class names
|
|
1158
|
+
_name = os.path.splitext(os.path.basename(_filename))[0]
|
|
1159
|
+
if _name == "__init__":
|
|
1160
|
+
continue
|
|
1161
|
+
_cap_name = _name[0].upper() + _name[1:]
|
|
1162
|
+
# import the module
|
|
1163
|
+
spec = importlib.util.spec_from_file_location(
|
|
1164
|
+
f"ansys.pyensight.core.utils.{_name}", _filename
|
|
1165
|
+
)
|
|
1166
|
+
if spec: # pragma: no cover
|
|
1167
|
+
_module = importlib.util.module_from_spec(spec)
|
|
1168
|
+
if spec.loader: # pragma: no cover
|
|
1169
|
+
spec.loader.exec_module(_module)
|
|
1170
|
+
# get the class from the module (query.py filename -> Query() object)
|
|
1171
|
+
_the_class = getattr(_module, _cap_name)
|
|
1172
|
+
# Create an instance, using ensight as the EnSight interface
|
|
1173
|
+
# and place it in this module.
|
|
1174
|
+
setattr(self._ensight.utils, _name, _the_class(self._ensight))
|
|
1175
|
+
except Exception as e: # pragma: no cover
|
|
1176
|
+
# Warn on import errors
|
|
1177
|
+
print(f"Error loading ensight.utils from: '{_filename}' : {e}")
|
|
1178
|
+
|
|
1179
|
+
MONITOR_NEW_TIMESTEPS_OFF = "off"
|
|
1180
|
+
MONITOR_NEW_TIMESTEPS_STAY_AT_CURRENT = "stay_at_current"
|
|
1181
|
+
MONITOR_NEW_TIMESTEPS_JUMP_TO_END = "jump_to_end"
|
|
1182
|
+
|
|
1183
|
+
def load_data(
|
|
1184
|
+
self,
|
|
1185
|
+
data_file: str,
|
|
1186
|
+
result_file: Optional[str] = None,
|
|
1187
|
+
file_format: Optional[str] = None,
|
|
1188
|
+
reader_options: Optional[dict] = None,
|
|
1189
|
+
new_case: bool = False,
|
|
1190
|
+
representation: str = "3D_feature_2D_full",
|
|
1191
|
+
monitor_new_timesteps: str = MONITOR_NEW_TIMESTEPS_OFF,
|
|
1192
|
+
) -> None:
|
|
1193
|
+
"""Load a dataset into the EnSight instance.
|
|
1194
|
+
|
|
1195
|
+
Load the data from a given file into EnSight. The new data
|
|
1196
|
+
replaces any currently loaded data in the session.
|
|
1197
|
+
|
|
1198
|
+
Parameters
|
|
1199
|
+
----------
|
|
1200
|
+
data_file : str
|
|
1201
|
+
Name of the data file to load.
|
|
1202
|
+
result_file : str, optional
|
|
1203
|
+
Name of the second data file for dual-file datasets.
|
|
1204
|
+
file_format : str, optional
|
|
1205
|
+
Name of the EnSight reader to use. The default is ``None``,
|
|
1206
|
+
in which case EnSight selects a reader.
|
|
1207
|
+
reader_options : dict, optional
|
|
1208
|
+
Dictionary of reader-specific option-value pairs that can be used
|
|
1209
|
+
to customize the reader behavior. The default is ``None``.
|
|
1210
|
+
new_case : bool, optional
|
|
1211
|
+
Whether to load the dataset in another case. The default is ``False``,
|
|
1212
|
+
in which case the dataset replaces the one (if any) loaded in the existing
|
|
1213
|
+
current case.
|
|
1214
|
+
representation : str, optional
|
|
1215
|
+
Default representation for the parts loaded. The default is
|
|
1216
|
+
``"3D_feature_2D_full"``.
|
|
1217
|
+
monitor_new_timesteps: str, optional
|
|
1218
|
+
Defaulted to off, if changed EnSight will monitor for new timesteps.
|
|
1219
|
+
The allowed values are MONITOR_NEW_TIMESTEPS_OFF, MONITOR_NEW_TIMESTEPS_STAY_AT_CURRENT
|
|
1220
|
+
and MONITOR_NEW_TIMESTEPS_JUMP_TO_END
|
|
1221
|
+
|
|
1222
|
+
Raises
|
|
1223
|
+
------
|
|
1224
|
+
RuntimeError
|
|
1225
|
+
If EnSight cannot guess the file format or an error occurs while the
|
|
1226
|
+
data is being read.
|
|
1227
|
+
|
|
1228
|
+
Examples
|
|
1229
|
+
--------
|
|
1230
|
+
>>> from ansys.pyensight.core import LocalLauncher
|
|
1231
|
+
>>> session = LocalLauncher().start()
|
|
1232
|
+
>>> session.load_data('D:\\data\\CFX\\example_data.res')
|
|
1233
|
+
|
|
1234
|
+
"""
|
|
1235
|
+
self._establish_connection()
|
|
1236
|
+
# what application are we talking to?
|
|
1237
|
+
target = self.cmd("ensight.version('product').lower()")
|
|
1238
|
+
if target == "envision":
|
|
1239
|
+
cmd = f'ensight.data.replace(r"""{data_file}""")'
|
|
1240
|
+
if self.cmd(cmd) != 0:
|
|
1241
|
+
raise RuntimeError("Unable to load the dataset.")
|
|
1242
|
+
return
|
|
1243
|
+
|
|
1244
|
+
# Handle case changes...
|
|
1245
|
+
cmds = [
|
|
1246
|
+
'ensight.case.link_modelparts_byname("OFF")',
|
|
1247
|
+
'ensight.case.create_viewport("OFF")',
|
|
1248
|
+
'ensight.case.apply_context("OFF")',
|
|
1249
|
+
"ensight.case.reflect_model_in(\"'none'\")",
|
|
1250
|
+
]
|
|
1251
|
+
for cmd in cmds:
|
|
1252
|
+
self.cmd(cmd, do_eval=False)
|
|
1253
|
+
|
|
1254
|
+
if new_case:
|
|
1255
|
+
# New case
|
|
1256
|
+
new_case_name = None
|
|
1257
|
+
for case in self.ensight.objs.core.CASES:
|
|
1258
|
+
if case.ACTIVE == 0:
|
|
1259
|
+
new_case_name = case.DESCRIPTION
|
|
1260
|
+
break
|
|
1261
|
+
if new_case_name is None:
|
|
1262
|
+
raise RuntimeError("No cases available for adding.")
|
|
1263
|
+
cmd = f'ensight.case.add("{new_case_name}")'
|
|
1264
|
+
self.cmd(cmd, do_eval=False)
|
|
1265
|
+
cmd = f'ensight.case.select("{new_case_name}")'
|
|
1266
|
+
self.cmd(cmd, do_eval=False)
|
|
1267
|
+
else:
|
|
1268
|
+
# Case replace
|
|
1269
|
+
current_case_name = self.ensight.objs.core.CURRENTCASE[0].DESCRIPTION
|
|
1270
|
+
cmd = f'ensight.case.replace("{current_case_name}", "{current_case_name}")'
|
|
1271
|
+
self.cmd(cmd, do_eval=False)
|
|
1272
|
+
cmd = f'ensight.case.select("{current_case_name}")'
|
|
1273
|
+
self.cmd(cmd, do_eval=False)
|
|
1274
|
+
|
|
1275
|
+
# Attempt to find the file format if none is specified
|
|
1276
|
+
if file_format is None:
|
|
1277
|
+
try:
|
|
1278
|
+
cmd = "ensight.objs.core.CURRENTCASE[0]"
|
|
1279
|
+
cmd += f'.queryfileformat(r"""{data_file}""")["reader"]'
|
|
1280
|
+
file_format = self.cmd(cmd)
|
|
1281
|
+
except RuntimeError:
|
|
1282
|
+
raise RuntimeError(f"Unable to determine file format for {data_file}")
|
|
1283
|
+
|
|
1284
|
+
# Load the data
|
|
1285
|
+
cmds = [
|
|
1286
|
+
"ensight.part.select_default()",
|
|
1287
|
+
"ensight.part.modify_begin()",
|
|
1288
|
+
f'ensight.part.elt_representation("{representation}")',
|
|
1289
|
+
"ensight.part.modify_end()",
|
|
1290
|
+
'ensight.data.binary_files_are("native")',
|
|
1291
|
+
f'ensight.data.format("{file_format}")',
|
|
1292
|
+
]
|
|
1293
|
+
if reader_options:
|
|
1294
|
+
for key, value in reader_options.items():
|
|
1295
|
+
option = f"""ensight.data.reader_option("{repr(key)} {repr(value)}")"""
|
|
1296
|
+
cmds.append(option)
|
|
1297
|
+
if result_file:
|
|
1298
|
+
cmds.append(f'ensight.data.result(r"""{result_file}""")')
|
|
1299
|
+
cmds.append("ensight.data.shift_time(1.000000, 0.000000, 0.000000)")
|
|
1300
|
+
cmds.append(f'ensight.solution_time.monitor_for_new_steps("{monitor_new_timesteps}")')
|
|
1301
|
+
cmds.append(f'ensight.data.replace(r"""{data_file}""")')
|
|
1302
|
+
for cmd in cmds:
|
|
1303
|
+
if self.cmd(cmd) != 0:
|
|
1304
|
+
raise RuntimeError("Unable to load the dataset.")
|
|
1305
|
+
|
|
1306
|
+
def download_pyansys_example(
|
|
1307
|
+
self,
|
|
1308
|
+
filename: str,
|
|
1309
|
+
directory: Optional[str] = None,
|
|
1310
|
+
root: Optional[str] = None,
|
|
1311
|
+
folder: Optional[bool] = None,
|
|
1312
|
+
) -> str:
|
|
1313
|
+
"""Download an example dataset from the ansys/example-data repository.
|
|
1314
|
+
The dataset is downloaded local to the EnSight server location, so that it can
|
|
1315
|
+
be downloaded even if running from a container.
|
|
1316
|
+
|
|
1317
|
+
Parameters
|
|
1318
|
+
----------
|
|
1319
|
+
filename: str
|
|
1320
|
+
The filename to download
|
|
1321
|
+
directory: str
|
|
1322
|
+
The directory to download the filename from
|
|
1323
|
+
root: str
|
|
1324
|
+
If set, the download will happen from another location
|
|
1325
|
+
folder: bool
|
|
1326
|
+
If set to True, it marks the filename to be a directory rather
|
|
1327
|
+
than a single file
|
|
1328
|
+
|
|
1329
|
+
Returns
|
|
1330
|
+
-------
|
|
1331
|
+
pathname: str
|
|
1332
|
+
The download location, local to the EnSight server directory.
|
|
1333
|
+
If folder is set to True, the download location will be a folder containing
|
|
1334
|
+
all the items available in the repository location under that folder.
|
|
1335
|
+
|
|
1336
|
+
Examples
|
|
1337
|
+
--------
|
|
1338
|
+
>>> from ansys.pyensight.core import DockerLauncher
|
|
1339
|
+
>>> session = DockerLauncher().start(data_directory="D:\\")
|
|
1340
|
+
>>> cas_file = session.download_pyansys_example("mixing_elbow.cas.h5","pyfluent/mixing_elbow")
|
|
1341
|
+
>>> dat_file = session.download_pyansys_example("mixing_elbow.dat.h5","pyfluent/mixing_elbow")
|
|
1342
|
+
>>> session.load_data(cas_file, result_file=dat_file)
|
|
1343
|
+
>>> remote = session.show("remote")
|
|
1344
|
+
>>> remote.browser()
|
|
1345
|
+
"""
|
|
1346
|
+
base_uri = "https://api.github.com/repos/ansys/example-data/contents"
|
|
1347
|
+
override_root = False
|
|
1348
|
+
if not folder:
|
|
1349
|
+
if root is not None:
|
|
1350
|
+
base_uri = root
|
|
1351
|
+
override_root = True
|
|
1352
|
+
uri = f"{base_uri}/{filename}"
|
|
1353
|
+
if directory:
|
|
1354
|
+
uri = f"{base_uri}/{directory}/{filename}"
|
|
1355
|
+
pathname = f"{self.launcher.session_directory}/{filename}"
|
|
1356
|
+
if not folder:
|
|
1357
|
+
if override_root:
|
|
1358
|
+
correct_url = uri
|
|
1359
|
+
else:
|
|
1360
|
+
correct_url = None
|
|
1361
|
+
with requests.get(uri) as r:
|
|
1362
|
+
data = r.json()
|
|
1363
|
+
correct_url = data["download_url"]
|
|
1364
|
+
if not correct_url:
|
|
1365
|
+
raise RuntimeError(f"Couldn't retrieve download URL from github uri {uri}")
|
|
1366
|
+
script = "import requests\n"
|
|
1367
|
+
script += "import shutil\n"
|
|
1368
|
+
script += "import os\n"
|
|
1369
|
+
script += f'url = "{correct_url}"\n'
|
|
1370
|
+
script += f'outpath = r"""{pathname}"""\n'
|
|
1371
|
+
script += "with requests.get(url, stream=True) as r:\n"
|
|
1372
|
+
script += " with open(outpath, 'wb') as f:\n"
|
|
1373
|
+
script += " shutil.copyfileobj(r.raw, f)\n"
|
|
1374
|
+
self.cmd(script, do_eval=False)
|
|
1375
|
+
else:
|
|
1376
|
+
script = "import requests\n"
|
|
1377
|
+
script += "import shutil\n"
|
|
1378
|
+
script += "import os\n"
|
|
1379
|
+
script += f'url = "{uri}"\n'
|
|
1380
|
+
script += "with requests.get(url) as r:\n"
|
|
1381
|
+
script += " data = r.json()\n"
|
|
1382
|
+
script += f' output_directory = r"""{pathname}"""\n'
|
|
1383
|
+
script += " os.makedirs(output_directory, exist_ok=True)\n"
|
|
1384
|
+
script += " for item in data:\n"
|
|
1385
|
+
script += " if item['type'] == 'file':\n"
|
|
1386
|
+
script += " file_url = item['download_url']\n"
|
|
1387
|
+
script += " filename = os.path.join(output_directory, item['name'])\n"
|
|
1388
|
+
script += " r = requests.get(file_url, stream=True)\n"
|
|
1389
|
+
script += " with open(filename, 'wb') as f:\n"
|
|
1390
|
+
script += " f.write(r.content)\n"
|
|
1391
|
+
self.cmd(script, do_eval=False)
|
|
1392
|
+
return pathname
|
|
1393
|
+
|
|
1394
|
+
def load_example(
|
|
1395
|
+
self, example_name: str, uncompress: bool = False, root: Optional[str] = None
|
|
1396
|
+
) -> str:
|
|
1397
|
+
"""Load an example dataset.
|
|
1398
|
+
|
|
1399
|
+
This method downloads an EnSight session file from a known location and loads
|
|
1400
|
+
it into the current EnSight instance. The URL for the dataset is formed by
|
|
1401
|
+
combining the value given for the ``example_name`` parameter with a root URL.
|
|
1402
|
+
The default base URL is provided by Ansys, but it can be overridden by specifying
|
|
1403
|
+
a value for the ``root`` parameter.
|
|
1404
|
+
|
|
1405
|
+
Parameters
|
|
1406
|
+
----------
|
|
1407
|
+
example_name : str
|
|
1408
|
+
Name of the EnSight session file (``.ens``) to download and load.
|
|
1409
|
+
uncompress : bool, optional
|
|
1410
|
+
Whether to unzip the downloaded file into the returned directory name.
|
|
1411
|
+
The default is ``False``.
|
|
1412
|
+
root : str, optional
|
|
1413
|
+
Base URL for the download.
|
|
1414
|
+
|
|
1415
|
+
Returns
|
|
1416
|
+
-------
|
|
1417
|
+
str
|
|
1418
|
+
Path to the downloaded file in the EnSight session.
|
|
1419
|
+
|
|
1420
|
+
Examples
|
|
1421
|
+
--------
|
|
1422
|
+
>>> from ansys.pyensight.core import LocalLauncher
|
|
1423
|
+
>>> session = LocalLauncher().start()
|
|
1424
|
+
>>> session.load_example("fluent_wing_example.ens")
|
|
1425
|
+
>>> remote = session.show("remote")
|
|
1426
|
+
>>> remote.browser()
|
|
1427
|
+
|
|
1428
|
+
"""
|
|
1429
|
+
base_uri = "https://s3.amazonaws.com/www3.ensight.com/PyEnSight/ExampleData"
|
|
1430
|
+
if root is not None: # pragma: no cover
|
|
1431
|
+
base_uri = root # pragma: no cover
|
|
1432
|
+
pathname = self.download_pyansys_example(example_name, root=base_uri)
|
|
1433
|
+
script = f'outpath = r"""{pathname}"""\n'
|
|
1434
|
+
if uncompress:
|
|
1435
|
+
# in this case, remove the extension and unzip the file
|
|
1436
|
+
pathname_dir = os.path.splitext(pathname)[0]
|
|
1437
|
+
script += "outpath_dir = os.path.splitext(outpath)[0]\n"
|
|
1438
|
+
script += "os.mkdir(outpath_dir)\n"
|
|
1439
|
+
script += "shutil.unpack_archive(outpath, outpath_dir, 'zip')\n"
|
|
1440
|
+
# return the directory name
|
|
1441
|
+
pathname = pathname_dir
|
|
1442
|
+
else:
|
|
1443
|
+
script += "ensight.objs.ensxml_restore_file(outpath)\n"
|
|
1444
|
+
self.cmd(script, do_eval=False)
|
|
1445
|
+
return pathname
|
|
1446
|
+
|
|
1447
|
+
def add_callback(
|
|
1448
|
+
self, target: Any, tag: str, attr_list: list, method: Callable, compress: bool = True
|
|
1449
|
+
) -> None:
|
|
1450
|
+
"""Register a callback with an event tuple.
|
|
1451
|
+
|
|
1452
|
+
For a given target object (such as ``"ensight.objs.core"``) and a list
|
|
1453
|
+
of attributes (such as ``["PARTS", "VARIABLES"]``), this method sets up a
|
|
1454
|
+
callback to be made when any of those attribute change on the target object.
|
|
1455
|
+
The target can also be an EnSight (not PyEnSight) class name, for example
|
|
1456
|
+
"ENS_PART". In this latter form, all objects of that type are watched for
|
|
1457
|
+
specified attribute changes.
|
|
1458
|
+
|
|
1459
|
+
The callback is made with a single argument, a string encoded in URL format
|
|
1460
|
+
with the supplied tag, the name of the attribute that changed and the UID
|
|
1461
|
+
of the object that changed. The string passed to the callback is in this form:
|
|
1462
|
+
``grpc://{sessionguid}/{tag}?enum={attribute}&uid={objectid}``.
|
|
1463
|
+
|
|
1464
|
+
Only one callback with the noted tag can be used in the session.
|
|
1465
|
+
|
|
1466
|
+
Parameters
|
|
1467
|
+
----------
|
|
1468
|
+
target : obj, str
|
|
1469
|
+
Name of the target object or name of a class as a string to
|
|
1470
|
+
match all objects of that class. A proxy class reference is
|
|
1471
|
+
also allowed. For example, ``session.ensight.objs.core``.
|
|
1472
|
+
tag : str
|
|
1473
|
+
Unique name for the callback. A tag can end with macros of
|
|
1474
|
+
the form ``{{attrname}}`` to return the value of an attribute of the
|
|
1475
|
+
target object. The macros should take the form of URI queries to
|
|
1476
|
+
simplify parsing.
|
|
1477
|
+
attr_list : list
|
|
1478
|
+
List of attributes of the target that are to result in the callback
|
|
1479
|
+
being called if changed.
|
|
1480
|
+
method : Callable
|
|
1481
|
+
Callable that is called with the returned URL.
|
|
1482
|
+
compress : bool, optional
|
|
1483
|
+
Whether to call only the last event if a repeated event is generated
|
|
1484
|
+
as a result of an action. The default is ``True``. If ``False``, every
|
|
1485
|
+
event results in a callback.
|
|
1486
|
+
|
|
1487
|
+
Examples
|
|
1488
|
+
--------
|
|
1489
|
+
A string similar to this is printed when the dataset is loaded and the part list
|
|
1490
|
+
changes:
|
|
1491
|
+
|
|
1492
|
+
`` Event : grpc://f6f74dae-f0ed-11ec-aa58-381428170733/partlist?enum=PARTS&uid=221``
|
|
1493
|
+
|
|
1494
|
+
>>> from ansys.pyensight.core import LocalLauncher
|
|
1495
|
+
>>> s = LocalLauncher().start()
|
|
1496
|
+
>>> def cb(v: str):
|
|
1497
|
+
>>> print("Event:", v)
|
|
1498
|
+
>>> s.add_callback("ensight.objs.core", "partlist", ["PARTS"], cb)
|
|
1499
|
+
>>> s.load_data("D:\\ANSYSDev\\data\\CFX\\HeatingCoil_001.res")
|
|
1500
|
+
"""
|
|
1501
|
+
self._establish_connection()
|
|
1502
|
+
# shorten the tag up to the query block. Macros are only legal in the query block.
|
|
1503
|
+
try:
|
|
1504
|
+
idx = tag.index("?")
|
|
1505
|
+
short_tag = tag[:idx]
|
|
1506
|
+
except ValueError:
|
|
1507
|
+
short_tag = tag
|
|
1508
|
+
if short_tag in self._callbacks:
|
|
1509
|
+
raise RuntimeError(f"A callback for tag '{short_tag}' already exists")
|
|
1510
|
+
# Build the addcallback string against the full tag
|
|
1511
|
+
flags = ""
|
|
1512
|
+
if compress:
|
|
1513
|
+
flags = ",flags=ensight.objs.EVENTMAP_FLAG_COMP_GLOBAL"
|
|
1514
|
+
if hasattr(target, "__OBJID__"):
|
|
1515
|
+
target = self.remote_obj(target.__OBJID__)
|
|
1516
|
+
cmd = f"ensight.objs.addcallback({target},None,"
|
|
1517
|
+
cmd += f"'{self._grpc.prefix()}{tag}',attrs={repr(attr_list)}{flags})"
|
|
1518
|
+
callback_id = self.cmd(cmd)
|
|
1519
|
+
# if this is the first callback, start the event stream
|
|
1520
|
+
if len(self._callbacks) == 0:
|
|
1521
|
+
self._grpc.event_stream_enable(callback=self._event_callback)
|
|
1522
|
+
# record the callback id along with the callback
|
|
1523
|
+
# if the callback URL starts with the short_tag, we make the callback
|
|
1524
|
+
self._callbacks[short_tag] = (callback_id, method)
|
|
1525
|
+
|
|
1526
|
+
def remove_callback(self, tag: str) -> None:
|
|
1527
|
+
"""Remove a callback that the :func`add_callback<ansys.pyensight.core.Session.add_callback>`
|
|
1528
|
+
method started.
|
|
1529
|
+
|
|
1530
|
+
Given a tag used to register a previous callback (``add_callback()``), remove
|
|
1531
|
+
this callback from the EnSight callback system.
|
|
1532
|
+
|
|
1533
|
+
Parameters
|
|
1534
|
+
----------
|
|
1535
|
+
tag : str
|
|
1536
|
+
Callback string tag.
|
|
1537
|
+
|
|
1538
|
+
Raises
|
|
1539
|
+
------
|
|
1540
|
+
RuntimeError
|
|
1541
|
+
If an invalid tag is supplied.
|
|
1542
|
+
|
|
1543
|
+
"""
|
|
1544
|
+
if tag not in self._callbacks:
|
|
1545
|
+
raise RuntimeError(f"A callback for tag '{tag}' does not exist")
|
|
1546
|
+
callback_id = self._callbacks[tag][0]
|
|
1547
|
+
del self._callbacks[tag]
|
|
1548
|
+
cmd = f"ensight.objs.removecallback({callback_id})"
|
|
1549
|
+
_ = self.cmd(cmd, do_eval=False)
|
|
1550
|
+
|
|
1551
|
+
def _event_callback(self, cmd: str) -> None:
|
|
1552
|
+
"""Pass the URL back to the registered callback.
|
|
1553
|
+
|
|
1554
|
+
This method matches the ``cmd`` URL with the registered callback and then
|
|
1555
|
+
makes the callback.
|
|
1556
|
+
|
|
1557
|
+
Parameters
|
|
1558
|
+
----------
|
|
1559
|
+
cmd : str
|
|
1560
|
+
URL callback from the gRPC event stream. The URL has this
|
|
1561
|
+
form: ``grpc://{sessionguid}/{tag}?enum={attribute}&uid={objectid}``.
|
|
1562
|
+
|
|
1563
|
+
"""
|
|
1564
|
+
# EnSight will always tack on '?enum='. If our tag uses ?macro={{attr}},
|
|
1565
|
+
# you will get too many '?' in the URL, making it difficult to parse.
|
|
1566
|
+
# So, we look for "?..." and a following "?enum=". If we see this, convert
|
|
1567
|
+
# "?enum=" into "&enum=".
|
|
1568
|
+
idx_question = cmd.find("?")
|
|
1569
|
+
idx_enum = cmd.find("?enum=")
|
|
1570
|
+
if idx_question < idx_enum:
|
|
1571
|
+
cmd = cmd.replace("?enum=", "&enum=")
|
|
1572
|
+
parse = urlparse(cmd)
|
|
1573
|
+
tag = parse.path[1:]
|
|
1574
|
+
for key, value in self._callbacks.items():
|
|
1575
|
+
# remember "key" is a shortened version of tag
|
|
1576
|
+
if tag.startswith(key):
|
|
1577
|
+
value[1](cmd)
|
|
1578
|
+
return
|
|
1579
|
+
print(f"Unhandled event: {cmd}")
|
|
1580
|
+
|
|
1581
|
+
# Object API helper functions
|
|
1582
|
+
@staticmethod
|
|
1583
|
+
def remote_obj(ensobjid: int) -> str:
|
|
1584
|
+
"""Generate a string that, for a given ``ENSOBJ`` object ID, returns
|
|
1585
|
+
a proxy object instance.
|
|
1586
|
+
|
|
1587
|
+
Parameters
|
|
1588
|
+
----------
|
|
1589
|
+
ensobjid: int
|
|
1590
|
+
ID of the ``ENSOBJ`` object.
|
|
1591
|
+
|
|
1592
|
+
Returns
|
|
1593
|
+
-------
|
|
1594
|
+
str
|
|
1595
|
+
String for the proxy object instance.
|
|
1596
|
+
"""
|
|
1597
|
+
return f"ensight.objs.wrap_id({ensobjid})"
|
|
1598
|
+
|
|
1599
|
+
def _prune_hash(self) -> None:
|
|
1600
|
+
"""Prune the ``ENSOBJ`` hash table.
|
|
1601
|
+
|
|
1602
|
+
The ``ENSOBJ`` hash table may need flushing if it gets too big. Do that here."""
|
|
1603
|
+
if len(self._ensobj_hash) > 1000000:
|
|
1604
|
+
self._ensobj_hash = {}
|
|
1605
|
+
|
|
1606
|
+
def add_ensobj_instance(self, obj: "ENSOBJ") -> None:
|
|
1607
|
+
"""Add a new ``ENSOBJ`` object instance to the hash table.
|
|
1608
|
+
|
|
1609
|
+
Parameters
|
|
1610
|
+
----------
|
|
1611
|
+
obj : ENSOBJ
|
|
1612
|
+
``ENSOBJ`` object instance.
|
|
1613
|
+
"""
|
|
1614
|
+
self._ensobj_hash[obj.__OBJID__] = obj
|
|
1615
|
+
|
|
1616
|
+
def obj_instance(self, ensobjid: int) -> Optional["ENSOBJ"]:
|
|
1617
|
+
"""Get any existing proxy object associated with an ID.
|
|
1618
|
+
|
|
1619
|
+
Parameters
|
|
1620
|
+
----------
|
|
1621
|
+
ensobjid: int
|
|
1622
|
+
ID of the ``ENSOBJ`` object.
|
|
1623
|
+
|
|
1624
|
+
"""
|
|
1625
|
+
return self._ensobj_hash.get(ensobjid, None)
|
|
1626
|
+
|
|
1627
|
+
def _obj_attr_subtype(self, classname: str) -> Tuple[Optional[int], Optional[dict]]:
|
|
1628
|
+
"""Get subtype information for a given class.
|
|
1629
|
+
|
|
1630
|
+
For an input class name, this method returns the proper Python proxy class name and,
|
|
1631
|
+
if the class supports subclasses, the attribute ID of the differentiating attribute.
|
|
1632
|
+
|
|
1633
|
+
Parameters
|
|
1634
|
+
----------
|
|
1635
|
+
classname : str
|
|
1636
|
+
Root class name to look up.
|
|
1637
|
+
|
|
1638
|
+
Returns
|
|
1639
|
+
-------
|
|
1640
|
+
Tuple[Optional[int], Optional[dict]]
|
|
1641
|
+
(attr_id, subclassnamedict): Attribute used to differentiate between classes
|
|
1642
|
+
and a dictionary of the class names for each value of the attribute.
|
|
1643
|
+
|
|
1644
|
+
"""
|
|
1645
|
+
if classname == "ENS_PART":
|
|
1646
|
+
return self.ensight.objs.enums.PARTTYPE, self._subtype_tables[classname]
|
|
1647
|
+
|
|
1648
|
+
elif classname == "ENS_ANNOT":
|
|
1649
|
+
return self.ensight.objs.enums.ANNOTTYPE, self._subtype_tables[classname]
|
|
1650
|
+
|
|
1651
|
+
elif classname == "ENS_TOOL":
|
|
1652
|
+
return self.ensight.objs.enums.TOOLTYPE, self._subtype_tables[classname]
|
|
1653
|
+
|
|
1654
|
+
return None, None
|
|
1655
|
+
|
|
1656
|
+
def _convert_ctor(self, s: str) -> str:
|
|
1657
|
+
"""Convert ENSOBJ object references into executable code in __repl__ strings.
|
|
1658
|
+
|
|
1659
|
+
The __repl__() implementation for an ENSOBJ subclass generates strings like these::
|
|
1660
|
+
|
|
1661
|
+
Class: ENS_GLOBALS, CvfObjID: 221, cached:yes
|
|
1662
|
+
Class: ENS_PART, desc: 'Sphere', CvfObjID: 1078, cached:no
|
|
1663
|
+
Class: ENS_PART, desc: 'engine', PartType: 0, CvfObjID: 1097, cached:no
|
|
1664
|
+
Class: ENS_GROUP, desc: '', Owned, CvfObjID: 1043, cached:no
|
|
1665
|
+
|
|
1666
|
+
This method detects strings like those and converts them into strings like these::
|
|
1667
|
+
|
|
1668
|
+
session.ensight.objs.ENS_GLOBALS(session, 221)
|
|
1669
|
+
session.ensight.objs.ENS_PART_MODEL(session, 1078, attr_id=1610612792, attr_value=0)
|
|
1670
|
+
|
|
1671
|
+
where:
|
|
1672
|
+
|
|
1673
|
+
1610612792 is ensight.objs.enums.PARTTYPE.
|
|
1674
|
+
|
|
1675
|
+
If a proxy object for the ID already exists, it can also generate strings like this::
|
|
1676
|
+
|
|
1677
|
+
session.obj_instance(221)
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
Parameters
|
|
1681
|
+
----------
|
|
1682
|
+
s : str
|
|
1683
|
+
String to convert.
|
|
1684
|
+
|
|
1685
|
+
"""
|
|
1686
|
+
self._prune_hash()
|
|
1687
|
+
offset = 0
|
|
1688
|
+
while True:
|
|
1689
|
+
# Find the object repl block to replace
|
|
1690
|
+
id = s.find("CvfObjID:", offset)
|
|
1691
|
+
if id == -1:
|
|
1692
|
+
break
|
|
1693
|
+
start = s.find("Class: ", offset)
|
|
1694
|
+
if (start == -1) or (start > id):
|
|
1695
|
+
break
|
|
1696
|
+
tail_len = 11
|
|
1697
|
+
tail = s.find(", cached:no", offset)
|
|
1698
|
+
if tail == -1:
|
|
1699
|
+
tail_len = 12
|
|
1700
|
+
tail = s.find(", cached:yes", offset)
|
|
1701
|
+
if tail == -1: # pragma: no cover
|
|
1702
|
+
break # pragma: no cover
|
|
1703
|
+
# just this object substring
|
|
1704
|
+
tmp = s[start + 7 : tail]
|
|
1705
|
+
# Subtype (PartType:, AnnotType:, ToolType:)
|
|
1706
|
+
subtype = None
|
|
1707
|
+
for name in ("PartType:", "AnnotType:", "ToolType:"):
|
|
1708
|
+
location = tmp.find(name)
|
|
1709
|
+
if location != -1:
|
|
1710
|
+
subtype = int(tmp[location + len(name) :].split(",")[0])
|
|
1711
|
+
break
|
|
1712
|
+
# Owned flag
|
|
1713
|
+
owned_flag = "Owned," in tmp
|
|
1714
|
+
# isolate the block to replace
|
|
1715
|
+
prefix = s[:start]
|
|
1716
|
+
suffix = s[tail + tail_len :]
|
|
1717
|
+
# parse out the object id and classname
|
|
1718
|
+
objid = int(s[id + 9 : tail])
|
|
1719
|
+
classname = s[start + 7 : tail]
|
|
1720
|
+
comma = classname.find(",")
|
|
1721
|
+
classname = classname[:comma]
|
|
1722
|
+
# pick the subclass based on the classname
|
|
1723
|
+
attr_id, classname_lookup = self._obj_attr_subtype(classname)
|
|
1724
|
+
# generate the replacement text
|
|
1725
|
+
if objid in self._ensobj_hash:
|
|
1726
|
+
replace_text = f"session.obj_instance({objid})"
|
|
1727
|
+
else:
|
|
1728
|
+
subclass_info = ""
|
|
1729
|
+
if attr_id is not None:
|
|
1730
|
+
if subtype is not None:
|
|
1731
|
+
# the 2024 R2 interface includes the subtype
|
|
1732
|
+
if (classname_lookup is not None) and (subtype in classname_lookup):
|
|
1733
|
+
classname = classname_lookup[subtype]
|
|
1734
|
+
subclass_info = f",attr_id={attr_id}, attr_value={subtype}"
|
|
1735
|
+
elif classname_lookup is not None: # pragma: no cover
|
|
1736
|
+
# if a "subclass" case and no subclass attrid value, ask for it...
|
|
1737
|
+
remote_name = self.remote_obj(objid)
|
|
1738
|
+
cmd = f"{remote_name}.getattr({attr_id})"
|
|
1739
|
+
attr_value = self.cmd(cmd)
|
|
1740
|
+
if attr_value in classname_lookup:
|
|
1741
|
+
classname = classname_lookup[attr_value]
|
|
1742
|
+
subclass_info = f",attr_id={attr_id}, attr_value={attr_value}"
|
|
1743
|
+
if owned_flag:
|
|
1744
|
+
subclass_info += ",owned=True"
|
|
1745
|
+
replace_text = f"session.ensight.objs.{classname}(session, {objid}{subclass_info})"
|
|
1746
|
+
if replace_text is None: # pragma: no cover
|
|
1747
|
+
break # pragma: no cover
|
|
1748
|
+
offset = start + len(replace_text)
|
|
1749
|
+
s = prefix + replace_text + suffix
|
|
1750
|
+
s = s.strip()
|
|
1751
|
+
if s.startswith("[") and s.endswith("]"):
|
|
1752
|
+
s = f"ensobjlist({s}, session=session)"
|
|
1753
|
+
return s
|
|
1754
|
+
|
|
1755
|
+
def capture_context(self, full_context: bool = False) -> "enscontext.EnsContext":
|
|
1756
|
+
"""Capture the current EnSight instance state.
|
|
1757
|
+
|
|
1758
|
+
This method causes the EnSight instance to save a context and return an ``EnsContext``
|
|
1759
|
+
object representing that saved state.
|
|
1760
|
+
|
|
1761
|
+
Parameters
|
|
1762
|
+
----------
|
|
1763
|
+
full_context : bool, optional
|
|
1764
|
+
Whether to include all aspects of the Ensight instance. The default is ``False``.
|
|
1765
|
+
|
|
1766
|
+
Returns
|
|
1767
|
+
-------
|
|
1768
|
+
enscontext.EnsContext
|
|
1769
|
+
|
|
1770
|
+
Examples
|
|
1771
|
+
--------
|
|
1772
|
+
>>> ctx = session.capture_context()
|
|
1773
|
+
>>> ctx.save("session_context.ctxz")
|
|
1774
|
+
|
|
1775
|
+
"""
|
|
1776
|
+
self.cmd("import ansys.pyensight.core.enscontext", do_eval=False)
|
|
1777
|
+
data_str = self.cmd(
|
|
1778
|
+
f"ansys.pyensight.core.enscontext._capture_context(ensight,{full_context})",
|
|
1779
|
+
do_eval=True,
|
|
1780
|
+
)
|
|
1781
|
+
context = EnsContext()
|
|
1782
|
+
context._from_data(data_str)
|
|
1783
|
+
return context
|
|
1784
|
+
|
|
1785
|
+
def restore_context(self, context: "enscontext.EnsContext") -> None:
|
|
1786
|
+
"""Restore the current EnSight instance state.
|
|
1787
|
+
|
|
1788
|
+
This method restores EnSight to the state stored in an ``EnsContext``
|
|
1789
|
+
object that was either read from disk or returned by the
|
|
1790
|
+
:func:`capture_context<ansys.pyensight.core.Session.capture_context>` method.
|
|
1791
|
+
|
|
1792
|
+
Parameters
|
|
1793
|
+
----------
|
|
1794
|
+
context : enscontext.EnsContext
|
|
1795
|
+
Context to set the current EnSight instance to.
|
|
1796
|
+
|
|
1797
|
+
Examples
|
|
1798
|
+
--------
|
|
1799
|
+
>>> tmp_ctx = session.capture_context()
|
|
1800
|
+
>>> session.restore_context(EnsContext("session_context.ctxz"))
|
|
1801
|
+
>>> session.restore_context(tmp_ctx)
|
|
1802
|
+
"""
|
|
1803
|
+
data_str = context._data(b64=True)
|
|
1804
|
+
self.cmd("import ansys.pyensight.core.enscontext", do_eval=False)
|
|
1805
|
+
self.cmd(
|
|
1806
|
+
f"ansys.pyensight.core.enscontext._restore_context(ensight,'{data_str}')", do_eval=False
|
|
1807
|
+
)
|
|
1808
|
+
|
|
1809
|
+
def ensight_version_check(
|
|
1810
|
+
self,
|
|
1811
|
+
version: Union[int, str],
|
|
1812
|
+
message: str = "",
|
|
1813
|
+
exception: bool = True,
|
|
1814
|
+
strict: bool = False,
|
|
1815
|
+
) -> bool:
|
|
1816
|
+
"""Check if the session is a specific version.
|
|
1817
|
+
|
|
1818
|
+
Different versions of pyensight Sessions may host different versions of EnSight.
|
|
1819
|
+
This method compares the version of the remote EnSight session to a specific version
|
|
1820
|
+
number. If the remote EnSight version is at least the specified version, then
|
|
1821
|
+
this method returns True. If the version of EnSight is earlier than the specified
|
|
1822
|
+
version, this method will raise an exception. The caller can specify the
|
|
1823
|
+
error string to be included. They may also specify if the version check should
|
|
1824
|
+
be for a specific version vs the specified version or higher. It is also possible
|
|
1825
|
+
to avoid the exception and instead just return True or False for cases when an
|
|
1826
|
+
alternative implementation might be used.
|
|
1827
|
+
|
|
1828
|
+
Parameters
|
|
1829
|
+
----------
|
|
1830
|
+
version : Union[int, str]
|
|
1831
|
+
The version number to compare the EnSight version against.
|
|
1832
|
+
message : str
|
|
1833
|
+
The message string to be used as the text for any raised exception.
|
|
1834
|
+
exception : bool
|
|
1835
|
+
If True, and the version comparison fails, an InvalidEnSightVersion is raised.
|
|
1836
|
+
Otherwise, the result of the comparison is returned.
|
|
1837
|
+
strict : bool
|
|
1838
|
+
If True, the comparison of the two versions will only pass if they
|
|
1839
|
+
are identical. If False, if the EnSight version is greater than or
|
|
1840
|
+
equal to the specified version the comparison will pass.
|
|
1841
|
+
|
|
1842
|
+
Returns
|
|
1843
|
+
-------
|
|
1844
|
+
True if the comparison succeeds, False otherwise.
|
|
1845
|
+
|
|
1846
|
+
Raises
|
|
1847
|
+
------
|
|
1848
|
+
InvalidEnSightVersion if the comparison fails and exception is True.
|
|
1849
|
+
"""
|
|
1850
|
+
ens_version = int(self.ensight.version("suffix"))
|
|
1851
|
+
# handle various input formats
|
|
1852
|
+
target = version
|
|
1853
|
+
if isinstance(target, str): # pragma: no cover
|
|
1854
|
+
# could be 'year RX' or the suffix as a string
|
|
1855
|
+
if "R" in target:
|
|
1856
|
+
tmp = [int(x) for x in target.split("R")]
|
|
1857
|
+
target = (tmp[0] - 2000) * 10 + tmp[1]
|
|
1858
|
+
else:
|
|
1859
|
+
target = int(target)
|
|
1860
|
+
# check validity
|
|
1861
|
+
valid = ens_version == target
|
|
1862
|
+
at_least = ""
|
|
1863
|
+
if not strict: # pragma: no cover
|
|
1864
|
+
at_least = "at least "
|
|
1865
|
+
valid = ens_version >= target
|
|
1866
|
+
if (not valid) and exception:
|
|
1867
|
+
ens_version = self.ensight.version("version-full")
|
|
1868
|
+
base_msg = f" ({at_least}'{version}' required, '{ens_version}' current)"
|
|
1869
|
+
if message: # pragma: no cover
|
|
1870
|
+
message += base_msg # pragma: no cover
|
|
1871
|
+
else:
|
|
1872
|
+
message = f"A newer version of EnSight is required to use this API:{base_msg}"
|
|
1873
|
+
raise InvalidEnSightVersion(message)
|
|
1874
|
+
return valid
|
|
1875
|
+
|
|
1876
|
+
def find_remote_unused_ports(
|
|
1877
|
+
self,
|
|
1878
|
+
count: int,
|
|
1879
|
+
start: Optional[int] = None,
|
|
1880
|
+
end: Optional[int] = None,
|
|
1881
|
+
avoid: Optional[list[int]] = None,
|
|
1882
|
+
) -> Optional[List[int]]:
|
|
1883
|
+
"""
|
|
1884
|
+
Find "count" unused ports on the host system. A port is considered
|
|
1885
|
+
unused if it does not respond to a "connect" attempt. Walk the ports
|
|
1886
|
+
from 'start' to 'end' looking for unused ports and avoiding any ports
|
|
1887
|
+
in the 'avoid' list. Stop once the desired number of ports have been
|
|
1888
|
+
found. If an insufficient number of ports were found, return None.
|
|
1889
|
+
An admin user check is used to skip [1-1023].
|
|
1890
|
+
|
|
1891
|
+
Parameters
|
|
1892
|
+
----------
|
|
1893
|
+
count: int
|
|
1894
|
+
number of unused ports to find
|
|
1895
|
+
start: int
|
|
1896
|
+
the first port to check or None (random start)
|
|
1897
|
+
end: int
|
|
1898
|
+
the last port to check or None (full range check)
|
|
1899
|
+
avoid: list
|
|
1900
|
+
an optional list of ports not to check
|
|
1901
|
+
|
|
1902
|
+
Returns
|
|
1903
|
+
-------
|
|
1904
|
+
the detected ports or None on failure
|
|
1905
|
+
"""
|
|
1906
|
+
cmd = "from cei import find_unused_ports\n"
|
|
1907
|
+
cmd += f"ports = find_unused_ports({count}, start={start}, end={end}, avoid={avoid})"
|
|
1908
|
+
self.cmd(cmd, do_eval=False)
|
|
1909
|
+
return self.cmd("ports")
|
|
1910
|
+
|
|
1911
|
+
def set_dsg_session(self, dsg_session: "DSGSession"):
|
|
1912
|
+
"""Set a DSG Session for the current PyEnSight session.
|
|
1913
|
+
|
|
1914
|
+
This is required if a DSGSession is running together with
|
|
1915
|
+
PyEnSight and the second might send gRPC requests while the first
|
|
1916
|
+
is blocked because the gRPC queue is full.
|
|
1917
|
+
|
|
1918
|
+
Parameters
|
|
1919
|
+
----------
|
|
1920
|
+
dsg_session: DSGSession
|
|
1921
|
+
a DSGSession object
|
|
1922
|
+
"""
|
|
1923
|
+
self._dsg_session = dsg_session
|