diff --git a/src/calng/AgipdCorrection.py b/src/calng/AgipdCorrection.py
index 14012742a24d9870efc0d368425b23e3b01eadca..d0e46321b1b1e0b6dbdbdb0b049eb60c67fc376c 100644
--- a/src/calng/AgipdCorrection.py
+++ b/src/calng/AgipdCorrection.py
@@ -1,6 +1,10 @@
+import enum
+
+import cupy
 import numpy as np
 from karabo.bound import (
     BOOL_ELEMENT,
+    DOUBLE_ELEMENT,
     FLOAT_ELEMENT,
     KARABO_CLASSINFO,
     NODE_ELEMENT,
@@ -10,11 +14,405 @@ from karabo.bound import (
 )
 from karabo.common.states import State
 
-from . import utils
+from . import base_gpu, calcat_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, preview_schema
-from .calcat_utils import AgipdCalcatFriend, AgipdConstants
+
+
+class AgipdConstants(enum.Enum):
+    SlopesFF = enum.auto()
+    ThresholdsDark = enum.auto()
+    Offset = enum.auto()
+    SlopesPC = enum.auto()
+    BadPixelsDark = enum.auto()
+    BadPixelsPC = enum.auto()
+    BadPixelsFF = enum.auto()
+
+
+# from pycalibration's enum.py
+class AgipdGainMode(enum.IntEnum):
+    ADAPTIVE_GAIN = 0
+    FIXED_HIGH_GAIN = 1
+    FIXED_MEDIUM_GAIN = 2
+    FIXED_LOW_GAIN = 3
+
+
+class BadPixelValues(enum.IntFlag):
+    """The European XFEL Bad Pixel Encoding
+
+    Straight from pycalibration's enum.py"""
+
+    OFFSET_OUT_OF_THRESHOLD = 2 ** 0
+    NOISE_OUT_OF_THRESHOLD = 2 ** 1
+    OFFSET_NOISE_EVAL_ERROR = 2 ** 2
+    NO_DARK_DATA = 2 ** 3
+    CI_GAIN_OUT_OF_THRESHOLD = 2 ** 4
+    CI_LINEAR_DEVIATION = 2 ** 5
+    CI_EVAL_ERROR = 2 ** 6
+    FF_GAIN_EVAL_ERROR = 2 ** 7
+    FF_GAIN_DEVIATION = 2 ** 8
+    FF_NO_ENTRIES = 2 ** 9
+    CI2_EVAL_ERROR = 2 ** 10
+    VALUE_IS_NAN = 2 ** 11
+    VALUE_OUT_OF_RANGE = 2 ** 12
+    GAIN_THRESHOLDING_ERROR = 2 ** 13
+    DATA_STD_IS_ZERO = 2 ** 14
+    ASIC_STD_BELOW_NOISE = 2 ** 15
+    INTERPOLATED = 2 ** 16
+    NOISY_ADC = 2 ** 17
+    OVERSCAN = 2 ** 18
+    NON_SENSITIVE = 2 ** 19
+    NON_LIN_RESPONSE_REGION = 2 ** 20
+
+
+class CorrectionFlags(enum.IntFlag):
+    NONE = 0
+    THRESHOLD = 1
+    OFFSET = 2
+    BLSHIFT = 4
+    REL_GAIN_PC = 8
+    REL_GAIN_XRAY = 16
+    BPMASK = 32
+
+
+class AgipdGpuRunner(base_gpu.BaseGpuRunner):
+    _kernel_source_filename = "agipd_gpu.cu"
+    _corrected_axis_order = "cxy"
+
+    def __init__(
+        self,
+        pixels_x,
+        pixels_y,
+        memory_cells,
+        constant_memory_cells,
+        input_data_dtype=cupy.uint16,
+        output_data_dtype=cupy.float32,
+        bad_pixel_mask_value=cupy.nan,
+        gain_mode=AgipdGainMode.ADAPTIVE_GAIN,
+        g_gain_value=1,
+    ):
+        self.gain_mode = gain_mode
+        if self.gain_mode is AgipdGainMode.ADAPTIVE_GAIN:
+            self.default_gain = cupy.uint8(gain_mode)
+        else:
+            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__(
+            pixels_x,
+            pixels_y,
+            memory_cells,
+            constant_memory_cells,
+            input_data_dtype,
+            output_data_dtype,
+        )
+        self.gain_map_gpu = cupy.empty(self.processed_shape, dtype=cupy.float32)
+        self.preview_buffer_getters.extend(
+            [self._get_raw_gain_for_preview, self._get_gain_map_for_preview]
+        )
+
+        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=cupy.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=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.set_bad_pixel_mask_value(bad_pixel_mask_value)
+        self.set_g_gain_value(g_gain_value)
+
+        self.update_block_size((1, 1, 64))
+
+    def _get_raw_for_preview(self):
+        return self.input_data_gpu[:, 0]
+
+    def _get_corrected_for_preview(self):
+        return self.processed_data_gpu
+
+    # special to AGIPD
+    def _get_raw_gain_for_preview(self):
+        return self.input_data_gpu[:, 1]
+
+    def _get_gain_map_for_preview(self):
+        return self.gain_map_gpu
+
+    def load_thresholds(self, threshold_map):
+        # shape: y, x, memory cell, thresholds and gain values
+        # note: the gain values are something like means used to derive thresholds
+        self.gain_thresholds_gpu.set(
+            np.transpose(threshold_map[..., :2], (2, 1, 0, 3)).astype(np.float32)
+        )
+
+    def load_offset_map(self, offset_map):
+        # shape: y, x, memory cell, gain stage
+        self.offset_map_gpu.set(
+            np.transpose(offset_map, (2, 1, 0, 3)).astype(np.float32)
+        )
+
+    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)
+        # the following may contain NaNs, though...
+        hg_slope = slopes_pc_map[0]
+        hg_intercept = slopes_pc_map[1]
+        mg_slope = slopes_pc_map[3]
+        mg_intercept = slopes_pc_map[4]
+        # TODO: remove sanitization (should happen in constant preparation notebook)
+        # from agipdlib.py: replace NaN with median (per memory cell)
+        # note: suffixes in agipdlib are "_m" and "_l", should probably be "_I"
+        for naughty_array in (hg_slope, hg_intercept, mg_slope, mg_intercept):
+            medians = np.nanmedian(naughty_array, axis=(1, 2))
+            nan_bool = np.isnan(naughty_array)
+            nan_cell, _, _ = np.where(nan_bool)
+            naughty_array[nan_bool] = medians[nan_cell]
+
+            too_low_bool = naughty_array < 0.8 * medians[:, np.newaxis, np.newaxis]
+            too_low_cell, _, _ = np.where(too_low_bool)
+            naughty_array[too_low_bool] = medians[too_low_cell]
+
+            too_high_bool = naughty_array > 1.2 * medians[:, np.newaxis, np.newaxis]
+            too_high_cell, _, _ = np.where(too_high_bool)
+            naughty_array[too_high_bool] = medians[too_high_cell]
+
+        frac_hg_mg = hg_slope / mg_slope
+        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
+        self.rel_gain_pc_map_gpu.set(np.transpose(rel_gain_map, (1, 3, 2, 0)))
+        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)
+
+    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
+        if slopes_ff_map.shape[2] == 2:
+            # TODO: remove support for old format
+            # old format, is per pixel only (shape is y, x, 2)
+            # note: we should not support this in online
+            slopes_ff_map = np.broadcast_to(
+                slopes_ff_map[..., 0][..., np.newaxis],
+                (self.pixels_y, self.pixels_x, self.constant_memory_cells),
+            )
+        self.rel_gain_xray_map_gpu.set(np.transpose(slopes_ff_map).astype(np.float32))
+
+    def set_g_gain_value(self, override_value):
+        self.g_gain_value = cupy.float32(override_value)
+
+    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: inquire what "mask for double size pixels" means
+        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.bad_pixel_map_gpu |= cupy.asarray(
+                    np.broadcast_to(
+                        np.transpose(bad_pixels_map)[..., np.newaxis],
+                        self.gm_map_shape,
+                    ),
+                    dtype=np.uint32,
+                )
+            elif bad_pixels_map.shape == (
+                self.constant_memory_cells,
+                self.pixels_y,
+                self.pixels_x,
+            ):
+                # oh, can also be old bad pixels pc?
+                self.bad_pixel_map_gpu |= cupy.asarray(
+                    np.broadcast_to(
+                        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? {bad_pixels_map.shape}"
+                )
+        else:
+            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)
+
+    def override_bad_pixel_flags_to_use(self, override_value):
+        self.bad_pixel_map_gpu &= cupy.uint32(override_value)
+
+    def set_bad_pixel_mask_value(self, mask_value):
+        self.bad_pixel_mask_value = cupy.float32(mask_value)
+
+    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.bad_pixel_map_gpu.fill(0)
+
+    # TODO: baseline shift
+
+    def correct(self, flags):
+        if flags & CorrectionFlags.BLSHIFT:
+            raise NotImplementedError("Baseline shift not implemented yet")
+        if self.gain_mode is not AgipdGainMode.ADAPTIVE_GAIN and (
+            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,
+                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.g_gain_value,
+                self.bad_pixel_map_gpu,
+                self.bad_pixel_mask_value,
+                self.gain_map_gpu,
+                self.processed_data_gpu,
+            ),
+        )
+
+    def _init_kernels(self):
+        kernel_source = self._kernel_template.render(
+            {
+                "pixels_x": self.pixels_x,
+                "pixels_y": self.pixels_y,
+                "data_memory_cells": self.memory_cells,
+                "constant_memory_cells": self.constant_memory_cells,
+                "input_data_dtype": utils.np_dtype_to_c_type(self.input_data_dtype),
+                "output_data_dtype": utils.np_dtype_to_c_type(self.output_data_dtype),
+                "corr_enum": utils.enum_to_c_template(CorrectionFlags),
+            }
+        )
+        self.source_module = cupy.RawModule(code=kernel_source)
+        self.correction_kernel = self.source_module.get_function("correct")
+
+
+class AgipdCalcatFriend(calcat_utils.BaseCalcatFriend):
+    _constant_enum_class = AgipdConstants
+
+    def __init__(self, device, *args, **kwargs):
+        super().__init__(device, *args, **kwargs)
+        self._constants_need_conditions = {
+            AgipdConstants.ThresholdsDark: self.dark_condition,
+            AgipdConstants.Offset: self.dark_condition,
+            AgipdConstants.SlopesPC: self.dark_condition,
+            AgipdConstants.SlopesFF: self.illuminated_condition,
+            AgipdConstants.BadPixelsDark: self.dark_condition,
+            AgipdConstants.BadPixelsPC: self.dark_condition,
+            AgipdConstants.BadPixelsFF: self.illuminated_condition,
+        }
+
+    @staticmethod
+    def add_schema(
+        schema,
+        managed_keys,
+        param_prefix="constantParameters",
+        status_prefix="foundConstants",
+    ):
+        super(AgipdCalcatFriend, AgipdCalcatFriend).add_schema(
+            schema, managed_keys, "AGIPD-Type", param_prefix, status_prefix
+        )
+        schema.setDefaultValue(f"{param_prefix}.memoryCells", 352)
+
+        (
+            DOUBLE_ELEMENT(schema)
+            .key(f"{param_prefix}.acquisitionRate")
+            .assignmentOptional()
+            .defaultValue(1.1)
+            .reconfigurable()
+            .commit(),
+            DOUBLE_ELEMENT(schema)
+            .key(f"{param_prefix}.gainSetting")
+            .assignmentOptional()
+            .defaultValue(0)
+            .reconfigurable()
+            .commit(),
+            DOUBLE_ELEMENT(schema)
+            .key(f"{param_prefix}.photonEnergy")
+            .assignmentOptional()
+            .defaultValue(9.2)
+            .reconfigurable()
+            .commit(),
+            DOUBLE_ELEMENT(schema)
+            .key(f"{param_prefix}.gainMode")
+            .assignmentOptional()
+            .defaultValue(0)
+            .reconfigurable()
+            .commit(),
+            DOUBLE_ELEMENT(schema)
+            .key(f"{param_prefix}.integrationTime")
+            .assignmentOptional()
+            .defaultValue(12)
+            .reconfigurable()
+            .commit(),
+        )
+        managed_keys.add(f"{param_prefix}.acquisitionRate")
+        managed_keys.add(f"{param_prefix}.gainSetting")
+        managed_keys.add(f"{param_prefix}.photonEnergy")
+        managed_keys.add(f"{param_prefix}.integrationTime")
+
+        calcat_utils.add_status_schema_from_enum(schema, status_prefix, AgipdConstants)
+
+    def dark_condition(self):
+        res = calcat_utils.OperatingConditions()
+        res["Memory cells"] = self._get_param("memoryCells")
+        res["Sensor Bias Voltage"] = self._get_param("biasVoltage")
+        res["Pixels X"] = self._get_param("pixelsX")
+        res["Pixels Y"] = self._get_param("pixelsY")
+        res["Acquisition rate"] = self._get_param("acquisitionRate")
+        # TODO: make configurable whether or not to include gain setting?
+        integration_time = self._get_param("integrationTime")
+        if integration_time != 12:
+            res["Integration Time"] = integration_time
+        res["Gain Setting"] = self._get_param("gainSetting")
+        return res
+
+    def illuminated_condition(self):
+        res = calcat_utils.OperatingConditions()
+        res["Memory cells"] = self._get_param("memoryCells")
+        res["Sensor Bias Voltage"] = self._get_param("biasVoltage")
+        res["Pixels X"] = self._get_param("pixelsX")
+        res["Pixels Y"] = self._get_param("pixelsY")
+        res["Source Energy"] = self._get_param("photonEnergy")
+        res["Acquisition rate"] = self._get_param("acquisitionRate")
+        integration_time = self._get_param("integrationTime")
+        if integration_time != 12:
+            res["Integration Time"] = integration_time
+        res["Gain Setting"] = self._get_param("gainSetting")
+        return res
 
 
 @KARABO_CLASSINFO("AgipdCorrection", deviceVersion)
