From a521827a8d02afdc12374e36bdb58863fc294581 Mon Sep 17 00:00:00 2001
From: Philipp Schmidt <philipp.schmidt@xfel.eu>
Date: Mon, 11 Apr 2022 14:06:35 +0200
Subject: [PATCH] Add option to round intensity to absolute photon numbers in
 AGIPD correct

---
 .../AGIPD/AGIPD_Correct_and_Verify.ipynb      | 41 +++++++++++++++++--
 src/cal_tools/agipdlib.py                     | 31 +++++++++++++-
 2 files changed, 68 insertions(+), 4 deletions(-)

diff --git a/notebooks/AGIPD/AGIPD_Correct_and_Verify.ipynb b/notebooks/AGIPD/AGIPD_Correct_and_Verify.ipynb
index 678a2b663..4684bdadf 100644
--- a/notebooks/AGIPD/AGIPD_Correct_and_Verify.ipynb
+++ b/notebooks/AGIPD/AGIPD_Correct_and_Verify.ipynb
@@ -40,9 +40,6 @@
     "cal_db_timeout = 30000 # in milliseconds\n",
     "creation_date_offset = \"00:00:00\" # add an offset to creation date, e.g. to get different constants\n",
     "\n",
-    "use_ppu_device = ''  # Device ID for a pulse picker device to only process picked trains, empty string to disable\n",
-    "ppu_train_offset = 0  # When using the pulse picker, offset between the PPU's sequence start and actually picked train\n",
-    "\n",
     "mem_cells = 0  # Number of memory cells used, set to 0 to automatically infer\n",
     "bias_voltage = 0  # bias voltage, set to 0 to use stored value in slow data.\n",
     "acq_rate = 0. # the detector acquisition rate, use 0 to try to auto-determine\n",
@@ -84,10 +81,17 @@
     "melt_snow = False # Identify (and optionally interpolate) 'snowy' pixels\n",
     "mask_zero_std = False # Mask pixels with zero standard deviation across train\n",
     "low_medium_gap = False # 5 sigma separation in thresholding between low and medium gain\n",
+    "round_photons = False  # Round to absolute number of photons, only use with gain corrections\n",
+    "\n",
+    "# Optional auxiliary devices\n",
+    "use_ppu_device = ''  # Device ID for a pulse picker device to only process picked trains, empty string to disable\n",
+    "ppu_train_offset = 0  # When using the pulse picker, offset between the PPU's sequence start and actually picked train\n",
     "\n",
     "use_litframe_device = '' # Device ID for a lit frame finder device to only process illuminated frames, empty string to disable\n",
     "energy_threshold = -1000 # The low limit for the energy (uJ) exposed by frames subject to processing. If -1000, selection by pulse energy is disabled\n",
     "\n",
+    "use_xgm_device = 'SA2_XTD1_XGM/XGM/DOOCS'  # DoocsXGM device ID to obtain actual photon energy, operating condition else.\n",
+    "\n",
     "# Output parameters\n",
     "recast_image_data = ''  # Cast data to a different dtype before saving\n",
     "compress_fields = ['gain', 'mask']  # Datasets in image group to compress.\n",
@@ -218,6 +222,7 @@
     "    corr_bools[\"melt_snow\"] = melt_snow\n",
     "    corr_bools[\"mask_zero_std\"] = mask_zero_std\n",
     "    corr_bools[\"low_medium_gap\"] = low_medium_gap\n",
+    "    corr_bools[\"round_photons\"] = round_photons\n",
     "\n",
     "# Many corrections don't apply to fixed gain mode; will explicitly disable later if detected\n",
     "disable_for_fixed_gain = [\n",
@@ -462,6 +467,35 @@
     "print(cell_sel.msg())"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "actual_photon_energy = None\n",
