ansys-pyensight-core 0.8.4__py3-none-any.whl → 0.8.6__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 (26) hide show
  1. ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/__init__.py +1 -0
  2. ansys/pyensight/core/exts/ansys.geometry.service/ansys/geometry/service/extension.py +407 -0
  3. ansys/pyensight/core/exts/ansys.geometry.service/config/extension.toml +59 -0
  4. ansys/pyensight/core/exts/ansys.geometry.service/data/icon.png +0 -0
  5. ansys/pyensight/core/exts/ansys.geometry.service/data/preview.png +0 -0
  6. ansys/pyensight/core/exts/ansys.geometry.service/docs/CHANGELOG.md +8 -0
  7. ansys/pyensight/core/exts/ansys.geometry.service/docs/README.md +13 -0
  8. ansys/pyensight/core/exts/ansys.geometry.service/docs/index.rst +18 -0
  9. ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/__init__.py +1 -0
  10. ansys/pyensight/core/exts/ansys.geometry.serviceui/ansys/geometry/serviceui/extension.py +193 -0
  11. ansys/pyensight/core/exts/ansys.geometry.serviceui/config/extension.toml +49 -0
  12. ansys/pyensight/core/exts/ansys.geometry.serviceui/data/icon.png +0 -0
  13. ansys/pyensight/core/exts/ansys.geometry.serviceui/data/preview.png +0 -0
  14. ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/CHANGELOG.md +8 -0
  15. ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/README.md +13 -0
  16. ansys/pyensight/core/exts/ansys.geometry.serviceui/docs/index.rst +18 -0
  17. ansys/pyensight/core/launcher.py +36 -1
  18. ansys/pyensight/core/locallauncher.py +3 -1
  19. ansys/pyensight/core/utils/dsg_server.py +36 -11
  20. ansys/pyensight/core/utils/omniverse.py +151 -95
  21. ansys/pyensight/core/utils/omniverse_dsg_server.py +91 -37
  22. {ansys_pyensight_core-0.8.4.dist-info → ansys_pyensight_core-0.8.6.dist-info}/METADATA +1 -1
  23. ansys_pyensight_core-0.8.6.dist-info/RECORD +52 -0
  24. ansys_pyensight_core-0.8.4.dist-info/RECORD +0 -36
  25. {ansys_pyensight_core-0.8.4.dist-info → ansys_pyensight_core-0.8.6.dist-info}/LICENSE +0 -0
  26. {ansys_pyensight_core-0.8.4.dist-info → ansys_pyensight_core-0.8.6.dist-info}/WHEEL +0 -0
