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.
- diffpes/__init__.py +65 -0
- diffpes/inout/__init__.py +88 -0
- diffpes/inout/chgcar.py +459 -0
- diffpes/inout/doscar.py +248 -0
- diffpes/inout/eigenval.py +258 -0
- diffpes/inout/hdf5.py +818 -0
- diffpes/inout/helpers.py +295 -0
- diffpes/inout/kpoints.py +433 -0
- diffpes/inout/plotting.py +757 -0
- diffpes/inout/poscar.py +174 -0
- diffpes/inout/procar.py +331 -0
- diffpes/inout/py.typed +0 -0
- diffpes/maths/__init__.py +68 -0
- diffpes/maths/dipole.py +368 -0
- diffpes/maths/gaunt.py +496 -0
- diffpes/maths/spherical_harmonics.py +336 -0
- diffpes/py.typed +0 -0
- diffpes/radial/__init__.py +47 -0
- diffpes/radial/bessel.py +198 -0
- diffpes/radial/integrate.py +128 -0
- diffpes/radial/wavefunctions.py +335 -0
- diffpes/simul/__init__.py +146 -0
- diffpes/simul/broadening.py +258 -0
- diffpes/simul/crosssections.py +196 -0
- diffpes/simul/expanded.py +1021 -0
- diffpes/simul/forward.py +586 -0
- diffpes/simul/oam.py +112 -0
- diffpes/simul/polarization.py +443 -0
- diffpes/simul/py.typed +0 -0
- diffpes/simul/resolution.py +122 -0
- diffpes/simul/self_energy.py +142 -0
- diffpes/simul/spectrum.py +1116 -0
- diffpes/simul/workflow.py +424 -0
- diffpes/tightb/__init__.py +68 -0
- diffpes/tightb/diagonalize.py +340 -0
- diffpes/tightb/hamiltonian.py +286 -0
- diffpes/tightb/projections.py +134 -0
- diffpes/types/__init__.py +162 -0
- diffpes/types/aliases.py +52 -0
- diffpes/types/bands.py +1064 -0
- diffpes/types/dos.py +510 -0
- diffpes/types/geometry.py +306 -0
- diffpes/types/kpath.py +365 -0
- diffpes/types/params.py +496 -0
- diffpes/types/py.typed +0 -0
- diffpes/types/radial_params.py +482 -0
- diffpes/types/self_energy.py +271 -0
- diffpes/types/tb_model.py +531 -0
- diffpes/types/volumetric.py +608 -0
- diffpes/utils/__init__.py +30 -0
- diffpes/utils/math.py +255 -0
- diffpes/utils/py.typed +0 -0
- diffpes-2026.3.1.dist-info/METADATA +176 -0
- diffpes-2026.3.1.dist-info/RECORD +55 -0
- diffpes-2026.3.1.dist-info/WHEEL +4 -0
diffpes/inout/poscar.py
ADDED
|
@@ -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
|
+
]
|
diffpes/inout/procar.py
ADDED
|
@@ -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
|
+
]
|