def normalize_dim(arr: DataType, dim_or_dims, keep_id=False): """ Normalizes the intensity so that all values along arr.sum(dims other than those in ``dim_or_dims``) have the same value. The function normalizes so that the average value of cells in the output is 1. :param dim_name: :return: """ dims = dim_or_dims if isinstance(dim_or_dims, str): dims = [dims] summed_arr = arr.fillna(arr.mean()).sum( [d for d in arr.dims if d not in dims]) normalized_arr = arr / (summed_arr / np.product(summed_arr.shape)) to_return = xr.DataArray(normalized_arr.values, arr.coords, arr.dims, attrs=arr.attrs) if not keep_id and 'id' in to_return.attrs: del to_return.attrs['id'] provenance(to_return, arr, { 'what': 'Normalize axis or axes', 'by': 'normalize_dim', 'dims': dims, }) return to_return
def set_data(self, data: DataType, **kwargs): original_data = normalize_to_spectrum(data) self.original_data = original_data if len(data.dims) > 2: assert 'eV' in original_data.dims data = data.sel(eV=slice(-0.05, 0.05)).sum('eV', keep_attrs=True) data.coords['eV'] = 0 else: data = original_data if 'eV' in data.dims: data = data.S.transpose_to_back('eV') self.data = data.copy(deep=True) if not kwargs: rng_mul = 1 if data.coords['hv'] < 12: rng_mul = 0.5 if data.coords['hv'] < 7: rng_mul = 0.25 if 'eV' in self.data.dims: kwargs = { 'kp': np.linspace(-2, 2, 400) * rng_mul, } else: kwargs = { 'kx': np.linspace(-3, 3, 300) * rng_mul, 'ky': np.linspace(-3, 3, 300) * rng_mul, } self.conversion_kwargs = kwargs
def compare(A: DataType, B: DataType): A = normalize_to_spectrum(A) attrs = A.attrs B = normalize_to_spectrum(B) # normalize total intensity TOTAL_INTENSITY = 1000000 A = A / (A.sum(A.dims) / TOTAL_INTENSITY) B = B / (B.sum(B.dims) / TOTAL_INTENSITY) A.attrs.update(**attrs) tool = ComparisonTool(other=B) return tool.make_tool(A)
def fs_gap(data: DataType, shape=None, energy_range=None): data = normalize_to_spectrum(data) if energy_range is None: energy_range = slice(-0.1, None) data.sel(eV=energy_range) reduction = None if shape is None: # Just rebin the data along 'phi' reduction = {'phi': 16} data = rebin(data, reduction=reduction, shape=shape) return broadcast_model(GStepBModel, data, ['phi', 'beta'])
def calculate_shirley_background(xps: DataType, energy_range: slice = None, eps=1e-7, max_iters=50, n_samples=5): """ Calculates a shirley background iteratively over the full energy range `energy_range`. :param xps: :param energy_range: :param eps: :param max_iters: :return: """ if energy_range is None: energy_range = slice(None, None) xps = normalize_to_spectrum(xps) xps_for_calc = xps.sel(eV=energy_range) bkg = calculate_shirley_background_full_range(xps_for_calc, eps, max_iters) full_bkg = xps * 0 left_idx = np.searchsorted(full_bkg.eV.values, bkg.eV.values[0], side='left') right_idx = left_idx + len(bkg) full_bkg.values[:left_idx] = bkg.values[0] full_bkg.values[left_idx:right_idx] = bkg.values full_bkg.values[right_idx:] = bkg.values[-1] return full_bkg
def determine_broadened_fermi_distribution(reference_data: DataType, fixed_temperature=True): """ Determine the parameters for broadening by temperature and instrumental resolution for a piece of data. As a general rule, we first try to estimate the instrumental broadening and linewidth broadening according to calibrations provided for the beamline + instrument, as a starting point. We also calculate the thermal broadening to expect, and fit an edge location. Then we use a Gaussian convolved Fermi-Dirac distribution against an affine density of states near the Fermi level, with a constant offset background above the Fermi level as a simple but effective model when away from lineshapes. These parameters can be used to bootstrap a fit to actual data or used directly in ``normalize_by_fermi_dirac``. :param reference_data: :return: """ params = {} if fixed_temperature: params['fd_width'] = { 'value': reference_data.S.temp * K_BOLTZMANN_EV_KELVIN, 'vary': False, } reference_data = normalize_to_spectrum(reference_data) sum_dims = list(reference_data.dims) sum_dims.remove('eV') return AffineBroadenedFD().guess_fit(reference_data.sum(sum_dims), params=params)
def estimate_prior_adjustment(data: DataType, region: Union[dict, str] = None) -> float: r""" Estimates the parameters of a distribution generating the intensity histogram of pixels in a spectrum. In a perfectly linear, single-electron single-count detector, this would be a poisson distribution with \lambda=mean(counts) over the window. Despite this, we can estimate \lambda phenomenologically and verify that a Poisson distribution provides a good prior for the data, allowing us to perform statistical bootstrapping. You should use this with a spectrum that has uniform intensity, i.e. with a copper reference or similar. :param data: :return: returns sigma / mu, adjustment factor for the Poisson distribution """ data = normalize_to_spectrum(data) if region is None: region = 'copper_prior' region = normalize_region(region) if 'cycle' in data.dims: data = data.sum('cycle') data = data.S.zero_spectrometer_edges().S.region_sel(region) values = data.values.ravel() values = values[np.where(values)] return np.std(values) / np.mean(values)
def symmetrize(data: DataType, subpixel=False, full_spectrum=False): """ Symmetrizes data across the chemical potential. This provides a crude tool by which gap analysis can be performed. In this implementation, subpixel accuracy is achieved by interpolating data. :param data: Input array. :param subpixel: Enable subpixel correction :param full_spectrum: Returns data above and below the chemical potential. By default, only the bound part of the spectrum (below the chemical potential) is returned, because the other half is identical. :return: """ data = normalize_to_spectrum(data).S.transpose_to_front('eV') if subpixel or full_spectrum: data = _shift_energy_interpolate(data) above = data.sel(eV=slice(0, None)) below = data.sel(eV=slice(None, 0)).copy(deep=True) l = len(above.coords['eV']) zeros = below.values * 0 zeros[-l:] = above.values[::-1] below.values = below.values + zeros if full_spectrum: if not subpixel: warnings.warn("full spectrum symmetrization uses subpixel correction") full_data = below.copy(deep=True) new_above = full_data.copy(deep=True)[::-1] new_above.coords['eV'] = (new_above.coords['eV'] * -1) full_data = xr.concat([full_data, new_above[1:]], dim='eV') result = full_data else: result = below return result
def normalize_sarpes_photocurrent(data: DataType): """ Normalizes the down channel so that it matches the up channel in terms of mean photocurrent. Destroys the integrity of "count" data. :param data: :return: """ copied = data.copy(deep=True) copied.down.values = ( copied.down * (copied.photocurrent_up / copied.photocurrent_down)).values return copied
def summarize(data: DataType, axes=None): data = normalize_to_spectrum(data) axes_shapes_for_dims = { 1: (1, 1), 2: (1, 1), 3: (2, 2), # one extra here 4: (3, 2), # corresponds to 4 choose 2 axes } if axes is None: fig, axes = plt.subplots(axes_shapes_for_dims.get(len(data.dims)), figsize=(8, 8)) flat_axes = axes.ravel() combinations = list(itertools.combinations(data.dims, 2)) for axi, combination in zip(flat_axes, combinations): data.sum(combination).plot(ax=axi) fancy_labels(axi) for i in range(len(combinations), len(flat_axes)): flat_axes[i].set_axis_off() return axes
def find_kf_by_mdc(slice: DataType, offset=0, **kwargs): """ Offset is used to control the radial offset from the pocket for studies where you want to go slightly off the Fermi momentum :param slice: :param offset: :param kwargs: :return: """ if isinstance(slice, xr.Dataset): slice = slice.data assert isinstance(slice, xr.DataArray) if 'eV' in slice.dims: slice = slice.sum('eV') lor = LorentzianModel() bkg = AffineBackgroundModel(prefix='b_') result = (lor + bkg).guess_fit(data=slice, params=kwargs) return result.params['center'].value + offset
def remove_incoherent_background(data: DataType, set_zero=True): """ Sometimes spectra are contaminated by data above the Fermi level for various reasons (such as broad core levels from 2nd harmonic light, or slow enough electrons in ToF experiments to be counted in subsequent pulses). :param data: :param set_zero: :return: """ data = normalize_to_spectrum(data) approximate_fermi_energy_level = data.S.find_spectrum_energy_edges().max() background = data.sel(eV=slice(approximate_fermi_energy_level + 0.1, None)) density = background.sum('eV') / (np.logical_not(np.isnan(background)) * 1).sum('eV') new = data - density if set_zero: new.values[new.values < 0] = 0 return new
def apply_mask(data: DataType, mask, replace=np.nan, radius=None, invert=False): """ Applies a logical mask, i.e. one given in terms of polygons, to a specific piece of data. This can be used to set values outside or inside a series of polygon masks to a given value or to NaN. Expanding or contracting the masked region can be accomplished with the radius argument, but by default strict inclusion is used. Some masks include a `fermi` parameter which allows for clipping the detector boundaries in a semi-automated fashion. If this is included, only 200meV above the Fermi level will be included in the returned data. This helps to prevent very large and undesirable regions filled with only the replacement value which can complicate automated analyses that rely on masking. :param data: Data to mask. :param mask: Logical definition of the mask, appropriate for passing to `polys_to_mask` :param replace: The value to substitute for pixels masked. :param radius: Radius by which to expand the masked area. :param invert: Allows logical inversion of the masked parts of the data. By default, the area inside the polygon sequence is replaced by `replace`. :return: """ data = normalize_to_spectrum(data) fermi = mask.get('fermi') if isinstance(mask, dict): dims = mask.get('dims', data.dims) mask = polys_to_mask(mask, data.coords, [s for i, s in enumerate(data.shape) if data.dims[i] in dims], radius=radius, invert=invert) masked_data = data.copy(deep=True) masked_data.values = masked_data.values * 1.0 masked_data.values[mask] = replace if fermi is not None: return masked_data.sel(eV=slice(None, fermi + 0.2)) return masked_data
def _shift_energy_interpolate(data: DataType, shift=None): if shift is not None: pass # raise NotImplementedError("arbitrary shift not yet implemented") data = normalize_to_spectrum(data).S.transpose_to_front('eV') new_data = data.copy(deep=True) new_axis = new_data.coords['eV'] new_values = new_data.values * 0 if shift is None: closest_to_zero = data.coords['eV'].sel(eV=0, method='nearest') shift = -closest_to_zero stride = data.T.stride('eV', generic_dim_names=False) if np.abs(shift) >= stride: n_strides = int(shift / stride) new_axis = new_axis + n_strides * stride shift = shift - stride * n_strides new_axis = new_axis + shift weight = float(shift / stride) new_values = new_values + data.values * (1 - weight) if shift > 0: new_values[1:] = new_values[1:] + data.values[:-1] * weight if shift < 0: new_values[:-1] = new_values[:-1] + data.values[1:] * weight new_data.coords['eV'] = new_axis new_data.values = new_values return new_data
def calculate_shirley_background_full_range(xps: DataType, eps=1e-7, max_iters=50, n_samples=5): """ Calculates a shirley background in the range of `energy_slice` according to: S(E) = I(E_right) + k * (A_right(E)) / (A_left(E) + A_right(E)) Typically k := I(E_right) - I(E_left) The iterative method is continued so long as the total background is not converged to relative error `eps`. The method continues for a maximum number of iterations `max_iters`. In practice, what we can do is to calculate the cumulative sum of the data along the energy axis of both the data and the current estimate of the background :param xps: :param eps: :return: """ xps = normalize_to_spectrum(xps) background = xps.copy(deep=True) cumulative_xps = np.cumsum(xps.values) total_xps = np.sum(xps.values) rel_error = np.inf i_left = np.mean(xps.values[:n_samples]) i_right = np.mean(xps.values[-n_samples:]) iter_count = 0 k = i_left - i_right for iter_count in range(max_iters): cumulative_background = np.cumsum(background.values) total_background = np.sum(background.values) new_bkg = background.copy(deep=True) for i in range(len(new_bkg)): new_bkg.values[i] = i_right + k * ( (total_xps - cumulative_xps[i] - (total_background - cumulative_background[i])) / (total_xps - total_background + 1e-5)) rel_error = np.abs(np.sum(new_bkg.values) - total_background) / (total_background) background = new_bkg if rel_error < eps: break if (iter_count + 1) == max_iters: warnings.warn('Shirley background calculation did not converge ' + 'after {} steps with relative error {}!'.format( max_iters, rel_error)) return background
def broadcast_model(model_cls: Union[type, TypeIterable], data: DataType, broadcast_dims, params=None, progress=True, dataset=True, weights=None, safe=False, prefixes=None, window=None, multithread=False): """ Perform a fit across a number of dimensions. Allows composite models as well as models defined and compiled through strings. :param model_cls: :param data: :param broadcast_dims: :param params: :param progress: :param dataset: :param weights: :param safe: :param window: :return: """ if params is None: params = {} if isinstance(broadcast_dims, str): broadcast_dims = [broadcast_dims] data = normalize_to_spectrum(data) cs = {} for dim in broadcast_dims: cs[dim] = data.coords[dim] other_axes = set(data.dims).difference(set(broadcast_dims)) template = data.sum(list(other_axes)) template.values = np.ndarray(template.shape, dtype=np.object) residual = data.copy(deep=True) residual.values = np.zeros(residual.shape) model = compile_model(parse_model(model_cls), params=params, prefixes=prefixes) if isinstance(params, (list, tuple)): params = {} new_params = model.make_params() n_fits = np.prod(np.array(list(template.S.dshape.values()))) identity = lambda x, *args, **kwargs: x wrap_progress = identity if progress: wrap_progress = tqdm_notebook _fit_func = functools.partial(_perform_fit, data=data, model=model, params=params, safe=safe, weights=weights, window=window) if multithread: with ProcessPoolExecutor() as executor: for fit_result, fit_residual, coords in executor.map( _fit_func, template.T.iter_coords()): template.loc[coords] = fit_result residual.loc[coords] = fit_residual else: for indices, cut_coords in wrap_progress( template.T.enumerate_iter_coords(), desc='Fitting', total=n_fits): fit_result, fit_residual, _ = _fit_func(cut_coords) template.loc[cut_coords] = fit_result residual.loc[cut_coords] = fit_residual if dataset: return xr.Dataset( { 'results': template, 'data': data, 'residual': residual, 'norm_residual': residual / data, }, residual.coords) template.attrs['original_data'] = data return template
def normalize_total(data: DataType): data = normalize_to_spectrum(data) return data / (data.sum(data.dims) / 1000000)
def convert_coordinates_to_kspace_forward(arr: DataType, **kwargs): """ Forward converts all the individual coordinates of the data array :param arr: :param kwargs: :return: """ arr = arr.copy(deep=True) skip = {'eV', 'cycle', 'delay', 'T'} keep = { 'eV', } all = {k: v for k, v in arr.indexes.items() if k not in skip} kept = {k: v for k, v in arr.indexes.items() if k in keep} old_dims = list(all.keys()) old_dims.sort() if not old_dims: return None dest_coords = { ('phi', ): ['kp', 'kz'], ('theta', ): ['kp', 'kz'], ('beta', ): ['kp', 'kz'], ( 'phi', 'theta', ): ['kx', 'ky', 'kz'], ( 'beta', 'phi', ): ['kx', 'ky', 'kz'], ( 'hv', 'phi', ): ['kx', 'ky', 'kz'], ('hv', ): ['kp', 'kz'], ( 'beta', 'hv', 'phi', ): ['kx', 'ky', 'kz'], ('hv', 'phi', 'theta'): ['kx', 'ky', 'kz'], ('hv', 'phi', 'psi'): ['kx', 'ky', 'kz'], ( 'chi', 'hv', 'phi', ): ['kx', 'ky', 'kz'], }.get(tuple(old_dims)) full_old_dims = old_dims + list(kept.keys()) projection_vectors = np.ndarray(shape=tuple( len(arr.coords[d]) for d in full_old_dims), dtype=object) # these are a little special, depending on the scan type we might not have a phi coordinate # that aspect of this is broken for now, but we need not worry def broadcast_by_dim_location(data, target_shape, dim_location=None): if isinstance(data, xr.DataArray): if not data.dims: data = data.item() if isinstance(data, ( int, float, )): return np.ones(target_shape) * data # else we are dealing with an actual array the_slice = [None] * len(target_shape) the_slice[dim_location] = slice(None, None, None) return np.asarray(data)[the_slice] raw_coords = { 'phi': arr.coords['phi'].values - arr.S.phi_offset, 'beta': (0 if arr.coords['beta'] is None else arr.coords['beta'].values) - arr.S.beta_offset, 'theta': (0 if arr.coords['theta'] is None else arr.coords['theta'].values) - arr.S.theta_offset, 'hv': arr.coords['hv'], } raw_coords = { k: broadcast_by_dim_location( v, projection_vectors.shape, full_old_dims.index(k) if k in full_old_dims else None) for k, v in raw_coords.items() } # fill in the vectors binding_energy = broadcast_by_dim_location( arr.coords['eV'] - arr.S.work_function, projection_vectors.shape, full_old_dims.index('eV') if 'eV' in full_old_dims else None) photon_energy = broadcast_by_dim_location( arr.coords['hv'], projection_vectors.shape, full_old_dims.index('hv') if 'hv' in full_old_dims else None) kinetic_energy = binding_energy + photon_energy inner_potential = arr.S.inner_potential # some notes on angle conversion: # BL4 conventions # angle conventions are standard: # phi = analyzer acceptance # polar = perpendicular scan angle # theta = parallel to analyzer slit rotation angle # [ 1 0 0 ] [ cos(polar) 0 sin(polar) ] [ 0 ] # [ 0 cos(theta) sin(theta) ] * [ 0 1 0 ] * [ k sin(phi) ] # [ 0 -sin(theta) cos(theta) ] [ -sin(polar) 0 cos(polar) ] [ k cos(phi) ] # # = # # [ 1 0 0 ] [ sin(polar) * cos(phi) ] # [ 0 cos(theta) sin(theta) ] * k [ sin(phi) ] # [ 0 -sin(theta) cos(theta) ] [ cos(polar) * cos(phi) ] # # = # # k ( sin(polar) * cos(phi), # cos(theta)*sin(phi) + cos(polar) * cos(phi) * sin(theta), # -sin(theta) * sin(phi) + cos(theta) * cos(polar) * cos(phi), # ) # # main chamber conventions, with no analyzer rotation (referred to as alpha angle in the Igor code # angle conventions are standard: # phi = analyzer acceptance # polar = perpendicular scan angle # theta = parallel to analyzer slit rotation angle # [ 1 0 0 ] [ sin(phi + theta) ] # [ 0 cos(polar) sin(polar) ] * k [ 0 ] # [ 0 -sin(polar) cos(polar) ] [ cos(phi + theta) ] # # = # # k (sin(phi + theta), cos(phi + theta) * sin(polar), cos(phi + theta) cos(polar), ) # # for now we are setting the theta angle to zero, this only has an effect for vertical slit analyzers, # and then only when the tilt angle is very large # TODO check me raw_translated = { 'kx': euler_to_kx(kinetic_energy, raw_coords['phi'], raw_coords['beta'], theta=0, slit_is_vertical=arr.S.is_slit_vertical), 'ky': euler_to_ky(kinetic_energy, raw_coords['phi'], raw_coords['beta'], theta=0, slit_is_vertical=arr.S.is_slit_vertical), 'kz': euler_to_kz(kinetic_energy, raw_coords['phi'], raw_coords['beta'], theta=0, slit_is_vertical=arr.S.is_slit_vertical, inner_potential=inner_potential), } if 'kp' in dest_coords: if np.sum(raw_translated['kx']**2) > np.sum(raw_translated['ky']**2): sign = raw_translated['kx'] / np.sqrt(raw_translated['kx']**2 + 1e-8) else: sign = raw_translated['ky'] / np.sqrt(raw_translated['ky']**2 + 1e-8) raw_translated['kp'] = np.sqrt(raw_translated['kx']**2 + raw_translated['ky']**2) * sign data_vars = {} for dest_coord in dest_coords: data_vars[dest_coord] = (full_old_dims, np.squeeze(raw_translated[dest_coord])) return xr.Dataset(data_vars, coords=arr.indexes)
def save_dataset(arr: DataType, filename=None, force=False): """ Persists a dataset to disk. In order to serialize some attributes, you may need to modify wrap and unwrap arrs above in order to make sure a parameter is saved. In some cases, such as when you would like to add information to the attributes, it is nice to be able to force a write, since a write would not take place if the file is already on disk. To do this you can set the ``force`` attribute. :param arr: :param force: :return: """ import arpes.xarray_extensions # pylint: disable=unused-import, redefined-outer-name with open(DATASET_CACHE_RECORD, 'r') as cache_record: records = json.load(cache_record) filename = filename or _filename_for(arr) if filename is None: filename = _filename_for(arr) attrs_filename = _filename_for_attrs(arr) else: attrs_filename = filename + '.attrs.json' if 'id' in arr.attrs and arr.attrs['id'] in records: if force: if os.path.exists(filename): os.replace(filename, filename + '.keep') if os.path.exists(attrs_filename): os.replace(attrs_filename, attrs_filename + '.keep') else: return df = arr.attrs.pop('df', None) arr.attrs.pop('', None) # protect against totally stripped attribute names arr = wrap_datavar_attrs(arr, original_data=arr) ref_attrs = arr.attrs.pop('ref_attrs', None) arr.to_netcdf(filename, engine='netcdf4') with open(attrs_filename, 'w') as file: json.dump(arr.attrs, file) if 'id' in arr.attrs: first_write = arr.attrs['id'] not in records records[arr.attrs['id']] = { 'file': filename, **{k: v for k, v in arr.attrs.items() if k in WHITELIST_KEYS} } else: first_write = False # this was a first write if first_write: with open(DATASET_CACHE_RECORD, 'w') as cache_record: json.dump(records, cache_record, sort_keys=True, indent=2) if ref_attrs is not None: arr.attrs['ref_attrs'] = ref_attrs arr = unwrap_datavar_attrs(arr) if df is not None: arr.attrs['df'] = df
def magnify_circular_regions_plot(data: DataType, magnified_points, mag=10, radius=0.05, cmap='viridis', color=None, edgecolor='red', out=None, ax=None, **kwargs): data = normalize_to_spectrum(data) fig = None if ax is None: fig, ax = plt.subplots(figsize=kwargs.get('figsize', ( 7, 5, ))) mesh = data.plot(ax=ax, cmap=cmap) clim = list(mesh.get_clim()) clim[1] = clim[1] / mag mask = np.zeros(shape=(len(data.values.ravel()), )) pts = np.zeros(shape=( len(data.values.ravel()), 2, )) mask = mask > 0 raveled = data.T.ravel() pts[:, 0] = raveled[data.dims[0]] pts[:, 1] = raveled[data.dims[1]] x0, y0 = ax.transAxes.transform((0, 0)) # lower left in pixels x1, y1 = ax.transAxes.transform((1, 1)) # upper right in pixes dx = x1 - x0 dy = y1 - y0 maxd = max(dx, dy) xlim, ylim = ax.get_xlim(), ax.get_ylim() width = radius * maxd / dx * (xlim[1] - xlim[0]) height = radius * maxd / dy * (ylim[1] - ylim[0]) if not isinstance(edgecolor, list): edgecolor = [edgecolor for _ in range(len(magnified_points))] if not isinstance(color, list): color = [color for _ in range(len(magnified_points))] pts[:, 1] = (pts[:, 1]) / (xlim[1] - xlim[0]) pts[:, 0] = (pts[:, 0]) / (ylim[1] - ylim[0]) print(np.min(pts[:, 1]), np.max(pts[:, 1])) print(np.min(pts[:, 0]), np.max(pts[:, 0])) for c, ec, point in zip(color, edgecolor, magnified_points): patch = matplotlib.patches.Ellipse(point, width, height, color=c, edgecolor=ec, fill=False, linewidth=2, zorder=4) patchfake = matplotlib.patches.Ellipse([point[1], point[0]], radius, radius) ax.add_patch(patch) mask = np.logical_or(mask, patchfake.contains_points(pts)) data_masked = data.copy(deep=True) data_masked.values = np.array(data_masked.values, dtype=np.float32) cm = matplotlib.cm.get_cmap(name='viridis') cm.set_bad(color=(1, 1, 1, 0)) data_masked.values[np.swapaxes( np.logical_not(mask.reshape(data.values.shape[::-1])), 0, 1)] = np.nan aspect = ax.get_aspect() extent = [xlim[0], xlim[1], ylim[0], ylim[1]] ax.imshow(data_masked.values, cmap=cm, extent=extent, zorder=3, clim=clim, origin='lower') ax.set_aspect(aspect) for spine in ['left', 'top', 'right', 'bottom']: ax.spines[spine].set_zorder(5) if out is not None: plt.savefig(path_for_plot(out), dpi=400) return path_for_plot(out) return fig, ax
def normalize_by_fermi_dirac(data: DataType, reference_data: DataType = None, plot=False, broadening=None, temperature_axis=None, temp_offset=0, **kwargs): """ Normalizes data according to a Fermi level reference on separate data or using the same source spectrum. To do this, a linear density of states is multiplied against a resolution broadened Fermi-Dirac distribution (`arpes.fits.fit_models.AffineBroadenedFD`). We then set the density of states to 1 and evaluate this model to obtain a reference that the desired spectrum is normalized by. :param data: Data to be normalized. :param reference_data: A reference spectrum, typically a metal reference. If not provided the integrated data is used. Beware: this is inappropriate if your data is gapped. :param plot: A debug flag, allowing you to view the normalization spectrum and relevant curve-fits. :param broadening: Detector broadening. :param temperature_axis: Temperature coordinate, used to adjust the quality of the reference for temperature dependent data. :param temp_offset: Temperature calibration in the case of low temperature data. Useful if the temperature at the sample is known to be hotter than the value recorded off of a diode. :param kwargs: :return: """ reference_data = data if reference_data is None else reference_data broadening_fit = determine_broadened_fermi_distribution(reference_data, **kwargs) broadening = broadening_fit.params['conv_width'].value if broadening is None else broadening if plot: print('Gaussian broadening is: {} meV (Gaussian sigma)'.format( broadening_fit.params['conv_width'].value * 1000)) print('Fermi edge location is: {} meV (fit chemical potential)'.format( broadening_fit.params['fd_center'].value * 1000)) print('Fermi width is: {} meV (fit fermi width)'.format( broadening_fit.params['fd_width'].value * 1000)) broadening_fit.plot() offset = broadening_fit.params['offset'].value without_offset = broadening_fit.eval(offset=0) cut_index = -np.argmax(without_offset[::-1] > 0.1 * offset) cut_energy = reference_data.coords['eV'].values[cut_index] if temperature_axis is None and 'temp' in data.dims: temperature_axis = 'temp' transpose_order = list(data.dims) transpose_order.remove('eV') if temperature_axis: transpose_order.remove(temperature_axis) transpose_order = transpose_order + [temperature_axis] transpose_order = transpose_order + ['eV'] without_background = (data - data.sel(eV=slice(cut_energy, None)).mean('eV')).transpose(*transpose_order) if temperature_axis: without_background = normalize_to_spectrum(without_background) divided = without_background.T.map_axes( temperature_axis, lambda x, coord: x / broadening_fit.eval( x=x.coords['eV'].values, lin_bkg=0, const_bkg=1, offset=0, conv_width=broadening, fd_width=(coord[temperature_axis] + temp_offset) * K_BOLTZMANN_EV_KELVIN)) else: without_background = normalize_to_spectrum(without_background) divided = without_background / broadening_fit.eval( x=data.coords['eV'].values, conv_width=broadening, lin_bkg=0, const_bkg=1, offset=0) divided.coords['eV'].values = divided.coords['eV'].values - broadening_fit.params['fd_center'].value return divided