@@ -0,0 +1,193 @@
1
+ import logging
2
+ from typing import Any, Optional
3
+ from urllib.parse import urlparse
4
+
5
+ import ansys.geometry.service
6
+ import omni.ext
7
+ import omni.ui as ui
8
+
9
+
10
+ class AnsysGeometryServiceUIExtension(omni.ext.IExt):
11
+ def __init__(self, *args, **kwargs) -> None:
12
+ super().__init__(*args, **kwargs)
13
+ self._window: Any = None
14
+ self._label_w: Any = None
15
+ self._logger = logging.getLogger(__name__.rsplit(".", 1)[0])
16
+ self._grpc = None
17
+ self._dsg_uri_w = None
18
+ self._dsg_token_w = None
19
+ self._omni_uri_w = None
20
+ self._temporal_w = None
21
+ self._vrmode_w = None
22
+ self._normalize_w = None
23
+ self._time_scale_w = None
24
+ self._connect_w = None
25
+ self._update_w = None
26
+ self._connected = False
27
+
28
+ @property
29
+ def service(self) -> Optional["AnsysGeometryServiceUIExtension"]:
30
+ return ansys.geometry.service.AnsysGeometryServiceServerExtension.get_instance()
31
+
32
+ def info(self, text: str) -> None:
33
+ self._logger.info(text)
34
+
35
+ def warning(self, text: str) -> None:
36
+ self._logger.warning(text)
37
+
38
+ def error(self, text: str) -> None:
39
+ self._logger.error(text)
40
+
41
+ def launch_server(self) -> None:
42
+ if self.service.is_server_running():
43
+ return
44
+ self.service.dsg_uri = self._dsg_uri_w.model.as_string
45
+ self.service.security_token = self._dsg_token_w.model.as_string
46
+ self.service.omni_uri = self._omni_uri_w.model.as_string
47
+ self.service.temporal = self._temporal_w.model.as_bool
48
+ self.service.vrmode = self._vrmode_w.model.as_bool
49
+ self.service.normalize_geometry = self._normalize_w.model.as_bool
50
+ scale = self._time_scale_w.model.as_float
51
+ if scale <= 0.0:
52
+ scale = 1.0
53
+ self.service.time_scale = scale
54
+ self.service.launch_server()
55
+ if not self.service.is_server_running():
56
+ self.error("Failed to launch omniverse service.")
57
+ return
58
+
59
+ # parse the DSG USI
60
+ parsed = urlparse(self.service.dsg_uri)
61
+ port = parsed.port
62
+ host = parsed.hostname
63
+
64
+ # make a direct grpc connection to the DSG server
65
+ from ansys.pyensight.core import ensight_grpc # pylint: disable=import-outside-toplevel
66
+
67
+ self._grpc = ensight_grpc.EnSightGRPC(
68
+ host=host, port=port, secret_key=self.service.security_token
69
+ )
70
+ self._grpc.connect()
71
+ if not self._grpc.is_connected():
72
+ self.error(f"Failed to connect to DSG service {host}:{port}")
73
+ return
74
+
75
+ self.info("Connected to DSG service")
76
+ self._connected = True
77
+
78
+ def stop_server(self) -> None:
79
+ if not self._connected:
80
+ return
81
+ self.service.stop_server()
82
+ self._grpc.shutdown()
83
+ self._grpc = None
84
+
85
+ self.info("Disconnect from DSG service")
86
+ self._connected = False
87
+
88
+ def connect_cb(self) -> None:
89
+ if self.service is None:
90
+ self.error("Unable to find ansys.geometry.service instance")
91
+ return
92
+ if self._connected:
93
+ self.stop_server()
94
+ else:
95
+ self.launch_server()
96
+ self.update_ui()
97
+
98
+ def update_cb(self) -> None:
99
+ if not self._connected:
100
+ self.error("No DSG service connected")
101
+ return
102
+ self._grpc.command("import enspyqtgui_int", do_eval=False)
103
+ update_cmd = "dynamicscenegraph://localhost/client/update"
104
+ if self._temporal_w.model.as_bool:
105
+ update_cmd += "?timesteps=1"
106
+ cmd = f'enspyqtgui_int.dynamic_scene_graph_command("{update_cmd}")'
107
+ self._grpc.command(cmd, do_eval=False)
108
+
109
+ def on_startup(self, ext_id: str) -> None:
110
+ self.info(f"ANSYS geometry service GUI startup: {ext_id}")
111
+ if self.service is None:
112
+ self.error("Unable to find ansys.geometry.service instance")
113
+ self.build_ui()
114
+ self.update_ui()
115
+
116
+ def update_ui(self) -> None:
117
+ if self._connected:
118
+ self._connect_w.text = "Disconnect from DSG Server"
119
+ self._label_w.text = f"Connected to: {self.service.dsg_uri}"
120
+ else:
121
+ self._connect_w.text = "Connect to DSG Server"
122
+ self._label_w.text = "No connected DSG server"
123
+ self._update_w.enabled = self._connected
124
+ self._temporal_w.enabled = True
125
+ self._vrmode_w.enabled = not self._connected
126
+ self._normalize_w.enabled = not self._connected
127
+ self._time_scale_w.enabled = not self._connected
128
+ self._dsg_uri_w.enabled = not self._connected
129
+ self._dsg_token_w.enabled = not self._connected
130
+ self._omni_uri_w.enabled = not self._connected
131
+
132
+ def build_ui(self) -> None:
133
+ self._window = ui.Window("ANSYS Geometry Service")
134
+ with self._window.frame:
135
+ with ui.VStack(height=0, spacing=5):
136
+ self._label_w = ui.Label("No connected DSG server")
137
+
138
+ with ui.HStack(spacing=5):
139
+ ui.Label("DSG Service URI:", alignment=ui.Alignment.RIGHT_CENTER, width=0)
140
+ self._dsg_uri_w = ui.StringField()
141
+ self._dsg_uri_w.model.as_string = self.service.dsg_uri
142
+
143
+ with ui.HStack(spacing=5):
144
+ ui.Label("DSG security code:", alignment=ui.Alignment.RIGHT_CENTER, width=0)
145
+ self._dsg_token_w = ui.StringField(password_mode=True)
146
+ self._dsg_token_w.model.as_string = self.service.security_token
147
+
148
+ with ui.HStack(spacing=5):
149
+ ui.Label("Omniverse URI:", alignment=ui.Alignment.RIGHT_CENTER, width=0)
150
+ self._omni_uri_w = ui.StringField()
151
+ self._omni_uri_w.model.as_string = self.service.omni_uri
152
+
153
+ with ui.HStack(spacing=5):
154
+ with ui.HStack(spacing=5):
155
+ self._temporal_w = ui.CheckBox(width=0)
156
+ self._temporal_w.model.set_value(self.service.temporal)
157
+ ui.Label("Temporal", alignment=ui.Alignment.LEFT_CENTER)
158
+
159
+ with ui.HStack(spacing=5):
160
+ self._vrmode_w = ui.CheckBox(width=0)
161
+ self._vrmode_w.model.set_value(self.service.vrmode)
162
+ ui.Label("VR Mode", alignment=ui.Alignment.LEFT_CENTER)
163
+
164
+ with ui.HStack(spacing=5):
165
+ self._normalize_w = ui.CheckBox(width=0)
166
+ self._normalize_w.model.set_value(self.service.normalize_geometry)
167
+ ui.Label("Normalize", alignment=ui.Alignment.LEFT_CENTER)
168
+
169
+ with ui.HStack(spacing=5):
170
+ ui.Label(
171
+ "Temporal scaling factor:", alignment=ui.Alignment.RIGHT_CENTER, width=0
172
+ )
173
+ self._time_scale_w = ui.FloatField()
174
+ self._time_scale_w.model.as_float = self.service.time_scale
175
+
176
+ with ui.HStack():
177
+ self._connect_w = ui.Button("Connect to DSG Server", clicked_fn=self.connect_cb)
178
+ self._update_w = ui.Button("Request Update", clicked_fn=self.update_cb)
179
+
180
+ def on_shutdown(self) -> None:
181
+ self.info("ANSYS geometry service shutdown")
182
+ self.stop_server()
183
+ self._window = None
184
+ self._label_w = None
185
+ self._dsg_uri_w = None
186
+ self._dsg_token_w = None
187
+ self._omni_uri_w = None
188
+ self._temporal_w = None
189
+ self._vrmode_w = None
190
+ self._normalize_w = None
191
+ self._time_scale_w = None
192
+ self._connect_w = None
193
+ self._update_w = None
@@ -0,0 +1,49 @@
1
+ [package]
2
+ # Semantic Versioning is used: https://semver.org/
3
+ version = "0.8.6"
4
+
5
+ # Lists people or organizations that are considered the "authors" of the package.
6
+ authors = ["ANSYS"]
7
+
8
+ # The title and description fields are primarily for displaying extension info in UI
9
+ title = "ANSYS Omniverse Geometry Service GUI"
10
+ description = "A geometry synchronization service that enables export of geometry scenes from ANSYS products to Omniverse."
11
+
12
+ # Path (relative to the root) or content of readme markdown file for UI.
13
+ readme = "docs/README.md"
14
+
15
+ # URL of the extension source repository.
16
+ repository = "https://github.com/ansys/pyensight"
17
+
18
+ # One of categories for UI.
19
+ category = "simulation"
20
+
21
+ # Keywords for the extension
22
+ keywords = ["ANSYS", "EnSight", "PyEnSight", "Fluent", "kit"]
23
+
24
+ # Location of change log file in target (final) folder of extension, relative to the root.
25
+ # More info on writing changelog: https://keepachangelog.com/en/1.0.0/
26
+ changelog = "docs/CHANGELOG.md"
27
+
28
+ # Preview image and icon. Folder named "data" automatically goes in git lfs (see .gitattributes file).
29
+ # Preview image is shown in "Overview" of Extensions window. Screenshot of an extension might be a good preview image.
30
+ preview_image = "data/preview.png"
31
+
32
+ # Icon is shown in Extensions window, it is recommended to be square, of size 256x256.
33
+ icon = "data/icon.png"
34
+
35
+ # Use omni.ui to build simple UI
36
+ [dependencies]
37
+ "omni.kit.uiapp" = {}
38
+ "ansys.geometry.service" = {}
39
+
40
+ # Main python module this extension provides, it will be publicly available as "import ansys.geometry.serviceui".
41
+ [[python.module]]
42
+ name = "ansys.geometry.serviceui"
43
+
44
+ [[test]]
45
+ # Extra dependencies only to be used during test run
46
+ dependencies = [
47
+ "omni.kit.ui_test" # UI testing extension
48
+ ]
49
+
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4
+
5
+
6
+ ## [0.9.0] - 2024-06-28
7
+ - Initial kit version of ANSYS Geometry Service GUI
8
+
@@ -0,0 +1,13 @@
1
+ # ANSYS Omniverse Geometry Service GUI [ansys.geometry.serviceui]
2
+
3
+ This Omniverse extension is a UI interface to the [ansys.geometry.service]
4
+ kit extension. It allows an Omniverse application user to connect to
5
+ a running copy of ANSYS EnSight or other application that supports the
6
+ Dynamic Scene Graph gRPC protocol. The GUI allows for the remote scene
7
+ to be pulled, on request, into a specified Omniverse location.
8
+
9
+
10
+ For more details on this extension see:
11
+ https://ensight.docs.pyansys.com/version/dev/user_guide/omniverse_info.html
12
+
13
+
@@ -0,0 +1,18 @@
1
+ ansys.geometry.serviceui
2
+ ########################
3
+
4
+
5
+ .. toctree::
6
+ :maxdepth: 1
7
+
8
+ README
9
+ CHANGELOG
10
+
11
+
12
+ .. automodule::"ansys.geometry.serviceui"
13
+ :platform: Windows-x86_64, Linux-x86_64
14
+ :members:
15
+ :undoc-members:
16
+ :show-inheritance:
17
+ :imported-members:
18
+ :exclude-members: contextmanager
@@ -13,10 +13,13 @@ Examples:
13
13
  """
14
14
  import os.path
15
15
  import platform
16
+ import random
17
+ import re
16
18
  import socket
17
19
  from typing import TYPE_CHECKING, Dict, List, Optional
18
20
  import warnings
19
21
 
22
+ import psutil
20
23
  import requests
21
24
 
22
25
  if TYPE_CHECKING:
@@ -165,6 +168,38 @@ class Launcher:
165
168
  """
