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,174 @@
1
+ """VASP POSCAR file parser.
2
+
3
+ Extended Summary
4
+ ----------------
5
+ Reads VASP POSCAR/CONTCAR crystal structure files and returns
6
+ a :class:`~diffpes.types.CrystalGeometry` PyTree containing lattice
7
+ vectors, atomic coordinates, element symbols, and atom counts.
8
+
9
+ Routine Listings
10
+ ----------------
11
+ :func:`read_poscar`
12
+ Parse a VASP POSCAR file into a CrystalGeometry.
13
+
14
+ Notes
15
+ -----
16
+ Handles both direct (fractional) and Cartesian coordinate formats,
17
+ optional selective dynamics, and automatic reciprocal lattice
18
+ computation.
19
+ """
20
+
21
+ from pathlib import Path
22
+
23
+ import jax.numpy as jnp
24
+ import numpy as np
25
+
26
+ from diffpes.types import CrystalGeometry, make_crystal_geometry
27
+
28
+
29
+ def read_poscar(
30
+ filename: str = "POSCAR",
31
+ ) -> CrystalGeometry:
32
+ """Parse a VASP POSCAR/CONTCAR file.
33
+
34
+ Reads a VASP POSCAR (or CONTCAR) file that defines the crystal
35
+ structure: lattice vectors, element symbols, atom counts, and
36
+ atomic coordinates in either direct (fractional) or Cartesian
37
+ form. The function returns a :class:`~diffpes.types.CrystalGeometry`
38
+ PyTree with coordinates always stored in fractional form and the
39
+ lattice pre-scaled by the universal scaling factor.
40
+
41
+ Extended Summary
42
+ ----------------
43
+ The POSCAR file format used by VASP has the following structure:
44
+
45
+ * **Line 1**: Comment / system name (discarded).
46
+ * **Line 2**: Universal scaling factor applied to all lattice
47
+ vectors (and to Cartesian coordinates, if applicable).
48
+ * **Lines 3-5**: Three rows of three floats defining the lattice
49
+ vectors ``a``, ``b``, ``c`` in Angstroms (before scaling).
50
+ * **Line 6**: Either element symbols (VASP >= 5) or atom counts
51
+ (VASP 4). If non-numeric, it is the species line.
52
+ * **Line 6 or 7**: Atom counts per species (list of integers).
53
+ * **Next line**: Optional ``"Selective dynamics"`` flag. If
54
+ present, the following line is the coordinate-type specifier.
55
+ * **Coordinate-type line**: ``"Direct"`` / ``"Fractional"`` for
56
+ fractional coordinates, or ``"Cartesian"`` / ``"Cart"`` for
57
+ Cartesian.
58
+ * **Coordinate lines**: ``natoms`` lines, each with at least three
59
+ floats. Additional columns (selective-dynamics T/F flags) are
60
+ silently ignored.
61
+
62
+ Implementation Logic
63
+ --------------------
64
+ 1. **Read comment line** (line 1) -- consumed and discarded.
65
+
66
+ 2. **Read scaling factor** (line 2) -- a single float that
67
+ multiplies all lattice vectors.
68
+
69
+ 3. **Read lattice vectors** (lines 3-5) -- three rows of three
70
+ floats each, stored as a (3, 3) NumPy array and immediately
71
+ multiplied by the scaling factor.
72
+
73
+ 4. **Detect optional species line** (line 6) -- if the line
74
+ contains no digits it is the VASP-5 style element-symbol line
75
+ (e.g. ``"Si O"``). The symbols are stored and the next line is
76
+ read. If the line contains digits, it is the atom-count line
77
+ (VASP-4 style) and ``symbols`` remains empty.
78
+
79
+ 5. **Read atom counts** -- parse the current line as a list of
80
+ integers giving the number of atoms per species.
81
+
82
+ 6. **Detect optional Selective Dynamics line** -- if the next line
83
+ starts with ``"s"`` or ``"S"``, selective-dynamics flags are
84
+ present; consume the line and read the coordinate-type line
85
+ that follows.
86
+
87
+ 7. **Determine coordinate type** -- if the coordinate-type line
88
+ starts with ``"c"``, ``"C"``, ``"k"``, or ``"K"`` the
89
+ coordinates are Cartesian; otherwise they are direct
90
+ (fractional).
91
+
92
+ 8. **Read coordinates** -- parse ``natoms`` lines, each with at
93
+ least three floats. Only the first three tokens are used
94
+ (selective-dynamics flags on positions 4-6 are ignored).
95
+
96
+ 9. **Convert Cartesian to fractional** (if needed) -- scale the
97
+ Cartesian coordinates by the scaling factor, then solve
98
+ ``lattice^T @ frac^T = cart^T`` via ``np.linalg.solve`` to
99
+ obtain fractional coordinates.
100
+
101
+ 10. **Construct PyTree** -- call ``make_crystal_geometry`` with the
102
+ scaled lattice, fractional coordinates, element symbols, and
103
+ atom counts. The reciprocal lattice is computed automatically
104
+ inside ``make_crystal_geometry`` as ``2 * pi * inv(lattice)^T``.
105
+
106
+ Parameters
107
+ ----------
108
+ filename : str, optional
109
+ Path to POSCAR file. Default is ``"POSCAR"``.
110
+
111
+ Returns
112
+ -------
113
+ geometry : CrystalGeometry
114
+ Crystal geometry with lattice, reciprocal lattice, fractional
115
+ coordinates, element symbols, and atom counts.
116
+
117
+ Raises
118
+ ------
119
+ IndexError
120
+ If the coordinate-type line is empty (malformed file).
121
+
122
+ Notes
123
+ -----
124
+ Coordinates are always returned in **fractional** (direct) form,
125
+ regardless of the input format. If the input is Cartesian, the
126
+ conversion ``frac = lattice^{-T} @ cart^T`` is performed via
127
+ ``np.linalg.solve`` for numerical stability. The optional
128
+ selective-dynamics flags (``T T F`` appended to coordinate lines)
129
+ are detected and skipped but not stored. VASP-4 style files that
130
+ lack the element-symbol line will produce an empty ``symbols``
131
+ tuple; the caller should supply symbols from an external source
132
+ (e.g. POTCAR) in that case.
133
+ """
134
+ path: Path = Path(filename)
135
+ with path.open("r") as fid:
136
+ _comment: str = fid.readline().strip()
137
+ scale: float = float(fid.readline().strip())
138
+ lattice: np.ndarray = np.zeros((3, 3), dtype=np.float64)
139
+ for i in range(3):
140
+ vals: list[float] = [float(x) for x in fid.readline().split()]
141
+ lattice[i, :] = vals
142
+ lattice = lattice * scale
143
+ line: str = fid.readline().strip()
144
+ symbols: tuple[str, ...] = ()
145
+ if not any(c.isdigit() for c in line):
146
+ symbols = tuple(line.split())
147
+ line = fid.readline().strip()
148
+ atom_counts: list[int] = [int(x) for x in line.split()]
149
+ natoms: int = sum(atom_counts)
150
+ line = fid.readline().strip()
151
+ selective: bool = False
152
+ if line[0].lower() == "s":
153
+ selective = True # noqa: F841
154
+ line = fid.readline().strip()
155
+ cartesian: bool = line[0].lower() in ("c", "k")
156
+ coords: np.ndarray = np.zeros((natoms, 3), dtype=np.float64)
157
+ for i in range(natoms):
158
+ vals = [float(x) for x in fid.readline().split()[:3]]
159
+ coords[i, :] = vals
160
+ if cartesian:
161
+ coords = coords * scale
162
+ coords = np.linalg.solve(lattice.T, coords.T).T
163
+ geometry: CrystalGeometry = make_crystal_geometry(
164
+ lattice=jnp.asarray(lattice),
165
+ coords=jnp.asarray(coords),
166
+ symbols=symbols,
167
+ atom_counts=atom_counts,
168
+ )
169
+ return geometry
170
+
171
+
172
+ __all__: list[str] = [
173
+ "read_poscar",
174
+ ]
@@ -0,0 +1,331 @@
1
+ """VASP PROCAR file parser.
2
+
3
+ Extended Summary
4
+ ----------------
5
+ Reads VASP PROCAR files containing orbital-resolved band
6
+ projections and returns an
7
+ :class:`~diffpes.types.OrbitalProjection` PyTree. Supports
8
+ non-spin, spin-polarized (ISPIN=2), and SOC layouts.
9
+
10
+ Routine Listings
11
+ ----------------
12
+ :func:`read_procar`
13
+ Parse a VASP PROCAR file into an OrbitalProjection.
14
+
15
+ Notes
16
+ -----
17
+ Orbital ordering follows VASP convention:
18
+ ``[s, py, pz, px, dxy, dyz, dz2, dxz, dx2-y2]``.
19
+ """
20
+
21
+ import re
22
+ from pathlib import Path
23
+
24
+ import jax.numpy as jnp
25
+ import numpy as np
26
+ from beartype.typing import Literal, Optional, Union
27
+ from jaxtyping import Array, Float
28
+
29
+ from diffpes.types import (
30
+ OrbitalProjection,
31
+ SpinOrbitalProjection,
32
+ make_orbital_projection,
33
+ make_spin_orbital_projection,
34
+ )
35
+
36
+ _NORBS: int = 9
37
+ _NSPIN_COMPONENTS: int = 6
38
+ _ISPIN2_BLOCKS: int = 2
39
+ _SOC_BLOCKS: int = 4
40
+
41
+
42
+ def read_procar(
43
+ filename: str = "PROCAR",
44
+ return_mode: Literal["legacy", "full"] = "legacy",
45
+ ) -> Union[OrbitalProjection, SpinOrbitalProjection]:
46
+ r"""Parse a VASP PROCAR file.
47
+
48
+ Reads a VASP PROCAR file that contains the orbital-resolved
49
+ projections of Kohn-Sham wave functions onto site-centred
50
+ spherical harmonics. Supports three layouts:
51
+
52
+ - **Non-spin** (ISPIN=1, no SOC): single block of k-points.
53
+ - **Spin-polarized** (ISPIN=2): two consecutive blocks of
54
+ k-points (one per spin channel).
55
+ - **SOC** (LSORBIT=.TRUE.): four consecutive blocks per k-point
56
+ (total, Sx, Sy, Sz projections).
57
+
58
+ Extended Summary
59
+ ----------------
60
+ The PROCAR file written by VASP (when ``LORBIT=11`` or ``12``)
61
+ contains site- and orbital-resolved projections of each Kohn-Sham
62
+ eigenstate. The file is organised into one or more *blocks*, each
63
+ containing:
64
+
65
+ * A header line with ``# of k-points``, ``# of bands``,
66
+ ``# of ions``.
67
+ * For each k-point: a coordinate line, then for each band: a
68
+ band-energy line, an orbital-name header, one projection line
69
+ per atom (columns: ion index, s, py, pz, px, dxy, dyz, dz2,
70
+ dxz, dx2-y2, tot), a ``tot`` summation line, and a blank line.
71
+
72
+ The number of blocks determines the spin layout:
73
+
74
+ * 1 block: non-spin-polarized (ISPIN=1).
75
+ * 2 blocks: spin-polarized (ISPIN=2), block 0 = spin-up,
76
+ block 1 = spin-down.
77
+ * 4 blocks: spin-orbit coupling (SOC), blocks = total, Sx, Sy, Sz.
78
+
79
+ Implementation Logic
80
+ --------------------
81
+ 1. **Read entire file** into a single string and delegate to
82
+ :func:`_parse_procar_blocks` to extract structured block data.
83
+
84
+ 2. **Determine spin layout** from the number of blocks.
85
+
86
+ 3. **Legacy mode or non-spin**: return an ``OrbitalProjection``
87
+ wrapping only the first block's projection array (shape
88
+ ``(K, B, A, 9)``).
89
+
90
+ 4. **Spin-polarized (ISPIN=2), full mode**:
91
+
92
+ a. Average spin-up and spin-down projections to get a
93
+ spin-averaged orbital weight per atom.
94
+ b. Construct a ``(K, B, A, 6)`` spin texture array where
95
+ channels 4-5 encode ``Sz+`` and ``Sz-`` (the positive and
96
+ negative parts of the spin-up minus spin-down difference
97
+ summed over orbitals).
98
+ c. Return a ``SpinOrbitalProjection``.
99
+
100
+ 5. **SOC (4 blocks), full mode**:
101
+
102
+ a. Use the total-projection block (block 0) as the orbital
103
+ weights.
104
+ b. Construct a ``(K, B, A, 6)`` spin texture array where
105
+ channels 0-1 encode ``Sx+`` / ``Sx-``, 2-3 encode
106
+ ``Sy+`` / ``Sy-``, and 4-5 encode ``Sz+`` / ``Sz-``.
107
+ Each component is the positive/negative part of the
108
+ orbital-summed projection from the corresponding
109
+ spin block.
110
+ c. Return a ``SpinOrbitalProjection``.
111
+
112
+ Parameters
113
+ ----------
114
+ filename : str, optional
115
+ Path to PROCAR file. Default is ``"PROCAR"``.
116
+ return_mode : {"legacy", "full"}, optional
117
+ ``"legacy"`` (default) returns an ``OrbitalProjection``
118
+ from the first spin block only (backward-compatible).
119
+ ``"full"`` returns a ``SpinOrbitalProjection`` (with
120
+ mandatory spin field) for ISPIN=2 and SOC data, or an
121
+ ``OrbitalProjection`` for non-spin data.
122
+
123
+ Returns
124
+ -------
125
+ orb_proj : OrbitalProjection or SpinOrbitalProjection
126
+ ``OrbitalProjection`` for legacy mode or non-spin data.
127
+ ``SpinOrbitalProjection`` for full mode with spin data.
128
+
129
+ Raises
130
+ ------
131
+ ValueError
132
+ If no valid PROCAR blocks are found in the file.
133
+
134
+ Notes
135
+ -----
136
+ The 9 orbital channels follow the VASP convention:
137
+ ``[s, py, pz, px, dxy, dyz, dz2, dxz, dx2-y2]``.
138
+ The ``tot`` column printed by VASP is **not** stored; only the
139
+ individual orbital columns are kept. For ISPIN=2 in full mode, the
140
+ spin-averaged projection ``(up + down) / 2`` is used as the
141
+ orbital weight because many downstream consumers expect a single
142
+ projection array rather than separate spin channels. The spin
143
+ texture is encoded as 6 non-negative channels
144
+ ``[Sx+, Sx-, Sy+, Sy-, Sz+, Sz-]`` following the convention used
145
+ by the ARPES simulation pipeline.
146
+ """
147
+ path: Path = Path(filename)
148
+ with path.open("r") as fid:
149
+ content: str = fid.read()
150
+
151
+ blocks: list[dict] = _parse_procar_blocks(content)
152
+
153
+ if not blocks:
154
+ msg = "No valid PROCAR blocks found."
155
+ raise ValueError(msg)
156
+
157
+ nblocks: int = len(blocks)
158
+ nkpts: int = blocks[0]["nkpts"]
159
+ nbands: int = blocks[0]["nbands"]
160
+ natoms: int = blocks[0]["natoms"]
161
+
162
+ is_spin_polarized: bool = nblocks == _ISPIN2_BLOCKS
163
+ is_soc: bool = nblocks == _SOC_BLOCKS
164
+
165
+ if return_mode == "legacy" or (not is_spin_polarized and not is_soc):
166
+ proj_arr: Float[Array, " K B A 9"] = jnp.asarray(
167
+ blocks[0]["projections"], dtype=jnp.float64
168
+ )
169
+ return make_orbital_projection(projections=proj_arr)
170
+
171
+ if is_spin_polarized:
172
+ proj_up: np.ndarray = blocks[0]["projections"]
173
+ proj_down: np.ndarray = blocks[1]["projections"]
174
+ avg: np.ndarray = (proj_up + proj_down) / 2.0
175
+ proj_arr = jnp.asarray(avg, dtype=jnp.float64)
176
+ spin_data: np.ndarray = np.zeros(
177
+ (nkpts, nbands, natoms, _NSPIN_COMPONENTS), dtype=np.float64
178
+ )
179
+ sz_diff: np.ndarray = np.sum(proj_up - proj_down, axis=-1)
180
+ spin_data[:, :, :, 4] = np.maximum(sz_diff, 0.0)
181
+ spin_data[:, :, :, 5] = np.maximum(-sz_diff, 0.0)
182
+ spin_arr: Float[Array, " K B A 6"] = jnp.asarray(
183
+ spin_data, dtype=jnp.float64
184
+ )
185
+ return make_spin_orbital_projection(
186
+ projections=proj_arr, spin=spin_arr
187
+ )
188
+
189
+ # SOC: 4 blocks = total, Sx, Sy, Sz
190
+ proj_total: np.ndarray = blocks[0]["projections"]
191
+ proj_sx: np.ndarray = blocks[1]["projections"]
192
+ proj_sy: np.ndarray = blocks[2]["projections"]
193
+ proj_sz: np.ndarray = blocks[3]["projections"]
194
+ proj_arr = jnp.asarray(proj_total, dtype=jnp.float64)
195
+
196
+ spin_data = np.zeros(
197
+ (nkpts, nbands, natoms, _NSPIN_COMPONENTS), dtype=np.float64
198
+ )
199
+ sx_sum: np.ndarray = np.sum(proj_sx, axis=-1)
200
+ sy_sum: np.ndarray = np.sum(proj_sy, axis=-1)
201
+ sz_sum: np.ndarray = np.sum(proj_sz, axis=-1)
202
+ spin_data[:, :, :, 0] = np.maximum(sx_sum, 0.0)
203
+ spin_data[:, :, :, 1] = np.maximum(-sx_sum, 0.0)
204
+ spin_data[:, :, :, 2] = np.maximum(sy_sum, 0.0)
205
+ spin_data[:, :, :, 3] = np.maximum(-sy_sum, 0.0)
206
+ spin_data[:, :, :, 4] = np.maximum(sz_sum, 0.0)
207
+ spin_data[:, :, :, 5] = np.maximum(-sz_sum, 0.0)
208
+ spin_arr = jnp.asarray(spin_data, dtype=jnp.float64)
209
+ return make_spin_orbital_projection(projections=proj_arr, spin=spin_arr)
210
+
211
+
212
+ def _parse_procar_blocks(
213
+ content: str,
214
+ ) -> list[dict]:
215
+ """Parse all PROCAR blocks from the full file content string.
216
+
217
+ Extended Summary
218
+ ----------------
219
+ A PROCAR file may contain 1, 2, or 4 consecutive blocks depending
220
+ on the spin configuration. Each block begins with a header line
221
+ matching ``"# of k-points: K # of bands: B # of ions: A"`` and
222
+ is followed by nested k-point / band / atom projection data.
223
+
224
+ Implementation Logic
225
+ --------------------
226
+ 1. Split the content into lines and scan for lines containing the
227
+ substring ``"k-points"`` (the block header).
228
+ 2. Extract ``(nkpts, nbands, natoms)`` from the header using a
229
+ regex that captures all integers on the line.
230
+ 3. Allocate a ``(nkpts, nbands, natoms, 9)`` NumPy array for the
231
+ orbital projections.
232
+ 4. For each k-point within the block:
233
+
234
+ a. Search forward for a line matching the pattern
235
+ ``k-point <index> : kx ky kz`` using a regex.
236
+ b. For each band within the k-point:
237
+
238
+ i. Skip forward until a line containing ``"band"`` is
239
+ found (the band energy header).
240
+ ii. Skip the orbital-name header line (``ion s py ...``).
241
+ iii. Read ``natoms`` lines, parsing columns 1 through 9
242
+ (skipping the ion index in column 0) as the orbital
243
+ projections.
244
+ iv. Skip the ``tot`` summation line and the trailing blank
245
+ line.
246
+
247
+ 5. Append a dict with keys ``'nkpts'``, ``'nbands'``,
248
+ ``'natoms'``, and ``'projections'`` for each block.
249
+ 6. Return the list of block dicts.
250
+
251
+ Parameters
252
+ ----------
253
+ content : str
254
+ The entire PROCAR file content as a single string.
255
+
256
+ Returns
257
+ -------
258
+ blocks : list[dict]
259
+ List of parsed blocks. Each dict contains:
260
+
261
+ * ``'nkpts'`` (int): number of k-points.
262
+ * ``'nbands'`` (int): number of bands.
263
+ * ``'natoms'`` (int): number of atoms (ions).
264
+ * ``'projections'`` (np.ndarray): orbital projections with
265
+ shape ``(nkpts, nbands, natoms, 9)`` and dtype ``float64``.
266
+
267
+ Notes
268
+ -----
269
+ The parser uses 1-based k-point indices from the file to place
270
+ data into the 0-based NumPy array (``k_idx = parsed_index - 1``).
271
+ Band and atom lines are read positionally (sequential order) rather
272
+ than by parsed index. The ``tot`` column (column 10 in the PROCAR
273
+ line) is not stored.
274
+ """
275
+ blocks: list[dict] = []
276
+ lines: list[str] = content.splitlines()
277
+ i: int = 0
278
+
279
+ while i < len(lines):
280
+ if "k-points" not in lines[i]:
281
+ i += 1
282
+ continue
283
+ header: str = lines[i]
284
+ params: list[int] = [int(x) for x in re.findall(r"\d+", header)]
285
+ nkpts: int = params[0]
286
+ nbands: int = params[1]
287
+ natoms: int = params[2]
288
+ projections: np.ndarray = np.zeros(
289
+ (nkpts, nbands, natoms, _NORBS), dtype=np.float64
290
+ )
291
+ i += 1
292
+
293
+ k_re: str = (
294
+ r"k-point\s+(\d+)\s*:\s*" r"([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)"
295
+ )
296
+ kpts_found: int = 0
297
+ while i < len(lines) and kpts_found < nkpts:
298
+ k_match: Optional[re.Match[str]] = re.search(k_re, lines[i])
299
+ if k_match is None:
300
+ i += 1
301
+ continue
302
+ k_idx: int = int(k_match.group(1)) - 1
303
+ i += 1
304
+ for b in range(nbands):
305
+ while i < len(lines) and "band" not in lines[i]:
306
+ i += 1
307
+ i += 1 # skip band header
308
+ i += 1 # skip orbital-name header
309
+ for a in range(natoms):
310
+ vals: list[float] = [float(x) for x in lines[i].split()]
311
+ projections[k_idx, b, a, :] = vals[1 : _NORBS + 1]
312
+ i += 1
313
+ i += 1 # skip tot line
314
+ i += 1 # skip blank line
315
+ kpts_found += 1
316
+
317
+ blocks.append(
318
+ {
319
+ "nkpts": nkpts,
320
+ "nbands": nbands,
321
+ "natoms": natoms,
322
+ "projections": projections,
323
+ }
324
+ )
325
+
326
+ return blocks
327
+
328
+
329
+ __all__: list[str] = [
330
+ "read_procar",
331
+ ]
diffpes/inout/py.typed ADDED
File without changes
@@ -0,0 +1,68 @@
1
+ r"""Angular matrix elements for dipole photoemission.
2
+
3
+ Extended Summary
4
+ ----------------
5
+ Provides Gaunt coefficients, real spherical harmonics, and full
6
+ dipole matrix element assembly for the differentiable ARPES
7
+ forward model. The dipole matrix element for orbital
8
+ :math:`(n, l, m)` combines radial integrals, Gaunt coefficients,
9
+ and spherical harmonics of the photoelectron direction:
10
+
11
+ .. math::
12
+
13
+ M(\mathbf{k}, n, l, m) = \sum_{l', m'} B_{n,l}^{l'}(|\mathbf{k}|)
14
+ \cdot G(l, m, l', m') \cdot Y_{l'}^{m'}(\hat{k})
15
+ \cdot \hat{e}_{q(m'-m)}
16
+
17
+ Routine Listings
18
+ ----------------
19
+ :data:`GAUNT_TABLE`
20
+ Precomputed dense array of real-valued Gaunt coefficients for
21
+ dipole transitions up to l_max = 4 (s through g orbitals).
22
+ :data:`L_MAX`
23
+ Maximum angular momentum supported by the precomputed Gaunt table.
24
+ :func:`build_gaunt_table`
25
+ Build a dipole Gaunt coefficient lookup table for a given l_max.
26
+ :func:`gaunt_lookup`
27
+ Look up a single Gaunt coefficient from the precomputed table.
28
+ :func:`real_spherical_harmonic`
29
+ Evaluate a single real spherical harmonic :math:`Y_l^m(\theta, \varphi)`.
30
+ :func:`real_spherical_harmonics_all`
31
+ Evaluate all real spherical harmonics up to a given l_max.
32
+ :func:`dipole_matrix_element_single`
33
+ Compute the complex dipole matrix element for one orbital (n, l, m).
34
+ :func:`dipole_intensity_orbital`
35
+ Compute :math:`|M|^2` for a single orbital.
36
+ :func:`dipole_intensities_all_orbitals`
37
+ Compute :math:`|M|^2` for every orbital in a Slater basis set.
38
+
39
+ Notes
40
+ -----
41
+ All functions are JAX-compatible and support JIT compilation and
42
+ automatic differentiation. The Gaunt table is computed once at
43
+ import time using pure Python and stored as a JAX array for O(1)
44
+ lookup during traced computation.
45
+ """
46
+
47
+ from .dipole import (
48
+ dipole_intensities_all_orbitals,
49
+ dipole_intensity_orbital,
50
+ dipole_matrix_element_single,
51
+ )
52
+ from .gaunt import GAUNT_TABLE, L_MAX, build_gaunt_table, gaunt_lookup
53
+ from .spherical_harmonics import (
54
+ real_spherical_harmonic,
55
+ real_spherical_harmonics_all,
56
+ )
57
+
58
+ __all__: list[str] = [
59
+ "GAUNT_TABLE",
60
+ "L_MAX",
61
+ "build_gaunt_table",
62
+ "dipole_intensities_all_orbitals",
63
+ "dipole_intensity_orbital",
64
+ "dipole_matrix_element_single",
65
+ "gaunt_lookup",
66
+ "real_spherical_harmonic",
67
+ "real_spherical_harmonics_all",
68
+ ]