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.

Files changed (34) hide show
  1. ansys/pyensight/core/dockerlauncher.py +6 -0
  2. ansys/pyensight/core/launcher.py +6 -1
  3. ansys/pyensight/core/locallauncher.py +4 -0
  4. ansys/pyensight/core/session.py +1 -1
  5. ansys/pyensight/core/utils/dsg_server.py +227 -35
  6. ansys/pyensight/core/utils/omniverse.py +84 -24
  7. ansys/pyensight/core/utils/omniverse_cli.py +481 -0
  8. ansys/pyensight/core/utils/omniverse_dsg_server.py +236 -426
  9. ansys/pyensight/core/utils/omniverse_glb_server.py +279 -0
  10. {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.9.dist-info}/METADATA +11 -5
  11. ansys_pyensight_core-0.8.9.dist-info/RECORD +34 -0
  12. ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/__init__.py +0 -1
  13. ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/extension.py +0 -407
  14. ansys/pyensight/core/exts/ansys.geometry.service/config/extension.toml +0 -59
  15. ansys/pyensight/core/exts/ansys.geometry.service/data/icon.png +0 -0
  16. ansys/pyensight/core/exts/ansys.geometry.service/data/preview.png +0 -0
  17. ansys/pyensight/core/exts/ansys.geometry.service/docs/CHANGELOG.md +0 -11
  18. ansys/pyensight/core/exts/ansys.geometry.service/docs/README.md +0 -13
  19. ansys/pyensight/core/exts/ansys.geometry.service/docs/index.rst +0 -18
  20. ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/__init__.py +0 -1
  21. ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/extension.py +0 -193
  22. ansys/pyensight/core/exts/ansys.geometry.serviceui/config/extension.toml +0 -49
  23. ansys/pyensight/core/exts/ansys.geometry.serviceui/data/icon.png +0 -0
  24. ansys/pyensight/core/exts/ansys.geometry.serviceui/data/preview.png +0 -0
  25. ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/CHANGELOG.md +0 -11
  26. ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/README.md +0 -13
  27. ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/index.rst +0 -18
  28. ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_BaseColor.png +0 -0
  29. ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_N.png +0 -0
  30. ansys/pyensight/core/utils/resources/Materials/Fieldstone/Fieldstone_ORM.png +0 -0
  31. ansys/pyensight/core/utils/resources/Materials/Fieldstone.mdl +0 -54
  32. ansys_pyensight_core-0.8.8.dist-info/RECORD +0 -52
  33. {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.9.dist-info}/LICENSE +0 -0
  34. {ansys_pyensight_core-0.8.8.dist-info → ansys_pyensight_core-0.8.9.dist-info}/WHEEL +0 -0
