From 3abb3068f2c557827d4ac56885916e8b5cb5f16a Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Wed, 2 Aug 2023 14:16:40 -0700 Subject: [PATCH 001/180] Move tests to their own folder. --- tests/io/lobster/__init__.py | 0 tests/io/lobster/test_lobster.py | 2470 +++++++++++++++++++++++++++ tests/io/lobster/test_lobsterenv.py | 832 +++++++++ 3 files changed, 3302 insertions(+) create mode 100644 tests/io/lobster/__init__.py create mode 100644 tests/io/lobster/test_lobster.py create mode 100644 tests/io/lobster/test_lobsterenv.py diff --git a/tests/io/lobster/__init__.py b/tests/io/lobster/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/io/lobster/test_lobster.py b/tests/io/lobster/test_lobster.py new file mode 100644 index 0000000000..62383774e1 --- /dev/null +++ b/tests/io/lobster/test_lobster.py @@ -0,0 +1,2470 @@ +from __future__ import annotations + +import json +import os +import tempfile +import unittest +import warnings + +import numpy as np +import pytest +from numpy.testing import assert_array_equal +from pytest import approx + +from pymatgen.core.structure import Structure +from pymatgen.electronic_structure.core import Orbital, Spin +from pymatgen.io.lobster import ( + Bandoverlaps, + Charge, + Cohpcar, + Doscar, + Fatband, + Grosspop, + Icohplist, + Lobsterin, + Lobsterout, + MadelungEnergies, + SitePotential, + Wavefunction, +) +from pymatgen.io.lobster.inputs import get_all_possible_basis_combinations +from pymatgen.io.vasp import Vasprun +from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar +from pymatgen.util.testing import PymatgenTest + +__author__ = "Janine George, Marco Esters" +__copyright__ = "Copyright 2017, The Materials Project" +__version__ = "0.2" +__email__ = "janine.george@uclouvain.be, esters@uoregon.edu" +__date__ = "Dec 10, 2017" + +test_dir_doscar = PymatgenTest.TEST_FILES_DIR + +this_dir = os.path.dirname(os.path.abspath(__file__)) + + +class CohpcarTest(PymatgenTest): + def setUp(self): + self.cohp_bise = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.BiSe.gz") + self.coop_bise = Cohpcar( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COOPCAR.lobster.BiSe.gz", + are_coops=True, + ) + self.cohp_fe = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COOPCAR.lobster.gz") + self.coop_fe = Cohpcar( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COOPCAR.lobster.gz", + are_coops=True, + ) + self.orb = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.orbitalwise.gz") + self.orb_notot = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.notot.orbitalwise.gz") + + # Lobster 3.1 (Test data is from prerelease of Lobster 3.1) + self.cohp_KF = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.KF.gz") + self.coop_KF = Cohpcar( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.KF.gz", + are_coops=True, + ) + + # example with f electrons + self.cohp_Na2UO4 = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.Na2UO4.gz") + self.coop_Na2UO4 = Cohpcar( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COOPCAR.lobster.Na2UO4.gz", + are_coops=True, + ) + self.cobi = Cohpcar( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COBICAR.lobster.gz", + are_cobis=True, + ) + + def test_attributes(self): + assert not self.cohp_bise.are_coops + assert self.coop_bise.are_coops + assert not self.cohp_bise.is_spin_polarized + assert not self.coop_bise.is_spin_polarized + assert not self.cohp_fe.are_coops + assert self.coop_fe.are_coops + assert self.cohp_fe.is_spin_polarized + assert self.coop_fe.is_spin_polarized + assert len(self.cohp_bise.energies) == 241 + assert len(self.coop_bise.energies) == 241 + assert len(self.cohp_fe.energies) == 301 + assert len(self.coop_fe.energies) == 301 + assert len(self.cohp_bise.cohp_data) == 12 + assert len(self.coop_bise.cohp_data) == 12 + assert len(self.cohp_fe.cohp_data) == 3 + assert len(self.coop_fe.cohp_data) == 3 + + # Lobster 3.1 + assert not self.cohp_KF.are_coops + assert self.coop_KF.are_coops + assert not self.cohp_KF.is_spin_polarized + assert not self.coop_KF.is_spin_polarized + assert len(self.cohp_KF.energies) == 6 + assert len(self.coop_KF.energies) == 6 + assert len(self.cohp_KF.cohp_data) == 7 + assert len(self.coop_KF.cohp_data) == 7 + + # Lobster 4.1.0 + assert not self.cohp_KF.are_cobis + assert not self.coop_KF.are_cobis + assert not self.cobi.are_coops + assert self.cobi.are_cobis + assert not self.cobi.is_spin_polarized + + def test_energies(self): + efermi_bise = 5.90043 + elim_bise = (-0.124679, 11.9255) + efermi_fe = 9.75576 + elim_fe = (-0.277681, 14.7725) + efermi_KF = -2.87475 + elim_KF = (-11.25000 + efermi_KF, 7.5000 + efermi_KF) + + assert self.cohp_bise.efermi == efermi_bise + assert self.coop_bise.efermi == efermi_bise + assert self.cohp_fe.efermi == efermi_fe + assert self.coop_fe.efermi == efermi_fe + # Lobster 3.1 + assert self.cohp_KF.efermi == efermi_KF + assert self.coop_KF.efermi == efermi_KF + + assert self.cohp_bise.energies[0] + self.cohp_bise.efermi == approx(elim_bise[0], abs=1e-4) + assert self.cohp_bise.energies[-1] + self.cohp_bise.efermi == approx(elim_bise[1], abs=1e-4) + assert self.coop_bise.energies[0] + self.coop_bise.efermi == approx(elim_bise[0], abs=1e-4) + assert self.coop_bise.energies[-1] + self.coop_bise.efermi == approx(elim_bise[1], abs=1e-4) + + assert self.cohp_fe.energies[0] + self.cohp_fe.efermi == approx(elim_fe[0], abs=1e-4) + assert self.cohp_fe.energies[-1] + self.cohp_fe.efermi == approx(elim_fe[1], abs=1e-4) + assert self.coop_fe.energies[0] + self.coop_fe.efermi == approx(elim_fe[0], abs=1e-4) + assert self.coop_fe.energies[-1] + self.coop_fe.efermi == approx(elim_fe[1], abs=1e-4) + + # Lobster 3.1 + assert self.cohp_KF.energies[0] + self.cohp_KF.efermi == approx(elim_KF[0], abs=1e-4) + assert self.cohp_KF.energies[-1] + self.cohp_KF.efermi == approx(elim_KF[1], abs=1e-4) + assert self.coop_KF.energies[0] + self.coop_KF.efermi == approx(elim_KF[0], abs=1e-4) + assert self.coop_KF.energies[-1] + self.coop_KF.efermi == approx(elim_KF[1], abs=1e-4) + + def test_cohp_data(self): + lengths_sites_bise = { + "1": (2.882308829886294, (0, 6)), + "2": (3.1014396233274444, (0, 9)), + "3": (2.8823088298862083, (1, 7)), + "4": (3.1014396233275434, (1, 8)), + "5": (3.0500070394403904, (2, 9)), + "6": (2.9167594580335807, (2, 10)), + "7": (3.05000703944039, (3, 8)), + "8": (2.9167594580335803, (3, 11)), + "9": (3.3752173204052101, (4, 11)), + "10": (3.0729354518345948, (4, 5)), + "11": (3.3752173204052101, (5, 10)), + } + lengths_sites_fe = { + "1": (2.8318907764979082, (7, 6)), + "2": (2.4524893531900283, (7, 8)), + } + # Lobster 3.1 + lengths_sites_KF = { + "1": (2.7119923200622269, (0, 1)), + "2": (2.7119923200622269, (0, 1)), + "3": (2.7119923576010501, (0, 1)), + "4": (2.7119923576010501, (0, 1)), + "5": (2.7119923200622269, (0, 1)), + "6": (2.7119923200622269, (0, 1)), + } + + for data in [self.cohp_bise.cohp_data, self.coop_bise.cohp_data]: + for bond, val in data.items(): + if bond != "average": + assert val["length"] == lengths_sites_bise[bond][0] + assert val["sites"] == lengths_sites_bise[bond][1] + assert len(val["COHP"][Spin.up]) == 241 + assert len(val["ICOHP"][Spin.up]) == 241 + for data in [self.cohp_fe.cohp_data, self.coop_fe.cohp_data]: + for bond, val in data.items(): + if bond != "average": + assert val["length"] == lengths_sites_fe[bond][0] + assert val["sites"] == lengths_sites_fe[bond][1] + assert len(val["COHP"][Spin.up]) == 301 + assert len(val["ICOHP"][Spin.up]) == 301 + + # Lobster 3.1 + for data in [self.cohp_KF.cohp_data, self.coop_KF.cohp_data]: + for bond, val in data.items(): + if bond != "average": + assert val["length"] == lengths_sites_KF[bond][0] + assert val["sites"] == lengths_sites_KF[bond][1] + assert len(val["COHP"][Spin.up]) == 6 + assert len(val["ICOHP"][Spin.up]) == 6 + + def test_orbital_resolved_cohp(self): + orbitals = [(Orbital(i), Orbital(j)) for j in range(4) for i in range(4)] + assert self.cohp_bise.orb_res_cohp is None + assert self.coop_bise.orb_res_cohp is None + assert self.cohp_fe.orb_res_cohp is None + assert self.coop_fe.orb_res_cohp is None + assert self.orb_notot.cohp_data["1"]["COHP"] is None + assert self.orb_notot.cohp_data["1"]["ICOHP"] is None + for orbs in self.orb.orb_res_cohp["1"]: + orb_set = self.orb.orb_res_cohp["1"][orbs]["orbitals"] + assert orb_set[0][0] == 4 + assert orb_set[1][0] == 4 + assert (orb_set[0][1], orb_set[1][1]) in orbitals + + # test d and f orbitals + comparelist = [ + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 5, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 6, + 7, + 7, + 7, + 7, + ] + comparelist2 = [ + "f0", + "f0", + "f0", + "f0", + "f1", + "f1", + "f1", + "f1", + "f2", + "f2", + "f2", + "f2", + "f3", + "f3", + "f3", + "f3", + "f_1", + "f_1", + "f_1", + "f_1", + "f_2", + "f_2", + "f_2", + "f_2", + "f_3", + "f_3", + "f_3", + "f_3", + "dx2", + "dx2", + "dx2", + "dx2", + "dxy", + "dxy", + "dxy", + "dxy", + "dxz", + "dxz", + "dxz", + "dxz", + "dyz", + "dyz", + "dyz", + "dyz", + "dz2", + "dz2", + "dz2", + "dz2", + "px", + "px", + "px", + "px", + "py", + "py", + "py", + "py", + "pz", + "pz", + "pz", + "pz", + "s", + "s", + "s", + "s", + "s", + "s", + "s", + "s", + ] + for iorb, orbs in enumerate(sorted(self.cohp_Na2UO4.orb_res_cohp["49"])): + orb_set = self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["orbitals"] + assert orb_set[0][0] == comparelist[iorb] + assert str(orb_set[0][1]) == comparelist2[iorb] + + # The sum of the orbital-resolved COHPs should be approximately + # the total COHP. Due to small deviations in the LOBSTER calculation, + # the precision is not very high though. + cohp = self.orb.cohp_data["1"]["COHP"][Spin.up] + icohp = self.orb.cohp_data["1"]["ICOHP"][Spin.up] + tot = np.sum( + [self.orb.orb_res_cohp["1"][orbs]["COHP"][Spin.up] for orbs in self.orb.orb_res_cohp["1"]], + axis=0, + ) + self.assert_all_close(tot, cohp, decimal=3) + tot = np.sum( + [self.orb.orb_res_cohp["1"][orbs]["ICOHP"][Spin.up] for orbs in self.orb.orb_res_cohp["1"]], + axis=0, + ) + self.assert_all_close(tot, icohp, decimal=3) + + # Lobster 3.1 + cohp_KF = self.cohp_KF.cohp_data["1"]["COHP"][Spin.up] + icohp_KF = self.cohp_KF.cohp_data["1"]["ICOHP"][Spin.up] + tot_KF = np.sum( + [self.cohp_KF.orb_res_cohp["1"][orbs]["COHP"][Spin.up] for orbs in self.cohp_KF.orb_res_cohp["1"]], + axis=0, + ) + self.assert_all_close(tot_KF, cohp_KF, decimal=3) + tot_KF = np.sum( + [self.cohp_KF.orb_res_cohp["1"][orbs]["ICOHP"][Spin.up] for orbs in self.cohp_KF.orb_res_cohp["1"]], + axis=0, + ) + self.assert_all_close(tot_KF, icohp_KF, decimal=3) + + # d and f orbitals + cohp_Na2UO4 = self.cohp_Na2UO4.cohp_data["49"]["COHP"][Spin.up] + icohp_Na2UO4 = self.cohp_Na2UO4.cohp_data["49"]["ICOHP"][Spin.up] + tot_Na2UO4 = np.sum( + [ + self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["COHP"][Spin.up] + for orbs in self.cohp_Na2UO4.orb_res_cohp["49"] + ], + axis=0, + ) + self.assert_all_close(tot_Na2UO4, cohp_Na2UO4, decimal=3) + tot_Na2UO4 = np.sum( + [ + self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["ICOHP"][Spin.up] + for orbs in self.cohp_Na2UO4.orb_res_cohp["49"] + ], + axis=0, + ) + self.assert_all_close(tot_Na2UO4, icohp_Na2UO4, decimal=3) + + +class IcohplistTest(unittest.TestCase): + def setUp(self): + self.icohp_bise = Icohplist(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOHPLIST.lobster.BiSe") + self.icoop_bise = Icohplist( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOOPLIST.lobster.BiSe", + are_coops=True, + ) + self.icohp_fe = Icohplist(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOHPLIST.lobster") + # allow gzipped files + self.icohp_gzipped = Icohplist(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOHPLIST.lobster.gz") + self.icoop_fe = Icohplist( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOHPLIST.lobster", + are_coops=True, + ) + # ICOBIs and orbitalwise ICOBILIST.lobster + self.icobi_orbitalwise = Icohplist( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOBILIST.lobster", + are_cobis=True, + ) + # TODO: test orbitalwise ICOHPs with and without spin polarization + + self.icobi = Icohplist( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOBILIST.lobster.withoutorbitals", + are_cobis=True, + ) + self.icobi_orbitalwise_spinpolarized = Icohplist( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOBILIST.lobster.spinpolarized", + are_cobis=True, + ) + # make sure the correct line is read to check if this is a orbitalwise ICOBILIST + self.icobi_orbitalwise_add = Icohplist( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOBILIST.lobster.additional_case", + are_cobis=True, + ) + self.icobi_orbitalwise_spinpolarized_add = Icohplist( + filename=os.path.join( + PymatgenTest.TEST_FILES_DIR, + "cohp", + "ICOBILIST.lobster.spinpolarized.additional_case", + ), + are_cobis=True, + ) + + def test_attributes(self): + assert not self.icohp_bise.are_coops + assert self.icoop_bise.are_coops + assert not self.icohp_bise.is_spin_polarized + assert not self.icoop_bise.is_spin_polarized + assert len(self.icohp_bise.icohplist) == 11 + assert len(self.icoop_bise.icohplist) == 11 + assert not self.icohp_fe.are_coops + assert self.icoop_fe.are_coops + assert self.icohp_fe.is_spin_polarized + assert self.icoop_fe.is_spin_polarized + assert len(self.icohp_fe.icohplist) == 2 + assert len(self.icoop_fe.icohplist) == 2 + # test are_cobis + assert not self.icohp_fe.are_coops + assert not self.icohp_fe.are_cobis + assert self.icoop_fe.are_coops + assert not self.icoop_fe.are_cobis + assert self.icobi.are_cobis + assert not self.icobi.are_coops + + # orbitalwise + assert self.icobi_orbitalwise.orbitalwise + assert not self.icobi.orbitalwise + + assert self.icobi_orbitalwise_spinpolarized.orbitalwise + + assert self.icobi_orbitalwise_add.orbitalwise + assert self.icobi_orbitalwise_spinpolarized_add.orbitalwise + + def test_values(self): + icohplist_bise = { + "1": { + "length": 2.88231, + "number_of_bonds": 3, + "icohp": {Spin.up: -2.18042}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "2": { + "length": 3.10144, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.14347}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "3": { + "length": 2.88231, + "number_of_bonds": 3, + "icohp": {Spin.up: -2.18042}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "4": { + "length": 3.10144, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.14348}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "5": { + "length": 3.05001, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.30006}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "6": { + "length": 2.91676, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.96843}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "7": { + "length": 3.05001, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.30006}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "8": { + "length": 2.91676, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.96843}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "9": { + "length": 3.37522, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.47531}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "10": { + "length": 3.07294, + "number_of_bonds": 3, + "icohp": {Spin.up: -2.38796}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "11": { + "length": 3.37522, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.47531}, + "translation": [0, 0, 0], + "orbitals": None, + }, + } + icooplist_bise = { + "1": { + "length": 2.88231, + "number_of_bonds": 3, + "icohp": {Spin.up: 0.14245}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "2": { + "length": 3.10144, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.04118}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "3": { + "length": 2.88231, + "number_of_bonds": 3, + "icohp": {Spin.up: 0.14245}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "4": { + "length": 3.10144, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.04118}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "5": { + "length": 3.05001, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.03516}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "6": { + "length": 2.91676, + "number_of_bonds": 3, + "icohp": {Spin.up: 0.10745}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "7": { + "length": 3.05001, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.03516}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "8": { + "length": 2.91676, + "number_of_bonds": 3, + "icohp": {Spin.up: 0.10745}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "9": { + "length": 3.37522, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.12395}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "10": { + "length": 3.07294, + "number_of_bonds": 3, + "icohp": {Spin.up: 0.24714}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "11": { + "length": 3.37522, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.12395}, + "translation": [0, 0, 0], + "orbitals": None, + }, + } + icooplist_fe = { + "1": { + "length": 2.83189, + "number_of_bonds": 2, + "icohp": {Spin.up: -0.10218, Spin.down: -0.19701}, + "translation": [0, 0, 0], + "orbitals": None, + }, + "2": { + "length": 2.45249, + "number_of_bonds": 1, + "icohp": {Spin.up: -0.28485, Spin.down: -0.58279}, + "translation": [0, 0, 0], + "orbitals": None, + }, + } + + assert icohplist_bise == self.icohp_bise.icohplist + assert self.icohp_bise.icohpcollection.extremum_icohpvalue() == -2.38796 + assert icooplist_fe == self.icoop_fe.icohplist + assert self.icoop_fe.icohpcollection.extremum_icohpvalue() == -0.29919 + assert icooplist_bise == self.icoop_bise.icohplist + assert self.icoop_bise.icohpcollection.extremum_icohpvalue() == 0.24714 + assert self.icobi.icohplist["1"]["icohp"][Spin.up] == approx(0.58649) + assert self.icobi_orbitalwise.icohplist["2"]["icohp"][Spin.up] == approx(0.58649) + assert self.icobi_orbitalwise.icohplist["1"]["icohp"][Spin.up] == approx(0.58649) + assert self.icobi_orbitalwise_spinpolarized.icohplist["1"]["icohp"][Spin.up] == approx(0.58649 / 2, abs=1e-3) + assert self.icobi_orbitalwise_spinpolarized.icohplist["1"]["icohp"][Spin.down] == approx(0.58649 / 2, abs=1e-3) + assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["icohp"][Spin.down] == approx(0.58649 / 2, abs=1e-3) + assert self.icobi.icohpcollection.extremum_icohpvalue() == 0.58649 + assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["orbitals"]["2s-6s"]["icohp"][Spin.up] == 0.0247 + + +class DoscarTest(unittest.TestCase): + def setUp(self): + # first for spin polarized version + doscar = os.path.join(test_dir_doscar, "DOSCAR.lobster.spin") + poscar = os.path.join(test_dir_doscar, "POSCAR.lobster.spin_DOS") + # not spin polarized + doscar2 = os.path.join(test_dir_doscar, "DOSCAR.lobster.nonspin") + poscar2 = os.path.join(test_dir_doscar, "POSCAR.lobster.nonspin_DOS") + os.path.join(test_dir_doscar, "DOSCAR.lobster.nonspin_zip.gz") + os.path.join(test_dir_doscar, "POSCAR.lobster.nonspin_DOS_zip.gz") + self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) + self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) + + self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) + self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) + + with open(os.path.join(test_dir_doscar, "structure_KF.json")) as f: + data = json.load(f) + + self.structure = Structure.from_dict(data) + + # test structure argument + self.DOSCAR_spin_pol2 = Doscar(doscar=doscar, structure_file=None, structure=Structure.from_file(poscar)) + + def test_complete_dos(self): + # first for spin polarized version + energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] + tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] + fermi = 0.0 + + PDOS_F_2s_up = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + PDOS_F_2s_down = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + PDOS_F_2py_up = [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029] + PDOS_F_2py_down = [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029] + PDOS_F_2pz_up = [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029] + PDOS_F_2pz_down = [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029] + PDOS_F_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] + PDOS_F_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] + + assert energies_spin == self.DOSCAR_spin_pol.completedos.energies.tolist() + assert tdos_up == self.DOSCAR_spin_pol.completedos.densities[Spin.up].tolist() + assert tdos_down == self.DOSCAR_spin_pol.completedos.densities[Spin.down].tolist() + assert fermi == approx(self.DOSCAR_spin_pol.completedos.efermi) + + assert np.allclose( + self.DOSCAR_spin_pol.completedos.structure.frac_coords, + self.structure.frac_coords, + ) + assert np.allclose( + self.DOSCAR_spin_pol2.completedos.structure.frac_coords, + self.structure.frac_coords, + ) + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == PDOS_F_2s_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.down].tolist() == PDOS_F_2s_down + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == PDOS_F_2py_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.down].tolist() == PDOS_F_2py_down + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == PDOS_F_2pz_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.down].tolist() == PDOS_F_2pz_down + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == PDOS_F_2px_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.down].tolist() == PDOS_F_2px_down + + energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] + PDOS_F_2s = [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060] + PDOS_F_2py = [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037] + PDOS_F_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] + PDOS_F_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] + + assert energies_nonspin == self.DOSCAR_nonspin_pol.completedos.energies.tolist() + + assert tdos_nonspin == self.DOSCAR_nonspin_pol.completedos.densities[Spin.up].tolist() + + assert fermi == approx(self.DOSCAR_nonspin_pol.completedos.efermi) + + assert self.DOSCAR_nonspin_pol.completedos.structure == self.structure + + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == PDOS_F_2s + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == PDOS_F_2py + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == PDOS_F_2pz + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == PDOS_F_2px + + def test_pdos(self): + # first for spin polarized version + + PDOS_F_2s_up = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + PDOS_F_2s_down = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + PDOS_F_2py_up = [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029] + PDOS_F_2py_down = [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029] + PDOS_F_2pz_up = [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029] + PDOS_F_2pz_down = [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029] + PDOS_F_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] + PDOS_F_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] + + assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.up].tolist() == PDOS_F_2s_up + assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.down].tolist() == PDOS_F_2s_down + assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.up].tolist() == PDOS_F_2py_up + assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.down].tolist() == PDOS_F_2py_down + assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.up].tolist() == PDOS_F_2pz_up + assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.down].tolist() == PDOS_F_2pz_down + assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.up].tolist() == PDOS_F_2px_up + assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.down].tolist() == PDOS_F_2px_down + + # non spin + PDOS_F_2s = [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060] + PDOS_F_2py = [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037] + PDOS_F_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] + PDOS_F_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] + + assert self.DOSCAR_nonspin_pol.pdos[0]["2s"][Spin.up].tolist() == PDOS_F_2s + assert self.DOSCAR_nonspin_pol.pdos[0]["2p_y"][Spin.up].tolist() == PDOS_F_2py + assert self.DOSCAR_nonspin_pol.pdos[0]["2p_z"][Spin.up].tolist() == PDOS_F_2pz + assert self.DOSCAR_nonspin_pol.pdos[0]["2p_x"][Spin.up].tolist() == PDOS_F_2px + + def test_tdos(self): + # first for spin polarized version + energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] + tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] + fermi = 0.0 + + assert energies_spin == self.DOSCAR_spin_pol.tdos.energies.tolist() + assert tdos_up == self.DOSCAR_spin_pol.tdos.densities[Spin.up].tolist() + assert tdos_down == self.DOSCAR_spin_pol.tdos.densities[Spin.down].tolist() + assert fermi == approx(self.DOSCAR_spin_pol.tdos.efermi) + + energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] + fermi = 0.0 + + assert energies_nonspin == self.DOSCAR_nonspin_pol.tdos.energies.tolist() + assert tdos_nonspin == self.DOSCAR_nonspin_pol.tdos.densities[Spin.up].tolist() + assert fermi == approx(self.DOSCAR_nonspin_pol.tdos.efermi) + + def test_energies(self): + # first for spin polarized version + energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + + assert energies_spin == self.DOSCAR_spin_pol.energies.tolist() + + energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + assert energies_nonspin == self.DOSCAR_nonspin_pol.energies.tolist() + + def test_tdensities(self): + # first for spin polarized version + tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] + tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] + + assert tdos_up == self.DOSCAR_spin_pol.tdensities[Spin.up].tolist() + assert tdos_down == self.DOSCAR_spin_pol.tdensities[Spin.down].tolist() + + tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] + assert tdos_nonspin == self.DOSCAR_nonspin_pol.tdensities[Spin.up].tolist() + + def test_itdensities(self): + itdos_up = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09650] + itdos_down = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09685] + assert itdos_up == self.DOSCAR_spin_pol.itdensities[Spin.up].tolist() + assert itdos_down == self.DOSCAR_spin_pol.itdensities[Spin.down].tolist() + + itdos_nonspin = [4.00000, 10.00000, 10.00000, 16.00000, 16.00000, 16.09067] + assert itdos_nonspin == self.DOSCAR_nonspin_pol.itdensities[Spin.up].tolist() + + def test_is_spin_polarized(self): + # first for spin polarized version + assert self.DOSCAR_spin_pol.is_spin_polarized + + assert not self.DOSCAR_nonspin_pol.is_spin_polarized + + +class ChargeTest(PymatgenTest): + def setUp(self): + self.charge2 = Charge(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/CHARGE.lobster.MnO") + # gzipped file + self.charge = Charge(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/CHARGE.lobster.MnO2.gz") + + def testattributes(self): + charge_Loewdin = [-1.25, 1.25] + charge_Mulliken = [-1.30, 1.30] + atomlist = ["O1", "Mn2"] + types = ["O", "Mn"] + num_atoms = 2 + assert_array_equal(charge_Mulliken, self.charge2.Mulliken) + assert_array_equal(charge_Loewdin, self.charge2.Loewdin) + assert_array_equal(atomlist, self.charge2.atomlist) + assert_array_equal(types, self.charge2.types) + assert_array_equal(num_atoms, self.charge2.num_atoms) + + def test_get_structure_with_charges(self): + structure_dict2 = { + "lattice": { + "c": 3.198244, + "volume": 23.132361565928807, + "b": 3.1982447183003364, + "gamma": 60.00000011873414, + "beta": 60.00000401737447, + "alpha": 60.00000742944491, + "matrix": [ + [2.769761, 0.0, 1.599122], + [0.923254, 2.611356, 1.599122], + [0.0, 0.0, 3.198244], + ], + "a": 3.1982443884113985, + }, + "@class": "Structure", + "sites": [ + { + "xyz": [1.846502883732, 1.305680611356, 3.198248797366], + "properties": {"Loewdin Charges": -1.25, "Mulliken Charges": -1.3}, + "abc": [0.499998, 0.500001, 0.500002], + "species": [{"occu": 1, "element": "O"}], + "label": "O", + }, + { + "xyz": [0.0, 0.0, 0.0], + "properties": {"Loewdin Charges": 1.25, "Mulliken Charges": 1.3}, + "abc": [0.0, 0.0, 0.0], + "species": [{"occu": 1, "element": "Mn"}], + "label": "Mn", + }, + ], + "charge": None, + "@module": "pymatgen.core.structure", + } + s2 = Structure.from_dict(structure_dict2) + assert s2 == self.charge2.get_structure_with_charges(os.path.join(this_dir, "../../tests/POSCAR.MnO")) + + +class LobsteroutTest(PymatgenTest): + def setUp(self): + warnings.simplefilter("ignore") + self.lobsterout_normal = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.normal") + # make sure .gz files are also read correctly + self.lobsterout_normal = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.normal2.gz") + self.lobsterout_fatband_grosspop_densityofenergies = Lobsterout( + filename=os.path.join( + PymatgenTest.TEST_FILES_DIR, + "cohp", + "lobsterout.fatband_grosspop_densityofenergy", + ) + ) + self.lobsterout_saveprojection = Lobsterout( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.saveprojection" + ) + self.lobsterout_skipping_all = Lobsterout( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.skipping_all" + ) + self.lobsterout_twospins = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.twospins") + self.lobsterout_GaAs = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.GaAs") + self.lobsterout_from_projection = Lobsterout( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout_from_projection" + ) + self.lobsterout_onethread = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.onethread") + self.lobsterout_cobi_madelung = Lobsterout( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout_cobi_madelung" + ) + self.lobsterout_doscar_lso = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout_doscar_lso") + + # TODO: implement skipping madelung/cobi + self.lobsterout_skipping_cobi_madelung = Lobsterout( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.skip_cobi_madelung" + ) + + def tearDown(self): + warnings.simplefilter("default") + + def testattributes(self): + assert self.lobsterout_normal.basis_functions == [ + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ] + assert self.lobsterout_normal.basis_type == ["pbeVaspFit2015"] + assert self.lobsterout_normal.charge_spilling == [0.0268] + assert self.lobsterout_normal.dft_program == "VASP" + assert self.lobsterout_normal.elements == ["Ti"] + assert self.lobsterout_normal.has_charge + assert self.lobsterout_normal.has_cohpcar + assert self.lobsterout_normal.has_coopcar + assert self.lobsterout_normal.has_doscar + assert not self.lobsterout_normal.has_projection + assert self.lobsterout_normal.has_bandoverlaps + assert not self.lobsterout_normal.has_density_of_energies + assert not self.lobsterout_normal.has_fatbands + assert not self.lobsterout_normal.has_grosspopulation + assert self.lobsterout_normal.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_normal.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_normal.is_restart_from_projection + assert self.lobsterout_normal.lobster_version == "v3.1.0" + assert self.lobsterout_normal.number_of_spins == 1 + assert self.lobsterout_normal.number_of_threads == 8 + assert self.lobsterout_normal.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "702"}, + "user_time": {"h": "0", "min": "0", "s": "20", "ms": "330"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, + } + assert self.lobsterout_normal.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_normal.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_fatband_grosspop_densityofenergies.basis_functions == [ + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ] + assert self.lobsterout_fatband_grosspop_densityofenergies.basis_type == ["pbeVaspFit2015"] + assert self.lobsterout_fatband_grosspop_densityofenergies.charge_spilling == [0.0268] + assert self.lobsterout_fatband_grosspop_densityofenergies.dft_program == "VASP" + assert self.lobsterout_fatband_grosspop_densityofenergies.elements == ["Ti"] + assert self.lobsterout_fatband_grosspop_densityofenergies.has_charge + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_cohpcar + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_coopcar + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_doscar + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_projection + assert self.lobsterout_fatband_grosspop_densityofenergies.has_bandoverlaps + assert self.lobsterout_fatband_grosspop_densityofenergies.has_density_of_energies + assert self.lobsterout_fatband_grosspop_densityofenergies.has_fatbands + assert self.lobsterout_fatband_grosspop_densityofenergies.has_grosspopulation + assert self.lobsterout_fatband_grosspop_densityofenergies.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_fatband_grosspop_densityofenergies.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_fatband_grosspop_densityofenergies.is_restart_from_projection + assert self.lobsterout_fatband_grosspop_densityofenergies.lobster_version == "v3.1.0" + assert self.lobsterout_fatband_grosspop_densityofenergies.number_of_spins == 1 + assert self.lobsterout_fatband_grosspop_densityofenergies.number_of_threads == 8 + assert self.lobsterout_fatband_grosspop_densityofenergies.timing == { + "wall_time": {"h": "0", "min": "0", "s": "4", "ms": "136"}, + "user_time": {"h": "0", "min": "0", "s": "18", "ms": "280"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "290"}, + } + assert self.lobsterout_fatband_grosspop_densityofenergies.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_fatband_grosspop_densityofenergies.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_saveprojection.basis_functions == [ + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ] + assert self.lobsterout_saveprojection.basis_type == ["pbeVaspFit2015"] + assert self.lobsterout_saveprojection.charge_spilling == [0.0268] + assert self.lobsterout_saveprojection.dft_program == "VASP" + assert self.lobsterout_saveprojection.elements == ["Ti"] + assert self.lobsterout_saveprojection.has_charge + assert not self.lobsterout_saveprojection.has_cohpcar + assert not self.lobsterout_saveprojection.has_coopcar + assert not self.lobsterout_saveprojection.has_doscar + assert self.lobsterout_saveprojection.has_projection + assert self.lobsterout_saveprojection.has_bandoverlaps + assert self.lobsterout_saveprojection.has_density_of_energies + assert not self.lobsterout_saveprojection.has_fatbands + assert not self.lobsterout_saveprojection.has_grosspopulation + assert self.lobsterout_saveprojection.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_saveprojection.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_saveprojection.is_restart_from_projection + assert self.lobsterout_saveprojection.lobster_version == "v3.1.0" + assert self.lobsterout_saveprojection.number_of_spins == 1 + assert self.lobsterout_saveprojection.number_of_threads == 8 + assert self.lobsterout_saveprojection.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "574"}, + "user_time": {"h": "0", "min": "0", "s": "18", "ms": "250"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "320"}, + } + assert self.lobsterout_saveprojection.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_saveprojection.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_skipping_all.basis_functions == [ + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ] + assert self.lobsterout_skipping_all.basis_type == ["pbeVaspFit2015"] + assert self.lobsterout_skipping_all.charge_spilling == [0.0268] + assert self.lobsterout_skipping_all.dft_program == "VASP" + assert self.lobsterout_skipping_all.elements == ["Ti"] + assert not self.lobsterout_skipping_all.has_charge + assert not self.lobsterout_skipping_all.has_cohpcar + assert not self.lobsterout_skipping_all.has_coopcar + assert not self.lobsterout_skipping_all.has_doscar + assert not self.lobsterout_skipping_all.has_projection + assert self.lobsterout_skipping_all.has_bandoverlaps + assert not self.lobsterout_skipping_all.has_density_of_energies + assert not self.lobsterout_skipping_all.has_fatbands + assert not self.lobsterout_skipping_all.has_grosspopulation + assert not self.lobsterout_skipping_all.has_cobicar + assert not self.lobsterout_skipping_all.has_madelung + assert self.lobsterout_skipping_all.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_skipping_all.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_skipping_all.is_restart_from_projection + assert self.lobsterout_skipping_all.lobster_version == "v3.1.0" + assert self.lobsterout_skipping_all.number_of_spins == 1 + assert self.lobsterout_skipping_all.number_of_threads == 8 + assert self.lobsterout_skipping_all.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "117"}, + "user_time": {"h": "0", "min": "0", "s": "16", "ms": "79"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "320"}, + } + assert self.lobsterout_skipping_all.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_skipping_all.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_twospins.basis_functions == [ + [ + "4s", + "4p_y", + "4p_z", + "4p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ] + assert self.lobsterout_twospins.basis_type == ["pbeVaspFit2015"] + assert self.lobsterout_twospins.charge_spilling[0] == approx(0.36619999999999997) + assert self.lobsterout_twospins.charge_spilling[1] == approx(0.36619999999999997) + assert self.lobsterout_twospins.dft_program == "VASP" + assert self.lobsterout_twospins.elements == ["Ti"] + assert self.lobsterout_twospins.has_charge + assert self.lobsterout_twospins.has_cohpcar + assert self.lobsterout_twospins.has_coopcar + assert self.lobsterout_twospins.has_doscar + assert not self.lobsterout_twospins.has_projection + assert self.lobsterout_twospins.has_bandoverlaps + assert not self.lobsterout_twospins.has_density_of_energies + assert not self.lobsterout_twospins.has_fatbands + assert not self.lobsterout_twospins.has_grosspopulation + assert self.lobsterout_twospins.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 19 and upwards will be ignored.", + ] + assert self.lobsterout_twospins.info_orthonormalization == [ + "60 of 294 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_twospins.is_restart_from_projection + assert self.lobsterout_twospins.lobster_version == "v3.1.0" + assert self.lobsterout_twospins.number_of_spins == 2 + assert self.lobsterout_twospins.number_of_threads == 8 + assert self.lobsterout_twospins.timing == { + "wall_time": {"h": "0", "min": "0", "s": "3", "ms": "71"}, + "user_time": {"h": "0", "min": "0", "s": "22", "ms": "660"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, + } + assert self.lobsterout_twospins.total_spilling[0] == approx([0.2567][0]) + assert self.lobsterout_twospins.total_spilling[1] == approx([0.2567][0]) + assert self.lobsterout_twospins.warning_lines == [ + "60 of 294 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_from_projection.basis_functions == [] + assert self.lobsterout_from_projection.basis_type == [] + assert self.lobsterout_from_projection.charge_spilling[0] == approx(0.0177) + assert self.lobsterout_from_projection.dft_program is None + assert self.lobsterout_from_projection.elements == [] + assert self.lobsterout_from_projection.has_charge + assert self.lobsterout_from_projection.has_cohpcar + assert self.lobsterout_from_projection.has_coopcar + assert self.lobsterout_from_projection.has_doscar + assert not self.lobsterout_from_projection.has_projection + assert not self.lobsterout_from_projection.has_bandoverlaps + assert not self.lobsterout_from_projection.has_density_of_energies + assert not self.lobsterout_from_projection.has_fatbands + assert not self.lobsterout_from_projection.has_grosspopulation + assert self.lobsterout_from_projection.info_lines == [] + assert self.lobsterout_from_projection.info_orthonormalization == [] + assert self.lobsterout_from_projection.is_restart_from_projection + assert self.lobsterout_from_projection.lobster_version == "v3.1.0" + assert self.lobsterout_from_projection.number_of_spins == 1 + assert self.lobsterout_from_projection.number_of_threads == 8 + assert self.lobsterout_from_projection.timing == { + "wall_time": {"h": "0", "min": "2", "s": "1", "ms": "890"}, + "user_time": {"h": "0", "min": "15", "s": "10", "ms": "530"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "400"}, + } + assert self.lobsterout_from_projection.total_spilling[0] == approx([0.1543][0]) + assert self.lobsterout_from_projection.warning_lines == [] + + assert self.lobsterout_GaAs.basis_functions == [ + ["4s", "4p_y", "4p_z", "4p_x"], + [ + "4s", + "4p_y", + "4p_z", + "4p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ], + ] + assert self.lobsterout_GaAs.basis_type == ["Bunge", "Bunge"] + assert self.lobsterout_GaAs.charge_spilling[0] == approx(0.0089) + assert self.lobsterout_GaAs.dft_program == "VASP" + assert self.lobsterout_GaAs.elements == ["As", "Ga"] + assert self.lobsterout_GaAs.has_charge + assert self.lobsterout_GaAs.has_cohpcar + assert self.lobsterout_GaAs.has_coopcar + assert self.lobsterout_GaAs.has_doscar + assert not self.lobsterout_GaAs.has_projection + assert not self.lobsterout_GaAs.has_bandoverlaps + assert not self.lobsterout_GaAs.has_density_of_energies + assert not self.lobsterout_GaAs.has_fatbands + assert not self.lobsterout_GaAs.has_grosspopulation + assert self.lobsterout_GaAs.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 14 and upwards will be ignored.", + ] + assert self.lobsterout_GaAs.info_orthonormalization == [] + assert not self.lobsterout_GaAs.is_restart_from_projection + assert self.lobsterout_GaAs.lobster_version == "v3.1.0" + assert self.lobsterout_GaAs.number_of_spins == 1 + assert self.lobsterout_GaAs.number_of_threads == 8 + assert self.lobsterout_GaAs.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "726"}, + "user_time": {"h": "0", "min": "0", "s": "12", "ms": "370"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "180"}, + } + assert self.lobsterout_GaAs.total_spilling[0] == approx([0.0859][0]) + + assert self.lobsterout_onethread.number_of_threads == 1 + # Test lobsterout of lobster-4.1.0 + assert self.lobsterout_cobi_madelung.has_cobicar + assert self.lobsterout_cobi_madelung.has_cohpcar + assert self.lobsterout_cobi_madelung.has_madelung + assert not self.lobsterout_cobi_madelung.has_doscar_lso + + assert self.lobsterout_doscar_lso.has_doscar_lso + + assert self.lobsterout_skipping_cobi_madelung.has_cobicar is False + assert self.lobsterout_skipping_cobi_madelung.has_madelung is False + + def test_get_doc(self): + comparedict = { + "restart_from_projection": False, + "lobster_version": "v3.1.0", + "threads": 8, + "dft_program": "VASP", + "charge_spilling": [0.0268], + "total_spilling": [0.044000000000000004], + "elements": ["Ti"], + "basis_type": ["pbeVaspFit2015"], + "basis_functions": [ + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ], + "timing": { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "702"}, + "user_time": {"h": "0", "min": "0", "s": "20", "ms": "330"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, + }, + "warning_lines": [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ], + "info_orthonormalization": ["3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5."], + "info_lines": [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ], + "has_doscar": True, + "has_doscar_lso": False, + "has_cohpcar": True, + "has_coopcar": True, + "has_charge": True, + "has_projection": False, + "has_bandoverlaps": True, + "has_fatbands": False, + "has_grosspopulation": False, + "has_density_of_energies": False, + } + for key, item in self.lobsterout_normal.get_doc().items(): + if key not in ["has_cobicar", "has_madelung"]: + if isinstance(item, str): + assert comparedict[key], item + elif isinstance(item, int): + assert comparedict[key] == item + elif key in ("charge_spilling", "total_spilling"): + assert item[0] == approx(comparedict[key][0]) + elif isinstance(item, (list, dict)): + assert item == comparedict[key] + + +class FatbandTest(PymatgenTest): + def setUp(self): + warnings.simplefilter("ignore") + self.fatband_SiO2_p_x = Fatband( + filenames=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x", + Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + ) + self.vasprun_SiO2_p_x = Vasprun( + filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml" + ) + self.bs_symmline = self.vasprun_SiO2_p_x.get_band_structure(line_mode=True, force_hybrid_mode=True) + self.fatband_SiO2_p = Fatband( + filenames=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p", + Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/KPOINTS", + vasprun=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/vasprun.xml", + ) + self.vasprun_SiO2_p = Vasprun(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/vasprun.xml") + self.bs_symmline2 = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) + self.fatband_SiO2_spin = Fatband( + filenames=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin", + Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/KPOINTS", + vasprun=os.path.join( + PymatgenTest.TEST_FILES_DIR, + "cohp", + "Fatband_SiO2/Test_Spin/vasprun.xml", + ), + ) + self.vasprun_SiO2_spin = Vasprun( + filename=os.path.join( + PymatgenTest.TEST_FILES_DIR, + "cohp", + "Fatband_SiO2/Test_Spin/vasprun.xml", + ) + ) + self.bs_symmline_spin = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) + + def tearDown(self): + warnings.simplefilter("default") + + def test_attributes(self): + assert list(self.fatband_SiO2_p_x.label_dict["M"]) == approx([0.5, 0.0, 0.0]) + assert self.fatband_SiO2_p_x.efermi == self.vasprun_SiO2_p_x.efermi + lattice1 = self.bs_symmline.lattice_rec.as_dict() + lattice2 = self.fatband_SiO2_p_x.lattice.as_dict() + for idx in range(3): + assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) + assert self.fatband_SiO2_p_x.eigenvals[Spin.up][1][1] - self.fatband_SiO2_p_x.efermi == -18.245 + assert self.fatband_SiO2_p_x.is_spinpolarized is False + assert self.fatband_SiO2_p_x.kpoints_array[3] == approx([0.03409091, 0, 0]) + assert self.fatband_SiO2_p_x.nbands == 36 + assert self.fatband_SiO2_p_x.p_eigenvals[Spin.up][2][1]["Si1"]["3p_x"] == 0.002 + assert self.fatband_SiO2_p_x.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) + assert self.fatband_SiO2_p_x.structure[0].species_string == "Si" + assert self.fatband_SiO2_p_x.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) + + assert list(self.fatband_SiO2_p.label_dict["M"]) == approx([0.5, 0.0, 0.0]) + assert self.fatband_SiO2_p.efermi == self.vasprun_SiO2_p.efermi + lattice1 = self.bs_symmline2.lattice_rec.as_dict() + lattice2 = self.fatband_SiO2_p.lattice.as_dict() + for idx in range(3): + assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) + assert self.fatband_SiO2_p.eigenvals[Spin.up][1][1] - self.fatband_SiO2_p.efermi == -18.245 + assert self.fatband_SiO2_p.is_spinpolarized is False + assert self.fatband_SiO2_p.kpoints_array[3] == approx([0.03409091, 0, 0]) + assert self.fatband_SiO2_p.nbands == 36 + assert self.fatband_SiO2_p.p_eigenvals[Spin.up][2][1]["Si1"]["3p"] == 0.042 + assert self.fatband_SiO2_p.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) + assert self.fatband_SiO2_p.structure[0].species_string == "Si" + assert self.fatband_SiO2_p.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) + + assert list(self.fatband_SiO2_spin.label_dict["M"]) == approx([0.5, 0.0, 0.0]) + assert self.fatband_SiO2_spin.efermi == self.vasprun_SiO2_spin.efermi + lattice1 = self.bs_symmline_spin.lattice_rec.as_dict() + lattice2 = self.fatband_SiO2_spin.lattice.as_dict() + for idx in range(3): + assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) + assert self.fatband_SiO2_spin.eigenvals[Spin.up][1][1] - self.fatband_SiO2_spin.efermi == -18.245 + assert self.fatband_SiO2_spin.eigenvals[Spin.down][1][1] - self.fatband_SiO2_spin.efermi == -18.245 + assert self.fatband_SiO2_spin.is_spinpolarized + assert self.fatband_SiO2_spin.kpoints_array[3] == approx([0.03409091, 0, 0]) + assert self.fatband_SiO2_spin.nbands == 36 + + assert self.fatband_SiO2_spin.p_eigenvals[Spin.up][2][1]["Si1"]["3p"] == 0.042 + assert self.fatband_SiO2_spin.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) + assert self.fatband_SiO2_spin.structure[0].species_string == "Si" + assert self.fatband_SiO2_spin.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) + + def test_raises(self): + with pytest.raises( + ValueError, match="The are two FATBAND files for the same atom and orbital. The program will stop" + ): + self.fatband_SiO2_p_x = Fatband( + filenames=[ + f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + ], + Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + ) + + with pytest.raises( + ValueError, + match=r"Make sure all relevant orbitals were generated and that no duplicates \(2p and 2p_x\) are present", + ): + self.fatband_SiO2_p_x = Fatband( + filenames=[ + f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/FATBAND_si1_3p.lobster", + ], + Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + ) + + with pytest.raises(ValueError, match="No FATBAND files in folder or given"): + self.fatband_SiO2_p_x = Fatband( + filenames=".", + Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + ) + + def test_get_bandstructure(self): + bs_p = self.fatband_SiO2_p.get_bandstructure() + atom1 = bs_p.structure[0] + atom2 = self.bs_symmline2.structure[0] + assert atom1.frac_coords[0] == approx(atom2.frac_coords[0]) + assert atom1.frac_coords[1] == approx(atom2.frac_coords[1]) + assert atom1.frac_coords[2] == approx(atom2.frac_coords[2]) + assert atom1.coords[0] == approx(atom2.coords[0]) + assert atom1.coords[1] == approx(atom2.coords[1]) + assert atom1.coords[2] == approx(atom2.coords[2]) + assert atom1.species_string == atom2.species_string + assert bs_p.efermi == self.bs_symmline2.efermi + branch1 = bs_p.branches[0] + branch2 = self.bs_symmline2.branches[0] + assert branch2["name"] == branch1["name"] + assert branch2["start_index"] == branch1["start_index"] + assert branch2["end_index"] == branch1["end_index"] + + assert bs_p.distance[30] == approx(self.bs_symmline2.distance[30]) + lattice1 = bs_p.lattice_rec.as_dict() + lattice2 = self.bs_symmline2.lattice_rec.as_dict() + assert lattice1["matrix"][0] == approx(lattice2["matrix"][0]) + assert lattice1["matrix"][1] == approx(lattice2["matrix"][1]) + assert lattice1["matrix"][2] == approx(lattice2["matrix"][2]) + + assert bs_p.kpoints[8].frac_coords[0] == approx(self.bs_symmline2.kpoints[8].frac_coords[0]) + assert bs_p.kpoints[8].frac_coords[1] == approx(self.bs_symmline2.kpoints[8].frac_coords[1]) + assert bs_p.kpoints[8].frac_coords[2] == approx(self.bs_symmline2.kpoints[8].frac_coords[2]) + assert bs_p.kpoints[8].cart_coords[0] == approx(self.bs_symmline2.kpoints[8].cart_coords[0]) + assert bs_p.kpoints[8].cart_coords[1] == approx(self.bs_symmline2.kpoints[8].cart_coords[1]) + assert bs_p.kpoints[8].cart_coords[2] == approx(self.bs_symmline2.kpoints[8].cart_coords[2]) + assert bs_p.kpoints[50].frac_coords[0] == approx(self.bs_symmline2.kpoints[50].frac_coords[0]) + assert bs_p.kpoints[50].frac_coords[1] == approx(self.bs_symmline2.kpoints[50].frac_coords[1]) + assert bs_p.kpoints[50].frac_coords[2] == approx(self.bs_symmline2.kpoints[50].frac_coords[2]) + assert bs_p.kpoints[50].cart_coords[0] == approx(self.bs_symmline2.kpoints[50].cart_coords[0]) + assert bs_p.kpoints[50].cart_coords[1] == approx(self.bs_symmline2.kpoints[50].cart_coords[1]) + assert bs_p.kpoints[50].cart_coords[2] == approx(self.bs_symmline2.kpoints[50].cart_coords[2]) + assert bs_p.get_band_gap()["energy"] == approx(self.bs_symmline2.get_band_gap()["energy"], abs=1e-2) + assert bs_p.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064)) + assert bs_p.get_projections_on_elements_and_orbitals({"Si": ["3p"]})[Spin.up][0][0]["Si"]["3p"] == approx(0.003) + assert bs_p.get_projections_on_elements_and_orbitals({"O": ["2p"]})[Spin.up][0][0]["O"]["2p"] == approx( + 0.002 * 3 + 0.003 * 3 + ) + dict_here = bs_p.get_projections_on_elements_and_orbitals({"Si": ["3s", "3p"], "O": ["2s", "2p"]})[Spin.up][0][ + 0 + ] + assert dict_here["Si"]["3s"] == approx(0.192) + assert dict_here["Si"]["3p"] == approx(0.003) + assert dict_here["O"]["2s"] == approx(0.792) + assert dict_here["O"]["2p"] == approx(0.015) + + bs_spin = self.fatband_SiO2_spin.get_bandstructure() + assert bs_spin.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064)) + assert bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3p"]})[Spin.up][0][0]["Si"]["3p"] == approx( + 0.003 + ) + assert bs_spin.get_projections_on_elements_and_orbitals({"O": ["2p"]})[Spin.up][0][0]["O"]["2p"] == approx( + 0.002 * 3 + 0.003 * 3 + ) + dict_here = bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3s", "3p"], "O": ["2s", "2p"]})[Spin.up][ + 0 + ][0] + assert dict_here["Si"]["3s"] == approx(0.192) + assert dict_here["Si"]["3p"] == approx(0.003) + assert dict_here["O"]["2s"] == approx(0.792) + assert dict_here["O"]["2p"] == approx(0.015) + + assert bs_spin.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064)) + assert bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3p"]})[Spin.down][0][0]["Si"]["3p"] == approx( + 0.003 + ) + assert bs_spin.get_projections_on_elements_and_orbitals({"O": ["2p"]})[Spin.down][0][0]["O"]["2p"] == approx( + 0.002 * 3 + 0.003 * 3 + ) + dict_here = bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3s", "3p"], "O": ["2s", "2p"]})[ + Spin.down + ][0][0] + assert dict_here["Si"]["3s"] == approx(0.192) + assert dict_here["Si"]["3p"] == approx(0.003) + assert dict_here["O"]["2s"] == approx(0.792) + assert dict_here["O"]["2p"] == approx(0.015) + bs_p_x = self.fatband_SiO2_p_x.get_bandstructure() + assert bs_p_x.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064), abs=1e-2) + + +class LobsterinTest(unittest.TestCase): + def setUp(self): + warnings.simplefilter("ignore") + self.Lobsterinfromfile = Lobsterin.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "lobsterin.1")) + self.Lobsterinfromfile2 = Lobsterin.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "lobsterin.2")) + self.Lobsterinfromfile3 = Lobsterin.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "lobsterin.3")) + self.Lobsterinfromfile4 = Lobsterin.from_file( + os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "lobsterin.4.gz") + ) + + def test_from_file(self): + # test read from file + assert self.Lobsterinfromfile["cohpstartenergy"] == approx(-15.0) + assert self.Lobsterinfromfile["cohpendenergy"] == approx(5.0) + assert self.Lobsterinfromfile["basisset"] == "pbeVaspFit2015" + assert self.Lobsterinfromfile["gaussiansmearingwidth"] == approx(0.1) + assert self.Lobsterinfromfile["basisfunctions"][0] == "Fe 3d 4p 4s" + assert self.Lobsterinfromfile["basisfunctions"][1] == "Co 3d 4p 4s" + assert self.Lobsterinfromfile["skipdos"] + assert self.Lobsterinfromfile["skipcohp"] + assert self.Lobsterinfromfile["skipcoop"] + assert self.Lobsterinfromfile["skippopulationanalysis"] + assert self.Lobsterinfromfile["skipgrosspopulation"] + + # test if comments are correctly removed + assert self.Lobsterinfromfile == self.Lobsterinfromfile2 + + def test_getitem(self): + # tests implementation of getitem, should be case independent + assert self.Lobsterinfromfile["COHPSTARTENERGY"] == approx(-15.0) + + def test_setitem(self): + # test implementation of setitem + self.Lobsterinfromfile["skipCOHP"] = False + assert self.Lobsterinfromfile["skipcohp"] is False + + def test_initialize_from_dict(self): + # initialize from dict + lobsterin = Lobsterin( + { + "cohpstartenergy": -15.0, + "cohpendenergy": 5.0, + "basisset": "pbeVaspFit2015", + "gaussiansmearingwidth": 0.1, + "basisfunctions": ["Fe 3d 4p 4s", "Co 3d 4p 4s"], + "skipdos": True, + "skipcohp": True, + "skipcoop": True, + "skippopulationanalysis": True, + "skipgrosspopulation": True, + } + ) + assert lobsterin["cohpstartenergy"] == approx(-15.0) + assert lobsterin["cohpendenergy"] == approx(5.0) + assert lobsterin["basisset"] == "pbeVaspFit2015" + assert lobsterin["gaussiansmearingwidth"] == approx(0.1) + assert lobsterin["basisfunctions"][0] == "Fe 3d 4p 4s" + assert lobsterin["basisfunctions"][1] == "Co 3d 4p 4s" + assert {*lobsterin} >= {"skipdos", "skipcohp", "skipcoop", "skippopulationanalysis", "skipgrosspopulation"} + with pytest.raises(IOError, match="There are duplicates for the keywords! The program will stop here."): + lobsterin2 = Lobsterin({"cohpstartenergy": -15.0, "cohpstartEnergy": -20.0}) + lobsterin2 = Lobsterin({"cohpstartenergy": -15.0}) + # can only calculate nbands if basis functions are provided + with pytest.raises(IOError, match="No basis functions are provided. The program cannot calculate nbands"): + lobsterin2._get_nbands(structure=Structure.from_file(os.path.join(test_dir_doscar, "POSCAR.Fe3O4"))) + + def test_standard_settings(self): + # test standard settings + for option in [ + "standard", + "standard_from_projection", + "standard_with_fatband", + "onlyprojection", + "onlydos", + "onlycohp", + "onlycoop", + "onlycobi", + "onlycohpcoop", + "onlycohpcoopcobi", + ]: + lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( + os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), + os.path.join(test_dir_doscar, "INCAR.lobster"), + os.path.join(test_dir_doscar, "POTCAR.Fe3O4"), + option=option, + ) + assert lobsterin1["cohpstartenergy"] == approx(-35.0) + assert lobsterin1["cohpendenergy"] == approx(5.0) + assert lobsterin1["basisset"] == "pbeVaspFit2015" + assert lobsterin1["gaussiansmearingwidth"] == approx(0.1) + assert lobsterin1["basisfunctions"][0] == "Fe 3d 4p 4s " + assert lobsterin1["basisfunctions"][1] == "O 2p 2s " + + if option in [ + "standard", + "standard_with_fatband", + "onlyprojection", + "onlycohp", + "onlycoop", + "onlycohpcoop", + ]: + assert lobsterin1["saveProjectiontoFile"] + if option in [ + "standard", + "standard_with_fatband", + "onlycohp", + "onlycoop", + "onlycohpcoop", + ]: + assert lobsterin1["cohpGenerator"] == "from 0.1 to 6.0 orbitalwise" + if option in ["standard"]: + assert "skipdos" not in lobsterin1 + assert "skipcohp" not in lobsterin1 + assert "skipcoop" not in lobsterin1 + if option in ["standard_with_fatband"]: + assert lobsterin1["createFatband"] == ["Fe 3d 4p 4s ", "O 2p 2s "] + assert "skipdos" not in lobsterin1 + assert "skipcohp" not in lobsterin1 + assert "skipcoop" not in lobsterin1 + if option in ["standard_from_projection"]: + assert lobsterin1["loadProjectionFromFile"], True + if option in [ + "onlyprojection", + "onlycohp", + "onlycoop", + "onlycobi", + "onlycohpcoop", + "onlycohpcoopcobi", + ]: + assert lobsterin1["skipdos"], True + assert lobsterin1["skipPopulationAnalysis"], True + assert lobsterin1["skipGrossPopulation"], True + assert lobsterin1["skipMadelungEnergy"], True + + if option in ["onlydos"]: + assert lobsterin1["skipPopulationAnalysis"], True + assert lobsterin1["skipGrossPopulation"], True + assert lobsterin1["skipcohp"], True + assert lobsterin1["skipcoop"], True + assert lobsterin1["skipcobi"], True + assert lobsterin1["skipMadelungEnergy"], True + if option in ["onlycohp"]: + assert lobsterin1["skipcoop"], True + assert lobsterin1["skipcobi"], True + if option in ["onlycoop"]: + assert lobsterin1["skipcohp"], True + assert lobsterin1["skipcobi"], True + if option in ["onlyprojection"]: + assert lobsterin1["skipdos"], True + if option in ["onlymadelung"]: + assert lobsterin1["skipPopulationAnalysis"], True + assert lobsterin1["skipGrossPopulation"], True + assert lobsterin1["skipcohp"], True + assert lobsterin1["skipcoop"], True + assert lobsterin1["skipcobi"], True + assert lobsterin1["skipdos"], True + # test basis functions by dict + lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( + os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), + os.path.join(test_dir_doscar, "INCAR.lobster"), + dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, + option="standard", + ) + assert lobsterin_new["basisfunctions"] == ["Fe 3d 4p 4s", "O 2s 2p"] + + # test gaussian smearing + lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( + os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), + os.path.join(test_dir_doscar, "INCAR.lobster2"), + dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, + option="standard", + ) + assert "gaussiansmearingwidth" not in lobsterin_new + + # fatband and ISMEAR=-5 does not work together + with pytest.raises(ValueError, match="ISMEAR has to be 0 for a fatband calculation with Lobster"): + lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( + os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), + os.path.join(test_dir_doscar, "INCAR.lobster2"), + dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, + option="standard_with_fatband", + ) + + def test_standard_with_energy_range_from_vasprun(self): + # test standard_with_energy_range_from_vasprun + lobsterin_comp = Lobsterin.standard_calculations_from_vasp_files( + os.path.join(test_dir_doscar, "POSCAR.C2.gz"), + os.path.join(test_dir_doscar, "INCAR.C2.gz"), + os.path.join(test_dir_doscar, "POTCAR.C2.gz"), + os.path.join(test_dir_doscar, "vasprun.xml.C2.gz"), + option="standard_with_energy_range_from_vasprun", + ) + assert lobsterin_comp["COHPstartEnergy"] == -28.3679 + assert lobsterin_comp["COHPendEnergy"] == 32.8968 + assert lobsterin_comp["COHPSteps"] == 301 + + def test_diff(self): + # test diff + assert self.Lobsterinfromfile.diff(self.Lobsterinfromfile2)["Different"] == {} + assert self.Lobsterinfromfile.diff(self.Lobsterinfromfile2)["Same"]["COHPSTARTENERGY"] == approx(-15.0) + + # test diff in both directions + for entry in self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Same"]: + assert entry in self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Same"] + for entry in self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Same"]: + assert entry in self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Same"] + for entry in self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Different"]: + assert entry in self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Different"] + for entry in self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Different"]: + assert entry in self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Different"] + + assert ( + self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Different"]["SKIPCOHP"]["lobsterin1"] + == self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Different"]["SKIPCOHP"]["lobsterin2"] + ) + + def test_get_basis(self): + # get basis functions + lobsterin1 = Lobsterin({}) + potcar = Potcar.from_file(os.path.join(test_dir_doscar, "POTCAR.Fe3O4")) + Potcar_names = [name["symbol"] for name in potcar.spec] + + assert lobsterin1.get_basis( + Structure.from_file(os.path.join(test_dir_doscar, "Fe3O4.cif")), + potcar_symbols=Potcar_names, + ) == ["Fe 3d 4p 4s ", "O 2p 2s "] + potcar = Potcar.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "POTCAR.GaAs")) + Potcar_names = [name["symbol"] for name in potcar.spec] + assert lobsterin1.get_basis( + Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "POSCAR.GaAs")), + potcar_symbols=Potcar_names, + ) == ["Ga 3d 4p 4s ", "As 4p 4s "] + + def test_get_all_possible_basis_functions(self): + potcar = Potcar.from_file(os.path.join(test_dir_doscar, "POTCAR.Fe3O4")) + Potcar_names = [name["symbol"] for name in potcar.spec] + result = Lobsterin.get_all_possible_basis_functions( + Structure.from_file(os.path.join(test_dir_doscar, "Fe3O4.cif")), + potcar_symbols=Potcar_names, + ) + assert result[0] == {"Fe": "3d 4s", "O": "2p 2s"} + assert result[1] == {"Fe": "3d 4s 4p", "O": "2p 2s"} + + potcar2 = Potcar.from_file(os.path.join(test_dir_doscar, "POT_GGA_PAW_PBE_54/POTCAR.Fe_pv.gz")) + Potcar_names2 = [name["symbol"] for name in potcar2.spec] + result2 = Lobsterin.get_all_possible_basis_functions( + Structure.from_file(os.path.join(test_dir_doscar, "Fe.cif")), + potcar_symbols=Potcar_names2, + ) + assert result2[0] == {"Fe": "3d 3p 4s"} + + def test_get_potcar_symbols(self): + lobsterin1 = Lobsterin({}) + assert lobsterin1._get_potcar_symbols(os.path.join(test_dir_doscar, "POTCAR.Fe3O4")) == ["Fe", "O"] + assert lobsterin1._get_potcar_symbols(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "POTCAR.GaAs")) == [ + "Ga_d", + "As", + ] + + def test_write_lobsterin(self): + # write lobsterin, read it and compare it + outfile_path = tempfile.mkstemp()[1] + lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( + os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), + os.path.join(test_dir_doscar, "INCAR.lobster"), + os.path.join(test_dir_doscar, "POTCAR.Fe3O4"), + option="standard", + ) + lobsterin1.write_lobsterin(outfile_path) + lobsterin2 = Lobsterin.from_file(outfile_path) + assert lobsterin1.diff(lobsterin2)["Different"] == {} + + def test_write_INCAR(self): + # write INCAR and compare + outfile_path = tempfile.mkstemp()[1] + lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( + os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), + os.path.join(test_dir_doscar, "INCAR.lobster"), + os.path.join(test_dir_doscar, "POTCAR.Fe3O4"), + option="standard", + ) + lobsterin1.write_INCAR( + os.path.join(test_dir_doscar, "INCAR.lobster3"), + outfile_path, + os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), + ) + + incar1 = Incar.from_file(os.path.join(test_dir_doscar, "INCAR.lobster3")) + incar2 = Incar.from_file(outfile_path) + + assert incar1.diff(incar2)["Different"] == { + "ISYM": {"INCAR1": 2, "INCAR2": -1}, + "NBANDS": {"INCAR1": None, "INCAR2": 86}, + "NSW": {"INCAR1": 500, "INCAR2": 0}, + "LWAVE": {"INCAR1": False, "INCAR2": True}, + } + + def test_write_KPOINTS(self): + # line mode + outfile_path = tempfile.mkstemp()[1] + outfile_path2 = tempfile.mkstemp(prefix="POSCAR")[1] + lobsterin1 = Lobsterin({}) + # test writing primitive cell + lobsterin1.write_POSCAR_with_standard_primitive( + POSCAR_input=os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), + POSCAR_output=outfile_path2, + ) + + lobsterin1.write_KPOINTS( + POSCAR_input=outfile_path2, + KPOINTS_output=outfile_path, + kpoints_line_density=58, + ) + kpoint = Kpoints.from_file(outfile_path) + assert kpoint.num_kpts == 562 + assert kpoint.kpts[-1][0] == approx(-0.5) + assert kpoint.kpts[-1][1] == approx(0.5) + assert kpoint.kpts[-1][2] == approx(0.5) + assert kpoint.labels[-1] == "T" + kpoint2 = Kpoints.from_file(os.path.join(test_dir_doscar, "KPOINTS_band.lobster")) + + labels = [] + number = 0 + for label in kpoint.labels: + if label is not None: + if number != 0: + if label != labels[number - 1]: + labels.append(label) + number += 1 + else: + labels.append(label) + number += 1 + + labels2 = [] + number2 = 0 + for label in kpoint2.labels: + if label is not None: + if number2 != 0: + if label != labels2[number2 - 1]: + labels2.append(label) + number2 += 1 + else: + labels2.append(label) + number2 += 1 + assert labels == labels2 + + # without line mode + lobsterin1.write_KPOINTS(POSCAR_input=outfile_path2, KPOINTS_output=outfile_path, line_mode=False) + kpoint = Kpoints.from_file(outfile_path) + kpoint2 = Kpoints.from_file(os.path.join(test_dir_doscar, "IBZKPT.lobster")) + + for num_kpt, list_kpoint in enumerate(kpoint.kpts): + assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) + assert list_kpoint[1] == approx(kpoint2.kpts[num_kpt][1]) + assert list_kpoint[2] == approx(kpoint2.kpts[num_kpt][2]) + + assert kpoint.num_kpts == 108 + + # without line mode, use grid instead of reciprocal density + lobsterin1.write_KPOINTS( + POSCAR_input=outfile_path2, + KPOINTS_output=outfile_path, + line_mode=False, + from_grid=True, + input_grid=[6, 6, 3], + ) + kpoint = Kpoints.from_file(outfile_path) + kpoint2 = Kpoints.from_file(os.path.join(test_dir_doscar, "IBZKPT.lobster")) + + for num_kpt, list_kpoint in enumerate(kpoint.kpts): + assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) + assert list_kpoint[1] == approx(kpoint2.kpts[num_kpt][1]) + assert list_kpoint[2] == approx(kpoint2.kpts[num_kpt][2]) + + assert kpoint.num_kpts == 108 + + # + # #without line mode, using a certain grid, isym=0 instead of -1 + lobsterin1.write_KPOINTS( + POSCAR_input=f"{PymatgenTest.TEST_FILES_DIR}/cohp/POSCAR.Li", + KPOINTS_output=outfile_path, + line_mode=False, + from_grid=True, + input_grid=[3, 3, 3], + isym=0, + ) + + kpoint1 = Kpoints.from_file(outfile_path) + kpoint2 = Kpoints.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "IBZKPT_3_3_3_Li")) + for ikpoint, kpoint in enumerate(kpoint1.kpts): + assert self.is_kpoint_in_list( + kpoint, + kpoint2.kpts, + kpoint1.kpts_weights[ikpoint], + kpoint2.kpts_weights, + ) + for ikpoint, kpoint in enumerate(kpoint2.kpts): + assert self.is_kpoint_in_list( + kpoint, + kpoint1.kpts, + kpoint2.kpts_weights[ikpoint], + kpoint1.kpts_weights, + ) + + lobsterin1.write_KPOINTS( + POSCAR_input=f"{PymatgenTest.TEST_FILES_DIR}/cohp/POSCAR.Li", + KPOINTS_output=outfile_path, + line_mode=False, + from_grid=True, + input_grid=[2, 2, 2], + isym=0, + ) + + kpoint1 = Kpoints.from_file(outfile_path) + kpoint2 = Kpoints.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "IBZKPT_2_2_2_Li")) + for ikpoint, kpoint in enumerate(kpoint1.kpts): + assert self.is_kpoint_in_list( + kpoint, + kpoint2.kpts, + kpoint1.kpts_weights[ikpoint], + kpoint2.kpts_weights, + ) + for ikpoint, kpoint in enumerate(kpoint2.kpts): + assert self.is_kpoint_in_list( + kpoint, + kpoint1.kpts, + kpoint2.kpts_weights[ikpoint], + kpoint1.kpts_weights, + ) + + def is_kpoint_in_list(self, kpoint, kpointlist, weight, weightlist) -> bool: + found = 0 + for ikpoint2, kpoint2 in enumerate(kpointlist): + if ( + np.isclose(kpoint[0], kpoint2[0]) + and np.isclose(kpoint[1], kpoint2[1]) + and np.isclose(kpoint[2], kpoint2[2]) + ): + if weight == weightlist[ikpoint2]: + found += 1 + elif ( + np.isclose(-kpoint[0], kpoint2[0]) + and np.isclose(-kpoint[1], kpoint2[1]) + and np.isclose(-kpoint[2], kpoint2[2]) + ) and weight == weightlist[ikpoint2]: + found += 1 + return found == 1 + + def test_MSONable_implementation(self): + # tests as dict and from dict methods + newLobsterin = Lobsterin.from_dict(self.Lobsterinfromfile.as_dict()) + assert newLobsterin == self.Lobsterinfromfile + newLobsterin.to_json() + + def tearDown(self): + warnings.simplefilter("default") + + +class BandoverlapsTest(unittest.TestCase): + def setUp(self): + warnings.simplefilter("ignore") + # test spin polarlized calc and non spinpolarized calc + + self.bandoverlaps1 = Bandoverlaps(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "bandOverlaps.lobster.1")) + self.bandoverlaps2 = Bandoverlaps(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "bandOverlaps.lobster.2")) + + self.bandoverlaps1_new = Bandoverlaps( + os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "bandOverlaps.lobster.new.1") + ) + self.bandoverlaps2_new = Bandoverlaps( + os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "bandOverlaps.lobster.new.2") + ) + + def test_attributes(self): + # bandoverlapsdict + assert self.bandoverlaps1.bandoverlapsdict[Spin.up]["0.5 0 0"]["maxDeviation"] == approx(0.000278953) + assert self.bandoverlaps1_new.bandoverlapsdict[Spin.up]["0 0 0"]["maxDeviation"] == approx(0.0640933) + assert self.bandoverlaps1.bandoverlapsdict[Spin.up]["0.5 0 0"]["matrix"][-1][-1] == approx(0.0188058) + assert self.bandoverlaps1_new.bandoverlapsdict[Spin.up]["0 0 0"]["matrix"][-1][-1] == approx(1.0) + assert self.bandoverlaps1.bandoverlapsdict[Spin.up]["0.5 0 0"]["matrix"][0][0] == approx(1) + assert self.bandoverlaps1_new.bandoverlapsdict[Spin.up]["0 0 0"]["matrix"][0][0] == approx(0.995849) + + assert self.bandoverlaps1.bandoverlapsdict[Spin.down]["0.0261194 0.0261194 0.473881"]["maxDeviation"] == approx( + 4.31567e-05 + ) + assert self.bandoverlaps1_new.bandoverlapsdict[Spin.down]["0 0 0"]["maxDeviation"] == approx(0.064369) + assert self.bandoverlaps1.bandoverlapsdict[Spin.down]["0.0261194 0.0261194 0.473881"]["matrix"][0][ + -1 + ] == approx(4.0066e-07) + assert self.bandoverlaps1_new.bandoverlapsdict[Spin.down]["0 0 0"]["matrix"][0][-1] == approx(1.37447e-09) + + # maxDeviation + assert self.bandoverlaps1.max_deviation[0] == approx(0.000278953) + assert self.bandoverlaps1_new.max_deviation[0] == approx(0.39824) + assert self.bandoverlaps1.max_deviation[-1] == approx(4.31567e-05) + assert self.bandoverlaps1_new.max_deviation[-1] == approx(0.324898) + + assert self.bandoverlaps2.max_deviation[0] == approx(0.000473319) + assert self.bandoverlaps2_new.max_deviation[0] == approx(0.403249) + assert self.bandoverlaps2.max_deviation[-1] == approx(1.48451e-05) + assert self.bandoverlaps2_new.max_deviation[-1] == approx(0.45154) + + def test_has_good_quality(self): + assert not self.bandoverlaps1.has_good_quality_maxDeviation(limit_maxDeviation=0.1) + assert not self.bandoverlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=0.1) + assert not self.bandoverlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=9, + number_occ_bands_spin_down=5, + limit_deviation=0.1, + spin_polarized=True, + ) + assert not self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=9, + number_occ_bands_spin_down=5, + limit_deviation=0.1, + spin_polarized=True, + ) + assert self.bandoverlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=3, + number_occ_bands_spin_down=0, + limit_deviation=0.001, + spin_polarized=True, + ) + assert self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=3, + number_occ_bands_spin_down=0, + limit_deviation=0.01, + spin_polarized=True, + ) + assert not self.bandoverlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=1, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=1, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.bandoverlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=0, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=0, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.bandoverlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=0, + number_occ_bands_spin_down=1, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=0, + number_occ_bands_spin_down=1, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.bandoverlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + number_occ_bands_spin_down=4, + limit_deviation=0.001, + spin_polarized=True, + ) + assert not self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + number_occ_bands_spin_down=4, + limit_deviation=0.001, + spin_polarized=True, + ) + + assert self.bandoverlaps1.has_good_quality_maxDeviation(limit_maxDeviation=100) + assert self.bandoverlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=100) + assert self.bandoverlaps2.has_good_quality_maxDeviation() + assert not self.bandoverlaps2_new.has_good_quality_maxDeviation() + assert not self.bandoverlaps2.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) + assert not self.bandoverlaps2_new.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) + assert not self.bandoverlaps2.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=10, limit_deviation=0.0000001 + ) + assert not self.bandoverlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=10, limit_deviation=0.0000001 + ) + assert self.bandoverlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=1, limit_deviation=0.1) + assert self.bandoverlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=0.1 + ) + + assert not self.bandoverlaps2.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=1e-8 + ) + assert not self.bandoverlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=1e-8 + ) + assert self.bandoverlaps2.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=10, limit_deviation=0.1 + ) + assert self.bandoverlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=2, limit_deviation=0.1 + ) + + assert self.bandoverlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=1, limit_deviation=0.1) + assert self.bandoverlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=0.1 + ) + + +class GrosspopTest(unittest.TestCase): + def setUp(self): + self.grosspop1 = Grosspop(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "GROSSPOP.lobster")) + + def testattributes(self): + assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3s"] == approx(0.52) + assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3p_y"] == approx(0.38) + assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3p_z"] == approx(0.37) + assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3p_x"] == approx(0.37) + assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["total"] == approx(1.64) + assert self.grosspop1.list_dict_grosspop[0]["element"] == "Si" + assert self.grosspop1.list_dict_grosspop[0]["Loewdin GP"]["3s"] == approx(0.61) + assert self.grosspop1.list_dict_grosspop[0]["Loewdin GP"]["3p_y"] == approx(0.52) + assert self.grosspop1.list_dict_grosspop[0]["Loewdin GP"]["3p_z"] == approx(0.52) + assert self.grosspop1.list_dict_grosspop[0]["Loewdin GP"]["3p_x"] == approx(0.52) + assert self.grosspop1.list_dict_grosspop[0]["Loewdin GP"]["total"] == approx(2.16) + assert self.grosspop1.list_dict_grosspop[5]["Mulliken GP"]["2s"] == approx(1.80) + assert self.grosspop1.list_dict_grosspop[5]["Loewdin GP"]["2s"] == approx(1.60) + assert self.grosspop1.list_dict_grosspop[5]["element"] == "O" + assert self.grosspop1.list_dict_grosspop[8]["Mulliken GP"]["2s"] == approx(1.80) + assert self.grosspop1.list_dict_grosspop[8]["Loewdin GP"]["2s"] == approx(1.60) + assert self.grosspop1.list_dict_grosspop[8]["element"] == "O" + + def test_structure_with_grosspop(self): + struct_dict = { + "@module": "pymatgen.core.structure", + "@class": "Structure", + "charge": None, + "lattice": { + "matrix": [ + [5.021897888834907, 4.53806e-11, 0.0], + [-2.5109484443388332, 4.349090983701526, 0.0], + [0.0, 0.0, 5.511929408565514], + ], + "a": 5.021897888834907, + "b": 5.0218974974248045, + "c": 5.511929408565514, + "alpha": 90.0, + "beta": 90.0, + "gamma": 119.99999598960493, + "volume": 120.38434608659402, + }, + "sites": [ + { + "species": [{"element": "Si", "occu": 1}], + "abc": [-3e-16, 0.4763431475490085, 0.6666669999999968], + "xyz": [-1.1960730853096477, 2.0716596881533986, 3.674621443020128], + "label": "Si", + "properties": {"Total Mulliken GP": 1.64, "Total Loewdin GP": 2.16}, + }, + { + "species": [{"element": "Si", "occu": 1}], + "abc": [0.5236568524509936, 0.5236568524509926, 0.0], + "xyz": [1.3148758827683875, 2.277431295571896, 0.0], + "label": "Si", + "properties": {"Total Mulliken GP": 1.64, "Total Loewdin GP": 2.16}, + }, + { + "species": [{"element": "Si", "occu": 1}], + "abc": [0.4763431475490066, -1.2e-15, 0.3333330000000032], + "xyz": [ + 2.392146647037334, + 2.1611518932482004e-11, + 1.8373079655453863, + ], + "label": "Si", + "properties": {"Total Mulliken GP": 1.64, "Total Loewdin GP": 2.16}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.1589037798059321, 0.7440031622164922, 0.4613477252144715], + "xyz": [-1.0701550264153763, 3.235737444648381, 2.5429160941844473], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.2559968377835071, 0.4149006175894398, 0.7946807252144676], + "xyz": [0.2437959189219816, 1.8044405351020447, 4.380224059729795], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.5850993824105679, 0.8410962201940679, 0.1280147252144683], + "xyz": [0.8263601076506712, 3.6580039876980064, 0.7056081286390611], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.7440031622164928, 0.1589037798059326, 0.5386522747855285], + "xyz": [3.337308710918233, 0.6910869960638374, 2.969013314381067], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.4149006175894392, 0.2559968377835, 0.2053192747855324], + "xyz": [1.4407936739605638, 1.1133535390791505, 1.13170534883572], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.841096220194068, 0.5850993824105675, 0.8719852747855317], + "xyz": [2.754744948452184, 2.5446504486493, 4.806321279926453], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + ], + } + + new_structure = self.grosspop1.get_structure_with_total_grosspop( + os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "POSCAR.SiO2") + ) + assert np.allclose(new_structure.frac_coords, Structure.from_dict(struct_dict).frac_coords) + + +class TestUtils(PymatgenTest): + def test_get_all_possible_basis_combinations(self): + # this basis is just for testing (not correct) + min_basis = ["Li 1s 2s ", "Na 1s 2s", "Si 1s 2s"] + max_basis = ["Li 1s 2p 2s ", "Na 1s 2p 2s", "Si 1s 2s"] + combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) + assert combinations_basis == [ + ["Li 1s 2s", "Na 1s 2s", "Si 1s 2s"], + ["Li 1s 2s", "Na 1s 2s 2p", "Si 1s 2s"], + ["Li 1s 2s 2p", "Na 1s 2s", "Si 1s 2s"], + ["Li 1s 2s 2p", "Na 1s 2s 2p", "Si 1s 2s"], + ] + + min_basis = ["Li 1s 2s"] + max_basis = ["Li 1s 2s 2p 3s"] + combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) + assert combinations_basis == [["Li 1s 2s"], ["Li 1s 2s 2p"], ["Li 1s 2s 3s"], ["Li 1s 2s 2p 3s"]] + + min_basis = ["Li 1s 2s", "Na 1s 2s"] + max_basis = ["Li 1s 2s 2p 3s", "Na 1s 2s 2p 3s"] + combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) + assert combinations_basis == [ + ["Li 1s 2s", "Na 1s 2s"], + ["Li 1s 2s", "Na 1s 2s 2p"], + ["Li 1s 2s", "Na 1s 2s 3s"], + ["Li 1s 2s", "Na 1s 2s 2p 3s"], + ["Li 1s 2s 2p", "Na 1s 2s"], + ["Li 1s 2s 2p", "Na 1s 2s 2p"], + ["Li 1s 2s 2p", "Na 1s 2s 3s"], + ["Li 1s 2s 2p", "Na 1s 2s 2p 3s"], + ["Li 1s 2s 3s", "Na 1s 2s"], + ["Li 1s 2s 3s", "Na 1s 2s 2p"], + ["Li 1s 2s 3s", "Na 1s 2s 3s"], + ["Li 1s 2s 3s", "Na 1s 2s 2p 3s"], + ["Li 1s 2s 2p 3s", "Na 1s 2s"], + ["Li 1s 2s 2p 3s", "Na 1s 2s 2p"], + ["Li 1s 2s 2p 3s", "Na 1s 2s 3s"], + ["Li 1s 2s 2p 3s", "Na 1s 2s 2p 3s"], + ] + + min_basis = ["Si 1s 2s 2p", "Na 1s 2s"] + max_basis = ["Si 1s 2s 2p 3s", "Na 1s 2s 2p 3s"] + combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) + assert combinations_basis == [ + ["Si 1s 2s 2p", "Na 1s 2s"], + ["Si 1s 2s 2p", "Na 1s 2s 2p"], + ["Si 1s 2s 2p", "Na 1s 2s 3s"], + ["Si 1s 2s 2p", "Na 1s 2s 2p 3s"], + ["Si 1s 2s 2p 3s", "Na 1s 2s"], + ["Si 1s 2s 2p 3s", "Na 1s 2s 2p"], + ["Si 1s 2s 2p 3s", "Na 1s 2s 3s"], + ["Si 1s 2s 2p 3s", "Na 1s 2s 2p 3s"], + ] + + +class WavefunctionTest(PymatgenTest): + def test_parse_file(self): + grid, points, real, imaginary, distance = Wavefunction._parse_file( + os.path.join( + test_dir_doscar, + "cohp", + "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + ) + ) + assert_array_equal([41, 41, 41], grid) + assert points[4][0] == approx(0.0000) + assert points[4][1] == approx(0.0000) + assert points[4][2] == approx(0.4000) + assert real[8] == approx(1.38863e-01) + assert imaginary[8] == approx(2.89645e-01) + assert len(imaginary) == 41 * 41 * 41 + assert len(real) == 41 * 41 * 41 + assert len(points) == 41 * 41 * 41 + assert distance[0] == approx(0.0000) + + def test_set_volumetric_data(self): + wave1 = Wavefunction( + filename=f"{test_dir_doscar}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(os.path.join(test_dir_doscar, "cohp", "POSCAR_O.gz")), + ) + + wave1.set_volumetric_data(grid=wave1.grid, structure=wave1.structure) + assert hasattr(wave1, "volumetricdata_real") + assert hasattr(wave1, "volumetricdata_imaginary") + + def test_get_volumetricdata_real(self): + wave1 = Wavefunction( + filename=os.path.join( + test_dir_doscar, + "cohp", + "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + ), + structure=Structure.from_file(os.path.join(test_dir_doscar, "cohp", "POSCAR_O.gz")), + ) + volumetricdata_real = wave1.get_volumetricdata_real() + assert volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) + + def test_get_volumetricdata_imaginary(self): + wave1 = Wavefunction( + filename=os.path.join( + test_dir_doscar, + "cohp", + "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + ), + structure=Structure.from_file(os.path.join(test_dir_doscar, "cohp", "POSCAR_O.gz")), + ) + volumetricdata_imaginary = wave1.get_volumetricdata_imaginary() + assert volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) + + def test_get_volumetricdata_density(self): + wave1 = Wavefunction( + filename=os.path.join( + test_dir_doscar, + "cohp", + "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + ), + structure=Structure.from_file(os.path.join(test_dir_doscar, "cohp", "POSCAR_O.gz")), + ) + volumetricdata_density = wave1.get_volumetricdata_density() + assert volumetricdata_density.data["total"][0, 0, 0] == approx((-3.0966 * -3.0966) + (-6.45895 * -6.45895)) + + def test_write_file(self): + wave1 = Wavefunction( + filename=os.path.join( + test_dir_doscar, + "cohp", + "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + ), + structure=Structure.from_file(os.path.join(test_dir_doscar, "cohp", "POSCAR_O.gz")), + ) + wave1.write_file(filename=os.path.join("wavecar_test.vasp"), part="real") + assert os.path.isfile("wavecar_test.vasp") + + wave1.write_file(filename=os.path.join("wavecar_test.vasp"), part="imaginary") + assert os.path.isfile("wavecar_test.vasp") + os.remove("wavecar_test.vasp") + wave1.write_file(filename=os.path.join("density.vasp"), part="density") + assert os.path.isfile("density.vasp") + os.remove("density.vasp") + + def tearDown(self): + warnings.simplefilter("default") + + +class SitePotentialsTest(PymatgenTest): + def setUp(self) -> None: + self.sitepotential = SitePotential(filename=f"{test_dir_doscar}/cohp/SitePotentials.lobster.perovskite") + + def test_attributes(self): + assert self.sitepotential.sitepotentials_Loewdin == [-8.77, -17.08, 9.57, 9.57, 8.45] + assert self.sitepotential.sitepotentials_Mulliken == [-11.38, -19.62, 11.18, 11.18, 10.09] + assert self.sitepotential.madelungenergies_Loewdin == approx(-28.64) + assert self.sitepotential.madelungenergies_Mulliken == approx(-40.02) + assert self.sitepotential.atomlist == ["La1", "Ta2", "N3", "N4", "O5"] + assert self.sitepotential.types == ["La", "Ta", "N", "N", "O"] + assert self.sitepotential.num_atoms == 5 + assert self.sitepotential.ewald_splitting == approx(3.14) + + def test_get_structure(self): + structure = self.sitepotential.get_structure_with_site_potentials( + os.path.join(test_dir_doscar, "cohp", "POSCAR.perovskite") + ) + assert structure.site_properties["Loewdin Site Potentials (eV)"] == [-8.77, -17.08, 9.57, 9.57, 8.45] + assert structure.site_properties["Mulliken Site Potentials (eV)"] == [-11.38, -19.62, 11.18, 11.18, 10.09] + + +class MadelungEnergiesTest(PymatgenTest): + def setUp(self) -> None: + self.madelungenergies = MadelungEnergies(filename=f"{test_dir_doscar}/cohp/MadelungEnergies.lobster.perovskite") + + def test_attributes(self): + assert self.madelungenergies.madelungenergies_Loewdin == approx(-28.64) + assert self.madelungenergies.madelungenergies_Mulliken == approx(-40.02) + assert self.madelungenergies.ewald_splitting == approx(3.14) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py new file mode 100644 index 0000000000..4365486969 --- /dev/null +++ b/tests/io/lobster/test_lobsterenv.py @@ -0,0 +1,832 @@ +from __future__ import annotations + +import os +import unittest + +import numpy as np +import pytest +from pytest import approx + +from pymatgen.analysis.graphs import StructureGraph +from pymatgen.core import Element +from pymatgen.core.structure import Structure +from pymatgen.electronic_structure.cohp import Cohp +from pymatgen.electronic_structure.core import Spin +from pymatgen.io.lobster.lobsterenv import LobsterNeighbors +from pymatgen.util.testing import PymatgenTest + +__author__ = "Janine George" +__copyright__ = "Copyright 2021, The Materials Project" +__version__ = "0.1" +__email__ = "janine.george@uclouvain.be" +__date__ = "Jan 14, 2021" + +test_dir_env = f"{PymatgenTest.TEST_FILES_DIR}/cohp/environments" +this_dir = os.path.dirname(os.path.abspath(__file__)) + + +class TestLobsterNeighbors(unittest.TestCase): + def setUp(self): + # test additional conditions first + # only consider cation anion bonds + + self.chemenvlobster1 = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + additional_condition=1, + ) + + # all bonds + self.chemenvlobster0 = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + additional_condition=0, + ) + + # only cation cation, anion anion bonds + self.chemenvlobster5 = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + additional_condition=5, + ) + + # only cation cation bonds + self.chemenvlobster6 = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + additional_condition=6, + ) + + # 2,3,4 are not tested so far + self.chemenvlobster2 = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + additional_condition=2, + ) + + self.chemenvlobster3 = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + additional_condition=3, + ) + + self.chemenvlobster4 = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + additional_condition=4, + ) + + # search for other testcase where 2,3,4 arrive at different results + self.chemenvlobster0_second = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + additional_condition=0, + ) + self.chemenvlobster1_second = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + additional_condition=1, + ) + + self.chemenvlobster2_second = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + additional_condition=2, + ) + + self.chemenvlobster5_second = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + additional_condition=5, + ) + + self.chemenvlobster5_second_percentage = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + additional_condition=5, + perc_strength_ICOHP=1.0, + ) + + self.chemenvlobster6_second = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + additional_condition=6, + ) + # coop / cobi + self.chemenvlobster1_coop_NaCl = LobsterNeighbors( + are_coops=True, + filename_ICOHP=os.path.join(test_dir_env, "ICOOPLIST.lobster.NaCl.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + additional_condition=1, + noise_cutoff=None, + ) + + self.chemenvlobster1_cobi_NaCl = LobsterNeighbors( + are_coops=True, + filename_ICOHP=os.path.join(test_dir_env, "ICOBILIST.lobster.NaCl.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + additional_condition=1, + noise_cutoff=None, + ) + + self.chemenvlobster1_cobi_mp470 = LobsterNeighbors( + are_coops=True, + filename_ICOHP=os.path.join(test_dir_env, "ICOBILIST.lobster.mp_470.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_470.gz")), + additional_condition=1, + ) + + # TODO: use charge instead of valence + self.chemenvlobster1_charges = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + additional_condition=1, + ) + self.chemenvlobster1_charges_noisecutoff = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_632319.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_632319.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp_632319.gz"), + additional_condition=1, + perc_strength_ICOHP=0.05, + noise_cutoff=0.1, + ) + self.chemenvlobster1_charges_wo_noisecutoff = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_632319.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_632319.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp_632319.gz"), + additional_condition=1, + perc_strength_ICOHP=0.05, + noise_cutoff=None, + ) + self.chemenvlobster1_charges_loewdin = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + additional_condition=1, + which_charge="Loewdin", + ) + self.chemenvlobster6_charges_additional_condition = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + additional_condition=6, + adapt_extremum_to_add_cond=True, + ) + self.chemenvlobster5_charges_additional_condition = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + additional_condition=5, + adapt_extremum_to_add_cond=True, + ) + self.chemenvlobster4_charges_additional_condition = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + additional_condition=4, + adapt_extremum_to_add_cond=True, + ) + self.chemenvlobster3_charges_additional_condition = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + additional_condition=3, + adapt_extremum_to_add_cond=True, + ) + self.chemenvlobster2_charges_additional_condition = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + additional_condition=2, + adapt_extremum_to_add_cond=True, + ) + self.chemenvlobster1_charges_additional_condition = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + additional_condition=1, + adapt_extremum_to_add_cond=True, + ) + + self.chemenvlobster0_charges_additional_condition = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + additional_condition=0, + adapt_extremum_to_add_cond=True, + ) + self.chemenvlobster0_NaSi = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.NaSi.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaSi.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.NaSi.gz"), + additional_condition=0, + adapt_extremum_to_add_cond=True, + ) + + def test_cation_anion_mode_without_ions(self): + with pytest.raises( + ValueError, match="Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work" + ): + _ = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "../ICOHPLIST.lobster"), + structure=Structure.from_file(os.path.join(test_dir_env, "../POSCAR")), + valences_from_charges=False, + additional_condition=1, + ) + with pytest.raises( + ValueError, match="All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work" + ): + _ = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "../ICOHPLIST.lobster"), + structure=Structure.from_file(os.path.join(test_dir_env, "../POSCAR")), + valences_from_charges=False, + additional_condition=1, + valences=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + ) + + def test_wrong_additional_correction(self): + with pytest.raises( + ValueError, match=r"Unexpected additional_condition=10, must be one of \[0, 1, 2, 3, 4, 5, 6\]" + ): + LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + additional_condition=10, + ) + + def test_set_limits(self): + test = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + additional_condition=1, + limits=[-100000, 0], + ) + assert test.limits == [-100000, 0] + + def test_molecules_allowed(self): + assert not self.chemenvlobster1.molecules_allowed + + def test_get_anion_types(self): + assert self.chemenvlobster0_second.get_anion_types() == {Element("O")} + assert self.chemenvlobster0_second.anion_types == {Element("O")} + + def test_get_nn_info(self): + # NO_ADDITIONAL_CONDITION = 0 + # ONLY_ANION_CATION_BONDS = 1 + # NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2 + # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3 + # ONLY_ELEMENT_TO_OXYGEN_BONDS = 4 + # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 + # ONLY_CATION_CATION_BONDS=6 + + # All bonds + # ReO3 + assert ( + len( + self.chemenvlobster0.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=0, + ) + ) + == 6 + ) + assert ( + len( + self.chemenvlobster0.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=1, + ) + ) + == 2 + ) + # ONLY_ANION_CATION_BONDS = 1 + assert ( + len( + self.chemenvlobster1.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=0, + ) + ) + == 6 + ) + assert ( + len( + self.chemenvlobster1.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=1, + ) + ) + == 2 + ) + assert ( + len( + self.chemenvlobster1_charges_noisecutoff.get_nn( + structure=self.chemenvlobster1_charges_noisecutoff.structure, + n=1, + ) + ) + == 0 + ) + assert ( + len( + self.chemenvlobster1_charges_wo_noisecutoff.get_nn( + structure=self.chemenvlobster1_charges_wo_noisecutoff.structure, + n=1, + ) + ) + == 8 + ) + # NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2 + assert ( + len( + self.chemenvlobster2.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=0, + ) + ) + == 6 + ) + assert ( + len( + self.chemenvlobster2.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=1, + ) + ) + == 2 + ) + # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3 + assert ( + len( + self.chemenvlobster3.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=0, + ) + ) + == 6 + ) + assert ( + len( + self.chemenvlobster3.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=1, + ) + ) + == 2 + ) + # ONLY_ELEMENT_TO_OXYGEN_BONDS = 4 + assert ( + len( + self.chemenvlobster4.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=0, + ) + ) + == 6 + ) + assert ( + len( + self.chemenvlobster4.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=1, + ) + ) + == 2 + ) + # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 + assert ( + len( + self.chemenvlobster5.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=0, + ) + ) + == 0 + ) + assert ( + len( + self.chemenvlobster5.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=1, + ) + ) + == 0 + ) + # ONLY_CATION_CATION_BONDS=6 + assert ( + len( + self.chemenvlobster6.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=0, + ) + ) + == 0 + ) + + assert ( + len( + self.chemenvlobster6.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + n=1, + ) + ) + == 0 + ) + + # All bonds + # mp-353, Ag2O + # all bonds + assert ( + len( + self.chemenvlobster0_second.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + n=0, + ) + ) + == 8 + ) + + # ONLY_ANION_CATION_BONDS = 1 + assert ( + len( + self.chemenvlobster1_second.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + n=0, + ) + ) + == 2 + ) + + assert ( + len( + self.chemenvlobster1_coop_NaCl.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + n=0, + ) + ) + == 6 + ) + + assert ( + len( + self.chemenvlobster1_cobi_NaCl.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + n=0, + ) + ) + == 6 + ) + + assert ( + len( + self.chemenvlobster1_cobi_mp470.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_470.gz")), + n=3, + ) + ) + == 3 + ) + + # NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2 + assert ( + len( + self.chemenvlobster2_second.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + n=0, + ) + ) + == 2 + ) + assert ( + len( + self.chemenvlobster2_second.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + n=4, + ) + ) + == 4 + ) + + # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 + assert ( + len( + self.chemenvlobster5_second.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + n=0, + ) + ) + == 6 + ) + assert ( + len( + self.chemenvlobster5_second.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + n=4, + ) + ) + == 0 + ) + # ONLY_CATION_CATION_BONDS=6 + assert ( + len( + self.chemenvlobster6_second.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + n=0, + ) + ) + == 6 + ) + assert ( + len( + self.chemenvlobster6_second.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + n=4, + ) + ) + == 0 + ) + + assert ( + len( + self.chemenvlobster5_second_percentage.get_nn( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + n=0, + ) + ) + == 0 + ) + + def test_structure_graph(self): + sg = self.chemenvlobster1_second.get_bonded_structure( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")) + ) + assert isinstance(sg, StructureGraph) + + def test_extended_structure_graph(self): + self.chemenvlobsterNaCl = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.NaCl.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.NaCl.gz"), + filename_blist_sg1=os.path.join(test_dir_env, "ICOBILIST.lobster.NaCl.gz"), + filename_blist_sg2=os.path.join(test_dir_env, "ICOOPLIST.lobster.NaCl.gz"), + add_additional_data_sg=True, + id_blist_sg1="icobi", + id_blist_sg2="icoop", + additional_condition=1, + ) + sg = self.chemenvlobsterNaCl.get_bonded_structure( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + decorate=True, + edge_properties=True, + weights=True, + ) + assert sg.graph.get_edge_data(0, 1)[0]["ICOHP"] == approx(-0.56541) + assert sg.graph.get_edge_data(0, 1)[0]["ICOBI"] == approx(0.08484) + assert sg.graph.get_edge_data(0, 1)[0]["ICOOP"] == approx(0.02826) + assert sg.graph.get_edge_data(0, 1)[0]["bond_label"] == "21" + assert sg.graph.get_edge_data(0, 1)[5]["bond_label"] == "30" + assert isinstance(sg, StructureGraph) + + def test_raises_extended_structure_graph(self): + with pytest.raises(ValueError, match="Algorithm can only work with ICOOPs, ICOBIs"): + self.chemenvlobsterNaCl = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.NaCl.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + valences_from_charges=True, + filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.NaCl.gz"), + filename_blist_sg1=os.path.join(test_dir_env, "ICOBILIST.lobster.NaCl.gz"), + filename_blist_sg2=os.path.join(test_dir_env, "ICOOPLIST.lobster.NaCl.gz"), + add_additional_data_sg=True, + id_blist_sg1="icopppp", + id_blist_sg2="icoop", + additional_condition=1, + ) + + def test_order_parameter(self): + assert self.chemenvlobster1_second.get_local_order_parameters( + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), n=0 + )["linear"] == approx(1.0) + + def test_get_structure_environments(self): + lse = self.chemenvlobster1_second.get_light_structure_environment() + assert lse.coordination_environments[0][0]["ce_symbol"] == "L:2" + assert lse.coordination_environments[5][0]["ce_symbol"] == "T:4" + + lse2 = self.chemenvlobster1.get_light_structure_environment() + assert lse2.coordination_environments[0][0]["ce_symbol"] == "O:6" + + def test_get_strucuture_environments_further_tests(self): + lse = self.chemenvlobster1_second.get_light_structure_environment() + lse.as_dict() + lse.get_statistics() + assert lse.uniquely_determines_coordination_environments + + def test_get_info_icohps_neighbors(self): + results = self.chemenvlobster1.get_info_icohps_to_neighbors(isites=[0]) + assert results[0] == approx(-33.26058) + for bond in results[1]: + assert bond == approx(-5.54345, abs=1e-3) + assert results[2] == 6 + assert results[3] == ["27", "30", "48", "49", "64", "73"] + + results2 = self.chemenvlobster1.get_info_icohps_to_neighbors(isites=None) + assert results2[0] == approx(-33.26058) + for bond in results2[1]: + assert bond == approx(-5.54345, abs=1e-3) + assert results2[2] == 6 + assert results2[3] == ["27", "30", "48", "49", "64", "73"] + assert results2[4] == [ + ["Re1", "O2"], + ["Re1", "O2"], + ["Re1", "O3"], + ["Re1", "O3"], + ["Re1", "O4"], + ["Re1", "O4"], + ] + + def test_get_sum_icohps_between_neighbors_of_atom(self): + # will only look at icohps between cations or anions + self.chemenvlobster1.get_info_icohps_to_neighbors(isites=[1]) + assert self.chemenvlobster1.get_info_icohps_between_neighbors(isites=[1])[2] == 1 + assert self.chemenvlobster1.get_info_icohps_between_neighbors(isites=[1])[0] == approx(-0.05507) + assert self.chemenvlobster1.get_info_icohps_between_neighbors(isites=[0])[2] == 15 + # use an example where this is easier to test (e.g., linear environment?) + + chemenv_here = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp-7000.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp-7000.gz")), + additional_condition=1, + ) + assert len(chemenv_here.get_info_icohps_between_neighbors(isites=[0])[4]) == 6 + + def test_get_plot_label(self): + label = self.chemenvlobster1._get_plot_label( + atoms=[["Re1", "O2"], ["Re1", "O2"], ["Re1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Re1", "O4"]], + per_bond=False, + ) + assert label == "6 x O-Re" + + label = self.chemenvlobster1._get_plot_label( + atoms=[["Re1", "O2"], ["Re1", "O2"], ["Re1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Si1", "O4"]], + per_bond=False, + ) + assert label == "5 x O-Re, 1 x O-Si" + + label = self.chemenvlobster1._get_plot_label( + atoms=[["Si1", "O2"], ["Si1", "O2"], ["Si1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Si1", "O4"]], + per_bond=False, + ) + assert label == "4 x O-Si, 2 x O-Re" + + label = self.chemenvlobster1._get_plot_label( + atoms=[["Re1", "O2"], ["Re1", "O2"], ["Re1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Re1", "O4"]], + per_bond=True, + ) + assert label == "6 x O-Re (per bond)" + + def test_get_info_cohps_to_neighbors(self): + chemenvlobster1 = LobsterNeighbors( + are_coops=False, + filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190_2.gz"), + structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + additional_condition=1, + ) + cohpcar_lobster_mp_190 = os.path.join(test_dir_env, "COHPCAR.lobster.mp-190.gz") + plot_label, summed_cohpcar_mp_190 = chemenvlobster1.get_info_cohps_to_neighbors( + path_to_COHPCAR=cohpcar_lobster_mp_190, + isites=[0], + only_bonds_to=["O"], + ) + assert plot_label == "6 x O-Re (per bond)" + assert isinstance(summed_cohpcar_mp_190, Cohp) + + coph_thing = chemenvlobster1.get_info_cohps_to_neighbors( + path_to_COHPCAR=cohpcar_lobster_mp_190, + isites=[0], + only_bonds_to=None, + per_bond=False, + )[1] + assert np.sum([coph_thing.icohp[Spin.up], coph_thing.icohp[Spin.down]], axis=0)[300] == approx( + chemenvlobster1.get_info_icohps_to_neighbors(isites=[0])[0] + ) + + # summed_spin_channel + coph_thing = chemenvlobster1.get_info_cohps_to_neighbors( + path_to_COHPCAR=cohpcar_lobster_mp_190, + isites=[0], + only_bonds_to=None, + per_bond=False, + summed_spin_channels=True, + )[1] + assert coph_thing.icohp[Spin.up][300] == approx(chemenvlobster1.get_info_icohps_to_neighbors(isites=[0])[0]) + + plot_label, summed_cohpcar_mp_190_Te = chemenvlobster1.get_info_cohps_to_neighbors( + path_to_COHPCAR=cohpcar_lobster_mp_190, + isites=[0], + only_bonds_to=["Te"], + ) + + assert plot_label is None + assert summed_cohpcar_mp_190_Te is None + + plot_label, _summed_cohpcar_NaSi = self.chemenvlobster0_NaSi.get_info_cohps_to_neighbors( + path_to_COHPCAR=os.path.join(test_dir_env, "COHPCAR.lobster.NaSi.gz"), + isites=[8], + onlycation_isites=False, + only_bonds_to=["Na"], + ) + assert plot_label == "1 x Na-Si (per bond)" + assert ( + self.chemenvlobster0_NaSi.get_info_cohps_to_neighbors( + path_to_COHPCAR=os.path.join(test_dir_env, "COHPCAR.lobster.NaSi.gz"), + isites=[8], + onlycation_isites=False, + only_bonds_to=["Si"], + )[0] + == "3 x Si-Si (per bond)" + ) + + chemenvlobster1.plot_cohps_of_neighbors( + path_to_COHPCAR=cohpcar_lobster_mp_190, + isites=[0], + only_bonds_to=["O"], + summed_spin_channels=True, + ) + + chemenvlobster1.plot_cohps_of_neighbors( + path_to_COHPCAR=cohpcar_lobster_mp_190, + isites=[0], + only_bonds_to=["O"], + summed_spin_channels=True, + xlim=[-10, 10], + ylim=None, + ) + + expected_msg = "COHPCAR and ICOHPLIST do not fit together" + with pytest.raises(ValueError, match=expected_msg): + # icohplist and cohpcar do not fit together + self.chemenvlobster1.get_info_cohps_to_neighbors( + path_to_COHPCAR=cohpcar_lobster_mp_190, + isites=[0], + only_bonds_to=None, + per_bond=False, + ) + + with pytest.raises(ValueError, match=expected_msg): + # icohplist and cohpcar do not fit together + self.chemenvlobster2.get_info_cohps_to_neighbors( + path_to_COHPCAR=cohpcar_lobster_mp_190, + isites=[0], + only_bonds_to=None, + per_bond=False, + ) From f2fc326906fd4d2e5b852edb0bff3f0ecae76fb1 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Wed, 2 Aug 2023 15:35:51 -0700 Subject: [PATCH 002/180] Fix tests. --- tests/io/lobster/test_lobster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/io/lobster/test_lobster.py b/tests/io/lobster/test_lobster.py index 62383774e1..122641b351 100644 --- a/tests/io/lobster/test_lobster.py +++ b/tests/io/lobster/test_lobster.py @@ -896,7 +896,7 @@ def test_get_structure_with_charges(self): "@module": "pymatgen.core.structure", } s2 = Structure.from_dict(structure_dict2) - assert s2 == self.charge2.get_structure_with_charges(os.path.join(this_dir, "../../tests/POSCAR.MnO")) + assert s2 == self.charge2.get_structure_with_charges(PymatgenTest.TEST_FILES_DIR / "POSCAR.MnO") class LobsteroutTest(PymatgenTest): From 4d936567681beabf6fc3c53e5722e72b3e5fc1b7 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Wed, 2 Aug 2023 21:34:05 -0700 Subject: [PATCH 003/180] Switch out of using Pymatgen.assert_all_close. --- tests/io/lobster/test_lobster.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/io/lobster/test_lobster.py b/tests/io/lobster/test_lobster.py index 122641b351..963093a089 100644 --- a/tests/io/lobster/test_lobster.py +++ b/tests/io/lobster/test_lobster.py @@ -364,12 +364,12 @@ def test_orbital_resolved_cohp(self): [self.orb.orb_res_cohp["1"][orbs]["COHP"][Spin.up] for orbs in self.orb.orb_res_cohp["1"]], axis=0, ) - self.assert_all_close(tot, cohp, decimal=3) + assert np.allclose(tot, cohp, atol=1e-3) tot = np.sum( [self.orb.orb_res_cohp["1"][orbs]["ICOHP"][Spin.up] for orbs in self.orb.orb_res_cohp["1"]], axis=0, ) - self.assert_all_close(tot, icohp, decimal=3) + assert np.allclose(tot, icohp, atol=1e-3) # Lobster 3.1 cohp_KF = self.cohp_KF.cohp_data["1"]["COHP"][Spin.up] @@ -378,12 +378,12 @@ def test_orbital_resolved_cohp(self): [self.cohp_KF.orb_res_cohp["1"][orbs]["COHP"][Spin.up] for orbs in self.cohp_KF.orb_res_cohp["1"]], axis=0, ) - self.assert_all_close(tot_KF, cohp_KF, decimal=3) + assert np.allclose(tot_KF, cohp_KF, atol=1e-3) tot_KF = np.sum( [self.cohp_KF.orb_res_cohp["1"][orbs]["ICOHP"][Spin.up] for orbs in self.cohp_KF.orb_res_cohp["1"]], axis=0, ) - self.assert_all_close(tot_KF, icohp_KF, decimal=3) + assert np.allclose(tot_KF, icohp_KF, atol=1e-3) # d and f orbitals cohp_Na2UO4 = self.cohp_Na2UO4.cohp_data["49"]["COHP"][Spin.up] @@ -395,7 +395,7 @@ def test_orbital_resolved_cohp(self): ], axis=0, ) - self.assert_all_close(tot_Na2UO4, cohp_Na2UO4, decimal=3) + assert np.allclose(tot_Na2UO4, cohp_Na2UO4, atol=1e-3) tot_Na2UO4 = np.sum( [ self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["ICOHP"][Spin.up] @@ -403,7 +403,7 @@ def test_orbital_resolved_cohp(self): ], axis=0, ) - self.assert_all_close(tot_Na2UO4, icohp_Na2UO4, decimal=3) + assert np.allclose(tot_Na2UO4, icohp_Na2UO4, atol=1e-3) class IcohplistTest(unittest.TestCase): From a3484b949cc9d8c6984b08c7d50439af8ad3f96d Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Thu, 3 Aug 2023 04:49:30 -0700 Subject: [PATCH 004/180] Ensure all tests follow pytest naming convention. --- tests/io/lobster/test_lobster.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/io/lobster/test_lobster.py b/tests/io/lobster/test_lobster.py index 963093a089..2048a8c636 100644 --- a/tests/io/lobster/test_lobster.py +++ b/tests/io/lobster/test_lobster.py @@ -43,7 +43,7 @@ this_dir = os.path.dirname(os.path.abspath(__file__)) -class CohpcarTest(PymatgenTest): +class TestCohpcar(PymatgenTest): def setUp(self): self.cohp_bise = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.BiSe.gz") self.coop_bise = Cohpcar( @@ -406,7 +406,7 @@ def test_orbital_resolved_cohp(self): assert np.allclose(tot_Na2UO4, icohp_Na2UO4, atol=1e-3) -class IcohplistTest(unittest.TestCase): +class TestIcohplist(unittest.TestCase): def setUp(self): self.icohp_bise = Icohplist(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOHPLIST.lobster.BiSe") self.icoop_bise = Icohplist( @@ -671,7 +671,7 @@ def test_values(self): assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["orbitals"]["2s-6s"]["icohp"][Spin.up] == 0.0247 -class DoscarTest(unittest.TestCase): +class TestDoscar(unittest.TestCase): def setUp(self): # first for spin polarized version doscar = os.path.join(test_dir_doscar, "DOSCAR.lobster.spin") @@ -841,7 +841,7 @@ def test_is_spin_polarized(self): assert not self.DOSCAR_nonspin_pol.is_spin_polarized -class ChargeTest(PymatgenTest): +class TestCharge(PymatgenTest): def setUp(self): self.charge2 = Charge(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/CHARGE.lobster.MnO") # gzipped file @@ -899,7 +899,7 @@ def test_get_structure_with_charges(self): assert s2 == self.charge2.get_structure_with_charges(PymatgenTest.TEST_FILES_DIR / "POSCAR.MnO") -class LobsteroutTest(PymatgenTest): +class TestLobsterout(PymatgenTest): def setUp(self): warnings.simplefilter("ignore") self.lobsterout_normal = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.normal") @@ -1355,7 +1355,7 @@ def test_get_doc(self): assert item == comparedict[key] -class FatbandTest(PymatgenTest): +class TestFatband(PymatgenTest): def setUp(self): warnings.simplefilter("ignore") self.fatband_SiO2_p_x = Fatband( @@ -1561,7 +1561,7 @@ def test_get_bandstructure(self): assert bs_p_x.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064), abs=1e-2) -class LobsterinTest(unittest.TestCase): +class TestLobsterin(unittest.TestCase): def setUp(self): warnings.simplefilter("ignore") self.Lobsterinfromfile = Lobsterin.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "lobsterin.1")) @@ -2019,7 +2019,7 @@ def tearDown(self): warnings.simplefilter("default") -class BandoverlapsTest(unittest.TestCase): +class TestBandoverlaps(unittest.TestCase): def setUp(self): warnings.simplefilter("ignore") # test spin polarlized calc and non spinpolarized calc @@ -2175,7 +2175,7 @@ def test_has_good_quality(self): ) -class GrosspopTest(unittest.TestCase): +class TestGrosspop(unittest.TestCase): def setUp(self): self.grosspop1 = Grosspop(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "GROSSPOP.lobster")) @@ -2349,7 +2349,7 @@ def test_get_all_possible_basis_combinations(self): ] -class WavefunctionTest(PymatgenTest): +class TestWavefunction(PymatgenTest): def test_parse_file(self): grid, points, real, imaginary, distance = Wavefunction._parse_file( os.path.join( @@ -2438,7 +2438,7 @@ def tearDown(self): warnings.simplefilter("default") -class SitePotentialsTest(PymatgenTest): +class TestSitePotentials(PymatgenTest): def setUp(self) -> None: self.sitepotential = SitePotential(filename=f"{test_dir_doscar}/cohp/SitePotentials.lobster.perovskite") @@ -2460,7 +2460,7 @@ def test_get_structure(self): assert structure.site_properties["Mulliken Site Potentials (eV)"] == [-11.38, -19.62, 11.18, 11.18, 10.09] -class MadelungEnergiesTest(PymatgenTest): +class TestMadelungEnergies(PymatgenTest): def setUp(self) -> None: self.madelungenergies = MadelungEnergies(filename=f"{test_dir_doscar}/cohp/MadelungEnergies.lobster.perovskite") From 6c5fbf99bf985401a366eb770c6e7b290f628f06 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sun, 6 Aug 2023 13:19:55 -0700 Subject: [PATCH 005/180] Move `PymatgenTest.TEST_FILES_DIR` attribute into module scope (#3227) * fix IonEntry doc str + type hints * move TEST_FILES_DIR attr from PymatgenTest into module scope * PymatgenTest.TEST_FILES_DIR -> TEST_FILES_DIR * delete TEST_FILES_DIR conftest fixture * fix AttributeError: type object 'TestChgcar' has no attribute 'TEST_FILES_DIR' tests/io/vasp/test_outputs.py:1444 * os.path.join(TEST_FILES_DIR, "...") -> f"{TEST_FILES_DIR}/..." --- tests/io/lobster/test_lobster.py | 172 +++++++++++++--------------- tests/io/lobster/test_lobsterenv.py | 4 +- 2 files changed, 79 insertions(+), 97 deletions(-) diff --git a/tests/io/lobster/test_lobster.py b/tests/io/lobster/test_lobster.py index 2048a8c636..b329752b65 100644 --- a/tests/io/lobster/test_lobster.py +++ b/tests/io/lobster/test_lobster.py @@ -30,7 +30,7 @@ from pymatgen.io.lobster.inputs import get_all_possible_basis_combinations from pymatgen.io.vasp import Vasprun from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar -from pymatgen.util.testing import PymatgenTest +from pymatgen.util.testing import TEST_FILES_DIR, PymatgenTest __author__ = "Janine George, Marco Esters" __copyright__ = "Copyright 2017, The Materials Project" @@ -38,41 +38,41 @@ __email__ = "janine.george@uclouvain.be, esters@uoregon.edu" __date__ = "Dec 10, 2017" -test_dir_doscar = PymatgenTest.TEST_FILES_DIR +test_dir_doscar = TEST_FILES_DIR this_dir = os.path.dirname(os.path.abspath(__file__)) class TestCohpcar(PymatgenTest): def setUp(self): - self.cohp_bise = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.BiSe.gz") + self.cohp_bise = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.BiSe.gz") self.coop_bise = Cohpcar( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COOPCAR.lobster.BiSe.gz", + filename=f"{TEST_FILES_DIR}/cohp/COOPCAR.lobster.BiSe.gz", are_coops=True, ) - self.cohp_fe = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COOPCAR.lobster.gz") + self.cohp_fe = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COOPCAR.lobster.gz") self.coop_fe = Cohpcar( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COOPCAR.lobster.gz", + filename=f"{TEST_FILES_DIR}/cohp/COOPCAR.lobster.gz", are_coops=True, ) - self.orb = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.orbitalwise.gz") - self.orb_notot = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.notot.orbitalwise.gz") + self.orb = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.orbitalwise.gz") + self.orb_notot = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.notot.orbitalwise.gz") # Lobster 3.1 (Test data is from prerelease of Lobster 3.1) - self.cohp_KF = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.KF.gz") + self.cohp_KF = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.KF.gz") self.coop_KF = Cohpcar( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.KF.gz", + filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.KF.gz", are_coops=True, ) # example with f electrons - self.cohp_Na2UO4 = Cohpcar(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COHPCAR.lobster.Na2UO4.gz") + self.cohp_Na2UO4 = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.Na2UO4.gz") self.coop_Na2UO4 = Cohpcar( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COOPCAR.lobster.Na2UO4.gz", + filename=f"{TEST_FILES_DIR}/cohp/COOPCAR.lobster.Na2UO4.gz", are_coops=True, ) self.cobi = Cohpcar( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/COBICAR.lobster.gz", + filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.gz", are_cobis=True, ) @@ -408,41 +408,41 @@ def test_orbital_resolved_cohp(self): class TestIcohplist(unittest.TestCase): def setUp(self): - self.icohp_bise = Icohplist(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOHPLIST.lobster.BiSe") + self.icohp_bise = Icohplist(filename=f"{TEST_FILES_DIR}/cohp/ICOHPLIST.lobster.BiSe") self.icoop_bise = Icohplist( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOOPLIST.lobster.BiSe", + filename=f"{TEST_FILES_DIR}/cohp/ICOOPLIST.lobster.BiSe", are_coops=True, ) - self.icohp_fe = Icohplist(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOHPLIST.lobster") + self.icohp_fe = Icohplist(filename=f"{TEST_FILES_DIR}/cohp/ICOHPLIST.lobster") # allow gzipped files - self.icohp_gzipped = Icohplist(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOHPLIST.lobster.gz") + self.icohp_gzipped = Icohplist(filename=f"{TEST_FILES_DIR}/cohp/ICOHPLIST.lobster.gz") self.icoop_fe = Icohplist( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOHPLIST.lobster", + filename=f"{TEST_FILES_DIR}/cohp/ICOHPLIST.lobster", are_coops=True, ) # ICOBIs and orbitalwise ICOBILIST.lobster self.icobi_orbitalwise = Icohplist( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOBILIST.lobster", + filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster", are_cobis=True, ) # TODO: test orbitalwise ICOHPs with and without spin polarization self.icobi = Icohplist( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOBILIST.lobster.withoutorbitals", + filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster.withoutorbitals", are_cobis=True, ) self.icobi_orbitalwise_spinpolarized = Icohplist( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOBILIST.lobster.spinpolarized", + filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster.spinpolarized", are_cobis=True, ) # make sure the correct line is read to check if this is a orbitalwise ICOBILIST self.icobi_orbitalwise_add = Icohplist( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/ICOBILIST.lobster.additional_case", + filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster.additional_case", are_cobis=True, ) self.icobi_orbitalwise_spinpolarized_add = Icohplist( filename=os.path.join( - PymatgenTest.TEST_FILES_DIR, + TEST_FILES_DIR, "cohp", "ICOBILIST.lobster.spinpolarized.additional_case", ), @@ -843,9 +843,9 @@ def test_is_spin_polarized(self): class TestCharge(PymatgenTest): def setUp(self): - self.charge2 = Charge(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/CHARGE.lobster.MnO") + self.charge2 = Charge(filename=f"{TEST_FILES_DIR}/cohp/CHARGE.lobster.MnO") # gzipped file - self.charge = Charge(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/CHARGE.lobster.MnO2.gz") + self.charge = Charge(filename=f"{TEST_FILES_DIR}/cohp/CHARGE.lobster.MnO2.gz") def testattributes(self): charge_Loewdin = [-1.25, 1.25] @@ -896,42 +896,34 @@ def test_get_structure_with_charges(self): "@module": "pymatgen.core.structure", } s2 = Structure.from_dict(structure_dict2) - assert s2 == self.charge2.get_structure_with_charges(PymatgenTest.TEST_FILES_DIR / "POSCAR.MnO") + assert s2 == self.charge2.get_structure_with_charges(TEST_FILES_DIR / "POSCAR.MnO") class TestLobsterout(PymatgenTest): def setUp(self): warnings.simplefilter("ignore") - self.lobsterout_normal = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.normal") + self.lobsterout_normal = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.normal") # make sure .gz files are also read correctly - self.lobsterout_normal = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.normal2.gz") + self.lobsterout_normal = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.normal2.gz") self.lobsterout_fatband_grosspop_densityofenergies = Lobsterout( filename=os.path.join( - PymatgenTest.TEST_FILES_DIR, + TEST_FILES_DIR, "cohp", "lobsterout.fatband_grosspop_densityofenergy", ) ) - self.lobsterout_saveprojection = Lobsterout( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.saveprojection" - ) - self.lobsterout_skipping_all = Lobsterout( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.skipping_all" - ) - self.lobsterout_twospins = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.twospins") - self.lobsterout_GaAs = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.GaAs") - self.lobsterout_from_projection = Lobsterout( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout_from_projection" - ) - self.lobsterout_onethread = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.onethread") - self.lobsterout_cobi_madelung = Lobsterout( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout_cobi_madelung" - ) - self.lobsterout_doscar_lso = Lobsterout(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout_doscar_lso") + self.lobsterout_saveprojection = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.saveprojection") + self.lobsterout_skipping_all = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.skipping_all") + self.lobsterout_twospins = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.twospins") + self.lobsterout_GaAs = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.GaAs") + self.lobsterout_from_projection = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout_from_projection") + self.lobsterout_onethread = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.onethread") + self.lobsterout_cobi_madelung = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout_cobi_madelung") + self.lobsterout_doscar_lso = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout_doscar_lso") # TODO: implement skipping madelung/cobi self.lobsterout_skipping_cobi_madelung = Lobsterout( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/lobsterout.skip_cobi_madelung" + filename=f"{TEST_FILES_DIR}/cohp/lobsterout.skip_cobi_madelung" ) def tearDown(self): @@ -1359,33 +1351,31 @@ class TestFatband(PymatgenTest): def setUp(self): warnings.simplefilter("ignore") self.fatband_SiO2_p_x = Fatband( - filenames=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x", - Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", - ) - self.vasprun_SiO2_p_x = Vasprun( - filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml" + filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x", + Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", ) + self.vasprun_SiO2_p_x = Vasprun(filename=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml") self.bs_symmline = self.vasprun_SiO2_p_x.get_band_structure(line_mode=True, force_hybrid_mode=True) self.fatband_SiO2_p = Fatband( - filenames=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p", - Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/KPOINTS", - vasprun=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/vasprun.xml", + filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p", + Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/KPOINTS", + vasprun=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/vasprun.xml", ) - self.vasprun_SiO2_p = Vasprun(filename=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/vasprun.xml") + self.vasprun_SiO2_p = Vasprun(filename=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/vasprun.xml") self.bs_symmline2 = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) self.fatband_SiO2_spin = Fatband( - filenames=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin", - Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/KPOINTS", + filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin", + Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/KPOINTS", vasprun=os.path.join( - PymatgenTest.TEST_FILES_DIR, + TEST_FILES_DIR, "cohp", "Fatband_SiO2/Test_Spin/vasprun.xml", ), ) self.vasprun_SiO2_spin = Vasprun( filename=os.path.join( - PymatgenTest.TEST_FILES_DIR, + TEST_FILES_DIR, "cohp", "Fatband_SiO2/Test_Spin/vasprun.xml", ) @@ -1449,11 +1439,11 @@ def test_raises(self): ): self.fatband_SiO2_p_x = Fatband( filenames=[ - f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", - f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", ], - Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", ) with pytest.raises( @@ -1462,18 +1452,18 @@ def test_raises(self): ): self.fatband_SiO2_p_x = Fatband( filenames=[ - f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", - f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/FATBAND_si1_3p.lobster", + f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/FATBAND_si1_3p.lobster", ], - Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", ) with pytest.raises(ValueError, match="No FATBAND files in folder or given"): self.fatband_SiO2_p_x = Fatband( filenames=".", - Kpointsfile=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun=f"{PymatgenTest.TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", ) def test_get_bandstructure(self): @@ -1564,12 +1554,10 @@ def test_get_bandstructure(self): class TestLobsterin(unittest.TestCase): def setUp(self): warnings.simplefilter("ignore") - self.Lobsterinfromfile = Lobsterin.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "lobsterin.1")) - self.Lobsterinfromfile2 = Lobsterin.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "lobsterin.2")) - self.Lobsterinfromfile3 = Lobsterin.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "lobsterin.3")) - self.Lobsterinfromfile4 = Lobsterin.from_file( - os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "lobsterin.4.gz") - ) + self.Lobsterinfromfile = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.1") + self.Lobsterinfromfile2 = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.2") + self.Lobsterinfromfile3 = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.3") + self.Lobsterinfromfile4 = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.4.gz") def test_from_file(self): # test read from file @@ -1787,10 +1775,10 @@ def test_get_basis(self): Structure.from_file(os.path.join(test_dir_doscar, "Fe3O4.cif")), potcar_symbols=Potcar_names, ) == ["Fe 3d 4p 4s ", "O 2p 2s "] - potcar = Potcar.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "POTCAR.GaAs")) + potcar = Potcar.from_file(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") Potcar_names = [name["symbol"] for name in potcar.spec] assert lobsterin1.get_basis( - Structure.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "POSCAR.GaAs")), + Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR.GaAs"), potcar_symbols=Potcar_names, ) == ["Ga 3d 4p 4s ", "As 4p 4s "] @@ -1815,7 +1803,7 @@ def test_get_all_possible_basis_functions(self): def test_get_potcar_symbols(self): lobsterin1 = Lobsterin({}) assert lobsterin1._get_potcar_symbols(os.path.join(test_dir_doscar, "POTCAR.Fe3O4")) == ["Fe", "O"] - assert lobsterin1._get_potcar_symbols(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "POTCAR.GaAs")) == [ + assert lobsterin1._get_potcar_symbols(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") == [ "Ga_d", "As", ] @@ -1940,7 +1928,7 @@ def test_write_KPOINTS(self): # # #without line mode, using a certain grid, isym=0 instead of -1 lobsterin1.write_KPOINTS( - POSCAR_input=f"{PymatgenTest.TEST_FILES_DIR}/cohp/POSCAR.Li", + POSCAR_input=f"{TEST_FILES_DIR}/cohp/POSCAR.Li", KPOINTS_output=outfile_path, line_mode=False, from_grid=True, @@ -1949,7 +1937,7 @@ def test_write_KPOINTS(self): ) kpoint1 = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "IBZKPT_3_3_3_Li")) + kpoint2 = Kpoints.from_file(f"{TEST_FILES_DIR}/cohp/IBZKPT_3_3_3_Li") for ikpoint, kpoint in enumerate(kpoint1.kpts): assert self.is_kpoint_in_list( kpoint, @@ -1966,7 +1954,7 @@ def test_write_KPOINTS(self): ) lobsterin1.write_KPOINTS( - POSCAR_input=f"{PymatgenTest.TEST_FILES_DIR}/cohp/POSCAR.Li", + POSCAR_input=f"{TEST_FILES_DIR}/cohp/POSCAR.Li", KPOINTS_output=outfile_path, line_mode=False, from_grid=True, @@ -1975,7 +1963,7 @@ def test_write_KPOINTS(self): ) kpoint1 = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "IBZKPT_2_2_2_Li")) + kpoint2 = Kpoints.from_file(f"{TEST_FILES_DIR}/cohp/IBZKPT_2_2_2_Li") for ikpoint, kpoint in enumerate(kpoint1.kpts): assert self.is_kpoint_in_list( kpoint, @@ -2024,15 +2012,11 @@ def setUp(self): warnings.simplefilter("ignore") # test spin polarlized calc and non spinpolarized calc - self.bandoverlaps1 = Bandoverlaps(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "bandOverlaps.lobster.1")) - self.bandoverlaps2 = Bandoverlaps(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "bandOverlaps.lobster.2")) + self.bandoverlaps1 = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.1") + self.bandoverlaps2 = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.2") - self.bandoverlaps1_new = Bandoverlaps( - os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "bandOverlaps.lobster.new.1") - ) - self.bandoverlaps2_new = Bandoverlaps( - os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "bandOverlaps.lobster.new.2") - ) + self.bandoverlaps1_new = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.new.1") + self.bandoverlaps2_new = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.new.2") def test_attributes(self): # bandoverlapsdict @@ -2177,7 +2161,7 @@ def test_has_good_quality(self): class TestGrosspop(unittest.TestCase): def setUp(self): - self.grosspop1 = Grosspop(os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "GROSSPOP.lobster")) + self.grosspop1 = Grosspop(f"{TEST_FILES_DIR}/cohp/GROSSPOP.lobster") def testattributes(self): assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3s"] == approx(0.52) @@ -2288,9 +2272,7 @@ def test_structure_with_grosspop(self): ], } - new_structure = self.grosspop1.get_structure_with_total_grosspop( - os.path.join(PymatgenTest.TEST_FILES_DIR, "cohp", "POSCAR.SiO2") - ) + new_structure = self.grosspop1.get_structure_with_total_grosspop(f"{TEST_FILES_DIR}/cohp/POSCAR.SiO2") assert np.allclose(new_structure.frac_coords, Structure.from_dict(struct_dict).frac_coords) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 4365486969..ef9814eca5 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -13,7 +13,7 @@ from pymatgen.electronic_structure.cohp import Cohp from pymatgen.electronic_structure.core import Spin from pymatgen.io.lobster.lobsterenv import LobsterNeighbors -from pymatgen.util.testing import PymatgenTest +from pymatgen.util.testing import TEST_FILES_DIR __author__ = "Janine George" __copyright__ = "Copyright 2021, The Materials Project" @@ -21,7 +21,7 @@ __email__ = "janine.george@uclouvain.be" __date__ = "Jan 14, 2021" -test_dir_env = f"{PymatgenTest.TEST_FILES_DIR}/cohp/environments" +test_dir_env = f"{TEST_FILES_DIR}/cohp/environments" this_dir = os.path.dirname(os.path.abspath(__file__)) From 45d324c95f64adc43600766a1391a61e3d6cef55 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Mon, 7 Aug 2023 13:13:41 -0700 Subject: [PATCH 006/180] f-string path construction everywhere, no need for `os.path.join(...)` (#3229) * simplify: f-string path construction everywhere, no need for os.path.join(...) * fix _get_oxid_state_guesses doc str * rename to _get_oxi_state_guesses --- tests/io/lobster/test_lobster.py | 96 ++++++----- tests/io/lobster/test_lobsterenv.py | 248 ++++++++++++++-------------- 2 files changed, 171 insertions(+), 173 deletions(-) diff --git a/tests/io/lobster/test_lobster.py b/tests/io/lobster/test_lobster.py index b329752b65..dde95c7d7c 100644 --- a/tests/io/lobster/test_lobster.py +++ b/tests/io/lobster/test_lobster.py @@ -674,20 +674,20 @@ def test_values(self): class TestDoscar(unittest.TestCase): def setUp(self): # first for spin polarized version - doscar = os.path.join(test_dir_doscar, "DOSCAR.lobster.spin") - poscar = os.path.join(test_dir_doscar, "POSCAR.lobster.spin_DOS") + doscar = f"{test_dir_doscar}/DOSCAR.lobster.spin" + poscar = f"{test_dir_doscar}/POSCAR.lobster.spin_DOS" # not spin polarized - doscar2 = os.path.join(test_dir_doscar, "DOSCAR.lobster.nonspin") - poscar2 = os.path.join(test_dir_doscar, "POSCAR.lobster.nonspin_DOS") - os.path.join(test_dir_doscar, "DOSCAR.lobster.nonspin_zip.gz") - os.path.join(test_dir_doscar, "POSCAR.lobster.nonspin_DOS_zip.gz") + doscar2 = f"{test_dir_doscar}/DOSCAR.lobster.nonspin" + poscar2 = f"{test_dir_doscar}/POSCAR.lobster.nonspin_DOS" + f"{test_dir_doscar}/DOSCAR.lobster.nonspin_zip.gz" + f"{test_dir_doscar}/POSCAR.lobster.nonspin_DOS_zip.gz" self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) - with open(os.path.join(test_dir_doscar, "structure_KF.json")) as f: + with open(f"{test_dir_doscar}/structure_KF.json") as f: data = json.load(f) self.structure = Structure.from_dict(data) @@ -1613,7 +1613,7 @@ def test_initialize_from_dict(self): lobsterin2 = Lobsterin({"cohpstartenergy": -15.0}) # can only calculate nbands if basis functions are provided with pytest.raises(IOError, match="No basis functions are provided. The program cannot calculate nbands"): - lobsterin2._get_nbands(structure=Structure.from_file(os.path.join(test_dir_doscar, "POSCAR.Fe3O4"))) + lobsterin2._get_nbands(structure=Structure.from_file(f"{test_dir_doscar}/POSCAR.Fe3O4")) def test_standard_settings(self): # test standard settings @@ -1630,9 +1630,9 @@ def test_standard_settings(self): "onlycohpcoopcobi", ]: lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( - os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), - os.path.join(test_dir_doscar, "INCAR.lobster"), - os.path.join(test_dir_doscar, "POTCAR.Fe3O4"), + f"{test_dir_doscar}/POSCAR.Fe3O4", + f"{test_dir_doscar}/INCAR.lobster", + f"{test_dir_doscar}/POTCAR.Fe3O4", option=option, ) assert lobsterin1["cohpstartenergy"] == approx(-35.0) @@ -1707,8 +1707,8 @@ def test_standard_settings(self): assert lobsterin1["skipdos"], True # test basis functions by dict lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( - os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), - os.path.join(test_dir_doscar, "INCAR.lobster"), + f"{test_dir_doscar}/POSCAR.Fe3O4", + f"{test_dir_doscar}/INCAR.lobster", dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, option="standard", ) @@ -1716,8 +1716,8 @@ def test_standard_settings(self): # test gaussian smearing lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( - os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), - os.path.join(test_dir_doscar, "INCAR.lobster2"), + f"{test_dir_doscar}/POSCAR.Fe3O4", + f"{test_dir_doscar}/INCAR.lobster2", dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, option="standard", ) @@ -1726,8 +1726,8 @@ def test_standard_settings(self): # fatband and ISMEAR=-5 does not work together with pytest.raises(ValueError, match="ISMEAR has to be 0 for a fatband calculation with Lobster"): lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( - os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), - os.path.join(test_dir_doscar, "INCAR.lobster2"), + f"{test_dir_doscar}/POSCAR.Fe3O4", + f"{test_dir_doscar}/INCAR.lobster2", dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, option="standard_with_fatband", ) @@ -1735,10 +1735,10 @@ def test_standard_settings(self): def test_standard_with_energy_range_from_vasprun(self): # test standard_with_energy_range_from_vasprun lobsterin_comp = Lobsterin.standard_calculations_from_vasp_files( - os.path.join(test_dir_doscar, "POSCAR.C2.gz"), - os.path.join(test_dir_doscar, "INCAR.C2.gz"), - os.path.join(test_dir_doscar, "POTCAR.C2.gz"), - os.path.join(test_dir_doscar, "vasprun.xml.C2.gz"), + f"{test_dir_doscar}/POSCAR.C2.gz", + f"{test_dir_doscar}/INCAR.C2.gz", + f"{test_dir_doscar}/POTCAR.C2.gz", + f"{test_dir_doscar}/vasprun.xml.C2.gz", option="standard_with_energy_range_from_vasprun", ) assert lobsterin_comp["COHPstartEnergy"] == -28.3679 @@ -1768,11 +1768,11 @@ def test_diff(self): def test_get_basis(self): # get basis functions lobsterin1 = Lobsterin({}) - potcar = Potcar.from_file(os.path.join(test_dir_doscar, "POTCAR.Fe3O4")) + potcar = Potcar.from_file(f"{test_dir_doscar}/POTCAR.Fe3O4") Potcar_names = [name["symbol"] for name in potcar.spec] assert lobsterin1.get_basis( - Structure.from_file(os.path.join(test_dir_doscar, "Fe3O4.cif")), + Structure.from_file(f"{test_dir_doscar}/Fe3O4.cif"), potcar_symbols=Potcar_names, ) == ["Fe 3d 4p 4s ", "O 2p 2s "] potcar = Potcar.from_file(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") @@ -1783,26 +1783,26 @@ def test_get_basis(self): ) == ["Ga 3d 4p 4s ", "As 4p 4s "] def test_get_all_possible_basis_functions(self): - potcar = Potcar.from_file(os.path.join(test_dir_doscar, "POTCAR.Fe3O4")) + potcar = Potcar.from_file(f"{test_dir_doscar}/POTCAR.Fe3O4") Potcar_names = [name["symbol"] for name in potcar.spec] result = Lobsterin.get_all_possible_basis_functions( - Structure.from_file(os.path.join(test_dir_doscar, "Fe3O4.cif")), + Structure.from_file(f"{test_dir_doscar}/Fe3O4.cif"), potcar_symbols=Potcar_names, ) assert result[0] == {"Fe": "3d 4s", "O": "2p 2s"} assert result[1] == {"Fe": "3d 4s 4p", "O": "2p 2s"} - potcar2 = Potcar.from_file(os.path.join(test_dir_doscar, "POT_GGA_PAW_PBE_54/POTCAR.Fe_pv.gz")) + potcar2 = Potcar.from_file(f"{test_dir_doscar}/POT_GGA_PAW_PBE_54/POTCAR.Fe_pv.gz") Potcar_names2 = [name["symbol"] for name in potcar2.spec] result2 = Lobsterin.get_all_possible_basis_functions( - Structure.from_file(os.path.join(test_dir_doscar, "Fe.cif")), + Structure.from_file(f"{test_dir_doscar}/Fe.cif"), potcar_symbols=Potcar_names2, ) assert result2[0] == {"Fe": "3d 3p 4s"} def test_get_potcar_symbols(self): lobsterin1 = Lobsterin({}) - assert lobsterin1._get_potcar_symbols(os.path.join(test_dir_doscar, "POTCAR.Fe3O4")) == ["Fe", "O"] + assert lobsterin1._get_potcar_symbols(f"{test_dir_doscar}/POTCAR.Fe3O4") == ["Fe", "O"] assert lobsterin1._get_potcar_symbols(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") == [ "Ga_d", "As", @@ -1812,9 +1812,9 @@ def test_write_lobsterin(self): # write lobsterin, read it and compare it outfile_path = tempfile.mkstemp()[1] lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( - os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), - os.path.join(test_dir_doscar, "INCAR.lobster"), - os.path.join(test_dir_doscar, "POTCAR.Fe3O4"), + f"{test_dir_doscar}/POSCAR.Fe3O4", + f"{test_dir_doscar}/INCAR.lobster", + f"{test_dir_doscar}/POTCAR.Fe3O4", option="standard", ) lobsterin1.write_lobsterin(outfile_path) @@ -1825,18 +1825,18 @@ def test_write_INCAR(self): # write INCAR and compare outfile_path = tempfile.mkstemp()[1] lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( - os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), - os.path.join(test_dir_doscar, "INCAR.lobster"), - os.path.join(test_dir_doscar, "POTCAR.Fe3O4"), + f"{test_dir_doscar}/POSCAR.Fe3O4", + f"{test_dir_doscar}/INCAR.lobster", + f"{test_dir_doscar}/POTCAR.Fe3O4", option="standard", ) lobsterin1.write_INCAR( - os.path.join(test_dir_doscar, "INCAR.lobster3"), + f"{test_dir_doscar}/INCAR.lobster3", outfile_path, - os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), + f"{test_dir_doscar}/POSCAR.Fe3O4", ) - incar1 = Incar.from_file(os.path.join(test_dir_doscar, "INCAR.lobster3")) + incar1 = Incar.from_file(f"{test_dir_doscar}/INCAR.lobster3") incar2 = Incar.from_file(outfile_path) assert incar1.diff(incar2)["Different"] == { @@ -1853,7 +1853,7 @@ def test_write_KPOINTS(self): lobsterin1 = Lobsterin({}) # test writing primitive cell lobsterin1.write_POSCAR_with_standard_primitive( - POSCAR_input=os.path.join(test_dir_doscar, "POSCAR.Fe3O4"), + POSCAR_input=f"{test_dir_doscar}/POSCAR.Fe3O4", POSCAR_output=outfile_path2, ) @@ -1868,7 +1868,7 @@ def test_write_KPOINTS(self): assert kpoint.kpts[-1][1] == approx(0.5) assert kpoint.kpts[-1][2] == approx(0.5) assert kpoint.labels[-1] == "T" - kpoint2 = Kpoints.from_file(os.path.join(test_dir_doscar, "KPOINTS_band.lobster")) + kpoint2 = Kpoints.from_file(f"{test_dir_doscar}/KPOINTS_band.lobster") labels = [] number = 0 @@ -1898,7 +1898,7 @@ def test_write_KPOINTS(self): # without line mode lobsterin1.write_KPOINTS(POSCAR_input=outfile_path2, KPOINTS_output=outfile_path, line_mode=False) kpoint = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(os.path.join(test_dir_doscar, "IBZKPT.lobster")) + kpoint2 = Kpoints.from_file(f"{test_dir_doscar}/IBZKPT.lobster") for num_kpt, list_kpoint in enumerate(kpoint.kpts): assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) @@ -1916,7 +1916,7 @@ def test_write_KPOINTS(self): input_grid=[6, 6, 3], ) kpoint = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(os.path.join(test_dir_doscar, "IBZKPT.lobster")) + kpoint2 = Kpoints.from_file(f"{test_dir_doscar}/IBZKPT.lobster") for num_kpt, list_kpoint in enumerate(kpoint.kpts): assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) @@ -2354,7 +2354,7 @@ def test_parse_file(self): def test_set_volumetric_data(self): wave1 = Wavefunction( filename=f"{test_dir_doscar}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(os.path.join(test_dir_doscar, "cohp", "POSCAR_O.gz")), + structure=Structure.from_file(f"{test_dir_doscar}/cohp/POSCAR_O.gz"), ) wave1.set_volumetric_data(grid=wave1.grid, structure=wave1.structure) @@ -2368,7 +2368,7 @@ def test_get_volumetricdata_real(self): "cohp", "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", ), - structure=Structure.from_file(os.path.join(test_dir_doscar, "cohp", "POSCAR_O.gz")), + structure=Structure.from_file(f"{test_dir_doscar}/cohp/POSCAR_O.gz"), ) volumetricdata_real = wave1.get_volumetricdata_real() assert volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) @@ -2380,7 +2380,7 @@ def test_get_volumetricdata_imaginary(self): "cohp", "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", ), - structure=Structure.from_file(os.path.join(test_dir_doscar, "cohp", "POSCAR_O.gz")), + structure=Structure.from_file(f"{test_dir_doscar}/cohp/POSCAR_O.gz"), ) volumetricdata_imaginary = wave1.get_volumetricdata_imaginary() assert volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) @@ -2392,7 +2392,7 @@ def test_get_volumetricdata_density(self): "cohp", "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", ), - structure=Structure.from_file(os.path.join(test_dir_doscar, "cohp", "POSCAR_O.gz")), + structure=Structure.from_file(f"{test_dir_doscar}/cohp/POSCAR_O.gz"), ) volumetricdata_density = wave1.get_volumetricdata_density() assert volumetricdata_density.data["total"][0, 0, 0] == approx((-3.0966 * -3.0966) + (-6.45895 * -6.45895)) @@ -2404,7 +2404,7 @@ def test_write_file(self): "cohp", "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", ), - structure=Structure.from_file(os.path.join(test_dir_doscar, "cohp", "POSCAR_O.gz")), + structure=Structure.from_file(f"{test_dir_doscar}/cohp/POSCAR_O.gz"), ) wave1.write_file(filename=os.path.join("wavecar_test.vasp"), part="real") assert os.path.isfile("wavecar_test.vasp") @@ -2435,9 +2435,7 @@ def test_attributes(self): assert self.sitepotential.ewald_splitting == approx(3.14) def test_get_structure(self): - structure = self.sitepotential.get_structure_with_site_potentials( - os.path.join(test_dir_doscar, "cohp", "POSCAR.perovskite") - ) + structure = self.sitepotential.get_structure_with_site_potentials(f"{test_dir_doscar}/cohp/POSCAR.perovskite") assert structure.site_properties["Loewdin Site Potentials (eV)"] == [-8.77, -17.08, 9.57, 9.57, 8.45] assert structure.site_properties["Mulliken Site Potentials (eV)"] == [-11.38, -19.62, 11.18, 11.18, 10.09] diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index ef9814eca5..c2c866603d 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -32,231 +32,231 @@ def setUp(self): self.chemenvlobster1 = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), additional_condition=1, ) # all bonds self.chemenvlobster0 = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), additional_condition=0, ) # only cation cation, anion anion bonds self.chemenvlobster5 = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), additional_condition=5, ) # only cation cation bonds self.chemenvlobster6 = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), additional_condition=6, ) # 2,3,4 are not tested so far self.chemenvlobster2 = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), additional_condition=2, ) self.chemenvlobster3 = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), additional_condition=3, ) self.chemenvlobster4 = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), additional_condition=4, ) # search for other testcase where 2,3,4 arrive at different results self.chemenvlobster0_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), additional_condition=0, ) self.chemenvlobster1_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), additional_condition=1, ) self.chemenvlobster2_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), additional_condition=2, ) self.chemenvlobster5_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), additional_condition=5, ) self.chemenvlobster5_second_percentage = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), additional_condition=5, perc_strength_ICOHP=1.0, ) self.chemenvlobster6_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), additional_condition=6, ) # coop / cobi self.chemenvlobster1_coop_NaCl = LobsterNeighbors( are_coops=True, - filename_ICOHP=os.path.join(test_dir_env, "ICOOPLIST.lobster.NaCl.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + filename_ICOHP=f"{test_dir_env}/ICOOPLIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), additional_condition=1, noise_cutoff=None, ) self.chemenvlobster1_cobi_NaCl = LobsterNeighbors( are_coops=True, - filename_ICOHP=os.path.join(test_dir_env, "ICOBILIST.lobster.NaCl.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + filename_ICOHP=f"{test_dir_env}/ICOBILIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), additional_condition=1, noise_cutoff=None, ) self.chemenvlobster1_cobi_mp470 = LobsterNeighbors( are_coops=True, - filename_ICOHP=os.path.join(test_dir_env, "ICOBILIST.lobster.mp_470.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_470.gz")), + filename_ICOHP=f"{test_dir_env}/ICOBILIST.lobster.mp_470.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_470.gz"), additional_condition=1, ) # TODO: use charge instead of valence self.chemenvlobster1_charges = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", additional_condition=1, ) self.chemenvlobster1_charges_noisecutoff = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_632319.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_632319.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_632319.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_632319.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp_632319.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp_632319.gz", additional_condition=1, perc_strength_ICOHP=0.05, noise_cutoff=0.1, ) self.chemenvlobster1_charges_wo_noisecutoff = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_632319.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_632319.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_632319.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_632319.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp_632319.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp_632319.gz", additional_condition=1, perc_strength_ICOHP=0.05, noise_cutoff=None, ) self.chemenvlobster1_charges_loewdin = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", additional_condition=1, which_charge="Loewdin", ) self.chemenvlobster6_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", additional_condition=6, adapt_extremum_to_add_cond=True, ) self.chemenvlobster5_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", additional_condition=5, adapt_extremum_to_add_cond=True, ) self.chemenvlobster4_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", additional_condition=4, adapt_extremum_to_add_cond=True, ) self.chemenvlobster3_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", additional_condition=3, adapt_extremum_to_add_cond=True, ) self.chemenvlobster2_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", additional_condition=2, adapt_extremum_to_add_cond=True, ) self.chemenvlobster1_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", additional_condition=1, adapt_extremum_to_add_cond=True, ) self.chemenvlobster0_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", additional_condition=0, adapt_extremum_to_add_cond=True, ) self.chemenvlobster0_NaSi = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.NaSi.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaSi.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.NaSi.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaSi.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.NaSi.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.NaSi.gz", additional_condition=0, adapt_extremum_to_add_cond=True, ) @@ -267,8 +267,8 @@ def test_cation_anion_mode_without_ions(self): ): _ = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "../ICOHPLIST.lobster"), - structure=Structure.from_file(os.path.join(test_dir_env, "../POSCAR")), + filename_ICOHP=f"{test_dir_env}/../ICOHPLIST.lobster", + structure=Structure.from_file(f"{test_dir_env}/../POSCAR"), valences_from_charges=False, additional_condition=1, ) @@ -277,8 +277,8 @@ def test_cation_anion_mode_without_ions(self): ): _ = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "../ICOHPLIST.lobster"), - structure=Structure.from_file(os.path.join(test_dir_env, "../POSCAR")), + filename_ICOHP=f"{test_dir_env}/../ICOHPLIST.lobster", + structure=Structure.from_file(f"{test_dir_env}/../POSCAR"), valences_from_charges=False, additional_condition=1, valences=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], @@ -290,20 +290,20 @@ def test_wrong_additional_correction(self): ): LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", additional_condition=10, ) def test_set_limits(self): test = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", additional_condition=1, limits=[-100000, 0], ) @@ -330,7 +330,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster0.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=0, ) ) @@ -339,7 +339,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster0.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=1, ) ) @@ -349,7 +349,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster1.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=0, ) ) @@ -358,7 +358,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster1.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=1, ) ) @@ -386,7 +386,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster2.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=0, ) ) @@ -395,7 +395,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster2.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=1, ) ) @@ -405,7 +405,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster3.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=0, ) ) @@ -414,7 +414,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster3.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=1, ) ) @@ -424,7 +424,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster4.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=0, ) ) @@ -433,7 +433,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster4.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=1, ) ) @@ -443,7 +443,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster5.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=0, ) ) @@ -452,7 +452,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster5.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=1, ) ) @@ -462,7 +462,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster6.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=0, ) ) @@ -472,7 +472,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster6.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), n=1, ) ) @@ -485,7 +485,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster0_second.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), n=0, ) ) @@ -496,7 +496,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster1_second.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), n=0, ) ) @@ -506,7 +506,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster1_coop_NaCl.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), n=0, ) ) @@ -516,7 +516,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster1_cobi_NaCl.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), n=0, ) ) @@ -526,7 +526,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster1_cobi_mp470.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_470.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_470.gz"), n=3, ) ) @@ -537,7 +537,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster2_second.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), n=0, ) ) @@ -546,7 +546,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster2_second.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), n=4, ) ) @@ -557,7 +557,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster5_second.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), n=0, ) ) @@ -566,7 +566,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster5_second.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), n=4, ) ) @@ -576,7 +576,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster6_second.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), n=0, ) ) @@ -585,7 +585,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster6_second.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), n=4, ) ) @@ -595,7 +595,7 @@ def test_get_nn_info(self): assert ( len( self.chemenvlobster5_second_percentage.get_nn( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), n=0, ) ) @@ -604,26 +604,26 @@ def test_get_nn_info(self): def test_structure_graph(self): sg = self.chemenvlobster1_second.get_bonded_structure( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")) + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz") ) assert isinstance(sg, StructureGraph) def test_extended_structure_graph(self): self.chemenvlobsterNaCl = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.NaCl.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.NaCl.gz"), - filename_blist_sg1=os.path.join(test_dir_env, "ICOBILIST.lobster.NaCl.gz"), - filename_blist_sg2=os.path.join(test_dir_env, "ICOOPLIST.lobster.NaCl.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.NaCl.gz", + filename_blist_sg1=f"{test_dir_env}/ICOBILIST.lobster.NaCl.gz", + filename_blist_sg2=f"{test_dir_env}/ICOOPLIST.lobster.NaCl.gz", add_additional_data_sg=True, id_blist_sg1="icobi", id_blist_sg2="icoop", additional_condition=1, ) sg = self.chemenvlobsterNaCl.get_bonded_structure( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), decorate=True, edge_properties=True, weights=True, @@ -639,12 +639,12 @@ def test_raises_extended_structure_graph(self): with pytest.raises(ValueError, match="Algorithm can only work with ICOOPs, ICOBIs"): self.chemenvlobsterNaCl = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.NaCl.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), valences_from_charges=True, - filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.NaCl.gz"), - filename_blist_sg1=os.path.join(test_dir_env, "ICOBILIST.lobster.NaCl.gz"), - filename_blist_sg2=os.path.join(test_dir_env, "ICOOPLIST.lobster.NaCl.gz"), + filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.NaCl.gz", + filename_blist_sg1=f"{test_dir_env}/ICOBILIST.lobster.NaCl.gz", + filename_blist_sg2=f"{test_dir_env}/ICOOPLIST.lobster.NaCl.gz", add_additional_data_sg=True, id_blist_sg1="icopppp", id_blist_sg2="icoop", @@ -653,7 +653,7 @@ def test_raises_extended_structure_graph(self): def test_order_parameter(self): assert self.chemenvlobster1_second.get_local_order_parameters( - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")), n=0 + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), n=0 )["linear"] == approx(1.0) def test_get_structure_environments(self): @@ -703,8 +703,8 @@ def test_get_sum_icohps_between_neighbors_of_atom(self): chemenv_here = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp-7000.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp-7000.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp-7000.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp-7000.gz"), additional_condition=1, ) assert len(chemenv_here.get_info_icohps_between_neighbors(isites=[0])[4]) == 6 @@ -737,11 +737,11 @@ def test_get_plot_label(self): def test_get_info_cohps_to_neighbors(self): chemenvlobster1 = LobsterNeighbors( are_coops=False, - filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_190_2.gz"), - structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_190.gz")), + filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190_2.gz", + structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), additional_condition=1, ) - cohpcar_lobster_mp_190 = os.path.join(test_dir_env, "COHPCAR.lobster.mp-190.gz") + cohpcar_lobster_mp_190 = f"{test_dir_env}/COHPCAR.lobster.mp-190.gz" plot_label, summed_cohpcar_mp_190 = chemenvlobster1.get_info_cohps_to_neighbors( path_to_COHPCAR=cohpcar_lobster_mp_190, isites=[0], @@ -780,7 +780,7 @@ def test_get_info_cohps_to_neighbors(self): assert summed_cohpcar_mp_190_Te is None plot_label, _summed_cohpcar_NaSi = self.chemenvlobster0_NaSi.get_info_cohps_to_neighbors( - path_to_COHPCAR=os.path.join(test_dir_env, "COHPCAR.lobster.NaSi.gz"), + path_to_COHPCAR=f"{test_dir_env}/COHPCAR.lobster.NaSi.gz", isites=[8], onlycation_isites=False, only_bonds_to=["Na"], @@ -788,7 +788,7 @@ def test_get_info_cohps_to_neighbors(self): assert plot_label == "1 x Na-Si (per bond)" assert ( self.chemenvlobster0_NaSi.get_info_cohps_to_neighbors( - path_to_COHPCAR=os.path.join(test_dir_env, "COHPCAR.lobster.NaSi.gz"), + path_to_COHPCAR=f"{test_dir_env}/COHPCAR.lobster.NaSi.gz", isites=[8], onlycation_isites=False, only_bonds_to=["Si"], From 1f2dd81badca1df5f47bde43d2dbd8289e2bd922 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Thu, 17 Aug 2023 15:35:33 -0700 Subject: [PATCH 007/180] Breaking: Have plot methods return `plt.Axes` object, not `matplotlib` module (#3237) * return plt.Axes instead of plt from plot methods * VoltageProfilePlotter.get_plot and get_chgint_plot * fix > plt.subplot(220 + count) E AttributeError: 'Axes' object has no attribute 'subplot' /home/runner/work/pymatgen/pymatgen/pymatgen/electronic_structure/plotter.py:1082 * fix > ylim = plt.ylim() E AttributeError: 'Axes' object has no attribute 'ylim' /home/runner/work/pymatgen/pymatgen/pymatgen/electronic_structure/plotter.py:3834 * fix > plt.gca().set_xticks(uniq_d) E AttributeError: 'Axes' object has no attribute 'gca' /home/runner/work/pymatgen/pymatgen/pymatgen/electronic_structure/plotter.py:360: * fix > ylim = plt.ylim() E AttributeError: 'Axes' object has no attribute 'ylim' /home/runner/work/pymatgen/pymatgen/pymatgen/electronic_structure/plotter.py:3838: AttributeError * fix mypy and add type hints to RelaxationAnalyzer * fix > ax.xlim((min(relevanty), max(relevanty))) E AttributeError: 'Axes' object has no attribute 'xlim' pymatgen/electronic_structure/plotter.py:3836: AttributeError * fix > plt.gca().set_xticks(uniq_d) E AttributeError: 'Axes' object has no attribute 'gca' /home/runner/work/pymatgen/pymatgen/pymatgen/phonon/plotter.py:266: AttributeError * don't use get_plot return value, use imported plt module directly * improve plot_compare_gs error message on incompatible plotters * update SpectrumPlotter.get_plot and pretty_polyfit_plot * fix > ax.setp(legend_text, fontsize=30) E AttributeError: 'Axes' object has no attribute 'setp' pymatgen/electronic_structure/plotter.py:3875: AttributeError * return plt.axes instead of plt in SurfaceEnergyPlotter.surface_chempot_range_map and get_ir_spectra * fix PhononDosPlotter.get_plot() * refactor tests * update test_gruneisen.py and electronic_structure/test_plotter.py to expect plt.axes instead of plt * fix SpectrumPlotter.get_plot and DosPlotter.get_plot * remove redundant os.path.join(".", ...) in pymatgen/io/feff/sets.py * replace all plt = pretty_plot(...) with ax = pretty_plot(...) and fix downstream repercussions * add missing set_ prefix to Axes methods * fix AttributeError: 'Axes' object has no attribute 'gca' def test_plot(self): # Disabling latex for testing. from matplotlib import axes, rc rc("text", usetex=False) self.plotter.add_dos("Total", self.dos) self.plotter.get_plot(units="mev") self.plotter_nostack.add_dos("Total", self.dos) plt = self.plotter_nostack.get_plot(units="mev") > ax = plt.gca() also attempt fix get_elt_projected_plots * update return type doc strings and type hints * pre-commit autoupdate && ruff . --fix * rename all subplot vars to ax * fix AttributeError: 'Axes' object has no attribute 'ylim' > assert plt.ylim() == (-4.0, 7.6348), "wrong ylim" and > ax.set_title(ax.get_title(), size=width * 4) E AttributeError: 'numpy.ndarray' object has no attribute 'set_title' * fix TestBSPlotter.test_get_plot() --- tests/io/lobster/test_lobsterenv.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index c2c866603d..c6f639c395 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -786,15 +786,13 @@ def test_get_info_cohps_to_neighbors(self): only_bonds_to=["Na"], ) assert plot_label == "1 x Na-Si (per bond)" - assert ( - self.chemenvlobster0_NaSi.get_info_cohps_to_neighbors( - path_to_COHPCAR=f"{test_dir_env}/COHPCAR.lobster.NaSi.gz", - isites=[8], - onlycation_isites=False, - only_bonds_to=["Si"], - )[0] - == "3 x Si-Si (per bond)" - ) + info = self.chemenvlobster0_NaSi.get_info_cohps_to_neighbors( + path_to_COHPCAR=f"{test_dir_env}/COHPCAR.lobster.NaSi.gz", + isites=[8], + onlycation_isites=False, + only_bonds_to=["Si"], + )[0] + assert info == "3 x Si-Si (per bond)" chemenvlobster1.plot_cohps_of_neighbors( path_to_COHPCAR=cohpcar_lobster_mp_190, From bb3007d8d8b90226b8a56cb6228e6f493d528374 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Fri, 18 Aug 2023 12:03:40 -0700 Subject: [PATCH 008/180] Use `numpy.testing.assert_allclose` over `assert np.allclose` (#3253) * fix return type doc str indentation * don't use pytest.approx() with assert len(...) * use numpy.testing.assert_allclose over assert np.allclose * np.allclose(np.array(...), ...) to np.allclose(..., ...) * fix mypy * replace assert_array_almost_equal with numpy-recommended assert_allclose * replace np.testing.assert_almost_equal with numpy-recommended assert_allclose * breaking snake_case abinit float_decimal kwarg * delete PymatgenTest.assert_all_close, always use numpy.testing.assert_allclose directly * manual increase assert_allclose atol * more manual increase assert_allclose atol * fix last 3 missed missing atols --- tests/io/lobster/test_lobster.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/io/lobster/test_lobster.py b/tests/io/lobster/test_lobster.py index dde95c7d7c..c949f553be 100644 --- a/tests/io/lobster/test_lobster.py +++ b/tests/io/lobster/test_lobster.py @@ -8,7 +8,7 @@ import numpy as np import pytest -from numpy.testing import assert_array_equal +from numpy.testing import assert_allclose, assert_array_equal from pytest import approx from pymatgen.core.structure import Structure @@ -364,12 +364,12 @@ def test_orbital_resolved_cohp(self): [self.orb.orb_res_cohp["1"][orbs]["COHP"][Spin.up] for orbs in self.orb.orb_res_cohp["1"]], axis=0, ) - assert np.allclose(tot, cohp, atol=1e-3) + assert_allclose(tot, cohp, atol=1e-3) tot = np.sum( [self.orb.orb_res_cohp["1"][orbs]["ICOHP"][Spin.up] for orbs in self.orb.orb_res_cohp["1"]], axis=0, ) - assert np.allclose(tot, icohp, atol=1e-3) + assert_allclose(tot, icohp, atol=1e-3) # Lobster 3.1 cohp_KF = self.cohp_KF.cohp_data["1"]["COHP"][Spin.up] @@ -378,12 +378,12 @@ def test_orbital_resolved_cohp(self): [self.cohp_KF.orb_res_cohp["1"][orbs]["COHP"][Spin.up] for orbs in self.cohp_KF.orb_res_cohp["1"]], axis=0, ) - assert np.allclose(tot_KF, cohp_KF, atol=1e-3) + assert_allclose(tot_KF, cohp_KF, atol=1e-3) tot_KF = np.sum( [self.cohp_KF.orb_res_cohp["1"][orbs]["ICOHP"][Spin.up] for orbs in self.cohp_KF.orb_res_cohp["1"]], axis=0, ) - assert np.allclose(tot_KF, icohp_KF, atol=1e-3) + assert_allclose(tot_KF, icohp_KF, atol=1e-3) # d and f orbitals cohp_Na2UO4 = self.cohp_Na2UO4.cohp_data["49"]["COHP"][Spin.up] @@ -395,7 +395,7 @@ def test_orbital_resolved_cohp(self): ], axis=0, ) - assert np.allclose(tot_Na2UO4, cohp_Na2UO4, atol=1e-3) + assert_allclose(tot_Na2UO4, cohp_Na2UO4, atol=1e-3) tot_Na2UO4 = np.sum( [ self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["ICOHP"][Spin.up] @@ -403,7 +403,7 @@ def test_orbital_resolved_cohp(self): ], axis=0, ) - assert np.allclose(tot_Na2UO4, icohp_Na2UO4, atol=1e-3) + assert_allclose(tot_Na2UO4, icohp_Na2UO4, atol=1e-3) class TestIcohplist(unittest.TestCase): @@ -716,11 +716,11 @@ def test_complete_dos(self): assert tdos_down == self.DOSCAR_spin_pol.completedos.densities[Spin.down].tolist() assert fermi == approx(self.DOSCAR_spin_pol.completedos.efermi) - assert np.allclose( + assert_allclose( self.DOSCAR_spin_pol.completedos.structure.frac_coords, self.structure.frac_coords, ) - assert np.allclose( + assert_allclose( self.DOSCAR_spin_pol2.completedos.structure.frac_coords, self.structure.frac_coords, ) @@ -2273,7 +2273,7 @@ def test_structure_with_grosspop(self): } new_structure = self.grosspop1.get_structure_with_total_grosspop(f"{TEST_FILES_DIR}/cohp/POSCAR.SiO2") - assert np.allclose(new_structure.frac_coords, Structure.from_dict(struct_dict).frac_coords) + assert_allclose(new_structure.frac_coords, Structure.from_dict(struct_dict).frac_coords) class TestUtils(PymatgenTest): From 6df80a65c9163488c6bd3646984661a86f2046ed Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sat, 19 Aug 2023 08:21:16 -0700 Subject: [PATCH 009/180] Don't let tests pollute the `pymatgen` repo (#3255) * delete unused tests/files/chemenv/images/detailed_voronoi_container_coordination_numbers_1.png * mv tests/files/chemenv/{json_test_files,json} mv tests/files/chemenv/structure_environments{_files,} * remove all shutil.rmtree from tests and refactor tests to not pollute git directory if necessary * pmg test_entrypoint run in tmp_path * refactor TestVampireCaller * delete all def setUp(self): warnings.simplefilter("ignore") and corresponding def tearDown(self): warnings.simplefilter("default") * delete questionable def tearDown(self): del self.attrs example: def tearDown(self): del self.ethylene del self.butadiene del self.cyclohexene seems pointless, class is garbage-collected anyway * remove all unused unittest.main() if __name__ == "__main__": import unittest unittest.main() * refactor * fix vestiges of old dir names * remove unused module-level var json_files_dir --- tests/io/lobster/test_lobster.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/io/lobster/test_lobster.py b/tests/io/lobster/test_lobster.py index c949f553be..a81a887168 100644 --- a/tests/io/lobster/test_lobster.py +++ b/tests/io/lobster/test_lobster.py @@ -4,7 +4,6 @@ import os import tempfile import unittest -import warnings import numpy as np import pytest @@ -901,7 +900,6 @@ def test_get_structure_with_charges(self): class TestLobsterout(PymatgenTest): def setUp(self): - warnings.simplefilter("ignore") self.lobsterout_normal = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.normal") # make sure .gz files are also read correctly self.lobsterout_normal = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.normal2.gz") @@ -926,9 +924,6 @@ def setUp(self): filename=f"{TEST_FILES_DIR}/cohp/lobsterout.skip_cobi_madelung" ) - def tearDown(self): - warnings.simplefilter("default") - def testattributes(self): assert self.lobsterout_normal.basis_functions == [ [ @@ -1349,7 +1344,6 @@ def test_get_doc(self): class TestFatband(PymatgenTest): def setUp(self): - warnings.simplefilter("ignore") self.fatband_SiO2_p_x = Fatband( filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x", Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", @@ -1382,9 +1376,6 @@ def setUp(self): ) self.bs_symmline_spin = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) - def tearDown(self): - warnings.simplefilter("default") - def test_attributes(self): assert list(self.fatband_SiO2_p_x.label_dict["M"]) == approx([0.5, 0.0, 0.0]) assert self.fatband_SiO2_p_x.efermi == self.vasprun_SiO2_p_x.efermi @@ -1553,7 +1544,6 @@ def test_get_bandstructure(self): class TestLobsterin(unittest.TestCase): def setUp(self): - warnings.simplefilter("ignore") self.Lobsterinfromfile = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.1") self.Lobsterinfromfile2 = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.2") self.Lobsterinfromfile3 = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.3") @@ -2003,13 +1993,9 @@ def test_MSONable_implementation(self): assert newLobsterin == self.Lobsterinfromfile newLobsterin.to_json() - def tearDown(self): - warnings.simplefilter("default") - class TestBandoverlaps(unittest.TestCase): def setUp(self): - warnings.simplefilter("ignore") # test spin polarlized calc and non spinpolarized calc self.bandoverlaps1 = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.1") @@ -2416,9 +2402,6 @@ def test_write_file(self): assert os.path.isfile("density.vasp") os.remove("density.vasp") - def tearDown(self): - warnings.simplefilter("default") - class TestSitePotentials(PymatgenTest): def setUp(self) -> None: From c25c3ecc01b8521340e631f2b5a6ff505ba81677 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Mon, 21 Aug 2023 10:56:43 -0700 Subject: [PATCH 010/180] Google-style doc string return types (#3258) * remove _files suffix from several tests/files/*_files dirs * delete analysis/solar/slme.py matrix_eigvals() * fix whitespace * mv tests/io/lobster/test_{lobster,inputs}.py update test names in .pytest-split-durations * str concat to f-str * use isinstance(x, Iterable) over hasattr(x, "__iter__") * delete needless bool(...) calls * fix bad f-str conversion * fix typos, use f-strings, remove docs_rst from .gitattributes linguist-vendored * Revert "remove _files suffix from several tests/files/*_files dirs" This reverts commit 3979a07eee890548ebaadbd923dbb98838d32b30. * delete dead code in pymatgen/electronic_structure/boltztrap2.py * google-style doc string return types --- tests/io/lobster/{test_lobster.py => test_inputs.py} | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) rename tests/io/lobster/{test_lobster.py => test_inputs.py} (99%) diff --git a/tests/io/lobster/test_lobster.py b/tests/io/lobster/test_inputs.py similarity index 99% rename from tests/io/lobster/test_lobster.py rename to tests/io/lobster/test_inputs.py index a81a887168..9fbccdceb4 100644 --- a/tests/io/lobster/test_lobster.py +++ b/tests/io/lobster/test_inputs.py @@ -1793,10 +1793,7 @@ def test_get_all_possible_basis_functions(self): def test_get_potcar_symbols(self): lobsterin1 = Lobsterin({}) assert lobsterin1._get_potcar_symbols(f"{test_dir_doscar}/POTCAR.Fe3O4") == ["Fe", "O"] - assert lobsterin1._get_potcar_symbols(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") == [ - "Ga_d", - "As", - ] + assert lobsterin1._get_potcar_symbols(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") == ["Ga_d", "As"] def test_write_lobsterin(self): # write lobsterin, read it and compare it @@ -1843,8 +1840,7 @@ def test_write_KPOINTS(self): lobsterin1 = Lobsterin({}) # test writing primitive cell lobsterin1.write_POSCAR_with_standard_primitive( - POSCAR_input=f"{test_dir_doscar}/POSCAR.Fe3O4", - POSCAR_output=outfile_path2, + POSCAR_input=f"{test_dir_doscar}/POSCAR.Fe3O4", POSCAR_output=outfile_path2 ) lobsterin1.write_KPOINTS( From ea138844b9f646edd1b06dd053a110862ddea484 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Fri, 25 Aug 2023 16:19:31 -0700 Subject: [PATCH 011/180] Add `PotcarSingle.__repr__` (#3273) * to f-string * add WIP PotcarSingle.__repr__ * test_repr() * ruff . --fix * delete repeated words --- tests/io/lobster/test_lobsterenv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index c6f639c395..5cb43a82a9 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -45,7 +45,7 @@ def setUp(self): additional_condition=0, ) - # only cation cation, anion anion bonds + # only cation-cation, anion-anion bonds self.chemenvlobster5 = LobsterNeighbors( are_coops=False, filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", @@ -53,7 +53,7 @@ def setUp(self): additional_condition=5, ) - # only cation cation bonds + # only cation-cation bonds self.chemenvlobster6 = LobsterNeighbors( are_coops=False, filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", From dba02a52a99fd2146ce1a1ff202ab9a3288c96e4 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sun, 17 Sep 2023 23:15:58 +0200 Subject: [PATCH 012/180] Snake case test method names (#3339) * refactor SymmOp.are_symmetrically_related * snake_case test method names * remove needless double underscore from private methods * google-style Note: section header in doc strings --- tests/io/lobster/test_inputs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 9fbccdceb4..8e7ff1fdb7 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -1808,7 +1808,7 @@ def test_write_lobsterin(self): lobsterin2 = Lobsterin.from_file(outfile_path) assert lobsterin1.diff(lobsterin2)["Different"] == {} - def test_write_INCAR(self): + def test_write_incar(self): # write INCAR and compare outfile_path = tempfile.mkstemp()[1] lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( @@ -1833,7 +1833,7 @@ def test_write_INCAR(self): "LWAVE": {"INCAR1": False, "INCAR2": True}, } - def test_write_KPOINTS(self): + def test_write_kpoints(self): # line mode outfile_path = tempfile.mkstemp()[1] outfile_path2 = tempfile.mkstemp(prefix="POSCAR")[1] @@ -1983,7 +1983,7 @@ def is_kpoint_in_list(self, kpoint, kpointlist, weight, weightlist) -> bool: found += 1 return found == 1 - def test_MSONable_implementation(self): + def test_msonable_implementation(self): # tests as dict and from dict methods newLobsterin = Lobsterin.from_dict(self.Lobsterinfromfile.as_dict()) assert newLobsterin == self.Lobsterinfromfile From 6d0a688a45285987608246ec3b5d636872fd76ce Mon Sep 17 00:00:00 2001 From: Aaron Kaplan <33381112+esoteric-ephemera@users.noreply.github.com> Date: Fri, 29 Sep 2023 17:03:15 -0700 Subject: [PATCH 013/180] Breaking: New method of POTCAR validation (#3351) * Added two funcs to PMG's PotcarSingle to better check POTCAR meta * updates to gen new meta for potcar validation * use only req'd potcar meta, and not full contents of psctr header * use only req'd potcar meta, and not full contents of psctr header 2/* * Use PSCTR instead of keywords attr to generate meta * Use PSCTR instead of keywords attr to generate meta * First attempt at new POTCAR hashing ready, tests pass * Cleanup and prep for PR * rm tmp.diff * gzip POTCAR_META.json * auto-format vasp/inputs.py * pre-commit auto-fixes * Updating in response to PMG maintainer comments * deduplicate cwd and rename module_dir * make is PotcarSingle.is_valid property * remove newlines from warning * snake_case * rename _str_to_py to _fortran_style_str_to_py_var; update test_identify_potcar to test_is_valid to ensure that PotcarSingle.is_valid is not sensitive to small numeric changes * tweak UnknownPotcarWarning msg * rm POT_GGA_PAW_PBE_54/POTCAR.Fe_broken.gz and try to invalidate POTCAR on the fly * test a 2nd POTCAR in TestPotcarSingle * fix 5 mypy errors in PotcarSingle.__init__ * google doc str * turn PotcarSingle hash methods into properties get_sha256_file_hash, get_potcar_file_hash, get_potcar_hash to sha256_file_hash, potcar_file_hash, potcar_hash * add test_potcar_file_hash and test_sha256_file_hash * no need to have data_stats as class method * remove PotcarSingle.keywords, replaced by PotcarSingle.PSCTR * rename PotcarSingle.PSCTR to PotcarSingle.keywords * test_is_valid use deep copies for each invalidation * reintroduce PotcarSingle.hash and file_hash attrs fixes TestTransformedStructure.test_get_vasp_input > return self.keywords[attr.upper()] E KeyError: 'HASH' (was broken in 983598) * delete alias test_dir_doscar for TEST_FILES_DIR * fix test_get_all_possible_basis_functions * fix TestMPScanRelaxSet.test_write_input polluting git repo on failure * git checkout master tests/files/POT_GGA_PAW_PBE_54/POTCAR.{Fe_pv,O}.gz trying to fix tests * mv pymatgen/io/vasp/{POTCAR_META,potcar_summary_stats}.json.gz * move _fortran_style_str_to_py_var into def is_valid() * rename PotcarSingle.sha256_file_hash to sha256_computed_file_hash * missed a hash_sha256_computed->sha256_computed_file_hash rename * add PotcarSingle property hash_sha256_from_file rename potcar_file_hash to md5_computed_file_hash rename potcar_hash to md5_header_hash * remove old unknown Potcar warning when self.hash_sha256_from_file != self.sha256_computed_file_hash prone to false positives * delete test_potcar_file_hash_warning, use that potcar test file as valid example in test_is_valid instead * remove aliases hash for md5_header_hash and file_hash for md5_computed_file_hash * load pymatgen and VASP hashes only once in module scope * git checkout master tests/files/POT_GGA_PAW_PBE_54/POTCAR.Fe_O.gz * remove vestiges of potcar_hash alias for md5_computed_file_hash --------- Co-authored-by: Aaron Kaplan Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 113 ++++++++++++++++---------------- 1 file changed, 56 insertions(+), 57 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 8e7ff1fdb7..c793cd28b9 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -37,7 +37,6 @@ __email__ = "janine.george@uclouvain.be, esters@uoregon.edu" __date__ = "Dec 10, 2017" -test_dir_doscar = TEST_FILES_DIR this_dir = os.path.dirname(os.path.abspath(__file__)) @@ -673,20 +672,20 @@ def test_values(self): class TestDoscar(unittest.TestCase): def setUp(self): # first for spin polarized version - doscar = f"{test_dir_doscar}/DOSCAR.lobster.spin" - poscar = f"{test_dir_doscar}/POSCAR.lobster.spin_DOS" + doscar = f"{TEST_FILES_DIR}/DOSCAR.lobster.spin" + poscar = f"{TEST_FILES_DIR}/POSCAR.lobster.spin_DOS" # not spin polarized - doscar2 = f"{test_dir_doscar}/DOSCAR.lobster.nonspin" - poscar2 = f"{test_dir_doscar}/POSCAR.lobster.nonspin_DOS" - f"{test_dir_doscar}/DOSCAR.lobster.nonspin_zip.gz" - f"{test_dir_doscar}/POSCAR.lobster.nonspin_DOS_zip.gz" + doscar2 = f"{TEST_FILES_DIR}/DOSCAR.lobster.nonspin" + poscar2 = f"{TEST_FILES_DIR}/POSCAR.lobster.nonspin_DOS" + f"{TEST_FILES_DIR}/DOSCAR.lobster.nonspin_zip.gz" + f"{TEST_FILES_DIR}/POSCAR.lobster.nonspin_DOS_zip.gz" self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) - with open(f"{test_dir_doscar}/structure_KF.json") as f: + with open(f"{TEST_FILES_DIR}/structure_KF.json") as f: data = json.load(f) self.structure = Structure.from_dict(data) @@ -1603,7 +1602,7 @@ def test_initialize_from_dict(self): lobsterin2 = Lobsterin({"cohpstartenergy": -15.0}) # can only calculate nbands if basis functions are provided with pytest.raises(IOError, match="No basis functions are provided. The program cannot calculate nbands"): - lobsterin2._get_nbands(structure=Structure.from_file(f"{test_dir_doscar}/POSCAR.Fe3O4")) + lobsterin2._get_nbands(structure=Structure.from_file(f"{TEST_FILES_DIR}/POSCAR.Fe3O4")) def test_standard_settings(self): # test standard settings @@ -1620,9 +1619,9 @@ def test_standard_settings(self): "onlycohpcoopcobi", ]: lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( - f"{test_dir_doscar}/POSCAR.Fe3O4", - f"{test_dir_doscar}/INCAR.lobster", - f"{test_dir_doscar}/POTCAR.Fe3O4", + f"{TEST_FILES_DIR}/POSCAR.Fe3O4", + f"{TEST_FILES_DIR}/INCAR.lobster", + f"{TEST_FILES_DIR}/POTCAR.Fe3O4", option=option, ) assert lobsterin1["cohpstartenergy"] == approx(-35.0) @@ -1697,8 +1696,8 @@ def test_standard_settings(self): assert lobsterin1["skipdos"], True # test basis functions by dict lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( - f"{test_dir_doscar}/POSCAR.Fe3O4", - f"{test_dir_doscar}/INCAR.lobster", + f"{TEST_FILES_DIR}/POSCAR.Fe3O4", + f"{TEST_FILES_DIR}/INCAR.lobster", dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, option="standard", ) @@ -1706,8 +1705,8 @@ def test_standard_settings(self): # test gaussian smearing lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( - f"{test_dir_doscar}/POSCAR.Fe3O4", - f"{test_dir_doscar}/INCAR.lobster2", + f"{TEST_FILES_DIR}/POSCAR.Fe3O4", + f"{TEST_FILES_DIR}/INCAR.lobster2", dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, option="standard", ) @@ -1716,8 +1715,8 @@ def test_standard_settings(self): # fatband and ISMEAR=-5 does not work together with pytest.raises(ValueError, match="ISMEAR has to be 0 for a fatband calculation with Lobster"): lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( - f"{test_dir_doscar}/POSCAR.Fe3O4", - f"{test_dir_doscar}/INCAR.lobster2", + f"{TEST_FILES_DIR}/POSCAR.Fe3O4", + f"{TEST_FILES_DIR}/INCAR.lobster2", dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, option="standard_with_fatband", ) @@ -1725,10 +1724,10 @@ def test_standard_settings(self): def test_standard_with_energy_range_from_vasprun(self): # test standard_with_energy_range_from_vasprun lobsterin_comp = Lobsterin.standard_calculations_from_vasp_files( - f"{test_dir_doscar}/POSCAR.C2.gz", - f"{test_dir_doscar}/INCAR.C2.gz", - f"{test_dir_doscar}/POTCAR.C2.gz", - f"{test_dir_doscar}/vasprun.xml.C2.gz", + f"{TEST_FILES_DIR}/POSCAR.C2.gz", + f"{TEST_FILES_DIR}/INCAR.C2.gz", + f"{TEST_FILES_DIR}/POTCAR.C2.gz", + f"{TEST_FILES_DIR}/vasprun.xml.C2.gz", option="standard_with_energy_range_from_vasprun", ) assert lobsterin_comp["COHPstartEnergy"] == -28.3679 @@ -1758,11 +1757,11 @@ def test_diff(self): def test_get_basis(self): # get basis functions lobsterin1 = Lobsterin({}) - potcar = Potcar.from_file(f"{test_dir_doscar}/POTCAR.Fe3O4") + potcar = Potcar.from_file(f"{TEST_FILES_DIR}/POTCAR.Fe3O4") Potcar_names = [name["symbol"] for name in potcar.spec] assert lobsterin1.get_basis( - Structure.from_file(f"{test_dir_doscar}/Fe3O4.cif"), + Structure.from_file(f"{TEST_FILES_DIR}/Fe3O4.cif"), potcar_symbols=Potcar_names, ) == ["Fe 3d 4p 4s ", "O 2p 2s "] potcar = Potcar.from_file(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") @@ -1773,35 +1772,35 @@ def test_get_basis(self): ) == ["Ga 3d 4p 4s ", "As 4p 4s "] def test_get_all_possible_basis_functions(self): - potcar = Potcar.from_file(f"{test_dir_doscar}/POTCAR.Fe3O4") + potcar = Potcar.from_file(f"{TEST_FILES_DIR}/POTCAR.Fe3O4") Potcar_names = [name["symbol"] for name in potcar.spec] result = Lobsterin.get_all_possible_basis_functions( - Structure.from_file(f"{test_dir_doscar}/Fe3O4.cif"), + Structure.from_file(f"{TEST_FILES_DIR}/Fe3O4.cif"), potcar_symbols=Potcar_names, ) assert result[0] == {"Fe": "3d 4s", "O": "2p 2s"} assert result[1] == {"Fe": "3d 4s 4p", "O": "2p 2s"} - potcar2 = Potcar.from_file(f"{test_dir_doscar}/POT_GGA_PAW_PBE_54/POTCAR.Fe_pv.gz") + potcar2 = Potcar.from_file(f"{TEST_FILES_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Fe.gz") Potcar_names2 = [name["symbol"] for name in potcar2.spec] result2 = Lobsterin.get_all_possible_basis_functions( - Structure.from_file(f"{test_dir_doscar}/Fe.cif"), + Structure.from_file(f"{TEST_FILES_DIR}/Fe.cif"), potcar_symbols=Potcar_names2, ) - assert result2[0] == {"Fe": "3d 3p 4s"} + assert result2[0] == {"Fe": "3d 4s"} def test_get_potcar_symbols(self): lobsterin1 = Lobsterin({}) - assert lobsterin1._get_potcar_symbols(f"{test_dir_doscar}/POTCAR.Fe3O4") == ["Fe", "O"] + assert lobsterin1._get_potcar_symbols(f"{TEST_FILES_DIR}/POTCAR.Fe3O4") == ["Fe", "O"] assert lobsterin1._get_potcar_symbols(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") == ["Ga_d", "As"] def test_write_lobsterin(self): # write lobsterin, read it and compare it outfile_path = tempfile.mkstemp()[1] lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( - f"{test_dir_doscar}/POSCAR.Fe3O4", - f"{test_dir_doscar}/INCAR.lobster", - f"{test_dir_doscar}/POTCAR.Fe3O4", + f"{TEST_FILES_DIR}/POSCAR.Fe3O4", + f"{TEST_FILES_DIR}/INCAR.lobster", + f"{TEST_FILES_DIR}/POTCAR.Fe3O4", option="standard", ) lobsterin1.write_lobsterin(outfile_path) @@ -1812,18 +1811,18 @@ def test_write_incar(self): # write INCAR and compare outfile_path = tempfile.mkstemp()[1] lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( - f"{test_dir_doscar}/POSCAR.Fe3O4", - f"{test_dir_doscar}/INCAR.lobster", - f"{test_dir_doscar}/POTCAR.Fe3O4", + f"{TEST_FILES_DIR}/POSCAR.Fe3O4", + f"{TEST_FILES_DIR}/INCAR.lobster", + f"{TEST_FILES_DIR}/POTCAR.Fe3O4", option="standard", ) lobsterin1.write_INCAR( - f"{test_dir_doscar}/INCAR.lobster3", + f"{TEST_FILES_DIR}/INCAR.lobster3", outfile_path, - f"{test_dir_doscar}/POSCAR.Fe3O4", + f"{TEST_FILES_DIR}/POSCAR.Fe3O4", ) - incar1 = Incar.from_file(f"{test_dir_doscar}/INCAR.lobster3") + incar1 = Incar.from_file(f"{TEST_FILES_DIR}/INCAR.lobster3") incar2 = Incar.from_file(outfile_path) assert incar1.diff(incar2)["Different"] == { @@ -1840,7 +1839,7 @@ def test_write_kpoints(self): lobsterin1 = Lobsterin({}) # test writing primitive cell lobsterin1.write_POSCAR_with_standard_primitive( - POSCAR_input=f"{test_dir_doscar}/POSCAR.Fe3O4", POSCAR_output=outfile_path2 + POSCAR_input=f"{TEST_FILES_DIR}/POSCAR.Fe3O4", POSCAR_output=outfile_path2 ) lobsterin1.write_KPOINTS( @@ -1854,7 +1853,7 @@ def test_write_kpoints(self): assert kpoint.kpts[-1][1] == approx(0.5) assert kpoint.kpts[-1][2] == approx(0.5) assert kpoint.labels[-1] == "T" - kpoint2 = Kpoints.from_file(f"{test_dir_doscar}/KPOINTS_band.lobster") + kpoint2 = Kpoints.from_file(f"{TEST_FILES_DIR}/KPOINTS_band.lobster") labels = [] number = 0 @@ -1884,7 +1883,7 @@ def test_write_kpoints(self): # without line mode lobsterin1.write_KPOINTS(POSCAR_input=outfile_path2, KPOINTS_output=outfile_path, line_mode=False) kpoint = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(f"{test_dir_doscar}/IBZKPT.lobster") + kpoint2 = Kpoints.from_file(f"{TEST_FILES_DIR}/IBZKPT.lobster") for num_kpt, list_kpoint in enumerate(kpoint.kpts): assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) @@ -1902,7 +1901,7 @@ def test_write_kpoints(self): input_grid=[6, 6, 3], ) kpoint = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(f"{test_dir_doscar}/IBZKPT.lobster") + kpoint2 = Kpoints.from_file(f"{TEST_FILES_DIR}/IBZKPT.lobster") for num_kpt, list_kpoint in enumerate(kpoint.kpts): assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) @@ -2317,7 +2316,7 @@ class TestWavefunction(PymatgenTest): def test_parse_file(self): grid, points, real, imaginary, distance = Wavefunction._parse_file( os.path.join( - test_dir_doscar, + TEST_FILES_DIR, "cohp", "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", ) @@ -2335,8 +2334,8 @@ def test_parse_file(self): def test_set_volumetric_data(self): wave1 = Wavefunction( - filename=f"{test_dir_doscar}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{test_dir_doscar}/cohp/POSCAR_O.gz"), + filename=f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), ) wave1.set_volumetric_data(grid=wave1.grid, structure=wave1.structure) @@ -2346,11 +2345,11 @@ def test_set_volumetric_data(self): def test_get_volumetricdata_real(self): wave1 = Wavefunction( filename=os.path.join( - test_dir_doscar, + TEST_FILES_DIR, "cohp", "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", ), - structure=Structure.from_file(f"{test_dir_doscar}/cohp/POSCAR_O.gz"), + structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), ) volumetricdata_real = wave1.get_volumetricdata_real() assert volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) @@ -2358,11 +2357,11 @@ def test_get_volumetricdata_real(self): def test_get_volumetricdata_imaginary(self): wave1 = Wavefunction( filename=os.path.join( - test_dir_doscar, + TEST_FILES_DIR, "cohp", "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", ), - structure=Structure.from_file(f"{test_dir_doscar}/cohp/POSCAR_O.gz"), + structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), ) volumetricdata_imaginary = wave1.get_volumetricdata_imaginary() assert volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) @@ -2370,11 +2369,11 @@ def test_get_volumetricdata_imaginary(self): def test_get_volumetricdata_density(self): wave1 = Wavefunction( filename=os.path.join( - test_dir_doscar, + TEST_FILES_DIR, "cohp", "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", ), - structure=Structure.from_file(f"{test_dir_doscar}/cohp/POSCAR_O.gz"), + structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), ) volumetricdata_density = wave1.get_volumetricdata_density() assert volumetricdata_density.data["total"][0, 0, 0] == approx((-3.0966 * -3.0966) + (-6.45895 * -6.45895)) @@ -2382,11 +2381,11 @@ def test_get_volumetricdata_density(self): def test_write_file(self): wave1 = Wavefunction( filename=os.path.join( - test_dir_doscar, + TEST_FILES_DIR, "cohp", "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", ), - structure=Structure.from_file(f"{test_dir_doscar}/cohp/POSCAR_O.gz"), + structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), ) wave1.write_file(filename=os.path.join("wavecar_test.vasp"), part="real") assert os.path.isfile("wavecar_test.vasp") @@ -2401,7 +2400,7 @@ def test_write_file(self): class TestSitePotentials(PymatgenTest): def setUp(self) -> None: - self.sitepotential = SitePotential(filename=f"{test_dir_doscar}/cohp/SitePotentials.lobster.perovskite") + self.sitepotential = SitePotential(filename=f"{TEST_FILES_DIR}/cohp/SitePotentials.lobster.perovskite") def test_attributes(self): assert self.sitepotential.sitepotentials_Loewdin == [-8.77, -17.08, 9.57, 9.57, 8.45] @@ -2414,14 +2413,14 @@ def test_attributes(self): assert self.sitepotential.ewald_splitting == approx(3.14) def test_get_structure(self): - structure = self.sitepotential.get_structure_with_site_potentials(f"{test_dir_doscar}/cohp/POSCAR.perovskite") + structure = self.sitepotential.get_structure_with_site_potentials(f"{TEST_FILES_DIR}/cohp/POSCAR.perovskite") assert structure.site_properties["Loewdin Site Potentials (eV)"] == [-8.77, -17.08, 9.57, 9.57, 8.45] assert structure.site_properties["Mulliken Site Potentials (eV)"] == [-11.38, -19.62, 11.18, 11.18, 10.09] class TestMadelungEnergies(PymatgenTest): def setUp(self) -> None: - self.madelungenergies = MadelungEnergies(filename=f"{test_dir_doscar}/cohp/MadelungEnergies.lobster.perovskite") + self.madelungenergies = MadelungEnergies(filename=f"{TEST_FILES_DIR}/cohp/MadelungEnergies.lobster.perovskite") def test_attributes(self): assert self.madelungenergies.madelungenergies_Loewdin == approx(-28.64) From 069e9404ff69976dbeb94ded11ac4f363dba4ce0 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Mon, 2 Oct 2023 16:49:53 -0700 Subject: [PATCH 014/180] remove single-arg os.path.join('path') calls --- tests/io/lobster/test_inputs.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index c793cd28b9..dadc1251ea 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -2368,11 +2368,7 @@ def test_get_volumetricdata_imaginary(self): def test_get_volumetricdata_density(self): wave1 = Wavefunction( - filename=os.path.join( - TEST_FILES_DIR, - "cohp", - "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - ), + filename=os.path.join(TEST_FILES_DIR, "cohp", "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz"), structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), ) volumetricdata_density = wave1.get_volumetricdata_density() @@ -2380,20 +2376,16 @@ def test_get_volumetricdata_density(self): def test_write_file(self): wave1 = Wavefunction( - filename=os.path.join( - TEST_FILES_DIR, - "cohp", - "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - ), + filename=f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), ) - wave1.write_file(filename=os.path.join("wavecar_test.vasp"), part="real") + wave1.write_file(filename="wavecar_test.vasp", part="real") assert os.path.isfile("wavecar_test.vasp") - wave1.write_file(filename=os.path.join("wavecar_test.vasp"), part="imaginary") + wave1.write_file(filename="wavecar_test.vasp", part="imaginary") assert os.path.isfile("wavecar_test.vasp") os.remove("wavecar_test.vasp") - wave1.write_file(filename=os.path.join("density.vasp"), part="density") + wave1.write_file(filename="density.vasp", part="density") assert os.path.isfile("density.vasp") os.remove("density.vasp") From 050ee3a66283205e301709931512c960780e9ba2 Mon Sep 17 00:00:00 2001 From: Christina Ertural <52951132+QuantumChemist@users.noreply.github.com> Date: Wed, 4 Oct 2023 21:22:00 +0200 Subject: [PATCH 015/180] New class to handle `NcICOBILIST.lobster` files (#2878) * Implemented a pymatgen class to handle NcICOBILIST.lobster files * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * checking lobster version * pre-commit auto-fixes * improved the tests for ncicobi * pre-commit auto-fixes * cleaned up the merge mess * cleaning up merging mess * get the failing tests to work * cleaning up merging mess * cleaning up merging * adopted PascalCase for ncicobilist class * adopted PascalCase for ncicobilist class * adopted PascalCase for ncicobilist class * captitalized the wrong C * snake_case * use ternary for self.is_spin_polarized * fix Attributes indentation * commented why filename (NcICOBILIST.lobster) = None for LOBSTER version check COBI features were only implemented in LOBSTER 4.1.0 * pre-commit auto-fixes * Update pymatgen/io/lobster/outputs.py Co-authored-by: J. George Signed-off-by: Christina Ertural <52951132+QuantumChemist@users.noreply.github.com> * improved the code * pre-commit auto-fixes * get the linting check working * pre-commit auto-fixes --------- Signed-off-by: Christina Ertural <52951132+QuantumChemist@users.noreply.github.com> Co-authored-by: certural Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Janosh Riebesell Co-authored-by: J. George --- tests/io/lobster/test_inputs.py | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index dadc1251ea..10889d27df 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -23,6 +23,7 @@ Lobsterin, Lobsterout, MadelungEnergies, + NciCobiList, SitePotential, Wavefunction, ) @@ -669,6 +670,44 @@ def test_values(self): assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["orbitals"]["2s-6s"]["icohp"][Spin.up] == 0.0247 +class TestNciCobiList(unittest.TestCase): + def setUp(self): + self.ncicobi = NciCobiList(filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster") + self.ncicobi_gz = NciCobiList(filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster.gz") + self.ncicobi_no_spin = NciCobiList(filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster.nospin") + self.ncicobi_no_spin_wo = NciCobiList( + filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster.nospin.withoutorbitals" + ) + self.ncicobi_wo = NciCobiList(filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster.withoutorbitals") + + def test_ncicobilist(self): + assert self.ncicobi.is_spin_polarized + assert not self.ncicobi_no_spin.is_spin_polarized + assert self.ncicobi_wo.is_spin_polarized + assert not self.ncicobi_no_spin_wo.is_spin_polarized + assert self.ncicobi.orbital_wise + assert self.ncicobi_no_spin.orbital_wise + assert not self.ncicobi_wo.orbital_wise + assert not self.ncicobi_no_spin_wo.orbital_wise + assert len(self.ncicobi.ncicobi_list) == 2 + assert self.ncicobi.ncicobi_list["2"]["number_of_atoms"] == 3 + assert self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.up] == approx(0.00009) + assert self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.down] == approx(0.00009) + assert self.ncicobi.ncicobi_list["2"]["interaction_type"] == "[X22[0,0,0]->Xs42[0,0,0]->X31[0,0,0]]" + assert ( + self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.up] == self.ncicobi_wo.ncicobi_list["2"]["ncicobi"][Spin.up] + ) + assert ( + self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.up] == self.ncicobi_gz.ncicobi_list["2"]["ncicobi"][Spin.up] + ) + assert ( + self.ncicobi.ncicobi_list["2"]["interaction_type"] == self.ncicobi_gz.ncicobi_list["2"]["interaction_type"] + ) + assert sum(self.ncicobi.ncicobi_list["2"]["ncicobi"].values()) == approx( + self.ncicobi_no_spin.ncicobi_list["2"]["ncicobi"][Spin.up] + ) + + class TestDoscar(unittest.TestCase): def setUp(self): # first for spin polarized version From e130da0ba83fd172e5f90d7ec0010b406ee90ef6 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Tue, 10 Oct 2023 10:18:35 -0700 Subject: [PATCH 016/180] Fix `ruff` `N806` (#3394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix typo * snake_case names * release.yml add 📦 Dependencies and 🏷️ Type Hints --- tests/io/lobster/test_inputs.py | 116 ++++++++++++++++---------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 10889d27df..818fcc1d36 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -739,14 +739,14 @@ def test_complete_dos(self): tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] fermi = 0.0 - PDOS_F_2s_up = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] - PDOS_F_2s_down = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] - PDOS_F_2py_up = [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029] - PDOS_F_2py_down = [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029] - PDOS_F_2pz_up = [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029] - PDOS_F_2pz_down = [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029] - PDOS_F_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] - PDOS_F_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] + pdos_f_2s_up = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + pdos_f_2s_down = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + pdos_f_2py_up = [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029] + pdos_f_2py_down = [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029] + pdos_f_2pz_up = [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029] + pdos_f_2pz_down = [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029] + pdos_f_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] + pdos_f_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] assert energies_spin == self.DOSCAR_spin_pol.completedos.energies.tolist() assert tdos_up == self.DOSCAR_spin_pol.completedos.densities[Spin.up].tolist() @@ -761,21 +761,21 @@ def test_complete_dos(self): self.DOSCAR_spin_pol2.completedos.structure.frac_coords, self.structure.frac_coords, ) - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == PDOS_F_2s_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.down].tolist() == PDOS_F_2s_down - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == PDOS_F_2py_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.down].tolist() == PDOS_F_2py_down - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == PDOS_F_2pz_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.down].tolist() == PDOS_F_2pz_down - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == PDOS_F_2px_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.down].tolist() == PDOS_F_2px_down + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == pdos_f_2s_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.down].tolist() == pdos_f_2s_down + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == pdos_f_2py_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.down].tolist() == pdos_f_2py_down + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == pdos_f_2pz_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.down].tolist() == pdos_f_2pz_down + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == pdos_f_2px_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.down].tolist() == pdos_f_2px_down energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] - PDOS_F_2s = [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060] - PDOS_F_2py = [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037] - PDOS_F_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] - PDOS_F_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] + pdos_f_2s = [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060] + pdos_f_2py = [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037] + pdos_f_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] + pdos_f_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] assert energies_nonspin == self.DOSCAR_nonspin_pol.completedos.energies.tolist() @@ -785,42 +785,42 @@ def test_complete_dos(self): assert self.DOSCAR_nonspin_pol.completedos.structure == self.structure - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == PDOS_F_2s - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == PDOS_F_2py - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == PDOS_F_2pz - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == PDOS_F_2px + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == pdos_f_2s + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == pdos_f_2py + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == pdos_f_2pz + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == pdos_f_2px def test_pdos(self): # first for spin polarized version - PDOS_F_2s_up = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] - PDOS_F_2s_down = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] - PDOS_F_2py_up = [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029] - PDOS_F_2py_down = [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029] - PDOS_F_2pz_up = [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029] - PDOS_F_2pz_down = [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029] - PDOS_F_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] - PDOS_F_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] - - assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.up].tolist() == PDOS_F_2s_up - assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.down].tolist() == PDOS_F_2s_down - assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.up].tolist() == PDOS_F_2py_up - assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.down].tolist() == PDOS_F_2py_down - assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.up].tolist() == PDOS_F_2pz_up - assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.down].tolist() == PDOS_F_2pz_down - assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.up].tolist() == PDOS_F_2px_up - assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.down].tolist() == PDOS_F_2px_down + pdos_f_2s_up = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + pdos_f_2s_down = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + pdos_f_2py_up = [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029] + pdos_f_2py_down = [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029] + pdos_f_2pz_up = [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029] + pdos_f_2pz_down = [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029] + pdos_f_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] + pdos_f_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] + + assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.up].tolist() == pdos_f_2s_up + assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.down].tolist() == pdos_f_2s_down + assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.up].tolist() == pdos_f_2py_up + assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.down].tolist() == pdos_f_2py_down + assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.up].tolist() == pdos_f_2pz_up + assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.down].tolist() == pdos_f_2pz_down + assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.up].tolist() == pdos_f_2px_up + assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.down].tolist() == pdos_f_2px_down # non spin - PDOS_F_2s = [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060] - PDOS_F_2py = [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037] - PDOS_F_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] - PDOS_F_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] + pdos_f_2s = [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060] + pdos_f_2py = [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037] + pdos_f_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] + pdos_f_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] - assert self.DOSCAR_nonspin_pol.pdos[0]["2s"][Spin.up].tolist() == PDOS_F_2s - assert self.DOSCAR_nonspin_pol.pdos[0]["2p_y"][Spin.up].tolist() == PDOS_F_2py - assert self.DOSCAR_nonspin_pol.pdos[0]["2p_z"][Spin.up].tolist() == PDOS_F_2pz - assert self.DOSCAR_nonspin_pol.pdos[0]["2p_x"][Spin.up].tolist() == PDOS_F_2px + assert self.DOSCAR_nonspin_pol.pdos[0]["2s"][Spin.up].tolist() == pdos_f_2s + assert self.DOSCAR_nonspin_pol.pdos[0]["2p_y"][Spin.up].tolist() == pdos_f_2py + assert self.DOSCAR_nonspin_pol.pdos[0]["2p_z"][Spin.up].tolist() == pdos_f_2pz + assert self.DOSCAR_nonspin_pol.pdos[0]["2p_x"][Spin.up].tolist() == pdos_f_2px def test_tdos(self): # first for spin polarized version @@ -1797,25 +1797,25 @@ def test_get_basis(self): # get basis functions lobsterin1 = Lobsterin({}) potcar = Potcar.from_file(f"{TEST_FILES_DIR}/POTCAR.Fe3O4") - Potcar_names = [name["symbol"] for name in potcar.spec] + potcar_names = [name["symbol"] for name in potcar.spec] assert lobsterin1.get_basis( Structure.from_file(f"{TEST_FILES_DIR}/Fe3O4.cif"), - potcar_symbols=Potcar_names, + potcar_symbols=potcar_names, ) == ["Fe 3d 4p 4s ", "O 2p 2s "] potcar = Potcar.from_file(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") - Potcar_names = [name["symbol"] for name in potcar.spec] + potcar_names = [name["symbol"] for name in potcar.spec] assert lobsterin1.get_basis( Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR.GaAs"), - potcar_symbols=Potcar_names, + potcar_symbols=potcar_names, ) == ["Ga 3d 4p 4s ", "As 4p 4s "] def test_get_all_possible_basis_functions(self): potcar = Potcar.from_file(f"{TEST_FILES_DIR}/POTCAR.Fe3O4") - Potcar_names = [name["symbol"] for name in potcar.spec] + potcar_names = [name["symbol"] for name in potcar.spec] result = Lobsterin.get_all_possible_basis_functions( Structure.from_file(f"{TEST_FILES_DIR}/Fe3O4.cif"), - potcar_symbols=Potcar_names, + potcar_symbols=potcar_names, ) assert result[0] == {"Fe": "3d 4s", "O": "2p 2s"} assert result[1] == {"Fe": "3d 4s 4p", "O": "2p 2s"} @@ -2023,9 +2023,9 @@ def is_kpoint_in_list(self, kpoint, kpointlist, weight, weightlist) -> bool: def test_msonable_implementation(self): # tests as dict and from dict methods - newLobsterin = Lobsterin.from_dict(self.Lobsterinfromfile.as_dict()) - assert newLobsterin == self.Lobsterinfromfile - newLobsterin.to_json() + new_lobsterin = Lobsterin.from_dict(self.Lobsterinfromfile.as_dict()) + assert new_lobsterin == self.Lobsterinfromfile + new_lobsterin.to_json() class TestBandoverlaps(unittest.TestCase): From 7087fd44ab197a9d8b629c6e24959062eb3e43a1 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Sat, 14 Oct 2023 16:24:20 +0200 Subject: [PATCH 017/180] Add LobsterMatrices parser to lobster.io.outputs (#3361) * add hamiltonMatrices.lobster parser to lobster.io.outputs * pre-commit auto-fixes * fix ruff error * temp mypy fix * update lobster.io __init__.py : include LobsterMatrices class * rename class and make it general to work for all matrices files of lobster * add tests and small test files * pre-commit auto-fixes * fix class description docstring * snake_case * use pytest.approx in TestLobsterMatrices.test_attributes * format to reduce white space --------- Co-authored-by: anaik Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 248 +++++----------- tests/io/lobster/test_lobsterenv.py | 436 ++++++++++++++-------------- 2 files changed, 297 insertions(+), 387 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 818fcc1d36..9c61bdfb23 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -21,6 +21,7 @@ Grosspop, Icohplist, Lobsterin, + LobsterMatrices, Lobsterout, MadelungEnergies, NciCobiList, @@ -39,7 +40,7 @@ __date__ = "Dec 10, 2017" -this_dir = os.path.dirname(os.path.abspath(__file__)) +module_dir = os.path.dirname(os.path.abspath(__file__)) class TestCohpcar(PymatgenTest): @@ -209,150 +210,29 @@ def test_orbital_resolved_cohp(self): assert (orb_set[0][1], orb_set[1][1]) in orbitals # test d and f orbitals - comparelist = [ - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 5, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 6, - 7, - 7, - 7, - 7, - ] - comparelist2 = [ - "f0", - "f0", - "f0", - "f0", - "f1", - "f1", - "f1", - "f1", - "f2", - "f2", - "f2", - "f2", - "f3", - "f3", - "f3", - "f3", - "f_1", - "f_1", - "f_1", - "f_1", - "f_2", - "f_2", - "f_2", - "f_2", - "f_3", - "f_3", - "f_3", - "f_3", - "dx2", - "dx2", - "dx2", - "dx2", - "dxy", - "dxy", - "dxy", - "dxy", - "dxz", - "dxz", - "dxz", - "dxz", - "dyz", - "dyz", - "dyz", - "dyz", - "dz2", - "dz2", - "dz2", - "dz2", - "px", - "px", - "px", - "px", - "py", - "py", - "py", - "py", - "pz", - "pz", - "pz", - "pz", - "s", - "s", - "s", - "s", - "s", - "s", - "s", - "s", + ref_list1 = [*[5] * 28, *[6] * 36, *[7] * 4] + ref_list2 = [ + *["f0"] * 4, + *["f1"] * 4, + *["f2"] * 4, + *["f3"] * 4, + *["f_1"] * 4, + *["f_2"] * 4, + *["f_3"] * 4, + *["dx2"] * 4, + *["dxy"] * 4, + *["dxz"] * 4, + *["dyz"] * 4, + *["dz2"] * 4, + *["px"] * 4, + *["py"] * 4, + *["pz"] * 4, + *["s"] * 8, ] for iorb, orbs in enumerate(sorted(self.cohp_Na2UO4.orb_res_cohp["49"])): orb_set = self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["orbitals"] - assert orb_set[0][0] == comparelist[iorb] - assert str(orb_set[0][1]) == comparelist2[iorb] + assert orb_set[0][0] == ref_list1[iorb] + assert str(orb_set[0][1]) == ref_list2[iorb] # The sum of the orbital-resolved COHPs should be approximately # the total COHP. Due to small deviations in the LOBSTER calculation, @@ -884,7 +764,7 @@ def setUp(self): # gzipped file self.charge = Charge(filename=f"{TEST_FILES_DIR}/cohp/CHARGE.lobster.MnO2.gz") - def testattributes(self): + def test_attributes(self): charge_Loewdin = [-1.25, 1.25] charge_Mulliken = [-1.30, 1.30] atomlist = ["O1", "Mn2"] @@ -962,20 +842,9 @@ def setUp(self): filename=f"{TEST_FILES_DIR}/cohp/lobsterout.skip_cobi_madelung" ) - def testattributes(self): + def test_attributes(self): assert self.lobsterout_normal.basis_functions == [ - [ - "3s", - "4s", - "3p_y", - "3p_z", - "3p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ] + ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] ] assert self.lobsterout_normal.basis_type == ["pbeVaspFit2015"] assert self.lobsterout_normal.charge_spilling == [0.0268] @@ -1017,18 +886,7 @@ def testattributes(self): ] assert self.lobsterout_fatband_grosspop_densityofenergies.basis_functions == [ - [ - "3s", - "4s", - "3p_y", - "3p_z", - "3p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ] + ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] ] assert self.lobsterout_fatband_grosspop_densityofenergies.basis_type == ["pbeVaspFit2015"] assert self.lobsterout_fatband_grosspop_densityofenergies.charge_spilling == [0.0268] @@ -2183,7 +2041,7 @@ class TestGrosspop(unittest.TestCase): def setUp(self): self.grosspop1 = Grosspop(f"{TEST_FILES_DIR}/cohp/GROSSPOP.lobster") - def testattributes(self): + def test_attributes(self): assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3s"] == approx(0.52) assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3p_y"] == approx(0.38) assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3p_z"] == approx(0.37) @@ -2457,3 +2315,55 @@ def test_attributes(self): assert self.madelungenergies.madelungenergies_Loewdin == approx(-28.64) assert self.madelungenergies.madelungenergies_Mulliken == approx(-40.02) assert self.madelungenergies.ewald_splitting == approx(3.14) + + +class TestLobsterMatrices(PymatgenTest): + def setUp(self) -> None: + self.hamilton_matrices = LobsterMatrices( + filename=f"{TEST_FILES_DIR}/cohp/Na_hamiltonMatrices.lobster.gz", e_fermi=-2.79650354 + ) + self.transfer_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/C_transferMatrices.lobster.gz") + self.overlap_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/Si_overlapMatrices.lobster.gz") + self.coeff_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/Si_coefficientMatricesLSO1.lobster.gz") + + def test_attributes(self): + # hamilton matrices + assert self.hamilton_matrices.average_onsite_energies == pytest.approx( + {"Na1_3s": 0.58855353, "Na1_2p_y": -25.72719646, "Na1_2p_z": -25.72719646, "Na1_2p_x": -25.72719646} + ) + ref_onsite_energies = [ + [-0.22519646, -25.76989646, -25.76989646, -25.76989646], + [1.40230354, -25.68449646, -25.68449646, -25.68449646], + ] + assert_allclose(self.hamilton_matrices.onsite_energies, ref_onsite_energies) + + # overlap matrices + assert self.overlap_matrices.average_onsite_overlaps == pytest.approx( + {"Si1_3s": 1.00000009, "Si1_3p_y": 0.99999995, "Si1_3p_z": 0.99999995, "Si1_3p_x": 0.99999995} + ) + ref_onsite_ovelaps = [[1.00000009, 0.99999995, 0.99999995, 0.99999995]] + assert_allclose(self.overlap_matrices.onsite_overlaps, ref_onsite_ovelaps) + + assert len(self.overlap_matrices.overlap_matrices) == 1 + # transfer matrices + ref_onsite_transfer = [ + [0.00357821, -0.13257223, 0.07208898, -0.00196828], + [-1.03655584e00, 4.35405500e-02, -4.86770000e-04, 2.69085640e-01], + ] + assert_allclose(self.transfer_matrices.onsite_transfer, ref_onsite_transfer) + + # coefficient matrices + assert list(self.coeff_matrices.coefficient_matrices["1"]) == [Spin.up, Spin.down] + assert self.coeff_matrices.average_onsite_coefficient == pytest.approx( + {"Si1_3s": -0.12317191, "Si1_3p_y": 0.39037373, "Si1_3p_z": -0.486769934, "Si1_3p_x": 0.1673625} + ) + + def test_raises(self): + with pytest.raises(ValueError, match="Please provide the fermi energy in eV"): + self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/Na_hamiltonMatrices.lobster.gz") + + with pytest.raises( + OSError, + match=r"Please check provided input file, it seems to be empty", + ): + self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/hamiltonMatrices.lobster") diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 5cb43a82a9..a1249070e1 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -21,8 +21,8 @@ __email__ = "janine.george@uclouvain.be" __date__ = "Jan 14, 2021" -test_dir_env = f"{TEST_FILES_DIR}/cohp/environments" -this_dir = os.path.dirname(os.path.abspath(__file__)) +test_dir = f"{TEST_FILES_DIR}/cohp/environments" +module_dir = os.path.dirname(os.path.abspath(__file__)) class TestLobsterNeighbors(unittest.TestCase): @@ -30,233 +30,233 @@ def setUp(self): # test additional conditions first # only consider cation anion bonds - self.chemenvlobster1 = LobsterNeighbors( + self.chem_env_lobster1 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), additional_condition=1, ) # all bonds - self.chemenvlobster0 = LobsterNeighbors( + self.chem_env_lobster0 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), additional_condition=0, ) # only cation-cation, anion-anion bonds - self.chemenvlobster5 = LobsterNeighbors( + self.chem_env_lobster5 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), additional_condition=5, ) # only cation-cation bonds - self.chemenvlobster6 = LobsterNeighbors( + self.chem_env_lobster6 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), additional_condition=6, ) # 2,3,4 are not tested so far - self.chemenvlobster2 = LobsterNeighbors( + self.chem_env_lobster2 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), additional_condition=2, ) - self.chemenvlobster3 = LobsterNeighbors( + self.chem_env_lobster3 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), additional_condition=3, ) - self.chemenvlobster4 = LobsterNeighbors( + self.chem_env_lobster4 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), additional_condition=4, ) # search for other testcase where 2,3,4 arrive at different results - self.chemenvlobster0_second = LobsterNeighbors( + self.chem_env_lobster0_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), additional_condition=0, ) - self.chemenvlobster1_second = LobsterNeighbors( + self.chem_env_lobster1_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), additional_condition=1, ) - self.chemenvlobster2_second = LobsterNeighbors( + self.chem_env_lobster2_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), additional_condition=2, ) - self.chemenvlobster5_second = LobsterNeighbors( + self.chem_env_lobster5_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), additional_condition=5, ) - self.chemenvlobster5_second_percentage = LobsterNeighbors( + self.chem_env_lobster5_second_percentage = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), additional_condition=5, perc_strength_ICOHP=1.0, ) - self.chemenvlobster6_second = LobsterNeighbors( + self.chem_env_lobster6_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), additional_condition=6, ) # coop / cobi - self.chemenvlobster1_coop_NaCl = LobsterNeighbors( + self.chem_env_lobster1_coop_NaCl = LobsterNeighbors( are_coops=True, - filename_ICOHP=f"{test_dir_env}/ICOOPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), + filename_ICOHP=f"{test_dir}/ICOOPLIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), additional_condition=1, noise_cutoff=None, ) - self.chemenvlobster1_cobi_NaCl = LobsterNeighbors( + self.chem_env_lobster1_cobi_NaCl = LobsterNeighbors( are_coops=True, - filename_ICOHP=f"{test_dir_env}/ICOBILIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), + filename_ICOHP=f"{test_dir}/ICOBILIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), additional_condition=1, noise_cutoff=None, ) - self.chemenvlobster1_cobi_mp470 = LobsterNeighbors( + self.chem_env_lobster1_cobi_mp470 = LobsterNeighbors( are_coops=True, - filename_ICOHP=f"{test_dir_env}/ICOBILIST.lobster.mp_470.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_470.gz"), + filename_ICOHP=f"{test_dir}/ICOBILIST.lobster.mp_470.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_470.gz"), additional_condition=1, ) # TODO: use charge instead of valence - self.chemenvlobster1_charges = LobsterNeighbors( + self.chem_env_lobster1_charges = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", additional_condition=1, ) - self.chemenvlobster1_charges_noisecutoff = LobsterNeighbors( + self.chem_env_lobster1_charges_noisecutoff = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_632319.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_632319.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_632319.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_632319.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp_632319.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp_632319.gz", additional_condition=1, perc_strength_ICOHP=0.05, noise_cutoff=0.1, ) - self.chemenvlobster1_charges_wo_noisecutoff = LobsterNeighbors( + self.chem_env_lobster1_charges_wo_noisecutoff = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_632319.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_632319.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_632319.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_632319.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp_632319.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp_632319.gz", additional_condition=1, perc_strength_ICOHP=0.05, noise_cutoff=None, ) - self.chemenvlobster1_charges_loewdin = LobsterNeighbors( + self.chem_env_lobster1_charges_loewdin = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", additional_condition=1, which_charge="Loewdin", ) - self.chemenvlobster6_charges_additional_condition = LobsterNeighbors( + self.chem_env_lobster6_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", additional_condition=6, adapt_extremum_to_add_cond=True, ) - self.chemenvlobster5_charges_additional_condition = LobsterNeighbors( + self.chem_env_lobster5_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", additional_condition=5, adapt_extremum_to_add_cond=True, ) - self.chemenvlobster4_charges_additional_condition = LobsterNeighbors( + self.chem_env_lobster4_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", additional_condition=4, adapt_extremum_to_add_cond=True, ) - self.chemenvlobster3_charges_additional_condition = LobsterNeighbors( + self.chem_env_lobster3_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", additional_condition=3, adapt_extremum_to_add_cond=True, ) - self.chemenvlobster2_charges_additional_condition = LobsterNeighbors( + self.chem_env_lobster2_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", additional_condition=2, adapt_extremum_to_add_cond=True, ) - self.chemenvlobster1_charges_additional_condition = LobsterNeighbors( + self.chem_env_lobster1_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", additional_condition=1, adapt_extremum_to_add_cond=True, ) - self.chemenvlobster0_charges_additional_condition = LobsterNeighbors( + self.chem_env_lobster0_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", additional_condition=0, adapt_extremum_to_add_cond=True, ) - self.chemenvlobster0_NaSi = LobsterNeighbors( + self.chem_env_lobster0_NaSi = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.NaSi.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaSi.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.NaSi.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.NaSi.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.NaSi.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.NaSi.gz", additional_condition=0, adapt_extremum_to_add_cond=True, ) @@ -267,8 +267,8 @@ def test_cation_anion_mode_without_ions(self): ): _ = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/../ICOHPLIST.lobster", - structure=Structure.from_file(f"{test_dir_env}/../POSCAR"), + filename_ICOHP=f"{test_dir}/../ICOHPLIST.lobster", + structure=Structure.from_file(f"{test_dir}/../POSCAR"), valences_from_charges=False, additional_condition=1, ) @@ -277,8 +277,8 @@ def test_cation_anion_mode_without_ions(self): ): _ = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/../ICOHPLIST.lobster", - structure=Structure.from_file(f"{test_dir_env}/../POSCAR"), + filename_ICOHP=f"{test_dir}/../ICOHPLIST.lobster", + structure=Structure.from_file(f"{test_dir}/../POSCAR"), valences_from_charges=False, additional_condition=1, valences=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], @@ -290,31 +290,31 @@ def test_wrong_additional_correction(self): ): LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", additional_condition=10, ) def test_set_limits(self): test = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", additional_condition=1, limits=[-100000, 0], ) assert test.limits == [-100000, 0] def test_molecules_allowed(self): - assert not self.chemenvlobster1.molecules_allowed + assert not self.chem_env_lobster1.molecules_allowed def test_get_anion_types(self): - assert self.chemenvlobster0_second.get_anion_types() == {Element("O")} - assert self.chemenvlobster0_second.anion_types == {Element("O")} + assert self.chem_env_lobster0_second.get_anion_types() == {Element("O")} + assert self.chem_env_lobster0_second.anion_types == {Element("O")} def test_get_nn_info(self): # NO_ADDITIONAL_CONDITION = 0 @@ -329,8 +329,8 @@ def test_get_nn_info(self): # ReO3 assert ( len( - self.chemenvlobster0.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster0.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=0, ) ) @@ -338,8 +338,8 @@ def test_get_nn_info(self): ) assert ( len( - self.chemenvlobster0.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster0.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=1, ) ) @@ -348,8 +348,8 @@ def test_get_nn_info(self): # ONLY_ANION_CATION_BONDS = 1 assert ( len( - self.chemenvlobster1.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster1.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=0, ) ) @@ -357,8 +357,8 @@ def test_get_nn_info(self): ) assert ( len( - self.chemenvlobster1.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster1.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=1, ) ) @@ -366,8 +366,8 @@ def test_get_nn_info(self): ) assert ( len( - self.chemenvlobster1_charges_noisecutoff.get_nn( - structure=self.chemenvlobster1_charges_noisecutoff.structure, + self.chem_env_lobster1_charges_noisecutoff.get_nn( + structure=self.chem_env_lobster1_charges_noisecutoff.structure, n=1, ) ) @@ -375,8 +375,8 @@ def test_get_nn_info(self): ) assert ( len( - self.chemenvlobster1_charges_wo_noisecutoff.get_nn( - structure=self.chemenvlobster1_charges_wo_noisecutoff.structure, + self.chem_env_lobster1_charges_wo_noisecutoff.get_nn( + structure=self.chem_env_lobster1_charges_wo_noisecutoff.structure, n=1, ) ) @@ -385,8 +385,8 @@ def test_get_nn_info(self): # NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2 assert ( len( - self.chemenvlobster2.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster2.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=0, ) ) @@ -394,8 +394,8 @@ def test_get_nn_info(self): ) assert ( len( - self.chemenvlobster2.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster2.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=1, ) ) @@ -404,8 +404,8 @@ def test_get_nn_info(self): # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3 assert ( len( - self.chemenvlobster3.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster3.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=0, ) ) @@ -413,8 +413,8 @@ def test_get_nn_info(self): ) assert ( len( - self.chemenvlobster3.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster3.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=1, ) ) @@ -423,8 +423,8 @@ def test_get_nn_info(self): # ONLY_ELEMENT_TO_OXYGEN_BONDS = 4 assert ( len( - self.chemenvlobster4.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster4.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=0, ) ) @@ -432,8 +432,8 @@ def test_get_nn_info(self): ) assert ( len( - self.chemenvlobster4.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster4.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=1, ) ) @@ -442,8 +442,8 @@ def test_get_nn_info(self): # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 assert ( len( - self.chemenvlobster5.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster5.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=0, ) ) @@ -451,8 +451,8 @@ def test_get_nn_info(self): ) assert ( len( - self.chemenvlobster5.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster5.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=1, ) ) @@ -461,8 +461,8 @@ def test_get_nn_info(self): # ONLY_CATION_CATION_BONDS=6 assert ( len( - self.chemenvlobster6.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster6.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=0, ) ) @@ -471,8 +471,8 @@ def test_get_nn_info(self): assert ( len( - self.chemenvlobster6.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + self.chem_env_lobster6.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), n=1, ) ) @@ -484,8 +484,8 @@ def test_get_nn_info(self): # all bonds assert ( len( - self.chemenvlobster0_second.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + self.chem_env_lobster0_second.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), n=0, ) ) @@ -495,8 +495,8 @@ def test_get_nn_info(self): # ONLY_ANION_CATION_BONDS = 1 assert ( len( - self.chemenvlobster1_second.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + self.chem_env_lobster1_second.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), n=0, ) ) @@ -505,8 +505,8 @@ def test_get_nn_info(self): assert ( len( - self.chemenvlobster1_coop_NaCl.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), + self.chem_env_lobster1_coop_NaCl.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), n=0, ) ) @@ -515,8 +515,8 @@ def test_get_nn_info(self): assert ( len( - self.chemenvlobster1_cobi_NaCl.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), + self.chem_env_lobster1_cobi_NaCl.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), n=0, ) ) @@ -525,8 +525,8 @@ def test_get_nn_info(self): assert ( len( - self.chemenvlobster1_cobi_mp470.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_470.gz"), + self.chem_env_lobster1_cobi_mp470.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_470.gz"), n=3, ) ) @@ -536,8 +536,8 @@ def test_get_nn_info(self): # NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2 assert ( len( - self.chemenvlobster2_second.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + self.chem_env_lobster2_second.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), n=0, ) ) @@ -545,8 +545,8 @@ def test_get_nn_info(self): ) assert ( len( - self.chemenvlobster2_second.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + self.chem_env_lobster2_second.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), n=4, ) ) @@ -556,8 +556,8 @@ def test_get_nn_info(self): # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 assert ( len( - self.chemenvlobster5_second.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + self.chem_env_lobster5_second.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), n=0, ) ) @@ -565,8 +565,8 @@ def test_get_nn_info(self): ) assert ( len( - self.chemenvlobster5_second.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + self.chem_env_lobster5_second.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), n=4, ) ) @@ -575,8 +575,8 @@ def test_get_nn_info(self): # ONLY_CATION_CATION_BONDS=6 assert ( len( - self.chemenvlobster6_second.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + self.chem_env_lobster6_second.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), n=0, ) ) @@ -584,8 +584,8 @@ def test_get_nn_info(self): ) assert ( len( - self.chemenvlobster6_second.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + self.chem_env_lobster6_second.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), n=4, ) ) @@ -594,8 +594,8 @@ def test_get_nn_info(self): assert ( len( - self.chemenvlobster5_second_percentage.get_nn( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), + self.chem_env_lobster5_second_percentage.get_nn( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), n=0, ) ) @@ -603,27 +603,27 @@ def test_get_nn_info(self): ) def test_structure_graph(self): - sg = self.chemenvlobster1_second.get_bonded_structure( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz") + sg = self.chem_env_lobster1_second.get_bonded_structure( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz") ) assert isinstance(sg, StructureGraph) def test_extended_structure_graph(self): - self.chemenvlobsterNaCl = LobsterNeighbors( + self.chem_env_lobsterNaCl = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.NaCl.gz", - filename_blist_sg1=f"{test_dir_env}/ICOBILIST.lobster.NaCl.gz", - filename_blist_sg2=f"{test_dir_env}/ICOOPLIST.lobster.NaCl.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.NaCl.gz", + filename_blist_sg1=f"{test_dir}/ICOBILIST.lobster.NaCl.gz", + filename_blist_sg2=f"{test_dir}/ICOOPLIST.lobster.NaCl.gz", add_additional_data_sg=True, id_blist_sg1="icobi", id_blist_sg2="icoop", additional_condition=1, ) - sg = self.chemenvlobsterNaCl.get_bonded_structure( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), + sg = self.chem_env_lobsterNaCl.get_bonded_structure( + structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), decorate=True, edge_properties=True, weights=True, @@ -637,14 +637,14 @@ def test_extended_structure_graph(self): def test_raises_extended_structure_graph(self): with pytest.raises(ValueError, match="Algorithm can only work with ICOOPs, ICOBIs"): - self.chemenvlobsterNaCl = LobsterNeighbors( + self.chem_env_lobsterNaCl = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.NaCl.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir_env}/CHARGE.lobster.NaCl.gz", - filename_blist_sg1=f"{test_dir_env}/ICOBILIST.lobster.NaCl.gz", - filename_blist_sg2=f"{test_dir_env}/ICOOPLIST.lobster.NaCl.gz", + filename_CHARGE=f"{test_dir}/CHARGE.lobster.NaCl.gz", + filename_blist_sg1=f"{test_dir}/ICOBILIST.lobster.NaCl.gz", + filename_blist_sg2=f"{test_dir}/ICOOPLIST.lobster.NaCl.gz", add_additional_data_sg=True, id_blist_sg1="icopppp", id_blist_sg2="icoop", @@ -652,33 +652,33 @@ def test_raises_extended_structure_graph(self): ) def test_order_parameter(self): - assert self.chemenvlobster1_second.get_local_order_parameters( - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_353.gz"), n=0 + assert self.chem_env_lobster1_second.get_local_order_parameters( + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), n=0 )["linear"] == approx(1.0) def test_get_structure_environments(self): - lse = self.chemenvlobster1_second.get_light_structure_environment() + lse = self.chem_env_lobster1_second.get_light_structure_environment() assert lse.coordination_environments[0][0]["ce_symbol"] == "L:2" assert lse.coordination_environments[5][0]["ce_symbol"] == "T:4" - lse2 = self.chemenvlobster1.get_light_structure_environment() + lse2 = self.chem_env_lobster1.get_light_structure_environment() assert lse2.coordination_environments[0][0]["ce_symbol"] == "O:6" def test_get_strucuture_environments_further_tests(self): - lse = self.chemenvlobster1_second.get_light_structure_environment() + lse = self.chem_env_lobster1_second.get_light_structure_environment() lse.as_dict() lse.get_statistics() assert lse.uniquely_determines_coordination_environments def test_get_info_icohps_neighbors(self): - results = self.chemenvlobster1.get_info_icohps_to_neighbors(isites=[0]) + results = self.chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0]) assert results[0] == approx(-33.26058) for bond in results[1]: assert bond == approx(-5.54345, abs=1e-3) assert results[2] == 6 assert results[3] == ["27", "30", "48", "49", "64", "73"] - results2 = self.chemenvlobster1.get_info_icohps_to_neighbors(isites=None) + results2 = self.chem_env_lobster1.get_info_icohps_to_neighbors(isites=None) assert results2[0] == approx(-33.26058) for bond in results2[1]: assert bond == approx(-5.54345, abs=1e-3) @@ -695,54 +695,54 @@ def test_get_info_icohps_neighbors(self): def test_get_sum_icohps_between_neighbors_of_atom(self): # will only look at icohps between cations or anions - self.chemenvlobster1.get_info_icohps_to_neighbors(isites=[1]) - assert self.chemenvlobster1.get_info_icohps_between_neighbors(isites=[1])[2] == 1 - assert self.chemenvlobster1.get_info_icohps_between_neighbors(isites=[1])[0] == approx(-0.05507) - assert self.chemenvlobster1.get_info_icohps_between_neighbors(isites=[0])[2] == 15 + self.chem_env_lobster1.get_info_icohps_to_neighbors(isites=[1]) + assert self.chem_env_lobster1.get_info_icohps_between_neighbors(isites=[1])[2] == 1 + assert self.chem_env_lobster1.get_info_icohps_between_neighbors(isites=[1])[0] == approx(-0.05507) + assert self.chem_env_lobster1.get_info_icohps_between_neighbors(isites=[0])[2] == 15 # use an example where this is easier to test (e.g., linear environment?) chemenv_here = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp-7000.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp-7000.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp-7000.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp-7000.gz"), additional_condition=1, ) assert len(chemenv_here.get_info_icohps_between_neighbors(isites=[0])[4]) == 6 def test_get_plot_label(self): - label = self.chemenvlobster1._get_plot_label( + label = self.chem_env_lobster1._get_plot_label( atoms=[["Re1", "O2"], ["Re1", "O2"], ["Re1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Re1", "O4"]], per_bond=False, ) assert label == "6 x O-Re" - label = self.chemenvlobster1._get_plot_label( + label = self.chem_env_lobster1._get_plot_label( atoms=[["Re1", "O2"], ["Re1", "O2"], ["Re1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Si1", "O4"]], per_bond=False, ) assert label == "5 x O-Re, 1 x O-Si" - label = self.chemenvlobster1._get_plot_label( + label = self.chem_env_lobster1._get_plot_label( atoms=[["Si1", "O2"], ["Si1", "O2"], ["Si1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Si1", "O4"]], per_bond=False, ) assert label == "4 x O-Si, 2 x O-Re" - label = self.chemenvlobster1._get_plot_label( + label = self.chem_env_lobster1._get_plot_label( atoms=[["Re1", "O2"], ["Re1", "O2"], ["Re1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Re1", "O4"]], per_bond=True, ) assert label == "6 x O-Re (per bond)" def test_get_info_cohps_to_neighbors(self): - chemenvlobster1 = LobsterNeighbors( + chem_env_lobster1 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir_env}/ICOHPLIST.lobster.mp_190_2.gz", - structure=Structure.from_file(f"{test_dir_env}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190_2.gz", + structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), additional_condition=1, ) - cohpcar_lobster_mp_190 = f"{test_dir_env}/COHPCAR.lobster.mp-190.gz" - plot_label, summed_cohpcar_mp_190 = chemenvlobster1.get_info_cohps_to_neighbors( + cohpcar_lobster_mp_190 = f"{test_dir}/COHPCAR.lobster.mp-190.gz" + plot_label, summed_cohpcar_mp_190 = chem_env_lobster1.get_info_cohps_to_neighbors( path_to_COHPCAR=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=["O"], @@ -750,27 +750,27 @@ def test_get_info_cohps_to_neighbors(self): assert plot_label == "6 x O-Re (per bond)" assert isinstance(summed_cohpcar_mp_190, Cohp) - coph_thing = chemenvlobster1.get_info_cohps_to_neighbors( + coph_thing = chem_env_lobster1.get_info_cohps_to_neighbors( path_to_COHPCAR=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=None, per_bond=False, )[1] assert np.sum([coph_thing.icohp[Spin.up], coph_thing.icohp[Spin.down]], axis=0)[300] == approx( - chemenvlobster1.get_info_icohps_to_neighbors(isites=[0])[0] + chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0] ) # summed_spin_channel - coph_thing = chemenvlobster1.get_info_cohps_to_neighbors( + coph_thing = chem_env_lobster1.get_info_cohps_to_neighbors( path_to_COHPCAR=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=None, per_bond=False, summed_spin_channels=True, )[1] - assert coph_thing.icohp[Spin.up][300] == approx(chemenvlobster1.get_info_icohps_to_neighbors(isites=[0])[0]) + assert coph_thing.icohp[Spin.up][300] == approx(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0]) - plot_label, summed_cohpcar_mp_190_Te = chemenvlobster1.get_info_cohps_to_neighbors( + plot_label, summed_cohpcar_mp_190_Te = chem_env_lobster1.get_info_cohps_to_neighbors( path_to_COHPCAR=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=["Te"], @@ -779,29 +779,29 @@ def test_get_info_cohps_to_neighbors(self): assert plot_label is None assert summed_cohpcar_mp_190_Te is None - plot_label, _summed_cohpcar_NaSi = self.chemenvlobster0_NaSi.get_info_cohps_to_neighbors( - path_to_COHPCAR=f"{test_dir_env}/COHPCAR.lobster.NaSi.gz", + plot_label, _summed_cohpcar_NaSi = self.chem_env_lobster0_NaSi.get_info_cohps_to_neighbors( + path_to_COHPCAR=f"{test_dir}/COHPCAR.lobster.NaSi.gz", isites=[8], onlycation_isites=False, only_bonds_to=["Na"], ) assert plot_label == "1 x Na-Si (per bond)" - info = self.chemenvlobster0_NaSi.get_info_cohps_to_neighbors( - path_to_COHPCAR=f"{test_dir_env}/COHPCAR.lobster.NaSi.gz", + info = self.chem_env_lobster0_NaSi.get_info_cohps_to_neighbors( + path_to_COHPCAR=f"{test_dir}/COHPCAR.lobster.NaSi.gz", isites=[8], onlycation_isites=False, only_bonds_to=["Si"], )[0] assert info == "3 x Si-Si (per bond)" - chemenvlobster1.plot_cohps_of_neighbors( + chem_env_lobster1.plot_cohps_of_neighbors( path_to_COHPCAR=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=["O"], summed_spin_channels=True, ) - chemenvlobster1.plot_cohps_of_neighbors( + chem_env_lobster1.plot_cohps_of_neighbors( path_to_COHPCAR=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=["O"], @@ -813,7 +813,7 @@ def test_get_info_cohps_to_neighbors(self): expected_msg = "COHPCAR and ICOHPLIST do not fit together" with pytest.raises(ValueError, match=expected_msg): # icohplist and cohpcar do not fit together - self.chemenvlobster1.get_info_cohps_to_neighbors( + self.chem_env_lobster1.get_info_cohps_to_neighbors( path_to_COHPCAR=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=None, @@ -822,7 +822,7 @@ def test_get_info_cohps_to_neighbors(self): with pytest.raises(ValueError, match=expected_msg): # icohplist and cohpcar do not fit together - self.chemenvlobster2.get_info_cohps_to_neighbors( + self.chem_env_lobster2.get_info_cohps_to_neighbors( path_to_COHPCAR=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=None, From e94d6c1484c30869a8b3b892e0273f6e9233659e Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Sun, 15 Oct 2023 16:36:41 +0200 Subject: [PATCH 018/180] Fix `LobsterMatrices` calculated incorrectly (#3407) * fix erroneous imaginary part due to typo * update LobstrMatrix parser tests > more robust testing --------- Co-authored-by: anaik Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 73 +++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 9c61bdfb23..3316207f91 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -2337,27 +2337,94 @@ def test_attributes(self): ] assert_allclose(self.hamilton_matrices.onsite_energies, ref_onsite_energies) + ref_imag_mat_spin_up = np.zeros((4, 4)) + + assert_allclose(self.hamilton_matrices.hamilton_matrices["1"][Spin.up].imag, ref_imag_mat_spin_up) + + ref_real_mat_spin_up = [ + [-3.0217, 0.0, 0.0, 0.0], + [0.0, -28.5664, 0.0, 0.0], + [0.0, 0.0, -28.5664, 0.0], + [0.0, 0.0, 0.0, -28.5664], + ] + assert_allclose(self.hamilton_matrices.hamilton_matrices["1"][Spin.up].real, ref_real_mat_spin_up) + # overlap matrices assert self.overlap_matrices.average_onsite_overlaps == pytest.approx( {"Si1_3s": 1.00000009, "Si1_3p_y": 0.99999995, "Si1_3p_z": 0.99999995, "Si1_3p_x": 0.99999995} ) ref_onsite_ovelaps = [[1.00000009, 0.99999995, 0.99999995, 0.99999995]] + assert_allclose(self.overlap_matrices.onsite_overlaps, ref_onsite_ovelaps) + ref_imag_mat = np.zeros((4, 4)) + + assert_allclose(self.overlap_matrices.overlap_matrices["1"].imag, ref_imag_mat) + + ref_real_mat = [ + [1.00000009, 0.0, 0.0, 0.0], + [0.0, 0.99999995, 0.0, 0.0], + [0.0, 0.0, 0.99999995, 0.0], + [0.0, 0.0, 0.0, 0.99999995], + ] + + assert_allclose(self.overlap_matrices.overlap_matrices["1"].real, ref_real_mat) + assert len(self.overlap_matrices.overlap_matrices) == 1 # transfer matrices ref_onsite_transfer = [ - [0.00357821, -0.13257223, 0.07208898, -0.00196828], - [-1.03655584e00, 4.35405500e-02, -4.86770000e-04, 2.69085640e-01], + [-0.70523233, -0.07099237, -0.65987499, -0.07090411], + [-0.03735031, -0.66865552, 0.69253776, 0.80648063], ] assert_allclose(self.transfer_matrices.onsite_transfer, ref_onsite_transfer) + ref_imag_mat_spin_down = [ + [-0.99920553, 0.0, 0.0, 0.0], + [0.0, 0.71219607, -0.06090336, -0.08690835], + [0.0, -0.04539545, -0.69302453, 0.08323944], + [0.0, -0.12220894, -0.09749622, -0.53739499], + ] + + assert_allclose(self.transfer_matrices.transfer_matrices["1"][Spin.down].imag, ref_imag_mat_spin_down) + + ref_real_mat_spin_down = [ + [-0.03735031, 0.0, 0.0, 0.0], + [0.0, -0.66865552, 0.06086057, 0.13042529], + [-0.0, 0.04262018, 0.69253776, -0.12491928], + [0.0, 0.11473763, 0.09742773, 0.80648063], + ] + + assert_allclose(self.transfer_matrices.transfer_matrices["1"][Spin.down].real, ref_real_mat_spin_down) + # coefficient matrices assert list(self.coeff_matrices.coefficient_matrices["1"]) == [Spin.up, Spin.down] assert self.coeff_matrices.average_onsite_coefficient == pytest.approx( - {"Si1_3s": -0.12317191, "Si1_3p_y": 0.39037373, "Si1_3p_z": -0.486769934, "Si1_3p_x": 0.1673625} + { + "Si1_3s": 0.6232626450000001, + "Si1_3p_y": -0.029367565000000012, + "Si1_3p_z": -0.50003867, + "Si1_3p_x": 0.13529422, + } ) + ref_imag_mat_spin_up = [ + [-0.59697342, 0.0, 0.0, 0.0], + [0.0, 0.50603774, 0.50538255, -0.26664607], + [0.0, -0.45269894, 0.56996771, 0.23223275], + [0.0, 0.47836456, 0.00476861, 0.50184424], + ] + + assert_allclose(self.coeff_matrices.coefficient_matrices["1"][Spin.up].imag, ref_imag_mat_spin_up) + + ref_real_mat_spin_up = [ + [0.80226096, 0.0, 0.0, 0.0], + [0.0, -0.33931137, -0.42979933, -0.34286226], + [0.0, 0.30354633, -0.48472536, 0.29861248], + [0.0, -0.32075579, -0.00405544, 0.64528776], + ] + + assert_allclose(self.coeff_matrices.coefficient_matrices["1"][Spin.up].real, ref_real_mat_spin_up) + def test_raises(self): with pytest.raises(ValueError, match="Please provide the fermi energy in eV"): self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/Na_hamiltonMatrices.lobster.gz") From 4858384209bdbe2209edcb0dec62f5a118170800 Mon Sep 17 00:00:00 2001 From: "J. George" Date: Thu, 2 Nov 2023 21:29:00 +0100 Subject: [PATCH 019/180] Fix lobsterin dict inheritance and treat \t in lobsterins correctly (#3439) * Fix lobsterin * update lobsterin * use re pattern match * Update tests, implement del * fix typo * clean comments * fix regex pattern to exclude comments, handle tabular spaces, update tests --------- Co-authored-by: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Co-authored-by: anaik --- tests/io/lobster/test_inputs.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 3316207f91..20c51cbbd9 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -1651,6 +1651,32 @@ def test_diff(self): == self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Different"]["SKIPCOHP"]["lobsterin2"] ) + def test_dict_functionality(self): + assert self.Lobsterinfromfile.get("COHPstartEnergy") == -15.0 + assert self.Lobsterinfromfile.get("COHPstartEnergy") == -15.0 + assert self.Lobsterinfromfile.get("COhPstartenergy") == -15.0 + lobsterincopy = self.Lobsterinfromfile.copy() + lobsterincopy.update({"cohpstarteNergy": -10.00}) + assert lobsterincopy["cohpstartenergy"] == -10.0 + lobsterincopy.pop("cohpstarteNergy") + assert "cohpstartenergy" not in lobsterincopy + lobsterincopy.pop("cohpendenergY") + lobsterincopy["cohpsteps"] = 100 + assert lobsterincopy["cohpsteps"] == 100 + before = len(lobsterincopy.items()) + lobsterincopy.popitem() + after = len(lobsterincopy.items()) + assert before != after + + def test_read_write_lobsterin(self): + outfile_path = tempfile.mkstemp()[1] + lobsterin1 = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.1") + lobsterin1.write_lobsterin(outfile_path) + lobsterin2 = Lobsterin.from_file(outfile_path) + assert lobsterin1.diff(lobsterin2)["Different"] == {} + + # TODO: will integer vs float break cohpsteps? + def test_get_basis(self): # get basis functions lobsterin1 = Lobsterin({}) From 1199600fdcbc6d113cd945e0b04b885167fbf538 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Fri, 3 Nov 2023 16:49:31 -0700 Subject: [PATCH 020/180] write test-created files to temporary directory (#3454) --- tests/io/lobster/test_inputs.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 20c51cbbd9..db9063e276 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -2302,15 +2302,13 @@ def test_write_file(self): filename=f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), ) - wave1.write_file(filename="wavecar_test.vasp", part="real") + wave1.write_file(filename=f"{self.tmp_path}/wavecar_test.vasp", part="real") assert os.path.isfile("wavecar_test.vasp") - wave1.write_file(filename="wavecar_test.vasp", part="imaginary") + wave1.write_file(filename=f"{self.tmp_path}/wavecar_test.vasp", part="imaginary") assert os.path.isfile("wavecar_test.vasp") - os.remove("wavecar_test.vasp") - wave1.write_file(filename="density.vasp", part="density") + wave1.write_file(filename=f"{self.tmp_path}/density.vasp", part="density") assert os.path.isfile("density.vasp") - os.remove("density.vasp") class TestSitePotentials(PymatgenTest): From d52324a227dec197309aa09bf3e196e527a9e574 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sun, 12 Nov 2023 18:39:49 -0800 Subject: [PATCH 021/180] rename single-letter vars like m->mol, a->pg_analyzer --- tests/io/lobster/test_inputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index db9063e276..cd39e80279 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -813,7 +813,7 @@ def test_get_structure_with_charges(self): "@module": "pymatgen.core.structure", } s2 = Structure.from_dict(structure_dict2) - assert s2 == self.charge2.get_structure_with_charges(TEST_FILES_DIR / "POSCAR.MnO") + assert s2 == self.charge2.get_structure_with_charges(f"{TEST_FILES_DIR}/POSCAR.MnO") class TestLobsterout(PymatgenTest): From 3fc779ecdf80067d6c3659ee820f99e920a5cec5 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Tue, 14 Nov 2023 08:16:12 -0800 Subject: [PATCH 022/180] replace tests 'str(x) is not None' with actual expected value (#3472) --- tests/io/lobster/test_inputs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index cd39e80279..fc6b084b9a 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -770,11 +770,11 @@ def test_attributes(self): atomlist = ["O1", "Mn2"] types = ["O", "Mn"] num_atoms = 2 - assert_array_equal(charge_Mulliken, self.charge2.Mulliken) - assert_array_equal(charge_Loewdin, self.charge2.Loewdin) - assert_array_equal(atomlist, self.charge2.atomlist) - assert_array_equal(types, self.charge2.types) - assert_array_equal(num_atoms, self.charge2.num_atoms) + assert charge_Mulliken == self.charge2.Mulliken + assert charge_Loewdin == self.charge2.Loewdin + assert atomlist == self.charge2.atomlist + assert types == self.charge2.types + assert num_atoms == self.charge2.num_atoms def test_get_structure_with_charges(self): structure_dict2 = { From 17884c93a7e8d6f9a4b472ad09d22fe23edd2561 Mon Sep 17 00:00:00 2001 From: Aaron Kaplan <33381112+esoteric-ephemera@users.noreply.github.com> Date: Wed, 17 Jan 2024 02:43:30 -0600 Subject: [PATCH 023/180] Improve handling of Vasprun POTCAR search, expanded fake POTCAR library for VASP I/O tests (#3491) * Added fake potcar library for tests * fix failing lobster test caused by removal of potcars * fix failing io.vasp.test_sets * fix alchemy.test_materials test * Add pymatgen.util.testing.FAKE_POTCAR_DIR var for fake potcar library, rename directory of fake potcars * refactor POTCAR search path in Vasprun.get_potcars * test_inputs.py move monkey-patching of potcar_summary_stats into new _mock_complete_potcar_summary_stats fixture * remove largely duplicate asserts assert input_set.potcar.functional == "PBE_64" in TestMatPESStaticSet * minimize diff * restore comments * Modify POTCAR scrambling to only modify parts of the POTCAR not printed to OUTCAR. Revise test data and fake POTCAR library * Make potcar_summary_stats a private attr of pymatgen.io.vasp.inputs.PotcarSingle to resolve slow docs loading time * update doc strings, fix typo * add comment explaining FAKE_POTCAR_DIR contents --------- Signed-off-by: Aaron Kaplan <33381112+esoteric-ephemera@users.noreply.github.com> Co-authored-by: Janosh Riebesell Co-authored-by: Aaron Kaplan --- tests/io/lobster/test_inputs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index fc6b084b9a..97586cf267 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -31,7 +31,7 @@ from pymatgen.io.lobster.inputs import get_all_possible_basis_combinations from pymatgen.io.vasp import Vasprun from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar -from pymatgen.util.testing import TEST_FILES_DIR, PymatgenTest +from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, PymatgenTest __author__ = "Janine George, Marco Esters" __copyright__ = "Copyright 2017, The Materials Project" @@ -1704,7 +1704,7 @@ def test_get_all_possible_basis_functions(self): assert result[0] == {"Fe": "3d 4s", "O": "2p 2s"} assert result[1] == {"Fe": "3d 4s 4p", "O": "2p 2s"} - potcar2 = Potcar.from_file(f"{TEST_FILES_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Fe.gz") + potcar2 = Potcar.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Fe.gz") Potcar_names2 = [name["symbol"] for name in potcar2.spec] result2 = Lobsterin.get_all_possible_basis_functions( Structure.from_file(f"{TEST_FILES_DIR}/Fe.cif"), From 8fbf89898100a03ad8361cf4e9b9a57438f5b37c Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Tue, 23 Jan 2024 10:49:41 +0100 Subject: [PATCH 024/180] CONSTANT_CASE module-scoped TEST_DIR --- tests/io/lobster/test_lobsterenv.py | 250 ++++++++++++++-------------- 1 file changed, 125 insertions(+), 125 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index a1249070e1..1f76da32d6 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -21,7 +21,7 @@ __email__ = "janine.george@uclouvain.be" __date__ = "Jan 14, 2021" -test_dir = f"{TEST_FILES_DIR}/cohp/environments" +TEST_DIR = f"{TEST_FILES_DIR}/cohp/environments" module_dir = os.path.dirname(os.path.abspath(__file__)) @@ -32,231 +32,231 @@ def setUp(self): self.chem_env_lobster1 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=1, ) # all bonds self.chem_env_lobster0 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=0, ) # only cation-cation, anion-anion bonds self.chem_env_lobster5 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=5, ) # only cation-cation bonds self.chem_env_lobster6 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=6, ) # 2,3,4 are not tested so far self.chem_env_lobster2 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=2, ) self.chem_env_lobster3 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=3, ) self.chem_env_lobster4 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=4, ) # search for other testcase where 2,3,4 arrive at different results self.chem_env_lobster0_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=0, ) self.chem_env_lobster1_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=1, ) self.chem_env_lobster2_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=2, ) self.chem_env_lobster5_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=5, ) self.chem_env_lobster5_second_percentage = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=5, perc_strength_ICOHP=1.0, ) self.chem_env_lobster6_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=6, ) # coop / cobi self.chem_env_lobster1_coop_NaCl = LobsterNeighbors( are_coops=True, - filename_ICOHP=f"{test_dir}/ICOOPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), additional_condition=1, noise_cutoff=None, ) self.chem_env_lobster1_cobi_NaCl = LobsterNeighbors( are_coops=True, - filename_ICOHP=f"{test_dir}/ICOBILIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), additional_condition=1, noise_cutoff=None, ) self.chem_env_lobster1_cobi_mp470 = LobsterNeighbors( are_coops=True, - filename_ICOHP=f"{test_dir}/ICOBILIST.lobster.mp_470.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_470.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOBILIST.lobster.mp_470.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_470.gz"), additional_condition=1, ) # TODO: use charge instead of valence self.chem_env_lobster1_charges = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, ) self.chem_env_lobster1_charges_noisecutoff = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_632319.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_632319.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_632319.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_632319.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp_632319.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp_632319.gz", additional_condition=1, perc_strength_ICOHP=0.05, noise_cutoff=0.1, ) self.chem_env_lobster1_charges_wo_noisecutoff = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_632319.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_632319.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_632319.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_632319.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp_632319.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp_632319.gz", additional_condition=1, perc_strength_ICOHP=0.05, noise_cutoff=None, ) self.chem_env_lobster1_charges_loewdin = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, which_charge="Loewdin", ) self.chem_env_lobster6_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=6, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster5_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=5, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster4_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=4, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster3_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=3, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster2_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=2, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster1_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster0_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=0, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster0_NaSi = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.NaSi.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.NaSi.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.NaSi.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaSi.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.NaSi.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.NaSi.gz", additional_condition=0, adapt_extremum_to_add_cond=True, ) @@ -267,8 +267,8 @@ def test_cation_anion_mode_without_ions(self): ): _ = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/../ICOHPLIST.lobster", - structure=Structure.from_file(f"{test_dir}/../POSCAR"), + filename_ICOHP=f"{TEST_DIR}/../ICOHPLIST.lobster", + structure=Structure.from_file(f"{TEST_DIR}/../POSCAR"), valences_from_charges=False, additional_condition=1, ) @@ -277,8 +277,8 @@ def test_cation_anion_mode_without_ions(self): ): _ = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/../ICOHPLIST.lobster", - structure=Structure.from_file(f"{test_dir}/../POSCAR"), + filename_ICOHP=f"{TEST_DIR}/../ICOHPLIST.lobster", + structure=Structure.from_file(f"{TEST_DIR}/../POSCAR"), valences_from_charges=False, additional_condition=1, valences=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], @@ -290,20 +290,20 @@ def test_wrong_additional_correction(self): ): LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=10, ) def test_set_limits(self): test = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.mp-353.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, limits=[-100000, 0], ) @@ -330,7 +330,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster0.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=0, ) ) @@ -339,7 +339,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster0.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=1, ) ) @@ -349,7 +349,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=0, ) ) @@ -358,7 +358,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=1, ) ) @@ -386,7 +386,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster2.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=0, ) ) @@ -395,7 +395,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster2.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=1, ) ) @@ -405,7 +405,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster3.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=0, ) ) @@ -414,7 +414,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster3.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=1, ) ) @@ -424,7 +424,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster4.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=0, ) ) @@ -433,7 +433,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster4.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=1, ) ) @@ -443,7 +443,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster5.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=0, ) ) @@ -452,7 +452,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster5.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=1, ) ) @@ -462,7 +462,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster6.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=0, ) ) @@ -472,7 +472,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster6.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), n=1, ) ) @@ -485,7 +485,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster0_second.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), n=0, ) ) @@ -496,7 +496,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1_second.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), n=0, ) ) @@ -506,7 +506,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1_coop_NaCl.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), n=0, ) ) @@ -516,7 +516,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1_cobi_NaCl.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), n=0, ) ) @@ -526,7 +526,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1_cobi_mp470.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_470.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_470.gz"), n=3, ) ) @@ -537,7 +537,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster2_second.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), n=0, ) ) @@ -546,7 +546,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster2_second.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), n=4, ) ) @@ -557,7 +557,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster5_second.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), n=0, ) ) @@ -566,7 +566,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster5_second.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), n=4, ) ) @@ -576,7 +576,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster6_second.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), n=0, ) ) @@ -585,7 +585,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster6_second.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), n=4, ) ) @@ -595,7 +595,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster5_second_percentage.get_nn( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), n=0, ) ) @@ -604,26 +604,26 @@ def test_get_nn_info(self): def test_structure_graph(self): sg = self.chem_env_lobster1_second.get_bonded_structure( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz") + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz") ) assert isinstance(sg, StructureGraph) def test_extended_structure_graph(self): self.chem_env_lobsterNaCl = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.NaCl.gz", - filename_blist_sg1=f"{test_dir}/ICOBILIST.lobster.NaCl.gz", - filename_blist_sg2=f"{test_dir}/ICOOPLIST.lobster.NaCl.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.NaCl.gz", + filename_blist_sg1=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", + filename_blist_sg2=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", add_additional_data_sg=True, id_blist_sg1="icobi", id_blist_sg2="icoop", additional_condition=1, ) sg = self.chem_env_lobsterNaCl.get_bonded_structure( - structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), decorate=True, edge_properties=True, weights=True, @@ -639,12 +639,12 @@ def test_raises_extended_structure_graph(self): with pytest.raises(ValueError, match="Algorithm can only work with ICOOPs, ICOBIs"): self.chem_env_lobsterNaCl = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.NaCl.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.NaCl.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), valences_from_charges=True, - filename_CHARGE=f"{test_dir}/CHARGE.lobster.NaCl.gz", - filename_blist_sg1=f"{test_dir}/ICOBILIST.lobster.NaCl.gz", - filename_blist_sg2=f"{test_dir}/ICOOPLIST.lobster.NaCl.gz", + filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.NaCl.gz", + filename_blist_sg1=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", + filename_blist_sg2=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", add_additional_data_sg=True, id_blist_sg1="icopppp", id_blist_sg2="icoop", @@ -653,7 +653,7 @@ def test_raises_extended_structure_graph(self): def test_order_parameter(self): assert self.chem_env_lobster1_second.get_local_order_parameters( - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_353.gz"), n=0 + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), n=0 )["linear"] == approx(1.0) def test_get_structure_environments(self): @@ -703,8 +703,8 @@ def test_get_sum_icohps_between_neighbors_of_atom(self): chemenv_here = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp-7000.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp-7000.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp-7000.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp-7000.gz"), additional_condition=1, ) assert len(chemenv_here.get_info_icohps_between_neighbors(isites=[0])[4]) == 6 @@ -737,11 +737,11 @@ def test_get_plot_label(self): def test_get_info_cohps_to_neighbors(self): chem_env_lobster1 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{test_dir}/ICOHPLIST.lobster.mp_190_2.gz", - structure=Structure.from_file(f"{test_dir}/POSCAR.mp_190.gz"), + filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190_2.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=1, ) - cohpcar_lobster_mp_190 = f"{test_dir}/COHPCAR.lobster.mp-190.gz" + cohpcar_lobster_mp_190 = f"{TEST_DIR}/COHPCAR.lobster.mp-190.gz" plot_label, summed_cohpcar_mp_190 = chem_env_lobster1.get_info_cohps_to_neighbors( path_to_COHPCAR=cohpcar_lobster_mp_190, isites=[0], @@ -780,14 +780,14 @@ def test_get_info_cohps_to_neighbors(self): assert summed_cohpcar_mp_190_Te is None plot_label, _summed_cohpcar_NaSi = self.chem_env_lobster0_NaSi.get_info_cohps_to_neighbors( - path_to_COHPCAR=f"{test_dir}/COHPCAR.lobster.NaSi.gz", + path_to_COHPCAR=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", isites=[8], onlycation_isites=False, only_bonds_to=["Na"], ) assert plot_label == "1 x Na-Si (per bond)" info = self.chem_env_lobster0_NaSi.get_info_cohps_to_neighbors( - path_to_COHPCAR=f"{test_dir}/COHPCAR.lobster.NaSi.gz", + path_to_COHPCAR=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", isites=[8], onlycation_isites=False, only_bonds_to=["Si"], From 09d4a39f1a40cf0c5da1c178762ec247ed544834 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Tue, 23 Jan 2024 13:13:41 +0100 Subject: [PATCH 025/180] rename single-letter variables f->file --- tests/io/lobster/test_inputs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 97586cf267..eb445fe42e 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -604,8 +604,8 @@ def setUp(self): self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) - with open(f"{TEST_FILES_DIR}/structure_KF.json") as f: - data = json.load(f) + with open(f"{TEST_FILES_DIR}/structure_KF.json") as file: + data = json.load(file) self.structure = Structure.from_dict(data) From 800b0f5151802be0381327f87f8c4421046c3d67 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Thu, 15 Feb 2024 16:25:44 +0100 Subject: [PATCH 026/180] Lobster io improvements (#3627) * add as_dict method to Bandoverlaps, Grosspop, Charge, Madelung, Sitepotential parser * add tests for as_dict method * Charges MSonable * adapt charges test * add deprecation warning to Charge.Loewdin and Charge.Mulliken * fix msonable sitepotential, grosspop, bandoverlap, icohplist, madelung * add robust test for msonable --------- Co-authored-by: Shyue Ping Ong --- tests/io/lobster/test_inputs.py | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index eb445fe42e..cb5790c2b0 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -11,6 +11,7 @@ from pytest import approx from pymatgen.core.structure import Structure +from pymatgen.electronic_structure.cohp import IcohpCollection from pymatgen.electronic_structure.core import Orbital, Spin from pymatgen.io.lobster import ( Bandoverlaps, @@ -549,6 +550,16 @@ def test_values(self): assert self.icobi.icohpcollection.extremum_icohpvalue() == 0.58649 assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["orbitals"]["2s-6s"]["icohp"][Spin.up] == 0.0247 + def test_msonable(self): + dict_data = self.icobi_orbitalwise_spinpolarized.as_dict() + icohplist_from_dict = Icohplist.from_dict(dict_data) + all_attributes = vars(self.icobi_orbitalwise_spinpolarized) + for attr_name, attr_value in all_attributes.items(): + if isinstance(attr_value, IcohpCollection): + assert getattr(icohplist_from_dict, attr_name).as_dict() == attr_value.as_dict() + else: + assert getattr(icohplist_from_dict, attr_name) == attr_value + class TestNciCobiList(unittest.TestCase): def setUp(self): @@ -815,6 +826,13 @@ def test_get_structure_with_charges(self): s2 = Structure.from_dict(structure_dict2) assert s2 == self.charge2.get_structure_with_charges(f"{TEST_FILES_DIR}/POSCAR.MnO") + def test_msonable(self): + dict_data = self.charge2.as_dict() + charge_from_dict = Charge.from_dict(dict_data) + all_attributes = vars(self.charge2) + for attr_name, attr_value in all_attributes.items(): + assert getattr(charge_from_dict, attr_name) == attr_value + class TestLobsterout(PymatgenTest): def setUp(self): @@ -2062,6 +2080,13 @@ def test_has_good_quality(self): number_occ_bands_spin_up=1, limit_deviation=0.1 ) + def test_msonable(self): + dict_data = self.bandoverlaps2_new.as_dict() + bandoverlaps_from_dict = Bandoverlaps.from_dict(dict_data) + all_attributes = vars(self.bandoverlaps2_new) + for attr_name, attr_value in all_attributes.items(): + assert getattr(bandoverlaps_from_dict, attr_name) == attr_value + class TestGrosspop(unittest.TestCase): def setUp(self): @@ -2179,6 +2204,13 @@ def test_structure_with_grosspop(self): new_structure = self.grosspop1.get_structure_with_total_grosspop(f"{TEST_FILES_DIR}/cohp/POSCAR.SiO2") assert_allclose(new_structure.frac_coords, Structure.from_dict(struct_dict).frac_coords) + def test_msonable(self): + dict_data = self.grosspop1.as_dict() + grosspop_from_dict = Grosspop.from_dict(dict_data) + all_attributes = vars(self.grosspop1) + for attr_name, attr_value in all_attributes.items(): + assert getattr(grosspop_from_dict, attr_name) == attr_value + class TestUtils(PymatgenTest): def test_get_all_possible_basis_combinations(self): @@ -2330,6 +2362,13 @@ def test_get_structure(self): assert structure.site_properties["Loewdin Site Potentials (eV)"] == [-8.77, -17.08, 9.57, 9.57, 8.45] assert structure.site_properties["Mulliken Site Potentials (eV)"] == [-11.38, -19.62, 11.18, 11.18, 10.09] + def test_msonable(self): + dict_data = self.sitepotential.as_dict() + sitepotential_from_dict = SitePotential.from_dict(dict_data) + all_attributes = vars(self.sitepotential) + for attr_name, attr_value in all_attributes.items(): + assert getattr(sitepotential_from_dict, attr_name) == attr_value + class TestMadelungEnergies(PymatgenTest): def setUp(self) -> None: @@ -2340,6 +2379,13 @@ def test_attributes(self): assert self.madelungenergies.madelungenergies_Mulliken == approx(-40.02) assert self.madelungenergies.ewald_splitting == approx(3.14) + def test_msonable(self): + dict_data = self.madelungenergies.as_dict() + madelung_from_dict = MadelungEnergies.from_dict(dict_data) + all_attributes = vars(self.madelungenergies) + for attr_name, attr_value in all_attributes.items(): + assert getattr(madelung_from_dict, attr_name) == attr_value + class TestLobsterMatrices(PymatgenTest): def setUp(self) -> None: From 989ee570df87beb34fa537dded265fbe84fcf349 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Sun, 18 Feb 2024 06:49:25 +0100 Subject: [PATCH 027/180] [WIP] Lobsterenv improvements (#3624) * add args to work with pymatgen objs * adapt and add test --- tests/io/lobster/test_lobsterenv.py | 159 ++++++++++++++++------------ 1 file changed, 93 insertions(+), 66 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 1f76da32d6..c84ffe9797 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -10,8 +10,9 @@ from pymatgen.analysis.graphs import StructureGraph from pymatgen.core import Element from pymatgen.core.structure import Structure -from pymatgen.electronic_structure.cohp import Cohp +from pymatgen.electronic_structure.cohp import Cohp, CompleteCohp from pymatgen.electronic_structure.core import Spin +from pymatgen.io.lobster import Charge, Icohplist from pymatgen.io.lobster.lobsterenv import LobsterNeighbors from pymatgen.util.testing import TEST_FILES_DIR @@ -32,7 +33,7 @@ def setUp(self): self.chem_env_lobster1 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=1, ) @@ -40,7 +41,7 @@ def setUp(self): # all bonds self.chem_env_lobster0 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=0, ) @@ -48,7 +49,7 @@ def setUp(self): # only cation-cation, anion-anion bonds self.chem_env_lobster5 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=5, ) @@ -56,7 +57,7 @@ def setUp(self): # only cation-cation bonds self.chem_env_lobster6 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=6, ) @@ -64,21 +65,21 @@ def setUp(self): # 2,3,4 are not tested so far self.chem_env_lobster2 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=2, ) self.chem_env_lobster3 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=3, ) self.chem_env_lobster4 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=4, ) @@ -86,49 +87,49 @@ def setUp(self): # search for other testcase where 2,3,4 arrive at different results self.chem_env_lobster0_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=0, ) self.chem_env_lobster1_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=1, ) self.chem_env_lobster2_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=2, ) self.chem_env_lobster5_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=5, ) self.chem_env_lobster5_second_percentage = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=5, - perc_strength_ICOHP=1.0, + perc_strength_icohp=1.0, ) self.chem_env_lobster6_second = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), additional_condition=6, ) # coop / cobi self.chem_env_lobster1_coop_NaCl = LobsterNeighbors( are_coops=True, - filename_ICOHP=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", + filename_icohp=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), additional_condition=1, noise_cutoff=None, @@ -136,7 +137,7 @@ def setUp(self): self.chem_env_lobster1_cobi_NaCl = LobsterNeighbors( are_coops=True, - filename_ICOHP=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", + filename_icohp=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), additional_condition=1, noise_cutoff=None, @@ -144,7 +145,7 @@ def setUp(self): self.chem_env_lobster1_cobi_mp470 = LobsterNeighbors( are_coops=True, - filename_ICOHP=f"{TEST_DIR}/ICOBILIST.lobster.mp_470.gz", + filename_icohp=f"{TEST_DIR}/ICOBILIST.lobster.mp_470.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_470.gz"), additional_condition=1, ) @@ -152,111 +153,125 @@ def setUp(self): # TODO: use charge instead of valence self.chem_env_lobster1_charges = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, ) self.chem_env_lobster1_charges_noisecutoff = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_632319.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_632319.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_632319.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp_632319.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp_632319.gz", additional_condition=1, - perc_strength_ICOHP=0.05, + perc_strength_icohp=0.05, noise_cutoff=0.1, ) self.chem_env_lobster1_charges_wo_noisecutoff = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_632319.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_632319.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_632319.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp_632319.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp_632319.gz", additional_condition=1, - perc_strength_ICOHP=0.05, + perc_strength_icohp=0.05, noise_cutoff=None, ) self.chem_env_lobster1_charges_loewdin = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, which_charge="Loewdin", ) self.chem_env_lobster6_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=6, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster5_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=5, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster4_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=4, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster3_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=3, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster2_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=2, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster1_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster0_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=0, adapt_extremum_to_add_cond=True, ) self.chem_env_lobster0_NaSi = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.NaSi.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaSi.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaSi.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.NaSi.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaSi.gz", + additional_condition=0, + adapt_extremum_to_add_cond=True, + ) + # Test LobsterNeighbors using pymatgen objects + self.obj_icohp = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster.NaSi.gz") + self.obj_charge = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.NaSi.gz") + self.chem_env_w_obj = LobsterNeighbors( + filename_icohp=None, + are_coops=False, + obj_icohp=self.obj_icohp, + obj_charge=self.obj_charge, + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaSi.gz"), + valences_from_charges=True, + filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaSi.gz", additional_condition=0, adapt_extremum_to_add_cond=True, ) @@ -267,7 +282,7 @@ def test_cation_anion_mode_without_ions(self): ): _ = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/../ICOHPLIST.lobster", + filename_icohp=f"{TEST_DIR}/../ICOHPLIST.lobster", structure=Structure.from_file(f"{TEST_DIR}/../POSCAR"), valences_from_charges=False, additional_condition=1, @@ -277,7 +292,7 @@ def test_cation_anion_mode_without_ions(self): ): _ = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/../ICOHPLIST.lobster", + filename_icohp=f"{TEST_DIR}/../ICOHPLIST.lobster", structure=Structure.from_file(f"{TEST_DIR}/../POSCAR"), valences_from_charges=False, additional_condition=1, @@ -290,20 +305,20 @@ def test_wrong_additional_correction(self): ): LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=10, ) def test_set_limits(self): test = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, limits=[-100000, 0], ) @@ -611,10 +626,10 @@ def test_structure_graph(self): def test_extended_structure_graph(self): self.chem_env_lobsterNaCl = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.NaCl.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaCl.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.NaCl.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaCl.gz", filename_blist_sg1=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", filename_blist_sg2=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", add_additional_data_sg=True, @@ -639,10 +654,10 @@ def test_raises_extended_structure_graph(self): with pytest.raises(ValueError, match="Algorithm can only work with ICOOPs, ICOBIs"): self.chem_env_lobsterNaCl = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.NaCl.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaCl.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), valences_from_charges=True, - filename_CHARGE=f"{TEST_DIR}/CHARGE.lobster.NaCl.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaCl.gz", filename_blist_sg1=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", filename_blist_sg2=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", add_additional_data_sg=True, @@ -703,7 +718,7 @@ def test_get_sum_icohps_between_neighbors_of_atom(self): chemenv_here = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp-7000.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-7000.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp-7000.gz"), additional_condition=1, ) @@ -737,13 +752,13 @@ def test_get_plot_label(self): def test_get_info_cohps_to_neighbors(self): chem_env_lobster1 = LobsterNeighbors( are_coops=False, - filename_ICOHP=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190_2.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190_2.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=1, ) cohpcar_lobster_mp_190 = f"{TEST_DIR}/COHPCAR.lobster.mp-190.gz" plot_label, summed_cohpcar_mp_190 = chem_env_lobster1.get_info_cohps_to_neighbors( - path_to_COHPCAR=cohpcar_lobster_mp_190, + path_to_cohpcar=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=["O"], ) @@ -751,7 +766,7 @@ def test_get_info_cohps_to_neighbors(self): assert isinstance(summed_cohpcar_mp_190, Cohp) coph_thing = chem_env_lobster1.get_info_cohps_to_neighbors( - path_to_COHPCAR=cohpcar_lobster_mp_190, + path_to_cohpcar=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=None, per_bond=False, @@ -762,7 +777,7 @@ def test_get_info_cohps_to_neighbors(self): # summed_spin_channel coph_thing = chem_env_lobster1.get_info_cohps_to_neighbors( - path_to_COHPCAR=cohpcar_lobster_mp_190, + path_to_cohpcar=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=None, per_bond=False, @@ -771,7 +786,7 @@ def test_get_info_cohps_to_neighbors(self): assert coph_thing.icohp[Spin.up][300] == approx(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0]) plot_label, summed_cohpcar_mp_190_Te = chem_env_lobster1.get_info_cohps_to_neighbors( - path_to_COHPCAR=cohpcar_lobster_mp_190, + path_to_cohpcar=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=["Te"], ) @@ -780,14 +795,26 @@ def test_get_info_cohps_to_neighbors(self): assert summed_cohpcar_mp_190_Te is None plot_label, _summed_cohpcar_NaSi = self.chem_env_lobster0_NaSi.get_info_cohps_to_neighbors( - path_to_COHPCAR=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", + path_to_cohpcar=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", isites=[8], onlycation_isites=False, only_bonds_to=["Na"], ) assert plot_label == "1 x Na-Si (per bond)" + + obj_cohpcar = CompleteCohp.from_file( + filename=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", fmt="LOBSTER", structure_file=f"{TEST_DIR}/POSCAR.NaSi.gz" + ) + plot_label_obj, _summed_cohpcar_NaSi_obj = self.chem_env_w_obj.get_info_cohps_to_neighbors( + obj_cohpcar=obj_cohpcar, + isites=[8], + onlycation_isites=False, + only_bonds_to=["Na"], + ) + assert plot_label_obj == "1 x Na-Si (per bond)" + info = self.chem_env_lobster0_NaSi.get_info_cohps_to_neighbors( - path_to_COHPCAR=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", + path_to_cohpcar=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", isites=[8], onlycation_isites=False, only_bonds_to=["Si"], @@ -795,14 +822,14 @@ def test_get_info_cohps_to_neighbors(self): assert info == "3 x Si-Si (per bond)" chem_env_lobster1.plot_cohps_of_neighbors( - path_to_COHPCAR=cohpcar_lobster_mp_190, + path_to_cohpcar=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=["O"], summed_spin_channels=True, ) chem_env_lobster1.plot_cohps_of_neighbors( - path_to_COHPCAR=cohpcar_lobster_mp_190, + path_to_cohpcar=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=["O"], summed_spin_channels=True, @@ -814,7 +841,7 @@ def test_get_info_cohps_to_neighbors(self): with pytest.raises(ValueError, match=expected_msg): # icohplist and cohpcar do not fit together self.chem_env_lobster1.get_info_cohps_to_neighbors( - path_to_COHPCAR=cohpcar_lobster_mp_190, + path_to_cohpcar=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=None, per_bond=False, @@ -823,7 +850,7 @@ def test_get_info_cohps_to_neighbors(self): with pytest.raises(ValueError, match=expected_msg): # icohplist and cohpcar do not fit together self.chem_env_lobster2.get_info_cohps_to_neighbors( - path_to_COHPCAR=cohpcar_lobster_mp_190, + path_to_cohpcar=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=None, per_bond=False, From e7d3223e285c56253d8d99634d8551fd4c1eea2b Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:04:38 +0100 Subject: [PATCH 028/180] Fix Lobsterenv Bug (#3637) * add args to work with pymatgen objs * adapt and add test * Fix charges if else checks * add tests for valences * refactor test_lobsterenv.py changes --------- Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_lobsterenv.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index c84ffe9797..8ad8c27ac4 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -261,6 +261,15 @@ def setUp(self): additional_condition=0, adapt_extremum_to_add_cond=True, ) + self.chem_env_lobster_NaSi_wo_charges = LobsterNeighbors( + are_coops=False, + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaSi.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaSi.gz"), + valences_from_charges=False, + filename_charge=None, + additional_condition=0, + adapt_extremum_to_add_cond=True, + ) # Test LobsterNeighbors using pymatgen objects self.obj_icohp = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster.NaSi.gz") self.obj_charge = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.NaSi.gz") @@ -855,3 +864,9 @@ def test_get_info_cohps_to_neighbors(self): only_bonds_to=None, per_bond=False, ) + + def test_valences(self): + assert self.chem_env_lobster1_charges_noisecutoff.valences == [0.75, -0.75] # Mulliken + assert self.chem_env_lobster1_charges_loewdin.valences == [0.27, 0.27, 0.27, 0.27, -0.54, -0.54] + assert self.chem_env_w_obj.valences == [0.67] * 4 + [0.7] * 4 + [-0.7] * 4 + [-0.68] * 4 # charge_obj + assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA From 2e535898e068bf1fb3ce78f0bea0dd8f59e6db89 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Sat, 24 Feb 2024 09:34:29 +0100 Subject: [PATCH 029/180] LOBSTER IO improvements (#3649) * add as_dict method to Bandoverlaps, Grosspop, Charge, Madelung, Sitepotential parser * add tests for as_dict method * Charges MSonable * adapt charges test * add deprecation warning to Charge.Loewdin and Charge.Mulliken * fix msonable sitepotential, grosspop, bandoverlap, icohplist, madelung * add robust test for msonable * make lobsterout msonable * add safety check for kwargs * Lobsterout._ATTRIBUTES list->tuple include value in ValueError msg * cover ValueError in test_msonable --------- Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index cb5790c2b0..6034778687 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -1192,7 +1192,7 @@ def test_attributes(self): assert self.lobsterout_skipping_cobi_madelung.has_madelung is False def test_get_doc(self): - comparedict = { + ref_data = { "restart_from_projection": False, "lobster_version": "v3.1.0", "threads": 8, @@ -1247,13 +1247,27 @@ def test_get_doc(self): for key, item in self.lobsterout_normal.get_doc().items(): if key not in ["has_cobicar", "has_madelung"]: if isinstance(item, str): - assert comparedict[key], item + assert ref_data[key], item elif isinstance(item, int): - assert comparedict[key] == item + assert ref_data[key] == item elif key in ("charge_spilling", "total_spilling"): - assert item[0] == approx(comparedict[key][0]) + assert item[0] == approx(ref_data[key][0]) elif isinstance(item, (list, dict)): - assert item == comparedict[key] + assert item == ref_data[key] + + def test_msonable(self): + dict_data = self.lobsterout_normal.as_dict() + lobsterout_from_dict = Lobsterout.from_dict(dict_data) + assert dict_data == lobsterout_from_dict.as_dict() + # test initialization with empty attributes (ensure file is not read again) + dict_data_empty = self.lobsterout_doscar_lso.ATTRIBUTE_DEFAULTS + lobsterout_empty_init_dict = Lobsterout.from_dict(dict_data_empty).as_dict() + for attribute in lobsterout_empty_init_dict: + if "@" not in attribute: + assert dict_data_empty[attribute] == lobsterout_empty_init_dict[attribute] + + with pytest.raises(ValueError, match="invalid=val is not a valid attribute for Lobsterout"): + Lobsterout(filename=None, invalid="val") class TestFatband(PymatgenTest): From 5836759515e8ad2ee3b8c8d0dc91b6b38845bbac Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Thu, 7 Mar 2024 07:16:38 +0100 Subject: [PATCH 030/180] ruff pre-commit to 0.3.1 + auto-fixes, e.g. metaclass=abc.ABCMeta to abc.ABC --- tests/io/lobster/test_inputs.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 6034778687..18eb9cb4f6 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -1577,16 +1577,16 @@ def test_standard_settings(self): "onlycohpcoop", ]: assert lobsterin1["cohpGenerator"] == "from 0.1 to 6.0 orbitalwise" - if option in ["standard"]: + if option == "standard": assert "skipdos" not in lobsterin1 assert "skipcohp" not in lobsterin1 assert "skipcoop" not in lobsterin1 - if option in ["standard_with_fatband"]: + if option == "standard_with_fatband": assert lobsterin1["createFatband"] == ["Fe 3d 4p 4s ", "O 2p 2s "] assert "skipdos" not in lobsterin1 assert "skipcohp" not in lobsterin1 assert "skipcoop" not in lobsterin1 - if option in ["standard_from_projection"]: + if option == "standard_from_projection": assert lobsterin1["loadProjectionFromFile"], True if option in [ "onlyprojection", @@ -1601,22 +1601,22 @@ def test_standard_settings(self): assert lobsterin1["skipGrossPopulation"], True assert lobsterin1["skipMadelungEnergy"], True - if option in ["onlydos"]: + if option == "onlydos": assert lobsterin1["skipPopulationAnalysis"], True assert lobsterin1["skipGrossPopulation"], True assert lobsterin1["skipcohp"], True assert lobsterin1["skipcoop"], True assert lobsterin1["skipcobi"], True assert lobsterin1["skipMadelungEnergy"], True - if option in ["onlycohp"]: + if option == "onlycohp": assert lobsterin1["skipcoop"], True assert lobsterin1["skipcobi"], True - if option in ["onlycoop"]: + if option == "onlycoop": assert lobsterin1["skipcohp"], True assert lobsterin1["skipcobi"], True - if option in ["onlyprojection"]: + if option == "onlyprojection": assert lobsterin1["skipdos"], True - if option in ["onlymadelung"]: + if option == "onlymadelung": assert lobsterin1["skipPopulationAnalysis"], True assert lobsterin1["skipGrossPopulation"], True assert lobsterin1["skipcohp"], True From 0cd0fa5efdbfa9f9a8fdbf00bc18d3fd9abbd883 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Thu, 7 Mar 2024 15:34:28 +0800 Subject: [PATCH 031/180] Clean up test files: VASP outputs (#3653) * move vasprun to dedicated dir * move OUTCAR to dedicated dir * fix vasp output file paths * move CHGCAR to dedicated dir * relocate DOSCAR * relocate EIGENVAL * relocate PROCAR * relocate WAVECAR * relocate XDATCAR * relocate LOCPOT * relocate DYNMAT * relocate ELFCAR * relocate CONTCAR * relocate IBZKPT * relocate WAVEDER and WAVEDERF * relocate trajectory XDATCAR * relocate OSZICAR * temp fix: borg * rename trajectory test files * remove duplicate OUTCARs * compress some CHGCAR OUTCAR PROCAR * compress formatted WAVEDER * pre-commit auto-fixes * sort import * compress OUTCAR and remove duplicate * compress more vasp output files * add missing gz extension * rename some vasprun to respect extension name * rename more vasprun to respect file extension * remove remaining vasprun to respect file extension * fix some paths after rename * redirect borg to output dir for efermi search * pre-commit auto-fixes * fix borg error * add test note * fix test_xe * rename vasprun.xe and use ScratchDir * define VASP_OUT_DIR = f"{TEST_FILES_DIR}/vasp/outputs" for shorter file paths --------- Signed-off-by: Haoyu (Daniel) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 18eb9cb4f6..608a487a5f 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -32,7 +32,7 @@ from pymatgen.io.lobster.inputs import get_all_possible_basis_combinations from pymatgen.io.vasp import Vasprun from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar -from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, PymatgenTest +from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_OUT_DIR, PymatgenTest __author__ = "Janine George, Marco Esters" __copyright__ = "Copyright 2017, The Materials Project" @@ -602,13 +602,13 @@ def test_ncicobilist(self): class TestDoscar(unittest.TestCase): def setUp(self): # first for spin polarized version - doscar = f"{TEST_FILES_DIR}/DOSCAR.lobster.spin" + doscar = f"{VASP_OUT_DIR}/DOSCAR.lobster.spin" poscar = f"{TEST_FILES_DIR}/POSCAR.lobster.spin_DOS" + # not spin polarized - doscar2 = f"{TEST_FILES_DIR}/DOSCAR.lobster.nonspin" + doscar2 = f"{VASP_OUT_DIR}/DOSCAR.lobster.nonspin" poscar2 = f"{TEST_FILES_DIR}/POSCAR.lobster.nonspin_DOS" - f"{TEST_FILES_DIR}/DOSCAR.lobster.nonspin_zip.gz" - f"{TEST_FILES_DIR}/POSCAR.lobster.nonspin_DOS_zip.gz" + self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) @@ -1656,7 +1656,7 @@ def test_standard_with_energy_range_from_vasprun(self): f"{TEST_FILES_DIR}/POSCAR.C2.gz", f"{TEST_FILES_DIR}/INCAR.C2.gz", f"{TEST_FILES_DIR}/POTCAR.C2.gz", - f"{TEST_FILES_DIR}/vasprun.xml.C2.gz", + f"{VASP_OUT_DIR}/vasprun.C2.xml.gz", option="standard_with_energy_range_from_vasprun", ) assert lobsterin_comp["COHPstartEnergy"] == -28.3679 @@ -1838,7 +1838,7 @@ def test_write_kpoints(self): # without line mode lobsterin1.write_KPOINTS(POSCAR_input=outfile_path2, KPOINTS_output=outfile_path, line_mode=False) kpoint = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(f"{TEST_FILES_DIR}/IBZKPT.lobster") + kpoint2 = Kpoints.from_file(f"{VASP_OUT_DIR}/IBZKPT.lobster") for num_kpt, list_kpoint in enumerate(kpoint.kpts): assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) @@ -1856,7 +1856,7 @@ def test_write_kpoints(self): input_grid=[6, 6, 3], ) kpoint = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(f"{TEST_FILES_DIR}/IBZKPT.lobster") + kpoint2 = Kpoints.from_file(f"{VASP_OUT_DIR}/IBZKPT.lobster") for num_kpt, list_kpoint in enumerate(kpoint.kpts): assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) From ad110ee4e0473bfd806a544dbf1712c5cccb6b50 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 8 Mar 2024 20:31:19 +0800 Subject: [PATCH 032/180] Clean up test files: VASP inputs (#3674) * relocate KPOINTS * pre-commit auto-fixes * define VASP_IN_DIR for vasp inputs test files * unify naming with underscore * relocate POSCARs * relocate POTCARs * relocate INCARs * fix some hidden vasp files * fix POSCAR relocate error * reverse POSCAR_LiO2 to POSCAR.LiO2 * Revert "reverse POSCAR_LiO2 to POSCAR.LiO2" This reverts commit ad354fca11ef699cab2af2ca13a6f23849067d50. * remove accidental DEBUG info * remove empty line * fix troublesome POSCAR.LiFePO4 * unify POTCAR naming * relocate feff test files * recover FePO4.vasp and correct name * reverse name of wrong potcars * remove duplicate `INCAR_3` * move another FEFF file * relocate last batch of feff files --------- Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 62 ++++++++++++++++----------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 608a487a5f..df536d2e77 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -32,7 +32,7 @@ from pymatgen.io.lobster.inputs import get_all_possible_basis_combinations from pymatgen.io.vasp import Vasprun from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar -from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_OUT_DIR, PymatgenTest +from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, PymatgenTest __author__ = "Janine George, Marco Esters" __copyright__ = "Copyright 2017, The Materials Project" @@ -603,11 +603,11 @@ class TestDoscar(unittest.TestCase): def setUp(self): # first for spin polarized version doscar = f"{VASP_OUT_DIR}/DOSCAR.lobster.spin" - poscar = f"{TEST_FILES_DIR}/POSCAR.lobster.spin_DOS" + poscar = f"{VASP_IN_DIR}/POSCAR.lobster.spin_DOS" # not spin polarized doscar2 = f"{VASP_OUT_DIR}/DOSCAR.lobster.nonspin" - poscar2 = f"{TEST_FILES_DIR}/POSCAR.lobster.nonspin_DOS" + poscar2 = f"{VASP_IN_DIR}/POSCAR.lobster.nonspin_DOS" self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) @@ -824,7 +824,7 @@ def test_get_structure_with_charges(self): "@module": "pymatgen.core.structure", } s2 = Structure.from_dict(structure_dict2) - assert s2 == self.charge2.get_structure_with_charges(f"{TEST_FILES_DIR}/POSCAR.MnO") + assert s2 == self.charge2.get_structure_with_charges(f"{VASP_IN_DIR}/POSCAR_MnO") def test_msonable(self): dict_data = self.charge2.as_dict() @@ -1531,7 +1531,7 @@ def test_initialize_from_dict(self): lobsterin2 = Lobsterin({"cohpstartenergy": -15.0}) # can only calculate nbands if basis functions are provided with pytest.raises(IOError, match="No basis functions are provided. The program cannot calculate nbands"): - lobsterin2._get_nbands(structure=Structure.from_file(f"{TEST_FILES_DIR}/POSCAR.Fe3O4")) + lobsterin2._get_nbands(structure=Structure.from_file(f"{VASP_IN_DIR}/POSCAR_Fe3O4")) def test_standard_settings(self): # test standard settings @@ -1548,9 +1548,9 @@ def test_standard_settings(self): "onlycohpcoopcobi", ]: lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( - f"{TEST_FILES_DIR}/POSCAR.Fe3O4", - f"{TEST_FILES_DIR}/INCAR.lobster", - f"{TEST_FILES_DIR}/POTCAR.Fe3O4", + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster", + f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz", option=option, ) assert lobsterin1["cohpstartenergy"] == approx(-35.0) @@ -1625,8 +1625,8 @@ def test_standard_settings(self): assert lobsterin1["skipdos"], True # test basis functions by dict lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( - f"{TEST_FILES_DIR}/POSCAR.Fe3O4", - f"{TEST_FILES_DIR}/INCAR.lobster", + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster", dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, option="standard", ) @@ -1634,8 +1634,8 @@ def test_standard_settings(self): # test gaussian smearing lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( - f"{TEST_FILES_DIR}/POSCAR.Fe3O4", - f"{TEST_FILES_DIR}/INCAR.lobster2", + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster2", dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, option="standard", ) @@ -1644,8 +1644,8 @@ def test_standard_settings(self): # fatband and ISMEAR=-5 does not work together with pytest.raises(ValueError, match="ISMEAR has to be 0 for a fatband calculation with Lobster"): lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( - f"{TEST_FILES_DIR}/POSCAR.Fe3O4", - f"{TEST_FILES_DIR}/INCAR.lobster2", + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster2", dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, option="standard_with_fatband", ) @@ -1653,9 +1653,9 @@ def test_standard_settings(self): def test_standard_with_energy_range_from_vasprun(self): # test standard_with_energy_range_from_vasprun lobsterin_comp = Lobsterin.standard_calculations_from_vasp_files( - f"{TEST_FILES_DIR}/POSCAR.C2.gz", - f"{TEST_FILES_DIR}/INCAR.C2.gz", - f"{TEST_FILES_DIR}/POTCAR.C2.gz", + f"{VASP_IN_DIR}/POSCAR_C2", + f"{VASP_IN_DIR}/INCAR_C2", + f"{VASP_IN_DIR}/POTCAR_C2.gz", f"{VASP_OUT_DIR}/vasprun.C2.xml.gz", option="standard_with_energy_range_from_vasprun", ) @@ -1712,7 +1712,7 @@ def test_read_write_lobsterin(self): def test_get_basis(self): # get basis functions lobsterin1 = Lobsterin({}) - potcar = Potcar.from_file(f"{TEST_FILES_DIR}/POTCAR.Fe3O4") + potcar = Potcar.from_file(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") potcar_names = [name["symbol"] for name in potcar.spec] assert lobsterin1.get_basis( @@ -1727,7 +1727,7 @@ def test_get_basis(self): ) == ["Ga 3d 4p 4s ", "As 4p 4s "] def test_get_all_possible_basis_functions(self): - potcar = Potcar.from_file(f"{TEST_FILES_DIR}/POTCAR.Fe3O4") + potcar = Potcar.from_file(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") potcar_names = [name["symbol"] for name in potcar.spec] result = Lobsterin.get_all_possible_basis_functions( Structure.from_file(f"{TEST_FILES_DIR}/Fe3O4.cif"), @@ -1746,16 +1746,16 @@ def test_get_all_possible_basis_functions(self): def test_get_potcar_symbols(self): lobsterin1 = Lobsterin({}) - assert lobsterin1._get_potcar_symbols(f"{TEST_FILES_DIR}/POTCAR.Fe3O4") == ["Fe", "O"] + assert lobsterin1._get_potcar_symbols(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") == ["Fe", "O"] assert lobsterin1._get_potcar_symbols(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") == ["Ga_d", "As"] def test_write_lobsterin(self): # write lobsterin, read it and compare it outfile_path = tempfile.mkstemp()[1] lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( - f"{TEST_FILES_DIR}/POSCAR.Fe3O4", - f"{TEST_FILES_DIR}/INCAR.lobster", - f"{TEST_FILES_DIR}/POTCAR.Fe3O4", + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster", + f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz", option="standard", ) lobsterin1.write_lobsterin(outfile_path) @@ -1766,18 +1766,18 @@ def test_write_incar(self): # write INCAR and compare outfile_path = tempfile.mkstemp()[1] lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( - f"{TEST_FILES_DIR}/POSCAR.Fe3O4", - f"{TEST_FILES_DIR}/INCAR.lobster", - f"{TEST_FILES_DIR}/POTCAR.Fe3O4", + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster", + f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz", option="standard", ) lobsterin1.write_INCAR( - f"{TEST_FILES_DIR}/INCAR.lobster3", + f"{VASP_IN_DIR}/INCAR.lobster3", outfile_path, - f"{TEST_FILES_DIR}/POSCAR.Fe3O4", + f"{VASP_IN_DIR}/POSCAR_Fe3O4", ) - incar1 = Incar.from_file(f"{TEST_FILES_DIR}/INCAR.lobster3") + incar1 = Incar.from_file(f"{VASP_IN_DIR}/INCAR.lobster3") incar2 = Incar.from_file(outfile_path) assert incar1.diff(incar2)["Different"] == { @@ -1794,7 +1794,7 @@ def test_write_kpoints(self): lobsterin1 = Lobsterin({}) # test writing primitive cell lobsterin1.write_POSCAR_with_standard_primitive( - POSCAR_input=f"{TEST_FILES_DIR}/POSCAR.Fe3O4", POSCAR_output=outfile_path2 + POSCAR_input=f"{VASP_IN_DIR}/POSCAR_Fe3O4", POSCAR_output=outfile_path2 ) lobsterin1.write_KPOINTS( @@ -1808,7 +1808,7 @@ def test_write_kpoints(self): assert kpoint.kpts[-1][1] == approx(0.5) assert kpoint.kpts[-1][2] == approx(0.5) assert kpoint.labels[-1] == "T" - kpoint2 = Kpoints.from_file(f"{TEST_FILES_DIR}/KPOINTS_band.lobster") + kpoint2 = Kpoints.from_file(f"{VASP_IN_DIR}/KPOINTS_band.lobster") labels = [] number = 0 From b473c11e61ef340b692b9531be97e774987b0937 Mon Sep 17 00:00:00 2001 From: "J. George" Date: Thu, 14 Mar 2024 19:08:51 +0200 Subject: [PATCH 033/180] Generalize fatband plots from Lobster (#3688) * fatband fix * pre-commit auto-fixes * update plot * pre-commit auto-fixes * Fix ruff * Fix linting * Fix linting * pre-commit auto-fixes * Fix linting * Fix linting * pre-commit auto-fixes * Fix linting * pre-commit auto-fixes * Type * Type * pre-commit auto-fixes * new test * Review comments * pre-commit auto-fixes * linting * remove redundant Lobsterout.ATTRIBUTE_DEFAULTS * fix: handle kwargs and filename missing case * Fix typo in Fatband init docs --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 77 +++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index df536d2e77..1fb8a83291 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -40,7 +40,6 @@ __email__ = "janine.george@uclouvain.be, esters@uoregon.edu" __date__ = "Dec 10, 2017" - module_dir = os.path.dirname(os.path.abspath(__file__)) @@ -1260,11 +1259,11 @@ def test_msonable(self): lobsterout_from_dict = Lobsterout.from_dict(dict_data) assert dict_data == lobsterout_from_dict.as_dict() # test initialization with empty attributes (ensure file is not read again) - dict_data_empty = self.lobsterout_doscar_lso.ATTRIBUTE_DEFAULTS + dict_data_empty = dict.fromkeys(self.lobsterout_doscar_lso._ATTRIBUTES, None) lobsterout_empty_init_dict = Lobsterout.from_dict(dict_data_empty).as_dict() for attribute in lobsterout_empty_init_dict: if "@" not in attribute: - assert dict_data_empty[attribute] == lobsterout_empty_init_dict[attribute] + assert lobsterout_empty_init_dict[attribute] is None with pytest.raises(ValueError, match="invalid=val is not a valid attribute for Lobsterout"): Lobsterout(filename=None, invalid="val") @@ -1272,29 +1271,47 @@ def test_msonable(self): class TestFatband(PymatgenTest): def setUp(self): + self.structure = Vasprun( + filename=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + ionic_step_skip=None, + ionic_step_offset=0, + parse_dos=True, + parse_eigen=False, + parse_projected_eigen=False, + parse_potcar_file=False, + occu_tol=1e-8, + exception_on_bad_xml=True, + ).final_structure self.fatband_SiO2_p_x = Fatband( filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x", - Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + structure=self.structure, + vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", ) self.vasprun_SiO2_p_x = Vasprun(filename=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml") self.bs_symmline = self.vasprun_SiO2_p_x.get_band_structure(line_mode=True, force_hybrid_mode=True) self.fatband_SiO2_p = Fatband( filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p", - Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/KPOINTS", - vasprun=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/vasprun.xml", + kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/KPOINTS", + vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/vasprun.xml", + structure=self.structure, + ) + self.fatband_SiO2_p2 = Fatband( + filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p", + kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/KPOINTS", + structure=self.structure, + vasprun_file=None, + efermi=1.0647039, ) self.vasprun_SiO2_p = Vasprun(filename=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/vasprun.xml") self.bs_symmline2 = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) self.fatband_SiO2_spin = Fatband( filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin", - Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/KPOINTS", - vasprun=os.path.join( - TEST_FILES_DIR, - "cohp", - "Fatband_SiO2/Test_Spin/vasprun.xml", - ), + kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/KPOINTS", + vasprun_file=os.path.join(TEST_FILES_DIR, "cohp", "Fatband_SiO2/Test_Spin/vasprun.xml"), + structure=self.structure, ) + self.vasprun_SiO2_spin = Vasprun( filename=os.path.join( TEST_FILES_DIR, @@ -1334,6 +1351,7 @@ def test_attributes(self): assert self.fatband_SiO2_p.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) assert self.fatband_SiO2_p.structure[0].species_string == "Si" assert self.fatband_SiO2_p.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) + assert self.fatband_SiO2_p.efermi == approx(1.0647039) assert list(self.fatband_SiO2_spin.label_dict["M"]) == approx([0.5, 0.0, 0.0]) assert self.fatband_SiO2_spin.efermi == self.vasprun_SiO2_spin.efermi @@ -1353,6 +1371,13 @@ def test_attributes(self): assert self.fatband_SiO2_spin.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) def test_raises(self): + with pytest.raises(ValueError, match="vasprun_file or efermi have to be provided"): + Fatband( + filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin", + kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/KPOINTS", + vasprun_file=None, + structure=self.structure, + ) with pytest.raises( ValueError, match="The are two FATBAND files for the same atom and orbital. The program will stop" ): @@ -1361,8 +1386,20 @@ def test_raises(self): f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", ], - Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + structure=self.structure, + ) + + with pytest.raises(ValueError, match="A structure object has to be provided"): + self.fatband_SiO2_p_x = Fatband( + filenames=[ + f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + ], + kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + structure=None, ) with pytest.raises( @@ -1374,15 +1411,17 @@ def test_raises(self): f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/FATBAND_si1_3p.lobster", ], - Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + structure=self.structure, ) with pytest.raises(ValueError, match="No FATBAND files in folder or given"): self.fatband_SiO2_p_x = Fatband( filenames=".", - Kpointsfile=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + structure=self.structure, ) def test_get_bandstructure(self): From 5519ab4e1634f02ead609fec2a83e921e58caa30 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Fri, 15 Mar 2024 11:49:55 +0100 Subject: [PATCH 034/180] Improve Bandoverlaps parser (#3689) * rewrite band_overlap_dict output format, update and add associated tests * rewrite band_overlap_dict output format, update and add associated tests * refactor * fix failing test due to refactoring * address review comment and fix some pyright errors * lst.append() -> concat --------- Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 194 ++++++++++++++------------------ 1 file changed, 87 insertions(+), 107 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 1fb8a83291..f13c5de213 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -945,18 +945,7 @@ def test_attributes(self): ] assert self.lobsterout_saveprojection.basis_functions == [ - [ - "3s", - "4s", - "3p_y", - "3p_z", - "3p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ] + ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] ] assert self.lobsterout_saveprojection.basis_type == ["pbeVaspFit2015"] assert self.lobsterout_saveprojection.charge_spilling == [0.0268] @@ -998,18 +987,7 @@ def test_attributes(self): ] assert self.lobsterout_skipping_all.basis_functions == [ - [ - "3s", - "4s", - "3p_y", - "3p_z", - "3p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ] + ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] ] assert self.lobsterout_skipping_all.basis_type == ["pbeVaspFit2015"] assert self.lobsterout_skipping_all.charge_spilling == [0.0268] @@ -1985,184 +1963,186 @@ def test_msonable_implementation(self): class TestBandoverlaps(unittest.TestCase): def setUp(self): - # test spin polarlized calc and non spinpolarized calc + # test spin-polarized calc and non spinpolarized calc - self.bandoverlaps1 = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.1") - self.bandoverlaps2 = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.2") + self.band_overlaps1 = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.1") + self.band_overlaps2 = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.2") - self.bandoverlaps1_new = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.new.1") - self.bandoverlaps2_new = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.new.2") + self.band_overlaps1_new = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.new.1") + self.band_overlaps2_new = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.new.2") def test_attributes(self): # bandoverlapsdict - assert self.bandoverlaps1.bandoverlapsdict[Spin.up]["0.5 0 0"]["maxDeviation"] == approx(0.000278953) - assert self.bandoverlaps1_new.bandoverlapsdict[Spin.up]["0 0 0"]["maxDeviation"] == approx(0.0640933) - assert self.bandoverlaps1.bandoverlapsdict[Spin.up]["0.5 0 0"]["matrix"][-1][-1] == approx(0.0188058) - assert self.bandoverlaps1_new.bandoverlapsdict[Spin.up]["0 0 0"]["matrix"][-1][-1] == approx(1.0) - assert self.bandoverlaps1.bandoverlapsdict[Spin.up]["0.5 0 0"]["matrix"][0][0] == approx(1) - assert self.bandoverlaps1_new.bandoverlapsdict[Spin.up]["0 0 0"]["matrix"][0][0] == approx(0.995849) - - assert self.bandoverlaps1.bandoverlapsdict[Spin.down]["0.0261194 0.0261194 0.473881"]["maxDeviation"] == approx( - 4.31567e-05 - ) - assert self.bandoverlaps1_new.bandoverlapsdict[Spin.down]["0 0 0"]["maxDeviation"] == approx(0.064369) - assert self.bandoverlaps1.bandoverlapsdict[Spin.down]["0.0261194 0.0261194 0.473881"]["matrix"][0][ - -1 - ] == approx(4.0066e-07) - assert self.bandoverlaps1_new.bandoverlapsdict[Spin.down]["0 0 0"]["matrix"][0][-1] == approx(1.37447e-09) + bo_dict = self.band_overlaps1.bandoverlapsdict + assert bo_dict[Spin.up]["max_deviations"][0] == approx(0.000278953) + assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["max_deviations"][10] == approx(0.0640933) + assert bo_dict[Spin.up]["matrices"][0].item(-1, -1) == approx(0.0188058) + assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["matrices"][10].item(-1, -1) == approx(1.0) + assert bo_dict[Spin.up]["matrices"][0].item(0, 0) == approx(1) + assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["matrices"][10].item(0, 0) == approx(0.995849) + + assert bo_dict[Spin.down]["max_deviations"][-1] == approx(4.31567e-05) + assert self.band_overlaps1_new.bandoverlapsdict[Spin.down]["max_deviations"][9] == approx(0.064369) + assert bo_dict[Spin.down]["matrices"][-1].item(0, -1) == approx(4.0066e-07) + assert self.band_overlaps1_new.bandoverlapsdict[Spin.down]["matrices"][9].item(0, -1) == approx(1.37447e-09) # maxDeviation - assert self.bandoverlaps1.max_deviation[0] == approx(0.000278953) - assert self.bandoverlaps1_new.max_deviation[0] == approx(0.39824) - assert self.bandoverlaps1.max_deviation[-1] == approx(4.31567e-05) - assert self.bandoverlaps1_new.max_deviation[-1] == approx(0.324898) + assert self.band_overlaps1.max_deviation[0] == approx(0.000278953) + assert self.band_overlaps1_new.max_deviation[0] == approx(0.39824) + assert self.band_overlaps1.max_deviation[-1] == approx(4.31567e-05) + assert self.band_overlaps1_new.max_deviation[-1] == approx(0.324898) - assert self.bandoverlaps2.max_deviation[0] == approx(0.000473319) - assert self.bandoverlaps2_new.max_deviation[0] == approx(0.403249) - assert self.bandoverlaps2.max_deviation[-1] == approx(1.48451e-05) - assert self.bandoverlaps2_new.max_deviation[-1] == approx(0.45154) + assert self.band_overlaps2.max_deviation[0] == approx(0.000473319) + assert self.band_overlaps2_new.max_deviation[0] == approx(0.403249) + assert self.band_overlaps2.max_deviation[-1] == approx(1.48451e-05) + assert self.band_overlaps2_new.max_deviation[-1] == approx(0.45154) def test_has_good_quality(self): - assert not self.bandoverlaps1.has_good_quality_maxDeviation(limit_maxDeviation=0.1) - assert not self.bandoverlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=0.1) - assert not self.bandoverlaps1.has_good_quality_check_occupied_bands( + assert not self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=0.1) + assert not self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=0.1) + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=9, number_occ_bands_spin_down=5, limit_deviation=0.1, spin_polarized=True, ) - assert not self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=9, number_occ_bands_spin_down=5, limit_deviation=0.1, spin_polarized=True, ) - assert self.bandoverlaps1.has_good_quality_check_occupied_bands( + assert self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=3, number_occ_bands_spin_down=0, - limit_deviation=0.001, + limit_deviation=1, spin_polarized=True, ) - assert self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + assert self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=3, number_occ_bands_spin_down=0, - limit_deviation=0.01, + limit_deviation=1, spin_polarized=True, ) - assert not self.bandoverlaps1.has_good_quality_check_occupied_bands( + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=1, limit_deviation=0.000001, spin_polarized=True, ) - assert not self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=1, limit_deviation=0.000001, spin_polarized=True, ) - assert not self.bandoverlaps1.has_good_quality_check_occupied_bands( + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=0, limit_deviation=0.000001, spin_polarized=True, ) - assert not self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=0, limit_deviation=0.000001, spin_polarized=True, ) - assert not self.bandoverlaps1.has_good_quality_check_occupied_bands( + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=0, number_occ_bands_spin_down=1, limit_deviation=0.000001, spin_polarized=True, ) - assert not self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=0, number_occ_bands_spin_down=1, limit_deviation=0.000001, spin_polarized=True, ) - assert not self.bandoverlaps1.has_good_quality_check_occupied_bands( + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=4, number_occ_bands_spin_down=4, limit_deviation=0.001, spin_polarized=True, ) - assert not self.bandoverlaps1_new.has_good_quality_check_occupied_bands( + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=4, number_occ_bands_spin_down=4, limit_deviation=0.001, spin_polarized=True, ) - assert self.bandoverlaps1.has_good_quality_maxDeviation(limit_maxDeviation=100) - assert self.bandoverlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=100) - assert self.bandoverlaps2.has_good_quality_maxDeviation() - assert not self.bandoverlaps2_new.has_good_quality_maxDeviation() - assert not self.bandoverlaps2.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) - assert not self.bandoverlaps2_new.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) - assert not self.bandoverlaps2.has_good_quality_check_occupied_bands( + assert self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=100) + assert self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=100) + assert self.band_overlaps2.has_good_quality_maxDeviation() + assert not self.band_overlaps2_new.has_good_quality_maxDeviation() + assert not self.band_overlaps2.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) + assert not self.band_overlaps2_new.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) + assert not self.band_overlaps2.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=10, limit_deviation=0.0000001 ) - assert not self.bandoverlaps2_new.has_good_quality_check_occupied_bands( + assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=10, limit_deviation=0.0000001 ) - assert self.bandoverlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=1, limit_deviation=0.1) - assert self.bandoverlaps2_new.has_good_quality_check_occupied_bands( + assert not self.band_overlaps2.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, limit_deviation=0.1 ) - assert not self.bandoverlaps2.has_good_quality_check_occupied_bands( + assert not self.band_overlaps2.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, limit_deviation=1e-8 ) - assert not self.bandoverlaps2_new.has_good_quality_check_occupied_bands( + assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, limit_deviation=1e-8 ) - assert self.bandoverlaps2.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=10, limit_deviation=0.1 - ) - assert self.bandoverlaps2_new.has_good_quality_check_occupied_bands( + assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=10, limit_deviation=1) + assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=2, limit_deviation=0.1 ) - - assert self.bandoverlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=1, limit_deviation=0.1) - assert self.bandoverlaps2_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, limit_deviation=0.1 + assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=1, limit_deviation=1) + assert self.band_overlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=2 ) def test_msonable(self): - dict_data = self.bandoverlaps2_new.as_dict() + dict_data = self.band_overlaps2_new.as_dict() bandoverlaps_from_dict = Bandoverlaps.from_dict(dict_data) - all_attributes = vars(self.bandoverlaps2_new) + all_attributes = vars(self.band_overlaps2_new) for attr_name, attr_value in all_attributes.items(): assert getattr(bandoverlaps_from_dict, attr_name) == attr_value + def test_keys(self): + bo_dict = self.band_overlaps1.band_overlaps_dict + bo_dict_new = self.band_overlaps1_new.band_overlaps_dict + bo_dict_2 = self.band_overlaps2.band_overlaps_dict + assert len(bo_dict[Spin.up]["k_points"]) == 408 + assert len(bo_dict_2[Spin.up]["max_deviations"]) == 2 + assert len(bo_dict_new[Spin.down]["matrices"]) == 73 + class TestGrosspop(unittest.TestCase): def setUp(self): self.grosspop1 = Grosspop(f"{TEST_FILES_DIR}/cohp/GROSSPOP.lobster") def test_attributes(self): - assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3s"] == approx(0.52) - assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3p_y"] == approx(0.38) - assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3p_z"] == approx(0.37) - assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["3p_x"] == approx(0.37) - assert self.grosspop1.list_dict_grosspop[0]["Mulliken GP"]["total"] == approx(1.64) - assert self.grosspop1.list_dict_grosspop[0]["element"] == "Si" - assert self.grosspop1.list_dict_grosspop[0]["Loewdin GP"]["3s"] == approx(0.61) - assert self.grosspop1.list_dict_grosspop[0]["Loewdin GP"]["3p_y"] == approx(0.52) - assert self.grosspop1.list_dict_grosspop[0]["Loewdin GP"]["3p_z"] == approx(0.52) - assert self.grosspop1.list_dict_grosspop[0]["Loewdin GP"]["3p_x"] == approx(0.52) - assert self.grosspop1.list_dict_grosspop[0]["Loewdin GP"]["total"] == approx(2.16) - assert self.grosspop1.list_dict_grosspop[5]["Mulliken GP"]["2s"] == approx(1.80) - assert self.grosspop1.list_dict_grosspop[5]["Loewdin GP"]["2s"] == approx(1.60) - assert self.grosspop1.list_dict_grosspop[5]["element"] == "O" - assert self.grosspop1.list_dict_grosspop[8]["Mulliken GP"]["2s"] == approx(1.80) - assert self.grosspop1.list_dict_grosspop[8]["Loewdin GP"]["2s"] == approx(1.60) - assert self.grosspop1.list_dict_grosspop[8]["element"] == "O" + gross_pop_list = self.grosspop1.list_dict_grosspop + assert gross_pop_list[0]["Mulliken GP"]["3s"] == approx(0.52) + assert gross_pop_list[0]["Mulliken GP"]["3p_y"] == approx(0.38) + assert gross_pop_list[0]["Mulliken GP"]["3p_z"] == approx(0.37) + assert gross_pop_list[0]["Mulliken GP"]["3p_x"] == approx(0.37) + assert gross_pop_list[0]["Mulliken GP"]["total"] == approx(1.64) + assert gross_pop_list[0]["element"] == "Si" + assert gross_pop_list[0]["Loewdin GP"]["3s"] == approx(0.61) + assert gross_pop_list[0]["Loewdin GP"]["3p_y"] == approx(0.52) + assert gross_pop_list[0]["Loewdin GP"]["3p_z"] == approx(0.52) + assert gross_pop_list[0]["Loewdin GP"]["3p_x"] == approx(0.52) + assert gross_pop_list[0]["Loewdin GP"]["total"] == approx(2.16) + assert gross_pop_list[5]["Mulliken GP"]["2s"] == approx(1.80) + assert gross_pop_list[5]["Loewdin GP"]["2s"] == approx(1.60) + assert gross_pop_list[5]["element"] == "O" + assert gross_pop_list[8]["Mulliken GP"]["2s"] == approx(1.80) + assert gross_pop_list[8]["Loewdin GP"]["2s"] == approx(1.60) + assert gross_pop_list[8]["element"] == "O" def test_structure_with_grosspop(self): struct_dict = { From a27be30573abb2df5e1dd0c164dafcf6acff9a68 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Fri, 15 Mar 2024 13:11:18 +0100 Subject: [PATCH 035/180] use f-strings for test file paths --- tests/io/lobster/test_inputs.py | 42 +++++++-------------------------- 1 file changed, 8 insertions(+), 34 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index f13c5de213..fe28bd913b 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -320,11 +320,7 @@ def setUp(self): are_cobis=True, ) self.icobi_orbitalwise_spinpolarized_add = Icohplist( - filename=os.path.join( - TEST_FILES_DIR, - "cohp", - "ICOBILIST.lobster.spinpolarized.additional_case", - ), + filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster.spinpolarized.additional_case", are_cobis=True, ) @@ -839,11 +835,7 @@ def setUp(self): # make sure .gz files are also read correctly self.lobsterout_normal = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.normal2.gz") self.lobsterout_fatband_grosspop_densityofenergies = Lobsterout( - filename=os.path.join( - TEST_FILES_DIR, - "cohp", - "lobsterout.fatband_grosspop_densityofenergy", - ) + filename=f"{TEST_FILES_DIR}/cohp/lobsterout.fatband_grosspop_densityofenergy" ) self.lobsterout_saveprojection = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.saveprojection") self.lobsterout_skipping_all = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.skipping_all") @@ -1286,17 +1278,11 @@ def setUp(self): self.fatband_SiO2_spin = Fatband( filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin", kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/KPOINTS", - vasprun_file=os.path.join(TEST_FILES_DIR, "cohp", "Fatband_SiO2/Test_Spin/vasprun.xml"), + vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/vasprun.xml", structure=self.structure, ) - self.vasprun_SiO2_spin = Vasprun( - filename=os.path.join( - TEST_FILES_DIR, - "cohp", - "Fatband_SiO2/Test_Spin/vasprun.xml", - ) - ) + self.vasprun_SiO2_spin = Vasprun(filename=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/vasprun.xml") self.bs_symmline_spin = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) def test_attributes(self): @@ -2303,11 +2289,7 @@ def test_get_all_possible_basis_combinations(self): class TestWavefunction(PymatgenTest): def test_parse_file(self): grid, points, real, imaginary, distance = Wavefunction._parse_file( - os.path.join( - TEST_FILES_DIR, - "cohp", - "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - ) + f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz" ) assert_array_equal([41, 41, 41], grid) assert points[4][0] == approx(0.0000) @@ -2332,11 +2314,7 @@ def test_set_volumetric_data(self): def test_get_volumetricdata_real(self): wave1 = Wavefunction( - filename=os.path.join( - TEST_FILES_DIR, - "cohp", - "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - ), + filename=f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), ) volumetricdata_real = wave1.get_volumetricdata_real() @@ -2344,11 +2322,7 @@ def test_get_volumetricdata_real(self): def test_get_volumetricdata_imaginary(self): wave1 = Wavefunction( - filename=os.path.join( - TEST_FILES_DIR, - "cohp", - "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - ), + filename=f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), ) volumetricdata_imaginary = wave1.get_volumetricdata_imaginary() @@ -2356,7 +2330,7 @@ def test_get_volumetricdata_imaginary(self): def test_get_volumetricdata_density(self): wave1 = Wavefunction( - filename=os.path.join(TEST_FILES_DIR, "cohp", "LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz"), + filename=f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), ) volumetricdata_density = wave1.get_volumetricdata_density() From cee7804d180c519a6416fe3e937e787ba63e05c8 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Fri, 15 Mar 2024 13:26:47 +0100 Subject: [PATCH 036/180] replace assert hasattr(...) with more sensitive asserts --- tests/io/lobster/test_inputs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index fe28bd913b..285ba382f1 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -2309,8 +2309,8 @@ def test_set_volumetric_data(self): ) wave1.set_volumetric_data(grid=wave1.grid, structure=wave1.structure) - assert hasattr(wave1, "volumetricdata_real") - assert hasattr(wave1, "volumetricdata_imaginary") + assert wave1.volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) + assert wave1.volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) def test_get_volumetricdata_real(self): wave1 = Wavefunction( From 3ab4b805a88815e7e92a22b3723a9eca8d9412be Mon Sep 17 00:00:00 2001 From: "J. George" Date: Sat, 16 Mar 2024 14:44:31 +0100 Subject: [PATCH 037/180] Plotting of Multicenter COBIs (#2926) * implementation to plot multicenter cobis * add tests --------- Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 85 ++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 285ba382f1..1ebea49120 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -75,6 +75,33 @@ def setUp(self): filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.gz", are_cobis=True, ) + # 3 center + self.cobi2 = Cohpcar( + filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.GeTe", + are_cobis=False, + are_multi_center_cobis=True, + ) + # 4 center + self.cobi3 = Cohpcar( + filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.GeTe_4center", are_cobis=False, are_multi_center_cobis=True + ) + # partially orbital-resolved + self.cobi4 = Cohpcar( + filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.GeTe.multi.orbitalwise", + are_cobis=False, + are_multi_center_cobis=True, + ) + # fully orbital-resolved + self.cobi5 = Cohpcar( + filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.GeTe.multi.orbitalwise.full", + are_cobis=False, + are_multi_center_cobis=True, + ) + # spin polarized + # fully orbital-resolved + self.cobi6 = Cohpcar( + filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.B2H6.spin", are_cobis=False, are_multi_center_cobis=True + ) def test_attributes(self): assert not self.cohp_bise.are_coops @@ -111,6 +138,11 @@ def test_attributes(self): assert self.cobi.are_cobis assert not self.cobi.is_spin_polarized + # test multi-center cobis + assert not self.cobi2.are_cobis + assert not self.cobi2.are_coops + assert self.cobi2.are_multi_center_cobis + def test_energies(self): efermi_bise = 5.90043 elim_bise = (-0.124679, 11.9255) @@ -195,6 +227,46 @@ def test_cohp_data(self): assert len(val["COHP"][Spin.up]) == 6 assert len(val["ICOHP"][Spin.up]) == 6 + for data in [self.cobi2.cohp_data]: + for bond, val in data.items(): + if bond != "average": + if int(bond) >= 13: + assert len(val["COHP"][Spin.up]) == 11 + assert len(val["cells"]) == 3 + else: + assert len(val["COHP"][Spin.up]) == 11 + assert len(val["cells"]) == 2 + + for data in [self.cobi3.cohp_data, self.cobi4.cohp_data]: + for bond, val in data.items(): + if bond != "average": + if int(bond) >= 13: + assert len(val["cells"]) == 4 + else: + assert len(val["cells"]) == 2 + for data in [self.cobi5.cohp_data]: + for bond, val in data.items(): + if bond != "average": + if int(bond) >= 25: + assert len(val["cells"]) == 4 + else: + assert len(val["cells"]) == 2 + for data in [self.cobi6.cohp_data]: + for bond, val in data.items(): + if bond != "average": + if int(bond) >= 21: + assert len(val["cells"]) == 3 + assert len(val["COHP"][Spin.up]) == 12 + assert len(val["COHP"][Spin.down]) == 12 + for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down]): + assert cohp1 == approx(cohp2, abs=1e-4) + else: + assert len(val["cells"]) == 2 + assert len(val["COHP"][Spin.up]) == 12 + assert len(val["COHP"][Spin.down]) == 12 + for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down]): + assert cohp1 == approx(cohp2, abs=1e-3) + def test_orbital_resolved_cohp(self): orbitals = [(Orbital(i), Orbital(j)) for j in range(4) for i in range(4)] assert self.cohp_bise.orb_res_cohp is None @@ -282,8 +354,20 @@ def test_orbital_resolved_cohp(self): ], axis=0, ) + assert_allclose(tot_Na2UO4, icohp_Na2UO4, atol=1e-3) + assert "5s-4s-5s-4s" in self.cobi4.orb_res_cohp["13"] + assert "5px-4px-5px-4px" in self.cobi4.orb_res_cohp["13"] + assert len(self.cobi4.orb_res_cohp["13"]["5px-4px-5px-4px"]["COHP"][Spin.up]) == 11 + + assert "5s-4s-5s-4s" in self.cobi5.orb_res_cohp["25"] + assert "5px-4px-5px-4px" in self.cobi5.orb_res_cohp["25"] + assert len(self.cobi5.orb_res_cohp["25"]["5px-4px-5px-4px"]["COHP"][Spin.up]) == 11 + + assert len(self.cobi6.orb_res_cohp["21"]["2py-1s-2s"]["COHP"][Spin.up]) == 12 + assert len(self.cobi6.orb_res_cohp["21"]["2py-1s-2s"]["COHP"][Spin.down]) == 12 + class TestIcohplist(unittest.TestCase): def setUp(self): @@ -304,7 +388,6 @@ def setUp(self): filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster", are_cobis=True, ) - # TODO: test orbitalwise ICOHPs with and without spin polarization self.icobi = Icohplist( filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster.withoutorbitals", From eb9b6938382bc83a983ccf52e97ac5addd5fbd1c Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Tue, 26 Mar 2024 12:48:24 +0100 Subject: [PATCH 038/180] Clean up tests (#3713) * clean up tests e.g. remove unnecessary for loops when using pytest.approx() * Fix typo in op_params --- tests/io/lobster/test_inputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 1ebea49120..50c10d5117 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -268,7 +268,7 @@ def test_cohp_data(self): assert cohp1 == approx(cohp2, abs=1e-3) def test_orbital_resolved_cohp(self): - orbitals = [(Orbital(i), Orbital(j)) for j in range(4) for i in range(4)] + orbitals = [(Orbital(jj), Orbital(ii)) for ii in range(4) for jj in range(4)] assert self.cohp_bise.orb_res_cohp is None assert self.coop_bise.orb_res_cohp is None assert self.cohp_fe.orb_res_cohp is None From 804a217ba50cd9290d12129231755d577fb868a3 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Wed, 27 Mar 2024 19:59:22 +0100 Subject: [PATCH 039/180] Remove unnecessary `unittest.TestCase` subclassing (#3718) * rename QuasiHarmonicDebyeApprox and deprecate old class * remove unnecessary subclassing of unittest.TestCase --- tests/io/lobster/test_inputs.py | 14 +++++++------- tests/io/lobster/test_lobsterenv.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 50c10d5117..6e1c87fcef 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -3,7 +3,7 @@ import json import os import tempfile -import unittest +from unittest import TestCase import numpy as np import pytest @@ -369,7 +369,7 @@ def test_orbital_resolved_cohp(self): assert len(self.cobi6.orb_res_cohp["21"]["2py-1s-2s"]["COHP"][Spin.down]) == 12 -class TestIcohplist(unittest.TestCase): +class TestIcohplist(TestCase): def setUp(self): self.icohp_bise = Icohplist(filename=f"{TEST_FILES_DIR}/cohp/ICOHPLIST.lobster.BiSe") self.icoop_bise = Icohplist( @@ -639,7 +639,7 @@ def test_msonable(self): assert getattr(icohplist_from_dict, attr_name) == attr_value -class TestNciCobiList(unittest.TestCase): +class TestNciCobiList(TestCase): def setUp(self): self.ncicobi = NciCobiList(filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster") self.ncicobi_gz = NciCobiList(filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster.gz") @@ -677,7 +677,7 @@ def test_ncicobilist(self): ) -class TestDoscar(unittest.TestCase): +class TestDoscar(TestCase): def setUp(self): # first for spin polarized version doscar = f"{VASP_OUT_DIR}/DOSCAR.lobster.spin" @@ -1556,7 +1556,7 @@ def test_get_bandstructure(self): assert bs_p_x.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064), abs=1e-2) -class TestLobsterin(unittest.TestCase): +class TestLobsterin(TestCase): def setUp(self): self.Lobsterinfromfile = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.1") self.Lobsterinfromfile2 = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.2") @@ -2030,7 +2030,7 @@ def test_msonable_implementation(self): new_lobsterin.to_json() -class TestBandoverlaps(unittest.TestCase): +class TestBandoverlaps(TestCase): def setUp(self): # test spin-polarized calc and non spinpolarized calc @@ -2189,7 +2189,7 @@ def test_keys(self): assert len(bo_dict_new[Spin.down]["matrices"]) == 73 -class TestGrosspop(unittest.TestCase): +class TestGrosspop(TestCase): def setUp(self): self.grosspop1 = Grosspop(f"{TEST_FILES_DIR}/cohp/GROSSPOP.lobster") diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 8ad8c27ac4..3b985fe033 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -import unittest +from unittest import TestCase import numpy as np import pytest @@ -26,7 +26,7 @@ module_dir = os.path.dirname(os.path.abspath(__file__)) -class TestLobsterNeighbors(unittest.TestCase): +class TestLobsterNeighbors(TestCase): def setUp(self): # test additional conditions first # only consider cation anion bonds From c8861881aa89f4e9f079ed4da03ec5cfc1dc9715 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Mon, 15 Apr 2024 16:26:12 +0800 Subject: [PATCH 040/180] `pyright` fixes for `ext/io/phonon/symmetry/transformations/util/vis/dev_scripts` and improve `io.lobster` (#3757) * Add a CI run that only tests non-optional deps (optimistically) - Hopefully all tests have been written in such a way that they will be skipped if the underlying required package is not found * Install dev deps at least * Try out pyright config for unbound vars only * Add pyright to CI * pyright fix * pyright fix in vis.structure_vtk * fix `utils` * fix `transformations` * fix `symmetry.kpath` * finish fixing `symmetry` * update `pyright` * suppress `pyright` reportMissingModuleSource warning * fix `phonon` * some fixes in `io` * format tweaks of `io.vasp.outputs` * fix unit test for `symmetry.kpath` * fix io.vasp * fix unit test for io.vasp.inputs * fix io.qchem * fix io.pwscf, need input * replace `match.group(i)` with `match[i]` * fix missing ) * fix typo * suppress `reportInvalidTypeForm` * fix io.nwchem and packmol * fix io.lobster.inputs and use snake_case * fix lobsterenv * fix io.icet * fix io.lammps * revert to try/except for speed * set default coords_are_cartesian for pwscf * fix io.gaussian * fix io.fiesta * fix io.cif * fix io.babel * fix io.ase * fix io.cif tests * fix io.feff * fix io.exciting * fix io.abinit * fix io.cp2k * fix io.lobster.outputs * take some (not all) code rabbit suggestion * fix ext * fix dev_scripts * fix logic error * simplify checking * remove some weird constructions in lobster io * pre-commit auto-fixes * resolve code rabbit suggestions * fix more new key issues * fix new_key problems with code simplification * make same logic for del * remove unnecessary `int` before `math.floor/ceil` * Fix lower vs other * tweak error type in io.lobster * fix error type in lobster test * fix nested if * pre-commit auto-fixes * remove DEBUG tag * comment out pyright until all chunks finished * replace runtime error with value error * use walrus op for if match := re.match(...) * remove unused pyright code until ready to enable * rename single-letter m->match * remove unused raw --------- Signed-off-by: Matthew Evans <7916000+ml-evs@users.noreply.github.com> Co-authored-by: Matthew Evans Co-authored-by: Matthew Evans <7916000+ml-evs@users.noreply.github.com> Co-authored-by: JaGeo Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 6e1c87fcef..1a0be9ab9d 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -693,7 +693,7 @@ def setUp(self): self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) - with open(f"{TEST_FILES_DIR}/structure_KF.json") as file: + with open(f"{TEST_FILES_DIR}/structure_KF.json", encoding="utf-8") as file: data = json.load(file) self.structure = Structure.from_dict(data) @@ -1612,11 +1612,11 @@ def test_initialize_from_dict(self): assert lobsterin["basisfunctions"][0] == "Fe 3d 4p 4s" assert lobsterin["basisfunctions"][1] == "Co 3d 4p 4s" assert {*lobsterin} >= {"skipdos", "skipcohp", "skipcoop", "skippopulationanalysis", "skipgrosspopulation"} - with pytest.raises(IOError, match="There are duplicates for the keywords! The program will stop here."): + with pytest.raises(KeyError, match="There are duplicates for the keywords!"): lobsterin2 = Lobsterin({"cohpstartenergy": -15.0, "cohpstartEnergy": -20.0}) lobsterin2 = Lobsterin({"cohpstartenergy": -15.0}) # can only calculate nbands if basis functions are provided - with pytest.raises(IOError, match="No basis functions are provided. The program cannot calculate nbands"): + with pytest.raises(ValueError, match="No basis functions are provided. The program cannot calculate nbands"): lobsterin2._get_nbands(structure=Structure.from_file(f"{VASP_IN_DIR}/POSCAR_Fe3O4")) def test_standard_settings(self): @@ -1752,7 +1752,7 @@ def test_standard_with_energy_range_from_vasprun(self): def test_diff(self): # test diff assert self.Lobsterinfromfile.diff(self.Lobsterinfromfile2)["Different"] == {} - assert self.Lobsterinfromfile.diff(self.Lobsterinfromfile2)["Same"]["COHPSTARTENERGY"] == approx(-15.0) + assert self.Lobsterinfromfile.diff(self.Lobsterinfromfile2)["Same"]["cohpstartenergy"] == approx(-15.0) # test diff in both directions for entry in self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Same"]: @@ -1765,8 +1765,8 @@ def test_diff(self): assert entry in self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Different"] assert ( - self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Different"]["SKIPCOHP"]["lobsterin1"] - == self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Different"]["SKIPCOHP"]["lobsterin2"] + self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Different"]["skipcohp"]["lobsterin1"] + == self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Different"]["skipcohp"]["lobsterin2"] ) def test_dict_functionality(self): @@ -1793,8 +1793,6 @@ def test_read_write_lobsterin(self): lobsterin2 = Lobsterin.from_file(outfile_path) assert lobsterin1.diff(lobsterin2)["Different"] == {} - # TODO: will integer vs float break cohpsteps? - def test_get_basis(self): # get basis functions lobsterin1 = Lobsterin({}) @@ -2590,7 +2588,7 @@ def test_raises(self): self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/Na_hamiltonMatrices.lobster.gz") with pytest.raises( - OSError, - match=r"Please check provided input file, it seems to be empty", + RuntimeError, + match="Please check provided input file, it seems to be empty", ): self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/hamiltonMatrices.lobster") From 34677913e71bcbfea24641ec2714df8aa89b38c8 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 19 Apr 2024 20:14:25 +0800 Subject: [PATCH 041/180] Separate test files by modules and collect test files `csv/cif` into folders (#3746) * collect `csv` * move json files * re replace of json paths * move cif files * move cif files * fix some path * fix another cif path * fix test_cli * move vasp to io.vasp * fix some vasp path * fix some vasp path * pre-commit auto-fixes * define global TEST_DIR * relocate `abinit` * relocate `cp2k` * relocate `exciting` * relocate `feff` * relocate `lammps` * relocate `pwmat` * fix vasp path * relocae qchem * relocate `xtb` * relocate `adf` * relocate `atat/mcsqs` * relocate cssr * relocate fiesta * relocate `nwchem` * relocate `packmol` * relocate `res` * fix some qchem path * fix some `qchem` paths * relocate `xyz/shengbte/wannier90` * fix some paths * relocate bader * relocate chargemol * relocate critic2 * Revert "relocate `xyz/shengbte/wannier90`" This reverts commit cbefbb87492408de7e6c61caae61c3bf946b9571. * relocate shengbte * relocate wannier90 * fix some paths * relocate `boltztrap(2)` * fix bader path * relocate apps.borg * relocate cohp * fix path * fix borg * fix cohp * relocate `chemenv` * relocate `graphs` * relocate openff * relocate `correction_calculator` * fix paths * relocate `fragmenter_files` * relocate `functional_groups` * relocate `grain_boundary` * relocate `magnetic_orderings` * fix paths * relocate fixture nmr for vasp * relocate site_symmetries * relocate space_group_structs * relocate `spectrum_test` * relocate `struct_predictor` * relocate `xr` and `core.trajectory` * relocate compatibility * relocate pwscf * relocate `io` * relocate `gruneisen` * relocate `mcif` * relocate `phonon` * relocate `thermal_displacement_matrices` under `phonon` * relocate `molecule_matcher` * remove seemingly unused `qc_aimd` * revert `new_qchem_files` back to `molecules` * fix path * relocate `structural_change` * remove unused var * fix some qchem path * fix paths * separate `xyz` files by modules * relocate `analysis.cost` * move files out of tests * remove duplicate cohp test file * move `qchem` files out of tests * fix path * move files out of tests * fix path * relocate json for `structure_matcher` * relocate `bandstructure` json * relocate json for `apps.battery` * relocate json for `dos` * fix path * relocate more json * fix typo in path * relocate more json files * fix path * fix typo in path * one last chunk of json * recover deleted json file * fix path * fix typo in path * clean up `molecules` * fix wrong location * fix quasirrho * fix qchem path * fix path * remove seemings unused files from qchem --- tests/io/lobster/test_inputs.py | 242 ++++++++++++++-------------- tests/io/lobster/test_lobsterenv.py | 2 +- 2 files changed, 121 insertions(+), 123 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 1a0be9ab9d..9dc9d75e57 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -34,6 +34,8 @@ from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, PymatgenTest +TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp" + __author__ = "Janine George, Marco Esters" __copyright__ = "Copyright 2017, The Materials Project" __version__ = "0.2" @@ -45,62 +47,62 @@ class TestCohpcar(PymatgenTest): def setUp(self): - self.cohp_bise = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.BiSe.gz") + self.cohp_bise = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.BiSe.gz") self.coop_bise = Cohpcar( - filename=f"{TEST_FILES_DIR}/cohp/COOPCAR.lobster.BiSe.gz", + filename=f"{TEST_DIR}/COOPCAR.lobster.BiSe.gz", are_coops=True, ) - self.cohp_fe = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COOPCAR.lobster.gz") + self.cohp_fe = Cohpcar(filename=f"{TEST_DIR}/COOPCAR.lobster.gz") self.coop_fe = Cohpcar( - filename=f"{TEST_FILES_DIR}/cohp/COOPCAR.lobster.gz", + filename=f"{TEST_DIR}/COOPCAR.lobster.gz", are_coops=True, ) - self.orb = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.orbitalwise.gz") - self.orb_notot = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.notot.orbitalwise.gz") + self.orb = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.orbitalwise.gz") + self.orb_notot = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.notot.orbitalwise.gz") # Lobster 3.1 (Test data is from prerelease of Lobster 3.1) - self.cohp_KF = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.KF.gz") + self.cohp_KF = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.KF.gz") self.coop_KF = Cohpcar( - filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.KF.gz", + filename=f"{TEST_DIR}/COHPCAR.lobster.KF.gz", are_coops=True, ) # example with f electrons - self.cohp_Na2UO4 = Cohpcar(filename=f"{TEST_FILES_DIR}/cohp/COHPCAR.lobster.Na2UO4.gz") + self.cohp_Na2UO4 = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.Na2UO4.gz") self.coop_Na2UO4 = Cohpcar( - filename=f"{TEST_FILES_DIR}/cohp/COOPCAR.lobster.Na2UO4.gz", + filename=f"{TEST_DIR}/COOPCAR.lobster.Na2UO4.gz", are_coops=True, ) self.cobi = Cohpcar( - filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.gz", + filename=f"{TEST_DIR}/COBICAR.lobster.gz", are_cobis=True, ) # 3 center self.cobi2 = Cohpcar( - filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.GeTe", + filename=f"{TEST_DIR}/COBICAR.lobster.GeTe", are_cobis=False, are_multi_center_cobis=True, ) # 4 center self.cobi3 = Cohpcar( - filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.GeTe_4center", are_cobis=False, are_multi_center_cobis=True + filename=f"{TEST_DIR}/COBICAR.lobster.GeTe_4center", are_cobis=False, are_multi_center_cobis=True ) # partially orbital-resolved self.cobi4 = Cohpcar( - filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.GeTe.multi.orbitalwise", + filename=f"{TEST_DIR}/COBICAR.lobster.GeTe.multi.orbitalwise", are_cobis=False, are_multi_center_cobis=True, ) # fully orbital-resolved self.cobi5 = Cohpcar( - filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.GeTe.multi.orbitalwise.full", + filename=f"{TEST_DIR}/COBICAR.lobster.GeTe.multi.orbitalwise.full", are_cobis=False, are_multi_center_cobis=True, ) # spin polarized # fully orbital-resolved self.cobi6 = Cohpcar( - filename=f"{TEST_FILES_DIR}/cohp/COBICAR.lobster.B2H6.spin", are_cobis=False, are_multi_center_cobis=True + filename=f"{TEST_DIR}/COBICAR.lobster.B2H6.spin", are_cobis=False, are_multi_center_cobis=True ) def test_attributes(self): @@ -371,39 +373,39 @@ def test_orbital_resolved_cohp(self): class TestIcohplist(TestCase): def setUp(self): - self.icohp_bise = Icohplist(filename=f"{TEST_FILES_DIR}/cohp/ICOHPLIST.lobster.BiSe") + self.icohp_bise = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster.BiSe") self.icoop_bise = Icohplist( - filename=f"{TEST_FILES_DIR}/cohp/ICOOPLIST.lobster.BiSe", + filename=f"{TEST_DIR}/ICOOPLIST.lobster.BiSe", are_coops=True, ) - self.icohp_fe = Icohplist(filename=f"{TEST_FILES_DIR}/cohp/ICOHPLIST.lobster") + self.icohp_fe = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster") # allow gzipped files - self.icohp_gzipped = Icohplist(filename=f"{TEST_FILES_DIR}/cohp/ICOHPLIST.lobster.gz") + self.icohp_gzipped = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster.gz") self.icoop_fe = Icohplist( - filename=f"{TEST_FILES_DIR}/cohp/ICOHPLIST.lobster", + filename=f"{TEST_DIR}/ICOHPLIST.lobster", are_coops=True, ) # ICOBIs and orbitalwise ICOBILIST.lobster self.icobi_orbitalwise = Icohplist( - filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster", + filename=f"{TEST_DIR}/ICOBILIST.lobster", are_cobis=True, ) self.icobi = Icohplist( - filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster.withoutorbitals", + filename=f"{TEST_DIR}/ICOBILIST.lobster.withoutorbitals", are_cobis=True, ) self.icobi_orbitalwise_spinpolarized = Icohplist( - filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster.spinpolarized", + filename=f"{TEST_DIR}/ICOBILIST.lobster.spinpolarized", are_cobis=True, ) # make sure the correct line is read to check if this is a orbitalwise ICOBILIST self.icobi_orbitalwise_add = Icohplist( - filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster.additional_case", + filename=f"{TEST_DIR}/ICOBILIST.lobster.additional_case", are_cobis=True, ) self.icobi_orbitalwise_spinpolarized_add = Icohplist( - filename=f"{TEST_FILES_DIR}/cohp/ICOBILIST.lobster.spinpolarized.additional_case", + filename=f"{TEST_DIR}/ICOBILIST.lobster.spinpolarized.additional_case", are_cobis=True, ) @@ -641,13 +643,11 @@ def test_msonable(self): class TestNciCobiList(TestCase): def setUp(self): - self.ncicobi = NciCobiList(filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster") - self.ncicobi_gz = NciCobiList(filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster.gz") - self.ncicobi_no_spin = NciCobiList(filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster.nospin") - self.ncicobi_no_spin_wo = NciCobiList( - filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster.nospin.withoutorbitals" - ) - self.ncicobi_wo = NciCobiList(filename=f"{TEST_FILES_DIR}/cohp/NcICOBILIST.lobster.withoutorbitals") + self.ncicobi = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster") + self.ncicobi_gz = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.gz") + self.ncicobi_no_spin = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.nospin") + self.ncicobi_no_spin_wo = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.nospin.withoutorbitals") + self.ncicobi_wo = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.withoutorbitals") def test_ncicobilist(self): assert self.ncicobi.is_spin_polarized @@ -693,7 +693,7 @@ def setUp(self): self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) - with open(f"{TEST_FILES_DIR}/structure_KF.json", encoding="utf-8") as file: + with open(f"{TEST_FILES_DIR}/electronic_structure/dos/structure_KF.json") as file: data = json.load(file) self.structure = Structure.from_dict(data) @@ -849,9 +849,9 @@ def test_is_spin_polarized(self): class TestCharge(PymatgenTest): def setUp(self): - self.charge2 = Charge(filename=f"{TEST_FILES_DIR}/cohp/CHARGE.lobster.MnO") + self.charge2 = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.MnO") # gzipped file - self.charge = Charge(filename=f"{TEST_FILES_DIR}/cohp/CHARGE.lobster.MnO2.gz") + self.charge = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.MnO2.gz") def test_attributes(self): charge_Loewdin = [-1.25, 1.25] @@ -914,25 +914,23 @@ def test_msonable(self): class TestLobsterout(PymatgenTest): def setUp(self): - self.lobsterout_normal = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.normal") + self.lobsterout_normal = Lobsterout(filename=f"{TEST_DIR}/lobsterout.normal") # make sure .gz files are also read correctly - self.lobsterout_normal = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.normal2.gz") + self.lobsterout_normal = Lobsterout(filename=f"{TEST_DIR}/lobsterout.normal2.gz") self.lobsterout_fatband_grosspop_densityofenergies = Lobsterout( - filename=f"{TEST_FILES_DIR}/cohp/lobsterout.fatband_grosspop_densityofenergy" + filename=f"{TEST_DIR}/lobsterout.fatband_grosspop_densityofenergy" ) - self.lobsterout_saveprojection = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.saveprojection") - self.lobsterout_skipping_all = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.skipping_all") - self.lobsterout_twospins = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.twospins") - self.lobsterout_GaAs = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.GaAs") - self.lobsterout_from_projection = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout_from_projection") - self.lobsterout_onethread = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout.onethread") - self.lobsterout_cobi_madelung = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout_cobi_madelung") - self.lobsterout_doscar_lso = Lobsterout(filename=f"{TEST_FILES_DIR}/cohp/lobsterout_doscar_lso") + self.lobsterout_saveprojection = Lobsterout(filename=f"{TEST_DIR}/lobsterout.saveprojection") + self.lobsterout_skipping_all = Lobsterout(filename=f"{TEST_DIR}/lobsterout.skipping_all") + self.lobsterout_twospins = Lobsterout(filename=f"{TEST_DIR}/lobsterout.twospins") + self.lobsterout_GaAs = Lobsterout(filename=f"{TEST_DIR}/lobsterout.GaAs") + self.lobsterout_from_projection = Lobsterout(filename=f"{TEST_DIR}/lobsterout_from_projection") + self.lobsterout_onethread = Lobsterout(filename=f"{TEST_DIR}/lobsterout.onethread") + self.lobsterout_cobi_madelung = Lobsterout(filename=f"{TEST_DIR}/lobsterout_cobi_madelung") + self.lobsterout_doscar_lso = Lobsterout(filename=f"{TEST_DIR}/lobsterout_doscar_lso") # TODO: implement skipping madelung/cobi - self.lobsterout_skipping_cobi_madelung = Lobsterout( - filename=f"{TEST_FILES_DIR}/cohp/lobsterout.skip_cobi_madelung" - ) + self.lobsterout_skipping_cobi_madelung = Lobsterout(filename=f"{TEST_DIR}/lobsterout.skip_cobi_madelung") def test_attributes(self): assert self.lobsterout_normal.basis_functions == [ @@ -1325,7 +1323,7 @@ def test_msonable(self): class TestFatband(PymatgenTest): def setUp(self): self.structure = Vasprun( - filename=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + filename=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", ionic_step_skip=None, ionic_step_offset=0, parse_dos=True, @@ -1336,36 +1334,36 @@ def setUp(self): exception_on_bad_xml=True, ).final_structure self.fatband_SiO2_p_x = Fatband( - filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x", - kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", + filenames=f"{TEST_DIR}/Fatband_SiO2/Test_p_x", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", structure=self.structure, - vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", ) - self.vasprun_SiO2_p_x = Vasprun(filename=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml") + self.vasprun_SiO2_p_x = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml") self.bs_symmline = self.vasprun_SiO2_p_x.get_band_structure(line_mode=True, force_hybrid_mode=True) self.fatband_SiO2_p = Fatband( - filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p", - kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/KPOINTS", - vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/vasprun.xml", + filenames=f"{TEST_DIR}/Fatband_SiO2/Test_p", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p/vasprun.xml", structure=self.structure, ) self.fatband_SiO2_p2 = Fatband( - filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p", - kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/KPOINTS", + filenames=f"{TEST_DIR}/Fatband_SiO2/Test_p", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p/KPOINTS", structure=self.structure, vasprun_file=None, efermi=1.0647039, ) - self.vasprun_SiO2_p = Vasprun(filename=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/vasprun.xml") + self.vasprun_SiO2_p = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p/vasprun.xml") self.bs_symmline2 = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) self.fatband_SiO2_spin = Fatband( - filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin", - kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/KPOINTS", - vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/vasprun.xml", + filenames=f"{TEST_DIR}/Fatband_SiO2/Test_Spin", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/vasprun.xml", structure=self.structure, ) - self.vasprun_SiO2_spin = Vasprun(filename=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/vasprun.xml") + self.vasprun_SiO2_spin = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/vasprun.xml") self.bs_symmline_spin = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) def test_attributes(self): @@ -1420,8 +1418,8 @@ def test_attributes(self): def test_raises(self): with pytest.raises(ValueError, match="vasprun_file or efermi have to be provided"): Fatband( - filenames=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin", - kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_Spin/KPOINTS", + filenames=f"{TEST_DIR}/Fatband_SiO2/Test_Spin", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/KPOINTS", vasprun_file=None, structure=self.structure, ) @@ -1430,22 +1428,22 @@ def test_raises(self): ): self.fatband_SiO2_p_x = Fatband( filenames=[ - f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", - f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", ], - kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", structure=self.structure, ) with pytest.raises(ValueError, match="A structure object has to be provided"): self.fatband_SiO2_p_x = Fatband( filenames=[ - f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", - f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", ], - kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", structure=None, ) @@ -1455,19 +1453,19 @@ def test_raises(self): ): self.fatband_SiO2_p_x = Fatband( filenames=[ - f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", - f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p/FATBAND_si1_3p.lobster", + f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_DIR}/Fatband_SiO2/Test_p/FATBAND_si1_3p.lobster", ], - kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", structure=self.structure, ) with pytest.raises(ValueError, match="No FATBAND files in folder or given"): self.fatband_SiO2_p_x = Fatband( filenames=".", - kpoints_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun_file=f"{TEST_FILES_DIR}/cohp/Fatband_SiO2/Test_p_x/vasprun.xml", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", structure=self.structure, ) @@ -1558,10 +1556,10 @@ def test_get_bandstructure(self): class TestLobsterin(TestCase): def setUp(self): - self.Lobsterinfromfile = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.1") - self.Lobsterinfromfile2 = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.2") - self.Lobsterinfromfile3 = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.3") - self.Lobsterinfromfile4 = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.4.gz") + self.Lobsterinfromfile = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.1") + self.Lobsterinfromfile2 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.2") + self.Lobsterinfromfile3 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.3") + self.Lobsterinfromfile4 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.4.gz") def test_from_file(self): # test read from file @@ -1788,7 +1786,7 @@ def test_dict_functionality(self): def test_read_write_lobsterin(self): outfile_path = tempfile.mkstemp()[1] - lobsterin1 = Lobsterin.from_file(f"{TEST_FILES_DIR}/cohp/lobsterin.1") + lobsterin1 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.1") lobsterin1.write_lobsterin(outfile_path) lobsterin2 = Lobsterin.from_file(outfile_path) assert lobsterin1.diff(lobsterin2)["Different"] == {} @@ -1800,13 +1798,13 @@ def test_get_basis(self): potcar_names = [name["symbol"] for name in potcar.spec] assert lobsterin1.get_basis( - Structure.from_file(f"{TEST_FILES_DIR}/Fe3O4.cif"), + Structure.from_file(f"{TEST_FILES_DIR}/cif/Fe3O4.cif"), potcar_symbols=potcar_names, ) == ["Fe 3d 4p 4s ", "O 2p 2s "] - potcar = Potcar.from_file(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") + potcar = Potcar.from_file(f"{TEST_DIR}/POTCAR.GaAs") potcar_names = [name["symbol"] for name in potcar.spec] assert lobsterin1.get_basis( - Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR.GaAs"), + Structure.from_file(f"{TEST_DIR}/POSCAR.GaAs"), potcar_symbols=potcar_names, ) == ["Ga 3d 4p 4s ", "As 4p 4s "] @@ -1814,7 +1812,7 @@ def test_get_all_possible_basis_functions(self): potcar = Potcar.from_file(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") potcar_names = [name["symbol"] for name in potcar.spec] result = Lobsterin.get_all_possible_basis_functions( - Structure.from_file(f"{TEST_FILES_DIR}/Fe3O4.cif"), + Structure.from_file(f"{TEST_FILES_DIR}/cif/Fe3O4.cif"), potcar_symbols=potcar_names, ) assert result[0] == {"Fe": "3d 4s", "O": "2p 2s"} @@ -1823,7 +1821,7 @@ def test_get_all_possible_basis_functions(self): potcar2 = Potcar.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Fe.gz") Potcar_names2 = [name["symbol"] for name in potcar2.spec] result2 = Lobsterin.get_all_possible_basis_functions( - Structure.from_file(f"{TEST_FILES_DIR}/Fe.cif"), + Structure.from_file(f"{TEST_FILES_DIR}/cif/Fe.cif"), potcar_symbols=Potcar_names2, ) assert result2[0] == {"Fe": "3d 4s"} @@ -1831,7 +1829,7 @@ def test_get_all_possible_basis_functions(self): def test_get_potcar_symbols(self): lobsterin1 = Lobsterin({}) assert lobsterin1._get_potcar_symbols(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") == ["Fe", "O"] - assert lobsterin1._get_potcar_symbols(f"{TEST_FILES_DIR}/cohp/POTCAR.GaAs") == ["Ga_d", "As"] + assert lobsterin1._get_potcar_symbols(f"{TEST_DIR}/POTCAR.GaAs") == ["Ga_d", "As"] def test_write_lobsterin(self): # write lobsterin, read it and compare it @@ -1952,7 +1950,7 @@ def test_write_kpoints(self): # # #without line mode, using a certain grid, isym=0 instead of -1 lobsterin1.write_KPOINTS( - POSCAR_input=f"{TEST_FILES_DIR}/cohp/POSCAR.Li", + POSCAR_input=f"{TEST_DIR}/POSCAR.Li", KPOINTS_output=outfile_path, line_mode=False, from_grid=True, @@ -1961,7 +1959,7 @@ def test_write_kpoints(self): ) kpoint1 = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(f"{TEST_FILES_DIR}/cohp/IBZKPT_3_3_3_Li") + kpoint2 = Kpoints.from_file(f"{TEST_DIR}/IBZKPT_3_3_3_Li") for ikpoint, kpoint in enumerate(kpoint1.kpts): assert self.is_kpoint_in_list( kpoint, @@ -1978,7 +1976,7 @@ def test_write_kpoints(self): ) lobsterin1.write_KPOINTS( - POSCAR_input=f"{TEST_FILES_DIR}/cohp/POSCAR.Li", + POSCAR_input=f"{TEST_DIR}/POSCAR.Li", KPOINTS_output=outfile_path, line_mode=False, from_grid=True, @@ -1987,7 +1985,7 @@ def test_write_kpoints(self): ) kpoint1 = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(f"{TEST_FILES_DIR}/cohp/IBZKPT_2_2_2_Li") + kpoint2 = Kpoints.from_file(f"{TEST_DIR}/IBZKPT_2_2_2_Li") for ikpoint, kpoint in enumerate(kpoint1.kpts): assert self.is_kpoint_in_list( kpoint, @@ -2032,11 +2030,11 @@ class TestBandoverlaps(TestCase): def setUp(self): # test spin-polarized calc and non spinpolarized calc - self.band_overlaps1 = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.1") - self.band_overlaps2 = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.2") + self.band_overlaps1 = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.1") + self.band_overlaps2 = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.2") - self.band_overlaps1_new = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.new.1") - self.band_overlaps2_new = Bandoverlaps(f"{TEST_FILES_DIR}/cohp/bandOverlaps.lobster.new.2") + self.band_overlaps1_new = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.new.1") + self.band_overlaps2_new = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.new.2") def test_attributes(self): # bandoverlapsdict @@ -2189,7 +2187,7 @@ def test_keys(self): class TestGrosspop(TestCase): def setUp(self): - self.grosspop1 = Grosspop(f"{TEST_FILES_DIR}/cohp/GROSSPOP.lobster") + self.grosspop1 = Grosspop(f"{TEST_DIR}/GROSSPOP.lobster") def test_attributes(self): gross_pop_list = self.grosspop1.list_dict_grosspop @@ -2301,7 +2299,7 @@ def test_structure_with_grosspop(self): ], } - new_structure = self.grosspop1.get_structure_with_total_grosspop(f"{TEST_FILES_DIR}/cohp/POSCAR.SiO2") + new_structure = self.grosspop1.get_structure_with_total_grosspop(f"{TEST_DIR}/POSCAR.SiO2") assert_allclose(new_structure.frac_coords, Structure.from_dict(struct_dict).frac_coords) def test_msonable(self): @@ -2370,7 +2368,7 @@ def test_get_all_possible_basis_combinations(self): class TestWavefunction(PymatgenTest): def test_parse_file(self): grid, points, real, imaginary, distance = Wavefunction._parse_file( - f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz" + f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz" ) assert_array_equal([41, 41, 41], grid) assert points[4][0] == approx(0.0000) @@ -2385,8 +2383,8 @@ def test_parse_file(self): def test_set_volumetric_data(self): wave1 = Wavefunction( - filename=f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), ) wave1.set_volumetric_data(grid=wave1.grid, structure=wave1.structure) @@ -2395,32 +2393,32 @@ def test_set_volumetric_data(self): def test_get_volumetricdata_real(self): wave1 = Wavefunction( - filename=f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), ) volumetricdata_real = wave1.get_volumetricdata_real() assert volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) def test_get_volumetricdata_imaginary(self): wave1 = Wavefunction( - filename=f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), ) volumetricdata_imaginary = wave1.get_volumetricdata_imaginary() assert volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) def test_get_volumetricdata_density(self): wave1 = Wavefunction( - filename=f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), ) volumetricdata_density = wave1.get_volumetricdata_density() assert volumetricdata_density.data["total"][0, 0, 0] == approx((-3.0966 * -3.0966) + (-6.45895 * -6.45895)) def test_write_file(self): wave1 = Wavefunction( - filename=f"{TEST_FILES_DIR}/cohp/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_FILES_DIR}/cohp/POSCAR_O.gz"), + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), ) wave1.write_file(filename=f"{self.tmp_path}/wavecar_test.vasp", part="real") assert os.path.isfile("wavecar_test.vasp") @@ -2433,7 +2431,7 @@ def test_write_file(self): class TestSitePotentials(PymatgenTest): def setUp(self) -> None: - self.sitepotential = SitePotential(filename=f"{TEST_FILES_DIR}/cohp/SitePotentials.lobster.perovskite") + self.sitepotential = SitePotential(filename=f"{TEST_DIR}/SitePotentials.lobster.perovskite") def test_attributes(self): assert self.sitepotential.sitepotentials_Loewdin == [-8.77, -17.08, 9.57, 9.57, 8.45] @@ -2446,7 +2444,7 @@ def test_attributes(self): assert self.sitepotential.ewald_splitting == approx(3.14) def test_get_structure(self): - structure = self.sitepotential.get_structure_with_site_potentials(f"{TEST_FILES_DIR}/cohp/POSCAR.perovskite") + structure = self.sitepotential.get_structure_with_site_potentials(f"{TEST_DIR}/POSCAR.perovskite") assert structure.site_properties["Loewdin Site Potentials (eV)"] == [-8.77, -17.08, 9.57, 9.57, 8.45] assert structure.site_properties["Mulliken Site Potentials (eV)"] == [-11.38, -19.62, 11.18, 11.18, 10.09] @@ -2460,7 +2458,7 @@ def test_msonable(self): class TestMadelungEnergies(PymatgenTest): def setUp(self) -> None: - self.madelungenergies = MadelungEnergies(filename=f"{TEST_FILES_DIR}/cohp/MadelungEnergies.lobster.perovskite") + self.madelungenergies = MadelungEnergies(filename=f"{TEST_DIR}/MadelungEnergies.lobster.perovskite") def test_attributes(self): assert self.madelungenergies.madelungenergies_Loewdin == approx(-28.64) @@ -2478,11 +2476,11 @@ def test_msonable(self): class TestLobsterMatrices(PymatgenTest): def setUp(self) -> None: self.hamilton_matrices = LobsterMatrices( - filename=f"{TEST_FILES_DIR}/cohp/Na_hamiltonMatrices.lobster.gz", e_fermi=-2.79650354 + filename=f"{TEST_DIR}/Na_hamiltonMatrices.lobster.gz", e_fermi=-2.79650354 ) - self.transfer_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/C_transferMatrices.lobster.gz") - self.overlap_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/Si_overlapMatrices.lobster.gz") - self.coeff_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/Si_coefficientMatricesLSO1.lobster.gz") + self.transfer_matrices = LobsterMatrices(filename=f"{TEST_DIR}/C_transferMatrices.lobster.gz") + self.overlap_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Si_overlapMatrices.lobster.gz") + self.coeff_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Si_coefficientMatricesLSO1.lobster.gz") def test_attributes(self): # hamilton matrices @@ -2585,10 +2583,10 @@ def test_attributes(self): def test_raises(self): with pytest.raises(ValueError, match="Please provide the fermi energy in eV"): - self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/Na_hamiltonMatrices.lobster.gz") + self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Na_hamiltonMatrices.lobster.gz") with pytest.raises( RuntimeError, match="Please check provided input file, it seems to be empty", ): - self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_FILES_DIR}/cohp/hamiltonMatrices.lobster") + self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_DIR}/hamiltonMatrices.lobster") diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 3b985fe033..713c4b17ef 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -22,7 +22,7 @@ __email__ = "janine.george@uclouvain.be" __date__ = "Jan 14, 2021" -TEST_DIR = f"{TEST_FILES_DIR}/cohp/environments" +TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp/environments" module_dir = os.path.dirname(os.path.abspath(__file__)) From b5a76a3d3744fbb07aee67e7d1ad2216523cdfac Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Tue, 30 Apr 2024 20:27:52 -0400 Subject: [PATCH 042/180] Officially support Python 3.12 and test in CI (#3685) * add python 3.12 to officially supported versions and test it in CI * down pin chgnet>=0.3.0 * fix weird typo nrafo_ew_tstructs * don't depend on tblite above 3.11 since unsupported https://github.com/tblite/tblite/issues/175 * improve TestVasprun.test_standard * drop Lobsterin inerheritance from UserDict, use simple dict instead and modify __getitem__ to get the salient __getitem__ behavior from UserDict * try DotDict as super class for Lobsterin * override Lobsterin.__contains__ to fix on py312 --------- Co-authored-by: JaGeo --- tests/io/lobster/test_inputs.py | 117 +++++++++++++++++--------------- 1 file changed, 61 insertions(+), 56 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 9dc9d75e57..2f790eb2da 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -1556,36 +1556,36 @@ def test_get_bandstructure(self): class TestLobsterin(TestCase): def setUp(self): - self.Lobsterinfromfile = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.1") - self.Lobsterinfromfile2 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.2") - self.Lobsterinfromfile3 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.3") - self.Lobsterinfromfile4 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.4.gz") + self.Lobsterin = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.1") + self.Lobsterin2 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.2") + self.Lobsterin3 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.3") + self.Lobsterin4 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.4.gz") def test_from_file(self): # test read from file - assert self.Lobsterinfromfile["cohpstartenergy"] == approx(-15.0) - assert self.Lobsterinfromfile["cohpendenergy"] == approx(5.0) - assert self.Lobsterinfromfile["basisset"] == "pbeVaspFit2015" - assert self.Lobsterinfromfile["gaussiansmearingwidth"] == approx(0.1) - assert self.Lobsterinfromfile["basisfunctions"][0] == "Fe 3d 4p 4s" - assert self.Lobsterinfromfile["basisfunctions"][1] == "Co 3d 4p 4s" - assert self.Lobsterinfromfile["skipdos"] - assert self.Lobsterinfromfile["skipcohp"] - assert self.Lobsterinfromfile["skipcoop"] - assert self.Lobsterinfromfile["skippopulationanalysis"] - assert self.Lobsterinfromfile["skipgrosspopulation"] + assert self.Lobsterin["cohpstartenergy"] == approx(-15.0) + assert self.Lobsterin["cohpendenergy"] == approx(5.0) + assert self.Lobsterin["basisset"] == "pbeVaspFit2015" + assert self.Lobsterin["gaussiansmearingwidth"] == approx(0.1) + assert self.Lobsterin["basisfunctions"][0] == "Fe 3d 4p 4s" + assert self.Lobsterin["basisfunctions"][1] == "Co 3d 4p 4s" + assert self.Lobsterin["skipdos"] + assert self.Lobsterin["skipcohp"] + assert self.Lobsterin["skipcoop"] + assert self.Lobsterin["skippopulationanalysis"] + assert self.Lobsterin["skipgrosspopulation"] # test if comments are correctly removed - assert self.Lobsterinfromfile == self.Lobsterinfromfile2 + assert self.Lobsterin == self.Lobsterin2 def test_getitem(self): # tests implementation of getitem, should be case independent - assert self.Lobsterinfromfile["COHPSTARTENERGY"] == approx(-15.0) + assert self.Lobsterin["COHPSTARTENERGY"] == approx(-15.0) def test_setitem(self): # test implementation of setitem - self.Lobsterinfromfile["skipCOHP"] = False - assert self.Lobsterinfromfile["skipcohp"] is False + self.Lobsterin["skipCOHP"] = False + assert self.Lobsterin["skipcohp"] is False def test_initialize_from_dict(self): # initialize from dict @@ -1614,7 +1614,7 @@ def test_initialize_from_dict(self): lobsterin2 = Lobsterin({"cohpstartenergy": -15.0, "cohpstartEnergy": -20.0}) lobsterin2 = Lobsterin({"cohpstartenergy": -15.0}) # can only calculate nbands if basis functions are provided - with pytest.raises(ValueError, match="No basis functions are provided. The program cannot calculate nbands"): + with pytest.raises(ValueError, match="No basis functions are provided. The program cannot calculate nbands."): lobsterin2._get_nbands(structure=Structure.from_file(f"{VASP_IN_DIR}/POSCAR_Fe3O4")) def test_standard_settings(self): @@ -1749,40 +1749,41 @@ def test_standard_with_energy_range_from_vasprun(self): def test_diff(self): # test diff - assert self.Lobsterinfromfile.diff(self.Lobsterinfromfile2)["Different"] == {} - assert self.Lobsterinfromfile.diff(self.Lobsterinfromfile2)["Same"]["cohpstartenergy"] == approx(-15.0) + assert self.Lobsterin.diff(self.Lobsterin2)["Different"] == {} + assert self.Lobsterin.diff(self.Lobsterin2)["Same"]["cohpstartenergy"] == approx(-15.0) # test diff in both directions - for entry in self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Same"]: - assert entry in self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Same"] - for entry in self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Same"]: - assert entry in self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Same"] - for entry in self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Different"]: - assert entry in self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Different"] - for entry in self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Different"]: - assert entry in self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Different"] + for entry in self.Lobsterin.diff(self.Lobsterin3)["Same"]: + assert entry in self.Lobsterin3.diff(self.Lobsterin)["Same"] + for entry in self.Lobsterin3.diff(self.Lobsterin)["Same"]: + assert entry in self.Lobsterin.diff(self.Lobsterin3)["Same"] + for entry in self.Lobsterin.diff(self.Lobsterin3)["Different"]: + assert entry in self.Lobsterin3.diff(self.Lobsterin)["Different"] + for entry in self.Lobsterin3.diff(self.Lobsterin)["Different"]: + assert entry in self.Lobsterin.diff(self.Lobsterin3)["Different"] assert ( - self.Lobsterinfromfile.diff(self.Lobsterinfromfile3)["Different"]["skipcohp"]["lobsterin1"] - == self.Lobsterinfromfile3.diff(self.Lobsterinfromfile)["Different"]["skipcohp"]["lobsterin2"] + self.Lobsterin.diff(self.Lobsterin3)["Different"]["skipcohp"]["lobsterin1"] + == self.Lobsterin3.diff(self.Lobsterin)["Different"]["skipcohp"]["lobsterin2"] ) def test_dict_functionality(self): - assert self.Lobsterinfromfile.get("COHPstartEnergy") == -15.0 - assert self.Lobsterinfromfile.get("COHPstartEnergy") == -15.0 - assert self.Lobsterinfromfile.get("COhPstartenergy") == -15.0 - lobsterincopy = self.Lobsterinfromfile.copy() - lobsterincopy.update({"cohpstarteNergy": -10.00}) - assert lobsterincopy["cohpstartenergy"] == -10.0 - lobsterincopy.pop("cohpstarteNergy") - assert "cohpstartenergy" not in lobsterincopy - lobsterincopy.pop("cohpendenergY") - lobsterincopy["cohpsteps"] = 100 - assert lobsterincopy["cohpsteps"] == 100 - before = len(lobsterincopy.items()) - lobsterincopy.popitem() - after = len(lobsterincopy.items()) - assert before != after + for key in ("COHPstartEnergy", "COHPstartEnergy", "COhPstartenergy"): + start_energy = self.Lobsterin.get(key) + assert start_energy == -15.0, f"{start_energy=}, {key=}" + lobsterin_copy = self.Lobsterin.copy() + lobsterin_copy.update({"cohpstarteNergy": -10.00}) + assert lobsterin_copy["cohpstartenergy"] == -10.0 + lobsterin_copy.pop("cohpstarteNergy") + assert "cohpstartenergy" not in lobsterin_copy + lobsterin_copy.pop("cohpendenergY") + lobsterin_copy["cohpsteps"] = 100 + assert lobsterin_copy["cohpsteps"] == 100 + len_before = len(lobsterin_copy.items()) + assert len_before == 9, f"{len_before=}" + lobsterin_copy.popitem() + len_after = len(lobsterin_copy.items()) + assert len_after == len_before - 1 def test_read_write_lobsterin(self): outfile_path = tempfile.mkstemp()[1] @@ -2019,10 +2020,10 @@ def is_kpoint_in_list(self, kpoint, kpointlist, weight, weightlist) -> bool: found += 1 return found == 1 - def test_msonable_implementation(self): + def test_as_from_dict(self): # tests as dict and from dict methods - new_lobsterin = Lobsterin.from_dict(self.Lobsterinfromfile.as_dict()) - assert new_lobsterin == self.Lobsterinfromfile + new_lobsterin = Lobsterin.from_dict(self.Lobsterin.as_dict()) + assert new_lobsterin == self.Lobsterin new_lobsterin.to_json() @@ -2420,13 +2421,17 @@ def test_write_file(self): filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), ) - wave1.write_file(filename=f"{self.tmp_path}/wavecar_test.vasp", part="real") - assert os.path.isfile("wavecar_test.vasp") + real_wavecar_path = f"{self.tmp_path}/real-wavecar.vasp" + wave1.write_file(filename=real_wavecar_path, part="real") + assert os.path.isfile(real_wavecar_path) - wave1.write_file(filename=f"{self.tmp_path}/wavecar_test.vasp", part="imaginary") - assert os.path.isfile("wavecar_test.vasp") - wave1.write_file(filename=f"{self.tmp_path}/density.vasp", part="density") - assert os.path.isfile("density.vasp") + imag_wavecar_path = f"{self.tmp_path}/imaginary-wavecar.vasp" + wave1.write_file(filename=imag_wavecar_path, part="imaginary") + assert os.path.isfile(imag_wavecar_path) + + density_wavecar_path = f"{self.tmp_path}/density-wavecar.vasp" + wave1.write_file(filename=density_wavecar_path, part="density") + assert os.path.isfile(density_wavecar_path) class TestSitePotentials(PymatgenTest): From 37613e5a5e9e75e5dfd750f436f54227a66dbe7e Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sat, 8 Jun 2024 23:39:40 +0800 Subject: [PATCH 043/180] Simplify case handling in `lobster.inputs` (#3848) * replace hard-coded cls name * docstring and type minor cleanup * BREAKING: rename LISTKEYWORDS to LIST_KEYWORDS * add some TODO tags * replace hard-coded cls name * docstring and type minor cleanup * BREAKING: rename LISTKEYWORDS to LIST_KEYWORDS * add TODO tags * fix case in name * pre-commit auto-fixes * properly access classvar * pre-commit auto-fixes * restore from_file position * use PathLike over str * avoid overwrite sha256 import * use elif to avoid repeated check * use set for membership check * simplify dict related magic methods * simplify write_lobsterin and add test for update operation * simplify from_file method * simplify comment remove and duplicate check * remove indentation level * handle multi space in split * fix typo in err msg * tweak ordering * tweak naming and comments * access classvar thru class instead of instance * tweak docstrings * simplify `diff` * add diff params to check * replace element with value * add unit test for __contains__ * revert error msg * add tests for exceptions * simplify magic method with super call * avoid bypassing method resolution order * relocate |= test case * tweak doc * fix issues proposed by Christina * add LOBSTER 5 keywords * use more specific classvar type * use `lobsterin keywords` * update comments as suggested * update supported ISYM value * add unit test for case of diff method * change default ISYM from -1 to 0 * add some missing keywords * set default isym in write_incar/kpoints directly to 0 * replace std lib tempfile with self.tmp_path pytest fixture * improve potential_type doc str --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Shyue Ping Ong Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 91 ++++++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 2f790eb2da..574676952d 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -2,7 +2,6 @@ import json import os -import tempfile from unittest import TestCase import numpy as np @@ -1554,7 +1553,7 @@ def test_get_bandstructure(self): assert bs_p_x.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064), abs=1e-2) -class TestLobsterin(TestCase): +class TestLobsterin(PymatgenTest): def setUp(self): self.Lobsterin = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.1") self.Lobsterin2 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.2") @@ -1562,7 +1561,7 @@ def setUp(self): self.Lobsterin4 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.4.gz") def test_from_file(self): - # test read from file + # Test reading from file assert self.Lobsterin["cohpstartenergy"] == approx(-15.0) assert self.Lobsterin["cohpendenergy"] == approx(5.0) assert self.Lobsterin["basisset"] == "pbeVaspFit2015" @@ -1575,17 +1574,55 @@ def test_from_file(self): assert self.Lobsterin["skippopulationanalysis"] assert self.Lobsterin["skipgrosspopulation"] - # test if comments are correctly removed + # Test if comments are correctly removed assert self.Lobsterin == self.Lobsterin2 - def test_getitem(self): - # tests implementation of getitem, should be case independent + def test_duplicates_from_file(self): + with open(f"{TEST_DIR}/lobsterin.1") as file: + original_file = file.readlines() + + # String and float keywords does not allow duplicates + float_dup_file = original_file.copy() + float_dup_file.append("cohpstartenergy -15.0") + + float_tmp_file = self.tmp_path / "tmp_lobster_in_float" + float_tmp_file.write_text("\n".join(float_dup_file)) + + with pytest.raises(ValueError, match="Same keyword cohpstartenergy twice!"): + _ = Lobsterin.from_file(float_tmp_file) + + # Boolean and list keywords allow duplicates + bool_dup_file = original_file.copy() + bool_dup_file.append("skipdos") + + bool_tmp_file = self.tmp_path / "tmp_lobster_in_bool" + bool_tmp_file.write_text("\n".join(bool_dup_file)) + + _ = Lobsterin.from_file(bool_tmp_file) # no error should be raised + + def test_magic_methods(self): + """Test __getitem__, __setitem__ and __contains__, + should be case independent. + """ + # Test __setitem__ assert self.Lobsterin["COHPSTARTENERGY"] == approx(-15.0) - def test_setitem(self): - # test implementation of setitem + with pytest.raises(KeyError, match="Key hello is currently not available"): + self.Lobsterin["HELLO"] = True + + # Test __getitem__ self.Lobsterin["skipCOHP"] = False assert self.Lobsterin["skipcohp"] is False + assert self.Lobsterin.get("skipCOHP") is False + + with pytest.raises(KeyError, match="key='World' is not available"): + _ = self.Lobsterin["World"] + + # Test __contains__ + assert "COHPSTARTENERGY" in self.Lobsterin + assert "cohpstartenergy" in self.Lobsterin + + assert "helloworld" not in self.Lobsterin def test_initialize_from_dict(self): # initialize from dict @@ -1767,10 +1804,25 @@ def test_diff(self): == self.Lobsterin3.diff(self.Lobsterin)["Different"]["skipcohp"]["lobsterin2"] ) + def test_diff_case_insensitivity(self): + """Test case-insensitivity of diff method.""" + with open(f"{TEST_DIR}/lobsterin.1", encoding="utf-8") as file: + lobsterin_content = file.read() + + lobsterin_content.replace("COHPstartEnergy -15.0", "cohpSTARTEnergy -15.0") + lobsterin_content.replace("skipcohp", "skipCOHP") + + tmp_file_path = self.tmp_path / "tmp_lobster_in" + tmp_file_path.write_text(lobsterin_content) + + lobsterin_diff_case = Lobsterin.from_file(tmp_file_path) + assert self.Lobsterin.diff(lobsterin_diff_case)["Different"] == {} + def test_dict_functionality(self): for key in ("COHPstartEnergy", "COHPstartEnergy", "COhPstartenergy"): start_energy = self.Lobsterin.get(key) assert start_energy == -15.0, f"{start_energy=}, {key=}" + lobsterin_copy = self.Lobsterin.copy() lobsterin_copy.update({"cohpstarteNergy": -10.00}) assert lobsterin_copy["cohpstartenergy"] == -10.0 @@ -1781,12 +1833,20 @@ def test_dict_functionality(self): assert lobsterin_copy["cohpsteps"] == 100 len_before = len(lobsterin_copy.items()) assert len_before == 9, f"{len_before=}" + lobsterin_copy.popitem() len_after = len(lobsterin_copy.items()) assert len_after == len_before - 1 + # Test case sensitivity of |= operator + self.Lobsterin["skipCOHP"] = True # Camel case + assert self.Lobsterin["skipcohp"] is True + + self.Lobsterin["skipcohp"] = False # lower case + assert self.Lobsterin["skipcohp"] is False + def test_read_write_lobsterin(self): - outfile_path = tempfile.mkstemp()[1] + outfile_path = self.tmp_path / "lobsterin_test" lobsterin1 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.1") lobsterin1.write_lobsterin(outfile_path) lobsterin2 = Lobsterin.from_file(outfile_path) @@ -1834,7 +1894,7 @@ def test_get_potcar_symbols(self): def test_write_lobsterin(self): # write lobsterin, read it and compare it - outfile_path = tempfile.mkstemp()[1] + outfile_path = self.tmp_path / "lobsterin_test" lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( f"{VASP_IN_DIR}/POSCAR_Fe3O4", f"{VASP_IN_DIR}/INCAR.lobster", @@ -1847,7 +1907,7 @@ def test_write_lobsterin(self): def test_write_incar(self): # write INCAR and compare - outfile_path = tempfile.mkstemp()[1] + outfile_path = self.tmp_path / "INCAR_test" lobsterin1 = Lobsterin.standard_calculations_from_vasp_files( f"{VASP_IN_DIR}/POSCAR_Fe3O4", f"{VASP_IN_DIR}/INCAR.lobster", @@ -1858,6 +1918,7 @@ def test_write_incar(self): f"{VASP_IN_DIR}/INCAR.lobster3", outfile_path, f"{VASP_IN_DIR}/POSCAR_Fe3O4", + isym=-1, ) incar1 = Incar.from_file(f"{VASP_IN_DIR}/INCAR.lobster3") @@ -1872,8 +1933,8 @@ def test_write_incar(self): def test_write_kpoints(self): # line mode - outfile_path = tempfile.mkstemp()[1] - outfile_path2 = tempfile.mkstemp(prefix="POSCAR")[1] + outfile_path = self.tmp_path / "KPOINTS_test" + outfile_path2 = self.tmp_path / "POSCAR_test" lobsterin1 = Lobsterin({}) # test writing primitive cell lobsterin1.write_POSCAR_with_standard_primitive( @@ -1884,6 +1945,7 @@ def test_write_kpoints(self): POSCAR_input=outfile_path2, KPOINTS_output=outfile_path, kpoints_line_density=58, + isym=-1, ) kpoint = Kpoints.from_file(outfile_path) assert kpoint.num_kpts == 562 @@ -1919,7 +1981,7 @@ def test_write_kpoints(self): assert labels == labels2 # without line mode - lobsterin1.write_KPOINTS(POSCAR_input=outfile_path2, KPOINTS_output=outfile_path, line_mode=False) + lobsterin1.write_KPOINTS(POSCAR_input=outfile_path2, KPOINTS_output=outfile_path, line_mode=False, isym=-1) kpoint = Kpoints.from_file(outfile_path) kpoint2 = Kpoints.from_file(f"{VASP_OUT_DIR}/IBZKPT.lobster") @@ -1937,6 +1999,7 @@ def test_write_kpoints(self): line_mode=False, from_grid=True, input_grid=[6, 6, 3], + isym=-1, ) kpoint = Kpoints.from_file(outfile_path) kpoint2 = Kpoints.from_file(f"{VASP_OUT_DIR}/IBZKPT.lobster") From 5a0397428e982fce127583209570ed262b5915d2 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 21 Jun 2024 23:11:33 +0800 Subject: [PATCH 044/180] Enable more ruff rules including `A`, `DTZ`, `PGH` and more (#3871) * enable ruff rule `PGH` * revert change in lobster.inputs test * fix some PathLike-related issues * more PGH003 fixes * more PGH003 fixes * fix unit test * more fixes for analysis module * finish all PGH * add more ruff rules * fix some `DTZ` datetime errors * fix `A001` * fix `TRY004` * fix more `TRY` errors * tweak format * tweak rule selections * tweak pyproject.toml * disable NPY rule for this PR * revert datetime utc replacement for now * pre-commit auto-fixes * reapply DTZ005 fixes * fix DTZ007 * fix PLR0124: compare with itself * fix unit test * suppress or fix `A002` * fix errors that don't show locally * replace `match.group(x)` with `match[x]` * correct err code * more group(x) changes * relocate no planned ruff families * reverse rule selection * enable rule family `D` for `pymatgen/viz` * restore assert x == x in __eq__ tests and ignore ruff PLR0124 in test files * cif -> CIF in doc str * snake_case var names * revert deletion of test for __eq__ * replace `[0:x]` slicing with `[:x]` * replace one more [0:x] slicing * replace multi-line `dict()` with `{}` * split xcfunc tests --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 574676952d..646a2f70eb 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -1839,10 +1839,10 @@ def test_dict_functionality(self): assert len_after == len_before - 1 # Test case sensitivity of |= operator - self.Lobsterin["skipCOHP"] = True # Camel case + self.Lobsterin |= {"skipCOHP": True} # Camel case assert self.Lobsterin["skipcohp"] is True - self.Lobsterin["skipcohp"] = False # lower case + self.Lobsterin |= {"skipcohp": False} # lower case assert self.Lobsterin["skipcohp"] is False def test_read_write_lobsterin(self): From 4c9cf5a81d65b62382c49d483748d35a382c4e23 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Wed, 26 Jun 2024 14:07:43 -0700 Subject: [PATCH 045/180] Move to src layout. --- __init__.py | 25 + inputs.py | 885 +++++++++ lobster_basis/BASIS_PBE_54_max.yaml | 189 ++ lobster_basis/BASIS_PBE_54_min.yaml | 189 ++ lobster_basis/BASIS_PBE_54_standard.yaml | 189 ++ lobsterenv.py | 1416 ++++++++++++++ outputs.py | 2157 ++++++++++++++++++++++ 7 files changed, 5050 insertions(+) create mode 100644 __init__.py create mode 100644 inputs.py create mode 100644 lobster_basis/BASIS_PBE_54_max.yaml create mode 100644 lobster_basis/BASIS_PBE_54_min.yaml create mode 100644 lobster_basis/BASIS_PBE_54_standard.yaml create mode 100644 lobsterenv.py create mode 100644 outputs.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000..f5e4a5f423 --- /dev/null +++ b/__init__.py @@ -0,0 +1,25 @@ +""" +This package implements modules for input and output to and from Lobster. It +imports the key classes form both lobster.inputs and lobster_outputs to allow most +classes to be simply called as pymatgen.io.lobster.Lobsterin for example, to retain +backwards compatibility. +""" + +from __future__ import annotations + +from .inputs import Lobsterin +from .outputs import ( + Bandoverlaps, + Charge, + Cohpcar, + Doscar, + Fatband, + Grosspop, + Icohplist, + LobsterMatrices, + Lobsterout, + MadelungEnergies, + NciCobiList, + SitePotential, + Wavefunction, +) diff --git a/inputs.py b/inputs.py new file mode 100644 index 0000000000..f3759a38b9 --- /dev/null +++ b/inputs.py @@ -0,0 +1,885 @@ +"""Module for reading LOBSTER input files. +For more information on LOBSTER see www.cohp.de. + +If you use this module, please cite: +J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, +"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +ChemPlusChem 2022, e202200123, +DOI: 10.1002/cplu.202200123. +""" + +from __future__ import annotations + +import itertools +import os +import re +import warnings +from collections import UserDict +from typing import TYPE_CHECKING + +import numpy as np +import spglib +from monty.io import zopen +from monty.json import MSONable +from monty.serialization import loadfn +from pymatgen.core.structure import Structure +from pymatgen.io.vasp import Vasprun +from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar +from pymatgen.symmetry.bandstructure import HighSymmKpath +from pymatgen.util.due import Doi, due + +if TYPE_CHECKING: + from typing import Any, ClassVar, Literal + + from pymatgen.core.composition import Composition + from pymatgen.util.typing import PathLike, Tuple3Ints + from typing_extensions import Self + +MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) + +__author__ = "Janine George, Marco Esters" +__copyright__ = "Copyright 2017, The Materials Project" +__version__ = "0.2" +__maintainer__ = "Janine George" +__email__ = "janinegeorge.ulfen@gmail.com" +__date__ = "Dec 13, 2017" + + +due.cite( + Doi("10.1002/cplu.202200123"), + description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +) + + +class Lobsterin(UserDict, MSONable): + """Handle and generate lobsterin files. + Furthermore, it can also modify INCAR files for LOBSTER, generate KPOINTS files for fatband calculations in LOBSTER, + and generate the standard primitive cells in a POSCAR file that are needed for the fatband calculations. + There are also several standard lobsterin files that can be easily generated. + + Reminder: lobsterin keywords are not case sensitive. + """ + + # These keywords need an additional float suffix + _FLOAT_KEYWORDS: tuple[str, ...] = ( + "COHPstartEnergy", + "COHPendEnergy", + "gaussianSmearingWidth", + "useDecimalPlaces", + "COHPSteps", + "basisRotation", + ) + + # These keywords need an additional string suffix + _STRING_KEYWORDS: tuple[str, ...] = ( + "basisSet", + "cohpGenerator", + "realspaceHamiltonian", + "realspaceOverlap", + "printPAWRealSpaceWavefunction", + "printLCAORealSpaceWavefunction", + "kSpaceCOHP", + "EwaldSum", + ) + + # The keywords themselves (without suffix) can trigger additional functionalities + _BOOLEAN_KEYWORDS: tuple[str, ...] = ( + "saveProjectionToFile", + "skipdos", + "skipcohp", + "skipcoop", + "skipcobi", + "skipMadelungEnergy", + "loadProjectionFromFile", + "printTotalSpilling", + "forceEnergyRange", + "DensityOfEnergy", + "BWDF", + "BWDFCOHP", + "skipPopulationAnalysis", + "skipGrossPopulation", + "userecommendedbasisfunctions", + "skipProjection", + "printLmosOnAtoms", + "writeBasisFunctions", + "writeMatricesToFile", + "noFFTforVisualization", + "RMSp", + "onlyReadVasprun.xml", + "noMemoryMappedFiles", + "skipPAWOrthonormalityTest", + "doNotIgnoreExcessiveBands", + "doNotUseAbsoluteSpilling", + "skipReOrthonormalization", + "forceV1HMatrix", + "useOriginalTetrahedronMethod", + "forceEnergyRange", + "bandwiseSpilling", + "kpointwiseSpilling", + "LSODOS", + "autoRotate", + "doNotOrthogonalizeBasis", + ) + + # These keywords need additional string suffixes. + # They could be used multiple times within one lobsterin. + _LIST_KEYWORDS: tuple[str, ...] = ( + "basisfunctions", + "cohpbetween", + "createFatband", + "customSTOforAtom", + "cobiBetween", + ) + + # Generate {lowered: original} mappings + FLOAT_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _FLOAT_KEYWORDS} + STRING_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _STRING_KEYWORDS} + BOOLEAN_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _BOOLEAN_KEYWORDS} + LIST_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _LIST_KEYWORDS} + + # All known keywords + AVAILABLE_KEYWORDS: ClassVar[dict[str, str]] = { + **FLOAT_KEYWORDS, + **STRING_KEYWORDS, + **BOOLEAN_KEYWORDS, + **LIST_KEYWORDS, + } + + def __init__(self, settingsdict: dict) -> None: + """ + Args: + settingsdict: dict to initialize Lobsterin. + """ + super().__init__() + + # Check for duplicates from case sensitivity + keys = tuple(map(str.lower, settingsdict.keys())) + if len(keys) != len(set(keys)): + raise KeyError("There are duplicates for the keywords!") + + self.update(settingsdict) + + def __setitem__(self, key: str, val: Any) -> None: + """ + Necessary due to the missing case sensitivity of lobsterin + keywords. Also clean the keys and values by stripping white spaces. + + Raises: + KeyError: if keyword is not available. + """ + key = key.strip().lower() + + if key not in type(self).AVAILABLE_KEYWORDS: + raise KeyError(f"Key {key} is currently not available") + + super().__setitem__(key, val.strip() if isinstance(val, str) else val) + + def __getitem__(self, key: str) -> Any: + """To avoid cases sensitivity problems.""" + try: + return super().__getitem__(key.strip().lower()) + + except KeyError as exc: + raise KeyError(f"{key=} is not available") from exc + + def __contains__(self, key: str) -> bool: # type: ignore[override] + """To avoid cases sensitivity problems.""" + return super().__contains__(key.lower().strip()) + + def __delitem__(self, key: str) -> None: + """To avoid cases sensitivity problems.""" + super().__delitem__(key.lower().strip()) + + def diff(self, other: Self) -> dict[str, dict[str, Any]]: + """Compare two Lobsterin and find which parameters are the same. + Similar to the diff method of Incar. + + Args: + other (Lobsterin): Lobsterin object to compare to. + + Returns: + dict: {"Same": same_params, "Different": diff_params} + """ + same_params = {} + diff_params = {} + + # Check self + for k1, v1 in self.items(): + if k1 not in other: + diff_params[k1] = {"lobsterin1": v1, "lobsterin2": None} + + # String keywords + elif isinstance(v1, str): + if v1 != other[k1]: + diff_params[k1] = {"lobsterin1": v1, "lobsterin2": other[k1]} + else: + same_params[k1] = v1 + + # List keywords + elif isinstance(v1, list): + new_set1 = {value.strip().lower() for value in v1} + new_set2 = {value.strip().lower() for value in other[k1]} + if new_set1 != new_set2: + diff_params[k1] = {"lobsterin1": v1, "lobsterin2": other[k1]} + + # Float/boolean keywords + elif v1 != other[k1]: + diff_params[k1] = {"lobsterin1": v1, "lobsterin2": other[k1]} + else: + same_params[k1] = v1 + + # Check other + for k2, v2 in other.items(): + if k2 not in self and k2 not in diff_params: + diff_params[k2] = {"lobsterin1": None, "lobsterin2": v2} + + return {"Same": same_params, "Different": diff_params} + + def write_lobsterin( + self, + path: PathLike = "lobsterin", + overwritedict: dict | None = None, + ) -> None: + """Write a lobsterin file, and recover keys to Camel case. + + Args: + path (str): filename of the output lobsterin file + overwritedict (dict): dict that can be used to update lobsterin, e.g. {"skipdos": True} + """ + # Update previous entries + self |= {} if overwritedict is None else overwritedict + + with open(path, mode="w", encoding="utf-8") as file: + for key in self: + if key in type(self).FLOAT_KEYWORDS or key in type(self).STRING_KEYWORDS: + file.write(f"{type(self).AVAILABLE_KEYWORDS[key]} {self.get(key)}\n") + + elif key in type(self).BOOLEAN_KEYWORDS: + file.write(f"{type(self).BOOLEAN_KEYWORDS[key]}\n") + + elif key in type(self).LIST_KEYWORDS: + for value in self.get(key): # type: ignore[union-attr] + file.write(f"{type(self).LIST_KEYWORDS[key]} {value}\n") + + def as_dict(self) -> dict: + """MSONable dict.""" + dct = dict(self) + dct["@module"] = type(self).__module__ + dct["@class"] = type(self).__name__ + return dct + + @classmethod + def from_dict(cls, dct: dict) -> Self: + """ + Args: + dct (dict): Dict representation. + + Returns: + Lobsterin + """ + return cls({key: val for key, val in dct.items() if key not in {"@module", "@class"}}) + + def _get_nbands(self, structure: Structure) -> int: + """Get number of bands.""" + if self.get("basisfunctions") is None: + raise ValueError("No basis functions are provided. The program cannot calculate nbands.") + + basis_functions: list[str] = [] + for string_basis in self["basisfunctions"]: + string_basis_raw = string_basis.strip().split(" ") + while "" in string_basis_raw: + string_basis_raw.remove("") + for _idx in range(int(structure.composition.element_composition[string_basis_raw[0]])): + basis_functions.extend(string_basis_raw[1:]) + + num_basis_functions = 0 + for basis in basis_functions: + if "s" in basis: + num_basis_functions += 1 + elif "p" in basis: + num_basis_functions += 3 + elif "d" in basis: + num_basis_functions += 5 + elif "f" in basis: + num_basis_functions += 7 + + return int(num_basis_functions) + + def write_INCAR( + self, + incar_input: PathLike = "INCAR", + incar_output: PathLike = "INCAR.lobster", + poscar_input: PathLike = "POSCAR", + isym: Literal[-1, 0] = 0, + further_settings: dict | None = None, + ) -> None: + """Write INCAR file. Will only make the run static, insert NBANDS, + set ISYM=0, LWAVE=True and you have to check for the rest. + + Args: + incar_input (PathLike): path to input INCAR + incar_output (PathLike): path to output INCAR + poscar_input (PathLike): path to input POSCAR + isym (Literal[-1, 0]): ISYM value. + further_settings (dict): A dict can be used to include further settings, e.g. {"ISMEAR":-5} + """ + # Read INCAR from file, which will be modified + incar = Incar.from_file(incar_input) + warnings.warn("Please check your incar_input before using it. This method only changes three settings!") + if isym in {-1, 0}: + incar["ISYM"] = isym + else: + raise ValueError(f"Got {isym=}, must be -1 or 0") + + incar["NSW"] = 0 + incar["LWAVE"] = True + + # Get NBANDS from _get_nbands (use basis set that is inserted) + incar["NBANDS"] = self._get_nbands(Structure.from_file(poscar_input)) + if further_settings is not None: + for key, item in further_settings.items(): + incar[key] = item + incar.write_file(incar_output) + + @staticmethod + def get_basis( + structure: Structure, + potcar_symbols: list[str], + address_basis_file: PathLike | None = None, + ) -> list[str]: + """Get the basis functions from given potcar_symbols, e.g., ["Fe_pv", "Si"]. + + Args: + structure (Structure): Structure object + potcar_symbols: list of potcar symbols + address_basis_file (PathLike): path to the basis file + + Returns: + basis + """ + if address_basis_file is None: + address_basis_file = f"{MODULE_DIR}/lobster_basis/BASIS_PBE_54_standard.yaml" + + atom_types_potcar = [name.split("_")[0] for name in potcar_symbols] + + if set(structure.symbol_set) != set(atom_types_potcar): + raise ValueError("Your POSCAR does not correspond to your POTCAR!") + + basis = loadfn(address_basis_file)["BASIS"] + + basis_functions = [] + list_forin = [] + for idx, name in enumerate(potcar_symbols): + if name not in basis: + raise ValueError( + f"Missing basis information for POTCAR symbol: {name}. Please provide the basis manually." + ) + basis_functions.append(basis[name].split()) + list_forin.append(f"{atom_types_potcar[idx]} {basis[name]}") + return list_forin + + @staticmethod + def get_all_possible_basis_functions( + structure: Structure, + potcar_symbols: list[str], + address_basis_file_min: PathLike | None = None, + address_basis_file_max: PathLike | None = None, + ) -> list[dict]: + """ + Args: + structure: Structure object + potcar_symbols: list of the potcar symbols + address_basis_file_min: path to file with the minimum required basis by the POTCAR + address_basis_file_max: path to file with the largest possible basis of the POTCAR. + + Returns: + list[dict]: Can be used to create new Lobsterin objects in + standard_calculations_from_vasp_files as dict_for_basis + """ + max_basis = Lobsterin.get_basis( + structure=structure, + potcar_symbols=potcar_symbols, + address_basis_file=address_basis_file_max or f"{MODULE_DIR}/lobster_basis/BASIS_PBE_54_max.yaml", + ) + min_basis = Lobsterin.get_basis( + structure=structure, + potcar_symbols=potcar_symbols, + address_basis_file=address_basis_file_min or f"{MODULE_DIR}/lobster_basis/BASIS_PBE_54_min.yaml", + ) + all_basis = get_all_possible_basis_combinations(min_basis=min_basis, max_basis=max_basis) + list_basis_dict = [] + for basis in all_basis: + basis_dict = {} + + for elba in basis: + basplit = elba.split() + basis_dict[basplit[0]] = " ".join(basplit[1:]) + list_basis_dict.append(basis_dict) + return list_basis_dict + + @staticmethod + def write_POSCAR_with_standard_primitive( + POSCAR_input: PathLike = "POSCAR", + POSCAR_output: PathLike = "POSCAR.lobster", + symprec: float = 0.01, + ) -> None: + """Write a POSCAR with the standard primitive cell. + This is needed to arrive at the correct kpath. + + Args: + POSCAR_input (PathLike): Input POSCAR file + POSCAR_output (PathLike): Output POSCAR file + symprec (float): precision to find symmetry + """ + structure = Structure.from_file(POSCAR_input) + kpath = HighSymmKpath(structure, symprec=symprec) + new_structure = kpath.prim + new_structure.to(fmt="POSCAR", filename=POSCAR_output) + + @staticmethod + def write_KPOINTS( + POSCAR_input: PathLike = "POSCAR", + KPOINTS_output: PathLike = "KPOINTS.lobster", + reciprocal_density: int = 100, + isym: Literal[-1, 0] = 0, + from_grid: bool = False, + input_grid: Tuple3Ints = (5, 5, 5), + line_mode: bool = True, + kpoints_line_density: int = 20, + symprec: float = 0.01, + ) -> None: + """Write a gamma-centered KPOINTS file for LOBSTER. + + Args: + POSCAR_input (PathLike): path to POSCAR + KPOINTS_output (PathLike): path to output KPOINTS + reciprocal_density (int): Grid density + isym (Literal[-1, 0]): ISYM value. + from_grid (bool): If True KPOINTS will be generated with the help of a grid given in input_grid. + Otherwise, they will be generated from the reciprocal_density + input_grid (tuple): grid to generate the KPOINTS file + line_mode (bool): If True, band structure will be generated + kpoints_line_density (int): density of the lines in the band structure + symprec (float): precision to determine symmetry + """ + structure = Structure.from_file(POSCAR_input) + if not from_grid: + kpoint_grid = Kpoints.automatic_density_by_vol(structure, reciprocal_density).kpts + mesh = kpoint_grid[0] + else: + mesh = input_grid + + # The following code is taken from SpacegroupAnalyzer + # We need to switch off symmetry here + matrix = structure.lattice.matrix + positions = structure.frac_coords + unique_species: list[Composition] = [] + zs = [] + magmoms = [] + + for species, group in itertools.groupby(structure, key=lambda s: s.species): + if species in unique_species: + ind = unique_species.index(species) + zs.extend([ind + 1] * len(tuple(group))) + else: + unique_species.append(species) + zs.extend([len(unique_species)] * len(tuple(group))) + + for site in structure: + if hasattr(site, "magmom"): + magmoms.append(site.magmom) + elif site.is_ordered and hasattr(site.specie, "spin"): + magmoms.append(site.specie.spin) + else: + magmoms.append(0) + + # For now, we are setting MAGMOM to zero. (Taken from INCAR class) + cell = matrix, positions, zs, magmoms + # TODO: what about this shift? + mapping, grid = spglib.get_ir_reciprocal_mesh(mesh, cell, is_shift=[0, 0, 0]) + + # Get the KPOINTS for the grid + if isym == -1: + kpts = [] + weights = [] + all_labels = [] + for gp in grid: + kpts.append(gp.astype(float) / mesh) + weights.append(float(1)) + all_labels.append("") + + # Time reversal symmetry: k and -k are equivalent + elif isym == 0: + kpts = [] + weights = [] + all_labels = [] + newlist = [list(gp) for gp in list(grid)] + mapping = [] + for gp in newlist: + minus_gp = [-k for k in gp] + if minus_gp in newlist and minus_gp != [0, 0, 0]: + mapping.append(newlist.index(minus_gp)) + else: + mapping.append(newlist.index(gp)) + + for igp, gp in enumerate(newlist): + if mapping[igp] > igp: + kpts.append(np.array(gp).astype(float) / mesh) + weights.append(float(2)) + all_labels.append("") + elif mapping[igp] == igp: + kpts.append(np.array(gp).astype(float) / mesh) + weights.append(float(1)) + all_labels.append("") + + else: + raise ValueError(f"Got {isym=}, must be -1 or 0") + + # Line mode + if line_mode: + kpath = HighSymmKpath(structure, symprec=symprec) + if not np.allclose(kpath.prim.lattice.matrix, structure.lattice.matrix): + raise ValueError( + "You are not using the standard primitive cell. The k-path is not correct. Please generate a " + "standard primitive cell first." + ) + + frac_k_points, labels = kpath.get_kpoints(line_density=kpoints_line_density, coords_are_cartesian=False) + + for k, f in enumerate(frac_k_points): + kpts.append(f) + weights.append(0.0) + all_labels.append(labels[k]) + comment = f"{isym=}, grid: {mesh} plus kpoint path" if line_mode else f"{isym=}, grid: {mesh}" + + kpoints_instance = Kpoints( + comment=comment, + style=Kpoints.supported_modes.Reciprocal, + num_kpts=len(kpts), + kpts=tuple(kpts), + kpts_weights=weights, + labels=all_labels, + ) + + kpoints_instance.write_file(filename=KPOINTS_output) + + @classmethod + def from_file(cls, lobsterin: PathLike) -> Self: + """Create Lobsterin from lobsterin file. + + Args: + lobsterin (PathLike): path to lobsterin. + + Returns: + Lobsterin object + """ + with zopen(lobsterin, mode="rt") as file: + lines = file.read().split("\n") + if not lines: + raise RuntimeError("lobsterin file contains no data.") + + lobsterin_dict: dict[str, Any] = {} + for line in lines: + # Remove comment lines and in-line comments + if line := re.split(r"[!#//]", line)[0].strip(): + # Extract keywords + line_parts = line.replace("\t", " ").strip().split() + if line_parts: + key = line_parts[0].lower() + else: + continue + + # Avoid duplicates for float/string keywords + if (key in cls.FLOAT_KEYWORDS or key in cls.STRING_KEYWORDS) and key in lobsterin_dict: + raise ValueError(f"Same keyword {key} twice!") + + # Parse by keyword type + if key in cls.BOOLEAN_KEYWORDS: + lobsterin_dict[key] = True + + elif key in cls.FLOAT_KEYWORDS: + lobsterin_dict[key] = float(line_parts[1]) + + elif key in cls.STRING_KEYWORDS: + lobsterin_dict[key] = " ".join(line_parts[1:]) + + elif key in cls.LIST_KEYWORDS: + if key in lobsterin_dict: + lobsterin_dict[key].append(" ".join(line_parts[1:])) + else: + lobsterin_dict[key] = [" ".join(line_parts[1:])] + + else: + raise ValueError(f"Invalid {key=}.") + + return cls(lobsterin_dict) + + @staticmethod + def _get_potcar_symbols(POTCAR_input: PathLike) -> list[str]: + """Get the name of the species in the POTCAR. + + Args: + POTCAR_input (PathLike): path to potcar file + + Returns: + list[str]: names of the species + """ + potcar = Potcar.from_file(POTCAR_input) + for pot in potcar: + if pot.potential_type != "PAW": + raise ValueError("Lobster only works with PAW! Use different POTCARs") + + # Warning about a bug in LOBSTER-4.1.0 + with zopen(POTCAR_input, mode="r") as file: + data = file.read() + + if isinstance(data, bytes): + data = data.decode("utf-8") + + if "SHA256" in data or "COPYR" in data: + warnings.warn( + "These POTCARs are not compatible with " + "Lobster up to version 4.1.0." + "\n The keywords SHA256 and COPYR " + "cannot be handled by Lobster" + " \n and will lead to wrong results." + ) + + if potcar.functional != "PBE": + raise RuntimeError("We only have BASIS options for PBE so far") + + return [name["symbol"] for name in potcar.spec] + + @classmethod + def standard_calculations_from_vasp_files( + cls, + POSCAR_input: PathLike = "POSCAR", + INCAR_input: PathLike = "INCAR", + POTCAR_input: PathLike | None = None, + Vasprun_output: PathLike = "vasprun.xml", + dict_for_basis: dict | None = None, + option: str = "standard", + ) -> Self: + """Generate lobsterin with standard settings. + + Args: + POSCAR_input (PathLike): path to POSCAR + INCAR_input (PathLike): path to INCAR + POTCAR_input (PathLike): path to POTCAR + Vasprun_output (PathLike): path to vasprun.xml + dict_for_basis (dict): can be provided: it should look the following: + dict_for_basis={"Fe":'3p 3d 4s 4f', "C": '2s 2p'} and will overwrite all settings from POTCAR_input + option (str): 'standard' will start a normal LOBSTER run where COHPs, COOPs, DOS, CHARGE etc. will be + calculated + 'standard_with_energy_range_from_vasprun' will start a normal LOBSTER run for entire energy range + of VASP static run. vasprun.xml file needs to be in current directory. + 'standard_from_projection' will start a normal LOBSTER run from a projection + 'standard_with_fatband' will do a fatband calculation, run over all orbitals + 'onlyprojection' will only do a projection + 'onlydos' will only calculate a projected dos + 'onlycohp' will only calculate cohp + 'onlycoop' will only calculate coop + 'onlycohpcoop' will only calculate cohp and coop + + Returns: + Lobsterin with standard settings + """ + warnings.warn( + "Always check and test the provided basis functions. The spilling of your Lobster calculation might help" + ) + + if option not in { + "standard", + "standard_from_projection", + "standard_with_fatband", + "standard_with_energy_range_from_vasprun", + "onlyprojection", + "onlydos", + "onlycohp", + "onlycoop", + "onlycobi", + "onlycohpcoop", + "onlycohpcoopcobi", + "onlymadelung", + }: + raise ValueError("The option is not valid!") + + lobsterin_dict: dict[str, Any] = { + # This basis set covers elements up to Lr (Z = 103) + "basisSet": "pbeVaspFit2015", + # Energies around e-fermi + "COHPstartEnergy": -35.0, + "COHPendEnergy": 5.0, + } + + if option in { + "standard", + "standard_with_energy_range_from_vasprun", + "onlycohp", + "onlycoop", + "onlycobi", + "onlycohpcoop", + "onlycohpcoopcobi", + "standard_with_fatband", + }: + # Every interaction with a distance of 6.0 Å is checked + lobsterin_dict["cohpGenerator"] = "from 0.1 to 6.0 orbitalwise" + # Save the projection + lobsterin_dict["saveProjectionToFile"] = True + + if option == "standard_from_projection": + lobsterin_dict["cohpGenerator"] = "from 0.1 to 6.0 orbitalwise" + lobsterin_dict["loadProjectionFromFile"] = True + + elif option == "standard_with_energy_range_from_vasprun": + vasp_run = Vasprun(Vasprun_output) + lobsterin_dict["COHPstartEnergy"] = round( + min(vasp_run.complete_dos.energies - vasp_run.complete_dos.efermi), 4 + ) + lobsterin_dict["COHPendEnergy"] = round( + max(vasp_run.complete_dos.energies - vasp_run.complete_dos.efermi), 4 + ) + lobsterin_dict["COHPSteps"] = len(vasp_run.complete_dos.energies) + + # TODO: add COBI here! might be relevant LOBSTER version + elif option == "onlycohp": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipcoop"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlycoop": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipcohp"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlycohpcoop": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlycohpcoopcobi": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlydos": + lobsterin_dict["skipcohp"] = True + lobsterin_dict["skipcoop"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlyprojection": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipcohp"] = True + lobsterin_dict["skipcoop"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + lobsterin_dict["saveProjectionToFile"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlycobi": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipcohp"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlymadelung": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipcohp"] = True + lobsterin_dict["skipcoop"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + lobsterin_dict["saveProjectionToFile"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + + incar = Incar.from_file(INCAR_input) + + if incar["ISMEAR"] == 0: + lobsterin_dict["gaussianSmearingWidth"] = incar["SIGMA"] + + if incar["ISMEAR"] != 0 and option == "standard_with_fatband": + raise ValueError("ISMEAR has to be 0 for a fatband calculation with Lobster") + + if dict_for_basis is not None: + # dict_for_basis = {"Fe":"3p 3d 4s 4f", "C": "2s 2p"} + # Will just insert this basis and not check with poscar + basis = [f"{key} {value}" for key, value in dict_for_basis.items()] + elif POTCAR_input is not None: + # Get basis functions from POTCAR + potcar_names = cls._get_potcar_symbols(POTCAR_input=POTCAR_input) + + basis = cls.get_basis(structure=Structure.from_file(POSCAR_input), potcar_symbols=potcar_names) + else: + raise ValueError("basis cannot be generated") + + lobsterin_dict["basisfunctions"] = basis + + if option == "standard_with_fatband": + lobsterin_dict["createFatband"] = basis + + return cls(lobsterin_dict) + + +def get_all_possible_basis_combinations(min_basis: list, max_basis: list) -> list[list[str]]: + """Get all possible basis combinations. + + Args: + min_basis: list of basis entries: e.g., ["Si 3p 3s"] + max_basis: list of basis entries: e.g., ["Si 3p 3s"]. + + Returns: + list[list[str]]: all possible combinations of basis functions, e.g. [["Si 3p 3s"]] + """ + max_basis_lists = [x.split() for x in max_basis] + min_basis_lists = [x.split() for x in min_basis] + + # Get all possible basis functions + basis_dict: dict[str, dict] = {} + for iel, el in enumerate(max_basis_lists): + basis_dict[el[0]] = {"fixed": [], "variable": [], "combinations": []} + for basis in el[1:]: + if basis in min_basis_lists[iel]: + basis_dict[el[0]]["fixed"].append(basis) + if basis not in min_basis_lists[iel]: + basis_dict[el[0]]["variable"].append(basis) + for L in range(len(basis_dict[el[0]]["variable"]) + 1): + for subset in itertools.combinations(basis_dict[el[0]]["variable"], L): + basis_dict[el[0]]["combinations"].append(" ".join([el[0]] + basis_dict[el[0]]["fixed"] + list(subset))) + + list_basis = [item["combinations"] for item in basis_dict.values()] + + # Get all combinations + start_basis = list_basis[0] + if len(list_basis) > 1: + for el in list_basis[1:]: + new_start_basis = [] + for elbasis in start_basis: + for elbasis2 in el: + if not isinstance(elbasis, list): + new_start_basis.append([elbasis, elbasis2]) + else: + new_start_basis.append([*elbasis.copy(), elbasis2]) + start_basis = new_start_basis + return start_basis + return [[basis] for basis in start_basis] diff --git a/lobster_basis/BASIS_PBE_54_max.yaml b/lobster_basis/BASIS_PBE_54_max.yaml new file mode 100644 index 0000000000..17a0bd7af5 --- /dev/null +++ b/lobster_basis/BASIS_PBE_54_max.yaml @@ -0,0 +1,189 @@ +BASIS: + Ac: '5f 6d 6p 6s 7s ' + Ag: '4d 5p 5s ' + Ag_pv: '4d 4p 5p 5s ' + Al: '3p 3s ' + Am: '5f 6d 6p 6s 7s ' + Ar: '3p 3s ' + As: '4p 4s ' + As_d: '3d 4p 4s ' + At: '6p 6s ' + Au: '5d 6p 6s ' + B: '2p 2s ' + B_h: '2p 2s ' + B_s: '2p 2s ' + Ba_sv: '5p 5s 6s ' + Be: '2p 2s ' + Be_sv: '1s 2p 2s ' + Bi: '6p 6s ' + Bi_d: '5d 6p 6s ' + Br: '4p 4s ' + C: '2p 2s ' + C_h: '2p 2s ' + C_s: '2p 2s ' + Ca_pv: '3p 4s ' + Ca_sv: '3p 3s 4s ' + Cd: '4d 5p 5s ' + Ce: '4f 5d 5p 5s 6s ' + Ce_3: '5d 5p 5s 6s ' + Ce_h: '4f 5d 5p 5s 6s ' + Cf: '5f 6p 6s 7s ' + Cl: '3p 3s ' + Cl_h: '3p 3s ' + Cm: '5f 6d 6p 6s 7s ' + Co: '3d 4p 4s ' + Co_pv: '3d 3p 4s ' + Co_sv: '3d 3p 3s 4p 4s ' + Cr: '3d 4p 4s ' + Cr_pv: '3d 3p 4s ' + Cr_sv: '3d 3p 3s 4s ' + Cs_sv: '5p 5s 6s ' + Cu: '3d 4p 4s ' + Cu_pv: '3d 3p 4s ' + Dy: '4f 5d 5p 5s 6s ' + Dy_3: '5d 5p 6s ' + Er: '4f 5d 5p 5s 6s ' + Er_2: '5d 5p 6s ' + Er_3: '5d 5p 6s ' + Eu: '4f 5d 5p 5s 6s ' + Eu_2: '5d 5p 6s ' + Eu_3: '5d 5p 6s ' + F: '2p 2s ' + F_h: '2p 2s ' + F_s: '2p 2s ' + Fe: '3d 4p 4s ' + Fe_pv: '3d 3p 4s ' + Fe_sv: '3d 3p 3s 4s ' + Fr_sv: '6p 6s 7s ' + Ga: '4p 4s ' + Ga_d: '3d 4p 4s ' + Ga_h: '3d 4p 4s ' + Gd: '4f 5d 5p 5s 6s ' + Gd_3: '5d 5p 6s ' + Ge: '4p 4s ' + Ge_d: '3d 4p 4s ' + Ge_h: '3d 4p 4s ' + H: '1s ' + H_h: '1s ' + H_s: '1s ' + He: '1s ' + Hf: '5d 6p 6s ' + Hf_pv: '5d 5p 6s ' + Hf_sv: '5d 5p 5s 6s ' + Hg: '5d 6p 6s ' + Ho: '4f 5d 5p 5s 6s ' + Ho_3: '5d 5p 6s ' + I: '5p 5s ' + In: '5p 5s ' + In_d: '4d 5p 5s ' + Ir: '5d 6p 6s ' + K_pv: '3p 4s ' + K_sv: '3p 3s 4s ' + Kr: '4p 4s ' + La: '4f 5d 5p 5s 6s ' + La_s: '4f 5d 5p 6s ' + Li: '2p 2s ' + Li_sv: '1s 2p 2s ' + Lu: '4f 5d 5p 5s 6s ' + Lu_3: '5d 5p 6s ' + Mg: '3p 3s ' + Mg_pv: '2p 3s ' + Mg_sv: '2p 2s 3s ' + Mn: '3d 4p 4s ' + Mn_pv: '3d 3p 4s ' + Mn_sv: '3d 3p 3s 4s ' + Mo: '4d 5p 5s ' + Mo_pv: '4d 4p 5s ' + Mo_sv: '4d 4p 4s 5s ' + N: '2p 2s ' + N_h: '2p 2s ' + N_s: '2p 2s ' + Na: '3p 3s ' + Na_pv: '2p 3s ' + Na_sv: '2p 2s 3s ' + Nb_pv: '4d 4p 5s ' + Nb_sv: '4d 4p 4s 5s ' + Nd: '4f 5d 5p 5s 6s ' + Nd_3: '5d 5p 5s 6s ' + Ne: '2p 2s ' + Ni: '3d 4p 4s ' + Ni_pv: '3d 3p 4s ' + Np: '5f 6d 6p 6s 7s ' + Np_s: '5f 6d 6p 6s 7s ' + O: '2p 2s ' + O_h: '2p 2s ' + O_s: '2p 2s ' + Os: '5d 6p 6s ' + Os_pv: '5d 5p 6s ' + P: '3p 3s ' + P_h: '3p 3s ' + Pa: '5f 6d 6p 6s 7s ' + Pa_s: '5f 6d 6p 7s ' + Pb: '6p 6s ' + Pb_d: '5d 6p 6s ' + Pd: '4d 5p 5s ' + Pd_pv: '4d 4p 5s ' + Pm: '4f 5d 5p 5s 6s ' + Pm_3: '5d 5p 5s 6s ' + Po: '6p 6s ' + Po_d: '5d 6p 6s ' + Pr: '4f 5d 5p 5s 6s ' + Pr_3: '5d 5p 5s 6s ' + Pt: '5d 6p 6s ' + Pt_pv: '5d 5p 6p 6s ' + Pu: '5f 6d 6p 6s 7s ' + Pu_s: '5f 6d 6p 6s 7s ' + Ra_sv: '6p 6s 7s ' + Rb_pv: '4p 5s ' + Rb_sv: '4p 4s 5s ' + Re: '5d 6s ' + Re_pv: '5d 5p 6s ' + Rh: '4d 5p 5s ' + Rh_pv: '4d 4p 5s ' + Rn: '6p 6s ' + Ru: '4d 5p 5s ' + Ru_pv: '4d 4p 5s ' + Ru_sv: '4d 4p 4s 5s ' + S: '3p 3s ' + S_h: '3p 3s ' + Sb: '5p 5s ' + Sc: '3d 4p 4s ' + Sc_sv: '3d 3p 3s 4s ' + Se: '4p 4s ' + Si: '3p 3s ' + Sm: '4f 5d 5p 5s 6s ' + Sm_3: '5d 5p 5s 6s ' + Sn: '5p 5s ' + Sn_d: '4d 5p 5s ' + Sr_sv: '4p 4s 5s ' + Ta: '5d 6p 6s ' + Ta_pv: '5d 5p 6s ' + Tb: '4f 5d 5p 5s 6s ' + Tb_3: '5d 5p 6s ' + Tc: '4d 5p 5s ' + Tc_pv: '4d 4p 5s ' + Tc_sv: '4d 4p 4s 5s ' + Te: '5p 5s ' + Th: '5f 6d 6p 6s 7s ' + Th_s: '5f 6d 6p 7s ' + Ti: '3d 4p 4s ' + Ti_pv: '3d 3p 4s ' + Ti_sv: '3d 3p 3s 4s ' + Tl: '6p 6s ' + Tl_d: '5d 6p 6s ' + Tm: '4f 5d 5p 5s 6s ' + Tm_3: '5d 5p 6s ' + U: '5f 6d 6p 6s 7s ' + U_s: '5f 6d 6p 6s 7s ' + V: '3d 4p 4s ' + V_pv: '3d 3p 4s ' + V_sv: '3d 3p 3s 4s ' + W: '5d 6p 6s ' + W_sv: '5d 5p 5s 6s ' + Xe: '5p 5s ' + Y_sv: '4d 4p 4s 5s ' + Yb: '4f 5d 5p 5s 6s ' + Yb_2: '5d 5p 6s ' + Yb_3: '5d 5p 6s ' + Zn: '3d 4p 4s ' + Zr_sv: '4d 4p 4s 5s ' diff --git a/lobster_basis/BASIS_PBE_54_min.yaml b/lobster_basis/BASIS_PBE_54_min.yaml new file mode 100644 index 0000000000..fd16339c5e --- /dev/null +++ b/lobster_basis/BASIS_PBE_54_min.yaml @@ -0,0 +1,189 @@ +BASIS: + Ac: '6d 6p 6s 7s ' + Ag: '4d 5s ' + Ag_pv: '4d 4p 5s ' + Al: '3p 3s ' + Am: '5f 6d 6p 6s 7s ' + Ar: '3p 3s ' + As: '4p 4s ' + As_d: '3d 4p 4s ' + At: '6p 6s ' + Au: '5d 6s ' + B: '2p 2s ' + B_h: '2p 2s ' + B_s: '2p 2s ' + Ba_sv: '5p 5s 6s ' + Be: '2s ' + Be_sv: '1s 2s ' + Bi: '6p 6s ' + Bi_d: '5d 6p 6s ' + Br: '4p 4s ' + C: '2p 2s ' + C_h: '2p 2s ' + C_s: '2p 2s ' + Ca_pv: '3p 4s ' + Ca_sv: '3p 3s 4s ' + Cd: '4d 5s ' + Ce: '4f 5d 5p 5s 6s ' + Ce_3: '5d 5p 5s 6s ' + Ce_h: '4f 5d 5p 5s 6s ' + Cf: '5f 6p 6s 7s ' + Cl: '3p 3s ' + Cl_h: '3p 3s ' + Cm: '5f 6d 6p 6s 7s ' + Co: '3d 4s ' + Co_pv: '3d 3p 4s ' + Co_sv: '3d 3p 3s 4s ' + Cr: '3d 4s ' + Cr_pv: '3d 3p 4s ' + Cr_sv: '3d 3p 3s 4s ' + Cs_sv: '5p 5s 6s ' + Cu: '3d 4s ' + Cu_pv: '3d 3p 4s ' + Dy: '4f 5d 5p 5s 6s ' + Dy_3: '5d 5p 6s ' + Er: '4f 5d 5p 5s 6s ' + Er_2: '5p 6s ' + Er_3: '5d 5p 6s ' + Eu: '4f 5p 5s 6s ' + Eu_2: '5p 6s ' + Eu_3: '5d 5p 6s ' + F: '2p 2s ' + F_h: '2p 2s ' + F_s: '2p 2s ' + Fe: '3d 4s ' + Fe_pv: '3d 3p 4s ' + Fe_sv: '3d 3p 3s 4s ' + Fr_sv: '6p 6s 7s ' + Ga: '4p 4s ' + Ga_d: '3d 4p 4s ' + Ga_h: '3d 4p 4s ' + Gd: '4f 5d 5p 5s 6s ' + Gd_3: '5d 5p 6s ' + Ge: '4p 4s ' + Ge_d: '3d 4p 4s ' + Ge_h: '3d 4p 4s ' + H: '1s ' + H_h: '1s ' + H_s: '1s ' + He: '1s ' + Hf: '5d 6s ' + Hf_pv: '5d 5p 6s ' + Hf_sv: '5d 5p 5s ' + Hg: '5d 6s ' + Ho: '4f 5d 5p 5s 6s ' + Ho_3: '5d 5p 6s ' + I: '5p 5s ' + In: '5p 5s ' + In_d: '4d 5p 5s ' + Ir: '5d 6s ' + K_pv: '3p 4s ' + K_sv: '3p 3s 4s ' + Kr: '4p 4s ' + La: '5d 5p 5s 6s ' + La_s: '5d 5p 6s ' + Li: '2s ' + Li_sv: '1s 2s ' + Lu: '4f 5d 5p 5s 6s ' + Lu_3: '5d 5p 6s ' + Mg: '3s ' + Mg_pv: '2p 3s ' + Mg_sv: '2p 2s 3s ' + Mn: '3d 4s ' + Mn_pv: '3d 3p 4s ' + Mn_sv: '3d 3p 3s 4s ' + Mo: '4d 5s ' + Mo_pv: '4d 4p 5s ' + Mo_sv: '4d 4p 4s 5s ' + N: '2p 2s ' + N_h: '2p 2s ' + N_s: '2p 2s ' + Na: '3s ' + Na_pv: '2p 3s ' + Na_sv: '2p 2s 3s ' + Nb_pv: '4d 4p 5s ' + Nb_sv: '4d 4p 4s 5s ' + Nd: '4f 5d 5p 5s 6s ' + Nd_3: '5d 5p 5s 6s ' + Ne: '2p 2s ' + Ni: '3d 4s ' + Ni_pv: '3d 3p 4s ' + Np: '5f 6d 6p 6s 7s ' + Np_s: '5f 6d 6p 6s 7s ' + O: '2p 2s ' + O_h: '2p 2s ' + O_s: '2p 2s ' + Os: '5d 6s ' + Os_pv: '5d 5p 6s ' + P: '3p 3s ' + P_h: '3p 3s ' + Pa: '5f 6d 6p 6s 7s ' + Pa_s: '5f 6d 6p 7s ' + Pb: '6p 6s ' + Pb_d: '5d 6p 6s ' + Pd: '4d 5s ' + Pd_pv: '4d 4p 5s ' + Pm: '4f 5d 5p 5s 6s ' + Pm_3: '5d 5p 5s 6s ' + Po: '6p 6s ' + Po_d: '5d 6p 6s ' + Pr: '4f 5d 5p 5s 6s ' + Pr_3: '5d 5p 5s 6s ' + Pt: '5d 6s ' + Pt_pv: '5d 5p 6s ' + Pu: '5f 6d 6p 6s 7s ' + Pu_s: '5f 6d 6p 6s 7s ' + Ra_sv: '6p 6s 7s ' + Rb_pv: '4p 5s ' + Rb_sv: '4p 4s 5s ' + Re: '5d 6s ' + Re_pv: '5d 5p 6s ' + Rh: '4d 5s ' + Rh_pv: '4d 4p 5s ' + Rn: '6p 6s ' + Ru: '4d 5s ' + Ru_pv: '4d 4p 5s ' + Ru_sv: '4d 4p 4s 5s ' + S: '3p 3s ' + S_h: '3p 3s ' + Sb: '5p 5s ' + Sc: '3d 4s ' + Sc_sv: '3d 3p 3s 4s ' + Se: '4p 4s ' + Si: '3p 3s ' + Sm: '4f 5d 5p 5s 6s ' + Sm_3: '5d 5p 5s 6s ' + Sn: '5p 5s ' + Sn_d: '4d 5p 5s ' + Sr_sv: '4p 4s 5s ' + Ta: '5d 6s ' + Ta_pv: '5d 5p 6s ' + Tb: '4f 5d 5p 5s 6s ' + Tb_3: '5d 5p 6s ' + Tc: '4d 5s ' + Tc_pv: '4d 4p 5s ' + Tc_sv: '4d 4p 4s 5s ' + Te: '5p 5s ' + Th: '5f 6d 6p 6s 7s ' + Th_s: '5f 6d 6p 7s ' + Ti: '3d 4s ' + Ti_pv: '3d 3p 4s ' + Ti_sv: '3d 3p 3s 4s ' + Tl: '6p 6s ' + Tl_d: '5d 6p 6s ' + Tm: '4f 5d 5p 5s 6s ' + Tm_3: '5d 5p 6s ' + U: '5f 6d 6p 6s 7s ' + U_s: '5f 6d 6p 6s 7s ' + V: '3d 4s ' + V_pv: '3d 3p 4s ' + V_sv: '3d 3p 3s 4s ' + W: '5d 6s ' + W_sv: '5d 5p 5s 6s ' + Xe: '5p 5s ' + Y_sv: '4d 4p 4s 5s ' + Yb: '4f 5p 5s 6s ' + Yb_2: '5p 6s ' + Yb_3: '5d 5p 6s ' + Zn: '3d 4s ' + Zr_sv: '4d 4p 4s 5s ' diff --git a/lobster_basis/BASIS_PBE_54_standard.yaml b/lobster_basis/BASIS_PBE_54_standard.yaml new file mode 100644 index 0000000000..8583c830fe --- /dev/null +++ b/lobster_basis/BASIS_PBE_54_standard.yaml @@ -0,0 +1,189 @@ +BASIS: + Ac: '5f 6d 6p 6s 7s ' + Ag: '4d 5p 5s ' + Ag_pv: '4d 4p 5p 5s ' + Al: '3p 3s ' + Am: '5f 6d 6p 6s 7s ' + Ar: '3p 3s ' + As: '4p 4s ' + As_d: '3d 4p 4s ' + At: '6p 6s ' + Au: '5d 6p 6s ' + B: '2p 2s ' + B_h: '2p 2s ' + B_s: '2p 2s ' + Ba_sv: '5p 5s 6s ' + Be: '2p 2s ' + Be_sv: '1s 2p 2s ' + Bi: '6p 6s ' + Bi_d: '5d 6p 6s ' + Br: '4p 4s ' + C: '2p 2s ' + C_h: '2p 2s ' + C_s: '2p 2s ' + Ca_pv: '3p 4s ' + Ca_sv: '3p 3s 4s ' + Cd: '4d 5p 5s ' + Ce: '4f 5d 5p 5s 6s ' + Ce_3: '5d 5p 5s 6s ' + Ce_h: '4f 5d 5p 5s 6s ' + Cf: '5f 6p 6s 7s ' + Cl: '3p 3s ' + Cl_h: '3p 3s ' + Cm: '5f 6d 6p 6s 7s ' + Co: '3d 4p 4s ' + Co_pv: '3d 3p 4s ' + Co_sv: '3d 3p 3s 4p 4s ' + Cr: '3d 4p 4s ' + Cr_pv: '3d 3p 4s ' + Cr_sv: '3d 3p 3s 4s ' + Cs_sv: '5p 5s 6s ' + Cu: '3d 4p 4s ' + Cu_pv: '3d 3p 4s ' + Dy: '4f 5d 5p 5s 6s ' + Dy_3: '5d 5p 6s ' + Er: '4f 5d 5p 5s 6s ' + Er_2: '5d 5p 6s ' + Er_3: '5d 5p 6s ' + Eu: '4f 5d 5p 5s 6s ' + Eu_2: '5d 5p 6s ' + Eu_3: '5d 5p 6s ' + F: '2p 2s ' + F_h: '2p 2s ' + F_s: '2p 2s ' + Fe: '3d 4p 4s ' + Fe_pv: '3d 3p 4s ' + Fe_sv: '3d 3p 3s 4s ' + Fr_sv: '6p 6s 7s ' + Ga: '4p 4s ' + Ga_d: '3d 4p 4s ' + Ga_h: '3d 4p 4s ' + Gd: '4f 5d 5p 5s 6s ' + Gd_3: '5d 5p 6s ' + Ge: '4p 4s ' + Ge_d: '3d 4p 4s ' + Ge_h: '3d 4p 4s ' + H: '1s ' + H_h: '1s ' + H_s: '1s ' + He: '1s ' + Hf: '5d 6p 6s ' + Hf_pv: '5d 5p 6s ' + Hf_sv: '5d 5p 5s 6s ' + Hg: '5d 6p 6s ' + Ho: '4f 5d 5p 5s 6s ' + Ho_3: '5d 5p 6s ' + I: '5p 5s ' + In: '5p 5s ' + In_d: '4d 5p 5s ' + Ir: '5d 6p 6s ' + K_pv: '3p 4s ' + K_sv: '3p 3s 4s ' + Kr: '4p 4s ' + La: '4f 5d 5p 5s 6s ' + La_s: '4f 5d 5p 6s ' + Li: '2p 2s ' + Li_sv: '1s 2s 2p ' + Lu: '4f 5d 5p 5s 6s ' + Lu_3: '5d 5p 6s ' + Mg: '3p 3s ' + Mg_pv: '2p 3s ' + Mg_sv: '2p 2s 3s ' + Mn: '3d 4p 4s ' + Mn_pv: '3d 3p 4s ' + Mn_sv: '3d 3p 3s 4s ' + Mo: '4d 5p 5s ' + Mo_pv: '4d 4p 5s ' + Mo_sv: '4d 4p 4s 5s ' + N: '2p 2s ' + N_h: '2p 2s ' + N_s: '2p 2s ' + Na: '3p 3s ' + Na_pv: '2p 3s ' + Na_sv: '2p 2s 3s ' + Nb_pv: '4d 4p 5s ' + Nb_sv: '4d 4p 4s 5s ' + Nd: '4f 5d 5p 5s 6s ' + Nd_3: '5d 5p 5s 6s ' + Ne: '2p 2s ' + Ni: '3d 4p 4s ' + Ni_pv: '3d 3p 4s ' + Np: '5f 6d 6p 6s 7s ' + Np_s: '5f 6d 6p 6s 7s ' + O: '2p 2s ' + O_h: '2p 2s ' + O_s: '2p 2s ' + Os: '5d 6p 6s ' + Os_pv: '5d 5p 6s ' + P: '3p 3s ' + P_h: '3p 3s ' + Pa: '5f 6d 6p 6s 7s ' + Pa_s: '5f 6d 6p 7s ' + Pb: '6p 6s ' + Pb_d: '5d 6p 6s ' + Pd: '4d 5p 5s ' + Pd_pv: '4d 4p 5s ' + Pm: '4f 5d 5p 5s 6s ' + Pm_3: '5d 5p 5s 6s ' + Po: '6p 6s ' + Po_d: '5d 6p 6s ' + Pr: '4f 5d 5p 5s 6s ' + Pr_3: '5d 5p 5s 6s ' + Pt: '5d 6p 6s ' + Pt_pv: '5d 5p 6p 6s ' + Pu: '5f 6d 6p 6s 7s ' + Pu_s: '5f 6d 6p 6s 7s ' + Ra_sv: '6p 6s 7s ' + Rb_pv: '4p 5s ' + Rb_sv: '4p 4s 5s ' + Re: '5d 6s ' + Re_pv: '5d 5p 6s ' + Rh: '4d 5p 5s ' + Rh_pv: '4d 4p 5s ' + Rn: '6p 6s ' + Ru: '4d 5p 5s ' + Ru_pv: '4d 4p 5s ' + Ru_sv: '4d 4p 4s 5s ' + S: '3p 3s ' + S_h: '3p 3s ' + Sb: '5p 5s ' + Sc: '3d 4p 4s ' + Sc_sv: '3d 3p 3s 4s ' + Se: '4p 4s ' + Si: '3p 3s ' + Sm: '4f 5d 5p 5s 6s ' + Sm_3: '5d 5p 5s 6s ' + Sn: '5p 5s ' + Sn_d: '4d 5p 5s ' + Sr_sv: '4p 4s 5s ' + Ta: '5d 6p 6s ' + Ta_pv: '5d 5p 6s ' + Tb: '4f 5d 5p 5s 6s ' + Tb_3: '5d 5p 6s ' + Tc: '4d 5p 5s ' + Tc_pv: '4d 4p 5s ' + Tc_sv: '4d 4p 4s 5s ' + Te: '5p 5s ' + Th: '5f 6d 6p 6s 7s ' + Th_s: '5f 6d 6p 7s ' + Ti: '3d 4p 4s ' + Ti_pv: '3d 3p 4s ' + Ti_sv: '3d 3p 3s 4s ' + Tl: '6p 6s ' + Tl_d: '5d 6p 6s ' + Tm: '4f 5d 5p 5s 6s ' + Tm_3: '5d 5p 6s ' + U: '5f 6d 6p 6s 7s ' + U_s: '5f 6d 6p 6s 7s ' + V: '3d 4p 4s ' + V_pv: '3d 3p 4s ' + V_sv: '3d 3p 3s 4s ' + W: '5d 6p 6s ' + W_sv: '5d 5p 5s 6s ' + Xe: '5p 5s ' + Y_sv: '4d 4p 4s 5s ' + Yb: '4f 5d 5p 5s 6s ' + Yb_2: '5d 5p 6s ' + Yb_3: '5d 5p 6s ' + Zn: '3d 4p 4s ' + Zr_sv: '4d 4p 4s 5s ' diff --git a/lobsterenv.py b/lobsterenv.py new file mode 100644 index 0000000000..7982b025c9 --- /dev/null +++ b/lobsterenv.py @@ -0,0 +1,1416 @@ +""" +This module provides classes to perform analyses of +the local environments (e.g., finding near neighbors) +of single sites in molecules and structures based on +bonding analysis with Lobster. +If you use this module, please cite: +J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, +"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +ChemPlusChem 2022, e202200123, +DOI: 10.1002/cplu.202200123. +""" + +from __future__ import annotations + +import collections +import copy +import math +import tempfile +from typing import TYPE_CHECKING, NamedTuple + +import numpy as np +from monty.dev import deprecated +from pymatgen.analysis.bond_valence import BVAnalyzer +from pymatgen.analysis.chemenv.coordination_environments.coordination_geometry_finder import LocalGeometryFinder +from pymatgen.analysis.chemenv.coordination_environments.structure_environments import LightStructureEnvironments +from pymatgen.analysis.local_env import NearNeighbors +from pymatgen.electronic_structure.cohp import CompleteCohp +from pymatgen.electronic_structure.core import Spin +from pymatgen.electronic_structure.plotter import CohpPlotter +from pymatgen.io.lobster import Charge, Icohplist +from pymatgen.util.due import Doi, due + +if TYPE_CHECKING: + from pymatgen.core import Structure + from pymatgen.core.periodic_table import Element + from typing_extensions import Self + +__author__ = "Janine George" +__copyright__ = "Copyright 2021, The Materials Project" +__version__ = "1.0" +__maintainer__ = "J. George" +__email__ = "janinegeorge.ulfen@gmail.com" +__status__ = "Production" +__date__ = "February 2, 2021" + +due.cite( + Doi("10.1002/cplu.202200123"), + description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +) + + +class LobsterNeighbors(NearNeighbors): + """ + This class combines capabilities from LocalEnv and ChemEnv to determine coordination environments based on + bonding analysis. + """ + + def __init__( + self, + structure: Structure, + filename_icohp: str | None = "ICOHPLIST.lobster", + obj_icohp: Icohplist | None = None, + are_coops: bool = False, + are_cobis: bool = False, + valences: list[float] | None = None, + limits: tuple[float, float] | None = None, + additional_condition: int = 0, + only_bonds_to: list[str] | None = None, + perc_strength_icohp: float = 0.15, + noise_cutoff: float = 0.1, + valences_from_charges: bool = False, + filename_charge: str | None = None, + obj_charge: Charge | None = None, + which_charge: str = "Mulliken", + adapt_extremum_to_add_cond: bool = False, + add_additional_data_sg: bool = False, + filename_blist_sg1: str | None = None, + filename_blist_sg2: str | None = None, + id_blist_sg1: str = "ICOOP", + id_blist_sg2: str = "ICOBI", + ) -> None: + """ + Args: + filename_icohp: (str) Path to ICOHPLIST.lobster or ICOOPLIST.lobster or ICOBILIST.lobster + obj_icohp: Icohplist object + structure: (Structure) typically constructed by Structure.from_file("POSCAR") + are_coops: (bool) if True, the file is a ICOOPLIST.lobster and not a ICOHPLIST.lobster; only tested for + ICOHPLIST.lobster so far + are_cobis: (bool) if True, the file is a ICOBILIST.lobster and not a ICOHPLIST.lobster + valences: (list[float]): gives valence/charge for each element + limits (tuple[float, float] | None): limit to decide which ICOHPs (ICOOP or ICOBI) should be considered + additional_condition (int): Additional condition that decides which kind of bonds will be considered + NO_ADDITIONAL_CONDITION = 0 + ONLY_ANION_CATION_BONDS = 1 + NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2 + ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3 + ONLY_ELEMENT_TO_OXYGEN_BONDS = 4 + DO_NOT_CONSIDER_ANION_CATION_BONDS=5 + ONLY_CATION_CATION_BONDS=6 + only_bonds_to: (list[str]) will only consider bonds to certain elements (e.g. ["O"] for oxygen) + perc_strength_icohp: if no limits are given, this will decide which icohps will still be considered ( + relative to + the strongest ICOHP (ICOOP or ICOBI) + noise_cutoff: if provided hardcodes the lower limit of icohps considered + valences_from_charges: if True and path to CHARGE.lobster is provided, will use Lobster charges ( + Mulliken) instead of valences + filename_charge: (str) Path to Charge.lobster + obj_charge: Charge object + which_charge: (str) "Mulliken" or "Loewdin" + adapt_extremum_to_add_cond: (bool) will adapt the limits to only focus on the bonds determined by the + additional condition + add_additional_data_sg: (bool) will add the information from filename_add_bondinglist_sg1, + filename_blist_sg1: (str) Path to additional ICOOP, ICOBI data for structure graphs + filename_blist_sg2: (str) Path to additional ICOOP, ICOBI data for structure graphs + id_blist_sg1: (str) Identity of data in filename_blist_sg1, + e.g. "icoop" or "icobi" + id_blist_sg2: (str) Identity of data in filename_blist_sg2, + e.g. "icoop" or "icobi". + """ + if filename_icohp is not None: + self.ICOHP = Icohplist(are_coops=are_coops, are_cobis=are_cobis, filename=filename_icohp) + elif obj_icohp is not None: + self.ICOHP = obj_icohp + else: + raise ValueError("Please provide either filename_icohp or obj_icohp") + self.Icohpcollection = self.ICOHP.icohpcollection + self.structure = structure + self.limits = limits + self.only_bonds_to = only_bonds_to + self.adapt_extremum_to_add_cond = adapt_extremum_to_add_cond + self.are_coops = are_coops + self.are_cobis = are_cobis + self.add_additional_data_sg = add_additional_data_sg + self.filename_blist_sg1 = filename_blist_sg1 + self.filename_blist_sg2 = filename_blist_sg2 + self.noise_cutoff = noise_cutoff + + allowed_arguments = ["icoop", "icobi"] + if id_blist_sg1.lower() not in allowed_arguments or id_blist_sg2.lower() not in allowed_arguments: + raise ValueError("Algorithm can only work with ICOOPs, ICOBIs") + self.id_blist_sg1 = id_blist_sg1 + self.id_blist_sg2 = id_blist_sg2 + if add_additional_data_sg: + if self.id_blist_sg1.lower() == "icoop": + are_coops_id1 = True + are_cobis_id1 = False + elif self.id_blist_sg1.lower() == "icobi": + are_coops_id1 = False + are_cobis_id1 = True + else: + raise ValueError("only icoops and icobis can be added") + self.bonding_list_1 = Icohplist( + filename=self.filename_blist_sg1, + are_coops=are_coops_id1, + are_cobis=are_cobis_id1, + ) + + if self.id_blist_sg2.lower() == "icoop": + are_coops_id2 = True + are_cobis_id2 = False + elif self.id_blist_sg2.lower() == "icobi": + are_coops_id2 = False + are_cobis_id2 = True + else: + raise ValueError("only icoops and icobis can be added") + + self.bonding_list_2 = Icohplist( + filename=self.filename_blist_sg2, + are_coops=are_coops_id2, + are_cobis=are_cobis_id2, + ) + + # will check if the additional condition is correctly delivered + if additional_condition not in range(7): + raise ValueError(f"Unexpected {additional_condition=}, must be one of {list(range(7))}") + self.additional_condition = additional_condition + + # will read in valences, will prefer manual setting of valences + self.valences: list[float] | None + if valences is None: + if valences_from_charges and filename_charge is not None: + chg = Charge(filename=filename_charge) + if which_charge == "Mulliken": + self.valences = chg.Mulliken + elif which_charge == "Loewdin": + self.valences = chg.Loewdin + elif valences_from_charges and obj_charge is not None: + chg = obj_charge + if which_charge == "Mulliken": + self.valences = chg.Mulliken + elif which_charge == "Loewdin": + self.valences = chg.Loewdin + else: + bv_analyzer = BVAnalyzer() + try: + self.valences = bv_analyzer.get_valences(structure=self.structure) + except ValueError: + self.valences = None + if additional_condition in [1, 3, 5, 6]: + raise ValueError( + "Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work" + ) + else: + self.valences = valences + if np.allclose(self.valences or [], np.zeros_like(self.valences)) and additional_condition in [1, 3, 5, 6]: + raise ValueError("All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work") + + if limits is None: + self.lowerlimit = self.upperlimit = None + else: + self.lowerlimit, self.upperlimit = limits + + # will evaluate coordination environments + self._evaluate_ce( + lowerlimit=self.lowerlimit, + upperlimit=self.upperlimit, + only_bonds_to=only_bonds_to, + additional_condition=self.additional_condition, + perc_strength_icohp=perc_strength_icohp, + adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, + ) + + @property + def structures_allowed(self) -> bool: + """Whether this NearNeighbors class can be used with Structure objects?""" + return True + + @property + def molecules_allowed(self) -> bool: + """Whether this NearNeighbors class can be used with Molecule objects?""" + return False + + @property + def anion_types(self) -> set[Element]: + """The types of anions present in crystal structure as a set. + + Returns: + set[Element]: describing anions in the crystal structure. + """ + if self.valences is None: + raise ValueError("No cations and anions defined") + + anion_species = [] + for site, val in zip(self.structure, self.valences): + if val < 0.0: + anion_species.append(site.specie) + + return set(anion_species) + + @deprecated(anion_types) + def get_anion_types(self): + return self.anion_types + + def get_nn_info(self, structure: Structure, n: int, use_weights: bool = False) -> dict: # type: ignore[override] + """Get coordination number, CN, of site with index n in structure. + + Args: + structure (Structure): input structure. + n (int): index of site for which to determine CN. + use_weights (bool): flag indicating whether (True) + to use weights for computing the coordination number + or not (False, default: each coordinated site has equal + weight). + True is not implemented for LobsterNeighbors + + Raises: + ValueError: if use_weights is True or if structure passed and structure used to + initialize LobsterNeighbors have different lengths. + + Returns: + dict[str, Any]: coordination number and a list of nearest neighbors. + """ + if use_weights: + raise ValueError("LobsterEnv cannot use weights") + if len(structure) != len(self.structure): + raise ValueError( + f"Length of structure ({len(structure)}) and LobsterNeighbors ({len(self.structure)}) differ" + ) + return self.sg_list[n] # type: ignore[return-value] + + def get_light_structure_environment(self, only_cation_environments=False, only_indices=None): + """Get a LobsterLightStructureEnvironments object + if the structure only contains coordination environments smaller 13. + + Args: + only_cation_environments: only data for cations will be returned + only_indices: will only evaluate the list of isites in this list + + Returns: + LobsterLightStructureEnvironments + """ + lgf = LocalGeometryFinder() + lgf.setup_structure(structure=self.structure) + list_ce_symbols = [] + list_csm = [] + list_permut = [] + for ival, _neigh_coords in enumerate(self.list_coords): + if (len(_neigh_coords)) > 13: + raise ValueError("Environment cannot be determined. Number of neighbors is larger than 13.") + # to avoid problems if _neigh_coords is empty + if _neigh_coords != []: + lgf.setup_local_geometry(isite=ival, coords=_neigh_coords, optimization=2) + cncgsm = lgf.get_coordination_symmetry_measures(optimization=2) + list_ce_symbols.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[0]) + list_csm.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["csm_wcs_ctwcc"]) + list_permut.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["indices"]) + else: + list_ce_symbols.append(None) + list_csm.append(None) + list_permut.append(None) + + if only_indices is None: + if not only_cation_environments: + lse = LobsterLightStructureEnvironments.from_Lobster( + list_ce_symbol=list_ce_symbols, + list_csm=list_csm, + list_permutation=list_permut, + list_neighsite=self.list_neighsite, + list_neighisite=self.list_neighisite, + structure=self.structure, + valences=self.valences, + ) + else: + new_list_ce_symbols = [] + new_list_csm = [] + new_list_permut = [] + new_list_neighsite = [] + new_list_neighisite = [] + + for ival, val in enumerate(self.valences): + if val >= 0.0: + new_list_ce_symbols.append(list_ce_symbols[ival]) + new_list_csm.append(list_csm[ival]) + new_list_permut.append(list_permut[ival]) + new_list_neighisite.append(self.list_neighisite[ival]) + new_list_neighsite.append(self.list_neighsite[ival]) + else: + new_list_ce_symbols.append(None) + new_list_csm.append(None) + new_list_permut.append([]) + new_list_neighisite.append([]) + new_list_neighsite.append([]) + + lse = LobsterLightStructureEnvironments.from_Lobster( + list_ce_symbol=new_list_ce_symbols, + list_csm=new_list_csm, + list_permutation=new_list_permut, + list_neighsite=new_list_neighsite, + list_neighisite=new_list_neighisite, + structure=self.structure, + valences=self.valences, + ) + else: + new_list_ce_symbols = [] + new_list_csm = [] + new_list_permut = [] + new_list_neighsite = [] + new_list_neighisite = [] + + for isite, _site in enumerate(self.structure): + if isite in only_indices: + new_list_ce_symbols.append(list_ce_symbols[isite]) + new_list_csm.append(list_csm[isite]) + new_list_permut.append(list_permut[isite]) + new_list_neighisite.append(self.list_neighisite[isite]) + new_list_neighsite.append(self.list_neighsite[isite]) + else: + new_list_ce_symbols.append(None) + new_list_csm.append(None) + new_list_permut.append([]) + new_list_neighisite.append([]) + new_list_neighsite.append([]) + + lse = LobsterLightStructureEnvironments.from_Lobster( + list_ce_symbol=new_list_ce_symbols, + list_csm=new_list_csm, + list_permutation=new_list_permut, + list_neighsite=new_list_neighsite, + list_neighisite=new_list_neighisite, + structure=self.structure, + valences=self.valences, + ) + + return lse + + def get_info_icohps_to_neighbors(self, isites=None, onlycation_isites=True): + """Get information on the icohps of neighbors for certain sites as identified by their site id. + This is useful for plotting the relevant cohps of a site in the structure. + (could be ICOOPLIST.lobster or ICOHPLIST.lobster or ICOBILIST.lobster). + + Args: + isites: list of site ids. If isite==None, all isites will be used to add the icohps of the neighbors + onlycation_isites: if True and if isite==None, it will only analyse the sites of the cations + + Returns: + ICOHPNeighborsInfo + """ + if self.valences is None and onlycation_isites: + raise ValueError("No valences are provided") + if isites is None: + if onlycation_isites: + isites = [i for i in range(len(self.structure)) if self.valences[i] >= 0.0] + else: + isites = list(range(len(self.structure))) + + summed_icohps = 0.0 + list_icohps = [] + number_bonds = 0 + labels = [] + atoms = [] + final_isites = [] + for ival, _site in enumerate(self.structure): + if ival in isites: + for keys, icohpsum in zip(self.list_keys[ival], self.list_icohps[ival]): + summed_icohps += icohpsum + list_icohps.append(icohpsum) + labels.append(keys) + atoms.append( + [ + self.Icohpcollection._list_atom1[int(keys) - 1], + self.Icohpcollection._list_atom2[int(keys) - 1], + ] + ) + number_bonds += 1 + final_isites.append(ival) + return ICOHPNeighborsInfo(summed_icohps, list_icohps, number_bonds, labels, atoms, final_isites) + + def plot_cohps_of_neighbors( + self, + path_to_cohpcar: str | None = "COHPCAR.lobster", + obj_cohpcar: CompleteCohp | None = None, + isites: list[int] | None = None, + onlycation_isites: bool = True, + only_bonds_to: list[str] | None = None, + per_bond: bool = False, + summed_spin_channels: bool = False, + xlim=None, + ylim=(-10, 6), + integrated: bool = False, + ): + """ + Will plot summed cohps or cobis or coops + (please be careful in the spin polarized case (plots might overlap (exactly!)). + + Args: + path_to_cohpcar: str, path to COHPCAR or COOPCAR or COBICAR + obj_cohpcar: CompleteCohp object + isites: list of site ids, if isite==[], all isites will be used to add the icohps of the neighbors + onlycation_isites: bool, will only use cations, if isite==[] + only_bonds_to: list of str, only anions in this list will be considered + per_bond: bool, will lead to a normalization of the plotted COHP per number of bond if True, + otherwise the sum + will be plotted + xlim: list of float, limits of x values + ylim: list of float, limits of y values + integrated: bool, if true will show integrated cohp instead of cohp + + Returns: + plt of the cohps or coops or cobis + """ + # include COHPPlotter and plot a sum of these COHPs + # might include option to add Spin channels + # implement only_bonds_to + cp = CohpPlotter(are_cobis=self.are_cobis, are_coops=self.are_coops) + + plotlabel, summed_cohp = self.get_info_cohps_to_neighbors( + path_to_cohpcar, + obj_cohpcar, + isites, + only_bonds_to, + onlycation_isites, + per_bond, + summed_spin_channels=summed_spin_channels, + ) + + cp.add_cohp(plotlabel, summed_cohp) + ax = cp.get_plot(integrated=integrated) + if xlim is not None: + ax.set_xlim(xlim) + + if ylim is not None: + ax.set_ylim(ylim) + + return ax + + def get_info_cohps_to_neighbors( + self, + path_to_cohpcar: str | None = "COHPCAR.lobster", + obj_cohpcar: CompleteCohp | None = None, + isites: list[int] | None = None, + only_bonds_to: list[str] | None = None, + onlycation_isites: bool = True, + per_bond: bool = True, + summed_spin_channels: bool = False, + ): + """Get info about the cohps (coops or cobis) as a summed cohp object and a label + from all sites mentioned in isites with neighbors. + + Args: + path_to_cohpcar: str, path to COHPCAR or COOPCAR or COBICAR + obj_cohpcar: CompleteCohp object + isites: list of int that indicate the number of the site + only_bonds_to: list of str, e.g. ["O"] to only show cohps of anything to oxygen + onlycation_isites: if isites=None, only cation sites will be returned + per_bond: will normalize per bond + summed_spin_channels: will sum all spin channels + + Returns: + str: label for COHP, CompleteCohp object which describes all cohps (coops or cobis) + of the sites as given by isites and the other parameters + """ + # TODO: add options for orbital-resolved cohps + _summed_icohps, _list_icohps, _number_bonds, labels, atoms, final_isites = self.get_info_icohps_to_neighbors( + isites=isites, onlycation_isites=onlycation_isites + ) + + with tempfile.TemporaryDirectory() as tmp_dir: + path = f"{tmp_dir}/POSCAR.vasp" + + self.structure.to(filename=path, fmt="poscar") + + if not hasattr(self, "completecohp"): + if path_to_cohpcar is not None and obj_cohpcar is None: + self.completecohp = CompleteCohp.from_file( + fmt="LOBSTER", + filename=path_to_cohpcar, + structure_file=path, + are_coops=self.are_coops, + are_cobis=self.are_cobis, + ) + elif obj_cohpcar is not None: + self.completecohp = obj_cohpcar + else: + raise ValueError("Please provide either path_to_cohpcar or obj_cohpcar") + + # will check that the number of bonds in ICOHPLIST and COHPCAR are identical + # further checks could be implemented + if len(self.Icohpcollection._list_atom1) != len(self.completecohp.bonds): + raise ValueError("COHPCAR and ICOHPLIST do not fit together") + is_spin_completecohp = Spin.down in self.completecohp.get_cohp_by_label("1").cohp + if self.Icohpcollection.is_spin_polarized != is_spin_completecohp: + raise ValueError("COHPCAR and ICOHPLIST do not fit together") + + if only_bonds_to is None: + # sort by anion type + divisor = len(labels) if per_bond else 1 + + plot_label = self._get_plot_label(atoms, per_bond) + summed_cohp = self.completecohp.get_summed_cohp_by_label_list( + label_list=labels, + divisor=divisor, + summed_spin_channels=summed_spin_channels, + ) + + else: + # labels of the COHPs that will be summed! + # iterate through labels and atoms and check which bonds can be included + new_labels = [] + new_atoms = [] + for key, atompair, isite in zip(labels, atoms, final_isites): + present = False + for atomtype in only_bonds_to: + # This is necessary to identify also bonds between the same elements correctly! + if str(self.structure[isite].species.elements[0]) != atomtype: + if atomtype in ( + self._split_string(atompair[0])[0], + self._split_string(atompair[1])[0], + ): + present = True + elif ( + atomtype == self._split_string(atompair[0])[0] + and atomtype == self._split_string(atompair[1])[0] + ): + present = True + + if present: + new_labels.append(key) + new_atoms.append(atompair) + if new_labels: + divisor = len(new_labels) if per_bond else 1 + + plot_label = self._get_plot_label(new_atoms, per_bond) + summed_cohp = self.completecohp.get_summed_cohp_by_label_list( + label_list=new_labels, + divisor=divisor, + summed_spin_channels=summed_spin_channels, + ) + else: + plot_label = None + + summed_cohp = None + + return plot_label, summed_cohp + + def _get_plot_label(self, atoms, per_bond): + # count the types of bonds and append a label: + all_labels = [] + for atoms_names in atoms: + new = [self._split_string(atoms_names[0])[0], self._split_string(atoms_names[1])[0]] + new.sort() + string_here = f"{new[0]}-{new[1]}" + all_labels.append(string_here) + count = collections.Counter(all_labels) + plotlabels = [] + for key, item in count.items(): + plotlabels.append(f"{item} x {key}") + label = ", ".join(plotlabels) + if per_bond: + label += " (per bond)" + return label + + def get_info_icohps_between_neighbors(self, isites=None, onlycation_isites=True): + """Get infos about interactions between neighbors of a certain atom. + + Args: + isites: list of site ids, if isite==None, all isites will be used + onlycation_isites: will only use cations, if isite==None + + Returns: + ICOHPNeighborsInfo + """ + lowerlimit = self.lowerlimit + upperlimit = self.upperlimit + + if self.valences is None and onlycation_isites: + raise ValueError("No valences are provided") + if isites is None: + if onlycation_isites: + isites = [i for i in range(len(self.structure)) if self.valences[i] >= 0.0] + else: + isites = list(range(len(self.structure))) + + summed_icohps = 0.0 + list_icohps = [] + number_bonds = 0 + labels = [] + atoms = [] + for isite in isites: + for in_site, n_site in enumerate(self.list_neighsite[isite]): + for in_site2, n_site2 in enumerate(self.list_neighsite[isite]): + if in_site < in_site2: + unitcell1 = self._determine_unit_cell(n_site) + unitcell2 = self._determine_unit_cell(n_site2) + + index_n_site = self._get_original_site(self.structure, n_site) + index_n_site2 = self._get_original_site(self.structure, n_site2) + + if index_n_site < index_n_site2: + translation = list(np.array(unitcell1) - np.array(unitcell2)) + elif index_n_site2 < index_n_site: + translation = list(np.array(unitcell2) - np.array(unitcell1)) + else: + translation = list(np.array(unitcell1) - np.array(unitcell2)) + + icohps = self._get_icohps( + icohpcollection=self.Icohpcollection, + isite=index_n_site, + lowerlimit=lowerlimit, + upperlimit=upperlimit, + only_bonds_to=self.only_bonds_to, + ) + + done = False + for icohp in icohps.values(): + atomnr1 = self._get_atomnumber(icohp._atom1) + atomnr2 = self._get_atomnumber(icohp._atom2) + label = icohp._label + + if (index_n_site == atomnr1 and index_n_site2 == atomnr2) or ( + index_n_site == atomnr2 and index_n_site2 == atomnr1 + ): + if atomnr1 != atomnr2: + if np.all(np.asarray(translation) == np.asarray(icohp._translation)): + summed_icohps += icohp.summed_icohp + list_icohps.append(icohp.summed_icohp) + number_bonds += 1 + labels.append(label) + atoms.append( + [ + self.Icohpcollection._list_atom1[int(label) - 1], + self.Icohpcollection._list_atom2[int(label) - 1], + ] + ) + + elif not done: + icohp_trans = -np.asarray( + [icohp._translation[0], icohp._translation[1], icohp._translation[2]] + ) + + if (np.all(np.asarray(translation) == np.asarray(icohp._translation))) or ( + np.all(np.asarray(translation) == icohp_trans) + ): + summed_icohps += icohp.summed_icohp + list_icohps.append(icohp.summed_icohp) + number_bonds += 1 + labels.append(label) + atoms.append( + [ + self.Icohpcollection._list_atom1[int(label) - 1], + self.Icohpcollection._list_atom2[int(label) - 1], + ] + ) + done = True + + return ICOHPNeighborsInfo(summed_icohps, list_icohps, number_bonds, labels, atoms, None) + + def _evaluate_ce( + self, + lowerlimit, + upperlimit, + only_bonds_to=None, + additional_condition: int = 0, + perc_strength_icohp: float = 0.15, + adapt_extremum_to_add_cond: bool = False, + ) -> None: + """ + Args: + lowerlimit: lower limit which determines the ICOHPs that are considered for the determination of the + neighbors + upperlimit: upper limit which determines the ICOHPs that are considered for the determination of the + neighbors + only_bonds_to: restricts the types of bonds that will be considered + additional_condition: Additional condition for the evaluation + perc_strength_icohp: will be used to determine how strong the ICOHPs (percentage*strongest ICOHP) will be + that are still considered for the evaluation + adapt_extremum_to_add_cond: will recalculate the limit based on the bonding type and not on the overall + extremum. + """ + # get extremum + if lowerlimit is None and upperlimit is None: + lowerlimit, upperlimit = self._get_limit_from_extremum( + self.Icohpcollection, + percentage=perc_strength_icohp, + adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, + additional_condition=additional_condition, + ) + + elif upperlimit is None or lowerlimit is None: + raise ValueError("Please give two limits or leave them both at None") + + # find environments based on ICOHP values + list_icohps, list_keys, list_lengths, list_neighisite, list_neighsite, list_coords = self._find_environments( + additional_condition, lowerlimit, upperlimit, only_bonds_to + ) + + self.list_icohps = list_icohps + self.list_lengths = list_lengths + self.list_keys = list_keys + self.list_neighsite = list_neighsite + self.list_neighisite = list_neighisite + self.list_coords = list_coords + + # make a structure graph + # make sure everything is relative to the given Structure and not just the atoms in the unit cell + if self.add_additional_data_sg: + self.sg_list = [ + [ + { + "site": neighbor, + "image": tuple( + int(round(i)) + for i in ( + neighbor.frac_coords + - self.structure[ + next( + isite + for isite, site in enumerate(self.structure) + if neighbor.is_periodic_image(site) + ) + ].frac_coords + ) + ), + "weight": 1, + # Here, the ICOBIs and ICOOPs are added based on the bond + # strength cutoff of the ICOHP + # more changes are necessary here if we use icobis for cutoffs + "edge_properties": { + "ICOHP": self.list_icohps[ineighbors][ineighbor], + "bond_length": self.list_lengths[ineighbors][ineighbor], + "bond_label": self.list_keys[ineighbors][ineighbor], + self.id_blist_sg1.upper(): self.bonding_list_1.icohpcollection.get_icohp_by_label( + self.list_keys[ineighbors][ineighbor] + ), + self.id_blist_sg2.upper(): self.bonding_list_2.icohpcollection.get_icohp_by_label( + self.list_keys[ineighbors][ineighbor] + ), + }, + "site_index": next( + isite for isite, site in enumerate(self.structure) if neighbor.is_periodic_image(site) + ), + } + for ineighbor, neighbor in enumerate(neighbors) + ] + for ineighbors, neighbors in enumerate(self.list_neighsite) + ] + else: + self.sg_list = [ + [ + { + "site": neighbor, + "image": tuple( + int(round(i)) + for i in ( + neighbor.frac_coords + - self.structure[ + next( + isite + for isite, site in enumerate(self.structure) + if neighbor.is_periodic_image(site) + ) + ].frac_coords + ) + ), + "weight": 1, + "edge_properties": { + "ICOHP": self.list_icohps[ineighbors][ineighbor], + "bond_length": self.list_lengths[ineighbors][ineighbor], + "bond_label": self.list_keys[ineighbors][ineighbor], + }, + "site_index": next( + isite for isite, site in enumerate(self.structure) if neighbor.is_periodic_image(site) + ), + } + for ineighbor, neighbor in enumerate(neighbors) + ] + for ineighbors, neighbors in enumerate(self.list_neighsite) + ] + + def _find_environments(self, additional_condition, lowerlimit, upperlimit, only_bonds_to): + """ + Will find all relevant neighbors based on certain restrictions. + + Args: + additional_condition (int): additional condition (see above) + lowerlimit (float): lower limit that tells you which ICOHPs are considered + upperlimit (float): upper limit that tells you which ICOHPs are considered + only_bonds_to (list): list of str, e.g. ["O"] that will ensure that only bonds to "O" will be considered + + Returns: + tuple: list of icohps, list of keys, list of lengths, list of neighisite, list of neighsite, list of coords + """ + # run over structure + list_neighsite = [] + list_neighisite = [] + list_coords = [] + list_icohps = [] + list_lengths = [] + list_keys = [] + for idx, site in enumerate(self.structure): + icohps = self._get_icohps( + icohpcollection=self.Icohpcollection, + isite=idx, + lowerlimit=lowerlimit, + upperlimit=upperlimit, + only_bonds_to=only_bonds_to, + ) + + additional_conds = self._find_relevant_atoms_additional_condition(idx, icohps, additional_condition) + keys_from_ICOHPs, lengths_from_ICOHPs, neighbors_from_ICOHPs, selected_ICOHPs = additional_conds + + if len(neighbors_from_ICOHPs) > 0: + centralsite = site + + neighbors_by_distance_start = self.structure.get_sites_in_sphere( + pt=centralsite.coords, + r=np.max(lengths_from_ICOHPs) + 0.5, + include_image=True, + include_index=True, + ) + + neighbors_by_distance = [] + list_distances = [] + index_here_list = [] + coords = [] + for neigh_new in sorted(neighbors_by_distance_start, key=lambda x: x[1]): + site_here = neigh_new[0].to_unit_cell() + index_here = neigh_new[2] + index_here_list.append(index_here) + cell_here = neigh_new[3] + new_coords = [ + site_here.frac_coords[0] + float(cell_here[0]), + site_here.frac_coords[1] + float(cell_here[1]), + site_here.frac_coords[2] + float(cell_here[2]), + ] + coords.append(site_here.lattice.get_cartesian_coords(new_coords)) + + # new_site = PeriodicSite( + # species=site_here.species_string, + # coords=site_here.lattice.get_cartesian_coords(new_coords), + # lattice=site_here.lattice, + # to_unit_cell=False, + # coords_are_cartesian=True, + # ) + neighbors_by_distance.append(neigh_new[0]) + list_distances.append(neigh_new[1]) + _list_neighsite = [] + _list_neighisite = [] + copied_neighbors_from_ICOHPs = copy.copy(neighbors_from_ICOHPs) + copied_distances_from_ICOHPs = copy.copy(lengths_from_ICOHPs) + _neigh_coords = [] + _neigh_frac_coords = [] + + for ineigh, neigh in enumerate(neighbors_by_distance): + index_here2 = index_here_list[ineigh] + + for idist, dist in enumerate(copied_distances_from_ICOHPs): + if ( + np.isclose(dist, list_distances[ineigh], rtol=1e-4) + and copied_neighbors_from_ICOHPs[idist] == index_here2 + ): + _list_neighsite.append(neigh) + _list_neighisite.append(index_here2) + _neigh_coords.append(coords[ineigh]) + _neigh_frac_coords.append(neigh.frac_coords) + del copied_distances_from_ICOHPs[idist] + del copied_neighbors_from_ICOHPs[idist] + break + + list_neighisite.append(_list_neighisite) + list_neighsite.append(_list_neighsite) + list_lengths.append(lengths_from_ICOHPs) + list_keys.append(keys_from_ICOHPs) + list_coords.append(_neigh_coords) + list_icohps.append(selected_ICOHPs) + + else: + list_neighsite.append([]) + list_neighisite.append([]) + list_icohps.append([]) + list_lengths.append([]) + list_keys.append([]) + list_coords.append([]) + return ( + list_icohps, + list_keys, + list_lengths, + list_neighisite, + list_neighsite, + list_coords, + ) + + def _find_relevant_atoms_additional_condition(self, isite, icohps, additional_condition): + """ + Will find all relevant atoms that fulfill the additional_conditions. + + Args: + isite: number of site in structure (starts with 0) + icohps: icohps + additional_condition (int): additional condition + + Returns: + tuple: keys, lengths and neighbors from selected ICOHPs and selected ICOHPs + """ + neighbors_from_ICOHPs = [] + lengths_from_ICOHPs = [] + icohps_from_ICOHPs = [] + keys_from_ICOHPs = [] + + for key, icohp in icohps.items(): + atomnr1 = self._get_atomnumber(icohp._atom1) + atomnr2 = self._get_atomnumber(icohp._atom2) + + # test additional conditions + val1 = val2 = None + if additional_condition in (1, 3, 5, 6): + val1 = self.valences[atomnr1] + val2 = self.valences[atomnr2] + + if additional_condition == 0: + # NO_ADDITIONAL_CONDITION + if atomnr1 == isite: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + elif atomnr2 == isite: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif additional_condition == 1: + # ONLY_ANION_CATION_BONDS + if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): + if atomnr1 == isite: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == isite: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif additional_condition == 2: + # NO_ELEMENT_TO_SAME_ELEMENT_BONDS + if icohp._atom1.rstrip("0123456789") != icohp._atom2.rstrip("0123456789"): + if atomnr1 == isite: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == isite: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif additional_condition == 3: + # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3 + if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and icohp._atom1.rstrip( + "0123456789" + ) != icohp._atom2.rstrip("0123456789"): + if atomnr1 == isite: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == isite: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif additional_condition == 4: + # ONLY_ELEMENT_TO_OXYGEN_BONDS = 4 + if icohp._atom1.rstrip("0123456789") == "O" or icohp._atom2.rstrip("0123456789") == "O": + if atomnr1 == isite: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == isite: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif additional_condition == 5: + # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 + if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): + if atomnr1 == isite: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == isite: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif additional_condition == 6 and val1 > 0.0 and val2 > 0.0: + # ONLY_CATION_CATION_BONDS=6 + if atomnr1 == isite: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == isite: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + return keys_from_ICOHPs, lengths_from_ICOHPs, neighbors_from_ICOHPs, icohps_from_ICOHPs + + @staticmethod + def _get_icohps(icohpcollection, isite, lowerlimit, upperlimit, only_bonds_to): + """Return icohp dict for certain site. + + Args: + icohpcollection: Icohpcollection object + isite (int): number of a site + lowerlimit (float): lower limit that tells you which ICOHPs are considered + upperlimit (float): upper limit that tells you which ICOHPs are considered + only_bonds_to (list): list of str, e.g. ["O"] that will ensure that only bonds to "O" will be considered + + Returns: + dict: of IcohpValues. The keys correspond to the values from the initial list_labels. + """ + return icohpcollection.get_icohp_dict_of_site( + site=isite, + maxbondlength=6.0, + minsummedicohp=lowerlimit, + maxsummedicohp=upperlimit, + only_bonds_to=only_bonds_to, + ) + + @staticmethod + def _get_atomnumber(atomstring) -> int: + """Get the number of the atom within the initial POSCAR (e.g., Return 0 for "Na1"). + + Args: + atomstring: string such as "Na1" + + Returns: + int: indicating the position in the POSCAR + """ + return int(LobsterNeighbors._split_string(atomstring)[1]) - 1 + + @staticmethod + def _split_string(s) -> tuple[str, str]: + """ + Will split strings such as "Na1" in "Na" and "1" and return "1". + + Args: + s (str): string + """ + head = s.rstrip("0123456789") + tail = s[len(head) :] + return head, tail + + @staticmethod + def _determine_unit_cell(site): + """ + Based on the site it will determine the unit cell, in which this site is based. + + Args: + site: site object + """ + unitcell = [] + for coord in site.frac_coords: + value = math.floor(round(coord, 4)) + unitcell.append(value) + + return unitcell + + def _adapt_extremum_to_add_cond(self, list_icohps, percentage): + """ + Convinicence method for returning the extremum of the given icohps or icoops or icobis list. + + Args: + list_icohps: can be a list of icohps or icobis or icobis + + Returns: + float: min value of input list of icohps / max value of input list of icobis or icobis + """ + which_extr = min if not self.are_coops and not self.are_cobis else max + return which_extr(list_icohps) * percentage + + def _get_limit_from_extremum( + self, + icohpcollection, + percentage=0.15, + adapt_extremum_to_add_cond=False, + additional_condition=0, + ): + """Get limits for the evaluation of the icohp values from an icohpcollection + Return -float("inf"), min(max_icohp*0.15,-0.1). Currently only works for ICOHPs. + + Args: + icohpcollection: icohpcollection object + percentage: will determine which ICOHPs or ICOOP or ICOBI will be considered + (only 0.15 from the maximum value) + adapt_extremum_to_add_cond: should the extrumum be adapted to the additional condition + additional_condition: additional condition to determine which bonds are relevant + + Returns: + tuple[float, float]: [-inf, min(strongest_icohp*0.15,-noise_cutoff)] / [max(strongest_icohp*0.15, + noise_cutoff), inf] + """ + extremum_based = None + + if not adapt_extremum_to_add_cond or additional_condition == 0: + extremum_based = icohpcollection.extremum_icohpvalue(summed_spin_channels=True) * percentage + elif additional_condition == 1: + # only cation anion bonds + list_icohps = [] + for value in icohpcollection._icohplist.values(): + atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1) + atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2) + + val1 = self.valences[atomnr1] + val2 = self.valences[atomnr2] + if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + elif additional_condition == 2: + # NO_ELEMENT_TO_SAME_ELEMENT_BONDS + list_icohps = [] + for value in icohpcollection._icohplist.values(): + if value._atom1.rstrip("0123456789") != value._atom2.rstrip("0123456789"): + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + elif additional_condition == 3: + # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3 + list_icohps = [] + for value in icohpcollection._icohplist.values(): + atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1) + atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2) + val1 = self.valences[atomnr1] + val2 = self.valences[atomnr2] + + if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and value._atom1.rstrip( + "0123456789" + ) != value._atom2.rstrip("0123456789"): + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + elif additional_condition == 4: + list_icohps = [] + for value in icohpcollection._icohplist.values(): + if value._atom1.rstrip("0123456789") == "O" or value._atom2.rstrip("0123456789") == "O": + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + elif additional_condition == 5: + # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 + list_icohps = [] + for value in icohpcollection._icohplist.values(): + atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1) + atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2) + val1 = self.valences[atomnr1] + val2 = self.valences[atomnr2] + + if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + elif additional_condition == 6: + # ONLY_CATION_CATION_BONDS=6 + list_icohps = [] + for value in icohpcollection._icohplist.values(): + atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1) + atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2) + val1 = self.valences[atomnr1] + val2 = self.valences[atomnr2] + + if val1 > 0.0 and val2 > 0.0: + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + if not self.are_coops and not self.are_cobis: + max_here = min(extremum_based, -self.noise_cutoff) if self.noise_cutoff is not None else extremum_based + return -float("inf"), max_here + if self.are_coops or self.are_cobis: + min_here = max(extremum_based, self.noise_cutoff) if self.noise_cutoff is not None else extremum_based + return min_here, float("inf") + + return None + + +class LobsterLightStructureEnvironments(LightStructureEnvironments): + """Store LightStructureEnvironments based on Lobster outputs.""" + + @classmethod + def from_Lobster( + cls, + list_ce_symbol, + list_csm, + list_permutation, + list_neighsite, + list_neighisite, + structure: Structure, + valences=None, + ) -> Self: + """ + Will set up a LightStructureEnvironments from Lobster. + + Args: + structure: Structure object + list_ce_symbol: list of symbols for coordination environments + list_csm: list of continuous symmetry measures + list_permutation: list of permutations + list_neighsite: list of neighboring sites + list_neighisite: list of neighboring isites (number of a site) + valences: list of valences + + Returns: + LobsterLightStructureEnvironments + """ + strategy = None + valences_origin = "user-defined" + + coordination_environments = [] + + all_nbs_sites = [] + all_nbs_sites_indices = [] + neighbors_sets = [] + counter = 0 + for isite in range(len(structure)): + # all_nbs_sites_here=[] + all_nbs_sites_indices_here = [] + # Coordination environment + if list_ce_symbol is not None: + ce_dict = { + "ce_symbol": list_ce_symbol[isite], + "ce_fraction": 1.0, + "csm": list_csm[isite], + "permutation": list_permutation[isite], + } + else: + ce_dict = None + + if list_neighisite[isite] is not None: + for idx_neigh_site, neigh_site in enumerate(list_neighsite[isite]): + diff = neigh_site.frac_coords - structure[list_neighisite[isite][idx_neigh_site]].frac_coords + round_diff = np.round(diff) + if not np.allclose(diff, round_diff): + raise ValueError( + "Weird, differences between one site in a periodic image cell is not integer ..." + ) + nb_image_cell = np.array(round_diff, int) + + all_nbs_sites_indices_here.append(counter) + + neighbor = { + "site": neigh_site, + "index": list_neighisite[isite][idx_neigh_site], + "image_cell": nb_image_cell, + } + all_nbs_sites.append(neighbor) + counter += 1 + + all_nbs_sites_indices.append(all_nbs_sites_indices_here) + else: + all_nbs_sites.append({"site": None, "index": None, "image_cell": None}) # all_nbs_sites_here) + all_nbs_sites_indices.append([]) # all_nbs_sites_indices_here) + + if list_neighisite[isite] is not None: + nb_set = cls.NeighborsSet( + structure=structure, + isite=isite, + all_nbs_sites=all_nbs_sites, + all_nbs_sites_indices=all_nbs_sites_indices[isite], + ) + + else: + nb_set = cls.NeighborsSet( + structure=structure, + isite=isite, + all_nbs_sites=[], + all_nbs_sites_indices=[], + ) + + coordination_environments.append([ce_dict]) + neighbors_sets.append([nb_set]) + + return cls( + strategy=strategy, + coordination_environments=coordination_environments, + all_nbs_sites=all_nbs_sites, + neighbors_sets=neighbors_sets, + structure=structure, + valences=valences, + valences_origin=valences_origin, + ) + + @property + def uniquely_determines_coordination_environments(self): + """True if the coordination environments are uniquely determined.""" + return True + + def as_dict(self): + """ + Bson-serializable dict representation of the LightStructureEnvironments object. + + Returns: + Bson-serializable dict representation of the LightStructureEnvironments object. + """ + return { + "@module": type(self).__module__, + "@class": type(self).__name__, + "strategy": self.strategy, + "structure": self.structure.as_dict(), + "coordination_environments": self.coordination_environments, + "all_nbs_sites": [ + { + "site": nb_site["site"].as_dict(), + "index": nb_site["index"], + "image_cell": [int(ii) for ii in nb_site["image_cell"]], + } + for nb_site in self._all_nbs_sites + ], + "neighbors_sets": [ + [nb_set.as_dict() for nb_set in site_nb_sets] or None for site_nb_sets in self.neighbors_sets + ], + "valences": self.valences, + } + + +class ICOHPNeighborsInfo(NamedTuple): + """ + Tuple to represent information on relevant bonds + Args: + total_icohp (float): sum of icohp values of neighbors to the selected sites [given by the id in structure] + list_icohps (list): list of summed icohp values for all identified interactions with neighbors + n_bonds (int): number of identified bonds to the selected sites + labels (list[str]): labels (from ICOHPLIST) for all identified bonds + atoms (list[list[str]]): list of list describing the species present in the identified interactions + (names from ICOHPLIST), e.g. ["Ag3", "O5"] + central_isites (list[int]): list of the central isite for each identified interaction. + """ + + total_icohp: float + list_icohps: list[float] + n_bonds: int + labels: list[str] + atoms: list[list[str]] + central_isites: list[int] | None diff --git a/outputs.py b/outputs.py new file mode 100644 index 0000000000..d17c550ba9 --- /dev/null +++ b/outputs.py @@ -0,0 +1,2157 @@ +""" +Module for reading Lobster output files. For more information +on LOBSTER see www.cohp.de. +If you use this module, please cite: +J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, +"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +ChemPlusChem 2022, e202200123, +DOI: 10.1002/cplu.202200123. +""" + +from __future__ import annotations + +import collections +import fnmatch +import itertools +import os +import re +import warnings +from collections import defaultdict +from typing import TYPE_CHECKING + +import numpy as np +from monty.io import zopen +from monty.json import MSONable +from pymatgen.core.structure import Structure +from pymatgen.electronic_structure.bandstructure import LobsterBandStructureSymmLine +from pymatgen.electronic_structure.core import Orbital, Spin +from pymatgen.electronic_structure.dos import Dos, LobsterCompleteDos +from pymatgen.io.vasp.inputs import Kpoints +from pymatgen.io.vasp.outputs import Vasprun, VolumetricData +from pymatgen.util.due import Doi, due + +if TYPE_CHECKING: + from typing import Any + + from pymatgen.core.structure import IStructure + from pymatgen.util.typing import PathLike + +__author__ = "Janine George, Marco Esters" +__copyright__ = "Copyright 2017, The Materials Project" +__version__ = "0.2" +__maintainer__ = "Janine George " +__email__ = "janinegeorge.ulfen@gmail.com" +__date__ = "Dec 13, 2017" + +MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) + +due.cite( + Doi("10.1002/cplu.202200123"), + description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +) + + +class Cohpcar: + """Read COHPCAR/COOPCAR/COBICAR files generated by LOBSTER. + + Attributes: + cohp_data (dict[str, Dict[str, Any]]): A dictionary containing the COHP data of the form: + {bond: {"COHP": {Spin.up: cohps, Spin.down:cohps}, + "ICOHP": {Spin.up: icohps, Spin.down: icohps}, + "length": bond length, + "sites": sites corresponding to the bond} + Also contains an entry for the average, which does not have a "length" key. + efermi (float): The Fermi energy in eV. + energies (Sequence[float]): Sequence of energies in eV. Note that LOBSTER shifts the energies + so that the Fermi energy is at zero. + is_spin_polarized (bool): Boolean to indicate if the calculation is spin polarized. + orb_cohp (dict[str, Dict[str, Dict[str, Any]]]): A dictionary containing the orbital-resolved COHPs of the form: + orb_cohp[label] = {bond_data["orb_label"]: { + "COHP": {Spin.up: cohps, Spin.down:cohps}, + "ICOHP": {Spin.up: icohps, Spin.down: icohps}, + "orbitals": orbitals, + "length": bond lengths, + "sites": sites corresponding to the bond}, + } + """ + + def __init__( + self, + are_coops: bool = False, + are_cobis: bool = False, + are_multi_center_cobis: bool = False, + filename: PathLike | None = None, + ) -> None: + """ + Args: + are_coops: Determines if the file includes COOPs. + Default is False for COHPs. + are_cobis: Determines if the file is a list of COHPs or COBIs. + Default is False for COHPs. + are_multi_center_cobis: Determines if the file include multi-center COBIS. + Default is False for two-center cobis. + filename: Name of the COHPCAR file. If it is None, the default + file name will be chosen, depending on the value of are_coops. + """ + if ( + (are_coops and are_cobis) + or (are_coops and are_multi_center_cobis) + or (are_cobis and are_multi_center_cobis) + ): + raise ValueError("You cannot have info about COOPs, COBIs and/or multi-center COBIS in the same file.") + self.are_coops = are_coops + self.are_cobis = are_cobis + self.are_multi_center_cobis = are_multi_center_cobis + + if filename is None: + if are_coops: + filename = "COOPCAR.lobster" + elif are_cobis or are_multi_center_cobis: + filename = "COBICAR.lobster" + else: + filename = "COHPCAR.lobster" + + with zopen(filename, mode="rt") as file: + contents = file.read().split("\n") + + # The parameters line is the second line in a COHPCAR file. It + # contains all parameters that are needed to map the file. + parameters = contents[1].split() + # Subtract 1 to skip the average + num_bonds = int(parameters[0]) if self.are_multi_center_cobis else int(parameters[0]) - 1 + self.efermi = float(parameters[-1]) + self.is_spin_polarized = int(parameters[1]) == 2 + spins = [Spin.up, Spin.down] if int(parameters[1]) == 2 else [Spin.up] + cohp_data: dict[str, dict[str, Any]] = {} + if not self.are_multi_center_cobis: + # The COHP data start in row num_bonds + 3 + data = np.array([np.array(row.split(), dtype=float) for row in contents[num_bonds + 3 :]]).transpose() + cohp_data = { + "average": { + "COHP": {spin: data[1 + 2 * s * (num_bonds + 1)] for s, spin in enumerate(spins)}, + "ICOHP": {spin: data[2 + 2 * s * (num_bonds + 1)] for s, spin in enumerate(spins)}, + } + } + else: + # The COBI data start in row num_bonds + 3 if multi-center cobis exist + data = np.array([np.array(row.split(), dtype=float) for row in contents[num_bonds + 3 :]]).transpose() + + self.energies = data[0] + + orb_cohp: dict[str, Any] = {} + # present for Lobster versions older than Lobster 2.2.0 + very_old = False + # the labeling had to be changed: there are more than one COHP for each atom combination + # this is done to make the labeling consistent with ICOHPLIST.lobster + bond_num = 0 + bond_data = {} + label = "" + for bond in range(num_bonds): + if not self.are_multi_center_cobis: + bond_data = self._get_bond_data(contents[3 + bond]) + label = str(bond_num) + orbs = bond_data["orbitals"] + cohp = {spin: data[2 * (bond + s * (num_bonds + 1)) + 3] for s, spin in enumerate(spins)} + icohp = {spin: data[2 * (bond + s * (num_bonds + 1)) + 4] for s, spin in enumerate(spins)} + if orbs is None: + bond_num += 1 + label = str(bond_num) + cohp_data[label] = { + "COHP": cohp, + "ICOHP": icohp, + "length": bond_data["length"], + "sites": bond_data["sites"], + "cells": None, + } + + elif label in orb_cohp: + orb_cohp[label].update( + { + bond_data["orb_label"]: { + "COHP": cohp, + "ICOHP": icohp, + "orbitals": orbs, + "length": bond_data["length"], + "sites": bond_data["sites"], + "cells": bond_data["cells"], + } + } + ) + else: + # present for Lobster versions older than Lobster 2.2.0 + if bond_num == 0: + very_old = True + if very_old: + bond_num += 1 + label = str(bond_num) + + orb_cohp[label] = { + bond_data["orb_label"]: { + "COHP": cohp, + "ICOHP": icohp, + "orbitals": orbs, + "length": bond_data["length"], + "sites": bond_data["sites"], + "cells": bond_data["cells"], + } + } + + else: + bond_data = self._get_bond_data(contents[2 + bond], are_multi_center_cobis=self.are_multi_center_cobis) + + label = str(bond_num) + + orbs = bond_data["orbitals"] + + cohp = {spin: data[2 * (bond + s * (num_bonds)) + 1] for s, spin in enumerate(spins)} + + icohp = {spin: data[2 * (bond + s * (num_bonds)) + 2] for s, spin in enumerate(spins)} + if orbs is None: + bond_num += 1 + label = str(bond_num) + cohp_data[label] = { + "COHP": cohp, + "ICOHP": icohp, + "length": bond_data["length"], + "sites": bond_data["sites"], + "cells": bond_data["cells"], + } + + elif label in orb_cohp: + orb_cohp[label].update( + { + bond_data["orb_label"]: { + "COHP": cohp, + "ICOHP": icohp, + "orbitals": orbs, + "length": bond_data["length"], + "sites": bond_data["sites"], + } + } + ) + else: + # present for Lobster versions older than Lobster 2.2.0 + if bond_num == 0: + very_old = True + if very_old: + bond_num += 1 + label = str(bond_num) + + orb_cohp[label] = { + bond_data["orb_label"]: { + "COHP": cohp, + "ICOHP": icohp, + "orbitals": orbs, + "length": bond_data["length"], + "sites": bond_data["sites"], + } + } + + # present for lobster older than 2.2.0 + if very_old: + for bond_str in orb_cohp: + cohp_data[bond_str] = { + "COHP": None, + "ICOHP": None, + "length": bond_data["length"], + "sites": bond_data["sites"], + } + self.orb_res_cohp = orb_cohp or None + self.cohp_data = cohp_data + + @staticmethod + def _get_bond_data(line: str, are_multi_center_cobis: bool = False) -> dict: + """Subroutine to extract bond label, site indices, and length from + a LOBSTER header line. The site indices are zero-based, so they + can be easily used with a Structure object. + + Example header line: No.4:Fe1->Fe9(2.4524893531900283) + Example header line for orbital-resolved COHP: + No.1:Fe1[3p_x]->Fe2[3d_x^2-y^2](2.456180552772262) + + Args: + line: line in the COHPCAR header describing the bond. + are_multi_center_cobis: indicates multi-center COBIs + + Returns: + Dict with the bond label, the bond length, a tuple of the site + indices, a tuple containing the orbitals (if orbital-resolved), + and a label for the orbitals (if orbital-resolved). + """ + if not are_multi_center_cobis: + line_new = line.rsplit("(", 1) + length = float(line_new[-1][:-1]) + + sites = line_new[0].replace("->", ":").split(":")[1:3] + site_indices = tuple(int(re.split(r"\D+", site)[1]) - 1 for site in sites) + # TODO: get cells here as well + + if "[" in sites[0]: + orbs = [re.findall(r"\[(.*)\]", site)[0] for site in sites] + orb_label, orbitals = get_orb_from_str(orbs) + else: + orbitals = None + orb_label = None + + return { + "length": length, + "sites": site_indices, + "cells": None, + "orbitals": orbitals, + "orb_label": orb_label, + } + + line_new = line.rsplit(sep="(", maxsplit=1) + + sites = line_new[0].replace("->", ":").split(":")[1:] + site_indices = tuple(int(re.split(r"\D+", site)[1]) - 1 for site in sites) + cells = [[int(i) for i in re.split(r"\[(.*?)\]", site)[1].split(" ") if i != ""] for site in sites] + # test orbitalwise implementations! + if sites[0].count("[") > 1: + orbs = [re.findall(r"\]\[(.*)\]", site)[0] for site in sites] + orb_label, orbitals = get_orb_from_str(orbs) + else: + orbitals = None + orb_label = None + + return { + "sites": site_indices, + "cells": cells, + "length": None, + "orbitals": orbitals, + "orb_label": orb_label, + } + + +class Icohplist(MSONable): + """Read ICOHPLIST/ICOOPLIST files generated by LOBSTER. + + Attributes: + are_coops (bool): Indicates whether the object is consisting of COOPs. + is_spin_polarized (bool): Boolean to indicate if the calculation is spin polarized. + Icohplist (dict[str, Dict[str, Union[float, int, Dict[Spin, float]]]]): Dict containing the + listfile data of the form: { + bond: "length": bond length, + "number_of_bonds": number of bonds + "icohp": {Spin.up: ICOHP(Ef) spin up, Spin.down: ...} + } + IcohpCollection (IcohpCollection): IcohpCollection Object. + """ + + def __init__( + self, + are_coops: bool = False, + are_cobis: bool = False, + filename: PathLike | None = None, + is_spin_polarized: bool = False, + orbitalwise: bool = False, + icohpcollection=None, + ): + """ + Args: + are_coops: Determines if the file is a list of ICOOPs. + Defaults to False for ICOHPs. + are_cobis: Determines if the file is a list of ICOBIs. + Defaults to False for ICOHPs. + filename: Name of the ICOHPLIST file. If it is None, the default + file name will be chosen, depending on the value of are_coops + is_spin_polarized: Boolean to indicate if the calculation is spin polarized + icohpcollection: IcohpCollection Object. + + """ + self._filename = filename + self.is_spin_polarized = is_spin_polarized + self.orbitalwise = orbitalwise + self._icohpcollection = icohpcollection + if are_coops and are_cobis: + raise ValueError("You cannot have info about COOPs and COBIs in the same file.") + self.are_coops = are_coops + self.are_cobis = are_cobis + if filename is None: + if are_coops: + filename = "ICOOPLIST.lobster" + elif are_cobis: + filename = "ICOBILIST.lobster" + else: + filename = "ICOHPLIST.lobster" + + # LOBSTER list files have an extra trailing blank line + # and we don't need the header. + if self._icohpcollection is None: + with zopen(filename, mode="rt") as file: + data = file.read().split("\n")[1:-1] + if len(data) == 0: + raise RuntimeError("ICOHPLIST file contains no data.") + + # Which Lobster version? + if len(data[0].split()) == 8: + version = "3.1.1" + elif len(data[0].split()) == 6: + version = "2.2.1" + warnings.warn("Please consider using the new Lobster version. See www.cohp.de.") + else: + raise ValueError + + # If the calculation is spin polarized, the line in the middle + # of the file will be another header line. + # TODO: adapt this for orbital-wise stuff + self.is_spin_polarized = "distance" in data[len(data) // 2] + + # check if orbital-wise ICOHPLIST + # include case when there is only one ICOHP!!! + self.orbitalwise = len(data) > 2 and "_" in data[1].split()[1] + + data_orbitals: list[str] = [] + if self.orbitalwise: + data_without_orbitals = [] + data_orbitals = [] + for line in data: + if "_" not in line.split()[1]: + data_without_orbitals += [line] + else: + data_orbitals += [line] + + else: + data_without_orbitals = data + + if "distance" in data_without_orbitals[len(data_without_orbitals) // 2]: + # TODO: adapt this for orbital-wise stuff + n_bonds = len(data_without_orbitals) // 2 + if n_bonds == 0: + raise RuntimeError("ICOHPLIST file contains no data.") + else: + n_bonds = len(data_without_orbitals) + + labels, atoms1, atoms2, lens, translations, nums, icohps = [], [], [], [], [], [], [] + + # initialize static variables + label = "" + atom1 = "" + atom2 = "" + length = None + num = None + translation = [] + + for bond in range(n_bonds): + line = data_without_orbitals[bond].split() + icohp = {} + if version == "2.2.1": + label = f"{line[0]}" + atom1 = str(line[1]) + atom2 = str(line[2]) + length = float(line[3]) + icohp[Spin.up] = float(line[4]) + num = int(line[5]) + translation = [0, 0, 0] + if self.is_spin_polarized: + icohp[Spin.down] = float(data_without_orbitals[bond + n_bonds + 1].split()[4]) + + elif version == "3.1.1": + label = f"{line[0]}" + atom1 = str(line[1]) + atom2 = str(line[2]) + length = float(line[3]) + translation = [int(line[4]), int(line[5]), int(line[6])] + icohp[Spin.up] = float(line[7]) + num = 1 + + if self.is_spin_polarized: + icohp[Spin.down] = float(data_without_orbitals[bond + n_bonds + 1].split()[7]) + + labels += [label] + atoms1 += [atom1] + atoms2 += [atom2] + lens += [length] + translations += [translation] + nums += [num] + icohps += [icohp] + + list_orb_icohp: list[dict] | None = None + if self.orbitalwise: + list_orb_icohp = [] + n_orbs = len(data_orbitals) // 2 if self.is_spin_polarized else len(data_orbitals) + + for i_data_orb in range(n_orbs): + data_orb = data_orbitals[i_data_orb] + icohp = {} + line = data_orb.split() + label = f"{line[0]}" + orbs = re.findall(r"_(.*?)(?=\s)", data_orb) + orb_label, orbitals = get_orb_from_str(orbs) + icohp[Spin.up] = float(line[7]) + + if self.is_spin_polarized: + icohp[Spin.down] = float(data_orbitals[n_orbs + i_data_orb].split()[7]) + + if len(list_orb_icohp) < int(label): + list_orb_icohp += [{orb_label: {"icohp": icohp, "orbitals": orbitals}}] + else: + list_orb_icohp[int(label) - 1][orb_label] = {"icohp": icohp, "orbitals": orbitals} + + # Avoid circular import + from pymatgen.electronic_structure.cohp import IcohpCollection + + self._icohpcollection = IcohpCollection( + are_coops=are_coops, + are_cobis=are_cobis, + list_labels=labels, + list_atom1=atoms1, + list_atom2=atoms2, + list_length=lens, + list_translation=translations, + list_num=nums, + list_icohp=icohps, + is_spin_polarized=self.is_spin_polarized, + list_orb_icohp=list_orb_icohp, + ) + + @property + def icohplist(self) -> dict[Any, dict[str, Any]]: + """The ICOHP list compatible with older version of this class.""" + icohp_dict = {} + for key, value in self._icohpcollection._icohplist.items(): + icohp_dict[key] = { + "length": value._length, + "number_of_bonds": value._num, + "icohp": value._icohp, + "translation": value._translation, + "orbitals": value._orbitals, + } + return icohp_dict + + @property + def icohpcollection(self): + """The IcohpCollection object.""" + return self._icohpcollection + + +class NciCobiList: + """Read NcICOBILIST (multi-center ICOBI) files generated by LOBSTER. + + Attributes: + is_spin_polarized (bool): Boolean to indicate if the calculation is spin polarized. + NciCobiList (dict): Dict containing the listfile data of the form: + {bond: "number_of_atoms": number of atoms involved in the multi-center interaction, + "ncicobi": {Spin.up: Nc-ICOBI(Ef) spin up, Spin.down: ...}}, + "interaction_type": type of the multi-center interaction + """ + + def __init__( + self, + filename: PathLike | None = "NcICOBILIST.lobster", + ) -> None: + """ + LOBSTER < 4.1.0: no COBI/ICOBI/NcICOBI. + + Args: + filename: Name of the NcICOBILIST file. + """ + # LOBSTER list files have an extra trailing blank line + # and we don't need the header. + with zopen(filename, mode="rt") as file: + data = file.read().split("\n")[1:-1] + if len(data) == 0: + raise RuntimeError("NcICOBILIST file contains no data.") + + # If the calculation is spin-polarized, the line in the middle + # of the file will be another header line. + self.is_spin_polarized = "spin" in data[len(data) // 2] # TODO: adapt this for orbitalwise case + + # check if orbitalwise NcICOBILIST + # include case when there is only one NcICOBI + self.orbital_wise = False # set as default + for entry in data: # NcICOBIs orbitalwise and non-orbitalwise can be mixed + if len(data) > 2 and "s]" in str(entry.split()[3:]): + self.orbital_wise = True + warnings.warn( + "This is an orbitalwise NcICOBILIST.lobster file. " + "Currently, the orbitalwise information is not read!" + ) + break # condition has only to be met once + + if self.orbital_wise: + data_without_orbitals = [] + for line in data: + if "_" not in str(line.split()[3:]) and "s]" not in str(line.split()[3:]): + data_without_orbitals += [line] + else: + data_without_orbitals = data + + if "spin" in data_without_orbitals[len(data_without_orbitals) // 2]: + # TODO: adapt this for orbitalwise case + n_bonds = len(data_without_orbitals) // 2 + if n_bonds == 0: + raise RuntimeError("NcICOBILIST file contains no data.") + else: + n_bonds = len(data_without_orbitals) + + self.list_labels = [] + self.list_n_atoms = [] + self.list_ncicobi = [] + self.list_interaction_type = [] + self.list_num = [] + + for bond in range(n_bonds): + line = data_without_orbitals[bond].split() + ncicobi = {} + + label = f"{line[0]}" + n_atoms = str(line[1]) + ncicobi[Spin.up] = float(line[2]) + interaction_type = str(line[3:]).replace("'", "").replace(" ", "") + num = 1 + + if self.is_spin_polarized: + ncicobi[Spin.down] = float(data_without_orbitals[bond + n_bonds + 1].split()[2]) + + self.list_labels += [label] + self.list_n_atoms += [n_atoms] + self.list_ncicobi += [ncicobi] + self.list_interaction_type += [interaction_type] + self.list_num += [num] + + # TODO: add functions to get orbital resolved NcICOBIs + + @property + def ncicobi_list(self) -> dict[Any, dict[str, Any]]: + """Returns: ncicobilist.""" + ncicobi_list = {} + for idx in range(len(self.list_labels)): + ncicobi_list[str(idx + 1)] = { + "number_of_atoms": int(self.list_n_atoms[idx]), + "ncicobi": self.list_ncicobi[idx], + "interaction_type": self.list_interaction_type[idx], + } + + return ncicobi_list + + +class Doscar: + """Deal with Lobster's projected DOS and local projected DOS. + The beforehand quantum-chemical calculation was performed with VASP. + + Attributes: + completedos (LobsterCompleteDos): LobsterCompleteDos Object. + pdos (list): List of Dict including numpy arrays with pdos. Access as + pdos[atomindex]['orbitalstring']['Spin.up/Spin.down']. + tdos (Dos): Dos Object of the total density of states. + energies (numpy.ndarray): Numpy array of the energies at which the DOS was calculated + (in eV, relative to Efermi). + tdensities (dict): tdensities[Spin.up]: numpy array of the total density of states for + the Spin.up contribution at each of the energies. tdensities[Spin.down]: numpy array + of the total density of states for the Spin.down contribution at each of the energies. + If is_spin_polarized=False, tdensities[Spin.up]: numpy array of the total density of states. + itdensities (dict): itdensities[Spin.up]: numpy array of the total density of states for + the Spin.up contribution at each of the energies. itdensities[Spin.down]: numpy array + of the total density of states for the Spin.down contribution at each of the energies. + If is_spin_polarized=False, itdensities[Spin.up]: numpy array of the total density of states. + is_spin_polarized (bool): Boolean. Tells if the system is spin polarized. + """ + + def __init__( + self, + doscar: PathLike = "DOSCAR.lobster", + structure_file: PathLike | None = "POSCAR", + structure: IStructure | Structure | None = None, + ): + """ + Args: + doscar: DOSCAR file, typically "DOSCAR.lobster" + structure_file: for vasp, this is typically "POSCAR" + structure: instead of a structure file, the structure can be given + directly. structure_file will be preferred. + """ + self._doscar = doscar + + self._final_structure = Structure.from_file(structure_file) if structure_file is not None else structure + + self._parse_doscar() + + def _parse_doscar(self): + doscar = self._doscar + + tdensities = {} + itdensities = {} + with zopen(doscar, mode="rt") as file: + n_atoms = int(file.readline().split()[0]) + efermi = float([file.readline() for nn in range(4)][3].split()[17]) + dos = [] + orbitals = [] + for _atom in range(n_atoms + 1): + line = file.readline() + ndos = int(line.split()[2]) + orbitals += [line.split(";")[-1].split()] + line = file.readline().split() + cdos = np.zeros((ndos, len(line))) + cdos[0] = np.array(line) + for nd in range(1, ndos): + line = file.readline().split() + cdos[nd] = np.array(line) + dos += [cdos] + doshere = np.array(dos[0]) + if len(doshere[0, :]) == 5: + self._is_spin_polarized = True + elif len(doshere[0, :]) == 3: + self._is_spin_polarized = False + else: + raise ValueError("There is something wrong with the DOSCAR. Can't extract spin polarization.") + energies = doshere[:, 0] + if not self._is_spin_polarized: + tdensities[Spin.up] = doshere[:, 1] + itdensities[Spin.up] = doshere[:, 2] + pdoss = [] + spin = Spin.up + for atom in range(n_atoms): + pdos = defaultdict(dict) + data = dos[atom + 1] + _, ncol = data.shape + + for orb_num, j in enumerate(range(1, ncol)): + orb = orbitals[atom + 1][orb_num] + pdos[orb][spin] = data[:, j] + pdoss += [pdos] + else: + tdensities[Spin.up] = doshere[:, 1] + tdensities[Spin.down] = doshere[:, 2] + itdensities[Spin.up] = doshere[:, 3] + itdensities[Spin.down] = doshere[:, 4] + pdoss = [] + for atom in range(n_atoms): + pdos = defaultdict(dict) + data = dos[atom + 1] + _, ncol = data.shape + orb_num = 0 + for j in range(1, ncol): + spin = Spin.down if j % 2 == 0 else Spin.up + orb = orbitals[atom + 1][orb_num] + pdos[orb][spin] = data[:, j] + if j % 2 == 0: + orb_num += 1 + pdoss += [pdos] + + self._efermi = efermi + self._pdos = pdoss + self._tdos = Dos(efermi, energies, tdensities) + self._energies = energies + self._tdensities = tdensities + self._itdensities = itdensities + final_struct = self._final_structure + + pdossneu = {final_struct[i]: pdos for i, pdos in enumerate(self._pdos)} + + self._completedos = LobsterCompleteDos(final_struct, self._tdos, pdossneu) + + @property + def completedos(self) -> LobsterCompleteDos: + """LobsterCompleteDos.""" + return self._completedos + + @property + def pdos(self) -> list: + """Projected DOS.""" + return self._pdos + + @property + def tdos(self) -> Dos: + """Total DOS.""" + return self._tdos + + @property + def energies(self) -> np.ndarray: + """Energies.""" + return self._energies + + @property + def tdensities(self) -> dict[Spin, np.ndarray]: + """Total densities as a np.ndarray.""" + return self._tdensities + + @property + def itdensities(self) -> dict[Spin, np.ndarray]: + """Integrated total densities as a np.ndarray.""" + return self._itdensities + + @property + def is_spin_polarized(self) -> bool: + """Whether run is spin polarized.""" + return self._is_spin_polarized + + +class Charge(MSONable): + """Read CHARGE files generated by LOBSTER. + + Attributes: + atomlist (list[str]): List of atoms in CHARGE.lobster. + types (list[str]): List of types of atoms in CHARGE.lobster. + mulliken (list[float]): List of Mulliken charges of atoms in CHARGE.lobster. + loewdin (list[float]): List of Loewdin charges of atoms in CHARGE.Loewdin. + num_atoms (int): Number of atoms in CHARGE.lobster. + """ + + def __init__( + self, + filename: PathLike = "CHARGE.lobster", + num_atoms: int | None = None, + atomlist: list[str] | None = None, + types: list[str] | None = None, + mulliken: list[float] | None = None, + loewdin: list[float] | None = None, + ): + """ + Args: + filename: The CHARGE file, typically "CHARGE.lobster". + num_atoms: number of atoms in the structure + atomlist: list of atoms in the structure + types: list of unique species in the structure + mulliken: list of Mulliken charges + loewdin: list of Loewdin charges. + """ + self._filename = filename + self.num_atoms = num_atoms + self.types = [] if types is None else types + self.atomlist = [] if atomlist is None else atomlist + self.mulliken = [] if mulliken is None else mulliken + self.loewdin = [] if loewdin is None else loewdin + + if self.num_atoms is None: + with zopen(filename, mode="rt") as file: + data = file.read().split("\n")[3:-3] + if len(data) == 0: + raise RuntimeError("CHARGE file contains no data.") + + self.num_atoms = len(data) + for atom in range(self.num_atoms): + line = data[atom].split() + self.atomlist += [line[1] + line[0]] + self.types += [line[1]] + self.mulliken += [float(line[2])] + self.loewdin += [float(line[3])] + + def get_structure_with_charges(self, structure_filename: PathLike) -> Structure: + """Get a Structure with Mulliken and Loewdin charges as site properties. + + Args: + structure_filename: filename of POSCAR + + Returns: + Structure Object with Mulliken and Loewdin charges as site properties. + """ + struct = Structure.from_file(structure_filename) + mulliken = self.mulliken + loewdin = self.loewdin + site_properties = {"Mulliken Charges": mulliken, "Loewdin Charges": loewdin} + return struct.copy(site_properties=site_properties) + + @property + def Mulliken(self): + warnings.warn("`Mulliken` attribute is deprecated. Use `mulliken` instead.", DeprecationWarning, stacklevel=2) + return self.mulliken + + @property + def Loewdin(self): + warnings.warn("`Loewdin` attribute is deprecated. Use `loewdin` instead.", DeprecationWarning, stacklevel=2) + return self.loewdin + + +class Lobsterout(MSONable): + """Read in the lobsterout and evaluate the spilling, save the basis, save warnings, save infos. + + Attributes: + basis_functions (list[str]): List of basis functions that were used in lobster run as strings. + basis_type (list[str]): List of basis type that were used in lobster run as strings. + charge_spilling (list[float]): List of charge spilling (first entry: result for spin 1, + second entry: result for spin 2 or not present). + dft_program (str): String representing the DFT program used for the calculation of the wave function. + elements (list[str]): List of strings of elements that were present in lobster calculation. + has_charge (bool): Whether CHARGE.lobster is present. + has_cohpcar (bool): Whether COHPCAR.lobster and ICOHPLIST.lobster are present. + has_madelung (bool): Whether SitePotentials.lobster and MadelungEnergies.lobster are present. + has_coopcar (bool): Whether COOPCAR.lobster and ICOOPLIST.lobster are present. + has_cobicar (bool): Whether COBICAR.lobster and ICOBILIST.lobster are present. + has_doscar (bool): Whether DOSCAR.lobster is present. + has_doscar_lso (bool): Whether DOSCAR.LSO.lobster is present. + has_projection (bool): Whether projectionData.lobster is present. + has_bandoverlaps (bool): Whether bandOverlaps.lobster is present. + has_density_of_energies (bool): Whether DensityOfEnergy.lobster is present. + has_fatbands (bool): Whether fatband calculation was performed. + has_grosspopulation (bool): Whether GROSSPOP.lobster is present. + info_lines (str): String with additional infos on the run. + info_orthonormalization (str): String with infos on orthonormalization. + is_restart_from_projection (bool): Boolean that indicates that calculation was restarted + from existing projection file. + lobster_version (str): String that indicates Lobster version. + number_of_spins (int): Integer indicating the number of spins. + number_of_threads (int): Integer that indicates how many threads were used. + timing (dict[str, float]): Dictionary with infos on timing. + total_spilling (list[float]): List of values indicating the total spilling for spin + channel 1 (and spin channel 2). + warning_lines (str): String with all warnings. + """ + + # valid Lobsterout instance attributes + _ATTRIBUTES = ( + "filename", + "is_restart_from_projection", + "lobster_version", + "number_of_threads", + "dft_program", + "number_of_spins", + "charge_spilling", + "total_spilling", + "elements", + "basis_type", + "basis_functions", + "timing", + "warning_lines", + "info_orthonormalization", + "info_lines", + "has_doscar", + "has_doscar_lso", + "has_cohpcar", + "has_coopcar", + "has_cobicar", + "has_charge", + "has_madelung", + "has_projection", + "has_bandoverlaps", + "has_fatbands", + "has_grosspopulation", + "has_density_of_energies", + ) + + # TODO: add tests for skipping COBI and madelung + # TODO: add tests for including COBI and madelung + def __init__(self, filename: PathLike | None, **kwargs) -> None: + """ + Args: + filename: The lobsterout file. + **kwargs: dict to initialize Lobsterout instance. + """ + self.filename = filename + if kwargs: + for attr, val in kwargs.items(): + if attr in self._ATTRIBUTES: + setattr(self, attr, val) + else: + raise ValueError(f"{attr}={val} is not a valid attribute for Lobsterout") + elif filename: + with zopen(filename, mode="rt") as file: # read in file + data = file.read().split("\n") + if len(data) == 0: + raise RuntimeError("lobsterout does not contain any data") + + # check if Lobster starts from a projection + self.is_restart_from_projection = "loading projection from projectionData.lobster..." in data + + self.lobster_version = self._get_lobster_version(data=data) + + self.number_of_threads = int(self._get_threads(data=data)) + self.dft_program = self._get_dft_program(data=data) + + self.number_of_spins = self._get_number_of_spins(data=data) + chargespilling, totalspilling = self._get_spillings(data=data, number_of_spins=self.number_of_spins) + self.charge_spilling = chargespilling + self.total_spilling = totalspilling + + elements, basistype, basisfunctions = self._get_elements_basistype_basisfunctions(data=data) + self.elements = elements + self.basis_type = basistype + self.basis_functions = basisfunctions + + wall_time, user_time, sys_time = self._get_timing(data=data) + self.timing = { + "wall_time": wall_time, + "user_time": user_time, + "sys_time": sys_time, + } + + warninglines = self._get_all_warning_lines(data=data) + self.warning_lines = warninglines + + orthowarning = self._get_warning_orthonormalization(data=data) + self.info_orthonormalization = orthowarning + + infos = self._get_all_info_lines(data=data) + self.info_lines = infos + + self.has_doscar = "writing DOSCAR.lobster..." in data and "SKIPPING writing DOSCAR.lobster..." not in data + self.has_doscar_lso = ( + "writing DOSCAR.LSO.lobster..." in data and "SKIPPING writing DOSCAR.LSO.lobster..." not in data + ) + self.has_cohpcar = ( + "writing COOPCAR.lobster and ICOOPLIST.lobster..." in data + and "SKIPPING writing COOPCAR.lobster and ICOOPLIST.lobster..." not in data + ) + self.has_coopcar = ( + "writing COHPCAR.lobster and ICOHPLIST.lobster..." in data + and "SKIPPING writing COHPCAR.lobster and ICOHPLIST.lobster..." not in data + ) + self.has_cobicar = ( + "writing COBICAR.lobster and ICOBILIST.lobster..." in data + and "SKIPPING writing COBICAR.lobster and ICOBILIST.lobster..." not in data + ) + + self.has_charge = "SKIPPING writing CHARGE.lobster..." not in data + self.has_projection = "saving projection to projectionData.lobster..." in data + self.has_bandoverlaps = ( + "WARNING: I dumped the band overlap matrices to the file bandOverlaps.lobster." in data + ) + self.has_fatbands = self._has_fatband(data=data) + self.has_grosspopulation = "writing CHARGE.lobster and GROSSPOP.lobster..." in data + self.has_density_of_energies = "writing DensityOfEnergy.lobster..." in data + self.has_madelung = ( + "writing SitePotentials.lobster and MadelungEnergies.lobster..." in data + and "skipping writing SitePotentials.lobster and MadelungEnergies.lobster..." not in data + ) + else: + raise ValueError("must provide either filename or kwargs to initialize Lobsterout") + + def get_doc(self) -> dict[str, Any]: + """Get the LobsterDict with all the information stored in lobsterout.""" + return { + # Check if LOBSTER starts from a projection + "restart_from_projection": self.is_restart_from_projection, + "lobster_version": self.lobster_version, + "threads": self.number_of_threads, + "dft_program": self.dft_program, + "charge_spilling": self.charge_spilling, + "total_spilling": self.total_spilling, + "elements": self.elements, + "basis_type": self.basis_type, + "basis_functions": self.basis_functions, + "timing": self.timing, + "warning_lines": self.warning_lines, + "info_orthonormalization": self.info_orthonormalization, + "info_lines": self.info_lines, + "has_doscar": self.has_doscar, + "has_doscar_lso": self.has_doscar_lso, + "has_cohpcar": self.has_cohpcar, + "has_coopcar": self.has_coopcar, + "has_cobicar": self.has_cobicar, + "has_charge": self.has_charge, + "has_madelung": self.has_madelung, + "has_projection": self.has_projection, + "has_bandoverlaps": self.has_bandoverlaps, + "has_fatbands": self.has_fatbands, + "has_grosspopulation": self.has_grosspopulation, + "has_density_of_energies": self.has_density_of_energies, + } + + def as_dict(self) -> dict: + """MSONable dict.""" + dct = dict(vars(self)) + dct["@module"] = type(self).__module__ + dct["@class"] = type(self).__name__ + + return dct + + @staticmethod + def _get_lobster_version(data): + for row in data: + splitrow = row.split() + if len(splitrow) > 1 and splitrow[0] == "LOBSTER": + return splitrow[1] + raise RuntimeError("Version not found.") + + @staticmethod + def _has_fatband(data): + for row in data: + splitrow = row.split() + if len(splitrow) > 1 and splitrow[1] == "FatBand": + return True + return False + + @staticmethod + def _get_dft_program(data): + for row in data: + splitrow = row.split() + if len(splitrow) > 4 and splitrow[3] == "program...": + return splitrow[4] + return None + + @staticmethod + def _get_number_of_spins(data): + return 2 if "spillings for spin channel 2" in data else 1 + + @staticmethod + def _get_threads(data): + for row in data: + splitrow = row.split() + if len(splitrow) > 11 and splitrow[11] in {"threads", "thread"}: + return splitrow[10] + raise ValueError("Threads not found.") + + @staticmethod + def _get_spillings(data, number_of_spins): + charge_spilling = [] + total_spilling = [] + for row in data: + splitrow = row.split() + if len(splitrow) > 2 and splitrow[2] == "spilling:": + if splitrow[1] == "charge": + charge_spilling += [np.float64(splitrow[3].replace("%", "")) / 100.0] + if splitrow[1] == "total": + total_spilling += [np.float64(splitrow[3].replace("%", "")) / 100.0] + + if len(charge_spilling) == number_of_spins and len(total_spilling) == number_of_spins: + break + + return charge_spilling, total_spilling + + @staticmethod + def _get_elements_basistype_basisfunctions(data): + begin = False + end = False + elements = [] + basistype = [] + basisfunctions = [] + for row in data: + if begin and not end: + splitrow = row.split() + if splitrow[0] not in [ + "INFO:", + "WARNING:", + "setting", + "calculating", + "post-processing", + "saving", + "spillings", + "writing", + ]: + elements += [splitrow[0]] + basistype += [splitrow[1].replace("(", "").replace(")", "")] + # last sign is a '' + basisfunctions += [splitrow[2:]] + else: + end = True + if "setting up local basis functions..." in row: + begin = True + return elements, basistype, basisfunctions + + @staticmethod + def _get_timing(data): + # Will give back wall, user and sys time + begin = False + user_time, wall_time, sys_time = [], [], [] + + for row in data: + splitrow = row.split() + if "finished" in splitrow: + begin = True + if begin: + if "wall" in splitrow: + wall_time = splitrow[2:10] + if "user" in splitrow: + user_time = splitrow[:8] + if "sys" in splitrow: + sys_time = splitrow[:8] + + wall_time_dict = {"h": wall_time[0], "min": wall_time[2], "s": wall_time[4], "ms": wall_time[6]} + user_time_dict = {"h": user_time[0], "min": user_time[2], "s": user_time[4], "ms": user_time[6]} + sys_time_dict = {"h": sys_time[0], "min": sys_time[2], "s": sys_time[4], "ms": sys_time[6]} + + return wall_time_dict, user_time_dict, sys_time_dict + + @staticmethod + def _get_warning_orthonormalization(data): + orthowarning = [] + for row in data: + splitrow = row.split() + if "orthonormalized" in splitrow: + orthowarning += [" ".join(splitrow[1:])] + return orthowarning + + @staticmethod + def _get_all_warning_lines(data): + ws = [] + for row in data: + splitrow = row.split() + if len(splitrow) > 0 and splitrow[0] == "WARNING:": + ws += [" ".join(splitrow[1:])] + return ws + + @staticmethod + def _get_all_info_lines(data): + infos = [] + for row in data: + splitrow = row.split() + if len(splitrow) > 0 and splitrow[0] == "INFO:": + infos += [" ".join(splitrow[1:])] + return infos + + +class Fatband: + """Read in FATBAND_x_y.lobster files. + + Attributes: + efermi (float): Fermi energy read in from vasprun.xml. + eigenvals (dict[Spin, np.ndarray]): Eigenvalues as a dictionary of numpy arrays of shape (nbands, nkpoints). + The first index of the array refers to the band and the second to the index of the kpoint. + The kpoints are ordered according to the order of the kpoints_array attribute. + If the band structure is not spin polarized, we only store one data set under Spin.up. + is_spin_polarized (bool): Boolean that tells you whether this was a spin-polarized calculation. + kpoints_array (list[np.ndarray]): List of kpoints as numpy arrays, in frac_coords of the given + lattice by default. + label_dict (dict[str, Union[str, np.ndarray]]): Dictionary that links a kpoint (in frac coords or Cartesian + coordinates depending on the coords attribute) to a label. + lattice (Lattice): Lattice object of reciprocal lattice as read in from vasprun.xml. + nbands (int): Number of bands used in the calculation. + p_eigenvals (dict[Spin, np.ndarray]): Dictionary of orbital projections as {spin: array of dict}. + The indices of the array are [band_index, kpoint_index]. + The dict is then built the following way: {"string of element": "string of orbital as read in + from FATBAND file"}. If the band structure is not spin polarized, we only store one data set under Spin.up. + structure (Structure): Structure read in from Structure object. + """ + + def __init__( + self, + filenames: PathLike | list = ".", + kpoints_file: PathLike = "KPOINTS", + vasprun_file: PathLike | None = "vasprun.xml", + structure: Structure | IStructure | None = None, + efermi: float | None = None, + ): + """ + Args: + filenames (list or string): can be a list of file names or a path to a folder from which all + "FATBAND_*" files will be read + kpoints_file (PathLike): KPOINTS file for bandstructure calculation, typically "KPOINTS". + vasprun_file (PathLike): Corresponding vasprun file. + Instead, the Fermi energy from the DFT run can be provided. Then, + this value should be set to None. + structure (Structure): Structure object. + efermi (float): fermi energy in eV. + """ + warnings.warn("Make sure all relevant FATBAND files were generated and read in!") + warnings.warn("Use Lobster 3.2.0 or newer for fatband calculations!") + + if structure is None: + raise ValueError("A structure object has to be provided") + self.structure = structure + if vasprun_file is None and efermi is None: + raise ValueError("vasprun_file or efermi have to be provided") + + self.lattice = self.structure.lattice.reciprocal_lattice + if vasprun_file is not None: + self.efermi = Vasprun( + filename=vasprun_file, + ionic_step_skip=None, + ionic_step_offset=0, + parse_dos=True, + parse_eigen=False, + parse_projected_eigen=False, + parse_potcar_file=False, + occu_tol=1e-8, + exception_on_bad_xml=True, + ).efermi + else: + self.efermi = efermi + kpoints_object = Kpoints.from_file(kpoints_file) + + atom_type = [] + atom_names = [] + orbital_names = [] + parameters = [] + + if not isinstance(filenames, list) or filenames is None: + filenames_new = [] + if filenames is None: + filenames = "." + for name in os.listdir(filenames): + if fnmatch.fnmatch(name, "FATBAND_*.lobster"): + filenames_new += [os.path.join(filenames, name)] + filenames = filenames_new + if len(filenames) == 0: + raise ValueError("No FATBAND files in folder or given") + for name in filenames: + with zopen(name, mode="rt") as file: + contents = file.read().split("\n") + + atom_names += [os.path.split(name)[1].split("_")[1].capitalize()] + parameters = contents[0].split() + atom_type += [re.split(r"[0-9]+", parameters[3])[0].capitalize()] + orbital_names += [parameters[4]] + + # get atomtype orbital dict + atom_orbital_dict = {} # type: dict + for iatom, atom in enumerate(atom_names): + if atom not in atom_orbital_dict: + atom_orbital_dict[atom] = [] + atom_orbital_dict[atom] += [orbital_names[iatom]] + # test if there are the same orbitals twice or if two different formats were used or if all necessary orbitals + # are there + for items in atom_orbital_dict.values(): + if len(set(items)) != len(items): + raise ValueError("The are two FATBAND files for the same atom and orbital. The program will stop.") + split = [] + for item in items: + split += [item.split("_")[0]] + for number in collections.Counter(split).values(): + if number not in (1, 3, 5, 7): + raise ValueError( + "Make sure all relevant orbitals were generated and that no duplicates (2p and 2p_x) are " + "present" + ) + + kpoints_array: list = [] + eigenvals: dict = {} + p_eigenvals: dict = {} + for ifilename, filename in enumerate(filenames): + with zopen(filename, mode="rt") as file: + contents = file.read().split("\n") + + if ifilename == 0: + self.nbands = int(parameters[6]) + self.number_kpts = kpoints_object.num_kpts - int(contents[1].split()[2]) + 1 + + if len(contents[1:]) == self.nbands + 2: + self.is_spinpolarized = False + elif len(contents[1:]) == self.nbands * 2 + 2: + self.is_spinpolarized = True + else: + linenumbers = [] + for iline, line in enumerate(contents[1 : self.nbands * 2 + 4]): + if line.split()[0] == "#": + linenumbers += [iline] + + if ifilename == 0: + self.is_spinpolarized = len(linenumbers) == 2 + + if ifilename == 0: + eigenvals = {} + eigenvals[Spin.up] = [[defaultdict(float) for _ in range(self.number_kpts)] for _ in range(self.nbands)] + if self.is_spinpolarized: + eigenvals[Spin.down] = [ + [defaultdict(float) for _ in range(self.number_kpts)] for _ in range(self.nbands) + ] + + p_eigenvals = {} + p_eigenvals[Spin.up] = [ + [ + { + str(elem): {str(orb): defaultdict(float) for orb in atom_orbital_dict[elem]} + for elem in atom_names + } + for _ in range(self.number_kpts) + ] + for _ in range(self.nbands) + ] + + if self.is_spinpolarized: + p_eigenvals[Spin.down] = [ + [ + { + str(elem): {str(orb): defaultdict(float) for orb in atom_orbital_dict[elem]} + for elem in atom_names + } + for _ in range(self.number_kpts) + ] + for _ in range(self.nbands) + ] + + idx_kpt = -1 + linenumber = 0 + iband = 0 + for line in contents[1:-1]: + if line.split()[0] == "#": + KPOINT = np.array( + [ + float(line.split()[4]), + float(line.split()[5]), + float(line.split()[6]), + ] + ) + if ifilename == 0: + kpoints_array += [KPOINT] + + linenumber = 0 + iband = 0 + idx_kpt += 1 + if linenumber == self.nbands: + iband = 0 + if line.split()[0] != "#": + if linenumber < self.nbands: + if ifilename == 0 and self.efermi is not None: + eigenvals[Spin.up][iband][idx_kpt] = float(line.split()[1]) + self.efermi + + p_eigenvals[Spin.up][iband][idx_kpt][atom_names[ifilename]][orbital_names[ifilename]] = float( + line.split()[2] + ) + if linenumber >= self.nbands and self.is_spinpolarized: + if ifilename == 0 and self.efermi is not None: + eigenvals[Spin.down][iband][idx_kpt] = float(line.split()[1]) + self.efermi + p_eigenvals[Spin.down][iband][idx_kpt][atom_names[ifilename]][orbital_names[ifilename]] = float( + line.split()[2] + ) + + linenumber += 1 + iband += 1 + + self.kpoints_array = kpoints_array + self.eigenvals = eigenvals + self.p_eigenvals = p_eigenvals + + label_dict = {} + if kpoints_object.labels is not None: + for idx, label in enumerate(kpoints_object.labels[-self.number_kpts :], start=0): + if label is not None: + label_dict[label] = kpoints_array[idx] + + self.label_dict = label_dict + + def get_bandstructure(self) -> LobsterBandStructureSymmLine: + """Get a LobsterBandStructureSymmLine object which can be plotted with a normal BSPlotter.""" + return LobsterBandStructureSymmLine( + kpoints=self.kpoints_array, + eigenvals=self.eigenvals, + lattice=self.lattice, + efermi=self.efermi, + labels_dict=self.label_dict, + structure=self.structure, + projections=self.p_eigenvals, + ) + + +class Bandoverlaps(MSONable): + """Read in bandOverlaps.lobster files. These files are not created during every Lobster run. + + Attributes: + band_overlaps_dict (dict[Spin, Dict[str, Dict[str, Union[float, np.ndarray]]]]): A dictionary + containing the band overlap data of the form: {spin: {"kpoint as string": {"maxDeviation": + float that describes the max deviation, "matrix": 2D array of the size number of bands + times number of bands including the overlap matrices with}}}. + max_deviation (list[float]): A list of floats describing the maximal deviation for each problematic kpoint. + """ + + def __init__( + self, + filename: str = "bandOverlaps.lobster", + band_overlaps_dict: dict[Any, dict] | None = None, # Any is spin number 1 or -1 + max_deviation: list[float] | None = None, + ): + """ + Args: + filename: filename of the "bandOverlaps.lobster" file. + band_overlaps_dict: A dictionary containing the band overlap data of the form: {spin: { + "k_points" : list of k-point array, + "max_deviations": list of max deviations associated with each k-point, + "matrices": list of the overlap matrices associated with each k-point + }}. + max_deviation (list[float]): A list of floats describing the maximal deviation for each problematic k-point. + """ + self._filename = filename + self.band_overlaps_dict = {} if band_overlaps_dict is None else band_overlaps_dict + self.max_deviation = [] if max_deviation is None else max_deviation + + if not self.band_overlaps_dict: + with zopen(filename, mode="rt") as file: + contents = file.read().split("\n") + + spin_numbers = [0, 1] if contents[0].split()[-1] == "0" else [1, 2] + + self._filename = filename + self._read(contents, spin_numbers) + + def _read(self, contents: list, spin_numbers: list): + """ + Will read in all contents of the file. + + Args: + contents: list of strings + spin_numbers: list of spin numbers depending on `Lobster` version. + """ + spin: Spin = Spin.up + kpoint_array: list = [] + overlaps: list = [] + # This has to be done like this because there can be different numbers of problematic k-points per spin + for line in contents: + if f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[0]}" in line: + spin = Spin.up + elif f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[1]}" in line: + spin = Spin.down + elif "k-point" in line: + kpoint = line.split(" ") + kpoint_array = [] + for kpointel in kpoint: + if kpointel not in {"at", "k-point", ""}: + kpoint_array += [float(kpointel)] + + elif "maxDeviation" in line: + if spin not in self.band_overlaps_dict: + self.band_overlaps_dict[spin] = {} + if "k_points" not in self.band_overlaps_dict[spin]: + self.band_overlaps_dict[spin]["k_points"] = [] + if "max_deviations" not in self.band_overlaps_dict[spin]: + self.band_overlaps_dict[spin]["max_deviations"] = [] + if "matrices" not in self.band_overlaps_dict[spin]: + self.band_overlaps_dict[spin]["matrices"] = [] + maxdev = line.split(" ")[2] + self.band_overlaps_dict[spin]["max_deviations"] += [float(maxdev)] + self.band_overlaps_dict[spin]["k_points"] += [kpoint_array] + self.max_deviation += [float(maxdev)] + overlaps = [] + + else: + rows = [] + for el in line.split(" "): + if el != "": + rows += [float(el)] + overlaps += [rows] + if len(overlaps) == len(rows): + self.band_overlaps_dict[spin]["matrices"] += [np.matrix(overlaps)] + + def has_good_quality_maxDeviation(self, limit_maxDeviation: float = 0.1) -> bool: + """ + Will check if the maxDeviation from the ideal bandoverlap is smaller or equal to limit_maxDeviation. + + Args: + limit_maxDeviation: limit of the maxDeviation + + Returns: + Boolean that will give you information about the quality of the projection. + """ + return all(deviation <= limit_maxDeviation for deviation in self.max_deviation) + + def has_good_quality_check_occupied_bands( + self, + number_occ_bands_spin_up: int, + number_occ_bands_spin_down: int | None = None, + spin_polarized: bool = False, + limit_deviation: float = 0.1, + ) -> bool: + """ + Will check if the deviation from the ideal bandoverlap of all occupied bands + is smaller or equal to limit_deviation. + + Args: + number_occ_bands_spin_up (int): number of occupied bands of spin up + number_occ_bands_spin_down (int): number of occupied bands of spin down + spin_polarized (bool): If True, then it was a spin polarized calculation + limit_deviation (float): limit of the maxDeviation + + Returns: + Boolean that will give you information about the quality of the projection + """ + for matrix in self.band_overlaps_dict[Spin.up]["matrices"]: + for iband1, band1 in enumerate(matrix): + for iband2, band2 in enumerate(band1): + if iband1 < number_occ_bands_spin_up and iband2 < number_occ_bands_spin_up: + if iband1 == iband2: + if abs(band2 - 1.0).all() > limit_deviation: + return False + elif band2.all() > limit_deviation: + return False + + if spin_polarized: + for matrix in self.band_overlaps_dict[Spin.down]["matrices"]: + for iband1, band1 in enumerate(matrix): + for iband2, band2 in enumerate(band1): + if number_occ_bands_spin_down is None: + raise ValueError("number_occ_bands_spin_down has to be specified") + + if iband1 < number_occ_bands_spin_down and iband2 < number_occ_bands_spin_down: + if iband1 == iband2: + if abs(band2 - 1.0).all() > limit_deviation: + return False + elif band2.all() > limit_deviation: + return False + + return True + + @property + def bandoverlapsdict(self): + msg = "`bandoverlapsdict` attribute is deprecated. Use `band_overlaps_dict` instead." + warnings.warn(msg, DeprecationWarning, stacklevel=2) + return self.band_overlaps_dict + + +class Grosspop(MSONable): + """Read in GROSSPOP.lobster files. + + Attributes: + list_dict_grosspop (list[dict[str, str| dict[str, str]]]): List of dictionaries + including all information about the grosspopulations. Each dictionary contains the following keys: + - 'element': The element symbol of the atom. + - 'Mulliken GP': A dictionary of Mulliken gross populations, where the keys are the orbital labels and the + values are the corresponding gross populations as strings. + - 'Loewdin GP': A dictionary of Loewdin gross populations, where the keys are the orbital labels and the + values are the corresponding gross populations as strings. + The 0th entry of the list refers to the first atom in GROSSPOP.lobster and so on. + """ + + def __init__(self, filename: str = "GROSSPOP.lobster", list_dict_grosspop: list[dict] | None = None): + """ + Args: + filename: filename of the "GROSSPOP.lobster" file + list_dict_grosspop: List of dictionaries including all information about the gross populations. + """ + # opens file + self._filename = filename + self.list_dict_grosspop = [] if list_dict_grosspop is None else list_dict_grosspop + if not self.list_dict_grosspop: + with zopen(filename, mode="rt") as file: + contents = file.read().split("\n") + # transfers content of file to list of dict + small_dict: dict[str, Any] = {} + for line in contents[3:]: + cleanline = [i for i in line.split(" ") if i != ""] + if len(cleanline) == 5: + small_dict = {} + small_dict["Mulliken GP"] = {} + small_dict["Loewdin GP"] = {} + small_dict["element"] = cleanline[1] + small_dict["Mulliken GP"][cleanline[2]] = float(cleanline[3]) + small_dict["Loewdin GP"][cleanline[2]] = float(cleanline[4]) + elif len(cleanline) > 0: + small_dict["Mulliken GP"][cleanline[0]] = float(cleanline[1]) + small_dict["Loewdin GP"][cleanline[0]] = float(cleanline[2]) + if "total" in cleanline[0]: + self.list_dict_grosspop += [small_dict] + + def get_structure_with_total_grosspop(self, structure_filename: str) -> Structure: + """Get a Structure with Mulliken and Loewdin total grosspopulations as site properties. + + Args: + structure_filename (str): filename of POSCAR + + Returns: + Structure Object with Mulliken and Loewdin total grosspopulations as site properties. + """ + struct = Structure.from_file(structure_filename) + # site_properties: dict[str, Any] = {} + mullikengp = [] + loewdingp = [] + for grosspop in self.list_dict_grosspop: + mullikengp += [grosspop["Mulliken GP"]["total"]] + loewdingp += [grosspop["Loewdin GP"]["total"]] + + site_properties = { + "Total Mulliken GP": mullikengp, + "Total Loewdin GP": loewdingp, + } + return struct.copy(site_properties=site_properties) + + +class Wavefunction: + """Read in wave function files from Lobster and transfer them into an object of the type VolumetricData. + + Attributes: + grid (tuple[int, int, int]): Grid for the wave function [Nx+1,Ny+1,Nz+1]. + points (list[Tuple[float, float, float]]): List of points. + real (list[float]): List of real part of wave function. + imaginary (list[float]): List of imaginary part of wave function. + distance (list[float]): List of distance to first point in wave function file. + """ + + def __init__(self, filename, structure): + """ + Args: + filename: filename of wavecar file from Lobster + structure: Structure object (e.g., created by Structure.from_file("")). + """ + self.filename = filename + self.structure = structure + self.grid, self.points, self.real, self.imaginary, self.distance = Wavefunction._parse_file(filename) + + @staticmethod + def _parse_file(filename): + with zopen(filename, mode="rt") as file: + contents = file.read().split("\n") + points = [] + distance = [] + real = [] + imaginary = [] + splitline = contents[0].split() + grid = [int(splitline[7]), int(splitline[8]), int(splitline[9])] + for line in contents[1:]: + splitline = line.split() + if len(splitline) >= 6: + points += [[float(splitline[0]), float(splitline[1]), float(splitline[2])]] + distance += [float(splitline[3])] + real += [float(splitline[4])] + imaginary += [float(splitline[5])] + + if len(real) != grid[0] * grid[1] * grid[2] or len(imaginary) != grid[0] * grid[1] * grid[2]: + raise ValueError("Something went wrong while reading the file") + + return grid, points, real, imaginary, distance + + def set_volumetric_data(self, grid, structure): + """ + Will create the VolumetricData Objects. + + Args: + grid: grid on which wavefunction was calculated, e.g. [1,2,2] + structure: Structure object + """ + Nx = grid[0] - 1 + Ny = grid[1] - 1 + Nz = grid[2] - 1 + a = structure.lattice.matrix[0] + b = structure.lattice.matrix[1] + c = structure.lattice.matrix[2] + new_x = [] + new_y = [] + new_z = [] + new_real = [] + new_imaginary = [] + new_density = [] + + for runner, (x, y, z) in enumerate(itertools.product(range(Nx + 1), range(Ny + 1), range(Nz + 1))): + x_here = x / float(Nx) * a[0] + y / float(Ny) * b[0] + z / float(Nz) * c[0] + y_here = x / float(Nx) * a[1] + y / float(Ny) * b[1] + z / float(Nz) * c[1] + z_here = x / float(Nx) * a[2] + y / float(Ny) * b[2] + z / float(Nz) * c[2] + + if x != Nx and y != Ny and z != Nz: + if ( + not np.isclose(self.points[runner][0], x_here, 1e-3) + and not np.isclose(self.points[runner][1], y_here, 1e-3) + and not np.isclose(self.points[runner][2], z_here, 1e-3) + ): + raise ValueError( + "The provided wavefunction from Lobster does not contain all relevant" + " points. " + "Please use a line similar to: printLCAORealSpaceWavefunction kpoint 1 " + "coordinates 0.0 0.0 0.0 coordinates 1.0 1.0 1.0 box bandlist 1 " + ) + + new_x += [x_here] + new_y += [y_here] + new_z += [z_here] + + new_real += [self.real[runner]] + new_imaginary += [self.imaginary[runner]] + new_density += [self.real[runner] ** 2 + self.imaginary[runner] ** 2] + + self.final_real = np.reshape(new_real, [Nx, Ny, Nz]) + self.final_imaginary = np.reshape(new_imaginary, [Nx, Ny, Nz]) + self.final_density = np.reshape(new_density, [Nx, Ny, Nz]) + + self.volumetricdata_real = VolumetricData(structure, {"total": self.final_real}) + self.volumetricdata_imaginary = VolumetricData(structure, {"total": self.final_imaginary}) + self.volumetricdata_density = VolumetricData(structure, {"total": self.final_density}) + + def get_volumetricdata_real(self): + """ + Will return a VolumetricData object including the real part of the wave function. + + Returns: + VolumetricData + """ + if not hasattr(self, "volumetricdata_real"): + self.set_volumetric_data(self.grid, self.structure) + return self.volumetricdata_real + + def get_volumetricdata_imaginary(self): + """ + Will return a VolumetricData object including the imaginary part of the wave function. + + Returns: + VolumetricData + """ + if not hasattr(self, "volumetricdata_imaginary"): + self.set_volumetric_data(self.grid, self.structure) + return self.volumetricdata_imaginary + + def get_volumetricdata_density(self): + """ + Will return a VolumetricData object including the imaginary part of the wave function. + + Returns: + VolumetricData + """ + if not hasattr(self, "volumetricdata_density"): + self.set_volumetric_data(self.grid, self.structure) + return self.volumetricdata_density + + def write_file(self, filename="WAVECAR.vasp", part="real"): + """ + Will save the wavefunction in a file format that can be read by VESTA + This will only work if the wavefunction from lobster was constructed with: + "printLCAORealSpaceWavefunction kpoint 1 coordinates 0.0 0.0 0.0 coordinates 1.0 1.0 1.0 box bandlist 1 2 3 4 + 5 6 " + or similar (the whole unit cell has to be covered!). + + Args: + filename: Filename for the output, e.g. WAVECAR.vasp + part: which part of the wavefunction will be saved ("real" or "imaginary") + """ + if not ( + hasattr(self, "volumetricdata_real") + and hasattr(self, "volumetricdata_imaginary") + and hasattr(self, "volumetricdata_density") + ): + self.set_volumetric_data(self.grid, self.structure) + if part == "real": + self.volumetricdata_real.write_file(filename) + elif part == "imaginary": + self.volumetricdata_imaginary.write_file(filename) + elif part == "density": + self.volumetricdata_density.write_file(filename) + else: + raise ValueError('part can be only "real" or "imaginary" or "density"') + + +# madelung and site potential classes +class MadelungEnergies(MSONable): + """Read MadelungEnergies.lobster files generated by LOBSTER. + + Attributes: + madelungenergies_mulliken (float): Float that gives the Madelung energy based on the Mulliken approach. + madelungenergies_loewdin (float): Float that gives the Madelung energy based on the Loewdin approach. + ewald_splitting (float): Ewald splitting parameter to compute SitePotentials. + """ + + def __init__( + self, + filename: str = "MadelungEnergies.lobster", + ewald_splitting: float | None = None, + madelungenergies_mulliken: float | None = None, + madelungenergies_loewdin: float | None = None, + ): + """ + Args: + filename: filename of the "MadelungEnergies.lobster" file. + """ + self._filename = filename + self.ewald_splitting = None if ewald_splitting is None else ewald_splitting + self.madelungenergies_loewdin = None if madelungenergies_loewdin is None else madelungenergies_loewdin + self.madelungenergies_mulliken = None if madelungenergies_mulliken is None else madelungenergies_mulliken + + if self.ewald_splitting is None: + with zopen(filename, mode="rt") as file: + data = file.read().split("\n")[5] + if len(data) == 0: + raise RuntimeError("MadelungEnergies file contains no data.") + line = data.split() + self._filename = filename + self.ewald_splitting = float(line[0]) + self.madelungenergies_mulliken = float(line[1]) + self.madelungenergies_loewdin = float(line[2]) + + @property + def madelungenergies_Loewdin(self): + warnings.warn( + "`madelungenergies_Loewdin` attribute is deprecated. Use `madelungenergies_loewdin` instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.madelungenergies_loewdin + + @property + def madelungenergies_Mulliken(self): + warnings.warn( + "`madelungenergies_Mulliken` attribute is deprecated. Use `madelungenergies_mulliken` instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.madelungenergies_mulliken + + +class SitePotential(MSONable): + """Read SitePotentials.lobster files generated by LOBSTER. + + Attributes: + atomlist (list[str]): List of atoms in SitePotentials.lobster. + types (list[str]): List of types of atoms in SitePotentials.lobster. + num_atoms (int): Number of atoms in SitePotentials.lobster. + sitepotentials_mulliken (list[float]): List of Mulliken potentials of sites in SitePotentials.lobster. + sitepotentials_loewdin (list[float]): List of Loewdin potentials of sites in SitePotentials.lobster. + madelungenergies_mulliken (float): Float that gives the Madelung energy based on the Mulliken approach. + madelungenergies_loewdin (float): Float that gives the Madelung energy based on the Loewdin approach. + ewald_splitting (float): Ewald Splitting parameter to compute SitePotentials. + """ + + def __init__( + self, + filename: str = "SitePotentials.lobster", + ewald_splitting: float | None = None, + num_atoms: int | None = None, + atomlist: list[str] | None = None, + types: list[str] | None = None, + sitepotentials_loewdin: list[float] | None = None, + sitepotentials_mulliken: list[float] | None = None, + madelungenergies_mulliken: float | None = None, + madelungenergies_loewdin: float | None = None, + ): + """ + Args: + filename: filename for the SitePotentials file, typically "SitePotentials.lobster" + ewald_splitting: ewald splitting parameter used for computing madelung energies + num_atoms: number of atoms in the structure + atomlist: list of atoms in the structure + types: list of unique atom types in the structure + sitepotentials_loewdin: Loewdin site potential + sitepotentials_mulliken: Mulliken site potential + madelungenergies_loewdin: Madelung energy based on the Loewdin approach + madelungenergies_mulliken: Madelung energy based on the Mulliken approach. + """ + self._filename = filename + self.ewald_splitting = [] if ewald_splitting is None else ewald_splitting + self.num_atoms = num_atoms + self.types = [] if types is None else types + self.atomlist = [] if atomlist is None else atomlist + self.sitepotentials_loewdin = [] if sitepotentials_loewdin is None else sitepotentials_loewdin + self.sitepotentials_mulliken = [] if sitepotentials_mulliken is None else sitepotentials_mulliken + self.madelungenergies_loewdin = [] if madelungenergies_loewdin is None else madelungenergies_loewdin + self.madelungenergies_mulliken = [] if madelungenergies_mulliken is None else madelungenergies_mulliken + + if self.num_atoms is None: + # site_potentials + with zopen(filename, mode="rt") as file: + data = file.read().split("\n") + if len(data) == 0: + raise RuntimeError("SitePotentials file contains no data.") + + self._filename = filename + self.ewald_splitting = float(data[0].split()[9]) + + data = data[5:-1] + self.num_atoms = len(data) - 2 + for atom in range(self.num_atoms): + line = data[atom].split() + self.atomlist += [line[1] + str(line[0])] + self.types += [line[1]] + self.sitepotentials_mulliken += [float(line[2])] + self.sitepotentials_loewdin += [float(line[3])] + + self.madelungenergies_mulliken = float(data[self.num_atoms + 1].split()[3]) + self.madelungenergies_loewdin = float(data[self.num_atoms + 1].split()[4]) + + def get_structure_with_site_potentials(self, structure_filename): + """Get a Structure with Mulliken and Loewdin charges as site properties. + + Args: + structure_filename: filename of POSCAR + + Returns: + Structure Object with Mulliken and Loewdin charges as site properties. + """ + struct = Structure.from_file(structure_filename) + mulliken = self.sitepotentials_mulliken + loewdin = self.sitepotentials_loewdin + site_properties = { + "Mulliken Site Potentials (eV)": mulliken, + "Loewdin Site Potentials (eV)": loewdin, + } + return struct.copy(site_properties=site_properties) + + @property + def sitepotentials_Mulliken(self): + warnings.warn( + "`sitepotentials_Mulliken` attribute is deprecated. Use `sitepotentials_mulliken` instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.sitepotentials_mulliken + + @property + def sitepotentials_Loewdin(self): + warnings.warn( + "`sitepotentials_Loewdin` attribute is deprecated. Use `sitepotentials_loewdin` instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.sitepotentials_loewdin + + @property + def madelungenergies_Mulliken(self): + warnings.warn( + "`madelungenergies_Mulliken` attribute is deprecated. Use `madelungenergies_mulliken` instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.madelungenergies_mulliken + + @property + def madelungenergies_Loewdin(self): + warnings.warn( + "`madelungenergies_Loewdin` attribute is deprecated. Use `madelungenergies_loewdin` instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.madelungenergies_loewdin + + +def get_orb_from_str(orbs): + """ + Args: + orbs: list of two or more str, e.g. ["2p_x", "3s"]. + + Returns: + list of tw Orbital objects + """ + # TODO: also useful for plotting of DOS + orb_labs = [ + "s", + "p_y", + "p_z", + "p_x", + "d_xy", + "d_yz", + "d_z^2", + "d_xz", + "d_x^2-y^2", + "f_y(3x^2-y^2)", + "f_xyz", + "f_yz^2", + "f_z^3", + "f_xz^2", + "f_z(x^2-y^2)", + "f_x(x^2-3y^2)", + ] + orbitals = [(int(orb[0]), Orbital(orb_labs.index(orb[1:]))) for orb in orbs] + + orb_label = "" + for iorb, orbital in enumerate(orbitals): + if iorb == 0: + orb_label += f"{orbital[0]}{orbital[1].name}" + else: + orb_label += f"-{orbital[0]}{orbital[1].name}" + + return orb_label, orbitals + + +class LobsterMatrices: + """Read Matrices file generated by LOBSTER (e.g. hamiltonMatrices.lobster). + + Attributes: + for filename == "hamiltonMatrices.lobster" + onsite_energies (list[np.arrays]): List real part of onsite energies from the matrices each k-point. + average_onsite_energies (dict): dict with average onsite elements energies for all k-points with keys as + basis used in the LOBSTER computation (uses only real part of matrix). + hamilton_matrices (dict[np.arrays]) : dict with the complex hamilton matrix + at each k-point with k-point and spin as keys + + for filename == "coefficientMatrices.lobster" + + onsite_coefficients (list[np.arrays]): List real part of onsite coefficients from the matrices each k-point. + average_onsite_coefficient (dict): dict with average onsite elements coefficients for all k-points with keys as + basis used in the LOBSTER computation (uses only real part of matrix). + coefficient_matrices (dict[np.arrays]) : dict with the coefficients matrix + at each k-point with k-point and spin as keys + + for filename == "transferMatrices.lobster" + + onsite_transfer (list[np.arrays]): List real part of onsite transfer coefficients from the matrices at each + k-point. + average_onsite_transfer (dict): dict with average onsite elements transfer coefficients for all k-points with + keys as basis used in the LOBSTER computation (uses only real part of matrix). + transfer_matrices (dict[np.arrays]) : dict with the coefficients matrix at + each k-point with k-point and spin as keys + + for filename == "overlapMatrices.lobster" + + onsite_overlaps (list[np.arrays]): List real part of onsite overlaps from the matrices each k-point. + average_onsite_overlaps (dict): dict with average onsite elements overlaps for all k-points with keys as + basis used in the LOBSTER computation (uses only real part of matrix). + overlap_matrices (dict[np.arrays]) : dict with the overlap matrix at + each k-point with k-point as keys + """ + + def __init__(self, e_fermi=None, filename: str = "hamiltonMatrices.lobster"): + """ + Args: + filename: filename for the hamiltonMatrices file, typically "hamiltonMatrices.lobster". + e_fermi: fermi level in eV for the structure only + relevant if input file contains hamilton matrices data. + """ + self._filename = filename + # hamiltonMatrices + with zopen(self._filename, mode="rt") as file: + file_data = file.readlines() + if len(file_data) == 0: + raise RuntimeError("Please check provided input file, it seems to be empty") + + pattern_coeff_hamil_trans = r"(\d+)\s+kpoint\s+(\d+)" # regex pattern to extract spin and k-point number + pattern_overlap = r"kpoint\s+(\d+)" # regex pattern to extract k-point number + + if "hamilton" in self._filename: + if e_fermi is None: + raise ValueError("Please provide the fermi energy in eV ") + self.onsite_energies, self.average_onsite_energies, self.hamilton_matrices = self._parse_matrix( + file_data=file_data, pattern=pattern_coeff_hamil_trans, e_fermi=e_fermi + ) + + elif "coefficient" in self._filename: + self.onsite_coefficients, self.average_onsite_coefficient, self.coefficient_matrices = self._parse_matrix( + file_data=file_data, pattern=pattern_coeff_hamil_trans, e_fermi=0 + ) + + elif "transfer" in self._filename: + self.onsite_transfer, self.average_onsite_transfer, self.transfer_matrices = self._parse_matrix( + file_data=file_data, pattern=pattern_coeff_hamil_trans, e_fermi=0 + ) + + elif "overlap" in self._filename: + self.onsite_overlaps, self.average_onsite_overlaps, self.overlap_matrices = self._parse_matrix( + file_data=file_data, pattern=pattern_overlap, e_fermi=0 + ) + + @staticmethod + def _parse_matrix(file_data, pattern, e_fermi): + complex_matrices = {} + matrix_diagonal_values = [] + start_inxs_real = [] + end_inxs_real = [] + start_inxs_imag = [] + end_inxs_imag = [] + # get indices of real and imaginary part of matrix for each k point + for idx, line in enumerate(file_data): + line = line.strip() + if "Real parts" in line: + start_inxs_real += [idx + 1] + if idx == 1: # ignore the first occurrence as files start with real matrices + pass + else: + end_inxs_imag += [idx - 1] + matches = re.search(pattern, file_data[idx - 1]) + if matches and len(matches.groups()) == 2: + k_point = matches.group(2) + complex_matrices[k_point] = {} + if "Imag parts" in line: + end_inxs_real += [idx - 1] + start_inxs_imag += [idx + 1] + # explicitly add the last line as files end with imaginary matrix + if idx == len(file_data) - 1: + end_inxs_imag += [len(file_data)] + + # extract matrix data and store diagonal elements + matrix_real = [] + matrix_imag = [] + for start_inx_real, end_inx_real, start_inx_imag, end_inx_imag in zip( + start_inxs_real, end_inxs_real, start_inxs_imag, end_inxs_imag + ): + # matrix with text headers + matrix_real = file_data[start_inx_real:end_inx_real] + matrix_imag = file_data[start_inx_imag:end_inx_imag] + + # extract only numerical data and convert to numpy arrays + matrix_array_real = np.array([line.split()[1:] for line in matrix_real[1:]], dtype=float) + matrix_array_imag = np.array([line.split()[1:] for line in matrix_imag[1:]], dtype=float) + + # combine real and imaginary parts to create a complex matrix + comp_matrix = matrix_array_real + 1j * matrix_array_imag + + matches = re.search(pattern, file_data[start_inx_real - 2]) + if matches and len(matches.groups()) == 2: + spin = Spin.up if matches.group(1) == "1" else Spin.down + k_point = matches.group(2) + complex_matrices[k_point].update({spin: comp_matrix}) + elif matches and len(matches.groups()) == 1: + k_point = matches.group(1) + complex_matrices.update({k_point: comp_matrix}) + matrix_diagonal_values += [comp_matrix.real.diagonal() - e_fermi] + + # extract elements basis functions as list + elements_basis_functions = [ + line.split()[:1][0] for line in matrix_real if line.split()[:1][0] != "basisfunction" + ] + + # get average row-wise + average_matrix_diagonal_values = np.array(matrix_diagonal_values, dtype=float).mean(axis=0) + + # get a dict with basis functions as keys and average values as values + average_average_matrix_diag_dict = dict(zip(elements_basis_functions, average_matrix_diagonal_values)) + + return matrix_diagonal_values, average_average_matrix_diag_dict, complex_matrices From 6dfae03ab4931496fae8a9a8fdc343c2a979898a Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Wed, 26 Jun 2024 14:07:43 -0700 Subject: [PATCH 046/180] Move to src layout. --- cohp.py | 1366 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1366 insertions(+) create mode 100644 cohp.py diff --git a/cohp.py b/cohp.py new file mode 100644 index 0000000000..17bcdce0be --- /dev/null +++ b/cohp.py @@ -0,0 +1,1366 @@ +"""This module defines classes to represent crystal orbital Hamilton +populations (COHP) and integrated COHP (ICOHP), but can also be used +for crystal orbital overlap populations (COOP) or crystal orbital bond indices (COBIs). +If you use this module, please cite: +J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, +"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +ChemPlusChem 2022, e202200123, +DOI: 10.1002/cplu.202200123. +""" + +from __future__ import annotations + +import re +import sys +import warnings +from typing import TYPE_CHECKING + +import numpy as np +from monty.json import MSONable +from pymatgen.core.sites import PeriodicSite +from pymatgen.core.structure import Structure +from pymatgen.electronic_structure.core import Orbital, Spin +from pymatgen.io.lmto import LMTOCopl +from pymatgen.io.lobster import Cohpcar +from pymatgen.util.coord import get_linear_interpolated_value +from pymatgen.util.due import Doi, due +from pymatgen.util.num import round_to_sigfigs +from scipy.interpolate import InterpolatedUnivariateSpline + +if TYPE_CHECKING: + from typing import Any + + from typing_extensions import Self + +__author__ = "Marco Esters, Janine George" +__copyright__ = "Copyright 2017, The Materials Project" +__version__ = "0.2" +__maintainer__ = "Janine George" +__email__ = "janinegeorge.ulfen@gmail.com" +__date__ = "Dec 13, 2017" + +due.cite( + Doi("10.1002/cplu.202200123"), + description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +) + + +class Cohp(MSONable): + """Basic COHP object.""" + + def __init__( + self, efermi, energies, cohp, are_coops=False, are_cobis=False, are_multi_center_cobis=False, icohp=None + ) -> None: + """ + Args: + are_coops: Indicates whether this object describes COOPs. + are_cobis: Indicates whether this object describes COBIs. + are_multi_center_cobis: Indicates whether this object describes multi-center COBIs + efermi: Fermi energy. + energies: A sequence of energies. + cohp ({Spin: np.array}): representing the COHP for each spin. + icohp ({Spin: np.array}): representing the ICOHP for each spin. + """ + self.are_coops = are_coops + self.are_cobis = are_cobis + self.are_multi_center_cobis = are_multi_center_cobis + self.efermi = efermi + self.energies = np.array(energies) + self.cohp = cohp + self.icohp = icohp + + def __repr__(self) -> str: + """Get a string that can be easily plotted (e.g. using gnuplot).""" + if self.are_coops: + cohp_str = "COOP" + elif self.are_cobis or self.are_multi_center_cobis: + cohp_str = "COBI" + else: + cohp_str = "COHP" + + header = ["Energy", f"{cohp_str}Up"] + data = [self.energies, self.cohp[Spin.up]] + if Spin.down in self.cohp: + header.append(f"{cohp_str}Down") + data.append(self.cohp[Spin.down]) + if self.icohp: + header.append(f"I{cohp_str}Up") + data.append(self.icohp[Spin.up]) + if Spin.down in self.cohp: + header.append(f"I{cohp_str}Down") + data.append(self.icohp[Spin.down]) + format_header = "#" + " ".join("{:15s}" for __ in header) + format_data = " ".join("{:.5f}" for __ in header) + str_arr = [format_header.format(*header)] + for idx in range(len(self.energies)): + str_arr.append(format_data.format(*(d[idx] for d in data))) + return "\n".join(str_arr) + + def as_dict(self): + """JSON-serializable dict representation of COHP.""" + dct = { + "@module": type(self).__module__, + "@class": type(self).__name__, + "are_coops": self.are_coops, + "are_cobis": self.are_cobis, + "are_multi_center_cobis": self.are_multi_center_cobis, + "efermi": self.efermi, + "energies": self.energies.tolist(), + "COHP": {str(spin): pops.tolist() for spin, pops in self.cohp.items()}, + } + if self.icohp: + dct["ICOHP"] = {str(spin): pops.tolist() for spin, pops in self.icohp.items()} + return dct + + def get_cohp(self, spin=None, integrated=False): + """Get the COHP or ICOHP for a particular spin. + + Args: + spin: Spin. Can be parsed as spin object, integer (-1/1) + or str ("up"/"down") + integrated: Return COHP (False) or ICOHP (True) + + Returns: + Returns the CHOP or ICOHP for the input spin. If Spin is + None and both spins are present, both spins will be returned + as a dictionary. + """ + populations = self.cohp if not integrated else self.icohp + + if populations is None: + return None + if spin is None: + return populations + if isinstance(spin, int): + spin = Spin(spin) + elif isinstance(spin, str): + spin = Spin({"up": 1, "down": -1}[spin.lower()]) + return {spin: populations[spin]} + + def get_icohp(self, spin=None): + """Convenient alternative to get the ICOHP for a particular spin.""" + return self.get_cohp(spin=spin, integrated=True) + + def get_interpolated_value(self, energy, integrated=False): + """Get the COHP for a particular energy. + + Args: + energy: Energy to return the COHP value for. + integrated: Return COHP (False) or ICOHP (True) + """ + inter = {} + for spin in self.cohp: + if not integrated: + inter[spin] = get_linear_interpolated_value(self.energies, self.cohp[spin], energy) + elif self.icohp is not None: + inter[spin] = get_linear_interpolated_value(self.energies, self.icohp[spin], energy) + else: + raise ValueError("ICOHP is empty.") + return inter + + def has_antibnd_states_below_efermi(self, spin=None, limit=0.01): + """Get dict indicating if there are antibonding states below the Fermi level depending on the spin + spin: Spin + limit: -COHP smaller -limit will be considered. + """ + populations = self.cohp + n_energies_below_efermi = len([energy for energy in self.energies if energy <= self.efermi]) + + if populations is None: + return None + if spin is None: + dict_to_return = {} + for sp, cohp_vals in populations.items(): + if (max(cohp_vals[:n_energies_below_efermi])) > limit: + dict_to_return[sp] = True + else: + dict_to_return[sp] = False + else: + dict_to_return = {} + if isinstance(spin, int): + spin = Spin(spin) + elif isinstance(spin, str): + spin = Spin({"up": 1, "down": -1}[spin.lower()]) + if (max(populations[spin][:n_energies_below_efermi])) > limit: + dict_to_return[spin] = True + else: + dict_to_return[spin] = False + + return dict_to_return + + @classmethod + def from_dict(cls, dct: dict[str, Any]) -> Self: + """Get a COHP object from a dict representation of the COHP.""" + icohp = {Spin(int(key)): np.array(val) for key, val in dct["ICOHP"].items()} if "ICOHP" in dct else None + are_cobis = dct.get("are_cobis", False) + are_multi_center_cobis = dct.get("are_multi_center_cobis", False) + return cls( + dct["efermi"], + dct["energies"], + {Spin(int(key)): np.array(val) for key, val in dct["COHP"].items()}, + icohp=icohp, + are_coops=dct["are_coops"], + are_cobis=are_cobis, + are_multi_center_cobis=are_multi_center_cobis, + ) + + +class CompleteCohp(Cohp): + """A wrapper class that defines an average COHP, and individual COHPs. + + Attributes: + are_coops (bool): Indicates whether the object is consisting of COOPs. + are_cobis (bool): Indicates whether the object is consisting of COBIs. + efermi (float): Fermi energy. + energies (Sequence[float]): Sequence of energies. + structure (pymatgen.Structure): Structure associated with the COHPs. + cohp (Sequence[float]): The average COHP. + icohp (Sequence[float]): The average ICOHP. + all_cohps (dict[str, Sequence[float]]): A dict of COHPs for individual bonds of the form {label: COHP}. + orb_res_cohp (dict[str, Dict[str, Sequence[float]]]): Orbital-resolved COHPs. + """ + + def __init__( + self, + structure, + avg_cohp, + cohp_dict, + bonds=None, + are_coops=False, + are_cobis=False, + are_multi_center_cobis=False, + orb_res_cohp=None, + ) -> None: + """ + Args: + structure: Structure associated with this COHP. + avg_cohp: The average cohp as a COHP object. + cohp_dict: A dict of COHP objects for individual bonds of the form + {label: COHP} + bonds: A dict containing information on the bonds of the form + {label: {key: val}}. The key-val pair can be any information + the user wants to put in, but typically contains the sites, + the bond length, and the number of bonds. If nothing is + supplied, it will default to an empty dict. + are_coops: indicates whether the Cohp objects are COOPs. + Defaults to False for COHPs. + are_cobis: indicates whether the Cohp objects are COBIs. + Defaults to False for COHPs. + are_multi_center_cobis: indicates whether the Cohp objects are multi-center COBIs. + Defaults to False for COHPs. + orb_res_cohp: Orbital-resolved COHPs. + """ + if ( + (are_coops and are_cobis) + or (are_coops and are_multi_center_cobis) + or (are_cobis and are_multi_center_cobis) + ): + raise ValueError("You cannot have info about COOPs, COBIs and/or multi-center COBIS in the same file.") + super().__init__( + avg_cohp.efermi, + avg_cohp.energies, + avg_cohp.cohp, + are_coops=are_coops, + are_cobis=are_cobis, + are_multi_center_cobis=are_multi_center_cobis, + icohp=avg_cohp.icohp, + ) + self.structure = structure + self.are_coops = are_coops + self.are_cobis = are_cobis + self.are_multi_center_cobis = are_multi_center_cobis + self.all_cohps = cohp_dict + self.orb_res_cohp = orb_res_cohp + self.bonds = bonds or {label: {} for label in self.all_cohps} + + def __str__(self) -> str: + if self.are_coops: + return f"Complete COOPs for {self.structure}" + if self.are_cobis: + return f"Complete COBIs for {self.structure}" + return f"Complete COHPs for {self.structure}" + + def as_dict(self): + """JSON-serializable dict representation of CompleteCohp.""" + dct = { + "@module": type(self).__module__, + "@class": type(self).__name__, + "are_coops": self.are_coops, + "are_cobis": self.are_cobis, + "are_multi_center_cobis": self.are_multi_center_cobis, + "efermi": self.efermi, + "structure": self.structure.as_dict(), + "energies": self.energies.tolist(), + "COHP": {"average": {str(spin): pops.tolist() for spin, pops in self.cohp.items()}}, + } + + if self.icohp is not None: + dct["ICOHP"] = {"average": {str(spin): pops.tolist() for spin, pops in self.icohp.items()}} + + for label in self.all_cohps: + dct["COHP"].update({label: {str(spin): pops.tolist() for spin, pops in self.all_cohps[label].cohp.items()}}) + if self.all_cohps[label].icohp is not None: + if "ICOHP" not in dct: + dct["ICOHP"] = { + label: {str(spin): pops.tolist() for spin, pops in self.all_cohps[label].icohp.items()} + } + else: + dct["ICOHP"].update( + {label: {str(spin): pops.tolist() for spin, pops in self.all_cohps[label].icohp.items()}} + ) + if False in [bond_dict == {} for bond_dict in self.bonds.values()]: + dct["bonds"] = { + bond: { + "length": self.bonds[bond]["length"], + "sites": [site.as_dict() for site in self.bonds[bond]["sites"]], + } + for bond in self.bonds + } + if self.orb_res_cohp: + orb_dict = {} + for label in self.orb_res_cohp: + orb_dict[label] = {} + for orbs in self.orb_res_cohp[label]: + cohp = {str(spin): pops.tolist() for spin, pops in self.orb_res_cohp[label][orbs]["COHP"].items()} + orb_dict[label][orbs] = {"COHP": cohp} + icohp = {str(spin): pops.tolist() for spin, pops in self.orb_res_cohp[label][orbs]["ICOHP"].items()} + orb_dict[label][orbs]["ICOHP"] = icohp + orbitals = [[orb[0], orb[1].name] for orb in self.orb_res_cohp[label][orbs]["orbitals"]] + orb_dict[label][orbs]["orbitals"] = orbitals + dct["orb_res_cohp"] = orb_dict + + return dct + + def get_cohp_by_label(self, label, summed_spin_channels=False): + """Get specific COHP object. + + Args: + label: string (for newer Lobster versions: a number) + summed_spin_channels: bool, will sum the spin channels and return the sum in Spin.up if true + + Returns: + Returns the COHP object to simplify plotting + """ + if label.lower() == "average": + divided_cohp = self.cohp + divided_icohp = self.icohp + + else: + divided_cohp = self.all_cohps[label].get_cohp(spin=None, integrated=False) + divided_icohp = self.all_cohps[label].get_icohp(spin=None) + + if summed_spin_channels and Spin.down in self.cohp: + final_cohp = {} + final_icohp = {} + final_cohp[Spin.up] = np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0) + final_icohp[Spin.up] = np.sum([divided_icohp[Spin.up], divided_icohp[Spin.down]], axis=0) + else: + final_cohp = divided_cohp + final_icohp = divided_icohp + + return Cohp( + efermi=self.efermi, + energies=self.energies, + cohp=final_cohp, + are_coops=self.are_coops, + are_cobis=self.are_cobis, + icohp=final_icohp, + ) + + def get_summed_cohp_by_label_list(self, label_list, divisor=1, summed_spin_channels=False): + """Get a COHP object that includes a summed COHP divided by divisor. + + Args: + label_list: list of labels for the COHP that should be included in the summed cohp + divisor: float/int, the summed cohp will be divided by this divisor + summed_spin_channels: bool, will sum the spin channels and return the sum in Spin.up if true + + Returns: + Returns a COHP object including a summed COHP + """ + # check if cohps are spinpolarized or not + first_cohpobject = self.get_cohp_by_label(label_list[0]) + summed_cohp = first_cohpobject.cohp.copy() + summed_icohp = first_cohpobject.icohp.copy() + for label in label_list[1:]: + cohp_here = self.get_cohp_by_label(label) + summed_cohp[Spin.up] = np.sum([summed_cohp[Spin.up], cohp_here.cohp[Spin.up]], axis=0) + + if Spin.down in summed_cohp: + summed_cohp[Spin.down] = np.sum([summed_cohp[Spin.down], cohp_here.cohp[Spin.down]], axis=0) + + summed_icohp[Spin.up] = np.sum([summed_icohp[Spin.up], cohp_here.icohp[Spin.up]], axis=0) + + if Spin.down in summed_icohp: + summed_icohp[Spin.down] = np.sum([summed_icohp[Spin.down], cohp_here.icohp[Spin.down]], axis=0) + + divided_cohp = {} + divided_icohp = {} + divided_cohp[Spin.up] = np.divide(summed_cohp[Spin.up], divisor) + divided_icohp[Spin.up] = np.divide(summed_icohp[Spin.up], divisor) + if Spin.down in summed_cohp: + divided_cohp[Spin.down] = np.divide(summed_cohp[Spin.down], divisor) + divided_icohp[Spin.down] = np.divide(summed_icohp[Spin.down], divisor) + + if summed_spin_channels and Spin.down in summed_cohp: + final_cohp = {} + final_icohp = {} + final_cohp[Spin.up] = np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0) + final_icohp[Spin.up] = np.sum([divided_icohp[Spin.up], divided_icohp[Spin.down]], axis=0) + else: + final_cohp = divided_cohp + final_icohp = divided_icohp + + return Cohp( + efermi=first_cohpobject.efermi, + energies=first_cohpobject.energies, + cohp=final_cohp, + are_coops=first_cohpobject.are_coops, + are_cobis=first_cohpobject.are_coops, + icohp=final_icohp, + ) + + def get_summed_cohp_by_label_and_orbital_list( + self, label_list, orbital_list, divisor=1, summed_spin_channels=False + ): + """Get a COHP object that includes a summed COHP divided by divisor. + + Args: + label_list: list of labels for the COHP that should be included in the summed cohp + orbital_list: list of orbitals for the COHPs that should be included in the summed cohp (same order as + label_list) + divisor: float/int, the summed cohp will be divided by this divisor + summed_spin_channels: bool, will sum the spin channels and return the sum in Spin.up if true + + Returns: + Returns a COHP object including a summed COHP + """ + # check length of label_list and orbital_list: + if not len(label_list) == len(orbital_list): + raise ValueError("label_list and orbital_list don't have the same length!") + # check if cohps are spinpolarized or not + first_cohpobject = self.get_orbital_resolved_cohp(label_list[0], orbital_list[0]) + summed_cohp = first_cohpobject.cohp.copy() + summed_icohp = first_cohpobject.icohp.copy() + for ilabel, label in enumerate(label_list[1:], start=1): + cohp_here = self.get_orbital_resolved_cohp(label, orbital_list[ilabel]) + summed_cohp[Spin.up] = np.sum([summed_cohp[Spin.up], cohp_here.cohp.copy()[Spin.up]], axis=0) + if Spin.down in summed_cohp: + summed_cohp[Spin.down] = np.sum([summed_cohp[Spin.down], cohp_here.cohp.copy()[Spin.down]], axis=0) + summed_icohp[Spin.up] = np.sum([summed_icohp[Spin.up], cohp_here.icohp.copy()[Spin.up]], axis=0) + if Spin.down in summed_icohp: + summed_icohp[Spin.down] = np.sum([summed_icohp[Spin.down], cohp_here.icohp.copy()[Spin.down]], axis=0) + + divided_cohp = {} + divided_icohp = {} + divided_cohp[Spin.up] = np.divide(summed_cohp[Spin.up], divisor) + divided_icohp[Spin.up] = np.divide(summed_icohp[Spin.up], divisor) + if Spin.down in summed_cohp: + divided_cohp[Spin.down] = np.divide(summed_cohp[Spin.down], divisor) + divided_icohp[Spin.down] = np.divide(summed_icohp[Spin.down], divisor) + + if summed_spin_channels and Spin.down in divided_cohp: + final_cohp = {} + final_icohp = {} + + final_cohp[Spin.up] = np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0) + final_icohp[Spin.up] = np.sum([divided_icohp[Spin.up], divided_icohp[Spin.down]], axis=0) + else: + final_cohp = divided_cohp + final_icohp = divided_icohp + + return Cohp( + efermi=first_cohpobject.efermi, + energies=first_cohpobject.energies, + cohp=final_cohp, + are_coops=first_cohpobject.are_coops, + are_cobis=first_cohpobject.are_cobis, + icohp=final_icohp, + ) + + def get_orbital_resolved_cohp(self, label, orbitals, summed_spin_channels=False): + """Get orbital-resolved COHP. + + Args: + label: bond label (Lobster: labels as in ICOHPLIST/ICOOPLIST.lobster). + + orbitals: The orbitals as a label, or list or tuple of the form + [(n1, orbital1), (n2, orbital2)]. Orbitals can either be str, + int, or Orbital. + + summed_spin_channels: bool, will sum the spin channels and return the sum in Spin.up if true + + Returns: + A Cohp object if CompleteCohp contains orbital-resolved cohp, + or None if it doesn't. + + Note: It currently assumes that orbitals are str if they aren't the + other valid types. This is not ideal, but the easiest way to + avoid unicode issues between python 2 and python 3. + """ + if self.orb_res_cohp is None: + return None + if isinstance(orbitals, (list, tuple)): + cohp_orbs = [d["orbitals"] for d in self.orb_res_cohp[label].values()] + orbs = [] + for orbital in orbitals: + if isinstance(orbital[1], int): + orbs.append((orbital[0], Orbital(orbital[1]))) + elif isinstance(orbital[1], Orbital): + orbs.append((orbital[0], orbital[1])) + elif isinstance(orbital[1], str): + orbs.append((orbital[0], Orbital[orbital[1]])) + else: + raise TypeError("Orbital must be str, int, or Orbital.") + orb_index = cohp_orbs.index(orbs) + orb_label = list(self.orb_res_cohp[label])[orb_index] + elif isinstance(orbitals, str): + orb_label = orbitals + else: + raise TypeError("Orbitals must be str, list, or tuple.") + try: + icohp = self.orb_res_cohp[label][orb_label]["ICOHP"] + except KeyError: + icohp = None + + start_cohp = self.orb_res_cohp[label][orb_label]["COHP"] + start_icohp = icohp + + if summed_spin_channels and Spin.down in start_cohp: + final_cohp = {} + final_icohp = {} + final_cohp[Spin.up] = np.sum([start_cohp[Spin.up], start_cohp[Spin.down]], axis=0) + if start_icohp is not None: + final_icohp[Spin.up] = np.sum([start_icohp[Spin.up], start_icohp[Spin.down]], axis=0) + else: + final_cohp = start_cohp + final_icohp = start_icohp + + return Cohp( + self.efermi, + self.energies, + final_cohp, + icohp=final_icohp, + are_coops=self.are_coops, + are_cobis=self.are_cobis, + ) + + @classmethod + def from_dict(cls, dct: dict) -> Self: + """Get CompleteCohp object from dict representation.""" + # TODO: clean that mess up? + cohp_dict = {} + efermi = dct["efermi"] + energies = dct["energies"] + structure = Structure.from_dict(dct["structure"]) + are_cobis = dct.get("are_cobis", False) + are_multi_center_cobis = dct.get("are_multi_center_cobis", False) + are_coops = dct["are_coops"] + avg_cohp = None + + if "bonds" in dct: + bonds = { + bond: { + "length": dct["bonds"][bond]["length"], + "sites": tuple(PeriodicSite.from_dict(site) for site in dct["bonds"][bond]["sites"]), + "cells": dct["bonds"][bond].get("cells", None), + } + for bond in dct["bonds"] + } + else: + bonds = None + for label in dct["COHP"]: + cohp = {Spin(int(spin)): np.array(dct["COHP"][label][spin]) for spin in dct["COHP"][label]} + try: + icohp = {Spin(int(spin)): np.array(dct["ICOHP"][label][spin]) for spin in dct["ICOHP"][label]} + except KeyError: + icohp = None + if label == "average": + avg_cohp = Cohp( + efermi, + energies, + cohp, + icohp=icohp, + are_coops=are_coops, + are_cobis=are_cobis, + are_multi_center_cobis=are_multi_center_cobis, + ) + else: + cohp_dict[label] = Cohp(efermi, energies, cohp, icohp=icohp) + + if "orb_res_cohp" in dct: + orb_cohp: dict[str, dict] = {} + for label in dct["orb_res_cohp"]: + orb_cohp[label] = {} + for orb in dct["orb_res_cohp"][label]: + cohp = { + Spin(int(s)): np.array(dct["orb_res_cohp"][label][orb]["COHP"][s], dtype=float) + for s in dct["orb_res_cohp"][label][orb]["COHP"] + } + try: + icohp = { + Spin(int(s)): np.array(dct["orb_res_cohp"][label][orb]["ICOHP"][s], dtype=float) + for s in dct["orb_res_cohp"][label][orb]["ICOHP"] + } + except KeyError: + icohp = None + orbitals = [(int(o[0]), Orbital[o[1]]) for o in dct["orb_res_cohp"][label][orb]["orbitals"]] + orb_cohp[label][orb] = { + "COHP": cohp, + "ICOHP": icohp, + "orbitals": orbitals, + } + # If no total COHPs are present, calculate the total + # COHPs from the single-orbital populations. Total COHPs + # may not be present when the COHP generator keyword is used + # in LOBSTER versions 2.2.0 and earlier. + if label not in dct["COHP"] or dct["COHP"][label] is None: + cohp = { + Spin.up: np.sum( + np.array([orb_cohp[label][orb]["COHP"][Spin.up] for orb in orb_cohp[label]]), + axis=0, + ) + } + try: + cohp[Spin.down] = np.sum( + np.array([orb_cohp[label][orb]["COHP"][Spin.down] for orb in orb_cohp[label]]), + axis=0, + ) + except KeyError: + pass + + orb_res_icohp = None in [orb_cohp[label][orb]["ICOHP"] for orb in orb_cohp[label]] + if (label not in dct["ICOHP"] or dct["ICOHP"][label] is None) and orb_res_icohp: + icohp = { + Spin.up: np.sum( + np.array([orb_cohp[label][orb]["ICOHP"][Spin.up] for orb in orb_cohp[label]]), + axis=0, + ) + } + try: + icohp[Spin.down] = np.sum( + np.array([orb_cohp[label][orb]["ICOHP"][Spin.down] for orb in orb_cohp[label]]), + axis=0, + ) + except KeyError: + pass + else: + orb_cohp = {} + + are_cobis = dct.get("are_cobis", False) + + return cls( + structure, + avg_cohp, + cohp_dict, + bonds=bonds, + are_coops=dct["are_coops"], + are_cobis=are_cobis, + are_multi_center_cobis=are_multi_center_cobis, + orb_res_cohp=orb_cohp, + ) + + @classmethod + def from_file( + cls, fmt, filename=None, structure_file=None, are_coops=False, are_cobis=False, are_multi_center_cobis=False + ) -> Self: + """ + Creates a CompleteCohp object from an output file of a COHP + calculation. Valid formats are either LMTO (for the Stuttgart + LMTO-ASA code) or LOBSTER (for the LOBSTER code). + + Args: + fmt: A string for the code that was used to calculate + the COHPs so that the output file can be handled + correctly. Can take the values "LMTO" or "LOBSTER". + filename: Name of the COHP output file. Defaults to COPL + for LMTO and COHPCAR.lobster/COOPCAR.lobster for LOBSTER. + structure_file: Name of the file containing the structure. + If no file name is given, use CTRL for LMTO and POSCAR + for LOBSTER. + are_coops: Indicates whether the populations are COOPs or + COHPs. Defaults to False for COHPs. + are_cobis: Indicates whether the populations are COBIs or + COHPs. Defaults to False for COHPs. + are_multi_center_cobis: Indicates whether this file + includes information on multi-center COBIs + + Returns: + A CompleteCohp object. + """ + if are_coops and are_cobis: + raise ValueError("You cannot have info about COOPs and COBIs in the same file.") + fmt = fmt.upper() + if fmt == "LMTO": + # LMTO COOPs and orbital-resolved COHP cannot be handled yet. + are_coops = False + are_cobis = False + orb_res_cohp = None + if structure_file is None: + structure_file = "CTRL" + if filename is None: + filename = "COPL" + cohp_file: LMTOCopl | Cohpcar = LMTOCopl(filename=filename, to_eV=True) + elif fmt == "LOBSTER": + if ( + (are_coops and are_cobis) + or (are_coops and are_multi_center_cobis) + or (are_cobis and are_multi_center_cobis) + ): + raise ValueError("You cannot have info about COOPs, COBIs and/or multi-center COBIS in the same file.") + if structure_file is None: + structure_file = "POSCAR" + if filename is None and filename is None: + if are_coops: + filename = "COOPCAR.lobster" + elif are_cobis or are_multi_center_cobis: + filename = "COBICAR.lobster" + else: + filename = "COHPCAR.lobster" + cohp_file = Cohpcar( + filename=filename, + are_coops=are_coops, + are_cobis=are_cobis, + are_multi_center_cobis=are_multi_center_cobis, + ) + orb_res_cohp = cohp_file.orb_res_cohp + else: + raise ValueError(f"Unknown format {fmt}. Valid formats are LMTO and LOBSTER.") + + structure = Structure.from_file(structure_file) + efermi = cohp_file.efermi + cohp_data = cohp_file.cohp_data + energies = cohp_file.energies + + # Lobster shifts the energies so that the Fermi energy is at zero. + # Shifting should be done by the plotter object though. + + spins = [Spin.up, Spin.down] if cohp_file.is_spin_polarized else [Spin.up] + if fmt == "LOBSTER": + energies += efermi + + if orb_res_cohp is not None: + # If no total COHPs are present, calculate the total + # COHPs from the single-orbital populations. Total COHPs + # may not be present when the cohpgenerator keyword is used + # in LOBSTER versions 2.2.0 and earlier. + # TODO: Test this more extensively + + for label in orb_res_cohp: + if cohp_file.cohp_data[label]["COHP"] is None: + cohp_data[label]["COHP"] = { + sp: np.sum( + [orb_res_cohp[label][orbs]["COHP"][sp] for orbs in orb_res_cohp[label]], + axis=0, + ) + for sp in spins + } + if cohp_file.cohp_data[label]["ICOHP"] is None: + cohp_data[label]["ICOHP"] = { + sp: np.sum( + [orb_res_cohp[label][orbs]["ICOHP"][sp] for orbs in orb_res_cohp[label]], + axis=0, + ) + for sp in spins + } + + if fmt == "LMTO": + # Calculate the average COHP for the LMTO file to be + # consistent with LOBSTER output. + avg_data: dict[str, dict] = {"COHP": {}, "ICOHP": {}} + for i in avg_data: + for spin in spins: + rows = np.array([v[i][spin] for v in cohp_data.values()]) + avg = np.mean(rows, axis=0) + # LMTO COHPs have 5 significant figures + avg_data[i].update({spin: np.array([round_to_sigfigs(a, 5) for a in avg], dtype=float)}) + avg_cohp = Cohp(efermi, energies, avg_data["COHP"], icohp=avg_data["ICOHP"]) + elif not are_multi_center_cobis: + avg_cohp = Cohp( + efermi, + energies, + cohp_data["average"]["COHP"], + icohp=cohp_data["average"]["ICOHP"], + are_coops=are_coops, + are_cobis=are_cobis, + are_multi_center_cobis=are_multi_center_cobis, + ) + del cohp_data["average"] + else: + # only include two-center cobis in average + # do this for both spin channels + cohp = {} + cohp[Spin.up] = np.array( + [np.array(c["COHP"][Spin.up]) for c in cohp_file.cohp_data.values() if len(c["sites"]) <= 2] + ).mean(axis=0) + try: + cohp[Spin.down] = np.array( + [np.array(c["COHP"][Spin.down]) for c in cohp_file.cohp_data.values() if len(c["sites"]) <= 2] + ).mean(axis=0) + except KeyError: + pass + try: + icohp = {} + icohp[Spin.up] = np.array( + [np.array(c["ICOHP"][Spin.up]) for c in cohp_file.cohp_data.values() if len(c["sites"]) <= 2] + ).mean(axis=0) + try: + icohp[Spin.down] = np.array( + [np.array(c["ICOHP"][Spin.down]) for c in cohp_file.cohp_data.values() if len(c["sites"]) <= 2] + ).mean(axis=0) + except KeyError: + pass + except KeyError: + icohp = None + avg_cohp = Cohp( + efermi, + energies, + cohp, + icohp=icohp, + are_coops=are_coops, + are_cobis=are_cobis, + are_multi_center_cobis=are_multi_center_cobis, + ) + + cohp_dict = { + key: Cohp( + efermi, + energies, + dct["COHP"], + icohp=dct["ICOHP"], + are_coops=are_coops, + are_cobis=are_cobis, + are_multi_center_cobis=are_multi_center_cobis, + ) + for key, dct in cohp_data.items() + } + + bond_dict = { + key: { + "length": dct["length"], + "sites": [structure[site] for site in dct["sites"]], + } + for key, dct in cohp_data.items() + } + + return cls( + structure, + avg_cohp, + cohp_dict, + bonds=bond_dict, + are_coops=are_coops, + are_cobis=are_cobis, + orb_res_cohp=orb_res_cohp, + ) + + +class IcohpValue(MSONable): + """Store information on an ICOHP or ICOOP value. + + Attributes: + energies (ndarray): Energy values for the COHP/ICOHP/COOP/ICOOP. + densities (ndarray): Density of states values for the COHP/ICOHP/COOP/ICOOP. + energies_are_cartesian (bool): Whether the energies are cartesian or not. + are_coops (bool): Whether the object is a COOP/ICOOP or not. + are_cobis (bool): Whether the object is a COBIS/ICOBIS or not. + icohp (dict): A dictionary of the ICOHP/COHP values. The keys are Spin.up and Spin.down. + summed_icohp (float): The summed ICOHP/COHP values. + num_bonds (int): The number of bonds used for the average COHP (relevant for Lobster versions <3.0). + """ + + def __init__( + self, label, atom1, atom2, length, translation, num, icohp, are_coops=False, are_cobis=False, orbitals=None + ) -> None: + """ + Args: + label: label for the icohp + atom1: str of atom that is contributing to the bond + atom2: str of second atom that is contributing to the bond + length: float of bond lengths + translation: translation list, e.g. [0,0,0] + num: integer describing how often the bond exists + icohp: dict={Spin.up: icohpvalue for spin.up, Spin.down: icohpvalue for spin.down} + are_coops: if True, this are COOPs + are_cobis: if True, this are COBIs + orbitals: {[str(Orbital1)-str(Orbital2)]: {"icohp":{Spin.up: icohpvalue for spin.up, Spin.down: + icohpvalue for spin.down}, "orbitals":[Orbital1, Orbital2]}}. + """ + if are_coops and are_cobis: + raise ValueError("You cannot have info about COOPs and COBIs in the same file.") + self._are_coops = are_coops + self._are_cobis = are_cobis + self._label = label + self._atom1 = atom1 + self._atom2 = atom2 + self._length = length + self._translation = translation + self._num = num + self._icohp = icohp + self._orbitals = orbitals + if Spin.down in self._icohp: + self._is_spin_polarized = True + else: + self._is_spin_polarized = False + + def __str__(self) -> str: + """String representation of the ICOHP/ICOOP.""" + if not self._are_coops and not self._are_cobis: + if self._is_spin_polarized: + return ( + f"ICOHP {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " + f"{self._icohp[Spin.up]} eV (Spin up) and {self._icohp[Spin.down]} eV (Spin down)" + ) + return ( + f"ICOHP {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " + f"{self._icohp[Spin.up]} eV (Spin up)" + ) + if self._are_coops and not self._are_cobis: + if self._is_spin_polarized: + return ( + f"ICOOP {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " + f"{self._icohp[Spin.up]} eV (Spin up) and {self._icohp[Spin.down]} eV (Spin down)" + ) + return ( + f"ICOOP {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " + f"{self._icohp[Spin.up]} eV (Spin up)" + ) + if self._are_cobis and not self._are_coops: + if self._is_spin_polarized: + return ( + f"ICOBI {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " + f"{self._icohp[Spin.up]} eV (Spin up) and {self._icohp[Spin.down]} eV (Spin down)" + ) + return ( + f"ICOBI {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " + f"{self._icohp[Spin.up]} eV (Spin up)" + ) + + return "" + + @property + def num_bonds(self): + """Tells the number of bonds for which the ICOHP value is an average. + + Returns: + Int. + """ + return self._num + + @property + def are_coops(self) -> bool: + """Tells if ICOOPs or not. + + Returns: + Boolean. + """ + return self._are_coops + + @property + def are_cobis(self) -> bool: + """Tells if ICOBIs or not. + + Returns: + Boolean. + """ + return self._are_cobis + + @property + def is_spin_polarized(self) -> bool: + """Tells if spin polarized calculation or not. + + Returns: + Boolean. + """ + return self._is_spin_polarized + + def icohpvalue(self, spin=Spin.up): + """ + Args: + spin: Spin.up or Spin.down. + + Returns: + float: corresponding to chosen spin. + """ + if not self.is_spin_polarized and spin == Spin.down: + raise ValueError("The calculation was not performed with spin polarization") + + return self._icohp[spin] + + def icohpvalue_orbital(self, orbitals, spin=Spin.up) -> float: + """ + Args: + orbitals: List of Orbitals or "str(Orbital1)-str(Orbital2)" + spin: Spin.up or Spin.down. + + Returns: + float: corresponding to chosen spin. + """ + if not self.is_spin_polarized and spin == Spin.down: + raise ValueError("The calculation was not performed with spin polarization") + if isinstance(orbitals, list): + orbitals = f"{orbitals[0]}-{orbitals[1]}" + return self._orbitals[orbitals]["icohp"][spin] + + @property + def icohp(self): + """Dict with ICOHPs for spin up and spin down. + + Returns: + dict={Spin.up: icohpvalue for spin.up, Spin.down: icohpvalue for spin.down}. + """ + return self._icohp + + @property + def summed_icohp(self): + """Sums ICOHPs of both spin channels for spin polarized compounds. + + Returns: + float: icohp value in eV. + """ + return self._icohp[Spin.down] + self._icohp[Spin.up] if self._is_spin_polarized else self._icohp[Spin.up] + + @property + def summed_orbital_icohp(self): + """Sums orbital-resolved ICOHPs of both spin channels for spin-polarized compounds. + + Returns: + dict[str, float]: "str(Orbital1)-str(Ortibal2)" mapped to ICOHP value in eV. + """ + orbital_icohp = {} + for orb, item in self._orbitals.items(): + orbital_icohp[orb] = ( + item["icohp"][Spin.up] + item["icohp"][Spin.down] if self._is_spin_polarized else item["icohp"][Spin.up] + ) + return orbital_icohp + + +class IcohpCollection(MSONable): + """Store IcohpValues. + + Attributes: + are_coops (bool): Boolean to indicate if these are ICOOPs. + are_cobis (bool): Boolean to indicate if these are ICOOPs. + is_spin_polarized (bool): Boolean to indicate if the Lobster calculation was done spin polarized or not. + """ + + def __init__( + self, + list_labels, + list_atom1, + list_atom2, + list_length, + list_translation, + list_num, + list_icohp, + is_spin_polarized, + list_orb_icohp=None, + are_coops=False, + are_cobis=False, + ) -> None: + """ + Args: + list_labels: list of labels for ICOHP/ICOOP values + list_atom1: list of str of atomnames e.g. "O1" + list_atom2: list of str of atomnames e.g. "O1" + list_length: list of lengths of corresponding bonds in Angstrom + list_translation: list of translation list, e.g. [0,0,0] + list_num: list of equivalent bonds, usually 1 starting from Lobster 3.0.0 + list_icohp: list of dict={Spin.up: icohpvalue for spin.up, Spin.down: icohpvalue for spin.down} + is_spin_polarized: Boolean to indicate if the Lobster calculation was done spin polarized or not Boolean to + indicate if the Lobster calculation was done spin polarized or not + list_orb_icohp: list of dict={[str(Orbital1)-str(Orbital2)]: {"icohp":{Spin.up: icohpvalue for spin.up, + Spin.down: icohpvalue for spin.down}, "orbitals":[Orbital1, Orbital2]}} + are_coops: Boolean to indicate whether ICOOPs are stored + are_cobis: Boolean to indicate whether ICOBIs are stored. + """ + if are_coops and are_cobis: + raise ValueError("You cannot have info about COOPs and COBIs in the same file.") + self._are_coops = are_coops + self._are_cobis = are_cobis + self._icohplist = {} + self._is_spin_polarized = is_spin_polarized + self._list_labels = list_labels + self._list_atom1 = list_atom1 + self._list_atom2 = list_atom2 + self._list_length = list_length + self._list_translation = list_translation + self._list_num = list_num + self._list_icohp = list_icohp + self._list_orb_icohp = list_orb_icohp + + for ilist, listel in enumerate(list_labels): + self._icohplist[listel] = IcohpValue( + label=listel, + atom1=list_atom1[ilist], + atom2=list_atom2[ilist], + length=list_length[ilist], + translation=list_translation[ilist], + num=list_num[ilist], + icohp=list_icohp[ilist], + are_coops=are_coops, + are_cobis=are_cobis, + orbitals=None if list_orb_icohp is None else list_orb_icohp[ilist], + ) + + def __str__(self) -> str: + lst = [] + for value in self._icohplist.values(): + lst.append(str(value)) + return "\n".join(lst) + + def get_icohp_by_label(self, label, summed_spin_channels=True, spin=Spin.up, orbitals=None) -> float: + """Get an icohp value for a certain bond as indicated by the label (bond labels starting by "1" as in + ICOHPLIST/ICOOPLIST). + + Args: + label: label in str format (usually the bond number in Icohplist.lobster/Icooplist.lobster + summed_spin_channels: Boolean to indicate whether the ICOHPs/ICOOPs of both spin channels should be summed + spin: if summed_spin_channels is equal to False, this spin indicates which spin channel should be returned + orbitals: List of Orbital or "str(Orbital1)-str(Orbital2)" + + Returns: + float: ICOHP/ICOOP value + """ + icohp_here: IcohpValue = self._icohplist[label] + if orbitals is None: + if summed_spin_channels: + return icohp_here.summed_icohp + return icohp_here.icohpvalue(spin) + + if isinstance(orbitals, list): + orbitals = f"{orbitals[0]}-{orbitals[1]}" + if summed_spin_channels: + return icohp_here.summed_orbital_icohp[orbitals] + + return icohp_here.icohpvalue_orbital(spin=spin, orbitals=orbitals) + + def get_summed_icohp_by_label_list(self, label_list, divisor=1.0, summed_spin_channels=True, spin=Spin.up) -> float: + """Get the sum of several ICOHP values that are indicated by a list of labels + (labels of the bonds are the same as in ICOHPLIST/ICOOPLIST). + + Args: + label_list: list of labels of the ICOHPs/ICOOPs that should be summed + divisor: is used to divide the sum + summed_spin_channels: Boolean to indicate whether the ICOHPs/ICOOPs of both spin channels should be summed + spin: if summed_spin_channels is equal to False, this spin indicates which spin channel should be returned + + Returns: + float: sum of all ICOHPs/ICOOPs as indicated with label_list + """ + sum_icohp = 0 + for label in label_list: + icohp_here = self._icohplist[label] + if icohp_here.num_bonds != 1: + warnings.warn("One of the ICOHP values is an average over bonds. This is currently not considered.") + if icohp_here._is_spin_polarized: + if summed_spin_channels: + sum_icohp = sum_icohp + icohp_here.summed_icohp + else: + sum_icohp = sum_icohp + icohp_here.icohpvalue(spin) + else: + sum_icohp = sum_icohp + icohp_here.icohpvalue(spin) + return sum_icohp / divisor + + def get_icohp_dict_by_bondlengths(self, minbondlength=0.0, maxbondlength=8.0): + """Get a dict of IcohpValues corresponding to certain bond lengths. + + Args: + minbondlength: defines the minimum of the bond lengths of the bonds + maxbondlength: defines the maximum of the bond lengths of the bonds. + + Returns: + dict of IcohpValues, the keys correspond to the values from the initial list_labels. + """ + new_icohp_dict = {} + for value in self._icohplist.values(): + if value._length >= minbondlength and value._length <= maxbondlength: + new_icohp_dict[value._label] = value + return new_icohp_dict + + def get_icohp_dict_of_site( + self, + site, + minsummedicohp=None, + maxsummedicohp=None, + minbondlength=0.0, + maxbondlength=8.0, + only_bonds_to=None, + ): + """Get a dict of IcohpValue for a certain site (indicated by integer). + + Args: + site: integer describing the site of interest, order as in Icohplist.lobster/Icooplist.lobster, starts at 0 + minsummedicohp: float, minimal icohp/icoop of the bonds that are considered. It is the summed ICOHP value + from both spin channels for spin polarized cases + maxsummedicohp: float, maximal icohp/icoop of the bonds that are considered. It is the summed ICOHP value + from both spin channels for spin polarized cases + minbondlength: float, defines the minimum of the bond lengths of the bonds + maxbondlength: float, defines the maximum of the bond lengths of the bonds + only_bonds_to: list of strings describing the bonding partners that are allowed, e.g. ['O'] + + Returns: + dict of IcohpValues, the keys correspond to the values from the initial list_labels + """ + new_icohp_dict = {} + for key, value in self._icohplist.items(): + atomnumber1 = int(re.split(r"(\d+)", value._atom1)[1]) - 1 + atomnumber2 = int(re.split(r"(\d+)", value._atom2)[1]) - 1 + if site in (atomnumber1, atomnumber2): + # manipulate order of atoms so that searched one is always atom1 + if site == atomnumber2: + save = value._atom1 + value._atom1 = value._atom2 + value._atom2 = save + + second_test = True if only_bonds_to is None else re.split("(\\d+)", value._atom2)[0] in only_bonds_to + if value._length >= minbondlength and value._length <= maxbondlength and second_test: + if minsummedicohp is not None: + if value.summed_icohp >= minsummedicohp: + if maxsummedicohp is not None: + if value.summed_icohp <= maxsummedicohp: + new_icohp_dict[key] = value + else: + new_icohp_dict[key] = value + elif maxsummedicohp is not None: + if value.summed_icohp <= maxsummedicohp: + new_icohp_dict[key] = value + else: + new_icohp_dict[key] = value + + return new_icohp_dict + + def extremum_icohpvalue(self, summed_spin_channels=True, spin=Spin.up): + """Get ICOHP/ICOOP of strongest bond. + + Args: + summed_spin_channels: Boolean to indicate whether the ICOHPs/ICOOPs of both spin channels should be summed. + + spin: if summed_spin_channels is equal to False, this spin indicates which spin channel should be returned + + Returns: + lowest ICOHP/largest ICOOP value (i.e. ICOHP/ICOOP value of strongest bond) + """ + extremum = -sys.float_info.max if self._are_coops or self._are_cobis else sys.float_info.max + + if not self._is_spin_polarized: + if spin == Spin.down: + warnings.warn("This spin channel does not exist. I am switching to Spin.up") + spin = Spin.up + + for value in self._icohplist.values(): + if not value.is_spin_polarized or not summed_spin_channels: + if not self._are_coops and not self._are_cobis: + if value.icohpvalue(spin) < extremum: + extremum = value.icohpvalue(spin) + elif value.icohpvalue(spin) > extremum: + extremum = value.icohpvalue(spin) + elif not self._are_coops and not self._are_cobis: + if value.summed_icohp < extremum: + extremum = value.summed_icohp + elif value.summed_icohp > extremum: + extremum = value.summed_icohp + return extremum + + @property + def is_spin_polarized(self) -> bool: + """Whether it is spin polarized.""" + return self._is_spin_polarized + + @property + def are_coops(self) -> bool: + """Whether this is a coop.""" + return self._are_coops + + @property + def are_cobis(self) -> bool: + """Whether this a cobi.""" + return self._are_cobis + + +def get_integrated_cohp_in_energy_range( + cohp, label, orbital=None, energy_range=None, relative_E_Fermi=True, summed_spin_channels=True +): + """Integrate CompleteCohp objects which include data on integrated COHPs + Args: + cohp: CompleteCohp object + label: label of the COHP data + orbital: If not None, a orbital resolved integrated COHP will be returned + energy_range: If None, returns icohp value at Fermi level. + If float, integrates from this float up to the Fermi level. + If [float,float], will integrate in between. + relative_E_Fermi: if True, energy scale with E_Fermi at 0 eV is chosen + summed_spin_channels: if True, Spin channels will be summed. + + Returns: + float indicating the integrated COHP if summed_spin_channels==True, otherwise dict of the following form { + Spin.up:float, Spin.down:float} + """ + summedicohp = {} + if orbital is None: + icohps = cohp.all_cohps[label].get_icohp(spin=None) + if summed_spin_channels and Spin.down in icohps: + summedicohp[Spin.up] = icohps[Spin.up] + icohps[Spin.down] + else: + summedicohp = icohps + else: + icohps = cohp.get_orbital_resolved_cohp(label=label, orbitals=orbital).icohp + if summed_spin_channels and Spin.down in icohps: + summedicohp[Spin.up] = icohps[Spin.up] + icohps[Spin.down] + else: + summedicohp = icohps + + if energy_range is None: + energies_corrected = cohp.energies - cohp.efermi + spl_spinup = InterpolatedUnivariateSpline(energies_corrected, summedicohp[Spin.up], ext=0) + + if not summed_spin_channels and Spin.down in icohps: + spl_spindown = InterpolatedUnivariateSpline(energies_corrected, summedicohp[Spin.down], ext=0) + return {Spin.up: spl_spinup(0.0), Spin.down: spl_spindown(0.0)} + if summed_spin_channels: + return spl_spinup(0.0) + + return {Spin.up: spl_spinup(0.0)} + + # returns icohp value at the Fermi level! + if isinstance(energy_range, float): + if relative_E_Fermi: + energies_corrected = cohp.energies - cohp.efermi + spl_spinup = InterpolatedUnivariateSpline(energies_corrected, summedicohp[Spin.up], ext=0) + + if not summed_spin_channels and Spin.down in icohps: + spl_spindown = InterpolatedUnivariateSpline(energies_corrected, summedicohp[Spin.down], ext=0) + return { + Spin.up: spl_spinup(0) - spl_spinup(energy_range), + Spin.down: spl_spindown(0) - spl_spindown(energy_range), + } + if summed_spin_channels: + return spl_spinup(0) - spl_spinup(energy_range) + return {Spin.up: spl_spinup(0) - spl_spinup(energy_range)} + + energies_corrected = cohp.energies + spl_spinup = InterpolatedUnivariateSpline(energies_corrected, summedicohp[Spin.up], ext=0) + + if not summed_spin_channels and Spin.down in icohps: + spl_spindown = InterpolatedUnivariateSpline(energies_corrected, summedicohp[Spin.down], ext=0) + return { + Spin.up: spl_spinup(cohp.efermi) - spl_spinup(energy_range), + Spin.down: spl_spindown(cohp.efermi) - spl_spindown(energy_range), + } + if summed_spin_channels: + return spl_spinup(cohp.efermi) - spl_spinup(energy_range) + return {Spin.up: spl_spinup(cohp.efermi) - spl_spinup(energy_range)} + + energies_corrected = cohp.energies - cohp.efermi if relative_E_Fermi else cohp.energies + + spl_spinup = InterpolatedUnivariateSpline(energies_corrected, summedicohp[Spin.up], ext=0) + + if not summed_spin_channels and Spin.down in icohps: + spl_spindown = InterpolatedUnivariateSpline(energies_corrected, summedicohp[Spin.down], ext=0) + return { + Spin.up: spl_spinup(energy_range[1]) - spl_spinup(energy_range[0]), + Spin.down: spl_spindown(energy_range[1]) - spl_spindown(energy_range[0]), + } + if summed_spin_channels: + return spl_spinup(energy_range[1]) - spl_spinup(energy_range[0]) + + return {Spin.up: spl_spinup(energy_range[1]) - spl_spinup(energy_range[0])} From 3354aa2cdb8a89886589c04c91892b4d0eeead5b Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Wed, 26 Jun 2024 14:10:41 -0700 Subject: [PATCH 047/180] Linting fixes. --- tests/io/lobster/test_inputs.py | 3 +-- tests/io/lobster/test_lobsterenv.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 646a2f70eb..62f413a194 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -7,8 +7,6 @@ import numpy as np import pytest from numpy.testing import assert_allclose, assert_array_equal -from pytest import approx - from pymatgen.core.structure import Structure from pymatgen.electronic_structure.cohp import IcohpCollection from pymatgen.electronic_structure.core import Orbital, Spin @@ -32,6 +30,7 @@ from pymatgen.io.vasp import Vasprun from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, PymatgenTest +from pytest import approx TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp" diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 713c4b17ef..a97f2d5ecd 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -5,8 +5,6 @@ import numpy as np import pytest -from pytest import approx - from pymatgen.analysis.graphs import StructureGraph from pymatgen.core import Element from pymatgen.core.structure import Structure @@ -15,6 +13,7 @@ from pymatgen.io.lobster import Charge, Icohplist from pymatgen.io.lobster.lobsterenv import LobsterNeighbors from pymatgen.util.testing import TEST_FILES_DIR +from pytest import approx __author__ = "Janine George" __copyright__ = "Copyright 2021, The Materials Project" From e92108a616d363004b7381e63acfe66bad2f4c09 Mon Sep 17 00:00:00 2001 From: Katharina Ueltzen <94910364+kaueltzen@users.noreply.github.com> Date: Sun, 14 Jul 2024 21:30:28 +0200 Subject: [PATCH 048/180] SpaceGroup changes (#3859) * Replaced SpaceGroup symbol attribute with its Hermann-Mauguin symbol, corrected Schoenflies point group attribute, changed handling of rhombohedral space group type settings by adding a SpaceGroup hexagonal bool attribute, modified and added tests. * Removed crystal class key from symm_ops.json. * Test for correct setting of hexagonal attribute when instantiating from int number. * Noted change and replacement option for SpaceGroup symbol in compatibility.md * Added from_space_group class method to PointGroup, added tests * Added mapping to standard setting in PointGroup.from_space_group(), modified symm_ops.json and symm_data.json (documented in dev_scripts/update_space_group_data.py) to have same notation, added point group and short Hermann Mauguin symbol to symm_ops, fixed some typos, fixed rhombohedral space group type orbit issue. * Updated core/test_surface.py to assign lattice as in SpaceGroup is_compatible(). * Modified databases and SpaceGroup init to ensure compatibility with non-underscore space group notations. * Added tests for issue #3862, modified full_symbol and point_group attribute setting. * Modified PointGroup.from_space_group() to also handle symbols with identity blickrichtungen and missed underscores, added warning to SpaceGroup init if full symbol is not available (for non-standard settings), added tests. * Added test for warning if SpaceGroup.full_symbol is not available. * Removed warning test. * tweak incompat warning * add test_full_symbol_warning * add author + date to dev_scripts/update_spacegroup_data.py * typos * warning occurs only once, move test_full_symbol_warning up as workaround to annoying test pollution from side effects to std lib warnings registry * Updated compatibility.md to also handle old symbol replacement of P2_12_12_1 and I2_12_12_1. * pre-commit auto-fixes * Updated dev script path to new src layout. --------- Signed-off-by: Katharina Ueltzen <94910364+kaueltzen@users.noreply.github.com> Co-authored-by: Janosh Riebesell Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: J. George --- tests/io/lobster/test_lobsterenv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index a97f2d5ecd..9a4230e232 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -687,7 +687,7 @@ def test_get_structure_environments(self): lse2 = self.chem_env_lobster1.get_light_structure_environment() assert lse2.coordination_environments[0][0]["ce_symbol"] == "O:6" - def test_get_strucuture_environments_further_tests(self): + def test_get_structure_environments_further_tests(self): lse = self.chem_env_lobster1_second.get_light_structure_environment() lse.as_dict() lse.get_statistics() From 2607a18965563ef3e5d359ab1dcca365fbc6897f Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Fri, 2 Aug 2024 17:57:38 -0400 Subject: [PATCH 049/180] Fix import sorting (#3968) * set ruff.isort.known-first-party = ["pymatgen"] * pre-commit run --all-files --- inputs.py | 4 +++- lobsterenv.py | 4 +++- outputs.py | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/inputs.py b/inputs.py index f3759a38b9..9eef7c654a 100644 --- a/inputs.py +++ b/inputs.py @@ -22,6 +22,7 @@ from monty.io import zopen from monty.json import MSONable from monty.serialization import loadfn + from pymatgen.core.structure import Structure from pymatgen.io.vasp import Vasprun from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar @@ -31,9 +32,10 @@ if TYPE_CHECKING: from typing import Any, ClassVar, Literal + from typing_extensions import Self + from pymatgen.core.composition import Composition from pymatgen.util.typing import PathLike, Tuple3Ints - from typing_extensions import Self MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/lobsterenv.py b/lobsterenv.py index 7982b025c9..919ea6bb67 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -20,6 +20,7 @@ import numpy as np from monty.dev import deprecated + from pymatgen.analysis.bond_valence import BVAnalyzer from pymatgen.analysis.chemenv.coordination_environments.coordination_geometry_finder import LocalGeometryFinder from pymatgen.analysis.chemenv.coordination_environments.structure_environments import LightStructureEnvironments @@ -31,9 +32,10 @@ from pymatgen.util.due import Doi, due if TYPE_CHECKING: + from typing_extensions import Self + from pymatgen.core import Structure from pymatgen.core.periodic_table import Element - from typing_extensions import Self __author__ = "Janine George" __copyright__ = "Copyright 2021, The Materials Project" diff --git a/outputs.py b/outputs.py index d17c550ba9..aacc38332b 100644 --- a/outputs.py +++ b/outputs.py @@ -22,6 +22,7 @@ import numpy as np from monty.io import zopen from monty.json import MSONable + from pymatgen.core.structure import Structure from pymatgen.electronic_structure.bandstructure import LobsterBandStructureSymmLine from pymatgen.electronic_structure.core import Orbital, Spin From e75b16c3098db4747759d78ecfe933f7d4acf22e Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Fri, 2 Aug 2024 17:57:38 -0400 Subject: [PATCH 050/180] Fix import sorting (#3968) * set ruff.isort.known-first-party = ["pymatgen"] * pre-commit run --all-files --- cohp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cohp.py b/cohp.py index 17bcdce0be..b641d2a399 100644 --- a/cohp.py +++ b/cohp.py @@ -17,6 +17,8 @@ import numpy as np from monty.json import MSONable +from scipy.interpolate import InterpolatedUnivariateSpline + from pymatgen.core.sites import PeriodicSite from pymatgen.core.structure import Structure from pymatgen.electronic_structure.core import Orbital, Spin @@ -25,7 +27,6 @@ from pymatgen.util.coord import get_linear_interpolated_value from pymatgen.util.due import Doi, due from pymatgen.util.num import round_to_sigfigs -from scipy.interpolate import InterpolatedUnivariateSpline if TYPE_CHECKING: from typing import Any From d35a432871fa2f6887ec14695f8b80cd5e56db1d Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Fri, 2 Aug 2024 17:57:38 -0400 Subject: [PATCH 051/180] Fix import sorting (#3968) * set ruff.isort.known-first-party = ["pymatgen"] * pre-commit run --all-files --- tests/io/lobster/test_inputs.py | 3 ++- tests/io/lobster/test_lobsterenv.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 62f413a194..646a2f70eb 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -7,6 +7,8 @@ import numpy as np import pytest from numpy.testing import assert_allclose, assert_array_equal +from pytest import approx + from pymatgen.core.structure import Structure from pymatgen.electronic_structure.cohp import IcohpCollection from pymatgen.electronic_structure.core import Orbital, Spin @@ -30,7 +32,6 @@ from pymatgen.io.vasp import Vasprun from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, PymatgenTest -from pytest import approx TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp" diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 9a4230e232..6ee94d8290 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -5,6 +5,8 @@ import numpy as np import pytest +from pytest import approx + from pymatgen.analysis.graphs import StructureGraph from pymatgen.core import Element from pymatgen.core.structure import Structure @@ -13,7 +15,6 @@ from pymatgen.io.lobster import Charge, Icohplist from pymatgen.io.lobster.lobsterenv import LobsterNeighbors from pymatgen.util.testing import TEST_FILES_DIR -from pytest import approx __author__ = "Janine George" __copyright__ = "Copyright 2021, The Materials Project" From fc8c76cca262c9cbc2400c396e0bd779fc3d50b8 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sat, 3 Aug 2024 21:46:45 +0800 Subject: [PATCH 052/180] Improve types for `electronic_structure.{bandstructure/cohp}` (#3873) * add types for bandstructure * relocate magic methods to top * add some types * fix type errors in bandstructure * temp save * first run of cohp, mypy errors to fix * fix collection generation * add type `SpinLike` and case tweaks * reduce repetition for `__str__` of `IcohpValue` * simplify condition * reduce indentation level * clarify `translation` * clarify `list_num` and other docstrings * clarify `label` as str * more type and docstring improvements * fix unit test * fix most mypy errors * fix remaining mypy errors * add DEBUG tag * reduce code repetition * Need Confirm: set `translation` as tuple * pre-commit auto-fixes * more type clarify * clarify `num` argument * clarify docstring of `bandstructure` * more minor tweaks * clarify type of labels_dict * replace unnecessary single-item list extend with append * fix typo * relocate magic method * clarify type of `list_icohp` * remove unused type alias * revert undesired rename * replace more single item extend with append * simplify dict generation * fix downstream lobsterpy error * tweak module docstring * need confirm: allow efermi to be None * pre-commit auto-fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- __init__.py | 2 +- outputs.py | 261 ++++++++++++++++++++++++++-------------------------- 2 files changed, 131 insertions(+), 132 deletions(-) diff --git a/__init__.py b/__init__.py index f5e4a5f423..e4b1100a2d 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,5 @@ """ -This package implements modules for input and output to and from Lobster. It +This package implements modules for input and output to and from LOBSTER. It imports the key classes form both lobster.inputs and lobster_outputs to allow most classes to be simply called as pymatgen.io.lobster.Lobsterin for example, to retain backwards compatibility. diff --git a/outputs.py b/outputs.py index aacc38332b..31ebe16b04 100644 --- a/outputs.py +++ b/outputs.py @@ -279,6 +279,7 @@ def _get_bond_data(line: str, are_multi_center_cobis: bool = False) -> dict: indices, a tuple containing the orbitals (if orbital-resolved), and a label for the orbitals (if orbital-resolved). """ + if not are_multi_center_cobis: line_new = line.rsplit("(", 1) length = float(line_new[-1][:-1]) @@ -357,7 +358,7 @@ def __init__( filename: Name of the ICOHPLIST file. If it is None, the default file name will be chosen, depending on the value of are_coops is_spin_polarized: Boolean to indicate if the calculation is spin polarized - icohpcollection: IcohpCollection Object. + icohpcollection: IcohpCollection Object """ self._filename = filename @@ -384,14 +385,14 @@ def __init__( if len(data) == 0: raise RuntimeError("ICOHPLIST file contains no data.") - # Which Lobster version? + # Determine LOBSTER version if len(data[0].split()) == 8: version = "3.1.1" elif len(data[0].split()) == 6: version = "2.2.1" - warnings.warn("Please consider using the new Lobster version. See www.cohp.de.") + warnings.warn("Please consider using the new LOBSTER version. See www.cohp.de.") else: - raise ValueError + raise ValueError("Unsupported LOBSTER version.") # If the calculation is spin polarized, the line in the middle # of the file will be another header line. @@ -408,9 +409,9 @@ def __init__( data_orbitals = [] for line in data: if "_" not in line.split()[1]: - data_without_orbitals += [line] + data_without_orbitals.append(line) else: - data_orbitals += [line] + data_orbitals.append(line) else: data_without_orbitals = data @@ -423,49 +424,45 @@ def __init__( else: n_bonds = len(data_without_orbitals) - labels, atoms1, atoms2, lens, translations, nums, icohps = [], [], [], [], [], [], [] - - # initialize static variables - label = "" - atom1 = "" - atom2 = "" - length = None - num = None - translation = [] + labels: list[str] = [] + atoms1: list[str] = [] + atoms2: list[str] = [] + lens: list[float] = [] + translations: list[tuple[int, int, int]] = [] + nums: list[int] = [] + icohps: list[dict[Spin, float]] = [] for bond in range(n_bonds): - line = data_without_orbitals[bond].split() - icohp = {} + line_parts = data_without_orbitals[bond].split() + + label = f"{line_parts[0]}" + atom1 = str(line_parts[1]) + atom2 = str(line_parts[2]) + length = float(line_parts[3]) + + icohp: dict[Spin, float] = {} if version == "2.2.1": - label = f"{line[0]}" - atom1 = str(line[1]) - atom2 = str(line[2]) - length = float(line[3]) - icohp[Spin.up] = float(line[4]) - num = int(line[5]) - translation = [0, 0, 0] + icohp[Spin.up] = float(line_parts[4]) + num = int(line_parts[5]) + translation = (0, 0, 0) if self.is_spin_polarized: icohp[Spin.down] = float(data_without_orbitals[bond + n_bonds + 1].split()[4]) - elif version == "3.1.1": - label = f"{line[0]}" - atom1 = str(line[1]) - atom2 = str(line[2]) - length = float(line[3]) - translation = [int(line[4]), int(line[5]), int(line[6])] - icohp[Spin.up] = float(line[7]) + else: # version == "3.1.1" + translation = (int(line_parts[4]), int(line_parts[5]), int(line_parts[6])) + icohp[Spin.up] = float(line_parts[7]) num = 1 if self.is_spin_polarized: icohp[Spin.down] = float(data_without_orbitals[bond + n_bonds + 1].split()[7]) - labels += [label] - atoms1 += [atom1] - atoms2 += [atom2] - lens += [length] - translations += [translation] - nums += [num] - icohps += [icohp] + labels.append(label) + atoms1.append(atom1) + atoms2.append(atom2) + lens.append(length) + translations.append(translation) + nums.append(num) + icohps.append(icohp) list_orb_icohp: list[dict] | None = None if self.orbitalwise: @@ -475,17 +472,17 @@ def __init__( for i_data_orb in range(n_orbs): data_orb = data_orbitals[i_data_orb] icohp = {} - line = data_orb.split() - label = f"{line[0]}" + line_parts = data_orb.split() + label = f"{line_parts[0]}" orbs = re.findall(r"_(.*?)(?=\s)", data_orb) orb_label, orbitals = get_orb_from_str(orbs) - icohp[Spin.up] = float(line[7]) + icohp[Spin.up] = float(line_parts[7]) if self.is_spin_polarized: icohp[Spin.down] = float(data_orbitals[n_orbs + i_data_orb].split()[7]) if len(list_orb_icohp) < int(label): - list_orb_icohp += [{orb_label: {"icohp": icohp, "orbitals": orbitals}}] + list_orb_icohp.append({orb_label: {"icohp": icohp, "orbitals": orbitals}}) else: list_orb_icohp[int(label) - 1][orb_label] = {"icohp": icohp, "orbitals": orbitals} @@ -499,7 +496,7 @@ def __init__( list_atom1=atoms1, list_atom2=atoms2, list_length=lens, - list_translation=translations, + list_translation=translations, # type: ignore[arg-type] list_num=nums, list_icohp=icohps, is_spin_polarized=self.is_spin_polarized, @@ -542,11 +539,12 @@ def __init__( filename: PathLike | None = "NcICOBILIST.lobster", ) -> None: """ - LOBSTER < 4.1.0: no COBI/ICOBI/NcICOBI. + LOBSTER < 4.1.0: no COBI/ICOBI/NcICOBI Args: filename: Name of the NcICOBILIST file. """ + # LOBSTER list files have an extra trailing blank line # and we don't need the header. with zopen(filename, mode="rt") as file: @@ -574,7 +572,7 @@ def __init__( data_without_orbitals = [] for line in data: if "_" not in str(line.split()[3:]) and "s]" not in str(line.split()[3:]): - data_without_orbitals += [line] + data_without_orbitals.append(line) else: data_without_orbitals = data @@ -605,17 +603,19 @@ def __init__( if self.is_spin_polarized: ncicobi[Spin.down] = float(data_without_orbitals[bond + n_bonds + 1].split()[2]) - self.list_labels += [label] - self.list_n_atoms += [n_atoms] - self.list_ncicobi += [ncicobi] - self.list_interaction_type += [interaction_type] - self.list_num += [num] + self.list_labels.append(label) + self.list_n_atoms.append(n_atoms) + self.list_ncicobi.append(ncicobi) + self.list_interaction_type.append(interaction_type) + self.list_num.append(num) # TODO: add functions to get orbital resolved NcICOBIs @property def ncicobi_list(self) -> dict[Any, dict[str, Any]]: - """Returns: ncicobilist.""" + """ + Returns: ncicobilist. + """ ncicobi_list = {} for idx in range(len(self.list_labels)): ncicobi_list[str(idx + 1)] = { @@ -688,7 +688,7 @@ def _parse_doscar(self): for nd in range(1, ndos): line = file.readline().split() cdos[nd] = np.array(line) - dos += [cdos] + dos.append(cdos) doshere = np.array(dos[0]) if len(doshere[0, :]) == 5: self._is_spin_polarized = True @@ -710,7 +710,7 @@ def _parse_doscar(self): for orb_num, j in enumerate(range(1, ncol)): orb = orbitals[atom + 1][orb_num] pdos[orb][spin] = data[:, j] - pdoss += [pdos] + pdoss.append(pdos) else: tdensities[Spin.up] = doshere[:, 1] tdensities[Spin.down] = doshere[:, 2] @@ -728,7 +728,7 @@ def _parse_doscar(self): pdos[orb][spin] = data[:, j] if j % 2 == 0: orb_num += 1 - pdoss += [pdos] + pdoss.append(pdos) self._efermi = efermi self._pdos = pdoss @@ -744,32 +744,32 @@ def _parse_doscar(self): @property def completedos(self) -> LobsterCompleteDos: - """LobsterCompleteDos.""" + """LobsterCompleteDos""" return self._completedos @property def pdos(self) -> list: - """Projected DOS.""" + """Projected DOS""" return self._pdos @property def tdos(self) -> Dos: - """Total DOS.""" + """Total DOS""" return self._tdos @property def energies(self) -> np.ndarray: - """Energies.""" + """Energies""" return self._energies @property def tdensities(self) -> dict[Spin, np.ndarray]: - """Total densities as a np.ndarray.""" + """total densities as a np.ndarray""" return self._tdensities @property def itdensities(self) -> dict[Spin, np.ndarray]: - """Integrated total densities as a np.ndarray.""" + """integrated total densities as a np.ndarray""" return self._itdensities @property @@ -805,7 +805,7 @@ def __init__( atomlist: list of atoms in the structure types: list of unique species in the structure mulliken: list of Mulliken charges - loewdin: list of Loewdin charges. + loewdin: list of Loewdin charges """ self._filename = filename self.num_atoms = num_atoms @@ -821,15 +821,15 @@ def __init__( raise RuntimeError("CHARGE file contains no data.") self.num_atoms = len(data) - for atom in range(self.num_atoms): - line = data[atom].split() - self.atomlist += [line[1] + line[0]] - self.types += [line[1]] - self.mulliken += [float(line[2])] - self.loewdin += [float(line[3])] + for atom_idx in range(self.num_atoms): + line_parts = data[atom_idx].split() + self.atomlist.append(line_parts[1] + line_parts[0]) + self.types.append(line_parts[1]) + self.mulliken.append(float(line_parts[2])) + self.loewdin.append(float(line_parts[3])) def get_structure_with_charges(self, structure_filename: PathLike) -> Structure: - """Get a Structure with Mulliken and Loewdin charges as site properties. + """Get a Structure with Mulliken and Loewdin charges as site properties Args: structure_filename: filename of POSCAR @@ -926,7 +926,7 @@ def __init__(self, filename: PathLike | None, **kwargs) -> None: """ Args: filename: The lobsterout file. - **kwargs: dict to initialize Lobsterout instance. + **kwargs: dict to initialize Lobsterout instance """ self.filename = filename if kwargs: @@ -1008,7 +1008,7 @@ def __init__(self, filename: PathLike | None, **kwargs) -> None: raise ValueError("must provide either filename or kwargs to initialize Lobsterout") def get_doc(self) -> dict[str, Any]: - """Get the LobsterDict with all the information stored in lobsterout.""" + """Get a dict with all the information in lobsterout.""" return { # Check if LOBSTER starts from a projection "restart_from_projection": self.is_restart_from_projection, @@ -1039,7 +1039,7 @@ def get_doc(self) -> dict[str, Any]: } def as_dict(self) -> dict: - """MSONable dict.""" + """MSONable dict""" dct = dict(vars(self)) dct["@module"] = type(self).__module__ dct["@class"] = type(self).__name__ @@ -1090,9 +1090,9 @@ def _get_spillings(data, number_of_spins): splitrow = row.split() if len(splitrow) > 2 and splitrow[2] == "spilling:": if splitrow[1] == "charge": - charge_spilling += [np.float64(splitrow[3].replace("%", "")) / 100.0] + charge_spilling.append(np.float64(splitrow[3].replace("%", "")) / 100.0) if splitrow[1] == "total": - total_spilling += [np.float64(splitrow[3].replace("%", "")) / 100.0] + total_spilling.append(np.float64(splitrow[3].replace("%", "")) / 100.0) if len(charge_spilling) == number_of_spins and len(total_spilling) == number_of_spins: break @@ -1108,8 +1108,8 @@ def _get_elements_basistype_basisfunctions(data): basisfunctions = [] for row in data: if begin and not end: - splitrow = row.split() - if splitrow[0] not in [ + row_parts = row.split() + if row_parts[0] not in { "INFO:", "WARNING:", "setting", @@ -1118,11 +1118,11 @@ def _get_elements_basistype_basisfunctions(data): "saving", "spillings", "writing", - ]: - elements += [splitrow[0]] - basistype += [splitrow[1].replace("(", "").replace(")", "")] + }: + elements.append(row_parts[0]) + basistype.append(row_parts[1].replace("(", "").replace(")", "")) # last sign is a '' - basisfunctions += [splitrow[2:]] + basisfunctions += [row_parts[2:]] else: end = True if "setting up local basis functions..." in row: @@ -1159,7 +1159,7 @@ def _get_warning_orthonormalization(data): for row in data: splitrow = row.split() if "orthonormalized" in splitrow: - orthowarning += [" ".join(splitrow[1:])] + orthowarning.append(" ".join(splitrow[1:])) return orthowarning @staticmethod @@ -1168,7 +1168,7 @@ def _get_all_warning_lines(data): for row in data: splitrow = row.split() if len(splitrow) > 0 and splitrow[0] == "WARNING:": - ws += [" ".join(splitrow[1:])] + ws.append(" ".join(splitrow[1:])) return ws @staticmethod @@ -1177,7 +1177,7 @@ def _get_all_info_lines(data): for row in data: splitrow = row.split() if len(splitrow) > 0 and splitrow[0] == "INFO:": - infos += [" ".join(splitrow[1:])] + infos.append(" ".join(splitrow[1:])) return infos @@ -1218,10 +1218,10 @@ def __init__( "FATBAND_*" files will be read kpoints_file (PathLike): KPOINTS file for bandstructure calculation, typically "KPOINTS". vasprun_file (PathLike): Corresponding vasprun file. - Instead, the Fermi energy from the DFT run can be provided. Then, + Instead, the Fermi level from the DFT run can be provided. Then, this value should be set to None. structure (Structure): Structure object. - efermi (float): fermi energy in eV. + efermi (float): Fermi level in eV. """ warnings.warn("Make sure all relevant FATBAND files were generated and read in!") warnings.warn("Use Lobster 3.2.0 or newer for fatband calculations!") @@ -1260,7 +1260,7 @@ def __init__( filenames = "." for name in os.listdir(filenames): if fnmatch.fnmatch(name, "FATBAND_*.lobster"): - filenames_new += [os.path.join(filenames, name)] + filenames_new.append(os.path.join(filenames, name)) filenames = filenames_new if len(filenames) == 0: raise ValueError("No FATBAND files in folder or given") @@ -1268,17 +1268,17 @@ def __init__( with zopen(name, mode="rt") as file: contents = file.read().split("\n") - atom_names += [os.path.split(name)[1].split("_")[1].capitalize()] + atom_names.append(os.path.split(name)[1].split("_")[1].capitalize()) parameters = contents[0].split() - atom_type += [re.split(r"[0-9]+", parameters[3])[0].capitalize()] - orbital_names += [parameters[4]] + atom_type.append(re.split(r"[0-9]+", parameters[3])[0].capitalize()) + orbital_names.append(parameters[4]) # get atomtype orbital dict atom_orbital_dict = {} # type: dict - for iatom, atom in enumerate(atom_names): + for idx, atom in enumerate(atom_names): if atom not in atom_orbital_dict: atom_orbital_dict[atom] = [] - atom_orbital_dict[atom] += [orbital_names[iatom]] + atom_orbital_dict[atom].append(orbital_names[idx]) # test if there are the same orbitals twice or if two different formats were used or if all necessary orbitals # are there for items in atom_orbital_dict.values(): @@ -1286,9 +1286,9 @@ def __init__( raise ValueError("The are two FATBAND files for the same atom and orbital. The program will stop.") split = [] for item in items: - split += [item.split("_")[0]] + split.append(item.split("_")[0]) for number in collections.Counter(split).values(): - if number not in (1, 3, 5, 7): + if number not in {1, 3, 5, 7}: raise ValueError( "Make sure all relevant orbitals were generated and that no duplicates (2p and 2p_x) are " "present" @@ -1313,7 +1313,7 @@ def __init__( linenumbers = [] for iline, line in enumerate(contents[1 : self.nbands * 2 + 4]): if line.split()[0] == "#": - linenumbers += [iline] + linenumbers.append(iline) if ifilename == 0: self.is_spinpolarized = len(linenumbers) == 2 @@ -1363,7 +1363,7 @@ def __init__( ] ) if ifilename == 0: - kpoints_array += [KPOINT] + kpoints_array.append(KPOINT) linenumber = 0 iband = 0 @@ -1406,7 +1406,7 @@ def get_bandstructure(self) -> LobsterBandStructureSymmLine: kpoints=self.kpoints_array, eigenvals=self.eigenvals, lattice=self.lattice, - efermi=self.efermi, + efermi=self.efermi, # type: ignore[arg-type] labels_dict=self.label_dict, structure=self.structure, projections=self.p_eigenvals, @@ -1415,7 +1415,6 @@ def get_bandstructure(self) -> LobsterBandStructureSymmLine: class Bandoverlaps(MSONable): """Read in bandOverlaps.lobster files. These files are not created during every Lobster run. - Attributes: band_overlaps_dict (dict[Spin, Dict[str, Dict[str, Union[float, np.ndarray]]]]): A dictionary containing the band overlap data of the form: {spin: {"kpoint as string": {"maxDeviation": @@ -1455,7 +1454,7 @@ def __init__( def _read(self, contents: list, spin_numbers: list): """ - Will read in all contents of the file. + Will read in all contents of the file Args: contents: list of strings @@ -1475,7 +1474,7 @@ def _read(self, contents: list, spin_numbers: list): kpoint_array = [] for kpointel in kpoint: if kpointel not in {"at", "k-point", ""}: - kpoint_array += [float(kpointel)] + kpoint_array.append(float(kpointel)) elif "maxDeviation" in line: if spin not in self.band_overlaps_dict: @@ -1487,23 +1486,23 @@ def _read(self, contents: list, spin_numbers: list): if "matrices" not in self.band_overlaps_dict[spin]: self.band_overlaps_dict[spin]["matrices"] = [] maxdev = line.split(" ")[2] - self.band_overlaps_dict[spin]["max_deviations"] += [float(maxdev)] + self.band_overlaps_dict[spin]["max_deviations"].append(float(maxdev)) self.band_overlaps_dict[spin]["k_points"] += [kpoint_array] - self.max_deviation += [float(maxdev)] + self.max_deviation.append(float(maxdev)) overlaps = [] else: rows = [] for el in line.split(" "): if el != "": - rows += [float(el)] + rows.append(float(el)) overlaps += [rows] if len(overlaps) == len(rows): self.band_overlaps_dict[spin]["matrices"] += [np.matrix(overlaps)] def has_good_quality_maxDeviation(self, limit_maxDeviation: float = 0.1) -> bool: """ - Will check if the maxDeviation from the ideal bandoverlap is smaller or equal to limit_maxDeviation. + Will check if the maxDeviation from the ideal bandoverlap is smaller or equal to limit_maxDeviation Args: limit_maxDeviation: limit of the maxDeviation @@ -1584,7 +1583,7 @@ def __init__(self, filename: str = "GROSSPOP.lobster", list_dict_grosspop: list[ """ Args: filename: filename of the "GROSSPOP.lobster" file - list_dict_grosspop: List of dictionaries including all information about the gross populations. + list_dict_grosspop: List of dictionaries including all information about the gross populations """ # opens file self._filename = filename @@ -1607,10 +1606,10 @@ def __init__(self, filename: str = "GROSSPOP.lobster", list_dict_grosspop: list[ small_dict["Mulliken GP"][cleanline[0]] = float(cleanline[1]) small_dict["Loewdin GP"][cleanline[0]] = float(cleanline[2]) if "total" in cleanline[0]: - self.list_dict_grosspop += [small_dict] + self.list_dict_grosspop.append(small_dict) def get_structure_with_total_grosspop(self, structure_filename: str) -> Structure: - """Get a Structure with Mulliken and Loewdin total grosspopulations as site properties. + """Get a Structure with Mulliken and Loewdin total grosspopulations as site properties Args: structure_filename (str): filename of POSCAR @@ -1619,7 +1618,6 @@ def get_structure_with_total_grosspop(self, structure_filename: str) -> Structur Structure Object with Mulliken and Loewdin total grosspopulations as site properties. """ struct = Structure.from_file(structure_filename) - # site_properties: dict[str, Any] = {} mullikengp = [] loewdingp = [] for grosspop in self.list_dict_grosspop: @@ -1668,9 +1666,9 @@ def _parse_file(filename): splitline = line.split() if len(splitline) >= 6: points += [[float(splitline[0]), float(splitline[1]), float(splitline[2])]] - distance += [float(splitline[3])] - real += [float(splitline[4])] - imaginary += [float(splitline[5])] + distance.append(float(splitline[3])) + real.append(float(splitline[4])) + imaginary.append(float(splitline[5])) if len(real) != grid[0] * grid[1] * grid[2] or len(imaginary) != grid[0] * grid[1] * grid[2]: raise ValueError("Something went wrong while reading the file") @@ -1716,9 +1714,9 @@ def set_volumetric_data(self, grid, structure): "coordinates 0.0 0.0 0.0 coordinates 1.0 1.0 1.0 box bandlist 1 " ) - new_x += [x_here] - new_y += [y_here] - new_z += [z_here] + new_x.append(x_here) + new_y.append(y_here) + new_z.append(z_here) new_real += [self.real[runner]] new_imaginary += [self.imaginary[runner]] @@ -1885,7 +1883,7 @@ def __init__( sitepotentials_loewdin: Loewdin site potential sitepotentials_mulliken: Mulliken site potential madelungenergies_loewdin: Madelung energy based on the Loewdin approach - madelungenergies_mulliken: Madelung energy based on the Mulliken approach. + madelungenergies_mulliken: Madelung energy based on the Mulliken approach """ self._filename = filename self.ewald_splitting = [] if ewald_splitting is None else ewald_splitting @@ -1910,17 +1908,17 @@ def __init__( data = data[5:-1] self.num_atoms = len(data) - 2 for atom in range(self.num_atoms): - line = data[atom].split() - self.atomlist += [line[1] + str(line[0])] - self.types += [line[1]] - self.sitepotentials_mulliken += [float(line[2])] - self.sitepotentials_loewdin += [float(line[3])] + line_parts = data[atom].split() + self.atomlist.append(line_parts[1] + str(line_parts[0])) + self.types.append(line_parts[1]) + self.sitepotentials_mulliken.append(float(line_parts[2])) + self.sitepotentials_loewdin.append(float(line_parts[3])) self.madelungenergies_mulliken = float(data[self.num_atoms + 1].split()[3]) self.madelungenergies_loewdin = float(data[self.num_atoms + 1].split()[4]) def get_structure_with_site_potentials(self, structure_filename): - """Get a Structure with Mulliken and Loewdin charges as site properties. + """Get a Structure with Mulliken and Loewdin charges as site properties Args: structure_filename: filename of POSCAR @@ -1983,7 +1981,7 @@ def get_orb_from_str(orbs): list of tw Orbital objects """ # TODO: also useful for plotting of DOS - orb_labs = [ + orb_labs = ( "s", "p_y", "p_z", @@ -2000,7 +1998,7 @@ def get_orb_from_str(orbs): "f_xz^2", "f_z(x^2-y^2)", "f_x(x^2-3y^2)", - ] + ) orbitals = [(int(orb[0]), Orbital(orb_labs.index(orb[1:]))) for orb in orbs] orb_label = "" @@ -2055,8 +2053,9 @@ def __init__(self, e_fermi=None, filename: str = "hamiltonMatrices.lobster"): Args: filename: filename for the hamiltonMatrices file, typically "hamiltonMatrices.lobster". e_fermi: fermi level in eV for the structure only - relevant if input file contains hamilton matrices data. + relevant if input file contains hamilton matrices data """ + self._filename = filename # hamiltonMatrices with zopen(self._filename, mode="rt") as file: @@ -2101,21 +2100,21 @@ def _parse_matrix(file_data, pattern, e_fermi): for idx, line in enumerate(file_data): line = line.strip() if "Real parts" in line: - start_inxs_real += [idx + 1] + start_inxs_real.append(idx + 1) if idx == 1: # ignore the first occurrence as files start with real matrices pass else: - end_inxs_imag += [idx - 1] + end_inxs_imag.append(idx - 1) matches = re.search(pattern, file_data[idx - 1]) if matches and len(matches.groups()) == 2: k_point = matches.group(2) complex_matrices[k_point] = {} if "Imag parts" in line: - end_inxs_real += [idx - 1] - start_inxs_imag += [idx + 1] + end_inxs_real.append(idx - 1) + start_inxs_imag.append(idx + 1) # explicitly add the last line as files end with imaginary matrix if idx == len(file_data) - 1: - end_inxs_imag += [len(file_data)] + end_inxs_imag.append(len(file_data)) # extract matrix data and store diagonal elements matrix_real = [] @@ -2136,13 +2135,13 @@ def _parse_matrix(file_data, pattern, e_fermi): matches = re.search(pattern, file_data[start_inx_real - 2]) if matches and len(matches.groups()) == 2: - spin = Spin.up if matches.group(1) == "1" else Spin.down - k_point = matches.group(2) + spin = Spin.up if matches[1] == "1" else Spin.down + k_point = matches[2] complex_matrices[k_point].update({spin: comp_matrix}) elif matches and len(matches.groups()) == 1: - k_point = matches.group(1) + k_point = matches[1] complex_matrices.update({k_point: comp_matrix}) - matrix_diagonal_values += [comp_matrix.real.diagonal() - e_fermi] + matrix_diagonal_values.append(comp_matrix.real.diagonal() - e_fermi) # extract elements basis functions as list elements_basis_functions = [ From a80b9a7b74f13a22daf3872e3964207e581b5e48 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sat, 3 Aug 2024 21:46:45 +0800 Subject: [PATCH 053/180] Improve types for `electronic_structure.{bandstructure/cohp}` (#3873) * add types for bandstructure * relocate magic methods to top * add some types * fix type errors in bandstructure * temp save * first run of cohp, mypy errors to fix * fix collection generation * add type `SpinLike` and case tweaks * reduce repetition for `__str__` of `IcohpValue` * simplify condition * reduce indentation level * clarify `translation` * clarify `list_num` and other docstrings * clarify `label` as str * more type and docstring improvements * fix unit test * fix most mypy errors * fix remaining mypy errors * add DEBUG tag * reduce code repetition * Need Confirm: set `translation` as tuple * pre-commit auto-fixes * more type clarify * clarify `num` argument * clarify docstring of `bandstructure` * more minor tweaks * clarify type of labels_dict * replace unnecessary single-item list extend with append * fix typo * relocate magic method * clarify type of `list_icohp` * remove unused type alias * revert undesired rename * replace more single item extend with append * simplify dict generation * fix downstream lobsterpy error * tweak module docstring * need confirm: allow efermi to be None * pre-commit auto-fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- cohp.py | 948 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 525 insertions(+), 423 deletions(-) diff --git a/cohp.py b/cohp.py index b641d2a399..acf4f8f45b 100644 --- a/cohp.py +++ b/cohp.py @@ -1,6 +1,8 @@ -"""This module defines classes to represent crystal orbital Hamilton -populations (COHP) and integrated COHP (ICOHP), but can also be used -for crystal orbital overlap populations (COOP) or crystal orbital bond indices (COBIs). +"""This module defines classes to represent: + - Crystal orbital Hamilton population (COHP) and integrated COHP (ICOHP). + - Crystal orbital overlap population (COOP). + - Crystal orbital bond index (COBI). + If you use this module, please cite: J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, "Automated Bonding Analysis with Crystal Orbital Hamilton Populations", @@ -29,10 +31,14 @@ from pymatgen.util.num import round_to_sigfigs if TYPE_CHECKING: - from typing import Any + from collections.abc import Sequence + from typing import Any, Literal + from numpy.typing import NDArray from typing_extensions import Self + from pymatgen.util.typing import PathLike, SpinLike, Vector3D + __author__ = "Marco Esters, Janine George" __copyright__ = "Copyright 2017, The Materials Project" __version__ = "0.2" @@ -50,17 +56,24 @@ class Cohp(MSONable): """Basic COHP object.""" def __init__( - self, efermi, energies, cohp, are_coops=False, are_cobis=False, are_multi_center_cobis=False, icohp=None + self, + efermi: float, + energies: Sequence[float], + cohp: dict[Spin, NDArray], + are_coops: bool = False, + are_cobis: bool = False, + are_multi_center_cobis: bool = False, + icohp: dict[Spin, NDArray] | None = None, ) -> None: """ Args: - are_coops: Indicates whether this object describes COOPs. - are_cobis: Indicates whether this object describes COBIs. - are_multi_center_cobis: Indicates whether this object describes multi-center COBIs - efermi: Fermi energy. - energies: A sequence of energies. - cohp ({Spin: np.array}): representing the COHP for each spin. - icohp ({Spin: np.array}): representing the ICOHP for each spin. + efermi (float): The Fermi level. + energies (Sequence[float]): Energies. + cohp ({Spin: NDArrary}): The COHP for each spin. + are_coops (bool): Whether this object describes COOPs. + are_cobis (bool): Whether this object describes COBIs. + are_multi_center_cobis (bool): Whether this object describes multi-center COBIs. + icohp ({Spin: NDArrary}): The ICOHP for each spin. """ self.are_coops = are_coops self.are_cobis = are_cobis @@ -71,7 +84,7 @@ def __init__( self.icohp = icohp def __repr__(self) -> str: - """Get a string that can be easily plotted (e.g. using gnuplot).""" + """A string that can be easily plotted (e.g. using gnuplot).""" if self.are_coops: cohp_str = "COOP" elif self.are_cobis or self.are_multi_center_cobis: @@ -97,7 +110,7 @@ def __repr__(self) -> str: str_arr.append(format_data.format(*(d[idx] for d in data))) return "\n".join(str_arr) - def as_dict(self): + def as_dict(self) -> dict[str, Any]: """JSON-serializable dict representation of COHP.""" dct = { "@module": type(self).__module__, @@ -113,20 +126,22 @@ def as_dict(self): dct["ICOHP"] = {str(spin): pops.tolist() for spin, pops in self.icohp.items()} return dct - def get_cohp(self, spin=None, integrated=False): + def get_cohp( + self, + spin: SpinLike | None = None, + integrated: bool = False, + ) -> dict[Spin, NDArray] | None: """Get the COHP or ICOHP for a particular spin. Args: - spin: Spin. Can be parsed as spin object, integer (-1/1) - or str ("up"/"down") - integrated: Return COHP (False) or ICOHP (True) + spin (SpinLike): Selected spin. If is None and both + spins are present, both will be returned. + integrated: Return ICOHP (True) or COHP (False). Returns: - Returns the CHOP or ICOHP for the input spin. If Spin is - None and both spins are present, both spins will be returned - as a dictionary. + dict: The COHP or ICOHP for the selected spin. """ - populations = self.cohp if not integrated else self.icohp + populations = self.icohp if integrated else self.cohp if populations is None: return None @@ -138,118 +153,127 @@ def get_cohp(self, spin=None, integrated=False): spin = Spin({"up": 1, "down": -1}[spin.lower()]) return {spin: populations[spin]} - def get_icohp(self, spin=None): - """Convenient alternative to get the ICOHP for a particular spin.""" + def get_icohp( + self, + spin: SpinLike | None = None, + ) -> dict[Spin, NDArray] | None: + """Convenient wrapper to get the ICOHP for a particular spin.""" return self.get_cohp(spin=spin, integrated=True) - def get_interpolated_value(self, energy, integrated=False): - """Get the COHP for a particular energy. + def get_interpolated_value( + self, + energy: float, + integrated: bool = False, + ) -> dict[Spin, float]: + """Get the interpolated COHP for a particular energy. Args: - energy: Energy to return the COHP value for. - integrated: Return COHP (False) or ICOHP (True) + energy (float): Energy to get the COHP value for. + integrated (bool): Return ICOHP (True) or COHP (False). """ - inter = {} + inters = {} for spin in self.cohp: if not integrated: - inter[spin] = get_linear_interpolated_value(self.energies, self.cohp[spin], energy) + inters[spin] = get_linear_interpolated_value(self.energies, self.cohp[spin], energy) elif self.icohp is not None: - inter[spin] = get_linear_interpolated_value(self.energies, self.icohp[spin], energy) + inters[spin] = get_linear_interpolated_value(self.energies, self.icohp[spin], energy) else: raise ValueError("ICOHP is empty.") - return inter + return inters + + def has_antibnd_states_below_efermi( + self, + spin: SpinLike | None = None, + limit: float = 0.01, + ) -> dict[Spin, bool] | None: + """Get dict of antibonding states below the Fermi level for the spin. - def has_antibnd_states_below_efermi(self, spin=None, limit=0.01): - """Get dict indicating if there are antibonding states below the Fermi level depending on the spin - spin: Spin - limit: -COHP smaller -limit will be considered. + Args: + spin (SpinLike): Selected spin. + limit (float): Only COHP higher than this value will be considered. """ populations = self.cohp n_energies_below_efermi = len([energy for energy in self.energies if energy <= self.efermi]) if populations is None: return None + + dict_to_return = {} if spin is None: - dict_to_return = {} for sp, cohp_vals in populations.items(): - if (max(cohp_vals[:n_energies_below_efermi])) > limit: - dict_to_return[sp] = True - else: - dict_to_return[sp] = False + # NOTE: Casting to bool is necessary, otherwise ended up + # getting "bool_" instead of "bool" from NumPy + dict_to_return[sp] = bool((max(cohp_vals[:n_energies_below_efermi])) > limit) + else: - dict_to_return = {} if isinstance(spin, int): spin = Spin(spin) elif isinstance(spin, str): spin = Spin({"up": 1, "down": -1}[spin.lower()]) - if (max(populations[spin][:n_energies_below_efermi])) > limit: - dict_to_return[spin] = True - else: - dict_to_return[spin] = False + dict_to_return[spin] = bool((max(populations[spin][:n_energies_below_efermi])) > limit) return dict_to_return @classmethod def from_dict(cls, dct: dict[str, Any]) -> Self: - """Get a COHP object from a dict representation of the COHP.""" + """Generate Cohp from a dict representation.""" icohp = {Spin(int(key)): np.array(val) for key, val in dct["ICOHP"].items()} if "ICOHP" in dct else None - are_cobis = dct.get("are_cobis", False) - are_multi_center_cobis = dct.get("are_multi_center_cobis", False) + return cls( dct["efermi"], dct["energies"], {Spin(int(key)): np.array(val) for key, val in dct["COHP"].items()}, icohp=icohp, are_coops=dct["are_coops"], - are_cobis=are_cobis, - are_multi_center_cobis=are_multi_center_cobis, + are_cobis=dct.get("are_cobis", False), + are_multi_center_cobis=dct.get("are_multi_center_cobis", False), ) class CompleteCohp(Cohp): - """A wrapper class that defines an average COHP, and individual COHPs. + """A wrapper that defines an average COHP, and individual COHPs. Attributes: - are_coops (bool): Indicates whether the object is consisting of COOPs. - are_cobis (bool): Indicates whether the object is consisting of COBIs. - efermi (float): Fermi energy. + are_coops (bool): Whether the object is consisting of COOPs. + are_cobis (bool): Whether the object is consisting of COBIs. + efermi (float): The Fermi level. energies (Sequence[float]): Sequence of energies. - structure (pymatgen.Structure): Structure associated with the COHPs. + structure (Structure): Structure associated with the COHPs. cohp (Sequence[float]): The average COHP. icohp (Sequence[float]): The average ICOHP. - all_cohps (dict[str, Sequence[float]]): A dict of COHPs for individual bonds of the form {label: COHP}. + all_cohps (dict[str, Sequence[float]]): COHPs for individual bonds of the form {label: COHP}. orb_res_cohp (dict[str, Dict[str, Sequence[float]]]): Orbital-resolved COHPs. """ def __init__( self, - structure, - avg_cohp, - cohp_dict, - bonds=None, - are_coops=False, - are_cobis=False, - are_multi_center_cobis=False, - orb_res_cohp=None, + structure: Structure, + avg_cohp: Cohp, + cohp_dict: dict[str, Cohp], + bonds: dict[str, Any] | None = None, + are_coops: bool = False, + are_cobis: bool = False, + are_multi_center_cobis: bool = False, + orb_res_cohp: dict[str, dict] | None = None, ) -> None: """ Args: - structure: Structure associated with this COHP. - avg_cohp: The average cohp as a COHP object. - cohp_dict: A dict of COHP objects for individual bonds of the form - {label: COHP} - bonds: A dict containing information on the bonds of the form - {label: {key: val}}. The key-val pair can be any information - the user wants to put in, but typically contains the sites, - the bond length, and the number of bonds. If nothing is + structure (Structure): Structure associated with this COHP. + avg_cohp (Cohp): The average COHP. + cohp_dict (dict[str, Cohp]): COHP for individual bonds of the form + {label: COHP}. + bonds (dict[str, Any]): Information on the bonds of the form + {label: {key: val}}. The value can be any information, + but typically contains the sites, the bond length, + and the number of bonds. If nothing is supplied, it will default to an empty dict. - are_coops: indicates whether the Cohp objects are COOPs. + are_coops (bool): Whether the Cohp objects are COOPs. Defaults to False for COHPs. - are_cobis: indicates whether the Cohp objects are COBIs. + are_cobis (bool): Whether the Cohp objects are COBIs. Defaults to False for COHPs. - are_multi_center_cobis: indicates whether the Cohp objects are multi-center COBIs. + are_multi_center_cobis (bool): Whether the Cohp objects are multi-center COBIs. Defaults to False for COHPs. - orb_res_cohp: Orbital-resolved COHPs. + orb_res_cohp (dict): Orbital-resolved COHPs. """ if ( (are_coops and are_cobis) @@ -257,6 +281,7 @@ def __init__( or (are_cobis and are_multi_center_cobis) ): raise ValueError("You cannot have info about COOPs, COBIs and/or multi-center COBIS in the same file.") + super().__init__( avg_cohp.efermi, avg_cohp.energies, @@ -276,12 +301,15 @@ def __init__( def __str__(self) -> str: if self.are_coops: - return f"Complete COOPs for {self.structure}" - if self.are_cobis: - return f"Complete COBIs for {self.structure}" - return f"Complete COHPs for {self.structure}" + header = "COOPs" + elif self.are_cobis: + header = "COBIs" + else: + header = "COHPs" + + return f"Complete {header} for {self.structure}" - def as_dict(self): + def as_dict(self) -> dict[str, Any]: """JSON-serializable dict representation of CompleteCohp.""" dct = { "@module": type(self).__module__, @@ -299,16 +327,14 @@ def as_dict(self): dct["ICOHP"] = {"average": {str(spin): pops.tolist() for spin, pops in self.icohp.items()}} for label in self.all_cohps: - dct["COHP"].update({label: {str(spin): pops.tolist() for spin, pops in self.all_cohps[label].cohp.items()}}) - if self.all_cohps[label].icohp is not None: + dct["COHP"] |= {label: {str(spin): pops.tolist() for spin, pops in self.all_cohps[label].cohp.items()}} + icohp = self.all_cohps[label].icohp + if icohp is not None: if "ICOHP" not in dct: - dct["ICOHP"] = { - label: {str(spin): pops.tolist() for spin, pops in self.all_cohps[label].icohp.items()} - } + dct["ICOHP"] = {label: {str(spin): pops.tolist() for spin, pops in icohp.items()}} else: - dct["ICOHP"].update( - {label: {str(spin): pops.tolist() for spin, pops in self.all_cohps[label].icohp.items()}} - ) + dct["ICOHP"] |= {label: {str(spin): pops.tolist() for spin, pops in icohp.items()}} + if False in [bond_dict == {} for bond_dict in self.bonds.values()]: dct["bonds"] = { bond: { @@ -317,44 +343,55 @@ def as_dict(self): } for bond in self.bonds } + if self.orb_res_cohp: - orb_dict = {} + orb_dict: dict[str, Any] = {} for label in self.orb_res_cohp: orb_dict[label] = {} for orbs in self.orb_res_cohp[label]: - cohp = {str(spin): pops.tolist() for spin, pops in self.orb_res_cohp[label][orbs]["COHP"].items()} - orb_dict[label][orbs] = {"COHP": cohp} - icohp = {str(spin): pops.tolist() for spin, pops in self.orb_res_cohp[label][orbs]["ICOHP"].items()} - orb_dict[label][orbs]["ICOHP"] = icohp - orbitals = [[orb[0], orb[1].name] for orb in self.orb_res_cohp[label][orbs]["orbitals"]] - orb_dict[label][orbs]["orbitals"] = orbitals + orb_dict[label][orbs] = { + "COHP": { + str(spin): pops.tolist() for spin, pops in self.orb_res_cohp[label][orbs]["COHP"].items() + }, + "ICOHP": { + str(spin): pops.tolist() for spin, pops in self.orb_res_cohp[label][orbs]["ICOHP"].items() + }, + "orbitals": [[orb[0], orb[1].name] for orb in self.orb_res_cohp[label][orbs]["orbitals"]], + } + dct["orb_res_cohp"] = orb_dict return dct - def get_cohp_by_label(self, label, summed_spin_channels=False): - """Get specific COHP object. + def get_cohp_by_label( + self, + label: str, + summed_spin_channels: bool = False, + ) -> Cohp: + """Get specific Cohp by the label, to simplify plotting. Args: - label: string (for newer Lobster versions: a number) - summed_spin_channels: bool, will sum the spin channels and return the sum in Spin.up if true + label (str): Label for the interaction. + summed_spin_channels (bool): Sum the spin channels and return the sum as Spin.up. Returns: - Returns the COHP object to simplify plotting + The Cohp. """ if label.lower() == "average": - divided_cohp = self.cohp - divided_icohp = self.icohp - + divided_cohp: dict[Spin, Any] | None = self.cohp + divided_icohp: dict[Spin, Any] | None = self.icohp else: divided_cohp = self.all_cohps[label].get_cohp(spin=None, integrated=False) divided_icohp = self.all_cohps[label].get_icohp(spin=None) + assert divided_cohp is not None + if summed_spin_channels and Spin.down in self.cohp: - final_cohp = {} - final_icohp = {} - final_cohp[Spin.up] = np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0) - final_icohp[Spin.up] = np.sum([divided_icohp[Spin.up], divided_icohp[Spin.down]], axis=0) + assert divided_icohp is not None + final_cohp: dict[Spin, Any] = {Spin.up: np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0)} + final_icohp: dict[Spin, Any] | None = { + Spin.up: np.sum([divided_icohp[Spin.up], divided_icohp[Spin.down]], axis=0) + } else: final_cohp = divided_cohp final_icohp = divided_icohp @@ -368,46 +405,50 @@ def get_cohp_by_label(self, label, summed_spin_channels=False): icohp=final_icohp, ) - def get_summed_cohp_by_label_list(self, label_list, divisor=1, summed_spin_channels=False): - """Get a COHP object that includes a summed COHP divided by divisor. + def get_summed_cohp_by_label_list( + self, + label_list: list[str], + divisor: float = 1, + summed_spin_channels: bool = False, + ) -> Cohp: + """Get a Cohp object that includes a summed COHP divided by divisor. Args: - label_list: list of labels for the COHP that should be included in the summed cohp - divisor: float/int, the summed cohp will be divided by this divisor - summed_spin_channels: bool, will sum the spin channels and return the sum in Spin.up if true + label_list (list[str]): Labels for the COHP to include. + divisor (float): The summed COHP will be divided by this divisor. + summed_spin_channels (bool): Sum the spin channels and return the sum in Spin.up. Returns: - Returns a COHP object including a summed COHP + A Cohp object for the summed COHP. """ - # check if cohps are spinpolarized or not + # Check if COHPs are spin polarized first_cohpobject = self.get_cohp_by_label(label_list[0]) summed_cohp = first_cohpobject.cohp.copy() + assert first_cohpobject.icohp is not None summed_icohp = first_cohpobject.icohp.copy() for label in label_list[1:]: - cohp_here = self.get_cohp_by_label(label) - summed_cohp[Spin.up] = np.sum([summed_cohp[Spin.up], cohp_here.cohp[Spin.up]], axis=0) + cohp = self.get_cohp_by_label(label) + icohp = cohp.icohp + assert icohp is not None + summed_cohp[Spin.up] = np.sum([summed_cohp[Spin.up], cohp.cohp[Spin.up]], axis=0) if Spin.down in summed_cohp: - summed_cohp[Spin.down] = np.sum([summed_cohp[Spin.down], cohp_here.cohp[Spin.down]], axis=0) + summed_cohp[Spin.down] = np.sum([summed_cohp[Spin.down], cohp.cohp[Spin.down]], axis=0) - summed_icohp[Spin.up] = np.sum([summed_icohp[Spin.up], cohp_here.icohp[Spin.up]], axis=0) + summed_icohp[Spin.up] = np.sum([summed_icohp[Spin.up], icohp[Spin.up]], axis=0) if Spin.down in summed_icohp: - summed_icohp[Spin.down] = np.sum([summed_icohp[Spin.down], cohp_here.icohp[Spin.down]], axis=0) + summed_icohp[Spin.down] = np.sum([summed_icohp[Spin.down], icohp[Spin.down]], axis=0) - divided_cohp = {} - divided_icohp = {} - divided_cohp[Spin.up] = np.divide(summed_cohp[Spin.up], divisor) - divided_icohp[Spin.up] = np.divide(summed_icohp[Spin.up], divisor) + divided_cohp = {Spin.up: np.divide(summed_cohp[Spin.up], divisor)} + divided_icohp = {Spin.up: np.divide(summed_icohp[Spin.up], divisor)} if Spin.down in summed_cohp: divided_cohp[Spin.down] = np.divide(summed_cohp[Spin.down], divisor) divided_icohp[Spin.down] = np.divide(summed_icohp[Spin.down], divisor) if summed_spin_channels and Spin.down in summed_cohp: - final_cohp = {} - final_icohp = {} - final_cohp[Spin.up] = np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0) - final_icohp[Spin.up] = np.sum([divided_icohp[Spin.up], divided_icohp[Spin.down]], axis=0) + final_cohp = {Spin.up: np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0)} + final_icohp = {Spin.up: np.sum([divided_icohp[Spin.up], divided_icohp[Spin.down]], axis=0)} else: final_cohp = divided_cohp final_icohp = divided_icohp @@ -422,50 +463,56 @@ def get_summed_cohp_by_label_list(self, label_list, divisor=1, summed_spin_chann ) def get_summed_cohp_by_label_and_orbital_list( - self, label_list, orbital_list, divisor=1, summed_spin_channels=False - ): - """Get a COHP object that includes a summed COHP divided by divisor. + self, + label_list: list[str], + orbital_list: list[str], + divisor: float = 1, + summed_spin_channels: bool = False, + ) -> Cohp: + """Get a Cohp object that includes a summed COHP divided by divisor. Args: - label_list: list of labels for the COHP that should be included in the summed cohp - orbital_list: list of orbitals for the COHPs that should be included in the summed cohp (same order as - label_list) - divisor: float/int, the summed cohp will be divided by this divisor - summed_spin_channels: bool, will sum the spin channels and return the sum in Spin.up if true + label_list (list[str]): Labels for the COHP that should be included. + orbital_list (list[str]): Orbitals for the COHPs that should be included + (same order as label_list). + divisor (float): The summed COHP will be divided by this divisor. + summed_spin_channels (bool): Sum the spin channels and return the sum in Spin.up. Returns: - Returns a COHP object including a summed COHP + A Cohp object including the summed COHP. """ - # check length of label_list and orbital_list: + # Check length of label_list and orbital_list if not len(label_list) == len(orbital_list): raise ValueError("label_list and orbital_list don't have the same length!") - # check if cohps are spinpolarized or not + + # Check if COHPs are spin polarized first_cohpobject = self.get_orbital_resolved_cohp(label_list[0], orbital_list[0]) + assert first_cohpobject is not None + assert first_cohpobject.icohp is not None summed_cohp = first_cohpobject.cohp.copy() summed_icohp = first_cohpobject.icohp.copy() - for ilabel, label in enumerate(label_list[1:], start=1): - cohp_here = self.get_orbital_resolved_cohp(label, orbital_list[ilabel]) - summed_cohp[Spin.up] = np.sum([summed_cohp[Spin.up], cohp_here.cohp.copy()[Spin.up]], axis=0) + + for idx, label in enumerate(label_list[1:], start=1): + cohp = self.get_orbital_resolved_cohp(label, orbital_list[idx]) + assert cohp is not None + assert cohp.icohp is not None + summed_cohp[Spin.up] = np.sum([summed_cohp[Spin.up], cohp.cohp.copy()[Spin.up]], axis=0) if Spin.down in summed_cohp: - summed_cohp[Spin.down] = np.sum([summed_cohp[Spin.down], cohp_here.cohp.copy()[Spin.down]], axis=0) - summed_icohp[Spin.up] = np.sum([summed_icohp[Spin.up], cohp_here.icohp.copy()[Spin.up]], axis=0) + summed_cohp[Spin.down] = np.sum([summed_cohp[Spin.down], cohp.cohp.copy()[Spin.down]], axis=0) + + summed_icohp[Spin.up] = np.sum([summed_icohp[Spin.up], cohp.icohp.copy()[Spin.up]], axis=0) if Spin.down in summed_icohp: - summed_icohp[Spin.down] = np.sum([summed_icohp[Spin.down], cohp_here.icohp.copy()[Spin.down]], axis=0) + summed_icohp[Spin.down] = np.sum([summed_icohp[Spin.down], cohp.icohp.copy()[Spin.down]], axis=0) - divided_cohp = {} - divided_icohp = {} - divided_cohp[Spin.up] = np.divide(summed_cohp[Spin.up], divisor) - divided_icohp[Spin.up] = np.divide(summed_icohp[Spin.up], divisor) + divided_cohp = {Spin.up: np.divide(summed_cohp[Spin.up], divisor)} + divided_icohp = {Spin.up: np.divide(summed_icohp[Spin.up], divisor)} if Spin.down in summed_cohp: divided_cohp[Spin.down] = np.divide(summed_cohp[Spin.down], divisor) divided_icohp[Spin.down] = np.divide(summed_icohp[Spin.down], divisor) if summed_spin_channels and Spin.down in divided_cohp: - final_cohp = {} - final_icohp = {} - - final_cohp[Spin.up] = np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0) - final_icohp[Spin.up] = np.sum([divided_icohp[Spin.up], divided_icohp[Spin.down]], axis=0) + final_cohp = {Spin.up: np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0)} + final_icohp = {Spin.up: np.sum([divided_icohp[Spin.up], divided_icohp[Spin.down]], axis=0)} else: final_cohp = divided_cohp final_icohp = divided_icohp @@ -479,30 +526,34 @@ def get_summed_cohp_by_label_and_orbital_list( icohp=final_icohp, ) - def get_orbital_resolved_cohp(self, label, orbitals, summed_spin_channels=False): + def get_orbital_resolved_cohp( + self, + label: str, + orbitals: str | list[tuple[str, Orbital]] | tuple[tuple[str, Orbital], ...], + summed_spin_channels: bool = False, + ) -> Cohp | None: """Get orbital-resolved COHP. Args: - label: bond label (Lobster: labels as in ICOHPLIST/ICOOPLIST.lobster). - - orbitals: The orbitals as a label, or list or tuple of the form - [(n1, orbital1), (n2, orbital2)]. Orbitals can either be str, - int, or Orbital. - - summed_spin_channels: bool, will sum the spin channels and return the sum in Spin.up if true + label (str): Bond labels as in ICOHPLIST/ICOOPLIST.lobster. + orbitals: The orbitals as a label, or list/tuple of + [(n1, orbital1), (n2, orbital2), ...]. + Where each orbital can either be str, int, or Orbital. + summed_spin_channels (bool): Sum the spin channels and return the sum as Spin.up. Returns: - A Cohp object if CompleteCohp contains orbital-resolved cohp, - or None if it doesn't. + A Cohp object if CompleteCohp contains orbital-resolved COHP, + or None if it doesn't. Note: It currently assumes that orbitals are str if they aren't the - other valid types. This is not ideal, but the easiest way to - avoid unicode issues between python 2 and python 3. + other valid types. This is not ideal, but is the easiest way to + avoid unicode issues between Python 2 and Python 3. """ if self.orb_res_cohp is None: return None + if isinstance(orbitals, (list, tuple)): - cohp_orbs = [d["orbitals"] for d in self.orb_res_cohp[label].values()] + cohp_orbs = [val["orbitals"] for val in self.orb_res_cohp[label].values()] orbs = [] for orbital in orbitals: if isinstance(orbital[1], int): @@ -515,10 +566,12 @@ def get_orbital_resolved_cohp(self, label, orbitals, summed_spin_channels=False) raise TypeError("Orbital must be str, int, or Orbital.") orb_index = cohp_orbs.index(orbs) orb_label = list(self.orb_res_cohp[label])[orb_index] + elif isinstance(orbitals, str): orb_label = orbitals else: raise TypeError("Orbitals must be str, list, or tuple.") + try: icohp = self.orb_res_cohp[label][orb_label]["ICOHP"] except KeyError: @@ -547,9 +600,11 @@ def get_orbital_resolved_cohp(self, label, orbitals, summed_spin_channels=False) ) @classmethod - def from_dict(cls, dct: dict) -> Self: - """Get CompleteCohp object from dict representation.""" - # TODO: clean that mess up? + def from_dict(cls, dct: dict[str, Any]) -> Self: + """Get CompleteCohp object from a dict representation. + + TODO: Clean this up. + """ cohp_dict = {} efermi = dct["efermi"] energies = dct["energies"] @@ -570,6 +625,7 @@ def from_dict(cls, dct: dict) -> Self: } else: bonds = None + for label in dct["COHP"]: cohp = {Spin(int(spin)): np.array(dct["COHP"][label][spin]) for spin in dct["COHP"][label]} try: @@ -590,7 +646,7 @@ def from_dict(cls, dct: dict) -> Self: cohp_dict[label] = Cohp(efermi, energies, cohp, icohp=icohp) if "orb_res_cohp" in dct: - orb_cohp: dict[str, dict] = {} + orb_cohp: dict[str, dict[Orbital, dict[str, Any]]] = {} for label in dct["orb_res_cohp"]: orb_cohp[label] = {} for orb in dct["orb_res_cohp"][label]: @@ -612,9 +668,9 @@ def from_dict(cls, dct: dict) -> Self: "orbitals": orbitals, } # If no total COHPs are present, calculate the total - # COHPs from the single-orbital populations. Total COHPs - # may not be present when the COHP generator keyword is used - # in LOBSTER versions 2.2.0 and earlier. + # COHPs from the single-orbital populations. + # Total COHPs may not be present when the COHP generator keyword + # is used in LOBSTER versions 2.2.0 and earlier. if label not in dct["COHP"] or dct["COHP"][label] is None: cohp = { Spin.up: np.sum( @@ -648,52 +704,52 @@ def from_dict(cls, dct: dict) -> Self: else: orb_cohp = {} - are_cobis = dct.get("are_cobis", False) - + assert avg_cohp is not None return cls( structure, avg_cohp, cohp_dict, bonds=bonds, are_coops=dct["are_coops"], - are_cobis=are_cobis, + are_cobis=dct.get("are_cobis", False), are_multi_center_cobis=are_multi_center_cobis, orb_res_cohp=orb_cohp, ) @classmethod def from_file( - cls, fmt, filename=None, structure_file=None, are_coops=False, are_cobis=False, are_multi_center_cobis=False + cls, + fmt: Literal["LMTO", "LOBSTER"], + filename: PathLike | None = None, + structure_file: PathLike | None = None, + are_coops: bool = False, + are_cobis: bool = False, + are_multi_center_cobis: bool = False, ) -> Self: - """ - Creates a CompleteCohp object from an output file of a COHP - calculation. Valid formats are either LMTO (for the Stuttgart - LMTO-ASA code) or LOBSTER (for the LOBSTER code). + """Create CompleteCohp from an output file of a COHP calculation. Args: - fmt: A string for the code that was used to calculate - the COHPs so that the output file can be handled - correctly. Can take the values "LMTO" or "LOBSTER". - filename: Name of the COHP output file. Defaults to COPL - for LMTO and COHPCAR.lobster/COOPCAR.lobster for LOBSTER. - structure_file: Name of the file containing the structure. - If no file name is given, use CTRL for LMTO and POSCAR - for LOBSTER. - are_coops: Indicates whether the populations are COOPs or - COHPs. Defaults to False for COHPs. - are_cobis: Indicates whether the populations are COBIs or - COHPs. Defaults to False for COHPs. - are_multi_center_cobis: Indicates whether this file - includes information on multi-center COBIs + fmt (Literal["LMTO", "LOBSTER"]): The code used to calculate COHPs. + filename (PathLike): The COHP output file. Defaults to "COPL" + for LMTO and "COHPCAR.lobster/COOPCAR.lobster" for LOBSTER. + structure_file (PathLike): The file containing the structure. + If None, use "CTRL" for LMTO and "POSCAR" for LOBSTER. + are_coops (bool): Whether the populations are COOPs or COHPs. + Defaults to False for COHPs. + are_cobis (bool): Whether the populations are COBIs or COHPs. + Defaults to False for COHPs. + are_multi_center_cobis (bool): Whether this file + includes information on multi-center COBIs. Returns: A CompleteCohp object. """ if are_coops and are_cobis: raise ValueError("You cannot have info about COOPs and COBIs in the same file.") - fmt = fmt.upper() + + fmt = fmt.upper() # type: ignore[assignment] if fmt == "LMTO": - # LMTO COOPs and orbital-resolved COHP cannot be handled yet. + # TODO: LMTO COOPs and orbital-resolved COHP cannot be handled yet are_coops = False are_cobis = False orb_res_cohp = None @@ -701,7 +757,9 @@ def from_file( structure_file = "CTRL" if filename is None: filename = "COPL" + cohp_file: LMTOCopl | Cohpcar = LMTOCopl(filename=filename, to_eV=True) + elif fmt == "LOBSTER": if ( (are_coops and are_cobis) @@ -725,6 +783,7 @@ def from_file( are_multi_center_cobis=are_multi_center_cobis, ) orb_res_cohp = cohp_file.orb_res_cohp + else: raise ValueError(f"Unknown format {fmt}. Valid formats are LMTO and LOBSTER.") @@ -733,9 +792,8 @@ def from_file( cohp_data = cohp_file.cohp_data energies = cohp_file.energies - # Lobster shifts the energies so that the Fermi energy is at zero. + # LOBSTER shifts the energies so that the Fermi level is at zero. # Shifting should be done by the plotter object though. - spins = [Spin.up, Spin.down] if cohp_file.is_spin_polarized else [Spin.up] if fmt == "LOBSTER": energies += efermi @@ -745,6 +803,7 @@ def from_file( # COHPs from the single-orbital populations. Total COHPs # may not be present when the cohpgenerator keyword is used # in LOBSTER versions 2.2.0 and earlier. + # TODO: Test this more extensively for label in orb_res_cohp: @@ -766,16 +825,16 @@ def from_file( } if fmt == "LMTO": - # Calculate the average COHP for the LMTO file to be - # consistent with LOBSTER output. - avg_data: dict[str, dict] = {"COHP": {}, "ICOHP": {}} - for i in avg_data: + # Calculate the average COHP for the LMTO file to be consistent with LOBSTER + avg_data: dict[Literal["COHP", "ICOHP"], dict] = {"COHP": {}, "ICOHP": {}} + for dtype in avg_data: for spin in spins: - rows = np.array([v[i][spin] for v in cohp_data.values()]) + rows = np.array([v[dtype][spin] for v in cohp_data.values()]) avg = np.mean(rows, axis=0) - # LMTO COHPs have 5 significant figures - avg_data[i].update({spin: np.array([round_to_sigfigs(a, 5) for a in avg], dtype=float)}) + # LMTO COHPs have 5 significant digits + avg_data[dtype] |= {spin: np.array([round_to_sigfigs(a, 5) for a in avg], dtype=float)} avg_cohp = Cohp(efermi, energies, avg_data["COHP"], icohp=avg_data["ICOHP"]) + elif not are_multi_center_cobis: avg_cohp = Cohp( efermi, @@ -787,9 +846,9 @@ def from_file( are_multi_center_cobis=are_multi_center_cobis, ) del cohp_data["average"] + else: - # only include two-center cobis in average - # do this for both spin channels + # Only include two-center COBIs in average for both spin channels cohp = {} cohp[Spin.up] = np.array( [np.array(c["COHP"][Spin.up]) for c in cohp_file.cohp_data.values() if len(c["sites"]) <= 2] @@ -800,6 +859,7 @@ def from_file( ).mean(axis=0) except KeyError: pass + try: icohp = {} icohp[Spin.up] = np.array( @@ -813,6 +873,7 @@ def from_file( pass except KeyError: icohp = None + avg_cohp = Cohp( efermi, energies, @@ -856,38 +917,53 @@ def from_file( class IcohpValue(MSONable): - """Store information on an ICOHP or ICOOP value. + """Information for an ICOHP or ICOOP value. Attributes: - energies (ndarray): Energy values for the COHP/ICOHP/COOP/ICOOP. - densities (ndarray): Density of states values for the COHP/ICOHP/COOP/ICOOP. - energies_are_cartesian (bool): Whether the energies are cartesian or not. - are_coops (bool): Whether the object is a COOP/ICOOP or not. - are_cobis (bool): Whether the object is a COBIS/ICOBIS or not. - icohp (dict): A dictionary of the ICOHP/COHP values. The keys are Spin.up and Spin.down. + energies (NDArray): Energy values for the COHP/ICOHP/COOP/ICOOP. + densities (NDArray): Density of states for the COHP/ICOHP/COOP/ICOOP. + energies_are_cartesian (bool): Whether the energies are cartesian. + are_coops (bool): Whether the object is COOP/ICOOP. + are_cobis (bool): Whether the object is COBIS/ICOBIS. + icohp (dict): The ICOHP/COHP values, whose keys are Spin.up and Spin.down. summed_icohp (float): The summed ICOHP/COHP values. - num_bonds (int): The number of bonds used for the average COHP (relevant for Lobster versions <3.0). + num_bonds (int): The number of bonds used for the average COHP (for LOBSTER versions <3.0). """ def __init__( - self, label, atom1, atom2, length, translation, num, icohp, are_coops=False, are_cobis=False, orbitals=None + self, + label: str, + atom1: str, + atom2: str, + length: float, + translation: Vector3D, + num: int, + icohp: dict[Spin, float], + are_coops: bool = False, + are_cobis: bool = False, + orbitals: dict[str, dict[Literal["icohp", "orbitals"], Any]] | None = None, ) -> None: """ Args: - label: label for the icohp - atom1: str of atom that is contributing to the bond - atom2: str of second atom that is contributing to the bond - length: float of bond lengths - translation: translation list, e.g. [0,0,0] - num: integer describing how often the bond exists - icohp: dict={Spin.up: icohpvalue for spin.up, Spin.down: icohpvalue for spin.down} - are_coops: if True, this are COOPs - are_cobis: if True, this are COBIs - orbitals: {[str(Orbital1)-str(Orbital2)]: {"icohp":{Spin.up: icohpvalue for spin.up, Spin.down: - icohpvalue for spin.down}, "orbitals":[Orbital1, Orbital2]}}. + label (str): Label for the ICOHP. + atom1 (str): The first atom that contributes to the bond. + atom2 (str): The second atom that contributes to the bond. + length (float): Bond length. + translation (Vector3D): cell translation vector, e.g. (0, 0, 0). + num (int): The number of equivalent bonds. + icohp (dict[Spin, float]): {Spin.up: ICOHP_up, Spin.down: ICOHP_down} + are_coops (bool): Whether these are COOPs. + are_cobis (bool): Whether these are COBIs. + orbitals (dict): {[str(Orbital1)-str(Orbital2)]: { + "icohp": { + Spin.up: IcohpValue for spin.up, + Spin.down: IcohpValue for spin.down + }, + "orbitals": [Orbital1, Orbital2, ...]}. """ if are_coops and are_cobis: raise ValueError("You cannot have info about COOPs and COBIs in the same file.") + self._are_coops = are_coops self._are_cobis = are_cobis self._label = label @@ -898,136 +974,127 @@ def __init__( self._num = num self._icohp = icohp self._orbitals = orbitals - if Spin.down in self._icohp: - self._is_spin_polarized = True - else: - self._is_spin_polarized = False + self._is_spin_polarized = Spin.down in self._icohp def __str__(self) -> str: """String representation of the ICOHP/ICOOP.""" - if not self._are_coops and not self._are_cobis: - if self._is_spin_polarized: - return ( - f"ICOHP {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " - f"{self._icohp[Spin.up]} eV (Spin up) and {self._icohp[Spin.down]} eV (Spin down)" - ) - return ( - f"ICOHP {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " - f"{self._icohp[Spin.up]} eV (Spin up)" - ) - if self._are_coops and not self._are_cobis: - if self._is_spin_polarized: - return ( - f"ICOOP {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " - f"{self._icohp[Spin.up]} eV (Spin up) and {self._icohp[Spin.down]} eV (Spin down)" - ) - return ( - f"ICOOP {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " - f"{self._icohp[Spin.up]} eV (Spin up)" - ) - if self._are_cobis and not self._are_coops: - if self._is_spin_polarized: - return ( - f"ICOBI {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " - f"{self._icohp[Spin.up]} eV (Spin up) and {self._icohp[Spin.down]} eV (Spin down)" - ) + # (are_coops and are_cobis) is never True + if self._are_coops: + header = "ICOOP" + elif self._are_cobis: + header = "ICOBI" + else: + header = "ICOHP" + + if self._is_spin_polarized: return ( - f"ICOBI {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " - f"{self._icohp[Spin.up]} eV (Spin up)" + f"{header} {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " + f"{self._icohp[Spin.up]} eV (Spin up) and {self._icohp[Spin.down]} eV (Spin down)" ) - return "" + return ( + f"{header} {self._label} between {self._atom1} and {self._atom2} ({self._translation}): " + f"{self._icohp[Spin.up]} eV (Spin up)" + ) @property - def num_bonds(self): - """Tells the number of bonds for which the ICOHP value is an average. + def num_bonds(self) -> int: + """The number of bonds for which the ICOHP value is an average. Returns: - Int. + int """ return self._num @property def are_coops(self) -> bool: - """Tells if ICOOPs or not. + """Whether these are ICOOPs. Returns: - Boolean. + bool """ return self._are_coops @property def are_cobis(self) -> bool: - """Tells if ICOBIs or not. + """Whether these are ICOBIs. Returns: - Boolean. + bool """ return self._are_cobis @property def is_spin_polarized(self) -> bool: - """Tells if spin polarized calculation or not. + """Whether this is a spin polarized calculation. Returns: - Boolean. + bool """ return self._is_spin_polarized - def icohpvalue(self, spin=Spin.up): + def icohpvalue(self, spin: Spin = Spin.up) -> float: """ Args: spin: Spin.up or Spin.down. Returns: - float: corresponding to chosen spin. + float: ICOHP value corresponding to chosen spin. """ if not self.is_spin_polarized and spin == Spin.down: raise ValueError("The calculation was not performed with spin polarization") return self._icohp[spin] - def icohpvalue_orbital(self, orbitals, spin=Spin.up) -> float: + def icohpvalue_orbital( + self, + orbitals: tuple[Orbital, Orbital] | str, + spin: Spin = Spin.up, + ) -> float: """ Args: - orbitals: List of Orbitals or "str(Orbital1)-str(Orbital2)" - spin: Spin.up or Spin.down. + orbitals: tuple[Orbital, Orbital] or "str(Orbital0)-str(Orbital1)". + spin (Spin): Spin.up or Spin.down. Returns: - float: corresponding to chosen spin. + float: ICOHP value corresponding to chosen spin. """ if not self.is_spin_polarized and spin == Spin.down: raise ValueError("The calculation was not performed with spin polarization") - if isinstance(orbitals, list): + + if isinstance(orbitals, (tuple, list)): orbitals = f"{orbitals[0]}-{orbitals[1]}" + + assert self._orbitals is not None return self._orbitals[orbitals]["icohp"][spin] @property - def icohp(self): + def icohp(self) -> dict[Spin, float]: """Dict with ICOHPs for spin up and spin down. Returns: - dict={Spin.up: icohpvalue for spin.up, Spin.down: icohpvalue for spin.down}. + dict[Spin, float]: {Spin.up: ICOHP_up, Spin.down: ICOHP_down}. """ return self._icohp @property - def summed_icohp(self): - """Sums ICOHPs of both spin channels for spin polarized compounds. + def summed_icohp(self) -> float: + """Summed ICOHPs of both spin channels if spin polarized. Returns: - float: icohp value in eV. + float: ICOHP value in eV. """ return self._icohp[Spin.down] + self._icohp[Spin.up] if self._is_spin_polarized else self._icohp[Spin.up] @property - def summed_orbital_icohp(self): - """Sums orbital-resolved ICOHPs of both spin channels for spin-polarized compounds. + def summed_orbital_icohp(self) -> dict[str, float]: + """Summed orbital-resolved ICOHPs of both spin channels if spin-polarized. Returns: - dict[str, float]: "str(Orbital1)-str(Ortibal2)" mapped to ICOHP value in eV. + dict[str, float]: "str(Orbital1)-str(Ortibal2)": ICOHP value in eV. """ orbital_icohp = {} + assert self._orbitals is not None for orb, item in self._orbitals.items(): orbital_icohp[orb] = ( item["icohp"][Spin.up] + item["icohp"][Spin.down] if self._is_spin_polarized else item["icohp"][Spin.up] @@ -1036,49 +1103,49 @@ def summed_orbital_icohp(self): class IcohpCollection(MSONable): - """Store IcohpValues. + """Collection of IcohpValues. Attributes: - are_coops (bool): Boolean to indicate if these are ICOOPs. - are_cobis (bool): Boolean to indicate if these are ICOOPs. - is_spin_polarized (bool): Boolean to indicate if the Lobster calculation was done spin polarized or not. + are_coops (bool): Whether these are ICOOPs. + are_cobis (bool): Whether these are ICOOPs. + is_spin_polarized (bool): Whether the calculation is spin polarized. """ def __init__( self, - list_labels, - list_atom1, - list_atom2, - list_length, - list_translation, - list_num, - list_icohp, - is_spin_polarized, - list_orb_icohp=None, - are_coops=False, - are_cobis=False, + list_labels: list[str], + list_atom1: list[str], + list_atom2: list[str], + list_length: list[float], + list_translation: list[Vector3D], + list_num: list[int], + list_icohp: list[dict[Spin, float]], + is_spin_polarized: bool, + list_orb_icohp: list[dict[str, dict[Literal["icohp", "orbitals"], Any]]] | None = None, + are_coops: bool = False, + are_cobis: bool = False, ) -> None: """ Args: - list_labels: list of labels for ICOHP/ICOOP values - list_atom1: list of str of atomnames e.g. "O1" - list_atom2: list of str of atomnames e.g. "O1" - list_length: list of lengths of corresponding bonds in Angstrom - list_translation: list of translation list, e.g. [0,0,0] - list_num: list of equivalent bonds, usually 1 starting from Lobster 3.0.0 - list_icohp: list of dict={Spin.up: icohpvalue for spin.up, Spin.down: icohpvalue for spin.down} - is_spin_polarized: Boolean to indicate if the Lobster calculation was done spin polarized or not Boolean to - indicate if the Lobster calculation was done spin polarized or not - list_orb_icohp: list of dict={[str(Orbital1)-str(Orbital2)]: {"icohp":{Spin.up: icohpvalue for spin.up, - Spin.down: icohpvalue for spin.down}, "orbitals":[Orbital1, Orbital2]}} - are_coops: Boolean to indicate whether ICOOPs are stored - are_cobis: Boolean to indicate whether ICOBIs are stored. + list_labels (list[str]): Labels for ICOHP/ICOOP values. + list_atom1 (list[str]): Atom names, e.g. "O1". + list_atom2 (list[str]): Atom names, e.g. "O1". + list_length (list[float]): Bond lengths in Angstrom. + list_translation (list[Vector3D]): Cell translation vectors. + list_num (list[int]): Numbers of equivalent bonds, usually 1 starting from LOBSTER 3.0.0. + list_icohp (list[dict]): Dicts as {Spin.up: ICOHP_up, Spin.down: ICOHP_down}. + is_spin_polarized (bool): Whether the calculation is spin polarized. + list_orb_icohp (list[dict]): Dicts as {[str(Orbital1)-str(Orbital2)]: { + "icohp": {Spin.up: IcohpValue for spin.up, Spin.down: IcohpValue for spin.down}, + "orbitals": [Orbital1, Orbital2]}. + are_coops (bool): Whether ICOOPs are stored. + are_cobis (bool): Whether ICOBIs are stored. """ if are_coops and are_cobis: raise ValueError("You cannot have info about COOPs and COBIs in the same file.") + self._are_coops = are_coops self._are_cobis = are_cobis - self._icohplist = {} self._is_spin_polarized = is_spin_polarized self._list_labels = list_labels self._list_atom1 = list_atom1 @@ -1089,132 +1156,153 @@ def __init__( self._list_icohp = list_icohp self._list_orb_icohp = list_orb_icohp - for ilist, listel in enumerate(list_labels): - self._icohplist[listel] = IcohpValue( - label=listel, - atom1=list_atom1[ilist], - atom2=list_atom2[ilist], - length=list_length[ilist], - translation=list_translation[ilist], - num=list_num[ilist], - icohp=list_icohp[ilist], + # TODO: DanielYang: self._icohplist name is misleading + # (not list), and confuses with self._list_icohp + self._icohplist: dict[str, IcohpValue] = {} + for idx, label in enumerate(list_labels): + self._icohplist[label] = IcohpValue( + label=label, + atom1=list_atom1[idx], + atom2=list_atom2[idx], + length=list_length[idx], + translation=list_translation[idx], + num=list_num[idx], + icohp=list_icohp[idx], are_coops=are_coops, are_cobis=are_cobis, - orbitals=None if list_orb_icohp is None else list_orb_icohp[ilist], + orbitals=None if list_orb_icohp is None else list_orb_icohp[idx], ) def __str__(self) -> str: - lst = [] - for value in self._icohplist.values(): - lst.append(str(value)) - return "\n".join(lst) + return "\n".join([str(value) for value in self._icohplist.values()]) - def get_icohp_by_label(self, label, summed_spin_channels=True, spin=Spin.up, orbitals=None) -> float: - """Get an icohp value for a certain bond as indicated by the label (bond labels starting by "1" as in - ICOHPLIST/ICOOPLIST). + def get_icohp_by_label( + self, + label: str, + summed_spin_channels: bool = True, + spin: Spin = Spin.up, + orbitals: str | tuple[Orbital, Orbital] | None = None, + ) -> float: + """Get an ICOHP value for a certain bond indicated by the label. Args: - label: label in str format (usually the bond number in Icohplist.lobster/Icooplist.lobster - summed_spin_channels: Boolean to indicate whether the ICOHPs/ICOOPs of both spin channels should be summed - spin: if summed_spin_channels is equal to False, this spin indicates which spin channel should be returned - orbitals: List of Orbital or "str(Orbital1)-str(Orbital2)" + label (str): The bond number in Icohplist.lobster/Icooplist.lobster, + starting from "1". + summed_spin_channels (bool): Whether the ICOHPs/ICOOPs of both + spin channels should be summed. + spin (Spin): If not summed_spin_channels, indicate + which spin channel should be returned. + orbitals: List of Orbital or "str(Orbital1)-str(Orbital2)". Returns: - float: ICOHP/ICOOP value + float: ICOHP/ICOOP value. """ - icohp_here: IcohpValue = self._icohplist[label] + icohp: IcohpValue = self._icohplist[label] + if orbitals is None: - if summed_spin_channels: - return icohp_here.summed_icohp - return icohp_here.icohpvalue(spin) + return icohp.summed_icohp if summed_spin_channels else icohp.icohpvalue(spin) - if isinstance(orbitals, list): + if isinstance(orbitals, (tuple, list)): orbitals = f"{orbitals[0]}-{orbitals[1]}" + if summed_spin_channels: - return icohp_here.summed_orbital_icohp[orbitals] + return icohp.summed_orbital_icohp[orbitals] - return icohp_here.icohpvalue_orbital(spin=spin, orbitals=orbitals) + return icohp.icohpvalue_orbital(spin=spin, orbitals=orbitals) - def get_summed_icohp_by_label_list(self, label_list, divisor=1.0, summed_spin_channels=True, spin=Spin.up) -> float: - """Get the sum of several ICOHP values that are indicated by a list of labels - (labels of the bonds are the same as in ICOHPLIST/ICOOPLIST). + def get_summed_icohp_by_label_list( + self, + label_list: list[str], + divisor: float = 1.0, + summed_spin_channels: bool = True, + spin: Spin = Spin.up, + ) -> float: + """Get the sum of ICOHP values. Args: - label_list: list of labels of the ICOHPs/ICOOPs that should be summed - divisor: is used to divide the sum - summed_spin_channels: Boolean to indicate whether the ICOHPs/ICOOPs of both spin channels should be summed - spin: if summed_spin_channels is equal to False, this spin indicates which spin channel should be returned + label_list (list[str]): Labels of the ICOHPs/ICOOPs that should be summed, + the same as in ICOHPLIST/ICOOPLIST. + divisor (float): Divisor used to divide the sum. + summed_spin_channels (bool): Whether the ICOHPs/ICOOPs of both + spin channels should be summed. + spin (Spin): If not summed_spin_channels, indicate + which spin channel should be returned. Returns: - float: sum of all ICOHPs/ICOOPs as indicated with label_list + float: Sum of ICOHPs selected with label_list. """ - sum_icohp = 0 + sum_icohp: float = 0 for label in label_list: - icohp_here = self._icohplist[label] - if icohp_here.num_bonds != 1: + icohp = self._icohplist[label] + if icohp.num_bonds != 1: warnings.warn("One of the ICOHP values is an average over bonds. This is currently not considered.") - if icohp_here._is_spin_polarized: - if summed_spin_channels: - sum_icohp = sum_icohp + icohp_here.summed_icohp - else: - sum_icohp = sum_icohp + icohp_here.icohpvalue(spin) + + if icohp._is_spin_polarized and summed_spin_channels: + sum_icohp = sum_icohp + icohp.summed_icohp else: - sum_icohp = sum_icohp + icohp_here.icohpvalue(spin) + sum_icohp = sum_icohp + icohp.icohpvalue(spin) + return sum_icohp / divisor - def get_icohp_dict_by_bondlengths(self, minbondlength=0.0, maxbondlength=8.0): - """Get a dict of IcohpValues corresponding to certain bond lengths. + def get_icohp_dict_by_bondlengths( + self, + minbondlength: float = 0.0, + maxbondlength: float = 8.0, + ) -> dict[str, IcohpValue]: + """Get IcohpValues within certain bond length range. Args: - minbondlength: defines the minimum of the bond lengths of the bonds - maxbondlength: defines the maximum of the bond lengths of the bonds. + minbondlength (float): The minimum bond length. + maxbondlength (float): The maximum bond length. Returns: - dict of IcohpValues, the keys correspond to the values from the initial list_labels. + dict[str, IcohpValue]: Keys are the labels from the initial list_labels. """ new_icohp_dict = {} for value in self._icohplist.values(): - if value._length >= minbondlength and value._length <= maxbondlength: + if minbondlength <= value._length <= maxbondlength: new_icohp_dict[value._label] = value return new_icohp_dict def get_icohp_dict_of_site( self, - site, - minsummedicohp=None, - maxsummedicohp=None, - minbondlength=0.0, - maxbondlength=8.0, - only_bonds_to=None, - ): - """Get a dict of IcohpValue for a certain site (indicated by integer). + site: int, + minsummedicohp: float | None = None, + maxsummedicohp: float | None = None, + minbondlength: float = 0.0, + maxbondlength: float = 8.0, + only_bonds_to: list[str] | None = None, + ) -> dict[str, IcohpValue]: + """Get IcohpValues for a certain site. Args: - site: integer describing the site of interest, order as in Icohplist.lobster/Icooplist.lobster, starts at 0 - minsummedicohp: float, minimal icohp/icoop of the bonds that are considered. It is the summed ICOHP value - from both spin channels for spin polarized cases - maxsummedicohp: float, maximal icohp/icoop of the bonds that are considered. It is the summed ICOHP value - from both spin channels for spin polarized cases - minbondlength: float, defines the minimum of the bond lengths of the bonds - maxbondlength: float, defines the maximum of the bond lengths of the bonds - only_bonds_to: list of strings describing the bonding partners that are allowed, e.g. ['O'] + site (int): The site of interest, ordered as in Icohplist.lobster/Icooplist.lobster, + starts from 0. + minsummedicohp (float): Minimal ICOHP/ICOOP of the bonds that are considered. + It is the summed ICOHP value from both spin channels for spin polarized cases + maxsummedicohp (float): Maximal ICOHP/ICOOP of the bonds that are considered. + It is the summed ICOHP value from both spin channels for spin polarized cases + minbondlength (float): The minimum bond length. + maxbondlength (float): The maximum bond length. + only_bonds_to (list[str]): The bonding partners that are allowed, e.g. ["O"]. Returns: - dict of IcohpValues, the keys correspond to the values from the initial list_labels + Dict of IcohpValues, the keys correspond to the values from the initial list_labels. """ new_icohp_dict = {} for key, value in self._icohplist.items(): atomnumber1 = int(re.split(r"(\d+)", value._atom1)[1]) - 1 atomnumber2 = int(re.split(r"(\d+)", value._atom2)[1]) - 1 if site in (atomnumber1, atomnumber2): - # manipulate order of atoms so that searched one is always atom1 + # Swap order of atoms so that searched one is always atom1 if site == atomnumber2: save = value._atom1 value._atom1 = value._atom2 value._atom2 = save second_test = True if only_bonds_to is None else re.split("(\\d+)", value._atom2)[0] in only_bonds_to - if value._length >= minbondlength and value._length <= maxbondlength and second_test: + if minbondlength <= value._length <= maxbondlength and second_test: + # TODO: DanielYang: merge the following condition blocks if minsummedicohp is not None: if value.summed_icohp >= minsummedicohp: if maxsummedicohp is not None: @@ -1230,16 +1318,21 @@ def get_icohp_dict_of_site( return new_icohp_dict - def extremum_icohpvalue(self, summed_spin_channels=True, spin=Spin.up): - """Get ICOHP/ICOOP of strongest bond. + def extremum_icohpvalue( + self, + summed_spin_channels: bool = True, + spin: Spin = Spin.up, + ) -> float: + """Get ICOHP/ICOOP of the strongest bond. Args: - summed_spin_channels: Boolean to indicate whether the ICOHPs/ICOOPs of both spin channels should be summed. - - spin: if summed_spin_channels is equal to False, this spin indicates which spin channel should be returned + summed_spin_channels (bool): Whether the ICOHPs/ICOOPs of both + spin channels should be summed. + spin (Spin): If not summed_spin_channels, this indicates which + spin channel should be returned. Returns: - lowest ICOHP/largest ICOOP value (i.e. ICOHP/ICOOP value of strongest bond) + Lowest ICOHP/largest ICOOP value (i.e. ICOHP/ICOOP value of strongest bond). """ extremum = -sys.float_info.max if self._are_coops or self._are_cobis else sys.float_info.max @@ -1255,60 +1348,71 @@ def extremum_icohpvalue(self, summed_spin_channels=True, spin=Spin.up): extremum = value.icohpvalue(spin) elif value.icohpvalue(spin) > extremum: extremum = value.icohpvalue(spin) + elif not self._are_coops and not self._are_cobis: if value.summed_icohp < extremum: extremum = value.summed_icohp + elif value.summed_icohp > extremum: extremum = value.summed_icohp + return extremum @property def is_spin_polarized(self) -> bool: - """Whether it is spin polarized.""" + """Whether this is spin polarized.""" return self._is_spin_polarized @property def are_coops(self) -> bool: - """Whether this is a coop.""" + """Whether this is COOP.""" return self._are_coops @property def are_cobis(self) -> bool: - """Whether this a cobi.""" + """Whether this is COBI.""" return self._are_cobis def get_integrated_cohp_in_energy_range( - cohp, label, orbital=None, energy_range=None, relative_E_Fermi=True, summed_spin_channels=True -): - """Integrate CompleteCohp objects which include data on integrated COHPs + cohp: CompleteCohp, + label: str, + orbital: str | None = None, + energy_range: float | tuple[float, float] | None = None, + relative_E_Fermi: bool = True, + summed_spin_channels: bool = True, +) -> float | dict[Spin, float]: + """Integrate CompleteCohps which include data of integrated COHPs (ICOHPs). + Args: - cohp: CompleteCohp object - label: label of the COHP data - orbital: If not None, a orbital resolved integrated COHP will be returned - energy_range: If None, returns icohp value at Fermi level. - If float, integrates from this float up to the Fermi level. - If [float,float], will integrate in between. - relative_E_Fermi: if True, energy scale with E_Fermi at 0 eV is chosen - summed_spin_channels: if True, Spin channels will be summed. + cohp (CompleteCohp): CompleteCohp object. + label (str): Label of the COHP data. + orbital (str): If not None, a orbital resolved integrated COHP will be returned. + energy_range: If None, return the ICOHP value at Fermi level. + If float, integrate from this value up to Fermi level. + If (float, float), integrate in between. + relative_E_Fermi (bool): Whether energy scale with Fermi level at 0 eV is chosen. + summed_spin_channels (bool): Whether Spin channels will be summed. Returns: - float indicating the integrated COHP if summed_spin_channels==True, otherwise dict of the following form { - Spin.up:float, Spin.down:float} + If summed_spin_channels: + float: the ICOHP. + else: + dict: {Spin.up: float, Spin.down: float} """ - summedicohp = {} if orbital is None: icohps = cohp.all_cohps[label].get_icohp(spin=None) - if summed_spin_channels and Spin.down in icohps: - summedicohp[Spin.up] = icohps[Spin.up] + icohps[Spin.down] - else: - summedicohp = icohps else: - icohps = cohp.get_orbital_resolved_cohp(label=label, orbitals=orbital).icohp - if summed_spin_channels and Spin.down in icohps: - summedicohp[Spin.up] = icohps[Spin.up] + icohps[Spin.down] - else: - summedicohp = icohps + _icohps = cohp.get_orbital_resolved_cohp(label=label, orbitals=orbital) + assert _icohps is not None + icohps = _icohps.icohp + + assert icohps is not None + summedicohp = {} + if summed_spin_channels and Spin.down in icohps: + summedicohp[Spin.up] = icohps[Spin.up] + icohps[Spin.down] + else: + summedicohp = icohps if energy_range is None: energies_corrected = cohp.energies - cohp.efermi @@ -1317,12 +1421,10 @@ def get_integrated_cohp_in_energy_range( if not summed_spin_channels and Spin.down in icohps: spl_spindown = InterpolatedUnivariateSpline(energies_corrected, summedicohp[Spin.down], ext=0) return {Spin.up: spl_spinup(0.0), Spin.down: spl_spindown(0.0)} - if summed_spin_channels: - return spl_spinup(0.0) - return {Spin.up: spl_spinup(0.0)} + return spl_spinup(0.0) if summed_spin_channels else {Spin.up: spl_spinup(0.0)} - # returns icohp value at the Fermi level! + # Return ICOHP value at the Fermi level if isinstance(energy_range, float): if relative_E_Fermi: energies_corrected = cohp.energies - cohp.efermi From 8923d1417fb7f6a126c533b8e41b436ed5a45aff Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Sat, 3 Aug 2024 21:46:45 +0800 Subject: [PATCH 054/180] Improve types for `electronic_structure.{bandstructure/cohp}` (#3873) * add types for bandstructure * relocate magic methods to top * add some types * fix type errors in bandstructure * temp save * first run of cohp, mypy errors to fix * fix collection generation * add type `SpinLike` and case tweaks * reduce repetition for `__str__` of `IcohpValue` * simplify condition * reduce indentation level * clarify `translation` * clarify `list_num` and other docstrings * clarify `label` as str * more type and docstring improvements * fix unit test * fix most mypy errors * fix remaining mypy errors * add DEBUG tag * reduce code repetition * Need Confirm: set `translation` as tuple * pre-commit auto-fixes * more type clarify * clarify `num` argument * clarify docstring of `bandstructure` * more minor tweaks * clarify type of labels_dict * replace unnecessary single-item list extend with append * fix typo * relocate magic method * clarify type of `list_icohp` * remove unused type alias * revert undesired rename * replace more single item extend with append * simplify dict generation * fix downstream lobsterpy error * tweak module docstring * need confirm: allow efermi to be None * pre-commit auto-fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- tests/io/lobster/test_inputs.py | 48 ++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 646a2f70eb..6675eaf70c 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -444,77 +444,77 @@ def test_values(self): "length": 2.88231, "number_of_bonds": 3, "icohp": {Spin.up: -2.18042}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "2": { "length": 3.10144, "number_of_bonds": 3, "icohp": {Spin.up: -1.14347}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "3": { "length": 2.88231, "number_of_bonds": 3, "icohp": {Spin.up: -2.18042}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "4": { "length": 3.10144, "number_of_bonds": 3, "icohp": {Spin.up: -1.14348}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "5": { "length": 3.05001, "number_of_bonds": 3, "icohp": {Spin.up: -1.30006}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "6": { "length": 2.91676, "number_of_bonds": 3, "icohp": {Spin.up: -1.96843}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "7": { "length": 3.05001, "number_of_bonds": 3, "icohp": {Spin.up: -1.30006}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "8": { "length": 2.91676, "number_of_bonds": 3, "icohp": {Spin.up: -1.96843}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "9": { "length": 3.37522, "number_of_bonds": 3, "icohp": {Spin.up: -0.47531}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "10": { "length": 3.07294, "number_of_bonds": 3, "icohp": {Spin.up: -2.38796}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "11": { "length": 3.37522, "number_of_bonds": 3, "icohp": {Spin.up: -0.47531}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, } @@ -523,77 +523,77 @@ def test_values(self): "length": 2.88231, "number_of_bonds": 3, "icohp": {Spin.up: 0.14245}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "2": { "length": 3.10144, "number_of_bonds": 3, "icohp": {Spin.up: -0.04118}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "3": { "length": 2.88231, "number_of_bonds": 3, "icohp": {Spin.up: 0.14245}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "4": { "length": 3.10144, "number_of_bonds": 3, "icohp": {Spin.up: -0.04118}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "5": { "length": 3.05001, "number_of_bonds": 3, "icohp": {Spin.up: -0.03516}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "6": { "length": 2.91676, "number_of_bonds": 3, "icohp": {Spin.up: 0.10745}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "7": { "length": 3.05001, "number_of_bonds": 3, "icohp": {Spin.up: -0.03516}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "8": { "length": 2.91676, "number_of_bonds": 3, "icohp": {Spin.up: 0.10745}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "9": { "length": 3.37522, "number_of_bonds": 3, "icohp": {Spin.up: -0.12395}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "10": { "length": 3.07294, "number_of_bonds": 3, "icohp": {Spin.up: 0.24714}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "11": { "length": 3.37522, "number_of_bonds": 3, "icohp": {Spin.up: -0.12395}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, } @@ -602,14 +602,14 @@ def test_values(self): "length": 2.83189, "number_of_bonds": 2, "icohp": {Spin.up: -0.10218, Spin.down: -0.19701}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, "2": { "length": 2.45249, "number_of_bonds": 1, "icohp": {Spin.up: -0.28485, Spin.down: -0.58279}, - "translation": [0, 0, 0], + "translation": (0, 0, 0), "orbitals": None, }, } From 4dc68e7ac7cd043eda8679547abfbf5f66b89251 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sat, 3 Aug 2024 09:59:30 -0400 Subject: [PATCH 055/180] standardize doc strings --- outputs.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/outputs.py b/outputs.py index 31ebe16b04..2a59f6a321 100644 --- a/outputs.py +++ b/outputs.py @@ -65,7 +65,7 @@ class Cohpcar: efermi (float): The Fermi energy in eV. energies (Sequence[float]): Sequence of energies in eV. Note that LOBSTER shifts the energies so that the Fermi energy is at zero. - is_spin_polarized (bool): Boolean to indicate if the calculation is spin polarized. + is_spin_polarized (bool): True if the calculation is spin polarized. orb_cohp (dict[str, Dict[str, Dict[str, Any]]]): A dictionary containing the orbital-resolved COHPs of the form: orb_cohp[label] = {bond_data["orb_label"]: { "COHP": {Spin.up: cohps, Spin.down:cohps}, @@ -329,8 +329,8 @@ class Icohplist(MSONable): """Read ICOHPLIST/ICOOPLIST files generated by LOBSTER. Attributes: - are_coops (bool): Indicates whether the object is consisting of COOPs. - is_spin_polarized (bool): Boolean to indicate if the calculation is spin polarized. + are_coops (bool): Indicates whether the object consists of COOPs. + is_spin_polarized (bool): True if the calculation is spin polarized. Icohplist (dict[str, Dict[str, Union[float, int, Dict[Spin, float]]]]): Dict containing the listfile data of the form: { bond: "length": bond length, @@ -527,7 +527,7 @@ class NciCobiList: """Read NcICOBILIST (multi-center ICOBI) files generated by LOBSTER. Attributes: - is_spin_polarized (bool): Boolean to indicate if the calculation is spin polarized. + is_spin_polarized (bool): True if the calculation is spin polarized. NciCobiList (dict): Dict containing the listfile data of the form: {bond: "number_of_atoms": number of atoms involved in the multi-center interaction, "ncicobi": {Spin.up: Nc-ICOBI(Ef) spin up, Spin.down: ...}}, @@ -646,7 +646,7 @@ class Doscar: the Spin.up contribution at each of the energies. itdensities[Spin.down]: numpy array of the total density of states for the Spin.down contribution at each of the energies. If is_spin_polarized=False, itdensities[Spin.up]: numpy array of the total density of states. - is_spin_polarized (bool): Boolean. Tells if the system is spin polarized. + is_spin_polarized (bool): Whether the system is spin polarized. """ def __init__( @@ -878,8 +878,8 @@ class Lobsterout(MSONable): has_grosspopulation (bool): Whether GROSSPOP.lobster is present. info_lines (str): String with additional infos on the run. info_orthonormalization (str): String with infos on orthonormalization. - is_restart_from_projection (bool): Boolean that indicates that calculation was restarted - from existing projection file. + is_restart_from_projection (bool): Whether calculation was restarted from existing + projection file. lobster_version (str): String that indicates Lobster version. number_of_spins (int): Integer indicating the number of spins. number_of_threads (int): Integer that indicates how many threads were used. @@ -1190,7 +1190,7 @@ class Fatband: The first index of the array refers to the band and the second to the index of the kpoint. The kpoints are ordered according to the order of the kpoints_array attribute. If the band structure is not spin polarized, we only store one data set under Spin.up. - is_spin_polarized (bool): Boolean that tells you whether this was a spin-polarized calculation. + is_spin_polarized (bool): Whether this was a spin-polarized calculation. kpoints_array (list[np.ndarray]): List of kpoints as numpy arrays, in frac_coords of the given lattice by default. label_dict (dict[str, Union[str, np.ndarray]]): Dictionary that links a kpoint (in frac coords or Cartesian @@ -1501,14 +1501,13 @@ def _read(self, contents: list, spin_numbers: list): self.band_overlaps_dict[spin]["matrices"] += [np.matrix(overlaps)] def has_good_quality_maxDeviation(self, limit_maxDeviation: float = 0.1) -> bool: - """ - Will check if the maxDeviation from the ideal bandoverlap is smaller or equal to limit_maxDeviation + """Will check if the maxDeviation from the ideal bandoverlap is smaller or equal to limit_maxDeviation Args: limit_maxDeviation: limit of the maxDeviation Returns: - Boolean that will give you information about the quality of the projection. + bool: Whether the quality of the projection is good. """ return all(deviation <= limit_maxDeviation for deviation in self.max_deviation) @@ -1530,7 +1529,7 @@ def has_good_quality_check_occupied_bands( limit_deviation (float): limit of the maxDeviation Returns: - Boolean that will give you information about the quality of the projection + bool: Whether the quality of the projection is good. """ for matrix in self.band_overlaps_dict[Spin.up]["matrices"]: for iband1, band1 in enumerate(matrix): From e8248e1c08972f5ebd28181e8afe00c365ffc681 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sat, 3 Aug 2024 09:59:30 -0400 Subject: [PATCH 056/180] standardize doc strings --- cohp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cohp.py b/cohp.py index acf4f8f45b..be757cbce9 100644 --- a/cohp.py +++ b/cohp.py @@ -234,8 +234,8 @@ class CompleteCohp(Cohp): """A wrapper that defines an average COHP, and individual COHPs. Attributes: - are_coops (bool): Whether the object is consisting of COOPs. - are_cobis (bool): Whether the object is consisting of COBIs. + are_coops (bool): Whether the object consists of COOPs. + are_cobis (bool): Whether the object consists of COBIs. efermi (float): The Fermi level. energies (Sequence[float]): Sequence of energies. structure (Structure): Structure associated with the COHPs. From 63a626a142f243291caf46b17413df597e6d3ad8 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sat, 3 Aug 2024 13:27:41 -0400 Subject: [PATCH 057/180] refactor dict.update() to use |= operator --- outputs.py | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/outputs.py b/outputs.py index 2a59f6a321..322fcbf261 100644 --- a/outputs.py +++ b/outputs.py @@ -166,18 +166,16 @@ def __init__( } elif label in orb_cohp: - orb_cohp[label].update( - { - bond_data["orb_label"]: { - "COHP": cohp, - "ICOHP": icohp, - "orbitals": orbs, - "length": bond_data["length"], - "sites": bond_data["sites"], - "cells": bond_data["cells"], - } + orb_cohp[label] |= { + bond_data["orb_label"]: { + "COHP": cohp, + "ICOHP": icohp, + "orbitals": orbs, + "length": bond_data["length"], + "sites": bond_data["sites"], + "cells": bond_data["cells"], } - ) + } else: # present for Lobster versions older than Lobster 2.2.0 if bond_num == 0: @@ -219,17 +217,15 @@ def __init__( } elif label in orb_cohp: - orb_cohp[label].update( - { - bond_data["orb_label"]: { - "COHP": cohp, - "ICOHP": icohp, - "orbitals": orbs, - "length": bond_data["length"], - "sites": bond_data["sites"], - } + orb_cohp[label] |= { + bond_data["orb_label"]: { + "COHP": cohp, + "ICOHP": icohp, + "orbitals": orbs, + "length": bond_data["length"], + "sites": bond_data["sites"], } - ) + } else: # present for Lobster versions older than Lobster 2.2.0 if bond_num == 0: @@ -2136,10 +2132,10 @@ def _parse_matrix(file_data, pattern, e_fermi): if matches and len(matches.groups()) == 2: spin = Spin.up if matches[1] == "1" else Spin.down k_point = matches[2] - complex_matrices[k_point].update({spin: comp_matrix}) + complex_matrices[k_point] |= {spin: comp_matrix} elif matches and len(matches.groups()) == 1: k_point = matches[1] - complex_matrices.update({k_point: comp_matrix}) + complex_matrices |= {k_point: comp_matrix} matrix_diagonal_values.append(comp_matrix.real.diagonal() - e_fermi) # extract elements basis functions as list From 3b60be0bfd19d585dab27010719a4e46b95a4f6d Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sun, 4 Aug 2024 07:24:35 -0400 Subject: [PATCH 058/180] fix double spaces in doc strings found with regex \b\s{2,}\b[^\d] --- outputs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/outputs.py b/outputs.py index 322fcbf261..e671fa03fe 100644 --- a/outputs.py +++ b/outputs.py @@ -1503,7 +1503,7 @@ def has_good_quality_maxDeviation(self, limit_maxDeviation: float = 0.1) -> bool limit_maxDeviation: limit of the maxDeviation Returns: - bool: Whether the quality of the projection is good. + bool: True if the quality of the projection is good. """ return all(deviation <= limit_maxDeviation for deviation in self.max_deviation) @@ -1525,7 +1525,7 @@ def has_good_quality_check_occupied_bands( limit_deviation (float): limit of the maxDeviation Returns: - bool: Whether the quality of the projection is good. + bool: True if the quality of the projection is good. """ for matrix in self.band_overlaps_dict[Spin.up]["matrices"]: for iband1, band1 in enumerate(matrix): From 0be5f86b3d7648171db93741ca4520a470ccc736 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Fri, 9 Aug 2024 21:20:12 +0800 Subject: [PATCH 059/180] Fix mypy errors for `io.cp2k` (#3984) * ignore mypy override error globally * standardize CP2K names * pre-commit auto-fixes * standarize more CP2K names * old trick: relocate magic methods to the top * format tweaks * fix type errors in cp2k inputs * remove reimport Sequence * use list[Kpoint] as getter return type * remove unnecessary cast --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- inputs.py | 2 +- lobsterenv.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inputs.py b/inputs.py index 9eef7c654a..bd80f07a92 100644 --- a/inputs.py +++ b/inputs.py @@ -184,7 +184,7 @@ def __getitem__(self, key: str) -> Any: except KeyError as exc: raise KeyError(f"{key=} is not available") from exc - def __contains__(self, key: str) -> bool: # type: ignore[override] + def __contains__(self, key: str) -> bool: """To avoid cases sensitivity problems.""" return super().__contains__(key.lower().strip()) diff --git a/lobsterenv.py b/lobsterenv.py index 919ea6bb67..c36694f02f 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -253,7 +253,7 @@ def anion_types(self) -> set[Element]: def get_anion_types(self): return self.anion_types - def get_nn_info(self, structure: Structure, n: int, use_weights: bool = False) -> dict: # type: ignore[override] + def get_nn_info(self, structure: Structure, n: int, use_weights: bool = False) -> dict: """Get coordination number, CN, of site with index n in structure. Args: From ee904ff48a6aa1cb3e45f1a10ea5bf8acca12b96 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel)" Date: Wed, 14 Aug 2024 23:41:34 +0800 Subject: [PATCH 060/180] Docstring tweaks for `io.vasp.inputs` and format tweaks for some other parts (#3996) * docstring tweaks * fix typo * reduce indentation level * fix vasp case to VASP * use walrus operator * revert overlapping functional changes from #3994 * my bad, I get confused hopping between two PRs * remove debug code from vasp.help * fix `use-named-expression` with sourcery * clean up Vasprun.as_dict * simplify dict generation * re-raise and update -> |= * simplify logic conditions * remove unused logger * remove a lot of unused logger, wondering if they exist for a reason? * remove some unused module_dir, they must have gone stranded * CAPS LOCK ENGAGED: Go up! module level variables! --- tests/io/lobster/test_inputs.py | 2 -- tests/io/lobster/test_lobsterenv.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 6675eaf70c..c5c2a98988 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -41,8 +41,6 @@ __email__ = "janine.george@uclouvain.be, esters@uoregon.edu" __date__ = "Dec 10, 2017" -module_dir = os.path.dirname(os.path.abspath(__file__)) - class TestCohpcar(PymatgenTest): def setUp(self): diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 6ee94d8290..63b5809d05 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os from unittest import TestCase import numpy as np @@ -23,7 +22,6 @@ __date__ = "Jan 14, 2021" TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp/environments" -module_dir = os.path.dirname(os.path.abspath(__file__)) class TestLobsterNeighbors(TestCase): From 454feb32543922627220a65e6b5fbdc84abd8fb3 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Thu, 15 Aug 2024 10:21:10 -0700 Subject: [PATCH 061/180] Ruff fixes. --- inputs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/inputs.py b/inputs.py index bd80f07a92..21d51854f4 100644 --- a/inputs.py +++ b/inputs.py @@ -249,7 +249,8 @@ def write_lobsterin( overwritedict (dict): dict that can be used to update lobsterin, e.g. {"skipdos": True} """ # Update previous entries - self |= {} if overwritedict is None else overwritedict + if overwritedict is not None: + self.update(overwritedict) with open(path, mode="w", encoding="utf-8") as file: for key in self: From 054df4e631e7fa469ae3dd14b4c696d2b05982c6 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Thu, 15 Aug 2024 10:21:10 -0700 Subject: [PATCH 062/180] Ruff fixes. --- cohp.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cohp.py b/cohp.py index be757cbce9..f7b980832d 100644 --- a/cohp.py +++ b/cohp.py @@ -1344,14 +1344,12 @@ def extremum_icohpvalue( for value in self._icohplist.values(): if not value.is_spin_polarized or not summed_spin_channels: if not self._are_coops and not self._are_cobis: - if value.icohpvalue(spin) < extremum: - extremum = value.icohpvalue(spin) + extremum = min(value.icohpvalue(spin), extremum) elif value.icohpvalue(spin) > extremum: extremum = value.icohpvalue(spin) elif not self._are_coops and not self._are_cobis: - if value.summed_icohp < extremum: - extremum = value.summed_icohp + extremum = min(value.summed_icohp, extremum) elif value.summed_icohp > extremum: extremum = value.summed_icohp From 2c8dee94ba2d92b69b9b5263ad390dbf70ff8667 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) Yang" Date: Thu, 22 Aug 2024 02:55:07 +0800 Subject: [PATCH 063/180] Drop Python 3.9 support (#4009) * drop python 3.9 and add python 3.13 * remove yanked matplotlib pin * remove clarified TODO tag * bump python version in CI * try to explicitly declare python 313 in mamba * use 3.13 pre-release rc1 * remove Python 3.13 * remove TODO for Python 2 * remove docstring TODO for unit test * enable this seemingly passing test * tweak and fix typo in get_dos_fp_similarity ValueError msg * ruff auto-fixes * manual fix: type unions use pipe op * replace union with | operator * add TypeAlias to honor type-alias-without-annotation (PYI026) * replace pariwise iteration using zip with itertools * fix error message * not sure why it failed, try to separate available index * sure I forgot about double quote and single quote * use | in INCAR tag check from #3958 * fix RUF017 * Revert "fix RUF017" as I haven't got time to verify This reverts commit b6dbf209153a7a76521b66b9a2f75e942cf63af1. * fix DeprecationWarning: dict interface (SpglibDataset['international']) is deprecated.Use attribute interface ({self.__class__.__name__}.{key}) instead --------- Co-authored-by: Janosh Riebesell --- lobsterenv.py | 6 +++--- outputs.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index c36694f02f..65916a7e9b 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -243,7 +243,7 @@ def anion_types(self) -> set[Element]: raise ValueError("No cations and anions defined") anion_species = [] - for site, val in zip(self.structure, self.valences): + for site, val in zip(self.structure, self.valences, strict=False): if val < 0.0: anion_species.append(site.specie) @@ -413,7 +413,7 @@ def get_info_icohps_to_neighbors(self, isites=None, onlycation_isites=True): final_isites = [] for ival, _site in enumerate(self.structure): if ival in isites: - for keys, icohpsum in zip(self.list_keys[ival], self.list_icohps[ival]): + for keys, icohpsum in zip(self.list_keys[ival], self.list_icohps[ival], strict=False): summed_icohps += icohpsum list_icohps.append(icohpsum) labels.append(keys) @@ -559,7 +559,7 @@ def get_info_cohps_to_neighbors( # iterate through labels and atoms and check which bonds can be included new_labels = [] new_atoms = [] - for key, atompair, isite in zip(labels, atoms, final_isites): + for key, atompair, isite in zip(labels, atoms, final_isites, strict=False): present = False for atomtype in only_bonds_to: # This is necessary to identify also bonds between the same elements correctly! diff --git a/outputs.py b/outputs.py index e671fa03fe..26e74ea67d 100644 --- a/outputs.py +++ b/outputs.py @@ -2115,7 +2115,7 @@ def _parse_matrix(file_data, pattern, e_fermi): matrix_real = [] matrix_imag = [] for start_inx_real, end_inx_real, start_inx_imag, end_inx_imag in zip( - start_inxs_real, end_inxs_real, start_inxs_imag, end_inxs_imag + start_inxs_real, end_inxs_real, start_inxs_imag, end_inxs_imag, strict=False ): # matrix with text headers matrix_real = file_data[start_inx_real:end_inx_real] @@ -2147,6 +2147,8 @@ def _parse_matrix(file_data, pattern, e_fermi): average_matrix_diagonal_values = np.array(matrix_diagonal_values, dtype=float).mean(axis=0) # get a dict with basis functions as keys and average values as values - average_average_matrix_diag_dict = dict(zip(elements_basis_functions, average_matrix_diagonal_values)) + average_average_matrix_diag_dict = dict( + zip(elements_basis_functions, average_matrix_diagonal_values, strict=False) + ) return matrix_diagonal_values, average_average_matrix_diag_dict, complex_matrices From ab618034fe45d7307b150fb72aa287a57c91a6f5 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) Yang" Date: Thu, 22 Aug 2024 02:55:07 +0800 Subject: [PATCH 064/180] Drop Python 3.9 support (#4009) * drop python 3.9 and add python 3.13 * remove yanked matplotlib pin * remove clarified TODO tag * bump python version in CI * try to explicitly declare python 313 in mamba * use 3.13 pre-release rc1 * remove Python 3.13 * remove TODO for Python 2 * remove docstring TODO for unit test * enable this seemingly passing test * tweak and fix typo in get_dos_fp_similarity ValueError msg * ruff auto-fixes * manual fix: type unions use pipe op * replace union with | operator * add TypeAlias to honor type-alias-without-annotation (PYI026) * replace pariwise iteration using zip with itertools * fix error message * not sure why it failed, try to separate available index * sure I forgot about double quote and single quote * use | in INCAR tag check from #3958 * fix RUF017 * Revert "fix RUF017" as I haven't got time to verify This reverts commit b6dbf209153a7a76521b66b9a2f75e942cf63af1. * fix DeprecationWarning: dict interface (SpglibDataset['international']) is deprecated.Use attribute interface ({self.__class__.__name__}.{key}) instead --------- Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index c5c2a98988..1f1ae7bd65 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -257,13 +257,13 @@ def test_cohp_data(self): assert len(val["cells"]) == 3 assert len(val["COHP"][Spin.up]) == 12 assert len(val["COHP"][Spin.down]) == 12 - for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down]): + for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down], strict=False): assert cohp1 == approx(cohp2, abs=1e-4) else: assert len(val["cells"]) == 2 assert len(val["COHP"][Spin.up]) == 12 assert len(val["COHP"][Spin.down]) == 12 - for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down]): + for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down], strict=False): assert cohp1 == approx(cohp2, abs=1e-3) def test_orbital_resolved_cohp(self): @@ -1299,7 +1299,7 @@ def test_get_doc(self): assert ref_data[key] == item elif key in ("charge_spilling", "total_spilling"): assert item[0] == approx(ref_data[key][0]) - elif isinstance(item, (list, dict)): + elif isinstance(item, list | dict): assert item == ref_data[key] def test_msonable(self): From f1cff7e07c86ee4c6d4ca228905db259dbde6279 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) Yang" Date: Thu, 22 Aug 2024 02:55:07 +0800 Subject: [PATCH 065/180] Drop Python 3.9 support (#4009) * drop python 3.9 and add python 3.13 * remove yanked matplotlib pin * remove clarified TODO tag * bump python version in CI * try to explicitly declare python 313 in mamba * use 3.13 pre-release rc1 * remove Python 3.13 * remove TODO for Python 2 * remove docstring TODO for unit test * enable this seemingly passing test * tweak and fix typo in get_dos_fp_similarity ValueError msg * ruff auto-fixes * manual fix: type unions use pipe op * replace union with | operator * add TypeAlias to honor type-alias-without-annotation (PYI026) * replace pariwise iteration using zip with itertools * fix error message * not sure why it failed, try to separate available index * sure I forgot about double quote and single quote * use | in INCAR tag check from #3958 * fix RUF017 * Revert "fix RUF017" as I haven't got time to verify This reverts commit b6dbf209153a7a76521b66b9a2f75e942cf63af1. * fix DeprecationWarning: dict interface (SpglibDataset['international']) is deprecated.Use attribute interface ({self.__class__.__name__}.{key}) instead --------- Co-authored-by: Janosh Riebesell --- cohp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cohp.py b/cohp.py index f7b980832d..a0e80b1982 100644 --- a/cohp.py +++ b/cohp.py @@ -552,7 +552,7 @@ def get_orbital_resolved_cohp( if self.orb_res_cohp is None: return None - if isinstance(orbitals, (list, tuple)): + if isinstance(orbitals, list | tuple): cohp_orbs = [val["orbitals"] for val in self.orb_res_cohp[label].values()] orbs = [] for orbital in orbitals: @@ -1062,7 +1062,7 @@ def icohpvalue_orbital( if not self.is_spin_polarized and spin == Spin.down: raise ValueError("The calculation was not performed with spin polarization") - if isinstance(orbitals, (tuple, list)): + if isinstance(orbitals, tuple | list): orbitals = f"{orbitals[0]}-{orbitals[1]}" assert self._orbitals is not None @@ -1202,7 +1202,7 @@ def get_icohp_by_label( if orbitals is None: return icohp.summed_icohp if summed_spin_channels else icohp.icohpvalue(spin) - if isinstance(orbitals, (tuple, list)): + if isinstance(orbitals, tuple | list): orbitals = f"{orbitals[0]}-{orbitals[1]}" if summed_spin_channels: From 293b430183c279e5e11162e912d46b8bf1ded34e Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) Yang" Date: Thu, 22 Aug 2024 17:19:08 +0800 Subject: [PATCH 066/180] Use `strict=True` with `zip` to ensure length equality (#4011) * global replacement of strict=True, very prone to error and need careful check * revert changes on non-zip functions * revert to strict=F for failing tests * revert a missing one * remove a call-overload ignore tag * fix another unit test * fix typos --------- Co-authored-by: Janosh Riebesell --- lobsterenv.py | 6 +++--- outputs.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 65916a7e9b..dcc91b2800 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -243,7 +243,7 @@ def anion_types(self) -> set[Element]: raise ValueError("No cations and anions defined") anion_species = [] - for site, val in zip(self.structure, self.valences, strict=False): + for site, val in zip(self.structure, self.valences, strict=True): if val < 0.0: anion_species.append(site.specie) @@ -413,7 +413,7 @@ def get_info_icohps_to_neighbors(self, isites=None, onlycation_isites=True): final_isites = [] for ival, _site in enumerate(self.structure): if ival in isites: - for keys, icohpsum in zip(self.list_keys[ival], self.list_icohps[ival], strict=False): + for keys, icohpsum in zip(self.list_keys[ival], self.list_icohps[ival], strict=True): summed_icohps += icohpsum list_icohps.append(icohpsum) labels.append(keys) @@ -559,7 +559,7 @@ def get_info_cohps_to_neighbors( # iterate through labels and atoms and check which bonds can be included new_labels = [] new_atoms = [] - for key, atompair, isite in zip(labels, atoms, final_isites, strict=False): + for key, atompair, isite in zip(labels, atoms, final_isites, strict=True): present = False for atomtype in only_bonds_to: # This is necessary to identify also bonds between the same elements correctly! diff --git a/outputs.py b/outputs.py index 26e74ea67d..5f6fea7e7e 100644 --- a/outputs.py +++ b/outputs.py @@ -2115,7 +2115,7 @@ def _parse_matrix(file_data, pattern, e_fermi): matrix_real = [] matrix_imag = [] for start_inx_real, end_inx_real, start_inx_imag, end_inx_imag in zip( - start_inxs_real, end_inxs_real, start_inxs_imag, end_inxs_imag, strict=False + start_inxs_real, end_inxs_real, start_inxs_imag, end_inxs_imag, strict=True ): # matrix with text headers matrix_real = file_data[start_inx_real:end_inx_real] @@ -2148,7 +2148,7 @@ def _parse_matrix(file_data, pattern, e_fermi): # get a dict with basis functions as keys and average values as values average_average_matrix_diag_dict = dict( - zip(elements_basis_functions, average_matrix_diagonal_values, strict=False) + zip(elements_basis_functions, average_matrix_diagonal_values, strict=True) ) return matrix_diagonal_values, average_average_matrix_diag_dict, complex_matrices From 561c5738fa0589ec7ad102a42d3a7e2a3b23125f Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) Yang" Date: Thu, 22 Aug 2024 17:19:08 +0800 Subject: [PATCH 067/180] Use `strict=True` with `zip` to ensure length equality (#4011) * global replacement of strict=True, very prone to error and need careful check * revert changes on non-zip functions * revert to strict=F for failing tests * revert a missing one * remove a call-overload ignore tag * fix another unit test * fix typos --------- Co-authored-by: Janosh Riebesell --- tests/io/lobster/test_inputs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 1f1ae7bd65..302618879c 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -257,13 +257,13 @@ def test_cohp_data(self): assert len(val["cells"]) == 3 assert len(val["COHP"][Spin.up]) == 12 assert len(val["COHP"][Spin.down]) == 12 - for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down], strict=False): + for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down], strict=True): assert cohp1 == approx(cohp2, abs=1e-4) else: assert len(val["cells"]) == 2 assert len(val["COHP"][Spin.up]) == 12 assert len(val["COHP"][Spin.down]) == 12 - for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down], strict=False): + for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down], strict=True): assert cohp1 == approx(cohp2, abs=1e-3) def test_orbital_resolved_cohp(self): From 366e88042307e29845c849f8080436ffa5ed45bc Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Thu, 22 Aug 2024 12:31:15 +0200 Subject: [PATCH 068/180] Fix ruff PD901 and prefer `sum` over `len`+`if` (#4012) * simplify: prefer sum over len + if https://github.com/astral-sh/ruff/issues/13050 * fix ruff PD901 pandas-df-variable-name * fix cast df_energies as float in cp2k parse_energy_file --- cohp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cohp.py b/cohp.py index a0e80b1982..687c474db3 100644 --- a/cohp.py +++ b/cohp.py @@ -193,7 +193,7 @@ def has_antibnd_states_below_efermi( limit (float): Only COHP higher than this value will be considered. """ populations = self.cohp - n_energies_below_efermi = len([energy for energy in self.energies if energy <= self.efermi]) + n_energies_below_efermi = sum(energy <= self.efermi for energy in self.energies) if populations is None: return None From ebcab67c0c3927076233525fdf2d98bb7c409956 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) Yang" Date: Fri, 30 Aug 2024 13:59:36 +0800 Subject: [PATCH 069/180] Improve type annotations for `io.lobster.{lobsterenv/outputs}` (#3887) * temp save of cleaning lobsterenv * fix type errors in lobsterenv * temp save of lobster.outputs * revert chemenv additional_condition rename * first go of lobster.outputs * fix typo in get_doc * separate lobster inputs and outputs tests * fix unit test * more var name and comment cleanups * fix my errors (some unit tests fail) * fix some unit test * fix unit test * Need Confirm: change `np.float64` to float for spilling * clarify data form in docstring * use deprecated decorator * pre-commit auto-fixes * pre-commit auto-fixes * remove DEBUG tag and var name tweaks * add more specific types * use PeriodicSite over Site * standardize idx var name * limit matplotlib version * remove matplotlib pin * remove left out matplotlib pin * remove ignore override tag * pre-commit auto-fixes * Apply suggestions from code review Signed-off-by: J. George * use specific type for get_nn_info return * add TODO for a very likely TODO comment * remove implemented TODO tag * replace Literal with | in docstring * use atom{idx}_list over atom{idx}s * revert bool description --------- Signed-off-by: J. George Co-authored-by: J. George --- __init__.py | 2 +- inputs.py | 4 +- lobsterenv.py | 905 ++++++++++++++++++----------------- outputs.py | 1250 ++++++++++++++++++++++++++----------------------- 4 files changed, 1140 insertions(+), 1021 deletions(-) diff --git a/__init__.py b/__init__.py index e4b1100a2d..9dbbcffa9e 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,6 @@ """ This package implements modules for input and output to and from LOBSTER. It -imports the key classes form both lobster.inputs and lobster_outputs to allow most +imports the key classes form both lobster.inputs and lobster.outputs to allow most classes to be simply called as pymatgen.io.lobster.Lobsterin for example, to retain backwards compatibility. """ diff --git a/inputs.py b/inputs.py index 21d51854f4..e75da24e52 100644 --- a/inputs.py +++ b/inputs.py @@ -323,7 +323,7 @@ def write_INCAR( incar_input (PathLike): path to input INCAR incar_output (PathLike): path to output INCAR poscar_input (PathLike): path to input POSCAR - isym (Literal[-1, 0]): ISYM value. + isym (-1 | 0): ISYM value. further_settings (dict): A dict can be used to include further settings, e.g. {"ISMEAR":-5} """ # Read INCAR from file, which will be modified @@ -457,7 +457,7 @@ def write_KPOINTS( POSCAR_input (PathLike): path to POSCAR KPOINTS_output (PathLike): path to output KPOINTS reciprocal_density (int): Grid density - isym (Literal[-1, 0]): ISYM value. + isym (-1 | 0): ISYM value. from_grid (bool): If True KPOINTS will be generated with the help of a grid given in input_grid. Otherwise, they will be generated from the reciprocal_density input_grid (tuple): grid to generate the KPOINTS file diff --git a/lobsterenv.py b/lobsterenv.py index dcc91b2800..03bc7957ee 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -1,8 +1,7 @@ -""" -This module provides classes to perform analyses of -the local environments (e.g., finding near neighbors) -of single sites in molecules and structures based on -bonding analysis with Lobster. +"""This module provides classes to perform analyses of the local +environments (e.g., finding near neighbors) of single sites in molecules +and structures based on bonding analysis with LOBSTER. + If you use this module, please cite: J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, "Automated Bonding Analysis with Crystal Orbital Hamilton Populations", @@ -18,6 +17,7 @@ import tempfile from typing import TYPE_CHECKING, NamedTuple +import matplotlib as mpl import numpy as np from monty.dev import deprecated @@ -32,10 +32,16 @@ from pymatgen.util.due import Doi, due if TYPE_CHECKING: + from typing import Any, Literal + + import matplotlib as mpl + from numpy.typing import NDArray from typing_extensions import Self - from pymatgen.core import Structure + from pymatgen.core import PeriodicNeighbor, PeriodicSite, Structure from pymatgen.core.periodic_table import Element + from pymatgen.electronic_structure.cohp import IcohpCollection, IcohpValue + from pymatgen.util.typing import PathLike __author__ = "Janine George" __copyright__ = "Copyright 2021, The Materials Project" @@ -53,71 +59,72 @@ class LobsterNeighbors(NearNeighbors): """ - This class combines capabilities from LocalEnv and ChemEnv to determine coordination environments based on - bonding analysis. + This class combines capabilities from LocalEnv and ChemEnv to determine + coordination environments based on bonding analysis. """ def __init__( self, structure: Structure, - filename_icohp: str | None = "ICOHPLIST.lobster", + filename_icohp: PathLike | None = "ICOHPLIST.lobster", obj_icohp: Icohplist | None = None, are_coops: bool = False, are_cobis: bool = False, valences: list[float] | None = None, limits: tuple[float, float] | None = None, - additional_condition: int = 0, + additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, only_bonds_to: list[str] | None = None, perc_strength_icohp: float = 0.15, noise_cutoff: float = 0.1, valences_from_charges: bool = False, - filename_charge: str | None = None, + filename_charge: PathLike | None = None, obj_charge: Charge | None = None, - which_charge: str = "Mulliken", + which_charge: Literal["Mulliken", "Loewdin"] = "Mulliken", adapt_extremum_to_add_cond: bool = False, add_additional_data_sg: bool = False, - filename_blist_sg1: str | None = None, - filename_blist_sg2: str | None = None, - id_blist_sg1: str = "ICOOP", - id_blist_sg2: str = "ICOBI", + filename_blist_sg1: PathLike | None = None, + filename_blist_sg2: PathLike | None = None, + id_blist_sg1: Literal["icoop", "icobi"] = "icoop", + id_blist_sg2: Literal["icoop", "icobi"] = "icobi", ) -> None: """ Args: - filename_icohp: (str) Path to ICOHPLIST.lobster or ICOOPLIST.lobster or ICOBILIST.lobster - obj_icohp: Icohplist object - structure: (Structure) typically constructed by Structure.from_file("POSCAR") - are_coops: (bool) if True, the file is a ICOOPLIST.lobster and not a ICOHPLIST.lobster; only tested for - ICOHPLIST.lobster so far - are_cobis: (bool) if True, the file is a ICOBILIST.lobster and not a ICOHPLIST.lobster - valences: (list[float]): gives valence/charge for each element - limits (tuple[float, float] | None): limit to decide which ICOHPs (ICOOP or ICOBI) should be considered - additional_condition (int): Additional condition that decides which kind of bonds will be considered - NO_ADDITIONAL_CONDITION = 0 - ONLY_ANION_CATION_BONDS = 1 - NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2 - ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3 - ONLY_ELEMENT_TO_OXYGEN_BONDS = 4 - DO_NOT_CONSIDER_ANION_CATION_BONDS=5 - ONLY_CATION_CATION_BONDS=6 - only_bonds_to: (list[str]) will only consider bonds to certain elements (e.g. ["O"] for oxygen) - perc_strength_icohp: if no limits are given, this will decide which icohps will still be considered ( - relative to - the strongest ICOHP (ICOOP or ICOBI) - noise_cutoff: if provided hardcodes the lower limit of icohps considered - valences_from_charges: if True and path to CHARGE.lobster is provided, will use Lobster charges ( - Mulliken) instead of valences - filename_charge: (str) Path to Charge.lobster - obj_charge: Charge object - which_charge: (str) "Mulliken" or "Loewdin" - adapt_extremum_to_add_cond: (bool) will adapt the limits to only focus on the bonds determined by the - additional condition - add_additional_data_sg: (bool) will add the information from filename_add_bondinglist_sg1, - filename_blist_sg1: (str) Path to additional ICOOP, ICOBI data for structure graphs - filename_blist_sg2: (str) Path to additional ICOOP, ICOBI data for structure graphs - id_blist_sg1: (str) Identity of data in filename_blist_sg1, - e.g. "icoop" or "icobi" - id_blist_sg2: (str) Identity of data in filename_blist_sg2, - e.g. "icoop" or "icobi". + filename_icohp (PathLike): Path to ICOHPLIST.lobster or + ICOOPLIST.lobster or ICOBILIST.lobster. + obj_icohp (Icohplist): Icohplist object. + structure (Structure): Typically constructed by Structure.from_file("POSCAR"). + are_coops (bool): Whether the file is a ICOOPLIST.lobster (True) or a + ICOHPLIST.lobster (False). Only tested for ICOHPLIST.lobster so far. + are_cobis (bool): Whether the file is a ICOBILIST.lobster (True) or + a ICOHPLIST.lobster (False). + valences (list[float]): Valence/charge for each element. + limits (tuple[float, float]): Range to decide which ICOHPs (ICOOP + or ICOBI) should be considered. + additional_condition (int): Additional condition that decides + which kind of bonds will be considered: + 0 - NO_ADDITIONAL_CONDITION + 1 - ONLY_ANION_CATION_BONDS + 2 - NO_ELEMENT_TO_SAME_ELEMENT_BONDS + 3 - ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS + 4 - ONLY_ELEMENT_TO_OXYGEN_BONDS + 5 - DO_NOT_CONSIDER_ANION_CATION_BONDS + 6 - ONLY_CATION_CATION_BONDS + only_bonds_to (list[str]): Only consider bonds to certain elements (e.g. ["O"] for oxygen). + perc_strength_icohp (float): If no "limits" are given, this will decide + which ICOHPs will be considered (relative to the strongest ICOHP/ICOOP/ICOBI). + noise_cutoff (float): The lower limit of ICOHPs considered. + valences_from_charges (bool): If True and path to CHARGE.lobster is provided, + will use LOBSTER charges (Mulliken) instead of valences. + filename_charge (PathLike): Path to Charge.lobster. + obj_charge (Charge): Charge object. + which_charge ("Mulliken" | "Loewdin"): Source of charge. + adapt_extremum_to_add_cond (bool): Whether to adapt the limits to only + focus on the bonds determined by the additional condition. + add_additional_data_sg (bool): Add the information from filename_add_bondinglist_sg1. + filename_blist_sg1 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. + filename_blist_sg2 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. + id_blist_sg1 ("icoop" | "icobi"): Identity of data in filename_blist_sg1. + id_blist_sg2 ("icoop" | "icobi"): Identity of data in filename_blist_sg2. """ if filename_icohp is not None: self.ICOHP = Icohplist(are_coops=are_coops, are_cobis=are_cobis, filename=filename_icohp) @@ -125,6 +132,7 @@ def __init__( self.ICOHP = obj_icohp else: raise ValueError("Please provide either filename_icohp or obj_icohp") + self.Icohpcollection = self.ICOHP.icohpcollection self.structure = structure self.limits = limits @@ -137,34 +145,33 @@ def __init__( self.filename_blist_sg2 = filename_blist_sg2 self.noise_cutoff = noise_cutoff - allowed_arguments = ["icoop", "icobi"] - if id_blist_sg1.lower() not in allowed_arguments or id_blist_sg2.lower() not in allowed_arguments: + self.id_blist_sg1 = id_blist_sg1.lower() + self.id_blist_sg2 = id_blist_sg2.lower() + + allowed_arguments = {"icoop", "icobi"} + if self.id_blist_sg1 not in allowed_arguments or self.id_blist_sg2 not in allowed_arguments: raise ValueError("Algorithm can only work with ICOOPs, ICOBIs") - self.id_blist_sg1 = id_blist_sg1 - self.id_blist_sg2 = id_blist_sg2 + if add_additional_data_sg: - if self.id_blist_sg1.lower() == "icoop": + if self.id_blist_sg1 == "icoop": are_coops_id1 = True are_cobis_id1 = False - elif self.id_blist_sg1.lower() == "icobi": + else: are_coops_id1 = False are_cobis_id1 = True - else: - raise ValueError("only icoops and icobis can be added") + self.bonding_list_1 = Icohplist( filename=self.filename_blist_sg1, are_coops=are_coops_id1, are_cobis=are_cobis_id1, ) - if self.id_blist_sg2.lower() == "icoop": + if self.id_blist_sg2 == "icoop": are_coops_id2 = True are_cobis_id2 = False - elif self.id_blist_sg2.lower() == "icobi": + else: are_coops_id2 = False are_cobis_id2 = True - else: - raise ValueError("only icoops and icobis can be added") self.bonding_list_2 = Icohplist( filename=self.filename_blist_sg2, @@ -172,12 +179,12 @@ def __init__( are_cobis=are_cobis_id2, ) - # will check if the additional condition is correctly delivered + # Check the additional condition if additional_condition not in range(7): raise ValueError(f"Unexpected {additional_condition=}, must be one of {list(range(7))}") self.additional_condition = additional_condition - # will read in valences, will prefer manual setting of valences + # Read in valences, will prefer manual setting of valences self.valences: list[float] | None if valences is None: if valences_from_charges and filename_charge is not None: @@ -186,25 +193,28 @@ def __init__( self.valences = chg.Mulliken elif which_charge == "Loewdin": self.valences = chg.Loewdin + elif valences_from_charges and obj_charge is not None: chg = obj_charge if which_charge == "Mulliken": self.valences = chg.Mulliken elif which_charge == "Loewdin": self.valences = chg.Loewdin + else: bv_analyzer = BVAnalyzer() try: self.valences = bv_analyzer.get_valences(structure=self.structure) - except ValueError: + except ValueError as exc: self.valences = None - if additional_condition in [1, 3, 5, 6]: + if additional_condition in {1, 3, 5, 6}: raise ValueError( "Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work" - ) + ) from exc else: self.valences = valences - if np.allclose(self.valences or [], np.zeros_like(self.valences)) and additional_condition in [1, 3, 5, 6]: + + if np.allclose(self.valences or [], np.zeros_like(self.valences)) and additional_condition in {1, 3, 5, 6}: raise ValueError("All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work") if limits is None: @@ -212,7 +222,7 @@ def __init__( else: self.lowerlimit, self.upperlimit = limits - # will evaluate coordination environments + # Evaluate coordination environments self._evaluate_ce( lowerlimit=self.lowerlimit, upperlimit=self.upperlimit, @@ -223,21 +233,21 @@ def __init__( ) @property - def structures_allowed(self) -> bool: - """Whether this NearNeighbors class can be used with Structure objects?""" + def structures_allowed(self) -> Literal[True]: + """Whether this LobsterNeighbors class can be used with Structure objects.""" return True @property - def molecules_allowed(self) -> bool: - """Whether this NearNeighbors class can be used with Molecule objects?""" + def molecules_allowed(self) -> Literal[False]: + """Whether this LobsterNeighbors class can be used with Molecule objects.""" return False @property def anion_types(self) -> set[Element]: - """The types of anions present in crystal structure as a set. + """The set of anion types in crystal structure. Returns: - set[Element]: describing anions in the crystal structure. + set[Element]: Anions in the crystal structure. """ if self.valences is None: raise ValueError("No cations and anions defined") @@ -250,43 +260,52 @@ def anion_types(self) -> set[Element]: return set(anion_species) @deprecated(anion_types) - def get_anion_types(self): + def get_anion_types(self) -> set[Element]: return self.anion_types - def get_nn_info(self, structure: Structure, n: int, use_weights: bool = False) -> dict: - """Get coordination number, CN, of site with index n in structure. + def get_nn_info( + self, + structure: Structure, + n: int, + use_weights: bool = False, + ) -> dict[str, Any]: + """Get coordination number (CN) of site by index. Args: - structure (Structure): input structure. - n (int): index of site for which to determine CN. - use_weights (bool): flag indicating whether (True) - to use weights for computing the coordination number - or not (False, default: each coordinated site has equal - weight). - True is not implemented for LobsterNeighbors + structure (Structure): Input structure. + n (int): Index of site for which to determine CN. + use_weights (bool): Whether to use weights for computing + the CN (True), or each coordinated site has equal weight (False). + The former is not implemented yet. Raises: - ValueError: if use_weights is True or if structure passed and structure used to - initialize LobsterNeighbors have different lengths. + ValueError: If use_weights is True, or if arg "structure" and structure + used to initialize LobsterNeighbors have different lengths. Returns: dict[str, Any]: coordination number and a list of nearest neighbors. """ if use_weights: raise ValueError("LobsterEnv cannot use weights") + if len(structure) != len(self.structure): raise ValueError( f"Length of structure ({len(structure)}) and LobsterNeighbors ({len(self.structure)}) differ" ) + return self.sg_list[n] # type: ignore[return-value] - def get_light_structure_environment(self, only_cation_environments=False, only_indices=None): - """Get a LobsterLightStructureEnvironments object - if the structure only contains coordination environments smaller 13. + def get_light_structure_environment( + self, + only_cation_environments: bool = False, + only_indices: list[int] | None = None, + ) -> LobsterLightStructureEnvironments: + """Get a LobsterLightStructureEnvironments object if the structure + only contains coordination environments smaller 13. Args: - only_cation_environments: only data for cations will be returned - only_indices: will only evaluate the list of isites in this list + only_cation_environments (bool): Only return data for cations. + only_indices (list[int]): Only evaluate indexes in this list. Returns: LobsterLightStructureEnvironments @@ -296,12 +315,12 @@ def get_light_structure_environment(self, only_cation_environments=False, only_i list_ce_symbols = [] list_csm = [] list_permut = [] - for ival, _neigh_coords in enumerate(self.list_coords): + for idx, _neigh_coords in enumerate(self.list_coords): if (len(_neigh_coords)) > 13: raise ValueError("Environment cannot be determined. Number of neighbors is larger than 13.") - # to avoid problems if _neigh_coords is empty + # Avoid problems if _neigh_coords is empty if _neigh_coords != []: - lgf.setup_local_geometry(isite=ival, coords=_neigh_coords, optimization=2) + lgf.setup_local_geometry(isite=idx, coords=_neigh_coords, optimization=2) cncgsm = lgf.get_coordination_symmetry_measures(optimization=2) list_ce_symbols.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[0]) list_csm.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["csm_wcs_ctwcc"]) @@ -311,9 +330,15 @@ def get_light_structure_environment(self, only_cation_environments=False, only_i list_csm.append(None) list_permut.append(None) + new_list_ce_symbols = [] + new_list_csm = [] + new_list_permut = [] + new_list_neighsite = [] + new_list_neighisite = [] + if only_indices is None: if not only_cation_environments: - lse = LobsterLightStructureEnvironments.from_Lobster( + return LobsterLightStructureEnvironments.from_Lobster( list_ce_symbol=list_ce_symbols, list_csm=list_csm, list_permutation=list_permut, @@ -322,50 +347,30 @@ def get_light_structure_environment(self, only_cation_environments=False, only_i structure=self.structure, valences=self.valences, ) - else: - new_list_ce_symbols = [] - new_list_csm = [] - new_list_permut = [] - new_list_neighsite = [] - new_list_neighisite = [] - - for ival, val in enumerate(self.valences): - if val >= 0.0: - new_list_ce_symbols.append(list_ce_symbols[ival]) - new_list_csm.append(list_csm[ival]) - new_list_permut.append(list_permut[ival]) - new_list_neighisite.append(self.list_neighisite[ival]) - new_list_neighsite.append(self.list_neighsite[ival]) - else: - new_list_ce_symbols.append(None) - new_list_csm.append(None) - new_list_permut.append([]) - new_list_neighisite.append([]) - new_list_neighsite.append([]) - - lse = LobsterLightStructureEnvironments.from_Lobster( - list_ce_symbol=new_list_ce_symbols, - list_csm=new_list_csm, - list_permutation=new_list_permut, - list_neighsite=new_list_neighsite, - list_neighisite=new_list_neighisite, - structure=self.structure, - valences=self.valences, - ) + + assert self.valences is not None + for idx, val in enumerate(self.valences): + if val >= 0.0: + new_list_ce_symbols.append(list_ce_symbols[idx]) + new_list_csm.append(list_csm[idx]) + new_list_permut.append(list_permut[idx]) + new_list_neighisite.append(self.list_neighisite[idx]) + new_list_neighsite.append(self.list_neighsite[idx]) + else: + new_list_ce_symbols.append(None) + new_list_csm.append(None) + new_list_permut.append([]) + new_list_neighisite.append([]) + new_list_neighsite.append([]) + else: - new_list_ce_symbols = [] - new_list_csm = [] - new_list_permut = [] - new_list_neighsite = [] - new_list_neighisite = [] - - for isite, _site in enumerate(self.structure): - if isite in only_indices: - new_list_ce_symbols.append(list_ce_symbols[isite]) - new_list_csm.append(list_csm[isite]) - new_list_permut.append(list_permut[isite]) - new_list_neighisite.append(self.list_neighisite[isite]) - new_list_neighsite.append(self.list_neighsite[isite]) + for site_idx, _site in enumerate(self.structure): + if site_idx in only_indices: + new_list_ce_symbols.append(list_ce_symbols[site_idx]) + new_list_csm.append(list_csm[site_idx]) + new_list_permut.append(list_permut[site_idx]) + new_list_neighisite.append(self.list_neighisite[site_idx]) + new_list_neighsite.append(self.list_neighsite[site_idx]) else: new_list_ce_symbols.append(None) new_list_csm.append(None) @@ -373,26 +378,33 @@ def get_light_structure_environment(self, only_cation_environments=False, only_i new_list_neighisite.append([]) new_list_neighsite.append([]) - lse = LobsterLightStructureEnvironments.from_Lobster( - list_ce_symbol=new_list_ce_symbols, - list_csm=new_list_csm, - list_permutation=new_list_permut, - list_neighsite=new_list_neighsite, - list_neighisite=new_list_neighisite, - structure=self.structure, - valences=self.valences, - ) + return LobsterLightStructureEnvironments.from_Lobster( + list_ce_symbol=new_list_ce_symbols, + list_csm=new_list_csm, + list_permutation=new_list_permut, + list_neighsite=new_list_neighsite, + list_neighisite=new_list_neighisite, + structure=self.structure, + valences=self.valences, + ) - return lse + def get_info_icohps_to_neighbors( + self, + isites: list[int] | None = None, + onlycation_isites: bool = True, + ) -> ICOHPNeighborsInfo: + """Get information on the ICOHPs of neighbors for certain sites + as identified by their site id. + + This is useful for plotting the COHPs (ICOOPLIST.lobster/ + ICOHPLIST.lobster/ICOBILIST.lobster) of a site in the structure. - def get_info_icohps_to_neighbors(self, isites=None, onlycation_isites=True): - """Get information on the icohps of neighbors for certain sites as identified by their site id. - This is useful for plotting the relevant cohps of a site in the structure. - (could be ICOOPLIST.lobster or ICOHPLIST.lobster or ICOBILIST.lobster). Args: - isites: list of site ids. If isite==None, all isites will be used to add the icohps of the neighbors - onlycation_isites: if True and if isite==None, it will only analyse the sites of the cations + isites (list[int]): Site IDs. If is None, all isites will be used + to add the ICOHPs of the neighbors. + onlycation_isites (bool): If True and if isite is None, will + only analyse the cations sites. Returns: ICOHPNeighborsInfo @@ -401,68 +413,66 @@ def get_info_icohps_to_neighbors(self, isites=None, onlycation_isites=True): raise ValueError("No valences are provided") if isites is None: if onlycation_isites: - isites = [i for i in range(len(self.structure)) if self.valences[i] >= 0.0] + assert self.valences is not None + isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] else: isites = list(range(len(self.structure))) - summed_icohps = 0.0 - list_icohps = [] - number_bonds = 0 - labels = [] - atoms = [] - final_isites = [] - for ival, _site in enumerate(self.structure): - if ival in isites: - for keys, icohpsum in zip(self.list_keys[ival], self.list_icohps[ival], strict=True): + summed_icohps: float = 0.0 + list_icohps: list[float] = [] + number_bonds: int = 0 + labels: list[str] = [] + atoms: list[list[str]] = [] + final_isites: list[int] = [] + assert self.Icohpcollection is not None + for idx, _site in enumerate(self.structure): + if idx in isites: + for key, icohpsum in zip(self.list_keys[idx], self.list_icohps[idx], strict=True): summed_icohps += icohpsum list_icohps.append(icohpsum) - labels.append(keys) + labels.append(key) atoms.append( [ - self.Icohpcollection._list_atom1[int(keys) - 1], - self.Icohpcollection._list_atom2[int(keys) - 1], + self.Icohpcollection._list_atom1[int(key) - 1], + self.Icohpcollection._list_atom2[int(key) - 1], ] ) number_bonds += 1 - final_isites.append(ival) + final_isites.append(idx) return ICOHPNeighborsInfo(summed_icohps, list_icohps, number_bonds, labels, atoms, final_isites) def plot_cohps_of_neighbors( self, - path_to_cohpcar: str | None = "COHPCAR.lobster", + path_to_cohpcar: PathLike | None = "COHPCAR.lobster", obj_cohpcar: CompleteCohp | None = None, isites: list[int] | None = None, onlycation_isites: bool = True, only_bonds_to: list[str] | None = None, per_bond: bool = False, summed_spin_channels: bool = False, - xlim=None, - ylim=(-10, 6), + xlim: tuple[float, float] | None = None, + ylim: tuple[float, float] = (-10, 6), integrated: bool = False, - ): - """ - Will plot summed cohps or cobis or coops - (please be careful in the spin polarized case (plots might overlap (exactly!)). + ) -> mpl.axes.Axes: + """Plot summed COHPs or COBIs or COOPs. + + Please be careful in the spin polarized case (plots might overlap). Args: - path_to_cohpcar: str, path to COHPCAR or COOPCAR or COBICAR - obj_cohpcar: CompleteCohp object - isites: list of site ids, if isite==[], all isites will be used to add the icohps of the neighbors - onlycation_isites: bool, will only use cations, if isite==[] - only_bonds_to: list of str, only anions in this list will be considered - per_bond: bool, will lead to a normalization of the plotted COHP per number of bond if True, - otherwise the sum - will be plotted - xlim: list of float, limits of x values - ylim: list of float, limits of y values - integrated: bool, if true will show integrated cohp instead of cohp + path_to_cohpcar (PathLike): Path to COHPCAR or COOPCAR or COBICAR. + obj_cohpcar (CompleteCohp): CompleteCohp object + isites (list[int]): Site IDs. If empty, all sites will be used to add the ICOHPs of the neighbors. + onlycation_isites (bool): Only use cations, if isite is empty. + only_bonds_to (list[str]): Only anions in this list will be considered. + per_bond (bool): Whether to plot a normalization of the plotted COHP + per number of bond (True), or the sum (False). + xlim (tuple[float, float]): Limits of x values. + ylim (tuple[float, float]): Limits of y values. + integrated (bool): Whether to show integrated COHP instead of COHP. Returns: - plt of the cohps or coops or cobis + plt of the COHPs or COBIs or COOPs. """ - # include COHPPlotter and plot a sum of these COHPs - # might include option to add Spin channels - # implement only_bonds_to cp = CohpPlotter(are_cobis=self.are_cobis, are_coops=self.are_coops) plotlabel, summed_cohp = self.get_info_cohps_to_neighbors( @@ -487,31 +497,32 @@ def plot_cohps_of_neighbors( def get_info_cohps_to_neighbors( self, - path_to_cohpcar: str | None = "COHPCAR.lobster", + path_to_cohpcar: PathLike | None = "COHPCAR.lobster", obj_cohpcar: CompleteCohp | None = None, isites: list[int] | None = None, only_bonds_to: list[str] | None = None, onlycation_isites: bool = True, per_bond: bool = True, summed_spin_channels: bool = False, - ): - """Get info about the cohps (coops or cobis) as a summed cohp object and a label - from all sites mentioned in isites with neighbors. + ) -> tuple[str | None, CompleteCohp | None]: + """Get the COHPs (COOPs or COBIs) as a summed Cohp object + and a label from all sites mentioned in isites with neighbors. Args: - path_to_cohpcar: str, path to COHPCAR or COOPCAR or COBICAR - obj_cohpcar: CompleteCohp object - isites: list of int that indicate the number of the site - only_bonds_to: list of str, e.g. ["O"] to only show cohps of anything to oxygen - onlycation_isites: if isites=None, only cation sites will be returned - per_bond: will normalize per bond - summed_spin_channels: will sum all spin channels + path_to_cohpcar (PathLike): Path to COHPCAR/COOPCAR/COBICAR. + obj_cohpcar (CompleteCohp): CompleteCohp object. + isites (list[int]): The indexes of the sites. + only_bonds_to (list[str]): Only show COHPs to selected element, e.g. ["O"]. + onlycation_isites (bool): If isites is None, only cation sites will be returned. + per_bond (bool): Whether to normalize per bond. + summed_spin_channels (bool): Whether to sum both spin channels. Returns: - str: label for COHP, CompleteCohp object which describes all cohps (coops or cobis) - of the sites as given by isites and the other parameters + str: Label for COHP. + CompleteCohp: Describe all COHPs/COOPs/COBIs of the sites + as given by isites and the other arguments. """ - # TODO: add options for orbital-resolved cohps + # TODO: add options for orbital-resolved COHPs _summed_icohps, _list_icohps, _number_bonds, labels, atoms, final_isites = self.get_info_icohps_to_neighbors( isites=isites, onlycation_isites=onlycation_isites ) @@ -535,16 +546,18 @@ def get_info_cohps_to_neighbors( else: raise ValueError("Please provide either path_to_cohpcar or obj_cohpcar") - # will check that the number of bonds in ICOHPLIST and COHPCAR are identical - # further checks could be implemented + # Check that the number of bonds in ICOHPLIST and COHPCAR are identical + # TODO: Further checks could be implemented + assert self.Icohpcollection is not None if len(self.Icohpcollection._list_atom1) != len(self.completecohp.bonds): raise ValueError("COHPCAR and ICOHPLIST do not fit together") + is_spin_completecohp = Spin.down in self.completecohp.get_cohp_by_label("1").cohp if self.Icohpcollection.is_spin_polarized != is_spin_completecohp: raise ValueError("COHPCAR and ICOHPLIST do not fit together") if only_bonds_to is None: - # sort by anion type + # Sort by anion type divisor = len(labels) if per_bond else 1 plot_label = self._get_plot_label(atoms, per_bond) @@ -555,19 +568,20 @@ def get_info_cohps_to_neighbors( ) else: - # labels of the COHPs that will be summed! - # iterate through labels and atoms and check which bonds can be included + # Labels of the COHPs that will be summed + # Iterate through labels and atoms and check which bonds can be included new_labels = [] new_atoms = [] + assert final_isites is not None for key, atompair, isite in zip(labels, atoms, final_isites, strict=True): present = False for atomtype in only_bonds_to: - # This is necessary to identify also bonds between the same elements correctly! + # This is necessary to identify also bonds between the same elements correctly if str(self.structure[isite].species.elements[0]) != atomtype: - if atomtype in ( + if atomtype in { self._split_string(atompair[0])[0], self._split_string(atompair[1])[0], - ): + }: present = True elif ( atomtype == self._split_string(atompair[0])[0] @@ -594,29 +608,32 @@ def get_info_cohps_to_neighbors( return plot_label, summed_cohp - def _get_plot_label(self, atoms, per_bond): - # count the types of bonds and append a label: + def _get_plot_label(self, atoms: list[list[str]], per_bond: bool) -> str: + """Count the types of bonds and append a label.""" all_labels = [] for atoms_names in atoms: new = [self._split_string(atoms_names[0])[0], self._split_string(atoms_names[1])[0]] new.sort() string_here = f"{new[0]}-{new[1]}" all_labels.append(string_here) - count = collections.Counter(all_labels) - plotlabels = [] - for key, item in count.items(): - plotlabels.append(f"{item} x {key}") + + counter = collections.Counter(all_labels) + plotlabels = [f"{item} x {key}" for key, item in counter.items()] label = ", ".join(plotlabels) if per_bond: label += " (per bond)" return label - def get_info_icohps_between_neighbors(self, isites=None, onlycation_isites=True): - """Get infos about interactions between neighbors of a certain atom. + def get_info_icohps_between_neighbors( + self, + isites: list[int] | None = None, + onlycation_isites: bool = True, + ) -> ICOHPNeighborsInfo: + """Get interactions between neighbors of certain sites. Args: - isites: list of site ids, if isite==None, all isites will be used - onlycation_isites: will only use cations, if isite==None + isites (list[int]): Site IDs. If is None, all sites will be used. + onlycation_isites (bool): Only use cations, if isite is None. Returns: ICOHPNeighborsInfo @@ -626,21 +643,24 @@ def get_info_icohps_between_neighbors(self, isites=None, onlycation_isites=True) if self.valences is None and onlycation_isites: raise ValueError("No valences are provided") + if isites is None: if onlycation_isites: - isites = [i for i in range(len(self.structure)) if self.valences[i] >= 0.0] + assert self.valences is not None + isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] else: isites = list(range(len(self.structure))) - summed_icohps = 0.0 - list_icohps = [] - number_bonds = 0 - labels = [] - atoms = [] + summed_icohps: float = 0.0 + list_icohps: list[float] = [] + number_bonds: int = 0 + labels: list[str] = [] + atoms: list[list[str]] = [] + assert self.Icohpcollection is not None for isite in isites: - for in_site, n_site in enumerate(self.list_neighsite[isite]): - for in_site2, n_site2 in enumerate(self.list_neighsite[isite]): - if in_site < in_site2: + for site_idx, n_site in enumerate(self.list_neighsite[isite]): + for site2_idx, n_site2 in enumerate(self.list_neighsite[isite]): + if site_idx < site2_idx: unitcell1 = self._determine_unit_cell(n_site) unitcell2 = self._determine_unit_cell(n_site2) @@ -656,7 +676,7 @@ def get_info_icohps_between_neighbors(self, isites=None, onlycation_isites=True) icohps = self._get_icohps( icohpcollection=self.Icohpcollection, - isite=index_n_site, + site_idx=index_n_site, lowerlimit=lowerlimit, upperlimit=upperlimit, only_bonds_to=self.only_bonds_to, @@ -708,39 +728,43 @@ def get_info_icohps_between_neighbors(self, isites=None, onlycation_isites=True) def _evaluate_ce( self, - lowerlimit, - upperlimit, - only_bonds_to=None, - additional_condition: int = 0, + lowerlimit: float | None, + upperlimit: float | None, + only_bonds_to: list[str] | None = None, + additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, perc_strength_icohp: float = 0.15, adapt_extremum_to_add_cond: bool = False, ) -> None: """ Args: - lowerlimit: lower limit which determines the ICOHPs that are considered for the determination of the - neighbors - upperlimit: upper limit which determines the ICOHPs that are considered for the determination of the - neighbors - only_bonds_to: restricts the types of bonds that will be considered - additional_condition: Additional condition for the evaluation - perc_strength_icohp: will be used to determine how strong the ICOHPs (percentage*strongest ICOHP) will be - that are still considered for the evaluation - adapt_extremum_to_add_cond: will recalculate the limit based on the bonding type and not on the overall - extremum. + lowerlimit (float): Lower limit which determines the ICOHPs + that are considered for the determination of the neighbors. + upperlimit (float): Upper limit which determines the ICOHPs + that are considered for the determination of the neighbors. + only_bonds_to (list[str]): Restrict the types of bonds that will be considered. + additional_condition (int): Additional condition for the evaluation. + perc_strength_icohp (float): Determine how strong the ICOHPs + (percentage * strongest_ICOHP) will be that are still considered. + adapt_extremum_to_add_cond (bool): Whether to recalculate the limit + based on the bonding type and not on the overall extremum. """ - # get extremum + # Get extremum if lowerlimit is None and upperlimit is None: - lowerlimit, upperlimit = self._get_limit_from_extremum( + assert self.Icohpcollection is not None + limits = self._get_limit_from_extremum( self.Icohpcollection, percentage=perc_strength_icohp, adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, additional_condition=additional_condition, ) + assert limits is not None + lowerlimit, upperlimit = limits + elif upperlimit is None or lowerlimit is None: raise ValueError("Please give two limits or leave them both at None") - # find environments based on ICOHP values + # Find environments based on ICOHP values list_icohps, list_keys, list_lengths, list_neighisite, list_neighsite, list_coords = self._find_environments( additional_condition, lowerlimit, upperlimit, only_bonds_to ) @@ -752,16 +776,20 @@ def _evaluate_ce( self.list_neighisite = list_neighisite self.list_coords = list_coords - # make a structure graph - # make sure everything is relative to the given Structure and not just the atoms in the unit cell + # Make a structure graph + # Make sure everything is relative to the given Structure and + # not just the atoms in the unit cell if self.add_additional_data_sg: + assert self.bonding_list_1.icohpcollection is not None + assert self.bonding_list_2.icohpcollection is not None + self.sg_list = [ [ { "site": neighbor, "image": tuple( - int(round(i)) - for i in ( + int(round(idx)) + for idx in ( neighbor.frac_coords - self.structure[ next( @@ -775,7 +803,7 @@ def _evaluate_ce( "weight": 1, # Here, the ICOBIs and ICOOPs are added based on the bond # strength cutoff of the ICOHP - # more changes are necessary here if we use icobis for cutoffs + # More changes are necessary here if we use ICOBIs for cutoffs "edge_properties": { "ICOHP": self.list_icohps[ineighbors][ineighbor], "bond_length": self.list_lengths[ineighbors][ineighbor], @@ -801,13 +829,13 @@ def _evaluate_ce( { "site": neighbor, "image": tuple( - int(round(i)) - for i in ( + int(round(idx)) + for idx in ( neighbor.frac_coords - self.structure[ next( - isite - for isite, site in enumerate(self.structure) + site_idx + for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) ) ].frac_coords @@ -820,7 +848,7 @@ def _evaluate_ce( "bond_label": self.list_keys[ineighbors][ineighbor], }, "site_index": next( - isite for isite, site in enumerate(self.structure) if neighbor.is_periodic_image(site) + site_idx for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) ), } for ineighbor, neighbor in enumerate(neighbors) @@ -828,30 +856,44 @@ def _evaluate_ce( for ineighbors, neighbors in enumerate(self.list_neighsite) ] - def _find_environments(self, additional_condition, lowerlimit, upperlimit, only_bonds_to): - """ - Will find all relevant neighbors based on certain restrictions. + def _find_environments( + self, + additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], + lowerlimit: float, + upperlimit: float, + only_bonds_to: list[str] | None, + ) -> tuple[ + list[list[IcohpValue]], + list[list[str]], + list[list[float]], + list[list[int]], + list[list[PeriodicNeighbor]], + list[list[NDArray]], + ]: + """Find all relevant neighbors based on certain restrictions. Args: - additional_condition (int): additional condition (see above) - lowerlimit (float): lower limit that tells you which ICOHPs are considered - upperlimit (float): upper limit that tells you which ICOHPs are considered - only_bonds_to (list): list of str, e.g. ["O"] that will ensure that only bonds to "O" will be considered + additional_condition (int): Additional condition. + lowerlimit (float): Lower limit that ICOHPs are considered. + upperlimit (float): Upper limit that ICOHPs are considered. + only_bonds_to (list[str]): Only bonds to these elements will be considered. Returns: - tuple: list of icohps, list of keys, list of lengths, list of neighisite, list of neighsite, list of coords + Tuple of ICOHPs, keys, lengths, neighisite, neighsite, coords. """ - # run over structure - list_neighsite = [] - list_neighisite = [] - list_coords = [] - list_icohps = [] - list_lengths = [] - list_keys = [] + list_icohps: list[list[IcohpValue]] = [] + list_keys: list[list[str]] = [] + list_lengths: list[list[float]] = [] + list_neighisite: list[list[int]] = [] + list_neighsite: list[list[PeriodicNeighbor]] = [] + list_coords: list[list[NDArray]] = [] + + # Run over structure + assert self.Icohpcollection is not None for idx, site in enumerate(self.structure): icohps = self._get_icohps( icohpcollection=self.Icohpcollection, - isite=idx, + site_idx=idx, lowerlimit=lowerlimit, upperlimit=upperlimit, only_bonds_to=only_bonds_to, @@ -902,20 +944,20 @@ def _find_environments(self, additional_condition, lowerlimit, upperlimit, only_ _neigh_coords = [] _neigh_frac_coords = [] - for ineigh, neigh in enumerate(neighbors_by_distance): - index_here2 = index_here_list[ineigh] + for neigh_idx, neigh in enumerate(neighbors_by_distance): + index_here2 = index_here_list[neigh_idx] - for idist, dist in enumerate(copied_distances_from_ICOHPs): + for dist_idx, dist in enumerate(copied_distances_from_ICOHPs): if ( - np.isclose(dist, list_distances[ineigh], rtol=1e-4) - and copied_neighbors_from_ICOHPs[idist] == index_here2 + np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) + and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 ): _list_neighsite.append(neigh) _list_neighisite.append(index_here2) - _neigh_coords.append(coords[ineigh]) + _neigh_coords.append(coords[neigh_idx]) _neigh_frac_coords.append(neigh.frac_coords) - del copied_distances_from_ICOHPs[idist] - del copied_neighbors_from_ICOHPs[idist] + del copied_distances_from_ICOHPs[dist_idx] + del copied_neighbors_from_ICOHPs[dist_idx] break list_neighisite.append(_list_neighisite) @@ -941,132 +983,137 @@ def _find_environments(self, additional_condition, lowerlimit, upperlimit, only_ list_coords, ) - def _find_relevant_atoms_additional_condition(self, isite, icohps, additional_condition): - """ - Will find all relevant atoms that fulfill the additional_conditions. + def _find_relevant_atoms_additional_condition( + self, + site_idx: int, + icohps: dict[str, IcohpValue], + additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], + ) -> tuple[list[str], list[float], list[int], list[IcohpValue]]: + """Find all relevant atoms that fulfill the additional condition. Args: - isite: number of site in structure (starts with 0) - icohps: icohps - additional_condition (int): additional condition + site_idx (int): Site index in structure (start from 0). + icohps (dict[str, IcohpValue]): ICOHP values. + additional_condition (int): Additional condition. Returns: - tuple: keys, lengths and neighbors from selected ICOHPs and selected ICOHPs + tuple: keys, lengths, neighbors from selected ICOHPs and selected ICOHPs. """ - neighbors_from_ICOHPs = [] - lengths_from_ICOHPs = [] - icohps_from_ICOHPs = [] - keys_from_ICOHPs = [] + keys_from_ICOHPs: list[str] = [] + lengths_from_ICOHPs: list[float] = [] + neighbors_from_ICOHPs: list[int] = [] + icohps_from_ICOHPs: list[IcohpValue] = [] for key, icohp in icohps.items(): atomnr1 = self._get_atomnumber(icohp._atom1) atomnr2 = self._get_atomnumber(icohp._atom2) - # test additional conditions + # Check additional conditions val1 = val2 = None - if additional_condition in (1, 3, 5, 6): + assert self.valences is not None + if additional_condition in {1, 3, 5, 6}: val1 = self.valences[atomnr1] val2 = self.valences[atomnr2] + # NO_ADDITIONAL_CONDITION if additional_condition == 0: - # NO_ADDITIONAL_CONDITION - if atomnr1 == isite: + if atomnr1 == site_idx: neighbors_from_ICOHPs.append(atomnr2) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - elif atomnr2 == isite: + elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + # ONLY_ANION_CATION_BONDS elif additional_condition == 1: - # ONLY_ANION_CATION_BONDS - if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): - if atomnr1 == isite: + if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): # type: ignore[operator] + if atomnr1 == site_idx: neighbors_from_ICOHPs.append(atomnr2) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - elif atomnr2 == isite: + elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + # NO_ELEMENT_TO_SAME_ELEMENT_BONDS elif additional_condition == 2: - # NO_ELEMENT_TO_SAME_ELEMENT_BONDS if icohp._atom1.rstrip("0123456789") != icohp._atom2.rstrip("0123456789"): - if atomnr1 == isite: + if atomnr1 == site_idx: neighbors_from_ICOHPs.append(atomnr2) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - elif atomnr2 == isite: + elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS elif additional_condition == 3: - # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3 - if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and icohp._atom1.rstrip( + if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and icohp._atom1.rstrip( # type: ignore[operator] "0123456789" ) != icohp._atom2.rstrip("0123456789"): - if atomnr1 == isite: + if atomnr1 == site_idx: neighbors_from_ICOHPs.append(atomnr2) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - elif atomnr2 == isite: + elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + # ONLY_ELEMENT_TO_OXYGEN_BONDS elif additional_condition == 4: - # ONLY_ELEMENT_TO_OXYGEN_BONDS = 4 if icohp._atom1.rstrip("0123456789") == "O" or icohp._atom2.rstrip("0123456789") == "O": - if atomnr1 == isite: + if atomnr1 == site_idx: neighbors_from_ICOHPs.append(atomnr2) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - elif atomnr2 == isite: + elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + # DO_NOT_CONSIDER_ANION_CATION_BONDS elif additional_condition == 5: - # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 - if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): - if atomnr1 == isite: + if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): # type: ignore[operator] + if atomnr1 == site_idx: neighbors_from_ICOHPs.append(atomnr2) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - elif atomnr2 == isite: + elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - elif additional_condition == 6 and val1 > 0.0 and val2 > 0.0: - # ONLY_CATION_CATION_BONDS=6 - if atomnr1 == isite: + # ONLY_CATION_CATION_BONDS + elif additional_condition == 6 and val1 > 0.0 and val2 > 0.0: # type: ignore[operator] + if atomnr1 == site_idx: neighbors_from_ICOHPs.append(atomnr2) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - elif atomnr2 == isite: + elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) @@ -1075,21 +1122,27 @@ def _find_relevant_atoms_additional_condition(self, isite, icohps, additional_co return keys_from_ICOHPs, lengths_from_ICOHPs, neighbors_from_ICOHPs, icohps_from_ICOHPs @staticmethod - def _get_icohps(icohpcollection, isite, lowerlimit, upperlimit, only_bonds_to): - """Return icohp dict for certain site. + def _get_icohps( + icohpcollection: IcohpCollection, + site_idx: int, + lowerlimit: float | None, + upperlimit: float | None, + only_bonds_to: list[str] | None, + ) -> dict[str, IcohpValue]: + """Get ICOHP dict for certain site. Args: - icohpcollection: Icohpcollection object - isite (int): number of a site - lowerlimit (float): lower limit that tells you which ICOHPs are considered - upperlimit (float): upper limit that tells you which ICOHPs are considered - only_bonds_to (list): list of str, e.g. ["O"] that will ensure that only bonds to "O" will be considered + icohpcollection (IcohpCollection): IcohpCollection object. + site_idx (int): Site index. + lowerlimit (float): Lower limit that ICOHPs are considered. + upperlimit (float): Upper limit that ICOHPs are considered. + only_bonds_to (list[str]): Only bonds to these elements will be considered, e.g. ["O"]. Returns: - dict: of IcohpValues. The keys correspond to the values from the initial list_labels. + dict of IcohpValues. The keys correspond to the initial list_labels. """ return icohpcollection.get_icohp_dict_of_site( - site=isite, + site=site_idx, maxbondlength=6.0, minsummedicohp=lowerlimit, maxsummedicohp=upperlimit, @@ -1097,36 +1150,34 @@ def _get_icohps(icohpcollection, isite, lowerlimit, upperlimit, only_bonds_to): ) @staticmethod - def _get_atomnumber(atomstring) -> int: - """Get the number of the atom within the initial POSCAR (e.g., Return 0 for "Na1"). + def _get_atomnumber(atomstring: str) -> int: + """Get the index of the atom within the POSCAR (e.g., Return 0 for "Na1"). Args: - atomstring: string such as "Na1" + atomstring (str): Atom as str, such as "Na1". Returns: - int: indicating the position in the POSCAR + int: Index of the atom in the POSCAR. """ return int(LobsterNeighbors._split_string(atomstring)[1]) - 1 @staticmethod def _split_string(s) -> tuple[str, str]: - """ - Will split strings such as "Na1" in "Na" and "1" and return "1". + """Split strings such as "Na1" into ["Na", "1"] and return "1". Args: - s (str): string + s (str): String to split. """ head = s.rstrip("0123456789") tail = s[len(head) :] return head, tail @staticmethod - def _determine_unit_cell(site): - """ - Based on the site it will determine the unit cell, in which this site is based. + def _determine_unit_cell(site: PeriodicSite) -> list[int]: + """Determine the unit cell based on the site. Args: - site: site object + site (PeriodicSite): The site. """ unitcell = [] for coord in site.frac_coords: @@ -1135,50 +1186,58 @@ def _determine_unit_cell(site): return unitcell - def _adapt_extremum_to_add_cond(self, list_icohps, percentage): - """ - Convinicence method for returning the extremum of the given icohps or icoops or icobis list. + def _adapt_extremum_to_add_cond( + self, + list_icohps: list[float], + percentage: float, + ) -> float: + """Get the extremum from the given ICOHPs or ICOOPs or ICOBIs. Args: - list_icohps: can be a list of icohps or icobis or icobis + list_icohps (list): ICOHPs or ICOOPs or ICOBIs. + percentage (float): The percentage to scale extremum. Returns: - float: min value of input list of icohps / max value of input list of icobis or icobis + float: Min value of ICOHPs, or max value of ICOOPs/ICOBIs. """ + which_extr = min if not self.are_coops and not self.are_cobis else max return which_extr(list_icohps) * percentage def _get_limit_from_extremum( self, - icohpcollection, - percentage=0.15, - adapt_extremum_to_add_cond=False, - additional_condition=0, - ): - """Get limits for the evaluation of the icohp values from an icohpcollection - Return -float("inf"), min(max_icohp*0.15,-0.1). Currently only works for ICOHPs. + icohpcollection: IcohpCollection, + percentage: float = 0.15, + adapt_extremum_to_add_cond: bool = False, + additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, + ) -> tuple[float, float] | None: + """Get range for the ICOHP values from an IcohpCollection. + + Currently only work for ICOHPs. Args: - icohpcollection: icohpcollection object - percentage: will determine which ICOHPs or ICOOP or ICOBI will be considered - (only 0.15 from the maximum value) - adapt_extremum_to_add_cond: should the extrumum be adapted to the additional condition - additional_condition: additional condition to determine which bonds are relevant + icohpcollection (IcohpCollection): IcohpCollection object. + percentage (float): Determine which ICOHPs/ICOOP/ICOBI will be considered. + adapt_extremum_to_add_cond (bool): Whether the extrumum be adapted to + the additional condition. + additional_condition (int): Additional condition to determine which bonds to include. Returns: - tuple[float, float]: [-inf, min(strongest_icohp*0.15,-noise_cutoff)] / [max(strongest_icohp*0.15, - noise_cutoff), inf] + tuple[float, float]: [-inf, min(strongest_icohp*0.15, -noise_cutoff)] + or [max(strongest_icohp*0.15, noise_cutoff), inf]. """ extremum_based = None + assert self.valences is not None if not adapt_extremum_to_add_cond or additional_condition == 0: extremum_based = icohpcollection.extremum_icohpvalue(summed_spin_channels=True) * percentage + elif additional_condition == 1: - # only cation anion bonds + # ONLY_ANION_CATION_BONDS list_icohps = [] for value in icohpcollection._icohplist.values(): - atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1) - atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2) + atomnr1 = type(self)._get_atomnumber(value._atom1) + atomnr2 = type(self)._get_atomnumber(value._atom2) val1 = self.valences[atomnr1] val2 = self.valences[atomnr2] @@ -1197,11 +1256,11 @@ def _get_limit_from_extremum( extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) elif additional_condition == 3: - # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3 + # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS list_icohps = [] for value in icohpcollection._icohplist.values(): - atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1) - atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2) + atomnr1 = type(self)._get_atomnumber(value._atom1) + atomnr2 = type(self)._get_atomnumber(value._atom2) val1 = self.valences[atomnr1] val2 = self.valences[atomnr2] @@ -1213,6 +1272,7 @@ def _get_limit_from_extremum( extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) elif additional_condition == 4: + # ONLY_ELEMENT_TO_OXYGEN_BONDS list_icohps = [] for value in icohpcollection._icohplist.values(): if value._atom1.rstrip("0123456789") == "O" or value._atom2.rstrip("0123456789") == "O": @@ -1221,11 +1281,11 @@ def _get_limit_from_extremum( extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) elif additional_condition == 5: - # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 + # DO_NOT_CONSIDER_ANION_CATION_BONDS list_icohps = [] for value in icohpcollection._icohplist.values(): - atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1) - atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2) + atomnr1 = type(self)._get_atomnumber(value._atom1) + atomnr2 = type(self)._get_atomnumber(value._atom2) val1 = self.valences[atomnr1] val2 = self.valences[atomnr2] @@ -1235,11 +1295,11 @@ def _get_limit_from_extremum( extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) elif additional_condition == 6: - # ONLY_CATION_CATION_BONDS=6 + # ONLY_CATION_CATION_BONDS list_icohps = [] for value in icohpcollection._icohplist.values(): - atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1) - atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2) + atomnr1 = type(self)._get_atomnumber(value._atom1) + atomnr2 = type(self)._get_atomnumber(value._atom2) val1 = self.valences[atomnr1] val2 = self.valences[atomnr2] @@ -1251,6 +1311,7 @@ def _get_limit_from_extremum( if not self.are_coops and not self.are_cobis: max_here = min(extremum_based, -self.noise_cutoff) if self.noise_cutoff is not None else extremum_based return -float("inf"), max_here + if self.are_coops or self.are_cobis: min_here = max(extremum_based, self.noise_cutoff) if self.noise_cutoff is not None else extremum_based return min_here, float("inf") @@ -1259,60 +1320,57 @@ def _get_limit_from_extremum( class LobsterLightStructureEnvironments(LightStructureEnvironments): - """Store LightStructureEnvironments based on Lobster outputs.""" + """Store LightStructureEnvironments based on LOBSTER outputs.""" @classmethod def from_Lobster( cls, - list_ce_symbol, - list_csm, - list_permutation, - list_neighsite, - list_neighisite, + list_ce_symbol: list[str], + list_csm: list[float], + list_permutation: list, + list_neighsite: list[PeriodicSite], + list_neighisite: list[list[int]], structure: Structure, - valences=None, + valences: list[float] | None = None, ) -> Self: - """ - Will set up a LightStructureEnvironments from Lobster. + """Set up a LightStructureEnvironments from LOBSTER. Args: - structure: Structure object - list_ce_symbol: list of symbols for coordination environments - list_csm: list of continuous symmetry measures - list_permutation: list of permutations - list_neighsite: list of neighboring sites - list_neighisite: list of neighboring isites (number of a site) - valences: list of valences + list_ce_symbol (list[str]): Coordination environments symbols. + list_csm (list[float]): Continuous symmetry measures. + list_permutation (list): Permutations. + list_neighsite (list[PeriodicSite]): Neighboring sites. + list_neighisite (list[list[int]]): Neighboring sites indexes. + structure (Structure): Structure object. + valences (list[float]): Valences. Returns: LobsterLightStructureEnvironments """ strategy = None valences_origin = "user-defined" - coordination_environments = [] - all_nbs_sites = [] all_nbs_sites_indices = [] neighbors_sets = [] counter = 0 - for isite in range(len(structure)): - # all_nbs_sites_here=[] - all_nbs_sites_indices_here = [] + + for site_idx in range(len(structure)): # Coordination environment if list_ce_symbol is not None: ce_dict = { - "ce_symbol": list_ce_symbol[isite], + "ce_symbol": list_ce_symbol[site_idx], "ce_fraction": 1.0, - "csm": list_csm[isite], - "permutation": list_permutation[isite], + "csm": list_csm[site_idx], + "permutation": list_permutation[site_idx], } else: ce_dict = None - if list_neighisite[isite] is not None: - for idx_neigh_site, neigh_site in enumerate(list_neighsite[isite]): - diff = neigh_site.frac_coords - structure[list_neighisite[isite][idx_neigh_site]].frac_coords + if list_neighisite[site_idx] is not None: + all_nbs_sites_indices_here = [] + for neigh_site_idx, neigh_site in enumerate(list_neighsite[site_idx]): + diff = neigh_site.frac_coords - structure[list_neighisite[site_idx][neigh_site_idx]].frac_coords round_diff = np.round(diff) if not np.allclose(diff, round_diff): raise ValueError( @@ -1324,29 +1382,30 @@ def from_Lobster( neighbor = { "site": neigh_site, - "index": list_neighisite[isite][idx_neigh_site], + "index": list_neighisite[site_idx][neigh_site_idx], "image_cell": nb_image_cell, } all_nbs_sites.append(neighbor) counter += 1 all_nbs_sites_indices.append(all_nbs_sites_indices_here) + else: - all_nbs_sites.append({"site": None, "index": None, "image_cell": None}) # all_nbs_sites_here) - all_nbs_sites_indices.append([]) # all_nbs_sites_indices_here) + all_nbs_sites.append({"site": None, "index": None, "image_cell": None}) + all_nbs_sites_indices.append([]) - if list_neighisite[isite] is not None: + if list_neighisite[site_idx] is not None: nb_set = cls.NeighborsSet( structure=structure, - isite=isite, + isite=site_idx, all_nbs_sites=all_nbs_sites, - all_nbs_sites_indices=all_nbs_sites_indices[isite], + all_nbs_sites_indices=all_nbs_sites_indices[site_idx], ) else: nb_set = cls.NeighborsSet( structure=structure, - isite=isite, + isite=site_idx, all_nbs_sites=[], all_nbs_sites_indices=[], ) @@ -1365,16 +1424,15 @@ def from_Lobster( ) @property - def uniquely_determines_coordination_environments(self): - """True if the coordination environments are uniquely determined.""" + def uniquely_determines_coordination_environments(self) -> Literal[True]: + """Whether the coordination environments are uniquely determined.""" return True - def as_dict(self): - """ - Bson-serializable dict representation of the LightStructureEnvironments object. + def as_dict(self) -> dict[str, Any]: + """Bson-serializable dict representation of the object. Returns: - Bson-serializable dict representation of the LightStructureEnvironments object. + Bson-serializable dict representation. """ return { "@module": type(self).__module__, @@ -1398,16 +1456,17 @@ def as_dict(self): class ICOHPNeighborsInfo(NamedTuple): - """ - Tuple to represent information on relevant bonds + """Tuple to record information on relevant bonds. + Args: - total_icohp (float): sum of icohp values of neighbors to the selected sites [given by the id in structure] - list_icohps (list): list of summed icohp values for all identified interactions with neighbors - n_bonds (int): number of identified bonds to the selected sites - labels (list[str]): labels (from ICOHPLIST) for all identified bonds - atoms (list[list[str]]): list of list describing the species present in the identified interactions - (names from ICOHPLIST), e.g. ["Ag3", "O5"] - central_isites (list[int]): list of the central isite for each identified interaction. + total_icohp (float): Sum of ICOHP values of neighbors to the selected + sites (given by the index in structure). + list_icohps (list): Summed ICOHP values for all identified interactions with neighbors. + n_bonds (int): Number of identified bonds to the selected sites. + labels (list[str]): Labels (from ICOHPLIST) for all identified bonds. + atoms (list[list[str]]): Lists describing the species present (from ICOHPLIST) + in the identified interactions , e.g. ["Ag3", "O5"]. + central_isites (list[int]): The central site indexes for each identified interaction. """ total_icohp: float diff --git a/outputs.py b/outputs.py index 5f6fea7e7e..f8e4032b6e 100644 --- a/outputs.py +++ b/outputs.py @@ -1,6 +1,6 @@ -""" -Module for reading Lobster output files. For more information -on LOBSTER see www.cohp.de. +"""Module for reading Lobster output files. +For more information on LOBSTER see www.cohp.de. + If you use this module, please cite: J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, "Automated Bonding Analysis with Crystal Orbital Hamilton Populations", @@ -17,9 +17,10 @@ import re import warnings from collections import defaultdict -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import numpy as np +from monty.dev import deprecated from monty.io import zopen from monty.json import MSONable @@ -30,12 +31,16 @@ from pymatgen.io.vasp.inputs import Kpoints from pymatgen.io.vasp.outputs import Vasprun, VolumetricData from pymatgen.util.due import Doi, due +from pymatgen.util.typing import PathLike if TYPE_CHECKING: - from typing import Any + from typing import Any, ClassVar, Literal + + from numpy.typing import NDArray from pymatgen.core.structure import IStructure - from pymatgen.util.typing import PathLike + from pymatgen.electronic_structure.cohp import IcohpCollection + from pymatgen.util.typing import Tuple3Ints, Vector3D __author__ = "Janine George, Marco Esters" __copyright__ = "Copyright 2017, The Materials Project" @@ -44,7 +49,6 @@ __email__ = "janinegeorge.ulfen@gmail.com" __date__ = "Dec 13, 2017" -MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) due.cite( Doi("10.1002/cplu.202200123"), @@ -56,17 +60,17 @@ class Cohpcar: """Read COHPCAR/COOPCAR/COBICAR files generated by LOBSTER. Attributes: - cohp_data (dict[str, Dict[str, Any]]): A dictionary containing the COHP data of the form: + cohp_data (dict[str, Dict[str, Any]]): The COHP data of the form: {bond: {"COHP": {Spin.up: cohps, Spin.down:cohps}, "ICOHP": {Spin.up: icohps, Spin.down: icohps}, "length": bond length, "sites": sites corresponding to the bond} Also contains an entry for the average, which does not have a "length" key. - efermi (float): The Fermi energy in eV. - energies (Sequence[float]): Sequence of energies in eV. Note that LOBSTER shifts the energies - so that the Fermi energy is at zero. + efermi (float): The Fermi level in eV. + energies (Sequence[float]): Sequence of energies in eV. Note that LOBSTER + shifts the energies so that the Fermi level is at zero. is_spin_polarized (bool): True if the calculation is spin polarized. - orb_cohp (dict[str, Dict[str, Dict[str, Any]]]): A dictionary containing the orbital-resolved COHPs of the form: + orb_cohp (dict[str, Dict[str, Dict[str, Any]]]): The orbital-resolved COHPs of the form: orb_cohp[label] = {bond_data["orb_label"]: { "COHP": {Spin.up: cohps, Spin.down:cohps}, "ICOHP": {Spin.up: icohps, Spin.down: icohps}, @@ -85,14 +89,14 @@ def __init__( ) -> None: """ Args: - are_coops: Determines if the file includes COOPs. - Default is False for COHPs. - are_cobis: Determines if the file is a list of COHPs or COBIs. - Default is False for COHPs. - are_multi_center_cobis: Determines if the file include multi-center COBIS. - Default is False for two-center cobis. - filename: Name of the COHPCAR file. If it is None, the default - file name will be chosen, depending on the value of are_coops. + are_coops (bool): Whether the file includes COOPs (True) or COHPs (False). + Default is False. + are_cobis (bool): Whether the file is COBIs (True) or COHPs (False). + Default is False. + are_multi_center_cobis (bool): Whether the file include multi-center COBIs (True) + or two-center COBIs (False). Default is False. + filename (PathLike): The COHPCAR file. If it is None, the default + file name will be chosen, depending on the value of are_coops. """ if ( (are_coops and are_cobis) @@ -100,6 +104,7 @@ def __init__( or (are_cobis and are_multi_center_cobis) ): raise ValueError("You cannot have info about COOPs, COBIs and/or multi-center COBIS in the same file.") + self.are_coops = are_coops self.are_cobis = are_cobis self.are_multi_center_cobis = are_multi_center_cobis @@ -113,11 +118,11 @@ def __init__( filename = "COHPCAR.lobster" with zopen(filename, mode="rt") as file: - contents = file.read().split("\n") + lines = file.read().split("\n") - # The parameters line is the second line in a COHPCAR file. It - # contains all parameters that are needed to map the file. - parameters = contents[1].split() + # The parameters line is the second line in a COHPCAR file. + # It contains all parameters that are needed to map the file. + parameters = lines[1].split() # Subtract 1 to skip the average num_bonds = int(parameters[0]) if self.are_multi_center_cobis else int(parameters[0]) - 1 self.efermi = float(parameters[-1]) @@ -125,8 +130,8 @@ def __init__( spins = [Spin.up, Spin.down] if int(parameters[1]) == 2 else [Spin.up] cohp_data: dict[str, dict[str, Any]] = {} if not self.are_multi_center_cobis: - # The COHP data start in row num_bonds + 3 - data = np.array([np.array(row.split(), dtype=float) for row in contents[num_bonds + 3 :]]).transpose() + # The COHP data start in line num_bonds + 3 + data = np.array([np.array(line.split(), dtype=float) for line in lines[num_bonds + 3 :]]).transpose() cohp_data = { "average": { "COHP": {spin: data[1 + 2 * s * (num_bonds + 1)] for s, spin in enumerate(spins)}, @@ -134,22 +139,23 @@ def __init__( } } else: - # The COBI data start in row num_bonds + 3 if multi-center cobis exist - data = np.array([np.array(row.split(), dtype=float) for row in contents[num_bonds + 3 :]]).transpose() + # The COBI data start in line num_bonds + 3 if multi-center cobis exist + data = np.array([np.array(line.split(), dtype=float) for line in lines[num_bonds + 3 :]]).transpose() self.energies = data[0] orb_cohp: dict[str, Any] = {} - # present for Lobster versions older than Lobster 2.2.0 + # Present for LOBSTER versions older than 2.2.0 very_old = False - # the labeling had to be changed: there are more than one COHP for each atom combination + + # The label has to be changed: there are more than one COHP for each atom combination # this is done to make the labeling consistent with ICOHPLIST.lobster bond_num = 0 bond_data = {} label = "" for bond in range(num_bonds): if not self.are_multi_center_cobis: - bond_data = self._get_bond_data(contents[3 + bond]) + bond_data = self._get_bond_data(lines[3 + bond]) label = str(bond_num) orbs = bond_data["orbitals"] cohp = {spin: data[2 * (bond + s * (num_bonds + 1)) + 3] for s, spin in enumerate(spins)} @@ -177,7 +183,7 @@ def __init__( } } else: - # present for Lobster versions older than Lobster 2.2.0 + # Present for LOBSTER versions older than 2.2.0 if bond_num == 0: very_old = True if very_old: @@ -196,15 +202,14 @@ def __init__( } else: - bond_data = self._get_bond_data(contents[2 + bond], are_multi_center_cobis=self.are_multi_center_cobis) + bond_data = self._get_bond_data(lines[2 + bond], are_multi_center_cobis=self.are_multi_center_cobis) label = str(bond_num) - orbs = bond_data["orbitals"] cohp = {spin: data[2 * (bond + s * (num_bonds)) + 1] for s, spin in enumerate(spins)} - icohp = {spin: data[2 * (bond + s * (num_bonds)) + 2] for s, spin in enumerate(spins)} + if orbs is None: bond_num += 1 label = str(bond_num) @@ -227,7 +232,7 @@ def __init__( } } else: - # present for Lobster versions older than Lobster 2.2.0 + # Present for LOBSTER versions older than 2.2.0 if bond_num == 0: very_old = True if very_old: @@ -244,7 +249,7 @@ def __init__( } } - # present for lobster older than 2.2.0 + # Present for LOBSTER older than 2.2.0 if very_old: for bond_str in orb_cohp: cohp_data[bond_str] = { @@ -257,12 +262,13 @@ def __init__( self.cohp_data = cohp_data @staticmethod - def _get_bond_data(line: str, are_multi_center_cobis: bool = False) -> dict: - """Subroutine to extract bond label, site indices, and length from + def _get_bond_data(line: str, are_multi_center_cobis: bool = False) -> dict[str, Any]: + """Extract bond label, site indices, and length from a LOBSTER header line. The site indices are zero-based, so they can be easily used with a Structure object. - Example header line: No.4:Fe1->Fe9(2.4524893531900283) + Example header line: + No.4:Fe1->Fe9(2.4524893531900283) Example header line for orbital-resolved COHP: No.1:Fe1[3p_x]->Fe2[3d_x^2-y^2](2.456180552772262) @@ -272,8 +278,8 @@ def _get_bond_data(line: str, are_multi_center_cobis: bool = False) -> dict: Returns: Dict with the bond label, the bond length, a tuple of the site - indices, a tuple containing the orbitals (if orbital-resolved), - and a label for the orbitals (if orbital-resolved). + indices, a tuple containing the orbitals (if orbital-resolved), + and a label for the orbitals (if orbital-resolved). """ if not are_multi_center_cobis: @@ -304,7 +310,7 @@ def _get_bond_data(line: str, are_multi_center_cobis: bool = False) -> dict: sites = line_new[0].replace("->", ":").split(":")[1:] site_indices = tuple(int(re.split(r"\D+", site)[1]) - 1 for site in sites) cells = [[int(i) for i in re.split(r"\[(.*?)\]", site)[1].split(" ") if i != ""] for site in sites] - # test orbitalwise implementations! + if sites[0].count("[") > 1: orbs = [re.findall(r"\]\[(.*)\]", site)[0] for site in sites] orb_label, orbitals = get_orb_from_str(orbs) @@ -325,13 +331,15 @@ class Icohplist(MSONable): """Read ICOHPLIST/ICOOPLIST files generated by LOBSTER. Attributes: - are_coops (bool): Indicates whether the object consists of COOPs. - is_spin_polarized (bool): True if the calculation is spin polarized. - Icohplist (dict[str, Dict[str, Union[float, int, Dict[Spin, float]]]]): Dict containing the - listfile data of the form: { - bond: "length": bond length, - "number_of_bonds": number of bonds - "icohp": {Spin.up: ICOHP(Ef) spin up, Spin.down: ...} + are_coops (bool): Whether the file includes COOPs (True) or COHPs (False). + is_spin_polarized (bool): Whether the calculation is spin polarized. + Icohplist (dict[str, Dict[str, Union[float, int, Dict[Spin, float]]]]): + The listfile data of the form: { + bond: { + "length": Bond length, + "number_of_bonds": Number of bonds, + "icohp": {Spin.up: ICOHP(Ef)_up, Spin.down: ...}, + } } IcohpCollection (IcohpCollection): IcohpCollection Object. """ @@ -343,26 +351,31 @@ def __init__( filename: PathLike | None = None, is_spin_polarized: bool = False, orbitalwise: bool = False, - icohpcollection=None, - ): + icohpcollection: IcohpCollection | None = None, + ) -> None: """ Args: - are_coops: Determines if the file is a list of ICOOPs. - Defaults to False for ICOHPs. - are_cobis: Determines if the file is a list of ICOBIs. - Defaults to False for ICOHPs. - filename: Name of the ICOHPLIST file. If it is None, the default - file name will be chosen, depending on the value of are_coops - is_spin_polarized: Boolean to indicate if the calculation is spin polarized - icohpcollection: IcohpCollection Object + are_coops (bool): Whether the file includes COOPs (True) or COHPs (False). + Default is False. + are_cobis (bool): Whether the file is COBIs (True) or COHPs (False). + Default is False. + filename (PathLike): The ICOHPLIST file. If it is None, the default + file name will be chosen, depending on the value of are_coops + is_spin_polarized (bool): Whether the calculation is spin polarized. + orbitalwise (bool): Whether the calculation is orbitalwise. + icohpcollection (IcohpCollection): IcohpCollection Object. """ + # Avoid circular import + from pymatgen.electronic_structure.cohp import IcohpCollection + self._filename = filename self.is_spin_polarized = is_spin_polarized self.orbitalwise = orbitalwise self._icohpcollection = icohpcollection if are_coops and are_cobis: raise ValueError("You cannot have info about COOPs and COBIs in the same file.") + self.are_coops = are_coops self.are_cobis = are_cobis if filename is None: @@ -377,40 +390,40 @@ def __init__( # and we don't need the header. if self._icohpcollection is None: with zopen(filename, mode="rt") as file: - data = file.read().split("\n")[1:-1] - if len(data) == 0: + lines = file.read().split("\n")[1:-1] + if len(lines) == 0: raise RuntimeError("ICOHPLIST file contains no data.") # Determine LOBSTER version - if len(data[0].split()) == 8: + if len(lines[0].split()) == 8: version = "3.1.1" - elif len(data[0].split()) == 6: + elif len(lines[0].split()) == 6: version = "2.2.1" - warnings.warn("Please consider using the new LOBSTER version. See www.cohp.de.") + warnings.warn("Please consider using a newer LOBSTER version. See www.cohp.de.") else: raise ValueError("Unsupported LOBSTER version.") # If the calculation is spin polarized, the line in the middle # of the file will be another header line. # TODO: adapt this for orbital-wise stuff - self.is_spin_polarized = "distance" in data[len(data) // 2] + self.is_spin_polarized = "distance" in lines[len(lines) // 2] - # check if orbital-wise ICOHPLIST - # include case when there is only one ICOHP!!! - self.orbitalwise = len(data) > 2 and "_" in data[1].split()[1] + # Check if is orbital-wise ICOHPLIST + # TODO: include case where there is only one ICOHP + self.orbitalwise = len(lines) > 2 and "_" in lines[1].split()[1] data_orbitals: list[str] = [] if self.orbitalwise: data_without_orbitals = [] data_orbitals = [] - for line in data: + for line in lines: if "_" not in line.split()[1]: data_without_orbitals.append(line) else: data_orbitals.append(line) else: - data_without_orbitals = data + data_without_orbitals = lines if "distance" in data_without_orbitals[len(data_without_orbitals) // 2]: # TODO: adapt this for orbital-wise stuff @@ -421,40 +434,39 @@ def __init__( n_bonds = len(data_without_orbitals) labels: list[str] = [] - atoms1: list[str] = [] - atoms2: list[str] = [] + atom1_list: list[str] = [] + atom2_list: list[str] = [] lens: list[float] = [] - translations: list[tuple[int, int, int]] = [] + translations: list[Tuple3Ints] = [] nums: list[int] = [] icohps: list[dict[Spin, float]] = [] for bond in range(n_bonds): line_parts = data_without_orbitals[bond].split() + icohp: dict[Spin, float] = {} - label = f"{line_parts[0]}" - atom1 = str(line_parts[1]) - atom2 = str(line_parts[2]) + label = line_parts[0] + atom1 = line_parts[1] + atom2 = line_parts[2] length = float(line_parts[3]) - icohp: dict[Spin, float] = {} - if version == "2.2.1": - icohp[Spin.up] = float(line_parts[4]) - num = int(line_parts[5]) - translation = (0, 0, 0) - if self.is_spin_polarized: - icohp[Spin.down] = float(data_without_orbitals[bond + n_bonds + 1].split()[4]) - - else: # version == "3.1.1" + if version == "3.1.1": + num = 1 translation = (int(line_parts[4]), int(line_parts[5]), int(line_parts[6])) icohp[Spin.up] = float(line_parts[7]) - num = 1 - if self.is_spin_polarized: icohp[Spin.down] = float(data_without_orbitals[bond + n_bonds + 1].split()[7]) + else: # if version == "2.2.1": + num = int(line_parts[5]) + translation = (0, 0, 0) + icohp[Spin.up] = float(line_parts[4]) + if self.is_spin_polarized: + icohp[Spin.down] = float(data_without_orbitals[bond + n_bonds + 1].split()[4]) + labels.append(label) - atoms1.append(atom1) - atoms2.append(atom2) + atom1_list.append(atom1) + atom2_list.append(atom2) lens.append(length) translations.append(translation) nums.append(num) @@ -465,17 +477,17 @@ def __init__( list_orb_icohp = [] n_orbs = len(data_orbitals) // 2 if self.is_spin_polarized else len(data_orbitals) - for i_data_orb in range(n_orbs): - data_orb = data_orbitals[i_data_orb] + for i_orb in range(n_orbs): + data_orb = data_orbitals[i_orb] icohp = {} line_parts = data_orb.split() - label = f"{line_parts[0]}" + label = line_parts[0] orbs = re.findall(r"_(.*?)(?=\s)", data_orb) orb_label, orbitals = get_orb_from_str(orbs) icohp[Spin.up] = float(line_parts[7]) if self.is_spin_polarized: - icohp[Spin.down] = float(data_orbitals[n_orbs + i_data_orb].split()[7]) + icohp[Spin.down] = float(data_orbitals[n_orbs + i_orb].split()[7]) if len(list_orb_icohp) < int(label): list_orb_icohp.append({orb_label: {"icohp": icohp, "orbitals": orbitals}}) @@ -489,8 +501,8 @@ def __init__( are_coops=are_coops, are_cobis=are_cobis, list_labels=labels, - list_atom1=atoms1, - list_atom2=atoms2, + list_atom1=atom1_list, + list_atom2=atom2_list, list_length=lens, list_translation=translations, # type: ignore[arg-type] list_num=nums, @@ -503,6 +515,8 @@ def __init__( def icohplist(self) -> dict[Any, dict[str, Any]]: """The ICOHP list compatible with older version of this class.""" icohp_dict = {} + assert self._icohpcollection is not None + for key, value in self._icohpcollection._icohplist.items(): icohp_dict[key] = { "length": value._length, @@ -514,7 +528,7 @@ def icohplist(self) -> dict[Any, dict[str, Any]]: return icohp_dict @property - def icohpcollection(self): + def icohpcollection(self) -> IcohpCollection | None: """The IcohpCollection object.""" return self._icohpcollection @@ -523,18 +537,20 @@ class NciCobiList: """Read NcICOBILIST (multi-center ICOBI) files generated by LOBSTER. Attributes: - is_spin_polarized (bool): True if the calculation is spin polarized. - NciCobiList (dict): Dict containing the listfile data of the form: - {bond: "number_of_atoms": number of atoms involved in the multi-center interaction, - "ncicobi": {Spin.up: Nc-ICOBI(Ef) spin up, Spin.down: ...}}, - "interaction_type": type of the multi-center interaction + is_spin_polarized (bool): Whether the calculation is spin polarized. + NciCobiList (dict): The listfile data of the form: + { + bond: { + "number_of_atoms": Number of atoms involved in the multi-center interaction, + "ncicobi": {Spin.up: Nc-ICOBI(Ef)_up, Spin.down: ...}, + "interaction_type": Type of the multi-center interaction, + } + } """ - def __init__( - self, - filename: PathLike | None = "NcICOBILIST.lobster", - ) -> None: + def __init__(self, filename: PathLike | None = "NcICOBILIST.lobster") -> None: """ + LOBSTER < 4.1.0: no COBI/ICOBI/NcICOBI Args: @@ -542,21 +558,22 @@ def __init__( """ # LOBSTER list files have an extra trailing blank line - # and we don't need the header. + # and we don't need the header with zopen(filename, mode="rt") as file: - data = file.read().split("\n")[1:-1] - if len(data) == 0: + lines = file.read().split("\n")[1:-1] + if len(lines) == 0: raise RuntimeError("NcICOBILIST file contains no data.") # If the calculation is spin-polarized, the line in the middle # of the file will be another header line. - self.is_spin_polarized = "spin" in data[len(data) // 2] # TODO: adapt this for orbitalwise case + # TODO: adapt this for orbitalwise case + self.is_spin_polarized = "spin" in lines[len(lines) // 2] - # check if orbitalwise NcICOBILIST + # Check if orbitalwise NcICOBILIST # include case when there is only one NcICOBI self.orbital_wise = False # set as default - for entry in data: # NcICOBIs orbitalwise and non-orbitalwise can be mixed - if len(data) > 2 and "s]" in str(entry.split()[3:]): + for entry in lines: # NcICOBIs orbitalwise and non-orbitalwise can be mixed + if len(lines) > 2 and "s]" in str(entry.split()[3:]): self.orbital_wise = True warnings.warn( "This is an orbitalwise NcICOBILIST.lobster file. " @@ -566,11 +583,11 @@ def __init__( if self.orbital_wise: data_without_orbitals = [] - for line in data: + for line in lines: if "_" not in str(line.split()[3:]) and "s]" not in str(line.split()[3:]): data_without_orbitals.append(line) else: - data_without_orbitals = data + data_without_orbitals = lines if "spin" in data_without_orbitals[len(data_without_orbitals) // 2]: # TODO: adapt this for orbitalwise case @@ -587,13 +604,13 @@ def __init__( self.list_num = [] for bond in range(n_bonds): - line = data_without_orbitals[bond].split() + line_parts = data_without_orbitals[bond].split() ncicobi = {} - label = f"{line[0]}" - n_atoms = str(line[1]) - ncicobi[Spin.up] = float(line[2]) - interaction_type = str(line[3:]).replace("'", "").replace(" ", "") + label = line_parts[0] + n_atoms = line_parts[1] + ncicobi[Spin.up] = float(line_parts[2]) + interaction_type = str(line_parts[3:]).replace("'", "").replace(" ", "") num = 1 if self.is_spin_polarized: @@ -610,7 +627,8 @@ def __init__( @property def ncicobi_list(self) -> dict[Any, dict[str, Any]]: """ - Returns: ncicobilist. + Returns: + dict: ncicobilist. """ ncicobi_list = {} for idx in range(len(self.list_labels)): @@ -624,24 +642,24 @@ def ncicobi_list(self) -> dict[Any, dict[str, Any]]: class Doscar: - """Deal with Lobster's projected DOS and local projected DOS. + """Store LOBSTER's projected DOS and local projected DOS. The beforehand quantum-chemical calculation was performed with VASP. Attributes: completedos (LobsterCompleteDos): LobsterCompleteDos Object. - pdos (list): List of Dict including numpy arrays with pdos. Access as + pdos (list): List of Dict including NumPy arrays with pdos. Access as pdos[atomindex]['orbitalstring']['Spin.up/Spin.down']. tdos (Dos): Dos Object of the total density of states. - energies (numpy.ndarray): Numpy array of the energies at which the DOS was calculated + energies (NDArray): Numpy array of the energies at which the DOS was calculated (in eV, relative to Efermi). - tdensities (dict): tdensities[Spin.up]: numpy array of the total density of states for - the Spin.up contribution at each of the energies. tdensities[Spin.down]: numpy array + tdensities (dict): tdensities[Spin.up]: NumPy array of the total density of states for + the Spin.up contribution at each of the energies. tdensities[Spin.down]: NumPy array of the total density of states for the Spin.down contribution at each of the energies. - If is_spin_polarized=False, tdensities[Spin.up]: numpy array of the total density of states. - itdensities (dict): itdensities[Spin.up]: numpy array of the total density of states for - the Spin.up contribution at each of the energies. itdensities[Spin.down]: numpy array + If is_spin_polarized=False, tdensities[Spin.up]: NumPy array of the total density of states. + itdensities (dict): itdensities[Spin.up]: NumPy array of the total density of states for + the Spin.up contribution at each of the energies. itdensities[Spin.down]: NumPy array of the total density of states for the Spin.down contribution at each of the energies. - If is_spin_polarized=False, itdensities[Spin.up]: numpy array of the total density of states. + If is_spin_polarized=False, itdensities[Spin.up]: NumPy array of the total density of states. is_spin_polarized (bool): Whether the system is spin polarized. """ @@ -650,13 +668,13 @@ def __init__( doscar: PathLike = "DOSCAR.lobster", structure_file: PathLike | None = "POSCAR", structure: IStructure | Structure | None = None, - ): + ) -> None: """ Args: - doscar: DOSCAR file, typically "DOSCAR.lobster" - structure_file: for vasp, this is typically "POSCAR" - structure: instead of a structure file, the structure can be given - directly. structure_file will be preferred. + doscar (PathLike): The DOSCAR file, typically "DOSCAR.lobster". + structure_file (PathLike): For VASP, this is typically "POSCAR". + structure (Structure): Instead of a structure file (preferred), + the Structure can be given directly. """ self._doscar = doscar @@ -678,13 +696,15 @@ def _parse_doscar(self): line = file.readline() ndos = int(line.split()[2]) orbitals += [line.split(";")[-1].split()] + line = file.readline().split() cdos = np.zeros((ndos, len(line))) cdos[0] = np.array(line) for nd in range(1, ndos): - line = file.readline().split() - cdos[nd] = np.array(line) + line_parts = file.readline().split() + cdos[nd] = np.array(line_parts) dos.append(cdos) + doshere = np.array(dos[0]) if len(doshere[0, :]) == 5: self._is_spin_polarized = True @@ -692,6 +712,7 @@ def _parse_doscar(self): self._is_spin_polarized = False else: raise ValueError("There is something wrong with the DOSCAR. Can't extract spin polarization.") + energies = doshere[:, 0] if not self._is_spin_polarized: tdensities[Spin.up] = doshere[:, 1] @@ -740,32 +761,32 @@ def _parse_doscar(self): @property def completedos(self) -> LobsterCompleteDos: - """LobsterCompleteDos""" + """LobsterCompleteDos.""" return self._completedos @property - def pdos(self) -> list: - """Projected DOS""" + def pdos(self) -> list[dict]: + """Projected DOS (PDOS).""" return self._pdos @property def tdos(self) -> Dos: - """Total DOS""" + """Total DOS (TDOS).""" return self._tdos @property - def energies(self) -> np.ndarray: - """Energies""" + def energies(self) -> NDArray: + """Energies.""" return self._energies @property - def tdensities(self) -> dict[Spin, np.ndarray]: - """total densities as a np.ndarray""" + def tdensities(self) -> dict[Spin, NDArray]: + """Total DOS as a np.array.""" return self._tdensities @property - def itdensities(self) -> dict[Spin, np.ndarray]: - """integrated total densities as a np.ndarray""" + def itdensities(self) -> dict[Spin, NDArray]: + """Integrated total DOS as a np.array.""" return self._itdensities @property @@ -793,15 +814,15 @@ def __init__( types: list[str] | None = None, mulliken: list[float] | None = None, loewdin: list[float] | None = None, - ): + ) -> None: """ Args: - filename: The CHARGE file, typically "CHARGE.lobster". - num_atoms: number of atoms in the structure - atomlist: list of atoms in the structure - types: list of unique species in the structure - mulliken: list of Mulliken charges - loewdin: list of Loewdin charges + filename (PathLike): The CHARGE file, typically "CHARGE.lobster". + num_atoms (int): Number of atoms in the structure. + atomlist (list[str]): Atoms in the structure. + types (list[str]): Unique species in the structure. + mulliken (list[float]): Mulliken charges. + loewdin (list[float]): Loewdin charges. """ self._filename = filename self.num_atoms = num_atoms @@ -812,13 +833,13 @@ def __init__( if self.num_atoms is None: with zopen(filename, mode="rt") as file: - data = file.read().split("\n")[3:-3] - if len(data) == 0: - raise RuntimeError("CHARGE file contains no data.") + lines = file.read().split("\n")[3:-3] + if len(lines) == 0: + raise RuntimeError("CHARGES file contains no data.") - self.num_atoms = len(data) + self.num_atoms = len(lines) for atom_idx in range(self.num_atoms): - line_parts = data[atom_idx].split() + line_parts = lines[atom_idx].split() self.atomlist.append(line_parts[1] + line_parts[0]) self.types.append(line_parts[1]) self.mulliken.append(float(line_parts[2])) @@ -828,7 +849,7 @@ def get_structure_with_charges(self, structure_filename: PathLike) -> Structure: """Get a Structure with Mulliken and Loewdin charges as site properties Args: - structure_filename: filename of POSCAR + structure_filename (PathLike): The POSCAR file. Returns: Structure Object with Mulliken and Loewdin charges as site properties. @@ -840,26 +861,26 @@ def get_structure_with_charges(self, structure_filename: PathLike) -> Structure: return struct.copy(site_properties=site_properties) @property - def Mulliken(self): - warnings.warn("`Mulliken` attribute is deprecated. Use `mulliken` instead.", DeprecationWarning, stacklevel=2) + @deprecated(message="Use `mulliken` instead.", category=DeprecationWarning) + def Mulliken(self) -> list[float]: return self.mulliken @property - def Loewdin(self): - warnings.warn("`Loewdin` attribute is deprecated. Use `loewdin` instead.", DeprecationWarning, stacklevel=2) + @deprecated(message="Use `loewdin` instead.", category=DeprecationWarning) + def Loewdin(self) -> list[float]: return self.loewdin class Lobsterout(MSONable): - """Read in the lobsterout and evaluate the spilling, save the basis, save warnings, save infos. + """Read the lobsterout and evaluate the spilling, save the basis, save warnings, save info. Attributes: - basis_functions (list[str]): List of basis functions that were used in lobster run as strings. - basis_type (list[str]): List of basis type that were used in lobster run as strings. - charge_spilling (list[float]): List of charge spilling (first entry: result for spin 1, + basis_functions (list[str]): Basis functions that were used in lobster run as strings. + basis_type (list[str]): Basis types that were used in lobster run as strings. + charge_spilling (list[float]): Charge spilling (first entry: result for spin 1, second entry: result for spin 2 or not present). - dft_program (str): String representing the DFT program used for the calculation of the wave function. - elements (list[str]): List of strings of elements that were present in lobster calculation. + dft_program (str): The DFT program used for the calculation of the wave function. + elements (list[str]): Elements that were present in LOBSTER calculation. has_charge (bool): Whether CHARGE.lobster is present. has_cohpcar (bool): Whether COHPCAR.lobster and ICOHPLIST.lobster are present. has_madelung (bool): Whether SitePotentials.lobster and MadelungEnergies.lobster are present. @@ -872,21 +893,20 @@ class Lobsterout(MSONable): has_density_of_energies (bool): Whether DensityOfEnergy.lobster is present. has_fatbands (bool): Whether fatband calculation was performed. has_grosspopulation (bool): Whether GROSSPOP.lobster is present. - info_lines (str): String with additional infos on the run. - info_orthonormalization (str): String with infos on orthonormalization. - is_restart_from_projection (bool): Whether calculation was restarted from existing - projection file. - lobster_version (str): String that indicates Lobster version. - number_of_spins (int): Integer indicating the number of spins. - number_of_threads (int): Integer that indicates how many threads were used. - timing (dict[str, float]): Dictionary with infos on timing. - total_spilling (list[float]): List of values indicating the total spilling for spin - channel 1 (and spin channel 2). + info_lines (str): Additional information on the run. + info_orthonormalization (str): Information on orthonormalization. + is_restart_from_projection (bool): Whether that calculation was restarted + from an existing projection file. + lobster_version (str): The LOBSTER version. + number_of_spins (int): The number of spins. + number_of_threads (int): How many threads were used. + timing (dict[str, float]): Dict with infos on timing. + total_spilling (list[float]): The total spilling for spin channel 1 (and spin channel 2). warning_lines (str): String with all warnings. """ - # valid Lobsterout instance attributes - _ATTRIBUTES = ( + # Valid Lobsterout attributes + _ATTRIBUTES: ClassVar[set[str]] = { "filename", "is_restart_from_projection", "lobster_version", @@ -914,14 +934,14 @@ class Lobsterout(MSONable): "has_fatbands", "has_grosspopulation", "has_density_of_energies", - ) + } - # TODO: add tests for skipping COBI and madelung - # TODO: add tests for including COBI and madelung + # TODO: add tests for skipping COBI and Madelung + # TODO: add tests for including COBI and Madelung def __init__(self, filename: PathLike | None, **kwargs) -> None: """ Args: - filename: The lobsterout file. + filename (PathLike): The lobsterout file. **kwargs: dict to initialize Lobsterout instance """ self.filename = filename @@ -932,79 +952,79 @@ def __init__(self, filename: PathLike | None, **kwargs) -> None: else: raise ValueError(f"{attr}={val} is not a valid attribute for Lobsterout") elif filename: - with zopen(filename, mode="rt") as file: # read in file - data = file.read().split("\n") - if len(data) == 0: + with zopen(filename, mode="rt") as file: + lines = file.read().split("\n") + if len(lines) == 0: raise RuntimeError("lobsterout does not contain any data") - # check if Lobster starts from a projection - self.is_restart_from_projection = "loading projection from projectionData.lobster..." in data + # Check if LOBSTER starts from a projection + self.is_restart_from_projection = "loading projection from projectionData.lobster..." in lines - self.lobster_version = self._get_lobster_version(data=data) + self.lobster_version = self._get_lobster_version(data=lines) - self.number_of_threads = int(self._get_threads(data=data)) - self.dft_program = self._get_dft_program(data=data) + self.number_of_threads = self._get_threads(data=lines) + self.dft_program = self._get_dft_program(data=lines) - self.number_of_spins = self._get_number_of_spins(data=data) - chargespilling, totalspilling = self._get_spillings(data=data, number_of_spins=self.number_of_spins) + self.number_of_spins = self._get_number_of_spins(data=lines) + chargespilling, totalspilling = self._get_spillings(data=lines, number_of_spins=self.number_of_spins) self.charge_spilling = chargespilling self.total_spilling = totalspilling - elements, basistype, basisfunctions = self._get_elements_basistype_basisfunctions(data=data) + elements, basistype, basisfunctions = self._get_elements_basistype_basisfunctions(data=lines) self.elements = elements self.basis_type = basistype self.basis_functions = basisfunctions - wall_time, user_time, sys_time = self._get_timing(data=data) + wall_time, user_time, sys_time = self._get_timing(data=lines) self.timing = { "wall_time": wall_time, "user_time": user_time, "sys_time": sys_time, } - warninglines = self._get_all_warning_lines(data=data) + warninglines = self._get_all_warning_lines(data=lines) self.warning_lines = warninglines - orthowarning = self._get_warning_orthonormalization(data=data) + orthowarning = self._get_warning_orthonormalization(data=lines) self.info_orthonormalization = orthowarning - infos = self._get_all_info_lines(data=data) + infos = self._get_all_info_lines(data=lines) self.info_lines = infos - self.has_doscar = "writing DOSCAR.lobster..." in data and "SKIPPING writing DOSCAR.lobster..." not in data + self.has_doscar = "writing DOSCAR.lobster..." in lines and "SKIPPING writing DOSCAR.lobster..." not in lines self.has_doscar_lso = ( - "writing DOSCAR.LSO.lobster..." in data and "SKIPPING writing DOSCAR.LSO.lobster..." not in data + "writing DOSCAR.LSO.lobster..." in lines and "SKIPPING writing DOSCAR.LSO.lobster..." not in lines ) self.has_cohpcar = ( - "writing COOPCAR.lobster and ICOOPLIST.lobster..." in data - and "SKIPPING writing COOPCAR.lobster and ICOOPLIST.lobster..." not in data + "writing COOPCAR.lobster and ICOOPLIST.lobster..." in lines + and "SKIPPING writing COOPCAR.lobster and ICOOPLIST.lobster..." not in lines ) self.has_coopcar = ( - "writing COHPCAR.lobster and ICOHPLIST.lobster..." in data - and "SKIPPING writing COHPCAR.lobster and ICOHPLIST.lobster..." not in data + "writing COHPCAR.lobster and ICOHPLIST.lobster..." in lines + and "SKIPPING writing COHPCAR.lobster and ICOHPLIST.lobster..." not in lines ) self.has_cobicar = ( - "writing COBICAR.lobster and ICOBILIST.lobster..." in data - and "SKIPPING writing COBICAR.lobster and ICOBILIST.lobster..." not in data + "writing COBICAR.lobster and ICOBILIST.lobster..." in lines + and "SKIPPING writing COBICAR.lobster and ICOBILIST.lobster..." not in lines ) - self.has_charge = "SKIPPING writing CHARGE.lobster..." not in data - self.has_projection = "saving projection to projectionData.lobster..." in data + self.has_charge = "SKIPPING writing CHARGE.lobster..." not in lines + self.has_projection = "saving projection to projectionData.lobster..." in lines self.has_bandoverlaps = ( - "WARNING: I dumped the band overlap matrices to the file bandOverlaps.lobster." in data + "WARNING: I dumped the band overlap matrices to the file bandOverlaps.lobster." in lines ) - self.has_fatbands = self._has_fatband(data=data) - self.has_grosspopulation = "writing CHARGE.lobster and GROSSPOP.lobster..." in data - self.has_density_of_energies = "writing DensityOfEnergy.lobster..." in data + self.has_fatbands = self._has_fatband(data=lines) + self.has_grosspopulation = "writing CHARGE.lobster and GROSSPOP.lobster..." in lines + self.has_density_of_energies = "writing DensityOfEnergy.lobster..." in lines self.has_madelung = ( - "writing SitePotentials.lobster and MadelungEnergies.lobster..." in data - and "skipping writing SitePotentials.lobster and MadelungEnergies.lobster..." not in data + "writing SitePotentials.lobster and MadelungEnergies.lobster..." in lines + and "skipping writing SitePotentials.lobster and MadelungEnergies.lobster..." not in lines ) else: raise ValueError("must provide either filename or kwargs to initialize Lobsterout") def get_doc(self) -> dict[str, Any]: - """Get a dict with all the information in lobsterout.""" + """Get a dict with all information stored in lobsterout.""" return { # Check if LOBSTER starts from a projection "restart_from_projection": self.is_restart_from_projection, @@ -1034,8 +1054,8 @@ def get_doc(self) -> dict[str, Any]: "has_density_of_energies": self.has_density_of_energies, } - def as_dict(self) -> dict: - """MSONable dict""" + def as_dict(self) -> dict[str, Any]: + """MSONable dict.""" dct = dict(vars(self)) dct["@module"] = type(self).__module__ dct["@class"] = type(self).__name__ @@ -1043,69 +1063,81 @@ def as_dict(self) -> dict: return dct @staticmethod - def _get_lobster_version(data): - for row in data: - splitrow = row.split() - if len(splitrow) > 1 and splitrow[0] == "LOBSTER": - return splitrow[1] + def _get_lobster_version(data: list[str]) -> str: + """Get LOBSTER version.""" + for line in data: + line_parts = line.split() + if len(line_parts) > 1 and line_parts[0] == "LOBSTER": + return line_parts[1] raise RuntimeError("Version not found.") @staticmethod - def _has_fatband(data): - for row in data: - splitrow = row.split() - if len(splitrow) > 1 and splitrow[1] == "FatBand": + def _has_fatband(data: list[str]) -> bool: + """Check whether calculation has hatband data.""" + for line in data: + line_parts = line.split() + if len(line_parts) > 1 and line_parts[1] == "FatBand": return True return False @staticmethod - def _get_dft_program(data): - for row in data: - splitrow = row.split() - if len(splitrow) > 4 and splitrow[3] == "program...": - return splitrow[4] + def _get_dft_program(data: list[str]) -> str | None: + """Get the DFT program used for calculation.""" + for line in data: + line_parts = line.split() + if len(line_parts) > 4 and line_parts[3] == "program...": + return line_parts[4] return None @staticmethod - def _get_number_of_spins(data): + def _get_number_of_spins(data: list[str]) -> Literal[1, 2]: + """Get index of spin channel.""" return 2 if "spillings for spin channel 2" in data else 1 @staticmethod - def _get_threads(data): - for row in data: - splitrow = row.split() - if len(splitrow) > 11 and splitrow[11] in {"threads", "thread"}: - return splitrow[10] + def _get_threads(data: list[str]) -> int: + """Get number of CPU threads.""" + for line in data: + line_parts = line.split() + if len(line_parts) > 11 and line_parts[11] in {"threads", "thread"}: + return int(line_parts[10]) raise ValueError("Threads not found.") @staticmethod - def _get_spillings(data, number_of_spins): - charge_spilling = [] - total_spilling = [] - for row in data: - splitrow = row.split() - if len(splitrow) > 2 and splitrow[2] == "spilling:": - if splitrow[1] == "charge": - charge_spilling.append(np.float64(splitrow[3].replace("%", "")) / 100.0) - if splitrow[1] == "total": - total_spilling.append(np.float64(splitrow[3].replace("%", "")) / 100.0) - - if len(charge_spilling) == number_of_spins and len(total_spilling) == number_of_spins: + def _get_spillings( + data: list[str], + number_of_spins: Literal[1, 2], + ) -> tuple[list[float], list[float]]: + """Get charge spillings and total spillings.""" + charge_spillings = [] + total_spillings = [] + for line in data: + line_parts = line.split() + if len(line_parts) > 2 and line_parts[2] == "spilling:": + if line_parts[1] == "charge": + charge_spillings.append(float(line_parts[3].replace("%", "")) / 100.0) + elif line_parts[1] == "total": + total_spillings.append(float(line_parts[3].replace("%", "")) / 100.0) + + if len(charge_spillings) == number_of_spins and len(total_spillings) == number_of_spins: break - return charge_spilling, total_spilling + return charge_spillings, total_spillings @staticmethod - def _get_elements_basistype_basisfunctions(data): + def _get_elements_basistype_basisfunctions( + data: list[str], + ) -> tuple[list[str], list[str], list[list[str]]]: + """Get elements, basis types and basis functions.""" begin = False end = False - elements = [] - basistype = [] - basisfunctions = [] - for row in data: + elements: list[str] = [] + basistypes: list[str] = [] + basisfunctions: list[list[str]] = [] + for line in data: if begin and not end: - row_parts = row.split() - if row_parts[0] not in { + line_parts = line.split() + if line_parts[0] not in { "INFO:", "WARNING:", "setting", @@ -1115,107 +1147,112 @@ def _get_elements_basistype_basisfunctions(data): "spillings", "writing", }: - elements.append(row_parts[0]) - basistype.append(row_parts[1].replace("(", "").replace(")", "")) - # last sign is a '' - basisfunctions += [row_parts[2:]] + elements.append(line_parts[0]) + basistypes.append(line_parts[1].replace("(", "").replace(")", "")) + # Last sign is '' + basisfunctions.append(line_parts[2:]) else: end = True - if "setting up local basis functions..." in row: + + if "setting up local basis functions..." in line: begin = True - return elements, basistype, basisfunctions + return elements, basistypes, basisfunctions @staticmethod - def _get_timing(data): - # Will give back wall, user and sys time + def _get_timing( + data: list[str], + ) -> tuple[dict[str, str], dict[str, str], dict[str, str]]: + """Get wall time, user time and system time.""" begin = False - user_time, wall_time, sys_time = [], [], [] + user_times, wall_times, sys_times = [], [], [] - for row in data: - splitrow = row.split() - if "finished" in splitrow: + for line in data: + line_parts = line.split() + if "finished" in line_parts: begin = True if begin: - if "wall" in splitrow: - wall_time = splitrow[2:10] - if "user" in splitrow: - user_time = splitrow[:8] - if "sys" in splitrow: - sys_time = splitrow[:8] + if "wall" in line_parts: + wall_times = line_parts[2:10] + if "user" in line_parts: + user_times = line_parts[:8] + if "sys" in line_parts: + sys_times = line_parts[:8] - wall_time_dict = {"h": wall_time[0], "min": wall_time[2], "s": wall_time[4], "ms": wall_time[6]} - user_time_dict = {"h": user_time[0], "min": user_time[2], "s": user_time[4], "ms": user_time[6]} - sys_time_dict = {"h": sys_time[0], "min": sys_time[2], "s": sys_time[4], "ms": sys_time[6]} + wall_time_dict = {"h": wall_times[0], "min": wall_times[2], "s": wall_times[4], "ms": wall_times[6]} + user_time_dict = {"h": user_times[0], "min": user_times[2], "s": user_times[4], "ms": user_times[6]} + sys_time_dict = {"h": sys_times[0], "min": sys_times[2], "s": sys_times[4], "ms": sys_times[6]} return wall_time_dict, user_time_dict, sys_time_dict @staticmethod - def _get_warning_orthonormalization(data): - orthowarning = [] - for row in data: - splitrow = row.split() - if "orthonormalized" in splitrow: - orthowarning.append(" ".join(splitrow[1:])) - return orthowarning + def _get_warning_orthonormalization(data: list[str]) -> list[str]: + """Get orthonormalization warnings.""" + orthowarnings = [] + for line in data: + line_parts = line.split() + if "orthonormalized" in line_parts: + orthowarnings.append(" ".join(line_parts[1:])) + return orthowarnings @staticmethod - def _get_all_warning_lines(data): - ws = [] - for row in data: - splitrow = row.split() - if len(splitrow) > 0 and splitrow[0] == "WARNING:": - ws.append(" ".join(splitrow[1:])) - return ws + def _get_all_warning_lines(data: list[str]) -> list[str]: + """Get all WARNING lines.""" + warnings_ = [] + for line in data: + line_parts = line.split() + if len(line_parts) > 0 and line_parts[0] == "WARNING:": + warnings_.append(" ".join(line_parts[1:])) + return warnings_ @staticmethod - def _get_all_info_lines(data): + def _get_all_info_lines(data: list[str]) -> list[str]: + """Get all INFO lines.""" infos = [] - for row in data: - splitrow = row.split() - if len(splitrow) > 0 and splitrow[0] == "INFO:": - infos.append(" ".join(splitrow[1:])) + for line in data: + line_parts = line.split() + if len(line_parts) > 0 and line_parts[0] == "INFO:": + infos.append(" ".join(line_parts[1:])) return infos class Fatband: - """Read in FATBAND_x_y.lobster files. + """Read FATBAND_x_y.lobster files. Attributes: - efermi (float): Fermi energy read in from vasprun.xml. - eigenvals (dict[Spin, np.ndarray]): Eigenvalues as a dictionary of numpy arrays of shape (nbands, nkpoints). + efermi (float): Fermi level read from vasprun.xml. + eigenvals (dict[Spin, NDArray]): Eigenvalues as a dictionary of NumPy arrays of shape (nbands, nkpoints). The first index of the array refers to the band and the second to the index of the kpoint. The kpoints are ordered according to the order of the kpoints_array attribute. If the band structure is not spin polarized, we only store one data set under Spin.up. is_spin_polarized (bool): Whether this was a spin-polarized calculation. - kpoints_array (list[np.ndarray]): List of kpoints as numpy arrays, in frac_coords of the given + kpoints_array (list[NDArray]): List of kpoints as NumPy arrays, in frac_coords of the given lattice by default. - label_dict (dict[str, Union[str, np.ndarray]]): Dictionary that links a kpoint (in frac coords or Cartesian + label_dict (dict[str, Union[str, NDArray]]): Dictionary that links a kpoint (in frac coords or Cartesian coordinates depending on the coords attribute) to a label. - lattice (Lattice): Lattice object of reciprocal lattice as read in from vasprun.xml. + lattice (Lattice): Lattice object of reciprocal lattice as read from vasprun.xml. nbands (int): Number of bands used in the calculation. - p_eigenvals (dict[Spin, np.ndarray]): Dictionary of orbital projections as {spin: array of dict}. + p_eigenvals (dict[Spin, NDArray]): Dictionary of orbital projections as {spin: array of dict}. The indices of the array are [band_index, kpoint_index]. The dict is then built the following way: {"string of element": "string of orbital as read in from FATBAND file"}. If the band structure is not spin polarized, we only store one data set under Spin.up. - structure (Structure): Structure read in from Structure object. + structure (Structure): Structure object. """ def __init__( self, - filenames: PathLike | list = ".", + filenames: PathLike | list[PathLike] = ".", kpoints_file: PathLike = "KPOINTS", vasprun_file: PathLike | None = "vasprun.xml", structure: Structure | IStructure | None = None, efermi: float | None = None, - ): + ) -> None: """ Args: - filenames (list or string): can be a list of file names or a path to a folder from which all - "FATBAND_*" files will be read + filenames (PathLike | list[PathLike]): File names or path to a + folder from which all "FATBAND_*" files will be read. kpoints_file (PathLike): KPOINTS file for bandstructure calculation, typically "KPOINTS". - vasprun_file (PathLike): Corresponding vasprun file. - Instead, the Fermi level from the DFT run can be provided. Then, - this value should be set to None. + vasprun_file (PathLike): Corresponding vasprun.xml file. Instead, the + Fermi level from the DFT run can be provided. Then, this should be set to None. structure (Structure): Structure object. efermi (float): Fermi level in eV. """ @@ -1245,38 +1282,43 @@ def __init__( self.efermi = efermi kpoints_object = Kpoints.from_file(kpoints_file) - atom_type = [] + # atom_type = [] atom_names = [] orbital_names = [] parameters = [] if not isinstance(filenames, list) or filenames is None: - filenames_new = [] + filenames_new: list[str] = [] if filenames is None: filenames = "." for name in os.listdir(filenames): if fnmatch.fnmatch(name, "FATBAND_*.lobster"): filenames_new.append(os.path.join(filenames, name)) - filenames = filenames_new + filenames = filenames_new # type: ignore[assignment] + + filenames = cast(list[PathLike], filenames) + if len(filenames) == 0: raise ValueError("No FATBAND files in folder or given") - for name in filenames: - with zopen(name, mode="rt") as file: - contents = file.read().split("\n") - atom_names.append(os.path.split(name)[1].split("_")[1].capitalize()) - parameters = contents[0].split() - atom_type.append(re.split(r"[0-9]+", parameters[3])[0].capitalize()) + for fname in filenames: + with zopen(fname, mode="rt") as file: + lines = file.read().split("\n") + + atom_names.append(os.path.split(fname)[1].split("_")[1].capitalize()) + parameters = lines[0].split() + # atom_type.append(re.split(r"[0-9]+", parameters[3])[0].capitalize()) orbital_names.append(parameters[4]) - # get atomtype orbital dict - atom_orbital_dict = {} # type: dict + # Get atomtype orbital dict + atom_orbital_dict: dict[str, list[str]] = {} for idx, atom in enumerate(atom_names): if atom not in atom_orbital_dict: atom_orbital_dict[atom] = [] atom_orbital_dict[atom].append(orbital_names[idx]) - # test if there are the same orbitals twice or if two different formats were used or if all necessary orbitals - # are there + + # Test if there are the same orbitals twice or if two different + # formats were used or if all necessary orbitals are there for items in atom_orbital_dict.values(): if len(set(items)) != len(items): raise ValueError("The are two FATBAND files for the same atom and orbital. The program will stop.") @@ -1295,19 +1337,19 @@ def __init__( p_eigenvals: dict = {} for ifilename, filename in enumerate(filenames): with zopen(filename, mode="rt") as file: - contents = file.read().split("\n") + lines = file.read().split("\n") if ifilename == 0: self.nbands = int(parameters[6]) - self.number_kpts = kpoints_object.num_kpts - int(contents[1].split()[2]) + 1 + self.number_kpts = kpoints_object.num_kpts - int(lines[1].split()[2]) + 1 - if len(contents[1:]) == self.nbands + 2: + if len(lines[1:]) == self.nbands + 2: self.is_spinpolarized = False - elif len(contents[1:]) == self.nbands * 2 + 2: + elif len(lines[1:]) == self.nbands * 2 + 2: self.is_spinpolarized = True else: linenumbers = [] - for iline, line in enumerate(contents[1 : self.nbands * 2 + 4]): + for iline, line in enumerate(lines[1 : self.nbands * 2 + 4]): if line.split()[0] == "#": linenumbers.append(iline) @@ -1349,7 +1391,7 @@ def __init__( idx_kpt = -1 linenumber = 0 iband = 0 - for line in contents[1:-1]: + for line in lines[1:-1]: if line.split()[0] == "#": KPOINT = np.array( [ @@ -1410,30 +1452,34 @@ def get_bandstructure(self) -> LobsterBandStructureSymmLine: class Bandoverlaps(MSONable): - """Read in bandOverlaps.lobster files. These files are not created during every Lobster run. + """Read bandOverlaps.lobster files, which are not created during every LOBSTER run. + Attributes: - band_overlaps_dict (dict[Spin, Dict[str, Dict[str, Union[float, np.ndarray]]]]): A dictionary + band_overlaps_dict (dict[Spin, Dict[str, Dict[str, Union[float, NDArray]]]]): A dictionary containing the band overlap data of the form: {spin: {"kpoint as string": {"maxDeviation": float that describes the max deviation, "matrix": 2D array of the size number of bands times number of bands including the overlap matrices with}}}. - max_deviation (list[float]): A list of floats describing the maximal deviation for each problematic kpoint. + max_deviation (list[float]): The maximal deviation for each problematic kpoint. """ def __init__( self, - filename: str = "bandOverlaps.lobster", - band_overlaps_dict: dict[Any, dict] | None = None, # Any is spin number 1 or -1 + filename: PathLike = "bandOverlaps.lobster", + band_overlaps_dict: dict[Spin, dict] | None = None, max_deviation: list[float] | None = None, - ): + ) -> None: """ Args: - filename: filename of the "bandOverlaps.lobster" file. - band_overlaps_dict: A dictionary containing the band overlap data of the form: {spin: { - "k_points" : list of k-point array, - "max_deviations": list of max deviations associated with each k-point, - "matrices": list of the overlap matrices associated with each k-point - }}. - max_deviation (list[float]): A list of floats describing the maximal deviation for each problematic k-point. + filename (PathLike): The "bandOverlaps.lobster" file. + band_overlaps_dict: The band overlap data of the form: + { + spin: { + "k_points" : list of k-point array, + "max_deviations": list of max deviations associated with each k-point, + "matrices": list of the overlap matrices associated with each k-point, + } + }. + max_deviation (list[float]): The maximal deviations for each problematic k-point. """ self._filename = filename self.band_overlaps_dict = {} if band_overlaps_dict is None else band_overlaps_dict @@ -1441,30 +1487,32 @@ def __init__( if not self.band_overlaps_dict: with zopen(filename, mode="rt") as file: - contents = file.read().split("\n") + lines = file.read().split("\n") - spin_numbers = [0, 1] if contents[0].split()[-1] == "0" else [1, 2] + spin_numbers = [0, 1] if lines[0].split()[-1] == "0" else [1, 2] self._filename = filename - self._read(contents, spin_numbers) + self._read(lines, spin_numbers) - def _read(self, contents: list, spin_numbers: list): - """ - Will read in all contents of the file + def _read(self, lines: list[str], spin_numbers: list[int]) -> None: + """Read all lines of the file. Args: - contents: list of strings - spin_numbers: list of spin numbers depending on `Lobster` version. + lines (list[str]): Lines of the file. + spin_numbers (list[int]): Spin numbers depending on LOBSTER version. """ spin: Spin = Spin.up kpoint_array: list = [] overlaps: list = [] - # This has to be done like this because there can be different numbers of problematic k-points per spin - for line in contents: + # This has to be done like this because there can be different numbers + # of problematic k-points per spin + for line in lines: if f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[0]}" in line: spin = Spin.up + elif f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[1]}" in line: spin = Spin.down + elif "k-point" in line: kpoint = line.split(" ") kpoint_array = [] @@ -1481,29 +1529,31 @@ def _read(self, contents: list, spin_numbers: list): self.band_overlaps_dict[spin]["max_deviations"] = [] if "matrices" not in self.band_overlaps_dict[spin]: self.band_overlaps_dict[spin]["matrices"] = [] + maxdev = line.split(" ")[2] self.band_overlaps_dict[spin]["max_deviations"].append(float(maxdev)) - self.band_overlaps_dict[spin]["k_points"] += [kpoint_array] + self.band_overlaps_dict[spin]["k_points"].append(kpoint_array) self.max_deviation.append(float(maxdev)) overlaps = [] else: - rows = [] + _lines = [] for el in line.split(" "): if el != "": - rows.append(float(el)) - overlaps += [rows] - if len(overlaps) == len(rows): - self.band_overlaps_dict[spin]["matrices"] += [np.matrix(overlaps)] + _lines.append(float(el)) + overlaps.append(_lines) + if len(overlaps) == len(_lines): + self.band_overlaps_dict[spin]["matrices"].append(np.matrix(overlaps)) def has_good_quality_maxDeviation(self, limit_maxDeviation: float = 0.1) -> bool: - """Will check if the maxDeviation from the ideal bandoverlap is smaller or equal to limit_maxDeviation + """Check if the maxDeviation from the ideal bandoverlap is smaller + or equal to a limit. Args: - limit_maxDeviation: limit of the maxDeviation + limit_maxDeviation (float): Upper Limit of the maxDeviation. Returns: - bool: True if the quality of the projection is good. + bool: Whether the ideal bandoverlap is smaller or equal to the limit. """ return all(deviation <= limit_maxDeviation for deviation in self.max_deviation) @@ -1514,15 +1564,14 @@ def has_good_quality_check_occupied_bands( spin_polarized: bool = False, limit_deviation: float = 0.1, ) -> bool: - """ - Will check if the deviation from the ideal bandoverlap of all occupied bands + """Check if the deviation from the ideal bandoverlap of all occupied bands is smaller or equal to limit_deviation. Args: - number_occ_bands_spin_up (int): number of occupied bands of spin up - number_occ_bands_spin_down (int): number of occupied bands of spin down - spin_polarized (bool): If True, then it was a spin polarized calculation - limit_deviation (float): limit of the maxDeviation + number_occ_bands_spin_up (int): Number of occupied bands of spin up. + number_occ_bands_spin_down (int): Number of occupied bands of spin down. + spin_polarized (bool): Whether this is a spin polarized calculation. + limit_deviation (float): Upper limit of the maxDeviation. Returns: bool: True if the quality of the projection is good. @@ -1554,14 +1603,13 @@ def has_good_quality_check_occupied_bands( return True @property - def bandoverlapsdict(self): - msg = "`bandoverlapsdict` attribute is deprecated. Use `band_overlaps_dict` instead." - warnings.warn(msg, DeprecationWarning, stacklevel=2) + @deprecated(message="Use `band_overlaps_dict` instead.", category=DeprecationWarning) + def bandoverlapsdict(self) -> dict: return self.band_overlaps_dict class Grosspop(MSONable): - """Read in GROSSPOP.lobster files. + """Read GROSSPOP.lobster files. Attributes: list_dict_grosspop (list[dict[str, str| dict[str, str]]]): List of dictionaries @@ -1574,109 +1622,126 @@ class Grosspop(MSONable): The 0th entry of the list refers to the first atom in GROSSPOP.lobster and so on. """ - def __init__(self, filename: str = "GROSSPOP.lobster", list_dict_grosspop: list[dict] | None = None): + def __init__( + self, + filename: PathLike = "GROSSPOP.lobster", + list_dict_grosspop: list[dict] | None = None, + ) -> None: """ Args: - filename: filename of the "GROSSPOP.lobster" file - list_dict_grosspop: List of dictionaries including all information about the gross populations + filename (PathLike): The "GROSSPOP.lobster" file. + list_dict_grosspop (list[dict]): All information about the gross populations. """ - # opens file self._filename = filename self.list_dict_grosspop = [] if list_dict_grosspop is None else list_dict_grosspop if not self.list_dict_grosspop: with zopen(filename, mode="rt") as file: - contents = file.read().split("\n") - # transfers content of file to list of dict + lines = file.read().split("\n") + + # Read file to list of dict small_dict: dict[str, Any] = {} - for line in contents[3:]: - cleanline = [i for i in line.split(" ") if i != ""] - if len(cleanline) == 5: - small_dict = {} - small_dict["Mulliken GP"] = {} - small_dict["Loewdin GP"] = {} - small_dict["element"] = cleanline[1] - small_dict["Mulliken GP"][cleanline[2]] = float(cleanline[3]) - small_dict["Loewdin GP"][cleanline[2]] = float(cleanline[4]) - elif len(cleanline) > 0: - small_dict["Mulliken GP"][cleanline[0]] = float(cleanline[1]) - small_dict["Loewdin GP"][cleanline[0]] = float(cleanline[2]) - if "total" in cleanline[0]: + for line in lines[3:]: + cleanlines = [idx for idx in line.split(" ") if idx != ""] + if len(cleanlines) == 5: + small_dict = {"Mulliken GP": {}, "Loewdin GP": {}, "element": cleanlines[1]} + small_dict["Mulliken GP"][cleanlines[2]] = float(cleanlines[3]) + small_dict["Loewdin GP"][cleanlines[2]] = float(cleanlines[4]) + + elif len(cleanlines) > 0: + small_dict["Mulliken GP"][cleanlines[0]] = float(cleanlines[1]) + small_dict["Loewdin GP"][cleanlines[0]] = float(cleanlines[2]) + if "total" in cleanlines[0]: self.list_dict_grosspop.append(small_dict) - def get_structure_with_total_grosspop(self, structure_filename: str) -> Structure: - """Get a Structure with Mulliken and Loewdin total grosspopulations as site properties + def get_structure_with_total_grosspop(self, structure_filename: PathLike) -> Structure: + """Get a Structure with Mulliken and Loewdin total grosspopulations as site properties. Args: - structure_filename (str): filename of POSCAR + structure_filename (PathLike): The POSCAR file. Returns: Structure Object with Mulliken and Loewdin total grosspopulations as site properties. """ struct = Structure.from_file(structure_filename) - mullikengp = [] - loewdingp = [] + mulliken_gps: list[dict] = [] + loewdin_gps: list[dict] = [] for grosspop in self.list_dict_grosspop: - mullikengp += [grosspop["Mulliken GP"]["total"]] - loewdingp += [grosspop["Loewdin GP"]["total"]] + mulliken_gps.append(grosspop["Mulliken GP"]["total"]) + loewdin_gps.append(grosspop["Loewdin GP"]["total"]) site_properties = { - "Total Mulliken GP": mullikengp, - "Total Loewdin GP": loewdingp, + "Total Mulliken GP": mulliken_gps, + "Total Loewdin GP": loewdin_gps, } return struct.copy(site_properties=site_properties) class Wavefunction: - """Read in wave function files from Lobster and transfer them into an object of the type VolumetricData. + """Read wave function files from LOBSTER and create an VolumetricData object. Attributes: - grid (tuple[int, int, int]): Grid for the wave function [Nx+1,Ny+1,Nz+1]. - points (list[Tuple[float, float, float]]): List of points. - real (list[float]): List of real part of wave function. - imaginary (list[float]): List of imaginary part of wave function. - distance (list[float]): List of distance to first point in wave function file. + grid (tuple[int, int, int]): Grid for the wave function [Nx+1, Ny+1, Nz+1]. + points (list[Tuple[float, float, float]]): Points. + real (list[float]): Real parts of wave function. + imaginary (list[float]): Imaginary parts of wave function. + distance (list[float]): Distances to the first point in wave function file. """ - def __init__(self, filename, structure): + def __init__(self, filename: PathLike, structure: Structure) -> None: """ Args: - filename: filename of wavecar file from Lobster - structure: Structure object (e.g., created by Structure.from_file("")). + filename (PathLike): The wavecar file from LOBSTER. + structure (Structure): The Structure object. """ self.filename = filename self.structure = structure self.grid, self.points, self.real, self.imaginary, self.distance = Wavefunction._parse_file(filename) @staticmethod - def _parse_file(filename): + def _parse_file( + filename: PathLike, + ) -> tuple[Tuple3Ints, list[Vector3D], list[float], list[float], list[float]]: + """Parse wave function file. + + Args: + filename (PathLike): The file to parse. + + Returns: + grid (tuple[int, int, int]): Grid for the wave function [Nx+1, Ny+1, Nz+1]. + points (list[Tuple[float, float, float]]): Points. + real (list[float]): Real parts of wave function. + imaginary (list[float]): Imaginary parts of wave function. + distance (list[float]): Distances to the first point in wave function file. + """ with zopen(filename, mode="rt") as file: - contents = file.read().split("\n") + lines = file.read().split("\n") + points = [] - distance = [] - real = [] - imaginary = [] - splitline = contents[0].split() - grid = [int(splitline[7]), int(splitline[8]), int(splitline[9])] - for line in contents[1:]: - splitline = line.split() - if len(splitline) >= 6: - points += [[float(splitline[0]), float(splitline[1]), float(splitline[2])]] - distance.append(float(splitline[3])) - real.append(float(splitline[4])) - imaginary.append(float(splitline[5])) - - if len(real) != grid[0] * grid[1] * grid[2] or len(imaginary) != grid[0] * grid[1] * grid[2]: + distances = [] + reals = [] + imaginaries = [] + line_parts = lines[0].split() + grid: Tuple3Ints = (int(line_parts[7]), int(line_parts[8]), int(line_parts[9])) + + for line in lines[1:]: + line_parts = line.split() + if len(line_parts) >= 6: + points.append((float(line_parts[0]), float(line_parts[1]), float(line_parts[2]))) + distances.append(float(line_parts[3])) + reals.append(float(line_parts[4])) + imaginaries.append(float(line_parts[5])) + + if len(reals) != grid[0] * grid[1] * grid[2] or len(imaginaries) != grid[0] * grid[1] * grid[2]: raise ValueError("Something went wrong while reading the file") - return grid, points, real, imaginary, distance + return grid, points, reals, imaginaries, distances - def set_volumetric_data(self, grid, structure): - """ - Will create the VolumetricData Objects. + def set_volumetric_data(self, grid: Tuple3Ints, structure: Structure) -> None: + """Create the VolumetricData instances. Args: - grid: grid on which wavefunction was calculated, e.g. [1,2,2] - structure: Structure object + grid (tuple[int, int, int]): Grid on which wavefunction was calculated, e.g. (1, 2, 2). + structure (Structure): The Structure object. """ Nx = grid[0] - 1 Ny = grid[1] - 1 @@ -1713,9 +1778,9 @@ def set_volumetric_data(self, grid, structure): new_y.append(y_here) new_z.append(z_here) - new_real += [self.real[runner]] - new_imaginary += [self.imaginary[runner]] - new_density += [self.real[runner] ** 2 + self.imaginary[runner] ** 2] + new_real.append(self.real[runner]) + new_imaginary.append(self.imaginary[runner]) + new_density.append(self.real[runner] ** 2 + self.imaginary[runner] ** 2) self.final_real = np.reshape(new_real, [Nx, Ny, Nz]) self.final_imaginary = np.reshape(new_imaginary, [Nx, Ny, Nz]) @@ -1725,9 +1790,8 @@ def set_volumetric_data(self, grid, structure): self.volumetricdata_imaginary = VolumetricData(structure, {"total": self.final_imaginary}) self.volumetricdata_density = VolumetricData(structure, {"total": self.final_density}) - def get_volumetricdata_real(self): - """ - Will return a VolumetricData object including the real part of the wave function. + def get_volumetricdata_real(self) -> VolumetricData: + """Get a VolumetricData object including the real part of the wave function. Returns: VolumetricData @@ -1736,9 +1800,8 @@ def get_volumetricdata_real(self): self.set_volumetric_data(self.grid, self.structure) return self.volumetricdata_real - def get_volumetricdata_imaginary(self): - """ - Will return a VolumetricData object including the imaginary part of the wave function. + def get_volumetricdata_imaginary(self) -> VolumetricData: + """Get a VolumetricData object including the imaginary part of the wave function. Returns: VolumetricData @@ -1747,9 +1810,8 @@ def get_volumetricdata_imaginary(self): self.set_volumetric_data(self.grid, self.structure) return self.volumetricdata_imaginary - def get_volumetricdata_density(self): - """ - Will return a VolumetricData object including the imaginary part of the wave function. + def get_volumetricdata_density(self) -> VolumetricData: + """Get a VolumetricData object including the density part of the wave function. Returns: VolumetricData @@ -1758,17 +1820,21 @@ def get_volumetricdata_density(self): self.set_volumetric_data(self.grid, self.structure) return self.volumetricdata_density - def write_file(self, filename="WAVECAR.vasp", part="real"): - """ - Will save the wavefunction in a file format that can be read by VESTA - This will only work if the wavefunction from lobster was constructed with: - "printLCAORealSpaceWavefunction kpoint 1 coordinates 0.0 0.0 0.0 coordinates 1.0 1.0 1.0 box bandlist 1 2 3 4 - 5 6 " - or similar (the whole unit cell has to be covered!). + def write_file( + self, + filename: PathLike = "WAVECAR.vasp", + part: Literal["real", "imaginary", "density"] = "real", + ) -> None: + """Save the wave function in a file that can be read by VESTA. + + This will only work if the wavefunction from lobster is constructed with: + "printLCAORealSpaceWavefunction kpoint 1 coordinates 0.0 0.0 0.0 + coordinates 1.0 1.0 1.0 box bandlist 1 2 3 4 5 6 " + or similar (the whole unit cell has to be covered!). Args: - filename: Filename for the output, e.g. WAVECAR.vasp - part: which part of the wavefunction will be saved ("real" or "imaginary") + filename (PathLike): The output file, e.g. "WAVECAR.vasp". + part ("real" | "imaginary" | "density"]): Part of the wavefunction to save. """ if not ( hasattr(self, "volumetricdata_real") @@ -1776,6 +1842,7 @@ def write_file(self, filename="WAVECAR.vasp", part="real"): and hasattr(self, "volumetricdata_density") ): self.set_volumetric_data(self.grid, self.structure) + if part == "real": self.volumetricdata_real.write_file(filename) elif part == "imaginary": @@ -1786,26 +1853,29 @@ def write_file(self, filename="WAVECAR.vasp", part="real"): raise ValueError('part can be only "real" or "imaginary" or "density"') -# madelung and site potential classes +# Madelung and site potential classes class MadelungEnergies(MSONable): """Read MadelungEnergies.lobster files generated by LOBSTER. Attributes: - madelungenergies_mulliken (float): Float that gives the Madelung energy based on the Mulliken approach. - madelungenergies_loewdin (float): Float that gives the Madelung energy based on the Loewdin approach. - ewald_splitting (float): Ewald splitting parameter to compute SitePotentials. + madelungenergies_mulliken (float): The Madelung energy based on the Mulliken approach. + madelungenergies_loewdin (float): The Madelung energy based on the Loewdin approach. + ewald_splitting (float): The Ewald splitting parameter to compute SitePotentials. """ def __init__( self, - filename: str = "MadelungEnergies.lobster", + filename: PathLike = "MadelungEnergies.lobster", ewald_splitting: float | None = None, madelungenergies_mulliken: float | None = None, madelungenergies_loewdin: float | None = None, - ): + ) -> None: """ Args: - filename: filename of the "MadelungEnergies.lobster" file. + filename (PathLike): The "MadelungEnergies.lobster" file. + ewald_splitting (float): The Ewald splitting parameter to compute SitePotentials. + madelungenergies_mulliken (float): The Madelung energy based on the Mulliken approach. + madelungenergies_loewdin (float): The Madelung energy based on the Loewdin approach. """ self._filename = filename self.ewald_splitting = None if ewald_splitting is None else ewald_splitting @@ -1814,31 +1884,24 @@ def __init__( if self.ewald_splitting is None: with zopen(filename, mode="rt") as file: - data = file.read().split("\n")[5] - if len(data) == 0: + lines = file.read().split("\n")[5] + if len(lines) == 0: raise RuntimeError("MadelungEnergies file contains no data.") - line = data.split() + + line_parts = lines.split() self._filename = filename - self.ewald_splitting = float(line[0]) - self.madelungenergies_mulliken = float(line[1]) - self.madelungenergies_loewdin = float(line[2]) + self.ewald_splitting = float(line_parts[0]) + self.madelungenergies_mulliken = float(line_parts[1]) + self.madelungenergies_loewdin = float(line_parts[2]) @property - def madelungenergies_Loewdin(self): - warnings.warn( - "`madelungenergies_Loewdin` attribute is deprecated. Use `madelungenergies_loewdin` instead.", - DeprecationWarning, - stacklevel=2, - ) + @deprecated(message="Use `madelungenergies_loewdin` instead.", category=DeprecationWarning) + def madelungenergies_Loewdin(self) -> float | None: return self.madelungenergies_loewdin @property - def madelungenergies_Mulliken(self): - warnings.warn( - "`madelungenergies_Mulliken` attribute is deprecated. Use `madelungenergies_mulliken` instead.", - DeprecationWarning, - stacklevel=2, - ) + @deprecated(message="Use `madelungenergies_mulliken` instead.", category=DeprecationWarning) + def madelungenergies_Mulliken(self) -> float | None: return self.madelungenergies_mulliken @@ -1846,19 +1909,19 @@ class SitePotential(MSONable): """Read SitePotentials.lobster files generated by LOBSTER. Attributes: - atomlist (list[str]): List of atoms in SitePotentials.lobster. - types (list[str]): List of types of atoms in SitePotentials.lobster. + atomlist (list[str]): Atoms in SitePotentials.lobster. + types (list[str]): Types of atoms in SitePotentials.lobster. num_atoms (int): Number of atoms in SitePotentials.lobster. - sitepotentials_mulliken (list[float]): List of Mulliken potentials of sites in SitePotentials.lobster. - sitepotentials_loewdin (list[float]): List of Loewdin potentials of sites in SitePotentials.lobster. - madelungenergies_mulliken (float): Float that gives the Madelung energy based on the Mulliken approach. - madelungenergies_loewdin (float): Float that gives the Madelung energy based on the Loewdin approach. - ewald_splitting (float): Ewald Splitting parameter to compute SitePotentials. + sitepotentials_mulliken (list[float]): Mulliken potentials of sites in SitePotentials.lobster. + sitepotentials_loewdin (list[float]): Loewdin potentials of sites in SitePotentials.lobster. + madelungenergies_mulliken (float): The Madelung energy based on the Mulliken approach. + madelungenergies_loewdin (float): The Madelung energy based on the Loewdin approach. + ewald_splitting (float): The Ewald Splitting parameter to compute SitePotentials. """ def __init__( self, - filename: str = "SitePotentials.lobster", + filename: PathLike = "SitePotentials.lobster", ewald_splitting: float | None = None, num_atoms: int | None = None, atomlist: list[str] | None = None, @@ -1867,18 +1930,18 @@ def __init__( sitepotentials_mulliken: list[float] | None = None, madelungenergies_mulliken: float | None = None, madelungenergies_loewdin: float | None = None, - ): + ) -> None: """ Args: - filename: filename for the SitePotentials file, typically "SitePotentials.lobster" - ewald_splitting: ewald splitting parameter used for computing madelung energies - num_atoms: number of atoms in the structure - atomlist: list of atoms in the structure - types: list of unique atom types in the structure - sitepotentials_loewdin: Loewdin site potential - sitepotentials_mulliken: Mulliken site potential - madelungenergies_loewdin: Madelung energy based on the Loewdin approach - madelungenergies_mulliken: Madelung energy based on the Mulliken approach + filename (PathLike): The SitePotentials file, typically "SitePotentials.lobster". + ewald_splitting (float): Ewald splitting parameter used for computing Madelung energies. + num_atoms (int): Number of atoms in the structure. + atomlist (list[str]): Atoms in the structure. + types (list[str]): Unique atom types in the structure. + sitepotentials_loewdin (list[float]): Loewdin site potentials. + sitepotentials_mulliken (list[float]): Mulliken site potentials. + madelungenergies_mulliken (float): Madelung energy based on the Mulliken approach. + madelungenergies_loewdin (float): Madelung energy based on the Loewdin approach. """ self._filename = filename self.ewald_splitting = [] if ewald_splitting is None else ewald_splitting @@ -1891,32 +1954,31 @@ def __init__( self.madelungenergies_mulliken = [] if madelungenergies_mulliken is None else madelungenergies_mulliken if self.num_atoms is None: - # site_potentials with zopen(filename, mode="rt") as file: - data = file.read().split("\n") - if len(data) == 0: + lines = file.read().split("\n") + if len(lines) == 0: raise RuntimeError("SitePotentials file contains no data.") self._filename = filename - self.ewald_splitting = float(data[0].split()[9]) + self.ewald_splitting = float(lines[0].split()[9]) - data = data[5:-1] - self.num_atoms = len(data) - 2 + lines = lines[5:-1] + self.num_atoms = len(lines) - 2 for atom in range(self.num_atoms): - line_parts = data[atom].split() - self.atomlist.append(line_parts[1] + str(line_parts[0])) + line_parts = lines[atom].split() + self.atomlist.append(line_parts[1] + line_parts[0]) self.types.append(line_parts[1]) self.sitepotentials_mulliken.append(float(line_parts[2])) self.sitepotentials_loewdin.append(float(line_parts[3])) - self.madelungenergies_mulliken = float(data[self.num_atoms + 1].split()[3]) - self.madelungenergies_loewdin = float(data[self.num_atoms + 1].split()[4]) + self.madelungenergies_mulliken = float(lines[self.num_atoms + 1].split()[3]) + self.madelungenergies_loewdin = float(lines[self.num_atoms + 1].split()[4]) - def get_structure_with_site_potentials(self, structure_filename): - """Get a Structure with Mulliken and Loewdin charges as site properties + def get_structure_with_site_potentials(self, structure_filename: PathLike) -> Structure: + """Get a Structure with Mulliken and Loewdin charges as site properties. Args: - structure_filename: filename of POSCAR + structure_filename (PathLike): The POSCAR file. Returns: Structure Object with Mulliken and Loewdin charges as site properties. @@ -1931,51 +1993,36 @@ def get_structure_with_site_potentials(self, structure_filename): return struct.copy(site_properties=site_properties) @property - def sitepotentials_Mulliken(self): - warnings.warn( - "`sitepotentials_Mulliken` attribute is deprecated. Use `sitepotentials_mulliken` instead.", - DeprecationWarning, - stacklevel=2, - ) + @deprecated(message="Use `sitepotentials_mulliken` instead.", category=DeprecationWarning) + def sitepotentials_Mulliken(self) -> list[float]: return self.sitepotentials_mulliken @property - def sitepotentials_Loewdin(self): - warnings.warn( - "`sitepotentials_Loewdin` attribute is deprecated. Use `sitepotentials_loewdin` instead.", - DeprecationWarning, - stacklevel=2, - ) + @deprecated(message="Use `sitepotentials_loewdin` instead.", category=DeprecationWarning) + def sitepotentials_Loewdin(self) -> list[float]: return self.sitepotentials_loewdin @property + @deprecated(message="Use `madelungenergies_mulliken` instead.", category=DeprecationWarning) def madelungenergies_Mulliken(self): - warnings.warn( - "`madelungenergies_Mulliken` attribute is deprecated. Use `madelungenergies_mulliken` instead.", - DeprecationWarning, - stacklevel=2, - ) return self.madelungenergies_mulliken @property + @deprecated(message="Use `madelungenergies_loewdin` instead.", category=DeprecationWarning) def madelungenergies_Loewdin(self): - warnings.warn( - "`madelungenergies_Loewdin` attribute is deprecated. Use `madelungenergies_loewdin` instead.", - DeprecationWarning, - stacklevel=2, - ) return self.madelungenergies_loewdin -def get_orb_from_str(orbs): - """ +def get_orb_from_str(orbs: list[str]) -> tuple[str, list[tuple[int, Orbital]]]: + """Get Orbitals from string representations. + Args: - orbs: list of two or more str, e.g. ["2p_x", "3s"]. + orbs (list[str]): Orbitals, e.g. ["2p_x", "3s"]. Returns: - list of tw Orbital objects + tuple[str, list[tuple[int, Orbital]]]: Orbital label, orbitals. """ - # TODO: also useful for plotting of DOS + # TODO: also use for plotting of DOS orb_labs = ( "s", "p_y", @@ -2010,52 +2057,59 @@ class LobsterMatrices: """Read Matrices file generated by LOBSTER (e.g. hamiltonMatrices.lobster). Attributes: - for filename == "hamiltonMatrices.lobster" - onsite_energies (list[np.arrays]): List real part of onsite energies from the matrices each k-point. - average_onsite_energies (dict): dict with average onsite elements energies for all k-points with keys as - basis used in the LOBSTER computation (uses only real part of matrix). - hamilton_matrices (dict[np.arrays]) : dict with the complex hamilton matrix - at each k-point with k-point and spin as keys - - for filename == "coefficientMatrices.lobster" - - onsite_coefficients (list[np.arrays]): List real part of onsite coefficients from the matrices each k-point. - average_onsite_coefficient (dict): dict with average onsite elements coefficients for all k-points with keys as - basis used in the LOBSTER computation (uses only real part of matrix). - coefficient_matrices (dict[np.arrays]) : dict with the coefficients matrix - at each k-point with k-point and spin as keys - - for filename == "transferMatrices.lobster" - - onsite_transfer (list[np.arrays]): List real part of onsite transfer coefficients from the matrices at each - k-point. - average_onsite_transfer (dict): dict with average onsite elements transfer coefficients for all k-points with - keys as basis used in the LOBSTER computation (uses only real part of matrix). - transfer_matrices (dict[np.arrays]) : dict with the coefficients matrix at - each k-point with k-point and spin as keys - - for filename == "overlapMatrices.lobster" - - onsite_overlaps (list[np.arrays]): List real part of onsite overlaps from the matrices each k-point. - average_onsite_overlaps (dict): dict with average onsite elements overlaps for all k-points with keys as - basis used in the LOBSTER computation (uses only real part of matrix). - overlap_matrices (dict[np.arrays]) : dict with the overlap matrix at - each k-point with k-point as keys + If filename == "hamiltonMatrices.lobster": + onsite_energies (list[NDArray]): Real parts of onsite energies from the + matrices each k-point. + average_onsite_energies (dict): Average onsite elements energies for + all k-points with keys as basis used in the LOBSTER computation + (uses only real part of matrix). + hamilton_matrices (dict[Spin, NDArray]): The complex Hamilton matrix at each + k-point with k-point and spin as keys. + + If filename == "coefficientMatrices.lobster": + onsite_coefficients (list[NDArray]): Real parts of onsite coefficients + from the matrices each k-point. + average_onsite_coefficient (dict): Average onsite elements coefficients + for all k-points with keys as basis used in the LOBSTER computation + (uses only real part of matrix). + coefficient_matrices (dict[Spin, NDArray]): The coefficients matrix + at each k-point with k-point and spin as keys. + + If filename == "transferMatrices.lobster": + onsite_transfer (list[NDArray]): Real parts of onsite transfer + coefficients from the matrices at each k-point. + average_onsite_transfer (dict): Average onsite elements transfer + coefficients for all k-points with keys as basis used in the + LOBSTER computation (uses only real part of matrix). + transfer_matrices (dict[Spin, NDArray]): The coefficients matrix at + each k-point with k-point and spin as keys. + + If filename == "overlapMatrices.lobster": + onsite_overlaps (list[NDArray]): Real parts of onsite overlaps + from the matrices each k-point. + average_onsite_overlaps (dict): Average onsite elements overlaps + for all k-points with keys as basis used in the LOBSTER + computation (uses only real part of matrix). + overlap_matrices (dict[NDArray]): The overlap matrix at + each k-point with k-point as keys. """ - def __init__(self, e_fermi=None, filename: str = "hamiltonMatrices.lobster"): + def __init__( + self, + e_fermi: float | None = None, + filename: PathLike = "hamiltonMatrices.lobster", + ) -> None: """ Args: - filename: filename for the hamiltonMatrices file, typically "hamiltonMatrices.lobster". - e_fermi: fermi level in eV for the structure only - relevant if input file contains hamilton matrices data + e_fermi (float): Fermi level in eV for the structure only. + Relevant if input file contains Hamilton matrices data. + filename (PathLike): The hamiltonMatrices file, typically "hamiltonMatrices.lobster". """ - self._filename = filename - # hamiltonMatrices + self._filename = str(filename) with zopen(self._filename, mode="rt") as file: - file_data = file.readlines() - if len(file_data) == 0: + lines = file.readlines() + if len(lines) == 0: raise RuntimeError("Please check provided input file, it seems to be empty") pattern_coeff_hamil_trans = r"(\d+)\s+kpoint\s+(\d+)" # regex pattern to extract spin and k-point number @@ -2065,33 +2119,37 @@ def __init__(self, e_fermi=None, filename: str = "hamiltonMatrices.lobster"): if e_fermi is None: raise ValueError("Please provide the fermi energy in eV ") self.onsite_energies, self.average_onsite_energies, self.hamilton_matrices = self._parse_matrix( - file_data=file_data, pattern=pattern_coeff_hamil_trans, e_fermi=e_fermi + file_data=lines, pattern=pattern_coeff_hamil_trans, e_fermi=e_fermi ) elif "coefficient" in self._filename: self.onsite_coefficients, self.average_onsite_coefficient, self.coefficient_matrices = self._parse_matrix( - file_data=file_data, pattern=pattern_coeff_hamil_trans, e_fermi=0 + file_data=lines, pattern=pattern_coeff_hamil_trans, e_fermi=0 ) elif "transfer" in self._filename: self.onsite_transfer, self.average_onsite_transfer, self.transfer_matrices = self._parse_matrix( - file_data=file_data, pattern=pattern_coeff_hamil_trans, e_fermi=0 + file_data=lines, pattern=pattern_coeff_hamil_trans, e_fermi=0 ) elif "overlap" in self._filename: self.onsite_overlaps, self.average_onsite_overlaps, self.overlap_matrices = self._parse_matrix( - file_data=file_data, pattern=pattern_overlap, e_fermi=0 + file_data=lines, pattern=pattern_overlap, e_fermi=0 ) @staticmethod - def _parse_matrix(file_data, pattern, e_fermi): - complex_matrices = {} + def _parse_matrix( + file_data: list[str], + pattern: str, + e_fermi: float, + ) -> tuple[list[float], dict, dict]: + complex_matrices: dict = {} matrix_diagonal_values = [] start_inxs_real = [] end_inxs_real = [] start_inxs_imag = [] end_inxs_imag = [] - # get indices of real and imaginary part of matrix for each k point + # Get indices of real and imaginary part of matrix for each k point for idx, line in enumerate(file_data): line = line.strip() if "Real parts" in line: @@ -2100,32 +2158,34 @@ def _parse_matrix(file_data, pattern, e_fermi): pass else: end_inxs_imag.append(idx - 1) + matches = re.search(pattern, file_data[idx - 1]) if matches and len(matches.groups()) == 2: - k_point = matches.group(2) - complex_matrices[k_point] = {} + complex_matrices[matches[2]] = {} + if "Imag parts" in line: end_inxs_real.append(idx - 1) start_inxs_imag.append(idx + 1) - # explicitly add the last line as files end with imaginary matrix + + # Explicitly add the last line as files end with imaginary matrix if idx == len(file_data) - 1: end_inxs_imag.append(len(file_data)) - # extract matrix data and store diagonal elements + # Extract matrix data and store diagonal elements matrix_real = [] matrix_imag = [] for start_inx_real, end_inx_real, start_inx_imag, end_inx_imag in zip( start_inxs_real, end_inxs_real, start_inxs_imag, end_inxs_imag, strict=True ): - # matrix with text headers + # Matrix with text headers matrix_real = file_data[start_inx_real:end_inx_real] matrix_imag = file_data[start_inx_imag:end_inx_imag] - # extract only numerical data and convert to numpy arrays + # Extract only numerical data and convert to NumPy arrays matrix_array_real = np.array([line.split()[1:] for line in matrix_real[1:]], dtype=float) matrix_array_imag = np.array([line.split()[1:] for line in matrix_imag[1:]], dtype=float) - # combine real and imaginary parts to create a complex matrix + # Combine real and imaginary parts to create a complex matrix comp_matrix = matrix_array_real + 1j * matrix_array_imag matches = re.search(pattern, file_data[start_inx_real - 2]) @@ -2138,15 +2198,15 @@ def _parse_matrix(file_data, pattern, e_fermi): complex_matrices |= {k_point: comp_matrix} matrix_diagonal_values.append(comp_matrix.real.diagonal() - e_fermi) - # extract elements basis functions as list + # Extract elements basis functions as list elements_basis_functions = [ line.split()[:1][0] for line in matrix_real if line.split()[:1][0] != "basisfunction" ] - # get average row-wise + # Get average row-wise average_matrix_diagonal_values = np.array(matrix_diagonal_values, dtype=float).mean(axis=0) - # get a dict with basis functions as keys and average values as values + # Get a dict with basis functions as keys and average values as values average_average_matrix_diag_dict = dict( zip(elements_basis_functions, average_matrix_diagonal_values, strict=True) ) From f775324e34adc5a4b32c2d3a124e1e41e56a5058 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) Yang" Date: Fri, 30 Aug 2024 13:59:36 +0800 Subject: [PATCH 070/180] Improve type annotations for `io.lobster.{lobsterenv/outputs}` (#3887) * temp save of cleaning lobsterenv * fix type errors in lobsterenv * temp save of lobster.outputs * revert chemenv additional_condition rename * first go of lobster.outputs * fix typo in get_doc * separate lobster inputs and outputs tests * fix unit test * more var name and comment cleanups * fix my errors (some unit tests fail) * fix some unit test * fix unit test * Need Confirm: change `np.float64` to float for spilling * clarify data form in docstring * use deprecated decorator * pre-commit auto-fixes * pre-commit auto-fixes * remove DEBUG tag and var name tweaks * add more specific types * use PeriodicSite over Site * standardize idx var name * limit matplotlib version * remove matplotlib pin * remove left out matplotlib pin * remove ignore override tag * pre-commit auto-fixes * Apply suggestions from code review Signed-off-by: J. George * use specific type for get_nn_info return * add TODO for a very likely TODO comment * remove implemented TODO tag * replace Literal with | in docstring * use atom{idx}_list over atom{idx}s * revert bool description --------- Signed-off-by: J. George Co-authored-by: J. George --- tests/io/lobster/test_inputs.py | 2049 +---------------------------- tests/io/lobster/test_outputs.py | 2063 ++++++++++++++++++++++++++++++ 2 files changed, 2064 insertions(+), 2048 deletions(-) create mode 100644 tests/io/lobster/test_outputs.py diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 302618879c..37e4e957e1 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -1,35 +1,12 @@ from __future__ import annotations -import json -import os -from unittest import TestCase - import numpy as np import pytest -from numpy.testing import assert_allclose, assert_array_equal from pytest import approx from pymatgen.core.structure import Structure -from pymatgen.electronic_structure.cohp import IcohpCollection -from pymatgen.electronic_structure.core import Orbital, Spin -from pymatgen.io.lobster import ( - Bandoverlaps, - Charge, - Cohpcar, - Doscar, - Fatband, - Grosspop, - Icohplist, - Lobsterin, - LobsterMatrices, - Lobsterout, - MadelungEnergies, - NciCobiList, - SitePotential, - Wavefunction, -) +from pymatgen.io.lobster import Lobsterin from pymatgen.io.lobster.inputs import get_all_possible_basis_combinations -from pymatgen.io.vasp import Vasprun from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, PymatgenTest @@ -42,1515 +19,6 @@ __date__ = "Dec 10, 2017" -class TestCohpcar(PymatgenTest): - def setUp(self): - self.cohp_bise = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.BiSe.gz") - self.coop_bise = Cohpcar( - filename=f"{TEST_DIR}/COOPCAR.lobster.BiSe.gz", - are_coops=True, - ) - self.cohp_fe = Cohpcar(filename=f"{TEST_DIR}/COOPCAR.lobster.gz") - self.coop_fe = Cohpcar( - filename=f"{TEST_DIR}/COOPCAR.lobster.gz", - are_coops=True, - ) - self.orb = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.orbitalwise.gz") - self.orb_notot = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.notot.orbitalwise.gz") - - # Lobster 3.1 (Test data is from prerelease of Lobster 3.1) - self.cohp_KF = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.KF.gz") - self.coop_KF = Cohpcar( - filename=f"{TEST_DIR}/COHPCAR.lobster.KF.gz", - are_coops=True, - ) - - # example with f electrons - self.cohp_Na2UO4 = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.Na2UO4.gz") - self.coop_Na2UO4 = Cohpcar( - filename=f"{TEST_DIR}/COOPCAR.lobster.Na2UO4.gz", - are_coops=True, - ) - self.cobi = Cohpcar( - filename=f"{TEST_DIR}/COBICAR.lobster.gz", - are_cobis=True, - ) - # 3 center - self.cobi2 = Cohpcar( - filename=f"{TEST_DIR}/COBICAR.lobster.GeTe", - are_cobis=False, - are_multi_center_cobis=True, - ) - # 4 center - self.cobi3 = Cohpcar( - filename=f"{TEST_DIR}/COBICAR.lobster.GeTe_4center", are_cobis=False, are_multi_center_cobis=True - ) - # partially orbital-resolved - self.cobi4 = Cohpcar( - filename=f"{TEST_DIR}/COBICAR.lobster.GeTe.multi.orbitalwise", - are_cobis=False, - are_multi_center_cobis=True, - ) - # fully orbital-resolved - self.cobi5 = Cohpcar( - filename=f"{TEST_DIR}/COBICAR.lobster.GeTe.multi.orbitalwise.full", - are_cobis=False, - are_multi_center_cobis=True, - ) - # spin polarized - # fully orbital-resolved - self.cobi6 = Cohpcar( - filename=f"{TEST_DIR}/COBICAR.lobster.B2H6.spin", are_cobis=False, are_multi_center_cobis=True - ) - - def test_attributes(self): - assert not self.cohp_bise.are_coops - assert self.coop_bise.are_coops - assert not self.cohp_bise.is_spin_polarized - assert not self.coop_bise.is_spin_polarized - assert not self.cohp_fe.are_coops - assert self.coop_fe.are_coops - assert self.cohp_fe.is_spin_polarized - assert self.coop_fe.is_spin_polarized - assert len(self.cohp_bise.energies) == 241 - assert len(self.coop_bise.energies) == 241 - assert len(self.cohp_fe.energies) == 301 - assert len(self.coop_fe.energies) == 301 - assert len(self.cohp_bise.cohp_data) == 12 - assert len(self.coop_bise.cohp_data) == 12 - assert len(self.cohp_fe.cohp_data) == 3 - assert len(self.coop_fe.cohp_data) == 3 - - # Lobster 3.1 - assert not self.cohp_KF.are_coops - assert self.coop_KF.are_coops - assert not self.cohp_KF.is_spin_polarized - assert not self.coop_KF.is_spin_polarized - assert len(self.cohp_KF.energies) == 6 - assert len(self.coop_KF.energies) == 6 - assert len(self.cohp_KF.cohp_data) == 7 - assert len(self.coop_KF.cohp_data) == 7 - - # Lobster 4.1.0 - assert not self.cohp_KF.are_cobis - assert not self.coop_KF.are_cobis - assert not self.cobi.are_coops - assert self.cobi.are_cobis - assert not self.cobi.is_spin_polarized - - # test multi-center cobis - assert not self.cobi2.are_cobis - assert not self.cobi2.are_coops - assert self.cobi2.are_multi_center_cobis - - def test_energies(self): - efermi_bise = 5.90043 - elim_bise = (-0.124679, 11.9255) - efermi_fe = 9.75576 - elim_fe = (-0.277681, 14.7725) - efermi_KF = -2.87475 - elim_KF = (-11.25000 + efermi_KF, 7.5000 + efermi_KF) - - assert self.cohp_bise.efermi == efermi_bise - assert self.coop_bise.efermi == efermi_bise - assert self.cohp_fe.efermi == efermi_fe - assert self.coop_fe.efermi == efermi_fe - # Lobster 3.1 - assert self.cohp_KF.efermi == efermi_KF - assert self.coop_KF.efermi == efermi_KF - - assert self.cohp_bise.energies[0] + self.cohp_bise.efermi == approx(elim_bise[0], abs=1e-4) - assert self.cohp_bise.energies[-1] + self.cohp_bise.efermi == approx(elim_bise[1], abs=1e-4) - assert self.coop_bise.energies[0] + self.coop_bise.efermi == approx(elim_bise[0], abs=1e-4) - assert self.coop_bise.energies[-1] + self.coop_bise.efermi == approx(elim_bise[1], abs=1e-4) - - assert self.cohp_fe.energies[0] + self.cohp_fe.efermi == approx(elim_fe[0], abs=1e-4) - assert self.cohp_fe.energies[-1] + self.cohp_fe.efermi == approx(elim_fe[1], abs=1e-4) - assert self.coop_fe.energies[0] + self.coop_fe.efermi == approx(elim_fe[0], abs=1e-4) - assert self.coop_fe.energies[-1] + self.coop_fe.efermi == approx(elim_fe[1], abs=1e-4) - - # Lobster 3.1 - assert self.cohp_KF.energies[0] + self.cohp_KF.efermi == approx(elim_KF[0], abs=1e-4) - assert self.cohp_KF.energies[-1] + self.cohp_KF.efermi == approx(elim_KF[1], abs=1e-4) - assert self.coop_KF.energies[0] + self.coop_KF.efermi == approx(elim_KF[0], abs=1e-4) - assert self.coop_KF.energies[-1] + self.coop_KF.efermi == approx(elim_KF[1], abs=1e-4) - - def test_cohp_data(self): - lengths_sites_bise = { - "1": (2.882308829886294, (0, 6)), - "2": (3.1014396233274444, (0, 9)), - "3": (2.8823088298862083, (1, 7)), - "4": (3.1014396233275434, (1, 8)), - "5": (3.0500070394403904, (2, 9)), - "6": (2.9167594580335807, (2, 10)), - "7": (3.05000703944039, (3, 8)), - "8": (2.9167594580335803, (3, 11)), - "9": (3.3752173204052101, (4, 11)), - "10": (3.0729354518345948, (4, 5)), - "11": (3.3752173204052101, (5, 10)), - } - lengths_sites_fe = { - "1": (2.8318907764979082, (7, 6)), - "2": (2.4524893531900283, (7, 8)), - } - # Lobster 3.1 - lengths_sites_KF = { - "1": (2.7119923200622269, (0, 1)), - "2": (2.7119923200622269, (0, 1)), - "3": (2.7119923576010501, (0, 1)), - "4": (2.7119923576010501, (0, 1)), - "5": (2.7119923200622269, (0, 1)), - "6": (2.7119923200622269, (0, 1)), - } - - for data in [self.cohp_bise.cohp_data, self.coop_bise.cohp_data]: - for bond, val in data.items(): - if bond != "average": - assert val["length"] == lengths_sites_bise[bond][0] - assert val["sites"] == lengths_sites_bise[bond][1] - assert len(val["COHP"][Spin.up]) == 241 - assert len(val["ICOHP"][Spin.up]) == 241 - for data in [self.cohp_fe.cohp_data, self.coop_fe.cohp_data]: - for bond, val in data.items(): - if bond != "average": - assert val["length"] == lengths_sites_fe[bond][0] - assert val["sites"] == lengths_sites_fe[bond][1] - assert len(val["COHP"][Spin.up]) == 301 - assert len(val["ICOHP"][Spin.up]) == 301 - - # Lobster 3.1 - for data in [self.cohp_KF.cohp_data, self.coop_KF.cohp_data]: - for bond, val in data.items(): - if bond != "average": - assert val["length"] == lengths_sites_KF[bond][0] - assert val["sites"] == lengths_sites_KF[bond][1] - assert len(val["COHP"][Spin.up]) == 6 - assert len(val["ICOHP"][Spin.up]) == 6 - - for data in [self.cobi2.cohp_data]: - for bond, val in data.items(): - if bond != "average": - if int(bond) >= 13: - assert len(val["COHP"][Spin.up]) == 11 - assert len(val["cells"]) == 3 - else: - assert len(val["COHP"][Spin.up]) == 11 - assert len(val["cells"]) == 2 - - for data in [self.cobi3.cohp_data, self.cobi4.cohp_data]: - for bond, val in data.items(): - if bond != "average": - if int(bond) >= 13: - assert len(val["cells"]) == 4 - else: - assert len(val["cells"]) == 2 - for data in [self.cobi5.cohp_data]: - for bond, val in data.items(): - if bond != "average": - if int(bond) >= 25: - assert len(val["cells"]) == 4 - else: - assert len(val["cells"]) == 2 - for data in [self.cobi6.cohp_data]: - for bond, val in data.items(): - if bond != "average": - if int(bond) >= 21: - assert len(val["cells"]) == 3 - assert len(val["COHP"][Spin.up]) == 12 - assert len(val["COHP"][Spin.down]) == 12 - for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down], strict=True): - assert cohp1 == approx(cohp2, abs=1e-4) - else: - assert len(val["cells"]) == 2 - assert len(val["COHP"][Spin.up]) == 12 - assert len(val["COHP"][Spin.down]) == 12 - for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down], strict=True): - assert cohp1 == approx(cohp2, abs=1e-3) - - def test_orbital_resolved_cohp(self): - orbitals = [(Orbital(jj), Orbital(ii)) for ii in range(4) for jj in range(4)] - assert self.cohp_bise.orb_res_cohp is None - assert self.coop_bise.orb_res_cohp is None - assert self.cohp_fe.orb_res_cohp is None - assert self.coop_fe.orb_res_cohp is None - assert self.orb_notot.cohp_data["1"]["COHP"] is None - assert self.orb_notot.cohp_data["1"]["ICOHP"] is None - for orbs in self.orb.orb_res_cohp["1"]: - orb_set = self.orb.orb_res_cohp["1"][orbs]["orbitals"] - assert orb_set[0][0] == 4 - assert orb_set[1][0] == 4 - assert (orb_set[0][1], orb_set[1][1]) in orbitals - - # test d and f orbitals - ref_list1 = [*[5] * 28, *[6] * 36, *[7] * 4] - ref_list2 = [ - *["f0"] * 4, - *["f1"] * 4, - *["f2"] * 4, - *["f3"] * 4, - *["f_1"] * 4, - *["f_2"] * 4, - *["f_3"] * 4, - *["dx2"] * 4, - *["dxy"] * 4, - *["dxz"] * 4, - *["dyz"] * 4, - *["dz2"] * 4, - *["px"] * 4, - *["py"] * 4, - *["pz"] * 4, - *["s"] * 8, - ] - for iorb, orbs in enumerate(sorted(self.cohp_Na2UO4.orb_res_cohp["49"])): - orb_set = self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["orbitals"] - assert orb_set[0][0] == ref_list1[iorb] - assert str(orb_set[0][1]) == ref_list2[iorb] - - # The sum of the orbital-resolved COHPs should be approximately - # the total COHP. Due to small deviations in the LOBSTER calculation, - # the precision is not very high though. - cohp = self.orb.cohp_data["1"]["COHP"][Spin.up] - icohp = self.orb.cohp_data["1"]["ICOHP"][Spin.up] - tot = np.sum( - [self.orb.orb_res_cohp["1"][orbs]["COHP"][Spin.up] for orbs in self.orb.orb_res_cohp["1"]], - axis=0, - ) - assert_allclose(tot, cohp, atol=1e-3) - tot = np.sum( - [self.orb.orb_res_cohp["1"][orbs]["ICOHP"][Spin.up] for orbs in self.orb.orb_res_cohp["1"]], - axis=0, - ) - assert_allclose(tot, icohp, atol=1e-3) - - # Lobster 3.1 - cohp_KF = self.cohp_KF.cohp_data["1"]["COHP"][Spin.up] - icohp_KF = self.cohp_KF.cohp_data["1"]["ICOHP"][Spin.up] - tot_KF = np.sum( - [self.cohp_KF.orb_res_cohp["1"][orbs]["COHP"][Spin.up] for orbs in self.cohp_KF.orb_res_cohp["1"]], - axis=0, - ) - assert_allclose(tot_KF, cohp_KF, atol=1e-3) - tot_KF = np.sum( - [self.cohp_KF.orb_res_cohp["1"][orbs]["ICOHP"][Spin.up] for orbs in self.cohp_KF.orb_res_cohp["1"]], - axis=0, - ) - assert_allclose(tot_KF, icohp_KF, atol=1e-3) - - # d and f orbitals - cohp_Na2UO4 = self.cohp_Na2UO4.cohp_data["49"]["COHP"][Spin.up] - icohp_Na2UO4 = self.cohp_Na2UO4.cohp_data["49"]["ICOHP"][Spin.up] - tot_Na2UO4 = np.sum( - [ - self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["COHP"][Spin.up] - for orbs in self.cohp_Na2UO4.orb_res_cohp["49"] - ], - axis=0, - ) - assert_allclose(tot_Na2UO4, cohp_Na2UO4, atol=1e-3) - tot_Na2UO4 = np.sum( - [ - self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["ICOHP"][Spin.up] - for orbs in self.cohp_Na2UO4.orb_res_cohp["49"] - ], - axis=0, - ) - - assert_allclose(tot_Na2UO4, icohp_Na2UO4, atol=1e-3) - - assert "5s-4s-5s-4s" in self.cobi4.orb_res_cohp["13"] - assert "5px-4px-5px-4px" in self.cobi4.orb_res_cohp["13"] - assert len(self.cobi4.orb_res_cohp["13"]["5px-4px-5px-4px"]["COHP"][Spin.up]) == 11 - - assert "5s-4s-5s-4s" in self.cobi5.orb_res_cohp["25"] - assert "5px-4px-5px-4px" in self.cobi5.orb_res_cohp["25"] - assert len(self.cobi5.orb_res_cohp["25"]["5px-4px-5px-4px"]["COHP"][Spin.up]) == 11 - - assert len(self.cobi6.orb_res_cohp["21"]["2py-1s-2s"]["COHP"][Spin.up]) == 12 - assert len(self.cobi6.orb_res_cohp["21"]["2py-1s-2s"]["COHP"][Spin.down]) == 12 - - -class TestIcohplist(TestCase): - def setUp(self): - self.icohp_bise = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster.BiSe") - self.icoop_bise = Icohplist( - filename=f"{TEST_DIR}/ICOOPLIST.lobster.BiSe", - are_coops=True, - ) - self.icohp_fe = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster") - # allow gzipped files - self.icohp_gzipped = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster.gz") - self.icoop_fe = Icohplist( - filename=f"{TEST_DIR}/ICOHPLIST.lobster", - are_coops=True, - ) - # ICOBIs and orbitalwise ICOBILIST.lobster - self.icobi_orbitalwise = Icohplist( - filename=f"{TEST_DIR}/ICOBILIST.lobster", - are_cobis=True, - ) - - self.icobi = Icohplist( - filename=f"{TEST_DIR}/ICOBILIST.lobster.withoutorbitals", - are_cobis=True, - ) - self.icobi_orbitalwise_spinpolarized = Icohplist( - filename=f"{TEST_DIR}/ICOBILIST.lobster.spinpolarized", - are_cobis=True, - ) - # make sure the correct line is read to check if this is a orbitalwise ICOBILIST - self.icobi_orbitalwise_add = Icohplist( - filename=f"{TEST_DIR}/ICOBILIST.lobster.additional_case", - are_cobis=True, - ) - self.icobi_orbitalwise_spinpolarized_add = Icohplist( - filename=f"{TEST_DIR}/ICOBILIST.lobster.spinpolarized.additional_case", - are_cobis=True, - ) - - def test_attributes(self): - assert not self.icohp_bise.are_coops - assert self.icoop_bise.are_coops - assert not self.icohp_bise.is_spin_polarized - assert not self.icoop_bise.is_spin_polarized - assert len(self.icohp_bise.icohplist) == 11 - assert len(self.icoop_bise.icohplist) == 11 - assert not self.icohp_fe.are_coops - assert self.icoop_fe.are_coops - assert self.icohp_fe.is_spin_polarized - assert self.icoop_fe.is_spin_polarized - assert len(self.icohp_fe.icohplist) == 2 - assert len(self.icoop_fe.icohplist) == 2 - # test are_cobis - assert not self.icohp_fe.are_coops - assert not self.icohp_fe.are_cobis - assert self.icoop_fe.are_coops - assert not self.icoop_fe.are_cobis - assert self.icobi.are_cobis - assert not self.icobi.are_coops - - # orbitalwise - assert self.icobi_orbitalwise.orbitalwise - assert not self.icobi.orbitalwise - - assert self.icobi_orbitalwise_spinpolarized.orbitalwise - - assert self.icobi_orbitalwise_add.orbitalwise - assert self.icobi_orbitalwise_spinpolarized_add.orbitalwise - - def test_values(self): - icohplist_bise = { - "1": { - "length": 2.88231, - "number_of_bonds": 3, - "icohp": {Spin.up: -2.18042}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "2": { - "length": 3.10144, - "number_of_bonds": 3, - "icohp": {Spin.up: -1.14347}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "3": { - "length": 2.88231, - "number_of_bonds": 3, - "icohp": {Spin.up: -2.18042}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "4": { - "length": 3.10144, - "number_of_bonds": 3, - "icohp": {Spin.up: -1.14348}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "5": { - "length": 3.05001, - "number_of_bonds": 3, - "icohp": {Spin.up: -1.30006}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "6": { - "length": 2.91676, - "number_of_bonds": 3, - "icohp": {Spin.up: -1.96843}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "7": { - "length": 3.05001, - "number_of_bonds": 3, - "icohp": {Spin.up: -1.30006}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "8": { - "length": 2.91676, - "number_of_bonds": 3, - "icohp": {Spin.up: -1.96843}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "9": { - "length": 3.37522, - "number_of_bonds": 3, - "icohp": {Spin.up: -0.47531}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "10": { - "length": 3.07294, - "number_of_bonds": 3, - "icohp": {Spin.up: -2.38796}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "11": { - "length": 3.37522, - "number_of_bonds": 3, - "icohp": {Spin.up: -0.47531}, - "translation": (0, 0, 0), - "orbitals": None, - }, - } - icooplist_bise = { - "1": { - "length": 2.88231, - "number_of_bonds": 3, - "icohp": {Spin.up: 0.14245}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "2": { - "length": 3.10144, - "number_of_bonds": 3, - "icohp": {Spin.up: -0.04118}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "3": { - "length": 2.88231, - "number_of_bonds": 3, - "icohp": {Spin.up: 0.14245}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "4": { - "length": 3.10144, - "number_of_bonds": 3, - "icohp": {Spin.up: -0.04118}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "5": { - "length": 3.05001, - "number_of_bonds": 3, - "icohp": {Spin.up: -0.03516}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "6": { - "length": 2.91676, - "number_of_bonds": 3, - "icohp": {Spin.up: 0.10745}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "7": { - "length": 3.05001, - "number_of_bonds": 3, - "icohp": {Spin.up: -0.03516}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "8": { - "length": 2.91676, - "number_of_bonds": 3, - "icohp": {Spin.up: 0.10745}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "9": { - "length": 3.37522, - "number_of_bonds": 3, - "icohp": {Spin.up: -0.12395}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "10": { - "length": 3.07294, - "number_of_bonds": 3, - "icohp": {Spin.up: 0.24714}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "11": { - "length": 3.37522, - "number_of_bonds": 3, - "icohp": {Spin.up: -0.12395}, - "translation": (0, 0, 0), - "orbitals": None, - }, - } - icooplist_fe = { - "1": { - "length": 2.83189, - "number_of_bonds": 2, - "icohp": {Spin.up: -0.10218, Spin.down: -0.19701}, - "translation": (0, 0, 0), - "orbitals": None, - }, - "2": { - "length": 2.45249, - "number_of_bonds": 1, - "icohp": {Spin.up: -0.28485, Spin.down: -0.58279}, - "translation": (0, 0, 0), - "orbitals": None, - }, - } - - assert icohplist_bise == self.icohp_bise.icohplist - assert self.icohp_bise.icohpcollection.extremum_icohpvalue() == -2.38796 - assert icooplist_fe == self.icoop_fe.icohplist - assert self.icoop_fe.icohpcollection.extremum_icohpvalue() == -0.29919 - assert icooplist_bise == self.icoop_bise.icohplist - assert self.icoop_bise.icohpcollection.extremum_icohpvalue() == 0.24714 - assert self.icobi.icohplist["1"]["icohp"][Spin.up] == approx(0.58649) - assert self.icobi_orbitalwise.icohplist["2"]["icohp"][Spin.up] == approx(0.58649) - assert self.icobi_orbitalwise.icohplist["1"]["icohp"][Spin.up] == approx(0.58649) - assert self.icobi_orbitalwise_spinpolarized.icohplist["1"]["icohp"][Spin.up] == approx(0.58649 / 2, abs=1e-3) - assert self.icobi_orbitalwise_spinpolarized.icohplist["1"]["icohp"][Spin.down] == approx(0.58649 / 2, abs=1e-3) - assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["icohp"][Spin.down] == approx(0.58649 / 2, abs=1e-3) - assert self.icobi.icohpcollection.extremum_icohpvalue() == 0.58649 - assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["orbitals"]["2s-6s"]["icohp"][Spin.up] == 0.0247 - - def test_msonable(self): - dict_data = self.icobi_orbitalwise_spinpolarized.as_dict() - icohplist_from_dict = Icohplist.from_dict(dict_data) - all_attributes = vars(self.icobi_orbitalwise_spinpolarized) - for attr_name, attr_value in all_attributes.items(): - if isinstance(attr_value, IcohpCollection): - assert getattr(icohplist_from_dict, attr_name).as_dict() == attr_value.as_dict() - else: - assert getattr(icohplist_from_dict, attr_name) == attr_value - - -class TestNciCobiList(TestCase): - def setUp(self): - self.ncicobi = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster") - self.ncicobi_gz = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.gz") - self.ncicobi_no_spin = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.nospin") - self.ncicobi_no_spin_wo = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.nospin.withoutorbitals") - self.ncicobi_wo = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.withoutorbitals") - - def test_ncicobilist(self): - assert self.ncicobi.is_spin_polarized - assert not self.ncicobi_no_spin.is_spin_polarized - assert self.ncicobi_wo.is_spin_polarized - assert not self.ncicobi_no_spin_wo.is_spin_polarized - assert self.ncicobi.orbital_wise - assert self.ncicobi_no_spin.orbital_wise - assert not self.ncicobi_wo.orbital_wise - assert not self.ncicobi_no_spin_wo.orbital_wise - assert len(self.ncicobi.ncicobi_list) == 2 - assert self.ncicobi.ncicobi_list["2"]["number_of_atoms"] == 3 - assert self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.up] == approx(0.00009) - assert self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.down] == approx(0.00009) - assert self.ncicobi.ncicobi_list["2"]["interaction_type"] == "[X22[0,0,0]->Xs42[0,0,0]->X31[0,0,0]]" - assert ( - self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.up] == self.ncicobi_wo.ncicobi_list["2"]["ncicobi"][Spin.up] - ) - assert ( - self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.up] == self.ncicobi_gz.ncicobi_list["2"]["ncicobi"][Spin.up] - ) - assert ( - self.ncicobi.ncicobi_list["2"]["interaction_type"] == self.ncicobi_gz.ncicobi_list["2"]["interaction_type"] - ) - assert sum(self.ncicobi.ncicobi_list["2"]["ncicobi"].values()) == approx( - self.ncicobi_no_spin.ncicobi_list["2"]["ncicobi"][Spin.up] - ) - - -class TestDoscar(TestCase): - def setUp(self): - # first for spin polarized version - doscar = f"{VASP_OUT_DIR}/DOSCAR.lobster.spin" - poscar = f"{VASP_IN_DIR}/POSCAR.lobster.spin_DOS" - - # not spin polarized - doscar2 = f"{VASP_OUT_DIR}/DOSCAR.lobster.nonspin" - poscar2 = f"{VASP_IN_DIR}/POSCAR.lobster.nonspin_DOS" - - self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) - self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) - - self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) - self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) - - with open(f"{TEST_FILES_DIR}/electronic_structure/dos/structure_KF.json") as file: - data = json.load(file) - - self.structure = Structure.from_dict(data) - - # test structure argument - self.DOSCAR_spin_pol2 = Doscar(doscar=doscar, structure_file=None, structure=Structure.from_file(poscar)) - - def test_complete_dos(self): - # first for spin polarized version - energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] - tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] - fermi = 0.0 - - pdos_f_2s_up = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] - pdos_f_2s_down = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] - pdos_f_2py_up = [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029] - pdos_f_2py_down = [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029] - pdos_f_2pz_up = [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029] - pdos_f_2pz_down = [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029] - pdos_f_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] - pdos_f_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] - - assert energies_spin == self.DOSCAR_spin_pol.completedos.energies.tolist() - assert tdos_up == self.DOSCAR_spin_pol.completedos.densities[Spin.up].tolist() - assert tdos_down == self.DOSCAR_spin_pol.completedos.densities[Spin.down].tolist() - assert fermi == approx(self.DOSCAR_spin_pol.completedos.efermi) - - assert_allclose( - self.DOSCAR_spin_pol.completedos.structure.frac_coords, - self.structure.frac_coords, - ) - assert_allclose( - self.DOSCAR_spin_pol2.completedos.structure.frac_coords, - self.structure.frac_coords, - ) - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == pdos_f_2s_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.down].tolist() == pdos_f_2s_down - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == pdos_f_2py_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.down].tolist() == pdos_f_2py_down - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == pdos_f_2pz_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.down].tolist() == pdos_f_2pz_down - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == pdos_f_2px_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.down].tolist() == pdos_f_2px_down - - energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] - pdos_f_2s = [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060] - pdos_f_2py = [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037] - pdos_f_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] - pdos_f_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] - - assert energies_nonspin == self.DOSCAR_nonspin_pol.completedos.energies.tolist() - - assert tdos_nonspin == self.DOSCAR_nonspin_pol.completedos.densities[Spin.up].tolist() - - assert fermi == approx(self.DOSCAR_nonspin_pol.completedos.efermi) - - assert self.DOSCAR_nonspin_pol.completedos.structure == self.structure - - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == pdos_f_2s - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == pdos_f_2py - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == pdos_f_2pz - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == pdos_f_2px - - def test_pdos(self): - # first for spin polarized version - - pdos_f_2s_up = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] - pdos_f_2s_down = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] - pdos_f_2py_up = [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029] - pdos_f_2py_down = [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029] - pdos_f_2pz_up = [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029] - pdos_f_2pz_down = [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029] - pdos_f_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] - pdos_f_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] - - assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.up].tolist() == pdos_f_2s_up - assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.down].tolist() == pdos_f_2s_down - assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.up].tolist() == pdos_f_2py_up - assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.down].tolist() == pdos_f_2py_down - assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.up].tolist() == pdos_f_2pz_up - assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.down].tolist() == pdos_f_2pz_down - assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.up].tolist() == pdos_f_2px_up - assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.down].tolist() == pdos_f_2px_down - - # non spin - pdos_f_2s = [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060] - pdos_f_2py = [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037] - pdos_f_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] - pdos_f_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] - - assert self.DOSCAR_nonspin_pol.pdos[0]["2s"][Spin.up].tolist() == pdos_f_2s - assert self.DOSCAR_nonspin_pol.pdos[0]["2p_y"][Spin.up].tolist() == pdos_f_2py - assert self.DOSCAR_nonspin_pol.pdos[0]["2p_z"][Spin.up].tolist() == pdos_f_2pz - assert self.DOSCAR_nonspin_pol.pdos[0]["2p_x"][Spin.up].tolist() == pdos_f_2px - - def test_tdos(self): - # first for spin polarized version - energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] - tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] - fermi = 0.0 - - assert energies_spin == self.DOSCAR_spin_pol.tdos.energies.tolist() - assert tdos_up == self.DOSCAR_spin_pol.tdos.densities[Spin.up].tolist() - assert tdos_down == self.DOSCAR_spin_pol.tdos.densities[Spin.down].tolist() - assert fermi == approx(self.DOSCAR_spin_pol.tdos.efermi) - - energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] - fermi = 0.0 - - assert energies_nonspin == self.DOSCAR_nonspin_pol.tdos.energies.tolist() - assert tdos_nonspin == self.DOSCAR_nonspin_pol.tdos.densities[Spin.up].tolist() - assert fermi == approx(self.DOSCAR_nonspin_pol.tdos.efermi) - - def test_energies(self): - # first for spin polarized version - energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - - assert energies_spin == self.DOSCAR_spin_pol.energies.tolist() - - energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - assert energies_nonspin == self.DOSCAR_nonspin_pol.energies.tolist() - - def test_tdensities(self): - # first for spin polarized version - tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] - tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] - - assert tdos_up == self.DOSCAR_spin_pol.tdensities[Spin.up].tolist() - assert tdos_down == self.DOSCAR_spin_pol.tdensities[Spin.down].tolist() - - tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] - assert tdos_nonspin == self.DOSCAR_nonspin_pol.tdensities[Spin.up].tolist() - - def test_itdensities(self): - itdos_up = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09650] - itdos_down = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09685] - assert itdos_up == self.DOSCAR_spin_pol.itdensities[Spin.up].tolist() - assert itdos_down == self.DOSCAR_spin_pol.itdensities[Spin.down].tolist() - - itdos_nonspin = [4.00000, 10.00000, 10.00000, 16.00000, 16.00000, 16.09067] - assert itdos_nonspin == self.DOSCAR_nonspin_pol.itdensities[Spin.up].tolist() - - def test_is_spin_polarized(self): - # first for spin polarized version - assert self.DOSCAR_spin_pol.is_spin_polarized - - assert not self.DOSCAR_nonspin_pol.is_spin_polarized - - -class TestCharge(PymatgenTest): - def setUp(self): - self.charge2 = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.MnO") - # gzipped file - self.charge = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.MnO2.gz") - - def test_attributes(self): - charge_Loewdin = [-1.25, 1.25] - charge_Mulliken = [-1.30, 1.30] - atomlist = ["O1", "Mn2"] - types = ["O", "Mn"] - num_atoms = 2 - assert charge_Mulliken == self.charge2.Mulliken - assert charge_Loewdin == self.charge2.Loewdin - assert atomlist == self.charge2.atomlist - assert types == self.charge2.types - assert num_atoms == self.charge2.num_atoms - - def test_get_structure_with_charges(self): - structure_dict2 = { - "lattice": { - "c": 3.198244, - "volume": 23.132361565928807, - "b": 3.1982447183003364, - "gamma": 60.00000011873414, - "beta": 60.00000401737447, - "alpha": 60.00000742944491, - "matrix": [ - [2.769761, 0.0, 1.599122], - [0.923254, 2.611356, 1.599122], - [0.0, 0.0, 3.198244], - ], - "a": 3.1982443884113985, - }, - "@class": "Structure", - "sites": [ - { - "xyz": [1.846502883732, 1.305680611356, 3.198248797366], - "properties": {"Loewdin Charges": -1.25, "Mulliken Charges": -1.3}, - "abc": [0.499998, 0.500001, 0.500002], - "species": [{"occu": 1, "element": "O"}], - "label": "O", - }, - { - "xyz": [0.0, 0.0, 0.0], - "properties": {"Loewdin Charges": 1.25, "Mulliken Charges": 1.3}, - "abc": [0.0, 0.0, 0.0], - "species": [{"occu": 1, "element": "Mn"}], - "label": "Mn", - }, - ], - "charge": None, - "@module": "pymatgen.core.structure", - } - s2 = Structure.from_dict(structure_dict2) - assert s2 == self.charge2.get_structure_with_charges(f"{VASP_IN_DIR}/POSCAR_MnO") - - def test_msonable(self): - dict_data = self.charge2.as_dict() - charge_from_dict = Charge.from_dict(dict_data) - all_attributes = vars(self.charge2) - for attr_name, attr_value in all_attributes.items(): - assert getattr(charge_from_dict, attr_name) == attr_value - - -class TestLobsterout(PymatgenTest): - def setUp(self): - self.lobsterout_normal = Lobsterout(filename=f"{TEST_DIR}/lobsterout.normal") - # make sure .gz files are also read correctly - self.lobsterout_normal = Lobsterout(filename=f"{TEST_DIR}/lobsterout.normal2.gz") - self.lobsterout_fatband_grosspop_densityofenergies = Lobsterout( - filename=f"{TEST_DIR}/lobsterout.fatband_grosspop_densityofenergy" - ) - self.lobsterout_saveprojection = Lobsterout(filename=f"{TEST_DIR}/lobsterout.saveprojection") - self.lobsterout_skipping_all = Lobsterout(filename=f"{TEST_DIR}/lobsterout.skipping_all") - self.lobsterout_twospins = Lobsterout(filename=f"{TEST_DIR}/lobsterout.twospins") - self.lobsterout_GaAs = Lobsterout(filename=f"{TEST_DIR}/lobsterout.GaAs") - self.lobsterout_from_projection = Lobsterout(filename=f"{TEST_DIR}/lobsterout_from_projection") - self.lobsterout_onethread = Lobsterout(filename=f"{TEST_DIR}/lobsterout.onethread") - self.lobsterout_cobi_madelung = Lobsterout(filename=f"{TEST_DIR}/lobsterout_cobi_madelung") - self.lobsterout_doscar_lso = Lobsterout(filename=f"{TEST_DIR}/lobsterout_doscar_lso") - - # TODO: implement skipping madelung/cobi - self.lobsterout_skipping_cobi_madelung = Lobsterout(filename=f"{TEST_DIR}/lobsterout.skip_cobi_madelung") - - def test_attributes(self): - assert self.lobsterout_normal.basis_functions == [ - ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] - ] - assert self.lobsterout_normal.basis_type == ["pbeVaspFit2015"] - assert self.lobsterout_normal.charge_spilling == [0.0268] - assert self.lobsterout_normal.dft_program == "VASP" - assert self.lobsterout_normal.elements == ["Ti"] - assert self.lobsterout_normal.has_charge - assert self.lobsterout_normal.has_cohpcar - assert self.lobsterout_normal.has_coopcar - assert self.lobsterout_normal.has_doscar - assert not self.lobsterout_normal.has_projection - assert self.lobsterout_normal.has_bandoverlaps - assert not self.lobsterout_normal.has_density_of_energies - assert not self.lobsterout_normal.has_fatbands - assert not self.lobsterout_normal.has_grosspopulation - assert self.lobsterout_normal.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 21 and upwards will be ignored.", - ] - assert self.lobsterout_normal.info_orthonormalization == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." - ] - assert not self.lobsterout_normal.is_restart_from_projection - assert self.lobsterout_normal.lobster_version == "v3.1.0" - assert self.lobsterout_normal.number_of_spins == 1 - assert self.lobsterout_normal.number_of_threads == 8 - assert self.lobsterout_normal.timing == { - "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "702"}, - "user_time": {"h": "0", "min": "0", "s": "20", "ms": "330"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, - } - assert self.lobsterout_normal.total_spilling[0] == approx([0.044000000000000004][0]) - assert self.lobsterout_normal.warning_lines == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", - "Generally, this is not a critical error. But to help you analyze it,", - "I dumped the band overlap matrices to the file bandOverlaps.lobster.", - "Please check how much they deviate from the identity matrix and decide to", - "use your results only, if you are sure that this is ok.", - ] - - assert self.lobsterout_fatband_grosspop_densityofenergies.basis_functions == [ - ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] - ] - assert self.lobsterout_fatband_grosspop_densityofenergies.basis_type == ["pbeVaspFit2015"] - assert self.lobsterout_fatband_grosspop_densityofenergies.charge_spilling == [0.0268] - assert self.lobsterout_fatband_grosspop_densityofenergies.dft_program == "VASP" - assert self.lobsterout_fatband_grosspop_densityofenergies.elements == ["Ti"] - assert self.lobsterout_fatband_grosspop_densityofenergies.has_charge - assert not self.lobsterout_fatband_grosspop_densityofenergies.has_cohpcar - assert not self.lobsterout_fatband_grosspop_densityofenergies.has_coopcar - assert not self.lobsterout_fatband_grosspop_densityofenergies.has_doscar - assert not self.lobsterout_fatband_grosspop_densityofenergies.has_projection - assert self.lobsterout_fatband_grosspop_densityofenergies.has_bandoverlaps - assert self.lobsterout_fatband_grosspop_densityofenergies.has_density_of_energies - assert self.lobsterout_fatband_grosspop_densityofenergies.has_fatbands - assert self.lobsterout_fatband_grosspop_densityofenergies.has_grosspopulation - assert self.lobsterout_fatband_grosspop_densityofenergies.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 21 and upwards will be ignored.", - ] - assert self.lobsterout_fatband_grosspop_densityofenergies.info_orthonormalization == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." - ] - assert not self.lobsterout_fatband_grosspop_densityofenergies.is_restart_from_projection - assert self.lobsterout_fatband_grosspop_densityofenergies.lobster_version == "v3.1.0" - assert self.lobsterout_fatband_grosspop_densityofenergies.number_of_spins == 1 - assert self.lobsterout_fatband_grosspop_densityofenergies.number_of_threads == 8 - assert self.lobsterout_fatband_grosspop_densityofenergies.timing == { - "wall_time": {"h": "0", "min": "0", "s": "4", "ms": "136"}, - "user_time": {"h": "0", "min": "0", "s": "18", "ms": "280"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "290"}, - } - assert self.lobsterout_fatband_grosspop_densityofenergies.total_spilling[0] == approx([0.044000000000000004][0]) - assert self.lobsterout_fatband_grosspop_densityofenergies.warning_lines == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", - "Generally, this is not a critical error. But to help you analyze it,", - "I dumped the band overlap matrices to the file bandOverlaps.lobster.", - "Please check how much they deviate from the identity matrix and decide to", - "use your results only, if you are sure that this is ok.", - ] - - assert self.lobsterout_saveprojection.basis_functions == [ - ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] - ] - assert self.lobsterout_saveprojection.basis_type == ["pbeVaspFit2015"] - assert self.lobsterout_saveprojection.charge_spilling == [0.0268] - assert self.lobsterout_saveprojection.dft_program == "VASP" - assert self.lobsterout_saveprojection.elements == ["Ti"] - assert self.lobsterout_saveprojection.has_charge - assert not self.lobsterout_saveprojection.has_cohpcar - assert not self.lobsterout_saveprojection.has_coopcar - assert not self.lobsterout_saveprojection.has_doscar - assert self.lobsterout_saveprojection.has_projection - assert self.lobsterout_saveprojection.has_bandoverlaps - assert self.lobsterout_saveprojection.has_density_of_energies - assert not self.lobsterout_saveprojection.has_fatbands - assert not self.lobsterout_saveprojection.has_grosspopulation - assert self.lobsterout_saveprojection.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 21 and upwards will be ignored.", - ] - assert self.lobsterout_saveprojection.info_orthonormalization == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." - ] - assert not self.lobsterout_saveprojection.is_restart_from_projection - assert self.lobsterout_saveprojection.lobster_version == "v3.1.0" - assert self.lobsterout_saveprojection.number_of_spins == 1 - assert self.lobsterout_saveprojection.number_of_threads == 8 - assert self.lobsterout_saveprojection.timing == { - "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "574"}, - "user_time": {"h": "0", "min": "0", "s": "18", "ms": "250"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "320"}, - } - assert self.lobsterout_saveprojection.total_spilling[0] == approx([0.044000000000000004][0]) - assert self.lobsterout_saveprojection.warning_lines == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", - "Generally, this is not a critical error. But to help you analyze it,", - "I dumped the band overlap matrices to the file bandOverlaps.lobster.", - "Please check how much they deviate from the identity matrix and decide to", - "use your results only, if you are sure that this is ok.", - ] - - assert self.lobsterout_skipping_all.basis_functions == [ - ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] - ] - assert self.lobsterout_skipping_all.basis_type == ["pbeVaspFit2015"] - assert self.lobsterout_skipping_all.charge_spilling == [0.0268] - assert self.lobsterout_skipping_all.dft_program == "VASP" - assert self.lobsterout_skipping_all.elements == ["Ti"] - assert not self.lobsterout_skipping_all.has_charge - assert not self.lobsterout_skipping_all.has_cohpcar - assert not self.lobsterout_skipping_all.has_coopcar - assert not self.lobsterout_skipping_all.has_doscar - assert not self.lobsterout_skipping_all.has_projection - assert self.lobsterout_skipping_all.has_bandoverlaps - assert not self.lobsterout_skipping_all.has_density_of_energies - assert not self.lobsterout_skipping_all.has_fatbands - assert not self.lobsterout_skipping_all.has_grosspopulation - assert not self.lobsterout_skipping_all.has_cobicar - assert not self.lobsterout_skipping_all.has_madelung - assert self.lobsterout_skipping_all.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 21 and upwards will be ignored.", - ] - assert self.lobsterout_skipping_all.info_orthonormalization == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." - ] - assert not self.lobsterout_skipping_all.is_restart_from_projection - assert self.lobsterout_skipping_all.lobster_version == "v3.1.0" - assert self.lobsterout_skipping_all.number_of_spins == 1 - assert self.lobsterout_skipping_all.number_of_threads == 8 - assert self.lobsterout_skipping_all.timing == { - "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "117"}, - "user_time": {"h": "0", "min": "0", "s": "16", "ms": "79"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "320"}, - } - assert self.lobsterout_skipping_all.total_spilling[0] == approx([0.044000000000000004][0]) - assert self.lobsterout_skipping_all.warning_lines == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", - "Generally, this is not a critical error. But to help you analyze it,", - "I dumped the band overlap matrices to the file bandOverlaps.lobster.", - "Please check how much they deviate from the identity matrix and decide to", - "use your results only, if you are sure that this is ok.", - ] - - assert self.lobsterout_twospins.basis_functions == [ - [ - "4s", - "4p_y", - "4p_z", - "4p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ] - ] - assert self.lobsterout_twospins.basis_type == ["pbeVaspFit2015"] - assert self.lobsterout_twospins.charge_spilling[0] == approx(0.36619999999999997) - assert self.lobsterout_twospins.charge_spilling[1] == approx(0.36619999999999997) - assert self.lobsterout_twospins.dft_program == "VASP" - assert self.lobsterout_twospins.elements == ["Ti"] - assert self.lobsterout_twospins.has_charge - assert self.lobsterout_twospins.has_cohpcar - assert self.lobsterout_twospins.has_coopcar - assert self.lobsterout_twospins.has_doscar - assert not self.lobsterout_twospins.has_projection - assert self.lobsterout_twospins.has_bandoverlaps - assert not self.lobsterout_twospins.has_density_of_energies - assert not self.lobsterout_twospins.has_fatbands - assert not self.lobsterout_twospins.has_grosspopulation - assert self.lobsterout_twospins.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 19 and upwards will be ignored.", - ] - assert self.lobsterout_twospins.info_orthonormalization == [ - "60 of 294 k-points could not be orthonormalized with an accuracy of 1.0E-5." - ] - assert not self.lobsterout_twospins.is_restart_from_projection - assert self.lobsterout_twospins.lobster_version == "v3.1.0" - assert self.lobsterout_twospins.number_of_spins == 2 - assert self.lobsterout_twospins.number_of_threads == 8 - assert self.lobsterout_twospins.timing == { - "wall_time": {"h": "0", "min": "0", "s": "3", "ms": "71"}, - "user_time": {"h": "0", "min": "0", "s": "22", "ms": "660"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, - } - assert self.lobsterout_twospins.total_spilling[0] == approx([0.2567][0]) - assert self.lobsterout_twospins.total_spilling[1] == approx([0.2567][0]) - assert self.lobsterout_twospins.warning_lines == [ - "60 of 294 k-points could not be orthonormalized with an accuracy of 1.0E-5.", - "Generally, this is not a critical error. But to help you analyze it,", - "I dumped the band overlap matrices to the file bandOverlaps.lobster.", - "Please check how much they deviate from the identity matrix and decide to", - "use your results only, if you are sure that this is ok.", - ] - - assert self.lobsterout_from_projection.basis_functions == [] - assert self.lobsterout_from_projection.basis_type == [] - assert self.lobsterout_from_projection.charge_spilling[0] == approx(0.0177) - assert self.lobsterout_from_projection.dft_program is None - assert self.lobsterout_from_projection.elements == [] - assert self.lobsterout_from_projection.has_charge - assert self.lobsterout_from_projection.has_cohpcar - assert self.lobsterout_from_projection.has_coopcar - assert self.lobsterout_from_projection.has_doscar - assert not self.lobsterout_from_projection.has_projection - assert not self.lobsterout_from_projection.has_bandoverlaps - assert not self.lobsterout_from_projection.has_density_of_energies - assert not self.lobsterout_from_projection.has_fatbands - assert not self.lobsterout_from_projection.has_grosspopulation - assert self.lobsterout_from_projection.info_lines == [] - assert self.lobsterout_from_projection.info_orthonormalization == [] - assert self.lobsterout_from_projection.is_restart_from_projection - assert self.lobsterout_from_projection.lobster_version == "v3.1.0" - assert self.lobsterout_from_projection.number_of_spins == 1 - assert self.lobsterout_from_projection.number_of_threads == 8 - assert self.lobsterout_from_projection.timing == { - "wall_time": {"h": "0", "min": "2", "s": "1", "ms": "890"}, - "user_time": {"h": "0", "min": "15", "s": "10", "ms": "530"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "400"}, - } - assert self.lobsterout_from_projection.total_spilling[0] == approx([0.1543][0]) - assert self.lobsterout_from_projection.warning_lines == [] - - assert self.lobsterout_GaAs.basis_functions == [ - ["4s", "4p_y", "4p_z", "4p_x"], - [ - "4s", - "4p_y", - "4p_z", - "4p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ], - ] - assert self.lobsterout_GaAs.basis_type == ["Bunge", "Bunge"] - assert self.lobsterout_GaAs.charge_spilling[0] == approx(0.0089) - assert self.lobsterout_GaAs.dft_program == "VASP" - assert self.lobsterout_GaAs.elements == ["As", "Ga"] - assert self.lobsterout_GaAs.has_charge - assert self.lobsterout_GaAs.has_cohpcar - assert self.lobsterout_GaAs.has_coopcar - assert self.lobsterout_GaAs.has_doscar - assert not self.lobsterout_GaAs.has_projection - assert not self.lobsterout_GaAs.has_bandoverlaps - assert not self.lobsterout_GaAs.has_density_of_energies - assert not self.lobsterout_GaAs.has_fatbands - assert not self.lobsterout_GaAs.has_grosspopulation - assert self.lobsterout_GaAs.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 14 and upwards will be ignored.", - ] - assert self.lobsterout_GaAs.info_orthonormalization == [] - assert not self.lobsterout_GaAs.is_restart_from_projection - assert self.lobsterout_GaAs.lobster_version == "v3.1.0" - assert self.lobsterout_GaAs.number_of_spins == 1 - assert self.lobsterout_GaAs.number_of_threads == 8 - assert self.lobsterout_GaAs.timing == { - "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "726"}, - "user_time": {"h": "0", "min": "0", "s": "12", "ms": "370"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "180"}, - } - assert self.lobsterout_GaAs.total_spilling[0] == approx([0.0859][0]) - - assert self.lobsterout_onethread.number_of_threads == 1 - # Test lobsterout of lobster-4.1.0 - assert self.lobsterout_cobi_madelung.has_cobicar - assert self.lobsterout_cobi_madelung.has_cohpcar - assert self.lobsterout_cobi_madelung.has_madelung - assert not self.lobsterout_cobi_madelung.has_doscar_lso - - assert self.lobsterout_doscar_lso.has_doscar_lso - - assert self.lobsterout_skipping_cobi_madelung.has_cobicar is False - assert self.lobsterout_skipping_cobi_madelung.has_madelung is False - - def test_get_doc(self): - ref_data = { - "restart_from_projection": False, - "lobster_version": "v3.1.0", - "threads": 8, - "dft_program": "VASP", - "charge_spilling": [0.0268], - "total_spilling": [0.044000000000000004], - "elements": ["Ti"], - "basis_type": ["pbeVaspFit2015"], - "basis_functions": [ - [ - "3s", - "4s", - "3p_y", - "3p_z", - "3p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ] - ], - "timing": { - "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "702"}, - "user_time": {"h": "0", "min": "0", "s": "20", "ms": "330"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, - }, - "warning_lines": [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", - "Generally, this is not a critical error. But to help you analyze it,", - "I dumped the band overlap matrices to the file bandOverlaps.lobster.", - "Please check how much they deviate from the identity matrix and decide to", - "use your results only, if you are sure that this is ok.", - ], - "info_orthonormalization": ["3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5."], - "info_lines": [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 21 and upwards will be ignored.", - ], - "has_doscar": True, - "has_doscar_lso": False, - "has_cohpcar": True, - "has_coopcar": True, - "has_charge": True, - "has_projection": False, - "has_bandoverlaps": True, - "has_fatbands": False, - "has_grosspopulation": False, - "has_density_of_energies": False, - } - for key, item in self.lobsterout_normal.get_doc().items(): - if key not in ["has_cobicar", "has_madelung"]: - if isinstance(item, str): - assert ref_data[key], item - elif isinstance(item, int): - assert ref_data[key] == item - elif key in ("charge_spilling", "total_spilling"): - assert item[0] == approx(ref_data[key][0]) - elif isinstance(item, list | dict): - assert item == ref_data[key] - - def test_msonable(self): - dict_data = self.lobsterout_normal.as_dict() - lobsterout_from_dict = Lobsterout.from_dict(dict_data) - assert dict_data == lobsterout_from_dict.as_dict() - # test initialization with empty attributes (ensure file is not read again) - dict_data_empty = dict.fromkeys(self.lobsterout_doscar_lso._ATTRIBUTES, None) - lobsterout_empty_init_dict = Lobsterout.from_dict(dict_data_empty).as_dict() - for attribute in lobsterout_empty_init_dict: - if "@" not in attribute: - assert lobsterout_empty_init_dict[attribute] is None - - with pytest.raises(ValueError, match="invalid=val is not a valid attribute for Lobsterout"): - Lobsterout(filename=None, invalid="val") - - -class TestFatband(PymatgenTest): - def setUp(self): - self.structure = Vasprun( - filename=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", - ionic_step_skip=None, - ionic_step_offset=0, - parse_dos=True, - parse_eigen=False, - parse_projected_eigen=False, - parse_potcar_file=False, - occu_tol=1e-8, - exception_on_bad_xml=True, - ).final_structure - self.fatband_SiO2_p_x = Fatband( - filenames=f"{TEST_DIR}/Fatband_SiO2/Test_p_x", - kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", - structure=self.structure, - vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", - ) - self.vasprun_SiO2_p_x = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml") - self.bs_symmline = self.vasprun_SiO2_p_x.get_band_structure(line_mode=True, force_hybrid_mode=True) - self.fatband_SiO2_p = Fatband( - filenames=f"{TEST_DIR}/Fatband_SiO2/Test_p", - kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p/KPOINTS", - vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p/vasprun.xml", - structure=self.structure, - ) - self.fatband_SiO2_p2 = Fatband( - filenames=f"{TEST_DIR}/Fatband_SiO2/Test_p", - kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p/KPOINTS", - structure=self.structure, - vasprun_file=None, - efermi=1.0647039, - ) - self.vasprun_SiO2_p = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p/vasprun.xml") - self.bs_symmline2 = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) - self.fatband_SiO2_spin = Fatband( - filenames=f"{TEST_DIR}/Fatband_SiO2/Test_Spin", - kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/KPOINTS", - vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/vasprun.xml", - structure=self.structure, - ) - - self.vasprun_SiO2_spin = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/vasprun.xml") - self.bs_symmline_spin = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) - - def test_attributes(self): - assert list(self.fatband_SiO2_p_x.label_dict["M"]) == approx([0.5, 0.0, 0.0]) - assert self.fatband_SiO2_p_x.efermi == self.vasprun_SiO2_p_x.efermi - lattice1 = self.bs_symmline.lattice_rec.as_dict() - lattice2 = self.fatband_SiO2_p_x.lattice.as_dict() - for idx in range(3): - assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) - assert self.fatband_SiO2_p_x.eigenvals[Spin.up][1][1] - self.fatband_SiO2_p_x.efermi == -18.245 - assert self.fatband_SiO2_p_x.is_spinpolarized is False - assert self.fatband_SiO2_p_x.kpoints_array[3] == approx([0.03409091, 0, 0]) - assert self.fatband_SiO2_p_x.nbands == 36 - assert self.fatband_SiO2_p_x.p_eigenvals[Spin.up][2][1]["Si1"]["3p_x"] == 0.002 - assert self.fatband_SiO2_p_x.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) - assert self.fatband_SiO2_p_x.structure[0].species_string == "Si" - assert self.fatband_SiO2_p_x.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) - - assert list(self.fatband_SiO2_p.label_dict["M"]) == approx([0.5, 0.0, 0.0]) - assert self.fatband_SiO2_p.efermi == self.vasprun_SiO2_p.efermi - lattice1 = self.bs_symmline2.lattice_rec.as_dict() - lattice2 = self.fatband_SiO2_p.lattice.as_dict() - for idx in range(3): - assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) - assert self.fatband_SiO2_p.eigenvals[Spin.up][1][1] - self.fatband_SiO2_p.efermi == -18.245 - assert self.fatband_SiO2_p.is_spinpolarized is False - assert self.fatband_SiO2_p.kpoints_array[3] == approx([0.03409091, 0, 0]) - assert self.fatband_SiO2_p.nbands == 36 - assert self.fatband_SiO2_p.p_eigenvals[Spin.up][2][1]["Si1"]["3p"] == 0.042 - assert self.fatband_SiO2_p.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) - assert self.fatband_SiO2_p.structure[0].species_string == "Si" - assert self.fatband_SiO2_p.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) - assert self.fatband_SiO2_p.efermi == approx(1.0647039) - - assert list(self.fatband_SiO2_spin.label_dict["M"]) == approx([0.5, 0.0, 0.0]) - assert self.fatband_SiO2_spin.efermi == self.vasprun_SiO2_spin.efermi - lattice1 = self.bs_symmline_spin.lattice_rec.as_dict() - lattice2 = self.fatband_SiO2_spin.lattice.as_dict() - for idx in range(3): - assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) - assert self.fatband_SiO2_spin.eigenvals[Spin.up][1][1] - self.fatband_SiO2_spin.efermi == -18.245 - assert self.fatband_SiO2_spin.eigenvals[Spin.down][1][1] - self.fatband_SiO2_spin.efermi == -18.245 - assert self.fatband_SiO2_spin.is_spinpolarized - assert self.fatband_SiO2_spin.kpoints_array[3] == approx([0.03409091, 0, 0]) - assert self.fatband_SiO2_spin.nbands == 36 - - assert self.fatband_SiO2_spin.p_eigenvals[Spin.up][2][1]["Si1"]["3p"] == 0.042 - assert self.fatband_SiO2_spin.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) - assert self.fatband_SiO2_spin.structure[0].species_string == "Si" - assert self.fatband_SiO2_spin.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) - - def test_raises(self): - with pytest.raises(ValueError, match="vasprun_file or efermi have to be provided"): - Fatband( - filenames=f"{TEST_DIR}/Fatband_SiO2/Test_Spin", - kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/KPOINTS", - vasprun_file=None, - structure=self.structure, - ) - with pytest.raises( - ValueError, match="The are two FATBAND files for the same atom and orbital. The program will stop" - ): - self.fatband_SiO2_p_x = Fatband( - filenames=[ - f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", - f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", - ], - kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", - structure=self.structure, - ) - - with pytest.raises(ValueError, match="A structure object has to be provided"): - self.fatband_SiO2_p_x = Fatband( - filenames=[ - f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", - f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", - ], - kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", - structure=None, - ) - - with pytest.raises( - ValueError, - match=r"Make sure all relevant orbitals were generated and that no duplicates \(2p and 2p_x\) are present", - ): - self.fatband_SiO2_p_x = Fatband( - filenames=[ - f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", - f"{TEST_DIR}/Fatband_SiO2/Test_p/FATBAND_si1_3p.lobster", - ], - kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", - structure=self.structure, - ) - - with pytest.raises(ValueError, match="No FATBAND files in folder or given"): - self.fatband_SiO2_p_x = Fatband( - filenames=".", - kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", - vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", - structure=self.structure, - ) - - def test_get_bandstructure(self): - bs_p = self.fatband_SiO2_p.get_bandstructure() - atom1 = bs_p.structure[0] - atom2 = self.bs_symmline2.structure[0] - assert atom1.frac_coords[0] == approx(atom2.frac_coords[0]) - assert atom1.frac_coords[1] == approx(atom2.frac_coords[1]) - assert atom1.frac_coords[2] == approx(atom2.frac_coords[2]) - assert atom1.coords[0] == approx(atom2.coords[0]) - assert atom1.coords[1] == approx(atom2.coords[1]) - assert atom1.coords[2] == approx(atom2.coords[2]) - assert atom1.species_string == atom2.species_string - assert bs_p.efermi == self.bs_symmline2.efermi - branch1 = bs_p.branches[0] - branch2 = self.bs_symmline2.branches[0] - assert branch2["name"] == branch1["name"] - assert branch2["start_index"] == branch1["start_index"] - assert branch2["end_index"] == branch1["end_index"] - - assert bs_p.distance[30] == approx(self.bs_symmline2.distance[30]) - lattice1 = bs_p.lattice_rec.as_dict() - lattice2 = self.bs_symmline2.lattice_rec.as_dict() - assert lattice1["matrix"][0] == approx(lattice2["matrix"][0]) - assert lattice1["matrix"][1] == approx(lattice2["matrix"][1]) - assert lattice1["matrix"][2] == approx(lattice2["matrix"][2]) - - assert bs_p.kpoints[8].frac_coords[0] == approx(self.bs_symmline2.kpoints[8].frac_coords[0]) - assert bs_p.kpoints[8].frac_coords[1] == approx(self.bs_symmline2.kpoints[8].frac_coords[1]) - assert bs_p.kpoints[8].frac_coords[2] == approx(self.bs_symmline2.kpoints[8].frac_coords[2]) - assert bs_p.kpoints[8].cart_coords[0] == approx(self.bs_symmline2.kpoints[8].cart_coords[0]) - assert bs_p.kpoints[8].cart_coords[1] == approx(self.bs_symmline2.kpoints[8].cart_coords[1]) - assert bs_p.kpoints[8].cart_coords[2] == approx(self.bs_symmline2.kpoints[8].cart_coords[2]) - assert bs_p.kpoints[50].frac_coords[0] == approx(self.bs_symmline2.kpoints[50].frac_coords[0]) - assert bs_p.kpoints[50].frac_coords[1] == approx(self.bs_symmline2.kpoints[50].frac_coords[1]) - assert bs_p.kpoints[50].frac_coords[2] == approx(self.bs_symmline2.kpoints[50].frac_coords[2]) - assert bs_p.kpoints[50].cart_coords[0] == approx(self.bs_symmline2.kpoints[50].cart_coords[0]) - assert bs_p.kpoints[50].cart_coords[1] == approx(self.bs_symmline2.kpoints[50].cart_coords[1]) - assert bs_p.kpoints[50].cart_coords[2] == approx(self.bs_symmline2.kpoints[50].cart_coords[2]) - assert bs_p.get_band_gap()["energy"] == approx(self.bs_symmline2.get_band_gap()["energy"], abs=1e-2) - assert bs_p.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064)) - assert bs_p.get_projections_on_elements_and_orbitals({"Si": ["3p"]})[Spin.up][0][0]["Si"]["3p"] == approx(0.003) - assert bs_p.get_projections_on_elements_and_orbitals({"O": ["2p"]})[Spin.up][0][0]["O"]["2p"] == approx( - 0.002 * 3 + 0.003 * 3 - ) - dict_here = bs_p.get_projections_on_elements_and_orbitals({"Si": ["3s", "3p"], "O": ["2s", "2p"]})[Spin.up][0][ - 0 - ] - assert dict_here["Si"]["3s"] == approx(0.192) - assert dict_here["Si"]["3p"] == approx(0.003) - assert dict_here["O"]["2s"] == approx(0.792) - assert dict_here["O"]["2p"] == approx(0.015) - - bs_spin = self.fatband_SiO2_spin.get_bandstructure() - assert bs_spin.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064)) - assert bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3p"]})[Spin.up][0][0]["Si"]["3p"] == approx( - 0.003 - ) - assert bs_spin.get_projections_on_elements_and_orbitals({"O": ["2p"]})[Spin.up][0][0]["O"]["2p"] == approx( - 0.002 * 3 + 0.003 * 3 - ) - dict_here = bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3s", "3p"], "O": ["2s", "2p"]})[Spin.up][ - 0 - ][0] - assert dict_here["Si"]["3s"] == approx(0.192) - assert dict_here["Si"]["3p"] == approx(0.003) - assert dict_here["O"]["2s"] == approx(0.792) - assert dict_here["O"]["2p"] == approx(0.015) - - assert bs_spin.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064)) - assert bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3p"]})[Spin.down][0][0]["Si"]["3p"] == approx( - 0.003 - ) - assert bs_spin.get_projections_on_elements_and_orbitals({"O": ["2p"]})[Spin.down][0][0]["O"]["2p"] == approx( - 0.002 * 3 + 0.003 * 3 - ) - dict_here = bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3s", "3p"], "O": ["2s", "2p"]})[ - Spin.down - ][0][0] - assert dict_here["Si"]["3s"] == approx(0.192) - assert dict_here["Si"]["3p"] == approx(0.003) - assert dict_here["O"]["2s"] == approx(0.792) - assert dict_here["O"]["2p"] == approx(0.015) - bs_p_x = self.fatband_SiO2_p_x.get_bandstructure() - assert bs_p_x.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064), abs=1e-2) - - class TestLobsterin(PymatgenTest): def setUp(self): self.Lobsterin = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.1") @@ -2088,290 +556,6 @@ def test_as_from_dict(self): new_lobsterin.to_json() -class TestBandoverlaps(TestCase): - def setUp(self): - # test spin-polarized calc and non spinpolarized calc - - self.band_overlaps1 = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.1") - self.band_overlaps2 = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.2") - - self.band_overlaps1_new = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.new.1") - self.band_overlaps2_new = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.new.2") - - def test_attributes(self): - # bandoverlapsdict - bo_dict = self.band_overlaps1.bandoverlapsdict - assert bo_dict[Spin.up]["max_deviations"][0] == approx(0.000278953) - assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["max_deviations"][10] == approx(0.0640933) - assert bo_dict[Spin.up]["matrices"][0].item(-1, -1) == approx(0.0188058) - assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["matrices"][10].item(-1, -1) == approx(1.0) - assert bo_dict[Spin.up]["matrices"][0].item(0, 0) == approx(1) - assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["matrices"][10].item(0, 0) == approx(0.995849) - - assert bo_dict[Spin.down]["max_deviations"][-1] == approx(4.31567e-05) - assert self.band_overlaps1_new.bandoverlapsdict[Spin.down]["max_deviations"][9] == approx(0.064369) - assert bo_dict[Spin.down]["matrices"][-1].item(0, -1) == approx(4.0066e-07) - assert self.band_overlaps1_new.bandoverlapsdict[Spin.down]["matrices"][9].item(0, -1) == approx(1.37447e-09) - - # maxDeviation - assert self.band_overlaps1.max_deviation[0] == approx(0.000278953) - assert self.band_overlaps1_new.max_deviation[0] == approx(0.39824) - assert self.band_overlaps1.max_deviation[-1] == approx(4.31567e-05) - assert self.band_overlaps1_new.max_deviation[-1] == approx(0.324898) - - assert self.band_overlaps2.max_deviation[0] == approx(0.000473319) - assert self.band_overlaps2_new.max_deviation[0] == approx(0.403249) - assert self.band_overlaps2.max_deviation[-1] == approx(1.48451e-05) - assert self.band_overlaps2_new.max_deviation[-1] == approx(0.45154) - - def test_has_good_quality(self): - assert not self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=0.1) - assert not self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=0.1) - assert not self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=9, - number_occ_bands_spin_down=5, - limit_deviation=0.1, - spin_polarized=True, - ) - assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=9, - number_occ_bands_spin_down=5, - limit_deviation=0.1, - spin_polarized=True, - ) - assert self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=3, - number_occ_bands_spin_down=0, - limit_deviation=1, - spin_polarized=True, - ) - assert self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=3, - number_occ_bands_spin_down=0, - limit_deviation=1, - spin_polarized=True, - ) - assert not self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, - number_occ_bands_spin_down=1, - limit_deviation=0.000001, - spin_polarized=True, - ) - assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, - number_occ_bands_spin_down=1, - limit_deviation=0.000001, - spin_polarized=True, - ) - assert not self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, - number_occ_bands_spin_down=0, - limit_deviation=0.000001, - spin_polarized=True, - ) - assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, - number_occ_bands_spin_down=0, - limit_deviation=0.000001, - spin_polarized=True, - ) - assert not self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=0, - number_occ_bands_spin_down=1, - limit_deviation=0.000001, - spin_polarized=True, - ) - assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=0, - number_occ_bands_spin_down=1, - limit_deviation=0.000001, - spin_polarized=True, - ) - assert not self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=4, - number_occ_bands_spin_down=4, - limit_deviation=0.001, - spin_polarized=True, - ) - assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=4, - number_occ_bands_spin_down=4, - limit_deviation=0.001, - spin_polarized=True, - ) - - assert self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=100) - assert self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=100) - assert self.band_overlaps2.has_good_quality_maxDeviation() - assert not self.band_overlaps2_new.has_good_quality_maxDeviation() - assert not self.band_overlaps2.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) - assert not self.band_overlaps2_new.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) - assert not self.band_overlaps2.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=10, limit_deviation=0.0000001 - ) - assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=10, limit_deviation=0.0000001 - ) - assert not self.band_overlaps2.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, limit_deviation=0.1 - ) - - assert not self.band_overlaps2.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, limit_deviation=1e-8 - ) - assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, limit_deviation=1e-8 - ) - assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=10, limit_deviation=1) - assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=2, limit_deviation=0.1 - ) - assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=1, limit_deviation=1) - assert self.band_overlaps2_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, limit_deviation=2 - ) - - def test_msonable(self): - dict_data = self.band_overlaps2_new.as_dict() - bandoverlaps_from_dict = Bandoverlaps.from_dict(dict_data) - all_attributes = vars(self.band_overlaps2_new) - for attr_name, attr_value in all_attributes.items(): - assert getattr(bandoverlaps_from_dict, attr_name) == attr_value - - def test_keys(self): - bo_dict = self.band_overlaps1.band_overlaps_dict - bo_dict_new = self.band_overlaps1_new.band_overlaps_dict - bo_dict_2 = self.band_overlaps2.band_overlaps_dict - assert len(bo_dict[Spin.up]["k_points"]) == 408 - assert len(bo_dict_2[Spin.up]["max_deviations"]) == 2 - assert len(bo_dict_new[Spin.down]["matrices"]) == 73 - - -class TestGrosspop(TestCase): - def setUp(self): - self.grosspop1 = Grosspop(f"{TEST_DIR}/GROSSPOP.lobster") - - def test_attributes(self): - gross_pop_list = self.grosspop1.list_dict_grosspop - assert gross_pop_list[0]["Mulliken GP"]["3s"] == approx(0.52) - assert gross_pop_list[0]["Mulliken GP"]["3p_y"] == approx(0.38) - assert gross_pop_list[0]["Mulliken GP"]["3p_z"] == approx(0.37) - assert gross_pop_list[0]["Mulliken GP"]["3p_x"] == approx(0.37) - assert gross_pop_list[0]["Mulliken GP"]["total"] == approx(1.64) - assert gross_pop_list[0]["element"] == "Si" - assert gross_pop_list[0]["Loewdin GP"]["3s"] == approx(0.61) - assert gross_pop_list[0]["Loewdin GP"]["3p_y"] == approx(0.52) - assert gross_pop_list[0]["Loewdin GP"]["3p_z"] == approx(0.52) - assert gross_pop_list[0]["Loewdin GP"]["3p_x"] == approx(0.52) - assert gross_pop_list[0]["Loewdin GP"]["total"] == approx(2.16) - assert gross_pop_list[5]["Mulliken GP"]["2s"] == approx(1.80) - assert gross_pop_list[5]["Loewdin GP"]["2s"] == approx(1.60) - assert gross_pop_list[5]["element"] == "O" - assert gross_pop_list[8]["Mulliken GP"]["2s"] == approx(1.80) - assert gross_pop_list[8]["Loewdin GP"]["2s"] == approx(1.60) - assert gross_pop_list[8]["element"] == "O" - - def test_structure_with_grosspop(self): - struct_dict = { - "@module": "pymatgen.core.structure", - "@class": "Structure", - "charge": None, - "lattice": { - "matrix": [ - [5.021897888834907, 4.53806e-11, 0.0], - [-2.5109484443388332, 4.349090983701526, 0.0], - [0.0, 0.0, 5.511929408565514], - ], - "a": 5.021897888834907, - "b": 5.0218974974248045, - "c": 5.511929408565514, - "alpha": 90.0, - "beta": 90.0, - "gamma": 119.99999598960493, - "volume": 120.38434608659402, - }, - "sites": [ - { - "species": [{"element": "Si", "occu": 1}], - "abc": [-3e-16, 0.4763431475490085, 0.6666669999999968], - "xyz": [-1.1960730853096477, 2.0716596881533986, 3.674621443020128], - "label": "Si", - "properties": {"Total Mulliken GP": 1.64, "Total Loewdin GP": 2.16}, - }, - { - "species": [{"element": "Si", "occu": 1}], - "abc": [0.5236568524509936, 0.5236568524509926, 0.0], - "xyz": [1.3148758827683875, 2.277431295571896, 0.0], - "label": "Si", - "properties": {"Total Mulliken GP": 1.64, "Total Loewdin GP": 2.16}, - }, - { - "species": [{"element": "Si", "occu": 1}], - "abc": [0.4763431475490066, -1.2e-15, 0.3333330000000032], - "xyz": [ - 2.392146647037334, - 2.1611518932482004e-11, - 1.8373079655453863, - ], - "label": "Si", - "properties": {"Total Mulliken GP": 1.64, "Total Loewdin GP": 2.16}, - }, - { - "species": [{"element": "O", "occu": 1}], - "abc": [0.1589037798059321, 0.7440031622164922, 0.4613477252144715], - "xyz": [-1.0701550264153763, 3.235737444648381, 2.5429160941844473], - "label": "O", - "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, - }, - { - "species": [{"element": "O", "occu": 1}], - "abc": [0.2559968377835071, 0.4149006175894398, 0.7946807252144676], - "xyz": [0.2437959189219816, 1.8044405351020447, 4.380224059729795], - "label": "O", - "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, - }, - { - "species": [{"element": "O", "occu": 1}], - "abc": [0.5850993824105679, 0.8410962201940679, 0.1280147252144683], - "xyz": [0.8263601076506712, 3.6580039876980064, 0.7056081286390611], - "label": "O", - "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, - }, - { - "species": [{"element": "O", "occu": 1}], - "abc": [0.7440031622164928, 0.1589037798059326, 0.5386522747855285], - "xyz": [3.337308710918233, 0.6910869960638374, 2.969013314381067], - "label": "O", - "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, - }, - { - "species": [{"element": "O", "occu": 1}], - "abc": [0.4149006175894392, 0.2559968377835, 0.2053192747855324], - "xyz": [1.4407936739605638, 1.1133535390791505, 1.13170534883572], - "label": "O", - "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, - }, - { - "species": [{"element": "O", "occu": 1}], - "abc": [0.841096220194068, 0.5850993824105675, 0.8719852747855317], - "xyz": [2.754744948452184, 2.5446504486493, 4.806321279926453], - "label": "O", - "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, - }, - ], - } - - new_structure = self.grosspop1.get_structure_with_total_grosspop(f"{TEST_DIR}/POSCAR.SiO2") - assert_allclose(new_structure.frac_coords, Structure.from_dict(struct_dict).frac_coords) - - def test_msonable(self): - dict_data = self.grosspop1.as_dict() - grosspop_from_dict = Grosspop.from_dict(dict_data) - all_attributes = vars(self.grosspop1) - for attr_name, attr_value in all_attributes.items(): - assert getattr(grosspop_from_dict, attr_name) == attr_value - - class TestUtils(PymatgenTest): def test_get_all_possible_basis_combinations(self): # this basis is just for testing (not correct) @@ -2425,234 +609,3 @@ def test_get_all_possible_basis_combinations(self): ["Si 1s 2s 2p 3s", "Na 1s 2s 3s"], ["Si 1s 2s 2p 3s", "Na 1s 2s 2p 3s"], ] - - -class TestWavefunction(PymatgenTest): - def test_parse_file(self): - grid, points, real, imaginary, distance = Wavefunction._parse_file( - f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz" - ) - assert_array_equal([41, 41, 41], grid) - assert points[4][0] == approx(0.0000) - assert points[4][1] == approx(0.0000) - assert points[4][2] == approx(0.4000) - assert real[8] == approx(1.38863e-01) - assert imaginary[8] == approx(2.89645e-01) - assert len(imaginary) == 41 * 41 * 41 - assert len(real) == 41 * 41 * 41 - assert len(points) == 41 * 41 * 41 - assert distance[0] == approx(0.0000) - - def test_set_volumetric_data(self): - wave1 = Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ) - - wave1.set_volumetric_data(grid=wave1.grid, structure=wave1.structure) - assert wave1.volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) - assert wave1.volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) - - def test_get_volumetricdata_real(self): - wave1 = Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ) - volumetricdata_real = wave1.get_volumetricdata_real() - assert volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) - - def test_get_volumetricdata_imaginary(self): - wave1 = Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ) - volumetricdata_imaginary = wave1.get_volumetricdata_imaginary() - assert volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) - - def test_get_volumetricdata_density(self): - wave1 = Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ) - volumetricdata_density = wave1.get_volumetricdata_density() - assert volumetricdata_density.data["total"][0, 0, 0] == approx((-3.0966 * -3.0966) + (-6.45895 * -6.45895)) - - def test_write_file(self): - wave1 = Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ) - real_wavecar_path = f"{self.tmp_path}/real-wavecar.vasp" - wave1.write_file(filename=real_wavecar_path, part="real") - assert os.path.isfile(real_wavecar_path) - - imag_wavecar_path = f"{self.tmp_path}/imaginary-wavecar.vasp" - wave1.write_file(filename=imag_wavecar_path, part="imaginary") - assert os.path.isfile(imag_wavecar_path) - - density_wavecar_path = f"{self.tmp_path}/density-wavecar.vasp" - wave1.write_file(filename=density_wavecar_path, part="density") - assert os.path.isfile(density_wavecar_path) - - -class TestSitePotentials(PymatgenTest): - def setUp(self) -> None: - self.sitepotential = SitePotential(filename=f"{TEST_DIR}/SitePotentials.lobster.perovskite") - - def test_attributes(self): - assert self.sitepotential.sitepotentials_Loewdin == [-8.77, -17.08, 9.57, 9.57, 8.45] - assert self.sitepotential.sitepotentials_Mulliken == [-11.38, -19.62, 11.18, 11.18, 10.09] - assert self.sitepotential.madelungenergies_Loewdin == approx(-28.64) - assert self.sitepotential.madelungenergies_Mulliken == approx(-40.02) - assert self.sitepotential.atomlist == ["La1", "Ta2", "N3", "N4", "O5"] - assert self.sitepotential.types == ["La", "Ta", "N", "N", "O"] - assert self.sitepotential.num_atoms == 5 - assert self.sitepotential.ewald_splitting == approx(3.14) - - def test_get_structure(self): - structure = self.sitepotential.get_structure_with_site_potentials(f"{TEST_DIR}/POSCAR.perovskite") - assert structure.site_properties["Loewdin Site Potentials (eV)"] == [-8.77, -17.08, 9.57, 9.57, 8.45] - assert structure.site_properties["Mulliken Site Potentials (eV)"] == [-11.38, -19.62, 11.18, 11.18, 10.09] - - def test_msonable(self): - dict_data = self.sitepotential.as_dict() - sitepotential_from_dict = SitePotential.from_dict(dict_data) - all_attributes = vars(self.sitepotential) - for attr_name, attr_value in all_attributes.items(): - assert getattr(sitepotential_from_dict, attr_name) == attr_value - - -class TestMadelungEnergies(PymatgenTest): - def setUp(self) -> None: - self.madelungenergies = MadelungEnergies(filename=f"{TEST_DIR}/MadelungEnergies.lobster.perovskite") - - def test_attributes(self): - assert self.madelungenergies.madelungenergies_Loewdin == approx(-28.64) - assert self.madelungenergies.madelungenergies_Mulliken == approx(-40.02) - assert self.madelungenergies.ewald_splitting == approx(3.14) - - def test_msonable(self): - dict_data = self.madelungenergies.as_dict() - madelung_from_dict = MadelungEnergies.from_dict(dict_data) - all_attributes = vars(self.madelungenergies) - for attr_name, attr_value in all_attributes.items(): - assert getattr(madelung_from_dict, attr_name) == attr_value - - -class TestLobsterMatrices(PymatgenTest): - def setUp(self) -> None: - self.hamilton_matrices = LobsterMatrices( - filename=f"{TEST_DIR}/Na_hamiltonMatrices.lobster.gz", e_fermi=-2.79650354 - ) - self.transfer_matrices = LobsterMatrices(filename=f"{TEST_DIR}/C_transferMatrices.lobster.gz") - self.overlap_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Si_overlapMatrices.lobster.gz") - self.coeff_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Si_coefficientMatricesLSO1.lobster.gz") - - def test_attributes(self): - # hamilton matrices - assert self.hamilton_matrices.average_onsite_energies == pytest.approx( - {"Na1_3s": 0.58855353, "Na1_2p_y": -25.72719646, "Na1_2p_z": -25.72719646, "Na1_2p_x": -25.72719646} - ) - ref_onsite_energies = [ - [-0.22519646, -25.76989646, -25.76989646, -25.76989646], - [1.40230354, -25.68449646, -25.68449646, -25.68449646], - ] - assert_allclose(self.hamilton_matrices.onsite_energies, ref_onsite_energies) - - ref_imag_mat_spin_up = np.zeros((4, 4)) - - assert_allclose(self.hamilton_matrices.hamilton_matrices["1"][Spin.up].imag, ref_imag_mat_spin_up) - - ref_real_mat_spin_up = [ - [-3.0217, 0.0, 0.0, 0.0], - [0.0, -28.5664, 0.0, 0.0], - [0.0, 0.0, -28.5664, 0.0], - [0.0, 0.0, 0.0, -28.5664], - ] - assert_allclose(self.hamilton_matrices.hamilton_matrices["1"][Spin.up].real, ref_real_mat_spin_up) - - # overlap matrices - assert self.overlap_matrices.average_onsite_overlaps == pytest.approx( - {"Si1_3s": 1.00000009, "Si1_3p_y": 0.99999995, "Si1_3p_z": 0.99999995, "Si1_3p_x": 0.99999995} - ) - ref_onsite_ovelaps = [[1.00000009, 0.99999995, 0.99999995, 0.99999995]] - - assert_allclose(self.overlap_matrices.onsite_overlaps, ref_onsite_ovelaps) - - ref_imag_mat = np.zeros((4, 4)) - - assert_allclose(self.overlap_matrices.overlap_matrices["1"].imag, ref_imag_mat) - - ref_real_mat = [ - [1.00000009, 0.0, 0.0, 0.0], - [0.0, 0.99999995, 0.0, 0.0], - [0.0, 0.0, 0.99999995, 0.0], - [0.0, 0.0, 0.0, 0.99999995], - ] - - assert_allclose(self.overlap_matrices.overlap_matrices["1"].real, ref_real_mat) - - assert len(self.overlap_matrices.overlap_matrices) == 1 - # transfer matrices - ref_onsite_transfer = [ - [-0.70523233, -0.07099237, -0.65987499, -0.07090411], - [-0.03735031, -0.66865552, 0.69253776, 0.80648063], - ] - assert_allclose(self.transfer_matrices.onsite_transfer, ref_onsite_transfer) - - ref_imag_mat_spin_down = [ - [-0.99920553, 0.0, 0.0, 0.0], - [0.0, 0.71219607, -0.06090336, -0.08690835], - [0.0, -0.04539545, -0.69302453, 0.08323944], - [0.0, -0.12220894, -0.09749622, -0.53739499], - ] - - assert_allclose(self.transfer_matrices.transfer_matrices["1"][Spin.down].imag, ref_imag_mat_spin_down) - - ref_real_mat_spin_down = [ - [-0.03735031, 0.0, 0.0, 0.0], - [0.0, -0.66865552, 0.06086057, 0.13042529], - [-0.0, 0.04262018, 0.69253776, -0.12491928], - [0.0, 0.11473763, 0.09742773, 0.80648063], - ] - - assert_allclose(self.transfer_matrices.transfer_matrices["1"][Spin.down].real, ref_real_mat_spin_down) - - # coefficient matrices - assert list(self.coeff_matrices.coefficient_matrices["1"]) == [Spin.up, Spin.down] - assert self.coeff_matrices.average_onsite_coefficient == pytest.approx( - { - "Si1_3s": 0.6232626450000001, - "Si1_3p_y": -0.029367565000000012, - "Si1_3p_z": -0.50003867, - "Si1_3p_x": 0.13529422, - } - ) - - ref_imag_mat_spin_up = [ - [-0.59697342, 0.0, 0.0, 0.0], - [0.0, 0.50603774, 0.50538255, -0.26664607], - [0.0, -0.45269894, 0.56996771, 0.23223275], - [0.0, 0.47836456, 0.00476861, 0.50184424], - ] - - assert_allclose(self.coeff_matrices.coefficient_matrices["1"][Spin.up].imag, ref_imag_mat_spin_up) - - ref_real_mat_spin_up = [ - [0.80226096, 0.0, 0.0, 0.0], - [0.0, -0.33931137, -0.42979933, -0.34286226], - [0.0, 0.30354633, -0.48472536, 0.29861248], - [0.0, -0.32075579, -0.00405544, 0.64528776], - ] - - assert_allclose(self.coeff_matrices.coefficient_matrices["1"][Spin.up].real, ref_real_mat_spin_up) - - def test_raises(self): - with pytest.raises(ValueError, match="Please provide the fermi energy in eV"): - self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Na_hamiltonMatrices.lobster.gz") - - with pytest.raises( - RuntimeError, - match="Please check provided input file, it seems to be empty", - ): - self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_DIR}/hamiltonMatrices.lobster") diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py new file mode 100644 index 0000000000..c1495e767f --- /dev/null +++ b/tests/io/lobster/test_outputs.py @@ -0,0 +1,2063 @@ +from __future__ import annotations + +import json +import os +from unittest import TestCase + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal +from pytest import approx + +from pymatgen.core.structure import Structure +from pymatgen.electronic_structure.cohp import IcohpCollection +from pymatgen.electronic_structure.core import Orbital, Spin +from pymatgen.io.lobster import ( + Bandoverlaps, + Charge, + Cohpcar, + Doscar, + Fatband, + Grosspop, + Icohplist, + LobsterMatrices, + Lobsterout, + MadelungEnergies, + NciCobiList, + SitePotential, + Wavefunction, +) +from pymatgen.io.vasp import Vasprun +from pymatgen.util.testing import TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, PymatgenTest + +TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp" + +__author__ = "Janine George, Marco Esters" +__copyright__ = "Copyright 2017, The Materials Project" +__version__ = "0.2" +__email__ = "janine.george@uclouvain.be, esters@uoregon.edu" +__date__ = "Dec 10, 2017" + + +class TestCohpcar(PymatgenTest): + def setUp(self): + self.cohp_bise = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.BiSe.gz") + self.coop_bise = Cohpcar( + filename=f"{TEST_DIR}/COOPCAR.lobster.BiSe.gz", + are_coops=True, + ) + self.cohp_fe = Cohpcar(filename=f"{TEST_DIR}/COOPCAR.lobster.gz") + self.coop_fe = Cohpcar( + filename=f"{TEST_DIR}/COOPCAR.lobster.gz", + are_coops=True, + ) + self.orb = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.orbitalwise.gz") + self.orb_notot = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.notot.orbitalwise.gz") + + # Lobster 3.1 (Test data is from prerelease of Lobster 3.1) + self.cohp_KF = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.KF.gz") + self.coop_KF = Cohpcar( + filename=f"{TEST_DIR}/COHPCAR.lobster.KF.gz", + are_coops=True, + ) + + # example with f electrons + self.cohp_Na2UO4 = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.Na2UO4.gz") + self.coop_Na2UO4 = Cohpcar( + filename=f"{TEST_DIR}/COOPCAR.lobster.Na2UO4.gz", + are_coops=True, + ) + self.cobi = Cohpcar( + filename=f"{TEST_DIR}/COBICAR.lobster.gz", + are_cobis=True, + ) + # 3 center + self.cobi2 = Cohpcar( + filename=f"{TEST_DIR}/COBICAR.lobster.GeTe", + are_cobis=False, + are_multi_center_cobis=True, + ) + # 4 center + self.cobi3 = Cohpcar( + filename=f"{TEST_DIR}/COBICAR.lobster.GeTe_4center", are_cobis=False, are_multi_center_cobis=True + ) + # partially orbital-resolved + self.cobi4 = Cohpcar( + filename=f"{TEST_DIR}/COBICAR.lobster.GeTe.multi.orbitalwise", + are_cobis=False, + are_multi_center_cobis=True, + ) + # fully orbital-resolved + self.cobi5 = Cohpcar( + filename=f"{TEST_DIR}/COBICAR.lobster.GeTe.multi.orbitalwise.full", + are_cobis=False, + are_multi_center_cobis=True, + ) + # spin polarized + # fully orbital-resolved + self.cobi6 = Cohpcar( + filename=f"{TEST_DIR}/COBICAR.lobster.B2H6.spin", are_cobis=False, are_multi_center_cobis=True + ) + + def test_attributes(self): + assert not self.cohp_bise.are_coops + assert self.coop_bise.are_coops + assert not self.cohp_bise.is_spin_polarized + assert not self.coop_bise.is_spin_polarized + assert not self.cohp_fe.are_coops + assert self.coop_fe.are_coops + assert self.cohp_fe.is_spin_polarized + assert self.coop_fe.is_spin_polarized + assert len(self.cohp_bise.energies) == 241 + assert len(self.coop_bise.energies) == 241 + assert len(self.cohp_fe.energies) == 301 + assert len(self.coop_fe.energies) == 301 + assert len(self.cohp_bise.cohp_data) == 12 + assert len(self.coop_bise.cohp_data) == 12 + assert len(self.cohp_fe.cohp_data) == 3 + assert len(self.coop_fe.cohp_data) == 3 + + # Lobster 3.1 + assert not self.cohp_KF.are_coops + assert self.coop_KF.are_coops + assert not self.cohp_KF.is_spin_polarized + assert not self.coop_KF.is_spin_polarized + assert len(self.cohp_KF.energies) == 6 + assert len(self.coop_KF.energies) == 6 + assert len(self.cohp_KF.cohp_data) == 7 + assert len(self.coop_KF.cohp_data) == 7 + + # Lobster 4.1.0 + assert not self.cohp_KF.are_cobis + assert not self.coop_KF.are_cobis + assert not self.cobi.are_coops + assert self.cobi.are_cobis + assert not self.cobi.is_spin_polarized + + # test multi-center cobis + assert not self.cobi2.are_cobis + assert not self.cobi2.are_coops + assert self.cobi2.are_multi_center_cobis + + def test_energies(self): + efermi_bise = 5.90043 + elim_bise = (-0.124679, 11.9255) + efermi_fe = 9.75576 + elim_fe = (-0.277681, 14.7725) + efermi_KF = -2.87475 + elim_KF = (-11.25000 + efermi_KF, 7.5000 + efermi_KF) + + assert self.cohp_bise.efermi == efermi_bise + assert self.coop_bise.efermi == efermi_bise + assert self.cohp_fe.efermi == efermi_fe + assert self.coop_fe.efermi == efermi_fe + # Lobster 3.1 + assert self.cohp_KF.efermi == efermi_KF + assert self.coop_KF.efermi == efermi_KF + + assert self.cohp_bise.energies[0] + self.cohp_bise.efermi == approx(elim_bise[0], abs=1e-4) + assert self.cohp_bise.energies[-1] + self.cohp_bise.efermi == approx(elim_bise[1], abs=1e-4) + assert self.coop_bise.energies[0] + self.coop_bise.efermi == approx(elim_bise[0], abs=1e-4) + assert self.coop_bise.energies[-1] + self.coop_bise.efermi == approx(elim_bise[1], abs=1e-4) + + assert self.cohp_fe.energies[0] + self.cohp_fe.efermi == approx(elim_fe[0], abs=1e-4) + assert self.cohp_fe.energies[-1] + self.cohp_fe.efermi == approx(elim_fe[1], abs=1e-4) + assert self.coop_fe.energies[0] + self.coop_fe.efermi == approx(elim_fe[0], abs=1e-4) + assert self.coop_fe.energies[-1] + self.coop_fe.efermi == approx(elim_fe[1], abs=1e-4) + + # Lobster 3.1 + assert self.cohp_KF.energies[0] + self.cohp_KF.efermi == approx(elim_KF[0], abs=1e-4) + assert self.cohp_KF.energies[-1] + self.cohp_KF.efermi == approx(elim_KF[1], abs=1e-4) + assert self.coop_KF.energies[0] + self.coop_KF.efermi == approx(elim_KF[0], abs=1e-4) + assert self.coop_KF.energies[-1] + self.coop_KF.efermi == approx(elim_KF[1], abs=1e-4) + + def test_cohp_data(self): + lengths_sites_bise = { + "1": (2.882308829886294, (0, 6)), + "2": (3.1014396233274444, (0, 9)), + "3": (2.8823088298862083, (1, 7)), + "4": (3.1014396233275434, (1, 8)), + "5": (3.0500070394403904, (2, 9)), + "6": (2.9167594580335807, (2, 10)), + "7": (3.05000703944039, (3, 8)), + "8": (2.9167594580335803, (3, 11)), + "9": (3.3752173204052101, (4, 11)), + "10": (3.0729354518345948, (4, 5)), + "11": (3.3752173204052101, (5, 10)), + } + lengths_sites_fe = { + "1": (2.8318907764979082, (7, 6)), + "2": (2.4524893531900283, (7, 8)), + } + # Lobster 3.1 + lengths_sites_KF = { + "1": (2.7119923200622269, (0, 1)), + "2": (2.7119923200622269, (0, 1)), + "3": (2.7119923576010501, (0, 1)), + "4": (2.7119923576010501, (0, 1)), + "5": (2.7119923200622269, (0, 1)), + "6": (2.7119923200622269, (0, 1)), + } + + for data in [self.cohp_bise.cohp_data, self.coop_bise.cohp_data]: + for bond, val in data.items(): + if bond != "average": + assert val["length"] == lengths_sites_bise[bond][0] + assert val["sites"] == lengths_sites_bise[bond][1] + assert len(val["COHP"][Spin.up]) == 241 + assert len(val["ICOHP"][Spin.up]) == 241 + for data in [self.cohp_fe.cohp_data, self.coop_fe.cohp_data]: + for bond, val in data.items(): + if bond != "average": + assert val["length"] == lengths_sites_fe[bond][0] + assert val["sites"] == lengths_sites_fe[bond][1] + assert len(val["COHP"][Spin.up]) == 301 + assert len(val["ICOHP"][Spin.up]) == 301 + + # Lobster 3.1 + for data in [self.cohp_KF.cohp_data, self.coop_KF.cohp_data]: + for bond, val in data.items(): + if bond != "average": + assert val["length"] == lengths_sites_KF[bond][0] + assert val["sites"] == lengths_sites_KF[bond][1] + assert len(val["COHP"][Spin.up]) == 6 + assert len(val["ICOHP"][Spin.up]) == 6 + + for data in [self.cobi2.cohp_data]: + for bond, val in data.items(): + if bond != "average": + if int(bond) >= 13: + assert len(val["COHP"][Spin.up]) == 11 + assert len(val["cells"]) == 3 + else: + assert len(val["COHP"][Spin.up]) == 11 + assert len(val["cells"]) == 2 + + for data in [self.cobi3.cohp_data, self.cobi4.cohp_data]: + for bond, val in data.items(): + if bond != "average": + if int(bond) >= 13: + assert len(val["cells"]) == 4 + else: + assert len(val["cells"]) == 2 + for data in [self.cobi5.cohp_data]: + for bond, val in data.items(): + if bond != "average": + if int(bond) >= 25: + assert len(val["cells"]) == 4 + else: + assert len(val["cells"]) == 2 + for data in [self.cobi6.cohp_data]: + for bond, val in data.items(): + if bond != "average": + if int(bond) >= 21: + assert len(val["cells"]) == 3 + assert len(val["COHP"][Spin.up]) == 12 + assert len(val["COHP"][Spin.down]) == 12 + for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down], strict=False): + assert cohp1 == approx(cohp2, abs=1e-4) + else: + assert len(val["cells"]) == 2 + assert len(val["COHP"][Spin.up]) == 12 + assert len(val["COHP"][Spin.down]) == 12 + for cohp1, cohp2 in zip(val["COHP"][Spin.up], val["COHP"][Spin.down], strict=False): + assert cohp1 == approx(cohp2, abs=1e-3) + + def test_orbital_resolved_cohp(self): + orbitals = [(Orbital(jj), Orbital(ii)) for ii in range(4) for jj in range(4)] + assert self.cohp_bise.orb_res_cohp is None + assert self.coop_bise.orb_res_cohp is None + assert self.cohp_fe.orb_res_cohp is None + assert self.coop_fe.orb_res_cohp is None + assert self.orb_notot.cohp_data["1"]["COHP"] is None + assert self.orb_notot.cohp_data["1"]["ICOHP"] is None + for orbs in self.orb.orb_res_cohp["1"]: + orb_set = self.orb.orb_res_cohp["1"][orbs]["orbitals"] + assert orb_set[0][0] == 4 + assert orb_set[1][0] == 4 + assert (orb_set[0][1], orb_set[1][1]) in orbitals + + # test d and f orbitals + ref_list1 = [*[5] * 28, *[6] * 36, *[7] * 4] + ref_list2 = [ + *["f0"] * 4, + *["f1"] * 4, + *["f2"] * 4, + *["f3"] * 4, + *["f_1"] * 4, + *["f_2"] * 4, + *["f_3"] * 4, + *["dx2"] * 4, + *["dxy"] * 4, + *["dxz"] * 4, + *["dyz"] * 4, + *["dz2"] * 4, + *["px"] * 4, + *["py"] * 4, + *["pz"] * 4, + *["s"] * 8, + ] + for iorb, orbs in enumerate(sorted(self.cohp_Na2UO4.orb_res_cohp["49"])): + orb_set = self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["orbitals"] + assert orb_set[0][0] == ref_list1[iorb] + assert str(orb_set[0][1]) == ref_list2[iorb] + + # The sum of the orbital-resolved COHPs should be approximately + # the total COHP. Due to small deviations in the LOBSTER calculation, + # the precision is not very high though. + cohp = self.orb.cohp_data["1"]["COHP"][Spin.up] + icohp = self.orb.cohp_data["1"]["ICOHP"][Spin.up] + tot = np.sum( + [self.orb.orb_res_cohp["1"][orbs]["COHP"][Spin.up] for orbs in self.orb.orb_res_cohp["1"]], + axis=0, + ) + assert_allclose(tot, cohp, atol=1e-3) + tot = np.sum( + [self.orb.orb_res_cohp["1"][orbs]["ICOHP"][Spin.up] for orbs in self.orb.orb_res_cohp["1"]], + axis=0, + ) + assert_allclose(tot, icohp, atol=1e-3) + + # Lobster 3.1 + cohp_KF = self.cohp_KF.cohp_data["1"]["COHP"][Spin.up] + icohp_KF = self.cohp_KF.cohp_data["1"]["ICOHP"][Spin.up] + tot_KF = np.sum( + [self.cohp_KF.orb_res_cohp["1"][orbs]["COHP"][Spin.up] for orbs in self.cohp_KF.orb_res_cohp["1"]], + axis=0, + ) + assert_allclose(tot_KF, cohp_KF, atol=1e-3) + tot_KF = np.sum( + [self.cohp_KF.orb_res_cohp["1"][orbs]["ICOHP"][Spin.up] for orbs in self.cohp_KF.orb_res_cohp["1"]], + axis=0, + ) + assert_allclose(tot_KF, icohp_KF, atol=1e-3) + + # d and f orbitals + cohp_Na2UO4 = self.cohp_Na2UO4.cohp_data["49"]["COHP"][Spin.up] + icohp_Na2UO4 = self.cohp_Na2UO4.cohp_data["49"]["ICOHP"][Spin.up] + tot_Na2UO4 = np.sum( + [ + self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["COHP"][Spin.up] + for orbs in self.cohp_Na2UO4.orb_res_cohp["49"] + ], + axis=0, + ) + assert_allclose(tot_Na2UO4, cohp_Na2UO4, atol=1e-3) + tot_Na2UO4 = np.sum( + [ + self.cohp_Na2UO4.orb_res_cohp["49"][orbs]["ICOHP"][Spin.up] + for orbs in self.cohp_Na2UO4.orb_res_cohp["49"] + ], + axis=0, + ) + + assert_allclose(tot_Na2UO4, icohp_Na2UO4, atol=1e-3) + + assert "5s-4s-5s-4s" in self.cobi4.orb_res_cohp["13"] + assert "5px-4px-5px-4px" in self.cobi4.orb_res_cohp["13"] + assert len(self.cobi4.orb_res_cohp["13"]["5px-4px-5px-4px"]["COHP"][Spin.up]) == 11 + + assert "5s-4s-5s-4s" in self.cobi5.orb_res_cohp["25"] + assert "5px-4px-5px-4px" in self.cobi5.orb_res_cohp["25"] + assert len(self.cobi5.orb_res_cohp["25"]["5px-4px-5px-4px"]["COHP"][Spin.up]) == 11 + + assert len(self.cobi6.orb_res_cohp["21"]["2py-1s-2s"]["COHP"][Spin.up]) == 12 + assert len(self.cobi6.orb_res_cohp["21"]["2py-1s-2s"]["COHP"][Spin.down]) == 12 + + +class TestDoscar(TestCase): + def setUp(self): + # first for spin polarized version + doscar = f"{VASP_OUT_DIR}/DOSCAR.lobster.spin" + poscar = f"{VASP_IN_DIR}/POSCAR.lobster.spin_DOS" + + # not spin polarized + doscar2 = f"{VASP_OUT_DIR}/DOSCAR.lobster.nonspin" + poscar2 = f"{VASP_IN_DIR}/POSCAR.lobster.nonspin_DOS" + + self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) + self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) + + self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) + self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) + + with open(f"{TEST_FILES_DIR}/electronic_structure/dos/structure_KF.json") as file: + data = json.load(file) + + self.structure = Structure.from_dict(data) + + # test structure argument + self.DOSCAR_spin_pol2 = Doscar(doscar=doscar, structure_file=None, structure=Structure.from_file(poscar)) + + def test_complete_dos(self): + # first for spin polarized version + energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] + tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] + fermi = 0.0 + + pdos_f_2s_up = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + pdos_f_2s_down = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + pdos_f_2py_up = [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029] + pdos_f_2py_down = [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029] + pdos_f_2pz_up = [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029] + pdos_f_2pz_down = [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029] + pdos_f_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] + pdos_f_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] + + assert energies_spin == self.DOSCAR_spin_pol.completedos.energies.tolist() + assert tdos_up == self.DOSCAR_spin_pol.completedos.densities[Spin.up].tolist() + assert tdos_down == self.DOSCAR_spin_pol.completedos.densities[Spin.down].tolist() + assert fermi == approx(self.DOSCAR_spin_pol.completedos.efermi) + + assert_allclose( + self.DOSCAR_spin_pol.completedos.structure.frac_coords, + self.structure.frac_coords, + ) + assert_allclose( + self.DOSCAR_spin_pol2.completedos.structure.frac_coords, + self.structure.frac_coords, + ) + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == pdos_f_2s_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.down].tolist() == pdos_f_2s_down + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == pdos_f_2py_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.down].tolist() == pdos_f_2py_down + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == pdos_f_2pz_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.down].tolist() == pdos_f_2pz_down + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == pdos_f_2px_up + assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.down].tolist() == pdos_f_2px_down + + energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] + pdos_f_2s = [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060] + pdos_f_2py = [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037] + pdos_f_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] + pdos_f_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] + + assert energies_nonspin == self.DOSCAR_nonspin_pol.completedos.energies.tolist() + + assert tdos_nonspin == self.DOSCAR_nonspin_pol.completedos.densities[Spin.up].tolist() + + assert fermi == approx(self.DOSCAR_nonspin_pol.completedos.efermi) + + assert self.DOSCAR_nonspin_pol.completedos.structure == self.structure + + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == pdos_f_2s + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == pdos_f_2py + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == pdos_f_2pz + assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == pdos_f_2px + + def test_pdos(self): + # first for spin polarized version + + pdos_f_2s_up = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + pdos_f_2s_down = [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069] + pdos_f_2py_up = [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029] + pdos_f_2py_down = [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029] + pdos_f_2pz_up = [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029] + pdos_f_2pz_down = [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029] + pdos_f_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] + pdos_f_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] + + assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.up].tolist() == pdos_f_2s_up + assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.down].tolist() == pdos_f_2s_down + assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.up].tolist() == pdos_f_2py_up + assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.down].tolist() == pdos_f_2py_down + assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.up].tolist() == pdos_f_2pz_up + assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.down].tolist() == pdos_f_2pz_down + assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.up].tolist() == pdos_f_2px_up + assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.down].tolist() == pdos_f_2px_down + + # non spin + pdos_f_2s = [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060] + pdos_f_2py = [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037] + pdos_f_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] + pdos_f_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] + + assert self.DOSCAR_nonspin_pol.pdos[0]["2s"][Spin.up].tolist() == pdos_f_2s + assert self.DOSCAR_nonspin_pol.pdos[0]["2p_y"][Spin.up].tolist() == pdos_f_2py + assert self.DOSCAR_nonspin_pol.pdos[0]["2p_z"][Spin.up].tolist() == pdos_f_2pz + assert self.DOSCAR_nonspin_pol.pdos[0]["2p_x"][Spin.up].tolist() == pdos_f_2px + + def test_tdos(self): + # first for spin polarized version + energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] + tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] + fermi = 0.0 + + assert energies_spin == self.DOSCAR_spin_pol.tdos.energies.tolist() + assert tdos_up == self.DOSCAR_spin_pol.tdos.densities[Spin.up].tolist() + assert tdos_down == self.DOSCAR_spin_pol.tdos.densities[Spin.down].tolist() + assert fermi == approx(self.DOSCAR_spin_pol.tdos.efermi) + + energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] + fermi = 0.0 + + assert energies_nonspin == self.DOSCAR_nonspin_pol.tdos.energies.tolist() + assert tdos_nonspin == self.DOSCAR_nonspin_pol.tdos.densities[Spin.up].tolist() + assert fermi == approx(self.DOSCAR_nonspin_pol.tdos.efermi) + + def test_energies(self): + # first for spin polarized version + energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + + assert energies_spin == self.DOSCAR_spin_pol.energies.tolist() + + energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + assert energies_nonspin == self.DOSCAR_nonspin_pol.energies.tolist() + + def test_tdensities(self): + # first for spin polarized version + tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] + tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] + + assert tdos_up == self.DOSCAR_spin_pol.tdensities[Spin.up].tolist() + assert tdos_down == self.DOSCAR_spin_pol.tdensities[Spin.down].tolist() + + tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] + assert tdos_nonspin == self.DOSCAR_nonspin_pol.tdensities[Spin.up].tolist() + + def test_itdensities(self): + itdos_up = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09650] + itdos_down = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09685] + assert itdos_up == self.DOSCAR_spin_pol.itdensities[Spin.up].tolist() + assert itdos_down == self.DOSCAR_spin_pol.itdensities[Spin.down].tolist() + + itdos_nonspin = [4.00000, 10.00000, 10.00000, 16.00000, 16.00000, 16.09067] + assert itdos_nonspin == self.DOSCAR_nonspin_pol.itdensities[Spin.up].tolist() + + def test_is_spin_polarized(self): + # first for spin polarized version + assert self.DOSCAR_spin_pol.is_spin_polarized + + assert not self.DOSCAR_nonspin_pol.is_spin_polarized + + +class TestCharge(PymatgenTest): + def setUp(self): + self.charge2 = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.MnO") + # gzipped file + self.charge = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.MnO2.gz") + + def test_attributes(self): + charge_Loewdin = [-1.25, 1.25] + charge_Mulliken = [-1.30, 1.30] + atomlist = ["O1", "Mn2"] + types = ["O", "Mn"] + num_atoms = 2 + assert charge_Mulliken == self.charge2.Mulliken + assert charge_Loewdin == self.charge2.Loewdin + assert atomlist == self.charge2.atomlist + assert types == self.charge2.types + assert num_atoms == self.charge2.num_atoms + + def test_get_structure_with_charges(self): + structure_dict2 = { + "lattice": { + "c": 3.198244, + "volume": 23.132361565928807, + "b": 3.1982447183003364, + "gamma": 60.00000011873414, + "beta": 60.00000401737447, + "alpha": 60.00000742944491, + "matrix": [ + [2.769761, 0.0, 1.599122], + [0.923254, 2.611356, 1.599122], + [0.0, 0.0, 3.198244], + ], + "a": 3.1982443884113985, + }, + "@class": "Structure", + "sites": [ + { + "xyz": [1.846502883732, 1.305680611356, 3.198248797366], + "properties": {"Loewdin Charges": -1.25, "Mulliken Charges": -1.3}, + "abc": [0.499998, 0.500001, 0.500002], + "species": [{"occu": 1, "element": "O"}], + "label": "O", + }, + { + "xyz": [0.0, 0.0, 0.0], + "properties": {"Loewdin Charges": 1.25, "Mulliken Charges": 1.3}, + "abc": [0.0, 0.0, 0.0], + "species": [{"occu": 1, "element": "Mn"}], + "label": "Mn", + }, + ], + "charge": None, + "@module": "pymatgen.core.structure", + } + s2 = Structure.from_dict(structure_dict2) + assert s2 == self.charge2.get_structure_with_charges(f"{VASP_IN_DIR}/POSCAR_MnO") + + def test_msonable(self): + dict_data = self.charge2.as_dict() + charge_from_dict = Charge.from_dict(dict_data) + all_attributes = vars(self.charge2) + for attr_name, attr_value in all_attributes.items(): + assert getattr(charge_from_dict, attr_name) == attr_value + + +class TestLobsterout(PymatgenTest): + def setUp(self): + self.lobsterout_normal = Lobsterout(filename=f"{TEST_DIR}/lobsterout.normal") + # make sure .gz files are also read correctly + self.lobsterout_normal = Lobsterout(filename=f"{TEST_DIR}/lobsterout.normal2.gz") + self.lobsterout_fatband_grosspop_densityofenergies = Lobsterout( + filename=f"{TEST_DIR}/lobsterout.fatband_grosspop_densityofenergy" + ) + self.lobsterout_saveprojection = Lobsterout(filename=f"{TEST_DIR}/lobsterout.saveprojection") + self.lobsterout_skipping_all = Lobsterout(filename=f"{TEST_DIR}/lobsterout.skipping_all") + self.lobsterout_twospins = Lobsterout(filename=f"{TEST_DIR}/lobsterout.twospins") + self.lobsterout_GaAs = Lobsterout(filename=f"{TEST_DIR}/lobsterout.GaAs") + self.lobsterout_from_projection = Lobsterout(filename=f"{TEST_DIR}/lobsterout_from_projection") + self.lobsterout_onethread = Lobsterout(filename=f"{TEST_DIR}/lobsterout.onethread") + self.lobsterout_cobi_madelung = Lobsterout(filename=f"{TEST_DIR}/lobsterout_cobi_madelung") + self.lobsterout_doscar_lso = Lobsterout(filename=f"{TEST_DIR}/lobsterout_doscar_lso") + + # TODO: implement skipping madelung/cobi + self.lobsterout_skipping_cobi_madelung = Lobsterout(filename=f"{TEST_DIR}/lobsterout.skip_cobi_madelung") + + def test_attributes(self): + assert self.lobsterout_normal.basis_functions == [ + ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] + ] + assert self.lobsterout_normal.basis_type == ["pbeVaspFit2015"] + assert self.lobsterout_normal.charge_spilling == [0.0268] + assert self.lobsterout_normal.dft_program == "VASP" + assert self.lobsterout_normal.elements == ["Ti"] + assert self.lobsterout_normal.has_charge + assert self.lobsterout_normal.has_cohpcar + assert self.lobsterout_normal.has_coopcar + assert self.lobsterout_normal.has_doscar + assert not self.lobsterout_normal.has_projection + assert self.lobsterout_normal.has_bandoverlaps + assert not self.lobsterout_normal.has_density_of_energies + assert not self.lobsterout_normal.has_fatbands + assert not self.lobsterout_normal.has_grosspopulation + assert self.lobsterout_normal.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_normal.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_normal.is_restart_from_projection + assert self.lobsterout_normal.lobster_version == "v3.1.0" + assert self.lobsterout_normal.number_of_spins == 1 + assert self.lobsterout_normal.number_of_threads == 8 + assert self.lobsterout_normal.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "702"}, + "user_time": {"h": "0", "min": "0", "s": "20", "ms": "330"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, + } + assert self.lobsterout_normal.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_normal.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_fatband_grosspop_densityofenergies.basis_functions == [ + ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] + ] + assert self.lobsterout_fatband_grosspop_densityofenergies.basis_type == ["pbeVaspFit2015"] + assert self.lobsterout_fatband_grosspop_densityofenergies.charge_spilling == [0.0268] + assert self.lobsterout_fatband_grosspop_densityofenergies.dft_program == "VASP" + assert self.lobsterout_fatband_grosspop_densityofenergies.elements == ["Ti"] + assert self.lobsterout_fatband_grosspop_densityofenergies.has_charge + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_cohpcar + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_coopcar + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_doscar + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_projection + assert self.lobsterout_fatband_grosspop_densityofenergies.has_bandoverlaps + assert self.lobsterout_fatband_grosspop_densityofenergies.has_density_of_energies + assert self.lobsterout_fatband_grosspop_densityofenergies.has_fatbands + assert self.lobsterout_fatband_grosspop_densityofenergies.has_grosspopulation + assert self.lobsterout_fatband_grosspop_densityofenergies.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_fatband_grosspop_densityofenergies.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_fatband_grosspop_densityofenergies.is_restart_from_projection + assert self.lobsterout_fatband_grosspop_densityofenergies.lobster_version == "v3.1.0" + assert self.lobsterout_fatband_grosspop_densityofenergies.number_of_spins == 1 + assert self.lobsterout_fatband_grosspop_densityofenergies.number_of_threads == 8 + assert self.lobsterout_fatband_grosspop_densityofenergies.timing == { + "wall_time": {"h": "0", "min": "0", "s": "4", "ms": "136"}, + "user_time": {"h": "0", "min": "0", "s": "18", "ms": "280"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "290"}, + } + assert self.lobsterout_fatband_grosspop_densityofenergies.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_fatband_grosspop_densityofenergies.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_saveprojection.basis_functions == [ + ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] + ] + assert self.lobsterout_saveprojection.basis_type == ["pbeVaspFit2015"] + assert self.lobsterout_saveprojection.charge_spilling == [0.0268] + assert self.lobsterout_saveprojection.dft_program == "VASP" + assert self.lobsterout_saveprojection.elements == ["Ti"] + assert self.lobsterout_saveprojection.has_charge + assert not self.lobsterout_saveprojection.has_cohpcar + assert not self.lobsterout_saveprojection.has_coopcar + assert not self.lobsterout_saveprojection.has_doscar + assert self.lobsterout_saveprojection.has_projection + assert self.lobsterout_saveprojection.has_bandoverlaps + assert self.lobsterout_saveprojection.has_density_of_energies + assert not self.lobsterout_saveprojection.has_fatbands + assert not self.lobsterout_saveprojection.has_grosspopulation + assert self.lobsterout_saveprojection.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_saveprojection.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_saveprojection.is_restart_from_projection + assert self.lobsterout_saveprojection.lobster_version == "v3.1.0" + assert self.lobsterout_saveprojection.number_of_spins == 1 + assert self.lobsterout_saveprojection.number_of_threads == 8 + assert self.lobsterout_saveprojection.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "574"}, + "user_time": {"h": "0", "min": "0", "s": "18", "ms": "250"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "320"}, + } + assert self.lobsterout_saveprojection.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_saveprojection.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_skipping_all.basis_functions == [ + ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] + ] + assert self.lobsterout_skipping_all.basis_type == ["pbeVaspFit2015"] + assert self.lobsterout_skipping_all.charge_spilling == [0.0268] + assert self.lobsterout_skipping_all.dft_program == "VASP" + assert self.lobsterout_skipping_all.elements == ["Ti"] + assert not self.lobsterout_skipping_all.has_charge + assert not self.lobsterout_skipping_all.has_cohpcar + assert not self.lobsterout_skipping_all.has_coopcar + assert not self.lobsterout_skipping_all.has_doscar + assert not self.lobsterout_skipping_all.has_projection + assert self.lobsterout_skipping_all.has_bandoverlaps + assert not self.lobsterout_skipping_all.has_density_of_energies + assert not self.lobsterout_skipping_all.has_fatbands + assert not self.lobsterout_skipping_all.has_grosspopulation + assert not self.lobsterout_skipping_all.has_cobicar + assert not self.lobsterout_skipping_all.has_madelung + assert self.lobsterout_skipping_all.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_skipping_all.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_skipping_all.is_restart_from_projection + assert self.lobsterout_skipping_all.lobster_version == "v3.1.0" + assert self.lobsterout_skipping_all.number_of_spins == 1 + assert self.lobsterout_skipping_all.number_of_threads == 8 + assert self.lobsterout_skipping_all.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "117"}, + "user_time": {"h": "0", "min": "0", "s": "16", "ms": "79"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "320"}, + } + assert self.lobsterout_skipping_all.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_skipping_all.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_twospins.basis_functions == [ + [ + "4s", + "4p_y", + "4p_z", + "4p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ] + assert self.lobsterout_twospins.basis_type == ["pbeVaspFit2015"] + assert self.lobsterout_twospins.charge_spilling[0] == approx(0.36619999999999997) + assert self.lobsterout_twospins.charge_spilling[1] == approx(0.36619999999999997) + assert self.lobsterout_twospins.dft_program == "VASP" + assert self.lobsterout_twospins.elements == ["Ti"] + assert self.lobsterout_twospins.has_charge + assert self.lobsterout_twospins.has_cohpcar + assert self.lobsterout_twospins.has_coopcar + assert self.lobsterout_twospins.has_doscar + assert not self.lobsterout_twospins.has_projection + assert self.lobsterout_twospins.has_bandoverlaps + assert not self.lobsterout_twospins.has_density_of_energies + assert not self.lobsterout_twospins.has_fatbands + assert not self.lobsterout_twospins.has_grosspopulation + assert self.lobsterout_twospins.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 19 and upwards will be ignored.", + ] + assert self.lobsterout_twospins.info_orthonormalization == [ + "60 of 294 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_twospins.is_restart_from_projection + assert self.lobsterout_twospins.lobster_version == "v3.1.0" + assert self.lobsterout_twospins.number_of_spins == 2 + assert self.lobsterout_twospins.number_of_threads == 8 + assert self.lobsterout_twospins.timing == { + "wall_time": {"h": "0", "min": "0", "s": "3", "ms": "71"}, + "user_time": {"h": "0", "min": "0", "s": "22", "ms": "660"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, + } + assert self.lobsterout_twospins.total_spilling[0] == approx([0.2567][0]) + assert self.lobsterout_twospins.total_spilling[1] == approx([0.2567][0]) + assert self.lobsterout_twospins.warning_lines == [ + "60 of 294 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_from_projection.basis_functions == [] + assert self.lobsterout_from_projection.basis_type == [] + assert self.lobsterout_from_projection.charge_spilling[0] == approx(0.0177) + assert self.lobsterout_from_projection.dft_program is None + assert self.lobsterout_from_projection.elements == [] + assert self.lobsterout_from_projection.has_charge + assert self.lobsterout_from_projection.has_cohpcar + assert self.lobsterout_from_projection.has_coopcar + assert self.lobsterout_from_projection.has_doscar + assert not self.lobsterout_from_projection.has_projection + assert not self.lobsterout_from_projection.has_bandoverlaps + assert not self.lobsterout_from_projection.has_density_of_energies + assert not self.lobsterout_from_projection.has_fatbands + assert not self.lobsterout_from_projection.has_grosspopulation + assert self.lobsterout_from_projection.info_lines == [] + assert self.lobsterout_from_projection.info_orthonormalization == [] + assert self.lobsterout_from_projection.is_restart_from_projection + assert self.lobsterout_from_projection.lobster_version == "v3.1.0" + assert self.lobsterout_from_projection.number_of_spins == 1 + assert self.lobsterout_from_projection.number_of_threads == 8 + assert self.lobsterout_from_projection.timing == { + "wall_time": {"h": "0", "min": "2", "s": "1", "ms": "890"}, + "user_time": {"h": "0", "min": "15", "s": "10", "ms": "530"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "400"}, + } + assert self.lobsterout_from_projection.total_spilling[0] == approx([0.1543][0]) + assert self.lobsterout_from_projection.warning_lines == [] + + assert self.lobsterout_GaAs.basis_functions == [ + ["4s", "4p_y", "4p_z", "4p_x"], + [ + "4s", + "4p_y", + "4p_z", + "4p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ], + ] + assert self.lobsterout_GaAs.basis_type == ["Bunge", "Bunge"] + assert self.lobsterout_GaAs.charge_spilling[0] == approx(0.0089) + assert self.lobsterout_GaAs.dft_program == "VASP" + assert self.lobsterout_GaAs.elements == ["As", "Ga"] + assert self.lobsterout_GaAs.has_charge + assert self.lobsterout_GaAs.has_cohpcar + assert self.lobsterout_GaAs.has_coopcar + assert self.lobsterout_GaAs.has_doscar + assert not self.lobsterout_GaAs.has_projection + assert not self.lobsterout_GaAs.has_bandoverlaps + assert not self.lobsterout_GaAs.has_density_of_energies + assert not self.lobsterout_GaAs.has_fatbands + assert not self.lobsterout_GaAs.has_grosspopulation + assert self.lobsterout_GaAs.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 14 and upwards will be ignored.", + ] + assert self.lobsterout_GaAs.info_orthonormalization == [] + assert not self.lobsterout_GaAs.is_restart_from_projection + assert self.lobsterout_GaAs.lobster_version == "v3.1.0" + assert self.lobsterout_GaAs.number_of_spins == 1 + assert self.lobsterout_GaAs.number_of_threads == 8 + assert self.lobsterout_GaAs.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "726"}, + "user_time": {"h": "0", "min": "0", "s": "12", "ms": "370"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "180"}, + } + assert self.lobsterout_GaAs.total_spilling[0] == approx([0.0859][0]) + + assert self.lobsterout_onethread.number_of_threads == 1 + # Test lobsterout of lobster-4.1.0 + assert self.lobsterout_cobi_madelung.has_cobicar + assert self.lobsterout_cobi_madelung.has_cohpcar + assert self.lobsterout_cobi_madelung.has_madelung + assert not self.lobsterout_cobi_madelung.has_doscar_lso + + assert self.lobsterout_doscar_lso.has_doscar_lso + + assert self.lobsterout_skipping_cobi_madelung.has_cobicar is False + assert self.lobsterout_skipping_cobi_madelung.has_madelung is False + + def test_get_doc(self): + ref_data = { + "restart_from_projection": False, + "lobster_version": "v3.1.0", + "threads": 8, + "dft_program": "VASP", + "charge_spilling": [0.0268], + "total_spilling": [0.044000000000000004], + "elements": ["Ti"], + "basis_type": ["pbeVaspFit2015"], + "basis_functions": [ + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ], + "timing": { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "702"}, + "user_time": {"h": "0", "min": "0", "s": "20", "ms": "330"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, + }, + "warning_lines": [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ], + "info_orthonormalization": ["3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5."], + "info_lines": [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ], + "has_doscar": True, + "has_doscar_lso": False, + "has_cohpcar": True, + "has_coopcar": True, + "has_charge": True, + "has_projection": False, + "has_bandoverlaps": True, + "has_fatbands": False, + "has_grosspopulation": False, + "has_density_of_energies": False, + } + for key, item in self.lobsterout_normal.get_doc().items(): + if key not in ["has_cobicar", "has_madelung"]: + if isinstance(item, str): + assert ref_data[key], item + elif isinstance(item, int): + assert ref_data[key] == item + elif key in ("charge_spilling", "total_spilling"): + assert item[0] == approx(ref_data[key][0]) + elif isinstance(item, list | dict): + assert item == ref_data[key] + + def test_msonable(self): + dict_data = self.lobsterout_normal.as_dict() + lobsterout_from_dict = Lobsterout.from_dict(dict_data) + assert dict_data == lobsterout_from_dict.as_dict() + # test initialization with empty attributes (ensure file is not read again) + dict_data_empty = dict.fromkeys(self.lobsterout_doscar_lso._ATTRIBUTES, None) + lobsterout_empty_init_dict = Lobsterout.from_dict(dict_data_empty).as_dict() + for attribute in lobsterout_empty_init_dict: + if "@" not in attribute: + assert lobsterout_empty_init_dict[attribute] is None + + with pytest.raises(ValueError, match="invalid=val is not a valid attribute for Lobsterout"): + Lobsterout(filename=None, invalid="val") + + +class TestFatband(PymatgenTest): + def setUp(self): + self.structure = Vasprun( + filename=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", + ionic_step_skip=None, + ionic_step_offset=0, + parse_dos=True, + parse_eigen=False, + parse_projected_eigen=False, + parse_potcar_file=False, + occu_tol=1e-8, + exception_on_bad_xml=True, + ).final_structure + self.fatband_SiO2_p_x = Fatband( + filenames=f"{TEST_DIR}/Fatband_SiO2/Test_p_x", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", + structure=self.structure, + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", + ) + self.vasprun_SiO2_p_x = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml") + self.bs_symmline = self.vasprun_SiO2_p_x.get_band_structure(line_mode=True, force_hybrid_mode=True) + self.fatband_SiO2_p = Fatband( + filenames=f"{TEST_DIR}/Fatband_SiO2/Test_p", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p/vasprun.xml", + structure=self.structure, + ) + self.fatband_SiO2_p2 = Fatband( + filenames=f"{TEST_DIR}/Fatband_SiO2/Test_p", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p/KPOINTS", + structure=self.structure, + vasprun_file=None, + efermi=1.0647039, + ) + self.vasprun_SiO2_p = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p/vasprun.xml") + self.bs_symmline2 = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) + self.fatband_SiO2_spin = Fatband( + filenames=f"{TEST_DIR}/Fatband_SiO2/Test_Spin", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/vasprun.xml", + structure=self.structure, + ) + + self.vasprun_SiO2_spin = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/vasprun.xml") + self.bs_symmline_spin = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) + + def test_attributes(self): + assert list(self.fatband_SiO2_p_x.label_dict["M"]) == approx([0.5, 0.0, 0.0]) + assert self.fatband_SiO2_p_x.efermi == self.vasprun_SiO2_p_x.efermi + lattice1 = self.bs_symmline.lattice_rec.as_dict() + lattice2 = self.fatband_SiO2_p_x.lattice.as_dict() + for idx in range(3): + assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) + assert self.fatband_SiO2_p_x.eigenvals[Spin.up][1][1] - self.fatband_SiO2_p_x.efermi == -18.245 + assert self.fatband_SiO2_p_x.is_spinpolarized is False + assert self.fatband_SiO2_p_x.kpoints_array[3] == approx([0.03409091, 0, 0]) + assert self.fatband_SiO2_p_x.nbands == 36 + assert self.fatband_SiO2_p_x.p_eigenvals[Spin.up][2][1]["Si1"]["3p_x"] == 0.002 + assert self.fatband_SiO2_p_x.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) + assert self.fatband_SiO2_p_x.structure[0].species_string == "Si" + assert self.fatband_SiO2_p_x.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) + + assert list(self.fatband_SiO2_p.label_dict["M"]) == approx([0.5, 0.0, 0.0]) + assert self.fatband_SiO2_p.efermi == self.vasprun_SiO2_p.efermi + lattice1 = self.bs_symmline2.lattice_rec.as_dict() + lattice2 = self.fatband_SiO2_p.lattice.as_dict() + for idx in range(3): + assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) + assert self.fatband_SiO2_p.eigenvals[Spin.up][1][1] - self.fatband_SiO2_p.efermi == -18.245 + assert self.fatband_SiO2_p.is_spinpolarized is False + assert self.fatband_SiO2_p.kpoints_array[3] == approx([0.03409091, 0, 0]) + assert self.fatband_SiO2_p.nbands == 36 + assert self.fatband_SiO2_p.p_eigenvals[Spin.up][2][1]["Si1"]["3p"] == 0.042 + assert self.fatband_SiO2_p.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) + assert self.fatband_SiO2_p.structure[0].species_string == "Si" + assert self.fatband_SiO2_p.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) + assert self.fatband_SiO2_p.efermi == approx(1.0647039) + + assert list(self.fatband_SiO2_spin.label_dict["M"]) == approx([0.5, 0.0, 0.0]) + assert self.fatband_SiO2_spin.efermi == self.vasprun_SiO2_spin.efermi + lattice1 = self.bs_symmline_spin.lattice_rec.as_dict() + lattice2 = self.fatband_SiO2_spin.lattice.as_dict() + for idx in range(3): + assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) + assert self.fatband_SiO2_spin.eigenvals[Spin.up][1][1] - self.fatband_SiO2_spin.efermi == -18.245 + assert self.fatband_SiO2_spin.eigenvals[Spin.down][1][1] - self.fatband_SiO2_spin.efermi == -18.245 + assert self.fatband_SiO2_spin.is_spinpolarized + assert self.fatband_SiO2_spin.kpoints_array[3] == approx([0.03409091, 0, 0]) + assert self.fatband_SiO2_spin.nbands == 36 + + assert self.fatband_SiO2_spin.p_eigenvals[Spin.up][2][1]["Si1"]["3p"] == 0.042 + assert self.fatband_SiO2_spin.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) + assert self.fatband_SiO2_spin.structure[0].species_string == "Si" + assert self.fatband_SiO2_spin.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) + + def test_raises(self): + with pytest.raises(ValueError, match="vasprun_file or efermi have to be provided"): + Fatband( + filenames=f"{TEST_DIR}/Fatband_SiO2/Test_Spin", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/KPOINTS", + vasprun_file=None, + structure=self.structure, + ) + with pytest.raises( + ValueError, match="The are two FATBAND files for the same atom and orbital. The program will stop" + ): + self.fatband_SiO2_p_x = Fatband( + filenames=[ + f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + ], + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", + structure=self.structure, + ) + + with pytest.raises(ValueError, match="A structure object has to be provided"): + self.fatband_SiO2_p_x = Fatband( + filenames=[ + f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + ], + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", + structure=None, + ) + + with pytest.raises( + ValueError, + match=r"Make sure all relevant orbitals were generated and that no duplicates \(2p and 2p_x\) are present", + ): + self.fatband_SiO2_p_x = Fatband( + filenames=[ + f"{TEST_DIR}/Fatband_SiO2/Test_p_x/FATBAND_si1_3p_x.lobster", + f"{TEST_DIR}/Fatband_SiO2/Test_p/FATBAND_si1_3p.lobster", + ], + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", + structure=self.structure, + ) + + with pytest.raises(ValueError, match="No FATBAND files in folder or given"): + self.fatband_SiO2_p_x = Fatband( + filenames=".", + kpoints_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/KPOINTS", + vasprun_file=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", + structure=self.structure, + ) + + def test_get_bandstructure(self): + bs_p = self.fatband_SiO2_p.get_bandstructure() + atom1 = bs_p.structure[0] + atom2 = self.bs_symmline2.structure[0] + assert atom1.frac_coords[0] == approx(atom2.frac_coords[0]) + assert atom1.frac_coords[1] == approx(atom2.frac_coords[1]) + assert atom1.frac_coords[2] == approx(atom2.frac_coords[2]) + assert atom1.coords[0] == approx(atom2.coords[0]) + assert atom1.coords[1] == approx(atom2.coords[1]) + assert atom1.coords[2] == approx(atom2.coords[2]) + assert atom1.species_string == atom2.species_string + assert bs_p.efermi == self.bs_symmline2.efermi + branch1 = bs_p.branches[0] + branch2 = self.bs_symmline2.branches[0] + assert branch2["name"] == branch1["name"] + assert branch2["start_index"] == branch1["start_index"] + assert branch2["end_index"] == branch1["end_index"] + + assert bs_p.distance[30] == approx(self.bs_symmline2.distance[30]) + lattice1 = bs_p.lattice_rec.as_dict() + lattice2 = self.bs_symmline2.lattice_rec.as_dict() + assert lattice1["matrix"][0] == approx(lattice2["matrix"][0]) + assert lattice1["matrix"][1] == approx(lattice2["matrix"][1]) + assert lattice1["matrix"][2] == approx(lattice2["matrix"][2]) + + assert bs_p.kpoints[8].frac_coords[0] == approx(self.bs_symmline2.kpoints[8].frac_coords[0]) + assert bs_p.kpoints[8].frac_coords[1] == approx(self.bs_symmline2.kpoints[8].frac_coords[1]) + assert bs_p.kpoints[8].frac_coords[2] == approx(self.bs_symmline2.kpoints[8].frac_coords[2]) + assert bs_p.kpoints[8].cart_coords[0] == approx(self.bs_symmline2.kpoints[8].cart_coords[0]) + assert bs_p.kpoints[8].cart_coords[1] == approx(self.bs_symmline2.kpoints[8].cart_coords[1]) + assert bs_p.kpoints[8].cart_coords[2] == approx(self.bs_symmline2.kpoints[8].cart_coords[2]) + assert bs_p.kpoints[50].frac_coords[0] == approx(self.bs_symmline2.kpoints[50].frac_coords[0]) + assert bs_p.kpoints[50].frac_coords[1] == approx(self.bs_symmline2.kpoints[50].frac_coords[1]) + assert bs_p.kpoints[50].frac_coords[2] == approx(self.bs_symmline2.kpoints[50].frac_coords[2]) + assert bs_p.kpoints[50].cart_coords[0] == approx(self.bs_symmline2.kpoints[50].cart_coords[0]) + assert bs_p.kpoints[50].cart_coords[1] == approx(self.bs_symmline2.kpoints[50].cart_coords[1]) + assert bs_p.kpoints[50].cart_coords[2] == approx(self.bs_symmline2.kpoints[50].cart_coords[2]) + assert bs_p.get_band_gap()["energy"] == approx(self.bs_symmline2.get_band_gap()["energy"], abs=1e-2) + assert bs_p.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064)) + assert bs_p.get_projections_on_elements_and_orbitals({"Si": ["3p"]})[Spin.up][0][0]["Si"]["3p"] == approx(0.003) + assert bs_p.get_projections_on_elements_and_orbitals({"O": ["2p"]})[Spin.up][0][0]["O"]["2p"] == approx( + 0.002 * 3 + 0.003 * 3 + ) + dict_here = bs_p.get_projections_on_elements_and_orbitals({"Si": ["3s", "3p"], "O": ["2s", "2p"]})[Spin.up][0][ + 0 + ] + assert dict_here["Si"]["3s"] == approx(0.192) + assert dict_here["Si"]["3p"] == approx(0.003) + assert dict_here["O"]["2s"] == approx(0.792) + assert dict_here["O"]["2p"] == approx(0.015) + + bs_spin = self.fatband_SiO2_spin.get_bandstructure() + assert bs_spin.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064)) + assert bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3p"]})[Spin.up][0][0]["Si"]["3p"] == approx( + 0.003 + ) + assert bs_spin.get_projections_on_elements_and_orbitals({"O": ["2p"]})[Spin.up][0][0]["O"]["2p"] == approx( + 0.002 * 3 + 0.003 * 3 + ) + dict_here = bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3s", "3p"], "O": ["2s", "2p"]})[Spin.up][ + 0 + ][0] + assert dict_here["Si"]["3s"] == approx(0.192) + assert dict_here["Si"]["3p"] == approx(0.003) + assert dict_here["O"]["2s"] == approx(0.792) + assert dict_here["O"]["2p"] == approx(0.015) + + assert bs_spin.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064)) + assert bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3p"]})[Spin.down][0][0]["Si"]["3p"] == approx( + 0.003 + ) + assert bs_spin.get_projections_on_elements_and_orbitals({"O": ["2p"]})[Spin.down][0][0]["O"]["2p"] == approx( + 0.002 * 3 + 0.003 * 3 + ) + dict_here = bs_spin.get_projections_on_elements_and_orbitals({"Si": ["3s", "3p"], "O": ["2s", "2p"]})[ + Spin.down + ][0][0] + assert dict_here["Si"]["3s"] == approx(0.192) + assert dict_here["Si"]["3p"] == approx(0.003) + assert dict_here["O"]["2s"] == approx(0.792) + assert dict_here["O"]["2p"] == approx(0.015) + bs_p_x = self.fatband_SiO2_p_x.get_bandstructure() + assert bs_p_x.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064), abs=1e-2) + + +class TestBandoverlaps(TestCase): + def setUp(self): + # test spin-polarized calc and non spinpolarized calc + + self.band_overlaps1 = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.1") + self.band_overlaps2 = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.2") + + self.band_overlaps1_new = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.new.1") + self.band_overlaps2_new = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.new.2") + + def test_attributes(self): + # bandoverlapsdict + bo_dict = self.band_overlaps1.bandoverlapsdict + assert bo_dict[Spin.up]["max_deviations"][0] == approx(0.000278953) + assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["max_deviations"][10] == approx(0.0640933) + assert bo_dict[Spin.up]["matrices"][0].item(-1, -1) == approx(0.0188058) + assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["matrices"][10].item(-1, -1) == approx(1.0) + assert bo_dict[Spin.up]["matrices"][0].item(0, 0) == approx(1) + assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["matrices"][10].item(0, 0) == approx(0.995849) + + assert bo_dict[Spin.down]["max_deviations"][-1] == approx(4.31567e-05) + assert self.band_overlaps1_new.bandoverlapsdict[Spin.down]["max_deviations"][9] == approx(0.064369) + assert bo_dict[Spin.down]["matrices"][-1].item(0, -1) == approx(4.0066e-07) + assert self.band_overlaps1_new.bandoverlapsdict[Spin.down]["matrices"][9].item(0, -1) == approx(1.37447e-09) + + # maxDeviation + assert self.band_overlaps1.max_deviation[0] == approx(0.000278953) + assert self.band_overlaps1_new.max_deviation[0] == approx(0.39824) + assert self.band_overlaps1.max_deviation[-1] == approx(4.31567e-05) + assert self.band_overlaps1_new.max_deviation[-1] == approx(0.324898) + + assert self.band_overlaps2.max_deviation[0] == approx(0.000473319) + assert self.band_overlaps2_new.max_deviation[0] == approx(0.403249) + assert self.band_overlaps2.max_deviation[-1] == approx(1.48451e-05) + assert self.band_overlaps2_new.max_deviation[-1] == approx(0.45154) + + def test_has_good_quality(self): + assert not self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=0.1) + assert not self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=0.1) + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=9, + number_occ_bands_spin_down=5, + limit_deviation=0.1, + spin_polarized=True, + ) + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=9, + number_occ_bands_spin_down=5, + limit_deviation=0.1, + spin_polarized=True, + ) + assert self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=3, + number_occ_bands_spin_down=0, + limit_deviation=1, + spin_polarized=True, + ) + assert self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=3, + number_occ_bands_spin_down=0, + limit_deviation=1, + spin_polarized=True, + ) + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=1, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=1, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=0, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=0, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=0, + number_occ_bands_spin_down=1, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=0, + number_occ_bands_spin_down=1, + limit_deviation=0.000001, + spin_polarized=True, + ) + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + number_occ_bands_spin_down=4, + limit_deviation=0.001, + spin_polarized=True, + ) + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + number_occ_bands_spin_down=4, + limit_deviation=0.001, + spin_polarized=True, + ) + + assert self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=100) + assert self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=100) + assert self.band_overlaps2.has_good_quality_maxDeviation() + assert not self.band_overlaps2_new.has_good_quality_maxDeviation() + assert not self.band_overlaps2.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) + assert not self.band_overlaps2_new.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) + assert not self.band_overlaps2.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=10, limit_deviation=0.0000001 + ) + assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=10, limit_deviation=0.0000001 + ) + assert not self.band_overlaps2.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=0.1 + ) + + assert not self.band_overlaps2.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=1e-8 + ) + assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=1e-8 + ) + assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=10, limit_deviation=1) + assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=2, limit_deviation=0.1 + ) + assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=1, limit_deviation=1) + assert self.band_overlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=2 + ) + + def test_msonable(self): + dict_data = self.band_overlaps2_new.as_dict() + bandoverlaps_from_dict = Bandoverlaps.from_dict(dict_data) + all_attributes = vars(self.band_overlaps2_new) + for attr_name, attr_value in all_attributes.items(): + assert getattr(bandoverlaps_from_dict, attr_name) == attr_value + + def test_keys(self): + bo_dict = self.band_overlaps1.band_overlaps_dict + bo_dict_new = self.band_overlaps1_new.band_overlaps_dict + bo_dict_2 = self.band_overlaps2.band_overlaps_dict + assert len(bo_dict[Spin.up]["k_points"]) == 408 + assert len(bo_dict_2[Spin.up]["max_deviations"]) == 2 + assert len(bo_dict_new[Spin.down]["matrices"]) == 73 + + +class TestGrosspop(TestCase): + def setUp(self): + self.grosspop1 = Grosspop(f"{TEST_DIR}/GROSSPOP.lobster") + + def test_attributes(self): + gross_pop_list = self.grosspop1.list_dict_grosspop + assert gross_pop_list[0]["Mulliken GP"]["3s"] == approx(0.52) + assert gross_pop_list[0]["Mulliken GP"]["3p_y"] == approx(0.38) + assert gross_pop_list[0]["Mulliken GP"]["3p_z"] == approx(0.37) + assert gross_pop_list[0]["Mulliken GP"]["3p_x"] == approx(0.37) + assert gross_pop_list[0]["Mulliken GP"]["total"] == approx(1.64) + assert gross_pop_list[0]["element"] == "Si" + assert gross_pop_list[0]["Loewdin GP"]["3s"] == approx(0.61) + assert gross_pop_list[0]["Loewdin GP"]["3p_y"] == approx(0.52) + assert gross_pop_list[0]["Loewdin GP"]["3p_z"] == approx(0.52) + assert gross_pop_list[0]["Loewdin GP"]["3p_x"] == approx(0.52) + assert gross_pop_list[0]["Loewdin GP"]["total"] == approx(2.16) + assert gross_pop_list[5]["Mulliken GP"]["2s"] == approx(1.80) + assert gross_pop_list[5]["Loewdin GP"]["2s"] == approx(1.60) + assert gross_pop_list[5]["element"] == "O" + assert gross_pop_list[8]["Mulliken GP"]["2s"] == approx(1.80) + assert gross_pop_list[8]["Loewdin GP"]["2s"] == approx(1.60) + assert gross_pop_list[8]["element"] == "O" + + def test_structure_with_grosspop(self): + struct_dict = { + "@module": "pymatgen.core.structure", + "@class": "Structure", + "charge": None, + "lattice": { + "matrix": [ + [5.021897888834907, 4.53806e-11, 0.0], + [-2.5109484443388332, 4.349090983701526, 0.0], + [0.0, 0.0, 5.511929408565514], + ], + "a": 5.021897888834907, + "b": 5.0218974974248045, + "c": 5.511929408565514, + "alpha": 90.0, + "beta": 90.0, + "gamma": 119.99999598960493, + "volume": 120.38434608659402, + }, + "sites": [ + { + "species": [{"element": "Si", "occu": 1}], + "abc": [-3e-16, 0.4763431475490085, 0.6666669999999968], + "xyz": [-1.1960730853096477, 2.0716596881533986, 3.674621443020128], + "label": "Si", + "properties": {"Total Mulliken GP": 1.64, "Total Loewdin GP": 2.16}, + }, + { + "species": [{"element": "Si", "occu": 1}], + "abc": [0.5236568524509936, 0.5236568524509926, 0.0], + "xyz": [1.3148758827683875, 2.277431295571896, 0.0], + "label": "Si", + "properties": {"Total Mulliken GP": 1.64, "Total Loewdin GP": 2.16}, + }, + { + "species": [{"element": "Si", "occu": 1}], + "abc": [0.4763431475490066, -1.2e-15, 0.3333330000000032], + "xyz": [ + 2.392146647037334, + 2.1611518932482004e-11, + 1.8373079655453863, + ], + "label": "Si", + "properties": {"Total Mulliken GP": 1.64, "Total Loewdin GP": 2.16}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.1589037798059321, 0.7440031622164922, 0.4613477252144715], + "xyz": [-1.0701550264153763, 3.235737444648381, 2.5429160941844473], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.2559968377835071, 0.4149006175894398, 0.7946807252144676], + "xyz": [0.2437959189219816, 1.8044405351020447, 4.380224059729795], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.5850993824105679, 0.8410962201940679, 0.1280147252144683], + "xyz": [0.8263601076506712, 3.6580039876980064, 0.7056081286390611], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.7440031622164928, 0.1589037798059326, 0.5386522747855285], + "xyz": [3.337308710918233, 0.6910869960638374, 2.969013314381067], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.4149006175894392, 0.2559968377835, 0.2053192747855324], + "xyz": [1.4407936739605638, 1.1133535390791505, 1.13170534883572], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + { + "species": [{"element": "O", "occu": 1}], + "abc": [0.841096220194068, 0.5850993824105675, 0.8719852747855317], + "xyz": [2.754744948452184, 2.5446504486493, 4.806321279926453], + "label": "O", + "properties": {"Total Mulliken GP": 7.18, "Total Loewdin GP": 6.92}, + }, + ], + } + + new_structure = self.grosspop1.get_structure_with_total_grosspop(f"{TEST_DIR}/POSCAR.SiO2") + assert_allclose(new_structure.frac_coords, Structure.from_dict(struct_dict).frac_coords) + + def test_msonable(self): + dict_data = self.grosspop1.as_dict() + grosspop_from_dict = Grosspop.from_dict(dict_data) + all_attributes = vars(self.grosspop1) + for attr_name, attr_value in all_attributes.items(): + assert getattr(grosspop_from_dict, attr_name) == attr_value + + +class TestIcohplist(TestCase): + def setUp(self): + self.icohp_bise = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster.BiSe") + self.icoop_bise = Icohplist( + filename=f"{TEST_DIR}/ICOOPLIST.lobster.BiSe", + are_coops=True, + ) + self.icohp_fe = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster") + # allow gzipped files + self.icohp_gzipped = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster.gz") + self.icoop_fe = Icohplist( + filename=f"{TEST_DIR}/ICOHPLIST.lobster", + are_coops=True, + ) + # ICOBIs and orbitalwise ICOBILIST.lobster + self.icobi_orbitalwise = Icohplist( + filename=f"{TEST_DIR}/ICOBILIST.lobster", + are_cobis=True, + ) + + self.icobi = Icohplist( + filename=f"{TEST_DIR}/ICOBILIST.lobster.withoutorbitals", + are_cobis=True, + ) + self.icobi_orbitalwise_spinpolarized = Icohplist( + filename=f"{TEST_DIR}/ICOBILIST.lobster.spinpolarized", + are_cobis=True, + ) + # make sure the correct line is read to check if this is a orbitalwise ICOBILIST + self.icobi_orbitalwise_add = Icohplist( + filename=f"{TEST_DIR}/ICOBILIST.lobster.additional_case", + are_cobis=True, + ) + self.icobi_orbitalwise_spinpolarized_add = Icohplist( + filename=f"{TEST_DIR}/ICOBILIST.lobster.spinpolarized.additional_case", + are_cobis=True, + ) + + def test_attributes(self): + assert not self.icohp_bise.are_coops + assert self.icoop_bise.are_coops + assert not self.icohp_bise.is_spin_polarized + assert not self.icoop_bise.is_spin_polarized + assert len(self.icohp_bise.icohplist) == 11 + assert len(self.icoop_bise.icohplist) == 11 + assert not self.icohp_fe.are_coops + assert self.icoop_fe.are_coops + assert self.icohp_fe.is_spin_polarized + assert self.icoop_fe.is_spin_polarized + assert len(self.icohp_fe.icohplist) == 2 + assert len(self.icoop_fe.icohplist) == 2 + # test are_cobis + assert not self.icohp_fe.are_coops + assert not self.icohp_fe.are_cobis + assert self.icoop_fe.are_coops + assert not self.icoop_fe.are_cobis + assert self.icobi.are_cobis + assert not self.icobi.are_coops + + # orbitalwise + assert self.icobi_orbitalwise.orbitalwise + assert not self.icobi.orbitalwise + + assert self.icobi_orbitalwise_spinpolarized.orbitalwise + + assert self.icobi_orbitalwise_add.orbitalwise + assert self.icobi_orbitalwise_spinpolarized_add.orbitalwise + + def test_values(self): + icohplist_bise = { + "1": { + "length": 2.88231, + "number_of_bonds": 3, + "icohp": {Spin.up: -2.18042}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "2": { + "length": 3.10144, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.14347}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "3": { + "length": 2.88231, + "number_of_bonds": 3, + "icohp": {Spin.up: -2.18042}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "4": { + "length": 3.10144, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.14348}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "5": { + "length": 3.05001, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.30006}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "6": { + "length": 2.91676, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.96843}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "7": { + "length": 3.05001, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.30006}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "8": { + "length": 2.91676, + "number_of_bonds": 3, + "icohp": {Spin.up: -1.96843}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "9": { + "length": 3.37522, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.47531}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "10": { + "length": 3.07294, + "number_of_bonds": 3, + "icohp": {Spin.up: -2.38796}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "11": { + "length": 3.37522, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.47531}, + "translation": (0, 0, 0), + "orbitals": None, + }, + } + icooplist_bise = { + "1": { + "length": 2.88231, + "number_of_bonds": 3, + "icohp": {Spin.up: 0.14245}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "2": { + "length": 3.10144, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.04118}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "3": { + "length": 2.88231, + "number_of_bonds": 3, + "icohp": {Spin.up: 0.14245}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "4": { + "length": 3.10144, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.04118}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "5": { + "length": 3.05001, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.03516}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "6": { + "length": 2.91676, + "number_of_bonds": 3, + "icohp": {Spin.up: 0.10745}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "7": { + "length": 3.05001, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.03516}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "8": { + "length": 2.91676, + "number_of_bonds": 3, + "icohp": {Spin.up: 0.10745}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "9": { + "length": 3.37522, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.12395}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "10": { + "length": 3.07294, + "number_of_bonds": 3, + "icohp": {Spin.up: 0.24714}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "11": { + "length": 3.37522, + "number_of_bonds": 3, + "icohp": {Spin.up: -0.12395}, + "translation": (0, 0, 0), + "orbitals": None, + }, + } + icooplist_fe = { + "1": { + "length": 2.83189, + "number_of_bonds": 2, + "icohp": {Spin.up: -0.10218, Spin.down: -0.19701}, + "translation": (0, 0, 0), + "orbitals": None, + }, + "2": { + "length": 2.45249, + "number_of_bonds": 1, + "icohp": {Spin.up: -0.28485, Spin.down: -0.58279}, + "translation": (0, 0, 0), + "orbitals": None, + }, + } + + assert icohplist_bise == self.icohp_bise.icohplist + assert self.icohp_bise.icohpcollection.extremum_icohpvalue() == -2.38796 + assert icooplist_fe == self.icoop_fe.icohplist + assert self.icoop_fe.icohpcollection.extremum_icohpvalue() == -0.29919 + assert icooplist_bise == self.icoop_bise.icohplist + assert self.icoop_bise.icohpcollection.extremum_icohpvalue() == 0.24714 + assert self.icobi.icohplist["1"]["icohp"][Spin.up] == approx(0.58649) + assert self.icobi_orbitalwise.icohplist["2"]["icohp"][Spin.up] == approx(0.58649) + assert self.icobi_orbitalwise.icohplist["1"]["icohp"][Spin.up] == approx(0.58649) + assert self.icobi_orbitalwise_spinpolarized.icohplist["1"]["icohp"][Spin.up] == approx(0.58649 / 2, abs=1e-3) + assert self.icobi_orbitalwise_spinpolarized.icohplist["1"]["icohp"][Spin.down] == approx(0.58649 / 2, abs=1e-3) + assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["icohp"][Spin.down] == approx(0.58649 / 2, abs=1e-3) + assert self.icobi.icohpcollection.extremum_icohpvalue() == 0.58649 + assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["orbitals"]["2s-6s"]["icohp"][Spin.up] == 0.0247 + + def test_msonable(self): + dict_data = self.icobi_orbitalwise_spinpolarized.as_dict() + icohplist_from_dict = Icohplist.from_dict(dict_data) + all_attributes = vars(self.icobi_orbitalwise_spinpolarized) + for attr_name, attr_value in all_attributes.items(): + if isinstance(attr_value, IcohpCollection): + assert getattr(icohplist_from_dict, attr_name).as_dict() == attr_value.as_dict() + else: + assert getattr(icohplist_from_dict, attr_name) == attr_value + + +class TestNciCobiList(TestCase): + def setUp(self): + self.ncicobi = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster") + self.ncicobi_gz = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.gz") + self.ncicobi_no_spin = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.nospin") + self.ncicobi_no_spin_wo = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.nospin.withoutorbitals") + self.ncicobi_wo = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.withoutorbitals") + + def test_ncicobilist(self): + assert self.ncicobi.is_spin_polarized + assert not self.ncicobi_no_spin.is_spin_polarized + assert self.ncicobi_wo.is_spin_polarized + assert not self.ncicobi_no_spin_wo.is_spin_polarized + assert self.ncicobi.orbital_wise + assert self.ncicobi_no_spin.orbital_wise + assert not self.ncicobi_wo.orbital_wise + assert not self.ncicobi_no_spin_wo.orbital_wise + assert len(self.ncicobi.ncicobi_list) == 2 + assert self.ncicobi.ncicobi_list["2"]["number_of_atoms"] == 3 + assert self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.up] == approx(0.00009) + assert self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.down] == approx(0.00009) + assert self.ncicobi.ncicobi_list["2"]["interaction_type"] == "[X22[0,0,0]->Xs42[0,0,0]->X31[0,0,0]]" + assert ( + self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.up] == self.ncicobi_wo.ncicobi_list["2"]["ncicobi"][Spin.up] + ) + assert ( + self.ncicobi.ncicobi_list["2"]["ncicobi"][Spin.up] == self.ncicobi_gz.ncicobi_list["2"]["ncicobi"][Spin.up] + ) + assert ( + self.ncicobi.ncicobi_list["2"]["interaction_type"] == self.ncicobi_gz.ncicobi_list["2"]["interaction_type"] + ) + assert sum(self.ncicobi.ncicobi_list["2"]["ncicobi"].values()) == approx( + self.ncicobi_no_spin.ncicobi_list["2"]["ncicobi"][Spin.up] + ) + + +class TestWavefunction(PymatgenTest): + def test_parse_file(self): + grid, points, real, imaginary, distance = Wavefunction._parse_file( + f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz" + ) + assert_array_equal([41, 41, 41], grid) + assert points[4][0] == approx(0.0000) + assert points[4][1] == approx(0.0000) + assert points[4][2] == approx(0.4000) + assert real[8] == approx(1.38863e-01) + assert imaginary[8] == approx(2.89645e-01) + assert len(imaginary) == 41 * 41 * 41 + assert len(real) == 41 * 41 * 41 + assert len(points) == 41 * 41 * 41 + assert distance[0] == approx(0.0000) + + def test_set_volumetric_data(self): + wave1 = Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ) + + wave1.set_volumetric_data(grid=wave1.grid, structure=wave1.structure) + assert wave1.volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) + assert wave1.volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) + + def test_get_volumetricdata_real(self): + wave1 = Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ) + volumetricdata_real = wave1.get_volumetricdata_real() + assert volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) + + def test_get_volumetricdata_imaginary(self): + wave1 = Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ) + volumetricdata_imaginary = wave1.get_volumetricdata_imaginary() + assert volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) + + def test_get_volumetricdata_density(self): + wave1 = Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ) + volumetricdata_density = wave1.get_volumetricdata_density() + assert volumetricdata_density.data["total"][0, 0, 0] == approx((-3.0966 * -3.0966) + (-6.45895 * -6.45895)) + + def test_write_file(self): + wave1 = Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ) + real_wavecar_path = f"{self.tmp_path}/real-wavecar.vasp" + wave1.write_file(filename=real_wavecar_path, part="real") + assert os.path.isfile(real_wavecar_path) + + imag_wavecar_path = f"{self.tmp_path}/imaginary-wavecar.vasp" + wave1.write_file(filename=imag_wavecar_path, part="imaginary") + assert os.path.isfile(imag_wavecar_path) + + density_wavecar_path = f"{self.tmp_path}/density-wavecar.vasp" + wave1.write_file(filename=density_wavecar_path, part="density") + assert os.path.isfile(density_wavecar_path) + + +class TestSitePotentials(PymatgenTest): + def setUp(self) -> None: + self.sitepotential = SitePotential(filename=f"{TEST_DIR}/SitePotentials.lobster.perovskite") + + def test_attributes(self): + assert self.sitepotential.sitepotentials_Loewdin == [-8.77, -17.08, 9.57, 9.57, 8.45] + assert self.sitepotential.sitepotentials_Mulliken == [-11.38, -19.62, 11.18, 11.18, 10.09] + assert self.sitepotential.madelungenergies_Loewdin == approx(-28.64) + assert self.sitepotential.madelungenergies_Mulliken == approx(-40.02) + assert self.sitepotential.atomlist == ["La1", "Ta2", "N3", "N4", "O5"] + assert self.sitepotential.types == ["La", "Ta", "N", "N", "O"] + assert self.sitepotential.num_atoms == 5 + assert self.sitepotential.ewald_splitting == approx(3.14) + + def test_get_structure(self): + structure = self.sitepotential.get_structure_with_site_potentials(f"{TEST_DIR}/POSCAR.perovskite") + assert structure.site_properties["Loewdin Site Potentials (eV)"] == [-8.77, -17.08, 9.57, 9.57, 8.45] + assert structure.site_properties["Mulliken Site Potentials (eV)"] == [-11.38, -19.62, 11.18, 11.18, 10.09] + + def test_msonable(self): + dict_data = self.sitepotential.as_dict() + sitepotential_from_dict = SitePotential.from_dict(dict_data) + all_attributes = vars(self.sitepotential) + for attr_name, attr_value in all_attributes.items(): + assert getattr(sitepotential_from_dict, attr_name) == attr_value + + +class TestMadelungEnergies(PymatgenTest): + def setUp(self) -> None: + self.madelungenergies = MadelungEnergies(filename=f"{TEST_DIR}/MadelungEnergies.lobster.perovskite") + + def test_attributes(self): + assert self.madelungenergies.madelungenergies_Loewdin == approx(-28.64) + assert self.madelungenergies.madelungenergies_Mulliken == approx(-40.02) + assert self.madelungenergies.ewald_splitting == approx(3.14) + + def test_msonable(self): + dict_data = self.madelungenergies.as_dict() + madelung_from_dict = MadelungEnergies.from_dict(dict_data) + all_attributes = vars(self.madelungenergies) + for attr_name, attr_value in all_attributes.items(): + assert getattr(madelung_from_dict, attr_name) == attr_value + + +class TestLobsterMatrices(PymatgenTest): + def setUp(self) -> None: + self.hamilton_matrices = LobsterMatrices( + filename=f"{TEST_DIR}/Na_hamiltonMatrices.lobster.gz", e_fermi=-2.79650354 + ) + self.transfer_matrices = LobsterMatrices(filename=f"{TEST_DIR}/C_transferMatrices.lobster.gz") + self.overlap_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Si_overlapMatrices.lobster.gz") + self.coeff_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Si_coefficientMatricesLSO1.lobster.gz") + + def test_attributes(self): + # hamilton matrices + assert self.hamilton_matrices.average_onsite_energies == pytest.approx( + {"Na1_3s": 0.58855353, "Na1_2p_y": -25.72719646, "Na1_2p_z": -25.72719646, "Na1_2p_x": -25.72719646} + ) + ref_onsite_energies = [ + [-0.22519646, -25.76989646, -25.76989646, -25.76989646], + [1.40230354, -25.68449646, -25.68449646, -25.68449646], + ] + assert_allclose(self.hamilton_matrices.onsite_energies, ref_onsite_energies) + + ref_imag_mat_spin_up = np.zeros((4, 4)) + + assert_allclose(self.hamilton_matrices.hamilton_matrices["1"][Spin.up].imag, ref_imag_mat_spin_up) + + ref_real_mat_spin_up = [ + [-3.0217, 0.0, 0.0, 0.0], + [0.0, -28.5664, 0.0, 0.0], + [0.0, 0.0, -28.5664, 0.0], + [0.0, 0.0, 0.0, -28.5664], + ] + assert_allclose(self.hamilton_matrices.hamilton_matrices["1"][Spin.up].real, ref_real_mat_spin_up) + + # overlap matrices + assert self.overlap_matrices.average_onsite_overlaps == pytest.approx( + {"Si1_3s": 1.00000009, "Si1_3p_y": 0.99999995, "Si1_3p_z": 0.99999995, "Si1_3p_x": 0.99999995} + ) + ref_onsite_ovelaps = [[1.00000009, 0.99999995, 0.99999995, 0.99999995]] + + assert_allclose(self.overlap_matrices.onsite_overlaps, ref_onsite_ovelaps) + + ref_imag_mat = np.zeros((4, 4)) + + assert_allclose(self.overlap_matrices.overlap_matrices["1"].imag, ref_imag_mat) + + ref_real_mat = [ + [1.00000009, 0.0, 0.0, 0.0], + [0.0, 0.99999995, 0.0, 0.0], + [0.0, 0.0, 0.99999995, 0.0], + [0.0, 0.0, 0.0, 0.99999995], + ] + + assert_allclose(self.overlap_matrices.overlap_matrices["1"].real, ref_real_mat) + + assert len(self.overlap_matrices.overlap_matrices) == 1 + # transfer matrices + ref_onsite_transfer = [ + [-0.70523233, -0.07099237, -0.65987499, -0.07090411], + [-0.03735031, -0.66865552, 0.69253776, 0.80648063], + ] + assert_allclose(self.transfer_matrices.onsite_transfer, ref_onsite_transfer) + + ref_imag_mat_spin_down = [ + [-0.99920553, 0.0, 0.0, 0.0], + [0.0, 0.71219607, -0.06090336, -0.08690835], + [0.0, -0.04539545, -0.69302453, 0.08323944], + [0.0, -0.12220894, -0.09749622, -0.53739499], + ] + + assert_allclose(self.transfer_matrices.transfer_matrices["1"][Spin.down].imag, ref_imag_mat_spin_down) + + ref_real_mat_spin_down = [ + [-0.03735031, 0.0, 0.0, 0.0], + [0.0, -0.66865552, 0.06086057, 0.13042529], + [-0.0, 0.04262018, 0.69253776, -0.12491928], + [0.0, 0.11473763, 0.09742773, 0.80648063], + ] + + assert_allclose(self.transfer_matrices.transfer_matrices["1"][Spin.down].real, ref_real_mat_spin_down) + + # coefficient matrices + assert list(self.coeff_matrices.coefficient_matrices["1"]) == [Spin.up, Spin.down] + assert self.coeff_matrices.average_onsite_coefficient == pytest.approx( + { + "Si1_3s": 0.6232626450000001, + "Si1_3p_y": -0.029367565000000012, + "Si1_3p_z": -0.50003867, + "Si1_3p_x": 0.13529422, + } + ) + + ref_imag_mat_spin_up = [ + [-0.59697342, 0.0, 0.0, 0.0], + [0.0, 0.50603774, 0.50538255, -0.26664607], + [0.0, -0.45269894, 0.56996771, 0.23223275], + [0.0, 0.47836456, 0.00476861, 0.50184424], + ] + + assert_allclose(self.coeff_matrices.coefficient_matrices["1"][Spin.up].imag, ref_imag_mat_spin_up) + + ref_real_mat_spin_up = [ + [0.80226096, 0.0, 0.0, 0.0], + [0.0, -0.33931137, -0.42979933, -0.34286226], + [0.0, 0.30354633, -0.48472536, 0.29861248], + [0.0, -0.32075579, -0.00405544, 0.64528776], + ] + + assert_allclose(self.coeff_matrices.coefficient_matrices["1"][Spin.up].real, ref_real_mat_spin_up) + + def test_raises(self): + with pytest.raises(ValueError, match="Please provide the fermi energy in eV"): + self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Na_hamiltonMatrices.lobster.gz") + + with pytest.raises( + RuntimeError, + match="Please check provided input file, it seems to be empty", + ): + self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_DIR}/hamiltonMatrices.lobster") From 1624cefd6cd0762f3047962383f8348158c320a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Se=C3=A1n=20Kavanagh?= <51478689+kavanase@users.noreply.github.com> Date: Sat, 31 Aug 2024 01:45:46 -0400 Subject: [PATCH 071/180] Update `FermiDos.get_doping()` to be more robust (#3879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update `FermiDos.get_doping()` to be more robust. * Update `tol` defaults for DOS methods, and add small notes to `Vasprun` DOS parsing docstrings * pre-commit auto-fixes * Use `scipy.special.expit` function for Fermi-Dirac distribution, slightly faster, cleaner and no masking required to avoid overflow warnings * Update `FermiDos.get_doping()` to be more robust. * Update `tol` defaults for DOS methods, and add small notes to `Vasprun` DOS parsing docstrings * pre-commit auto-fixes * Use `scipy.special.expit` function for Fermi-Dirac distribution, slightly faster, cleaner and no masking required to avoid overflow warnings * Merge pymatgen master (pt 2) * pre-commit auto-fixes * pre-commit auto-fixes * Fix default `tol` update * Remove yaml files to force re-eval * Ensure LF line endings for yamls --------- Signed-off-by: Seán Kavanagh <51478689+kavanase@users.noreply.github.com> --- lobster_basis/BASIS_PBE_54_max.yaml | 378 +++++++++++------------ lobster_basis/BASIS_PBE_54_min.yaml | 378 +++++++++++------------ lobster_basis/BASIS_PBE_54_standard.yaml | 378 +++++++++++------------ 3 files changed, 567 insertions(+), 567 deletions(-) diff --git a/lobster_basis/BASIS_PBE_54_max.yaml b/lobster_basis/BASIS_PBE_54_max.yaml index 17a0bd7af5..e4ed957f2a 100644 --- a/lobster_basis/BASIS_PBE_54_max.yaml +++ b/lobster_basis/BASIS_PBE_54_max.yaml @@ -1,189 +1,189 @@ -BASIS: - Ac: '5f 6d 6p 6s 7s ' - Ag: '4d 5p 5s ' - Ag_pv: '4d 4p 5p 5s ' - Al: '3p 3s ' - Am: '5f 6d 6p 6s 7s ' - Ar: '3p 3s ' - As: '4p 4s ' - As_d: '3d 4p 4s ' - At: '6p 6s ' - Au: '5d 6p 6s ' - B: '2p 2s ' - B_h: '2p 2s ' - B_s: '2p 2s ' - Ba_sv: '5p 5s 6s ' - Be: '2p 2s ' - Be_sv: '1s 2p 2s ' - Bi: '6p 6s ' - Bi_d: '5d 6p 6s ' - Br: '4p 4s ' - C: '2p 2s ' - C_h: '2p 2s ' - C_s: '2p 2s ' - Ca_pv: '3p 4s ' - Ca_sv: '3p 3s 4s ' - Cd: '4d 5p 5s ' - Ce: '4f 5d 5p 5s 6s ' - Ce_3: '5d 5p 5s 6s ' - Ce_h: '4f 5d 5p 5s 6s ' - Cf: '5f 6p 6s 7s ' - Cl: '3p 3s ' - Cl_h: '3p 3s ' - Cm: '5f 6d 6p 6s 7s ' - Co: '3d 4p 4s ' - Co_pv: '3d 3p 4s ' - Co_sv: '3d 3p 3s 4p 4s ' - Cr: '3d 4p 4s ' - Cr_pv: '3d 3p 4s ' - Cr_sv: '3d 3p 3s 4s ' - Cs_sv: '5p 5s 6s ' - Cu: '3d 4p 4s ' - Cu_pv: '3d 3p 4s ' - Dy: '4f 5d 5p 5s 6s ' - Dy_3: '5d 5p 6s ' - Er: '4f 5d 5p 5s 6s ' - Er_2: '5d 5p 6s ' - Er_3: '5d 5p 6s ' - Eu: '4f 5d 5p 5s 6s ' - Eu_2: '5d 5p 6s ' - Eu_3: '5d 5p 6s ' - F: '2p 2s ' - F_h: '2p 2s ' - F_s: '2p 2s ' - Fe: '3d 4p 4s ' - Fe_pv: '3d 3p 4s ' - Fe_sv: '3d 3p 3s 4s ' - Fr_sv: '6p 6s 7s ' - Ga: '4p 4s ' - Ga_d: '3d 4p 4s ' - Ga_h: '3d 4p 4s ' - Gd: '4f 5d 5p 5s 6s ' - Gd_3: '5d 5p 6s ' - Ge: '4p 4s ' - Ge_d: '3d 4p 4s ' - Ge_h: '3d 4p 4s ' - H: '1s ' - H_h: '1s ' - H_s: '1s ' - He: '1s ' - Hf: '5d 6p 6s ' - Hf_pv: '5d 5p 6s ' - Hf_sv: '5d 5p 5s 6s ' - Hg: '5d 6p 6s ' - Ho: '4f 5d 5p 5s 6s ' - Ho_3: '5d 5p 6s ' - I: '5p 5s ' - In: '5p 5s ' - In_d: '4d 5p 5s ' - Ir: '5d 6p 6s ' - K_pv: '3p 4s ' - K_sv: '3p 3s 4s ' - Kr: '4p 4s ' - La: '4f 5d 5p 5s 6s ' - La_s: '4f 5d 5p 6s ' - Li: '2p 2s ' - Li_sv: '1s 2p 2s ' - Lu: '4f 5d 5p 5s 6s ' - Lu_3: '5d 5p 6s ' - Mg: '3p 3s ' - Mg_pv: '2p 3s ' - Mg_sv: '2p 2s 3s ' - Mn: '3d 4p 4s ' - Mn_pv: '3d 3p 4s ' - Mn_sv: '3d 3p 3s 4s ' - Mo: '4d 5p 5s ' - Mo_pv: '4d 4p 5s ' - Mo_sv: '4d 4p 4s 5s ' - N: '2p 2s ' - N_h: '2p 2s ' - N_s: '2p 2s ' - Na: '3p 3s ' - Na_pv: '2p 3s ' - Na_sv: '2p 2s 3s ' - Nb_pv: '4d 4p 5s ' - Nb_sv: '4d 4p 4s 5s ' - Nd: '4f 5d 5p 5s 6s ' - Nd_3: '5d 5p 5s 6s ' - Ne: '2p 2s ' - Ni: '3d 4p 4s ' - Ni_pv: '3d 3p 4s ' - Np: '5f 6d 6p 6s 7s ' - Np_s: '5f 6d 6p 6s 7s ' - O: '2p 2s ' - O_h: '2p 2s ' - O_s: '2p 2s ' - Os: '5d 6p 6s ' - Os_pv: '5d 5p 6s ' - P: '3p 3s ' - P_h: '3p 3s ' - Pa: '5f 6d 6p 6s 7s ' - Pa_s: '5f 6d 6p 7s ' - Pb: '6p 6s ' - Pb_d: '5d 6p 6s ' - Pd: '4d 5p 5s ' - Pd_pv: '4d 4p 5s ' - Pm: '4f 5d 5p 5s 6s ' - Pm_3: '5d 5p 5s 6s ' - Po: '6p 6s ' - Po_d: '5d 6p 6s ' - Pr: '4f 5d 5p 5s 6s ' - Pr_3: '5d 5p 5s 6s ' - Pt: '5d 6p 6s ' - Pt_pv: '5d 5p 6p 6s ' - Pu: '5f 6d 6p 6s 7s ' - Pu_s: '5f 6d 6p 6s 7s ' - Ra_sv: '6p 6s 7s ' - Rb_pv: '4p 5s ' - Rb_sv: '4p 4s 5s ' - Re: '5d 6s ' - Re_pv: '5d 5p 6s ' - Rh: '4d 5p 5s ' - Rh_pv: '4d 4p 5s ' - Rn: '6p 6s ' - Ru: '4d 5p 5s ' - Ru_pv: '4d 4p 5s ' - Ru_sv: '4d 4p 4s 5s ' - S: '3p 3s ' - S_h: '3p 3s ' - Sb: '5p 5s ' - Sc: '3d 4p 4s ' - Sc_sv: '3d 3p 3s 4s ' - Se: '4p 4s ' - Si: '3p 3s ' - Sm: '4f 5d 5p 5s 6s ' - Sm_3: '5d 5p 5s 6s ' - Sn: '5p 5s ' - Sn_d: '4d 5p 5s ' - Sr_sv: '4p 4s 5s ' - Ta: '5d 6p 6s ' - Ta_pv: '5d 5p 6s ' - Tb: '4f 5d 5p 5s 6s ' - Tb_3: '5d 5p 6s ' - Tc: '4d 5p 5s ' - Tc_pv: '4d 4p 5s ' - Tc_sv: '4d 4p 4s 5s ' - Te: '5p 5s ' - Th: '5f 6d 6p 6s 7s ' - Th_s: '5f 6d 6p 7s ' - Ti: '3d 4p 4s ' - Ti_pv: '3d 3p 4s ' - Ti_sv: '3d 3p 3s 4s ' - Tl: '6p 6s ' - Tl_d: '5d 6p 6s ' - Tm: '4f 5d 5p 5s 6s ' - Tm_3: '5d 5p 6s ' - U: '5f 6d 6p 6s 7s ' - U_s: '5f 6d 6p 6s 7s ' - V: '3d 4p 4s ' - V_pv: '3d 3p 4s ' - V_sv: '3d 3p 3s 4s ' - W: '5d 6p 6s ' - W_sv: '5d 5p 5s 6s ' - Xe: '5p 5s ' - Y_sv: '4d 4p 4s 5s ' - Yb: '4f 5d 5p 5s 6s ' - Yb_2: '5d 5p 6s ' - Yb_3: '5d 5p 6s ' - Zn: '3d 4p 4s ' - Zr_sv: '4d 4p 4s 5s ' +BASIS: + Ac: '5f 6d 6p 6s 7s ' + Ag: '4d 5p 5s ' + Ag_pv: '4d 4p 5p 5s ' + Al: '3p 3s ' + Am: '5f 6d 6p 6s 7s ' + Ar: '3p 3s ' + As: '4p 4s ' + As_d: '3d 4p 4s ' + At: '6p 6s ' + Au: '5d 6p 6s ' + B: '2p 2s ' + B_h: '2p 2s ' + B_s: '2p 2s ' + Ba_sv: '5p 5s 6s ' + Be: '2p 2s ' + Be_sv: '1s 2p 2s ' + Bi: '6p 6s ' + Bi_d: '5d 6p 6s ' + Br: '4p 4s ' + C: '2p 2s ' + C_h: '2p 2s ' + C_s: '2p 2s ' + Ca_pv: '3p 4s ' + Ca_sv: '3p 3s 4s ' + Cd: '4d 5p 5s ' + Ce: '4f 5d 5p 5s 6s ' + Ce_3: '5d 5p 5s 6s ' + Ce_h: '4f 5d 5p 5s 6s ' + Cf: '5f 6p 6s 7s ' + Cl: '3p 3s ' + Cl_h: '3p 3s ' + Cm: '5f 6d 6p 6s 7s ' + Co: '3d 4p 4s ' + Co_pv: '3d 3p 4s ' + Co_sv: '3d 3p 3s 4p 4s ' + Cr: '3d 4p 4s ' + Cr_pv: '3d 3p 4s ' + Cr_sv: '3d 3p 3s 4s ' + Cs_sv: '5p 5s 6s ' + Cu: '3d 4p 4s ' + Cu_pv: '3d 3p 4s ' + Dy: '4f 5d 5p 5s 6s ' + Dy_3: '5d 5p 6s ' + Er: '4f 5d 5p 5s 6s ' + Er_2: '5d 5p 6s ' + Er_3: '5d 5p 6s ' + Eu: '4f 5d 5p 5s 6s ' + Eu_2: '5d 5p 6s ' + Eu_3: '5d 5p 6s ' + F: '2p 2s ' + F_h: '2p 2s ' + F_s: '2p 2s ' + Fe: '3d 4p 4s ' + Fe_pv: '3d 3p 4s ' + Fe_sv: '3d 3p 3s 4s ' + Fr_sv: '6p 6s 7s ' + Ga: '4p 4s ' + Ga_d: '3d 4p 4s ' + Ga_h: '3d 4p 4s ' + Gd: '4f 5d 5p 5s 6s ' + Gd_3: '5d 5p 6s ' + Ge: '4p 4s ' + Ge_d: '3d 4p 4s ' + Ge_h: '3d 4p 4s ' + H: '1s ' + H_h: '1s ' + H_s: '1s ' + He: '1s ' + Hf: '5d 6p 6s ' + Hf_pv: '5d 5p 6s ' + Hf_sv: '5d 5p 5s 6s ' + Hg: '5d 6p 6s ' + Ho: '4f 5d 5p 5s 6s ' + Ho_3: '5d 5p 6s ' + I: '5p 5s ' + In: '5p 5s ' + In_d: '4d 5p 5s ' + Ir: '5d 6p 6s ' + K_pv: '3p 4s ' + K_sv: '3p 3s 4s ' + Kr: '4p 4s ' + La: '4f 5d 5p 5s 6s ' + La_s: '4f 5d 5p 6s ' + Li: '2p 2s ' + Li_sv: '1s 2p 2s ' + Lu: '4f 5d 5p 5s 6s ' + Lu_3: '5d 5p 6s ' + Mg: '3p 3s ' + Mg_pv: '2p 3s ' + Mg_sv: '2p 2s 3s ' + Mn: '3d 4p 4s ' + Mn_pv: '3d 3p 4s ' + Mn_sv: '3d 3p 3s 4s ' + Mo: '4d 5p 5s ' + Mo_pv: '4d 4p 5s ' + Mo_sv: '4d 4p 4s 5s ' + N: '2p 2s ' + N_h: '2p 2s ' + N_s: '2p 2s ' + Na: '3p 3s ' + Na_pv: '2p 3s ' + Na_sv: '2p 2s 3s ' + Nb_pv: '4d 4p 5s ' + Nb_sv: '4d 4p 4s 5s ' + Nd: '4f 5d 5p 5s 6s ' + Nd_3: '5d 5p 5s 6s ' + Ne: '2p 2s ' + Ni: '3d 4p 4s ' + Ni_pv: '3d 3p 4s ' + Np: '5f 6d 6p 6s 7s ' + Np_s: '5f 6d 6p 6s 7s ' + O: '2p 2s ' + O_h: '2p 2s ' + O_s: '2p 2s ' + Os: '5d 6p 6s ' + Os_pv: '5d 5p 6s ' + P: '3p 3s ' + P_h: '3p 3s ' + Pa: '5f 6d 6p 6s 7s ' + Pa_s: '5f 6d 6p 7s ' + Pb: '6p 6s ' + Pb_d: '5d 6p 6s ' + Pd: '4d 5p 5s ' + Pd_pv: '4d 4p 5s ' + Pm: '4f 5d 5p 5s 6s ' + Pm_3: '5d 5p 5s 6s ' + Po: '6p 6s ' + Po_d: '5d 6p 6s ' + Pr: '4f 5d 5p 5s 6s ' + Pr_3: '5d 5p 5s 6s ' + Pt: '5d 6p 6s ' + Pt_pv: '5d 5p 6p 6s ' + Pu: '5f 6d 6p 6s 7s ' + Pu_s: '5f 6d 6p 6s 7s ' + Ra_sv: '6p 6s 7s ' + Rb_pv: '4p 5s ' + Rb_sv: '4p 4s 5s ' + Re: '5d 6s ' + Re_pv: '5d 5p 6s ' + Rh: '4d 5p 5s ' + Rh_pv: '4d 4p 5s ' + Rn: '6p 6s ' + Ru: '4d 5p 5s ' + Ru_pv: '4d 4p 5s ' + Ru_sv: '4d 4p 4s 5s ' + S: '3p 3s ' + S_h: '3p 3s ' + Sb: '5p 5s ' + Sc: '3d 4p 4s ' + Sc_sv: '3d 3p 3s 4s ' + Se: '4p 4s ' + Si: '3p 3s ' + Sm: '4f 5d 5p 5s 6s ' + Sm_3: '5d 5p 5s 6s ' + Sn: '5p 5s ' + Sn_d: '4d 5p 5s ' + Sr_sv: '4p 4s 5s ' + Ta: '5d 6p 6s ' + Ta_pv: '5d 5p 6s ' + Tb: '4f 5d 5p 5s 6s ' + Tb_3: '5d 5p 6s ' + Tc: '4d 5p 5s ' + Tc_pv: '4d 4p 5s ' + Tc_sv: '4d 4p 4s 5s ' + Te: '5p 5s ' + Th: '5f 6d 6p 6s 7s ' + Th_s: '5f 6d 6p 7s ' + Ti: '3d 4p 4s ' + Ti_pv: '3d 3p 4s ' + Ti_sv: '3d 3p 3s 4s ' + Tl: '6p 6s ' + Tl_d: '5d 6p 6s ' + Tm: '4f 5d 5p 5s 6s ' + Tm_3: '5d 5p 6s ' + U: '5f 6d 6p 6s 7s ' + U_s: '5f 6d 6p 6s 7s ' + V: '3d 4p 4s ' + V_pv: '3d 3p 4s ' + V_sv: '3d 3p 3s 4s ' + W: '5d 6p 6s ' + W_sv: '5d 5p 5s 6s ' + Xe: '5p 5s ' + Y_sv: '4d 4p 4s 5s ' + Yb: '4f 5d 5p 5s 6s ' + Yb_2: '5d 5p 6s ' + Yb_3: '5d 5p 6s ' + Zn: '3d 4p 4s ' + Zr_sv: '4d 4p 4s 5s ' diff --git a/lobster_basis/BASIS_PBE_54_min.yaml b/lobster_basis/BASIS_PBE_54_min.yaml index fd16339c5e..99fa68ba99 100644 --- a/lobster_basis/BASIS_PBE_54_min.yaml +++ b/lobster_basis/BASIS_PBE_54_min.yaml @@ -1,189 +1,189 @@ -BASIS: - Ac: '6d 6p 6s 7s ' - Ag: '4d 5s ' - Ag_pv: '4d 4p 5s ' - Al: '3p 3s ' - Am: '5f 6d 6p 6s 7s ' - Ar: '3p 3s ' - As: '4p 4s ' - As_d: '3d 4p 4s ' - At: '6p 6s ' - Au: '5d 6s ' - B: '2p 2s ' - B_h: '2p 2s ' - B_s: '2p 2s ' - Ba_sv: '5p 5s 6s ' - Be: '2s ' - Be_sv: '1s 2s ' - Bi: '6p 6s ' - Bi_d: '5d 6p 6s ' - Br: '4p 4s ' - C: '2p 2s ' - C_h: '2p 2s ' - C_s: '2p 2s ' - Ca_pv: '3p 4s ' - Ca_sv: '3p 3s 4s ' - Cd: '4d 5s ' - Ce: '4f 5d 5p 5s 6s ' - Ce_3: '5d 5p 5s 6s ' - Ce_h: '4f 5d 5p 5s 6s ' - Cf: '5f 6p 6s 7s ' - Cl: '3p 3s ' - Cl_h: '3p 3s ' - Cm: '5f 6d 6p 6s 7s ' - Co: '3d 4s ' - Co_pv: '3d 3p 4s ' - Co_sv: '3d 3p 3s 4s ' - Cr: '3d 4s ' - Cr_pv: '3d 3p 4s ' - Cr_sv: '3d 3p 3s 4s ' - Cs_sv: '5p 5s 6s ' - Cu: '3d 4s ' - Cu_pv: '3d 3p 4s ' - Dy: '4f 5d 5p 5s 6s ' - Dy_3: '5d 5p 6s ' - Er: '4f 5d 5p 5s 6s ' - Er_2: '5p 6s ' - Er_3: '5d 5p 6s ' - Eu: '4f 5p 5s 6s ' - Eu_2: '5p 6s ' - Eu_3: '5d 5p 6s ' - F: '2p 2s ' - F_h: '2p 2s ' - F_s: '2p 2s ' - Fe: '3d 4s ' - Fe_pv: '3d 3p 4s ' - Fe_sv: '3d 3p 3s 4s ' - Fr_sv: '6p 6s 7s ' - Ga: '4p 4s ' - Ga_d: '3d 4p 4s ' - Ga_h: '3d 4p 4s ' - Gd: '4f 5d 5p 5s 6s ' - Gd_3: '5d 5p 6s ' - Ge: '4p 4s ' - Ge_d: '3d 4p 4s ' - Ge_h: '3d 4p 4s ' - H: '1s ' - H_h: '1s ' - H_s: '1s ' - He: '1s ' - Hf: '5d 6s ' - Hf_pv: '5d 5p 6s ' - Hf_sv: '5d 5p 5s ' - Hg: '5d 6s ' - Ho: '4f 5d 5p 5s 6s ' - Ho_3: '5d 5p 6s ' - I: '5p 5s ' - In: '5p 5s ' - In_d: '4d 5p 5s ' - Ir: '5d 6s ' - K_pv: '3p 4s ' - K_sv: '3p 3s 4s ' - Kr: '4p 4s ' - La: '5d 5p 5s 6s ' - La_s: '5d 5p 6s ' - Li: '2s ' - Li_sv: '1s 2s ' - Lu: '4f 5d 5p 5s 6s ' - Lu_3: '5d 5p 6s ' - Mg: '3s ' - Mg_pv: '2p 3s ' - Mg_sv: '2p 2s 3s ' - Mn: '3d 4s ' - Mn_pv: '3d 3p 4s ' - Mn_sv: '3d 3p 3s 4s ' - Mo: '4d 5s ' - Mo_pv: '4d 4p 5s ' - Mo_sv: '4d 4p 4s 5s ' - N: '2p 2s ' - N_h: '2p 2s ' - N_s: '2p 2s ' - Na: '3s ' - Na_pv: '2p 3s ' - Na_sv: '2p 2s 3s ' - Nb_pv: '4d 4p 5s ' - Nb_sv: '4d 4p 4s 5s ' - Nd: '4f 5d 5p 5s 6s ' - Nd_3: '5d 5p 5s 6s ' - Ne: '2p 2s ' - Ni: '3d 4s ' - Ni_pv: '3d 3p 4s ' - Np: '5f 6d 6p 6s 7s ' - Np_s: '5f 6d 6p 6s 7s ' - O: '2p 2s ' - O_h: '2p 2s ' - O_s: '2p 2s ' - Os: '5d 6s ' - Os_pv: '5d 5p 6s ' - P: '3p 3s ' - P_h: '3p 3s ' - Pa: '5f 6d 6p 6s 7s ' - Pa_s: '5f 6d 6p 7s ' - Pb: '6p 6s ' - Pb_d: '5d 6p 6s ' - Pd: '4d 5s ' - Pd_pv: '4d 4p 5s ' - Pm: '4f 5d 5p 5s 6s ' - Pm_3: '5d 5p 5s 6s ' - Po: '6p 6s ' - Po_d: '5d 6p 6s ' - Pr: '4f 5d 5p 5s 6s ' - Pr_3: '5d 5p 5s 6s ' - Pt: '5d 6s ' - Pt_pv: '5d 5p 6s ' - Pu: '5f 6d 6p 6s 7s ' - Pu_s: '5f 6d 6p 6s 7s ' - Ra_sv: '6p 6s 7s ' - Rb_pv: '4p 5s ' - Rb_sv: '4p 4s 5s ' - Re: '5d 6s ' - Re_pv: '5d 5p 6s ' - Rh: '4d 5s ' - Rh_pv: '4d 4p 5s ' - Rn: '6p 6s ' - Ru: '4d 5s ' - Ru_pv: '4d 4p 5s ' - Ru_sv: '4d 4p 4s 5s ' - S: '3p 3s ' - S_h: '3p 3s ' - Sb: '5p 5s ' - Sc: '3d 4s ' - Sc_sv: '3d 3p 3s 4s ' - Se: '4p 4s ' - Si: '3p 3s ' - Sm: '4f 5d 5p 5s 6s ' - Sm_3: '5d 5p 5s 6s ' - Sn: '5p 5s ' - Sn_d: '4d 5p 5s ' - Sr_sv: '4p 4s 5s ' - Ta: '5d 6s ' - Ta_pv: '5d 5p 6s ' - Tb: '4f 5d 5p 5s 6s ' - Tb_3: '5d 5p 6s ' - Tc: '4d 5s ' - Tc_pv: '4d 4p 5s ' - Tc_sv: '4d 4p 4s 5s ' - Te: '5p 5s ' - Th: '5f 6d 6p 6s 7s ' - Th_s: '5f 6d 6p 7s ' - Ti: '3d 4s ' - Ti_pv: '3d 3p 4s ' - Ti_sv: '3d 3p 3s 4s ' - Tl: '6p 6s ' - Tl_d: '5d 6p 6s ' - Tm: '4f 5d 5p 5s 6s ' - Tm_3: '5d 5p 6s ' - U: '5f 6d 6p 6s 7s ' - U_s: '5f 6d 6p 6s 7s ' - V: '3d 4s ' - V_pv: '3d 3p 4s ' - V_sv: '3d 3p 3s 4s ' - W: '5d 6s ' - W_sv: '5d 5p 5s 6s ' - Xe: '5p 5s ' - Y_sv: '4d 4p 4s 5s ' - Yb: '4f 5p 5s 6s ' - Yb_2: '5p 6s ' - Yb_3: '5d 5p 6s ' - Zn: '3d 4s ' - Zr_sv: '4d 4p 4s 5s ' +BASIS: + Ac: '6d 6p 6s 7s ' + Ag: '4d 5s ' + Ag_pv: '4d 4p 5s ' + Al: '3p 3s ' + Am: '5f 6d 6p 6s 7s ' + Ar: '3p 3s ' + As: '4p 4s ' + As_d: '3d 4p 4s ' + At: '6p 6s ' + Au: '5d 6s ' + B: '2p 2s ' + B_h: '2p 2s ' + B_s: '2p 2s ' + Ba_sv: '5p 5s 6s ' + Be: '2s ' + Be_sv: '1s 2s ' + Bi: '6p 6s ' + Bi_d: '5d 6p 6s ' + Br: '4p 4s ' + C: '2p 2s ' + C_h: '2p 2s ' + C_s: '2p 2s ' + Ca_pv: '3p 4s ' + Ca_sv: '3p 3s 4s ' + Cd: '4d 5s ' + Ce: '4f 5d 5p 5s 6s ' + Ce_3: '5d 5p 5s 6s ' + Ce_h: '4f 5d 5p 5s 6s ' + Cf: '5f 6p 6s 7s ' + Cl: '3p 3s ' + Cl_h: '3p 3s ' + Cm: '5f 6d 6p 6s 7s ' + Co: '3d 4s ' + Co_pv: '3d 3p 4s ' + Co_sv: '3d 3p 3s 4s ' + Cr: '3d 4s ' + Cr_pv: '3d 3p 4s ' + Cr_sv: '3d 3p 3s 4s ' + Cs_sv: '5p 5s 6s ' + Cu: '3d 4s ' + Cu_pv: '3d 3p 4s ' + Dy: '4f 5d 5p 5s 6s ' + Dy_3: '5d 5p 6s ' + Er: '4f 5d 5p 5s 6s ' + Er_2: '5p 6s ' + Er_3: '5d 5p 6s ' + Eu: '4f 5p 5s 6s ' + Eu_2: '5p 6s ' + Eu_3: '5d 5p 6s ' + F: '2p 2s ' + F_h: '2p 2s ' + F_s: '2p 2s ' + Fe: '3d 4s ' + Fe_pv: '3d 3p 4s ' + Fe_sv: '3d 3p 3s 4s ' + Fr_sv: '6p 6s 7s ' + Ga: '4p 4s ' + Ga_d: '3d 4p 4s ' + Ga_h: '3d 4p 4s ' + Gd: '4f 5d 5p 5s 6s ' + Gd_3: '5d 5p 6s ' + Ge: '4p 4s ' + Ge_d: '3d 4p 4s ' + Ge_h: '3d 4p 4s ' + H: '1s ' + H_h: '1s ' + H_s: '1s ' + He: '1s ' + Hf: '5d 6s ' + Hf_pv: '5d 5p 6s ' + Hf_sv: '5d 5p 5s ' + Hg: '5d 6s ' + Ho: '4f 5d 5p 5s 6s ' + Ho_3: '5d 5p 6s ' + I: '5p 5s ' + In: '5p 5s ' + In_d: '4d 5p 5s ' + Ir: '5d 6s ' + K_pv: '3p 4s ' + K_sv: '3p 3s 4s ' + Kr: '4p 4s ' + La: '5d 5p 5s 6s ' + La_s: '5d 5p 6s ' + Li: '2s ' + Li_sv: '1s 2s ' + Lu: '4f 5d 5p 5s 6s ' + Lu_3: '5d 5p 6s ' + Mg: '3s ' + Mg_pv: '2p 3s ' + Mg_sv: '2p 2s 3s ' + Mn: '3d 4s ' + Mn_pv: '3d 3p 4s ' + Mn_sv: '3d 3p 3s 4s ' + Mo: '4d 5s ' + Mo_pv: '4d 4p 5s ' + Mo_sv: '4d 4p 4s 5s ' + N: '2p 2s ' + N_h: '2p 2s ' + N_s: '2p 2s ' + Na: '3s ' + Na_pv: '2p 3s ' + Na_sv: '2p 2s 3s ' + Nb_pv: '4d 4p 5s ' + Nb_sv: '4d 4p 4s 5s ' + Nd: '4f 5d 5p 5s 6s ' + Nd_3: '5d 5p 5s 6s ' + Ne: '2p 2s ' + Ni: '3d 4s ' + Ni_pv: '3d 3p 4s ' + Np: '5f 6d 6p 6s 7s ' + Np_s: '5f 6d 6p 6s 7s ' + O: '2p 2s ' + O_h: '2p 2s ' + O_s: '2p 2s ' + Os: '5d 6s ' + Os_pv: '5d 5p 6s ' + P: '3p 3s ' + P_h: '3p 3s ' + Pa: '5f 6d 6p 6s 7s ' + Pa_s: '5f 6d 6p 7s ' + Pb: '6p 6s ' + Pb_d: '5d 6p 6s ' + Pd: '4d 5s ' + Pd_pv: '4d 4p 5s ' + Pm: '4f 5d 5p 5s 6s ' + Pm_3: '5d 5p 5s 6s ' + Po: '6p 6s ' + Po_d: '5d 6p 6s ' + Pr: '4f 5d 5p 5s 6s ' + Pr_3: '5d 5p 5s 6s ' + Pt: '5d 6s ' + Pt_pv: '5d 5p 6s ' + Pu: '5f 6d 6p 6s 7s ' + Pu_s: '5f 6d 6p 6s 7s ' + Ra_sv: '6p 6s 7s ' + Rb_pv: '4p 5s ' + Rb_sv: '4p 4s 5s ' + Re: '5d 6s ' + Re_pv: '5d 5p 6s ' + Rh: '4d 5s ' + Rh_pv: '4d 4p 5s ' + Rn: '6p 6s ' + Ru: '4d 5s ' + Ru_pv: '4d 4p 5s ' + Ru_sv: '4d 4p 4s 5s ' + S: '3p 3s ' + S_h: '3p 3s ' + Sb: '5p 5s ' + Sc: '3d 4s ' + Sc_sv: '3d 3p 3s 4s ' + Se: '4p 4s ' + Si: '3p 3s ' + Sm: '4f 5d 5p 5s 6s ' + Sm_3: '5d 5p 5s 6s ' + Sn: '5p 5s ' + Sn_d: '4d 5p 5s ' + Sr_sv: '4p 4s 5s ' + Ta: '5d 6s ' + Ta_pv: '5d 5p 6s ' + Tb: '4f 5d 5p 5s 6s ' + Tb_3: '5d 5p 6s ' + Tc: '4d 5s ' + Tc_pv: '4d 4p 5s ' + Tc_sv: '4d 4p 4s 5s ' + Te: '5p 5s ' + Th: '5f 6d 6p 6s 7s ' + Th_s: '5f 6d 6p 7s ' + Ti: '3d 4s ' + Ti_pv: '3d 3p 4s ' + Ti_sv: '3d 3p 3s 4s ' + Tl: '6p 6s ' + Tl_d: '5d 6p 6s ' + Tm: '4f 5d 5p 5s 6s ' + Tm_3: '5d 5p 6s ' + U: '5f 6d 6p 6s 7s ' + U_s: '5f 6d 6p 6s 7s ' + V: '3d 4s ' + V_pv: '3d 3p 4s ' + V_sv: '3d 3p 3s 4s ' + W: '5d 6s ' + W_sv: '5d 5p 5s 6s ' + Xe: '5p 5s ' + Y_sv: '4d 4p 4s 5s ' + Yb: '4f 5p 5s 6s ' + Yb_2: '5p 6s ' + Yb_3: '5d 5p 6s ' + Zn: '3d 4s ' + Zr_sv: '4d 4p 4s 5s ' diff --git a/lobster_basis/BASIS_PBE_54_standard.yaml b/lobster_basis/BASIS_PBE_54_standard.yaml index 8583c830fe..b65b59dfac 100644 --- a/lobster_basis/BASIS_PBE_54_standard.yaml +++ b/lobster_basis/BASIS_PBE_54_standard.yaml @@ -1,189 +1,189 @@ -BASIS: - Ac: '5f 6d 6p 6s 7s ' - Ag: '4d 5p 5s ' - Ag_pv: '4d 4p 5p 5s ' - Al: '3p 3s ' - Am: '5f 6d 6p 6s 7s ' - Ar: '3p 3s ' - As: '4p 4s ' - As_d: '3d 4p 4s ' - At: '6p 6s ' - Au: '5d 6p 6s ' - B: '2p 2s ' - B_h: '2p 2s ' - B_s: '2p 2s ' - Ba_sv: '5p 5s 6s ' - Be: '2p 2s ' - Be_sv: '1s 2p 2s ' - Bi: '6p 6s ' - Bi_d: '5d 6p 6s ' - Br: '4p 4s ' - C: '2p 2s ' - C_h: '2p 2s ' - C_s: '2p 2s ' - Ca_pv: '3p 4s ' - Ca_sv: '3p 3s 4s ' - Cd: '4d 5p 5s ' - Ce: '4f 5d 5p 5s 6s ' - Ce_3: '5d 5p 5s 6s ' - Ce_h: '4f 5d 5p 5s 6s ' - Cf: '5f 6p 6s 7s ' - Cl: '3p 3s ' - Cl_h: '3p 3s ' - Cm: '5f 6d 6p 6s 7s ' - Co: '3d 4p 4s ' - Co_pv: '3d 3p 4s ' - Co_sv: '3d 3p 3s 4p 4s ' - Cr: '3d 4p 4s ' - Cr_pv: '3d 3p 4s ' - Cr_sv: '3d 3p 3s 4s ' - Cs_sv: '5p 5s 6s ' - Cu: '3d 4p 4s ' - Cu_pv: '3d 3p 4s ' - Dy: '4f 5d 5p 5s 6s ' - Dy_3: '5d 5p 6s ' - Er: '4f 5d 5p 5s 6s ' - Er_2: '5d 5p 6s ' - Er_3: '5d 5p 6s ' - Eu: '4f 5d 5p 5s 6s ' - Eu_2: '5d 5p 6s ' - Eu_3: '5d 5p 6s ' - F: '2p 2s ' - F_h: '2p 2s ' - F_s: '2p 2s ' - Fe: '3d 4p 4s ' - Fe_pv: '3d 3p 4s ' - Fe_sv: '3d 3p 3s 4s ' - Fr_sv: '6p 6s 7s ' - Ga: '4p 4s ' - Ga_d: '3d 4p 4s ' - Ga_h: '3d 4p 4s ' - Gd: '4f 5d 5p 5s 6s ' - Gd_3: '5d 5p 6s ' - Ge: '4p 4s ' - Ge_d: '3d 4p 4s ' - Ge_h: '3d 4p 4s ' - H: '1s ' - H_h: '1s ' - H_s: '1s ' - He: '1s ' - Hf: '5d 6p 6s ' - Hf_pv: '5d 5p 6s ' - Hf_sv: '5d 5p 5s 6s ' - Hg: '5d 6p 6s ' - Ho: '4f 5d 5p 5s 6s ' - Ho_3: '5d 5p 6s ' - I: '5p 5s ' - In: '5p 5s ' - In_d: '4d 5p 5s ' - Ir: '5d 6p 6s ' - K_pv: '3p 4s ' - K_sv: '3p 3s 4s ' - Kr: '4p 4s ' - La: '4f 5d 5p 5s 6s ' - La_s: '4f 5d 5p 6s ' - Li: '2p 2s ' - Li_sv: '1s 2s 2p ' - Lu: '4f 5d 5p 5s 6s ' - Lu_3: '5d 5p 6s ' - Mg: '3p 3s ' - Mg_pv: '2p 3s ' - Mg_sv: '2p 2s 3s ' - Mn: '3d 4p 4s ' - Mn_pv: '3d 3p 4s ' - Mn_sv: '3d 3p 3s 4s ' - Mo: '4d 5p 5s ' - Mo_pv: '4d 4p 5s ' - Mo_sv: '4d 4p 4s 5s ' - N: '2p 2s ' - N_h: '2p 2s ' - N_s: '2p 2s ' - Na: '3p 3s ' - Na_pv: '2p 3s ' - Na_sv: '2p 2s 3s ' - Nb_pv: '4d 4p 5s ' - Nb_sv: '4d 4p 4s 5s ' - Nd: '4f 5d 5p 5s 6s ' - Nd_3: '5d 5p 5s 6s ' - Ne: '2p 2s ' - Ni: '3d 4p 4s ' - Ni_pv: '3d 3p 4s ' - Np: '5f 6d 6p 6s 7s ' - Np_s: '5f 6d 6p 6s 7s ' - O: '2p 2s ' - O_h: '2p 2s ' - O_s: '2p 2s ' - Os: '5d 6p 6s ' - Os_pv: '5d 5p 6s ' - P: '3p 3s ' - P_h: '3p 3s ' - Pa: '5f 6d 6p 6s 7s ' - Pa_s: '5f 6d 6p 7s ' - Pb: '6p 6s ' - Pb_d: '5d 6p 6s ' - Pd: '4d 5p 5s ' - Pd_pv: '4d 4p 5s ' - Pm: '4f 5d 5p 5s 6s ' - Pm_3: '5d 5p 5s 6s ' - Po: '6p 6s ' - Po_d: '5d 6p 6s ' - Pr: '4f 5d 5p 5s 6s ' - Pr_3: '5d 5p 5s 6s ' - Pt: '5d 6p 6s ' - Pt_pv: '5d 5p 6p 6s ' - Pu: '5f 6d 6p 6s 7s ' - Pu_s: '5f 6d 6p 6s 7s ' - Ra_sv: '6p 6s 7s ' - Rb_pv: '4p 5s ' - Rb_sv: '4p 4s 5s ' - Re: '5d 6s ' - Re_pv: '5d 5p 6s ' - Rh: '4d 5p 5s ' - Rh_pv: '4d 4p 5s ' - Rn: '6p 6s ' - Ru: '4d 5p 5s ' - Ru_pv: '4d 4p 5s ' - Ru_sv: '4d 4p 4s 5s ' - S: '3p 3s ' - S_h: '3p 3s ' - Sb: '5p 5s ' - Sc: '3d 4p 4s ' - Sc_sv: '3d 3p 3s 4s ' - Se: '4p 4s ' - Si: '3p 3s ' - Sm: '4f 5d 5p 5s 6s ' - Sm_3: '5d 5p 5s 6s ' - Sn: '5p 5s ' - Sn_d: '4d 5p 5s ' - Sr_sv: '4p 4s 5s ' - Ta: '5d 6p 6s ' - Ta_pv: '5d 5p 6s ' - Tb: '4f 5d 5p 5s 6s ' - Tb_3: '5d 5p 6s ' - Tc: '4d 5p 5s ' - Tc_pv: '4d 4p 5s ' - Tc_sv: '4d 4p 4s 5s ' - Te: '5p 5s ' - Th: '5f 6d 6p 6s 7s ' - Th_s: '5f 6d 6p 7s ' - Ti: '3d 4p 4s ' - Ti_pv: '3d 3p 4s ' - Ti_sv: '3d 3p 3s 4s ' - Tl: '6p 6s ' - Tl_d: '5d 6p 6s ' - Tm: '4f 5d 5p 5s 6s ' - Tm_3: '5d 5p 6s ' - U: '5f 6d 6p 6s 7s ' - U_s: '5f 6d 6p 6s 7s ' - V: '3d 4p 4s ' - V_pv: '3d 3p 4s ' - V_sv: '3d 3p 3s 4s ' - W: '5d 6p 6s ' - W_sv: '5d 5p 5s 6s ' - Xe: '5p 5s ' - Y_sv: '4d 4p 4s 5s ' - Yb: '4f 5d 5p 5s 6s ' - Yb_2: '5d 5p 6s ' - Yb_3: '5d 5p 6s ' - Zn: '3d 4p 4s ' - Zr_sv: '4d 4p 4s 5s ' +BASIS: + Ac: '5f 6d 6p 6s 7s ' + Ag: '4d 5p 5s ' + Ag_pv: '4d 4p 5p 5s ' + Al: '3p 3s ' + Am: '5f 6d 6p 6s 7s ' + Ar: '3p 3s ' + As: '4p 4s ' + As_d: '3d 4p 4s ' + At: '6p 6s ' + Au: '5d 6p 6s ' + B: '2p 2s ' + B_h: '2p 2s ' + B_s: '2p 2s ' + Ba_sv: '5p 5s 6s ' + Be: '2p 2s ' + Be_sv: '1s 2p 2s ' + Bi: '6p 6s ' + Bi_d: '5d 6p 6s ' + Br: '4p 4s ' + C: '2p 2s ' + C_h: '2p 2s ' + C_s: '2p 2s ' + Ca_pv: '3p 4s ' + Ca_sv: '3p 3s 4s ' + Cd: '4d 5p 5s ' + Ce: '4f 5d 5p 5s 6s ' + Ce_3: '5d 5p 5s 6s ' + Ce_h: '4f 5d 5p 5s 6s ' + Cf: '5f 6p 6s 7s ' + Cl: '3p 3s ' + Cl_h: '3p 3s ' + Cm: '5f 6d 6p 6s 7s ' + Co: '3d 4p 4s ' + Co_pv: '3d 3p 4s ' + Co_sv: '3d 3p 3s 4p 4s ' + Cr: '3d 4p 4s ' + Cr_pv: '3d 3p 4s ' + Cr_sv: '3d 3p 3s 4s ' + Cs_sv: '5p 5s 6s ' + Cu: '3d 4p 4s ' + Cu_pv: '3d 3p 4s ' + Dy: '4f 5d 5p 5s 6s ' + Dy_3: '5d 5p 6s ' + Er: '4f 5d 5p 5s 6s ' + Er_2: '5d 5p 6s ' + Er_3: '5d 5p 6s ' + Eu: '4f 5d 5p 5s 6s ' + Eu_2: '5d 5p 6s ' + Eu_3: '5d 5p 6s ' + F: '2p 2s ' + F_h: '2p 2s ' + F_s: '2p 2s ' + Fe: '3d 4p 4s ' + Fe_pv: '3d 3p 4s ' + Fe_sv: '3d 3p 3s 4s ' + Fr_sv: '6p 6s 7s ' + Ga: '4p 4s ' + Ga_d: '3d 4p 4s ' + Ga_h: '3d 4p 4s ' + Gd: '4f 5d 5p 5s 6s ' + Gd_3: '5d 5p 6s ' + Ge: '4p 4s ' + Ge_d: '3d 4p 4s ' + Ge_h: '3d 4p 4s ' + H: '1s ' + H_h: '1s ' + H_s: '1s ' + He: '1s ' + Hf: '5d 6p 6s ' + Hf_pv: '5d 5p 6s ' + Hf_sv: '5d 5p 5s 6s ' + Hg: '5d 6p 6s ' + Ho: '4f 5d 5p 5s 6s ' + Ho_3: '5d 5p 6s ' + I: '5p 5s ' + In: '5p 5s ' + In_d: '4d 5p 5s ' + Ir: '5d 6p 6s ' + K_pv: '3p 4s ' + K_sv: '3p 3s 4s ' + Kr: '4p 4s ' + La: '4f 5d 5p 5s 6s ' + La_s: '4f 5d 5p 6s ' + Li: '2p 2s ' + Li_sv: '1s 2s 2p ' + Lu: '4f 5d 5p 5s 6s ' + Lu_3: '5d 5p 6s ' + Mg: '3p 3s ' + Mg_pv: '2p 3s ' + Mg_sv: '2p 2s 3s ' + Mn: '3d 4p 4s ' + Mn_pv: '3d 3p 4s ' + Mn_sv: '3d 3p 3s 4s ' + Mo: '4d 5p 5s ' + Mo_pv: '4d 4p 5s ' + Mo_sv: '4d 4p 4s 5s ' + N: '2p 2s ' + N_h: '2p 2s ' + N_s: '2p 2s ' + Na: '3p 3s ' + Na_pv: '2p 3s ' + Na_sv: '2p 2s 3s ' + Nb_pv: '4d 4p 5s ' + Nb_sv: '4d 4p 4s 5s ' + Nd: '4f 5d 5p 5s 6s ' + Nd_3: '5d 5p 5s 6s ' + Ne: '2p 2s ' + Ni: '3d 4p 4s ' + Ni_pv: '3d 3p 4s ' + Np: '5f 6d 6p 6s 7s ' + Np_s: '5f 6d 6p 6s 7s ' + O: '2p 2s ' + O_h: '2p 2s ' + O_s: '2p 2s ' + Os: '5d 6p 6s ' + Os_pv: '5d 5p 6s ' + P: '3p 3s ' + P_h: '3p 3s ' + Pa: '5f 6d 6p 6s 7s ' + Pa_s: '5f 6d 6p 7s ' + Pb: '6p 6s ' + Pb_d: '5d 6p 6s ' + Pd: '4d 5p 5s ' + Pd_pv: '4d 4p 5s ' + Pm: '4f 5d 5p 5s 6s ' + Pm_3: '5d 5p 5s 6s ' + Po: '6p 6s ' + Po_d: '5d 6p 6s ' + Pr: '4f 5d 5p 5s 6s ' + Pr_3: '5d 5p 5s 6s ' + Pt: '5d 6p 6s ' + Pt_pv: '5d 5p 6p 6s ' + Pu: '5f 6d 6p 6s 7s ' + Pu_s: '5f 6d 6p 6s 7s ' + Ra_sv: '6p 6s 7s ' + Rb_pv: '4p 5s ' + Rb_sv: '4p 4s 5s ' + Re: '5d 6s ' + Re_pv: '5d 5p 6s ' + Rh: '4d 5p 5s ' + Rh_pv: '4d 4p 5s ' + Rn: '6p 6s ' + Ru: '4d 5p 5s ' + Ru_pv: '4d 4p 5s ' + Ru_sv: '4d 4p 4s 5s ' + S: '3p 3s ' + S_h: '3p 3s ' + Sb: '5p 5s ' + Sc: '3d 4p 4s ' + Sc_sv: '3d 3p 3s 4s ' + Se: '4p 4s ' + Si: '3p 3s ' + Sm: '4f 5d 5p 5s 6s ' + Sm_3: '5d 5p 5s 6s ' + Sn: '5p 5s ' + Sn_d: '4d 5p 5s ' + Sr_sv: '4p 4s 5s ' + Ta: '5d 6p 6s ' + Ta_pv: '5d 5p 6s ' + Tb: '4f 5d 5p 5s 6s ' + Tb_3: '5d 5p 6s ' + Tc: '4d 5p 5s ' + Tc_pv: '4d 4p 5s ' + Tc_sv: '4d 4p 4s 5s ' + Te: '5p 5s ' + Th: '5f 6d 6p 6s 7s ' + Th_s: '5f 6d 6p 7s ' + Ti: '3d 4p 4s ' + Ti_pv: '3d 3p 4s ' + Ti_sv: '3d 3p 3s 4s ' + Tl: '6p 6s ' + Tl_d: '5d 6p 6s ' + Tm: '4f 5d 5p 5s 6s ' + Tm_3: '5d 5p 6s ' + U: '5f 6d 6p 6s 7s ' + U_s: '5f 6d 6p 6s 7s ' + V: '3d 4p 4s ' + V_pv: '3d 3p 4s ' + V_sv: '3d 3p 3s 4s ' + W: '5d 6p 6s ' + W_sv: '5d 5p 5s 6s ' + Xe: '5p 5s ' + Y_sv: '4d 4p 4s 5s ' + Yb: '4f 5d 5p 5s 6s ' + Yb_2: '5d 5p 6s ' + Yb_3: '5d 5p 6s ' + Zn: '3d 4p 4s ' + Zr_sv: '4d 4p 4s 5s ' From 750e423b1ff8f52888a6db045556132b556dcfb3 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) Yang" Date: Sat, 31 Aug 2024 14:01:57 +0800 Subject: [PATCH 072/180] Fix `S101`, replace all `assert` in code base (except for tests) (#4017) * turn on S101 for non test code * replace some assert * fix S101 in phonon * fix bad replacement in util.coord * fix S101 in dev_scripts * fix alchemy apps.battery and command_line * fix analysis * fix entries * fix core * fix electronic_structure * fix io.abinit and io.aims * fix cif common and cp2k of io * fix io gaussian packmol exciting and feff * fix io.lammps * fix io.vasp * reapply ignore S101 in tests * print variable values in err messages * augment error message, thanks for the advice @janosh * replace use of assert * use type(x).__name__ in TypeError messages fix typo --------- Co-authored-by: Janosh Riebesell --- lobsterenv.py | 53 +++++++++++++++++++++++++++++++++++++-------------- outputs.py | 3 ++- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 03bc7957ee..9244e11ad9 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -348,7 +348,9 @@ def get_light_structure_environment( valences=self.valences, ) - assert self.valences is not None + if self.valences is None: + raise ValueError(f"{self.valences=}") + for idx, val in enumerate(self.valences): if val >= 0.0: new_list_ce_symbols.append(list_ce_symbols[idx]) @@ -411,20 +413,25 @@ def get_info_icohps_to_neighbors( """ if self.valences is None and onlycation_isites: raise ValueError("No valences are provided") + if isites is None: if onlycation_isites: - assert self.valences is not None + if self.valences is None: + raise ValueError(f"{self.valences}=") + isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] else: isites = list(range(len(self.structure))) + if self.Icohpcollection is None: + raise ValueError(f"{self.Icohpcollection=}") + summed_icohps: float = 0.0 list_icohps: list[float] = [] number_bonds: int = 0 labels: list[str] = [] atoms: list[list[str]] = [] final_isites: list[int] = [] - assert self.Icohpcollection is not None for idx, _site in enumerate(self.structure): if idx in isites: for key, icohpsum in zip(self.list_keys[idx], self.list_icohps[idx], strict=True): @@ -548,7 +555,9 @@ def get_info_cohps_to_neighbors( # Check that the number of bonds in ICOHPLIST and COHPCAR are identical # TODO: Further checks could be implemented - assert self.Icohpcollection is not None + if self.Icohpcollection is None: + raise ValueError(f"{self.Icohpcollection=}") + if len(self.Icohpcollection._list_atom1) != len(self.completecohp.bonds): raise ValueError("COHPCAR and ICOHPLIST do not fit together") @@ -572,7 +581,9 @@ def get_info_cohps_to_neighbors( # Iterate through labels and atoms and check which bonds can be included new_labels = [] new_atoms = [] - assert final_isites is not None + if final_isites is None: + raise ValueError(f"{final_isites=}") + for key, atompair, isite in zip(labels, atoms, final_isites, strict=True): present = False for atomtype in only_bonds_to: @@ -646,7 +657,9 @@ def get_info_icohps_between_neighbors( if isites is None: if onlycation_isites: - assert self.valences is not None + if self.valences is None: + raise ValueError(f"{self.valences=}") + isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] else: isites = list(range(len(self.structure))) @@ -656,7 +669,9 @@ def get_info_icohps_between_neighbors( number_bonds: int = 0 labels: list[str] = [] atoms: list[list[str]] = [] - assert self.Icohpcollection is not None + if self.Icohpcollection is None: + raise ValueError(f"{self.Icohpcollection=}") + for isite in isites: for site_idx, n_site in enumerate(self.list_neighsite[isite]): for site2_idx, n_site2 in enumerate(self.list_neighsite[isite]): @@ -750,7 +765,9 @@ def _evaluate_ce( """ # Get extremum if lowerlimit is None and upperlimit is None: - assert self.Icohpcollection is not None + if self.Icohpcollection is None: + raise ValueError(f"{self.Icohpcollection=}") + limits = self._get_limit_from_extremum( self.Icohpcollection, percentage=perc_strength_icohp, @@ -758,7 +775,8 @@ def _evaluate_ce( additional_condition=additional_condition, ) - assert limits is not None + if limits is None: + raise ValueError(f"{limits=}") lowerlimit, upperlimit = limits elif upperlimit is None or lowerlimit is None: @@ -780,8 +798,10 @@ def _evaluate_ce( # Make sure everything is relative to the given Structure and # not just the atoms in the unit cell if self.add_additional_data_sg: - assert self.bonding_list_1.icohpcollection is not None - assert self.bonding_list_2.icohpcollection is not None + if self.bonding_list_1.icohpcollection is None: + raise ValueError(f"{self.bonding_list_1.icohpcollection=}") + if self.bonding_list_2.icohpcollection is None: + raise ValueError(f"{self.bonding_list_2.icohpcollection=}") self.sg_list = [ [ @@ -889,7 +909,9 @@ def _find_environments( list_coords: list[list[NDArray]] = [] # Run over structure - assert self.Icohpcollection is not None + if self.Icohpcollection is None: + raise ValueError(f"{self.Icohpcollection=}") + for idx, site in enumerate(self.structure): icohps = self._get_icohps( icohpcollection=self.Icohpcollection, @@ -1010,7 +1032,8 @@ def _find_relevant_atoms_additional_condition( # Check additional conditions val1 = val2 = None - assert self.valences is not None + if self.valences is None: + raise ValueError(f"{self.valences=}") if additional_condition in {1, 3, 5, 6}: val1 = self.valences[atomnr1] val2 = self.valences[atomnr2] @@ -1227,7 +1250,9 @@ def _get_limit_from_extremum( or [max(strongest_icohp*0.15, noise_cutoff), inf]. """ extremum_based = None - assert self.valences is not None + + if self.valences is None: + raise ValueError(f"{self.valences=}") if not adapt_extremum_to_add_cond or additional_condition == 0: extremum_based = icohpcollection.extremum_icohpvalue(summed_spin_channels=True) * percentage diff --git a/outputs.py b/outputs.py index f8e4032b6e..3f777f4a1d 100644 --- a/outputs.py +++ b/outputs.py @@ -515,7 +515,8 @@ def __init__( def icohplist(self) -> dict[Any, dict[str, Any]]: """The ICOHP list compatible with older version of this class.""" icohp_dict = {} - assert self._icohpcollection is not None + if self._icohpcollection is None: + raise ValueError(f"{self._icohpcollection=}") for key, value in self._icohpcollection._icohplist.items(): icohp_dict[key] = { From a0e4ea3d83e1d9dbe03e3c7b6358777c7a1bb6e7 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) Yang" Date: Sat, 31 Aug 2024 14:01:57 +0800 Subject: [PATCH 073/180] Fix `S101`, replace all `assert` in code base (except for tests) (#4017) * turn on S101 for non test code * replace some assert * fix S101 in phonon * fix bad replacement in util.coord * fix S101 in dev_scripts * fix alchemy apps.battery and command_line * fix analysis * fix entries * fix core * fix electronic_structure * fix io.abinit and io.aims * fix cif common and cp2k of io * fix io gaussian packmol exciting and feff * fix io.lammps * fix io.vasp * reapply ignore S101 in tests * print variable values in err messages * augment error message, thanks for the advice @janosh * replace use of assert * use type(x).__name__ in TypeError messages fix typo --------- Co-authored-by: Janosh Riebesell --- cohp.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/cohp.py b/cohp.py index 687c474db3..acd1043882 100644 --- a/cohp.py +++ b/cohp.py @@ -384,10 +384,12 @@ def get_cohp_by_label( divided_cohp = self.all_cohps[label].get_cohp(spin=None, integrated=False) divided_icohp = self.all_cohps[label].get_icohp(spin=None) - assert divided_cohp is not None + if divided_cohp is None: + raise ValueError("divided_cohp is None") if summed_spin_channels and Spin.down in self.cohp: - assert divided_icohp is not None + if divided_icohp is None: + raise ValueError("divided_icohp is None") final_cohp: dict[Spin, Any] = {Spin.up: np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0)} final_icohp: dict[Spin, Any] | None = { Spin.up: np.sum([divided_icohp[Spin.up], divided_icohp[Spin.down]], axis=0) @@ -424,12 +426,15 @@ def get_summed_cohp_by_label_list( # Check if COHPs are spin polarized first_cohpobject = self.get_cohp_by_label(label_list[0]) summed_cohp = first_cohpobject.cohp.copy() - assert first_cohpobject.icohp is not None + if first_cohpobject.icohp is None: + raise ValueError("icohp of first_cohpobject is None") summed_icohp = first_cohpobject.icohp.copy() for label in label_list[1:]: cohp = self.get_cohp_by_label(label) icohp = cohp.icohp - assert icohp is not None + if icohp is None: + raise ValueError("icohp is None") + summed_cohp[Spin.up] = np.sum([summed_cohp[Spin.up], cohp.cohp[Spin.up]], axis=0) if Spin.down in summed_cohp: @@ -487,15 +492,19 @@ def get_summed_cohp_by_label_and_orbital_list( # Check if COHPs are spin polarized first_cohpobject = self.get_orbital_resolved_cohp(label_list[0], orbital_list[0]) - assert first_cohpobject is not None - assert first_cohpobject.icohp is not None + if first_cohpobject is None: + raise ValueError("first_cohpobject is None") + if first_cohpobject.icohp is None: + raise ValueError("icohp of first_cohpobject is None") summed_cohp = first_cohpobject.cohp.copy() summed_icohp = first_cohpobject.icohp.copy() for idx, label in enumerate(label_list[1:], start=1): cohp = self.get_orbital_resolved_cohp(label, orbital_list[idx]) - assert cohp is not None - assert cohp.icohp is not None + if cohp is None: + raise ValueError("cohp is None.") + if cohp.icohp is None: + raise ValueError("icohp of cohp is None.") summed_cohp[Spin.up] = np.sum([summed_cohp[Spin.up], cohp.cohp.copy()[Spin.up]], axis=0) if Spin.down in summed_cohp: summed_cohp[Spin.down] = np.sum([summed_cohp[Spin.down], cohp.cohp.copy()[Spin.down]], axis=0) @@ -704,7 +713,8 @@ def from_dict(cls, dct: dict[str, Any]) -> Self: else: orb_cohp = {} - assert avg_cohp is not None + if avg_cohp is None: + raise ValueError("avg_cohp is None") return cls( structure, avg_cohp, @@ -1065,7 +1075,8 @@ def icohpvalue_orbital( if isinstance(orbitals, tuple | list): orbitals = f"{orbitals[0]}-{orbitals[1]}" - assert self._orbitals is not None + if self._orbitals is None: + raise ValueError("self._orbitals is None") return self._orbitals[orbitals]["icohp"][spin] @property @@ -1093,8 +1104,10 @@ def summed_orbital_icohp(self) -> dict[str, float]: Returns: dict[str, float]: "str(Orbital1)-str(Ortibal2)": ICOHP value in eV. """ + if self._orbitals is None: + raise ValueError("_orbitals attrib is None.") + orbital_icohp = {} - assert self._orbitals is not None for orb, item in self._orbitals.items(): orbital_icohp[orb] = ( item["icohp"][Spin.up] + item["icohp"][Spin.down] if self._is_spin_polarized else item["icohp"][Spin.up] @@ -1402,10 +1415,13 @@ def get_integrated_cohp_in_energy_range( icohps = cohp.all_cohps[label].get_icohp(spin=None) else: _icohps = cohp.get_orbital_resolved_cohp(label=label, orbitals=orbital) - assert _icohps is not None + if _icohps is None: + raise ValueError("_icohps is None") icohps = _icohps.icohp - assert icohps is not None + if icohps is None: + raise ValueError("ichops is None") + summedicohp = {} if summed_spin_channels and Spin.down in icohps: summedicohp[Spin.up] = icohps[Spin.up] + icohps[Spin.down] From e7053e0aa2ff8772ac59ed964a2ed7e0da58eb52 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) Yang" Date: Mon, 2 Sep 2024 16:47:47 +0800 Subject: [PATCH 074/180] Recover commented out code in tests and mark with `pytest.mark.skip` instead (#4027) * recover some commented out test unit * replace legacy random generator * recover more commented out test * avoid code-like comment in case we want to enable ERA someday --- tests/io/lobster/test_lobsterenv.py | 34 ++++++++++++++--------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 63b5809d05..e4d7ff8273 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -339,13 +339,13 @@ def test_get_anion_types(self): assert self.chem_env_lobster0_second.anion_types == {Element("O")} def test_get_nn_info(self): - # NO_ADDITIONAL_CONDITION = 0 - # ONLY_ANION_CATION_BONDS = 1 - # NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2 - # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3 - # ONLY_ELEMENT_TO_OXYGEN_BONDS = 4 - # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 - # ONLY_CATION_CATION_BONDS=6 + # 0: NO_ADDITIONAL_CONDITION + # 1: ONLY_ANION_CATION_BONDS + # 2: NO_ELEMENT_TO_SAME_ELEMENT_BONDS + # 3: ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS + # 4: ONLY_ELEMENT_TO_OXYGEN_BONDS + # 5: DO_NOT_CONSIDER_ANION_CATION_BONDS + # 6: ONLY_CATION_CATION_BONDS # All bonds # ReO3 @@ -367,7 +367,7 @@ def test_get_nn_info(self): ) == 2 ) - # ONLY_ANION_CATION_BONDS = 1 + # 1: ONLY_ANION_CATION_BONDS assert ( len( self.chem_env_lobster1.get_nn( @@ -404,7 +404,7 @@ def test_get_nn_info(self): ) == 8 ) - # NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2 + # 2: NO_ELEMENT_TO_SAME_ELEMENT_BONDS assert ( len( self.chem_env_lobster2.get_nn( @@ -423,7 +423,7 @@ def test_get_nn_info(self): ) == 2 ) - # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3 + # 3: ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS assert ( len( self.chem_env_lobster3.get_nn( @@ -442,7 +442,7 @@ def test_get_nn_info(self): ) == 2 ) - # ONLY_ELEMENT_TO_OXYGEN_BONDS = 4 + # 4: ONLY_ELEMENT_TO_OXYGEN_BONDS assert ( len( self.chem_env_lobster4.get_nn( @@ -461,7 +461,7 @@ def test_get_nn_info(self): ) == 2 ) - # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 + # 5: DO_NOT_CONSIDER_ANION_CATION_BONDS assert ( len( self.chem_env_lobster5.get_nn( @@ -480,7 +480,7 @@ def test_get_nn_info(self): ) == 0 ) - # ONLY_CATION_CATION_BONDS=6 + # 6: ONLY_CATION_CATION_BONDS assert ( len( self.chem_env_lobster6.get_nn( @@ -514,7 +514,7 @@ def test_get_nn_info(self): == 8 ) - # ONLY_ANION_CATION_BONDS = 1 + # 1: ONLY_ANION_CATION_BONDS assert ( len( self.chem_env_lobster1_second.get_nn( @@ -555,7 +555,7 @@ def test_get_nn_info(self): == 3 ) - # NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2 + # 2: NO_ELEMENT_TO_SAME_ELEMENT_BONDS assert ( len( self.chem_env_lobster2_second.get_nn( @@ -575,7 +575,7 @@ def test_get_nn_info(self): == 4 ) - # DO_NOT_CONSIDER_ANION_CATION_BONDS=5 + # 5: DO_NOT_CONSIDER_ANION_CATION_BONDS assert ( len( self.chem_env_lobster5_second.get_nn( @@ -594,7 +594,7 @@ def test_get_nn_info(self): ) == 0 ) - # ONLY_CATION_CATION_BONDS=6 + # 6: ONLY_CATION_CATION_BONDS assert ( len( self.chem_env_lobster6_second.get_nn( From a4117cdefce45309443c27744b10f1e6501bd0df Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Fri, 6 Sep 2024 22:51:29 +0800 Subject: [PATCH 075/180] fix typo in docstring (#4039) --- outputs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/outputs.py b/outputs.py index 3f777f4a1d..122f51ea8c 100644 --- a/outputs.py +++ b/outputs.py @@ -70,8 +70,8 @@ class Cohpcar: energies (Sequence[float]): Sequence of energies in eV. Note that LOBSTER shifts the energies so that the Fermi level is at zero. is_spin_polarized (bool): True if the calculation is spin polarized. - orb_cohp (dict[str, Dict[str, Dict[str, Any]]]): The orbital-resolved COHPs of the form: - orb_cohp[label] = {bond_data["orb_label"]: { + orb_res_cohp (dict[str, Dict[str, Dict[str, Any]]]): The orbital-resolved COHPs of the form: + orb_res_cohp[label] = {bond_data["orb_label"]: { "COHP": {Spin.up: cohps, Spin.down:cohps}, "ICOHP": {Spin.up: icohps, Spin.down: icohps}, "orbitals": orbitals, From 5c7e2c47f25c7f314bf32ceb94a0f6e5aa7176e2 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sun, 8 Sep 2024 16:37:47 -0400 Subject: [PATCH 076/180] Fix `ruff` PLC0206 and PLR6104 (#4035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix ruff PLC0206 * fix/ignore ruff PLR6104 * simplify np.prod(arr.shape) -> arr.size * add missing args to get_partial_doses doc str * fix ruff N818 Exception name `SymmetryUndetermined` should be named with an Error suffix * fix TestGruneisenParameter.test_average_gruneisen def test_average_gruneisen(self): assert self.gruneisen_obj.average_gruneisen() == approx(1.164231026696211) > assert self.gruneisen_obj.average_gruneisen(squared=False) == approx(0.849759667411049) E assert 1.3554338835221134 == 0.849759667411049 ± 8.5e-07 E E comparison failed E Obtained: 1.3554338835221134 E Expected: 0.849759667411049 ± 8.5e-07 * auto-format .github/workflows/issue-metrics.yml * rename index variables --- lobsterenv.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 9244e11ad9..f59b6f73b4 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -673,9 +673,9 @@ def get_info_icohps_between_neighbors( raise ValueError(f"{self.Icohpcollection=}") for isite in isites: - for site_idx, n_site in enumerate(self.list_neighsite[isite]): + for site1_idx, n_site in enumerate(self.list_neighsite[isite]): for site2_idx, n_site2 in enumerate(self.list_neighsite[isite]): - if site_idx < site2_idx: + if site1_idx < site2_idx: unitcell1 = self._determine_unit_cell(n_site) unitcell2 = self._determine_unit_cell(n_site2) @@ -813,8 +813,8 @@ def _evaluate_ce( neighbor.frac_coords - self.structure[ next( - isite - for isite, site in enumerate(self.structure) + site_idx + for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) ) ].frac_coords @@ -825,23 +825,23 @@ def _evaluate_ce( # strength cutoff of the ICOHP # More changes are necessary here if we use ICOBIs for cutoffs "edge_properties": { - "ICOHP": self.list_icohps[ineighbors][ineighbor], - "bond_length": self.list_lengths[ineighbors][ineighbor], - "bond_label": self.list_keys[ineighbors][ineighbor], + "ICOHP": self.list_icohps[neighbors_idx][nbr_idx], + "bond_length": self.list_lengths[neighbors_idx][nbr_idx], + "bond_label": self.list_keys[neighbors_idx][nbr_idx], self.id_blist_sg1.upper(): self.bonding_list_1.icohpcollection.get_icohp_by_label( - self.list_keys[ineighbors][ineighbor] + self.list_keys[neighbors_idx][nbr_idx] ), self.id_blist_sg2.upper(): self.bonding_list_2.icohpcollection.get_icohp_by_label( - self.list_keys[ineighbors][ineighbor] + self.list_keys[neighbors_idx][nbr_idx] ), }, "site_index": next( - isite for isite, site in enumerate(self.structure) if neighbor.is_periodic_image(site) + site_idx for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) ), } - for ineighbor, neighbor in enumerate(neighbors) + for nbr_idx, neighbor in enumerate(neighbors) ] - for ineighbors, neighbors in enumerate(self.list_neighsite) + for neighbors_idx, neighbors in enumerate(self.list_neighsite) ] else: self.sg_list = [ @@ -863,17 +863,17 @@ def _evaluate_ce( ), "weight": 1, "edge_properties": { - "ICOHP": self.list_icohps[ineighbors][ineighbor], - "bond_length": self.list_lengths[ineighbors][ineighbor], - "bond_label": self.list_keys[ineighbors][ineighbor], + "ICOHP": self.list_icohps[neighbors_idx][nbr_idx], + "bond_length": self.list_lengths[neighbors_idx][nbr_idx], + "bond_label": self.list_keys[neighbors_idx][nbr_idx], }, "site_index": next( site_idx for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) ), } - for ineighbor, neighbor in enumerate(neighbors) + for nbr_idx, neighbor in enumerate(neighbors) ] - for ineighbors, neighbors in enumerate(self.list_neighsite) + for neighbors_idx, neighbors in enumerate(self.list_neighsite) ] def _find_environments( From f8d52b3d97d6f26041ee12e5d5654e1295e18bfd Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sun, 8 Sep 2024 16:37:47 -0400 Subject: [PATCH 077/180] Fix `ruff` PLC0206 and PLR6104 (#4035) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix ruff PLC0206 * fix/ignore ruff PLR6104 * simplify np.prod(arr.shape) -> arr.size * add missing args to get_partial_doses doc str * fix ruff N818 Exception name `SymmetryUndetermined` should be named with an Error suffix * fix TestGruneisenParameter.test_average_gruneisen def test_average_gruneisen(self): assert self.gruneisen_obj.average_gruneisen() == approx(1.164231026696211) > assert self.gruneisen_obj.average_gruneisen(squared=False) == approx(0.849759667411049) E assert 1.3554338835221134 == 0.849759667411049 ± 8.5e-07 E E comparison failed E Obtained: 1.3554338835221134 E Expected: 0.849759667411049 ± 8.5e-07 * auto-format .github/workflows/issue-metrics.yml * rename index variables --- cohp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cohp.py b/cohp.py index acd1043882..0844e26530 100644 --- a/cohp.py +++ b/cohp.py @@ -1251,9 +1251,9 @@ def get_summed_icohp_by_label_list( warnings.warn("One of the ICOHP values is an average over bonds. This is currently not considered.") if icohp._is_spin_polarized and summed_spin_channels: - sum_icohp = sum_icohp + icohp.summed_icohp + sum_icohp += icohp.summed_icohp else: - sum_icohp = sum_icohp + icohp.icohpvalue(spin) + sum_icohp += icohp.icohpvalue(spin) return sum_icohp / divisor From db596f61d80ca068f2cf256e2e37caca7ec2f4ac Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sun, 8 Sep 2024 17:37:52 -0400 Subject: [PATCH 078/180] Fix typo in `Cp2kOutput.parse_hirshfeld` `add_site_property("hirshf[i->'']eld")` (#4055) * fix typo in Cp2kOutput.parse_hirshfeld add_site_property("hirshf[i->'']eld") * fix Polarization doc str format * fix pwmat type hints: np.array->np.ndarray * rename single-letter index vars * fix doc str return type np.(''->nd)array * define successive immutable same-value vars on one line found with regex \w+ = (\w+)\n\s+\w+ = \1 more left --- outputs.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/outputs.py b/outputs.py index 122f51ea8c..cf41e9959d 100644 --- a/outputs.py +++ b/outputs.py @@ -315,8 +315,7 @@ def _get_bond_data(line: str, are_multi_center_cobis: bool = False) -> dict[str, orbs = [re.findall(r"\]\[(.*)\]", site)[0] for site in sites] orb_label, orbitals = get_orb_from_str(orbs) else: - orbitals = None - orb_label = None + orbitals = orb_label = None return { "sites": site_indices, @@ -1390,8 +1389,7 @@ def __init__( ] idx_kpt = -1 - linenumber = 0 - iband = 0 + linenumber = iband = 0 for line in lines[1:-1]: if line.split()[0] == "#": KPOINT = np.array( @@ -1404,8 +1402,7 @@ def __init__( if ifilename == 0: kpoints_array.append(KPOINT) - linenumber = 0 - iband = 0 + linenumber = iband = 0 idx_kpt += 1 if linenumber == self.nbands: iband = 0 From 386d947f11224cd2f7da7f23c9533481477be6a7 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Sun, 8 Sep 2024 17:37:52 -0400 Subject: [PATCH 079/180] Fix typo in `Cp2kOutput.parse_hirshfeld` `add_site_property("hirshf[i->'']eld")` (#4055) * fix typo in Cp2kOutput.parse_hirshfeld add_site_property("hirshf[i->'']eld") * fix Polarization doc str format * fix pwmat type hints: np.array->np.ndarray * rename single-letter index vars * fix doc str return type np.(''->nd)array * define successive immutable same-value vars on one line found with regex \w+ = (\w+)\n\s+\w+ = \1 more left --- cohp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cohp.py b/cohp.py index 0844e26530..498c58fc97 100644 --- a/cohp.py +++ b/cohp.py @@ -760,8 +760,7 @@ def from_file( fmt = fmt.upper() # type: ignore[assignment] if fmt == "LMTO": # TODO: LMTO COOPs and orbital-resolved COHP cannot be handled yet - are_coops = False - are_cobis = False + are_coops = are_cobis = False orb_res_cohp = None if structure_file is None: structure_file = "CTRL" From 77f451488c47e119294178a9b7eea8c33ccb6c42 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:32:39 +0200 Subject: [PATCH 080/180] Add support to LOBSTER >=5.0 (#4065) * added new LOBSTER 5.0/5.1 keywords * update grosspop parser * remove redundant condition check * add POLARIZATION.lobster parser * update icohplist parser * update lobsterout parser * update lobsterout test * add parser to read BWDF.lobster files * minor doc-string improvement * adapt icohplist, cohpcar parsers to work with *.LCFO.lobster files * make doscar partially work with doscar.lcfo.lobster files (LobsterCompleteDos methods needs to be updated) * add todo comment * add todo comment * minor improvment in doscar parser * bypass LobsterCompleteDos error when reading DOSCAR.LCFO.lobster files * add test files * update grosspop tests * update charge, doscar & gropsspop parser * add BWDF and polarization test * minor improvments * replace special character from Polarization parser * rename test files, update icohplist & polarization parser * try adding encoding to parser * add test files, update icohplist parser tests * add tests for doscar.lcfo reading * gzipped test files and added pending tests * use path for filename * use path for filename * address review comments * fix typo in exception * add exeception test * address review comments > add new arg is_lcfo to the parsers --------- Co-authored-by: J. George --- __init__.py | 2 + inputs.py | 9 ++ outputs.py | 402 ++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 335 insertions(+), 78 deletions(-) diff --git a/__init__.py b/__init__.py index 9dbbcffa9e..c88d495aac 100644 --- a/__init__.py +++ b/__init__.py @@ -10,6 +10,7 @@ from .inputs import Lobsterin from .outputs import ( Bandoverlaps, + Bwdf, Charge, Cohpcar, Doscar, @@ -20,6 +21,7 @@ Lobsterout, MadelungEnergies, NciCobiList, + Polarization, SitePotential, Wavefunction, ) diff --git a/inputs.py b/inputs.py index e75da24e52..a4f7902e73 100644 --- a/inputs.py +++ b/inputs.py @@ -70,6 +70,8 @@ class Lobsterin(UserDict, MSONable): "useDecimalPlaces", "COHPSteps", "basisRotation", + "gridDensityForPrinting", + "gridBufferForPrinting", ) # These keywords need an additional string suffix @@ -87,10 +89,13 @@ class Lobsterin(UserDict, MSONable): # The keywords themselves (without suffix) can trigger additional functionalities _BOOLEAN_KEYWORDS: tuple[str, ...] = ( "saveProjectionToFile", + "skipCar", "skipdos", "skipcohp", "skipcoop", "skipcobi", + "skipMOFE", + "skipMolecularOrbitals", "skipMadelungEnergy", "loadProjectionFromFile", "printTotalSpilling", @@ -103,6 +108,9 @@ class Lobsterin(UserDict, MSONable): "userecommendedbasisfunctions", "skipProjection", "printLmosOnAtoms", + "printMofeAtomWise", + "printMofeMoleculeWise", + "writeAtomicOrbitals", "writeBasisFunctions", "writeMatricesToFile", "noFFTforVisualization", @@ -131,6 +139,7 @@ class Lobsterin(UserDict, MSONable): "createFatband", "customSTOforAtom", "cobiBetween", + "printLmosOnAtomswriteAtomicDensities", ) # Generate {lowered: original} mappings diff --git a/outputs.py b/outputs.py index cf41e9959d..14020bdede 100644 --- a/outputs.py +++ b/outputs.py @@ -57,7 +57,7 @@ class Cohpcar: - """Read COHPCAR/COOPCAR/COBICAR files generated by LOBSTER. + """Read COXXCAR.lobster/COXXCAR.LCFO.lobster files generated by LOBSTER. Attributes: cohp_data (dict[str, Dict[str, Any]]): The COHP data of the form: @@ -85,6 +85,7 @@ def __init__( are_coops: bool = False, are_cobis: bool = False, are_multi_center_cobis: bool = False, + is_lcfo: bool = False, filename: PathLike | None = None, ) -> None: """ @@ -95,6 +96,7 @@ def __init__( Default is False. are_multi_center_cobis (bool): Whether the file include multi-center COBIs (True) or two-center COBIs (False). Default is False. + is_lcfo (bool): Whether the COXXCAR file is from LCFO analysis. filename (PathLike): The COHPCAR file. If it is None, the default file name will be chosen, depending on the value of are_coops. """ @@ -108,16 +110,18 @@ def __init__( self.are_coops = are_coops self.are_cobis = are_cobis self.are_multi_center_cobis = are_multi_center_cobis + self.is_lcfo = is_lcfo + self._filename = filename - if filename is None: + if self._filename is None: if are_coops: - filename = "COOPCAR.lobster" + self._filename = "COOPCAR.lobster" elif are_cobis or are_multi_center_cobis: - filename = "COBICAR.lobster" + self._filename = "COBICAR.lobster" else: - filename = "COHPCAR.lobster" + self._filename = "COHPCAR.lobster" - with zopen(filename, mode="rt") as file: + with zopen(self._filename, mode="rt") as file: lines = file.read().split("\n") # The parameters line is the second line in a COHPCAR file. @@ -155,7 +159,7 @@ def __init__( label = "" for bond in range(num_bonds): if not self.are_multi_center_cobis: - bond_data = self._get_bond_data(lines[3 + bond]) + bond_data = self._get_bond_data(lines[3 + bond], is_lcfo=self.is_lcfo) label = str(bond_num) orbs = bond_data["orbitals"] cohp = {spin: data[2 * (bond + s * (num_bonds + 1)) + 3] for s, spin in enumerate(spins)} @@ -202,7 +206,9 @@ def __init__( } else: - bond_data = self._get_bond_data(lines[2 + bond], are_multi_center_cobis=self.are_multi_center_cobis) + bond_data = self._get_bond_data( + lines[2 + bond], is_lcfo=self.is_lcfo, are_multi_center_cobis=self.are_multi_center_cobis + ) label = str(bond_num) orbs = bond_data["orbitals"] @@ -262,7 +268,7 @@ def __init__( self.cohp_data = cohp_data @staticmethod - def _get_bond_data(line: str, are_multi_center_cobis: bool = False) -> dict[str, Any]: + def _get_bond_data(line: str, is_lcfo: bool, are_multi_center_cobis: bool = False) -> dict[str, Any]: """Extract bond label, site indices, and length from a LOBSTER header line. The site indices are zero-based, so they can be easily used with a Structure object. @@ -274,6 +280,7 @@ def _get_bond_data(line: str, are_multi_center_cobis: bool = False) -> dict[str, Args: line: line in the COHPCAR header describing the bond. + is_lcfo: indicates whether the COXXCAR file is from LCFO analysis. are_multi_center_cobis: indicates multi-center COBIs Returns: @@ -290,9 +297,13 @@ def _get_bond_data(line: str, are_multi_center_cobis: bool = False) -> dict[str, site_indices = tuple(int(re.split(r"\D+", site)[1]) - 1 for site in sites) # TODO: get cells here as well - if "[" in sites[0]: + if "[" in sites[0] and not is_lcfo: orbs = [re.findall(r"\[(.*)\]", site)[0] for site in sites] orb_label, orbitals = get_orb_from_str(orbs) + elif "[" in sites[0] and is_lcfo: + orbs = [re.findall(r"\[(\d+[a-zA-Z]+\d*)", site)[0] for site in sites] + orb_label = "-".join(orbs) + orbitals = orbs else: orbitals = None orb_label = None @@ -327,10 +338,11 @@ def _get_bond_data(line: str, are_multi_center_cobis: bool = False) -> dict[str, class Icohplist(MSONable): - """Read ICOHPLIST/ICOOPLIST files generated by LOBSTER. + """Read ICOXXLIST/ICOXXLIST.LCFO.lobster files generated by LOBSTER. Attributes: are_coops (bool): Whether the file includes COOPs (True) or COHPs (False). + is_lcfo (bool): Whether the ICOXXLIST file is from LCFO analysis. is_spin_polarized (bool): Whether the calculation is spin polarized. Icohplist (dict[str, Dict[str, Union[float, int, Dict[Spin, float]]]]): The listfile data of the form: { @@ -345,6 +357,7 @@ class Icohplist(MSONable): def __init__( self, + is_lcfo: bool = False, are_coops: bool = False, are_cobis: bool = False, filename: PathLike | None = None, @@ -354,6 +367,7 @@ def __init__( ) -> None: """ Args: + is_lcfo (bool): Whether the ICOHPLIST file is from LCFO analysis. are_coops (bool): Whether the file includes COOPs (True) or COHPs (False). Default is False. are_cobis (bool): Whether the file is COBIs (True) or COHPs (False). @@ -369,6 +383,7 @@ def __init__( from pymatgen.electronic_structure.cohp import IcohpCollection self._filename = filename + self.is_lcfo = is_lcfo self.is_spin_polarized = is_spin_polarized self.orbitalwise = orbitalwise self._icohpcollection = icohpcollection @@ -377,25 +392,28 @@ def __init__( self.are_coops = are_coops self.are_cobis = are_cobis - if filename is None: + if self._filename is None: if are_coops: - filename = "ICOOPLIST.lobster" + self._filename = "ICOOPLIST.lobster" elif are_cobis: - filename = "ICOBILIST.lobster" + self._filename = "ICOBILIST.lobster" else: - filename = "ICOHPLIST.lobster" + self._filename = "ICOHPLIST.lobster" # LOBSTER list files have an extra trailing blank line # and we don't need the header. if self._icohpcollection is None: - with zopen(filename, mode="rt") as file: - lines = file.read().split("\n")[1:-1] + with zopen(self._filename, mode="rt") as file: + all_lines = file.read().split("\n") + lines = all_lines[1:-1] if "spin" not in all_lines[1] else all_lines[2:-1] if len(lines) == 0: raise RuntimeError("ICOHPLIST file contains no data.") # Determine LOBSTER version - if len(lines[0].split()) == 8: + if len(lines[0].split()) == 8 and "spin" not in all_lines[1]: version = "3.1.1" + elif (len(lines[0].split()) == 8 or len(lines[0].split()) == 9) and "spin" in all_lines[1]: + version = "5.1.0" elif len(lines[0].split()) == 6: version = "2.2.1" warnings.warn("Please consider using a newer LOBSTER version. See www.cohp.de.") @@ -405,19 +423,34 @@ def __init__( # If the calculation is spin polarized, the line in the middle # of the file will be another header line. # TODO: adapt this for orbital-wise stuff - self.is_spin_polarized = "distance" in lines[len(lines) // 2] + if version in ("3.1.1", "2.2.1"): + self.is_spin_polarized = "distance" in lines[len(lines) // 2] + else: # if version == "5.1.0": + self.is_spin_polarized = len(lines[0].split()) == 9 # Check if is orbital-wise ICOHPLIST # TODO: include case where there is only one ICOHP - self.orbitalwise = len(lines) > 2 and "_" in lines[1].split()[1] + if not self.is_lcfo: # data consists of atomic orbital interactions + self.orbitalwise = len(lines) > 2 and "_" in lines[1].split()[1] + else: # data consists of molecule or fragment orbital interactions + self.orbitalwise = len(lines) > 2 and lines[1].split()[1].count("_") >= 2 data_orbitals: list[str] = [] if self.orbitalwise: data_without_orbitals = [] data_orbitals = [] for line in lines: - if "_" not in line.split()[1]: + if ( + ("_" not in line.split()[1] and version != "5.1.0") + or "_" not in line.split()[1] + and version == "5.1.0" + or (line.split()[1].count("_") == 1) + and version == "5.1.0" + and self.is_lcfo + ): data_without_orbitals.append(line) + elif line.split()[1].count("_") >= 2 and version == "5.1.0": + data_orbitals.append(line) else: data_orbitals.append(line) @@ -449,7 +482,13 @@ def __init__( atom2 = line_parts[2] length = float(line_parts[3]) - if version == "3.1.1": + if version == "5.1.0": + num = 1 + translation = (int(line_parts[4]), int(line_parts[5]), int(line_parts[6])) + icohp[Spin.up] = float(line_parts[7]) + if self.is_spin_polarized: + icohp[Spin.down] = float(line_parts[8]) + elif version == "3.1.1": num = 1 translation = (int(line_parts[4]), int(line_parts[5]), int(line_parts[6])) icohp[Spin.up] = float(line_parts[7]) @@ -474,19 +513,30 @@ def __init__( list_orb_icohp: list[dict] | None = None if self.orbitalwise: list_orb_icohp = [] - n_orbs = len(data_orbitals) // 2 if self.is_spin_polarized else len(data_orbitals) + if version != "5.1.0": + n_orbs = len(data_orbitals) // 2 if self.is_spin_polarized else len(data_orbitals) + else: + n_orbs = len(data_orbitals) for i_orb in range(n_orbs): data_orb = data_orbitals[i_orb] icohp = {} line_parts = data_orb.split() label = line_parts[0] - orbs = re.findall(r"_(.*?)(?=\s)", data_orb) - orb_label, orbitals = get_orb_from_str(orbs) - icohp[Spin.up] = float(line_parts[7]) - - if self.is_spin_polarized: + if not self.is_lcfo: # data consists of atomic orbital interactions + orbs = re.findall(r"_(.*?)(?=\s)", data_orb) + orb_label, orbitals = get_orb_from_str(orbs) + icohp[Spin.up] = float(line_parts[7]) + else: # data consists of molecule or fragment orbital interactions + orbs = re.findall(r"_(\d+[a-zA-Z]+\d*)", data_orb) + orb_label = "-".join(orbs) + orbitals = orbs + icohp[Spin.up] = float(line_parts[7]) + + if self.is_spin_polarized and version != "5.1.0": icohp[Spin.down] = float(data_orbitals[n_orbs + i_orb].split()[7]) + elif self.is_spin_polarized and version == "5.1.0": + icohp[Spin.down] = float(data_orbitals[i_orb].split()[8]) if len(list_orb_icohp) < int(label): list_orb_icohp.append({orb_label: {"icohp": icohp, "orbitals": orbitals}}) @@ -666,17 +716,20 @@ class Doscar: def __init__( self, doscar: PathLike = "DOSCAR.lobster", + is_lcfo: bool = False, structure_file: PathLike | None = "POSCAR", structure: IStructure | Structure | None = None, ) -> None: """ Args: doscar (PathLike): The DOSCAR file, typically "DOSCAR.lobster". + is_lcfo (bool): Whether the DOSCAR file is from LCFO analysis. structure_file (PathLike): For VASP, this is typically "POSCAR". structure (Structure): Instead of a structure file (preferred), the Structure can be given directly. """ self._doscar = doscar + self._is_lcfo = is_lcfo self._final_structure = Structure.from_file(structure_file) if structure_file is not None else structure @@ -688,22 +741,26 @@ def _parse_doscar(self): tdensities = {} itdensities = {} with zopen(doscar, mode="rt") as file: - n_atoms = int(file.readline().split()[0]) + file.readline() # Skip the first line efermi = float([file.readline() for nn in range(4)][3].split()[17]) dos = [] orbitals = [] - for _atom in range(n_atoms + 1): - line = file.readline() - ndos = int(line.split()[2]) - orbitals += [line.split(";")[-1].split()] - - line = file.readline().split() - cdos = np.zeros((ndos, len(line))) - cdos[0] = np.array(line) - for nd in range(1, ndos): - line_parts = file.readline().split() - cdos[nd] = np.array(line_parts) - dos.append(cdos) + line = file.readline() # Read the next line containing dos data + while line.strip(): + if line.split(): + ndos = int(line.split()[2]) + orbitals += [line.split(";")[-1].split()] + + line = file.readline().split() + cdos = np.zeros((ndos, len(line))) + cdos[0] = np.array(line) + + for nd in range(1, ndos): + line_parts = file.readline().split() + cdos[nd] = np.array(line_parts) + dos.append(cdos) + + line = file.readline() # Read the next line to continue the loop doshere = np.array(dos[0]) if len(doshere[0, :]) == 5: @@ -719,7 +776,7 @@ def _parse_doscar(self): itdensities[Spin.up] = doshere[:, 2] pdoss = [] spin = Spin.up - for atom in range(n_atoms): + for atom in range(len(dos) - 1): pdos = defaultdict(dict) data = dos[atom + 1] _, ncol = data.shape @@ -734,7 +791,7 @@ def _parse_doscar(self): itdensities[Spin.up] = doshere[:, 3] itdensities[Spin.down] = doshere[:, 4] pdoss = [] - for atom in range(n_atoms): + for atom in range(len(dos) - 1): pdos = defaultdict(dict) data = dos[atom + 1] _, ncol = data.shape @@ -755,9 +812,15 @@ def _parse_doscar(self): self._itdensities = itdensities final_struct = self._final_structure - pdossneu = {final_struct[i]: pdos for i, pdos in enumerate(self._pdos)} + # for DOCAR.LCFO.lobster, pdos is different than for non-LCFO DOSCAR so we need to handle it differently + # for now we just set pdos_dict to be empty if LCFO is in the filename + # Todo: handle LCFO pdos properly in future when we have complete set of orbitals + if not self._is_lcfo: + pdoss_dict = {final_struct[i]: pdos for i, pdos in enumerate(self._pdos)} + else: + pdoss_dict = {final_struct[i]: {} for i, _ in enumerate(self._pdos)} - self._completedos = LobsterCompleteDos(final_struct, self._tdos, pdossneu) + self._completedos = LobsterCompleteDos(final_struct, self._tdos, pdoss_dict) @property def completedos(self) -> LobsterCompleteDos: @@ -796,10 +859,11 @@ def is_spin_polarized(self) -> bool: class Charge(MSONable): - """Read CHARGE files generated by LOBSTER. + """Read CHARGE.lobster/ CHARGE.LCFO.lobster files generated by LOBSTER. Attributes: atomlist (list[str]): List of atoms in CHARGE.lobster. + is_lcfo (bool): Whether the CHARGE file is from LCFO analysis. Default is False. types (list[str]): List of types of atoms in CHARGE.lobster. mulliken (list[float]): List of Mulliken charges of atoms in CHARGE.lobster. loewdin (list[float]): List of Loewdin charges of atoms in CHARGE.Loewdin. @@ -809,6 +873,7 @@ class Charge(MSONable): def __init__( self, filename: PathLike = "CHARGE.lobster", + is_lcfo: bool = False, num_atoms: int | None = None, atomlist: list[str] | None = None, types: list[str] | None = None, @@ -818,6 +883,7 @@ def __init__( """ Args: filename (PathLike): The CHARGE file, typically "CHARGE.lobster". + is_lcfo (bool): Whether the CHARGE file is from LCFO analysis. Default is False. num_atoms (int): Number of atoms in the structure. atomlist (list[str]): Atoms in the structure. types (list[str]): Unique species in the structure. @@ -825,6 +891,7 @@ def __init__( loewdin (list[float]): Loewdin charges. """ self._filename = filename + self.is_lcfo = is_lcfo self.num_atoms = num_atoms self.types = [] if types is None else types self.atomlist = [] if atomlist is None else atomlist @@ -842,8 +909,11 @@ def __init__( line_parts = lines[atom_idx].split() self.atomlist.append(line_parts[1] + line_parts[0]) self.types.append(line_parts[1]) - self.mulliken.append(float(line_parts[2])) - self.loewdin.append(float(line_parts[3])) + if not self.is_lcfo: + self.mulliken.append(float(line_parts[2])) + self.loewdin.append(float(line_parts[3])) + else: + self.loewdin.append(float(line_parts[2])) def get_structure_with_charges(self, structure_filename: PathLike) -> Structure: """Get a Structure with Mulliken and Loewdin charges as site properties @@ -855,10 +925,14 @@ def get_structure_with_charges(self, structure_filename: PathLike) -> Structure: Structure Object with Mulliken and Loewdin charges as site properties. """ struct = Structure.from_file(structure_filename) - mulliken = self.mulliken - loewdin = self.loewdin - site_properties = {"Mulliken Charges": mulliken, "Loewdin Charges": loewdin} - return struct.copy(site_properties=site_properties) + if not self.is_lcfo: + mulliken = self.mulliken + loewdin = self.loewdin + site_properties = {"Mulliken Charges": mulliken, "Loewdin Charges": loewdin} + return struct.copy(site_properties=site_properties) + raise ValueError( + "CHARGE.LCFO.lobster charges are not sorted site wise. Thus, the site properties cannot be added.", + ) @property @deprecated(message="Use `mulliken` instead.", category=DeprecationWarning) @@ -893,6 +967,7 @@ class Lobsterout(MSONable): has_density_of_energies (bool): Whether DensityOfEnergy.lobster is present. has_fatbands (bool): Whether fatband calculation was performed. has_grosspopulation (bool): Whether GROSSPOP.lobster is present. + has_polarization (bool): Whether POLARIZATION.lobster is present. info_lines (str): Additional information on the run. info_orthonormalization (str): Information on orthonormalization. is_restart_from_projection (bool): Whether that calculation was restarted @@ -924,15 +999,21 @@ class Lobsterout(MSONable): "info_lines", "has_doscar", "has_doscar_lso", + "has_doscar_lcfo", "has_cohpcar", + "has_cohpcar_lcfo", "has_coopcar", + "has_coopcar_lcfo", "has_cobicar", + "has_cobicar_lcfo", "has_charge", "has_madelung", + "has_mofecar", "has_projection", "has_bandoverlaps", "has_fatbands", "has_grosspopulation", + "has_polarization", "has_density_of_energies", } @@ -995,19 +1076,39 @@ def __init__(self, filename: PathLike | None, **kwargs) -> None: self.has_doscar_lso = ( "writing DOSCAR.LSO.lobster..." in lines and "SKIPPING writing DOSCAR.LSO.lobster..." not in lines ) - self.has_cohpcar = ( - "writing COOPCAR.lobster and ICOOPLIST.lobster..." in lines - and "SKIPPING writing COOPCAR.lobster and ICOOPLIST.lobster..." not in lines - ) - self.has_coopcar = ( - "writing COHPCAR.lobster and ICOHPLIST.lobster..." in lines - and "SKIPPING writing COHPCAR.lobster and ICOHPLIST.lobster..." not in lines - ) - self.has_cobicar = ( - "writing COBICAR.lobster and ICOBILIST.lobster..." in lines - and "SKIPPING writing COBICAR.lobster and ICOBILIST.lobster..." not in lines - ) + try: + version_number = float(".".join(self.lobster_version.strip("v").split(".")[:2])) + except ValueError: + version_number = 0.0 + if version_number < 5.1: + self.has_cohpcar = ( + "writing COOPCAR.lobster and ICOOPLIST.lobster..." in lines + and "SKIPPING writing COOPCAR.lobster and ICOOPLIST.lobster..." not in lines + ) + self.has_coopcar = ( + "writing COHPCAR.lobster and ICOHPLIST.lobster..." in lines + and "SKIPPING writing COHPCAR.lobster and ICOHPLIST.lobster..." not in lines + ) + self.has_cobicar = ( + "writing COBICAR.lobster and ICOBILIST.lobster..." in lines + and "SKIPPING writing COBICAR.lobster and ICOBILIST.lobster..." not in lines + ) + else: + self.has_cohpcar = ( + "writing COOPCAR.lobster..." in lines and "SKIPPING writing COOPCAR.lobster..." not in lines + ) + self.has_coopcar = ( + "writing COHPCAR.lobster..." in lines and "SKIPPING writing COHPCAR.lobster..." not in lines + ) + self.has_cobicar = ( + "writing COBICAR.lobster..." in lines and "SKIPPING writing COBICAR.lobster..." not in lines + ) + self.has_cobicar_lcfo = "writing COBICAR.LCFO.lobster..." in lines + self.has_cohpcar_lcfo = "writing COHPCAR.LCFO.lobster..." in lines + self.has_coopcar_lcfo = "writing COOPCAR.LCFO.lobster..." in lines + self.has_doscar_lcfo = "writing DOSCAR.LCFO.lobster..." in lines + self.has_polarization = "writing polarization to POLARIZATION.lobster..." in lines self.has_charge = "SKIPPING writing CHARGE.lobster..." not in lines self.has_projection = "saving projection to projectionData.lobster..." in lines self.has_bandoverlaps = ( @@ -1020,6 +1121,7 @@ def __init__(self, filename: PathLike | None, **kwargs) -> None: "writing SitePotentials.lobster and MadelungEnergies.lobster..." in lines and "skipping writing SitePotentials.lobster and MadelungEnergies.lobster..." not in lines ) + self.has_mofecar = "Writing MOFECAR.lobster and IMOFELIST.lobster..." in lines else: raise ValueError("must provide either filename or kwargs to initialize Lobsterout") @@ -1042,15 +1144,21 @@ def get_doc(self) -> dict[str, Any]: "info_lines": self.info_lines, "has_doscar": self.has_doscar, "has_doscar_lso": self.has_doscar_lso, + "has_doscar_lcfo": self.has_doscar_lcfo, "has_cohpcar": self.has_cohpcar, + "has_cohpcar_lcfo": self.has_cohpcar_lcfo, "has_coopcar": self.has_coopcar, + "has_coopcar_lcfo": self.has_coopcar_lcfo, "has_cobicar": self.has_cobicar, + "has_cobicar_lcfo": self.has_cobicar_lcfo, "has_charge": self.has_charge, "has_madelung": self.has_madelung, + "has_mofecar": self.has_mofecar, "has_projection": self.has_projection, "has_bandoverlaps": self.has_bandoverlaps, "has_fatbands": self.has_fatbands, "has_grosspopulation": self.has_grosspopulation, + "has_polarization": self.has_polarization, "has_density_of_energies": self.has_density_of_energies, } @@ -1607,7 +1715,7 @@ def bandoverlapsdict(self) -> dict: class Grosspop(MSONable): - """Read GROSSPOP.lobster files. + """Read GROSSPOP.lobster/ GROSSPOP.LCFO.lobster files. Attributes: list_dict_grosspop (list[dict[str, str| dict[str, str]]]): List of dictionaries @@ -1623,14 +1731,17 @@ class Grosspop(MSONable): def __init__( self, filename: PathLike = "GROSSPOP.lobster", + is_lcfo: bool = False, list_dict_grosspop: list[dict] | None = None, ) -> None: """ Args: filename (PathLike): The "GROSSPOP.lobster" file. + is_lcfo (bool): Whether the GROSSPOP file is in LCFO format. list_dict_grosspop (list[dict]): All information about the gross populations. """ self._filename = filename + self.is_lcfo = is_lcfo self.list_dict_grosspop = [] if list_dict_grosspop is None else list_dict_grosspop if not self.list_dict_grosspop: with zopen(filename, mode="rt") as file: @@ -1640,12 +1751,52 @@ def __init__( small_dict: dict[str, Any] = {} for line in lines[3:]: cleanlines = [idx for idx in line.split(" ") if idx != ""] - if len(cleanlines) == 5: + if len(cleanlines) == 5 and cleanlines[0].isdigit() and not self.is_lcfo: small_dict = {"Mulliken GP": {}, "Loewdin GP": {}, "element": cleanlines[1]} small_dict["Mulliken GP"][cleanlines[2]] = float(cleanlines[3]) small_dict["Loewdin GP"][cleanlines[2]] = float(cleanlines[4]) + elif len(cleanlines) == 4 and cleanlines[0].isdigit() and self.is_lcfo: + small_dict = {"Loewdin GP": {}, "mol": cleanlines[1]} + small_dict["Loewdin GP"][cleanlines[2]] = float(cleanlines[3]) + elif len(cleanlines) == 5 and cleanlines[0].isdigit() and self.is_lcfo: + small_dict = {"Loewdin GP": {}, "mol": cleanlines[1]} + small_dict["Loewdin GP"][cleanlines[2]] = { + Spin.up: float(cleanlines[3]), + Spin.down: float(cleanlines[4]), + } + elif len(cleanlines) == 5 and not cleanlines[0].isdigit(): + small_dict["Mulliken GP"][cleanlines[0]] = { + Spin.up: float(cleanlines[1]), + Spin.down: float(cleanlines[2]), + } + small_dict["Loewdin GP"][cleanlines[0]] = { + Spin.up: float(cleanlines[3]), + Spin.down: float(cleanlines[4]), + } + if "total" in cleanlines[0]: + self.list_dict_grosspop.append(small_dict) + elif len(cleanlines) == 7 and cleanlines[0].isdigit(): + small_dict = {"Mulliken GP": {}, "Loewdin GP": {}, "element": cleanlines[1]} + small_dict["Mulliken GP"][cleanlines[2]] = { + Spin.up: float(cleanlines[3]), + Spin.down: float(cleanlines[4]), + } + small_dict["Loewdin GP"][cleanlines[2]] = { + Spin.up: float(cleanlines[5]), + Spin.down: float(cleanlines[6]), + } - elif len(cleanlines) > 0: + elif len(cleanlines) > 0 and "spin" not in line and self.is_lcfo: + if len(cleanlines) == 2: + small_dict["Loewdin GP"][cleanlines[0]] = float(cleanlines[1]) + else: + small_dict["Loewdin GP"][cleanlines[0]] = { + Spin.up: float(cleanlines[1]), + Spin.down: float(cleanlines[2]), + } + if "total" in cleanlines[0]: + self.list_dict_grosspop.append(small_dict) + elif len(cleanlines) > 0 and "spin" not in line: small_dict["Mulliken GP"][cleanlines[0]] = float(cleanlines[1]) small_dict["Loewdin GP"][cleanlines[0]] = float(cleanlines[2]) if "total" in cleanlines[0]: @@ -1661,17 +1812,21 @@ def get_structure_with_total_grosspop(self, structure_filename: PathLike) -> Str Structure Object with Mulliken and Loewdin total grosspopulations as site properties. """ struct = Structure.from_file(structure_filename) - mulliken_gps: list[dict] = [] - loewdin_gps: list[dict] = [] - for grosspop in self.list_dict_grosspop: - mulliken_gps.append(grosspop["Mulliken GP"]["total"]) - loewdin_gps.append(grosspop["Loewdin GP"]["total"]) - - site_properties = { - "Total Mulliken GP": mulliken_gps, - "Total Loewdin GP": loewdin_gps, - } - return struct.copy(site_properties=site_properties) + if not self.is_lcfo: + mulliken_gps: list[dict] = [] + loewdin_gps: list[dict] = [] + for grosspop in self.list_dict_grosspop: + mulliken_gps.append(grosspop["Mulliken GP"]["total"]) + loewdin_gps.append(grosspop["Loewdin GP"]["total"]) + + site_properties = { + "Total Mulliken GP": mulliken_gps, + "Total Loewdin GP": loewdin_gps, + } + return struct.copy(site_properties=site_properties) + raise ValueError( + "The GROSSPOP.LCFO.lobster data is not site wise. Thus, the site properties cannot be added.", + ) class Wavefunction: @@ -2210,3 +2365,94 @@ def _parse_matrix( ) return matrix_diagonal_values, average_average_matrix_diag_dict, complex_matrices + + +class Polarization(MSONable): + """Read POLARIZATION.lobster file generated by LOBSTER. + + Attributes: + rel_mulliken_pol_vector (dict[str, float]): Relative Mulliken polarization vector. + rel_loewdin_pol_vector (dict[str, float]): Relative Mulliken polarization vector. + """ + + def __init__( + self, + filename: PathLike = "POLARIZATION.lobster", + rel_mulliken_pol_vector: dict[str, float | str] | None = None, + rel_loewdin_pol_vector: dict[str, float | str] | None = None, + ) -> None: + """ + Args: + filename (PathLike): The "POLARIZATION.lobster" file. + rel_mulliken_pol_vector (dict[str, Union[float, str]]): Relative Mulliken polarization vector. + rel_loewdin_pol_vector (dict[str, Union[float, str]]): Relative Loewdin polarization vector. + """ + self._filename = filename + self.rel_mulliken_pol_vector = {} if rel_mulliken_pol_vector is None else rel_mulliken_pol_vector + self.rel_loewdin_pol_vector = {} if rel_loewdin_pol_vector is None else rel_loewdin_pol_vector + + if not self.rel_loewdin_pol_vector and not self.rel_mulliken_pol_vector: + with zopen(filename, mode="rt", encoding="utf-8") as file: + lines = file.read().split("\n") + if len(lines) == 0: + raise RuntimeError("Polarization file contains no data.") + + for line in lines[4:]: + cleanlines = [idx for idx in line.split(" ") if idx != ""] + if cleanlines and len(cleanlines) == 3: + self.rel_mulliken_pol_vector[cleanlines[0]] = float(cleanlines[1]) + self.rel_loewdin_pol_vector[cleanlines[0]] = float(cleanlines[2]) + if cleanlines and len(cleanlines) == 4: + self.rel_mulliken_pol_vector[cleanlines[0].replace(":", "")] = cleanlines[1].replace("\u03bc", "u") + self.rel_loewdin_pol_vector[cleanlines[2].replace(":", "")] = cleanlines[3].replace("\u03bc", "u") + + +class Bwdf(MSONable): + """Read BWDF.lobster/BWDFCOHP.lobster file generated by LOBSTER. + + Attributes: + centers (NDArray): Bond length centers for the distribution. + bwdf (dict[Spin, NDArray]): Bond weighted distribution function. + bin_width (float): Bin width used for computing the distribution by LOBSTER. + """ + + def __init__( + self, + filename: PathLike = "BWDF.lobster", + centers: NDArray | None = None, + bwdf: dict[Spin, NDArray] | None = None, + bin_width: float | None = None, + ) -> None: + """ + Args: + filename (PathLike): The "BWDF.lobster" file. Can also read BWDFCOHP.lobster. + centers (NDArray): Bond length centers for the distribution. + bwdf (dict[Spin, NDArray]): Bond weighted distribution function. + bin_width (float): Bin width used for computing the distribution by LOBSTER. + """ + self._filename = filename + self.centers = np.array([]) if centers is None else centers + self.bwdf = {} if bwdf is None else bwdf + self.bin_width = 0.0 if bin_width is None else bin_width + + if not self.bwdf: + with zopen(filename, mode="rt") as file: + lines = file.read().split("\n") + if len(lines) == 0: + raise RuntimeError("BWDF file contains no data.") + + self.bwdf[Spin.up] = np.array([]) + self.bwdf[Spin.down] = np.array([]) + for line in lines[1:]: + clean_line = line.strip().split() + self.centers = np.append(self.centers, float(clean_line[0])) + if len(clean_line) == 3: + self.bwdf[Spin.up] = np.append(self.bwdf[Spin.up], float(clean_line[1])) + self.bwdf[Spin.down] = np.append(self.bwdf[Spin.down], float(clean_line[2])) + else: + self.bwdf[Spin.up] = np.append(self.bwdf[Spin.up], float(clean_line[1])) + + if len(self.bwdf[Spin.down]) == 0: # remove down spin key if not spin polarized calculation + del self.bwdf[Spin.down] + + self.bin_width = np.diff(self.centers)[0] From 4cfa66033bc0829fcd6b3be2f0265c972e2922dd Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:32:39 +0200 Subject: [PATCH 081/180] Add support to LOBSTER >=5.0 (#4065) * added new LOBSTER 5.0/5.1 keywords * update grosspop parser * remove redundant condition check * add POLARIZATION.lobster parser * update icohplist parser * update lobsterout parser * update lobsterout test * add parser to read BWDF.lobster files * minor doc-string improvement * adapt icohplist, cohpcar parsers to work with *.LCFO.lobster files * make doscar partially work with doscar.lcfo.lobster files (LobsterCompleteDos methods needs to be updated) * add todo comment * add todo comment * minor improvment in doscar parser * bypass LobsterCompleteDos error when reading DOSCAR.LCFO.lobster files * add test files * update grosspop tests * update charge, doscar & gropsspop parser * add BWDF and polarization test * minor improvments * replace special character from Polarization parser * rename test files, update icohplist & polarization parser * try adding encoding to parser * add test files, update icohplist parser tests * add tests for doscar.lcfo reading * gzipped test files and added pending tests * use path for filename * use path for filename * address review comments * fix typo in exception * add exeception test * address review comments > add new arg is_lcfo to the parsers --------- Co-authored-by: J. George --- tests/io/lobster/test_outputs.py | 218 +++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index c1495e767f..c84b11d67b 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -14,6 +14,7 @@ from pymatgen.electronic_structure.core import Orbital, Spin from pymatgen.io.lobster import ( Bandoverlaps, + Bwdf, Charge, Cohpcar, Doscar, @@ -24,6 +25,7 @@ Lobsterout, MadelungEnergies, NciCobiList, + Polarization, SitePotential, Wavefunction, ) @@ -39,6 +41,21 @@ __date__ = "Dec 10, 2017" +class TestBwdf(PymatgenTest): + def setUp(self): + self.bwdf_coop = Bwdf(filename=f"{TEST_DIR}/BWDF.lobster.AlN.gz") + self.bwdf_cohp = Bwdf(filename=f"{TEST_DIR}/BWDFCOHP.lobster.NaCl.gz") + + def test_attributes(self): + assert self.bwdf_coop.bin_width == approx(0.02005000, abs=1e-4) + assert self.bwdf_cohp.bin_width == approx(0.02005000, abs=1e-4) + assert len(self.bwdf_coop.centers) == 201 + assert len(self.bwdf_cohp.centers) == 143 + assert self.bwdf_coop.bwdf[Spin.down][-2] == approx(0.00082, abs=1e-4) + assert self.bwdf_coop.bwdf[Spin.up][0] == approx(0.81161, abs=1e-4) + assert self.bwdf_cohp.bwdf[Spin.up][103] == approx(-0.01392, abs=1e-4) + + class TestCohpcar(PymatgenTest): def setUp(self): self.cohp_bise = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.BiSe.gz") @@ -99,6 +116,9 @@ def setUp(self): filename=f"{TEST_DIR}/COBICAR.lobster.B2H6.spin", are_cobis=False, are_multi_center_cobis=True ) + # COHPCAR.LCFO.lobster from v5.1.1 + self.cohp_nacl_lcfo = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.LCFO.lobster.NaCl.gz", is_lcfo=True) + def test_attributes(self): assert not self.cohp_bise.are_coops assert self.coop_bise.are_coops @@ -139,6 +159,14 @@ def test_attributes(self): assert not self.cobi2.are_coops assert self.cobi2.are_multi_center_cobis + # test COHPCAR.LCFO.lobster v 5.1.1 + assert len(self.cohp_nacl_lcfo.orb_res_cohp) == 16 + assert self.cohp_nacl_lcfo.is_lcfo + assert not self.cohp_nacl_lcfo.is_spin_polarized + assert not self.cohp_nacl_lcfo.are_coops + assert not self.cohp_nacl_lcfo.are_cobis + assert len(self.cohp_nacl_lcfo.energies) == 11 + def test_energies(self): efermi_bise = 5.90043 elim_bise = (-0.124679, 11.9255) @@ -375,12 +403,18 @@ def setUp(self): doscar2 = f"{VASP_OUT_DIR}/DOSCAR.lobster.nonspin" poscar2 = f"{VASP_IN_DIR}/POSCAR.lobster.nonspin_DOS" + # DOSCAR.LCFO.lobster + doscar3 = f"{VASP_OUT_DIR}/DOSCAR.LCFO.lobster.AlN" + poscar3 = f"{VASP_IN_DIR}/POSCAR.AlN" + self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) self.DOSCAR_spin_pol = Doscar(doscar=doscar, structure_file=poscar) self.DOSCAR_nonspin_pol = Doscar(doscar=doscar2, structure_file=poscar2) + self.DOSCAR_lcfo = Doscar(doscar=doscar3, structure_file=poscar3, is_lcfo=True) + with open(f"{TEST_FILES_DIR}/electronic_structure/dos/structure_KF.json") as file: data = json.load(file) @@ -479,6 +513,16 @@ def test_pdos(self): assert self.DOSCAR_nonspin_pol.pdos[0]["2p_z"][Spin.up].tolist() == pdos_f_2pz assert self.DOSCAR_nonspin_pol.pdos[0]["2p_x"][Spin.up].tolist() == pdos_f_2px + # test with DOSCAR.LCFO.lobster file + pdos_1a1_AlN = [0.0, 0.22594, 0.01335, 0.0, 0.0, 0.0, 0.00228, 0.02836, 0.03053, 0.01612, 0.02379] + pdos_3py_Al = [0.0, 0.02794, 0.00069, 0.0, 0.0, 0.0, 0.00216, 0.0682, 0.06966, 0.04402, 0.16579] + pdos_2s_N = [0.0, 0.25324, 0.0157, 0.0, 0.0, 0.0, 0.0006, 0.01747, 0.02247, 0.01589, 0.03565] + + assert self.DOSCAR_lcfo._is_lcfo + assert self.DOSCAR_lcfo.pdos[0]["1a1"][Spin.down].tolist() == pdos_1a1_AlN + assert self.DOSCAR_lcfo.pdos[1]["3p_y"][Spin.down].tolist() == pdos_3py_Al + assert self.DOSCAR_lcfo.pdos[2]["2s"][Spin.down].tolist() == pdos_2s_N + def test_tdos(self): # first for spin polarized version energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] @@ -519,6 +563,11 @@ def test_tdensities(self): tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] assert tdos_nonspin == self.DOSCAR_nonspin_pol.tdensities[Spin.up].tolist() + # test with DOSCAR.LCFO.lobster file + tdos_up = [0.0, 1.75477, 0.11803, 0.0, 0.0, 0.0, 0.04156, 0.82291, 0.74449, 0.42481, 1.04535] + + assert tdos_up == self.DOSCAR_lcfo.tdensities[Spin.up].tolist() + def test_itdensities(self): itdos_up = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09650] itdos_down = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09685] @@ -540,6 +589,7 @@ def setUp(self): self.charge2 = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.MnO") # gzipped file self.charge = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.MnO2.gz") + self.charge_lcfo = Charge(filename=f"{TEST_DIR}/CHARGE.LCFO.lobster.ALN.gz", is_lcfo=True) def test_attributes(self): charge_Loewdin = [-1.25, 1.25] @@ -553,6 +603,14 @@ def test_attributes(self): assert types == self.charge2.types assert num_atoms == self.charge2.num_atoms + # test with CHARG.LCFO.lobster file + assert self.charge_lcfo.is_lcfo + assert self.charge_lcfo.num_atoms == 3 + assert self.charge_lcfo.types == ["AlN", "Al", "N"] + assert self.charge_lcfo.atomlist == ["AlN1", "Al2", "N3"] + assert self.charge_lcfo.loewdin == [0.0, 1.02, -1.02] + assert not self.charge_lcfo.mulliken + def test_get_structure_with_charges(self): structure_dict2 = { "lattice": { @@ -592,6 +650,11 @@ def test_get_structure_with_charges(self): s2 = Structure.from_dict(structure_dict2) assert s2 == self.charge2.get_structure_with_charges(f"{VASP_IN_DIR}/POSCAR_MnO") + def test_exception(self): + structure_file = f"{VASP_IN_DIR}/POSCAR.AlN" + with pytest.raises(ValueError, match="CHARGE.LCFO.lobster charges are not sorted site wise"): + self.charge_lcfo.get_structure_with_charges(structure_filename=structure_file) + def test_msonable(self): dict_data = self.charge2.as_dict() charge_from_dict = Charge.from_dict(dict_data) @@ -620,6 +683,9 @@ def setUp(self): # TODO: implement skipping madelung/cobi self.lobsterout_skipping_cobi_madelung = Lobsterout(filename=f"{TEST_DIR}/lobsterout.skip_cobi_madelung") + # Lobster v5.1.1 + self.lobsterout_v511 = Lobsterout(filename=f"{TEST_DIR}/lobsterout_v511.gz") + def test_attributes(self): assert self.lobsterout_normal.basis_functions == [ ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] @@ -973,14 +1039,20 @@ def test_get_doc(self): ], "has_doscar": True, "has_doscar_lso": False, + "has_doscar_lcfo": False, "has_cohpcar": True, + "has_cohpcar_lcfo": False, "has_coopcar": True, + "has_coopcar_lcfo": False, + "has_cobicar_lcfo": False, "has_charge": True, "has_projection": False, "has_bandoverlaps": True, "has_fatbands": False, "has_grosspopulation": False, + "has_polarization": False, "has_density_of_energies": False, + "has_mofecar": False, } for key, item in self.lobsterout_normal.get_doc().items(): if key not in ["has_cobicar", "has_madelung"]: @@ -992,6 +1064,71 @@ def test_get_doc(self): assert item[0] == approx(ref_data[key][0]) elif isinstance(item, list | dict): assert item == ref_data[key] + ref_data_v511 = { + "restart_from_projection": False, + "lobster_version": "v5.1.1", + "threads": 8, + "dft_program": "VASP", + "charge_spilling": [0.0111, 0.0111], + "total_spilling": [], + "elements": ["N", "Al"], + "basis_type": ["pbevaspfit2015", "pbevaspfit2015"], + "basis_functions": [["2s", "2p_y", "2p_z", "2p_x"], ["3s", "3p_y", "3p_z", "3p_x"]], + "timing": { + "wall_time": {"h": "0", "min": "4", "s": "8", "ms": "368"}, + "user_time": {"h": "0", "min": "22", "s": "34", "ms": "960"}, + "sys_time": {"h": "0", "min": "0", "s": "4", "ms": "100"}, + }, + "warning_lines": [ + "2 of 1354 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ], + "info_orthonormalization": ["2 of 1354 k-points could not be orthonormalized with an accuracy of 1.0E-5."], + "info_lines": [ + "No errors detected in LCFO definition...", + "Some atoms were missing in the LCFO basis you defined.", + "In order to avoid problems with matrix handling later,", + "I added each remaining atom as a new fragment.", + "This is NOT an error.", + "You did not specify a value for the point density", + "using the gridDensityForPrinting keyword.", + "Therefore, I will use the default value of 0.05.", + "You did not specify a value for the point density", + "using the gridBufferForPrinting keyword.", + "Therefore, I will use the default value of 3.04 Angstroms.", + ], + "has_doscar": True, + "has_doscar_lso": False, + "has_doscar_lcfo": True, + "has_cohpcar": True, + "has_cohpcar_lcfo": True, + "has_coopcar": True, + "has_coopcar_lcfo": False, + "has_cobicar": True, + "has_cobicar_lcfo": True, + "has_charge": True, + "has_madelung": True, + "has_mofecar": True, + "has_projection": True, + "has_bandoverlaps": True, + "has_fatbands": False, + "has_grosspopulation": True, + "has_polarization": True, + "has_density_of_energies": False, + } + + for key, item in self.lobsterout_v511.get_doc().items(): + if isinstance(item, str): + assert ref_data_v511[key], item + elif isinstance(item, int): + assert ref_data_v511[key] == item + elif key == "charge_spilling": + assert item[0] == approx(ref_data_v511[key][0]) + elif isinstance(item, list | dict): + assert item == ref_data_v511[key] def test_msonable(self): dict_data = self.lobsterout_normal.as_dict() @@ -1404,9 +1541,16 @@ def test_keys(self): class TestGrosspop(TestCase): def setUp(self): self.grosspop1 = Grosspop(f"{TEST_DIR}/GROSSPOP.lobster") + self.grosspop_511_sp = Grosspop(f"{TEST_DIR}/GROSSPOP_511_sp.lobster.AlN.gz") + self.grosspop_511_nsp = Grosspop(f"{TEST_DIR}/GROSSPOP_511_nsp.lobster.NaCl.gz") + self.grosspop_511_lcfo = Grosspop(f"{TEST_DIR}/GROSSPOP.LCFO.lobster.AlN.gz", is_lcfo=True) def test_attributes(self): gross_pop_list = self.grosspop1.list_dict_grosspop + gross_pop_list_511_sp = self.grosspop_511_sp.list_dict_grosspop + gross_pop_list_511_nsp = self.grosspop_511_nsp.list_dict_grosspop + gross_pop_list_lcfo = self.grosspop_511_lcfo.list_dict_grosspop + assert gross_pop_list[0]["Mulliken GP"]["3s"] == approx(0.52) assert gross_pop_list[0]["Mulliken GP"]["3p_y"] == approx(0.38) assert gross_pop_list[0]["Mulliken GP"]["3p_z"] == approx(0.37) @@ -1425,6 +1569,19 @@ def test_attributes(self): assert gross_pop_list[8]["Loewdin GP"]["2s"] == approx(1.60) assert gross_pop_list[8]["element"] == "O" + # v5.1 spin polarized + assert gross_pop_list_511_sp[1]["Mulliken GP"]["3p_y"][Spin.up] == approx(0.19) + assert gross_pop_list_511_sp[3]["Loewdin GP"]["2s"][Spin.down] == approx(0.7) + + # v5.1 non spin polarized + assert gross_pop_list_511_nsp[0]["Mulliken GP"]["3s"] == approx(0.22) + assert gross_pop_list_511_nsp[-1]["Loewdin GP"]["total"] == approx(7.67) + + # v.5.1.1 LCFO + assert self.grosspop_511_lcfo.is_lcfo + assert gross_pop_list_lcfo[0]["mol"] == "AlN" + assert gross_pop_list_lcfo[0]["Loewdin GP"]["4a1"][Spin.up] == approx(0.07) + def test_structure_with_grosspop(self): struct_dict = { "@module": "pymatgen.core.structure", @@ -1518,6 +1675,11 @@ def test_structure_with_grosspop(self): new_structure = self.grosspop1.get_structure_with_total_grosspop(f"{TEST_DIR}/POSCAR.SiO2") assert_allclose(new_structure.frac_coords, Structure.from_dict(struct_dict).frac_coords) + def test_exception(self): + structure_file = f"{VASP_IN_DIR}/POSCAR.AlN" + with pytest.raises(ValueError, match="The GROSSPOP.LCFO.lobster data is not site wise"): + self.grosspop_511_lcfo.get_structure_with_total_grosspop(structure_filename=structure_file) + def test_msonable(self): dict_data = self.grosspop1.as_dict() grosspop_from_dict = Grosspop.from_dict(dict_data) @@ -1540,6 +1702,17 @@ def setUp(self): filename=f"{TEST_DIR}/ICOHPLIST.lobster", are_coops=True, ) + # ICOHPLIST.lobster from Lobster >v5 + self.icohp_aln_511_sp = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST_511_sp.lobster.AlN.gz") + self.icohp_nacl_511_nsp = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST_511_nsp.lobster.NaCl.gz") + + # ICOHPLIST.LCFO.lobster from Lobster v5.1.1 + self.icohp_lcfo = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.LCFO.lobster.AlN.gz", is_lcfo=True) + self.icohp_lcfo_non_orbitalwise = Icohplist( + filename=f"{TEST_DIR}/ICOHPLIST_non_orbitalwise.LCFO.lobster.AlN.gz", + is_lcfo=True, + ) + # ICOBIs and orbitalwise ICOBILIST.lobster self.icobi_orbitalwise = Icohplist( filename=f"{TEST_DIR}/ICOBILIST.lobster", @@ -1594,6 +1767,19 @@ def test_attributes(self): assert self.icobi_orbitalwise_add.orbitalwise assert self.icobi_orbitalwise_spinpolarized_add.orbitalwise + # >v5 ICOHPLIST + assert self.icohp_aln_511_sp.is_spin_polarized + assert len(self.icohp_aln_511_sp.icohplist) == 64 + assert not self.icohp_nacl_511_nsp.is_spin_polarized + assert len(self.icohp_nacl_511_nsp.icohplist) == 152 + + # v5.1.1 LCFO + assert self.icohp_lcfo.is_lcfo + assert self.icohp_lcfo.is_spin_polarized + assert len(self.icohp_lcfo.icohplist) == 28 + assert not self.icohp_lcfo_non_orbitalwise.orbitalwise + assert len(self.icohp_lcfo_non_orbitalwise.icohplist) == 27 + def test_values(self): icohplist_bise = { "1": { @@ -1785,6 +1971,17 @@ def test_values(self): assert self.icobi.icohpcollection.extremum_icohpvalue() == 0.58649 assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["orbitals"]["2s-6s"]["icohp"][Spin.up] == 0.0247 + # >v5 ICOHPLIST + assert self.icohp_aln_511_sp.icohplist["2"]["icohp"][Spin.up] == approx(-0.21482) + assert self.icohp_aln_511_sp.icohplist["2"]["icohp"][Spin.down] == approx(-0.21493) + assert self.icohp_nacl_511_nsp.icohplist["13"]["icohp"][Spin.up] == approx(-0.03021) + assert self.icohp_nacl_511_nsp.icohplist["10"]["orbitals"]["3s-2py"]["icohp"][Spin.up] == approx(-0.00113) + + # v5.1.1 LCFO + assert self.icohp_lcfo.icohplist["15"]["orbitals"]["3a1-3p"]["icohp"][Spin.up] == approx(-0.00586) + assert self.icohp_lcfo_non_orbitalwise.icohplist["16"]["icohp"][Spin.up] == approx(-0.2983) + assert self.icohp_lcfo_non_orbitalwise.icohplist["16"]["icohp"][Spin.down] == approx(-0.29842) + def test_msonable(self): dict_data = self.icobi_orbitalwise_spinpolarized.as_dict() icohplist_from_dict = Icohplist.from_dict(dict_data) @@ -2061,3 +2258,24 @@ def test_raises(self): match="Please check provided input file, it seems to be empty", ): self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_DIR}/hamiltonMatrices.lobster") + + +class TestPolarization(PymatgenTest): + def setUp(self) -> None: + self.polarization = Polarization(filename=f"{TEST_DIR}/POLARIZATION.lobster.AlN.gz") + + def test_attributes(self): + assert self.polarization.rel_loewdin_pol_vector == { + "x": -0.0, + "y": -0.01, + "z": 45.62, + "abs": 45.62, + "unit": "uC/cm2", + } + assert self.polarization.rel_mulliken_pol_vector == { + "x": -0.0, + "y": -0.02, + "z": 56.14, + "abs": 56.14, + "unit": "uC/cm2", + } From 50321d685e5b4f82a4c68d130988c88ab0129c64 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Tue, 22 Oct 2024 04:57:41 +0800 Subject: [PATCH 082/180] Remove all `no_type_check` mark, fix new `mypy` error in `1.12.0` (#4110) * remove no_type_check mark * remove no_type_check from plotter as it seems this have to go first * remove mypy version pin * take it slow and pin mypy for now * fix io.lmto * fix electronic structure plotter * fix phase diagram * fix pourbaix diagram * migrate config * bump pyright version * fix pyright in io.lmto * fix pyright in phase diagram * remove mypy pin * fix vis.structure_vtk * fix core ptable * pre-commit auto-fixes * fix lobtser outputd * fix typo in vasprun test * handle converged_electronic for vasp ALGO=CHI * fix another vasp.output error * tweak code style a bit * suppress overload error for now * fix electronic structure plotter * tweak code style a bit * fix type * fix incorrect type of cn * use overload type * bump pre-commit, mainly to get mypy 1.12 * fix incorrect return type of get_parameters * fix most errors in local_env * all done * use set to check membership * a bit more specific type for DEFAULT_OP_PARAMS --------- Signed-off-by: Shyue Ping Ong Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Shyue Ping Ong --- outputs.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/outputs.py b/outputs.py index 14020bdede..3b307570eb 100644 --- a/outputs.py +++ b/outputs.py @@ -2097,14 +2097,14 @@ def __init__( madelungenergies_loewdin (float): Madelung energy based on the Loewdin approach. """ self._filename = filename - self.ewald_splitting = [] if ewald_splitting is None else ewald_splitting - self.num_atoms = num_atoms - self.types = [] if types is None else types - self.atomlist = [] if atomlist is None else atomlist - self.sitepotentials_loewdin = [] if sitepotentials_loewdin is None else sitepotentials_loewdin - self.sitepotentials_mulliken = [] if sitepotentials_mulliken is None else sitepotentials_mulliken - self.madelungenergies_loewdin = [] if madelungenergies_loewdin is None else madelungenergies_loewdin - self.madelungenergies_mulliken = [] if madelungenergies_mulliken is None else madelungenergies_mulliken + self.ewald_splitting: list | float = ewald_splitting or [] + self.num_atoms: int | None = num_atoms + self.types: list[str] = types or [] + self.atomlist: list[str] = atomlist or [] + self.sitepotentials_loewdin: list[float] = sitepotentials_loewdin or [] + self.sitepotentials_mulliken: list[float] = sitepotentials_mulliken or [] + self.madelungenergies_loewdin: list | float = madelungenergies_loewdin or [] + self.madelungenergies_mulliken: list | float = madelungenergies_mulliken or [] if self.num_atoms is None: with zopen(filename, mode="rt") as file: From 83257e33bcca855a8d69a94c97ca7c54562b8321 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Tue, 22 Oct 2024 18:04:11 -0700 Subject: [PATCH 083/180] Ruff format. --- lobsterenv.py | 36 ++++++++++++++++---- outputs.py | 92 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index f59b6f73b4..dd8a53badf 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -623,7 +623,10 @@ def _get_plot_label(self, atoms: list[list[str]], per_bond: bool) -> str: """Count the types of bonds and append a label.""" all_labels = [] for atoms_names in atoms: - new = [self._split_string(atoms_names[0])[0], self._split_string(atoms_names[1])[0]] + new = [ + self._split_string(atoms_names[0])[0], + self._split_string(atoms_names[1])[0], + ] new.sort() string_here = f"{new[0]}-{new[1]}" all_labels.append(string_here) @@ -721,7 +724,11 @@ def get_info_icohps_between_neighbors( elif not done: icohp_trans = -np.asarray( - [icohp._translation[0], icohp._translation[1], icohp._translation[2]] + [ + icohp._translation[0], + icohp._translation[1], + icohp._translation[2], + ] ) if (np.all(np.asarray(translation) == np.asarray(icohp._translation))) or ( @@ -783,9 +790,14 @@ def _evaluate_ce( raise ValueError("Please give two limits or leave them both at None") # Find environments based on ICOHP values - list_icohps, list_keys, list_lengths, list_neighisite, list_neighsite, list_coords = self._find_environments( - additional_condition, lowerlimit, upperlimit, only_bonds_to - ) + ( + list_icohps, + list_keys, + list_lengths, + list_neighisite, + list_neighsite, + list_coords, + ) = self._find_environments(additional_condition, lowerlimit, upperlimit, only_bonds_to) self.list_icohps = list_icohps self.list_lengths = list_lengths @@ -922,7 +934,12 @@ def _find_environments( ) additional_conds = self._find_relevant_atoms_additional_condition(idx, icohps, additional_condition) - keys_from_ICOHPs, lengths_from_ICOHPs, neighbors_from_ICOHPs, selected_ICOHPs = additional_conds + ( + keys_from_ICOHPs, + lengths_from_ICOHPs, + neighbors_from_ICOHPs, + selected_ICOHPs, + ) = additional_conds if len(neighbors_from_ICOHPs) > 0: centralsite = site @@ -1142,7 +1159,12 @@ def _find_relevant_atoms_additional_condition( icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - return keys_from_ICOHPs, lengths_from_ICOHPs, neighbors_from_ICOHPs, icohps_from_ICOHPs + return ( + keys_from_ICOHPs, + lengths_from_ICOHPs, + neighbors_from_ICOHPs, + icohps_from_ICOHPs, + ) @staticmethod def _get_icohps( diff --git a/outputs.py b/outputs.py index 3b307570eb..96528279df 100644 --- a/outputs.py +++ b/outputs.py @@ -207,7 +207,9 @@ def __init__( else: bond_data = self._get_bond_data( - lines[2 + bond], is_lcfo=self.is_lcfo, are_multi_center_cobis=self.are_multi_center_cobis + lines[2 + bond], + is_lcfo=self.is_lcfo, + are_multi_center_cobis=self.are_multi_center_cobis, ) label = str(bond_num) @@ -484,13 +486,21 @@ def __init__( if version == "5.1.0": num = 1 - translation = (int(line_parts[4]), int(line_parts[5]), int(line_parts[6])) + translation = ( + int(line_parts[4]), + int(line_parts[5]), + int(line_parts[6]), + ) icohp[Spin.up] = float(line_parts[7]) if self.is_spin_polarized: icohp[Spin.down] = float(line_parts[8]) elif version == "3.1.1": num = 1 - translation = (int(line_parts[4]), int(line_parts[5]), int(line_parts[6])) + translation = ( + int(line_parts[4]), + int(line_parts[5]), + int(line_parts[6]), + ) icohp[Spin.up] = float(line_parts[7]) if self.is_spin_polarized: icohp[Spin.down] = float(data_without_orbitals[bond + n_bonds + 1].split()[7]) @@ -541,7 +551,10 @@ def __init__( if len(list_orb_icohp) < int(label): list_orb_icohp.append({orb_label: {"icohp": icohp, "orbitals": orbitals}}) else: - list_orb_icohp[int(label) - 1][orb_label] = {"icohp": icohp, "orbitals": orbitals} + list_orb_icohp[int(label) - 1][orb_label] = { + "icohp": icohp, + "orbitals": orbitals, + } # Avoid circular import from pymatgen.electronic_structure.cohp import IcohpCollection @@ -1286,9 +1299,24 @@ def _get_timing( if "sys" in line_parts: sys_times = line_parts[:8] - wall_time_dict = {"h": wall_times[0], "min": wall_times[2], "s": wall_times[4], "ms": wall_times[6]} - user_time_dict = {"h": user_times[0], "min": user_times[2], "s": user_times[4], "ms": user_times[6]} - sys_time_dict = {"h": sys_times[0], "min": sys_times[2], "s": sys_times[4], "ms": sys_times[6]} + wall_time_dict = { + "h": wall_times[0], + "min": wall_times[2], + "s": wall_times[4], + "ms": wall_times[6], + } + user_time_dict = { + "h": user_times[0], + "min": user_times[2], + "s": user_times[4], + "ms": user_times[6], + } + sys_time_dict = { + "h": sys_times[0], + "min": sys_times[2], + "s": sys_times[4], + "ms": sys_times[6], + } return wall_time_dict, user_time_dict, sys_time_dict @@ -1752,7 +1780,11 @@ def __init__( for line in lines[3:]: cleanlines = [idx for idx in line.split(" ") if idx != ""] if len(cleanlines) == 5 and cleanlines[0].isdigit() and not self.is_lcfo: - small_dict = {"Mulliken GP": {}, "Loewdin GP": {}, "element": cleanlines[1]} + small_dict = { + "Mulliken GP": {}, + "Loewdin GP": {}, + "element": cleanlines[1], + } small_dict["Mulliken GP"][cleanlines[2]] = float(cleanlines[3]) small_dict["Loewdin GP"][cleanlines[2]] = float(cleanlines[4]) elif len(cleanlines) == 4 and cleanlines[0].isdigit() and self.is_lcfo: @@ -1776,7 +1808,11 @@ def __init__( if "total" in cleanlines[0]: self.list_dict_grosspop.append(small_dict) elif len(cleanlines) == 7 and cleanlines[0].isdigit(): - small_dict = {"Mulliken GP": {}, "Loewdin GP": {}, "element": cleanlines[1]} + small_dict = { + "Mulliken GP": {}, + "Loewdin GP": {}, + "element": cleanlines[1], + } small_dict["Mulliken GP"][cleanlines[2]] = { Spin.up: float(cleanlines[3]), Spin.down: float(cleanlines[4]), @@ -2271,24 +2307,32 @@ def __init__( if "hamilton" in self._filename: if e_fermi is None: raise ValueError("Please provide the fermi energy in eV ") - self.onsite_energies, self.average_onsite_energies, self.hamilton_matrices = self._parse_matrix( - file_data=lines, pattern=pattern_coeff_hamil_trans, e_fermi=e_fermi - ) + ( + self.onsite_energies, + self.average_onsite_energies, + self.hamilton_matrices, + ) = self._parse_matrix(file_data=lines, pattern=pattern_coeff_hamil_trans, e_fermi=e_fermi) elif "coefficient" in self._filename: - self.onsite_coefficients, self.average_onsite_coefficient, self.coefficient_matrices = self._parse_matrix( - file_data=lines, pattern=pattern_coeff_hamil_trans, e_fermi=0 - ) + ( + self.onsite_coefficients, + self.average_onsite_coefficient, + self.coefficient_matrices, + ) = self._parse_matrix(file_data=lines, pattern=pattern_coeff_hamil_trans, e_fermi=0) elif "transfer" in self._filename: - self.onsite_transfer, self.average_onsite_transfer, self.transfer_matrices = self._parse_matrix( - file_data=lines, pattern=pattern_coeff_hamil_trans, e_fermi=0 - ) + ( + self.onsite_transfer, + self.average_onsite_transfer, + self.transfer_matrices, + ) = self._parse_matrix(file_data=lines, pattern=pattern_coeff_hamil_trans, e_fermi=0) elif "overlap" in self._filename: - self.onsite_overlaps, self.average_onsite_overlaps, self.overlap_matrices = self._parse_matrix( - file_data=lines, pattern=pattern_overlap, e_fermi=0 - ) + ( + self.onsite_overlaps, + self.average_onsite_overlaps, + self.overlap_matrices, + ) = self._parse_matrix(file_data=lines, pattern=pattern_overlap, e_fermi=0) @staticmethod def _parse_matrix( @@ -2364,7 +2408,11 @@ def _parse_matrix( zip(elements_basis_functions, average_matrix_diagonal_values, strict=True) ) - return matrix_diagonal_values, average_average_matrix_diag_dict, complex_matrices + return ( + matrix_diagonal_values, + average_average_matrix_diag_dict, + complex_matrices, + ) class Polarization(MSONable): From b7b825a3b64c9076aedb6f8432b45543b4da61a7 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Tue, 22 Oct 2024 18:04:11 -0700 Subject: [PATCH 084/180] Ruff format. --- tests/io/lobster/test_inputs.py | 42 +++++- tests/io/lobster/test_lobsterenv.py | 63 +++++++-- tests/io/lobster/test_outputs.py | 205 ++++++++++++++++++++++++---- 3 files changed, 268 insertions(+), 42 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 37e4e957e1..59960d5970 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -112,12 +112,21 @@ def test_initialize_from_dict(self): assert lobsterin["gaussiansmearingwidth"] == approx(0.1) assert lobsterin["basisfunctions"][0] == "Fe 3d 4p 4s" assert lobsterin["basisfunctions"][1] == "Co 3d 4p 4s" - assert {*lobsterin} >= {"skipdos", "skipcohp", "skipcoop", "skippopulationanalysis", "skipgrosspopulation"} + assert {*lobsterin} >= { + "skipdos", + "skipcohp", + "skipcoop", + "skippopulationanalysis", + "skipgrosspopulation", + } with pytest.raises(KeyError, match="There are duplicates for the keywords!"): lobsterin2 = Lobsterin({"cohpstartenergy": -15.0, "cohpstartEnergy": -20.0}) lobsterin2 = Lobsterin({"cohpstartenergy": -15.0}) # can only calculate nbands if basis functions are provided - with pytest.raises(ValueError, match="No basis functions are provided. The program cannot calculate nbands."): + with pytest.raises( + ValueError, + match="No basis functions are provided. The program cannot calculate nbands.", + ): lobsterin2._get_nbands(structure=Structure.from_file(f"{VASP_IN_DIR}/POSCAR_Fe3O4")) def test_standard_settings(self): @@ -229,7 +238,10 @@ def test_standard_settings(self): assert "gaussiansmearingwidth" not in lobsterin_new # fatband and ISMEAR=-5 does not work together - with pytest.raises(ValueError, match="ISMEAR has to be 0 for a fatband calculation with Lobster"): + with pytest.raises( + ValueError, + match="ISMEAR has to be 0 for a fatband calculation with Lobster", + ): lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( f"{VASP_IN_DIR}/POSCAR_Fe3O4", f"{VASP_IN_DIR}/INCAR.lobster2", @@ -355,8 +367,14 @@ def test_get_all_possible_basis_functions(self): def test_get_potcar_symbols(self): lobsterin1 = Lobsterin({}) - assert lobsterin1._get_potcar_symbols(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") == ["Fe", "O"] - assert lobsterin1._get_potcar_symbols(f"{TEST_DIR}/POTCAR.GaAs") == ["Ga_d", "As"] + assert lobsterin1._get_potcar_symbols(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") == [ + "Fe", + "O", + ] + assert lobsterin1._get_potcar_symbols(f"{TEST_DIR}/POTCAR.GaAs") == [ + "Ga_d", + "As", + ] def test_write_lobsterin(self): # write lobsterin, read it and compare it @@ -447,7 +465,12 @@ def test_write_kpoints(self): assert labels == labels2 # without line mode - lobsterin1.write_KPOINTS(POSCAR_input=outfile_path2, KPOINTS_output=outfile_path, line_mode=False, isym=-1) + lobsterin1.write_KPOINTS( + POSCAR_input=outfile_path2, + KPOINTS_output=outfile_path, + line_mode=False, + isym=-1, + ) kpoint = Kpoints.from_file(outfile_path) kpoint2 = Kpoints.from_file(f"{VASP_OUT_DIR}/IBZKPT.lobster") @@ -572,7 +595,12 @@ def test_get_all_possible_basis_combinations(self): min_basis = ["Li 1s 2s"] max_basis = ["Li 1s 2s 2p 3s"] combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) - assert combinations_basis == [["Li 1s 2s"], ["Li 1s 2s 2p"], ["Li 1s 2s 3s"], ["Li 1s 2s 2p 3s"]] + assert combinations_basis == [ + ["Li 1s 2s"], + ["Li 1s 2s 2p"], + ["Li 1s 2s 3s"], + ["Li 1s 2s 2p 3s"], + ] min_basis = ["Li 1s 2s", "Na 1s 2s"] max_basis = ["Li 1s 2s 2p 3s", "Na 1s 2s 2p 3s"] diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index e4d7ff8273..2d4700a7b8 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -285,7 +285,8 @@ def setUp(self): def test_cation_anion_mode_without_ions(self): with pytest.raises( - ValueError, match="Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work" + ValueError, + match="Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work", ): _ = LobsterNeighbors( are_coops=False, @@ -295,7 +296,8 @@ def test_cation_anion_mode_without_ions(self): additional_condition=1, ) with pytest.raises( - ValueError, match="All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work" + ValueError, + match="All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work", ): _ = LobsterNeighbors( are_coops=False, @@ -308,7 +310,8 @@ def test_cation_anion_mode_without_ions(self): def test_wrong_additional_correction(self): with pytest.raises( - ValueError, match=r"Unexpected additional_condition=10, must be one of \[0, 1, 2, 3, 4, 5, 6\]" + ValueError, + match=r"Unexpected additional_condition=10, must be one of \[0, 1, 2, 3, 4, 5, 6\]", ): LobsterNeighbors( are_coops=False, @@ -733,25 +736,53 @@ def test_get_sum_icohps_between_neighbors_of_atom(self): def test_get_plot_label(self): label = self.chem_env_lobster1._get_plot_label( - atoms=[["Re1", "O2"], ["Re1", "O2"], ["Re1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Re1", "O4"]], + atoms=[ + ["Re1", "O2"], + ["Re1", "O2"], + ["Re1", "O3"], + ["Re1", "O3"], + ["Re1", "O4"], + ["Re1", "O4"], + ], per_bond=False, ) assert label == "6 x O-Re" label = self.chem_env_lobster1._get_plot_label( - atoms=[["Re1", "O2"], ["Re1", "O2"], ["Re1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Si1", "O4"]], + atoms=[ + ["Re1", "O2"], + ["Re1", "O2"], + ["Re1", "O3"], + ["Re1", "O3"], + ["Re1", "O4"], + ["Si1", "O4"], + ], per_bond=False, ) assert label == "5 x O-Re, 1 x O-Si" label = self.chem_env_lobster1._get_plot_label( - atoms=[["Si1", "O2"], ["Si1", "O2"], ["Si1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Si1", "O4"]], + atoms=[ + ["Si1", "O2"], + ["Si1", "O2"], + ["Si1", "O3"], + ["Re1", "O3"], + ["Re1", "O4"], + ["Si1", "O4"], + ], per_bond=False, ) assert label == "4 x O-Si, 2 x O-Re" label = self.chem_env_lobster1._get_plot_label( - atoms=[["Re1", "O2"], ["Re1", "O2"], ["Re1", "O3"], ["Re1", "O3"], ["Re1", "O4"], ["Re1", "O4"]], + atoms=[ + ["Re1", "O2"], + ["Re1", "O2"], + ["Re1", "O3"], + ["Re1", "O3"], + ["Re1", "O4"], + ["Re1", "O4"], + ], per_bond=True, ) assert label == "6 x O-Re (per bond)" @@ -810,7 +841,9 @@ def test_get_info_cohps_to_neighbors(self): assert plot_label == "1 x Na-Si (per bond)" obj_cohpcar = CompleteCohp.from_file( - filename=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", fmt="LOBSTER", structure_file=f"{TEST_DIR}/POSCAR.NaSi.gz" + filename=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", + fmt="LOBSTER", + structure_file=f"{TEST_DIR}/POSCAR.NaSi.gz", ) plot_label_obj, _summed_cohpcar_NaSi_obj = self.chem_env_w_obj.get_info_cohps_to_neighbors( obj_cohpcar=obj_cohpcar, @@ -864,7 +897,17 @@ def test_get_info_cohps_to_neighbors(self): ) def test_valences(self): - assert self.chem_env_lobster1_charges_noisecutoff.valences == [0.75, -0.75] # Mulliken - assert self.chem_env_lobster1_charges_loewdin.valences == [0.27, 0.27, 0.27, 0.27, -0.54, -0.54] + assert self.chem_env_lobster1_charges_noisecutoff.valences == [ + 0.75, + -0.75, + ] # Mulliken + assert self.chem_env_lobster1_charges_loewdin.valences == [ + 0.27, + 0.27, + 0.27, + 0.27, + -0.54, + -0.54, + ] assert self.chem_env_w_obj.valences == [0.67] * 4 + [0.7] * 4 + [-0.7] * 4 + [-0.68] * 4 # charge_obj assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index c84b11d67b..86a7e589c2 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -96,7 +96,9 @@ def setUp(self): ) # 4 center self.cobi3 = Cohpcar( - filename=f"{TEST_DIR}/COBICAR.lobster.GeTe_4center", are_cobis=False, are_multi_center_cobis=True + filename=f"{TEST_DIR}/COBICAR.lobster.GeTe_4center", + are_cobis=False, + are_multi_center_cobis=True, ) # partially orbital-resolved self.cobi4 = Cohpcar( @@ -113,7 +115,9 @@ def setUp(self): # spin polarized # fully orbital-resolved self.cobi6 = Cohpcar( - filename=f"{TEST_DIR}/COBICAR.lobster.B2H6.spin", are_cobis=False, are_multi_center_cobis=True + filename=f"{TEST_DIR}/COBICAR.lobster.B2H6.spin", + are_cobis=False, + are_multi_center_cobis=True, ) # COHPCAR.LCFO.lobster from v5.1.1 @@ -514,9 +518,45 @@ def test_pdos(self): assert self.DOSCAR_nonspin_pol.pdos[0]["2p_x"][Spin.up].tolist() == pdos_f_2px # test with DOSCAR.LCFO.lobster file - pdos_1a1_AlN = [0.0, 0.22594, 0.01335, 0.0, 0.0, 0.0, 0.00228, 0.02836, 0.03053, 0.01612, 0.02379] - pdos_3py_Al = [0.0, 0.02794, 0.00069, 0.0, 0.0, 0.0, 0.00216, 0.0682, 0.06966, 0.04402, 0.16579] - pdos_2s_N = [0.0, 0.25324, 0.0157, 0.0, 0.0, 0.0, 0.0006, 0.01747, 0.02247, 0.01589, 0.03565] + pdos_1a1_AlN = [ + 0.0, + 0.22594, + 0.01335, + 0.0, + 0.0, + 0.0, + 0.00228, + 0.02836, + 0.03053, + 0.01612, + 0.02379, + ] + pdos_3py_Al = [ + 0.0, + 0.02794, + 0.00069, + 0.0, + 0.0, + 0.0, + 0.00216, + 0.0682, + 0.06966, + 0.04402, + 0.16579, + ] + pdos_2s_N = [ + 0.0, + 0.25324, + 0.0157, + 0.0, + 0.0, + 0.0, + 0.0006, + 0.01747, + 0.02247, + 0.01589, + 0.03565, + ] assert self.DOSCAR_lcfo._is_lcfo assert self.DOSCAR_lcfo.pdos[0]["1a1"][Spin.down].tolist() == pdos_1a1_AlN @@ -564,7 +604,19 @@ def test_tdensities(self): assert tdos_nonspin == self.DOSCAR_nonspin_pol.tdensities[Spin.up].tolist() # test with DOSCAR.LCFO.lobster file - tdos_up = [0.0, 1.75477, 0.11803, 0.0, 0.0, 0.0, 0.04156, 0.82291, 0.74449, 0.42481, 1.04535] + tdos_up = [ + 0.0, + 1.75477, + 0.11803, + 0.0, + 0.0, + 0.0, + 0.04156, + 0.82291, + 0.74449, + 0.42481, + 1.04535, + ] assert tdos_up == self.DOSCAR_lcfo.tdensities[Spin.up].tolist() @@ -688,7 +740,18 @@ def setUp(self): def test_attributes(self): assert self.lobsterout_normal.basis_functions == [ - ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] ] assert self.lobsterout_normal.basis_type == ["pbeVaspFit2015"] assert self.lobsterout_normal.charge_spilling == [0.0268] @@ -730,7 +793,18 @@ def test_attributes(self): ] assert self.lobsterout_fatband_grosspop_densityofenergies.basis_functions == [ - ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] ] assert self.lobsterout_fatband_grosspop_densityofenergies.basis_type == ["pbeVaspFit2015"] assert self.lobsterout_fatband_grosspop_densityofenergies.charge_spilling == [0.0268] @@ -772,7 +846,18 @@ def test_attributes(self): ] assert self.lobsterout_saveprojection.basis_functions == [ - ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] ] assert self.lobsterout_saveprojection.basis_type == ["pbeVaspFit2015"] assert self.lobsterout_saveprojection.charge_spilling == [0.0268] @@ -814,7 +899,18 @@ def test_attributes(self): ] assert self.lobsterout_skipping_all.basis_functions == [ - ["3s", "4s", "3p_y", "3p_z", "3p_x", "3d_xy", "3d_yz", "3d_z^2", "3d_xz", "3d_x^2-y^2"] + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] ] assert self.lobsterout_skipping_all.basis_type == ["pbeVaspFit2015"] assert self.lobsterout_skipping_all.charge_spilling == [0.0268] @@ -1073,7 +1169,10 @@ def test_get_doc(self): "total_spilling": [], "elements": ["N", "Al"], "basis_type": ["pbevaspfit2015", "pbevaspfit2015"], - "basis_functions": [["2s", "2p_y", "2p_z", "2p_x"], ["3s", "3p_y", "3p_z", "3p_x"]], + "basis_functions": [ + ["2s", "2p_y", "2p_z", "2p_x"], + ["3s", "3p_y", "3p_z", "3p_x"], + ], "timing": { "wall_time": {"h": "0", "min": "4", "s": "8", "ms": "368"}, "user_time": {"h": "0", "min": "22", "s": "34", "ms": "960"}, @@ -1249,7 +1348,8 @@ def test_raises(self): structure=self.structure, ) with pytest.raises( - ValueError, match="The are two FATBAND files for the same atom and orbital. The program will stop" + ValueError, + match="The are two FATBAND files for the same atom and orbital. The program will stop", ): self.fatband_SiO2_p_x = Fatband( filenames=[ @@ -2102,8 +2202,20 @@ def setUp(self) -> None: self.sitepotential = SitePotential(filename=f"{TEST_DIR}/SitePotentials.lobster.perovskite") def test_attributes(self): - assert self.sitepotential.sitepotentials_Loewdin == [-8.77, -17.08, 9.57, 9.57, 8.45] - assert self.sitepotential.sitepotentials_Mulliken == [-11.38, -19.62, 11.18, 11.18, 10.09] + assert self.sitepotential.sitepotentials_Loewdin == [ + -8.77, + -17.08, + 9.57, + 9.57, + 8.45, + ] + assert self.sitepotential.sitepotentials_Mulliken == [ + -11.38, + -19.62, + 11.18, + 11.18, + 10.09, + ] assert self.sitepotential.madelungenergies_Loewdin == approx(-28.64) assert self.sitepotential.madelungenergies_Mulliken == approx(-40.02) assert self.sitepotential.atomlist == ["La1", "Ta2", "N3", "N4", "O5"] @@ -2113,8 +2225,20 @@ def test_attributes(self): def test_get_structure(self): structure = self.sitepotential.get_structure_with_site_potentials(f"{TEST_DIR}/POSCAR.perovskite") - assert structure.site_properties["Loewdin Site Potentials (eV)"] == [-8.77, -17.08, 9.57, 9.57, 8.45] - assert structure.site_properties["Mulliken Site Potentials (eV)"] == [-11.38, -19.62, 11.18, 11.18, 10.09] + assert structure.site_properties["Loewdin Site Potentials (eV)"] == [ + -8.77, + -17.08, + 9.57, + 9.57, + 8.45, + ] + assert structure.site_properties["Mulliken Site Potentials (eV)"] == [ + -11.38, + -19.62, + 11.18, + 11.18, + 10.09, + ] def test_msonable(self): dict_data = self.sitepotential.as_dict() @@ -2153,7 +2277,12 @@ def setUp(self) -> None: def test_attributes(self): # hamilton matrices assert self.hamilton_matrices.average_onsite_energies == pytest.approx( - {"Na1_3s": 0.58855353, "Na1_2p_y": -25.72719646, "Na1_2p_z": -25.72719646, "Na1_2p_x": -25.72719646} + { + "Na1_3s": 0.58855353, + "Na1_2p_y": -25.72719646, + "Na1_2p_z": -25.72719646, + "Na1_2p_x": -25.72719646, + } ) ref_onsite_energies = [ [-0.22519646, -25.76989646, -25.76989646, -25.76989646], @@ -2163,7 +2292,10 @@ def test_attributes(self): ref_imag_mat_spin_up = np.zeros((4, 4)) - assert_allclose(self.hamilton_matrices.hamilton_matrices["1"][Spin.up].imag, ref_imag_mat_spin_up) + assert_allclose( + self.hamilton_matrices.hamilton_matrices["1"][Spin.up].imag, + ref_imag_mat_spin_up, + ) ref_real_mat_spin_up = [ [-3.0217, 0.0, 0.0, 0.0], @@ -2171,11 +2303,19 @@ def test_attributes(self): [0.0, 0.0, -28.5664, 0.0], [0.0, 0.0, 0.0, -28.5664], ] - assert_allclose(self.hamilton_matrices.hamilton_matrices["1"][Spin.up].real, ref_real_mat_spin_up) + assert_allclose( + self.hamilton_matrices.hamilton_matrices["1"][Spin.up].real, + ref_real_mat_spin_up, + ) # overlap matrices assert self.overlap_matrices.average_onsite_overlaps == pytest.approx( - {"Si1_3s": 1.00000009, "Si1_3p_y": 0.99999995, "Si1_3p_z": 0.99999995, "Si1_3p_x": 0.99999995} + { + "Si1_3s": 1.00000009, + "Si1_3p_y": 0.99999995, + "Si1_3p_z": 0.99999995, + "Si1_3p_x": 0.99999995, + } ) ref_onsite_ovelaps = [[1.00000009, 0.99999995, 0.99999995, 0.99999995]] @@ -2209,7 +2349,10 @@ def test_attributes(self): [0.0, -0.12220894, -0.09749622, -0.53739499], ] - assert_allclose(self.transfer_matrices.transfer_matrices["1"][Spin.down].imag, ref_imag_mat_spin_down) + assert_allclose( + self.transfer_matrices.transfer_matrices["1"][Spin.down].imag, + ref_imag_mat_spin_down, + ) ref_real_mat_spin_down = [ [-0.03735031, 0.0, 0.0, 0.0], @@ -2218,10 +2361,16 @@ def test_attributes(self): [0.0, 0.11473763, 0.09742773, 0.80648063], ] - assert_allclose(self.transfer_matrices.transfer_matrices["1"][Spin.down].real, ref_real_mat_spin_down) + assert_allclose( + self.transfer_matrices.transfer_matrices["1"][Spin.down].real, + ref_real_mat_spin_down, + ) # coefficient matrices - assert list(self.coeff_matrices.coefficient_matrices["1"]) == [Spin.up, Spin.down] + assert list(self.coeff_matrices.coefficient_matrices["1"]) == [ + Spin.up, + Spin.down, + ] assert self.coeff_matrices.average_onsite_coefficient == pytest.approx( { "Si1_3s": 0.6232626450000001, @@ -2238,7 +2387,10 @@ def test_attributes(self): [0.0, 0.47836456, 0.00476861, 0.50184424], ] - assert_allclose(self.coeff_matrices.coefficient_matrices["1"][Spin.up].imag, ref_imag_mat_spin_up) + assert_allclose( + self.coeff_matrices.coefficient_matrices["1"][Spin.up].imag, + ref_imag_mat_spin_up, + ) ref_real_mat_spin_up = [ [0.80226096, 0.0, 0.0, 0.0], @@ -2247,7 +2399,10 @@ def test_attributes(self): [0.0, -0.32075579, -0.00405544, 0.64528776], ] - assert_allclose(self.coeff_matrices.coefficient_matrices["1"][Spin.up].real, ref_real_mat_spin_up) + assert_allclose( + self.coeff_matrices.coefficient_matrices["1"][Spin.up].real, + ref_real_mat_spin_up, + ) def test_raises(self): with pytest.raises(ValueError, match="Please provide the fermi energy in eV"): From de13adfa3c344e42fd6ac89c0bab89b9a0e28e33 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Tue, 22 Oct 2024 18:04:11 -0700 Subject: [PATCH 085/180] Ruff format. --- cohp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cohp.py b/cohp.py index 498c58fc97..cb82939c4f 100644 --- a/cohp.py +++ b/cohp.py @@ -1133,7 +1133,7 @@ def __init__( list_num: list[int], list_icohp: list[dict[Spin, float]], is_spin_polarized: bool, - list_orb_icohp: list[dict[str, dict[Literal["icohp", "orbitals"], Any]]] | None = None, + list_orb_icohp: (list[dict[str, dict[Literal["icohp", "orbitals"], Any]]] | None) = None, are_coops: bool = False, are_cobis: bool = False, ) -> None: From a955591778ea49c183062d60cf8f9a93cf3a3e79 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Wed, 30 Oct 2024 08:58:38 +0100 Subject: [PATCH 086/180] partially test translations in lobsterenv --- lobsterenv.py | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index dd8a53badf..c485c0e35f 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -796,7 +796,7 @@ def _evaluate_ce( list_lengths, list_neighisite, list_neighsite, - list_coords, + list_coords ) = self._find_environments(additional_condition, lowerlimit, upperlimit, only_bonds_to) self.list_icohps = list_icohps @@ -939,11 +939,14 @@ def _find_environments( lengths_from_ICOHPs, neighbors_from_ICOHPs, selected_ICOHPs, + translations_ICOHPs, ) = additional_conds if len(neighbors_from_ICOHPs) > 0: centralsite = site - + copysite=copy.copy(centralsite) + cell_start=centralsite.frac_coords-copysite.to_unit_cell().frac_coords + print(cell_start) neighbors_by_distance_start = self.structure.get_sites_in_sphere( pt=centralsite.coords, r=np.max(lengths_from_ICOHPs) + 0.5, @@ -955,11 +958,13 @@ def _find_environments( list_distances = [] index_here_list = [] coords = [] + translations_by_distance=[] for neigh_new in sorted(neighbors_by_distance_start, key=lambda x: x[1]): site_here = neigh_new[0].to_unit_cell() index_here = neigh_new[2] index_here_list.append(index_here) cell_here = neigh_new[3] + new_coords = [ site_here.frac_coords[0] + float(cell_here[0]), site_here.frac_coords[1] + float(cell_here[1]), @@ -967,38 +972,40 @@ def _find_environments( ] coords.append(site_here.lattice.get_cartesian_coords(new_coords)) - # new_site = PeriodicSite( - # species=site_here.species_string, - # coords=site_here.lattice.get_cartesian_coords(new_coords), - # lattice=site_here.lattice, - # to_unit_cell=False, - # coords_are_cartesian=True, - # ) neighbors_by_distance.append(neigh_new[0]) list_distances.append(neigh_new[1]) + translations_by_distance.append(cell_here) _list_neighsite = [] _list_neighisite = [] copied_neighbors_from_ICOHPs = copy.copy(neighbors_from_ICOHPs) copied_distances_from_ICOHPs = copy.copy(lengths_from_ICOHPs) + copied_translations_from_ICOHPs = copy.copy(translations_ICOHPs) _neigh_coords = [] _neigh_frac_coords = [] for neigh_idx, neigh in enumerate(neighbors_by_distance): index_here2 = index_here_list[neigh_idx] - + print(index_here2) for dist_idx, dist in enumerate(copied_distances_from_ICOHPs): if ( np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 + and ((copied_translations_from_ICOHPs[dist_idx][0] == -translations_by_distance[neigh_idx][0] and copied_translations_from_ICOHPs[dist_idx][1] == -translations_by_distance[neigh_idx][1] and copied_translations_from_ICOHPs[dist_idx][2] == -translations_by_distance[neigh_idx][2]) or (copied_translations_from_ICOHPs[dist_idx][0] == translations_by_distance[neigh_idx][0] and copied_translations_from_ICOHPs[dist_idx][1] == translations_by_distance[neigh_idx][1] and copied_translations_from_ICOHPs[dist_idx][2] == translations_by_distance[neigh_idx][2])) ): _list_neighsite.append(neigh) _list_neighisite.append(index_here2) _neigh_coords.append(coords[neigh_idx]) _neigh_frac_coords.append(neigh.frac_coords) + print("test") + print(copied_translations_from_ICOHPs[dist_idx]) + print(translations_by_distance[neigh_idx]) + print(cell_start) del copied_distances_from_ICOHPs[dist_idx] del copied_neighbors_from_ICOHPs[dist_idx] + del copied_translations_from_ICOHPs[dist_idx] break + print(_list_neighsite) list_neighisite.append(_list_neighisite) list_neighsite.append(_list_neighsite) list_lengths.append(lengths_from_ICOHPs) @@ -1042,6 +1049,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs: list[float] = [] neighbors_from_ICOHPs: list[int] = [] icohps_from_ICOHPs: list[IcohpValue] = [] + translation_from_ICOHPs: list[list[int,int,int]]=[] for key, icohp in icohps.items(): atomnr1 = self._get_atomnumber(icohp._atom1) @@ -1062,11 +1070,14 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + # add translation to icohp value + translation_from_ICOHPs.append(icohp.translation) elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) # ONLY_ANION_CATION_BONDS elif additional_condition == 1: @@ -1076,12 +1087,14 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) # NO_ELEMENT_TO_SAME_ELEMENT_BONDS elif additional_condition == 2: @@ -1091,12 +1104,14 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS elif additional_condition == 3: @@ -1108,12 +1123,14 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) # ONLY_ELEMENT_TO_OXYGEN_BONDS elif additional_condition == 4: @@ -1123,12 +1140,14 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) # DO_NOT_CONSIDER_ANION_CATION_BONDS elif additional_condition == 5: @@ -1138,12 +1157,14 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) # ONLY_CATION_CATION_BONDS elif additional_condition == 6 and val1 > 0.0 and val2 > 0.0: # type: ignore[operator] @@ -1152,18 +1173,21 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) elif atomnr2 == site_idx: neighbors_from_ICOHPs.append(atomnr1) lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) + translation_from_ICOHPs.append(icohp.translation) return ( keys_from_ICOHPs, lengths_from_ICOHPs, neighbors_from_ICOHPs, icohps_from_ICOHPs, + translation_from_ICOHPs ) @staticmethod From 4034ca1e2f45a84d08c076bf392b2d28049e9679 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Wed, 30 Oct 2024 08:58:38 +0100 Subject: [PATCH 087/180] partially test translations in lobsterenv --- cohp.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cohp.py b/cohp.py index cb82939c4f..78c5ebbe6b 100644 --- a/cohp.py +++ b/cohp.py @@ -1042,6 +1042,10 @@ def is_spin_polarized(self) -> bool: """ return self._is_spin_polarized + @property + def translation(self)->list[int,int,int]: + return self._translation + def icohpvalue(self, spin: Spin = Spin.up) -> float: """ Args: From 3fdb3ecf937792a0bf653509c8455072ab740753 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 08:00:52 +0000 Subject: [PATCH 088/180] pre-commit auto-fixes --- lobsterenv.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index c485c0e35f..19393cf18c 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -790,14 +790,9 @@ def _evaluate_ce( raise ValueError("Please give two limits or leave them both at None") # Find environments based on ICOHP values - ( - list_icohps, - list_keys, - list_lengths, - list_neighisite, - list_neighsite, - list_coords - ) = self._find_environments(additional_condition, lowerlimit, upperlimit, only_bonds_to) + (list_icohps, list_keys, list_lengths, list_neighisite, list_neighsite, list_coords) = self._find_environments( + additional_condition, lowerlimit, upperlimit, only_bonds_to + ) self.list_icohps = list_icohps self.list_lengths = list_lengths @@ -944,8 +939,8 @@ def _find_environments( if len(neighbors_from_ICOHPs) > 0: centralsite = site - copysite=copy.copy(centralsite) - cell_start=centralsite.frac_coords-copysite.to_unit_cell().frac_coords + copysite = copy.copy(centralsite) + cell_start = centralsite.frac_coords - copysite.to_unit_cell().frac_coords print(cell_start) neighbors_by_distance_start = self.structure.get_sites_in_sphere( pt=centralsite.coords, @@ -958,7 +953,7 @@ def _find_environments( list_distances = [] index_here_list = [] coords = [] - translations_by_distance=[] + translations_by_distance = [] for neigh_new in sorted(neighbors_by_distance_start, key=lambda x: x[1]): site_here = neigh_new[0].to_unit_cell() index_here = neigh_new[2] @@ -990,7 +985,24 @@ def _find_environments( if ( np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 - and ((copied_translations_from_ICOHPs[dist_idx][0] == -translations_by_distance[neigh_idx][0] and copied_translations_from_ICOHPs[dist_idx][1] == -translations_by_distance[neigh_idx][1] and copied_translations_from_ICOHPs[dist_idx][2] == -translations_by_distance[neigh_idx][2]) or (copied_translations_from_ICOHPs[dist_idx][0] == translations_by_distance[neigh_idx][0] and copied_translations_from_ICOHPs[dist_idx][1] == translations_by_distance[neigh_idx][1] and copied_translations_from_ICOHPs[dist_idx][2] == translations_by_distance[neigh_idx][2])) + and ( + ( + copied_translations_from_ICOHPs[dist_idx][0] + == -translations_by_distance[neigh_idx][0] + and copied_translations_from_ICOHPs[dist_idx][1] + == -translations_by_distance[neigh_idx][1] + and copied_translations_from_ICOHPs[dist_idx][2] + == -translations_by_distance[neigh_idx][2] + ) + or ( + copied_translations_from_ICOHPs[dist_idx][0] + == translations_by_distance[neigh_idx][0] + and copied_translations_from_ICOHPs[dist_idx][1] + == translations_by_distance[neigh_idx][1] + and copied_translations_from_ICOHPs[dist_idx][2] + == translations_by_distance[neigh_idx][2] + ) + ) ): _list_neighsite.append(neigh) _list_neighisite.append(index_here2) @@ -1049,7 +1061,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs: list[float] = [] neighbors_from_ICOHPs: list[int] = [] icohps_from_ICOHPs: list[IcohpValue] = [] - translation_from_ICOHPs: list[list[int,int,int]]=[] + translation_from_ICOHPs: list[list[int, int, int]] = [] for key, icohp in icohps.items(): atomnr1 = self._get_atomnumber(icohp._atom1) @@ -1187,7 +1199,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs, neighbors_from_ICOHPs, icohps_from_ICOHPs, - translation_from_ICOHPs + translation_from_ICOHPs, ) @staticmethod From 5643523f2907bf4351827ff5c9206ce46e688068 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 08:00:52 +0000 Subject: [PATCH 089/180] pre-commit auto-fixes --- cohp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cohp.py b/cohp.py index 78c5ebbe6b..1ef1ee418e 100644 --- a/cohp.py +++ b/cohp.py @@ -1043,7 +1043,7 @@ def is_spin_polarized(self) -> bool: return self._is_spin_polarized @property - def translation(self)->list[int,int,int]: + def translation(self) -> list[int, int, int]: return self._translation def icohpvalue(self, spin: Spin = Spin.up) -> float: From 5a490ac3ba6cc218ed9d13326a84777aa7518705 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Wed, 13 Nov 2024 02:02:05 +0800 Subject: [PATCH 090/180] Show `FutureWarning` and `DeprecationWarning` for `pytest` (#4138) * show FutureWarning and DeprecationWarning * TODO: mark FutureWarning and DeprecationWarning for this PR * from pymatgen.core.interface import GrainBoundaryGenerator * Suppress DeprecationWarnings on pkg_resources from pybtex * suppress all warning from pybtex * use single quote * ignore Userwarning * bump plotly to avoid SyntaxWarning * DeprecationWarning: Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. * suppress spglib.get_symmetry * fix get_vasp_input * add comment * replace piezo sensitivity * suppress warning * suppress known bader warning * add comment for something Im unable to fix * fix lammps input * n_elems * simplify warning filter in test * fix get_vasp_input * avoid warning leakage * fix lobster * replace phonopy * revert filter warn for that we might want to fix * fix pandas na filter * enhance filter msg * NEED CONFIRM: replace plotly colorbar * suppress intended test for MaterialsProjectCompatibility * fix pd.read_json * replace matrix with array * suppress too many figure warning * fix openff * clean up warning filter * add comment * filter intended usage * clean up warn filter * revert plotly bump * enhance comment * fix extractall * remove TODO tag as issue opened * copy central_diff_weights from scipy * copy derivative for quasiharmonic * fix extractall --- lobsterenv.py | 8 ++++---- outputs.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index dd8a53badf..be5622e92c 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -190,16 +190,16 @@ def __init__( if valences_from_charges and filename_charge is not None: chg = Charge(filename=filename_charge) if which_charge == "Mulliken": - self.valences = chg.Mulliken + self.valences = chg.mulliken elif which_charge == "Loewdin": - self.valences = chg.Loewdin + self.valences = chg.loewdin elif valences_from_charges and obj_charge is not None: chg = obj_charge if which_charge == "Mulliken": - self.valences = chg.Mulliken + self.valences = chg.mulliken elif which_charge == "Loewdin": - self.valences = chg.Loewdin + self.valences = chg.loewdin else: bv_analyzer = BVAnalyzer() diff --git a/outputs.py b/outputs.py index 96528279df..d9d9276d3b 100644 --- a/outputs.py +++ b/outputs.py @@ -1677,7 +1677,7 @@ def _read(self, lines: list[str], spin_numbers: list[int]) -> None: _lines.append(float(el)) overlaps.append(_lines) if len(overlaps) == len(_lines): - self.band_overlaps_dict[spin]["matrices"].append(np.matrix(overlaps)) + self.band_overlaps_dict[spin]["matrices"].append(np.array(overlaps)) def has_good_quality_maxDeviation(self, limit_maxDeviation: float = 0.1) -> bool: """Check if the maxDeviation from the ideal bandoverlap is smaller From 7c62138e270cb453cd41bb9a72e25e3cd8a02102 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Wed, 13 Nov 2024 02:02:05 +0800 Subject: [PATCH 091/180] Show `FutureWarning` and `DeprecationWarning` for `pytest` (#4138) * show FutureWarning and DeprecationWarning * TODO: mark FutureWarning and DeprecationWarning for this PR * from pymatgen.core.interface import GrainBoundaryGenerator * Suppress DeprecationWarnings on pkg_resources from pybtex * suppress all warning from pybtex * use single quote * ignore Userwarning * bump plotly to avoid SyntaxWarning * DeprecationWarning: Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead. * suppress spglib.get_symmetry * fix get_vasp_input * add comment * replace piezo sensitivity * suppress warning * suppress known bader warning * add comment for something Im unable to fix * fix lammps input * n_elems * simplify warning filter in test * fix get_vasp_input * avoid warning leakage * fix lobster * replace phonopy * revert filter warn for that we might want to fix * fix pandas na filter * enhance filter msg * NEED CONFIRM: replace plotly colorbar * suppress intended test for MaterialsProjectCompatibility * fix pd.read_json * replace matrix with array * suppress too many figure warning * fix openff * clean up warning filter * add comment * filter intended usage * clean up warn filter * revert plotly bump * enhance comment * fix extractall * remove TODO tag as issue opened * copy central_diff_weights from scipy * copy derivative for quasiharmonic * fix extractall --- tests/io/lobster/test_lobsterenv.py | 1 + tests/io/lobster/test_outputs.py | 30 ++++++++++++++--------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 2d4700a7b8..4f9cc9959b 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -337,6 +337,7 @@ def test_set_limits(self): def test_molecules_allowed(self): assert not self.chem_env_lobster1.molecules_allowed + @pytest.mark.filterwarnings("ignore:get_anion_types is deprecated") def test_get_anion_types(self): assert self.chem_env_lobster0_second.get_anion_types() == {Element("O")} assert self.chem_env_lobster0_second.anion_types == {Element("O")} diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index 86a7e589c2..30ea62e687 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -649,8 +649,8 @@ def test_attributes(self): atomlist = ["O1", "Mn2"] types = ["O", "Mn"] num_atoms = 2 - assert charge_Mulliken == self.charge2.Mulliken - assert charge_Loewdin == self.charge2.Loewdin + assert charge_Mulliken == self.charge2.mulliken + assert charge_Loewdin == self.charge2.loewdin assert atomlist == self.charge2.atomlist assert types == self.charge2.types assert num_atoms == self.charge2.num_atoms @@ -1490,19 +1490,19 @@ def setUp(self): self.band_overlaps2_new = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.new.2") def test_attributes(self): - # bandoverlapsdict - bo_dict = self.band_overlaps1.bandoverlapsdict + # band_overlaps_dict + bo_dict = self.band_overlaps1.band_overlaps_dict assert bo_dict[Spin.up]["max_deviations"][0] == approx(0.000278953) - assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["max_deviations"][10] == approx(0.0640933) + assert self.band_overlaps1_new.band_overlaps_dict[Spin.up]["max_deviations"][10] == approx(0.0640933) assert bo_dict[Spin.up]["matrices"][0].item(-1, -1) == approx(0.0188058) - assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["matrices"][10].item(-1, -1) == approx(1.0) + assert self.band_overlaps1_new.band_overlaps_dict[Spin.up]["matrices"][10].item(-1, -1) == approx(1.0) assert bo_dict[Spin.up]["matrices"][0].item(0, 0) == approx(1) - assert self.band_overlaps1_new.bandoverlapsdict[Spin.up]["matrices"][10].item(0, 0) == approx(0.995849) + assert self.band_overlaps1_new.band_overlaps_dict[Spin.up]["matrices"][10].item(0, 0) == approx(0.995849) assert bo_dict[Spin.down]["max_deviations"][-1] == approx(4.31567e-05) - assert self.band_overlaps1_new.bandoverlapsdict[Spin.down]["max_deviations"][9] == approx(0.064369) + assert self.band_overlaps1_new.band_overlaps_dict[Spin.down]["max_deviations"][9] == approx(0.064369) assert bo_dict[Spin.down]["matrices"][-1].item(0, -1) == approx(4.0066e-07) - assert self.band_overlaps1_new.bandoverlapsdict[Spin.down]["matrices"][9].item(0, -1) == approx(1.37447e-09) + assert self.band_overlaps1_new.band_overlaps_dict[Spin.down]["matrices"][9].item(0, -1) == approx(1.37447e-09) # maxDeviation assert self.band_overlaps1.max_deviation[0] == approx(0.000278953) @@ -2202,22 +2202,22 @@ def setUp(self) -> None: self.sitepotential = SitePotential(filename=f"{TEST_DIR}/SitePotentials.lobster.perovskite") def test_attributes(self): - assert self.sitepotential.sitepotentials_Loewdin == [ + assert self.sitepotential.sitepotentials_loewdin == [ -8.77, -17.08, 9.57, 9.57, 8.45, ] - assert self.sitepotential.sitepotentials_Mulliken == [ + assert self.sitepotential.sitepotentials_mulliken == [ -11.38, -19.62, 11.18, 11.18, 10.09, ] - assert self.sitepotential.madelungenergies_Loewdin == approx(-28.64) - assert self.sitepotential.madelungenergies_Mulliken == approx(-40.02) + assert self.sitepotential.madelungenergies_loewdin == approx(-28.64) + assert self.sitepotential.madelungenergies_mulliken == approx(-40.02) assert self.sitepotential.atomlist == ["La1", "Ta2", "N3", "N4", "O5"] assert self.sitepotential.types == ["La", "Ta", "N", "N", "O"] assert self.sitepotential.num_atoms == 5 @@ -2253,8 +2253,8 @@ def setUp(self) -> None: self.madelungenergies = MadelungEnergies(filename=f"{TEST_DIR}/MadelungEnergies.lobster.perovskite") def test_attributes(self): - assert self.madelungenergies.madelungenergies_Loewdin == approx(-28.64) - assert self.madelungenergies.madelungenergies_Mulliken == approx(-40.02) + assert self.madelungenergies.madelungenergies_loewdin == approx(-28.64) + assert self.madelungenergies.madelungenergies_mulliken == approx(-40.02) assert self.madelungenergies.ewald_splitting == approx(3.14) def test_msonable(self): From 71872bce38529b6cf099faecc9dc8b2cae7d9b2f Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Thu, 14 Nov 2024 01:45:49 +0800 Subject: [PATCH 092/180] Replace hard-coded `np.allclose/isclose` and `math.isclose` (for complex expression) (#4164) * replace hard-coded math.isclose * add todo tag * replace more isclose in code * remove TODO tag * replace more in code * fix seemingly wrong quote position * avoid import when it's used only once or very few * replace last batch * revert change to isotropic check * replace some numpy isclose allclose * remove debug tag * remove some hard coded np allclose * revert some change on very simple evals * avoid unnecessary compare with zero * revert simple comparison * avoid minus zero * revert some simple expressions * simplify sci notation * revert simple comparisons * avoid 1.0e-x as it's already float * revert simple compare * use sci not * revert simple * use abs as we don't need always float * fix round usage * all close * revert as i'm not sure about the shape broadcasting * avoid import from numpy * clean up math import, reduce namespace cluster * simplify all close * sci notation * simplify import of math * simplify assert all close * simplify int(len(a) / b) to len(a) // b --- lobsterenv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index be5622e92c..81b600452e 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -820,7 +820,7 @@ def _evaluate_ce( { "site": neighbor, "image": tuple( - int(round(idx)) + round(idx) for idx in ( neighbor.frac_coords - self.structure[ @@ -861,7 +861,7 @@ def _evaluate_ce( { "site": neighbor, "image": tuple( - int(round(idx)) + round(idx) for idx in ( neighbor.frac_coords - self.structure[ From f5e4992e9765cbac7f6e580cdfede8f27732c3f8 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Sat, 16 Nov 2024 21:37:02 +0800 Subject: [PATCH 093/180] Cleanup `codespell` ignore patterns (#4175) --- outputs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/outputs.py b/outputs.py index d9d9276d3b..bc5f8e1780 100644 --- a/outputs.py +++ b/outputs.py @@ -768,9 +768,9 @@ def _parse_doscar(self): cdos = np.zeros((ndos, len(line))) cdos[0] = np.array(line) - for nd in range(1, ndos): + for idx_dos in range(1, ndos): line_parts = file.readline().split() - cdos[nd] = np.array(line_parts) + cdos[idx_dos] = np.array(line_parts) dos.append(cdos) line = file.readline() # Read the next line to continue the loop From c5696f477076317c0625592225192ee52a2af98c Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:32:58 +0100 Subject: [PATCH 094/180] Fix incorrect comparison logic and update tests (#4181) * fix erroneous comp logic and simply complexcity based on suggestions * fix tests that correctly evaluate the checks * pre-commit auto-fixes * seperate has_good_quality_check_occupied_bands tests * address review comment * pre-commit auto-fixes * address review comment > change numeric values to scientific notation * pre-commit auto-fixes * add static method to get sub_array (helps in testing) * update tests * remove duplicate accidental test lines * remove duplicate assert * update test as per review suggestion * address review comments * address review comments2 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- outputs.py | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/outputs.py b/outputs.py index bc5f8e1780..300e2f68c9 100644 --- a/outputs.py +++ b/outputs.py @@ -1710,29 +1710,17 @@ def has_good_quality_check_occupied_bands( Returns: bool: True if the quality of the projection is good. """ - for matrix in self.band_overlaps_dict[Spin.up]["matrices"]: - for iband1, band1 in enumerate(matrix): - for iband2, band2 in enumerate(band1): - if iband1 < number_occ_bands_spin_up and iband2 < number_occ_bands_spin_up: - if iband1 == iband2: - if abs(band2 - 1.0).all() > limit_deviation: - return False - elif band2.all() > limit_deviation: - return False - - if spin_polarized: - for matrix in self.band_overlaps_dict[Spin.down]["matrices"]: - for iband1, band1 in enumerate(matrix): - for iband2, band2 in enumerate(band1): - if number_occ_bands_spin_down is None: - raise ValueError("number_occ_bands_spin_down has to be specified") - - if iband1 < number_occ_bands_spin_down and iband2 < number_occ_bands_spin_down: - if iband1 == iband2: - if abs(band2 - 1.0).all() > limit_deviation: - return False - elif band2.all() > limit_deviation: - return False + if spin_polarized and number_occ_bands_spin_down is None: + raise ValueError("number_occ_bands_spin_down has to be specified") + + for spin in (Spin.up, Spin.down) if spin_polarized else (Spin.up,): + num_occ_bands = number_occ_bands_spin_up if spin is Spin.up else number_occ_bands_spin_down + + for overlap_matrix in self.band_overlaps_dict[spin]["matrices"]: + sub_array = np.asarray(overlap_matrix)[:num_occ_bands, :num_occ_bands] + + if not np.allclose(sub_array, np.identity(num_occ_bands), atol=limit_deviation, rtol=0): + return False return True From 9bf37019368895a1b6922f3691b8db5b99698c8a Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:32:58 +0100 Subject: [PATCH 095/180] Fix incorrect comparison logic and update tests (#4181) * fix erroneous comp logic and simply complexcity based on suggestions * fix tests that correctly evaluate the checks * pre-commit auto-fixes * seperate has_good_quality_check_occupied_bands tests * address review comment * pre-commit auto-fixes * address review comment > change numeric values to scientific notation * pre-commit auto-fixes * add static method to get sub_array (helps in testing) * update tests * remove duplicate accidental test lines * remove duplicate assert * update test as per review suggestion * address review comments * address review comments2 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- tests/io/lobster/test_outputs.py | 117 +++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 21 deletions(-) diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index 30ea62e687..4dac8c4a01 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy import json import os from unittest import TestCase @@ -1481,7 +1482,7 @@ def test_get_bandstructure(self): class TestBandoverlaps(TestCase): def setUp(self): - # test spin-polarized calc and non spinpolarized calc + # test spin-polarized calc and non spin-polarized calc self.band_overlaps1 = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.1") self.band_overlaps2 = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.2") @@ -1515,9 +1516,18 @@ def test_attributes(self): assert self.band_overlaps2.max_deviation[-1] == approx(1.48451e-05) assert self.band_overlaps2_new.max_deviation[-1] == approx(0.45154) - def test_has_good_quality(self): + def test_has_good_quality_maxDeviation(self): assert not self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=0.1) assert not self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=0.1) + + assert self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=100) + assert self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=100) + assert self.band_overlaps2.has_good_quality_maxDeviation() + assert not self.band_overlaps2_new.has_good_quality_maxDeviation() + assert not self.band_overlaps2.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) + assert not self.band_overlaps2_new.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) + + def test_has_good_quality_check_occupied_bands(self): assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=9, number_occ_bands_spin_down=5, @@ -1545,65 +1555,58 @@ def test_has_good_quality(self): assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=1, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=1, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=0, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=0, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=0, number_occ_bands_spin_down=1, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=0, number_occ_bands_spin_down=1, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=4, number_occ_bands_spin_down=4, - limit_deviation=0.001, + limit_deviation=1e-3, spin_polarized=True, ) assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=4, number_occ_bands_spin_down=4, - limit_deviation=0.001, + limit_deviation=1e-3, spin_polarized=True, ) - - assert self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=100) - assert self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=100) - assert self.band_overlaps2.has_good_quality_maxDeviation() - assert not self.band_overlaps2_new.has_good_quality_maxDeviation() - assert not self.band_overlaps2.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) - assert not self.band_overlaps2_new.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) assert not self.band_overlaps2.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=10, limit_deviation=0.0000001 + number_occ_bands_spin_up=10, limit_deviation=1e-7 ) assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=10, limit_deviation=0.0000001 + number_occ_bands_spin_up=10, limit_deviation=1e-7 ) - assert not self.band_overlaps2.has_good_quality_check_occupied_bands( + assert self.band_overlaps2.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, limit_deviation=0.1 ) @@ -1614,7 +1617,7 @@ def test_has_good_quality(self): number_occ_bands_spin_up=1, limit_deviation=1e-8 ) assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=10, limit_deviation=1) - assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( + assert self.band_overlaps2_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=2, limit_deviation=0.1 ) assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=1, limit_deviation=1) @@ -1622,6 +1625,78 @@ def test_has_good_quality(self): number_occ_bands_spin_up=1, limit_deviation=2 ) + def test_has_good_quality_check_occupied_bands_patched(self): + """Test with patched data.""" + + limit_deviation = 0.1 + + rng = np.random.default_rng(42) # set seed for reproducibility + + band_overlaps = copy.deepcopy(self.band_overlaps1_new) + + number_occ_bands_spin_up_all = list(range(band_overlaps.band_overlaps_dict[Spin.up]["matrices"][0].shape[0])) + number_occ_bands_spin_down_all = list( + range(band_overlaps.band_overlaps_dict[Spin.down]["matrices"][0].shape[0]) + ) + + for actual_deviation in [0.05, 0.1, 0.2, 0.5, 1.0]: + for spin in (Spin.up, Spin.down): + for number_occ_bands_spin_up, number_occ_bands_spin_down in zip( + number_occ_bands_spin_up_all, number_occ_bands_spin_down_all, strict=False + ): + for i_arr, array in enumerate(band_overlaps.band_overlaps_dict[spin]["matrices"]): + number_occ_bands = number_occ_bands_spin_up if spin is Spin.up else number_occ_bands_spin_down + + shape = array.shape + assert np.all(np.array(shape) >= number_occ_bands) + assert len(shape) == 2 + assert shape[0] == shape[1] + + # Generate a noisy background array + patch_array = rng.uniform(0, 10, shape) + + # Patch the top-left sub-array (the part that would be checked) + patch_array[:number_occ_bands, :number_occ_bands] = np.identity(number_occ_bands) + rng.uniform( + 0, actual_deviation, (number_occ_bands, number_occ_bands) + ) + + band_overlaps.band_overlaps_dict[spin]["matrices"][i_arr] = patch_array + + result = band_overlaps.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=number_occ_bands_spin_up, + number_occ_bands_spin_down=number_occ_bands_spin_down, + spin_polarized=True, + limit_deviation=limit_deviation, + ) + # Assert for expected results + if ( + actual_deviation == 0.05 + and number_occ_bands_spin_up <= 7 + and number_occ_bands_spin_down <= 7 + and spin is Spin.up + or actual_deviation == 0.05 + and spin is Spin.down + or actual_deviation == 0.1 + or actual_deviation in [0.2, 0.5, 1.0] + and number_occ_bands_spin_up == 0 + and number_occ_bands_spin_down == 0 + ): + assert result + else: + assert not result + + def test_exceptions(self): + with pytest.raises(ValueError, match="number_occ_bands_spin_down has to be specified"): + self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + spin_polarized=True, + ) + with pytest.raises(ValueError, match="number_occ_bands_spin_down has to be specified"): + self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + spin_polarized=True, + ) + def test_msonable(self): dict_data = self.band_overlaps2_new.as_dict() bandoverlaps_from_dict = Bandoverlaps.from_dict(dict_data) From 2e913c75e6e980039164f3b7a8953c6bc63c58df Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Wed, 11 Dec 2024 09:51:58 +0800 Subject: [PATCH 096/180] `zopen`: explicit binary/text `mode` , and explicit `encoding` as UTF-8 (#4219) * explicit mode for zopen * fix bad mode for cifwrite * remove tag * remove an unnecessary cast to int * TODO: test monty zopen change * fix implicit mode * Revert "TODO: test monty zopen change" This reverts commit c9bf970928e7e1d235a835e3cb4ad45a149ce010. * Revert "Revert "TODO: test monty zopen change"" This reverts commit be411a0f9d13bb3ebf77ecd5997ce5b21af81910. * Revert "Revert "Revert "TODO: test monty zopen change""" This reverts commit e75c196d47c614022e534741178eabd1316f9efe. * explicit text mode for stout * some explicit utf-8 for zopen * TO BE REVERTED: test monty pr * add the rest encoding * Revert "TO BE REVERTED: test monty pr" This reverts commit d899c87e60e58b5719f1123a60f992a9428d0e51. --- inputs.py | 4 ++-- outputs.py | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/inputs.py b/inputs.py index a4f7902e73..3bbe0518a1 100644 --- a/inputs.py +++ b/inputs.py @@ -585,7 +585,7 @@ def from_file(cls, lobsterin: PathLike) -> Self: Returns: Lobsterin object """ - with zopen(lobsterin, mode="rt") as file: + with zopen(lobsterin, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") if not lines: raise RuntimeError("lobsterin file contains no data.") @@ -642,7 +642,7 @@ def _get_potcar_symbols(POTCAR_input: PathLike) -> list[str]: raise ValueError("Lobster only works with PAW! Use different POTCARs") # Warning about a bug in LOBSTER-4.1.0 - with zopen(POTCAR_input, mode="r") as file: + with zopen(POTCAR_input, mode="rt", encoding="utf-8") as file: data = file.read() if isinstance(data, bytes): diff --git a/outputs.py b/outputs.py index 300e2f68c9..78946c1fc5 100644 --- a/outputs.py +++ b/outputs.py @@ -121,7 +121,7 @@ def __init__( else: self._filename = "COHPCAR.lobster" - with zopen(self._filename, mode="rt") as file: + with zopen(self._filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") # The parameters line is the second line in a COHPCAR file. @@ -405,7 +405,7 @@ def __init__( # LOBSTER list files have an extra trailing blank line # and we don't need the header. if self._icohpcollection is None: - with zopen(self._filename, mode="rt") as file: + with zopen(self._filename, mode="rt", encoding="utf-8") as file: all_lines = file.read().split("\n") lines = all_lines[1:-1] if "spin" not in all_lines[1] else all_lines[2:-1] if len(lines) == 0: @@ -622,7 +622,7 @@ def __init__(self, filename: PathLike | None = "NcICOBILIST.lobster") -> None: # LOBSTER list files have an extra trailing blank line # and we don't need the header - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n")[1:-1] if len(lines) == 0: raise RuntimeError("NcICOBILIST file contains no data.") @@ -753,7 +753,7 @@ def _parse_doscar(self): tdensities = {} itdensities = {} - with zopen(doscar, mode="rt") as file: + with zopen(doscar, mode="rt", encoding="utf-8") as file: file.readline() # Skip the first line efermi = float([file.readline() for nn in range(4)][3].split()[17]) dos = [] @@ -912,7 +912,7 @@ def __init__( self.loewdin = [] if loewdin is None else loewdin if self.num_atoms is None: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n")[3:-3] if len(lines) == 0: raise RuntimeError("CHARGES file contains no data.") @@ -1046,7 +1046,7 @@ def __init__(self, filename: PathLike | None, **kwargs) -> None: else: raise ValueError(f"{attr}={val} is not a valid attribute for Lobsterout") elif filename: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") if len(lines) == 0: raise RuntimeError("lobsterout does not contain any data") @@ -1438,7 +1438,7 @@ def __init__( raise ValueError("No FATBAND files in folder or given") for fname in filenames: - with zopen(fname, mode="rt") as file: + with zopen(fname, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") atom_names.append(os.path.split(fname)[1].split("_")[1].capitalize()) @@ -1472,7 +1472,7 @@ def __init__( eigenvals: dict = {} p_eigenvals: dict = {} for ifilename, filename in enumerate(filenames): - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") if ifilename == 0: @@ -1620,7 +1620,7 @@ def __init__( self.max_deviation = [] if max_deviation is None else max_deviation if not self.band_overlaps_dict: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") spin_numbers = [0, 1] if lines[0].split()[-1] == "0" else [1, 2] @@ -1760,7 +1760,7 @@ def __init__( self.is_lcfo = is_lcfo self.list_dict_grosspop = [] if list_dict_grosspop is None else list_dict_grosspop if not self.list_dict_grosspop: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") # Read file to list of dict @@ -1890,7 +1890,7 @@ def _parse_file( imaginary (list[float]): Imaginary parts of wave function. distance (list[float]): Distances to the first point in wave function file. """ - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") points = [] @@ -2060,7 +2060,7 @@ def __init__( self.madelungenergies_mulliken = None if madelungenergies_mulliken is None else madelungenergies_mulliken if self.ewald_splitting is None: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n")[5] if len(lines) == 0: raise RuntimeError("MadelungEnergies file contains no data.") @@ -2131,7 +2131,7 @@ def __init__( self.madelungenergies_mulliken: list | float = madelungenergies_mulliken or [] if self.num_atoms is None: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") if len(lines) == 0: raise RuntimeError("SitePotentials file contains no data.") @@ -2284,7 +2284,7 @@ def __init__( """ self._filename = str(filename) - with zopen(self._filename, mode="rt") as file: + with zopen(self._filename, mode="rt", encoding="utf-8") as file: lines = file.readlines() if len(lines) == 0: raise RuntimeError("Please check provided input file, it seems to be empty") @@ -2472,7 +2472,7 @@ def __init__( self.bin_width = 0.0 if bin_width is None else bin_width if not self.bwdf: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") if len(lines) == 0: raise RuntimeError("BWDF file contains no data.") From 537746e7807b8773bfe7785d4c7e8dcc78ac3a16 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:52:16 -0800 Subject: [PATCH 097/180] pre-commit autoupdate (#4213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.7.2 → v0.8.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.2...v0.8.1) - [github.com/MarcoGorelli/cython-lint: v0.16.2 → v0.16.6](https://github.com/MarcoGorelli/cython-lint/compare/v0.16.2...v0.16.6) - [github.com/igorshubovych/markdownlint-cli: v0.42.0 → v0.43.0](https://github.com/igorshubovych/markdownlint-cli/compare/v0.42.0...v0.43.0) - [github.com/kynan/nbstripout: 0.8.0 → 0.8.1](https://github.com/kynan/nbstripout/compare/0.8.0...0.8.1) - [github.com/RobertCraigie/pyright-python: v1.1.387 → v1.1.389](https://github.com/RobertCraigie/pyright-python/compare/v1.1.387...v1.1.389) * pre-commit auto-fixes * Convert to f-syntax to satisfy ruff Signed-off-by: Matthew Horton --------- Signed-off-by: Matthew Horton Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Matthew Horton --- outputs.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/outputs.py b/outputs.py index 78946c1fc5..421816c5bd 100644 --- a/outputs.py +++ b/outputs.py @@ -444,11 +444,8 @@ def __init__( for line in lines: if ( ("_" not in line.split()[1] and version != "5.1.0") - or "_" not in line.split()[1] - and version == "5.1.0" - or (line.split()[1].count("_") == 1) - and version == "5.1.0" - and self.is_lcfo + or ("_" not in line.split()[1] and version == "5.1.0") + or ((line.split()[1].count("_") == 1) and version == "5.1.0" and self.is_lcfo) ): data_without_orbitals.append(line) elif line.split()[1].count("_") >= 2 and version == "5.1.0": From 856bf576ab825029f6eec39df355fb686ced5509 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:52:16 -0800 Subject: [PATCH 098/180] pre-commit autoupdate (#4213) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.7.2 → v0.8.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.7.2...v0.8.1) - [github.com/MarcoGorelli/cython-lint: v0.16.2 → v0.16.6](https://github.com/MarcoGorelli/cython-lint/compare/v0.16.2...v0.16.6) - [github.com/igorshubovych/markdownlint-cli: v0.42.0 → v0.43.0](https://github.com/igorshubovych/markdownlint-cli/compare/v0.42.0...v0.43.0) - [github.com/kynan/nbstripout: 0.8.0 → 0.8.1](https://github.com/kynan/nbstripout/compare/0.8.0...0.8.1) - [github.com/RobertCraigie/pyright-python: v1.1.387 → v1.1.389](https://github.com/RobertCraigie/pyright-python/compare/v1.1.387...v1.1.389) * pre-commit auto-fixes * Convert to f-syntax to satisfy ruff Signed-off-by: Matthew Horton --------- Signed-off-by: Matthew Horton Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Matthew Horton --- tests/io/lobster/test_outputs.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index 4dac8c4a01..f46f69b8af 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -1670,16 +1670,19 @@ def test_has_good_quality_check_occupied_bands_patched(self): ) # Assert for expected results if ( - actual_deviation == 0.05 - and number_occ_bands_spin_up <= 7 - and number_occ_bands_spin_down <= 7 - and spin is Spin.up - or actual_deviation == 0.05 - and spin is Spin.down + ( + actual_deviation == 0.05 + and number_occ_bands_spin_up <= 7 + and number_occ_bands_spin_down <= 7 + and spin is Spin.up + ) + or (actual_deviation == 0.05 and spin is Spin.down) or actual_deviation == 0.1 - or actual_deviation in [0.2, 0.5, 1.0] - and number_occ_bands_spin_up == 0 - and number_occ_bands_spin_down == 0 + or ( + actual_deviation in [0.2, 0.5, 1.0] + and number_occ_bands_spin_up == 0 + and number_occ_bands_spin_down == 0 + ) ): assert result else: From bda120ce7b8793ca2820a84522fecd74bf2c1c5a Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Wed, 11 Dec 2024 09:52:44 +0800 Subject: [PATCH 099/180] Increase warnings `stacklevel` to 2, un-ignore RUFF `B028` (#4217) * increase warning stacklevel * unignore b028 ruff rule * fix some * fix more in apps, command_line and ext * remove unnecessary explicit UserWarning * replace explicit stacklevel 1 * fix io * fix entries * fix electronic_structure * avoid manual deprecation warning * fix core * fix analysis * futurewarning for legacy mprester --- inputs.py | 11 ++++++++--- outputs.py | 18 ++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/inputs.py b/inputs.py index 3bbe0518a1..c83b57cfe4 100644 --- a/inputs.py +++ b/inputs.py @@ -337,7 +337,10 @@ def write_INCAR( """ # Read INCAR from file, which will be modified incar = Incar.from_file(incar_input) - warnings.warn("Please check your incar_input before using it. This method only changes three settings!") + warnings.warn( + "Please check your incar_input before using it. This method only changes three settings!", + stacklevel=2, + ) if isym in {-1, 0}: incar["ISYM"] = isym else: @@ -654,7 +657,8 @@ def _get_potcar_symbols(POTCAR_input: PathLike) -> list[str]: "Lobster up to version 4.1.0." "\n The keywords SHA256 and COPYR " "cannot be handled by Lobster" - " \n and will lead to wrong results." + " \n and will lead to wrong results.", + stacklevel=2, ) if potcar.functional != "PBE": @@ -697,7 +701,8 @@ def standard_calculations_from_vasp_files( Lobsterin with standard settings """ warnings.warn( - "Always check and test the provided basis functions. The spilling of your Lobster calculation might help" + "Always check and test the provided basis functions. The spilling of your Lobster calculation might help", + stacklevel=2, ) if option not in { diff --git a/outputs.py b/outputs.py index 421816c5bd..90291feec7 100644 --- a/outputs.py +++ b/outputs.py @@ -418,7 +418,10 @@ def __init__( version = "5.1.0" elif len(lines[0].split()) == 6: version = "2.2.1" - warnings.warn("Please consider using a newer LOBSTER version. See www.cohp.de.") + warnings.warn( + "Please consider using a newer LOBSTER version. See www.cohp.de.", + stacklevel=2, + ) else: raise ValueError("Unsupported LOBSTER version.") @@ -637,7 +640,8 @@ def __init__(self, filename: PathLike | None = "NcICOBILIST.lobster") -> None: self.orbital_wise = True warnings.warn( "This is an orbitalwise NcICOBILIST.lobster file. " - "Currently, the orbitalwise information is not read!" + "Currently, the orbitalwise information is not read!", + stacklevel=2, ) break # condition has only to be met once @@ -1389,8 +1393,14 @@ def __init__( structure (Structure): Structure object. efermi (float): Fermi level in eV. """ - warnings.warn("Make sure all relevant FATBAND files were generated and read in!") - warnings.warn("Use Lobster 3.2.0 or newer for fatband calculations!") + warnings.warn( + "Make sure all relevant FATBAND files were generated and read in!", + stacklevel=2, + ) + warnings.warn( + "Use Lobster 3.2.0 or newer for fatband calculations!", + stacklevel=2, + ) if structure is None: raise ValueError("A structure object has to be provided") From 429a02b9ec5517e6a39a8c5ecdb6428b7ffafb1b Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Wed, 11 Dec 2024 09:52:44 +0800 Subject: [PATCH 100/180] Increase warnings `stacklevel` to 2, un-ignore RUFF `B028` (#4217) * increase warning stacklevel * unignore b028 ruff rule * fix some * fix more in apps, command_line and ext * remove unnecessary explicit UserWarning * replace explicit stacklevel 1 * fix io * fix entries * fix electronic_structure * avoid manual deprecation warning * fix core * fix analysis * futurewarning for legacy mprester --- cohp.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cohp.py b/cohp.py index cb82939c4f..3fc2b2cc46 100644 --- a/cohp.py +++ b/cohp.py @@ -1247,7 +1247,9 @@ def get_summed_icohp_by_label_list( for label in label_list: icohp = self._icohplist[label] if icohp.num_bonds != 1: - warnings.warn("One of the ICOHP values is an average over bonds. This is currently not considered.") + warnings.warn( + "One of the ICOHP values is an average over bonds. This is currently not considered.", stacklevel=2 + ) if icohp._is_spin_polarized and summed_spin_channels: sum_icohp += icohp.summed_icohp @@ -1350,7 +1352,7 @@ def extremum_icohpvalue( if not self._is_spin_polarized: if spin == Spin.down: - warnings.warn("This spin channel does not exist. I am switching to Spin.up") + warnings.warn("This spin channel does not exist. I am switching to Spin.up", stacklevel=2) spin = Spin.up for value in self._icohplist.values(): From b5f89407baf5964115d94501da8074462210341a Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Tue, 10 Dec 2024 18:21:22 -0800 Subject: [PATCH 101/180] =?UTF-8?q?Revert=20"`zopen`:=20explicit=20binary/?= =?UTF-8?q?text=20`mode`=20,=20and=20explicit=20`encoding`=20as=20UTF?= =?UTF-8?q?=E2=80=A6"=20(#4221)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 2e913c75e6e980039164f3b7a8953c6bc63c58df. --- inputs.py | 4 ++-- outputs.py | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/inputs.py b/inputs.py index c83b57cfe4..18a6a53c65 100644 --- a/inputs.py +++ b/inputs.py @@ -588,7 +588,7 @@ def from_file(cls, lobsterin: PathLike) -> Self: Returns: Lobsterin object """ - with zopen(lobsterin, mode="rt", encoding="utf-8") as file: + with zopen(lobsterin, mode="rt") as file: lines = file.read().split("\n") if not lines: raise RuntimeError("lobsterin file contains no data.") @@ -645,7 +645,7 @@ def _get_potcar_symbols(POTCAR_input: PathLike) -> list[str]: raise ValueError("Lobster only works with PAW! Use different POTCARs") # Warning about a bug in LOBSTER-4.1.0 - with zopen(POTCAR_input, mode="rt", encoding="utf-8") as file: + with zopen(POTCAR_input, mode="r") as file: data = file.read() if isinstance(data, bytes): diff --git a/outputs.py b/outputs.py index 90291feec7..a0ca239bfe 100644 --- a/outputs.py +++ b/outputs.py @@ -121,7 +121,7 @@ def __init__( else: self._filename = "COHPCAR.lobster" - with zopen(self._filename, mode="rt", encoding="utf-8") as file: + with zopen(self._filename, mode="rt") as file: lines = file.read().split("\n") # The parameters line is the second line in a COHPCAR file. @@ -405,7 +405,7 @@ def __init__( # LOBSTER list files have an extra trailing blank line # and we don't need the header. if self._icohpcollection is None: - with zopen(self._filename, mode="rt", encoding="utf-8") as file: + with zopen(self._filename, mode="rt") as file: all_lines = file.read().split("\n") lines = all_lines[1:-1] if "spin" not in all_lines[1] else all_lines[2:-1] if len(lines) == 0: @@ -622,7 +622,7 @@ def __init__(self, filename: PathLike | None = "NcICOBILIST.lobster") -> None: # LOBSTER list files have an extra trailing blank line # and we don't need the header - with zopen(filename, mode="rt", encoding="utf-8") as file: + with zopen(filename, mode="rt") as file: lines = file.read().split("\n")[1:-1] if len(lines) == 0: raise RuntimeError("NcICOBILIST file contains no data.") @@ -754,7 +754,7 @@ def _parse_doscar(self): tdensities = {} itdensities = {} - with zopen(doscar, mode="rt", encoding="utf-8") as file: + with zopen(doscar, mode="rt") as file: file.readline() # Skip the first line efermi = float([file.readline() for nn in range(4)][3].split()[17]) dos = [] @@ -913,7 +913,7 @@ def __init__( self.loewdin = [] if loewdin is None else loewdin if self.num_atoms is None: - with zopen(filename, mode="rt", encoding="utf-8") as file: + with zopen(filename, mode="rt") as file: lines = file.read().split("\n")[3:-3] if len(lines) == 0: raise RuntimeError("CHARGES file contains no data.") @@ -1047,7 +1047,7 @@ def __init__(self, filename: PathLike | None, **kwargs) -> None: else: raise ValueError(f"{attr}={val} is not a valid attribute for Lobsterout") elif filename: - with zopen(filename, mode="rt", encoding="utf-8") as file: + with zopen(filename, mode="rt") as file: lines = file.read().split("\n") if len(lines) == 0: raise RuntimeError("lobsterout does not contain any data") @@ -1445,7 +1445,7 @@ def __init__( raise ValueError("No FATBAND files in folder or given") for fname in filenames: - with zopen(fname, mode="rt", encoding="utf-8") as file: + with zopen(fname, mode="rt") as file: lines = file.read().split("\n") atom_names.append(os.path.split(fname)[1].split("_")[1].capitalize()) @@ -1479,7 +1479,7 @@ def __init__( eigenvals: dict = {} p_eigenvals: dict = {} for ifilename, filename in enumerate(filenames): - with zopen(filename, mode="rt", encoding="utf-8") as file: + with zopen(filename, mode="rt") as file: lines = file.read().split("\n") if ifilename == 0: @@ -1627,7 +1627,7 @@ def __init__( self.max_deviation = [] if max_deviation is None else max_deviation if not self.band_overlaps_dict: - with zopen(filename, mode="rt", encoding="utf-8") as file: + with zopen(filename, mode="rt") as file: lines = file.read().split("\n") spin_numbers = [0, 1] if lines[0].split()[-1] == "0" else [1, 2] @@ -1767,7 +1767,7 @@ def __init__( self.is_lcfo = is_lcfo self.list_dict_grosspop = [] if list_dict_grosspop is None else list_dict_grosspop if not self.list_dict_grosspop: - with zopen(filename, mode="rt", encoding="utf-8") as file: + with zopen(filename, mode="rt") as file: lines = file.read().split("\n") # Read file to list of dict @@ -1897,7 +1897,7 @@ def _parse_file( imaginary (list[float]): Imaginary parts of wave function. distance (list[float]): Distances to the first point in wave function file. """ - with zopen(filename, mode="rt", encoding="utf-8") as file: + with zopen(filename, mode="rt") as file: lines = file.read().split("\n") points = [] @@ -2067,7 +2067,7 @@ def __init__( self.madelungenergies_mulliken = None if madelungenergies_mulliken is None else madelungenergies_mulliken if self.ewald_splitting is None: - with zopen(filename, mode="rt", encoding="utf-8") as file: + with zopen(filename, mode="rt") as file: lines = file.read().split("\n")[5] if len(lines) == 0: raise RuntimeError("MadelungEnergies file contains no data.") @@ -2138,7 +2138,7 @@ def __init__( self.madelungenergies_mulliken: list | float = madelungenergies_mulliken or [] if self.num_atoms is None: - with zopen(filename, mode="rt", encoding="utf-8") as file: + with zopen(filename, mode="rt") as file: lines = file.read().split("\n") if len(lines) == 0: raise RuntimeError("SitePotentials file contains no data.") @@ -2291,7 +2291,7 @@ def __init__( """ self._filename = str(filename) - with zopen(self._filename, mode="rt", encoding="utf-8") as file: + with zopen(self._filename, mode="rt") as file: lines = file.readlines() if len(lines) == 0: raise RuntimeError("Please check provided input file, it seems to be empty") @@ -2479,7 +2479,7 @@ def __init__( self.bin_width = 0.0 if bin_width is None else bin_width if not self.bwdf: - with zopen(filename, mode="rt", encoding="utf-8") as file: + with zopen(filename, mode="rt") as file: lines = file.read().split("\n") if len(lines) == 0: raise RuntimeError("BWDF file contains no data.") From 8b2de587a9c6f8305b632ae60eac2a9de32c3fa6 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Fri, 10 Jan 2025 02:13:29 +0800 Subject: [PATCH 102/180] Reapply `zopen` explicit UTF-8 encoding, enable optional `EncodingWarning` PEP 597 in tests (#4222) * explicit mode for zopen * fix bad mode for cifwrite * remove tag * remove an unnecessary cast to int * TODO: test monty zopen change * fix implicit mode * Revert "TODO: test monty zopen change" This reverts commit c9bf970928e7e1d235a835e3cb4ad45a149ce010. * Revert "Revert "TODO: test monty zopen change"" This reverts commit be411a0f9d13bb3ebf77ecd5997ce5b21af81910. * Revert "Revert "Revert "TODO: test monty zopen change""" This reverts commit e75c196d47c614022e534741178eabd1316f9efe. * explicit text mode for stout * some explicit utf-8 for zopen * TO BE REVERTED: test monty pr * add the rest encoding * Revert "TO BE REVERTED: test monty pr" This reverts commit d899c87e60e58b5719f1123a60f992a9428d0e51. * enable PYTHONWARNDEFAULTENCODING * TO BE REVERTED: test leaving out encoding * fix zopen encoding warning as error * Revert "TO BE REVERTED: test leaving out encoding" This reverts commit 2ccf30c72e7e16c95f3c8c0cd2aa216ab097c1eb. * add custom warning * Reapply "TO BE REVERTED: test leaving out encoding" This reverts commit 24617c8fba266525584098ef52f48e44e9308480. * more descriptive comment * Revert "Reapply "TO BE REVERTED: test leaving out encoding"" This reverts commit 6438670420c7c4f7a43c8e277ba03bfcd4935d98. --- inputs.py | 4 ++-- outputs.py | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/inputs.py b/inputs.py index 18a6a53c65..c83b57cfe4 100644 --- a/inputs.py +++ b/inputs.py @@ -588,7 +588,7 @@ def from_file(cls, lobsterin: PathLike) -> Self: Returns: Lobsterin object """ - with zopen(lobsterin, mode="rt") as file: + with zopen(lobsterin, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") if not lines: raise RuntimeError("lobsterin file contains no data.") @@ -645,7 +645,7 @@ def _get_potcar_symbols(POTCAR_input: PathLike) -> list[str]: raise ValueError("Lobster only works with PAW! Use different POTCARs") # Warning about a bug in LOBSTER-4.1.0 - with zopen(POTCAR_input, mode="r") as file: + with zopen(POTCAR_input, mode="rt", encoding="utf-8") as file: data = file.read() if isinstance(data, bytes): diff --git a/outputs.py b/outputs.py index a0ca239bfe..90291feec7 100644 --- a/outputs.py +++ b/outputs.py @@ -121,7 +121,7 @@ def __init__( else: self._filename = "COHPCAR.lobster" - with zopen(self._filename, mode="rt") as file: + with zopen(self._filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") # The parameters line is the second line in a COHPCAR file. @@ -405,7 +405,7 @@ def __init__( # LOBSTER list files have an extra trailing blank line # and we don't need the header. if self._icohpcollection is None: - with zopen(self._filename, mode="rt") as file: + with zopen(self._filename, mode="rt", encoding="utf-8") as file: all_lines = file.read().split("\n") lines = all_lines[1:-1] if "spin" not in all_lines[1] else all_lines[2:-1] if len(lines) == 0: @@ -622,7 +622,7 @@ def __init__(self, filename: PathLike | None = "NcICOBILIST.lobster") -> None: # LOBSTER list files have an extra trailing blank line # and we don't need the header - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n")[1:-1] if len(lines) == 0: raise RuntimeError("NcICOBILIST file contains no data.") @@ -754,7 +754,7 @@ def _parse_doscar(self): tdensities = {} itdensities = {} - with zopen(doscar, mode="rt") as file: + with zopen(doscar, mode="rt", encoding="utf-8") as file: file.readline() # Skip the first line efermi = float([file.readline() for nn in range(4)][3].split()[17]) dos = [] @@ -913,7 +913,7 @@ def __init__( self.loewdin = [] if loewdin is None else loewdin if self.num_atoms is None: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n")[3:-3] if len(lines) == 0: raise RuntimeError("CHARGES file contains no data.") @@ -1047,7 +1047,7 @@ def __init__(self, filename: PathLike | None, **kwargs) -> None: else: raise ValueError(f"{attr}={val} is not a valid attribute for Lobsterout") elif filename: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") if len(lines) == 0: raise RuntimeError("lobsterout does not contain any data") @@ -1445,7 +1445,7 @@ def __init__( raise ValueError("No FATBAND files in folder or given") for fname in filenames: - with zopen(fname, mode="rt") as file: + with zopen(fname, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") atom_names.append(os.path.split(fname)[1].split("_")[1].capitalize()) @@ -1479,7 +1479,7 @@ def __init__( eigenvals: dict = {} p_eigenvals: dict = {} for ifilename, filename in enumerate(filenames): - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") if ifilename == 0: @@ -1627,7 +1627,7 @@ def __init__( self.max_deviation = [] if max_deviation is None else max_deviation if not self.band_overlaps_dict: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") spin_numbers = [0, 1] if lines[0].split()[-1] == "0" else [1, 2] @@ -1767,7 +1767,7 @@ def __init__( self.is_lcfo = is_lcfo self.list_dict_grosspop = [] if list_dict_grosspop is None else list_dict_grosspop if not self.list_dict_grosspop: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") # Read file to list of dict @@ -1897,7 +1897,7 @@ def _parse_file( imaginary (list[float]): Imaginary parts of wave function. distance (list[float]): Distances to the first point in wave function file. """ - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") points = [] @@ -2067,7 +2067,7 @@ def __init__( self.madelungenergies_mulliken = None if madelungenergies_mulliken is None else madelungenergies_mulliken if self.ewald_splitting is None: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n")[5] if len(lines) == 0: raise RuntimeError("MadelungEnergies file contains no data.") @@ -2138,7 +2138,7 @@ def __init__( self.madelungenergies_mulliken: list | float = madelungenergies_mulliken or [] if self.num_atoms is None: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") if len(lines) == 0: raise RuntimeError("SitePotentials file contains no data.") @@ -2291,7 +2291,7 @@ def __init__( """ self._filename = str(filename) - with zopen(self._filename, mode="rt") as file: + with zopen(self._filename, mode="rt", encoding="utf-8") as file: lines = file.readlines() if len(lines) == 0: raise RuntimeError("Please check provided input file, it seems to be empty") @@ -2479,7 +2479,7 @@ def __init__( self.bin_width = 0.0 if bin_width is None else bin_width if not self.bwdf: - with zopen(filename, mode="rt") as file: + with zopen(filename, mode="rt", encoding="utf-8") as file: lines = file.read().split("\n") if len(lines) == 0: raise RuntimeError("BWDF file contains no data.") From 94fd612190d57317bd7ed16634d6dc6dca3a9e12 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Fri, 10 Jan 2025 04:37:58 +0800 Subject: [PATCH 103/180] Explicit UTF-8 encoding for VASP input files with `zopen`, and `open` for other text files (#4218) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * explicit utf-8 encoding for kpoints from file * explicit utf-8 elsewhere * fix root level and dev_scripts * simplify PMG PKG path * fix analysis, cli, command_line * fix electronic_structure, entries and ext * fix io, phonon and symmetry * fix alchemy and anlysis tests * fix apps, command_line, core, elec_struct, entries, ext and vis tests * finish io and phonon tests * remove unnecessary seek * revert encoding for json dump * type custom paths * revert another json dump * ignore userwarning by default * relocate test-only env var * remove unneeded default tag for non-userwarning * also explicit utf-8 for json dump though forced ASCII * utf8 is alias to utf-8 in codecs, but maybe prefer the standard name * fix missing encoding in comment * add test for Γ decoding * better error message --------- Signed-off-by: Matthew Horton Co-authored-by: Shyue Ping Ong Co-authored-by: Matthew Horton --- tests/io/lobster/test_inputs.py | 2 +- tests/io/lobster/test_outputs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 59960d5970..8df13ae58a 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -44,7 +44,7 @@ def test_from_file(self): assert self.Lobsterin == self.Lobsterin2 def test_duplicates_from_file(self): - with open(f"{TEST_DIR}/lobsterin.1") as file: + with open(f"{TEST_DIR}/lobsterin.1", encoding="utf-8") as file: original_file = file.readlines() # String and float keywords does not allow duplicates diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index f46f69b8af..2fb647e5d8 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -420,7 +420,7 @@ def setUp(self): self.DOSCAR_lcfo = Doscar(doscar=doscar3, structure_file=poscar3, is_lcfo=True) - with open(f"{TEST_FILES_DIR}/electronic_structure/dos/structure_KF.json") as file: + with open(f"{TEST_FILES_DIR}/electronic_structure/dos/structure_KF.json", encoding="utf-8") as file: data = json.load(file) self.structure = Structure.from_dict(data) From acee19194d216171021d176c4171d265195b4084 Mon Sep 17 00:00:00 2001 From: "Haoyu (Daniel) YANG" Date: Fri, 24 Jan 2025 23:21:27 +0800 Subject: [PATCH 104/180] Avoid using full equality (`==`) to compare float, avoid `assert_array_equal` compare float array (#4159) * replace some float equality check * explicit encoding * charge is also float * enhance types * access gcd via math namespace as math is already imported * put dunder method to top * fix typo * tweak _proj implementation * support array like * add arg and return type * tweak type * avoid more == for float comparison * replace some == in test, more left to do * replace more in core test * replace more in test * replace even more * replace last batch * clean up assert approx * replace pytest.approx with approx * also fix membership check * replace some equality check of list * replace some sequences * fix test * replace float comparison as dict * fix test * replace more float compare, mostly for VASP * fix test * fix approx in condition block * replace sci notation * suppress buggy ruff sim300 * number_of_permutations to int * revert change for formula_double_format, in favor of another PR * c_indices seems to be int * use sci notation for crazily large int * simplify numpy.testing usage * set tol as pos arg * avoid array equal for list of str * assert_array_equal should not be used on float array * fix module level var name * more assert_array_equal on complex number * simplify approx on dict value * avoid module level var when it's used only 3 times * pytext.approx to approx * fix approx on nested dict * avoid unnecessary convert to np.array * array_equal to all close for float array * assert all close for float array * capital class attrib is treated as constant --- tests/io/lobster/test_inputs.py | 8 +- tests/io/lobster/test_lobsterenv.py | 3 +- tests/io/lobster/test_outputs.py | 170 ++++++++++++++-------------- 3 files changed, 92 insertions(+), 89 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 8df13ae58a..9c407d41bf 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -258,8 +258,8 @@ def test_standard_with_energy_range_from_vasprun(self): f"{VASP_OUT_DIR}/vasprun.C2.xml.gz", option="standard_with_energy_range_from_vasprun", ) - assert lobsterin_comp["COHPstartEnergy"] == -28.3679 - assert lobsterin_comp["COHPendEnergy"] == 32.8968 + assert lobsterin_comp["COHPstartEnergy"] == approx(-28.3679) + assert lobsterin_comp["COHPendEnergy"] == approx(32.8968) assert lobsterin_comp["COHPSteps"] == 301 def test_diff(self): @@ -299,11 +299,11 @@ def test_diff_case_insensitivity(self): def test_dict_functionality(self): for key in ("COHPstartEnergy", "COHPstartEnergy", "COhPstartenergy"): start_energy = self.Lobsterin.get(key) - assert start_energy == -15.0, f"{start_energy=}, {key=}" + assert start_energy == approx(-15.0), f"{start_energy=}, {key=}" lobsterin_copy = self.Lobsterin.copy() lobsterin_copy.update({"cohpstarteNergy": -10.00}) - assert lobsterin_copy["cohpstartenergy"] == -10.0 + assert lobsterin_copy["cohpstartenergy"] == approx(-10.0) lobsterin_copy.pop("cohpstarteNergy") assert "cohpstartenergy" not in lobsterin_copy lobsterin_copy.pop("cohpendenergY") diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 4f9cc9959b..1d56695803 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -4,6 +4,7 @@ import numpy as np import pytest +from numpy.testing import assert_allclose from pytest import approx from pymatgen.analysis.graphs import StructureGraph @@ -910,5 +911,5 @@ def test_valences(self): -0.54, -0.54, ] - assert self.chem_env_w_obj.valences == [0.67] * 4 + [0.7] * 4 + [-0.7] * 4 + [-0.68] * 4 # charge_obj + assert_allclose(self.chem_env_w_obj.valences, [0.67] * 4 + [0.7] * 4 + [-0.7] * 4 + [-0.68] * 4) # charge_obj assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index 2fb647e5d8..3b60027117 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -180,13 +180,13 @@ def test_energies(self): efermi_KF = -2.87475 elim_KF = (-11.25000 + efermi_KF, 7.5000 + efermi_KF) - assert self.cohp_bise.efermi == efermi_bise - assert self.coop_bise.efermi == efermi_bise - assert self.cohp_fe.efermi == efermi_fe - assert self.coop_fe.efermi == efermi_fe + assert self.cohp_bise.efermi == approx(efermi_bise) + assert self.coop_bise.efermi == approx(efermi_bise) + assert self.cohp_fe.efermi == approx(efermi_fe) + assert self.coop_fe.efermi == approx(efermi_fe) # Lobster 3.1 - assert self.cohp_KF.efermi == efermi_KF - assert self.coop_KF.efermi == efermi_KF + assert self.cohp_KF.efermi == approx(efermi_KF) + assert self.coop_KF.efermi == approx(efermi_KF) assert self.cohp_bise.energies[0] + self.cohp_bise.efermi == approx(elim_bise[0], abs=1e-4) assert self.cohp_bise.energies[-1] + self.cohp_bise.efermi == approx(elim_bise[1], abs=1e-4) @@ -444,9 +444,9 @@ def test_complete_dos(self): pdos_f_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] pdos_f_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] - assert energies_spin == self.DOSCAR_spin_pol.completedos.energies.tolist() - assert tdos_up == self.DOSCAR_spin_pol.completedos.densities[Spin.up].tolist() - assert tdos_down == self.DOSCAR_spin_pol.completedos.densities[Spin.down].tolist() + assert_allclose(energies_spin, self.DOSCAR_spin_pol.completedos.energies) + assert_allclose(tdos_up, self.DOSCAR_spin_pol.completedos.densities[Spin.up]) + assert_allclose(tdos_down, self.DOSCAR_spin_pol.completedos.densities[Spin.down]) assert fermi == approx(self.DOSCAR_spin_pol.completedos.efermi) assert_allclose( @@ -457,14 +457,14 @@ def test_complete_dos(self): self.DOSCAR_spin_pol2.completedos.structure.frac_coords, self.structure.frac_coords, ) - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == pdos_f_2s_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.down].tolist() == pdos_f_2s_down - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == pdos_f_2py_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.down].tolist() == pdos_f_2py_down - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == pdos_f_2pz_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.down].tolist() == pdos_f_2pz_down - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == pdos_f_2px_up - assert self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.down].tolist() == pdos_f_2px_down + assert_allclose(self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up], pdos_f_2s_up) + assert_allclose(self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.down], pdos_f_2s_down) + assert_allclose(self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up], pdos_f_2py_up) + assert_allclose(self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.down], pdos_f_2py_down) + assert_allclose(self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up], pdos_f_2pz_up) + assert_allclose(self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.down], pdos_f_2pz_down) + assert_allclose(self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up], pdos_f_2px_up) + assert_allclose(self.DOSCAR_spin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.down], pdos_f_2px_down) energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] @@ -473,18 +473,18 @@ def test_complete_dos(self): pdos_f_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] pdos_f_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] - assert energies_nonspin == self.DOSCAR_nonspin_pol.completedos.energies.tolist() + assert_allclose(energies_nonspin, self.DOSCAR_nonspin_pol.completedos.energies) - assert tdos_nonspin == self.DOSCAR_nonspin_pol.completedos.densities[Spin.up].tolist() + assert_allclose(tdos_nonspin, self.DOSCAR_nonspin_pol.completedos.densities[Spin.up]) assert fermi == approx(self.DOSCAR_nonspin_pol.completedos.efermi) assert self.DOSCAR_nonspin_pol.completedos.structure == self.structure - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up].tolist() == pdos_f_2s - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up].tolist() == pdos_f_2py - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up].tolist() == pdos_f_2pz - assert self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up].tolist() == pdos_f_2px + assert_allclose(self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2s"][Spin.up], pdos_f_2s) + assert_allclose(self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_y"][Spin.up], pdos_f_2py) + assert_allclose(self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_z"][Spin.up], pdos_f_2pz) + assert_allclose(self.DOSCAR_nonspin_pol.completedos.pdos[self.structure[0]]["2p_x"][Spin.up], pdos_f_2px) def test_pdos(self): # first for spin polarized version @@ -498,14 +498,14 @@ def test_pdos(self): pdos_f_2px_up = [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029] pdos_f_2px_down = [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029] - assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.up].tolist() == pdos_f_2s_up - assert self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.down].tolist() == pdos_f_2s_down - assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.up].tolist() == pdos_f_2py_up - assert self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.down].tolist() == pdos_f_2py_down - assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.up].tolist() == pdos_f_2pz_up - assert self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.down].tolist() == pdos_f_2pz_down - assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.up].tolist() == pdos_f_2px_up - assert self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.down].tolist() == pdos_f_2px_down + assert_allclose(self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.up], pdos_f_2s_up) + assert_allclose(self.DOSCAR_spin_pol.pdos[0]["2s"][Spin.down], pdos_f_2s_down) + assert_allclose(self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.up], pdos_f_2py_up) + assert_allclose(self.DOSCAR_spin_pol.pdos[0]["2p_y"][Spin.down], pdos_f_2py_down) + assert_allclose(self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.up], pdos_f_2pz_up) + assert_allclose(self.DOSCAR_spin_pol.pdos[0]["2p_z"][Spin.down], pdos_f_2pz_down) + assert_allclose(self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.up], pdos_f_2px_up) + assert_allclose(self.DOSCAR_spin_pol.pdos[0]["2p_x"][Spin.down], pdos_f_2px_down) # non spin pdos_f_2s = [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060] @@ -513,10 +513,10 @@ def test_pdos(self): pdos_f_2pz = [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037] pdos_f_2px = [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037] - assert self.DOSCAR_nonspin_pol.pdos[0]["2s"][Spin.up].tolist() == pdos_f_2s - assert self.DOSCAR_nonspin_pol.pdos[0]["2p_y"][Spin.up].tolist() == pdos_f_2py - assert self.DOSCAR_nonspin_pol.pdos[0]["2p_z"][Spin.up].tolist() == pdos_f_2pz - assert self.DOSCAR_nonspin_pol.pdos[0]["2p_x"][Spin.up].tolist() == pdos_f_2px + assert_allclose(self.DOSCAR_nonspin_pol.pdos[0]["2s"][Spin.up], pdos_f_2s) + assert_allclose(self.DOSCAR_nonspin_pol.pdos[0]["2p_y"][Spin.up], pdos_f_2py) + assert_allclose(self.DOSCAR_nonspin_pol.pdos[0]["2p_z"][Spin.up], pdos_f_2pz) + assert_allclose(self.DOSCAR_nonspin_pol.pdos[0]["2p_x"][Spin.up], pdos_f_2px) # test with DOSCAR.LCFO.lobster file pdos_1a1_AlN = [ @@ -560,9 +560,9 @@ def test_pdos(self): ] assert self.DOSCAR_lcfo._is_lcfo - assert self.DOSCAR_lcfo.pdos[0]["1a1"][Spin.down].tolist() == pdos_1a1_AlN - assert self.DOSCAR_lcfo.pdos[1]["3p_y"][Spin.down].tolist() == pdos_3py_Al - assert self.DOSCAR_lcfo.pdos[2]["2s"][Spin.down].tolist() == pdos_2s_N + assert_allclose(self.DOSCAR_lcfo.pdos[0]["1a1"][Spin.down], pdos_1a1_AlN) + assert_allclose(self.DOSCAR_lcfo.pdos[1]["3p_y"][Spin.down], pdos_3py_Al) + assert_allclose(self.DOSCAR_lcfo.pdos[2]["2s"][Spin.down], pdos_2s_N) def test_tdos(self): # first for spin polarized version @@ -571,38 +571,38 @@ def test_tdos(self): tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] fermi = 0.0 - assert energies_spin == self.DOSCAR_spin_pol.tdos.energies.tolist() - assert tdos_up == self.DOSCAR_spin_pol.tdos.densities[Spin.up].tolist() - assert tdos_down == self.DOSCAR_spin_pol.tdos.densities[Spin.down].tolist() + assert_allclose(energies_spin, self.DOSCAR_spin_pol.tdos.energies) + assert_allclose(tdos_up, self.DOSCAR_spin_pol.tdos.densities[Spin.up]) + assert_allclose(tdos_down, self.DOSCAR_spin_pol.tdos.densities[Spin.down]) assert fermi == approx(self.DOSCAR_spin_pol.tdos.efermi) energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] fermi = 0.0 - assert energies_nonspin == self.DOSCAR_nonspin_pol.tdos.energies.tolist() - assert tdos_nonspin == self.DOSCAR_nonspin_pol.tdos.densities[Spin.up].tolist() + assert_allclose(energies_nonspin, self.DOSCAR_nonspin_pol.tdos.energies) + assert_allclose(tdos_nonspin, self.DOSCAR_nonspin_pol.tdos.densities[Spin.up]) assert fermi == approx(self.DOSCAR_nonspin_pol.tdos.efermi) def test_energies(self): # first for spin polarized version energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - assert energies_spin == self.DOSCAR_spin_pol.energies.tolist() + assert_allclose(energies_spin, self.DOSCAR_spin_pol.energies) energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - assert energies_nonspin == self.DOSCAR_nonspin_pol.energies.tolist() + assert_allclose(energies_nonspin, self.DOSCAR_nonspin_pol.energies) def test_tdensities(self): # first for spin polarized version tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] - assert tdos_up == self.DOSCAR_spin_pol.tdensities[Spin.up].tolist() - assert tdos_down == self.DOSCAR_spin_pol.tdensities[Spin.down].tolist() + assert_allclose(tdos_up, self.DOSCAR_spin_pol.tdensities[Spin.up]) + assert_allclose(tdos_down, self.DOSCAR_spin_pol.tdensities[Spin.down]) tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] - assert tdos_nonspin == self.DOSCAR_nonspin_pol.tdensities[Spin.up].tolist() + assert_allclose(tdos_nonspin, self.DOSCAR_nonspin_pol.tdensities[Spin.up]) # test with DOSCAR.LCFO.lobster file tdos_up = [ @@ -619,16 +619,16 @@ def test_tdensities(self): 1.04535, ] - assert tdos_up == self.DOSCAR_lcfo.tdensities[Spin.up].tolist() + assert_allclose(tdos_up, self.DOSCAR_lcfo.tdensities[Spin.up]) def test_itdensities(self): itdos_up = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09650] itdos_down = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09685] - assert itdos_up == self.DOSCAR_spin_pol.itdensities[Spin.up].tolist() - assert itdos_down == self.DOSCAR_spin_pol.itdensities[Spin.down].tolist() + assert_allclose(itdos_up, self.DOSCAR_spin_pol.itdensities[Spin.up]) + assert_allclose(itdos_down, self.DOSCAR_spin_pol.itdensities[Spin.down]) itdos_nonspin = [4.00000, 10.00000, 10.00000, 16.00000, 16.00000, 16.09067] - assert itdos_nonspin == self.DOSCAR_nonspin_pol.itdensities[Spin.up].tolist() + assert_allclose(itdos_nonspin, self.DOSCAR_nonspin_pol.itdensities[Spin.up]) def test_is_spin_polarized(self): # first for spin polarized version @@ -661,7 +661,7 @@ def test_attributes(self): assert self.charge_lcfo.num_atoms == 3 assert self.charge_lcfo.types == ["AlN", "Al", "N"] assert self.charge_lcfo.atomlist == ["AlN1", "Al2", "N3"] - assert self.charge_lcfo.loewdin == [0.0, 1.02, -1.02] + assert_allclose(self.charge_lcfo.loewdin, [0.0, 1.02, -1.02]) assert not self.charge_lcfo.mulliken def test_get_structure_with_charges(self): @@ -755,7 +755,7 @@ def test_attributes(self): ] ] assert self.lobsterout_normal.basis_type == ["pbeVaspFit2015"] - assert self.lobsterout_normal.charge_spilling == [0.0268] + assert_allclose(self.lobsterout_normal.charge_spilling, [0.0268]) assert self.lobsterout_normal.dft_program == "VASP" assert self.lobsterout_normal.elements == ["Ti"] assert self.lobsterout_normal.has_charge @@ -808,7 +808,7 @@ def test_attributes(self): ] ] assert self.lobsterout_fatband_grosspop_densityofenergies.basis_type == ["pbeVaspFit2015"] - assert self.lobsterout_fatband_grosspop_densityofenergies.charge_spilling == [0.0268] + assert_allclose(self.lobsterout_fatband_grosspop_densityofenergies.charge_spilling, [0.0268]) assert self.lobsterout_fatband_grosspop_densityofenergies.dft_program == "VASP" assert self.lobsterout_fatband_grosspop_densityofenergies.elements == ["Ti"] assert self.lobsterout_fatband_grosspop_densityofenergies.has_charge @@ -861,7 +861,7 @@ def test_attributes(self): ] ] assert self.lobsterout_saveprojection.basis_type == ["pbeVaspFit2015"] - assert self.lobsterout_saveprojection.charge_spilling == [0.0268] + assert_allclose(self.lobsterout_saveprojection.charge_spilling, [0.0268]) assert self.lobsterout_saveprojection.dft_program == "VASP" assert self.lobsterout_saveprojection.elements == ["Ti"] assert self.lobsterout_saveprojection.has_charge @@ -914,7 +914,7 @@ def test_attributes(self): ] ] assert self.lobsterout_skipping_all.basis_type == ["pbeVaspFit2015"] - assert self.lobsterout_skipping_all.charge_spilling == [0.0268] + assert_allclose(self.lobsterout_skipping_all.charge_spilling, [0.0268]) assert self.lobsterout_skipping_all.dft_program == "VASP" assert self.lobsterout_skipping_all.elements == ["Ti"] assert not self.lobsterout_skipping_all.has_charge @@ -1078,7 +1078,7 @@ def test_attributes(self): "user_time": {"h": "0", "min": "0", "s": "12", "ms": "370"}, "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "180"}, } - assert self.lobsterout_GaAs.total_spilling[0] == approx([0.0859][0]) + assert self.lobsterout_GaAs.total_spilling[0] == approx(0.0859) assert self.lobsterout_onethread.number_of_threads == 1 # Test lobsterout of lobster-4.1.0 @@ -1292,35 +1292,35 @@ def setUp(self): self.bs_symmline_spin = self.vasprun_SiO2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) def test_attributes(self): - assert list(self.fatband_SiO2_p_x.label_dict["M"]) == approx([0.5, 0.0, 0.0]) + assert_allclose(list(self.fatband_SiO2_p_x.label_dict["M"]), [0.5, 0.0, 0.0]) assert self.fatband_SiO2_p_x.efermi == self.vasprun_SiO2_p_x.efermi lattice1 = self.bs_symmline.lattice_rec.as_dict() lattice2 = self.fatband_SiO2_p_x.lattice.as_dict() for idx in range(3): assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) - assert self.fatband_SiO2_p_x.eigenvals[Spin.up][1][1] - self.fatband_SiO2_p_x.efermi == -18.245 + assert self.fatband_SiO2_p_x.eigenvals[Spin.up][1][1] - self.fatband_SiO2_p_x.efermi == approx(-18.245) assert self.fatband_SiO2_p_x.is_spinpolarized is False - assert self.fatband_SiO2_p_x.kpoints_array[3] == approx([0.03409091, 0, 0]) + assert_allclose(self.fatband_SiO2_p_x.kpoints_array[3], [0.03409091, 0, 0]) assert self.fatband_SiO2_p_x.nbands == 36 - assert self.fatband_SiO2_p_x.p_eigenvals[Spin.up][2][1]["Si1"]["3p_x"] == 0.002 - assert self.fatband_SiO2_p_x.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) + assert self.fatband_SiO2_p_x.p_eigenvals[Spin.up][2][1]["Si1"]["3p_x"] == approx(0.002) + assert_allclose(self.fatband_SiO2_p_x.structure[0].frac_coords, [0.0, 0.47634315, 0.666667]) assert self.fatband_SiO2_p_x.structure[0].species_string == "Si" - assert self.fatband_SiO2_p_x.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) + assert_allclose(self.fatband_SiO2_p_x.structure[0].coords, [-1.19607309, 2.0716597, 3.67462144]) - assert list(self.fatband_SiO2_p.label_dict["M"]) == approx([0.5, 0.0, 0.0]) + assert_allclose(list(self.fatband_SiO2_p.label_dict["M"]), [0.5, 0.0, 0.0]) assert self.fatband_SiO2_p.efermi == self.vasprun_SiO2_p.efermi lattice1 = self.bs_symmline2.lattice_rec.as_dict() lattice2 = self.fatband_SiO2_p.lattice.as_dict() for idx in range(3): assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) - assert self.fatband_SiO2_p.eigenvals[Spin.up][1][1] - self.fatband_SiO2_p.efermi == -18.245 + assert self.fatband_SiO2_p.eigenvals[Spin.up][1][1] - self.fatband_SiO2_p.efermi == approx(-18.245) assert self.fatband_SiO2_p.is_spinpolarized is False - assert self.fatband_SiO2_p.kpoints_array[3] == approx([0.03409091, 0, 0]) + assert_allclose(self.fatband_SiO2_p.kpoints_array[3], [0.03409091, 0, 0]) assert self.fatband_SiO2_p.nbands == 36 - assert self.fatband_SiO2_p.p_eigenvals[Spin.up][2][1]["Si1"]["3p"] == 0.042 - assert self.fatband_SiO2_p.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) + assert self.fatband_SiO2_p.p_eigenvals[Spin.up][2][1]["Si1"]["3p"] == approx(0.042) + assert_allclose(self.fatband_SiO2_p.structure[0].frac_coords, [0.0, 0.47634315, 0.666667]) assert self.fatband_SiO2_p.structure[0].species_string == "Si" - assert self.fatband_SiO2_p.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) + assert_allclose(self.fatband_SiO2_p.structure[0].coords, [-1.19607309, 2.0716597, 3.67462144]) assert self.fatband_SiO2_p.efermi == approx(1.0647039) assert list(self.fatband_SiO2_spin.label_dict["M"]) == approx([0.5, 0.0, 0.0]) @@ -1329,16 +1329,16 @@ def test_attributes(self): lattice2 = self.fatband_SiO2_spin.lattice.as_dict() for idx in range(3): assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) - assert self.fatband_SiO2_spin.eigenvals[Spin.up][1][1] - self.fatband_SiO2_spin.efermi == -18.245 - assert self.fatband_SiO2_spin.eigenvals[Spin.down][1][1] - self.fatband_SiO2_spin.efermi == -18.245 + assert self.fatband_SiO2_spin.eigenvals[Spin.up][1][1] - self.fatband_SiO2_spin.efermi == approx(-18.245) + assert self.fatband_SiO2_spin.eigenvals[Spin.down][1][1] - self.fatband_SiO2_spin.efermi == approx(-18.245) assert self.fatband_SiO2_spin.is_spinpolarized - assert self.fatband_SiO2_spin.kpoints_array[3] == approx([0.03409091, 0, 0]) + assert_allclose(self.fatband_SiO2_spin.kpoints_array[3], [0.03409091, 0, 0]) assert self.fatband_SiO2_spin.nbands == 36 - assert self.fatband_SiO2_spin.p_eigenvals[Spin.up][2][1]["Si1"]["3p"] == 0.042 - assert self.fatband_SiO2_spin.structure[0].frac_coords == approx([0.0, 0.47634315, 0.666667]) + assert self.fatband_SiO2_spin.p_eigenvals[Spin.up][2][1]["Si1"]["3p"] == approx(0.042) + assert_allclose(self.fatband_SiO2_spin.structure[0].frac_coords, [0.0, 0.47634315, 0.666667]) assert self.fatband_SiO2_spin.structure[0].species_string == "Si" - assert self.fatband_SiO2_spin.structure[0].coords == approx([-1.19607309, 2.0716597, 3.67462144]) + assert_allclose(self.fatband_SiO2_spin.structure[0].coords, [-1.19607309, 2.0716597, 3.67462144]) def test_raises(self): with pytest.raises(ValueError, match="vasprun_file or efermi have to be provided"): @@ -2135,19 +2135,21 @@ def test_values(self): } assert icohplist_bise == self.icohp_bise.icohplist - assert self.icohp_bise.icohpcollection.extremum_icohpvalue() == -2.38796 + assert self.icohp_bise.icohpcollection.extremum_icohpvalue() == approx(-2.38796) assert icooplist_fe == self.icoop_fe.icohplist - assert self.icoop_fe.icohpcollection.extremum_icohpvalue() == -0.29919 + assert self.icoop_fe.icohpcollection.extremum_icohpvalue() == approx(-0.29919) assert icooplist_bise == self.icoop_bise.icohplist - assert self.icoop_bise.icohpcollection.extremum_icohpvalue() == 0.24714 + assert self.icoop_bise.icohpcollection.extremum_icohpvalue() == approx(0.24714) assert self.icobi.icohplist["1"]["icohp"][Spin.up] == approx(0.58649) assert self.icobi_orbitalwise.icohplist["2"]["icohp"][Spin.up] == approx(0.58649) assert self.icobi_orbitalwise.icohplist["1"]["icohp"][Spin.up] == approx(0.58649) assert self.icobi_orbitalwise_spinpolarized.icohplist["1"]["icohp"][Spin.up] == approx(0.58649 / 2, abs=1e-3) assert self.icobi_orbitalwise_spinpolarized.icohplist["1"]["icohp"][Spin.down] == approx(0.58649 / 2, abs=1e-3) assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["icohp"][Spin.down] == approx(0.58649 / 2, abs=1e-3) - assert self.icobi.icohpcollection.extremum_icohpvalue() == 0.58649 - assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["orbitals"]["2s-6s"]["icohp"][Spin.up] == 0.0247 + assert self.icobi.icohpcollection.extremum_icohpvalue() == approx(0.58649) + assert self.icobi_orbitalwise_spinpolarized.icohplist["2"]["orbitals"]["2s-6s"]["icohp"][Spin.up] == approx( + 0.0247 + ) # >v5 ICOHPLIST assert self.icohp_aln_511_sp.icohplist["2"]["icohp"][Spin.up] == approx(-0.21482) @@ -2354,7 +2356,7 @@ def setUp(self) -> None: def test_attributes(self): # hamilton matrices - assert self.hamilton_matrices.average_onsite_energies == pytest.approx( + assert self.hamilton_matrices.average_onsite_energies == approx( { "Na1_3s": 0.58855353, "Na1_2p_y": -25.72719646, @@ -2387,7 +2389,7 @@ def test_attributes(self): ) # overlap matrices - assert self.overlap_matrices.average_onsite_overlaps == pytest.approx( + assert self.overlap_matrices.average_onsite_overlaps == approx( { "Si1_3s": 1.00000009, "Si1_3p_y": 0.99999995, @@ -2449,7 +2451,7 @@ def test_attributes(self): Spin.up, Spin.down, ] - assert self.coeff_matrices.average_onsite_coefficient == pytest.approx( + assert self.coeff_matrices.average_onsite_coefficient == approx( { "Si1_3s": 0.6232626450000001, "Si1_3p_y": -0.029367565000000012, From 73ac3b48c0f7741f7c03ee37cbbf43890ba62949 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Fri, 24 Jan 2025 19:34:22 +0100 Subject: [PATCH 105/180] merge current mastr --- lobsterenv.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index bde0d210e4..1dcd39ad2f 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -16,6 +16,7 @@ import math import tempfile from typing import TYPE_CHECKING, NamedTuple +import warnings import matplotlib as mpl import numpy as np @@ -941,7 +942,6 @@ def _find_environments( centralsite = site copysite = copy.copy(centralsite) cell_start = centralsite.frac_coords - copysite.to_unit_cell().frac_coords - print(cell_start) neighbors_by_distance_start = self.structure.get_sites_in_sphere( pt=centralsite.coords, r=np.max(lengths_from_ICOHPs) + 0.5, @@ -980,7 +980,6 @@ def _find_environments( for neigh_idx, neigh in enumerate(neighbors_by_distance): index_here2 = index_here_list[neigh_idx] - print(index_here2) for dist_idx, dist in enumerate(copied_distances_from_ICOHPs): if ( np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) @@ -1008,16 +1007,11 @@ def _find_environments( _list_neighisite.append(index_here2) _neigh_coords.append(coords[neigh_idx]) _neigh_frac_coords.append(neigh.frac_coords) - print("test") - print(copied_translations_from_ICOHPs[dist_idx]) - print(translations_by_distance[neigh_idx]) - print(cell_start) del copied_distances_from_ICOHPs[dist_idx] del copied_neighbors_from_ICOHPs[dist_idx] del copied_translations_from_ICOHPs[dist_idx] break - print(_list_neighsite) list_neighisite.append(_list_neighisite) list_neighsite.append(_list_neighsite) list_lengths.append(lengths_from_ICOHPs) From 27edee90849eb297529d41682c5767b4f2029b23 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Fri, 24 Jan 2025 19:34:22 +0100 Subject: [PATCH 106/180] merge current mastr --- cohp.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cohp.py b/cohp.py index 8f415bf5b6..25a4ffd7b7 100644 --- a/cohp.py +++ b/cohp.py @@ -1044,6 +1044,13 @@ def is_spin_polarized(self) -> bool: @property def translation(self) -> list[int, int, int]: + """ + Returns the translation vector with respect to the origin cell + as defined in LOBSTER. + + Returns: + list[int, int, int] + """ return self._translation def icohpvalue(self, spin: Spin = Spin.up) -> float: From b30a4c858df7ab5705581efbc8becf7c95c26590 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 18:35:03 +0000 Subject: [PATCH 107/180] pre-commit auto-fixes --- lobsterenv.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 1dcd39ad2f..1b8f73d0a6 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -16,7 +16,6 @@ import math import tempfile from typing import TYPE_CHECKING, NamedTuple -import warnings import matplotlib as mpl import numpy as np @@ -941,7 +940,7 @@ def _find_environments( if len(neighbors_from_ICOHPs) > 0: centralsite = site copysite = copy.copy(centralsite) - cell_start = centralsite.frac_coords - copysite.to_unit_cell().frac_coords + centralsite.frac_coords - copysite.to_unit_cell().frac_coords neighbors_by_distance_start = self.structure.get_sites_in_sphere( pt=centralsite.coords, r=np.max(lengths_from_ICOHPs) + 0.5, From 79c6ef752bf0b740cfa6590378dbeb6d47938ecb Mon Sep 17 00:00:00 2001 From: JaGeo Date: Thu, 6 Feb 2025 18:46:08 +0100 Subject: [PATCH 108/180] add new test data --- lobsterenv.py | 76 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 1dcd39ad2f..ec7838d654 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -87,6 +87,7 @@ def __init__( filename_blist_sg2: PathLike | None = None, id_blist_sg1: Literal["icoop", "icobi"] = "icoop", id_blist_sg2: Literal["icoop", "icobi"] = "icobi", + backward_compatibility: bool = False, ) -> None: """ Args: @@ -126,6 +127,7 @@ def __init__( filename_blist_sg2 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. id_blist_sg1 ("icoop" | "icobi"): Identity of data in filename_blist_sg1. id_blist_sg2 ("icoop" | "icobi"): Identity of data in filename_blist_sg2. + backward_compatibility (bool): compatiblity with neighbor detection prior 2025 (less strict). """ if filename_icohp is not None: self.ICOHP = Icohplist(are_coops=are_coops, are_cobis=are_cobis, filename=filename_icohp) @@ -148,6 +150,7 @@ def __init__( self.id_blist_sg1 = id_blist_sg1.lower() self.id_blist_sg2 = id_blist_sg2.lower() + self.backward_compatibility = backward_compatibility allowed_arguments = {"icoop", "icobi"} if self.id_blist_sg1 not in allowed_arguments or self.id_blist_sg2 not in allowed_arguments: @@ -700,7 +703,7 @@ def get_info_icohps_between_neighbors( upperlimit=upperlimit, only_bonds_to=self.only_bonds_to, ) - + # TODO: check if necessary! done = False for icohp in icohps.values(): atomnr1 = self._get_atomnumber(icohp._atom1) @@ -981,28 +984,57 @@ def _find_environments( for neigh_idx, neigh in enumerate(neighbors_by_distance): index_here2 = index_here_list[neigh_idx] for dist_idx, dist in enumerate(copied_distances_from_ICOHPs): - if ( - np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) - and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 - and ( - ( - copied_translations_from_ICOHPs[dist_idx][0] - == -translations_by_distance[neigh_idx][0] - and copied_translations_from_ICOHPs[dist_idx][1] - == -translations_by_distance[neigh_idx][1] - and copied_translations_from_ICOHPs[dist_idx][2] - == -translations_by_distance[neigh_idx][2] - ) - or ( - copied_translations_from_ICOHPs[dist_idx][0] - == translations_by_distance[neigh_idx][0] - and copied_translations_from_ICOHPs[dist_idx][1] - == translations_by_distance[neigh_idx][1] - and copied_translations_from_ICOHPs[dist_idx][2] - == translations_by_distance[neigh_idx][2] - ) + if not self.backward_compatibility: + comparison = ( + np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) + and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 + and (( + copied_translations_from_ICOHPs[dist_idx][0] == + translations_by_distance[neigh_idx][0] + and copied_translations_from_ICOHPs[dist_idx][1] == + translations_by_distance[neigh_idx][1] + and copied_translations_from_ICOHPs[dist_idx][2] == + translations_by_distance[neigh_idx][2] + ) or + ( + copied_translations_from_ICOHPs[dist_idx][0] == + -translations_by_distance[neigh_idx][0] + and copied_translations_from_ICOHPs[dist_idx][1] == + -translations_by_distance[neigh_idx][1] + and copied_translations_from_ICOHPs[dist_idx][2] == + -translations_by_distance[neigh_idx][2] + ) + ) + ) - ): + else: + comparison = ( + np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) + and copied_neighbors_from_ICOHPs[dist_idx] == index_here2) + # comparison = ( + # np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) + # and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 + # and ( + # ( + # copied_translations_from_ICOHPs[dist_idx][0] == - + # translations_by_distance[neigh_idx][0] + # and copied_translations_from_ICOHPs[dist_idx][1] == - + # translations_by_distance[neigh_idx][1] + # and copied_translations_from_ICOHPs[dist_idx][2] == - + # translations_by_distance[neigh_idx][2] + # ) + # or ( + # copied_translations_from_ICOHPs[dist_idx][0] == + # translations_by_distance[neigh_idx][0] + # and copied_translations_from_ICOHPs[dist_idx][1] == + # translations_by_distance[neigh_idx][1] + # and copied_translations_from_ICOHPs[dist_idx][2] == + # translations_by_distance[neigh_idx][2] + # ) + # ) + # ) + + if comparison: _list_neighsite.append(neigh) _list_neighisite.append(index_here2) _neigh_coords.append(coords[neigh_idx]) From 3b58fb7641869d812d1aa583be820696baf35389 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Thu, 6 Feb 2025 18:46:08 +0100 Subject: [PATCH 109/180] add new test data --- tests/io/lobster/test_lobsterenv.py | 237 +++++++++++++++------------- 1 file changed, 124 insertions(+), 113 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 4f9cc9959b..ce02682749 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -31,104 +31,109 @@ def setUp(self): self.chem_env_lobster1 = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), additional_condition=1, + perc_strength_icohp=0.3, + noise_cutoff=0.0 ) # all bonds self.chem_env_lobster0 = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), additional_condition=0, ) # only cation-cation, anion-anion bonds self.chem_env_lobster5 = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), additional_condition=5, ) # only cation-cation bonds self.chem_env_lobster6 = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), additional_condition=6, ) # 2,3,4 are not tested so far self.chem_env_lobster2 = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), additional_condition=2, ) self.chem_env_lobster3 = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), additional_condition=3, ) self.chem_env_lobster4 = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), additional_condition=4, ) # search for other testcase where 2,3,4 arrive at different results self.chem_env_lobster0_second = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), additional_condition=0, + perc_strength_icohp=0.05 ) self.chem_env_lobster1_second = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), additional_condition=1, ) self.chem_env_lobster2_second = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), additional_condition=2, ) self.chem_env_lobster5_second = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), additional_condition=5, + perc_strength_icohp=0.05, ) self.chem_env_lobster5_second_percentage = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), additional_condition=5, perc_strength_icohp=1.0, ) self.chem_env_lobster6_second = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), additional_condition=6, + perc_strength_icohp=0.05 ) # coop / cobi self.chem_env_lobster1_coop_NaCl = LobsterNeighbors( are_coops=True, filename_icohp=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), additional_condition=1, noise_cutoff=None, ) @@ -136,51 +141,51 @@ def setUp(self): self.chem_env_lobster1_cobi_NaCl = LobsterNeighbors( are_coops=True, filename_icohp=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), additional_condition=1, noise_cutoff=None, ) self.chem_env_lobster1_cobi_mp470 = LobsterNeighbors( are_coops=True, - filename_icohp=f"{TEST_DIR}/ICOBILIST.lobster.mp_470.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_470.gz"), + filename_icohp=f"{TEST_DIR}/ICOBILIST.lobster.mp-470.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-470.gz"), additional_condition=1, ) # TODO: use charge instead of valence self.chem_env_lobster1_charges = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, ) self.chem_env_lobster1_charges_noisecutoff = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_632319.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_632319.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-632319.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-632319.gz"), valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp_632319.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-632319.gz", additional_condition=1, perc_strength_icohp=0.05, noise_cutoff=0.1, ) self.chem_env_lobster1_charges_wo_noisecutoff = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_632319.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_632319.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-632319.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-632319.gz"), valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp_632319.gz", + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-632319.gz", additional_condition=1, perc_strength_icohp=0.05, noise_cutoff=None, ) self.chem_env_lobster1_charges_loewdin = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, @@ -188,8 +193,8 @@ def setUp(self): ) self.chem_env_lobster6_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=6, @@ -197,8 +202,8 @@ def setUp(self): ) self.chem_env_lobster5_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=5, @@ -206,8 +211,8 @@ def setUp(self): ) self.chem_env_lobster4_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=4, @@ -215,8 +220,8 @@ def setUp(self): ) self.chem_env_lobster3_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=3, @@ -224,8 +229,8 @@ def setUp(self): ) self.chem_env_lobster2_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=2, @@ -233,8 +238,8 @@ def setUp(self): ) self.chem_env_lobster1_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, @@ -243,8 +248,8 @@ def setUp(self): self.chem_env_lobster0_charges_additional_condition = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=0, @@ -253,7 +258,7 @@ def setUp(self): self.chem_env_lobster0_NaSi = LobsterNeighbors( are_coops=False, filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaSi.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaSi.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaSi.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaSi.gz", additional_condition=0, @@ -262,7 +267,7 @@ def setUp(self): self.chem_env_lobster_NaSi_wo_charges = LobsterNeighbors( are_coops=False, filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaSi.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaSi.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaSi.gz"), valences_from_charges=False, filename_charge=None, additional_condition=0, @@ -276,7 +281,7 @@ def setUp(self): are_coops=False, obj_icohp=self.obj_icohp, obj_charge=self.obj_charge, - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaSi.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaSi.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaSi.gz", additional_condition=0, @@ -315,8 +320,8 @@ def test_wrong_additional_correction(self): ): LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=10, @@ -325,8 +330,8 @@ def test_wrong_additional_correction(self): def test_set_limits(self): test = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_353.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", additional_condition=1, @@ -356,7 +361,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster0.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=0, ) ) @@ -365,7 +370,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster0.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=1, ) ) @@ -375,7 +380,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=0, ) ) @@ -384,7 +389,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=1, ) ) @@ -412,7 +417,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster2.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=0, ) ) @@ -421,7 +426,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster2.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=1, ) ) @@ -431,7 +436,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster3.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=0, ) ) @@ -440,7 +445,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster3.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=1, ) ) @@ -450,7 +455,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster4.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=0, ) ) @@ -459,7 +464,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster4.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=1, ) ) @@ -469,7 +474,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster5.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=0, ) ) @@ -478,7 +483,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster5.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=1, ) ) @@ -488,7 +493,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster6.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=0, ) ) @@ -498,7 +503,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster6.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), n=1, ) ) @@ -511,7 +516,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster0_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), n=0, ) ) @@ -522,7 +527,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), n=0, ) ) @@ -532,7 +537,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1_coop_NaCl.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), n=0, ) ) @@ -542,7 +547,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1_cobi_NaCl.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), n=0, ) ) @@ -552,7 +557,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster1_cobi_mp470.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_470.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-470.gz"), n=3, ) ) @@ -563,7 +568,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster2_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), n=0, ) ) @@ -572,7 +577,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster2_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), n=4, ) ) @@ -583,7 +588,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster5_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), n=0, ) ) @@ -592,17 +597,18 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster5_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), n=4, ) ) - == 0 + == 4 ) + # 6: ONLY_CATION_CATION_BONDS assert ( len( self.chem_env_lobster6_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), n=0, ) ) @@ -611,7 +617,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster6_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), n=4, ) ) @@ -621,7 +627,7 @@ def test_get_nn_info(self): assert ( len( self.chem_env_lobster5_second_percentage.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), n=0, ) ) @@ -630,7 +636,7 @@ def test_get_nn_info(self): def test_structure_graph(self): sg = self.chem_env_lobster1_second.get_bonded_structure( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz") + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz") ) assert isinstance(sg, StructureGraph) @@ -638,7 +644,7 @@ def test_extended_structure_graph(self): self.chem_env_lobsterNaCl = LobsterNeighbors( are_coops=False, filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaCl.gz", filename_blist_sg1=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", @@ -649,14 +655,14 @@ def test_extended_structure_graph(self): additional_condition=1, ) sg = self.chem_env_lobsterNaCl.get_bonded_structure( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), decorate=True, edge_properties=True, weights=True, ) - assert sg.graph.get_edge_data(0, 1)[0]["ICOHP"] == approx(-0.56541) - assert sg.graph.get_edge_data(0, 1)[0]["ICOBI"] == approx(0.08484) - assert sg.graph.get_edge_data(0, 1)[0]["ICOOP"] == approx(0.02826) + assert sg.graph.get_edge_data(0, 1)[0]["ICOHP"] == approx(-0.59138) + assert sg.graph.get_edge_data(0, 1)[0]["ICOBI"] == approx(0.08682) + assert sg.graph.get_edge_data(0, 1)[0]["ICOOP"] == approx(0.03007) assert sg.graph.get_edge_data(0, 1)[0]["bond_label"] == "21" assert sg.graph.get_edge_data(0, 1)[5]["bond_label"] == "30" assert isinstance(sg, StructureGraph) @@ -666,7 +672,7 @@ def test_raises_extended_structure_graph(self): self.chem_env_lobsterNaCl = LobsterNeighbors( are_coops=False, filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.NaCl.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), valences_from_charges=True, filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaCl.gz", filename_blist_sg1=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", @@ -679,7 +685,7 @@ def test_raises_extended_structure_graph(self): def test_order_parameter(self): assert self.chem_env_lobster1_second.get_local_order_parameters( - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_353.gz"), n=0 + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), n=0 )["linear"] == approx(1.0) def test_get_structure_environments(self): @@ -698,16 +704,16 @@ def test_get_structure_environments_further_tests(self): def test_get_info_icohps_neighbors(self): results = self.chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0]) - assert results[0] == approx(-33.26058) + assert results[0] == approx(-33.87452) for bond in results[1]: - assert bond == approx(-5.54345, abs=1e-3) + assert bond == approx(-5.64612, abs=1e-2) assert results[2] == 6 assert results[3] == ["27", "30", "48", "49", "64", "73"] results2 = self.chem_env_lobster1.get_info_icohps_to_neighbors(isites=None) - assert results2[0] == approx(-33.26058) + assert results2[0] == approx(-33.87452) for bond in results2[1]: - assert bond == approx(-5.54345, abs=1e-3) + assert bond == approx(-5.64455, abs=1e-2) assert results2[2] == 6 assert results2[3] == ["27", "30", "48", "49", "64", "73"] assert results2[4] == [ @@ -723,16 +729,21 @@ def test_get_sum_icohps_between_neighbors_of_atom(self): # will only look at icohps between cations or anions self.chem_env_lobster1.get_info_icohps_to_neighbors(isites=[1]) assert self.chem_env_lobster1.get_info_icohps_between_neighbors(isites=[1])[2] == 1 - assert self.chem_env_lobster1.get_info_icohps_between_neighbors(isites=[1])[0] == approx(-0.05507) - assert self.chem_env_lobster1.get_info_icohps_between_neighbors(isites=[0])[2] == 15 + assert self.chem_env_lobster1.get_info_icohps_between_neighbors(isites=[1])[0] == approx(-0.04535) + + # confirmed by looking at the VESTA output + assert self.chem_env_lobster1.get_info_icohps_between_neighbors(isites=[0])[2] == 12 # use an example where this is easier to test (e.g., linear environment?) chemenv_here = LobsterNeighbors( are_coops=False, filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-7000.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp-7000.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-7000.gz"), additional_condition=1, + perc_strength_icohp=0.15, + noise_cutoff=0.0 ) + print(chemenv_here.get_info_icohps_between_neighbors(isites=[0])) assert len(chemenv_here.get_info_icohps_between_neighbors(isites=[0])[4]) == 6 def test_get_plot_label(self): @@ -792,10 +803,10 @@ def test_get_info_cohps_to_neighbors(self): chem_env_lobster1 = LobsterNeighbors( are_coops=False, filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190_2.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), additional_condition=1, ) - cohpcar_lobster_mp_190 = f"{TEST_DIR}/COHPCAR.lobster.mp-190.gz" + cohpcar_lobster_mp_190 = f"{TEST_DIR}/COHPCAR.lobster.mp-190_2.gz" plot_label, summed_cohpcar_mp_190 = chem_env_lobster1.get_info_cohps_to_neighbors( path_to_cohpcar=cohpcar_lobster_mp_190, isites=[0], @@ -844,7 +855,7 @@ def test_get_info_cohps_to_neighbors(self): obj_cohpcar = CompleteCohp.from_file( filename=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", fmt="LOBSTER", - structure_file=f"{TEST_DIR}/POSCAR.NaSi.gz", + structure_file=f"{TEST_DIR}/CONTCAR.NaSi.gz", ) plot_label_obj, _summed_cohpcar_NaSi_obj = self.chem_env_w_obj.get_info_cohps_to_neighbors( obj_cohpcar=obj_cohpcar, @@ -903,12 +914,12 @@ def test_valences(self): -0.75, ] # Mulliken assert self.chem_env_lobster1_charges_loewdin.valences == [ - 0.27, - 0.27, - 0.27, - 0.27, - -0.54, - -0.54, + 0.51, + 0.51, + 0.51, + 0.51, + -1.01, + -1.01, ] - assert self.chem_env_w_obj.valences == [0.67] * 4 + [0.7] * 4 + [-0.7] * 4 + [-0.68] * 4 # charge_obj + assert self.chem_env_w_obj.valences == [0.66] * 4 + [0.69] * 4 + [-0.68] * 3 +[-0.69] + [-0.67] * 4 # charge_obj assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA From 93978da064ad9c3fb89fa80be659dc2ce0e44f99 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:50:46 +0000 Subject: [PATCH 110/180] pre-commit auto-fixes --- lobsterenv.py | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index d474f1c5e2..98fa67501d 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -987,29 +987,30 @@ def _find_environments( comparison = ( np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 - and (( - copied_translations_from_ICOHPs[dist_idx][0] == - translations_by_distance[neigh_idx][0] - and copied_translations_from_ICOHPs[dist_idx][1] == - translations_by_distance[neigh_idx][1] - and copied_translations_from_ICOHPs[dist_idx][2] == - translations_by_distance[neigh_idx][2] - ) or - ( - copied_translations_from_ICOHPs[dist_idx][0] == - -translations_by_distance[neigh_idx][0] - and copied_translations_from_ICOHPs[dist_idx][1] == - -translations_by_distance[neigh_idx][1] - and copied_translations_from_ICOHPs[dist_idx][2] == - -translations_by_distance[neigh_idx][2] - ) - ) - + and ( + ( + copied_translations_from_ICOHPs[dist_idx][0] + == translations_by_distance[neigh_idx][0] + and copied_translations_from_ICOHPs[dist_idx][1] + == translations_by_distance[neigh_idx][1] + and copied_translations_from_ICOHPs[dist_idx][2] + == translations_by_distance[neigh_idx][2] + ) + or ( + copied_translations_from_ICOHPs[dist_idx][0] + == -translations_by_distance[neigh_idx][0] + and copied_translations_from_ICOHPs[dist_idx][1] + == -translations_by_distance[neigh_idx][1] + and copied_translations_from_ICOHPs[dist_idx][2] + == -translations_by_distance[neigh_idx][2] + ) + ) ) else: comparison = ( - np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) - and copied_neighbors_from_ICOHPs[dist_idx] == index_here2) + np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) + and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 + ) # comparison = ( # np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) # and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 From 40b2b280a101fe61c23797d373ae94b1528eb988 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Thu, 6 Feb 2025 19:00:50 +0100 Subject: [PATCH 111/180] fix some more --- tests/io/lobster/test_lobsterenv.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 6f290208e2..b0a12aa07f 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -922,9 +922,6 @@ def test_valences(self): -1.01, -1.01, ] -<<<<<<< HEAD - assert self.chem_env_w_obj.valences == [0.66] * 4 + [0.69] * 4 + [-0.68] * 3 +[-0.69] + [-0.67] * 4 # charge_obj -======= - assert_allclose(self.chem_env_w_obj.valences, [0.67] * 4 + [0.7] * 4 + [-0.7] * 4 + [-0.68] * 4) # charge_obj ->>>>>>> a9225720951d05e022c09cd380afa784c146b267 + + assert_allclose(self.chem_env_w_obj.valences == [0.66] * 4 + [0.69] * 4 + [-0.68] * 3 +[-0.69] + [-0.67] * 4) # charge_obj assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA From 96fb06d31adf651d61d7fb0321b3d660a6c3a1cc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 18:02:25 +0000 Subject: [PATCH 112/180] pre-commit auto-fixes --- tests/io/lobster/test_lobsterenv.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index b0a12aa07f..322fa1f3dd 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -36,7 +36,7 @@ def setUp(self): structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), additional_condition=1, perc_strength_icohp=0.3, - noise_cutoff=0.0 + noise_cutoff=0.0, ) # all bonds @@ -91,7 +91,7 @@ def setUp(self): filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), additional_condition=0, - perc_strength_icohp=0.05 + perc_strength_icohp=0.05, ) self.chem_env_lobster1_second = LobsterNeighbors( are_coops=False, @@ -128,7 +128,7 @@ def setUp(self): filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), additional_condition=6, - perc_strength_icohp=0.05 + perc_strength_icohp=0.05, ) # coop / cobi self.chem_env_lobster1_coop_NaCl = LobsterNeighbors( @@ -742,7 +742,7 @@ def test_get_sum_icohps_between_neighbors_of_atom(self): structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-7000.gz"), additional_condition=1, perc_strength_icohp=0.15, - noise_cutoff=0.0 + noise_cutoff=0.0, ) print(chemenv_here.get_info_icohps_between_neighbors(isites=[0])) assert len(chemenv_here.get_info_icohps_between_neighbors(isites=[0])[4]) == 6 @@ -923,5 +923,7 @@ def test_valences(self): -1.01, ] - assert_allclose(self.chem_env_w_obj.valences == [0.66] * 4 + [0.69] * 4 + [-0.68] * 3 +[-0.69] + [-0.67] * 4) # charge_obj + assert_allclose( + self.chem_env_w_obj.valences == [0.66] * 4 + [0.69] * 4 + [-0.68] * 3 + [-0.69] + [-0.67] * 4 + ) # charge_obj assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA From ce92b65e4a4e5cdcfec90ba5e2bee2e02f91e98f Mon Sep 17 00:00:00 2001 From: JaGeo Date: Thu, 6 Feb 2025 19:31:14 +0100 Subject: [PATCH 113/180] fix nearly everything except for two inter neighbor things --- lobsterenv.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 98fa67501d..62b00768c1 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -86,7 +86,7 @@ def __init__( filename_blist_sg2: PathLike | None = None, id_blist_sg1: Literal["icoop", "icobi"] = "icoop", id_blist_sg2: Literal["icoop", "icobi"] = "icobi", - backward_compatibility: bool = False, + backward_compatibility: bool = True, ) -> None: """ Args: @@ -943,7 +943,7 @@ def _find_environments( if len(neighbors_from_ICOHPs) > 0: centralsite = site copysite = copy.copy(centralsite) - centralsite.frac_coords - copysite.to_unit_cell().frac_coords + #print(centralsite.frac_coords - copysite.to_unit_cell().frac_coords) neighbors_by_distance_start = self.structure.get_sites_in_sphere( pt=centralsite.coords, r=np.max(lengths_from_ICOHPs) + 0.5, @@ -996,14 +996,14 @@ def _find_environments( and copied_translations_from_ICOHPs[dist_idx][2] == translations_by_distance[neigh_idx][2] ) - or ( - copied_translations_from_ICOHPs[dist_idx][0] - == -translations_by_distance[neigh_idx][0] - and copied_translations_from_ICOHPs[dist_idx][1] - == -translations_by_distance[neigh_idx][1] - and copied_translations_from_ICOHPs[dist_idx][2] - == -translations_by_distance[neigh_idx][2] - ) + # or ( + # copied_translations_from_ICOHPs[dist_idx][0] + # == -translations_by_distance[neigh_idx][0] + # and copied_translations_from_ICOHPs[dist_idx][1] + # == -translations_by_distance[neigh_idx][1] + # and copied_translations_from_ICOHPs[dist_idx][2] + # == -translations_by_distance[neigh_idx][2] + # ) ) ) else: @@ -1115,7 +1115,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) + translation_from_ICOHPs.append(-icohp.translation) # ONLY_ANION_CATION_BONDS elif additional_condition == 1: @@ -1132,7 +1132,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) + translation_from_ICOHPs.append(-icohp.translation) # NO_ELEMENT_TO_SAME_ELEMENT_BONDS elif additional_condition == 2: @@ -1149,7 +1149,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) + translation_from_ICOHPs.append(-icohp.translation) # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS elif additional_condition == 3: @@ -1168,7 +1168,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) + translation_from_ICOHPs.append(-icohp.translation) # ONLY_ELEMENT_TO_OXYGEN_BONDS elif additional_condition == 4: @@ -1185,7 +1185,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) + translation_from_ICOHPs.append(-icohp.translation) # DO_NOT_CONSIDER_ANION_CATION_BONDS elif additional_condition == 5: @@ -1202,7 +1202,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) + translation_from_ICOHPs.append(-icohp.translation) # ONLY_CATION_CATION_BONDS elif additional_condition == 6 and val1 > 0.0 and val2 > 0.0: # type: ignore[operator] @@ -1218,7 +1218,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) + translation_from_ICOHPs.append(-icohp.translation) return ( keys_from_ICOHPs, From 268f92d9c18f5480ad4e0e04481f440a9013652a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 18:34:09 +0000 Subject: [PATCH 114/180] pre-commit auto-fixes --- lobsterenv.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 62b00768c1..c2deacd3b4 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -942,8 +942,8 @@ def _find_environments( if len(neighbors_from_ICOHPs) > 0: centralsite = site - copysite = copy.copy(centralsite) - #print(centralsite.frac_coords - copysite.to_unit_cell().frac_coords) + copy.copy(centralsite) + # print(centralsite.frac_coords - copysite.to_unit_cell().frac_coords) neighbors_by_distance_start = self.structure.get_sites_in_sphere( pt=centralsite.coords, r=np.max(lengths_from_ICOHPs) + 0.5, @@ -988,14 +988,12 @@ def _find_environments( np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 and ( - ( - copied_translations_from_ICOHPs[dist_idx][0] - == translations_by_distance[neigh_idx][0] - and copied_translations_from_ICOHPs[dist_idx][1] - == translations_by_distance[neigh_idx][1] - and copied_translations_from_ICOHPs[dist_idx][2] - == translations_by_distance[neigh_idx][2] - ) + copied_translations_from_ICOHPs[dist_idx][0] + == translations_by_distance[neigh_idx][0] + and copied_translations_from_ICOHPs[dist_idx][1] + == translations_by_distance[neigh_idx][1] + and copied_translations_from_ICOHPs[dist_idx][2] + == translations_by_distance[neigh_idx][2] # or ( # copied_translations_from_ICOHPs[dist_idx][0] # == -translations_by_distance[neigh_idx][0] From ea884be26d202f80e32b67501a85daadf0c31001 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Fri, 7 Feb 2025 08:15:52 +0100 Subject: [PATCH 115/180] add more fixes --- lobsterenv.py | 70 ++++++++++++++++++++++----------------------------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 62b00768c1..f6107adc53 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -86,7 +86,7 @@ def __init__( filename_blist_sg2: PathLike | None = None, id_blist_sg1: Literal["icoop", "icobi"] = "icoop", id_blist_sg2: Literal["icoop", "icobi"] = "icobi", - backward_compatibility: bool = True, + backward_compatibility: bool = False, ) -> None: """ Args: @@ -536,7 +536,7 @@ def get_info_cohps_to_neighbors( _summed_icohps, _list_icohps, _number_bonds, labels, atoms, final_isites = self.get_info_icohps_to_neighbors( isites=isites, onlycation_isites=onlycation_isites ) - + print(_list_icohps) with tempfile.TemporaryDirectory() as tmp_dir: path = f"{tmp_dir}/POSCAR.vasp" @@ -942,8 +942,6 @@ def _find_environments( if len(neighbors_from_ICOHPs) > 0: centralsite = site - copysite = copy.copy(centralsite) - #print(centralsite.frac_coords - copysite.to_unit_cell().frac_coords) neighbors_by_distance_start = self.structure.get_sites_in_sphere( pt=centralsite.coords, r=np.max(lengths_from_ICOHPs) + 0.5, @@ -977,8 +975,14 @@ def _find_environments( copied_neighbors_from_ICOHPs = copy.copy(neighbors_from_ICOHPs) copied_distances_from_ICOHPs = copy.copy(lengths_from_ICOHPs) copied_translations_from_ICOHPs = copy.copy(translations_ICOHPs) + copied_icohps_from_ICOHPs = copy.copy(selected_ICOHPs) + copied_keys_from_ICOHPs = copy.copy(keys_from_ICOHPs) _neigh_coords = [] _neigh_frac_coords = [] + _list_icohps=[] + _list_lengths=[] + _list_keys=[] + _list_translations=[] for neigh_idx, neigh in enumerate(neighbors_by_distance): index_here2 = index_here_list[neigh_idx] @@ -990,20 +994,20 @@ def _find_environments( and ( ( copied_translations_from_ICOHPs[dist_idx][0] - == translations_by_distance[neigh_idx][0] + == -translations_by_distance[neigh_idx][0] and copied_translations_from_ICOHPs[dist_idx][1] - == translations_by_distance[neigh_idx][1] + == -translations_by_distance[neigh_idx][1] and copied_translations_from_ICOHPs[dist_idx][2] - == translations_by_distance[neigh_idx][2] + == -translations_by_distance[neigh_idx][2] ) - # or ( - # copied_translations_from_ICOHPs[dist_idx][0] - # == -translations_by_distance[neigh_idx][0] - # and copied_translations_from_ICOHPs[dist_idx][1] - # == -translations_by_distance[neigh_idx][1] - # and copied_translations_from_ICOHPs[dist_idx][2] - # == -translations_by_distance[neigh_idx][2] - # ) + or ( + copied_translations_from_ICOHPs[dist_idx][0] + == translations_by_distance[neigh_idx][0] + and copied_translations_from_ICOHPs[dist_idx][1] + == translations_by_distance[neigh_idx][1] + and copied_translations_from_ICOHPs[dist_idx][2] + == translations_by_distance[neigh_idx][2] + ) ) ) else: @@ -1011,45 +1015,31 @@ def _find_environments( np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 ) - # comparison = ( - # np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) - # and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 - # and ( - # ( - # copied_translations_from_ICOHPs[dist_idx][0] == - - # translations_by_distance[neigh_idx][0] - # and copied_translations_from_ICOHPs[dist_idx][1] == - - # translations_by_distance[neigh_idx][1] - # and copied_translations_from_ICOHPs[dist_idx][2] == - - # translations_by_distance[neigh_idx][2] - # ) - # or ( - # copied_translations_from_ICOHPs[dist_idx][0] == - # translations_by_distance[neigh_idx][0] - # and copied_translations_from_ICOHPs[dist_idx][1] == - # translations_by_distance[neigh_idx][1] - # and copied_translations_from_ICOHPs[dist_idx][2] == - # translations_by_distance[neigh_idx][2] - # ) - # ) - # ) if comparison: _list_neighsite.append(neigh) _list_neighisite.append(index_here2) _neigh_coords.append(coords[neigh_idx]) _neigh_frac_coords.append(neigh.frac_coords) + _list_icohps.append(copied_icohps_from_ICOHPs[dist_idx]) + _list_lengths.append(copied_distances_from_ICOHPs[dist_idx]) + _list_keys.append(copied_keys_from_ICOHPs[dist_idx]) + _list_translations.append(copied_translations_from_ICOHPs[dist_idx]) del copied_distances_from_ICOHPs[dist_idx] del copied_neighbors_from_ICOHPs[dist_idx] del copied_translations_from_ICOHPs[dist_idx] + del copied_icohps_from_ICOHPs[dist_idx] + del copied_keys_from_ICOHPs[dist_idx] break list_neighisite.append(_list_neighisite) list_neighsite.append(_list_neighsite) - list_lengths.append(lengths_from_ICOHPs) - list_keys.append(keys_from_ICOHPs) + list_lengths.append(_list_lengths) + list_keys.append(_list_keys) list_coords.append(_neigh_coords) - list_icohps.append(selected_ICOHPs) + list_icohps.append(_list_icohps) + print(list_icohps) + print(list_lengths) else: list_neighsite.append([]) From 638ba850019ffd31bba96ccc377892434d400ae3 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Fri, 7 Feb 2025 08:15:52 +0100 Subject: [PATCH 116/180] add more fixes --- tests/io/lobster/test_lobsterenv.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 322fa1f3dd..1026205845 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -709,14 +709,14 @@ def test_get_info_icohps_neighbors(self): for bond in results[1]: assert bond == approx(-5.64612, abs=1e-2) assert results[2] == 6 - assert results[3] == ["27", "30", "48", "49", "64", "73"] + assert results[3] == ['48', '27', '64', '73', '49', '30'] results2 = self.chem_env_lobster1.get_info_icohps_to_neighbors(isites=None) assert results2[0] == approx(-33.87452) for bond in results2[1]: assert bond == approx(-5.64455, abs=1e-2) assert results2[2] == 6 - assert results2[3] == ["27", "30", "48", "49", "64", "73"] + assert results2[3] == ['48', '27', '64', '73', '49', '30'] assert results2[4] == [ ["Re1", "O2"], ["Re1", "O2"], @@ -924,6 +924,6 @@ def test_valences(self): ] assert_allclose( - self.chem_env_w_obj.valences == [0.66] * 4 + [0.69] * 4 + [-0.68] * 3 + [-0.69] + [-0.67] * 4 + self.chem_env_w_obj.valences , [0.66] * 4 + [0.69] * 4 + [-0.68] * 3 + [-0.69] + [-0.67] * 4 ) # charge_obj assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA From 615bfe499289909aef455b166f746a0f479acde6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 07:18:16 +0000 Subject: [PATCH 117/180] pre-commit auto-fixes --- lobsterenv.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index f6107adc53..16d4c2a17e 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -979,10 +979,10 @@ def _find_environments( copied_keys_from_ICOHPs = copy.copy(keys_from_ICOHPs) _neigh_coords = [] _neigh_frac_coords = [] - _list_icohps=[] - _list_lengths=[] - _list_keys=[] - _list_translations=[] + _list_icohps = [] + _list_lengths = [] + _list_keys = [] + _list_translations = [] for neigh_idx, neigh in enumerate(neighbors_by_distance): index_here2 = index_here_list[neigh_idx] @@ -1000,14 +1000,14 @@ def _find_environments( and copied_translations_from_ICOHPs[dist_idx][2] == -translations_by_distance[neigh_idx][2] ) - or ( - copied_translations_from_ICOHPs[dist_idx][0] - == translations_by_distance[neigh_idx][0] - and copied_translations_from_ICOHPs[dist_idx][1] - == translations_by_distance[neigh_idx][1] - and copied_translations_from_ICOHPs[dist_idx][2] - == translations_by_distance[neigh_idx][2] - ) + or ( + copied_translations_from_ICOHPs[dist_idx][0] + == translations_by_distance[neigh_idx][0] + and copied_translations_from_ICOHPs[dist_idx][1] + == translations_by_distance[neigh_idx][1] + and copied_translations_from_ICOHPs[dist_idx][2] + == translations_by_distance[neigh_idx][2] + ) ) ) else: @@ -1022,7 +1022,7 @@ def _find_environments( _neigh_coords.append(coords[neigh_idx]) _neigh_frac_coords.append(neigh.frac_coords) _list_icohps.append(copied_icohps_from_ICOHPs[dist_idx]) - _list_lengths.append(copied_distances_from_ICOHPs[dist_idx]) + _list_lengths.append(dist) _list_keys.append(copied_keys_from_ICOHPs[dist_idx]) _list_translations.append(copied_translations_from_ICOHPs[dist_idx]) del copied_distances_from_ICOHPs[dist_idx] From ca2b8fefabfd6bc0fd7f2ba88c7203ba22d07db0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 07:18:16 +0000 Subject: [PATCH 118/180] pre-commit auto-fixes --- tests/io/lobster/test_lobsterenv.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 1026205845..6dc0ba3b20 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -709,14 +709,14 @@ def test_get_info_icohps_neighbors(self): for bond in results[1]: assert bond == approx(-5.64612, abs=1e-2) assert results[2] == 6 - assert results[3] == ['48', '27', '64', '73', '49', '30'] + assert results[3] == ["48", "27", "64", "73", "49", "30"] results2 = self.chem_env_lobster1.get_info_icohps_to_neighbors(isites=None) assert results2[0] == approx(-33.87452) for bond in results2[1]: assert bond == approx(-5.64455, abs=1e-2) assert results2[2] == 6 - assert results2[3] == ['48', '27', '64', '73', '49', '30'] + assert results2[3] == ["48", "27", "64", "73", "49", "30"] assert results2[4] == [ ["Re1", "O2"], ["Re1", "O2"], @@ -924,6 +924,6 @@ def test_valences(self): ] assert_allclose( - self.chem_env_w_obj.valences , [0.66] * 4 + [0.69] * 4 + [-0.68] * 3 + [-0.69] + [-0.67] * 4 + self.chem_env_w_obj.valences, [0.66] * 4 + [0.69] * 4 + [-0.68] * 3 + [-0.69] + [-0.67] * 4 ) # charge_obj assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA From d66c4f96e88c06248aa3223ac5ac4ae7fcc8a228 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Fri, 7 Feb 2025 09:36:17 +0100 Subject: [PATCH 119/180] fix the just introduced bug and add a warning for very rare cases that we do not cover yet --- lobsterenv.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index f6107adc53..4633e3f0c1 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -16,7 +16,7 @@ import math import tempfile from typing import TYPE_CHECKING, NamedTuple - +import warnings import matplotlib as mpl import numpy as np from monty.dev import deprecated @@ -940,6 +940,8 @@ def _find_environments( translations_ICOHPs, ) = additional_conds + check_ICOHPs(lengths_from_ICOHPs=lengths_from_ICOHPs,selected_ICOHPs=selected_ICOHPs, translation=translations_ICOHPs) + if len(neighbors_from_ICOHPs) > 0: centralsite = site neighbors_by_distance_start = self.structure.get_sites_in_sphere( @@ -977,6 +979,7 @@ def _find_environments( copied_translations_from_ICOHPs = copy.copy(translations_ICOHPs) copied_icohps_from_ICOHPs = copy.copy(selected_ICOHPs) copied_keys_from_ICOHPs = copy.copy(keys_from_ICOHPs) + print(copied_keys_from_ICOHPs) _neigh_coords = [] _neigh_frac_coords = [] _list_icohps=[] @@ -1038,9 +1041,8 @@ def _find_environments( list_keys.append(_list_keys) list_coords.append(_neigh_coords) list_icohps.append(_list_icohps) - print(list_icohps) - print(list_lengths) - + print(_list_keys) + print(keys_from_ICOHPs) else: list_neighsite.append([]) list_neighisite.append([]) @@ -1048,6 +1050,7 @@ def _find_environments( list_lengths.append([]) list_keys.append([]) list_coords.append([]) + return ( list_icohps, list_keys, @@ -1574,3 +1577,17 @@ class ICOHPNeighborsInfo(NamedTuple): labels: list[str] atoms: list[list[str]] central_isites: list[int] | None + + + + + +def check_ICOHPs(lengths_from_ICOHPs, selected_ICOHPs, translation, length_threshold=0.01, energy_threshold=0.1): + for i in range(len(lengths_from_ICOHPs)): + for j in range(i + 1, len(lengths_from_ICOHPs)): + if abs(lengths_from_ICOHPs[i] - lengths_from_ICOHPs[j]) < length_threshold: + if abs(selected_ICOHPs[i] - selected_ICOHPs[j]) > energy_threshold: + if translation[i][0]==-translation[j][0] and translation[i][1]==-translation[j][1] and translation[i][2]==-translation[j][2]: + warnings.warn(f"Lengths {lengths_from_ICOHPs[i]} and {lengths_from_ICOHPs[j]} are very close " + f"and translation exactly opposite, but corresponding ICOHPs {selected_ICOHPs[i]} " + f"and {selected_ICOHPs[j]} are not. Our neighbor detection might fail.") From baf8b55914f1a88b797ea9690cb8f67c4218e4ec Mon Sep 17 00:00:00 2001 From: JaGeo Date: Fri, 7 Feb 2025 09:36:17 +0100 Subject: [PATCH 120/180] fix the just introduced bug and add a warning for very rare cases that we do not cover yet --- tests/io/lobster/test_lobsterenv.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 1026205845..a84d026e6a 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -717,14 +717,10 @@ def test_get_info_icohps_neighbors(self): assert bond == approx(-5.64455, abs=1e-2) assert results2[2] == 6 assert results2[3] == ['48', '27', '64', '73', '49', '30'] - assert results2[4] == [ - ["Re1", "O2"], - ["Re1", "O2"], - ["Re1", "O3"], - ["Re1", "O3"], - ["Re1", "O4"], - ["Re1", "O4"], - ] + from collections import Counter + assert Counter(map(tuple, results2[4])) == Counter([ + ('Re1', 'O2'), ('Re1', 'O2'), ('Re1', 'O3'), ('Re1', 'O3'), ('Re1', 'O4'), ('Re1', 'O4') + ]) def test_get_sum_icohps_between_neighbors_of_atom(self): # will only look at icohps between cations or anions @@ -803,11 +799,11 @@ def test_get_plot_label(self): def test_get_info_cohps_to_neighbors(self): chem_env_lobster1 = LobsterNeighbors( are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190_2.gz", + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), additional_condition=1, ) - cohpcar_lobster_mp_190 = f"{TEST_DIR}/COHPCAR.lobster.mp-190_2.gz" + cohpcar_lobster_mp_190 = f"{TEST_DIR}/COHPCAR.lobster.mp-190.gz" plot_label, summed_cohpcar_mp_190 = chem_env_lobster1.get_info_cohps_to_neighbors( path_to_cohpcar=cohpcar_lobster_mp_190, isites=[0], @@ -822,7 +818,10 @@ def test_get_info_cohps_to_neighbors(self): only_bonds_to=None, per_bond=False, )[1] - assert np.sum([coph_thing.icohp[Spin.up], coph_thing.icohp[Spin.down]], axis=0)[300] == approx( + print(coph_thing.icohp[Spin.up][300]) + print(np.sum(coph_thing.cohp[Spin.up][300])) + print(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])) + assert coph_thing.icohp[Spin.up][300] == approx( chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0] ) From f0e126fa4d6d9cd5f009f9eadcfe17e5d841baee Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:41:19 +0000 Subject: [PATCH 121/180] pre-commit auto-fixes --- tests/io/lobster/test_lobsterenv.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index bf995d0606..a2c06bd98a 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -717,12 +717,12 @@ def test_get_info_icohps_neighbors(self): assert bond == approx(-5.64455, abs=1e-2) assert results2[2] == 6 - assert results2[3] == ['48', '27', '64', '73', '49', '30'] + assert results2[3] == ["48", "27", "64", "73", "49", "30"] from collections import Counter - assert Counter(map(tuple, results2[4])) == Counter([ - ('Re1', 'O2'), ('Re1', 'O2'), ('Re1', 'O3'), ('Re1', 'O3'), ('Re1', 'O4'), ('Re1', 'O4') - ]) + assert Counter(map(tuple, results2[4])) == Counter( + [("Re1", "O2"), ("Re1", "O2"), ("Re1", "O3"), ("Re1", "O3"), ("Re1", "O4"), ("Re1", "O4")] + ) def test_get_sum_icohps_between_neighbors_of_atom(self): # will only look at icohps between cations or anions @@ -823,9 +823,7 @@ def test_get_info_cohps_to_neighbors(self): print(coph_thing.icohp[Spin.up][300]) print(np.sum(coph_thing.cohp[Spin.up][300])) print(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])) - assert coph_thing.icohp[Spin.up][300] == approx( - chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0] - ) + assert coph_thing.icohp[Spin.up][300] == approx(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0]) # summed_spin_channel coph_thing = chem_env_lobster1.get_info_cohps_to_neighbors( From 0c469924b6863eafb16746edda58d63314003f96 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:41:19 +0000 Subject: [PATCH 122/180] pre-commit auto-fixes --- lobsterenv.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 9f95c396a7..0f05e94f99 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -15,8 +15,9 @@ import copy import math import tempfile -from typing import TYPE_CHECKING, NamedTuple import warnings +from typing import TYPE_CHECKING, NamedTuple + import matplotlib as mpl import numpy as np from monty.dev import deprecated @@ -940,7 +941,11 @@ def _find_environments( translations_ICOHPs, ) = additional_conds - check_ICOHPs(lengths_from_ICOHPs=lengths_from_ICOHPs,selected_ICOHPs=selected_ICOHPs, translation=translations_ICOHPs) + check_ICOHPs( + lengths_from_ICOHPs=lengths_from_ICOHPs, + selected_ICOHPs=selected_ICOHPs, + translation=translations_ICOHPs, + ) if len(neighbors_from_ICOHPs) > 0: centralsite = site @@ -1579,15 +1584,19 @@ class ICOHPNeighborsInfo(NamedTuple): central_isites: list[int] | None - - - def check_ICOHPs(lengths_from_ICOHPs, selected_ICOHPs, translation, length_threshold=0.01, energy_threshold=0.1): for i in range(len(lengths_from_ICOHPs)): for j in range(i + 1, len(lengths_from_ICOHPs)): if abs(lengths_from_ICOHPs[i] - lengths_from_ICOHPs[j]) < length_threshold: if abs(selected_ICOHPs[i] - selected_ICOHPs[j]) > energy_threshold: - if translation[i][0]==-translation[j][0] and translation[i][1]==-translation[j][1] and translation[i][2]==-translation[j][2]: - warnings.warn(f"Lengths {lengths_from_ICOHPs[i]} and {lengths_from_ICOHPs[j]} are very close " - f"and translation exactly opposite, but corresponding ICOHPs {selected_ICOHPs[i]} " - f"and {selected_ICOHPs[j]} are not. Our neighbor detection might fail.") + if ( + translation[i][0] == -translation[j][0] + and translation[i][1] == -translation[j][1] + and translation[i][2] == -translation[j][2] + ): + warnings.warn( + f"Lengths {lengths_from_ICOHPs[i]} and {lengths_from_ICOHPs[j]} are very close " + f"and translation exactly opposite, but corresponding ICOHPs {selected_ICOHPs[i]} " + f"and {selected_ICOHPs[j]} are not. Our neighbor detection might fail.", + stacklevel=2, + ) From 4263230c85041d078e14d083f8d9ba5a0edd1bda Mon Sep 17 00:00:00 2001 From: JaGeo Date: Fri, 7 Feb 2025 09:43:44 +0100 Subject: [PATCH 123/180] fix next test by adapting cutoff --- tests/io/lobster/test_lobsterenv.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index bf995d0606..4787cf92c8 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -739,11 +739,11 @@ def test_get_sum_icohps_between_neighbors_of_atom(self): filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-7000.gz", structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-7000.gz"), additional_condition=1, - perc_strength_icohp=0.15, + perc_strength_icohp=0.05, noise_cutoff=0.0, ) - print(chemenv_here.get_info_icohps_between_neighbors(isites=[0])) - assert len(chemenv_here.get_info_icohps_between_neighbors(isites=[0])[4]) == 6 + + assert len(chemenv_here.get_info_icohps_between_neighbors(isites=[0])[4]) == 2 def test_get_plot_label(self): label = self.chem_env_lobster1._get_plot_label( From 04f3220b3b5982f7b4a6f9363b8f862fc5c9025b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:44:38 +0000 Subject: [PATCH 124/180] pre-commit auto-fixes --- lobsterenv.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 0f05e94f99..ccde5d60aa 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -1588,15 +1588,14 @@ def check_ICOHPs(lengths_from_ICOHPs, selected_ICOHPs, translation, length_thres for i in range(len(lengths_from_ICOHPs)): for j in range(i + 1, len(lengths_from_ICOHPs)): if abs(lengths_from_ICOHPs[i] - lengths_from_ICOHPs[j]) < length_threshold: - if abs(selected_ICOHPs[i] - selected_ICOHPs[j]) > energy_threshold: - if ( - translation[i][0] == -translation[j][0] - and translation[i][1] == -translation[j][1] - and translation[i][2] == -translation[j][2] - ): - warnings.warn( - f"Lengths {lengths_from_ICOHPs[i]} and {lengths_from_ICOHPs[j]} are very close " - f"and translation exactly opposite, but corresponding ICOHPs {selected_ICOHPs[i]} " - f"and {selected_ICOHPs[j]} are not. Our neighbor detection might fail.", - stacklevel=2, - ) + if abs(selected_ICOHPs[i] - selected_ICOHPs[j]) > energy_threshold and ( + translation[i][0] == -translation[j][0] + and translation[i][1] == -translation[j][1] + and translation[i][2] == -translation[j][2] + ): + warnings.warn( + f"Lengths {lengths_from_ICOHPs[i]} and {lengths_from_ICOHPs[j]} are very close " + f"and translation exactly opposite, but corresponding ICOHPs {selected_ICOHPs[i]} " + f"and {selected_ICOHPs[j]} are not. Our neighbor detection might fail.", + stacklevel=2, + ) From 9c9dbf2fd601a250829314434da8fca51356e057 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Fri, 7 Feb 2025 10:12:52 +0100 Subject: [PATCH 125/180] fix some bugs --- lobsterenv.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 0f05e94f99..3caa9a3e0c 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -537,7 +537,6 @@ def get_info_cohps_to_neighbors( _summed_icohps, _list_icohps, _number_bonds, labels, atoms, final_isites = self.get_info_icohps_to_neighbors( isites=isites, onlycation_isites=onlycation_isites ) - print(_list_icohps) with tempfile.TemporaryDirectory() as tmp_dir: path = f"{tmp_dir}/POSCAR.vasp" @@ -984,7 +983,6 @@ def _find_environments( copied_translations_from_ICOHPs = copy.copy(translations_ICOHPs) copied_icohps_from_ICOHPs = copy.copy(selected_ICOHPs) copied_keys_from_ICOHPs = copy.copy(keys_from_ICOHPs) - print(copied_keys_from_ICOHPs) _neigh_coords = [] _neigh_frac_coords = [] _list_icohps = [] @@ -1046,8 +1044,6 @@ def _find_environments( list_keys.append(_list_keys) list_coords.append(_neigh_coords) list_icohps.append(_list_icohps) - print(_list_keys) - print(keys_from_ICOHPs) else: list_neighsite.append([]) list_neighisite.append([]) From 6f7ffbb76caf61d60a99284609d93d16a9fa6f50 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Fri, 7 Feb 2025 10:12:52 +0100 Subject: [PATCH 126/180] fix some bugs --- tests/io/lobster/test_lobsterenv.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 00487b3219..d72ee90a3c 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -814,16 +814,15 @@ def test_get_info_cohps_to_neighbors(self): assert plot_label == "6 x O-Re (per bond)" assert isinstance(summed_cohpcar_mp_190, Cohp) - coph_thing = chem_env_lobster1.get_info_cohps_to_neighbors( + cohp_result = chem_env_lobster1.get_info_cohps_to_neighbors( path_to_cohpcar=cohpcar_lobster_mp_190, isites=[0], only_bonds_to=None, per_bond=False, )[1] - print(coph_thing.icohp[Spin.up][300]) - print(np.sum(coph_thing.cohp[Spin.up][300])) - print(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])) - assert coph_thing.icohp[Spin.up][300] == approx(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0]) + print(cohp_result.icohp[Spin.up][700]) + print(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0]) + assert cohp_result.icohp[Spin.up][700] == approx(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0]) # summed_spin_channel coph_thing = chem_env_lobster1.get_info_cohps_to_neighbors( @@ -833,7 +832,7 @@ def test_get_info_cohps_to_neighbors(self): per_bond=False, summed_spin_channels=True, )[1] - assert coph_thing.icohp[Spin.up][300] == approx(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0]) + assert coph_thing.icohp[Spin.up][700] == approx(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0]) plot_label, summed_cohpcar_mp_190_Te = chem_env_lobster1.get_info_cohps_to_neighbors( path_to_cohpcar=cohpcar_lobster_mp_190, @@ -893,7 +892,7 @@ def test_get_info_cohps_to_neighbors(self): with pytest.raises(ValueError, match=expected_msg): # icohplist and cohpcar do not fit together self.chem_env_lobster1.get_info_cohps_to_neighbors( - path_to_cohpcar=cohpcar_lobster_mp_190, + path_to_cohpcar=f"{TEST_DIR}/COHPCAR.lobster.mp-190_2.gz", isites=[0], only_bonds_to=None, per_bond=False, @@ -902,7 +901,7 @@ def test_get_info_cohps_to_neighbors(self): with pytest.raises(ValueError, match=expected_msg): # icohplist and cohpcar do not fit together self.chem_env_lobster2.get_info_cohps_to_neighbors( - path_to_cohpcar=cohpcar_lobster_mp_190, + path_to_cohpcar=f"{TEST_DIR}/COHPCAR.lobster.mp-190_2.gz", isites=[0], only_bonds_to=None, per_bond=False, From cc2d5a3b4014d9243cb5bc5739bf1af9cb9f3c9d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 09:24:44 +0000 Subject: [PATCH 127/180] pre-commit auto-fixes --- tests/io/lobster/test_lobsterenv.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index d72ee90a3c..f65b739413 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -2,7 +2,6 @@ from unittest import TestCase -import numpy as np import pytest from numpy.testing import assert_allclose from pytest import approx From 9f67623571a6698133b4406f95510a13ca42e167 Mon Sep 17 00:00:00 2001 From: JaGeo Date: Fri, 7 Feb 2025 10:39:13 +0100 Subject: [PATCH 128/180] remove translation correction --- lobsterenv.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index fdf03e4791..38696e92f3 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -1109,7 +1109,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(-icohp.translation) + translation_from_ICOHPs.append(icohp.translation) # ONLY_ANION_CATION_BONDS elif additional_condition == 1: @@ -1126,7 +1126,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(-icohp.translation) + translation_from_ICOHPs.append(icohp.translation) # NO_ELEMENT_TO_SAME_ELEMENT_BONDS elif additional_condition == 2: @@ -1143,7 +1143,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(-icohp.translation) + translation_from_ICOHPs.append(icohp.translation) # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS elif additional_condition == 3: @@ -1162,7 +1162,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(-icohp.translation) + translation_from_ICOHPs.append(icohp.translation) # ONLY_ELEMENT_TO_OXYGEN_BONDS elif additional_condition == 4: @@ -1179,7 +1179,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(-icohp.translation) + translation_from_ICOHPs.append(icohp.translation) # DO_NOT_CONSIDER_ANION_CATION_BONDS elif additional_condition == 5: @@ -1196,7 +1196,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(-icohp.translation) + translation_from_ICOHPs.append(icohp.translation) # ONLY_CATION_CATION_BONDS elif additional_condition == 6 and val1 > 0.0 and val2 > 0.0: # type: ignore[operator] @@ -1212,7 +1212,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs.append(icohp._length) icohps_from_ICOHPs.append(icohp.summed_icohp) keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(-icohp.translation) + translation_from_ICOHPs.append(icohp.translation) return ( keys_from_ICOHPs, From fcf279f34626706cef1053080b476540ab306e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haoyu=20=28Daniel=29=20YANG=20=E6=9D=A8=E6=B5=A9=E5=AE=87?= Date: Mon, 17 Mar 2025 14:39:07 +0100 Subject: [PATCH 129/180] Fix new `ruff` error in linting pipeline (#4327) * ruff autofix TC006: runtime cast value * autofix PLC1802: len(var) used as condition without comparison * ignore SIM905 for now * fix LOG015: log directly on root logger * NEED CONFIRM: avoid configuring the root logger * fix RUF040 Non-string literal used as assert message, likely many tests are not working correctly * remove todo tag in favor of a separate PR --- tests/io/lobster/test_inputs.py | 44 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index 9c407d41bf..c7f8c4dd38 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -183,7 +183,7 @@ def test_standard_settings(self): assert "skipcohp" not in lobsterin1 assert "skipcoop" not in lobsterin1 if option == "standard_from_projection": - assert lobsterin1["loadProjectionFromFile"], True + assert lobsterin1["loadProjectionFromFile"] if option in [ "onlyprojection", "onlycohp", @@ -192,33 +192,33 @@ def test_standard_settings(self): "onlycohpcoop", "onlycohpcoopcobi", ]: - assert lobsterin1["skipdos"], True - assert lobsterin1["skipPopulationAnalysis"], True - assert lobsterin1["skipGrossPopulation"], True - assert lobsterin1["skipMadelungEnergy"], True + assert lobsterin1["skipdos"] + assert lobsterin1["skipPopulationAnalysis"] + assert lobsterin1["skipGrossPopulation"] + assert lobsterin1["skipMadelungEnergy"] if option == "onlydos": - assert lobsterin1["skipPopulationAnalysis"], True - assert lobsterin1["skipGrossPopulation"], True - assert lobsterin1["skipcohp"], True - assert lobsterin1["skipcoop"], True - assert lobsterin1["skipcobi"], True - assert lobsterin1["skipMadelungEnergy"], True + assert lobsterin1["skipPopulationAnalysis"] + assert lobsterin1["skipGrossPopulation"] + assert lobsterin1["skipcohp"] + assert lobsterin1["skipcoop"] + assert lobsterin1["skipcobi"] + assert lobsterin1["skipMadelungEnergy"] if option == "onlycohp": - assert lobsterin1["skipcoop"], True - assert lobsterin1["skipcobi"], True + assert lobsterin1["skipcoop"] + assert lobsterin1["skipcobi"] if option == "onlycoop": - assert lobsterin1["skipcohp"], True - assert lobsterin1["skipcobi"], True + assert lobsterin1["skipcohp"] + assert lobsterin1["skipcobi"] if option == "onlyprojection": - assert lobsterin1["skipdos"], True + assert lobsterin1["skipdos"] if option == "onlymadelung": - assert lobsterin1["skipPopulationAnalysis"], True - assert lobsterin1["skipGrossPopulation"], True - assert lobsterin1["skipcohp"], True - assert lobsterin1["skipcoop"], True - assert lobsterin1["skipcobi"], True - assert lobsterin1["skipdos"], True + assert lobsterin1["skipPopulationAnalysis"] + assert lobsterin1["skipGrossPopulation"] + assert lobsterin1["skipcohp"] + assert lobsterin1["skipcoop"] + assert lobsterin1["skipcobi"] + assert lobsterin1["skipdos"] # test basis functions by dict lobsterin_new = Lobsterin.standard_calculations_from_vasp_files( f"{VASP_IN_DIR}/POSCAR_Fe3O4", From 59e10bc15a0af497ab1eb82e9087aba763b7aace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haoyu=20=28Daniel=29=20YANG=20=E6=9D=A8=E6=B5=A9=E5=AE=87?= Date: Mon, 17 Mar 2025 14:39:07 +0100 Subject: [PATCH 130/180] Fix new `ruff` error in linting pipeline (#4327) * ruff autofix TC006: runtime cast value * autofix PLC1802: len(var) used as condition without comparison * ignore SIM905 for now * fix LOG015: log directly on root logger * NEED CONFIRM: avoid configuring the root logger * fix RUF040 Non-string literal used as assert message, likely many tests are not working correctly * remove todo tag in favor of a separate PR --- outputs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/outputs.py b/outputs.py index 90291feec7..85f6d7bf47 100644 --- a/outputs.py +++ b/outputs.py @@ -31,7 +31,6 @@ from pymatgen.io.vasp.inputs import Kpoints from pymatgen.io.vasp.outputs import Vasprun, VolumetricData from pymatgen.util.due import Doi, due -from pymatgen.util.typing import PathLike if TYPE_CHECKING: from typing import Any, ClassVar, Literal @@ -40,7 +39,7 @@ from pymatgen.core.structure import IStructure from pymatgen.electronic_structure.cohp import IcohpCollection - from pymatgen.util.typing import Tuple3Ints, Vector3D + from pymatgen.util.typing import PathLike, Tuple3Ints, Vector3D __author__ = "Janine George, Marco Esters" __copyright__ = "Copyright 2017, The Materials Project" @@ -1439,7 +1438,7 @@ def __init__( filenames_new.append(os.path.join(filenames, name)) filenames = filenames_new # type: ignore[assignment] - filenames = cast(list[PathLike], filenames) + filenames = cast("list[PathLike]", filenames) if len(filenames) == 0: raise ValueError("No FATBAND files in folder or given") From a7a5a76935c6c948f9031ed1cc9e7171c2c3ef24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haoyu=20=28Daniel=29=20YANG=20=E6=9D=A8=E6=B5=A9=E5=AE=87?= Date: Thu, 17 Apr 2025 16:43:53 +0200 Subject: [PATCH 131/180] Deprecate `PymatgenTest`, migrate tests to `pytest` from `unittest` (#4212) * revert for a separate migration PR * Revert "revert for a separate migration PR" This reverts commit 6f95ba8890000b88ca790bbada4ffbfcd06b042c. * remove hard-coded and failing test * batch remove PymatgenTest inheritance and see what fail * migrate alchemy and command_line * deprecate pymatgen with matscitest * migrate utils * migrate optimization * migrate vis * global replacement of setUp and TearDown, setupClass * revert all changes to before global replace PymatgenTest * deprecate pymatgen with matscitest * global replace setUp with setup_method * replace tearDown * replace setUpClass(cls) with setup_class(cls) * global remove inheritance from TestCase * global replace inheritance from PymatgenTest * global replace PymatgenTest import * global replace get_structure * global replace PymatgenTest with MatSciTest * fix tests * fix internal index and add require for pytest style * fix test * remove seemingly unused conftest * add deprecated decorator and enhance docstring * inherit to reduce code duplicate * add some test for legacy tester * also check future warning * remove unnecessary chdir * prep for merge * fix bad merge behaviour * pre-commit auto-fixes * fix merge * migrate recently added tests --------- Signed-off-by: Haoyu (Daniel) YANG Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- tests/io/lobster/test_inputs.py | 8 ++-- tests/io/lobster/test_lobsterenv.py | 6 +-- tests/io/lobster/test_outputs.py | 61 ++++++++++++++--------------- 3 files changed, 36 insertions(+), 39 deletions(-) diff --git a/tests/io/lobster/test_inputs.py b/tests/io/lobster/test_inputs.py index c7f8c4dd38..0c3a8e2f2f 100644 --- a/tests/io/lobster/test_inputs.py +++ b/tests/io/lobster/test_inputs.py @@ -8,7 +8,7 @@ from pymatgen.io.lobster import Lobsterin from pymatgen.io.lobster.inputs import get_all_possible_basis_combinations from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar -from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, PymatgenTest +from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, MatSciTest TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp" @@ -19,8 +19,8 @@ __date__ = "Dec 10, 2017" -class TestLobsterin(PymatgenTest): - def setUp(self): +class TestLobsterin(MatSciTest): + def setup_method(self): self.Lobsterin = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.1") self.Lobsterin2 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.2") self.Lobsterin3 = Lobsterin.from_file(f"{TEST_DIR}/lobsterin.3") @@ -579,7 +579,7 @@ def test_as_from_dict(self): new_lobsterin.to_json() -class TestUtils(PymatgenTest): +class TestUtils(MatSciTest): def test_get_all_possible_basis_combinations(self): # this basis is just for testing (not correct) min_basis = ["Li 1s 2s ", "Na 1s 2s", "Si 1s 2s"] diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 1d56695803..35160ca4d9 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -1,7 +1,5 @@ from __future__ import annotations -from unittest import TestCase - import numpy as np import pytest from numpy.testing import assert_allclose @@ -25,8 +23,8 @@ TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp/environments" -class TestLobsterNeighbors(TestCase): - def setUp(self): +class TestLobsterNeighbors: + def setup_method(self): # test additional conditions first # only consider cation anion bonds diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index 3b60027117..3f63cff41c 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -3,7 +3,6 @@ import copy import json import os -from unittest import TestCase import numpy as np import pytest @@ -31,7 +30,7 @@ Wavefunction, ) from pymatgen.io.vasp import Vasprun -from pymatgen.util.testing import TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, PymatgenTest +from pymatgen.util.testing import TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, MatSciTest TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp" @@ -42,8 +41,8 @@ __date__ = "Dec 10, 2017" -class TestBwdf(PymatgenTest): - def setUp(self): +class TestBwdf(MatSciTest): + def setup_method(self): self.bwdf_coop = Bwdf(filename=f"{TEST_DIR}/BWDF.lobster.AlN.gz") self.bwdf_cohp = Bwdf(filename=f"{TEST_DIR}/BWDFCOHP.lobster.NaCl.gz") @@ -57,8 +56,8 @@ def test_attributes(self): assert self.bwdf_cohp.bwdf[Spin.up][103] == approx(-0.01392, abs=1e-4) -class TestCohpcar(PymatgenTest): - def setUp(self): +class TestCohpcar(MatSciTest): + def setup_method(self): self.cohp_bise = Cohpcar(filename=f"{TEST_DIR}/COHPCAR.lobster.BiSe.gz") self.coop_bise = Cohpcar( filename=f"{TEST_DIR}/COOPCAR.lobster.BiSe.gz", @@ -398,8 +397,8 @@ def test_orbital_resolved_cohp(self): assert len(self.cobi6.orb_res_cohp["21"]["2py-1s-2s"]["COHP"][Spin.down]) == 12 -class TestDoscar(TestCase): - def setUp(self): +class TestDoscar: + def setup_method(self): # first for spin polarized version doscar = f"{VASP_OUT_DIR}/DOSCAR.lobster.spin" poscar = f"{VASP_IN_DIR}/POSCAR.lobster.spin_DOS" @@ -637,8 +636,8 @@ def test_is_spin_polarized(self): assert not self.DOSCAR_nonspin_pol.is_spin_polarized -class TestCharge(PymatgenTest): - def setUp(self): +class TestCharge(MatSciTest): + def setup_method(self): self.charge2 = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.MnO") # gzipped file self.charge = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.MnO2.gz") @@ -716,8 +715,8 @@ def test_msonable(self): assert getattr(charge_from_dict, attr_name) == attr_value -class TestLobsterout(PymatgenTest): - def setUp(self): +class TestLobsterout(MatSciTest): + def setup_method(self): self.lobsterout_normal = Lobsterout(filename=f"{TEST_DIR}/lobsterout.normal") # make sure .gz files are also read correctly self.lobsterout_normal = Lobsterout(filename=f"{TEST_DIR}/lobsterout.normal2.gz") @@ -1245,8 +1244,8 @@ def test_msonable(self): Lobsterout(filename=None, invalid="val") -class TestFatband(PymatgenTest): - def setUp(self): +class TestFatband(MatSciTest): + def setup_method(self): self.structure = Vasprun( filename=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", ionic_step_skip=None, @@ -1480,8 +1479,8 @@ def test_get_bandstructure(self): assert bs_p_x.get_projection_on_elements()[Spin.up][0][0]["Si"] == approx(3 * (0.001 + 0.064), abs=1e-2) -class TestBandoverlaps(TestCase): - def setUp(self): +class TestBandoverlaps: + def setup_method(self): # test spin-polarized calc and non spin-polarized calc self.band_overlaps1 = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.1") @@ -1716,8 +1715,8 @@ def test_keys(self): assert len(bo_dict_new[Spin.down]["matrices"]) == 73 -class TestGrosspop(TestCase): - def setUp(self): +class TestGrosspop: + def setup_method(self): self.grosspop1 = Grosspop(f"{TEST_DIR}/GROSSPOP.lobster") self.grosspop_511_sp = Grosspop(f"{TEST_DIR}/GROSSPOP_511_sp.lobster.AlN.gz") self.grosspop_511_nsp = Grosspop(f"{TEST_DIR}/GROSSPOP_511_nsp.lobster.NaCl.gz") @@ -1866,8 +1865,8 @@ def test_msonable(self): assert getattr(grosspop_from_dict, attr_name) == attr_value -class TestIcohplist(TestCase): - def setUp(self): +class TestIcohplist: + def setup_method(self): self.icohp_bise = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster.BiSe") self.icoop_bise = Icohplist( filename=f"{TEST_DIR}/ICOOPLIST.lobster.BiSe", @@ -2173,8 +2172,8 @@ def test_msonable(self): assert getattr(icohplist_from_dict, attr_name) == attr_value -class TestNciCobiList(TestCase): - def setUp(self): +class TestNciCobiList: + def setup_method(self): self.ncicobi = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster") self.ncicobi_gz = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.gz") self.ncicobi_no_spin = NciCobiList(filename=f"{TEST_DIR}/NcICOBILIST.lobster.nospin") @@ -2209,7 +2208,7 @@ def test_ncicobilist(self): ) -class TestWavefunction(PymatgenTest): +class TestWavefunction(MatSciTest): def test_parse_file(self): grid, points, real, imaginary, distance = Wavefunction._parse_file( f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz" @@ -2277,8 +2276,8 @@ def test_write_file(self): assert os.path.isfile(density_wavecar_path) -class TestSitePotentials(PymatgenTest): - def setUp(self) -> None: +class TestSitePotentials(MatSciTest): + def setup_method(self) -> None: self.sitepotential = SitePotential(filename=f"{TEST_DIR}/SitePotentials.lobster.perovskite") def test_attributes(self): @@ -2328,8 +2327,8 @@ def test_msonable(self): assert getattr(sitepotential_from_dict, attr_name) == attr_value -class TestMadelungEnergies(PymatgenTest): - def setUp(self) -> None: +class TestMadelungEnergies(MatSciTest): + def setup_method(self) -> None: self.madelungenergies = MadelungEnergies(filename=f"{TEST_DIR}/MadelungEnergies.lobster.perovskite") def test_attributes(self): @@ -2345,8 +2344,8 @@ def test_msonable(self): assert getattr(madelung_from_dict, attr_name) == attr_value -class TestLobsterMatrices(PymatgenTest): - def setUp(self) -> None: +class TestLobsterMatrices(MatSciTest): + def setup_method(self) -> None: self.hamilton_matrices = LobsterMatrices( filename=f"{TEST_DIR}/Na_hamiltonMatrices.lobster.gz", e_fermi=-2.79650354 ) @@ -2495,8 +2494,8 @@ def test_raises(self): self.hamilton_matrices = LobsterMatrices(filename=f"{TEST_DIR}/hamiltonMatrices.lobster") -class TestPolarization(PymatgenTest): - def setUp(self) -> None: +class TestPolarization(MatSciTest): + def setup_method(self) -> None: self.polarization = Polarization(filename=f"{TEST_DIR}/POLARIZATION.lobster.AlN.gz") def test_attributes(self): From 968ea43aefcf855ddc129362603e8cb14f975fca Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Sat, 26 Apr 2025 07:05:22 -0700 Subject: [PATCH 132/180] Cleanup all unnecessary type definitions. There really isn't a need to define things like Tuple3Ints, etc. when it is easier to just read the primitive types. We only introduce type definitions for things that are not easily covered under typical primitives or NDArray. --- inputs.py | 4 ++-- outputs.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/inputs.py b/inputs.py index c83b57cfe4..ae9f10fc92 100644 --- a/inputs.py +++ b/inputs.py @@ -35,7 +35,7 @@ from typing_extensions import Self from pymatgen.core.composition import Composition - from pymatgen.util.typing import PathLike, Tuple3Ints + from pymatgen.util.typing import PathLike MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -458,7 +458,7 @@ def write_KPOINTS( reciprocal_density: int = 100, isym: Literal[-1, 0] = 0, from_grid: bool = False, - input_grid: Tuple3Ints = (5, 5, 5), + input_grid: tuple[int, int, int] = (5, 5, 5), line_mode: bool = True, kpoints_line_density: int = 20, symprec: float = 0.01, diff --git a/outputs.py b/outputs.py index 85f6d7bf47..bc94991de2 100644 --- a/outputs.py +++ b/outputs.py @@ -39,7 +39,7 @@ from pymatgen.core.structure import IStructure from pymatgen.electronic_structure.cohp import IcohpCollection - from pymatgen.util.typing import PathLike, Tuple3Ints, Vector3D + from pymatgen.util.typing import PathLike __author__ = "Janine George, Marco Esters" __copyright__ = "Copyright 2017, The Materials Project" @@ -470,7 +470,7 @@ def __init__( atom1_list: list[str] = [] atom2_list: list[str] = [] lens: list[float] = [] - translations: list[Tuple3Ints] = [] + translations: list[tuple[int, int, int]] = [] nums: list[int] = [] icohps: list[dict[Spin, float]] = [] @@ -1883,7 +1883,7 @@ def __init__(self, filename: PathLike, structure: Structure) -> None: @staticmethod def _parse_file( filename: PathLike, - ) -> tuple[Tuple3Ints, list[Vector3D], list[float], list[float], list[float]]: + ) -> tuple[tuple[int, int, int], list[tuple[float, float, float]], list[float], list[float], list[float]]: """Parse wave function file. Args: @@ -1904,7 +1904,7 @@ def _parse_file( reals = [] imaginaries = [] line_parts = lines[0].split() - grid: Tuple3Ints = (int(line_parts[7]), int(line_parts[8]), int(line_parts[9])) + grid: tuple[int, int, int] = (int(line_parts[7]), int(line_parts[8]), int(line_parts[9])) for line in lines[1:]: line_parts = line.split() @@ -1919,7 +1919,7 @@ def _parse_file( return grid, points, reals, imaginaries, distances - def set_volumetric_data(self, grid: Tuple3Ints, structure: Structure) -> None: + def set_volumetric_data(self, grid: tuple[int, int, int], structure: Structure) -> None: """Create the VolumetricData instances. Args: From bfa8eef2e19514bc7b21df38a560265d99f07516 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Sat, 26 Apr 2025 07:05:22 -0700 Subject: [PATCH 133/180] Cleanup all unnecessary type definitions. There really isn't a need to define things like Tuple3Ints, etc. when it is easier to just read the primitive types. We only introduce type definitions for things that are not easily covered under typical primitives or NDArray. --- cohp.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cohp.py b/cohp.py index 3fc2b2cc46..0cccc4839d 100644 --- a/cohp.py +++ b/cohp.py @@ -37,7 +37,7 @@ from numpy.typing import NDArray from typing_extensions import Self - from pymatgen.util.typing import PathLike, SpinLike, Vector3D + from pymatgen.util.typing import PathLike, SpinLike __author__ = "Marco Esters, Janine George" __copyright__ = "Copyright 2017, The Materials Project" @@ -945,7 +945,7 @@ def __init__( atom1: str, atom2: str, length: float, - translation: Vector3D, + translation: tuple[float, float, float], num: int, icohp: dict[Spin, float], are_coops: bool = False, @@ -958,7 +958,7 @@ def __init__( atom1 (str): The first atom that contributes to the bond. atom2 (str): The second atom that contributes to the bond. length (float): Bond length. - translation (Vector3D): cell translation vector, e.g. (0, 0, 0). + translation (tuple[float, float, float]): cell translation vector, e.g. (0, 0, 0). num (int): The number of equivalent bonds. icohp (dict[Spin, float]): {Spin.up: ICOHP_up, Spin.down: ICOHP_down} are_coops (bool): Whether these are COOPs. @@ -1129,7 +1129,7 @@ def __init__( list_atom1: list[str], list_atom2: list[str], list_length: list[float], - list_translation: list[Vector3D], + list_translation: list[tuple[float, float, float]], list_num: list[int], list_icohp: list[dict[Spin, float]], is_spin_polarized: bool, @@ -1143,7 +1143,7 @@ def __init__( list_atom1 (list[str]): Atom names, e.g. "O1". list_atom2 (list[str]): Atom names, e.g. "O1". list_length (list[float]): Bond lengths in Angstrom. - list_translation (list[Vector3D]): Cell translation vectors. + list_translation (list[tuple[float, float, float]]): Cell translation vectors. list_num (list[int]): Numbers of equivalent bonds, usually 1 starting from LOBSTER 3.0.0. list_icohp (list[dict]): Dicts as {Spin.up: ICOHP_up, Spin.down: ICOHP_down}. is_spin_polarized (bool): Whether the calculation is spin polarized. From 0e756fd8d474677bd388213de7ef34fe19b67737 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Sat, 26 Apr 2025 16:35:15 -0700 Subject: [PATCH 134/180] More typing fixes. --- inputs.py | 5 +++-- lobsterenv.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/inputs.py b/inputs.py index ae9f10fc92..00e055b417 100644 --- a/inputs.py +++ b/inputs.py @@ -35,6 +35,7 @@ from typing_extensions import Self from pymatgen.core.composition import Composition + from pymatgen.core.structure import IStructure from pymatgen.util.typing import PathLike MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -358,7 +359,7 @@ def write_INCAR( @staticmethod def get_basis( - structure: Structure, + structure: Structure | IStructure, potcar_symbols: list[str], address_basis_file: PathLike | None = None, ) -> list[str]: @@ -395,7 +396,7 @@ def get_basis( @staticmethod def get_all_possible_basis_functions( - structure: Structure, + structure: Structure | IStructure, potcar_symbols: list[str], address_basis_file_min: PathLike | None = None, address_basis_file_max: PathLike | None = None, diff --git a/lobsterenv.py b/lobsterenv.py index 81b600452e..eaae21ecc8 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -38,7 +38,7 @@ from numpy.typing import NDArray from typing_extensions import Self - from pymatgen.core import PeriodicNeighbor, PeriodicSite, Structure + from pymatgen.core import IStructure, PeriodicNeighbor, PeriodicSite, Structure from pymatgen.core.periodic_table import Element from pymatgen.electronic_structure.cohp import IcohpCollection, IcohpValue from pymatgen.util.typing import PathLike @@ -65,7 +65,7 @@ class LobsterNeighbors(NearNeighbors): def __init__( self, - structure: Structure, + structure: Structure | IStructure, filename_icohp: PathLike | None = "ICOHPLIST.lobster", obj_icohp: Icohplist | None = None, are_coops: bool = False, @@ -265,7 +265,7 @@ def get_anion_types(self) -> set[Element]: def get_nn_info( self, - structure: Structure, + structure: Structure | IStructure, n: int, use_weights: bool = False, ) -> dict[str, Any]: @@ -1377,7 +1377,7 @@ def from_Lobster( list_permutation: list, list_neighsite: list[PeriodicSite], list_neighisite: list[list[int]], - structure: Structure, + structure: Structure | IStructure, valences: list[float] | None = None, ) -> Self: """Set up a LightStructureEnvironments from LOBSTER. From a244348a7e161027bd91b52c1291c1fe8f628768 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Mon, 28 Apr 2025 10:07:32 -0700 Subject: [PATCH 135/180] More type fixes. --- cohp.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/cohp.py b/cohp.py index 0cccc4839d..20fdfcc981 100644 --- a/cohp.py +++ b/cohp.py @@ -31,10 +31,10 @@ from pymatgen.util.num import round_to_sigfigs if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Mapping from typing import Any, Literal - from numpy.typing import NDArray + from numpy.typing import ArrayLike, NDArray from typing_extensions import Self from pymatgen.util.typing import PathLike, SpinLike @@ -58,12 +58,12 @@ class Cohp(MSONable): def __init__( self, efermi: float, - energies: Sequence[float], - cohp: dict[Spin, NDArray], + energies: ArrayLike, + cohp: Mapping[Spin, NDArray], are_coops: bool = False, are_cobis: bool = False, are_multi_center_cobis: bool = False, - icohp: dict[Spin, NDArray] | None = None, + icohp: Mapping[Spin, NDArray] | None = None, ) -> None: """ Args: @@ -146,7 +146,7 @@ def get_cohp( if populations is None: return None if spin is None: - return populations + return populations # type: ignore[return-value] if isinstance(spin, int): spin = Spin(spin) elif isinstance(spin, str): @@ -378,8 +378,8 @@ def get_cohp_by_label( The Cohp. """ if label.lower() == "average": - divided_cohp: dict[Spin, Any] | None = self.cohp - divided_icohp: dict[Spin, Any] | None = self.icohp + divided_cohp: Mapping[Spin, NDArray] | None = self.cohp + divided_icohp: Mapping[Spin, NDArray] | None = self.icohp else: divided_cohp = self.all_cohps[label].get_cohp(spin=None, integrated=False) divided_icohp = self.all_cohps[label].get_icohp(spin=None) @@ -390,8 +390,10 @@ def get_cohp_by_label( if summed_spin_channels and Spin.down in self.cohp: if divided_icohp is None: raise ValueError("divided_icohp is None") - final_cohp: dict[Spin, Any] = {Spin.up: np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0)} - final_icohp: dict[Spin, Any] | None = { + final_cohp: Mapping[Spin, NDArray] = { + Spin.up: np.sum([divided_cohp[Spin.up], divided_cohp[Spin.down]], axis=0) + } + final_icohp: Mapping[Spin, NDArray] | None = { Spin.up: np.sum([divided_icohp[Spin.up], divided_icohp[Spin.down]], axis=0) } else: @@ -1418,7 +1420,7 @@ def get_integrated_cohp_in_energy_range( _icohps = cohp.get_orbital_resolved_cohp(label=label, orbitals=orbital) if _icohps is None: raise ValueError("_icohps is None") - icohps = _icohps.icohp + icohps = _icohps.icohp # type: ignore[assignment] if icohps is None: raise ValueError("ichops is None") From 0e8d71e8f2b08836bd970aef572f481f079dbe65 Mon Sep 17 00:00:00 2001 From: Ali Hussain Umar Bhatti <65511923+alibh95@users.noreply.github.com> Date: Fri, 2 May 2025 06:17:06 +0900 Subject: [PATCH 136/180] fix(outputs.py): handle missing trailing newline in ICOHPLIST.lobster (#4350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(outputs.py): handle missing trailing newline in ICOHPLIST.lobster LOBSTER 5.1.1 sometimes produces an ICOHPLIST.lobster file without a trailing newline, which causes the Icohplist parser to miscount bond entries (e.g., 1079 vs. 1080). This commit updates the file-reading logic in Icohplist.__init__ to use splitlines() and filters out any blank lines, ensuring that only valid, non-empty lines are parsed. This change prevents the ValueError ("COHPCAR and ICOHPLIST do not fit together") and aligns the bond count with COHPCAR.lobster. Ref: See related issue in lobsterpy (#389) for additional context. Signed-off-by: Ali Hussain Umar Bhatti <65511923+alibh95@users.noreply.github.com> * pre-commit auto-fixes * Update outputs.py * Add test_missing_trailing_newline * Robust header detection and version parsing in Icohplist - Strip only trailing blank lines so a missing final newline still works - Compute header length dynamically (skip title and optional spin‐line) - Determine LOBSTER version by column count (6→2.2.1, 8→3.1.1, 9→5.1.0) - Preserve non‐orbitalwise LCFO entries (fixes length mismatch for non-orbitalwise LCFO lists) --------- Signed-off-by: Ali Hussain Umar Bhatti <65511923+alibh95@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Shyue Ping Ong --- outputs.py | 60 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/outputs.py b/outputs.py index bc94991de2..d793dbd898 100644 --- a/outputs.py +++ b/outputs.py @@ -405,24 +405,35 @@ def __init__( # and we don't need the header. if self._icohpcollection is None: with zopen(self._filename, mode="rt", encoding="utf-8") as file: - all_lines = file.read().split("\n") - lines = all_lines[1:-1] if "spin" not in all_lines[1] else all_lines[2:-1] - if len(lines) == 0: - raise RuntimeError("ICOHPLIST file contains no data.") - - # Determine LOBSTER version - if len(lines[0].split()) == 8 and "spin" not in all_lines[1]: - version = "3.1.1" - elif (len(lines[0].split()) == 8 or len(lines[0].split()) == 9) and "spin" in all_lines[1]: - version = "5.1.0" - elif len(lines[0].split()) == 6: - version = "2.2.1" - warnings.warn( - "Please consider using a newer LOBSTER version. See www.cohp.de.", - stacklevel=2, - ) - else: - raise ValueError("Unsupported LOBSTER version.") + all_lines = file.read().splitlines() + + # strip *trailing* blank lines only + all_lines = [line for line in all_lines if line.strip()] + # --- detect header length robustly --- + header_len = 0 + try: + int(all_lines[0].split()[0]) + except ValueError: + header_len += 1 + if header_len < len(all_lines) and "spin" in all_lines[header_len].lower(): + header_len += 1 + lines = all_lines[header_len:] + if not lines: + raise RuntimeError("ICOHPLIST file contains no data.") + # --- version by column count only --- + ncol = len(lines[0].split()) + if ncol == 6: + version = "2.2.1" + warnings.warn( + "Please consider using a newer LOBSTER version. See www.cohp.de.", + stacklevel=2, + ) + elif ncol == 8: + version = "3.1.1" + elif ncol == 9: + version = "5.1.0" + else: + raise ValueError(f"Unsupported LOBSTER version ({ncol} columns).") # If the calculation is spin polarized, the line in the middle # of the file will be another header line. @@ -587,6 +598,10 @@ def icohplist(self) -> dict[Any, dict[str, Any]]: "translation": value._translation, "orbitals": value._orbitals, } + + # for LCFO only files drop the single orbital resolved entry when not in orbitalwise mode + if self.is_lcfo and not self.orbitalwise: + icohp_dict = {k: d for k, d in icohp_dict.items() if d.get("orbitals") is None} return icohp_dict @property @@ -1720,7 +1735,12 @@ def has_good_quality_check_occupied_bands( raise ValueError("number_occ_bands_spin_down has to be specified") for spin in (Spin.up, Spin.down) if spin_polarized else (Spin.up,): - num_occ_bands = number_occ_bands_spin_up if spin is Spin.up else number_occ_bands_spin_down + if spin is Spin.up: + num_occ_bands = number_occ_bands_spin_up + else: + if number_occ_bands_spin_down is None: + raise ValueError("number_occ_bands_spin_down has to be specified") + num_occ_bands = number_occ_bands_spin_down for overlap_matrix in self.band_overlaps_dict[spin]["matrices"]: sub_array = np.asarray(overlap_matrix)[:num_occ_bands, :num_occ_bands] @@ -2333,7 +2353,7 @@ def _parse_matrix( file_data: list[str], pattern: str, e_fermi: float, - ) -> tuple[list[float], dict, dict]: + ) -> tuple[list[np.ndarray], dict[Any, Any], dict[Any, Any]]: complex_matrices: dict = {} matrix_diagonal_values = [] start_inxs_real = [] From 4772c0f81e893dea3be90347f35923cb74e6fde3 Mon Sep 17 00:00:00 2001 From: Ali Hussain Umar Bhatti <65511923+alibh95@users.noreply.github.com> Date: Fri, 2 May 2025 06:17:06 +0900 Subject: [PATCH 137/180] fix(outputs.py): handle missing trailing newline in ICOHPLIST.lobster (#4350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(outputs.py): handle missing trailing newline in ICOHPLIST.lobster LOBSTER 5.1.1 sometimes produces an ICOHPLIST.lobster file without a trailing newline, which causes the Icohplist parser to miscount bond entries (e.g., 1079 vs. 1080). This commit updates the file-reading logic in Icohplist.__init__ to use splitlines() and filters out any blank lines, ensuring that only valid, non-empty lines are parsed. This change prevents the ValueError ("COHPCAR and ICOHPLIST do not fit together") and aligns the bond count with COHPCAR.lobster. Ref: See related issue in lobsterpy (#389) for additional context. Signed-off-by: Ali Hussain Umar Bhatti <65511923+alibh95@users.noreply.github.com> * pre-commit auto-fixes * Update outputs.py * Add test_missing_trailing_newline * Robust header detection and version parsing in Icohplist - Strip only trailing blank lines so a missing final newline still works - Compute header length dynamically (skip title and optional spin‐line) - Determine LOBSTER version by column count (6→2.2.1, 8→3.1.1, 9→5.1.0) - Preserve non‐orbitalwise LCFO entries (fixes length mismatch for non-orbitalwise LCFO lists) --------- Signed-off-by: Ali Hussain Umar Bhatti <65511923+alibh95@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Shyue Ping Ong --- tests/io/lobster/test_outputs.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index 3f63cff41c..f275801076 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -3,6 +3,7 @@ import copy import json import os +import tempfile import numpy as np import pytest @@ -1955,7 +1956,7 @@ def test_attributes(self): assert self.icohp_lcfo.is_spin_polarized assert len(self.icohp_lcfo.icohplist) == 28 assert not self.icohp_lcfo_non_orbitalwise.orbitalwise - assert len(self.icohp_lcfo_non_orbitalwise.icohplist) == 27 + assert len(self.icohp_lcfo_non_orbitalwise.icohplist) == 28 def test_values(self): icohplist_bise = { @@ -2171,6 +2172,23 @@ def test_msonable(self): else: assert getattr(icohplist_from_dict, attr_name) == attr_value + def test_missing_trailing_newline(self): + content = ( + "1 Co1 O1 1.00000 0 0 0 -0.50000 -1.00000\n" + "2 Co2 O2 1.10000 0 0 0 -0.60000 -1.10000" + ) + + with tempfile.NamedTemporaryFile("w+", delete=False) as tmp: + tmp.write(content) + tmp.flush() + fname = tmp.name + try: + ip = Icohplist(filename=fname) + assert len(ip.icohplist) == 2 + assert ip.icohplist["1"]["icohp"][Spin.up] == approx(-0.5) + finally: + os.remove(fname) + class TestNciCobiList: def setup_method(self): From 8e939c80d6dfe22de30e4894f97288c9896ecf6f Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Fri, 9 May 2025 07:04:36 -0700 Subject: [PATCH 138/180] Type fixes. --- inputs.py | 8 ++++---- lobsterenv.py | 28 +++++++++++++-------------- outputs.py | 52 ++++++++++++++++++++++----------------------------- 3 files changed, 40 insertions(+), 48 deletions(-) diff --git a/inputs.py b/inputs.py index 00e055b417..cfca75832f 100644 --- a/inputs.py +++ b/inputs.py @@ -512,7 +512,7 @@ def write_KPOINTS( # For now, we are setting MAGMOM to zero. (Taken from INCAR class) cell = matrix, positions, zs, magmoms # TODO: what about this shift? - mapping, grid = spglib.get_ir_reciprocal_mesh(mesh, cell, is_shift=[0, 0, 0]) + mapping, grid = spglib.get_ir_reciprocal_mesh(mesh, cell, is_shift=[0, 0, 0]) # type:ignore[arg-type] # Get the KPOINTS for the grid if isym == -1: @@ -530,7 +530,7 @@ def write_KPOINTS( weights = [] all_labels = [] newlist = [list(gp) for gp in list(grid)] - mapping = [] + mapping = [] # type:ignore[assignment] for gp in newlist: minus_gp = [-k for k in gp] if minus_gp in newlist and minus_gp != [0, 0, 0]: @@ -590,7 +590,7 @@ def from_file(cls, lobsterin: PathLike) -> Self: Lobsterin object """ with zopen(lobsterin, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n") + lines: list[str] = file.read().split("\n") # type:ignore[arg-type,assignment] if not lines: raise RuntimeError("lobsterin file contains no data.") @@ -640,7 +640,7 @@ def _get_potcar_symbols(POTCAR_input: PathLike) -> list[str]: Returns: list[str]: names of the species """ - potcar = Potcar.from_file(POTCAR_input) + potcar = Potcar.from_file(POTCAR_input) # type:ignore[arg-type] for pot in potcar: if pot.potential_type != "PAW": raise ValueError("Lobster only works with PAW! Use different POTCARs") diff --git a/lobsterenv.py b/lobsterenv.py index eaae21ecc8..2f54f82218 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -204,7 +204,7 @@ def __init__( else: bv_analyzer = BVAnalyzer() try: - self.valences = bv_analyzer.get_valences(structure=self.structure) + self.valences = bv_analyzer.get_valences(structure=self.structure) # type:ignore[arg-type] except ValueError as exc: self.valences = None if additional_condition in {1, 3, 5, 6}: @@ -311,7 +311,7 @@ def get_light_structure_environment( LobsterLightStructureEnvironments """ lgf = LocalGeometryFinder() - lgf.setup_structure(structure=self.structure) + lgf.setup_structure(structure=self.structure) # type:ignore[arg-type] list_ce_symbols = [] list_csm = [] list_permut = [] @@ -342,7 +342,7 @@ def get_light_structure_environment( list_ce_symbol=list_ce_symbols, list_csm=list_csm, list_permutation=list_permut, - list_neighsite=self.list_neighsite, + list_neighsite=self.list_neighsite, # type:ignore[arg-type] list_neighisite=self.list_neighisite, structure=self.structure, valences=self.valences, @@ -384,7 +384,7 @@ def get_light_structure_environment( list_ce_symbol=new_list_ce_symbols, list_csm=new_list_csm, list_permutation=new_list_permut, - list_neighsite=new_list_neighsite, + list_neighsite=new_list_neighsite, # type:ignore[arg-type] list_neighisite=new_list_neighisite, structure=self.structure, valences=self.valences, @@ -436,7 +436,7 @@ def get_info_icohps_to_neighbors( if idx in isites: for key, icohpsum in zip(self.list_keys[idx], self.list_icohps[idx], strict=True): summed_icohps += icohpsum - list_icohps.append(icohpsum) + list_icohps.append(icohpsum) # type:ignore[arg-type] labels.append(key) atoms.append( [ @@ -617,7 +617,7 @@ def get_info_cohps_to_neighbors( summed_cohp = None - return plot_label, summed_cohp + return plot_label, summed_cohp # type:ignore[return-value] def _get_plot_label(self, atoms: list[list[str]], per_bond: bool) -> str: """Count the types of bonds and append a label.""" @@ -682,8 +682,8 @@ def get_info_icohps_between_neighbors( unitcell1 = self._determine_unit_cell(n_site) unitcell2 = self._determine_unit_cell(n_site2) - index_n_site = self._get_original_site(self.structure, n_site) - index_n_site2 = self._get_original_site(self.structure, n_site2) + index_n_site = self._get_original_site(self.structure, n_site) # type:ignore[arg-type] + index_n_site2 = self._get_original_site(self.structure, n_site2) # type:ignore[arg-type] if index_n_site < index_n_site2: translation = list(np.array(unitcell1) - np.array(unitcell2)) @@ -913,7 +913,7 @@ def _find_environments( Returns: Tuple of ICOHPs, keys, lengths, neighisite, neighsite, coords. """ - list_icohps: list[list[IcohpValue]] = [] + list_icohps: list[list[float]] = [] list_keys: list[list[str]] = [] list_lengths: list[list[float]] = [] list_neighisite: list[list[int]] = [] @@ -1013,7 +1013,7 @@ def _find_environments( list_lengths.append([]) list_keys.append([]) list_coords.append([]) - return ( + return ( # type:ignore[return-value] list_icohps, list_keys, list_lengths, @@ -1027,7 +1027,7 @@ def _find_relevant_atoms_additional_condition( site_idx: int, icohps: dict[str, IcohpValue], additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], - ) -> tuple[list[str], list[float], list[int], list[IcohpValue]]: + ) -> tuple[list[str], list[float], list[int], list[float]]: """Find all relevant atoms that fulfill the additional condition. Args: @@ -1041,7 +1041,7 @@ def _find_relevant_atoms_additional_condition( keys_from_ICOHPs: list[str] = [] lengths_from_ICOHPs: list[float] = [] neighbors_from_ICOHPs: list[int] = [] - icohps_from_ICOHPs: list[IcohpValue] = [] + icohps_from_ICOHPs: list[float] = [] for key, icohp in icohps.items(): atomnr1 = self._get_atomnumber(icohp._atom1) @@ -1443,7 +1443,7 @@ def from_Lobster( if list_neighisite[site_idx] is not None: nb_set = cls.NeighborsSet( - structure=structure, + structure=structure, # type:ignore[arg-type] isite=site_idx, all_nbs_sites=all_nbs_sites, all_nbs_sites_indices=all_nbs_sites_indices[site_idx], @@ -1451,7 +1451,7 @@ def from_Lobster( else: nb_set = cls.NeighborsSet( - structure=structure, + structure=structure, # type:ignore[arg-type] isite=site_idx, all_nbs_sites=[], all_nbs_sites_indices=[], diff --git a/outputs.py b/outputs.py index d793dbd898..4c66f2c7fb 100644 --- a/outputs.py +++ b/outputs.py @@ -55,6 +55,11 @@ ) +def _get_lines(filename) -> list[str]: + with zopen(filename, mode="rt", encoding="utf-8") as file: + return file.read().split("\n") # type:ignore[return-value,arg-type] + + class Cohpcar: """Read COXXCAR.lobster/COXXCAR.LCFO.lobster files generated by LOBSTER. @@ -120,8 +125,7 @@ def __init__( else: self._filename = "COHPCAR.lobster" - with zopen(self._filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n") + lines = _get_lines(filename) # The parameters line is the second line in a COHPCAR file. # It contains all parameters that are needed to map the file. @@ -405,7 +409,7 @@ def __init__( # and we don't need the header. if self._icohpcollection is None: with zopen(self._filename, mode="rt", encoding="utf-8") as file: - all_lines = file.read().splitlines() + all_lines: list[str] = file.read().splitlines() # type:ignore[assignment] # strip *trailing* blank lines only all_lines = [line for line in all_lines if line.strip()] @@ -625,7 +629,7 @@ class NciCobiList: } """ - def __init__(self, filename: PathLike | None = "NcICOBILIST.lobster") -> None: + def __init__(self, filename: PathLike = "NcICOBILIST.lobster") -> None: """ LOBSTER < 4.1.0: no COBI/ICOBI/NcICOBI @@ -636,8 +640,7 @@ def __init__(self, filename: PathLike | None = "NcICOBILIST.lobster") -> None: # LOBSTER list files have an extra trailing blank line # and we don't need the header - with zopen(filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n")[1:-1] + lines = _get_lines(filename)[1:-1] if len(lines) == 0: raise RuntimeError("NcICOBILIST file contains no data.") @@ -927,8 +930,7 @@ def __init__( self.loewdin = [] if loewdin is None else loewdin if self.num_atoms is None: - with zopen(filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n")[3:-3] + lines = _get_lines(filename)[3:-3] # type:ignore[arg-type,assignment] if len(lines) == 0: raise RuntimeError("CHARGES file contains no data.") @@ -1061,8 +1063,7 @@ def __init__(self, filename: PathLike | None, **kwargs) -> None: else: raise ValueError(f"{attr}={val} is not a valid attribute for Lobsterout") elif filename: - with zopen(filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n") + lines = _get_lines(filename) if len(lines) == 0: raise RuntimeError("lobsterout does not contain any data") @@ -1459,8 +1460,7 @@ def __init__( raise ValueError("No FATBAND files in folder or given") for fname in filenames: - with zopen(fname, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n") + lines = _get_lines(fname) atom_names.append(os.path.split(fname)[1].split("_")[1].capitalize()) parameters = lines[0].split() @@ -1493,8 +1493,7 @@ def __init__( eigenvals: dict = {} p_eigenvals: dict = {} for ifilename, filename in enumerate(filenames): - with zopen(filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n") + lines = _get_lines(filename) if ifilename == 0: self.nbands = int(parameters[6]) @@ -1601,7 +1600,7 @@ def get_bandstructure(self) -> LobsterBandStructureSymmLine: lattice=self.lattice, efermi=self.efermi, # type: ignore[arg-type] labels_dict=self.label_dict, - structure=self.structure, + structure=self.structure, # type:ignore[arg-type] projections=self.p_eigenvals, ) @@ -1641,8 +1640,7 @@ def __init__( self.max_deviation = [] if max_deviation is None else max_deviation if not self.band_overlaps_dict: - with zopen(filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n") + lines = _get_lines(filename) spin_numbers = [0, 1] if lines[0].split()[-1] == "0" else [1, 2] @@ -1786,8 +1784,7 @@ def __init__( self.is_lcfo = is_lcfo self.list_dict_grosspop = [] if list_dict_grosspop is None else list_dict_grosspop if not self.list_dict_grosspop: - with zopen(filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n") + lines = _get_lines(filename) # Read file to list of dict small_dict: dict[str, Any] = {} @@ -1916,8 +1913,7 @@ def _parse_file( imaginary (list[float]): Imaginary parts of wave function. distance (list[float]): Distances to the first point in wave function file. """ - with zopen(filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n") + lines = _get_lines(filename) points = [] distances = [] @@ -2086,8 +2082,7 @@ def __init__( self.madelungenergies_mulliken = None if madelungenergies_mulliken is None else madelungenergies_mulliken if self.ewald_splitting is None: - with zopen(filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n")[5] + lines = _get_lines(filename)[5] if len(lines) == 0: raise RuntimeError("MadelungEnergies file contains no data.") @@ -2157,8 +2152,7 @@ def __init__( self.madelungenergies_mulliken: list | float = madelungenergies_mulliken or [] if self.num_atoms is None: - with zopen(filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n") + lines = _get_lines(filename) if len(lines) == 0: raise RuntimeError("SitePotentials file contains no data.") @@ -2311,7 +2305,7 @@ def __init__( self._filename = str(filename) with zopen(self._filename, mode="rt", encoding="utf-8") as file: - lines = file.readlines() + lines: list[str] = file.readlines() # type:ignore[assignment] if len(lines) == 0: raise RuntimeError("Please check provided input file, it seems to be empty") @@ -2454,8 +2448,7 @@ def __init__( self.rel_loewdin_pol_vector = {} if rel_loewdin_pol_vector is None else rel_loewdin_pol_vector if not self.rel_loewdin_pol_vector and not self.rel_mulliken_pol_vector: - with zopen(filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n") + lines = _get_lines(filename) if len(lines) == 0: raise RuntimeError("Polarization file contains no data.") @@ -2498,8 +2491,7 @@ def __init__( self.bin_width = 0.0 if bin_width is None else bin_width if not self.bwdf: - with zopen(filename, mode="rt", encoding="utf-8") as file: - lines = file.read().split("\n") + lines = _get_lines(filename) if len(lines) == 0: raise RuntimeError("BWDF file contains no data.") From 72463dc4531eccbc1b4e7a261ba15338068fb264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haoyu=20=28Daniel=29=20YANG=20=E6=9D=A8=E6=B5=A9=E5=AE=87?= Date: Wed, 28 May 2025 21:42:21 +0200 Subject: [PATCH 139/180] Add `orjson` as required dependency as default JSON handler when custom encoder/decoder is not needed (#4411) * clean up pyproject * try to remove pybtex warning * sort optional dependencies * allow pyproject change to trigger test * also include workflow and setup in test workflow trigger * add orjson as dependency * replace json.dumps * replace json.loads * fix phonondos numpy handling * remove indent and see how many test will break * replace json.load * fix JSON str whitespace sensitivity * replace json.dump * fix os.path.exists * replace str | PathLike * use pmg typealias pathlike * clean up import * minor clean up of module docstring --- tests/io/lobster/test_outputs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index f275801076..5d8f6c1931 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -1,11 +1,11 @@ from __future__ import annotations import copy -import json import os import tempfile import numpy as np +import orjson import pytest from numpy.testing import assert_allclose, assert_array_equal from pytest import approx @@ -420,8 +420,8 @@ def setup_method(self): self.DOSCAR_lcfo = Doscar(doscar=doscar3, structure_file=poscar3, is_lcfo=True) - with open(f"{TEST_FILES_DIR}/electronic_structure/dos/structure_KF.json", encoding="utf-8") as file: - data = json.load(file) + with open(f"{TEST_FILES_DIR}/electronic_structure/dos/structure_KF.json", "rb") as file: + data = orjson.loads(file.read()) self.structure = Structure.from_dict(data) From 8000b82b08c9ba9aa3faa860e2c9aa0a789f33b3 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Thu, 29 May 2025 04:26:14 +0200 Subject: [PATCH 140/180] Add custom as_dict/from_dict method for proper initialization of attributes of IcohpCollection (#4391) * add custom from_dict method for proper initialization of attributes * pre-commit auto-fixes * add custom as_dict method for proper serialization * adapt existing test to check for serialization --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- cohp.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/cohp.py b/cohp.py index 20fdfcc981..2782cda330 100644 --- a/cohp.py +++ b/cohp.py @@ -1387,6 +1387,75 @@ def are_cobis(self) -> bool: """Whether this is COBI.""" return self._are_cobis + def as_dict(self) -> dict[str, Any]: + """JSON-serializable dict representation of COHP.""" + return { + "@module": type(self).__module__, + "@class": type(self).__name__, + "are_coops": self._are_coops, + "are_cobis": self._are_cobis, + "list_labels": self._list_labels, + "list_atom1": self._list_atom1, + "list_atom2": self._list_atom2, + "list_length": self._list_length, + "list_translation": self._list_translation, + "list_num": self._list_num, + "list_icohp": [{str(spin): value for spin, value in icohp.items()} for icohp in self._list_icohp], + "is_spin_polarized": self._is_spin_polarized, + "list_orb_icohp": [ + { + key: { + "icohp": {str(spin): value for spin, value in val["icohp"].items()}, + "orbitals": [[n, int(orb)] for n, orb in val["orbitals"]], + } + for key, val in entry.items() + } + for entry in self._list_orb_icohp + ], + } + + @classmethod + def from_dict(cls, dct: dict[str, Any]) -> Self: + """Generate IcohpCollection from a dict representation.""" + list_icohp = [] + for icohp_dict in dct["list_icohp"]: + new_icohp_dict = {} + for spin_key, value in icohp_dict.items(): + # Convert string/int to Spin enum + spin_enum = ( + Spin(int(spin_key)) if isinstance(spin_key, int) or spin_key in ("1", "-1") else Spin[spin_key] + ) + new_icohp_dict[spin_enum] = value + list_icohp.append(new_icohp_dict) + + new_list_orb = [{orb_label: {} for orb_label in bond} for bond in dct["list_orb_icohp"]] # type:ignore[var-annotated] + for bond_num, lab_orb_icohp in enumerate(dct["list_orb_icohp"]): + for orb in lab_orb_icohp: + for key in lab_orb_icohp[orb]: + sub_dict = {} + if key == "icohp": + sub_dict[key] = { + Spin.up: lab_orb_icohp[orb][key]["1"], + Spin.down: lab_orb_icohp[orb][key]["-1"], + } + + if key == "orbitals": + orb_temp = [] + + for item in lab_orb_icohp[orb][key]: + item[1] = Orbital(item[1]) + orb_temp.append(item) + sub_dict[key] = orb_temp # type: ignore[assignment] + + new_list_orb[bond_num][orb].update(sub_dict) + + dct["list_icohp"] = list_icohp + dct["list_orb_icohp"] = new_list_orb + + init_dict = {k: v for k, v in dct.items() if "@" not in k} + + return cls(**init_dict) + def get_integrated_cohp_in_energy_range( cohp: CompleteCohp, From 3d365921fb196cd50ef7a1ae499d6f5c4bf00b09 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Jul 2025 09:19:58 -0700 Subject: [PATCH 141/180] pre-commit autoupdate (#4450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.12 → v0.12.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.12...v0.12.2) - [github.com/pre-commit/mirrors-mypy: v1.16.0 → v1.16.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.16.0...v1.16.1) - [github.com/MarcoGorelli/cython-lint: v0.16.6 → v0.16.7](https://github.com/MarcoGorelli/cython-lint/compare/v0.16.6...v0.16.7) - [github.com/RobertCraigie/pyright-python: v1.1.401 → v1.1.402](https://github.com/RobertCraigie/pyright-python/compare/v1.1.401...v1.1.402) * pre-commit auto-fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- inputs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/inputs.py b/inputs.py index cfca75832f..16c8d534d8 100644 --- a/inputs.py +++ b/inputs.py @@ -271,8 +271,7 @@ def write_lobsterin( file.write(f"{type(self).BOOLEAN_KEYWORDS[key]}\n") elif key in type(self).LIST_KEYWORDS: - for value in self.get(key): # type: ignore[union-attr] - file.write(f"{type(self).LIST_KEYWORDS[key]} {value}\n") + file.writelines(f"{type(self).LIST_KEYWORDS[key]} {value}\n" for value in self.get(key)) def as_dict(self) -> dict: """MSONable dict.""" From 859e5d0864dce85e2fffe8bbed1958d83d1b3866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haoyu=20=28Daniel=29=20YANG=20=E6=9D=A8=E6=B5=A9=E5=AE=87?= Date: Tue, 29 Jul 2025 18:20:58 +0200 Subject: [PATCH 142/180] Remove LOBSTER output file trailing line sensitivity (#4448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * clean up tests * remove trailing line sensitivity * simplify temp file * lint fix * add test * fix filename Co-authored-by: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Signed-off-by: Haoyu (Daniel) YANG 杨浩宇 * also test default filename * test `_get_lines` --------- Signed-off-by: Haoyu (Daniel) YANG 杨浩宇 Co-authored-by: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> --- outputs.py | 57 ++++++++++++++++++++++++------------------------------ 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/outputs.py b/outputs.py index 4c66f2c7fb..12fe3ab0a8 100644 --- a/outputs.py +++ b/outputs.py @@ -57,7 +57,7 @@ def _get_lines(filename) -> list[str]: with zopen(filename, mode="rt", encoding="utf-8") as file: - return file.read().split("\n") # type:ignore[return-value,arg-type] + return cast("list[str]", file.read().splitlines()) class Cohpcar: @@ -109,7 +109,7 @@ def __init__( or (are_coops and are_multi_center_cobis) or (are_cobis and are_multi_center_cobis) ): - raise ValueError("You cannot have info about COOPs, COBIs and/or multi-center COBIS in the same file.") + raise ValueError("You cannot have info about COOPs, COBIs and/or multi-center COBIs in the same file.") self.are_coops = are_coops self.are_cobis = are_cobis @@ -125,7 +125,7 @@ def __init__( else: self._filename = "COHPCAR.lobster" - lines = _get_lines(filename) + lines: list[str] = _get_lines(self._filename) # The parameters line is the second line in a COHPCAR file. # It contains all parameters that are needed to map the file. @@ -136,24 +136,23 @@ def __init__( self.is_spin_polarized = int(parameters[1]) == 2 spins = [Spin.up, Spin.down] if int(parameters[1]) == 2 else [Spin.up] cohp_data: dict[str, dict[str, Any]] = {} + + # The COHP/COBI data start from line num_bonds + 3 + data = np.array([np.array(line.split(), dtype=float) for line in lines[num_bonds + 3 :]]).transpose() + if not self.are_multi_center_cobis: - # The COHP data start in line num_bonds + 3 - data = np.array([np.array(line.split(), dtype=float) for line in lines[num_bonds + 3 :]]).transpose() cohp_data = { "average": { "COHP": {spin: data[1 + 2 * s * (num_bonds + 1)] for s, spin in enumerate(spins)}, "ICOHP": {spin: data[2 + 2 * s * (num_bonds + 1)] for s, spin in enumerate(spins)}, } } - else: - # The COBI data start in line num_bonds + 3 if multi-center cobis exist - data = np.array([np.array(line.split(), dtype=float) for line in lines[num_bonds + 3 :]]).transpose() self.energies = data[0] orb_cohp: dict[str, Any] = {} # Present for LOBSTER versions older than 2.2.0 - very_old = False + older_than_2_2_0: bool = False # The label has to be changed: there are more than one COHP for each atom combination # this is done to make the labeling consistent with ICOHPLIST.lobster @@ -192,8 +191,8 @@ def __init__( else: # Present for LOBSTER versions older than 2.2.0 if bond_num == 0: - very_old = True - if very_old: + older_than_2_2_0 = True + if older_than_2_2_0: bond_num += 1 label = str(bond_num) @@ -245,8 +244,8 @@ def __init__( else: # Present for LOBSTER versions older than 2.2.0 if bond_num == 0: - very_old = True - if very_old: + older_than_2_2_0 = True + if older_than_2_2_0: bond_num += 1 label = str(bond_num) @@ -261,7 +260,7 @@ def __init__( } # Present for LOBSTER older than 2.2.0 - if very_old: + if older_than_2_2_0: for bond_str in orb_cohp: cohp_data[bond_str] = { "COHP": None, @@ -405,14 +404,10 @@ def __init__( else: self._filename = "ICOHPLIST.lobster" - # LOBSTER list files have an extra trailing blank line - # and we don't need the header. if self._icohpcollection is None: with zopen(self._filename, mode="rt", encoding="utf-8") as file: - all_lines: list[str] = file.read().splitlines() # type:ignore[assignment] + all_lines: list[str] = cast("list[str]", file.read().splitlines()) - # strip *trailing* blank lines only - all_lines = [line for line in all_lines if line.strip()] # --- detect header length robustly --- header_len = 0 try: @@ -442,7 +437,7 @@ def __init__( # If the calculation is spin polarized, the line in the middle # of the file will be another header line. # TODO: adapt this for orbital-wise stuff - if version in ("3.1.1", "2.2.1"): + if version in {"3.1.1", "2.2.1"}: self.is_spin_polarized = "distance" in lines[len(lines) // 2] else: # if version == "5.1.0": self.is_spin_polarized = len(lines[0].split()) == 9 @@ -637,10 +632,8 @@ def __init__(self, filename: PathLike = "NcICOBILIST.lobster") -> None: Args: filename: Name of the NcICOBILIST file. """ - - # LOBSTER list files have an extra trailing blank line - # and we don't need the header - lines = _get_lines(filename)[1:-1] + # We don't need the header + lines = _get_lines(filename)[1:] if len(lines) == 0: raise RuntimeError("NcICOBILIST file contains no data.") @@ -930,7 +923,7 @@ def __init__( self.loewdin = [] if loewdin is None else loewdin if self.num_atoms is None: - lines = _get_lines(filename)[3:-3] # type:ignore[arg-type,assignment] + lines = _get_lines(filename)[3:-2] if len(lines) == 0: raise RuntimeError("CHARGES file contains no data.") @@ -1105,10 +1098,12 @@ def __init__(self, filename: PathLike | None, **kwargs) -> None: self.has_doscar_lso = ( "writing DOSCAR.LSO.lobster..." in lines and "SKIPPING writing DOSCAR.LSO.lobster..." not in lines ) + try: version_number = float(".".join(self.lobster_version.strip("v").split(".")[:2])) except ValueError: version_number = 0.0 + if version_number < 5.1: self.has_cohpcar = ( "writing COOPCAR.lobster and ICOOPLIST.lobster..." in lines @@ -1452,9 +1447,7 @@ def __init__( for name in os.listdir(filenames): if fnmatch.fnmatch(name, "FATBAND_*.lobster"): filenames_new.append(os.path.join(filenames, name)) - filenames = filenames_new # type: ignore[assignment] - - filenames = cast("list[PathLike]", filenames) + filenames = cast("list[PathLike]", filenames_new) if len(filenames) == 0: raise ValueError("No FATBAND files in folder or given") @@ -1546,7 +1539,7 @@ def __init__( idx_kpt = -1 linenumber = iband = 0 - for line in lines[1:-1]: + for line in lines[1:]: if line.split()[0] == "#": KPOINT = np.array( [ @@ -1600,7 +1593,7 @@ def get_bandstructure(self) -> LobsterBandStructureSymmLine: lattice=self.lattice, efermi=self.efermi, # type: ignore[arg-type] labels_dict=self.label_dict, - structure=self.structure, # type:ignore[arg-type] + structure=self.structure, # type: ignore[arg-type] projections=self.p_eigenvals, ) @@ -2159,7 +2152,7 @@ def __init__( self._filename = filename self.ewald_splitting = float(lines[0].split()[9]) - lines = lines[5:-1] + lines = lines[5:] self.num_atoms = len(lines) - 2 for atom in range(self.num_atoms): line_parts = lines[atom].split() @@ -2305,7 +2298,7 @@ def __init__( self._filename = str(filename) with zopen(self._filename, mode="rt", encoding="utf-8") as file: - lines: list[str] = file.readlines() # type:ignore[assignment] + lines: list[str] = cast("list[str]", file.readlines()) if len(lines) == 0: raise RuntimeError("Please check provided input file, it seems to be empty") From e5ded9dc96fdd319f819b330e978971f879f93d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haoyu=20=28Daniel=29=20YANG=20=E6=9D=A8=E6=B5=A9=E5=AE=87?= Date: Tue, 29 Jul 2025 18:20:58 +0200 Subject: [PATCH 143/180] Remove LOBSTER output file trailing line sensitivity (#4448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * clean up tests * remove trailing line sensitivity * simplify temp file * lint fix * add test * fix filename Co-authored-by: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Signed-off-by: Haoyu (Daniel) YANG 杨浩宇 * also test default filename * test `_get_lines` --------- Signed-off-by: Haoyu (Daniel) YANG 杨浩宇 Co-authored-by: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> --- tests/io/lobster/test_outputs.py | 69 +++++++++++++++++++------------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index 5d8f6c1931..6362b40d4f 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -1,8 +1,8 @@ from __future__ import annotations import copy +import gzip import os -import tempfile import numpy as np import orjson @@ -30,6 +30,7 @@ SitePotential, Wavefunction, ) +from pymatgen.io.lobster.outputs import _get_lines from pymatgen.io.vasp import Vasprun from pymatgen.util.testing import TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, MatSciTest @@ -64,7 +65,17 @@ def setup_method(self): filename=f"{TEST_DIR}/COOPCAR.lobster.BiSe.gz", are_coops=True, ) - self.cohp_fe = Cohpcar(filename=f"{TEST_DIR}/COOPCAR.lobster.gz") + + # Make sure Cohpcar also works with terminating line ending char + gz_path = f"{TEST_DIR}/COOPCAR.lobster.gz" + with gzip.open(gz_path, "rt", encoding="utf-8") as f: + content = f.read() + "\n" + + # Test default filename (None should be redirected to "COHPCAR.lobster") + with open("COHPCAR.lobster", "w", encoding="utf-8") as f: + f.write(content) + + self.cohp_fe = Cohpcar(filename=None) self.coop_fe = Cohpcar( filename=f"{TEST_DIR}/COOPCAR.lobster.gz", are_coops=True, @@ -645,16 +656,11 @@ def setup_method(self): self.charge_lcfo = Charge(filename=f"{TEST_DIR}/CHARGE.LCFO.lobster.ALN.gz", is_lcfo=True) def test_attributes(self): - charge_Loewdin = [-1.25, 1.25] - charge_Mulliken = [-1.30, 1.30] - atomlist = ["O1", "Mn2"] - types = ["O", "Mn"] - num_atoms = 2 - assert charge_Mulliken == self.charge2.mulliken - assert charge_Loewdin == self.charge2.loewdin - assert atomlist == self.charge2.atomlist - assert types == self.charge2.types - assert num_atoms == self.charge2.num_atoms + assert self.charge2.mulliken == approx([-1.30, 1.30]) + assert self.charge2.loewdin == approx([-1.25, 1.25]) + assert self.charge2.atomlist == ["O1", "Mn2"] + assert self.charge2.types == ["O", "Mn"] + assert self.charge2.num_atoms == 2 # test with CHARG.LCFO.lobster file assert self.charge_lcfo.is_lcfo @@ -1866,7 +1872,7 @@ def test_msonable(self): assert getattr(grosspop_from_dict, attr_name) == attr_value -class TestIcohplist: +class TestIcohplist(MatSciTest): def setup_method(self): self.icohp_bise = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster.BiSe") self.icoop_bise = Icohplist( @@ -2173,21 +2179,16 @@ def test_msonable(self): assert getattr(icohplist_from_dict, attr_name) == attr_value def test_missing_trailing_newline(self): - content = ( - "1 Co1 O1 1.00000 0 0 0 -0.50000 -1.00000\n" - "2 Co2 O2 1.10000 0 0 0 -0.60000 -1.10000" - ) + fname = f"{self.tmp_path}/icohplist" + with open(fname, mode="w", encoding="utf-8") as f: + f.write( + "1 Co1 O1 1.00000 0 0 0 -0.50000 -1.00000\n" + "2 Co2 O2 1.10000 0 0 0 -0.60000 -1.10000" + ) - with tempfile.NamedTemporaryFile("w+", delete=False) as tmp: - tmp.write(content) - tmp.flush() - fname = tmp.name - try: - ip = Icohplist(filename=fname) - assert len(ip.icohplist) == 2 - assert ip.icohplist["1"]["icohp"][Spin.up] == approx(-0.5) - finally: - os.remove(fname) + ip = Icohplist(filename=fname) + assert len(ip.icohplist) == 2 + assert ip.icohplist["1"]["icohp"][Spin.up] == approx(-0.5) class TestNciCobiList: @@ -2531,3 +2532,17 @@ def test_attributes(self): "abs": 56.14, "unit": "uC/cm2", } + + +def test_get_lines(): + """Ensure `_get_lines` is not trailing end char sensitive.""" + with open("without-end-char", mode="wb") as f: + f.write(b"first line\nsecond line") + + with open("with-end-char", mode="wb") as f: + f.write(b"first line\nsecond line\n") + + without_end_char = _get_lines("without-end-char") + with_end_char = _get_lines("with-end-char") + + assert len(with_end_char) == len(without_end_char) == 2 From ebdf9296f2595315adf950e4247bc38a34a106a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haoyu=20=28Daniel=29=20YANG=20=E6=9D=A8=E6=B5=A9=E5=AE=87?= Date: Wed, 30 Jul 2025 14:29:35 +0200 Subject: [PATCH 144/180] Fix `ruff` PERF401: manual-list-comprehension (#4449) * unignore PERF401 rule * fix core * fix dev_scripts * fix ext * fix entries * fix symmetry * fix transformations * fix alchemy * fix apps * fix command line * fix phonon * fix lobster * fix vasp * fix io * fix electronic structure * fix analysis * fix typo * fix some mypy error * add test --- outputs.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/outputs.py b/outputs.py index 12fe3ab0a8..de91d07d3c 100644 --- a/outputs.py +++ b/outputs.py @@ -656,10 +656,10 @@ def __init__(self, filename: PathLike = "NcICOBILIST.lobster") -> None: break # condition has only to be met once if self.orbital_wise: - data_without_orbitals = [] - for line in lines: - if "_" not in str(line.split()[3:]) and "s]" not in str(line.split()[3:]): - data_without_orbitals.append(line) + data_without_orbitals = [ + line for line in lines if "_" not in str(line.split()[3:]) and "s]" not in str(line.split()[3:]) + ] + else: data_without_orbitals = lines @@ -1441,12 +1441,15 @@ def __init__( parameters = [] if not isinstance(filenames, list) or filenames is None: - filenames_new: list[str] = [] if filenames is None: filenames = "." - for name in os.listdir(filenames): - if fnmatch.fnmatch(name, "FATBAND_*.lobster"): - filenames_new.append(os.path.join(filenames, name)) + + filenames_new = [ + os.path.join(filenames, name) + for name in os.listdir(filenames) + if fnmatch.fnmatch(name, "FATBAND_*.lobster") + ] + filenames = cast("list[PathLike]", filenames_new) if len(filenames) == 0: @@ -1472,9 +1475,7 @@ def __init__( for items in atom_orbital_dict.values(): if len(set(items)) != len(items): raise ValueError("The are two FATBAND files for the same atom and orbital. The program will stop.") - split = [] - for item in items: - split.append(item.split("_")[0]) + split = [item.split("_")[0] for item in items] for number in collections.Counter(split).values(): if number not in {1, 3, 5, 7}: raise ValueError( @@ -1683,10 +1684,8 @@ def _read(self, lines: list[str], spin_numbers: list[int]) -> None: overlaps = [] else: - _lines = [] - for el in line.split(" "): - if el != "": - _lines.append(float(el)) + _lines = [float(el) for el in line.split(" ") if el != ""] + overlaps.append(_lines) if len(overlaps) == len(_lines): self.band_overlaps_dict[spin]["matrices"].append(np.array(overlaps)) From a83e74369e2b6d7054e2755633c241eb51f1c43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haoyu=20=28Daniel=29=20YANG=20=E6=9D=A8=E6=B5=A9=E5=AE=87?= Date: Wed, 30 Jul 2025 14:29:35 +0200 Subject: [PATCH 145/180] Fix `ruff` PERF401: manual-list-comprehension (#4449) * unignore PERF401 rule * fix core * fix dev_scripts * fix ext * fix entries * fix symmetry * fix transformations * fix alchemy * fix apps * fix command line * fix phonon * fix lobster * fix vasp * fix io * fix electronic structure * fix analysis * fix typo * fix some mypy error * add test --- cohp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cohp.py b/cohp.py index 2782cda330..9e1d623c23 100644 --- a/cohp.py +++ b/cohp.py @@ -106,8 +106,7 @@ def __repr__(self) -> str: format_header = "#" + " ".join("{:15s}" for __ in header) format_data = " ".join("{:.5f}" for __ in header) str_arr = [format_header.format(*header)] - for idx in range(len(self.energies)): - str_arr.append(format_data.format(*(d[idx] for d in data))) + str_arr.extend(format_data.format(*(d[idx] for d in data)) for idx in range(len(self.energies))) return "\n".join(str_arr) def as_dict(self) -> dict[str, Any]: From 76e4a826bbe2f5f3c0305df4f23cfb152388de51 Mon Sep 17 00:00:00 2001 From: Tom Demeyere <115232841+tomdemeyere@users.noreply.github.com> Date: Thu, 21 Aug 2025 19:26:32 +0200 Subject: [PATCH 146/180] Fix `has_cobicar` when NcICOBILIST is present (#4447) * fix has_cobi when NcICOBILIST is present * linebreak * pre-commit auto-fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- outputs.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/outputs.py b/outputs.py index de91d07d3c..4044463bea 100644 --- a/outputs.py +++ b/outputs.py @@ -1106,27 +1106,28 @@ def __init__(self, filename: PathLike | None, **kwargs) -> None: if version_number < 5.1: self.has_cohpcar = ( - "writing COOPCAR.lobster and ICOOPLIST.lobster..." in lines - and "SKIPPING writing COOPCAR.lobster and ICOOPLIST.lobster..." not in lines - ) - self.has_coopcar = ( "writing COHPCAR.lobster and ICOHPLIST.lobster..." in lines and "SKIPPING writing COHPCAR.lobster and ICOHPLIST.lobster..." not in lines ) + self.has_coopcar = ( + "writing COOPCAR.lobster and ICOOPLIST.lobster..." in lines + and "SKIPPING writing COOPCAR.lobster and ICOOPLIST.lobster..." not in lines + ) self.has_cobicar = ( "writing COBICAR.lobster and ICOBILIST.lobster..." in lines and "SKIPPING writing COBICAR.lobster and ICOBILIST.lobster..." not in lines ) else: self.has_cohpcar = ( - "writing COOPCAR.lobster..." in lines and "SKIPPING writing COOPCAR.lobster..." not in lines + "writing COHPCAR.lobster..." in lines and "SKIPPING writing COHPCAR.lobster..." not in lines ) self.has_coopcar = ( - "writing COHPCAR.lobster..." in lines and "SKIPPING writing COHPCAR.lobster..." not in lines + "writing COOPCAR.lobster..." in lines and "SKIPPING writing COOPCAR.lobster..." not in lines ) self.has_cobicar = ( - "writing COBICAR.lobster..." in lines and "SKIPPING writing COBICAR.lobster..." not in lines - ) + "writing COBICAR.lobster..." in lines + or "Writing COBICAR.lobster, ICOBILIST.lobster and NcICOBILIST.lobster..." in lines + ) and "SKIPPING writing COBICAR.lobster..." not in lines self.has_cobicar_lcfo = "writing COBICAR.LCFO.lobster..." in lines self.has_cohpcar_lcfo = "writing COHPCAR.LCFO.lobster..." in lines From e855bb31f85ad762325c7cfc995e88c555927337 Mon Sep 17 00:00:00 2001 From: kueltzen Date: Sat, 10 Jan 2026 10:46:40 +0100 Subject: [PATCH 147/180] Added get_nn_info test case that fails without PR changes (correct ordering of all edge attributes) --- tests/io/lobster/test_lobsterenv.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index f65b739413..39c6c547e8 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -634,6 +634,17 @@ def test_get_nn_info(self): == 0 ) + # NaSi + # all bonds + # passes after changes introduced in PR #4148 (correct ordering of all edge attributes) + nn0 = self.chem_env_lobster0_NaSi.get_nn_info(structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaSi.gz"), n=0) + nn11 = self.chem_env_lobster0_NaSi.get_nn_info( + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaSi.gz"), n=11 + ) + assert next(d["edge_properties"] for d in nn0 if d.get("site_index") == 11) == next( + d["edge_properties"] for d in nn11 if d.get("site_index") == 0 + ) + def test_structure_graph(self): sg = self.chem_env_lobster1_second.get_bonded_structure( structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz") From f6eb0c1143fe77920c029e47eb1b2d5f78e534f4 Mon Sep 17 00:00:00 2001 From: kueltzen Date: Wed, 14 Jan 2026 10:27:35 +0100 Subject: [PATCH 148/180] Fix ruff codespell mypy --- lobsterenv.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index ab2312c48f..b41649e737 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -127,7 +127,7 @@ def __init__( filename_blist_sg2 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. id_blist_sg1 ("icoop" | "icobi"): Identity of data in filename_blist_sg1. id_blist_sg2 ("icoop" | "icobi"): Identity of data in filename_blist_sg2. - backward_compatibility (bool): compatiblity with neighbor detection prior 2025 (less strict). + backward_compatibility (bool): compatibility with neighbor detection prior 2025 (less strict). """ if filename_icohp is not None: self.ICOHP = Icohplist(are_coops=are_coops, are_cobis=are_cobis, filename=filename_icohp) @@ -1065,7 +1065,7 @@ def _find_relevant_atoms_additional_condition( site_idx: int, icohps: dict[str, IcohpValue], additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], - ) -> tuple[list[str], list[float], list[int], list[float]]: + ) -> tuple[list[str], list[float], list[int], list[float], list[tuple[float, float, float]]]: """Find all relevant atoms that fulfill the additional condition. Args: @@ -1079,8 +1079,8 @@ def _find_relevant_atoms_additional_condition( keys_from_ICOHPs: list[str] = [] lengths_from_ICOHPs: list[float] = [] neighbors_from_ICOHPs: list[int] = [] - icohps_from_ICOHPs: list[IcohpValue] = [] - translation_from_ICOHPs: list[list[int, int, int]] = [] + icohps_from_ICOHPs: list[float] = [] + translation_from_ICOHPs: list[tuple[float, float, float]] = [] for key, icohp in icohps.items(): atomnr1 = self._get_atomnumber(icohp._atom1) @@ -1582,15 +1582,18 @@ class ICOHPNeighborsInfo(NamedTuple): def check_ICOHPs(lengths_from_ICOHPs, selected_ICOHPs, translation, length_threshold=0.01, energy_threshold=0.1): for i in range(len(lengths_from_ICOHPs)): for j in range(i + 1, len(lengths_from_ICOHPs)): - if abs(lengths_from_ICOHPs[i] - lengths_from_ICOHPs[j]) < length_threshold: - if abs(selected_ICOHPs[i] - selected_ICOHPs[j]) > energy_threshold and ( + if ( + abs(lengths_from_ICOHPs[i] - lengths_from_ICOHPs[j]) < length_threshold + and abs(selected_ICOHPs[i] - selected_ICOHPs[j]) > energy_threshold + and ( translation[i][0] == -translation[j][0] and translation[i][1] == -translation[j][1] and translation[i][2] == -translation[j][2] - ): - warnings.warn( - f"Lengths {lengths_from_ICOHPs[i]} and {lengths_from_ICOHPs[j]} are very close " - f"and translation exactly opposite, but corresponding ICOHPs {selected_ICOHPs[i]} " - f"and {selected_ICOHPs[j]} are not. Our neighbor detection might fail.", - stacklevel=2, - ) + ) + ): + warnings.warn( + f"Lengths {lengths_from_ICOHPs[i]} and {lengths_from_ICOHPs[j]} are very close " + f"and translation exactly opposite, but corresponding ICOHPs {selected_ICOHPs[i]} " + f"and {selected_ICOHPs[j]} are not. Our neighbor detection might fail.", + stacklevel=2, + ) From d737804c56b3a86321e02db97160870ece7024ef Mon Sep 17 00:00:00 2001 From: kueltzen Date: Wed, 14 Jan 2026 10:27:35 +0100 Subject: [PATCH 149/180] Fix ruff codespell mypy --- cohp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cohp.py b/cohp.py index ae55d7c875..f7a3652fba 100644 --- a/cohp.py +++ b/cohp.py @@ -980,6 +980,7 @@ def __init__( self._atom1 = atom1 self._atom2 = atom2 self._length = length + # TODO switch to integer translation in LOBSTER classes self._translation = translation self._num = num self._icohp = icohp @@ -1044,13 +1045,13 @@ def is_spin_polarized(self) -> bool: return self._is_spin_polarized @property - def translation(self) -> list[int, int, int]: + def translation(self) -> tuple[float, float, float]: """ Returns the translation vector with respect to the origin cell as defined in LOBSTER. Returns: - list[int, int, int] + tuple[float, float, float] """ return self._translation From 62a9c94f692d499736c869b963fa239dc159929a Mon Sep 17 00:00:00 2001 From: kueltzen Date: Wed, 14 Jan 2026 10:50:12 +0100 Subject: [PATCH 150/180] check_ICOHPs skips those in same unit cell and uses int translations (TODO move translation in all LOBSTER classes to int instead of float) --- lobsterenv.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index b41649e737..878f1d6932 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -1585,10 +1585,11 @@ def check_ICOHPs(lengths_from_ICOHPs, selected_ICOHPs, translation, length_thres if ( abs(lengths_from_ICOHPs[i] - lengths_from_ICOHPs[j]) < length_threshold and abs(selected_ICOHPs[i] - selected_ICOHPs[j]) > energy_threshold + and [int(idx) for idx in translation[i]] != [0.0, 0.0, 0.0] and ( - translation[i][0] == -translation[j][0] - and translation[i][1] == -translation[j][1] - and translation[i][2] == -translation[j][2] + int(translation[i][0]) == -int(translation[j][0]) + and int(translation[i][1]) == -int(translation[j][1]) + and int(translation[i][2]) == -int(translation[j][2]) ) ): warnings.warn( From c7092bbd0ef266ff7fb8405e56563c39d44351d3 Mon Sep 17 00:00:00 2001 From: kueltzen Date: Wed, 14 Jan 2026 11:59:33 +0100 Subject: [PATCH 151/180] Switched translation in LOBSTER classes to tuple[int, int, int] --- lobsterenv.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 878f1d6932..80bc2e9864 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -1065,7 +1065,7 @@ def _find_relevant_atoms_additional_condition( site_idx: int, icohps: dict[str, IcohpValue], additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], - ) -> tuple[list[str], list[float], list[int], list[float], list[tuple[float, float, float]]]: + ) -> tuple[list[str], list[float], list[int], list[float], list[tuple[int, int, int]]]: """Find all relevant atoms that fulfill the additional condition. Args: @@ -1080,7 +1080,7 @@ def _find_relevant_atoms_additional_condition( lengths_from_ICOHPs: list[float] = [] neighbors_from_ICOHPs: list[int] = [] icohps_from_ICOHPs: list[float] = [] - translation_from_ICOHPs: list[tuple[float, float, float]] = [] + translation_from_ICOHPs: list[tuple[int, int, int]] = [] for key, icohp in icohps.items(): atomnr1 = self._get_atomnumber(icohp._atom1) @@ -1587,9 +1587,9 @@ def check_ICOHPs(lengths_from_ICOHPs, selected_ICOHPs, translation, length_thres and abs(selected_ICOHPs[i] - selected_ICOHPs[j]) > energy_threshold and [int(idx) for idx in translation[i]] != [0.0, 0.0, 0.0] and ( - int(translation[i][0]) == -int(translation[j][0]) - and int(translation[i][1]) == -int(translation[j][1]) - and int(translation[i][2]) == -int(translation[j][2]) + translation[i][0] == -translation[j][0] + and translation[i][1] == -translation[j][1] + and translation[i][2] == -translation[j][2] ) ): warnings.warn( From 98ddf14c65074789213112d5988c5268ec199c7f Mon Sep 17 00:00:00 2001 From: kueltzen Date: Wed, 14 Jan 2026 11:59:33 +0100 Subject: [PATCH 152/180] Switched translation in LOBSTER classes to tuple[int, int, int] --- cohp.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cohp.py b/cohp.py index f7a3652fba..d2eb33a694 100644 --- a/cohp.py +++ b/cohp.py @@ -946,7 +946,7 @@ def __init__( atom1: str, atom2: str, length: float, - translation: tuple[float, float, float], + translation: tuple[int, int, int], num: int, icohp: dict[Spin, float], are_coops: bool = False, @@ -959,7 +959,7 @@ def __init__( atom1 (str): The first atom that contributes to the bond. atom2 (str): The second atom that contributes to the bond. length (float): Bond length. - translation (tuple[float, float, float]): cell translation vector, e.g. (0, 0, 0). + translation (tuple[int, int, int]): cell translation vector, e.g. (0, 0, 0). num (int): The number of equivalent bonds. icohp (dict[Spin, float]): {Spin.up: ICOHP_up, Spin.down: ICOHP_down} are_coops (bool): Whether these are COOPs. @@ -980,7 +980,6 @@ def __init__( self._atom1 = atom1 self._atom2 = atom2 self._length = length - # TODO switch to integer translation in LOBSTER classes self._translation = translation self._num = num self._icohp = icohp @@ -1045,13 +1044,13 @@ def is_spin_polarized(self) -> bool: return self._is_spin_polarized @property - def translation(self) -> tuple[float, float, float]: + def translation(self) -> tuple[int, int, int]: """ Returns the translation vector with respect to the origin cell as defined in LOBSTER. Returns: - tuple[float, float, float] + tuple[int, int, int] """ return self._translation @@ -1142,7 +1141,7 @@ def __init__( list_atom1: list[str], list_atom2: list[str], list_length: list[float], - list_translation: list[tuple[float, float, float]], + list_translation: list[tuple[int, int, int]], list_num: list[int], list_icohp: list[dict[Spin, float]], is_spin_polarized: bool, @@ -1156,7 +1155,7 @@ def __init__( list_atom1 (list[str]): Atom names, e.g. "O1". list_atom2 (list[str]): Atom names, e.g. "O1". list_length (list[float]): Bond lengths in Angstrom. - list_translation (list[tuple[float, float, float]]): Cell translation vectors. + list_translation (list[tuple[int, int, int]]): Cell translation vectors. list_num (list[int]): Numbers of equivalent bonds, usually 1 starting from LOBSTER 3.0.0. list_icohp (list[dict]): Dicts as {Spin.up: ICOHP_up, Spin.down: ICOHP_down}. is_spin_polarized (bool): Whether the calculation is spin polarized. From f88bdfb96710ea885567dfa6763b3c99442e025f Mon Sep 17 00:00:00 2001 From: kueltzen Date: Wed, 14 Jan 2026 12:02:16 +0100 Subject: [PATCH 153/180] removed mypy ignore --- outputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/outputs.py b/outputs.py index 4044463bea..086440e218 100644 --- a/outputs.py +++ b/outputs.py @@ -575,7 +575,7 @@ def __init__( list_atom1=atom1_list, list_atom2=atom2_list, list_length=lens, - list_translation=translations, # type: ignore[arg-type] + list_translation=translations, list_num=nums, list_icohp=icohps, is_spin_polarized=self.is_spin_polarized, From d74d20ecdda9987ec60931c202fb76c948533a46 Mon Sep 17 00:00:00 2001 From: kueltzen Date: Wed, 14 Jan 2026 12:16:22 +0100 Subject: [PATCH 154/180] Added warning for case with backward_compatibility True --- lobsterenv.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lobsterenv.py b/lobsterenv.py index 80bc2e9864..b987108fb3 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -1017,6 +1017,12 @@ def _find_environments( ) ) else: + warnings.warn( + "You are using an older version for neighbor detection that might not consider rare " + "LOBSTER edge cases. Consider switching LobsterNeighbors().backward_compatibility " + "to False for more advanced neighbor detection.", + stacklevel=2, + ) comparison = ( np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 From 16d01767855513e80bfa5a8a3e590eac22077e29 Mon Sep 17 00:00:00 2001 From: kueltzen Date: Wed, 14 Jan 2026 12:28:01 +0100 Subject: [PATCH 155/180] check_ICOHPs float to int correction --- lobsterenv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lobsterenv.py b/lobsterenv.py index b987108fb3..f9841ce2d9 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -1591,7 +1591,7 @@ def check_ICOHPs(lengths_from_ICOHPs, selected_ICOHPs, translation, length_thres if ( abs(lengths_from_ICOHPs[i] - lengths_from_ICOHPs[j]) < length_threshold and abs(selected_ICOHPs[i] - selected_ICOHPs[j]) > energy_threshold - and [int(idx) for idx in translation[i]] != [0.0, 0.0, 0.0] + and translation[i] != (0, 0, 0) and ( translation[i][0] == -translation[j][0] and translation[i][1] == -translation[j][1] From 68268355a93bd9bde9464dc92f974dcd8b4f854a Mon Sep 17 00:00:00 2001 From: kueltzen Date: Wed, 14 Jan 2026 14:09:34 +0100 Subject: [PATCH 156/180] Added test for backward_compatibility warning --- tests/io/lobster/test_lobsterenv.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 870e5c984f..a2a2c49969 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -933,3 +933,11 @@ def test_valences(self): self.chem_env_w_obj.valences, [0.66] * 4 + [0.69] * 4 + [-0.68] * 3 + [-0.69] + [-0.67] * 4 ) # charge_obj assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA + + def test_backward_compatibility_warning(self): + with pytest.warns(UserWarning, match="You are using an older version for neighbor detection"): + _ = LobsterNeighbors( + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), + backward_compatibility=True, + ) From 5784075d3dec169bea5308ad0794673446583379 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Wed, 18 Feb 2026 09:34:34 +0800 Subject: [PATCH 157/180] Ruff fixes. --- inputs.py | 4 +--- lobsterenv.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/inputs.py b/inputs.py index 16c8d534d8..90d9331c06 100644 --- a/inputs.py +++ b/inputs.py @@ -30,9 +30,7 @@ from pymatgen.util.due import Doi, due if TYPE_CHECKING: - from typing import Any, ClassVar, Literal - - from typing_extensions import Self + from typing import Any, ClassVar, Literal, Self from pymatgen.core.composition import Composition from pymatgen.core.structure import IStructure diff --git a/lobsterenv.py b/lobsterenv.py index 2f54f82218..1f4d5ffcae 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -32,11 +32,10 @@ from pymatgen.util.due import Doi, due if TYPE_CHECKING: - from typing import Any, Literal + from typing import Any, Literal, Self import matplotlib as mpl from numpy.typing import NDArray - from typing_extensions import Self from pymatgen.core import IStructure, PeriodicNeighbor, PeriodicSite, Structure from pymatgen.core.periodic_table import Element From e8bb722bc8f698e10fcde9223ec783ed29599a91 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Wed, 18 Feb 2026 09:34:34 +0800 Subject: [PATCH 158/180] Ruff fixes. --- cohp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cohp.py b/cohp.py index 9e1d623c23..b583ab7fbb 100644 --- a/cohp.py +++ b/cohp.py @@ -32,10 +32,9 @@ if TYPE_CHECKING: from collections.abc import Mapping - from typing import Any, Literal + from typing import Any, Literal, Self from numpy.typing import ArrayLike, NDArray - from typing_extensions import Self from pymatgen.util.typing import PathLike, SpinLike From f49afc3d574fda0d00987084a1a26d6416fa5321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Haoyu=20=28Daniel=29=20YANG=20=E6=9D=A8=E6=B5=A9=E5=AE=87?= Date: Wed, 18 Feb 2026 23:32:01 +0100 Subject: [PATCH 159/180] Replace float compare with `==` with `math.isclose/approx` (#4591) * replace float compare with isclose * simplify some if/elif/else logic --- tests/io/lobster/test_outputs.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index 6362b40d4f..dacb34f8e5 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -2,6 +2,7 @@ import copy import gzip +import math import os import numpy as np @@ -1677,15 +1678,15 @@ def test_has_good_quality_check_occupied_bands_patched(self): # Assert for expected results if ( ( - actual_deviation == 0.05 + math.isclose(actual_deviation, 0.05) and number_occ_bands_spin_up <= 7 and number_occ_bands_spin_down <= 7 and spin is Spin.up ) - or (actual_deviation == 0.05 and spin is Spin.down) - or actual_deviation == 0.1 + or (math.isclose(actual_deviation, 0.05) and spin is Spin.down) + or math.isclose(actual_deviation, 0.1) or ( - actual_deviation in [0.2, 0.5, 1.0] + any(np.isclose(actual_deviation, [0.2, 0.5, 1.0])) and number_occ_bands_spin_up == 0 and number_occ_bands_spin_down == 0 ) From 833a44d83bc66655cc13fef65e1ef948d65d9ed2 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Fri, 20 Feb 2026 02:07:40 +0100 Subject: [PATCH 160/180] Simplify and clean up LobsterNeighbors (#4482) * prep for deprecation * add from_files method * mypy ignore * ignore assignment * rename args * add tests for from_files class method * add error raised files * improve error handling for non-standard environments * add tests for on_error case --- tests/io/lobster/test_lobsterenv.py | 119 +++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 35160ca4d9..39d6dfe86e 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -1,5 +1,7 @@ from __future__ import annotations +import warnings + import numpy as np import pytest from numpy.testing import assert_allclose @@ -42,6 +44,13 @@ def setup_method(self): structure=Structure.from_file(f"{TEST_DIR}/POSCAR.mp_190.gz"), additional_condition=0, ) + # test __init_new__ using from_files + self.chem_env_lobster0_from_file = LobsterNeighbors.from_files( + are_coops=False, + structure_path=f"{TEST_DIR}/POSCAR.mp_190.gz", + icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + additional_condition=0, + ) # only cation-cation, anion-anion bonds self.chem_env_lobster5 = LobsterNeighbors( @@ -51,6 +60,13 @@ def setup_method(self): additional_condition=5, ) + self.chem_env_lobster5_from_file = LobsterNeighbors.from_files( + are_coops=False, + structure_path=f"{TEST_DIR}/POSCAR.mp_190.gz", + icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + additional_condition=5, + ) + # only cation-cation bonds self.chem_env_lobster6 = LobsterNeighbors( are_coops=False, @@ -59,6 +75,13 @@ def setup_method(self): additional_condition=6, ) + self.chem_env_lobster6_from_file = LobsterNeighbors.from_files( + are_coops=False, + structure_path=f"{TEST_DIR}/POSCAR.mp_190.gz", + icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + additional_condition=6, + ) + # 2,3,4 are not tested so far self.chem_env_lobster2 = LobsterNeighbors( are_coops=False, @@ -132,6 +155,14 @@ def setup_method(self): noise_cutoff=None, ) + self.chem_env_lobster1_coop_NaCl_from_file = LobsterNeighbors.from_files( + are_coops=True, + structure_path=f"{TEST_DIR}/POSCAR.NaCl.gz", + icoxxlist_path=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", + additional_condition=1, + noise_cutoff=None, + ) + self.chem_env_lobster1_cobi_NaCl = LobsterNeighbors( are_coops=True, filename_icohp=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", @@ -140,6 +171,14 @@ def setup_method(self): noise_cutoff=None, ) + self.chem_env_lobster1_cobi_NaCl_from_file = LobsterNeighbors.from_files( + are_coops=True, + structure_path=f"{TEST_DIR}/POSCAR.NaCl.gz", + icoxxlist_path=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", + additional_condition=1, + noise_cutoff=None, + ) + self.chem_env_lobster1_cobi_mp470 = LobsterNeighbors( are_coops=True, filename_icohp=f"{TEST_DIR}/ICOBILIST.lobster.mp_470.gz", @@ -281,6 +320,58 @@ def setup_method(self): additional_condition=0, adapt_extremum_to_add_cond=True, ) + # test LSE on_error + self.chem_env_on_error = LobsterNeighbors( + are_coops=False, + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-1018096.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-1018096.gz"), + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-1018096.gz", + additional_condition=0, + adapt_extremum_to_add_cond=True, + valences_from_charges=True, + ) + + def test_init_new(self): + # additional condition 0 + ref_lse = self.chem_env_lobster0.get_light_structure_environment(only_cation_environments=False) + test_lse = self.chem_env_lobster0_from_file.get_light_structure_environment(only_cation_environments=False) + + assert self.chem_env_lobster0.anion_types == self.chem_env_lobster0_from_file.anion_types + assert ref_lse.coordination_environments == test_lse.coordination_environments + assert ref_lse.valences == test_lse.valences + + # additional condition 5 + ref_lse = self.chem_env_lobster5.get_light_structure_environment(only_cation_environments=False) + test_lse = self.chem_env_lobster5_from_file.get_light_structure_environment(only_cation_environments=False) + + assert self.chem_env_lobster5.anion_types == self.chem_env_lobster5_from_file.anion_types + assert ref_lse.coordination_environments == test_lse.coordination_environments + assert ref_lse.valences == test_lse.valences + + # additional condition 6 + ref_lse = self.chem_env_lobster6.get_light_structure_environment(only_cation_environments=False) + test_lse = self.chem_env_lobster6_from_file.get_light_structure_environment(only_cation_environments=False) + assert self.chem_env_lobster6.anion_types == self.chem_env_lobster6_from_file.anion_types + assert ref_lse.coordination_environments == test_lse.coordination_environments + assert ref_lse.valences == test_lse.valences + + # coop NaCl + ref_lse = self.chem_env_lobster1_coop_NaCl.get_light_structure_environment(only_cation_environments=False) + test_lse = self.chem_env_lobster1_coop_NaCl_from_file.get_light_structure_environment( + only_cation_environments=False + ) + assert self.chem_env_lobster1_coop_NaCl.anion_types == self.chem_env_lobster1_coop_NaCl_from_file.anion_types + assert ref_lse.coordination_environments == test_lse.coordination_environments + assert ref_lse.valences == test_lse.valences + + # cobi NaCl + ref_lse = self.chem_env_lobster1_cobi_NaCl.get_light_structure_environment(only_cation_environments=False) + test_lse = self.chem_env_lobster1_cobi_NaCl_from_file.get_light_structure_environment( + only_cation_environments=False + ) + assert self.chem_env_lobster1_cobi_NaCl.anion_types == self.chem_env_lobster1_cobi_NaCl_from_file.anion_types + assert ref_lse.coordination_environments == test_lse.coordination_environments + assert ref_lse.valences == test_lse.valences def test_cation_anion_mode_without_ions(self): with pytest.raises( @@ -846,7 +937,7 @@ def test_get_info_cohps_to_neighbors(self): structure_file=f"{TEST_DIR}/POSCAR.NaSi.gz", ) plot_label_obj, _summed_cohpcar_NaSi_obj = self.chem_env_w_obj.get_info_cohps_to_neighbors( - obj_cohpcar=obj_cohpcar, + coxxcar_obj=obj_cohpcar, isites=[8], onlycation_isites=False, only_bonds_to=["Na"], @@ -911,3 +1002,29 @@ def test_valences(self): ] assert_allclose(self.chem_env_w_obj.valences, [0.67] * 4 + [0.7] * 4 + [-0.7] * 4 + [-0.68] * 4) # charge_obj assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA + + def test_on_error_case(self): + with pytest.raises( + ValueError, + match=r"Environment cannot be determined for site 0\. Number of neighbors \(18\) is larger than 13\.", + ): + _ = self.chem_env_on_error.get_light_structure_environment(on_error="raise") + + with pytest.warns( + UserWarning, + match=r"Site 0 has 18 neighbors \(>13\)\. Using coordination number instead of geometry\.", + ) as warnings_record: + lse = self.chem_env_on_error.get_light_structure_environment(on_error="warn") + + assert lse.coordination_environments[0][0]["ce_symbol"] == "18" + assert [str(warn.message) for warn in warnings_record] == [ + "Site 0 has 18 neighbors (>13). Using coordination number instead of geometry.", + "Site 1 has 18 neighbors (>13). Using coordination number instead of geometry.", + ] + + # if on_error is "ignore", no error should be raised and no warning should be issued + with warnings.catch_warnings(record=True) as warnings_record: + lse = self.chem_env_on_error.get_light_structure_environment(on_error="ignore") + + assert lse.coordination_environments[0][0]["ce_symbol"] == "18" + assert warnings_record == [] From c5b9ad8a2704951609429e70e17daa71d1ea1512 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Fri, 20 Feb 2026 02:07:40 +0100 Subject: [PATCH 161/180] Simplify and clean up LobsterNeighbors (#4482) * prep for deprecation * add from_files method * mypy ignore * ignore assignment * rename args * add tests for from_files class method * add error raised files * improve error handling for non-standard environments * add tests for on_error case --- lobsterenv.py | 265 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 249 insertions(+), 16 deletions(-) diff --git a/lobsterenv.py b/lobsterenv.py index 1f4d5ffcae..7ddfd2f2e2 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -15,6 +15,7 @@ import copy import math import tempfile +import warnings from typing import TYPE_CHECKING, NamedTuple import matplotlib as mpl @@ -25,6 +26,7 @@ from pymatgen.analysis.chemenv.coordination_environments.coordination_geometry_finder import LocalGeometryFinder from pymatgen.analysis.chemenv.coordination_environments.structure_environments import LightStructureEnvironments from pymatgen.analysis.local_env import NearNeighbors +from pymatgen.core import Structure from pymatgen.electronic_structure.cohp import CompleteCohp from pymatgen.electronic_structure.core import Spin from pymatgen.electronic_structure.plotter import CohpPlotter @@ -37,7 +39,7 @@ import matplotlib as mpl from numpy.typing import NDArray - from pymatgen.core import IStructure, PeriodicNeighbor, PeriodicSite, Structure + from pymatgen.core import IStructure, PeriodicNeighbor, PeriodicSite from pymatgen.core.periodic_table import Element from pymatgen.electronic_structure.cohp import IcohpCollection, IcohpValue from pymatgen.util.typing import PathLike @@ -125,6 +127,20 @@ def __init__( id_blist_sg1 ("icoop" | "icobi"): Identity of data in filename_blist_sg1. id_blist_sg2 ("icoop" | "icobi"): Identity of data in filename_blist_sg2. """ + warnings.warn( + "Instantiation with file paths (filename_icohp, filename_charge, filename_blist_sg1, filename_blist_sg2.) " + "is deprecated and will be removed on 31-03-2026. " + "Please use `LobsterNeighbors.from_file` instead.", + DeprecationWarning, + stacklevel=2, + ) + warnings.warn( + "Class init args obj_icohp, obj_charge will be " + "renamed to icoxxlist_obj and charge_obj respectively on 31-03-2026.", + DeprecationWarning, + stacklevel=2, + ) + if filename_icohp is not None: self.ICOHP = Icohplist(are_coops=are_coops, are_cobis=are_cobis, filename=filename_icohp) elif obj_icohp is not None: @@ -231,6 +247,200 @@ def __init__( adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, ) + def __init_new__( + self, + structure: Structure, + icoxxlist_obj: Icohplist, + are_coops: bool = False, + are_cobis: bool = False, + charge_obj: Charge | None = None, + valences: list[float] | None = None, + limits: tuple[float, float] | None = None, + additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, + only_bonds_to: list[str] | None = None, + perc_strength_icohp: float = 0.15, + noise_cutoff: float = 0.1, + valences_from_charges: bool = False, + which_charge: Literal["Mulliken", "Loewdin"] = "Mulliken", + adapt_extremum_to_add_cond: bool = False, + add_additional_data_sg: bool = False, + bonding_list_1: Icohplist | None = None, + bonding_list_2: Icohplist | None = None, + id_blist_sg1: Literal["icoop", "icobi"] = "icoop", + id_blist_sg2: Literal["icoop", "icobi"] = "icobi", + ): + """ + Args: + structure (Structure): Typically constructed by Structure.from_file("POSCAR"). + icoxxlist_obj (Icohplist): Icohplist object. + are_coops (bool): Whether the file is a ICOOPLIST.lobster (True) or a + ICOHPLIST.lobster (False). Only tested for ICOHPLIST.lobster so far. + are_cobis (bool): Whether the file is a ICOBILIST.lobster (True) or + a ICOHPLIST.lobster (False). + charge_obj (Charge): Charge object. + valences (list[float]): Valence/charge for each element. + limits (tuple[float, float]): Range to decide which ICOHPs (ICOOP + or ICOBI) should be considered. + additional_condition (int): Additional condition that decides + which kind of bonds will be considered + 0 - NO_ADDITIONAL_CONDITION + 1 - ONLY_ANION_CATION_BONDS + 2 - NO_ELEMENT_TO_SAME_ELEMENT_BONDS + 3 - ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS + 4 - ONLY_ELEMENT_TO_OXYGEN_BONDS + 5 - DO_NOT_CONSIDER_ANION_CATION_BONDS + 6 - ONLY_CATION_CATION_BONDS + only_bonds_to (list[str]): Only consider bonds to certain elements (e.g. ["O"] for oxygen). + perc_strength_icohp (float): If no "limits" are given, this will decide + which ICOHPs will be considered (relative to the strongest ICOHP/ICOOP/ICOBI). + noise_cutoff (float): The lower limit of ICOHPs considered. + valences_from_charges (bool): If True and path to CHARGE.lobster is provided, + will use LOBSTER charges (Mulliken) instead of valences. + which_charge ("Mulliken" | "Loewdin"): Source of charge. + adapt_extremum_to_add_cond (bool): Whether to adapt the limits to only + focus on the bonds determined by the additional condition. + add_additional_data_sg (bool): Add the information from bonding_list_1. + bonding_list_1 (Icohplist): Additional ICOOP, ICOBI data for structure graphs. + bonding_list_2 (Icohplist): Additional ICOOP, ICOBI data for structure graphs. + id_blist_sg1 ("icoop" | "icobi"): Identity of data in bonding_list_1. + id_blist_sg2 ("icoop" | "icobi"): Identity of data in bonding_list_2. + """ + self.structure = structure + self.ICOHP = icoxxlist_obj + self.Icohpcollection = icoxxlist_obj.icohpcollection + self.charge_obj = charge_obj + self.valences = valences + self.limits = limits + self.only_bonds_to = only_bonds_to + self.adapt_extremum_to_add_cond = adapt_extremum_to_add_cond + self.add_additional_data_sg = add_additional_data_sg + self.bonding_list_1 = bonding_list_1 # type:ignore[assignment] + self.bonding_list_2 = bonding_list_2 # type:ignore[assignment] + self.id_blist_sg1 = id_blist_sg1.lower() + self.id_blist_sg2 = id_blist_sg2.lower() + self.noise_cutoff = noise_cutoff + self.additional_condition = additional_condition + self.are_coops = are_coops + self.are_cobis = are_cobis + + # validate + if self.id_blist_sg1 not in {"icoop", "icobi"} or self.id_blist_sg2 not in {"icoop", "icobi"}: + raise ValueError("Algorithm can only work with ICOOPs, ICOBIs") + + if additional_condition not in range(7): + raise ValueError(f"Unexpected {additional_condition=}, must be one of {list(range(7))}") + + if self.valences is None and valences_from_charges: + if which_charge == "Mulliken": + self.valences = charge_obj.mulliken + elif which_charge == "Loewdin": + self.valences = charge_obj.loewdin + else: + bv_analyzer = BVAnalyzer() + try: + self.valences = bv_analyzer.get_valences(structure=self.structure) # type:ignore[arg-type] + except ValueError as exc: + self.valences = None + if additional_condition in {1, 3, 5, 6}: + raise ValueError( + "Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work" + ) from exc + + if np.allclose(self.valences or [], np.zeros_like(self.valences)) and additional_condition in {1, 3, 5, 6}: + raise ValueError("All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work") + + if limits is None: + self.lowerlimit = self.upperlimit = None + else: + self.lowerlimit, self.upperlimit = limits + + # evaluate coordination environments + self._evaluate_ce( + lowerlimit=limits[0] if limits else None, + upperlimit=limits[1] if limits else None, + only_bonds_to=only_bonds_to, + additional_condition=additional_condition, + perc_strength_icohp=perc_strength_icohp, + adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, + ) + + @classmethod + def from_files( + cls, + structure_path: PathLike = "CONTCAR", + icoxxlist_path: PathLike = "ICOHPLIST.lobster", + are_coops: bool = False, + are_cobis: bool = False, + charge_path: PathLike | None = None, + blist_sg1_path: PathLike | None = None, + blist_sg2_path: PathLike | None = None, + id_blist_sg1: Literal["icoop", "icobi"] = "icoop", + id_blist_sg2: Literal["icoop", "icobi"] = "icobi", + **kwargs, + ): + """ + Instanitate LobsterNeighbors using file paths. + + Args: + structure_path (PathLike): Path to structure file, typically CONTCAR + icoxxlist_path (PathLike): Path to ICOHPLIST.lobster or + ICOOPLIST.lobster or ICOBILIST.lobster. + are_coops (bool): Whether the file is a ICOOPLIST.lobster (True) or a + ICOHPLIST.lobster (False). Only tested for ICOHPLIST.lobster so far. + are_cobis (bool): Whether the file is a ICOBILIST.lobster (True) or + a ICOHPLIST.lobster (False). + charge_path (PathLike): Path to Charge.lobster. + blist_sg1_path (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. + blist_sg2_path (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. + id_blist_sg1 ("icoop" | "icobi"): Population type in blist_sg1_path. + id_blist_sg2 ("icoop" | "icobi"): Population type in in blist_sg2_path. + """ + structure = Structure.from_file(structure_path) + icoxxlist_obj = Icohplist(filename=icoxxlist_path, are_coops=are_coops, are_cobis=are_cobis) + charge_obj = Charge(filename=charge_path) if charge_path else None + bonding_list_1 = bonding_list_2 = None + + if kwargs.get("add_additional_data_sg", False): + if id_blist_sg1 == "icoop": + are_coops_id1 = True + are_cobis_id1 = False + else: + are_coops_id1 = False + are_cobis_id1 = True + + bonding_list_1 = Icohplist( + filename=blist_sg1_path, + are_coops=are_coops_id1, + are_cobis=are_cobis_id1, + ) + + if id_blist_sg2 == "icoop": + are_coops_id2 = True + are_cobis_id2 = False + else: + are_coops_id2 = False + are_cobis_id2 = True + + bonding_list_2 = Icohplist( + filename=blist_sg2_path, + are_coops=are_coops_id2, + are_cobis=are_cobis_id2, + ) + + obj = cls.__new__(cls) + + obj.__init_new__( + structure=structure, + icoxxlist_obj=icoxxlist_obj, + are_coops=are_coops, + are_cobis=are_cobis, + charge_obj=charge_obj, + bonding_list_1=bonding_list_1, + bonding_list_2=bonding_list_2, + **kwargs, + ) + return obj + @property def structures_allowed(self) -> Literal[True]: """Whether this LobsterNeighbors class can be used with Structure objects.""" @@ -298,6 +508,7 @@ def get_light_structure_environment( self, only_cation_environments: bool = False, only_indices: list[int] | None = None, + on_error: Literal["raise", "warn", "ignore"] = "raise", ) -> LobsterLightStructureEnvironments: """Get a LobsterLightStructureEnvironments object if the structure only contains coordination environments smaller 13. @@ -305,18 +516,40 @@ def get_light_structure_environment( Args: only_cation_environments (bool): Only return data for cations. only_indices (list[int]): Only evaluate indexes in this list. + on_error ("raise" | "warn" | "ignore"): Whether to raise an error, warn or ignore + if the environment of a site cannot be determined. Returns: LobsterLightStructureEnvironments """ lgf = LocalGeometryFinder() lgf.setup_structure(structure=self.structure) # type:ignore[arg-type] - list_ce_symbols = [] - list_csm = [] - list_permut = [] + list_ce_symbols = [] # type: list[str | None] + list_csm = [] # type: list[float | None] + list_permut = [] # type: list[list[int] | None] for idx, _neigh_coords in enumerate(self.list_coords): if (len(_neigh_coords)) > 13: - raise ValueError("Environment cannot be determined. Number of neighbors is larger than 13.") + if on_error == "raise": + raise ValueError( + f"Environment cannot be determined for site {idx}. " + f"Number of neighbors ({len(_neigh_coords)}) is larger than 13." + ) + if on_error == "warn": + warnings.warn( + f"Site {idx} has {len(_neigh_coords)} neighbors (>13). " + f"Using coordination number instead of geometry.", + stacklevel=2, + ) + list_ce_symbols.append(str(len(_neigh_coords))) + list_csm.append(None) + list_permut.append(None) + continue + # "ignore" + list_ce_symbols.append(str(len(_neigh_coords))) + list_csm.append(None) + list_permut.append(None) + continue + # Avoid problems if _neigh_coords is empty if _neigh_coords != []: lgf.setup_local_geometry(isite=idx, coords=_neigh_coords, optimization=2) @@ -450,7 +683,7 @@ def get_info_icohps_to_neighbors( def plot_cohps_of_neighbors( self, path_to_cohpcar: PathLike | None = "COHPCAR.lobster", - obj_cohpcar: CompleteCohp | None = None, + coxxcar_obj: CompleteCohp | None = None, isites: list[int] | None = None, onlycation_isites: bool = True, only_bonds_to: list[str] | None = None, @@ -466,7 +699,7 @@ def plot_cohps_of_neighbors( Args: path_to_cohpcar (PathLike): Path to COHPCAR or COOPCAR or COBICAR. - obj_cohpcar (CompleteCohp): CompleteCohp object + coxxcar_obj (CompleteCohp | None): CompleteCohp object isites (list[int]): Site IDs. If empty, all sites will be used to add the ICOHPs of the neighbors. onlycation_isites (bool): Only use cations, if isite is empty. only_bonds_to (list[str]): Only anions in this list will be considered. @@ -483,7 +716,7 @@ def plot_cohps_of_neighbors( plotlabel, summed_cohp = self.get_info_cohps_to_neighbors( path_to_cohpcar, - obj_cohpcar, + coxxcar_obj, isites, only_bonds_to, onlycation_isites, @@ -504,7 +737,7 @@ def plot_cohps_of_neighbors( def get_info_cohps_to_neighbors( self, path_to_cohpcar: PathLike | None = "COHPCAR.lobster", - obj_cohpcar: CompleteCohp | None = None, + coxxcar_obj: CompleteCohp | None = None, isites: list[int] | None = None, only_bonds_to: list[str] | None = None, onlycation_isites: bool = True, @@ -516,7 +749,7 @@ def get_info_cohps_to_neighbors( Args: path_to_cohpcar (PathLike): Path to COHPCAR/COOPCAR/COBICAR. - obj_cohpcar (CompleteCohp): CompleteCohp object. + coxxcar_obj (CompleteCohp | None): CompleteCohp object. isites (list[int]): The indexes of the sites. only_bonds_to (list[str]): Only show COHPs to selected element, e.g. ["O"]. onlycation_isites (bool): If isites is None, only cation sites will be returned. @@ -539,7 +772,7 @@ def get_info_cohps_to_neighbors( self.structure.to(filename=path, fmt="poscar") if not hasattr(self, "completecohp"): - if path_to_cohpcar is not None and obj_cohpcar is None: + if path_to_cohpcar is not None and coxxcar_obj is None: self.completecohp = CompleteCohp.from_file( fmt="LOBSTER", filename=path_to_cohpcar, @@ -547,10 +780,10 @@ def get_info_cohps_to_neighbors( are_coops=self.are_coops, are_cobis=self.are_cobis, ) - elif obj_cohpcar is not None: - self.completecohp = obj_cohpcar + elif coxxcar_obj is not None: + self.completecohp = coxxcar_obj else: - raise ValueError("Please provide either path_to_cohpcar or obj_cohpcar") + raise ValueError("Please provide either path_to_cohpcar or coxxcar_obj") # Check that the number of bonds in ICOHPLIST and COHPCAR are identical # TODO: Further checks could be implemented @@ -1371,8 +1604,8 @@ class LobsterLightStructureEnvironments(LightStructureEnvironments): @classmethod def from_Lobster( cls, - list_ce_symbol: list[str], - list_csm: list[float], + list_ce_symbol: list[str | None], + list_csm: list[float | None], list_permutation: list, list_neighsite: list[PeriodicSite], list_neighisite: list[list[int]], From 7950da780663ad6b6eab06fe63c2459d950eed95 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Fri, 20 Feb 2026 02:08:04 +0100 Subject: [PATCH 162/180] Fix icohpcollection & add Missing keyword in Lobsterin (#4481) * add custom from_dict method for proper initialization of attributes * pre-commit auto-fixes * add custom as_dict method for proper serialization * adapt existing test to check for serialization * fix icohplist deserialization * add test for nsp * fix LCFO files serialization * update test * Add 'molecule' to the list of keywords * Move molecule to list keywords --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- cohp.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/cohp.py b/cohp.py index b583ab7fbb..080a5430f2 100644 --- a/cohp.py +++ b/cohp.py @@ -1404,7 +1404,11 @@ def as_dict(self) -> dict[str, Any]: { key: { "icohp": {str(spin): value for spin, value in val["icohp"].items()}, - "orbitals": [[n, int(orb)] for n, orb in val["orbitals"]], + "orbitals": ( + [[n, int(orb)] for n, orb in val["orbitals"]] + if isinstance(val["orbitals"][0], (list, tuple)) + else list(val["orbitals"]) # Handle LCFO orbitals + ), } for key, val in entry.items() } @@ -1432,17 +1436,24 @@ def from_dict(cls, dct: dict[str, Any]) -> Self: for key in lab_orb_icohp[orb]: sub_dict = {} if key == "icohp": - sub_dict[key] = { - Spin.up: lab_orb_icohp[orb][key]["1"], - Spin.down: lab_orb_icohp[orb][key]["-1"], - } + if dct.get("is_spin_polarized"): + sub_dict[key] = { + Spin.up: lab_orb_icohp[orb][key]["1"], + Spin.down: lab_orb_icohp[orb][key]["-1"], + } + else: + sub_dict[key] = {Spin.up: lab_orb_icohp[orb][key]["1"]} if key == "orbitals": orb_temp = [] for item in lab_orb_icohp[orb][key]: - item[1] = Orbital(item[1]) - orb_temp.append(item) + # Handle LCFO orbitals + if isinstance(item, (list, tuple)): + item[1] = Orbital(item[1]) + orb_temp.append(tuple(item)) + else: + orb_temp.append(item) sub_dict[key] = orb_temp # type: ignore[assignment] new_list_orb[bond_num][orb].update(sub_dict) From e65cf001a60e7da32bb566726acfef1f5fca9db6 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Fri, 20 Feb 2026 02:08:04 +0100 Subject: [PATCH 163/180] Fix icohpcollection & add Missing keyword in Lobsterin (#4481) * add custom from_dict method for proper initialization of attributes * pre-commit auto-fixes * add custom as_dict method for proper serialization * adapt existing test to check for serialization * fix icohplist deserialization * add test for nsp * fix LCFO files serialization * update test * Add 'molecule' to the list of keywords * Move molecule to list keywords --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- tests/io/lobster/test_outputs.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index dacb34f8e5..51c351335f 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -2170,14 +2170,15 @@ def test_values(self): assert self.icohp_lcfo_non_orbitalwise.icohplist["16"]["icohp"][Spin.down] == approx(-0.29842) def test_msonable(self): - dict_data = self.icobi_orbitalwise_spinpolarized.as_dict() - icohplist_from_dict = Icohplist.from_dict(dict_data) - all_attributes = vars(self.icobi_orbitalwise_spinpolarized) - for attr_name, attr_value in all_attributes.items(): - if isinstance(attr_value, IcohpCollection): - assert getattr(icohplist_from_dict, attr_name).as_dict() == attr_value.as_dict() - else: - assert getattr(icohplist_from_dict, attr_name) == attr_value + for icohplist_obj in [self.icobi_orbitalwise_spinpolarized, self.icohp_nacl_511_nsp, self.icohp_lcfo]: + dict_data = icohplist_obj.as_dict() + icohplist_from_dict = Icohplist.from_dict(dict_data) + all_attributes = vars(icohplist_obj) + for attr_name, attr_value in all_attributes.items(): + if isinstance(attr_value, IcohpCollection): + assert getattr(icohplist_from_dict, attr_name).as_dict() == attr_value.as_dict() + else: + assert getattr(icohplist_from_dict, attr_name) == attr_value def test_missing_trailing_newline(self): fname = f"{self.tmp_path}/icohplist" From d0fc6ffc9518fded53a1ed1dfcab2e5d1b08396c Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Fri, 20 Feb 2026 02:08:04 +0100 Subject: [PATCH 164/180] Fix icohpcollection & add Missing keyword in Lobsterin (#4481) * add custom from_dict method for proper initialization of attributes * pre-commit auto-fixes * add custom as_dict method for proper serialization * adapt existing test to check for serialization * fix icohplist deserialization * add test for nsp * fix LCFO files serialization * update test * Add 'molecule' to the list of keywords * Move molecule to list keywords --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- inputs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/inputs.py b/inputs.py index 90d9331c06..c62d4d98b2 100644 --- a/inputs.py +++ b/inputs.py @@ -138,6 +138,7 @@ class Lobsterin(UserDict, MSONable): "createFatband", "customSTOforAtom", "cobiBetween", + "molecule", "printLmosOnAtomswriteAtomicDensities", ) From 91d2cb58b5dbb6addf0b23b5b8d08d3cd689a181 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 06:23:22 +0000 Subject: [PATCH 165/180] pre-commit auto-fixes --- tests/io/lobster/test_lobsterenv.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 1c60af8de1..ec3be3aa25 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -2,7 +2,6 @@ import warnings -import numpy as np import pytest from numpy.testing import assert_allclose from pytest import approx From 91ee32cc17d8596c99fefa8d8a79a57cb5223e3c Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Fri, 20 Feb 2026 06:52:59 +0000 Subject: [PATCH 166/180] fix missing new attribute and failing test --- tests/io/lobster/test_lobsterenv.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index ec3be3aa25..b238b20606 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -48,8 +48,8 @@ def setup_method(self): # test __init_new__ using from_files self.chem_env_lobster0_from_file = LobsterNeighbors.from_files( are_coops=False, - structure_path=f"{TEST_DIR}/POSCAR.mp_190.gz", - icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + structure_path=f"{TEST_DIR}/CONTCAR.mp-190.gz", + icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", additional_condition=0, ) @@ -63,8 +63,8 @@ def setup_method(self): self.chem_env_lobster5_from_file = LobsterNeighbors.from_files( are_coops=False, - structure_path=f"{TEST_DIR}/POSCAR.mp_190.gz", - icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + structure_path=f"{TEST_DIR}/CONTCAR.mp-190.gz", + icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", additional_condition=5, ) @@ -78,8 +78,8 @@ def setup_method(self): self.chem_env_lobster6_from_file = LobsterNeighbors.from_files( are_coops=False, - structure_path=f"{TEST_DIR}/POSCAR.mp_190.gz", - icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp_190.gz", + structure_path=f"{TEST_DIR}/CONTCAR.mp-190.gz", + icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", additional_condition=6, ) @@ -161,7 +161,7 @@ def setup_method(self): self.chem_env_lobster1_coop_NaCl_from_file = LobsterNeighbors.from_files( are_coops=True, - structure_path=f"{TEST_DIR}/POSCAR.NaCl.gz", + structure_path=f"{TEST_DIR}/CONTCAR.NaCl.gz", icoxxlist_path=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", additional_condition=1, noise_cutoff=None, @@ -177,7 +177,7 @@ def setup_method(self): self.chem_env_lobster1_cobi_NaCl_from_file = LobsterNeighbors.from_files( are_coops=True, - structure_path=f"{TEST_DIR}/POSCAR.NaCl.gz", + structure_path=f"{TEST_DIR}/CONTCAR.NaCl.gz", icoxxlist_path=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", additional_condition=1, noise_cutoff=None, From 9ea9f16d674244ad215a3c3ca93b6d487cef176c Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Fri, 20 Feb 2026 06:52:59 +0000 Subject: [PATCH 167/180] fix missing new attribute and failing test --- lobsterenv.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lobsterenv.py b/lobsterenv.py index 99a1c0c810..cf00295553 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -271,6 +271,7 @@ def __init_new__( bonding_list_2: Icohplist | None = None, id_blist_sg1: Literal["icoop", "icobi"] = "icoop", id_blist_sg2: Literal["icoop", "icobi"] = "icobi", + backward_compatibility: bool = False, ): """ Args: @@ -307,6 +308,7 @@ def __init_new__( bonding_list_2 (Icohplist): Additional ICOOP, ICOBI data for structure graphs. id_blist_sg1 ("icoop" | "icobi"): Identity of data in bonding_list_1. id_blist_sg2 ("icoop" | "icobi"): Identity of data in bonding_list_2. + backward_compatibility (bool): If True, will use the old behavior of the code. """ self.structure = structure self.ICOHP = icoxxlist_obj @@ -326,6 +328,8 @@ def __init_new__( self.are_coops = are_coops self.are_cobis = are_cobis + self.backward_compatibility = backward_compatibility + # validate if self.id_blist_sg1 not in {"icoop", "icobi"} or self.id_blist_sg2 not in {"icoop", "icobi"}: raise ValueError("Algorithm can only work with ICOOPs, ICOBIs") From 74ef20db9d76371c1058cfbf3269557a377bda63 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Fri, 20 Feb 2026 20:11:26 +0800 Subject: [PATCH 168/180] Temporary fix for tests. Will be removed soon anyawy. --- tests/io/lobster/test_lobsterenv.py | 70 ++++++++++++++--------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 39d6dfe86e..ce5d3ba509 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -1,7 +1,5 @@ from __future__ import annotations -import warnings - import numpy as np import pytest from numpy.testing import assert_allclose @@ -321,15 +319,15 @@ def setup_method(self): adapt_extremum_to_add_cond=True, ) # test LSE on_error - self.chem_env_on_error = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-1018096.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-1018096.gz"), - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-1018096.gz", - additional_condition=0, - adapt_extremum_to_add_cond=True, - valences_from_charges=True, - ) + # self.chem_env_on_error = LobsterNeighbors( + # are_coops=False, + # filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-1018096.gz", + # structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-1018096.gz"), + # filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-1018096.gz", + # additional_condition=0, + # adapt_extremum_to_add_cond=True, + # valences_from_charges=True, + # ) def test_init_new(self): # additional condition 0 @@ -1003,28 +1001,28 @@ def test_valences(self): assert_allclose(self.chem_env_w_obj.valences, [0.67] * 4 + [0.7] * 4 + [-0.7] * 4 + [-0.68] * 4) # charge_obj assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA - def test_on_error_case(self): - with pytest.raises( - ValueError, - match=r"Environment cannot be determined for site 0\. Number of neighbors \(18\) is larger than 13\.", - ): - _ = self.chem_env_on_error.get_light_structure_environment(on_error="raise") - - with pytest.warns( - UserWarning, - match=r"Site 0 has 18 neighbors \(>13\)\. Using coordination number instead of geometry\.", - ) as warnings_record: - lse = self.chem_env_on_error.get_light_structure_environment(on_error="warn") - - assert lse.coordination_environments[0][0]["ce_symbol"] == "18" - assert [str(warn.message) for warn in warnings_record] == [ - "Site 0 has 18 neighbors (>13). Using coordination number instead of geometry.", - "Site 1 has 18 neighbors (>13). Using coordination number instead of geometry.", - ] - - # if on_error is "ignore", no error should be raised and no warning should be issued - with warnings.catch_warnings(record=True) as warnings_record: - lse = self.chem_env_on_error.get_light_structure_environment(on_error="ignore") - - assert lse.coordination_environments[0][0]["ce_symbol"] == "18" - assert warnings_record == [] + # def test_on_error_case(self): + # with pytest.raises( + # ValueError, + # match=r"Environment cannot be determined for site 0\. Number of neighbors \(18\) is larger than 13\.", + # ): + # _ = self.chem_env_on_error.get_light_structure_environment(on_error="raise") + + # with pytest.warns( + # UserWarning, + # match=r"Site 0 has 18 neighbors \(>13\)\. Using coordination number instead of geometry\.", + # ) as warnings_record: + # lse = self.chem_env_on_error.get_light_structure_environment(on_error="warn") + + # assert lse.coordination_environments[0][0]["ce_symbol"] == "18" + # assert [str(warn.message) for warn in warnings_record] == [ + # "Site 0 has 18 neighbors (>13). Using coordination number instead of geometry.", + # "Site 1 has 18 neighbors (>13). Using coordination number instead of geometry.", + # ] + + # # if on_error is "ignore", no error should be raised and no warning should be issued + # with warnings.catch_warnings(record=True) as warnings_record: + # lse = self.chem_env_on_error.get_light_structure_environment(on_error="ignore") + + # assert lse.coordination_environments[0][0]["ce_symbol"] == "18" + # assert warnings_record == [] From cdaac3db12641ec3a521e1559f5dd7e6cdf7c0e1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:13:12 +0000 Subject: [PATCH 169/180] pre-commit auto-fixes --- tests/io/lobster/test_lobsterenv.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index 48b0ce8e51..cc769b3b24 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -1041,20 +1041,20 @@ def test_on_error_case(self): _ = self.chem_env_on_error.get_light_structure_environment(on_error="raise") with pytest.warns( - UserWarning, - match=r"Site 0 has 18 neighbors \(>13\)\. Using coordination number instead of geometry\.", + UserWarning, + match=r"Site 0 has 18 neighbors \(>13\)\. Using coordination number instead of geometry\.", ) as warnings_record: - lse = self.chem_env_on_error.get_light_structure_environment(on_error="warn") + lse = self.chem_env_on_error.get_light_structure_environment(on_error="warn") assert lse.coordination_environments[0][0]["ce_symbol"] == "18" assert [str(warn.message) for warn in warnings_record] == [ - "Site 0 has 18 neighbors (>13). Using coordination number instead of geometry.", - "Site 1 has 18 neighbors (>13). Using coordination number instead of geometry.", + "Site 0 has 18 neighbors (>13). Using coordination number instead of geometry.", + "Site 1 has 18 neighbors (>13). Using coordination number instead of geometry.", ] # if on_error is "ignore", no error should be raised and no warning should be issued with warnings.catch_warnings(record=True) as warnings_record: - lse = self.chem_env_on_error.get_light_structure_environment(on_error="ignore") + lse = self.chem_env_on_error.get_light_structure_environment(on_error="ignore") assert lse.coordination_environments[0][0]["ce_symbol"] == "18" assert warnings_record == [] From 3ba2d2daa8148c6b91865a54234d48b7759a44f2 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:16:24 +0000 Subject: [PATCH 170/180] uncomment test --- tests/io/lobster/test_lobsterenv.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index cc769b3b24..b31560c467 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -325,15 +325,15 @@ def setup_method(self): adapt_extremum_to_add_cond=True, ) # test LSE on_error - # self.chem_env_on_error = LobsterNeighbors( - # are_coops=False, - # filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-1018096.gz", - # structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-1018096.gz"), - # filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-1018096.gz", - # additional_condition=0, - # adapt_extremum_to_add_cond=True, - # valences_from_charges=True, - # ) + self.chem_env_on_error = LobsterNeighbors( + are_coops=False, + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-1018096.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-1018096.gz"), + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-1018096.gz", + additional_condition=0, + adapt_extremum_to_add_cond=True, + valences_from_charges=True, + ) def test_init_new(self): # additional condition 0 From b53e58d1367a88b7cedcbfeb92e67f9bf1773a94 Mon Sep 17 00:00:00 2001 From: Aakash Ashok Naik <91958822+naik-aakash@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:19:41 +0000 Subject: [PATCH 171/180] uncomment test --- tests/io/lobster/test_lobsterenv.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py index b31560c467..b238b20606 100644 --- a/tests/io/lobster/test_lobsterenv.py +++ b/tests/io/lobster/test_lobsterenv.py @@ -326,13 +326,13 @@ def setup_method(self): ) # test LSE on_error self.chem_env_on_error = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-1018096.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-1018096.gz"), - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-1018096.gz", - additional_condition=0, - adapt_extremum_to_add_cond=True, - valences_from_charges=True, + are_coops=False, + filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-1018096.gz", + structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-1018096.gz"), + filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-1018096.gz", + additional_condition=0, + adapt_extremum_to_add_cond=True, + valences_from_charges=True, ) def test_init_new(self): From ce78a3b79cb4beb03274e863fee818bfce63cd2c Mon Sep 17 00:00:00 2001 From: Tom Demeyere <115232841+tomdemeyere@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:43:20 +0100 Subject: [PATCH 172/180] refactor(lobster): Rewrite LOBSTER parsers with memory-efficient streaming architecture (#4592) * moving to the future * simpler serialization, spin keys taken care of * final commit? * no `Self` for python 3.10 * changes allowing safe jsanazite * for now * final? * pre-commit auto-fixes * LOBSTER_OBJECTS constant * pre-commit auto-fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Shyue Ping Ong --- future/__init__.py | 102 ++ future/constants.py | 26 + future/core.py | 449 +++++ future/inputs.py | 959 +++++++++++ future/lobster_basis/BASIS_PBE_54_max.yaml | 189 ++ future/lobster_basis/BASIS_PBE_54_min.yaml | 189 ++ .../lobster_basis/BASIS_PBE_54_standard.yaml | 189 ++ future/lobsterenv.py | 1523 +++++++++++++++++ future/outputs/__init__.py | 80 + future/outputs/bands.py | 500 ++++++ future/outputs/coxxcar.py | 380 ++++ future/outputs/doscar.py | 216 +++ future/outputs/icoxxlist.py | 428 +++++ future/outputs/lobsterout.py | 439 +++++ future/outputs/misc.py | 670 ++++++++ future/outputs/populations.py | 157 ++ future/types.py | 96 ++ future/utils.py | 124 ++ future/versioning.py | 25 + 19 files changed, 6741 insertions(+) create mode 100644 future/__init__.py create mode 100644 future/constants.py create mode 100644 future/core.py create mode 100644 future/inputs.py create mode 100644 future/lobster_basis/BASIS_PBE_54_max.yaml create mode 100644 future/lobster_basis/BASIS_PBE_54_min.yaml create mode 100644 future/lobster_basis/BASIS_PBE_54_standard.yaml create mode 100644 future/lobsterenv.py create mode 100644 future/outputs/__init__.py create mode 100644 future/outputs/bands.py create mode 100644 future/outputs/coxxcar.py create mode 100644 future/outputs/doscar.py create mode 100644 future/outputs/icoxxlist.py create mode 100644 future/outputs/lobsterout.py create mode 100644 future/outputs/misc.py create mode 100644 future/outputs/populations.py create mode 100644 future/types.py create mode 100644 future/utils.py create mode 100644 future/versioning.py diff --git a/future/__init__.py b/future/__init__.py new file mode 100644 index 0000000000..557324027a --- /dev/null +++ b/future/__init__.py @@ -0,0 +1,102 @@ +"""Modules for input and output to and from LOBSTER. + +This package provides classes and utilities for reading and writing LOBSTER input and output files. +""" + +from __future__ import annotations + +from pymatgen.io.lobster.future.inputs import LobsterIn +from pymatgen.io.lobster.future.outputs import ( + BWDF, + CHARGE, + CHARGE_LCFO, + COBICAR, + COBICAR_LCFO, + COHPCAR, + COHPCAR_LCFO, + COOPCAR, + DOSCAR, + DOSCAR_LCFO, + GROSSPOP, + GROSSPOP_LCFO, + ICOBILIST, + ICOBILIST_LCFO, + ICOHPLIST, + ICOHPLIST_LCFO, + ICOOPLIST, + ICOXXLIST, + POLARIZATION, + BandOverlaps, + Fatband, + Fatbands, + LobsterMatrices, + LobsterOut, + MadelungEnergies, + NcICOBILIST, + SitePotentials, + Wavefunction, +) + +__all__ = [ + "BWDF", + "CHARGE", + "CHARGE_LCFO", + "COBICAR", + "COBICAR_LCFO", + "COHPCAR", + "COHPCAR_LCFO", + "COOPCAR", + "DOSCAR", + "DOSCAR_LCFO", + "GROSSPOP", + "GROSSPOP_LCFO", + "ICOBILIST", + "ICOBILIST_LCFO", + "ICOHPLIST", + "ICOHPLIST_LCFO", + "ICOOPLIST", + "ICOXXLIST", + "POLARIZATION", + "BandOverlaps", + "Fatband", + "Fatbands", + "LobsterIn", + "LobsterMatrices", + "LobsterOut", + "MadelungEnergies", + "NcICOBILIST", + "SitePotentials", + "Wavefunction", +] + +LOBSTER_OBJECTS = { + "BWDF": BWDF, + "CHARGE": CHARGE, + "CHARGE_LCFO": CHARGE_LCFO, + "COBICAR": COBICAR, + "COBICAR_LCFO": COBICAR_LCFO, + "COHPCAR": COHPCAR, + "COHPCAR_LCFO": COHPCAR_LCFO, + "COOPCAR": COOPCAR, + "DOSCAR": DOSCAR, + "DOSCAR_LCFO": DOSCAR_LCFO, + "GROSSPOP": GROSSPOP, + "GROSSPOP_LCFO": GROSSPOP_LCFO, + "ICOBILIST": ICOBILIST, + "ICOBILIST_LCFO": ICOBILIST_LCFO, + "ICOHPLIST": ICOHPLIST, + "ICOHPLIST_LCFO": ICOHPLIST_LCFO, + "ICOOPLIST": ICOOPLIST, + "ICOXXLIST": ICOXXLIST, + "POLARIZATION": POLARIZATION, + "BandOverlaps": BandOverlaps, + "Fatband": Fatband, + "Fatbands": Fatbands, + "LobsterIn": LobsterIn, + "LobsterMatrices": LobsterMatrices, + "LobsterOut": LobsterOut, + "MadelungEnergies": MadelungEnergies, + "NcICOBILIST": NcICOBILIST, + "SitePotentials": SitePotentials, + "Wavefunction": Wavefunction, +} diff --git a/future/constants.py b/future/constants.py new file mode 100644 index 0000000000..7e90caf302 --- /dev/null +++ b/future/constants.py @@ -0,0 +1,26 @@ +"""Constants for LOBSTER input and output parsing.""" + +from __future__ import annotations + +"""The version of LOBSTER that this code is compatible with. To be updated when necessary.""" +LOBSTER_VERSION: str = "5.1.1" + +"""A tuple of possible orbitals that LOBSTER can handle and their string representations in LOBSTER output files.""" +LOBSTER_ORBITALS: tuple[str, ...] = ( + "s", + "p_x", + "p_y", + "p_z", + "d_xy", + "d_xz", + "d_yz", + "d_z^2", + "d_x^2-y^2", + "f_xyz", + "f_xz^2", + "f_yz^2", + "f_z^3", + "f_x(x^2-3y^2)", + "f_y(3x^2-y^2)", + "f_z(x^2-y^2)", +) diff --git a/future/core.py b/future/core.py new file mode 100644 index 0000000000..6d7c727d5c --- /dev/null +++ b/future/core.py @@ -0,0 +1,449 @@ +from __future__ import annotations + +from abc import abstractmethod +from collections import Counter +from functools import cached_property +from pathlib import Path +from typing import TYPE_CHECKING, Any, ClassVar, Self, cast + +import numpy as np +from monty.io import zopen +from monty.json import MontyDecoder, MSONable + +from pymatgen.io.lobster.future.constants import LOBSTER_VERSION +from pymatgen.io.lobster.future.utils import convert_spin_keys, restore_spin_keys + +if TYPE_CHECKING: + from collections.abc import Callable, Generator + + from numpy.typing import NDArray + + from pymatgen.io.lobster.future.types import LobsterInteraction, LobsterInteractionData, Spin + from pymatgen.util.typing import PathLike + + +class LobsterFile(MSONable): + """ + Representation of a LOBSTER file. + + This class provides a framework for parsing and processing LOBSTER output files. + It supports version-specific processing through a registry of version processors. + + Attributes: + filename (PathLike): Name or path of the file. Defaults to the class default. + lobster_version (str): Version string parsed from the file or the default LOBSTER_VERSION. + version_processors (ClassVar[dict[tuple[str, str | None], Callable]]): Registry of version processors. + spins (list[Spin] | None): List of Spin objects if the file contains spin-polarized data, else None. + """ + + version_processors: ClassVar[dict[tuple[str, str | None], Callable]] + + spins: list[Spin] | None = None + + def __init__( + self, + filename: PathLike | None = None, + process_immediately: bool = True, + lobster_version: str | None = None, + ) -> None: + """ + Initialize a LobsterFile instance. + + Args: + filename (PathLike | None): Path to the file. If None, uses the default filename. + process_immediately (bool): Whether to process the file immediately upon initialization. Defaults to True. + lobster_version (str | None): LOBSTER version string to use for parsing. If None, attempts to detect + from file or falls back to default. + """ + self.filename = Path(filename or self.get_default_filename()).expanduser().resolve() + self.lobster_version = lobster_version or self.get_file_version() or LOBSTER_VERSION + + if process_immediately: + self.process() + + def __init_subclass__(cls, **kwargs: Any) -> None: + """ + Automatically registers version processors for subclasses. + This method scans the subclass for methods decorated with @version_processor + and adds them to the version_processors registry. + """ + super().__init_subclass__(**kwargs) + + version_processors = {} + for base in cls.__bases__: + if hasattr(base, "version_processors"): + version_processors.update(base.version_processors) + + for value in cls.__dict__.values(): + if hasattr(value, "version_info"): + version_processors[value.version_info] = value + + cls.version_processors = version_processors + + def process(self) -> None: + """ + Processes the file using the appropriate version processor. + + Selects the best matching version processor from the `version_processors` registry and invokes it. + + Raises: + ValueError: If no processor matches the file version. + RuntimeError: If the selected processor raises an exception. + """ + eligible_methods = [] + + if not self.filename.exists(): + raise FileNotFoundError(f"The file {self.filename} does not exist.") + + for ( + minimum_version, + maximum_version, + ), processor in self.version_processors.items(): + if LobsterFile.check_version(self.lobster_version, minimum_version, maximum_version): + eligible_methods.append((minimum_version, processor)) + + if not eligible_methods: + raise ValueError(f"No processor found for version {self.lobster_version}") + + best_method = max(eligible_methods, key=lambda x: x[0])[-1] + + try: + best_method(self) + except Exception as e: + processor_name = getattr(best_method, "__name__", str(best_method)) + + raise RuntimeError( + f"Error occurred during file processing with {processor_name} (version {self.lobster_version}): {e}" + ) from e + + @staticmethod + def check_version(actual: str, minimum: str, maximum: str | None) -> bool: + """ + Checks whether a version string falls within a min/max inclusive range. + + Args: + actual (str): Version string to check (e.g., "1.2.3"). + minimum (str): Minimum acceptable version string (exclusive). + maximum (str | None): Maximum acceptable version string (exclusive) or None for no upper bound. + + Returns: + bool: True if `actual` is > `minimum` and < `maximum` (if provided), otherwise False. + """ + actual_parts = tuple(map(int, actual.split("."))) + minimum_parts = tuple(map(int, minimum.split("."))) + + if actual_parts < minimum_parts: + return False + + if maximum is not None: + maximum_parts = tuple(map(int, maximum.split("."))) + + if actual_parts > maximum_parts: + return False + + return True + + def get_file_version(self) -> str | None: + """ + Retrieves the file version. Override in subclasses to extract version from file content if possible. + + Returns: + str | None: Version string (e.g., "1.2.3") if found, else None. + """ + + @classmethod + @abstractmethod + def get_default_filename(cls) -> str: + """ + Returns the default filename for this LobsterFile subclass. + + Returns: + str: The default filename. + """ + ... + + @cached_property + def lines(self) -> list[str]: + """ + Returns all lines from the file as a list of strings. + + Returns: + list[str]: Lines from the file. + + Raises: + ValueError: If the file is empty. + """ + with zopen(self.filename, mode="rt", encoding="utf-8") as file: + lines = file.read().splitlines() + + if len(lines) == 0: + raise ValueError(f"The file {self.filename} is empty.") + + return cast("list[str]", lines) + + def iterate_lines(self) -> Generator[str]: + """ + Iterates over lines in the file, yielding each line as a string. + + Yields: + str: Each line in the file, stripped of whitespace. + """ + with zopen(self.filename, mode="rt", encoding="utf-8") as file: + for line in file: + yield cast("str", line.strip()) + + def as_dict(self) -> dict[str, Any]: + """ + Serializes the LobsterFile object to a dictionary. + Spin keys in dictionaries are converted to strings for JSON compatibility. + + Returns: + dict[str, Any]: Dictionary with keys "@module", "@class", "@version", and all attributes of the object. + """ + dictionary = { + "@module": self.__class__.__module__, + "@class": self.__class__.__name__, + "@version": None, + } + + for k, v in vars(self).items(): + dictionary[k] = convert_spin_keys(v) + + return dictionary + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> Self: + """ + Deserializes a LobsterFile object from a dictionary. Spin keys in dictionaries are restored from strings. + + Args: + d (dict[str, Any]): Dictionary produced by as_dict or similar serialization. + + Returns: + LobsterFile: Instance of LobsterFile with attributes populated from the dictionary. + """ + instance = cls.__new__(cls) + + decoded_dictionary = { + k: restore_spin_keys(MontyDecoder().process_decoded(v)) for k, v in d.items() if not k.startswith("@") + } + + for k, v in decoded_dictionary.items(): + setattr(instance, k, v) + + instance.filename = Path(instance.filename) + + return instance + + @property + def has_spin(self) -> bool: + """ + Indicates whether the file could contain spin-polarized data. + + Returns: + bool: True if this file type supports spin, False otherwise. + """ + return self.spins is not None + + @property + def is_spin_polarized(self) -> bool: + """ + Indicates whether the file contains spin-polarized data. + + Returns: + bool: True if multiple spins are present, False otherwise. + """ + return self.has_spin and len(self.spins) > 1 + + +class LobsterInteractionsHolder(LobsterFile): + """ + Container for LOBSTER interaction data. This class holds interaction metadata. It provides methods for filtering and + retrieving interaction data based on various criteria. + + Attributes: + interactions (list[LobsterInteractionData]): List of interaction metadata dicts. + """ + + interactions: list[LobsterInteractionData] + data: NDArray[np.float64] + + def get_interaction_indices_by_properties( + self, + indices: list[int] | None = None, + centers: list[str] | None = None, + cells: list[list[int]] | None = None, + orbitals: list[str] | None = None, + length: tuple[float, float] | None = None, + ) -> list[int]: + """ + Returns indices of interactions that match provided property filters. + + Args: + indices (list[int] | None): Optional sequence of interaction indices to match. + centers (list[str] | None): Optional sequence of center name substrings; interaction must contain each + center substring the required number of times. + cells (list[list[int]] | None): Optional sequence of cell vectors to match against interaction cells. + orbitals (list[str] | None): Optional sequence of orbital name substrings; interaction must contain each + orbital substring the required number of times. + length (tuple[float, float] | None): Optional (min, max) tuple to filter interactions by length inclusive. + + Returns: + list[int]: Sorted list of interaction indices that match all supplied filters. If no filters are supplied, + returns an empty list. + """ + matching_sets = [] + + if indices is not None: + matching_sets.append({i for i, bond in enumerate(self.interactions) if bond["index"] in indices}) + + if centers is not None: + center_counts = Counter(centers) + matching_centers = set() + + for i, bond in enumerate(self.interactions): + bond_centers = bond.get("centers", []) + + if all( + sum(center_suffix in b for b in bond_centers) >= required_count + for center_suffix, required_count in center_counts.items() + ): + matching_centers.add(i) + + matching_sets.append(matching_centers) + + if cells is not None: + matching_sets.append( + { + i + for i, bond in enumerate(self.interactions) + if bond.get("cells") + and all(np.all(np.equal(bond.get("cells"), cell), axis=1).any() for cell in cells if cell) + } + ) + + if orbitals is not None: + if not orbitals: + matching_sets.append( + {i for i, bond in enumerate(self.interactions) if all(o is None for o in bond.get("orbitals", []))} + ) + else: + orbital_counts = Counter(orbitals) + matching_orbitals = set() + + for i, bond in enumerate(self.interactions): + bond_orbitals = bond.get("orbitals", []) + + if all( + sum(orbital_suffix in b for b in bond_orbitals if b) >= required_count + for orbital_suffix, required_count in orbital_counts.items() + ): + matching_orbitals.add(i) + + matching_sets.append(matching_orbitals) + + if length is not None: + matching_sets.append( + { + i + for i, bond in enumerate(self.interactions) + if (this_length := bond.get("length")) is not None and length[0] <= this_length <= length[1] + } + ) + + return sorted(set.intersection(*matching_sets)) if matching_sets else [] + + def get_interactions_by_properties( + self: LobsterInteractionsHolder, + indices: list[int] | None = None, + centers: list[str] | None = None, + cells: list[list[int]] | None = None, + orbitals: list[str] | None = None, + length: tuple[float, float] | None = None, + ) -> list[LobsterInteractionData]: + """Return interaction metadata dicts that match the provided filters. + + Args: + indices (list[int] | None): Interaction indices to filter. + centers (list[str] | None): Atom centers to filter. + cells (list[list[int]] | None): Unit cell indices to filter. + orbitals (list[str] | None): Orbitals to filter. + length (tuple[float, float] | None): Length range to filter. + + Returns: + list[dict]: List of interaction dictionaries matching the filters. + """ + interaction_indices = self.get_interaction_indices_by_properties(indices, centers, cells, orbitals, length) + + return [bond for i, bond in enumerate(self.interactions) if i in interaction_indices] + + @staticmethod + def get_label_from_interaction( + interaction: LobsterInteraction, + include_centers: bool = True, + include_orbitals: bool = True, + include_cells: bool = False, + include_length: bool = False, + ) -> str: + """ + Generates a label string for a given interaction. + + Args: + interaction (LobsterInteraction): Interaction metadata dictionary. + Returns: + str: Formatted label string representing the interaction. + """ + parts = [] + + for center, orbital, cell in zip( + interaction["centers"], + interaction["orbitals"], + interaction["cells"], + strict=True, + ): + tmp = "" + if include_centers: + tmp += center + + if include_cells and cell is not None: + tmp += f"[{' '.join(map(str, cell))}]" + + if include_orbitals and orbital: + tmp += f"[{orbital}]" + + parts.append(tmp) + + if not parts: + raise ValueError(f"Could not generate label from interaction {interaction}") + + if include_length and interaction["length"] is not None: + parts[-1] += f"({interaction['length']:.3f})" + + return "->".join(parts) + + @abstractmethod + def process_data_into_interactions(self) -> None: + """ + Abstract method to process raw data into structured interaction metadata. Must be implemented by subclasses. + This method should populate the `interactions` attribute based on the raw `data` and any other relevant + attributes. The exact processing logic will depend on the specific format and content of the data in the + subclass. + """ + ... + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> Self: + """Deserialize object from dictionary produced by `as_dict`. + + Args: + d (dict): Dictionary produced by `as_dict`. + + Returns: + COXXCAR: Reconstructed instance. + """ + instance = super().from_dict(d) + instance.data = np.asarray(instance.data, dtype=np.float64) + + instance.process_data_into_interactions() + + return instance diff --git a/future/inputs.py b/future/inputs.py new file mode 100644 index 0000000000..fb583a5a98 --- /dev/null +++ b/future/inputs.py @@ -0,0 +1,959 @@ +"""Module for reading LOBSTER input files. +For more information on LOBSTER see www.cohp.de. + +If you use this module, please cite: +J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, +"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +ChemPlusChem 2022, e202200123, +DOI: 10.1002/cplu.202200123. +""" + +from __future__ import annotations + +import itertools +import os +import re +import warnings +from collections import UserDict +from typing import TYPE_CHECKING + +import numpy as np +import spglib +from monty.io import zopen +from monty.json import MSONable +from monty.serialization import loadfn + +from pymatgen.core.structure import Structure +from pymatgen.io.vasp import Vasprun +from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar +from pymatgen.symmetry.bandstructure import HighSymmKpath +from pymatgen.util.due import Doi, due + +if TYPE_CHECKING: + from typing import Any, ClassVar, Literal, Self + + from pymatgen.core.composition import Composition + from pymatgen.core.structure import IStructure + from pymatgen.util.typing import PathLike + +MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) + +__author__ = "Janine George, Marco Esters" +__copyright__ = "Copyright 2017, The Materials Project" +__version__ = "0.2" +__maintainer__ = "Janine George" +__email__ = "janinegeorge.ulfen@gmail.com" +__date__ = "Dec 13, 2017" + + +due.cite( + Doi("10.1002/cplu.202200123"), + description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +) + + +class LobsterIn(UserDict, MSONable): + """Handles and generates lobsterin files for LOBSTER calculations. + + This class provides methods to create, modify, and write `lobsterin` files, + which are input files for the LOBSTER program. It also includes utilities + for generating related files such as INCAR, KPOINTS, and POSCAR, and for + comparing different `lobsterin` configurations. + + Reminder: LOBSTER input keywords are not case-sensitive. + + Attributes: + FLOAT_KEYWORDS (ClassVar[dict[str, str]]): Keywords requiring float values. + STRING_KEYWORDS (ClassVar[dict[str, str]]): Keywords requiring string values. + BOOLEAN_KEYWORDS (ClassVar[dict[str, str]]): Keywords that are boolean flags. + LIST_KEYWORDS (ClassVar[dict[str, str]]): Keywords requiring lists of strings. + AVAILABLE_KEYWORDS (ClassVar[dict[str, str]]): All known keywords mapped to their canonical forms. + """ + + # These keywords need an additional float suffix + _FLOAT_KEYWORDS: tuple[str, ...] = ( + "COHPstartEnergy", + "COHPendEnergy", + "gaussianSmearingWidth", + "useDecimalPlaces", + "COHPSteps", + "basisRotation", + "gridDensityForPrinting", + "gridBufferForPrinting", + ) + + # These keywords need an additional string suffix + _STRING_KEYWORDS: tuple[str, ...] = ( + "basisSet", + "cohpGenerator", + "realspaceHamiltonian", + "realspaceOverlap", + "printPAWRealSpaceWavefunction", + "printLCAORealSpaceWavefunction", + "kSpaceCOHP", + "EwaldSum", + ) + + # The keywords themselves (without suffix) can trigger additional functionalities + _BOOLEAN_KEYWORDS: tuple[str, ...] = ( + "saveProjectionToFile", + "skipCar", + "skipdos", + "skipcohp", + "skipcoop", + "skipcobi", + "skipMOFE", + "skipMolecularOrbitals", + "skipMadelungEnergy", + "loadProjectionFromFile", + "printTotalSpilling", + "forceEnergyRange", + "DensityOfEnergy", + "BWDF", + "BWDFCOHP", + "skipPopulationAnalysis", + "skipGrossPopulation", + "userecommendedbasisfunctions", + "skipProjection", + "printLmosOnAtoms", + "printMofeAtomWise", + "printMofeMoleculeWise", + "writeAtomicOrbitals", + "writeBasisFunctions", + "writeMatricesToFile", + "noFFTforVisualization", + "RMSp", + "onlyReadVasprun.xml", + "noMemoryMappedFiles", + "skipPAWOrthonormalityTest", + "doNotIgnoreExcessiveBands", + "doNotUseAbsoluteSpilling", + "skipReOrthonormalization", + "forceV1HMatrix", + "useOriginalTetrahedronMethod", + "forceEnergyRange", + "bandwiseSpilling", + "kpointwiseSpilling", + "LSODOS", + "autoRotate", + "doNotOrthogonalizeBasis", + ) + + # These keywords need additional string suffixes. + # They could be used multiple times within one lobsterin. + _LIST_KEYWORDS: tuple[str, ...] = ( + "basisfunctions", + "cohpbetween", + "createFatband", + "customSTOforAtom", + "cobiBetween", + "printLmosOnAtomswriteAtomicDensities", + ) + + # Generate {lowered: original} mappings + FLOAT_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _FLOAT_KEYWORDS} + STRING_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _STRING_KEYWORDS} + BOOLEAN_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _BOOLEAN_KEYWORDS} + LIST_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _LIST_KEYWORDS} + + # All known keywords + AVAILABLE_KEYWORDS: ClassVar[dict[str, str]] = { + **FLOAT_KEYWORDS, + **STRING_KEYWORDS, + **BOOLEAN_KEYWORDS, + **LIST_KEYWORDS, + } + + def __init__(self, settingsdict: dict) -> None: + """Initializes a LobsterIn object with the given settings. + + Args: + settingsdict (dict): Dictionary of settings to initialize the LobsterIn object. + + Raises: + KeyError: If there are duplicate keywords (case-insensitive). + """ + super().__init__() + + # Check for duplicates from case sensitivity + keys = tuple(map(str.lower, settingsdict.keys())) + if len(keys) != len(set(keys)): + raise KeyError("There are duplicates for the keywords!") + + self.update(settingsdict) + + def __setitem__(self, key: str, val: Any) -> None: + """Sets a keyword-value pair in the LobsterIn object. + + Args: + key (str): The keyword to set. + val (Any): The value to associate with the keyword. + + Raises: + KeyError: If the keyword is not recognized. + """ + key = key.strip().lower() + + if key not in type(self).AVAILABLE_KEYWORDS: + raise KeyError(f"Key {key} is currently not available") + + super().__setitem__(key, val.strip() if isinstance(val, str) else val) + + def __getitem__(self, key: str) -> Any: + """Gets the value associated with a keyword. + + Args: + key (str): The keyword to retrieve. + + Returns: + Any: The value associated with the keyword. + + Raises: + KeyError: If the keyword is not found. + """ + try: + return super().__getitem__(key.strip().lower()) + + except KeyError as exc: + raise KeyError(f"{key=} is not available") from exc + + def __contains__(self, key: str) -> bool: + """Checks if a keyword exists in the LobsterIn object. + + Args: + key (str): The keyword to check. + + Returns: + bool: True if the keyword exists, False otherwise. + """ + return super().__contains__(key.lower().strip()) + + def __delitem__(self, key: str) -> None: + """Deletes a keyword from the LobsterIn object. + + Args: + key (str): The keyword to delete. + """ + super().__delitem__(key.lower().strip()) + + def diff(self, other: Self) -> dict[str, dict[str, Any]]: + """Compares two LobsterIn objects and identifies differences. + + Args: + other (LobsterIn): The other LobsterIn object to compare. + + Returns: + dict[str, dict[str, Any]]: A dictionary with keys "Same" and "Different", + containing the same and differing parameters, respectively. + """ + same_params = {} + diff_params = {} + + # Check self + for k1, v1 in self.items(): + if k1 not in other: + diff_params[k1] = {"lobsterin1": v1, "lobsterin2": None} + + # String keywords + elif isinstance(v1, str): + if v1 != other[k1]: + diff_params[k1] = {"lobsterin1": v1, "lobsterin2": other[k1]} + else: + same_params[k1] = v1 + + # List keywords + elif isinstance(v1, list): + new_set1 = {value.strip().lower() for value in v1} + new_set2 = {value.strip().lower() for value in other[k1]} + if new_set1 != new_set2: + diff_params[k1] = {"lobsterin1": v1, "lobsterin2": other[k1]} + + # Float/boolean keywords + elif v1 != other[k1]: + diff_params[k1] = {"lobsterin1": v1, "lobsterin2": other[k1]} + else: + same_params[k1] = v1 + + # Check other + for k2, v2 in other.items(): + if k2 not in self and k2 not in diff_params: + diff_params[k2] = {"lobsterin1": None, "lobsterin2": v2} + + return {"Same": same_params, "Different": diff_params} + + def write_lobsterin( + self, + path: PathLike = "lobsterin", + overwritedict: dict | None = None, + ) -> None: + """Writes the LobsterIn object to a `lobsterin` file. + + Args: + path (PathLike): The output file path. Defaults to "lobsterin". + overwritedict (dict | None): A dictionary of settings to overwrite before writing. + """ + # Update previous entries + if overwritedict is not None: + self.update(overwritedict) + + with open(path, mode="w", encoding="utf-8") as file: + for key in self: + if key in type(self).FLOAT_KEYWORDS or key in type(self).STRING_KEYWORDS: + file.write(f"{type(self).AVAILABLE_KEYWORDS[key]} {self.get(key)}\n") + + elif key in type(self).BOOLEAN_KEYWORDS: + file.write(f"{type(self).BOOLEAN_KEYWORDS[key]}\n") + + elif key in type(self).LIST_KEYWORDS: + file.writelines(f"{type(self).LIST_KEYWORDS[key]} {value}\n" for value in self.get(key)) + + def as_dict(self) -> dict: + """Converts the LobsterIn object to a dictionary. + + Returns: + dict: A dictionary representation of the LobsterIn object. + """ + dct = dict(self) + dct["@module"] = type(self).__module__ + dct["@class"] = type(self).__name__ + return dct + + @classmethod + def from_dict(cls, dct: dict) -> Self: + """Creates a LobsterIn object from a dictionary. + + Args: + dct (dict): A dictionary representation of a LobsterIn object. + + Returns: + LobsterIn: The created LobsterIn object. + """ + return cls({key: val for key, val in dct.items() if key not in {"@module", "@class"}}) + + def _get_nbands(self, structure: Structure) -> int: + """Calculates the number of bands based on the structure and basis functions. + + Args: + structure (Structure): The structure object. + + Returns: + int: The number of bands. + + Raises: + ValueError: If no basis functions are provided. + """ + if self.get("basisfunctions") is None: + raise ValueError("No basis functions are provided. The program cannot calculate nbands.") + + basis_functions: list[str] = [] + for string_basis in self["basisfunctions"]: + string_basis_raw = string_basis.strip().split(" ") + while "" in string_basis_raw: + string_basis_raw.remove("") + for _idx in range(int(structure.composition.element_composition[string_basis_raw[0]])): + basis_functions.extend(string_basis_raw[1:]) + + num_basis_functions = 0 + for basis in basis_functions: + if "s" in basis: + num_basis_functions += 1 + elif "p" in basis: + num_basis_functions += 3 + elif "d" in basis: + num_basis_functions += 5 + elif "f" in basis: + num_basis_functions += 7 + + return int(num_basis_functions) + + def write_INCAR( + self, + incar_input: PathLike = "INCAR", + incar_output: PathLike = "INCAR.lobster", + poscar_input: PathLike = "POSCAR", + isym: Literal[-1, 0] = 0, + further_settings: dict | None = None, + ) -> None: + """Writes a modified INCAR file for LOBSTER calculations. + + Args: + incar_input (PathLike): Path to the input INCAR file. + incar_output (PathLike): Path to the output INCAR file. + poscar_input (PathLike): Path to the input POSCAR file. + isym (Literal[-1, 0]): ISYM value to set. Defaults to 0. + further_settings (dict | None): Additional settings to include in the INCAR file. + + Raises: + ValueError: If `isym` is not -1 or 0. + """ + # Read INCAR from file, which will be modified + incar = Incar.from_file(incar_input) + warnings.warn( + "Please check your incar_input before using it. This method only changes three settings!", + stacklevel=2, + ) + if isym in {-1, 0}: + incar["ISYM"] = isym + else: + raise ValueError(f"Got {isym=}, must be -1 or 0") + + incar["NSW"] = 0 + incar["LWAVE"] = True + + # Get NBANDS from _get_nbands (use basis set that is inserted) + incar["NBANDS"] = self._get_nbands(Structure.from_file(poscar_input)) + if further_settings is not None: + for key, item in further_settings.items(): + incar[key] = item + incar.write_file(incar_output) + + @staticmethod + def get_basis( + structure: Structure | IStructure, + potcar_symbols: list[str], + address_basis_file: PathLike | None = None, + ) -> list[str]: + """Retrieves basis functions for the given structure and POTCAR symbols. + + Args: + structure (Structure | IStructure): The structure object. + potcar_symbols (list[str]): List of POTCAR symbols. + address_basis_file (PathLike | None): Path to the basis file. Defaults to None. + + Returns: + list[str]: List of basis functions. + + Raises: + ValueError: If the POSCAR does not match the POTCAR or if basis information is missing. + """ + if address_basis_file is None: + address_basis_file = f"{MODULE_DIR}/lobster_basis/BASIS_PBE_54_standard.yaml" + + atom_types_potcar = [name.split("_")[0] for name in potcar_symbols] + + if set(structure.symbol_set) != set(atom_types_potcar): + raise ValueError("Your POSCAR does not correspond to your POTCAR!") + + basis = loadfn(address_basis_file)["BASIS"] + + basis_functions = [] + list_forin = [] + for idx, name in enumerate(potcar_symbols): + if name not in basis: + raise ValueError( + f"Missing basis information for POTCAR symbol: {name}. Please provide the basis manually." + ) + basis_functions.append(basis[name].split()) + list_forin.append(f"{atom_types_potcar[idx]} {basis[name]}") + return list_forin + + @staticmethod + def get_all_possible_basis_functions( + structure: Structure | IStructure, + potcar_symbols: list[str], + address_basis_file_min: PathLike | None = None, + address_basis_file_max: PathLike | None = None, + ) -> list[dict]: + """ + Args: + structure: Structure object + potcar_symbols: list of the potcar symbols + address_basis_file_min: path to file with the minimum required basis by the POTCAR + address_basis_file_max: path to file with the largest possible basis of the POTCAR. + + Returns: + list[dict]: Can be used to create new Lobsterin objects in + standard_calculations_from_vasp_files as dict_for_basis + """ + max_basis = LobsterIn.get_basis( + structure=structure, + potcar_symbols=potcar_symbols, + address_basis_file=address_basis_file_max or f"{MODULE_DIR}/lobster_basis/BASIS_PBE_54_max.yaml", + ) + min_basis = LobsterIn.get_basis( + structure=structure, + potcar_symbols=potcar_symbols, + address_basis_file=address_basis_file_min or f"{MODULE_DIR}/lobster_basis/BASIS_PBE_54_min.yaml", + ) + all_basis = get_all_possible_basis_combinations(min_basis=min_basis, max_basis=max_basis) + list_basis_dict = [] + for basis in all_basis: + basis_dict = {} + + for elba in basis: + basplit = elba.split() + basis_dict[basplit[0]] = " ".join(basplit[1:]) + list_basis_dict.append(basis_dict) + return list_basis_dict + + @staticmethod + def write_POSCAR_with_standard_primitive( + POSCAR_input: PathLike = "POSCAR", + POSCAR_output: PathLike = "POSCAR.lobster", + symprec: float = 0.01, + ) -> None: + """Writes a POSCAR file with the standard primitive cell. + + Args: + POSCAR_input (PathLike): Path to the input POSCAR file. + POSCAR_output (PathLike): Path to the output POSCAR file. + symprec (float): Precision for symmetry determination. Defaults to 0.01. + """ + structure = Structure.from_file(POSCAR_input) + kpath = HighSymmKpath(structure, symprec=symprec) + new_structure = kpath.prim + new_structure.to(fmt="POSCAR", filename=POSCAR_output) + + @staticmethod + def write_KPOINTS( + POSCAR_input: PathLike = "POSCAR", + KPOINTS_output: PathLike = "KPOINTS.lobster", + reciprocal_density: int = 100, + isym: Literal[-1, 0] = 0, + from_grid: bool = False, + input_grid: tuple[int, int, int] = (5, 5, 5), + line_mode: bool = True, + kpoints_line_density: int = 20, + symprec: float = 0.01, + ) -> None: + """Write a gamma-centered KPOINTS file for LOBSTER. + + Args: + POSCAR_input (PathLike): path to POSCAR + KPOINTS_output (PathLike): path to output KPOINTS + reciprocal_density (int): Grid density + isym (-1 | 0): ISYM value. + from_grid (bool): If True KPOINTS will be generated with the help of a grid given in input_grid. + Otherwise, they will be generated from the reciprocal_density + input_grid (tuple): grid to generate the KPOINTS file + line_mode (bool): If True, band structure will be generated + kpoints_line_density (int): density of the lines in the band structure + symprec (float): precision to determine symmetry + """ + structure = Structure.from_file(POSCAR_input) + if not from_grid: + kpoint_grid = Kpoints.automatic_density_by_vol(structure, reciprocal_density).kpts + mesh = kpoint_grid[0] + else: + mesh = input_grid + + # The following code is taken from SpacegroupAnalyzer + # We need to switch off symmetry here + matrix = structure.lattice.matrix + positions = structure.frac_coords + unique_species: list[Composition] = [] + zs = [] + magmoms = [] + + for species, group in itertools.groupby(structure, key=lambda s: s.species): + if species in unique_species: + ind = unique_species.index(species) + zs.extend([ind + 1] * len(tuple(group))) + else: + unique_species.append(species) + zs.extend([len(unique_species)] * len(tuple(group))) + + for site in structure: + if hasattr(site, "magmom"): + magmoms.append(site.magmom) + elif site.is_ordered and hasattr(site.specie, "spin"): + magmoms.append(site.specie.spin) + else: + magmoms.append(0) + + # For now, we are setting MAGMOM to zero. (Taken from INCAR class) + cell = matrix, positions, zs, magmoms + # TODO: what about this shift? + mapping, grid = spglib.get_ir_reciprocal_mesh(mesh, cell, is_shift=[0, 0, 0]) # type:ignore[arg-type] + + # Get the KPOINTS for the grid + if isym == -1: + kpts = [] + weights = [] + all_labels = [] + for gp in grid: + kpts.append(gp.astype(float) / mesh) + weights.append(float(1)) + all_labels.append("") + + # Time reversal symmetry: k and -k are equivalent + elif isym == 0: + kpts = [] + weights = [] + all_labels = [] + newlist = [list(gp) for gp in list(grid)] + mapping = [] # type:ignore[assignment] + for gp in newlist: + minus_gp = [-k for k in gp] + if minus_gp in newlist and minus_gp != [0, 0, 0]: + mapping.append(newlist.index(minus_gp)) + else: + mapping.append(newlist.index(gp)) + + for igp, gp in enumerate(newlist): + if mapping[igp] > igp: + kpts.append(np.array(gp).astype(float) / mesh) + weights.append(float(2)) + all_labels.append("") + elif mapping[igp] == igp: + kpts.append(np.array(gp).astype(float) / mesh) + weights.append(float(1)) + all_labels.append("") + + else: + raise ValueError(f"Got {isym=}, must be -1 or 0") + + # Line mode + if line_mode: + kpath = HighSymmKpath(structure, symprec=symprec) + if not np.allclose(kpath.prim.lattice.matrix, structure.lattice.matrix): + raise ValueError( + "You are not using the standard primitive cell. The k-path is not correct. Please generate a " + "standard primitive cell first." + ) + + frac_k_points, labels = kpath.get_kpoints(line_density=kpoints_line_density, coords_are_cartesian=False) + + for k, f in enumerate(frac_k_points): + kpts.append(f) + weights.append(0.0) + all_labels.append(labels[k]) + comment = f"{isym=}, grid: {mesh} plus kpoint path" if line_mode else f"{isym=}, grid: {mesh}" + + kpoints_instance = Kpoints( + comment=comment, + style=Kpoints.supported_modes.Reciprocal, + num_kpts=len(kpts), + kpts=tuple(kpts), + kpts_weights=weights, + labels=all_labels, + ) + + kpoints_instance.write_file(filename=KPOINTS_output) + + @classmethod + def from_file(cls, lobsterin: PathLike) -> Self: + """Creates a LobsterIn object from a `lobsterin` file. + + Args: + lobsterin (PathLike): Path to the `lobsterin` file. + + Returns: + LobsterIn: The created LobsterIn object. + + Raises: + RuntimeError: If the `lobsterin` file contains no data. + ValueError: If invalid keywords are found or if there are duplicate keywords. + """ + with zopen(lobsterin, mode="rt", encoding="utf-8") as file: + lines: list[str] = file.read().split("\n") # type:ignore[arg-type,assignment] + if not lines: + raise RuntimeError("lobsterin file contains no data.") + + lobsterin_dict: dict[str, Any] = {} + for line in lines: + # Remove comment lines and in-line comments + if line := re.split(r"[!#//]", line)[0].strip(): + # Extract keywords + line_parts = line.replace("\t", " ").strip().split() + if line_parts: + key = line_parts[0].lower() + else: + continue + + # Avoid duplicates for float/string keywords + if (key in cls.FLOAT_KEYWORDS or key in cls.STRING_KEYWORDS) and key in lobsterin_dict: + raise ValueError(f"Same keyword {key} twice!") + + # Parse by keyword type + if key in cls.BOOLEAN_KEYWORDS: + lobsterin_dict[key] = True + + elif key in cls.FLOAT_KEYWORDS: + lobsterin_dict[key] = float(line_parts[1]) + + elif key in cls.STRING_KEYWORDS: + lobsterin_dict[key] = " ".join(line_parts[1:]) + + elif key in cls.LIST_KEYWORDS: + if key in lobsterin_dict: + lobsterin_dict[key].append(" ".join(line_parts[1:])) + else: + lobsterin_dict[key] = [" ".join(line_parts[1:])] + + else: + raise ValueError(f"Invalid {key=}.") + + return cls(lobsterin_dict) + + @staticmethod + def _get_potcar_symbols(POTCAR_input: PathLike) -> list[str]: + """Get the name of the species in the POTCAR. + + Args: + POTCAR_input (PathLike): path to potcar file + + Returns: + list[str]: names of the species + """ + potcar = Potcar.from_file(POTCAR_input) # type:ignore[arg-type] + for pot in potcar: + if pot.potential_type != "PAW": + raise ValueError("Lobster only works with PAW! Use different POTCARs") + + # Warning about a bug in LOBSTER-4.1.0 + with zopen(POTCAR_input, mode="rt", encoding="utf-8") as file: + data = file.read() + + if isinstance(data, bytes): + data = data.decode("utf-8") + + if "SHA256" in data or "COPYR" in data: + warnings.warn( + "These POTCARs are not compatible with " + "Lobster up to version 4.1.0." + "\n The keywords SHA256 and COPYR " + "cannot be handled by Lobster" + " \n and will lead to wrong results.", + stacklevel=2, + ) + + if potcar.functional != "PBE": + raise RuntimeError("We only have BASIS options for PBE so far") + + return [name["symbol"] for name in potcar.spec] + + @classmethod + def standard_calculations_from_vasp_files( + cls, + POSCAR_input: PathLike = "POSCAR", + INCAR_input: PathLike = "INCAR", + POTCAR_input: PathLike | None = None, + Vasprun_output: PathLike = "vasprun.xml", + dict_for_basis: dict | None = None, + option: str = "standard", + ) -> Self: + """Generate lobsterin with standard settings. + + Args: + POSCAR_input (PathLike): path to POSCAR + INCAR_input (PathLike): path to INCAR + POTCAR_input (PathLike): path to POTCAR + Vasprun_output (PathLike): path to vasprun.xml + dict_for_basis (dict): can be provided: it should look the following: + dict_for_basis={"Fe":'3p 3d 4s 4f', "C": '2s 2p'} and will overwrite all settings from POTCAR_input + option (str): 'standard' will start a normal LOBSTER run where COHPs, COOPs, DOS, CHARGE etc. will be + calculated + 'standard_with_energy_range_from_vasprun' will start a normal LOBSTER run for entire energy range + of VASP static run. vasprun.xml file needs to be in current directory. + 'standard_from_projection' will start a normal LOBSTER run from a projection + 'standard_with_fatband' will do a fatband calculation, run over all orbitals + 'onlyprojection' will only do a projection + 'onlydos' will only calculate a projected dos + 'onlycohp' will only calculate cohp + 'onlycoop' will only calculate coop + 'onlycohpcoop' will only calculate cohp and coop + + Returns: + Lobsterin with standard settings + """ + warnings.warn( + "Always check and test the provided basis functions. The spilling of your Lobster calculation might help", + stacklevel=2, + ) + + if option not in { + "standard", + "standard_from_projection", + "standard_with_fatband", + "standard_with_energy_range_from_vasprun", + "onlyprojection", + "onlydos", + "onlycohp", + "onlycoop", + "onlycobi", + "onlycohpcoop", + "onlycohpcoopcobi", + "onlymadelung", + }: + raise ValueError("The option is not valid!") + + lobsterin_dict: dict[str, Any] = { + # This basis set covers elements up to Lr (Z = 103) + "basisSet": "pbeVaspFit2015", + # Energies around e-fermi + "COHPstartEnergy": -35.0, + "COHPendEnergy": 5.0, + } + + if option in { + "standard", + "standard_with_energy_range_from_vasprun", + "onlycohp", + "onlycoop", + "onlycobi", + "onlycohpcoop", + "onlycohpcoopcobi", + "standard_with_fatband", + }: + # Every interaction with a distance of 6.0 Å is checked + lobsterin_dict["cohpGenerator"] = "from 0.1 to 6.0 orbitalwise" + # Save the projection + lobsterin_dict["saveProjectionToFile"] = True + + if option == "standard_from_projection": + lobsterin_dict["cohpGenerator"] = "from 0.1 to 6.0 orbitalwise" + lobsterin_dict["loadProjectionFromFile"] = True + + elif option == "standard_with_energy_range_from_vasprun": + vasp_run = Vasprun(Vasprun_output) + lobsterin_dict["COHPstartEnergy"] = round( + min(vasp_run.complete_dos.energies - vasp_run.complete_dos.efermi), 4 + ) + lobsterin_dict["COHPendEnergy"] = round( + max(vasp_run.complete_dos.energies - vasp_run.complete_dos.efermi), 4 + ) + lobsterin_dict["COHPSteps"] = len(vasp_run.complete_dos.energies) + + # TODO: add COBI here! might be relevant LOBSTER version + elif option == "onlycohp": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipcoop"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlycoop": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipcohp"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlycohpcoop": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlycohpcoopcobi": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlydos": + lobsterin_dict["skipcohp"] = True + lobsterin_dict["skipcoop"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlyprojection": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipcohp"] = True + lobsterin_dict["skipcoop"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + lobsterin_dict["saveProjectionToFile"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlycobi": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipcohp"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + lobsterin_dict["skipMadelungEnergy"] = True + + elif option == "onlymadelung": + lobsterin_dict["skipdos"] = True + lobsterin_dict["skipcohp"] = True + lobsterin_dict["skipcoop"] = True + lobsterin_dict["skipPopulationAnalysis"] = True + lobsterin_dict["skipGrossPopulation"] = True + lobsterin_dict["saveProjectionToFile"] = True + # LOBSTER-4.1.0 + lobsterin_dict["skipcobi"] = True + + incar = Incar.from_file(INCAR_input) + + if incar["ISMEAR"] == 0: + lobsterin_dict["gaussianSmearingWidth"] = incar["SIGMA"] + + if incar["ISMEAR"] != 0 and option == "standard_with_fatband": + raise ValueError("ISMEAR has to be 0 for a fatband calculation with Lobster") + + if dict_for_basis is not None: + # dict_for_basis = {"Fe":"3p 3d 4s 4f", "C": "2s 2p"} + # Will just insert this basis and not check with poscar + basis = [f"{key} {value}" for key, value in dict_for_basis.items()] + elif POTCAR_input is not None: + # Get basis functions from POTCAR + potcar_names = cls._get_potcar_symbols(POTCAR_input=POTCAR_input) + + basis = cls.get_basis(structure=Structure.from_file(POSCAR_input), potcar_symbols=potcar_names) + else: + raise ValueError("basis cannot be generated") + + lobsterin_dict["basisfunctions"] = basis + + if option == "standard_with_fatband": + lobsterin_dict["createFatband"] = basis + + return cls(lobsterin_dict) + + +def get_all_possible_basis_combinations(min_basis: list, max_basis: list) -> list[list[str]]: + """Get all possible basis combinations. + + Args: + min_basis: list of basis entries: e.g., ["Si 3p 3s"] + max_basis: list of basis entries: e.g., ["Si 3p 3s"]. + + Returns: + list[list[str]]: all possible combinations of basis functions, e.g. [["Si 3p 3s"]] + """ + max_basis_lists = [x.split() for x in max_basis] + min_basis_lists = [x.split() for x in min_basis] + + # Get all possible basis functions + basis_dict: dict[str, dict] = {} + for iel, el in enumerate(max_basis_lists): + basis_dict[el[0]] = {"fixed": [], "variable": [], "combinations": []} + for basis in el[1:]: + if basis in min_basis_lists[iel]: + basis_dict[el[0]]["fixed"].append(basis) + if basis not in min_basis_lists[iel]: + basis_dict[el[0]]["variable"].append(basis) + for L in range(len(basis_dict[el[0]]["variable"]) + 1): + for subset in itertools.combinations(basis_dict[el[0]]["variable"], L): + basis_dict[el[0]]["combinations"].append(" ".join([el[0]] + basis_dict[el[0]]["fixed"] + list(subset))) + + list_basis = [item["combinations"] for item in basis_dict.values()] + + # Get all combinations + start_basis = list_basis[0] + if len(list_basis) > 1: + for el in list_basis[1:]: + new_start_basis = [] + for elbasis in start_basis: + for elbasis2 in el: + if not isinstance(elbasis, list): + new_start_basis.append([elbasis, elbasis2]) + else: + new_start_basis.append([*elbasis.copy(), elbasis2]) + start_basis = new_start_basis + return start_basis + return [[basis] for basis in start_basis] diff --git a/future/lobster_basis/BASIS_PBE_54_max.yaml b/future/lobster_basis/BASIS_PBE_54_max.yaml new file mode 100644 index 0000000000..e4ed957f2a --- /dev/null +++ b/future/lobster_basis/BASIS_PBE_54_max.yaml @@ -0,0 +1,189 @@ +BASIS: + Ac: '5f 6d 6p 6s 7s ' + Ag: '4d 5p 5s ' + Ag_pv: '4d 4p 5p 5s ' + Al: '3p 3s ' + Am: '5f 6d 6p 6s 7s ' + Ar: '3p 3s ' + As: '4p 4s ' + As_d: '3d 4p 4s ' + At: '6p 6s ' + Au: '5d 6p 6s ' + B: '2p 2s ' + B_h: '2p 2s ' + B_s: '2p 2s ' + Ba_sv: '5p 5s 6s ' + Be: '2p 2s ' + Be_sv: '1s 2p 2s ' + Bi: '6p 6s ' + Bi_d: '5d 6p 6s ' + Br: '4p 4s ' + C: '2p 2s ' + C_h: '2p 2s ' + C_s: '2p 2s ' + Ca_pv: '3p 4s ' + Ca_sv: '3p 3s 4s ' + Cd: '4d 5p 5s ' + Ce: '4f 5d 5p 5s 6s ' + Ce_3: '5d 5p 5s 6s ' + Ce_h: '4f 5d 5p 5s 6s ' + Cf: '5f 6p 6s 7s ' + Cl: '3p 3s ' + Cl_h: '3p 3s ' + Cm: '5f 6d 6p 6s 7s ' + Co: '3d 4p 4s ' + Co_pv: '3d 3p 4s ' + Co_sv: '3d 3p 3s 4p 4s ' + Cr: '3d 4p 4s ' + Cr_pv: '3d 3p 4s ' + Cr_sv: '3d 3p 3s 4s ' + Cs_sv: '5p 5s 6s ' + Cu: '3d 4p 4s ' + Cu_pv: '3d 3p 4s ' + Dy: '4f 5d 5p 5s 6s ' + Dy_3: '5d 5p 6s ' + Er: '4f 5d 5p 5s 6s ' + Er_2: '5d 5p 6s ' + Er_3: '5d 5p 6s ' + Eu: '4f 5d 5p 5s 6s ' + Eu_2: '5d 5p 6s ' + Eu_3: '5d 5p 6s ' + F: '2p 2s ' + F_h: '2p 2s ' + F_s: '2p 2s ' + Fe: '3d 4p 4s ' + Fe_pv: '3d 3p 4s ' + Fe_sv: '3d 3p 3s 4s ' + Fr_sv: '6p 6s 7s ' + Ga: '4p 4s ' + Ga_d: '3d 4p 4s ' + Ga_h: '3d 4p 4s ' + Gd: '4f 5d 5p 5s 6s ' + Gd_3: '5d 5p 6s ' + Ge: '4p 4s ' + Ge_d: '3d 4p 4s ' + Ge_h: '3d 4p 4s ' + H: '1s ' + H_h: '1s ' + H_s: '1s ' + He: '1s ' + Hf: '5d 6p 6s ' + Hf_pv: '5d 5p 6s ' + Hf_sv: '5d 5p 5s 6s ' + Hg: '5d 6p 6s ' + Ho: '4f 5d 5p 5s 6s ' + Ho_3: '5d 5p 6s ' + I: '5p 5s ' + In: '5p 5s ' + In_d: '4d 5p 5s ' + Ir: '5d 6p 6s ' + K_pv: '3p 4s ' + K_sv: '3p 3s 4s ' + Kr: '4p 4s ' + La: '4f 5d 5p 5s 6s ' + La_s: '4f 5d 5p 6s ' + Li: '2p 2s ' + Li_sv: '1s 2p 2s ' + Lu: '4f 5d 5p 5s 6s ' + Lu_3: '5d 5p 6s ' + Mg: '3p 3s ' + Mg_pv: '2p 3s ' + Mg_sv: '2p 2s 3s ' + Mn: '3d 4p 4s ' + Mn_pv: '3d 3p 4s ' + Mn_sv: '3d 3p 3s 4s ' + Mo: '4d 5p 5s ' + Mo_pv: '4d 4p 5s ' + Mo_sv: '4d 4p 4s 5s ' + N: '2p 2s ' + N_h: '2p 2s ' + N_s: '2p 2s ' + Na: '3p 3s ' + Na_pv: '2p 3s ' + Na_sv: '2p 2s 3s ' + Nb_pv: '4d 4p 5s ' + Nb_sv: '4d 4p 4s 5s ' + Nd: '4f 5d 5p 5s 6s ' + Nd_3: '5d 5p 5s 6s ' + Ne: '2p 2s ' + Ni: '3d 4p 4s ' + Ni_pv: '3d 3p 4s ' + Np: '5f 6d 6p 6s 7s ' + Np_s: '5f 6d 6p 6s 7s ' + O: '2p 2s ' + O_h: '2p 2s ' + O_s: '2p 2s ' + Os: '5d 6p 6s ' + Os_pv: '5d 5p 6s ' + P: '3p 3s ' + P_h: '3p 3s ' + Pa: '5f 6d 6p 6s 7s ' + Pa_s: '5f 6d 6p 7s ' + Pb: '6p 6s ' + Pb_d: '5d 6p 6s ' + Pd: '4d 5p 5s ' + Pd_pv: '4d 4p 5s ' + Pm: '4f 5d 5p 5s 6s ' + Pm_3: '5d 5p 5s 6s ' + Po: '6p 6s ' + Po_d: '5d 6p 6s ' + Pr: '4f 5d 5p 5s 6s ' + Pr_3: '5d 5p 5s 6s ' + Pt: '5d 6p 6s ' + Pt_pv: '5d 5p 6p 6s ' + Pu: '5f 6d 6p 6s 7s ' + Pu_s: '5f 6d 6p 6s 7s ' + Ra_sv: '6p 6s 7s ' + Rb_pv: '4p 5s ' + Rb_sv: '4p 4s 5s ' + Re: '5d 6s ' + Re_pv: '5d 5p 6s ' + Rh: '4d 5p 5s ' + Rh_pv: '4d 4p 5s ' + Rn: '6p 6s ' + Ru: '4d 5p 5s ' + Ru_pv: '4d 4p 5s ' + Ru_sv: '4d 4p 4s 5s ' + S: '3p 3s ' + S_h: '3p 3s ' + Sb: '5p 5s ' + Sc: '3d 4p 4s ' + Sc_sv: '3d 3p 3s 4s ' + Se: '4p 4s ' + Si: '3p 3s ' + Sm: '4f 5d 5p 5s 6s ' + Sm_3: '5d 5p 5s 6s ' + Sn: '5p 5s ' + Sn_d: '4d 5p 5s ' + Sr_sv: '4p 4s 5s ' + Ta: '5d 6p 6s ' + Ta_pv: '5d 5p 6s ' + Tb: '4f 5d 5p 5s 6s ' + Tb_3: '5d 5p 6s ' + Tc: '4d 5p 5s ' + Tc_pv: '4d 4p 5s ' + Tc_sv: '4d 4p 4s 5s ' + Te: '5p 5s ' + Th: '5f 6d 6p 6s 7s ' + Th_s: '5f 6d 6p 7s ' + Ti: '3d 4p 4s ' + Ti_pv: '3d 3p 4s ' + Ti_sv: '3d 3p 3s 4s ' + Tl: '6p 6s ' + Tl_d: '5d 6p 6s ' + Tm: '4f 5d 5p 5s 6s ' + Tm_3: '5d 5p 6s ' + U: '5f 6d 6p 6s 7s ' + U_s: '5f 6d 6p 6s 7s ' + V: '3d 4p 4s ' + V_pv: '3d 3p 4s ' + V_sv: '3d 3p 3s 4s ' + W: '5d 6p 6s ' + W_sv: '5d 5p 5s 6s ' + Xe: '5p 5s ' + Y_sv: '4d 4p 4s 5s ' + Yb: '4f 5d 5p 5s 6s ' + Yb_2: '5d 5p 6s ' + Yb_3: '5d 5p 6s ' + Zn: '3d 4p 4s ' + Zr_sv: '4d 4p 4s 5s ' diff --git a/future/lobster_basis/BASIS_PBE_54_min.yaml b/future/lobster_basis/BASIS_PBE_54_min.yaml new file mode 100644 index 0000000000..99fa68ba99 --- /dev/null +++ b/future/lobster_basis/BASIS_PBE_54_min.yaml @@ -0,0 +1,189 @@ +BASIS: + Ac: '6d 6p 6s 7s ' + Ag: '4d 5s ' + Ag_pv: '4d 4p 5s ' + Al: '3p 3s ' + Am: '5f 6d 6p 6s 7s ' + Ar: '3p 3s ' + As: '4p 4s ' + As_d: '3d 4p 4s ' + At: '6p 6s ' + Au: '5d 6s ' + B: '2p 2s ' + B_h: '2p 2s ' + B_s: '2p 2s ' + Ba_sv: '5p 5s 6s ' + Be: '2s ' + Be_sv: '1s 2s ' + Bi: '6p 6s ' + Bi_d: '5d 6p 6s ' + Br: '4p 4s ' + C: '2p 2s ' + C_h: '2p 2s ' + C_s: '2p 2s ' + Ca_pv: '3p 4s ' + Ca_sv: '3p 3s 4s ' + Cd: '4d 5s ' + Ce: '4f 5d 5p 5s 6s ' + Ce_3: '5d 5p 5s 6s ' + Ce_h: '4f 5d 5p 5s 6s ' + Cf: '5f 6p 6s 7s ' + Cl: '3p 3s ' + Cl_h: '3p 3s ' + Cm: '5f 6d 6p 6s 7s ' + Co: '3d 4s ' + Co_pv: '3d 3p 4s ' + Co_sv: '3d 3p 3s 4s ' + Cr: '3d 4s ' + Cr_pv: '3d 3p 4s ' + Cr_sv: '3d 3p 3s 4s ' + Cs_sv: '5p 5s 6s ' + Cu: '3d 4s ' + Cu_pv: '3d 3p 4s ' + Dy: '4f 5d 5p 5s 6s ' + Dy_3: '5d 5p 6s ' + Er: '4f 5d 5p 5s 6s ' + Er_2: '5p 6s ' + Er_3: '5d 5p 6s ' + Eu: '4f 5p 5s 6s ' + Eu_2: '5p 6s ' + Eu_3: '5d 5p 6s ' + F: '2p 2s ' + F_h: '2p 2s ' + F_s: '2p 2s ' + Fe: '3d 4s ' + Fe_pv: '3d 3p 4s ' + Fe_sv: '3d 3p 3s 4s ' + Fr_sv: '6p 6s 7s ' + Ga: '4p 4s ' + Ga_d: '3d 4p 4s ' + Ga_h: '3d 4p 4s ' + Gd: '4f 5d 5p 5s 6s ' + Gd_3: '5d 5p 6s ' + Ge: '4p 4s ' + Ge_d: '3d 4p 4s ' + Ge_h: '3d 4p 4s ' + H: '1s ' + H_h: '1s ' + H_s: '1s ' + He: '1s ' + Hf: '5d 6s ' + Hf_pv: '5d 5p 6s ' + Hf_sv: '5d 5p 5s ' + Hg: '5d 6s ' + Ho: '4f 5d 5p 5s 6s ' + Ho_3: '5d 5p 6s ' + I: '5p 5s ' + In: '5p 5s ' + In_d: '4d 5p 5s ' + Ir: '5d 6s ' + K_pv: '3p 4s ' + K_sv: '3p 3s 4s ' + Kr: '4p 4s ' + La: '5d 5p 5s 6s ' + La_s: '5d 5p 6s ' + Li: '2s ' + Li_sv: '1s 2s ' + Lu: '4f 5d 5p 5s 6s ' + Lu_3: '5d 5p 6s ' + Mg: '3s ' + Mg_pv: '2p 3s ' + Mg_sv: '2p 2s 3s ' + Mn: '3d 4s ' + Mn_pv: '3d 3p 4s ' + Mn_sv: '3d 3p 3s 4s ' + Mo: '4d 5s ' + Mo_pv: '4d 4p 5s ' + Mo_sv: '4d 4p 4s 5s ' + N: '2p 2s ' + N_h: '2p 2s ' + N_s: '2p 2s ' + Na: '3s ' + Na_pv: '2p 3s ' + Na_sv: '2p 2s 3s ' + Nb_pv: '4d 4p 5s ' + Nb_sv: '4d 4p 4s 5s ' + Nd: '4f 5d 5p 5s 6s ' + Nd_3: '5d 5p 5s 6s ' + Ne: '2p 2s ' + Ni: '3d 4s ' + Ni_pv: '3d 3p 4s ' + Np: '5f 6d 6p 6s 7s ' + Np_s: '5f 6d 6p 6s 7s ' + O: '2p 2s ' + O_h: '2p 2s ' + O_s: '2p 2s ' + Os: '5d 6s ' + Os_pv: '5d 5p 6s ' + P: '3p 3s ' + P_h: '3p 3s ' + Pa: '5f 6d 6p 6s 7s ' + Pa_s: '5f 6d 6p 7s ' + Pb: '6p 6s ' + Pb_d: '5d 6p 6s ' + Pd: '4d 5s ' + Pd_pv: '4d 4p 5s ' + Pm: '4f 5d 5p 5s 6s ' + Pm_3: '5d 5p 5s 6s ' + Po: '6p 6s ' + Po_d: '5d 6p 6s ' + Pr: '4f 5d 5p 5s 6s ' + Pr_3: '5d 5p 5s 6s ' + Pt: '5d 6s ' + Pt_pv: '5d 5p 6s ' + Pu: '5f 6d 6p 6s 7s ' + Pu_s: '5f 6d 6p 6s 7s ' + Ra_sv: '6p 6s 7s ' + Rb_pv: '4p 5s ' + Rb_sv: '4p 4s 5s ' + Re: '5d 6s ' + Re_pv: '5d 5p 6s ' + Rh: '4d 5s ' + Rh_pv: '4d 4p 5s ' + Rn: '6p 6s ' + Ru: '4d 5s ' + Ru_pv: '4d 4p 5s ' + Ru_sv: '4d 4p 4s 5s ' + S: '3p 3s ' + S_h: '3p 3s ' + Sb: '5p 5s ' + Sc: '3d 4s ' + Sc_sv: '3d 3p 3s 4s ' + Se: '4p 4s ' + Si: '3p 3s ' + Sm: '4f 5d 5p 5s 6s ' + Sm_3: '5d 5p 5s 6s ' + Sn: '5p 5s ' + Sn_d: '4d 5p 5s ' + Sr_sv: '4p 4s 5s ' + Ta: '5d 6s ' + Ta_pv: '5d 5p 6s ' + Tb: '4f 5d 5p 5s 6s ' + Tb_3: '5d 5p 6s ' + Tc: '4d 5s ' + Tc_pv: '4d 4p 5s ' + Tc_sv: '4d 4p 4s 5s ' + Te: '5p 5s ' + Th: '5f 6d 6p 6s 7s ' + Th_s: '5f 6d 6p 7s ' + Ti: '3d 4s ' + Ti_pv: '3d 3p 4s ' + Ti_sv: '3d 3p 3s 4s ' + Tl: '6p 6s ' + Tl_d: '5d 6p 6s ' + Tm: '4f 5d 5p 5s 6s ' + Tm_3: '5d 5p 6s ' + U: '5f 6d 6p 6s 7s ' + U_s: '5f 6d 6p 6s 7s ' + V: '3d 4s ' + V_pv: '3d 3p 4s ' + V_sv: '3d 3p 3s 4s ' + W: '5d 6s ' + W_sv: '5d 5p 5s 6s ' + Xe: '5p 5s ' + Y_sv: '4d 4p 4s 5s ' + Yb: '4f 5p 5s 6s ' + Yb_2: '5p 6s ' + Yb_3: '5d 5p 6s ' + Zn: '3d 4s ' + Zr_sv: '4d 4p 4s 5s ' diff --git a/future/lobster_basis/BASIS_PBE_54_standard.yaml b/future/lobster_basis/BASIS_PBE_54_standard.yaml new file mode 100644 index 0000000000..b65b59dfac --- /dev/null +++ b/future/lobster_basis/BASIS_PBE_54_standard.yaml @@ -0,0 +1,189 @@ +BASIS: + Ac: '5f 6d 6p 6s 7s ' + Ag: '4d 5p 5s ' + Ag_pv: '4d 4p 5p 5s ' + Al: '3p 3s ' + Am: '5f 6d 6p 6s 7s ' + Ar: '3p 3s ' + As: '4p 4s ' + As_d: '3d 4p 4s ' + At: '6p 6s ' + Au: '5d 6p 6s ' + B: '2p 2s ' + B_h: '2p 2s ' + B_s: '2p 2s ' + Ba_sv: '5p 5s 6s ' + Be: '2p 2s ' + Be_sv: '1s 2p 2s ' + Bi: '6p 6s ' + Bi_d: '5d 6p 6s ' + Br: '4p 4s ' + C: '2p 2s ' + C_h: '2p 2s ' + C_s: '2p 2s ' + Ca_pv: '3p 4s ' + Ca_sv: '3p 3s 4s ' + Cd: '4d 5p 5s ' + Ce: '4f 5d 5p 5s 6s ' + Ce_3: '5d 5p 5s 6s ' + Ce_h: '4f 5d 5p 5s 6s ' + Cf: '5f 6p 6s 7s ' + Cl: '3p 3s ' + Cl_h: '3p 3s ' + Cm: '5f 6d 6p 6s 7s ' + Co: '3d 4p 4s ' + Co_pv: '3d 3p 4s ' + Co_sv: '3d 3p 3s 4p 4s ' + Cr: '3d 4p 4s ' + Cr_pv: '3d 3p 4s ' + Cr_sv: '3d 3p 3s 4s ' + Cs_sv: '5p 5s 6s ' + Cu: '3d 4p 4s ' + Cu_pv: '3d 3p 4s ' + Dy: '4f 5d 5p 5s 6s ' + Dy_3: '5d 5p 6s ' + Er: '4f 5d 5p 5s 6s ' + Er_2: '5d 5p 6s ' + Er_3: '5d 5p 6s ' + Eu: '4f 5d 5p 5s 6s ' + Eu_2: '5d 5p 6s ' + Eu_3: '5d 5p 6s ' + F: '2p 2s ' + F_h: '2p 2s ' + F_s: '2p 2s ' + Fe: '3d 4p 4s ' + Fe_pv: '3d 3p 4s ' + Fe_sv: '3d 3p 3s 4s ' + Fr_sv: '6p 6s 7s ' + Ga: '4p 4s ' + Ga_d: '3d 4p 4s ' + Ga_h: '3d 4p 4s ' + Gd: '4f 5d 5p 5s 6s ' + Gd_3: '5d 5p 6s ' + Ge: '4p 4s ' + Ge_d: '3d 4p 4s ' + Ge_h: '3d 4p 4s ' + H: '1s ' + H_h: '1s ' + H_s: '1s ' + He: '1s ' + Hf: '5d 6p 6s ' + Hf_pv: '5d 5p 6s ' + Hf_sv: '5d 5p 5s 6s ' + Hg: '5d 6p 6s ' + Ho: '4f 5d 5p 5s 6s ' + Ho_3: '5d 5p 6s ' + I: '5p 5s ' + In: '5p 5s ' + In_d: '4d 5p 5s ' + Ir: '5d 6p 6s ' + K_pv: '3p 4s ' + K_sv: '3p 3s 4s ' + Kr: '4p 4s ' + La: '4f 5d 5p 5s 6s ' + La_s: '4f 5d 5p 6s ' + Li: '2p 2s ' + Li_sv: '1s 2s 2p ' + Lu: '4f 5d 5p 5s 6s ' + Lu_3: '5d 5p 6s ' + Mg: '3p 3s ' + Mg_pv: '2p 3s ' + Mg_sv: '2p 2s 3s ' + Mn: '3d 4p 4s ' + Mn_pv: '3d 3p 4s ' + Mn_sv: '3d 3p 3s 4s ' + Mo: '4d 5p 5s ' + Mo_pv: '4d 4p 5s ' + Mo_sv: '4d 4p 4s 5s ' + N: '2p 2s ' + N_h: '2p 2s ' + N_s: '2p 2s ' + Na: '3p 3s ' + Na_pv: '2p 3s ' + Na_sv: '2p 2s 3s ' + Nb_pv: '4d 4p 5s ' + Nb_sv: '4d 4p 4s 5s ' + Nd: '4f 5d 5p 5s 6s ' + Nd_3: '5d 5p 5s 6s ' + Ne: '2p 2s ' + Ni: '3d 4p 4s ' + Ni_pv: '3d 3p 4s ' + Np: '5f 6d 6p 6s 7s ' + Np_s: '5f 6d 6p 6s 7s ' + O: '2p 2s ' + O_h: '2p 2s ' + O_s: '2p 2s ' + Os: '5d 6p 6s ' + Os_pv: '5d 5p 6s ' + P: '3p 3s ' + P_h: '3p 3s ' + Pa: '5f 6d 6p 6s 7s ' + Pa_s: '5f 6d 6p 7s ' + Pb: '6p 6s ' + Pb_d: '5d 6p 6s ' + Pd: '4d 5p 5s ' + Pd_pv: '4d 4p 5s ' + Pm: '4f 5d 5p 5s 6s ' + Pm_3: '5d 5p 5s 6s ' + Po: '6p 6s ' + Po_d: '5d 6p 6s ' + Pr: '4f 5d 5p 5s 6s ' + Pr_3: '5d 5p 5s 6s ' + Pt: '5d 6p 6s ' + Pt_pv: '5d 5p 6p 6s ' + Pu: '5f 6d 6p 6s 7s ' + Pu_s: '5f 6d 6p 6s 7s ' + Ra_sv: '6p 6s 7s ' + Rb_pv: '4p 5s ' + Rb_sv: '4p 4s 5s ' + Re: '5d 6s ' + Re_pv: '5d 5p 6s ' + Rh: '4d 5p 5s ' + Rh_pv: '4d 4p 5s ' + Rn: '6p 6s ' + Ru: '4d 5p 5s ' + Ru_pv: '4d 4p 5s ' + Ru_sv: '4d 4p 4s 5s ' + S: '3p 3s ' + S_h: '3p 3s ' + Sb: '5p 5s ' + Sc: '3d 4p 4s ' + Sc_sv: '3d 3p 3s 4s ' + Se: '4p 4s ' + Si: '3p 3s ' + Sm: '4f 5d 5p 5s 6s ' + Sm_3: '5d 5p 5s 6s ' + Sn: '5p 5s ' + Sn_d: '4d 5p 5s ' + Sr_sv: '4p 4s 5s ' + Ta: '5d 6p 6s ' + Ta_pv: '5d 5p 6s ' + Tb: '4f 5d 5p 5s 6s ' + Tb_3: '5d 5p 6s ' + Tc: '4d 5p 5s ' + Tc_pv: '4d 4p 5s ' + Tc_sv: '4d 4p 4s 5s ' + Te: '5p 5s ' + Th: '5f 6d 6p 6s 7s ' + Th_s: '5f 6d 6p 7s ' + Ti: '3d 4p 4s ' + Ti_pv: '3d 3p 4s ' + Ti_sv: '3d 3p 3s 4s ' + Tl: '6p 6s ' + Tl_d: '5d 6p 6s ' + Tm: '4f 5d 5p 5s 6s ' + Tm_3: '5d 5p 6s ' + U: '5f 6d 6p 6s 7s ' + U_s: '5f 6d 6p 6s 7s ' + V: '3d 4p 4s ' + V_pv: '3d 3p 4s ' + V_sv: '3d 3p 3s 4s ' + W: '5d 6p 6s ' + W_sv: '5d 5p 5s 6s ' + Xe: '5p 5s ' + Y_sv: '4d 4p 4s 5s ' + Yb: '4f 5d 5p 5s 6s ' + Yb_2: '5d 5p 6s ' + Yb_3: '5d 5p 6s ' + Zn: '3d 4p 4s ' + Zr_sv: '4d 4p 4s 5s ' diff --git a/future/lobsterenv.py b/future/lobsterenv.py new file mode 100644 index 0000000000..1f4d5ffcae --- /dev/null +++ b/future/lobsterenv.py @@ -0,0 +1,1523 @@ +"""This module provides classes to perform analyses of the local +environments (e.g., finding near neighbors) of single sites in molecules +and structures based on bonding analysis with LOBSTER. + +If you use this module, please cite: +J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, +"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +ChemPlusChem 2022, e202200123, +DOI: 10.1002/cplu.202200123. +""" + +from __future__ import annotations + +import collections +import copy +import math +import tempfile +from typing import TYPE_CHECKING, NamedTuple + +import matplotlib as mpl +import numpy as np +from monty.dev import deprecated + +from pymatgen.analysis.bond_valence import BVAnalyzer +from pymatgen.analysis.chemenv.coordination_environments.coordination_geometry_finder import LocalGeometryFinder +from pymatgen.analysis.chemenv.coordination_environments.structure_environments import LightStructureEnvironments +from pymatgen.analysis.local_env import NearNeighbors +from pymatgen.electronic_structure.cohp import CompleteCohp +from pymatgen.electronic_structure.core import Spin +from pymatgen.electronic_structure.plotter import CohpPlotter +from pymatgen.io.lobster import Charge, Icohplist +from pymatgen.util.due import Doi, due + +if TYPE_CHECKING: + from typing import Any, Literal, Self + + import matplotlib as mpl + from numpy.typing import NDArray + + from pymatgen.core import IStructure, PeriodicNeighbor, PeriodicSite, Structure + from pymatgen.core.periodic_table import Element + from pymatgen.electronic_structure.cohp import IcohpCollection, IcohpValue + from pymatgen.util.typing import PathLike + +__author__ = "Janine George" +__copyright__ = "Copyright 2021, The Materials Project" +__version__ = "1.0" +__maintainer__ = "J. George" +__email__ = "janinegeorge.ulfen@gmail.com" +__status__ = "Production" +__date__ = "February 2, 2021" + +due.cite( + Doi("10.1002/cplu.202200123"), + description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +) + + +class LobsterNeighbors(NearNeighbors): + """ + This class combines capabilities from LocalEnv and ChemEnv to determine + coordination environments based on bonding analysis. + """ + + def __init__( + self, + structure: Structure | IStructure, + filename_icohp: PathLike | None = "ICOHPLIST.lobster", + obj_icohp: Icohplist | None = None, + are_coops: bool = False, + are_cobis: bool = False, + valences: list[float] | None = None, + limits: tuple[float, float] | None = None, + additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, + only_bonds_to: list[str] | None = None, + perc_strength_icohp: float = 0.15, + noise_cutoff: float = 0.1, + valences_from_charges: bool = False, + filename_charge: PathLike | None = None, + obj_charge: Charge | None = None, + which_charge: Literal["Mulliken", "Loewdin"] = "Mulliken", + adapt_extremum_to_add_cond: bool = False, + add_additional_data_sg: bool = False, + filename_blist_sg1: PathLike | None = None, + filename_blist_sg2: PathLike | None = None, + id_blist_sg1: Literal["icoop", "icobi"] = "icoop", + id_blist_sg2: Literal["icoop", "icobi"] = "icobi", + ) -> None: + """ + Args: + filename_icohp (PathLike): Path to ICOHPLIST.lobster or + ICOOPLIST.lobster or ICOBILIST.lobster. + obj_icohp (Icohplist): Icohplist object. + structure (Structure): Typically constructed by Structure.from_file("POSCAR"). + are_coops (bool): Whether the file is a ICOOPLIST.lobster (True) or a + ICOHPLIST.lobster (False). Only tested for ICOHPLIST.lobster so far. + are_cobis (bool): Whether the file is a ICOBILIST.lobster (True) or + a ICOHPLIST.lobster (False). + valences (list[float]): Valence/charge for each element. + limits (tuple[float, float]): Range to decide which ICOHPs (ICOOP + or ICOBI) should be considered. + additional_condition (int): Additional condition that decides + which kind of bonds will be considered: + 0 - NO_ADDITIONAL_CONDITION + 1 - ONLY_ANION_CATION_BONDS + 2 - NO_ELEMENT_TO_SAME_ELEMENT_BONDS + 3 - ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS + 4 - ONLY_ELEMENT_TO_OXYGEN_BONDS + 5 - DO_NOT_CONSIDER_ANION_CATION_BONDS + 6 - ONLY_CATION_CATION_BONDS + only_bonds_to (list[str]): Only consider bonds to certain elements (e.g. ["O"] for oxygen). + perc_strength_icohp (float): If no "limits" are given, this will decide + which ICOHPs will be considered (relative to the strongest ICOHP/ICOOP/ICOBI). + noise_cutoff (float): The lower limit of ICOHPs considered. + valences_from_charges (bool): If True and path to CHARGE.lobster is provided, + will use LOBSTER charges (Mulliken) instead of valences. + filename_charge (PathLike): Path to Charge.lobster. + obj_charge (Charge): Charge object. + which_charge ("Mulliken" | "Loewdin"): Source of charge. + adapt_extremum_to_add_cond (bool): Whether to adapt the limits to only + focus on the bonds determined by the additional condition. + add_additional_data_sg (bool): Add the information from filename_add_bondinglist_sg1. + filename_blist_sg1 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. + filename_blist_sg2 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. + id_blist_sg1 ("icoop" | "icobi"): Identity of data in filename_blist_sg1. + id_blist_sg2 ("icoop" | "icobi"): Identity of data in filename_blist_sg2. + """ + if filename_icohp is not None: + self.ICOHP = Icohplist(are_coops=are_coops, are_cobis=are_cobis, filename=filename_icohp) + elif obj_icohp is not None: + self.ICOHP = obj_icohp + else: + raise ValueError("Please provide either filename_icohp or obj_icohp") + + self.Icohpcollection = self.ICOHP.icohpcollection + self.structure = structure + self.limits = limits + self.only_bonds_to = only_bonds_to + self.adapt_extremum_to_add_cond = adapt_extremum_to_add_cond + self.are_coops = are_coops + self.are_cobis = are_cobis + self.add_additional_data_sg = add_additional_data_sg + self.filename_blist_sg1 = filename_blist_sg1 + self.filename_blist_sg2 = filename_blist_sg2 + self.noise_cutoff = noise_cutoff + + self.id_blist_sg1 = id_blist_sg1.lower() + self.id_blist_sg2 = id_blist_sg2.lower() + + allowed_arguments = {"icoop", "icobi"} + if self.id_blist_sg1 not in allowed_arguments or self.id_blist_sg2 not in allowed_arguments: + raise ValueError("Algorithm can only work with ICOOPs, ICOBIs") + + if add_additional_data_sg: + if self.id_blist_sg1 == "icoop": + are_coops_id1 = True + are_cobis_id1 = False + else: + are_coops_id1 = False + are_cobis_id1 = True + + self.bonding_list_1 = Icohplist( + filename=self.filename_blist_sg1, + are_coops=are_coops_id1, + are_cobis=are_cobis_id1, + ) + + if self.id_blist_sg2 == "icoop": + are_coops_id2 = True + are_cobis_id2 = False + else: + are_coops_id2 = False + are_cobis_id2 = True + + self.bonding_list_2 = Icohplist( + filename=self.filename_blist_sg2, + are_coops=are_coops_id2, + are_cobis=are_cobis_id2, + ) + + # Check the additional condition + if additional_condition not in range(7): + raise ValueError(f"Unexpected {additional_condition=}, must be one of {list(range(7))}") + self.additional_condition = additional_condition + + # Read in valences, will prefer manual setting of valences + self.valences: list[float] | None + if valences is None: + if valences_from_charges and filename_charge is not None: + chg = Charge(filename=filename_charge) + if which_charge == "Mulliken": + self.valences = chg.mulliken + elif which_charge == "Loewdin": + self.valences = chg.loewdin + + elif valences_from_charges and obj_charge is not None: + chg = obj_charge + if which_charge == "Mulliken": + self.valences = chg.mulliken + elif which_charge == "Loewdin": + self.valences = chg.loewdin + + else: + bv_analyzer = BVAnalyzer() + try: + self.valences = bv_analyzer.get_valences(structure=self.structure) # type:ignore[arg-type] + except ValueError as exc: + self.valences = None + if additional_condition in {1, 3, 5, 6}: + raise ValueError( + "Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work" + ) from exc + else: + self.valences = valences + + if np.allclose(self.valences or [], np.zeros_like(self.valences)) and additional_condition in {1, 3, 5, 6}: + raise ValueError("All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work") + + if limits is None: + self.lowerlimit = self.upperlimit = None + else: + self.lowerlimit, self.upperlimit = limits + + # Evaluate coordination environments + self._evaluate_ce( + lowerlimit=self.lowerlimit, + upperlimit=self.upperlimit, + only_bonds_to=only_bonds_to, + additional_condition=self.additional_condition, + perc_strength_icohp=perc_strength_icohp, + adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, + ) + + @property + def structures_allowed(self) -> Literal[True]: + """Whether this LobsterNeighbors class can be used with Structure objects.""" + return True + + @property + def molecules_allowed(self) -> Literal[False]: + """Whether this LobsterNeighbors class can be used with Molecule objects.""" + return False + + @property + def anion_types(self) -> set[Element]: + """The set of anion types in crystal structure. + + Returns: + set[Element]: Anions in the crystal structure. + """ + if self.valences is None: + raise ValueError("No cations and anions defined") + + anion_species = [] + for site, val in zip(self.structure, self.valences, strict=True): + if val < 0.0: + anion_species.append(site.specie) + + return set(anion_species) + + @deprecated(anion_types) + def get_anion_types(self) -> set[Element]: + return self.anion_types + + def get_nn_info( + self, + structure: Structure | IStructure, + n: int, + use_weights: bool = False, + ) -> dict[str, Any]: + """Get coordination number (CN) of site by index. + + Args: + structure (Structure): Input structure. + n (int): Index of site for which to determine CN. + use_weights (bool): Whether to use weights for computing + the CN (True), or each coordinated site has equal weight (False). + The former is not implemented yet. + + Raises: + ValueError: If use_weights is True, or if arg "structure" and structure + used to initialize LobsterNeighbors have different lengths. + + Returns: + dict[str, Any]: coordination number and a list of nearest neighbors. + """ + if use_weights: + raise ValueError("LobsterEnv cannot use weights") + + if len(structure) != len(self.structure): + raise ValueError( + f"Length of structure ({len(structure)}) and LobsterNeighbors ({len(self.structure)}) differ" + ) + + return self.sg_list[n] # type: ignore[return-value] + + def get_light_structure_environment( + self, + only_cation_environments: bool = False, + only_indices: list[int] | None = None, + ) -> LobsterLightStructureEnvironments: + """Get a LobsterLightStructureEnvironments object if the structure + only contains coordination environments smaller 13. + + Args: + only_cation_environments (bool): Only return data for cations. + only_indices (list[int]): Only evaluate indexes in this list. + + Returns: + LobsterLightStructureEnvironments + """ + lgf = LocalGeometryFinder() + lgf.setup_structure(structure=self.structure) # type:ignore[arg-type] + list_ce_symbols = [] + list_csm = [] + list_permut = [] + for idx, _neigh_coords in enumerate(self.list_coords): + if (len(_neigh_coords)) > 13: + raise ValueError("Environment cannot be determined. Number of neighbors is larger than 13.") + # Avoid problems if _neigh_coords is empty + if _neigh_coords != []: + lgf.setup_local_geometry(isite=idx, coords=_neigh_coords, optimization=2) + cncgsm = lgf.get_coordination_symmetry_measures(optimization=2) + list_ce_symbols.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[0]) + list_csm.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["csm_wcs_ctwcc"]) + list_permut.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["indices"]) + else: + list_ce_symbols.append(None) + list_csm.append(None) + list_permut.append(None) + + new_list_ce_symbols = [] + new_list_csm = [] + new_list_permut = [] + new_list_neighsite = [] + new_list_neighisite = [] + + if only_indices is None: + if not only_cation_environments: + return LobsterLightStructureEnvironments.from_Lobster( + list_ce_symbol=list_ce_symbols, + list_csm=list_csm, + list_permutation=list_permut, + list_neighsite=self.list_neighsite, # type:ignore[arg-type] + list_neighisite=self.list_neighisite, + structure=self.structure, + valences=self.valences, + ) + + if self.valences is None: + raise ValueError(f"{self.valences=}") + + for idx, val in enumerate(self.valences): + if val >= 0.0: + new_list_ce_symbols.append(list_ce_symbols[idx]) + new_list_csm.append(list_csm[idx]) + new_list_permut.append(list_permut[idx]) + new_list_neighisite.append(self.list_neighisite[idx]) + new_list_neighsite.append(self.list_neighsite[idx]) + else: + new_list_ce_symbols.append(None) + new_list_csm.append(None) + new_list_permut.append([]) + new_list_neighisite.append([]) + new_list_neighsite.append([]) + + else: + for site_idx, _site in enumerate(self.structure): + if site_idx in only_indices: + new_list_ce_symbols.append(list_ce_symbols[site_idx]) + new_list_csm.append(list_csm[site_idx]) + new_list_permut.append(list_permut[site_idx]) + new_list_neighisite.append(self.list_neighisite[site_idx]) + new_list_neighsite.append(self.list_neighsite[site_idx]) + else: + new_list_ce_symbols.append(None) + new_list_csm.append(None) + new_list_permut.append([]) + new_list_neighisite.append([]) + new_list_neighsite.append([]) + + return LobsterLightStructureEnvironments.from_Lobster( + list_ce_symbol=new_list_ce_symbols, + list_csm=new_list_csm, + list_permutation=new_list_permut, + list_neighsite=new_list_neighsite, # type:ignore[arg-type] + list_neighisite=new_list_neighisite, + structure=self.structure, + valences=self.valences, + ) + + def get_info_icohps_to_neighbors( + self, + isites: list[int] | None = None, + onlycation_isites: bool = True, + ) -> ICOHPNeighborsInfo: + """Get information on the ICOHPs of neighbors for certain sites + as identified by their site id. + + This is useful for plotting the COHPs (ICOOPLIST.lobster/ + ICOHPLIST.lobster/ICOBILIST.lobster) of a site in the structure. + + + Args: + isites (list[int]): Site IDs. If is None, all isites will be used + to add the ICOHPs of the neighbors. + onlycation_isites (bool): If True and if isite is None, will + only analyse the cations sites. + + Returns: + ICOHPNeighborsInfo + """ + if self.valences is None and onlycation_isites: + raise ValueError("No valences are provided") + + if isites is None: + if onlycation_isites: + if self.valences is None: + raise ValueError(f"{self.valences}=") + + isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] + else: + isites = list(range(len(self.structure))) + + if self.Icohpcollection is None: + raise ValueError(f"{self.Icohpcollection=}") + + summed_icohps: float = 0.0 + list_icohps: list[float] = [] + number_bonds: int = 0 + labels: list[str] = [] + atoms: list[list[str]] = [] + final_isites: list[int] = [] + for idx, _site in enumerate(self.structure): + if idx in isites: + for key, icohpsum in zip(self.list_keys[idx], self.list_icohps[idx], strict=True): + summed_icohps += icohpsum + list_icohps.append(icohpsum) # type:ignore[arg-type] + labels.append(key) + atoms.append( + [ + self.Icohpcollection._list_atom1[int(key) - 1], + self.Icohpcollection._list_atom2[int(key) - 1], + ] + ) + number_bonds += 1 + final_isites.append(idx) + return ICOHPNeighborsInfo(summed_icohps, list_icohps, number_bonds, labels, atoms, final_isites) + + def plot_cohps_of_neighbors( + self, + path_to_cohpcar: PathLike | None = "COHPCAR.lobster", + obj_cohpcar: CompleteCohp | None = None, + isites: list[int] | None = None, + onlycation_isites: bool = True, + only_bonds_to: list[str] | None = None, + per_bond: bool = False, + summed_spin_channels: bool = False, + xlim: tuple[float, float] | None = None, + ylim: tuple[float, float] = (-10, 6), + integrated: bool = False, + ) -> mpl.axes.Axes: + """Plot summed COHPs or COBIs or COOPs. + + Please be careful in the spin polarized case (plots might overlap). + + Args: + path_to_cohpcar (PathLike): Path to COHPCAR or COOPCAR or COBICAR. + obj_cohpcar (CompleteCohp): CompleteCohp object + isites (list[int]): Site IDs. If empty, all sites will be used to add the ICOHPs of the neighbors. + onlycation_isites (bool): Only use cations, if isite is empty. + only_bonds_to (list[str]): Only anions in this list will be considered. + per_bond (bool): Whether to plot a normalization of the plotted COHP + per number of bond (True), or the sum (False). + xlim (tuple[float, float]): Limits of x values. + ylim (tuple[float, float]): Limits of y values. + integrated (bool): Whether to show integrated COHP instead of COHP. + + Returns: + plt of the COHPs or COBIs or COOPs. + """ + cp = CohpPlotter(are_cobis=self.are_cobis, are_coops=self.are_coops) + + plotlabel, summed_cohp = self.get_info_cohps_to_neighbors( + path_to_cohpcar, + obj_cohpcar, + isites, + only_bonds_to, + onlycation_isites, + per_bond, + summed_spin_channels=summed_spin_channels, + ) + + cp.add_cohp(plotlabel, summed_cohp) + ax = cp.get_plot(integrated=integrated) + if xlim is not None: + ax.set_xlim(xlim) + + if ylim is not None: + ax.set_ylim(ylim) + + return ax + + def get_info_cohps_to_neighbors( + self, + path_to_cohpcar: PathLike | None = "COHPCAR.lobster", + obj_cohpcar: CompleteCohp | None = None, + isites: list[int] | None = None, + only_bonds_to: list[str] | None = None, + onlycation_isites: bool = True, + per_bond: bool = True, + summed_spin_channels: bool = False, + ) -> tuple[str | None, CompleteCohp | None]: + """Get the COHPs (COOPs or COBIs) as a summed Cohp object + and a label from all sites mentioned in isites with neighbors. + + Args: + path_to_cohpcar (PathLike): Path to COHPCAR/COOPCAR/COBICAR. + obj_cohpcar (CompleteCohp): CompleteCohp object. + isites (list[int]): The indexes of the sites. + only_bonds_to (list[str]): Only show COHPs to selected element, e.g. ["O"]. + onlycation_isites (bool): If isites is None, only cation sites will be returned. + per_bond (bool): Whether to normalize per bond. + summed_spin_channels (bool): Whether to sum both spin channels. + + Returns: + str: Label for COHP. + CompleteCohp: Describe all COHPs/COOPs/COBIs of the sites + as given by isites and the other arguments. + """ + # TODO: add options for orbital-resolved COHPs + _summed_icohps, _list_icohps, _number_bonds, labels, atoms, final_isites = self.get_info_icohps_to_neighbors( + isites=isites, onlycation_isites=onlycation_isites + ) + + with tempfile.TemporaryDirectory() as tmp_dir: + path = f"{tmp_dir}/POSCAR.vasp" + + self.structure.to(filename=path, fmt="poscar") + + if not hasattr(self, "completecohp"): + if path_to_cohpcar is not None and obj_cohpcar is None: + self.completecohp = CompleteCohp.from_file( + fmt="LOBSTER", + filename=path_to_cohpcar, + structure_file=path, + are_coops=self.are_coops, + are_cobis=self.are_cobis, + ) + elif obj_cohpcar is not None: + self.completecohp = obj_cohpcar + else: + raise ValueError("Please provide either path_to_cohpcar or obj_cohpcar") + + # Check that the number of bonds in ICOHPLIST and COHPCAR are identical + # TODO: Further checks could be implemented + if self.Icohpcollection is None: + raise ValueError(f"{self.Icohpcollection=}") + + if len(self.Icohpcollection._list_atom1) != len(self.completecohp.bonds): + raise ValueError("COHPCAR and ICOHPLIST do not fit together") + + is_spin_completecohp = Spin.down in self.completecohp.get_cohp_by_label("1").cohp + if self.Icohpcollection.is_spin_polarized != is_spin_completecohp: + raise ValueError("COHPCAR and ICOHPLIST do not fit together") + + if only_bonds_to is None: + # Sort by anion type + divisor = len(labels) if per_bond else 1 + + plot_label = self._get_plot_label(atoms, per_bond) + summed_cohp = self.completecohp.get_summed_cohp_by_label_list( + label_list=labels, + divisor=divisor, + summed_spin_channels=summed_spin_channels, + ) + + else: + # Labels of the COHPs that will be summed + # Iterate through labels and atoms and check which bonds can be included + new_labels = [] + new_atoms = [] + if final_isites is None: + raise ValueError(f"{final_isites=}") + + for key, atompair, isite in zip(labels, atoms, final_isites, strict=True): + present = False + for atomtype in only_bonds_to: + # This is necessary to identify also bonds between the same elements correctly + if str(self.structure[isite].species.elements[0]) != atomtype: + if atomtype in { + self._split_string(atompair[0])[0], + self._split_string(atompair[1])[0], + }: + present = True + elif ( + atomtype == self._split_string(atompair[0])[0] + and atomtype == self._split_string(atompair[1])[0] + ): + present = True + + if present: + new_labels.append(key) + new_atoms.append(atompair) + if new_labels: + divisor = len(new_labels) if per_bond else 1 + + plot_label = self._get_plot_label(new_atoms, per_bond) + summed_cohp = self.completecohp.get_summed_cohp_by_label_list( + label_list=new_labels, + divisor=divisor, + summed_spin_channels=summed_spin_channels, + ) + else: + plot_label = None + + summed_cohp = None + + return plot_label, summed_cohp # type:ignore[return-value] + + def _get_plot_label(self, atoms: list[list[str]], per_bond: bool) -> str: + """Count the types of bonds and append a label.""" + all_labels = [] + for atoms_names in atoms: + new = [ + self._split_string(atoms_names[0])[0], + self._split_string(atoms_names[1])[0], + ] + new.sort() + string_here = f"{new[0]}-{new[1]}" + all_labels.append(string_here) + + counter = collections.Counter(all_labels) + plotlabels = [f"{item} x {key}" for key, item in counter.items()] + label = ", ".join(plotlabels) + if per_bond: + label += " (per bond)" + return label + + def get_info_icohps_between_neighbors( + self, + isites: list[int] | None = None, + onlycation_isites: bool = True, + ) -> ICOHPNeighborsInfo: + """Get interactions between neighbors of certain sites. + + Args: + isites (list[int]): Site IDs. If is None, all sites will be used. + onlycation_isites (bool): Only use cations, if isite is None. + + Returns: + ICOHPNeighborsInfo + """ + lowerlimit = self.lowerlimit + upperlimit = self.upperlimit + + if self.valences is None and onlycation_isites: + raise ValueError("No valences are provided") + + if isites is None: + if onlycation_isites: + if self.valences is None: + raise ValueError(f"{self.valences=}") + + isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] + else: + isites = list(range(len(self.structure))) + + summed_icohps: float = 0.0 + list_icohps: list[float] = [] + number_bonds: int = 0 + labels: list[str] = [] + atoms: list[list[str]] = [] + if self.Icohpcollection is None: + raise ValueError(f"{self.Icohpcollection=}") + + for isite in isites: + for site1_idx, n_site in enumerate(self.list_neighsite[isite]): + for site2_idx, n_site2 in enumerate(self.list_neighsite[isite]): + if site1_idx < site2_idx: + unitcell1 = self._determine_unit_cell(n_site) + unitcell2 = self._determine_unit_cell(n_site2) + + index_n_site = self._get_original_site(self.structure, n_site) # type:ignore[arg-type] + index_n_site2 = self._get_original_site(self.structure, n_site2) # type:ignore[arg-type] + + if index_n_site < index_n_site2: + translation = list(np.array(unitcell1) - np.array(unitcell2)) + elif index_n_site2 < index_n_site: + translation = list(np.array(unitcell2) - np.array(unitcell1)) + else: + translation = list(np.array(unitcell1) - np.array(unitcell2)) + + icohps = self._get_icohps( + icohpcollection=self.Icohpcollection, + site_idx=index_n_site, + lowerlimit=lowerlimit, + upperlimit=upperlimit, + only_bonds_to=self.only_bonds_to, + ) + + done = False + for icohp in icohps.values(): + atomnr1 = self._get_atomnumber(icohp._atom1) + atomnr2 = self._get_atomnumber(icohp._atom2) + label = icohp._label + + if (index_n_site == atomnr1 and index_n_site2 == atomnr2) or ( + index_n_site == atomnr2 and index_n_site2 == atomnr1 + ): + if atomnr1 != atomnr2: + if np.all(np.asarray(translation) == np.asarray(icohp._translation)): + summed_icohps += icohp.summed_icohp + list_icohps.append(icohp.summed_icohp) + number_bonds += 1 + labels.append(label) + atoms.append( + [ + self.Icohpcollection._list_atom1[int(label) - 1], + self.Icohpcollection._list_atom2[int(label) - 1], + ] + ) + + elif not done: + icohp_trans = -np.asarray( + [ + icohp._translation[0], + icohp._translation[1], + icohp._translation[2], + ] + ) + + if (np.all(np.asarray(translation) == np.asarray(icohp._translation))) or ( + np.all(np.asarray(translation) == icohp_trans) + ): + summed_icohps += icohp.summed_icohp + list_icohps.append(icohp.summed_icohp) + number_bonds += 1 + labels.append(label) + atoms.append( + [ + self.Icohpcollection._list_atom1[int(label) - 1], + self.Icohpcollection._list_atom2[int(label) - 1], + ] + ) + done = True + + return ICOHPNeighborsInfo(summed_icohps, list_icohps, number_bonds, labels, atoms, None) + + def _evaluate_ce( + self, + lowerlimit: float | None, + upperlimit: float | None, + only_bonds_to: list[str] | None = None, + additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, + perc_strength_icohp: float = 0.15, + adapt_extremum_to_add_cond: bool = False, + ) -> None: + """ + Args: + lowerlimit (float): Lower limit which determines the ICOHPs + that are considered for the determination of the neighbors. + upperlimit (float): Upper limit which determines the ICOHPs + that are considered for the determination of the neighbors. + only_bonds_to (list[str]): Restrict the types of bonds that will be considered. + additional_condition (int): Additional condition for the evaluation. + perc_strength_icohp (float): Determine how strong the ICOHPs + (percentage * strongest_ICOHP) will be that are still considered. + adapt_extremum_to_add_cond (bool): Whether to recalculate the limit + based on the bonding type and not on the overall extremum. + """ + # Get extremum + if lowerlimit is None and upperlimit is None: + if self.Icohpcollection is None: + raise ValueError(f"{self.Icohpcollection=}") + + limits = self._get_limit_from_extremum( + self.Icohpcollection, + percentage=perc_strength_icohp, + adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, + additional_condition=additional_condition, + ) + + if limits is None: + raise ValueError(f"{limits=}") + lowerlimit, upperlimit = limits + + elif upperlimit is None or lowerlimit is None: + raise ValueError("Please give two limits or leave them both at None") + + # Find environments based on ICOHP values + ( + list_icohps, + list_keys, + list_lengths, + list_neighisite, + list_neighsite, + list_coords, + ) = self._find_environments(additional_condition, lowerlimit, upperlimit, only_bonds_to) + + self.list_icohps = list_icohps + self.list_lengths = list_lengths + self.list_keys = list_keys + self.list_neighsite = list_neighsite + self.list_neighisite = list_neighisite + self.list_coords = list_coords + + # Make a structure graph + # Make sure everything is relative to the given Structure and + # not just the atoms in the unit cell + if self.add_additional_data_sg: + if self.bonding_list_1.icohpcollection is None: + raise ValueError(f"{self.bonding_list_1.icohpcollection=}") + if self.bonding_list_2.icohpcollection is None: + raise ValueError(f"{self.bonding_list_2.icohpcollection=}") + + self.sg_list = [ + [ + { + "site": neighbor, + "image": tuple( + round(idx) + for idx in ( + neighbor.frac_coords + - self.structure[ + next( + site_idx + for site_idx, site in enumerate(self.structure) + if neighbor.is_periodic_image(site) + ) + ].frac_coords + ) + ), + "weight": 1, + # Here, the ICOBIs and ICOOPs are added based on the bond + # strength cutoff of the ICOHP + # More changes are necessary here if we use ICOBIs for cutoffs + "edge_properties": { + "ICOHP": self.list_icohps[neighbors_idx][nbr_idx], + "bond_length": self.list_lengths[neighbors_idx][nbr_idx], + "bond_label": self.list_keys[neighbors_idx][nbr_idx], + self.id_blist_sg1.upper(): self.bonding_list_1.icohpcollection.get_icohp_by_label( + self.list_keys[neighbors_idx][nbr_idx] + ), + self.id_blist_sg2.upper(): self.bonding_list_2.icohpcollection.get_icohp_by_label( + self.list_keys[neighbors_idx][nbr_idx] + ), + }, + "site_index": next( + site_idx for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) + ), + } + for nbr_idx, neighbor in enumerate(neighbors) + ] + for neighbors_idx, neighbors in enumerate(self.list_neighsite) + ] + else: + self.sg_list = [ + [ + { + "site": neighbor, + "image": tuple( + round(idx) + for idx in ( + neighbor.frac_coords + - self.structure[ + next( + site_idx + for site_idx, site in enumerate(self.structure) + if neighbor.is_periodic_image(site) + ) + ].frac_coords + ) + ), + "weight": 1, + "edge_properties": { + "ICOHP": self.list_icohps[neighbors_idx][nbr_idx], + "bond_length": self.list_lengths[neighbors_idx][nbr_idx], + "bond_label": self.list_keys[neighbors_idx][nbr_idx], + }, + "site_index": next( + site_idx for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) + ), + } + for nbr_idx, neighbor in enumerate(neighbors) + ] + for neighbors_idx, neighbors in enumerate(self.list_neighsite) + ] + + def _find_environments( + self, + additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], + lowerlimit: float, + upperlimit: float, + only_bonds_to: list[str] | None, + ) -> tuple[ + list[list[IcohpValue]], + list[list[str]], + list[list[float]], + list[list[int]], + list[list[PeriodicNeighbor]], + list[list[NDArray]], + ]: + """Find all relevant neighbors based on certain restrictions. + + Args: + additional_condition (int): Additional condition. + lowerlimit (float): Lower limit that ICOHPs are considered. + upperlimit (float): Upper limit that ICOHPs are considered. + only_bonds_to (list[str]): Only bonds to these elements will be considered. + + Returns: + Tuple of ICOHPs, keys, lengths, neighisite, neighsite, coords. + """ + list_icohps: list[list[float]] = [] + list_keys: list[list[str]] = [] + list_lengths: list[list[float]] = [] + list_neighisite: list[list[int]] = [] + list_neighsite: list[list[PeriodicNeighbor]] = [] + list_coords: list[list[NDArray]] = [] + + # Run over structure + if self.Icohpcollection is None: + raise ValueError(f"{self.Icohpcollection=}") + + for idx, site in enumerate(self.structure): + icohps = self._get_icohps( + icohpcollection=self.Icohpcollection, + site_idx=idx, + lowerlimit=lowerlimit, + upperlimit=upperlimit, + only_bonds_to=only_bonds_to, + ) + + additional_conds = self._find_relevant_atoms_additional_condition(idx, icohps, additional_condition) + ( + keys_from_ICOHPs, + lengths_from_ICOHPs, + neighbors_from_ICOHPs, + selected_ICOHPs, + ) = additional_conds + + if len(neighbors_from_ICOHPs) > 0: + centralsite = site + + neighbors_by_distance_start = self.structure.get_sites_in_sphere( + pt=centralsite.coords, + r=np.max(lengths_from_ICOHPs) + 0.5, + include_image=True, + include_index=True, + ) + + neighbors_by_distance = [] + list_distances = [] + index_here_list = [] + coords = [] + for neigh_new in sorted(neighbors_by_distance_start, key=lambda x: x[1]): + site_here = neigh_new[0].to_unit_cell() + index_here = neigh_new[2] + index_here_list.append(index_here) + cell_here = neigh_new[3] + new_coords = [ + site_here.frac_coords[0] + float(cell_here[0]), + site_here.frac_coords[1] + float(cell_here[1]), + site_here.frac_coords[2] + float(cell_here[2]), + ] + coords.append(site_here.lattice.get_cartesian_coords(new_coords)) + + # new_site = PeriodicSite( + # species=site_here.species_string, + # coords=site_here.lattice.get_cartesian_coords(new_coords), + # lattice=site_here.lattice, + # to_unit_cell=False, + # coords_are_cartesian=True, + # ) + neighbors_by_distance.append(neigh_new[0]) + list_distances.append(neigh_new[1]) + _list_neighsite = [] + _list_neighisite = [] + copied_neighbors_from_ICOHPs = copy.copy(neighbors_from_ICOHPs) + copied_distances_from_ICOHPs = copy.copy(lengths_from_ICOHPs) + _neigh_coords = [] + _neigh_frac_coords = [] + + for neigh_idx, neigh in enumerate(neighbors_by_distance): + index_here2 = index_here_list[neigh_idx] + + for dist_idx, dist in enumerate(copied_distances_from_ICOHPs): + if ( + np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) + and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 + ): + _list_neighsite.append(neigh) + _list_neighisite.append(index_here2) + _neigh_coords.append(coords[neigh_idx]) + _neigh_frac_coords.append(neigh.frac_coords) + del copied_distances_from_ICOHPs[dist_idx] + del copied_neighbors_from_ICOHPs[dist_idx] + break + + list_neighisite.append(_list_neighisite) + list_neighsite.append(_list_neighsite) + list_lengths.append(lengths_from_ICOHPs) + list_keys.append(keys_from_ICOHPs) + list_coords.append(_neigh_coords) + list_icohps.append(selected_ICOHPs) + + else: + list_neighsite.append([]) + list_neighisite.append([]) + list_icohps.append([]) + list_lengths.append([]) + list_keys.append([]) + list_coords.append([]) + return ( # type:ignore[return-value] + list_icohps, + list_keys, + list_lengths, + list_neighisite, + list_neighsite, + list_coords, + ) + + def _find_relevant_atoms_additional_condition( + self, + site_idx: int, + icohps: dict[str, IcohpValue], + additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], + ) -> tuple[list[str], list[float], list[int], list[float]]: + """Find all relevant atoms that fulfill the additional condition. + + Args: + site_idx (int): Site index in structure (start from 0). + icohps (dict[str, IcohpValue]): ICOHP values. + additional_condition (int): Additional condition. + + Returns: + tuple: keys, lengths, neighbors from selected ICOHPs and selected ICOHPs. + """ + keys_from_ICOHPs: list[str] = [] + lengths_from_ICOHPs: list[float] = [] + neighbors_from_ICOHPs: list[int] = [] + icohps_from_ICOHPs: list[float] = [] + + for key, icohp in icohps.items(): + atomnr1 = self._get_atomnumber(icohp._atom1) + atomnr2 = self._get_atomnumber(icohp._atom2) + + # Check additional conditions + val1 = val2 = None + if self.valences is None: + raise ValueError(f"{self.valences=}") + if additional_condition in {1, 3, 5, 6}: + val1 = self.valences[atomnr1] + val2 = self.valences[atomnr2] + + # NO_ADDITIONAL_CONDITION + if additional_condition == 0: + if atomnr1 == site_idx: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + elif atomnr2 == site_idx: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + # ONLY_ANION_CATION_BONDS + elif additional_condition == 1: + if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): # type: ignore[operator] + if atomnr1 == site_idx: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == site_idx: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + # NO_ELEMENT_TO_SAME_ELEMENT_BONDS + elif additional_condition == 2: + if icohp._atom1.rstrip("0123456789") != icohp._atom2.rstrip("0123456789"): + if atomnr1 == site_idx: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == site_idx: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS + elif additional_condition == 3: + if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and icohp._atom1.rstrip( # type: ignore[operator] + "0123456789" + ) != icohp._atom2.rstrip("0123456789"): + if atomnr1 == site_idx: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == site_idx: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + # ONLY_ELEMENT_TO_OXYGEN_BONDS + elif additional_condition == 4: + if icohp._atom1.rstrip("0123456789") == "O" or icohp._atom2.rstrip("0123456789") == "O": + if atomnr1 == site_idx: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == site_idx: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + # DO_NOT_CONSIDER_ANION_CATION_BONDS + elif additional_condition == 5: + if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): # type: ignore[operator] + if atomnr1 == site_idx: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == site_idx: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + # ONLY_CATION_CATION_BONDS + elif additional_condition == 6 and val1 > 0.0 and val2 > 0.0: # type: ignore[operator] + if atomnr1 == site_idx: + neighbors_from_ICOHPs.append(atomnr2) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + elif atomnr2 == site_idx: + neighbors_from_ICOHPs.append(atomnr1) + lengths_from_ICOHPs.append(icohp._length) + icohps_from_ICOHPs.append(icohp.summed_icohp) + keys_from_ICOHPs.append(key) + + return ( + keys_from_ICOHPs, + lengths_from_ICOHPs, + neighbors_from_ICOHPs, + icohps_from_ICOHPs, + ) + + @staticmethod + def _get_icohps( + icohpcollection: IcohpCollection, + site_idx: int, + lowerlimit: float | None, + upperlimit: float | None, + only_bonds_to: list[str] | None, + ) -> dict[str, IcohpValue]: + """Get ICOHP dict for certain site. + + Args: + icohpcollection (IcohpCollection): IcohpCollection object. + site_idx (int): Site index. + lowerlimit (float): Lower limit that ICOHPs are considered. + upperlimit (float): Upper limit that ICOHPs are considered. + only_bonds_to (list[str]): Only bonds to these elements will be considered, e.g. ["O"]. + + Returns: + dict of IcohpValues. The keys correspond to the initial list_labels. + """ + return icohpcollection.get_icohp_dict_of_site( + site=site_idx, + maxbondlength=6.0, + minsummedicohp=lowerlimit, + maxsummedicohp=upperlimit, + only_bonds_to=only_bonds_to, + ) + + @staticmethod + def _get_atomnumber(atomstring: str) -> int: + """Get the index of the atom within the POSCAR (e.g., Return 0 for "Na1"). + + Args: + atomstring (str): Atom as str, such as "Na1". + + Returns: + int: Index of the atom in the POSCAR. + """ + return int(LobsterNeighbors._split_string(atomstring)[1]) - 1 + + @staticmethod + def _split_string(s) -> tuple[str, str]: + """Split strings such as "Na1" into ["Na", "1"] and return "1". + + Args: + s (str): String to split. + """ + head = s.rstrip("0123456789") + tail = s[len(head) :] + return head, tail + + @staticmethod + def _determine_unit_cell(site: PeriodicSite) -> list[int]: + """Determine the unit cell based on the site. + + Args: + site (PeriodicSite): The site. + """ + unitcell = [] + for coord in site.frac_coords: + value = math.floor(round(coord, 4)) + unitcell.append(value) + + return unitcell + + def _adapt_extremum_to_add_cond( + self, + list_icohps: list[float], + percentage: float, + ) -> float: + """Get the extremum from the given ICOHPs or ICOOPs or ICOBIs. + + Args: + list_icohps (list): ICOHPs or ICOOPs or ICOBIs. + percentage (float): The percentage to scale extremum. + + Returns: + float: Min value of ICOHPs, or max value of ICOOPs/ICOBIs. + """ + + which_extr = min if not self.are_coops and not self.are_cobis else max + return which_extr(list_icohps) * percentage + + def _get_limit_from_extremum( + self, + icohpcollection: IcohpCollection, + percentage: float = 0.15, + adapt_extremum_to_add_cond: bool = False, + additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, + ) -> tuple[float, float] | None: + """Get range for the ICOHP values from an IcohpCollection. + + Currently only work for ICOHPs. + + Args: + icohpcollection (IcohpCollection): IcohpCollection object. + percentage (float): Determine which ICOHPs/ICOOP/ICOBI will be considered. + adapt_extremum_to_add_cond (bool): Whether the extrumum be adapted to + the additional condition. + additional_condition (int): Additional condition to determine which bonds to include. + + Returns: + tuple[float, float]: [-inf, min(strongest_icohp*0.15, -noise_cutoff)] + or [max(strongest_icohp*0.15, noise_cutoff), inf]. + """ + extremum_based = None + + if self.valences is None: + raise ValueError(f"{self.valences=}") + + if not adapt_extremum_to_add_cond or additional_condition == 0: + extremum_based = icohpcollection.extremum_icohpvalue(summed_spin_channels=True) * percentage + + elif additional_condition == 1: + # ONLY_ANION_CATION_BONDS + list_icohps = [] + for value in icohpcollection._icohplist.values(): + atomnr1 = type(self)._get_atomnumber(value._atom1) + atomnr2 = type(self)._get_atomnumber(value._atom2) + + val1 = self.valences[atomnr1] + val2 = self.valences[atomnr2] + if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + elif additional_condition == 2: + # NO_ELEMENT_TO_SAME_ELEMENT_BONDS + list_icohps = [] + for value in icohpcollection._icohplist.values(): + if value._atom1.rstrip("0123456789") != value._atom2.rstrip("0123456789"): + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + elif additional_condition == 3: + # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS + list_icohps = [] + for value in icohpcollection._icohplist.values(): + atomnr1 = type(self)._get_atomnumber(value._atom1) + atomnr2 = type(self)._get_atomnumber(value._atom2) + val1 = self.valences[atomnr1] + val2 = self.valences[atomnr2] + + if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and value._atom1.rstrip( + "0123456789" + ) != value._atom2.rstrip("0123456789"): + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + elif additional_condition == 4: + # ONLY_ELEMENT_TO_OXYGEN_BONDS + list_icohps = [] + for value in icohpcollection._icohplist.values(): + if value._atom1.rstrip("0123456789") == "O" or value._atom2.rstrip("0123456789") == "O": + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + elif additional_condition == 5: + # DO_NOT_CONSIDER_ANION_CATION_BONDS + list_icohps = [] + for value in icohpcollection._icohplist.values(): + atomnr1 = type(self)._get_atomnumber(value._atom1) + atomnr2 = type(self)._get_atomnumber(value._atom2) + val1 = self.valences[atomnr1] + val2 = self.valences[atomnr2] + + if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + elif additional_condition == 6: + # ONLY_CATION_CATION_BONDS + list_icohps = [] + for value in icohpcollection._icohplist.values(): + atomnr1 = type(self)._get_atomnumber(value._atom1) + atomnr2 = type(self)._get_atomnumber(value._atom2) + val1 = self.valences[atomnr1] + val2 = self.valences[atomnr2] + + if val1 > 0.0 and val2 > 0.0: + list_icohps.append(value.summed_icohp) + + extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) + + if not self.are_coops and not self.are_cobis: + max_here = min(extremum_based, -self.noise_cutoff) if self.noise_cutoff is not None else extremum_based + return -float("inf"), max_here + + if self.are_coops or self.are_cobis: + min_here = max(extremum_based, self.noise_cutoff) if self.noise_cutoff is not None else extremum_based + return min_here, float("inf") + + return None + + +class LobsterLightStructureEnvironments(LightStructureEnvironments): + """Store LightStructureEnvironments based on LOBSTER outputs.""" + + @classmethod + def from_Lobster( + cls, + list_ce_symbol: list[str], + list_csm: list[float], + list_permutation: list, + list_neighsite: list[PeriodicSite], + list_neighisite: list[list[int]], + structure: Structure | IStructure, + valences: list[float] | None = None, + ) -> Self: + """Set up a LightStructureEnvironments from LOBSTER. + + Args: + list_ce_symbol (list[str]): Coordination environments symbols. + list_csm (list[float]): Continuous symmetry measures. + list_permutation (list): Permutations. + list_neighsite (list[PeriodicSite]): Neighboring sites. + list_neighisite (list[list[int]]): Neighboring sites indexes. + structure (Structure): Structure object. + valences (list[float]): Valences. + + Returns: + LobsterLightStructureEnvironments + """ + strategy = None + valences_origin = "user-defined" + coordination_environments = [] + all_nbs_sites = [] + all_nbs_sites_indices = [] + neighbors_sets = [] + counter = 0 + + for site_idx in range(len(structure)): + # Coordination environment + if list_ce_symbol is not None: + ce_dict = { + "ce_symbol": list_ce_symbol[site_idx], + "ce_fraction": 1.0, + "csm": list_csm[site_idx], + "permutation": list_permutation[site_idx], + } + else: + ce_dict = None + + if list_neighisite[site_idx] is not None: + all_nbs_sites_indices_here = [] + for neigh_site_idx, neigh_site in enumerate(list_neighsite[site_idx]): + diff = neigh_site.frac_coords - structure[list_neighisite[site_idx][neigh_site_idx]].frac_coords + round_diff = np.round(diff) + if not np.allclose(diff, round_diff): + raise ValueError( + "Weird, differences between one site in a periodic image cell is not integer ..." + ) + nb_image_cell = np.array(round_diff, int) + + all_nbs_sites_indices_here.append(counter) + + neighbor = { + "site": neigh_site, + "index": list_neighisite[site_idx][neigh_site_idx], + "image_cell": nb_image_cell, + } + all_nbs_sites.append(neighbor) + counter += 1 + + all_nbs_sites_indices.append(all_nbs_sites_indices_here) + + else: + all_nbs_sites.append({"site": None, "index": None, "image_cell": None}) + all_nbs_sites_indices.append([]) + + if list_neighisite[site_idx] is not None: + nb_set = cls.NeighborsSet( + structure=structure, # type:ignore[arg-type] + isite=site_idx, + all_nbs_sites=all_nbs_sites, + all_nbs_sites_indices=all_nbs_sites_indices[site_idx], + ) + + else: + nb_set = cls.NeighborsSet( + structure=structure, # type:ignore[arg-type] + isite=site_idx, + all_nbs_sites=[], + all_nbs_sites_indices=[], + ) + + coordination_environments.append([ce_dict]) + neighbors_sets.append([nb_set]) + + return cls( + strategy=strategy, + coordination_environments=coordination_environments, + all_nbs_sites=all_nbs_sites, + neighbors_sets=neighbors_sets, + structure=structure, + valences=valences, + valences_origin=valences_origin, + ) + + @property + def uniquely_determines_coordination_environments(self) -> Literal[True]: + """Whether the coordination environments are uniquely determined.""" + return True + + def as_dict(self) -> dict[str, Any]: + """Bson-serializable dict representation of the object. + + Returns: + Bson-serializable dict representation. + """ + return { + "@module": type(self).__module__, + "@class": type(self).__name__, + "strategy": self.strategy, + "structure": self.structure.as_dict(), + "coordination_environments": self.coordination_environments, + "all_nbs_sites": [ + { + "site": nb_site["site"].as_dict(), + "index": nb_site["index"], + "image_cell": [int(ii) for ii in nb_site["image_cell"]], + } + for nb_site in self._all_nbs_sites + ], + "neighbors_sets": [ + [nb_set.as_dict() for nb_set in site_nb_sets] or None for site_nb_sets in self.neighbors_sets + ], + "valences": self.valences, + } + + +class ICOHPNeighborsInfo(NamedTuple): + """Tuple to record information on relevant bonds. + + Args: + total_icohp (float): Sum of ICOHP values of neighbors to the selected + sites (given by the index in structure). + list_icohps (list): Summed ICOHP values for all identified interactions with neighbors. + n_bonds (int): Number of identified bonds to the selected sites. + labels (list[str]): Labels (from ICOHPLIST) for all identified bonds. + atoms (list[list[str]]): Lists describing the species present (from ICOHPLIST) + in the identified interactions , e.g. ["Ag3", "O5"]. + central_isites (list[int]): The central site indexes for each identified interaction. + """ + + total_icohp: float + list_icohps: list[float] + n_bonds: int + labels: list[str] + atoms: list[list[str]] + central_isites: list[int] | None diff --git a/future/outputs/__init__.py b/future/outputs/__init__.py new file mode 100644 index 0000000000..f804c3f064 --- /dev/null +++ b/future/outputs/__init__.py @@ -0,0 +1,80 @@ +"""Module for reading Lobster output files. For more information on LOBSTER see www.cohp.de. + +If you use this module, please cite: + +J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, +"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", ChemPlusChem 2022, e202200123, +DOI: 10.1002/cplu.202200123. +""" + +from __future__ import annotations + +from pymatgen.io.lobster.future.outputs.bands import BandOverlaps, Fatband, Fatbands +from pymatgen.io.lobster.future.outputs.coxxcar import COBICAR, COBICAR_LCFO, COHPCAR, COHPCAR_LCFO, COOPCAR, COXXCAR +from pymatgen.io.lobster.future.outputs.doscar import DOSCAR, DOSCAR_LCFO +from pymatgen.io.lobster.future.outputs.icoxxlist import ( + ICOBILIST, + ICOBILIST_LCFO, + ICOHPLIST, + ICOHPLIST_LCFO, + ICOOPLIST, + ICOXXLIST, + NcICOBILIST, +) +from pymatgen.io.lobster.future.outputs.lobsterout import LobsterOut +from pymatgen.io.lobster.future.outputs.misc import ( + BWDF, + BWDFCOHP, + POLARIZATION, + LobsterMatrices, + MadelungEnergies, + SitePotentials, + Wavefunction, +) +from pymatgen.io.lobster.future.outputs.populations import CHARGE, CHARGE_LCFO, GROSSPOP, GROSSPOP_LCFO +from pymatgen.util.due import Doi, due # type: ignore[import] + +__author__ = "Tom Demeyere" +__copyright__ = "Copyright 2025, The Materials Project" +__version__ = "0.3" +__maintainer__ = "Tom Demeyere" +__email__ = "tom.demeyere@bam.de" +__date__ = "Sep. 30, 2025" + +due.cite( + Doi("10.1002/cplu.202200123"), + description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +) + +__all__ = [ + "BWDF", + "BWDFCOHP", + "CHARGE", + "CHARGE_LCFO", + "COBICAR", + "COBICAR_LCFO", + "COHPCAR", + "COHPCAR_LCFO", + "COOPCAR", + "COXXCAR", + "DOSCAR", + "DOSCAR_LCFO", + "GROSSPOP", + "GROSSPOP_LCFO", + "ICOBILIST", + "ICOBILIST_LCFO", + "ICOHPLIST", + "ICOHPLIST_LCFO", + "ICOOPLIST", + "ICOXXLIST", + "POLARIZATION", + "BandOverlaps", + "Fatband", + "Fatbands", + "LobsterMatrices", + "LobsterOut", + "MadelungEnergies", + "NcICOBILIST", + "SitePotentials", + "Wavefunction", +] diff --git a/future/outputs/bands.py b/future/outputs/bands.py new file mode 100644 index 0000000000..77e1b181ba --- /dev/null +++ b/future/outputs/bands.py @@ -0,0 +1,500 @@ +from __future__ import annotations + +import re +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal, Self, cast + +import numpy as np +from monty.json import MSONable + +from pymatgen.core.structure import Structure +from pymatgen.electronic_structure.core import Spin +from pymatgen.io.lobster.future.constants import LOBSTER_VERSION +from pymatgen.io.lobster.future.core import LobsterFile +from pymatgen.io.lobster.future.utils import natural_sort, parse_orbital_from_text +from pymatgen.io.lobster.future.versioning import version_processor +from pymatgen.io.vasp.inputs import Kpoints +from pymatgen.io.vasp.outputs import Vasprun + +if TYPE_CHECKING: + from pymatgen.core.structure import IStructure + from pymatgen.io.lobster.future.types import LobsterBandOverlaps, LobsterFatband + from pymatgen.util.typing import PathLike + + +class BandOverlaps(LobsterFile): + """Parser for bandOverlaps.lobster files. + + Parses band overlap information produced by LOBSTER and stores it in a structured dictionary keyed by spin. + See the :class:`~pymatgen.io.lobster.future.types.LobsterBandOverlaps` type for details. + + Attributes: + band_overlaps (dict[Spin, dict]): + "k_points", "max_deviations", and "matrices" holding the corresponding data. + - "k_points" (list[list[float]]): List of k-point coordinates. + - "max_deviations" (list[float]): List of maximal deviations for each k-point. + - "matrices" (list[np.ndarray]): List of overlap matrices for each k-point. + + each holding data for each spin channel. + """ + + @version_processor(max_version="3.2") + def parse_file_v3_2_legacy(self) -> None: + """Parse bandOverlaps.lobster file for LOBSTER versions ≤3.2. + + Uses legacy spin numbering [0, 1] for parsing. + """ + self.parse_file(spin_numbers=[0, 1]) + + @version_processor(min_version="4.0") + def parse_file_v4_0(self) -> None: + """Parse bandOverlaps.lobster file for LOBSTER versions ≥4.0. + + Uses updated spin numbering [1, 2] for parsing. + """ + self.parse_file(spin_numbers=[1, 2]) + + def parse_file(self, spin_numbers: list[int]) -> None: + """Read all lines of the file and populate `self.band_overlaps`. + + Args: + spin_numbers (list[int]): Two integers indicating the spin numbering used + in the file (e.g., [0, 1] for legacy or [1, 2] for newer versions). + + Raises: + ValueError: If no data is found for a key in the bandOverlaps file. + """ + n_kpoints = {Spin.up: 0, Spin.down: 0} + matrix_size = None + + current_spin = Spin.up + for line in self.iterate_lines(): + if f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[0]}" in line: + current_spin = Spin.up + elif f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[1]}" in line: + current_spin = Spin.down + elif "maxDeviation" in line: + n_kpoints[current_spin] += 1 + elif matrix_size is None: + try: + float(line.split()[0]) + matrix_size = len(line.split()) + except ValueError: + continue + + if matrix_size is None: + raise ValueError("No data found for band overlaps in the file.") + + self.band_overlaps: LobsterBandOverlaps = { + "k_points": {}, + "max_deviations": {}, + "matrices": {}, + } + + self.spins = [Spin.up] + if n_kpoints[Spin.down] > 0: + self.spins.append(Spin.down) + + for spin in self.spins: + n = n_kpoints[spin] + self.band_overlaps["k_points"][spin] = np.empty((n, 3), dtype=np.float64) + self.band_overlaps["max_deviations"][spin] = np.empty(n, dtype=np.float64) + self.band_overlaps["matrices"][spin] = np.empty((n, matrix_size, matrix_size), dtype=np.float64) + + current_spin = Spin.up + kpoint_idx = {Spin.up: 0, Spin.down: 0} + matrix_row = 0 + for line in self.iterate_lines(): + if f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[0]}" in line: + current_spin = Spin.up + elif f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[1]}" in line: + current_spin = Spin.down + elif "k-point" in line: + self.band_overlaps["k_points"][current_spin][kpoint_idx[current_spin]] = [ + float(el) for el in line.strip().split()[-3:] + ] + kpoint_idx[current_spin] += 1 + elif "maxDeviation" in line: + maxdev = line.split(" ")[-1] + self.band_overlaps["max_deviations"][current_spin][kpoint_idx[current_spin] - 1] = float(maxdev) + + matrix_row = 0 + elif line.strip(): + try: + parts = [float(el) for el in re.split(r"\s+", line.strip())] + except ValueError: + raise ValueError(f"Incomplete or non-numeric data found in bandOverlaps file at line: {line}") + + if len(parts) == matrix_size: + self.band_overlaps["matrices"][current_spin][kpoint_idx[current_spin] - 1, matrix_row] = parts + + matrix_row += 1 + + def has_good_quality_max_deviation(self, limit_max_deviation: float = 0.1) -> bool: + """Check if the maxDeviation values are within a given limit. + + Args: + limit_max_deviation (float): Upper limit for acceptable max_deviation. + + Returns: + bool: True if all recorded max_deviation values are <= limit_max_deviation. + """ + return all( + deviation <= limit_max_deviation for deviation in self.band_overlaps["max_deviations"].get(Spin.up, []) + ) and all( + deviation <= limit_max_deviation for deviation in self.band_overlaps["max_deviations"].get(Spin.down, []) + ) + + def has_good_quality_check_occupied_bands( + self, + number_occ_bands_spin_up: int, + number_occ_bands_spin_down: int | None = None, + spin_polarized: bool = False, + limit_deviation: float = 0.1, + ) -> bool: + """Check if deviation from the ideal overlap for occupied bands is acceptable. + + Args: + number_occ_bands_spin_up (int): Number of occupied bands for spin up. + number_occ_bands_spin_down (int | None): Number of occupied bands for spin down. + Required if spin_polarized is True. + spin_polarized (bool): Whether the calculation is spin-polarized. + limit_deviation (float): Acceptable absolute tolerance for deviations. + + Raises: + ValueError: If `number_occ_bands_spin_down` is not specified for spin-polarized calculations. + + Returns: + bool: True if all occupied-band submatrices are close to identity within the tolerance. + """ + if spin_polarized and number_occ_bands_spin_down is None: + raise ValueError("number_occ_bands_spin_down has to be specified") + + for spin in (Spin.up, Spin.down) if spin_polarized else (Spin.up,): + if spin is Spin.up: + num_occ_bands = number_occ_bands_spin_up + else: + if number_occ_bands_spin_down is None: + raise ValueError("number_occ_bands_spin_down has to be specified") + num_occ_bands = number_occ_bands_spin_down + + for overlap_matrix in self.band_overlaps["matrices"][spin]: + sub_array = np.asarray(overlap_matrix)[:num_occ_bands, :num_occ_bands] + + if not np.allclose(sub_array, np.identity(num_occ_bands), atol=limit_deviation, rtol=0): + return False + + return True + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for the `BandOverlaps` class. + + Returns: + str: Default filename. + """ + return "bandOverlaps.lobster" + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> Self: + """Reconstruct a BandOverlaps instance from a dictionary. + + Args: + d (dict[str, Any]): Dictionary representation of a BandOverlaps instance. + + Returns: + BandOverlaps: Reconstructed instance. + """ + instance = super().from_dict(d) + + instance.band_overlaps["k_points"] = { + spin: np.asarray(k_points, dtype=np.float64) + for spin, k_points in instance.band_overlaps["k_points"].items() + } + instance.band_overlaps["max_deviations"] = { + spin: np.asarray(deviations, dtype=np.float64) + for spin, deviations in instance.band_overlaps["max_deviations"].items() + } + instance.band_overlaps["matrices"] = { + spin: np.asarray(matrices, dtype=np.float64) + for spin, matrices in instance.band_overlaps["matrices"].items() + } + + return instance + + +class Fatbands(MSONable): + """Reader for multiple FATBAND_*.lobster files in a directory. + + Collects FATBAND files, reads VASP outputs for the Fermi level and kpoints, and aggregates per-file parsed data. + + Attributes: + efermi (float): Fermi level read from vasprun.xml. + spins (list[Spin]): Spins present in the FATBAND files. + kpoints (Kpoints): Parsed KPOINTS used for the lobster FatBand calculations. + filenames (list[Path]): Sorted list of matched filenames. + structure (Structure): Structure object used for projections. + reciprocal_lattice (Lattice): Reciprocal lattice of the structure. + lobster_version (str): LOBSTER version string used for parsing. + fatbands (list[dict]): Aggregated parsed fatband data after process() is called. + """ + + def __init__( + self, + directory: PathLike = ".", + structure: IStructure | None = None, + kpoints_file: PathLike = "KPOINTS", + vasprun_file: PathLike = "vasprun.xml", + process_immediately: bool = True, + lobster_version: str | None = None, + ) -> None: + """Initialize the Fatbands reader. + + Args: + directory (PathLike): Path to directory containing FATBAND files. + structure (IStructure | None): Structure object. If None, POSCAR.lobster is read from directory. + kpoints_file (PathLike): Name of the KPOINTS file to be read in the directory. + vasprun_file (PathLike): Name of the vasprun.xml file to be read in the directory. + process_immediately (bool): If True, process FATBAND files immediately after initialization. + lobster_version (str | None): Optional LOBSTER version string. If None, the default LOBSTER_VERSION is used. + + Raises: + FileNotFoundError: If required files are missing in the directory. + ValueError: If no FATBAND files are found or KPOINTS file lacks weights. + """ + self.directory = Path(directory) + + self.filenames = sorted( + [str(i) for i in self.directory.glob(Fatband.get_default_filename())], + key=natural_sort, + ) + + if len(self.filenames) == 0: + raise ValueError("No FATBAND files found in the provided directory") + + self.efermi = Vasprun( + filename=self.directory / vasprun_file, + parse_eigen=False, + parse_potcar_file=False, + ).efermi + self.spins = [Spin.up] + full_kpoints = Kpoints.from_file(self.directory / kpoints_file) + + if full_kpoints.kpts_weights is not None: + filtered_data = [ + (k, w, n) + for k, w, n in zip( + full_kpoints.kpts, + full_kpoints.kpts_weights, + (full_kpoints.labels if full_kpoints.labels else [None] * len(full_kpoints.kpts)), + strict=True, + ) + if w == 0 + ] + + new_kpts, new_weights, new_labels = zip(*filtered_data, strict=True) if filtered_data else ([], [], []) + + coord_type = full_kpoints.coord_type + + if coord_type is None: + pass + elif coord_type not in {"Reciprocal", "Cartesian"}: + raise ValueError("KPOINTS coord_type must be 'Reciprocal' or 'Cartesian' for `Fatbands` parsing.") + + coord_type = cast("Literal['Reciprocal', 'Cartesian'] | None", coord_type) + + self.kpoints = Kpoints( + comment=full_kpoints.comment, + num_kpts=len(new_kpts), + style=full_kpoints.style, + kpts=list(new_kpts), + kpts_weights=list(new_weights), + labels=list(new_labels) if any(new_labels) else None, + coord_type=coord_type, + ) + else: + raise ValueError("KPOINTS file must contain weights for `Fatbands` parsing.") + + if structure is None: + try: + self.structure = Structure.from_file(Path(directory, "POSCAR.lobster")) + except FileNotFoundError: + raise FileNotFoundError("No POSCAR.lobster file found in directory, structure has to be given") + else: + self.structure = structure + + self.reciprocal_lattice = self.structure.lattice.reciprocal_lattice + + self.fatbands: list[LobsterFatband] = [] + + self.lobster_version = lobster_version or LOBSTER_VERSION + + if process_immediately: + self.process() + + def process(self) -> None: + """Parse all FATBAND files and aggregate fatband data. + + Raises: + ValueError: If the number of kpoints does not match or if there is a mix of spin-polarized and + non-spin-polarized files. + """ + is_spin_polarized = None + + for filename in self.filenames: + fatband = Fatband( + filename=filename, + process_immediately=False, + ) + fatband.lobster_version = self.lobster_version + + fatband.process() + fatband_data = fatband.fatband + + for spin in fatband.spins: + if len(fatband_data["projections"][spin]) != self.kpoints.num_kpts: + raise ValueError( + f"Number of kpoints ({self.kpoints.num_kpts}) does not " + f"match number of kpoints for {filename} " + f"({len(fatband_data['projections'][spin])})" + ) + + if is_spin_polarized is None: + is_spin_polarized = len(fatband_data["projections"]) > 1 + elif is_spin_polarized != (len(fatband_data["projections"]) > 1): + raise ValueError("Mix of spin polarized and non-spin polarized FATBAND files") + + self.fatbands.append(fatband_data) + + if is_spin_polarized: + self.spins.append(Spin.down) + + as_dict = LobsterFile.as_dict + + has_spin = LobsterFile.has_spin + + is_spin_polarized = LobsterFile.is_spin_polarized + + +class Fatband(LobsterFile): + """Parser for a single FATBAND_*.lobster file. + + Parses a single FATBAND file and stores: + center (str): Central atom/species label parsed from filename. + orbital (str): Orbital descriptor parsed from filename. + nbands (int): Number of bands in the FATBAND file. + fatband (LobsterFatband): Parsed fatband data dictionary. Please see + :class:`~pymatgen.io.lobster.future.types.LobsterFatband` for details. + + The parsed data is available in the fatband attribute after parse_file(). + """ + + def __init__( + self, + filename: PathLike, + process_immediately: bool = True, + lobster_version: str | None = None, + ) -> None: + """Initialize a Fatband parser. + + Args: + filename (PathLike): Path to the FATBAND file to parse. + process_immediately (bool): If True, parse the file during initialization. + lobster_version (str | None): LOBSTER version string to use for parsing. If None, attempts to detect + from file or falls back to default. + + Raises: + ValueError: If the orbital name cannot be parsed from the filename. + """ + self.center = Path(filename).name.split("_")[1].title() + + if orbital := parse_orbital_from_text(Path(filename).stem): + self.orbital = orbital + else: + raise ValueError( + f"Could not parse orbital from filename {filename}. " + "Ensure it follows the FATBAND_
_.lobster pattern." + ) + + super().__init__( + filename=filename, + process_immediately=process_immediately, + lobster_version=lobster_version, + ) + + @version_processor() + def parse_file(self) -> None: + """Parse the FATBAND file and populate the fatband attribute.""" + fatband: dict[str, dict[Spin, list[Any]]] = { + "energies": {Spin.up: []}, + "projections": {Spin.up: []}, + } + self.spins = [Spin.up] + + current_spin = Spin.up + for idx, line in enumerate(self.iterate_lines()): + if idx == 0: + self.nbands = int(line.split()[-1]) + continue + + if line.startswith("#"): + current_spin = Spin.up + + fatband["energies"][current_spin].append([]) + fatband["projections"][current_spin].append([]) + + continue + + if len(fatband["projections"][current_spin][-1]) == self.nbands: + current_spin = Spin.down + + if current_spin not in self.spins: + self.spins.append(current_spin) + fatband["energies"][current_spin] = [[]] + fatband["projections"][current_spin] = [[]] + else: + fatband["energies"][current_spin].append([]) + fatband["projections"][current_spin].append([]) + + if data := re.findall(r"[+-]?(?:[0-9]*[.])?[0-9]+", line): + fatband["energies"][current_spin][-1].append(float(data[1])) + fatband["projections"][current_spin][-1].append(float(data[-1])) + + self.fatband: LobsterFatband = { + "center": self.center, + "orbital": self.orbital, + "energies": fatband["energies"], + "projections": fatband["projections"], + } + + self.convert_to_numpy_arrays() + + def convert_to_numpy_arrays(self) -> None: + """Convert lists in band_overlaps to numpy arrays.""" + for spin in self.spins: + self.fatband["energies"][spin] = np.asarray(self.fatband["energies"][spin], dtype=np.float64) + self.fatband["projections"][spin] = np.asarray(self.fatband["projections"][spin], dtype=np.float64) + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> Self: + """Reconstruct a Fatband instance from a dictionary. + + Args: + d (dict[str, Any]): Dictionary representation of a Fatband instance. + + Returns: + Fatband: Reconstructed instance. + """ + instance = super().from_dict(d) + instance.convert_to_numpy_arrays() + + return instance + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for the Fatband class. + + Returns: + str: Default filename pattern. + """ + return "FATBAND_*.lobster" diff --git a/future/outputs/coxxcar.py b/future/outputs/coxxcar.py new file mode 100644 index 0000000000..f7099b4b0b --- /dev/null +++ b/future/outputs/coxxcar.py @@ -0,0 +1,380 @@ +from __future__ import annotations + +import re +from collections import defaultdict +from itertools import islice +from typing import TYPE_CHECKING + +import numpy as np + +from pymatgen.electronic_structure.core import Spin +from pymatgen.io.lobster.future.core import LobsterInteractionsHolder +from pymatgen.io.lobster.future.versioning import version_processor + +if TYPE_CHECKING: + from typing import ClassVar, Literal + + from numpy.typing import NDArray + + +class COXXCAR(LobsterInteractionsHolder): + """Reader for COXXCAR-style files (COOPCAR, COHPCAR, COBICAR). + + Parses LOBSTER's COXXCAR outputs and organizes bond and orbital-resolved interaction data. + + Attributes: + filename (PathLike): Input file path. + num_bonds (int): Number of bond interactions reported. + num_data (int): Number of energy/data points. + efermi (float): Fermi energy from the file. + spins (list[Spin]): Present spin channels. + interactions (list[dict]): Parsed interaction metadata. + data (np.ndarray): Raw numerical table parsed from the file. + """ + + interactions_regex: ClassVar[str] = ( + r"(?i)([a-z]+\d*(?:\_\d+)?)(?:\[(\-?\d+\s+\-?\d+\s+\-?\d+)\])?(?:\[([^]\s]*)\])?(?:\(([^)]*)\))?" + ) + coxxcar_type: ClassVar[str] + + @property + def energies(self) -> NDArray[np.floating]: + """Return the energy grid. + + Returns: + NDArray[np.floating]: Energies (first column of self.data). + """ + return self.data[:, 0] + + def parse_header(self) -> None: + """Parse the file header and set metadata attributes. + + Args: + lines (list[str]): Lines of the COXXCAR file. + """ + data = list(islice(self.iterate_lines(), 2))[1].split() + + self.num_bonds = int(data[0]) + self.num_data = int(data[2]) + self.efermi = float(data[-1]) + + if int(data[1]) == 2: + self.spins = [Spin.up, Spin.down] + else: + self.spins = [Spin.up] + + def parse_bonds(self) -> None: + """Parse the bonds/interactions header block. + + Args: + lines (list[str]): Lines of the COXXCAR file. + """ + self.interactions = [] + + self.parse_header() + + lines_generator = islice(self.iterate_lines(), 2, self.num_bonds + 2) + + for line in lines_generator: + if "Average" in line: + self.interactions.append( + { + "index": 0, + "centers": ["Average"], + "orbitals": [None], + "cells": [[]], + "length": None, + } + ) + continue + + bond_index, bond_data = line.split(":", 1) + + if bond_regex_results := re.search(r"No\.(\d+)", bond_index): + bond_index = bond_regex_results.group(1) + else: + raise ValueError(f"Could not parse bond index from line: {line}") + + length = None + + bond_tmp: dict[str, list] = defaultdict(list) + + centers = bond_data.split("->") + for center in centers: + if match := re.search(self.interactions_regex, center): + match = match.groups() + bond_tmp["centers"].append(match[0]) + else: + raise ValueError(f"Could not parse interaction from line: {line}") + + if match[1]: + bond_tmp["cells"].append([int(x) for x in match[1].split()]) + else: + bond_tmp["cells"].append([]) + + bond_tmp["orbitals"].append(match[2]) + + if match[3]: + length = float(match[3]) + + bond = { + "index": int(bond_index), + "centers": bond_tmp["centers"], + "cells": bond_tmp["cells"], + "orbitals": bond_tmp["orbitals"], + "length": length, + } + + self.interactions.append(bond) + + def parse_data(self) -> None: + """Parse the numerical data block into `self.data` and validate shape. + + Args: + lines (list[str]): Lines of the COXXCAR file. + + Raises: + ValueError: If the parsed data array shape does not match the expected shape. + """ + self.data = np.genfromtxt( + self.iterate_lines(), + dtype=np.float64, + skip_header=self.num_bonds + 2, + loose=False, + ) + + if self.data.shape != (self.num_data, self.num_bonds * 2 * len(self.spins) + 1): + raise ValueError( + f"Data shape {self.data.shape} does not match expected shape " + f"({self.num_data}, {self.num_bonds * 2 * len(self.spins) + 1})." + ) + + self.process_data_into_interactions() + + def process_data_into_interactions(self) -> None: + """Populate each interaction dict with 'coxx' and 'icoxx' views. + + Assigns numpy views into `self.data` for each spin channel. + """ + for i, interaction in enumerate(self.interactions): + real_indices = self.interaction_indices_to_data_indices_mapping( + i, + spins=self.spins, + ) + + interaction["coxx"] = {} + interaction["icoxx"] = {} + + if len(self.spins) == 1: + interaction["coxx"][Spin.up] = self.data[:, real_indices[0]] + interaction["icoxx"][Spin.up] = self.data[:, real_indices[1]] + else: + interaction["coxx"][Spin.up] = self.data[:, real_indices[0]] + interaction["icoxx"][Spin.up] = self.data[:, real_indices[1]] + interaction["coxx"][Spin.down] = self.data[:, real_indices[2]] + interaction["icoxx"][Spin.down] = self.data[:, real_indices[3]] + + @version_processor(min_version="5.1") + def parse_file(self) -> None: + """Parse the full COXXCAR file (header and data).""" + self.parse_bonds() + self.parse_data() + + def get_data_indices_by_properties( + self, + indices: list[int] | None = None, + centers: list[str] | None = None, + cells: list[list[int]] | None = None, + orbitals: list[str] | None = None, + length: tuple[float, float] | None = None, + spins: list[Literal[1, -1]] | None = None, + data_type: Literal["coxx", "icoxx"] | None = None, + ) -> list[int]: + """Return data-column indices matching the provided interaction properties. + + Args: + indices (list[int] | None): Interaction indices to filter. + centers (list[str] | None): Atom centers to filter. + cells (list[list[int]] | None): Unit cell indices to filter. + orbitals (list[str] | None): Orbitals to filter. + length (tuple[float, float] | None): Length range to filter. + spins (list[Spin] | None): Spins to include. + data_type (Literal["coxx", "icoxx"] | None): Restrict column type. + + Returns: + list[int]: Sorted list of data column indices that match the filters. + """ + return self.interaction_indices_to_data_indices_mapping( + sorted( + self.get_interaction_indices_by_properties( + indices, + centers, + cells, + orbitals, + length, + ) + ), + spins=spins or self.spins, + data_type=data_type, + ) + + def get_data_by_properties( + self, + indices: list[int] | None = None, + centers: list[str] | None = None, + cells: list[list[int]] | None = None, + orbitals: list[str] | None = None, + length: tuple[float, float] | None = None, + spins: list[Literal[1, -1]] | None = None, + data_type: Literal["coxx", "icoxx"] | None = None, + ) -> NDArray[np.floating]: + """Return the data columns matching the provided interaction properties. + + Args: + indices (list[int] | None): Interaction indices to filter. + centers (list[str] | None): Atom centers to filter. + cells (list[list[int]] | None): Unit cell indices to filter. + orbitals (list[str] | None): Orbitals to filter. + length (tuple[float, float] | None): Length range to filter. + spins (list[Spin] | None): Spins to include. + data_type (Literal["coxx", "icoxx"] | None): Restrict column type. + + Returns: + np.ndarray: Array with shape (n_energies, n_selected_columns). + """ + bond_indices = self.get_interaction_indices_by_properties(indices, centers, cells, orbitals, length) + spins = spins or self.spins + + return self.data[ + :, + self.interaction_indices_to_data_indices_mapping( + bond_indices, + spins=spins, + data_type=data_type, + ), + ] + + def interaction_indices_to_data_indices_mapping( + self, + interaction_indices: int | list[int], + spins: Literal[1, -1] | list[Literal[1, -1]] | None = None, + data_type: Literal["coxx", "icoxx"] | None = None, + ) -> list[int]: + """Map interaction indices to column indices in `self.data`. + + Args: + interaction_indices (int | list[int]): Single index or list of interaction indices. + spins (Spin | list[Spin] | None): Spin(s) to include. + data_type (Literal["coxx", "icoxx"] | None): Select columns of that type. + + Returns: + list[int]: Sorted list of integer column indices into `self.data`. + + Raises: + ValueError: If an invalid Spin is requested. + """ + if spins is None: + spins = self.spins + + if spins in (1, -1): + spins = [spins] + if isinstance(interaction_indices, int): + interaction_indices = [interaction_indices] + + if set(spins) - set(self.spins): + raise ValueError(f"Requested `Spin` {spins} is not valid. Valid `Spin`s are: {self.spins}.") + + index_range = np.arange(0, self.num_bonds * 2 * len(spins) + 1) + + if data_type == "icoxx": + index_range = index_range[1::2] + elif data_type == "coxx": + index_range = index_range[::2] + + real_indices = [] + for bond_index in interaction_indices: + real_indices.extend([bond_index * 2 + 1, bond_index * 2 + 2]) + + if Spin.down in spins: + real_indices.extend( + [ + (self.num_bonds + bond_index) * 2 + 1, + (self.num_bonds + bond_index) * 2 + 2, + ] + ) + + real_indices = np.array(real_indices, dtype=int) + real_indices = np.intersect1d(real_indices, index_range) + + return sorted(real_indices.tolist()) + + +class COBICAR(COXXCAR): + """Reader for COBICAR.lobster files. + + Attributes: + coxxcar_type (str): Type of COXXCAR file ("COBICAR"). + is_lcfo (bool): Whether the file is in LCFO format. + """ + + coxxcar_type: ClassVar[str] = "COBICAR" + + is_lcfo: ClassVar[bool] = False + + @classmethod + def get_default_filename(cls) -> str: + """Return the default filename for COBICAR.""" + return "COBICAR.LCFO.lobster" if cls.is_lcfo else "COBICAR.lobster" + + +class COHPCAR(COXXCAR): + """Reader for COHPCAR.lobster files. + + Attributes: + coxxcar_type (str): Type of COXXCAR file ("COHPCAR"). + is_lcfo (bool): Whether the file is in LCFO format. + """ + + coxxcar_type: ClassVar[str] = "COHPCAR" + + is_lcfo: ClassVar[bool] = False + + @classmethod + def get_default_filename(cls) -> str: + """Return the default filename for COOPCAR.""" + return "COHPCAR.LCFO.lobster" if cls.is_lcfo else "COHPCAR.lobster" + + +class COOPCAR(COXXCAR): + """Reader for COOPCAR.lobster files. + + Attributes: + coxxcar_type (str): Type of COXXCAR file ("COOPCAR"). + """ + + coxxcar_type: ClassVar[str] = "COOPCAR" + + @classmethod + def get_default_filename(cls) -> str: + """Return the default filename for COOPCAR.""" + return "COOPCAR.lobster" + + +class COHPCAR_LCFO(COHPCAR): + """Reader for COHPCAR.LCFO.lobster files. + + Attributes: + is_lcfo (bool): Always True for LCFO format. + """ + + is_lcfo: ClassVar[bool] = True + + +class COBICAR_LCFO(COBICAR): + """Reader for COBICAR.LCFO.lobster files. + + Attributes: + is_lcfo (bool): Always True for LCFO format. + """ + + is_lcfo: ClassVar[bool] = True diff --git a/future/outputs/doscar.py b/future/outputs/doscar.py new file mode 100644 index 0000000000..cfbae457bd --- /dev/null +++ b/future/outputs/doscar.py @@ -0,0 +1,216 @@ +from __future__ import annotations + +import re +from collections import defaultdict +from itertools import islice +from typing import TYPE_CHECKING + +import numpy as np + +from pymatgen.core.periodic_table import Element +from pymatgen.electronic_structure.core import Spin +from pymatgen.electronic_structure.dos import Dos +from pymatgen.io.lobster.future.core import LobsterFile +from pymatgen.io.lobster.future.versioning import version_processor + +if TYPE_CHECKING: + from typing import ClassVar + + from numpy import floating + from numpy.typing import NDArray + + from pymatgen.util.typing import PathLike + + +class DOSCAR(LobsterFile): + """Represents LOBSTER's projected DOS and local projected DOS. + + This class parses and stores data from the DOSCAR file generated by LOBSTER, + which contains information about the total and projected density of states + (DOS) for a quantum-chemical calculation performed with VASP. + + Attributes: + completedos (LobsterCompleteDos): Complete DOS data. + pdos (list[dict[str, dict[Spin, np.ndarray]]]): Projected DOS data. + Access as `pdos[atomindex]['orbitalstring'][Spin.up/Spin.down]`. + tdos (Dos): Total density of states. + energies (np.ndarray): Energies at which the DOS was calculated + (in eV, relative to Efermi). + tdensities (dict[Spin, np.ndarray]): Total density arrays for each spin channel. + itdensities (dict[Spin, np.ndarray]): Integrated total density arrays for each spin channel. + is_spin_polarized (bool): Whether the system is spin polarized. + """ + + is_lcfo: ClassVar[bool] = False + + def __init__( + self, + filename: PathLike | None = None, + process_immediately: bool = True, + lobster_version: str | None = None, + ) -> None: + """Initialize a DOSCAR object. + + Args: + filename (PathLike | None): Path to the DOSCAR file, typically "DOSCAR.lobster". + process_immediately (bool): Whether to process the file immediately upon initialization. + lobster_version (str | None): LOBSTER version string to use for parsing. If None, attempts to detect + + Raises: + ValueError: If neither `structure_file` nor `structure` is provided. + """ + self.projected_dos: dict[str, dict[str, Dos]] + self.total_dos: Dos + self.integrated_total_dos: Dos + + super().__init__( + filename=filename, + process_immediately=process_immediately, + lobster_version=lobster_version, + ) + + @version_processor() + def process(self) -> None: + """Process the DOSCAR file and extract DOS data. + + Parses the DOSCAR file to extract total DOS, projected DOS, energies, + and integrated densities. Sets the appropriate attributes based on + whether the calculation is spin-polarized. + + Raises: + ValueError: If the DOSCAR file format is invalid or spin polarization + cannot be determined. + """ + total_dos, integrated_total_dos = {}, {} + data: list[NDArray] = [] + + centers: list[str] = [] + orbitals: list[list[str]] = [] + + self.spins = [Spin.up] + + header_regex = r"\s*\S+\s+\S+\s+(\d+)\s+(\S+)\s+1\.0+(?:;(.*);(.*))?" + + efermi = None + ndos = 0 + + lines_iter = iter(self.iterate_lines()) + + center_counts = defaultdict(int) + + for line in islice(lines_iter, 5, None): + if match := re.match(header_regex, line): + ndos = int(match.group(1)) + + if efermi is None: + efermi = float(match.group(2)) + + if center_match := match.group(3): + center_match = center_match.strip() + + if center_match.startswith("Z="): + center_match = Element.from_Z(int(center_match.split()[-1])).symbol + + center_counts[center_match] += 1 + + separator = "_" if self.is_lcfo else "" + + centers.append(f"{center_match}{separator}{center_counts[center_match]}") + + if orbital_match := match.group(4): + orbitals += [[orb.strip() for orb in orbital_match.split()]] + + tmp_dos = [] + if line.strip(): + for _ in range(ndos): + line = next(lines_iter).split() + tmp_dos.append(line) + + data.append(np.array(tmp_dos, dtype=float)) + + if len(data[0][0, :]) == 5: + self.spins.append(Spin.down) + elif len(data[0][0, :]) != 3: + raise ValueError("There is something wrong with the DOSCAR. Can't extract spin polarization.") + + if efermi is None: + raise ValueError("There is something wrong with the DOSCAR. Can't find efermi.") + + energies = data[0][:, 0] + projected_dos = {} + + if self.is_spin_polarized: + total_dos[Spin.up] = data[0][:, 1] + total_dos[Spin.down] = data[0][:, 2] + + integrated_total_dos[Spin.up] = data[0][:, 3] + integrated_total_dos[Spin.down] = data[0][:, 4] + else: + total_dos[Spin.up] = data[0][:, 1] + integrated_total_dos[Spin.up] = data[0][:, 2] + + for atom_counter in range(len(data) - 1): + block_data = data[atom_counter + 1] + center = centers[atom_counter] + + if center not in projected_dos: + projected_dos[center] = {} + + for spin_index, spin in enumerate(self.spins): + for orbital_index, row in enumerate(range(spin_index + 1, block_data.shape[1], len(self.spins))): + orbital = orbitals[atom_counter][orbital_index] + + if orbital not in projected_dos[center]: + projected_dos[center][orbital] = {} + + projected_dos[center][orbital][spin] = block_data[:, row] + + for center, orbitals in projected_dos.items(): + for orbital, dos in orbitals.items(): + projected_dos[center][orbital] = Dos(efermi, energies, dos) + + self.projected_dos: dict[str, dict[str, Dos]] = dict(projected_dos) + self.total_dos: Dos = Dos(efermi, energies, total_dos) + self.integrated_total_dos: Dos = Dos(efermi, energies, integrated_total_dos) + + @property + def efermi(self) -> float: + """Fermi energy in eV.""" + return self.total_dos.efermi + + @property + def energies(self) -> NDArray[floating]: + """Energies at which the DOS was calculated (in eV, relative to efermi).""" + return self.total_dos.energies + + @property + def is_spin_polarized(self) -> bool: + """Whether the system is spin polarized. + + Returns: + bool: True if the system is spin polarized, False otherwise. + """ + return len(self.spins) == 2 + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for the DOSCAR. + + Returns: + str: Default filename string. "DOSCAR.lobster" for regular DOSCAR, + "DOSCAR.LCFO.lobster" for LCFO analysis. + """ + return "DOSCAR.lobster" if not cls.is_lcfo else "DOSCAR.LCFO.lobster" + + +class DOSCAR_LCFO(DOSCAR): + """Represents LOBSTER's projected DOS and local projected DOS for LCFO analysis. + + This class handles DOSCAR files generated from LCFO analysis, which have + a different format than regular DOSCAR files. + + Attributes: + is_lcfo (bool): Class variable indicating this is for LCFO analysis. Always True. + """ + + is_lcfo: ClassVar[bool] = True diff --git a/future/outputs/icoxxlist.py b/future/outputs/icoxxlist.py new file mode 100644 index 0000000000..ad8c89a614 --- /dev/null +++ b/future/outputs/icoxxlist.py @@ -0,0 +1,428 @@ +from __future__ import annotations + +import re +from collections import defaultdict +from itertools import islice +from typing import TYPE_CHECKING + +import numpy as np + +from pymatgen.electronic_structure.core import Spin +from pymatgen.io.lobster.future.core import LobsterInteractionsHolder +from pymatgen.io.lobster.future.outputs.coxxcar import COXXCAR +from pymatgen.io.lobster.future.utils import parse_orbital_from_text +from pymatgen.io.lobster.future.versioning import version_processor + +if TYPE_CHECKING: + from typing import ClassVar, Literal + + from numpy.typing import NDArray + + from pymatgen.io.lobster.future.types import LobsterInteractionData + + +class ICOXXLIST(LobsterInteractionsHolder): + """Reader for ICOXX data files (ICOHPLIST, ICOOPLIST, ICOBILIST). + + Parses interaction data from ICOXXLIST files, including spin-resolved values. + + Attributes: + interactions (list[LobsterInteractionData]): List of parsed interactions. + spins (list[Spin]): List of spins present in the file. + data (NDArray[np.floating]): Array of ICOXX values for each interaction and spin. + icoxxlist_type (str): Type of ICOXXLIST file ("COHP", "COOP", "COBI"). + is_lcfo (bool): Whether the file is in LCFO format (if applicable). + """ + + interactions_regex: ClassVar[str] = ( + r"(?i)\s*(\d+)\s+(\S+\s+\S+)\s+(\d+\.\d+)\s+(\-?\d+\s+\-?\d+\s+\-?\d+)?\s+(\-?\d+\.\d+)(?:\s+(\-?\d+\.\d+))?" + ) + icoxxlist_type: ClassVar[str] + + @version_processor(max_version="5.1") + def parse_file_legacy(self) -> None: + """Parse ICOXXLIST file using legacy format (versions ≤5.1). + + Extracts interaction data, including spin-resolved values, and populates + the `interactions`, `spins`, and `data` attributes. + + Raises: + ValueError: If the file contains invalid spin values or cannot parse + interaction lines. + """ + self.interactions = [] + self.spins = [] + + interaction_counter = 0 + for line in self.iterate_lines(): + if not line: + continue + + if line.startswith(f"{self.icoxxlist_type.upper()}#"): + if spin_regex := re.search(r"(?i)for spin\s+(\d)", line): + spin_regex = int(spin_regex.group(1)) + else: + continue + + if spin_regex == 1: + self.spins.append(Spin.up) + elif spin_regex == 2: + self.spins.append(Spin.down) + interaction_counter = 0 + else: + raise ValueError(f"Invalid spin value {spin_regex} in line: {line}") + else: + if matches := re.search(self.interactions_regex, line): + matches = matches.groups() + else: + raise ValueError(f"Could not parse interaction line: {line}") + + first_center, second_center = matches[1].split() + + first_orbital = parse_orbital_from_text(first_center) + second_orbital = parse_orbital_from_text(second_center) + + index = int(matches[0]) + centers = [first_center, second_center] + length = float(matches[2]) + + cells = [[], []] if matches[3] is None else [[0, 0, 0], [int(i) for i in matches[3].split()]] + + bond_tmp: LobsterInteractionData = { + "index": index, + "centers": centers, + "cells": cells, + "orbitals": [first_orbital, second_orbital], + "length": length, + "icoxx": { + self.spins[-1]: float(matches[4]), + }, + } + + if self.spins[-1] == Spin.up: + self.interactions.append(bond_tmp) + elif self.spins[-1] == Spin.down: + interaction = self.interactions[interaction_counter] + if "icoxx" in interaction: + interaction["icoxx"][Spin.down] = float(matches[4]) + else: + raise ValueError( + f"Down spin ICOXX value found without corresponding up spin value in line: {line}" + ) + + interaction_counter += 1 + + self.data = np.full((len(self.interactions), len(self.spins)), np.nan, dtype=np.float64) + + for i, interaction in enumerate(self.interactions): + if "icoxx" in interaction: + icoxx = interaction["icoxx"] + else: + raise ValueError(f"No ICOXX data found for interaction: {interaction}") + + if Spin.up in icoxx: + self.data[i, 0] = icoxx[Spin.up] + if Spin.down in icoxx: + self.data[i, 1] = icoxx[Spin.down] + + @version_processor(min_version="5.1") + def parse_file(self) -> None: + """Parse ICOXXLIST file using modern format (versions ≥5.1). + + Extracts interaction data, including spin-resolved values, and populates + the `interactions`, `spins`, and `data` attributes. + + Raises: + ValueError: If the file contains invalid spin values or cannot parse + interaction lines. + """ + self.interactions = [] + self.spins = [] + + for line in islice(self.iterate_lines(), 1, None): + if not line: + continue + + if spin_regex := re.findall(r"(?i)for spin\s+(\d)", line): + self.spins.append(Spin.up) + + if len(spin_regex) == 2: + self.spins.append(Spin.down) + else: + if matches := re.search(self.interactions_regex, line): + matches = matches.groups() + else: + raise ValueError(f"Could not parse interaction line: {line}") + + first_center, second_center = matches[1].split() + + first_orbital = parse_orbital_from_text(first_center) + second_orbital = parse_orbital_from_text(second_center) + + index = int(matches[0]) + centers = [ + first_center.replace(f"_{first_orbital}", ""), + second_center.replace(f"_{second_orbital}", ""), + ] + length = float(matches[2]) + + cells = [[], []] if matches[3] is None else [[0, 0, 0], [int(i) for i in matches[3].split()]] + + bond_tmp: LobsterInteractionData = { + "index": index, + "centers": centers, + "cells": cells, + "orbitals": [first_orbital, second_orbital], + "length": length, + "icoxx": { + Spin.up: float(matches[4]), + }, + } + + if len(self.spins) == 2: + bond_tmp["icoxx"][Spin.down] = float(matches[5]) + + self.interactions.append(bond_tmp) + + self.data = np.full((len(self.interactions), len(self.spins)), np.nan, dtype=np.float64) + + for i, interaction in enumerate(self.interactions): + if "icoxx" in interaction: + icoxx = interaction["icoxx"] + else: + raise ValueError(f"No ICOXX data found for interaction: {interaction}") + + if Spin.up in icoxx: + self.data[i, 0] = icoxx[Spin.up] + if Spin.down in icoxx: + self.data[i, 1] = icoxx[Spin.down] + + def get_data_by_properties( + self: LobsterInteractionsHolder, + indices: list[int] | None = None, + centers: list[str] | None = None, + cells: list[list[int]] | None = None, + orbitals: list[str] | None = None, + length: tuple[float, float] | None = None, + spins: list[Literal[1, -1]] | None = None, + ) -> NDArray[np.floating]: + """Get the data for bonds matching specified properties. + + Args: + indices (list[int] | None): Indices of bonds to retrieve. + centers (list[str] | None): Centers of bonds to retrieve. + cells (list[list[int]] | None): Cells of bonds to retrieve. + orbitals (list[str] | None): Orbitals of bonds to retrieve. + length (tuple[float, float] | None): Length range to filter. + spins (list[Spin] | None): Spins to retrieve. + + Returns: + NDArray[np.floating]: Array of data for specified bonds. + """ + interaction_indices = self.get_interaction_indices_by_properties(indices, centers, cells, orbitals, length) + + spins = spins or self.spins + spin_indices = [0 if spin == Spin.up else 1 for spin in spins] + + return self.data[np.ix_(interaction_indices, spin_indices)] + + def process_data_into_interactions(self) -> None: + """Populate each interaction dict with 'coxx' and 'icoxx' views. + + Assigns numpy views into `self.data` for each spin channel. + """ + spin_indices = {spin: i for i, spin in enumerate(self.spins)} + + for i, interaction in enumerate(self.interactions): + interaction["icoxx"] = {} + for spin, index in spin_indices.items(): + interaction["icoxx"][spin] = float(self.data[i, index]) + + +class ICOHPLIST(ICOXXLIST): + """Reader for ICOHPLIST.lobster files. + + Attributes: + icoxxlist_type (str): Type of ICOXXLIST file ("COHP"). + is_lcfo (bool): Whether the file is in LCFO format. + """ + + icoxxlist_type: ClassVar[str] = "COHP" + is_lcfo: ClassVar[bool] = False + + @classmethod + def get_default_filename(cls) -> str: + """Return the default filename for `ICOHPLIST`.""" + return "ICOHPLIST.LCFO.lobster" if cls.is_lcfo else "ICOHPLIST.lobster" + + +class ICOOPLIST(ICOXXLIST): + """Reader for ICOOPLIST.lobster files. + + Attributes: + icoxxlist_type (str): Type of ICOXXLIST file ("COOP"). + """ + + icoxxlist_type: ClassVar[str] = "COOP" + + @classmethod + def get_default_filename(cls) -> str: + """Return the default filename for `ICOOPLIST`.""" + return "ICOOPLIST.lobster" + + +class ICOBILIST(ICOXXLIST): + """Reader for ICOBILIST.lobster files. + + Attributes: + icoxxlist_type (str): Type of ICOXXLIST file ("COBI"). + is_lcfo (bool): Whether the file is in LCFO format. + """ + + icoxxlist_type: ClassVar[str] = "COBI" + is_lcfo: ClassVar[bool] = False + + @classmethod + def get_default_filename(cls) -> str: + """Return the default filename for ICOBILIST.""" + return "ICOBILIST.LCFO.lobster" if cls.is_lcfo else "ICOBILIST.lobster" + + +class ICOHPLIST_LCFO(ICOHPLIST): + """Reader for ICOHPLIST.LCFO.lobster files. + + Attributes: + is_lcfo (bool): Always True for LCFO format. + """ + + is_lcfo: ClassVar[bool] = True + + +class ICOBILIST_LCFO(ICOBILIST): + """Reader for ICOBILIST.LCFO.lobster files. + + Attributes: + is_lcfo (bool): Always True for LCFO format. + """ + + is_lcfo: ClassVar[bool] = True + + +class NcICOBILIST(LobsterInteractionsHolder): + """Reader for NcICOBILIST.lobster files. + + Parses non-conventional ICOBI interaction data. + + Attributes: + interactions (list): List of parsed interactions. + spins (list[Spin]): List of spins present in the file. + data (NDArray[np.floating]): Array of ICOXX values for each interaction and spin. + """ + + interactions_regex: ClassVar[str] = COXXCAR.interactions_regex + + @version_processor(min_version="5.1") + def parse_file(self) -> None: + """Parse the `NcICOBILIST` file. + + Extracts interaction data, including spin-resolved values, and populates + the `interactions`, `spins`, and `data` attributes. + + Raises: + ValueError: If the file contains invalid spin values or cannot parse + interaction lines. + """ + self.interactions = [] + self.spins = [] + + interaction_counter = 0 + + for line in self.iterate_lines(): + if not line: + continue + + if line.startswith(("COBI#", "for spin")): + interaction_counter = 0 + if match := re.search(r"(?i)for spin\s+(\d)", line): + spin_regex = int(match.group(1)) + else: + continue + + if spin_regex == 1: + self.spins.append(Spin.up) + elif spin_regex == 2: + self.spins.append(Spin.down) + else: + raise ValueError(f"Invalid spin value {spin_regex} in line: {line}") + else: + line = re.split(r"\s+(?![^\[]*\])", line) + + bond_tmp = defaultdict(list) + length = None + + index = int(line[0]) + + nc_icobi_value = float(line[-2]) + + for center in line[-1].split("->"): + if match := re.search(self.interactions_regex, center): + match = match.groups() + else: + raise ValueError(f"Could not parse interaction center line: {line}") + + bond_tmp["centers"].append(match[0]) + + if match[1]: + bond_tmp["cells"].append([int(x) for x in match[1].split()]) + else: + bond_tmp["cells"].append([]) + + bond_tmp["orbitals"].append(match[2]) + + if match[3]: + length = float(match[3]) + + current_spin = self.spins[-1] + if current_spin == Spin.up: + self.interactions.append( + { + "index": index, + "centers": bond_tmp["centers"], + "cells": bond_tmp["cells"], + "orbitals": bond_tmp["orbitals"], + "length": length, + "icoxx": { + Spin.up: nc_icobi_value, + }, + } + ) + elif current_spin == Spin.down: + interaction = self.interactions[interaction_counter] + if "icoxx" in interaction: + interaction["icoxx"][Spin.down] = nc_icobi_value + else: + raise ValueError(f"Invalid spin value {current_spin} in line: {line}") + + interaction_counter += 1 + + self.data = np.full((len(self.interactions), len(self.spins)), np.nan, dtype=np.float64) + + for i, interaction in enumerate(self.interactions): + if "icoxx" in interaction: + icoxx = interaction["icoxx"] + else: + raise ValueError(f"No ICOXX data found for interaction: {interaction}") + + if Spin.up in icoxx: + self.data[i, 0] = icoxx[Spin.up] + if Spin.down in icoxx: + self.data[i, 1] = icoxx[Spin.down] + + get_data_by_properties = ICOXXLIST.get_data_by_properties + + process_data_into_interactions = ICOXXLIST.process_data_into_interactions + + @classmethod + def get_default_filename(cls) -> str: + """Return the default filename for NcICOBILIST.""" + return "NcICOBILIST.lobster" diff --git a/future/outputs/lobsterout.py b/future/outputs/lobsterout.py new file mode 100644 index 0000000000..cadcb39dee --- /dev/null +++ b/future/outputs/lobsterout.py @@ -0,0 +1,439 @@ +from __future__ import annotations + +import re +from typing import TYPE_CHECKING +from warnings import warn + +from pymatgen.io.lobster.future.constants import LOBSTER_VERSION +from pymatgen.io.lobster.future.core import LobsterFile +from pymatgen.io.lobster.future.versioning import version_processor + +if TYPE_CHECKING: + from typing import Literal + + +class LobsterOut(LobsterFile): + """Parser for `lobsterout` file from LOBSTER. + + This class reads the `lobsterout` file and extracts information about + basis functions, spillings, warnings, timing, and file presence. It supports + parsing for different LOBSTER versions and provides attributes to access + the parsed data. + + Attributes: + basis_functions (list[str]): Basis functions used in the LOBSTER run. + basis_type (list[str]): Basis types used in the LOBSTER run. + charge_spilling (list[float]): Charge spilling for each spin channel. + dft_program (str): DFT program used for the calculation. + elements (list[str]): Elements present in the calculation. + has_charge (bool): Whether `CHARGE.lobster` is present. + has_cohpcar (bool): Whether `COHPCAR.lobster` and `ICOHPLIST.lobster` are present. + has_madelung (bool): Whether `SitePotentials.lobster` and `MadelungEnergies.lobster` are present. + has_coopcar (bool): Whether `COOPCAR.lobster` and `ICOOPLIST.lobster` are present. + has_cobicar (bool): Whether `COBICAR.lobster` and `ICOBILIST.lobster` are present. + has_doscar (bool): Whether `DOSCAR.lobster` is present. + has_doscar_lso (bool): Whether `DOSCAR.LSO.lobster` is present. + has_projection (bool): Whether `projectionData.lobster` is present. + has_bandoverlaps (bool): Whether `bandOverlaps.lobster` is present. + has_density_of_energies (bool): Whether `DensityOfEnergy.lobster` is present. + has_fatbands (bool): Whether fatband calculation was performed. + has_grosspopulation (bool): Whether `GROSSPOP.lobster` is present. + has_polarization (bool): Whether `POLARIZATION.lobster` is present. + info_lines (list[str]): Additional information on the run. + info_orthonormalization (list[str]): Information on orthonormalization. + is_restart_from_projection (bool): Whether calculation was restarted from a projection file. + lobster_version (str): The LOBSTER version. + number_of_spins (int): Number of spins. + number_of_threads (int): Number of threads used. + timing (dict[str, float]): Timing information. + total_spilling (list[float]): Total spilling for each spin channel. + warning_lines (list[str]): All warning messages. + """ + + @version_processor(min_version="5.1") + def _process_v5_1(self) -> None: + """Process `lobsterout` (version ≥ 5.1). + + This method extracts file presence flags and other attributes + specific to LOBSTER version 5.1 and later. + + Note: + This method is automatically invoked for files with version ≥5.1. + """ + lines = self.lines + + self.has_cohpcar = "writing COOPCAR.lobster..." in lines and "SKIPPING writing COOPCAR.lobster..." not in lines + self.has_coopcar = "writing COHPCAR.lobster..." in lines and "SKIPPING writing COHPCAR.lobster..." not in lines + self.has_cobicar = "writing COBICAR.lobster..." in lines and "SKIPPING writing COBICAR.lobster..." not in lines + + self._process_common() + + def _process_common(self) -> None: + """Process common parts of `lobsterout` for all versions. + + This method extracts general information such as timing, warnings, + basis functions, and file presence flags that are applicable across + all LOBSTER versions. + """ + lines = self.lines + + self.is_restart_from_projection = "loading projection from projectionData.lobster..." in lines + + self.has_error = "ERROR:" in lines + + if self.has_error: + self.error_lines = [line for line in lines if line.startswith("ERROR:")] + raise RuntimeError(f"LOBSTER calculation ended with errors:\n{self.error_lines}") + + self.number_of_threads = self._get_threads(lines) + self.dft_program = self._get_dft_program(lines) + + self.number_of_spins = self._get_number_of_spins(lines) + self.charge_spilling, self.total_spilling = self._get_spillings( + lines=lines, number_of_spins=self.number_of_spins + ) + + self.elements, self.basis_type, self.basis_functions = self._get_elements_basistype_basisfunctions(lines) + + wall_time, user_time, sys_time = self._get_timing(lines) + self.timing = { + "wall_time": wall_time, + "user_time": user_time, + "sys_time": sys_time, + } + + self.warning_lines = self._get_all_warning_lines(lines) + + self.info_orthonormalization = self._get_warning_orthonormalization(lines) + + self.info_lines = self._get_all_info_lines(lines) + + self.has_doscar = "writing DOSCAR.lobster..." in lines and "SKIPPING writing DOSCAR.lobster..." not in lines + self.has_doscar_lso = ( + "writing DOSCAR.LSO.lobster..." in lines and "SKIPPING writing DOSCAR.LSO.lobster..." not in lines + ) + + self.has_cobicar_lcfo = "writing COBICAR.LCFO.lobster..." in lines + self.has_cohpcar_lcfo = "writing COHPCAR.LCFO.lobster..." in lines + self.has_coopcar_lcfo = "writing COOPCAR.LCFO.lobster..." in lines + self.has_doscar_lcfo = "writing DOSCAR.LCFO.lobster..." in lines + self.has_polarization = "writing polarization to POLARIZATION.lobster..." in lines + self.has_charge = "SKIPPING writing CHARGE.lobster..." not in lines + self.has_projection = "saving projection to projectionData.lobster..." in lines + self.has_bandoverlaps = "WARNING: I dumped the band overlap matrices to the file bandOverlaps.lobster." in lines + self.has_fatbands = self._has_fatband(lines) + self.has_grosspopulation = "writing CHARGE.lobster and GROSSPOP.lobster..." in lines + self.has_density_of_energies = "writing DensityOfEnergy.lobster..." in lines + self.has_madelung = ( + "writing SitePotentials.lobster and MadelungEnergies.lobster..." in lines + and "skipping writing SitePotentials.lobster and MadelungEnergies.lobster..." not in lines + ) + self.has_mofecar = "Writing MOFECAR.lobster and IMOFELIST.lobster..." in lines + + @version_processor(max_version="5.0") + def _process_legacy(self) -> None: + """Process `lobsterout` for legacy versions (≤5.0). + + This method extracts file presence flags and other attributes + specific to LOBSTER versions ≤5.0. + + Note: + This method is automatically invoked for files with version ≤5.0. + """ + lines = self.lines + + self.has_cohpcar = ( + "writing COOPCAR.lobster and ICOOPLIST.lobster..." in lines + and "SKIPPING writing COOPCAR.lobster and ICOOPLIST.lobster..." not in lines + ) + self.has_coopcar = ( + "writing COHPCAR.lobster and ICOHPLIST.lobster..." in lines + and "SKIPPING writing COHPCAR.lobster and ICOHPLIST.lobster..." not in lines + ) + self.has_cobicar = ( + "writing COBICAR.lobster and ICOBILIST.lobster..." in lines + and "SKIPPING writing COBICAR.lobster and ICOBILIST.lobster..." not in lines + ) + + self._process_common() + + def process(self) -> None: + """Parse the `lobsterout` file and populate attributes. + + This method determines the LOBSTER version and invokes the appropriate + version-specific processing method. + + Raises: + RuntimeError: If the LOBSTER version cannot be determined. + """ + self.lobster_version = self.get_lobster_version(self.lines) + + super().process() + + @staticmethod + def get_lobster_version(lines: list[str]) -> str: + """Get the LOBSTER version from the `lobsterout` lines. + + Args: + lines (list[str]): Lines of the `lobsterout` file. + + Returns: + str: The LOBSTER version. + + Raises: + RuntimeError: If the version line is not found. + """ + for line in lines: + if version := re.search(r"(?i)LOBSTER\s(?:v(\d+\.\d+\.\d+))", line): + return version.group(1) + + warn( + f"Could not find LOBSTER version in lobsterout. Defaulting to v{LOBSTER_VERSION}", + stacklevel=2, + ) + + return LOBSTER_VERSION + + @staticmethod + def _has_fatband(lines: list[str]) -> bool: + """Check whether the calculation includes fatband data. + + Args: + lines (list[str]): Lines of the `lobsterout` file. + + Returns: + bool: True if fatband data is present, False otherwise. + """ + for line in lines: + line_parts = line.split() + if len(line_parts) > 1 and line_parts[1] == "FatBand": + return True + return False + + @staticmethod + def _get_dft_program(lines: list[str]) -> str | None: + """Get the DFT program used for the calculation. + + Args: + lines (list[str]): Lines of the `lobsterout` file. + + Returns: + str | None: The name of the DFT program, or None if not found. + """ + for line in lines: + line_parts = line.split() + if len(line_parts) > 4 and line_parts[3] == "program...": + return line_parts[4] + return None + + @staticmethod + def _get_number_of_spins(lines: list[str]) -> Literal[1, 2]: + """Get the number of spin channels. + + Args: + lines (list[str]): Lines of the `lobsterout` file. + + Returns: + Literal[1, 2]: 1 if single spin channel, 2 if two spin channels. + """ + return 2 if "spillings for spin channel 2" in lines else 1 + + @staticmethod + def _get_threads(lines: list[str]) -> int: + """Get the number of CPU threads used. + + Args: + lines (list[str]): Lines of the `lobsterout` file. + + Returns: + int: Number of threads. + + Raises: + ValueError: If the number of threads cannot be determined. + """ + for line in lines: + line_parts = line.split() + if len(line_parts) > 11 and line_parts[11] in {"threads", "thread"}: + return int(line_parts[10]) + raise ValueError("Threads not found.") + + @staticmethod + def _get_spillings( + lines: list[str], + number_of_spins: Literal[1, 2], + ) -> tuple[list[float], list[float]]: + """Get charge spillings and total spillings. + + Args: + lines (list[str]): Lines of the `lobsterout` file. + number_of_spins (Literal[1, 2]): Number of spin channels. + + Returns: + tuple[list[float], list[float]]: Charge spillings and total spillings + for each spin channel. + """ + charge_spillings = [] + total_spillings = [] + for line in lines: + line_parts = line.split() + if len(line_parts) > 2 and line_parts[2] == "spilling:": + if line_parts[1] == "charge": + charge_spillings.append(float(line_parts[3].replace("%", "")) / 100.0) + elif line_parts[1] == "total": + total_spillings.append(float(line_parts[3].replace("%", "")) / 100.0) + + if len(charge_spillings) == number_of_spins and len(total_spillings) == number_of_spins: + break + + return charge_spillings, total_spillings + + @staticmethod + def _get_elements_basistype_basisfunctions( + lines: list[str], + ) -> tuple[list[str], list[str], list[list[str]]]: + """Get elements, basis types, and basis functions. + + Args: + lines (list[str]): Lines of the `lobsterout` file. + + Returns: + tuple[list[str], list[str], list[list[str]]]: Elements, basis types, + and basis functions used in the calculation. + """ + begin = False + end = False + elements: list[str] = [] + basistypes: list[str] = [] + basisfunctions: list[list[str]] = [] + for line in lines: + if begin and not end: + line_parts = line.split() + if line_parts[0] not in { + "INFO:", + "WARNING:", + "setting", + "calculating", + "post-processing", + "saving", + "spillings", + "writing", + }: + elements.append(line_parts[0]) + basistypes.append(line_parts[1].replace("(", "").replace(")", "")) + # Last sign is '' + basisfunctions.append(line_parts[2:]) + else: + end = True + + if "setting up local basis functions..." in line: + begin = True + return elements, basistypes, basisfunctions + + @staticmethod + def _get_timing( + lines: list[str], + ) -> tuple[dict[str, str], dict[str, str], dict[str, str]]: + """Get wall time, user time, and system time. + + Args: + lines (list[str]): Lines of the `lobsterout` file. + + Returns: + tuple[dict[str, str], dict[str, str], dict[str, str]]: Dictionaries + containing timing information for wall, user, and system times. + """ + begin = False + user_times, wall_times, sys_times = [], [], [] + + for line in lines: + line_parts = line.split() + if "finished" in line_parts: + begin = True + if begin: + if "wall" in line_parts: + wall_times = line_parts[2:10] + if "user" in line_parts: + user_times = line_parts[:8] + if "sys" in line_parts: + sys_times = line_parts[:8] + + wall_time_dict = { + "h": wall_times[0], + "min": wall_times[2], + "s": wall_times[4], + "ms": wall_times[6], + } + user_time_dict = { + "h": user_times[0], + "min": user_times[2], + "s": user_times[4], + "ms": user_times[6], + } + sys_time_dict = { + "h": sys_times[0], + "min": sys_times[2], + "s": sys_times[4], + "ms": sys_times[6], + } + + return wall_time_dict, user_time_dict, sys_time_dict + + @staticmethod + def _get_warning_orthonormalization(lines: list[str]) -> list[str]: + """Get orthonormalization warnings. + + Args: + lines (list[str]): Lines of the `lobsterout` file. + + Returns: + list[str]: List of orthonormalization warnings. + """ + orthowarnings = [] + for line in lines: + line_parts = line.split() + if "orthonormalized" in line_parts: + orthowarnings.append(" ".join(line_parts[1:])) + return orthowarnings + + @staticmethod + def _get_all_warning_lines(lines: list[str]) -> list[str]: + """Get all warning lines. + + Args: + lines (list[str]): Lines of the `lobsterout` file. + + Returns: + list[str]: List of warning messages. + """ + warnings_ = [] + for line in lines: + line_parts = line.split() + if len(line_parts) > 0 and line_parts[0] == "WARNING:": + warnings_.append(" ".join(line_parts[1:])) + return warnings_ + + @staticmethod + def _get_all_info_lines(lines: list[str]) -> list[str]: + """Get all informational lines. + + Args: + lines (list[str]): Lines of the `lobsterout` file. + + Returns: + list[str]: List of informational messages. + """ + infos = [] + for line in lines: + line_parts = line.split() + if len(line_parts) > 0 and line_parts[0] == "INFO:": + infos.append(" ".join(line_parts[1:])) + return infos + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for `LobsterOut`. + + Returns: + str: The default filename. + """ + return "lobsterout" diff --git a/future/outputs/misc.py b/future/outputs/misc.py new file mode 100644 index 0000000000..0979c4f7ce --- /dev/null +++ b/future/outputs/misc.py @@ -0,0 +1,670 @@ +from __future__ import annotations + +import itertools +import re +from itertools import islice +from typing import TYPE_CHECKING, Any, ClassVar, Self + +import numpy as np + +from pymatgen.electronic_structure.core import Orbital, Spin +from pymatgen.io.lobster.future.constants import LOBSTER_ORBITALS +from pymatgen.io.lobster.future.core import LobsterFile +from pymatgen.io.lobster.future.utils import parse_orbital_from_text +from pymatgen.io.lobster.future.versioning import version_processor +from pymatgen.io.vasp.outputs import VolumetricData + +if TYPE_CHECKING: + from typing import Literal + + from numpy import floating + + from pymatgen.core.structure import Structure + from pymatgen.io.lobster.future.types import LobsterMatrixData + from pymatgen.util.typing import PathLike + + +class Wavefunction(LobsterFile): + """Parser for wave function files from LOBSTER. + + Reads wave function files and creates VolumetricData objects. + + Attributes: + grid (tuple[int, int, int]): Grid for the wave function [Nx+1, Ny+1, Nz+1]. + points (list[tuple[float, float, float]]): Points in real space. + reals (list[float]): Real parts of the wave function. + imaginaries (list[float]): Imaginary parts of the wave function. + distances (list[float]): Distances to the first point in the wave function file. + structure (Structure): Structure object associated with the calculation. + """ + + def __init__( + self, + filename: PathLike, + structure: Structure, + process_immediately: bool = True, + lobster_version: str | None = None, + ) -> None: + """Initialize the Wavefunction parser. + + Args: + filename (PathLike): The wavecar file from LOBSTER. + structure (Structure): The Structure object. + process_immediately (bool): Whether to parse the file immediately. Defaults to True. + """ + super().__init__( + filename, + process_immediately=process_immediately, + lobster_version=lobster_version, + ) + + self.structure = structure + + @version_processor() + def parse_file( + self, + ) -> None: + """Parse wave function file. + + Reads the wave function file and extracts grid, points, real and imaginary parts, + and distances. + + Raises: + ValueError: If the number of real or imaginary parts does not match the expected grid size. + """ + lines_generator = self.iterate_lines() + + line_parts = next(lines_generator).split() + + self.grid: tuple[int, int, int] = [ + int(line_parts[7]), + int(line_parts[8]), + int(line_parts[9]), + ] + n_points = self.grid[0] * self.grid[1] * self.grid[2] + + self.points = np.empty((n_points, 3), dtype=np.float64) + self.distances = np.empty(n_points, dtype=np.float64) + self.reals = np.empty(n_points, dtype=np.float64) + self.imaginaries = np.empty(n_points, dtype=np.float64) + + i = 0 + for line in lines_generator: + line_parts = line.split() + + if len(line_parts) >= 6: + self.points[i] = ( + float(line_parts[0]), + float(line_parts[1]), + float(line_parts[2]), + ) + self.distances[i] = float(line_parts[3]) + self.reals[i] = float(line_parts[4]) + self.imaginaries[i] = float(line_parts[5]) + i += 1 + + if ( + len(self.reals) != self.grid[0] * self.grid[1] * self.grid[2] + or len(self.imaginaries) != self.grid[0] * self.grid[1] * self.grid[2] + ): + raise ValueError("Something went wrong while reading the file") + + def set_volumetric_data(self, grid: tuple[int, int, int], structure: Structure) -> None: + """Create VolumetricData instances for real, imaginary, and density parts. + + Args: + grid (tuple[int, int, int]): Grid on which wavefunction was calculated. + structure (Structure): Structure object. + + Raises: + ValueError: If the wavefunction file does not contain all relevant points. + """ + Nx = grid[0] - 1 + Ny = grid[1] - 1 + Nz = grid[2] - 1 + a = structure.lattice.matrix[0] + b = structure.lattice.matrix[1] + c = structure.lattice.matrix[2] + new_x = [] + new_y = [] + new_z = [] + new_real = [] + new_imaginary = [] + new_density = [] + + for runner, (x, y, z) in enumerate(itertools.product(range(Nx + 1), range(Ny + 1), range(Nz + 1))): + x_here = x / float(Nx) * a[0] + y / float(Ny) * b[0] + z / float(Nz) * c[0] + y_here = x / float(Nx) * a[1] + y / float(Ny) * b[1] + z / float(Nz) * c[1] + z_here = x / float(Nx) * a[2] + y / float(Ny) * b[2] + z / float(Nz) * c[2] + + if x != Nx and y != Ny and z != Nz: + if ( + not np.isclose(self.points[runner][0], x_here, 1e-3) + and not np.isclose(self.points[runner][1], y_here, 1e-3) + and not np.isclose(self.points[runner][2], z_here, 1e-3) + ): + raise ValueError( + "The provided wavefunction from Lobster does not contain all relevant" + " points. " + "Please use a line similar to: printLCAORealSpaceWavefunction kpoint 1 " + "coordinates 0.0 0.0 0.0 coordinates 1.0 1.0 1.0 box bandlist 1 " + ) + + new_x.append(x_here) + new_y.append(y_here) + new_z.append(z_here) + + new_real.append(self.reals[runner]) + new_imaginary.append(self.imaginaries[runner]) + new_density.append(self.reals[runner] ** 2 + self.imaginaries[runner] ** 2) + + self.final_real = np.reshape(new_real, [Nx, Ny, Nz]) + self.final_imaginary = np.reshape(new_imaginary, [Nx, Ny, Nz]) + self.final_density = np.reshape(new_density, [Nx, Ny, Nz]) + + self.volumetricdata_real = VolumetricData(structure, {"total": self.final_real}) + self.volumetricdata_imaginary = VolumetricData(structure, {"total": self.final_imaginary}) + self.volumetricdata_density = VolumetricData(structure, {"total": self.final_density}) + + def get_volumetricdata_real(self) -> VolumetricData: + """Get VolumetricData object for the real part of the wave function. + + Returns: + VolumetricData: Real part volumetric data. + """ + if not hasattr(self, "volumetricdata_real"): + self.set_volumetric_data(self.grid, self.structure) + return self.volumetricdata_real + + def get_volumetricdata_imaginary(self) -> VolumetricData: + """Get VolumetricData object for the imaginary part of the wave function. + + Returns: + VolumetricData: Imaginary part volumetric data. + """ + if not hasattr(self, "volumetricdata_imaginary"): + self.set_volumetric_data(self.grid, self.structure) + return self.volumetricdata_imaginary + + def get_volumetricdata_density(self) -> VolumetricData: + """Get VolumetricData object for the density part of the wave function. + + Returns: + VolumetricData: Density volumetric data. + """ + if not hasattr(self, "volumetricdata_density"): + self.set_volumetric_data(self.grid, self.structure) + return self.volumetricdata_density + + def write_file( + self, + filename: PathLike = "WAVECAR.vasp", + part: Literal["real", "imaginary", "density"] = "real", + ) -> None: + """Save the wave function in a file readable by VESTA. + + Args: + filename (PathLike): Output file name. Defaults to "WAVECAR.vasp". + part (Literal["real", "imaginary", "density"]): Which part to save. Defaults to "real". + + Raises: + ValueError: If the specified part is not "real", "imaginary", or "density". + """ + if not ( + hasattr(self, "volumetricdata_real") + and hasattr(self, "volumetricdata_imaginary") + and hasattr(self, "volumetricdata_density") + ): + self.set_volumetric_data(self.grid, self.structure) + + if part == "real": + self.volumetricdata_real.write_file(filename) + elif part == "imaginary": + self.volumetricdata_imaginary.write_file(filename) + elif part == "density": + self.volumetricdata_density.write_file(filename) + else: + raise ValueError('part can be only "real" or "imaginary" or "density"') + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> Self: + """""" + instance = super().from_dict(d) + + instance.points = np.asarray(instance.points, dtype=np.float64) + instance.distances = np.asarray(instance.distances, dtype=np.float64) + instance.reals = np.asarray(instance.reals, dtype=np.float64) + instance.imaginaries = np.asarray(instance.imaginaries, dtype=np.float64) + + return instance + + +class MadelungEnergies(LobsterFile): + """Parser for MadelungEnergies.lobster files. + + Attributes: + madelung_energies_mulliken (float): Madelung energy (Mulliken). + madelung_energies_loewdin (float): Madelung energy (Loewdin). + ewald_splitting (float): Ewald splitting parameter. + """ + + @version_processor() + def parse_file(self) -> None: + """Parse MadelungEnergies.lobster file. + + Extracts the Ewald splitting parameter and Madelung energies. + + Returns: + None + """ + line = self.lines[5] + + line_parts = line.split() + + self.ewald_splitting = float(line_parts[0]) + self.madelung_energies_mulliken = float(line_parts[1]) + self.madelung_energies_loewdin = float(line_parts[2]) + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for MadelungEnergies. + + Returns: + str: Default filename. + """ + return "MadelungEnergies.lobster" + + +class SitePotentials(LobsterFile): + """Parser for SitePotentials.lobster files. + + Attributes: + centers (list[str]): Atom centers. + site_potentials_mulliken (list[float]): Mulliken site potentials. + site_potentials_loewdin (list[float]): Loewdin site potentials. + madelung_energies_mulliken (float): Madelung energy (Mulliken). + madelung_energies_loewdin (float): Madelung energy (Loewdin). + ewald_splitting (float): Ewald splitting parameter. + """ + + @version_processor() + def parse_file(self) -> None: + """Parse SitePotentials.lobster file. + + Extracts site potentials, Madelung energies, and Ewald splitting parameter. + + Returns: + None + """ + self.centers = [] + self.site_potentials_mulliken = [] + self.site_potentials_loewdin = [] + + for line in self.iterate_lines(): + if ewald_splitting := re.search(r"splitting parameter\s+(\S+)", line): + self.ewald_splitting = float(ewald_splitting.group(1)) + + if madelung_energies := re.search(r"Madelung Energy \(eV\)\s*(\S+)\s+(\S+)", line): + self.madelung_energies_mulliken = float(madelung_energies.group(1)) + self.madelung_energies_loewdin = float(madelung_energies.group(2)) + + if data := re.search(r"(\d+)\s+([a-zA-Z]{1,2})\s+(\S+)\s+(\S+)", line): + data = data.groups() + self.centers.append(data[1] + data[0]) + self.site_potentials_mulliken.append(float(data[2])) + self.site_potentials_loewdin.append(float(data[3])) + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for SitePotentials. + + Returns: + str: Default filename. + """ + return "SitePotentials.lobster" + + +def get_orb_from_str(orbs: list[str]) -> tuple[str, list[tuple[int, Orbital]]]: + """Get Orbitals from string representations. + + Args: + orbs (list[str]): Orbitals, e.g. ["2p_x", "3s"]. + + Returns: + tuple[str, list[tuple[int, Orbital]]]: Orbital label and list of orbitals. + """ + orbitals = [(int(orb[0]), Orbital(LOBSTER_ORBITALS.index(orb[1:]))) for orb in orbs] + + orb_label = "" + for iorb, orbital in enumerate(orbitals): + if iorb == 0: + orb_label += f"{orbital[0]}{orbital[1].name}" + else: + orb_label += f"-{orbital[0]}{orbital[1].name}" + + return orb_label, orbitals + + +class LobsterMatrices(LobsterFile): + """Parser for LOBSTER matrix files. + + Attributes: + matrix_type (str): Type of matrix (hamilton, coefficient, transfer, overlap). + centers (list[str]): Atom centers. + orbitals (list[str]): Orbitals. + matrices (LobsterMatrixData): Matrix data for each k-point and spin. + efermi (float): Fermi energy (for Hamilton matrices). + """ + + matrix_types: ClassVar[set[str]] = { + "hamilton", + "coefficient", + "transfer", + "overlap", + } + + def __init__( + self, + filename: PathLike | None = None, + matrix_type: str | None = None, + efermi: float | None = None, + process_immediately: bool = True, + lobster_version: str | None = None, + ) -> None: + """Initialize LOBSTER matrices parser. + + Args: + filename: Path to the matrix file + matrix_type: Type of matrix. If None, inferred from filename + efermi: Fermi level in eV (required for Hamilton matrices) + process_immediately: Whether to parse the file immediately + """ + super().__init__( + filename=filename, + process_immediately=False, + lobster_version=lobster_version, + ) + + self.efermi = efermi + + self.matrix_type = matrix_type or self.get_matrix_type() + + self.centers: list[str] = [] + self.orbitals: list[str] = [] + self.matrices: LobsterMatrixData = {} + + if self.matrix_type == "hamilton" and self.efermi is None: + raise ValueError("Fermi energy (eV) required for Hamilton matrices") + + if process_immediately: + self.parse_file() + + def get_matrix_type(self) -> str: + """Infer matrix type from filename. + + Returns: + str: Matrix type. + """ + name = str(self.filename).lower() + + for matrix_type in self.matrix_types: + if matrix_type in name: + return matrix_type + + raise ValueError(f"Cannot infer matrix type from filename: {self.filename}") + + @version_processor() + def parse_file(self) -> None: + """Parse matrix data and set instance attributes. + + Returns: + None + """ + header_regex_pattern = r"kpoint\s+(\d+)" if self.matrix_type == "overlap" else r"(\d+)\s+kpoint\s+(\d+)" + + current_kpoint, current_spin = None, None + multiplier = 1 + + lines_generator = self.iterate_lines() + for line in lines_generator: + if header_match := re.search(header_regex_pattern, line): + header_match = header_match.groups() + if self.matrix_type != "overlap": + current_spin = Spin.up if header_match[0] == "1" else Spin.down + + current_kpoint = header_match[-1] + elif "real parts" in line.lower(): + multiplier = 1 + elif "imag parts" in line.lower(): + multiplier = 1j + elif line.startswith("basisfunction"): + num_parts = len(re.findall(r"band\s+\d+", line)) if "band" in line else len(line.split()[1:]) + + if current_kpoint not in self.matrices: + if current_kpoint is None: + raise ValueError("Could not read any k-point before matrix data.") + + self.matrices[current_kpoint] = {current_spin: np.zeros((num_parts, num_parts), dtype=complex)} + elif current_spin not in self.matrices[current_kpoint]: + self.matrices[current_kpoint][current_spin] = np.zeros((num_parts, num_parts), dtype=complex) + + values = [] + for _ in range(num_parts): + line_split = next(lines_generator).split() + + values.append([float(val) * multiplier for val in line_split[1:]]) + + if len(self.centers) != num_parts and len(self.orbitals) != num_parts: + self.centers.append(line_split[0].split("_")[0].title()) + orbital = parse_orbital_from_text(line_split[0]) + + if orbital is None: + raise ValueError( + f"Could not read orbital format: {line_split[0]} when parsing header line: {line}" + ) + + self.orbitals.append(orbital) + + self.matrices[current_kpoint][current_spin] += np.array(values, dtype=complex) + + def get_onsite_values(self, center: str | None = None, orbital: str | None = None) -> dict | float | floating: + """Get onsite values for specific centers/orbitals. + + Args: + center (str | None): Specific center or None for all. + orbital (str | None): Specific orbital or None for all. + + Returns: + dict | float | floating: Dict of values or single value if both specified. + """ + results = {} + + energy_shift = self.efermi if self.matrix_type == "hamilton" else 0 + + for i, (c, o) in enumerate(zip(self.centers, self.orbitals, strict=True)): + if (center is None or c == center) and (orbital is None or o == orbital): + values = [m[i, i].real - energy_shift for kpoint in self.matrices.values() for m in kpoint.values()] + avg_value = np.mean(values) + + if center and orbital: + return avg_value + + results[f"{c}_{o}"] = avg_value + + return results + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for the LobsterMatrices class. + + Returns: + str: Default filename. + """ + return "hamiltonMatrices.lobster" + + def as_dict(self) -> dict[str, Any]: + """Serialize object to a dictionary. + + Returns: + dict[str, Any]: Dictionary representation of the object. + """ + dictionary = super().as_dict() + + for kpoint in dictionary["matrices"]: + for spin in dictionary["matrices"][kpoint]: + matrix_data = dictionary["matrices"][kpoint][spin] + dictionary["matrices"][kpoint][spin] = { + "real": matrix_data.real, + "imag": matrix_data.imag, + } + + return dictionary + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> Self: + """Deserialize from dictionary.""" + instance = super().from_dict(d) + + for kpoint in instance.matrices: + for spin in instance.matrices[kpoint]: + matrix_data = instance.matrices[kpoint][spin] + instance.matrices[kpoint][spin] = np.asarray(matrix_data["real"]) + 1j * np.asarray(matrix_data["imag"]) + + return instance + + +class POLARIZATION(LobsterFile): + """Parser for POLARIZATION.lobster file. + + Attributes: + rel_mulliken_pol_vector (dict[str, float]): Relative Mulliken polarization vector. + rel_loewdin_pol_vector (dict[str, float]): Relative Loewdin polarization vector. + """ + + @version_processor() + def parse_file(self) -> None: + """Parse POLARIZATION.lobster file. + + Returns: + None + """ + self.rel_mulliken_pol_vector = {} + self.rel_loewdin_pol_vector = {} + + for line in islice(self.iterate_lines(), 3, None): + cleanlines = [idx for idx in line.split(" ") if idx != ""] + if cleanlines and len(cleanlines) == 3: + self.rel_mulliken_pol_vector[cleanlines[0]] = float(cleanlines[1]) + self.rel_loewdin_pol_vector[cleanlines[0]] = float(cleanlines[2]) + if cleanlines and len(cleanlines) == 4: + self.rel_mulliken_pol_vector[cleanlines[0].replace(":", "")] = cleanlines[1].replace("\u03bc", "u") + self.rel_loewdin_pol_vector[cleanlines[2].replace(":", "")] = cleanlines[3].replace("\u03bc", "u") + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for the Polarization class. + + Returns: + str: Default filename. + """ + return "POLARIZATION.lobster" + + +class BWDF(LobsterFile): + """Parser for BWDF.lobster/BWDFCOHP.lobster files. + + Attributes: + centers (NDArray): Bond length centers for the distribution. + bwdf (dict[Literal[1, -1], NDArray]): Bond weighted distribution function. + bin_width (float): Bin width used for computing the distribution by LOBSTER. + """ + + is_cohp: ClassVar[bool] = False + + def __init__( + self, + filename: PathLike | None = None, + process_immediately: bool = True, + lobster_version: str | None = None, + ) -> None: + """ + Args: + filename (PathLike): The BWDF file from LOBSTER, typically "BWDF.lobster" + or "BWDFCOHP.lobster". + """ + self.bwdf = {} + self.centers = np.array([]) + self.data = np.array([[]]) + + super().__init__( + filename=filename, + process_immediately=process_immediately, + lobster_version=lobster_version, + ) + + @version_processor() + def parse_file(self) -> None: + """Parse BWDF.lobster/BWDFCOHP.lobster file. + + Returns: + None + """ + self.bwdf = {} + self.data = np.genfromtxt(self.iterate_lines(), dtype=float, skip_header=1) + + self.process_data_into_bwdf_centers() + + def process_data_into_bwdf_centers(self) -> None: + """Process data into bwdf and centers. + + Returns: + None + """ + self.centers = self.data[:, 0] + self.bwdf[Spin.up] = self.data[:, 1] + + if self.data.shape[1] > 2: + self.bwdf[Spin.down] = self.data[:, 2] + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for the BWDF class. + + Returns: + str: Default filename. + """ + return "BWDF.lobster" + + @classmethod + def from_dict(cls, d: dict[str, Any]) -> Self: + """Deserialize object from dictionary produced by `as_dict`. + + Args: + d (dict[str, Any]): Dictionary representation of the object. + + Returns: + Self: Deserialized BWDF object. + """ + instance = super().from_dict(d) + + instance.data = np.asarray(instance.data, dtype=np.float64) + instance.process_data_into_bwdf_centers() + + return instance + + +class BWDFCOHP(BWDF): + """Parser for BWDFCOHP.lobster files. + + Returns: + None + """ + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for the BWDFCOHP class. + + Returns: + str: Default filename. + """ + return "BWDFCOHP.lobster" diff --git a/future/outputs/populations.py b/future/outputs/populations.py new file mode 100644 index 0000000000..16610a1423 --- /dev/null +++ b/future/outputs/populations.py @@ -0,0 +1,157 @@ +"""Classes for parsing LOBSTER population analysis output files. + +Provides classes to read and parse population analysis files generated by LOBSTER, +such as CHARGE.lobster, CHARGE.LCFO.lobster, GROSSPOP.lobster, and GROSSPOP.LCFO.lobster. +Extracts Mulliken and Loewdin charges and gross populations for further analysis. +""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from pymatgen.electronic_structure.core import Spin +from pymatgen.io.lobster.future.core import LobsterFile +from pymatgen.io.lobster.future.versioning import version_processor + +if TYPE_CHECKING: + from typing import ClassVar + + from pymatgen.io.lobster.future.types import LobsterPopulations + + +class CHARGE(LobsterFile): + """Parser for CHARGE.lobster and CHARGE.LCFO.lobster files. + + Attributes: + is_lcfo (bool): Whether the CHARGE file is from LCFO analysis. + centers (list[str]): Atom labels with indices. + mulliken (list[float]): Mulliken charges for each atom. + loewdin (list[float]): Loewdin charges for each atom. + """ + + charge_regex: ClassVar[str] = r"(\d+)\s+([a-zA-Z]+)\s+(\S+)\s+(\S+)" + is_lcfo: ClassVar[bool] = False + + @version_processor() + def parse_file(self) -> None: + """Parse the CHARGE file and extract Mulliken and Loewdin charges. + + Populates the `centers`, `mulliken`, and `loewdin` attributes with data + extracted from the file. + + Returns: + None + """ + self.centers = [] + self.mulliken = [] + self.loewdin = [] + + for line in self.iterate_lines(): + if data := re.search(self.charge_regex, line): + data = data.groups() + self.centers.append(data[1] + data[0]) + + if self.is_lcfo: + self.loewdin.append(float(data[2])) + else: + self.mulliken.append(float(data[2])) + self.loewdin.append(float(data[3])) + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for CHARGE files. + + Returns: + str: Default filename for the CHARGE file, depending on LCFO mode. + """ + return "CHARGE.lobster" if not cls.is_lcfo else "CHARGE.LCFO.lobster" + + +class CHARGE_LCFO(CHARGE): + """Parser for CHARGE.LCFO.lobster files. + + Attributes: + is_lcfo (bool): Whether the CHARGE file is from LCFO analysis. + """ + + charge_regex: ClassVar[str] = r"(\d+)\s+([a-zA-Z]+)\s+(\S+)" + is_lcfo: ClassVar[bool] = True + + +class GROSSPOP(LobsterFile): + """Parser for GROSSPOP.lobster and GROSSPOP.LCFO.lobster files. + + Attributes: + is_lcfo (bool): Whether the GROSSPOP file is from LCFO analysis. + populations (LobsterPopulations): Population dictionaries for each atom. + spins (list[Spin]): List of spins present in the file. + """ + + is_lcfo: ClassVar[bool] = False + + @version_processor() + def parse_file(self) -> None: + """Parse the GROSSPOP file and extract gross population data. + + Populates the `populations` attribute with a nested dictionary structure + containing Mulliken and Loewdin populations for each atom and orbital. + """ + self.populations: LobsterPopulations = {} + self.spins = [Spin.up] + + keys = [] + + current_atom = "" + for line in self.iterate_lines(): + mulliken_key = re.findall(r"(?i)Mulliken GP", line) + loewdin_key = re.findall(r"(?i)Loewdin GP", line) + + if mulliken_key: + keys.append("mulliken") + if loewdin_key: + keys.append("loewdin") + + if len(loewdin_key) > 1 or len(mulliken_key) > 1: + self.spins.append(Spin.down) + + if "total" in line.lower(): + pass + elif data := re.search( + r"(?:(\d+)\s+)?(?:([a-zA-Z]+)\s+)?(\S+)\s+([0-9\.]+)\s+([0-9\.]+)(?:\s+)?([0-9\.]+)?(?:\s+)?([0-9\.]+)?", + line, + ): + groups = data.groups() + populations = {} + if groups[0] is not None and groups[1] is not None: + current_atom = groups[1] + groups[0] + self.populations[current_atom] = {} + if groups[2] is not None: + self.populations[current_atom][groups[2]] = populations + else: + continue + + for i, spin in enumerate(self.spins): + populations[spin] = {} + for j, key in enumerate(keys): + populations[spin][key] = float(groups[3 + i + j * len(self.spins)]) + + @classmethod + def get_default_filename(cls) -> str: + """Get the default filename for GROSSPOP files. + + Returns: + str: Default filename for the GROSSPOP file, depending on LCFO mode. + """ + return "GROSSPOP.lobster" if not cls.is_lcfo else "GROSSPOP.LCFO.lobster" + + +class GROSSPOP_LCFO(GROSSPOP): + """Parser for GROSSPOP.LCFO.lobster files. + + Attributes: + is_lcfo (bool): Whether the GROSSPOP file is in LCFO format. + populations (LobsterPopulations): Population dictionaries for each atom. + """ + + is_lcfo: ClassVar[bool] = True diff --git a/future/types.py b/future/types.py new file mode 100644 index 0000000000..65fd89cac1 --- /dev/null +++ b/future/types.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal, TypedDict + +from numpy import complexfloating +from numpy.typing import NDArray + +from pymatgen.electronic_structure.core import Spin + +if TYPE_CHECKING: + from typing import TypeAlias + + from numpy import floating, integer + + +class LobsterInteraction(TypedDict): + """Dictionary representing a chemical interaction in LOBSTER. + + This dictionary stores information about a specific interaction between atoms in a structure. + + Attributes: + index (int): The index of the interaction. + centers (list[str]): List of strings representing the centers of the interaction (e.g., "Fe1", "O2"). + cells (list[list[int]] | NDArray[integer]): List of lists of integers representing the cells of the interaction + (e.g., [0, 0, 0]). + orbitals (list[str | None]): List of strings representing the orbitals involved in the interaction + (e.g., "2s", "2p_x"). + length (float | None): The length of the interaction, representing the distance between the centers. + """ + + index: int + centers: list[str] + cells: list[list[int]] | NDArray[integer] + orbitals: list[str | None] + length: float | None + + +class LobsterInteractionData(LobsterInteraction, total=False): + """Dictionary representing a chemical interaction in LOBSTER with additional COXX/ICOXX data. + + Extends `LobsterInteraction` by adding COXX and ICOXX values for each spin. + + Attributes: + coxx (dict[Spin, NDArray[floating] | float]): COXX values for each spin. + icoxx (dict[Spin, NDArray[floating] | float]): ICOXX values for each spin. + """ + + coxx: dict[Spin, NDArray[floating] | float] + icoxx: dict[Spin, NDArray[floating] | float] + + +LobsterPopulations: TypeAlias = dict[str, dict[str, dict[Spin, dict[Literal["mulliken", "loewdin"], float]]]] + +LobsterMatrixData: TypeAlias = dict[str, dict[Spin | None, NDArray[complexfloating]]] + + +class LobsterBandOverlaps(TypedDict): + """Dictionary representing band overlaps in LOBSTER. + + Attributes: + k_points (dict[Spin, list[list[float]]]): List of k-points for each spin. + matrices (dict[Spin, list[NDArray[floating]]]): List of matrices for each spin. + max_deviations (dict[Spin, list[float]]): List of maximal deviations for each spin. + """ + + k_points: dict[Spin, list[list[float]]] + matrices: dict[Spin, list[NDArray[floating]]] + max_deviations: dict[Spin, list[float]] + + +class LobsterFatband(TypedDict): + """Dictionary representing fatband data in LOBSTER. + + Attributes: + center (str): Atom associated with the fatband. + orbital (str): Orbital associated with the fatband. + energies (dict[Spin, NDArray[floating]]): Energies at each k-point for each spin. + projections (dict[Spin, NDArray[floating]]): Weights/projections at each k-point for each spin. + """ + + center: str + orbital: str + energies: dict[Spin, NDArray[floating]] + projections: dict[Spin, NDArray[floating]] + + +class LobsterFatbands(TypedDict): + """Dictionary representing multiple fatbands in LOBSTER. + + Attributes: + k_points (list[list[float]]): List of k-points. + bands (list[LobsterFatband]): List of fatband dictionaries. + """ + + k_points: list[list[float]] + bands: list[LobsterFatband] diff --git a/future/utils.py b/future/utils.py new file mode 100644 index 0000000000..42e19bed67 --- /dev/null +++ b/future/utils.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import re +from typing import Any + +from pymatgen.electronic_structure.core import Spin +from pymatgen.io.lobster.future.constants import LOBSTER_ORBITALS + + +def natural_sort(list_to_sort: str) -> list[Any]: + """Sort a list of strings in human order. + + This function sorts strings in a way that humans would expect, + taking into account numerical values within the strings. + + Args: + list_to_sort (str): List of strings to sort. + + Returns: + list[Any]: Sorted list of strings in human order. + + Example: + >>> natural_sort(["file10", "file2", "file1"]) + ['file1', 'file2', 'file10'] + """ + return [int(text) if text.isdigit() else text.lower() for text in re.split(r"(\d+)", list_to_sort)] + + +def parse_orbital_from_text(text: str) -> str | None: + """Parse the orbital from a text string of an ICOXXLIST file. + + This function extracts the orbital information from a given text string. + It checks for valid orbital patterns and returns the matched orbital. + + Args: + text (str): Text string to parse the orbital from. + + Returns: + str | None: Parsed orbital string if a valid orbital is found, + otherwise None. + + Example: + >>> parse_orbital_from_text("1s_2p_x") + '2p_x' + """ + parts = text.split("_") + + if len(parts) == 1: + return None + + for orbital in LOBSTER_ORBITALS: + if match := re.search(rf"\d+{re.escape(orbital)}", "_".join(parts[-2:])): + return match.group(0) + + return parts[-1] if re.match(r"\d+[a-z]+", parts[-1]) else None + + +def convert_spin_keys(obj: Any) -> Any: + """Convert an object to a JSON-compatible format recursively. + + This function ensures that the input object is converted into a format + that can be serialized into JSON. It handles lists, tuples, dictionaries, + and enums. + + Args: + obj (Any): Input object to convert. + + Returns: + Any: JSON-compatible representation of the input object. + + Example: + >>> make_json_compatible({"key": Enum("Example", "value")}) + {'key': 'value'} + """ + if isinstance(obj, (list, tuple)): + return [convert_spin_keys(item) for item in obj] + + if isinstance(obj, dict): + new_dict = {} + + for k, v in obj.items(): + new_key = f"@Spin({k})" if isinstance(k, Spin) else k + new_dict[new_key] = convert_spin_keys(v) + + return new_dict + + return obj + + +def restore_spin_keys(obj: Any) -> Any: + """Restore Spin enum keys from JSON-serialized format. + + This function recursively processes an object and converts string keys + "1" and "-1" back to Spin.up and Spin.down enum values. It handles + nested lists, tuples, and dictionaries. + + Args: + obj (Any): Input object with potential Spin keys as strings. + + Returns: + Any: Object with Spin enum keys restored. + + Example: + >>> restore_spin_keys({"1": [1, 2], "-1": [3, 4]}) + {: [1, 2], : [3, 4]} + """ + if isinstance(obj, (list, tuple)): + return type(obj)(restore_spin_keys(item) for item in obj) + + if isinstance(obj, dict): + new_dict = {} + for k, v in obj.items(): + if k == "@Spin(1)": + new_key = Spin.up + elif k == "@Spin(-1)": + new_key = Spin.down + else: + new_key = k + + new_dict[new_key] = restore_spin_keys(v) + + return new_dict + + return obj diff --git a/future/versioning.py b/future/versioning.py new file mode 100644 index 0000000000..111cc07657 --- /dev/null +++ b/future/versioning.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from collections.abc import Callable +from typing import TypeVar + +F = TypeVar("F", bound=Callable) + + +def version_processor(min_version: str = "0.0", max_version: str | None = None) -> Callable[[F], F]: + """Decorator to mark a method as a version processor. + + Args: + min_version (str): Minimum version for which the processor is valid. + max_version (str | None): Maximum version for which the processor is valid. + + Returns: + Callable[[F], F]: Decorator for versioned processor methods. + """ + + def decorator(func: F) -> F: + setattr(func, "version_info", (min_version, max_version)) # NOQA: B010 + + return func + + return decorator From 676fa3bcffadea420e72a1c9861a1e3c29592f3f Mon Sep 17 00:00:00 2001 From: Tom Demeyere <115232841+tomdemeyere@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:43:20 +0100 Subject: [PATCH 173/180] refactor(lobster): Rewrite LOBSTER parsers with memory-efficient streaming architecture (#4592) * moving to the future * simpler serialization, spin keys taken care of * final commit? * no `Self` for python 3.10 * changes allowing safe jsanazite * for now * final? * pre-commit auto-fixes * LOBSTER_OBJECTS constant * pre-commit auto-fixes --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Shyue Ping Ong --- tests/io/lobster/future/__init__.py | 0 tests/io/lobster/future/test_inputs.py | 639 ++++++++ tests/io/lobster/future/test_outputs.py | 1917 +++++++++++++++++++++++ 3 files changed, 2556 insertions(+) create mode 100644 tests/io/lobster/future/__init__.py create mode 100644 tests/io/lobster/future/test_inputs.py create mode 100644 tests/io/lobster/future/test_outputs.py diff --git a/tests/io/lobster/future/__init__.py b/tests/io/lobster/future/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/io/lobster/future/test_inputs.py b/tests/io/lobster/future/test_inputs.py new file mode 100644 index 0000000000..1be64369e2 --- /dev/null +++ b/tests/io/lobster/future/test_inputs.py @@ -0,0 +1,639 @@ +from __future__ import annotations + +import numpy as np +import pytest +from pytest import approx + +from pymatgen.core.structure import Structure +from pymatgen.io.lobster.future import LobsterIn +from pymatgen.io.lobster.future.inputs import get_all_possible_basis_combinations +from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar +from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, MatSciTest + +TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp" + +__author__ = "Janine George, Marco Esters" +__copyright__ = "Copyright 2017, The Materials Project" +__version__ = "0.2" +__email__ = "janine.george@uclouvain.be, esters@uoregon.edu" +__date__ = "Dec 10, 2017" + + +class TestLobsterIn(MatSciTest): + def setup_method(self): + self.LobsterIn = LobsterIn.from_file(f"{TEST_DIR}/lobsterin.1") + self.LobsterIn2 = LobsterIn.from_file(f"{TEST_DIR}/lobsterin.2") + self.LobsterIn3 = LobsterIn.from_file(f"{TEST_DIR}/lobsterin.3") + self.LobsterIn4 = LobsterIn.from_file(f"{TEST_DIR}/lobsterin.4.gz") + + def test_from_file(self): + # Test reading from file + assert self.LobsterIn["cohpstartenergy"] == approx(-15.0) + assert self.LobsterIn["cohpendenergy"] == approx(5.0) + assert self.LobsterIn["basisset"] == "pbeVaspFit2015" + assert self.LobsterIn["gaussiansmearingwidth"] == approx(0.1) + assert self.LobsterIn["basisfunctions"][0] == "Fe 3d 4p 4s" + assert self.LobsterIn["basisfunctions"][1] == "Co 3d 4p 4s" + assert self.LobsterIn["skipdos"] + assert self.LobsterIn["skipcohp"] + assert self.LobsterIn["skipcoop"] + assert self.LobsterIn["skippopulationanalysis"] + assert self.LobsterIn["skipgrosspopulation"] + + # Test if comments are correctly removed + assert self.LobsterIn == self.LobsterIn2 + + def test_duplicates_from_file(self): + with open(f"{TEST_DIR}/lobsterin.1", encoding="utf-8") as file: + original_file = file.readlines() + + # String and float keywords does not allow duplicates + float_dup_file = original_file.copy() + float_dup_file.append("cohpstartenergy -15.0") + + float_tmp_file = self.tmp_path / "tmp_lobster_in_float" + float_tmp_file.write_text("\n".join(float_dup_file)) + + with pytest.raises(ValueError, match="Same keyword cohpstartenergy twice!"): + _ = LobsterIn.from_file(float_tmp_file) + + # Boolean and list keywords allow duplicates + bool_dup_file = original_file.copy() + bool_dup_file.append("skipdos") + + bool_tmp_file = self.tmp_path / "tmp_lobster_in_bool" + bool_tmp_file.write_text("\n".join(bool_dup_file)) + + _ = LobsterIn.from_file(bool_tmp_file) # no error should be raised + + def test_magic_methods(self): + """Test __getitem__, __setitem__ and __contains__, + should be case independent. + """ + # Test __setitem__ + assert self.LobsterIn["COHPSTARTENERGY"] == approx(-15.0) + + with pytest.raises(KeyError, match="Key hello is currently not available"): + self.LobsterIn["HELLO"] = True + + # Test __getitem__ + self.LobsterIn["skipCOHP"] = False + assert self.LobsterIn["skipcohp"] is False + assert self.LobsterIn.get("skipCOHP") is False + + with pytest.raises(KeyError, match="key='World' is not available"): + _ = self.LobsterIn["World"] + + # Test __contains__ + assert "COHPSTARTENERGY" in self.LobsterIn + assert "cohpstartenergy" in self.LobsterIn + + assert "helloworld" not in self.LobsterIn + + def test_initialize_from_dict(self): + # initialize from dict + lobsterin = LobsterIn( + { + "cohpstartenergy": -15.0, + "cohpendenergy": 5.0, + "basisset": "pbeVaspFit2015", + "gaussiansmearingwidth": 0.1, + "basisfunctions": ["Fe 3d 4p 4s", "Co 3d 4p 4s"], + "skipdos": True, + "skipcohp": True, + "skipcoop": True, + "skippopulationanalysis": True, + "skipgrosspopulation": True, + } + ) + assert lobsterin["cohpstartenergy"] == approx(-15.0) + assert lobsterin["cohpendenergy"] == approx(5.0) + assert lobsterin["basisset"] == "pbeVaspFit2015" + assert lobsterin["gaussiansmearingwidth"] == approx(0.1) + assert lobsterin["basisfunctions"][0] == "Fe 3d 4p 4s" + assert lobsterin["basisfunctions"][1] == "Co 3d 4p 4s" + assert {*lobsterin} >= { + "skipdos", + "skipcohp", + "skipcoop", + "skippopulationanalysis", + "skipgrosspopulation", + } + with pytest.raises(KeyError, match="There are duplicates for the keywords!"): + lobsterin2 = LobsterIn({"cohpstartenergy": -15.0, "cohpstartEnergy": -20.0}) + lobsterin2 = LobsterIn({"cohpstartenergy": -15.0}) + # can only calculate nbands if basis functions are provided + with pytest.raises( + ValueError, + match="No basis functions are provided. The program cannot calculate nbands.", + ): + lobsterin2._get_nbands(structure=Structure.from_file(f"{VASP_IN_DIR}/POSCAR_Fe3O4")) + + def test_standard_settings(self): + # test standard settings + for option in [ + "standard", + "standard_from_projection", + "standard_with_fatband", + "onlyprojection", + "onlydos", + "onlycohp", + "onlycoop", + "onlycobi", + "onlycohpcoop", + "onlycohpcoopcobi", + ]: + lobsterin1 = LobsterIn.standard_calculations_from_vasp_files( + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster", + f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz", + option=option, + ) + assert lobsterin1["cohpstartenergy"] == approx(-35.0) + assert lobsterin1["cohpendenergy"] == approx(5.0) + assert lobsterin1["basisset"] == "pbeVaspFit2015" + assert lobsterin1["gaussiansmearingwidth"] == approx(0.1) + assert lobsterin1["basisfunctions"][0] == "Fe 3d 4p 4s " + assert lobsterin1["basisfunctions"][1] == "O 2p 2s " + + if option in [ + "standard", + "standard_with_fatband", + "onlyprojection", + "onlycohp", + "onlycoop", + "onlycohpcoop", + ]: + assert lobsterin1["saveProjectiontoFile"] + if option in [ + "standard", + "standard_with_fatband", + "onlycohp", + "onlycoop", + "onlycohpcoop", + ]: + assert lobsterin1["cohpGenerator"] == "from 0.1 to 6.0 orbitalwise" + if option == "standard": + assert "skipdos" not in lobsterin1 + assert "skipcohp" not in lobsterin1 + assert "skipcoop" not in lobsterin1 + if option == "standard_with_fatband": + assert lobsterin1["createFatband"] == ["Fe 3d 4p 4s ", "O 2p 2s "] + assert "skipdos" not in lobsterin1 + assert "skipcohp" not in lobsterin1 + assert "skipcoop" not in lobsterin1 + if option == "standard_from_projection": + assert lobsterin1["loadProjectionFromFile"] + if option in [ + "onlyprojection", + "onlycohp", + "onlycoop", + "onlycobi", + "onlycohpcoop", + "onlycohpcoopcobi", + ]: + assert lobsterin1["skipdos"] + assert lobsterin1["skipPopulationAnalysis"] + assert lobsterin1["skipGrossPopulation"] + assert lobsterin1["skipMadelungEnergy"] + + if option == "onlydos": + assert lobsterin1["skipPopulationAnalysis"] + assert lobsterin1["skipGrossPopulation"] + assert lobsterin1["skipcohp"] + assert lobsterin1["skipcoop"] + assert lobsterin1["skipcobi"] + assert lobsterin1["skipMadelungEnergy"] + if option == "onlycohp": + assert lobsterin1["skipcoop"] + assert lobsterin1["skipcobi"] + if option == "onlycoop": + assert lobsterin1["skipcohp"] + assert lobsterin1["skipcobi"] + if option == "onlyprojection": + assert lobsterin1["skipdos"] + if option == "onlymadelung": + assert lobsterin1["skipPopulationAnalysis"] + assert lobsterin1["skipGrossPopulation"] + assert lobsterin1["skipcohp"] + assert lobsterin1["skipcoop"] + assert lobsterin1["skipcobi"] + assert lobsterin1["skipdos"] + # test basis functions by dict + lobsterin_new = LobsterIn.standard_calculations_from_vasp_files( + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster", + dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, + option="standard", + ) + assert lobsterin_new["basisfunctions"] == ["Fe 3d 4p 4s", "O 2s 2p"] + + # test gaussian smearing + lobsterin_new = LobsterIn.standard_calculations_from_vasp_files( + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster2", + dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, + option="standard", + ) + assert "gaussiansmearingwidth" not in lobsterin_new + + # fatband and ISMEAR=-5 does not work together + with pytest.raises( + ValueError, + match="ISMEAR has to be 0 for a fatband calculation with Lobster", + ): + lobsterin_new = LobsterIn.standard_calculations_from_vasp_files( + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster2", + dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, + option="standard_with_fatband", + ) + + def test_standard_with_energy_range_from_vasprun(self): + # test standard_with_energy_range_from_vasprun + lobsterin_comp = LobsterIn.standard_calculations_from_vasp_files( + f"{VASP_IN_DIR}/POSCAR_C2", + f"{VASP_IN_DIR}/INCAR_C2", + f"{VASP_IN_DIR}/POTCAR_C2.gz", + f"{VASP_OUT_DIR}/vasprun.C2.xml.gz", + option="standard_with_energy_range_from_vasprun", + ) + assert lobsterin_comp["COHPstartEnergy"] == approx(-28.3679) + assert lobsterin_comp["COHPendEnergy"] == approx(32.8968) + assert lobsterin_comp["COHPSteps"] == 301 + + def test_diff(self): + # test diff + assert self.LobsterIn.diff(self.LobsterIn2)["Different"] == {} + assert self.LobsterIn.diff(self.LobsterIn2)["Same"]["cohpstartenergy"] == approx(-15.0) + + # test diff in both directions + for entry in self.LobsterIn.diff(self.LobsterIn3)["Same"]: + assert entry in self.LobsterIn3.diff(self.LobsterIn)["Same"] + for entry in self.LobsterIn3.diff(self.LobsterIn)["Same"]: + assert entry in self.LobsterIn.diff(self.LobsterIn3)["Same"] + for entry in self.LobsterIn.diff(self.LobsterIn3)["Different"]: + assert entry in self.LobsterIn3.diff(self.LobsterIn)["Different"] + for entry in self.LobsterIn3.diff(self.LobsterIn)["Different"]: + assert entry in self.LobsterIn.diff(self.LobsterIn3)["Different"] + + assert ( + self.LobsterIn.diff(self.LobsterIn3)["Different"]["skipcohp"]["lobsterin1"] + == self.LobsterIn3.diff(self.LobsterIn)["Different"]["skipcohp"]["lobsterin2"] + ) + + def test_diff_case_insensitivity(self): + """Test case-insensitivity of diff method.""" + with open(f"{TEST_DIR}/lobsterin.1", encoding="utf-8") as file: + lobsterin_content = file.read() + + lobsterin_content.replace("COHPstartEnergy -15.0", "cohpSTARTEnergy -15.0") + lobsterin_content.replace("skipcohp", "skipCOHP") + + tmp_file_path = self.tmp_path / "tmp_lobster_in" + tmp_file_path.write_text(lobsterin_content) + + lobsterin_diff_case = LobsterIn.from_file(tmp_file_path) + assert self.LobsterIn.diff(lobsterin_diff_case)["Different"] == {} + + def test_dict_functionality(self): + for key in ("COHPstartEnergy", "COHPstartEnergy", "COhPstartenergy"): + start_energy = self.LobsterIn.get(key) + assert start_energy == approx(-15.0), f"{start_energy=}, {key=}" + + lobsterin_copy = self.LobsterIn.copy() + lobsterin_copy.update({"cohpstarteNergy": -10.00}) + assert lobsterin_copy["cohpstartenergy"] == approx(-10.0) + lobsterin_copy.pop("cohpstarteNergy") + assert "cohpstartenergy" not in lobsterin_copy + lobsterin_copy.pop("cohpendenergY") + lobsterin_copy["cohpsteps"] = 100 + assert lobsterin_copy["cohpsteps"] == 100 + len_before = len(lobsterin_copy.items()) + assert len_before == 9, f"{len_before=}" + + lobsterin_copy.popitem() + len_after = len(lobsterin_copy.items()) + assert len_after == len_before - 1 + + # Test case sensitivity of |= operator + self.LobsterIn |= {"skipCOHP": True} # Camel case + assert self.LobsterIn["skipcohp"] is True + + self.LobsterIn |= {"skipcohp": False} # lower case + assert self.LobsterIn["skipcohp"] is False + + def test_read_write_lobsterin(self): + outfile_path = self.tmp_path / "lobsterin_test" + lobsterin1 = LobsterIn.from_file(f"{TEST_DIR}/lobsterin.1") + lobsterin1.write_lobsterin(outfile_path) + lobsterin2 = LobsterIn.from_file(outfile_path) + assert lobsterin1.diff(lobsterin2)["Different"] == {} + + def test_get_basis(self): + # get basis functions + lobsterin1 = LobsterIn({}) + potcar = Potcar.from_file(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") + potcar_names = [name["symbol"] for name in potcar.spec] + + assert lobsterin1.get_basis( + Structure.from_file(f"{TEST_FILES_DIR}/cif/Fe3O4.cif"), + potcar_symbols=potcar_names, + ) == ["Fe 3d 4p 4s ", "O 2p 2s "] + potcar = Potcar.from_file(f"{TEST_DIR}/POTCAR.GaAs") + potcar_names = [name["symbol"] for name in potcar.spec] + assert lobsterin1.get_basis( + Structure.from_file(f"{TEST_DIR}/POSCAR.GaAs"), + potcar_symbols=potcar_names, + ) == ["Ga 3d 4p 4s ", "As 4p 4s "] + + def test_get_all_possible_basis_functions(self): + potcar = Potcar.from_file(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") + potcar_names = [name["symbol"] for name in potcar.spec] + result = LobsterIn.get_all_possible_basis_functions( + Structure.from_file(f"{TEST_FILES_DIR}/cif/Fe3O4.cif"), + potcar_symbols=potcar_names, + ) + assert result[0] == {"Fe": "3d 4s", "O": "2p 2s"} + assert result[1] == {"Fe": "3d 4s 4p", "O": "2p 2s"} + + potcar2 = Potcar.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Fe.gz") + Potcar_names2 = [name["symbol"] for name in potcar2.spec] + result2 = LobsterIn.get_all_possible_basis_functions( + Structure.from_file(f"{TEST_FILES_DIR}/cif/Fe.cif"), + potcar_symbols=Potcar_names2, + ) + assert result2[0] == {"Fe": "3d 4s"} + + def test_get_potcar_symbols(self): + lobsterin1 = LobsterIn({}) + assert lobsterin1._get_potcar_symbols(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") == [ + "Fe", + "O", + ] + assert lobsterin1._get_potcar_symbols(f"{TEST_DIR}/POTCAR.GaAs") == [ + "Ga_d", + "As", + ] + + def test_write_lobsterin(self): + # write lobsterin, read it and compare it + outfile_path = self.tmp_path / "lobsterin_test" + lobsterin1 = LobsterIn.standard_calculations_from_vasp_files( + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster", + f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz", + option="standard", + ) + lobsterin1.write_lobsterin(outfile_path) + lobsterin2 = LobsterIn.from_file(outfile_path) + assert lobsterin1.diff(lobsterin2)["Different"] == {} + + def test_write_incar(self): + # write INCAR and compare + outfile_path = self.tmp_path / "INCAR_test" + lobsterin1 = LobsterIn.standard_calculations_from_vasp_files( + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + f"{VASP_IN_DIR}/INCAR.lobster", + f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz", + option="standard", + ) + lobsterin1.write_INCAR( + f"{VASP_IN_DIR}/INCAR.lobster3", + outfile_path, + f"{VASP_IN_DIR}/POSCAR_Fe3O4", + isym=-1, + ) + + incar1 = Incar.from_file(f"{VASP_IN_DIR}/INCAR.lobster3") + incar2 = Incar.from_file(outfile_path) + + assert incar1.diff(incar2)["Different"] == { + "ISYM": {"INCAR1": 2, "INCAR2": -1}, + "NBANDS": {"INCAR1": None, "INCAR2": 86}, + "NSW": {"INCAR1": 500, "INCAR2": 0}, + "LWAVE": {"INCAR1": False, "INCAR2": True}, + } + + def test_write_kpoints(self): + # line mode + outfile_path = self.tmp_path / "KPOINTS_test" + outfile_path2 = self.tmp_path / "POSCAR_test" + lobsterin1 = LobsterIn({}) + # test writing primitive cell + lobsterin1.write_POSCAR_with_standard_primitive( + POSCAR_input=f"{VASP_IN_DIR}/POSCAR_Fe3O4", POSCAR_output=outfile_path2 + ) + + lobsterin1.write_KPOINTS( + POSCAR_input=outfile_path2, + KPOINTS_output=outfile_path, + kpoints_line_density=58, + isym=-1, + ) + kpoint = Kpoints.from_file(outfile_path) + assert kpoint.num_kpts == 562 + assert kpoint.kpts[-1][0] == approx(-0.5) + assert kpoint.kpts[-1][1] == approx(0.5) + assert kpoint.kpts[-1][2] == approx(0.5) + assert kpoint.labels[-1] == "T" + kpoint2 = Kpoints.from_file(f"{VASP_IN_DIR}/KPOINTS_band.lobster") + + labels = [] + number = 0 + for label in kpoint.labels: + if label is not None: + if number != 0: + if label != labels[number - 1]: + labels.append(label) + number += 1 + else: + labels.append(label) + number += 1 + + labels2 = [] + number2 = 0 + for label in kpoint2.labels: + if label is not None: + if number2 != 0: + if label != labels2[number2 - 1]: + labels2.append(label) + number2 += 1 + else: + labels2.append(label) + number2 += 1 + assert labels == labels2 + + # without line mode + lobsterin1.write_KPOINTS( + POSCAR_input=outfile_path2, + KPOINTS_output=outfile_path, + line_mode=False, + isym=-1, + ) + kpoint = Kpoints.from_file(outfile_path) + kpoint2 = Kpoints.from_file(f"{VASP_OUT_DIR}/IBZKPT.lobster") + + for num_kpt, list_kpoint in enumerate(kpoint.kpts): + assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) + assert list_kpoint[1] == approx(kpoint2.kpts[num_kpt][1]) + assert list_kpoint[2] == approx(kpoint2.kpts[num_kpt][2]) + + assert kpoint.num_kpts == 108 + + # without line mode, use grid instead of reciprocal density + lobsterin1.write_KPOINTS( + POSCAR_input=outfile_path2, + KPOINTS_output=outfile_path, + line_mode=False, + from_grid=True, + input_grid=[6, 6, 3], + isym=-1, + ) + kpoint = Kpoints.from_file(outfile_path) + kpoint2 = Kpoints.from_file(f"{VASP_OUT_DIR}/IBZKPT.lobster") + + for num_kpt, list_kpoint in enumerate(kpoint.kpts): + assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) + assert list_kpoint[1] == approx(kpoint2.kpts[num_kpt][1]) + assert list_kpoint[2] == approx(kpoint2.kpts[num_kpt][2]) + + assert kpoint.num_kpts == 108 + + # + # #without line mode, using a certain grid, isym=0 instead of -1 + lobsterin1.write_KPOINTS( + POSCAR_input=f"{TEST_DIR}/POSCAR.Li", + KPOINTS_output=outfile_path, + line_mode=False, + from_grid=True, + input_grid=[3, 3, 3], + isym=0, + ) + + kpoint1 = Kpoints.from_file(outfile_path) + kpoint2 = Kpoints.from_file(f"{TEST_DIR}/IBZKPT_3_3_3_Li") + for ikpoint, kpoint in enumerate(kpoint1.kpts): + assert self.is_kpoint_in_list( + kpoint, + kpoint2.kpts, + kpoint1.kpts_weights[ikpoint], + kpoint2.kpts_weights, + ) + for ikpoint, kpoint in enumerate(kpoint2.kpts): + assert self.is_kpoint_in_list( + kpoint, + kpoint1.kpts, + kpoint2.kpts_weights[ikpoint], + kpoint1.kpts_weights, + ) + + lobsterin1.write_KPOINTS( + POSCAR_input=f"{TEST_DIR}/POSCAR.Li", + KPOINTS_output=outfile_path, + line_mode=False, + from_grid=True, + input_grid=[2, 2, 2], + isym=0, + ) + + kpoint1 = Kpoints.from_file(outfile_path) + kpoint2 = Kpoints.from_file(f"{TEST_DIR}/IBZKPT_2_2_2_Li") + for ikpoint, kpoint in enumerate(kpoint1.kpts): + assert self.is_kpoint_in_list( + kpoint, + kpoint2.kpts, + kpoint1.kpts_weights[ikpoint], + kpoint2.kpts_weights, + ) + for ikpoint, kpoint in enumerate(kpoint2.kpts): + assert self.is_kpoint_in_list( + kpoint, + kpoint1.kpts, + kpoint2.kpts_weights[ikpoint], + kpoint1.kpts_weights, + ) + + def is_kpoint_in_list(self, kpoint, kpointlist, weight, weightlist) -> bool: + found = 0 + for ikpoint2, kpoint2 in enumerate(kpointlist): + if ( + np.isclose(kpoint[0], kpoint2[0]) + and np.isclose(kpoint[1], kpoint2[1]) + and np.isclose(kpoint[2], kpoint2[2]) + ): + if weight == weightlist[ikpoint2]: + found += 1 + elif ( + np.isclose(-kpoint[0], kpoint2[0]) + and np.isclose(-kpoint[1], kpoint2[1]) + and np.isclose(-kpoint[2], kpoint2[2]) + ) and weight == weightlist[ikpoint2]: + found += 1 + return found == 1 + + def test_as_from_dict(self): + # tests as dict and from dict methods + new_lobsterin = LobsterIn.from_dict(self.LobsterIn.as_dict()) + assert new_lobsterin == self.LobsterIn + new_lobsterin.to_json() + + +class TestUtils(MatSciTest): + def test_get_all_possible_basis_combinations(self): + # this basis is just for testing (not correct) + min_basis = ["Li 1s 2s ", "Na 1s 2s", "Si 1s 2s"] + max_basis = ["Li 1s 2p 2s ", "Na 1s 2p 2s", "Si 1s 2s"] + combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) + assert combinations_basis == [ + ["Li 1s 2s", "Na 1s 2s", "Si 1s 2s"], + ["Li 1s 2s", "Na 1s 2s 2p", "Si 1s 2s"], + ["Li 1s 2s 2p", "Na 1s 2s", "Si 1s 2s"], + ["Li 1s 2s 2p", "Na 1s 2s 2p", "Si 1s 2s"], + ] + + min_basis = ["Li 1s 2s"] + max_basis = ["Li 1s 2s 2p 3s"] + combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) + assert combinations_basis == [ + ["Li 1s 2s"], + ["Li 1s 2s 2p"], + ["Li 1s 2s 3s"], + ["Li 1s 2s 2p 3s"], + ] + + min_basis = ["Li 1s 2s", "Na 1s 2s"] + max_basis = ["Li 1s 2s 2p 3s", "Na 1s 2s 2p 3s"] + combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) + assert combinations_basis == [ + ["Li 1s 2s", "Na 1s 2s"], + ["Li 1s 2s", "Na 1s 2s 2p"], + ["Li 1s 2s", "Na 1s 2s 3s"], + ["Li 1s 2s", "Na 1s 2s 2p 3s"], + ["Li 1s 2s 2p", "Na 1s 2s"], + ["Li 1s 2s 2p", "Na 1s 2s 2p"], + ["Li 1s 2s 2p", "Na 1s 2s 3s"], + ["Li 1s 2s 2p", "Na 1s 2s 2p 3s"], + ["Li 1s 2s 3s", "Na 1s 2s"], + ["Li 1s 2s 3s", "Na 1s 2s 2p"], + ["Li 1s 2s 3s", "Na 1s 2s 3s"], + ["Li 1s 2s 3s", "Na 1s 2s 2p 3s"], + ["Li 1s 2s 2p 3s", "Na 1s 2s"], + ["Li 1s 2s 2p 3s", "Na 1s 2s 2p"], + ["Li 1s 2s 2p 3s", "Na 1s 2s 3s"], + ["Li 1s 2s 2p 3s", "Na 1s 2s 2p 3s"], + ] + + min_basis = ["Si 1s 2s 2p", "Na 1s 2s"] + max_basis = ["Si 1s 2s 2p 3s", "Na 1s 2s 2p 3s"] + combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) + assert combinations_basis == [ + ["Si 1s 2s 2p", "Na 1s 2s"], + ["Si 1s 2s 2p", "Na 1s 2s 2p"], + ["Si 1s 2s 2p", "Na 1s 2s 3s"], + ["Si 1s 2s 2p", "Na 1s 2s 2p 3s"], + ["Si 1s 2s 2p 3s", "Na 1s 2s"], + ["Si 1s 2s 2p 3s", "Na 1s 2s 2p"], + ["Si 1s 2s 2p 3s", "Na 1s 2s 3s"], + ["Si 1s 2s 2p 3s", "Na 1s 2s 2p 3s"], + ] diff --git a/tests/io/lobster/future/test_outputs.py b/tests/io/lobster/future/test_outputs.py new file mode 100644 index 0000000000..478f62dc27 --- /dev/null +++ b/tests/io/lobster/future/test_outputs.py @@ -0,0 +1,1917 @@ +from __future__ import annotations + +import copy +import json +import os +from typing import TYPE_CHECKING + +import numpy as np +import pytest +from monty.json import MontyEncoder, jsanitize +from numpy.testing import assert_allclose, assert_array_equal +from pytest import approx + +from pymatgen.core.structure import Structure +from pymatgen.electronic_structure.core import Spin +from pymatgen.io.lobster.future import ( + BWDF, + CHARGE, + CHARGE_LCFO, + COBICAR, + COBICAR_LCFO, + COOPCAR, + DOSCAR, + DOSCAR_LCFO, + GROSSPOP, + GROSSPOP_LCFO, + ICOBILIST, + ICOHPLIST, + ICOHPLIST_LCFO, + ICOOPLIST, + POLARIZATION, + BandOverlaps, + Fatband, + Fatbands, + LobsterMatrices, + LobsterOut, + MadelungEnergies, + NcICOBILIST, + SitePotentials, + Wavefunction, +) +from pymatgen.io.vasp import Vasprun +from pymatgen.util.testing import TEST_FILES_DIR, VASP_OUT_DIR, MatSciTest + +if TYPE_CHECKING: + from monty.json import MSONable + +TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp" + +__author__ = "Janine George, Marco Esters" +__copyright__ = "Copyright 2017, The Materials Project" +__version__ = "0.2" +__email__ = "janine.george@uclouvain.be, esters@uoregon.edu" +__date__ = "Dec 10, 2017" + + +class TestBWDF(MatSciTest): + """Test BWDF and BWDFCOHP classes.""" + + def setup_method(self): + self.bwdf_coop = BWDF(filename=f"{TEST_DIR}/BWDF.lobster.AlN.gz") + self.bwdf_cohp = BWDF(filename=f"{TEST_DIR}/BWDFCOHP.lobster.NaCl.gz") + + def test_attributes(self): + assert len(self.bwdf_coop.centers) == 201 + assert len(self.bwdf_cohp.centers) == 143 + assert self.bwdf_coop.bwdf[Spin.down][-2] == approx(0.00082, abs=1e-4) + assert self.bwdf_coop.bwdf[Spin.up][0] == approx(0.81161, abs=1e-4) + assert self.bwdf_cohp.bwdf[Spin.up][103] == approx(-0.01392, abs=1e-4) + + +class TestDOSCAR(MatSciTest): + def setup_method(self): + doscar = f"{VASP_OUT_DIR}/DOSCAR.lobster.spin" + doscar2 = f"{VASP_OUT_DIR}/DOSCAR.lobster.nonspin" + doscar3 = f"{VASP_OUT_DIR}/DOSCAR.LCFO.lobster.AlN" + + self.doscar_spin_pol = DOSCAR(filename=doscar) + self.doscar_nonspin_pol = DOSCAR(filename=doscar2) + self.doscar_lcfo = DOSCAR_LCFO(filename=doscar3) + self.doscar_spin_pol2 = DOSCAR(filename=doscar) + + def test_pdos(self): + """Test projected densities of states (PDOS) from DOSCAR files.""" + expected_pdos_spin = { + "2s": { + Spin.up: [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069], + Spin.down: [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069], + }, + "2p_y": { + Spin.up: [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029], + Spin.down: [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029], + }, + "2p_z": { + Spin.up: [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029], + Spin.down: [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029], + }, + "2p_x": { + Spin.up: [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029], + Spin.down: [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029], + }, + } + + expected_pdos_nonspin = { + "2s": [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060], + "2p_y": [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037], + "2p_z": [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037], + "2p_x": [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037], + } + + for orbital, spin_data in expected_pdos_spin.items(): + for spin, expected_values in spin_data.items(): + assert_allclose( + self.doscar_spin_pol.projected_dos["F1"][orbital].densities[spin], + expected_values, + ) + + for orbital, expected_values in expected_pdos_nonspin.items(): + assert_allclose( + self.doscar_nonspin_pol.projected_dos["F1"][orbital].densities[Spin.up], + expected_values, + ) + + pdos_1a1_AlN = [ + 0.0, + 0.22594, + 0.01335, + 0.0, + 0.0, + 0.0, + 0.00228, + 0.02836, + 0.03053, + 0.01612, + 0.02379, + ] + pdos_3py_Al = [ + 0.0, + 0.02794, + 0.00069, + 0.0, + 0.0, + 0.0, + 0.00216, + 0.0682, + 0.06966, + 0.04402, + 0.16579, + ] + pdos_2s_N = [ + 0.0, + 0.25324, + 0.0157, + 0.0, + 0.0, + 0.0, + 0.0006, + 0.01747, + 0.02247, + 0.01589, + 0.03565, + ] + + assert self.doscar_lcfo.is_lcfo + assert_allclose(self.doscar_lcfo.projected_dos["AlN_1"]["1a1"].densities[Spin.down], pdos_1a1_AlN) + assert_allclose(self.doscar_lcfo.projected_dos["Al_1"]["3p_y"].densities[Spin.down], pdos_3py_Al) + assert_allclose(self.doscar_lcfo.projected_dos["N_1"]["2s"].densities[Spin.down], pdos_2s_N) + + def test_tdos(self): + """Test total densities of states (TDOS) from DOSCAR files.""" + energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] + tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] + + assert_allclose(energies_spin, self.doscar_spin_pol.total_dos.energies) + assert_allclose(tdos_up, self.doscar_spin_pol.total_dos.densities[Spin.up]) + assert_allclose(tdos_down, self.doscar_spin_pol.total_dos.densities[Spin.down]) + # assert fermi == approx(self.doscar_spin_pol.total_dos.efermi) + + energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] + + assert_allclose(energies_nonspin, self.doscar_nonspin_pol.total_dos.energies) + assert_allclose(tdos_nonspin, self.doscar_nonspin_pol.total_dos.densities[Spin.up]) + # assert fermi == approx(self.doscar_nonspin_pol.total_dos.efermi) + + def test_energies(self): + """Test energies from DOSCAR files.""" + energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + + assert_allclose(energies_spin, self.doscar_spin_pol.energies) + + energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] + assert_allclose(energies_nonspin, self.doscar_nonspin_pol.energies) + + def test_itdensities(self): + """Test integrated total densities from DOSCAR files.""" + itdos_up = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09650] + itdos_down = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09685] + assert_allclose(itdos_up, self.doscar_spin_pol.integrated_total_dos.densities[Spin.up]) + assert_allclose(itdos_down, self.doscar_spin_pol.integrated_total_dos.densities[Spin.down]) + + itdos_nonspin = [4.00000, 10.00000, 10.00000, 16.00000, 16.00000, 16.09067] + assert_allclose(itdos_nonspin, self.doscar_nonspin_pol.integrated_total_dos.densities[Spin.up]) + + def test_is_spin_polarized(self): + """Test is_spin_polarized attribute from DOSCAR files.""" + assert self.doscar_spin_pol.is_spin_polarized + assert not self.doscar_nonspin_pol.is_spin_polarized + + +class TestCHARGE(MatSciTest): + def setup_method(self): + """Setup for CHARGE and CHARGE_LCFO tests.""" + self.charge2 = CHARGE(filename=f"{TEST_DIR}/CHARGE.lobster.MnO") + self.charge = CHARGE(filename=f"{TEST_DIR}/CHARGE.lobster.MnO2.gz") + self.charge_lcfo = CHARGE_LCFO(filename=f"{TEST_DIR}/CHARGE.LCFO.lobster.ALN.gz") + + def test_attributes(self): + """Test attributes of CHARGE and CHARGE_LCFO classes.""" + charge_loewdin = [-1.25, 1.25] + charge_mulliken = [-1.30, 1.30] + + assert charge_mulliken == self.charge2.mulliken + assert charge_loewdin == self.charge2.loewdin + + assert self.charge_lcfo.is_lcfo + + assert_allclose(self.charge_lcfo.loewdin, [0.0, 1.02, -1.02]) + assert not self.charge_lcfo.mulliken + + def test_msonable(self): + """Test MSONable functionality of CHARGE class.""" + dict_data = self.charge2.as_dict() + charge_from_dict = CHARGE.from_dict(dict_data) + all_attributes = vars(self.charge2) + for attr_name, attr_value in all_attributes.items(): + assert getattr(charge_from_dict, attr_name) == attr_value + + +class TestLobsterOut(MatSciTest): + def setup_method(self): + """Setup for LobsterOut tests.""" + self.lobsterout_normal = LobsterOut(filename=f"{TEST_DIR}/lobsterout.normal") + # make sure .gz files are also read correctly + self.lobsterout_normal = LobsterOut(filename=f"{TEST_DIR}/lobsterout.normal2.gz") + self.lobsterout_fatband_grosspop_densityofenergies = LobsterOut( + filename=f"{TEST_DIR}/lobsterout.fatband_grosspop_densityofenergy" + ) + self.lobsterout_saveprojection = LobsterOut(filename=f"{TEST_DIR}/lobsterout.saveprojection") + self.lobsterout_skipping_all = LobsterOut(filename=f"{TEST_DIR}/lobsterout.skipping_all") + self.lobsterout_twospins = LobsterOut(filename=f"{TEST_DIR}/lobsterout.twospins") + self.lobsterout_GaAs = LobsterOut(filename=f"{TEST_DIR}/lobsterout.GaAs") + self.lobsterout_from_projection = LobsterOut(filename=f"{TEST_DIR}/lobsterout_from_projection") + self.lobsterout_onethread = LobsterOut(filename=f"{TEST_DIR}/lobsterout.onethread") + self.lobsterout_cobi_madelung = LobsterOut(filename=f"{TEST_DIR}/lobsterout_cobi_madelung") + self.lobsterout_doscar_lso = LobsterOut(filename=f"{TEST_DIR}/lobsterout_doscar_lso") + + # TODO: implement skipping madelung/cobi + self.lobsterout_skipping_cobi_madelung = LobsterOut(filename=f"{TEST_DIR}/lobsterout.skip_cobi_madelung") + + self.lobsterout_v511 = LobsterOut(filename=f"{TEST_DIR}/lobsterout_v511.gz") + + def test_attributes(self): + assert self.lobsterout_normal.basis_functions == [ + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ] + assert self.lobsterout_normal.basis_type == ["pbeVaspFit2015"] + assert_allclose(self.lobsterout_normal.charge_spilling, [0.0268]) + assert self.lobsterout_normal.dft_program == "VASP" + assert self.lobsterout_normal.elements == ["Ti"] + assert self.lobsterout_normal.has_charge + assert self.lobsterout_normal.has_cohpcar + assert self.lobsterout_normal.has_coopcar + assert self.lobsterout_normal.has_doscar + assert not self.lobsterout_normal.has_projection + assert self.lobsterout_normal.has_bandoverlaps + assert not self.lobsterout_normal.has_density_of_energies + assert not self.lobsterout_normal.has_fatbands + assert not self.lobsterout_normal.has_grosspopulation + assert self.lobsterout_normal.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_normal.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_normal.is_restart_from_projection + assert self.lobsterout_normal.lobster_version == "3.1.0" + assert self.lobsterout_normal.number_of_spins == 1 + assert self.lobsterout_normal.number_of_threads == 8 + assert self.lobsterout_normal.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "702"}, + "user_time": {"h": "0", "min": "0", "s": "20", "ms": "330"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, + } + assert self.lobsterout_normal.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_normal.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_fatband_grosspop_densityofenergies.basis_functions == [ + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ] + assert self.lobsterout_fatband_grosspop_densityofenergies.basis_type == ["pbeVaspFit2015"] + assert_allclose(self.lobsterout_fatband_grosspop_densityofenergies.charge_spilling, [0.0268]) + assert self.lobsterout_fatband_grosspop_densityofenergies.dft_program == "VASP" + assert self.lobsterout_fatband_grosspop_densityofenergies.elements == ["Ti"] + assert self.lobsterout_fatband_grosspop_densityofenergies.has_charge + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_cohpcar + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_coopcar + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_doscar + assert not self.lobsterout_fatband_grosspop_densityofenergies.has_projection + assert self.lobsterout_fatband_grosspop_densityofenergies.has_bandoverlaps + assert self.lobsterout_fatband_grosspop_densityofenergies.has_density_of_energies + assert self.lobsterout_fatband_grosspop_densityofenergies.has_fatbands + assert self.lobsterout_fatband_grosspop_densityofenergies.has_grosspopulation + assert self.lobsterout_fatband_grosspop_densityofenergies.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_fatband_grosspop_densityofenergies.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_fatband_grosspop_densityofenergies.is_restart_from_projection + assert self.lobsterout_fatband_grosspop_densityofenergies.lobster_version == "3.1.0" + assert self.lobsterout_fatband_grosspop_densityofenergies.number_of_spins == 1 + assert self.lobsterout_fatband_grosspop_densityofenergies.number_of_threads == 8 + assert self.lobsterout_fatband_grosspop_densityofenergies.timing == { + "wall_time": {"h": "0", "min": "0", "s": "4", "ms": "136"}, + "user_time": {"h": "0", "min": "0", "s": "18", "ms": "280"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "290"}, + } + assert self.lobsterout_fatband_grosspop_densityofenergies.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_fatband_grosspop_densityofenergies.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_saveprojection.basis_functions == [ + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ] + assert self.lobsterout_saveprojection.basis_type == ["pbeVaspFit2015"] + assert_allclose(self.lobsterout_saveprojection.charge_spilling, [0.0268]) + assert self.lobsterout_saveprojection.dft_program == "VASP" + assert self.lobsterout_saveprojection.elements == ["Ti"] + assert self.lobsterout_saveprojection.has_charge + assert not self.lobsterout_saveprojection.has_cohpcar + assert not self.lobsterout_saveprojection.has_coopcar + assert not self.lobsterout_saveprojection.has_doscar + assert self.lobsterout_saveprojection.has_projection + assert self.lobsterout_saveprojection.has_bandoverlaps + assert self.lobsterout_saveprojection.has_density_of_energies + assert not self.lobsterout_saveprojection.has_fatbands + assert not self.lobsterout_saveprojection.has_grosspopulation + assert self.lobsterout_saveprojection.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_saveprojection.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_saveprojection.is_restart_from_projection + assert self.lobsterout_saveprojection.lobster_version == "3.1.0" + assert self.lobsterout_saveprojection.number_of_spins == 1 + assert self.lobsterout_saveprojection.number_of_threads == 8 + assert self.lobsterout_saveprojection.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "574"}, + "user_time": {"h": "0", "min": "0", "s": "18", "ms": "250"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "320"}, + } + assert self.lobsterout_saveprojection.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_saveprojection.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_skipping_all.basis_functions == [ + [ + "3s", + "4s", + "3p_y", + "3p_z", + "3p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ] + assert self.lobsterout_skipping_all.basis_type == ["pbeVaspFit2015"] + assert_allclose(self.lobsterout_skipping_all.charge_spilling, [0.0268]) + assert self.lobsterout_skipping_all.dft_program == "VASP" + assert self.lobsterout_skipping_all.elements == ["Ti"] + assert not self.lobsterout_skipping_all.has_charge + assert not self.lobsterout_skipping_all.has_cohpcar + assert not self.lobsterout_skipping_all.has_coopcar + assert not self.lobsterout_skipping_all.has_doscar + assert not self.lobsterout_skipping_all.has_projection + assert self.lobsterout_skipping_all.has_bandoverlaps + assert not self.lobsterout_skipping_all.has_density_of_energies + assert not self.lobsterout_skipping_all.has_fatbands + assert not self.lobsterout_skipping_all.has_grosspopulation + assert not self.lobsterout_skipping_all.has_cobicar + assert not self.lobsterout_skipping_all.has_madelung + assert self.lobsterout_skipping_all.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 21 and upwards will be ignored.", + ] + assert self.lobsterout_skipping_all.info_orthonormalization == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_skipping_all.is_restart_from_projection + assert self.lobsterout_skipping_all.lobster_version == "3.1.0" + assert self.lobsterout_skipping_all.number_of_spins == 1 + assert self.lobsterout_skipping_all.number_of_threads == 8 + assert self.lobsterout_skipping_all.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "117"}, + "user_time": {"h": "0", "min": "0", "s": "16", "ms": "79"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "320"}, + } + assert self.lobsterout_skipping_all.total_spilling[0] == approx([0.044000000000000004][0]) + assert self.lobsterout_skipping_all.warning_lines == [ + "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_twospins.basis_functions == [ + [ + "4s", + "4p_y", + "4p_z", + "4p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ] + ] + assert self.lobsterout_twospins.basis_type == ["pbeVaspFit2015"] + assert self.lobsterout_twospins.charge_spilling[0] == approx(0.36619999999999997) + assert self.lobsterout_twospins.charge_spilling[1] == approx(0.36619999999999997) + assert self.lobsterout_twospins.dft_program == "VASP" + assert self.lobsterout_twospins.elements == ["Ti"] + assert self.lobsterout_twospins.has_charge + assert self.lobsterout_twospins.has_cohpcar + assert self.lobsterout_twospins.has_coopcar + assert self.lobsterout_twospins.has_doscar + assert not self.lobsterout_twospins.has_projection + assert self.lobsterout_twospins.has_bandoverlaps + assert not self.lobsterout_twospins.has_density_of_energies + assert not self.lobsterout_twospins.has_fatbands + assert not self.lobsterout_twospins.has_grosspopulation + assert self.lobsterout_twospins.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 19 and upwards will be ignored.", + ] + assert self.lobsterout_twospins.info_orthonormalization == [ + "60 of 294 k-points could not be orthonormalized with an accuracy of 1.0E-5." + ] + assert not self.lobsterout_twospins.is_restart_from_projection + assert self.lobsterout_twospins.lobster_version == "3.1.0" + assert self.lobsterout_twospins.number_of_spins == 2 + assert self.lobsterout_twospins.number_of_threads == 8 + assert self.lobsterout_twospins.timing == { + "wall_time": {"h": "0", "min": "0", "s": "3", "ms": "71"}, + "user_time": {"h": "0", "min": "0", "s": "22", "ms": "660"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, + } + assert self.lobsterout_twospins.total_spilling[0] == approx([0.2567][0]) + assert self.lobsterout_twospins.total_spilling[1] == approx([0.2567][0]) + assert self.lobsterout_twospins.warning_lines == [ + "60 of 294 k-points could not be orthonormalized with an accuracy of 1.0E-5.", + "Generally, this is not a critical error. But to help you analyze it,", + "I dumped the band overlap matrices to the file bandOverlaps.lobster.", + "Please check how much they deviate from the identity matrix and decide to", + "use your results only, if you are sure that this is ok.", + ] + + assert self.lobsterout_from_projection.basis_functions == [] + assert self.lobsterout_from_projection.basis_type == [] + assert self.lobsterout_from_projection.charge_spilling[0] == approx(0.0177) + assert self.lobsterout_from_projection.dft_program is None + assert self.lobsterout_from_projection.elements == [] + assert self.lobsterout_from_projection.has_charge + assert self.lobsterout_from_projection.has_cohpcar + assert self.lobsterout_from_projection.has_coopcar + assert self.lobsterout_from_projection.has_doscar + assert not self.lobsterout_from_projection.has_projection + assert not self.lobsterout_from_projection.has_bandoverlaps + assert not self.lobsterout_from_projection.has_density_of_energies + assert not self.lobsterout_from_projection.has_fatbands + assert not self.lobsterout_from_projection.has_grosspopulation + assert self.lobsterout_from_projection.info_lines == [] + assert self.lobsterout_from_projection.info_orthonormalization == [] + assert self.lobsterout_from_projection.is_restart_from_projection + assert self.lobsterout_from_projection.lobster_version == "3.1.0" + assert self.lobsterout_from_projection.number_of_spins == 1 + assert self.lobsterout_from_projection.number_of_threads == 8 + assert self.lobsterout_from_projection.timing == { + "wall_time": {"h": "0", "min": "2", "s": "1", "ms": "890"}, + "user_time": {"h": "0", "min": "15", "s": "10", "ms": "530"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "400"}, + } + assert self.lobsterout_from_projection.total_spilling[0] == approx([0.1543][0]) + assert self.lobsterout_from_projection.warning_lines == [] + + assert self.lobsterout_GaAs.basis_functions == [ + ["4s", "4p_y", "4p_z", "4p_x"], + [ + "4s", + "4p_y", + "4p_z", + "4p_x", + "3d_xy", + "3d_yz", + "3d_z^2", + "3d_xz", + "3d_x^2-y^2", + ], + ] + assert self.lobsterout_GaAs.basis_type == ["Bunge", "Bunge"] + assert self.lobsterout_GaAs.charge_spilling[0] == approx(0.0089) + assert self.lobsterout_GaAs.dft_program == "VASP" + assert self.lobsterout_GaAs.elements == ["As", "Ga"] + assert self.lobsterout_GaAs.has_charge + assert self.lobsterout_GaAs.has_cohpcar + assert self.lobsterout_GaAs.has_coopcar + assert self.lobsterout_GaAs.has_doscar + assert not self.lobsterout_GaAs.has_projection + assert not self.lobsterout_GaAs.has_bandoverlaps + assert not self.lobsterout_GaAs.has_density_of_energies + assert not self.lobsterout_GaAs.has_fatbands + assert not self.lobsterout_GaAs.has_grosspopulation + assert self.lobsterout_GaAs.info_lines == [ + "There are more PAW bands than local basis functions available.", + "To prevent trouble in orthonormalization and Hamiltonian reconstruction", + "the PAW bands from 14 and upwards will be ignored.", + ] + assert self.lobsterout_GaAs.info_orthonormalization == [] + assert not self.lobsterout_GaAs.is_restart_from_projection + assert self.lobsterout_GaAs.lobster_version == "3.1.0" + assert self.lobsterout_GaAs.number_of_spins == 1 + assert self.lobsterout_GaAs.number_of_threads == 8 + assert self.lobsterout_GaAs.timing == { + "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "726"}, + "user_time": {"h": "0", "min": "0", "s": "12", "ms": "370"}, + "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "180"}, + } + assert self.lobsterout_GaAs.total_spilling[0] == approx(0.0859) + + assert self.lobsterout_onethread.number_of_threads == 1 + # Test lobsterout of lobster-4.1.0 + assert self.lobsterout_cobi_madelung.has_cobicar + assert self.lobsterout_cobi_madelung.has_cohpcar + assert self.lobsterout_cobi_madelung.has_madelung + assert not self.lobsterout_cobi_madelung.has_doscar_lso + + assert self.lobsterout_doscar_lso.has_doscar_lso + + assert self.lobsterout_skipping_cobi_madelung.has_cobicar is False + assert self.lobsterout_skipping_cobi_madelung.has_madelung is False + + def test_msonable(self): + """Test the as_dict and from_dict methods for Lobsterout.""" + dict_data = self.lobsterout_normal.as_dict() + + lobsterout_from_dict = LobsterOut.from_dict(dict_data) + assert dict_data == lobsterout_from_dict.as_dict() + + with pytest.raises(TypeError, match="got an unexpected keyword argument 'invalid'"): + LobsterOut(filename=None, invalid="invalid") # type: ignore[testing] + + +class TestFatbands(MatSciTest): + def setup_method(self): + self.structure = Vasprun( + filename=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", + ionic_step_skip=None, + ionic_step_offset=0, + parse_dos=True, + parse_eigen=False, + parse_projected_eigen=False, + parse_potcar_file=False, + occu_tol=1e-8, + exception_on_bad_xml=True, + ).final_structure + self.fatbands_sio2_p_x = Fatbands( + directory=f"{TEST_DIR}/Fatband_SiO2/Test_p_x", + structure=self.structure, + ) + self.vasprun_sio2_p_x = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml") + self.bs_symmline = self.vasprun_sio2_p_x.get_band_structure(line_mode=True, force_hybrid_mode=True) + self.fatbands_sio2_p = Fatbands( + directory=f"{TEST_DIR}/Fatband_SiO2/Test_p", + structure=self.structure, + ) + self.single_fatband = Fatband(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p/FATBAND_o4_2p.lobster") + self.vasprun_sio2_p = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p/vasprun.xml") + self.bs_symmline2 = self.vasprun_sio2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) + self.fatbands_sio2_spin = Fatbands( + directory=f"{TEST_DIR}/Fatband_SiO2/Test_Spin", + structure=self.structure, + ) + + self.vasprun_sio2_spin = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/vasprun.xml") + self.bs_symmline_spin = self.vasprun_sio2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) + + def test_attributes(self): + assert self.fatbands_sio2_p_x.efermi == self.vasprun_sio2_p_x.efermi + lattice1 = self.bs_symmline.lattice_rec.as_dict() + lattice2 = self.fatbands_sio2_p_x.reciprocal_lattice.as_dict() + for idx in range(3): + assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) + + assert self.fatbands_sio2_p_x.fatbands[1]["energies"][Spin.up][1][1] == approx(-18.245) + assert len(self.fatbands_sio2_p_x.spins) == 1 + assert_allclose(self.fatbands_sio2_p_x.kpoints.kpts[3], [0.03409091, 0, 0]) + + assert len(self.fatbands_sio2_p_x.fatbands[0]["projections"][Spin.up]) == len( + self.fatbands_sio2_p_x.kpoints.kpts + ) + + assert self.single_fatband.nbands == 36 + assert self.single_fatband.center == "O4" + assert self.single_fatband.orbital == "2p" + assert len(self.single_fatband.spins) == 1 + assert len(self.single_fatband.fatband["energies"][Spin.up][0]) == 36 + + assert self.fatbands_sio2_p_x.fatbands[-1]["center"] == "Si3" + assert self.fatbands_sio2_p_x.fatbands[-1]["orbital"] == "3s" + assert self.fatbands_sio2_p_x.fatbands[-1]["projections"][Spin.up][2][1] == approx(0.013) + assert self.fatbands_sio2_p_x.fatbands[-1]["energies"][Spin.up][2][2] == approx(-18.245) + assert_allclose(self.fatbands_sio2_p_x.structure[0].frac_coords, [0.0, 0.47634315, 0.666667]) + assert self.fatbands_sio2_p_x.structure[0].species_string == "Si" + assert_allclose( + self.fatbands_sio2_p_x.structure[0].coords, + [-1.19607309, 2.0716597, 3.67462144], + ) + + assert self.fatbands_sio2_p.efermi == self.vasprun_sio2_p.efermi + lattice1 = self.bs_symmline2.lattice_rec.as_dict() + lattice2 = self.fatbands_sio2_p.reciprocal_lattice.as_dict() + for idx in range(3): + assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) + assert self.fatbands_sio2_p.fatbands[0]["energies"][Spin.up][1][1] == approx(-18.245) + assert len(self.fatbands_sio2_p.spins) == 1 + assert_allclose(self.fatbands_sio2_p.kpoints.kpts[3], [0.03409091, 0, 0]) + + assert self.fatbands_sio2_p.fatbands[2]["projections"][Spin.up][2][1] == approx(0.002) + assert_allclose(self.fatbands_sio2_p.structure[0].frac_coords, [0.0, 0.47634315, 0.666667]) + assert self.fatbands_sio2_p.structure[0].species_string == "Si" + assert_allclose( + self.fatbands_sio2_p.structure[0].coords, + [-1.19607309, 2.0716597, 3.67462144], + ) + assert self.fatbands_sio2_p.efermi == approx(1.0647039) + + assert self.fatbands_sio2_spin.efermi == self.vasprun_sio2_spin.efermi + lattice1 = self.bs_symmline_spin.lattice_rec.as_dict() + lattice2 = self.fatbands_sio2_spin.reciprocal_lattice.as_dict() + for idx in range(3): + assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) + assert self.fatbands_sio2_spin.fatbands[1]["energies"][Spin.up][1][1] == approx(-18.245) + assert self.fatbands_sio2_spin.fatbands[0]["energies"][Spin.down][1][1] == approx(-18.245) + assert len(self.fatbands_sio2_spin.spins) == 2 + assert_allclose(self.fatbands_sio2_spin.kpoints.kpts[3], [0.03409091, 0, 0]) + assert len(self.fatbands_sio2_spin.fatbands[0]["energies"][Spin.up][0]) == 36 + assert len(self.fatbands_sio2_spin.fatbands[0]["energies"][Spin.down][0]) == 36 + + assert self.fatbands_sio2_spin.fatbands[0]["projections"][Spin.up][2][1] == approx(0.003) + assert_allclose( + self.fatbands_sio2_spin.structure[0].frac_coords, + [0.0, 0.47634315, 0.666667], + ) + assert self.fatbands_sio2_spin.structure[0].species_string == "Si" + assert_allclose( + self.fatbands_sio2_spin.structure[0].coords, + [-1.19607309, 2.0716597, 3.67462144], + ) + + +class TestBandOverlaps(MatSciTest): + def setup_method(self): + with pytest.raises(RuntimeError, match="Incomplete or non-numeric data found in bandOverlaps"): + self.band_overlaps1 = BandOverlaps(f"{TEST_DIR}/bandOverlaps.lobster.1") + + self.band_overlaps1 = BandOverlaps(f"{TEST_DIR}/bandOverlaps.lobster.1", process_immediately=False) + self.band_overlaps2 = BandOverlaps(f"{TEST_DIR}/bandOverlaps.lobster.2", process_immediately=False) + + self.band_overlaps1.lobster_version = "3.1.0" + self.band_overlaps2.lobster_version = "2.7.0" + + self.band_overlaps1.process() + self.band_overlaps2.process() + + self.band_overlaps1_new = BandOverlaps(f"{TEST_DIR}/bandOverlaps.lobster.new.1") + self.band_overlaps2_new = BandOverlaps(f"{TEST_DIR}/bandOverlaps.lobster.new.2") + + def test_attributes(self): + # band_overlaps_dict + bo_dict = self.band_overlaps1.band_overlaps + assert bo_dict["max_deviations"][Spin.up][0] == approx(0.000278953) + assert self.band_overlaps1_new.band_overlaps["max_deviations"][Spin.up][10] == approx(0.0640933) + assert bo_dict["matrices"][Spin.up][0].item(-1, -1) == approx(0.0188058) + assert self.band_overlaps1_new.band_overlaps["matrices"][Spin.up][10].item(-1, -1) == approx(1.0) + assert bo_dict["matrices"][Spin.up][0].item(0, 0) == approx(1) + assert self.band_overlaps1_new.band_overlaps["matrices"][Spin.up][10].item(0, 0) == approx(0.995849) + + assert bo_dict["max_deviations"][Spin.down][-1] == approx(4.31567e-05) + assert self.band_overlaps1_new.band_overlaps["max_deviations"][Spin.down][9] == approx(0.064369) + assert bo_dict["matrices"][Spin.down][-1].item(0, -1) == approx(4.0066e-07) + assert self.band_overlaps1_new.band_overlaps["matrices"][Spin.down][9].item(0, -1) == approx(1.37447e-09) + + def test_has_good_quality_maxDeviation(self): + assert not self.band_overlaps1.has_good_quality_max_deviation(limit_max_deviation=0.1) + assert not self.band_overlaps1_new.has_good_quality_max_deviation(limit_max_deviation=0.1) + + assert self.band_overlaps1.has_good_quality_max_deviation(limit_max_deviation=100) + assert self.band_overlaps1_new.has_good_quality_max_deviation(limit_max_deviation=100) + assert self.band_overlaps2.has_good_quality_max_deviation() + assert not self.band_overlaps2_new.has_good_quality_max_deviation() + assert not self.band_overlaps2.has_good_quality_max_deviation(limit_max_deviation=0.0000001) + assert not self.band_overlaps2_new.has_good_quality_max_deviation(limit_max_deviation=0.0000001) + + def test_has_good_quality_check_occupied_bands(self): + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=9, + number_occ_bands_spin_down=5, + limit_deviation=0.1, + spin_polarized=True, + ) + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=9, + number_occ_bands_spin_down=5, + limit_deviation=0.1, + spin_polarized=True, + ) + assert self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=3, + number_occ_bands_spin_down=0, + limit_deviation=1, + spin_polarized=True, + ) + assert self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=3, + number_occ_bands_spin_down=0, + limit_deviation=1, + spin_polarized=True, + ) + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=1, + limit_deviation=1e-6, + spin_polarized=True, + ) + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=1, + limit_deviation=1e-6, + spin_polarized=True, + ) + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=0, + limit_deviation=1e-6, + spin_polarized=True, + ) + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, + number_occ_bands_spin_down=0, + limit_deviation=1e-6, + spin_polarized=True, + ) + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=0, + number_occ_bands_spin_down=1, + limit_deviation=1e-6, + spin_polarized=True, + ) + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=0, + number_occ_bands_spin_down=1, + limit_deviation=1e-6, + spin_polarized=True, + ) + assert not self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + number_occ_bands_spin_down=4, + limit_deviation=1e-3, + spin_polarized=True, + ) + assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + number_occ_bands_spin_down=4, + limit_deviation=1e-3, + spin_polarized=True, + ) + assert not self.band_overlaps2.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=10, limit_deviation=1e-7 + ) + assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=10, limit_deviation=1e-7 + ) + assert self.band_overlaps2.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=0.1 + ) + + assert not self.band_overlaps2.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=1e-8 + ) + assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=1e-8 + ) + assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=10, limit_deviation=1) + assert self.band_overlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=2, limit_deviation=0.1 + ) + assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=1, limit_deviation=1) + assert self.band_overlaps2_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=1, limit_deviation=2 + ) + + def test_has_good_quality_check_occupied_bands_patched(self): + """Test with patched data.""" + + limit_deviation = 0.1 + + rng = np.random.default_rng(42) # set seed for reproducibility + + band_overlaps = copy.deepcopy(self.band_overlaps1_new) + + number_occ_bands_spin_up_all = list(range(band_overlaps.band_overlaps["matrices"][Spin.up][0].shape[0])) + number_occ_bands_spin_down_all = list(range(band_overlaps.band_overlaps["matrices"][Spin.down][0].shape[0])) + + for actual_deviation in [0.05, 0.1, 0.2, 0.5, 1.0]: + for spin in (Spin.up, Spin.down): + for number_occ_bands_spin_up, number_occ_bands_spin_down in zip( + number_occ_bands_spin_up_all, + number_occ_bands_spin_down_all, + strict=False, + ): + for i_arr, array in enumerate(band_overlaps.band_overlaps["matrices"][spin]): + number_occ_bands = number_occ_bands_spin_up if spin is Spin.up else number_occ_bands_spin_down + + shape = array.shape + assert np.all(np.array(shape) >= number_occ_bands) + assert len(shape) == 2 + assert shape[0] == shape[1] + + # Generate a noisy background array + patch_array = rng.uniform(0, 10, shape) + + # Patch the top-left sub-array (the part that would be checked) + patch_array[:number_occ_bands, :number_occ_bands] = np.identity(number_occ_bands) + rng.uniform( + 0, actual_deviation, (number_occ_bands, number_occ_bands) + ) + + band_overlaps.band_overlaps["matrices"][spin][i_arr] = patch_array + + result = band_overlaps.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=number_occ_bands_spin_up, + number_occ_bands_spin_down=number_occ_bands_spin_down, + spin_polarized=True, + limit_deviation=limit_deviation, + ) + + if ( + ( + actual_deviation == 0.05 + and number_occ_bands_spin_up <= 7 + and number_occ_bands_spin_down <= 7 + and spin is Spin.up + ) + or (actual_deviation == 0.05 and spin is Spin.down) + or actual_deviation == 0.1 + or ( + actual_deviation in [0.2, 0.5, 1.0] + and number_occ_bands_spin_up == 0 + and number_occ_bands_spin_down == 0 + ) + ): + assert result + else: + assert not result + + def test_exceptions(self): + with pytest.raises(ValueError, match="number_occ_bands_spin_down has to be specified"): + self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + spin_polarized=True, + ) + with pytest.raises(ValueError, match="number_occ_bands_spin_down has to be specified"): + self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + spin_polarized=True, + ) + + def test_keys(self): + bo_dict = self.band_overlaps1.band_overlaps + bo_dict_new = self.band_overlaps1_new.band_overlaps + bo_dict_2 = self.band_overlaps2.band_overlaps + assert len(bo_dict["k_points"][Spin.up]) == 408 + assert len(bo_dict_2["max_deviations"][Spin.up]) == 2 + assert len(bo_dict_new["matrices"][Spin.down]) == 73 + + +class TestGROSSPOP(MatSciTest): + def setup_method(self): + self.grosspop1 = GROSSPOP(f"{TEST_DIR}/GROSSPOP.lobster") + self.grosspop_511_sp = GROSSPOP(f"{TEST_DIR}/GROSSPOP_511_sp.lobster.AlN.gz") + self.grosspop_511_nsp = GROSSPOP(f"{TEST_DIR}/GROSSPOP_511_nsp.lobster.NaCl.gz") + self.grosspop_511_lcfo = GROSSPOP_LCFO(f"{TEST_DIR}/GROSSPOP.LCFO.lobster.AlN.gz") + + def test_attributes(self): + gross_pop_list = self.grosspop1.populations + gross_pop_list_511_sp = self.grosspop_511_sp.populations + gross_pop_list_511_nsp = self.grosspop_511_nsp.populations + gross_pop_list_lcfo = self.grosspop_511_lcfo.populations + + assert gross_pop_list["Si1"]["3p_y"][Spin.up]["mulliken"] == approx(0.38) + assert gross_pop_list["Si1"]["3p_z"][Spin.up]["mulliken"] == approx(0.37) + assert gross_pop_list["Si1"]["3p_x"][Spin.up]["mulliken"] == approx(0.37) + assert gross_pop_list["Si1"]["3p_y"][Spin.up]["loewdin"] == approx(0.52) + assert gross_pop_list["Si1"]["3p_z"][Spin.up]["loewdin"] == approx(0.52) + assert gross_pop_list["Si1"]["3p_x"][Spin.up]["loewdin"] == approx(0.52) + assert gross_pop_list["O5"]["2s"][Spin.up]["mulliken"] == approx(1.80) + assert gross_pop_list["O5"]["2s"][Spin.up]["loewdin"] == approx(1.60) + assert gross_pop_list["O8"]["2s"][Spin.up]["mulliken"] == approx(1.80) + assert gross_pop_list["O8"]["2s"][Spin.up]["loewdin"] == approx(1.60) + assert len(gross_pop_list) == 9 + + # v5.1 spin polarized + assert len(self.grosspop_511_sp.spins) == 2 + assert gross_pop_list_511_sp["Al1"]["3p_x"][Spin.up]["mulliken"] == approx(0.19) + assert gross_pop_list_511_sp["N3"]["2s"][Spin.down]["loewdin"] == approx(0.7) + + # v5.1 non spin polarized + assert len(self.grosspop_511_nsp.spins) == 1 + assert self.grosspop_511_lcfo.is_lcfo + assert gross_pop_list_511_nsp["Na1"]["3s"][Spin.up]["mulliken"] == approx(0.22) + assert gross_pop_list_511_nsp["Na1"]["3s"][Spin.up]["loewdin"] == approx(0.33) + + # v.5.1.1 LCFO + assert self.grosspop_511_lcfo.is_lcfo + assert gross_pop_list_lcfo["AlN1"]["1a1"][Spin.up]["loewdin"] == approx(0.81) + assert gross_pop_list_lcfo["AlN1"]["1a1"][Spin.down]["loewdin"] == approx(0.81) + + with pytest.raises(KeyError): + assert gross_pop_list_lcfo["AlN1"]["1a1"][Spin.up]["mulliken"] + + def test_msonable(self): + dict_data = self.grosspop1.as_dict() + grosspop_from_dict = GROSSPOP.from_dict(dict_data) + all_attributes = vars(self.grosspop1) + for attr_name, attr_value in all_attributes.items(): + assert getattr(grosspop_from_dict, attr_name) == attr_value + + +class TestICOXXLIST(MatSciTest): + def setup_method(self): + self.icohp_bise = ICOHPLIST(filename=f"{TEST_DIR}/ICOHPLIST.lobster.BiSe", process_immediately=False) + self.icohp_bise.lobster_version = "3.1.0" + self.icohp_bise.process() + + self.icoop_bise = ICOOPLIST(filename=f"{TEST_DIR}/ICOOPLIST.lobster.BiSe", process_immediately=False) + self.icoop_bise.lobster_version = "3.2.0" + self.icoop_bise.process() + + self.icohp_fe = ICOHPLIST(filename=f"{TEST_DIR}/ICOHPLIST.lobster", process_immediately=False) + self.icohp_fe.lobster_version = "2.7.0" + self.icohp_fe.process() + + self.icohp_gzipped = ICOHPLIST(filename=f"{TEST_DIR}/ICOHPLIST.lobster.gz", process_immediately=False) + self.icohp_gzipped.lobster_version = "3.1.0" + self.icohp_gzipped.process() + + self.icoop_fe = ICOOPLIST(filename=f"{TEST_DIR}/ICOOPLIST.lobster", process_immediately=False) + self.icoop_fe.lobster_version = "2.7.0" + self.icoop_fe.process() + + self.icohp_aln_511_sp = ICOHPLIST(filename=f"{TEST_DIR}/ICOHPLIST_511_sp.lobster.AlN.gz") + self.icohp_nacl_511_nsp = ICOHPLIST( + filename=f"{TEST_DIR}/ICOHPLIST_511_nsp.lobster.NaCl.gz", + process_immediately=False, + ) + self.icohp_nacl_511_nsp.lobster_version = "5.0.5" + self.icohp_nacl_511_nsp.process() + + # ICOHPLIST.LCFO.lobster from Lobster v5.1.1 + self.icohp_lcfo = ICOHPLIST_LCFO(filename=f"{TEST_DIR}/ICOHPLIST.LCFO.lobster.AlN.gz") + self.icohp_lcfo_non_orbitalwise = ICOHPLIST_LCFO( + filename=f"{TEST_DIR}/ICOHPLIST_non_orbitalwise.LCFO.lobster.AlN.gz", + ) + + self.icobi_orbitalwise = ICOBILIST(filename=f"{TEST_DIR}/ICOBILIST.lobster", process_immediately=False) + self.icobi_orbitalwise.lobster_version = "3.1.0" + self.icobi_orbitalwise.process() + + self.icobi = ICOBILIST( + filename=f"{TEST_DIR}/ICOBILIST.lobster.withoutorbitals", + process_immediately=False, + ) + self.icobi.lobster_version = "3.1.0" + self.icobi.process() + + self.icobi_orbitalwise_spinpolarized = ICOBILIST( + filename=f"{TEST_DIR}/ICOBILIST.lobster.spinpolarized", + process_immediately=False, + ) + self.icobi_orbitalwise_spinpolarized.lobster_version = "4.5.0" + self.icobi_orbitalwise_spinpolarized.process() + # make sure the correct line is read to check if this is a orbitalwise ICOBILIST + self.icobi_orbitalwise_add = ICOBILIST( + filename=f"{TEST_DIR}/ICOBILIST.lobster.additional_case", + process_immediately=False, + ) + self.icobi_orbitalwise_add.lobster_version = "3.1.0" + self.icobi_orbitalwise_add.process() + + self.icobi_orbitalwise_spinpolarized_add = ICOBILIST( + filename=f"{TEST_DIR}/ICOBILIST.lobster.spinpolarized.additional_case", + process_immediately=False, + ) + self.icobi_orbitalwise_spinpolarized_add.lobster_version = "4.5.0" + self.icobi_orbitalwise_spinpolarized_add.process() + + def test_attributes(self): + assert len(self.icohp_bise.spins) == 1 + assert len(self.icohp_bise.interactions) == 11 + assert len(self.icohp_fe.spins) == 2 + assert len(self.icohp_fe.interactions) == 2 + assert len(self.icoop_fe.spins) == 2 + assert len(self.icoop_fe.interactions) == 2 + + # >v5 ICOHPLIST + assert len(self.icohp_aln_511_sp.spins) == 2 + + assert len(self.icohp_aln_511_sp.interactions) == 1088 + assert len(self.icohp_nacl_511_nsp.spins) == 1 + assert len(self.icohp_nacl_511_nsp.interactions) == 2584 + + # v5.1.1 LCFO + assert self.icohp_lcfo.is_lcfo + assert len(self.icohp_lcfo.spins) == 2 + assert len(self.icohp_lcfo.interactions) == 1180 + assert len(self.icohp_lcfo_non_orbitalwise.interactions) == 28 + + def test_values(self): + icohplist_bise = [ + { + "index": 1, + "centers": ["Bi1", "Se7"], + "length": 2.88231, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -2.18042}, + }, + { + "index": 2, + "centers": ["Bi1", "Se10"], + "length": 3.10144, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -1.14347}, + }, + { + "index": 3, + "centers": ["Bi2", "Se8"], + "length": 2.88231, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -2.18042}, + }, + { + "index": 4, + "centers": ["Bi2", "Se9"], + "length": 3.10144, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -1.14348}, + }, + { + "index": 5, + "centers": ["Bi3", "Se10"], + "length": 3.05001, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -1.30006}, + }, + { + "index": 6, + "centers": ["Bi3", "Se11"], + "length": 2.91676, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -1.96843}, + }, + { + "index": 7, + "centers": ["Bi4", "Se9"], + "length": 3.05001, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -1.30006}, + }, + { + "index": 8, + "centers": ["Bi4", "Se12"], + "length": 2.91676, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -1.96843}, + }, + { + "index": 9, + "centers": ["Bi5", "Se12"], + "length": 3.37522, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -0.47531}, + }, + { + "index": 10, + "centers": ["Bi5", "Bi6"], + "length": 3.07294, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -2.38796}, + }, + { + "index": 11, + "centers": ["Bi6", "Se11"], + "length": 3.37522, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -0.47531}, + }, + ] + icooplist_bise = [ + { + "index": 1, + "centers": ["Bi1", "Se7"], + "length": 2.88231, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: 0.14245}, + }, + { + "index": 2, + "centers": ["Bi1", "Se10"], + "length": 3.10144, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -0.04118}, + }, + { + "index": 3, + "centers": ["Bi2", "Se8"], + "length": 2.88231, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: 0.14245}, + }, + { + "index": 4, + "centers": ["Bi2", "Se9"], + "length": 3.10144, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -0.04118}, + }, + { + "index": 5, + "centers": ["Bi3", "Se10"], + "length": 3.05001, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -0.03516}, + }, + { + "index": 6, + "centers": ["Bi3", "Se11"], + "length": 2.91676, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: 0.10745}, + }, + { + "index": 7, + "centers": ["Bi4", "Se9"], + "length": 3.05001, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -0.03516}, + }, + { + "index": 8, + "centers": ["Bi4", "Se12"], + "length": 2.91676, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: 0.10745}, + }, + { + "index": 9, + "centers": ["Bi5", "Se12"], + "length": 3.37522, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -0.12395}, + }, + { + "index": 10, + "centers": ["Bi5", "Bi6"], + "length": 3.07294, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: 0.24714}, + }, + { + "index": 11, + "centers": ["Bi6", "Se11"], + "length": 3.37522, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -0.12395}, + }, + ] + icooplist_fe = [ + { + "index": 1, + "centers": ["Fe8", "Fe7"], + "length": 2.83189, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -0.11389, Spin.down: -0.20828}, + }, + { + "index": 2, + "centers": ["Fe8", "Fe9"], + "length": 2.45249, + "cells": [[], []], + "orbitals": [None, None], + "icoxx": {Spin.up: -0.04087, Spin.down: -0.05756}, + }, + ] + + assert icohplist_bise == self.icohp_bise.interactions + assert icooplist_fe == self.icoop_fe.interactions + assert icooplist_bise == self.icoop_bise.interactions + + assert self.icobi.interactions[1]["icoxx"][Spin.up] == approx(0.58649) + assert self.icobi_orbitalwise.interactions[2]["icoxx"][Spin.up] == approx(0.02559) + assert self.icobi_orbitalwise.interactions[1]["icoxx"][Spin.up] == approx(0.04940) + assert self.icobi_orbitalwise_spinpolarized.interactions[1]["icoxx"][Spin.up] == approx(0.04940 / 2, abs=1e-3) + assert self.icobi_orbitalwise_spinpolarized.interactions[1]["icoxx"][Spin.down] == approx(0.04940 / 2, abs=1e-3) + assert self.icobi_orbitalwise_spinpolarized.interactions[2]["icoxx"][Spin.down] == approx(0.01279, abs=1e-3) + assert self.icobi_orbitalwise_spinpolarized.interactions[2]["orbitals"] == [ + "2p_y", + "6s", + ] + + # >v5 ICOHPLIST + assert self.icohp_aln_511_sp.interactions[2]["icoxx"][Spin.up] == approx(0.00102) + assert self.icohp_aln_511_sp.interactions[2]["icoxx"][Spin.down] == approx(0.00104) + assert self.icohp_nacl_511_nsp.interactions[13]["icoxx"][Spin.up] == approx(0.0) + assert self.icohp_nacl_511_nsp.interactions[10]["orbitals"] == ["2p_y", "2p_z"] + + # v5.1.1 LCFO + assert self.icohp_lcfo.interactions[15]["orbitals"] == ["2a1", "4e"] + assert self.icohp_lcfo_non_orbitalwise.interactions[16]["icoxx"][Spin.up] == approx(-0.21495) + assert self.icohp_lcfo_non_orbitalwise.interactions[16]["icoxx"][Spin.down] == approx(-0.21498) + + def test_msonable(self): + dict_data = self.icobi_orbitalwise_spinpolarized.as_dict() + icohplist_from_dict = ICOHPLIST.from_dict(dict_data) + all_attributes = vars(self.icobi_orbitalwise_spinpolarized) + for attr_name, attr_value in all_attributes.items(): + if isinstance(attr_value, np.ndarray): + assert_array_equal(getattr(icohplist_from_dict, attr_name), attr_value) + else: + assert getattr(icohplist_from_dict, attr_name) == attr_value + + +class TestWavefunction(MatSciTest): + def test_parse_file(self): + wf = Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ) + + assert_array_equal([41, 41, 41], wf.grid) + assert wf.points[4][0] == approx(0.0000) + assert wf.points[4][1] == approx(0.0000) + assert wf.points[4][2] == approx(0.4000) + assert wf.reals[8] == approx(1.38863e-01) + assert wf.imaginaries[8] == approx(2.89645e-01) + assert len(wf.imaginaries) == 41 * 41 * 41 + assert len(wf.reals) == 41 * 41 * 41 + assert len(wf.points) == 41 * 41 * 41 + assert wf.distances[0] == approx(0.0000) + + def test_set_volumetric_data(self): + wave1 = Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ) + + wave1.set_volumetric_data(grid=wave1.grid, structure=wave1.structure) + assert wave1.volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) + assert wave1.volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) + + def test_get_volumetricdata_real(self): + wave1 = Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ) + volumetricdata_real = wave1.get_volumetricdata_real() + assert volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) + + def test_get_volumetricdata_imaginary(self): + wave1 = Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ) + volumetricdata_imaginary = wave1.get_volumetricdata_imaginary() + assert volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) + + def test_get_volumetricdata_density(self): + wave1 = Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ) + volumetricdata_density = wave1.get_volumetricdata_density() + assert volumetricdata_density.data["total"][0, 0, 0] == approx((-3.0966 * -3.0966) + (-6.45895 * -6.45895)) + + def test_write_file(self): + wave1 = Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ) + real_wavecar_path = f"{self.tmp_path}/real-wavecar.vasp" + wave1.write_file(filename=real_wavecar_path, part="real") + assert os.path.isfile(real_wavecar_path) + + imag_wavecar_path = f"{self.tmp_path}/imaginary-wavecar.vasp" + wave1.write_file(filename=imag_wavecar_path, part="imaginary") + assert os.path.isfile(imag_wavecar_path) + + density_wavecar_path = f"{self.tmp_path}/density-wavecar.vasp" + wave1.write_file(filename=density_wavecar_path, part="density") + assert os.path.isfile(density_wavecar_path) + + +class TestSitePotentials(MatSciTest): + def setup_method(self) -> None: + self.sitepotentials = SitePotentials(filename=f"{TEST_DIR}/SitePotentials.lobster.perovskite") + + def test_attributes(self): + assert self.sitepotentials.site_potentials_loewdin == [ + -8.77, + -17.08, + 9.57, + 9.57, + 8.45, + ] + assert self.sitepotentials.site_potentials_mulliken == [ + -11.38, + -19.62, + 11.18, + 11.18, + 10.09, + ] + assert self.sitepotentials.madelung_energies_loewdin == approx(-28.64) + assert self.sitepotentials.madelung_energies_mulliken == approx(-40.02) + assert self.sitepotentials.centers == ["La1", "Ta2", "N3", "N4", "O5"] + assert len(self.sitepotentials.centers) == 5 + assert self.sitepotentials.ewald_splitting == approx(3.14) + + def test_msonable(self): + dict_data = self.sitepotentials.as_dict() + sitepotential_from_dict = SitePotentials.from_dict(dict_data) + all_attributes = vars(self.sitepotentials) + for attr_name, attr_value in all_attributes.items(): + assert getattr(sitepotential_from_dict, attr_name) == attr_value + + +class TestMadelungEnergies(MatSciTest): + def setup_method(self) -> None: + self.madelungenergies = MadelungEnergies(filename=f"{TEST_DIR}/MadelungEnergies.lobster.perovskite") + + def test_attributes(self): + assert self.madelungenergies.madelung_energies_loewdin == approx(-28.64) + assert self.madelungenergies.madelung_energies_mulliken == approx(-40.02) + assert self.madelungenergies.ewald_splitting == approx(3.14) + + def test_msonable(self): + dict_data = self.madelungenergies.as_dict() + madelung_from_dict = MadelungEnergies.from_dict(dict_data) + all_attributes = vars(self.madelungenergies) + for attr_name, attr_value in all_attributes.items(): + assert getattr(madelung_from_dict, attr_name) == attr_value + + +class TestLobsterMatrices(MatSciTest): + def setup_method(self) -> None: + self.hamilton_matrices = LobsterMatrices( + filename=f"{TEST_DIR}/Na_hamiltonMatrices.lobster.gz", efermi=-2.79650354 + ) + self.transfer_matrices = LobsterMatrices(filename=f"{TEST_DIR}/C_transferMatrices.lobster.gz") + self.overlap_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Si_overlapMatrices.lobster.gz") + self.coeff_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Si_coefficientMatricesLSO1.lobster.gz") + + def test_attributes(self): + assert self.hamilton_matrices.efermi == -2.79650354 + assert self.hamilton_matrices.centers == ["Na1", "Na1", "Na1", "Na1"] + + assert "1" in self.hamilton_matrices.matrices + assert len(self.hamilton_matrices.matrices) == 1 + + assert isinstance(self.hamilton_matrices.matrices["1"], dict) + for spin in self.hamilton_matrices.matrices["1"]: + assert spin in [Spin.up, Spin.down] + assert isinstance(self.hamilton_matrices.matrices["1"][spin], np.ndarray) + assert self.hamilton_matrices.matrices["1"][spin].shape == (4, 4) + + assert self.hamilton_matrices.orbitals == ["3s", "2p_y", "2p_z", "2p_x"] + + with pytest.raises(KeyError): + assert self.hamilton_matrices.matrices["2"][Spin.down] + + assert self.hamilton_matrices.matrices["1"][Spin.up][0, 0].real == approx(-3.02170000) + assert self.hamilton_matrices.matrices["1"][Spin.up][0, 0].imag == approx(0.0) + + assert self.hamilton_matrices.get_onsite_values("Na1", "3s") == approx( + (-3.0217 + 2.79650354 - 1.39420000 + 2.79650354) / 2 + ) + + assert self.hamilton_matrices.get_onsite_values("Na1", "2p_x") == approx( + (-28.56640000 + 2.79650354 - 28.48100000 + 2.79650354) / 2 + ) + + onsite_values = self.hamilton_matrices.get_onsite_values() + assert isinstance(onsite_values, dict) + + for key in onsite_values: + assert key in ["Na1_3s", "Na1_2p_y", "Na1_2p_z", "Na1_2p_x"] + assert isinstance(onsite_values[key], float) + + assert self.overlap_matrices.efermi is None + assert self.overlap_matrices.centers == ["Si1", "Si1", "Si1", "Si1"] + assert self.overlap_matrices.orbitals == ["3s", "3p_y", "3p_z", "3p_x"] + + assert "1" in self.overlap_matrices.matrices + assert len(self.overlap_matrices.matrices) == 1 + + assert isinstance(self.overlap_matrices.matrices["1"], dict) + for spin in self.overlap_matrices.matrices["1"]: + assert spin in [None] + assert isinstance(self.overlap_matrices.matrices["1"][spin], np.ndarray) + assert self.overlap_matrices.matrices["1"][spin].shape == (4, 4) + + for m in range(4): + assert self.overlap_matrices.matrices["1"][None][m, m].real == approx(1.00000000) + + assert self.transfer_matrices.efermi is None + assert self.transfer_matrices.centers == ["C1", "C1", "C1", "C1"] + assert self.transfer_matrices.orbitals == ["2s", "2p_y", "2p_z", "2p_x"] + + assert isinstance(self.transfer_matrices.matrices["1"], dict) + + assert "1" in self.transfer_matrices.matrices + assert len(self.transfer_matrices.matrices) == 1 + + for spin in self.transfer_matrices.matrices["1"]: + assert spin in [Spin.up, Spin.down] + assert isinstance(self.transfer_matrices.matrices["1"][spin], np.ndarray) + assert self.transfer_matrices.matrices["1"][spin].shape == (4, 4) + + assert self.coeff_matrices.efermi is None + assert self.coeff_matrices.centers == ["Si1", "Si1", "Si1", "Si1"] + assert self.coeff_matrices.orbitals == ["3s", "3p_y", "3p_z", "3p_x"] + + assert isinstance(self.coeff_matrices.matrices["1"], dict) + assert "1" in self.coeff_matrices.matrices + assert len(self.coeff_matrices.matrices) == 1 + for spin in self.coeff_matrices.matrices["1"]: + assert spin in [Spin.up, Spin.down] + assert isinstance(self.coeff_matrices.matrices["1"][spin], np.ndarray) + assert self.coeff_matrices.matrices["1"][spin].shape == (4, 4) + + +class TestPOLARIZATION(MatSciTest): + def setup_method(self) -> None: + self.polarization = POLARIZATION(filename=f"{TEST_DIR}/POLARIZATION.lobster.AlN.gz") + + def test_attributes(self): + assert self.polarization.rel_loewdin_pol_vector == { + "x": -0.0, + "y": -0.01, + "z": 45.62, + "abs": 45.62, + "unit": "uC/cm2", + } + assert self.polarization.rel_mulliken_pol_vector == { + "x": -0.0, + "y": -0.02, + "z": 56.14, + "abs": 56.14, + "unit": "uC/cm2", + } + + +class TestCOBICAR(MatSciTest): + """Tests for COBICAR class.""" + + def test_read_cobicar_spin(self): + cobicar = COBICAR(TEST_DIR + "/COBICAR.lobster.B2H6.spin") + + assert cobicar.is_spin_polarized + assert cobicar.data.shape == ( + cobicar.num_data, + cobicar.num_bonds * 2 * (len(cobicar.spins)) + 1, + ) + + for interaction in cobicar.interactions: + assert "coxx" in interaction + assert "icoxx" in interaction + + assert len(interaction["coxx"]) == 2 + + assert Spin.up in interaction["icoxx"] + assert Spin.down in interaction["icoxx"] + + assert len(cobicar.energies) == cobicar.num_data + + assert len(cobicar.get_interactions_by_properties(centers=["H4", "B1", "H7"])) == 5 + assert len(cobicar.get_interactions_by_properties(centers=["H4", "B1", "H7"], orbitals=["2s"])) == 1 + + assert cobicar.get_data_by_properties( + centers=["H4", "B1", "H7"], + orbitals=["2s"], + spins=[Spin.up, Spin.down], + ).shape == (cobicar.num_data, 4) + + assert cobicar.get_data_by_properties( + centers=["H4", "B1", "H7"], + orbitals=["2s"], + spins=[Spin.down], + data_type="icoxx", + ).shape == (cobicar.num_data, 1) + + def test_read_cobicar_4_centers(self): + cobicar = COBICAR(TEST_DIR + "/COBICAR.lobster.GeTe_4center") + + assert len(cobicar.get_interactions_by_properties(centers=["Ge", "Ge", "T", "Te"])) == 1 + + def test_read_cobicar_4_centers_orbital_resolved(self, tmp_path): + cobicar = COBICAR(TEST_DIR + "/COBICAR.lobster.GeTe.multi.orbitalwise") + + assert ( + len( + cobicar.get_interactions_by_properties( + centers=["Ge", "Ge", "T", "Te"], + orbitals=["5p_z", "4p_z", "5p_z", "p_x"], + ) + ) + == 2 + ) + + assert len(cobicar.get_interactions_by_properties(indices=[13])) == 257 + assert len(cobicar.get_interactions_by_properties(orbitals=["2p_x"])) == 0 + assert len(cobicar.get_interactions_by_properties(cells=[[1, 0, 0]])) == 257 + + interactions = cobicar.get_interaction_indices_by_properties( + centers=["Ge", "Ge", "T", "Te"], + orbitals=["5p_z", "4p_z", "5p_z", "p_x"], + ) + + assert len(interactions) == 2 + assert cobicar.interactions[interactions[0]]["centers"] == [ + "Te8", + "Ge1", + "Te8", + "Ge1", + ] + assert cobicar.interactions[interactions[0]]["orbitals"] == [ + "5p_z", + "4p_z", + "5p_z", + "4p_x", + ] + + data_indices = cobicar.interaction_indices_to_data_indices_mapping(interactions) + + assert len(data_indices) == 4 + assert data_indices[0] == interactions[0] * 2 + 1 + assert data_indices[1] == interactions[0] * 2 + 2 + assert data_indices[2] == interactions[1] * 2 + 1 + assert data_indices[3] == interactions[1] * 2 + 2 + + cobicar.save(f"{tmp_path}/cobicar.json") + cobicar_from_json = COBICAR.load(f"{tmp_path}/cobicar.json") + + assert cobicar_from_json.is_spin_polarized == cobicar.is_spin_polarized + assert cobicar_from_json.num_data == cobicar.num_data + assert cobicar_from_json.num_bonds == cobicar.num_bonds + assert len(cobicar_from_json.interactions) == len(cobicar.interactions) + + for interaction1, interaction2 in zip(cobicar_from_json.interactions, cobicar.interactions, strict=True): + assert interaction1["centers"] == interaction2["centers"] + assert interaction1["orbitals"] == interaction2["orbitals"] + assert_allclose(interaction1["coxx"][Spin.up], interaction2["coxx"][Spin.up]) + assert_allclose(interaction1["icoxx"][Spin.up], interaction2["icoxx"][Spin.up]) + + assert_allclose(cobicar_from_json.data, cobicar.data) + + ya_cobicar = COBICAR.from_dict(cobicar.as_dict()) + + for interaction1, interaction2 in zip(ya_cobicar.interactions, cobicar_from_json.interactions, strict=True): + assert interaction1["centers"] == interaction2["centers"] + assert interaction1["orbitals"] == interaction2["orbitals"] + assert_allclose(interaction1["coxx"][Spin.up], interaction2["coxx"][Spin.up]) + assert_allclose(interaction1["icoxx"][Spin.up], interaction2["icoxx"][Spin.up]) + + assert_allclose(ya_cobicar.data, cobicar_from_json.data) + + +class TestCOHPCAR(MatSciTest): + """Tests for COHPCAR class.""" + + def test_read_cobicar_lcfo(self): + cohpcar = COBICAR_LCFO(TEST_DIR + "/COHPCAR.LCFO.lobster.NaCl.gz") + + assert cohpcar.is_lcfo + + assert cohpcar.interactions[2]["centers"] == ["NaCl_1", "NaCl_1"] + assert cohpcar.interactions[2]["orbitals"] == ["1a1", "1a1"] + assert cohpcar.interactions[2]["length"] == approx(2.8473125412856759) + + assert cohpcar.data.shape == (cohpcar.num_data, cohpcar.num_bonds * 2 + 1) + + assert cohpcar.interactions[-1]["centers"] == ["Na2_2", "Cl_6"] + assert cohpcar.interactions[-1]["orbitals"] == ["2a1u", "3p_x"] + + assert ( + len( + cohpcar.get_interactions_by_properties( + centers=["NaCl_1", "Na2_2"], + orbitals=["a1", "a1"], + ) + ) + == 48 + ) + + +class TestCOOPCAR(MatSciTest): + """Tests for COOPCAR class.""" + + def test_coopcar(self): + coopcar = COOPCAR(TEST_DIR + "/COOPCAR.lobster.gz") + + assert coopcar.is_spin_polarized + assert coopcar.data.shape == ( + coopcar.num_data, + coopcar.num_bonds * 2 * (len(coopcar.spins)) + 1, + ) + + for interaction in coopcar.interactions: + assert "coxx" in interaction + assert "icoxx" in interaction + + assert len(interaction["coxx"]) == 2 + + assert Spin.up in interaction["icoxx"] + assert Spin.down in interaction["icoxx"] + + assert len(coopcar.energies) == coopcar.num_data + + assert len(coopcar.get_interactions_by_properties(centers=["Fe8", "Fe7"])) == 1 + assert coopcar.get_data_by_properties(centers=["Fe8", "Fe9"])[0, -1] == approx(-0.00099) + + def test_coopcar_2(self): + coopcar = COOPCAR(TEST_DIR + "/COOPCAR.lobster.BiSe.gz") + + assert not coopcar.is_spin_polarized + assert coopcar.data.shape == (coopcar.num_data, coopcar.num_bonds * 2 + 1) + + def test_coopcar_3(self): + coopcar = COOPCAR(TEST_DIR + "/COOPCAR.lobster.KF.gz") + + assert not coopcar.is_spin_polarized + assert coopcar.energies.shape == (coopcar.num_data,) + + assert coopcar.data.shape == (coopcar.num_data, coopcar.num_bonds * 2 + 1) + + assert coopcar.interactions[0]["centers"] == ["Average"] + assert coopcar.interactions[1]["centers"] == ["F1", "K2"] + + +class TestNcICOBILIST(MatSciTest): + """Tests for NcICOBILIST class.""" + + def test_ncicobilist(self): + ncicobi = NcICOBILIST(filename=f"{TEST_DIR}/NcICOBILIST.lobster.nospin.withoutorbitals") + + assert len(ncicobi.spins) == 1 + assert ncicobi.interactions[0]["centers"] == ["X1", "X20"] + assert ncicobi.interactions[0]["orbitals"] == [None, None] + assert ncicobi.interactions[0]["icoxx"][Spin.up] == approx(0) + + with pytest.raises(KeyError): + ncicobi.interactions[0]["icoxx"][Spin.down] + + assert ncicobi.data.shape == (2, 1) + + assert ncicobi.get_interactions_by_properties( + centers=["X22"], + )[0]["icoxx"][Spin.up] == approx(0.00018) + + assert ncicobi.get_data_by_properties( + centers=["X22"], + spins=[Spin.up], + ) == approx(0.00018) + + def test_ncicobilist_spin(self): + ncicobi = NcICOBILIST(filename=f"{TEST_DIR}/NcICOBILIST.lobster") + + assert len(ncicobi.spins) == 2 + + assert ncicobi.interactions[0]["centers"] == ["X1", "X20"] + assert ncicobi.interactions[0]["orbitals"] == [None, None] + assert ncicobi.interactions[1]["icoxx"][Spin.up] == approx(0.00009) + assert ncicobi.interactions[1]["icoxx"][Spin.down] == approx(0.00009) + + assert ncicobi.data.shape == (24, 2) + + interaction = ncicobi.get_interactions_by_properties(orbitals=["4d_x^2-y^2", "4d_x^2-y^2"]) + + assert len(interaction) == 1 + assert interaction[0]["index"] == 2 + assert interaction[0]["centers"] == ["X22", "Xs42", "X31"] + assert interaction[0]["orbitals"] == ["4d_x^2-y^2", "3p", "4d_x^2-y^2"] + + for interaction in ncicobi.interactions: + assert "icoxx" in interaction + assert Spin.up in interaction["icoxx"] + assert Spin.down in interaction["icoxx"] + + assert "centers" in interaction + assert "orbitals" in interaction + assert "length" in interaction + + +class TestMsonable(MatSciTest): + def setup_method(self) -> None: + self.objects_to_test: dict[type[MSONable], str] = { + BWDF: f"{TEST_DIR}/BWDF.lobster.AlN.gz", + CHARGE: f"{TEST_DIR}/CHARGE.lobster.MnO2.gz", + CHARGE_LCFO: f"{TEST_DIR}/CHARGE.LCFO.lobster.ALN.gz", + COBICAR: f"{TEST_DIR}/COBICAR.lobster.B2H6.spin", + COBICAR_LCFO: f"{TEST_DIR}/COHPCAR.LCFO.lobster.NaCl.gz", + COOPCAR: f"{TEST_DIR}/COOPCAR.lobster.gz", + GROSSPOP: f"{TEST_DIR}/GROSSPOP.lobster", + GROSSPOP_LCFO: f"{TEST_DIR}/GROSSPOP.LCFO.lobster.AlN.gz", + ICOHPLIST: f"{TEST_DIR}/ICOHPLIST_511_sp.lobster.AlN.gz", + POLARIZATION: f"{TEST_DIR}/POLARIZATION.lobster.AlN.gz", + BandOverlaps: f"{TEST_DIR}/bandOverlaps.lobster.new.1", + Fatband: f"{TEST_DIR}/Fatband_SiO2/Test_p/FATBAND_o4_2p.lobster", + LobsterOut: f"{TEST_DIR}/lobsterout.normal", + MadelungEnergies: f"{TEST_DIR}/MadelungEnergies.lobster.perovskite", + NcICOBILIST: f"{TEST_DIR}/NcICOBILIST.lobster.nospin.withoutorbitals", + SitePotentials: f"{TEST_DIR}/SitePotentials.lobster.perovskite", + } + self.instances_to_test = [ + DOSCAR( + filename=f"{VASP_OUT_DIR}/DOSCAR.lobster.spin", + ), + DOSCAR_LCFO( + filename=f"{VASP_OUT_DIR}/DOSCAR.LCFO.lobster.AlN", + ), + LobsterMatrices( + filename=f"{TEST_DIR}/Na_hamiltonMatrices.lobster.gz", + efermi=-2.79650354, + ), + Wavefunction( + filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", + structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), + ), + ] + + def test_json_save_load(self, tmp_path): + """Tests saving and loading of all MSONable classes in this package.""" + + def check_msonability(instance: MSONable) -> None: + instance.save(f"{tmp_path}/{instance.__class__.__name__.lower()}.json") + instance_from_json = instance.load(f"{tmp_path}/{instance.__class__.__name__.lower()}.json") + + json1 = json.dumps(instance.as_dict(), cls=MontyEncoder, sort_keys=True) + json2 = json.dumps(instance_from_json.as_dict(), cls=MontyEncoder, sort_keys=True) + assert json1 == json2 + + for attr_name in vars(instance): + assert hasattr(instance_from_json, attr_name) + + for obj, file_path in self.objects_to_test.items(): + instance: MSONable = obj(filename=file_path) + + check_msonability(instance) + + for instance in self.instances_to_test: + check_msonability(instance) + + def test_jsanitize_from_dict(self): + def _equals(a, b) -> bool: + if type(a) is not type(b): + return False + + if isinstance(a, np.ndarray): + return np.array_equal(a, b) + + if isinstance(a, float): + return np.isclose(a, b, atol=1e-4) + + if isinstance(a, dict): + if a.keys() != b.keys(): + return False + return all(_equals(a[k], b[k]) for k in a) + + if isinstance(a, (list, tuple)): + if len(a) != len(b): + return False + return all(_equals(x, y) for x, y in zip(a, b, strict=True)) + + return type(a) is type(b) + + for obj, file_path in self.objects_to_test.items(): + instance: MSONable = obj(filename=file_path) + + jsonable_data = jsanitize(instance, strict=True, allow_bson=True) + + instance_from_dict = obj.from_dict(jsonable_data) + + for attr_name in vars(instance): + assert hasattr(instance_from_dict, attr_name) + + original = getattr(instance, attr_name) + restored = getattr(instance_from_dict, attr_name) + + assert _equals(restored, original) + + for instance in self.instances_to_test: + jsonable_data = jsanitize(instance, strict=True, allow_bson=True) + + instance_from_dict = instance.from_dict(jsonable_data) + + for attr_name in vars(instance): + assert hasattr(instance_from_dict, attr_name) + + original = getattr(instance, attr_name) + restored = getattr(instance_from_dict, attr_name) + + assert _equals(restored, original) From 94df814c203f9fa53e1e8a93d3a5c672e476c86d Mon Sep 17 00:00:00 2001 From: naik-aakash Date: Wed, 25 Feb 2026 16:32:09 +0100 Subject: [PATCH 174/180] move lobsterenv module --- future/lobsterenv.py | 1521 +--------------------------------- lobsterenv.py | 1839 +----------------------------------------- 2 files changed, 10 insertions(+), 3350 deletions(-) diff --git a/future/lobsterenv.py b/future/lobsterenv.py index 1f4d5ffcae..8662155c52 100644 --- a/future/lobsterenv.py +++ b/future/lobsterenv.py @@ -1,1523 +1,12 @@ -"""This module provides classes to perform analyses of the local -environments (e.g., finding near neighbors) of single sites in molecules -and structures based on bonding analysis with LOBSTER. - -If you use this module, please cite: -J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, -"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", -ChemPlusChem 2022, e202200123, -DOI: 10.1002/cplu.202200123. -""" +"""Deprecated class for analyzing NearNeighbors using ICOHPs/ICOOPs/ICOBIs.""" from __future__ import annotations -import collections -import copy -import math -import tempfile -from typing import TYPE_CHECKING, NamedTuple - -import matplotlib as mpl -import numpy as np from monty.dev import deprecated -from pymatgen.analysis.bond_valence import BVAnalyzer -from pymatgen.analysis.chemenv.coordination_environments.coordination_geometry_finder import LocalGeometryFinder -from pymatgen.analysis.chemenv.coordination_environments.structure_environments import LightStructureEnvironments -from pymatgen.analysis.local_env import NearNeighbors -from pymatgen.electronic_structure.cohp import CompleteCohp -from pymatgen.electronic_structure.core import Spin -from pymatgen.electronic_structure.plotter import CohpPlotter -from pymatgen.io.lobster import Charge, Icohplist -from pymatgen.util.due import Doi, due - -if TYPE_CHECKING: - from typing import Any, Literal, Self - - import matplotlib as mpl - from numpy.typing import NDArray - from pymatgen.core import IStructure, PeriodicNeighbor, PeriodicSite, Structure - from pymatgen.core.periodic_table import Element - from pymatgen.electronic_structure.cohp import IcohpCollection, IcohpValue - from pymatgen.util.typing import PathLike - -__author__ = "Janine George" -__copyright__ = "Copyright 2021, The Materials Project" -__version__ = "1.0" -__maintainer__ = "J. George" -__email__ = "janinegeorge.ulfen@gmail.com" -__status__ = "Production" -__date__ = "February 2, 2021" - -due.cite( - Doi("10.1002/cplu.202200123"), - description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +@deprecated( + replacement="`pymatgen.analysis.lobster_env.LobsterNeighbors`", category=DeprecationWarning, deadline=(2026, 3, 31) ) - - -class LobsterNeighbors(NearNeighbors): - """ - This class combines capabilities from LocalEnv and ChemEnv to determine - coordination environments based on bonding analysis. - """ - - def __init__( - self, - structure: Structure | IStructure, - filename_icohp: PathLike | None = "ICOHPLIST.lobster", - obj_icohp: Icohplist | None = None, - are_coops: bool = False, - are_cobis: bool = False, - valences: list[float] | None = None, - limits: tuple[float, float] | None = None, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, - only_bonds_to: list[str] | None = None, - perc_strength_icohp: float = 0.15, - noise_cutoff: float = 0.1, - valences_from_charges: bool = False, - filename_charge: PathLike | None = None, - obj_charge: Charge | None = None, - which_charge: Literal["Mulliken", "Loewdin"] = "Mulliken", - adapt_extremum_to_add_cond: bool = False, - add_additional_data_sg: bool = False, - filename_blist_sg1: PathLike | None = None, - filename_blist_sg2: PathLike | None = None, - id_blist_sg1: Literal["icoop", "icobi"] = "icoop", - id_blist_sg2: Literal["icoop", "icobi"] = "icobi", - ) -> None: - """ - Args: - filename_icohp (PathLike): Path to ICOHPLIST.lobster or - ICOOPLIST.lobster or ICOBILIST.lobster. - obj_icohp (Icohplist): Icohplist object. - structure (Structure): Typically constructed by Structure.from_file("POSCAR"). - are_coops (bool): Whether the file is a ICOOPLIST.lobster (True) or a - ICOHPLIST.lobster (False). Only tested for ICOHPLIST.lobster so far. - are_cobis (bool): Whether the file is a ICOBILIST.lobster (True) or - a ICOHPLIST.lobster (False). - valences (list[float]): Valence/charge for each element. - limits (tuple[float, float]): Range to decide which ICOHPs (ICOOP - or ICOBI) should be considered. - additional_condition (int): Additional condition that decides - which kind of bonds will be considered: - 0 - NO_ADDITIONAL_CONDITION - 1 - ONLY_ANION_CATION_BONDS - 2 - NO_ELEMENT_TO_SAME_ELEMENT_BONDS - 3 - ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - 4 - ONLY_ELEMENT_TO_OXYGEN_BONDS - 5 - DO_NOT_CONSIDER_ANION_CATION_BONDS - 6 - ONLY_CATION_CATION_BONDS - only_bonds_to (list[str]): Only consider bonds to certain elements (e.g. ["O"] for oxygen). - perc_strength_icohp (float): If no "limits" are given, this will decide - which ICOHPs will be considered (relative to the strongest ICOHP/ICOOP/ICOBI). - noise_cutoff (float): The lower limit of ICOHPs considered. - valences_from_charges (bool): If True and path to CHARGE.lobster is provided, - will use LOBSTER charges (Mulliken) instead of valences. - filename_charge (PathLike): Path to Charge.lobster. - obj_charge (Charge): Charge object. - which_charge ("Mulliken" | "Loewdin"): Source of charge. - adapt_extremum_to_add_cond (bool): Whether to adapt the limits to only - focus on the bonds determined by the additional condition. - add_additional_data_sg (bool): Add the information from filename_add_bondinglist_sg1. - filename_blist_sg1 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. - filename_blist_sg2 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. - id_blist_sg1 ("icoop" | "icobi"): Identity of data in filename_blist_sg1. - id_blist_sg2 ("icoop" | "icobi"): Identity of data in filename_blist_sg2. - """ - if filename_icohp is not None: - self.ICOHP = Icohplist(are_coops=are_coops, are_cobis=are_cobis, filename=filename_icohp) - elif obj_icohp is not None: - self.ICOHP = obj_icohp - else: - raise ValueError("Please provide either filename_icohp or obj_icohp") - - self.Icohpcollection = self.ICOHP.icohpcollection - self.structure = structure - self.limits = limits - self.only_bonds_to = only_bonds_to - self.adapt_extremum_to_add_cond = adapt_extremum_to_add_cond - self.are_coops = are_coops - self.are_cobis = are_cobis - self.add_additional_data_sg = add_additional_data_sg - self.filename_blist_sg1 = filename_blist_sg1 - self.filename_blist_sg2 = filename_blist_sg2 - self.noise_cutoff = noise_cutoff - - self.id_blist_sg1 = id_blist_sg1.lower() - self.id_blist_sg2 = id_blist_sg2.lower() - - allowed_arguments = {"icoop", "icobi"} - if self.id_blist_sg1 not in allowed_arguments or self.id_blist_sg2 not in allowed_arguments: - raise ValueError("Algorithm can only work with ICOOPs, ICOBIs") - - if add_additional_data_sg: - if self.id_blist_sg1 == "icoop": - are_coops_id1 = True - are_cobis_id1 = False - else: - are_coops_id1 = False - are_cobis_id1 = True - - self.bonding_list_1 = Icohplist( - filename=self.filename_blist_sg1, - are_coops=are_coops_id1, - are_cobis=are_cobis_id1, - ) - - if self.id_blist_sg2 == "icoop": - are_coops_id2 = True - are_cobis_id2 = False - else: - are_coops_id2 = False - are_cobis_id2 = True - - self.bonding_list_2 = Icohplist( - filename=self.filename_blist_sg2, - are_coops=are_coops_id2, - are_cobis=are_cobis_id2, - ) - - # Check the additional condition - if additional_condition not in range(7): - raise ValueError(f"Unexpected {additional_condition=}, must be one of {list(range(7))}") - self.additional_condition = additional_condition - - # Read in valences, will prefer manual setting of valences - self.valences: list[float] | None - if valences is None: - if valences_from_charges and filename_charge is not None: - chg = Charge(filename=filename_charge) - if which_charge == "Mulliken": - self.valences = chg.mulliken - elif which_charge == "Loewdin": - self.valences = chg.loewdin - - elif valences_from_charges and obj_charge is not None: - chg = obj_charge - if which_charge == "Mulliken": - self.valences = chg.mulliken - elif which_charge == "Loewdin": - self.valences = chg.loewdin - - else: - bv_analyzer = BVAnalyzer() - try: - self.valences = bv_analyzer.get_valences(structure=self.structure) # type:ignore[arg-type] - except ValueError as exc: - self.valences = None - if additional_condition in {1, 3, 5, 6}: - raise ValueError( - "Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work" - ) from exc - else: - self.valences = valences - - if np.allclose(self.valences or [], np.zeros_like(self.valences)) and additional_condition in {1, 3, 5, 6}: - raise ValueError("All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work") - - if limits is None: - self.lowerlimit = self.upperlimit = None - else: - self.lowerlimit, self.upperlimit = limits - - # Evaluate coordination environments - self._evaluate_ce( - lowerlimit=self.lowerlimit, - upperlimit=self.upperlimit, - only_bonds_to=only_bonds_to, - additional_condition=self.additional_condition, - perc_strength_icohp=perc_strength_icohp, - adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, - ) - - @property - def structures_allowed(self) -> Literal[True]: - """Whether this LobsterNeighbors class can be used with Structure objects.""" - return True - - @property - def molecules_allowed(self) -> Literal[False]: - """Whether this LobsterNeighbors class can be used with Molecule objects.""" - return False - - @property - def anion_types(self) -> set[Element]: - """The set of anion types in crystal structure. - - Returns: - set[Element]: Anions in the crystal structure. - """ - if self.valences is None: - raise ValueError("No cations and anions defined") - - anion_species = [] - for site, val in zip(self.structure, self.valences, strict=True): - if val < 0.0: - anion_species.append(site.specie) - - return set(anion_species) - - @deprecated(anion_types) - def get_anion_types(self) -> set[Element]: - return self.anion_types - - def get_nn_info( - self, - structure: Structure | IStructure, - n: int, - use_weights: bool = False, - ) -> dict[str, Any]: - """Get coordination number (CN) of site by index. - - Args: - structure (Structure): Input structure. - n (int): Index of site for which to determine CN. - use_weights (bool): Whether to use weights for computing - the CN (True), or each coordinated site has equal weight (False). - The former is not implemented yet. - - Raises: - ValueError: If use_weights is True, or if arg "structure" and structure - used to initialize LobsterNeighbors have different lengths. - - Returns: - dict[str, Any]: coordination number and a list of nearest neighbors. - """ - if use_weights: - raise ValueError("LobsterEnv cannot use weights") - - if len(structure) != len(self.structure): - raise ValueError( - f"Length of structure ({len(structure)}) and LobsterNeighbors ({len(self.structure)}) differ" - ) - - return self.sg_list[n] # type: ignore[return-value] - - def get_light_structure_environment( - self, - only_cation_environments: bool = False, - only_indices: list[int] | None = None, - ) -> LobsterLightStructureEnvironments: - """Get a LobsterLightStructureEnvironments object if the structure - only contains coordination environments smaller 13. - - Args: - only_cation_environments (bool): Only return data for cations. - only_indices (list[int]): Only evaluate indexes in this list. - - Returns: - LobsterLightStructureEnvironments - """ - lgf = LocalGeometryFinder() - lgf.setup_structure(structure=self.structure) # type:ignore[arg-type] - list_ce_symbols = [] - list_csm = [] - list_permut = [] - for idx, _neigh_coords in enumerate(self.list_coords): - if (len(_neigh_coords)) > 13: - raise ValueError("Environment cannot be determined. Number of neighbors is larger than 13.") - # Avoid problems if _neigh_coords is empty - if _neigh_coords != []: - lgf.setup_local_geometry(isite=idx, coords=_neigh_coords, optimization=2) - cncgsm = lgf.get_coordination_symmetry_measures(optimization=2) - list_ce_symbols.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[0]) - list_csm.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["csm_wcs_ctwcc"]) - list_permut.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["indices"]) - else: - list_ce_symbols.append(None) - list_csm.append(None) - list_permut.append(None) - - new_list_ce_symbols = [] - new_list_csm = [] - new_list_permut = [] - new_list_neighsite = [] - new_list_neighisite = [] - - if only_indices is None: - if not only_cation_environments: - return LobsterLightStructureEnvironments.from_Lobster( - list_ce_symbol=list_ce_symbols, - list_csm=list_csm, - list_permutation=list_permut, - list_neighsite=self.list_neighsite, # type:ignore[arg-type] - list_neighisite=self.list_neighisite, - structure=self.structure, - valences=self.valences, - ) - - if self.valences is None: - raise ValueError(f"{self.valences=}") - - for idx, val in enumerate(self.valences): - if val >= 0.0: - new_list_ce_symbols.append(list_ce_symbols[idx]) - new_list_csm.append(list_csm[idx]) - new_list_permut.append(list_permut[idx]) - new_list_neighisite.append(self.list_neighisite[idx]) - new_list_neighsite.append(self.list_neighsite[idx]) - else: - new_list_ce_symbols.append(None) - new_list_csm.append(None) - new_list_permut.append([]) - new_list_neighisite.append([]) - new_list_neighsite.append([]) - - else: - for site_idx, _site in enumerate(self.structure): - if site_idx in only_indices: - new_list_ce_symbols.append(list_ce_symbols[site_idx]) - new_list_csm.append(list_csm[site_idx]) - new_list_permut.append(list_permut[site_idx]) - new_list_neighisite.append(self.list_neighisite[site_idx]) - new_list_neighsite.append(self.list_neighsite[site_idx]) - else: - new_list_ce_symbols.append(None) - new_list_csm.append(None) - new_list_permut.append([]) - new_list_neighisite.append([]) - new_list_neighsite.append([]) - - return LobsterLightStructureEnvironments.from_Lobster( - list_ce_symbol=new_list_ce_symbols, - list_csm=new_list_csm, - list_permutation=new_list_permut, - list_neighsite=new_list_neighsite, # type:ignore[arg-type] - list_neighisite=new_list_neighisite, - structure=self.structure, - valences=self.valences, - ) - - def get_info_icohps_to_neighbors( - self, - isites: list[int] | None = None, - onlycation_isites: bool = True, - ) -> ICOHPNeighborsInfo: - """Get information on the ICOHPs of neighbors for certain sites - as identified by their site id. - - This is useful for plotting the COHPs (ICOOPLIST.lobster/ - ICOHPLIST.lobster/ICOBILIST.lobster) of a site in the structure. - - - Args: - isites (list[int]): Site IDs. If is None, all isites will be used - to add the ICOHPs of the neighbors. - onlycation_isites (bool): If True and if isite is None, will - only analyse the cations sites. - - Returns: - ICOHPNeighborsInfo - """ - if self.valences is None and onlycation_isites: - raise ValueError("No valences are provided") - - if isites is None: - if onlycation_isites: - if self.valences is None: - raise ValueError(f"{self.valences}=") - - isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] - else: - isites = list(range(len(self.structure))) - - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - summed_icohps: float = 0.0 - list_icohps: list[float] = [] - number_bonds: int = 0 - labels: list[str] = [] - atoms: list[list[str]] = [] - final_isites: list[int] = [] - for idx, _site in enumerate(self.structure): - if idx in isites: - for key, icohpsum in zip(self.list_keys[idx], self.list_icohps[idx], strict=True): - summed_icohps += icohpsum - list_icohps.append(icohpsum) # type:ignore[arg-type] - labels.append(key) - atoms.append( - [ - self.Icohpcollection._list_atom1[int(key) - 1], - self.Icohpcollection._list_atom2[int(key) - 1], - ] - ) - number_bonds += 1 - final_isites.append(idx) - return ICOHPNeighborsInfo(summed_icohps, list_icohps, number_bonds, labels, atoms, final_isites) - - def plot_cohps_of_neighbors( - self, - path_to_cohpcar: PathLike | None = "COHPCAR.lobster", - obj_cohpcar: CompleteCohp | None = None, - isites: list[int] | None = None, - onlycation_isites: bool = True, - only_bonds_to: list[str] | None = None, - per_bond: bool = False, - summed_spin_channels: bool = False, - xlim: tuple[float, float] | None = None, - ylim: tuple[float, float] = (-10, 6), - integrated: bool = False, - ) -> mpl.axes.Axes: - """Plot summed COHPs or COBIs or COOPs. - - Please be careful in the spin polarized case (plots might overlap). - - Args: - path_to_cohpcar (PathLike): Path to COHPCAR or COOPCAR or COBICAR. - obj_cohpcar (CompleteCohp): CompleteCohp object - isites (list[int]): Site IDs. If empty, all sites will be used to add the ICOHPs of the neighbors. - onlycation_isites (bool): Only use cations, if isite is empty. - only_bonds_to (list[str]): Only anions in this list will be considered. - per_bond (bool): Whether to plot a normalization of the plotted COHP - per number of bond (True), or the sum (False). - xlim (tuple[float, float]): Limits of x values. - ylim (tuple[float, float]): Limits of y values. - integrated (bool): Whether to show integrated COHP instead of COHP. - - Returns: - plt of the COHPs or COBIs or COOPs. - """ - cp = CohpPlotter(are_cobis=self.are_cobis, are_coops=self.are_coops) - - plotlabel, summed_cohp = self.get_info_cohps_to_neighbors( - path_to_cohpcar, - obj_cohpcar, - isites, - only_bonds_to, - onlycation_isites, - per_bond, - summed_spin_channels=summed_spin_channels, - ) - - cp.add_cohp(plotlabel, summed_cohp) - ax = cp.get_plot(integrated=integrated) - if xlim is not None: - ax.set_xlim(xlim) - - if ylim is not None: - ax.set_ylim(ylim) - - return ax - - def get_info_cohps_to_neighbors( - self, - path_to_cohpcar: PathLike | None = "COHPCAR.lobster", - obj_cohpcar: CompleteCohp | None = None, - isites: list[int] | None = None, - only_bonds_to: list[str] | None = None, - onlycation_isites: bool = True, - per_bond: bool = True, - summed_spin_channels: bool = False, - ) -> tuple[str | None, CompleteCohp | None]: - """Get the COHPs (COOPs or COBIs) as a summed Cohp object - and a label from all sites mentioned in isites with neighbors. - - Args: - path_to_cohpcar (PathLike): Path to COHPCAR/COOPCAR/COBICAR. - obj_cohpcar (CompleteCohp): CompleteCohp object. - isites (list[int]): The indexes of the sites. - only_bonds_to (list[str]): Only show COHPs to selected element, e.g. ["O"]. - onlycation_isites (bool): If isites is None, only cation sites will be returned. - per_bond (bool): Whether to normalize per bond. - summed_spin_channels (bool): Whether to sum both spin channels. - - Returns: - str: Label for COHP. - CompleteCohp: Describe all COHPs/COOPs/COBIs of the sites - as given by isites and the other arguments. - """ - # TODO: add options for orbital-resolved COHPs - _summed_icohps, _list_icohps, _number_bonds, labels, atoms, final_isites = self.get_info_icohps_to_neighbors( - isites=isites, onlycation_isites=onlycation_isites - ) - - with tempfile.TemporaryDirectory() as tmp_dir: - path = f"{tmp_dir}/POSCAR.vasp" - - self.structure.to(filename=path, fmt="poscar") - - if not hasattr(self, "completecohp"): - if path_to_cohpcar is not None and obj_cohpcar is None: - self.completecohp = CompleteCohp.from_file( - fmt="LOBSTER", - filename=path_to_cohpcar, - structure_file=path, - are_coops=self.are_coops, - are_cobis=self.are_cobis, - ) - elif obj_cohpcar is not None: - self.completecohp = obj_cohpcar - else: - raise ValueError("Please provide either path_to_cohpcar or obj_cohpcar") - - # Check that the number of bonds in ICOHPLIST and COHPCAR are identical - # TODO: Further checks could be implemented - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - if len(self.Icohpcollection._list_atom1) != len(self.completecohp.bonds): - raise ValueError("COHPCAR and ICOHPLIST do not fit together") - - is_spin_completecohp = Spin.down in self.completecohp.get_cohp_by_label("1").cohp - if self.Icohpcollection.is_spin_polarized != is_spin_completecohp: - raise ValueError("COHPCAR and ICOHPLIST do not fit together") - - if only_bonds_to is None: - # Sort by anion type - divisor = len(labels) if per_bond else 1 - - plot_label = self._get_plot_label(atoms, per_bond) - summed_cohp = self.completecohp.get_summed_cohp_by_label_list( - label_list=labels, - divisor=divisor, - summed_spin_channels=summed_spin_channels, - ) - - else: - # Labels of the COHPs that will be summed - # Iterate through labels and atoms and check which bonds can be included - new_labels = [] - new_atoms = [] - if final_isites is None: - raise ValueError(f"{final_isites=}") - - for key, atompair, isite in zip(labels, atoms, final_isites, strict=True): - present = False - for atomtype in only_bonds_to: - # This is necessary to identify also bonds between the same elements correctly - if str(self.structure[isite].species.elements[0]) != atomtype: - if atomtype in { - self._split_string(atompair[0])[0], - self._split_string(atompair[1])[0], - }: - present = True - elif ( - atomtype == self._split_string(atompair[0])[0] - and atomtype == self._split_string(atompair[1])[0] - ): - present = True - - if present: - new_labels.append(key) - new_atoms.append(atompair) - if new_labels: - divisor = len(new_labels) if per_bond else 1 - - plot_label = self._get_plot_label(new_atoms, per_bond) - summed_cohp = self.completecohp.get_summed_cohp_by_label_list( - label_list=new_labels, - divisor=divisor, - summed_spin_channels=summed_spin_channels, - ) - else: - plot_label = None - - summed_cohp = None - - return plot_label, summed_cohp # type:ignore[return-value] - - def _get_plot_label(self, atoms: list[list[str]], per_bond: bool) -> str: - """Count the types of bonds and append a label.""" - all_labels = [] - for atoms_names in atoms: - new = [ - self._split_string(atoms_names[0])[0], - self._split_string(atoms_names[1])[0], - ] - new.sort() - string_here = f"{new[0]}-{new[1]}" - all_labels.append(string_here) - - counter = collections.Counter(all_labels) - plotlabels = [f"{item} x {key}" for key, item in counter.items()] - label = ", ".join(plotlabels) - if per_bond: - label += " (per bond)" - return label - - def get_info_icohps_between_neighbors( - self, - isites: list[int] | None = None, - onlycation_isites: bool = True, - ) -> ICOHPNeighborsInfo: - """Get interactions between neighbors of certain sites. - - Args: - isites (list[int]): Site IDs. If is None, all sites will be used. - onlycation_isites (bool): Only use cations, if isite is None. - - Returns: - ICOHPNeighborsInfo - """ - lowerlimit = self.lowerlimit - upperlimit = self.upperlimit - - if self.valences is None and onlycation_isites: - raise ValueError("No valences are provided") - - if isites is None: - if onlycation_isites: - if self.valences is None: - raise ValueError(f"{self.valences=}") - - isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] - else: - isites = list(range(len(self.structure))) - - summed_icohps: float = 0.0 - list_icohps: list[float] = [] - number_bonds: int = 0 - labels: list[str] = [] - atoms: list[list[str]] = [] - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - for isite in isites: - for site1_idx, n_site in enumerate(self.list_neighsite[isite]): - for site2_idx, n_site2 in enumerate(self.list_neighsite[isite]): - if site1_idx < site2_idx: - unitcell1 = self._determine_unit_cell(n_site) - unitcell2 = self._determine_unit_cell(n_site2) - - index_n_site = self._get_original_site(self.structure, n_site) # type:ignore[arg-type] - index_n_site2 = self._get_original_site(self.structure, n_site2) # type:ignore[arg-type] - - if index_n_site < index_n_site2: - translation = list(np.array(unitcell1) - np.array(unitcell2)) - elif index_n_site2 < index_n_site: - translation = list(np.array(unitcell2) - np.array(unitcell1)) - else: - translation = list(np.array(unitcell1) - np.array(unitcell2)) - - icohps = self._get_icohps( - icohpcollection=self.Icohpcollection, - site_idx=index_n_site, - lowerlimit=lowerlimit, - upperlimit=upperlimit, - only_bonds_to=self.only_bonds_to, - ) - - done = False - for icohp in icohps.values(): - atomnr1 = self._get_atomnumber(icohp._atom1) - atomnr2 = self._get_atomnumber(icohp._atom2) - label = icohp._label - - if (index_n_site == atomnr1 and index_n_site2 == atomnr2) or ( - index_n_site == atomnr2 and index_n_site2 == atomnr1 - ): - if atomnr1 != atomnr2: - if np.all(np.asarray(translation) == np.asarray(icohp._translation)): - summed_icohps += icohp.summed_icohp - list_icohps.append(icohp.summed_icohp) - number_bonds += 1 - labels.append(label) - atoms.append( - [ - self.Icohpcollection._list_atom1[int(label) - 1], - self.Icohpcollection._list_atom2[int(label) - 1], - ] - ) - - elif not done: - icohp_trans = -np.asarray( - [ - icohp._translation[0], - icohp._translation[1], - icohp._translation[2], - ] - ) - - if (np.all(np.asarray(translation) == np.asarray(icohp._translation))) or ( - np.all(np.asarray(translation) == icohp_trans) - ): - summed_icohps += icohp.summed_icohp - list_icohps.append(icohp.summed_icohp) - number_bonds += 1 - labels.append(label) - atoms.append( - [ - self.Icohpcollection._list_atom1[int(label) - 1], - self.Icohpcollection._list_atom2[int(label) - 1], - ] - ) - done = True - - return ICOHPNeighborsInfo(summed_icohps, list_icohps, number_bonds, labels, atoms, None) - - def _evaluate_ce( - self, - lowerlimit: float | None, - upperlimit: float | None, - only_bonds_to: list[str] | None = None, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, - perc_strength_icohp: float = 0.15, - adapt_extremum_to_add_cond: bool = False, - ) -> None: - """ - Args: - lowerlimit (float): Lower limit which determines the ICOHPs - that are considered for the determination of the neighbors. - upperlimit (float): Upper limit which determines the ICOHPs - that are considered for the determination of the neighbors. - only_bonds_to (list[str]): Restrict the types of bonds that will be considered. - additional_condition (int): Additional condition for the evaluation. - perc_strength_icohp (float): Determine how strong the ICOHPs - (percentage * strongest_ICOHP) will be that are still considered. - adapt_extremum_to_add_cond (bool): Whether to recalculate the limit - based on the bonding type and not on the overall extremum. - """ - # Get extremum - if lowerlimit is None and upperlimit is None: - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - limits = self._get_limit_from_extremum( - self.Icohpcollection, - percentage=perc_strength_icohp, - adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, - additional_condition=additional_condition, - ) - - if limits is None: - raise ValueError(f"{limits=}") - lowerlimit, upperlimit = limits - - elif upperlimit is None or lowerlimit is None: - raise ValueError("Please give two limits or leave them both at None") - - # Find environments based on ICOHP values - ( - list_icohps, - list_keys, - list_lengths, - list_neighisite, - list_neighsite, - list_coords, - ) = self._find_environments(additional_condition, lowerlimit, upperlimit, only_bonds_to) - - self.list_icohps = list_icohps - self.list_lengths = list_lengths - self.list_keys = list_keys - self.list_neighsite = list_neighsite - self.list_neighisite = list_neighisite - self.list_coords = list_coords - - # Make a structure graph - # Make sure everything is relative to the given Structure and - # not just the atoms in the unit cell - if self.add_additional_data_sg: - if self.bonding_list_1.icohpcollection is None: - raise ValueError(f"{self.bonding_list_1.icohpcollection=}") - if self.bonding_list_2.icohpcollection is None: - raise ValueError(f"{self.bonding_list_2.icohpcollection=}") - - self.sg_list = [ - [ - { - "site": neighbor, - "image": tuple( - round(idx) - for idx in ( - neighbor.frac_coords - - self.structure[ - next( - site_idx - for site_idx, site in enumerate(self.structure) - if neighbor.is_periodic_image(site) - ) - ].frac_coords - ) - ), - "weight": 1, - # Here, the ICOBIs and ICOOPs are added based on the bond - # strength cutoff of the ICOHP - # More changes are necessary here if we use ICOBIs for cutoffs - "edge_properties": { - "ICOHP": self.list_icohps[neighbors_idx][nbr_idx], - "bond_length": self.list_lengths[neighbors_idx][nbr_idx], - "bond_label": self.list_keys[neighbors_idx][nbr_idx], - self.id_blist_sg1.upper(): self.bonding_list_1.icohpcollection.get_icohp_by_label( - self.list_keys[neighbors_idx][nbr_idx] - ), - self.id_blist_sg2.upper(): self.bonding_list_2.icohpcollection.get_icohp_by_label( - self.list_keys[neighbors_idx][nbr_idx] - ), - }, - "site_index": next( - site_idx for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) - ), - } - for nbr_idx, neighbor in enumerate(neighbors) - ] - for neighbors_idx, neighbors in enumerate(self.list_neighsite) - ] - else: - self.sg_list = [ - [ - { - "site": neighbor, - "image": tuple( - round(idx) - for idx in ( - neighbor.frac_coords - - self.structure[ - next( - site_idx - for site_idx, site in enumerate(self.structure) - if neighbor.is_periodic_image(site) - ) - ].frac_coords - ) - ), - "weight": 1, - "edge_properties": { - "ICOHP": self.list_icohps[neighbors_idx][nbr_idx], - "bond_length": self.list_lengths[neighbors_idx][nbr_idx], - "bond_label": self.list_keys[neighbors_idx][nbr_idx], - }, - "site_index": next( - site_idx for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) - ), - } - for nbr_idx, neighbor in enumerate(neighbors) - ] - for neighbors_idx, neighbors in enumerate(self.list_neighsite) - ] - - def _find_environments( - self, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], - lowerlimit: float, - upperlimit: float, - only_bonds_to: list[str] | None, - ) -> tuple[ - list[list[IcohpValue]], - list[list[str]], - list[list[float]], - list[list[int]], - list[list[PeriodicNeighbor]], - list[list[NDArray]], - ]: - """Find all relevant neighbors based on certain restrictions. - - Args: - additional_condition (int): Additional condition. - lowerlimit (float): Lower limit that ICOHPs are considered. - upperlimit (float): Upper limit that ICOHPs are considered. - only_bonds_to (list[str]): Only bonds to these elements will be considered. - - Returns: - Tuple of ICOHPs, keys, lengths, neighisite, neighsite, coords. - """ - list_icohps: list[list[float]] = [] - list_keys: list[list[str]] = [] - list_lengths: list[list[float]] = [] - list_neighisite: list[list[int]] = [] - list_neighsite: list[list[PeriodicNeighbor]] = [] - list_coords: list[list[NDArray]] = [] - - # Run over structure - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - for idx, site in enumerate(self.structure): - icohps = self._get_icohps( - icohpcollection=self.Icohpcollection, - site_idx=idx, - lowerlimit=lowerlimit, - upperlimit=upperlimit, - only_bonds_to=only_bonds_to, - ) - - additional_conds = self._find_relevant_atoms_additional_condition(idx, icohps, additional_condition) - ( - keys_from_ICOHPs, - lengths_from_ICOHPs, - neighbors_from_ICOHPs, - selected_ICOHPs, - ) = additional_conds - - if len(neighbors_from_ICOHPs) > 0: - centralsite = site - - neighbors_by_distance_start = self.structure.get_sites_in_sphere( - pt=centralsite.coords, - r=np.max(lengths_from_ICOHPs) + 0.5, - include_image=True, - include_index=True, - ) - - neighbors_by_distance = [] - list_distances = [] - index_here_list = [] - coords = [] - for neigh_new in sorted(neighbors_by_distance_start, key=lambda x: x[1]): - site_here = neigh_new[0].to_unit_cell() - index_here = neigh_new[2] - index_here_list.append(index_here) - cell_here = neigh_new[3] - new_coords = [ - site_here.frac_coords[0] + float(cell_here[0]), - site_here.frac_coords[1] + float(cell_here[1]), - site_here.frac_coords[2] + float(cell_here[2]), - ] - coords.append(site_here.lattice.get_cartesian_coords(new_coords)) - - # new_site = PeriodicSite( - # species=site_here.species_string, - # coords=site_here.lattice.get_cartesian_coords(new_coords), - # lattice=site_here.lattice, - # to_unit_cell=False, - # coords_are_cartesian=True, - # ) - neighbors_by_distance.append(neigh_new[0]) - list_distances.append(neigh_new[1]) - _list_neighsite = [] - _list_neighisite = [] - copied_neighbors_from_ICOHPs = copy.copy(neighbors_from_ICOHPs) - copied_distances_from_ICOHPs = copy.copy(lengths_from_ICOHPs) - _neigh_coords = [] - _neigh_frac_coords = [] - - for neigh_idx, neigh in enumerate(neighbors_by_distance): - index_here2 = index_here_list[neigh_idx] - - for dist_idx, dist in enumerate(copied_distances_from_ICOHPs): - if ( - np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) - and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 - ): - _list_neighsite.append(neigh) - _list_neighisite.append(index_here2) - _neigh_coords.append(coords[neigh_idx]) - _neigh_frac_coords.append(neigh.frac_coords) - del copied_distances_from_ICOHPs[dist_idx] - del copied_neighbors_from_ICOHPs[dist_idx] - break - - list_neighisite.append(_list_neighisite) - list_neighsite.append(_list_neighsite) - list_lengths.append(lengths_from_ICOHPs) - list_keys.append(keys_from_ICOHPs) - list_coords.append(_neigh_coords) - list_icohps.append(selected_ICOHPs) - - else: - list_neighsite.append([]) - list_neighisite.append([]) - list_icohps.append([]) - list_lengths.append([]) - list_keys.append([]) - list_coords.append([]) - return ( # type:ignore[return-value] - list_icohps, - list_keys, - list_lengths, - list_neighisite, - list_neighsite, - list_coords, - ) - - def _find_relevant_atoms_additional_condition( - self, - site_idx: int, - icohps: dict[str, IcohpValue], - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], - ) -> tuple[list[str], list[float], list[int], list[float]]: - """Find all relevant atoms that fulfill the additional condition. - - Args: - site_idx (int): Site index in structure (start from 0). - icohps (dict[str, IcohpValue]): ICOHP values. - additional_condition (int): Additional condition. - - Returns: - tuple: keys, lengths, neighbors from selected ICOHPs and selected ICOHPs. - """ - keys_from_ICOHPs: list[str] = [] - lengths_from_ICOHPs: list[float] = [] - neighbors_from_ICOHPs: list[int] = [] - icohps_from_ICOHPs: list[float] = [] - - for key, icohp in icohps.items(): - atomnr1 = self._get_atomnumber(icohp._atom1) - atomnr2 = self._get_atomnumber(icohp._atom2) - - # Check additional conditions - val1 = val2 = None - if self.valences is None: - raise ValueError(f"{self.valences=}") - if additional_condition in {1, 3, 5, 6}: - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - # NO_ADDITIONAL_CONDITION - if additional_condition == 0: - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # ONLY_ANION_CATION_BONDS - elif additional_condition == 1: - if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): # type: ignore[operator] - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # NO_ELEMENT_TO_SAME_ELEMENT_BONDS - elif additional_condition == 2: - if icohp._atom1.rstrip("0123456789") != icohp._atom2.rstrip("0123456789"): - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - elif additional_condition == 3: - if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and icohp._atom1.rstrip( # type: ignore[operator] - "0123456789" - ) != icohp._atom2.rstrip("0123456789"): - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # ONLY_ELEMENT_TO_OXYGEN_BONDS - elif additional_condition == 4: - if icohp._atom1.rstrip("0123456789") == "O" or icohp._atom2.rstrip("0123456789") == "O": - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # DO_NOT_CONSIDER_ANION_CATION_BONDS - elif additional_condition == 5: - if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): # type: ignore[operator] - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # ONLY_CATION_CATION_BONDS - elif additional_condition == 6 and val1 > 0.0 and val2 > 0.0: # type: ignore[operator] - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - return ( - keys_from_ICOHPs, - lengths_from_ICOHPs, - neighbors_from_ICOHPs, - icohps_from_ICOHPs, - ) - - @staticmethod - def _get_icohps( - icohpcollection: IcohpCollection, - site_idx: int, - lowerlimit: float | None, - upperlimit: float | None, - only_bonds_to: list[str] | None, - ) -> dict[str, IcohpValue]: - """Get ICOHP dict for certain site. - - Args: - icohpcollection (IcohpCollection): IcohpCollection object. - site_idx (int): Site index. - lowerlimit (float): Lower limit that ICOHPs are considered. - upperlimit (float): Upper limit that ICOHPs are considered. - only_bonds_to (list[str]): Only bonds to these elements will be considered, e.g. ["O"]. - - Returns: - dict of IcohpValues. The keys correspond to the initial list_labels. - """ - return icohpcollection.get_icohp_dict_of_site( - site=site_idx, - maxbondlength=6.0, - minsummedicohp=lowerlimit, - maxsummedicohp=upperlimit, - only_bonds_to=only_bonds_to, - ) - - @staticmethod - def _get_atomnumber(atomstring: str) -> int: - """Get the index of the atom within the POSCAR (e.g., Return 0 for "Na1"). - - Args: - atomstring (str): Atom as str, such as "Na1". - - Returns: - int: Index of the atom in the POSCAR. - """ - return int(LobsterNeighbors._split_string(atomstring)[1]) - 1 - - @staticmethod - def _split_string(s) -> tuple[str, str]: - """Split strings such as "Na1" into ["Na", "1"] and return "1". - - Args: - s (str): String to split. - """ - head = s.rstrip("0123456789") - tail = s[len(head) :] - return head, tail - - @staticmethod - def _determine_unit_cell(site: PeriodicSite) -> list[int]: - """Determine the unit cell based on the site. - - Args: - site (PeriodicSite): The site. - """ - unitcell = [] - for coord in site.frac_coords: - value = math.floor(round(coord, 4)) - unitcell.append(value) - - return unitcell - - def _adapt_extremum_to_add_cond( - self, - list_icohps: list[float], - percentage: float, - ) -> float: - """Get the extremum from the given ICOHPs or ICOOPs or ICOBIs. - - Args: - list_icohps (list): ICOHPs or ICOOPs or ICOBIs. - percentage (float): The percentage to scale extremum. - - Returns: - float: Min value of ICOHPs, or max value of ICOOPs/ICOBIs. - """ - - which_extr = min if not self.are_coops and not self.are_cobis else max - return which_extr(list_icohps) * percentage - - def _get_limit_from_extremum( - self, - icohpcollection: IcohpCollection, - percentage: float = 0.15, - adapt_extremum_to_add_cond: bool = False, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, - ) -> tuple[float, float] | None: - """Get range for the ICOHP values from an IcohpCollection. - - Currently only work for ICOHPs. - - Args: - icohpcollection (IcohpCollection): IcohpCollection object. - percentage (float): Determine which ICOHPs/ICOOP/ICOBI will be considered. - adapt_extremum_to_add_cond (bool): Whether the extrumum be adapted to - the additional condition. - additional_condition (int): Additional condition to determine which bonds to include. - - Returns: - tuple[float, float]: [-inf, min(strongest_icohp*0.15, -noise_cutoff)] - or [max(strongest_icohp*0.15, noise_cutoff), inf]. - """ - extremum_based = None - - if self.valences is None: - raise ValueError(f"{self.valences=}") - - if not adapt_extremum_to_add_cond or additional_condition == 0: - extremum_based = icohpcollection.extremum_icohpvalue(summed_spin_channels=True) * percentage - - elif additional_condition == 1: - # ONLY_ANION_CATION_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 2: - # NO_ELEMENT_TO_SAME_ELEMENT_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - if value._atom1.rstrip("0123456789") != value._atom2.rstrip("0123456789"): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 3: - # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and value._atom1.rstrip( - "0123456789" - ) != value._atom2.rstrip("0123456789"): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 4: - # ONLY_ELEMENT_TO_OXYGEN_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - if value._atom1.rstrip("0123456789") == "O" or value._atom2.rstrip("0123456789") == "O": - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 5: - # DO_NOT_CONSIDER_ANION_CATION_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 6: - # ONLY_CATION_CATION_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - if val1 > 0.0 and val2 > 0.0: - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - if not self.are_coops and not self.are_cobis: - max_here = min(extremum_based, -self.noise_cutoff) if self.noise_cutoff is not None else extremum_based - return -float("inf"), max_here - - if self.are_coops or self.are_cobis: - min_here = max(extremum_based, self.noise_cutoff) if self.noise_cutoff is not None else extremum_based - return min_here, float("inf") - - return None - - -class LobsterLightStructureEnvironments(LightStructureEnvironments): - """Store LightStructureEnvironments based on LOBSTER outputs.""" - - @classmethod - def from_Lobster( - cls, - list_ce_symbol: list[str], - list_csm: list[float], - list_permutation: list, - list_neighsite: list[PeriodicSite], - list_neighisite: list[list[int]], - structure: Structure | IStructure, - valences: list[float] | None = None, - ) -> Self: - """Set up a LightStructureEnvironments from LOBSTER. - - Args: - list_ce_symbol (list[str]): Coordination environments symbols. - list_csm (list[float]): Continuous symmetry measures. - list_permutation (list): Permutations. - list_neighsite (list[PeriodicSite]): Neighboring sites. - list_neighisite (list[list[int]]): Neighboring sites indexes. - structure (Structure): Structure object. - valences (list[float]): Valences. - - Returns: - LobsterLightStructureEnvironments - """ - strategy = None - valences_origin = "user-defined" - coordination_environments = [] - all_nbs_sites = [] - all_nbs_sites_indices = [] - neighbors_sets = [] - counter = 0 - - for site_idx in range(len(structure)): - # Coordination environment - if list_ce_symbol is not None: - ce_dict = { - "ce_symbol": list_ce_symbol[site_idx], - "ce_fraction": 1.0, - "csm": list_csm[site_idx], - "permutation": list_permutation[site_idx], - } - else: - ce_dict = None - - if list_neighisite[site_idx] is not None: - all_nbs_sites_indices_here = [] - for neigh_site_idx, neigh_site in enumerate(list_neighsite[site_idx]): - diff = neigh_site.frac_coords - structure[list_neighisite[site_idx][neigh_site_idx]].frac_coords - round_diff = np.round(diff) - if not np.allclose(diff, round_diff): - raise ValueError( - "Weird, differences between one site in a periodic image cell is not integer ..." - ) - nb_image_cell = np.array(round_diff, int) - - all_nbs_sites_indices_here.append(counter) - - neighbor = { - "site": neigh_site, - "index": list_neighisite[site_idx][neigh_site_idx], - "image_cell": nb_image_cell, - } - all_nbs_sites.append(neighbor) - counter += 1 - - all_nbs_sites_indices.append(all_nbs_sites_indices_here) - - else: - all_nbs_sites.append({"site": None, "index": None, "image_cell": None}) - all_nbs_sites_indices.append([]) - - if list_neighisite[site_idx] is not None: - nb_set = cls.NeighborsSet( - structure=structure, # type:ignore[arg-type] - isite=site_idx, - all_nbs_sites=all_nbs_sites, - all_nbs_sites_indices=all_nbs_sites_indices[site_idx], - ) - - else: - nb_set = cls.NeighborsSet( - structure=structure, # type:ignore[arg-type] - isite=site_idx, - all_nbs_sites=[], - all_nbs_sites_indices=[], - ) - - coordination_environments.append([ce_dict]) - neighbors_sets.append([nb_set]) - - return cls( - strategy=strategy, - coordination_environments=coordination_environments, - all_nbs_sites=all_nbs_sites, - neighbors_sets=neighbors_sets, - structure=structure, - valences=valences, - valences_origin=valences_origin, - ) - - @property - def uniquely_determines_coordination_environments(self) -> Literal[True]: - """Whether the coordination environments are uniquely determined.""" - return True - - def as_dict(self) -> dict[str, Any]: - """Bson-serializable dict representation of the object. - - Returns: - Bson-serializable dict representation. - """ - return { - "@module": type(self).__module__, - "@class": type(self).__name__, - "strategy": self.strategy, - "structure": self.structure.as_dict(), - "coordination_environments": self.coordination_environments, - "all_nbs_sites": [ - { - "site": nb_site["site"].as_dict(), - "index": nb_site["index"], - "image_cell": [int(ii) for ii in nb_site["image_cell"]], - } - for nb_site in self._all_nbs_sites - ], - "neighbors_sets": [ - [nb_set.as_dict() for nb_set in site_nb_sets] or None for site_nb_sets in self.neighbors_sets - ], - "valences": self.valences, - } - - -class ICOHPNeighborsInfo(NamedTuple): - """Tuple to record information on relevant bonds. - - Args: - total_icohp (float): Sum of ICOHP values of neighbors to the selected - sites (given by the index in structure). - list_icohps (list): Summed ICOHP values for all identified interactions with neighbors. - n_bonds (int): Number of identified bonds to the selected sites. - labels (list[str]): Labels (from ICOHPLIST) for all identified bonds. - atoms (list[list[str]]): Lists describing the species present (from ICOHPLIST) - in the identified interactions , e.g. ["Ag3", "O5"]. - central_isites (list[int]): The central site indexes for each identified interaction. - """ - - total_icohp: float - list_icohps: list[float] - n_bonds: int - labels: list[str] - atoms: list[list[str]] - central_isites: list[int] | None +class LobsterNeighbors: + """Deprecated LobsterNeighbors class for analyzing NearNeighbor interactions using ICOHPs/ICOOPs/ICOBIs.""" diff --git a/lobsterenv.py b/lobsterenv.py index cf00295553..8662155c52 100644 --- a/lobsterenv.py +++ b/lobsterenv.py @@ -1,1841 +1,12 @@ -"""This module provides classes to perform analyses of the local -environments (e.g., finding near neighbors) of single sites in molecules -and structures based on bonding analysis with LOBSTER. - -If you use this module, please cite: -J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, -"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", -ChemPlusChem 2022, e202200123, -DOI: 10.1002/cplu.202200123. -""" +"""Deprecated class for analyzing NearNeighbors using ICOHPs/ICOOPs/ICOBIs.""" from __future__ import annotations -import collections -import copy -import math -import tempfile -import warnings -from typing import TYPE_CHECKING, NamedTuple - -import matplotlib as mpl -import numpy as np from monty.dev import deprecated -from pymatgen.analysis.bond_valence import BVAnalyzer -from pymatgen.analysis.chemenv.coordination_environments.coordination_geometry_finder import LocalGeometryFinder -from pymatgen.analysis.chemenv.coordination_environments.structure_environments import LightStructureEnvironments -from pymatgen.analysis.local_env import NearNeighbors -from pymatgen.core import Structure -from pymatgen.electronic_structure.cohp import CompleteCohp -from pymatgen.electronic_structure.core import Spin -from pymatgen.electronic_structure.plotter import CohpPlotter -from pymatgen.io.lobster import Charge, Icohplist -from pymatgen.util.due import Doi, due - -if TYPE_CHECKING: - from typing import Any, Literal, Self - - import matplotlib as mpl - from numpy.typing import NDArray - from pymatgen.core import IStructure, PeriodicNeighbor, PeriodicSite - from pymatgen.core.periodic_table import Element - from pymatgen.electronic_structure.cohp import IcohpCollection, IcohpValue - from pymatgen.util.typing import PathLike - -__author__ = "Janine George" -__copyright__ = "Copyright 2021, The Materials Project" -__version__ = "1.0" -__maintainer__ = "J. George" -__email__ = "janinegeorge.ulfen@gmail.com" -__status__ = "Production" -__date__ = "February 2, 2021" - -due.cite( - Doi("10.1002/cplu.202200123"), - description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", +@deprecated( + replacement="`pymatgen.analysis.lobster_env.LobsterNeighbors`", category=DeprecationWarning, deadline=(2026, 3, 31) ) - - -class LobsterNeighbors(NearNeighbors): - """ - This class combines capabilities from LocalEnv and ChemEnv to determine - coordination environments based on bonding analysis. - """ - - def __init__( - self, - structure: Structure | IStructure, - filename_icohp: PathLike | None = "ICOHPLIST.lobster", - obj_icohp: Icohplist | None = None, - are_coops: bool = False, - are_cobis: bool = False, - valences: list[float] | None = None, - limits: tuple[float, float] | None = None, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, - only_bonds_to: list[str] | None = None, - perc_strength_icohp: float = 0.15, - noise_cutoff: float = 0.1, - valences_from_charges: bool = False, - filename_charge: PathLike | None = None, - obj_charge: Charge | None = None, - which_charge: Literal["Mulliken", "Loewdin"] = "Mulliken", - adapt_extremum_to_add_cond: bool = False, - add_additional_data_sg: bool = False, - filename_blist_sg1: PathLike | None = None, - filename_blist_sg2: PathLike | None = None, - id_blist_sg1: Literal["icoop", "icobi"] = "icoop", - id_blist_sg2: Literal["icoop", "icobi"] = "icobi", - backward_compatibility: bool = False, - ) -> None: - """ - Args: - filename_icohp (PathLike): Path to ICOHPLIST.lobster or - ICOOPLIST.lobster or ICOBILIST.lobster. - obj_icohp (Icohplist): Icohplist object. - structure (Structure): Typically constructed by Structure.from_file("POSCAR"). - are_coops (bool): Whether the file is a ICOOPLIST.lobster (True) or a - ICOHPLIST.lobster (False). Only tested for ICOHPLIST.lobster so far. - are_cobis (bool): Whether the file is a ICOBILIST.lobster (True) or - a ICOHPLIST.lobster (False). - valences (list[float]): Valence/charge for each element. - limits (tuple[float, float]): Range to decide which ICOHPs (ICOOP - or ICOBI) should be considered. - additional_condition (int): Additional condition that decides - which kind of bonds will be considered: - 0 - NO_ADDITIONAL_CONDITION - 1 - ONLY_ANION_CATION_BONDS - 2 - NO_ELEMENT_TO_SAME_ELEMENT_BONDS - 3 - ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - 4 - ONLY_ELEMENT_TO_OXYGEN_BONDS - 5 - DO_NOT_CONSIDER_ANION_CATION_BONDS - 6 - ONLY_CATION_CATION_BONDS - only_bonds_to (list[str]): Only consider bonds to certain elements (e.g. ["O"] for oxygen). - perc_strength_icohp (float): If no "limits" are given, this will decide - which ICOHPs will be considered (relative to the strongest ICOHP/ICOOP/ICOBI). - noise_cutoff (float): The lower limit of ICOHPs considered. - valences_from_charges (bool): If True and path to CHARGE.lobster is provided, - will use LOBSTER charges (Mulliken) instead of valences. - filename_charge (PathLike): Path to Charge.lobster. - obj_charge (Charge): Charge object. - which_charge ("Mulliken" | "Loewdin"): Source of charge. - adapt_extremum_to_add_cond (bool): Whether to adapt the limits to only - focus on the bonds determined by the additional condition. - add_additional_data_sg (bool): Add the information from filename_add_bondinglist_sg1. - filename_blist_sg1 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. - filename_blist_sg2 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. - id_blist_sg1 ("icoop" | "icobi"): Identity of data in filename_blist_sg1. - id_blist_sg2 ("icoop" | "icobi"): Identity of data in filename_blist_sg2. - backward_compatibility (bool): compatibility with neighbor detection prior 2025 (less strict). - """ - warnings.warn( - "Instantiation with file paths (filename_icohp, filename_charge, filename_blist_sg1, filename_blist_sg2.) " - "is deprecated and will be removed on 31-03-2026. " - "Please use `LobsterNeighbors.from_file` instead.", - DeprecationWarning, - stacklevel=2, - ) - warnings.warn( - "Class init args obj_icohp, obj_charge will be " - "renamed to icoxxlist_obj and charge_obj respectively on 31-03-2026.", - DeprecationWarning, - stacklevel=2, - ) - - if filename_icohp is not None: - self.ICOHP = Icohplist(are_coops=are_coops, are_cobis=are_cobis, filename=filename_icohp) - elif obj_icohp is not None: - self.ICOHP = obj_icohp - else: - raise ValueError("Please provide either filename_icohp or obj_icohp") - - self.Icohpcollection = self.ICOHP.icohpcollection - self.structure = structure - self.limits = limits - self.only_bonds_to = only_bonds_to - self.adapt_extremum_to_add_cond = adapt_extremum_to_add_cond - self.are_coops = are_coops - self.are_cobis = are_cobis - self.add_additional_data_sg = add_additional_data_sg - self.filename_blist_sg1 = filename_blist_sg1 - self.filename_blist_sg2 = filename_blist_sg2 - self.noise_cutoff = noise_cutoff - - self.id_blist_sg1 = id_blist_sg1.lower() - self.id_blist_sg2 = id_blist_sg2.lower() - self.backward_compatibility = backward_compatibility - - allowed_arguments = {"icoop", "icobi"} - if self.id_blist_sg1 not in allowed_arguments or self.id_blist_sg2 not in allowed_arguments: - raise ValueError("Algorithm can only work with ICOOPs, ICOBIs") - - if add_additional_data_sg: - if self.id_blist_sg1 == "icoop": - are_coops_id1 = True - are_cobis_id1 = False - else: - are_coops_id1 = False - are_cobis_id1 = True - - self.bonding_list_1 = Icohplist( - filename=self.filename_blist_sg1, - are_coops=are_coops_id1, - are_cobis=are_cobis_id1, - ) - - if self.id_blist_sg2 == "icoop": - are_coops_id2 = True - are_cobis_id2 = False - else: - are_coops_id2 = False - are_cobis_id2 = True - - self.bonding_list_2 = Icohplist( - filename=self.filename_blist_sg2, - are_coops=are_coops_id2, - are_cobis=are_cobis_id2, - ) - - # Check the additional condition - if additional_condition not in range(7): - raise ValueError(f"Unexpected {additional_condition=}, must be one of {list(range(7))}") - self.additional_condition = additional_condition - - # Read in valences, will prefer manual setting of valences - self.valences: list[float] | None - if valences is None: - if valences_from_charges and filename_charge is not None: - chg = Charge(filename=filename_charge) - if which_charge == "Mulliken": - self.valences = chg.mulliken - elif which_charge == "Loewdin": - self.valences = chg.loewdin - - elif valences_from_charges and obj_charge is not None: - chg = obj_charge - if which_charge == "Mulliken": - self.valences = chg.mulliken - elif which_charge == "Loewdin": - self.valences = chg.loewdin - - else: - bv_analyzer = BVAnalyzer() - try: - self.valences = bv_analyzer.get_valences(structure=self.structure) # type:ignore[arg-type] - except ValueError as exc: - self.valences = None - if additional_condition in {1, 3, 5, 6}: - raise ValueError( - "Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work" - ) from exc - else: - self.valences = valences - - if np.allclose(self.valences or [], np.zeros_like(self.valences)) and additional_condition in {1, 3, 5, 6}: - raise ValueError("All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work") - - if limits is None: - self.lowerlimit = self.upperlimit = None - else: - self.lowerlimit, self.upperlimit = limits - - # Evaluate coordination environments - self._evaluate_ce( - lowerlimit=self.lowerlimit, - upperlimit=self.upperlimit, - only_bonds_to=only_bonds_to, - additional_condition=self.additional_condition, - perc_strength_icohp=perc_strength_icohp, - adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, - ) - - def __init_new__( - self, - structure: Structure, - icoxxlist_obj: Icohplist, - are_coops: bool = False, - are_cobis: bool = False, - charge_obj: Charge | None = None, - valences: list[float] | None = None, - limits: tuple[float, float] | None = None, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, - only_bonds_to: list[str] | None = None, - perc_strength_icohp: float = 0.15, - noise_cutoff: float = 0.1, - valences_from_charges: bool = False, - which_charge: Literal["Mulliken", "Loewdin"] = "Mulliken", - adapt_extremum_to_add_cond: bool = False, - add_additional_data_sg: bool = False, - bonding_list_1: Icohplist | None = None, - bonding_list_2: Icohplist | None = None, - id_blist_sg1: Literal["icoop", "icobi"] = "icoop", - id_blist_sg2: Literal["icoop", "icobi"] = "icobi", - backward_compatibility: bool = False, - ): - """ - Args: - structure (Structure): Typically constructed by Structure.from_file("POSCAR"). - icoxxlist_obj (Icohplist): Icohplist object. - are_coops (bool): Whether the file is a ICOOPLIST.lobster (True) or a - ICOHPLIST.lobster (False). Only tested for ICOHPLIST.lobster so far. - are_cobis (bool): Whether the file is a ICOBILIST.lobster (True) or - a ICOHPLIST.lobster (False). - charge_obj (Charge): Charge object. - valences (list[float]): Valence/charge for each element. - limits (tuple[float, float]): Range to decide which ICOHPs (ICOOP - or ICOBI) should be considered. - additional_condition (int): Additional condition that decides - which kind of bonds will be considered - 0 - NO_ADDITIONAL_CONDITION - 1 - ONLY_ANION_CATION_BONDS - 2 - NO_ELEMENT_TO_SAME_ELEMENT_BONDS - 3 - ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - 4 - ONLY_ELEMENT_TO_OXYGEN_BONDS - 5 - DO_NOT_CONSIDER_ANION_CATION_BONDS - 6 - ONLY_CATION_CATION_BONDS - only_bonds_to (list[str]): Only consider bonds to certain elements (e.g. ["O"] for oxygen). - perc_strength_icohp (float): If no "limits" are given, this will decide - which ICOHPs will be considered (relative to the strongest ICOHP/ICOOP/ICOBI). - noise_cutoff (float): The lower limit of ICOHPs considered. - valences_from_charges (bool): If True and path to CHARGE.lobster is provided, - will use LOBSTER charges (Mulliken) instead of valences. - which_charge ("Mulliken" | "Loewdin"): Source of charge. - adapt_extremum_to_add_cond (bool): Whether to adapt the limits to only - focus on the bonds determined by the additional condition. - add_additional_data_sg (bool): Add the information from bonding_list_1. - bonding_list_1 (Icohplist): Additional ICOOP, ICOBI data for structure graphs. - bonding_list_2 (Icohplist): Additional ICOOP, ICOBI data for structure graphs. - id_blist_sg1 ("icoop" | "icobi"): Identity of data in bonding_list_1. - id_blist_sg2 ("icoop" | "icobi"): Identity of data in bonding_list_2. - backward_compatibility (bool): If True, will use the old behavior of the code. - """ - self.structure = structure - self.ICOHP = icoxxlist_obj - self.Icohpcollection = icoxxlist_obj.icohpcollection - self.charge_obj = charge_obj - self.valences = valences - self.limits = limits - self.only_bonds_to = only_bonds_to - self.adapt_extremum_to_add_cond = adapt_extremum_to_add_cond - self.add_additional_data_sg = add_additional_data_sg - self.bonding_list_1 = bonding_list_1 # type:ignore[assignment] - self.bonding_list_2 = bonding_list_2 # type:ignore[assignment] - self.id_blist_sg1 = id_blist_sg1.lower() - self.id_blist_sg2 = id_blist_sg2.lower() - self.noise_cutoff = noise_cutoff - self.additional_condition = additional_condition - self.are_coops = are_coops - self.are_cobis = are_cobis - - self.backward_compatibility = backward_compatibility - - # validate - if self.id_blist_sg1 not in {"icoop", "icobi"} or self.id_blist_sg2 not in {"icoop", "icobi"}: - raise ValueError("Algorithm can only work with ICOOPs, ICOBIs") - - if additional_condition not in range(7): - raise ValueError(f"Unexpected {additional_condition=}, must be one of {list(range(7))}") - - if self.valences is None and valences_from_charges: - if which_charge == "Mulliken": - self.valences = charge_obj.mulliken - elif which_charge == "Loewdin": - self.valences = charge_obj.loewdin - else: - bv_analyzer = BVAnalyzer() - try: - self.valences = bv_analyzer.get_valences(structure=self.structure) # type:ignore[arg-type] - except ValueError as exc: - self.valences = None - if additional_condition in {1, 3, 5, 6}: - raise ValueError( - "Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work" - ) from exc - - if np.allclose(self.valences or [], np.zeros_like(self.valences)) and additional_condition in {1, 3, 5, 6}: - raise ValueError("All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work") - - if limits is None: - self.lowerlimit = self.upperlimit = None - else: - self.lowerlimit, self.upperlimit = limits - - # evaluate coordination environments - self._evaluate_ce( - lowerlimit=limits[0] if limits else None, - upperlimit=limits[1] if limits else None, - only_bonds_to=only_bonds_to, - additional_condition=additional_condition, - perc_strength_icohp=perc_strength_icohp, - adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, - ) - - @classmethod - def from_files( - cls, - structure_path: PathLike = "CONTCAR", - icoxxlist_path: PathLike = "ICOHPLIST.lobster", - are_coops: bool = False, - are_cobis: bool = False, - charge_path: PathLike | None = None, - blist_sg1_path: PathLike | None = None, - blist_sg2_path: PathLike | None = None, - id_blist_sg1: Literal["icoop", "icobi"] = "icoop", - id_blist_sg2: Literal["icoop", "icobi"] = "icobi", - **kwargs, - ): - """ - Instanitate LobsterNeighbors using file paths. - - Args: - structure_path (PathLike): Path to structure file, typically CONTCAR - icoxxlist_path (PathLike): Path to ICOHPLIST.lobster or - ICOOPLIST.lobster or ICOBILIST.lobster. - are_coops (bool): Whether the file is a ICOOPLIST.lobster (True) or a - ICOHPLIST.lobster (False). Only tested for ICOHPLIST.lobster so far. - are_cobis (bool): Whether the file is a ICOBILIST.lobster (True) or - a ICOHPLIST.lobster (False). - charge_path (PathLike): Path to Charge.lobster. - blist_sg1_path (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. - blist_sg2_path (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. - id_blist_sg1 ("icoop" | "icobi"): Population type in blist_sg1_path. - id_blist_sg2 ("icoop" | "icobi"): Population type in in blist_sg2_path. - """ - structure = Structure.from_file(structure_path) - icoxxlist_obj = Icohplist(filename=icoxxlist_path, are_coops=are_coops, are_cobis=are_cobis) - charge_obj = Charge(filename=charge_path) if charge_path else None - bonding_list_1 = bonding_list_2 = None - - if kwargs.get("add_additional_data_sg", False): - if id_blist_sg1 == "icoop": - are_coops_id1 = True - are_cobis_id1 = False - else: - are_coops_id1 = False - are_cobis_id1 = True - - bonding_list_1 = Icohplist( - filename=blist_sg1_path, - are_coops=are_coops_id1, - are_cobis=are_cobis_id1, - ) - - if id_blist_sg2 == "icoop": - are_coops_id2 = True - are_cobis_id2 = False - else: - are_coops_id2 = False - are_cobis_id2 = True - - bonding_list_2 = Icohplist( - filename=blist_sg2_path, - are_coops=are_coops_id2, - are_cobis=are_cobis_id2, - ) - - obj = cls.__new__(cls) - - obj.__init_new__( - structure=structure, - icoxxlist_obj=icoxxlist_obj, - are_coops=are_coops, - are_cobis=are_cobis, - charge_obj=charge_obj, - bonding_list_1=bonding_list_1, - bonding_list_2=bonding_list_2, - **kwargs, - ) - return obj - - @property - def structures_allowed(self) -> Literal[True]: - """Whether this LobsterNeighbors class can be used with Structure objects.""" - return True - - @property - def molecules_allowed(self) -> Literal[False]: - """Whether this LobsterNeighbors class can be used with Molecule objects.""" - return False - - @property - def anion_types(self) -> set[Element]: - """The set of anion types in crystal structure. - - Returns: - set[Element]: Anions in the crystal structure. - """ - if self.valences is None: - raise ValueError("No cations and anions defined") - - anion_species = [] - for site, val in zip(self.structure, self.valences, strict=True): - if val < 0.0: - anion_species.append(site.specie) - - return set(anion_species) - - @deprecated(anion_types) - def get_anion_types(self) -> set[Element]: - return self.anion_types - - def get_nn_info( - self, - structure: Structure | IStructure, - n: int, - use_weights: bool = False, - ) -> dict[str, Any]: - """Get coordination number (CN) of site by index. - - Args: - structure (Structure): Input structure. - n (int): Index of site for which to determine CN. - use_weights (bool): Whether to use weights for computing - the CN (True), or each coordinated site has equal weight (False). - The former is not implemented yet. - - Raises: - ValueError: If use_weights is True, or if arg "structure" and structure - used to initialize LobsterNeighbors have different lengths. - - Returns: - dict[str, Any]: coordination number and a list of nearest neighbors. - """ - if use_weights: - raise ValueError("LobsterEnv cannot use weights") - - if len(structure) != len(self.structure): - raise ValueError( - f"Length of structure ({len(structure)}) and LobsterNeighbors ({len(self.structure)}) differ" - ) - - return self.sg_list[n] # type: ignore[return-value] - - def get_light_structure_environment( - self, - only_cation_environments: bool = False, - only_indices: list[int] | None = None, - on_error: Literal["raise", "warn", "ignore"] = "raise", - ) -> LobsterLightStructureEnvironments: - """Get a LobsterLightStructureEnvironments object if the structure - only contains coordination environments smaller 13. - - Args: - only_cation_environments (bool): Only return data for cations. - only_indices (list[int]): Only evaluate indexes in this list. - on_error ("raise" | "warn" | "ignore"): Whether to raise an error, warn or ignore - if the environment of a site cannot be determined. - - Returns: - LobsterLightStructureEnvironments - """ - lgf = LocalGeometryFinder() - lgf.setup_structure(structure=self.structure) # type:ignore[arg-type] - list_ce_symbols = [] # type: list[str | None] - list_csm = [] # type: list[float | None] - list_permut = [] # type: list[list[int] | None] - for idx, _neigh_coords in enumerate(self.list_coords): - if (len(_neigh_coords)) > 13: - if on_error == "raise": - raise ValueError( - f"Environment cannot be determined for site {idx}. " - f"Number of neighbors ({len(_neigh_coords)}) is larger than 13." - ) - if on_error == "warn": - warnings.warn( - f"Site {idx} has {len(_neigh_coords)} neighbors (>13). " - f"Using coordination number instead of geometry.", - stacklevel=2, - ) - list_ce_symbols.append(str(len(_neigh_coords))) - list_csm.append(None) - list_permut.append(None) - continue - # "ignore" - list_ce_symbols.append(str(len(_neigh_coords))) - list_csm.append(None) - list_permut.append(None) - continue - - # Avoid problems if _neigh_coords is empty - if _neigh_coords != []: - lgf.setup_local_geometry(isite=idx, coords=_neigh_coords, optimization=2) - cncgsm = lgf.get_coordination_symmetry_measures(optimization=2) - list_ce_symbols.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[0]) - list_csm.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["csm_wcs_ctwcc"]) - list_permut.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["indices"]) - else: - list_ce_symbols.append(None) - list_csm.append(None) - list_permut.append(None) - - new_list_ce_symbols = [] - new_list_csm = [] - new_list_permut = [] - new_list_neighsite = [] - new_list_neighisite = [] - - if only_indices is None: - if not only_cation_environments: - return LobsterLightStructureEnvironments.from_Lobster( - list_ce_symbol=list_ce_symbols, - list_csm=list_csm, - list_permutation=list_permut, - list_neighsite=self.list_neighsite, # type:ignore[arg-type] - list_neighisite=self.list_neighisite, - structure=self.structure, - valences=self.valences, - ) - - if self.valences is None: - raise ValueError(f"{self.valences=}") - - for idx, val in enumerate(self.valences): - if val >= 0.0: - new_list_ce_symbols.append(list_ce_symbols[idx]) - new_list_csm.append(list_csm[idx]) - new_list_permut.append(list_permut[idx]) - new_list_neighisite.append(self.list_neighisite[idx]) - new_list_neighsite.append(self.list_neighsite[idx]) - else: - new_list_ce_symbols.append(None) - new_list_csm.append(None) - new_list_permut.append([]) - new_list_neighisite.append([]) - new_list_neighsite.append([]) - - else: - for site_idx, _site in enumerate(self.structure): - if site_idx in only_indices: - new_list_ce_symbols.append(list_ce_symbols[site_idx]) - new_list_csm.append(list_csm[site_idx]) - new_list_permut.append(list_permut[site_idx]) - new_list_neighisite.append(self.list_neighisite[site_idx]) - new_list_neighsite.append(self.list_neighsite[site_idx]) - else: - new_list_ce_symbols.append(None) - new_list_csm.append(None) - new_list_permut.append([]) - new_list_neighisite.append([]) - new_list_neighsite.append([]) - - return LobsterLightStructureEnvironments.from_Lobster( - list_ce_symbol=new_list_ce_symbols, - list_csm=new_list_csm, - list_permutation=new_list_permut, - list_neighsite=new_list_neighsite, # type:ignore[arg-type] - list_neighisite=new_list_neighisite, - structure=self.structure, - valences=self.valences, - ) - - def get_info_icohps_to_neighbors( - self, - isites: list[int] | None = None, - onlycation_isites: bool = True, - ) -> ICOHPNeighborsInfo: - """Get information on the ICOHPs of neighbors for certain sites - as identified by their site id. - - This is useful for plotting the COHPs (ICOOPLIST.lobster/ - ICOHPLIST.lobster/ICOBILIST.lobster) of a site in the structure. - - - Args: - isites (list[int]): Site IDs. If is None, all isites will be used - to add the ICOHPs of the neighbors. - onlycation_isites (bool): If True and if isite is None, will - only analyse the cations sites. - - Returns: - ICOHPNeighborsInfo - """ - if self.valences is None and onlycation_isites: - raise ValueError("No valences are provided") - - if isites is None: - if onlycation_isites: - if self.valences is None: - raise ValueError(f"{self.valences}=") - - isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] - else: - isites = list(range(len(self.structure))) - - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - summed_icohps: float = 0.0 - list_icohps: list[float] = [] - number_bonds: int = 0 - labels: list[str] = [] - atoms: list[list[str]] = [] - final_isites: list[int] = [] - for idx, _site in enumerate(self.structure): - if idx in isites: - for key, icohpsum in zip(self.list_keys[idx], self.list_icohps[idx], strict=True): - summed_icohps += icohpsum - list_icohps.append(icohpsum) # type:ignore[arg-type] - labels.append(key) - atoms.append( - [ - self.Icohpcollection._list_atom1[int(key) - 1], - self.Icohpcollection._list_atom2[int(key) - 1], - ] - ) - number_bonds += 1 - final_isites.append(idx) - return ICOHPNeighborsInfo(summed_icohps, list_icohps, number_bonds, labels, atoms, final_isites) - - def plot_cohps_of_neighbors( - self, - path_to_cohpcar: PathLike | None = "COHPCAR.lobster", - coxxcar_obj: CompleteCohp | None = None, - isites: list[int] | None = None, - onlycation_isites: bool = True, - only_bonds_to: list[str] | None = None, - per_bond: bool = False, - summed_spin_channels: bool = False, - xlim: tuple[float, float] | None = None, - ylim: tuple[float, float] = (-10, 6), - integrated: bool = False, - ) -> mpl.axes.Axes: - """Plot summed COHPs or COBIs or COOPs. - - Please be careful in the spin polarized case (plots might overlap). - - Args: - path_to_cohpcar (PathLike): Path to COHPCAR or COOPCAR or COBICAR. - coxxcar_obj (CompleteCohp | None): CompleteCohp object - isites (list[int]): Site IDs. If empty, all sites will be used to add the ICOHPs of the neighbors. - onlycation_isites (bool): Only use cations, if isite is empty. - only_bonds_to (list[str]): Only anions in this list will be considered. - per_bond (bool): Whether to plot a normalization of the plotted COHP - per number of bond (True), or the sum (False). - xlim (tuple[float, float]): Limits of x values. - ylim (tuple[float, float]): Limits of y values. - integrated (bool): Whether to show integrated COHP instead of COHP. - - Returns: - plt of the COHPs or COBIs or COOPs. - """ - cp = CohpPlotter(are_cobis=self.are_cobis, are_coops=self.are_coops) - - plotlabel, summed_cohp = self.get_info_cohps_to_neighbors( - path_to_cohpcar, - coxxcar_obj, - isites, - only_bonds_to, - onlycation_isites, - per_bond, - summed_spin_channels=summed_spin_channels, - ) - - cp.add_cohp(plotlabel, summed_cohp) - ax = cp.get_plot(integrated=integrated) - if xlim is not None: - ax.set_xlim(xlim) - - if ylim is not None: - ax.set_ylim(ylim) - - return ax - - def get_info_cohps_to_neighbors( - self, - path_to_cohpcar: PathLike | None = "COHPCAR.lobster", - coxxcar_obj: CompleteCohp | None = None, - isites: list[int] | None = None, - only_bonds_to: list[str] | None = None, - onlycation_isites: bool = True, - per_bond: bool = True, - summed_spin_channels: bool = False, - ) -> tuple[str | None, CompleteCohp | None]: - """Get the COHPs (COOPs or COBIs) as a summed Cohp object - and a label from all sites mentioned in isites with neighbors. - - Args: - path_to_cohpcar (PathLike): Path to COHPCAR/COOPCAR/COBICAR. - coxxcar_obj (CompleteCohp | None): CompleteCohp object. - isites (list[int]): The indexes of the sites. - only_bonds_to (list[str]): Only show COHPs to selected element, e.g. ["O"]. - onlycation_isites (bool): If isites is None, only cation sites will be returned. - per_bond (bool): Whether to normalize per bond. - summed_spin_channels (bool): Whether to sum both spin channels. - - Returns: - str: Label for COHP. - CompleteCohp: Describe all COHPs/COOPs/COBIs of the sites - as given by isites and the other arguments. - """ - # TODO: add options for orbital-resolved COHPs - _summed_icohps, _list_icohps, _number_bonds, labels, atoms, final_isites = self.get_info_icohps_to_neighbors( - isites=isites, onlycation_isites=onlycation_isites - ) - with tempfile.TemporaryDirectory() as tmp_dir: - path = f"{tmp_dir}/POSCAR.vasp" - - self.structure.to(filename=path, fmt="poscar") - - if not hasattr(self, "completecohp"): - if path_to_cohpcar is not None and coxxcar_obj is None: - self.completecohp = CompleteCohp.from_file( - fmt="LOBSTER", - filename=path_to_cohpcar, - structure_file=path, - are_coops=self.are_coops, - are_cobis=self.are_cobis, - ) - elif coxxcar_obj is not None: - self.completecohp = coxxcar_obj - else: - raise ValueError("Please provide either path_to_cohpcar or coxxcar_obj") - - # Check that the number of bonds in ICOHPLIST and COHPCAR are identical - # TODO: Further checks could be implemented - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - if len(self.Icohpcollection._list_atom1) != len(self.completecohp.bonds): - raise ValueError("COHPCAR and ICOHPLIST do not fit together") - - is_spin_completecohp = Spin.down in self.completecohp.get_cohp_by_label("1").cohp - if self.Icohpcollection.is_spin_polarized != is_spin_completecohp: - raise ValueError("COHPCAR and ICOHPLIST do not fit together") - - if only_bonds_to is None: - # Sort by anion type - divisor = len(labels) if per_bond else 1 - - plot_label = self._get_plot_label(atoms, per_bond) - summed_cohp = self.completecohp.get_summed_cohp_by_label_list( - label_list=labels, - divisor=divisor, - summed_spin_channels=summed_spin_channels, - ) - - else: - # Labels of the COHPs that will be summed - # Iterate through labels and atoms and check which bonds can be included - new_labels = [] - new_atoms = [] - if final_isites is None: - raise ValueError(f"{final_isites=}") - - for key, atompair, isite in zip(labels, atoms, final_isites, strict=True): - present = False - for atomtype in only_bonds_to: - # This is necessary to identify also bonds between the same elements correctly - if str(self.structure[isite].species.elements[0]) != atomtype: - if atomtype in { - self._split_string(atompair[0])[0], - self._split_string(atompair[1])[0], - }: - present = True - elif ( - atomtype == self._split_string(atompair[0])[0] - and atomtype == self._split_string(atompair[1])[0] - ): - present = True - - if present: - new_labels.append(key) - new_atoms.append(atompair) - if new_labels: - divisor = len(new_labels) if per_bond else 1 - - plot_label = self._get_plot_label(new_atoms, per_bond) - summed_cohp = self.completecohp.get_summed_cohp_by_label_list( - label_list=new_labels, - divisor=divisor, - summed_spin_channels=summed_spin_channels, - ) - else: - plot_label = None - - summed_cohp = None - - return plot_label, summed_cohp # type:ignore[return-value] - - def _get_plot_label(self, atoms: list[list[str]], per_bond: bool) -> str: - """Count the types of bonds and append a label.""" - all_labels = [] - for atoms_names in atoms: - new = [ - self._split_string(atoms_names[0])[0], - self._split_string(atoms_names[1])[0], - ] - new.sort() - string_here = f"{new[0]}-{new[1]}" - all_labels.append(string_here) - - counter = collections.Counter(all_labels) - plotlabels = [f"{item} x {key}" for key, item in counter.items()] - label = ", ".join(plotlabels) - if per_bond: - label += " (per bond)" - return label - - def get_info_icohps_between_neighbors( - self, - isites: list[int] | None = None, - onlycation_isites: bool = True, - ) -> ICOHPNeighborsInfo: - """Get interactions between neighbors of certain sites. - - Args: - isites (list[int]): Site IDs. If is None, all sites will be used. - onlycation_isites (bool): Only use cations, if isite is None. - - Returns: - ICOHPNeighborsInfo - """ - lowerlimit = self.lowerlimit - upperlimit = self.upperlimit - - if self.valences is None and onlycation_isites: - raise ValueError("No valences are provided") - - if isites is None: - if onlycation_isites: - if self.valences is None: - raise ValueError(f"{self.valences=}") - - isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] - else: - isites = list(range(len(self.structure))) - - summed_icohps: float = 0.0 - list_icohps: list[float] = [] - number_bonds: int = 0 - labels: list[str] = [] - atoms: list[list[str]] = [] - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - for isite in isites: - for site1_idx, n_site in enumerate(self.list_neighsite[isite]): - for site2_idx, n_site2 in enumerate(self.list_neighsite[isite]): - if site1_idx < site2_idx: - unitcell1 = self._determine_unit_cell(n_site) - unitcell2 = self._determine_unit_cell(n_site2) - - index_n_site = self._get_original_site(self.structure, n_site) # type:ignore[arg-type] - index_n_site2 = self._get_original_site(self.structure, n_site2) # type:ignore[arg-type] - - if index_n_site < index_n_site2: - translation = list(np.array(unitcell1) - np.array(unitcell2)) - elif index_n_site2 < index_n_site: - translation = list(np.array(unitcell2) - np.array(unitcell1)) - else: - translation = list(np.array(unitcell1) - np.array(unitcell2)) - - icohps = self._get_icohps( - icohpcollection=self.Icohpcollection, - site_idx=index_n_site, - lowerlimit=lowerlimit, - upperlimit=upperlimit, - only_bonds_to=self.only_bonds_to, - ) - # TODO: check if necessary! - done = False - for icohp in icohps.values(): - atomnr1 = self._get_atomnumber(icohp._atom1) - atomnr2 = self._get_atomnumber(icohp._atom2) - label = icohp._label - - if (index_n_site == atomnr1 and index_n_site2 == atomnr2) or ( - index_n_site == atomnr2 and index_n_site2 == atomnr1 - ): - if atomnr1 != atomnr2: - if np.all(np.asarray(translation) == np.asarray(icohp._translation)): - summed_icohps += icohp.summed_icohp - list_icohps.append(icohp.summed_icohp) - number_bonds += 1 - labels.append(label) - atoms.append( - [ - self.Icohpcollection._list_atom1[int(label) - 1], - self.Icohpcollection._list_atom2[int(label) - 1], - ] - ) - - elif not done: - icohp_trans = -np.asarray( - [ - icohp._translation[0], - icohp._translation[1], - icohp._translation[2], - ] - ) - - if (np.all(np.asarray(translation) == np.asarray(icohp._translation))) or ( - np.all(np.asarray(translation) == icohp_trans) - ): - summed_icohps += icohp.summed_icohp - list_icohps.append(icohp.summed_icohp) - number_bonds += 1 - labels.append(label) - atoms.append( - [ - self.Icohpcollection._list_atom1[int(label) - 1], - self.Icohpcollection._list_atom2[int(label) - 1], - ] - ) - done = True - - return ICOHPNeighborsInfo(summed_icohps, list_icohps, number_bonds, labels, atoms, None) - - def _evaluate_ce( - self, - lowerlimit: float | None, - upperlimit: float | None, - only_bonds_to: list[str] | None = None, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, - perc_strength_icohp: float = 0.15, - adapt_extremum_to_add_cond: bool = False, - ) -> None: - """ - Args: - lowerlimit (float): Lower limit which determines the ICOHPs - that are considered for the determination of the neighbors. - upperlimit (float): Upper limit which determines the ICOHPs - that are considered for the determination of the neighbors. - only_bonds_to (list[str]): Restrict the types of bonds that will be considered. - additional_condition (int): Additional condition for the evaluation. - perc_strength_icohp (float): Determine how strong the ICOHPs - (percentage * strongest_ICOHP) will be that are still considered. - adapt_extremum_to_add_cond (bool): Whether to recalculate the limit - based on the bonding type and not on the overall extremum. - """ - # Get extremum - if lowerlimit is None and upperlimit is None: - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - limits = self._get_limit_from_extremum( - self.Icohpcollection, - percentage=perc_strength_icohp, - adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, - additional_condition=additional_condition, - ) - - if limits is None: - raise ValueError(f"{limits=}") - lowerlimit, upperlimit = limits - - elif upperlimit is None or lowerlimit is None: - raise ValueError("Please give two limits or leave them both at None") - - # Find environments based on ICOHP values - (list_icohps, list_keys, list_lengths, list_neighisite, list_neighsite, list_coords) = self._find_environments( - additional_condition, lowerlimit, upperlimit, only_bonds_to - ) - - self.list_icohps = list_icohps - self.list_lengths = list_lengths - self.list_keys = list_keys - self.list_neighsite = list_neighsite - self.list_neighisite = list_neighisite - self.list_coords = list_coords - - # Make a structure graph - # Make sure everything is relative to the given Structure and - # not just the atoms in the unit cell - if self.add_additional_data_sg: - if self.bonding_list_1.icohpcollection is None: - raise ValueError(f"{self.bonding_list_1.icohpcollection=}") - if self.bonding_list_2.icohpcollection is None: - raise ValueError(f"{self.bonding_list_2.icohpcollection=}") - - self.sg_list = [ - [ - { - "site": neighbor, - "image": tuple( - round(idx) - for idx in ( - neighbor.frac_coords - - self.structure[ - next( - site_idx - for site_idx, site in enumerate(self.structure) - if neighbor.is_periodic_image(site) - ) - ].frac_coords - ) - ), - "weight": 1, - # Here, the ICOBIs and ICOOPs are added based on the bond - # strength cutoff of the ICOHP - # More changes are necessary here if we use ICOBIs for cutoffs - "edge_properties": { - "ICOHP": self.list_icohps[neighbors_idx][nbr_idx], - "bond_length": self.list_lengths[neighbors_idx][nbr_idx], - "bond_label": self.list_keys[neighbors_idx][nbr_idx], - self.id_blist_sg1.upper(): self.bonding_list_1.icohpcollection.get_icohp_by_label( - self.list_keys[neighbors_idx][nbr_idx] - ), - self.id_blist_sg2.upper(): self.bonding_list_2.icohpcollection.get_icohp_by_label( - self.list_keys[neighbors_idx][nbr_idx] - ), - }, - "site_index": next( - site_idx for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) - ), - } - for nbr_idx, neighbor in enumerate(neighbors) - ] - for neighbors_idx, neighbors in enumerate(self.list_neighsite) - ] - else: - self.sg_list = [ - [ - { - "site": neighbor, - "image": tuple( - round(idx) - for idx in ( - neighbor.frac_coords - - self.structure[ - next( - site_idx - for site_idx, site in enumerate(self.structure) - if neighbor.is_periodic_image(site) - ) - ].frac_coords - ) - ), - "weight": 1, - "edge_properties": { - "ICOHP": self.list_icohps[neighbors_idx][nbr_idx], - "bond_length": self.list_lengths[neighbors_idx][nbr_idx], - "bond_label": self.list_keys[neighbors_idx][nbr_idx], - }, - "site_index": next( - site_idx for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) - ), - } - for nbr_idx, neighbor in enumerate(neighbors) - ] - for neighbors_idx, neighbors in enumerate(self.list_neighsite) - ] - - def _find_environments( - self, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], - lowerlimit: float, - upperlimit: float, - only_bonds_to: list[str] | None, - ) -> tuple[ - list[list[IcohpValue]], - list[list[str]], - list[list[float]], - list[list[int]], - list[list[PeriodicNeighbor]], - list[list[NDArray]], - ]: - """Find all relevant neighbors based on certain restrictions. - - Args: - additional_condition (int): Additional condition. - lowerlimit (float): Lower limit that ICOHPs are considered. - upperlimit (float): Upper limit that ICOHPs are considered. - only_bonds_to (list[str]): Only bonds to these elements will be considered. - - Returns: - Tuple of ICOHPs, keys, lengths, neighisite, neighsite, coords. - """ - list_icohps: list[list[float]] = [] - list_keys: list[list[str]] = [] - list_lengths: list[list[float]] = [] - list_neighisite: list[list[int]] = [] - list_neighsite: list[list[PeriodicNeighbor]] = [] - list_coords: list[list[NDArray]] = [] - - # Run over structure - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - for idx, site in enumerate(self.structure): - icohps = self._get_icohps( - icohpcollection=self.Icohpcollection, - site_idx=idx, - lowerlimit=lowerlimit, - upperlimit=upperlimit, - only_bonds_to=only_bonds_to, - ) - - additional_conds = self._find_relevant_atoms_additional_condition(idx, icohps, additional_condition) - ( - keys_from_ICOHPs, - lengths_from_ICOHPs, - neighbors_from_ICOHPs, - selected_ICOHPs, - translations_ICOHPs, - ) = additional_conds - - check_ICOHPs( - lengths_from_ICOHPs=lengths_from_ICOHPs, - selected_ICOHPs=selected_ICOHPs, - translation=translations_ICOHPs, - ) - - if len(neighbors_from_ICOHPs) > 0: - centralsite = site - neighbors_by_distance_start = self.structure.get_sites_in_sphere( - pt=centralsite.coords, - r=np.max(lengths_from_ICOHPs) + 0.5, - include_image=True, - include_index=True, - ) - - neighbors_by_distance = [] - list_distances = [] - index_here_list = [] - coords = [] - translations_by_distance = [] - for neigh_new in sorted(neighbors_by_distance_start, key=lambda x: x[1]): - site_here = neigh_new[0].to_unit_cell() - index_here = neigh_new[2] - index_here_list.append(index_here) - cell_here = neigh_new[3] - - new_coords = [ - site_here.frac_coords[0] + float(cell_here[0]), - site_here.frac_coords[1] + float(cell_here[1]), - site_here.frac_coords[2] + float(cell_here[2]), - ] - coords.append(site_here.lattice.get_cartesian_coords(new_coords)) - - neighbors_by_distance.append(neigh_new[0]) - list_distances.append(neigh_new[1]) - translations_by_distance.append(cell_here) - _list_neighsite = [] - _list_neighisite = [] - copied_neighbors_from_ICOHPs = copy.copy(neighbors_from_ICOHPs) - copied_distances_from_ICOHPs = copy.copy(lengths_from_ICOHPs) - copied_translations_from_ICOHPs = copy.copy(translations_ICOHPs) - copied_icohps_from_ICOHPs = copy.copy(selected_ICOHPs) - copied_keys_from_ICOHPs = copy.copy(keys_from_ICOHPs) - _neigh_coords = [] - _neigh_frac_coords = [] - _list_icohps = [] - _list_lengths = [] - _list_keys = [] - _list_translations = [] - - for neigh_idx, neigh in enumerate(neighbors_by_distance): - index_here2 = index_here_list[neigh_idx] - for dist_idx, dist in enumerate(copied_distances_from_ICOHPs): - if not self.backward_compatibility: - comparison = ( - np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) - and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 - and ( - ( - copied_translations_from_ICOHPs[dist_idx][0] - == -translations_by_distance[neigh_idx][0] - and copied_translations_from_ICOHPs[dist_idx][1] - == -translations_by_distance[neigh_idx][1] - and copied_translations_from_ICOHPs[dist_idx][2] - == -translations_by_distance[neigh_idx][2] - ) - or ( - copied_translations_from_ICOHPs[dist_idx][0] - == translations_by_distance[neigh_idx][0] - and copied_translations_from_ICOHPs[dist_idx][1] - == translations_by_distance[neigh_idx][1] - and copied_translations_from_ICOHPs[dist_idx][2] - == translations_by_distance[neigh_idx][2] - ) - ) - ) - else: - warnings.warn( - "You are using an older version for neighbor detection that might not consider rare " - "LOBSTER edge cases. Consider switching LobsterNeighbors().backward_compatibility " - "to False for more advanced neighbor detection.", - stacklevel=2, - ) - comparison = ( - np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) - and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 - ) - - if comparison: - _list_neighsite.append(neigh) - _list_neighisite.append(index_here2) - _neigh_coords.append(coords[neigh_idx]) - _neigh_frac_coords.append(neigh.frac_coords) - _list_icohps.append(copied_icohps_from_ICOHPs[dist_idx]) - _list_lengths.append(dist) - _list_keys.append(copied_keys_from_ICOHPs[dist_idx]) - _list_translations.append(copied_translations_from_ICOHPs[dist_idx]) - del copied_distances_from_ICOHPs[dist_idx] - del copied_neighbors_from_ICOHPs[dist_idx] - del copied_translations_from_ICOHPs[dist_idx] - del copied_icohps_from_ICOHPs[dist_idx] - del copied_keys_from_ICOHPs[dist_idx] - break - - list_neighisite.append(_list_neighisite) - list_neighsite.append(_list_neighsite) - list_lengths.append(_list_lengths) - list_keys.append(_list_keys) - list_coords.append(_neigh_coords) - list_icohps.append(_list_icohps) - else: - list_neighsite.append([]) - list_neighisite.append([]) - list_icohps.append([]) - list_lengths.append([]) - list_keys.append([]) - list_coords.append([]) - return ( # type:ignore[return-value] - list_icohps, - list_keys, - list_lengths, - list_neighisite, - list_neighsite, - list_coords, - ) - - def _find_relevant_atoms_additional_condition( - self, - site_idx: int, - icohps: dict[str, IcohpValue], - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], - ) -> tuple[list[str], list[float], list[int], list[float], list[tuple[int, int, int]]]: - """Find all relevant atoms that fulfill the additional condition. - - Args: - site_idx (int): Site index in structure (start from 0). - icohps (dict[str, IcohpValue]): ICOHP values. - additional_condition (int): Additional condition. - - Returns: - tuple: keys, lengths, neighbors from selected ICOHPs and selected ICOHPs. - """ - keys_from_ICOHPs: list[str] = [] - lengths_from_ICOHPs: list[float] = [] - neighbors_from_ICOHPs: list[int] = [] - icohps_from_ICOHPs: list[float] = [] - translation_from_ICOHPs: list[tuple[int, int, int]] = [] - - for key, icohp in icohps.items(): - atomnr1 = self._get_atomnumber(icohp._atom1) - atomnr2 = self._get_atomnumber(icohp._atom2) - - # Check additional conditions - val1 = val2 = None - if self.valences is None: - raise ValueError(f"{self.valences=}") - if additional_condition in {1, 3, 5, 6}: - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - # NO_ADDITIONAL_CONDITION - if additional_condition == 0: - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - # add translation to icohp value - translation_from_ICOHPs.append(icohp.translation) - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - # ONLY_ANION_CATION_BONDS - elif additional_condition == 1: - if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): # type: ignore[operator] - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - # NO_ELEMENT_TO_SAME_ELEMENT_BONDS - elif additional_condition == 2: - if icohp._atom1.rstrip("0123456789") != icohp._atom2.rstrip("0123456789"): - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - elif additional_condition == 3: - if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and icohp._atom1.rstrip( # type: ignore[operator] - "0123456789" - ) != icohp._atom2.rstrip("0123456789"): - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - # ONLY_ELEMENT_TO_OXYGEN_BONDS - elif additional_condition == 4: - if icohp._atom1.rstrip("0123456789") == "O" or icohp._atom2.rstrip("0123456789") == "O": - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - # DO_NOT_CONSIDER_ANION_CATION_BONDS - elif additional_condition == 5: - if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): # type: ignore[operator] - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - # ONLY_CATION_CATION_BONDS - elif additional_condition == 6 and val1 > 0.0 and val2 > 0.0: # type: ignore[operator] - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - translation_from_ICOHPs.append(icohp.translation) - - return ( - keys_from_ICOHPs, - lengths_from_ICOHPs, - neighbors_from_ICOHPs, - icohps_from_ICOHPs, - translation_from_ICOHPs, - ) - - @staticmethod - def _get_icohps( - icohpcollection: IcohpCollection, - site_idx: int, - lowerlimit: float | None, - upperlimit: float | None, - only_bonds_to: list[str] | None, - ) -> dict[str, IcohpValue]: - """Get ICOHP dict for certain site. - - Args: - icohpcollection (IcohpCollection): IcohpCollection object. - site_idx (int): Site index. - lowerlimit (float): Lower limit that ICOHPs are considered. - upperlimit (float): Upper limit that ICOHPs are considered. - only_bonds_to (list[str]): Only bonds to these elements will be considered, e.g. ["O"]. - - Returns: - dict of IcohpValues. The keys correspond to the initial list_labels. - """ - return icohpcollection.get_icohp_dict_of_site( - site=site_idx, - maxbondlength=6.0, - minsummedicohp=lowerlimit, - maxsummedicohp=upperlimit, - only_bonds_to=only_bonds_to, - ) - - @staticmethod - def _get_atomnumber(atomstring: str) -> int: - """Get the index of the atom within the POSCAR (e.g., Return 0 for "Na1"). - - Args: - atomstring (str): Atom as str, such as "Na1". - - Returns: - int: Index of the atom in the POSCAR. - """ - return int(LobsterNeighbors._split_string(atomstring)[1]) - 1 - - @staticmethod - def _split_string(s) -> tuple[str, str]: - """Split strings such as "Na1" into ["Na", "1"] and return "1". - - Args: - s (str): String to split. - """ - head = s.rstrip("0123456789") - tail = s[len(head) :] - return head, tail - - @staticmethod - def _determine_unit_cell(site: PeriodicSite) -> list[int]: - """Determine the unit cell based on the site. - - Args: - site (PeriodicSite): The site. - """ - unitcell = [] - for coord in site.frac_coords: - value = math.floor(round(coord, 4)) - unitcell.append(value) - - return unitcell - - def _adapt_extremum_to_add_cond( - self, - list_icohps: list[float], - percentage: float, - ) -> float: - """Get the extremum from the given ICOHPs or ICOOPs or ICOBIs. - - Args: - list_icohps (list): ICOHPs or ICOOPs or ICOBIs. - percentage (float): The percentage to scale extremum. - - Returns: - float: Min value of ICOHPs, or max value of ICOOPs/ICOBIs. - """ - - which_extr = min if not self.are_coops and not self.are_cobis else max - return which_extr(list_icohps) * percentage - - def _get_limit_from_extremum( - self, - icohpcollection: IcohpCollection, - percentage: float = 0.15, - adapt_extremum_to_add_cond: bool = False, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, - ) -> tuple[float, float] | None: - """Get range for the ICOHP values from an IcohpCollection. - - Currently only work for ICOHPs. - - Args: - icohpcollection (IcohpCollection): IcohpCollection object. - percentage (float): Determine which ICOHPs/ICOOP/ICOBI will be considered. - adapt_extremum_to_add_cond (bool): Whether the extrumum be adapted to - the additional condition. - additional_condition (int): Additional condition to determine which bonds to include. - - Returns: - tuple[float, float]: [-inf, min(strongest_icohp*0.15, -noise_cutoff)] - or [max(strongest_icohp*0.15, noise_cutoff), inf]. - """ - extremum_based = None - - if self.valences is None: - raise ValueError(f"{self.valences=}") - - if not adapt_extremum_to_add_cond or additional_condition == 0: - extremum_based = icohpcollection.extremum_icohpvalue(summed_spin_channels=True) * percentage - - elif additional_condition == 1: - # ONLY_ANION_CATION_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 2: - # NO_ELEMENT_TO_SAME_ELEMENT_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - if value._atom1.rstrip("0123456789") != value._atom2.rstrip("0123456789"): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 3: - # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and value._atom1.rstrip( - "0123456789" - ) != value._atom2.rstrip("0123456789"): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 4: - # ONLY_ELEMENT_TO_OXYGEN_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - if value._atom1.rstrip("0123456789") == "O" or value._atom2.rstrip("0123456789") == "O": - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 5: - # DO_NOT_CONSIDER_ANION_CATION_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 6: - # ONLY_CATION_CATION_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - if val1 > 0.0 and val2 > 0.0: - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - if not self.are_coops and not self.are_cobis: - max_here = min(extremum_based, -self.noise_cutoff) if self.noise_cutoff is not None else extremum_based - return -float("inf"), max_here - - if self.are_coops or self.are_cobis: - min_here = max(extremum_based, self.noise_cutoff) if self.noise_cutoff is not None else extremum_based - return min_here, float("inf") - - return None - - -class LobsterLightStructureEnvironments(LightStructureEnvironments): - """Store LightStructureEnvironments based on LOBSTER outputs.""" - - @classmethod - def from_Lobster( - cls, - list_ce_symbol: list[str | None], - list_csm: list[float | None], - list_permutation: list, - list_neighsite: list[PeriodicSite], - list_neighisite: list[list[int]], - structure: Structure | IStructure, - valences: list[float] | None = None, - ) -> Self: - """Set up a LightStructureEnvironments from LOBSTER. - - Args: - list_ce_symbol (list[str]): Coordination environments symbols. - list_csm (list[float]): Continuous symmetry measures. - list_permutation (list): Permutations. - list_neighsite (list[PeriodicSite]): Neighboring sites. - list_neighisite (list[list[int]]): Neighboring sites indexes. - structure (Structure): Structure object. - valences (list[float]): Valences. - - Returns: - LobsterLightStructureEnvironments - """ - strategy = None - valences_origin = "user-defined" - coordination_environments = [] - all_nbs_sites = [] - all_nbs_sites_indices = [] - neighbors_sets = [] - counter = 0 - - for site_idx in range(len(structure)): - # Coordination environment - if list_ce_symbol is not None: - ce_dict = { - "ce_symbol": list_ce_symbol[site_idx], - "ce_fraction": 1.0, - "csm": list_csm[site_idx], - "permutation": list_permutation[site_idx], - } - else: - ce_dict = None - - if list_neighisite[site_idx] is not None: - all_nbs_sites_indices_here = [] - for neigh_site_idx, neigh_site in enumerate(list_neighsite[site_idx]): - diff = neigh_site.frac_coords - structure[list_neighisite[site_idx][neigh_site_idx]].frac_coords - round_diff = np.round(diff) - if not np.allclose(diff, round_diff): - raise ValueError( - "Weird, differences between one site in a periodic image cell is not integer ..." - ) - nb_image_cell = np.array(round_diff, int) - - all_nbs_sites_indices_here.append(counter) - - neighbor = { - "site": neigh_site, - "index": list_neighisite[site_idx][neigh_site_idx], - "image_cell": nb_image_cell, - } - all_nbs_sites.append(neighbor) - counter += 1 - - all_nbs_sites_indices.append(all_nbs_sites_indices_here) - - else: - all_nbs_sites.append({"site": None, "index": None, "image_cell": None}) - all_nbs_sites_indices.append([]) - - if list_neighisite[site_idx] is not None: - nb_set = cls.NeighborsSet( - structure=structure, # type:ignore[arg-type] - isite=site_idx, - all_nbs_sites=all_nbs_sites, - all_nbs_sites_indices=all_nbs_sites_indices[site_idx], - ) - - else: - nb_set = cls.NeighborsSet( - structure=structure, # type:ignore[arg-type] - isite=site_idx, - all_nbs_sites=[], - all_nbs_sites_indices=[], - ) - - coordination_environments.append([ce_dict]) - neighbors_sets.append([nb_set]) - - return cls( - strategy=strategy, - coordination_environments=coordination_environments, - all_nbs_sites=all_nbs_sites, - neighbors_sets=neighbors_sets, - structure=structure, - valences=valences, - valences_origin=valences_origin, - ) - - @property - def uniquely_determines_coordination_environments(self) -> Literal[True]: - """Whether the coordination environments are uniquely determined.""" - return True - - def as_dict(self) -> dict[str, Any]: - """Bson-serializable dict representation of the object. - - Returns: - Bson-serializable dict representation. - """ - return { - "@module": type(self).__module__, - "@class": type(self).__name__, - "strategy": self.strategy, - "structure": self.structure.as_dict(), - "coordination_environments": self.coordination_environments, - "all_nbs_sites": [ - { - "site": nb_site["site"].as_dict(), - "index": nb_site["index"], - "image_cell": [int(ii) for ii in nb_site["image_cell"]], - } - for nb_site in self._all_nbs_sites - ], - "neighbors_sets": [ - [nb_set.as_dict() for nb_set in site_nb_sets] or None for site_nb_sets in self.neighbors_sets - ], - "valences": self.valences, - } - - -class ICOHPNeighborsInfo(NamedTuple): - """Tuple to record information on relevant bonds. - - Args: - total_icohp (float): Sum of ICOHP values of neighbors to the selected - sites (given by the index in structure). - list_icohps (list): Summed ICOHP values for all identified interactions with neighbors. - n_bonds (int): Number of identified bonds to the selected sites. - labels (list[str]): Labels (from ICOHPLIST) for all identified bonds. - atoms (list[list[str]]): Lists describing the species present (from ICOHPLIST) - in the identified interactions , e.g. ["Ag3", "O5"]. - central_isites (list[int]): The central site indexes for each identified interaction. - """ - - total_icohp: float - list_icohps: list[float] - n_bonds: int - labels: list[str] - atoms: list[list[str]] - central_isites: list[int] | None - - -def check_ICOHPs(lengths_from_ICOHPs, selected_ICOHPs, translation, length_threshold=0.01, energy_threshold=0.1): - for i in range(len(lengths_from_ICOHPs)): - for j in range(i + 1, len(lengths_from_ICOHPs)): - if ( - abs(lengths_from_ICOHPs[i] - lengths_from_ICOHPs[j]) < length_threshold - and abs(selected_ICOHPs[i] - selected_ICOHPs[j]) > energy_threshold - and translation[i] != (0, 0, 0) - and ( - translation[i][0] == -translation[j][0] - and translation[i][1] == -translation[j][1] - and translation[i][2] == -translation[j][2] - ) - ): - warnings.warn( - f"Lengths {lengths_from_ICOHPs[i]} and {lengths_from_ICOHPs[j]} are very close " - f"and translation exactly opposite, but corresponding ICOHPs {selected_ICOHPs[i]} " - f"and {selected_ICOHPs[j]} are not. Our neighbor detection might fail.", - stacklevel=2, - ) +class LobsterNeighbors: + """Deprecated LobsterNeighbors class for analyzing NearNeighbor interactions using ICOHPs/ICOOPs/ICOBIs.""" From 5070414e07683e32b453938f99cb9fe5dd388577 Mon Sep 17 00:00:00 2001 From: naik-aakash Date: Wed, 25 Feb 2026 16:32:37 +0100 Subject: [PATCH 175/180] move lobsterenv test --- tests/io/lobster/test_lobsterenv.py | 1060 --------------------------- 1 file changed, 1060 deletions(-) delete mode 100644 tests/io/lobster/test_lobsterenv.py diff --git a/tests/io/lobster/test_lobsterenv.py b/tests/io/lobster/test_lobsterenv.py deleted file mode 100644 index b238b20606..0000000000 --- a/tests/io/lobster/test_lobsterenv.py +++ /dev/null @@ -1,1060 +0,0 @@ -from __future__ import annotations - -import warnings - -import pytest -from numpy.testing import assert_allclose -from pytest import approx - -from pymatgen.analysis.graphs import StructureGraph -from pymatgen.core import Element -from pymatgen.core.structure import Structure -from pymatgen.electronic_structure.cohp import Cohp, CompleteCohp -from pymatgen.electronic_structure.core import Spin -from pymatgen.io.lobster import Charge, Icohplist -from pymatgen.io.lobster.lobsterenv import LobsterNeighbors -from pymatgen.util.testing import TEST_FILES_DIR - -__author__ = "Janine George" -__copyright__ = "Copyright 2021, The Materials Project" -__version__ = "0.1" -__email__ = "janine.george@uclouvain.be" -__date__ = "Jan 14, 2021" - -TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp/environments" - - -class TestLobsterNeighbors: - def setup_method(self): - # test additional conditions first - # only consider cation anion bonds - - self.chem_env_lobster1 = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - additional_condition=1, - perc_strength_icohp=0.3, - noise_cutoff=0.0, - ) - - # all bonds - self.chem_env_lobster0 = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - additional_condition=0, - ) - # test __init_new__ using from_files - self.chem_env_lobster0_from_file = LobsterNeighbors.from_files( - are_coops=False, - structure_path=f"{TEST_DIR}/CONTCAR.mp-190.gz", - icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - additional_condition=0, - ) - - # only cation-cation, anion-anion bonds - self.chem_env_lobster5 = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - additional_condition=5, - ) - - self.chem_env_lobster5_from_file = LobsterNeighbors.from_files( - are_coops=False, - structure_path=f"{TEST_DIR}/CONTCAR.mp-190.gz", - icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - additional_condition=5, - ) - - # only cation-cation bonds - self.chem_env_lobster6 = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - additional_condition=6, - ) - - self.chem_env_lobster6_from_file = LobsterNeighbors.from_files( - are_coops=False, - structure_path=f"{TEST_DIR}/CONTCAR.mp-190.gz", - icoxxlist_path=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - additional_condition=6, - ) - - # 2,3,4 are not tested so far - self.chem_env_lobster2 = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - additional_condition=2, - ) - - self.chem_env_lobster3 = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - additional_condition=3, - ) - - self.chem_env_lobster4 = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - additional_condition=4, - ) - - # search for other testcase where 2,3,4 arrive at different results - self.chem_env_lobster0_second = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - additional_condition=0, - perc_strength_icohp=0.05, - ) - self.chem_env_lobster1_second = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - additional_condition=1, - ) - - self.chem_env_lobster2_second = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - additional_condition=2, - ) - - self.chem_env_lobster5_second = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - additional_condition=5, - perc_strength_icohp=0.05, - ) - - self.chem_env_lobster5_second_percentage = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - additional_condition=5, - perc_strength_icohp=1.0, - ) - - self.chem_env_lobster6_second = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - additional_condition=6, - perc_strength_icohp=0.05, - ) - # coop / cobi - self.chem_env_lobster1_coop_NaCl = LobsterNeighbors( - are_coops=True, - filename_icohp=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), - additional_condition=1, - noise_cutoff=None, - ) - - self.chem_env_lobster1_coop_NaCl_from_file = LobsterNeighbors.from_files( - are_coops=True, - structure_path=f"{TEST_DIR}/CONTCAR.NaCl.gz", - icoxxlist_path=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", - additional_condition=1, - noise_cutoff=None, - ) - - self.chem_env_lobster1_cobi_NaCl = LobsterNeighbors( - are_coops=True, - filename_icohp=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), - additional_condition=1, - noise_cutoff=None, - ) - - self.chem_env_lobster1_cobi_NaCl_from_file = LobsterNeighbors.from_files( - are_coops=True, - structure_path=f"{TEST_DIR}/CONTCAR.NaCl.gz", - icoxxlist_path=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", - additional_condition=1, - noise_cutoff=None, - ) - - self.chem_env_lobster1_cobi_mp470 = LobsterNeighbors( - are_coops=True, - filename_icohp=f"{TEST_DIR}/ICOBILIST.lobster.mp-470.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-470.gz"), - additional_condition=1, - ) - - # TODO: use charge instead of valence - self.chem_env_lobster1_charges = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", - additional_condition=1, - ) - self.chem_env_lobster1_charges_noisecutoff = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-632319.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-632319.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-632319.gz", - additional_condition=1, - perc_strength_icohp=0.05, - noise_cutoff=0.1, - ) - self.chem_env_lobster1_charges_wo_noisecutoff = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-632319.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-632319.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-632319.gz", - additional_condition=1, - perc_strength_icohp=0.05, - noise_cutoff=None, - ) - self.chem_env_lobster1_charges_loewdin = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", - additional_condition=1, - which_charge="Loewdin", - ) - self.chem_env_lobster6_charges_additional_condition = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", - additional_condition=6, - adapt_extremum_to_add_cond=True, - ) - self.chem_env_lobster5_charges_additional_condition = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", - additional_condition=5, - adapt_extremum_to_add_cond=True, - ) - self.chem_env_lobster4_charges_additional_condition = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", - additional_condition=4, - adapt_extremum_to_add_cond=True, - ) - self.chem_env_lobster3_charges_additional_condition = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", - additional_condition=3, - adapt_extremum_to_add_cond=True, - ) - self.chem_env_lobster2_charges_additional_condition = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", - additional_condition=2, - adapt_extremum_to_add_cond=True, - ) - self.chem_env_lobster1_charges_additional_condition = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", - additional_condition=1, - adapt_extremum_to_add_cond=True, - ) - - self.chem_env_lobster0_charges_additional_condition = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", - additional_condition=0, - adapt_extremum_to_add_cond=True, - ) - self.chem_env_lobster0_NaSi = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaSi.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaSi.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaSi.gz", - additional_condition=0, - adapt_extremum_to_add_cond=True, - ) - self.chem_env_lobster_NaSi_wo_charges = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaSi.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaSi.gz"), - valences_from_charges=False, - filename_charge=None, - additional_condition=0, - adapt_extremum_to_add_cond=True, - ) - # Test LobsterNeighbors using pymatgen objects - self.obj_icohp = Icohplist(filename=f"{TEST_DIR}/ICOHPLIST.lobster.NaSi.gz") - self.obj_charge = Charge(filename=f"{TEST_DIR}/CHARGE.lobster.NaSi.gz") - self.chem_env_w_obj = LobsterNeighbors( - filename_icohp=None, - are_coops=False, - obj_icohp=self.obj_icohp, - obj_charge=self.obj_charge, - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaSi.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaSi.gz", - additional_condition=0, - adapt_extremum_to_add_cond=True, - ) - # test LSE on_error - self.chem_env_on_error = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-1018096.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-1018096.gz"), - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-1018096.gz", - additional_condition=0, - adapt_extremum_to_add_cond=True, - valences_from_charges=True, - ) - - def test_init_new(self): - # additional condition 0 - ref_lse = self.chem_env_lobster0.get_light_structure_environment(only_cation_environments=False) - test_lse = self.chem_env_lobster0_from_file.get_light_structure_environment(only_cation_environments=False) - - assert self.chem_env_lobster0.anion_types == self.chem_env_lobster0_from_file.anion_types - assert ref_lse.coordination_environments == test_lse.coordination_environments - assert ref_lse.valences == test_lse.valences - - # additional condition 5 - ref_lse = self.chem_env_lobster5.get_light_structure_environment(only_cation_environments=False) - test_lse = self.chem_env_lobster5_from_file.get_light_structure_environment(only_cation_environments=False) - - assert self.chem_env_lobster5.anion_types == self.chem_env_lobster5_from_file.anion_types - assert ref_lse.coordination_environments == test_lse.coordination_environments - assert ref_lse.valences == test_lse.valences - - # additional condition 6 - ref_lse = self.chem_env_lobster6.get_light_structure_environment(only_cation_environments=False) - test_lse = self.chem_env_lobster6_from_file.get_light_structure_environment(only_cation_environments=False) - assert self.chem_env_lobster6.anion_types == self.chem_env_lobster6_from_file.anion_types - assert ref_lse.coordination_environments == test_lse.coordination_environments - assert ref_lse.valences == test_lse.valences - - # coop NaCl - ref_lse = self.chem_env_lobster1_coop_NaCl.get_light_structure_environment(only_cation_environments=False) - test_lse = self.chem_env_lobster1_coop_NaCl_from_file.get_light_structure_environment( - only_cation_environments=False - ) - assert self.chem_env_lobster1_coop_NaCl.anion_types == self.chem_env_lobster1_coop_NaCl_from_file.anion_types - assert ref_lse.coordination_environments == test_lse.coordination_environments - assert ref_lse.valences == test_lse.valences - - # cobi NaCl - ref_lse = self.chem_env_lobster1_cobi_NaCl.get_light_structure_environment(only_cation_environments=False) - test_lse = self.chem_env_lobster1_cobi_NaCl_from_file.get_light_structure_environment( - only_cation_environments=False - ) - assert self.chem_env_lobster1_cobi_NaCl.anion_types == self.chem_env_lobster1_cobi_NaCl_from_file.anion_types - assert ref_lse.coordination_environments == test_lse.coordination_environments - assert ref_lse.valences == test_lse.valences - - def test_cation_anion_mode_without_ions(self): - with pytest.raises( - ValueError, - match="Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work", - ): - _ = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/../ICOHPLIST.lobster", - structure=Structure.from_file(f"{TEST_DIR}/../POSCAR"), - valences_from_charges=False, - additional_condition=1, - ) - with pytest.raises( - ValueError, - match="All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work", - ): - _ = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/../ICOHPLIST.lobster", - structure=Structure.from_file(f"{TEST_DIR}/../POSCAR"), - valences_from_charges=False, - additional_condition=1, - valences=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - ) - - def test_wrong_additional_correction(self): - with pytest.raises( - ValueError, - match=r"Unexpected additional_condition=10, must be one of \[0, 1, 2, 3, 4, 5, 6\]", - ): - LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", - additional_condition=10, - ) - - def test_set_limits(self): - test = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-353.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.mp-353.gz", - additional_condition=1, - limits=[-100000, 0], - ) - assert test.limits == [-100000, 0] - - def test_molecules_allowed(self): - assert not self.chem_env_lobster1.molecules_allowed - - @pytest.mark.filterwarnings("ignore:get_anion_types is deprecated") - def test_get_anion_types(self): - assert self.chem_env_lobster0_second.get_anion_types() == {Element("O")} - assert self.chem_env_lobster0_second.anion_types == {Element("O")} - - def test_get_nn_info(self): - # 0: NO_ADDITIONAL_CONDITION - # 1: ONLY_ANION_CATION_BONDS - # 2: NO_ELEMENT_TO_SAME_ELEMENT_BONDS - # 3: ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - # 4: ONLY_ELEMENT_TO_OXYGEN_BONDS - # 5: DO_NOT_CONSIDER_ANION_CATION_BONDS - # 6: ONLY_CATION_CATION_BONDS - - # All bonds - # ReO3 - assert ( - len( - self.chem_env_lobster0.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=0, - ) - ) - == 6 - ) - assert ( - len( - self.chem_env_lobster0.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=1, - ) - ) - == 2 - ) - # 1: ONLY_ANION_CATION_BONDS - assert ( - len( - self.chem_env_lobster1.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=0, - ) - ) - == 6 - ) - assert ( - len( - self.chem_env_lobster1.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=1, - ) - ) - == 2 - ) - assert ( - len( - self.chem_env_lobster1_charges_noisecutoff.get_nn( - structure=self.chem_env_lobster1_charges_noisecutoff.structure, - n=1, - ) - ) - == 0 - ) - assert ( - len( - self.chem_env_lobster1_charges_wo_noisecutoff.get_nn( - structure=self.chem_env_lobster1_charges_wo_noisecutoff.structure, - n=1, - ) - ) - == 8 - ) - # 2: NO_ELEMENT_TO_SAME_ELEMENT_BONDS - assert ( - len( - self.chem_env_lobster2.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=0, - ) - ) - == 6 - ) - assert ( - len( - self.chem_env_lobster2.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=1, - ) - ) - == 2 - ) - # 3: ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - assert ( - len( - self.chem_env_lobster3.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=0, - ) - ) - == 6 - ) - assert ( - len( - self.chem_env_lobster3.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=1, - ) - ) - == 2 - ) - # 4: ONLY_ELEMENT_TO_OXYGEN_BONDS - assert ( - len( - self.chem_env_lobster4.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=0, - ) - ) - == 6 - ) - assert ( - len( - self.chem_env_lobster4.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=1, - ) - ) - == 2 - ) - # 5: DO_NOT_CONSIDER_ANION_CATION_BONDS - assert ( - len( - self.chem_env_lobster5.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=0, - ) - ) - == 0 - ) - assert ( - len( - self.chem_env_lobster5.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=1, - ) - ) - == 0 - ) - # 6: ONLY_CATION_CATION_BONDS - assert ( - len( - self.chem_env_lobster6.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=0, - ) - ) - == 0 - ) - - assert ( - len( - self.chem_env_lobster6.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - n=1, - ) - ) - == 0 - ) - - # All bonds - # mp-353, Ag2O - # all bonds - assert ( - len( - self.chem_env_lobster0_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - n=0, - ) - ) - == 8 - ) - - # 1: ONLY_ANION_CATION_BONDS - assert ( - len( - self.chem_env_lobster1_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - n=0, - ) - ) - == 2 - ) - - assert ( - len( - self.chem_env_lobster1_coop_NaCl.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), - n=0, - ) - ) - == 6 - ) - - assert ( - len( - self.chem_env_lobster1_cobi_NaCl.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), - n=0, - ) - ) - == 6 - ) - - assert ( - len( - self.chem_env_lobster1_cobi_mp470.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-470.gz"), - n=3, - ) - ) - == 3 - ) - - # 2: NO_ELEMENT_TO_SAME_ELEMENT_BONDS - assert ( - len( - self.chem_env_lobster2_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - n=0, - ) - ) - == 2 - ) - assert ( - len( - self.chem_env_lobster2_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - n=4, - ) - ) - == 4 - ) - - # 5: DO_NOT_CONSIDER_ANION_CATION_BONDS - assert ( - len( - self.chem_env_lobster5_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - n=0, - ) - ) - == 6 - ) - assert ( - len( - self.chem_env_lobster5_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - n=4, - ) - ) - == 4 - ) - - # 6: ONLY_CATION_CATION_BONDS - assert ( - len( - self.chem_env_lobster6_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - n=0, - ) - ) - == 6 - ) - assert ( - len( - self.chem_env_lobster6_second.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - n=4, - ) - ) - == 0 - ) - - assert ( - len( - self.chem_env_lobster5_second_percentage.get_nn( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), - n=0, - ) - ) - == 0 - ) - - # NaSi - # all bonds - # passes after changes introduced in PR #4148 (correct ordering of all edge attributes) - nn0 = self.chem_env_lobster0_NaSi.get_nn_info(structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaSi.gz"), n=0) - nn11 = self.chem_env_lobster0_NaSi.get_nn_info( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaSi.gz"), n=11 - ) - assert next(d["edge_properties"] for d in nn0 if d.get("site_index") == 11) == next( - d["edge_properties"] for d in nn11 if d.get("site_index") == 0 - ) - - def test_structure_graph(self): - sg = self.chem_env_lobster1_second.get_bonded_structure( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz") - ) - assert isinstance(sg, StructureGraph) - - def test_extended_structure_graph(self): - self.chem_env_lobsterNaCl = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaCl.gz", - filename_blist_sg1=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", - filename_blist_sg2=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", - add_additional_data_sg=True, - id_blist_sg1="icobi", - id_blist_sg2="icoop", - additional_condition=1, - ) - sg = self.chem_env_lobsterNaCl.get_bonded_structure( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), - decorate=True, - edge_properties=True, - weights=True, - ) - assert sg.graph.get_edge_data(0, 1)[0]["ICOHP"] == approx(-0.59138) - assert sg.graph.get_edge_data(0, 1)[0]["ICOBI"] == approx(0.08682) - assert sg.graph.get_edge_data(0, 1)[0]["ICOOP"] == approx(0.03007) - assert sg.graph.get_edge_data(0, 1)[0]["bond_label"] == "21" - assert sg.graph.get_edge_data(0, 1)[5]["bond_label"] == "30" - assert isinstance(sg, StructureGraph) - - def test_raises_extended_structure_graph(self): - with pytest.raises(ValueError, match="Algorithm can only work with ICOOPs, ICOBIs"): - self.chem_env_lobsterNaCl = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.NaCl.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.NaCl.gz"), - valences_from_charges=True, - filename_charge=f"{TEST_DIR}/CHARGE.lobster.NaCl.gz", - filename_blist_sg1=f"{TEST_DIR}/ICOBILIST.lobster.NaCl.gz", - filename_blist_sg2=f"{TEST_DIR}/ICOOPLIST.lobster.NaCl.gz", - add_additional_data_sg=True, - id_blist_sg1="icopppp", - id_blist_sg2="icoop", - additional_condition=1, - ) - - def test_order_parameter(self): - assert self.chem_env_lobster1_second.get_local_order_parameters( - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-353.gz"), n=0 - )["linear"] == approx(1.0) - - def test_get_structure_environments(self): - lse = self.chem_env_lobster1_second.get_light_structure_environment() - assert lse.coordination_environments[0][0]["ce_symbol"] == "L:2" - assert lse.coordination_environments[5][0]["ce_symbol"] == "T:4" - - lse2 = self.chem_env_lobster1.get_light_structure_environment() - assert lse2.coordination_environments[0][0]["ce_symbol"] == "O:6" - - def test_get_structure_environments_further_tests(self): - lse = self.chem_env_lobster1_second.get_light_structure_environment() - lse.as_dict() - lse.get_statistics() - assert lse.uniquely_determines_coordination_environments - - def test_get_info_icohps_neighbors(self): - results = self.chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0]) - assert results[0] == approx(-33.87452) - for bond in results[1]: - assert bond == approx(-5.64612, abs=1e-2) - assert results[2] == 6 - assert results[3] == ["48", "27", "64", "73", "49", "30"] - - results2 = self.chem_env_lobster1.get_info_icohps_to_neighbors(isites=None) - assert results2[0] == approx(-33.87452) - for bond in results2[1]: - assert bond == approx(-5.64455, abs=1e-2) - assert results2[2] == 6 - - assert results2[3] == ["48", "27", "64", "73", "49", "30"] - from collections import Counter - - assert Counter(map(tuple, results2[4])) == Counter( - [("Re1", "O2"), ("Re1", "O2"), ("Re1", "O3"), ("Re1", "O3"), ("Re1", "O4"), ("Re1", "O4")] - ) - - def test_get_sum_icohps_between_neighbors_of_atom(self): - # will only look at icohps between cations or anions - self.chem_env_lobster1.get_info_icohps_to_neighbors(isites=[1]) - assert self.chem_env_lobster1.get_info_icohps_between_neighbors(isites=[1])[2] == 1 - assert self.chem_env_lobster1.get_info_icohps_between_neighbors(isites=[1])[0] == approx(-0.04535) - - # confirmed by looking at the VESTA output - assert self.chem_env_lobster1.get_info_icohps_between_neighbors(isites=[0])[2] == 12 - # use an example where this is easier to test (e.g., linear environment?) - - chemenv_here = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-7000.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-7000.gz"), - additional_condition=1, - perc_strength_icohp=0.05, - noise_cutoff=0.0, - ) - - assert len(chemenv_here.get_info_icohps_between_neighbors(isites=[0])[4]) == 2 - - def test_get_plot_label(self): - label = self.chem_env_lobster1._get_plot_label( - atoms=[ - ["Re1", "O2"], - ["Re1", "O2"], - ["Re1", "O3"], - ["Re1", "O3"], - ["Re1", "O4"], - ["Re1", "O4"], - ], - per_bond=False, - ) - assert label == "6 x O-Re" - - label = self.chem_env_lobster1._get_plot_label( - atoms=[ - ["Re1", "O2"], - ["Re1", "O2"], - ["Re1", "O3"], - ["Re1", "O3"], - ["Re1", "O4"], - ["Si1", "O4"], - ], - per_bond=False, - ) - assert label == "5 x O-Re, 1 x O-Si" - - label = self.chem_env_lobster1._get_plot_label( - atoms=[ - ["Si1", "O2"], - ["Si1", "O2"], - ["Si1", "O3"], - ["Re1", "O3"], - ["Re1", "O4"], - ["Si1", "O4"], - ], - per_bond=False, - ) - assert label == "4 x O-Si, 2 x O-Re" - - label = self.chem_env_lobster1._get_plot_label( - atoms=[ - ["Re1", "O2"], - ["Re1", "O2"], - ["Re1", "O3"], - ["Re1", "O3"], - ["Re1", "O4"], - ["Re1", "O4"], - ], - per_bond=True, - ) - assert label == "6 x O-Re (per bond)" - - def test_get_info_cohps_to_neighbors(self): - chem_env_lobster1 = LobsterNeighbors( - are_coops=False, - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - additional_condition=1, - ) - cohpcar_lobster_mp_190 = f"{TEST_DIR}/COHPCAR.lobster.mp-190.gz" - plot_label, summed_cohpcar_mp_190 = chem_env_lobster1.get_info_cohps_to_neighbors( - path_to_cohpcar=cohpcar_lobster_mp_190, - isites=[0], - only_bonds_to=["O"], - ) - assert plot_label == "6 x O-Re (per bond)" - assert isinstance(summed_cohpcar_mp_190, Cohp) - - cohp_result = chem_env_lobster1.get_info_cohps_to_neighbors( - path_to_cohpcar=cohpcar_lobster_mp_190, - isites=[0], - only_bonds_to=None, - per_bond=False, - )[1] - print(cohp_result.icohp[Spin.up][700]) - print(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0]) - assert cohp_result.icohp[Spin.up][700] == approx(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0]) - - # summed_spin_channel - coph_thing = chem_env_lobster1.get_info_cohps_to_neighbors( - path_to_cohpcar=cohpcar_lobster_mp_190, - isites=[0], - only_bonds_to=None, - per_bond=False, - summed_spin_channels=True, - )[1] - assert coph_thing.icohp[Spin.up][700] == approx(chem_env_lobster1.get_info_icohps_to_neighbors(isites=[0])[0]) - - plot_label, summed_cohpcar_mp_190_Te = chem_env_lobster1.get_info_cohps_to_neighbors( - path_to_cohpcar=cohpcar_lobster_mp_190, - isites=[0], - only_bonds_to=["Te"], - ) - - assert plot_label is None - assert summed_cohpcar_mp_190_Te is None - - plot_label, _summed_cohpcar_NaSi = self.chem_env_lobster0_NaSi.get_info_cohps_to_neighbors( - path_to_cohpcar=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", - isites=[8], - onlycation_isites=False, - only_bonds_to=["Na"], - ) - assert plot_label == "1 x Na-Si (per bond)" - - obj_cohpcar = CompleteCohp.from_file( - filename=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", - fmt="LOBSTER", - structure_file=f"{TEST_DIR}/CONTCAR.NaSi.gz", - ) - plot_label_obj, _summed_cohpcar_NaSi_obj = self.chem_env_w_obj.get_info_cohps_to_neighbors( - coxxcar_obj=obj_cohpcar, - isites=[8], - onlycation_isites=False, - only_bonds_to=["Na"], - ) - assert plot_label_obj == "1 x Na-Si (per bond)" - - info = self.chem_env_lobster0_NaSi.get_info_cohps_to_neighbors( - path_to_cohpcar=f"{TEST_DIR}/COHPCAR.lobster.NaSi.gz", - isites=[8], - onlycation_isites=False, - only_bonds_to=["Si"], - )[0] - assert info == "3 x Si-Si (per bond)" - - chem_env_lobster1.plot_cohps_of_neighbors( - path_to_cohpcar=cohpcar_lobster_mp_190, - isites=[0], - only_bonds_to=["O"], - summed_spin_channels=True, - ) - - chem_env_lobster1.plot_cohps_of_neighbors( - path_to_cohpcar=cohpcar_lobster_mp_190, - isites=[0], - only_bonds_to=["O"], - summed_spin_channels=True, - xlim=[-10, 10], - ylim=None, - ) - - expected_msg = "COHPCAR and ICOHPLIST do not fit together" - with pytest.raises(ValueError, match=expected_msg): - # icohplist and cohpcar do not fit together - self.chem_env_lobster1.get_info_cohps_to_neighbors( - path_to_cohpcar=f"{TEST_DIR}/COHPCAR.lobster.mp-190_2.gz", - isites=[0], - only_bonds_to=None, - per_bond=False, - ) - - with pytest.raises(ValueError, match=expected_msg): - # icohplist and cohpcar do not fit together - self.chem_env_lobster2.get_info_cohps_to_neighbors( - path_to_cohpcar=f"{TEST_DIR}/COHPCAR.lobster.mp-190_2.gz", - isites=[0], - only_bonds_to=None, - per_bond=False, - ) - - def test_valences(self): - assert self.chem_env_lobster1_charges_noisecutoff.valences == [ - 0.75, - -0.75, - ] # Mulliken - assert self.chem_env_lobster1_charges_loewdin.valences == [ - 0.51, - 0.51, - 0.51, - 0.51, - -1.01, - -1.01, - ] - - assert_allclose( - self.chem_env_w_obj.valences, [0.66] * 4 + [0.69] * 4 + [-0.68] * 3 + [-0.69] + [-0.67] * 4 - ) # charge_obj - assert self.chem_env_lobster_NaSi_wo_charges.valences == [1] * 8 + [-1] * 8 # BVA - - def test_backward_compatibility_warning(self): - with pytest.warns(UserWarning, match="You are using an older version for neighbor detection"): - _ = LobsterNeighbors( - filename_icohp=f"{TEST_DIR}/ICOHPLIST.lobster.mp-190.gz", - structure=Structure.from_file(f"{TEST_DIR}/CONTCAR.mp-190.gz"), - backward_compatibility=True, - ) - - def test_on_error_case(self): - with pytest.raises( - ValueError, - match=r"Environment cannot be determined for site 0\. Number of neighbors \(18\) is larger than 13\.", - ): - _ = self.chem_env_on_error.get_light_structure_environment(on_error="raise") - - with pytest.warns( - UserWarning, - match=r"Site 0 has 18 neighbors \(>13\)\. Using coordination number instead of geometry\.", - ) as warnings_record: - lse = self.chem_env_on_error.get_light_structure_environment(on_error="warn") - - assert lse.coordination_environments[0][0]["ce_symbol"] == "18" - assert [str(warn.message) for warn in warnings_record] == [ - "Site 0 has 18 neighbors (>13). Using coordination number instead of geometry.", - "Site 1 has 18 neighbors (>13). Using coordination number instead of geometry.", - ] - - # if on_error is "ignore", no error should be raised and no warning should be issued - with warnings.catch_warnings(record=True) as warnings_record: - lse = self.chem_env_on_error.get_light_structure_environment(on_error="ignore") - - assert lse.coordination_environments[0][0]["ce_symbol"] == "18" - assert warnings_record == [] From 070985515843b7d98e8c6244a839c48eb8c6d2dc Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Tue, 3 Mar 2026 05:16:37 +0800 Subject: [PATCH 176/180] Major reorganization of pymatgen repo (#4595) * Try initial deletion. * More source removal and tests removal. * More cleanup. * More cleanup. * Remove commandline tests. * Update pymatgen-test-files. * Fix ruff. * Fix missing package files. * Cleanup pyproject.toml. * There is no need for splits anymore. * Make sure moyopy tests are skipped if not installed. * Cleanup test workflow. --- future/outputs/bands.py | 2 +- sets.py | 161 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 sets.py diff --git a/future/outputs/bands.py b/future/outputs/bands.py index 77e1b181ba..50cb9c0c5c 100644 --- a/future/outputs/bands.py +++ b/future/outputs/bands.py @@ -286,7 +286,7 @@ def __init__( for k, w, n in zip( full_kpoints.kpts, full_kpoints.kpts_weights, - (full_kpoints.labels if full_kpoints.labels else [None] * len(full_kpoints.kpts)), + (full_kpoints.labels or [None] * len(full_kpoints.kpts)), strict=True, ) if w == 0 diff --git a/sets.py b/sets.py new file mode 100644 index 0000000000..830e584f33 --- /dev/null +++ b/sets.py @@ -0,0 +1,161 @@ +""" +This module defines the VaspInputSet abstract base class and a concrete implementation for the parameters developed +and tested by the core team of pymatgen, including the Materials Virtual Lab, Materials Project and the MIT high +throughput project. The basic concept behind an input set is to specify a scheme to generate a consistent set of VASP +inputs from a structure without further user intervention. This ensures comparability across runs. + +Read the following carefully before implementing new input sets: + +1. 99% of what needs to be done can be done by specifying user_incar_settings to override some of the defaults of + various input sets. Unless there is an extremely good reason to add a new set, **do not** add one. e.g. if you want + to turn the Hubbard U off, just set "LDAU": False as a user_incar_setting. +2. All derivative input sets should inherit appropriate configurations (e.g., from MPRelaxSet), and more often than + not, VaspInputSet should be the superclass. Superclass delegation should be used where possible. In particular, + you are not supposed to implement your own as_dict or from_dict for derivative sets unless you know what you are + doing. Improper overriding the as_dict and from_dict protocols is the major cause of implementation headaches. If + you need an example, look at how the MPStaticSet is initialized. + +The above are recommendations. The following are **UNBREAKABLE** rules: + +1. All input sets must take in a structure, list of structures or None as the first argument. If None, the input set + should perform a stateless initialization and before any output can be written, a structure must be set. +2. user_incar_settings, user_kpoints_settings and user__settings are ABSOLUTE. Any new sets you implement + must obey this. If a user wants to override your settings, you assume he knows what he is doing. Do not + magically override user supplied settings. You can issue a warning if you think the user is wrong. +3. All input sets must save all supplied args and kwargs as instance variables. e.g. self.arg = arg and + self.kwargs = kwargs in the __init__. This ensures the as_dict and from_dict work correctly. +""" + +from __future__ import annotations + +import os +import warnings +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any + +from pymatgen.io.lobster import Lobsterin +from pymatgen.io.vasp.sets import BadInputSetWarning, MPRelaxSet, VaspInputSet + +if TYPE_CHECKING: + from collections.abc import Sequence + from typing import Literal + + UserPotcarFunctional = ( + Literal[ + "PBE", + "PBE_52", + "PBE_54", + "PBE_64", + "LDA", + "LDA_52", + "LDA_54", + "PW91", + "LDA_US", + "PW91_US", + ] + | None + ) + +MODULE_DIR = os.path.dirname(__file__) + + +@dataclass +class LobsterSet(VaspInputSet): + """Input set to prepare VASP runs that can be digested by Lobster (See cohp.de). + + Args: + structure (Structure): input structure. + isym (int): ISYM entry for INCAR, only isym=-1 and isym=0 are allowed + ismear (int): ISMEAR entry for INCAR, only ismear=-5 and ismear=0 are allowed + reciprocal_density (int): density of k-mesh by reciprocal volume + user_supplied_basis (dict): dict including basis functions for all elements in + structure, e.g. {"Fe": "3d 3p 4s", "O": "2s 2p"}; if not supplied, a + standard basis is used + address_basis_file (str): address to a file similar to + "BASIS_PBE_54_standard.yaml" in pymatgen.io.lobster.lobster_basis + user_potcar_settings (dict): dict including potcar settings for all elements in + structure, e.g. {"Fe": "Fe_pv", "O": "O"}; if not supplied, a standard basis + is used. + **kwargs: Other kwargs supported by VaspInputSet. + """ + + isym: int = 0 + ismear: int = -5 + reciprocal_density: int | None = None + address_basis_file: str | None = None + user_supplied_basis: dict | None = None + + # Latest POTCARs are preferred + # Choose PBE_54 unless the user specifies a different potcar_functional + user_potcar_functional: UserPotcarFunctional = "PBE_54" + + CONFIG = MPRelaxSet.CONFIG + _valid_potcars: Sequence[str] | None = ("PBE_52", "PBE_54", "PBE_64") + + def __post_init__(self) -> None: + super().__post_init__() + warnings.warn( + "Make sure that all parameters are okay! This is a brand new implementation.", + stacklevel=2, + ) + + if self.user_potcar_functional in ["PBE_52", "PBE_64"]: + warnings.warn( + f"Using {self.user_potcar_functional} POTCARs with basis functions generated for PBE_54 POTCARs. " + "Basis functions for elements with obsoleted, updated or newly added POTCARs in " + f"{self.user_potcar_functional} will not be available and may cause errors or inaccuracies.", + BadInputSetWarning, + stacklevel=2, + ) + if self.isym not in {-1, 0}: + raise ValueError("Lobster cannot digest WAVEFUNCTIONS with symmetry. isym must be -1 or 0") + if self.ismear not in {-5, 0}: + raise ValueError("Lobster usually works with ismear=-5 or ismear=0") + + self._config_dict["POTCAR"]["W"] = "W_sv" + + @property + def kpoints_updates(self) -> dict[str, int]: + """Updates to the kpoints configuration for this calculation type.""" + # Test if this is okay + return {"reciprocal_density": self.reciprocal_density or 310} + + @property + def incar_updates(self) -> dict[str, Any]: + """Updates to the INCAR config for this calculation type.""" + + potcar_symbols = self.potcar_symbols + + # Predefined basis! Check if the basis is okay! (charge spilling and bandoverlaps!) + if self.user_supplied_basis is None and self.address_basis_file is None: + basis = Lobsterin.get_basis(structure=self.structure, potcar_symbols=potcar_symbols) # type: ignore[arg-type] + elif self.address_basis_file is not None: + basis = Lobsterin.get_basis( + structure=self.structure, # type: ignore[arg-type] + potcar_symbols=potcar_symbols, + address_basis_file=self.address_basis_file, + ) + elif self.user_supplied_basis is not None: + # Test if all elements from structure are in user_supplied_basis + for atom_type in self.structure.symbol_set: # type: ignore[union-attr] + if atom_type not in self.user_supplied_basis: + raise ValueError(f"There are no basis functions for the atom type {atom_type}") + basis = [f"{key} {value}" for key, value in self.user_supplied_basis.items()] + else: + basis = None + + lobsterin = Lobsterin(settingsdict={"basisfunctions": basis}) + nbands = lobsterin._get_nbands(structure=self.structure) # type: ignore[arg-type] + + return { + "EDIFF": 1e-6, + "NSW": 0, + "LWAVE": True, + "ISYM": self.isym, + "NBANDS": nbands, + "IBRION": -1, + "ISMEAR": self.ismear, + "LORBIT": 11, + "ICHARG": 0, + "ALGO": "Normal", + } From 60bc568cd2e043ccca35f4e96107ac4a18d4caa8 Mon Sep 17 00:00:00 2001 From: Shyue Ping Ong Date: Tue, 3 Mar 2026 05:16:37 +0800 Subject: [PATCH 177/180] Major reorganization of pymatgen repo (#4595) * Try initial deletion. * More source removal and tests removal. * More cleanup. * More cleanup. * Remove commandline tests. * Update pymatgen-test-files. * Fix ruff. * Fix missing package files. * Cleanup pyproject.toml. * There is no need for splits anymore. * Make sure moyopy tests are skipped if not installed. * Cleanup test workflow. --- tests/io/lobster/future/test_outputs.py | 2 +- tests/io/lobster/test_sets.py | 116 ++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 tests/io/lobster/test_sets.py diff --git a/tests/io/lobster/future/test_outputs.py b/tests/io/lobster/future/test_outputs.py index 478f62dc27..c66e1cf829 100644 --- a/tests/io/lobster/future/test_outputs.py +++ b/tests/io/lobster/future/test_outputs.py @@ -1510,7 +1510,7 @@ def test_attributes(self): assert isinstance(self.overlap_matrices.matrices["1"], dict) for spin in self.overlap_matrices.matrices["1"]: - assert spin in [None] + assert spin is None assert isinstance(self.overlap_matrices.matrices["1"][spin], np.ndarray) assert self.overlap_matrices.matrices["1"][spin].shape == (4, 4) diff --git a/tests/io/lobster/test_sets.py b/tests/io/lobster/test_sets.py new file mode 100644 index 0000000000..3d7948c094 --- /dev/null +++ b/tests/io/lobster/test_sets.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +import re + +import pytest +from pytest import approx + +from pymatgen.core import SETTINGS, Structure +from pymatgen.io.lobster.sets import LobsterSet +from pymatgen.io.vasp.sets import MODULE_DIR, BadInputSetWarning +from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_IN_DIR, MatSciTest + +TEST_DIR = f"{TEST_FILES_DIR}/io/vasp" + +pytest.MonkeyPatch().setitem(SETTINGS, "PMG_VASP_PSP_DIR", str(FAKE_POTCAR_DIR)) + +NO_PSP_DIR = SETTINGS.get("PMG_VASP_PSP_DIR") is None +skip_if_no_psp_dir = pytest.mark.skipif(NO_PSP_DIR, reason="PMG_VASP_PSP_DIR is not set") + + +class TestLobsterSet(MatSciTest): + def setup_method(self): + self.set = LobsterSet + file_path = f"{VASP_IN_DIR}/POSCAR" + file_path2 = f"{VASP_IN_DIR}/POSCAR.lobster.spin_DOS" + self.struct = Structure.from_file(file_path) + self.struct2 = Structure.from_file(file_path2) + + # test for different parameters! + self.lobsterset1 = self.set(self.struct, isym=-1, ismear=-5) + self.lobsterset2 = self.set(self.struct, isym=0, ismear=0) + # only allow isym=-1 and isym=0 + with pytest.raises( + ValueError, + match=re.escape("Lobster cannot digest WAVEFUNCTIONS with symmetry. isym must be -1 or 0"), + ): + self.lobsterset_new = self.set(self.struct, isym=2, ismear=0) + with pytest.raises(ValueError, match="Lobster usually works with ismear=-5 or ismear=0"): + self.lobsterset_new = self.set(self.struct, isym=-1, ismear=2) + # test if one can still hand over grid density of kpoints + self.lobsterset3 = self.set(self.struct, isym=0, ismear=0, user_kpoints_settings={"grid_density": 6000}) + # check if users can overwrite settings in this class with the help of user_incar_settings + self.lobsterset4 = self.set(self.struct, user_incar_settings={"ALGO": "Fast"}) + # use basis functions supplied by user + self.lobsterset5 = self.set( + self.struct, + user_supplied_basis={"Fe": "3d 3p 4s", "P": "3p 3s", "O": "2p 2s"}, + ) + with pytest.raises(ValueError, match="There are no basis functions for the atom type O"): + self.lobsterset6 = self.set(self.struct, user_supplied_basis={"Fe": "3d 3p 4s", "P": "3p 3s"}).incar + self.lobsterset7 = self.set( + self.struct, + address_basis_file=f"{MODULE_DIR}/../lobster/lobster_basis/BASIS_PBE_54_standard.yaml", + ) + with pytest.warns(BadInputSetWarning, match="Overriding the POTCAR"): + self.lobsterset6 = self.set(self.struct) + + # test W_sw + self.lobsterset8 = self.set(Structure.from_file(f"{TEST_FILES_DIR}/electronic_structure/cohp/POSCAR.W")) + + # test if potcar selection is consistent with PBE_54 + self.lobsterset9 = self.set(self.struct2) + + def test_incar(self): + incar1 = self.lobsterset1.incar + assert "NBANDS" in incar1 + assert incar1["NBANDS"] == 116 + assert incar1["NSW"] == 0 + assert incar1["ISMEAR"] == -5 + assert incar1["ISYM"] == -1 + assert incar1["ALGO"] == "Normal" + assert incar1["EDIFF"] == approx(1e-6) + incar2 = self.lobsterset2.incar + assert incar2["ISYM"] == 0 + assert incar2["ISMEAR"] == 0 + incar4 = self.lobsterset4.incar + assert incar4["ALGO"] == "Fast" + + def test_kpoints(self): + kpoints1 = self.lobsterset1.kpoints + assert kpoints1.comment.split()[5] == "6138" + kpoints2 = self.lobsterset2.kpoints + assert kpoints2.comment.split()[5] == "6138" + kpoints3 = self.lobsterset3.kpoints + assert kpoints3.comment.split()[5] == "6000" + + @skip_if_no_psp_dir + def test_potcar(self): + # PBE_54 is preferred at the moment + functional, symbol = "PBE_54", "K_sv" + assert self.lobsterset1.user_potcar_functional == functional + # test if potcars selected are consistent with PBE_54 + assert self.lobsterset2.potcar.symbols == ["Fe_pv", "P", "O"] + # test if error raised contains correct potcar symbol for K element as PBE_54 set + with pytest.raises( + FileNotFoundError, + match=f"You do not have the right POTCAR with {functional=} and {symbol=}", + ): + _ = self.lobsterset9.potcar.symbols + + def test_as_from_dict(self): + dict_here = self.lobsterset1.as_dict() + + lobsterset_new = self.set.from_dict(dict_here) + # test relevant parts again + incar1 = lobsterset_new.incar + assert "NBANDS" in incar1 + assert incar1["NBANDS"] == 116 + assert incar1["NSW"] == 0 + assert incar1["NSW"] == 0 + assert incar1["ISMEAR"] == -5 + assert incar1["ISYM"] == -1 + assert incar1["ALGO"] == "Normal" + kpoints1 = lobsterset_new.kpoints + assert kpoints1.comment.split()[5] == "6138" + assert lobsterset_new.user_potcar_functional == "PBE_54" From 771042ac3eaf0b408721c742de957848a87567fa Mon Sep 17 00:00:00 2001 From: naik-aakash Date: Mon, 9 Mar 2026 15:17:11 +0100 Subject: [PATCH 178/180] remove future module --- src/pymatgen/io/lobster/future/__init__.py | 102 -- src/pymatgen/io/lobster/future/constants.py | 26 - src/pymatgen/io/lobster/future/core.py | 449 ----- src/pymatgen/io/lobster/future/inputs.py | 959 ----------- .../lobster_basis/BASIS_PBE_54_max.yaml | 189 -- .../lobster_basis/BASIS_PBE_54_min.yaml | 189 -- .../lobster_basis/BASIS_PBE_54_standard.yaml | 189 -- src/pymatgen/io/lobster/future/lobsterenv.py | 1523 ----------------- .../io/lobster/future/outputs/__init__.py | 80 - .../io/lobster/future/outputs/bands.py | 500 ------ .../io/lobster/future/outputs/coxxcar.py | 380 ---- .../io/lobster/future/outputs/doscar.py | 216 --- .../io/lobster/future/outputs/icoxxlist.py | 428 ----- .../io/lobster/future/outputs/lobsterout.py | 439 ----- .../io/lobster/future/outputs/misc.py | 670 -------- .../io/lobster/future/outputs/populations.py | 157 -- src/pymatgen/io/lobster/future/types.py | 96 -- src/pymatgen/io/lobster/future/utils.py | 124 -- src/pymatgen/io/lobster/future/versioning.py | 25 - 19 files changed, 6741 deletions(-) delete mode 100644 src/pymatgen/io/lobster/future/__init__.py delete mode 100644 src/pymatgen/io/lobster/future/constants.py delete mode 100644 src/pymatgen/io/lobster/future/core.py delete mode 100644 src/pymatgen/io/lobster/future/inputs.py delete mode 100644 src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_max.yaml delete mode 100644 src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_min.yaml delete mode 100644 src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_standard.yaml delete mode 100644 src/pymatgen/io/lobster/future/lobsterenv.py delete mode 100644 src/pymatgen/io/lobster/future/outputs/__init__.py delete mode 100644 src/pymatgen/io/lobster/future/outputs/bands.py delete mode 100644 src/pymatgen/io/lobster/future/outputs/coxxcar.py delete mode 100644 src/pymatgen/io/lobster/future/outputs/doscar.py delete mode 100644 src/pymatgen/io/lobster/future/outputs/icoxxlist.py delete mode 100644 src/pymatgen/io/lobster/future/outputs/lobsterout.py delete mode 100644 src/pymatgen/io/lobster/future/outputs/misc.py delete mode 100644 src/pymatgen/io/lobster/future/outputs/populations.py delete mode 100644 src/pymatgen/io/lobster/future/types.py delete mode 100644 src/pymatgen/io/lobster/future/utils.py delete mode 100644 src/pymatgen/io/lobster/future/versioning.py diff --git a/src/pymatgen/io/lobster/future/__init__.py b/src/pymatgen/io/lobster/future/__init__.py deleted file mode 100644 index 557324027a..0000000000 --- a/src/pymatgen/io/lobster/future/__init__.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Modules for input and output to and from LOBSTER. - -This package provides classes and utilities for reading and writing LOBSTER input and output files. -""" - -from __future__ import annotations - -from pymatgen.io.lobster.future.inputs import LobsterIn -from pymatgen.io.lobster.future.outputs import ( - BWDF, - CHARGE, - CHARGE_LCFO, - COBICAR, - COBICAR_LCFO, - COHPCAR, - COHPCAR_LCFO, - COOPCAR, - DOSCAR, - DOSCAR_LCFO, - GROSSPOP, - GROSSPOP_LCFO, - ICOBILIST, - ICOBILIST_LCFO, - ICOHPLIST, - ICOHPLIST_LCFO, - ICOOPLIST, - ICOXXLIST, - POLARIZATION, - BandOverlaps, - Fatband, - Fatbands, - LobsterMatrices, - LobsterOut, - MadelungEnergies, - NcICOBILIST, - SitePotentials, - Wavefunction, -) - -__all__ = [ - "BWDF", - "CHARGE", - "CHARGE_LCFO", - "COBICAR", - "COBICAR_LCFO", - "COHPCAR", - "COHPCAR_LCFO", - "COOPCAR", - "DOSCAR", - "DOSCAR_LCFO", - "GROSSPOP", - "GROSSPOP_LCFO", - "ICOBILIST", - "ICOBILIST_LCFO", - "ICOHPLIST", - "ICOHPLIST_LCFO", - "ICOOPLIST", - "ICOXXLIST", - "POLARIZATION", - "BandOverlaps", - "Fatband", - "Fatbands", - "LobsterIn", - "LobsterMatrices", - "LobsterOut", - "MadelungEnergies", - "NcICOBILIST", - "SitePotentials", - "Wavefunction", -] - -LOBSTER_OBJECTS = { - "BWDF": BWDF, - "CHARGE": CHARGE, - "CHARGE_LCFO": CHARGE_LCFO, - "COBICAR": COBICAR, - "COBICAR_LCFO": COBICAR_LCFO, - "COHPCAR": COHPCAR, - "COHPCAR_LCFO": COHPCAR_LCFO, - "COOPCAR": COOPCAR, - "DOSCAR": DOSCAR, - "DOSCAR_LCFO": DOSCAR_LCFO, - "GROSSPOP": GROSSPOP, - "GROSSPOP_LCFO": GROSSPOP_LCFO, - "ICOBILIST": ICOBILIST, - "ICOBILIST_LCFO": ICOBILIST_LCFO, - "ICOHPLIST": ICOHPLIST, - "ICOHPLIST_LCFO": ICOHPLIST_LCFO, - "ICOOPLIST": ICOOPLIST, - "ICOXXLIST": ICOXXLIST, - "POLARIZATION": POLARIZATION, - "BandOverlaps": BandOverlaps, - "Fatband": Fatband, - "Fatbands": Fatbands, - "LobsterIn": LobsterIn, - "LobsterMatrices": LobsterMatrices, - "LobsterOut": LobsterOut, - "MadelungEnergies": MadelungEnergies, - "NcICOBILIST": NcICOBILIST, - "SitePotentials": SitePotentials, - "Wavefunction": Wavefunction, -} diff --git a/src/pymatgen/io/lobster/future/constants.py b/src/pymatgen/io/lobster/future/constants.py deleted file mode 100644 index 7e90caf302..0000000000 --- a/src/pymatgen/io/lobster/future/constants.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Constants for LOBSTER input and output parsing.""" - -from __future__ import annotations - -"""The version of LOBSTER that this code is compatible with. To be updated when necessary.""" -LOBSTER_VERSION: str = "5.1.1" - -"""A tuple of possible orbitals that LOBSTER can handle and their string representations in LOBSTER output files.""" -LOBSTER_ORBITALS: tuple[str, ...] = ( - "s", - "p_x", - "p_y", - "p_z", - "d_xy", - "d_xz", - "d_yz", - "d_z^2", - "d_x^2-y^2", - "f_xyz", - "f_xz^2", - "f_yz^2", - "f_z^3", - "f_x(x^2-3y^2)", - "f_y(3x^2-y^2)", - "f_z(x^2-y^2)", -) diff --git a/src/pymatgen/io/lobster/future/core.py b/src/pymatgen/io/lobster/future/core.py deleted file mode 100644 index 6d7c727d5c..0000000000 --- a/src/pymatgen/io/lobster/future/core.py +++ /dev/null @@ -1,449 +0,0 @@ -from __future__ import annotations - -from abc import abstractmethod -from collections import Counter -from functools import cached_property -from pathlib import Path -from typing import TYPE_CHECKING, Any, ClassVar, Self, cast - -import numpy as np -from monty.io import zopen -from monty.json import MontyDecoder, MSONable - -from pymatgen.io.lobster.future.constants import LOBSTER_VERSION -from pymatgen.io.lobster.future.utils import convert_spin_keys, restore_spin_keys - -if TYPE_CHECKING: - from collections.abc import Callable, Generator - - from numpy.typing import NDArray - - from pymatgen.io.lobster.future.types import LobsterInteraction, LobsterInteractionData, Spin - from pymatgen.util.typing import PathLike - - -class LobsterFile(MSONable): - """ - Representation of a LOBSTER file. - - This class provides a framework for parsing and processing LOBSTER output files. - It supports version-specific processing through a registry of version processors. - - Attributes: - filename (PathLike): Name or path of the file. Defaults to the class default. - lobster_version (str): Version string parsed from the file or the default LOBSTER_VERSION. - version_processors (ClassVar[dict[tuple[str, str | None], Callable]]): Registry of version processors. - spins (list[Spin] | None): List of Spin objects if the file contains spin-polarized data, else None. - """ - - version_processors: ClassVar[dict[tuple[str, str | None], Callable]] - - spins: list[Spin] | None = None - - def __init__( - self, - filename: PathLike | None = None, - process_immediately: bool = True, - lobster_version: str | None = None, - ) -> None: - """ - Initialize a LobsterFile instance. - - Args: - filename (PathLike | None): Path to the file. If None, uses the default filename. - process_immediately (bool): Whether to process the file immediately upon initialization. Defaults to True. - lobster_version (str | None): LOBSTER version string to use for parsing. If None, attempts to detect - from file or falls back to default. - """ - self.filename = Path(filename or self.get_default_filename()).expanduser().resolve() - self.lobster_version = lobster_version or self.get_file_version() or LOBSTER_VERSION - - if process_immediately: - self.process() - - def __init_subclass__(cls, **kwargs: Any) -> None: - """ - Automatically registers version processors for subclasses. - This method scans the subclass for methods decorated with @version_processor - and adds them to the version_processors registry. - """ - super().__init_subclass__(**kwargs) - - version_processors = {} - for base in cls.__bases__: - if hasattr(base, "version_processors"): - version_processors.update(base.version_processors) - - for value in cls.__dict__.values(): - if hasattr(value, "version_info"): - version_processors[value.version_info] = value - - cls.version_processors = version_processors - - def process(self) -> None: - """ - Processes the file using the appropriate version processor. - - Selects the best matching version processor from the `version_processors` registry and invokes it. - - Raises: - ValueError: If no processor matches the file version. - RuntimeError: If the selected processor raises an exception. - """ - eligible_methods = [] - - if not self.filename.exists(): - raise FileNotFoundError(f"The file {self.filename} does not exist.") - - for ( - minimum_version, - maximum_version, - ), processor in self.version_processors.items(): - if LobsterFile.check_version(self.lobster_version, minimum_version, maximum_version): - eligible_methods.append((minimum_version, processor)) - - if not eligible_methods: - raise ValueError(f"No processor found for version {self.lobster_version}") - - best_method = max(eligible_methods, key=lambda x: x[0])[-1] - - try: - best_method(self) - except Exception as e: - processor_name = getattr(best_method, "__name__", str(best_method)) - - raise RuntimeError( - f"Error occurred during file processing with {processor_name} (version {self.lobster_version}): {e}" - ) from e - - @staticmethod - def check_version(actual: str, minimum: str, maximum: str | None) -> bool: - """ - Checks whether a version string falls within a min/max inclusive range. - - Args: - actual (str): Version string to check (e.g., "1.2.3"). - minimum (str): Minimum acceptable version string (exclusive). - maximum (str | None): Maximum acceptable version string (exclusive) or None for no upper bound. - - Returns: - bool: True if `actual` is > `minimum` and < `maximum` (if provided), otherwise False. - """ - actual_parts = tuple(map(int, actual.split("."))) - minimum_parts = tuple(map(int, minimum.split("."))) - - if actual_parts < minimum_parts: - return False - - if maximum is not None: - maximum_parts = tuple(map(int, maximum.split("."))) - - if actual_parts > maximum_parts: - return False - - return True - - def get_file_version(self) -> str | None: - """ - Retrieves the file version. Override in subclasses to extract version from file content if possible. - - Returns: - str | None: Version string (e.g., "1.2.3") if found, else None. - """ - - @classmethod - @abstractmethod - def get_default_filename(cls) -> str: - """ - Returns the default filename for this LobsterFile subclass. - - Returns: - str: The default filename. - """ - ... - - @cached_property - def lines(self) -> list[str]: - """ - Returns all lines from the file as a list of strings. - - Returns: - list[str]: Lines from the file. - - Raises: - ValueError: If the file is empty. - """ - with zopen(self.filename, mode="rt", encoding="utf-8") as file: - lines = file.read().splitlines() - - if len(lines) == 0: - raise ValueError(f"The file {self.filename} is empty.") - - return cast("list[str]", lines) - - def iterate_lines(self) -> Generator[str]: - """ - Iterates over lines in the file, yielding each line as a string. - - Yields: - str: Each line in the file, stripped of whitespace. - """ - with zopen(self.filename, mode="rt", encoding="utf-8") as file: - for line in file: - yield cast("str", line.strip()) - - def as_dict(self) -> dict[str, Any]: - """ - Serializes the LobsterFile object to a dictionary. - Spin keys in dictionaries are converted to strings for JSON compatibility. - - Returns: - dict[str, Any]: Dictionary with keys "@module", "@class", "@version", and all attributes of the object. - """ - dictionary = { - "@module": self.__class__.__module__, - "@class": self.__class__.__name__, - "@version": None, - } - - for k, v in vars(self).items(): - dictionary[k] = convert_spin_keys(v) - - return dictionary - - @classmethod - def from_dict(cls, d: dict[str, Any]) -> Self: - """ - Deserializes a LobsterFile object from a dictionary. Spin keys in dictionaries are restored from strings. - - Args: - d (dict[str, Any]): Dictionary produced by as_dict or similar serialization. - - Returns: - LobsterFile: Instance of LobsterFile with attributes populated from the dictionary. - """ - instance = cls.__new__(cls) - - decoded_dictionary = { - k: restore_spin_keys(MontyDecoder().process_decoded(v)) for k, v in d.items() if not k.startswith("@") - } - - for k, v in decoded_dictionary.items(): - setattr(instance, k, v) - - instance.filename = Path(instance.filename) - - return instance - - @property - def has_spin(self) -> bool: - """ - Indicates whether the file could contain spin-polarized data. - - Returns: - bool: True if this file type supports spin, False otherwise. - """ - return self.spins is not None - - @property - def is_spin_polarized(self) -> bool: - """ - Indicates whether the file contains spin-polarized data. - - Returns: - bool: True if multiple spins are present, False otherwise. - """ - return self.has_spin and len(self.spins) > 1 - - -class LobsterInteractionsHolder(LobsterFile): - """ - Container for LOBSTER interaction data. This class holds interaction metadata. It provides methods for filtering and - retrieving interaction data based on various criteria. - - Attributes: - interactions (list[LobsterInteractionData]): List of interaction metadata dicts. - """ - - interactions: list[LobsterInteractionData] - data: NDArray[np.float64] - - def get_interaction_indices_by_properties( - self, - indices: list[int] | None = None, - centers: list[str] | None = None, - cells: list[list[int]] | None = None, - orbitals: list[str] | None = None, - length: tuple[float, float] | None = None, - ) -> list[int]: - """ - Returns indices of interactions that match provided property filters. - - Args: - indices (list[int] | None): Optional sequence of interaction indices to match. - centers (list[str] | None): Optional sequence of center name substrings; interaction must contain each - center substring the required number of times. - cells (list[list[int]] | None): Optional sequence of cell vectors to match against interaction cells. - orbitals (list[str] | None): Optional sequence of orbital name substrings; interaction must contain each - orbital substring the required number of times. - length (tuple[float, float] | None): Optional (min, max) tuple to filter interactions by length inclusive. - - Returns: - list[int]: Sorted list of interaction indices that match all supplied filters. If no filters are supplied, - returns an empty list. - """ - matching_sets = [] - - if indices is not None: - matching_sets.append({i for i, bond in enumerate(self.interactions) if bond["index"] in indices}) - - if centers is not None: - center_counts = Counter(centers) - matching_centers = set() - - for i, bond in enumerate(self.interactions): - bond_centers = bond.get("centers", []) - - if all( - sum(center_suffix in b for b in bond_centers) >= required_count - for center_suffix, required_count in center_counts.items() - ): - matching_centers.add(i) - - matching_sets.append(matching_centers) - - if cells is not None: - matching_sets.append( - { - i - for i, bond in enumerate(self.interactions) - if bond.get("cells") - and all(np.all(np.equal(bond.get("cells"), cell), axis=1).any() for cell in cells if cell) - } - ) - - if orbitals is not None: - if not orbitals: - matching_sets.append( - {i for i, bond in enumerate(self.interactions) if all(o is None for o in bond.get("orbitals", []))} - ) - else: - orbital_counts = Counter(orbitals) - matching_orbitals = set() - - for i, bond in enumerate(self.interactions): - bond_orbitals = bond.get("orbitals", []) - - if all( - sum(orbital_suffix in b for b in bond_orbitals if b) >= required_count - for orbital_suffix, required_count in orbital_counts.items() - ): - matching_orbitals.add(i) - - matching_sets.append(matching_orbitals) - - if length is not None: - matching_sets.append( - { - i - for i, bond in enumerate(self.interactions) - if (this_length := bond.get("length")) is not None and length[0] <= this_length <= length[1] - } - ) - - return sorted(set.intersection(*matching_sets)) if matching_sets else [] - - def get_interactions_by_properties( - self: LobsterInteractionsHolder, - indices: list[int] | None = None, - centers: list[str] | None = None, - cells: list[list[int]] | None = None, - orbitals: list[str] | None = None, - length: tuple[float, float] | None = None, - ) -> list[LobsterInteractionData]: - """Return interaction metadata dicts that match the provided filters. - - Args: - indices (list[int] | None): Interaction indices to filter. - centers (list[str] | None): Atom centers to filter. - cells (list[list[int]] | None): Unit cell indices to filter. - orbitals (list[str] | None): Orbitals to filter. - length (tuple[float, float] | None): Length range to filter. - - Returns: - list[dict]: List of interaction dictionaries matching the filters. - """ - interaction_indices = self.get_interaction_indices_by_properties(indices, centers, cells, orbitals, length) - - return [bond for i, bond in enumerate(self.interactions) if i in interaction_indices] - - @staticmethod - def get_label_from_interaction( - interaction: LobsterInteraction, - include_centers: bool = True, - include_orbitals: bool = True, - include_cells: bool = False, - include_length: bool = False, - ) -> str: - """ - Generates a label string for a given interaction. - - Args: - interaction (LobsterInteraction): Interaction metadata dictionary. - Returns: - str: Formatted label string representing the interaction. - """ - parts = [] - - for center, orbital, cell in zip( - interaction["centers"], - interaction["orbitals"], - interaction["cells"], - strict=True, - ): - tmp = "" - if include_centers: - tmp += center - - if include_cells and cell is not None: - tmp += f"[{' '.join(map(str, cell))}]" - - if include_orbitals and orbital: - tmp += f"[{orbital}]" - - parts.append(tmp) - - if not parts: - raise ValueError(f"Could not generate label from interaction {interaction}") - - if include_length and interaction["length"] is not None: - parts[-1] += f"({interaction['length']:.3f})" - - return "->".join(parts) - - @abstractmethod - def process_data_into_interactions(self) -> None: - """ - Abstract method to process raw data into structured interaction metadata. Must be implemented by subclasses. - This method should populate the `interactions` attribute based on the raw `data` and any other relevant - attributes. The exact processing logic will depend on the specific format and content of the data in the - subclass. - """ - ... - - @classmethod - def from_dict(cls, d: dict[str, Any]) -> Self: - """Deserialize object from dictionary produced by `as_dict`. - - Args: - d (dict): Dictionary produced by `as_dict`. - - Returns: - COXXCAR: Reconstructed instance. - """ - instance = super().from_dict(d) - instance.data = np.asarray(instance.data, dtype=np.float64) - - instance.process_data_into_interactions() - - return instance diff --git a/src/pymatgen/io/lobster/future/inputs.py b/src/pymatgen/io/lobster/future/inputs.py deleted file mode 100644 index fb583a5a98..0000000000 --- a/src/pymatgen/io/lobster/future/inputs.py +++ /dev/null @@ -1,959 +0,0 @@ -"""Module for reading LOBSTER input files. -For more information on LOBSTER see www.cohp.de. - -If you use this module, please cite: -J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, -"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", -ChemPlusChem 2022, e202200123, -DOI: 10.1002/cplu.202200123. -""" - -from __future__ import annotations - -import itertools -import os -import re -import warnings -from collections import UserDict -from typing import TYPE_CHECKING - -import numpy as np -import spglib -from monty.io import zopen -from monty.json import MSONable -from monty.serialization import loadfn - -from pymatgen.core.structure import Structure -from pymatgen.io.vasp import Vasprun -from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar -from pymatgen.symmetry.bandstructure import HighSymmKpath -from pymatgen.util.due import Doi, due - -if TYPE_CHECKING: - from typing import Any, ClassVar, Literal, Self - - from pymatgen.core.composition import Composition - from pymatgen.core.structure import IStructure - from pymatgen.util.typing import PathLike - -MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) - -__author__ = "Janine George, Marco Esters" -__copyright__ = "Copyright 2017, The Materials Project" -__version__ = "0.2" -__maintainer__ = "Janine George" -__email__ = "janinegeorge.ulfen@gmail.com" -__date__ = "Dec 13, 2017" - - -due.cite( - Doi("10.1002/cplu.202200123"), - description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", -) - - -class LobsterIn(UserDict, MSONable): - """Handles and generates lobsterin files for LOBSTER calculations. - - This class provides methods to create, modify, and write `lobsterin` files, - which are input files for the LOBSTER program. It also includes utilities - for generating related files such as INCAR, KPOINTS, and POSCAR, and for - comparing different `lobsterin` configurations. - - Reminder: LOBSTER input keywords are not case-sensitive. - - Attributes: - FLOAT_KEYWORDS (ClassVar[dict[str, str]]): Keywords requiring float values. - STRING_KEYWORDS (ClassVar[dict[str, str]]): Keywords requiring string values. - BOOLEAN_KEYWORDS (ClassVar[dict[str, str]]): Keywords that are boolean flags. - LIST_KEYWORDS (ClassVar[dict[str, str]]): Keywords requiring lists of strings. - AVAILABLE_KEYWORDS (ClassVar[dict[str, str]]): All known keywords mapped to their canonical forms. - """ - - # These keywords need an additional float suffix - _FLOAT_KEYWORDS: tuple[str, ...] = ( - "COHPstartEnergy", - "COHPendEnergy", - "gaussianSmearingWidth", - "useDecimalPlaces", - "COHPSteps", - "basisRotation", - "gridDensityForPrinting", - "gridBufferForPrinting", - ) - - # These keywords need an additional string suffix - _STRING_KEYWORDS: tuple[str, ...] = ( - "basisSet", - "cohpGenerator", - "realspaceHamiltonian", - "realspaceOverlap", - "printPAWRealSpaceWavefunction", - "printLCAORealSpaceWavefunction", - "kSpaceCOHP", - "EwaldSum", - ) - - # The keywords themselves (without suffix) can trigger additional functionalities - _BOOLEAN_KEYWORDS: tuple[str, ...] = ( - "saveProjectionToFile", - "skipCar", - "skipdos", - "skipcohp", - "skipcoop", - "skipcobi", - "skipMOFE", - "skipMolecularOrbitals", - "skipMadelungEnergy", - "loadProjectionFromFile", - "printTotalSpilling", - "forceEnergyRange", - "DensityOfEnergy", - "BWDF", - "BWDFCOHP", - "skipPopulationAnalysis", - "skipGrossPopulation", - "userecommendedbasisfunctions", - "skipProjection", - "printLmosOnAtoms", - "printMofeAtomWise", - "printMofeMoleculeWise", - "writeAtomicOrbitals", - "writeBasisFunctions", - "writeMatricesToFile", - "noFFTforVisualization", - "RMSp", - "onlyReadVasprun.xml", - "noMemoryMappedFiles", - "skipPAWOrthonormalityTest", - "doNotIgnoreExcessiveBands", - "doNotUseAbsoluteSpilling", - "skipReOrthonormalization", - "forceV1HMatrix", - "useOriginalTetrahedronMethod", - "forceEnergyRange", - "bandwiseSpilling", - "kpointwiseSpilling", - "LSODOS", - "autoRotate", - "doNotOrthogonalizeBasis", - ) - - # These keywords need additional string suffixes. - # They could be used multiple times within one lobsterin. - _LIST_KEYWORDS: tuple[str, ...] = ( - "basisfunctions", - "cohpbetween", - "createFatband", - "customSTOforAtom", - "cobiBetween", - "printLmosOnAtomswriteAtomicDensities", - ) - - # Generate {lowered: original} mappings - FLOAT_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _FLOAT_KEYWORDS} - STRING_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _STRING_KEYWORDS} - BOOLEAN_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _BOOLEAN_KEYWORDS} - LIST_KEYWORDS: ClassVar[dict[str, str]] = {key.lower(): key for key in _LIST_KEYWORDS} - - # All known keywords - AVAILABLE_KEYWORDS: ClassVar[dict[str, str]] = { - **FLOAT_KEYWORDS, - **STRING_KEYWORDS, - **BOOLEAN_KEYWORDS, - **LIST_KEYWORDS, - } - - def __init__(self, settingsdict: dict) -> None: - """Initializes a LobsterIn object with the given settings. - - Args: - settingsdict (dict): Dictionary of settings to initialize the LobsterIn object. - - Raises: - KeyError: If there are duplicate keywords (case-insensitive). - """ - super().__init__() - - # Check for duplicates from case sensitivity - keys = tuple(map(str.lower, settingsdict.keys())) - if len(keys) != len(set(keys)): - raise KeyError("There are duplicates for the keywords!") - - self.update(settingsdict) - - def __setitem__(self, key: str, val: Any) -> None: - """Sets a keyword-value pair in the LobsterIn object. - - Args: - key (str): The keyword to set. - val (Any): The value to associate with the keyword. - - Raises: - KeyError: If the keyword is not recognized. - """ - key = key.strip().lower() - - if key not in type(self).AVAILABLE_KEYWORDS: - raise KeyError(f"Key {key} is currently not available") - - super().__setitem__(key, val.strip() if isinstance(val, str) else val) - - def __getitem__(self, key: str) -> Any: - """Gets the value associated with a keyword. - - Args: - key (str): The keyword to retrieve. - - Returns: - Any: The value associated with the keyword. - - Raises: - KeyError: If the keyword is not found. - """ - try: - return super().__getitem__(key.strip().lower()) - - except KeyError as exc: - raise KeyError(f"{key=} is not available") from exc - - def __contains__(self, key: str) -> bool: - """Checks if a keyword exists in the LobsterIn object. - - Args: - key (str): The keyword to check. - - Returns: - bool: True if the keyword exists, False otherwise. - """ - return super().__contains__(key.lower().strip()) - - def __delitem__(self, key: str) -> None: - """Deletes a keyword from the LobsterIn object. - - Args: - key (str): The keyword to delete. - """ - super().__delitem__(key.lower().strip()) - - def diff(self, other: Self) -> dict[str, dict[str, Any]]: - """Compares two LobsterIn objects and identifies differences. - - Args: - other (LobsterIn): The other LobsterIn object to compare. - - Returns: - dict[str, dict[str, Any]]: A dictionary with keys "Same" and "Different", - containing the same and differing parameters, respectively. - """ - same_params = {} - diff_params = {} - - # Check self - for k1, v1 in self.items(): - if k1 not in other: - diff_params[k1] = {"lobsterin1": v1, "lobsterin2": None} - - # String keywords - elif isinstance(v1, str): - if v1 != other[k1]: - diff_params[k1] = {"lobsterin1": v1, "lobsterin2": other[k1]} - else: - same_params[k1] = v1 - - # List keywords - elif isinstance(v1, list): - new_set1 = {value.strip().lower() for value in v1} - new_set2 = {value.strip().lower() for value in other[k1]} - if new_set1 != new_set2: - diff_params[k1] = {"lobsterin1": v1, "lobsterin2": other[k1]} - - # Float/boolean keywords - elif v1 != other[k1]: - diff_params[k1] = {"lobsterin1": v1, "lobsterin2": other[k1]} - else: - same_params[k1] = v1 - - # Check other - for k2, v2 in other.items(): - if k2 not in self and k2 not in diff_params: - diff_params[k2] = {"lobsterin1": None, "lobsterin2": v2} - - return {"Same": same_params, "Different": diff_params} - - def write_lobsterin( - self, - path: PathLike = "lobsterin", - overwritedict: dict | None = None, - ) -> None: - """Writes the LobsterIn object to a `lobsterin` file. - - Args: - path (PathLike): The output file path. Defaults to "lobsterin". - overwritedict (dict | None): A dictionary of settings to overwrite before writing. - """ - # Update previous entries - if overwritedict is not None: - self.update(overwritedict) - - with open(path, mode="w", encoding="utf-8") as file: - for key in self: - if key in type(self).FLOAT_KEYWORDS or key in type(self).STRING_KEYWORDS: - file.write(f"{type(self).AVAILABLE_KEYWORDS[key]} {self.get(key)}\n") - - elif key in type(self).BOOLEAN_KEYWORDS: - file.write(f"{type(self).BOOLEAN_KEYWORDS[key]}\n") - - elif key in type(self).LIST_KEYWORDS: - file.writelines(f"{type(self).LIST_KEYWORDS[key]} {value}\n" for value in self.get(key)) - - def as_dict(self) -> dict: - """Converts the LobsterIn object to a dictionary. - - Returns: - dict: A dictionary representation of the LobsterIn object. - """ - dct = dict(self) - dct["@module"] = type(self).__module__ - dct["@class"] = type(self).__name__ - return dct - - @classmethod - def from_dict(cls, dct: dict) -> Self: - """Creates a LobsterIn object from a dictionary. - - Args: - dct (dict): A dictionary representation of a LobsterIn object. - - Returns: - LobsterIn: The created LobsterIn object. - """ - return cls({key: val for key, val in dct.items() if key not in {"@module", "@class"}}) - - def _get_nbands(self, structure: Structure) -> int: - """Calculates the number of bands based on the structure and basis functions. - - Args: - structure (Structure): The structure object. - - Returns: - int: The number of bands. - - Raises: - ValueError: If no basis functions are provided. - """ - if self.get("basisfunctions") is None: - raise ValueError("No basis functions are provided. The program cannot calculate nbands.") - - basis_functions: list[str] = [] - for string_basis in self["basisfunctions"]: - string_basis_raw = string_basis.strip().split(" ") - while "" in string_basis_raw: - string_basis_raw.remove("") - for _idx in range(int(structure.composition.element_composition[string_basis_raw[0]])): - basis_functions.extend(string_basis_raw[1:]) - - num_basis_functions = 0 - for basis in basis_functions: - if "s" in basis: - num_basis_functions += 1 - elif "p" in basis: - num_basis_functions += 3 - elif "d" in basis: - num_basis_functions += 5 - elif "f" in basis: - num_basis_functions += 7 - - return int(num_basis_functions) - - def write_INCAR( - self, - incar_input: PathLike = "INCAR", - incar_output: PathLike = "INCAR.lobster", - poscar_input: PathLike = "POSCAR", - isym: Literal[-1, 0] = 0, - further_settings: dict | None = None, - ) -> None: - """Writes a modified INCAR file for LOBSTER calculations. - - Args: - incar_input (PathLike): Path to the input INCAR file. - incar_output (PathLike): Path to the output INCAR file. - poscar_input (PathLike): Path to the input POSCAR file. - isym (Literal[-1, 0]): ISYM value to set. Defaults to 0. - further_settings (dict | None): Additional settings to include in the INCAR file. - - Raises: - ValueError: If `isym` is not -1 or 0. - """ - # Read INCAR from file, which will be modified - incar = Incar.from_file(incar_input) - warnings.warn( - "Please check your incar_input before using it. This method only changes three settings!", - stacklevel=2, - ) - if isym in {-1, 0}: - incar["ISYM"] = isym - else: - raise ValueError(f"Got {isym=}, must be -1 or 0") - - incar["NSW"] = 0 - incar["LWAVE"] = True - - # Get NBANDS from _get_nbands (use basis set that is inserted) - incar["NBANDS"] = self._get_nbands(Structure.from_file(poscar_input)) - if further_settings is not None: - for key, item in further_settings.items(): - incar[key] = item - incar.write_file(incar_output) - - @staticmethod - def get_basis( - structure: Structure | IStructure, - potcar_symbols: list[str], - address_basis_file: PathLike | None = None, - ) -> list[str]: - """Retrieves basis functions for the given structure and POTCAR symbols. - - Args: - structure (Structure | IStructure): The structure object. - potcar_symbols (list[str]): List of POTCAR symbols. - address_basis_file (PathLike | None): Path to the basis file. Defaults to None. - - Returns: - list[str]: List of basis functions. - - Raises: - ValueError: If the POSCAR does not match the POTCAR or if basis information is missing. - """ - if address_basis_file is None: - address_basis_file = f"{MODULE_DIR}/lobster_basis/BASIS_PBE_54_standard.yaml" - - atom_types_potcar = [name.split("_")[0] for name in potcar_symbols] - - if set(structure.symbol_set) != set(atom_types_potcar): - raise ValueError("Your POSCAR does not correspond to your POTCAR!") - - basis = loadfn(address_basis_file)["BASIS"] - - basis_functions = [] - list_forin = [] - for idx, name in enumerate(potcar_symbols): - if name not in basis: - raise ValueError( - f"Missing basis information for POTCAR symbol: {name}. Please provide the basis manually." - ) - basis_functions.append(basis[name].split()) - list_forin.append(f"{atom_types_potcar[idx]} {basis[name]}") - return list_forin - - @staticmethod - def get_all_possible_basis_functions( - structure: Structure | IStructure, - potcar_symbols: list[str], - address_basis_file_min: PathLike | None = None, - address_basis_file_max: PathLike | None = None, - ) -> list[dict]: - """ - Args: - structure: Structure object - potcar_symbols: list of the potcar symbols - address_basis_file_min: path to file with the minimum required basis by the POTCAR - address_basis_file_max: path to file with the largest possible basis of the POTCAR. - - Returns: - list[dict]: Can be used to create new Lobsterin objects in - standard_calculations_from_vasp_files as dict_for_basis - """ - max_basis = LobsterIn.get_basis( - structure=structure, - potcar_symbols=potcar_symbols, - address_basis_file=address_basis_file_max or f"{MODULE_DIR}/lobster_basis/BASIS_PBE_54_max.yaml", - ) - min_basis = LobsterIn.get_basis( - structure=structure, - potcar_symbols=potcar_symbols, - address_basis_file=address_basis_file_min or f"{MODULE_DIR}/lobster_basis/BASIS_PBE_54_min.yaml", - ) - all_basis = get_all_possible_basis_combinations(min_basis=min_basis, max_basis=max_basis) - list_basis_dict = [] - for basis in all_basis: - basis_dict = {} - - for elba in basis: - basplit = elba.split() - basis_dict[basplit[0]] = " ".join(basplit[1:]) - list_basis_dict.append(basis_dict) - return list_basis_dict - - @staticmethod - def write_POSCAR_with_standard_primitive( - POSCAR_input: PathLike = "POSCAR", - POSCAR_output: PathLike = "POSCAR.lobster", - symprec: float = 0.01, - ) -> None: - """Writes a POSCAR file with the standard primitive cell. - - Args: - POSCAR_input (PathLike): Path to the input POSCAR file. - POSCAR_output (PathLike): Path to the output POSCAR file. - symprec (float): Precision for symmetry determination. Defaults to 0.01. - """ - structure = Structure.from_file(POSCAR_input) - kpath = HighSymmKpath(structure, symprec=symprec) - new_structure = kpath.prim - new_structure.to(fmt="POSCAR", filename=POSCAR_output) - - @staticmethod - def write_KPOINTS( - POSCAR_input: PathLike = "POSCAR", - KPOINTS_output: PathLike = "KPOINTS.lobster", - reciprocal_density: int = 100, - isym: Literal[-1, 0] = 0, - from_grid: bool = False, - input_grid: tuple[int, int, int] = (5, 5, 5), - line_mode: bool = True, - kpoints_line_density: int = 20, - symprec: float = 0.01, - ) -> None: - """Write a gamma-centered KPOINTS file for LOBSTER. - - Args: - POSCAR_input (PathLike): path to POSCAR - KPOINTS_output (PathLike): path to output KPOINTS - reciprocal_density (int): Grid density - isym (-1 | 0): ISYM value. - from_grid (bool): If True KPOINTS will be generated with the help of a grid given in input_grid. - Otherwise, they will be generated from the reciprocal_density - input_grid (tuple): grid to generate the KPOINTS file - line_mode (bool): If True, band structure will be generated - kpoints_line_density (int): density of the lines in the band structure - symprec (float): precision to determine symmetry - """ - structure = Structure.from_file(POSCAR_input) - if not from_grid: - kpoint_grid = Kpoints.automatic_density_by_vol(structure, reciprocal_density).kpts - mesh = kpoint_grid[0] - else: - mesh = input_grid - - # The following code is taken from SpacegroupAnalyzer - # We need to switch off symmetry here - matrix = structure.lattice.matrix - positions = structure.frac_coords - unique_species: list[Composition] = [] - zs = [] - magmoms = [] - - for species, group in itertools.groupby(structure, key=lambda s: s.species): - if species in unique_species: - ind = unique_species.index(species) - zs.extend([ind + 1] * len(tuple(group))) - else: - unique_species.append(species) - zs.extend([len(unique_species)] * len(tuple(group))) - - for site in structure: - if hasattr(site, "magmom"): - magmoms.append(site.magmom) - elif site.is_ordered and hasattr(site.specie, "spin"): - magmoms.append(site.specie.spin) - else: - magmoms.append(0) - - # For now, we are setting MAGMOM to zero. (Taken from INCAR class) - cell = matrix, positions, zs, magmoms - # TODO: what about this shift? - mapping, grid = spglib.get_ir_reciprocal_mesh(mesh, cell, is_shift=[0, 0, 0]) # type:ignore[arg-type] - - # Get the KPOINTS for the grid - if isym == -1: - kpts = [] - weights = [] - all_labels = [] - for gp in grid: - kpts.append(gp.astype(float) / mesh) - weights.append(float(1)) - all_labels.append("") - - # Time reversal symmetry: k and -k are equivalent - elif isym == 0: - kpts = [] - weights = [] - all_labels = [] - newlist = [list(gp) for gp in list(grid)] - mapping = [] # type:ignore[assignment] - for gp in newlist: - minus_gp = [-k for k in gp] - if minus_gp in newlist and minus_gp != [0, 0, 0]: - mapping.append(newlist.index(minus_gp)) - else: - mapping.append(newlist.index(gp)) - - for igp, gp in enumerate(newlist): - if mapping[igp] > igp: - kpts.append(np.array(gp).astype(float) / mesh) - weights.append(float(2)) - all_labels.append("") - elif mapping[igp] == igp: - kpts.append(np.array(gp).astype(float) / mesh) - weights.append(float(1)) - all_labels.append("") - - else: - raise ValueError(f"Got {isym=}, must be -1 or 0") - - # Line mode - if line_mode: - kpath = HighSymmKpath(structure, symprec=symprec) - if not np.allclose(kpath.prim.lattice.matrix, structure.lattice.matrix): - raise ValueError( - "You are not using the standard primitive cell. The k-path is not correct. Please generate a " - "standard primitive cell first." - ) - - frac_k_points, labels = kpath.get_kpoints(line_density=kpoints_line_density, coords_are_cartesian=False) - - for k, f in enumerate(frac_k_points): - kpts.append(f) - weights.append(0.0) - all_labels.append(labels[k]) - comment = f"{isym=}, grid: {mesh} plus kpoint path" if line_mode else f"{isym=}, grid: {mesh}" - - kpoints_instance = Kpoints( - comment=comment, - style=Kpoints.supported_modes.Reciprocal, - num_kpts=len(kpts), - kpts=tuple(kpts), - kpts_weights=weights, - labels=all_labels, - ) - - kpoints_instance.write_file(filename=KPOINTS_output) - - @classmethod - def from_file(cls, lobsterin: PathLike) -> Self: - """Creates a LobsterIn object from a `lobsterin` file. - - Args: - lobsterin (PathLike): Path to the `lobsterin` file. - - Returns: - LobsterIn: The created LobsterIn object. - - Raises: - RuntimeError: If the `lobsterin` file contains no data. - ValueError: If invalid keywords are found or if there are duplicate keywords. - """ - with zopen(lobsterin, mode="rt", encoding="utf-8") as file: - lines: list[str] = file.read().split("\n") # type:ignore[arg-type,assignment] - if not lines: - raise RuntimeError("lobsterin file contains no data.") - - lobsterin_dict: dict[str, Any] = {} - for line in lines: - # Remove comment lines and in-line comments - if line := re.split(r"[!#//]", line)[0].strip(): - # Extract keywords - line_parts = line.replace("\t", " ").strip().split() - if line_parts: - key = line_parts[0].lower() - else: - continue - - # Avoid duplicates for float/string keywords - if (key in cls.FLOAT_KEYWORDS or key in cls.STRING_KEYWORDS) and key in lobsterin_dict: - raise ValueError(f"Same keyword {key} twice!") - - # Parse by keyword type - if key in cls.BOOLEAN_KEYWORDS: - lobsterin_dict[key] = True - - elif key in cls.FLOAT_KEYWORDS: - lobsterin_dict[key] = float(line_parts[1]) - - elif key in cls.STRING_KEYWORDS: - lobsterin_dict[key] = " ".join(line_parts[1:]) - - elif key in cls.LIST_KEYWORDS: - if key in lobsterin_dict: - lobsterin_dict[key].append(" ".join(line_parts[1:])) - else: - lobsterin_dict[key] = [" ".join(line_parts[1:])] - - else: - raise ValueError(f"Invalid {key=}.") - - return cls(lobsterin_dict) - - @staticmethod - def _get_potcar_symbols(POTCAR_input: PathLike) -> list[str]: - """Get the name of the species in the POTCAR. - - Args: - POTCAR_input (PathLike): path to potcar file - - Returns: - list[str]: names of the species - """ - potcar = Potcar.from_file(POTCAR_input) # type:ignore[arg-type] - for pot in potcar: - if pot.potential_type != "PAW": - raise ValueError("Lobster only works with PAW! Use different POTCARs") - - # Warning about a bug in LOBSTER-4.1.0 - with zopen(POTCAR_input, mode="rt", encoding="utf-8") as file: - data = file.read() - - if isinstance(data, bytes): - data = data.decode("utf-8") - - if "SHA256" in data or "COPYR" in data: - warnings.warn( - "These POTCARs are not compatible with " - "Lobster up to version 4.1.0." - "\n The keywords SHA256 and COPYR " - "cannot be handled by Lobster" - " \n and will lead to wrong results.", - stacklevel=2, - ) - - if potcar.functional != "PBE": - raise RuntimeError("We only have BASIS options for PBE so far") - - return [name["symbol"] for name in potcar.spec] - - @classmethod - def standard_calculations_from_vasp_files( - cls, - POSCAR_input: PathLike = "POSCAR", - INCAR_input: PathLike = "INCAR", - POTCAR_input: PathLike | None = None, - Vasprun_output: PathLike = "vasprun.xml", - dict_for_basis: dict | None = None, - option: str = "standard", - ) -> Self: - """Generate lobsterin with standard settings. - - Args: - POSCAR_input (PathLike): path to POSCAR - INCAR_input (PathLike): path to INCAR - POTCAR_input (PathLike): path to POTCAR - Vasprun_output (PathLike): path to vasprun.xml - dict_for_basis (dict): can be provided: it should look the following: - dict_for_basis={"Fe":'3p 3d 4s 4f', "C": '2s 2p'} and will overwrite all settings from POTCAR_input - option (str): 'standard' will start a normal LOBSTER run where COHPs, COOPs, DOS, CHARGE etc. will be - calculated - 'standard_with_energy_range_from_vasprun' will start a normal LOBSTER run for entire energy range - of VASP static run. vasprun.xml file needs to be in current directory. - 'standard_from_projection' will start a normal LOBSTER run from a projection - 'standard_with_fatband' will do a fatband calculation, run over all orbitals - 'onlyprojection' will only do a projection - 'onlydos' will only calculate a projected dos - 'onlycohp' will only calculate cohp - 'onlycoop' will only calculate coop - 'onlycohpcoop' will only calculate cohp and coop - - Returns: - Lobsterin with standard settings - """ - warnings.warn( - "Always check and test the provided basis functions. The spilling of your Lobster calculation might help", - stacklevel=2, - ) - - if option not in { - "standard", - "standard_from_projection", - "standard_with_fatband", - "standard_with_energy_range_from_vasprun", - "onlyprojection", - "onlydos", - "onlycohp", - "onlycoop", - "onlycobi", - "onlycohpcoop", - "onlycohpcoopcobi", - "onlymadelung", - }: - raise ValueError("The option is not valid!") - - lobsterin_dict: dict[str, Any] = { - # This basis set covers elements up to Lr (Z = 103) - "basisSet": "pbeVaspFit2015", - # Energies around e-fermi - "COHPstartEnergy": -35.0, - "COHPendEnergy": 5.0, - } - - if option in { - "standard", - "standard_with_energy_range_from_vasprun", - "onlycohp", - "onlycoop", - "onlycobi", - "onlycohpcoop", - "onlycohpcoopcobi", - "standard_with_fatband", - }: - # Every interaction with a distance of 6.0 Å is checked - lobsterin_dict["cohpGenerator"] = "from 0.1 to 6.0 orbitalwise" - # Save the projection - lobsterin_dict["saveProjectionToFile"] = True - - if option == "standard_from_projection": - lobsterin_dict["cohpGenerator"] = "from 0.1 to 6.0 orbitalwise" - lobsterin_dict["loadProjectionFromFile"] = True - - elif option == "standard_with_energy_range_from_vasprun": - vasp_run = Vasprun(Vasprun_output) - lobsterin_dict["COHPstartEnergy"] = round( - min(vasp_run.complete_dos.energies - vasp_run.complete_dos.efermi), 4 - ) - lobsterin_dict["COHPendEnergy"] = round( - max(vasp_run.complete_dos.energies - vasp_run.complete_dos.efermi), 4 - ) - lobsterin_dict["COHPSteps"] = len(vasp_run.complete_dos.energies) - - # TODO: add COBI here! might be relevant LOBSTER version - elif option == "onlycohp": - lobsterin_dict["skipdos"] = True - lobsterin_dict["skipcoop"] = True - lobsterin_dict["skipPopulationAnalysis"] = True - lobsterin_dict["skipGrossPopulation"] = True - # LOBSTER-4.1.0 - lobsterin_dict["skipcobi"] = True - lobsterin_dict["skipMadelungEnergy"] = True - - elif option == "onlycoop": - lobsterin_dict["skipdos"] = True - lobsterin_dict["skipcohp"] = True - lobsterin_dict["skipPopulationAnalysis"] = True - lobsterin_dict["skipGrossPopulation"] = True - # LOBSTER-4.1.0 - lobsterin_dict["skipcobi"] = True - lobsterin_dict["skipMadelungEnergy"] = True - - elif option == "onlycohpcoop": - lobsterin_dict["skipdos"] = True - lobsterin_dict["skipPopulationAnalysis"] = True - lobsterin_dict["skipGrossPopulation"] = True - # LOBSTER-4.1.0 - lobsterin_dict["skipcobi"] = True - lobsterin_dict["skipMadelungEnergy"] = True - - elif option == "onlycohpcoopcobi": - lobsterin_dict["skipdos"] = True - lobsterin_dict["skipPopulationAnalysis"] = True - lobsterin_dict["skipGrossPopulation"] = True - lobsterin_dict["skipMadelungEnergy"] = True - - elif option == "onlydos": - lobsterin_dict["skipcohp"] = True - lobsterin_dict["skipcoop"] = True - lobsterin_dict["skipPopulationAnalysis"] = True - lobsterin_dict["skipGrossPopulation"] = True - # LOBSTER-4.1.0 - lobsterin_dict["skipcobi"] = True - lobsterin_dict["skipMadelungEnergy"] = True - - elif option == "onlyprojection": - lobsterin_dict["skipdos"] = True - lobsterin_dict["skipcohp"] = True - lobsterin_dict["skipcoop"] = True - lobsterin_dict["skipPopulationAnalysis"] = True - lobsterin_dict["skipGrossPopulation"] = True - lobsterin_dict["saveProjectionToFile"] = True - # LOBSTER-4.1.0 - lobsterin_dict["skipcobi"] = True - lobsterin_dict["skipMadelungEnergy"] = True - - elif option == "onlycobi": - lobsterin_dict["skipdos"] = True - lobsterin_dict["skipcohp"] = True - lobsterin_dict["skipPopulationAnalysis"] = True - lobsterin_dict["skipGrossPopulation"] = True - # LOBSTER-4.1.0 - lobsterin_dict["skipcobi"] = True - lobsterin_dict["skipMadelungEnergy"] = True - - elif option == "onlymadelung": - lobsterin_dict["skipdos"] = True - lobsterin_dict["skipcohp"] = True - lobsterin_dict["skipcoop"] = True - lobsterin_dict["skipPopulationAnalysis"] = True - lobsterin_dict["skipGrossPopulation"] = True - lobsterin_dict["saveProjectionToFile"] = True - # LOBSTER-4.1.0 - lobsterin_dict["skipcobi"] = True - - incar = Incar.from_file(INCAR_input) - - if incar["ISMEAR"] == 0: - lobsterin_dict["gaussianSmearingWidth"] = incar["SIGMA"] - - if incar["ISMEAR"] != 0 and option == "standard_with_fatband": - raise ValueError("ISMEAR has to be 0 for a fatband calculation with Lobster") - - if dict_for_basis is not None: - # dict_for_basis = {"Fe":"3p 3d 4s 4f", "C": "2s 2p"} - # Will just insert this basis and not check with poscar - basis = [f"{key} {value}" for key, value in dict_for_basis.items()] - elif POTCAR_input is not None: - # Get basis functions from POTCAR - potcar_names = cls._get_potcar_symbols(POTCAR_input=POTCAR_input) - - basis = cls.get_basis(structure=Structure.from_file(POSCAR_input), potcar_symbols=potcar_names) - else: - raise ValueError("basis cannot be generated") - - lobsterin_dict["basisfunctions"] = basis - - if option == "standard_with_fatband": - lobsterin_dict["createFatband"] = basis - - return cls(lobsterin_dict) - - -def get_all_possible_basis_combinations(min_basis: list, max_basis: list) -> list[list[str]]: - """Get all possible basis combinations. - - Args: - min_basis: list of basis entries: e.g., ["Si 3p 3s"] - max_basis: list of basis entries: e.g., ["Si 3p 3s"]. - - Returns: - list[list[str]]: all possible combinations of basis functions, e.g. [["Si 3p 3s"]] - """ - max_basis_lists = [x.split() for x in max_basis] - min_basis_lists = [x.split() for x in min_basis] - - # Get all possible basis functions - basis_dict: dict[str, dict] = {} - for iel, el in enumerate(max_basis_lists): - basis_dict[el[0]] = {"fixed": [], "variable": [], "combinations": []} - for basis in el[1:]: - if basis in min_basis_lists[iel]: - basis_dict[el[0]]["fixed"].append(basis) - if basis not in min_basis_lists[iel]: - basis_dict[el[0]]["variable"].append(basis) - for L in range(len(basis_dict[el[0]]["variable"]) + 1): - for subset in itertools.combinations(basis_dict[el[0]]["variable"], L): - basis_dict[el[0]]["combinations"].append(" ".join([el[0]] + basis_dict[el[0]]["fixed"] + list(subset))) - - list_basis = [item["combinations"] for item in basis_dict.values()] - - # Get all combinations - start_basis = list_basis[0] - if len(list_basis) > 1: - for el in list_basis[1:]: - new_start_basis = [] - for elbasis in start_basis: - for elbasis2 in el: - if not isinstance(elbasis, list): - new_start_basis.append([elbasis, elbasis2]) - else: - new_start_basis.append([*elbasis.copy(), elbasis2]) - start_basis = new_start_basis - return start_basis - return [[basis] for basis in start_basis] diff --git a/src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_max.yaml b/src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_max.yaml deleted file mode 100644 index e4ed957f2a..0000000000 --- a/src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_max.yaml +++ /dev/null @@ -1,189 +0,0 @@ -BASIS: - Ac: '5f 6d 6p 6s 7s ' - Ag: '4d 5p 5s ' - Ag_pv: '4d 4p 5p 5s ' - Al: '3p 3s ' - Am: '5f 6d 6p 6s 7s ' - Ar: '3p 3s ' - As: '4p 4s ' - As_d: '3d 4p 4s ' - At: '6p 6s ' - Au: '5d 6p 6s ' - B: '2p 2s ' - B_h: '2p 2s ' - B_s: '2p 2s ' - Ba_sv: '5p 5s 6s ' - Be: '2p 2s ' - Be_sv: '1s 2p 2s ' - Bi: '6p 6s ' - Bi_d: '5d 6p 6s ' - Br: '4p 4s ' - C: '2p 2s ' - C_h: '2p 2s ' - C_s: '2p 2s ' - Ca_pv: '3p 4s ' - Ca_sv: '3p 3s 4s ' - Cd: '4d 5p 5s ' - Ce: '4f 5d 5p 5s 6s ' - Ce_3: '5d 5p 5s 6s ' - Ce_h: '4f 5d 5p 5s 6s ' - Cf: '5f 6p 6s 7s ' - Cl: '3p 3s ' - Cl_h: '3p 3s ' - Cm: '5f 6d 6p 6s 7s ' - Co: '3d 4p 4s ' - Co_pv: '3d 3p 4s ' - Co_sv: '3d 3p 3s 4p 4s ' - Cr: '3d 4p 4s ' - Cr_pv: '3d 3p 4s ' - Cr_sv: '3d 3p 3s 4s ' - Cs_sv: '5p 5s 6s ' - Cu: '3d 4p 4s ' - Cu_pv: '3d 3p 4s ' - Dy: '4f 5d 5p 5s 6s ' - Dy_3: '5d 5p 6s ' - Er: '4f 5d 5p 5s 6s ' - Er_2: '5d 5p 6s ' - Er_3: '5d 5p 6s ' - Eu: '4f 5d 5p 5s 6s ' - Eu_2: '5d 5p 6s ' - Eu_3: '5d 5p 6s ' - F: '2p 2s ' - F_h: '2p 2s ' - F_s: '2p 2s ' - Fe: '3d 4p 4s ' - Fe_pv: '3d 3p 4s ' - Fe_sv: '3d 3p 3s 4s ' - Fr_sv: '6p 6s 7s ' - Ga: '4p 4s ' - Ga_d: '3d 4p 4s ' - Ga_h: '3d 4p 4s ' - Gd: '4f 5d 5p 5s 6s ' - Gd_3: '5d 5p 6s ' - Ge: '4p 4s ' - Ge_d: '3d 4p 4s ' - Ge_h: '3d 4p 4s ' - H: '1s ' - H_h: '1s ' - H_s: '1s ' - He: '1s ' - Hf: '5d 6p 6s ' - Hf_pv: '5d 5p 6s ' - Hf_sv: '5d 5p 5s 6s ' - Hg: '5d 6p 6s ' - Ho: '4f 5d 5p 5s 6s ' - Ho_3: '5d 5p 6s ' - I: '5p 5s ' - In: '5p 5s ' - In_d: '4d 5p 5s ' - Ir: '5d 6p 6s ' - K_pv: '3p 4s ' - K_sv: '3p 3s 4s ' - Kr: '4p 4s ' - La: '4f 5d 5p 5s 6s ' - La_s: '4f 5d 5p 6s ' - Li: '2p 2s ' - Li_sv: '1s 2p 2s ' - Lu: '4f 5d 5p 5s 6s ' - Lu_3: '5d 5p 6s ' - Mg: '3p 3s ' - Mg_pv: '2p 3s ' - Mg_sv: '2p 2s 3s ' - Mn: '3d 4p 4s ' - Mn_pv: '3d 3p 4s ' - Mn_sv: '3d 3p 3s 4s ' - Mo: '4d 5p 5s ' - Mo_pv: '4d 4p 5s ' - Mo_sv: '4d 4p 4s 5s ' - N: '2p 2s ' - N_h: '2p 2s ' - N_s: '2p 2s ' - Na: '3p 3s ' - Na_pv: '2p 3s ' - Na_sv: '2p 2s 3s ' - Nb_pv: '4d 4p 5s ' - Nb_sv: '4d 4p 4s 5s ' - Nd: '4f 5d 5p 5s 6s ' - Nd_3: '5d 5p 5s 6s ' - Ne: '2p 2s ' - Ni: '3d 4p 4s ' - Ni_pv: '3d 3p 4s ' - Np: '5f 6d 6p 6s 7s ' - Np_s: '5f 6d 6p 6s 7s ' - O: '2p 2s ' - O_h: '2p 2s ' - O_s: '2p 2s ' - Os: '5d 6p 6s ' - Os_pv: '5d 5p 6s ' - P: '3p 3s ' - P_h: '3p 3s ' - Pa: '5f 6d 6p 6s 7s ' - Pa_s: '5f 6d 6p 7s ' - Pb: '6p 6s ' - Pb_d: '5d 6p 6s ' - Pd: '4d 5p 5s ' - Pd_pv: '4d 4p 5s ' - Pm: '4f 5d 5p 5s 6s ' - Pm_3: '5d 5p 5s 6s ' - Po: '6p 6s ' - Po_d: '5d 6p 6s ' - Pr: '4f 5d 5p 5s 6s ' - Pr_3: '5d 5p 5s 6s ' - Pt: '5d 6p 6s ' - Pt_pv: '5d 5p 6p 6s ' - Pu: '5f 6d 6p 6s 7s ' - Pu_s: '5f 6d 6p 6s 7s ' - Ra_sv: '6p 6s 7s ' - Rb_pv: '4p 5s ' - Rb_sv: '4p 4s 5s ' - Re: '5d 6s ' - Re_pv: '5d 5p 6s ' - Rh: '4d 5p 5s ' - Rh_pv: '4d 4p 5s ' - Rn: '6p 6s ' - Ru: '4d 5p 5s ' - Ru_pv: '4d 4p 5s ' - Ru_sv: '4d 4p 4s 5s ' - S: '3p 3s ' - S_h: '3p 3s ' - Sb: '5p 5s ' - Sc: '3d 4p 4s ' - Sc_sv: '3d 3p 3s 4s ' - Se: '4p 4s ' - Si: '3p 3s ' - Sm: '4f 5d 5p 5s 6s ' - Sm_3: '5d 5p 5s 6s ' - Sn: '5p 5s ' - Sn_d: '4d 5p 5s ' - Sr_sv: '4p 4s 5s ' - Ta: '5d 6p 6s ' - Ta_pv: '5d 5p 6s ' - Tb: '4f 5d 5p 5s 6s ' - Tb_3: '5d 5p 6s ' - Tc: '4d 5p 5s ' - Tc_pv: '4d 4p 5s ' - Tc_sv: '4d 4p 4s 5s ' - Te: '5p 5s ' - Th: '5f 6d 6p 6s 7s ' - Th_s: '5f 6d 6p 7s ' - Ti: '3d 4p 4s ' - Ti_pv: '3d 3p 4s ' - Ti_sv: '3d 3p 3s 4s ' - Tl: '6p 6s ' - Tl_d: '5d 6p 6s ' - Tm: '4f 5d 5p 5s 6s ' - Tm_3: '5d 5p 6s ' - U: '5f 6d 6p 6s 7s ' - U_s: '5f 6d 6p 6s 7s ' - V: '3d 4p 4s ' - V_pv: '3d 3p 4s ' - V_sv: '3d 3p 3s 4s ' - W: '5d 6p 6s ' - W_sv: '5d 5p 5s 6s ' - Xe: '5p 5s ' - Y_sv: '4d 4p 4s 5s ' - Yb: '4f 5d 5p 5s 6s ' - Yb_2: '5d 5p 6s ' - Yb_3: '5d 5p 6s ' - Zn: '3d 4p 4s ' - Zr_sv: '4d 4p 4s 5s ' diff --git a/src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_min.yaml b/src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_min.yaml deleted file mode 100644 index 99fa68ba99..0000000000 --- a/src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_min.yaml +++ /dev/null @@ -1,189 +0,0 @@ -BASIS: - Ac: '6d 6p 6s 7s ' - Ag: '4d 5s ' - Ag_pv: '4d 4p 5s ' - Al: '3p 3s ' - Am: '5f 6d 6p 6s 7s ' - Ar: '3p 3s ' - As: '4p 4s ' - As_d: '3d 4p 4s ' - At: '6p 6s ' - Au: '5d 6s ' - B: '2p 2s ' - B_h: '2p 2s ' - B_s: '2p 2s ' - Ba_sv: '5p 5s 6s ' - Be: '2s ' - Be_sv: '1s 2s ' - Bi: '6p 6s ' - Bi_d: '5d 6p 6s ' - Br: '4p 4s ' - C: '2p 2s ' - C_h: '2p 2s ' - C_s: '2p 2s ' - Ca_pv: '3p 4s ' - Ca_sv: '3p 3s 4s ' - Cd: '4d 5s ' - Ce: '4f 5d 5p 5s 6s ' - Ce_3: '5d 5p 5s 6s ' - Ce_h: '4f 5d 5p 5s 6s ' - Cf: '5f 6p 6s 7s ' - Cl: '3p 3s ' - Cl_h: '3p 3s ' - Cm: '5f 6d 6p 6s 7s ' - Co: '3d 4s ' - Co_pv: '3d 3p 4s ' - Co_sv: '3d 3p 3s 4s ' - Cr: '3d 4s ' - Cr_pv: '3d 3p 4s ' - Cr_sv: '3d 3p 3s 4s ' - Cs_sv: '5p 5s 6s ' - Cu: '3d 4s ' - Cu_pv: '3d 3p 4s ' - Dy: '4f 5d 5p 5s 6s ' - Dy_3: '5d 5p 6s ' - Er: '4f 5d 5p 5s 6s ' - Er_2: '5p 6s ' - Er_3: '5d 5p 6s ' - Eu: '4f 5p 5s 6s ' - Eu_2: '5p 6s ' - Eu_3: '5d 5p 6s ' - F: '2p 2s ' - F_h: '2p 2s ' - F_s: '2p 2s ' - Fe: '3d 4s ' - Fe_pv: '3d 3p 4s ' - Fe_sv: '3d 3p 3s 4s ' - Fr_sv: '6p 6s 7s ' - Ga: '4p 4s ' - Ga_d: '3d 4p 4s ' - Ga_h: '3d 4p 4s ' - Gd: '4f 5d 5p 5s 6s ' - Gd_3: '5d 5p 6s ' - Ge: '4p 4s ' - Ge_d: '3d 4p 4s ' - Ge_h: '3d 4p 4s ' - H: '1s ' - H_h: '1s ' - H_s: '1s ' - He: '1s ' - Hf: '5d 6s ' - Hf_pv: '5d 5p 6s ' - Hf_sv: '5d 5p 5s ' - Hg: '5d 6s ' - Ho: '4f 5d 5p 5s 6s ' - Ho_3: '5d 5p 6s ' - I: '5p 5s ' - In: '5p 5s ' - In_d: '4d 5p 5s ' - Ir: '5d 6s ' - K_pv: '3p 4s ' - K_sv: '3p 3s 4s ' - Kr: '4p 4s ' - La: '5d 5p 5s 6s ' - La_s: '5d 5p 6s ' - Li: '2s ' - Li_sv: '1s 2s ' - Lu: '4f 5d 5p 5s 6s ' - Lu_3: '5d 5p 6s ' - Mg: '3s ' - Mg_pv: '2p 3s ' - Mg_sv: '2p 2s 3s ' - Mn: '3d 4s ' - Mn_pv: '3d 3p 4s ' - Mn_sv: '3d 3p 3s 4s ' - Mo: '4d 5s ' - Mo_pv: '4d 4p 5s ' - Mo_sv: '4d 4p 4s 5s ' - N: '2p 2s ' - N_h: '2p 2s ' - N_s: '2p 2s ' - Na: '3s ' - Na_pv: '2p 3s ' - Na_sv: '2p 2s 3s ' - Nb_pv: '4d 4p 5s ' - Nb_sv: '4d 4p 4s 5s ' - Nd: '4f 5d 5p 5s 6s ' - Nd_3: '5d 5p 5s 6s ' - Ne: '2p 2s ' - Ni: '3d 4s ' - Ni_pv: '3d 3p 4s ' - Np: '5f 6d 6p 6s 7s ' - Np_s: '5f 6d 6p 6s 7s ' - O: '2p 2s ' - O_h: '2p 2s ' - O_s: '2p 2s ' - Os: '5d 6s ' - Os_pv: '5d 5p 6s ' - P: '3p 3s ' - P_h: '3p 3s ' - Pa: '5f 6d 6p 6s 7s ' - Pa_s: '5f 6d 6p 7s ' - Pb: '6p 6s ' - Pb_d: '5d 6p 6s ' - Pd: '4d 5s ' - Pd_pv: '4d 4p 5s ' - Pm: '4f 5d 5p 5s 6s ' - Pm_3: '5d 5p 5s 6s ' - Po: '6p 6s ' - Po_d: '5d 6p 6s ' - Pr: '4f 5d 5p 5s 6s ' - Pr_3: '5d 5p 5s 6s ' - Pt: '5d 6s ' - Pt_pv: '5d 5p 6s ' - Pu: '5f 6d 6p 6s 7s ' - Pu_s: '5f 6d 6p 6s 7s ' - Ra_sv: '6p 6s 7s ' - Rb_pv: '4p 5s ' - Rb_sv: '4p 4s 5s ' - Re: '5d 6s ' - Re_pv: '5d 5p 6s ' - Rh: '4d 5s ' - Rh_pv: '4d 4p 5s ' - Rn: '6p 6s ' - Ru: '4d 5s ' - Ru_pv: '4d 4p 5s ' - Ru_sv: '4d 4p 4s 5s ' - S: '3p 3s ' - S_h: '3p 3s ' - Sb: '5p 5s ' - Sc: '3d 4s ' - Sc_sv: '3d 3p 3s 4s ' - Se: '4p 4s ' - Si: '3p 3s ' - Sm: '4f 5d 5p 5s 6s ' - Sm_3: '5d 5p 5s 6s ' - Sn: '5p 5s ' - Sn_d: '4d 5p 5s ' - Sr_sv: '4p 4s 5s ' - Ta: '5d 6s ' - Ta_pv: '5d 5p 6s ' - Tb: '4f 5d 5p 5s 6s ' - Tb_3: '5d 5p 6s ' - Tc: '4d 5s ' - Tc_pv: '4d 4p 5s ' - Tc_sv: '4d 4p 4s 5s ' - Te: '5p 5s ' - Th: '5f 6d 6p 6s 7s ' - Th_s: '5f 6d 6p 7s ' - Ti: '3d 4s ' - Ti_pv: '3d 3p 4s ' - Ti_sv: '3d 3p 3s 4s ' - Tl: '6p 6s ' - Tl_d: '5d 6p 6s ' - Tm: '4f 5d 5p 5s 6s ' - Tm_3: '5d 5p 6s ' - U: '5f 6d 6p 6s 7s ' - U_s: '5f 6d 6p 6s 7s ' - V: '3d 4s ' - V_pv: '3d 3p 4s ' - V_sv: '3d 3p 3s 4s ' - W: '5d 6s ' - W_sv: '5d 5p 5s 6s ' - Xe: '5p 5s ' - Y_sv: '4d 4p 4s 5s ' - Yb: '4f 5p 5s 6s ' - Yb_2: '5p 6s ' - Yb_3: '5d 5p 6s ' - Zn: '3d 4s ' - Zr_sv: '4d 4p 4s 5s ' diff --git a/src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_standard.yaml b/src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_standard.yaml deleted file mode 100644 index b65b59dfac..0000000000 --- a/src/pymatgen/io/lobster/future/lobster_basis/BASIS_PBE_54_standard.yaml +++ /dev/null @@ -1,189 +0,0 @@ -BASIS: - Ac: '5f 6d 6p 6s 7s ' - Ag: '4d 5p 5s ' - Ag_pv: '4d 4p 5p 5s ' - Al: '3p 3s ' - Am: '5f 6d 6p 6s 7s ' - Ar: '3p 3s ' - As: '4p 4s ' - As_d: '3d 4p 4s ' - At: '6p 6s ' - Au: '5d 6p 6s ' - B: '2p 2s ' - B_h: '2p 2s ' - B_s: '2p 2s ' - Ba_sv: '5p 5s 6s ' - Be: '2p 2s ' - Be_sv: '1s 2p 2s ' - Bi: '6p 6s ' - Bi_d: '5d 6p 6s ' - Br: '4p 4s ' - C: '2p 2s ' - C_h: '2p 2s ' - C_s: '2p 2s ' - Ca_pv: '3p 4s ' - Ca_sv: '3p 3s 4s ' - Cd: '4d 5p 5s ' - Ce: '4f 5d 5p 5s 6s ' - Ce_3: '5d 5p 5s 6s ' - Ce_h: '4f 5d 5p 5s 6s ' - Cf: '5f 6p 6s 7s ' - Cl: '3p 3s ' - Cl_h: '3p 3s ' - Cm: '5f 6d 6p 6s 7s ' - Co: '3d 4p 4s ' - Co_pv: '3d 3p 4s ' - Co_sv: '3d 3p 3s 4p 4s ' - Cr: '3d 4p 4s ' - Cr_pv: '3d 3p 4s ' - Cr_sv: '3d 3p 3s 4s ' - Cs_sv: '5p 5s 6s ' - Cu: '3d 4p 4s ' - Cu_pv: '3d 3p 4s ' - Dy: '4f 5d 5p 5s 6s ' - Dy_3: '5d 5p 6s ' - Er: '4f 5d 5p 5s 6s ' - Er_2: '5d 5p 6s ' - Er_3: '5d 5p 6s ' - Eu: '4f 5d 5p 5s 6s ' - Eu_2: '5d 5p 6s ' - Eu_3: '5d 5p 6s ' - F: '2p 2s ' - F_h: '2p 2s ' - F_s: '2p 2s ' - Fe: '3d 4p 4s ' - Fe_pv: '3d 3p 4s ' - Fe_sv: '3d 3p 3s 4s ' - Fr_sv: '6p 6s 7s ' - Ga: '4p 4s ' - Ga_d: '3d 4p 4s ' - Ga_h: '3d 4p 4s ' - Gd: '4f 5d 5p 5s 6s ' - Gd_3: '5d 5p 6s ' - Ge: '4p 4s ' - Ge_d: '3d 4p 4s ' - Ge_h: '3d 4p 4s ' - H: '1s ' - H_h: '1s ' - H_s: '1s ' - He: '1s ' - Hf: '5d 6p 6s ' - Hf_pv: '5d 5p 6s ' - Hf_sv: '5d 5p 5s 6s ' - Hg: '5d 6p 6s ' - Ho: '4f 5d 5p 5s 6s ' - Ho_3: '5d 5p 6s ' - I: '5p 5s ' - In: '5p 5s ' - In_d: '4d 5p 5s ' - Ir: '5d 6p 6s ' - K_pv: '3p 4s ' - K_sv: '3p 3s 4s ' - Kr: '4p 4s ' - La: '4f 5d 5p 5s 6s ' - La_s: '4f 5d 5p 6s ' - Li: '2p 2s ' - Li_sv: '1s 2s 2p ' - Lu: '4f 5d 5p 5s 6s ' - Lu_3: '5d 5p 6s ' - Mg: '3p 3s ' - Mg_pv: '2p 3s ' - Mg_sv: '2p 2s 3s ' - Mn: '3d 4p 4s ' - Mn_pv: '3d 3p 4s ' - Mn_sv: '3d 3p 3s 4s ' - Mo: '4d 5p 5s ' - Mo_pv: '4d 4p 5s ' - Mo_sv: '4d 4p 4s 5s ' - N: '2p 2s ' - N_h: '2p 2s ' - N_s: '2p 2s ' - Na: '3p 3s ' - Na_pv: '2p 3s ' - Na_sv: '2p 2s 3s ' - Nb_pv: '4d 4p 5s ' - Nb_sv: '4d 4p 4s 5s ' - Nd: '4f 5d 5p 5s 6s ' - Nd_3: '5d 5p 5s 6s ' - Ne: '2p 2s ' - Ni: '3d 4p 4s ' - Ni_pv: '3d 3p 4s ' - Np: '5f 6d 6p 6s 7s ' - Np_s: '5f 6d 6p 6s 7s ' - O: '2p 2s ' - O_h: '2p 2s ' - O_s: '2p 2s ' - Os: '5d 6p 6s ' - Os_pv: '5d 5p 6s ' - P: '3p 3s ' - P_h: '3p 3s ' - Pa: '5f 6d 6p 6s 7s ' - Pa_s: '5f 6d 6p 7s ' - Pb: '6p 6s ' - Pb_d: '5d 6p 6s ' - Pd: '4d 5p 5s ' - Pd_pv: '4d 4p 5s ' - Pm: '4f 5d 5p 5s 6s ' - Pm_3: '5d 5p 5s 6s ' - Po: '6p 6s ' - Po_d: '5d 6p 6s ' - Pr: '4f 5d 5p 5s 6s ' - Pr_3: '5d 5p 5s 6s ' - Pt: '5d 6p 6s ' - Pt_pv: '5d 5p 6p 6s ' - Pu: '5f 6d 6p 6s 7s ' - Pu_s: '5f 6d 6p 6s 7s ' - Ra_sv: '6p 6s 7s ' - Rb_pv: '4p 5s ' - Rb_sv: '4p 4s 5s ' - Re: '5d 6s ' - Re_pv: '5d 5p 6s ' - Rh: '4d 5p 5s ' - Rh_pv: '4d 4p 5s ' - Rn: '6p 6s ' - Ru: '4d 5p 5s ' - Ru_pv: '4d 4p 5s ' - Ru_sv: '4d 4p 4s 5s ' - S: '3p 3s ' - S_h: '3p 3s ' - Sb: '5p 5s ' - Sc: '3d 4p 4s ' - Sc_sv: '3d 3p 3s 4s ' - Se: '4p 4s ' - Si: '3p 3s ' - Sm: '4f 5d 5p 5s 6s ' - Sm_3: '5d 5p 5s 6s ' - Sn: '5p 5s ' - Sn_d: '4d 5p 5s ' - Sr_sv: '4p 4s 5s ' - Ta: '5d 6p 6s ' - Ta_pv: '5d 5p 6s ' - Tb: '4f 5d 5p 5s 6s ' - Tb_3: '5d 5p 6s ' - Tc: '4d 5p 5s ' - Tc_pv: '4d 4p 5s ' - Tc_sv: '4d 4p 4s 5s ' - Te: '5p 5s ' - Th: '5f 6d 6p 6s 7s ' - Th_s: '5f 6d 6p 7s ' - Ti: '3d 4p 4s ' - Ti_pv: '3d 3p 4s ' - Ti_sv: '3d 3p 3s 4s ' - Tl: '6p 6s ' - Tl_d: '5d 6p 6s ' - Tm: '4f 5d 5p 5s 6s ' - Tm_3: '5d 5p 6s ' - U: '5f 6d 6p 6s 7s ' - U_s: '5f 6d 6p 6s 7s ' - V: '3d 4p 4s ' - V_pv: '3d 3p 4s ' - V_sv: '3d 3p 3s 4s ' - W: '5d 6p 6s ' - W_sv: '5d 5p 5s 6s ' - Xe: '5p 5s ' - Y_sv: '4d 4p 4s 5s ' - Yb: '4f 5d 5p 5s 6s ' - Yb_2: '5d 5p 6s ' - Yb_3: '5d 5p 6s ' - Zn: '3d 4p 4s ' - Zr_sv: '4d 4p 4s 5s ' diff --git a/src/pymatgen/io/lobster/future/lobsterenv.py b/src/pymatgen/io/lobster/future/lobsterenv.py deleted file mode 100644 index 1f4d5ffcae..0000000000 --- a/src/pymatgen/io/lobster/future/lobsterenv.py +++ /dev/null @@ -1,1523 +0,0 @@ -"""This module provides classes to perform analyses of the local -environments (e.g., finding near neighbors) of single sites in molecules -and structures based on bonding analysis with LOBSTER. - -If you use this module, please cite: -J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, -"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", -ChemPlusChem 2022, e202200123, -DOI: 10.1002/cplu.202200123. -""" - -from __future__ import annotations - -import collections -import copy -import math -import tempfile -from typing import TYPE_CHECKING, NamedTuple - -import matplotlib as mpl -import numpy as np -from monty.dev import deprecated - -from pymatgen.analysis.bond_valence import BVAnalyzer -from pymatgen.analysis.chemenv.coordination_environments.coordination_geometry_finder import LocalGeometryFinder -from pymatgen.analysis.chemenv.coordination_environments.structure_environments import LightStructureEnvironments -from pymatgen.analysis.local_env import NearNeighbors -from pymatgen.electronic_structure.cohp import CompleteCohp -from pymatgen.electronic_structure.core import Spin -from pymatgen.electronic_structure.plotter import CohpPlotter -from pymatgen.io.lobster import Charge, Icohplist -from pymatgen.util.due import Doi, due - -if TYPE_CHECKING: - from typing import Any, Literal, Self - - import matplotlib as mpl - from numpy.typing import NDArray - - from pymatgen.core import IStructure, PeriodicNeighbor, PeriodicSite, Structure - from pymatgen.core.periodic_table import Element - from pymatgen.electronic_structure.cohp import IcohpCollection, IcohpValue - from pymatgen.util.typing import PathLike - -__author__ = "Janine George" -__copyright__ = "Copyright 2021, The Materials Project" -__version__ = "1.0" -__maintainer__ = "J. George" -__email__ = "janinegeorge.ulfen@gmail.com" -__status__ = "Production" -__date__ = "February 2, 2021" - -due.cite( - Doi("10.1002/cplu.202200123"), - description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", -) - - -class LobsterNeighbors(NearNeighbors): - """ - This class combines capabilities from LocalEnv and ChemEnv to determine - coordination environments based on bonding analysis. - """ - - def __init__( - self, - structure: Structure | IStructure, - filename_icohp: PathLike | None = "ICOHPLIST.lobster", - obj_icohp: Icohplist | None = None, - are_coops: bool = False, - are_cobis: bool = False, - valences: list[float] | None = None, - limits: tuple[float, float] | None = None, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, - only_bonds_to: list[str] | None = None, - perc_strength_icohp: float = 0.15, - noise_cutoff: float = 0.1, - valences_from_charges: bool = False, - filename_charge: PathLike | None = None, - obj_charge: Charge | None = None, - which_charge: Literal["Mulliken", "Loewdin"] = "Mulliken", - adapt_extremum_to_add_cond: bool = False, - add_additional_data_sg: bool = False, - filename_blist_sg1: PathLike | None = None, - filename_blist_sg2: PathLike | None = None, - id_blist_sg1: Literal["icoop", "icobi"] = "icoop", - id_blist_sg2: Literal["icoop", "icobi"] = "icobi", - ) -> None: - """ - Args: - filename_icohp (PathLike): Path to ICOHPLIST.lobster or - ICOOPLIST.lobster or ICOBILIST.lobster. - obj_icohp (Icohplist): Icohplist object. - structure (Structure): Typically constructed by Structure.from_file("POSCAR"). - are_coops (bool): Whether the file is a ICOOPLIST.lobster (True) or a - ICOHPLIST.lobster (False). Only tested for ICOHPLIST.lobster so far. - are_cobis (bool): Whether the file is a ICOBILIST.lobster (True) or - a ICOHPLIST.lobster (False). - valences (list[float]): Valence/charge for each element. - limits (tuple[float, float]): Range to decide which ICOHPs (ICOOP - or ICOBI) should be considered. - additional_condition (int): Additional condition that decides - which kind of bonds will be considered: - 0 - NO_ADDITIONAL_CONDITION - 1 - ONLY_ANION_CATION_BONDS - 2 - NO_ELEMENT_TO_SAME_ELEMENT_BONDS - 3 - ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - 4 - ONLY_ELEMENT_TO_OXYGEN_BONDS - 5 - DO_NOT_CONSIDER_ANION_CATION_BONDS - 6 - ONLY_CATION_CATION_BONDS - only_bonds_to (list[str]): Only consider bonds to certain elements (e.g. ["O"] for oxygen). - perc_strength_icohp (float): If no "limits" are given, this will decide - which ICOHPs will be considered (relative to the strongest ICOHP/ICOOP/ICOBI). - noise_cutoff (float): The lower limit of ICOHPs considered. - valences_from_charges (bool): If True and path to CHARGE.lobster is provided, - will use LOBSTER charges (Mulliken) instead of valences. - filename_charge (PathLike): Path to Charge.lobster. - obj_charge (Charge): Charge object. - which_charge ("Mulliken" | "Loewdin"): Source of charge. - adapt_extremum_to_add_cond (bool): Whether to adapt the limits to only - focus on the bonds determined by the additional condition. - add_additional_data_sg (bool): Add the information from filename_add_bondinglist_sg1. - filename_blist_sg1 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. - filename_blist_sg2 (PathLike): Path to additional ICOOP, ICOBI data for structure graphs. - id_blist_sg1 ("icoop" | "icobi"): Identity of data in filename_blist_sg1. - id_blist_sg2 ("icoop" | "icobi"): Identity of data in filename_blist_sg2. - """ - if filename_icohp is not None: - self.ICOHP = Icohplist(are_coops=are_coops, are_cobis=are_cobis, filename=filename_icohp) - elif obj_icohp is not None: - self.ICOHP = obj_icohp - else: - raise ValueError("Please provide either filename_icohp or obj_icohp") - - self.Icohpcollection = self.ICOHP.icohpcollection - self.structure = structure - self.limits = limits - self.only_bonds_to = only_bonds_to - self.adapt_extremum_to_add_cond = adapt_extremum_to_add_cond - self.are_coops = are_coops - self.are_cobis = are_cobis - self.add_additional_data_sg = add_additional_data_sg - self.filename_blist_sg1 = filename_blist_sg1 - self.filename_blist_sg2 = filename_blist_sg2 - self.noise_cutoff = noise_cutoff - - self.id_blist_sg1 = id_blist_sg1.lower() - self.id_blist_sg2 = id_blist_sg2.lower() - - allowed_arguments = {"icoop", "icobi"} - if self.id_blist_sg1 not in allowed_arguments or self.id_blist_sg2 not in allowed_arguments: - raise ValueError("Algorithm can only work with ICOOPs, ICOBIs") - - if add_additional_data_sg: - if self.id_blist_sg1 == "icoop": - are_coops_id1 = True - are_cobis_id1 = False - else: - are_coops_id1 = False - are_cobis_id1 = True - - self.bonding_list_1 = Icohplist( - filename=self.filename_blist_sg1, - are_coops=are_coops_id1, - are_cobis=are_cobis_id1, - ) - - if self.id_blist_sg2 == "icoop": - are_coops_id2 = True - are_cobis_id2 = False - else: - are_coops_id2 = False - are_cobis_id2 = True - - self.bonding_list_2 = Icohplist( - filename=self.filename_blist_sg2, - are_coops=are_coops_id2, - are_cobis=are_cobis_id2, - ) - - # Check the additional condition - if additional_condition not in range(7): - raise ValueError(f"Unexpected {additional_condition=}, must be one of {list(range(7))}") - self.additional_condition = additional_condition - - # Read in valences, will prefer manual setting of valences - self.valences: list[float] | None - if valences is None: - if valences_from_charges and filename_charge is not None: - chg = Charge(filename=filename_charge) - if which_charge == "Mulliken": - self.valences = chg.mulliken - elif which_charge == "Loewdin": - self.valences = chg.loewdin - - elif valences_from_charges and obj_charge is not None: - chg = obj_charge - if which_charge == "Mulliken": - self.valences = chg.mulliken - elif which_charge == "Loewdin": - self.valences = chg.loewdin - - else: - bv_analyzer = BVAnalyzer() - try: - self.valences = bv_analyzer.get_valences(structure=self.structure) # type:ignore[arg-type] - except ValueError as exc: - self.valences = None - if additional_condition in {1, 3, 5, 6}: - raise ValueError( - "Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work" - ) from exc - else: - self.valences = valences - - if np.allclose(self.valences or [], np.zeros_like(self.valences)) and additional_condition in {1, 3, 5, 6}: - raise ValueError("All valences are equal to 0, additional_conditions 1, 3, 5 and 6 will not work") - - if limits is None: - self.lowerlimit = self.upperlimit = None - else: - self.lowerlimit, self.upperlimit = limits - - # Evaluate coordination environments - self._evaluate_ce( - lowerlimit=self.lowerlimit, - upperlimit=self.upperlimit, - only_bonds_to=only_bonds_to, - additional_condition=self.additional_condition, - perc_strength_icohp=perc_strength_icohp, - adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, - ) - - @property - def structures_allowed(self) -> Literal[True]: - """Whether this LobsterNeighbors class can be used with Structure objects.""" - return True - - @property - def molecules_allowed(self) -> Literal[False]: - """Whether this LobsterNeighbors class can be used with Molecule objects.""" - return False - - @property - def anion_types(self) -> set[Element]: - """The set of anion types in crystal structure. - - Returns: - set[Element]: Anions in the crystal structure. - """ - if self.valences is None: - raise ValueError("No cations and anions defined") - - anion_species = [] - for site, val in zip(self.structure, self.valences, strict=True): - if val < 0.0: - anion_species.append(site.specie) - - return set(anion_species) - - @deprecated(anion_types) - def get_anion_types(self) -> set[Element]: - return self.anion_types - - def get_nn_info( - self, - structure: Structure | IStructure, - n: int, - use_weights: bool = False, - ) -> dict[str, Any]: - """Get coordination number (CN) of site by index. - - Args: - structure (Structure): Input structure. - n (int): Index of site for which to determine CN. - use_weights (bool): Whether to use weights for computing - the CN (True), or each coordinated site has equal weight (False). - The former is not implemented yet. - - Raises: - ValueError: If use_weights is True, or if arg "structure" and structure - used to initialize LobsterNeighbors have different lengths. - - Returns: - dict[str, Any]: coordination number and a list of nearest neighbors. - """ - if use_weights: - raise ValueError("LobsterEnv cannot use weights") - - if len(structure) != len(self.structure): - raise ValueError( - f"Length of structure ({len(structure)}) and LobsterNeighbors ({len(self.structure)}) differ" - ) - - return self.sg_list[n] # type: ignore[return-value] - - def get_light_structure_environment( - self, - only_cation_environments: bool = False, - only_indices: list[int] | None = None, - ) -> LobsterLightStructureEnvironments: - """Get a LobsterLightStructureEnvironments object if the structure - only contains coordination environments smaller 13. - - Args: - only_cation_environments (bool): Only return data for cations. - only_indices (list[int]): Only evaluate indexes in this list. - - Returns: - LobsterLightStructureEnvironments - """ - lgf = LocalGeometryFinder() - lgf.setup_structure(structure=self.structure) # type:ignore[arg-type] - list_ce_symbols = [] - list_csm = [] - list_permut = [] - for idx, _neigh_coords in enumerate(self.list_coords): - if (len(_neigh_coords)) > 13: - raise ValueError("Environment cannot be determined. Number of neighbors is larger than 13.") - # Avoid problems if _neigh_coords is empty - if _neigh_coords != []: - lgf.setup_local_geometry(isite=idx, coords=_neigh_coords, optimization=2) - cncgsm = lgf.get_coordination_symmetry_measures(optimization=2) - list_ce_symbols.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[0]) - list_csm.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["csm_wcs_ctwcc"]) - list_permut.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["indices"]) - else: - list_ce_symbols.append(None) - list_csm.append(None) - list_permut.append(None) - - new_list_ce_symbols = [] - new_list_csm = [] - new_list_permut = [] - new_list_neighsite = [] - new_list_neighisite = [] - - if only_indices is None: - if not only_cation_environments: - return LobsterLightStructureEnvironments.from_Lobster( - list_ce_symbol=list_ce_symbols, - list_csm=list_csm, - list_permutation=list_permut, - list_neighsite=self.list_neighsite, # type:ignore[arg-type] - list_neighisite=self.list_neighisite, - structure=self.structure, - valences=self.valences, - ) - - if self.valences is None: - raise ValueError(f"{self.valences=}") - - for idx, val in enumerate(self.valences): - if val >= 0.0: - new_list_ce_symbols.append(list_ce_symbols[idx]) - new_list_csm.append(list_csm[idx]) - new_list_permut.append(list_permut[idx]) - new_list_neighisite.append(self.list_neighisite[idx]) - new_list_neighsite.append(self.list_neighsite[idx]) - else: - new_list_ce_symbols.append(None) - new_list_csm.append(None) - new_list_permut.append([]) - new_list_neighisite.append([]) - new_list_neighsite.append([]) - - else: - for site_idx, _site in enumerate(self.structure): - if site_idx in only_indices: - new_list_ce_symbols.append(list_ce_symbols[site_idx]) - new_list_csm.append(list_csm[site_idx]) - new_list_permut.append(list_permut[site_idx]) - new_list_neighisite.append(self.list_neighisite[site_idx]) - new_list_neighsite.append(self.list_neighsite[site_idx]) - else: - new_list_ce_symbols.append(None) - new_list_csm.append(None) - new_list_permut.append([]) - new_list_neighisite.append([]) - new_list_neighsite.append([]) - - return LobsterLightStructureEnvironments.from_Lobster( - list_ce_symbol=new_list_ce_symbols, - list_csm=new_list_csm, - list_permutation=new_list_permut, - list_neighsite=new_list_neighsite, # type:ignore[arg-type] - list_neighisite=new_list_neighisite, - structure=self.structure, - valences=self.valences, - ) - - def get_info_icohps_to_neighbors( - self, - isites: list[int] | None = None, - onlycation_isites: bool = True, - ) -> ICOHPNeighborsInfo: - """Get information on the ICOHPs of neighbors for certain sites - as identified by their site id. - - This is useful for plotting the COHPs (ICOOPLIST.lobster/ - ICOHPLIST.lobster/ICOBILIST.lobster) of a site in the structure. - - - Args: - isites (list[int]): Site IDs. If is None, all isites will be used - to add the ICOHPs of the neighbors. - onlycation_isites (bool): If True and if isite is None, will - only analyse the cations sites. - - Returns: - ICOHPNeighborsInfo - """ - if self.valences is None and onlycation_isites: - raise ValueError("No valences are provided") - - if isites is None: - if onlycation_isites: - if self.valences is None: - raise ValueError(f"{self.valences}=") - - isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] - else: - isites = list(range(len(self.structure))) - - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - summed_icohps: float = 0.0 - list_icohps: list[float] = [] - number_bonds: int = 0 - labels: list[str] = [] - atoms: list[list[str]] = [] - final_isites: list[int] = [] - for idx, _site in enumerate(self.structure): - if idx in isites: - for key, icohpsum in zip(self.list_keys[idx], self.list_icohps[idx], strict=True): - summed_icohps += icohpsum - list_icohps.append(icohpsum) # type:ignore[arg-type] - labels.append(key) - atoms.append( - [ - self.Icohpcollection._list_atom1[int(key) - 1], - self.Icohpcollection._list_atom2[int(key) - 1], - ] - ) - number_bonds += 1 - final_isites.append(idx) - return ICOHPNeighborsInfo(summed_icohps, list_icohps, number_bonds, labels, atoms, final_isites) - - def plot_cohps_of_neighbors( - self, - path_to_cohpcar: PathLike | None = "COHPCAR.lobster", - obj_cohpcar: CompleteCohp | None = None, - isites: list[int] | None = None, - onlycation_isites: bool = True, - only_bonds_to: list[str] | None = None, - per_bond: bool = False, - summed_spin_channels: bool = False, - xlim: tuple[float, float] | None = None, - ylim: tuple[float, float] = (-10, 6), - integrated: bool = False, - ) -> mpl.axes.Axes: - """Plot summed COHPs or COBIs or COOPs. - - Please be careful in the spin polarized case (plots might overlap). - - Args: - path_to_cohpcar (PathLike): Path to COHPCAR or COOPCAR or COBICAR. - obj_cohpcar (CompleteCohp): CompleteCohp object - isites (list[int]): Site IDs. If empty, all sites will be used to add the ICOHPs of the neighbors. - onlycation_isites (bool): Only use cations, if isite is empty. - only_bonds_to (list[str]): Only anions in this list will be considered. - per_bond (bool): Whether to plot a normalization of the plotted COHP - per number of bond (True), or the sum (False). - xlim (tuple[float, float]): Limits of x values. - ylim (tuple[float, float]): Limits of y values. - integrated (bool): Whether to show integrated COHP instead of COHP. - - Returns: - plt of the COHPs or COBIs or COOPs. - """ - cp = CohpPlotter(are_cobis=self.are_cobis, are_coops=self.are_coops) - - plotlabel, summed_cohp = self.get_info_cohps_to_neighbors( - path_to_cohpcar, - obj_cohpcar, - isites, - only_bonds_to, - onlycation_isites, - per_bond, - summed_spin_channels=summed_spin_channels, - ) - - cp.add_cohp(plotlabel, summed_cohp) - ax = cp.get_plot(integrated=integrated) - if xlim is not None: - ax.set_xlim(xlim) - - if ylim is not None: - ax.set_ylim(ylim) - - return ax - - def get_info_cohps_to_neighbors( - self, - path_to_cohpcar: PathLike | None = "COHPCAR.lobster", - obj_cohpcar: CompleteCohp | None = None, - isites: list[int] | None = None, - only_bonds_to: list[str] | None = None, - onlycation_isites: bool = True, - per_bond: bool = True, - summed_spin_channels: bool = False, - ) -> tuple[str | None, CompleteCohp | None]: - """Get the COHPs (COOPs or COBIs) as a summed Cohp object - and a label from all sites mentioned in isites with neighbors. - - Args: - path_to_cohpcar (PathLike): Path to COHPCAR/COOPCAR/COBICAR. - obj_cohpcar (CompleteCohp): CompleteCohp object. - isites (list[int]): The indexes of the sites. - only_bonds_to (list[str]): Only show COHPs to selected element, e.g. ["O"]. - onlycation_isites (bool): If isites is None, only cation sites will be returned. - per_bond (bool): Whether to normalize per bond. - summed_spin_channels (bool): Whether to sum both spin channels. - - Returns: - str: Label for COHP. - CompleteCohp: Describe all COHPs/COOPs/COBIs of the sites - as given by isites and the other arguments. - """ - # TODO: add options for orbital-resolved COHPs - _summed_icohps, _list_icohps, _number_bonds, labels, atoms, final_isites = self.get_info_icohps_to_neighbors( - isites=isites, onlycation_isites=onlycation_isites - ) - - with tempfile.TemporaryDirectory() as tmp_dir: - path = f"{tmp_dir}/POSCAR.vasp" - - self.structure.to(filename=path, fmt="poscar") - - if not hasattr(self, "completecohp"): - if path_to_cohpcar is not None and obj_cohpcar is None: - self.completecohp = CompleteCohp.from_file( - fmt="LOBSTER", - filename=path_to_cohpcar, - structure_file=path, - are_coops=self.are_coops, - are_cobis=self.are_cobis, - ) - elif obj_cohpcar is not None: - self.completecohp = obj_cohpcar - else: - raise ValueError("Please provide either path_to_cohpcar or obj_cohpcar") - - # Check that the number of bonds in ICOHPLIST and COHPCAR are identical - # TODO: Further checks could be implemented - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - if len(self.Icohpcollection._list_atom1) != len(self.completecohp.bonds): - raise ValueError("COHPCAR and ICOHPLIST do not fit together") - - is_spin_completecohp = Spin.down in self.completecohp.get_cohp_by_label("1").cohp - if self.Icohpcollection.is_spin_polarized != is_spin_completecohp: - raise ValueError("COHPCAR and ICOHPLIST do not fit together") - - if only_bonds_to is None: - # Sort by anion type - divisor = len(labels) if per_bond else 1 - - plot_label = self._get_plot_label(atoms, per_bond) - summed_cohp = self.completecohp.get_summed_cohp_by_label_list( - label_list=labels, - divisor=divisor, - summed_spin_channels=summed_spin_channels, - ) - - else: - # Labels of the COHPs that will be summed - # Iterate through labels and atoms and check which bonds can be included - new_labels = [] - new_atoms = [] - if final_isites is None: - raise ValueError(f"{final_isites=}") - - for key, atompair, isite in zip(labels, atoms, final_isites, strict=True): - present = False - for atomtype in only_bonds_to: - # This is necessary to identify also bonds between the same elements correctly - if str(self.structure[isite].species.elements[0]) != atomtype: - if atomtype in { - self._split_string(atompair[0])[0], - self._split_string(atompair[1])[0], - }: - present = True - elif ( - atomtype == self._split_string(atompair[0])[0] - and atomtype == self._split_string(atompair[1])[0] - ): - present = True - - if present: - new_labels.append(key) - new_atoms.append(atompair) - if new_labels: - divisor = len(new_labels) if per_bond else 1 - - plot_label = self._get_plot_label(new_atoms, per_bond) - summed_cohp = self.completecohp.get_summed_cohp_by_label_list( - label_list=new_labels, - divisor=divisor, - summed_spin_channels=summed_spin_channels, - ) - else: - plot_label = None - - summed_cohp = None - - return plot_label, summed_cohp # type:ignore[return-value] - - def _get_plot_label(self, atoms: list[list[str]], per_bond: bool) -> str: - """Count the types of bonds and append a label.""" - all_labels = [] - for atoms_names in atoms: - new = [ - self._split_string(atoms_names[0])[0], - self._split_string(atoms_names[1])[0], - ] - new.sort() - string_here = f"{new[0]}-{new[1]}" - all_labels.append(string_here) - - counter = collections.Counter(all_labels) - plotlabels = [f"{item} x {key}" for key, item in counter.items()] - label = ", ".join(plotlabels) - if per_bond: - label += " (per bond)" - return label - - def get_info_icohps_between_neighbors( - self, - isites: list[int] | None = None, - onlycation_isites: bool = True, - ) -> ICOHPNeighborsInfo: - """Get interactions between neighbors of certain sites. - - Args: - isites (list[int]): Site IDs. If is None, all sites will be used. - onlycation_isites (bool): Only use cations, if isite is None. - - Returns: - ICOHPNeighborsInfo - """ - lowerlimit = self.lowerlimit - upperlimit = self.upperlimit - - if self.valences is None and onlycation_isites: - raise ValueError("No valences are provided") - - if isites is None: - if onlycation_isites: - if self.valences is None: - raise ValueError(f"{self.valences=}") - - isites = [idx for idx in range(len(self.structure)) if self.valences[idx] >= 0.0] - else: - isites = list(range(len(self.structure))) - - summed_icohps: float = 0.0 - list_icohps: list[float] = [] - number_bonds: int = 0 - labels: list[str] = [] - atoms: list[list[str]] = [] - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - for isite in isites: - for site1_idx, n_site in enumerate(self.list_neighsite[isite]): - for site2_idx, n_site2 in enumerate(self.list_neighsite[isite]): - if site1_idx < site2_idx: - unitcell1 = self._determine_unit_cell(n_site) - unitcell2 = self._determine_unit_cell(n_site2) - - index_n_site = self._get_original_site(self.structure, n_site) # type:ignore[arg-type] - index_n_site2 = self._get_original_site(self.structure, n_site2) # type:ignore[arg-type] - - if index_n_site < index_n_site2: - translation = list(np.array(unitcell1) - np.array(unitcell2)) - elif index_n_site2 < index_n_site: - translation = list(np.array(unitcell2) - np.array(unitcell1)) - else: - translation = list(np.array(unitcell1) - np.array(unitcell2)) - - icohps = self._get_icohps( - icohpcollection=self.Icohpcollection, - site_idx=index_n_site, - lowerlimit=lowerlimit, - upperlimit=upperlimit, - only_bonds_to=self.only_bonds_to, - ) - - done = False - for icohp in icohps.values(): - atomnr1 = self._get_atomnumber(icohp._atom1) - atomnr2 = self._get_atomnumber(icohp._atom2) - label = icohp._label - - if (index_n_site == atomnr1 and index_n_site2 == atomnr2) or ( - index_n_site == atomnr2 and index_n_site2 == atomnr1 - ): - if atomnr1 != atomnr2: - if np.all(np.asarray(translation) == np.asarray(icohp._translation)): - summed_icohps += icohp.summed_icohp - list_icohps.append(icohp.summed_icohp) - number_bonds += 1 - labels.append(label) - atoms.append( - [ - self.Icohpcollection._list_atom1[int(label) - 1], - self.Icohpcollection._list_atom2[int(label) - 1], - ] - ) - - elif not done: - icohp_trans = -np.asarray( - [ - icohp._translation[0], - icohp._translation[1], - icohp._translation[2], - ] - ) - - if (np.all(np.asarray(translation) == np.asarray(icohp._translation))) or ( - np.all(np.asarray(translation) == icohp_trans) - ): - summed_icohps += icohp.summed_icohp - list_icohps.append(icohp.summed_icohp) - number_bonds += 1 - labels.append(label) - atoms.append( - [ - self.Icohpcollection._list_atom1[int(label) - 1], - self.Icohpcollection._list_atom2[int(label) - 1], - ] - ) - done = True - - return ICOHPNeighborsInfo(summed_icohps, list_icohps, number_bonds, labels, atoms, None) - - def _evaluate_ce( - self, - lowerlimit: float | None, - upperlimit: float | None, - only_bonds_to: list[str] | None = None, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, - perc_strength_icohp: float = 0.15, - adapt_extremum_to_add_cond: bool = False, - ) -> None: - """ - Args: - lowerlimit (float): Lower limit which determines the ICOHPs - that are considered for the determination of the neighbors. - upperlimit (float): Upper limit which determines the ICOHPs - that are considered for the determination of the neighbors. - only_bonds_to (list[str]): Restrict the types of bonds that will be considered. - additional_condition (int): Additional condition for the evaluation. - perc_strength_icohp (float): Determine how strong the ICOHPs - (percentage * strongest_ICOHP) will be that are still considered. - adapt_extremum_to_add_cond (bool): Whether to recalculate the limit - based on the bonding type and not on the overall extremum. - """ - # Get extremum - if lowerlimit is None and upperlimit is None: - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - limits = self._get_limit_from_extremum( - self.Icohpcollection, - percentage=perc_strength_icohp, - adapt_extremum_to_add_cond=adapt_extremum_to_add_cond, - additional_condition=additional_condition, - ) - - if limits is None: - raise ValueError(f"{limits=}") - lowerlimit, upperlimit = limits - - elif upperlimit is None or lowerlimit is None: - raise ValueError("Please give two limits or leave them both at None") - - # Find environments based on ICOHP values - ( - list_icohps, - list_keys, - list_lengths, - list_neighisite, - list_neighsite, - list_coords, - ) = self._find_environments(additional_condition, lowerlimit, upperlimit, only_bonds_to) - - self.list_icohps = list_icohps - self.list_lengths = list_lengths - self.list_keys = list_keys - self.list_neighsite = list_neighsite - self.list_neighisite = list_neighisite - self.list_coords = list_coords - - # Make a structure graph - # Make sure everything is relative to the given Structure and - # not just the atoms in the unit cell - if self.add_additional_data_sg: - if self.bonding_list_1.icohpcollection is None: - raise ValueError(f"{self.bonding_list_1.icohpcollection=}") - if self.bonding_list_2.icohpcollection is None: - raise ValueError(f"{self.bonding_list_2.icohpcollection=}") - - self.sg_list = [ - [ - { - "site": neighbor, - "image": tuple( - round(idx) - for idx in ( - neighbor.frac_coords - - self.structure[ - next( - site_idx - for site_idx, site in enumerate(self.structure) - if neighbor.is_periodic_image(site) - ) - ].frac_coords - ) - ), - "weight": 1, - # Here, the ICOBIs and ICOOPs are added based on the bond - # strength cutoff of the ICOHP - # More changes are necessary here if we use ICOBIs for cutoffs - "edge_properties": { - "ICOHP": self.list_icohps[neighbors_idx][nbr_idx], - "bond_length": self.list_lengths[neighbors_idx][nbr_idx], - "bond_label": self.list_keys[neighbors_idx][nbr_idx], - self.id_blist_sg1.upper(): self.bonding_list_1.icohpcollection.get_icohp_by_label( - self.list_keys[neighbors_idx][nbr_idx] - ), - self.id_blist_sg2.upper(): self.bonding_list_2.icohpcollection.get_icohp_by_label( - self.list_keys[neighbors_idx][nbr_idx] - ), - }, - "site_index": next( - site_idx for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) - ), - } - for nbr_idx, neighbor in enumerate(neighbors) - ] - for neighbors_idx, neighbors in enumerate(self.list_neighsite) - ] - else: - self.sg_list = [ - [ - { - "site": neighbor, - "image": tuple( - round(idx) - for idx in ( - neighbor.frac_coords - - self.structure[ - next( - site_idx - for site_idx, site in enumerate(self.structure) - if neighbor.is_periodic_image(site) - ) - ].frac_coords - ) - ), - "weight": 1, - "edge_properties": { - "ICOHP": self.list_icohps[neighbors_idx][nbr_idx], - "bond_length": self.list_lengths[neighbors_idx][nbr_idx], - "bond_label": self.list_keys[neighbors_idx][nbr_idx], - }, - "site_index": next( - site_idx for site_idx, site in enumerate(self.structure) if neighbor.is_periodic_image(site) - ), - } - for nbr_idx, neighbor in enumerate(neighbors) - ] - for neighbors_idx, neighbors in enumerate(self.list_neighsite) - ] - - def _find_environments( - self, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], - lowerlimit: float, - upperlimit: float, - only_bonds_to: list[str] | None, - ) -> tuple[ - list[list[IcohpValue]], - list[list[str]], - list[list[float]], - list[list[int]], - list[list[PeriodicNeighbor]], - list[list[NDArray]], - ]: - """Find all relevant neighbors based on certain restrictions. - - Args: - additional_condition (int): Additional condition. - lowerlimit (float): Lower limit that ICOHPs are considered. - upperlimit (float): Upper limit that ICOHPs are considered. - only_bonds_to (list[str]): Only bonds to these elements will be considered. - - Returns: - Tuple of ICOHPs, keys, lengths, neighisite, neighsite, coords. - """ - list_icohps: list[list[float]] = [] - list_keys: list[list[str]] = [] - list_lengths: list[list[float]] = [] - list_neighisite: list[list[int]] = [] - list_neighsite: list[list[PeriodicNeighbor]] = [] - list_coords: list[list[NDArray]] = [] - - # Run over structure - if self.Icohpcollection is None: - raise ValueError(f"{self.Icohpcollection=}") - - for idx, site in enumerate(self.structure): - icohps = self._get_icohps( - icohpcollection=self.Icohpcollection, - site_idx=idx, - lowerlimit=lowerlimit, - upperlimit=upperlimit, - only_bonds_to=only_bonds_to, - ) - - additional_conds = self._find_relevant_atoms_additional_condition(idx, icohps, additional_condition) - ( - keys_from_ICOHPs, - lengths_from_ICOHPs, - neighbors_from_ICOHPs, - selected_ICOHPs, - ) = additional_conds - - if len(neighbors_from_ICOHPs) > 0: - centralsite = site - - neighbors_by_distance_start = self.structure.get_sites_in_sphere( - pt=centralsite.coords, - r=np.max(lengths_from_ICOHPs) + 0.5, - include_image=True, - include_index=True, - ) - - neighbors_by_distance = [] - list_distances = [] - index_here_list = [] - coords = [] - for neigh_new in sorted(neighbors_by_distance_start, key=lambda x: x[1]): - site_here = neigh_new[0].to_unit_cell() - index_here = neigh_new[2] - index_here_list.append(index_here) - cell_here = neigh_new[3] - new_coords = [ - site_here.frac_coords[0] + float(cell_here[0]), - site_here.frac_coords[1] + float(cell_here[1]), - site_here.frac_coords[2] + float(cell_here[2]), - ] - coords.append(site_here.lattice.get_cartesian_coords(new_coords)) - - # new_site = PeriodicSite( - # species=site_here.species_string, - # coords=site_here.lattice.get_cartesian_coords(new_coords), - # lattice=site_here.lattice, - # to_unit_cell=False, - # coords_are_cartesian=True, - # ) - neighbors_by_distance.append(neigh_new[0]) - list_distances.append(neigh_new[1]) - _list_neighsite = [] - _list_neighisite = [] - copied_neighbors_from_ICOHPs = copy.copy(neighbors_from_ICOHPs) - copied_distances_from_ICOHPs = copy.copy(lengths_from_ICOHPs) - _neigh_coords = [] - _neigh_frac_coords = [] - - for neigh_idx, neigh in enumerate(neighbors_by_distance): - index_here2 = index_here_list[neigh_idx] - - for dist_idx, dist in enumerate(copied_distances_from_ICOHPs): - if ( - np.isclose(dist, list_distances[neigh_idx], rtol=1e-4) - and copied_neighbors_from_ICOHPs[dist_idx] == index_here2 - ): - _list_neighsite.append(neigh) - _list_neighisite.append(index_here2) - _neigh_coords.append(coords[neigh_idx]) - _neigh_frac_coords.append(neigh.frac_coords) - del copied_distances_from_ICOHPs[dist_idx] - del copied_neighbors_from_ICOHPs[dist_idx] - break - - list_neighisite.append(_list_neighisite) - list_neighsite.append(_list_neighsite) - list_lengths.append(lengths_from_ICOHPs) - list_keys.append(keys_from_ICOHPs) - list_coords.append(_neigh_coords) - list_icohps.append(selected_ICOHPs) - - else: - list_neighsite.append([]) - list_neighisite.append([]) - list_icohps.append([]) - list_lengths.append([]) - list_keys.append([]) - list_coords.append([]) - return ( # type:ignore[return-value] - list_icohps, - list_keys, - list_lengths, - list_neighisite, - list_neighsite, - list_coords, - ) - - def _find_relevant_atoms_additional_condition( - self, - site_idx: int, - icohps: dict[str, IcohpValue], - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6], - ) -> tuple[list[str], list[float], list[int], list[float]]: - """Find all relevant atoms that fulfill the additional condition. - - Args: - site_idx (int): Site index in structure (start from 0). - icohps (dict[str, IcohpValue]): ICOHP values. - additional_condition (int): Additional condition. - - Returns: - tuple: keys, lengths, neighbors from selected ICOHPs and selected ICOHPs. - """ - keys_from_ICOHPs: list[str] = [] - lengths_from_ICOHPs: list[float] = [] - neighbors_from_ICOHPs: list[int] = [] - icohps_from_ICOHPs: list[float] = [] - - for key, icohp in icohps.items(): - atomnr1 = self._get_atomnumber(icohp._atom1) - atomnr2 = self._get_atomnumber(icohp._atom2) - - # Check additional conditions - val1 = val2 = None - if self.valences is None: - raise ValueError(f"{self.valences=}") - if additional_condition in {1, 3, 5, 6}: - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - # NO_ADDITIONAL_CONDITION - if additional_condition == 0: - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # ONLY_ANION_CATION_BONDS - elif additional_condition == 1: - if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): # type: ignore[operator] - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # NO_ELEMENT_TO_SAME_ELEMENT_BONDS - elif additional_condition == 2: - if icohp._atom1.rstrip("0123456789") != icohp._atom2.rstrip("0123456789"): - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - elif additional_condition == 3: - if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and icohp._atom1.rstrip( # type: ignore[operator] - "0123456789" - ) != icohp._atom2.rstrip("0123456789"): - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # ONLY_ELEMENT_TO_OXYGEN_BONDS - elif additional_condition == 4: - if icohp._atom1.rstrip("0123456789") == "O" or icohp._atom2.rstrip("0123456789") == "O": - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # DO_NOT_CONSIDER_ANION_CATION_BONDS - elif additional_condition == 5: - if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): # type: ignore[operator] - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - # ONLY_CATION_CATION_BONDS - elif additional_condition == 6 and val1 > 0.0 and val2 > 0.0: # type: ignore[operator] - if atomnr1 == site_idx: - neighbors_from_ICOHPs.append(atomnr2) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - elif atomnr2 == site_idx: - neighbors_from_ICOHPs.append(atomnr1) - lengths_from_ICOHPs.append(icohp._length) - icohps_from_ICOHPs.append(icohp.summed_icohp) - keys_from_ICOHPs.append(key) - - return ( - keys_from_ICOHPs, - lengths_from_ICOHPs, - neighbors_from_ICOHPs, - icohps_from_ICOHPs, - ) - - @staticmethod - def _get_icohps( - icohpcollection: IcohpCollection, - site_idx: int, - lowerlimit: float | None, - upperlimit: float | None, - only_bonds_to: list[str] | None, - ) -> dict[str, IcohpValue]: - """Get ICOHP dict for certain site. - - Args: - icohpcollection (IcohpCollection): IcohpCollection object. - site_idx (int): Site index. - lowerlimit (float): Lower limit that ICOHPs are considered. - upperlimit (float): Upper limit that ICOHPs are considered. - only_bonds_to (list[str]): Only bonds to these elements will be considered, e.g. ["O"]. - - Returns: - dict of IcohpValues. The keys correspond to the initial list_labels. - """ - return icohpcollection.get_icohp_dict_of_site( - site=site_idx, - maxbondlength=6.0, - minsummedicohp=lowerlimit, - maxsummedicohp=upperlimit, - only_bonds_to=only_bonds_to, - ) - - @staticmethod - def _get_atomnumber(atomstring: str) -> int: - """Get the index of the atom within the POSCAR (e.g., Return 0 for "Na1"). - - Args: - atomstring (str): Atom as str, such as "Na1". - - Returns: - int: Index of the atom in the POSCAR. - """ - return int(LobsterNeighbors._split_string(atomstring)[1]) - 1 - - @staticmethod - def _split_string(s) -> tuple[str, str]: - """Split strings such as "Na1" into ["Na", "1"] and return "1". - - Args: - s (str): String to split. - """ - head = s.rstrip("0123456789") - tail = s[len(head) :] - return head, tail - - @staticmethod - def _determine_unit_cell(site: PeriodicSite) -> list[int]: - """Determine the unit cell based on the site. - - Args: - site (PeriodicSite): The site. - """ - unitcell = [] - for coord in site.frac_coords: - value = math.floor(round(coord, 4)) - unitcell.append(value) - - return unitcell - - def _adapt_extremum_to_add_cond( - self, - list_icohps: list[float], - percentage: float, - ) -> float: - """Get the extremum from the given ICOHPs or ICOOPs or ICOBIs. - - Args: - list_icohps (list): ICOHPs or ICOOPs or ICOBIs. - percentage (float): The percentage to scale extremum. - - Returns: - float: Min value of ICOHPs, or max value of ICOOPs/ICOBIs. - """ - - which_extr = min if not self.are_coops and not self.are_cobis else max - return which_extr(list_icohps) * percentage - - def _get_limit_from_extremum( - self, - icohpcollection: IcohpCollection, - percentage: float = 0.15, - adapt_extremum_to_add_cond: bool = False, - additional_condition: Literal[0, 1, 2, 3, 4, 5, 6] = 0, - ) -> tuple[float, float] | None: - """Get range for the ICOHP values from an IcohpCollection. - - Currently only work for ICOHPs. - - Args: - icohpcollection (IcohpCollection): IcohpCollection object. - percentage (float): Determine which ICOHPs/ICOOP/ICOBI will be considered. - adapt_extremum_to_add_cond (bool): Whether the extrumum be adapted to - the additional condition. - additional_condition (int): Additional condition to determine which bonds to include. - - Returns: - tuple[float, float]: [-inf, min(strongest_icohp*0.15, -noise_cutoff)] - or [max(strongest_icohp*0.15, noise_cutoff), inf]. - """ - extremum_based = None - - if self.valences is None: - raise ValueError(f"{self.valences=}") - - if not adapt_extremum_to_add_cond or additional_condition == 0: - extremum_based = icohpcollection.extremum_icohpvalue(summed_spin_channels=True) * percentage - - elif additional_condition == 1: - # ONLY_ANION_CATION_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - if (val1 < 0.0 < val2) or (val2 < 0.0 < val1): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 2: - # NO_ELEMENT_TO_SAME_ELEMENT_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - if value._atom1.rstrip("0123456789") != value._atom2.rstrip("0123456789"): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 3: - # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - if ((val1 < 0.0 < val2) or (val2 < 0.0 < val1)) and value._atom1.rstrip( - "0123456789" - ) != value._atom2.rstrip("0123456789"): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 4: - # ONLY_ELEMENT_TO_OXYGEN_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - if value._atom1.rstrip("0123456789") == "O" or value._atom2.rstrip("0123456789") == "O": - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 5: - # DO_NOT_CONSIDER_ANION_CATION_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0): - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - elif additional_condition == 6: - # ONLY_CATION_CATION_BONDS - list_icohps = [] - for value in icohpcollection._icohplist.values(): - atomnr1 = type(self)._get_atomnumber(value._atom1) - atomnr2 = type(self)._get_atomnumber(value._atom2) - val1 = self.valences[atomnr1] - val2 = self.valences[atomnr2] - - if val1 > 0.0 and val2 > 0.0: - list_icohps.append(value.summed_icohp) - - extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage) - - if not self.are_coops and not self.are_cobis: - max_here = min(extremum_based, -self.noise_cutoff) if self.noise_cutoff is not None else extremum_based - return -float("inf"), max_here - - if self.are_coops or self.are_cobis: - min_here = max(extremum_based, self.noise_cutoff) if self.noise_cutoff is not None else extremum_based - return min_here, float("inf") - - return None - - -class LobsterLightStructureEnvironments(LightStructureEnvironments): - """Store LightStructureEnvironments based on LOBSTER outputs.""" - - @classmethod - def from_Lobster( - cls, - list_ce_symbol: list[str], - list_csm: list[float], - list_permutation: list, - list_neighsite: list[PeriodicSite], - list_neighisite: list[list[int]], - structure: Structure | IStructure, - valences: list[float] | None = None, - ) -> Self: - """Set up a LightStructureEnvironments from LOBSTER. - - Args: - list_ce_symbol (list[str]): Coordination environments symbols. - list_csm (list[float]): Continuous symmetry measures. - list_permutation (list): Permutations. - list_neighsite (list[PeriodicSite]): Neighboring sites. - list_neighisite (list[list[int]]): Neighboring sites indexes. - structure (Structure): Structure object. - valences (list[float]): Valences. - - Returns: - LobsterLightStructureEnvironments - """ - strategy = None - valences_origin = "user-defined" - coordination_environments = [] - all_nbs_sites = [] - all_nbs_sites_indices = [] - neighbors_sets = [] - counter = 0 - - for site_idx in range(len(structure)): - # Coordination environment - if list_ce_symbol is not None: - ce_dict = { - "ce_symbol": list_ce_symbol[site_idx], - "ce_fraction": 1.0, - "csm": list_csm[site_idx], - "permutation": list_permutation[site_idx], - } - else: - ce_dict = None - - if list_neighisite[site_idx] is not None: - all_nbs_sites_indices_here = [] - for neigh_site_idx, neigh_site in enumerate(list_neighsite[site_idx]): - diff = neigh_site.frac_coords - structure[list_neighisite[site_idx][neigh_site_idx]].frac_coords - round_diff = np.round(diff) - if not np.allclose(diff, round_diff): - raise ValueError( - "Weird, differences between one site in a periodic image cell is not integer ..." - ) - nb_image_cell = np.array(round_diff, int) - - all_nbs_sites_indices_here.append(counter) - - neighbor = { - "site": neigh_site, - "index": list_neighisite[site_idx][neigh_site_idx], - "image_cell": nb_image_cell, - } - all_nbs_sites.append(neighbor) - counter += 1 - - all_nbs_sites_indices.append(all_nbs_sites_indices_here) - - else: - all_nbs_sites.append({"site": None, "index": None, "image_cell": None}) - all_nbs_sites_indices.append([]) - - if list_neighisite[site_idx] is not None: - nb_set = cls.NeighborsSet( - structure=structure, # type:ignore[arg-type] - isite=site_idx, - all_nbs_sites=all_nbs_sites, - all_nbs_sites_indices=all_nbs_sites_indices[site_idx], - ) - - else: - nb_set = cls.NeighborsSet( - structure=structure, # type:ignore[arg-type] - isite=site_idx, - all_nbs_sites=[], - all_nbs_sites_indices=[], - ) - - coordination_environments.append([ce_dict]) - neighbors_sets.append([nb_set]) - - return cls( - strategy=strategy, - coordination_environments=coordination_environments, - all_nbs_sites=all_nbs_sites, - neighbors_sets=neighbors_sets, - structure=structure, - valences=valences, - valences_origin=valences_origin, - ) - - @property - def uniquely_determines_coordination_environments(self) -> Literal[True]: - """Whether the coordination environments are uniquely determined.""" - return True - - def as_dict(self) -> dict[str, Any]: - """Bson-serializable dict representation of the object. - - Returns: - Bson-serializable dict representation. - """ - return { - "@module": type(self).__module__, - "@class": type(self).__name__, - "strategy": self.strategy, - "structure": self.structure.as_dict(), - "coordination_environments": self.coordination_environments, - "all_nbs_sites": [ - { - "site": nb_site["site"].as_dict(), - "index": nb_site["index"], - "image_cell": [int(ii) for ii in nb_site["image_cell"]], - } - for nb_site in self._all_nbs_sites - ], - "neighbors_sets": [ - [nb_set.as_dict() for nb_set in site_nb_sets] or None for site_nb_sets in self.neighbors_sets - ], - "valences": self.valences, - } - - -class ICOHPNeighborsInfo(NamedTuple): - """Tuple to record information on relevant bonds. - - Args: - total_icohp (float): Sum of ICOHP values of neighbors to the selected - sites (given by the index in structure). - list_icohps (list): Summed ICOHP values for all identified interactions with neighbors. - n_bonds (int): Number of identified bonds to the selected sites. - labels (list[str]): Labels (from ICOHPLIST) for all identified bonds. - atoms (list[list[str]]): Lists describing the species present (from ICOHPLIST) - in the identified interactions , e.g. ["Ag3", "O5"]. - central_isites (list[int]): The central site indexes for each identified interaction. - """ - - total_icohp: float - list_icohps: list[float] - n_bonds: int - labels: list[str] - atoms: list[list[str]] - central_isites: list[int] | None diff --git a/src/pymatgen/io/lobster/future/outputs/__init__.py b/src/pymatgen/io/lobster/future/outputs/__init__.py deleted file mode 100644 index f804c3f064..0000000000 --- a/src/pymatgen/io/lobster/future/outputs/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Module for reading Lobster output files. For more information on LOBSTER see www.cohp.de. - -If you use this module, please cite: - -J. George, G. Petretto, A. Naik, M. Esters, A. J. Jackson, R. Nelson, R. Dronskowski, G.-M. Rignanese, G. Hautier, -"Automated Bonding Analysis with Crystal Orbital Hamilton Populations", ChemPlusChem 2022, e202200123, -DOI: 10.1002/cplu.202200123. -""" - -from __future__ import annotations - -from pymatgen.io.lobster.future.outputs.bands import BandOverlaps, Fatband, Fatbands -from pymatgen.io.lobster.future.outputs.coxxcar import COBICAR, COBICAR_LCFO, COHPCAR, COHPCAR_LCFO, COOPCAR, COXXCAR -from pymatgen.io.lobster.future.outputs.doscar import DOSCAR, DOSCAR_LCFO -from pymatgen.io.lobster.future.outputs.icoxxlist import ( - ICOBILIST, - ICOBILIST_LCFO, - ICOHPLIST, - ICOHPLIST_LCFO, - ICOOPLIST, - ICOXXLIST, - NcICOBILIST, -) -from pymatgen.io.lobster.future.outputs.lobsterout import LobsterOut -from pymatgen.io.lobster.future.outputs.misc import ( - BWDF, - BWDFCOHP, - POLARIZATION, - LobsterMatrices, - MadelungEnergies, - SitePotentials, - Wavefunction, -) -from pymatgen.io.lobster.future.outputs.populations import CHARGE, CHARGE_LCFO, GROSSPOP, GROSSPOP_LCFO -from pymatgen.util.due import Doi, due # type: ignore[import] - -__author__ = "Tom Demeyere" -__copyright__ = "Copyright 2025, The Materials Project" -__version__ = "0.3" -__maintainer__ = "Tom Demeyere" -__email__ = "tom.demeyere@bam.de" -__date__ = "Sep. 30, 2025" - -due.cite( - Doi("10.1002/cplu.202200123"), - description="Automated Bonding Analysis with Crystal Orbital Hamilton Populations", -) - -__all__ = [ - "BWDF", - "BWDFCOHP", - "CHARGE", - "CHARGE_LCFO", - "COBICAR", - "COBICAR_LCFO", - "COHPCAR", - "COHPCAR_LCFO", - "COOPCAR", - "COXXCAR", - "DOSCAR", - "DOSCAR_LCFO", - "GROSSPOP", - "GROSSPOP_LCFO", - "ICOBILIST", - "ICOBILIST_LCFO", - "ICOHPLIST", - "ICOHPLIST_LCFO", - "ICOOPLIST", - "ICOXXLIST", - "POLARIZATION", - "BandOverlaps", - "Fatband", - "Fatbands", - "LobsterMatrices", - "LobsterOut", - "MadelungEnergies", - "NcICOBILIST", - "SitePotentials", - "Wavefunction", -] diff --git a/src/pymatgen/io/lobster/future/outputs/bands.py b/src/pymatgen/io/lobster/future/outputs/bands.py deleted file mode 100644 index 50cb9c0c5c..0000000000 --- a/src/pymatgen/io/lobster/future/outputs/bands.py +++ /dev/null @@ -1,500 +0,0 @@ -from __future__ import annotations - -import re -from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Self, cast - -import numpy as np -from monty.json import MSONable - -from pymatgen.core.structure import Structure -from pymatgen.electronic_structure.core import Spin -from pymatgen.io.lobster.future.constants import LOBSTER_VERSION -from pymatgen.io.lobster.future.core import LobsterFile -from pymatgen.io.lobster.future.utils import natural_sort, parse_orbital_from_text -from pymatgen.io.lobster.future.versioning import version_processor -from pymatgen.io.vasp.inputs import Kpoints -from pymatgen.io.vasp.outputs import Vasprun - -if TYPE_CHECKING: - from pymatgen.core.structure import IStructure - from pymatgen.io.lobster.future.types import LobsterBandOverlaps, LobsterFatband - from pymatgen.util.typing import PathLike - - -class BandOverlaps(LobsterFile): - """Parser for bandOverlaps.lobster files. - - Parses band overlap information produced by LOBSTER and stores it in a structured dictionary keyed by spin. - See the :class:`~pymatgen.io.lobster.future.types.LobsterBandOverlaps` type for details. - - Attributes: - band_overlaps (dict[Spin, dict]): - "k_points", "max_deviations", and "matrices" holding the corresponding data. - - "k_points" (list[list[float]]): List of k-point coordinates. - - "max_deviations" (list[float]): List of maximal deviations for each k-point. - - "matrices" (list[np.ndarray]): List of overlap matrices for each k-point. - - each holding data for each spin channel. - """ - - @version_processor(max_version="3.2") - def parse_file_v3_2_legacy(self) -> None: - """Parse bandOverlaps.lobster file for LOBSTER versions ≤3.2. - - Uses legacy spin numbering [0, 1] for parsing. - """ - self.parse_file(spin_numbers=[0, 1]) - - @version_processor(min_version="4.0") - def parse_file_v4_0(self) -> None: - """Parse bandOverlaps.lobster file for LOBSTER versions ≥4.0. - - Uses updated spin numbering [1, 2] for parsing. - """ - self.parse_file(spin_numbers=[1, 2]) - - def parse_file(self, spin_numbers: list[int]) -> None: - """Read all lines of the file and populate `self.band_overlaps`. - - Args: - spin_numbers (list[int]): Two integers indicating the spin numbering used - in the file (e.g., [0, 1] for legacy or [1, 2] for newer versions). - - Raises: - ValueError: If no data is found for a key in the bandOverlaps file. - """ - n_kpoints = {Spin.up: 0, Spin.down: 0} - matrix_size = None - - current_spin = Spin.up - for line in self.iterate_lines(): - if f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[0]}" in line: - current_spin = Spin.up - elif f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[1]}" in line: - current_spin = Spin.down - elif "maxDeviation" in line: - n_kpoints[current_spin] += 1 - elif matrix_size is None: - try: - float(line.split()[0]) - matrix_size = len(line.split()) - except ValueError: - continue - - if matrix_size is None: - raise ValueError("No data found for band overlaps in the file.") - - self.band_overlaps: LobsterBandOverlaps = { - "k_points": {}, - "max_deviations": {}, - "matrices": {}, - } - - self.spins = [Spin.up] - if n_kpoints[Spin.down] > 0: - self.spins.append(Spin.down) - - for spin in self.spins: - n = n_kpoints[spin] - self.band_overlaps["k_points"][spin] = np.empty((n, 3), dtype=np.float64) - self.band_overlaps["max_deviations"][spin] = np.empty(n, dtype=np.float64) - self.band_overlaps["matrices"][spin] = np.empty((n, matrix_size, matrix_size), dtype=np.float64) - - current_spin = Spin.up - kpoint_idx = {Spin.up: 0, Spin.down: 0} - matrix_row = 0 - for line in self.iterate_lines(): - if f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[0]}" in line: - current_spin = Spin.up - elif f"Overlap Matrix (abs) of the orthonormalized projected bands for spin {spin_numbers[1]}" in line: - current_spin = Spin.down - elif "k-point" in line: - self.band_overlaps["k_points"][current_spin][kpoint_idx[current_spin]] = [ - float(el) for el in line.strip().split()[-3:] - ] - kpoint_idx[current_spin] += 1 - elif "maxDeviation" in line: - maxdev = line.split(" ")[-1] - self.band_overlaps["max_deviations"][current_spin][kpoint_idx[current_spin] - 1] = float(maxdev) - - matrix_row = 0 - elif line.strip(): - try: - parts = [float(el) for el in re.split(r"\s+", line.strip())] - except ValueError: - raise ValueError(f"Incomplete or non-numeric data found in bandOverlaps file at line: {line}") - - if len(parts) == matrix_size: - self.band_overlaps["matrices"][current_spin][kpoint_idx[current_spin] - 1, matrix_row] = parts - - matrix_row += 1 - - def has_good_quality_max_deviation(self, limit_max_deviation: float = 0.1) -> bool: - """Check if the maxDeviation values are within a given limit. - - Args: - limit_max_deviation (float): Upper limit for acceptable max_deviation. - - Returns: - bool: True if all recorded max_deviation values are <= limit_max_deviation. - """ - return all( - deviation <= limit_max_deviation for deviation in self.band_overlaps["max_deviations"].get(Spin.up, []) - ) and all( - deviation <= limit_max_deviation for deviation in self.band_overlaps["max_deviations"].get(Spin.down, []) - ) - - def has_good_quality_check_occupied_bands( - self, - number_occ_bands_spin_up: int, - number_occ_bands_spin_down: int | None = None, - spin_polarized: bool = False, - limit_deviation: float = 0.1, - ) -> bool: - """Check if deviation from the ideal overlap for occupied bands is acceptable. - - Args: - number_occ_bands_spin_up (int): Number of occupied bands for spin up. - number_occ_bands_spin_down (int | None): Number of occupied bands for spin down. - Required if spin_polarized is True. - spin_polarized (bool): Whether the calculation is spin-polarized. - limit_deviation (float): Acceptable absolute tolerance for deviations. - - Raises: - ValueError: If `number_occ_bands_spin_down` is not specified for spin-polarized calculations. - - Returns: - bool: True if all occupied-band submatrices are close to identity within the tolerance. - """ - if spin_polarized and number_occ_bands_spin_down is None: - raise ValueError("number_occ_bands_spin_down has to be specified") - - for spin in (Spin.up, Spin.down) if spin_polarized else (Spin.up,): - if spin is Spin.up: - num_occ_bands = number_occ_bands_spin_up - else: - if number_occ_bands_spin_down is None: - raise ValueError("number_occ_bands_spin_down has to be specified") - num_occ_bands = number_occ_bands_spin_down - - for overlap_matrix in self.band_overlaps["matrices"][spin]: - sub_array = np.asarray(overlap_matrix)[:num_occ_bands, :num_occ_bands] - - if not np.allclose(sub_array, np.identity(num_occ_bands), atol=limit_deviation, rtol=0): - return False - - return True - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for the `BandOverlaps` class. - - Returns: - str: Default filename. - """ - return "bandOverlaps.lobster" - - @classmethod - def from_dict(cls, d: dict[str, Any]) -> Self: - """Reconstruct a BandOverlaps instance from a dictionary. - - Args: - d (dict[str, Any]): Dictionary representation of a BandOverlaps instance. - - Returns: - BandOverlaps: Reconstructed instance. - """ - instance = super().from_dict(d) - - instance.band_overlaps["k_points"] = { - spin: np.asarray(k_points, dtype=np.float64) - for spin, k_points in instance.band_overlaps["k_points"].items() - } - instance.band_overlaps["max_deviations"] = { - spin: np.asarray(deviations, dtype=np.float64) - for spin, deviations in instance.band_overlaps["max_deviations"].items() - } - instance.band_overlaps["matrices"] = { - spin: np.asarray(matrices, dtype=np.float64) - for spin, matrices in instance.band_overlaps["matrices"].items() - } - - return instance - - -class Fatbands(MSONable): - """Reader for multiple FATBAND_*.lobster files in a directory. - - Collects FATBAND files, reads VASP outputs for the Fermi level and kpoints, and aggregates per-file parsed data. - - Attributes: - efermi (float): Fermi level read from vasprun.xml. - spins (list[Spin]): Spins present in the FATBAND files. - kpoints (Kpoints): Parsed KPOINTS used for the lobster FatBand calculations. - filenames (list[Path]): Sorted list of matched filenames. - structure (Structure): Structure object used for projections. - reciprocal_lattice (Lattice): Reciprocal lattice of the structure. - lobster_version (str): LOBSTER version string used for parsing. - fatbands (list[dict]): Aggregated parsed fatband data after process() is called. - """ - - def __init__( - self, - directory: PathLike = ".", - structure: IStructure | None = None, - kpoints_file: PathLike = "KPOINTS", - vasprun_file: PathLike = "vasprun.xml", - process_immediately: bool = True, - lobster_version: str | None = None, - ) -> None: - """Initialize the Fatbands reader. - - Args: - directory (PathLike): Path to directory containing FATBAND files. - structure (IStructure | None): Structure object. If None, POSCAR.lobster is read from directory. - kpoints_file (PathLike): Name of the KPOINTS file to be read in the directory. - vasprun_file (PathLike): Name of the vasprun.xml file to be read in the directory. - process_immediately (bool): If True, process FATBAND files immediately after initialization. - lobster_version (str | None): Optional LOBSTER version string. If None, the default LOBSTER_VERSION is used. - - Raises: - FileNotFoundError: If required files are missing in the directory. - ValueError: If no FATBAND files are found or KPOINTS file lacks weights. - """ - self.directory = Path(directory) - - self.filenames = sorted( - [str(i) for i in self.directory.glob(Fatband.get_default_filename())], - key=natural_sort, - ) - - if len(self.filenames) == 0: - raise ValueError("No FATBAND files found in the provided directory") - - self.efermi = Vasprun( - filename=self.directory / vasprun_file, - parse_eigen=False, - parse_potcar_file=False, - ).efermi - self.spins = [Spin.up] - full_kpoints = Kpoints.from_file(self.directory / kpoints_file) - - if full_kpoints.kpts_weights is not None: - filtered_data = [ - (k, w, n) - for k, w, n in zip( - full_kpoints.kpts, - full_kpoints.kpts_weights, - (full_kpoints.labels or [None] * len(full_kpoints.kpts)), - strict=True, - ) - if w == 0 - ] - - new_kpts, new_weights, new_labels = zip(*filtered_data, strict=True) if filtered_data else ([], [], []) - - coord_type = full_kpoints.coord_type - - if coord_type is None: - pass - elif coord_type not in {"Reciprocal", "Cartesian"}: - raise ValueError("KPOINTS coord_type must be 'Reciprocal' or 'Cartesian' for `Fatbands` parsing.") - - coord_type = cast("Literal['Reciprocal', 'Cartesian'] | None", coord_type) - - self.kpoints = Kpoints( - comment=full_kpoints.comment, - num_kpts=len(new_kpts), - style=full_kpoints.style, - kpts=list(new_kpts), - kpts_weights=list(new_weights), - labels=list(new_labels) if any(new_labels) else None, - coord_type=coord_type, - ) - else: - raise ValueError("KPOINTS file must contain weights for `Fatbands` parsing.") - - if structure is None: - try: - self.structure = Structure.from_file(Path(directory, "POSCAR.lobster")) - except FileNotFoundError: - raise FileNotFoundError("No POSCAR.lobster file found in directory, structure has to be given") - else: - self.structure = structure - - self.reciprocal_lattice = self.structure.lattice.reciprocal_lattice - - self.fatbands: list[LobsterFatband] = [] - - self.lobster_version = lobster_version or LOBSTER_VERSION - - if process_immediately: - self.process() - - def process(self) -> None: - """Parse all FATBAND files and aggregate fatband data. - - Raises: - ValueError: If the number of kpoints does not match or if there is a mix of spin-polarized and - non-spin-polarized files. - """ - is_spin_polarized = None - - for filename in self.filenames: - fatband = Fatband( - filename=filename, - process_immediately=False, - ) - fatband.lobster_version = self.lobster_version - - fatband.process() - fatband_data = fatband.fatband - - for spin in fatband.spins: - if len(fatband_data["projections"][spin]) != self.kpoints.num_kpts: - raise ValueError( - f"Number of kpoints ({self.kpoints.num_kpts}) does not " - f"match number of kpoints for {filename} " - f"({len(fatband_data['projections'][spin])})" - ) - - if is_spin_polarized is None: - is_spin_polarized = len(fatband_data["projections"]) > 1 - elif is_spin_polarized != (len(fatband_data["projections"]) > 1): - raise ValueError("Mix of spin polarized and non-spin polarized FATBAND files") - - self.fatbands.append(fatband_data) - - if is_spin_polarized: - self.spins.append(Spin.down) - - as_dict = LobsterFile.as_dict - - has_spin = LobsterFile.has_spin - - is_spin_polarized = LobsterFile.is_spin_polarized - - -class Fatband(LobsterFile): - """Parser for a single FATBAND_*.lobster file. - - Parses a single FATBAND file and stores: - center (str): Central atom/species label parsed from filename. - orbital (str): Orbital descriptor parsed from filename. - nbands (int): Number of bands in the FATBAND file. - fatband (LobsterFatband): Parsed fatband data dictionary. Please see - :class:`~pymatgen.io.lobster.future.types.LobsterFatband` for details. - - The parsed data is available in the fatband attribute after parse_file(). - """ - - def __init__( - self, - filename: PathLike, - process_immediately: bool = True, - lobster_version: str | None = None, - ) -> None: - """Initialize a Fatband parser. - - Args: - filename (PathLike): Path to the FATBAND file to parse. - process_immediately (bool): If True, parse the file during initialization. - lobster_version (str | None): LOBSTER version string to use for parsing. If None, attempts to detect - from file or falls back to default. - - Raises: - ValueError: If the orbital name cannot be parsed from the filename. - """ - self.center = Path(filename).name.split("_")[1].title() - - if orbital := parse_orbital_from_text(Path(filename).stem): - self.orbital = orbital - else: - raise ValueError( - f"Could not parse orbital from filename {filename}. " - "Ensure it follows the FATBAND_
_.lobster pattern." - ) - - super().__init__( - filename=filename, - process_immediately=process_immediately, - lobster_version=lobster_version, - ) - - @version_processor() - def parse_file(self) -> None: - """Parse the FATBAND file and populate the fatband attribute.""" - fatband: dict[str, dict[Spin, list[Any]]] = { - "energies": {Spin.up: []}, - "projections": {Spin.up: []}, - } - self.spins = [Spin.up] - - current_spin = Spin.up - for idx, line in enumerate(self.iterate_lines()): - if idx == 0: - self.nbands = int(line.split()[-1]) - continue - - if line.startswith("#"): - current_spin = Spin.up - - fatband["energies"][current_spin].append([]) - fatband["projections"][current_spin].append([]) - - continue - - if len(fatband["projections"][current_spin][-1]) == self.nbands: - current_spin = Spin.down - - if current_spin not in self.spins: - self.spins.append(current_spin) - fatband["energies"][current_spin] = [[]] - fatband["projections"][current_spin] = [[]] - else: - fatband["energies"][current_spin].append([]) - fatband["projections"][current_spin].append([]) - - if data := re.findall(r"[+-]?(?:[0-9]*[.])?[0-9]+", line): - fatband["energies"][current_spin][-1].append(float(data[1])) - fatband["projections"][current_spin][-1].append(float(data[-1])) - - self.fatband: LobsterFatband = { - "center": self.center, - "orbital": self.orbital, - "energies": fatband["energies"], - "projections": fatband["projections"], - } - - self.convert_to_numpy_arrays() - - def convert_to_numpy_arrays(self) -> None: - """Convert lists in band_overlaps to numpy arrays.""" - for spin in self.spins: - self.fatband["energies"][spin] = np.asarray(self.fatband["energies"][spin], dtype=np.float64) - self.fatband["projections"][spin] = np.asarray(self.fatband["projections"][spin], dtype=np.float64) - - @classmethod - def from_dict(cls, d: dict[str, Any]) -> Self: - """Reconstruct a Fatband instance from a dictionary. - - Args: - d (dict[str, Any]): Dictionary representation of a Fatband instance. - - Returns: - Fatband: Reconstructed instance. - """ - instance = super().from_dict(d) - instance.convert_to_numpy_arrays() - - return instance - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for the Fatband class. - - Returns: - str: Default filename pattern. - """ - return "FATBAND_*.lobster" diff --git a/src/pymatgen/io/lobster/future/outputs/coxxcar.py b/src/pymatgen/io/lobster/future/outputs/coxxcar.py deleted file mode 100644 index f7099b4b0b..0000000000 --- a/src/pymatgen/io/lobster/future/outputs/coxxcar.py +++ /dev/null @@ -1,380 +0,0 @@ -from __future__ import annotations - -import re -from collections import defaultdict -from itertools import islice -from typing import TYPE_CHECKING - -import numpy as np - -from pymatgen.electronic_structure.core import Spin -from pymatgen.io.lobster.future.core import LobsterInteractionsHolder -from pymatgen.io.lobster.future.versioning import version_processor - -if TYPE_CHECKING: - from typing import ClassVar, Literal - - from numpy.typing import NDArray - - -class COXXCAR(LobsterInteractionsHolder): - """Reader for COXXCAR-style files (COOPCAR, COHPCAR, COBICAR). - - Parses LOBSTER's COXXCAR outputs and organizes bond and orbital-resolved interaction data. - - Attributes: - filename (PathLike): Input file path. - num_bonds (int): Number of bond interactions reported. - num_data (int): Number of energy/data points. - efermi (float): Fermi energy from the file. - spins (list[Spin]): Present spin channels. - interactions (list[dict]): Parsed interaction metadata. - data (np.ndarray): Raw numerical table parsed from the file. - """ - - interactions_regex: ClassVar[str] = ( - r"(?i)([a-z]+\d*(?:\_\d+)?)(?:\[(\-?\d+\s+\-?\d+\s+\-?\d+)\])?(?:\[([^]\s]*)\])?(?:\(([^)]*)\))?" - ) - coxxcar_type: ClassVar[str] - - @property - def energies(self) -> NDArray[np.floating]: - """Return the energy grid. - - Returns: - NDArray[np.floating]: Energies (first column of self.data). - """ - return self.data[:, 0] - - def parse_header(self) -> None: - """Parse the file header and set metadata attributes. - - Args: - lines (list[str]): Lines of the COXXCAR file. - """ - data = list(islice(self.iterate_lines(), 2))[1].split() - - self.num_bonds = int(data[0]) - self.num_data = int(data[2]) - self.efermi = float(data[-1]) - - if int(data[1]) == 2: - self.spins = [Spin.up, Spin.down] - else: - self.spins = [Spin.up] - - def parse_bonds(self) -> None: - """Parse the bonds/interactions header block. - - Args: - lines (list[str]): Lines of the COXXCAR file. - """ - self.interactions = [] - - self.parse_header() - - lines_generator = islice(self.iterate_lines(), 2, self.num_bonds + 2) - - for line in lines_generator: - if "Average" in line: - self.interactions.append( - { - "index": 0, - "centers": ["Average"], - "orbitals": [None], - "cells": [[]], - "length": None, - } - ) - continue - - bond_index, bond_data = line.split(":", 1) - - if bond_regex_results := re.search(r"No\.(\d+)", bond_index): - bond_index = bond_regex_results.group(1) - else: - raise ValueError(f"Could not parse bond index from line: {line}") - - length = None - - bond_tmp: dict[str, list] = defaultdict(list) - - centers = bond_data.split("->") - for center in centers: - if match := re.search(self.interactions_regex, center): - match = match.groups() - bond_tmp["centers"].append(match[0]) - else: - raise ValueError(f"Could not parse interaction from line: {line}") - - if match[1]: - bond_tmp["cells"].append([int(x) for x in match[1].split()]) - else: - bond_tmp["cells"].append([]) - - bond_tmp["orbitals"].append(match[2]) - - if match[3]: - length = float(match[3]) - - bond = { - "index": int(bond_index), - "centers": bond_tmp["centers"], - "cells": bond_tmp["cells"], - "orbitals": bond_tmp["orbitals"], - "length": length, - } - - self.interactions.append(bond) - - def parse_data(self) -> None: - """Parse the numerical data block into `self.data` and validate shape. - - Args: - lines (list[str]): Lines of the COXXCAR file. - - Raises: - ValueError: If the parsed data array shape does not match the expected shape. - """ - self.data = np.genfromtxt( - self.iterate_lines(), - dtype=np.float64, - skip_header=self.num_bonds + 2, - loose=False, - ) - - if self.data.shape != (self.num_data, self.num_bonds * 2 * len(self.spins) + 1): - raise ValueError( - f"Data shape {self.data.shape} does not match expected shape " - f"({self.num_data}, {self.num_bonds * 2 * len(self.spins) + 1})." - ) - - self.process_data_into_interactions() - - def process_data_into_interactions(self) -> None: - """Populate each interaction dict with 'coxx' and 'icoxx' views. - - Assigns numpy views into `self.data` for each spin channel. - """ - for i, interaction in enumerate(self.interactions): - real_indices = self.interaction_indices_to_data_indices_mapping( - i, - spins=self.spins, - ) - - interaction["coxx"] = {} - interaction["icoxx"] = {} - - if len(self.spins) == 1: - interaction["coxx"][Spin.up] = self.data[:, real_indices[0]] - interaction["icoxx"][Spin.up] = self.data[:, real_indices[1]] - else: - interaction["coxx"][Spin.up] = self.data[:, real_indices[0]] - interaction["icoxx"][Spin.up] = self.data[:, real_indices[1]] - interaction["coxx"][Spin.down] = self.data[:, real_indices[2]] - interaction["icoxx"][Spin.down] = self.data[:, real_indices[3]] - - @version_processor(min_version="5.1") - def parse_file(self) -> None: - """Parse the full COXXCAR file (header and data).""" - self.parse_bonds() - self.parse_data() - - def get_data_indices_by_properties( - self, - indices: list[int] | None = None, - centers: list[str] | None = None, - cells: list[list[int]] | None = None, - orbitals: list[str] | None = None, - length: tuple[float, float] | None = None, - spins: list[Literal[1, -1]] | None = None, - data_type: Literal["coxx", "icoxx"] | None = None, - ) -> list[int]: - """Return data-column indices matching the provided interaction properties. - - Args: - indices (list[int] | None): Interaction indices to filter. - centers (list[str] | None): Atom centers to filter. - cells (list[list[int]] | None): Unit cell indices to filter. - orbitals (list[str] | None): Orbitals to filter. - length (tuple[float, float] | None): Length range to filter. - spins (list[Spin] | None): Spins to include. - data_type (Literal["coxx", "icoxx"] | None): Restrict column type. - - Returns: - list[int]: Sorted list of data column indices that match the filters. - """ - return self.interaction_indices_to_data_indices_mapping( - sorted( - self.get_interaction_indices_by_properties( - indices, - centers, - cells, - orbitals, - length, - ) - ), - spins=spins or self.spins, - data_type=data_type, - ) - - def get_data_by_properties( - self, - indices: list[int] | None = None, - centers: list[str] | None = None, - cells: list[list[int]] | None = None, - orbitals: list[str] | None = None, - length: tuple[float, float] | None = None, - spins: list[Literal[1, -1]] | None = None, - data_type: Literal["coxx", "icoxx"] | None = None, - ) -> NDArray[np.floating]: - """Return the data columns matching the provided interaction properties. - - Args: - indices (list[int] | None): Interaction indices to filter. - centers (list[str] | None): Atom centers to filter. - cells (list[list[int]] | None): Unit cell indices to filter. - orbitals (list[str] | None): Orbitals to filter. - length (tuple[float, float] | None): Length range to filter. - spins (list[Spin] | None): Spins to include. - data_type (Literal["coxx", "icoxx"] | None): Restrict column type. - - Returns: - np.ndarray: Array with shape (n_energies, n_selected_columns). - """ - bond_indices = self.get_interaction_indices_by_properties(indices, centers, cells, orbitals, length) - spins = spins or self.spins - - return self.data[ - :, - self.interaction_indices_to_data_indices_mapping( - bond_indices, - spins=spins, - data_type=data_type, - ), - ] - - def interaction_indices_to_data_indices_mapping( - self, - interaction_indices: int | list[int], - spins: Literal[1, -1] | list[Literal[1, -1]] | None = None, - data_type: Literal["coxx", "icoxx"] | None = None, - ) -> list[int]: - """Map interaction indices to column indices in `self.data`. - - Args: - interaction_indices (int | list[int]): Single index or list of interaction indices. - spins (Spin | list[Spin] | None): Spin(s) to include. - data_type (Literal["coxx", "icoxx"] | None): Select columns of that type. - - Returns: - list[int]: Sorted list of integer column indices into `self.data`. - - Raises: - ValueError: If an invalid Spin is requested. - """ - if spins is None: - spins = self.spins - - if spins in (1, -1): - spins = [spins] - if isinstance(interaction_indices, int): - interaction_indices = [interaction_indices] - - if set(spins) - set(self.spins): - raise ValueError(f"Requested `Spin` {spins} is not valid. Valid `Spin`s are: {self.spins}.") - - index_range = np.arange(0, self.num_bonds * 2 * len(spins) + 1) - - if data_type == "icoxx": - index_range = index_range[1::2] - elif data_type == "coxx": - index_range = index_range[::2] - - real_indices = [] - for bond_index in interaction_indices: - real_indices.extend([bond_index * 2 + 1, bond_index * 2 + 2]) - - if Spin.down in spins: - real_indices.extend( - [ - (self.num_bonds + bond_index) * 2 + 1, - (self.num_bonds + bond_index) * 2 + 2, - ] - ) - - real_indices = np.array(real_indices, dtype=int) - real_indices = np.intersect1d(real_indices, index_range) - - return sorted(real_indices.tolist()) - - -class COBICAR(COXXCAR): - """Reader for COBICAR.lobster files. - - Attributes: - coxxcar_type (str): Type of COXXCAR file ("COBICAR"). - is_lcfo (bool): Whether the file is in LCFO format. - """ - - coxxcar_type: ClassVar[str] = "COBICAR" - - is_lcfo: ClassVar[bool] = False - - @classmethod - def get_default_filename(cls) -> str: - """Return the default filename for COBICAR.""" - return "COBICAR.LCFO.lobster" if cls.is_lcfo else "COBICAR.lobster" - - -class COHPCAR(COXXCAR): - """Reader for COHPCAR.lobster files. - - Attributes: - coxxcar_type (str): Type of COXXCAR file ("COHPCAR"). - is_lcfo (bool): Whether the file is in LCFO format. - """ - - coxxcar_type: ClassVar[str] = "COHPCAR" - - is_lcfo: ClassVar[bool] = False - - @classmethod - def get_default_filename(cls) -> str: - """Return the default filename for COOPCAR.""" - return "COHPCAR.LCFO.lobster" if cls.is_lcfo else "COHPCAR.lobster" - - -class COOPCAR(COXXCAR): - """Reader for COOPCAR.lobster files. - - Attributes: - coxxcar_type (str): Type of COXXCAR file ("COOPCAR"). - """ - - coxxcar_type: ClassVar[str] = "COOPCAR" - - @classmethod - def get_default_filename(cls) -> str: - """Return the default filename for COOPCAR.""" - return "COOPCAR.lobster" - - -class COHPCAR_LCFO(COHPCAR): - """Reader for COHPCAR.LCFO.lobster files. - - Attributes: - is_lcfo (bool): Always True for LCFO format. - """ - - is_lcfo: ClassVar[bool] = True - - -class COBICAR_LCFO(COBICAR): - """Reader for COBICAR.LCFO.lobster files. - - Attributes: - is_lcfo (bool): Always True for LCFO format. - """ - - is_lcfo: ClassVar[bool] = True diff --git a/src/pymatgen/io/lobster/future/outputs/doscar.py b/src/pymatgen/io/lobster/future/outputs/doscar.py deleted file mode 100644 index cfbae457bd..0000000000 --- a/src/pymatgen/io/lobster/future/outputs/doscar.py +++ /dev/null @@ -1,216 +0,0 @@ -from __future__ import annotations - -import re -from collections import defaultdict -from itertools import islice -from typing import TYPE_CHECKING - -import numpy as np - -from pymatgen.core.periodic_table import Element -from pymatgen.electronic_structure.core import Spin -from pymatgen.electronic_structure.dos import Dos -from pymatgen.io.lobster.future.core import LobsterFile -from pymatgen.io.lobster.future.versioning import version_processor - -if TYPE_CHECKING: - from typing import ClassVar - - from numpy import floating - from numpy.typing import NDArray - - from pymatgen.util.typing import PathLike - - -class DOSCAR(LobsterFile): - """Represents LOBSTER's projected DOS and local projected DOS. - - This class parses and stores data from the DOSCAR file generated by LOBSTER, - which contains information about the total and projected density of states - (DOS) for a quantum-chemical calculation performed with VASP. - - Attributes: - completedos (LobsterCompleteDos): Complete DOS data. - pdos (list[dict[str, dict[Spin, np.ndarray]]]): Projected DOS data. - Access as `pdos[atomindex]['orbitalstring'][Spin.up/Spin.down]`. - tdos (Dos): Total density of states. - energies (np.ndarray): Energies at which the DOS was calculated - (in eV, relative to Efermi). - tdensities (dict[Spin, np.ndarray]): Total density arrays for each spin channel. - itdensities (dict[Spin, np.ndarray]): Integrated total density arrays for each spin channel. - is_spin_polarized (bool): Whether the system is spin polarized. - """ - - is_lcfo: ClassVar[bool] = False - - def __init__( - self, - filename: PathLike | None = None, - process_immediately: bool = True, - lobster_version: str | None = None, - ) -> None: - """Initialize a DOSCAR object. - - Args: - filename (PathLike | None): Path to the DOSCAR file, typically "DOSCAR.lobster". - process_immediately (bool): Whether to process the file immediately upon initialization. - lobster_version (str | None): LOBSTER version string to use for parsing. If None, attempts to detect - - Raises: - ValueError: If neither `structure_file` nor `structure` is provided. - """ - self.projected_dos: dict[str, dict[str, Dos]] - self.total_dos: Dos - self.integrated_total_dos: Dos - - super().__init__( - filename=filename, - process_immediately=process_immediately, - lobster_version=lobster_version, - ) - - @version_processor() - def process(self) -> None: - """Process the DOSCAR file and extract DOS data. - - Parses the DOSCAR file to extract total DOS, projected DOS, energies, - and integrated densities. Sets the appropriate attributes based on - whether the calculation is spin-polarized. - - Raises: - ValueError: If the DOSCAR file format is invalid or spin polarization - cannot be determined. - """ - total_dos, integrated_total_dos = {}, {} - data: list[NDArray] = [] - - centers: list[str] = [] - orbitals: list[list[str]] = [] - - self.spins = [Spin.up] - - header_regex = r"\s*\S+\s+\S+\s+(\d+)\s+(\S+)\s+1\.0+(?:;(.*);(.*))?" - - efermi = None - ndos = 0 - - lines_iter = iter(self.iterate_lines()) - - center_counts = defaultdict(int) - - for line in islice(lines_iter, 5, None): - if match := re.match(header_regex, line): - ndos = int(match.group(1)) - - if efermi is None: - efermi = float(match.group(2)) - - if center_match := match.group(3): - center_match = center_match.strip() - - if center_match.startswith("Z="): - center_match = Element.from_Z(int(center_match.split()[-1])).symbol - - center_counts[center_match] += 1 - - separator = "_" if self.is_lcfo else "" - - centers.append(f"{center_match}{separator}{center_counts[center_match]}") - - if orbital_match := match.group(4): - orbitals += [[orb.strip() for orb in orbital_match.split()]] - - tmp_dos = [] - if line.strip(): - for _ in range(ndos): - line = next(lines_iter).split() - tmp_dos.append(line) - - data.append(np.array(tmp_dos, dtype=float)) - - if len(data[0][0, :]) == 5: - self.spins.append(Spin.down) - elif len(data[0][0, :]) != 3: - raise ValueError("There is something wrong with the DOSCAR. Can't extract spin polarization.") - - if efermi is None: - raise ValueError("There is something wrong with the DOSCAR. Can't find efermi.") - - energies = data[0][:, 0] - projected_dos = {} - - if self.is_spin_polarized: - total_dos[Spin.up] = data[0][:, 1] - total_dos[Spin.down] = data[0][:, 2] - - integrated_total_dos[Spin.up] = data[0][:, 3] - integrated_total_dos[Spin.down] = data[0][:, 4] - else: - total_dos[Spin.up] = data[0][:, 1] - integrated_total_dos[Spin.up] = data[0][:, 2] - - for atom_counter in range(len(data) - 1): - block_data = data[atom_counter + 1] - center = centers[atom_counter] - - if center not in projected_dos: - projected_dos[center] = {} - - for spin_index, spin in enumerate(self.spins): - for orbital_index, row in enumerate(range(spin_index + 1, block_data.shape[1], len(self.spins))): - orbital = orbitals[atom_counter][orbital_index] - - if orbital not in projected_dos[center]: - projected_dos[center][orbital] = {} - - projected_dos[center][orbital][spin] = block_data[:, row] - - for center, orbitals in projected_dos.items(): - for orbital, dos in orbitals.items(): - projected_dos[center][orbital] = Dos(efermi, energies, dos) - - self.projected_dos: dict[str, dict[str, Dos]] = dict(projected_dos) - self.total_dos: Dos = Dos(efermi, energies, total_dos) - self.integrated_total_dos: Dos = Dos(efermi, energies, integrated_total_dos) - - @property - def efermi(self) -> float: - """Fermi energy in eV.""" - return self.total_dos.efermi - - @property - def energies(self) -> NDArray[floating]: - """Energies at which the DOS was calculated (in eV, relative to efermi).""" - return self.total_dos.energies - - @property - def is_spin_polarized(self) -> bool: - """Whether the system is spin polarized. - - Returns: - bool: True if the system is spin polarized, False otherwise. - """ - return len(self.spins) == 2 - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for the DOSCAR. - - Returns: - str: Default filename string. "DOSCAR.lobster" for regular DOSCAR, - "DOSCAR.LCFO.lobster" for LCFO analysis. - """ - return "DOSCAR.lobster" if not cls.is_lcfo else "DOSCAR.LCFO.lobster" - - -class DOSCAR_LCFO(DOSCAR): - """Represents LOBSTER's projected DOS and local projected DOS for LCFO analysis. - - This class handles DOSCAR files generated from LCFO analysis, which have - a different format than regular DOSCAR files. - - Attributes: - is_lcfo (bool): Class variable indicating this is for LCFO analysis. Always True. - """ - - is_lcfo: ClassVar[bool] = True diff --git a/src/pymatgen/io/lobster/future/outputs/icoxxlist.py b/src/pymatgen/io/lobster/future/outputs/icoxxlist.py deleted file mode 100644 index ad8c89a614..0000000000 --- a/src/pymatgen/io/lobster/future/outputs/icoxxlist.py +++ /dev/null @@ -1,428 +0,0 @@ -from __future__ import annotations - -import re -from collections import defaultdict -from itertools import islice -from typing import TYPE_CHECKING - -import numpy as np - -from pymatgen.electronic_structure.core import Spin -from pymatgen.io.lobster.future.core import LobsterInteractionsHolder -from pymatgen.io.lobster.future.outputs.coxxcar import COXXCAR -from pymatgen.io.lobster.future.utils import parse_orbital_from_text -from pymatgen.io.lobster.future.versioning import version_processor - -if TYPE_CHECKING: - from typing import ClassVar, Literal - - from numpy.typing import NDArray - - from pymatgen.io.lobster.future.types import LobsterInteractionData - - -class ICOXXLIST(LobsterInteractionsHolder): - """Reader for ICOXX data files (ICOHPLIST, ICOOPLIST, ICOBILIST). - - Parses interaction data from ICOXXLIST files, including spin-resolved values. - - Attributes: - interactions (list[LobsterInteractionData]): List of parsed interactions. - spins (list[Spin]): List of spins present in the file. - data (NDArray[np.floating]): Array of ICOXX values for each interaction and spin. - icoxxlist_type (str): Type of ICOXXLIST file ("COHP", "COOP", "COBI"). - is_lcfo (bool): Whether the file is in LCFO format (if applicable). - """ - - interactions_regex: ClassVar[str] = ( - r"(?i)\s*(\d+)\s+(\S+\s+\S+)\s+(\d+\.\d+)\s+(\-?\d+\s+\-?\d+\s+\-?\d+)?\s+(\-?\d+\.\d+)(?:\s+(\-?\d+\.\d+))?" - ) - icoxxlist_type: ClassVar[str] - - @version_processor(max_version="5.1") - def parse_file_legacy(self) -> None: - """Parse ICOXXLIST file using legacy format (versions ≤5.1). - - Extracts interaction data, including spin-resolved values, and populates - the `interactions`, `spins`, and `data` attributes. - - Raises: - ValueError: If the file contains invalid spin values or cannot parse - interaction lines. - """ - self.interactions = [] - self.spins = [] - - interaction_counter = 0 - for line in self.iterate_lines(): - if not line: - continue - - if line.startswith(f"{self.icoxxlist_type.upper()}#"): - if spin_regex := re.search(r"(?i)for spin\s+(\d)", line): - spin_regex = int(spin_regex.group(1)) - else: - continue - - if spin_regex == 1: - self.spins.append(Spin.up) - elif spin_regex == 2: - self.spins.append(Spin.down) - interaction_counter = 0 - else: - raise ValueError(f"Invalid spin value {spin_regex} in line: {line}") - else: - if matches := re.search(self.interactions_regex, line): - matches = matches.groups() - else: - raise ValueError(f"Could not parse interaction line: {line}") - - first_center, second_center = matches[1].split() - - first_orbital = parse_orbital_from_text(first_center) - second_orbital = parse_orbital_from_text(second_center) - - index = int(matches[0]) - centers = [first_center, second_center] - length = float(matches[2]) - - cells = [[], []] if matches[3] is None else [[0, 0, 0], [int(i) for i in matches[3].split()]] - - bond_tmp: LobsterInteractionData = { - "index": index, - "centers": centers, - "cells": cells, - "orbitals": [first_orbital, second_orbital], - "length": length, - "icoxx": { - self.spins[-1]: float(matches[4]), - }, - } - - if self.spins[-1] == Spin.up: - self.interactions.append(bond_tmp) - elif self.spins[-1] == Spin.down: - interaction = self.interactions[interaction_counter] - if "icoxx" in interaction: - interaction["icoxx"][Spin.down] = float(matches[4]) - else: - raise ValueError( - f"Down spin ICOXX value found without corresponding up spin value in line: {line}" - ) - - interaction_counter += 1 - - self.data = np.full((len(self.interactions), len(self.spins)), np.nan, dtype=np.float64) - - for i, interaction in enumerate(self.interactions): - if "icoxx" in interaction: - icoxx = interaction["icoxx"] - else: - raise ValueError(f"No ICOXX data found for interaction: {interaction}") - - if Spin.up in icoxx: - self.data[i, 0] = icoxx[Spin.up] - if Spin.down in icoxx: - self.data[i, 1] = icoxx[Spin.down] - - @version_processor(min_version="5.1") - def parse_file(self) -> None: - """Parse ICOXXLIST file using modern format (versions ≥5.1). - - Extracts interaction data, including spin-resolved values, and populates - the `interactions`, `spins`, and `data` attributes. - - Raises: - ValueError: If the file contains invalid spin values or cannot parse - interaction lines. - """ - self.interactions = [] - self.spins = [] - - for line in islice(self.iterate_lines(), 1, None): - if not line: - continue - - if spin_regex := re.findall(r"(?i)for spin\s+(\d)", line): - self.spins.append(Spin.up) - - if len(spin_regex) == 2: - self.spins.append(Spin.down) - else: - if matches := re.search(self.interactions_regex, line): - matches = matches.groups() - else: - raise ValueError(f"Could not parse interaction line: {line}") - - first_center, second_center = matches[1].split() - - first_orbital = parse_orbital_from_text(first_center) - second_orbital = parse_orbital_from_text(second_center) - - index = int(matches[0]) - centers = [ - first_center.replace(f"_{first_orbital}", ""), - second_center.replace(f"_{second_orbital}", ""), - ] - length = float(matches[2]) - - cells = [[], []] if matches[3] is None else [[0, 0, 0], [int(i) for i in matches[3].split()]] - - bond_tmp: LobsterInteractionData = { - "index": index, - "centers": centers, - "cells": cells, - "orbitals": [first_orbital, second_orbital], - "length": length, - "icoxx": { - Spin.up: float(matches[4]), - }, - } - - if len(self.spins) == 2: - bond_tmp["icoxx"][Spin.down] = float(matches[5]) - - self.interactions.append(bond_tmp) - - self.data = np.full((len(self.interactions), len(self.spins)), np.nan, dtype=np.float64) - - for i, interaction in enumerate(self.interactions): - if "icoxx" in interaction: - icoxx = interaction["icoxx"] - else: - raise ValueError(f"No ICOXX data found for interaction: {interaction}") - - if Spin.up in icoxx: - self.data[i, 0] = icoxx[Spin.up] - if Spin.down in icoxx: - self.data[i, 1] = icoxx[Spin.down] - - def get_data_by_properties( - self: LobsterInteractionsHolder, - indices: list[int] | None = None, - centers: list[str] | None = None, - cells: list[list[int]] | None = None, - orbitals: list[str] | None = None, - length: tuple[float, float] | None = None, - spins: list[Literal[1, -1]] | None = None, - ) -> NDArray[np.floating]: - """Get the data for bonds matching specified properties. - - Args: - indices (list[int] | None): Indices of bonds to retrieve. - centers (list[str] | None): Centers of bonds to retrieve. - cells (list[list[int]] | None): Cells of bonds to retrieve. - orbitals (list[str] | None): Orbitals of bonds to retrieve. - length (tuple[float, float] | None): Length range to filter. - spins (list[Spin] | None): Spins to retrieve. - - Returns: - NDArray[np.floating]: Array of data for specified bonds. - """ - interaction_indices = self.get_interaction_indices_by_properties(indices, centers, cells, orbitals, length) - - spins = spins or self.spins - spin_indices = [0 if spin == Spin.up else 1 for spin in spins] - - return self.data[np.ix_(interaction_indices, spin_indices)] - - def process_data_into_interactions(self) -> None: - """Populate each interaction dict with 'coxx' and 'icoxx' views. - - Assigns numpy views into `self.data` for each spin channel. - """ - spin_indices = {spin: i for i, spin in enumerate(self.spins)} - - for i, interaction in enumerate(self.interactions): - interaction["icoxx"] = {} - for spin, index in spin_indices.items(): - interaction["icoxx"][spin] = float(self.data[i, index]) - - -class ICOHPLIST(ICOXXLIST): - """Reader for ICOHPLIST.lobster files. - - Attributes: - icoxxlist_type (str): Type of ICOXXLIST file ("COHP"). - is_lcfo (bool): Whether the file is in LCFO format. - """ - - icoxxlist_type: ClassVar[str] = "COHP" - is_lcfo: ClassVar[bool] = False - - @classmethod - def get_default_filename(cls) -> str: - """Return the default filename for `ICOHPLIST`.""" - return "ICOHPLIST.LCFO.lobster" if cls.is_lcfo else "ICOHPLIST.lobster" - - -class ICOOPLIST(ICOXXLIST): - """Reader for ICOOPLIST.lobster files. - - Attributes: - icoxxlist_type (str): Type of ICOXXLIST file ("COOP"). - """ - - icoxxlist_type: ClassVar[str] = "COOP" - - @classmethod - def get_default_filename(cls) -> str: - """Return the default filename for `ICOOPLIST`.""" - return "ICOOPLIST.lobster" - - -class ICOBILIST(ICOXXLIST): - """Reader for ICOBILIST.lobster files. - - Attributes: - icoxxlist_type (str): Type of ICOXXLIST file ("COBI"). - is_lcfo (bool): Whether the file is in LCFO format. - """ - - icoxxlist_type: ClassVar[str] = "COBI" - is_lcfo: ClassVar[bool] = False - - @classmethod - def get_default_filename(cls) -> str: - """Return the default filename for ICOBILIST.""" - return "ICOBILIST.LCFO.lobster" if cls.is_lcfo else "ICOBILIST.lobster" - - -class ICOHPLIST_LCFO(ICOHPLIST): - """Reader for ICOHPLIST.LCFO.lobster files. - - Attributes: - is_lcfo (bool): Always True for LCFO format. - """ - - is_lcfo: ClassVar[bool] = True - - -class ICOBILIST_LCFO(ICOBILIST): - """Reader for ICOBILIST.LCFO.lobster files. - - Attributes: - is_lcfo (bool): Always True for LCFO format. - """ - - is_lcfo: ClassVar[bool] = True - - -class NcICOBILIST(LobsterInteractionsHolder): - """Reader for NcICOBILIST.lobster files. - - Parses non-conventional ICOBI interaction data. - - Attributes: - interactions (list): List of parsed interactions. - spins (list[Spin]): List of spins present in the file. - data (NDArray[np.floating]): Array of ICOXX values for each interaction and spin. - """ - - interactions_regex: ClassVar[str] = COXXCAR.interactions_regex - - @version_processor(min_version="5.1") - def parse_file(self) -> None: - """Parse the `NcICOBILIST` file. - - Extracts interaction data, including spin-resolved values, and populates - the `interactions`, `spins`, and `data` attributes. - - Raises: - ValueError: If the file contains invalid spin values or cannot parse - interaction lines. - """ - self.interactions = [] - self.spins = [] - - interaction_counter = 0 - - for line in self.iterate_lines(): - if not line: - continue - - if line.startswith(("COBI#", "for spin")): - interaction_counter = 0 - if match := re.search(r"(?i)for spin\s+(\d)", line): - spin_regex = int(match.group(1)) - else: - continue - - if spin_regex == 1: - self.spins.append(Spin.up) - elif spin_regex == 2: - self.spins.append(Spin.down) - else: - raise ValueError(f"Invalid spin value {spin_regex} in line: {line}") - else: - line = re.split(r"\s+(?![^\[]*\])", line) - - bond_tmp = defaultdict(list) - length = None - - index = int(line[0]) - - nc_icobi_value = float(line[-2]) - - for center in line[-1].split("->"): - if match := re.search(self.interactions_regex, center): - match = match.groups() - else: - raise ValueError(f"Could not parse interaction center line: {line}") - - bond_tmp["centers"].append(match[0]) - - if match[1]: - bond_tmp["cells"].append([int(x) for x in match[1].split()]) - else: - bond_tmp["cells"].append([]) - - bond_tmp["orbitals"].append(match[2]) - - if match[3]: - length = float(match[3]) - - current_spin = self.spins[-1] - if current_spin == Spin.up: - self.interactions.append( - { - "index": index, - "centers": bond_tmp["centers"], - "cells": bond_tmp["cells"], - "orbitals": bond_tmp["orbitals"], - "length": length, - "icoxx": { - Spin.up: nc_icobi_value, - }, - } - ) - elif current_spin == Spin.down: - interaction = self.interactions[interaction_counter] - if "icoxx" in interaction: - interaction["icoxx"][Spin.down] = nc_icobi_value - else: - raise ValueError(f"Invalid spin value {current_spin} in line: {line}") - - interaction_counter += 1 - - self.data = np.full((len(self.interactions), len(self.spins)), np.nan, dtype=np.float64) - - for i, interaction in enumerate(self.interactions): - if "icoxx" in interaction: - icoxx = interaction["icoxx"] - else: - raise ValueError(f"No ICOXX data found for interaction: {interaction}") - - if Spin.up in icoxx: - self.data[i, 0] = icoxx[Spin.up] - if Spin.down in icoxx: - self.data[i, 1] = icoxx[Spin.down] - - get_data_by_properties = ICOXXLIST.get_data_by_properties - - process_data_into_interactions = ICOXXLIST.process_data_into_interactions - - @classmethod - def get_default_filename(cls) -> str: - """Return the default filename for NcICOBILIST.""" - return "NcICOBILIST.lobster" diff --git a/src/pymatgen/io/lobster/future/outputs/lobsterout.py b/src/pymatgen/io/lobster/future/outputs/lobsterout.py deleted file mode 100644 index cadcb39dee..0000000000 --- a/src/pymatgen/io/lobster/future/outputs/lobsterout.py +++ /dev/null @@ -1,439 +0,0 @@ -from __future__ import annotations - -import re -from typing import TYPE_CHECKING -from warnings import warn - -from pymatgen.io.lobster.future.constants import LOBSTER_VERSION -from pymatgen.io.lobster.future.core import LobsterFile -from pymatgen.io.lobster.future.versioning import version_processor - -if TYPE_CHECKING: - from typing import Literal - - -class LobsterOut(LobsterFile): - """Parser for `lobsterout` file from LOBSTER. - - This class reads the `lobsterout` file and extracts information about - basis functions, spillings, warnings, timing, and file presence. It supports - parsing for different LOBSTER versions and provides attributes to access - the parsed data. - - Attributes: - basis_functions (list[str]): Basis functions used in the LOBSTER run. - basis_type (list[str]): Basis types used in the LOBSTER run. - charge_spilling (list[float]): Charge spilling for each spin channel. - dft_program (str): DFT program used for the calculation. - elements (list[str]): Elements present in the calculation. - has_charge (bool): Whether `CHARGE.lobster` is present. - has_cohpcar (bool): Whether `COHPCAR.lobster` and `ICOHPLIST.lobster` are present. - has_madelung (bool): Whether `SitePotentials.lobster` and `MadelungEnergies.lobster` are present. - has_coopcar (bool): Whether `COOPCAR.lobster` and `ICOOPLIST.lobster` are present. - has_cobicar (bool): Whether `COBICAR.lobster` and `ICOBILIST.lobster` are present. - has_doscar (bool): Whether `DOSCAR.lobster` is present. - has_doscar_lso (bool): Whether `DOSCAR.LSO.lobster` is present. - has_projection (bool): Whether `projectionData.lobster` is present. - has_bandoverlaps (bool): Whether `bandOverlaps.lobster` is present. - has_density_of_energies (bool): Whether `DensityOfEnergy.lobster` is present. - has_fatbands (bool): Whether fatband calculation was performed. - has_grosspopulation (bool): Whether `GROSSPOP.lobster` is present. - has_polarization (bool): Whether `POLARIZATION.lobster` is present. - info_lines (list[str]): Additional information on the run. - info_orthonormalization (list[str]): Information on orthonormalization. - is_restart_from_projection (bool): Whether calculation was restarted from a projection file. - lobster_version (str): The LOBSTER version. - number_of_spins (int): Number of spins. - number_of_threads (int): Number of threads used. - timing (dict[str, float]): Timing information. - total_spilling (list[float]): Total spilling for each spin channel. - warning_lines (list[str]): All warning messages. - """ - - @version_processor(min_version="5.1") - def _process_v5_1(self) -> None: - """Process `lobsterout` (version ≥ 5.1). - - This method extracts file presence flags and other attributes - specific to LOBSTER version 5.1 and later. - - Note: - This method is automatically invoked for files with version ≥5.1. - """ - lines = self.lines - - self.has_cohpcar = "writing COOPCAR.lobster..." in lines and "SKIPPING writing COOPCAR.lobster..." not in lines - self.has_coopcar = "writing COHPCAR.lobster..." in lines and "SKIPPING writing COHPCAR.lobster..." not in lines - self.has_cobicar = "writing COBICAR.lobster..." in lines and "SKIPPING writing COBICAR.lobster..." not in lines - - self._process_common() - - def _process_common(self) -> None: - """Process common parts of `lobsterout` for all versions. - - This method extracts general information such as timing, warnings, - basis functions, and file presence flags that are applicable across - all LOBSTER versions. - """ - lines = self.lines - - self.is_restart_from_projection = "loading projection from projectionData.lobster..." in lines - - self.has_error = "ERROR:" in lines - - if self.has_error: - self.error_lines = [line for line in lines if line.startswith("ERROR:")] - raise RuntimeError(f"LOBSTER calculation ended with errors:\n{self.error_lines}") - - self.number_of_threads = self._get_threads(lines) - self.dft_program = self._get_dft_program(lines) - - self.number_of_spins = self._get_number_of_spins(lines) - self.charge_spilling, self.total_spilling = self._get_spillings( - lines=lines, number_of_spins=self.number_of_spins - ) - - self.elements, self.basis_type, self.basis_functions = self._get_elements_basistype_basisfunctions(lines) - - wall_time, user_time, sys_time = self._get_timing(lines) - self.timing = { - "wall_time": wall_time, - "user_time": user_time, - "sys_time": sys_time, - } - - self.warning_lines = self._get_all_warning_lines(lines) - - self.info_orthonormalization = self._get_warning_orthonormalization(lines) - - self.info_lines = self._get_all_info_lines(lines) - - self.has_doscar = "writing DOSCAR.lobster..." in lines and "SKIPPING writing DOSCAR.lobster..." not in lines - self.has_doscar_lso = ( - "writing DOSCAR.LSO.lobster..." in lines and "SKIPPING writing DOSCAR.LSO.lobster..." not in lines - ) - - self.has_cobicar_lcfo = "writing COBICAR.LCFO.lobster..." in lines - self.has_cohpcar_lcfo = "writing COHPCAR.LCFO.lobster..." in lines - self.has_coopcar_lcfo = "writing COOPCAR.LCFO.lobster..." in lines - self.has_doscar_lcfo = "writing DOSCAR.LCFO.lobster..." in lines - self.has_polarization = "writing polarization to POLARIZATION.lobster..." in lines - self.has_charge = "SKIPPING writing CHARGE.lobster..." not in lines - self.has_projection = "saving projection to projectionData.lobster..." in lines - self.has_bandoverlaps = "WARNING: I dumped the band overlap matrices to the file bandOverlaps.lobster." in lines - self.has_fatbands = self._has_fatband(lines) - self.has_grosspopulation = "writing CHARGE.lobster and GROSSPOP.lobster..." in lines - self.has_density_of_energies = "writing DensityOfEnergy.lobster..." in lines - self.has_madelung = ( - "writing SitePotentials.lobster and MadelungEnergies.lobster..." in lines - and "skipping writing SitePotentials.lobster and MadelungEnergies.lobster..." not in lines - ) - self.has_mofecar = "Writing MOFECAR.lobster and IMOFELIST.lobster..." in lines - - @version_processor(max_version="5.0") - def _process_legacy(self) -> None: - """Process `lobsterout` for legacy versions (≤5.0). - - This method extracts file presence flags and other attributes - specific to LOBSTER versions ≤5.0. - - Note: - This method is automatically invoked for files with version ≤5.0. - """ - lines = self.lines - - self.has_cohpcar = ( - "writing COOPCAR.lobster and ICOOPLIST.lobster..." in lines - and "SKIPPING writing COOPCAR.lobster and ICOOPLIST.lobster..." not in lines - ) - self.has_coopcar = ( - "writing COHPCAR.lobster and ICOHPLIST.lobster..." in lines - and "SKIPPING writing COHPCAR.lobster and ICOHPLIST.lobster..." not in lines - ) - self.has_cobicar = ( - "writing COBICAR.lobster and ICOBILIST.lobster..." in lines - and "SKIPPING writing COBICAR.lobster and ICOBILIST.lobster..." not in lines - ) - - self._process_common() - - def process(self) -> None: - """Parse the `lobsterout` file and populate attributes. - - This method determines the LOBSTER version and invokes the appropriate - version-specific processing method. - - Raises: - RuntimeError: If the LOBSTER version cannot be determined. - """ - self.lobster_version = self.get_lobster_version(self.lines) - - super().process() - - @staticmethod - def get_lobster_version(lines: list[str]) -> str: - """Get the LOBSTER version from the `lobsterout` lines. - - Args: - lines (list[str]): Lines of the `lobsterout` file. - - Returns: - str: The LOBSTER version. - - Raises: - RuntimeError: If the version line is not found. - """ - for line in lines: - if version := re.search(r"(?i)LOBSTER\s(?:v(\d+\.\d+\.\d+))", line): - return version.group(1) - - warn( - f"Could not find LOBSTER version in lobsterout. Defaulting to v{LOBSTER_VERSION}", - stacklevel=2, - ) - - return LOBSTER_VERSION - - @staticmethod - def _has_fatband(lines: list[str]) -> bool: - """Check whether the calculation includes fatband data. - - Args: - lines (list[str]): Lines of the `lobsterout` file. - - Returns: - bool: True if fatband data is present, False otherwise. - """ - for line in lines: - line_parts = line.split() - if len(line_parts) > 1 and line_parts[1] == "FatBand": - return True - return False - - @staticmethod - def _get_dft_program(lines: list[str]) -> str | None: - """Get the DFT program used for the calculation. - - Args: - lines (list[str]): Lines of the `lobsterout` file. - - Returns: - str | None: The name of the DFT program, or None if not found. - """ - for line in lines: - line_parts = line.split() - if len(line_parts) > 4 and line_parts[3] == "program...": - return line_parts[4] - return None - - @staticmethod - def _get_number_of_spins(lines: list[str]) -> Literal[1, 2]: - """Get the number of spin channels. - - Args: - lines (list[str]): Lines of the `lobsterout` file. - - Returns: - Literal[1, 2]: 1 if single spin channel, 2 if two spin channels. - """ - return 2 if "spillings for spin channel 2" in lines else 1 - - @staticmethod - def _get_threads(lines: list[str]) -> int: - """Get the number of CPU threads used. - - Args: - lines (list[str]): Lines of the `lobsterout` file. - - Returns: - int: Number of threads. - - Raises: - ValueError: If the number of threads cannot be determined. - """ - for line in lines: - line_parts = line.split() - if len(line_parts) > 11 and line_parts[11] in {"threads", "thread"}: - return int(line_parts[10]) - raise ValueError("Threads not found.") - - @staticmethod - def _get_spillings( - lines: list[str], - number_of_spins: Literal[1, 2], - ) -> tuple[list[float], list[float]]: - """Get charge spillings and total spillings. - - Args: - lines (list[str]): Lines of the `lobsterout` file. - number_of_spins (Literal[1, 2]): Number of spin channels. - - Returns: - tuple[list[float], list[float]]: Charge spillings and total spillings - for each spin channel. - """ - charge_spillings = [] - total_spillings = [] - for line in lines: - line_parts = line.split() - if len(line_parts) > 2 and line_parts[2] == "spilling:": - if line_parts[1] == "charge": - charge_spillings.append(float(line_parts[3].replace("%", "")) / 100.0) - elif line_parts[1] == "total": - total_spillings.append(float(line_parts[3].replace("%", "")) / 100.0) - - if len(charge_spillings) == number_of_spins and len(total_spillings) == number_of_spins: - break - - return charge_spillings, total_spillings - - @staticmethod - def _get_elements_basistype_basisfunctions( - lines: list[str], - ) -> tuple[list[str], list[str], list[list[str]]]: - """Get elements, basis types, and basis functions. - - Args: - lines (list[str]): Lines of the `lobsterout` file. - - Returns: - tuple[list[str], list[str], list[list[str]]]: Elements, basis types, - and basis functions used in the calculation. - """ - begin = False - end = False - elements: list[str] = [] - basistypes: list[str] = [] - basisfunctions: list[list[str]] = [] - for line in lines: - if begin and not end: - line_parts = line.split() - if line_parts[0] not in { - "INFO:", - "WARNING:", - "setting", - "calculating", - "post-processing", - "saving", - "spillings", - "writing", - }: - elements.append(line_parts[0]) - basistypes.append(line_parts[1].replace("(", "").replace(")", "")) - # Last sign is '' - basisfunctions.append(line_parts[2:]) - else: - end = True - - if "setting up local basis functions..." in line: - begin = True - return elements, basistypes, basisfunctions - - @staticmethod - def _get_timing( - lines: list[str], - ) -> tuple[dict[str, str], dict[str, str], dict[str, str]]: - """Get wall time, user time, and system time. - - Args: - lines (list[str]): Lines of the `lobsterout` file. - - Returns: - tuple[dict[str, str], dict[str, str], dict[str, str]]: Dictionaries - containing timing information for wall, user, and system times. - """ - begin = False - user_times, wall_times, sys_times = [], [], [] - - for line in lines: - line_parts = line.split() - if "finished" in line_parts: - begin = True - if begin: - if "wall" in line_parts: - wall_times = line_parts[2:10] - if "user" in line_parts: - user_times = line_parts[:8] - if "sys" in line_parts: - sys_times = line_parts[:8] - - wall_time_dict = { - "h": wall_times[0], - "min": wall_times[2], - "s": wall_times[4], - "ms": wall_times[6], - } - user_time_dict = { - "h": user_times[0], - "min": user_times[2], - "s": user_times[4], - "ms": user_times[6], - } - sys_time_dict = { - "h": sys_times[0], - "min": sys_times[2], - "s": sys_times[4], - "ms": sys_times[6], - } - - return wall_time_dict, user_time_dict, sys_time_dict - - @staticmethod - def _get_warning_orthonormalization(lines: list[str]) -> list[str]: - """Get orthonormalization warnings. - - Args: - lines (list[str]): Lines of the `lobsterout` file. - - Returns: - list[str]: List of orthonormalization warnings. - """ - orthowarnings = [] - for line in lines: - line_parts = line.split() - if "orthonormalized" in line_parts: - orthowarnings.append(" ".join(line_parts[1:])) - return orthowarnings - - @staticmethod - def _get_all_warning_lines(lines: list[str]) -> list[str]: - """Get all warning lines. - - Args: - lines (list[str]): Lines of the `lobsterout` file. - - Returns: - list[str]: List of warning messages. - """ - warnings_ = [] - for line in lines: - line_parts = line.split() - if len(line_parts) > 0 and line_parts[0] == "WARNING:": - warnings_.append(" ".join(line_parts[1:])) - return warnings_ - - @staticmethod - def _get_all_info_lines(lines: list[str]) -> list[str]: - """Get all informational lines. - - Args: - lines (list[str]): Lines of the `lobsterout` file. - - Returns: - list[str]: List of informational messages. - """ - infos = [] - for line in lines: - line_parts = line.split() - if len(line_parts) > 0 and line_parts[0] == "INFO:": - infos.append(" ".join(line_parts[1:])) - return infos - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for `LobsterOut`. - - Returns: - str: The default filename. - """ - return "lobsterout" diff --git a/src/pymatgen/io/lobster/future/outputs/misc.py b/src/pymatgen/io/lobster/future/outputs/misc.py deleted file mode 100644 index 0979c4f7ce..0000000000 --- a/src/pymatgen/io/lobster/future/outputs/misc.py +++ /dev/null @@ -1,670 +0,0 @@ -from __future__ import annotations - -import itertools -import re -from itertools import islice -from typing import TYPE_CHECKING, Any, ClassVar, Self - -import numpy as np - -from pymatgen.electronic_structure.core import Orbital, Spin -from pymatgen.io.lobster.future.constants import LOBSTER_ORBITALS -from pymatgen.io.lobster.future.core import LobsterFile -from pymatgen.io.lobster.future.utils import parse_orbital_from_text -from pymatgen.io.lobster.future.versioning import version_processor -from pymatgen.io.vasp.outputs import VolumetricData - -if TYPE_CHECKING: - from typing import Literal - - from numpy import floating - - from pymatgen.core.structure import Structure - from pymatgen.io.lobster.future.types import LobsterMatrixData - from pymatgen.util.typing import PathLike - - -class Wavefunction(LobsterFile): - """Parser for wave function files from LOBSTER. - - Reads wave function files and creates VolumetricData objects. - - Attributes: - grid (tuple[int, int, int]): Grid for the wave function [Nx+1, Ny+1, Nz+1]. - points (list[tuple[float, float, float]]): Points in real space. - reals (list[float]): Real parts of the wave function. - imaginaries (list[float]): Imaginary parts of the wave function. - distances (list[float]): Distances to the first point in the wave function file. - structure (Structure): Structure object associated with the calculation. - """ - - def __init__( - self, - filename: PathLike, - structure: Structure, - process_immediately: bool = True, - lobster_version: str | None = None, - ) -> None: - """Initialize the Wavefunction parser. - - Args: - filename (PathLike): The wavecar file from LOBSTER. - structure (Structure): The Structure object. - process_immediately (bool): Whether to parse the file immediately. Defaults to True. - """ - super().__init__( - filename, - process_immediately=process_immediately, - lobster_version=lobster_version, - ) - - self.structure = structure - - @version_processor() - def parse_file( - self, - ) -> None: - """Parse wave function file. - - Reads the wave function file and extracts grid, points, real and imaginary parts, - and distances. - - Raises: - ValueError: If the number of real or imaginary parts does not match the expected grid size. - """ - lines_generator = self.iterate_lines() - - line_parts = next(lines_generator).split() - - self.grid: tuple[int, int, int] = [ - int(line_parts[7]), - int(line_parts[8]), - int(line_parts[9]), - ] - n_points = self.grid[0] * self.grid[1] * self.grid[2] - - self.points = np.empty((n_points, 3), dtype=np.float64) - self.distances = np.empty(n_points, dtype=np.float64) - self.reals = np.empty(n_points, dtype=np.float64) - self.imaginaries = np.empty(n_points, dtype=np.float64) - - i = 0 - for line in lines_generator: - line_parts = line.split() - - if len(line_parts) >= 6: - self.points[i] = ( - float(line_parts[0]), - float(line_parts[1]), - float(line_parts[2]), - ) - self.distances[i] = float(line_parts[3]) - self.reals[i] = float(line_parts[4]) - self.imaginaries[i] = float(line_parts[5]) - i += 1 - - if ( - len(self.reals) != self.grid[0] * self.grid[1] * self.grid[2] - or len(self.imaginaries) != self.grid[0] * self.grid[1] * self.grid[2] - ): - raise ValueError("Something went wrong while reading the file") - - def set_volumetric_data(self, grid: tuple[int, int, int], structure: Structure) -> None: - """Create VolumetricData instances for real, imaginary, and density parts. - - Args: - grid (tuple[int, int, int]): Grid on which wavefunction was calculated. - structure (Structure): Structure object. - - Raises: - ValueError: If the wavefunction file does not contain all relevant points. - """ - Nx = grid[0] - 1 - Ny = grid[1] - 1 - Nz = grid[2] - 1 - a = structure.lattice.matrix[0] - b = structure.lattice.matrix[1] - c = structure.lattice.matrix[2] - new_x = [] - new_y = [] - new_z = [] - new_real = [] - new_imaginary = [] - new_density = [] - - for runner, (x, y, z) in enumerate(itertools.product(range(Nx + 1), range(Ny + 1), range(Nz + 1))): - x_here = x / float(Nx) * a[0] + y / float(Ny) * b[0] + z / float(Nz) * c[0] - y_here = x / float(Nx) * a[1] + y / float(Ny) * b[1] + z / float(Nz) * c[1] - z_here = x / float(Nx) * a[2] + y / float(Ny) * b[2] + z / float(Nz) * c[2] - - if x != Nx and y != Ny and z != Nz: - if ( - not np.isclose(self.points[runner][0], x_here, 1e-3) - and not np.isclose(self.points[runner][1], y_here, 1e-3) - and not np.isclose(self.points[runner][2], z_here, 1e-3) - ): - raise ValueError( - "The provided wavefunction from Lobster does not contain all relevant" - " points. " - "Please use a line similar to: printLCAORealSpaceWavefunction kpoint 1 " - "coordinates 0.0 0.0 0.0 coordinates 1.0 1.0 1.0 box bandlist 1 " - ) - - new_x.append(x_here) - new_y.append(y_here) - new_z.append(z_here) - - new_real.append(self.reals[runner]) - new_imaginary.append(self.imaginaries[runner]) - new_density.append(self.reals[runner] ** 2 + self.imaginaries[runner] ** 2) - - self.final_real = np.reshape(new_real, [Nx, Ny, Nz]) - self.final_imaginary = np.reshape(new_imaginary, [Nx, Ny, Nz]) - self.final_density = np.reshape(new_density, [Nx, Ny, Nz]) - - self.volumetricdata_real = VolumetricData(structure, {"total": self.final_real}) - self.volumetricdata_imaginary = VolumetricData(structure, {"total": self.final_imaginary}) - self.volumetricdata_density = VolumetricData(structure, {"total": self.final_density}) - - def get_volumetricdata_real(self) -> VolumetricData: - """Get VolumetricData object for the real part of the wave function. - - Returns: - VolumetricData: Real part volumetric data. - """ - if not hasattr(self, "volumetricdata_real"): - self.set_volumetric_data(self.grid, self.structure) - return self.volumetricdata_real - - def get_volumetricdata_imaginary(self) -> VolumetricData: - """Get VolumetricData object for the imaginary part of the wave function. - - Returns: - VolumetricData: Imaginary part volumetric data. - """ - if not hasattr(self, "volumetricdata_imaginary"): - self.set_volumetric_data(self.grid, self.structure) - return self.volumetricdata_imaginary - - def get_volumetricdata_density(self) -> VolumetricData: - """Get VolumetricData object for the density part of the wave function. - - Returns: - VolumetricData: Density volumetric data. - """ - if not hasattr(self, "volumetricdata_density"): - self.set_volumetric_data(self.grid, self.structure) - return self.volumetricdata_density - - def write_file( - self, - filename: PathLike = "WAVECAR.vasp", - part: Literal["real", "imaginary", "density"] = "real", - ) -> None: - """Save the wave function in a file readable by VESTA. - - Args: - filename (PathLike): Output file name. Defaults to "WAVECAR.vasp". - part (Literal["real", "imaginary", "density"]): Which part to save. Defaults to "real". - - Raises: - ValueError: If the specified part is not "real", "imaginary", or "density". - """ - if not ( - hasattr(self, "volumetricdata_real") - and hasattr(self, "volumetricdata_imaginary") - and hasattr(self, "volumetricdata_density") - ): - self.set_volumetric_data(self.grid, self.structure) - - if part == "real": - self.volumetricdata_real.write_file(filename) - elif part == "imaginary": - self.volumetricdata_imaginary.write_file(filename) - elif part == "density": - self.volumetricdata_density.write_file(filename) - else: - raise ValueError('part can be only "real" or "imaginary" or "density"') - - @classmethod - def from_dict(cls, d: dict[str, Any]) -> Self: - """""" - instance = super().from_dict(d) - - instance.points = np.asarray(instance.points, dtype=np.float64) - instance.distances = np.asarray(instance.distances, dtype=np.float64) - instance.reals = np.asarray(instance.reals, dtype=np.float64) - instance.imaginaries = np.asarray(instance.imaginaries, dtype=np.float64) - - return instance - - -class MadelungEnergies(LobsterFile): - """Parser for MadelungEnergies.lobster files. - - Attributes: - madelung_energies_mulliken (float): Madelung energy (Mulliken). - madelung_energies_loewdin (float): Madelung energy (Loewdin). - ewald_splitting (float): Ewald splitting parameter. - """ - - @version_processor() - def parse_file(self) -> None: - """Parse MadelungEnergies.lobster file. - - Extracts the Ewald splitting parameter and Madelung energies. - - Returns: - None - """ - line = self.lines[5] - - line_parts = line.split() - - self.ewald_splitting = float(line_parts[0]) - self.madelung_energies_mulliken = float(line_parts[1]) - self.madelung_energies_loewdin = float(line_parts[2]) - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for MadelungEnergies. - - Returns: - str: Default filename. - """ - return "MadelungEnergies.lobster" - - -class SitePotentials(LobsterFile): - """Parser for SitePotentials.lobster files. - - Attributes: - centers (list[str]): Atom centers. - site_potentials_mulliken (list[float]): Mulliken site potentials. - site_potentials_loewdin (list[float]): Loewdin site potentials. - madelung_energies_mulliken (float): Madelung energy (Mulliken). - madelung_energies_loewdin (float): Madelung energy (Loewdin). - ewald_splitting (float): Ewald splitting parameter. - """ - - @version_processor() - def parse_file(self) -> None: - """Parse SitePotentials.lobster file. - - Extracts site potentials, Madelung energies, and Ewald splitting parameter. - - Returns: - None - """ - self.centers = [] - self.site_potentials_mulliken = [] - self.site_potentials_loewdin = [] - - for line in self.iterate_lines(): - if ewald_splitting := re.search(r"splitting parameter\s+(\S+)", line): - self.ewald_splitting = float(ewald_splitting.group(1)) - - if madelung_energies := re.search(r"Madelung Energy \(eV\)\s*(\S+)\s+(\S+)", line): - self.madelung_energies_mulliken = float(madelung_energies.group(1)) - self.madelung_energies_loewdin = float(madelung_energies.group(2)) - - if data := re.search(r"(\d+)\s+([a-zA-Z]{1,2})\s+(\S+)\s+(\S+)", line): - data = data.groups() - self.centers.append(data[1] + data[0]) - self.site_potentials_mulliken.append(float(data[2])) - self.site_potentials_loewdin.append(float(data[3])) - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for SitePotentials. - - Returns: - str: Default filename. - """ - return "SitePotentials.lobster" - - -def get_orb_from_str(orbs: list[str]) -> tuple[str, list[tuple[int, Orbital]]]: - """Get Orbitals from string representations. - - Args: - orbs (list[str]): Orbitals, e.g. ["2p_x", "3s"]. - - Returns: - tuple[str, list[tuple[int, Orbital]]]: Orbital label and list of orbitals. - """ - orbitals = [(int(orb[0]), Orbital(LOBSTER_ORBITALS.index(orb[1:]))) for orb in orbs] - - orb_label = "" - for iorb, orbital in enumerate(orbitals): - if iorb == 0: - orb_label += f"{orbital[0]}{orbital[1].name}" - else: - orb_label += f"-{orbital[0]}{orbital[1].name}" - - return orb_label, orbitals - - -class LobsterMatrices(LobsterFile): - """Parser for LOBSTER matrix files. - - Attributes: - matrix_type (str): Type of matrix (hamilton, coefficient, transfer, overlap). - centers (list[str]): Atom centers. - orbitals (list[str]): Orbitals. - matrices (LobsterMatrixData): Matrix data for each k-point and spin. - efermi (float): Fermi energy (for Hamilton matrices). - """ - - matrix_types: ClassVar[set[str]] = { - "hamilton", - "coefficient", - "transfer", - "overlap", - } - - def __init__( - self, - filename: PathLike | None = None, - matrix_type: str | None = None, - efermi: float | None = None, - process_immediately: bool = True, - lobster_version: str | None = None, - ) -> None: - """Initialize LOBSTER matrices parser. - - Args: - filename: Path to the matrix file - matrix_type: Type of matrix. If None, inferred from filename - efermi: Fermi level in eV (required for Hamilton matrices) - process_immediately: Whether to parse the file immediately - """ - super().__init__( - filename=filename, - process_immediately=False, - lobster_version=lobster_version, - ) - - self.efermi = efermi - - self.matrix_type = matrix_type or self.get_matrix_type() - - self.centers: list[str] = [] - self.orbitals: list[str] = [] - self.matrices: LobsterMatrixData = {} - - if self.matrix_type == "hamilton" and self.efermi is None: - raise ValueError("Fermi energy (eV) required for Hamilton matrices") - - if process_immediately: - self.parse_file() - - def get_matrix_type(self) -> str: - """Infer matrix type from filename. - - Returns: - str: Matrix type. - """ - name = str(self.filename).lower() - - for matrix_type in self.matrix_types: - if matrix_type in name: - return matrix_type - - raise ValueError(f"Cannot infer matrix type from filename: {self.filename}") - - @version_processor() - def parse_file(self) -> None: - """Parse matrix data and set instance attributes. - - Returns: - None - """ - header_regex_pattern = r"kpoint\s+(\d+)" if self.matrix_type == "overlap" else r"(\d+)\s+kpoint\s+(\d+)" - - current_kpoint, current_spin = None, None - multiplier = 1 - - lines_generator = self.iterate_lines() - for line in lines_generator: - if header_match := re.search(header_regex_pattern, line): - header_match = header_match.groups() - if self.matrix_type != "overlap": - current_spin = Spin.up if header_match[0] == "1" else Spin.down - - current_kpoint = header_match[-1] - elif "real parts" in line.lower(): - multiplier = 1 - elif "imag parts" in line.lower(): - multiplier = 1j - elif line.startswith("basisfunction"): - num_parts = len(re.findall(r"band\s+\d+", line)) if "band" in line else len(line.split()[1:]) - - if current_kpoint not in self.matrices: - if current_kpoint is None: - raise ValueError("Could not read any k-point before matrix data.") - - self.matrices[current_kpoint] = {current_spin: np.zeros((num_parts, num_parts), dtype=complex)} - elif current_spin not in self.matrices[current_kpoint]: - self.matrices[current_kpoint][current_spin] = np.zeros((num_parts, num_parts), dtype=complex) - - values = [] - for _ in range(num_parts): - line_split = next(lines_generator).split() - - values.append([float(val) * multiplier for val in line_split[1:]]) - - if len(self.centers) != num_parts and len(self.orbitals) != num_parts: - self.centers.append(line_split[0].split("_")[0].title()) - orbital = parse_orbital_from_text(line_split[0]) - - if orbital is None: - raise ValueError( - f"Could not read orbital format: {line_split[0]} when parsing header line: {line}" - ) - - self.orbitals.append(orbital) - - self.matrices[current_kpoint][current_spin] += np.array(values, dtype=complex) - - def get_onsite_values(self, center: str | None = None, orbital: str | None = None) -> dict | float | floating: - """Get onsite values for specific centers/orbitals. - - Args: - center (str | None): Specific center or None for all. - orbital (str | None): Specific orbital or None for all. - - Returns: - dict | float | floating: Dict of values or single value if both specified. - """ - results = {} - - energy_shift = self.efermi if self.matrix_type == "hamilton" else 0 - - for i, (c, o) in enumerate(zip(self.centers, self.orbitals, strict=True)): - if (center is None or c == center) and (orbital is None or o == orbital): - values = [m[i, i].real - energy_shift for kpoint in self.matrices.values() for m in kpoint.values()] - avg_value = np.mean(values) - - if center and orbital: - return avg_value - - results[f"{c}_{o}"] = avg_value - - return results - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for the LobsterMatrices class. - - Returns: - str: Default filename. - """ - return "hamiltonMatrices.lobster" - - def as_dict(self) -> dict[str, Any]: - """Serialize object to a dictionary. - - Returns: - dict[str, Any]: Dictionary representation of the object. - """ - dictionary = super().as_dict() - - for kpoint in dictionary["matrices"]: - for spin in dictionary["matrices"][kpoint]: - matrix_data = dictionary["matrices"][kpoint][spin] - dictionary["matrices"][kpoint][spin] = { - "real": matrix_data.real, - "imag": matrix_data.imag, - } - - return dictionary - - @classmethod - def from_dict(cls, d: dict[str, Any]) -> Self: - """Deserialize from dictionary.""" - instance = super().from_dict(d) - - for kpoint in instance.matrices: - for spin in instance.matrices[kpoint]: - matrix_data = instance.matrices[kpoint][spin] - instance.matrices[kpoint][spin] = np.asarray(matrix_data["real"]) + 1j * np.asarray(matrix_data["imag"]) - - return instance - - -class POLARIZATION(LobsterFile): - """Parser for POLARIZATION.lobster file. - - Attributes: - rel_mulliken_pol_vector (dict[str, float]): Relative Mulliken polarization vector. - rel_loewdin_pol_vector (dict[str, float]): Relative Loewdin polarization vector. - """ - - @version_processor() - def parse_file(self) -> None: - """Parse POLARIZATION.lobster file. - - Returns: - None - """ - self.rel_mulliken_pol_vector = {} - self.rel_loewdin_pol_vector = {} - - for line in islice(self.iterate_lines(), 3, None): - cleanlines = [idx for idx in line.split(" ") if idx != ""] - if cleanlines and len(cleanlines) == 3: - self.rel_mulliken_pol_vector[cleanlines[0]] = float(cleanlines[1]) - self.rel_loewdin_pol_vector[cleanlines[0]] = float(cleanlines[2]) - if cleanlines and len(cleanlines) == 4: - self.rel_mulliken_pol_vector[cleanlines[0].replace(":", "")] = cleanlines[1].replace("\u03bc", "u") - self.rel_loewdin_pol_vector[cleanlines[2].replace(":", "")] = cleanlines[3].replace("\u03bc", "u") - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for the Polarization class. - - Returns: - str: Default filename. - """ - return "POLARIZATION.lobster" - - -class BWDF(LobsterFile): - """Parser for BWDF.lobster/BWDFCOHP.lobster files. - - Attributes: - centers (NDArray): Bond length centers for the distribution. - bwdf (dict[Literal[1, -1], NDArray]): Bond weighted distribution function. - bin_width (float): Bin width used for computing the distribution by LOBSTER. - """ - - is_cohp: ClassVar[bool] = False - - def __init__( - self, - filename: PathLike | None = None, - process_immediately: bool = True, - lobster_version: str | None = None, - ) -> None: - """ - Args: - filename (PathLike): The BWDF file from LOBSTER, typically "BWDF.lobster" - or "BWDFCOHP.lobster". - """ - self.bwdf = {} - self.centers = np.array([]) - self.data = np.array([[]]) - - super().__init__( - filename=filename, - process_immediately=process_immediately, - lobster_version=lobster_version, - ) - - @version_processor() - def parse_file(self) -> None: - """Parse BWDF.lobster/BWDFCOHP.lobster file. - - Returns: - None - """ - self.bwdf = {} - self.data = np.genfromtxt(self.iterate_lines(), dtype=float, skip_header=1) - - self.process_data_into_bwdf_centers() - - def process_data_into_bwdf_centers(self) -> None: - """Process data into bwdf and centers. - - Returns: - None - """ - self.centers = self.data[:, 0] - self.bwdf[Spin.up] = self.data[:, 1] - - if self.data.shape[1] > 2: - self.bwdf[Spin.down] = self.data[:, 2] - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for the BWDF class. - - Returns: - str: Default filename. - """ - return "BWDF.lobster" - - @classmethod - def from_dict(cls, d: dict[str, Any]) -> Self: - """Deserialize object from dictionary produced by `as_dict`. - - Args: - d (dict[str, Any]): Dictionary representation of the object. - - Returns: - Self: Deserialized BWDF object. - """ - instance = super().from_dict(d) - - instance.data = np.asarray(instance.data, dtype=np.float64) - instance.process_data_into_bwdf_centers() - - return instance - - -class BWDFCOHP(BWDF): - """Parser for BWDFCOHP.lobster files. - - Returns: - None - """ - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for the BWDFCOHP class. - - Returns: - str: Default filename. - """ - return "BWDFCOHP.lobster" diff --git a/src/pymatgen/io/lobster/future/outputs/populations.py b/src/pymatgen/io/lobster/future/outputs/populations.py deleted file mode 100644 index 16610a1423..0000000000 --- a/src/pymatgen/io/lobster/future/outputs/populations.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Classes for parsing LOBSTER population analysis output files. - -Provides classes to read and parse population analysis files generated by LOBSTER, -such as CHARGE.lobster, CHARGE.LCFO.lobster, GROSSPOP.lobster, and GROSSPOP.LCFO.lobster. -Extracts Mulliken and Loewdin charges and gross populations for further analysis. -""" - -from __future__ import annotations - -import re -from typing import TYPE_CHECKING - -from pymatgen.electronic_structure.core import Spin -from pymatgen.io.lobster.future.core import LobsterFile -from pymatgen.io.lobster.future.versioning import version_processor - -if TYPE_CHECKING: - from typing import ClassVar - - from pymatgen.io.lobster.future.types import LobsterPopulations - - -class CHARGE(LobsterFile): - """Parser for CHARGE.lobster and CHARGE.LCFO.lobster files. - - Attributes: - is_lcfo (bool): Whether the CHARGE file is from LCFO analysis. - centers (list[str]): Atom labels with indices. - mulliken (list[float]): Mulliken charges for each atom. - loewdin (list[float]): Loewdin charges for each atom. - """ - - charge_regex: ClassVar[str] = r"(\d+)\s+([a-zA-Z]+)\s+(\S+)\s+(\S+)" - is_lcfo: ClassVar[bool] = False - - @version_processor() - def parse_file(self) -> None: - """Parse the CHARGE file and extract Mulliken and Loewdin charges. - - Populates the `centers`, `mulliken`, and `loewdin` attributes with data - extracted from the file. - - Returns: - None - """ - self.centers = [] - self.mulliken = [] - self.loewdin = [] - - for line in self.iterate_lines(): - if data := re.search(self.charge_regex, line): - data = data.groups() - self.centers.append(data[1] + data[0]) - - if self.is_lcfo: - self.loewdin.append(float(data[2])) - else: - self.mulliken.append(float(data[2])) - self.loewdin.append(float(data[3])) - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for CHARGE files. - - Returns: - str: Default filename for the CHARGE file, depending on LCFO mode. - """ - return "CHARGE.lobster" if not cls.is_lcfo else "CHARGE.LCFO.lobster" - - -class CHARGE_LCFO(CHARGE): - """Parser for CHARGE.LCFO.lobster files. - - Attributes: - is_lcfo (bool): Whether the CHARGE file is from LCFO analysis. - """ - - charge_regex: ClassVar[str] = r"(\d+)\s+([a-zA-Z]+)\s+(\S+)" - is_lcfo: ClassVar[bool] = True - - -class GROSSPOP(LobsterFile): - """Parser for GROSSPOP.lobster and GROSSPOP.LCFO.lobster files. - - Attributes: - is_lcfo (bool): Whether the GROSSPOP file is from LCFO analysis. - populations (LobsterPopulations): Population dictionaries for each atom. - spins (list[Spin]): List of spins present in the file. - """ - - is_lcfo: ClassVar[bool] = False - - @version_processor() - def parse_file(self) -> None: - """Parse the GROSSPOP file and extract gross population data. - - Populates the `populations` attribute with a nested dictionary structure - containing Mulliken and Loewdin populations for each atom and orbital. - """ - self.populations: LobsterPopulations = {} - self.spins = [Spin.up] - - keys = [] - - current_atom = "" - for line in self.iterate_lines(): - mulliken_key = re.findall(r"(?i)Mulliken GP", line) - loewdin_key = re.findall(r"(?i)Loewdin GP", line) - - if mulliken_key: - keys.append("mulliken") - if loewdin_key: - keys.append("loewdin") - - if len(loewdin_key) > 1 or len(mulliken_key) > 1: - self.spins.append(Spin.down) - - if "total" in line.lower(): - pass - elif data := re.search( - r"(?:(\d+)\s+)?(?:([a-zA-Z]+)\s+)?(\S+)\s+([0-9\.]+)\s+([0-9\.]+)(?:\s+)?([0-9\.]+)?(?:\s+)?([0-9\.]+)?", - line, - ): - groups = data.groups() - populations = {} - if groups[0] is not None and groups[1] is not None: - current_atom = groups[1] + groups[0] - self.populations[current_atom] = {} - if groups[2] is not None: - self.populations[current_atom][groups[2]] = populations - else: - continue - - for i, spin in enumerate(self.spins): - populations[spin] = {} - for j, key in enumerate(keys): - populations[spin][key] = float(groups[3 + i + j * len(self.spins)]) - - @classmethod - def get_default_filename(cls) -> str: - """Get the default filename for GROSSPOP files. - - Returns: - str: Default filename for the GROSSPOP file, depending on LCFO mode. - """ - return "GROSSPOP.lobster" if not cls.is_lcfo else "GROSSPOP.LCFO.lobster" - - -class GROSSPOP_LCFO(GROSSPOP): - """Parser for GROSSPOP.LCFO.lobster files. - - Attributes: - is_lcfo (bool): Whether the GROSSPOP file is in LCFO format. - populations (LobsterPopulations): Population dictionaries for each atom. - """ - - is_lcfo: ClassVar[bool] = True diff --git a/src/pymatgen/io/lobster/future/types.py b/src/pymatgen/io/lobster/future/types.py deleted file mode 100644 index 65fd89cac1..0000000000 --- a/src/pymatgen/io/lobster/future/types.py +++ /dev/null @@ -1,96 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Literal, TypedDict - -from numpy import complexfloating -from numpy.typing import NDArray - -from pymatgen.electronic_structure.core import Spin - -if TYPE_CHECKING: - from typing import TypeAlias - - from numpy import floating, integer - - -class LobsterInteraction(TypedDict): - """Dictionary representing a chemical interaction in LOBSTER. - - This dictionary stores information about a specific interaction between atoms in a structure. - - Attributes: - index (int): The index of the interaction. - centers (list[str]): List of strings representing the centers of the interaction (e.g., "Fe1", "O2"). - cells (list[list[int]] | NDArray[integer]): List of lists of integers representing the cells of the interaction - (e.g., [0, 0, 0]). - orbitals (list[str | None]): List of strings representing the orbitals involved in the interaction - (e.g., "2s", "2p_x"). - length (float | None): The length of the interaction, representing the distance between the centers. - """ - - index: int - centers: list[str] - cells: list[list[int]] | NDArray[integer] - orbitals: list[str | None] - length: float | None - - -class LobsterInteractionData(LobsterInteraction, total=False): - """Dictionary representing a chemical interaction in LOBSTER with additional COXX/ICOXX data. - - Extends `LobsterInteraction` by adding COXX and ICOXX values for each spin. - - Attributes: - coxx (dict[Spin, NDArray[floating] | float]): COXX values for each spin. - icoxx (dict[Spin, NDArray[floating] | float]): ICOXX values for each spin. - """ - - coxx: dict[Spin, NDArray[floating] | float] - icoxx: dict[Spin, NDArray[floating] | float] - - -LobsterPopulations: TypeAlias = dict[str, dict[str, dict[Spin, dict[Literal["mulliken", "loewdin"], float]]]] - -LobsterMatrixData: TypeAlias = dict[str, dict[Spin | None, NDArray[complexfloating]]] - - -class LobsterBandOverlaps(TypedDict): - """Dictionary representing band overlaps in LOBSTER. - - Attributes: - k_points (dict[Spin, list[list[float]]]): List of k-points for each spin. - matrices (dict[Spin, list[NDArray[floating]]]): List of matrices for each spin. - max_deviations (dict[Spin, list[float]]): List of maximal deviations for each spin. - """ - - k_points: dict[Spin, list[list[float]]] - matrices: dict[Spin, list[NDArray[floating]]] - max_deviations: dict[Spin, list[float]] - - -class LobsterFatband(TypedDict): - """Dictionary representing fatband data in LOBSTER. - - Attributes: - center (str): Atom associated with the fatband. - orbital (str): Orbital associated with the fatband. - energies (dict[Spin, NDArray[floating]]): Energies at each k-point for each spin. - projections (dict[Spin, NDArray[floating]]): Weights/projections at each k-point for each spin. - """ - - center: str - orbital: str - energies: dict[Spin, NDArray[floating]] - projections: dict[Spin, NDArray[floating]] - - -class LobsterFatbands(TypedDict): - """Dictionary representing multiple fatbands in LOBSTER. - - Attributes: - k_points (list[list[float]]): List of k-points. - bands (list[LobsterFatband]): List of fatband dictionaries. - """ - - k_points: list[list[float]] - bands: list[LobsterFatband] diff --git a/src/pymatgen/io/lobster/future/utils.py b/src/pymatgen/io/lobster/future/utils.py deleted file mode 100644 index 42e19bed67..0000000000 --- a/src/pymatgen/io/lobster/future/utils.py +++ /dev/null @@ -1,124 +0,0 @@ -from __future__ import annotations - -import re -from typing import Any - -from pymatgen.electronic_structure.core import Spin -from pymatgen.io.lobster.future.constants import LOBSTER_ORBITALS - - -def natural_sort(list_to_sort: str) -> list[Any]: - """Sort a list of strings in human order. - - This function sorts strings in a way that humans would expect, - taking into account numerical values within the strings. - - Args: - list_to_sort (str): List of strings to sort. - - Returns: - list[Any]: Sorted list of strings in human order. - - Example: - >>> natural_sort(["file10", "file2", "file1"]) - ['file1', 'file2', 'file10'] - """ - return [int(text) if text.isdigit() else text.lower() for text in re.split(r"(\d+)", list_to_sort)] - - -def parse_orbital_from_text(text: str) -> str | None: - """Parse the orbital from a text string of an ICOXXLIST file. - - This function extracts the orbital information from a given text string. - It checks for valid orbital patterns and returns the matched orbital. - - Args: - text (str): Text string to parse the orbital from. - - Returns: - str | None: Parsed orbital string if a valid orbital is found, - otherwise None. - - Example: - >>> parse_orbital_from_text("1s_2p_x") - '2p_x' - """ - parts = text.split("_") - - if len(parts) == 1: - return None - - for orbital in LOBSTER_ORBITALS: - if match := re.search(rf"\d+{re.escape(orbital)}", "_".join(parts[-2:])): - return match.group(0) - - return parts[-1] if re.match(r"\d+[a-z]+", parts[-1]) else None - - -def convert_spin_keys(obj: Any) -> Any: - """Convert an object to a JSON-compatible format recursively. - - This function ensures that the input object is converted into a format - that can be serialized into JSON. It handles lists, tuples, dictionaries, - and enums. - - Args: - obj (Any): Input object to convert. - - Returns: - Any: JSON-compatible representation of the input object. - - Example: - >>> make_json_compatible({"key": Enum("Example", "value")}) - {'key': 'value'} - """ - if isinstance(obj, (list, tuple)): - return [convert_spin_keys(item) for item in obj] - - if isinstance(obj, dict): - new_dict = {} - - for k, v in obj.items(): - new_key = f"@Spin({k})" if isinstance(k, Spin) else k - new_dict[new_key] = convert_spin_keys(v) - - return new_dict - - return obj - - -def restore_spin_keys(obj: Any) -> Any: - """Restore Spin enum keys from JSON-serialized format. - - This function recursively processes an object and converts string keys - "1" and "-1" back to Spin.up and Spin.down enum values. It handles - nested lists, tuples, and dictionaries. - - Args: - obj (Any): Input object with potential Spin keys as strings. - - Returns: - Any: Object with Spin enum keys restored. - - Example: - >>> restore_spin_keys({"1": [1, 2], "-1": [3, 4]}) - {: [1, 2], : [3, 4]} - """ - if isinstance(obj, (list, tuple)): - return type(obj)(restore_spin_keys(item) for item in obj) - - if isinstance(obj, dict): - new_dict = {} - for k, v in obj.items(): - if k == "@Spin(1)": - new_key = Spin.up - elif k == "@Spin(-1)": - new_key = Spin.down - else: - new_key = k - - new_dict[new_key] = restore_spin_keys(v) - - return new_dict - - return obj diff --git a/src/pymatgen/io/lobster/future/versioning.py b/src/pymatgen/io/lobster/future/versioning.py deleted file mode 100644 index 111cc07657..0000000000 --- a/src/pymatgen/io/lobster/future/versioning.py +++ /dev/null @@ -1,25 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable -from typing import TypeVar - -F = TypeVar("F", bound=Callable) - - -def version_processor(min_version: str = "0.0", max_version: str | None = None) -> Callable[[F], F]: - """Decorator to mark a method as a version processor. - - Args: - min_version (str): Minimum version for which the processor is valid. - max_version (str | None): Maximum version for which the processor is valid. - - Returns: - Callable[[F], F]: Decorator for versioned processor methods. - """ - - def decorator(func: F) -> F: - setattr(func, "version_info", (min_version, max_version)) # NOQA: B010 - - return func - - return decorator From 664fa302d12be489c9fe9307b2670cfb870144c1 Mon Sep 17 00:00:00 2001 From: naik-aakash Date: Mon, 9 Mar 2026 16:22:00 +0100 Subject: [PATCH 179/180] remove exisiting tests --- tests/io/lobster/future/__init__.py | 0 tests/io/lobster/future/test_inputs.py | 639 -------- tests/io/lobster/future/test_outputs.py | 1917 ----------------------- 3 files changed, 2556 deletions(-) delete mode 100644 tests/io/lobster/future/__init__.py delete mode 100644 tests/io/lobster/future/test_inputs.py delete mode 100644 tests/io/lobster/future/test_outputs.py diff --git a/tests/io/lobster/future/__init__.py b/tests/io/lobster/future/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/io/lobster/future/test_inputs.py b/tests/io/lobster/future/test_inputs.py deleted file mode 100644 index 1be64369e2..0000000000 --- a/tests/io/lobster/future/test_inputs.py +++ /dev/null @@ -1,639 +0,0 @@ -from __future__ import annotations - -import numpy as np -import pytest -from pytest import approx - -from pymatgen.core.structure import Structure -from pymatgen.io.lobster.future import LobsterIn -from pymatgen.io.lobster.future.inputs import get_all_possible_basis_combinations -from pymatgen.io.vasp.inputs import Incar, Kpoints, Potcar -from pymatgen.util.testing import FAKE_POTCAR_DIR, TEST_FILES_DIR, VASP_IN_DIR, VASP_OUT_DIR, MatSciTest - -TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp" - -__author__ = "Janine George, Marco Esters" -__copyright__ = "Copyright 2017, The Materials Project" -__version__ = "0.2" -__email__ = "janine.george@uclouvain.be, esters@uoregon.edu" -__date__ = "Dec 10, 2017" - - -class TestLobsterIn(MatSciTest): - def setup_method(self): - self.LobsterIn = LobsterIn.from_file(f"{TEST_DIR}/lobsterin.1") - self.LobsterIn2 = LobsterIn.from_file(f"{TEST_DIR}/lobsterin.2") - self.LobsterIn3 = LobsterIn.from_file(f"{TEST_DIR}/lobsterin.3") - self.LobsterIn4 = LobsterIn.from_file(f"{TEST_DIR}/lobsterin.4.gz") - - def test_from_file(self): - # Test reading from file - assert self.LobsterIn["cohpstartenergy"] == approx(-15.0) - assert self.LobsterIn["cohpendenergy"] == approx(5.0) - assert self.LobsterIn["basisset"] == "pbeVaspFit2015" - assert self.LobsterIn["gaussiansmearingwidth"] == approx(0.1) - assert self.LobsterIn["basisfunctions"][0] == "Fe 3d 4p 4s" - assert self.LobsterIn["basisfunctions"][1] == "Co 3d 4p 4s" - assert self.LobsterIn["skipdos"] - assert self.LobsterIn["skipcohp"] - assert self.LobsterIn["skipcoop"] - assert self.LobsterIn["skippopulationanalysis"] - assert self.LobsterIn["skipgrosspopulation"] - - # Test if comments are correctly removed - assert self.LobsterIn == self.LobsterIn2 - - def test_duplicates_from_file(self): - with open(f"{TEST_DIR}/lobsterin.1", encoding="utf-8") as file: - original_file = file.readlines() - - # String and float keywords does not allow duplicates - float_dup_file = original_file.copy() - float_dup_file.append("cohpstartenergy -15.0") - - float_tmp_file = self.tmp_path / "tmp_lobster_in_float" - float_tmp_file.write_text("\n".join(float_dup_file)) - - with pytest.raises(ValueError, match="Same keyword cohpstartenergy twice!"): - _ = LobsterIn.from_file(float_tmp_file) - - # Boolean and list keywords allow duplicates - bool_dup_file = original_file.copy() - bool_dup_file.append("skipdos") - - bool_tmp_file = self.tmp_path / "tmp_lobster_in_bool" - bool_tmp_file.write_text("\n".join(bool_dup_file)) - - _ = LobsterIn.from_file(bool_tmp_file) # no error should be raised - - def test_magic_methods(self): - """Test __getitem__, __setitem__ and __contains__, - should be case independent. - """ - # Test __setitem__ - assert self.LobsterIn["COHPSTARTENERGY"] == approx(-15.0) - - with pytest.raises(KeyError, match="Key hello is currently not available"): - self.LobsterIn["HELLO"] = True - - # Test __getitem__ - self.LobsterIn["skipCOHP"] = False - assert self.LobsterIn["skipcohp"] is False - assert self.LobsterIn.get("skipCOHP") is False - - with pytest.raises(KeyError, match="key='World' is not available"): - _ = self.LobsterIn["World"] - - # Test __contains__ - assert "COHPSTARTENERGY" in self.LobsterIn - assert "cohpstartenergy" in self.LobsterIn - - assert "helloworld" not in self.LobsterIn - - def test_initialize_from_dict(self): - # initialize from dict - lobsterin = LobsterIn( - { - "cohpstartenergy": -15.0, - "cohpendenergy": 5.0, - "basisset": "pbeVaspFit2015", - "gaussiansmearingwidth": 0.1, - "basisfunctions": ["Fe 3d 4p 4s", "Co 3d 4p 4s"], - "skipdos": True, - "skipcohp": True, - "skipcoop": True, - "skippopulationanalysis": True, - "skipgrosspopulation": True, - } - ) - assert lobsterin["cohpstartenergy"] == approx(-15.0) - assert lobsterin["cohpendenergy"] == approx(5.0) - assert lobsterin["basisset"] == "pbeVaspFit2015" - assert lobsterin["gaussiansmearingwidth"] == approx(0.1) - assert lobsterin["basisfunctions"][0] == "Fe 3d 4p 4s" - assert lobsterin["basisfunctions"][1] == "Co 3d 4p 4s" - assert {*lobsterin} >= { - "skipdos", - "skipcohp", - "skipcoop", - "skippopulationanalysis", - "skipgrosspopulation", - } - with pytest.raises(KeyError, match="There are duplicates for the keywords!"): - lobsterin2 = LobsterIn({"cohpstartenergy": -15.0, "cohpstartEnergy": -20.0}) - lobsterin2 = LobsterIn({"cohpstartenergy": -15.0}) - # can only calculate nbands if basis functions are provided - with pytest.raises( - ValueError, - match="No basis functions are provided. The program cannot calculate nbands.", - ): - lobsterin2._get_nbands(structure=Structure.from_file(f"{VASP_IN_DIR}/POSCAR_Fe3O4")) - - def test_standard_settings(self): - # test standard settings - for option in [ - "standard", - "standard_from_projection", - "standard_with_fatband", - "onlyprojection", - "onlydos", - "onlycohp", - "onlycoop", - "onlycobi", - "onlycohpcoop", - "onlycohpcoopcobi", - ]: - lobsterin1 = LobsterIn.standard_calculations_from_vasp_files( - f"{VASP_IN_DIR}/POSCAR_Fe3O4", - f"{VASP_IN_DIR}/INCAR.lobster", - f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz", - option=option, - ) - assert lobsterin1["cohpstartenergy"] == approx(-35.0) - assert lobsterin1["cohpendenergy"] == approx(5.0) - assert lobsterin1["basisset"] == "pbeVaspFit2015" - assert lobsterin1["gaussiansmearingwidth"] == approx(0.1) - assert lobsterin1["basisfunctions"][0] == "Fe 3d 4p 4s " - assert lobsterin1["basisfunctions"][1] == "O 2p 2s " - - if option in [ - "standard", - "standard_with_fatband", - "onlyprojection", - "onlycohp", - "onlycoop", - "onlycohpcoop", - ]: - assert lobsterin1["saveProjectiontoFile"] - if option in [ - "standard", - "standard_with_fatband", - "onlycohp", - "onlycoop", - "onlycohpcoop", - ]: - assert lobsterin1["cohpGenerator"] == "from 0.1 to 6.0 orbitalwise" - if option == "standard": - assert "skipdos" not in lobsterin1 - assert "skipcohp" not in lobsterin1 - assert "skipcoop" not in lobsterin1 - if option == "standard_with_fatband": - assert lobsterin1["createFatband"] == ["Fe 3d 4p 4s ", "O 2p 2s "] - assert "skipdos" not in lobsterin1 - assert "skipcohp" not in lobsterin1 - assert "skipcoop" not in lobsterin1 - if option == "standard_from_projection": - assert lobsterin1["loadProjectionFromFile"] - if option in [ - "onlyprojection", - "onlycohp", - "onlycoop", - "onlycobi", - "onlycohpcoop", - "onlycohpcoopcobi", - ]: - assert lobsterin1["skipdos"] - assert lobsterin1["skipPopulationAnalysis"] - assert lobsterin1["skipGrossPopulation"] - assert lobsterin1["skipMadelungEnergy"] - - if option == "onlydos": - assert lobsterin1["skipPopulationAnalysis"] - assert lobsterin1["skipGrossPopulation"] - assert lobsterin1["skipcohp"] - assert lobsterin1["skipcoop"] - assert lobsterin1["skipcobi"] - assert lobsterin1["skipMadelungEnergy"] - if option == "onlycohp": - assert lobsterin1["skipcoop"] - assert lobsterin1["skipcobi"] - if option == "onlycoop": - assert lobsterin1["skipcohp"] - assert lobsterin1["skipcobi"] - if option == "onlyprojection": - assert lobsterin1["skipdos"] - if option == "onlymadelung": - assert lobsterin1["skipPopulationAnalysis"] - assert lobsterin1["skipGrossPopulation"] - assert lobsterin1["skipcohp"] - assert lobsterin1["skipcoop"] - assert lobsterin1["skipcobi"] - assert lobsterin1["skipdos"] - # test basis functions by dict - lobsterin_new = LobsterIn.standard_calculations_from_vasp_files( - f"{VASP_IN_DIR}/POSCAR_Fe3O4", - f"{VASP_IN_DIR}/INCAR.lobster", - dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, - option="standard", - ) - assert lobsterin_new["basisfunctions"] == ["Fe 3d 4p 4s", "O 2s 2p"] - - # test gaussian smearing - lobsterin_new = LobsterIn.standard_calculations_from_vasp_files( - f"{VASP_IN_DIR}/POSCAR_Fe3O4", - f"{VASP_IN_DIR}/INCAR.lobster2", - dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, - option="standard", - ) - assert "gaussiansmearingwidth" not in lobsterin_new - - # fatband and ISMEAR=-5 does not work together - with pytest.raises( - ValueError, - match="ISMEAR has to be 0 for a fatband calculation with Lobster", - ): - lobsterin_new = LobsterIn.standard_calculations_from_vasp_files( - f"{VASP_IN_DIR}/POSCAR_Fe3O4", - f"{VASP_IN_DIR}/INCAR.lobster2", - dict_for_basis={"Fe": "3d 4p 4s", "O": "2s 2p"}, - option="standard_with_fatband", - ) - - def test_standard_with_energy_range_from_vasprun(self): - # test standard_with_energy_range_from_vasprun - lobsterin_comp = LobsterIn.standard_calculations_from_vasp_files( - f"{VASP_IN_DIR}/POSCAR_C2", - f"{VASP_IN_DIR}/INCAR_C2", - f"{VASP_IN_DIR}/POTCAR_C2.gz", - f"{VASP_OUT_DIR}/vasprun.C2.xml.gz", - option="standard_with_energy_range_from_vasprun", - ) - assert lobsterin_comp["COHPstartEnergy"] == approx(-28.3679) - assert lobsterin_comp["COHPendEnergy"] == approx(32.8968) - assert lobsterin_comp["COHPSteps"] == 301 - - def test_diff(self): - # test diff - assert self.LobsterIn.diff(self.LobsterIn2)["Different"] == {} - assert self.LobsterIn.diff(self.LobsterIn2)["Same"]["cohpstartenergy"] == approx(-15.0) - - # test diff in both directions - for entry in self.LobsterIn.diff(self.LobsterIn3)["Same"]: - assert entry in self.LobsterIn3.diff(self.LobsterIn)["Same"] - for entry in self.LobsterIn3.diff(self.LobsterIn)["Same"]: - assert entry in self.LobsterIn.diff(self.LobsterIn3)["Same"] - for entry in self.LobsterIn.diff(self.LobsterIn3)["Different"]: - assert entry in self.LobsterIn3.diff(self.LobsterIn)["Different"] - for entry in self.LobsterIn3.diff(self.LobsterIn)["Different"]: - assert entry in self.LobsterIn.diff(self.LobsterIn3)["Different"] - - assert ( - self.LobsterIn.diff(self.LobsterIn3)["Different"]["skipcohp"]["lobsterin1"] - == self.LobsterIn3.diff(self.LobsterIn)["Different"]["skipcohp"]["lobsterin2"] - ) - - def test_diff_case_insensitivity(self): - """Test case-insensitivity of diff method.""" - with open(f"{TEST_DIR}/lobsterin.1", encoding="utf-8") as file: - lobsterin_content = file.read() - - lobsterin_content.replace("COHPstartEnergy -15.0", "cohpSTARTEnergy -15.0") - lobsterin_content.replace("skipcohp", "skipCOHP") - - tmp_file_path = self.tmp_path / "tmp_lobster_in" - tmp_file_path.write_text(lobsterin_content) - - lobsterin_diff_case = LobsterIn.from_file(tmp_file_path) - assert self.LobsterIn.diff(lobsterin_diff_case)["Different"] == {} - - def test_dict_functionality(self): - for key in ("COHPstartEnergy", "COHPstartEnergy", "COhPstartenergy"): - start_energy = self.LobsterIn.get(key) - assert start_energy == approx(-15.0), f"{start_energy=}, {key=}" - - lobsterin_copy = self.LobsterIn.copy() - lobsterin_copy.update({"cohpstarteNergy": -10.00}) - assert lobsterin_copy["cohpstartenergy"] == approx(-10.0) - lobsterin_copy.pop("cohpstarteNergy") - assert "cohpstartenergy" not in lobsterin_copy - lobsterin_copy.pop("cohpendenergY") - lobsterin_copy["cohpsteps"] = 100 - assert lobsterin_copy["cohpsteps"] == 100 - len_before = len(lobsterin_copy.items()) - assert len_before == 9, f"{len_before=}" - - lobsterin_copy.popitem() - len_after = len(lobsterin_copy.items()) - assert len_after == len_before - 1 - - # Test case sensitivity of |= operator - self.LobsterIn |= {"skipCOHP": True} # Camel case - assert self.LobsterIn["skipcohp"] is True - - self.LobsterIn |= {"skipcohp": False} # lower case - assert self.LobsterIn["skipcohp"] is False - - def test_read_write_lobsterin(self): - outfile_path = self.tmp_path / "lobsterin_test" - lobsterin1 = LobsterIn.from_file(f"{TEST_DIR}/lobsterin.1") - lobsterin1.write_lobsterin(outfile_path) - lobsterin2 = LobsterIn.from_file(outfile_path) - assert lobsterin1.diff(lobsterin2)["Different"] == {} - - def test_get_basis(self): - # get basis functions - lobsterin1 = LobsterIn({}) - potcar = Potcar.from_file(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") - potcar_names = [name["symbol"] for name in potcar.spec] - - assert lobsterin1.get_basis( - Structure.from_file(f"{TEST_FILES_DIR}/cif/Fe3O4.cif"), - potcar_symbols=potcar_names, - ) == ["Fe 3d 4p 4s ", "O 2p 2s "] - potcar = Potcar.from_file(f"{TEST_DIR}/POTCAR.GaAs") - potcar_names = [name["symbol"] for name in potcar.spec] - assert lobsterin1.get_basis( - Structure.from_file(f"{TEST_DIR}/POSCAR.GaAs"), - potcar_symbols=potcar_names, - ) == ["Ga 3d 4p 4s ", "As 4p 4s "] - - def test_get_all_possible_basis_functions(self): - potcar = Potcar.from_file(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") - potcar_names = [name["symbol"] for name in potcar.spec] - result = LobsterIn.get_all_possible_basis_functions( - Structure.from_file(f"{TEST_FILES_DIR}/cif/Fe3O4.cif"), - potcar_symbols=potcar_names, - ) - assert result[0] == {"Fe": "3d 4s", "O": "2p 2s"} - assert result[1] == {"Fe": "3d 4s 4p", "O": "2p 2s"} - - potcar2 = Potcar.from_file(f"{FAKE_POTCAR_DIR}/POT_GGA_PAW_PBE_54/POTCAR.Fe.gz") - Potcar_names2 = [name["symbol"] for name in potcar2.spec] - result2 = LobsterIn.get_all_possible_basis_functions( - Structure.from_file(f"{TEST_FILES_DIR}/cif/Fe.cif"), - potcar_symbols=Potcar_names2, - ) - assert result2[0] == {"Fe": "3d 4s"} - - def test_get_potcar_symbols(self): - lobsterin1 = LobsterIn({}) - assert lobsterin1._get_potcar_symbols(f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz") == [ - "Fe", - "O", - ] - assert lobsterin1._get_potcar_symbols(f"{TEST_DIR}/POTCAR.GaAs") == [ - "Ga_d", - "As", - ] - - def test_write_lobsterin(self): - # write lobsterin, read it and compare it - outfile_path = self.tmp_path / "lobsterin_test" - lobsterin1 = LobsterIn.standard_calculations_from_vasp_files( - f"{VASP_IN_DIR}/POSCAR_Fe3O4", - f"{VASP_IN_DIR}/INCAR.lobster", - f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz", - option="standard", - ) - lobsterin1.write_lobsterin(outfile_path) - lobsterin2 = LobsterIn.from_file(outfile_path) - assert lobsterin1.diff(lobsterin2)["Different"] == {} - - def test_write_incar(self): - # write INCAR and compare - outfile_path = self.tmp_path / "INCAR_test" - lobsterin1 = LobsterIn.standard_calculations_from_vasp_files( - f"{VASP_IN_DIR}/POSCAR_Fe3O4", - f"{VASP_IN_DIR}/INCAR.lobster", - f"{VASP_IN_DIR}/POTCAR_Fe3O4.gz", - option="standard", - ) - lobsterin1.write_INCAR( - f"{VASP_IN_DIR}/INCAR.lobster3", - outfile_path, - f"{VASP_IN_DIR}/POSCAR_Fe3O4", - isym=-1, - ) - - incar1 = Incar.from_file(f"{VASP_IN_DIR}/INCAR.lobster3") - incar2 = Incar.from_file(outfile_path) - - assert incar1.diff(incar2)["Different"] == { - "ISYM": {"INCAR1": 2, "INCAR2": -1}, - "NBANDS": {"INCAR1": None, "INCAR2": 86}, - "NSW": {"INCAR1": 500, "INCAR2": 0}, - "LWAVE": {"INCAR1": False, "INCAR2": True}, - } - - def test_write_kpoints(self): - # line mode - outfile_path = self.tmp_path / "KPOINTS_test" - outfile_path2 = self.tmp_path / "POSCAR_test" - lobsterin1 = LobsterIn({}) - # test writing primitive cell - lobsterin1.write_POSCAR_with_standard_primitive( - POSCAR_input=f"{VASP_IN_DIR}/POSCAR_Fe3O4", POSCAR_output=outfile_path2 - ) - - lobsterin1.write_KPOINTS( - POSCAR_input=outfile_path2, - KPOINTS_output=outfile_path, - kpoints_line_density=58, - isym=-1, - ) - kpoint = Kpoints.from_file(outfile_path) - assert kpoint.num_kpts == 562 - assert kpoint.kpts[-1][0] == approx(-0.5) - assert kpoint.kpts[-1][1] == approx(0.5) - assert kpoint.kpts[-1][2] == approx(0.5) - assert kpoint.labels[-1] == "T" - kpoint2 = Kpoints.from_file(f"{VASP_IN_DIR}/KPOINTS_band.lobster") - - labels = [] - number = 0 - for label in kpoint.labels: - if label is not None: - if number != 0: - if label != labels[number - 1]: - labels.append(label) - number += 1 - else: - labels.append(label) - number += 1 - - labels2 = [] - number2 = 0 - for label in kpoint2.labels: - if label is not None: - if number2 != 0: - if label != labels2[number2 - 1]: - labels2.append(label) - number2 += 1 - else: - labels2.append(label) - number2 += 1 - assert labels == labels2 - - # without line mode - lobsterin1.write_KPOINTS( - POSCAR_input=outfile_path2, - KPOINTS_output=outfile_path, - line_mode=False, - isym=-1, - ) - kpoint = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(f"{VASP_OUT_DIR}/IBZKPT.lobster") - - for num_kpt, list_kpoint in enumerate(kpoint.kpts): - assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) - assert list_kpoint[1] == approx(kpoint2.kpts[num_kpt][1]) - assert list_kpoint[2] == approx(kpoint2.kpts[num_kpt][2]) - - assert kpoint.num_kpts == 108 - - # without line mode, use grid instead of reciprocal density - lobsterin1.write_KPOINTS( - POSCAR_input=outfile_path2, - KPOINTS_output=outfile_path, - line_mode=False, - from_grid=True, - input_grid=[6, 6, 3], - isym=-1, - ) - kpoint = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(f"{VASP_OUT_DIR}/IBZKPT.lobster") - - for num_kpt, list_kpoint in enumerate(kpoint.kpts): - assert list_kpoint[0] == approx(kpoint2.kpts[num_kpt][0]) - assert list_kpoint[1] == approx(kpoint2.kpts[num_kpt][1]) - assert list_kpoint[2] == approx(kpoint2.kpts[num_kpt][2]) - - assert kpoint.num_kpts == 108 - - # - # #without line mode, using a certain grid, isym=0 instead of -1 - lobsterin1.write_KPOINTS( - POSCAR_input=f"{TEST_DIR}/POSCAR.Li", - KPOINTS_output=outfile_path, - line_mode=False, - from_grid=True, - input_grid=[3, 3, 3], - isym=0, - ) - - kpoint1 = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(f"{TEST_DIR}/IBZKPT_3_3_3_Li") - for ikpoint, kpoint in enumerate(kpoint1.kpts): - assert self.is_kpoint_in_list( - kpoint, - kpoint2.kpts, - kpoint1.kpts_weights[ikpoint], - kpoint2.kpts_weights, - ) - for ikpoint, kpoint in enumerate(kpoint2.kpts): - assert self.is_kpoint_in_list( - kpoint, - kpoint1.kpts, - kpoint2.kpts_weights[ikpoint], - kpoint1.kpts_weights, - ) - - lobsterin1.write_KPOINTS( - POSCAR_input=f"{TEST_DIR}/POSCAR.Li", - KPOINTS_output=outfile_path, - line_mode=False, - from_grid=True, - input_grid=[2, 2, 2], - isym=0, - ) - - kpoint1 = Kpoints.from_file(outfile_path) - kpoint2 = Kpoints.from_file(f"{TEST_DIR}/IBZKPT_2_2_2_Li") - for ikpoint, kpoint in enumerate(kpoint1.kpts): - assert self.is_kpoint_in_list( - kpoint, - kpoint2.kpts, - kpoint1.kpts_weights[ikpoint], - kpoint2.kpts_weights, - ) - for ikpoint, kpoint in enumerate(kpoint2.kpts): - assert self.is_kpoint_in_list( - kpoint, - kpoint1.kpts, - kpoint2.kpts_weights[ikpoint], - kpoint1.kpts_weights, - ) - - def is_kpoint_in_list(self, kpoint, kpointlist, weight, weightlist) -> bool: - found = 0 - for ikpoint2, kpoint2 in enumerate(kpointlist): - if ( - np.isclose(kpoint[0], kpoint2[0]) - and np.isclose(kpoint[1], kpoint2[1]) - and np.isclose(kpoint[2], kpoint2[2]) - ): - if weight == weightlist[ikpoint2]: - found += 1 - elif ( - np.isclose(-kpoint[0], kpoint2[0]) - and np.isclose(-kpoint[1], kpoint2[1]) - and np.isclose(-kpoint[2], kpoint2[2]) - ) and weight == weightlist[ikpoint2]: - found += 1 - return found == 1 - - def test_as_from_dict(self): - # tests as dict and from dict methods - new_lobsterin = LobsterIn.from_dict(self.LobsterIn.as_dict()) - assert new_lobsterin == self.LobsterIn - new_lobsterin.to_json() - - -class TestUtils(MatSciTest): - def test_get_all_possible_basis_combinations(self): - # this basis is just for testing (not correct) - min_basis = ["Li 1s 2s ", "Na 1s 2s", "Si 1s 2s"] - max_basis = ["Li 1s 2p 2s ", "Na 1s 2p 2s", "Si 1s 2s"] - combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) - assert combinations_basis == [ - ["Li 1s 2s", "Na 1s 2s", "Si 1s 2s"], - ["Li 1s 2s", "Na 1s 2s 2p", "Si 1s 2s"], - ["Li 1s 2s 2p", "Na 1s 2s", "Si 1s 2s"], - ["Li 1s 2s 2p", "Na 1s 2s 2p", "Si 1s 2s"], - ] - - min_basis = ["Li 1s 2s"] - max_basis = ["Li 1s 2s 2p 3s"] - combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) - assert combinations_basis == [ - ["Li 1s 2s"], - ["Li 1s 2s 2p"], - ["Li 1s 2s 3s"], - ["Li 1s 2s 2p 3s"], - ] - - min_basis = ["Li 1s 2s", "Na 1s 2s"] - max_basis = ["Li 1s 2s 2p 3s", "Na 1s 2s 2p 3s"] - combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) - assert combinations_basis == [ - ["Li 1s 2s", "Na 1s 2s"], - ["Li 1s 2s", "Na 1s 2s 2p"], - ["Li 1s 2s", "Na 1s 2s 3s"], - ["Li 1s 2s", "Na 1s 2s 2p 3s"], - ["Li 1s 2s 2p", "Na 1s 2s"], - ["Li 1s 2s 2p", "Na 1s 2s 2p"], - ["Li 1s 2s 2p", "Na 1s 2s 3s"], - ["Li 1s 2s 2p", "Na 1s 2s 2p 3s"], - ["Li 1s 2s 3s", "Na 1s 2s"], - ["Li 1s 2s 3s", "Na 1s 2s 2p"], - ["Li 1s 2s 3s", "Na 1s 2s 3s"], - ["Li 1s 2s 3s", "Na 1s 2s 2p 3s"], - ["Li 1s 2s 2p 3s", "Na 1s 2s"], - ["Li 1s 2s 2p 3s", "Na 1s 2s 2p"], - ["Li 1s 2s 2p 3s", "Na 1s 2s 3s"], - ["Li 1s 2s 2p 3s", "Na 1s 2s 2p 3s"], - ] - - min_basis = ["Si 1s 2s 2p", "Na 1s 2s"] - max_basis = ["Si 1s 2s 2p 3s", "Na 1s 2s 2p 3s"] - combinations_basis = get_all_possible_basis_combinations(min_basis, max_basis) - assert combinations_basis == [ - ["Si 1s 2s 2p", "Na 1s 2s"], - ["Si 1s 2s 2p", "Na 1s 2s 2p"], - ["Si 1s 2s 2p", "Na 1s 2s 3s"], - ["Si 1s 2s 2p", "Na 1s 2s 2p 3s"], - ["Si 1s 2s 2p 3s", "Na 1s 2s"], - ["Si 1s 2s 2p 3s", "Na 1s 2s 2p"], - ["Si 1s 2s 2p 3s", "Na 1s 2s 3s"], - ["Si 1s 2s 2p 3s", "Na 1s 2s 2p 3s"], - ] diff --git a/tests/io/lobster/future/test_outputs.py b/tests/io/lobster/future/test_outputs.py deleted file mode 100644 index c66e1cf829..0000000000 --- a/tests/io/lobster/future/test_outputs.py +++ /dev/null @@ -1,1917 +0,0 @@ -from __future__ import annotations - -import copy -import json -import os -from typing import TYPE_CHECKING - -import numpy as np -import pytest -from monty.json import MontyEncoder, jsanitize -from numpy.testing import assert_allclose, assert_array_equal -from pytest import approx - -from pymatgen.core.structure import Structure -from pymatgen.electronic_structure.core import Spin -from pymatgen.io.lobster.future import ( - BWDF, - CHARGE, - CHARGE_LCFO, - COBICAR, - COBICAR_LCFO, - COOPCAR, - DOSCAR, - DOSCAR_LCFO, - GROSSPOP, - GROSSPOP_LCFO, - ICOBILIST, - ICOHPLIST, - ICOHPLIST_LCFO, - ICOOPLIST, - POLARIZATION, - BandOverlaps, - Fatband, - Fatbands, - LobsterMatrices, - LobsterOut, - MadelungEnergies, - NcICOBILIST, - SitePotentials, - Wavefunction, -) -from pymatgen.io.vasp import Vasprun -from pymatgen.util.testing import TEST_FILES_DIR, VASP_OUT_DIR, MatSciTest - -if TYPE_CHECKING: - from monty.json import MSONable - -TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp" - -__author__ = "Janine George, Marco Esters" -__copyright__ = "Copyright 2017, The Materials Project" -__version__ = "0.2" -__email__ = "janine.george@uclouvain.be, esters@uoregon.edu" -__date__ = "Dec 10, 2017" - - -class TestBWDF(MatSciTest): - """Test BWDF and BWDFCOHP classes.""" - - def setup_method(self): - self.bwdf_coop = BWDF(filename=f"{TEST_DIR}/BWDF.lobster.AlN.gz") - self.bwdf_cohp = BWDF(filename=f"{TEST_DIR}/BWDFCOHP.lobster.NaCl.gz") - - def test_attributes(self): - assert len(self.bwdf_coop.centers) == 201 - assert len(self.bwdf_cohp.centers) == 143 - assert self.bwdf_coop.bwdf[Spin.down][-2] == approx(0.00082, abs=1e-4) - assert self.bwdf_coop.bwdf[Spin.up][0] == approx(0.81161, abs=1e-4) - assert self.bwdf_cohp.bwdf[Spin.up][103] == approx(-0.01392, abs=1e-4) - - -class TestDOSCAR(MatSciTest): - def setup_method(self): - doscar = f"{VASP_OUT_DIR}/DOSCAR.lobster.spin" - doscar2 = f"{VASP_OUT_DIR}/DOSCAR.lobster.nonspin" - doscar3 = f"{VASP_OUT_DIR}/DOSCAR.LCFO.lobster.AlN" - - self.doscar_spin_pol = DOSCAR(filename=doscar) - self.doscar_nonspin_pol = DOSCAR(filename=doscar2) - self.doscar_lcfo = DOSCAR_LCFO(filename=doscar3) - self.doscar_spin_pol2 = DOSCAR(filename=doscar) - - def test_pdos(self): - """Test projected densities of states (PDOS) from DOSCAR files.""" - expected_pdos_spin = { - "2s": { - Spin.up: [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069], - Spin.down: [0.00000, 0.00159, 0.00000, 0.00011, 0.00000, 0.00069], - }, - "2p_y": { - Spin.up: [0.00000, 0.00160, 0.00000, 0.25801, 0.00000, 0.00029], - Spin.down: [0.00000, 0.00161, 0.00000, 0.25819, 0.00000, 0.00029], - }, - "2p_z": { - Spin.up: [0.00000, 0.00161, 0.00000, 0.25823, 0.00000, 0.00029], - Spin.down: [0.00000, 0.00160, 0.00000, 0.25795, 0.00000, 0.00029], - }, - "2p_x": { - Spin.up: [0.00000, 0.00160, 0.00000, 0.25805, 0.00000, 0.00029], - Spin.down: [0.00000, 0.00161, 0.00000, 0.25814, 0.00000, 0.00029], - }, - } - - expected_pdos_nonspin = { - "2s": [0.00000, 0.00320, 0.00000, 0.00017, 0.00000, 0.00060], - "2p_y": [0.00000, 0.00322, 0.00000, 0.51635, 0.00000, 0.00037], - "2p_z": [0.00000, 0.00322, 0.00000, 0.51636, 0.00000, 0.00037], - "2p_x": [0.00000, 0.00322, 0.00000, 0.51634, 0.00000, 0.00037], - } - - for orbital, spin_data in expected_pdos_spin.items(): - for spin, expected_values in spin_data.items(): - assert_allclose( - self.doscar_spin_pol.projected_dos["F1"][orbital].densities[spin], - expected_values, - ) - - for orbital, expected_values in expected_pdos_nonspin.items(): - assert_allclose( - self.doscar_nonspin_pol.projected_dos["F1"][orbital].densities[Spin.up], - expected_values, - ) - - pdos_1a1_AlN = [ - 0.0, - 0.22594, - 0.01335, - 0.0, - 0.0, - 0.0, - 0.00228, - 0.02836, - 0.03053, - 0.01612, - 0.02379, - ] - pdos_3py_Al = [ - 0.0, - 0.02794, - 0.00069, - 0.0, - 0.0, - 0.0, - 0.00216, - 0.0682, - 0.06966, - 0.04402, - 0.16579, - ] - pdos_2s_N = [ - 0.0, - 0.25324, - 0.0157, - 0.0, - 0.0, - 0.0, - 0.0006, - 0.01747, - 0.02247, - 0.01589, - 0.03565, - ] - - assert self.doscar_lcfo.is_lcfo - assert_allclose(self.doscar_lcfo.projected_dos["AlN_1"]["1a1"].densities[Spin.down], pdos_1a1_AlN) - assert_allclose(self.doscar_lcfo.projected_dos["Al_1"]["3p_y"].densities[Spin.down], pdos_3py_Al) - assert_allclose(self.doscar_lcfo.projected_dos["N_1"]["2s"].densities[Spin.down], pdos_2s_N) - - def test_tdos(self): - """Test total densities of states (TDOS) from DOSCAR files.""" - energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - tdos_up = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02577] - tdos_down = [0.00000, 0.79999, 0.00000, 0.79999, 0.00000, 0.02586] - - assert_allclose(energies_spin, self.doscar_spin_pol.total_dos.energies) - assert_allclose(tdos_up, self.doscar_spin_pol.total_dos.densities[Spin.up]) - assert_allclose(tdos_down, self.doscar_spin_pol.total_dos.densities[Spin.down]) - # assert fermi == approx(self.doscar_spin_pol.total_dos.efermi) - - energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - tdos_nonspin = [0.00000, 1.60000, 0.00000, 1.60000, 0.00000, 0.02418] - - assert_allclose(energies_nonspin, self.doscar_nonspin_pol.total_dos.energies) - assert_allclose(tdos_nonspin, self.doscar_nonspin_pol.total_dos.densities[Spin.up]) - # assert fermi == approx(self.doscar_nonspin_pol.total_dos.efermi) - - def test_energies(self): - """Test energies from DOSCAR files.""" - energies_spin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - - assert_allclose(energies_spin, self.doscar_spin_pol.energies) - - energies_nonspin = [-11.25000, -7.50000, -3.75000, 0.00000, 3.75000, 7.50000] - assert_allclose(energies_nonspin, self.doscar_nonspin_pol.energies) - - def test_itdensities(self): - """Test integrated total densities from DOSCAR files.""" - itdos_up = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09650] - itdos_down = [1.99997, 4.99992, 4.99992, 7.99987, 7.99987, 8.09685] - assert_allclose(itdos_up, self.doscar_spin_pol.integrated_total_dos.densities[Spin.up]) - assert_allclose(itdos_down, self.doscar_spin_pol.integrated_total_dos.densities[Spin.down]) - - itdos_nonspin = [4.00000, 10.00000, 10.00000, 16.00000, 16.00000, 16.09067] - assert_allclose(itdos_nonspin, self.doscar_nonspin_pol.integrated_total_dos.densities[Spin.up]) - - def test_is_spin_polarized(self): - """Test is_spin_polarized attribute from DOSCAR files.""" - assert self.doscar_spin_pol.is_spin_polarized - assert not self.doscar_nonspin_pol.is_spin_polarized - - -class TestCHARGE(MatSciTest): - def setup_method(self): - """Setup for CHARGE and CHARGE_LCFO tests.""" - self.charge2 = CHARGE(filename=f"{TEST_DIR}/CHARGE.lobster.MnO") - self.charge = CHARGE(filename=f"{TEST_DIR}/CHARGE.lobster.MnO2.gz") - self.charge_lcfo = CHARGE_LCFO(filename=f"{TEST_DIR}/CHARGE.LCFO.lobster.ALN.gz") - - def test_attributes(self): - """Test attributes of CHARGE and CHARGE_LCFO classes.""" - charge_loewdin = [-1.25, 1.25] - charge_mulliken = [-1.30, 1.30] - - assert charge_mulliken == self.charge2.mulliken - assert charge_loewdin == self.charge2.loewdin - - assert self.charge_lcfo.is_lcfo - - assert_allclose(self.charge_lcfo.loewdin, [0.0, 1.02, -1.02]) - assert not self.charge_lcfo.mulliken - - def test_msonable(self): - """Test MSONable functionality of CHARGE class.""" - dict_data = self.charge2.as_dict() - charge_from_dict = CHARGE.from_dict(dict_data) - all_attributes = vars(self.charge2) - for attr_name, attr_value in all_attributes.items(): - assert getattr(charge_from_dict, attr_name) == attr_value - - -class TestLobsterOut(MatSciTest): - def setup_method(self): - """Setup for LobsterOut tests.""" - self.lobsterout_normal = LobsterOut(filename=f"{TEST_DIR}/lobsterout.normal") - # make sure .gz files are also read correctly - self.lobsterout_normal = LobsterOut(filename=f"{TEST_DIR}/lobsterout.normal2.gz") - self.lobsterout_fatband_grosspop_densityofenergies = LobsterOut( - filename=f"{TEST_DIR}/lobsterout.fatband_grosspop_densityofenergy" - ) - self.lobsterout_saveprojection = LobsterOut(filename=f"{TEST_DIR}/lobsterout.saveprojection") - self.lobsterout_skipping_all = LobsterOut(filename=f"{TEST_DIR}/lobsterout.skipping_all") - self.lobsterout_twospins = LobsterOut(filename=f"{TEST_DIR}/lobsterout.twospins") - self.lobsterout_GaAs = LobsterOut(filename=f"{TEST_DIR}/lobsterout.GaAs") - self.lobsterout_from_projection = LobsterOut(filename=f"{TEST_DIR}/lobsterout_from_projection") - self.lobsterout_onethread = LobsterOut(filename=f"{TEST_DIR}/lobsterout.onethread") - self.lobsterout_cobi_madelung = LobsterOut(filename=f"{TEST_DIR}/lobsterout_cobi_madelung") - self.lobsterout_doscar_lso = LobsterOut(filename=f"{TEST_DIR}/lobsterout_doscar_lso") - - # TODO: implement skipping madelung/cobi - self.lobsterout_skipping_cobi_madelung = LobsterOut(filename=f"{TEST_DIR}/lobsterout.skip_cobi_madelung") - - self.lobsterout_v511 = LobsterOut(filename=f"{TEST_DIR}/lobsterout_v511.gz") - - def test_attributes(self): - assert self.lobsterout_normal.basis_functions == [ - [ - "3s", - "4s", - "3p_y", - "3p_z", - "3p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ] - ] - assert self.lobsterout_normal.basis_type == ["pbeVaspFit2015"] - assert_allclose(self.lobsterout_normal.charge_spilling, [0.0268]) - assert self.lobsterout_normal.dft_program == "VASP" - assert self.lobsterout_normal.elements == ["Ti"] - assert self.lobsterout_normal.has_charge - assert self.lobsterout_normal.has_cohpcar - assert self.lobsterout_normal.has_coopcar - assert self.lobsterout_normal.has_doscar - assert not self.lobsterout_normal.has_projection - assert self.lobsterout_normal.has_bandoverlaps - assert not self.lobsterout_normal.has_density_of_energies - assert not self.lobsterout_normal.has_fatbands - assert not self.lobsterout_normal.has_grosspopulation - assert self.lobsterout_normal.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 21 and upwards will be ignored.", - ] - assert self.lobsterout_normal.info_orthonormalization == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." - ] - assert not self.lobsterout_normal.is_restart_from_projection - assert self.lobsterout_normal.lobster_version == "3.1.0" - assert self.lobsterout_normal.number_of_spins == 1 - assert self.lobsterout_normal.number_of_threads == 8 - assert self.lobsterout_normal.timing == { - "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "702"}, - "user_time": {"h": "0", "min": "0", "s": "20", "ms": "330"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, - } - assert self.lobsterout_normal.total_spilling[0] == approx([0.044000000000000004][0]) - assert self.lobsterout_normal.warning_lines == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", - "Generally, this is not a critical error. But to help you analyze it,", - "I dumped the band overlap matrices to the file bandOverlaps.lobster.", - "Please check how much they deviate from the identity matrix and decide to", - "use your results only, if you are sure that this is ok.", - ] - - assert self.lobsterout_fatband_grosspop_densityofenergies.basis_functions == [ - [ - "3s", - "4s", - "3p_y", - "3p_z", - "3p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ] - ] - assert self.lobsterout_fatband_grosspop_densityofenergies.basis_type == ["pbeVaspFit2015"] - assert_allclose(self.lobsterout_fatband_grosspop_densityofenergies.charge_spilling, [0.0268]) - assert self.lobsterout_fatband_grosspop_densityofenergies.dft_program == "VASP" - assert self.lobsterout_fatband_grosspop_densityofenergies.elements == ["Ti"] - assert self.lobsterout_fatband_grosspop_densityofenergies.has_charge - assert not self.lobsterout_fatband_grosspop_densityofenergies.has_cohpcar - assert not self.lobsterout_fatband_grosspop_densityofenergies.has_coopcar - assert not self.lobsterout_fatband_grosspop_densityofenergies.has_doscar - assert not self.lobsterout_fatband_grosspop_densityofenergies.has_projection - assert self.lobsterout_fatband_grosspop_densityofenergies.has_bandoverlaps - assert self.lobsterout_fatband_grosspop_densityofenergies.has_density_of_energies - assert self.lobsterout_fatband_grosspop_densityofenergies.has_fatbands - assert self.lobsterout_fatband_grosspop_densityofenergies.has_grosspopulation - assert self.lobsterout_fatband_grosspop_densityofenergies.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 21 and upwards will be ignored.", - ] - assert self.lobsterout_fatband_grosspop_densityofenergies.info_orthonormalization == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." - ] - assert not self.lobsterout_fatband_grosspop_densityofenergies.is_restart_from_projection - assert self.lobsterout_fatband_grosspop_densityofenergies.lobster_version == "3.1.0" - assert self.lobsterout_fatband_grosspop_densityofenergies.number_of_spins == 1 - assert self.lobsterout_fatband_grosspop_densityofenergies.number_of_threads == 8 - assert self.lobsterout_fatband_grosspop_densityofenergies.timing == { - "wall_time": {"h": "0", "min": "0", "s": "4", "ms": "136"}, - "user_time": {"h": "0", "min": "0", "s": "18", "ms": "280"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "290"}, - } - assert self.lobsterout_fatband_grosspop_densityofenergies.total_spilling[0] == approx([0.044000000000000004][0]) - assert self.lobsterout_fatband_grosspop_densityofenergies.warning_lines == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", - "Generally, this is not a critical error. But to help you analyze it,", - "I dumped the band overlap matrices to the file bandOverlaps.lobster.", - "Please check how much they deviate from the identity matrix and decide to", - "use your results only, if you are sure that this is ok.", - ] - - assert self.lobsterout_saveprojection.basis_functions == [ - [ - "3s", - "4s", - "3p_y", - "3p_z", - "3p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ] - ] - assert self.lobsterout_saveprojection.basis_type == ["pbeVaspFit2015"] - assert_allclose(self.lobsterout_saveprojection.charge_spilling, [0.0268]) - assert self.lobsterout_saveprojection.dft_program == "VASP" - assert self.lobsterout_saveprojection.elements == ["Ti"] - assert self.lobsterout_saveprojection.has_charge - assert not self.lobsterout_saveprojection.has_cohpcar - assert not self.lobsterout_saveprojection.has_coopcar - assert not self.lobsterout_saveprojection.has_doscar - assert self.lobsterout_saveprojection.has_projection - assert self.lobsterout_saveprojection.has_bandoverlaps - assert self.lobsterout_saveprojection.has_density_of_energies - assert not self.lobsterout_saveprojection.has_fatbands - assert not self.lobsterout_saveprojection.has_grosspopulation - assert self.lobsterout_saveprojection.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 21 and upwards will be ignored.", - ] - assert self.lobsterout_saveprojection.info_orthonormalization == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." - ] - assert not self.lobsterout_saveprojection.is_restart_from_projection - assert self.lobsterout_saveprojection.lobster_version == "3.1.0" - assert self.lobsterout_saveprojection.number_of_spins == 1 - assert self.lobsterout_saveprojection.number_of_threads == 8 - assert self.lobsterout_saveprojection.timing == { - "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "574"}, - "user_time": {"h": "0", "min": "0", "s": "18", "ms": "250"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "320"}, - } - assert self.lobsterout_saveprojection.total_spilling[0] == approx([0.044000000000000004][0]) - assert self.lobsterout_saveprojection.warning_lines == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", - "Generally, this is not a critical error. But to help you analyze it,", - "I dumped the band overlap matrices to the file bandOverlaps.lobster.", - "Please check how much they deviate from the identity matrix and decide to", - "use your results only, if you are sure that this is ok.", - ] - - assert self.lobsterout_skipping_all.basis_functions == [ - [ - "3s", - "4s", - "3p_y", - "3p_z", - "3p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ] - ] - assert self.lobsterout_skipping_all.basis_type == ["pbeVaspFit2015"] - assert_allclose(self.lobsterout_skipping_all.charge_spilling, [0.0268]) - assert self.lobsterout_skipping_all.dft_program == "VASP" - assert self.lobsterout_skipping_all.elements == ["Ti"] - assert not self.lobsterout_skipping_all.has_charge - assert not self.lobsterout_skipping_all.has_cohpcar - assert not self.lobsterout_skipping_all.has_coopcar - assert not self.lobsterout_skipping_all.has_doscar - assert not self.lobsterout_skipping_all.has_projection - assert self.lobsterout_skipping_all.has_bandoverlaps - assert not self.lobsterout_skipping_all.has_density_of_energies - assert not self.lobsterout_skipping_all.has_fatbands - assert not self.lobsterout_skipping_all.has_grosspopulation - assert not self.lobsterout_skipping_all.has_cobicar - assert not self.lobsterout_skipping_all.has_madelung - assert self.lobsterout_skipping_all.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 21 and upwards will be ignored.", - ] - assert self.lobsterout_skipping_all.info_orthonormalization == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5." - ] - assert not self.lobsterout_skipping_all.is_restart_from_projection - assert self.lobsterout_skipping_all.lobster_version == "3.1.0" - assert self.lobsterout_skipping_all.number_of_spins == 1 - assert self.lobsterout_skipping_all.number_of_threads == 8 - assert self.lobsterout_skipping_all.timing == { - "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "117"}, - "user_time": {"h": "0", "min": "0", "s": "16", "ms": "79"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "320"}, - } - assert self.lobsterout_skipping_all.total_spilling[0] == approx([0.044000000000000004][0]) - assert self.lobsterout_skipping_all.warning_lines == [ - "3 of 147 k-points could not be orthonormalized with an accuracy of 1.0E-5.", - "Generally, this is not a critical error. But to help you analyze it,", - "I dumped the band overlap matrices to the file bandOverlaps.lobster.", - "Please check how much they deviate from the identity matrix and decide to", - "use your results only, if you are sure that this is ok.", - ] - - assert self.lobsterout_twospins.basis_functions == [ - [ - "4s", - "4p_y", - "4p_z", - "4p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ] - ] - assert self.lobsterout_twospins.basis_type == ["pbeVaspFit2015"] - assert self.lobsterout_twospins.charge_spilling[0] == approx(0.36619999999999997) - assert self.lobsterout_twospins.charge_spilling[1] == approx(0.36619999999999997) - assert self.lobsterout_twospins.dft_program == "VASP" - assert self.lobsterout_twospins.elements == ["Ti"] - assert self.lobsterout_twospins.has_charge - assert self.lobsterout_twospins.has_cohpcar - assert self.lobsterout_twospins.has_coopcar - assert self.lobsterout_twospins.has_doscar - assert not self.lobsterout_twospins.has_projection - assert self.lobsterout_twospins.has_bandoverlaps - assert not self.lobsterout_twospins.has_density_of_energies - assert not self.lobsterout_twospins.has_fatbands - assert not self.lobsterout_twospins.has_grosspopulation - assert self.lobsterout_twospins.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 19 and upwards will be ignored.", - ] - assert self.lobsterout_twospins.info_orthonormalization == [ - "60 of 294 k-points could not be orthonormalized with an accuracy of 1.0E-5." - ] - assert not self.lobsterout_twospins.is_restart_from_projection - assert self.lobsterout_twospins.lobster_version == "3.1.0" - assert self.lobsterout_twospins.number_of_spins == 2 - assert self.lobsterout_twospins.number_of_threads == 8 - assert self.lobsterout_twospins.timing == { - "wall_time": {"h": "0", "min": "0", "s": "3", "ms": "71"}, - "user_time": {"h": "0", "min": "0", "s": "22", "ms": "660"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "310"}, - } - assert self.lobsterout_twospins.total_spilling[0] == approx([0.2567][0]) - assert self.lobsterout_twospins.total_spilling[1] == approx([0.2567][0]) - assert self.lobsterout_twospins.warning_lines == [ - "60 of 294 k-points could not be orthonormalized with an accuracy of 1.0E-5.", - "Generally, this is not a critical error. But to help you analyze it,", - "I dumped the band overlap matrices to the file bandOverlaps.lobster.", - "Please check how much they deviate from the identity matrix and decide to", - "use your results only, if you are sure that this is ok.", - ] - - assert self.lobsterout_from_projection.basis_functions == [] - assert self.lobsterout_from_projection.basis_type == [] - assert self.lobsterout_from_projection.charge_spilling[0] == approx(0.0177) - assert self.lobsterout_from_projection.dft_program is None - assert self.lobsterout_from_projection.elements == [] - assert self.lobsterout_from_projection.has_charge - assert self.lobsterout_from_projection.has_cohpcar - assert self.lobsterout_from_projection.has_coopcar - assert self.lobsterout_from_projection.has_doscar - assert not self.lobsterout_from_projection.has_projection - assert not self.lobsterout_from_projection.has_bandoverlaps - assert not self.lobsterout_from_projection.has_density_of_energies - assert not self.lobsterout_from_projection.has_fatbands - assert not self.lobsterout_from_projection.has_grosspopulation - assert self.lobsterout_from_projection.info_lines == [] - assert self.lobsterout_from_projection.info_orthonormalization == [] - assert self.lobsterout_from_projection.is_restart_from_projection - assert self.lobsterout_from_projection.lobster_version == "3.1.0" - assert self.lobsterout_from_projection.number_of_spins == 1 - assert self.lobsterout_from_projection.number_of_threads == 8 - assert self.lobsterout_from_projection.timing == { - "wall_time": {"h": "0", "min": "2", "s": "1", "ms": "890"}, - "user_time": {"h": "0", "min": "15", "s": "10", "ms": "530"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "400"}, - } - assert self.lobsterout_from_projection.total_spilling[0] == approx([0.1543][0]) - assert self.lobsterout_from_projection.warning_lines == [] - - assert self.lobsterout_GaAs.basis_functions == [ - ["4s", "4p_y", "4p_z", "4p_x"], - [ - "4s", - "4p_y", - "4p_z", - "4p_x", - "3d_xy", - "3d_yz", - "3d_z^2", - "3d_xz", - "3d_x^2-y^2", - ], - ] - assert self.lobsterout_GaAs.basis_type == ["Bunge", "Bunge"] - assert self.lobsterout_GaAs.charge_spilling[0] == approx(0.0089) - assert self.lobsterout_GaAs.dft_program == "VASP" - assert self.lobsterout_GaAs.elements == ["As", "Ga"] - assert self.lobsterout_GaAs.has_charge - assert self.lobsterout_GaAs.has_cohpcar - assert self.lobsterout_GaAs.has_coopcar - assert self.lobsterout_GaAs.has_doscar - assert not self.lobsterout_GaAs.has_projection - assert not self.lobsterout_GaAs.has_bandoverlaps - assert not self.lobsterout_GaAs.has_density_of_energies - assert not self.lobsterout_GaAs.has_fatbands - assert not self.lobsterout_GaAs.has_grosspopulation - assert self.lobsterout_GaAs.info_lines == [ - "There are more PAW bands than local basis functions available.", - "To prevent trouble in orthonormalization and Hamiltonian reconstruction", - "the PAW bands from 14 and upwards will be ignored.", - ] - assert self.lobsterout_GaAs.info_orthonormalization == [] - assert not self.lobsterout_GaAs.is_restart_from_projection - assert self.lobsterout_GaAs.lobster_version == "3.1.0" - assert self.lobsterout_GaAs.number_of_spins == 1 - assert self.lobsterout_GaAs.number_of_threads == 8 - assert self.lobsterout_GaAs.timing == { - "wall_time": {"h": "0", "min": "0", "s": "2", "ms": "726"}, - "user_time": {"h": "0", "min": "0", "s": "12", "ms": "370"}, - "sys_time": {"h": "0", "min": "0", "s": "0", "ms": "180"}, - } - assert self.lobsterout_GaAs.total_spilling[0] == approx(0.0859) - - assert self.lobsterout_onethread.number_of_threads == 1 - # Test lobsterout of lobster-4.1.0 - assert self.lobsterout_cobi_madelung.has_cobicar - assert self.lobsterout_cobi_madelung.has_cohpcar - assert self.lobsterout_cobi_madelung.has_madelung - assert not self.lobsterout_cobi_madelung.has_doscar_lso - - assert self.lobsterout_doscar_lso.has_doscar_lso - - assert self.lobsterout_skipping_cobi_madelung.has_cobicar is False - assert self.lobsterout_skipping_cobi_madelung.has_madelung is False - - def test_msonable(self): - """Test the as_dict and from_dict methods for Lobsterout.""" - dict_data = self.lobsterout_normal.as_dict() - - lobsterout_from_dict = LobsterOut.from_dict(dict_data) - assert dict_data == lobsterout_from_dict.as_dict() - - with pytest.raises(TypeError, match="got an unexpected keyword argument 'invalid'"): - LobsterOut(filename=None, invalid="invalid") # type: ignore[testing] - - -class TestFatbands(MatSciTest): - def setup_method(self): - self.structure = Vasprun( - filename=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml", - ionic_step_skip=None, - ionic_step_offset=0, - parse_dos=True, - parse_eigen=False, - parse_projected_eigen=False, - parse_potcar_file=False, - occu_tol=1e-8, - exception_on_bad_xml=True, - ).final_structure - self.fatbands_sio2_p_x = Fatbands( - directory=f"{TEST_DIR}/Fatband_SiO2/Test_p_x", - structure=self.structure, - ) - self.vasprun_sio2_p_x = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p_x/vasprun.xml") - self.bs_symmline = self.vasprun_sio2_p_x.get_band_structure(line_mode=True, force_hybrid_mode=True) - self.fatbands_sio2_p = Fatbands( - directory=f"{TEST_DIR}/Fatband_SiO2/Test_p", - structure=self.structure, - ) - self.single_fatband = Fatband(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p/FATBAND_o4_2p.lobster") - self.vasprun_sio2_p = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_p/vasprun.xml") - self.bs_symmline2 = self.vasprun_sio2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) - self.fatbands_sio2_spin = Fatbands( - directory=f"{TEST_DIR}/Fatband_SiO2/Test_Spin", - structure=self.structure, - ) - - self.vasprun_sio2_spin = Vasprun(filename=f"{TEST_DIR}/Fatband_SiO2/Test_Spin/vasprun.xml") - self.bs_symmline_spin = self.vasprun_sio2_p.get_band_structure(line_mode=True, force_hybrid_mode=True) - - def test_attributes(self): - assert self.fatbands_sio2_p_x.efermi == self.vasprun_sio2_p_x.efermi - lattice1 = self.bs_symmline.lattice_rec.as_dict() - lattice2 = self.fatbands_sio2_p_x.reciprocal_lattice.as_dict() - for idx in range(3): - assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) - - assert self.fatbands_sio2_p_x.fatbands[1]["energies"][Spin.up][1][1] == approx(-18.245) - assert len(self.fatbands_sio2_p_x.spins) == 1 - assert_allclose(self.fatbands_sio2_p_x.kpoints.kpts[3], [0.03409091, 0, 0]) - - assert len(self.fatbands_sio2_p_x.fatbands[0]["projections"][Spin.up]) == len( - self.fatbands_sio2_p_x.kpoints.kpts - ) - - assert self.single_fatband.nbands == 36 - assert self.single_fatband.center == "O4" - assert self.single_fatband.orbital == "2p" - assert len(self.single_fatband.spins) == 1 - assert len(self.single_fatband.fatband["energies"][Spin.up][0]) == 36 - - assert self.fatbands_sio2_p_x.fatbands[-1]["center"] == "Si3" - assert self.fatbands_sio2_p_x.fatbands[-1]["orbital"] == "3s" - assert self.fatbands_sio2_p_x.fatbands[-1]["projections"][Spin.up][2][1] == approx(0.013) - assert self.fatbands_sio2_p_x.fatbands[-1]["energies"][Spin.up][2][2] == approx(-18.245) - assert_allclose(self.fatbands_sio2_p_x.structure[0].frac_coords, [0.0, 0.47634315, 0.666667]) - assert self.fatbands_sio2_p_x.structure[0].species_string == "Si" - assert_allclose( - self.fatbands_sio2_p_x.structure[0].coords, - [-1.19607309, 2.0716597, 3.67462144], - ) - - assert self.fatbands_sio2_p.efermi == self.vasprun_sio2_p.efermi - lattice1 = self.bs_symmline2.lattice_rec.as_dict() - lattice2 = self.fatbands_sio2_p.reciprocal_lattice.as_dict() - for idx in range(3): - assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) - assert self.fatbands_sio2_p.fatbands[0]["energies"][Spin.up][1][1] == approx(-18.245) - assert len(self.fatbands_sio2_p.spins) == 1 - assert_allclose(self.fatbands_sio2_p.kpoints.kpts[3], [0.03409091, 0, 0]) - - assert self.fatbands_sio2_p.fatbands[2]["projections"][Spin.up][2][1] == approx(0.002) - assert_allclose(self.fatbands_sio2_p.structure[0].frac_coords, [0.0, 0.47634315, 0.666667]) - assert self.fatbands_sio2_p.structure[0].species_string == "Si" - assert_allclose( - self.fatbands_sio2_p.structure[0].coords, - [-1.19607309, 2.0716597, 3.67462144], - ) - assert self.fatbands_sio2_p.efermi == approx(1.0647039) - - assert self.fatbands_sio2_spin.efermi == self.vasprun_sio2_spin.efermi - lattice1 = self.bs_symmline_spin.lattice_rec.as_dict() - lattice2 = self.fatbands_sio2_spin.reciprocal_lattice.as_dict() - for idx in range(3): - assert lattice1["matrix"][idx] == approx(lattice2["matrix"][idx]) - assert self.fatbands_sio2_spin.fatbands[1]["energies"][Spin.up][1][1] == approx(-18.245) - assert self.fatbands_sio2_spin.fatbands[0]["energies"][Spin.down][1][1] == approx(-18.245) - assert len(self.fatbands_sio2_spin.spins) == 2 - assert_allclose(self.fatbands_sio2_spin.kpoints.kpts[3], [0.03409091, 0, 0]) - assert len(self.fatbands_sio2_spin.fatbands[0]["energies"][Spin.up][0]) == 36 - assert len(self.fatbands_sio2_spin.fatbands[0]["energies"][Spin.down][0]) == 36 - - assert self.fatbands_sio2_spin.fatbands[0]["projections"][Spin.up][2][1] == approx(0.003) - assert_allclose( - self.fatbands_sio2_spin.structure[0].frac_coords, - [0.0, 0.47634315, 0.666667], - ) - assert self.fatbands_sio2_spin.structure[0].species_string == "Si" - assert_allclose( - self.fatbands_sio2_spin.structure[0].coords, - [-1.19607309, 2.0716597, 3.67462144], - ) - - -class TestBandOverlaps(MatSciTest): - def setup_method(self): - with pytest.raises(RuntimeError, match="Incomplete or non-numeric data found in bandOverlaps"): - self.band_overlaps1 = BandOverlaps(f"{TEST_DIR}/bandOverlaps.lobster.1") - - self.band_overlaps1 = BandOverlaps(f"{TEST_DIR}/bandOverlaps.lobster.1", process_immediately=False) - self.band_overlaps2 = BandOverlaps(f"{TEST_DIR}/bandOverlaps.lobster.2", process_immediately=False) - - self.band_overlaps1.lobster_version = "3.1.0" - self.band_overlaps2.lobster_version = "2.7.0" - - self.band_overlaps1.process() - self.band_overlaps2.process() - - self.band_overlaps1_new = BandOverlaps(f"{TEST_DIR}/bandOverlaps.lobster.new.1") - self.band_overlaps2_new = BandOverlaps(f"{TEST_DIR}/bandOverlaps.lobster.new.2") - - def test_attributes(self): - # band_overlaps_dict - bo_dict = self.band_overlaps1.band_overlaps - assert bo_dict["max_deviations"][Spin.up][0] == approx(0.000278953) - assert self.band_overlaps1_new.band_overlaps["max_deviations"][Spin.up][10] == approx(0.0640933) - assert bo_dict["matrices"][Spin.up][0].item(-1, -1) == approx(0.0188058) - assert self.band_overlaps1_new.band_overlaps["matrices"][Spin.up][10].item(-1, -1) == approx(1.0) - assert bo_dict["matrices"][Spin.up][0].item(0, 0) == approx(1) - assert self.band_overlaps1_new.band_overlaps["matrices"][Spin.up][10].item(0, 0) == approx(0.995849) - - assert bo_dict["max_deviations"][Spin.down][-1] == approx(4.31567e-05) - assert self.band_overlaps1_new.band_overlaps["max_deviations"][Spin.down][9] == approx(0.064369) - assert bo_dict["matrices"][Spin.down][-1].item(0, -1) == approx(4.0066e-07) - assert self.band_overlaps1_new.band_overlaps["matrices"][Spin.down][9].item(0, -1) == approx(1.37447e-09) - - def test_has_good_quality_maxDeviation(self): - assert not self.band_overlaps1.has_good_quality_max_deviation(limit_max_deviation=0.1) - assert not self.band_overlaps1_new.has_good_quality_max_deviation(limit_max_deviation=0.1) - - assert self.band_overlaps1.has_good_quality_max_deviation(limit_max_deviation=100) - assert self.band_overlaps1_new.has_good_quality_max_deviation(limit_max_deviation=100) - assert self.band_overlaps2.has_good_quality_max_deviation() - assert not self.band_overlaps2_new.has_good_quality_max_deviation() - assert not self.band_overlaps2.has_good_quality_max_deviation(limit_max_deviation=0.0000001) - assert not self.band_overlaps2_new.has_good_quality_max_deviation(limit_max_deviation=0.0000001) - - def test_has_good_quality_check_occupied_bands(self): - assert not self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=9, - number_occ_bands_spin_down=5, - limit_deviation=0.1, - spin_polarized=True, - ) - assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=9, - number_occ_bands_spin_down=5, - limit_deviation=0.1, - spin_polarized=True, - ) - assert self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=3, - number_occ_bands_spin_down=0, - limit_deviation=1, - spin_polarized=True, - ) - assert self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=3, - number_occ_bands_spin_down=0, - limit_deviation=1, - spin_polarized=True, - ) - assert not self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, - number_occ_bands_spin_down=1, - limit_deviation=1e-6, - spin_polarized=True, - ) - assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, - number_occ_bands_spin_down=1, - limit_deviation=1e-6, - spin_polarized=True, - ) - assert not self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, - number_occ_bands_spin_down=0, - limit_deviation=1e-6, - spin_polarized=True, - ) - assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, - number_occ_bands_spin_down=0, - limit_deviation=1e-6, - spin_polarized=True, - ) - assert not self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=0, - number_occ_bands_spin_down=1, - limit_deviation=1e-6, - spin_polarized=True, - ) - assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=0, - number_occ_bands_spin_down=1, - limit_deviation=1e-6, - spin_polarized=True, - ) - assert not self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=4, - number_occ_bands_spin_down=4, - limit_deviation=1e-3, - spin_polarized=True, - ) - assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=4, - number_occ_bands_spin_down=4, - limit_deviation=1e-3, - spin_polarized=True, - ) - assert not self.band_overlaps2.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=10, limit_deviation=1e-7 - ) - assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=10, limit_deviation=1e-7 - ) - assert self.band_overlaps2.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, limit_deviation=0.1 - ) - - assert not self.band_overlaps2.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, limit_deviation=1e-8 - ) - assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, limit_deviation=1e-8 - ) - assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=10, limit_deviation=1) - assert self.band_overlaps2_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=2, limit_deviation=0.1 - ) - assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=1, limit_deviation=1) - assert self.band_overlaps2_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=1, limit_deviation=2 - ) - - def test_has_good_quality_check_occupied_bands_patched(self): - """Test with patched data.""" - - limit_deviation = 0.1 - - rng = np.random.default_rng(42) # set seed for reproducibility - - band_overlaps = copy.deepcopy(self.band_overlaps1_new) - - number_occ_bands_spin_up_all = list(range(band_overlaps.band_overlaps["matrices"][Spin.up][0].shape[0])) - number_occ_bands_spin_down_all = list(range(band_overlaps.band_overlaps["matrices"][Spin.down][0].shape[0])) - - for actual_deviation in [0.05, 0.1, 0.2, 0.5, 1.0]: - for spin in (Spin.up, Spin.down): - for number_occ_bands_spin_up, number_occ_bands_spin_down in zip( - number_occ_bands_spin_up_all, - number_occ_bands_spin_down_all, - strict=False, - ): - for i_arr, array in enumerate(band_overlaps.band_overlaps["matrices"][spin]): - number_occ_bands = number_occ_bands_spin_up if spin is Spin.up else number_occ_bands_spin_down - - shape = array.shape - assert np.all(np.array(shape) >= number_occ_bands) - assert len(shape) == 2 - assert shape[0] == shape[1] - - # Generate a noisy background array - patch_array = rng.uniform(0, 10, shape) - - # Patch the top-left sub-array (the part that would be checked) - patch_array[:number_occ_bands, :number_occ_bands] = np.identity(number_occ_bands) + rng.uniform( - 0, actual_deviation, (number_occ_bands, number_occ_bands) - ) - - band_overlaps.band_overlaps["matrices"][spin][i_arr] = patch_array - - result = band_overlaps.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=number_occ_bands_spin_up, - number_occ_bands_spin_down=number_occ_bands_spin_down, - spin_polarized=True, - limit_deviation=limit_deviation, - ) - - if ( - ( - actual_deviation == 0.05 - and number_occ_bands_spin_up <= 7 - and number_occ_bands_spin_down <= 7 - and spin is Spin.up - ) - or (actual_deviation == 0.05 and spin is Spin.down) - or actual_deviation == 0.1 - or ( - actual_deviation in [0.2, 0.5, 1.0] - and number_occ_bands_spin_up == 0 - and number_occ_bands_spin_down == 0 - ) - ): - assert result - else: - assert not result - - def test_exceptions(self): - with pytest.raises(ValueError, match="number_occ_bands_spin_down has to be specified"): - self.band_overlaps1.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=4, - spin_polarized=True, - ) - with pytest.raises(ValueError, match="number_occ_bands_spin_down has to be specified"): - self.band_overlaps1_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=4, - spin_polarized=True, - ) - - def test_keys(self): - bo_dict = self.band_overlaps1.band_overlaps - bo_dict_new = self.band_overlaps1_new.band_overlaps - bo_dict_2 = self.band_overlaps2.band_overlaps - assert len(bo_dict["k_points"][Spin.up]) == 408 - assert len(bo_dict_2["max_deviations"][Spin.up]) == 2 - assert len(bo_dict_new["matrices"][Spin.down]) == 73 - - -class TestGROSSPOP(MatSciTest): - def setup_method(self): - self.grosspop1 = GROSSPOP(f"{TEST_DIR}/GROSSPOP.lobster") - self.grosspop_511_sp = GROSSPOP(f"{TEST_DIR}/GROSSPOP_511_sp.lobster.AlN.gz") - self.grosspop_511_nsp = GROSSPOP(f"{TEST_DIR}/GROSSPOP_511_nsp.lobster.NaCl.gz") - self.grosspop_511_lcfo = GROSSPOP_LCFO(f"{TEST_DIR}/GROSSPOP.LCFO.lobster.AlN.gz") - - def test_attributes(self): - gross_pop_list = self.grosspop1.populations - gross_pop_list_511_sp = self.grosspop_511_sp.populations - gross_pop_list_511_nsp = self.grosspop_511_nsp.populations - gross_pop_list_lcfo = self.grosspop_511_lcfo.populations - - assert gross_pop_list["Si1"]["3p_y"][Spin.up]["mulliken"] == approx(0.38) - assert gross_pop_list["Si1"]["3p_z"][Spin.up]["mulliken"] == approx(0.37) - assert gross_pop_list["Si1"]["3p_x"][Spin.up]["mulliken"] == approx(0.37) - assert gross_pop_list["Si1"]["3p_y"][Spin.up]["loewdin"] == approx(0.52) - assert gross_pop_list["Si1"]["3p_z"][Spin.up]["loewdin"] == approx(0.52) - assert gross_pop_list["Si1"]["3p_x"][Spin.up]["loewdin"] == approx(0.52) - assert gross_pop_list["O5"]["2s"][Spin.up]["mulliken"] == approx(1.80) - assert gross_pop_list["O5"]["2s"][Spin.up]["loewdin"] == approx(1.60) - assert gross_pop_list["O8"]["2s"][Spin.up]["mulliken"] == approx(1.80) - assert gross_pop_list["O8"]["2s"][Spin.up]["loewdin"] == approx(1.60) - assert len(gross_pop_list) == 9 - - # v5.1 spin polarized - assert len(self.grosspop_511_sp.spins) == 2 - assert gross_pop_list_511_sp["Al1"]["3p_x"][Spin.up]["mulliken"] == approx(0.19) - assert gross_pop_list_511_sp["N3"]["2s"][Spin.down]["loewdin"] == approx(0.7) - - # v5.1 non spin polarized - assert len(self.grosspop_511_nsp.spins) == 1 - assert self.grosspop_511_lcfo.is_lcfo - assert gross_pop_list_511_nsp["Na1"]["3s"][Spin.up]["mulliken"] == approx(0.22) - assert gross_pop_list_511_nsp["Na1"]["3s"][Spin.up]["loewdin"] == approx(0.33) - - # v.5.1.1 LCFO - assert self.grosspop_511_lcfo.is_lcfo - assert gross_pop_list_lcfo["AlN1"]["1a1"][Spin.up]["loewdin"] == approx(0.81) - assert gross_pop_list_lcfo["AlN1"]["1a1"][Spin.down]["loewdin"] == approx(0.81) - - with pytest.raises(KeyError): - assert gross_pop_list_lcfo["AlN1"]["1a1"][Spin.up]["mulliken"] - - def test_msonable(self): - dict_data = self.grosspop1.as_dict() - grosspop_from_dict = GROSSPOP.from_dict(dict_data) - all_attributes = vars(self.grosspop1) - for attr_name, attr_value in all_attributes.items(): - assert getattr(grosspop_from_dict, attr_name) == attr_value - - -class TestICOXXLIST(MatSciTest): - def setup_method(self): - self.icohp_bise = ICOHPLIST(filename=f"{TEST_DIR}/ICOHPLIST.lobster.BiSe", process_immediately=False) - self.icohp_bise.lobster_version = "3.1.0" - self.icohp_bise.process() - - self.icoop_bise = ICOOPLIST(filename=f"{TEST_DIR}/ICOOPLIST.lobster.BiSe", process_immediately=False) - self.icoop_bise.lobster_version = "3.2.0" - self.icoop_bise.process() - - self.icohp_fe = ICOHPLIST(filename=f"{TEST_DIR}/ICOHPLIST.lobster", process_immediately=False) - self.icohp_fe.lobster_version = "2.7.0" - self.icohp_fe.process() - - self.icohp_gzipped = ICOHPLIST(filename=f"{TEST_DIR}/ICOHPLIST.lobster.gz", process_immediately=False) - self.icohp_gzipped.lobster_version = "3.1.0" - self.icohp_gzipped.process() - - self.icoop_fe = ICOOPLIST(filename=f"{TEST_DIR}/ICOOPLIST.lobster", process_immediately=False) - self.icoop_fe.lobster_version = "2.7.0" - self.icoop_fe.process() - - self.icohp_aln_511_sp = ICOHPLIST(filename=f"{TEST_DIR}/ICOHPLIST_511_sp.lobster.AlN.gz") - self.icohp_nacl_511_nsp = ICOHPLIST( - filename=f"{TEST_DIR}/ICOHPLIST_511_nsp.lobster.NaCl.gz", - process_immediately=False, - ) - self.icohp_nacl_511_nsp.lobster_version = "5.0.5" - self.icohp_nacl_511_nsp.process() - - # ICOHPLIST.LCFO.lobster from Lobster v5.1.1 - self.icohp_lcfo = ICOHPLIST_LCFO(filename=f"{TEST_DIR}/ICOHPLIST.LCFO.lobster.AlN.gz") - self.icohp_lcfo_non_orbitalwise = ICOHPLIST_LCFO( - filename=f"{TEST_DIR}/ICOHPLIST_non_orbitalwise.LCFO.lobster.AlN.gz", - ) - - self.icobi_orbitalwise = ICOBILIST(filename=f"{TEST_DIR}/ICOBILIST.lobster", process_immediately=False) - self.icobi_orbitalwise.lobster_version = "3.1.0" - self.icobi_orbitalwise.process() - - self.icobi = ICOBILIST( - filename=f"{TEST_DIR}/ICOBILIST.lobster.withoutorbitals", - process_immediately=False, - ) - self.icobi.lobster_version = "3.1.0" - self.icobi.process() - - self.icobi_orbitalwise_spinpolarized = ICOBILIST( - filename=f"{TEST_DIR}/ICOBILIST.lobster.spinpolarized", - process_immediately=False, - ) - self.icobi_orbitalwise_spinpolarized.lobster_version = "4.5.0" - self.icobi_orbitalwise_spinpolarized.process() - # make sure the correct line is read to check if this is a orbitalwise ICOBILIST - self.icobi_orbitalwise_add = ICOBILIST( - filename=f"{TEST_DIR}/ICOBILIST.lobster.additional_case", - process_immediately=False, - ) - self.icobi_orbitalwise_add.lobster_version = "3.1.0" - self.icobi_orbitalwise_add.process() - - self.icobi_orbitalwise_spinpolarized_add = ICOBILIST( - filename=f"{TEST_DIR}/ICOBILIST.lobster.spinpolarized.additional_case", - process_immediately=False, - ) - self.icobi_orbitalwise_spinpolarized_add.lobster_version = "4.5.0" - self.icobi_orbitalwise_spinpolarized_add.process() - - def test_attributes(self): - assert len(self.icohp_bise.spins) == 1 - assert len(self.icohp_bise.interactions) == 11 - assert len(self.icohp_fe.spins) == 2 - assert len(self.icohp_fe.interactions) == 2 - assert len(self.icoop_fe.spins) == 2 - assert len(self.icoop_fe.interactions) == 2 - - # >v5 ICOHPLIST - assert len(self.icohp_aln_511_sp.spins) == 2 - - assert len(self.icohp_aln_511_sp.interactions) == 1088 - assert len(self.icohp_nacl_511_nsp.spins) == 1 - assert len(self.icohp_nacl_511_nsp.interactions) == 2584 - - # v5.1.1 LCFO - assert self.icohp_lcfo.is_lcfo - assert len(self.icohp_lcfo.spins) == 2 - assert len(self.icohp_lcfo.interactions) == 1180 - assert len(self.icohp_lcfo_non_orbitalwise.interactions) == 28 - - def test_values(self): - icohplist_bise = [ - { - "index": 1, - "centers": ["Bi1", "Se7"], - "length": 2.88231, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -2.18042}, - }, - { - "index": 2, - "centers": ["Bi1", "Se10"], - "length": 3.10144, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -1.14347}, - }, - { - "index": 3, - "centers": ["Bi2", "Se8"], - "length": 2.88231, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -2.18042}, - }, - { - "index": 4, - "centers": ["Bi2", "Se9"], - "length": 3.10144, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -1.14348}, - }, - { - "index": 5, - "centers": ["Bi3", "Se10"], - "length": 3.05001, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -1.30006}, - }, - { - "index": 6, - "centers": ["Bi3", "Se11"], - "length": 2.91676, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -1.96843}, - }, - { - "index": 7, - "centers": ["Bi4", "Se9"], - "length": 3.05001, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -1.30006}, - }, - { - "index": 8, - "centers": ["Bi4", "Se12"], - "length": 2.91676, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -1.96843}, - }, - { - "index": 9, - "centers": ["Bi5", "Se12"], - "length": 3.37522, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -0.47531}, - }, - { - "index": 10, - "centers": ["Bi5", "Bi6"], - "length": 3.07294, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -2.38796}, - }, - { - "index": 11, - "centers": ["Bi6", "Se11"], - "length": 3.37522, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -0.47531}, - }, - ] - icooplist_bise = [ - { - "index": 1, - "centers": ["Bi1", "Se7"], - "length": 2.88231, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: 0.14245}, - }, - { - "index": 2, - "centers": ["Bi1", "Se10"], - "length": 3.10144, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -0.04118}, - }, - { - "index": 3, - "centers": ["Bi2", "Se8"], - "length": 2.88231, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: 0.14245}, - }, - { - "index": 4, - "centers": ["Bi2", "Se9"], - "length": 3.10144, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -0.04118}, - }, - { - "index": 5, - "centers": ["Bi3", "Se10"], - "length": 3.05001, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -0.03516}, - }, - { - "index": 6, - "centers": ["Bi3", "Se11"], - "length": 2.91676, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: 0.10745}, - }, - { - "index": 7, - "centers": ["Bi4", "Se9"], - "length": 3.05001, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -0.03516}, - }, - { - "index": 8, - "centers": ["Bi4", "Se12"], - "length": 2.91676, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: 0.10745}, - }, - { - "index": 9, - "centers": ["Bi5", "Se12"], - "length": 3.37522, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -0.12395}, - }, - { - "index": 10, - "centers": ["Bi5", "Bi6"], - "length": 3.07294, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: 0.24714}, - }, - { - "index": 11, - "centers": ["Bi6", "Se11"], - "length": 3.37522, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -0.12395}, - }, - ] - icooplist_fe = [ - { - "index": 1, - "centers": ["Fe8", "Fe7"], - "length": 2.83189, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -0.11389, Spin.down: -0.20828}, - }, - { - "index": 2, - "centers": ["Fe8", "Fe9"], - "length": 2.45249, - "cells": [[], []], - "orbitals": [None, None], - "icoxx": {Spin.up: -0.04087, Spin.down: -0.05756}, - }, - ] - - assert icohplist_bise == self.icohp_bise.interactions - assert icooplist_fe == self.icoop_fe.interactions - assert icooplist_bise == self.icoop_bise.interactions - - assert self.icobi.interactions[1]["icoxx"][Spin.up] == approx(0.58649) - assert self.icobi_orbitalwise.interactions[2]["icoxx"][Spin.up] == approx(0.02559) - assert self.icobi_orbitalwise.interactions[1]["icoxx"][Spin.up] == approx(0.04940) - assert self.icobi_orbitalwise_spinpolarized.interactions[1]["icoxx"][Spin.up] == approx(0.04940 / 2, abs=1e-3) - assert self.icobi_orbitalwise_spinpolarized.interactions[1]["icoxx"][Spin.down] == approx(0.04940 / 2, abs=1e-3) - assert self.icobi_orbitalwise_spinpolarized.interactions[2]["icoxx"][Spin.down] == approx(0.01279, abs=1e-3) - assert self.icobi_orbitalwise_spinpolarized.interactions[2]["orbitals"] == [ - "2p_y", - "6s", - ] - - # >v5 ICOHPLIST - assert self.icohp_aln_511_sp.interactions[2]["icoxx"][Spin.up] == approx(0.00102) - assert self.icohp_aln_511_sp.interactions[2]["icoxx"][Spin.down] == approx(0.00104) - assert self.icohp_nacl_511_nsp.interactions[13]["icoxx"][Spin.up] == approx(0.0) - assert self.icohp_nacl_511_nsp.interactions[10]["orbitals"] == ["2p_y", "2p_z"] - - # v5.1.1 LCFO - assert self.icohp_lcfo.interactions[15]["orbitals"] == ["2a1", "4e"] - assert self.icohp_lcfo_non_orbitalwise.interactions[16]["icoxx"][Spin.up] == approx(-0.21495) - assert self.icohp_lcfo_non_orbitalwise.interactions[16]["icoxx"][Spin.down] == approx(-0.21498) - - def test_msonable(self): - dict_data = self.icobi_orbitalwise_spinpolarized.as_dict() - icohplist_from_dict = ICOHPLIST.from_dict(dict_data) - all_attributes = vars(self.icobi_orbitalwise_spinpolarized) - for attr_name, attr_value in all_attributes.items(): - if isinstance(attr_value, np.ndarray): - assert_array_equal(getattr(icohplist_from_dict, attr_name), attr_value) - else: - assert getattr(icohplist_from_dict, attr_name) == attr_value - - -class TestWavefunction(MatSciTest): - def test_parse_file(self): - wf = Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ) - - assert_array_equal([41, 41, 41], wf.grid) - assert wf.points[4][0] == approx(0.0000) - assert wf.points[4][1] == approx(0.0000) - assert wf.points[4][2] == approx(0.4000) - assert wf.reals[8] == approx(1.38863e-01) - assert wf.imaginaries[8] == approx(2.89645e-01) - assert len(wf.imaginaries) == 41 * 41 * 41 - assert len(wf.reals) == 41 * 41 * 41 - assert len(wf.points) == 41 * 41 * 41 - assert wf.distances[0] == approx(0.0000) - - def test_set_volumetric_data(self): - wave1 = Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ) - - wave1.set_volumetric_data(grid=wave1.grid, structure=wave1.structure) - assert wave1.volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) - assert wave1.volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) - - def test_get_volumetricdata_real(self): - wave1 = Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ) - volumetricdata_real = wave1.get_volumetricdata_real() - assert volumetricdata_real.data["total"][0, 0, 0] == approx(-3.0966) - - def test_get_volumetricdata_imaginary(self): - wave1 = Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ) - volumetricdata_imaginary = wave1.get_volumetricdata_imaginary() - assert volumetricdata_imaginary.data["total"][0, 0, 0] == approx(-6.45895e00) - - def test_get_volumetricdata_density(self): - wave1 = Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ) - volumetricdata_density = wave1.get_volumetricdata_density() - assert volumetricdata_density.data["total"][0, 0, 0] == approx((-3.0966 * -3.0966) + (-6.45895 * -6.45895)) - - def test_write_file(self): - wave1 = Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ) - real_wavecar_path = f"{self.tmp_path}/real-wavecar.vasp" - wave1.write_file(filename=real_wavecar_path, part="real") - assert os.path.isfile(real_wavecar_path) - - imag_wavecar_path = f"{self.tmp_path}/imaginary-wavecar.vasp" - wave1.write_file(filename=imag_wavecar_path, part="imaginary") - assert os.path.isfile(imag_wavecar_path) - - density_wavecar_path = f"{self.tmp_path}/density-wavecar.vasp" - wave1.write_file(filename=density_wavecar_path, part="density") - assert os.path.isfile(density_wavecar_path) - - -class TestSitePotentials(MatSciTest): - def setup_method(self) -> None: - self.sitepotentials = SitePotentials(filename=f"{TEST_DIR}/SitePotentials.lobster.perovskite") - - def test_attributes(self): - assert self.sitepotentials.site_potentials_loewdin == [ - -8.77, - -17.08, - 9.57, - 9.57, - 8.45, - ] - assert self.sitepotentials.site_potentials_mulliken == [ - -11.38, - -19.62, - 11.18, - 11.18, - 10.09, - ] - assert self.sitepotentials.madelung_energies_loewdin == approx(-28.64) - assert self.sitepotentials.madelung_energies_mulliken == approx(-40.02) - assert self.sitepotentials.centers == ["La1", "Ta2", "N3", "N4", "O5"] - assert len(self.sitepotentials.centers) == 5 - assert self.sitepotentials.ewald_splitting == approx(3.14) - - def test_msonable(self): - dict_data = self.sitepotentials.as_dict() - sitepotential_from_dict = SitePotentials.from_dict(dict_data) - all_attributes = vars(self.sitepotentials) - for attr_name, attr_value in all_attributes.items(): - assert getattr(sitepotential_from_dict, attr_name) == attr_value - - -class TestMadelungEnergies(MatSciTest): - def setup_method(self) -> None: - self.madelungenergies = MadelungEnergies(filename=f"{TEST_DIR}/MadelungEnergies.lobster.perovskite") - - def test_attributes(self): - assert self.madelungenergies.madelung_energies_loewdin == approx(-28.64) - assert self.madelungenergies.madelung_energies_mulliken == approx(-40.02) - assert self.madelungenergies.ewald_splitting == approx(3.14) - - def test_msonable(self): - dict_data = self.madelungenergies.as_dict() - madelung_from_dict = MadelungEnergies.from_dict(dict_data) - all_attributes = vars(self.madelungenergies) - for attr_name, attr_value in all_attributes.items(): - assert getattr(madelung_from_dict, attr_name) == attr_value - - -class TestLobsterMatrices(MatSciTest): - def setup_method(self) -> None: - self.hamilton_matrices = LobsterMatrices( - filename=f"{TEST_DIR}/Na_hamiltonMatrices.lobster.gz", efermi=-2.79650354 - ) - self.transfer_matrices = LobsterMatrices(filename=f"{TEST_DIR}/C_transferMatrices.lobster.gz") - self.overlap_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Si_overlapMatrices.lobster.gz") - self.coeff_matrices = LobsterMatrices(filename=f"{TEST_DIR}/Si_coefficientMatricesLSO1.lobster.gz") - - def test_attributes(self): - assert self.hamilton_matrices.efermi == -2.79650354 - assert self.hamilton_matrices.centers == ["Na1", "Na1", "Na1", "Na1"] - - assert "1" in self.hamilton_matrices.matrices - assert len(self.hamilton_matrices.matrices) == 1 - - assert isinstance(self.hamilton_matrices.matrices["1"], dict) - for spin in self.hamilton_matrices.matrices["1"]: - assert spin in [Spin.up, Spin.down] - assert isinstance(self.hamilton_matrices.matrices["1"][spin], np.ndarray) - assert self.hamilton_matrices.matrices["1"][spin].shape == (4, 4) - - assert self.hamilton_matrices.orbitals == ["3s", "2p_y", "2p_z", "2p_x"] - - with pytest.raises(KeyError): - assert self.hamilton_matrices.matrices["2"][Spin.down] - - assert self.hamilton_matrices.matrices["1"][Spin.up][0, 0].real == approx(-3.02170000) - assert self.hamilton_matrices.matrices["1"][Spin.up][0, 0].imag == approx(0.0) - - assert self.hamilton_matrices.get_onsite_values("Na1", "3s") == approx( - (-3.0217 + 2.79650354 - 1.39420000 + 2.79650354) / 2 - ) - - assert self.hamilton_matrices.get_onsite_values("Na1", "2p_x") == approx( - (-28.56640000 + 2.79650354 - 28.48100000 + 2.79650354) / 2 - ) - - onsite_values = self.hamilton_matrices.get_onsite_values() - assert isinstance(onsite_values, dict) - - for key in onsite_values: - assert key in ["Na1_3s", "Na1_2p_y", "Na1_2p_z", "Na1_2p_x"] - assert isinstance(onsite_values[key], float) - - assert self.overlap_matrices.efermi is None - assert self.overlap_matrices.centers == ["Si1", "Si1", "Si1", "Si1"] - assert self.overlap_matrices.orbitals == ["3s", "3p_y", "3p_z", "3p_x"] - - assert "1" in self.overlap_matrices.matrices - assert len(self.overlap_matrices.matrices) == 1 - - assert isinstance(self.overlap_matrices.matrices["1"], dict) - for spin in self.overlap_matrices.matrices["1"]: - assert spin is None - assert isinstance(self.overlap_matrices.matrices["1"][spin], np.ndarray) - assert self.overlap_matrices.matrices["1"][spin].shape == (4, 4) - - for m in range(4): - assert self.overlap_matrices.matrices["1"][None][m, m].real == approx(1.00000000) - - assert self.transfer_matrices.efermi is None - assert self.transfer_matrices.centers == ["C1", "C1", "C1", "C1"] - assert self.transfer_matrices.orbitals == ["2s", "2p_y", "2p_z", "2p_x"] - - assert isinstance(self.transfer_matrices.matrices["1"], dict) - - assert "1" in self.transfer_matrices.matrices - assert len(self.transfer_matrices.matrices) == 1 - - for spin in self.transfer_matrices.matrices["1"]: - assert spin in [Spin.up, Spin.down] - assert isinstance(self.transfer_matrices.matrices["1"][spin], np.ndarray) - assert self.transfer_matrices.matrices["1"][spin].shape == (4, 4) - - assert self.coeff_matrices.efermi is None - assert self.coeff_matrices.centers == ["Si1", "Si1", "Si1", "Si1"] - assert self.coeff_matrices.orbitals == ["3s", "3p_y", "3p_z", "3p_x"] - - assert isinstance(self.coeff_matrices.matrices["1"], dict) - assert "1" in self.coeff_matrices.matrices - assert len(self.coeff_matrices.matrices) == 1 - for spin in self.coeff_matrices.matrices["1"]: - assert spin in [Spin.up, Spin.down] - assert isinstance(self.coeff_matrices.matrices["1"][spin], np.ndarray) - assert self.coeff_matrices.matrices["1"][spin].shape == (4, 4) - - -class TestPOLARIZATION(MatSciTest): - def setup_method(self) -> None: - self.polarization = POLARIZATION(filename=f"{TEST_DIR}/POLARIZATION.lobster.AlN.gz") - - def test_attributes(self): - assert self.polarization.rel_loewdin_pol_vector == { - "x": -0.0, - "y": -0.01, - "z": 45.62, - "abs": 45.62, - "unit": "uC/cm2", - } - assert self.polarization.rel_mulliken_pol_vector == { - "x": -0.0, - "y": -0.02, - "z": 56.14, - "abs": 56.14, - "unit": "uC/cm2", - } - - -class TestCOBICAR(MatSciTest): - """Tests for COBICAR class.""" - - def test_read_cobicar_spin(self): - cobicar = COBICAR(TEST_DIR + "/COBICAR.lobster.B2H6.spin") - - assert cobicar.is_spin_polarized - assert cobicar.data.shape == ( - cobicar.num_data, - cobicar.num_bonds * 2 * (len(cobicar.spins)) + 1, - ) - - for interaction in cobicar.interactions: - assert "coxx" in interaction - assert "icoxx" in interaction - - assert len(interaction["coxx"]) == 2 - - assert Spin.up in interaction["icoxx"] - assert Spin.down in interaction["icoxx"] - - assert len(cobicar.energies) == cobicar.num_data - - assert len(cobicar.get_interactions_by_properties(centers=["H4", "B1", "H7"])) == 5 - assert len(cobicar.get_interactions_by_properties(centers=["H4", "B1", "H7"], orbitals=["2s"])) == 1 - - assert cobicar.get_data_by_properties( - centers=["H4", "B1", "H7"], - orbitals=["2s"], - spins=[Spin.up, Spin.down], - ).shape == (cobicar.num_data, 4) - - assert cobicar.get_data_by_properties( - centers=["H4", "B1", "H7"], - orbitals=["2s"], - spins=[Spin.down], - data_type="icoxx", - ).shape == (cobicar.num_data, 1) - - def test_read_cobicar_4_centers(self): - cobicar = COBICAR(TEST_DIR + "/COBICAR.lobster.GeTe_4center") - - assert len(cobicar.get_interactions_by_properties(centers=["Ge", "Ge", "T", "Te"])) == 1 - - def test_read_cobicar_4_centers_orbital_resolved(self, tmp_path): - cobicar = COBICAR(TEST_DIR + "/COBICAR.lobster.GeTe.multi.orbitalwise") - - assert ( - len( - cobicar.get_interactions_by_properties( - centers=["Ge", "Ge", "T", "Te"], - orbitals=["5p_z", "4p_z", "5p_z", "p_x"], - ) - ) - == 2 - ) - - assert len(cobicar.get_interactions_by_properties(indices=[13])) == 257 - assert len(cobicar.get_interactions_by_properties(orbitals=["2p_x"])) == 0 - assert len(cobicar.get_interactions_by_properties(cells=[[1, 0, 0]])) == 257 - - interactions = cobicar.get_interaction_indices_by_properties( - centers=["Ge", "Ge", "T", "Te"], - orbitals=["5p_z", "4p_z", "5p_z", "p_x"], - ) - - assert len(interactions) == 2 - assert cobicar.interactions[interactions[0]]["centers"] == [ - "Te8", - "Ge1", - "Te8", - "Ge1", - ] - assert cobicar.interactions[interactions[0]]["orbitals"] == [ - "5p_z", - "4p_z", - "5p_z", - "4p_x", - ] - - data_indices = cobicar.interaction_indices_to_data_indices_mapping(interactions) - - assert len(data_indices) == 4 - assert data_indices[0] == interactions[0] * 2 + 1 - assert data_indices[1] == interactions[0] * 2 + 2 - assert data_indices[2] == interactions[1] * 2 + 1 - assert data_indices[3] == interactions[1] * 2 + 2 - - cobicar.save(f"{tmp_path}/cobicar.json") - cobicar_from_json = COBICAR.load(f"{tmp_path}/cobicar.json") - - assert cobicar_from_json.is_spin_polarized == cobicar.is_spin_polarized - assert cobicar_from_json.num_data == cobicar.num_data - assert cobicar_from_json.num_bonds == cobicar.num_bonds - assert len(cobicar_from_json.interactions) == len(cobicar.interactions) - - for interaction1, interaction2 in zip(cobicar_from_json.interactions, cobicar.interactions, strict=True): - assert interaction1["centers"] == interaction2["centers"] - assert interaction1["orbitals"] == interaction2["orbitals"] - assert_allclose(interaction1["coxx"][Spin.up], interaction2["coxx"][Spin.up]) - assert_allclose(interaction1["icoxx"][Spin.up], interaction2["icoxx"][Spin.up]) - - assert_allclose(cobicar_from_json.data, cobicar.data) - - ya_cobicar = COBICAR.from_dict(cobicar.as_dict()) - - for interaction1, interaction2 in zip(ya_cobicar.interactions, cobicar_from_json.interactions, strict=True): - assert interaction1["centers"] == interaction2["centers"] - assert interaction1["orbitals"] == interaction2["orbitals"] - assert_allclose(interaction1["coxx"][Spin.up], interaction2["coxx"][Spin.up]) - assert_allclose(interaction1["icoxx"][Spin.up], interaction2["icoxx"][Spin.up]) - - assert_allclose(ya_cobicar.data, cobicar_from_json.data) - - -class TestCOHPCAR(MatSciTest): - """Tests for COHPCAR class.""" - - def test_read_cobicar_lcfo(self): - cohpcar = COBICAR_LCFO(TEST_DIR + "/COHPCAR.LCFO.lobster.NaCl.gz") - - assert cohpcar.is_lcfo - - assert cohpcar.interactions[2]["centers"] == ["NaCl_1", "NaCl_1"] - assert cohpcar.interactions[2]["orbitals"] == ["1a1", "1a1"] - assert cohpcar.interactions[2]["length"] == approx(2.8473125412856759) - - assert cohpcar.data.shape == (cohpcar.num_data, cohpcar.num_bonds * 2 + 1) - - assert cohpcar.interactions[-1]["centers"] == ["Na2_2", "Cl_6"] - assert cohpcar.interactions[-1]["orbitals"] == ["2a1u", "3p_x"] - - assert ( - len( - cohpcar.get_interactions_by_properties( - centers=["NaCl_1", "Na2_2"], - orbitals=["a1", "a1"], - ) - ) - == 48 - ) - - -class TestCOOPCAR(MatSciTest): - """Tests for COOPCAR class.""" - - def test_coopcar(self): - coopcar = COOPCAR(TEST_DIR + "/COOPCAR.lobster.gz") - - assert coopcar.is_spin_polarized - assert coopcar.data.shape == ( - coopcar.num_data, - coopcar.num_bonds * 2 * (len(coopcar.spins)) + 1, - ) - - for interaction in coopcar.interactions: - assert "coxx" in interaction - assert "icoxx" in interaction - - assert len(interaction["coxx"]) == 2 - - assert Spin.up in interaction["icoxx"] - assert Spin.down in interaction["icoxx"] - - assert len(coopcar.energies) == coopcar.num_data - - assert len(coopcar.get_interactions_by_properties(centers=["Fe8", "Fe7"])) == 1 - assert coopcar.get_data_by_properties(centers=["Fe8", "Fe9"])[0, -1] == approx(-0.00099) - - def test_coopcar_2(self): - coopcar = COOPCAR(TEST_DIR + "/COOPCAR.lobster.BiSe.gz") - - assert not coopcar.is_spin_polarized - assert coopcar.data.shape == (coopcar.num_data, coopcar.num_bonds * 2 + 1) - - def test_coopcar_3(self): - coopcar = COOPCAR(TEST_DIR + "/COOPCAR.lobster.KF.gz") - - assert not coopcar.is_spin_polarized - assert coopcar.energies.shape == (coopcar.num_data,) - - assert coopcar.data.shape == (coopcar.num_data, coopcar.num_bonds * 2 + 1) - - assert coopcar.interactions[0]["centers"] == ["Average"] - assert coopcar.interactions[1]["centers"] == ["F1", "K2"] - - -class TestNcICOBILIST(MatSciTest): - """Tests for NcICOBILIST class.""" - - def test_ncicobilist(self): - ncicobi = NcICOBILIST(filename=f"{TEST_DIR}/NcICOBILIST.lobster.nospin.withoutorbitals") - - assert len(ncicobi.spins) == 1 - assert ncicobi.interactions[0]["centers"] == ["X1", "X20"] - assert ncicobi.interactions[0]["orbitals"] == [None, None] - assert ncicobi.interactions[0]["icoxx"][Spin.up] == approx(0) - - with pytest.raises(KeyError): - ncicobi.interactions[0]["icoxx"][Spin.down] - - assert ncicobi.data.shape == (2, 1) - - assert ncicobi.get_interactions_by_properties( - centers=["X22"], - )[0]["icoxx"][Spin.up] == approx(0.00018) - - assert ncicobi.get_data_by_properties( - centers=["X22"], - spins=[Spin.up], - ) == approx(0.00018) - - def test_ncicobilist_spin(self): - ncicobi = NcICOBILIST(filename=f"{TEST_DIR}/NcICOBILIST.lobster") - - assert len(ncicobi.spins) == 2 - - assert ncicobi.interactions[0]["centers"] == ["X1", "X20"] - assert ncicobi.interactions[0]["orbitals"] == [None, None] - assert ncicobi.interactions[1]["icoxx"][Spin.up] == approx(0.00009) - assert ncicobi.interactions[1]["icoxx"][Spin.down] == approx(0.00009) - - assert ncicobi.data.shape == (24, 2) - - interaction = ncicobi.get_interactions_by_properties(orbitals=["4d_x^2-y^2", "4d_x^2-y^2"]) - - assert len(interaction) == 1 - assert interaction[0]["index"] == 2 - assert interaction[0]["centers"] == ["X22", "Xs42", "X31"] - assert interaction[0]["orbitals"] == ["4d_x^2-y^2", "3p", "4d_x^2-y^2"] - - for interaction in ncicobi.interactions: - assert "icoxx" in interaction - assert Spin.up in interaction["icoxx"] - assert Spin.down in interaction["icoxx"] - - assert "centers" in interaction - assert "orbitals" in interaction - assert "length" in interaction - - -class TestMsonable(MatSciTest): - def setup_method(self) -> None: - self.objects_to_test: dict[type[MSONable], str] = { - BWDF: f"{TEST_DIR}/BWDF.lobster.AlN.gz", - CHARGE: f"{TEST_DIR}/CHARGE.lobster.MnO2.gz", - CHARGE_LCFO: f"{TEST_DIR}/CHARGE.LCFO.lobster.ALN.gz", - COBICAR: f"{TEST_DIR}/COBICAR.lobster.B2H6.spin", - COBICAR_LCFO: f"{TEST_DIR}/COHPCAR.LCFO.lobster.NaCl.gz", - COOPCAR: f"{TEST_DIR}/COOPCAR.lobster.gz", - GROSSPOP: f"{TEST_DIR}/GROSSPOP.lobster", - GROSSPOP_LCFO: f"{TEST_DIR}/GROSSPOP.LCFO.lobster.AlN.gz", - ICOHPLIST: f"{TEST_DIR}/ICOHPLIST_511_sp.lobster.AlN.gz", - POLARIZATION: f"{TEST_DIR}/POLARIZATION.lobster.AlN.gz", - BandOverlaps: f"{TEST_DIR}/bandOverlaps.lobster.new.1", - Fatband: f"{TEST_DIR}/Fatband_SiO2/Test_p/FATBAND_o4_2p.lobster", - LobsterOut: f"{TEST_DIR}/lobsterout.normal", - MadelungEnergies: f"{TEST_DIR}/MadelungEnergies.lobster.perovskite", - NcICOBILIST: f"{TEST_DIR}/NcICOBILIST.lobster.nospin.withoutorbitals", - SitePotentials: f"{TEST_DIR}/SitePotentials.lobster.perovskite", - } - self.instances_to_test = [ - DOSCAR( - filename=f"{VASP_OUT_DIR}/DOSCAR.lobster.spin", - ), - DOSCAR_LCFO( - filename=f"{VASP_OUT_DIR}/DOSCAR.LCFO.lobster.AlN", - ), - LobsterMatrices( - filename=f"{TEST_DIR}/Na_hamiltonMatrices.lobster.gz", - efermi=-2.79650354, - ), - Wavefunction( - filename=f"{TEST_DIR}/LCAOWaveFunctionAfterLSO1PlotOfSpin1Kpoint1band1.gz", - structure=Structure.from_file(f"{TEST_DIR}/POSCAR_O.gz"), - ), - ] - - def test_json_save_load(self, tmp_path): - """Tests saving and loading of all MSONable classes in this package.""" - - def check_msonability(instance: MSONable) -> None: - instance.save(f"{tmp_path}/{instance.__class__.__name__.lower()}.json") - instance_from_json = instance.load(f"{tmp_path}/{instance.__class__.__name__.lower()}.json") - - json1 = json.dumps(instance.as_dict(), cls=MontyEncoder, sort_keys=True) - json2 = json.dumps(instance_from_json.as_dict(), cls=MontyEncoder, sort_keys=True) - assert json1 == json2 - - for attr_name in vars(instance): - assert hasattr(instance_from_json, attr_name) - - for obj, file_path in self.objects_to_test.items(): - instance: MSONable = obj(filename=file_path) - - check_msonability(instance) - - for instance in self.instances_to_test: - check_msonability(instance) - - def test_jsanitize_from_dict(self): - def _equals(a, b) -> bool: - if type(a) is not type(b): - return False - - if isinstance(a, np.ndarray): - return np.array_equal(a, b) - - if isinstance(a, float): - return np.isclose(a, b, atol=1e-4) - - if isinstance(a, dict): - if a.keys() != b.keys(): - return False - return all(_equals(a[k], b[k]) for k in a) - - if isinstance(a, (list, tuple)): - if len(a) != len(b): - return False - return all(_equals(x, y) for x, y in zip(a, b, strict=True)) - - return type(a) is type(b) - - for obj, file_path in self.objects_to_test.items(): - instance: MSONable = obj(filename=file_path) - - jsonable_data = jsanitize(instance, strict=True, allow_bson=True) - - instance_from_dict = obj.from_dict(jsonable_data) - - for attr_name in vars(instance): - assert hasattr(instance_from_dict, attr_name) - - original = getattr(instance, attr_name) - restored = getattr(instance_from_dict, attr_name) - - assert _equals(restored, original) - - for instance in self.instances_to_test: - jsonable_data = jsanitize(instance, strict=True, allow_bson=True) - - instance_from_dict = instance.from_dict(jsonable_data) - - for attr_name in vars(instance): - assert hasattr(instance_from_dict, attr_name) - - original = getattr(instance, attr_name) - restored = getattr(instance_from_dict, attr_name) - - assert _equals(restored, original) From bf4e8c0ff5716ca429fcb948561a2db2156bd646 Mon Sep 17 00:00:00 2001 From: naik-aakash Date: Mon, 9 Mar 2026 17:13:06 +0100 Subject: [PATCH 180/180] Add cohp.py tests from pymatgen repo with full history --- tests/electronic_structure/test_cohp.py | 1527 +++++++++++++++++++++++ 1 file changed, 1527 insertions(+) create mode 100644 tests/electronic_structure/test_cohp.py diff --git a/tests/electronic_structure/test_cohp.py b/tests/electronic_structure/test_cohp.py new file mode 100644 index 0000000000..dd397320e2 --- /dev/null +++ b/tests/electronic_structure/test_cohp.py @@ -0,0 +1,1527 @@ +from __future__ import annotations + +import orjson +import pytest +from numpy.testing import assert_allclose +from pytest import approx + +from pymatgen.electronic_structure.cohp import ( + Cohp, + CompleteCohp, + IcohpCollection, + IcohpValue, + get_integrated_cohp_in_energy_range, +) +from pymatgen.electronic_structure.core import Orbital, Spin +from pymatgen.util.testing import TEST_FILES_DIR, MatSciTest + +TEST_DIR = f"{TEST_FILES_DIR}/electronic_structure/cohp" + + +class TestCohp: + def setup_method(self): + with open(f"{TEST_DIR}/cohp.json", "rb") as file: + self.cohp = Cohp.from_dict(orjson.loads(file.read())) + self.cohp_only = Cohp(self.cohp.efermi, self.cohp.energies, self.cohp.cohp) + with open(f"{TEST_DIR}/coop.json", "rb") as file: + self.coop = Cohp.from_dict(orjson.loads(file.read())) + with open(f"{TEST_DIR}/cobi.json", "rb") as file: + self.cobi = Cohp.from_dict(orjson.loads(file.read())) + + def test_as_from_dict(self): + with open(f"{TEST_DIR}/cohp.json", "rb") as file: + cohp_dict = orjson.loads(file.read()) + assert self.cohp.as_dict() == cohp_dict + + with open(f"{TEST_DIR}/cobi.json", "rb") as file: + cobi_dict = orjson.loads(file.read()) + assert self.cobi.as_dict() == cobi_dict + + def test_attributes(self): + assert len(self.cohp.energies) == 301 + assert self.cohp.efermi == approx(9.75576) + assert self.coop.efermi == approx(5.90043) + assert not self.cohp.are_coops + assert self.coop.are_coops + assert not self.coop.are_cobis + assert not self.cobi.are_coops + assert self.cobi.are_cobis + + def test_get_icohp(self): + assert self.cohp.get_icohp() == self.cohp.get_cohp(integrated=True) + assert self.cohp_only.get_icohp() is None + + def test_get_interpolated_value(self): + # icohp_ef are the ICHOP(Ef) values taken from + # the ICOHPLIST.lobster file. + icohp_ef_dict = {Spin.up: -0.10218, Spin.down: -0.19701} + icoop_ef_dict = {Spin.up: 0.24714} + icohp_ef = self.cohp.get_interpolated_value(self.cohp.efermi, integrated=True) + icoop_ef = self.coop.get_interpolated_value(self.coop.efermi, integrated=True) + assert icohp_ef_dict == approx(icohp_ef) + assert icoop_ef_dict == approx(icoop_ef) + with pytest.raises(ValueError, match="ICOHP is empty"): + self.cohp_only.get_interpolated_value(5.0, integrated=True) + + def test_str(self): + header = "#Energy COOPUp ICOOPUp \n" + + with open(f"{TEST_DIR}/cohp.str", encoding="utf-8") as file: + str_cohp = file.read() + assert str(self.cohp) == str_cohp + assert str(self.coop).strip().startswith(header) + + with open(f"{TEST_DIR}/coop.str", encoding="utf-8") as file: + str_coop = file.read() + assert str(self.coop) == str_coop + assert str(self.coop).strip().startswith(header) + + def test_antibnd_states_below_efermi(self): + assert self.cohp.has_antibnd_states_below_efermi(spin=None) == { + Spin.up: True, + Spin.down: True, + } + assert self.cohp.has_antibnd_states_below_efermi(spin=None, limit=0.5) == { + Spin.up: False, + Spin.down: False, + } + assert self.cohp.has_antibnd_states_below_efermi(spin=Spin.up, limit=0.5) == {Spin.up: False} + + +class TestIcohpValue: + def setup_method(self): + # without spin polarization + label = "1" + atom1 = "K1" + atom2 = "F2" + length = "2.3" + translation = [-1, 0, 0] + num = 1 + icohp = {Spin.up: -2.0} + are_coops = False + self.icohpvalue = IcohpValue( + label=label, + atom1=atom1, + atom2=atom2, + length=length, + translation=translation, + num=num, + icohp=icohp, + are_coops=are_coops, + ) + + label_sp = "1" + atom1_sp = "K1" + atom2_sp = "F2" + length_sp = "2.3" + translation_sp = [-1, 0, 0] + num_sp = 1 + icohp_sp = {Spin.up: -1.1, Spin.down: -1.0} + are_coops_sp = False + self.icohpvalue_sp = IcohpValue( + label=label_sp, + atom1=atom1_sp, + atom2=atom2_sp, + length=length_sp, + translation=translation_sp, + num=num_sp, + icohp=icohp_sp, + are_coops=are_coops_sp, + ) + + def test_attributes(self): + # without spin polarization + assert self.icohpvalue_sp.num_bonds == 1 + assert self.icohpvalue_sp.are_coops is False + assert self.icohpvalue_sp.is_spin_polarized + assert self.icohpvalue.icohp == approx({Spin.up: -2.0}) + + # with spin polarization + assert self.icohpvalue_sp.num_bonds == 1 + assert self.icohpvalue_sp.are_coops is False + assert self.icohpvalue_sp.is_spin_polarized + assert self.icohpvalue_sp.icohp == approx({Spin.up: -1.1, Spin.down: -1.0}) + + def test_icohpvalue(self): + # without spin polarization + assert self.icohpvalue.icohpvalue(spin=Spin.up) == approx(-2.0) + + # with spin polarization + assert self.icohpvalue_sp.icohpvalue(spin=Spin.up) == approx(-1.1) + assert self.icohpvalue_sp.icohpvalue(spin=Spin.down) == approx(-1.0) + + def test_summed_icohp(self): + # without spin polarization + assert self.icohpvalue.summed_icohp == approx(-2.0) + + # with spin polarization + assert self.icohpvalue_sp.summed_icohp == approx(-2.1) + + def test_str(self): + # without spin polarization + assert str(self.icohpvalue) == "ICOHP 1 between K1 and F2 ([-1, 0, 0]): -2.0 eV (Spin up)" + + # with spin polarization + expected = "ICOHP 1 between K1 and F2 ([-1, 0, 0]): -1.1 eV (Spin up) and -1.0 eV (Spin down)" + assert str(self.icohpvalue_sp) == expected + + +class TestCombinedIcohp: + def setup_method(self): + # without spin polarization: + are_coops = are_cobis = is_spin_polarized = False + list_atom2 = ["K2", "K2", "K2", "K2", "K2", "K2"] + list_icohp = [ + {Spin.up: -0.40075}, + {Spin.up: -0.40074}, + {Spin.up: -0.40079}, + {Spin.up: -0.40079}, + {Spin.up: -0.40074}, + {Spin.up: -0.40075}, + ] + list_icoop = [ + {Spin.up: 0.02342}, + {Spin.up: 0.02342}, + {Spin.up: 0.02343}, + {Spin.up: 0.02343}, + {Spin.up: 0.02342}, + {Spin.up: 0.02342}, + ] + list_labels = ["1", "2", "3", "4", "5", "6"] + list_length = [2.71199, 2.71199, 2.71199, 2.71199, 2.71199, 2.71199] + list_num = [1, 1, 1, 1, 1, 1] + list_atom1 = ["F1", "F1", "F1", "F1", "F1", "F1"] + list_translation = [ + [0, -1, -1], + [-1, 0, -1], + [0, 0, -1], + [-1, -1, 0], + [0, -1, 0], + [-1, 0, 0], + ] + self.icohpcollection_KF = IcohpCollection( + is_spin_polarized=is_spin_polarized, + are_coops=are_coops, + are_cobis=are_cobis, + list_labels=list_labels, + list_atom1=list_atom1, + list_atom2=list_atom2, + list_length=list_length, + list_translation=list_translation, + list_num=list_num, + list_icohp=list_icohp, + ) + + self.icoopcollection_KF = IcohpCollection( + is_spin_polarized=is_spin_polarized, + are_coops=True, + list_labels=list_labels, + list_atom1=list_atom1, + list_atom2=list_atom2, + list_length=list_length, + list_translation=list_translation, + list_num=list_num, + list_icohp=list_icoop, + ) + self.icohpcollection_orbitalwise = IcohpCollection.from_dict( + { + "@module": "pymatgen.electronic_structure.cohp", + "@class": "IcohpCollection", + "@version": None, + "list_labels": ["1", "2"], + "list_atom1": ["O5", "O5"], + "list_atom2": ["Ta2", "Ta2"], + "list_length": [1.99474, 1.99474], + "list_translation": [[0, 0, -1], [0, 0, 0]], + "list_num": [1, 1], + "list_icohp": [ + {"1": 0.29324, "-1": 0.29324}, + {"1": 0.29324, "-1": 0.29324}, + ], + "is_spin_polarized": True, + "list_orb_icohp": [ + { + "2s-6s": { + "icohp": {"1": 0.0247, "-1": 0.0247}, + "orbitals": [[2, 0], [6, 0]], + }, + "2s-5py": { + "icohp": {"1": 8e-05, "-1": 8e-05}, + "orbitals": [[2, 0], [5, 1]], + }, + }, + { + "2s-6s": { + "icohp": {"1": 0.0247, "-1": 0.0247}, + "orbitals": [[2, 0], [6, 0]], + }, + "2s-5py": { + "icohp": {"1": 0.5, "-1": 0}, + "orbitals": [[2, 0], [5, 1]], + }, + }, + ], + "are_coops": False, + "are_cobis": True, + } + ) + # with spin polarization: + list_atom2_sp = ["Fe7", "Fe9"] + list_labels_sp = ["1", "2"] + list_translation_sp = [[0, 0, 0], [0, 0, 0]] + list_length_sp = [2.83189, 2.45249] + list_atom1_sp = ["Fe8", "Fe8"] + is_spin_polarized_sp = True + are_coops_sp = False + list_num_sp = [2, 1] + list_icohp_sp = [ + {Spin.up: -0.10218, Spin.down: -0.19701}, + {Spin.up: -0.28485, Spin.down: -0.58279}, + ] + list_icoop_sp = [ + {Spin.up: -0.11389, Spin.down: -0.20828}, + {Spin.up: -0.04087, Spin.down: -0.05756}, + ] + + self.icohpcollection_Fe = IcohpCollection( + is_spin_polarized=is_spin_polarized_sp, + are_coops=are_coops_sp, + are_cobis=False, + list_labels=list_labels_sp, + list_atom1=list_atom1_sp, + list_atom2=list_atom2_sp, + list_length=list_length_sp, + list_translation=list_translation_sp, + list_num=list_num_sp, + list_icohp=list_icohp_sp, + ) + self.icoopcollection_Fe = IcohpCollection( + is_spin_polarized=is_spin_polarized_sp, + are_coops=True, + list_labels=list_labels_sp, + list_atom1=list_atom1_sp, + list_atom2=list_atom2_sp, + list_length=list_length_sp, + list_translation=list_translation_sp, + list_num=list_num_sp, + list_icohp=list_icoop_sp, + ) + + def test_get_icohp_by_label(self): + # without spin polarization + + # ICOHPs + assert self.icohpcollection_KF.get_icohp_by_label("1") == approx(-0.40075) + assert self.icohpcollection_KF.get_icohp_by_label("2") == approx(-0.40074) + assert self.icohpcollection_KF.get_icohp_by_label("3") == approx(-0.40079) + assert self.icohpcollection_KF.get_icohp_by_label("4") == approx(-0.40079) + assert self.icohpcollection_KF.get_icohp_by_label("5") == approx(-0.40074) + assert self.icohpcollection_KF.get_icohp_by_label("6") == approx(-0.40075) + + # with spin polarization + # summed spin + # ICOHPs + assert self.icohpcollection_Fe.get_icohp_by_label("1") == approx(-0.10218 - 0.19701) + assert self.icohpcollection_Fe.get_icohp_by_label("2") == approx(-0.28485 - 0.58279) + + # Spin up + # ICOHPs + assert self.icohpcollection_Fe.get_icohp_by_label("1", summed_spin_channels=False) == approx(-0.10218) + assert self.icohpcollection_Fe.get_icohp_by_label("2", summed_spin_channels=False) == approx(-0.28485) + + # Spin down + # ICOHPs + assert self.icohpcollection_Fe.get_icohp_by_label("1", summed_spin_channels=False, spin=Spin.down) == approx( + -0.19701 + ) + assert self.icohpcollection_Fe.get_icohp_by_label("2", summed_spin_channels=False, spin=Spin.down) == approx( + -0.58279 + ) + + # orbitalwise + assert self.icohpcollection_orbitalwise.get_icohp_by_label("1", orbitals="2s-6s") == approx(0.0494) + assert self.icohpcollection_orbitalwise.get_icohp_by_label( + "1", orbitals="2s-6s", spin=Spin.up, summed_spin_channels=False + ) == approx(0.0247) + assert self.icohpcollection_orbitalwise.get_icohp_by_label( + "1", orbitals="2s-6s", spin=Spin.down, summed_spin_channels=False + ) == approx(0.0247) + assert self.icohpcollection_orbitalwise.get_icohp_by_label( + "2", orbitals="2s-5py", spin=Spin.up, summed_spin_channels=False + ) == approx(0.5) + + def test_get_summed_icohp_by_label_list(self): + # without spin polarization + assert self.icohpcollection_KF.get_summed_icohp_by_label_list( + ["1", "2", "3", "4", "5", "6"], divisor=6.0 + ) == approx(-0.40076) + + # with spin polarization + sum1 = (-0.10218 - 0.19701 - 0.28485 - 0.58279) / 2.0 + sum2 = (-0.10218 - 0.28485) / 2.0 + sum3 = (-0.19701 - 0.58279) / 2.0 + assert self.icohpcollection_Fe.get_summed_icohp_by_label_list(["1", "2"], divisor=2.0) == approx(sum1) + assert self.icohpcollection_Fe.get_summed_icohp_by_label_list( + ["1", "2"], summed_spin_channels=False, divisor=2.0 + ) == approx(sum2) + assert self.icohpcollection_Fe.get_summed_icohp_by_label_list( + ["1", "2"], summed_spin_channels=False, spin=Spin.down, divisor=2.0 + ) == approx(sum3) + + def test_get_icohp_dict_by_bondlengths(self): + # without spin polarization + icohpvalue = {} + icohpvalue["1"] = { + "@module": "pymatgen.electronic_structure.cohp", + "num": 1, + "length": 2.71199, + "icohp": {Spin.up: -0.40075}, + "are_coops": False, + "are_cobis": False, + "label": "1", + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "translation": [0, -1, -1], + "orbitals": None, + } + icohpvalue["2"] = { + "@module": "pymatgen.electronic_structure.cohp", + "num": 1, + "length": 2.71199, + "icohp": {Spin.up: -0.40074}, + "are_coops": False, + "are_cobis": False, + "label": "2", + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "translation": [-1, 0, -1], + "orbitals": None, + } + icohpvalue["3"] = { + "@module": "pymatgen.electronic_structure.cohp", + "num": 1, + "length": 2.71199, + "icohp": {Spin.up: -0.40079}, + "are_coops": False, + "are_cobis": False, + "label": "3", + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "translation": [0, 0, -1], + "orbitals": None, + } + icohpvalue["4"] = { + "@module": "pymatgen.electronic_structure.cohp", + "num": 1, + "length": 2.71199, + "icohp": {Spin.up: -0.40079}, + "are_coops": False, + "are_cobis": False, + "label": "4", + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "translation": [-1, -1, 0], + "orbitals": None, + } + icohpvalue["5"] = { + "@module": "pymatgen.electronic_structure.cohp", + "num": 1, + "length": 2.71199, + "icohp": {Spin.up: -0.40074}, + "are_coops": False, + "are_cobis": False, + "label": "5", + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "translation": [0, -1, 0], + "orbitals": None, + } + icohpvalue["6"] = { + "@module": "pymatgen.electronic_structure.cohp", + "num": 1, + "length": 2.71199, + "icohp": {Spin.up: -0.40075}, + "are_coops": False, + "are_cobis": False, + "label": "6", + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "translation": [-1, 0, 0], + "orbitals": None, + } + + dict_KF = self.icohpcollection_KF.get_icohp_dict_by_bondlengths(minbondlength=0.0, maxbondlength=8.0) + for key, value in sorted(dict_KF.items()): + v = value.as_dict() + if "@version" in v: + v.pop("@version") + assert v == icohpvalue[key] + + assert self.icohpcollection_KF.get_icohp_dict_by_bondlengths(minbondlength=0.0, maxbondlength=1.0) == {} + + # with spin polarization + icohpvalue_spin = {} + icohpvalue_spin["1"] = { + "num": 2, + "atom2": "Fe7", + "translation": [0, 0, 0], + "@module": "pymatgen.electronic_structure.cohp", + "are_coops": False, + "are_cobis": False, + "atom1": "Fe8", + "label": "1", + "length": 2.83189, + "@class": "IcohpValue", + "icohp": {Spin.up: -0.10218, Spin.down: -0.19701}, + "orbitals": None, + } + icohpvalue_spin["2"] = { + "num": 1, + "atom2": "Fe9", + "translation": [0, 0, 0], + "@module": "pymatgen.electronic_structure.cohp", + "are_coops": False, + "are_cobis": False, + "atom1": "Fe8", + "label": "2", + "length": 2.45249, + "@class": "IcohpValue", + "icohp": {Spin.up: -0.28485, Spin.down: -0.58279}, + "orbitals": None, + } + + dict_Fe = self.icohpcollection_Fe.get_icohp_dict_by_bondlengths(minbondlength=0.0, maxbondlength=8.0) + for key, value in sorted(dict_Fe.items()): + v = value.as_dict() + if "@version" in v: + v.pop("@version") + assert v == icohpvalue_spin[key] + + dict_Fe2 = self.icohpcollection_Fe.get_icohp_dict_by_bondlengths(minbondlength=2.5, maxbondlength=2.9) + assert len(dict_Fe2) == 1 + for key, value in sorted(dict_Fe2.items()): + v = value.as_dict() + if "@version" in v: + v.pop("@version") + assert v == icohpvalue_spin[key] + + def test_get_icohp_dict_of_site(self): + # without spin polarization + icohpvalue = {} + icohpvalue["1"] = { + "translation": [0, -1, -1], + "are_coops": False, + "are_cobis": False, + "@module": "pymatgen.electronic_structure.cohp", + "length": 2.71199, + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "num": 1, + "label": "1", + "icohp": {Spin.up: -0.40075}, + "orbitals": None, + } + icohpvalue["2"] = { + "translation": [-1, 0, -1], + "are_coops": False, + "are_cobis": False, + "@module": "pymatgen.electronic_structure.cohp", + "length": 2.71199, + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "num": 1, + "label": "2", + "icohp": {Spin.up: -0.40074}, + "orbitals": None, + } + icohpvalue["3"] = { + "translation": [0, 0, -1], + "are_coops": False, + "are_cobis": False, + "@module": "pymatgen.electronic_structure.cohp", + "length": 2.71199, + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "num": 1, + "label": "3", + "icohp": {Spin.up: -0.40079}, + "orbitals": None, + } + icohpvalue["4"] = { + "translation": [-1, -1, 0], + "are_coops": False, + "are_cobis": False, + "@module": "pymatgen.electronic_structure.cohp", + "length": 2.71199, + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "num": 1, + "label": "4", + "icohp": {Spin.up: -0.40079}, + "orbitals": None, + } + icohpvalue["5"] = { + "translation": [0, -1, 0], + "are_coops": False, + "are_cobis": False, + "@module": "pymatgen.electronic_structure.cohp", + "length": 2.71199, + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "num": 1, + "label": "5", + "icohp": {Spin.up: -0.40074}, + "orbitals": None, + } + icohpvalue["6"] = { + "translation": [-1, 0, 0], + "are_coops": False, + "are_cobis": False, + "@module": "pymatgen.electronic_structure.cohp", + "length": 2.71199, + "atom2": "K2", + "@class": "IcohpValue", + "atom1": "F1", + "num": 1, + "label": "6", + "icohp": {Spin.up: -0.40075}, + "orbitals": None, + } + + dict_KF = self.icohpcollection_KF.get_icohp_dict_of_site(site=0) + + for key, value in sorted(dict_KF.items()): + v = value.as_dict() + if "@version" in v: + v.pop("@version") + assert v == icohpvalue[key] + + # compare number of results dependent on minsummedicohp, maxsummedicohp,minbondlength, maxbondlength, and + # only_bonds_to + dict_KF_2 = self.icohpcollection_KF.get_icohp_dict_of_site( + site=0, + minsummedicohp=None, + maxsummedicohp=-0.0, + minbondlength=0.0, + maxbondlength=8.0, + ) + dict_KF_3 = self.icohpcollection_KF.get_icohp_dict_of_site( + site=0, + minsummedicohp=None, + maxsummedicohp=-0.5, + minbondlength=0.0, + maxbondlength=8.0, + ) + dict_KF_4 = self.icohpcollection_KF.get_icohp_dict_of_site( + site=0, + minsummedicohp=0.0, + maxsummedicohp=None, + minbondlength=0.0, + maxbondlength=8.0, + ) + dict_KF_5 = self.icohpcollection_KF.get_icohp_dict_of_site( + site=0, + minsummedicohp=None, + maxsummedicohp=None, + minbondlength=0.0, + maxbondlength=2.0, + ) + dict_KF_6 = self.icohpcollection_KF.get_icohp_dict_of_site( + site=0, + minsummedicohp=None, + maxsummedicohp=None, + minbondlength=3.0, + maxbondlength=8.0, + ) + dict_KF_7 = self.icohpcollection_KF.get_icohp_dict_of_site(site=0, only_bonds_to=["K"]) + dict_KF_8 = self.icohpcollection_KF.get_icohp_dict_of_site(site=1, only_bonds_to=["K"]) + dict_KF_9 = self.icohpcollection_KF.get_icohp_dict_of_site(site=1, only_bonds_to=["F"]) + + assert len(dict_KF_2) == 6 + assert len(dict_KF_3) == 0 + assert len(dict_KF_4) == 0 + assert len(dict_KF_5) == 0 + assert len(dict_KF_6) == 0 + assert len(dict_KF_7) == 6 + assert len(dict_KF_8) == 0 + assert len(dict_KF_9) == 6 + + # spin polarization + + dict_Fe = self.icohpcollection_Fe.get_icohp_dict_of_site(site=0) + assert len(dict_Fe) == 0 + + # Fe8 + dict_Fe2 = self.icohpcollection_Fe.get_icohp_dict_of_site(site=7) + assert len(dict_Fe2) == 2 + # Test the values + + icohplist_Fe = {} + icohplist_Fe["1"] = { + "are_coops": False, + "are_cobis": False, + "translation": [0, 0, 0], + "icohp": {Spin.down: -0.19701, Spin.up: -0.10218}, + "length": 2.83189, + "@module": "pymatgen.electronic_structure.cohp", + "atom1": "Fe8", + "atom2": "Fe7", + "label": "1", + "orbitals": None, + "@class": "IcohpValue", + "num": 2, + } + icohplist_Fe["2"] = { + "are_coops": False, + "are_cobis": False, + "translation": [0, 0, 0], + "icohp": {Spin.down: -0.58279, Spin.up: -0.28485}, + "length": 2.45249, + "@module": "pymatgen.electronic_structure.cohp", + "atom1": "Fe8", + "atom2": "Fe9", + "label": "2", + "orbitals": None, + "@class": "IcohpValue", + "num": 1, + } + + for key, value in sorted(dict_Fe2.items()): + v = value.as_dict() + if "@version" in v: + v.pop("@version") + assert v == icohplist_Fe[key] + + # Fe9 + dict_Fe3 = self.icohpcollection_Fe.get_icohp_dict_of_site(site=8) + assert len(dict_Fe3) == 1 + + # compare number of results dependent on minsummedicohp, maxsummedicohp,minbondlength, maxbondlength + # Fe8 + dict_Fe4 = self.icohpcollection_Fe.get_icohp_dict_of_site( + site=7, + minsummedicohp=-0.3, + maxsummedicohp=None, + minbondlength=0.0, + maxbondlength=8.0, + ) + assert len(dict_Fe4) == 1 + values = list(dict_Fe4.values()) + v = values[0].as_dict() + if "@version" in v: + v.pop("@version") + assert v == icohplist_Fe["1"] + + dict_Fe5 = self.icohpcollection_Fe.get_icohp_dict_of_site( + site=7, + minsummedicohp=None, + maxsummedicohp=-0.3, + minbondlength=0.0, + maxbondlength=8.0, + ) + assert len(dict_Fe5) == 1 + values = list(dict_Fe5.values()) + v = values[0].as_dict() + if "@version" in v: + v.pop("@version") + assert v == icohplist_Fe["2"] + + dict_Fe6 = self.icohpcollection_Fe.get_icohp_dict_of_site( + site=7, + minsummedicohp=None, + maxsummedicohp=None, + minbondlength=0.0, + maxbondlength=2.5, + ) + + assert len(dict_Fe6) == 1 + values = list(dict_Fe6.values()) + v = values[0].as_dict() + if "@version" in v: + v.pop("@version") + assert v == icohplist_Fe["2"] + + dict_Fe7 = self.icohpcollection_Fe.get_icohp_dict_of_site( + site=7, + minsummedicohp=None, + maxsummedicohp=None, + minbondlength=2.5, + maxbondlength=8.0, + ) + assert len(dict_Fe7) == 1 + values = list(dict_Fe7.values()) + v = values[0].as_dict() + if "@version" in v: + v.pop("@version") + assert v == icohplist_Fe["1"] + + def test_extremum_icohpvalue(self): + # without spin polarization + # ICOHPs + assert self.icohpcollection_KF.extremum_icohpvalue() == approx(-0.40079) + # ICOOPs + assert self.icoopcollection_KF.extremum_icohpvalue() == approx(0.02343) + # with spin polarization + # summed spin + # ICOHPs + assert self.icohpcollection_Fe.extremum_icohpvalue() == approx(-0.86764) + assert self.icoopcollection_Fe.extremum_icohpvalue() == approx(-0.09842999999999999) + # ICOOPs + # spin up + # ICOHPs + assert self.icohpcollection_Fe.extremum_icohpvalue(summed_spin_channels=False) == approx(-0.28485) + # ICOOPs + assert self.icoopcollection_Fe.extremum_icohpvalue(summed_spin_channels=False) == approx(-0.04087) + # spin down + # ICOHPs + assert self.icohpcollection_Fe.extremum_icohpvalue(summed_spin_channels=False, spin=Spin.down) == approx( + -0.58279 + ) + # ICOOPs + assert self.icoopcollection_Fe.extremum_icohpvalue(summed_spin_channels=False, spin=Spin.down) == approx( + -0.05756 + ) + + +class TestCompleteCohp(MatSciTest): + def setup_method(self): + filepath = f"{TEST_DIR}/complete_cohp_lobster.json" + with open(filepath, "rb") as file: + self.cohp_lobster_dict = CompleteCohp.from_dict(orjson.loads(file.read())) + filepath = f"{TEST_DIR}/complete_coop_lobster.json" + with open(filepath, "rb") as file: + self.coop_lobster_dict = CompleteCohp.from_dict(orjson.loads(file.read())) + filepath = f"{TEST_DIR}/complete_cohp_lmto.json" + with open(filepath, "rb") as file: + self.cohp_lmto_dict = CompleteCohp.from_dict(orjson.loads(file.read())) + filepath = f"{TEST_DIR}/complete_cohp_orbitalwise.json" + with open(filepath, "rb") as file: + self.cohp_orb_dict = CompleteCohp.from_dict(orjson.loads(file.read())) + # Lobster 3.0 + filepath = f"{TEST_DIR}/complete_cohp_forb.json" + with open(filepath, "rb") as file: + self.cohp_lobster_forb_dict = CompleteCohp.from_dict(orjson.loads(file.read())) + + # Lobster 2.0 + filepath = f"{TEST_DIR}/COPL.BiSe" + structure = f"{TEST_DIR}/CTRL.BiSe" + self.cohp_lmto = CompleteCohp.from_file("lmto", filename=filepath, structure_file=structure) + filepath = f"{TEST_DIR}/COHPCAR.lobster.gz" + structure = f"{TEST_DIR}/POSCAR" + self.cohp_lobster = CompleteCohp.from_file("lobster", filename=filepath, structure_file=structure) + # with open(f"{TEST_DIR}/complete_cohp_lobster.json", "w", encoding="utf-8") as file: + # json.dump(self.cohp_lobster.as_dict(), file) + filepath = f"{TEST_DIR}/COOPCAR.lobster.BiSe.gz" + structure = f"{TEST_DIR}/POSCAR.BiSe" + self.coop_lobster = CompleteCohp.from_file( + "lobster", filename=filepath, structure_file=structure, are_coops=True + ) + filepath = f"{TEST_DIR}/COHPCAR.lobster.orbitalwise.gz" + structure = f"{TEST_DIR}/POSCAR.orbitalwise" + self.cohp_orb = CompleteCohp.from_file("lobster", filename=filepath, structure_file=structure) + # with open(f"{TEST_DIR}/complete_cohp_orbitalwise.json", "w", encoding="utf-8") as file: + # json.dump(self.cohp_orb.as_dict(), file) + filepath = f"{TEST_DIR}/COHPCAR.lobster.notot.orbitalwise.gz" + self.cohp_notot = CompleteCohp.from_file("lobster", filename=filepath, structure_file=structure) + # Lobster 3.0 + filepath = f"{TEST_DIR}/COHPCAR.lobster.Na2UO4.gz" + structure = f"{TEST_DIR}/POSCAR.Na2UO4" + self.cohp_lobster_forb = CompleteCohp.from_file("lobster", filename=filepath, structure_file=structure) + + # spinpolarized case: + filepath = f"{TEST_DIR}/environments/COHPCAR.lobster.mp-190_2.gz" + structure = f"{TEST_DIR}/environments/CONTCAR.mp-190.gz" + self.cohp_lobster_spin_polarized = CompleteCohp.from_file( + "lobster", filename=filepath, structure_file=structure + ) + # COBI + filepath = f"{TEST_DIR}/COBICAR.lobster.gz" + structure = f"{TEST_DIR}/POSCAR.COBI" + + self.cobi = CompleteCohp.from_file("lobster", filename=filepath, structure_file=structure, are_cobis=True) + + # COBI multi-center + filepath = f"{TEST_DIR}/COBICAR.lobster.GeTe.multi.orbitalwise.full" + structure = f"{TEST_DIR}/POSCAR.GeTe" + self.cobi_multi = CompleteCohp.from_file( + "lobster", + filename=filepath, + structure_file=structure, + are_multi_center_cobis=True, + ) + + # COBI multi-center + filepath = f"{TEST_DIR}/COBICAR.lobster.B2H6.spin" + structure = f"{TEST_DIR}/POSCAR.B2H6" + self.cobi_multi_B2H6 = CompleteCohp.from_file( + "lobster", + filename=filepath, + structure_file=structure, + are_multi_center_cobis=True, + ) + + # COBI multi-center + filepath = f"{TEST_DIR}/COBICAR.lobster.B2H6.spin.average.2" + structure = f"{TEST_DIR}/POSCAR.B2H6" + self.cobi_multi_B2H6_average2 = CompleteCohp.from_file( + "lobster", filename=filepath, structure_file=structure, are_cobis=True + ) + + def test_attributes(self): + assert not self.cohp_lobster.are_coops + assert not self.cohp_lobster.are_cobis + assert not self.cohp_lobster_dict.are_coops + assert not self.cohp_lmto.are_coops + assert not self.cohp_lmto_dict.are_coops + assert self.coop_lobster.are_coops + assert self.coop_lobster_dict.are_coops + assert not self.cohp_lobster_forb.are_coops + assert not self.cohp_lobster_forb_dict.are_coops + + assert len(self.cohp_lobster.energies) == 301 + assert len(self.cohp_lmto.energies) == 801 + assert len(self.coop_lobster.energies) == 241 + assert len(self.cohp_lobster_forb.energies) == 7 + + assert self.cohp_lobster.efermi == approx(9.75576) + assert self.cohp_lmto.efermi == approx(-2.3433) + assert self.coop_lobster.efermi == approx(5.90043) + assert self.cohp_lobster_forb.efermi == approx(4.12875) + + assert self.cobi.are_cobis + assert not self.cobi.are_coops + + assert self.cohp_lobster_forb.cohp[Spin.up][0] == approx(0.00000) + assert self.cohp_lobster_forb.icohp[Spin.up][0] == approx(-0.09040) + + def test_average_multi_center_cobi(self): + # tests if the averages for a mult-center cobi are computed in the same way as in Lobster + for cohp1, cohp2 in zip( + self.cobi_multi_B2H6.get_cohp_by_label("average").cohp[Spin.up], + self.cobi_multi_B2H6_average2.get_cohp_by_label("average").cohp[Spin.up], + strict=True, + ): + assert cohp1 == approx(cohp2, abs=1e-4) + + for cohp1, cohp2 in zip( + self.cobi_multi_B2H6.get_cohp_by_label("average").cohp[Spin.down], + self.cobi_multi_B2H6_average2.get_cohp_by_label("average").cohp[Spin.down], + strict=True, + ): + assert cohp1 == approx(cohp2, abs=1e-4) + + for icohp1, icohp2 in zip( + self.cobi_multi_B2H6.get_cohp_by_label("average").icohp[Spin.up], + self.cobi_multi_B2H6_average2.get_cohp_by_label("average").icohp[Spin.up], + strict=True, + ): + assert icohp1 == approx(icohp2, abs=1e-4) + + for icohp1, icohp2 in zip( + self.cobi_multi_B2H6.get_cohp_by_label("average").icohp[Spin.down], + self.cobi_multi_B2H6_average2.get_cohp_by_label("average").icohp[Spin.down], + strict=True, + ): + assert icohp1 == approx(icohp2, abs=1e-4) + + def test_dict(self): + # The JSON files are dict representations of the COHPs from the LMTO + # and LOBSTER calculations and should thus be the same. + + def is_equal(a, b): + a_dict = a.as_dict() + b_dict = b.as_dict() + del a_dict["structure"] + del b_dict["structure"] + return a_dict == b_dict and a.structure == b.structure + + assert is_equal(self.cohp_lobster, self.cohp_lobster_dict) + assert is_equal(self.cohp_orb, self.cohp_orb_dict) + # Lobster 3.0, including f orbitals + + assert is_equal(self.cohp_lobster_forb, self.cohp_lobster_forb_dict) + + # Testing the LMTO dicts will be more involved. Since the average + # is calculated and not read, there may be differences in rounding + # with a very small number of matrix elements, which would cause the + # test to fail + cohp_lmto_dict = self.cohp_lmto.as_dict() + for key in ["COHP", "ICOHP"]: + assert_allclose( + cohp_lmto_dict[key]["average"]["1"], + self.cohp_lmto_dict.as_dict()[key]["average"]["1"], + 5, + ) + # check if the same dicts are generated + cobi_new = CompleteCohp.from_dict(self.cobi_multi.as_dict()) + assert is_equal(self.cobi_multi, cobi_new) + # for key in cohp_lmto_dict: + # if key not in ["COHP", "ICOHP"]: + # assert cohp_lmto_dict[key] == self.cohp_lmto_dict.as_dict()[key] + # else: + # for bond in cohp_lmto_dict[key]: + # if bond != "average": + # assert cohp_lmto_dict[key][bond] == self.cohp_lmto_dict.as_dict()[key][bond] + + def test_icohp_values(self): + # icohp_ef are the ICHOP(Ef) values taken from + # the ICOHPLIST.lobster file. + icohp_ef_dict = { + "1": {Spin.up: -0.10218, Spin.down: -0.19701}, + "2": {Spin.up: -0.28485, Spin.down: -0.58279}, + } + all_cohps_lobster = self.cohp_lobster.all_cohps + for bond, val in icohp_ef_dict.items(): + icohp_ef = all_cohps_lobster[bond].get_interpolated_value(self.cohp_lobster.efermi, integrated=True) + assert val == icohp_ef + + icoop_ef_dict = { + "1": {Spin.up: 0.14245}, + "2": {Spin.up: -0.04118}, + "3": {Spin.up: 0.14245}, + "4": {Spin.up: -0.04118}, + "5": {Spin.up: -0.03516}, + "6": {Spin.up: 0.10745}, + "7": {Spin.up: -0.03516}, + "8": {Spin.up: 0.10745}, + "9": {Spin.up: -0.12395}, + "10": {Spin.up: 0.24714}, + "11": {Spin.up: -0.12395}, + } + all_coops_lobster = self.coop_lobster.all_cohps + for bond, val in icoop_ef_dict.items(): + icoop_ef = all_coops_lobster[bond].get_interpolated_value(self.coop_lobster.efermi, integrated=True) + assert val == icoop_ef + + def test_get_cohp_by_label(self): + assert self.cohp_orb.get_cohp_by_label("1").energies[0] == approx(-11.7225) + assert self.cohp_orb.get_cohp_by_label("1").energies[5] == approx(-11.47187) + assert not self.cohp_orb.get_cohp_by_label("1").are_coops + assert self.cohp_orb.get_cohp_by_label("1").cohp[Spin.up][0] == approx(0.0) + assert self.cohp_orb.get_cohp_by_label("1").cohp[Spin.up][300] == approx(0.03392) + assert self.cohp_orb.get_cohp_by_label("average").cohp[Spin.up][230] == approx(-0.08792) + assert self.cohp_orb.get_cohp_by_label("average").energies[230] == approx(-0.19368000000000007) + assert not self.cohp_orb.get_cohp_by_label("average").are_coops + # test methods from super class that could be overwritten + assert self.cohp_orb.get_icohp()[Spin.up][3] == approx(0.0) + assert self.cohp_orb.get_cohp()[Spin.up][3] == approx(0.0) + + def test_get_cohp_by_label_summed_spin(self): + # files without spin polarization + assert self.cohp_orb.get_cohp_by_label("1", summed_spin_channels=True).energies[0] == approx(-11.7225) + assert self.cohp_orb.get_cohp_by_label("1", summed_spin_channels=True).energies[5] == approx(-11.47187) + assert not self.cohp_orb.get_cohp_by_label("1", summed_spin_channels=True).are_coops + assert self.cohp_orb.get_cohp_by_label("1", summed_spin_channels=True).cohp[Spin.up][0] == approx(0.0) + assert self.cohp_orb.get_cohp_by_label("1", summed_spin_channels=True).cohp[Spin.up][300] == approx(0.03392) + assert self.cohp_orb.get_cohp_by_label("average", summed_spin_channels=True).cohp[Spin.up][230] == approx( + -0.08792 + ) + assert self.cohp_orb.get_cohp_by_label("average", summed_spin_channels=True).energies[230] == approx( + -0.19368000000000007 + ) + assert not self.cohp_orb.get_cohp_by_label("average", summed_spin_channels=True).are_coops + + # file with spin polarization + assert self.cohp_lobster_spin_polarized.get_cohp_by_label("1", summed_spin_channels=False).cohp[Spin.up][ + 300 + ] * 2 == approx( + self.cohp_lobster_spin_polarized.get_cohp_by_label("1", summed_spin_channels=True).cohp[Spin.up][300] + ) + assert self.cohp_lobster_spin_polarized.get_cohp_by_label("1", summed_spin_channels=False).cohp[Spin.down][ + 300 + ] * 2 == approx( + self.cohp_lobster_spin_polarized.get_cohp_by_label("1", summed_spin_channels=True).cohp[Spin.up][300] + ) + assert self.cohp_lobster_spin_polarized.get_cohp_by_label("1", summed_spin_channels=True).energies[0] == approx( + -15.03759 + 1.96204 + ) + assert self.cohp_lobster_spin_polarized.get_cohp_by_label("1", summed_spin_channels=True).energies[5] == approx( + -14.78697 + 1.96204 + ) + assert not self.cohp_lobster_spin_polarized.get_cohp_by_label("1", summed_spin_channels=True).are_coops + + def test_get_summed_cohp_by_label_list(self): + assert self.cohp_orb.get_summed_cohp_by_label_list(["1"]).energies[0] == approx(-11.7225) + assert self.cohp_orb.get_summed_cohp_by_label_list(["1", "1"]).energies[0] == approx(-11.7225) + assert self.cohp_orb.get_summed_cohp_by_label_list(["1"]).energies[5] == approx(-11.47187) + assert not self.cohp_orb.get_summed_cohp_by_label_list(["1"]).are_coops + assert self.cohp_orb.get_summed_cohp_by_label_list(["1"]).cohp[Spin.up][0] == approx(0.0) + assert self.cohp_orb.get_summed_cohp_by_label_list(["1", "1"]).cohp[Spin.up][0] == approx(0.0) + assert self.cohp_orb.get_summed_cohp_by_label_list(["1", "1"]).cohp[Spin.up][300] == approx(0.03392 * 2.0) + assert self.cohp_orb.get_summed_cohp_by_label_list(["1", "1"], divisor=2).cohp[Spin.up][300] == approx(0.03392) + + def test_get_summed_cohp_by_label_list_summed_spin(self): + # files without spin polarization + assert self.cohp_orb.get_summed_cohp_by_label_list(["1"], summed_spin_channels=True).energies[0] == approx( + -11.7225 + ) + assert self.cohp_orb.get_summed_cohp_by_label_list(["1", "1"], summed_spin_channels=True).energies[0] == approx( + -11.7225 + ) + assert self.cohp_orb.get_summed_cohp_by_label_list(["1"], summed_spin_channels=True).energies[5] == approx( + -11.47187 + ) + assert not self.cohp_orb.get_summed_cohp_by_label_list(["1"], summed_spin_channels=True).are_coops + assert self.cohp_orb.get_summed_cohp_by_label_list(["1"], summed_spin_channels=True).cohp[Spin.up][0] == approx( + 0.0 + ) + assert self.cohp_orb.get_summed_cohp_by_label_list(["1", "1"], summed_spin_channels=True).cohp[Spin.up][ + 0 + ] == approx(0.0) + assert self.cohp_orb.get_summed_cohp_by_label_list(["1", "1"], summed_spin_channels=True).cohp[Spin.up][ + 300 + ] == approx(0.03392 * 2.0) + assert self.cohp_orb.get_summed_cohp_by_label_list(["1", "1"], summed_spin_channels=True, divisor=2).cohp[ + Spin.up + ][300] == approx(0.03392) + + # file with spin polarization + assert self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_list(["1"], summed_spin_channels=False).cohp[ + Spin.up + ][300] * 2 == approx( + self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_list(["1"], summed_spin_channels=True).cohp[ + Spin.up + ][300] + ) + assert self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_list(["1"], summed_spin_channels=False).cohp[ + Spin.down + ][300] * 2 == approx( + self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_list(["1"], summed_spin_channels=True).cohp[ + Spin.up + ][300] + ) + assert self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_list( + ["1", "1"], summed_spin_channels=True + ).energies[0] == approx(-15.03759 + 1.96204) + assert self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_list( + ["1"], summed_spin_channels=True + ).energies[5] == approx(-14.78697 + 1.96204) + assert not self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_list( + ["1"], summed_spin_channels=True + ).are_coops + + def test_get_summed_cohp_by_label_and_orbital_list(self): + ref = self.cohp_orb.orb_res_cohp["1"]["4s-4px"] + ref2 = self.cohp_orb.orb_res_cohp["1"]["4px-4pz"] + cohp_label = self.cohp_orb.get_summed_cohp_by_label_and_orbital_list(["1"], ["4s-4px"]) + cohp_label2 = self.cohp_orb.get_summed_cohp_by_label_and_orbital_list(["1", "1"], ["4s-4px", "4s-4px"]) + cohp_label2x = self.cohp_orb.get_summed_cohp_by_label_and_orbital_list( + ["1", "1"], ["4s-4px", "4s-4px"], divisor=2 + ) + cohp_label3 = self.cohp_orb.get_summed_cohp_by_label_and_orbital_list(["1", "1"], ["4px-4pz", "4s-4px"]) + + assert_allclose(cohp_label.cohp[Spin.up], ref["COHP"][Spin.up]) + assert_allclose(cohp_label2.cohp[Spin.up], ref["COHP"][Spin.up] * 2.0) + assert_allclose(cohp_label3.cohp[Spin.up], ref["COHP"][Spin.up] + ref2["COHP"][Spin.up]) + assert_allclose(cohp_label.icohp[Spin.up], ref["ICOHP"][Spin.up]) + assert_allclose(cohp_label2.icohp[Spin.up], ref["ICOHP"][Spin.up] * 2.0) + assert_allclose(cohp_label2x.icohp[Spin.up], ref["ICOHP"][Spin.up]) + assert_allclose(cohp_label3.icohp[Spin.up], ref["ICOHP"][Spin.up] + ref2["ICOHP"][Spin.up]) + expected_msg = "label_list and orbital_list don't have the same length" + with pytest.raises(ValueError, match=expected_msg): + self.cohp_orb.get_summed_cohp_by_label_and_orbital_list(["1"], ["4px-4pz", "4s-4px"]) + with pytest.raises(ValueError, match=expected_msg): + self.cohp_orb.get_summed_cohp_by_label_and_orbital_list(["1", "2"], ["4s-4px"]) + + def test_get_summed_cohp_by_label_and_orbital_list_summed_spin_channels(self): + ref = self.cohp_orb.orb_res_cohp["1"]["4s-4px"] + ref2 = self.cohp_orb.orb_res_cohp["1"]["4px-4pz"] + cohp_label = self.cohp_orb.get_summed_cohp_by_label_and_orbital_list( + ["1"], ["4s-4px"], summed_spin_channels=True + ) + cohp_label2 = self.cohp_orb.get_summed_cohp_by_label_and_orbital_list( + ["1", "1"], ["4s-4px", "4s-4px"], summed_spin_channels=True + ) + cohp_label2x = self.cohp_orb.get_summed_cohp_by_label_and_orbital_list( + ["1", "1"], ["4s-4px", "4s-4px"], divisor=2, summed_spin_channels=True + ) + cohp_label3 = self.cohp_orb.get_summed_cohp_by_label_and_orbital_list( + ["1", "1"], ["4px-4pz", "4s-4px"], summed_spin_channels=True + ) + + assert_allclose(cohp_label.cohp[Spin.up], ref["COHP"][Spin.up]) + assert_allclose(cohp_label2.cohp[Spin.up], ref["COHP"][Spin.up] * 2.0) + assert_allclose(cohp_label3.cohp[Spin.up], ref["COHP"][Spin.up] + ref2["COHP"][Spin.up]) + assert_allclose(cohp_label.icohp[Spin.up], ref["ICOHP"][Spin.up]) + assert_allclose(cohp_label2.icohp[Spin.up], ref["ICOHP"][Spin.up] * 2.0) + assert_allclose(cohp_label2x.icohp[Spin.up], ref["ICOHP"][Spin.up]) + assert_allclose(cohp_label3.icohp[Spin.up], ref["ICOHP"][Spin.up] + ref2["ICOHP"][Spin.up]) + expected_msg = "label_list and orbital_list don't have the same length" + with pytest.raises(ValueError, match=expected_msg): + self.cohp_orb.get_summed_cohp_by_label_and_orbital_list( + ["1"], ["4px-4pz", "4s-4px"], summed_spin_channels=True + ) + with pytest.raises(ValueError, match=expected_msg): + self.cohp_orb.get_summed_cohp_by_label_and_orbital_list(["1", "2"], ["4s-4px"], summed_spin_channels=True) + + # files with spin polarization + assert self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_and_orbital_list( + ["1"], ["6s-6s"], summed_spin_channels=False + ).cohp[Spin.up][300] * 2 == approx( + self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_and_orbital_list( + ["1"], ["6s-6s"], summed_spin_channels=True + ).cohp[Spin.up][300] + ) + assert self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_and_orbital_list( + ["1"], ["6s-6s"], summed_spin_channels=False + ).cohp[Spin.down][300] * 2 == approx( + self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_and_orbital_list( + ["1"], ["6s-6s"], summed_spin_channels=True + ).cohp[Spin.up][300] + ) + assert self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_and_orbital_list( + ["1"], ["6s-6s"], summed_spin_channels=True + ).energies[0] == approx(-15.03759 + 1.96204) + assert self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_and_orbital_list( + ["1"], ["6s-6s"], summed_spin_channels=True + ).energies[5] == approx(-14.78697 + 1.96204) + assert not self.cohp_lobster_spin_polarized.get_summed_cohp_by_label_and_orbital_list( + ["1"], ["6s-6s"], summed_spin_channels=True + ).are_coops + + def test_orbital_resolved_cohp(self): + # When read from a COHPCAR file, total COHPs are calculated from + # the orbital-resolved COHPs if the total is missing. This may be + # case for LOBSTER version 2.2.0 and earlier due to a bug with the + # cohpgenerator keyword. The calculated total should be approximately + # the total COHP calculated by LOBSTER. Due to numerical errors in + # the LOBSTER calculation, the precision is not very high though. + + assert_allclose( + self.cohp_orb.all_cohps["1"].cohp[Spin.up], + self.cohp_notot.all_cohps["1"].cohp[Spin.up], + atol=1e-3, + ) + assert_allclose( + self.cohp_orb.all_cohps["1"].icohp[Spin.up], + self.cohp_notot.all_cohps["1"].icohp[Spin.up], + atol=1e-3, + ) + + # Tests different methods for getting orbital-resolved COHPs + ref = self.cohp_orb.orb_res_cohp["1"]["4s-4px"] + cohp_label = self.cohp_orb.get_orbital_resolved_cohp("1", "4s-4px") + assert cohp_label.cohp == ref["COHP"] + assert cohp_label.icohp == ref["ICOHP"] + orbitals = [[Orbital.s, Orbital.px], ["s", "px"], [0, 3]] + cohps = [self.cohp_orb.get_orbital_resolved_cohp("1", [[4, orb[0]], [4, orb[1]]]) for orb in orbitals] + for cohp in cohps: + assert cohp.as_dict() == cohp_label.as_dict() + + def test_orbital_resolved_cohp_summed_spin_channels(self): + ref = self.cohp_orb.orb_res_cohp["1"]["4s-4px"] + cohp_label = self.cohp_orb.get_orbital_resolved_cohp("1", "4s-4px", summed_spin_channels=True) + assert cohp_label.cohp == ref["COHP"] + assert cohp_label.icohp == ref["ICOHP"] + orbitals = [[Orbital.s, Orbital.px], ["s", "px"], [0, 3]] + cohps = [ + self.cohp_orb.get_orbital_resolved_cohp("1", [[4, orb[0]], [4, orb[1]]], summed_spin_channels=True) + for orb in orbitals + ] + + for cohp in cohps: + assert cohp.as_dict() == cohp_label.as_dict() + + # spin polarization + assert self.cohp_lobster_spin_polarized.get_orbital_resolved_cohp( + "1", "6s-6s", summed_spin_channels=False + ).cohp[Spin.up][300] * 2 == approx( + self.cohp_lobster_spin_polarized.get_orbital_resolved_cohp("1", "6s-6s", summed_spin_channels=True).cohp[ + Spin.up + ][300] + ) + assert self.cohp_lobster_spin_polarized.get_orbital_resolved_cohp( + "1", "6s-6s", summed_spin_channels=False + ).cohp[Spin.down][300] * 2 == approx( + self.cohp_lobster_spin_polarized.get_orbital_resolved_cohp("1", "6s-6s", summed_spin_channels=True).cohp[ + Spin.up + ][300] + ) + assert self.cohp_lobster_spin_polarized.get_orbital_resolved_cohp( + "1", "6s-6s", summed_spin_channels=True + ).energies[0] == approx(-15.03759 + 1.96204) + assert self.cohp_lobster_spin_polarized.get_orbital_resolved_cohp( + "1", "6s-6s", summed_spin_channels=True + ).energies[5] == approx(-14.78697 + 1.96204) + assert not self.cohp_lobster_spin_polarized.get_orbital_resolved_cohp( + "1", "6s-6s", summed_spin_channels=True + ).are_coops + + +class TestMethod: + def setup_method(self): + filepath = f"{TEST_DIR}/COHPCAR.lobster.gz" + structure = f"{TEST_DIR}/POSCAR" + self.cohp_lobster = CompleteCohp.from_file("lobster", filename=filepath, structure_file=structure) + + filepath = f"{TEST_DIR}/COHPCAR.lobster.orbitalwise.gz" + structure = f"{TEST_DIR}/POSCAR.orbitalwise" + self.cohp_orb = CompleteCohp.from_file("lobster", filename=filepath, structure_file=structure) + + filepath = f"{TEST_DIR}/environments/COHPCAR.lobster.mp-190_2.gz" + structure = f"{TEST_DIR}/environments/CONTCAR.mp-190.gz" + self.cohp_lobster_spin_polarized = CompleteCohp.from_file( + "lobster", filename=filepath, structure_file=structure + ) + + def test_get_integrated_cohp_in_energy_range_full(self): + # integration up to Fermi level + + cohp = self.cohp_lobster + result = get_integrated_cohp_in_energy_range( + cohp, + label="1", + orbital=None, + energy_range=None, + relative_E_Fermi=True, + summed_spin_channels=True, + ) + assert result == approx(-0.10218 - 0.19701) + + result = get_integrated_cohp_in_energy_range( + cohp, + label="1", + orbital=None, + energy_range=None, + relative_E_Fermi=True, + summed_spin_channels=False, + ) + assert result[Spin.up] == approx(-0.10218) + assert result[Spin.down] == approx(-0.19701) + + # One without spin polarization + + result = get_integrated_cohp_in_energy_range( + self.cohp_orb, + label="1", + orbital=None, + energy_range=None, + relative_E_Fermi=False, + summed_spin_channels=False, + ) + assert result[Spin.up] == approx(-4.36062) + + result = get_integrated_cohp_in_energy_range( + self.cohp_orb, + label="1", + orbital=None, + energy_range=None, + relative_E_Fermi=True, + summed_spin_channels=False, + ) + + assert result[Spin.up] == approx(-4.36062) + + result = get_integrated_cohp_in_energy_range( + self.cohp_orb, + label="1", + orbital=None, + energy_range=None, + relative_E_Fermi=True, + summed_spin_channels=True, + ) + + assert result == approx(-4.36062) + # something else for orbital resolved version + # self.cohp_lobster_spin_polarized + + result = get_integrated_cohp_in_energy_range( + self.cohp_lobster_spin_polarized, + label="1", + orbital="6s-6s", + energy_range=None, + relative_E_Fermi=True, + summed_spin_channels=False, + ) + + assert result[Spin.up] == approx(-0.00006) + assert result[Spin.down] == approx(-0.00006) + + result = get_integrated_cohp_in_energy_range( + self.cohp_lobster_spin_polarized, + label="1", + orbital="6s-6s", + energy_range=None, + relative_E_Fermi=True, + summed_spin_channels=True, + ) + + assert result == approx(-0.00006 * 2) + + def test_get_integrated_cohp_in_energy_range_onefloat(self): + # only one float is given for energy range + cohp = self.cohp_lobster + fermi = cohp.efermi + result = get_integrated_cohp_in_energy_range( + cohp, + label="1", + orbital=None, + energy_range=-0.60201, + relative_E_Fermi=True, + summed_spin_channels=True, + ) + + assert result == approx(-0.10218 - 0.19701 + 0.14894 + 0.21889) + + result = get_integrated_cohp_in_energy_range( + cohp, + label="1", + orbital=None, + energy_range=-0.60201, + relative_E_Fermi=True, + summed_spin_channels=False, + ) + + assert result[Spin.up] == approx(-0.10218 + 0.14894) + assert result[Spin.down] == approx(-0.19701 + 0.21889) + # only one float is given for energy range (relative to E-fermi) + + result = get_integrated_cohp_in_energy_range( + cohp, + label="1", + orbital=None, + energy_range=-0.60201 + fermi, + relative_E_Fermi=False, + summed_spin_channels=True, + ) + assert result == approx(-0.10218 - 0.19701 + 0.14894 + 0.21889) + + result = get_integrated_cohp_in_energy_range( + cohp, + label="1", + orbital=None, + energy_range=-0.60201 + fermi, + relative_E_Fermi=False, + summed_spin_channels=False, + ) + + assert result[Spin.up] == approx(-0.10218 + 0.14894) + assert result[Spin.down] == approx(-0.19701 + 0.21889) + + # without spin + fermi = self.cohp_orb.efermi + result = get_integrated_cohp_in_energy_range( + self.cohp_orb, + label="1", + orbital=None, + energy_range=-14.0350 + fermi, + relative_E_Fermi=False, + summed_spin_channels=False, + ) + + assert result[Spin.up] == approx(-4.36062) + + result = get_integrated_cohp_in_energy_range( + self.cohp_orb, + label="1", + orbital=None, + energy_range=-14.03509, + relative_E_Fermi=True, + summed_spin_channels=False, + ) + assert result[Spin.up] == approx(-4.36062) + + result = get_integrated_cohp_in_energy_range( + self.cohp_orb, + label="1", + orbital=None, + energy_range=-14.03509, + relative_E_Fermi=True, + summed_spin_channels=True, + ) + + assert result == approx(-4.36062) + + def test_get_integrated_cohp_in_energy_range_whole_range(self): + cohp = self.cohp_lobster + fermi = cohp.efermi + result = get_integrated_cohp_in_energy_range( + cohp, + label="1", + orbital=None, + energy_range=[-0.60201, 0], + relative_E_Fermi=True, + summed_spin_channels=True, + ) + assert result == approx(-0.10218 - 0.19701 + 0.14894 + 0.21889) + + result = get_integrated_cohp_in_energy_range( + cohp, + label="1", + orbital=None, + energy_range=[-0.60201, 0], + relative_E_Fermi=True, + summed_spin_channels=False, + ) + + assert result[Spin.up] == approx(-0.10218 + 0.14894) + assert result[Spin.down] == approx(-0.19701 + 0.21889) + # whole energy range + + result = get_integrated_cohp_in_energy_range( + cohp, + label="1", + orbital=None, + energy_range=[-0.60201 + fermi, 0 + fermi], + relative_E_Fermi=False, + summed_spin_channels=True, + ) + assert result == approx(-0.10218 - 0.19701 + 0.14894 + 0.21889) + + result = get_integrated_cohp_in_energy_range( + cohp, + label="1", + orbital=None, + energy_range=[-0.60201 + fermi, 0 + fermi], + relative_E_Fermi=False, + summed_spin_channels=False, + ) + + assert result[Spin.up] == approx(-0.10218 + 0.14894) + assert result[Spin.down] == approx(-0.19701 + 0.21889) + + # without spin + fermi = self.cohp_orb.efermi + result = get_integrated_cohp_in_energy_range( + self.cohp_orb, + label="1", + orbital=None, + energy_range=[-14.0350 + fermi, fermi], + relative_E_Fermi=False, + summed_spin_channels=False, + ) + + assert result[Spin.up] == approx(-4.36062) + + result = get_integrated_cohp_in_energy_range( + self.cohp_orb, + label="1", + orbital=None, + energy_range=[-14.0350, 0], + relative_E_Fermi=True, + summed_spin_channels=False, + ) + + assert result[Spin.up] == approx(-4.36062) + + result = get_integrated_cohp_in_energy_range( + self.cohp_orb, + label="1", + orbital=None, + energy_range=[-14.0350, 0], + relative_E_Fermi=True, + summed_spin_channels=True, + ) + + assert result == approx(-4.36062)