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:
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:
build_arpes_kmesh()creates a fixed-\(k_z\) map.build_kmesh_hv()creates a \((k_\parallel,h\nu)\) map.
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:
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
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
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,
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
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)\):
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.