Source code for abtem.core.complex

import dask.array as da
import numba as nb  # type: ignore
import numpy as np

from abtem.core.backend import check_cupy_is_installed, cp


@nb.vectorize(
    [nb.complex64(nb.float32), nb.complex128(nb.float64), nb.complex64(nb.complex64)]
)
def _complex_exponential(x):
    """
    Calculate the complex exponential.

    The ``cos(x) + i sin(x)`` form is fast for real ``x`` but overflows for
    complex ``x`` with a large imaginary part (``cosh``/``sinh`` blow up), so
    complex input is routed to ``exp`` in :func:`complex_exponential` instead.
    """
    return np.cos(x) + 1.0j * np.sin(x)


@nb.vectorize([nb.float32(nb.complex64), nb.float64(nb.complex128)])
def _abs2(x):
    """
    Calculate the absolute square of a complex number.
    """
    return x.real**2 + x.imag**2


if cp is not None:
    _abs2_cupy_float32 = cp.ElementwiseKernel(
        in_params="float32 x, float32 y",
        out_params="float32 z",
        operation="z = x * x + y * y",
        name="abs_squared",
    )
    _abs2_cupy_float64 = cp.ElementwiseKernel(
        in_params="float64 x, float64 y",
        out_params="float64 z",
        operation="z = x * x + y * y",
        name="abs_squared",
    )
    # Fused kernels: compute exp(i*x) = cos(x) + i*sin(x) directly from a
    # real-valued input, avoiding the intermediate complex allocation that
    # cp.exp(1j*x) would create.
    _complex_exp_cupy_float32 = cp.ElementwiseKernel(
        in_params="float32 x",
        out_params="complex64 z",
        operation="z = thrust::complex<float>(cosf(x), sinf(x))",
        name="complex_exp_float32",
    )
    _complex_exp_cupy_float64 = cp.ElementwiseKernel(
        in_params="float64 x",
        out_params="complex128 z",
        operation="z = thrust::complex<double>(cos(x), sin(x))",
        name="complex_exp_float64",
    )
    # Fused kernels: compute exp(i * scale * x) in a single GPU pass, avoiding
    # the intermediate real allocation that `complex_exponential(scale * x)`
    # would create (saves one slice-sized buffer per chunk).
    _complex_exp_scaled_cupy_float32 = cp.ElementwiseKernel(
        in_params="float32 x, float32 scale",
        out_params="complex64 z",
        operation="z = thrust::complex<float>(cosf(scale * x), sinf(scale * x))",
        name="complex_exp_scaled_float32",
    )
    _complex_exp_scaled_cupy_float64 = cp.ElementwiseKernel(
        in_params="float64 x, float64 scale",
        out_params="complex128 z",
        operation="z = thrust::complex<double>(cos(scale * x), sin(scale * x))",
        name="complex_exp_scaled_float64",
    )
else:
    _abs2_cupy_float32 = None
    _abs2_cupy_float64 = None
    _complex_exp_cupy_float32 = None
    _complex_exp_cupy_float64 = None
    _complex_exp_scaled_cupy_float32 = None
    _complex_exp_scaled_cupy_float64 = None


[docs] def abs2(x: np.ndarray | da.core.Array) -> np.ndarray | da.core.Array: """ Fast calculation of the absolute square of a complex number. """ if isinstance(x, np.ndarray): return _abs2(x) if isinstance(x, da.core.Array): return da.map_blocks(abs2, x) check_cupy_is_installed() # type: ignore if isinstance(x, cp.ndarray): if x.dtype == "complex64": return _abs2_cupy_float32(x.real, x.imag) elif x.dtype == "complex128": return _abs2_cupy_float64(x.real, x.imag) else: raise RuntimeError( "Unsupported dtype for calculation of abs2 with cupy array." ) raise ValueError("abs2 only supports numpy arrays, dask arrays and cupy arrays.")
[docs] def complex_exponential(x: np.ndarray | da.core.Array) -> np.ndarray | da.core.Array: """ Fast calculation of the complex exponential. """ if isinstance(x, np.ndarray): if np.iscomplexobj(x): # cos(z) + i sin(z) overflows for large |Im z| (strongly evanescent # propagator components); exp is overflow-safe and decays to zero. return np.exp(1.0j * x) return _complex_exponential(x) if isinstance(x, da.core.Array): return da.map_blocks(complex_exponential, x) check_cupy_is_installed() # type: ignore if isinstance(x, cp.ndarray): if x.dtype == np.float32: return _complex_exp_cupy_float32(x) elif x.dtype == np.float64: return _complex_exp_cupy_float64(x) else: # Fallback for complex or other dtypes (e.g. already-complex input) return cp.exp(1.0j * x) raise ValueError( "complex_exponential only supports numpy arrays, dask arrays and cupy arrays." )
[docs] def complex_exponential_scaled( x: np.ndarray | da.core.Array, scale: float ) -> np.ndarray | da.core.Array: """Compute ``exp(i * scale * x)`` without creating a temporary real array. On GPU, the scale multiplication is fused into the sin/cos kernel, saving one slice-sized real buffer allocation per chunk compared to the two-step ``complex_exponential(scale * x)``. On CPU, falls back to the equivalent two-step computation (NumPy/Numba). Parameters ---------- x : array-like Real-valued input array. scale : float Scalar multiplier applied before the exponential. """ if isinstance(x, np.ndarray): return _complex_exponential(x.dtype.type(scale) * x) if isinstance(x, da.core.Array): return da.map_blocks(complex_exponential_scaled, x, scale=scale) check_cupy_is_installed() # type: ignore if isinstance(x, cp.ndarray): if x.dtype == np.float32: return _complex_exp_scaled_cupy_float32(x, cp.float32(scale)) elif x.dtype == np.float64: return _complex_exp_scaled_cupy_float64(x, cp.float64(scale)) else: return cp.exp(1.0j * scale * x) raise ValueError( "complex_exponential_scaled only supports numpy arrays, dask arrays and cupy arrays." )