ansys-pyensight-core 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. ansys/pyensight/core/__init__.py +41 -0
  2. ansys/pyensight/core/common.py +341 -0
  3. ansys/pyensight/core/deep_pixel_view.html +98 -0
  4. ansys/pyensight/core/dockerlauncher.py +1124 -0
  5. ansys/pyensight/core/dvs.py +872 -0
  6. ansys/pyensight/core/enscontext.py +345 -0
  7. ansys/pyensight/core/enshell_grpc.py +641 -0
  8. ansys/pyensight/core/ensight_grpc.py +874 -0
  9. ansys/pyensight/core/ensobj.py +515 -0
  10. ansys/pyensight/core/launch_ensight.py +296 -0
  11. ansys/pyensight/core/launcher.py +388 -0
  12. ansys/pyensight/core/libuserd.py +2110 -0
  13. ansys/pyensight/core/listobj.py +280 -0
  14. ansys/pyensight/core/locallauncher.py +579 -0
  15. ansys/pyensight/core/py.typed +0 -0
  16. ansys/pyensight/core/renderable.py +880 -0
  17. ansys/pyensight/core/session.py +1923 -0
  18. ansys/pyensight/core/sgeo_poll.html +24 -0
  19. ansys/pyensight/core/utils/__init__.py +21 -0
  20. ansys/pyensight/core/utils/adr.py +111 -0
  21. ansys/pyensight/core/utils/dsg_server.py +1220 -0
  22. ansys/pyensight/core/utils/export.py +606 -0
  23. ansys/pyensight/core/utils/omniverse.py +769 -0
  24. ansys/pyensight/core/utils/omniverse_cli.py +614 -0
  25. ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
  26. ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
  27. ansys/pyensight/core/utils/parts.py +1221 -0
  28. ansys/pyensight/core/utils/query.py +487 -0
  29. ansys/pyensight/core/utils/readers.py +300 -0
  30. ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
  31. ansys/pyensight/core/utils/support.py +128 -0
  32. ansys/pyensight/core/utils/variables.py +2019 -0
  33. ansys/pyensight/core/utils/views.py +674 -0
  34. ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
  35. ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
  36. ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
  37. ansys_pyensight_core-0.11.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,769 @@
