From ec2c4d835b93de6ae4f5a7d52a574f94ca5304ed Mon Sep 17 00:00:00 2001
From: David Hammer <dhammer@mailbox.org>
Date: Wed, 9 Feb 2022 11:45:02 +0100
Subject: [PATCH] Rename SimpleAssember, split output in regular and preview

---
 setup.py                                      |   2 +-
 src/calng/CalibrationManager.py               |   2 +-
 ...impleAssembler.py => DetectorAssembler.py} | 115 +++++++++++++-----
 src/calng/scenes.py                           |   8 +-
 4 files changed, 93 insertions(+), 34 deletions(-)
 rename src/calng/{SimpleAssembler.py => DetectorAssembler.py} (71%)

diff --git a/setup.py b/setup.py
index 31a693e4..389c53da 100644
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,7 @@ setup(name='calng',
               'ManualDsscGeometry = calng.ManualDsscGeometry:ManualDsscGeometry',
               'ManualJungfrauGeometry = calng.ManualJungfrauGeometry:ManualJungfrauGeometry',
               'ShmemToZMQ = calng.ShmemToZMQ:ShmemToZMQ',
-              'SimpleAssembler = calng.SimpleAssembler:SimpleAssembler',
+              'DetectorAssembler = calng.DetectorAssembler:DetectorAssembler',
           ],
 
           'karabo.middlelayer_device': [
diff --git a/src/calng/CalibrationManager.py b/src/calng/CalibrationManager.py
index bde13b4b..20b91305 100644
--- a/src/calng/CalibrationManager.py
+++ b/src/calng/CalibrationManager.py
@@ -86,7 +86,7 @@ class ClassIdsNode(Configurable):
         displayedName='Assembler class',
         description='Device class to use for assembling the matched output of '
                     'a preview layer.',
-        defaultValue='FemDataAssembler',
+        defaultValue='DetectorAssembler',
         accessMode=AccessMode.INITONLY,
         assignment=Assignment.MANDATORY)
 
