From a387117a4ca8efc6319ffa68fff1bf302a3f847f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Le=20Guyader?= <loic.le.guyader@xfel.eu> Date: Sat, 19 Oct 2019 19:26:32 +0200 Subject: [PATCH] Azimuthal integration and plotting --- DSSC.py | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 216 insertions(+), 22 deletions(-) diff --git a/DSSC.py b/DSSC.py index f3b19ff..015e227 100644 --- a/DSSC.py +++ b/DSSC.py @@ -5,11 +5,13 @@ import shutil from tqdm.auto import tqdm import os import warnings +import psutil import karabo_data as kd from karabo_data.geometry2 import DSSC_1MGeometry import ToolBox as tb import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import ImageGrid import numpy as np import xarray as xr import h5py @@ -18,13 +20,14 @@ from imageio import imread class DSSC: - def __init__(self, semester, proposal, topic='SCS'): + def __init__(self, semester, proposal, topic='SCS', distance=1): """ Create a DSSC object to process DSSC data. inputs: semester: semester string proposal: proposal number string topic: topic, by default SCS + distance: distance sample to DSSC detector in meter """ self.semester = semester @@ -32,12 +35,19 @@ class DSSC: self.topic = topic self.tempdir = None self.save_folder = f'/gpfs/exfel/exp/{self.topic}/{self.semester}/{self.proposal}/usr/condensed_runs/' + self.distance = distance + self.px_pitch_h = 236 # horizontal pitch in microns + self.px_pitch_v = 204 # vertical pitch in microns + self.aspect = self.px_pitch_v/self.px_pitch_h # aspect ratio of the DSSC images + self.geom = None + self.mask = None print('DSSC configuration') print(f'Topic: {self.topic}') print(f'Semester: {self.semester}') print(f'Proposal: {self.proposal}') print(f'Default save folder: {self.save_folder}') + print(f'Sample to DSSC distance: {self.distance} m') if not os.path.exists(self.save_folder): warnings.warn(f'Default save folder does not exist: {self.save_folder}') @@ -63,7 +73,7 @@ class DSSC: self.run = kd.open_run(self.proposal, self.run_nr) self.isDark = isDark - self.plot_title = f'{self.semester} run: {self.run_nr}' + self.plot_title = f'{self.proposal} run: {self.run_nr}' self.fpt = self.run.detector_info('SCS_DET_DSSC1M-1/DET/0CH0:xtdf')['frames_per_train'] self.nbunches = self.run.get_array('SCS_RR_UTC/MDL/BUNCH_DECODER', 'sase3.nPulses.value') @@ -133,20 +143,27 @@ class DSSC: dims=['scan_variable'], coords={'scan_variable': self.scan['scan_variable'].values}, name='counts') + self.scan_points = self.scan.groupby('scan_variable').mean('trainId').coords['scan_variable'].values + self.scan_points_counts = self.scan_counts.groupby('scan_variable').sum() + self.plot_scan() - fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=[5, 5]) - ax1.plot(self.scan.groupby('scan_variable').mean('trainId').coords['scan_variable'].values, - self.scan_counts.groupby('scan_variable').sum(), - 'o-', ms=2) - ax1.set_xlabel('scan variable') + def plot_scan(self): + """ Plot a previously defined scan to see the scan range and the statistics. + """ + if self.scan: + fig, (ax1, ax2) = plt.subplots(nrows=2, figsize=[5, 5]) + else: + fig, ax1 = plt.subplots(nrows=1, figsize=[5, 2.5]) + + ax1.plot(self.scan_points, self.scan_points_counts, 'o-', ms=2) + ax1.set_xlabel(f'{self.scan_vname}') ax1.set_ylabel('# trains') ax1.set_title(self.plot_title) - ax2.plot(self.scan['scan_variable']) - ax2.set_xlabel('train #') - ax2.set_ylabel(f'{vname}') - - plt.tight_layout() + if self.scan: + ax2.plot(self.scan['scan_variable']) + ax2.set_xlabel('train #') + ax2.set_ylabel(f'{self.scan_vname}') def load_xgm(self): """ Loads pulse resolved dedicated SAS3 data from the SCS XGM. @@ -174,7 +191,6 @@ class DSSC: plt.xlabel(f"{tb.mnemonics['SCS_SA3']['source']}{tb.mnemonics['SCS_SA3']['key']}") plt.ylabel('density') plt.title(self.plot_title) - plt.tight_layout() def xgm_filter(self, xgm_low=-np.inf, xgm_high=np.inf): """ Filters the data by train. If one pulse within a train has an SASE3 SCS XGM value below @@ -213,14 +229,15 @@ class DSSC: ( 4.528, -4.912) # TL ] path = '/gpfs/exfel/sw/software/exfel_environments/misc/git/karabo_data/docs/dssc_geo_june19.h5' - geom = DSSC_1MGeometry.from_h5_file_and_quad_positions(path, quad_pos) - return geom + self.geom = DSSC_1MGeometry.from_h5_file_and_quad_positions(path, quad_pos) + return self.geom - def load_mask(self, fname): + def load_mask(self, fname, plot=True): """ Load a DSSC mask file. input: fname: string of the filename of the mask file + plot: if True, the loaded mask is plotted """ @@ -228,6 +245,10 @@ class DSSC: dssc_mask = dssc_mask.astype(float)[..., 0] // 255 dssc_mask[dssc_mask==0] = np.nan self.mask = dssc_mask + + if plot: + plt.figure() + plt.imshow(self.mask) def create_virtual_dssc_datasets(self, run, path=''): """ Create virtual datasets for each 16 DSSC modules used for the multiprocessing. @@ -247,8 +268,8 @@ class DSSC: vds_list.append([vds_filename, module_vds]) return vds_list - def crunch(self): - """ Crunch through the DSSC data using multiprocessing. + def binning(self): + """ Bin the DSSC data by the predifined scan type (DSSC.define()) using multiprocessing """ if self.vds_scan is None: @@ -260,9 +281,12 @@ class DSSC: self.scan = self.scan.to_dataset(name='scan_variable') self.scan.to_netcdf(self.vds_scan, group='data') - max_GB = 400 + # get available memory in GB, we will try to use 80 % of it + max_GB = psutil.virtual_memory().available/1024**3 + print(f'max available memory: {max_GB} GB') + # max_GB / (8byte * 16modules * 128px * 512px * N_pulses) - self.chunksize = int(max_GB * 1024**3 // (8 * 16 * 128 * 512 * self.fpt)) + self.chunksize = int(0.8*max_GB * 1024**3 // (8 * 16 * 128 * 512 * self.fpt)) print('processing', self.chunksize, 'trains per chunk') @@ -293,6 +317,8 @@ class DSSC: self.module_data = xr.merge([self.module_data, self.scan.groupby('scan_variable').mean('trainId')]) self.module_data = self.module_data.squeeze() + + self.module_data.attrs['scan_variable'] = self.scan_vname def save(self, save_folder=None, overwrite=False): """ Save the crunched data. @@ -305,9 +331,9 @@ class DSSC: save_folder = this.save_folder if self.isDark: - fname = f'run{self.run_nr}_el.h5' # no scan + fname = f'run{self.run_nr}_dark.h5' # no scan else: - fname = f'run{self.run_nr}_by-delay_el.h5' # run with delay scan (change for other scan types!) + fname = f'run{self.run_nr}.h5' # run with delay scan (change for other scan types!) save_path = os.path.join(save_folder, fname) @@ -322,6 +348,174 @@ class DSSC: print('saving: ', save_path) else: print('file', save_path, 'exists and overwrite is False') + + def load_binned(self, runNB, dark_runNB, xgm_norm = True, save_folder=None): + """ load previously binned (crunched) DSSC data by DSSC.crunch() and DSSC.save() + + inputs: + runNB: run number to load + dark_runNB: run number of the corresponding dark + xgm_norm: normlize by XGM data if True + save_folder: path string where the crunched data are saved + """ + + if save_folder is None: + save_folder = self.save_folder + + self.plot_title = f'{self.proposal} run: {runNB} dark: {dark_runNB}' + + dark = xr.open_dataset(os.path.join(save_folder, f'run{dark_runNB}_dark.h5'), group='data') + binned = xr.open_dataset(os.path.join(save_folder, f'run{runNB}.h5'), group='data') + + binned['pumped'] = (binned['pumped'] - dark['pumped'].values) + binned['unpumped'] = (binned['unpumped'] - dark['unpumped'].values) + + if xgm_norm: + binned['pumped'] = binned['pumped'] / binned['xgm_pumped'] + binned['unpumped'] = binned['unpumped'] / binned['xgm_unpumped'] + + self.scan_points = binned['scan_variable'] + self.scan_points_counts = binned['sum_count'][:, 0] + self.scan_vname = binned.attrs['scan_variable'] + self.scan = None + + self.binned = binned + + def plot_DSSC(self, use_mask = True, p_low = 1, p_high = 98, vmin = None, vmax = None): + """ Plot pumped and unpumped DSSC images. + + inputs: + use_mask: if True, a mask is applied on the DSSC. + p_low: low percentile value to adjust the contrast scale on the unpumped and pumped image + p_high: high percentile value to adjust the contrast scale on the unpumped and pumped image + vmin: low value of the image scale + vmax: high value of the image scale + """ + + if use_mask: + if self.mask is None: + raise ValueError('No mask was loaded !') + + mask = self.mask + mask_txt = ' masked' + else: + mask = 1 + mask_txt = '' + + if self.geom is None: + self.load_geom() + + im_pump_mean, _ = self.geom.position_modules_fast(self.binned['pumped'].mean('scan_variable')) + im_unpump_mean, _ = self.geom.position_modules_fast(self.binned['unpumped'].mean('scan_variable')) + + im_pump_mean = mask*im_pump_mean + im_unpump_mean = mask*im_unpump_mean + + fig = plt.figure(figsize=(9, 4)) + grid = ImageGrid(fig, 111, + nrows_ncols=(1,2), + axes_pad=0.15, + share_all=True, + cbar_location="right", + cbar_mode="single", + cbar_size="7%", + cbar_pad=0.15, + ) + + _vmin, _vmax = np.percentile(im_pump_mean[~np.isnan(im_pump_mean)], [p_low, p_high]) + if vmin is None: + vmin = _vmin + if vmax is None: + vmax = _vmax + + im = grid[0].imshow(im_pump_mean, vmin=vmin, vmax=vmax, aspect=self.aspect) + grid[0].set_title('pumped' + mask_txt) + + im = grid[1].imshow(im_unpump_mean, vmin=vmin, vmax=vmax, aspect=self.aspect) + grid[1].set_title('unpumped' + mask_txt) + + grid[-1].cax.colorbar(im) + grid[-1].cax.toggle_label(True) + + fig.suptitle(self.plot_title) + + + def azimuthal_int(self, wl, center=None, angle_range=[0, 360], dr=1, use_mask=True): + """ Perform azimuthal integration of 1D binned DSSC run. + + inputs: + wl: photon wavelength + center: center of integration + angle_range: angles of integration + dr: dr + use_mask: if True, use the loaded mask + """ + + if self.geom is None: + self.load_geom() + + if use_mask: + if self.mask is None: + raise ValueError('No mask was loaded !') + + mask = self.mask + mask_txt = ' masked' + else: + mask = 1 + mask_txt = '' + + im_pumped_arranged, c_geom = self.geom.position_modules_fast(self.binned['pumped'].values) + im_unpumped_arranged, c_geom = self.geom.position_modules_fast(self.binned['unpumped'].values) + + im_pumped_arranged *= mask + im_unpumped_arranged *= mask + + im_pumped_mean = im_pumped_arranged.mean(axis=0) + im_unpumped_mean = im_unpumped_arranged.mean(axis=0) + + if center is None: + center = c_geom + + ai = tb.azimuthal_integrator(im_pumped_mean.shape, center, angle_range, dr=dr) + norm = ai(~np.isnan(im_pumped_mean)) + + az_pump = [] + az_unpump = [] + + for i in tqdm(range(len(self.binned['scan_variable']))): + az_pump.append(ai(im_pumped_arranged[i]) / norm) + az_unpump.append(ai(im_unpumped_arranged[i]) / norm) + + az_pump = np.stack(az_pump) + az_unpump = np.stack(az_unpump) + + coords = {'scan_variable': self.binned['scan_variable'], 'distance': ai.distance} + azimuthal = xr.DataArray(az_pump, dims=['scan_variable', 'distance'], coords=coords) + azimuthal = azimuthal.to_dataset(name='pumped') + azimuthal['unpumped'] = xr.DataArray(az_unpump, dims=['scan_variable', 'distance'], coords=coords) + azimuthal = azimuthal.transpose('distance', 'scan_variable') + + #t0 = 225.5 + #azimuthal['delay'] = (t0 - azimuthal.delay)*6.6 + #azimuthal['delay'] = azimuthal.delay + + azimuthal['delta_q (1/nm)'] = 2e-9 * np.pi * np.sin( + np.arctan(azimuthal.distance * self.px_pitch_v*1e-6 / self.distance)) / wl + + self.azimuthal = azimuthal.swap_dims({'distance': 'delta_q (1/nm)'}) + + def plot_azimuthal_int(self): + """ Plot a computed azimuthal integration. + """ + fig, [ax1, ax2] = plt.subplots(nrows=2, sharex=True, sharey=True) + + xr.plot.imshow(self.azimuthal.pumped, ax=ax1, robust=True) + ax1.set_title('unpumped') + xr.plot.imshow(self.azimuthal.pumped - self.azimuthal.unpumped, ax=ax2, robust=True) + ax2.set_title('pumped - unpumped') + + ax2.set_xlabel(self.scan_vname) + fig.suptitle(f'{self.plot_title}') # since 'self' is not pickable, this function has to be outside the DSSC class so that it can be used # by the multiprocessing pool.map function -- GitLab