166
169
  return
167
170
 
171
+ def _find_ports_used_by_other_pyensight_and_ensight(self):
172
+ """Find ports to avoid when looking for empty ports.
173
+
174
+ The ports are found iterating the current processes and
175
+ looking for PyEnSight/EnSight sessions and their command
176
+ lines.
177
+ """
178
+ pyensight_found = []
179
+ ensight_found = []
180
+ for process in psutil.process_iter():
181
+ try:
182
+ process_cmdline = process.cmdline()
183
+ except (psutil.AccessDenied, psutil.ZombieProcess):
184
+ continue
185
+ if not process_cmdline:
186
+ continue
187
+ if len(process_cmdline) > 1:
188
+ if "websocketserver.py" in os.path.basename(process_cmdline[1]):
189
+ pyensight_found.append(process_cmdline)
190
+ if any(["ensight" in os.path.basename(x) for x in process_cmdline]):
191
+ if any([x == "-ports" for x in process_cmdline]):
192
+ ensight_found.append(process_cmdline)
193
+ ports = []
194
+ for command_line in pyensight_found:
195
+ for command in command_line:
196
+ if re.match(r"^\d{4,5}$", command):
197
+ ports.append(int(command))
198
+ for command_line in ensight_found:
199
+ idx = command_line.index("-ports") + 1
200
+ ports.append(int(command_line[idx]))
201
+ return list(set(ports))
202
+
168
203
  @staticmethod
