"""
Classes for defining capacity fade mechanisms.
"""
from abc import ABC, abstractmethod
from functools import reduce
from operator import ior
[docs]class DegradationMechanism(ABC):
"""Abstract base class to be implemented by specific degradation mechanisms."""
def __init__(self, **c_products: float):
self.c_products = c_products
[docs] @abstractmethod
def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, float]:
"""Applies desired degradation mechanisms to oxidized/reduced species at each time step."""
raise NotImplementedError
[docs]class ChemicalDegradationOxidized(DegradationMechanism):
"""
Provides an N-th order chemical degradation mechanism for an oxidized species.
Parameters
----------
rate_order : int
Rate order for chemical degradation reaction.
rate_constant : float
N-th order rate constant of chemical degradation of oxidized species (unit is rate order-dependent).
"""
def __init__(self, rate_order: int, rate_constant: float) -> None:
super().__init__()
self.rate_order = rate_order
self.rate_constant = rate_constant
if self.rate_order < 0:
raise ValueError("'rate_order' must be >= 0")
if self.rate_constant <= 0.0:
raise ValueError("'rate_constant' must be > 0.0")
[docs] def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, float]:
"""
Applies N-th order chemical degradation of N*[redox-active species] --> [redox-inactive species] at each
time step. Returns updated concentrations; concentration may be unchanged if species does not degrade.
Parameters
----------
c_ox : float
Concentration of oxidized species (M).
c_red : float
Concentration of reduced species (M).
time_step : float
Time interval size (s).
Returns
-------
delta_ox : float
Change in concentration of oxidized species (M).
delta_red : float
Change in concentration of reduced species (M). This will always be zero.
"""
delta_ox = -(time_step * self.rate_constant * (c_ox ** self.rate_order))
delta_red = 0.0
return delta_ox, delta_red
[docs]class ChemicalDegradationReduced(DegradationMechanism):
"""
Provides an N-th order chemical degradation mechanism for a reduced species.
Parameters
----------
rate_order : int
Rate order for chemical degradation reaction.
rate_constant : float
N-th order rate constant of chemical degradation of reduced species (unit is rate order-dependent).
"""
def __init__(self, rate_order: int, rate_constant: float) -> None:
super().__init__()
self.rate_order = rate_order
self.rate_constant = rate_constant
if self.rate_order < 0:
raise ValueError("'rate_order' must be >= 0")
if self.rate_constant <= 0.0:
raise ValueError("'rate_constant' must be > 0.0")
[docs] def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, float]:
"""
Applies N-th order chemical degradation of N*[redox-active species] --> [redox-inactive species] at each
time step. Returns updated concentrations; concentration may be unchanged if species does not degrade.
Parameters
----------
c_ox : float
Concentration of oxidized species (M).
c_red : float
Concentration of reduced species (M).
time_step : float
Time interval size (s).
Returns
-------
delta_ox : float
Change in concentration of oxidized species (M). This will always be zero.
delta_red : float
Change in concentration of reduced species (M).
"""
delta_ox = 0.0
delta_red = -(time_step * self.rate_constant * (c_red ** self.rate_order))
return delta_ox, delta_red
[docs]class AutoOxidation(DegradationMechanism):
"""
Provides a 1st order auto-oxidation mechanism, (red --> ox) with no loss of active material. This can be thought
of as a chemical oxidation of the redox-active, balanced by an oxidant e.g., water splitting (HER). This could
occur in a low-potential negolyte and be considered a self-discharge. If it is desired for the concentration of
the oxidant to affect the chemical oxidation rate, the initial oxidant concentration and the stoichiometric factor
i.e., red + n*oxidant --> ox + ..., can be input. This could simulate the effect of H2 leaving the system.
Parameters
----------
rate_constant : float
First order rate of auto-oxidation (1/s).
c_oxidant : float
Initial concentration of oxidant, defaults to 0.0 (M).
oxidant_stoich : int
Number of oxidants involved in the chemical oxidation of the redox-active species, defaults to 0.
"""
def __init__(self, rate_constant: float, c_oxidant: float = 0.0, oxidant_stoich: int = 0) -> None:
super().__init__()
self.rate_constant = rate_constant
self.c_oxidant = c_oxidant
self.oxidant_stoich = oxidant_stoich
if self.rate_constant <= 0.0:
raise ValueError("'rate_constant' must be > 0.0")
if self.c_oxidant < 0.0:
raise ValueError("'c_oxidant' must be >= 0.0")
if self.oxidant_stoich < 0:
raise ValueError("'oxidant_stoich' must be >= 0")
if (self.oxidant_stoich > 0 and c_oxidant == 0.0) or (self.oxidant_stoich == 0 and c_oxidant > 0.0):
raise ValueError("'c_oxidant' and 'oxidant_stoich' must both be zero, or both be positive")
[docs] def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, float]:
"""
Applies an auto-oxidation mechanism to oxidized/reduced species at each time step.
Defaults to first order process: red --> ox.
Parameters
----------
c_ox : float
Concentration of oxidized species (M).
c_red : float
Concentration of reduced species (M).
time_step : float
Time interval size (s).
Returns
-------
delta_ox : float
Change in concentration of oxidized species (M).
delta_red : float
Change in concentration of reduced species (M).
"""
delta_concentration = time_step * self.rate_constant * c_red * (self.c_oxidant ** self.oxidant_stoich)
delta_ox = delta_concentration
delta_red = -delta_concentration
self.c_oxidant -= delta_concentration * self.oxidant_stoich
self.c_oxidant = max(self.c_oxidant, 0.0)
return delta_ox, delta_red
[docs]class AutoReduction(DegradationMechanism):
"""
Provides a 1st order auto-reduction mechanism, (ox --> red) with no loss of active material. This can be thought
of as a chemical reduction of the redox-active, balanced by a reductant e.g., water splitting (OER). This could
occur in a high-potential posolyte and be considered a self-discharge. If it is desired for the concentration of
the reductant to affect the chemical reduction rate, the initial reductant concentration and the stoichiometric
factor i.e., ox + n*reductant --> red + ..., can be input. This could simulate the effect of O2 leaving the system.
Parameters
----------
rate_constant : float
First order rate of auto-reduction (1/s).
c_reductant : float
Initial concentration of reductant, defaults to 0.0 (M).
reductant_stoich : int
Number of reductants involved in the chemical reduction of the redox-active species, defaults to 0.
"""
def __init__(self, rate_constant: float, c_reductant: float = 0.0, reductant_stoich: int = 0) -> None:
super().__init__()
self.rate_constant = rate_constant
self.c_reductant = c_reductant
self.reductant_stoich = reductant_stoich
if self.rate_constant <= 0.0:
raise ValueError("'rate_constant' must be > 0.0")
if self.c_reductant < 0.0:
raise ValueError("'c_reductant' must be >= 0.0")
if self.reductant_stoich < 0:
raise ValueError("'reductant_stoich' must be >= 0")
if (self.reductant_stoich > 0 and c_reductant == 0.0) or (self.reductant_stoich == 0 and c_reductant > 0.0):
raise ValueError("'c_reductant' and 'reductant_stoich' must both be zero, or both be positive")
[docs] def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, float]:
"""
Applies an auto-reduction mechanism to oxidized/reduced species at each time step.
Defaults to first order process: ox --> red.
Parameters
----------
c_ox : float
Concentration of oxidized species (M).
c_red : float
Concentration of reduced species (M).
time_step : float
Time interval size (s).
Returns
-------
delta_ox : float
Change in concentration of oxidized species (M).
delta_red : float
Change in concentration of reduced species (M).
"""
delta_concentration = time_step * self.rate_constant * c_ox * (self.c_reductant ** self.reductant_stoich)
delta_ox = -delta_concentration
delta_red = delta_concentration
self.c_reductant -= delta_concentration * self.reductant_stoich
self.c_reductant = max(self.c_reductant, 0.0)
return delta_ox, delta_red
[docs]class Dimerization(DegradationMechanism):
"""
Provides a reversible dimerization mechanism: ox + red <--> dimer.
Parameters
----------
forward_rate_constant : float
Second order rate constant for forward reaction (1/(M s)).
backward_rate_constant : float
First order rate constant for backward reaction (1/s).
c_dimer : float
Initial concentration of dimer, defaults to 0.0 (M).
"""
def __init__(self, forward_rate_constant: float, backward_rate_constant: float, c_dimer: float = 0.0) -> None:
super().__init__(c_dimer=c_dimer)
self.forward_rate_constant = forward_rate_constant
self.backward_rate_constant = backward_rate_constant
if self.forward_rate_constant <= 0.0:
raise ValueError("'forward_rate_constant' must be > 0.0")
if self.backward_rate_constant <= 0.0:
raise ValueError("'backward_rate_constant' must be > 0.0")
if c_dimer < 0.0:
raise ValueError("'c_dimer' must be >= 0.0")
[docs] def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, float]:
"""
Applies a reversible dimerization mechanism to oxidized/reduced species at each time step.
Returns updated concentrations.
Parameters
----------
c_ox : float
Concentration of oxidized species (M).
c_red : float
Concentration of reduced species (M).
time_step : float
Time interval size (s).
Returns
-------
delta_ox : float
Change in concentration of oxidized species (M).
delta_red : float
Change in concentration of reduced species (M).
"""
delta_concentration = time_step * (
(self.forward_rate_constant * c_ox * c_red) - (self.backward_rate_constant * self.c_products['c_dimer'])
)
delta_ox = -delta_concentration
delta_red = -delta_concentration
self.c_products['c_dimer'] += delta_concentration
return delta_ox, delta_red
[docs]class MultiDegradationMechanism(DegradationMechanism):
"""
Provides usage of multiple degradation mechanisms that implement the DegradationMechanism abstract base class.
Allows for different and/or multiple mechanisms to be applied to reduced and/or oxidized species. Degradation
mechanisms are applied in the same order as the input list.
Parameters
----------
mechanisms : list[DegradationMechanism]
List of degradation mechanism subclass instances. Note that multiple instances of the same mechanism type cannot
be used in the same MultiDegradationMechanism.
"""
def __init__(self, mechanisms: list[DegradationMechanism]) -> None:
self.mechanisms = mechanisms
c_products: dict[str, float] = {}
for mechanism in self.mechanisms:
if not isinstance(mechanism, DegradationMechanism):
raise ValueError(f"Mechanism {mechanism} is not of type DegradationMechanism")
# Compute the union of c_products across all mechanisms
c_products |= mechanism.c_products
super().__init__(**c_products)
[docs] def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, float]:
"""
Applies multiple degradation mechanisms to oxidized/reduced species at each time step.
Concentration may be unchanged if species does not degrade.
Parameters
----------
c_ox : float
Concentration of oxidized species (M).
c_red : float
Concentration of reduced species (M).
time_step : float
Time interval size (s).
Returns
-------
delta_ox : float
Change in concentration of oxidized species (M).
delta_red : float
Change in concentration of reduced species (M).
"""
deltas = [mechanism.degrade(c_ox, c_red, time_step) for mechanism in self.mechanisms]
delta_ox, delta_red = (sum(x) for x in zip(*deltas))
# Update the c_products dictionary to be the union of all mechanisms' dictionaries
self.c_products = reduce(ior, [mechanism.c_products for mechanism in self.mechanisms], {})
return delta_ox, delta_red