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/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Differentiable ARPES simulations in JAX.
|
|
2
|
+
|
|
3
|
+
Extended Summary
|
|
4
|
+
----------------
|
|
5
|
+
A comprehensive toolkit for Angle-Resolved PhotoEmission
|
|
6
|
+
Spectroscopy (ARPES) simulations using JAX for automatic
|
|
7
|
+
differentiation and GPU acceleration. Supports five levels
|
|
8
|
+
of physical sophistication from basic Gaussian convolution
|
|
9
|
+
to full polarization-dependent dipole matrix element
|
|
10
|
+
calculations.
|
|
11
|
+
|
|
12
|
+
Routine Listings
|
|
13
|
+
----------------
|
|
14
|
+
:mod:`inout`
|
|
15
|
+
VASP file parsers (POSCAR, EIGENVAL, KPOINTS, DOSCAR, PROCAR).
|
|
16
|
+
:mod:`radial`
|
|
17
|
+
Differentiable radial primitives (Bessel, wavefunctions, integrals).
|
|
18
|
+
:mod:`simul`
|
|
19
|
+
ARPES simulation functions at five complexity levels.
|
|
20
|
+
:mod:`types`
|
|
21
|
+
PyTree data structures and factory functions.
|
|
22
|
+
:mod:`utils`
|
|
23
|
+
Mathematical utilities (Faddeeva function, normalization).
|
|
24
|
+
|
|
25
|
+
Examples
|
|
26
|
+
--------
|
|
27
|
+
>>> import diffpes
|
|
28
|
+
>>> bands = diffpes.inout.read_eigenval("EIGENVAL", fermi_energy=-1.5)
|
|
29
|
+
>>> orb = diffpes.inout.read_procar("PROCAR")
|
|
30
|
+
>>> params = diffpes.types.make_simulation_params(sigma=0.04)
|
|
31
|
+
>>> spectrum = diffpes.simul.simulate_basic(bands, orb, params)
|
|
32
|
+
|
|
33
|
+
Notes
|
|
34
|
+
-----
|
|
35
|
+
All computations are JAX-compatible and support automatic
|
|
36
|
+
differentiation for gradient-based optimization of ARPES
|
|
37
|
+
simulation parameters.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import os
|
|
41
|
+
from importlib.metadata import version
|
|
42
|
+
|
|
43
|
+
os.environ.setdefault(
|
|
44
|
+
"XLA_FLAGS",
|
|
45
|
+
"--xla_cpu_multi_thread_eigen=true intra_op_parallelism_threads=0",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
import jax # noqa: E402
|
|
49
|
+
|
|
50
|
+
jax.config.update("jax_enable_x64", True)
|
|
51
|
+
|
|
52
|
+
from . import inout, maths, radial, simul, tightb, types, utils # noqa: E402
|
|
53
|
+
|
|
54
|
+
__version__: str = version("diffpes")
|
|
55
|
+
|
|
56
|
+
__all__: list[str] = [
|
|
57
|
+
"__version__",
|
|
58
|
+
"inout",
|
|
59
|
+
"maths",
|
|
60
|
+
"radial",
|
|
61
|
+
"simul",
|
|
62
|
+
"tightb",
|
|
63
|
+
"types",
|
|
64
|
+
"utils",
|
|
65
|
+
]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""VASP file parsers for ARPES simulation input.
|
|
2
|
+
|
|
3
|
+
Extended Summary
|
|
4
|
+
----------------
|
|
5
|
+
Provides parsers for VASP output files (POSCAR, EIGENVAL, KPOINTS,
|
|
6
|
+
DOSCAR, PROCAR, CHGCAR) that return PyTree data structures suitable
|
|
7
|
+
for ARPES simulations.
|
|
8
|
+
|
|
9
|
+
Routine Listings
|
|
10
|
+
----------------
|
|
11
|
+
:func:`apply_kpath_ticks`
|
|
12
|
+
Annotate a plot axis with KPathInfo symmetry labels.
|
|
13
|
+
:func:`load_from_h5`
|
|
14
|
+
Load PyTrees from an HDF5 file.
|
|
15
|
+
:func:`plot_arpes_spectrum`
|
|
16
|
+
Plot an ARPES map from an ArpesSpectrum PyTree.
|
|
17
|
+
:func:`plot_arpes_with_kpath`
|
|
18
|
+
Plot an ARPES map and apply KPathInfo axis annotations.
|
|
19
|
+
:func:`plot_band_scatter_preset`
|
|
20
|
+
Plot projected bands with size-weighted scatter markers.
|
|
21
|
+
:func:`plot_band_scatter_with_kpath`
|
|
22
|
+
Plot projected-band scatter and annotate with KPathInfo.
|
|
23
|
+
:func:`list_band_scatter_presets`
|
|
24
|
+
Return supported preset names for band-scatter plotting.
|
|
25
|
+
:func:`read_doscar`
|
|
26
|
+
Parse VASP DOSCAR into DensityOfStates.
|
|
27
|
+
:func:`read_chgcar`
|
|
28
|
+
Parse VASP CHGCAR into VolumetricData.
|
|
29
|
+
:func:`read_eigenval`
|
|
30
|
+
Parse VASP EIGENVAL into BandStructure.
|
|
31
|
+
:func:`read_kpoints`
|
|
32
|
+
Parse VASP KPOINTS into KPathInfo.
|
|
33
|
+
:func:`read_poscar`
|
|
34
|
+
Parse VASP POSCAR into CrystalGeometry.
|
|
35
|
+
:func:`read_procar`
|
|
36
|
+
Parse VASP PROCAR into OrbitalProjection.
|
|
37
|
+
:func:`save_to_h5`
|
|
38
|
+
Save one or more named PyTrees to an HDF5 file.
|
|
39
|
+
|
|
40
|
+
Notes
|
|
41
|
+
-----
|
|
42
|
+
All parsers use standard Python I/O (not JAX) since file
|
|
43
|
+
parsing is inherently sequential. They convert parsed data
|
|
44
|
+
to JAX arrays via factory functions.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from .chgcar import read_chgcar
|
|
48
|
+
from .doscar import read_doscar
|
|
49
|
+
from .eigenval import read_eigenval
|
|
50
|
+
from .hdf5 import load_from_h5, save_to_h5
|
|
51
|
+
from .helpers import (
|
|
52
|
+
aggregate_atoms,
|
|
53
|
+
check_consistency,
|
|
54
|
+
reduce_orbitals,
|
|
55
|
+
select_atoms,
|
|
56
|
+
)
|
|
57
|
+
from .kpoints import read_kpoints
|
|
58
|
+
from .plotting import (
|
|
59
|
+
apply_kpath_ticks,
|
|
60
|
+
list_band_scatter_presets,
|
|
61
|
+
plot_arpes_spectrum,
|
|
62
|
+
plot_arpes_with_kpath,
|
|
63
|
+
plot_band_scatter_preset,
|
|
64
|
+
plot_band_scatter_with_kpath,
|
|
65
|
+
)
|
|
66
|
+
from .poscar import read_poscar
|
|
67
|
+
from .procar import read_procar
|
|
68
|
+
|
|
69
|
+
__all__: list[str] = [
|
|
70
|
+
"aggregate_atoms",
|
|
71
|
+
"apply_kpath_ticks",
|
|
72
|
+
"check_consistency",
|
|
73
|
+
"list_band_scatter_presets",
|
|
74
|
+
"load_from_h5",
|
|
75
|
+
"plot_band_scatter_preset",
|
|
76
|
+
"plot_band_scatter_with_kpath",
|
|
77
|
+
"plot_arpes_spectrum",
|
|
78
|
+
"plot_arpes_with_kpath",
|
|
79
|
+
"read_chgcar",
|
|
80
|
+
"read_doscar",
|
|
81
|
+
"read_eigenval",
|
|
82
|
+
"read_kpoints",
|
|
83
|
+
"read_poscar",
|
|
84
|
+
"read_procar",
|
|
85
|
+
"reduce_orbitals",
|
|
86
|
+
"save_to_h5",
|
|
87
|
+
"select_atoms",
|
|
88
|
+
]
|
diffpes/inout/chgcar.py
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""VASP CHGCAR file parser.
|
|
2
|
+
|
|
3
|
+
Extended Summary
|
|
4
|
+
----------------
|
|
5
|
+
Reads VASP CHGCAR volumetric files and returns a
|
|
6
|
+
:class:`~diffpes.types.VolumetricData` PyTree containing the crystal
|
|
7
|
+
geometry, charge density, and optional magnetization density.
|
|
8
|
+
For SOC calculations (4 grid blocks), returns an
|
|
9
|
+
:class:`~diffpes.types.SOCVolumetricData` with vector magnetization.
|
|
10
|
+
|
|
11
|
+
Routine Listings
|
|
12
|
+
----------------
|
|
13
|
+
:func:`read_chgcar`
|
|
14
|
+
Parse a VASP CHGCAR file into VolumetricData or SOCVolumetricData.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import jax.numpy as jnp
|
|
20
|
+
import numpy as np
|
|
21
|
+
from beartype.typing import Optional, Tuple
|
|
22
|
+
|
|
23
|
+
from diffpes.types import (
|
|
24
|
+
SOCVolumetricData,
|
|
25
|
+
VolumetricData,
|
|
26
|
+
make_soc_volumetric_data,
|
|
27
|
+
make_volumetric_data,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_LATTICE_ROWS: int = 3
|
|
31
|
+
_XYZ_COMPONENTS: int = 3
|
|
32
|
+
_SCALAR_LINE_COMPONENTS: int = 3
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_N_SOC_MAG_BLOCKS: int = 3
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_chgcar(
|
|
39
|
+
filename: str = "CHGCAR",
|
|
40
|
+
) -> VolumetricData | SOCVolumetricData:
|
|
41
|
+
"""Parse a VASP CHGCAR file.
|
|
42
|
+
|
|
43
|
+
Supports three layouts:
|
|
44
|
+
|
|
45
|
+
- **ISPIN=1**: 1 grid block (charge only).
|
|
46
|
+
- **ISPIN=2**: 2 grid blocks (charge + scalar magnetization).
|
|
47
|
+
- **SOC** (LSORBIT): 4 grid blocks (charge, mx, my, mz).
|
|
48
|
+
|
|
49
|
+
Extended Summary
|
|
50
|
+
----------------
|
|
51
|
+
The CHGCAR file produced by VASP begins with a POSCAR-style header
|
|
52
|
+
(comment, scaling factor, lattice vectors, species, atom counts,
|
|
53
|
+
coordinate type, and atomic positions) followed by one or more
|
|
54
|
+
volumetric data blocks on a real-space FFT grid. Each block starts
|
|
55
|
+
with a line of three integers (``NGX NGY NGZ``) giving the grid
|
|
56
|
+
dimensions, followed by the flattened grid values written in
|
|
57
|
+
Fortran column-major order (x fastest, z slowest).
|
|
58
|
+
|
|
59
|
+
Implementation Logic
|
|
60
|
+
--------------------
|
|
61
|
+
1. **Read POSCAR header** -- delegates to :func:`_read_poscar_header`
|
|
62
|
+
to obtain the lattice matrix, fractional coordinates, element
|
|
63
|
+
symbols, and atom counts.
|
|
64
|
+
|
|
65
|
+
2. **Compute cell volume** -- ``|a . (b x c)|`` from the three
|
|
66
|
+
lattice vectors. Raises if the volume is zero (degenerate cell).
|
|
67
|
+
|
|
68
|
+
3. **Locate first grid block** -- scans remaining lines for the
|
|
69
|
+
first line containing exactly three positive integers (the FFT
|
|
70
|
+
grid dimensions) via :func:`_find_next_grid_line`.
|
|
71
|
+
|
|
72
|
+
4. **Parse charge density** -- reads ``NGX * NGY * NGZ`` floats
|
|
73
|
+
from subsequent lines via :func:`_parse_float_block`, reshapes
|
|
74
|
+
in Fortran order, and divides by the cell volume to convert
|
|
75
|
+
from the VASP convention (charge * volume) to charge density
|
|
76
|
+
(electrons / Angstrom^3).
|
|
77
|
+
|
|
78
|
+
5. **Parse magnetization blocks** -- repeats the grid-search and
|
|
79
|
+
float-parsing loop up to three more times (for ISPIN=2 or SOC
|
|
80
|
+
calculations). The loop terminates early if no further grid
|
|
81
|
+
header is found.
|
|
82
|
+
|
|
83
|
+
6. **Construct return value** -- based on the number of
|
|
84
|
+
magnetization grids found:
|
|
85
|
+
|
|
86
|
+
* 0 grids: ISPIN=1. Returns ``VolumetricData`` with
|
|
87
|
+
``magnetization=None``.
|
|
88
|
+
* 1 grid: ISPIN=2. Returns ``VolumetricData`` with a scalar
|
|
89
|
+
magnetization density (spin-up minus spin-down).
|
|
90
|
+
* 3 grids: SOC. Returns ``SOCVolumetricData`` with the
|
|
91
|
+
three grids stacked into a ``(NGX, NGY, NGZ, 3)``
|
|
92
|
+
magnetization vector field (mx, my, mz). The scalar
|
|
93
|
+
``magnetization`` field is set to the mz component for
|
|
94
|
+
backward compatibility.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
filename : str, optional
|
|
99
|
+
Path to CHGCAR file. Default is ``"CHGCAR"``.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
volumetric : VolumetricData or SOCVolumetricData
|
|
104
|
+
``VolumetricData`` for ISPIN=1 or ISPIN=2 files.
|
|
105
|
+
``SOCVolumetricData`` for SOC files (4 grid blocks).
|
|
106
|
+
|
|
107
|
+
Raises
|
|
108
|
+
------
|
|
109
|
+
ValueError
|
|
110
|
+
If the lattice volume is zero or if the grid dimensions cannot
|
|
111
|
+
be located or the data block is truncated.
|
|
112
|
+
|
|
113
|
+
Notes
|
|
114
|
+
-----
|
|
115
|
+
All returned densities are in units of electrons / Angstrom^3 (the
|
|
116
|
+
raw VASP values, which encode charge * volume, are divided by the
|
|
117
|
+
cell volume). Coordinates are stored in fractional form after
|
|
118
|
+
conversion from Cartesian if necessary. The grid arrays use
|
|
119
|
+
Fortran (column-major) ordering to match the VASP write convention.
|
|
120
|
+
"""
|
|
121
|
+
path: Path = Path(filename)
|
|
122
|
+
with path.open("r") as fid:
|
|
123
|
+
lattice: np.ndarray
|
|
124
|
+
coords: np.ndarray
|
|
125
|
+
symbols: tuple[str, ...]
|
|
126
|
+
atom_counts: list[int]
|
|
127
|
+
lattice, coords, symbols, atom_counts = _read_poscar_header(fid)
|
|
128
|
+
rest_lines: list[str] = [line.rstrip("\n") for line in fid]
|
|
129
|
+
|
|
130
|
+
volume: float = abs(
|
|
131
|
+
float(
|
|
132
|
+
np.dot(
|
|
133
|
+
lattice[0, :],
|
|
134
|
+
np.cross(lattice[1, :], lattice[2, :]),
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
if volume == 0.0:
|
|
139
|
+
msg: str = "CHGCAR lattice volume is zero."
|
|
140
|
+
raise ValueError(msg)
|
|
141
|
+
|
|
142
|
+
first_grid_idx: Optional[int]
|
|
143
|
+
grid_shape: tuple[int, int, int]
|
|
144
|
+
first_grid_idx, grid_shape = _find_next_grid_line(rest_lines, 0)
|
|
145
|
+
if first_grid_idx is None:
|
|
146
|
+
msg: str = "Could not locate CHGCAR charge-density grid dimensions."
|
|
147
|
+
raise ValueError(msg)
|
|
148
|
+
|
|
149
|
+
ngrid: int = int(np.prod(np.asarray(grid_shape, dtype=np.int64)))
|
|
150
|
+
charge_vals: np.ndarray
|
|
151
|
+
end_idx: int
|
|
152
|
+
charge_vals, end_idx = _parse_float_block(
|
|
153
|
+
rest_lines,
|
|
154
|
+
first_grid_idx + 1,
|
|
155
|
+
ngrid,
|
|
156
|
+
)
|
|
157
|
+
charge_grid: np.ndarray = (
|
|
158
|
+
charge_vals.reshape(grid_shape, order="F") / volume
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Read all remaining grid blocks (up to 3 for SOC: mx, my, mz)
|
|
162
|
+
mag_grids: list[np.ndarray] = []
|
|
163
|
+
scan_idx: int = end_idx
|
|
164
|
+
while len(mag_grids) < _N_SOC_MAG_BLOCKS:
|
|
165
|
+
next_idx: Optional[int]
|
|
166
|
+
next_shape: tuple[int, int, int]
|
|
167
|
+
next_idx, next_shape = _find_next_grid_line(rest_lines, scan_idx)
|
|
168
|
+
if next_idx is None:
|
|
169
|
+
break
|
|
170
|
+
ngrid_mag: int = int(np.prod(np.asarray(next_shape, dtype=np.int64)))
|
|
171
|
+
mag_vals: np.ndarray
|
|
172
|
+
mag_vals, scan_idx = _parse_float_block(
|
|
173
|
+
rest_lines,
|
|
174
|
+
next_idx + 1,
|
|
175
|
+
ngrid_mag,
|
|
176
|
+
)
|
|
177
|
+
mag_grids.append(mag_vals.reshape(next_shape, order="F") / volume)
|
|
178
|
+
|
|
179
|
+
lattice_arr: jnp.ndarray = jnp.asarray(lattice, dtype=jnp.float64)
|
|
180
|
+
coords_arr: jnp.ndarray = jnp.asarray(coords, dtype=jnp.float64)
|
|
181
|
+
charge_arr: jnp.ndarray = jnp.asarray(charge_grid, dtype=jnp.float64)
|
|
182
|
+
counts_arr: jnp.ndarray = jnp.asarray(atom_counts, dtype=jnp.int32)
|
|
183
|
+
|
|
184
|
+
if len(mag_grids) == _N_SOC_MAG_BLOCKS:
|
|
185
|
+
# SOC: blocks are mx, my, mz
|
|
186
|
+
mag_vector: np.ndarray = np.stack(mag_grids, axis=-1)
|
|
187
|
+
result_soc: SOCVolumetricData = make_soc_volumetric_data(
|
|
188
|
+
lattice=lattice_arr,
|
|
189
|
+
coords=coords_arr,
|
|
190
|
+
charge=charge_arr,
|
|
191
|
+
magnetization=jnp.asarray(mag_grids[2], dtype=jnp.float64),
|
|
192
|
+
magnetization_vector=jnp.asarray(mag_vector, dtype=jnp.float64),
|
|
193
|
+
grid_shape=grid_shape,
|
|
194
|
+
symbols=symbols,
|
|
195
|
+
atom_counts=counts_arr,
|
|
196
|
+
)
|
|
197
|
+
return result_soc
|
|
198
|
+
|
|
199
|
+
result: VolumetricData = make_volumetric_data(
|
|
200
|
+
lattice=lattice_arr,
|
|
201
|
+
coords=coords_arr,
|
|
202
|
+
charge=charge_arr,
|
|
203
|
+
magnetization=(
|
|
204
|
+
None
|
|
205
|
+
if not mag_grids
|
|
206
|
+
else jnp.asarray(mag_grids[0], dtype=jnp.float64)
|
|
207
|
+
),
|
|
208
|
+
grid_shape=grid_shape,
|
|
209
|
+
symbols=symbols,
|
|
210
|
+
atom_counts=counts_arr,
|
|
211
|
+
)
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _read_poscar_header(
|
|
216
|
+
fid, # noqa: ANN001
|
|
217
|
+
) -> tuple[np.ndarray, np.ndarray, tuple[str, ...], list[int]]:
|
|
218
|
+
"""Read the POSCAR-like header section at the start of a CHGCAR file.
|
|
219
|
+
|
|
220
|
+
Extended Summary
|
|
221
|
+
----------------
|
|
222
|
+
The first section of a CHGCAR file is identical to a POSCAR file:
|
|
223
|
+
comment, scaling factor, 3x3 lattice matrix, optional species names,
|
|
224
|
+
atom counts, optional selective-dynamics flag, coordinate type, and
|
|
225
|
+
atomic positions. This helper reads all of those fields from the
|
|
226
|
+
already-opened file handle and returns them as NumPy arrays / Python
|
|
227
|
+
containers.
|
|
228
|
+
|
|
229
|
+
Implementation Logic
|
|
230
|
+
--------------------
|
|
231
|
+
1. Read and discard the comment line.
|
|
232
|
+
2. Read the universal scaling factor (float).
|
|
233
|
+
3. Read three lines of three floats each for the lattice vectors and
|
|
234
|
+
multiply by the scaling factor.
|
|
235
|
+
4. Read the next line. If it contains no digits, treat it as the
|
|
236
|
+
VASP-5 element-symbol line and advance to the following line for
|
|
237
|
+
atom counts. Otherwise parse atom counts directly.
|
|
238
|
+
5. Compute total atom count as the sum of per-species counts.
|
|
239
|
+
6. Read the coordinate-type line. If it starts with ``'s'`` or
|
|
240
|
+
``'S'``, selective dynamics is present; consume it and read the
|
|
241
|
+
next line for the actual coordinate type.
|
|
242
|
+
7. Determine whether coordinates are Cartesian (``'c'``/``'k'``)
|
|
243
|
+
or direct (fractional).
|
|
244
|
+
8. Read ``natoms`` coordinate lines of three floats each.
|
|
245
|
+
9. If Cartesian, scale by the scaling factor and convert to
|
|
246
|
+
fractional via ``np.linalg.solve(lattice.T, coords.T).T``.
|
|
247
|
+
|
|
248
|
+
Parameters
|
|
249
|
+
----------
|
|
250
|
+
fid : file-like
|
|
251
|
+
Open file handle positioned at the start of the CHGCAR file.
|
|
252
|
+
|
|
253
|
+
Returns
|
|
254
|
+
-------
|
|
255
|
+
lattice : np.ndarray
|
|
256
|
+
Scaled lattice vectors, shape ``(3, 3)``.
|
|
257
|
+
coords : np.ndarray
|
|
258
|
+
Fractional atomic coordinates, shape ``(natoms, 3)``.
|
|
259
|
+
symbols : tuple[str, ...]
|
|
260
|
+
Element symbols (empty tuple for VASP-4 style files).
|
|
261
|
+
atom_counts : list[int]
|
|
262
|
+
Number of atoms per species.
|
|
263
|
+
|
|
264
|
+
Raises
|
|
265
|
+
------
|
|
266
|
+
ValueError
|
|
267
|
+
If a lattice line has fewer than 3 components or a coordinate
|
|
268
|
+
line has fewer than 3 components.
|
|
269
|
+
"""
|
|
270
|
+
_comment: str = fid.readline().strip()
|
|
271
|
+
scale: float = float(fid.readline().strip())
|
|
272
|
+
|
|
273
|
+
lattice: np.ndarray = np.zeros(
|
|
274
|
+
(_LATTICE_ROWS, _XYZ_COMPONENTS),
|
|
275
|
+
dtype=np.float64,
|
|
276
|
+
)
|
|
277
|
+
for row in range(_LATTICE_ROWS):
|
|
278
|
+
vals: list[float] = [float(x) for x in fid.readline().split()]
|
|
279
|
+
if len(vals) < _XYZ_COMPONENTS:
|
|
280
|
+
msg: str = "Invalid CHGCAR lattice line."
|
|
281
|
+
raise ValueError(msg)
|
|
282
|
+
lattice[row, :] = vals[:_XYZ_COMPONENTS]
|
|
283
|
+
lattice = lattice * scale
|
|
284
|
+
|
|
285
|
+
line: str = fid.readline().strip()
|
|
286
|
+
symbols: tuple[str, ...] = ()
|
|
287
|
+
if line and not any(char.isdigit() for char in line):
|
|
288
|
+
symbols = tuple(line.split())
|
|
289
|
+
line = fid.readline().strip()
|
|
290
|
+
atom_counts: list[int] = [int(x) for x in line.split()]
|
|
291
|
+
natoms: int = sum(atom_counts)
|
|
292
|
+
|
|
293
|
+
coord_line: str = fid.readline().strip()
|
|
294
|
+
if coord_line and coord_line[0].lower() == "s":
|
|
295
|
+
coord_line = fid.readline().strip()
|
|
296
|
+
cartesian: bool = bool(coord_line) and coord_line[0].lower() in ("c", "k")
|
|
297
|
+
|
|
298
|
+
coords: np.ndarray = np.zeros((natoms, _XYZ_COMPONENTS), dtype=np.float64)
|
|
299
|
+
for atom_idx in range(natoms):
|
|
300
|
+
vals = [float(x) for x in fid.readline().split()[:_XYZ_COMPONENTS]]
|
|
301
|
+
if len(vals) < _XYZ_COMPONENTS:
|
|
302
|
+
msg: str = "Invalid CHGCAR coordinate line."
|
|
303
|
+
raise ValueError(msg)
|
|
304
|
+
coords[atom_idx, :] = vals
|
|
305
|
+
|
|
306
|
+
if cartesian:
|
|
307
|
+
coords = coords * scale
|
|
308
|
+
coords = np.linalg.solve(lattice.T, coords.T).T
|
|
309
|
+
|
|
310
|
+
return lattice, coords, symbols, atom_counts
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _find_next_grid_line(
|
|
314
|
+
lines: list[str],
|
|
315
|
+
start_idx: int,
|
|
316
|
+
) -> Tuple[Optional[int], Tuple[int, int, int]]:
|
|
317
|
+
"""Find the next line containing exactly three positive integers.
|
|
318
|
+
|
|
319
|
+
Extended Summary
|
|
320
|
+
----------------
|
|
321
|
+
Scans forward through ``lines`` starting at ``start_idx`` to locate
|
|
322
|
+
the next FFT grid-dimension header. In CHGCAR files, each
|
|
323
|
+
volumetric data block is preceded by a single line of three positive
|
|
324
|
+
integers ``NGX NGY NGZ``.
|
|
325
|
+
|
|
326
|
+
Implementation Logic
|
|
327
|
+
--------------------
|
|
328
|
+
1. Iterate from ``start_idx`` to the end of ``lines``.
|
|
329
|
+
2. Skip blank lines and lines that do not split into exactly 3
|
|
330
|
+
tokens.
|
|
331
|
+
3. Attempt to parse all three tokens as integers. If any token
|
|
332
|
+
fails to convert, skip the line.
|
|
333
|
+
4. If all three integers are positive, return the line index and the
|
|
334
|
+
``(NGX, NGY, NGZ)`` tuple.
|
|
335
|
+
5. If no matching line is found, return ``(None, (0, 0, 0))``.
|
|
336
|
+
|
|
337
|
+
Parameters
|
|
338
|
+
----------
|
|
339
|
+
lines : list[str]
|
|
340
|
+
All remaining lines of the CHGCAR file (after the POSCAR
|
|
341
|
+
header has been consumed).
|
|
342
|
+
start_idx : int
|
|
343
|
+
Index within ``lines`` at which to begin scanning.
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
idx : int or None
|
|
348
|
+
Line index of the grid header, or ``None`` if not found.
|
|
349
|
+
grid_shape : tuple[int, int, int]
|
|
350
|
+
``(NGX, NGY, NGZ)`` grid dimensions, or ``(0, 0, 0)`` if
|
|
351
|
+
not found.
|
|
352
|
+
"""
|
|
353
|
+
for idx in range(start_idx, len(lines)):
|
|
354
|
+
stripped: str = lines[idx].strip()
|
|
355
|
+
if not stripped:
|
|
356
|
+
continue
|
|
357
|
+
parts: list[str] = stripped.split()
|
|
358
|
+
if len(parts) != _SCALAR_LINE_COMPONENTS:
|
|
359
|
+
continue
|
|
360
|
+
try:
|
|
361
|
+
values: tuple[int, int, int] = (
|
|
362
|
+
int(parts[0]),
|
|
363
|
+
int(parts[1]),
|
|
364
|
+
int(parts[2]),
|
|
365
|
+
)
|
|
366
|
+
except ValueError:
|
|
367
|
+
continue
|
|
368
|
+
if values[0] > 0 and values[1] > 0 and values[2] > 0:
|
|
369
|
+
return idx, values
|
|
370
|
+
return None, (0, 0, 0)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _parse_float_block(
|
|
374
|
+
lines: list[str],
|
|
375
|
+
start_idx: int,
|
|
376
|
+
nvals: int,
|
|
377
|
+
) -> tuple[np.ndarray, int]:
|
|
378
|
+
"""Parse ``nvals`` whitespace-separated floats starting at ``start_idx``.
|
|
379
|
+
|
|
380
|
+
Extended Summary
|
|
381
|
+
----------------
|
|
382
|
+
Reads a contiguous block of floating-point values spread across
|
|
383
|
+
multiple lines in the CHGCAR file. VASP writes volumetric data in
|
|
384
|
+
rows of typically 5 or 10 values per line, but the exact count can
|
|
385
|
+
vary. This function consumes lines until exactly ``nvals`` values
|
|
386
|
+
have been collected.
|
|
387
|
+
|
|
388
|
+
Implementation Logic
|
|
389
|
+
--------------------
|
|
390
|
+
1. Initialize an empty collection and a running line index.
|
|
391
|
+
2. For each line starting at ``start_idx``:
|
|
392
|
+
|
|
393
|
+
a. Skip blank lines.
|
|
394
|
+
b. Attempt to parse every whitespace-separated token as a float.
|
|
395
|
+
c. If any token fails to parse, treat the entire line as
|
|
396
|
+
non-data (e.g., an augmentation-charge header) and stop
|
|
397
|
+
consuming from it.
|
|
398
|
+
d. Append parsed floats to the collection, taking at most as
|
|
399
|
+
many as needed to reach ``nvals``.
|
|
400
|
+
|
|
401
|
+
3. After the loop, verify that exactly ``nvals`` values were
|
|
402
|
+
collected. Raise ``ValueError`` on a short read.
|
|
403
|
+
|
|
404
|
+
Parameters
|
|
405
|
+
----------
|
|
406
|
+
lines : list[str]
|
|
407
|
+
All remaining lines of the CHGCAR file.
|
|
408
|
+
start_idx : int
|
|
409
|
+
Index within ``lines`` at which to begin reading floats.
|
|
410
|
+
nvals : int
|
|
411
|
+
Total number of floats to collect.
|
|
412
|
+
|
|
413
|
+
Returns
|
|
414
|
+
-------
|
|
415
|
+
values : np.ndarray
|
|
416
|
+
1D array of shape ``(nvals,)`` with dtype ``float64``.
|
|
417
|
+
end_idx : int
|
|
418
|
+
Index of the first line *after* the last consumed line,
|
|
419
|
+
suitable for passing as ``start_idx`` to a subsequent call.
|
|
420
|
+
|
|
421
|
+
Raises
|
|
422
|
+
------
|
|
423
|
+
ValueError
|
|
424
|
+
If the end of ``lines`` is reached before ``nvals`` floats
|
|
425
|
+
have been collected.
|
|
426
|
+
"""
|
|
427
|
+
values: list[float] = []
|
|
428
|
+
idx: int = start_idx
|
|
429
|
+
|
|
430
|
+
while idx < len(lines) and len(values) < nvals:
|
|
431
|
+
stripped: str = lines[idx].strip()
|
|
432
|
+
if not stripped:
|
|
433
|
+
idx += 1
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
parts: list[str] = stripped.split()
|
|
437
|
+
row_vals: list[float] = []
|
|
438
|
+
row_valid: bool = True
|
|
439
|
+
for token in parts:
|
|
440
|
+
try:
|
|
441
|
+
row_vals.append(float(token))
|
|
442
|
+
except ValueError:
|
|
443
|
+
row_valid = False
|
|
444
|
+
break
|
|
445
|
+
if row_valid:
|
|
446
|
+
needed: int = nvals - len(values)
|
|
447
|
+
values.extend(row_vals[:needed])
|
|
448
|
+
idx += 1
|
|
449
|
+
|
|
450
|
+
if len(values) != nvals:
|
|
451
|
+
msg: str = "Unexpected end of CHGCAR data block."
|
|
452
|
+
raise ValueError(msg)
|
|
453
|
+
|
|
454
|
+
return np.asarray(values, dtype=np.float64), idx
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
__all__: list[str] = [
|
|
458
|
+
"read_chgcar",
|
|
459
|
+
]
|