From 319a0a116d95bf9fe1430458e007b78ff5e7de78 Mon Sep 17 00:00:00 2001
From: David Hammer <dhammer@mailbox.org>
Date: Mon, 11 Apr 2022 13:46:06 +0200
Subject: [PATCH] Enable overriding constant using specific file

---
 src/calng/base_calcat.py     |  78 +++++++++++++++++-------
 src/calng/base_correction.py |  29 +++++++--
 src/calng/scenes.py          | 112 ++++++++++++++++++++++++++++++++++-
 3 files changed, 190 insertions(+), 29 deletions(-)

diff --git a/src/calng/base_calcat.py b/src/calng/base_calcat.py
index 5d126cec..0061c92c 100644
--- a/src/calng/base_calcat.py
+++ b/src/calng/base_calcat.py
@@ -160,22 +160,30 @@ def add_status_schema_from_enum(schema, prefix, enum_class):
 
             STRING_ELEMENT(schema)
             .key(f"{constant_node}.dataFilePath")
-            .displayedName("[Debug] Data file path")
+            .displayedName("Data file path")
             .description(
-                "Where is the actual file for the currently loaded constant located?"
+                "Where is the actual file for the constant located?"
             )
-            .readOnly()
-            .initialValue("")
+            .assignmentOptional()
+            .defaultValue("")
+            .reconfigurable()
             .commit(),
 
             STRING_ELEMENT(schema)
             .key(f"{constant_node}.dataSetName")
-            .displayedName("[Debug] Data set name")
+            .displayedName("Data set name")
             .description(
                 "Within the actual data file, where is the constant data located?"
             )
-            .readOnly()
-            .initialValue("")
+            .assignmentOptional()
+            .defaultValue("")
+            .reconfigurable()
+            .commit(),
+
+            SLOT_ELEMENT(schema)
+            .key(f"{constant_node}.overrideConstantFromFile")
+            .displayedName("Override from file")
+            .description("Constant from manually specified .h5 file and data set name")
             .commit(),
         )
 
@@ -559,7 +567,20 @@ class BaseCalcatFriend:
 
         return constant_data
 
-    def get_specific_constant_version(self, constant):
+    def get_constant_version_and_call_me_back(self, constant, callback):
+        """Runs get_constant_version in thread, will call callback on completion"""
+        # TODO: do we want to use asyncio / "modern" async?
+        # TODO: consider moving out of this class, closer to correction device
+        def aux():
+            with self.api_lock:
+                data = self.get_constant_version(constant)
+            callback(constant, data)
+
+        thread = threading.Thread(target=aux)
+        thread.start()
+        return thread
+
+    def get_overridden_constant_version(self, constant):
         # TODO: warn if PDU or constant type does not match
         # TODO: warn if result is list (happens for empty version ID)
         constant_version_id = self.device.get(
@@ -577,34 +598,45 @@ class BaseCalcatFriend:
             self.cached_constants[constant] = constant_data
         self._set_status(constant, "beginValidityAt", resp["data"]["begin_at"])
         self._set_status(constant, "calibrationId", "manual override")
-        self._set_status(constant, "conditionId", "manual override")
-        self._set_status(constant, "constantId", "manual override")
+        self._set_status(constant, "usedConditionId", "manual override")
+        self._set_status(constant, "usedConstantId", "manual override")
         self._set_status(constant, "constantVersionId", constant_version_id)
         self._set_status(constant, "found", True)
         return constant_data
 
-    def get_constant_version_and_call_me_back(self, constant, callback):
-        """Runs get_constant_version in thread, will call callback on completion"""
-        # TODO: do we want to use asyncio / "modern" async?
-        # TODO: consider moving out of this class, closer to correction device
+    def get_overridden_constant_version_and_call_me_back(self, constant, callback):
+        """Blindly load whatever CalCat points to for CCV - user must be confident that
+        this CCV corresponds to correct kind of constant."""
+
+        # TODO: warn user about all the things that go wrong
         def aux():
             with self.api_lock:
-                data = self.get_constant_version(constant)
+                data = self.get_overriden_constant_version(constant)
             callback(constant, data)
 
         thread = threading.Thread(target=aux)
         thread.start()
         return thread
 
-    def get_specific_constant_version_and_call_me_back(self, constant, callback):
-        """Blindly load whatever CalCat points to for CCV - user must be confident that
-        this CCV corresponds to correct kind of constant."""
-
-        # TODO: warn user about all the things that go wrong
+    def get_overridden_constant_from_file_and_call_me_back(self, constant, callback):
         def aux():
-            with self.api_lock:
-                data = self.get_specific_constant_version(constant)
-            callback(constant, data)
+            file_path = self.device.get(
+                f"{self.status_prefix}.{constant.name}.dataFilePath"
+            )
+            data_set_name = self.device.get(
+                f"{self.status_prefix}.{constant.name}.dataSetName"
+            )
+            with h5py.File(file_path, "r") as fd:
+                constant_data = np.array(fd[data_set_name]["data"])
+            with self.cached_constants_lock:
+                self.cached_constants[constant] = constant_data
+            self._set_status(constant, "beginValidityAt", "manual override")
+            self._set_status(constant, "calibrationId", "manual override")
+            self._set_status(constant, "usedConditionId", "manual override")
+            self._set_status(constant, "usedConstantId", "manual override")
+            self._set_status(constant, "constantVersionId", "manual override")
+            self._set_status(constant, "found", True)
+            callback(constant, constant_data)
 
         thread = threading.Thread(target=aux)
         thread.start()
diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py
index e498d34a..ddba4484 100644
--- a/src/calng/base_correction.py
+++ b/src/calng/base_correction.py
@@ -330,7 +330,7 @@ class BaseCorrection(PythonDevice):
             .key("availableScenes")
             .setSpecialDisplayType(DT_SCENES)
             .readOnly()
-            .initialValue(["overview"])
+            .initialValue(["overview", "constant_overrides"])
             .commit(),
         )
 
