# coding: utf-8
# Distributed under the terms of the MIT License.
""" This module implements the Electrode class, which contains methods
used for calculating electrode properties from phase diagrams.
"""
import numpy as np
from typing import List, Tuple
from matador.utils.chem_utils import get_formula_from_stoich
EPS = 1e-12
[docs]class Electrode:
"""The Electrode class wraps phase diagrams of battery electrode
materials, and provides useful methods for electrochemical analysis.
Note:
The units for relevant quantities are as follows:
- gravimetric capacity --> mAh/g
- volumetric capacity --> mAh/cm^3
- gravimetric energy density --> Wh/kg
- volumetric energy density --> Wh/l
Attributes:
kind (str): either 'anode' or 'cathode'. Effects conventions
used when computing capacities.
ion (str): species label for the active ion.
valence (int): number of electrons transferred per ion inserted/
removed. Currently assumes perfect Coulombic efficiency, and
full oxidation at each stage (e.g. Li->Li+ and Mg->Mg2+).
"""
_kinds = ["anode", "cathode"]
valence_data = {"Li": 1, "Na": 1, "K": 1, "Mg": 2, "Ca": 2}
""" This comment contains some stubs for functionality that has not yet been implemented.
def __init__(self, active_ion, base_material, phases, kind='anode', valence=None):
\"\"\" Initialise Electrode material for the given ion from a convex hull.
Parameters:
active_ion (str): label of species to use as active ion.
base_formula (str): formula of the starting material (either
"empty" anode or "full" cathode).
phases (:obj:`QueryConvexHull`/:obj:`list` of :obj:`dict`):
either a QueryConvexHull object for the material system
of interest, or a list of phases.
Keyword arguments:
kind (str): either 'anode' or 'cathode'. This decides what
convention to use when computing capacities.
valence (int): the valence of the active ion, if not included
this will be looked up for common ions.
\"\"\"
if kind not in self._kinds:
raise TypeError('Electrode kind must be one of {}'.format(self._kinds))
self.kind = kind
self.ion = active_ion
if valence is not None:
self.valence = valence
else:
self.valence = self._valence_data.get('active_ion')
if self.valence is None:
raise RuntimeError('Unable to lookup valence of {}, please pass it manually'
.format(self.ion))
# TODO: detect different materials inside convex hull
# or directly compute if just a list of phases
@property
def gravimetric_capacities(self):
pass
@property
def volumetric_capacities(self):
pass
@property
def voltages(self):
pass
@property
def average_voltage(self):
pass
@property
def gravimetric_energy_density(self):
pass
@property
def volumetric_energy_density(self):
pass
@property
def max_gravimetric_capacity(self):
pass
@property
def max_volumetric_capacity(self):
pass
@property
def voltage_curve(self):
pass
@property
def pdf_vs_voltage(self):
pass
@property
def pdf_vs_capacity(self):
pass
@property
def pxrd_vs_voltage(self):
pass
@property
def pxrd_vs_capacity(self):
pass
"""
[docs] @classmethod
def calculate_average_voltage(cls, capacities, voltages):
"""For a given set of capacities and voltages, compute
the average voltage during charge/discharge.
"""
trim = None
if np.isnan(capacities[-1]):
trim = -1
return np.sum(voltages[1:trim] * np.diff(capacities[:trim])) / np.nanmax(
capacities
)
[docs] @classmethod
def calculate_energy_density(cls, capacities, voltages) -> float:
"""Compute the energy density in Wh/kg from the maximum capacity and
the average voltage.
"""
return cls.calculate_average_voltage(capacities, voltages) * np.nanmax(
capacities
)
@classmethod
def _find_starting_materials(self, points, stoichs, include_elemental=False):
"""Iterate over an array of compositions and energies to find
the starting points of the electrode, i.e. those with zero concentration
of the active ion.
Parameters:
points (np.ndarray): array of concentrations with the 0-th column
containing the concentration of the active ion with final columns
corresponding to formation energies.
stoichs (list):L list of input stoichiometries.
Keyword arguments:
include_elemental (bool): whether to include single element starting materials
in the case of e.g. ternary phase diagrams.
Returns:
(np.ndarray, list): the filtered concentrations and stoichiometries
that correspond to electrode starting materials.
"""
endpoints = []
endstoichs = []
for ind, point in enumerate(points):
if point[0] == 0:
conc = np.array(point)
conc[-1] = 1 - np.sum(point[:-1])
if include_elemental or np.min(conc[1:]) > EPS:
if not any(
[
point.tolist() == test_point.tolist()
for test_point in endpoints
]
):
endpoints.append(point)
endstoichs.append(stoichs[ind])
return endpoints, endstoichs
[docs]class VoltageProfile:
"""Container class for data associated with a voltage profile.
Attributes:
starting_stoichiometry: the initial stoichiometry of the electrode.
capacities: list of gravimetric capacities in mAh/g.
voltage: list of voltages at each capacity step.
average_voltage: the average voltage across the full cycle.
reactions: a list of (coefficient, formula) tuples showing the
progression of the balanced reaction, e.g.
`[((1, "PSn")), ((1, "LiP"), (1, "LiSn"))]`
active_ion: species label of the active ion.
"""
def __init__(
self,
starting_stoichiometry: Tuple[Tuple[str, float], ...],
capacities: List[float],
voltages: List[float],
average_voltage: float,
active_ion: str,
reactions: List[Tuple[Tuple[float, str], ...]],
):
"""Initialise the voltage profile with the given data."""
n_steps = len(voltages)
if any(_len != n_steps for _len in [len(capacities), len(reactions) + 1]):
raise RuntimeError(
"Invalid size of initial arrays, capacities and voltages must have same length."
"reactions array must have length 1 smaller than voltages"
)
self.starting_stoichiometry = starting_stoichiometry
self.starting_formula = get_formula_from_stoich(
starting_stoichiometry, unicode_sub=True
)
self.capacities = capacities
self.average_voltage = average_voltage
self.voltages = voltages
self.energy_density = Electrode.calculate_energy_density(
self.capacities, self.voltages
)
self.reactions = reactions
self.active_ion = active_ion
[docs] def voltage_summary(self, csv=False):
"""Prints a voltage data summary.
Keyword arguments:
csv (bool/str): whether or not to write a CSV file containing the data.
If this contains a string use this as the filename.
"""
data_str = "# {} into {}\n".format(self.active_ion, self.starting_formula)
data_str += "# Average voltage: {:4.2f} V\n".format(self.average_voltage)
data_str += "# {:^10} \t{:^10}\n".format("Q (mAh/g)", "Voltage (V)")
for idx, _ in enumerate(self.voltages):
data_str += "{:>10.2f} \t{:>10.8f}".format(
self.capacities[idx], self.voltages[idx]
)
if idx != len(self.voltages) - 1:
data_str += "\n"
if csv:
if isinstance(csv, str):
fname = csv
else:
fname = "{}_voltages.csv".format(self.starting_formula)
with open(fname, "w") as f:
f.write(data_str)
return data_str
def __str__(self):
return self.voltage_summary(csv=False)
def __repr__(self):
return f"<{self.__class__.__name__}: {self.active_ion} into {self.starting_formula}>"