Source code for abtem.prism.utils

from typing import Tuple

import numpy as np

from abtem.core.backend import get_array_module
from abtem.core.complex import complex_exponential
from abtem.core.energy import energy2wavelength
from abtem.core.utils import expand_dims_to_broadcast


[docs] def batch_crop_2d(array: np.ndarray, corners: np.ndarray, new_shape: Tuple[int, int]): xp = get_array_module(array) if len(array.shape) > 3: old_shape = array.shape batch_shape = array.shape[: -len(corners.shape) - 1] array = array.reshape((-1,) + array.shape[-2:]) corners = corners.reshape((-1, 2)) if batch_shape: assert array.shape[0] == corners.shape[0] * np.prod(batch_shape) corners = np.tile(corners, (np.prod(batch_shape), 1)) else: old_shape = None # if xp is cp: i = xp.arange(array.shape[0])[:, None, None] ix = xp.arange(new_shape[0]) + xp.asarray(corners[:, 0, None]) iy = xp.arange(new_shape[1]) + xp.asarray(corners[:, 1, None]) ix = ix[:, :, None] iy = iy[:, None] array = array[i, ix, iy] # else: # array = np.lib.stride_tricks.sliding_window_view(array, (1,) + new_shape) # array = array[xp.arange(array.shape[0]), corners[:, 0], corners[:, 1], 0] if old_shape is not None: array = array.reshape(old_shape[:-2] + array.shape[-2:]) return array
[docs] def minimum_crop(positions: np.ndarray, shape): xp = get_array_module(positions) offset = (shape[0] // 2, shape[1] // 2) corners = xp.rint(positions - xp.asarray(offset)).astype(int) upper_corners = corners + xp.asarray(shape) crop_corner = (xp.min(corners[..., 0]).item(), xp.min(corners[..., 1]).item()) size = ( xp.max(upper_corners[..., 0]).item() - crop_corner[0], xp.max(upper_corners[..., 1]).item() - crop_corner[1], ) corners -= xp.asarray(crop_corner) return crop_corner, size, corners
[docs] def wrapped_slices(start: int, stop: int, n: int) -> Tuple[slice, slice]: """Two slices covering ``[start, stop)`` on a periodic axis of length ``n``. ``start`` may be any integer. Normalising it into ``[0, n)`` first leaves only two cases -- the window fits, or it wraps once -- which is what two slices can express. The previous implementation assumed ``-n <= start < n`` and ``0 < stop <= 2n`` and split on the raw values. For a window lying entirely in negative indices (``stop <= 0``) it produced ``slice(0, stop)``, which is Python's *negative indexing* and selects ``n + stop`` elements rather than none, so the two pieces concatenated to ``n + size`` elements instead of ``size``. The mis-shaped array then surfaced as a broadcast error frames away from here. Raises ------ RuntimeError If ``stop - start > n``. Two slices cannot cover more than one period; ``wrapped_crop_2d`` catches this and takes its general path. """ size = stop - start if size > n: raise RuntimeError( f"a window of {size} elements exceeds the period {n} and cannot be " f"expressed as two slices (start = {start}, stop = {stop})" ) start %= n stop = start + size if stop <= n: return slice(start, stop), slice(0, 0) return slice(start, None), slice(0, stop - n)
def _wrapped_gather_2d( array: np.ndarray, corner: Tuple[int, int], size: Tuple[int, int] ) -> np.ndarray: """``wrapped_crop_2d`` for windows wider than one period. Gathers by modulo index, which is total for any corner and any size. It copies through fancy indexing, so it is kept off the common path -- but it is correct where the slice pair cannot be, and unlike the ``xp.pad(..., mode="wrap")`` fallback it replaced, it does not fail for windows spanning several periods. """ xp = get_array_module(array) i = (corner[0] + xp.arange(size[0])) % array.shape[-2] j = (corner[1] + xp.arange(size[1])) % array.shape[-1] return array[..., i[:, None], j[None, :]] def _check_crop_shape(cropped: np.ndarray, size: Tuple[int, int]) -> np.ndarray: """Fail here rather than at a broadcast three frames downstream. A wrongly sized crop used to reach the caller intact and only show up as an operand-shape mismatch elsewhere. The check is a shape comparison, so it costs nothing on the hot path, and it is a complete detector for this class of defect: when the slice pair is wrong it is the shape that is wrong. """ if cropped.shape[-2:] != tuple(size): raise RuntimeError( f"wrapped_crop_2d produced {cropped.shape[-2:]}, expected {tuple(size)}" ) return cropped
[docs] def wrapped_crop_2d( array: np.ndarray, corner: Tuple[int, int], size: Tuple[int, int] ) -> np.ndarray: upper_corner = (corner[0] + size[0], corner[1] + size[1]) xp = get_array_module(array) try: a, c = wrapped_slices(corner[0], upper_corner[0], array.shape[-2]) b, d = wrapped_slices(corner[1], upper_corner[1], array.shape[-1]) except RuntimeError: return _wrapped_gather_2d(array, corner, size) A = array[..., a, b] B = array[..., c, b] D = array[..., c, d] C = array[..., a, d] if A.size == 0: AB = B elif B.size == 0: AB = A else: AB = xp.concatenate([A, B], axis=-2) if C.size == 0: CD = D elif D.size == 0: CD = C else: CD = xp.concatenate([C, D], axis=-2) if CD.size == 0: return _check_crop_shape(AB, size) if AB.size == 0: cropped = CD else: cropped = xp.concatenate([AB, CD], axis=-1) return _check_crop_shape(cropped, size)
[docs] def prism_wave_vectors( cutoff: float, extent: Tuple[float, float], energy: float, interpolation: Tuple[int, int], xp=np, ) -> np.ndarray: wavelength = energy2wavelength(energy) n_max = int(np.ceil(cutoff / 1.0e3 / (wavelength / extent[0] * interpolation[0]))) m_max = int(np.ceil(cutoff / 1.0e3 / (wavelength / extent[1] * interpolation[1]))) n = np.arange(-n_max, n_max + 1, dtype=np.float32) w = np.asarray(extent[0], dtype=np.float32) m = np.arange(-m_max, m_max + 1, dtype=np.float32) h = np.asarray(extent[1], dtype=np.float32) kx = n / w * np.float32(interpolation[0]) ky = m / h * np.float32(interpolation[1]) mask = kx[:, None] ** 2 + ky[None, :] ** 2 < (cutoff / 1.0e3 / wavelength) ** 2 kx, ky = np.meshgrid(kx, ky, indexing="ij") kx = kx[mask] ky = ky[mask] return xp.asarray([kx, ky]).T
[docs] def plane_waves( wave_vectors: np.ndarray, extent: Tuple[float, float], gpts: Tuple[int, int], reverse: bool = False, ) -> np.ndarray: xp = get_array_module(wave_vectors) x = xp.linspace(0, extent[0], gpts[0], endpoint=False, dtype=np.float32) y = xp.linspace(0, extent[1], gpts[1], endpoint=False, dtype=np.float32) sign = -1.0 if reverse else 1.0 array = complex_exponential( sign * 2 * np.pi * wave_vectors[:, 0, None, None] * x[:, None] ) * complex_exponential( sign * 2 * np.pi * wave_vectors[:, 1, None, None] * y[None, :] ) return array
def _planewave_shift_coefficients(positions, wave_vectors): xp = get_array_module(positions) # wave_vectors = xp.asarray(wave_vectors) coefficients = complex_exponential( -2.0 * xp.pi * positions[..., 0, None] * wave_vectors[:, 0][None] ) # print(coefficients.shape, coefficients.dtype) coefficients *= complex_exponential( -2.0 * xp.pi * positions[..., 1, None] * wave_vectors[:, 1][None] ) return coefficients
[docs] def prism_coefficients(positions, wave_vectors, xp, ctf=None): wave_vectors = xp.asarray(wave_vectors) coefficients = _planewave_shift_coefficients(positions, wave_vectors) if ctf is not None: alpha = ( xp.sqrt(wave_vectors[:, 0] ** 2 + wave_vectors[:, 1] ** 2) * ctf.wavelength ) phi = xp.arctan2(wave_vectors[:, 0], wave_vectors[:, 1]) basis = ctf._evaluate_from_angular_grid(alpha, phi) basis, coefficients = expand_dims_to_broadcast( basis, coefficients, match_dims=[(-1,), (-1,)] ) coefficients = coefficients * basis return coefficients