diffpes.maths¶
Differentiable Cartesian rotations and angular matrix elements for dipole photoemission.
Compute angular matrix elements for dipole photoemission.
Extended Summary¶
The subpackage provides Gaunt coefficients and real spherical harmonics. It also assembles the full dipole matrix element for the differentiable ARPES forward model. The dipole matrix element for orbital \((n, l, m)\) combines radial integrals, Gaunt coefficients, and spherical harmonics of the photoelectron direction:
The following list describes the submodules:
dipoleAssemble full dipole matrix elements.
gauntBuild the Gaunt coefficient table for dipole transitions.
rotationsConstruct differentiable three-dimensional rotations.
safeProvide named gradient-safe elementary operations.
spherical_harmonicsCompute real spherical harmonics in JAX.
Routine Listings¶
GAUNT_TABLEModule-level precomputed Gaunt coefficient table for l_max=4.
build_gaunt_table()Build the dipole Gaunt coefficient lookup table.
dipole_intensities_all_orbitals()Compute
|M|^2for all orbitals in the basis.dipole_intensity_orbital()Compute
|M|^2for one orbital.dipole_matrix_element_single()Compute dipole matrix element for a single orbital (n, l, m).
gaunt_lookup()Look up a single Gaunt coefficient from the precomputed table.
real_spherical_harmonic()Evaluate a single real spherical harmonic.
real_spherical_harmonics_all()Evaluate all real spherical harmonics up to l_max.
rodrigues_rotation()Construct a rotation matrix with Rodrigues’ formula.
safe_arccos()Evaluate arccos with saturated values and zero boundary gradients.
safe_arctan2()Evaluate arctan2 with a zero value and gradient at the origin.
safe_divide()Divide with a fallback and zero quotient gradients at zero denominators.
safe_log()Evaluate log with a finite floor and zero gradients below it.
safe_norm()Compute a Euclidean norm with a zero gradient at zero vectors.
safe_power()Raise positive inputs to a power and return zero otherwise.
safe_sqrt()Evaluate sqrt on positive inputs and return zero otherwise.
Notes
All functions support JAX transformations and automatic differentiation. Pure Python computes the Gaunt table once during import. The module stores the table as a JAX array for constant-time lookup during traced computation.
- diffpes.maths.dipole_intensities_all_orbitals(k_vec: Float[Array, '3'], r_grid: Float[Array, 'R'], slater_params: SlaterParams, efield: Complex[Array, '3']) Float[Array, 'O'][source]¶
Compute
|M|^2for all orbitals in the basis.The function scans every orbital in the Slater basis. It computes each radial wavefunction from
slater_paramsand then computes the squared dipole matrix element.jax.lax.switchspecializes one branch for each static(n, l, m)tuple.jax.lax.scancarries the differentiable arrays along the orbital axis.For each orbital o, the function:
Extracts quantum numbers \((n_o, l_o, m_o)\) and Slater exponent \(\zeta_o\) from
slater_params.Evaluates the normalized Slater radial function \(R(r) = N r^{n-1} e^{-\zeta r}\) on the supplied grid.
Weights by the multi-zeta coefficient
slater_params.coefficients[o, 0](first column for single-zeta bases).Calls dipole_intensity_orbital to compute \(|M|^2\).
The function stacks the results into a one-dimensional array. Its length equals the number of orbitals in the basis.
- See:
- Parameters:
k_vec (
Float[Array, '3']) – Photoelectron wavevector.r_grid (
Float[Array, 'R']) – Radial grid.slater_params (
SlaterParams) – Slater exponents and orbital basis.efield (
Complex[Array, '3']) – Polarization vector.
- Returns:
intensities –
|M|^2per orbital.- Return type:
Float[Array, 'O']
Notes
The number and quantum numbers of orbitals remain static PyTree metadata, so changing the basis structure retraces the function. Slater exponents and coefficients remain traced leaves with gradients through the scan.
- diffpes.maths.dipole_intensity_orbital(k_vec: Float[Array, '3'], r_grid: Float[Array, 'R'], radial_values: Float[Array, 'R'], l: int, m: int, efield: Complex[Array, '3']) Float[Array, ''][source]¶
Compute
|M|^2for one orbital.The function computes the photoemission intensity for one initial-state orbital. The quantum numbers (l, m) and the sampled radial wavefunction define this orbital. The intensity is the squared modulus of the complex dipole matrix element:
\[I(\mathbf{k}) = |M(\mathbf{k}, l, m)|^2\]This wrapper calls dipole_matrix_element_single and returns \(|M|^2 = M \cdot M^*\). The construction gives a real, nonnegative result. JAX differentiates the result with respect to all continuous inputs.
- See:
- Parameters:
- Returns:
intensity – Squared modulus of the matrix element.
- Return type:
Float[Array, '']
Notes
The function computes the complex matrix element first. It then applies the modulus squared once, after the coherent channel sum is complete. Gradients flow through both operations for every continuous input.
- diffpes.maths.dipole_matrix_element_single(k_vec: Float[Array, '3'], r_grid: Float[Array, 'R'], radial_values: Float[Array, 'R'], l: int, m: int, efield: Complex[Array, '3']) Complex[Array, ''][source]¶
Compute dipole matrix element for a single orbital (n, l, m).
\[M = \sum_{q} \hat{e}_q \sum_{l'} B^{l'}(|k|) \cdot G(l, m, l', m+q) \cdot Y_{l'}^{m+q}(\hat{k})\]where the sum is over dipole components q in {-1, 0, +1} and final-state angular momenta l’ in {l-1, l+1}.
The function assembles the full photoemission dipole matrix element from four quantities:
Radial integral \(B^{l'}(|k|)\) – the overlap between the initial radial wavefunction and the final-state spherical Bessel function \(j_{l'}(kr)\). The radial integral applies the \(r^3\) weight and trapezoidal quadrature.
Gaunt coefficient \(G(l, m, l', m')\) – the angular integral that couples the initial and final states through the dipole operator. The function reads this coefficient from
GAUNT_TABLE.Real spherical harmonic \(Y_{l'}^{m'}(\hat{k})\) – the angular part of the final-state plane wave expansion. The function computes it at the direction of the photoelectron wavevector.
Polarization component \(\hat{e}_q\) – the q-th spherical component of the polarization vector. The _cartesian_to_spherical_dipole function maps the Cartesian components to the real harmonic basis.
The dipole selection rule \(l' = l \pm 1\) restricts the final-state sum to at most two terms per q value. The magnetic selection rule \(m' = m + q\) with \(|q| \le 1\) means at most three q values contribute.
Numerical stability routes the wavevector norm, normalization, polar angle, and azimuthal angle through the named safe-math primitives. Their guard conventions select zero subgradients at the zero vector and at the angular coordinate singularities without perturbing non-singular values.
- See:
- Parameters:
k_vec (
Float[Array, '3']) – Photoelectron wavevector in Cartesian coordinates.r_grid (
Float[Array, 'R']) – Radial grid for integration.radial_values (
Float[Array, 'R']) – R(r) sampled on r_grid.l (
int) – Angular momentum quantum number of the initial orbital.m (
int) – Magnetic quantum number of the initial orbital.efield (
Complex[Array, '3']) – Polarization vector in Cartesian coordinates.
- Returns:
M – Complex dipole matrix element.
- Return type:
Complex[Array, '']
Notes
Python unrolls the loop over q and l’ during tracing. Static quantum numbers determine the iteration bounds. This process produces one fixed computation graph for each (l, m) pair. Different orbitals therefore produce distinct XLA programs.
- diffpes.maths.build_gaunt_table(l_max: int = 4) Float[Array, 'L_src M_src 3 L_dst M_dst'][source]¶
Build the dipole Gaunt coefficient lookup table.
The function computes each nonzero real Gaunt coefficient through
l_max. It stores the coefficients in a dense five-dimensional NumPy array. It then converts the table to a JAX array for constant-time lookup.The five axes correspond to:
axis 0 (
l): initial angular momentum, sizel_max + 1.axis 1 (
m + l_max): offset initial magnetic quantum number, size2 * l_max + 1, so that m = -l_max maps to index 0.axis 2 (
q + 1): dipole component index, size 3, for q in {-1, 0, +1}.axis 3 (
lp): final angular momentum, sizel_max + 2(because the dipole operator can promote l to l + 1).axis 4 (
mp + l_max): offset final magnetic quantum number, size2 * (l_max + 1) + 1.
The construction visits each valid combination of quantum numbers. It applies the dipole selection rules and calls _real_gaunt_dipole. The initialized zero values represent forbidden transitions.
Use the following expression to index the table:
GAUNT_TABLE[l, m + l_max, q + 1, lp, mp + l_max]where q in {-1, 0, +1} indexes the three dipole components.- See:
Implementation Logic¶
Allocate the dense coefficient table:
table: Float[NDArray, "L1 M1 Q L2 M2"] = np.zeros( (l_src_dim, m_src_dim, q_dim, l_dst_dim, m_dst_dim), dtype=np.float64, )
The dense layout gives constant-time lookup for valid quantum numbers.
Convert the completed table to JAX:
gaunt_table: Float[ Array, "L_src M_src 3 L_dst M_dst" ] = jnp.asarray(table, dtype=jnp.float64)
The JAX constant can participate in differentiable forward models.
- param l_max:
Maximum angular momentum. Default 4 (s through g).
- type l_max:
int, default:4- returns:
gaunt_table – Dense array of Gaunt coefficients. Shape:
(l_max+1, 2*l_max+1, 3, l_max+2, 2*(l_max+1)+1).- rtype:
Float[Array, 'L_src M_src 3 L_dst M_dst']
Notes
The function uses pure Python and NumPy because it runs once during module import. The module stores the result as a JAX array constant. Therefore, gradient computations do not include it as a trainable parameter.
- diffpes.maths.gaunt_lookup(l: int, m: int, q: int, lp: int, mp: int) float[source]¶
Look up a single Gaunt coefficient from the precomputed table.
The function provides an accessor for the module-level
GAUNT_TABLE. It converts the physical quantum numbers into offsets for the dense storage layout. The index mapping is:lindexes axis 0 directly.m + L_MAXoffsets the magnetic quantum number to a non-negative index on axis 1.q + 1maps q in {-1, 0, +1} to indices {0, 1, 2} on axis 2.lpindexes axis 3 directly.mp + L_MAXoffsets m’ on axis 4.
The function converts the returned value to a Python float for non-JAX contexts. In JAX-traced code, index
GAUNT_TABLEdirectly to avoid Python overhead.- See:
Implementation Logic¶
Index and convert the coefficient:
coefficient: float = float( GAUNT_TABLE[l, m + L_MAX, q + 1, lp, mp + L_MAX] )
The offsets map signed quantum numbers to dense array indices.
- param l:
Initial state angular momentum.
- type l:
- param m:
Initial state magnetic quantum number.
- type m:
- param q:
Dipole component (-1, 0, or +1).
- type q:
- param lp:
Final state angular momentum.
- type lp:
- param mp:
Final state magnetic quantum number.
- type mp:
- returns:
coefficient – The Gaunt coefficient.
- rtype:
- diffpes.maths.rodrigues_rotation(axis: Float[Array, '3'], angle: float | Float[Array, '']) Float[Array, '3 3'][source]¶
Construct a rotation matrix with Rodrigues’ formula.
The function safely normalizes the rotation axis. It then constructs an active rotation matrix for column vectors.
- See:
- Parameters:
axis (
Float[Array, '3']) – Rotation axis in Cartesian coordinates.angle (
Union[float,Float[Array, '']]) – Active rotation angle in radians.
- Returns:
rotation – Active Cartesian rotation matrix.
- Return type:
Float[Array, '3 3']
Notes
The function uses \(R = I + \sin(\alpha)[\hat n]_\times + (1 - \cos(\alpha))[\hat n]_\times^2\). The safe normalization gives the identity matrix for a zero axis.
- diffpes.maths.safe_arccos(x: Float[Array, '...']) Float[Array, '...'][source]¶
Evaluate arccos with saturated values and zero boundary gradients.
Computes real angles on the closed cosine range. Values outside the range use the nearest physical endpoint.
- See:
- Parameters:
x (
']) – Real cosine values.- Returns:
angles – Angles in radians, saturated to
pibelow -1 and zero above 1.- Return type:
']
Notes
Inputs strictly inside
(-1, 1)use the ordinaryarccosoperation. Constants supply values at or beyond either endpoint. This selection gives zero subgradients and avoids the infinite endpoint derivative.
- diffpes.maths.safe_arctan2(y: Float[Array, '...'], x: Float[Array, '...']) Float[Array, '...'][source]¶
Evaluate arctan2 with a zero value and gradient at the origin.
Computes four-quadrant angles for broadcast-compatible Cartesian coordinates. The origin uses an explicit boundary convention.
- See:
- Parameters:
y (
']) – Vertical coordinates.x (
']) – Horizontal coordinates broadcast-compatible withy.
- Returns:
angles – Four-quadrant angles in radians, with zero at
(0, 0).- Return type:
']
Notes
At the indeterminate origin, sanitized coordinates
(0, 1)keep the inactive branch finite. The selected value and both coordinate subgradients at the origin are zero.
- diffpes.maths.safe_divide(numerator: Float[Array, '...'], denominator: Float[Array, '...'], fallback: float | Float[Array, ''] = 0.0) Float[Array, '...'][source]¶
Divide with a fallback and zero quotient gradients at zero denominators.
Applies elementwise guarded division to broadcast-compatible real arrays. The fallback defines the value on the zero-denominator boundary.
- See:
- Parameters:
numerator (
']) – Dividend array.denominator (
']) – Divisor array broadcast-compatible withnumerator.fallback (
Union[float,Float[Array, '']], default:0.0) – Value returned whereverdenominatoris zero.
- Returns:
quotient – Broadcast quotient with
fallbackat zero denominators.- Return type:
']
Notes
The function replaces a zero denominator with one before it evaluates the inactive division branch. At this boundary, both quotient operands have zero selected subgradients. A traced
fallbackretains its usual selected-value gradient.
- diffpes.maths.safe_log(x: Float[Array, '...'], floor: float | Float[Array, ''] = 1e-300) Float[Array, '...'][source]¶
Evaluate log with a finite floor and zero gradients below it.
Computes the natural logarithm on a positive guarded domain. The floor keeps the returned values finite for small or nonpositive inputs.
- See:
- Parameters:
x (
']) – Real input array.floor (
Union[float,Float[Array, '']], default:1e-300) – Positive lower bound used before taking the logarithm.
- Returns:
logarithms – Natural logarithms of
maximum(x, floor).- Return type:
']
Notes
The function replaces inputs at or below the positive floor before it computes the logarithm. Their selected subgradient with respect to
xis zero.
- diffpes.maths.safe_norm(x: Float[Array, '... n'], axis: int = -1, keepdims: bool = False) Float[Array, '...'][source]¶
Compute a Euclidean norm with a zero gradient at zero vectors.
Reduces the selected vector axis with a guarded square root. The operation supports batched vectors and an optional retained axis.
- See:
- Parameters:
x (
n']) – Real vectors.axis (
int, default:-1) – (static — a compile-time constant; changing it triggers retracing) Axis containing vector components.keepdims (
bool, default:False) – (static — a compile-time constant; changing it triggers retracing) Whether the reduced axis remains with length one.
- Returns:
norms – Euclidean norms reduced along
axis.- Return type:
']
Notes
The function passes the squared norm to
safe_sqrt(). A zero vector has value zero and a zero selected gradient.
- diffpes.maths.safe_power(x: Float[Array, '...'], exponent: float | Float[Array, '']) Float[Array, '...'][source]¶
Raise positive inputs to a power and return zero otherwise.
Computes real powers on positive bases for arbitrary real exponents. A guarded branch keeps fractional powers outside that domain finite.
- See:
- Parameters:
x (
']) – Real bases.exponent (
Union[float,Float[Array, '']]) – Real exponent, including non-integer values.
- Returns:
powers –
x**exponentfor positivexand zero otherwise.- Return type:
']
Notes
The function replaces nonpositive bases with one before exponentiation. This replacement prevents complex or invalid results from fractional powers. Both inputs have zero selected subgradients on the guarded set.
- diffpes.maths.safe_sqrt(x: Float[Array, '...']) Float[Array, '...'][source]¶
Evaluate sqrt on positive inputs and return zero otherwise.
Applies the principal real square root only on its positive domain. The guarded branch defines a finite value outside that domain.
- See:
- Parameters:
x (
']) – Real input array.- Returns:
roots – Principal square roots, with zero for
x <= 0.- Return type:
']
Notes
The function replaces nonpositive inputs with one before it computes the square root. The selected value and subgradient are zero for
x <= 0.
- diffpes.maths.real_spherical_harmonic(l: int, m: int, theta: Float[Array, '...'], phi: Float[Array, '...']) Float[Array, '...'][source]¶
Evaluate a single real spherical harmonic.
The function computes \(Y_l^m(\theta, \varphi)\).
The function computes the real-valued spherical harmonic with the Condon-Shortley phase convention:
\[ \begin{align}\begin{aligned}Y_l^m(\theta, \varphi) = \sqrt{2} \, N_l^{|m|} \, P_l^{|m|}(\cos\theta) \, \cos(m\varphi) \quad (m > 0)\\Y_l^0(\theta, \varphi) = N_l^0 \, P_l^0(\cos\theta)\\Y_l^m(\theta, \varphi) = (-1)^{|m|} \sqrt{2} \, N_l^{|m|} \, P_l^{|m|}(\cos\theta) \, \sin(|m|\varphi) \quad (m < 0)\end{aligned}\end{align} \]For negative m, the \((-1)^{|m|}\) factor cancels the Condon-Shortley phase. The _associated_legendre_plm function includes this phase in \(P_l^{|m|}\). The result follows the real-to-complex convention of the Gaunt table. Consequently, the complex-basis Gaunt coefficients match direct integrals of these real harmonics.
The _normalization function computes \(N_l^{|m|}\). It uses exact integer arithmetic for the factorial ratio.
JAX can transform this function and differentiate it with respect to
thetaandphi.Implementation Logic¶
Compute the associated Legendre values:
cos_theta: Float[Array, " ..."] = jnp.cos(theta) am: int = abs(m) plm: Float[Array, " ..."] = _associated_legendre_plm( l, am, cos_theta )
These values provide the angular basis for each real branch.
Select the real harmonic branch:
if m > 0: ylm = jnp.sqrt(2.0) * norm * plm * jnp.cos(m * phi) return ylm
The sign of
mselects the cosine, constant, or sine basis.
- param l:
Degree (0 <= l).
- type l:
- param m:
Order (-l <= m <= l).
- type m:
- param theta:
Polar angle from z-axis in radians.
- type theta:
']- param phi:
Azimuthal angle in radians.
- type phi:
']- returns:
ylm – Real spherical harmonic values.
- rtype:
']- raises ValueError:
If
lis negative orabs(m)exceedsl.
- diffpes.maths.real_spherical_harmonics_all(l_max: int, theta: Float[Array, '...'], phi: Float[Array, '...']) Float[Array, '... N'][source]¶
Evaluate all real spherical harmonics up to l_max.
The function computes every real spherical harmonic \(Y_l^m(\theta, \varphi)\) for \(0 \le l \le l_{\max}\) and \(-l \le m \le l\), returning them stacked along a new trailing axis.
The ordering is the standard “degree-then-order” layout:
\[(0,0),\;(1,-1),(1,0),(1,1),\;(2,-2),\ldots,(2,2),\;\ldots\]so that the index into the last axis for a given (l, m) pair is \(l^2 + l + m\). The total number of harmonics is \((l_{\max}+1)^2\).
Python unrolls the loop over (l, m) during tracing. The real_spherical_harmonic function computes each harmonic independently.
jnp.stackjoins the results. Each l value therefore starts a separate Legendre recurrence chain. The implementation does not share recurrence values across l values.Implementation Logic¶
Collect harmonics in canonical order:
for l in range(l_max + 1): for m in range(-l, l + 1): results.append( real_spherical_harmonic(l, m, theta, phi) )
The nested ranges produce the documented degree-then-order sequence.
Stack the harmonic fields:
ylm_all: Float[Array, " ... N"] = jnp.stack( results, axis=-1 )
The new trailing axis indexes each
(l, m)pair.
- param l_max:
Maximum angular momentum.
- type l_max:
- param theta:
Polar angle from z-axis.
- type theta:
']- param phi:
Azimuthal angle.
- type phi:
']- returns:
ylm_all – All spherical harmonics stacked along the last axis, where
N = (l_max + 1)**2.- rtype:
N']