ansys-pyensight-core 0.8.12__py3-none-any.whl → 0.8.13__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.

@@ -1,490 +1,520 @@
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(verbose=1, handler=update_handler, vrmode=self.vrmode)
280
- if single_file_upload:
281
- start_time = time.time()
282
- logging.info(f"Uploading file: {the_dir}.")
283
- try:
284
- glb_link.start_uploads([0.0, 0.0])
285
- glb_link.upload_file(the_dir)
286
- glb_link.end_uploads()
287
- except Exception as error:
288
- logging.error(f"Unable to upload file: {the_dir}: {error}")
289
- logging.info(f"Uploaded in {(time.time() - start_time):.2f}")
290
- else:
291
- logging.info(f"Starting file monitoring for {the_dir}.")
292
- the_dir_path = pathlib.Path(the_dir)
293
- try:
294
- stop_file = os.path.join(the_dir, "shutdown")
295
- orig_destination = omni_link.destination
296
- while not os.path.exists(stop_file):
297
- loop_time = time.time()
298
- files_to_remove = []
299
- for filename in glob.glob(os.path.join(the_dir, "*.upload")):
300
- # reset to the launch URI/directory
301
- omni_link.destination = orig_destination
302
- # Keep track of the files and time values
303
- files_to_remove.append(filename)
304
- files_to_process = []
305
- file_timestamps = []
306
- if os.path.getsize(filename) == 0:
307
- # replace the ".upload" extension with ".glb"
308
- glb_file = os.path.splitext(filename)[0] + ".glb"
309
- if os.path.exists(glb_file):
310
- files_to_process.append(glb_file)
311
- file_timestamps.append(0.0)
312
- files_to_remove.append(glb_file)
313
- else:
314
- # read the .upload file json content
315
- try:
316
- with open(filename, "r") as fp:
317
- glb_info = json.load(fp)
318
- except Exception:
319
- logging.error(f"Unable to read file: {filename}")
320
- continue
321
- # if specified, set the URI/directory target
322
- omni_link.destination = glb_info.get("destination", orig_destination)
323
- # Get the GLB files to process
324
- the_files = glb_info.get("files", [])
325
- files_to_remove.extend(the_files)
326
- # Times not used for now, but parse them anyway
327
- the_times = glb_info.get("times", [0.0] * len(the_files))
328
- file_timestamps.extend(the_times)
329
- # Validate a few things
330
- if len(the_files) != len(the_times):
331
- logging.error(
332
- f"Number of times and files are not the same in: {filename}"
333
- )
334
- continue
335
- files_to_process.extend(the_files)
336
- # manage time
337
- timeline = sorted(set(file_timestamps))
338
- if len(timeline) != 1:
339
- logging.warning("Time values not currently supported.")
340
- if len(files_to_process) > 1:
341
- logging.warning("Multiple glb files not currently fully supported.")
342
- # Upload the files
343
- glb_link.start_uploads([timeline[0], timeline[-1]])
344
- for glb_file, timestamp in zip(files_to_process, file_timestamps):
345
- start_time = time.time()
346
- logging.info(f"Uploading file: {glb_file} to {omni_link.destination}.")
347
- try:
348
- time_idx = timeline.index(timestamp) + 1
349
- if time_idx == len(timeline):
350
- time_idx -= 1
351
- limits = [timestamp, timeline[time_idx]]
352
- glb_link.upload_file(glb_file, timeline=limits)
353
- except Exception as error:
354
- logging.error(f"Unable to upload file: {glb_file}: {error}")
355
- logging.info(f"Uploaded in {(time.time() - start_time):.2f}s")
356
- glb_link.end_uploads()
357
- for filename in files_to_remove:
358
- try:
359
- # Only delete the file if it is in the_dir_path
360
- filename_path = pathlib.Path(filename)
361
- if filename_path.is_relative_to(the_dir_path):
362
- os.remove(filename)
363
- except IOError:
364
- pass
365
- if time.time() - loop_time < 0.1:
366
- time.sleep(0.25)
367
- except Exception as error:
368
- logging.error(f"Error encountered while monitoring: {error}")
369
- logging.info("Stopping file monitoring.")
370
- try:
371
- os.remove(stop_file)
372
- except IOError:
373
- logging.error("Unable to remove 'shutdown' file.")
374
-
375
- omni_link.shutdown()
376
-
377
-
378
- if __name__ == "__main__":
379
- parser = argparse.ArgumentParser(description="PyEnSight Omniverse Geometry Service")
380
- parser.add_argument(
381
- "destination", default="", type=str, help="The directory to save the USD scene graph into."
382
- )
383
- parser.add_argument(
384
- "--verbose",
385
- metavar="verbose_level",
386
- default=0,
387
- type=partial(int_range_type, min_value=0, max_value=3),
388
- help="Enable logging information (0-3). Default: 0",
389
- )
390
- parser.add_argument(
391
- "--log_file",
392
- metavar="log_filename",
393
- default="",
394
- type=str,
395
- help="Save logging output to the named log file instead of stdout.",
396
- )
397
- parser.add_argument(
398
- "--dsg_uri",
399
- default="grpc://127.0.0.1:5234",
400
- type=str,
401
- help="The URI of the EnSight Dynamic Scene Graph server. Default: grpc://127.0.0.1:5234",
402
- )
403
- parser.add_argument(
404
- "--security_token",
405
- metavar="token",
406
- default="",
407
- type=str,
408
- help="Dynamic scene graph API security token. Default: none",
409
- )
410
- parser.add_argument(
411
- "--monitor_directory",
412
- metavar="glb_directory",
413
- default="",
414
- type=str,
415
- help="Monitor specified directory for GLB files to be exported. Default: none",
416
- )
417
- parser.add_argument(
418
- "--time_scale",
419
- metavar="time_scale",
420
- default=1.0,
421
- type=float,
422
- help="Scaling factor to be applied to input time values. Default: 1.0",
423
- )
424
- parser.add_argument(
425
- "--normalize_geometry",
426
- metavar="yes|no|true|false|1|0",
427
- default=False,
428
- type=str2bool_type,
429
- help="Enable mapping of geometry to a normalized Cartesian space. Default: false",
430
- )
431
- parser.add_argument(
432
- "--include_camera",
433
- metavar="yes|no|true|false|1|0",
434
- default=True,
435
- type=str2bool_type,
436
- help="Include the camera in the output USD scene graph. Default: true",
437
- )
438
- parser.add_argument(
439
- "--temporal",
440
- metavar="yes|no|true|false|1|0",
441
- default=False,
442
- type=str2bool_type,
443
- help="Export a temporal scene graph. Default: false",
444
- )
445
- parser.add_argument(
446
- "--oneshot",
447
- metavar="yes|no|true|false|1|0",
448
- default=False,
449
- type=str2bool_type,
450
- help="Convert a single geometry into USD and exit. Default: false",
451
- )
452
-
453
- # parse the command line
454
- args = parser.parse_args()
455
-
456
- # set up logging
457
- level = logging.ERROR
458
- if args.verbose == 1:
459
- level = logging.WARN
460
- elif args.verbose == 2:
461
- level = logging.INFO
462
- elif args.verbose == 3:
463
- level = logging.DEBUG
464
- log_args = dict(format="GeometryService:%(levelname)s:%(message)s", level=level)
465
- if args.log_file:
466
- log_args["filename"] = args.log_file
467
- # start with a clean logging instance
468
- while logging.root.hasHandlers():
469
- logging.root.removeHandler(logging.root.handlers[0])
470
- logging.basicConfig(**log_args) # type: ignore
471
-
472
- # Build the server object
473
- server = OmniverseGeometryServer(
474
- destination=args.destination,
475
- dsg_uri=args.dsg_uri,
476
- security_token=args.security_token,
477
- monitor_directory=args.monitor_directory,
478
- time_scale=args.time_scale,
479
- normalize_geometry=args.normalize_geometry,
480
- vrmode=not args.include_camera,
481
- temporal=args.temporal,
482
- )
483
-
484
- # run the server
485
- logging.info("Server startup.")
486
- if server.monitor_directory:
487
- server.run_monitor()
488
- else:
489
- server.run_server(one_shot=args.oneshot)
490
- logging.info("Server shutdown.")
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
+ line_width: float = -0.0001,
82
+ use_lines: bool = False,
83
+ ) -> None:
84
+ self._dsg_uri = dsg_uri
85
+ self._destination = destination
86
+ self._security_token = security_token
87
+ if not self._security_token:
88
+ self._security_token = os.environ.get("ENSIGHT_SECURITY_TOKEN", "")
89
+ self._temporal = temporal
90
+ self._vrmode = vrmode
91
+ self._time_scale = time_scale
92
+ self._normalize_geometry = normalize_geometry
93
+ self._version = "unknown"
94
+ self._shutdown = False
95
+ self._server_process = None
96
+ self._status_filename: str = ""
97
+ self._monitor_directory: str = monitor_directory
98
+ self._line_width = line_width
99
+ self._use_lines = use_lines
100
+
101
+ @property
102
+ def monitor_directory(self) -> Optional[str]:
103
+ if self._monitor_directory:
104
+ return self._monitor_directory
105
+ # converts "" -> None
106
+ return None
107
+
108
+ @property
109
+ def pyensight_version(self) -> str:
110
+ """The ansys.pyensight.core version"""
111
+ return ansys.pyensight.core.VERSION
112
+
113
+ @property
114
+ def dsg_uri(self) -> str:
115
+ """The endpoint of a Dynamic Scene Graph service: grpc://{hostname}:{port}"""
116
+ return self._dsg_uri
117
+
118
+ @dsg_uri.setter
119
+ def dsg_uri(self, uri: str) -> None:
120
+ self._dsg_uri = uri
121
+
122
+ @property
123
+ def destination(self) -> str:
124
+ """The endpoint of an Omniverse Nucleus service: omniverse://{hostname}/{path}"""
125
+ return self._destination
126
+
127
+ @destination.setter
128
+ def destination(self, value: str) -> None:
129
+ self._destination = value
130
+
131
+ @property
132
+ def security_token(self) -> str:
133
+ """The security token of the DSG service instance."""
134
+ return self._security_token
135
+
136
+ @security_token.setter
137
+ def security_token(self, value: str) -> None:
138
+ self._security_token = value
139
+
140
+ @property
141
+ def temporal(self) -> bool:
142
+ """If True, the DSG update should include all timesteps."""
143
+ return self._temporal
144
+
145
+ @temporal.setter
146
+ def temporal(self, value: bool) -> None:
147
+ self._temporal = bool(value)
148
+
149
+ @property
150
+ def vrmode(self) -> bool:
151
+ """If True, the DSG update should not include camera transforms."""
152
+ return self._vrmode
153
+
154
+ @vrmode.setter
155
+ def vrmode(self, value: bool) -> None:
156
+ self._vrmode = bool(value)
157
+
158
+ @property
159
+ def normalize_geometry(self) -> bool:
160
+ """If True, the DSG geometry should be remapped into normalized space."""
161
+ return self._normalize_geometry
162
+
163
+ @normalize_geometry.setter
164
+ def normalize_geometry(self, val: bool) -> None:
165
+ self._normalize_geometry = val
166
+
167
+ @property
168
+ def time_scale(self) -> float:
169
+ """Value to multiply DSG time values by before passing to Omniverse"""
170
+ return self._time_scale
171
+
172
+ @time_scale.setter
173
+ def time_scale(self, value: float) -> None:
174
+ self._time_scale = value
175
+
176
+ def run_server(self, one_shot: bool = False) -> None:
177
+ """
178
+ Run a DSG to Omniverse server in process.
179
+
180
+ Note: this method does not return until the DSG connection is dropped or
181
+ self.stop_server() has been called.
182
+
183
+ Parameters
184
+ ----------
185
+ one_shot : bool
186
+ If True, only run the server to transfer a single scene and
187
+ then return.
188
+ """
189
+
190
+ # Build the Omniverse connection
191
+ omni_link = ov_dsg_server.OmniverseWrapper(
192
+ destination=self._destination, line_width=self._line_width, use_lines=self._use_lines
193
+ )
194
+ logging.info("Omniverse connection established.")
195
+
196
+ # parse the DSG USI
197
+ parsed = urlparse(self.dsg_uri)
198
+ port = parsed.port
199
+ host = parsed.hostname
200
+
201
+ # link it to a DSG session
202
+ update_handler = ov_dsg_server.OmniverseUpdateHandler(omni_link)
203
+ dsg_link = dsg_server.DSGSession(
204
+ port=port,
205
+ host=host,
206
+ vrmode=self.vrmode,
207
+ security_code=self.security_token,
208
+ verbose=1,
209
+ normalize_geometry=self.normalize_geometry,
210
+ time_scale=self.time_scale,
211
+ handler=update_handler,
212
+ )
213
+
214
+ # Start the DSG link
215
+ logging.info(f"Making DSG connection to: {self.dsg_uri}")
216
+ err = dsg_link.start()
217
+ if err < 0:
218
+ logging.error("Omniverse connection failed.")
219
+ return
220
+
221
+ # Initial pull request
222
+ dsg_link.request_an_update(animation=self.temporal)
223
+
224
+ # until the link is dropped, continue
225
+ while not dsg_link.is_shutdown() and not self._shutdown:
226
+ dsg_link.handle_one_update()
227
+ if one_shot:
228
+ break
229
+
230
+ logging.info("Shutting down DSG connection")
231
+ dsg_link.end()
232
+ omni_link.shutdown()
233
+
234
+ def run_monitor(self):
235
+ """
236
+ Run monitor and upload GLB files to Omniverse in process. There are two cases:
237
+
238
+ 1) the "directory name" is actually a .glb file. In this case, simply push
239
+ the glb file contents to Omniverse.
240
+
241
+ 2) If a directory, then we periodically scan the directory for files named "*.upload".
242
+ If this file is found, there are two cases:
243
+
244
+ a) The file is empty. In this case, for a file named ABC.upload, the file
245
+ ABC.glb will be read and uploaded before both files are deleted.
246
+
247
+ b) The file contains valid json. In this case, the json object is parsed with
248
+ the following format (two glb files for the first timestep and one for the second):
249
+
250
+ {
251
+ "version": 1,
252
+ "destination": "",
253
+ "files": ["a.glb", "b.glb", "c.glb"],
254
+ "times": [0.0, 0.0, 1.0]
255
+ }
256
+
257
+ "times" is optional and defaults to [0*len("files")]. Once processed,
258
+ all the files referenced in the json and the json file itself are deleted.
259
+ "omniuri" is optional and defaults to the passed Omniverse path.
260
+
261
+ Note: In this mode, the method does not return until a "shutdown" file or
262
+ an error is encountered.
263
+
264
+ TODO: add "push" mechanism to trigger a DSG push from the connected session. This
265
+ can be done via the monitor mechanism and used by the Omniverse kit to implement
266
+ a "pull".
267
+ """
268
+ the_dir = self.monitor_directory
269
+ single_file_upload = False
270
+ if os.path.isfile(the_dir) and the_dir.lower().endswith(".glb"):
271
+ single_file_upload = True
272
+ else:
273
+ if not os.path.isdir(the_dir):
274
+ logging.error(f"The monitor directory {the_dir} does not exist.")
275
+ return
276
+
277
+ # Build the Omniverse connection
278
+ omni_link = ov_dsg_server.OmniverseWrapper(
279
+ destination=self._destination, line_width=self._line_width, use_lines=self._use_lines
280
+ )
281
+ logging.info("Omniverse connection established.")
282
+
283
+ # use an OmniverseUpdateHandler
284
+ update_handler = ov_dsg_server.OmniverseUpdateHandler(omni_link)
285
+
286
+ # Link it to the GLB file monitoring service
287
+ glb_link = ov_glb_server.GLBSession(verbose=1, handler=update_handler, vrmode=self.vrmode)
288
+ if single_file_upload:
289
+ start_time = time.time()
290
+ logging.info(f"Uploading file: {the_dir}.")
291
+ try:
292
+ glb_link.start_uploads([0.0, 0.0])
293
+ glb_link.upload_file(the_dir)
294
+ glb_link.end_uploads()
295
+ except Exception as error:
296
+ logging.error(f"Unable to upload file: {the_dir}: {error}")
297
+ logging.info(f"Uploaded in {(time.time() - start_time):.2f}")
298
+ else:
299
+ logging.info(f"Starting file monitoring for {the_dir}.")
300
+ the_dir_path = pathlib.Path(the_dir)
301
+ try:
302
+ stop_file = os.path.join(the_dir, "shutdown")
303
+ orig_destination = omni_link.destination
304
+ while not os.path.exists(stop_file):
305
+ loop_time = time.time()
306
+ files_to_remove = []
307
+ for filename in glob.glob(os.path.join(the_dir, "*.upload")):
308
+ # reset to the launch URI/directory
309
+ omni_link.destination = orig_destination
310
+ # Keep track of the files and time values
311
+ files_to_remove.append(filename)
312
+ files_to_process = []
313
+ file_timestamps = []
314
+ if os.path.getsize(filename) == 0:
315
+ # replace the ".upload" extension with ".glb"
316
+ glb_file = os.path.splitext(filename)[0] + ".glb"
317
+ if os.path.exists(glb_file):
318
+ files_to_process.append(glb_file)
319
+ file_timestamps.append(0.0)
320
+ files_to_remove.append(glb_file)
321
+ else:
322
+ # read the .upload file json content
323
+ try:
324
+ with open(filename, "r") as fp:
325
+ glb_info = json.load(fp)
326
+ except Exception:
327
+ logging.error(f"Unable to read file: {filename}")
328
+ continue
329
+ # if specified, set the URI/directory target
330
+ omni_link.destination = glb_info.get("destination", orig_destination)
331
+ # Get the GLB files to process
332
+ the_files = glb_info.get("files", [])
333
+ files_to_remove.extend(the_files)
334
+ # Times not used for now, but parse them anyway
335
+ the_times = glb_info.get("times", [0.0] * len(the_files))
336
+ file_timestamps.extend(the_times)
337
+ # Validate a few things
338
+ if len(the_files) != len(the_times):
339
+ logging.error(
340
+ f"Number of times and files are not the same in: {filename}"
341
+ )
342
+ continue
343
+ files_to_process.extend(the_files)
344
+ # manage time
345
+ timeline = sorted(set(file_timestamps))
346
+ if len(timeline) != 1:
347
+ logging.warning("Time values not currently supported.")
348
+ if len(files_to_process) > 1:
349
+ logging.warning("Multiple glb files not currently fully supported.")
350
+ # Upload the files
351
+ glb_link.start_uploads([timeline[0], timeline[-1]])
352
+ for glb_file, timestamp in zip(files_to_process, file_timestamps):
353
+ start_time = time.time()
354
+ logging.info(f"Uploading file: {glb_file} to {omni_link.destination}.")
355
+ try:
356
+ time_idx = timeline.index(timestamp) + 1
357
+ if time_idx == len(timeline):
358
+ time_idx -= 1
359
+ limits = [timestamp, timeline[time_idx]]
360
+ glb_link.upload_file(glb_file, timeline=limits)
361
+ except Exception as error:
362
+ logging.error(f"Unable to upload file: {glb_file}: {error}")
363
+ logging.info(f"Uploaded in {(time.time() - start_time):.2f}s")
364
+ glb_link.end_uploads()
365
+ for filename in files_to_remove:
366
+ try:
367
+ # Only delete the file if it is in the_dir_path
368
+ filename_path = pathlib.Path(filename)
369
+ if filename_path.is_relative_to(the_dir_path):
370
+ os.remove(filename)
371
+ except IOError:
372
+ pass
373
+ if time.time() - loop_time < 0.1:
374
+ time.sleep(0.25)
375
+ except Exception as error:
376
+ logging.error(f"Error encountered while monitoring: {error}")
377
+ logging.info("Stopping file monitoring.")
378
+ try:
379
+ os.remove(stop_file)
380
+ except IOError:
381
+ logging.error("Unable to remove 'shutdown' file.")
382
+
383
+ omni_link.shutdown()
384
+
385
+
386
+ if __name__ == "__main__":
387
+ parser = argparse.ArgumentParser(description="PyEnSight Omniverse Geometry Service")
388
+ parser.add_argument(
389
+ "destination", default="", type=str, help="The directory to save the USD scene graph into."
390
+ )
391
+ parser.add_argument(
392
+ "--verbose",
393
+ metavar="verbose_level",
394
+ default=0,
395
+ type=partial(int_range_type, min_value=0, max_value=3),
396
+ help="Enable logging information (0-3). Default: 0",
397
+ )
398
+ parser.add_argument(
399
+ "--log_file",
400
+ metavar="log_filename",
401
+ default="",
402
+ type=str,
403
+ help="Save logging output to the named log file instead of stdout.",
404
+ )
405
+ parser.add_argument(
406
+ "--dsg_uri",
407
+ default="grpc://127.0.0.1:5234",
408
+ type=str,
409
+ help="The URI of the EnSight Dynamic Scene Graph server. Default: grpc://127.0.0.1:5234",
410
+ )
411
+ parser.add_argument(
412
+ "--security_token",
413
+ metavar="token",
414
+ default="",
415
+ type=str,
416
+ help="Dynamic scene graph API security token. Default: none",
417
+ )
418
+ parser.add_argument(
419
+ "--monitor_directory",
420
+ metavar="glb_directory",
421
+ default="",
422
+ type=str,
423
+ help="Monitor specified directory for GLB files to be exported. Default: none",
424
+ )
425
+ parser.add_argument(
426
+ "--time_scale",
427
+ metavar="time_scale",
428
+ default=1.0,
429
+ type=float,
430
+ help="Scaling factor to be applied to input time values. Default: 1.0",
431
+ )
432
+ parser.add_argument(
433
+ "--normalize_geometry",
434
+ metavar="yes|no|true|false|1|0",
435
+ default=False,
436
+ type=str2bool_type,
437
+ help="Enable mapping of geometry to a normalized Cartesian space. Default: false",
438
+ )
439
+ parser.add_argument(
440
+ "--include_camera",
441
+ metavar="yes|no|true|false|1|0",
442
+ default=True,
443
+ type=str2bool_type,
444
+ help="Include the camera in the output USD scene graph. Default: true",
445
+ )
446
+ parser.add_argument(
447
+ "--temporal",
448
+ metavar="yes|no|true|false|1|0",
449
+ default=False,
450
+ type=str2bool_type,
451
+ help="Export a temporal scene graph. Default: false",
452
+ )
453
+ parser.add_argument(
454
+ "--oneshot",
455
+ metavar="yes|no|true|false|1|0",
456
+ default=False,
457
+ type=str2bool_type,
458
+ help="Convert a single geometry into USD and exit. Default: false",
459
+ )
460
+ line_default: Any = os.environ.get("ANSYS_OV_LINE_WIDTH", None)
461
+ if line_default is not None:
462
+ try:
463
+ line_default = float(line_default)
464
+ except ValueError:
465
+ line_default = None
466
+ # Potential future default: -0.0001
467
+ parser.add_argument(
468
+ "--line_width",
469
+ metavar="line_width",
470
+ default=line_default,
471
+ type=float,
472
+ help=f"Width of lines: >0=absolute size. <0=fraction of diagonal. 0=wireframe. Default: {line_default}",
473
+ )
474
+
475
+ # parse the command line
476
+ args = parser.parse_args()
477
+
478
+ # set up logging
479
+ level = logging.ERROR
480
+ if args.verbose == 1:
481
+ level = logging.WARN
482
+ elif args.verbose == 2:
483
+ level = logging.INFO
484
+ elif args.verbose == 3:
485
+ level = logging.DEBUG
486
+ log_args = dict(format="GeometryService:%(levelname)s:%(message)s", level=level)
487
+ if args.log_file:
488
+ log_args["filename"] = args.log_file
489
+ # start with a clean logging instance
490
+ while logging.root.hasHandlers():
491
+ logging.root.removeHandler(logging.root.handlers[0])
492
+ logging.basicConfig(**log_args) # type: ignore
493
+
494
+ # size of lines in data units or fraction of bounding box diagonal
495
+ use_lines = args.line_width is not None
496
+ line_width = -0.0001
497
+ if args.line_width is not None:
498
+ line_width = args.line_width
499
+
500
+ # Build the server object
501
+ server = OmniverseGeometryServer(
502
+ destination=args.destination,
503
+ dsg_uri=args.dsg_uri,
504
+ security_token=args.security_token,
505
+ monitor_directory=args.monitor_directory,
506
+ time_scale=args.time_scale,
507
+ normalize_geometry=args.normalize_geometry,
508
+ vrmode=not args.include_camera,
509
+ temporal=args.temporal,
510
+ line_width=line_width,
511
+ use_lines=use_lines,
512
+ )
513
+
514
+ # run the server
515
+ logging.info("Server startup.")
516
+ if server.monitor_directory:
517
+ server.run_monitor()
518
+ else:
519
+ server.run_server(one_shot=args.oneshot)
520
+ logging.info("Server shutdown.")