169
204
  def _find_unused_ports(count: int, avoid: Optional[List[int]] = None) -> Optional[List[int]]:
170
205
  """Find "count" unused ports on the host system
@@ -191,7 +226,7 @@ class Launcher:
191
226
  ports = list()
192
227
 
193
228
  # pick a starting port number
194
- start = os.getpid() % 64000
229
+ start = random.randint(1024, 64000)
195
230
  # We will scan for 65530 ports unless end is specified
196
231
  port_mod = 65530
197
232
  end = start + port_mod - 1
@@ -123,7 +123,8 @@ class LocalLauncher(Launcher):
123
123
  self.session_directory = tempfile.mkdtemp(prefix="pyensight_")
124
124
 
125
125
  # gRPC port, VNC port, websocketserver ws, websocketserver html
126
- self._ports = self._find_unused_ports(4)
126
+ to_avoid = self._find_ports_used_by_other_pyensight_and_ensight()
127
+ self._ports = self._find_unused_ports(5, avoid=to_avoid)
127
128
  if self._ports is None:
128
129
  raise RuntimeError("Unable to allocate local ports for EnSight session")
129
130
  is_windows = self._is_windows()
@@ -152,6 +153,7 @@ class LocalLauncher(Launcher):
152
153
  cmd.extend(["-grpc_server", str(self._ports[0])])
153
154
  vnc_url = f"vnc://%%3Frfb_port={self._ports[1]}%%26use_auth=0"
154
155
  cmd.extend(["-vnc", vnc_url])
156
+ cmd.extend(["-ports", str(self._ports[4])])
155
157
 
156
158
  use_egl = self._use_egl()
157
159
 
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import os
3
3
  import queue
4
+ import sys
4
5
  import threading
5
6
  from typing import Any, Dict, List, Optional
6
7
 
@@ -94,7 +95,7 @@ class Part(object):
94
95
  else:
95
96
  self.tcoords_var_id = None
96
97
 
97
- def build(self):
98
+ def nodal_surface_rep(self):
98
99
  """
