ARPES Geometry and Kinematics

ARPES measures photoelectron energy and detector angles. Diffpes converts these measurements into crystal momentum with differentiable JAX functions.

Experiment Geometry

ExperimentGeometry stores one beamline and sample configuration. Every numerical field is a traced JAX leaf. The slit value is static because it selects a detector convention.

import jax.numpy as jnp

from diffpes.simul import polarization_from_angles
from diffpes.types import make_experiment_geometry

polarization = polarization_from_angles(0.75, 0.1, "p")
experiment = make_experiment_geometry(
    photon_energy_ev=50.0,
    polarization=polarization,
    sample_azimuth=0.05,
    work_function_ev=4.5,
    inner_potential_ev=12.0,
    slit="H",
)

The factory normalizes the polarization vector. This operation removes its intensity-scale gauge. The factory rejects invalid inputs during eager and compiled execution.

Crystal Coordinates

CrystalGeometry stores real-space lattice rows in Angstrom. Its reciprocal rows use inverse Angstrom and include \(2\pi\).

Diffpes uses one row-vector conversion contract:

\[ \mathbf{k}_{\mathrm{cart}}=\mathbf{k}_{\mathrm{frac}}B, \qquad \mathbf{k}_{\mathrm{frac}} =\frac{\mathbf{k}_{\mathrm{cart}}A^T}{2\pi}. \]

Use kpoints_frac_to_cart() and kpoints_cart_to_frac() for these conversions. Do not normalize fractional coordinates before conversion. A fractional direction does not represent a Cartesian direction for a non-orthogonal lattice.

Paths and Rasters

build_kpath() interpolates labeled anchors. It returns a KPath with traced fractional coordinates. kpath_arc_length() supplies the Cartesian plotting axis.

build_bz_mesh() returns a fixed-size reciprocal mesh and a first-zone mask. The mask avoids a data-dependent gather under jit. first_bz_mask() uses squared Wigner-Seitz distances and a static reciprocal shell. Its singular-value bound proves that the shell is complete for the supplied points. Increase shell_radius when a skew or anisotropic basis fails this conservative check. The function raises an error instead of returning an uncertified mask. The mesh builder also rejects a basis when reciprocal-vector inequalities cannot prove that its fixed fractional cube contains the complete first zone. Use a reduced basis in that case.

Two builders create ARPES rasters:

Both builders rotate laboratory momentum into the sample frame. Their KGrid outputs retain static raster shapes.

Free-Electron Kinematics

The three-step model gives the vacuum kinetic energy:

\[ E_{\mathrm{kin}}=h\nu-W-|E_B|. \]

kinetic_energy_ev() applies a named validity floor of 0.01 eV. Values above this floor keep their exact derivatives. Values below it receive a zero selected derivative.

The final-state magnitude is

\[ k_f=\sqrt{\frac{E_{\mathrm{kin}}} {\hbar^2/(2m_e)}}. \]

final_state_k_inv_ang() evaluates this expression in inverse Angstrom. Diffpes uses \(\hbar^2/(2m_e)=3.8099821\,\mathrm{eV\,\mathring{A}^2}\).

The free-electron inner-potential model gives

\[ k_z=\sqrt{\frac{(h\nu-W)- (\hbar^2/2m_e)k_\parallel^2+V_0} {\hbar^2/(2m_e)}}. \]

kz_from_inner_potential() returns complex \(k_z\) and a propagating-channel mask. A negative radicand produces an evanescent channel. The function does not replace that channel with a real value.

For a propagating channel,

\[ \frac{\partial k_z}{\partial V_0} =\frac{1}{2(\hbar^2/2m_e)k_z}. \]

This nonzero derivative supplies the \(V_0\) row in an experiment-design Jacobian.

Detector Coordinates

The analyzer uses two angles, tx and ty. Diffpes applies active rotations to column vectors. The slit convention is

\[ R_H=R_x(t_y)R_y(t_x), \qquad R_V=R_x(t_x)R_y(t_y). \]

The pinned Chinook comparison uses declared source-coordinate mappings. For the horizontal slit, tilt.k_mesh uses \(T=-t_x\) and \(P=t_y\). gen_all_pol uses \(\theta=-t_x\) and \(\phi=-t_y\). For the vertical slit, tilt.k_mesh uses \(T=-t_y\) and \(P=t_x\). gen_all_pol uses \(\theta=-t_y\) and \(\phi=-t_x\). The mappings give one active frame despite Chinook’s different raw signs for the horizontal Ty coordinate.

detector_rotation() constructs this shared frame. detector_angles_to_kpar() maps detector angles to parallel momentum. kpar_to_detector_angles() provides the inverse inside the physical disk \(|k_\parallel|<k_f\).

emission_angles() converts Cartesian momentum to polar and azimuthal angles. Azimuth is undefined at normal emission. The function returns a guarded value there, but derivative tests exclude that point.

Polarization Frame

polarization_from_angles() constructs s, p, circular, or linear polarization. It returns the complex Cartesian amplitude without an intensity reduction.

rotate_polarization_grid() applies the detector frame to each complex vector. rotate_frame_vectors() applies the same operation to real vectors, including future spin axes.

The spherical components use order \((q=-1,0,+1)\):

\[ \epsilon_{-1}=\frac{\epsilon_x-i\epsilon_y}{\sqrt{2}}, \quad \epsilon_0=\epsilon_z, \quad \epsilon_{+1}=-\frac{\epsilon_x+i\epsilon_y}{\sqrt{2}}. \]

polarization_to_spherical() performs this complex-linear transform. It preserves \(\sum_q|\epsilon_q|^2\).

Information Flow

Geometry fields stay inside the JAX graph. A photon-energy scan can therefore differentiate \(k_z\) with respect to \(V_0\), \(W\), and every photon-energy point.

import jax
import jax.numpy as jnp

from diffpes.simul import kz_from_inner_potential

k_parallel = jnp.linspace(0.0, 1.0, 8)
photon_energy = jnp.linspace(25.0, 100.0, 6)

def scan(parameters):
    v0 = parameters[0]
    work_function = parameters[1]
    energies = parameters[2:]
    kz, _ = jax.vmap(
        lambda energy: kz_from_inner_potential(
            energy,
            work_function,
            v0,
            k_parallel,
        )
    )(energies)
    return jnp.real(kz)

parameters = jnp.concatenate((jnp.array([12.0, 4.5]), photon_energy))
jacobian = jax.jacfwd(scan)(parameters)

The work function also shifts the kinetic-energy reference. A simultaneous Fermi-level offset can compensate that role. This direction is the \(W\leftrightarrow E_F\) gauge. A photon-energy scan adds separate \(k_z\) information and can reduce the gauge.

See Geometry and kinematics for a complete executable example.