diff --git a/src/calng/AgipdCorrection.py b/src/calng/AgipdCorrection.py index c763ebbb5511304cd29f60f772db9233e6f5e1b6..3b894b80b2ebcc5c09c1b359e7b3ff7deb4db7f4 100644 --- a/src/calng/AgipdCorrection.py +++ b/src/calng/AgipdCorrection.py @@ -14,7 +14,7 @@ from karabo.bound import ( ) from karabo.common.states import State -from . import shmem_utils +from . import shmem_utils, utils from ._version import version as deviceVersion from .agipd_gpu import AgipdGainMode, AgipdGpuRunner, BadPixelValues, CorrectionFlags from .base_correction import BaseCorrection, add_correction_step_schema @@ -35,7 +35,10 @@ class AgipdCorrection(BaseCorrection): _gpu_runner_class = AgipdGpuRunner _calcat_friend_class = AgipdCalcatFriend _constant_enum_class = AgipdConstants - _managed_keys = BaseCorrection._managed_keys[:] + ["overrideInputAxisOrder"] + _managed_keys = BaseCorrection._managed_keys | { + "overrideInputAxisOrder", + "sendGainMap", + } # this is just extending (not mandatory) _schema_cache_fields = BaseCorrection._schema_cache_fields | { @@ -69,7 +72,6 @@ class AgipdCorrection(BaseCorrection): .defaultValue(False) .commit(), ) - AgipdCorrection._managed_keys.append("sendGainMap") # TODO: make sendGainMap reconfigurable ( @@ -166,12 +168,12 @@ class AgipdCorrection(BaseCorrection): ) .commit(), ) - AgipdCorrection._managed_keys.append( + AgipdCorrection._managed_keys.add( "corrections.relGainPc.overrideMdAdditionalOffset" ) - AgipdCorrection._managed_keys.append("corrections.relGainPc.mdAdditionalOffset") - AgipdCorrection._managed_keys.append("corrections.relGainXray.gGainValue") - AgipdCorrection._managed_keys.append("corrections.badPixels.maskingValue") + AgipdCorrection._managed_keys.add("corrections.relGainPc.mdAdditionalOffset") + AgipdCorrection._managed_keys.add("corrections.relGainXray.gGainValue") + AgipdCorrection._managed_keys.add("corrections.badPixels.maskingValue") # TODO: DRY / encapsulate for field in BadPixelValues: ( @@ -182,7 +184,7 @@ class AgipdCorrection(BaseCorrection): .reconfigurable() .commit() ) - AgipdCorrection._managed_keys.append( + AgipdCorrection._managed_keys.add( f"corrections.badPixels.subsetToUse.{field.name}" ) @@ -191,7 +193,7 @@ class AgipdCorrection(BaseCorrection): VECTOR_STRING_ELEMENT(expected) .key("managedKeys") .assignmentOptional() - .defaultValue(AgipdCorrection._managed_keys) + .defaultValue(list(AgipdCorrection._managed_keys)) .commit() ) @@ -312,20 +314,17 @@ class AgipdCorrection(BaseCorrection): if do_generate_preview: if self._correction_flag_enabled != self._correction_flag_preview: self.gpu_runner.correct(self._correction_flag_preview) - # WARNING: actually looking for cell ID at the request of SPB - preview_slice_index = self._schema_cache["preview.pulse"] - if preview_slice_index >= 0: - # look at cell_table to find which index this pulse ID is in - cell_id_found = np.where(cell_table == preview_slice_index)[0] - if len(cell_id_found) == 0: - cell_found_instead = cell_table[0] - self.log_status_info( - f"Cell {preview_slice_index} not found, arbitrary cell " - f"{cell_found_instead} will be shown instead." - ) - preview_slice_index = 0 - else: - preview_slice_index = cell_id_found[0] + ( + preview_slice_index, + preview_cell, + preview_pulse, + ) = utils.pick_frame_index( + self._schema_cache["preview.selectionMode"], + self._schema_cache["preview.index"], + cell_table, + pulse_table, + warn_func=self.log_status_warn, + ) preview_raw, preview_corrected = self.gpu_runner.compute_preview( preview_slice_index ) diff --git a/src/calng/DsscCorrection.py b/src/calng/DsscCorrection.py index 7bd0b665becdfb3eda6e12391810b3162818e6a2..e5fdc6ee362c220ffea95dbe5317cb0406aff245 100644 --- a/src/calng/DsscCorrection.py +++ b/src/calng/DsscCorrection.py @@ -4,6 +4,7 @@ import numpy as np from karabo.bound import KARABO_CLASSINFO, VECTOR_STRING_ELEMENT from karabo.common.states import State +from . import utils from ._version import version as deviceVersion from .base_correction import BaseCorrection, add_correction_step_schema from .calcat_utils import DsscCalcatFriend, DsscConstants @@ -18,7 +19,7 @@ class DsscCorrection(BaseCorrection): _gpu_runner_class = DsscGpuRunner _calcat_friend_class = DsscCalcatFriend _constant_enum_class = DsscConstants - _managed_keys = BaseCorrection._managed_keys[:] + _managed_keys = BaseCorrection._managed_keys.copy() @staticmethod def expectedParameters(expected): @@ -34,7 +35,7 @@ class DsscCorrection(BaseCorrection): VECTOR_STRING_ELEMENT(expected) .key("managedKeys") .assignmentOptional() - .defaultValue(DsscCorrection._managed_keys) + .defaultValue(list(DsscCorrection._managed_keys)) .commit() ) @@ -118,22 +119,17 @@ class DsscCorrection(BaseCorrection): if do_generate_preview: if self._correction_flag_enabled != self._correction_flag_preview: self.gpu_runner.correct(self._correction_flag_preview) - preview_slice_index = self._schema_cache["preview.pulse"] - if preview_slice_index >= 0: - # look at pulse_table to find which index this pulse ID is in - pulse_id_found = np.where(pulse_table == preview_slice_index)[0] - if len(pulse_id_found) == 0: - pulse_found_instead = pulse_table[0] - msg = ( - f"Pulse {preview_slice_index} not found in " - f"image.pulseId, arbitrary pulse " - f"{pulse_found_instead} will be shown." - ) - self.log.WARN(msg) - self.set("status", msg) - preview_slice_index = 0 - else: - preview_slice_index = pulse_id_found[0] + ( + preview_slice_index, + preview_cell, + preview_pulse, + ) = utils.pick_frame_index( + self._schema_cache["preview.selectionMode"], + self._schema_cache["preview.index"], + cell_table, + pulse_table, + warn_func=self.log_status_warn, + ) preview_raw, preview_corrected = self.gpu_runner.compute_preview( preview_slice_index, ) diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py index 271736c04fa92af3cc0eeaa1d23804b39576479b..5aa158c24e7c21d29f4af99b19010056eec664d1 100644 --- a/src/calng/base_correction.py +++ b/src/calng/base_correction.py @@ -46,15 +46,16 @@ class BaseCorrection(PythonDevice): _correction_flag_class = None # subclass must set to some enum class _gpu_runner_class = None # subclass must set this _gpu_runner_init_args = {} # subclass can set this (TODO: remove, design better) - _managed_keys = [ + _managed_keys = { "outputShmemBufferSize", "dataFormat.outputAxisOrder", "dataFormat.outputImageDtype", "preview.enable", - "preview.pulse", + "preview.index", + "preview.selectionMode", "preview.trainIdModulo", "loadMostRecentConstants", - ] # subclass must extend this and put it in schema + } # subclass must extend this and put it in schema _schema_cache_fields = { "doAnything", "constantParameters.memoryCells", @@ -63,7 +64,8 @@ class BaseCorrection(PythonDevice): "dataFormat.pixelsY", "dataFormat.outputAxisOrder", "preview.enable", - "preview.pulse", + "preview.index", + "preview.selectionMode", "preview.trainIdModulo", "processingStateTimeout", "state", @@ -318,8 +320,14 @@ class BaseCorrection(PythonDevice): preview_schema = Schema() ( - NODE_ELEMENT(preview_schema).key("data").commit(), - NDARRAY_ELEMENT(preview_schema).key("data.adc").dtype("FLOAT").commit(), + NODE_ELEMENT(preview_schema).key("image").commit(), + NDARRAY_ELEMENT(preview_schema).key("image.data").dtype("FLOAT").commit(), + UINT64_ELEMENT(preview_schema) + .key("image.trainId") + .displayedName("Train ID") + .assignmentOptional() + .defaultValue(0) + .commit(), ) ( NODE_ELEMENT(expected).key("preview").displayedName("Preview").commit(), @@ -340,23 +348,32 @@ class BaseCorrection(PythonDevice): .commit(), # TODO: Split into AGIPD-specific or see if others like cell ID over pulse ID INT32_ELEMENT(expected) - .key("preview.pulse") - .displayedName("Cell (or stat) for preview") + .key("preview.index") + .displayedName("Index (or stat) for preview") .description( - "If this value is ≥ 0, the corresponding index from data will be " - "sliced for the preview. If this value is ≤ 0, preview will be one of " - "the following stats:" - "-1: max, " - "-2: mean, " - "-3: sum, " - "-4: stdev. " - "Max means selecting the pulse with the maximum integrated value. The " - "others are computed across all filtered pulses in the train." + "If this value is ≥ 0, the corresponding index (frame, cell, or pulse) " + "will be sliced for the preview output. If this value is ≤ 0, preview " + "will be one of the following stats: -1: max, -2: mean, -3: sum, -4: " + "stdev. These stats are computed across memory cells." ) .assignmentOptional() .defaultValue(0) .reconfigurable() .commit(), + STRING_ELEMENT(expected) + .key("preview.selectionMode") + .displayedName("Index selection mode") + .description( + "The value of preview.index can be used in multiple ways, controlled " + "by this value. If this is set to 'frame', preview.index is sliced " + "directly from data. If 'cell' (or 'pulse') is selected, I will look at " + "cell (or pulse) table for the requested cell (or pulse ID)." + ) + .options("frame,cell,pulse") + .assignmentOptional() + .defaultValue("frame") + .reconfigurable() + .commit(), UINT32_ELEMENT(expected) .key("preview.trainIdModulo") .displayedName("Train modulo for throttling") @@ -604,7 +621,6 @@ class BaseCorrection(PythonDevice): # TODO: allow sending *all* frames for commissioning (request: Jola) preview_hash = Hash() preview_hash.set("image.trainId", train_id) - preview_hash.set("image.cellId", self._schema_cache["preview.pulse"]) # note: have to construct because setting .tid after init is broken timestamp = Timestamp(Epochstamp(), Trainstamp(train_id)) @@ -736,5 +752,5 @@ def add_correction_step_schema(schema, managed_keys, field_flag_mapping): .reconfigurable() .commit(), ) - managed_keys.append(f"{node_name}.enable") - managed_keys.append(f"{node_name}.preview") + managed_keys.add(f"{node_name}.enable") + managed_keys.add(f"{node_name}.preview") diff --git a/src/calng/calcat_utils.py b/src/calng/calcat_utils.py index 9d394f047a435040f8669ca89d3eb14f8b5bb0f2..032dcad4fc4ccedb801a0a840b140d117f9c2fb0 100644 --- a/src/calng/calcat_utils.py +++ b/src/calng/calcat_utils.py @@ -253,12 +253,12 @@ class BaseCalcatFriend: .reconfigurable() .commit(), ) - managed_keys.append(f"{param_prefix}.deviceMappingSnapshotAt") - managed_keys.append(f"{param_prefix}.constantVersionEventAt") - managed_keys.append(f"{param_prefix}.memoryCells") - managed_keys.append(f"{param_prefix}.pixelsX") - managed_keys.append(f"{param_prefix}.pixelsY") - managed_keys.append(f"{param_prefix}.biasVoltage") + managed_keys.add(f"{param_prefix}.deviceMappingSnapshotAt") + managed_keys.add(f"{param_prefix}.constantVersionEventAt") + managed_keys.add(f"{param_prefix}.memoryCells") + managed_keys.add(f"{param_prefix}.pixelsX") + managed_keys.add(f"{param_prefix}.pixelsY") + managed_keys.add(f"{param_prefix}.biasVoltage") def __init__( self, @@ -599,9 +599,9 @@ class AgipdCalcatFriend(BaseCalcatFriend): .reconfigurable() .commit(), ) - managed_keys.append(f"{param_prefix}.acquisitionRate") - managed_keys.append(f"{param_prefix}.gainSetting") - managed_keys.append(f"{param_prefix}.photonEnergy") + managed_keys.add(f"{param_prefix}.acquisitionRate") + managed_keys.add(f"{param_prefix}.gainSetting") + managed_keys.add(f"{param_prefix}.photonEnergy") _add_status_schema_from_enum(schema, status_prefix, AgipdConstants) diff --git a/src/calng/utils.py b/src/calng/utils.py index 2e9b25467fb737131b9a003d475d06d12bfd20c1..0bd0a25a6c5af8273812dd9f6bbe2ae7a988fd62 100644 --- a/src/calng/utils.py +++ b/src/calng/utils.py @@ -8,6 +8,64 @@ import timeit import numpy as np +def pick_frame_index(selection_mode, index, cell_table, pulse_table, warn_func=None): + """When selecting a single frame to preview, an obvious question is whether the + number the operator provides is a frame index, a cell ID, or a pulse ID. This + function allows any of the three, translating into frame index. + + As this will be used by correction devices, the warn_func parameter allows the + function to issue warnings via this instead of raising exceptions. + + Indices below zero are special values and thus returned directly. + + Returns: (found frame index, corresponding cell ID, corresponding pulse ID)""" + + if index < 0: + return index, index, index + + # TODO: enum + if selection_mode == "frame": + if index >= len(cell_table): + if warn_func is not None: + warn_func( + f"Index {index} out of range for cell table of length " + f"{len(cell_table)}, returning index 0 instead" + ) + frame_index = 0 + else: + frame_index = index + + return frame_index, cell_table[frame_index], pulse_table[frame_index] + elif selection_mode == "cell": + found = np.where(cell_table == index)[0] + if len(found) > 0: + cell = index + frame_index = found[0] + else: + cell = cell_table[0] + if warn_func is not None: + warn_func( + f"Cell {index} not found, arbitrary cell {cell} returned instead" + ) + frame_index = 0 + return frame_index, cell, pulse_table[frame_index] + elif selection_mode == "pulse": + found = np.where(pulse_table == index)[0] + if len(found) > 0: + pulse = index + frame_index = found[0] + else: + pulse = pulse_table[0] + if warn_func is not None: + warn_func( + f"Pulse {index} not found, arbitrary pulse {pulse} returned instead" + ) + frame_index = 0 + return frame_index, cell_table[frame_index], pulse + else: + raise ValueError(f"Invalid selection mode '{selection_mode}'") + + def threadsafe_cache(fun): """This decorator imitates functools.cache, but threadsafer