diff --git a/src/calng/base_correction.py b/src/calng/base_correction.py
index bd88ad7df4d59ae3602358ecd6045dcd3ab95c75..575022fa7e33f102a356bc86321999f93d8e2523 100644
--- a/src/calng/base_correction.py
+++ b/src/calng/base_correction.py
@@ -91,10 +91,10 @@ class BaseCorrection(PythonDevice):
         raise NotImplementedError()
 
     def _successfully_loaded_constant_to_runner(self, constant):
-        field_name = self._constant_to_correction_name[constant]
-        key = f"corrections.{field_name}.available"
-        if not self.unsafe_get(key):
-            self.set(key, True)
+        for field_name in self._constant_to_correction_names[constant]:
+            key = f"corrections.{field_name}.available"
+            if not self.unsafe_get(key):
+                self.set(key, True)
 
         self._update_correction_flags()
         self.log_status_info(f"Done loading {constant.name} to GPU")
@@ -575,11 +575,10 @@ class BaseCorrection(PythonDevice):
         self._correction_applied_hash = Hash()
         # note: does not handle one constant enabling multiple correction steps
         # (do we need to generalize "decide if this correction step is available"?)
-        self._constant_to_correction_name = {
-            constant: name
-            for (name, _, constants) in self._correction_steps
-            for constant in constants
-        }
+        self._constant_to_correction_names = {}
+        for (name, _, constants) in self._correction_steps:
+            for constant in constants:
+                self._constant_to_correction_names.setdefault(constant, set()).add(name)
         self._buffer_lock = threading.Lock()
         self._last_processing_started = 0  # used for processing time and timeout
 
diff --git a/src/calng/corrections/AgipdCorrection.py b/src/calng/corrections/AgipdCorrection.py
index 1eaf7aa9fa0336626baacf4ec9be5c894b175a2f..71cf1f2e0e12dd8c654e3bcc2f7d635b5f824eac 100644
--- a/src/calng/corrections/AgipdCorrection.py
+++ b/src/calng/corrections/AgipdCorrection.py
@@ -34,6 +34,7 @@ class AgipdGainMode(enum.IntEnum):
     FIXED_LOW_GAIN = 3
 
 
+# note: if this is extended further, bump up the dtype passed to kernel
 class CorrectionFlags(enum.IntFlag):
     NONE = 0
     THRESHOLD = 1
@@ -42,6 +43,8 @@ class CorrectionFlags(enum.IntFlag):
     REL_GAIN_PC = 8
     GAIN_XRAY = 16
     BPMASK = 32
+    FORCE_MG_IF_BELOW = 64
+    FORCE_HG_IF_BELOW = 128
 
 
 class AgipdGpuRunner(base_kernel_runner.BaseGpuRunner):
@@ -59,6 +62,8 @@ class AgipdGpuRunner(base_kernel_runner.BaseGpuRunner):
         bad_pixel_mask_value=np.nan,
         gain_mode=AgipdGainMode.ADAPTIVE_GAIN,
         g_gain_value=1,
+        mg_hard_threshold=2000,
+        hg_hard_threshold=2000,
     ):
         global cupy
         import cupy
@@ -95,6 +100,8 @@ class AgipdGpuRunner(base_kernel_runner.BaseGpuRunner):
         self.rel_gain_xray_map_gpu = cupy.empty(self.map_shape, dtype=np.float32)
         self.bad_pixel_map_gpu = cupy.empty(self.gm_map_shape, dtype=np.uint32)
         self.set_bad_pixel_mask_value(bad_pixel_mask_value)
+        self.set_mg_hard_threshold(mg_hard_threshold)
+        self.set_hg_hard_threshold(hg_hard_threshold)
         self.set_g_gain_value(g_gain_value)
         self.flush_buffers(set(AgipdConstants))
 
@@ -231,6 +238,12 @@ class AgipdGpuRunner(base_kernel_runner.BaseGpuRunner):
     def set_bad_pixel_mask_value(self, mask_value):
         self.bad_pixel_mask_value = cupy.float32(mask_value)
 
