From 59ad4bd581579f7358f5d03fd61b7ecc56dbbf54 Mon Sep 17 00:00:00 2001 From: Eric Fell Date: Tue, 23 Jan 2024 10:02:20 -0500 Subject: [PATCH 1/4] fixes to docstring punctutation, types --- src/rfbzero/experiment.py | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/rfbzero/experiment.py b/src/rfbzero/experiment.py index 62da16a..0bbf1f9 100644 --- a/src/rfbzero/experiment.py +++ b/src/rfbzero/experiment.py @@ -21,9 +21,9 @@ class CyclingProtocolResults: ---------- duration : float Simulation time (s). - time_increment: float + time_increment : float Simulation time step (s). - charge_first: bool + charge_first : bool True if CLS charges first, False if CLS discharges first. """ @@ -209,15 +209,15 @@ class _CycleMode(ABC): True if charging, False if discharging. cell_model : ZeroDModel Defined cell parameters for simulating. - results: CyclingProtocolResults + results : CyclingProtocolResults Container for the simulation result data. - update_concentrations: Callable[[float], None] + update_concentrations : Callable[[float], None] Performs coulomb counting, concentration updates via (optional) degradation and crossover mechanisms. - current: float + current : float Desired initial current for cycling. - current_lim_cls: float + current_lim_cls : float Limiting current of CLS (A). - current_lim_ncls: float + current_lim_ncls : float Limiting current of NCLS (A). """ @@ -281,13 +281,13 @@ class _ConstantCurrentCycleMode(_CycleMode): True if charging, False if discharging. cell_model : ZeroDModel Defined cell parameters for simulating. - results: CyclingProtocolResults + results : CyclingProtocolResults Container for the simulation result data. - update_concentrations: Callable[[float], None] + update_concentrations : Callable[[float], None] Performs coulomb counting, concentration updates via (optional) degradation and crossover mechanisms. - current: float + current : float Desired current for CC cycling during cycling mode (A). - voltage_limit: float + voltage_limit : float Desired voltage limit for CC cycling during cycling mode (V). voltage_limit_capacity_check : bool True if CC mode, False if constant voltage (CV) mode. @@ -371,19 +371,19 @@ class _ConstantVoltageCycleMode(_CycleMode): True if charging, False if discharging. cell_model : ZeroDModel Defined cell parameters for simulating. - results: CyclingProtocolResults + results : CyclingProtocolResults Container for the simulation result data. - update_concentrations: Callable[[float], None] + update_concentrations : Callable[[float], None] Performs coulomb counting, concentration updates via (optional) degradation and crossover mechanisms. - current_cutoff: float + current_cutoff : float Current cutoff for CV mode. Below it, simulation switches from charge to discharge and vice versa (A). - voltage_limit: float + voltage_limit : float Desired voltage limit for CV cycling during cycling mode (V). - current_estimate: float + current_estimate : float Guess for next step's current value, used for the solver (A). - current_lim_cls: float + current_lim_cls : float Limiting current of CLS (A). - current_lim_ncls: float + current_lim_ncls : float Limiting current of NCLS (A). """ @@ -601,9 +601,9 @@ class ConstantCurrent(CyclingProtocol): Voltage below which cell will switch to charge (V). current : float Instantaneous current flowing (A). - current_charge: float + current_charge : float Desired charging current for CC cycling (A). - current_discharge: + current_discharge : float Desired discharging current for CC cycling (A). Input must be a negative value. charge_first : bool @@ -824,9 +824,9 @@ class ConstantCurrentConstantVoltage(CyclingProtocol): Current above which CV discharging will switch to CC portion of CCCV charge (A). current : float Instantaneous current flowing (A). - current_charge: float + current_charge : float Desired charging current for CC cycling (A). - current_discharge: + current_discharge : float Desired discharging current for CC cycling (A). Input must be a negative value. charge_first : bool From b57d895b45a22b701e306bc75079ea6ad8849a22 Mon Sep 17 00:00:00 2001 From: Eric Fell Date: Tue, 23 Jan 2024 10:50:21 -0500 Subject: [PATCH 2/4] incorporate autoredox release mechanisms into autoox/autored --- src/rfbzero/degradation.py | 174 ++++++++++++++++--------------------- tests/test_degradation.py | 17 +--- 2 files changed, 75 insertions(+), 116 deletions(-) diff --git a/src/rfbzero/degradation.py b/src/rfbzero/degradation.py index d6e44d4..092a1a1 100644 --- a/src/rfbzero/degradation.py +++ b/src/rfbzero/degradation.py @@ -76,25 +76,40 @@ def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, fl 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 the reduction of some species not of interest to the model - i.e., water splitting (HER). This could occur in a low-potential negolyte and be considered a self-discharge. + 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) -> None: + + def __init__(self, rate_constant: float, c_oxidant: float = 0.0, oxidant_stoich: int = 0) -> None: 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") + def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, float]: """ - Assumes first order process: red --> ox. + Defaults to first order process: red --> ox. Parameters ---------- @@ -114,34 +129,50 @@ def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, fl """ - delta_concentration = timestep * self.rate_constant * c_red + delta_concentration = timestep * self.rate_constant * c_red * (self.c_oxidant ** self.oxidant_stoich) c_ox += delta_concentration c_red -= delta_concentration + self.c_oxidant -= delta_concentration * self.oxidant_stoich + self.c_oxidant = max(self.c_oxidant, 0.0) return c_ox, c_red 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 the oxidation of some species not of interest to the model - i.e., water splitting (OER). This could occur in a high-potential posolyte and be considered a self-discharge. + 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-oxidation (1/s). + 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) -> None: + def __init__(self, rate_constant: float, c_reductant: float = 0.0, reductant_stoich: int = 0) -> None: 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") + def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, float]: """ - Assumes first order process: ox --> red. + Defaults to first order process: ox --> red. Parameters ---------- @@ -161,41 +192,48 @@ def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, fl """ - delta_concentration = timestep * self.rate_constant * c_ox + delta_concentration = timestep * self.rate_constant * c_ox * (self.c_reductant ** self.reductant_stoich) c_ox -= delta_concentration c_red += delta_concentration + self.c_reductant -= delta_concentration * self.reductant_stoich + self.c_reductant = max(self.c_reductant, 0.0) return c_ox, c_red -class AutoReductionO2Release(DegradationMechanism): +class Dimerization(DegradationMechanism): """ - Provides a 1st order auto-reduction mechanism, (ox --> red) with no loss of active material, but a rate constant - that decreases linearly with time. Similar to a reduction of the redox-active, balanced by the oxidation of some - species not of interest to the model, but that can escape i.e., oxygen evolution (OER). This results in a - decreasing rate constant over time. + Provides a reversible dimerization mechanism: ox + red <--> dimer. Parameters ---------- - rate_constant : float - First order rate of auto-oxidation (1/s). - release_factor : float - Rate of gas release e.g. (unit/s). + 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, rate_constant: float, release_factor: float) -> None: - self.rate_constant = rate_constant - self.release_factor = release_factor + def __init__(self, forward_rate_constant: float, backward_rate_constant: float, c_dimer: float = 0.0) -> None: + self.forward_rate_constant = forward_rate_constant + self.backward_rate_constant = backward_rate_constant + self.c_dimer = c_dimer - if self.rate_constant < 0.0: - raise ValueError("'rate_constant' must be > 0.0") - if self.release_factor < 0.0: - raise ValueError("'release_factor' must be > 0.0") + 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 self.c_dimer < 0.0: + raise ValueError("'c_dimer' must be >= 0.0") def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, float]: """ - Assumes first order process: ox --> red. + Reversible dimerization: ox + red <--> dimer. + Returns updated concentrations. Parameters ---------- @@ -209,20 +247,17 @@ def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, fl Returns ------- c_ox : float - Updated concentration of oxidized species (M). + Concentration of oxidized species (M). c_red : float - Updated concentration of reduced species (M). + Concentration of reduced species (M). """ - # normal auto-reduction step - delta_concentration = timestep * self.rate_constant * c_ox - c_ox -= delta_concentration - c_red += delta_concentration + delta = timestep * ((self.forward_rate_constant * c_ox * c_red) - (self.backward_rate_constant * self.c_dimer)) - # now continuously adjust rate constant based on release factor - self.rate_constant -= self.rate_constant * self.release_factor * timestep - self.rate_constant = max(self.rate_constant, 0.0) + self.c_dimer += delta + c_red -= delta + c_ox -= delta return c_ox, c_red @@ -272,64 +307,3 @@ def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, fl for mechanism in self.mechanisms: c_ox, c_red = mechanism.degrade(c_ox, c_red, timestep) return c_ox, c_red - - -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 (M). - - """ - - def __init__(self, forward_rate_constant: float, backward_rate_constant: float, c_dimer: float = 0.0) -> None: - self.forward_rate_constant = forward_rate_constant - self.backward_rate_constant = backward_rate_constant - self.c_dimer = c_dimer - - 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 self.c_dimer < 0.0: - raise ValueError("'c_dimer' must be >= 0.0") - - def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, float]: - """ - Reversible dimerization: ox + red <--> dimer. - Returns updated concentrations. - - Parameters - ---------- - c_ox : float - Concentration of oxidized species (M). - c_red : float - Concentration of reduced species (M). - timestep : float - Time interval size (s). - - Returns - ------- - c_ox : float - Concentration of oxidized species (M). - c_red : float - Concentration of reduced species (M). - - """ - - delta = timestep * ((self.forward_rate_constant * c_ox * c_red) - (self.backward_rate_constant * self.c_dimer)) - - self.c_dimer += delta - c_red -= delta - c_ox -= delta - - return c_ox, c_red diff --git a/tests/test_degradation.py b/tests/test_degradation.py index 335a811..8cb3c08 100644 --- a/tests/test_degradation.py +++ b/tests/test_degradation.py @@ -2,7 +2,7 @@ #import numpy as np from rfbzero.degradation import (DegradationMechanism, ChemicalDegradation, AutoOxidation, AutoReduction, - MultiDegradationMechanism, AutoReductionO2Release, Dimerization) + Dimerization, MultiDegradationMechanism) class TestDegradationMechanism: @@ -68,21 +68,6 @@ def test_auto_red_degrade(self): assert c_r == 0.51 -class TestAutoReductionO2Release: - - @pytest.mark.parametrize("constant,factor", [(-1, -1), (0, -1), (-0.01, 11)]) - def test_auto_red_o2_init(self, constant, factor): - with pytest.raises(ValueError): - AutoReductionO2Release(rate_constant=constant, release_factor=factor) - - def test_auto_red_o2_degrade(self): - test_autoredo2 = AutoReductionO2Release(rate_constant=0.1, release_factor=0.01) - c_o, c_r = test_autoredo2.degrade(c_ox=1, c_red=0.5, timestep=0.1) - assert c_o == 0.99 - assert c_r == 0.51 - assert test_autoredo2.rate_constant == 0.0999 - - class TestMultiDegradationMechanism: @pytest.mark.parametrize("mech", [(1, True)]) From a4edfa06d10bb2b270978f0e7877ec50a96cecfd Mon Sep 17 00:00:00 2001 From: Eric Fell Date: Tue, 23 Jan 2024 19:32:47 -0500 Subject: [PATCH 3/4] add checks and tests for invalid auto-ox/red inputs --- src/rfbzero/degradation.py | 6 ++++++ tests/test_degradation.py | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/rfbzero/degradation.py b/src/rfbzero/degradation.py index 092a1a1..c26cf63 100644 --- a/src/rfbzero/degradation.py +++ b/src/rfbzero/degradation.py @@ -107,6 +107,9 @@ def __init__(self, rate_constant: float, c_oxidant: float = 0.0, oxidant_stoich: 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.0 and c_oxidant > 0): + raise ValueError("'c_oxidant' and 'oxidant_stoich' must both be zero, or both be non-zero") + def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, float]: """ Defaults to first order process: red --> ox. @@ -170,6 +173,9 @@ def __init__(self, rate_constant: float, c_reductant: float = 0.0, reductant_sto 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.0 and c_reductant > 0): + raise ValueError("'c_reductant' and 'reductant_stoich' must both be zero, or both be non-zero") + def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, float]: """ Defaults to first order process: ox --> red. diff --git a/tests/test_degradation.py b/tests/test_degradation.py index 8cb3c08..3e84431 100644 --- a/tests/test_degradation.py +++ b/tests/test_degradation.py @@ -47,6 +47,11 @@ def test_auto_ox_init(self, constant): with pytest.raises(ValueError): AutoOxidation(rate_constant=constant) + @pytest.mark.parametrize("c_oxid, stoich", [(0, 1), (-1, -1), (2, -0.01)]) + def test_auto_ox_stoich(self, c_oxid, stoich): + with pytest.raises(ValueError): + AutoOxidation(rate_constant=0.1, c_oxidant=c_oxid, oxidant_stoich=stoich) + def test_auto_ox_degrade(self): test_autoox = AutoOxidation(rate_constant=0.1) c_o, c_r = test_autoox.degrade(c_ox=1, c_red=0.5, timestep=0.1) @@ -61,6 +66,11 @@ def test_auto_red_init(self, constant): with pytest.raises(ValueError): AutoReduction(rate_constant=constant) + @pytest.mark.parametrize("c_reduct, stoich", [(0, 5), (-11, -1), (32, -0.61)]) + def test_auto_red_stoich(self, c_reduct, stoich): + with pytest.raises(ValueError): + AutoReduction(rate_constant=0.1, c_reductant=c_reduct, reductant_stoich=stoich) + def test_auto_red_degrade(self): test_autored = AutoReduction(rate_constant=0.1) c_o, c_r = test_autored.degrade(c_ox=1, c_red=0.5, timestep=0.1) From ce5c8c12ebb749a766e0bac8c8551ce70711b531 Mon Sep 17 00:00:00 2001 From: Eric Fell Date: Tue, 23 Jan 2024 20:32:17 -0500 Subject: [PATCH 4/4] typos --- src/rfbzero/degradation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/rfbzero/degradation.py b/src/rfbzero/degradation.py index c26cf63..4b7866c 100644 --- a/src/rfbzero/degradation.py +++ b/src/rfbzero/degradation.py @@ -107,8 +107,8 @@ def __init__(self, rate_constant: float, c_oxidant: float = 0.0, oxidant_stoich: 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.0 and c_oxidant > 0): - raise ValueError("'c_oxidant' and 'oxidant_stoich' must both be zero, or both be non-zero") + 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") def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, float]: """ @@ -173,8 +173,8 @@ def __init__(self, rate_constant: float, c_reductant: float = 0.0, reductant_sto 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.0 and c_reductant > 0): - raise ValueError("'c_reductant' and 'reductant_stoich' must both be zero, or both be non-zero") + 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") def degrade(self, c_ox: float, c_red: float, timestep: float) -> tuple[float, float]: """