99
100
  This function processes the geometry arrays and converts them into nodal representation.
100
101
  It will duplicate triangles as needed (to preserve element normals) and will convert
@@ -115,10 +116,8 @@ class Part(object):
115
116
  """
116
117
  if self.cmd is None:
117
118
  return None, None, None, None, None, None
118
- if self.conn_lines.size:
119
- self.session.log(
120
- f"Note: part '{self.cmd.name}' contains lines which are not currently supported."
121
- )
119
+ if self.conn_tris.size == 0:
120
+ self.session.log(f"Note: part '{self.cmd.name}' contains no triangles.")
122
121
  return None, None, None, None, None, None
123
122
  verts = self.coords
124
123
  if self.session.normalize_geometry and self.session.scene_bounds is not None:
@@ -237,7 +236,7 @@ class Part(object):
237
236
  tcoords = tmp
238
237
 
239
238
  self.session.log(
240
- f"Part '{self.cmd.name}' defined: {self.coords.size/3} verts, {self.conn_tris.size/3} tris, {self.conn_lines.size/2} lines."
239
+ f"Part '{self.cmd.name}' defined: {self.coords.size/3} verts, {self.conn_tris.size/3} tris."
241
240
  )
242
241
  command = self.cmd
243
242
 
@@ -350,6 +349,7 @@ class DSGSession(object):
350
349
  verbose: int = 0,
351
350
  normalize_geometry: bool = False,
352
351
  vrmode: bool = False,
352
+ time_scale: float = 1.0,
353
353
  handler: UpdateHandler = UpdateHandler(),
354
354
  ):
355
355
  """
