diffpes 2026.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. diffpes/__init__.py +65 -0
  2. diffpes/inout/__init__.py +88 -0
  3. diffpes/inout/chgcar.py +459 -0
  4. diffpes/inout/doscar.py +248 -0
  5. diffpes/inout/eigenval.py +258 -0
  6. diffpes/inout/hdf5.py +818 -0
  7. diffpes/inout/helpers.py +295 -0
  8. diffpes/inout/kpoints.py +433 -0
  9. diffpes/inout/plotting.py +757 -0
  10. diffpes/inout/poscar.py +174 -0
  11. diffpes/inout/procar.py +331 -0
  12. diffpes/inout/py.typed +0 -0
  13. diffpes/maths/__init__.py +68 -0
  14. diffpes/maths/dipole.py +368 -0
  15. diffpes/maths/gaunt.py +496 -0
  16. diffpes/maths/spherical_harmonics.py +336 -0
  17. diffpes/py.typed +0 -0
  18. diffpes/radial/__init__.py +47 -0
  19. diffpes/radial/bessel.py +198 -0
  20. diffpes/radial/integrate.py +128 -0
  21. diffpes/radial/wavefunctions.py +335 -0
  22. diffpes/simul/__init__.py +146 -0
  23. diffpes/simul/broadening.py +258 -0
  24. diffpes/simul/crosssections.py +196 -0
  25. diffpes/simul/expanded.py +1021 -0
  26. diffpes/simul/forward.py +586 -0
  27. diffpes/simul/oam.py +112 -0
  28. diffpes/simul/polarization.py +443 -0
  29. diffpes/simul/py.typed +0 -0
  30. diffpes/simul/resolution.py +122 -0
  31. diffpes/simul/self_energy.py +142 -0
  32. diffpes/simul/spectrum.py +1116 -0
  33. diffpes/simul/workflow.py +424 -0
  34. diffpes/tightb/__init__.py +68 -0
  35. diffpes/tightb/diagonalize.py +340 -0
  36. diffpes/tightb/hamiltonian.py +286 -0
  37. diffpes/tightb/projections.py +134 -0
  38. diffpes/types/__init__.py +162 -0
  39. diffpes/types/aliases.py +52 -0
  40. diffpes/types/bands.py +1064 -0
  41. diffpes/types/dos.py +510 -0
  42. diffpes/types/geometry.py +306 -0
  43. diffpes/types/kpath.py +365 -0
  44. diffpes/types/params.py +496 -0
  45. diffpes/types/py.typed +0 -0
  46. diffpes/types/radial_params.py +482 -0
  47. diffpes/types/self_energy.py +271 -0
  48. diffpes/types/tb_model.py +531 -0
  49. diffpes/types/volumetric.py +608 -0
  50. diffpes/utils/__init__.py +30 -0
  51. diffpes/utils/math.py +255 -0
  52. diffpes/utils/py.typed +0 -0
  53. diffpes-2026.3.1.dist-info/METADATA +176 -0
  54. diffpes-2026.3.1.dist-info/RECORD +55 -0
  55. diffpes-2026.3.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,368 @@