+    def set_mg_hard_threshold(self, value):
+        self.mg_hard_threshold = cupy.float32(value)
+
+    def set_hg_hard_threshold(self, value):
+        self.hg_hard_threshold = cupy.float32(value)
+
     def flush_buffers(self, constants):
         if AgipdConstants.Offset in constants:
             self.offset_map_gpu.fill(0)
@@ -267,6 +280,8 @@ class AgipdGpuRunner(base_kernel_runner.BaseGpuRunner):
                 cupy.uint8(flags),
                 self.default_gain,
                 self.gain_thresholds_gpu,
+                self.mg_hard_threshold,
+                self.hg_hard_threshold,
                 self.offset_map_gpu,
                 self.rel_gain_pc_map_gpu,
                 self.md_additional_offset_gpu,
@@ -418,6 +433,16 @@ class AgipdCorrection(base_correction.BaseCorrection):
         # step name (used in schema), flag to enable for kernel, constants required
         ("thresholding", CorrectionFlags.THRESHOLD, {AgipdConstants.ThresholdsDark}),
         ("offset", CorrectionFlags.OFFSET, {AgipdConstants.Offset}),
+        (  # TODO: specify that /both/ constants are needed, not just one or the other
+            "forceMgIfBelow",
+            CorrectionFlags.FORCE_MG_IF_BELOW,
+            {AgipdConstants.ThresholdsDark, AgipdConstants.Offset},
+        ),
+        (
+            "forceHgIfBelow",
+            CorrectionFlags.FORCE_HG_IF_BELOW,
+            {AgipdConstants.ThresholdsDark, AgipdConstants.Offset},
+        ),
         ("relGainPc", CorrectionFlags.REL_GAIN_PC, {AgipdConstants.SlopesPC}),
         ("gainXray", CorrectionFlags.GAIN_XRAY, {AgipdConstants.SlopesFF}),
         (
@@ -478,8 +503,42 @@ class AgipdCorrection(base_correction.BaseCorrection):
             expected, AgipdCorrection._managed_keys
         )
 
+        # turn off the force MG / HG steps by default
+        for step in ("forceMgIfBelow", "forceHgIfBelow"):
+            for flag in ("enable", "preview"):
+                (
+                    OVERWRITE_ELEMENT(expected)
+                    .key(f"corrections.{step}.{flag}")
+                    .setNewDefaultValue(False)
+                    .commit(),
+                )
         # additional settings specific to AGIPD correction steps
         (
+            FLOAT_ELEMENT(expected)
+            .key("corrections.forceMgIfBelow.hardThreshold")
+            .description(
+                "If enabled, any pixels assigned to low gain stage which would be "
+                "below this threshold after having medium gain offset subtracted will "
+                "be reassigned to medium gain."
+            )
+            .assignmentOptional()
+            .defaultValue(1000)
+            .reconfigurable()
+            .commit(),
+
+            FLOAT_ELEMENT(expected)
+            .key("corrections.forceHgIfBelow.hardThreshold")
+            .description(
+                "Like forceMgIfBelow, but potentially reassigning from medium gain to "
+                "high gain based on threshold and pixel value minus low gain offset. "
+                "Applied after forceMgIfBelow, pixels can theoretically be reassigned "
+                "twice, from LG to MG and to HG."
+            )
+            .assignmentOptional()
+            .defaultValue(1000)
+            .reconfigurable()
+            .commit(),
+
             BOOL_ELEMENT(expected)
             .key("corrections.relGainPc.overrideMdAdditionalOffset")
             .displayedName("Override md_additional_offset")
@@ -523,11 +582,13 @@ class AgipdCorrection(base_correction.BaseCorrection):
             .reconfigurable()
             .commit(),
         )
