ansys-pyensight-core 0.8.12__py3-none-any.whl → 0.8.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

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