ansys-pyensight-core 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ansys/pyensight/core/__init__.py +41 -0
- ansys/pyensight/core/common.py +341 -0
- ansys/pyensight/core/deep_pixel_view.html +98 -0
- ansys/pyensight/core/dockerlauncher.py +1124 -0
- ansys/pyensight/core/dvs.py +872 -0
- ansys/pyensight/core/enscontext.py +345 -0
- ansys/pyensight/core/enshell_grpc.py +641 -0
- ansys/pyensight/core/ensight_grpc.py +874 -0
- ansys/pyensight/core/ensobj.py +515 -0
- ansys/pyensight/core/launch_ensight.py +296 -0
- ansys/pyensight/core/launcher.py +388 -0
- ansys/pyensight/core/libuserd.py +2110 -0
- ansys/pyensight/core/listobj.py +280 -0
- ansys/pyensight/core/locallauncher.py +579 -0
- ansys/pyensight/core/py.typed +0 -0
- ansys/pyensight/core/renderable.py +880 -0
- ansys/pyensight/core/session.py +1923 -0
- ansys/pyensight/core/sgeo_poll.html +24 -0
- ansys/pyensight/core/utils/__init__.py +21 -0
- ansys/pyensight/core/utils/adr.py +111 -0
- ansys/pyensight/core/utils/dsg_server.py +1220 -0
- ansys/pyensight/core/utils/export.py +606 -0
- ansys/pyensight/core/utils/omniverse.py +769 -0
- ansys/pyensight/core/utils/omniverse_cli.py +614 -0
- ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
- ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
- ansys/pyensight/core/utils/parts.py +1221 -0
- ansys/pyensight/core/utils/query.py +487 -0
- ansys/pyensight/core/utils/readers.py +300 -0
- ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
- ansys/pyensight/core/utils/support.py +128 -0
- ansys/pyensight/core/utils/variables.py +2019 -0
- ansys/pyensight/core/utils/views.py +674 -0
- ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
- ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
- ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
- ansys_pyensight_core-0.11.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,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}")
|