1
+ r"""Full dipole matrix element assembly.
2
+
3
+ Extended Summary
4
+ ----------------
5
+ Combines radial integrals :math:`B^{l'}(k)`, Gaunt coefficients,
6
+ real spherical harmonics :math:`Y_{l'm'}(\hat{k})`, and the
7
+ polarization vector :math:`\hat{e}` to compute photoemission
8
+ dipole matrix elements from first principles:
9
+
10
+ .. math::
11
+
12
+ M(\mathbf{k}, n, l, m) = \sum_{l', m'} B_{n,l}^{l'}(|\mathbf{k}|)
13
+ \cdot G(l, m, l', m') \cdot Y_{l'}^{m'}(\hat{k})
14
+ \cdot \hat{e}_{q(m'-m)}
15
+
16
+ where :math:`q = m' - m` selects the dipole component and
17
+ :math:`\hat{e}_q` is the corresponding spherical component of
18
+ the polarization vector.
19
+
20
+ Routine Listings
21
+ ----------------
22
+ :func:`dipole_matrix_element_single`
23
+ Compute dipole matrix element for a single orbital (n, l, m).
24
+ :func:`dipole_intensity_orbital`
25
+ Compute |M|^2 for one orbital.
26
+ :func:`dipole_intensities_all_orbitals`
27
+ Compute |M|^2 for all orbitals in the basis.
28
+ """
29
+
30
+ import jax.numpy as jnp
31
+ from beartype import beartype
32
+ from jaxtyping import Array, Complex, Float, jaxtyped
33
+
34
+ from diffpes.radial import radial_integral, slater_radial
35
+ from diffpes.types import SlaterParams
36
+
37
+ from .gaunt import GAUNT_TABLE, L_MAX
38
+ from .spherical_harmonics import real_spherical_harmonic
39
+
40
+
41
+ def _cartesian_to_spherical_dipole(
42
+ efield: Complex[Array, " 3"],
43
+ ) -> Complex[Array, " 3"]:
44
+ r"""Convert Cartesian E-field to real-harmonic dipole components.
45
+
46
+ Maps (e_x, e_y, e_z) to (e_{q=-1}, e_{q=0}, e_{q=+1}) where
47
+ the q index matches the real spherical harmonic convention for
48
+ the dipole operator (l=1):
49
+
50
+ - q = -1 corresponds to Y_1^{-1}(real) ~ sin(theta)sin(phi) ~ y
51
+ - q = 0 corresponds to Y_1^0(real) ~ cos(theta) ~ z
52
+ - q = +1 corresponds to Y_1^{+1}(real) ~ sin(theta)cos(phi) ~ x
53
+
54
+ Extended Summary
55
+ ----------------
56
+ The dipole operator :math:`\hat{r}` is expanded in the real spherical
57
+ harmonic basis for l=1. In Cartesian coordinates the three components
58
+ of the position vector are :math:`(x, y, z)`, and they map to real
59
+ spherical harmonics as:
60
+
61
+ .. math::
62
+
63
+ x = r \sin\theta \cos\phi \propto Y_1^{+1}(\text{real}) \quad (q=+1)
64
+
65
+ y = r \sin\theta \sin\phi \propto Y_1^{-1}(\text{real}) \quad (q=-1)
66
+
67
+ z = r \cos\theta \propto Y_1^{0}(\text{real}) \quad (q=0)
68
+
69
+ The returned array is ordered by ascending q: ``[e_y, e_z, e_x]``,
70
+ so that indexing with ``q_idx = q + 1`` (for q in {-1, 0, +1})
71
+ selects the correct Cartesian component of the polarization vector.
72
+
73
+ This is a pure permutation with no complex rotation because the
74
+ real spherical harmonics for l=1 directly correspond to the
75
+ Cartesian axes without mixing.
76
+
77
+ Parameters
78
+ ----------
79
+ efield : Complex[Array, " 3"]
80
+ Polarization vector ``(e_x, e_y, e_z)`` in Cartesian coordinates.
81
+
82
+ Returns
83
+ -------
84
+ e_spherical : Complex[Array, " 3"]
85
+ ``(e_{q=-1}, e_{q=0}, e_{q=+1})``.
86
+ """
87
+ ex: Complex[Array, ""] = efield[0]
88
+ ey: Complex[Array, ""] = efield[1]
89
+ ez: Complex[Array, ""] = efield[2]
90
+ # q=+1 ~ x, q=-1 ~ y, q=0 ~ z (real spherical harmonic convention)
91
+ e_spherical: Complex[Array, " 3"] = jnp.array([ey, ez, ex], dtype=jnp.complex128)
92
+ return e_spherical
93
+
94
+
95
+ @jaxtyped(typechecker=beartype)
96
+ def dipole_matrix_element_single(
97
+ k_vec: Float[Array, " 3"],
98
+ r_grid: Float[Array, " R"],
99
+ radial_values: Float[Array, " R"],
100
+ l: int,
101
+ m: int,
102
+ efield: Complex[Array, " 3"],
103
+ ) -> Complex[Array, " "]:
104
+ r"""Compute dipole matrix element for a single orbital (n, l, m).
105
+
106
+ .. math::
107
+
108
+ M = \sum_{q} \hat{e}_q \sum_{l'} B^{l'}(|k|) \cdot
109
+ G(l, m, l', m+q) \cdot Y_{l'}^{m+q}(\hat{k})
110
+
111
+ where the sum is over dipole components q in {-1, 0, +1} and
112
+ final-state angular momenta l' in {l-1, l+1}.
113
+
114
+ Extended Summary
115
+ ----------------
116
+ This function assembles the full photoemission dipole matrix element
117
+ by combining four ingredients:
118
+
119
+ 1. **Radial integral** :math:`B^{l'}(|k|)` -- the overlap between
120
+ the initial radial wavefunction and the final-state spherical
121
+ Bessel function :math:`j_{l'}(kr)`, weighted by :math:`r^3`,
122
+ evaluated via trapezoidal quadrature on the supplied radial grid.
123
+
124
+ 2. **Gaunt coefficient** :math:`G(l, m, l', m')` -- the angular
125
+ integral coupling initial (l, m) and final (l', m') states
126
+ through the dipole operator, looked up from the precomputed
127
+ ``GAUNT_TABLE``.
128
+
129
+ 3. **Real spherical harmonic** :math:`Y_{l'}^{m'}(\hat{k})` --
130
+ the angular part of the final-state plane wave expansion,
131
+ evaluated at the direction of the photoelectron wavevector.
132
+
133
+ 4. **Polarization component** :math:`\hat{e}_q` -- the q-th
134
+ spherical component of the polarization vector, obtained by
135
+ mapping Cartesian (x, y, z) to the real harmonic basis via
136
+ `_cartesian_to_spherical_dipole`.
137
+
138
+ The dipole selection rule :math:`l' = l \pm 1` restricts the
139
+ final-state sum to at most two terms per q value. The magnetic
140
+ selection rule :math:`m' = m + q` with :math:`|q| \le 1` means
141
+ at most three q values contribute.
142
+
143
+ Numerical stability techniques:
144
+
145
+ - **Gradient-safe norm**: :math:`|k|` is computed as
146
+ :math:`\sqrt{k \cdot k + \epsilon}` with :math:`\epsilon = 10^{-30}`
147
+ to avoid NaN gradients when :math:`k = 0`.
148
+ - **Safe polar angle**: :math:`\cos\theta` is clipped to
149
+ :math:`[-1 + 10^{-7}, 1 - 10^{-7}]` to prevent singularities in
150
+ ``arccos`` gradients at the poles.
151
+ - **Safe azimuthal angle**: a small offset is added to the x-component
152
+ in ``arctan2`` to avoid indeterminate gradients at the origin.
153
+
154
+ Parameters
155
+ ----------
156
+ k_vec : Float[Array, " 3"]
157
+ Photoelectron wavevector in Cartesian coordinates.
158
+ r_grid : Float[Array, " R"]
159
+ Radial grid for integration.
160
+ radial_values : Float[Array, " R"]
161
+ R(r) sampled on r_grid.
162
+ l : int
163
+ Angular momentum quantum number of the initial orbital.
164
+ m : int
165
+ Magnetic quantum number of the initial orbital.
166
+ efield : Complex[Array, " 3"]
167
+ Polarization vector in Cartesian coordinates.
168
+
169
+ Returns
170
+ -------
171
+ M : Complex[Array, " "]
172
+ Complex dipole matrix element.
173
+
174
+ Notes
175
+ -----
176
+ The loop over q and l' is unrolled at Python trace time (not
177
+ inside ``jax.lax`` control flow) because the iteration bounds
178
+ depend on the static quantum numbers (l, m). This produces a
179
+ fixed computation graph per (l, m) pair, which is efficient
180
+ for JIT compilation but means different orbitals trace distinct
181
+ XLA programs.
182
+ """
183
+ # Gradient-safe norm: eps prevents NaN grad at k_vec=0
184
+ k_mag: Float[Array, ""] = jnp.sqrt(jnp.dot(k_vec, k_vec) + 1e-30)
185
+ k_hat: Float[Array, " 3"] = k_vec / k_mag
186
+
187
+ # Convert k_hat to spherical angles (safe for grad at poles/origin)
188
+ theta_k: Float[Array, ""] = jnp.arccos(
189
+ jnp.clip(k_hat[2], -1.0 + 1e-7, 1.0 - 1e-7)
190
+ )
191
+ phi_k: Float[Array, ""] = jnp.arctan2(k_hat[1], k_hat[0] + 1e-30)
192
+
193
+ # Polarization in spherical dipole components
194
+ e_sph: Complex[Array, " 3"] = _cartesian_to_spherical_dipole(efield)
195
+
196
+ M_total: Complex[Array, ""] = jnp.zeros((), dtype=jnp.complex128)
197
+
198
+ for q_idx, q in enumerate((-1, 0, 1)):
199
+ mp: int = m + q # final state m'
200
+ eq: Complex[Array, ""] = e_sph[q_idx]
201
+
202
+ for lp in (l - 1, l + 1):
203
+ if lp < 0 or lp > L_MAX + 1:
204
+ continue
205
+ if abs(mp) > lp:
206
+ continue
207
+
208
+ # Radial integral B^{l'}(|k|)
209
+ B_lp: Float[Array, ""] = radial_integral(
210
+ k_mag, r_grid, radial_values, lp
211
+ )
212
+
213
+ # Gaunt coefficient G(l, m, l', m')
214
+ G: Float[Array, ""] = GAUNT_TABLE[
215
+ l, m + L_MAX, q + 1, lp, mp + L_MAX
216
+ ]
217
+
218
+ # Spherical harmonic Y_{l'}^{m'}(k_hat)
219
+ Y_lp_mp: Float[Array, ""] = real_spherical_harmonic(
220
+ lp, mp, theta_k, phi_k
221
+ )
222
+
223
+ M_total = M_total + eq * B_lp * G * Y_lp_mp
224
+
225
+ M: Complex[Array, ""] = M_total
226
+ return M
227
+
228
+
229
+ @jaxtyped(typechecker=beartype)
230
+ def dipole_intensity_orbital(
231
+ k_vec: Float[Array, " 3"],
232
+ r_grid: Float[Array, " R"],
233
+ radial_values: Float[Array, " R"],
234
+ l: int,
235
+ m: int,
236
+ efield: Complex[Array, " 3"],
237
+ ) -> Float[Array, " "]:
238
+ r"""Compute |M|^2 for one orbital.
239
+
240
+ Extended Summary
241
+ ----------------
242
+ Computes the photoemission intensity for a single initial-state
243
+ orbital characterized by quantum numbers (l, m) and a radial
244
+ wavefunction sampled on a grid. The intensity is the squared
245
+ modulus of the complex dipole matrix element:
246
+
247
+ .. math::
248
+
249
+ I(\mathbf{k}) = |M(\mathbf{k}, l, m)|^2
250
+
251
+ This is a thin wrapper that calls `dipole_matrix_element_single`
252
+ and returns :math:`|M|^2 = M \cdot M^*`. The result is real and
253
+ non-negative by construction, and is differentiable with respect
254
+ to all continuous inputs (k_vec, r_grid, radial_values, efield)
255
+ through JAX automatic differentiation.
256
+
257
+ Parameters
258
+ ----------
259
+ k_vec : Float[Array, " 3"]
260
+ Photoelectron wavevector.
261
+ r_grid : Float[Array, " R"]
262
+ Radial grid.
263
+ radial_values : Float[Array, " R"]
264
+ Radial wavefunction on grid.
265
+ l : int
266
+ Angular momentum.
267
+ m : int
268
+ Magnetic quantum number.
269
+ efield : Complex[Array, " 3"]
270
+ Polarization vector.
271
+
272
+ Returns
273
+ -------
274
+ intensity : Float[Array, " "]
275
+ Squared modulus of the matrix element.
276
+ """
277
+ M: Complex[Array, ""] = dipole_matrix_element_single(
278
+ k_vec, r_grid, radial_values, l, m, efield
279
+ )
280
+ intensity: Float[Array, ""] = jnp.abs(M) ** 2
281
+ return intensity
282
+
283
+
284
+ @jaxtyped(typechecker=beartype)
285
+ def dipole_intensities_all_orbitals(
286
+ k_vec: Float[Array, " 3"],
287
+ r_grid: Float[Array, " R"],
288
+ slater_params: SlaterParams,
289
+ efield: Complex[Array, " 3"],
290
+ ) -> Float[Array, " O"]:
291
+ r"""Compute |M|^2 for all orbitals in the basis.
292
+
293
+ Extended Summary
294
+ ----------------
295
+ Iterates over every orbital in the Slater basis set, computes its
296
+ radial wavefunction from ``slater_params``, and evaluates the
297
+ squared dipole matrix element. The loop is unrolled at Python
298
+ trace time because each orbital has different static quantum
299
+ numbers (n, l, m) that determine the structure of the recurrence
300
+ relations and Gaunt table lookups. This means each orbital
301
+ produces a distinct sub-graph in the XLA program.
302
+
303
+ For each orbital *o*, the function:
304
+
305
+ 1. Extracts quantum numbers :math:`(n_o, l_o, m_o)` and Slater
306
+ exponent :math:`\zeta_o` from ``slater_params``.
307
+ 2. Evaluates the normalized Slater radial function
308
+ :math:`R(r) = N r^{n-1} e^{-\zeta r}` on the supplied grid.
309
+ 3. Weights by the multi-zeta coefficient
310
+ ``slater_params.coefficients[o, 0]`` (first column for
311
+ single-zeta bases).
312
+ 4. Calls `dipole_intensity_orbital` to compute :math:`|M|^2`.
313
+
314
+ The results are stacked into a 1-D array of length equal to the
315
+ number of orbitals in the basis.
316
+
317
+ Parameters
318
+ ----------
319
+ k_vec : Float[Array, " 3"]
320
+ Photoelectron wavevector.
321
+ r_grid : Float[Array, " R"]
322
+ Radial grid.
323
+ slater_params : SlaterParams
324
+ Slater exponents and orbital basis.
325
+ efield : Complex[Array, " 3"]
326
+ Polarization vector.
327
+
328
+ Returns
329
+ -------
330
+ intensities : Float[Array, " O"]
331
+ |M|^2 per orbital.
332
+
333
+ Notes
334
+ -----
335
+ Because the loop is Python-level (not ``jax.lax.scan``), the
336
+ number of orbitals is baked into the traced program. Changing the
337
+ basis size requires re-tracing / re-JITting.
338
+ """
339
+ basis = slater_params.orbital_basis
340
+ n_orbitals: int = len(basis.n_values)
341
+ results: list[Float[Array, ""]] = []
342
+
343
+ for o in range(n_orbitals):
344
+ n_o: int = basis.n_values[o]
345
+ l_o: int = basis.l_values[o]
346
+ m_o: int = basis.m_values[o]
347
+ zeta_o: Float[Array, ""] = slater_params.zeta[o]
348
+
349
+ # Compute radial wavefunction on the grid
350
+ R_values: Float[Array, " R"] = slater_radial(r_grid, n_o, zeta_o)
351
+
352
+ # Weight by multi-zeta coefficient (first column for single-zeta)
353
+ R_values = R_values * slater_params.coefficients[o, 0]
354
+
355
+ intensity: Float[Array, ""] = dipole_intensity_orbital(
356
+ k_vec, r_grid, R_values, l_o, m_o, efield
357
+ )
358
+ results.append(intensity)
359
+
360
+ intensities: Float[Array, " O"] = jnp.stack(results)
361
+ return intensities
362
+
363
+
364
+ __all__: list[str] = [
365
+ "dipole_intensities_all_orbitals",
366
+ "dipole_intensity_orbital",
367
+ "dipole_matrix_element_single",
368
+ ]