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.
Files changed (37) hide show
  1. ansys/pyensight/core/__init__.py +41 -0
  2. ansys/pyensight/core/common.py +341 -0
  3. ansys/pyensight/core/deep_pixel_view.html +98 -0
  4. ansys/pyensight/core/dockerlauncher.py +1124 -0
  5. ansys/pyensight/core/dvs.py +872 -0
  6. ansys/pyensight/core/enscontext.py +345 -0
  7. ansys/pyensight/core/enshell_grpc.py +641 -0
  8. ansys/pyensight/core/ensight_grpc.py +874 -0
  9. ansys/pyensight/core/ensobj.py +515 -0
  10. ansys/pyensight/core/launch_ensight.py +296 -0
  11. ansys/pyensight/core/launcher.py +388 -0
  12. ansys/pyensight/core/libuserd.py +2110 -0
  13. ansys/pyensight/core/listobj.py +280 -0
  14. ansys/pyensight/core/locallauncher.py +579 -0
  15. ansys/pyensight/core/py.typed +0 -0
  16. ansys/pyensight/core/renderable.py +880 -0
  17. ansys/pyensight/core/session.py +1923 -0
  18. ansys/pyensight/core/sgeo_poll.html +24 -0
  19. ansys/pyensight/core/utils/__init__.py +21 -0
  20. ansys/pyensight/core/utils/adr.py +111 -0
  21. ansys/pyensight/core/utils/dsg_server.py +1220 -0
  22. ansys/pyensight/core/utils/export.py +606 -0
  23. ansys/pyensight/core/utils/omniverse.py +769 -0
  24. ansys/pyensight/core/utils/omniverse_cli.py +614 -0
  25. ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
  26. ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
  27. ansys/pyensight/core/utils/parts.py +1221 -0
  28. ansys/pyensight/core/utils/query.py +487 -0
  29. ansys/pyensight/core/utils/readers.py +300 -0
  30. ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
  31. ansys/pyensight/core/utils/support.py +128 -0
  32. ansys/pyensight/core/utils/variables.py +2019 -0
  33. ansys/pyensight/core/utils/views.py +674 -0
  34. ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
  35. ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
  36. ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
  37. 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