Source code for toolbox_scs.detectors.azimuthal_integrator

import logging
import numpy as np

__all__ = [
    'AzimuthalIntegrator',
    'AzimuthalIntegratorDSSC'
]

log = logging.getLogger(__name__)


[docs]class AzimuthalIntegrator(object): def __init__(self, imageshape, center, polar_range, aspect=204/236, **kwargs): ''' Create a reusable integrator for repeated azimuthal integration of similar images. Calculates array indices for a given parameter set that allows fast recalculation. Parameters ========== imageshape : tuple of ints The shape of the images to be integrated over. center : tuple of ints center coordinates in pixels polar_range : tuple of ints start and stop polar angle (in degrees) to restrict integration to wedges dr : int, optional radial width of the integration slices. Takes non-square DSSC pixels into account. nrings : int, optional Number of integration rings. Can be given as an alternative to dr aspect: float, default 204/236 for DSSC aspect ratio of the pixel pitch Returns ======= ai : azimuthal_integrator instance Instance can directly be called with image data: > az_intensity = ai(image) radial distances and the polar mask are accessible as attributes: > ai.distance > ai.polar_mask ''' self.xcoord = None self.ycoord = None self._calc_dist_array(imageshape, center, aspect) self._calc_polar_mask(polar_range) self._calc_indices(**kwargs)
[docs] def _calc_dist_array(self, shape, center, aspect): '''Calculate pixel coordinates for the given shape.''' self.center = center self.shape = shape self.aspect = aspect cx, cy = center log.info(f'azimuthal center: {center}') sx, sy = shape xcoord, ycoord = np.ogrid[:sx, :sy] self.xcoord = xcoord - cx self.ycoord = ycoord - cy # distance from center, hexagonal pixel shape taken into account self.dist_array = np.hypot(self.xcoord * aspect, self.ycoord)
[docs] def _calc_indices(self, **kwargs): '''Calculates the list of indices for the flattened image array.''' maxdist = self.dist_array.max() mindist = self.dist_array.min() dr = kwargs.get('dr', None) nrings = kwargs.get('nrings', None) if (dr is None) and (nrings is None): raise AssertionError('Either <dr> or <nrings> needs to be given.') if (dr is not None) and (nrings is not None): log.warning('Both <dr> and <nrings> given. <dr> takes precedence.') if (dr is None): dr = maxdist / nrings idx = np.indices(dimensions=self.shape) self.index_array = np.ravel_multi_index(idx, self.shape) self.distance = np.array([]) self.flat_indices = [] for dist in np.arange(mindist, maxdist + dr, dr): ring_mask = (self.polar_mask * (self.dist_array >= (dist - dr)) * (self.dist_array < dist)) self.flat_indices.append(self.index_array[ring_mask]) self.distance = np.append(self.distance, dist)
[docs] def _calc_polar_mask(self, polar_range): self.polar_range = polar_range prange = np.abs(polar_range[1] - polar_range[0]) if prange > 180: raise ValueError('Integration angle too wide, should be within 180' ' degrees') if prange < 1e-6: raise ValueError('Integration angle too narrow') if prange == 180: self.polar_mask = np.ones(self.shape, dtype=bool) else: tmin, tmax = np.deg2rad(np.sort(polar_range)) % np.pi polar_array = np.arctan2(self.xcoord, self.ycoord) polar_array = np.mod(polar_array, np.pi) self.polar_mask = (polar_array > tmin) * (polar_array < tmax)
[docs] def calc_q(self, distance, wavelength): '''Calculate momentum transfer coordinate. Parameters ========== distance : float Sample - detector distance in meter wavelength : float wavelength of scattered light in meter Returns ======= deltaq : np.ndarray Momentum transfer coordinate in 1/m ''' res = 4 * np.pi \ * np.sin(np.arctan(self.distance / distance) / 2) / wavelength return res
[docs] def __call__(self, image): assert self.shape == image.shape, 'image shape does not match' image_flat = np.ravel(image) return np.array([np.nansum(image_flat[indices]) for indices in self.flat_indices])
[docs]class AzimuthalIntegratorDSSC(AzimuthalIntegrator): def __init__(self, geom, polar_range, dxdy=(0, 0), **kwargs): ''' Create a reusable integrator for repeated azimuthal integration of similar images. Calculates array indices for a given parameter set that allows fast recalculation. Directly uses a extra_geom.detectors.DSSC_1MGeometry instance for correct pixel positions Parameters ========== geom : extra_geom.detectors.DSSC_1MGeometry loaded geometry instance polar_range : tuple of ints start and stop polar angle (in degrees) to restrict integration to wedges dr : int, optional radial width of the integration slices. Takes non-square DSSC pixels into account. nrings : int, optional Number of integration rings. Can be given as an alternative to dr dxdy : tuple of floats, default (0, 0) global coordinate shift to adjust center outside of geom object (meter) Returns ======= ai : azimuthal_integrator instance Instance can directly be called with image data: > az_intensity = ai(image) radial distances and the polar mask are accessible as attributes: > ai.distance > ai.polar_mask ''' self.xcoord = None self.ycoord = None self._calc_dist_array(geom, dxdy) self._calc_polar_mask(polar_range) self._calc_indices(**kwargs)
[docs] def _calc_dist_array(self, geom, dxdy): self.dxdy = dxdy pos = geom.get_pixel_positions() self.shape = pos.shape[:-1] self.xcoord = pos[..., 0] + dxdy[0] self.ycoord = pos[..., 1] + dxdy[1] self.dist_array = np.hypot(self.xcoord, self.ycoord)