-        AgipdCorrection._managed_keys.add(
-            "corrections.relGainPc.overrideMdAdditionalOffset"
-        )
-        AgipdCorrection._managed_keys.add("corrections.relGainPc.mdAdditionalOffset")
-        AgipdCorrection._managed_keys.add("corrections.gainXray.gGainValue")
+        AgipdCorrection._managed_keys |= {
+            "corrections.forceMgIfBelow.hardThreshold",
+            "corrections.forceHgIfBelow.hardThreshold",
+            "corrections.relGainPc.overrideMdAdditionalOffset",
+            "corrections.relGainPc.mdAdditionalOffset",
+            "corrections.gainXray.gGainValue",
+        }
 
         # mandatory: manager needs this in schema
         (
@@ -569,6 +630,12 @@ class AgipdCorrection(base_correction.BaseCorrection):
             "gain_mode": self.gain_mode,
             "bad_pixel_mask_value": self.bad_pixel_mask_value,
             "g_gain_value": self.unsafe_get("corrections.gainXray.gGainValue"),
+            "mg_hard_threshold": self.unsafe_get(
+                "corrections.forceMgIfBelow.hardThreshold"
+            ),
+            "hg_hard_threshold": self.unsafe_get(
+                "corrections.forceHgIfBelow.hardThreshold"
+            ),
         }
 
     @property
@@ -717,6 +784,16 @@ class AgipdCorrection(base_correction.BaseCorrection):
             self.flush_constants()
             self._update_buffers()
 
+        if update.has("corrections.forceMgIfBelow.hardThreshold"):
+            self.kernel_runner.set_mg_hard_threshold(
+                self.get("corrections.forceMgIfBelow.hardThreshold")
+            )
+
+        if update.has("corrections.forceHgIfBelow.hardThreshold"):
+            self.kernel_runner.set_hg_hard_threshold(
+                self.get("corrections.forceHgIfBelow.hardThreshold")
+            )
+
         if update.has("corrections.gainXray.gGainValue"):
             self.kernel_runner.set_g_gain_value(
                 self.get("corrections.gainXray.gGainValue")
diff --git a/src/calng/kernels/agipd_gpu.cu b/src/calng/kernels/agipd_gpu.cu
index 1e8cf672614fe108970530cc46fae419049a01ff..fed8a5c69a5417486eff07d38884097011993b5a 100644
--- a/src/calng/kernels/agipd_gpu.cu
+++ b/src/calng/kernels/agipd_gpu.cu
@@ -16,6 +16,8 @@ extern "C" {
 	                        // default_gain can be 0, 1, or 2, and is relevant for fixed gain mode (no THRESHOLD)
 	                        const unsigned char default_gain,
 	                        const float* threshold_map,
+	                        const float mg_hard_threshold,
+	                        const float hg_hard_threshold,
 	                        const float* offset_map,
 	                        const float* rel_gain_pc_map,
 	                        const float* md_additional_offset,
@@ -99,16 +101,25 @@ extern "C" {
 					gain = 2;
 				}
 			}
+
+			const size_t gm_map_index_without_gain = map_cell * gm_map_stride_cell +
+				y * gm_map_stride_y +
+				x * gm_map_stride_x;
+			if ((corr_flags & FORCE_MG_IF_BELOW) && (gain == 2) &&
+			    (res - offset_map[gm_map_index_without_gain + 1 * gm_map_stride_gain] < mg_hard_threshold)) {
+				gain = 1;
+			}
+			if ((corr_flags & FORCE_HG_IF_BELOW) && (gain == 1) &&
+			    (res - offset_map[gm_map_index_without_gain] < hg_hard_threshold)) {
+				gain = 0;
+			}
 			gain_map[output_index] = (float)gain;
 
 			const size_t map_index = map_cell * map_stride_cell +
 				y * map_stride_y +
 				x * map_stride_x;
 
-			const size_t gm_map_index = gain * gm_map_stride_gain +
-				map_cell * gm_map_stride_cell +
-				y * gm_map_stride_y +
-				x * gm_map_stride_x;
+			const size_t gm_map_index = gm_map_index_without_gain + gain * gm_map_stride_gain;
 
 			if ((corr_flags & BPMASK) && bad_pixel_map[gm_map_index]) {
 				res = bad_pixel_mask_value;