1
+ # Copyright (C) 2022 - 2026 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: MIT
3
+ #
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ import glob
24
+ import json
25
+ import os
26
+ import platform
27
+ import subprocess
28
+ import sys
29
+ import tempfile
30
+ import threading
31
+ from types import ModuleType
32
+ from typing import TYPE_CHECKING, List, Optional, Union
33
+ from urllib.parse import ParseResult, urlparse
34
+ import uuid
35
+
36
+ from ansys.pyensight.core.common import grpc_version_check
37
+ import psutil
38
+
39
+ if TYPE_CHECKING:
40
+ try:
41
+ import ensight
42
+ except ImportError:
43
+ from ansys.api.pyensight import ensight_api
44
+
45
+
46
+ def _handle_fluids_one(install_path):
47
+ cei_path = install_path
48
+ interpreter = os.path.join(cei_path, "bin", "cpython")
49
+ if platform.system() == "Windows":
50
+ interpreter += ".bat"
51
+ return interpreter
52
+
53
+
54
+ class OmniverseKitInstance:
55
+ """Interface to an Omniverse application instance
56
+
57
+ Parameters
58
+ ----------
59
+ pid : int
60
+ The process id of the launched instance
61
+ """
62
+
63
+ def __init__(self, proc: subprocess.Popen) -> None:
64
+ self._proc: subprocess.Popen = proc
65
+ self._returncode: Optional[int] = None
66
+ self._rendering = False
67
+ self._lines_read = 0
68
+ self._scanner_thread = threading.Thread(
69
+ target=OmniverseKitInstance._scan_stdout, args=(self,)
70
+ )
71
+ self._scanner_thread.start()
72
+ self._simba_url: Optional[ParseResult] = None
73
+
74
+ def __del__(self) -> None:
75
+ """Close down the instance on delete"""
76
+ self.close()
77
+
78
+ def close(self) -> None:
79
+ """Shutdown the Omniverse instance
80
+
81
+ If the instance associated with this object is still running,
82
+ shut it down.
83
+ """
84
+ if not self.is_running():
85
+ return
86
+ proc = psutil.Process(self._proc.pid)
87
+ for child in proc.children(recursive=True):
88
+ if psutil.pid_exists(child.pid):
89
+ # This can be a race condition, so it is ok if the child is dead already
90
+ try:
91
+ child.terminate()
92
+ except psutil.NoSuchProcess:
93
+ pass
94
+ # Same issue, this process might already be shutting down, so NoSuchProcess is ok.
95
+ try:
96
+ proc.terminate()
97
+ except psutil.NoSuchProcess:
98
+ pass
99
+ self._scanner_thread.join()
100
+
101
+ # On a forced close, set a return code of 0
102
+ self._returncode = 0
103
+ self._simba_url = None
104
+
105
+ @staticmethod
106
+ def _scan_stdout(oki: "OmniverseKitInstance"):
107
+ while oki._proc and oki._proc.poll() is None:
108
+ if oki._proc.stdout is not None:
109
+ output_line = oki._proc.stdout.readline().decode("utf-8")
110
+ oki._lines_read = oki._lines_read + 1
111
+ if "RTX ready" in output_line:
112
+ oki._rendering = True
113
+ if output_line.startswith("Server running on "):
114
+ urlstr = output_line.removeprefix("Server running on ")
115
+ oki._simba_url = urlparse(urlstr)
116
+
117
+ def is_rendering(self) -> bool:
118
+ """Check if the instance has finished launching and is ready to render
119
+
120
+ Returns
121
+ -------
122
+ bool
123
+ True if the instance is ready to render.
124
+ """
125
+ return self.is_running() and self._rendering
126
+
127
+ def is_running(self) -> bool:
128
+ """Check if the instance is still running
129
+
130
+ Returns
131
+ -------
132
+ bool
133
+ True if the instance is still running.
134
+ """
135
+ if self._proc is None:
136
+ return False
137
+ if self._proc.poll() is None:
138
+ return True
139
+ return False
140
+
141
+ def simba_url(self) -> Optional[ParseResult]:
142
+ """Return the URL for the Simba server
143
+
144
+ Returns
145
+ -------
146
+ ParseResult or None
147
+ URL for the Simba server, or None if it is not running
148
+ """
149
+ return self._simba_url
150
+
151
+ def returncode(self) -> Optional[int]:
152
+ """Get the return code if the process has stopped, or None if still running
153
+
154
+ Returns
155
+ -------
156
+ int or None
157
+ Get the return code if the process has stopped, or None if still running
158
+ """
159
+ if self._returncode is not None:
160
+ return self._returncode
161
+ if self.is_running():
162
+ return None
163
+ self._returncode = self._proc.returncode
164
+ return self._returncode
165
+
166
+
167
+ # Deprecated
168
+ def find_kit_filename(fallback_directory: Optional[str] = None) -> Optional[str]:
169
+ """
170
+ Use a combination of the current omniverse application and the information
171
+ in the local .nvidia-omniverse/config/omniverse.toml file to come up with
172
+ the pathname of a kit executable suitable for hosting another copy of the
173
+ ansys.geometry.server kit.
174
+
175
+ Returns
176
+ -------
177
+ Optional[str]
178
+ The pathname of a kit executable or None
179
+
180
+ """
181
+ # parse the toml config file for the location of the installed apps
182
+ try:
183
+ import tomllib
184
+ except ModuleNotFoundError:
185
+ import pip._vendor.tomli as tomllib
186
+
187
+ homedir = os.path.expanduser("~")
188
+ ov_config = os.path.join(homedir, ".nvidia-omniverse", "config", "omniverse.toml")
189
+ if not os.path.exists(ov_config):
190
+ return None
191
+ # read the Omniverse configuration toml file
192
+ with open(ov_config, "r") as ov_file:
193
+ ov_data = ov_file.read()
194
+ config = tomllib.loads(ov_data)
195
+ appdir = config.get("paths", {}).get("library_root", fallback_directory)
196
+
197
+ # If we are running inside an Omniverse app, use that information
198
+ try:
199
+ import omni.kit.app
200
+
201
+ # get the current application
202
+ app = omni.kit.app.get_app()
203
+ app_name = app.get_app_filename().split(".")[-1]
204
+ app_version = app.get_app_version().split("-")[0]
205
+ # and where it is installed
206
+ appdir = os.path.join(appdir, f"{app_name}-{app_version}")
207
+ except ModuleNotFoundError:
208
+ # Names should be like: "C:\\Users\\foo\\AppData\\Local\\ov\\pkg\\create-2023.2.3\\launcher.toml"
209
+ target = None
210
+ target_version = None
211
+ for d in glob.glob(os.path.join(appdir, "*", "launcher.toml")):
212
+ test_dir = os.path.dirname(d)
213
+ # the name will be something like "create-2023.2.3"
214
+ name = os.path.basename(test_dir).split("-")
215
+ if len(name) != 2:
216
+ continue
217
+ if name[0] not in ("kit", "create", "view"):
218
+ continue
219
+ if (target_version is None) or (name[1] > target_version):
220
+ target = test_dir
221
+ target_version = name[1]
222
+ if target is None:
223
+ return None
224
+ appdir = target
225
+
226
+ # Windows: 'kit.bat' in '.' or 'kit' followed by 'kit.exe' in '.' or 'kit'
227
+ # Linux: 'kit.sh' in '.' or 'kit' followed by 'kit' in '.' or 'kit'
228
+ exe_names = ["kit.sh", "kit"]
229
+ if sys.platform.startswith("win"):
230
+ exe_names = ["kit.bat", "kit.exe"]
231
+
232
+ # look in 4 places...
233
+ for dir_name in [appdir, os.path.join(appdir, "kit")]:
234
+ for exe_name in exe_names:
235
+ if os.path.exists(os.path.join(dir_name, exe_name)):
236
+ return os.path.join(dir_name, exe_name)
237
+
238
+ return None
239
+
240
+
241
+ # Deprecated
242
+ def launch_kit_instance(
243
+ kit_path: Optional[str] = None,
244
+ extension_paths: Optional[List[str]] = None,
245
+ extensions: Optional[List[str]] = None,
246
+ cli_options: Optional[List[str]] = None,
247
+ log_file: Optional[str] = None,
248
+ log_level: str = "warn",
249
+ ) -> "OmniverseKitInstance":
250
+ """Launch an Omniverse application instance
251
+
252
+ Parameters
253
+ ----------
254
+ kit_path : Optional[str]
255
+ The full pathname of to a binary capable of serving as a kit runner.
256
+ extension_paths : Optional[List[str]]
257
+ List of directory names to include the in search for kits.
258
+ extensions : Optional[List[str]]
259
+ List of kit extensions to be loaded into the launched kit instance.
260
+ log_file : Optional[str]
261
+ The name of a text file where the logging information for the instance will be saved.
262
+ log_level : str
263
+ The level of the logging information to record: "verbose", "info", "warn", "error", "fatal",
264
+ the default is "warn".
265
+
266
+ Returns
267
+ -------
268
+ OmniverseKitInstance
269
+ The object interface for the launched instance
270
+
271
+ Examples
272
+ --------
273
+ Run a simple, empty GUI kit instance.
274
+
275
+ >>> from ansys.pyensight.core.utils import omniverse
276
+ >>> ov = omniverse.launch_kit_instance(extensions=['omni.kit.uiapp'])
277
+
278
+ """
279
+ # build the command line
280
+ if not kit_path:
281
+ kit_path = find_kit_filename()
282
+ if not kit_path:
283
+ raise RuntimeError("Unable to find a suitable Omniverse kit install")
284
+ cmd = [kit_path]
285
+ if extension_paths:
286
+ for path in extension_paths:
287
+ cmd.extend(["--ext-folder", path])
288
+ if extensions:
289
+ for ext in extensions:
290
+ cmd.extend(["--enable", ext])
291
+ if cli_options:
292
+ for opt in cli_options:
293
+ cmd.append(opt)
294
+ if log_level not in ("verbose", "info", "warn", "error", "fatal"):
295
+ raise RuntimeError(f"Invalid logging level: {log_level}")
296
+ cmd.append(f"--/log/level={log_level}")
297
+ if log_file:
298
+ cmd.append(f"--/log/file={log_file}")
299
+ cmd.append("--/log/enabled=true")
300
+ # Launch the process
301
+ env_vars = os.environ.copy()
302
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env_vars)
303
+ return OmniverseKitInstance(p)
304
+
305
+
306
+ def find_app(ansys_installation: Optional[str] = None) -> Optional[str]:
307
+ dirs_to_check = []
308
+ if ansys_installation:
309
+ # Given a different Ansys install
310
+ local_tp = os.path.join(os.path.join(ansys_installation, "tp", "showcase"))
311
+ if os.path.exists(local_tp):
312
+ dirs_to_check.append(local_tp)
313
+ # Dev Folder
314
+ omni_platform_dir = "linux-x86_64"
315
+ if sys.platform.startswith("win"):
316
+ omni_platform_dir = "windows-x86_64"
317
+ local_dev_omni = os.path.join(
318
+ ansys_installation,
319
+ "omni_build",
320
+ "kit-app-template",
321
+ "_build",
322
+ omni_platform_dir,
323
+ "release",
324
+ )
325
+ if os.path.exists(local_dev_omni):
326
+ dirs_to_check.append(local_dev_omni)
327
+ if "PYENSIGHT_ANSYS_INSTALLATION" in os.environ:
328
+ env_inst = os.environ["PYENSIGHT_ANSYS_INSTALLATION"]
329
+ dirs_to_check.append(os.path.join(env_inst, "tp", "showcase"))
330
+
331
+ # Look for most recent Ansys install, 25.2 or later
332
+ awp_roots = []
333
+ for env_name in dict(os.environ).keys():
334
+ if env_name.startswith("AWP_ROOT") and int(env_name[len("AWP_ROOT") :]) >= 252:
335
+ awp_roots.append(env_name)
336
+ awp_roots.sort(reverse=True)
337
+ for env_name in awp_roots:
338
+ dirs_to_check.append(os.path.join(os.environ[env_name], "tp", "showcase"))
339
+
340
+ # check all the collected locations in order
341
+ for install_dir in dirs_to_check:
342
+ launch_file = os.path.join(install_dir, "ansys_tools_omni_core.py")
343
+ if os.path.isfile(launch_file):
344
+ return launch_file
345
+ return None
346
+
347
+
348
+ def launch_app(
349
+ usd_file: Optional[str] = "",
350
+ layout: Optional[str] = "default",
351
+ streaming: Optional[bool] = False,
352
+ offscreen: Optional[bool] = False,
353
+ log_file: Optional[str] = None,
354
+ log_level: Optional[str] = "warn",
355
+ cli_options: Optional[List[str]] = None,
356
+ ansys_installation: Optional[str] = None,
357
+ interpreter: Optional[str] = None,
358
+ ) -> "OmniverseKitInstance":
359
+ """Launch the Ansys Omniverse application
360
+
361
+ Parameters
362
+ ----------
363
+ # usd_file : Optional[str]
364
+ # A .usd file to open on startup
365
+ # layout : Optional[str]
366
+ # A UI layout. viewer, composer, or composer_slim
367
+ # streaming : Optional[bool]
368
+ # Enable webrtc streaming to enable the window in a web page
369
+ # offscreen : Optional[str]
370
+ # Run the app offscreen. Useful when streaming.
371
+ # log_file : Optional[str]
372
+ # The name of a text file where the logging information for the instance will be saved.
373
+ # log_level : Optional[str]
374
+ # The level of the logging information to record: "verbose", "info", "warn", "error", "fatal",
375
+ # the default is "warn".
376
+ # cli_options : Optional[List[str]]
377
+ # Other command line options
378
+
379
+ Returns
380
+ -------
381
+ OmniverseKitInstance
382
+ The object interface for the launched instance
383
+
384
+ Examples
385
+ --------
386
+ Run the app with default options
387
+
388
+ >>> from ansys.pyensight.core.utils import omniverse
389
+ >>> ov = omniverse.launch_app()
390
+
391
+ """
392
+ cmd = [sys.executable]
393
+ if interpreter:
394
+ cmd = [interpreter]
395
+ app = find_app(ansys_installation=ansys_installation)
396
+ if not app:
397
+ raise RuntimeError("Unable to find the Ansys Omniverse app")
398
+ cmd.extend([app])
399
+ if usd_file:
400
+ cmd.extend(["-f", usd_file])
401
+ if layout:
402
+ cmd.extend(["-l", layout])
403
+ if streaming:
404
+ cmd.extend(["-s"])
405
+ if offscreen:
406
+ cmd.extend(["-o"])
407
+ if cli_options:
408
+ cmd.extend(cli_options)
409
+ if log_level:
410
+ if log_level not in ("verbose", "info", "warn", "error", "fatal"):
411
+ raise RuntimeError(f"Invalid logging level: {log_level}")
412
+ cmd.extend([f"--/log/level={log_level}"])
413
+ if log_file:
414
+ cmd.extend(["--/log/enabled=true", f"--/log/file={log_file}"])
415
+
416
+ # Launch the process
417
+ env_vars = os.environ.copy()
418
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env_vars)
419
+ return OmniverseKitInstance(p)
420
+
421
+
422
+ class Omniverse:
423
+ """Provides the ``ensight.utils.omniverse`` interface.
424
+
425
+ The omniverse class methods provide an interface between an EnSight session
426
+ and an Omniverse instance. See :ref:`omniverse_info` for additional details.
427
+
428
+ Parameters
429
+ ----------
430
+ interface: Union["ensight_api.ensight", "ensight"]
431
+ Entity that provides the ``ensight`` namespace. In the case of
432
+ EnSight Python, the ``ensight`` module is passed. In the case
433
+ of PyEnSight, ``Session.ensight`` is passed.
434
+
435
+ Notes
436
+ -----
437
+ This interface is only available when using pyensight (they do not work with
438
+ the ensight Python interpreter) and the module must be used in an interpreter
439
+ that includes the Omniverse Python modules (e.g. omni and pxr). Only a single
440
+ Omniverse connection can be established within a single pyensight session.
441
+
442
+ Examples
443
+ --------
444
+
445
+ >>> from ansys.pyensight.core import LocalLauncher
446
+ >>> session = LocalLauncher().start()
447
+ >>> ov = session.ensight.utils.omniverse
448
+ >>> ov.create_connection("D:\\Omniverse\\Example")
449
+ >>> ov.update()
450
+ >>> ov.close_connection()
451
+
452
+ """
453
+
454
+ def __init__(self, interface: Union["ensight_api.ensight", "ensight"]):
455
+ self._ensight = interface
456
+ self._server_pid: Optional[int] = None
457
+ self._interpreter: str = ""
458
+ self._status_filename: str = ""
459
+
460
+ def _check_modules(self) -> None:
461
+ """Verify that the Python interpreter is correct
462
+
463
+ Check for module dependencies. If not present, raise an exception.
464
+
465
+ Raises
466
+ ------
467
+ RuntimeError
468
+ if the necessary modules are missing.
469
+
470
+ """
471
+ # One time check for this
472
+ if len(self._interpreter):
473
+ return
474
+
475
+ # if a module, then we are inside EnSight
476
+ if isinstance(self._ensight, ModuleType): # pragma: no cover
477
+ # in this case, we can just use cpython
478
+ import ceiversion
479
+ import enve
480
+
481
+ cei_home = os.environ.get("CEI_HOME", enve.home())
482
+ self._interpreter = os.path.join(cei_home, "bin", f"cpython{ceiversion.apex_suffix}")
483
+ if platform.system() == "Windows":
484
+ self._interpreter += ".bat"
485
+ return
486
+ # Check if the python interpreter is kit itself
487
+ is_omni = False
488
+ try:
489
+ import omni # noqa: F401
490
+
491
+ is_omni = "kit" in os.path.basename(sys.executable)
492
+ except ModuleNotFoundError:
493
+ pass
494
+ # Using the python interpreter running this code
495
+ self._interpreter = sys.executable
496
+ if "fluids_one" in self._interpreter: # compiled simba-app
497
+ self._interpreter = _handle_fluids_one(self._ensight._session._install_path)
498
+ if is_omni:
499
+ kit_path = os.path.dirname(sys.executable)
500
+ self._interpreter = os.path.join(kit_path, "python")
501
+ if platform.system() == "Windows":
502
+ self._interpreter += ".bat"
503
+ else:
504
+ self._interpreter += ".sh"
505
+
506
+ # in the future, these will be part of the pyensight wheel
507
+ # dependencies, but for now we include this check.
508
+ try:
509
+ import pxr # noqa: F401
510
+ import pygltflib # noqa: F401
511
+ except Exception:
512
+ raise RuntimeError("Unable to detect omniverse dependencies: usd-core, pygltflib.")
513
+
514
+ def is_running_omniverse(self) -> bool:
515
+ """Check that an Omniverse connection is active
516
+
517
+ Returns
518
+ -------
519
+ bool
520
+ True if the connection is active, False otherwise.
521
+ """
522
+ if self._server_pid is None:
523
+ return False
524
+ if psutil.pid_exists(self._server_pid):
525
+ return True
526
+ self._server_pid = None
527
+ return False
528
+
529
+ def create_connection(
530
+ self,
531
+ omniverse_path: str,
532
+ include_camera: bool = False,
533
+ normalize_geometry: bool = False,
534
+ temporal: bool = False,
535
+ live: bool = True,
536
+ debug_filename: str = "",
537
+ time_scale: float = 1.0,
538
+ line_width: float = 0.0,
539
+ options: dict = {},
540
+ ) -> None:
541
+ """Ensure that an EnSight dsg -> omniverse server is running
542
+
543
+ Connect the current EnSight session to an Omniverse server.
544
+ This is done by launching a new service that makes a dynamic scene graph
545
+ connection to the EnSight session and pushes updates to the Omniverse server.
546
+ The initial EnSight scene will be pushed after the connection is established.
547
+
548
+ Parameters
549
+ ----------
550
+ omniverse_path : str
551
+ The directory name where the USD files should be saved. For example:
552
+ "C:/Users/test/OV/usdfiles"
553
+ include_camera : bool
554
+ If True, apply the EnSight camera to the Omniverse scene. This option
555
+ should be used if the target viewer is in AR/VR mode. Defaults to False.
556
+ normalize_geometry : bool
557
+ Omniverse units are in meters. If the source dataset is not in the correct
558
+ unit system or is just too large/small, this option will remap the geometry
559
+ to a unit cube. Defaults to False.
560
+ temporal : bool
561
+ If True, save all timesteps.
562
+ live : bool
563
+ If True, one can call 'update()' to send updated geometry to Omniverse.
564
+ If False, the Omniverse connection will push a single update and then
565
+ disconnect. Defaults to True.
566
+ time_scale : float
567
+ Multiply all EnSight time values by this factor before exporting to Omniverse.
568
+ The default is 1.0.
569
+ debug_filename : str
570
+ If the name of a file is provided, it will be used to save logging information on
571
+ the connection between EnSight and Omniverse. This option is no longer supported,
572
+ but the API remains for backwards compatibility.
573
+ line_width : float
574
+ If set, line objects will be represented as "tubes" of the size specified by
575
+ this factor. The default is 0.0 and causes lines not to be exported.
576
+ options : dict
577
+ Allows for a fallback for the grpc host/port and the security token.
578
+ """
579
+ if not isinstance(self._ensight, ModuleType):
580
+ self._ensight._session.ensight_version_check("2023 R2")
581
+ self._check_modules()
582
+ if self.is_running_omniverse():
583
+ raise RuntimeError("An Omniverse server connection is already active.")
584
+ dsg_uri = None
585
+ is_win = "Win" in platform.system()
586
+ grpc_use_tcp_sockets = False
587
+ grpc_allow_network_connectsion = False
588
+ grpc_disable_tls = False
589
+ disable_grpc_options = False
590
+ if not isinstance(self._ensight, ModuleType):
591
+ # Make sure the internal ui module is loaded
592
+ grpc_use_tcp_sockets = self._ensight._session._grpc_use_tcp_sockets
593
+ grpc_allow_network_connectsion = self._ensight._session._grpc_allow_network_connections
594
+ grpc_disable_tls = self._ensight._session._grpc_disable_tls
595
+ disable_grpc_options = self._ensight._session._disable_grpc_options
596
+ self._ensight._session.cmd("import enspyqtgui_int", do_eval=False)
597
+ # Get the gRPC connection details and use them to launch the service
598
+ use_tcp_sockets = self._ensight._session._grpc_use_tcp_sockets
599
+ hostname = self._ensight._session.grpc.host
600
+ token = self._ensight._session.grpc.security_token
601
+ if not is_win and not use_tcp_sockets and not disable_grpc_options:
602
+ uds_path = self._ensight._session._grpc_uds_pathname
603
+ dsg_uds_path = "/tmp/greeter"
604
+ if uds_path:
605
+ dsg_uds_path = uds_path
606
+ dsg_uri = f"unix:{dsg_uds_path}.sock"
607
+ else:
608
+ port = self._ensight._session._grpc_port
609
+ hostname = self._ensight._session.grpc.host
610
+ token = self._ensight._session.grpc.security_token
611
+ dsg_uri = f"grpc://{hostname}:{port}"
612
+ else:
613
+ import ceiversion
614
+
615
+ ensight_full_version = ceiversion.ensight_full
616
+ ensight_internal_version = ceiversion.ensight
617
+ disable_grpc_options = not grpc_version_check(
618
+ ensight_full_version=ensight_full_version, internal_version=ensight_internal_version
619
+ )
620
+ hostname = options.get("host", "127.0.0.1")
621
+ port = options.get("port", 12345)
622
+ uds_path = options.get("uds_path")
623
+ token = options.get("security", "")
624
+ if uds_path and not is_win:
625
+ dsg_uri = f"unix:{uds_path}.sock"
626
+ else:
627
+ dsg_uri = f"grpc://{hostname}:{port}"
628
+
629
+ # Launch the server via the 'ansys.pyensight.core.utils.omniverse_cli' module
630
+ cmd = [self._interpreter]
631
+ cmd.extend(["-m", "ansys.pyensight.core.utils.omniverse_cli"])
632
+ cmd.append(omniverse_path)
633
+ if token:
634
+ cmd.extend(["--security_token", token])
635
+ if temporal:
636
+ cmd.extend(["--temporal", "true"])
637
+ if not include_camera:
638
+ cmd.extend(["--include_camera", "false"])
639
+ if normalize_geometry:
640
+ cmd.extend(["--normalize_geometry", "true"])
641
+ if time_scale != 1.0:
642
+ cmd.extend(["--time_scale", str(time_scale)])
643
+ if line_width != 0.0:
644
+ cmd.extend(["--line_width", str(line_width)])
645
+ if not live:
646
+ cmd.extend(["--oneshot", "1"])
647
+ if grpc_allow_network_connectsion:
648
+ cmd.extend(["--grpc_allow_network_connections", "1"])
649
+ if grpc_disable_tls:
650
+ cmd.extend(["--grpc_disable_tls", "1"])
651
+ if grpc_use_tcp_sockets:
652
+ cmd.extend(["--grpc_use_tcp_sockets", "1"])
653
+ if disable_grpc_options:
654
+ cmd.extend(["--disable_grpc_options", "1"])
655
+ cmd.extend(["--dsg_uri", dsg_uri])
656
+ env_vars = os.environ.copy()
657
+ # we are launching the kit from EnSight or PyEnSight. In these cases, we
658
+ # inform the kit instance of:
659
+ # (1) the name of the "server status" file, if any
660
+ self._new_status_file()
661
+ env_vars["ANSYS_OV_SERVER_STATUS_FILENAME"] = self._status_filename
662
+ process = subprocess.Popen(cmd, close_fds=True, env=env_vars)
663
+ self._server_pid = process.pid
664
+
665
+ def _new_status_file(self, new=True) -> None:
666
+ """
667
+ Remove any existing status file and create a new one if requested.
668
+
669
+ Parameters
670
+ ----------
671
+ new : bool
672
+ If True, create a new status file.
673
+ """
674
+ if self._status_filename:
675
+ try:
676
+ os.remove(self._status_filename)
677
+ except OSError:
678
+ pass
679
+ self._status_filename = ""
680
+ if new:
681
+ self._status_filename = os.path.join(
682
+ tempfile.gettempdir(), str(uuid.uuid1()) + "_gs_status.txt"
683
+ )
684
+
685
+ def read_status_file(self) -> dict:
686
+ """Read the status file and return its contents as a dictionary.
687
+
688
+ Note: this can fail if the file is being written to when this call is made, so expect
689
+ failures.
690
+
691
+ Returns
692
+ -------
693
+ Optional[dict]
694
+ A dictionary with the fields 'status', 'start_time', 'processed_buffers', 'total_buffers' or empty
695
+ """
696
+ if not self._status_filename:
697
+ return {}
698
+ try:
699
+ with open(self._status_filename, "r") as status_file:
700
+ data = json.load(status_file)
701
+ except Exception:
702
+ return {}
703
+ return data
704
+
705
+ def close_connection(self) -> None:
706
+ """Shut down the open EnSight dsg -> omniverse server
707
+
708
+ Break the connection between the EnSight instance and Omniverse.
709
+
710
+ """
711
+ self._check_modules()
712
+ if not self.is_running_omniverse():
713
+ return
714
+ proc = psutil.Process(self._server_pid)
715
+ for child in proc.children(recursive=True):
716
+ if psutil.pid_exists(child.pid):
717
+ # This can be a race condition, so it is ok if the child is dead already
718
+ try:
719
+ child.kill()
720
+ except psutil.NoSuchProcess:
721
+ pass
722
+ # Same issue, this process might already be shutting down, so NoSuchProcess is ok.
723
+ try:
724
+ proc.kill()
725
+ except psutil.NoSuchProcess:
726
+ pass
727
+ self._server_pid = None
728
+ self._new_status_file(new=False)
729
+
730
+ def update(self, temporal: bool = False, line_width: float = 0.0) -> None:
731
+ """Update the geometry in Omniverse
732
+
733
+ Export the current EnSight scene to the current Omniverse connection.
734
+
735
+ Parameters
736
+ ----------
737
+ temporal : bool
738
+ If True, export all timesteps.
739
+ line_width : float
740
+ If set to a non-zero value, lines will be exported with this thickness.
741
+ This feature is only available in 2025 R2 and later.
742
+ """
743
+ update_cmd = "dynamicscenegraph://localhost/client/update"
744
+ prefix = "?"
745
+ if temporal:
746
+ update_cmd += f"{prefix}timesteps=1"
747
+ prefix = "&"
748
+ if line_width != 0.0:
749
+ add_linewidth = False
750
+ if isinstance(self._ensight, ModuleType):
751
+ add_linewidth = True
752
+ else:
753
+ # only in 2025 R2 and beyond
754
+ if self._ensight._session.ensight_version_check("2025 R2", exception=False):
755
+ add_linewidth = True
756
+ if add_linewidth:
757
+ update_cmd += f"{prefix}ANSYS_linewidth={line_width}"
758
+ prefix = "&"
759
+ self._check_modules()
760
+ if not self.is_running_omniverse():
761
+ raise RuntimeError("No Omniverse server connection is currently active.")
762
+ if not isinstance(self._ensight, ModuleType):
763
+ self._ensight._session.ensight_version_check("2023 R2")
764
+ cmd = f'enspyqtgui_int.dynamic_scene_graph_command("{update_cmd}")'
765
+ self._ensight._session.cmd(cmd, do_eval=False)
766
+ else:
767
+ import enspyqtgui_int
768
+
769
+ enspyqtgui_int.dynamic_scene_graph_command(f"{update_cmd}")