ansys-pyensight-core 0.8.12__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ansys-pyensight-core might be problematic. Click here for more details.
- ansys/pyensight/core/common.py +216 -216
- ansys/pyensight/core/ensight_grpc.py +432 -432
- ansys/pyensight/core/libuserd.py +1953 -1953
- ansys/pyensight/core/renderable.py +853 -853
- ansys/pyensight/core/session.py +1820 -1820
- ansys/pyensight/core/utils/dsg_server.py +1085 -934
- ansys/pyensight/core/utils/export.py +584 -584
- ansys/pyensight/core/utils/omniverse.py +362 -362
- ansys/pyensight/core/utils/omniverse_cli.py +520 -490
- ansys/pyensight/core/utils/omniverse_dsg_server.py +882 -692
- ansys/pyensight/core/utils/omniverse_glb_server.py +631 -625
- ansys/pyensight/core/utils/parts.py +1199 -1199
- {ansys_pyensight_core-0.8.12.dist-info → ansys_pyensight_core-0.9.0.dist-info}/METADATA +3 -3
- {ansys_pyensight_core-0.8.12.dist-info → ansys_pyensight_core-0.9.0.dist-info}/RECORD +16 -16
- {ansys_pyensight_core-0.8.12.dist-info → ansys_pyensight_core-0.9.0.dist-info}/WHEEL +1 -1
- {ansys_pyensight_core-0.8.12.dist-info → ansys_pyensight_core-0.9.0.dist-info}/LICENSE +0 -0
|
@@ -1,362 +1,362 @@
|
|
|
1
|
-
import glob
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import platform
|
|
5
|
-
import subprocess
|
|
6
|
-
import sys
|
|
7
|
-
import tempfile
|
|
8
|
-
from types import ModuleType
|
|
9
|
-
from typing import TYPE_CHECKING, Optional, Union
|
|
10
|
-
import uuid
|
|
11
|
-
|
|
12
|
-
import psutil
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
try:
|
|
16
|
-
import ensight
|
|
17
|
-
except ImportError:
|
|
18
|
-
from ansys.api.pyensight import ensight_api
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class Omniverse:
|
|
22
|
-
"""Provides the ``ensight.utils.omniverse`` interface.
|
|
23
|
-
|
|
24
|
-
The omniverse class methods provide an interface between an EnSight session
|
|
25
|
-
and an Omniverse instance. See :ref:`omniverse_info` for additional details.
|
|
26
|
-
|
|
27
|
-
Parameters
|
|
28
|
-
----------
|
|
29
|
-
interface: Union["ensight_api.ensight", "ensight"]
|
|
30
|
-
Entity that provides the ``ensight`` namespace. In the case of
|
|
31
|
-
EnSight Python, the ``ensight`` module is passed. In the case
|
|
32
|
-
of PyEnSight, ``Session.ensight`` is passed.
|
|
33
|
-
|
|
34
|
-
Notes
|
|
35
|
-
-----
|
|
36
|
-
This interface is only available when using pyensight (they do not work with
|
|
37
|
-
the ensight Python interpreter) and the module must be used in an interpreter
|
|
38
|
-
that includes the Omniverse Python modules (e.g. omni and pxr). Only a single
|
|
39
|
-
Omniverse connection can be established within a single pyensight session.
|
|
40
|
-
|
|
41
|
-
Examples
|
|
42
|
-
--------
|
|
43
|
-
|
|
44
|
-
>>> from ansys.pyensight.core import LocalLauncher
|
|
45
|
-
>>> session = LocalLauncher().start()
|
|
46
|
-
>>> ov = session.ensight.utils.omniverse
|
|
47
|
-
>>> ov.create_connection(r"D:\Omniverse\Example")
|
|
48
|
-
>>> ov.update()
|
|
49
|
-
>>> ov.close_connection()
|
|
50
|
-
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
def __init__(self, interface: Union["ensight_api.ensight", "ensight"]):
|
|
54
|
-
self._ensight = interface
|
|
55
|
-
self._server_pid: Optional[int] = None
|
|
56
|
-
self._interpreter: str = ""
|
|
57
|
-
self._status_filename: str = ""
|
|
58
|
-
|
|
59
|
-
@staticmethod
|
|
60
|
-
def find_kit_filename(fallback_directory: Optional[str] = None) -> Optional[str]:
|
|
61
|
-
"""
|
|
62
|
-
Use a combination of the current omniverse application and the information
|
|
63
|
-
in the local .nvidia-omniverse/config/omniverse.toml file to come up with
|
|
64
|
-
the pathname of a kit executable suitable for hosting another copy of the
|
|
65
|
-
ansys.geometry.server kit.
|
|
66
|
-
|
|
67
|
-
Returns
|
|
68
|
-
-------
|
|
69
|
-
Optional[str]
|
|
70
|
-
The pathname of a kit executable or None
|
|
71
|
-
|
|
72
|
-
"""
|
|
73
|
-
# parse the toml config file for the location of the installed apps
|
|
74
|
-
try:
|
|
75
|
-
import tomllib
|
|
76
|
-
except ModuleNotFoundError:
|
|
77
|
-
import pip._vendor.tomli as tomllib
|
|
78
|
-
|
|
79
|
-
homedir = os.path.expanduser("~")
|
|
80
|
-
ov_config = os.path.join(homedir, ".nvidia-omniverse", "config", "omniverse.toml")
|
|
81
|
-
if not os.path.exists(ov_config):
|
|
82
|
-
return None
|
|
83
|
-
# read the Omniverse configuration toml file
|
|
84
|
-
with open(ov_config, "r") as ov_file:
|
|
85
|
-
ov_data = ov_file.read()
|
|
86
|
-
config = tomllib.loads(ov_data)
|
|
87
|
-
appdir = config.get("paths", {}).get("library_root", fallback_directory)
|
|
88
|
-
|
|
89
|
-
# If we are running inside an Omniverse app, use that information
|
|
90
|
-
try:
|
|
91
|
-
import omni.kit.app
|
|
92
|
-
|
|
93
|
-
# get the current application
|
|
94
|
-
app = omni.kit.app.get_app()
|
|
95
|
-
app_name = app.get_app_filename().split(".")[-1]
|
|
96
|
-
app_version = app.get_app_version().split("-")[0]
|
|
97
|
-
# and where it is installed
|
|
98
|
-
appdir = os.path.join(appdir, f"{app_name}-{app_version}")
|
|
99
|
-
except ModuleNotFoundError:
|
|
100
|
-
# Names should be like: "C:\\Users\\foo\\AppData\\Local\\ov\\pkg\\create-2023.2.3\\launcher.toml"
|
|
101
|
-
target = None
|
|
102
|
-
target_version = None
|
|
103
|
-
for d in glob.glob(os.path.join(appdir, "*", "launcher.toml")):
|
|
104
|
-
test_dir = os.path.dirname(d)
|
|
105
|
-
# the name will be something like "create-2023.2.3"
|
|
106
|
-
name = os.path.basename(test_dir).split("-")
|
|
107
|
-
if len(name) != 2:
|
|
108
|
-
continue
|
|
109
|
-
if name[0] not in ("kit", "create", "view"):
|
|
110
|
-
continue
|
|
111
|
-
if (target_version is None) or (name[1] > target_version):
|
|
112
|
-
target = test_dir
|
|
113
|
-
target_version = name[1]
|
|
114
|
-
if target is None:
|
|
115
|
-
return None
|
|
116
|
-
appdir = target
|
|
117
|
-
|
|
118
|
-
# Windows: 'kit.bat' in '.' or 'kit' followed by 'kit.exe' in '.' or 'kit'
|
|
119
|
-
# Linux: 'kit.sh' in '.' or 'kit' followed by 'kit' in '.' or 'kit'
|
|
120
|
-
exe_names = ["kit.sh", "kit"]
|
|
121
|
-
if sys.platform.startswith("win"):
|
|
122
|
-
exe_names = ["kit.bat", "kit.exe"]
|
|
123
|
-
|
|
124
|
-
# look in 4 places...
|
|
125
|
-
for dir_name in [appdir, os.path.join(appdir, "kit")]:
|
|
126
|
-
for exe_name in exe_names:
|
|
127
|
-
if os.path.exists(os.path.join(dir_name, exe_name)):
|
|
128
|
-
return os.path.join(dir_name, exe_name)
|
|
129
|
-
|
|
130
|
-
return None
|
|
131
|
-
|
|
132
|
-
def _check_modules(self) -> None:
|
|
133
|
-
"""Verify that the Python interpreter is correct
|
|
134
|
-
|
|
135
|
-
Check for module dependencies. If not present, raise an exception.
|
|
136
|
-
|
|
137
|
-
Raises
|
|
138
|
-
------
|
|
139
|
-
RuntimeError
|
|
140
|
-
if the necessary modules are missing.
|
|
141
|
-
|
|
142
|
-
"""
|
|
143
|
-
# One time check for this
|
|
144
|
-
if len(self._interpreter):
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
# if a module, then we are inside EnSight
|
|
148
|
-
if isinstance(self._ensight, ModuleType): # pragma: no cover
|
|
149
|
-
# in this case, we can just use cpython
|
|
150
|
-
import ceiversion
|
|
151
|
-
import enve
|
|
152
|
-
|
|
153
|
-
cei_home = os.environ.get("CEI_HOME", enve.home())
|
|
154
|
-
self._interpreter = os.path.join(cei_home, "bin", f"cpython{ceiversion.apex_suffix}")
|
|
155
|
-
if platform.system() == "Windows":
|
|
156
|
-
self._interpreter += ".bat"
|
|
157
|
-
return
|
|
158
|
-
# Using the python interpreter running this code
|
|
159
|
-
self._interpreter = sys.executable
|
|
160
|
-
|
|
161
|
-
# in the future, these will be part of the pyensight wheel
|
|
162
|
-
# dependencies, but for now we include this check.
|
|
163
|
-
try:
|
|
164
|
-
import pxr # noqa: F401
|
|
165
|
-
import pygltflib # noqa: F401
|
|
166
|
-
except Exception:
|
|
167
|
-
raise RuntimeError("Unable to detect omniverse dependencies: usd-core, pygltflib.")
|
|
168
|
-
|
|
169
|
-
def is_running_omniverse(self) -> bool:
|
|
170
|
-
"""Check that an Omniverse connection is active
|
|
171
|
-
|
|
172
|
-
Returns
|
|
173
|
-
-------
|
|
174
|
-
bool
|
|
175
|
-
True if the connection is active, False otherwise.
|
|
176
|
-
"""
|
|
177
|
-
if self._server_pid is None:
|
|
178
|
-
return False
|
|
179
|
-
if psutil.pid_exists(self._server_pid):
|
|
180
|
-
return True
|
|
181
|
-
self._server_pid = None
|
|
182
|
-
return False
|
|
183
|
-
|
|
184
|
-
def create_connection(
|
|
185
|
-
self,
|
|
186
|
-
omniverse_path: str,
|
|
187
|
-
include_camera: bool = False,
|
|
188
|
-
normalize_geometry: bool = False,
|
|
189
|
-
temporal: bool = False,
|
|
190
|
-
live: bool = True,
|
|
191
|
-
debug_filename: str = "",
|
|
192
|
-
time_scale: float = 1.0,
|
|
193
|
-
options: dict = {},
|
|
194
|
-
) -> None:
|
|
195
|
-
"""Ensure that an EnSight dsg -> omniverse server is running
|
|
196
|
-
|
|
197
|
-
Connect the current EnSight session to an Omniverse server.
|
|
198
|
-
This is done by launching a new service that makes a dynamic scene graph
|
|
199
|
-
connection to the EnSight session and pushes updates to the Omniverse server.
|
|
200
|
-
The initial EnSight scene will be pushed after the connection is established.
|
|
201
|
-
|
|
202
|
-
Parameters
|
|
203
|
-
----------
|
|
204
|
-
omniverse_path : str
|
|
205
|
-
The directory name where the USD files should be saved. For example:
|
|
206
|
-
"C:/Users/test/OV/usdfiles"
|
|
207
|
-
include_camera : bool
|
|
208
|
-
If True, apply the EnSight camera to the Omniverse scene. This option
|
|
209
|
-
should be used if the target viewer is in AR/VR mode. Defaults to False.
|
|
210
|
-
normalize_geometry : bool
|
|
211
|
-
Omniverse units are in meters. If the source dataset is not in the correct
|
|
212
|
-
unit system or is just too large/small, this option will remap the geometry
|
|
213
|
-
to a unit cube. Defaults to False.
|
|
214
|
-
temporal : bool
|
|
215
|
-
If True, save all timesteps.
|
|
216
|
-
live : bool
|
|
217
|
-
If True, one can call 'update()' to send updated geometry to Omniverse.
|
|
218
|
-
If False, the Omniverse connection will push a single update and then
|
|
219
|
-
disconnect. Defaults to True.
|
|
220
|
-
time_scale : float
|
|
221
|
-
Multiply all EnSight time values by this factor before exporting to Omniverse.
|
|
222
|
-
The default is 1.0.
|
|
223
|
-
debug_filename : str
|
|
224
|
-
If the name of a file is provided, it will be used to save logging information on
|
|
225
|
-
the connection between EnSight and Omniverse. This option is no longer supported,
|
|
226
|
-
but the API remains for backwards compatibility.
|
|
227
|
-
options : dict
|
|
228
|
-
Allows for a fallback for the grpc host/port and the security token.
|
|
229
|
-
"""
|
|
230
|
-
if not isinstance(self._ensight, ModuleType):
|
|
231
|
-
self._ensight._session.ensight_version_check("2023 R2")
|
|
232
|
-
self._check_modules()
|
|
233
|
-
if self.is_running_omniverse():
|
|
234
|
-
raise RuntimeError("An Omniverse server connection is already active.")
|
|
235
|
-
if not isinstance(self._ensight, ModuleType):
|
|
236
|
-
# Make sure the internal ui module is loaded
|
|
237
|
-
self._ensight._session.cmd("import enspyqtgui_int", do_eval=False)
|
|
238
|
-
# Get the gRPC connection details and use them to launch the service
|
|
239
|
-
port = self._ensight._session.grpc.port()
|
|
240
|
-
hostname = self._ensight._session.grpc.host
|
|
241
|
-
token = self._ensight._session.grpc.security_token
|
|
242
|
-
else:
|
|
243
|
-
hostname = options.get("host", "127.0.0.1")
|
|
244
|
-
port = options.get("port", 12345)
|
|
245
|
-
token = options.get("security", "")
|
|
246
|
-
|
|
247
|
-
# Launch the server via the 'ansys.pyensight.core.utils.omniverse_cli' module
|
|
248
|
-
dsg_uri = f"grpc://{hostname}:{port}"
|
|
249
|
-
cmd = [self._interpreter]
|
|
250
|
-
cmd.extend(["-m", "ansys.pyensight.core.utils.omniverse_cli"])
|
|
251
|
-
cmd.append(omniverse_path)
|
|
252
|
-
if token:
|
|
253
|
-
cmd.extend(["--security_token", token])
|
|
254
|
-
if temporal:
|
|
255
|
-
cmd.extend(["--temporal", "true"])
|
|
256
|
-
if not include_camera:
|
|
257
|
-
cmd.extend(["--include_camera", "false"])
|
|
258
|
-
if normalize_geometry:
|
|
259
|
-
cmd.extend(["--normalize_geometry", "true"])
|
|
260
|
-
if time_scale != 1.0:
|
|
261
|
-
cmd.extend(["--time_scale", str(time_scale)])
|
|
262
|
-
if not live:
|
|
263
|
-
cmd.extend(["--oneshot", "1"])
|
|
264
|
-
cmd.extend(["--dsg_uri", dsg_uri])
|
|
265
|
-
env_vars = os.environ.copy()
|
|
266
|
-
# we are launching the kit from EnSight or PyEnSight. In these cases, we
|
|
267
|
-
# inform the kit instance of:
|
|
268
|
-
# (1) the name of the "server status" file, if any
|
|
269
|
-
self._new_status_file()
|
|
270
|
-
env_vars["ANSYS_OV_SERVER_STATUS_FILENAME"] = self._status_filename
|
|
271
|
-
process = subprocess.Popen(cmd, close_fds=True, env=env_vars)
|
|
272
|
-
self._server_pid = process.pid
|
|
273
|
-
|
|
274
|
-
def _new_status_file(self, new=True) -> None:
|
|
275
|
-
"""
|
|
276
|
-
Remove any existing status file and create a new one if requested.
|
|
277
|
-
|
|
278
|
-
Parameters
|
|
279
|
-
----------
|
|
280
|
-
new : bool
|
|
281
|
-
If True, create a new status file.
|
|
282
|
-
"""
|
|
283
|
-
if self._status_filename:
|
|
284
|
-
try:
|
|
285
|
-
os.remove(self._status_filename)
|
|
286
|
-
except OSError:
|
|
287
|
-
pass
|
|
288
|
-
self._status_filename = ""
|
|
289
|
-
if new:
|
|
290
|
-
self._status_filename = os.path.join(
|
|
291
|
-
tempfile.gettempdir(), str(uuid.uuid1()) + "_gs_status.txt"
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
def read_status_file(self) -> dict:
|
|
295
|
-
"""Read the status file and return its contents as a dictionary.
|
|
296
|
-
|
|
297
|
-
Note: this can fail if the file is being written to when this call is made, so expect
|
|
298
|
-
failures.
|
|
299
|
-
|
|
300
|
-
Returns
|
|
301
|
-
-------
|
|
302
|
-
Optional[dict]
|
|
303
|
-
A dictionary with the fields 'status', 'start_time', 'processed_buffers', 'total_buffers' or empty
|
|
304
|
-
"""
|
|
305
|
-
if not self._status_filename:
|
|
306
|
-
return {}
|
|
307
|
-
try:
|
|
308
|
-
with open(self._status_filename, "r") as status_file:
|
|
309
|
-
data = json.load(status_file)
|
|
310
|
-
except Exception:
|
|
311
|
-
return {}
|
|
312
|
-
return data
|
|
313
|
-
|
|
314
|
-
def close_connection(self) -> None:
|
|
315
|
-
"""Shut down the open EnSight dsg -> omniverse server
|
|
316
|
-
|
|
317
|
-
Break the connection between the EnSight instance and Omniverse.
|
|
318
|
-
|
|
319
|
-
"""
|
|
320
|
-
self._check_modules()
|
|
321
|
-
if not self.is_running_omniverse():
|
|
322
|
-
return
|
|
323
|
-
proc = psutil.Process(self._server_pid)
|
|
324
|
-
for child in proc.children(recursive=True):
|
|
325
|
-
if psutil.pid_exists(child.pid):
|
|
326
|
-
# This can be a race condition, so it is ok if the child is dead already
|
|
327
|
-
try:
|
|
328
|
-
child.kill()
|
|
329
|
-
except psutil.NoSuchProcess:
|
|
330
|
-
pass
|
|
331
|
-
# Same issue, this process might already be shutting down, so NoSuchProcess is ok.
|
|
332
|
-
try:
|
|
333
|
-
proc.kill()
|
|
334
|
-
except psutil.NoSuchProcess:
|
|
335
|
-
pass
|
|
336
|
-
self._server_pid = None
|
|
337
|
-
self._new_status_file(new=False)
|
|
338
|
-
|
|
339
|
-
def update(self, temporal: bool = False) -> None:
|
|
340
|
-
"""Update the geometry in Omniverse
|
|
341
|
-
|
|
342
|
-
Export the current EnSight scene to the current Omniverse connection.
|
|
343
|
-
|
|
344
|
-
Parameters
|
|
345
|
-
----------
|
|
346
|
-
temporal : bool
|
|
347
|
-
If True, export all timesteps.
|
|
348
|
-
"""
|
|
349
|
-
update_cmd = "dynamicscenegraph://localhost/client/update"
|
|
350
|
-
if temporal:
|
|
351
|
-
update_cmd += "?timesteps=1"
|
|
352
|
-
self._check_modules()
|
|
353
|
-
if not self.is_running_omniverse():
|
|
354
|
-
raise RuntimeError("No Omniverse server connection is currently active.")
|
|
355
|
-
if not isinstance(self._ensight, ModuleType):
|
|
356
|
-
self._ensight._session.ensight_version_check("2023 R2")
|
|
357
|
-
cmd = f'enspyqtgui_int.dynamic_scene_graph_command("{update_cmd}")'
|
|
358
|
-
self._ensight._session.cmd(cmd, do_eval=False)
|
|
359
|
-
else:
|
|
360
|
-
import enspyqtgui_int
|
|
361
|
-
|
|
362
|
-
enspyqtgui_int.dynamic_scene_graph_command(f"{update_cmd}")
|
|
1
|
+
import glob
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from types import ModuleType
|
|
9
|
+
from typing import TYPE_CHECKING, Optional, Union
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
import psutil
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
try:
|
|
16
|
+
import ensight
|
|
17
|
+
except ImportError:
|
|
18
|
+
from ansys.api.pyensight import ensight_api
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Omniverse:
|
|
22
|
+
"""Provides the ``ensight.utils.omniverse`` interface.
|
|
23
|
+
|
|
24
|
+
The omniverse class methods provide an interface between an EnSight session
|
|
25
|
+
and an Omniverse instance. See :ref:`omniverse_info` for additional details.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
interface: Union["ensight_api.ensight", "ensight"]
|
|
30
|
+
Entity that provides the ``ensight`` namespace. In the case of
|
|
31
|
+
EnSight Python, the ``ensight`` module is passed. In the case
|
|
32
|
+
of PyEnSight, ``Session.ensight`` is passed.
|
|
33
|
+
|
|
34
|
+
Notes
|
|
35
|
+
-----
|
|
36
|
+
This interface is only available when using pyensight (they do not work with
|
|
37
|
+
the ensight Python interpreter) and the module must be used in an interpreter
|
|
38
|
+
that includes the Omniverse Python modules (e.g. omni and pxr). Only a single
|
|
39
|
+
Omniverse connection can be established within a single pyensight session.
|
|
40
|
+
|
|
41
|
+
Examples
|
|
42
|
+
--------
|
|
43
|
+
|
|
44
|
+
>>> from ansys.pyensight.core import LocalLauncher
|
|
45
|
+
>>> session = LocalLauncher().start()
|
|
46
|
+
>>> ov = session.ensight.utils.omniverse
|
|
47
|
+
>>> ov.create_connection(r"D:\Omniverse\Example")
|
|
48
|
+
>>> ov.update()
|
|
49
|
+
>>> ov.close_connection()
|
|
50
|
+
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, interface: Union["ensight_api.ensight", "ensight"]):
|
|
54
|
+
self._ensight = interface
|
|
55
|
+
self._server_pid: Optional[int] = None
|
|
56
|
+
self._interpreter: str = ""
|
|
57
|
+
self._status_filename: str = ""
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def find_kit_filename(fallback_directory: Optional[str] = None) -> Optional[str]:
|
|
61
|
+
"""
|
|
62
|
+
Use a combination of the current omniverse application and the information
|
|
63
|
+
in the local .nvidia-omniverse/config/omniverse.toml file to come up with
|
|
64
|
+
the pathname of a kit executable suitable for hosting another copy of the
|
|
65
|
+
ansys.geometry.server kit.
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
Optional[str]
|
|
70
|
+
The pathname of a kit executable or None
|
|
71
|
+
|
|
72
|
+
"""
|
|
73
|
+
# parse the toml config file for the location of the installed apps
|
|
74
|
+
try:
|
|
75
|
+
import tomllib
|
|
76
|
+
except ModuleNotFoundError:
|
|
77
|
+
import pip._vendor.tomli as tomllib
|
|
78
|
+
|
|
79
|
+
homedir = os.path.expanduser("~")
|
|
80
|
+
ov_config = os.path.join(homedir, ".nvidia-omniverse", "config", "omniverse.toml")
|
|
81
|
+
if not os.path.exists(ov_config):
|
|
82
|
+
return None
|
|
83
|
+
# read the Omniverse configuration toml file
|
|
84
|
+
with open(ov_config, "r") as ov_file:
|
|
85
|
+
ov_data = ov_file.read()
|
|
86
|
+
config = tomllib.loads(ov_data)
|
|
87
|
+
appdir = config.get("paths", {}).get("library_root", fallback_directory)
|
|
88
|
+
|
|
89
|
+
# If we are running inside an Omniverse app, use that information
|
|
90
|
+
try:
|
|
91
|
+
import omni.kit.app
|
|
92
|
+
|
|
93
|
+
# get the current application
|
|
94
|
+
app = omni.kit.app.get_app()
|
|
95
|
+
app_name = app.get_app_filename().split(".")[-1]
|
|
96
|
+
app_version = app.get_app_version().split("-")[0]
|
|
97
|
+
# and where it is installed
|
|
98
|
+
appdir = os.path.join(appdir, f"{app_name}-{app_version}")
|
|
99
|
+
except ModuleNotFoundError:
|
|
100
|
+
# Names should be like: "C:\\Users\\foo\\AppData\\Local\\ov\\pkg\\create-2023.2.3\\launcher.toml"
|
|
101
|
+
target = None
|
|
102
|
+
target_version = None
|
|
103
|
+
for d in glob.glob(os.path.join(appdir, "*", "launcher.toml")):
|
|
104
|
+
test_dir = os.path.dirname(d)
|
|
105
|
+
# the name will be something like "create-2023.2.3"
|
|
106
|
+
name = os.path.basename(test_dir).split("-")
|
|
107
|
+
if len(name) != 2:
|
|
108
|
+
continue
|
|
109
|
+
if name[0] not in ("kit", "create", "view"):
|
|
110
|
+
continue
|
|
111
|
+
if (target_version is None) or (name[1] > target_version):
|
|
112
|
+
target = test_dir
|
|
113
|
+
target_version = name[1]
|
|
114
|
+
if target is None:
|
|
115
|
+
return None
|
|
116
|
+
appdir = target
|
|
117
|
+
|
|
118
|
+
# Windows: 'kit.bat' in '.' or 'kit' followed by 'kit.exe' in '.' or 'kit'
|
|
119
|
+
# Linux: 'kit.sh' in '.' or 'kit' followed by 'kit' in '.' or 'kit'
|
|
120
|
+
exe_names = ["kit.sh", "kit"]
|
|
121
|
+
if sys.platform.startswith("win"):
|
|
122
|
+
exe_names = ["kit.bat", "kit.exe"]
|
|
123
|
+
|
|
124
|
+
# look in 4 places...
|
|
125
|
+
for dir_name in [appdir, os.path.join(appdir, "kit")]:
|
|
126
|
+
for exe_name in exe_names:
|
|
127
|
+
if os.path.exists(os.path.join(dir_name, exe_name)):
|
|
128
|
+
return os.path.join(dir_name, exe_name)
|
|
129
|
+
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
def _check_modules(self) -> None:
|
|
133
|
+
"""Verify that the Python interpreter is correct
|
|
134
|
+
|
|
135
|
+
Check for module dependencies. If not present, raise an exception.
|
|
136
|
+
|
|
137
|
+
Raises
|
|
138
|
+
------
|
|
139
|
+
RuntimeError
|
|
140
|
+
if the necessary modules are missing.
|
|
141
|
+
|
|
142
|
+
"""
|
|
143
|
+
# One time check for this
|
|
144
|
+
if len(self._interpreter):
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# if a module, then we are inside EnSight
|
|
148
|
+
if isinstance(self._ensight, ModuleType): # pragma: no cover
|
|
149
|
+
# in this case, we can just use cpython
|
|
150
|
+
import ceiversion
|
|
151
|
+
import enve
|
|
152
|
+
|
|
153
|
+
cei_home = os.environ.get("CEI_HOME", enve.home())
|
|
154
|
+
self._interpreter = os.path.join(cei_home, "bin", f"cpython{ceiversion.apex_suffix}")
|
|
155
|
+
if platform.system() == "Windows":
|
|
156
|
+
self._interpreter += ".bat"
|
|
157
|
+
return
|
|
158
|
+
# Using the python interpreter running this code
|
|
159
|
+
self._interpreter = sys.executable
|
|
160
|
+
|
|
161
|
+
# in the future, these will be part of the pyensight wheel
|
|
162
|
+
# dependencies, but for now we include this check.
|
|
163
|
+
try:
|
|
164
|
+
import pxr # noqa: F401
|
|
165
|
+
import pygltflib # noqa: F401
|
|
166
|
+
except Exception:
|
|
167
|
+
raise RuntimeError("Unable to detect omniverse dependencies: usd-core, pygltflib.")
|
|
168
|
+
|
|
169
|
+
def is_running_omniverse(self) -> bool:
|
|
170
|
+
"""Check that an Omniverse connection is active
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
bool
|
|
175
|
+
True if the connection is active, False otherwise.
|
|
176
|
+
"""
|
|
177
|
+
if self._server_pid is None:
|
|
178
|
+
return False
|
|
179
|
+
if psutil.pid_exists(self._server_pid):
|
|
180
|
+
return True
|
|
181
|
+
self._server_pid = None
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
def create_connection(
|
|
185
|
+
self,
|
|
186
|
+
omniverse_path: str,
|
|
187
|
+
include_camera: bool = False,
|
|
188
|
+
normalize_geometry: bool = False,
|
|
189
|
+
temporal: bool = False,
|
|
190
|
+
live: bool = True,
|
|
191
|
+
debug_filename: str = "",
|
|
192
|
+
time_scale: float = 1.0,
|
|
193
|
+
options: dict = {},
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Ensure that an EnSight dsg -> omniverse server is running
|
|
196
|
+
|
|
197
|
+
Connect the current EnSight session to an Omniverse server.
|
|
198
|
+
This is done by launching a new service that makes a dynamic scene graph
|
|
199
|
+
connection to the EnSight session and pushes updates to the Omniverse server.
|
|
200
|
+
The initial EnSight scene will be pushed after the connection is established.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
omniverse_path : str
|
|
205
|
+
The directory name where the USD files should be saved. For example:
|
|
206
|
+
"C:/Users/test/OV/usdfiles"
|
|
207
|
+
include_camera : bool
|
|
208
|
+
If True, apply the EnSight camera to the Omniverse scene. This option
|
|
209
|
+
should be used if the target viewer is in AR/VR mode. Defaults to False.
|
|
210
|
+
normalize_geometry : bool
|
|
211
|
+
Omniverse units are in meters. If the source dataset is not in the correct
|
|
212
|
+
unit system or is just too large/small, this option will remap the geometry
|
|
213
|
+
to a unit cube. Defaults to False.
|
|
214
|
+
temporal : bool
|
|
215
|
+
If True, save all timesteps.
|
|
216
|
+
live : bool
|
|
217
|
+
If True, one can call 'update()' to send updated geometry to Omniverse.
|
|
218
|
+
If False, the Omniverse connection will push a single update and then
|
|
219
|
+
disconnect. Defaults to True.
|
|
220
|
+
time_scale : float
|
|
221
|
+
Multiply all EnSight time values by this factor before exporting to Omniverse.
|
|
222
|
+
The default is 1.0.
|
|
223
|
+
debug_filename : str
|
|
224
|
+
If the name of a file is provided, it will be used to save logging information on
|
|
225
|
+
the connection between EnSight and Omniverse. This option is no longer supported,
|
|
226
|
+
but the API remains for backwards compatibility.
|
|
227
|
+
options : dict
|
|
228
|
+
Allows for a fallback for the grpc host/port and the security token.
|
|
229
|
+
"""
|
|
230
|
+
if not isinstance(self._ensight, ModuleType):
|
|
231
|
+
self._ensight._session.ensight_version_check("2023 R2")
|
|
232
|
+
self._check_modules()
|
|
233
|
+
if self.is_running_omniverse():
|
|
234
|
+
raise RuntimeError("An Omniverse server connection is already active.")
|
|
235
|
+
if not isinstance(self._ensight, ModuleType):
|
|
236
|
+
# Make sure the internal ui module is loaded
|
|
237
|
+
self._ensight._session.cmd("import enspyqtgui_int", do_eval=False)
|
|
238
|
+
# Get the gRPC connection details and use them to launch the service
|
|
239
|
+
port = self._ensight._session.grpc.port()
|
|
240
|
+
hostname = self._ensight._session.grpc.host
|
|
241
|
+
token = self._ensight._session.grpc.security_token
|
|
242
|
+
else:
|
|
243
|
+
hostname = options.get("host", "127.0.0.1")
|
|
244
|
+
port = options.get("port", 12345)
|
|
245
|
+
token = options.get("security", "")
|
|
246
|
+
|
|
247
|
+
# Launch the server via the 'ansys.pyensight.core.utils.omniverse_cli' module
|
|
248
|
+
dsg_uri = f"grpc://{hostname}:{port}"
|
|
249
|
+
cmd = [self._interpreter]
|
|
250
|
+
cmd.extend(["-m", "ansys.pyensight.core.utils.omniverse_cli"])
|
|
251
|
+
cmd.append(omniverse_path)
|
|
252
|
+
if token:
|
|
253
|
+
cmd.extend(["--security_token", token])
|
|
254
|
+
if temporal:
|
|
255
|
+
cmd.extend(["--temporal", "true"])
|
|
256
|
+
if not include_camera:
|
|
257
|
+
cmd.extend(["--include_camera", "false"])
|
|
258
|
+
if normalize_geometry:
|
|
259
|
+
cmd.extend(["--normalize_geometry", "true"])
|
|
260
|
+
if time_scale != 1.0:
|
|
261
|
+
cmd.extend(["--time_scale", str(time_scale)])
|
|
262
|
+
if not live:
|
|
263
|
+
cmd.extend(["--oneshot", "1"])
|
|
264
|
+
cmd.extend(["--dsg_uri", dsg_uri])
|
|
265
|
+
env_vars = os.environ.copy()
|
|
266
|
+
# we are launching the kit from EnSight or PyEnSight. In these cases, we
|
|
267
|
+
# inform the kit instance of:
|
|
268
|
+
# (1) the name of the "server status" file, if any
|
|
269
|
+
self._new_status_file()
|
|
270
|
+
env_vars["ANSYS_OV_SERVER_STATUS_FILENAME"] = self._status_filename
|
|
271
|
+
process = subprocess.Popen(cmd, close_fds=True, env=env_vars)
|
|
272
|
+
self._server_pid = process.pid
|
|
273
|
+
|
|
274
|
+
def _new_status_file(self, new=True) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Remove any existing status file and create a new one if requested.
|
|
277
|
+
|
|
278
|
+
Parameters
|
|
279
|
+
----------
|
|
280
|
+
new : bool
|
|
281
|
+
If True, create a new status file.
|
|
282
|
+
"""
|
|
283
|
+
if self._status_filename:
|
|
284
|
+
try:
|
|
285
|
+
os.remove(self._status_filename)
|
|
286
|
+
except OSError:
|
|
287
|
+
pass
|
|
288
|
+
self._status_filename = ""
|
|
289
|
+
if new:
|
|
290
|
+
self._status_filename = os.path.join(
|
|
291
|
+
tempfile.gettempdir(), str(uuid.uuid1()) + "_gs_status.txt"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def read_status_file(self) -> dict:
|
|
295
|
+
"""Read the status file and return its contents as a dictionary.
|
|
296
|
+
|
|
297
|
+
Note: this can fail if the file is being written to when this call is made, so expect
|
|
298
|
+
failures.
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
Optional[dict]
|
|
303
|
+
A dictionary with the fields 'status', 'start_time', 'processed_buffers', 'total_buffers' or empty
|
|
304
|
+
"""
|
|
305
|
+
if not self._status_filename:
|
|
306
|
+
return {}
|
|
307
|
+
try:
|
|
308
|
+
with open(self._status_filename, "r") as status_file:
|
|
309
|
+
data = json.load(status_file)
|
|
310
|
+
except Exception:
|
|
311
|
+
return {}
|
|
312
|
+
return data
|
|
313
|
+
|
|
314
|
+
def close_connection(self) -> None:
|
|
315
|
+
"""Shut down the open EnSight dsg -> omniverse server
|
|
316
|
+
|
|
317
|
+
Break the connection between the EnSight instance and Omniverse.
|
|
318
|
+
|
|
319
|
+
"""
|
|
320
|
+
self._check_modules()
|
|
321
|
+
if not self.is_running_omniverse():
|
|
322
|
+
return
|
|
323
|
+
proc = psutil.Process(self._server_pid)
|
|
324
|
+
for child in proc.children(recursive=True):
|
|
325
|
+
if psutil.pid_exists(child.pid):
|
|
326
|
+
# This can be a race condition, so it is ok if the child is dead already
|
|
327
|
+
try:
|
|
328
|
+
child.kill()
|
|
329
|
+
except psutil.NoSuchProcess:
|
|
330
|
+
pass
|
|
331
|
+
# Same issue, this process might already be shutting down, so NoSuchProcess is ok.
|
|
332
|
+
try:
|
|
333
|
+
proc.kill()
|
|
334
|
+
except psutil.NoSuchProcess:
|
|
335
|
+
pass
|
|
336
|
+
self._server_pid = None
|
|
337
|
+
self._new_status_file(new=False)
|
|
338
|
+
|
|
339
|
+
def update(self, temporal: bool = False) -> None:
|
|
340
|
+
"""Update the geometry in Omniverse
|
|
341
|
+
|
|
342
|
+
Export the current EnSight scene to the current Omniverse connection.
|
|
343
|
+
|
|
344
|
+
Parameters
|
|
345
|
+
----------
|
|
346
|
+
temporal : bool
|
|
347
|
+
If True, export all timesteps.
|
|
348
|
+
"""
|
|
349
|
+
update_cmd = "dynamicscenegraph://localhost/client/update"
|
|
350
|
+
if temporal:
|
|
351
|
+
update_cmd += "?timesteps=1"
|
|
352
|
+
self._check_modules()
|
|
353
|
+
if not self.is_running_omniverse():
|
|
354
|
+
raise RuntimeError("No Omniverse server connection is currently active.")
|
|
355
|
+
if not isinstance(self._ensight, ModuleType):
|
|
356
|
+
self._ensight._session.ensight_version_check("2023 R2")
|
|
357
|
+
cmd = f'enspyqtgui_int.dynamic_scene_graph_command("{update_cmd}")'
|
|
358
|
+
self._ensight._session.cmd(cmd, do_eval=False)
|
|
359
|
+
else:
|
|
360
|
+
import enspyqtgui_int
|
|
361
|
+
|
|
362
|
+
enspyqtgui_int.dynamic_scene_graph_command(f"{update_cmd}")
|