Skip to content

Commit

Permalink
Merge pull request #149 from jmmshn/electrode_doc
Browse files Browse the repository at this point in the history
Electrode Doc
  • Loading branch information
shyamd committed Jan 26, 2021
2 parents 5c7ae13 + 3841e6a commit e30cbf2
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 9 deletions.
15 changes: 8 additions & 7 deletions .pre-commit-config.yaml
Expand Up @@ -2,11 +2,12 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: flake8
- repo: https://github.com/ambv/black
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: flake8

- repo: https://github.com/psf/black
rev: stable
hooks:
- id: black
Expand All @@ -15,5 +16,5 @@ repos:
- repo: https://github.com/pycqa/isort
rev: 5.5.2
hooks:
- id: isort
files: 'emmet-core/*'
- id: isort
files: 'emmet-core/.*'
4 changes: 2 additions & 2 deletions emmet-builders/requirements.txt
@@ -1,3 +1,3 @@
pymatgen==2020.4.29
maggma==0.20.0
pymatgen==2020.12.31
maggma==0.25.0
emmet-core
260 changes: 260 additions & 0 deletions emmet-core/emmet/core/electrode.py
@@ -0,0 +1,260 @@
from datetime import datetime
from typing import Dict, List

from monty.json import MontyDecoder
from pydantic import BaseModel, Field, validator
from pymatgen.apps.battery.battery_abc import AbstractElectrode
from pymatgen.apps.battery.conversion_battery import ConversionElectrode
from pymatgen.apps.battery.insertion_battery import InsertionElectrode
from pymatgen.core.periodic_table import Element
from pymatgen.entries.computed_entries import ComputedEntry

from emmet.stubs import Composition, Structure


class VoltagePairDoc(BaseModel):
"""
Data for individual voltage steps.
Note: Each voltage step is represented as a sub_electrode (ConversionElectrode/InsertionElectrode)
object to gain access to some basic statistics about the voltage step
"""

max_delta_volume: str = Field(
None,
description="Volume changes in % for a particular voltage step using: "
"max(charge, discharge) / min(charge, discharge) - 1",
)

average_voltage: float = Field(
None,
description="The average voltage in V for a particular voltage step.",
)

capacity_grav: float = Field(None, description="Gravimetric capacity in mAh/g.")

capacity_vol: float = Field(None, description="Volumetric capacity in mAh/cc.")

energy_grav: float = Field(
None, description="Gravimetric energy (Specific energy) in Wh/kg."
)

energy_vol: float = Field(
None, description="Volumetric energy (Energy Density) in Wh/l."
)

fracA_charge: float = Field(
None, description="Atomic fraction of the working ion in the charged state."
)

fracA_discharge: float = Field(
None, description="Atomic fraction of the working ion in the discharged state."
)

@classmethod
def from_sub_electrode(cls, sub_electrode: AbstractElectrode, **kwargs):
"""
Convert A pymatgen electrode object to a document
"""
return cls(**sub_electrode.get_summary_dict(), **kwargs)


class InsertionVoltagePairDoc(VoltagePairDoc):
"""
Features specific to insertion electrode
"""

stability_charge: float = Field(
None, description="The energy above hull of the charged material."
)

stability_discharge: float = Field(
None, description="The energy above hull of the discharged material."
)


class InsertionElectrodeDoc(InsertionVoltagePairDoc):
"""
Insertion electrode
"""

task_id: str = Field(None, description="The id for this battery document.")

framework_formula: str = Field(
None, description="The id for this battery document."
)

host_structure: Structure = Field(
None,
description="Host structure (structure without the working ion)",
)

adj_pairs: List[InsertionVoltagePairDoc] = Field(
None,
description="Returns all the Voltage Steps",
)

working_ion: Element = Field(
None,
description="The working ion as an Element object",
)

num_steps: float = Field(
None,
description="The number of distinct voltage steps in from fully charge to "
"discharge based on the stable intermediate states",
)

max_voltage_step: float = Field(
None, description="Maximum absolute difference in adjacent voltage steps"
)

last_updated: datetime = Field(
None,
description="Timestamp for the most recent calculation for this Material document",
)

framework: Composition

# Make sure that the datetime field is properly formatted
@validator("last_updated", pre=True)
def last_updated_dict_ok(cls, v):
return MontyDecoder().process_decoded(v)

@classmethod
def from_entries(
cls,
grouped_entries: List[ComputedEntry],
working_ion_entry: ComputedEntry,
task_id: str,
host_structure: Structure,
):
ie = InsertionElectrode.from_entries(
entries=grouped_entries, working_ion_entry=working_ion_entry
)
d = ie.get_summary_dict()
d["num_steps"] = d.pop("nsteps", None)
d["last_updated"] = datetime.utcnow()
return cls(
task_id=task_id,
host_structure=host_structure.as_dict(),
framework=Composition(d["framework_formula"]),
**d
)


class ConversionVoltagePairDoc(VoltagePairDoc):
"""
Features specific to conversion electrode
"""

reactions: List[str] = Field(
None,
description="The reaction(s) the characterizes that particular voltage step.",
)


class ConversionElectrodeDoc(ConversionVoltagePairDoc):
task_id: str = Field(None, description="The id for this battery document.")

adj_pairs: List[ConversionVoltagePairDoc] = Field(
None,
description="Returns all the adjacent Voltage Steps",
)

working_ion: Element = Field(
None,
description="The working ion as an Element object",
)

num_steps: float = Field(
None,
description="The number of distinct voltage steps in from fully charge to "
"discharge based on the stable intermediate states",
)

max_voltage_step: float = Field(
None, description="Maximum absolute difference in adjacent voltage steps"
)

last_updated: datetime = Field(
None,
description="Timestamp for the most recent calculation for this Material document",
)

