ansys-pyensight-core 0.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. ansys/pyensight/core/__init__.py +41 -0
  2. ansys/pyensight/core/common.py +341 -0
  3. ansys/pyensight/core/deep_pixel_view.html +98 -0
  4. ansys/pyensight/core/dockerlauncher.py +1124 -0
  5. ansys/pyensight/core/dvs.py +872 -0
  6. ansys/pyensight/core/enscontext.py +345 -0
  7. ansys/pyensight/core/enshell_grpc.py +641 -0
  8. ansys/pyensight/core/ensight_grpc.py +874 -0
  9. ansys/pyensight/core/ensobj.py +515 -0
  10. ansys/pyensight/core/launch_ensight.py +296 -0
  11. ansys/pyensight/core/launcher.py +388 -0
  12. ansys/pyensight/core/libuserd.py +2110 -0
  13. ansys/pyensight/core/listobj.py +280 -0
  14. ansys/pyensight/core/locallauncher.py +579 -0
  15. ansys/pyensight/core/py.typed +0 -0
  16. ansys/pyensight/core/renderable.py +880 -0
  17. ansys/pyensight/core/session.py +1923 -0
  18. ansys/pyensight/core/sgeo_poll.html +24 -0
  19. ansys/pyensight/core/utils/__init__.py +21 -0
  20. ansys/pyensight/core/utils/adr.py +111 -0
  21. ansys/pyensight/core/utils/dsg_server.py +1220 -0
  22. ansys/pyensight/core/utils/export.py +606 -0
  23. ansys/pyensight/core/utils/omniverse.py +769 -0
  24. ansys/pyensight/core/utils/omniverse_cli.py +614 -0
  25. ansys/pyensight/core/utils/omniverse_dsg_server.py +1196 -0
  26. ansys/pyensight/core/utils/omniverse_glb_server.py +848 -0
  27. ansys/pyensight/core/utils/parts.py +1221 -0
  28. ansys/pyensight/core/utils/query.py +487 -0
  29. ansys/pyensight/core/utils/readers.py +300 -0
  30. ansys/pyensight/core/utils/resources/Materials/000_sky.exr +0 -0
  31. ansys/pyensight/core/utils/support.py +128 -0
  32. ansys/pyensight/core/utils/variables.py +2019 -0
  33. ansys/pyensight/core/utils/views.py +674 -0
  34. ansys_pyensight_core-0.11.0.dist-info/METADATA +309 -0
  35. ansys_pyensight_core-0.11.0.dist-info/RECORD +37 -0
  36. ansys_pyensight_core-0.11.0.dist-info/WHEEL +4 -0
  37. ansys_pyensight_core-0.11.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,874 @@
