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)