# Make sure that the datetime field is properly formatted
@validator("last_updated", pre=True)
def last_updated_dict_ok(cls, v):
return MontyDecoder().process_decoded(v)

@classmethod
def from_composition_and_entries(
cls,
composition: Composition,
entries: List[ComputedEntry],
working_ion_symbol: str,
task_id: str,
):
ce = ConversionElectrode.from_composition_and_entries(
comp=composition,
entries_in_chemsys=entries,
working_ion_symbol=working_ion_symbol,
)
d = ce.get_summary_dict()
d["num_steps"] = d.pop("nsteps", None)
d["last_updated"] = datetime.utcnow()
return cls(task_id=task_id, framework=Composition(d["framework_formula"]), **d)


class StructureGroupDoc(BaseModel):
"""
Document model for the intermediate structure matching database used to build the insertion electrode documents.
"""

task_id: str = Field(
None,
description="The combined task_id of the grouped document is given by the numerically smallest task id "
"followed by '_Li' or whichever working atom is considered the working ion during grouping.",
)

structure_matched: bool = Field(
None,
description="True if the structures in this group has been matched to each other. This is False for groups "
"that contain all the left over structures with the same framework.",
)

has_distinct_compositions: bool = Field(
None,
description="True if multiple working ion fractions are available in the group, which means a voltage "
"step exits.",
)

grouped_task_ids: List[str] = Field(
None,
description="The ids of the materials that have been grouped by the structure matcher.",
)

entry_data: Dict = Field(
None,
description="Dictionary keyed by the task_id, contains the 'composition' and 'volume' of each material.",
)

framework_formula: str = Field(
None, description="The formula of the host framework."
)

working_ion: Element = Field(None, description="The working ion")

chemsys: str = Field(
None,
description="The chemsys this group belongs to. Always includes the working ion",
)

last_updated: datetime = Field(
None,
description="Timestamp for the most recent calculation for this Material document",
)

# Make sure that the datetime field is properly formatted
@validator("last_updated", pre=True)
def last_updated_dict_ok(cls, v):
return MontyDecoder().process_decoded(v)
95 changes: 95 additions & 0 deletions tests/emmet-core/test_electrodes.py
@@ -0,0 +1,95 @@
import pytest
from monty.serialization import loadfn
from pymatgen import Composition
from pymatgen.apps.battery.conversion_battery import ConversionElectrode
from pymatgen.apps.battery.insertion_battery import InsertionElectrode
from pymatgen.entries.computed_entries import ComputedEntry

from emmet.core.electrode import (
ConversionElectrodeDoc,
ConversionVoltagePairDoc,
InsertionElectrodeDoc,
InsertionVoltagePairDoc,
)


@pytest.fixture(scope="session")
def insertion_elec(test_dir):
"""
Recycle the test cases from pymatgen
"""
entry_Li = ComputedEntry("Li", -1.90753119)
# more cases can be added later if problems are found
entries_LTO = loadfn(test_dir / "LiTiO2_batt.json")
ie_LTO = InsertionElectrode.from_entries(entries_LTO, entry_Li)

d = {
"LTO": (ie_LTO, entries_LTO[0].structure, entry_Li),
}
return d


@pytest.fixture(scope="session")
def conversion_elec(test_dir):
conversion_eletrodes = {}

entries_LCO = loadfn(test_dir / "LiCoO2_batt.json")
c = ConversionElectrode.from_composition_and_entries(
Composition("LiCoO2"), entries_LCO, working_ion_symbol="Li"
)
conversion_eletrodes["LiCoO2"] = {
"working_ion": "Li",
"CE": c,
"entries": entries_LCO,
}

expected_properties = {
"LiCoO2": {
"average_voltage": 2.26940307125,
"capacity_grav": 903.19752911225669,
"capacity_vol": 2903.35804724,
"energy_grav": 2049.7192465127678,
"energy_vol": 6588.8896693479574,
}
}

return {
k: (conversion_eletrodes[k], expected_properties[k])
for k in conversion_eletrodes.keys()
}


def test_InsertionDocs(insertion_elec):
for k, (elec, struct, wion_entry) in insertion_elec.items():
# Make sure that main document can be created using an InsertionElectrode object
ie = InsertionElectrodeDoc.from_entries(
grouped_entries=elec._stable_entries,
working_ion_entry=wion_entry,
task_id="mp-1234",
host_structure=struct,
)
assert ie.average_voltage == elec.get_average_voltage()
# Make sure that each adjacent pair can be converted into a sub electrode
for sub_elec in elec.get_sub_electrodes(adjacent_only=True):
vp = InsertionVoltagePairDoc.from_sub_electrode(sub_electrode=sub_elec)
assert vp.average_voltage == sub_elec.get_average_voltage()


def test_ConversionDocs_from_entries(conversion_elec):
for k, (elec, expected) in conversion_elec.items():
vp = ConversionElectrodeDoc.from_composition_and_entries(
Composition(k),
entries=elec["entries"],
working_ion_symbol=elec["working_ion"],
task_id="mp-1234",
)
res_d = vp.dict()
for k, v in expected.items():
assert res_d[k] == pytest.approx(v, 0.01)


def test_ConversionDocs_from_sub_electrodes(conversion_elec):
for k, (elec, expected) in conversion_elec.items():
for sub_elec in elec["CE"].get_sub_electrodes(adjacent_only=True):
vp = ConversionVoltagePairDoc.from_sub_electrode(sub_electrode=sub_elec)
assert vp.average_voltage == sub_elec.get_average_voltage()
1 change: 1 addition & 0 deletions tests/test_files/LiCoO2_batt.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions tests/test_files/LiTiO2_batt.json

Large diffs are not rendered by default.

0 comments on commit e30cbf2

Please sign in to comment.