@@ -614,9 +614,17 @@ class BaseCorrection(PythonDevice):
         # register slots
         # TODO: the CalCatFriend could add these for us
         # note: overly complicated for closure to work
-        def make_wrapper_capturing_constant(constant):
+        def make_wrapper_1(constant):
             def aux():
-                self.calcat_friend.get_specific_constant_version_and_call_me_back(
+                self.calcat_friend.get_overridden_constant_version_and_call_me_back(
+                    constant, self._load_constant_to_runner
+                )
+
+            return aux
+
+        def make_wrapper_2(constant):
+            def aux():
+                self.calcat_friend.get_overridden_constant_from_file_and_call_me_back(
                     constant, self._load_constant_to_runner
                 )
 
@@ -626,7 +634,13 @@ class BaseCorrection(PythonDevice):
             slot_name = f"foundConstants.{constant.name}.overrideConstantVersion"
             meth_name = slot_name.replace(".", "_")
             self.KARABO_SLOT(
-                make_wrapper_capturing_constant(constant),
+                make_wrapper_1(constant),
+                slotName=meth_name,
+            )
+            slot_name = f"foundConstants.{constant.name}.overrideConstantFromFile"
+            meth_name = slot_name.replace(".", "_")
+            self.KARABO_SLOT(
+                make_wrapper_2(constant),
                 slotName=meth_name,
             )
 
@@ -725,7 +739,7 @@ class BaseCorrection(PythonDevice):
         self._update_correction_flags()
 
     def _lock_and_update_in_background(self, method):
-        # TODO: securely handle errors (postReconfigure may succeed and device state not)
+        # TODO: securely handle errors (postReconfigure may succeed, device state not)
         def runner():
             with self._buffer_lock, utils.StateContext(self, State.CHANGING):
                 method()
@@ -769,6 +783,11 @@ class BaseCorrection(PythonDevice):
                 device_id=self.getInstanceId(),
                 schema=self.getFullSchema(),
             )
+        elif name == "constant_overrides":
+            payload["data"] = scenes.correction_device_constant_overrides(
+                device_id=self.getInstanceId(),
+                schema=self.getFullSchema(),
+            )
         elif name.startswith("browse_schema"):
             if ":" in name:
                 prefix = name[len("browse_schema:"):]
diff --git a/src/calng/scenes.py b/src/calng/scenes.py
index 58168b48..ed9dd89e 100644
--- a/src/calng/scenes.py
+++ b/src/calng/scenes.py
@@ -331,6 +331,16 @@ class FoundConstantsColumn(VerticalLayout):
                 padding=0,
             )
             self.children.append(constant_row)
