Source code for arpes.utilities.conversion.base

"""Infrastructure code for defining coordinate transforms and momentum conversion."""

from __future__ import annotations

from abc import abstractmethod
from logging import DEBUG, INFO
from typing import TYPE_CHECKING

import numpy as np
import xarray as xr

from arpes.debug import setup_logger
from arpes.xarray_extensions.accessor.spectrum_type import AngleUnit

if TYPE_CHECKING:
    from collections.abc import Callable, Hashable

    from _typeshed import Incomplete
    from numpy.typing import NDArray

    from .calibration import DetectorCalibration

__all__ = ["K_SPACE_BORDER", "MOMENTUM_BREAKPOINTS", "CoordinateConverter"]

LOGLEVELS = (DEBUG, INFO)
LOGLEVEL = LOGLEVELS[1]
logger = setup_logger(__name__, LOGLEVEL)

K_SPACE_BORDER = 0.02
MOMENTUM_BREAKPOINTS: list[float] = [
    0.0005,
    0.001,
    0.002,
    0.005,
    0.01,
    0.02,
    0.05,
    0.1,
    0.2,
    0.5,
    1,
]


[docs] class CoordinateConverter: """Infrastructure code to support a new coordinate conversion routine. In order to do coordinate conversion from c_i to d_i, we need to give functions c_i(d_j), i.e. to implement the inverse transform. This is so that we convert by interpolating the function from a regular grid of d_i values back to the original data expressed in c_i. From this, we can see what responsibilities these conversion classes hold: * They need to specify how to calculate c_i(d_j) * They need to cache computations so that computations of c_i(d_j) can be performed efficiently for different coordinates c_i * Because they know how to do the inverse conversion, they need to know how to choose reasonable grid bounds for the forward transform, so that this can be handled automatically. These different roles and how they are accomplished are discussed in detail below. """
[docs] def __init__( self, arr: xr.DataArray, dim_order: list[str] | None = None, *, calibration: DetectorCalibration | None = None, ) -> None: """Intern the volume so that we can check on things during computation.""" self.arr = arr self.dim_order = dim_order self.calibration = calibration self.phi: NDArray[np.floating] | None = None
@staticmethod @abstractmethod def prep(arr: xr.DataArray) -> None: """Perform preprocessing of the array to convert before we start. The CoordinateConverter.prep method allows you to pre-compute some transformations that are common to the individual coordinate transform methods as an optimization. This is useful if you want the conversion methods to have separation of concern, but if it is advantageous for them to be able to share a computation of some variable. An example of this is in BE-kx-ky conversion, where computing k_p_tot is a step in both converting kx and ky, so we would like to avoid doing it twice. Of course, you can neglect this function entirely. Another technique is to simple cache computations as they arrive. This is the technique that is used in ConvertKxKy below """ assert isinstance(arr, xr.DataArray) @property def is_slit_vertical(self) -> bool: """For hemispherical analyzers, whether the slit is vertical or horizontal. This is an ARPES specific detail, so this conversion code is not strictly general, but a future refactor could just push these details to a subclass. """ # 89 - 91 degrees angle_tolerance = 1.0 angle_unit = self.arr.S.angle_unit alpha = self.arr.S.lookup_offset_coord("alpha") return ( float(np.abs(alpha - 90.0)) < angle_tolerance if angle_unit is AngleUnit.DEG else np.abs(alpha - np.pi / 2) < np.deg2rad(angle_tolerance) ) @staticmethod def kspace_to_BE( binding_energy: NDArray[np.floating], *args: NDArray[np.floating], ) -> NDArray[np.floating]: """The energy conservation equation for ARPES. This does not depend on any details of the angular conversion (it's the identity) so we can put the conversion code here in the base class. """ if args: pass return binding_energy @abstractmethod def conversion_for( self, dim: Hashable, ) -> Callable[[NDArray[np.floating]], NDArray[np.floating]]: """Fetches the method responsible for calculating `dim` from momentum coordinates.""" def identity_transform(self, axis_name: Hashable, *args: Incomplete) -> NDArray[np.floating]: """Just returns the coordinate requested from args. Useful if the transform is the identity. """ assert isinstance(self.dim_order, (list | tuple)), ( f"self.dim_oder should be list | tuple, but {type(self.dim_order)}" ) return args[self.dim_order.index(str(axis_name))] @abstractmethod def get_coordinates( self, resolution: dict[str, float] | None = None, bounds: dict[str, tuple[float, float]] | None = None, ) -> dict[Hashable, NDArray[np.floating]]: """Calculates the coordinates which should be used in momentum space. Args: resolution(dict): Represents conversion resolution key: momentum name, such as "kp", value: resolution, typical value is 0.001 bounds(dict, optional): bounds of the momentum coordinates Returns: dict[str, NDArray[np.float] Object that is to be used the coordinates in the momentum converted data. Thus the keys are "kp", "kx", and "eV", but not "phi" """