@@ -192,8 +590,6 @@ class AgipdCorrection(BaseCorrection):
             "g_gain_value": config.get("corrections.relGainXray.gGainValue"),
         }
 
-        self._shmem_buffer_gain_map = None
-
         # configurability: overriding md_additional_offset
         if config.get("corrections.relGainPc.overrideMdAdditionalOffset"):
             self._override_md_additional_offset = config.get(
diff --git a/src/calng/DsscCorrection.py b/src/calng/DsscCorrection.py
index 8698cf68ecf1bc9b9c9aab457c32ef7a2588e370..8c586b3df556abf734badd0e80677e5c5ad1a20d 100644
--- a/src/calng/DsscCorrection.py
+++ b/src/calng/DsscCorrection.py
@@ -1,12 +1,150 @@
+import enum
+
+import cupy
 import numpy as np
-from karabo.bound import KARABO_CLASSINFO, VECTOR_STRING_ELEMENT
+from karabo.bound import DOUBLE_ELEMENT, KARABO_CLASSINFO, VECTOR_STRING_ELEMENT
 from karabo.common.states import State
 
-from . import utils
+from . import base_gpu, calcat_utils, utils
 from ._version import version as deviceVersion
 from .base_correction import BaseCorrection, add_correction_step_schema
-from .calcat_utils import DsscCalcatFriend, DsscConstants
-from .dssc_gpu import CorrectionFlags, DsscGpuRunner
+
+
+class CorrectionFlags(enum.IntFlag):
+    NONE = 0
+    OFFSET = 1
+
+
+class DsscConstants(enum.Enum):
+    Offset = enum.auto()
+
+
+class DsscGpuRunner(base_gpu.BaseGpuRunner):
+    _kernel_source_filename = "dssc_gpu.cu"
+    _corrected_axis_order = "cyx"
+
+    def __init__(
+        self,
+        pixels_x,
+        pixels_y,
+        memory_cells,
+        constant_memory_cells,
+        input_data_dtype=np.uint16,
+        output_data_dtype=np.float32,
+    ):
+        self.input_shape = (memory_cells, pixels_y, pixels_x)
+        self.processed_shape = self.input_shape
+        super().__init__(
+            pixels_x,
+            pixels_y,
+            memory_cells,
+            constant_memory_cells,
+            input_data_dtype,
+            output_data_dtype,
+        )
+
+        self.map_shape = (self.constant_memory_cells, self.pixels_y, self.pixels_x)
+        self.offset_map_gpu = cupy.empty(self.map_shape, dtype=np.float32)
+
+        self._init_kernels()
+
+        self.offset_map_gpu = cupy.empty(self.map_shape, dtype=np.float32)
+
+        self.update_block_size((1, 1, 64))
+
+    def _get_raw_for_preview(self):
+        return self.input_data_gpu
+
+    def _get_corrected_for_preview(self):
+        return self.processed_data_gpu
+
+    def load_offset_map(self, offset_map):
+        # can have an extra dimension for some reason
+        if len(offset_map.shape) == 4:  # old format (see offsetcorrection_dssc.py)?
+            offset_map = offset_map[..., 0]
+        # shape (now): x, y, memory cell
+        offset_map = np.transpose(offset_map).astype(np.float32)
+        self.offset_map_gpu.set(offset_map)
+
+    def correct(self, flags):
+        self.correction_kernel(
+            self.full_grid,
+            self.full_block,
+            (
+                self.input_data_gpu,
+                self.cell_table_gpu,
+                np.uint8(flags),
+                self.offset_map_gpu,
+                self.processed_data_gpu,
+            ),
+        )
+
+    def _init_kernels(self):
+        kernel_source = self._kernel_template.render(
+            {
+                "pixels_x": self.pixels_x,
+                "pixels_y": self.pixels_y,
+                "data_memory_cells": self.memory_cells,
+                "constant_memory_cells": self.constant_memory_cells,
+                "input_data_dtype": utils.np_dtype_to_c_type(self.input_data_dtype),
+                "output_data_dtype": utils.np_dtype_to_c_type(self.output_data_dtype),
+                "corr_enum": utils.enum_to_c_template(CorrectionFlags),
+            }
+        )
+        self.source_module = cupy.RawModule(code=kernel_source)
+        self.correction_kernel = self.source_module.get_function("correct")
+
+
+class DsscCalcatFriend(calcat_utils.BaseCalcatFriend):
+    _constant_enum_class = DsscConstants
+
+    def __init__(self, device, *args, **kwargs):
+        super().__init__(device, *args, **kwargs)
+        self._constants_need_conditions = {
+            DsscConstants.Offset: self.dark_condition,
+        }
+
+    @staticmethod
+    def add_schema(
+        schema,
+        managed_keys,
+        param_prefix="constantParameters",
+        status_prefix="foundConstants",
+    ):
+        super(DsscCalcatFriend, DsscCalcatFriend).add_schema(
+            schema, managed_keys, "DSSC-Type", param_prefix, status_prefix
+        )
+        schema.setDefaultValue(f"{param_prefix}.memoryCells", 400)
+        (
+            DOUBLE_ELEMENT(schema)
+            .key(f"{param_prefix}.pulseIdChecksum")
+            .assignmentOptional()
+            .defaultValue(2.8866323107820637e-36)
+            .commit(),
+            DOUBLE_ELEMENT(schema)
+            .key(f"{param_prefix}.acquisitionRate")
+            .assignmentOptional()
+            .defaultValue(4.5)
+            .commit(),
+            DOUBLE_ELEMENT(schema)
+            .key(f"{param_prefix}.encodedGain")
+            .assignmentOptional()
+            .defaultValue(67328)
+            .commit(),
+        )
+
+        calcat_utils.add_status_schema_from_enum(schema, status_prefix, DsscConstants)
+
+    def dark_condition(self):
+        res = calcat_utils.OperatingConditions()
+        res["Memory cells"] = self._get_param("memoryCells")
+        res["Sensor Bias Voltage"] = self._get_param("biasVoltage")
+        res["Pixels X"] = self._get_param("pixelsX")
+        res["Pixels Y"] = self._get_param("pixelsY")
+        # res["Pulse id checksum"] = self._get_param("pulseIdChecksum")
+        # res["Acquisition rate"] = self._get_param("acquisitionRate")
+        # res["Encoded gain"] = self._get_param("encodedGain")
+        return res
 
 
 @KARABO_CLASSINFO("DsscCorrection", deviceVersion)
diff --git a/src/calng/agipd_gpu.py b/src/calng/agipd_gpu.py
deleted file mode 100644
index 98d26c547e49f4a6bf30d12b937b74d17601d517..0000000000000000000000000000000000000000
--- a/src/calng/agipd_gpu.py
+++ /dev/null
@@ -1,297 +0,0 @@
-import enum
-
-import cupy
-import numpy as np
-
-from . import base_gpu, utils
-
-
-class CorrectionFlags(enum.IntFlag):
-    NONE = 0
-    THRESHOLD = 1
-    OFFSET = 2
-    BLSHIFT = 4
-    REL_GAIN_PC = 8
-    REL_GAIN_XRAY = 16
-    BPMASK = 32
-
-
-# from pycalibration's enum.py
-class AgipdGainMode(enum.IntEnum):
-    ADAPTIVE_GAIN = 0
-    FIXED_HIGH_GAIN = 1
-    FIXED_MEDIUM_GAIN = 2
-    FIXED_LOW_GAIN = 3
-
-
-class AgipdGpuRunner(base_gpu.BaseGpuRunner):
-    _kernel_source_filename = "agipd_gpu.cu"
-    _corrected_axis_order = "cxy"
-
-    def __init__(
-        self,
-        pixels_x,
-        pixels_y,
-        memory_cells,
-        constant_memory_cells,
-        input_data_dtype=cupy.uint16,
-        output_data_dtype=cupy.float32,
-        bad_pixel_mask_value=cupy.nan,
-        gain_mode=AgipdGainMode.ADAPTIVE_GAIN,
-        g_gain_value=1,
-    ):
-        self.gain_mode = gain_mode
-        if self.gain_mode is AgipdGainMode.ADAPTIVE_GAIN:
-            self.default_gain = cupy.uint8(gain_mode)
-        else:
-            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__(
-            pixels_x,
-            pixels_y,
-            memory_cells,
-            constant_memory_cells,
-            input_data_dtype,
-            output_data_dtype,
-        )
-        self.gain_map_gpu = cupy.empty(self.processed_shape, dtype=cupy.float32)
-        self.preview_buffer_getters.extend(
-            [self._get_raw_gain_for_preview, self._get_gain_map_for_preview]
-        )
-
-        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=cupy.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=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.set_bad_pixel_mask_value(bad_pixel_mask_value)
-        self.set_g_gain_value(g_gain_value)
-
-        self.update_block_size((1, 1, 64))
-
-    def _get_raw_for_preview(self):
-        return self.input_data_gpu[:, 0]
-
-    def _get_corrected_for_preview(self):
-        return self.processed_data_gpu
-
-    # special to AGIPD
-    def _get_raw_gain_for_preview(self):
-        return self.input_data_gpu[:, 1]
-
-    def _get_gain_map_for_preview(self):
-        return self.gain_map_gpu
-
-    def load_thresholds(self, threshold_map):
-        # shape: y, x, memory cell, thresholds and gain values
-        # note: the gain values are something like means used to derive thresholds
-        self.gain_thresholds_gpu.set(
-            np.transpose(threshold_map[..., :2], (2, 1, 0, 3)).astype(np.float32)
-        )
-
-    def load_offset_map(self, offset_map):
-        # shape: y, x, memory cell, gain stage
-        self.offset_map_gpu.set(
-            np.transpose(offset_map, (2, 1, 0, 3)).astype(np.float32)
-        )
-
-    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)
-        # the following may contain NaNs, though...
-        hg_slope = slopes_pc_map[0]
-        hg_intercept = slopes_pc_map[1]
-        mg_slope = slopes_pc_map[3]
-        mg_intercept = slopes_pc_map[4]
-        # TODO: remove sanitization (should happen in constant preparation notebook)
-        # from agipdlib.py: replace NaN with median (per memory cell)
-        # note: suffixes in agipdlib are "_m" and "_l", should probably be "_I"
-        for naughty_array in (hg_slope, hg_intercept, mg_slope, mg_intercept):
-            medians = np.nanmedian(naughty_array, axis=(1, 2))
-            nan_bool = np.isnan(naughty_array)
-            nan_cell, _, _ = np.where(nan_bool)
-            naughty_array[nan_bool] = medians[nan_cell]
-
-            too_low_bool = naughty_array < 0.8 * medians[:, np.newaxis, np.newaxis]
-            too_low_cell, _, _ = np.where(too_low_bool)
-            naughty_array[too_low_bool] = medians[too_low_cell]
-
-            too_high_bool = naughty_array > 1.2 * medians[:, np.newaxis, np.newaxis]
-            too_high_cell, _, _ = np.where(too_high_bool)
-            naughty_array[too_high_bool] = medians[too_high_cell]
-
-        frac_hg_mg = hg_slope / mg_slope
-        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
-        self.rel_gain_pc_map_gpu.set(np.transpose(rel_gain_map, (1, 3, 2, 0)))
-        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)
-
-    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
-        if slopes_ff_map.shape[2] == 2:
-            # TODO: remove support for old format
-            # old format, is per pixel only (shape is y, x, 2)
-            # note: we should not support this in online
-            slopes_ff_map = np.broadcast_to(
-                slopes_ff_map[..., 0][..., np.newaxis],
-                (self.pixels_y, self.pixels_x, self.constant_memory_cells),
-            )
-        self.rel_gain_xray_map_gpu.set(np.transpose(slopes_ff_map).astype(np.float32))
-
-    def set_g_gain_value(self, override_value):
-        self.g_gain_value = cupy.float32(override_value)
-
-    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: inquire what "mask for double size pixels" means
-        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.bad_pixel_map_gpu |= cupy.asarray(
-                    np.broadcast_to(
-                        np.transpose(bad_pixels_map)[..., np.newaxis],
-                        self.gm_map_shape,
-                    ),
-                    dtype=np.uint32,
-                )
-            elif bad_pixels_map.shape == (
-                self.constant_memory_cells,
-                self.pixels_y,
-                self.pixels_x,
-            ):
-                # oh, can also be old bad pixels pc?
-                self.bad_pixel_map_gpu |= cupy.asarray(
-                    np.broadcast_to(
-                        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? {bad_pixels_map.shape}"
-                )
-        else:
-            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)
-
-    def override_bad_pixel_flags_to_use(self, override_value):
-        self.bad_pixel_map_gpu &= cupy.uint32(override_value)
-
-    def set_bad_pixel_mask_value(self, mask_value):
-        self.bad_pixel_mask_value = cupy.float32(mask_value)
-
-    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.bad_pixel_map_gpu.fill(0)
-
-    # TODO: baseline shift
-
-    def correct(self, flags):
-        if flags & CorrectionFlags.BLSHIFT:
-            raise NotImplementedError("Baseline shift not implemented yet")
-        if self.gain_mode is not AgipdGainMode.ADAPTIVE_GAIN and (
-            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,
-                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.g_gain_value,
-                self.bad_pixel_map_gpu,
-                self.bad_pixel_mask_value,
-                self.gain_map_gpu,
-                self.processed_data_gpu,
-            ),
-        )
-
-    def _init_kernels(self):
-        kernel_source = self._kernel_template.render(
-            {
-                "pixels_x": self.pixels_x,
-                "pixels_y": self.pixels_y,
-                "data_memory_cells": self.memory_cells,
-                "constant_memory_cells": self.constant_memory_cells,
-                "input_data_dtype": utils.np_dtype_to_c_type(self.input_data_dtype),
-                "output_data_dtype": utils.np_dtype_to_c_type(self.output_data_dtype),
-                "corr_enum": utils.enum_to_c_template(CorrectionFlags),
-            }
-        )
-        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 = 2 ** 0
-    NOISE_OUT_OF_THRESHOLD = 2 ** 1
-    OFFSET_NOISE_EVAL_ERROR = 2 ** 2
-    NO_DARK_DATA = 2 ** 3
-    CI_GAIN_OUT_OF_THRESHOLD = 2 ** 4
-    CI_LINEAR_DEVIATION = 2 ** 5
-    CI_EVAL_ERROR = 2 ** 6
-    FF_GAIN_EVAL_ERROR = 2 ** 7
-    FF_GAIN_DEVIATION = 2 ** 8
-    FF_NO_ENTRIES = 2 ** 9
-    CI2_EVAL_ERROR = 2 ** 10
-    VALUE_IS_NAN = 2 ** 11
-    VALUE_OUT_OF_RANGE = 2 ** 12
-    GAIN_THRESHOLDING_ERROR = 2 ** 13
-    DATA_STD_IS_ZERO = 2 ** 14
-    ASIC_STD_BELOW_NOISE = 2 ** 15
-    INTERPOLATED = 2 ** 16
-    NOISY_ADC = 2 ** 17
-    OVERSCAN = 2 ** 18
-    NON_SENSITIVE = 2 ** 19
-    NON_LIN_RESPONSE_REGION = 2 ** 20
diff --git a/src/calng/calcat_utils.py b/src/calng/calcat_utils.py
index 49403228e64b6279de8ade0c75c391709cc9d4c3..0b9099cc02b671543d665d81b23c73b57323af6b 100644
--- a/src/calng/calcat_utils.py
+++ b/src/calng/calcat_utils.py
@@ -1,10 +1,8 @@
 import copy