@@ -0,0 +1,481 @@
1
+ import argparse
2
+ from functools import partial
3
+ import glob
4
+ import json
5
+ import logging
6
+ import os
7
+ import pathlib
8
+ import time
9
+ from typing import Any, List, Optional
10
+ from urllib.parse import urlparse
11
+
12
+ import ansys.pyensight.core
13
+ import ansys.pyensight.core.utils.dsg_server as dsg_server
14
+ import ansys.pyensight.core.utils.omniverse_dsg_server as ov_dsg_server
15
+ import ansys.pyensight.core.utils.omniverse_glb_server as ov_glb_server
16
+
17
+
18
+ def str2bool_type(v: Any) -> bool:
19
+ """
20
+ This function is designed to be a 'type=' filter for an argparse entry returning a boolean.
21
+ It allows for additional, common alternative strings as booleans. These include 'yes','no',
22
+ 'true','false','t','f','y','n','1' and '0'. If the value does not meet the requirements,
23
+ the function will raise the argparse.ArgumentTypeError exception.
24
+ :param v: The (potential) boolean argument.
25
+ :return: The actual boolean value.
26
+ :raises: argparse.ArgumentTypeError
27
+ """
28
+ if isinstance(v, bool):
29
+ return v
30
+ if v.lower() in ("yes", "true", "t", "y", "1"):
31
+ return True
32
+ elif v.lower() in ("no", "false", "f", "n", "0"):
33
+ return False
34
+ else:
35
+ raise argparse.ArgumentTypeError("Boolean value expected.")
36
+
37
+
38
+ def int_range_type(
39
+ v: Any, min_value: int = 0, max_value: int = 100, allow: Optional[List[int]] = None
40
+ ) -> int:
41
+ """
42
+ This function is designed to be a 'type=' filter for an argparse entry returning an integer value within
43
+ a specified range. If the value does not meet the requirements, the function will raise the
44
+ argparse.ArgumentTypeError exception. This function is normally used with functools.partial to bind
45
+ the minimum and maximum values. For example: type=partial(int_range_type, min_value=0, max_value=65535)
46
+ :param v: The (potential) integer argument.
47
+ :param min_value: The minimum legal integer value.
48
+ :param max_value: The maximum legal integer value.
49
+ :param allow:A list of additional, legal values
50
+ :return: The validated integer value.
51
+ :raises: argparse.ArgumentTypeError
52
+ """
53
+ try:
54
+ value = int(v)
55
+ except ValueError:
56
+ raise argparse.ArgumentTypeError("Integer value expected.")
57
+ if allow is None:
58
+ allow = []
59
+ if (value >= min_value) and (value <= max_value):
60
+ return value
61
+ elif value in allow:
62
+ return value
63
+ else:
64
+ msg = f"Integer value is not in the range [{min_value},{max_value}]"
65
+ if allow:
66
+ msg += f" or in the list {allow}"
67
+ raise argparse.ArgumentTypeError(msg + ".")
68
+
69
+
70
+ class OmniverseGeometryServer(object):
71
+ def __init__(
72
+ self,
73
+ security_token: str = "",
74
+ destination: str = "",
75
+ temporal: bool = False,
76
+ vrmode: bool = False,
77
+ time_scale: float = 1.0,
78
+ normalize_geometry: bool = False,
79
+ dsg_uri: str = "",
80
+ monitor_directory: str = "",
81
+ ) -> None:
82
+ self._dsg_uri = dsg_uri
83
+ self._destination = destination
84
+ self._security_token = security_token
85
+ if not self._security_token:
86
+ self._security_token = os.environ.get("ENSIGHT_SECURITY_TOKEN", "")
87
+ self._temporal = temporal
88
+ self._vrmode = vrmode
89
+ self._time_scale = time_scale
90
+ self._normalize_geometry = normalize_geometry
91
+ self._version = "unknown"
92
+ self._shutdown = False
93
+ self._server_process = None
94
+ self._status_filename: str = ""
95
+ self._monitor_directory: str = monitor_directory
96
+
97
+ @property
98
+ def monitor_directory(self) -> Optional[str]:
99
+ if self._monitor_directory:
100
+ return self._monitor_directory
101
+ # converts "" -> None
102
+ return None
103
+
104
+ @property
105
+ def pyensight_version(self) -> str:
106
+ """The ansys.pyensight.core version"""
107
+ return ansys.pyensight.core.VERSION
108
+
109
+ @property
110
+ def dsg_uri(self) -> str:
111
+ """The endpoint of a Dynamic Scene Graph service: grpc://{hostname}:{port}"""
112
+ return self._dsg_uri
113
+
114
+ @dsg_uri.setter
115
+ def dsg_uri(self, uri: str) -> None:
116
+ self._dsg_uri = uri
117
+
118
+ @property
119
+ def destination(self) -> str:
120
+ """The endpoint of an Omniverse Nucleus service: omniverse://{hostname}/{path}"""
121
+ return self._destination
122
+
123
+ @destination.setter
124
+ def destination(self, value: str) -> None:
125
+ self._destination = value
126
+
127
+ @property
128
+ def security_token(self) -> str:
129
+ """The security token of the DSG service instance."""
130
+ return self._security_token
131
+
132
+ @security_token.setter
133
+ def security_token(self, value: str) -> None:
134
+ self._security_token = value
135
+
136
+ @property
137
+ def temporal(self) -> bool:
138
+ """If True, the DSG update should include all timesteps."""
139
+ return self._temporal
140
+
141
+ @temporal.setter
142
+ def temporal(self, value: bool) -> None:
143
+ self._temporal = bool(value)
144
+
145
+ @property
146
+ def vrmode(self) -> bool:
147
+ """If True, the DSG update should not include camera transforms."""
148
+ return self._vrmode
149
+
150
+ @vrmode.setter
151
+ def vrmode(self, value: bool) -> None:
152
+ self._vrmode = bool(value)
153
+
154
+ @property
155
+ def normalize_geometry(self) -> bool:
156
+ """If True, the DSG geometry should be remapped into normalized space."""
157
+ return self._normalize_geometry
158
+
159
+ @normalize_geometry.setter
160
+ def normalize_geometry(self, val: bool) -> None:
161
+ self._normalize_geometry = val
162
+
163
+ @property
164
+ def time_scale(self) -> float:
165
+ """Value to multiply DSG time values by before passing to Omniverse"""
166
+ return self._time_scale
167
+
168
+ @time_scale.setter
169
+ def time_scale(self, value: float) -> None:
170
+ self._time_scale = value
171
+
172
+ def run_server(self, one_shot: bool = False) -> None:
173
+ """
174
+ Run a DSG to Omniverse server in process.
175
+
176
+ Note: this method does not return until the DSG connection is dropped or
177
+ self.stop_server() has been called.
178
+
179
+ Parameters
180
+ ----------
181
+ one_shot : bool
182
+ If True, only run the server to transfer a single scene and
183
+ then return.
184
+ """
185
+
186
+ # Build the Omniverse connection
187
+ omni_link = ov_dsg_server.OmniverseWrapper(destination=self._destination)
188
+ logging.info("Omniverse connection established.")
189
+
190
+ # parse the DSG USI
191
+ parsed = urlparse(self.dsg_uri)
192
+ port = parsed.port
193
+ host = parsed.hostname
194
+
195
+ # link it to a DSG session
196
+ update_handler = ov_dsg_server.OmniverseUpdateHandler(omni_link)
197
+ dsg_link = dsg_server.DSGSession(
198
+ port=port,
199
+ host=host,
200
+ vrmode=self.vrmode,
201
+ security_code=self.security_token,
202
+ verbose=1,
203
+ normalize_geometry=self.normalize_geometry,
204
+ time_scale=self.time_scale,
205
+ handler=update_handler,
206
+ )
207
+
208
+ # Start the DSG link
209
+ logging.info(f"Making DSG connection to: {self.dsg_uri}")
210
+ err = dsg_link.start()
211
+ if err < 0:
212
+ logging.error("Omniverse connection failed.")
213
+ return
214
+
215
+ # Initial pull request
216
+ dsg_link.request_an_update(animation=self.temporal)
217
+
218
+ # until the link is dropped, continue
219
+ while not dsg_link.is_shutdown() and not self._shutdown:
220
+ dsg_link.handle_one_update()
221
+ if one_shot:
222
+ break
223
+
224
+ logging.info("Shutting down DSG connection")
225
+ dsg_link.end()
226
+ omni_link.shutdown()
227
+
228
+ def run_monitor(self):
229
+ """
230
+ Run monitor and upload GLB files to Omniverse in process. There are two cases:
231
+
232
+ 1) the "directory name" is actually a .glb file. In this case, simply push
233
+ the glb file contents to Omniverse.
234
+
235
+ 2) If a directory, then we periodically scan the directory for files named "*.upload".
236
+ If this file is found, there are two cases:
237
+
238
+ a) The file is empty. In this case, for a file named ABC.upload, the file
239
+ ABC.glb will be read and uploaded before both files are deleted.
240
+
241
+ b) The file contains valid json. In this case, the json object is parsed with
242
+ the following format (two glb files for the first timestep and one for the second):
243
+
244
+ {
245
+ "version": 1,
246
+ "destination": "",
247
+ "files": ["a.glb", "b.glb", "c.glb"],
248
+ "times": [0.0, 0.0, 1.0]
249
+ }
250
+
251
+ "times" is optional and defaults to [0*len("files")]. Once processed,
252
+ all the files referenced in the json and the json file itself are deleted.
253
+ "omniuri" is optional and defaults to the passed Omniverse path.
254
+
255
+ Note: In this mode, the method does not return until a "shutdown" file or
256
+ an error is encountered.
257
+
258
+ TODO: add "push" mechanism to trigger a DSG push from the connected session. This
259
+ can be done via the monitor mechanism and used by the Omniverse kit to implement
260
+ a "pull".
261
+ """
262
+ the_dir = self.monitor_directory
263
+ single_file_upload = False
264
+ if os.path.isfile(the_dir) and the_dir.lower().endswith(".glb"):
265
+ single_file_upload = True
266
+ else:
267
+ if not os.path.isdir(the_dir):
268
+ logging.error(f"The monitor directory {the_dir} does not exist.")
269
+ return
270
+
271
+ # Build the Omniverse connection
272
+ omni_link = ov_dsg_server.OmniverseWrapper(destination=self._destination)
273
+ logging.info("Omniverse connection established.")
274
+
275
+ # use an OmniverseUpdateHandler
276
+ update_handler = ov_dsg_server.OmniverseUpdateHandler(omni_link)
277
+
278
+ # Link it to the GLB file monitoring service
279
+ glb_link = ov_glb_server.GLBSession(
280
+ verbose=1,
281
+ handler=update_handler,
282
+ )
283
+ if single_file_upload:
284
+ start_time = time.time()
285
+ logging.info(f"Uploading file: {the_dir}.")
286
+ try:
287
+ glb_link.upload_file(the_dir)
288
+ except Exception as error:
289
+ logging.warning(f"Error uploading file: {the_dir}: {error}")
290
+ logging.info(f"Uploaded in {(time.time() - start_time):.2f}")
291
+ else:
292
+ logging.info(f"Starting file monitoring for {the_dir}.")
293
+ the_dir_path = pathlib.Path(the_dir)
294
+ try:
295
+ stop_file = os.path.join(the_dir, "shutdown")
296
+ orig_destination = omni_link.destination
297
+ while not os.path.exists(stop_file):
298
+ loop_time = time.time()
299
+ files_to_remove = []
300
+ for filename in glob.glob(os.path.join(the_dir, "*", "*.upload")):
301
+ # reset to the launch URI/directory
302
+ omni_link.destination = orig_destination
303
+ # Keep track of the files and time values
304
+ files_to_remove.append(filename)
305
+ files_to_process = []
306
+ file_timestamps = []
307
+ if os.path.getsize(filename) == 0:
308
+ # replace the ".upload" extension with ".glb"
309
+ glb_file = os.path.splitext(filename)[0] + ".glb"
310
+ if os.path.exists(glb_file):
311
+ files_to_process.append(glb_file)
312
+ file_timestamps.append(0.0)
313
+ files_to_remove.append(glb_file)
314
+ else:
315
+ # read the .upload file json content
316
+ try:
317
+ with open(filename, "r") as fp:
318
+ glb_info = json.load(fp)
319
+ except Exception:
320
+ logging.warning(f"Error reading file: {filename}")
321
+ continue
322
+ # if specified, set the URI/directory target
323
+ omni_link.destination = glb_info.get("destination", orig_destination)
324
+ # Get the GLB files to process
325
+ the_files = glb_info.get("files", [])
326
+ files_to_remove.extend(the_files)
327
+ # Times not used for now, but parse them anyway
328
+ the_times = glb_info.get("times", [0.0] * len(the_files))
329
+ # Validate a few things
330
+ if len(the_files) != len(the_times):
331
+ logging.warning(
332
+ f"Number of times and files are not the same in: {filename}"
333
+ )
334
+ continue
335
+ if len(set(the_times)) != 1:
336
+ logging.warning("Time values not currently supported.")
337
+ if len(the_files) > 1:
338
+ logging.warning("Multiple glb files not currently fully supported.")
339
+ files_to_process.extend(the_files)
340
+ # Upload the files
341
+ for glb_file in files_to_process:
342
+ start_time = time.time()
343
+ logging.info(
344
+ f"Uploading file: {glb_file} to {omni_link.destination}."
345
+ )
346
+ try:
347
+ glb_link.upload_file(glb_file)
348
+ except Exception as error:
349
+ logging.warning(f"Error uploading file: {glb_file}: {error}")
350
+ logging.info(f"Uploaded in {(time.time() - start_time):%.2f}")
351
+ for filename in files_to_remove:
352
+ try:
353
+ # Only delete the file if it is in the_dir_path
354
+ filename_path = pathlib.Path(filename)
355
+ if filename_path.is_relative_to(the_dir_path):
356
+ os.remove(filename)
357
+ except IOError:
358
+ pass
359
+ if time.time() - loop_time < 0.1:
360
+ time.sleep(0.25)
361
+ except Exception as error:
362
+ logging.error(f"Error encountered while monitoring: {error}")
363
+ logging.info("Stopping file monitoring.")
364
+ os.remove(stop_file)
365
+
366
+ omni_link.shutdown()
367
+
368
+
369
+ if __name__ == "__main__":
370
+ parser = argparse.ArgumentParser(description="PyEnSight Omniverse Geometry Service")
371
+ parser.add_argument(
372
+ "destination", default="", type=str, help="The directory to save the USD scene graph into."
373
+ )
374
+ parser.add_argument(
375
+ "--verbose",
376
+ metavar="verbose_level",
377
+ default=0,
378
+ type=partial(int_range_type, min_value=0, max_value=3),
379
+ help="Enable logging information (0-3). Default: 0",
380
+ )
381
+ parser.add_argument(
382
+ "--log_file",
383
+ metavar="log_filename",
384
+ default="",
385
+ type=str,
386
+ help="Save logging output to the named log file instead of stdout.",
387
+ )
388
+ parser.add_argument(
389
+ "--dsg_uri",
390
+ default="grpc://127.0.0.1:5234",
391
+ type=str,
392
+ help="The URI of the EnSight Dynamic Scene Graph server. Default: grpc://127.0.0.1:5234",
393
+ )
394
+ parser.add_argument(
395
+ "--security_token",
396
+ metavar="token",
397
+ default="",
398
+ type=str,
399
+ help="Dynamic scene graph API security token. Default: none",
400
+ )
401
+ parser.add_argument(
402
+ "--monitor_directory",
403
+ metavar="glb_directory",
404
+ default="",
405
+ type=str,
406
+ help="Monitor specified directory for GLB files to be exported. Default: none",
407
+ )
408
+ parser.add_argument(
409
+ "--time_scale",
410
+ metavar="time_scale",
411
+ default=1.0,
412
+ type=float,
413
+ help="Scaling factor to be applied to input time values. Default: 1.0",
414
+ )
415
+ parser.add_argument(
416
+ "--normalize_geometry",
417
+ metavar="yes|no|true|false|1|0",
418
+ default=False,
419
+ type=str2bool_type,
420
+ help="Enable mapping of geometry to a normalized Cartesian space. Default: false",
421
+ )
422
+ parser.add_argument(
423
+ "--include_camera",
424
+ metavar="yes|no|true|false|1|0",
425
+ default=True,
426
+ type=str2bool_type,
427
+ help="Include the camera in the output USD scene graph. Default: true",
428
+ )
429
+ parser.add_argument(
430
+ "--temporal",
431
+ metavar="yes|no|true|false|1|0",
432
+ default=False,
433
+ type=str2bool_type,
434
+ help="Export a temporal scene graph. Default: false",
435
+ )
436
+ parser.add_argument(
437
+ "--oneshot",
438
+ metavar="yes|no|true|false|1|0",
439
+ default=False,
440
+ type=str2bool_type,
441
+ help="Convert a single geometry into USD and exit. Default: false",
442
+ )
443
+
444
+ # parse the command line
445
+ args = parser.parse_args()
446
+
447
+ # set up logging
448
+ level = logging.ERROR
449
+ if args.verbose == 1:
450
+ level = logging.WARN
451
+ elif args.verbose == 2:
452
+ level = logging.INFO
453
+ elif args.verbose == 3:
454
+ level = logging.DEBUG
455
+ log_args = dict(format="GeometryService:%(levelname)s:%(message)s", level=level)
456
+ if args.log_file:
457
+ log_args["filename"] = args.log_file
458
+ # start with a clean logging instance
459
+ while logging.root.hasHandlers():
460
+ logging.root.removeHandler(logging.root.handlers[0])
461
+ logging.basicConfig(**log_args) # type: ignore
462
+
463
+ # Build the server object
464
+ server = OmniverseGeometryServer(
465
+ destination=args.destination,
466
+ dsg_uri=args.dsg_uri,
467
+ security_token=args.security_token,
468
+ monitor_directory=args.monitor_directory,
469
+ time_scale=args.time_scale,
470
+ normalize_geometry=args.normalize_geometry,
471
+ vrmode=not args.include_camera,
472
+ temporal=args.temporal,
473
+ )
474
+
475
+ # run the server
476
+ logging.info("Server startup.")
477
+ if server.monitor_directory:
478
+ server.run_monitor()
479
+ else:
480
+ server.run_server(one_shot=args.oneshot)
481
+ logging.info("Server shutdown.")