From c3a72c6d329a33c9273cbfe04ec554ef95bb4c83 Mon Sep 17 00:00:00 2001
From: David Hammer <dhammer@mailbox.org>
Date: Thu, 23 Sep 2021 14:40:59 +0200
Subject: [PATCH] Working configurability of bad pixel flags,
 md_additional_offset

---
 src/calng/AgipdCorrection.py | 91 +++++++++++++++++++++++------------
 src/calng/agipd_gpu.py       | 93 ++++++++++++++++++++++--------------
 src/calng/base_correction.py |  1 -
 3 files changed, 119 insertions(+), 66 deletions(-)

diff --git a/src/calng/AgipdCorrection.py b/src/calng/AgipdCorrection.py
index dec34dd3..451cd011 100644
--- a/src/calng/AgipdCorrection.py
+++ b/src/calng/AgipdCorrection.py
@@ -3,10 +3,10 @@ import timeit
 import numpy as np
 from karabo.bound import (
     BOOL_ELEMENT,
+    NODE_ELEMENT,
     FLOAT_ELEMENT,
     KARABO_CLASSINFO,
     STRING_ELEMENT,
-    TABLE_ELEMENT,
     UINT32_ELEMENT,
     Hash,
     Schema,
@@ -28,7 +28,7 @@ class AgipdCorrection(BaseCorrection):
         ("offset", CorrectionFlags.OFFSET),
         ("relGainPc", CorrectionFlags.REL_GAIN_PC),
         ("relGainXray", CorrectionFlags.REL_GAIN_XRAY),
-        ("badpixels", CorrectionFlags.BPMASK),
+        ("badPixels", CorrectionFlags.BPMASK),
     )
     _gpu_runner_class = AgipdGpuRunner
 
@@ -121,29 +121,30 @@ class AgipdCorrection(BaseCorrection):
             .commit()
         )
         # TODO: hook this up to actual correction done
-        bad_pixel_selection_schema = Schema()
+        # NOTE: wanted to configure this as table, could not make reaonly table with reconfigurable bools in rows
         (
-            STRING_ELEMENT(bad_pixel_selection_schema).key("name").readOnly().commit(),
-            UINT32_ELEMENT(bad_pixel_selection_schema).key("value").readOnly().commit(),
-            BOOL_ELEMENT(bad_pixel_selection_schema)
-            .key("apply")
-            .assignmentOptional()
-            .defaultValue(True)
-            .reconfigurable()
-            .commit(),
-            TABLE_ELEMENT(expected)
-            .key("corrections.badPixelFlagsApplied")
-            .setNodeSchema(bad_pixel_selection_schema)
-            .assignmentOptional()
-            .defaultValue(
-                [
-                    Hash("name", field.name, "value", field.value, "apply", True)
-                    for field in BadPixelValues
-                ]
+            NODE_ELEMENT(expected)
+            .key("corrections.badPixelFlagsToUse")
+            .displayedName("Bad pixel flags to use")
+            .description(
+                "The booleans under this node allow for selecting a subset of bad "
+                "pixel values to take into account when doing bad pixel masking. "
+                "Upon updating these flags, the map used for bad pixel masking will "
+                "be ANDed with this selection. TEMPORARY NOTE: if you want to toggle "
+                "a disabled flag back on, please reload constants for this to take "
+                "effect (will be triggered automatically in future version)."
             )
-            .reconfigurable()
             .commit(),
         )
+        for field in BadPixelValues:
+            (
+                BOOL_ELEMENT(expected)
+                .key(f"corrections.badPixelFlagsToUse.{field.name}")
+                .assignmentOptional()
+                .defaultValue(True)
+                .reconfigurable()
+                .commit()
+            )
 
     @property
     def input_data_shape(self):
@@ -177,12 +178,19 @@ class AgipdCorrection(BaseCorrection):
             "no-reshape": None,
         }[config.get("dataFormat.outputAxisOrder")]
         self._update_shapes()
+
+        # configurability: overriding md_additional_offset
         if config.get("corrections.overrideMdAdditionalOffset"):
             self._override_md_additional_offset = config.get(
                 "corrections.mdAdditionalOffset"
             )
         else:
             self._override_md_additional_offset = None
+
+        # configurability: disabling subset of bad pixel masking bits
+        self._has_updated_bad_pixel_selection = False
+        self._update_bad_pixel_selection()
+
         self.updateState(State.ON)
 
     def process_input(self, data, metadata):
@@ -307,12 +315,12 @@ class AgipdCorrection(BaseCorrection):
         )
 
     def _load_constant_to_gpu(self, constant_name, constant_data):