@@ -379,6 +379,9 @@ class DSGSession(object):
379
379
  vrmode : bool
380
380
  If True, do not include the EnSight camera in the generated view group. The default
381
381
  is to include the EnSight view in the scene transformations.
382
+ time_scale : float
383
+ All DSG protobuffers time values will be multiplied by this factor after
384
+ being received. The default is ``1.0``.
382
385
  handler : UpdateHandler
383
386
  This is an UpdateHandler subclass that is called back when the state of
384
387
  a scene transfer changes. For example, methods are called when the
@@ -395,11 +398,17 @@ class DSGSession(object):
395
398
  self._dsg = None
396
399
  self._normalize_geometry = normalize_geometry
397
400
  self._vrmode = vrmode
401
+ self._time_scale = time_scale
402
+ self._time_limits = [
403
+ sys.float_info.max,
404
+ -sys.float_info.max,
405
+ ] # Min/max across all time steps
398
406
  self._mesh_block_count = 0
399
407
  self._variables: Dict[int, Any] = dict()
400
408
  self._groups: Dict[int, Any] = dict()
401
409
  self._part: Part = Part(self)
402
410
  self._scene_bounds: Optional[List] = None
411
+ self._cur_timeline: List = [0.0, 0.0] # Start/End time for current update
403
412
  self._callback_handler.session = self
404
413
 
405
414
  @property
@@ -438,6 +447,20 @@ class DSGSession(object):
438
447
  def part(self) -> Part:
439
448
  return self._part
440
449
 
450
+ @property
451
+ def time_limits(self) -> List:
452
+ return self._time_limits
453
+
454
+ @property
455
+ def cur_timeline(self) -> List:
456
+ return self._cur_timeline
457
+
458
+ @cur_timeline.setter
459
+ def cur_timeline(self, timeline: List) -> None:
460
+ self._cur_timeline = timeline
461
+ self._time_limits[0] = min(self._time_limits[0], self._cur_timeline[0])
462
+ self._time_limits[1] = max(self._time_limits[1], self._cur_timeline[1])
463
+
441
464
  @property
442
465
  def grpc(self) -> ensight_grpc.EnSightGRPC:
443
466
  return self._grpc
@@ -483,7 +506,7 @@ class DSGSession(object):
483
506
  def end(self):
484
507
  """Stop a gRPC connection to the EnSight instance"""
485
508
  self._callback_handler.end_connection()
486
- self._grpc.stop_server()
509
+ self._grpc.shutdown()
487
510
  self._shutdown = True
488
511
  self._thread.join()
489
512
  self._grpc.shutdown()
@@ -515,8 +538,6 @@ class DSGSession(object):
515
538
  cmd.init.allow_incremental_updates = False
516
539
  cmd.init.maximum_chunk_size = 1024 * 1024
517
540
  self._dsg_queue.put(cmd) # type:ignore
518
- # Handle the update messages
519
- self.handle_one_update()
520
541
 
521
542
  def _poll_messages(self) -> None:
522
543
  """Core interface to grab DSG events from gRPC and queue them for processing
@@ -626,7 +647,6 @@ class DSGSession(object):
626
647
  There is always a part being modified. This method completes the current part, committing
627
648
  it to the handler.
628
649
  """
629
- self._part.build()
630
650
  self._callback_handler.finalize_part(self.part)
631
651
  self._mesh_block_count += 1
632
652
 
@@ -686,10 +706,15 @@ class DSGSession(object):
686
706
  """Handle a DSG UPDATE_VIEW command
687
707
 
688
708
  Parameters
689
- ----------s
709
+ ----------
690
710
  view:
691
711
  The command coming from the EnSight stream.
692
712
  """
713
+ self._finish_part()
693
714
  self._scene_bounds = None
694
715
  self._groups[view.id] = view
716
+ if len(view.timeline) == 2:
717
+ view.timeline[0] *= self._time_scale
718
+ view.timeline[1] *= self._time_scale
719
+ self.cur_timeline = [view.timeline[0], view.timeline[1]]
695
720
  self._callback_handler.add_group(view.id, view=True)