Source code for src.cvsim.fit_curve

"""
Module for cyclic voltammogram fitting, using a semi-integration
simulated CV for one- and two-electron processes.
"""

from abc import ABC, abstractmethod
from typing import TypeAlias, Callable
import numpy as np
from scipy.optimize import curve_fit
from .mechanisms import CyclicVoltammetryScheme, E_rev, E_q, E_qC, EE, SquareScheme


_ParamGuess: TypeAlias = None | float | tuple[float, float] | tuple[float, float, float]

# Max allowable position for initial voltage oscillations in CV scan. Oftentimes the
# first few voltage points returned from potentiostat can oscillate without a defined scan direction.
VOLTAGE_OSCILLATION_LIMIT = 10


[docs] class FitMechanism(ABC): """ Scheme for fitting cyclic voltammograms. Parameters ---------- voltage_to_fit : list[float] | np.ndarray Array of voltage data of the CV to fit. current_to_fit : list[float] | np.ndarray Array of current data of the CV to fit. scan_rate : float Potential sweep rate (V/s). c_bulk : float Bulk concentration of redox species (mM or mol/m^3). step_size : float Voltage increment during CV scan (mV). disk_radius : float Radius of disk macro-electrode (mm). temperature : float Temperature (K). Default is 298.0 K (24.85C). reduction_potential : float | None Reduction potential of the one-electron transfer process (V vs. reference). If known, can be fixed value, otherwise defaults to None. diffusion_reactant : float | None Diffusion coefficient of reactant (cm^2/s). If known, can be fixed value, otherwise defaults to None. diffusion_product : float | None Diffusion coefficient of product (cm^2/s). If known, can be fixed value, otherwise defaults to None. """ def __init__( self, voltage_to_fit: list[float] | np.ndarray, current_to_fit: list[float] | np.ndarray, scan_rate: float, c_bulk: float, step_size: float, disk_radius: float, temperature: float = 298.0, reduction_potential: float | None = None, diffusion_reactant: float | None = None, diffusion_product: float | None = None, ) -> None: if len(voltage_to_fit) != len(current_to_fit): raise ValueError("'voltage_to_fit' and 'current_to_fit' must be equal length") self._ensure_positive('step_size', step_size) self._ensure_positive('scan_rate', scan_rate) self._ensure_positive('disk_radius', disk_radius) self._ensure_positive('c_bulk', c_bulk) self._ensure_positive('temperature', temperature) self._ensure_positive_or_none('diffusion_reactant', diffusion_reactant) self._ensure_positive_or_none('diffusion_product', diffusion_product) self.current_to_fit = current_to_fit self.scan_rate = scan_rate self.c_bulk = c_bulk self.step_size = step_size self.disk_radius = disk_radius self.temperature = temperature self.reduction_potential = reduction_potential self.diffusion_reactant = diffusion_reactant self.diffusion_product = diffusion_product # rounding the start/reverse potentials from the input experimental voltage data to # 2 decimal places helps reduce noise and--based on authors' experience--it is pretty rare # to see start/reverse potentials initialized in the lab being declared to the third decimal place. self.start_potential = round(voltage_to_fit[0], 2) start_potential_mv = round(self.start_potential * 1000) if voltage_to_fit[VOLTAGE_OSCILLATION_LIMIT] > self.start_potential: # scan starts towards more positive self.switch_potential = round(max(voltage_to_fit), 2) else: # scan starts towards more negative self.switch_potential = round(min(voltage_to_fit), 2) switch_potential_mv = round(self.switch_potential * 1000) # make a cleaner x array scan_direction = -1 if self.start_potential < self.switch_potential else 1 delta_theta = scan_direction * self.step_size thetas = [round((i - delta_theta)) for i in [start_potential_mv, switch_potential_mv]] forward_scan = np.arange(thetas[0], thetas[1], step=delta_theta * -1) reverse_scan = np.append(forward_scan[-2::-1], start_potential_mv) self.voltage = np.concatenate([forward_scan, reverse_scan]) / 1000 # Contains only variables with a user-specified fixed value. # These params are shared by all CVsim mechanisms self.fixed_vars = { 'reduction_potential': reduction_potential, 'diffusion_reactant': diffusion_reactant, 'diffusion_product': diffusion_product, } # Values are [initial guess, lower bound, upper bound] # These params are shared by all CVsim mechanisms self.default_vars = { 'reduction_potential': [ round((self.voltage[np.argmax(self.current_to_fit)] + self.voltage[np.argmin(self.current_to_fit)]) / 2, 3), min(self.start_potential, self.switch_potential), max(self.start_potential, self.switch_potential), ], 'diffusion_reactant': [1e-6, 5e-8, 1e-4], 'diffusion_product': [1e-6, 5e-8, 1e-4], } @staticmethod def _ensure_positive(param: str, value: float): if value <= 0.0: raise ValueError(f"'{param}' must be > 0.0") @staticmethod def _ensure_positive_or_none(param: str, value: float | None): if value is not None and value <= 0.0: raise ValueError(f"'{param}' must be > 0.0 or None") @staticmethod def _ensure_open_unit_interval_or_none(param: str, value: float | None): if value is not None and not 0.0 < value < 1.0: raise ValueError(f"'{param}' must be between 0.0 and 1.0, or None") @staticmethod def _non_none_dict(mapping: dict): return {k: v for k, v in mapping.items() if v is not None} @staticmethod def _fit_var_checker(fit_vars: dict, fit_default_vars: dict) -> dict: # take fit_vars dict, for each in it, replace the initial guess/bounds if specified for param, value in fit_vars.items(): if isinstance(value, float | int): # Initial guess fit_default_vars[param][0] = value elif isinstance(value, tuple) and len(value) == 2: # Lower and upper bound if value[0] >= value[1]: raise ValueError(f"'{param}' lower bound must be lower than upper bound") fit_default_vars[param][1] = value[0] fit_default_vars[param][2] = value[1] elif isinstance(value, tuple) and len(value) == 3: if not value[1] < value[0] < value[2]: raise ValueError(f"'{param}' lower bound must be lower than upper bound and guess between them") fit_default_vars[param] = list(value) elif not None: raise ValueError(f"'{param}' allowed inputs: " f"None | float | tuple[float, float] | tuple[float, float, float]") return fit_default_vars @abstractmethod def _scheme(self, get_var: Callable[[str], float]) -> CyclicVoltammetryScheme: raise NotImplementedError def _fit(self, fit_vars: dict[str, _ParamGuess]) -> tuple[np.ndarray, np.ndarray, dict[str, float]]: fit_vars = self._non_none_dict(fit_vars) fixed_vars = self._non_none_dict(self.fixed_vars) # check intersection of fixed_vars / fit_vars dicts. if so raise error intersection_errors = fixed_vars.keys() & fit_vars.keys() if intersection_errors: raise ValueError(f"Cannot input fixed value and guess value for {*intersection_errors,}") # get params that will be fit fitting_params = [ param for param in self.default_vars if param not in fixed_vars.keys() ] # create deep copy of default dict, and trim to set of fit variables fit_default_vars = {k: list(v) for k, v in self.default_vars.items() if k in fitting_params} var_index = {var: index for index, var in enumerate(fit_default_vars.keys())} fit_default_vars = self._fit_var_checker(fit_vars, fit_default_vars) for param, (initial, lower, upper) in fit_default_vars.items(): if not lower < initial < upper: # check if default initial guess is outside bounds, set guess to avg of bounds # not useful if spans many order of magnitudes, could use logarithmic mean fit_default_vars[param] = [(lower + upper) / 2, lower, upper] # check if user's guess was outside bounds if initial != self.default_vars[param][0]: raise ValueError(f"Initial guess for '{param}' is outside user-defined bounds") print(f"final fitting vars: {fit_default_vars}") initial_guesses, lower_bounds, upper_bounds = zip(*fit_default_vars.values()) print(f'Initial guesses: {initial_guesses}') print(f'Lower/Upper bounds: {lower_bounds}/{upper_bounds}') print(f'Fixed params: {list(fixed_vars)}') print(f'Fitting for: {list(fitting_params)}') def get_var(args: tuple[float, ...], param: str) -> float: # Helper function to retrieve value for fixed variable if it exists, or retrieve the # guess for the parameter that is passed in via curve_fit. if param in fixed_vars: return fixed_vars[param] return args[var_index[param]] def fit_function( x: list[float] | np.ndarray, # pylint: disable=unused-argument *args: float, ) -> np.ndarray: # Inner function used by scipy's curve_fit to fit a CV according to the mechanism. # Note that Scipy's `curve_fit` does not allow for the user to pass in a function with various dynamic # parameters so `fit_function` and `get_var` are used to pass CV simulations to `curve_fit` with optional # inputs of initial guesses/bounds from `fit`. print(f"trying values: {args}") _, i_fit = self._scheme(lambda param: get_var(args, param)).simulate() return i_fit # fit raw data but exclude first data point, as semi-analytical method skips time=0 fit_results = curve_fit( f=fit_function, xdata=self.voltage, ydata=self.current_to_fit[1:], p0=initial_guesses, bounds=[lower_bounds, upper_bounds], ) popt, pcov = list(fit_results) current_fit = fit_function(self.voltage, *popt) sigma = np.sqrt(np.diag(pcov)) # one standard deviation of the parameters final_fit: dict[str, float] = {} for val, error, param in zip(popt, sigma, fitting_params): final_fit[param] = val print(f"Final fit: '{param}': {val:.2E} +/- {error:.0E}") # Semi-analytical method does not compute the first point (i.e. time=0) # so the starting voltage data point with a zero current is reinserted self.voltage = np.insert(self.voltage, 0, self.start_potential) current_fit = np.insert(current_fit, 0, 0) return self.voltage, current_fit, final_fit
[docs] class FitE_rev(FitMechanism): """ Scheme for fitting a CV for a reversible (Nernstian) one-electron transfer mechanism. Parameters ---------- voltage_to_fit : list[float] | np.ndarray Array of voltage data of the CV to fit. current_to_fit : list[float] | np.ndarray Array of current data of the CV to fit. scan_rate : float Potential sweep rate (V/s). c_bulk : float Bulk concentration of redox species (mM or mol/m^3). step_size : float Voltage increment during CV scan (mV). disk_radius : float Radius of disk macro-electrode (mm). temperature : float Temperature (K). Default is 298.0 K (24.85C). reduction_potential : float | None Reduction potential of the one-electron transfer process (V vs. reference). If known, can be fixed value, otherwise defaults to None. diffusion_reactant : float | None Diffusion coefficient of reactant (cm^2/s). If known, can be fixed value, otherwise defaults to None. diffusion_product : float | None Diffusion coefficient of product (cm^2/s). If known, can be fixed value, otherwise defaults to None. """ def __init__( self, voltage_to_fit: list[float] | np.ndarray, current_to_fit: list[float] | np.ndarray, scan_rate: float, c_bulk: float, step_size: float, disk_radius: float, temperature: float = 298.0, reduction_potential: float | None = None, diffusion_reactant: float | None = None, diffusion_product: float | None = None, ) -> None: super().__init__( voltage_to_fit, current_to_fit, scan_rate, c_bulk, step_size, disk_radius, temperature, reduction_potential, diffusion_reactant, diffusion_product, ) def _scheme(self, get_var: Callable[[str], float]) -> CyclicVoltammetryScheme: return E_rev( start_potential=self.start_potential, switch_potential=self.switch_potential, reduction_potential=get_var('reduction_potential'), scan_rate=self.scan_rate, c_bulk=self.c_bulk, diffusion_reactant=get_var('diffusion_reactant'), diffusion_product=get_var('diffusion_product'), step_size=self.step_size, disk_radius=self.disk_radius, temperature=self.temperature, )
[docs] def fit( self, reduction_potential: _ParamGuess = None, diffusion_reactant: _ParamGuess = None, diffusion_product: _ParamGuess = None, ) -> tuple[np.ndarray, np.ndarray, dict[str, float]]: """ Fits the CV for a reversible (Nernstian) one-electron transfer mechanism. If a parameter is given, it must be a: float for initial guess of parameter; tuple[float, float] for (lower bound, upper bound) of the initial guess; or tuple[float, float, float] for (initial guess, lower bound, upper bound). Parameters ---------- reduction_potential : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the reduction potential of the one-electron transfer process (V vs. reference). Defaults to None. diffusion_reactant : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the diffusion coefficient of reactant (cm^2/s). Defaults to None. diffusion_product : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the diffusion coefficient of product (cm^2/s). Defaults to None. Returns ------- voltage : np.ndarray Array of potential (V) values of the CV fit. current_fit : np.ndarray Array of current (A) values of the CV fit. final_fit : dict[str, float] Dictionary of final fitting parameter values of the CV fit. """ return self._fit({ 'reduction_potential': reduction_potential, 'diffusion_reactant': diffusion_reactant, 'diffusion_product': diffusion_product, })
[docs] class FitE_q(FitMechanism): """ Scheme for fitting a CV for a quasi-reversible one-electron transfer mechanism. Parameters ---------- voltage_to_fit : list[float] | np.ndarray Array of voltage data of the CV to fit. current_to_fit : list[float] | np.ndarray Array of current data of the CV to fit. scan_rate : float Potential sweep rate (V/s). c_bulk : float Bulk concentration of redox species (mM or mol/m^3). step_size : float Voltage increment during CV scan (mV). disk_radius : float Radius of disk macro-electrode (mm). temperature : float Temperature (K). Default is 298.0 K (24.85C). reduction_potential : float | None Reduction potential of the one-electron transfer process (V vs. reference). If known, can be fixed value, otherwise defaults to None. diffusion_reactant : float | None Diffusion coefficient of reactant (cm^2/s). If known, can be fixed value, otherwise defaults to None. diffusion_product : float | None Diffusion coefficient of product (cm^2/s). If known, can be fixed value, otherwise defaults to None. alpha : float | None Charge transfer coefficient (no units). If known, can be fixed value, otherwise defaults to None. k0 : float | None Standard electrochemical rate constant (cm/s). If known, can be fixed value, otherwise defaults to None. """ def __init__( self, voltage_to_fit: list[float] | np.ndarray, current_to_fit: list[float] | np.ndarray, scan_rate: float, c_bulk: float, step_size: float, disk_radius: float, temperature: float = 298.0, reduction_potential: float | None = None, diffusion_reactant: float | None = None, diffusion_product: float | None = None, alpha: float | None = None, k0: float | None = None, ) -> None: super().__init__( voltage_to_fit, current_to_fit, scan_rate, c_bulk, step_size, disk_radius, temperature, reduction_potential, diffusion_reactant, diffusion_product, ) self._ensure_open_unit_interval_or_none('alpha', alpha) self._ensure_positive_or_none('k0', k0) self.alpha = alpha self.k0 = k0 self.fixed_vars |= { 'alpha': alpha, 'k0': k0, } # default [initial guess, lower bound, upper bound] self.default_vars |= { 'alpha': [0.5, 0.01, 0.99], 'k0': [1e-5, 1e-8, 1e-3], } def _scheme(self, get_var: Callable[[str], float]) -> CyclicVoltammetryScheme: return E_q( start_potential=self.start_potential, switch_potential=self.switch_potential, reduction_potential=get_var('reduction_potential'), scan_rate=self.scan_rate, c_bulk=self.c_bulk, diffusion_reactant=get_var('diffusion_reactant'), diffusion_product=get_var('diffusion_product'), alpha=get_var('alpha'), k0=get_var('k0'), step_size=self.step_size, disk_radius=self.disk_radius, temperature=self.temperature, )
[docs] def fit( self, reduction_potential: _ParamGuess = None, diffusion_reactant: _ParamGuess = None, diffusion_product: _ParamGuess = None, alpha: _ParamGuess = None, k0: _ParamGuess = None, ) -> tuple[np.ndarray, np.ndarray, dict[str, float]]: """ Fits the CV for a quasi-reversible one-electron transfer mechanism. If a parameter is given, it must be a: float for initial guess of parameter; tuple[float, float] for (lower bound, upper bound) of the initial guess; or tuple[float, float, float] for (initial guess, lower bound, upper bound). Parameters ---------- reduction_potential : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the reduction potential of the one-electron transfer process (V vs. reference). Defaults to None. diffusion_reactant : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the diffusion coefficient of reactant (cm^2/s). Defaults to None. diffusion_product : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the diffusion coefficient of product (cm^2/s). Defaults to None. alpha : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the charge transfer coefficient (no units). Defaults to None. k0 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the standard electrochemical rate constant (cm/s). Defaults to None. Returns ------- voltage : np.ndarray Array of potential (V) values of the CV fit. current_fit : np.ndarray Array of current (A) values of the CV fit. final_fit : dict[str, float] Dictionary of final fitting parameter values of the CV fit. """ return self._fit({ 'reduction_potential': reduction_potential, 'diffusion_reactant': diffusion_reactant, 'diffusion_product': diffusion_product, 'alpha': alpha, 'k0': k0, })
[docs] class FitE_qC(FitMechanism): """ Scheme for fitting a CV for a quasi-reversible one-electron transfer, followed by a reversible first order homogeneous chemical transformation mechanism. Parameters ---------- voltage_to_fit : list[float] | np.ndarray Array of voltage data of the CV to fit. current_to_fit : list[float] | np.ndarray Array of current data of the CV to fit. scan_rate : float Potential sweep rate (V/s). c_bulk : float Bulk concentration of redox species (mM or mol/m^3). step_size : float Voltage increment during CV scan (mV). disk_radius : float Radius of disk macro-electrode (mm). temperature : float Temperature (K). Default is 298.0 K (24.85C). reduction_potential : float | None Reduction potential of the one-electron transfer process (V vs. reference). If known, can be fixed value, otherwise defaults to None. diffusion_reactant : float | None Diffusion coefficient of reactant (cm^2/s). If known, can be fixed value, otherwise defaults to None. diffusion_product : float | None Diffusion coefficient of product (cm^2/s). If known, can be fixed value, otherwise defaults to None. alpha : float | None Charge transfer coefficient (no units). If known, can be fixed value, otherwise defaults to None. k0 : float | None Standard electrochemical rate constant (cm/s). If known, can be fixed value, otherwise defaults to None. k_forward : float | None First order forward chemical rate constant (1/s). If known, can be fixed value, otherwise defaults to None. k_backward : float | None First order backward chemical rate constant (1/s). If known, can be fixed value, otherwise defaults to None. """ def __init__( self, voltage_to_fit: list[float] | np.ndarray, current_to_fit: list[float] | np.ndarray, scan_rate: float, c_bulk: float, step_size: float, disk_radius: float, temperature: float = 298.0, reduction_potential: float | None = None, diffusion_reactant: float | None = None, diffusion_product: float | None = None, alpha: float | None = None, k0: float | None = None, k_forward: float | None = None, k_backward: float | None = None, ) -> None: super().__init__( voltage_to_fit, current_to_fit, scan_rate, c_bulk, step_size, disk_radius, temperature, reduction_potential, diffusion_reactant, diffusion_product, ) self._ensure_open_unit_interval_or_none('alpha', alpha) self._ensure_positive_or_none('k0', k0) self._ensure_positive_or_none('k_forward', k_forward) self._ensure_positive_or_none('k_backward', k_backward) self.alpha = alpha self.k0 = k0 self.k_forward = k_forward self.k_backward = k_backward self.fixed_vars |= { 'alpha': alpha, 'k0': k0, 'k_forward': k_forward, 'k_backward': k_backward, } # default [initial guess, lower bound, upper bound] self.default_vars |= { 'alpha': [0.5, 0.01, 0.99], 'k0': [1e-5, 1e-8, 1e-3], 'k_forward': [1e-3, 1e-8, 1e3], 'k_backward': [1e-3, 1e-8, 1e3], } def _scheme(self, get_var: Callable[[str], float]) -> CyclicVoltammetryScheme: return E_qC( start_potential=self.start_potential, switch_potential=self.switch_potential, reduction_potential=get_var('reduction_potential'), scan_rate=self.scan_rate, c_bulk=self.c_bulk, diffusion_reactant=get_var('diffusion_reactant'), diffusion_product=get_var('diffusion_product'), alpha=get_var('alpha'), k0=get_var('k0'), k_forward=get_var('k_forward'), k_backward=get_var('k_backward'), step_size=self.step_size, disk_radius=self.disk_radius, temperature=self.temperature, )
[docs] def fit( self, reduction_potential: _ParamGuess = None, diffusion_reactant: _ParamGuess = None, diffusion_product: _ParamGuess = None, alpha: _ParamGuess = None, k0: _ParamGuess = None, k_forward: _ParamGuess = None, k_backward: _ParamGuess = None, ) -> tuple[np.ndarray, np.ndarray, dict[str, float]]: """ Fits the CV for a quasi-reversible one-electron transfer, followed by a reversible first order homogeneous chemical transformation mechanism. If a parameter is given, it must be a: float for initial guess of parameter; tuple[float, float] for (lower bound, upper bound) of the initial guess; or tuple[float, float, float] for (initial guess, lower bound, upper bound). Parameters ---------- reduction_potential : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the reduction potential of the one-electron transfer process (V vs. reference). Defaults to None. diffusion_reactant : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the diffusion coefficient of reactant (cm^2/s). Defaults to None. diffusion_product : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the diffusion coefficient of product (cm^2/s). Defaults to None. alpha : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the charge transfer coefficient (no units). Defaults to None. k0 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the standard electrochemical rate constant (cm/s). Defaults to None. k_forward : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the first order forward chemical rate constant (1/s). Defaults to None. k_backward : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the first order backward chemical rate constant (1/s). Defaults to None. Returns ------- voltage : np.ndarray Array of potential (V) values of the CV fit. current_fit : np.ndarray Array of current (A) values of the CV fit. final_fit : dict[str, float] Dictionary of final fitting parameter values of the CV fit. """ return self._fit({ 'reduction_potential': reduction_potential, 'diffusion_reactant': diffusion_reactant, 'diffusion_product': diffusion_product, 'alpha': alpha, 'k0': k0, 'k_forward': k_forward, 'k_backward': k_backward, })
[docs] class FitEE(FitMechanism): """ Scheme for fitting a CV for a two successive one-electron quasi-reversible transfer mechanism. Parameters ---------- voltage_to_fit : list[float] | np.ndarray Array of voltage data of the CV to fit. current_to_fit : list[float] | np.ndarray Array of current data of the CV to fit. scan_rate : float Potential sweep rate (V/s). c_bulk : float Bulk concentration of redox species (mM or mol/m^3). step_size : float Voltage increment during CV scan (mV). disk_radius : float Radius of disk macro-electrode (mm). temperature : float Temperature (K). Default is 298.0 K (24.85C). reduction_potential : float | None Reduction potential of the first one-electron transfer process (V vs. reference). If known, can be fixed value, otherwise defaults to None. reduction_potential2 : float | None Reduction potential of the second one-electron transfer process (V vs. reference). If known, can be fixed value, otherwise defaults to None. diffusion_reactant : float | None Diffusion coefficient of reactant (cm^2/s). If known, can be fixed value, otherwise defaults to None. diffusion_intermediate : float | None Diffusion coefficient of intermediate (cm^2/s). If known, can be fixed value, otherwise defaults to None. diffusion_product : float | None Diffusion coefficient of product (cm^2/s). If known, can be fixed value, otherwise defaults to None. alpha : float | None Charge transfer coefficient of first redox process (no units). If known, can be fixed value, otherwise defaults to None. alpha2 : float | None Charge transfer coefficient of second redox process (no units). If known, can be fixed value, otherwise defaults to None. k0 : float | None Standard electrochemical rate constant of first redox process (cm/s). If known, can be fixed value, otherwise defaults to None. k0_2 : float | None Standard electrochemical rate constant of second redox process (cm/s). If known, can be fixed value, otherwise defaults to None. """ def __init__( self, voltage_to_fit: list[float] | np.ndarray, current_to_fit: list[float] | np.ndarray, scan_rate: float, c_bulk: float, step_size: float, disk_radius: float, temperature: float = 298.0, reduction_potential: float | None = None, reduction_potential2: float | None = None, diffusion_reactant: float | None = None, diffusion_intermediate: float | None = None, diffusion_product: float | None = None, alpha: float | None = None, alpha2: float | None = None, k0: float | None = None, k0_2: float | None = None, ) -> None: super().__init__( voltage_to_fit, current_to_fit, scan_rate, c_bulk, step_size, disk_radius, temperature, reduction_potential, diffusion_reactant, diffusion_product, ) self._ensure_positive_or_none('diffusion_intermediate', diffusion_intermediate) self._ensure_open_unit_interval_or_none('alpha', alpha) self._ensure_open_unit_interval_or_none('alpha2', alpha2) self._ensure_positive_or_none('k0', k0) self._ensure_positive_or_none('k0_2', k0_2) self.reduction_potential2 = reduction_potential2 self.diffusion_intermediate = diffusion_intermediate self.alpha = alpha self.alpha2 = alpha2 self.k0 = k0 self.k0_2 = k0_2 self.fixed_vars |= { 'reduction_potential2': reduction_potential2, 'diffusion_intermediate': diffusion_intermediate, 'alpha': alpha, 'alpha2': alpha2, 'k0': k0, 'k0_2': k0_2, } # default [initial guess, lower bound, upper bound] self.default_vars |= { 'reduction_potential2': [ round((self.voltage[np.argmax(self.current_to_fit)] + self.voltage[np.argmin(self.current_to_fit)]) / 2, 3), min(self.start_potential, self.switch_potential), max(self.start_potential, self.switch_potential), ], 'diffusion_intermediate': [1e-6, 5e-8, 1e-4], 'alpha': [0.5, 0.01, 0.99], 'alpha2': [0.5, 0.01, 0.99], 'k0': [1e-5, 1e-8, 1e-3], 'k0_2': [1e-5, 1e-8, 1e-3], } def _scheme(self, get_var: Callable[[str], float]) -> CyclicVoltammetryScheme: return EE( start_potential=self.start_potential, switch_potential=self.switch_potential, reduction_potential=get_var('reduction_potential'), reduction_potential2=get_var('reduction_potential2'), scan_rate=self.scan_rate, c_bulk=self.c_bulk, diffusion_reactant=get_var('diffusion_reactant'), diffusion_intermediate=get_var('diffusion_intermediate'), diffusion_product=get_var('diffusion_product'), alpha=get_var('alpha'), alpha2=get_var('alpha2'), k0=get_var('k0'), k0_2=get_var('k0_2'), step_size=self.step_size, disk_radius=self.disk_radius, temperature=self.temperature, )
[docs] def fit( self, reduction_potential: _ParamGuess = None, reduction_potential2: _ParamGuess = None, diffusion_reactant: _ParamGuess = None, diffusion_intermediate: _ParamGuess = None, diffusion_product: _ParamGuess = None, alpha: _ParamGuess = None, alpha2: _ParamGuess = None, k0: _ParamGuess = None, k0_2: _ParamGuess = None, ) -> tuple[np.ndarray, np.ndarray, dict[str, float]]: """ Fits the CV for a two successive one-electron quasi-reversible transfer mechanism. If a parameter is given, it must be a: float for initial guess of parameter; tuple[float, float] for (lower bound, upper bound) of the initial guess; or tuple[float, float, float] for (initial guess, lower bound, upper bound). Parameters ---------- reduction_potential : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the reduction potential of the first one-electron transfer process (V vs. reference). Defaults to None. reduction_potential2 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the reduction potential of the second one-electron transfer process (V vs. reference). Defaults to None. diffusion_reactant : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the diffusion coefficient of reactant (cm^2/s). Defaults to None. diffusion_intermediate : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the diffusion coefficient of intermediate (cm^2/s). Defaults to None. diffusion_product : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the diffusion coefficient of product (cm^2/s). Defaults to None. alpha : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the charge transfer coefficient of the first redox process (no units). Defaults to None. alpha2 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the charge transfer coefficient of the second redox process (no units). Defaults to None. k0 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the standard electrochemical rate constant of the first redox process (cm/s). Defaults to None. k0_2 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the standard electrochemical rate constant of the second redox process (cm/s). Defaults to None. Returns ------- voltage : np.ndarray Array of potential (V) values of the CV fit. current_fit : np.ndarray Array of current (A) values of the CV fit. final_fit : dict[str, float] Dictionary of final fitting parameter values of the CV fit. """ return self._fit({ 'reduction_potential': reduction_potential, 'reduction_potential2': reduction_potential2, 'diffusion_reactant': diffusion_reactant, 'diffusion_intermediate': diffusion_intermediate, 'diffusion_product': diffusion_product, 'alpha': alpha, 'alpha2': alpha2, 'k0': k0, 'k0_2': k0_2, })
[docs] class FitSquareScheme(FitMechanism): """ Scheme for fitting a CV for two quasi-reversible, one-electron transfers of homogeneously interconverting reactants (Square Scheme) mechanism. Parameters ---------- voltage_to_fit : list[float] | np.ndarray Array of voltage data of the CV to fit. current_to_fit : list[float] | np.ndarray Array of current data of the CV to fit. scan_rate : float Potential sweep rate (V/s). c_bulk : float Bulk concentration of redox species (mM or mol/m^3). step_size : float Voltage increment during CV scan (mV). disk_radius : float Radius of disk macro-electrode (mm). temperature : float Temperature (K). Default is 298.0 K (24.85C). reduction_potential : float | None Reduction potential of the first one-electron transfer process (V vs. reference). If known, can be fixed value, otherwise defaults to None. reduction_potential2 : float | None Reduction potential of the second one-electron transfer process (V vs. reference). If known, can be fixed value, otherwise defaults to None. diffusion_reactant : float | None Diffusion coefficient of reactant (cm^2/s). If known, can be fixed value, otherwise defaults to None. diffusion_product : float | None Diffusion coefficient of product (cm^2/s). If known, can be fixed value, otherwise defaults to None. alpha : float | None Charge transfer coefficient of first redox process (no units). If known, can be fixed value, otherwise defaults to None. alpha2 : float | None Charge transfer coefficient of second redox process (no units). If known, can be fixed value, otherwise defaults to None. k0 : float | None Standard electrochemical rate constant of first redox process (cm/s). If known, can be fixed value, otherwise defaults to None. k0_2 : float | None Standard electrochemical rate constant of second redox process (cm/s). If known, can be fixed value, otherwise defaults to None. k_forward : float | None First order forward chemical rate constant for first redox species (1/s). If known, can be fixed value, otherwise defaults to None. k_backward : float | None First order backward chemical rate constant for first redox species (1/s). If known, can be fixed value, otherwise defaults to None. k_forward2 : float | None First order forward chemical rate constant for second redox species (1/s). If known, can be fixed value, otherwise defaults to None. k_backward2 : float | None First order backward chemical rate constant for second redox species (1/s). If known, can be fixed value, otherwise defaults to None. """ def __init__( self, voltage_to_fit: list[float] | np.ndarray, current_to_fit: list[float] | np.ndarray, scan_rate: float, c_bulk: float, step_size: float, disk_radius: float, temperature: float = 298.0, reduction_potential: float | None = None, reduction_potential2: float | None = None, diffusion_reactant: float | None = None, diffusion_product: float | None = None, alpha: float | None = None, alpha2: float | None = None, k0: float | None = None, k0_2: float | None = None, k_forward: float | None = None, k_backward: float | None = None, k_forward2: float | None = None, k_backward2: float | None = None, ) -> None: super().__init__( voltage_to_fit, current_to_fit, scan_rate, c_bulk, step_size, disk_radius, temperature, reduction_potential, diffusion_reactant, diffusion_product, ) self._ensure_open_unit_interval_or_none('alpha', alpha) self._ensure_open_unit_interval_or_none('alpha2', alpha2) self._ensure_positive_or_none('k0', k0) self._ensure_positive_or_none('k0_2', k0_2) self._ensure_positive_or_none('k_forward', k_forward) self._ensure_positive_or_none('k_backward', k_backward) self._ensure_positive_or_none('k_forward2', k_forward2) self._ensure_positive_or_none('k_backward2', k_backward2) self.reduction_potential2 = reduction_potential2 self.alpha = alpha self.alpha2 = alpha2 self.k0 = k0 self.k0_2 = k0_2 self.k_forward = k_forward self.k_backward = k_backward self.k_forward2 = k_forward2 self.k_backward2 = k_backward2 self.fixed_vars |= { 'reduction_potential2': reduction_potential2, 'alpha': alpha, 'alpha2': alpha2, 'k0': k0, 'k0_2': k0_2, 'k_forward': k_forward, 'k_backward': k_backward, 'k_forward2': k_forward2, 'k_backward2': k_backward2, } # default [initial guess, lower bound, upper bound] self.default_vars |= { 'reduction_potential2': [ round((self.voltage[np.argmax(self.current_to_fit)] + self.voltage[np.argmin(self.current_to_fit)]) / 2, 3), min(self.start_potential, self.switch_potential), max(self.start_potential, self.switch_potential), ], 'alpha': [0.5, 0.01, 0.99], 'alpha2': [0.5, 0.01, 0.99], 'k0': [1e-5, 1e-8, 1e-3], 'k0_2': [1e-5, 1e-8, 1e-3], 'k_forward': [1e-1, 5e-4, 1e3], 'k_backward': [1e-1, 5e-4, 1e3], 'k_forward2': [1e-1, 5e-4, 1e3], 'k_backward2': [1e-1, 5e-4, 1e3], } def _scheme(self, get_var: Callable[[str], float]) -> CyclicVoltammetryScheme: return SquareScheme( start_potential=self.start_potential, switch_potential=self.switch_potential, reduction_potential=get_var('reduction_potential'), reduction_potential2=get_var('reduction_potential2'), scan_rate=self.scan_rate, c_bulk=self.c_bulk, diffusion_reactant=get_var('diffusion_reactant'), diffusion_product=get_var('diffusion_product'), alpha=get_var('alpha'), alpha2=get_var('alpha2'), k0=get_var('k0'), k0_2=get_var('k0_2'), k_forward=get_var('k_forward'), k_backward=get_var('k_backward'), k_forward2=get_var('k_forward2'), k_backward2=get_var('k_backward2'), step_size=self.step_size, disk_radius=self.disk_radius, temperature=self.temperature, )
[docs] def fit( self, reduction_potential: _ParamGuess = None, reduction_potential2: _ParamGuess = None, diffusion_reactant: _ParamGuess = None, diffusion_product: _ParamGuess = None, alpha: _ParamGuess = None, alpha2: _ParamGuess = None, k0: _ParamGuess = None, k0_2: _ParamGuess = None, k_forward: _ParamGuess = None, k_backward: _ParamGuess = None, k_forward2: _ParamGuess = None, k_backward2: _ParamGuess = None, ) -> tuple[np.ndarray, np.ndarray, dict[str, float]]: """ Fits the CV for a Square Scheme mechanism. If a parameter is given, it must be a: float for initial guess of parameter; tuple[float, float] for (lower bound, upper bound) of the initial guess; or tuple[float, float, float] for (initial guess, lower bound, upper bound). Parameters ---------- reduction_potential : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the reduction potential of the first one-electron transfer process (V vs. reference). Defaults to None. reduction_potential2 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the reduction potential of the second one-electron transfer process (V vs. reference). Defaults to None. diffusion_reactant : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the diffusion coefficient of reactant (cm^2/s). Defaults to None. diffusion_product : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the diffusion coefficient of product (cm^2/s). Defaults to None. alpha : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the charge transfer coefficient of the first redox process (no units). Defaults to None. alpha2 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the charge transfer coefficient of the second redox process (no units). Defaults to None. k0 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the standard electrochemical rate constant of the first redox process (cm/s). Defaults to None. k0_2 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the standard electrochemical rate constant of the second redox process (cm/s). Defaults to None. k_forward : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the first order forward chemical rate constant for the first redox species (1/s). Defaults to None. k_backward : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the first order backward chemical rate constant for the first redox species (1/s). Defaults to None. k_forward2 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the first order forward chemical rate constant for the second redox species (1/s). Defaults to None. k_backward2 : None | float | tuple[float, float] | tuple[float, float, float] Optional guess for the first order backward chemical rate constant for the second redox species (1/s). Defaults to None. Returns ------- voltage : np.ndarray Array of potential (V) values of the CV fit. current_fit : np.ndarray Array of current (A) values of the CV fit. final_fit : dict[str, float] Dictionary of final fitting parameter values of the CV fit. """ return self._fit({ 'reduction_potential': reduction_potential, 'reduction_potential2': reduction_potential2, 'diffusion_reactant': diffusion_reactant, 'diffusion_product': diffusion_product, 'alpha': alpha, 'alpha2': alpha2, 'k0': k0, 'k0_2': k0_2, 'k_forward': k_forward, 'k_backward': k_backward, 'k_forward2': k_forward2, 'k_backward2': k_backward2, })