+        # TODO: encode correction / constant dependencies in a clever way
         if constant_name == "ThresholdsDark":
             if self.gain_mode is not AgipdGainMode.ADAPTIVE_GAIN:
                 self.log.INFO("Loaded ThresholdsDark ignored due to fixed gain mode")
                 return
             self.gpu_runner.load_thresholds(constant_data)
-            # TODO: encode correction / constant dependencies in a clever way
             if not self.get("corrections.available.thresholding"):
                 self.set("corrections.available.thresholding", True)
                 self.set("corrections.enabled.thresholding", True)
@@ -340,12 +348,13 @@ class AgipdCorrection(BaseCorrection):
                 self.set("corrections.enabled.relGainXray", True)
                 self.set("corrections.preview.relGainXray", True)
         elif "BadPixels" in constant_name:
-            # TODO: implement loading bad pixels
-            self.gpu_runner.load_badpixels_map(constant_data)
-            if not self.get("corrections.available.badpixels"):
-                self.set("corrections.available.badpixels", True)
-                self.set("corrections.enabled.badpixels", True)
-                self.set("corrections.preview.badpixels", True)
+            self.gpu_runner.load_bad_pixels_map(
+                constant_data, override_flags_to_use=self._override_bad_pixel_flags
+            )
+            if not self.get("corrections.available.badPixels"):
+                self.set("corrections.available.badPixels", True)
+                self.set("corrections.enabled.badPixels", True)
+                self.set("corrections.preview.badPixels", True)
 
     def _update_pulse_filter(self, filter_string):
         """Called whenever the pulse filter changes, typically followed by
@@ -358,12 +367,34 @@ class AgipdCorrection(BaseCorrection):
         assert np.max(new_filter) < self.get("dataFormat.memoryCells")
         self.pulse_filter = new_filter
 
+    def _update_bad_pixel_selection(self):
+        selection = 0
+        for field in BadPixelValues:
+            if self.get(f"corrections.badPixelFlagsToUse.{field.name}"):
+                selection |= field
+        self._override_bad_pixel_flags = selection
+
+    def preReconfigure(self, config):
+        super().preReconfigure(config)
+        if any(
+            path.startswith("corrections.badPixelFlagsToUse")
+            for path in config.getPaths()
+        ):
+            self._has_updated_bad_pixel_selection = False
+
     def postReconfigure(self):
         super().postReconfigure()
+
         if self.get("corrections.overrideMdAdditionalOffset"):
-            self._override_md_additional_offset = self.get("corrections.mdAdditionalOffset")
-            self.gpu_runner.md_additional_offset_gpu.fill(
+            self._override_md_additional_offset = self.get(
+                "corrections.mdAdditionalOffset"
+            )
+            self.gpu_runner.md_additional_offset_gpu.override_md_additional_offset(
                 self._override_md_additional_offset
             )
         else:
             self._override_md_additional_offset = None
+
+        if not self._has_updated_bad_pixel_selection:
+            self._update_bad_pixel_selection()
+            self.gpu_runner.override_bad_pixel_flags_to_use(self._override_bad_pixel_flags)
diff --git a/src/calng/agipd_gpu.py b/src/calng/agipd_gpu.py
index b03ad918..808a9de3 100644
--- a/src/calng/agipd_gpu.py
+++ b/src/calng/agipd_gpu.py
@@ -34,16 +34,16 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
         memory_cells,
         constant_memory_cells,
         output_transpose=(1, 2, 0),  # default: memorycells-fast
-        input_data_dtype=np.uint16,
-        output_data_dtype=np.float32,
-        badpixel_mask_value=np.float32(np.nan),
+        input_data_dtype=cupy.uint16,
+        output_data_dtype=cupy.float32,
+        bad_pixel_mask_value=cupy.float32(cupy.nan),
         gain_mode=AgipdGainMode.ADAPTIVE_GAIN,
     ):
         self.gain_mode = gain_mode
         if self.gain_mode is AgipdGainMode.ADAPTIVE_GAIN:
-            self.default_gain = np.uint8(gain_mode)
+            self.default_gain = cupy.uint8(gain_mode)
         else:
-            self.default_gain = np.uint8(gain_mode - 1)
+            self.default_gain = cupy.uint8(gain_mode - 1)
         self.input_shape = (memory_cells, 2, pixels_x, pixels_y)
         self.processed_shape = (memory_cells, pixels_x, pixels_y)
         super().__init__(
@@ -55,22 +55,22 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
             input_data_dtype,
             output_data_dtype,
         )
-        self.gain_map_gpu = cupy.empty(self.processed_shape, dtype=np.uint8)
+        self.gain_map_gpu = cupy.empty(self.processed_shape, dtype=cupy.uint8)
 
         self.map_shape = (self.constant_memory_cells, self.pixels_x, self.pixels_y)
         self.gm_map_shape = self.map_shape + (3,)  # for gain-mapped constants
         self.threshold_map_shape = self.map_shape + (2,)
         # constants
         self.gain_thresholds_gpu = cupy.empty(
-            self.threshold_map_shape, dtype=np.float32
+            self.threshold_map_shape, dtype=cupy.float32
         )
-        self.offset_map_gpu = cupy.zeros(self.gm_map_shape, dtype=np.float32)
-        self.rel_gain_pc_map_gpu = cupy.ones(self.gm_map_shape, dtype=np.float32)
+        self.offset_map_gpu = cupy.zeros(self.gm_map_shape, dtype=cupy.float32)
+        self.rel_gain_pc_map_gpu = cupy.ones(self.gm_map_shape, dtype=cupy.float32)
         # not gm_map_shape because it only applies to medium gain pixels
-        self.md_additional_offset_gpu = cupy.zeros(self.map_shape, dtype=np.float32)
-        self.rel_gain_xray_map_gpu = cupy.ones(self.map_shape, dtype=np.float32)
-        self.badpixel_map_gpu = cupy.zeros(self.gm_map_shape, dtype=np.uint32)
-        self.badpixel_mask_value = badpixel_mask_value  # TODO: make this configurable
+        self.md_additional_offset_gpu = cupy.zeros(self.map_shape, dtype=cupy.float32)
+        self.rel_gain_xray_map_gpu = cupy.ones(self.map_shape, dtype=cupy.float32)
+        self.bad_pixel_map_gpu = cupy.zeros(self.gm_map_shape, dtype=cupy.uint32)
+        self.bad_pixel_mask_value = bad_pixel_mask_value  # TODO: make this configurable
 
         self.update_block_size((1, 1, 64))
 
@@ -93,7 +93,7 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
             np.transpose(offset_map, (2, 1, 0, 3)).astype(np.float32)
         )
 
-    def load_rel_gain_pc_map(self, slopes_pc_map):
+    def load_rel_gain_pc_map(self, slopes_pc_map, override_md_additional_offset=None):
         # pc has funny shape (11, 352, 128, 512) from file
         # this is (fi, memory cell, y, x)
         slopes_pc_map = slopes_pc_map.astype(np.float32)
@@ -120,18 +120,28 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
             naughty_array[too_high_bool] = medians[too_high_cell]
 
         frac_hg_mg = hg_slope / mg_slope
-        md_additional_offset = (hg_intercept - mg_intercept * frac_hg_mg).astype(
-            np.float32
-        )
         rel_gain_map = np.ones(
             (3, self.constant_memory_cells, self.pixels_y, self.pixels_x),
             dtype=np.float32,
         )
         rel_gain_map[1] = rel_gain_map[0] * frac_hg_mg
         rel_gain_map[2] = rel_gain_map[1] * 4.48
-        # TODO: enable overriding md_additional_offset_gpu based on user input
         self.rel_gain_pc_map_gpu.set(np.transpose(rel_gain_map, (1, 3, 2, 0)))
-        self.md_additional_offset_gpu.set(np.transpose(md_additional_offset, (0, 2, 1)))
+        if override_md_additional_offset is None:
+            md_additional_offset = (hg_intercept - mg_intercept * frac_hg_mg).astype(
+                np.float32
+            )
+            self.md_additional_offset_gpu.set(
+                np.transpose(md_additional_offset, (0, 2, 1))
+            )
+        else:
+            self.override_md_additional_offset(override_md_additional_offset)
+        print(
+            f"Rel gain pc min max: {self.rel_gain_pc_map_gpu.min()}, {self.rel_gain_pc_map_gpu.max()}"
+        )
+
+    def override_md_additional_offset(self, override_value):
+        self.md_additional_offset_gpu.fill(override_value)
 
     def load_rel_gain_ff_map(self, slopes_ff_map):
         # constant shape: y, x, memory cell
@@ -148,54 +158,66 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
         # TODO: maybe clamp and replace NaNs like slopes_pc
         self.rel_gain_xray_map_gpu.set(np.transpose(slopes_ff_map).astype(np.float32))
 
-    def load_badpixels_map(self, badpixels_map):
-        print(f"Loading bad pixels with shape: {badpixels_map.shape}")
+    def load_bad_pixels_map(self, bad_pixels_map, override_flags_to_use=None):
+        print(f"Loading bad pixels with shape: {bad_pixels_map.shape}")
         # will simply OR with already loaded, does not take into account which ones
         # TODO: allow configuring subset of bad pixels to care about
         # TODO: allow configuring value for masked pixels
         # TODO: inquire what "mask for double size pixels" means
-        if len(badpixels_map.shape) == 3:
-            if badpixels_map.shape == (
+        if len(bad_pixels_map.shape) == 3:
+            if bad_pixels_map.shape == (
                 self.pixels_y,
                 self.pixels_x,
                 self.constant_memory_cells,
             ):
                 # BadPixelsFF is not per gain stage - broadcasting along gain dimension
-                self.badpixel_map_gpu |= cupy.asarray(
+                self.bad_pixel_map_gpu |= cupy.asarray(
                     np.broadcast_to(
-                        np.transpose(badpixels_map)[..., np.newaxis],
+                        np.transpose(bad_pixels_map)[..., np.newaxis],
                         self.gm_map_shape,
                     ),
                     dtype=np.uint32,
                 )
-            elif badpixels_map.shape == (
+            elif bad_pixels_map.shape == (
                 self.constant_memory_cells,
                 self.pixels_y,
                 self.pixels_x,
             ):
                 # oh, can also be old bad pixels pc?
-                self.badpixel_map_gpu |= cupy.asarray(
+                self.bad_pixel_map_gpu |= cupy.asarray(
                     np.broadcast_to(
-                        np.transpose(badpixels_map, (0, 2, 1))[..., np.newaxis],
+                        np.transpose(bad_pixels_map, (0, 2, 1))[..., np.newaxis],
                         self.gm_map_shape,
                     ),
                     dtype=np.uint32,
                 )
             else:
                 raise ValueError(
-                    f"What in the world is this shape? {badpixels_map.shape}"
+                    f"What in the world is this shape? {bad_pixels_map.shape}"
                 )
         else:
-            self.badpixel_map_gpu |= cupy.asarray(
-                np.transpose(badpixels_map, (2, 1, 0, 3)), dtype=np.uint32
+            self.bad_pixel_map_gpu |= cupy.asarray(
+                np.transpose(bad_pixels_map, (2, 1, 0, 3)), dtype=np.uint32
             )
 
+        if override_flags_to_use is not None:
+            self.override_bad_pixel_flags_to_use(override_flags_to_use)
+        print(
+            f"Percentage of bad pixels now: {cupy.count_nonzero(self.bad_pixel_map_gpu) / self.bad_pixel_map_gpu.size * 100:.02f}"
+        )
+
+    def override_bad_pixel_flags_to_use(self, override_value):
+        self.bad_pixel_map_gpu &= cupy.uint32(override_value)
+        print(
+            f"Percentage of bad pixels now: {cupy.count_nonzero(self.bad_pixel_map_gpu) / self.bad_pixel_map_gpu.size * 100:.02f}"
+        )
+
     def flush_buffers(self):
         self.offset_map_gpu.fill(0)
         self.rel_gain_pc_map_gpu.fill(1)
         self.md_additional_offset_gpu.fill(0)
         self.rel_gain_xray_map_gpu.fill(1)
-        self.badpixel_map_gpu.fill(0)
+        self.bad_pixel_map_gpu.fill(0)
 
     # TODO: baseline shift
 
@@ -206,21 +228,22 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner):
             flags & CorrectionFlags.THRESHOLD
         ):
             raise ValueError("Cannot do gain thresholding in fixed gain mode")
+
         self.correction_kernel(
             self.full_grid,
             self.full_block,
             (
                 self.input_data_gpu,
                 self.cell_table_gpu,
-                np.uint8(flags),
+                cupy.uint8(flags),
                 self.default_gain,
                 self.gain_thresholds_gpu,
                 self.offset_map_gpu,
                 self.rel_gain_pc_map_gpu,
                 self.md_additional_offset_gpu,
                 self.rel_gain_xray_map_gpu,
-                self.badpixel_map_gpu,
-                self.badpixel_mask_value,
+                self.bad_pixel_map_gpu,
+                self.bad_pixel_mask_value,
                 self.gain_map_gpu,
                 self.processed_data_gpu,
             ),
diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py
index 15ed974d..117ebb86 100644
--- a/src/calng/base_correction.py
+++ b/src/calng/base_correction.py
@@ -570,7 +570,6 @@ class BaseCorrection(calibrationBase.CalibrationReceiverBaseDevice):
     def _update_shapes(self):
         """(Re)initialize buffers according to expected data shapes"""
         self.log.INFO("Updating shapes")
-
         # reflect the axis reordering in the expected output shape
         self.set("dataFormat.inputDataShape", list(self.input_data_shape))
         self.set("dataFormat.outputDataShape", list(self.output_data_shape))
-- 
GitLab