diff --git a/.pylintrc b/.pylintrc index 597972d..e06dde6 100644 --- a/.pylintrc +++ b/.pylintrc @@ -436,6 +436,7 @@ disable=raw-checker-failed, too-many-arguments, too-many-locals, too-many-statements, + too-many-lines, protected-access # Enable the message, report, category or checker with the given id(s). You can diff --git a/src/rfbzero/crossover.py b/src/rfbzero/crossover.py index 7b93669..e7c4ae8 100644 --- a/src/rfbzero/crossover.py +++ b/src/rfbzero/crossover.py @@ -51,7 +51,7 @@ def crossover( c_red_ncls: float, volume_cls: float, volume_ncls: float, - time_step: float + time_step: float, ) -> tuple[float, float, float, float, float, float]: """ Calculation of crossover species, considering permeabilities of oxidized/reduced species. @@ -100,14 +100,14 @@ def crossover( c_red_difference = c_red_cls - c_red_ncls # amount of species (mols) added/subtracted, divide by 1000 for L to cm^3 conversion - delta_ox_mols = time_step * self.permeability_ox * membrane_constant * (c_ox_difference / 1000) - delta_red_mols = time_step * self.permeability_red * membrane_constant * (c_red_difference / 1000) + crossed_ox_mols = time_step * self.permeability_ox * membrane_constant * (c_ox_difference / 1000) + crossed_red_mols = time_step * self.permeability_red * membrane_constant * (c_red_difference / 1000) # update concentrations (M) - c_ox_cls -= delta_ox_mols / volume_cls - c_ox_ncls += delta_ox_mols / volume_ncls + delta_ox_cls = -(crossed_ox_mols / volume_cls) + delta_ox_ncls = crossed_ox_mols / volume_ncls - c_red_cls -= delta_red_mols / volume_cls - c_red_ncls += delta_red_mols / volume_ncls + delta_red_cls = -(crossed_red_mols / volume_cls) + delta_red_ncls = crossed_red_mols / volume_ncls - return c_ox_cls, c_red_cls, c_ox_ncls, c_red_ncls, delta_ox_mols, delta_red_mols + return delta_ox_cls, delta_red_cls, delta_ox_ncls, delta_red_ncls, crossed_ox_mols, crossed_red_mols diff --git a/src/rfbzero/degradation.py b/src/rfbzero/degradation.py index fefcda2..c578a44 100644 --- a/src/rfbzero/degradation.py +++ b/src/rfbzero/degradation.py @@ -8,6 +8,9 @@ class DegradationMechanism(ABC): """Abstract base class to be implemented by specific degradation mechanisms.""" + def __init__(self, **c_products: float): + self.c_products = c_products + @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.""" @@ -28,6 +31,7 @@ class ChemicalDegradationOxidized(DegradationMechanism): """ def __init__(self, rate_order: int, rate_constant: float) -> None: + super().__init__() self.rate_order = rate_order self.rate_constant = rate_constant @@ -53,15 +57,16 @@ def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, f Returns ------- - c_ox : float - Updated concentration of oxidized species (M). - c_red : float - Unchanged concentration of reduced species (M). + 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. """ - c_ox -= (time_step * self.rate_constant * (c_ox ** self.rate_order)) - return c_ox, c_red + delta_ox = -(time_step * self.rate_constant * (c_ox ** self.rate_order)) + delta_red = 0.0 + return delta_ox, delta_red class ChemicalDegradationReduced(DegradationMechanism): @@ -78,6 +83,7 @@ class ChemicalDegradationReduced(DegradationMechanism): """ def __init__(self, rate_order: int, rate_constant: float) -> None: + super().__init__() self.rate_order = rate_order self.rate_constant = rate_constant @@ -103,15 +109,16 @@ def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, f Returns ------- - c_ox : float - Unchanged concentration of oxidized species (M). - c_red : float - Updated concentration of reduced species (M). + 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). """ - c_red -= (time_step * self.rate_constant * (c_red ** self.rate_order)) - return c_ox, c_red + delta_ox = 0.0 + delta_red = -(time_step * self.rate_constant * (c_red ** self.rate_order)) + return delta_ox, delta_red class AutoOxidation(DegradationMechanism): @@ -134,6 +141,7 @@ class AutoOxidation(DegradationMechanism): """ 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 @@ -166,20 +174,21 @@ def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, f Returns ------- - c_ox : float - Updated concentration of oxidized species (M). - c_red : float - Updated concentration of reduced species (M). + 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) - c_ox += delta_concentration - c_red -= delta_concentration + 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 c_ox, c_red + + return delta_ox, delta_red class AutoReduction(DegradationMechanism): @@ -201,6 +210,7 @@ class AutoReduction(DegradationMechanism): """ 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 @@ -233,20 +243,21 @@ def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, f Returns ------- - c_ox : float - Updated concentration of oxidized species (M). - c_red : float - Updated concentration of reduced species (M). + 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) - c_ox -= delta_concentration - c_red += delta_concentration + 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 c_ox, c_red + + return delta_ox, delta_red class Dimerization(DegradationMechanism): @@ -265,9 +276,9 @@ class Dimerization(DegradationMechanism): """ 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 - self.c_dimer = c_dimer if self.forward_rate_constant <= 0.0: raise ValueError("'forward_rate_constant' must be > 0.0") @@ -275,7 +286,7 @@ def __init__(self, forward_rate_constant: float, backward_rate_constant: float, if self.backward_rate_constant <= 0.0: raise ValueError("'backward_rate_constant' must be > 0.0") - if self.c_dimer < 0.0: + if c_dimer < 0.0: raise ValueError("'c_dimer' must be >= 0.0") def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, float]: @@ -294,22 +305,22 @@ def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, f Returns ------- - c_ox : float - Concentration of oxidized species (M). - c_red : float - Concentration of reduced species (M). + 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_dimer) + (self.forward_rate_constant * c_ox * c_red) - (self.backward_rate_constant * self.c_products['c_dimer']) ) - self.c_dimer += delta_concentration - c_red -= delta_concentration - c_ox -= delta_concentration + delta_ox = -delta_concentration + delta_red = -delta_concentration + self.c_products['c_dimer'] += delta_concentration - return c_ox, c_red + return delta_ox, delta_red class MultiDegradationMechanism(DegradationMechanism): @@ -325,6 +336,7 @@ class MultiDegradationMechanism(DegradationMechanism): """ def __init__(self, mechanisms: list[DegradationMechanism]) -> None: + super().__init__() self.mechanisms = mechanisms for mechanism in self.mechanisms: @@ -347,13 +359,13 @@ def degrade(self, c_ox: float, c_red: float, time_step: float) -> tuple[float, f Returns ------- - c_ox : float - Concentration of oxidized species (M). - c_red : float - Concentration of reduced species (M). + delta_ox : float + Change in concentration of oxidized species (M). + delta_red : float + Change in concentration of reduced species (M). """ - for mechanism in self.mechanisms: - c_ox, c_red = mechanism.degrade(c_ox, c_red, time_step) - return c_ox, c_red + 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)) + return delta_ox, delta_red diff --git a/src/rfbzero/experiment.py b/src/rfbzero/experiment.py index d52e32a..1eb2e67 100644 --- a/src/rfbzero/experiment.py +++ b/src/rfbzero/experiment.py @@ -23,15 +23,28 @@ class CyclingResults: Simulation time (s). time_step : float Simulation time step (s). - charge_first : bool - True if CLS charges first, False if CLS discharges first. + charge_first : bool, optional + True if CLS charges first, False if CLS discharges first. Defaults to True. + products_cls : list[str], optional + The names of any additional product species in the CLS. + products_ncls : list[str], optional + The names of any additional product species in the NCLS. """ - def __init__(self, duration: float, time_step: float, charge_first: bool = True) -> None: + def __init__( + self, + duration: float, + time_step: float, + charge_first: bool = True, + products_cls: list[str] = None, + products_ncls: list[str] = None, + ) -> None: self.duration = duration self.time_step = time_step self.charge_first = charge_first + self.products_cls = products_cls or [] + self.products_ncls = products_ncls or [] self.compute_soc = True #: The number of time steps that were desired from the simulation. @@ -58,10 +71,20 @@ def __init__(self, duration: float, time_step: float, charge_first: bool = True) self.c_ox_ncls: list[float] = [0.0] * self.max_steps #: The NCLS concentration of reduced species (M), at each time step. self.c_red_ncls: list[float] = [0.0] * self.max_steps + + #: The CLS concentrations of any product species (M), at each time step. + self.c_products_cls: dict[str, list[float]] = { + species: [0.0] * self.max_steps for species in self.products_cls + } + #: The NCLS concentrations of any product species (M), at each time step. + self.c_products_ncls: dict[str, list[float]] = { + species: [0.0] * self.max_steps for species in self.products_ncls + } + #: Oxidized species crossing (mols), at each time step. Only meaningful for symmetric cell. - self.delta_ox_mols: list[float] = [0.0] * self.max_steps + self.crossed_ox_mols: list[float] = [0.0] * self.max_steps #: Reduced species crossing (mols), at each time step. Only meaningful for symmetric cell. - self.delta_red_mols: list[float] = [0.0] * self.max_steps + self.crossed_red_mols: list[float] = [0.0] * self.max_steps #: The CLS state of charge, at each time step. self.soc_cls: list[float] = [0.0] * self.max_steps #: The NCLS state of charge, at each time step. @@ -99,13 +122,15 @@ def __init__(self, duration: float, time_step: float, charge_first: bool = True) def _record_step( self, cell_model: ZeroDModel, + c_products_cls: dict[str, float], + c_products_ncls: dict[str, float], charge: bool, current: float, cell_v: float, ocv: float, n_act: float = 0.0, n_mt: float = 0.0, - total_overpotential: float = 0.0 + total_overpotential: float = 0.0, ) -> None: """Records simulation data at valid time steps.""" # Update capacity @@ -131,8 +156,14 @@ def _record_step( self.c_red_cls[self.steps] = cls_red self.c_ox_ncls[self.steps] = ncls_ox self.c_red_ncls[self.steps] = ncls_red - self.delta_ox_mols[self.steps] = cell_model.delta_ox_mols - self.delta_red_mols[self.steps] = cell_model.delta_red_mols + + for species in self.products_cls: + self.c_products_cls[species][self.steps] = c_products_cls[species] + for species in self.products_ncls: + self.c_products_ncls[species][self.steps] = c_products_ncls[species] + + self.crossed_ox_mols[self.steps] = cell_model.delta_ox_mols + self.crossed_red_mols[self.steps] = cell_model.delta_red_mols # Compute state-of-charge if self.compute_soc: @@ -176,8 +207,8 @@ def _finalize(self) -> None: self.c_red_cls = self.c_red_cls[:self.steps] self.c_ox_ncls = self.c_ox_ncls[:self.steps] self.c_red_ncls = self.c_red_ncls[:self.steps] - self.delta_ox_mols = self.delta_ox_mols[:self.steps] - self.delta_red_mols = self.delta_red_mols[:self.steps] + self.crossed_ox_mols = self.crossed_ox_mols[:self.steps] + self.crossed_red_mols = self.crossed_red_mols[:self.steps] self.soc_cls = self.soc_cls[:self.steps] self.soc_ncls = self.soc_ncls[:self.steps] @@ -209,7 +240,7 @@ class _CycleMode(ABC): Defined cell parameters for simulation. results : CyclingResults Container for the simulation result data. - update_concentrations : Callable[[float], None] + update_concentrations : Callable[[float], tuple[dict[str, float], dict[str, float]]] Performs coulomb counting, concentration updates via (optional) degradation and crossover mechanisms. current : float Desired initial current for cycling. @@ -224,10 +255,10 @@ def __init__( charge: bool, cell_model: ZeroDModel, results: CyclingResults, - update_concentrations: Callable[[float], None], + update_concentrations: Callable[[float], tuple[dict[str, float], dict[str, float]]], current: float, current_lim_cls: float = None, - current_lim_ncls: float = None + current_lim_ncls: float = None, ) -> None: self.charge = charge self.cell_model = cell_model @@ -281,7 +312,7 @@ class _ConstantCurrentCycleMode(_CycleMode): Defined cell parameters for simulation. results : CyclingResults Container for the simulation result data. - update_concentrations : Callable[[float], None] + update_concentrations : Callable[[float], tuple[dict[str, float], dict[str, float]]] Performs coulomb counting, concentration updates via (optional) degradation and crossover mechanisms. current : float Desired current for CC cycling during cycling mode (A). @@ -296,10 +327,10 @@ def __init__( charge: bool, cell_model: ZeroDModel, results: CyclingResults, - update_concentrations: Callable[[float], None], + update_concentrations: Callable[[float], tuple[dict[str, float], dict[str, float]]], current: float, voltage_limit: float, - voltage_limit_capacity_check: bool = True + voltage_limit_capacity_check: bool = True, ) -> None: super().__init__(charge, cell_model, results, update_concentrations, current) self.voltage_limit = voltage_limit @@ -338,7 +369,7 @@ def cycle_step(self) -> CyclingStatus: cycling_status = CyclingStatus.NORMAL # Calculate species' concentrations - self.update_concentrations(self.current) + c_products_cls, c_products_ncls = self.update_concentrations(self.current) # Handle edge case where the voltage limits are never reached if self.cell_model._negative_concentrations(): @@ -360,13 +391,15 @@ def cycle_step(self) -> CyclingStatus: # Update results self.results._record_step( self.cell_model, + c_products_cls, + c_products_ncls, self.charge, self.current, cell_v, ocv, n_act, n_mt, - total_overpotential + total_overpotential, ) return self._check_time(cycling_status) @@ -384,7 +417,7 @@ class _ConstantVoltageCycleMode(_CycleMode): Defined cell parameters for simulation. results : CyclingResults Container for the simulation result data. - update_concentrations : Callable[[float], None] + update_concentrations : Callable[[float], tuple[dict[str, float], dict[str, float]]] Performs coulomb counting, concentration updates via (optional) degradation and crossover mechanisms. current_cutoff : float Current cutoff for CV mode. Below it, simulation switches from charge to discharge and vice versa (A). @@ -403,12 +436,12 @@ def __init__( charge: bool, cell_model: ZeroDModel, results: CyclingResults, - update_concentrations: Callable[[float], None], + update_concentrations: Callable[[float], tuple[dict[str, float], dict[str, float]]], current_cutoff: float, voltage_limit: float, current_estimate: float, current_lim_cls: float = None, - current_lim_ncls: float = None + current_lim_ncls: float = None, ) -> None: super().__init__(charge, cell_model, results, update_concentrations, current_estimate, current_lim_cls, current_lim_ncls) @@ -431,7 +464,7 @@ def cycle_step(self) -> CyclingStatus: # Adapting the solver's guess to the updated current self.__find_min_current(ocv) - self.update_concentrations(self.current) + c_products_cls, c_products_ncls = self.update_concentrations(self.current) # Check if any reactant remains if self.cell_model._negative_concentrations(): @@ -439,7 +472,15 @@ def cycle_step(self) -> CyclingStatus: return self._check_capacity(CyclingStatus.NEGATIVE_CONCENTRATIONS) # Update results - self.results._record_step(self.cell_model, self.charge, self.current, self.voltage_limit, ocv) + self.results._record_step( + self.cell_model, + c_products_cls, + c_products_ncls, + self.charge, + self.current, + self.voltage_limit, + ocv, + ) if abs(self.current) <= abs(self.current_cutoff): return self._check_capacity(CyclingStatus.CURRENT_CUTOFF_REACHED) @@ -462,7 +503,7 @@ def solver(current) -> float: loss_solve, *_ = self.cell_model._total_overpotential( current.item(), self.current_lim_cls, - self.current_lim_ncls + self.current_lim_ncls, ) return self.voltage_limit - ocv - self.__current_direction() * loss_solve @@ -526,7 +567,7 @@ def _validate_cycle_values( value: Optional[float], value_charge: Optional[float], value_discharge: Optional[float], - name: str + name: str, ) -> tuple[float, float]: """Checks validity of user inputs for current limits and/or cutoffs.""" if value is not None and (value_charge is not None or value_discharge is not None): @@ -554,8 +595,8 @@ def _validate_protocol( degradation: Optional[DegradationMechanism], cls_degradation: Optional[DegradationMechanism], ncls_degradation: Optional[DegradationMechanism], - crossover: Optional[Crossover] - ) -> tuple[CyclingResults, Callable[[float], None]]: + crossover: Optional[Crossover], + ) -> tuple[CyclingResults, Callable[[float], tuple[dict[str, float], dict[str, float]]]]: """Checks validity of user inputs for voltage limits and optional degradation and crossover mechanisms.""" if not self.voltage_limit_discharge < cell_model.ocv_50_soc < self.voltage_limit_charge: raise ValueError("Ensure that 'voltage_limit_discharge' < 'ocv_50_soc' < 'voltage_limit_charge'") @@ -577,16 +618,19 @@ def _validate_protocol( # the passed in instances can be reused across protocol runs. cls_degradation = copy.deepcopy(cls_degradation) ncls_degradation = copy.deepcopy(ncls_degradation) + c_products_cls = cls_degradation.c_products if cls_degradation else {} + c_products_ncls = ncls_degradation.c_products if ncls_degradation else {} if cell_model._negative_concentrations(): raise ValueError('Negative concentration detected') - def update_concentrations(i: float) -> None: + def update_concentrations(i: float) -> tuple[dict[str, float], dict[str, float]]: # Performs coulomb counting, concentration updates via (optional) degradation and crossover mechanisms - cell_model._coulomb_counter(i, cls_degradation, ncls_degradation, crossover) + return cell_model._coulomb_counter(i, cls_degradation, ncls_degradation, crossover) # Initialize data results object to be sent to user - results = CyclingResults(duration, cell_model.time_step, self.charge_first) + results = CyclingResults(duration, cell_model.time_step, self.charge_first, + list(c_products_cls.keys()), list(c_products_ncls.keys())) print(f'{duration} sec of cycling, time steps: {cell_model.time_step} sec') return results, update_concentrations @@ -633,7 +677,7 @@ def __init__( ) -> None: super().__init__(voltage_limit_charge, voltage_limit_discharge, charge_first) self.current_charge, self.current_discharge = self._validate_cycle_values( - current, current_charge, current_discharge, 'current' + current, current_charge, current_discharge, 'current', ) def run( @@ -643,7 +687,7 @@ def run( degradation: DegradationMechanism = None, cls_degradation: DegradationMechanism = None, ncls_degradation: DegradationMechanism = None, - crossover: Crossover = None + crossover: Crossover = None, ) -> CyclingResults: """ Applies the constant current (CC) protocol and (optional) degradation/crossover mechanisms to a cell model. @@ -671,7 +715,7 @@ def run( """ results, update_concentrations = self._validate_protocol( - duration, cell_model, degradation, cls_degradation, ncls_degradation, crossover + duration, cell_model, degradation, cls_degradation, ncls_degradation, crossover, ) def get_cycle_mode(charge: bool) -> _ConstantCurrentCycleMode: @@ -682,7 +726,7 @@ def get_cycle_mode(charge: bool) -> _ConstantCurrentCycleMode: results, update_concentrations, self.current_charge if charge else self.current_discharge, - self.voltage_limit_charge if charge else self.voltage_limit_discharge + self.voltage_limit_charge if charge else self.voltage_limit_discharge, ) cycle_mode = get_cycle_mode(self.charge_first) @@ -744,7 +788,7 @@ def __init__( ) -> None: super().__init__(voltage_limit_charge, voltage_limit_discharge, charge_first) self.current_cutoff_charge, self.current_cutoff_discharge = self._validate_cycle_values( - current_cutoff, current_cutoff_charge, current_cutoff_discharge, 'current_cutoff' + current_cutoff, current_cutoff_charge, current_cutoff_discharge, 'current_cutoff', ) def run( @@ -754,7 +798,7 @@ def run( degradation: DegradationMechanism = None, cls_degradation: DegradationMechanism = None, ncls_degradation: DegradationMechanism = None, - crossover: Crossover = None + crossover: Crossover = None, ) -> CyclingResults: """ Applies the constant voltage (CV) cycling protocol and (optional) degradation/crossover mechanisms to a cell @@ -783,7 +827,7 @@ def run( """ results, update_concentrations = self._validate_protocol( - duration, cell_model, degradation, cls_degradation, ncls_degradation, crossover + duration, cell_model, degradation, cls_degradation, ncls_degradation, crossover, ) def get_cycle_mode(charge: bool) -> _ConstantVoltageCycleMode: @@ -794,7 +838,7 @@ def get_cycle_mode(charge: bool) -> _ConstantVoltageCycleMode: update_concentrations, self.current_cutoff_charge if charge else self.current_cutoff_discharge, self.voltage_limit_charge if charge else self.voltage_limit_discharge, - 0.0 + 0.0, ) cycle_mode = get_cycle_mode(self.charge_first) @@ -860,10 +904,10 @@ def __init__( ) -> None: super().__init__(voltage_limit_charge, voltage_limit_discharge, charge_first) self.current_cutoff_charge, self.current_cutoff_discharge = self._validate_cycle_values( - current_cutoff, current_cutoff_charge, current_cutoff_discharge, 'current_cutoff' + current_cutoff, current_cutoff_charge, current_cutoff_discharge, 'current_cutoff', ) self.current_charge, self.current_discharge = self._validate_cycle_values( - current, current_charge, current_discharge, 'current' + current, current_charge, current_discharge, 'current', ) def run( @@ -873,7 +917,7 @@ def run( degradation: DegradationMechanism = None, cls_degradation: DegradationMechanism = None, ncls_degradation: DegradationMechanism = None, - crossover: Crossover = None + crossover: Crossover = None, ) -> CyclingResults: """ Applies the constant current constant voltage (CCCV) cycling protocol and (optional) degradation/crossover @@ -901,7 +945,7 @@ def run( """ results, update_concentrations = self._validate_protocol( - duration, cell_model, degradation, cls_degradation, ncls_degradation, crossover + duration, cell_model, degradation, cls_degradation, ncls_degradation, crossover, ) def get_cc_cycle_mode(charge: bool) -> _ConstantCurrentCycleMode: @@ -912,7 +956,7 @@ def get_cc_cycle_mode(charge: bool) -> _ConstantCurrentCycleMode: update_concentrations, self.current_charge if charge else self.current_discharge, self.voltage_limit_charge if charge else self.voltage_limit_discharge, - voltage_limit_capacity_check=False + voltage_limit_capacity_check=False, ) def get_cv_cycle_mode( @@ -930,7 +974,7 @@ def get_cv_cycle_mode( self.voltage_limit_charge if charge else self.voltage_limit_discharge, current_estimate, current_lim_cls, - current_lim_ncls + current_lim_ncls, ) cycle_mode: _CycleMode = get_cc_cycle_mode(self.charge_first) @@ -950,8 +994,11 @@ def get_cv_cycle_mode( if is_cc_mode: if cycling_status == CyclingStatus.VOLTAGE_LIMIT_REACHED: is_cc_mode = False - cycle_mode = get_cv_cycle_mode(cycle_mode.charge, cycle_mode.current, - cycle_mode.current_lim_cls, cycle_mode.current_lim_ncls) + cycle_mode = get_cv_cycle_mode(cycle_mode.charge, + cycle_mode.current, + cycle_mode.current_lim_cls, + cycle_mode.current_lim_ncls, + ) cycling_status = CyclingStatus.NORMAL elif cycling_status == CyclingStatus.NEGATIVE_CONCENTRATIONS: # Record info for the half cycle diff --git a/src/rfbzero/redox_flow_cell.py b/src/rfbzero/redox_flow_cell.py index 47d6a11..3098884 100644 --- a/src/rfbzero/redox_flow_cell.py +++ b/src/rfbzero/redox_flow_cell.py @@ -106,7 +106,7 @@ def __init__( roughness_factor: float = 26.0, num_electrons_cls: int = 1, num_electrons_ncls: int = 1, - temperature: float = 298.0 + temperature: float = 298.0, ) -> None: self.volume_cls = volume_cls self.volume_ncls = volume_ncls @@ -145,7 +145,6 @@ def __init__( 'ocv_50_soc': self.ocv_50_soc, 'resistance': self.resistance, 'k_0_cls': self.k_0_cls, 'k_0_ncls': self.k_0_ncls, 'geometric_area': self.geometric_area, 'time_step': self.time_step, 'k_mt': self.k_mt, 'const_i_ex': self.const_i_ex, - 'num_electrons_cls': self.num_electrons_cls, 'num_electrons_ncls': self.num_electrons_ncls, 'temperature': temperature}.items(): if key not in ['ocv_50_soc', 'resistance', @@ -382,8 +381,8 @@ def _coulomb_counter( current: float, cls_degradation: DegradationMechanism = None, ncls_degradation: DegradationMechanism = None, - cross_over: Crossover = None - ) -> None: + cross_over: Crossover = None, + ) -> tuple[dict[str, float], dict[str, float]]: """ Updates all species' concentrations at each time step. Contributions from faradaic current, (optional) degradation mechanisms, and (optional) crossover mechanism. @@ -401,10 +400,10 @@ def _coulomb_counter( Returns ------- - delta_ox_mols : float - Oxidized species crossing, at each time step (mols). - delta_red_mols : float - Reduced species crossing, at each time step (mols). + c_products_cls : dict[str, float] + Updated concentrations of all CLS product species (M). + c_products_ncls : dict[str, float] + Updated concentrations of all NCLS product species (M). """ @@ -413,40 +412,57 @@ def _coulomb_counter( delta_cls = ((self.time_step * current) / (F * self.num_electrons_cls * self.volume_cls)) * direction delta_ncls = ((self.time_step * current) / (F * self.num_electrons_ncls * self.volume_ncls)) * direction + self.prev_c_ox_cls = self.c_ox_cls + self.prev_c_red_cls = self.c_red_cls + self.prev_c_ox_ncls = self.c_ox_ncls + self.prev_c_red_ncls = self.c_red_ncls + # update CLS and NCLS concentrations - c_ox_cls = self.c_ox_cls - delta_cls - c_red_cls = self.c_red_cls + delta_cls - c_ox_ncls = self.c_ox_ncls + delta_ncls - c_red_ncls = self.c_red_ncls - delta_ncls + new_c_ox_cls = self.c_ox_cls - delta_cls + new_c_red_cls = self.c_red_cls + delta_cls + new_c_ox_ncls = self.c_ox_ncls + delta_ncls + new_c_red_ncls = self.c_red_ncls - delta_ncls # for no crossover situation - delta_ox_mols = 0.0 - delta_red_mols = 0.0 + crossed_ox_mols = 0.0 + crossed_red_mols = 0.0 # Coulomb counting from optional degradation and/or crossover mechanisms + c_products_cls = {} if cls_degradation is not None: - c_ox_cls, c_red_cls = cls_degradation.degrade(c_ox_cls, c_red_cls, self.time_step) + delta_ox_cls, delta_red_cls = cls_degradation.degrade(self.c_ox_cls, self.c_red_cls, + self.time_step) + new_c_ox_cls += delta_ox_cls + new_c_red_cls += delta_red_cls + c_products_cls = cls_degradation.c_products + c_products_ncls = {} if ncls_degradation is not None: - c_ox_ncls, c_red_ncls = ncls_degradation.degrade(c_ox_ncls, c_red_ncls, self.time_step) + delta_ox_ncls, delta_red_ncls = ncls_degradation.degrade(self.c_ox_ncls, self.c_red_ncls, + self.time_step) + new_c_ox_ncls += delta_ox_ncls + new_c_red_ncls += delta_red_ncls + c_products_ncls = ncls_degradation.c_products if cross_over is not None: - (c_ox_cls, c_red_cls, c_ox_ncls, c_red_ncls, delta_ox_mols, - delta_red_mols) = cross_over.crossover(self.geometric_area, c_ox_cls, c_red_cls, c_ox_ncls, c_red_ncls, - self.volume_cls, self.volume_ncls, self.time_step) - # update concentrations to self - self.prev_c_ox_cls = self.c_ox_cls - self.prev_c_red_cls = self.c_red_cls - self.prev_c_ox_ncls = self.c_ox_ncls - self.prev_c_red_ncls = self.c_red_ncls - - self.c_ox_cls = c_ox_cls - self.c_red_cls = c_red_cls - self.c_ox_ncls = c_ox_ncls - self.c_red_ncls = c_red_ncls - - self.delta_ox_mols = delta_ox_mols - self.delta_red_mols = delta_red_mols + delta_ox_cls, delta_red_cls, delta_ox_ncls, delta_red_ncls, crossed_ox_mols, crossed_red_mols = \ + cross_over.crossover(self.geometric_area, self.c_ox_cls, self.c_red_cls, self.c_ox_ncls, + self.c_red_ncls, self.volume_cls, self.volume_ncls, self.time_step) + new_c_ox_cls += delta_ox_cls + new_c_red_cls += delta_red_cls + new_c_ox_ncls += delta_ox_ncls + new_c_red_ncls += delta_red_ncls + + # Update new concentrations to self + self.c_ox_cls = new_c_ox_cls + self.c_red_cls = new_c_red_cls + self.c_ox_ncls = new_c_ox_ncls + self.c_red_ncls = new_c_red_ncls + + self.delta_ox_mols = crossed_ox_mols + self.delta_red_mols = crossed_red_mols + + return c_products_cls, c_products_ncls def _revert_concentrations(self) -> None: """Resets concentrations to previous value if a (invalid) negative concentration is calculated.""" diff --git a/tests/test_crossover.py b/tests/test_crossover.py index d1c5777..2749fad 100644 --- a/tests/test_crossover.py +++ b/tests/test_crossover.py @@ -22,12 +22,17 @@ def test_init(self): permeability_ox=0, permeability_red=0) + with pytest.raises(ValueError): + test_cross = Crossover(membrane_thickness=1, + permeability_ox=1, + permeability_red=-3) + def test_crossover(self): test_cross = Crossover(membrane_thickness=10000, permeability_ox=2, permeability_red=1) - expected = (0.51, 0.9975, 1.495, 0.50125, -0.01, 0.0025) + expected = (0.01, -0.0025, -0.005, 0.00125, -0.01, 0.0025) assert expected == test_cross.crossover( geometric_area=5.0, c_ox_cls=0.5, diff --git a/tests/test_degradation.py b/tests/test_degradation.py index 4ddf988..37283e2 100644 --- a/tests/test_degradation.py +++ b/tests/test_degradation.py @@ -1,5 +1,5 @@ import pytest -#import numpy as np +import numpy as np from rfbzero.degradation import (DegradationMechanism, ChemicalDegradationOxidized, ChemicalDegradationReduced, AutoOxidation, AutoReduction, Dimerization, MultiDegradationMechanism) @@ -22,18 +22,18 @@ def test_chem_deg_init(self, order, constant): ChemicalDegradationReduced(rate_order=order, rate_constant=constant) with pytest.raises(ValueError): - ChemicalDegradationReduced(rate_order=order, rate_constant=constant) + ChemicalDegradationOxidized(rate_order=order, rate_constant=constant) def test_chem_deg_degrade(self): test_chemdeg = ChemicalDegradationReduced(rate_order=1, rate_constant=0.1) - c_o, c_r = test_chemdeg.degrade(c_ox=1, c_red=0.5, time_step=0.1) - assert c_o == 1 - assert c_r == 0.495 + delta_c_o, delta_c_r = test_chemdeg.degrade(c_ox=1, c_red=0.5, time_step=0.1) + assert np.isclose(delta_c_o, 0.0) + assert np.isclose(delta_c_r, -0.005) test_chemdeg2 = ChemicalDegradationOxidized(rate_order=1, rate_constant=0.1) - c_ox, c_red = test_chemdeg2.degrade(c_ox=1, c_red=0.5, time_step=0.1) - assert c_ox == 0.99 - assert c_red == 0.5 + delta_c_ox, delta_c_red = test_chemdeg2.degrade(c_ox=1, c_red=0.5, time_step=0.1) + assert np.isclose(delta_c_ox, -0.01) + assert np.isclose(delta_c_red, 0.0) class TestAutoOxidation: @@ -50,9 +50,9 @@ def test_auto_ox_stoich(self, c_oxid, 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, time_step=0.1) - assert c_o == 1.005 - assert c_r == 0.495 + delta_c_o, delta_c_r = test_autoox.degrade(c_ox=1, c_red=0.5, time_step=0.1) + assert np.isclose(delta_c_o, 0.005) + assert np.isclose(delta_c_r, -0.005) class TestAutoReduction: @@ -69,9 +69,23 @@ def test_auto_red_stoich(self, c_reduct, 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, time_step=0.1) - assert c_o == 0.99 - assert c_r == 0.51 + delta_c_o, delta_c_r = test_autored.degrade(c_ox=1, c_red=0.5, time_step=0.1) + assert np.isclose(delta_c_o, -0.01) + assert np.isclose(delta_c_r, 0.01) + + +class TestDimerization: + + @pytest.mark.parametrize("k_f,k_b,c_dim", [(-1,-1,-1), (1,-1,1), (4,11,-1)]) + def test_dimerization_init(self, k_f, k_b, c_dim): + with pytest.raises(ValueError): + Dimerization(forward_rate_constant=k_f, backward_rate_constant=k_b, c_dimer=c_dim) + + def test_dimerization_degrade(self): + test_dimerize = Dimerization(forward_rate_constant=2, backward_rate_constant=1, c_dimer=0.5) + delta_c_o, delta_c_r = test_dimerize.degrade(c_ox=1, c_red=0.5, time_step=1) + assert np.isclose(delta_c_o, -0.5) + assert np.isclose(delta_c_r, -0.5) class TestMultiDegradationMechanism: @@ -80,26 +94,12 @@ class TestMultiDegradationMechanism: def test_multi_deg_init(self, mech): with pytest.raises(ValueError): MultiDegradationMechanism(mechanisms=mech) - #raise NotImplementedError + def test_multi_deg_degrade(self): mech_1 = AutoReduction(rate_constant=0.1) mech_2 = AutoOxidation(rate_constant=0.1) mech_list = [mech_1, mech_2] test_multi = MultiDegradationMechanism(mechanisms=mech_list) - c_o, c_r = test_multi.degrade(c_ox=1, c_red=0.5, time_step=0.1) - assert c_o == 0.9951 - assert c_r == 0.5049 - - -class TestDimerization: - - @pytest.mark.parametrize("k_f,k_b,c_dim", [(-1,-1,-1), (1,-1,1), (4,11,-1)]) - def test_dimerization_init(self, k_f, k_b, c_dim): - with pytest.raises(ValueError): - Dimerization(forward_rate_constant=k_f, backward_rate_constant=k_b, c_dimer=c_dim) - - def test_dimerization_degrade(self): - test_dimerize = Dimerization(forward_rate_constant=2, backward_rate_constant=1, c_dimer=0.5) - c_o, c_r = test_dimerize.degrade(c_ox=1, c_red=0.5, time_step=1) - assert c_o == 0.5 - assert c_r == 0.0 + delta_c_o, delta_c_r = test_multi.degrade(c_ox=1, c_red=0.5, time_step=0.1) + assert np.isclose(delta_c_o, -0.005) + assert np.isclose(delta_c_r, 0.005) diff --git a/tests/test_experiment.py b/tests/test_experiment.py index f792c2a..9a69ddc 100644 --- a/tests/test_experiment.py +++ b/tests/test_experiment.py @@ -1,10 +1,11 @@ +import itertools import pytest import numpy as np from rfbzero.experiment import ConstantCurrent, ConstantCurrentConstantVoltage, ConstantVoltage from rfbzero.redox_flow_cell import ZeroDModel -from rfbzero.degradation import ChemicalDegradationOxidized, ChemicalDegradationReduced, AutoOxidation, AutoReduction, \ - MultiDegradationMechanism # , Dimerization +from rfbzero.degradation import (ChemicalDegradationOxidized, ChemicalDegradationReduced, AutoOxidation, AutoReduction, + MultiDegradationMechanism, Dimerization) from rfbzero.crossover import Crossover @@ -174,6 +175,283 @@ def test_cccv_symmetric_cell(self): vals = all_results.half_cycle_capacity[:5] assert np.isclose(vals, expected).all() + def test_cccv_current_inputs(self): + with pytest.raises(ValueError): + cell = ZeroDModel(volume_cls=0.005, # L + volume_ncls=0.03, # L + c_ox_cls=0.01, # M + c_red_cls=0.01, # M + c_ox_ncls=0.01, # M + c_red_ncls=0.01, # M + ocv_50_soc=1.1, # V + resistance=0.8, # ohms + k_0_cls=1e-3, # cm/s + k_0_ncls=1e-3, # cm/s + ) + + # define cycling protocol + protocol = ConstantCurrentConstantVoltage( + voltage_limit_charge=1.45, + voltage_limit_discharge=0.8, + current_cutoff_charge=0.005, + current_cutoff_discharge=-0.005, + current=0.1, + current_charge=0.01, + current_discharge=-0.1, + ) + + # putting it all together + all_results = protocol.run(cell_model=cell, + duration=1000, # cycle time to simulate (s) + ) + + with pytest.raises(ValueError): + cell = ZeroDModel(volume_cls=0.005, # L + volume_ncls=0.03, # L + c_ox_cls=0.01, # M + c_red_cls=0.01, # M + c_ox_ncls=0.01, # M + c_red_ncls=0.01, # M + ocv_50_soc=1.1, # V + resistance=0.8, # ohms + k_0_cls=1e-3, # cm/s + k_0_ncls=1e-3, # cm/s + ) + + # define cycling protocol + protocol = ConstantCurrentConstantVoltage( + voltage_limit_charge=1.45, + voltage_limit_discharge=0.8, + current_cutoff_charge=0.005, + current_cutoff_discharge=-0.005, + current=0.1, + current_charge=0.01, + ) + + # putting it all together + all_results = protocol.run(cell_model=cell, + duration=1000, # cycle time to simulate (s) + ) + + with pytest.raises(ValueError): + cell = ZeroDModel(volume_cls=0.005, # L + volume_ncls=0.03, # L + c_ox_cls=0.01, # M + c_red_cls=0.01, # M + c_ox_ncls=0.01, # M + c_red_ncls=0.01, # M + ocv_50_soc=1.1, # V + resistance=0.8, # ohms + k_0_cls=1e-3, # cm/s + k_0_ncls=1e-3, # cm/s + ) + + # define cycling protocol + protocol = ConstantCurrentConstantVoltage( + voltage_limit_charge=1.45, + voltage_limit_discharge=0.8, + current_cutoff_charge=0.005, + current_cutoff_discharge=-0.005, + current_charge=-0.01, + current_discharge=0.1, + ) + + # putting it all together + all_results = protocol.run(cell_model=cell, + duration=1000, # cycle time to simulate (s) + ) + + with pytest.raises(ValueError): + cell = ZeroDModel(volume_cls=0.005, # L + volume_ncls=0.03, # L + c_ox_cls=0.01, # M + c_red_cls=0.01, # M + c_ox_ncls=0.01, # M + c_red_ncls=0.01, # M + ocv_50_soc=1.1, # V + resistance=0.8, # ohms + k_0_cls=1e-3, # cm/s + k_0_ncls=1e-3, # cm/s + ) + + # define cycling protocol + protocol = ConstantCurrentConstantVoltage( + voltage_limit_charge=1.45, + voltage_limit_discharge=0.8, + current_cutoff_charge=0.005, + current_cutoff_discharge=-0.005, + current_charge=0.01, + current_discharge=0.1, + ) + + # putting it all together + all_results = protocol.run(cell_model=cell, + duration=1000, # cycle time to simulate (s) + ) + + with pytest.raises(ValueError): + cell = ZeroDModel(volume_cls=0.005, # L + volume_ncls=0.03, # L + c_ox_cls=0.01, # M + c_red_cls=0.01, # M + c_ox_ncls=0.01, # M + c_red_ncls=0.01, # M + ocv_50_soc=1.1, # V + resistance=0.8, # ohms + k_0_cls=1e-3, # cm/s + k_0_ncls=1e-3, # cm/s + ) + + # define cycling protocol + protocol = ConstantCurrentConstantVoltage( + voltage_limit_charge=1.45, + voltage_limit_discharge=0.8, + current=0.1, + current_cutoff=-0.01, + ) + + # putting it all together + all_results = protocol.run(cell_model=cell, + duration=1000, # cycle time to simulate (s) + ) + + with pytest.raises(ValueError): + cell = ZeroDModel(volume_cls=0.005, # L + volume_ncls=0.03, # L + c_ox_cls=0.01, # M + c_red_cls=0.01, # M + c_ox_ncls=0.01, # M + c_red_ncls=0.01, # M + ocv_50_soc=1.1, # V + resistance=0.8, # ohms + k_0_cls=1e-3, # cm/s + k_0_ncls=1e-3, # cm/s + ) + + # define cycling protocol + protocol = ConstantCurrentConstantVoltage( + voltage_limit_charge=1.45, + voltage_limit_discharge=0.8, + current_cutoff_charge=0.005, + current_charge=0.01, + ) + + # putting it all together + all_results = protocol.run(cell_model=cell, + duration=1000, # cycle time to simulate (s) + ) + + def test_cccv_voltage_inputs(self): + with pytest.raises(ValueError): + cell = ZeroDModel(volume_cls=0.005, # L + volume_ncls=0.03, # L + c_ox_cls=0.01, # M + c_red_cls=0.01, # M + c_ox_ncls=0.01, # M + c_red_ncls=0.01, # M + ocv_50_soc=0.0, # V + resistance=0.8, # ohms + k_0_cls=1e-3, # cm/s + k_0_ncls=1e-3, # cm/s + ) + + # define cycling protocol + protocol = ConstantCurrentConstantVoltage( + voltage_limit_charge=0.4, + voltage_limit_discharge=0.2, + current_cutoff_charge=0.005, + current_cutoff_discharge=-0.005, + current=0.1, + ) + + # putting it all together + all_results = protocol.run(cell_model=cell, + duration=1000, # cycle time to simulate (s) + ) + + with pytest.raises(ValueError): + cell = ZeroDModel(volume_cls=0.005, # L + volume_ncls=0.03, # L + c_ox_cls=0.01, # M + c_red_cls=0.01, # M + c_ox_ncls=0.01, # M + c_red_ncls=0.01, # M + ocv_50_soc=1.0, # V + resistance=0.8, # ohms + k_0_cls=1e-3, # cm/s + k_0_ncls=1e-3, # cm/s + ) + + # define cycling protocol + protocol = ConstantCurrentConstantVoltage( + voltage_limit_charge=0.4, + voltage_limit_discharge=1.2, + current_cutoff_charge=0.005, + current_cutoff_discharge=-0.005, + current=0.1, + ) + + # putting it all together + all_results = protocol.run(cell_model=cell, + duration=1000, # cycle time to simulate (s) + ) + + with pytest.raises(ValueError): + cell = ZeroDModel(volume_cls=0.005, # L + volume_ncls=0.03, # L + c_ox_cls=0.01, # M + c_red_cls=0.01, # M + c_ox_ncls=0.01, # M + c_red_ncls=0.01, # M + ocv_50_soc=1.0, # V + resistance=0.8, # ohms + k_0_cls=1e-3, # cm/s + k_0_ncls=1e-3, # cm/s + ) + + # define cycling protocol + protocol = ConstantCurrentConstantVoltage( + voltage_limit_charge=1.4, + voltage_limit_discharge=-0.2, + current_cutoff_charge=0.005, + current_cutoff_discharge=-0.005, + current=0.1, + ) + + # putting it all together + all_results = protocol.run(cell_model=cell, + duration=1000, # cycle time to simulate (s) + ) + + with pytest.raises(ValueError): + cell = ZeroDModel(volume_cls=0.005, # L + volume_ncls=0.03, # L + c_ox_cls=0.01, # M + c_red_cls=0.01, # M + c_ox_ncls=0.01, # M + c_red_ncls=0.01, # M + ocv_50_soc=1.0, # V + resistance=0.8, # ohms + k_0_cls=1e-3, # cm/s + k_0_ncls=1e-3, # cm/s + ) + + # define cycling protocol + protocol = ConstantCurrentConstantVoltage( + voltage_limit_charge=1.4, + voltage_limit_discharge=-0.2, + current_cutoff_charge=0.005, + current_cutoff_discharge=-0.005, + current=0.1, + ) + + # putting it all together + all_results = protocol.run(cell_model=cell, + duration=1000, # cycle time to simulate (s) + degradation=AutoReduction(rate_constant=1e-4), + ncls_degradation=ChemicalDegradationReduced(rate_order=1, rate_constant=3e-3), + ) + class TestAsymmetricCurrents: def test_cc(self): @@ -256,3 +534,47 @@ def test_cc(self, capsys): cyclestatus = captured.out.strip().rsplit('time steps: ', 1)[1] assert cyclestatus == warn_out + + +class TestDegradationCommutativity: + degradations = [ + ChemicalDegradationOxidized(rate_order=1, rate_constant=0.01), + ChemicalDegradationReduced(rate_order=1, rate_constant=0.01), + AutoOxidation(rate_constant=0.01), + AutoReduction(rate_constant=0.01), + Dimerization(forward_rate_constant=2, backward_rate_constant=1, c_dimer=0.5) + ] + + @pytest.mark.parametrize("degradation1,degradation2", itertools.combinations(degradations, 2)) + def test_degradations_are_commutative(self, degradation1, degradation2): + mechanism1 = MultiDegradationMechanism([degradation1, degradation2]) + mechanism2 = MultiDegradationMechanism([degradation2, degradation1]) + + cell1, cell2 = [ + ZeroDModel(volume_cls=0.005, # L + volume_ncls=0.05, # L + c_ox_cls=0.01, # M + c_red_cls=0.01, # M + c_ox_ncls=0.01, # M + c_red_ncls=0.01, # M + ocv_50_soc=0.0, # V + resistance=1.0, # ohms + k_0_cls=1e-3, # cm/s + k_0_ncls=1e-3, # cm/s + num_electrons_cls=1, # electrons + num_electrons_ncls=1, # electrons + ) + for _ in range(2) + ] + + protocol = ConstantCurrent(voltage_limit_charge=0.2, # V + voltage_limit_discharge=-0.2, # V + current=0.05, # A + ) + + results1 = protocol.run(cell_model=cell1, duration=1000, degradation=mechanism1) + results2 = protocol.run(cell_model=cell2, duration=1000, degradation=mechanism2) + assert results1.c_ox_cls == results2.c_ox_cls + assert results1.c_red_cls == results2.c_red_cls + assert results1.c_ox_ncls == results2.c_ox_ncls + assert results1.c_red_ncls == results2.c_red_ncls diff --git a/tests/test_redox_flow_cell.py b/tests/test_redox_flow_cell.py index ba46d0c..300004f 100644 --- a/tests/test_redox_flow_cell.py +++ b/tests/test_redox_flow_cell.py @@ -24,7 +24,9 @@ class TestClassRedoxFlowCell: (0.001, 0.1, 0.1, 0.001, 0.01, 0.01, 1, 0.1, 1e-3, 1e-3, 0.5, 0.5, 2.5, 3), (0.001, 0.1, 0.1, 0.001, 0.01, 0.01, 1, 0.1, 1e-3, 1e-3, 0.5, 0.5, 1.5, 1.5), (0.2, 0.01, 15, 12, 0.01, 0.01, 1.5, 0.1, 1e-3, 1e-3, 0.5, 0.5, 5, 1), - (0.2, 0.01, 0.01, 0.01, 0.01, 0.01, 0.0, 0.1, 1e-3, 1e-3, 0.5, 0.5, 1, 1) + (0.2, 0.01, 0.01, 0.01, 0.01, 0.01, 0.0, 0.1, 1e-3, 1e-3, 0.5, 0.5, 1, 1), + (0.002, 0.01, 0.01, 0.01, 0.01, 0.01, 1.0, 0.1, 1e-3, 1e-3, 0.5, 0.5, -2, 1), + (0.002, 0.01, 0.01, 0.01, 0.01, 0.01, 1.0, 0.1, 1e-3, 1e-3, 0.5, 0.5, 2, -11), ]) def test_class_init(self, v_cls, v_ncls, ox_cls, red_cls, ox_ncls, red_ncls, ocv, res, k_c, k_n, a_c, a_n, n_c, n_n): with pytest.raises(ValueError):