1
+ # Copyright (C) 2022 - 2026 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: MIT
3
+ #
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ """ensight_grpc module
24
+
25
+ This package defines the EnSightGRPC class which provides a simpler
26
+ interface to the EnSight gRPC interface, including event streams.
27
+
28
+ """
29
+ from concurrent import futures
30
+ import os
31
+ import platform
32
+ import sys
33
+ import tempfile
34
+ import threading
35
+ from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union
36
+ import uuid
37
+
38
+ from ansys.api.pyensight.v0 import dynamic_scene_graph_pb2_grpc, ensight_pb2, ensight_pb2_grpc
39
+ from ansys.tools.common.cyberchannel import create_channel
40
+ import grpc
41
+
42
+ if TYPE_CHECKING:
43
+ from ansys.pyensight.core.utils.dsg_server import DSGSession
44
+
45
+
46
+ class EnSightGRPC(object):
47
+ """Wrapper around a gRPC connection to an EnSight instance
48
+
49
+ This class provides an asynchronous interface to the EnSight
50
+ core gRPC interface. It can handle remote event
51
+ streams, providing a much simpler interface to the EnSight
52
+ application. The default is to make a connection to an EnSight
53
+ gRPC server on port 12345 on the loopback host.
54
+
55
+ Parameters
56
+ ----------
57
+ host: str, optional
58
+ Hostname where there EnSight gRPC server is running.
59
+ port: int, optional
60
+ Port to make the gRPC connection to
61
+ secret_key: str, optional
62
+ Connection secret key
63
+ grpc_use_tcp_sockets: bool, optional
64
+ If using gRPC, and if True, then allow TCP Socket based connections
65
+ instead of only local connections.
66
+ grpc_allow_network_connections: bool, optional
67
+ If using gRPC and using TCP Socket based connections, listen on all networks.
68
+ grpc_disable_tls: bool, optional
69
+ If using gRPC and using TCP Socket based connections, disable TLS.
70
+ grpc_uds_pathname: str, optional
71
+ If using gRPC and using Unix Domain Socket based connections, explicitly
72
+ set the pathname to the shared UDS file instead of using the default.
73
+ disable_grpc_options: bool, optional
74
+ Whether to disable the gRPC options check, and allow to run older
75
+ versions of EnSight
76
+ WARNING:
77
+ Overriding the default values for these options: grpc_use_tcp_sockets, grpc_allow_network_connections,
78
+ and grpc_disable_tls
79
+ can possibly permit control of this computer and any data which resides on it.
80
+ Modification of this configuration is not recommended. Please see the
81
+ documentation for your installed product for additional information.
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ host: str = "127.0.0.1",
87
+ port: int = 12345,
88
+ secret_key: str = "",
89
+ grpc_use_tcp_sockets: bool = False,
90
+ grpc_allow_network_connections: bool = False,
91
+ grpc_disable_tls: bool = False,
92
+ grpc_uds_pathname: Optional[str] = None,
93
+ disable_grpc_options: bool = False,
94
+ ):
95
+ self._host = host
96
+ self._port = port
97
+ self._channel = None
98
+ self._stub = None
99
+ self._dsg_stub = None
100
+ self._security_token = secret_key
101
+ self._grpc_use_tcp_sockets = grpc_use_tcp_sockets
102
+ self._grpc_allow_network_connections = grpc_allow_network_connections
103
+ self._grpc_disable_tls = grpc_disable_tls
104
+ self._grpc_uds_pathname = grpc_uds_pathname
105
+ self._session_name: str = ""
106
+ # Streaming APIs
107
+ # Event (strings)
108
+ self._event_stream = None
109
+ self._event_thread: Optional[threading.Thread] = None
110
+ self._events: List[Any] = list()
111
+ # Callback for events (self._events not used)
112
+ self._event_callback: Optional[Callable] = None
113
+ self._prefix: Optional[str] = None
114
+ self._shmem_module = None
115
+ self._shmem_filename: Optional[str] = None
116
+ self._shmem_client = None
117
+ self._image_stream = None
118
+ self._image_thread = None
119
+ self._image = None
120
+ self._image_number = 0
121
+ self._sub_service = None
122
+ self._dsg_session: Optional["DSGSession"] = None
123
+ self._disable_grpc_options = disable_grpc_options
124
+
125
+ def set_dsg_session(self, dsg_session: "DSGSession"):
126
+ self._dsg_session = dsg_session
127
+
128
+ @property
129
+ def host(self) -> str:
130
+ """The gRPC server (EnSight) hostname"""
131
+ return self._host
132
+
133
+ def port(self) -> int:
134
+ """The gRPC server (EnSight) port number"""
135
+ return self._port
136
+
137
+ @property
138
+ def security_token(self) -> str:
139
+ """The gRPC server (EnSight) secret key
140
+
141
+ EnSight supports a security token in either numeric (-security {int}) or
142
+ string (ENSIGHT_SECURITY_TOKEN environmental variable) form. If EnSight
143
+ is using a security token, all gRPC calls must include this token. This
144
+ call sets the token for all grPC calls made by this class.
145
+ """
146
+ return self._security_token
147
+
148
+ @security_token.setter
149
+ def security_token(self, name: str) -> None:
150
+ self._security_token = name # pragma: no cover
151
+
152
+ @property
153
+ def grpc_use_tcp_sockets(self) -> bool:
154
+ """Get whether to use Unix Domain Sockets or TCP Sockets for gRPC"""
155
+ return self._grpc_use_tcp_sockets
156
+
157
+ @grpc_use_tcp_sockets.setter
158
+ def grpc_use_tcp_sockets(self, use_sockets: bool) -> None:
159
+ """Set whether to use Unix Domain Sockets or TCP Sockets for gRPC"""
160
+ self._grpc_use_tcp_sockets = use_sockets
161
+
162
+ @property
163
+ def grpc_allow_network_connections(self) -> bool:
164
+ """Get whether to allow listening on all networks if using TCP Sockets for gRPC"""
165
+ return self._grpc_use_tcp_sockets
166
+
167
+ @grpc_allow_network_connections.setter
168
+ def grpc_allow_network_connections(self, allow: bool) -> None:
169
+ """Set whether to allow listening on all networks if using TCP Sockets for gRPC"""
170
+ self._grpc_allow_network_connections = allow
171
+
172
+ @property
173
+ def grpc_disable_tls(self) -> bool:
174
+ """Get whether to use TLS for TCP Sockets for gRPC"""
175
+ return self._grpc_disable_tls
176
+
177
+ @grpc_disable_tls.setter
178
+ def grpc_disable_tls(self, disable_tls: bool) -> None:
179
+ """Set whether to use TLS for TCP Sockets for gRPC"""
180
+ self._grpc_disable_tls = disable_tls
181
+
182
+ @property
183
+ def grpc_uds_pathname(self) -> Optional[str]:
184
+ """Get the pathname for the UDS file if not using the default for gRPC"""
185
+ return self._grpc_uds_pathname
186
+
187
+ @grpc_uds_pathname.setter
188
+ def grpc_uds_pathname(self, uds_pathname: str) -> None:
189
+ """Set the pathname for the UDS file if not using the default for gRPC"""
190
+ self._grpc_uds_pathname = uds_pathname
191
+
192
+ @property
193
+ def session_name(self) -> str:
194
+ """The gRPC server session name
195
+
196
+ EnSight gRPC calls can include the session name via 'session_name' metadata.
197
+ A client session may provide a session name via this property.
198
+ """
199
+ return self._session_name
200
+
201
+ @session_name.setter
202
+ def session_name(self, name: str) -> None:
203
+ self._session_name = name
204
+
205
+ def shutdown(self, stop_ensight: bool = False, force: bool = False) -> None:
206
+ """Close down the gRPC connection
207
+
208
+ Disconnect all connections to the gRPC server. If stop_ensight is True, send the
209
+ 'Exit' command to the EnSight gRPC server.
210
+
211
+ Parameters
212
+ ----------
213
+ stop_ensight: bool, optional
214
+ if True, send an 'Exit' command to the gRPC server.
215
+ force: bool, optional
216
+ if stop_ensight and force are true, stop EnSight aggressively
217
+ """
218
+ if self.is_connected(): # pragma: no cover
219
+ # if requested, send 'Exit'
220
+ if stop_ensight: # pragma: no cover
221
+ # the gRPC ExitRequest is exactly that, a request in some
222
+ # cases the operation needs to be forced
223
+ if force: # pragma: no cover
224
+ try:
225
+ self.command("ensight.exit(0)", do_eval=False)
226
+ except IOError: # pragma: no cover
227
+ # we expect this as the exit can result in the gRPC call failing
228
+ pass # pragma: no cover
229
+ else:
230
+ if self._stub: # pragma: no cover
231
+ _ = self._stub.Exit(
232
+ ensight_pb2.ExitRequest(), metadata=self._metadata()
233
+ ) # pragma: no cover
234
+ # clean up control objects
235
+ self._stub = None
236
+ self._dsg_stub = None
237
+ if self._channel:
238
+ self._channel.close()
239
+ self._channel = None
240
+ if self._shmem_client:
241
+ if self._shmem_module:
242
+ self._shmem_module.stream_destroy(self._shmem_client)
243
+ else:
244
+ self.command("ensight_grpc_shmem.stream_destroy(enscl._shmem_client)")
245
+ self._shmem_client = None
246
+
247
+ def is_connected(self) -> bool:
248
+ """Check to see if the gRPC connection is live
249
+
250
+ Returns
251
+ -------
252
+ True if the connection is active.
253
+ """
254
+ return self._channel is not None
255
+
256
+ def connect(self, timeout: float = 15.0) -> None:
257
+ """Establish the gRPC connection to EnSight
258
+
259
+ Attempt to connect to an EnSight gRPC server using the host and port
260
+ established by the constructor. Note on failure, this function just
261
+ returns, but is_connected() will return False.
262
+
263
+ Parameters
264
+ ----------
265
+ timeout: float
266
+ how long to wait for the connection to timeout
267
+ """
268
+ if self.is_connected():
269
+ return
270
+ # set up the channel
271
+ transport_mode = None
272
+ host = None
273
+ port = None
274
+ uds_service = None
275
+ uds_dir = None
276
+ options = [
277
+ ("grpc.max_receive_message_length", -1),
278
+ ("grpc.max_send_message_length", -1),
279
+ ("grpc.testing.fixed_reconnect_backoff_ms", 1100),
280
+ ]
281
+ if self._grpc_use_tcp_sockets:
282
+ host = self._host
283
+ transport_mode = "mtls"
284
+ if self._grpc_disable_tls:
285
+ transport_mode = "insecure"
286
+ port = self._port
287
+ else:
288
+ host = "127.0.0.1"
289
+ if sys.platform == "win32":
290
+ transport_mode = "wnua"
291
+ port = self._port
292
+ else:
293
+ transport_mode = "uds"
294
+ if self._grpc_uds_pathname:
295
+ uds_service = os.path.basename(self._grpc_uds_pathname)
296
+ uds_dir = os.path.dirname(self._grpc_uds_pathname)
297
+ else:
298
+ uds_dir = "/tmp"
299
+ uds_service = "greeter"
300
+ # Ignore the security options if the version of EnSight cannot handle them
301
+ if self._disable_grpc_options:
302
+ transport_mode = "insecure"
303
+ host = self._host
304
+ port = self._port
305
+ uds_dir = None
306
+ uds_service = None
307
+ self._channel = create_channel(
308
+ host=host,
309
+ port=port,
310
+ transport_mode=transport_mode,
311
+ uds_dir=uds_dir,
312
+ uds_service=uds_service,
313
+ grpc_options=options,
314
+ )
315
+ try:
316
+ grpc.channel_ready_future(self._channel).result(timeout=timeout)
317
+ except grpc.FutureTimeoutError: # pragma: no cover
318
+ self._channel = None # pragma: no cover
319
+ return # pragma: no cover
320
+ # hook up the stub interface
321
+ self._stub = ensight_pb2_grpc.EnSightServiceStub(self._channel)
322
+ self._dsg_stub = dynamic_scene_graph_pb2_grpc.DynamicSceneGraphServiceStub(self._channel)
323
+
324
+ def _metadata(self) -> List[Tuple[bytes, Union[str, bytes]]]:
325
+ """Compute the gRPC stream metadata
326
+
327
+ Compute the list to be passed to the gRPC calls for things like security
328
+ and the session name.
329
+
330
+ """
331
+ ret: List[Tuple[bytes, Union[str, bytes]]] = list()
332
+ s: Union[str, bytes]
333
+ if self._security_token: # pragma: no cover
334
+ s = self._security_token
335
+ if type(s) == str: # pragma: no cover
336
+ s = s.encode("utf-8")
337
+ ret.append((b"shared_secret", s))
338
+ if self.session_name: # pragma: no cover
339
+ s = self.session_name.encode("utf-8")
340
+ ret.append((b"session_name", s))
341
+ return ret
342
+
343
+ def render(
344
+ self,
345
+ width: int = 640,
346
+ height: int = 480,
347
+ aa: int = 1,
348
+ png: bool = True,
349
+ highlighting: bool = False,
350
+ ) -> bytes:
351
+ """Generate a rendering of the current EnSight scene
352
+
353
+ Render the current scene at a specific size and using a specific number of anti-aliasing
354
+ passes. The return value can be a byte array (width*height*3) bytes or a PNG image.
355
+
356
+ Parameters
357
+ ----------
358
+ width: int, optional
359
+ width of the image to render
360
+ height: int, optional
361
+ height of the image to render
362
+ aa: int, optional
363
+ number of antialiasing passes to use in generating the image
364
+ png: bool, optional
365
+ if True, the return value is a PNG image bytestream. Otherwise, it is a simple
366
+ bytes object with width*height*3 values.
367
+ highlighting: bool, optional
368
+ if True, selection highlighting will be included in the image.
369
+
370
+ Returns
371
+ -------
372
+ bytes
373
+ bytes object representation of the rendered image
374
+
375
+ Raises
376
+ ------
377
+ IOError if the operation fails
378
+ """
379
+ self.connect()
380
+ ret_type = ensight_pb2.RenderRequest.IMAGE_RAW
381
+ if png: # pragma: no cover
382
+ ret_type = ensight_pb2.RenderRequest.IMAGE_PNG
383
+ response: Any
384
+ try:
385
+ if self._stub: # pragma: no cover
386
+ response = self._stub.RenderImage(
387
+ ensight_pb2.RenderRequest(
388
+ type=ret_type,
389
+ image_width=width,
390
+ image_height=height,
391
+ image_aa_passes=aa,
392
+ include_highlighting=highlighting,
393
+ ),
394
+ metadata=self._metadata(),
395
+ )
396
+ except Exception: # pragma: no cover
397
+ raise IOError("gRPC connection dropped") # pragma: no cover
398
+ return response.value
399
+
400
+ def geometry(self) -> bytes:
401
+ """Return the current scene geometry in glTF format
402
+
403
+ Package up the geometry currently being viewed in the EnSight session as
404
+ a glTF stream. Return this stream as an array of byte. Note: no
405
+ intermediate files are utilized.
406
+
407
+ Note: currently there is a limitation of glTF files to 2GB
408
+
409
+ Returns
410
+ -------
411
+ bytes object representation of the glTF file
412
+
413
+ Raises
414
+ ------
415
+ IOError if the operation fails
416
+ """
417
+ self.connect()
418
+ response: Any
419
+ try:
420
+ if self._stub: # pragma: no cover
421
+ response = self._stub.GetGeometry(
422
+ ensight_pb2.GeometryRequest(type=ensight_pb2.GeometryRequest.GEOMETRY_GLB),
423
+ metadata=self._metadata(),
424
+ )
425
+ except Exception: # pragma: no cover
426
+ raise IOError("gRPC connection dropped") # pragma: no cover
427
+ return response.value
428
+
429
+ def command(self, command_string: str, do_eval: bool = True, json: bool = False) -> Any:
430
+ """Send a Python command string to be executed in EnSight
431
+
432
+ The string will be run or evaluated in the EnSight Python interpreter via the
433
+ EnSightService::RunPython() gRPC all. If an exception or other error occurs, this
434
+ function will throw a RuntimeError. If do_eval is False, the return value will be None,
435
+ otherwise it will be the returned string (eval() will not be performed). If json is True,
436
+ the return value will be a JSON representation of the report execution result.
437
+
438
+ Parameters
439
+ ----------
440
+ command_string: str
441
+ The string to execute
442
+ do_eval: bool, optional
443
+ If True, a return value will be computed and returned
444
+ json: bool, optional
445
+ If True and do_eval is True, the return value will be a JSON representation of
446
+ the evaluated value.
447
+
448
+ Returns
449
+ -------
450
+ Any
451
+ None, a string ready for Python eval() or a JSON string.
452
+
453
+ Raises
454
+ ------
455
+ RuntimeError if the operation fails.
456
+ IOError if the communication fails.
457
+ """
458
+ self.connect()
459
+ flags = ensight_pb2.PythonRequest.EXEC_RETURN_PYTHON
460
+ response: Any
461
+ if json: # pragma: no cover
462
+ flags = ensight_pb2.PythonRequest.EXEC_RETURN_JSON # pragma: no cover
463
+ if not do_eval:
464
+ flags = ensight_pb2.PythonRequest.EXEC_NO_RESULT
465
+ try:
466
+ if self._stub: # pragma: no cover
467
+ response = self._stub.RunPython(
468
+ ensight_pb2.PythonRequest(type=flags, command=command_string),
469
+ metadata=self._metadata(),
470
+ )
471
+ except Exception:
472
+ raise IOError("gRPC connection dropped")
473
+ if response.error < 0: # pragma: no cover
474
+ raise RuntimeError(response.value) # pragma: no cover
475
+ if flags == ensight_pb2.PythonRequest.EXEC_NO_RESULT:
476
+ return None
477
+ # This was moved externally so pre-processing could be performed
478
+ # elif flags == ensight_pb2.PythonRequest.EXEC_RETURN_PYTHON:
479
+ # return eval(response.value)
480
+ return response.value
481
+
482
+ def prefix(self) -> str:
483
+ """Return the unique prefix for this instance.
484
+
485
+ Some EnSight gRPC APIs require a unique prefix so that EnSight can handle
486
+ multiple, simultaneous remote connections. This method will generate a GUID-based
487
+ prefix.
488
+
489
+ Returns
490
+ -------
491
+ str
492
+ A unique (for this session) prefix string of the form: grpc://{uuid}/
493
+ """
494
+ # prefix URIs will have the format: "grpc://{uuid}/{callbackname}?enum={}&uid={}"
495
+ if self._prefix is None:
496
+ self._prefix = "grpc://" + str(uuid.uuid1()) + "/"
497
+ return self._prefix
498
+
499
+ def event_stream_enable(self, callback: Optional[Callable] = None) -> None:
500
+ """Enable a simple gRPC-based event stream from EnSight
501
+
502
+ This method makes a EnSightService::GetEventStream() gRPC call into EnSight, returning
503
+ an ensightservice::EventReply stream. The method creates a thread to hold this
504
+ stream open and read new events from it. The thread adds the event strings to
505
+ a list of events stored on this instance. If callback is not None, the object
506
+ will be called with the event string, otherwise they can be retrieved using get_event().
507
+ """
508
+ if self._event_stream is not None: # pragma: no cover
509
+ return # pragma: no cover
510
+ self._event_callback = callback
511
+ self.connect()
512
+ if self._stub: # pragma: no cover
513
+ self._event_stream = self._stub.GetEventStream(
514
+ ensight_pb2.EventStreamRequest(prefix=self.prefix()),
515
+ metadata=self._metadata(),
516
+ )
517
+ self._event_thread = threading.Thread(target=self._poll_events)
518
+ self._event_thread.daemon = True
519
+ self._event_thread.start()
520
+
521
+ def event_stream_is_enabled(self) -> bool:
522
+ """Check to see if the event stream is enabled
523
+
524
+ If an event stream has been successfully established via
525
+ event_stream_enable(), then this function returns True.
526
+
527
+ Returns
528
+ -------
529
+ True if a ensightservice::EventReply steam is active
530
+ """
531
+ return self._event_stream is not None # pragma: no cover
532
+
533
+ def dynamic_scene_graph_stream(self, client_cmds): # pragma: no cover
534
+ """Open up a dynamic scene graph stream
535
+
536
+ Make a DynamicSceneGraphService::GetSceneStream() rpc call and return
537
+ a ensightservice::SceneUpdateCommand stream instance.
538
+
539
+ Parameters
540
+ ----------
541
+ client_cmds
542
+ iterator that produces ensightservice::SceneClientCommand objects
543
+
544
+ Returns
545
+ -------
546
+ ensightservice::SceneUpdateCommand stream instance
547
+ """
548
+ self.connect()
549
+ return self._dsg_stub.GetSceneStream(client_cmds, metadata=self._metadata())
550
+
551
+ def get_event(self) -> Optional[str]: # pragma: no cover
552
+ """Retrieve and remove the oldest ensightservice::EventReply string
553
+
554
+ When any of the event streaming systems is enabled, Python threads will receive the
555
+ event records and store them in this instance in an ordered fashion. This method
556
+ retrieves the oldest ensightservice::EventReply string in the queue.
557
+
558
+ Returns
559
+ -------
560
+ None or the oldest event string in the queue.
561
+ """
562
+ try:
563
+ return self._events.pop(0)
564
+ except IndexError:
565
+ return None
566
+
567
+ def _put_event(self, evt: "ensight_pb2.EventReply") -> None:
568
+ """Add an event record to the event queue on this instance
569
+
570
+ This method is used by threads to make the events they receive available to
571
+ calling applications via get_event().
572
+ """
573
+ if self._event_callback: # pragma: no cover
574
+ self._event_callback(evt.tag)
575
+ return
576
+ self._events.append(evt.tag) # pragma: no cover
577
+
578
+ def _poll_events(self) -> None:
579
+ """Internal method to handle event streams
580
+
581
+ This method is called by a Python thread to read events via the established
582
+ ensightservice::EventReply stream.
583
+ """
584
+ try:
585
+ while self._stub is not None: # pragma: no cover
586
+ evt = self._event_stream.next()
587
+ self._put_event(evt)
588
+ except Exception:
589
+ # signal that the gRPC connection has broken
590
+ self._event_stream = None
591
+ self._event_thread = None
592
+
593
+ def _attempt_shared_mem_import(self):
594
+ try:
595
+ import ensight_grpc_shmem
596
+
597
+ self._shmem_module = ensight_grpc_shmem
598
+ except ModuleNotFoundError:
599
+ try:
600
+ self.command("import enve", do_eval=False)
601
+ cei_home = eval(self.command("enve.home()"))
602
+ self.command("import ceiversion", do_eval=False)
603
+ cei_version = eval(self.command("ceiversion.version_suffix"))
604
+ self.command("import sys", do_eval=False)
605
+ py_version = eval(self.command("sys.version_info[:3]"))
606
+ is_win = True if "Win" in platform.system() else False
607
+ plat = "win64" if is_win else "linux_2.6_64"
608
+ _lib = "DLLs" if is_win else f"lib/python{py_version[0]}.{py_version[1]}"
609
+ dll_loc = os.path.join(
610
+ cei_home,
611
+ f"apex{cei_version}",
612
+ "machines",
613
+ plat,
614
+ f"Python-{py_version[0]}.{py_version[1]}.{py_version[2]}",
615
+ _lib,
616
+ )
617
+ if os.path.exists(dll_loc):
618
+ sys.path.append(dll_loc)
619
+ import ensight_grpc_shmem
620
+
621
+ self._shmem_module = ensight_grpc_shmem
622
+ except ModuleNotFoundError:
623
+ pass
624
+
625
+ @classmethod
626
+ def _find_filename(cls, size=1024 * 1024 * 25):
627
+ """Create a file on disk to support shared memory transport.
628
+
629
+ A file, 25MB in size, will be created using the pid of the current
630
+ process to generate the filename. It will be located in a temporary
631
+ directory.
632
+ """
633
+ tempdir = tempfile.mkdtemp(prefix="pyensight_shmem")
634
+ for i in range(100):
635
+ filename = os.path.join(tempdir, "shmem_{}.bin".format(os.getpid() + i))
636
+ if not os.path.exists(filename):
637
+ try:
638
+ tmp = open(filename, "wb")
639
+ tmp.write(b"\0" * size) # 25MB
640
+ tmp.close()
641
+ return filename
642
+ except Exception:
643
+ pass
644
+ return None
645
+
646
+ def get_image(self):
647
+ """Retrieve the current EnSight image.
648
+
649
+ When any of the image streaming systems is enabled, Python threads will receive the
650
+ most recent image and store them in this instance. The frame stored in this instance
651
+ can be accessed by calling this method
652
+
653
+ Returns
654
+ -------
655
+ (tuple):
656
+ A tuple containing a dictionary defining the image binary
657
+ (pixels=bytearray, width=w, height=h) and the image frame number.
658
+ """
659
+ return self._image, self._image_number
660
+
661
+ def _start_sub_service(self):
662
+ """Start a gRPC client service.
663
+ When the client calls one subscribe_events() or subscribe_images() with the
664
+ connection set to GRPC, the interface requires the client to start a gRPC server
665
+ that EnSight will call back to with event/image messages. This method starts
666
+ such a gRPC server."""
667
+ try:
668
+ if self._sub_service is not None:
669
+ return
670
+ self._sub_service = _EnSightSubServicer(parent=self)
671
+ self._sub_service.start()
672
+ except Exception:
673
+ self._sub_service = None
674
+
675
+ def subscribe_images(self, flip_vertical=False, use_shmem=True):
676
+ """Subscribe to an image stream.
677
+
678
+ This methond makes a EnSightService::SubscribeImages() gRPC call. If
679
+ use_shmem is False, the transport system will be made over gRPC. It causes
680
+ EnSight to make a reverse gRPC connection over with gRPC calls with the
681
+ various images will be made. If use_shmem is True (the default), the \ref shmem will be used.
682
+
683
+ Parameters
684
+ ---------
685
+ flip_vertical: bool
686
+ If True, the image pixels will be flipped over the X axis
687
+ use_shmem: bool
688
+ If True, use the shared memory transport, otherwise use reverse gRPC"""
689
+ self.connect()
690
+ if use_shmem:
691
+ try:
692
+ # we need a shared memory file
693
+ self._shmem_filename = self._find_filename()
694
+ if self._shmem_filename is not None:
695
+ conn_type = ensight_pb2.SubscribeImageOptions.SHARED_MEM
696
+ options = dict(filename=self._shmem_filename)
697
+ image_options = ensight_pb2.SubscribeImageOptions(
698
+ prefix=self.prefix(),
699
+ type=conn_type,
700
+ options=options,
701
+ flip_vertical=flip_vertical,
702
+ chunk=False,
703
+ )
704
+ _ = self._stub.SubscribeImages(image_options, metadata=self._metadata())
705
+ # start the local server
706
+ if not self._shmem_module:
707
+ self._attempt_shared_mem_import()
708
+ if self._shmem_module:
709
+ self._shmem_client = self._shmem_module.stream_create(self._shmem_filename)
710
+ else:
711
+ self.command("import ensight_grpc_shmem", do_eval=False)
712
+ to_send = self._shmem_filename.replace("\\", "\\\\")
713
+ self.command(
714
+ f"enscl._shmem_client = ensight_grpc_shmem.stream_create('{to_send}')",
715
+ do_eval=False,
716
+ )
717
+ if self.command("enscl._shmem_client is not None"):
718
+ self._shmem_client = True
719
+
720
+ # turn on the polling thread
721
+ self._image_thread = threading.Thread(target=self._poll_images)
722
+ self._image_thread.daemon = True
723
+ self._image_thread.start()
724
+ return
725
+ except Exception as e:
726
+ print("Unable to subscribe to an image stream via shared memory: {}".format(str(e)))
727
+
728
+ self._start_sub_service()
729
+ conn_type = ensight_pb2.SubscribeImageOptions.GRPC
730
+ options = {}
731
+ if self._sub_service:
732
+ options = dict(uri=self._sub_service._uri)
733
+ image_options = ensight_pb2.SubscribeImageOptions(
734
+ prefix=self.prefix(),
735
+ type=conn_type,
736
+ options=options,
737
+ flip_vertical=flip_vertical,
738
+ chunk=True,
739
+ )
740
+ _ = self._stub.SubscribeImages(image_options, metadata=self._metadata())
741
+
742
+ def image_stream_enable(self, flip_vertical=False):
743
+ """Enable a simple gRPC-based image stream from EnSight.
744
+
745
+ This method makes a EnSightService::GetImageStream() gRPC call into EnSight, returning
746
+ an ensightservice::ImageReply stream. The method creates a thread to hold this
747
+ stream open and read new image frames from it. The thread places the read images
748
+ in this object. An external application can retrieve the most recent one using
749
+ get_image().
750
+
751
+ Parameters
752
+ ----------
753
+ flip_vertical: bool
754
+ If True, the image will be flipped over the X axis before being sent from EnSight."""
755
+ if self._image_stream is not None:
756
+ return
757
+ self.connect()
758
+ self._image_stream = self._stub.GetImageStream(
759
+ ensight_pb2.ImageStreamRequest(flip_vertical=flip_vertical, chunk=True),
760
+ metadata=self._metadata(),
761
+ )
762
+ self._image_thread = threading.Thread(target=self._poll_images)
763
+ self._image_thread.daemon = True
764
+ self._image_thread.start()
765
+
766
+ def _put_image(self, the_image):
767
+ """Store an image on this instance.
768
+
769
+ This method is used by threads to store the latest image they receive
770
+ so it can be accessed by get_image.
771
+ """
772
+ self._image = the_image
773
+ self._image_number += 1
774
+
775
+ def image_stream_is_enabled(self):
776
+ """Check to see if the image stream is enabled.
777
+
778
+ If an image stream has been successfully established via image_stream_enable(),
779
+ then this function returns True.
780
+
781
+ Returns
782
+ -------
783
+ (bool):
784
+ True if a ensightservice::ImageReply steam is active
785
+ """
786
+ return self._image_stream is not None
787
+
788
+ def _poll_images(self):
789
+ """Handle image streams.
790
+
791
+ This method is called by a Python thread to read imagery via the shared memory
792
+ transport system or the the ensightservice::ImageReply stream.
793
+ """
794
+ try:
795
+ while self._stub is not None:
796
+ if self._shmem_client:
797
+ if self._shmem_module:
798
+ img = self._shmem_module.stream_lock(self._shmem_client)
799
+ else:
800
+ img = self.command("ensight_grpc_shmem.stream_lock(enscl._shmem_client)")
801
+ if type(img) is dict:
802
+ the_image = dict(
803
+ pixels=img["pixeldata"], width=img["width"], height=img["height"]
804
+ )
805
+ self._put_image(the_image)
806
+ if self._shmem_module:
807
+ self._shmem_module.stream_unlock(self._shmem_client)
808
+ else:
809
+ self.command(
810
+ "ensight_grpc_shmem.stream_unlock(enscl._shmem_client)",
811
+ do_eval=False,
812
+ )
813
+
814
+ if self._image_stream is not None:
815
+ img = self._image_stream.next()
816
+ buffer = img.pixels
817
+
818
+ while not img.final:
819
+ img = self._image_stream.next()
820
+ buffer += img.pixels
821
+
822
+ the_image = dict(pixels=buffer, width=img.width, height=img.height)
823
+ self._put_image(the_image)
824
+ except Exception:
825
+ # signal that the gRPC connection has broken
826
+ self._image_stream = None
827
+ self._image_thread = None
828
+ self._image = None
829
+
830
+
831
+ class _EnSightSubServicer(ensight_pb2_grpc.EnSightSubscriptionServicer):
832
+ """Internal class handling reverse subscription connections.
833
+ The EnSight gRPC interface has a mechanism for reversing the gRPC
834
+ streams called Subscriptions. Image and event streams can be
835
+ subscribed to. In this mode, the client application starts a
836
+ gRPC server that implements the EnSightSubscription protocol.
837
+ EnSight will connect back to the client using this protocol and
838
+ send images/events back to the client as regular (non-stream)
839
+ rpc calls. This can be useful in situations where it is difficult
840
+ keep a long-running stream alive.
841
+ The EnSightSubServicer class implements a gRPC server for the client application.
842
+ """
843
+
844
+ def __init__(self, parent: Optional["EnSightGRPC"] = None):
845
+ self._server: Optional["grpc.Server"] = None
846
+ self._uri: str = ""
847
+ self._parent = parent
848
+
849
+ def PublishEvent(self, request: Any, context: Any) -> "ensight_pb2.GenericResponse":
850
+ """Publish an event to the remote server."""
851
+ if self._parent is not None:
852
+ self._parent._put_event(request)
853
+ return ensight_pb2.GenericResponse(str="Event Published")
854
+
855
+ def PublishImage(self, request_iterator: Any, context: Any) -> "ensight_pb2.GenericResponse":
856
+ """Publish a single image (possibly in chucks) to the remote server."""
857
+ img: Any = request_iterator.next()
858
+ buffer = img.pixels
859
+ while not img.final:
860
+ img = request_iterator.next()
861
+ buffer += img.pixels
862
+ the_image = dict(pixels=buffer, width=img.width, height=img.height)
863
+ if self._parent is not None:
864
+ self._parent._put_image(the_image)
865
+ return ensight_pb2.GenericResponse(str="Image Published")
866
+
867
+ def start(self):
868
+ """Start the gRPC server to be used for the EnSight Subscription Service."""
869
+ self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
870
+ ensight_pb2_grpc.add_EnSightSubscriptionServicer_to_server(self, self._server)
871
+ # Start the server on localhost with a random port
872
+ port = self._server.add_insecure_port("localhost:0")
873
+ self._uri = "localhost:" + str(port)
874
+ self._server.start()