Esempio n. 1
0
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
Esempio n. 2
0
    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)
Esempio n. 4
0
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
Esempio n. 6
0
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)
Esempio n. 8
0
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
Esempio n. 9
0
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
Esempio n. 10
0
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
Esempio n. 11
0
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
Esempio n. 13
0
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
Esempio n. 14
0
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
Esempio n. 15
0
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
Esempio n. 17
0
def normalize_total(data: DataType):
    data = normalize_to_spectrum(data)

    return data / (data.sum(data.dims) / 1000000)
Esempio n. 18
0
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)
Esempio n. 19
0
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
Esempio n. 21
0
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