ansys-pyensight-core 0.9.1__py3-none-any.whl → 0.9.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ansys-pyensight-core might be problematic. Click here for more details.
- ansys/pyensight/core/__init__.py +1 -1
- ansys/pyensight/core/common.py +17 -0
- ansys/pyensight/core/dockerlauncher.py +4 -0
- ansys/pyensight/core/dvs.py +792 -0
- ansys/pyensight/core/ensight_grpc.py +302 -0
- ansys/pyensight/core/libuserd.py +200 -76
- ansys/pyensight/core/utils/omniverse.py +202 -77
- ansys/pyensight/core/utils/omniverse_cli.py +19 -11
- ansys/pyensight/core/utils/omniverse_dsg_server.py +113 -44
- ansys/pyensight/core/utils/omniverse_glb_server.py +32 -10
- {ansys_pyensight_core-0.9.1.dist-info → ansys_pyensight_core-0.9.3.dist-info}/METADATA +3 -2
- {ansys_pyensight_core-0.9.1.dist-info → ansys_pyensight_core-0.9.3.dist-info}/RECORD +14 -13
- {ansys_pyensight_core-0.9.1.dist-info → ansys_pyensight_core-0.9.3.dist-info}/LICENSE +0 -0
- {ansys_pyensight_core-0.9.1.dist-info → ansys_pyensight_core-0.9.3.dist-info}/WHEEL +0 -0
|
@@ -4,6 +4,11 @@ This package defines the EnSightGRPC class which provides a simpler
|
|
|
4
4
|
interface to the EnSight gRPC interface, including event streams.
|
|
5
5
|
|
|
6
6
|
"""
|
|
7
|
+
from concurrent import futures
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
7
12
|
import threading
|
|
8
13
|
from typing import Any, Callable, List, Optional, Tuple, Union
|
|
9
14
|
import uuid
|
|
@@ -47,6 +52,14 @@ class EnSightGRPC(object):
|
|
|
47
52
|
# Callback for events (self._events not used)
|
|
48
53
|
self._event_callback: Optional[Callable] = None
|
|
49
54
|
self._prefix: Optional[str] = None
|
|
55
|
+
self._shmem_module = None
|
|
56
|
+
self._shmem_filename: Optional[str] = None
|
|
57
|
+
self._shmem_client = None
|
|
58
|
+
self._image_stream = None
|
|
59
|
+
self._image_thread = None
|
|
60
|
+
self._image = None
|
|
61
|
+
self._image_number = 0
|
|
62
|
+
self._sub_service = None
|
|
50
63
|
|
|
51
64
|
@property
|
|
52
65
|
def host(self) -> str:
|
|
@@ -120,6 +133,12 @@ class EnSightGRPC(object):
|
|
|
120
133
|
if self._channel:
|
|
121
134
|
self._channel.close()
|
|
122
135
|
self._channel = None
|
|
136
|
+
if self._shmem_client:
|
|
137
|
+
if self._shmem_module:
|
|
138
|
+
self._shmem_module.stream_destroy(self._shmem_client)
|
|
139
|
+
else:
|
|
140
|
+
self.command("ensight_grpc_shmem.stream_destroy(enscl._shmem_client)")
|
|
141
|
+
self._shmem_client = None
|
|
123
142
|
|
|
124
143
|
def is_connected(self) -> bool:
|
|
125
144
|
"""Check to see if the gRPC connection is live
|
|
@@ -430,3 +449,286 @@ class EnSightGRPC(object):
|
|
|
430
449
|
# signal that the gRPC connection has broken
|
|
431
450
|
self._event_stream = None
|
|
432
451
|
self._event_thread = None
|
|
452
|
+
|
|
453
|
+
def _attempt_shared_mem_import(self):
|
|
454
|
+
try:
|
|
455
|
+
import ensight_grpc_shmem
|
|
456
|
+
|
|
457
|
+
self._shmem_module = ensight_grpc_shmem
|
|
458
|
+
except ModuleNotFoundError:
|
|
459
|
+
try:
|
|
460
|
+
self.command("import enve", do_eval=False)
|
|
461
|
+
cei_home = eval(self.command("enve.home()"))
|
|
462
|
+
self.command("import ceiversion", do_eval=False)
|
|
463
|
+
cei_version = eval(self.command("ceiversion.version_suffix"))
|
|
464
|
+
self.command("import sys", do_eval=False)
|
|
465
|
+
py_version = eval(self.command("sys.version_info[:3]"))
|
|
466
|
+
is_win = True if "Win" in platform.system() else False
|
|
467
|
+
plat = "win64" if is_win else "linux_2.6_64"
|
|
468
|
+
_lib = "DLLs" if is_win else f"lib/python{py_version[0]}.{py_version[1]}"
|
|
469
|
+
dll_loc = os.path.join(
|
|
470
|
+
cei_home,
|
|
471
|
+
f"apex{cei_version}",
|
|
472
|
+
"machines",
|
|
473
|
+
plat,
|
|
474
|
+
f"Python-{py_version[0]}.{py_version[1]}.{py_version[2]}",
|
|
475
|
+
_lib,
|
|
476
|
+
)
|
|
477
|
+
if os.path.exists(dll_loc):
|
|
478
|
+
sys.path.append(dll_loc)
|
|
479
|
+
import ensight_grpc_shmem
|
|
480
|
+
|
|
481
|
+
self._shmem_module = ensight_grpc_shmem
|
|
482
|
+
except ModuleNotFoundError:
|
|
483
|
+
pass
|
|
484
|
+
|
|
485
|
+
@classmethod
|
|
486
|
+
def _find_filename(cls, size=1024 * 1024 * 25):
|
|
487
|
+
"""Create a file on disk to support shared memory transport.
|
|
488
|
+
|
|
489
|
+
A file, 25MB in size, will be created using the pid of the current
|
|
490
|
+
process to generate the filename. It will be located in a temporary
|
|
491
|
+
directory.
|
|
492
|
+
"""
|
|
493
|
+
tempdir = tempfile.mkdtemp(prefix="pyensight_shmem")
|
|
494
|
+
for i in range(100):
|
|
495
|
+
filename = os.path.join(tempdir, "shmem_{}.bin".format(os.getpid() + i))
|
|
496
|
+
if not os.path.exists(filename):
|
|
497
|
+
try:
|
|
498
|
+
tmp = open(filename, "wb")
|
|
499
|
+
tmp.write(b"\0" * size) # 25MB
|
|
500
|
+
tmp.close()
|
|
501
|
+
return filename
|
|
502
|
+
except Exception:
|
|
503
|
+
pass
|
|
504
|
+
return None
|
|
505
|
+
|
|
506
|
+
def get_image(self):
|
|
507
|
+
"""Retrieve the current EnSight image.
|
|
508
|
+
|
|
509
|
+
When any of the image streaming systems is enabled, Python threads will receive the
|
|
510
|
+
most recent image and store them in this instance. The frame stored in this instance
|
|
511
|
+
can be accessed by calling this method
|
|
512
|
+
|
|
513
|
+
Returns
|
|
514
|
+
-------
|
|
515
|
+
(tuple):
|
|
516
|
+
A tuple containing a dictionary defining the image binary
|
|
517
|
+
(pixels=bytearray, width=w, height=h) and the image frame number.
|
|
518
|
+
"""
|
|
519
|
+
return self._image, self._image_number
|
|
520
|
+
|
|
521
|
+
def _start_sub_service(self):
|
|
522
|
+
"""Start a gRPC client service.
|
|
523
|
+
When the client calls one subscribe_events() or subscribe_images() with the
|
|
524
|
+
connection set to GRPC, the interface requires the client to start a gRPC server
|
|
525
|
+
that EnSight will call back to with event/image messages. This method starts
|
|
526
|
+
such a gRPC server."""
|
|
527
|
+
try:
|
|
528
|
+
if self._sub_service is not None:
|
|
529
|
+
return
|
|
530
|
+
self._sub_service = _EnSightSubServicer(parent=self)
|
|
531
|
+
self._sub_service.start()
|
|
532
|
+
except Exception:
|
|
533
|
+
self._sub_service = None
|
|
534
|
+
|
|
535
|
+
def subscribe_images(self, flip_vertical=False, use_shmem=True):
|
|
536
|
+
"""Subscribe to an image stream.
|
|
537
|
+
|
|
538
|
+
This methond makes a EnSightService::SubscribeImages() gRPC call. If
|
|
539
|
+
use_shmem is False, the transport system will be made over gRPC. It causes
|
|
540
|
+
EnSight to make a reverse gRPC connection over with gRPC calls with the
|
|
541
|
+
various images will be made. If use_shmem is True (the default), the \ref shmem will be used.
|
|
542
|
+
|
|
543
|
+
Parameters
|
|
544
|
+
---------
|
|
545
|
+
flip_vertical: bool
|
|
546
|
+
If True, the image pixels will be flipped over the X axis
|
|
547
|
+
use_shmem: bool
|
|
548
|
+
If True, use the shared memory transport, otherwise use reverse gRPC"""
|
|
549
|
+
self.connect()
|
|
550
|
+
if use_shmem:
|
|
551
|
+
try:
|
|
552
|
+
# we need a shared memory file
|
|
553
|
+
self._shmem_filename = self._find_filename()
|
|
554
|
+
if self._shmem_filename is not None:
|
|
555
|
+
conn_type = ensight_pb2.SubscribeImageOptions.SHARED_MEM
|
|
556
|
+
options = dict(filename=self._shmem_filename)
|
|
557
|
+
image_options = ensight_pb2.SubscribeImageOptions(
|
|
558
|
+
prefix=self.prefix(),
|
|
559
|
+
type=conn_type,
|
|
560
|
+
options=options,
|
|
561
|
+
flip_vertical=flip_vertical,
|
|
562
|
+
chunk=False,
|
|
563
|
+
)
|
|
564
|
+
_ = self._stub.SubscribeImages(image_options, metadata=self._metadata())
|
|
565
|
+
# start the local server
|
|
566
|
+
if not self._shmem_module:
|
|
567
|
+
self._attempt_shared_mem_import()
|
|
568
|
+
if self._shmem_module:
|
|
569
|
+
self._shmem_client = self._shmem_module.stream_create(self._shmem_filename)
|
|
570
|
+
else:
|
|
571
|
+
self.command("import ensight_grpc_shmem", do_eval=False)
|
|
572
|
+
to_send = self._shmem_filename.replace("\\", "\\\\")
|
|
573
|
+
self.command(
|
|
574
|
+
f"enscl._shmem_client = ensight_grpc_shmem.stream_create('{to_send}')",
|
|
575
|
+
do_eval=False,
|
|
576
|
+
)
|
|
577
|
+
if self.command("enscl._shmem_client is not None"):
|
|
578
|
+
self._shmem_client = True
|
|
579
|
+
|
|
580
|
+
# turn on the polling thread
|
|
581
|
+
self._image_thread = threading.Thread(target=self._poll_images)
|
|
582
|
+
self._image_thread.daemon = True
|
|
583
|
+
self._image_thread.start()
|
|
584
|
+
return
|
|
585
|
+
except Exception as e:
|
|
586
|
+
print("Unable to subscribe to an image stream via shared memory: {}".format(str(e)))
|
|
587
|
+
|
|
588
|
+
self._start_sub_service()
|
|
589
|
+
conn_type = ensight_pb2.SubscribeImageOptions.GRPC
|
|
590
|
+
options = {}
|
|
591
|
+
if self._sub_service:
|
|
592
|
+
options = dict(uri=self._sub_service._uri)
|
|
593
|
+
image_options = ensight_pb2.SubscribeImageOptions(
|
|
594
|
+
prefix=self.prefix(),
|
|
595
|
+
type=conn_type,
|
|
596
|
+
options=options,
|
|
597
|
+
flip_vertical=flip_vertical,
|
|
598
|
+
chunk=True,
|
|
599
|
+
)
|
|
600
|
+
_ = self._stub.SubscribeImages(image_options, metadata=self._metadata())
|
|
601
|
+
|
|
602
|
+
def image_stream_enable(self, flip_vertical=False):
|
|
603
|
+
"""Enable a simple gRPC-based image stream from EnSight.
|
|
604
|
+
|
|
605
|
+
This method makes a EnSightService::GetImageStream() gRPC call into EnSight, returning
|
|
606
|
+
an ensightservice::ImageReply stream. The method creates a thread to hold this
|
|
607
|
+
stream open and read new image frames from it. The thread places the read images
|
|
608
|
+
in this object. An external application can retrieve the most recent one using
|
|
609
|
+
get_image().
|
|
610
|
+
|
|
611
|
+
Parameters
|
|
612
|
+
----------
|
|
613
|
+
flip_vertical: bool
|
|
614
|
+
If True, the image will be flipped over the X axis before being sent from EnSight."""
|
|
615
|
+
if self._image_stream is not None:
|
|
616
|
+
return
|
|
617
|
+
self.connect()
|
|
618
|
+
self._image_stream = self._stub.GetImageStream(
|
|
619
|
+
ensight_pb2.ImageStreamRequest(flip_vertical=flip_vertical, chunk=True),
|
|
620
|
+
metadata=self._metadata(),
|
|
621
|
+
)
|
|
622
|
+
self._image_thread = threading.Thread(target=self._poll_images)
|
|
623
|
+
self._image_thread.daemon = True
|
|
624
|
+
self._image_thread.start()
|
|
625
|
+
|
|
626
|
+
def _put_image(self, the_image):
|
|
627
|
+
"""Store an image on this instance.
|
|
628
|
+
|
|
629
|
+
This method is used by threads to store the latest image they receive
|
|
630
|
+
so it can be accessed by get_image.
|
|
631
|
+
"""
|
|
632
|
+
self._image = the_image
|
|
633
|
+
self._image_number += 1
|
|
634
|
+
|
|
635
|
+
def image_stream_is_enabled(self):
|
|
636
|
+
"""Check to see if the image stream is enabled.
|
|
637
|
+
|
|
638
|
+
If an image stream has been successfully established via image_stream_enable(),
|
|
639
|
+
then this function returns True.
|
|
640
|
+
|
|
641
|
+
Returns
|
|
642
|
+
-------
|
|
643
|
+
(bool):
|
|
644
|
+
True if a ensightservice::ImageReply steam is active
|
|
645
|
+
"""
|
|
646
|
+
return self._image_stream is not None
|
|
647
|
+
|
|
648
|
+
def _poll_images(self):
|
|
649
|
+
"""Handle image streams.
|
|
650
|
+
|
|
651
|
+
This method is called by a Python thread to read imagery via the shared memory
|
|
652
|
+
transport system or the the ensightservice::ImageReply stream.
|
|
653
|
+
"""
|
|
654
|
+
try:
|
|
655
|
+
while self._stub is not None:
|
|
656
|
+
if self._shmem_client:
|
|
657
|
+
if self._shmem_module:
|
|
658
|
+
img = self._shmem_module.stream_lock(self._shmem_client)
|
|
659
|
+
else:
|
|
660
|
+
img = self.command("ensight_grpc_shmem.stream_lock(enscl._shmem_client)")
|
|
661
|
+
if type(img) is dict:
|
|
662
|
+
the_image = dict(
|
|
663
|
+
pixels=img["pixeldata"], width=img["width"], height=img["height"]
|
|
664
|
+
)
|
|
665
|
+
self._put_image(the_image)
|
|
666
|
+
if self._shmem_module:
|
|
667
|
+
self._shmem_module.stream_unlock(self._shmem_client)
|
|
668
|
+
else:
|
|
669
|
+
self.command(
|
|
670
|
+
"ensight_grpc_shmem.stream_unlock(enscl._shmem_client)",
|
|
671
|
+
do_eval=False,
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if self._image_stream is not None:
|
|
675
|
+
img = self._image_stream.next()
|
|
676
|
+
buffer = img.pixels
|
|
677
|
+
|
|
678
|
+
while not img.final:
|
|
679
|
+
img = self._image_stream.next()
|
|
680
|
+
buffer += img.pixels
|
|
681
|
+
|
|
682
|
+
the_image = dict(pixels=buffer, width=img.width, height=img.height)
|
|
683
|
+
self._put_image(the_image)
|
|
684
|
+
except Exception:
|
|
685
|
+
# signal that the gRPC connection has broken
|
|
686
|
+
self._image_stream = None
|
|
687
|
+
self._image_thread = None
|
|
688
|
+
self._image = None
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
class _EnSightSubServicer(ensight_pb2_grpc.EnSightSubscriptionServicer):
|
|
692
|
+
"""Internal class handling reverse subscription connections.
|
|
693
|
+
The EnSight gRPC interface has a mechanism for reversing the gRPC
|
|
694
|
+
streams called Subscriptions. Image and event streams can be
|
|
695
|
+
subscribed to. In this mode, the client application starts a
|
|
696
|
+
gRPC server that implements the EnSightSubscription protocol.
|
|
697
|
+
EnSight will connect back to the client using this protocol and
|
|
698
|
+
send images/events back to the client as regular (non-stream)
|
|
699
|
+
rpc calls. This can be useful in situations where it is difficult
|
|
700
|
+
keep a long-running stream alive.
|
|
701
|
+
The EnSightSubServicer class implements a gRPC server for the client application.
|
|
702
|
+
"""
|
|
703
|
+
|
|
704
|
+
def __init__(self, parent: Optional["EnSightGRPC"] = None):
|
|
705
|
+
self._server: Optional["grpc.Server"] = None
|
|
706
|
+
self._uri: str = ""
|
|
707
|
+
self._parent = parent
|
|
708
|
+
|
|
709
|
+
def PublishEvent(self, request: Any, context: Any) -> "ensight_pb2.GenericResponse":
|
|
710
|
+
"""Publish an event to the remote server."""
|
|
711
|
+
if self._parent is not None:
|
|
712
|
+
self._parent._put_event(request)
|
|
713
|
+
return ensight_pb2.GenericResponse(str="Event Published")
|
|
714
|
+
|
|
715
|
+
def PublishImage(self, request_iterator: Any, context: Any) -> "ensight_pb2.GenericResponse":
|
|
716
|
+
"""Publish a single image (possibly in chucks) to the remote server."""
|
|
717
|
+
img: Any = request_iterator.next()
|
|
718
|
+
buffer = img.pixels
|
|
719
|
+
while not img.final:
|
|
720
|
+
img = request_iterator.next()
|
|
721
|
+
buffer += img.pixels
|
|
722
|
+
the_image = dict(pixels=buffer, width=img.width, height=img.height)
|
|
723
|
+
if self._parent is not None:
|
|
724
|
+
self._parent._put_image(the_image)
|
|
725
|
+
return ensight_pb2.GenericResponse(str="Image Published")
|
|
726
|
+
|
|
727
|
+
def start(self):
|
|
728
|
+
"""Start the gRPC server to be used for the EnSight Subscription Service."""
|
|
729
|
+
self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
|
|
730
|
+
ensight_pb2_grpc.add_EnSightSubscriptionServicer_to_server(self, self._server)
|
|
731
|
+
# Start the server on localhost with a random port
|
|
732
|
+
port = self._server.add_insecure_port("localhost:0")
|
|
733
|
+
self._uri = "localhost:" + str(port)
|
|
734
|
+
self._server.start()
|