From bfcd3a8c6a06541f745fadd7e3ecaea0ead5924e Mon Sep 17 00:00:00 2001
From: David Hammer <dhammer@mailbox.org>
Date: Fri, 17 Dec 2021 16:02:16 +0100
Subject: [PATCH] Specify frameFilter without eval

---
 src/calng/base_correction.py | 88 ++++++++++++++++++++++++++++++------
 1 file changed, 75 insertions(+), 13 deletions(-)

diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py
index 033ba715..b3243e30 100644
--- a/src/calng/base_correction.py
+++ b/src/calng/base_correction.py
@@ -180,7 +180,8 @@ class BaseCorrection(PythonDevice):
         "dataFormat.outputAxisOrder",
         "dataFormat.outputImageDtype",
         "dataFormat.overrideInputAxisOrder",
-        "frameFilter",
+        "frameFilter.type",
+        "frameFilter.spec",
         "preview.enable",
         "preview.index",
         "preview.selectionMode",
@@ -266,7 +267,7 @@ class BaseCorrection(PythonDevice):
             .defaultValue([])
             .commit(),
 
-            STRING_ELEMENT(expected)
+            NODE_ELEMENT(expected)
             .key("frameFilter")
             .displayedName("Frame filter")
             .description(
@@ -274,15 +275,46 @@ class BaseCorrection(PythonDevice):
                 "filter will be discarded before any processing happens and will not "
                 "get to dataOutput or preview. Note that this filter goes by frame "
                 "index rather than cell ID or pulse ID; set accordingly. Handle with "
-                "care - an invalid filter can prevent all processing. The filter is "
-                "specified as a string which is evaluated into numpy uint16 array. A "
-                "valid filter could for eaxmple be 'np.arange(0, 352, 2)'."
+                "care - an invalid filter can prevent all processing. How the filter "
+                "is specified depends on frameFilter.type. See frameFilter.current to "
+                "inspect the currently set frame filter array (if any)."
+            )
+            .commit(),
+
+            STRING_ELEMENT(expected)
+            .key("frameFilter.type")
+            .displayedName("Filter definition type")
+            .description(
+                "Controls how frameFilter.spec is used. The default value of 'none' "
+                "means that no filter is set (regardless of frameFilter.spec). "
+                "'arange' allows between one and three integers separated by ',' which "
+                "are parsed and passed directly to numpy.arange. 'commaseparated' "
+                "reads a list of integers separated by commas."
             )
+            .options("none,arange,commaseparated")
+            .assignmentOptional()
+            .defaultValue("none")
+            .reconfigurable()
+            .commit(),
+
+            STRING_ELEMENT(expected)
+            .key("frameFilter.spec")
             .assignmentOptional()
             .defaultValue("")
             .reconfigurable()
             .commit(),
 
+            VECTOR_UINT32_ELEMENT(expected)
+            .key("frameFilter.current")
+            .displayedName("Current filter")
+            .description(
+                "This read-only value is used to display the contents of the current "
+                "frame filter. An empty array means no filtering is done."
+            )
+            .readOnly()
+            .initialValue([])
+            .commit(),
+
             UINT32_ELEMENT(expected)
             .key("outputShmemBufferSize")
             .displayedName("Output buffer size limit")
@@ -787,20 +819,50 @@ class BaseCorrection(PythonDevice):
         self.log.DEBUG(f"Corrections for preview: {str(preview)}")
 
     def _update_frame_filter(self):
-        """Parse frameFilter string (if set) and update cached filter array. Will set
+        """Parse frameFilter string (if set) and update cached filter array. May update
         dataFormat.filteredFrames, so one will typically want to call _update_buffers
         afterwards."""
-        filter_string = self.get("frameFilter")
-        if filter_string.strip() == "":
+        # TODO: add some validation to preReconfigure
+        self.log.DEBUG("Updating frame filter")
+        filter_type = self.get("frameFilter.type")
+        filter_string = self.get("frameFilter.spec")
+
+        if filter_type == "none" or filter_string.strip() == "":
             self._frame_filter = None
+        elif filter_type == "arange":
+            try:
+                numbers = tuple(int(part) for part in filter_string.split(","))
+            except (ValueError, TypeError):
+                self.log_status_warn(
+                    f"Invalid frame filter specification: {filter_string}"
+                )
+            else:
+                self._frame_filter = np.arange(*numbers, dtype=np.uint16)
+        elif filter_type == "commaseparated":
+            try:
+                self._frame_filter = np.fromstring(
+                    filter_string, sep=",", dtype=np.uint16
+                )
+            except ValueError:
+                # note: only in the future will numpy actually give ValueError
+                self.log_status_warn(
+                    f"Invalid frame filter specification: {filter_string}"
+                )
+        else:
+            self.log_status_warn(f"Invalid frame filter type '{filter_type}'")
+
+        if self._frame_filter is None:
             self.set("dataFormat.filteredFrames", self.get("dataFormat.memoryCells"))
+            self.set("frameFilter.current", [])
         else:
-            self._frame_filter = np.array(eval(filter_string), dtype=np.uint16)
             self.set("dataFormat.filteredFrames", self._frame_filter.size)
-            if self._frame_filter.min() < 0 or self._frame_filter.max() >= self.get(
-                "dataFormat.memoryCells"
-            ):
-                self.log_status_warn("Invalid frame filter set, expect exceptions!")
+            self.set("frameFilter.current", list(map(int, self._frame_filter)))
+
+        if self._frame_filter is not None and (
+            self._frame_filter.min() < 0
+            or self._frame_filter.max() >= self.get("dataFormat.memoryCells")
+        ):
+            self.log_status_warn("Invalid frame filter set, expect exceptions!")
 
     def _update_buffers(self):
         """(Re)initialize buffers / kernel runner according to expected data shapes"""
-- 
GitLab