diff --git a/src/calng/AgipdCorrection.py b/src/calng/AgipdCorrection.py index 136ea595e587e184ef8498aa3027e3ef9c466ff6..b7d4d629909da9adc2c337301edcda627c02881a 100644 --- a/src/calng/AgipdCorrection.py +++ b/src/calng/AgipdCorrection.py @@ -1,13 +1,21 @@ import timeit import numpy as np -from karabo.bound import BOOL_ELEMENT, KARABO_CLASSINFO +from karabo.bound import ( + BOOL_ELEMENT, + KARABO_CLASSINFO, + STRING_ELEMENT, + TABLE_ELEMENT, + UINT32_ELEMENT, + Hash, + Schema, +) from karabo.common.states import State from . import utils from ._version import version as deviceVersion +from .agipd_gpu import AgipdGpuRunner, BadPixelValues, CorrectionFlags from .base_correction import BaseCorrection -from .agipd_gpu import AgipdGpuRunner, CorrectionFlags @KARABO_CLASSINFO("AgipdCorrection", deviceVersion) @@ -80,6 +88,31 @@ class AgipdCorrection(BaseCorrection): .commit(), ) + # TODO: hook this up to actual correction done + bad_pixel_selection_schema = Schema() + ( + 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 + ] + ) + .reconfigurable() + .commit(), + ) + @property def input_data_shape(self): return ( @@ -128,7 +161,7 @@ class AgipdCorrection(BaseCorrection): # TODO: what are these empty things for? if not data.has("image"): - self.log.INFO("Ignoring hash without image node") + # self.log.INFO("Ignoring hash without image node") return time_start = timeit.default_timer() @@ -147,7 +180,6 @@ class AgipdCorrection(BaseCorrection): self.set( "status", f"Updating input shapes based on received {image_data.shape}" ) - # TODO: truncate if > 800 self.set("dataFormat.memoryCells", image_data.shape[0]) with self._buffer_lock: # TODO: pulse filter update after reimplementation @@ -235,7 +267,6 @@ class AgipdCorrection(BaseCorrection): ) def _load_constant_to_gpu(self, constant_name, constant_data): - # TODO: also hook flushConstants or whatever it is called if constant_name == "ThresholdsDark": self.gpu_runner.load_thresholds(constant_data) # TODO: encode correction / constant dependencies in a clever way @@ -263,7 +294,11 @@ class AgipdCorrection(BaseCorrection): 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) def _update_pulse_filter(self, filter_string): """Called whenever the pulse filter changes, typically followed by diff --git a/src/calng/agipd_gpu.py b/src/calng/agipd_gpu.py index 196aa30acd9d46ac1559e1e0af4711a63d408788..4ec912cb48fe1f65a91753e288d53df07b848ef2 100644 --- a/src/calng/agipd_gpu.py +++ b/src/calng/agipd_gpu.py @@ -28,6 +28,7 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner): 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), ): self.input_shape = (memory_cells, 2, pixels_x, pixels_y) self.processed_shape = (memory_cells, pixels_x, pixels_y) @@ -51,9 +52,11 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner): ) 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) + # 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.map_shape, dtype=np.uint32) + 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.update_block_size((1, 1, 64)) @@ -112,7 +115,7 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner): ) rel_gain_map[1] = rel_gain_map[0] * frac_hg_mg rel_gain_map[2] = rel_gain_map[1] * 4.48 - # TODO: enable overriding this based on user input + # 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))) @@ -131,11 +134,34 @@ 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}") + # will simply OR with already loaded, does not take into account which ones + # TODO: simple loading + # 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: + # BadPixelsFF is not per gain stage... + # TODO: broadcast? + ... + else: + self.badpixel_map_gpu |= cupy.asarray( + np.transpose(badpixels_map, (2, 1, 0, 3)), dtype=np.uint32 + ) + + 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) + + # TODO: baseline shift + def correct(self, flags): if flags & CorrectionFlags.BLSHIFT: raise NotImplementedError("Baseline shift not implemented yet") - if flags & CorrectionFlags.BPMASK: - raise NotImplementedError("Bad pixel masking not implemented yet") self.correction_kernel( self.full_grid, self.full_block, @@ -149,6 +175,7 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner): self.md_additional_offset_gpu, self.rel_gain_xray_map_gpu, self.badpixel_map_gpu, + self.badpixel_mask_value, self.gain_map_gpu, self.processed_data_gpu, ), @@ -168,3 +195,31 @@ class AgipdGpuRunner(base_gpu.BaseGpuRunner): ) self.source_module = cupy.RawModule(code=kernel_source) self.correction_kernel = self.source_module.get_function("correct") + + +class BadPixelValues(enum.IntFlag): + """The European XFEL Bad Pixel Encoding + + Straight from pycalibration's enum.py""" + + OFFSET_OUT_OF_THRESHOLD = 0b000000000000000000001 # bit 1 + NOISE_OUT_OF_THRESHOLD = 0b000000000000000000010 # bit 2 + OFFSET_NOISE_EVAL_ERROR = 0b000000000000000000100 # bit 3 + NO_DARK_DATA = 0b000000000000000001000 # bit 4 + CI_GAIN_OF_OF_THRESHOLD = 0b000000000000000010000 # bit 5 + CI_LINEAR_DEVIATION = 0b000000000000000100000 # bit 6 + CI_EVAL_ERROR = 0b000000000000001000000 # bit 7 + FF_GAIN_EVAL_ERROR = 0b000000000000010000000 # bit 8 + FF_GAIN_DEVIATION = 0b000000000000100000000 # bit 9 + FF_NO_ENTRIES = 0b000000000001000000000 # bit 10 + CI2_EVAL_ERROR = 0b000000000010000000000 # bit 11 + VALUE_IS_NAN = 0b000000000100000000000 # bit 12 + VALUE_OUT_OF_RANGE = 0b000000001000000000000 # bit 13 + GAIN_THRESHOLDING_ERROR = 0b000000010000000000000 # bit 14 + DATA_STD_IS_ZERO = 0b000000100000000000000 # bit 15 + ASIC_STD_BELOW_NOISE = 0b000001000000000000000 # bit 16 + INTERPOLATED = 0b000010000000000000000 # bit 17 + NOISY_ADC = 0b000100000000000000000 # bit 18 + OVERSCAN = 0b001000000000000000000 # bit 19 + NON_SENSITIVE = 0b010000000000000000000 # bit 20 + NON_LIN_RESPONSE_REGION = 0b100000000000000000000 # bit 21 diff --git a/src/calng/agipd_gpu_kernels.cpp b/src/calng/agipd_gpu_kernels.cpp index 898e0b87866457a7d57ba4fdc379b6c3f52fde17..357c82a8b8270f9dfee94af3957c7a8d0a40f469 100644 --- a/src/calng/agipd_gpu_kernels.cpp +++ b/src/calng/agipd_gpu_kernels.cpp @@ -1,5 +1,4 @@ #include <cuda_fp16.h> -#include <math_constants.h> {{corr_enum}} @@ -20,6 +19,7 @@ extern "C" { const float* md_additional_offset, const float* rel_gain_xray_map, const unsigned int* bad_pixel_map, + const float bad_pixel_mask_value, unsigned char* gain_map, {{output_data_dtype}}* output) { const size_t X = {{pixels_x}}; @@ -107,7 +107,7 @@ extern "C" { x * gm_map_stride_x; if ((corr_flags & BPMASK) && bad_pixel_map[gm_map_index]) { - corrected = CUDART_NAN_F; + corrected = bad_pixel_mask_value; } else { if (corr_flags & OFFSET) { corrected -= offset_map[gm_map_index]; diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py index cfc3c3a6dde280853ba3b2116c2e544239c7541b..1f87b97322fa94c981f346fcaa33beeea5b28f71 100644 --- a/src/calng/base_correction.py +++ b/src/calng/base_correction.py @@ -463,8 +463,6 @@ class BaseCorrection(calibrationBase.CalibrationReceiverBaseDevice): def requestConstant(self, name, mostRecent=False, tryRemote=True): """constantLoaded hook would have gotten called without naming constant, so here we go. Ugly hooking it.""" - # TODO: clear from device, too - # TODO: update correction capability flag if name in self._cached_constants: del self._cached_constants[name] super().requestConstant(name, mostRecent, tryRemote) @@ -477,6 +475,14 @@ class BaseCorrection(calibrationBase.CalibrationReceiverBaseDevice): self._load_constant_to_gpu(name, constant) self._update_correction_flags() + def flush_constants(self): + """Override from CalibrationReceiverBaseDevice to also flush GPU buffers""" + super().flush_constants() + for correction_step, _ in self._correction_slot_names: + self.set(f"corrections.available.{correction_step}", False) + self.gpu_runner.flush_buffers() + self._update_correction_flags() + def _write_output(self, data, old_metadata): metadata = ChannelMetaData( old_metadata.get("source"), diff --git a/src/calng/base_gpu.py b/src/calng/base_gpu.py index 5347fc2a1b730c10cd3fd5663e9c16a73342a603..fc716ee3e3d4d87dd4655a8d537493f86c5a4dbb 100644 --- a/src/calng/base_gpu.py +++ b/src/calng/base_gpu.py @@ -78,14 +78,18 @@ class BaseGpuRunner: # functions to get data from respective buffers to cell, x, y shape for preview computation # TODO: handle shape juggling programmatically, removing need for these two helpers - def _preview_preprocess_raw(): + def _preview_preprocess_raw(self): """Should return view of self.input_data_gpu with shape (cell, x, y)""" raise NotImplementedError() - def _preview_preprocess_corr(): + def _preview_preprocess_corr(self): """Should return view of self.processed_data_gpu with shape (cell, x, y)""" raise NotImplementedError() + def flush_buffers(self): + """Optional reset GPU buffers (implement in appropriate subclasses""" + pass + def correct(self, flags): """Correct (already loaded) image data according to flags