Source code for eisfit.circuits

from .fitting import circuit_fit, computeCircuit, calculateCircuitLength
from .plotting import plot_nyquist
import matplotlib.pyplot as plt
import numpy as np


[docs]class BaseCircuit: """ Base class for equivalent circuit models """ def __init__(self, initial_guess=None, name=None, algorithm='leastsq', bounds=None): """ Base constructor for any equivalent circuit model """ # if supplied, check that initial_guess is valid and store if initial_guess is not None: for i in initial_guess: assert isinstance(i, (float, int, np.int32, np.float64)),\ 'value {} in initial_guess is not a number'.format(i) # initalize class attributes self.initial_guess = initial_guess self.name = name self.algorithm = algorithm self.bounds = bounds # initialize fit parameters and confidence intervals self.parameters_ = None self.conf_ = None
[docs] def fit(self, frequencies, impedance): """ Fit the circuit model Parameters ---------- frequencies: numpy array Frequencies impedance: numpy array of dtype 'complex128' Impedance values to fit Returns ------- self: returns an instance of self """ # check that inputs are valid: # frequencies: array of numbers # impedance: array of complex numbers # impedance and frequency match in length assert isinstance(frequencies, np.ndarray),\ 'frequencies is not of type np.ndarray' assert isinstance(frequencies[0], (float, int, np.int32, np.float64)),\ 'frequencies does not contain a number' assert isinstance(impedance, np.ndarray),\ 'impedance is not of type np.ndarray' assert isinstance(impedance[0], (complex, np.complex128)),\ 'impedance does not contain complex numbers' assert len(frequencies) == len(impedance),\ 'mismatch in length of input frequencies and impedances' if self.initial_guess is not None: parameters, conf = circuit_fit(frequencies, impedance, self.circuit, self.initial_guess, self.algorithm, bounds=self.bounds) self.parameters_ = parameters if conf is not None: self.conf_ = conf else: # TODO auto calc guess raise ValueError('no initial guess supplied') return self
def _is_fit(self): """ check if model has been fit (parameters_ is not None) """ if self.parameters_ is not None: return True else: return False
[docs] def predict(self, frequencies): """ Predict impedance using a fit equivalent circuit model Parameters ---------- frequencies: numpy array Frequencies Returns ------- impedance: numpy array of dtype 'complex128' Predicted impedance """ # check that inputs are valid: # frequencies: array of numbers assert isinstance(frequencies, np.ndarray),\ 'frequencies is not of type np.ndarray' assert isinstance(frequencies[0], (float, int, np.int32, np.float64)),\ 'frequencies does not contain a number' if self._is_fit(): return computeCircuit(self.circuit, self.parameters_.tolist(), frequencies.tolist()) else: raise ValueError("The model hasn't been fit yet. " + "Please call the `.fit` method before trying to" + " predict model output")
def __str__(self): """ Defines the pretty printing of the circuit """ # parse the element names from the circuit string names = self.circuit.replace('p', '').replace('(', '').replace(')', '') names = names.replace(',', '-').replace('/', '-').split('-') to_print = '\n-------------------------------\n' # noqa E222 to_print += 'Circuit: {}\n'.format(self.name) to_print += 'Circuit string: {}\n'.format(self.circuit) to_print += 'Algorithm: {}\n'.format(self.algorithm) if self._is_fit(): to_print += 'Fit: True\n' to_print += 'Fit parameters:\n' for name, param in zip(names, self.parameters_): to_print += '\t{} = {:.2e}\n'.format(name, param) else: to_print += 'Fit: False\n' to_print += 'Initial guesses:\n' for name, param in zip(names, self.initial_guess): to_print += '\t{} = {:.2e}\n'.format(name, param) to_print += '\n-------------------------------\n' return to_print
[docs] def plot(self, f_data=None, Z_data=None, CI=True): """ a convenience method for plotting Nyquist plots Parameters ---------- f_data: np.array of type float Frequencies of input data (for Bode plots) Z_data: np.array of type complex Impedance data to plot CI: boolean Include bootstrapped confidence intervals in plot Returns ------- ax: matplotlib.axes axes of the created nyquist plot """ fig, ax = plt.subplots(figsize=(5, 5)) if Z_data is not None: ax = plot_nyquist(ax, f_data, Z_data) if self._is_fit(): if f_data is not None: f_pred = f_data else: f_pred = np.logspace(5, -3) Z_fit = self.predict(f_pred) ax = plot_nyquist(ax, f_data, Z_fit, fit=True) if CI: N = 1000 n = len(self.parameters_) f_pred = np.logspace(np.log10(min(f_data)), np.log10(max(f_data)), num=100) params = self.parameters_ confs = self.conf_ full_range = np.ndarray(shape=(N, len(f_pred)), dtype=complex) for i in range(N): self.parameters_ = params + \ confs*np.random.uniform(-2, 2, size=n) full_range[i, :] = self.predict(f_pred) self.parameters_ = params min_Z = [] max_Z = [] for x in np.real(Z_fit): ys = [] for run in full_range: ind = np.argmin(np.abs(run.real - x)) ys.append(run[ind].imag) min_Z.append(x + 1j*min(ys)) max_Z.append(x + 1j*max(ys)) ax.fill_between(np.real(min_Z), -np.imag(min_Z), -np.imag(max_Z), alpha='.2') plt.show()
[docs]class Randles(BaseCircuit): """ A Randles circuit model class """ def __init__(self, CPE=False, **kwargs): """ Constructor for the Randles' circuit class Parameters ---------- CPE: boolean Use a constant phase element instead of a capacitor """ super().__init__(**kwargs) if CPE: self.name = 'Randles w/ CPE' self.circuit = 'R_0-p(R_1,E_1/E_2)-W_1/W_2' circuit_length = calculateCircuitLength(self.circuit) assert len(self.initial_guess) == circuit_length, \ 'Initial guess length needs to be equal to parameter length' else: self.name = 'Randles' self.circuit = 'R_0-p(R_1,C_1)-W_1/W_2' circuit_length = calculateCircuitLength(self.circuit) assert len(self.initial_guess) == circuit_length, \ 'Initial guess length needs to be equal to parameter length'
[docs]class DefineCircuit(BaseCircuit): def __init__(self, circuit=None, **kwargs): """ Constructor for a customizable equivalent circuit model Parameters ---------- circuit: string A string that should be interpreted as an equivalent circuit """ super().__init__(**kwargs) self.circuit = circuit circuit_length = calculateCircuitLength(self.circuit) assert len(self.initial_guess) == circuit_length, \ 'Initial guess length needs to be equal to {circuit_length}'
[docs]class FlexiCircuit(BaseCircuit): def __init__(self, max_elements=None, generations=2, popsize=30, initial_guess=None): """ Constructor for the Flexible Circuit class Parameters ---------- max_elements: integer The maximum number of elements available to the algorithm solve_time: integer The maximum allowed solve time, in seconds. """ self.name = 'Flexible Circuit' self.initial_guess = initial_guess self.generations = generations self.popsize = popsize self.max_elements = max_elements # self.bounds = bounds
[docs] def fit(self, frequencies, impedances): from scipy.optimize import leastsq from .genetic import make_population from .fitting import residuals n = 5 f = frequencies Z = impedances for i in range(self.generations): scores = [] self.population = make_population(self.popsize, n) for pop in self.population: self.circuit = pop print(self.circuit) circuit_length = calculateCircuitLength(self.circuit) # for j in range(n-4,n+4): # print(residuals(self.initial_guess,Z,f,self.circuit)) # try: # print(self.circuit) self.initial_guess = list(np.ones(circuit_length)/10) # print(residuals(self.initial_guess,Z,F,)) # print(self.initial_guess) p_values, covar, ff, _, _ = leastsq(residuals, self.initial_guess, args=(Z, f, self.circuit), maxfev=100000, ftol=1E-13, full_output=True) print(p_values) scores.append([np.square(ff['fvec']).mean(), pop]) print(scores) return scores