+        self.children.append(
+            DeviceSceneLinkModel(
+                text="Overrides",
+                keys=[f"{device_id}.availableScenes"],
+                target="constant_overrides",
+                target_window=SceneTargetWindow.Dialog,
+                width=8 * BASE_INC,
+                height=BASE_INC,
+            )
+        )
 
 
 class ConstantLoadedAmpeln(HorizontalLayout):
@@ -340,8 +350,8 @@ class ConstantLoadedAmpeln(HorizontalLayout):
             [
                 ColorBoolModel(
                     keys=[f"{device_id}.{prefix}.{key}.found"],
-                    height=BASE_INC,
                     width=BASE_INC,
+                    height=BASE_INC,
                 )
                 for key in schema_hash.get(prefix).getKeys()
             ]
@@ -866,6 +876,106 @@ def correction_device_overview(device_id, schema):
     )
 
 
+@scene_generator
+def correction_device_constant_overrides(device_id, schema, prefix="foundConstants"):
+    schema_hash = schema_to_hash(schema)
+
+    return HorizontalLayout(
+        children=[
+            VerticalLayout(
+                VerticalLayout(
+                    LabelModel(text="constant", width=6 * BASE_INC, height=BASE_INC),
+                    LabelModel(text="timestamp", width=6 * BASE_INC, height=BASE_INC),
+                    padding=0,
+                ),
+                VerticalLayout(
+                    LabelModel(
+                        text="constant version ID",
+                        width=6 * BASE_INC,
+                        height=BASE_INC,
+                    ),
+                    Space(width=6 * BASE_INC, height=BASE_INC),
+                    padding=0,
+                ),
+                VerticalLayout(
+                    LabelModel(
+                        text="data file name", width=6 * BASE_INC, height=BASE_INC
+                    ),
+                    LabelModel(
+                        text="dataset name", width=6 * BASE_INC, height=BASE_INC
+                    ),
+                    Space(width=6 * BASE_INC, height=BASE_INC),
+                    padding=0,
+                ),
+            )
+        ]
+        + [
+            VerticalLayout(
+                VerticalLayout(
+                    HorizontalLayout(
+                        ColorBoolModel(
+                            keys=[f"{device_id}.{prefix}.{constant}.found"],
+                            width=BASE_INC,
+                            height=BASE_INC,
+                        ),
+                        LabelModel(
+                            text=constant,
+                            width=7 * BASE_INC,
+                            height=BASE_INC,
+                        ),
+                    ),
+                    DisplayLabelModel(
+                        keys=[f"{device_id}.{prefix}.{constant}.beginValidityAt"],
+                        width=8 * BASE_INC,
+                        height=BASE_INC,
+                        font_size=9,
+                    ),
+                    padding=0,
+                ),
+                VerticalLayout(
+                    LineEditModel(
+                        klass="EditableLineEdit",
+                        keys=[f"{device_id}.{prefix}.{constant}.constantVersionId"],
+                        width=8 * BASE_INC,
+                        height=BASE_INC,
+                    ),
+                    DisplayCommandModel(
+                        keys=[
+                            f"{device_id}.{prefix}.{constant}.overrideConstantVersion"
+                        ],
+                        width=8 * BASE_INC,
+                        height=BASE_INC,
+                    ),
+                    padding=0,
+                ),
+                VerticalLayout(
+                    LineEditModel(
+                        klass="EditableLineEdit",
+                        keys=[f"{device_id}.{prefix}.{constant}.dataFilePath"],
+                        width=8 * BASE_INC,
+                        height=BASE_INC,
+                    ),
+                    LineEditModel(
+                        klass="EditableLineEdit",
+                        keys=[f"{device_id}.{prefix}.{constant}.dataSetName"],
+                        width=8 * BASE_INC,
+                        height=BASE_INC,
+                    ),
+                    DisplayCommandModel(
+                        keys=[
+                            f"{device_id}.{prefix}.{constant}.overrideConstantFromFile"
+                        ],
+                        width=8 * BASE_INC,
+                        height=BASE_INC,
+                    ),
+                    padding=0,
+                ),
+            )
+            for constant in schema_hash.get(prefix).getKeys()
+        ]
+    )
+
+
 @scene_generator
 def manager_device_overview(
     manager_device_id,
-- 
GitLab