+    "\n",
+    "if use_xgm_device:\n",
+    "    # Try to obtain photon energy from XGM device.\n",
+    "    wavelength_data = dc[use_xgm_device, 'pulseEnergy.wavelengthUsed']\n",
+    "    \n",
+    "    try:\n",
+    "        from scipy.constants import h, c, e\n",
+    "        \n",
+    "        # Read wavelength as a single value and convert to hv.\n",
+    "        actual_photon_energy = (h * c / e) / (wavelength_data.as_single_value(rtol=1e-2) * 1e-6)\n",
+    "        print(f'Obtained actual photon energy {actual_photon_energy:.3f} keV from {use_xgm_device}')\n",
+    "    except ValueError:\n",
+    "        if round_photons:\n",
+    "            print('WARNING: XGM source available but actual photon energy varies greater than 1%, '\n",
+    "                  'photon rounding disabled!')\n",
+    "            round_photons = False\n",
+    "\n",
+    "if actual_photon_energy is None and round_photons:\n",
+    "    print('WARNING: Using operating condition for actual photon energy in photon rounding mode, this is NOT reliable!')\n",
+    "    actual_photon_energy = photon_energy"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},
@@ -496,6 +530,7 @@
     "agipd_corr.cm_n_itr = cm_n_itr\n",
     "agipd_corr.noisy_adc_threshold = noisy_adc_threshold\n",
     "agipd_corr.ff_gain = ff_gain\n",
+    "agipd_corr.actual_photon_energy = actual_photon_energy\n",
     "\n",
     "agipd_corr.compress_fields = compress_fields\n",
     "if recast_image_data:\n",
diff --git a/src/cal_tools/agipdlib.py b/src/cal_tools/agipdlib.py
index 69f4d4aab..c86cd2d25 100644
--- a/src/cal_tools/agipdlib.py
+++ b/src/cal_tools/agipdlib.py
@@ -343,6 +343,7 @@ class AgipdCorrections:
         self.hg_hard_threshold = 100
         self.noisy_adc_threshold = 0.25
         self.ff_gain = 1
+        self.actual_photon_energy = 9.2
 
         # Output parameters
         self.compress_fields = ['gain', 'mask']
@@ -367,7 +368,7 @@ class AgipdCorrections:
                           'zero_orange', 'force_hg_if_below',
                           'force_mg_if_below', 'mask_noisy_adc',
                           'melt_snow', 'common_mode', 'mask_zero_std',
-                          'low_medium_gap']
+                          'low_medium_gap', 'round_photons']
 
         if set(corr_bools).issubset(tot_corr_bools):
             self.corr_bools = corr_bools
@@ -851,6 +852,34 @@ class AgipdCorrections:
             msk[bidx] |= BadPixels.VALUE_OUT_OF_RANGE
             del bidx
 
+        # Round keV-normalized intensity to photons.
+        if self.corr_bools.get("round_photons"):
+            data /= self.actual_photon_energy
+            np.round(data, out=data)
+
+            # This could also be done before and its mask inverted for
+            # rounding, but the performance difference is negligible.
+            bidx = data < 0
+            data[bidx] = 0
+            msk[bidx] |= BadPixels.VALUE_OUT_OF_RANGE
+            del bidx
+
+        if np.issubdtype(self.recast_image_fields.get('image'), np.integer):
+            # If the image data is meant to be recast to an integer
+            # type, make sure its values are within its bounds.
+
+            type_info = np.iinfo(self.recast_image_data['image'])
+
+            bidx = data < type_info.min
+            data[bidx] = 0
+            msk[bidx] |= BadPixels.VALUE_OUT_OF_RANGE
+            del bidx
+
+            bidx = data > type_info.max
+            data[bidx] = type_info.max
+            msk[bidx] |= BadPixels.VALUE_OUT_OF_RANGE
+            del bidx
+
         # Mask entire ADC if they are noise above a threshold
         # TODO: Needs clarification if needed,
         # the returned arg is not used.
-- 
GitLab