-import enum
 import functools
 import json
 import pathlib
 import threading
-import typing
 
 import calibration_client
 import h5py
@@ -51,7 +49,7 @@ class CalibrationClientConfigError(Exception):
     pass
 
 
-def _add_status_schema_from_enum(schema, prefix, enum_class):
+def add_status_schema_from_enum(schema, prefix, enum_class):
     for constant in enum_class:
         constant_node = f"{prefix}.{constant.name}"
         (
@@ -532,164 +530,3 @@ class BaseCalcatFriend:
             if warning is not None:
                 self.device.log_status_warn(warning)
                 raise to_raise
-
-
-class AgipdConstants(enum.Enum):
-    SlopesFF = enum.auto()
-    ThresholdsDark = enum.auto()
-    Offset = enum.auto()
-    SlopesPC = enum.auto()
-    BadPixelsDark = enum.auto()
-    BadPixelsPC = enum.auto()
-    BadPixelsFF = enum.auto()
-
-
-class AgipdCalcatFriend(BaseCalcatFriend):
-    _constant_enum_class = AgipdConstants
-
-    def __init__(self, device, *args, **kwargs):
-        super().__init__(device, *args, **kwargs)
-        self._constants_need_conditions = {
-            AgipdConstants.ThresholdsDark: self.dark_condition,
-            AgipdConstants.Offset: self.dark_condition,
-            AgipdConstants.SlopesPC: self.dark_condition,
-            AgipdConstants.SlopesFF: self.illuminated_condition,
-            AgipdConstants.BadPixelsDark: self.dark_condition,
-            AgipdConstants.BadPixelsPC: self.dark_condition,
-            AgipdConstants.BadPixelsFF: self.illuminated_condition,
-        }
-
-    @staticmethod
-    def add_schema(
-        schema,
-        managed_keys,
-        param_prefix="constantParameters",
-        status_prefix="foundConstants",
-    ):
-        super(AgipdCalcatFriend, AgipdCalcatFriend).add_schema(
-            schema, managed_keys, "AGIPD-Type", param_prefix, status_prefix
-        )
-        schema.setDefaultValue(f"{param_prefix}.memoryCells", 352)
-
-        (
-            DOUBLE_ELEMENT(schema)
-            .key(f"{param_prefix}.acquisitionRate")
-            .assignmentOptional()
-            .defaultValue(1.1)
-            .reconfigurable()
-            .commit(),
-            DOUBLE_ELEMENT(schema)
-            .key(f"{param_prefix}.gainSetting")
-            .assignmentOptional()
-            .defaultValue(0)
-            .reconfigurable()
-            .commit(),
-            DOUBLE_ELEMENT(schema)
-            .key(f"{param_prefix}.photonEnergy")
-            .assignmentOptional()
-            .defaultValue(9.2)
-            .reconfigurable()
-            .commit(),
-            DOUBLE_ELEMENT(schema)
-            .key(f"{param_prefix}.gainMode")
-            .assignmentOptional()
-            .defaultValue(0)
-            .reconfigurable()
-            .commit(),
-            DOUBLE_ELEMENT(schema)
-            .key(f"{param_prefix}.integrationTime")
-            .assignmentOptional()
-            .defaultValue(12)
-            .reconfigurable()
-            .commit(),
-        )
-        managed_keys.add(f"{param_prefix}.acquisitionRate")
-        managed_keys.add(f"{param_prefix}.gainSetting")
-        managed_keys.add(f"{param_prefix}.photonEnergy")
-        managed_keys.add(f"{param_prefix}.integrationTime")
-
-        _add_status_schema_from_enum(schema, status_prefix, AgipdConstants)
-
-    def dark_condition(self):
-        res = OperatingConditions()
-        res["Memory cells"] = self._get_param("memoryCells")
-        res["Sensor Bias Voltage"] = self._get_param("biasVoltage")
-        res["Pixels X"] = self._get_param("pixelsX")
-        res["Pixels Y"] = self._get_param("pixelsY")
-        res["Acquisition rate"] = self._get_param("acquisitionRate")
-        # TODO: make configurable whether or not to include gain setting?
-        integration_time = self._get_param("integrationTime")
-        if integration_time != 12:
-            res["Integration Time"] = integration_time
-        res["Gain Setting"] = self._get_param("gainSetting")
-        return res
-
-    def illuminated_condition(self):
-        res = OperatingConditions()
-        res["Memory cells"] = self._get_param("memoryCells")
-        res["Sensor Bias Voltage"] = self._get_param("biasVoltage")
-        res["Pixels X"] = self._get_param("pixelsX")
-        res["Pixels Y"] = self._get_param("pixelsY")
-        res["Source Energy"] = self._get_param("photonEnergy")
-        res["Acquisition rate"] = self._get_param("acquisitionRate")
-        integration_time = self._get_param("integrationTime")
-        if integration_time != 12:
-            res["Integration Time"] = integration_time
-        res["Gain Setting"] = self._get_param("gainSetting")
-        return res
-
-
-class DsscConstants(enum.Enum):
-    Offset = enum.auto()
-
-
-class DsscCalcatFriend(BaseCalcatFriend):
-    _constant_enum_class = DsscConstants
-
-    def __init__(self, device, *args, **kwargs):
-        super().__init__(device, *args, **kwargs)
-        self._constants_need_conditions = {
-            DsscConstants.Offset: self.dark_condition,
-        }
-
-    @staticmethod
-    def add_schema(
-        schema,
-        managed_keys,
-        param_prefix="constantParameters",
-        status_prefix="foundConstants",
-    ):
-        super(DsscCalcatFriend, DsscCalcatFriend).add_schema(
-            schema, managed_keys, "DSSC-Type", param_prefix, status_prefix
-        )
-        schema.setDefaultValue(f"{param_prefix}.memoryCells", 400)
-        (
-            DOUBLE_ELEMENT(schema)
-            .key(f"{param_prefix}.pulseIdChecksum")
-            .assignmentOptional()
-            .defaultValue(2.8866323107820637e-36)
-            .commit(),
-            DOUBLE_ELEMENT(schema)
-            .key(f"{param_prefix}.acquisitionRate")
-            .assignmentOptional()
-            .defaultValue(4.5)
-            .commit(),
-            DOUBLE_ELEMENT(schema)
-            .key(f"{param_prefix}.encodedGain")
-            .assignmentOptional()
-            .defaultValue(67328)
-            .commit(),
-        )
-
-        _add_status_schema_from_enum(schema, status_prefix, DsscConstants)
-
-    def dark_condition(self):
-        res = OperatingConditions()
-        res["Memory cells"] = self._get_param("memoryCells")
-        res["Sensor Bias Voltage"] = self._get_param("biasVoltage")
-        res["Pixels X"] = self._get_param("pixelsX")
-        res["Pixels Y"] = self._get_param("pixelsY")
-        # res["Pulse id checksum"] = self._get_param("pulseIdChecksum")
-        # res["Acquisition rate"] = self._get_param("acquisitionRate")
-        # res["Encoded gain"] = self._get_param("encodedGain")
-        return res
diff --git a/src/calng/dssc_gpu.py b/src/calng/dssc_gpu.py
deleted file mode 100644
index b9dc3b091c6e14374adddbb08a9ad4d04340e9f3..0000000000000000000000000000000000000000
--- a/src/calng/dssc_gpu.py
+++ /dev/null
@@ -1,87 +0,0 @@
-import enum
-
-import cupy
-import numpy as np
-
-from . import base_gpu, utils
-
-
-class CorrectionFlags(enum.IntFlag):
-    NONE = 0
-    OFFSET = 1
-
-
-class DsscGpuRunner(base_gpu.BaseGpuRunner):
-    _kernel_source_filename = "dssc_gpu.cu"
-    _corrected_axis_order = "cyx"
-
-    def __init__(
-        self,
-        pixels_x,
-        pixels_y,
-        memory_cells,
-        constant_memory_cells,
-        input_data_dtype=np.uint16,
-        output_data_dtype=np.float32,
-    ):
-        self.input_shape = (memory_cells, pixels_y, pixels_x)
-        self.processed_shape = self.input_shape
-        super().__init__(
-            pixels_x,
-            pixels_y,
-            memory_cells,
-            constant_memory_cells,
-            input_data_dtype,
-            output_data_dtype,
-        )
-
-        self.map_shape = (self.constant_memory_cells, self.pixels_y, self.pixels_x)
-        self.offset_map_gpu = cupy.empty(self.map_shape, dtype=np.float32)
-
-        self._init_kernels()
-
-        self.offset_map_gpu = cupy.empty(self.map_shape, dtype=np.float32)
-
-        self.update_block_size((1, 1, 64))
-
-    def _get_raw_for_preview(self):
-        return self.input_data_gpu
-
-    def _get_corrected_for_preview(self):
-        return self.processed_data_gpu
-
-    def load_offset_map(self, offset_map):
-        # can have an extra dimension for some reason
-        if len(offset_map.shape) == 4:  # old format (see offsetcorrection_dssc.py)?
-            offset_map = offset_map[..., 0]
-        # shape (now): x, y, memory cell
-        offset_map = np.transpose(offset_map).astype(np.float32)
-        self.offset_map_gpu.set(offset_map)
-
-    def correct(self, flags):
-        self.correction_kernel(
-            self.full_grid,
-            self.full_block,
-            (
-                self.input_data_gpu,
-                self.cell_table_gpu,
-                np.uint8(flags),
-                self.offset_map_gpu,
-                self.processed_data_gpu,
-            ),
-        )
-
-    def _init_kernels(self):
-        kernel_source = self._kernel_template.render(
-            {
-                "pixels_x": self.pixels_x,
-                "pixels_y": self.pixels_y,
-                "data_memory_cells": self.memory_cells,
-                "constant_memory_cells": self.constant_memory_cells,
-                "input_data_dtype": utils.np_dtype_to_c_type(self.input_data_dtype),
-                "output_data_dtype": utils.np_dtype_to_c_type(self.output_data_dtype),
-                "corr_enum": utils.enum_to_c_template(CorrectionFlags),
-            }
-        )
-        self.source_module = cupy.RawModule(code=kernel_source)
-        self.correction_kernel = self.source_module.get_function("correct")