Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Electrode Doc #149

Merged
merged 48 commits into from
Jan 26, 2021
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
7284859
added builder to popluate URL data for references
jmmshn Nov 1, 2020
d1b4265
removed sandbox from electrode builder
jmmshn Nov 30, 2020
97a242f
removed sandbox from electrode builder
jmmshn Nov 30, 2020
74d2650
removed sandbox from electrode builder
jmmshn Nov 30, 2020
8758cb9
removed sandbox from electrode builder
jmmshn Nov 30, 2020
8fe3af6
updated builder
jmmshn Dec 1, 2020
c6ea5b7
split electrode builder into two parts
jmmshn Dec 17, 2020
1ec5a4c
Merge branch 'master' into s_group
jmmshn Dec 17, 2020
642e0fe
cleanup
jmmshn Dec 17, 2020
11b4349
cleanup
jmmshn Dec 17, 2020
5061028
cleanup
jmmshn Dec 17, 2020
e04688a
cleanup
jmmshn Dec 17, 2020
0d0150e
bare branch
jmmshn Dec 17, 2020
56593fe
added electrode document models
jmmshn Dec 18, 2020
fce294d
working document model and tests
jmmshn Dec 18, 2020
9e0da7a
working document model and tests
jmmshn Dec 18, 2020
0e38b84
added framework and lu
jmmshn Dec 18, 2020
3d15edf
typo
jmmshn Dec 18, 2020
9857615
import from init
jmmshn Dec 18, 2020
d174c3c
intermediate model to store the result of structure matching
jmmshn Dec 19, 2020
c4918af
pmg version
jmmshn Dec 19, 2020
2484e5f
pmg version
jmmshn Dec 19, 2020
a5f259b
black
jmmshn Dec 19, 2020
f5c74d3
black
jmmshn Dec 19, 2020
377bba5
mypy
jmmshn Dec 19, 2020
5a62430
moved test
jmmshn Dec 19, 2020
989c774
Merge remote-tracking branch 'materialsproject/master' into electrode…
jmmshn Dec 19, 2020
8e2180c
moved test
jmmshn Dec 19, 2020
c43ac89
testfile location
jmmshn Dec 19, 2020
e0e6da8
length
jmmshn Dec 19, 2020
2a703e4
length
jmmshn Dec 19, 2020
d5d294f
length
jmmshn Dec 19, 2020
bb9c5c9
length
jmmshn Dec 19, 2020
3a4f5fe
length
jmmshn Dec 19, 2020
825e3db
copied testfiles over
jmmshn Dec 19, 2020
2aa3b7a
copied testfiles over
jmmshn Dec 19, 2020
0e8a199
testing
jmmshn Dec 21, 2020
1eff36a
testing
jmmshn Dec 21, 2020
1a385fc
Merge remote-tracking branch 'materialsproject/master' into electrode…
jmmshn Jan 13, 2021
bc6824b
same typo
jmmshn Jan 13, 2021
bb644c8
updated prec
jmmshn Jan 26, 2021
601ff63
Merge remote-tracking branch 'materialsproject/master' into electrode…
jmmshn Jan 26, 2021
efb7b7e
Elements
jmmshn Jan 26, 2021
783f8f4
Elements
jmmshn Jan 26, 2021
daceb18
tests cleanup
jmmshn Jan 26, 2021
5e6c9fb
tests cleanup
jmmshn Jan 26, 2021
c81d6a0
tests cleanup
jmmshn Jan 26, 2021
3841e6a
tests cleanup
jmmshn Jan 26, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 16 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@ 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
rev: stable
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- id: flake8
args: [--max-line-length=131]
shyamd marked this conversation as resolved.
Show resolved Hide resolved

- repo: https://github.com/psf/black
rev: 19.10b0 # Replace by any tag/version: https://github.com/psf/black/tags
shyamd marked this conversation as resolved.
Show resolved Hide resolved
hooks:
- id: black
language_version: python3.8
args: ["--line-length=120"]

- repo: https://github.com/pycqa/isort
rev: 5.5.2
hooks:
- id: isort
files: 'emmet-core/*'
- id: isort
files: 'emmet-core/.*'

- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v0.790'
hooks:
- id: mypy
shyamd marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions emmet-builders/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
pymatgen==2020.4.29
maggma==0.20.0
pymatgen>=2020.12.18
maggma>=0.20.0
shyamd marked this conversation as resolved.
Show resolved Hide resolved
emmet-core
222 changes: 222 additions & 0 deletions emmet-core/emmet/core/electrode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
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 ElementBase
from pymatgen.entries.computed_entries import ComputedEntry

from emmet.stubs import Composition, Structure


class WorkingIon(ElementBase):
Li = "Li"
Ca = "Ca"
Mg = "Mg"

shyamd marked this conversation as resolved.
Show resolved Hide resolved

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: WorkingIon = 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: WorkingIon = 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: WorkingIon = 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)
124 changes: 124 additions & 0 deletions tests/emmet-core/test_electrodes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import json
import os
from pprint import pprint

import pytest
from monty.json import MontyDecoder
from monty.serialization import dumpfn, 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
TODO: All thie can be serialized in battery_data.json after PR to pymatgen
"""
entry_Li = ComputedEntry("Li", -1.90753119)
entry_Mg = loadfn(test_dir / "Mg_batt.json")
entry_Ca = ComputedEntry("Ca", -1.99689568)

entries_LTO = loadfn(test_dir / "LiTiO2_batt.json")
entries_MVO = loadfn(test_dir / "MgVO_batt.json")
entries_CMO = loadfn(test_dir / "CaMoO2_batt.json")

ie_LTO = InsertionElectrode.from_entries(entries_LTO, entry_Li)
ie_MVO = InsertionElectrode.from_entries(entries_MVO, entry_Mg)
ie_CMO = InsertionElectrode.from_entries(entries_CMO, entry_Ca)

d = {
"LTO": (ie_LTO, entries_LTO[0].structure, entry_Li),
"MVO": (ie_MVO, entries_MVO[0].structure, entry_Mg),
"CMO": (ie_CMO, entries_CMO[0].structure, entry_Ca),
}
# d = loadfn(test_dir/"battery_data.json", cls=MontyDecoder) # awaiting pmg update
return d


kmap = {"specific_energy": "energy_grav", "energy_density": "energy_vol"}


@pytest.fixture(scope="session")
def conversion_elec(test_dir):
formulas = ["LiCoO2", "FeF3", "MnO2"]
conversion_eletrodes = {}
for f in formulas:

entries = loadfn(test_dir / f"{f}_batt.json", cls=MontyDecoder)
if f in ["LiCoO2", "FeF3"]:
working_ion = "Li"
elif f in ["MnO2"]:
working_ion = "Mg"
c = ConversionElectrode.from_composition_and_entries(Composition(f), entries, working_ion_symbol=working_ion)
conversion_eletrodes[f] = {
"working_ion": working_ion,
"CE": c,
"entries": entries,
}

expected_properties = {
"LiCoO2": {
"average_voltage": 2.26940307125,
"capacity_grav": 903.19752911225669,
"capacity_vol": 2903.35804724,
"energy_grav": 2049.7192465127678,
"energy_vol": 6588.8896693479574,
},
"FeF3": {
"average_voltage": 3.06179925889,
"capacity_grav": 601.54508701578118,
"capacity_vol": 2132.2069115142394,
"energy_grav": 1841.8103016131706,
"energy_vol": 6528.38954147,
},
"MnO2": {
"average_voltage": 1.7127027687901726,
"capacity_grav": 790.9142070034802,
"capacity_vol": 3543.202003526853,
"energy_grav": 1354.6009522103434,
"energy_vol": 6068.451881823329,
},
}

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


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(conversion_elec):
for k, (elec, expected) in conversion_elec.items():
# Make sure that main document can be created using an InsertionElectrode object
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()

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)
1 change: 1 addition & 0 deletions tests/test_files/CaMoO2_batt.json

Large diffs are not rendered by default.

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

Large diffs are not rendered by default.

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.