Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #149 from jmmshn/electrode_doc
Electrode Doc
- Loading branch information
Showing
6 changed files
with
367 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.31 | ||
maggma==0.25.0 | ||
emmet-core |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.