diff --git a/notebooks/LPD/LPD_Correct_Fast.ipynb b/notebooks/LPD/LPD_Correct_Fast.ipynb index 04d35400aafeaa9197d79ff6a11439d697c84df0..539ce31b7c14bd5657275d2856bb99d5078a8a21 100644 --- a/notebooks/LPD/LPD_Correct_Fast.ipynb +++ b/notebooks/LPD/LPD_Correct_Fast.ipynb @@ -33,6 +33,7 @@ "karabo_id = 'FXE_DET_LPD1M-1' # Karabo domain for detector.\n", "input_source = '{karabo_id}/DET/{module_index}CH0:xtdf' # Input fast data source.\n", "output_source = '{karabo_id}/CORR/{module_index}CH0:output' # Output fast data source, empty to use same as input.\n", + "control_source = '{karabo_id}/COMP/FEM_MDL_COMP' # Control data source.\n", "xgm_source = 'SA1_XTD2_XGM/DOOCS/MAIN'\n", "xgm_pulse_count_key = 'pulseEnergy.numberOfSa1BunchesActual'\n", "\n", @@ -55,6 +56,9 @@ "rel_gain = True # Gain correction based on RelativeGain constant.\n", "ff_map = True # Gain correction based on FFMap constant.\n", "gain_amp_map = True # Gain correction based on GainAmpMap constant.\n", + "combine_parallel_gain = True # Combine parallel gain images into a single frame.\n", + "threshold_sigma_high = 5.0 # Sigma level for threshold between high and medium gain.\n", + "threshold_sigma_mid = 100.0 # Sigma level for threshold between medium and low gain.\n", "\n", "# Output options\n", "ignore_no_frames_no_pulses = False # Whether to run without SA1 pulses AND frames.\n", @@ -244,13 +248,24 @@ "source": [ "start = perf_counter()\n", "\n", + "raw_data = xd.RunDirectory(run_folder)\n", + "\n", + "try:\n", + " parallel_gain = bool(raw_data[control_source.format(karabo_id=karabo_id)].run_value('femAsicGainOverride'))\n", + "except KeyError:\n", + " warning('Missing femAsicGainOverride property FEM control device, assuming auto gain')\n", + " parallel_gain = False\n", + "print('Parallel gain mode:', parallel_gain)\n", + "\n", "cell_ids_pattern_s = None\n", "if use_cell_order != 'never':\n", + " mem_cell_pattern = get_mem_cell_pattern(raw_data, det_inp_sources)\n", + " \n", + " if parallel_gain:\n", + " mem_cell_pattern = mem_cell_pattern[:len(mem_cell_pattern) // 3]\n", + " \n", " # Read the order of memory cells used\n", - " raw_data = xd.DataCollection.from_paths([e[1] for e in data_to_process])\n", - " cell_ids_pattern_s = make_cell_order_condition(\n", - " use_cell_order, get_mem_cell_pattern(raw_data, det_inp_sources)\n", - " )\n", + " cell_ids_pattern_s = make_cell_order_condition(use_cell_order, mem_cell_pattern)\n", "print(\"Memory cells order:\", cell_ids_pattern_s)\n", "\n", "conditions = LPDConditions(\n", @@ -259,6 +274,7 @@ " feedback_capacitor=capacitor,\n", " source_energy=photon_energy,\n", " memory_cell_order=cell_ids_pattern_s,\n", + " parallel_gain=parallel_gain,\n", " category=category,\n", ")\n", "\n", @@ -269,6 +285,8 @@ " expected_constants.update(['FFMap', 'BadPixelsFF'])\n", "if gain_amp_map:\n", " expected_constants.add('GainAmpMap')\n", + "if parallel_gain:\n", + " expected_constants.add('Noise')\n", "\n", "lpd_consts = CalibrationData.from_condition(\n", " conditions,\n", @@ -345,6 +363,7 @@ "source": [ "# These are intended in order cell, X, Y, gain\n", "ccv_offsets = {}\n", + "ccv_noise = {}\n", "ccv_gains = {}\n", "ccv_masks = {}\n", "\n", @@ -352,6 +371,7 @@ "\n", "constant_order = {\n", " 'Offset': (2, 1, 0, 3),\n", + " 'Noise': (2, 1, 0, 3),\n", " 'BadPixelsDark': (2, 1, 0, 3),\n", " 'RelativeGain': (2, 0, 1, 3),\n", " 'FFMap': (2, 0, 1, 3),\n", @@ -378,6 +398,11 @@ " ccv_offsets[aggregator] = np.zeros(ccv_shape, dtype=np.float32)\n", " \n", " ccv_gains[aggregator] = np.ones(ccv_shape, dtype=np.float32)\n", + "\n", + " if parallel_gain and 'Noise' in consts:\n", + " ccv_noise[aggregator] = _prepare_data('Noise', np.float32)\n", + " else:\n", + " ccv_noise[aggregator] = np.zeros(ccv_shape, dtype=np.float32)\n", " \n", " if 'BadPixelsDark' in consts:\n", " ccv_masks[aggregator] = _prepare_data('BadPixelsDark', np.uint32)\n", @@ -433,24 +458,67 @@ " in_raw = inp_source['image.data'].ndarray().reshape(-1, 256, 256)\n", " in_cell = inp_source['image.cellId'].ndarray().reshape(-1)\n", " in_pulse = inp_source['image.pulseId'].ndarray().reshape(-1)\n", + " frame_counts = inp_source['image.data'].data_counts(labelled=False).astype(np.int32)\n", " read_time = perf_counter() - start\n", + "\n", + " parallel_gain_indices = None\n", + " \n", + " if parallel_gain:\n", + " actual_frame_counts = frame_counts // 3\n", + "\n", + " # Indices map where to find each of the high/medium/low gain images for each actual\n", + " # frame event.\n", + " parallel_gain_indices = np.zeros((actual_frame_counts.sum(), 3), dtype=np.int32)\n", + "\n", + " # Build indices for high gain as a range in each train, running from the cumulative sum\n", + " # of apparent frames from all trains before to the actual number of frames in this train.\n", + " np.concatenate(\n", + " [np.arange(actual_frame_counts[0])] +\n", + " [np.arange(offset, offset+actual_count) for offset, actual_count\n", + " in zip(np.cumsum(frame_counts), actual_frame_counts[1:])],\n", + " out=parallel_gain_indices[:, 0])\n", + "\n", + " # The delta between the gain stages is the number of actual frames.\n", + " gain_index_deltas = np.repeat(actual_frame_counts, actual_frame_counts)\n", + "\n", + " # Build indices for medium gain and high gain by adding the gain index deltas in between\n", + " # each of them.\n", + " np.add(parallel_gain_indices[:, 0], gain_index_deltas, out=parallel_gain_indices[:, 1])\n", + " np.add(parallel_gain_indices[:, 1], gain_index_deltas, out=parallel_gain_indices[:, 2])\n", + "\n", + " # Pick cell and pulse IDs from high gain. This is also done if frames are not combined\n", + " # in order to correct corrupt tables in medium and low gain, and if needed brought back\n", + " # to the original shape further below.\n", + " in_cell = np.take(in_cell, parallel_gain_indices[:, 0])\n", + " in_pulse = np.take(in_pulse, parallel_gain_indices[:, 0])\n", + "\n", + " if combine_parallel_gain:\n", + " # Replace supposed frame counts by actual frame counts.\n", + " frame_counts = actual_frame_counts\n", + " else:\n", + " # Replicate corrected cell and pulse IDs from high gain to other gains.\n", + " in_cell = np.repeat(in_cell, actual_frame_counts)\n", + " in_pulse = np.repeat(in_pulse, actual_frame_counts)\n", + " \n", + " # Disable gain indices to not combine.\n", + " parallel_gain_indices = None\n", " \n", " # Allocate output arrays.\n", - " out_data = np.zeros((in_raw.shape[0], 256, 256), dtype=np.float32)\n", - " out_gain = np.zeros((in_raw.shape[0], 256, 256), dtype=np.uint8)\n", - " out_mask = np.zeros((in_raw.shape[0], 256, 256), dtype=np.uint32)\n", + " num_frames = frame_counts.sum()\n", + " out_data = np.zeros((num_frames, 256, 256), dtype=np.float32)\n", + " out_gain = np.zeros((num_frames, 256, 256), dtype=np.uint8)\n", + " out_mask = np.zeros((num_frames, 256, 256), dtype=np.uint32)\n", " \n", " start = perf_counter()\n", " correct_lpd_frames(in_raw, in_cell,\n", " out_data, out_gain, out_mask,\n", - " ccv_offsets[aggregator], ccv_gains[aggregator], ccv_masks[aggregator],\n", - " num_threads=num_threads_per_worker)\n", + " ccv_offsets[aggregator], ccv_noise[aggregator], ccv_gains[aggregator], ccv_masks[aggregator],\n", + " parallel_gain_indices, threshold_sigma_high, threshold_sigma_mid,\n", + " num_threads=16)\n", " correct_time = perf_counter() - start\n", " \n", - " image_counts = inp_source['image.data'].data_counts(labelled=False)\n", - " \n", " start = perf_counter()\n", - " if (not outp_path.exists() or overwrite) and image_counts.sum() > 0:\n", + " if (not outp_path.exists() or overwrite) and num_frames > 0:\n", " outp_source_name = output_source.format(karabo_id=karabo_id, module_index=module_index)\n", "\n", " with DataFile(outp_path, 'w') as outp_file: \n", @@ -461,7 +529,7 @@ " \n", " outp_source = outp_file.create_instrument_source(outp_source_name)\n", " \n", - " outp_source.create_index(image=image_counts)\n", + " outp_source.create_index(image=frame_counts)\n", " outp_source.create_key('image.cellId', data=in_cell,\n", " chunks=(min(chunks_ids, in_cell.shape[0]),))\n", " outp_source.create_key('image.pulseId', data=in_pulse,\n", @@ -477,11 +545,11 @@ " write_time = perf_counter() - start\n", " \n", " total_time = open_time + read_time + correct_time + write_time\n", - " frame_rate = in_raw.shape[0] / total_time\n", + " frame_rate = num_frames / total_time\n", " \n", " print('{}\\t{}\\t{:.3f}\\t{:.3f}\\t{:.3f}\\t{:.3f}\\t{:.3f}\\t{}\\t{:.1f}'.format(\n", " wid, aggregator, open_time, read_time, correct_time, write_time, total_time,\n", - " in_raw.shape[0], frame_rate))\n", + " num_frames, frame_rate))\n", " \n", " in_raw = None\n", " in_cell = None\n", diff --git a/src/cal_tools/lpdalgs.pyx b/src/cal_tools/lpdalgs.pyx index 66b8b097c71a4a7cb6b1681e4678e0f04c2f43e7..6c746172de4cfc109d20c0bf3e3c780ee35e9d3d 100644 --- a/src/cal_tools/lpdalgs.pyx +++ b/src/cal_tools/lpdalgs.pyx @@ -1,13 +1,14 @@ + from cython cimport boundscheck, wraparound, cdivision from cython.view cimport contiguous from cython.parallel cimport prange from cal_tools.enums import BadPixels -ctypedef unsigned short cell_t ctypedef unsigned short raw_t ctypedef float data_t ctypedef unsigned char gain_t +ctypedef unsigned short cell_t ctypedef unsigned int mask_t cdef mask_t WRONG_GAIN_VALUE = BadPixels.WRONG_GAIN_VALUE, \ @@ -17,7 +18,6 @@ cdef mask_t WRONG_GAIN_VALUE = BadPixels.WRONG_GAIN_VALUE, \ @boundscheck(False) @wraparound(False) -@cdivision(True) def correct_lpd_frames( # (frame, x, y) raw_t[:, :, ::contiguous] in_raw, @@ -29,10 +29,17 @@ def correct_lpd_frames( mask_t[:, :, ::contiguous] out_mask, # (cell, x, y, gain) - float[:, :, :, ::contiguous] ccv_offset, - float[:, :, :, ::contiguous] ccv_gain, + data_t[:, :, :, ::contiguous] ccv_offset, + data_t[:, :, :, ::contiguous] ccv_noise, + data_t[:, :, :, ::contiguous] ccv_gain, mask_t[:, :, :, ::contiguous] ccv_mask, + # (frame, gain) + int[:, ::contiguous] parallel_gain_indices, + + data_t threshold_sigma_high, + data_t threshold_sigma_mid, + int num_threads=1, ): cdef int frame, ss, fs @@ -41,25 +48,34 @@ def correct_lpd_frames( cdef gain_t gain cdef mask_t mask - for frame in prange(in_raw.shape[0], nogil=True, num_threads=num_threads): + cdef bint adaptive_gain = parallel_gain_indices is None + + for frame in prange(out_data.shape[0], nogil=True, num_threads=num_threads): cell = in_cell[frame] for ss in range(in_raw.shape[1]): for fs in range(in_raw.shape[2]): - # Decode intensity and gain from raw data. - data = <data_t>(in_raw[frame, ss, fs] & 0xFFF) - gain = <gain_t>((in_raw[frame, ss, fs] & 0x3000) >> 12) + if adaptive_gain: + # Decode intensity and gain from raw data. + data = <data_t>(in_raw[frame, ss, fs] & 0xFFF) + gain = <gain_t>((in_raw[frame, ss, fs] & 0x3000) >> 12) + + else: + _parallel_gain_thresholding( + in_raw, parallel_gain_indices, ccv_noise, + threshold_sigma_high, threshold_sigma_mid, + frame, cell, ss, fs, + &data, &gain) if gain <= 2: + data = data - ccv_offset[cell, ss, fs, gain] + data = data * ccv_gain[cell, ss, fs, gain] mask = ccv_mask[cell, ss, fs, gain] else: data = 0.0 gain = 0 mask = WRONG_GAIN_VALUE - data = data - ccv_offset[cell, ss, fs, gain] - data = data * ccv_gain[cell, ss, fs, gain] - if data > 1e7 or data < -1e7: data = 0.0 mask = mask | VALUE_OUT_OF_RANGE @@ -67,3 +83,58 @@ def correct_lpd_frames( out_data[frame, ss, fs] = data out_gain[frame, ss, fs] = gain out_mask[frame, ss, fs] = mask + + +@boundscheck(False) +@wraparound(False) +cdef inline void _parallel_gain_thresholding( + raw_t[:, :, ::contiguous] in_raw, + int[:, ::contiguous] parallel_gain_indices, + data_t[:, :, :, ::contiguous] ccv_noise, + data_t sigma_high, data_t sigma_mid, + int frame, int cell, int ss, int fs, + data_t* data_ptr, gain_t* gain_ptr +) noexcept nogil: + cdef int frame_high, frame_mid, frame_low + cdef data_t data_high, data_mid, data_low + cdef data_t threshold_high, threshold_mid + + # Obtain indices to this pixel in each of three gain images. + frame_high = parallel_gain_indices[frame, 0] + frame_mid = parallel_gain_indices[frame, 1] + frame_low = parallel_gain_indices[frame, 2] + + if ( + ((in_raw[frame_high, ss, fs] & 0x3000) >> 12) == 0 and + ((in_raw[frame_mid, ss, fs] & 0x3000) >> 12) == 1 and + ((in_raw[frame_low, ss, fs] & 0x3000) >> 12) == 2 + ): + # Verify that this pixel is recorded in the correct gain stage + # in each of the gain images. Memory cells in the transition + # regions between the gains can sometimes end up in the wrong + # one. + + # Decode intensity in every gain stage. + data_high = <data_t>(in_raw[frame_high, ss, fs] & 0xFFF) + data_mid = <data_t>(in_raw[frame_mid, ss, fs] & 0xFFF) + data_low = <data_t>(in_raw[frame_low, ss, fs] & 0xFFF) + + # Compute thresholds based on noise level. + threshold_high = 4096 - sigma_high * ccv_noise[cell, ss, fs, 0] + threshold_mid = 4096 - sigma_mid * ccv_noise[cell, ss, fs, 1] + + # Pick the optimal gain stage for this pixel. + if data_mid > threshold_mid: + data_ptr[0] = data_low + gain_ptr[0] = 2 + elif data_high > threshold_high: + data_ptr[0] = data_mid + gain_ptr[0] = 1 + else: + data_ptr[0] = data_high + gain_ptr[0] = 0 + + else: + # Using an invalid gain stage triggers bad pixel masking later + # in the correction kernel. + gain_ptr[0] = 4