diff --git a/pymatgen/io/vasp/outputs.py b/pymatgen/io/vasp/outputs.py index ef7577ea059..f1b477d9bc0 100644 --- a/pymatgen/io/vasp/outputs.py +++ b/pymatgen/io/vasp/outputs.py @@ -287,6 +287,7 @@ def __init__( parse_projected_eigen=False, parse_potcar_file=True, occu_tol=1e-8, + separate_spins=False, exception_on_bad_xml=True, ): """ @@ -325,6 +326,10 @@ def __init__( occu_tol (float): Sets the minimum tol for the determination of the vbm and cbm. Usually the default of 1e-8 works well enough, but there may be pathological cases. + separate_spins (bool): Whether the band gap, CBM, and VBM should be + reported for each individual spin channel. Defaults to False, + which computes the eigenvalue band properties independent of + the spin orientation. If True, the calculation must be spin-polarized. exception_on_bad_xml (bool): Whether to throw a ParseException if a malformed XML is detected. Default to True, which ensures only proper vasprun.xml are parsed. You can set to False if you want @@ -335,6 +340,7 @@ def __init__( self.ionic_step_skip = ionic_step_skip self.ionic_step_offset = ionic_step_offset self.occu_tol = occu_tol + self.separate_spins = separate_spins self.exception_on_bad_xml = exception_on_bad_xml with zopen(filename, "rt") as f: @@ -346,7 +352,7 @@ def __init__( preamble = steps.pop(0) self.nionic_steps = len(steps) new_steps = steps[ionic_step_offset :: int(ionic_step_skip)] - # add the tailing informat in the last step from the run + # add the tailing information in the last step from the run to_parse = "".join(new_steps) if steps[-1] != new_steps[-1]: to_parse = "{}{}{}".format(preamble, to_parse, steps[-1].split("")[-1]) @@ -952,13 +958,26 @@ def get_band_structure( def eigenvalue_band_properties(self): """ Band properties from the eigenvalues as a tuple, - (band gap, cbm, vbm, is_band_gap_direct). + (band gap, cbm, vbm, is_band_gap_direct). In the case of separate_spins=True, + the band gap, cbm, vbm, and is_band_gap_direct are each lists of length 2, + with index 0 representing the spin-up channel and index 1 representing + the spin-down channel. """ vbm = -float("inf") vbm_kpoint = None cbm = float("inf") cbm_kpoint = None + vbm_spins = [] + vbm_spins_kpoints = [] + cbm_spins = [] + cbm_spins_kpoints = [] + if self.separate_spins and len(self.eigenvalues.keys()) != 2: + raise ValueError("The separate_spins flag can only be True if ISPIN = 2") + for spin, d in self.eigenvalues.items(): + if self.separate_spins: + vbm = -float("inf") + cbm = float("inf") for k, val in enumerate(d): for (eigenval, occu) in val: if occu > self.occu_tol and eigenval > vbm: @@ -967,6 +986,18 @@ def eigenvalue_band_properties(self): elif occu <= self.occu_tol and eigenval < cbm: cbm = eigenval cbm_kpoint = k + if self.separate_spins: + vbm_spins.append(vbm) + vbm_spins_kpoints.append(vbm_kpoint) + cbm_spins.append(cbm) + cbm_spins_kpoints.append(cbm_kpoint) + if self.separate_spins: + return ( + [max(cbm_spins[0] - vbm_spins[0], 0), max(cbm_spins[1] - vbm_spins[1], 0)], + [cbm_spins[0], cbm_spins[1]], + [vbm_spins[0], vbm_spins[1]], + [vbm_spins_kpoints[0] == cbm_spins_kpoints[0], vbm_spins_kpoints[1] == cbm_spins_kpoints[1]], + ) return max(cbm - vbm, 0), cbm, vbm, vbm_kpoint == cbm_kpoint def calculate_efermi(self): @@ -1451,6 +1482,7 @@ def __init__( parse_projected_eigen: Union[bool, str] = False, parse_potcar_file: Union[bool, str] = False, occu_tol: float = 1e-8, + separate_spins: bool = False, ): """ Args: @@ -1468,9 +1500,14 @@ def __init__( occu_tol: Sets the minimum tol for the determination of the vbm and cbm. Usually the default of 1e-8 works well enough, but there may be pathological cases. + separate_spins (bool): Whether the band gap, CBM, and VBM should be + reported for each individual spin channel. Defaults to False, + which computes the eigenvalue band properties independent of + the spin orientation. If True, the calculation must be spin-polarized. """ self.filename = filename self.occu_tol = occu_tol + self.separate_spins = separate_spins with zopen(filename, "rt") as f: self.efermi = None @@ -5223,13 +5260,17 @@ class Eigenval: kpoint index is 0-based (unlike the 1-based indexing in VASP). """ - def __init__(self, filename, occu_tol=1e-8): + def __init__(self, filename, occu_tol=1e-8, separate_spins=False): """ Reads input from filename to construct Eigenval object Args: filename (str): filename of EIGENVAL to read in occu_tol (float): tolerance for determining band gap + separate_spins (bool): whether the band gap, CBM, and VBM should be + reported for each individual spin channel. Defaults to False, + which computes the eigenvalue band properties independent of + the spin orientation. If True, the calculation must be spin-polarized. Returns: a pymatgen.io.vasp.outputs.Eigenval object @@ -5237,6 +5278,7 @@ def __init__(self, filename, occu_tol=1e-8): self.filename = filename self.occu_tol = occu_tol + self.separate_spins = separate_spins with zopen(filename, "r") as f: self.ispin = int(f.readline().split()[-1]) @@ -5279,14 +5321,26 @@ def __init__(self, filename, occu_tol=1e-8): def eigenvalue_band_properties(self): """ Band properties from the eigenvalues as a tuple, - (band gap, cbm, vbm, is_band_gap_direct). + (band gap, cbm, vbm, is_band_gap_direct). In the case of separate_spins=True, + the band gap, cbm, vbm, and is_band_gap_direct are each lists of length 2, + with index 0 representing the spin-up channel and index 1 representing + the spin-down channel. """ - vbm = -float("inf") vbm_kpoint = None cbm = float("inf") cbm_kpoint = None + vbm_spins = [] + vbm_spins_kpoints = [] + cbm_spins = [] + cbm_spins_kpoints = [] + if self.separate_spins and len(self.eigenvalues.keys()) != 2: + raise ValueError("The separate_spins flag can only be True if ISPIN = 2") + for spin, d in self.eigenvalues.items(): + if self.separate_spins: + vbm = -float("inf") + cbm = float("inf") for k, val in enumerate(d): for (eigenval, occu) in val: if occu > self.occu_tol and eigenval > vbm: @@ -5295,6 +5349,18 @@ def eigenvalue_band_properties(self): elif occu <= self.occu_tol and eigenval < cbm: cbm = eigenval cbm_kpoint = k + if self.separate_spins: + vbm_spins.append(vbm) + vbm_spins_kpoints.append(vbm_kpoint) + cbm_spins.append(cbm) + cbm_spins_kpoints.append(cbm_kpoint) + if self.separate_spins: + return ( + [max(cbm_spins[0] - vbm_spins[0], 0), max(cbm_spins[1] - vbm_spins[1], 0)], + [cbm_spins[0], cbm_spins[1]], + [vbm_spins[0], vbm_spins[1]], + [vbm_spins_kpoints[0] == cbm_spins_kpoints[0], vbm_spins_kpoints[1] == cbm_spins_kpoints[1]], + ) return max(cbm - vbm, 0), cbm, vbm, vbm_kpoint == cbm_kpoint diff --git a/pymatgen/io/vasp/tests/test_outputs.py b/pymatgen/io/vasp/tests/test_outputs.py index 23a631334ec..14a6b0f3401 100644 --- a/pymatgen/io/vasp/tests/test_outputs.py +++ b/pymatgen/io/vasp/tests/test_outputs.py @@ -710,6 +710,21 @@ def test_kpointset_electronvelocities(self): vasprun = Vasprun(vpath, parse_potcar_file=False) self.assertEqual(vasprun.eigenvalues[Spin.up].shape[0], len(vasprun.actual_kpoints)) + def test_eigenvalue_band_properties_separate_spins(self): + eig = Vasprun(self.TEST_FILES_DIR / "vasprun_eig_separate_spins.xml.gz", separate_spins=True) + props = eig.eigenvalue_band_properties + eig2 = Vasprun(self.TEST_FILES_DIR / "vasprun_eig_separate_spins.xml.gz", separate_spins=False) + props2 = eig2.eigenvalue_band_properties + self.assertAlmostEqual(props[0][0], 2.8772, places=4) + self.assertAlmostEqual(props[0][1], 1.2810, places=4) + self.assertAlmostEqual(props[1][0], 3.6741, places=4) + self.assertAlmostEqual(props[1][1], 1.6225, places=4) + self.assertAlmostEqual(props[2][0], 0.7969, places=4) + self.assertAlmostEqual(props[2][1], 0.3415, places=4) + self.assertAlmostEqual(props2[0], np.min(props[1]) - np.max(props[2]), places=4) + self.assertEqual(props[3][0], True) + self.assertEqual(props[3][1], True) + class OutcarTest(PymatgenTest): _multiprocess_shared_ = True @@ -2017,6 +2032,21 @@ def test_eigenvalue_band_properties(self): self.assertAlmostEqual(props[2], 1.1434, places=4) self.assertEqual(props[3], False) + def test_eigenvalue_band_properties_separate_spins(self): + eig = Eigenval(self.TEST_FILES_DIR / "EIGENVAL_separate_spins.gz", separate_spins=True) + props = eig.eigenvalue_band_properties + eig2 = Eigenval(self.TEST_FILES_DIR / "EIGENVAL_separate_spins.gz", separate_spins=False) + props2 = eig2.eigenvalue_band_properties + self.assertAlmostEqual(props[0][0], 2.8772, places=4) + self.assertAlmostEqual(props[0][1], 1.2810, places=4) + self.assertAlmostEqual(props[1][0], 3.6741, places=4) + self.assertAlmostEqual(props[1][1], 1.6225, places=4) + self.assertAlmostEqual(props[2][0], 0.7969, places=4) + self.assertAlmostEqual(props[2][1], 0.3415, places=4) + self.assertAlmostEqual(props2[0], np.min(props[1]) - np.max(props[2]), places=4) + self.assertEqual(props[3][0], True) + self.assertEqual(props[3][1], True) + class WavederTest(PymatgenTest): _multiprocess_shared_ = True diff --git a/test_files/EIGENVAL_separate_spins.gz b/test_files/EIGENVAL_separate_spins.gz new file mode 100644 index 00000000000..d69c410afa7 Binary files /dev/null and b/test_files/EIGENVAL_separate_spins.gz differ diff --git a/test_files/vasprun_eig_separate_spins.xml.gz b/test_files/vasprun_eig_separate_spins.xml.gz new file mode 100644 index 00000000000..889f38f2e8a Binary files /dev/null and b/test_files/vasprun_eig_separate_spins.xml.gz differ