ansys-pyensight-core 0.8.8__py3-none-any.whl → 0.8.9__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/dockerlauncher.py +6 -0
- ansys/pyensight/core/launcher.py +6 -1
- ansys/pyensight/core/locallauncher.py +4 -0
- ansys/pyensight/core/session.py +1 -1
- ansys/pyensight/core/utils/dsg_server.py +227 -35
- ansys/pyensight/core/utils/omniverse.py +84 -24
- ansys/pyensight/core/utils/omniverse_cli.py +481 -0
- ansys/pyensight/core/utils/omniverse_dsg_server.py +236 -426
- ansys/pyensight/core/utils/omniverse_glb_server.py +279 -0
- {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.9.dist-info}/METADATA +11 -5
- ansys_pyensight_core-0.8.9.dist-info/RECORD +34 -0
- ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/__init__.py +0 -1
- ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/extension.py +0 -407
- ansys/pyensight/core/exts/ansys.geometry.service/config/extension.toml +0 -59
- ansys/pyensight/core/exts/ansys.geometry.service/data/icon.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.service/data/preview.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.service/docs/CHANGELOG.md +0 -11
- ansys/pyensight/core/exts/ansys.geometry.service/docs/README.md +0 -13
- ansys/pyensight/core/exts/ansys.geometry.service/docs/index.rst +0 -18
- ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/__init__.py +0 -1
- ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/extension.py +0 -193
- ansys/pyensight/core/exts/ansys.geometry.serviceui/config/extension.toml +0 -49
- ansys/pyensight/core/exts/ansys.geometry.serviceui/data/icon.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.serviceui/data/preview.png +0 -0
- ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/CHANGELOG.md +0 -11
- ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/README.md +0 -13
- ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/index.rst +0 -18
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_BaseColor.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_N.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_ORM.png +0 -0
- ansys/pyensight/core/utils/resources/Materials/Fieldstone.mdl +0 -54
- ansys_pyensight_core-0.8.8.dist-info/RECORD +0 -52
- {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.9.dist-info}/LICENSE +0 -0
- {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.9.dist-info}/WHEEL +0 -0
|
@@ -87,6 +87,9 @@ class DockerLauncher(Launcher):
|
|
|
87
87
|
Number of EnSight servers to use for SOS (Server of Server) mode.
|
|
88
88
|
This parameter is defined on the parent ``Launcher`` class, where
|
|
89
89
|
the default is ``None``, in which case SOS mode is not used.
|
|
90
|
+
additional_command_line_options: list, optional
|
|
91
|
+
Additional command line options to be used to launch EnSight.
|
|
92
|
+
Arguments that contain spaces are not supported.
|
|
90
93
|
|
|
91
94
|
Examples
|
|
92
95
|
--------
|
|
@@ -504,6 +507,9 @@ class DockerLauncher(Launcher):
|
|
|
504
507
|
|
|
505
508
|
vnc_url = "vnc://%%3Frfb_port=1999%%26use_auth=0"
|
|
506
509
|
ensight_args += " -vnc " + vnc_url
|
|
510
|
+
if self._additional_command_line_options:
|
|
511
|
+
ensight_args += " "
|
|
512
|
+
ensight_args += " ".join(self._additional_command_line_options)
|
|
507
513
|
|
|
508
514
|
logging.debug(f"Starting EnSight with args: {ensight_args}\n")
|
|
509
515
|
ret = self._enshell.start_ensight(ensight_args, ensight_env_vars)
|
ansys/pyensight/core/launcher.py
CHANGED
|
@@ -59,7 +59,10 @@ class Launcher:
|
|
|
59
59
|
enable_rest_api : bool, optional
|
|
60
60
|
Whether to enable the EnSight REST API. The default is ``False``.
|
|
61
61
|
This parameter is supported in EnSight 2024 R1 and later.
|
|
62
|
-
|
|
62
|
+
additional_command_line_options: list, optional
|
|
63
|
+
Additional command line options to be used to launch EnSight.
|
|
64
|
+
Please note, when using DockerLauncher, arguments that contain spaces
|
|
65
|
+
are not supported.
|
|
63
66
|
"""
|
|
64
67
|
|
|
65
68
|
def __init__(
|
|
@@ -68,6 +71,7 @@ class Launcher:
|
|
|
68
71
|
use_egl: bool = False,
|
|
69
72
|
use_sos: Optional[int] = None,
|
|
70
73
|
enable_rest_api: bool = False,
|
|
74
|
+
additional_command_line_options: Optional[List] = None,
|
|
71
75
|
) -> None:
|
|
72
76
|
self._timeout = timeout
|
|
73
77
|
self._use_egl_param_val: bool = use_egl
|
|
@@ -87,6 +91,7 @@ class Launcher:
|
|
|
87
91
|
self._egl_env_val = False
|
|
88
92
|
# a dict of any optional launcher specific query parameters for URLs
|
|
89
93
|
self._query_parameters: Dict[str, str] = {}
|
|
94
|
+
self._additional_command_line_options = additional_command_line_options
|
|
90
95
|
|
|
91
96
|
@property
|
|
92
97
|
def session_directory(self) -> str:
|
|
@@ -56,6 +56,8 @@ class LocalLauncher(Launcher):
|
|
|
56
56
|
Number of EnSight servers to use for SOS (Server of Server) mode.
|
|
57
57
|
This parameter is defined on the parent ``Launcher`` class, where
|
|
58
58
|
the default is ``None``, in which case SOS mode is not used.
|
|
59
|
+
additional_command_line_options: list, optional
|
|
60
|
+
Additional command line options to be used to launch EnSight.
|
|
59
61
|
|
|
60
62
|
Examples
|
|
61
63
|
--------
|
|
@@ -154,6 +156,8 @@ class LocalLauncher(Launcher):
|
|
|
154
156
|
vnc_url = f"vnc://%%3Frfb_port={self._ports[1]}%%26use_auth=0"
|
|
155
157
|
cmd.extend(["-vnc", vnc_url])
|
|
156
158
|
cmd.extend(["-ports", str(self._ports[4])])
|
|
159
|
+
if self._additional_command_line_options:
|
|
160
|
+
cmd.extend(self._additional_command_line_options)
|
|
157
161
|
|
|
158
162
|
use_egl = self._use_egl()
|
|
159
163
|
|
ansys/pyensight/core/session.py
CHANGED
|
@@ -1064,7 +1064,7 @@ class Session:
|
|
|
1064
1064
|
onlyfiles = [f for f in listdir(_utils_dir) if os.path.isfile(os.path.join(_utils_dir, f))]
|
|
1065
1065
|
for _basename in onlyfiles:
|
|
1066
1066
|
# skip over any files with the "_server" in their names
|
|
1067
|
-
if "_server" in _basename:
|
|
1067
|
+
if "_server" in _basename or "_cli" in _basename:
|
|
1068
1068
|
continue
|
|
1069
1069
|
_filename = os.path.join(_utils_dir, _basename)
|
|
1070
1070
|
try:
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
1
3
|
import logging
|
|
2
4
|
import os
|
|
3
5
|
import queue
|
|
4
6
|
import sys
|
|
5
7
|
import threading
|
|
8
|
+
import time
|
|
6
9
|
from typing import Any, Dict, List, Optional
|
|
7
10
|
|
|
8
11
|
from ansys.api.pyensight.v0 import dynamic_scene_graph_pb2
|
|
@@ -32,9 +35,10 @@ class Part(object):
|
|
|
32
35
|
self.normals = numpy.array([], dtype="float32")
|
|
33
36
|
self.normals_elem = False
|
|
34
37
|
self.tcoords = numpy.array([], dtype="float32")
|
|
35
|
-
self.tcoords_var_id: Optional[int] = None
|
|
36
38
|
self.tcoords_elem = False
|
|
39
|
+
self.node_sizes = numpy.array([], dtype="float32")
|
|
37
40
|
self.cmd: Optional[Any] = None
|
|
41
|
+
self.hash = hashlib.new("sha256")
|
|
38
42
|
self.reset()
|
|
39
43
|
|
|
40
44
|
def reset(self, cmd: Any = None) -> None:
|
|
@@ -46,6 +50,10 @@ class Part(object):
|
|
|
46
50
|
self.tcoords = numpy.array([], dtype="float32")
|
|
47
51
|
self.tcoords_var_id = None
|
|
48
52
|
self.tcoords_elem = False
|
|
53
|
+
self.node_sizes = numpy.array([], dtype="float32")
|
|
54
|
+
self.hash = hashlib.new("sha256")
|
|
55
|
+
if cmd is not None:
|
|
56
|
+
self.hash.update(cmd.hash.encode("utf-8"))
|
|
49
57
|
self.cmd = cmd
|
|
50
58
|
|
|
51
59
|
def update_geom(self, cmd: dynamic_scene_graph_pb2.UpdateGeom) -> None:
|
|
@@ -83,17 +91,25 @@ class Part(object):
|
|
|
83
91
|
):
|
|
84
92
|
# Get the variable definition
|
|
85
93
|
if cmd.variable_id in self.session.variables:
|
|
86
|
-
self.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
self.tcoords
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
self.
|
|
94
|
+
if self.cmd.color_variableid == cmd.variable_id: # type: ignore
|
|
95
|
+
# Receive the colorby var values
|
|
96
|
+
self.tcoords_elem = (
|
|
97
|
+
cmd.payload_type == dynamic_scene_graph_pb2.UpdateGeom.ELEM_VARIABLE
|
|
98
|
+
)
|
|
99
|
+
if self.tcoords.size != cmd.total_array_size:
|
|
100
|
+
self.tcoords = numpy.resize(self.tcoords, cmd.total_array_size)
|
|
101
|
+
self.tcoords[
|
|
102
|
+
cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)
|
|
103
|
+
] = cmd.flt_array
|
|
104
|
+
if self.cmd.node_size_variableid == cmd.variable_id: # type: ignore
|
|
105
|
+
# Receive the node size var values
|
|
106
|
+
if self.node_sizes.size != cmd.total_array_size:
|
|
107
|
+
self.node_sizes = numpy.resize(self.node_sizes, cmd.total_array_size)
|
|
108
|
+
self.node_sizes[
|
|
109
|
+
cmd.chunk_offset : cmd.chunk_offset + len(cmd.flt_array)
|
|
110
|
+
] = cmd.flt_array
|
|
111
|
+
# Combine the hashes for the UpdatePart and all UpdateGeom messages
|
|
112
|
+
self.hash.update(cmd.hash.encode("utf-8"))
|
|
97
113
|
|
|
98
114
|
def nodal_surface_rep(self):
|
|
99
115
|
"""
|
|
@@ -120,26 +136,7 @@ class Part(object):
|
|
|
120
136
|
self.session.log(f"Note: part '{self.cmd.name}' contains no triangles.")
|
|
121
137
|
return None, None, None, None, None, None
|
|
122
138
|
verts = self.coords
|
|
123
|
-
|
|
124
|
-
midx = (self.session.scene_bounds[3] + self.session.scene_bounds[0]) * 0.5
|
|
125
|
-
midy = (self.session.scene_bounds[4] + self.session.scene_bounds[1]) * 0.5
|
|
126
|
-
midz = (self.session.scene_bounds[5] + self.session.scene_bounds[2]) * 0.5
|
|
127
|
-
dx = self.session.scene_bounds[3] - self.session.scene_bounds[0]
|
|
128
|
-
dy = self.session.scene_bounds[4] - self.session.scene_bounds[1]
|
|
129
|
-
dz = self.session.scene_bounds[5] - self.session.scene_bounds[2]
|
|
130
|
-
s = dx
|
|
131
|
-
if dy > s:
|
|
132
|
-
s = dy
|
|
133
|
-
if dz > s:
|
|
134
|
-
s = dz
|
|
135
|
-
if s == 0:
|
|
136
|
-
s = 1.0
|
|
137
|
-
num_verts = int(verts.size / 3)
|
|
138
|
-
for i in range(num_verts):
|
|
139
|
-
j = i * 3
|
|
140
|
-
verts[j + 0] = (verts[j + 0] - midx) / s
|
|
141
|
-
verts[j + 1] = (verts[j + 1] - midy) / s
|
|
142
|
-
verts[j + 2] = (verts[j + 2] - midz) / s
|
|
139
|
+
self.normalize_verts(verts)
|
|
143
140
|
|
|
144
141
|
conn = self.conn_tris
|
|
145
142
|
normals = self.normals
|
|
@@ -242,6 +239,141 @@ class Part(object):
|
|
|
242
239
|
|
|
243
240
|
return command, verts, conn, normals, tcoords, var_cmd
|
|
244
241
|
|
|
242
|
+
def normalize_verts(self, verts: numpy.ndarray):
|
|
243
|
+
"""
|
|
244
|
+
This function scales and translates vertices, so the longest axis in the scene is of
|
|
245
|
+
length 1.0, and data is centered at the origin
|
|
246
|
+
|
|
247
|
+
Returns the scale factor
|
|
248
|
+
"""
|
|
249
|
+
s = 1.0
|
|
250
|
+
if self.session.normalize_geometry and self.session.scene_bounds is not None:
|
|
251
|
+
num_verts = int(verts.size / 3)
|
|
252
|
+
midx = (self.session.scene_bounds[3] + self.session.scene_bounds[0]) * 0.5
|
|
253
|
+
midy = (self.session.scene_bounds[4] + self.session.scene_bounds[1]) * 0.5
|
|
254
|
+
midz = (self.session.scene_bounds[5] + self.session.scene_bounds[2]) * 0.5
|
|
255
|
+
dx = self.session.scene_bounds[3] - self.session.scene_bounds[0]
|
|
256
|
+
dy = self.session.scene_bounds[4] - self.session.scene_bounds[1]
|
|
257
|
+
dz = self.session.scene_bounds[5] - self.session.scene_bounds[2]
|
|
258
|
+
s = dx
|
|
259
|
+
if dy > s:
|
|
260
|
+
s = dy
|
|
261
|
+
if dz > s:
|
|
262
|
+
s = dz
|
|
263
|
+
if s == 0:
|
|
264
|
+
s = 1.0
|
|
265
|
+
for i in range(num_verts):
|
|
266
|
+
j = i * 3
|
|
267
|
+
verts[j + 0] = (verts[j + 0] - midx) / s
|
|
268
|
+
verts[j + 1] = (verts[j + 1] - midy) / s
|
|
269
|
+
verts[j + 2] = (verts[j + 2] - midz) / s
|
|
270
|
+
return 1.0 / s
|
|
271
|
+
|
|
272
|
+
def point_rep(self):
|
|
273
|
+
"""
|
|
274
|
+
This function processes the geometry arrays and returns values to represent point data
|
|
275
|
+
|
|
276
|
+
Returns
|
|
277
|
+
-------
|
|
278
|
+
On failure, the method returns None for the first return value. The returned tuple is:
|
|
279
|
+
|
|
280
|
+
(part_command, vertices, sizes, colors, var_command)
|
|
281
|
+
|
|
282
|
+
part_command: UPDATE_PART command object
|
|
283
|
+
vertices: numpy array of per-node coordinates
|
|
284
|
+
sizes: numpy array of per-node radii
|
|
285
|
+
colors: numpy array of per-node rgb colors
|
|
286
|
+
var_command: UPDATE_VARIABLE command object for the variable the colors correspond to, if any
|
|
287
|
+
"""
|
|
288
|
+
if self.cmd is None:
|
|
289
|
+
return None, None, None, None, None
|
|
290
|
+
if self.cmd.render != self.cmd.NODES:
|
|
291
|
+
# Early out. Rendering type for this object is a surface rep, not a point rep
|
|
292
|
+
return None, None, None, None, None
|
|
293
|
+
verts = self.coords
|
|
294
|
+
num_verts = int(verts.size / 3)
|
|
295
|
+
norm_scale = self.normalize_verts(verts)
|
|
296
|
+
|
|
297
|
+
# Convert var values in self.tcoords to RGB colors
|
|
298
|
+
# For now, look up RGB colors. Planned USD enhancements should allow tex coords instead.
|
|
299
|
+
colors = None
|
|
300
|
+
var_cmd = None
|
|
301
|
+
|
|
302
|
+
if self.tcoords.size and self.tcoords.size == num_verts:
|
|
303
|
+
var_dsg_id = self.cmd.color_variableid
|
|
304
|
+
var_cmd = self.session.variables[var_dsg_id]
|
|
305
|
+
if len(var_cmd.levels) == 0:
|
|
306
|
+
self.session.log(
|
|
307
|
+
f"Note: Node rep not created for part '{self.cmd.name}'. It has var values, but a palette with 0 levels."
|
|
308
|
+
)
|
|
309
|
+
return None, None, None, None, None
|
|
310
|
+
|
|
311
|
+
p_min = None
|
|
312
|
+
p_max = None
|
|
313
|
+
for lvl in var_cmd.levels:
|
|
314
|
+
if (p_min is None) or (p_min > lvl.value):
|
|
315
|
+
p_min = lvl.value
|
|
316
|
+
if (p_max is None) or (p_max < lvl.value):
|
|
317
|
+
p_max = lvl.value
|
|
318
|
+
|
|
319
|
+
num_texels = int(len(var_cmd.texture) / 4)
|
|
320
|
+
|
|
321
|
+
colors = numpy.ndarray((num_verts * 3,), dtype="float32")
|
|
322
|
+
low_color = [c / 255.0 for c in var_cmd.texture[0:3]]
|
|
323
|
+
high_color = [
|
|
324
|
+
c / 255.0 for c in var_cmd.texture[4 * (num_texels - 1) : 4 * (num_texels - 1) + 3]
|
|
325
|
+
]
|
|
326
|
+
if p_min == p_max:
|
|
327
|
+
# Special case where palette min == palette max
|
|
328
|
+
mid_color = var_cmd[4 * (num_texels // 2) : 4 * (num_texels // 2) + 3]
|
|
329
|
+
for idx in range(num_verts):
|
|
330
|
+
val = self.tcoords[idx]
|
|
331
|
+
if val == p_min:
|
|
332
|
+
colors[idx * 3 : idx * 3 + 3] = mid_color
|
|
333
|
+
elif val < p_min:
|
|
334
|
+
colors[idx * 3 : idx * 3 + 3] = low_color
|
|
335
|
+
elif val > p_min:
|
|
336
|
+
colors[idx * 3 : idx * 3 + 3] = high_color
|
|
337
|
+
else:
|
|
338
|
+
for idx in range(num_verts):
|
|
339
|
+
val = self.tcoords[idx]
|
|
340
|
+
if val <= p_min:
|
|
341
|
+
colors[idx * 3 : idx * 3 + 3] = low_color
|
|
342
|
+
else:
|
|
343
|
+
pal_pos = (num_texels - 1) * (val - p_min) / (p_max - p_min)
|
|
344
|
+
pal_idx, pal_sub = divmod(pal_pos, 1)
|
|
345
|
+
pal_idx = int(pal_idx)
|
|
346
|
+
|
|
347
|
+
if pal_idx >= num_texels - 1:
|
|
348
|
+
colors[idx * 3 : idx * 3 + 3] = high_color
|
|
349
|
+
else:
|
|
350
|
+
col0 = var_cmd.texture[pal_idx * 4 : pal_idx * 4 + 3]
|
|
351
|
+
col1 = var_cmd.texture[4 + pal_idx * 4 : 4 + pal_idx * 4 + 3]
|
|
352
|
+
for ii in range(0, 3):
|
|
353
|
+
colors[idx * 3 + ii] = (
|
|
354
|
+
col0[ii] * pal_sub + col1[ii] * (1.0 - pal_sub)
|
|
355
|
+
) / 255.0
|
|
356
|
+
self.session.log(f"Part '{self.cmd.name}' defined: {self.coords.size/3} points.")
|
|
357
|
+
|
|
358
|
+
node_sizes = None
|
|
359
|
+
if self.node_sizes.size and self.node_sizes.size == num_verts:
|
|
360
|
+
# Pass out the node sizes if there is a size-by variable
|
|
361
|
+
node_size_default = self.cmd.node_size_default * norm_scale
|
|
362
|
+
node_sizes = numpy.ndarray((num_verts,), dtype="float32")
|
|
363
|
+
for ii in range(0, num_verts):
|
|
364
|
+
node_sizes[ii] = self.node_sizes[ii] * node_size_default
|
|
365
|
+
elif norm_scale != 1.0:
|
|
366
|
+
# Pass out the node sizes if the model is normalized to fit in a unit cube
|
|
367
|
+
node_size_default = self.cmd.node_size_default * norm_scale
|
|
368
|
+
node_sizes = numpy.ndarray((num_verts,), dtype="float32")
|
|
369
|
+
for ii in range(0, num_verts):
|
|
370
|
+
node_sizes[ii] = node_size_default
|
|
371
|
+
|
|
372
|
+
self.session.log(f"Part '{self.cmd.name}' defined: {self.coords.size/3} points.")
|
|
373
|
+
command = self.cmd
|
|
374
|
+
|
|
375
|
+
return command, verts, node_sizes, colors, var_cmd
|
|
376
|
+
|
|
245
377
|
|
|
246
378
|
class UpdateHandler(object):
|
|
247
379
|
"""
|
|
@@ -410,6 +542,9 @@ class DSGSession(object):
|
|
|
410
542
|
self._scene_bounds: Optional[List] = None
|
|
411
543
|
self._cur_timeline: List = [0.0, 0.0] # Start/End time for current update
|
|
412
544
|
self._callback_handler.session = self
|
|
545
|
+
# log any status changes to this file. external apps will be monitoring
|
|
546
|
+
self._status_file = os.environ.get("ANSYS_OV_SERVER_STATUS_FILENAME", "")
|
|
547
|
+
self._status = dict(status="idle", start_time=0.0, processed_buffers=0, total_buffers=0)
|
|
413
548
|
|
|
414
549
|
@property
|
|
415
550
|
def scene_bounds(self) -> Optional[List]:
|
|
@@ -474,6 +609,15 @@ class DSGSession(object):
|
|
|
474
609
|
if level < self._verbose:
|
|
475
610
|
logging.info(s)
|
|
476
611
|
|
|
612
|
+
@staticmethod
|
|
613
|
+
def warn(s: str) -> None:
|
|
614
|
+
"""Issue a warning to the logging system
|
|
615
|
+
|
|
616
|
+
The logging message is mapped to "warn" and cannot be blocked via verbosity
|
|
617
|
+
checks.
|
|
618
|
+
"""
|
|
619
|
+
logging.warning(s)
|
|
620
|
+
|
|
477
621
|
def start(self) -> int:
|
|
478
622
|
"""Start a gRPC connection to an EnSight instance
|
|
479
623
|
|
|
@@ -518,6 +662,38 @@ class DSGSession(object):
|
|
|
518
662
|
"""Check the service shutdown request status"""
|
|
519
663
|
return self._shutdown
|
|
520
664
|
|
|
665
|
+
def _update_status_file(self, timed: bool = False):
|
|
666
|
+
"""
|
|
667
|
+
Update the status file contents. The status file will contain the
|
|
668
|
+
following json object, stored as: self._status
|
|
669
|
+
|
|
670
|
+
{
|
|
671
|
+
'status' : "working|idle",
|
|
672
|
+
'start_time' : timestamp_of_update_begin,
|
|
673
|
+
'processed_buffers' : number_of_protobuffers_processed,
|
|
674
|
+
'total_buffers' : number_of_protobuffers_total,
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
Parameters
|
|
678
|
+
----------
|
|
679
|
+
timed : bool, optional:
|
|
680
|
+
if True, only update every second.
|
|
681
|
+
|
|
682
|
+
"""
|
|
683
|
+
if self._status_file:
|
|
684
|
+
current_time = time.time()
|
|
685
|
+
if timed:
|
|
686
|
+
last_time = self._status.get("last_time", 0.0)
|
|
687
|
+
if current_time - last_time < 1.0: # type: ignore
|
|
688
|
+
return
|
|
689
|
+
self._status["last_time"] = current_time
|
|
690
|
+
try:
|
|
691
|
+
message = json.dumps(self._status)
|
|
692
|
+
with open(self._status_file, "w") as status_file:
|
|
693
|
+
status_file.write(message)
|
|
694
|
+
except IOError:
|
|
695
|
+
pass # Note failure is expected here in some cases
|
|
696
|
+
|
|
521
697
|
def request_an_update(self, animation: bool = False, allow_spontaneous: bool = True) -> None:
|
|
522
698
|
"""Start a DSG update
|
|
523
699
|
Send a command to the DSG protocol to "init" an update.
|
|
@@ -590,12 +766,21 @@ class DSGSession(object):
|
|
|
590
766
|
self._mesh_block_count = 0 # reset when a new group shows up
|
|
591
767
|
self._callback_handler.begin_update()
|
|
592
768
|
|
|
769
|
+
# Update our status
|
|
770
|
+
self._status = dict(
|
|
771
|
+
status="working", start_time=time.time(), processed_buffers=1, total_buffers=1
|
|
772
|
+
)
|
|
773
|
+
self._update_status_file()
|
|
774
|
+
|
|
593
775
|
# handle the various commands until UPDATE_SCENE_END
|
|
594
776
|
cmd = self._get_next_message()
|
|
595
777
|
while (cmd is not None) and (
|
|
596
778
|
cmd.command_type != dynamic_scene_graph_pb2.SceneUpdateCommand.UPDATE_SCENE_END
|
|
597
779
|
):
|
|
598
780
|
self._handle_update_command(cmd)
|
|
781
|
+
self._status["processed_buffers"] += 1 # type: ignore
|
|
782
|
+
self._status["total_buffers"] = self._status["processed_buffers"] + self._message_queue.qsize() # type: ignore
|
|
783
|
+
self._update_status_file(timed=True)
|
|
599
784
|
cmd = self._get_next_message()
|
|
600
785
|
|
|
601
786
|
# Flush the last part
|
|
@@ -603,6 +788,10 @@ class DSGSession(object):
|
|
|
603
788
|
|
|
604
789
|
self._callback_handler.end_update()
|
|
605
790
|
|
|
791
|
+
# Update our status
|
|
792
|
+
self._status = dict(status="idle", start_time=0.0, processed_buffers=0, total_buffers=0)
|
|
793
|
+
self._update_status_file()
|
|
794
|
+
|
|
606
795
|
def _handle_update_command(self, cmd: dynamic_scene_graph_pb2.SceneUpdateCommand) -> None:
|
|
607
796
|
"""Dispatch out a scene update command to the proper handler
|
|
608
797
|
|
|
@@ -647,10 +836,13 @@ class DSGSession(object):
|
|
|
647
836
|
There is always a part being modified. This method completes the current part, committing
|
|
648
837
|
it to the handler.
|
|
649
838
|
"""
|
|
650
|
-
|
|
839
|
+
try:
|
|
840
|
+
self._callback_handler.finalize_part(self.part)
|
|
841
|
+
except Exception as e:
|
|
842
|
+
self.warn(f"Error encountered while finalizing part geometry: {str(e)}")
|
|
651
843
|
self._mesh_block_count += 1
|
|
652
844
|
|
|
653
|
-
def _handle_part(self,
|
|
845
|
+
def _handle_part(self, part_cmd: Any) -> None:
|
|
654
846
|
"""Handle a DSG UPDATE_PART command
|
|
655
847
|
|
|
656
848
|
Finish the current part and set up the next part.
|
|
@@ -661,7 +853,7 @@ class DSGSession(object):
|
|
|
661
853
|
The command coming from the EnSight stream.
|
|
662
854
|
"""
|
|
663
855
|
self._finish_part()
|
|
664
|
-
self._part.reset(
|
|
856
|
+
self._part.reset(part_cmd)
|
|
665
857
|
|
|
666
858
|
def _handle_group(self, group: Any) -> None:
|
|
667
859
|
"""Handle a DSG UPDATE_GROUP command
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import glob
|
|
2
|
+
import json
|
|
2
3
|
import os
|
|
4
|
+
import platform
|
|
3
5
|
import subprocess
|
|
4
6
|
import sys
|
|
7
|
+
import tempfile
|
|
5
8
|
from types import ModuleType
|
|
6
9
|
from typing import TYPE_CHECKING, Optional, Union
|
|
10
|
+
import uuid
|
|
7
11
|
|
|
8
12
|
import psutil
|
|
9
13
|
|
|
@@ -13,8 +17,6 @@ if TYPE_CHECKING:
|
|
|
13
17
|
except ImportError:
|
|
14
18
|
from ansys.api.pyensight import ensight_api
|
|
15
19
|
|
|
16
|
-
import ansys.pyensight.core
|
|
17
|
-
|
|
18
20
|
|
|
19
21
|
class Omniverse:
|
|
20
22
|
"""Provides the ``ensight.utils.omniverse`` interface.
|
|
@@ -52,6 +54,7 @@ class Omniverse:
|
|
|
52
54
|
self._ensight = interface
|
|
53
55
|
self._server_pid: Optional[int] = None
|
|
54
56
|
self._interpreter: str = ""
|
|
57
|
+
self._status_filename: str = ""
|
|
55
58
|
|
|
56
59
|
@staticmethod
|
|
57
60
|
def find_kit_filename(fallback_directory: Optional[str] = None) -> Optional[str]:
|
|
@@ -129,10 +132,7 @@ class Omniverse:
|
|
|
129
132
|
def _check_modules(self) -> None:
|
|
130
133
|
"""Verify that the Python interpreter is correct
|
|
131
134
|
|
|
132
|
-
Check for
|
|
133
|
-
If pxr is there as well, then we can just use sys.executable.
|
|
134
|
-
If not, check to see if 'kit.bat' or 'kit.sh' can be found and
|
|
135
|
-
arrange to use those instead.
|
|
135
|
+
Check for module dependencies. If not present, raise an exception.
|
|
136
136
|
|
|
137
137
|
Raises
|
|
138
138
|
------
|
|
@@ -144,11 +144,27 @@ class Omniverse:
|
|
|
144
144
|
if len(self._interpreter):
|
|
145
145
|
return
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
if
|
|
149
|
-
|
|
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"
|
|
150
157
|
return
|
|
151
|
-
|
|
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.")
|
|
152
168
|
|
|
153
169
|
def is_running_omniverse(self) -> bool:
|
|
154
170
|
"""Check that an Omniverse connection is active
|
|
@@ -228,30 +244,73 @@ class Omniverse:
|
|
|
228
244
|
port = options.get("port", 12345)
|
|
229
245
|
token = options.get("security", "")
|
|
230
246
|
|
|
231
|
-
# Launch the server via the 'ansys.
|
|
247
|
+
# Launch the server via the 'ansys.pyensight.core.utils.omniverse_cli' module
|
|
232
248
|
dsg_uri = f"grpc://{hostname}:{port}"
|
|
233
|
-
kit_dir = os.path.join(os.path.dirname(ansys.pyensight.core.__file__), "exts")
|
|
234
249
|
cmd = [self._interpreter]
|
|
235
|
-
cmd.extend(["
|
|
236
|
-
cmd.
|
|
250
|
+
cmd.extend(["-m", "ansys.pyensight.core.utils.omniverse_cli"])
|
|
251
|
+
cmd.append(omniverse_path)
|
|
237
252
|
if token:
|
|
238
|
-
cmd.
|
|
253
|
+
cmd.extend(["--security_token", token])
|
|
239
254
|
if temporal:
|
|
240
|
-
cmd.
|
|
255
|
+
cmd.extend(["--temporal", "true"])
|
|
241
256
|
if not include_camera:
|
|
242
|
-
cmd.
|
|
257
|
+
cmd.extend(["--include_camera", "false"])
|
|
243
258
|
if normalize_geometry:
|
|
244
|
-
cmd.
|
|
259
|
+
cmd.extend(["--normalize_geometry", "true"])
|
|
245
260
|
if time_scale != 1.0:
|
|
246
|
-
cmd.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
cmd.
|
|
261
|
+
cmd.extend(["--time_scale", str(time_scale)])
|
|
262
|
+
if not live:
|
|
263
|
+
cmd.extend(["--oneshot", "1"])
|
|
264
|
+
cmd.extend(["--dsg_uri", dsg_uri])
|
|
250
265
|
env_vars = os.environ.copy()
|
|
251
|
-
|
|
252
|
-
|
|
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)
|
|
253
272
|
self._server_pid = process.pid
|
|
254
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
|
+
|
|
255
314
|
def close_connection(self) -> None:
|
|
256
315
|
"""Shut down the open EnSight dsg -> omniverse server
|
|
257
316
|
|
|
@@ -275,6 +334,7 @@ class Omniverse:
|
|
|
275
334
|
except psutil.NoSuchProcess:
|
|
276
335
|
pass
|
|
277
336
|
self._server_pid = None
|
|
337
|
+
self._new_status_file(new=False)
|
|
278
338
|
|
|
279
339
|
def update(self, temporal: bool = False) -> None:
|
|
280
340
|
"""Update the geometry in Omniverse
|