diff --git a/src/calng/SimpleAssembler.py b/src/calng/DetectorAssembler.py
similarity index 71%
rename from src/calng/SimpleAssembler.py
rename to src/calng/DetectorAssembler.py
index 847265f6..75a11b87 100644
--- a/src/calng/SimpleAssembler.py
+++ b/src/calng/DetectorAssembler.py
@@ -9,6 +9,8 @@ from karabo.bound import (
     FLOAT_ELEMENT,
     IMAGEDATA_ELEMENT,
     INPUT_CHANNEL,
+    NDARRAY_ELEMENT,
+    NODE_ELEMENT,
     KARABO_CLASSINFO,
     OUTPUT_CHANNEL,
     OVERWRITE_ELEMENT,
@@ -27,16 +29,25 @@ from karabo.bound import (
     Trainstamp,
     Unit,
 )
-from karabo.common.api import KARABO_SCHEMA_DISPLAY_TYPE_SCENES as DT_SCENES
 from TrainMatcher import TrainMatcher
-from TrainMatcher import scenes as trainmatcher_scenes
 
 from . import scenes
 from ._version import version as deviceVersion
 
+output_schema = Schema()
+(
+    NODE_ELEMENT(output_schema).key("image").commit(),
+
+    NDARRAY_ELEMENT(output_schema).key("image.data").commit(),
+
+    UINT64_ELEMENT(output_schema).key("trainId").readOnly().commit(),
+)
+
 preview_schema = Schema()
 (
-    IMAGEDATA_ELEMENT(preview_schema).key("image").commit(),
+    NODE_ELEMENT(preview_schema).key("image").commit(),
+
+    IMAGEDATA_ELEMENT(preview_schema).key("image.data").commit(),
 
     UINT64_ELEMENT(preview_schema).key("trainId").readOnly().commit(),
 )
@@ -46,8 +57,8 @@ daq_source_re = re.compile(r".*\/DET\/.*?(\d+):daqOutput")
 
 
 # TODO: merge scene with TrainMatcher's nice overview
-@KARABO_CLASSINFO("SimpleAssembler", deviceVersion)
-class SimpleAssembler(TrainMatcher.TrainMatcher):
+@KARABO_CLASSINFO("DetectorAssembler", deviceVersion)
+class DetectorAssembler(TrainMatcher.TrainMatcher):
     @staticmethod
     def expectedParameters(expected):
         (
@@ -84,8 +95,18 @@ class SimpleAssembler(TrainMatcher.TrainMatcher):
             .defaultValue("image.data")
             .commit(),
 
+            NODE_ELEMENT(expected)
+            .key("preview")
+            .description(
+                "The preview output is intended for Karabo GUI previews. It differs "
+                "from the main output in that it is lower rate (controlled by train "
+                "stride), can be downsampled, and is given the ImageData type for use "
+                "within Karabo."
+            )
+            .commit(),
+
             UINT32_ELEMENT(expected)
-            .key("downsamplingFactor")
+            .key("preview.downsamplingFactor")
             .description(
                 "If greater than 1, the assembled image will be downsampled by this "
                 "factor in x and y dimensions before sending. This is only to save "
@@ -98,7 +119,7 @@ class SimpleAssembler(TrainMatcher.TrainMatcher):
             .commit(),
 
             STRING_ELEMENT(expected)
-            .key("downsamplingFunction")
+            .key("preview.downsamplingFunction")
             .description("Reduction function used during downsampling.")
             .assignmentOptional()
             .defaultValue("nanmax")
@@ -106,6 +127,19 @@ class SimpleAssembler(TrainMatcher.TrainMatcher):
             .reconfigurable()
             .commit(),
 
+            OUTPUT_CHANNEL(expected)
+            .key("preview.output")
+            .dataSchema(preview_schema)
+            .commit(),
+
+            UINT32_ELEMENT(expected)
+            .key("preview.trainStride")
+            .displayedName("Train stride")
+            .description("Only trains which are a multiple of this are sent to preview")
+            .assignmentOptional()
+            .defaultValue(10)
+            .commit(),
+
             INPUT_CHANNEL(expected)
             .key("geometryInput")
             .displayedName("Geometry input")
@@ -113,7 +147,7 @@ class SimpleAssembler(TrainMatcher.TrainMatcher):
 
             OUTPUT_CHANNEL(expected)  # can OVERWRITE_ELEMENT even do this?
             .key("output")
-            .dataSchema(preview_schema)
+            .dataSchema(output_schema)
             .commit(),
         )
 
@@ -130,6 +164,7 @@ class SimpleAssembler(TrainMatcher.TrainMatcher):
         self.KARABO_SLOT(self.requestScene)
 
         self.ask_for_geometry()
+        self.preview_output = self.signalSlotable.getOutputChannel("preview.output")
         self.start()
 
     def requestScene(self, params):
@@ -137,7 +172,7 @@ class SimpleAssembler(TrainMatcher.TrainMatcher):
         scene_name = params.get("name", default="")
         if scene_name == "overview":
             payload = Hash("name", scene_name, "success", True)
-            payload["data"] = scenes.simple_assembler_overview(
+            payload["data"] = scenes.detector_assembler_overview(
                 device_id=self.getInstanceId(),
                 geometry_device_id=self.get("geometryInput.connectedOutputChannels")[
                     0
@@ -195,7 +230,7 @@ class SimpleAssembler(TrainMatcher.TrainMatcher):
     def _send(self, train_id, sources):
         # TODO: adapt to appropriate hook for new TrainMatcher (no _send)
         if self.geometry is None:
-            self.log.WARN("Have not received a geometry yet")
+            self.log.WARN("Have not received a geometry yet, will not send anything")
             return
 
         timestamp = Timestamp(Epochstamp(), Trainstamp(train_id))
@@ -207,36 +242,51 @@ class SimpleAssembler(TrainMatcher.TrainMatcher):
             self.input_buffer[module_index] = np.squeeze(data.get(self._path_to_stack))
             module_indices_unfilled.discard(module_index)
 
-        for unfilled_module in module_indices_unfilled:
-            self.input_buffer[unfilled_module].fill(0)
+        for module_index in module_indices_unfilled:
+            self.input_buffer[module_index].fill(0)
             # TODO: configurable treatment of missing modules
 
         # TODO: reusable output buffer to save on allocation
         assembled, _ = self.geometry.position_modules_fast(self.input_buffer)
 
-        downsampling_factor = self.get("downsamplingFactor")
-        if downsampling_factor > 1:
-            assembled = downsample_2d(
-                assembled,
-                downsampling_factor,
-                reduction_fun=getattr(np, self.get("downsamplingFunction"))
-            )
-
         # TODO: optionally include control data
         out_hash = Hash(
-            "image",
-            ImageData(
-                # TODO: get around this being mirrored...
-                (assembled[::-1, ::-1]).astype(np.int32),
-                Dims(*assembled.shape),
-                Encoding.GRAY,
-            ),
+            "image.data",
+            assembled,
             "trainId",
             train_id,
         )
+        # TODO: just get channel once, reuse (should not need to reinject)
         channel = self.signalSlotable.getOutputChannel("output")
-        channel.write(out_hash, ChannelMetaData(self.getInstanceId(), timestamp))
+        output_metadata = ChannelMetaData(self.getInstanceId(), timestamp)
+        channel.write(out_hash, output_metadata)
         channel.update()
+
+        if train_id % self.unsafe_get("preview.trainStride") == 0:
+            downsampling_factor = self.unsafe_get("preview.downsamplingFactor")
+            if downsampling_factor > 1:
+                assembled = downsample_2d(
+                    assembled,
+                    downsampling_factor,
+                    reduction_fun=getattr(
+                        np, self.unsafe_get("preview.downsamplingFunction")
+                    ),
+                )
+            out_hash = Hash(
+                "image.data",
+                ImageData(
+                    # TODO: get around this being mirrored...
+                    assembled.astype(np.int32)[::-1, ::-1],
+                    Dims(*assembled.shape),
+                    Encoding.GRAY,
+                ),
+                "trainId",
+                train_id,
+            )
+            channel = self.signalSlotable.getOutputChannel("preview.output")
+            channel.write(out_hash, output_metadata)
+            channel.update()
+
         self.rate_out.update()
 
     @functools.lru_cache()
@@ -277,3 +327,12 @@ def downsample_2d(arr, factor, reduction_fun=np.nanmax):
             ), axis=0
         )
     return arr
+
+
+# forward-compatible unsafe_get proposed by @haufs
+if not hasattr(DetectorAssembler, "unsafe_get"):
+    def unsafe_get(self, key):
+        """See base_correction.py"""
+        return self._parameters.get(key)
+
+    setattr(DetectorAssembler, "unsafe_get", unsafe_get)
diff --git a/src/calng/scenes.py b/src/calng/scenes.py
index d550ce91..9edf8c40 100644
--- a/src/calng/scenes.py
+++ b/src/calng/scenes.py
@@ -556,7 +556,7 @@ class AssemblerDeviceStatus(VerticalLayout):
                         height=BASE_INC,
                     ),
                     ComboBoxModel(
-                        keys=[f"{device_id}.downsamplingFactor"],
+                        keys=[f"{device_id}.preview.downsamplingFactor"],
                         width=7 * BASE_INC,
                         height=BASE_INC,
                         klass="EditableComboBox",
@@ -570,7 +570,7 @@ class AssemblerDeviceStatus(VerticalLayout):
                         height=BASE_INC,
                     ),
                     ComboBoxModel(
-                        keys=[f"{device_id}.downsamplingFunction"],
+                        keys=[f"{device_id}.preview.downsamplingFunction"],
                         width=7 * BASE_INC,
                         height=BASE_INC,
                         klass="EditableComboBox",
@@ -795,7 +795,7 @@ def correction_constant_dashboard(
 
 
 @scene_generator
-def simple_assembler_overview(device_id, geometry_device_id):
+def detector_assembler_overview(device_id, geometry_device_id):
     return VerticalLayout(
         HorizontalLayout(
             AssemblerDeviceStatus(device_id),
@@ -818,7 +818,7 @@ def simple_assembler_overview(device_id, geometry_device_id):
             ),
         ),
         titled("Preview image")(boxed(dummy_wrap(DetectorGraphModel)))(
-            keys=[f"{device_id}.preview.output.schema.image"],
+            keys=[f"{device_id}.preview.output.schema.image.data"],
             colormap="viridis",
             width=30 * BASE_INC,
             height=30 * BASE_INC,
-- 
GitLab