diff --git a/DSSC1module.py b/DSSC1module.py index 380e0c9e4bc0e3aff095a00691764f94d8832bdc..b053da1b2efd3e29135648eb3ef79480c092c4e9 100644 --- a/DSSC1module.py +++ b/DSSC1module.py @@ -106,7 +106,7 @@ class DSSC1module: except: self.delay_mm = 0*self.nrj self.t0 = t0 - self.delay_ps = tb.positionToDelay(self.delay_mm, origin=self.t0) + self.delay_ps = tb.positionToDelay(self.delay_mm, origin=self.t0, invert=True) def collect_dssc_module_file(self): """ Collect the raw DSSC module h5 files. diff --git a/FastCCD.py b/FastCCD.py new file mode 100644 index 0000000000000000000000000000000000000000..1f4124c5081a6f57cd077dbdbbfebb19da6a1e50 --- /dev/null +++ b/FastCCD.py @@ -0,0 +1,641 @@ +from joblib import Parallel, delayed, parallel_backend +from time import strftime +import tempfile +import shutil +from tqdm.auto import tqdm +import os +import warnings +import psutil + +import karabo_data as kd +from karabo_data.read_machinery import find_proposal +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 +from glob import glob + +from imageio import imread + +class FastCCD: + + def __init__(self, proposal, distance=1, raw=False): + """ Create a FastCCD object to process FaStCCD data. + + inputs: + proposal: (int,str) proposal number string + distance: (float) distance sample to FastCCD detector in meter + raw: use processed data from the calibration pipeline or raw files + + """ + if isinstance(proposal,int): + proposal = 'p{:06d}'.format(proposal) + + self.runFolder = find_proposal(proposal) + self.semester = self.runFolder.split('/')[-2] + self.proposal = proposal + self.topic = self.runFolder.split('/')[-3] + self.tempdir = None + self.save_folder = os.path.join(self.runFolder, 'usr/condensed_runs/') + self.distance = distance + self.px_pitch_h = 30 # pitch in microns + self.px_pitch_v = 30 # pitch in microns + + self.aspect = 1 # aspect ratio of the FastCCD images + self.mask = None + self.max_fraction_memory = 0.8 + self.filter_mask = None + self.raw = raw + self.gain = 1 + + print('FastCCD 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 FastCCD distance: {self.distance} m') + print(f'Using raw files: {self.raw}') + + if not os.path.exists(self.save_folder): + warnings.warn(f'Default save folder does not exist: {self.save_folder}') + + self.max_fraction_memory = 0.8 + self.Nworker = 10 + self.maxSaturatedPixel = 1 + + + def __del__(self): + # deleting temporay folder + if self.tempdir: + shutil.rmtree(self.tempdir) + + def open_run(self, run_nr, isDark=False, t0=0.0): + """ Open a run with karabo-data and prepare the virtual dataset for multiprocessing + + inputs: + run_nr: the run number + isDark: True if the run is a dark run + t0: optional t0 in mm + """ + + print('Opening run data with karabo-data') + self.run_nr = run_nr + self.xgm = None + + self.run = kd.open_run(self.proposal, self.run_nr) + self.plot_title = f'{self.proposal} run: {self.run_nr}' + self.isDark = isDark + self.fpt = 1 + #self.nbunches = self.run.get_array('SCS_RR_UTC/MDL/BUNCH_DECODER', 'sase3.nPulses.value') + #self.nbunches = np.unique(self.nbunches) + self.nbunches = 1 + #if len(self.nbunches) == 1: + # self.nbunches = self.nbunches[0] + #else: + # warnings.warn('not all trains have same length FastCCD data') + # print(f'nbunches: {self.nbunches}') + # self.nbunches = self.nbunches[-1] + + print(f'FastCCD frames per train: {self.fpt}') + print(f'SA3 bunches per train: {self.nbunches}') + + print('Collecting FastCCD module files') + self.collect_fastccd_file() + + print(f'Loading XGM data') + try: + self.xgm = self.run.get_array(tb.mnemonics['SCS_SA3']['source'], + tb.mnemonics['SCS_SA3']['key'], + roi=kd.by_index[:self.nbunches]) + self.xgm = self.xgm.squeeze() # remove the pulseId dimension since XGM should have only 1 value per train + except: + self.xgm = xr.DataArray(np.zeros_like(self.run.train_ids),dims = 'trainId', coords = {"trainId":self.run.train_ids}) + + print(f'Loading mono nrj data') + try: + self.nrj = self.run.get_array(tb.mnemonics['nrj']['source'], + tb.mnemonics['nrj']['key']) + + except: + self.nrj = xr.DataArray(np.zeros_like(self.run.train_ids),dims = 'trainId', coords = {"trainId":self.run.train_ids}) + + + print(f'Loading delay line data') + try: + self.delay_mm = self.run.get_array(tb.mnemonics['PP800_DelayLine']['source'], + tb.mnemonics['PP800_DelayLine']['key']) + except: + self.delay_mm = xr.DataArray(np.zeros_like(self.run.train_ids),dims = 'trainId', coords = {"trainId":self.run.train_ids}) + self.t0 = t0 + self.delay_ps = tb.positionToDelay(self.delay_mm, origin=self.t0, invert=True) + + print(f'Loading Fast ADC5 data') + try: + self.FastADC5 = self.run.get_array(tb.mnemonics['FastADC5raw']['source'], tb.mnemonics['FastADC5raw']['key']).max('dim_0') + self.FastADC5[self.FastADC5<35000] = 0 + self.FastADC5[self.FastADC5>=35000] = 1 + except: + self.FastADC5 = xr.DataArray(np.zeros_like(self.run.train_ids),dims = 'trainId', coords = {"trainId":self.run.train_ids}) + + # create a dummy scan variable for dark run + # for other type or run, use FastCCD.define_run function to overwrite it + self.scan = xr.DataArray(np.ones_like(self.run.train_ids), dims=['trainId'],coords={'trainId': self.run.train_ids}) + self.scan = self.scan.to_dataset(name='scan_variable') + self.scan_vname = 'dummy' + + def load_gain(self, fname): + """ Load a gain map by giving the filename + """ + self.gain = np.load(fname)['arr_0'][:,:,0] + + def collect_fastccd_file(self): + """ Collect the raw fastCCD h5 files. + """ + + if self.raw: + folder = 'raw' + else: + folder = 'proc' + + pattern = self.runFolder + f'/{folder}/r{self.run_nr:04d}/RAW-R{self.run_nr:04d}-DA05-S*.h5' + self.h5list = glob(pattern) + + def define_scan(self, vname, bins): + """ + Prepare the binning of the FastCCD data. + + inputs: + vname: variable name for the scan, can be a mnemonic string from ToolBox + or a dictionnary with ['source', 'key'] fields + bins: step size (or bins_edge but not yet implemented) + """ + + if vname == 'delay_ps': + self.scan = self.delay_ps + else: + if type(vname) is dict: + self.scan = self.run.get_array(vname['source'], vname['key']) + elif type(vname) is str: + if vname not in tb.mnemonics: + raise ValueError(f'{vname} not found in the ToolBox mnemonics table') + self.scan = self.run.get_array(tb.mnemonics[vname]['source'], tb.mnemonics[vname]['key']) + else: + raise ValueError(f'vname should be a string or a dict. We got {type(vname)}') + + if (type(bins) is int) or (type(bins) is float): + self.scan = bins * np.round(self.scan / bins) + else: + # TODO: digitize the data + raise ValueError(f'To be implemented') + self.scan_vname = vname + + self.scan = self.scan.to_dataset(name='scan_variable') + #self.scan['xgm_pumped'] = self.xgm[:, :self.nbunches:2].mean('dim_0') + #self.scan['xgm_unpumped'] = self.xgm[:, 1:self.nbunches:2].mean('dim_0') + #self.scan.to_netcdf(self.vds_scan, group='data') + + self.scan_counts = xr.DataArray(np.ones(len(self.scan['scan_variable'])), + 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() + + 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) + + if self.scan: + ax2.plot(self.scan['scan_variable']) + ax2.set_xlabel('train #') + ax2.set_ylabel(f'{self.scan_vname}') + + def plot_xgm_hist(self, nbins=100): + """ Plots an histogram of the SCS XGM dedicated SAS3 data. + + inputs: + nbins: number of the bins for the histogram. + """ + + hist, bins_edges = np.histogram(self.xgm, nbins, density=True) + width = 1.0 * (bins_edges[1] - bins_edges[0]) + bins_center = 0.5*(bins_edges[:-1] + bins_edges[1:]) + + plt.figure(figsize=(5,3)) + plt.bar(bins_center, hist, align='center', width=width) + plt.xlabel(f"{tb.mnemonics['SCS_SA3']['source']}{tb.mnemonics['SCS_SA3']['key']}") + plt.ylabel('density') + plt.title(self.plot_title) + + 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 + xgm_low or above xgm_high, that train will be dropped from the dataset. + + inputs: + xgm_low: low threshold value + xgm_high: high threshold value + """ + + if self.isDark: + warnings.warn(f'This run was loaded as dark. Filtering on xgm makes no sense. Aborting') + return + + self.xgm_low = xgm_low + self.xgm_high = xgm_high + + filter_mask = (self.xgm > self.xgm_low) * (self.xgm < self.xgm_high) + + if self.filter_mask is None: + self.filter_mask = filter_mask + else: + self.filter_mask = self.filter_mask*filter_mask + + valid = filter_mask.astype(bool) + self.xgm = self.xgm.where(valid).dropna('trainId') + self.scan = self.scan.sel({'trainId': self.xgm.trainId}) + nrejected = len(self.run.train_ids) - len(self.xgm.trainId) + print((f'Rejecting {nrejected} out of {len(self.run.train_ids)} trains due to xgm ' + f'thresholds: [{self.xgm_low}, {self.xgm_high}]')) + + def load_mask(self, fname, plot=True): + """ Load a FastCCD mask file. + + input: + fname: string of the filename of the mask file + plot: if True, the loaded mask is plotted + """ + + + fccd_mask = imread(fname) + fccd_mask = fccd_mask.astype(float)[..., 0] // 255 + fccd_mask[fccd_mask==0] = np.nan + self.mask = fccd_mask + + if plot: + plt.figure() + plt.imshow(self.mask) + + def binning(self): + """ Bin the FastCCD data by the predifined scan type (FastCCD.define()) using multiprocessing + + """ + + # 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(self.max_fraction_memory*max_GB * 1024**3 // (self.Nworker * 16 * 1934 * 960 * self.fpt)) + + print('processing', self.chunksize, 'trains per chunk') + assert self.chunksize > 500, "cannot load FastCCD h5 files in memory" + + jobs = [] + + for m,h5fname in enumerate(self.h5list): + jobs.append(dict( + fpt=self.fpt, + h5fname=h5fname, + chunksize=self.chunksize, + nbunches=self.nbunches, + workerId=m, + Nworker=self.Nworker, + scan = self.scan, + xgm = self.xgm, + FastADC5 = self.FastADC5 + #maxSaturatedPixel=self.maxSaturatedPixel + )) + + timestamp = strftime('%X') + print(f'start time: {timestamp}') + + with parallel_backend('threading', n_jobs=self.Nworker): + res = Parallel( verbose=20)( + delayed(process_one_module)(job) for job in tqdm(jobs) + ) + + print('finished:', strftime('%X')) + + # rearange the multiprocessed data + # this is to get rid of the worker dimension, there is no sum over worker really involved + self.module_data = xr.concat(res, dim='workerId').sum(dim='workerId') + + self.module_data['pumped'] = self.module_data['pumped'] / self.module_data['sum_count_pumped'] + self.module_data['unpumped'] = self.module_data['unpumped'] / self.module_data['sum_count_unpumped'] + self.module_data['xgm_pumped'] = self.module_data['xgm_pumped'] / self.module_data['sum_count_pumped'] + self.module_data['xgm_unpumped'] = self.module_data['xgm_unpumped'] / self.module_data['sum_count_unpumped'] + + self.module_data['run'] = self.run_nr + self.module_data['t0'] = self.t0 + + self.plot_title = f"{self.proposal} run: {self.module_data['run'].values}" + self.module_data.attrs['plot_title'] = self.plot_title + self.module_data.attrs['scan_variable'] = self.scan_vname + + + def save(self, save_folder=None, overwrite=False): + """ Save the crunched data. + + inputs: + save_folder: string of the fodler where to save the data. + overwrite: boolean whether or not to overwrite existing files. + """ + if save_folder is None: + save_folder = self.save_folder + + if self.isDark: + fname = f'run{self.run_nr}_dark.h5' # no scan + else: + fname = f'run{self.run_nr}.h5' # run with delay scan (change for other scan types!) + + + save_path = os.path.join(save_folder, fname) + file_exists = os.path.isfile(save_path) + + if not file_exists or (file_exists and overwrite): + if file_exists: + warnings.warn(f'Overwriting file: {save_path}') + os.remove(save_path) + self.module_data.to_netcdf(save_path, group='data') + os.chmod(save_path, 0o664) + print('saving: ', save_path) + else: + print('file', save_path, 'exists and overwrite is False') + + def load_binned(self, runNB, dark_runNB=None, xgm_norm = True, save_folder=None): + """ load previously binned (crunched) FastCCD data by FastCCD.crunch() and FastCCD.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}' + + binned = xr.open_dataset(os.path.join(save_folder, f'run{runNB}.h5'), group='data', cache=False) + + if dark_runNB is not None: + dark = xr.open_dataset(os.path.join(save_folder, f'run{dark_runNB}_dark.h5'), group='data', cache=False) + binned['pumped'] = self.gain*(binned['pumped'] - dark['pumped'].squeeze(drop=True)) + binned['unpumped'] = self.gain*(binned['unpumped'] - dark['unpumped'].squeeze(drop=True)) + + 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_pumped'] + binned['sum_count_unpumped'] + self.scan_vname = binned.attrs['scan_variable'] + self.scan = None + + self.binned = binned + + def plot_FastCCD(self, use_mask = True, p_low = 1, p_high = 98, vmin = None, vmax = None): + """ Plot pumped and unpumped FastCCD images. + + inputs: + use_mask: if True, a mask is applied on the FastCCD. + 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 = '' + + + im_pump_mean = self.binned['pumped'].mean('scan_variable') + im_unpump_mean = self.binned['unpumped'].mean('scan_variable') + + self.im_pump_mean = mask*im_pump_mean + self.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, + ) + + tmp = self.im_pump_mean.values.flatten() + try: + _vmin, _vmax = np.percentile(tmp[~np.isnan(tmp)], [p_low, p_high]) + except: + _vmin, _vmax = (None, None) + + if vmin is None: + vmin = _vmin + if vmax is None: + vmax = _vmax + + im = grid[0].imshow(self.im_pump_mean, vmin=vmin, vmax=vmax, aspect=self.aspect) + grid[0].set_title('pumped' + mask_txt) + + im = grid[1].imshow(self.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, 180-1e-6], dr=1, use_mask=True): + """ Perform azimuthal integration of 1D binned FastCCD 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 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 = self.binned['pumped'].values + im_unpumped_arranged = 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) + + ai = tb.azimuthal_integrator(im_pumped_mean.shape, center, angle_range, dr=dr, aspect=1) + 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 + + azimuthal.attrs = self.binned.attrs + + self.azimuthal = azimuthal.swap_dims({'distance': 'delta_q (1/nm)'}) + + def plot_azimuthal_int(self, kind='difference', lim=None): + """ Plot a computed azimuthal integration. + + inputs: + kind: (str) either 'difference' or 'relative' to change the type of plot. + """ + fig, [ax1, ax2, ax3] = plt.subplots(nrows=3, sharex=True, sharey=True) + + xr.plot.imshow(self.azimuthal.pumped, ax=ax1, vmin=0, robust=True) + ax1.set_title('pumped') + + xr.plot.imshow(self.azimuthal.unpumped, ax=ax2, vmin=0, robust=True) + ax2.set_title('unpumped') + + if kind == 'difference': + val = self.azimuthal.pumped - self.azimuthal.unpumped + ax3.set_title('pumped - unpumped') + elif kind == 'relative': + val = (self.azimuthal.pumped - self.azimuthal.unpumped)/self.azimuthal.unpumped + ax3.set_title('(pumped - unpumped)/unpumped') + else: + raise ValueError('kind should be either difference or relative') + + if lim is None: + xr.plot.imshow(val, ax=ax3, robust=True) + else: + xr.plot.imshow(val, ax=ax3, vmin=lim[0], vmax=lim[1]) + + ax3.set_xlabel(self.scan_vname) + fig.suptitle(f'{self.plot_title}') + + def plot_azimuthal_line_cut(self, data, qranges, qwidths): + """ Plot line scans on top of the data. + + inputs: + data: an azimuthal integrated xarray DataArray with 'delta_q (1/nm)' as one of its dimension. + qranges: a list of q-range + qwidth: a list of q-width, same length as qranges + """ + + fig, [ax1, ax2] = plt.subplots(nrows=2, sharex=True, figsize=[8, 7]) + + xr.plot.imshow(data, ax=ax1, robust=True) + + # attributes are not propagated during xarray mathematical operation https://github.com/pydata/xarray/issues/988 + # so we might not have in data the scan vaiable name anymore + ax1.set_xlabel(self.scan_vname) + fig.suptitle(f'{self.plot_title}') + + for i, (qr, qw) in enumerate(zip(qranges, qwidths)): + sel = (data['delta_q (1/nm)'] > (qr - qw/2)) * (data['delta_q (1/nm)'] < (qr + qw/2)) + val = data.where(sel).mean('delta_q (1/nm)') + ax2.plot(data.scan_variable, val, c=f'C{i}', label=f'q = {qr:.2f}') + + ax1.axhline(qr - qw/2, c=f'C{i}', lw=1) + ax1.axhline(qr + qw/2, c=f'C{i}', lw=1) + ax2.legend() + ax2.set_xlabel(self.scan_vname) + + +# since 'self' is not pickable, this function has to be outside the FastCCD class so that it can be used +# by the multiprocessing pool.map function +def process_one_module(job): + fpt = job['fpt'] + Nworker = job['Nworker'] + workerId = job['workerId'] + scan = job['scan'] + chunksize = job['chunksize'] + nbunches = job['nbunches'] + h5fname = job['h5fname'] + xgm = job['xgm'] + FastADC5 = job['FastADC5'] + #maxSaturatedPixel = job['maxSaturatedPixel'] + + image_path = f'/INSTRUMENT/SCS_CDIDET_FCCD2M/DAQ/FCCD:daqOutput/data/image/pixels' + + # crunching + with h5py.File(h5fname, 'r') as m: + fastccd_trains = m['/INSTRUMENT/SCS_CDIDET_FCCD2M/DAQ/FCCD:daqOutput/data/trainId'][()] + data = m[image_path][()].squeeze().astype(np.float64) + + unique_trainIds, unique_list = np.unique(fastccd_trains, return_index = True) + unique_nz_list = np.nonzero(unique_trainIds)[0] + + fastccd_trains = unique_trainIds[unique_nz_list] + coords = {'trainId': fastccd_trains} + + fastccd = xr.DataArray(data[unique_nz_list, :, :], dims=['trainId', 'x', 'y'], coords=coords) + fastccd = fastccd.where(fastccd.sum(('x','y'), skipna=True) > 0) + + aligned_vals = xr.align(*[fastccd, xgm, FastADC5], join='inner') + ds = xr.Dataset(dict(zip(['fastccd', 'xgm', 'FastADC5'], aligned_vals))) + ds['sum_count'] = xr.full_like(ds['fastccd'][..., 0, 0], fill_value=1) + + # grouping and summing + ds['scan_variable'] = scan['scan_variable'] # this only adds scan data for matching trainIds + ds = ds.dropna('trainId') + #print(ds) + + data_pumped = ds.where(ds['FastADC5'] > 0, drop=True).groupby('scan_variable').sum('trainId') + data_unpumped = ds.where(ds['FastADC5'] < 1, drop=True).groupby('scan_variable').sum('trainId') + + module_data = data_pumped['fastccd'].to_dataset('pumped') + module_data['unpumped'] = data_unpumped['fastccd'] + module_data['sum_count_pumped'] = data_pumped['sum_count'] + module_data['sum_count_unpumped'] = data_unpumped['sum_count'] + module_data['xgm_pumped'] = data_pumped['xgm'] + module_data['xgm_unpumped'] = data_unpumped['xgm'] + module_data['workerId'] = workerId + + return module_data \ No newline at end of file diff --git a/Load.py b/Load.py index d8c42b9b2c9965a307cb1f10f83b1ca26b0206e8..0d343b71f1f0955707c6f26ccd00f7f386579872 100644 --- a/Load.py +++ b/Load.py @@ -67,6 +67,10 @@ mnemonics = { "UND": {'source':'SA3_XTD10_UND/DOOCS/PHOTON_ENERGY', 'key':'actualPosition.value', 'dim':None}, + #DPS imagers + "DPS2CAM2": {'source':'SCS_BLU_DPS-2/CAM/IMAGER2CAMERA:daqOutput', + 'key':'data.image.pixels', + 'dim':['dps2cam2_y', 'dps2cam2_x']}, # XTD10 XGM ## keithley "XTD10_photonFlux": {'source':'SA3_XTD10_XGM/XGM/DOOCS', @@ -499,7 +503,7 @@ def concatenateRuns(runs): return result = xr.concat(orderedRuns, dim='trainId') - result.attrs['run'] = [run.attrs['run'] for run in orderedRuns] - result.attrs['runFolder'] = [run.attrs['runFolder'] for run in orderedRuns] + for k in orderedRuns[0].attrs.keys(): + result.attrs[k] = [run.attrs[k] for run in orderedRuns] return result diff --git a/XAS.py b/XAS.py index 54b3c255af654dbff0369700d5dc90f96a60ed8c..8ca0b7cc683e59ca3c0a9bdc6127e60c70fb2ca4 100644 --- a/XAS.py +++ b/XAS.py @@ -12,6 +12,7 @@ import numpy as np import matplotlib.gridspec as gridspec import matplotlib.pyplot as plt +import re def absorption(T, Io): """ Compute the absorption A = -ln(T/Io) @@ -190,9 +191,9 @@ def xas(nrun, bins=None, Iokey='SCS_SA3', Itkey='MCP3apd', nrjkey='nrj', Iooffse color='C1', alpha=0.2) ax1_twin.set_ylabel('Io') try: - proposalNB=int(nrun.attrs['runFolder'].split('/')[-4][1:]) - runNB=int(nrun.attrs['runFolder'].split('/')[-2][1:]) - ax1.set_title('run {:d} p{:}'.format(runNB, proposalNB)) + proposalNB=int(re.findall(r'p(\d{6})', nrun.attrs['runFolder'])[0]) + runNB=int(re.findall(r'r(\d{4})', nrun.attrs['runFolder'])[0]) + ax1.set_title(f'run {runNB} p{proposalNB}') except: f.suptitle(nrun.attrs['plot_title']) diff --git a/__init__.py b/__init__.py index 805786d9a446860059852c1080c7da48e0fffcfa..ee34d75e82c0a49e2a417f4f11842be049b66376 100644 --- a/__init__.py +++ b/__init__.py @@ -7,3 +7,4 @@ from ToolBox.DSSC import DSSC from ToolBox.azimuthal_integrator import * from ToolBox.DSSC1module import * from ToolBox.bunch_pattern import * +from ToolBox.FastCCD import * diff --git a/azimuthal_integrator.py b/azimuthal_integrator.py index e064a392c2aaf497a365b17a25b3df5461c1f753..b84df6c347c9445e713d53234562857f29098135 100644 --- a/azimuthal_integrator.py +++ b/azimuthal_integrator.py @@ -1,7 +1,7 @@ import numpy as np class azimuthal_integrator(object): - def __init__(self, imageshape, center, polar_range, dr=2): + def __init__(self, imageshape, center, polar_range, dr=2, aspect=204/236): ''' Create a reusable integrator for repeated azimuthal integration of similar images. Calculates array indices for a given parameter set that allows @@ -21,6 +21,9 @@ class azimuthal_integrator(object): dr : int, default 2 radial width of the integration slices. Takes non-square DSSC pixels into account. + aspect: float, default 204/236 for DSSC + aspect ratio of the pixel pitch + Returns ======= ai : azimuthal_integrator instance @@ -39,7 +42,7 @@ class azimuthal_integrator(object): ycoord -= cy # distance from center, hexagonal pixel shape taken into account - dist_array = np.hypot(xcoord * 204 / 236, ycoord) + dist_array = np.hypot(xcoord * aspect, ycoord) # array of polar angles if np.abs(polar_range[1]-polar_range[0]) > 180: @@ -53,7 +56,7 @@ class azimuthal_integrator(object): polar_array = np.mod(polar_array, np.pi) self.polar_mask = (polar_array > tmin) * (polar_array < tmax) - self.maxdist = min(sx - cx, sy - cy) + self.maxdist = max(sx - cx, sy - cy) ix, iy = np.indices(dimensions=(sx, sy)) self.index_array = np.ravel_multi_index((ix, iy), (sx, sy)) diff --git a/knife_edge.py b/knife_edge.py index 224b8a88bda810ad6371e613e89a7130b4efc017..e7b906bab1fd8bd28ec5683f5bbcf12bcb18e21d 100644 --- a/knife_edge.py +++ b/knife_edge.py @@ -8,10 +8,12 @@ import matplotlib.pyplot as plt import numpy as np from scipy.special import erfc from scipy.optimize import curve_fit +import bisect -def knife_edge(nrun, axisKey='scannerX', signalKey='FastADC4peaks', p0=None, full=False, plot=False): +def knife_edge(nrun, axisKey='scannerX', signalKey='FastADC4peaks', + axisRange=[None,None], p0=None, full=False, plot=False): ''' Calculates the beam radius at 1/e^2 from a knife-edge scan by fitting with - erfc function: f(a, u) = a*erfc(u) or f(a, u) = a*erfc(-u) where + erfc function: f(a,b,u) = a*erfc(u)+b or f(a,b,u) = a*erfc(-u)+b where u = sqrt(2)*(x-x0)/w0 with w0 the beam radius at 1/e^2 and x0 the beam center. Inputs: nrun: xarray Dataset containing the detector signal and the motor @@ -19,22 +21,24 @@ def knife_edge(nrun, axisKey='scannerX', signalKey='FastADC4peaks', p0=None, ful axisKey: string, key of the axis against which the knife-edge is performed. signalKey: string, key of the detector signal. - p0: list, initial parameters used for the fit: x0, w0, a. If None, a beam + axisRange: list of length 2, minimum and maximum values between which to apply + the fit. + p0: list, initial parameters used for the fit: x0, w0, a, b. If None, a beam radius of 100 um is assumed. full: bool: If False, returns the beam radius and standard error. If True, returns the popt, pcov list of parameters and covariance matrix from - curve_fit. + curve_fit as well as the fitting function. plot: bool: If True, plots the data and the result of the fit. Outputs: If full is False, ndarray with beam radius at 1/e^2 in mm and standard error from the fit in mm. If full is True, returns popt and pcov from curve_fit function. ''' - def integPowerUp(x, x0, w0, a): - return a*erfc(-np.sqrt(2)*(x-x0)/w0) + def integPowerUp(x, x0, w0, a, b): + return a*erfc(-np.sqrt(2)*(x-x0)/w0) + b - def integPowerDown(x, x0, w0, a): - return a*erfc(np.sqrt(2)*(x-x0)/w0) + def integPowerDown(x, x0, w0, a, b): + return a*erfc(np.sqrt(2)*(x-x0)/w0) + b #get the number of pulses per train from the signal source: dim = nrun[signalKey].dims[1] @@ -46,22 +50,38 @@ def knife_edge(nrun, axisKey='scannerX', signalKey='FastADC4peaks', p0=None, ful sortIdx = np.argsort(positions) positions = positions[sortIdx] intensities = nrun[signalKey].values.flatten()[sortIdx] - + + if axisRange[0] is None or axisRange[0] < positions[0]: + idxMin = 0 + else: + if axisRange[0] >= positions[-1]: + raise ValueError('The minimum value of axisRange is too large') + idxMin = bisect.bisect(positions, axisRange[0]) + if axisRange[1] is None or axisRange[1] > positions[-1]: + idxMax = None + else: + if axisRange[1] <= positions[0]: + raise ValueError('The maximum value of axisRange is too small') + idxMax = bisect.bisect(positions, axisRange[1]) + 1 + positions = positions[idxMin:idxMax] + intensities = intensities[idxMin:idxMax] + # estimate a linear slope fitting the data to determine which function to fit slope = np.cov(positions, intensities)[0][1]/np.var(positions) if slope < 0: func = integPowerDown - funcStr = 'a*erfc(np.sqrt(2)*(x-x0)/w0)' + funcStr = 'a*erfc(np.sqrt(2)*(x-x0)/w0) + b' else: func = integPowerUp - funcStr = 'a*erfc(-np.sqrt(2)*(x-x0)/w0)' + funcStr = 'a*erfc(-np.sqrt(2)*(x-x0)/w0) + b' if p0 is None: - p0 = [np.mean(positions), 0.1, np.max(intensities)/2] + p0 = [np.mean(positions), 0.1, np.max(intensities)/2, 0] popt, pcov = curve_fit(func, positions, intensities, p0=p0) print('fitting function:', funcStr) print('w0 = (%.1f +/- %.1f) um'%(popt[1]*1e3, pcov[1,1]**0.5*1e3)) - print('x0 = (%.3f +/- %.3f) mm'%(popt[0], pcov[0,0]**0.5*1e3)) - print('a = %e +/- %e '%(popt[2], pcov[2,2]**0.5*1e3)) + print('x0 = (%.3f +/- %.3f) mm'%(popt[0], pcov[0,0]**0.5)) + print('a = %e +/- %e '%(popt[2], pcov[2,2]**0.5)) + print('b = %e +/- %e '%(popt[3], pcov[3,3]**0.5)) if plot: xfit = np.linspace(positions.min(), positions.max(), 1000) @@ -78,6 +98,6 @@ def knife_edge(nrun, axisKey='scannerX', signalKey='FastADC4peaks', p0=None, ful plt.title(nrun.attrs['runFolder']) plt.tight_layout() if full: - return popt, pcov + return popt, pcov, func else: return np.array([popt[1], pcov[1,1]**0.5]) diff --git a/xgm.py b/xgm.py index 3501090794a9cc90dba537083040f9665c62a2cf..d38165e6b2485f86b598aa00e89813f4aee6b94e 100644 --- a/xgm.py +++ b/xgm.py @@ -8,6 +8,7 @@ import matplotlib.pyplot as plt import numpy as np import xarray as xr +from scipy.signal import find_peaks # XGM def cleanXGMdata(data, npulses=None, sase3First=True): @@ -468,7 +469,7 @@ def getTIMapd(data, mcp=1, use_apd=True, intstart=None, intstop=None, print(f'Warning: apd parameter was set to record 1 pulse out of {period} @ 4.5 MHz ' + f'but XFEL delivered 1 pulse out of {period_from_bunch_pattern}.') maxPulses = data['npulses_sase3'].max().values - if period*pulseIdDim < period_from_bunch_pattern*maxPulses: + if period*pulseIdDim < period_from_bunch_pattern*(maxPulses-1): print(f'Warning: Number of pulses and/or rep. rate in apd parameters were set ' + f'too low ({pulseIdDim})to record the {maxPulses} SASE 3 pulses') peaks = data[f'MCP{mcp}apd'] @@ -862,6 +863,51 @@ def fastAdcPeaks(data, channel, intstart, intstop, bkgstart, bkgstop, period=Non results[:,i] = integ return results +def autoFindFastAdcPeaks(data, channel=5, threshold=35000, display=False, plot=False): + ''' Automatically finds positive peaks in channel of Fast ADC trace, assuming + a minimum absolute height of 'threshold' counts and a minimum width of 4 + samples. The find_peaks function and determination of the peak region and + baseline subtraction is optimized for typical photodiode signals of the + SCS instrument (ILH, FFT reflectometer, FFT diag stage). + Inputs: + data: xarray Dataset containing Fast ADC traces + key: data key of the array of traces + threshold: minimum height of the peaks + display: bool, displays info on the pulses found + plot: plots regions of integration of the first pulse in the trace + Output: + peaks: DataArray of the integrated peaks + ''' + + key = f'FastADC{channel}raw' + if key not in data: + raise ValueError(f'{key} not found in data set') + trace = data[key].where(data['npulses_sase3']>0, drop=True).isel(trainId=0).values + centers, peaks = find_peaks(trace, height=threshold, width=(4, None)) + c = centers[0] + w = np.average(peaks['widths']).astype(int) + period = np.median(np.diff(centers)).astype(int) + npulses = centers.shape[0] + intstart = int(c - w/4) + 1 + intstop = int(c + w/4) + 1 + bkgstop = int(peaks['left_ips'][0])-5 + bkgstart = bkgstop - 10 + if display: + print(f'Found {npulses} pulses, avg. width={w}, period={period} samples, ' + + f'rep. rate={1e6/(9.230769*period):.3f} kHz') + fAdcPeaks = fastAdcPeaks(data, channel=channel, intstart=intstart, intstop=intstop, + bkgstart=bkgstart, bkgstop=bkgstop, period=period, npulses=npulses) + if plot: + plt.figure() + plt.plot(trace, 'o-', ms=3) + for i in range(npulses): + plt.axvline(intstart+i*period, ls='--', color='g') + plt.axvline(intstop+i*period, ls='--', color='r') + plt.axvline(bkgstart+i*period, ls='--', color='lightgrey') + plt.axvline(bkgstop+i*period, ls='--', color='grey') + plt.title(f'Fast ADC {channel} trace') + plt.xlim(bkgstart-10, intstop + 50) + return fAdcPeaks def mergeFastAdcPeaks(data, channel, intstart, intstop, bkgstart, bkgstop, period=None, npulses=None, dim='lasPulseId'):