From 42bbab2bfae50075226a1fe9544d6f3b08ba9654 Mon Sep 17 00:00:00 2001 From: Shyam D Date: Thu, 27 May 2021 11:17:40 -0700 Subject: [PATCH 1/3] new oxidation state doc --- emmet-core/emmet/core/oxidation_states.py | 81 +++++++++++++++++++++++ tests/emmet-core/test_oxidation_states.py | 53 +++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 emmet-core/emmet/core/oxidation_states.py create mode 100644 tests/emmet-core/test_oxidation_states.py diff --git a/emmet-core/emmet/core/oxidation_states.py b/emmet-core/emmet/core/oxidation_states.py new file mode 100644 index 0000000000..43a6f498df --- /dev/null +++ b/emmet-core/emmet/core/oxidation_states.py @@ -0,0 +1,81 @@ +import logging +from itertools import groupby +from typing import Dict, List, Literal + +import numpy as np +from pydantic import BaseModel +from pymatgen.analysis.bond_valence import BVAnalyzer +from pymatgen.core import Structure +from pymatgen.core.periodic_table import Specie + + +class OxidationStateDocument(BaseModel): + + possible_species: List[str] + possible_valences: List[float] + average_oxidation_states: Dict[str, float] + method: Literal["BVAnalyzer", "oxi_state_guesses"] + structure: Structure + + @classmethod + def from_structure(cls, structure: Structure): + structure.remove_oxidation_states() + try: + bva = BVAnalyzer() + valences = bva.get_valences(structure) + possible_species = { + str(Specie(structure[idx].specie, oxidation_state=valence)) + for idx, valence in enumerate(valences) + } + + structure.add_oxidation_state_by_site(valences) + + # construct a dict of average oxi_states for use + # by MP2020 corrections. The format should mirror + # the output of the first element from Composition.oxi_state_guesses() + # e.g. {'Li': 1.0, 'O': -2.0} + s_list = [(t.specie.element, t.specie.oxi_state) for t in structure.sites] + s_list = sorted(s_list, key=lambda x: x[0]) + oxi_state_dict = {} + for c, g in groupby(s_list, key=lambda x: x[0]): + oxi_state_dict[str(c)] = np.mean([e[1] for e in g]) + + d = { + "possible_species": list(possible_species), + "possible_valences": valences, + "average_oxidation_states": oxi_state_dict, + "method": "BVAnalyzer", + "structure": structure, + } + + return cls(**d) + + except Exception as e: + logging.error("BVAnalyzer failed with: {}".format(e)) + + try: + first_oxi_state_guess = structure.composition.oxi_state_guesses( + max_sites=-50 + )[0] + valences = [ + first_oxi_state_guess[site.species_string] for site in structure + ] + possible_species = { + str(Specie(el, oxidation_state=valence)) + for el, valence in first_oxi_state_guess.items() + } + + structure.add_oxidation_state_by_site(valences) + + d = { + "possible_species": list(possible_species), + "possible_valences": valences, + "average_oxidation_states": first_oxi_state_guess, + "method": "oxi_state_guesses", + "structure": structure, + } + + return cls(**d) + + except Exception as e: + logging.error("Oxidation state guess failed with: {}".format(e)) diff --git a/tests/emmet-core/test_oxidation_states.py b/tests/emmet-core/test_oxidation_states.py new file mode 100644 index 0000000000..896a4ac7d0 --- /dev/null +++ b/tests/emmet-core/test_oxidation_states.py @@ -0,0 +1,53 @@ +import pytest +from pymatgen.core import Structure +from pymatgen.util.testing import PymatgenTest + +from emmet.core.oxidation_states import OxidationStateDocument + +test_structures = { + name: struc + for name, struc in PymatgenTest.TEST_STRUCTURES.items() + if name + in [ + "SiO2", + "Li2O", + "LiFePO4", + "TlBiSe2", + "K2O2", + "Li3V2(PO4)3", + "CsCl", + "Li2O2", + "NaFePO4", + "Pb2TiZrO6", + "SrTiO3", + ] +} + +fail_structures = { + name: struc + for name, struc in PymatgenTest.TEST_STRUCTURES.items() + if name + in [ + "TiO2", + "BaNiO3", + "VO2", + ] +} + + +@pytest.mark.parametrize("structure", test_structures.values()) +def test_oxidation_state(structure: Structure): + """Very simple test to make sure this actually works""" + + doc = OxidationStateDocument.from_structure(structure) + print(structure.composition) + assert doc is not None + + +@pytest.mark.parametrize("structure", fail_structures.values()) +def test_oxidation_state_failures(structure: Structure): + """Very simple test to make sure this actually fails""" + + doc = OxidationStateDocument.from_structure(structure) + print(structure.composition) + assert doc is None From 68143e7e1660f8db4499ae3cbb32d5efe911634d Mon Sep 17 00:00:00 2001 From: Shyam D Date: Thu, 27 May 2021 11:20:11 -0700 Subject: [PATCH 2/3] udpate name to `Doc` --- emmet-core/emmet/core/oxidation_states.py | 2 +- tests/emmet-core/test_oxidation_states.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/emmet-core/emmet/core/oxidation_states.py b/emmet-core/emmet/core/oxidation_states.py index 43a6f498df..57040d4e24 100644 --- a/emmet-core/emmet/core/oxidation_states.py +++ b/emmet-core/emmet/core/oxidation_states.py @@ -9,7 +9,7 @@ from pymatgen.core.periodic_table import Specie -class OxidationStateDocument(BaseModel): +class OxidationStateDoc(BaseModel): possible_species: List[str] possible_valences: List[float] diff --git a/tests/emmet-core/test_oxidation_states.py b/tests/emmet-core/test_oxidation_states.py index 896a4ac7d0..df19ae60bf 100644 --- a/tests/emmet-core/test_oxidation_states.py +++ b/tests/emmet-core/test_oxidation_states.py @@ -2,7 +2,7 @@ from pymatgen.core import Structure from pymatgen.util.testing import PymatgenTest -from emmet.core.oxidation_states import OxidationStateDocument +from emmet.core.oxidation_states import OxidationStateDoc test_structures = { name: struc @@ -39,7 +39,7 @@ def test_oxidation_state(structure: Structure): """Very simple test to make sure this actually works""" - doc = OxidationStateDocument.from_structure(structure) + doc = OxidationStateDoc.from_structure(structure) print(structure.composition) assert doc is not None @@ -48,6 +48,6 @@ def test_oxidation_state(structure: Structure): def test_oxidation_state_failures(structure: Structure): """Very simple test to make sure this actually fails""" - doc = OxidationStateDocument.from_structure(structure) + doc = OxidationStateDoc.from_structure(structure) print(structure.composition) assert doc is None From 4921d69b40faa213cb0be860f0f218777887284b Mon Sep 17 00:00:00 2001 From: Shyam D Date: Thu, 27 May 2021 11:28:11 -0700 Subject: [PATCH 3/3] use defaultdict to average oxidation statees --- emmet-core/emmet/core/oxidation_states.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/emmet-core/emmet/core/oxidation_states.py b/emmet-core/emmet/core/oxidation_states.py index 57040d4e24..41ecfc1a50 100644 --- a/emmet-core/emmet/core/oxidation_states.py +++ b/emmet-core/emmet/core/oxidation_states.py @@ -1,12 +1,14 @@ import logging +from collections import defaultdict from itertools import groupby -from typing import Dict, List, Literal +from typing import Dict, List import numpy as np from pydantic import BaseModel from pymatgen.analysis.bond_valence import BVAnalyzer from pymatgen.core import Structure from pymatgen.core.periodic_table import Specie +from typing_extensions import Literal class OxidationStateDoc(BaseModel): @@ -34,11 +36,15 @@ def from_structure(cls, structure: Structure): # by MP2020 corrections. The format should mirror # the output of the first element from Composition.oxi_state_guesses() # e.g. {'Li': 1.0, 'O': -2.0} - s_list = [(t.specie.element, t.specie.oxi_state) for t in structure.sites] - s_list = sorted(s_list, key=lambda x: x[0]) - oxi_state_dict = {} - for c, g in groupby(s_list, key=lambda x: x[0]): - oxi_state_dict[str(c)] = np.mean([e[1] for e in g]) + + site_oxidation_list = defaultdict(list) + for site in structure: + site_oxidation_list[site.specie.element].append(site.specie.oxi_state) + + oxi_state_dict = { + str(el): np.mean(oxi_states) + for el, oxi_states in site_oxidation_list.items() + } d = { "possible_species": list(possible_species),