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.

@@ -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()