ansys-pyensight-core 0.7.11__py3-none-any.whl → 0.8.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.

Potentially problematic release.


This version of ansys-pyensight-core might be problematic. Click here for more details.

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