def activity_steps( data, mols, cycles, cycle_str='selector', mode='average', t_int=15, t_tail=30, t_pre=15, find_max=False, t_max_buffer=5, V_max_buffer=5, find_min=False, t_min_buffer=5, V_min_buffer=5, background=None, t_bg=None, unit='pmol/s', ax='new', tspan_plot=None, verbose=True, ): ''' Powerful function for determining activity and faradaic efficiency for a set of potential steps. Requires calibrated molecule objects (mols) and cycle numbers, which by default refer to data['selector'] if mode='average', it integrates over the last t_int of each cycle. If mode='integral', it integrates from t_pre before the start until t_tail after the end of each cycle. If find_max=True, rather than using the full timespan of the cycle, it finds the timespan at which the potential is within V_max_buffer mV of its maximum value, and cuts of t_max_buffer, and then uses this timespan as above. Correspondingly for find_min, V_min_buffer, and t_min_buffer. A timespan for which to get the background signals at each of the masses can be given as t_bg. Alternately, background can be set to 'linear' in which case it draws a line connecting the signals just past the endpoints of the timespan for each cycle. If ax is not None, it highlights the area under the signals and EC currents that are integrated/averaged. The function returns a dictionary including: 'Qs': the integrated charges or averaged currents for each cycle 'ns': dictionary containing, for each molecule, the integrated amounts or average flux for each cycle 'Vs': the average potential for each cycle 'ax': the axes on which the function plotted. ''' if verbose: print('\n\nfunction \'evaluate_datapoints\' at your service!\n') # ----- parse inputs -------- # try: iter(mols) except TypeError: mols = [mols] mdict = dict([(m.name, m) for m in mols]) if mode in ['average', 'averaging', 'mean']: mode = 'average' elif mode in ['integral', 'integrate', 'integrating']: mode = 'integral' if t_bg is not None: bgs = {} for mol, m in mdict.items(): x_bg, y_bg = m.get_flux(data, tspan=t_bg, removebackground=False, unit=unit) bgs[mol] = np.mean(y_bg) # should perhaps give additional options for bg, but honostly t_bg is pretty good else: bgs = dict([(mol, 0) for mol in mdict.keys()]) if ax == 'new': ax = plot_experiment(data, mols, removebackground=False, tspan=tspan_plot, emphasis=None, unit=unit) else: try: iter(ax) except TypeError: ax = [ax] Qs, Vs = np.array([]), np.array([]) ns = dict([(mol, np.array([])) for mol in mdict.keys()]) for cycle in cycles: c = select_cycles(data, [cycle], cycle_str=cycle_str, verbose=verbose) if find_max: t_v, v = get_potential(c) v_max = max(v) mask = v_max - V_max_buffer * 1e-3 < v t_max = t_v[mask] t_start = t_max[0] + t_max_buffer t_end = t_max[-1] - t_max_buffer elif find_min: t_v, v = get_potential(c) v_min = min(v) mask = v < v_min + V_min_buffer * 1e-3 t_min = t_v[mask] t_start = t_min[0] + t_min_buffer t_end = t_min[-1] - t_min_buffer else: t_start = c['time/s'][0] t_end = c['time/s'][-1] if mode == 'average': try: iter(t_int) except TypeError: tspan = [t_end - t_int, t_end] else: tspan = [t_start + t_int[0], t_start + t_int[-1]] elif mode == 'integral': c = select_cycles(data, [cycle - 1, cycle, cycle + 1], cycle_str=cycle_str, verbose=verbose) tspan = [t_start - t_pre, t_end + t_tail] t_v, v = get_potential(c, tspan=tspan, verbose=verbose) V = np.mean(v) Vs = np.append(Vs, V) t, I = get_current(c, tspan=tspan, verbose=verbose, unit='A') if mode == 'average': Q = np.mean(I) elif mode == 'integral': Q = np.trapz(I, t) Qs = np.append(Qs, Q) for mol, m in mdict.items(): x, y0 = m.get_flux(c, tspan=tspan, unit=unit, verbose=verbose) bg = bgs[mol] y = y0 - bg if mode == 'average': yy = np.mean(y) elif mode == 'integral': yy = np.trapz(y, x) ns[mol] = np.append(ns[mol], yy) if ax is not None: try: iter(bg) except TypeError: bg = bg * np.ones(y0.shape) color = m.get_color() ax[0].fill_between(x, y0, bg, where=y0 > bg, color=color, alpha=0.5) if ax is not None: ax[1].plot(t_v, v, 'k-', linewidth=3) J = I * 1e3 / data['A_el'] bg_J = np.zeros(J.shape) ax[2].fill_between(t, J, bg_J, color='0.5', alpha=0.5) if verbose: print('\nfunction \'evaluate_datapoints\' finished!\n\n') return {'Qs': Qs, 'ns': ns, 'Vs': Vs, 'ax': ax}
def get_datapoints(dataset, cycles, mols=['H2', 'C2H4', 'CH4'], tspan=[0, 100], t_steady=[50, 60], Vcycle=0, transient='CH4', colors=None, cycle_str=None, plotcycles=False, plottransient=False, data_type='CA', verbose=True): ''' Ways to control this function: (1) put in a dictionary for mols with plotting colors and sub-dictionaries for products that should be split into transient ('dyn') and steady-state ('ss') for example, mols={'C2H4':'g', 'CH4':{'ss':'r','dyn':[0.8, 0, 0]} All transient integrations will use same t_steady (2) ''' if verbose: print('\n\nfunction \'get_datapoints\' at your service!\n') #interpret inputs. if type(mols) is dict: colors = mols.copy() if colors is None: colors = mols if type(mols) is dict: mols = list(mols.keys()) if type(transient) is str: transient = [transient] else: mols = list(colors.keys()) transient = [ mol for (mol, value) in colors.items() if type(value) is dict ] if type(t_steady) is dict: transient = list(t_steady.keys()) else: ts = t_steady t_steady = {} if type(colors) is dict: for mol in transient: if type(colors[mol]) is dict: colors[mol] = colors[mol]['ss'] #just for plotting cycles with appropriate colors if Vcycle in ['previous', 'last', 'rest']: Vcycle = -1 elif Vcycle in ['present', 'current', 'same', 'work']: Vcycle = 0 elif Vcycle in ['next']: Vcycle = 1 V_str = dataset['V_str'] #prepare space for results: V = [] integrals = {} for mol in mols: if mol in transient: integrals[mol] = {'ss': [], 'dyn': []} if mol not in t_steady.keys(): t_steady[mol] = ts else: integrals[mol] = [] #get results: for cycle in cycles: off_data = select_cycles(dataset, cycles=cycle + Vcycle, t_zero='start', data_type=data_type, cycle_str=cycle_str, verbose=verbose) #off_data is data from the cycle that the independent variable is obtained form on_data = select_cycles(dataset, cycles=[cycle, cycle + 1], t_zero='start', data_type=data_type, cycle_str=cycle_str, verbose=verbose) #on_data is data from the cycle, and following cycle for the tail, that the dependent variable is obtained from t_off = off_data['time/s'] V_off = off_data[V_str] V += [np.trapz(V_off, t_off) / (t_off[-1] - t_off[0])] if plotcycles: title = str(cycle) + ', U = ' + str(V[-1]) plot_experiment(on_data, mols=colors, title=title, verbose=verbose) for mol in integrals.keys(): title = mol + ', cycle=' + str(cycle) + ', U=' + str(V[-1]) if verbose: print('working on: ' + str(mol)) x, y = get_flux(on_data, tspan=tspan, mol=mol, removebackground=True, unit='nmol/s', verbose=verbose) if type(integrals[mol]) is dict: ts = t_steady[mol] if plottransient: ax = 'new' else: ax = None ss, dyn = integrate_transient(x, y, tspan=tspan, t_steady=ts, ax=ax, title=title, verbose=verbose) integrals[mol]['ss'] += [ss] integrals[mol]['dyn'] += [dyn] else: integrals[mol] += [np.trapz(y, x)] integrals['V'] = V if verbose: print('\nfunction \'get_datapoints\' finished!\n\n') return integrals
def calibration_curve(data, mol, mass='primary', n_el=-2, cycles=None, cycle_str='selector', mode='average', t_int=15, t_tail=30, t_pre=15, find_max=False, t_max_buffer=5, V_max_buffer=5, find_min=False, t_min_buffer=5, V_min_buffer=5, background=None, t_bg=None, tspan_plot=None, remove_EC_bg=False, color=None, force_through_zero=False, ax='new', J_color='0.5', unit=None, out='Molecule', verbose=True): ''' Powerful function for integrating a molecule when the assumption of 100% faradaic efficiency can be made. Requires a dataset, and cycle numbers, which by default refer to data['selector'] if mode='average', it integrates over the last t_int of each cycle. If mode='integral', it integrates from t_pre before the start until t_tail after the end of each cycle. If find_max=True, rather than using the full timespan of the cycle, it finds the timespan at which the potential is within V_max_buffer mV of its maximum value, and cuts of t_max_buffer, and then uses this timespan as above. Correspondingly for find_min, V_min_buffer, and t_min_buffer. A timespan for which to get the background signals at each of the masses can be given as t_bg. Alternately, background can be set to 'linear' in which case it draws a line connecting the signals just past the endpoints of the timespan for each cycle. If ax is not None, it highlights the area under the signals and EC currents that are integrated/averaged, and also makes the calibration curve. The can return any or multiple of the following: 'Qs': the integrated charges or averaged currents for each cycle 'ns': the corresponding amount or flux for each cycle 'Ys': the integrated or averaged signal for each cycle 'Vs': the average potential for each cycle 'F_cal': calibration factor in C/mol 'Molecule': Molecule object with the calibration factor 'ax': the axes on which the function plotted. out specifies what the function returns. By default, it returns the molecule ''' # ----- parse inputs -------- # m = Molecule(mol) if mass == 'primary': mass = m.primary else: m.primary = mass if mode in ['average', 'averaging', 'mean']: mode = 'average' elif mode in ['integral', 'integrate', 'integrating']: mode = 'integral' use_bg_fun = False if t_bg is not None: x_bg, y_bg = get_signal(data, mass=mass, tspan=t_bg, unit='A') bg = np.mean(y_bg) elif callable(background): use_bg_fun = True elif background is not None and type(background) is not str: bg = background else: bg = 0 if unit is None: if mode == 'average': unit = 'p' # pmol/s and pA elif mode == 'integral': unit = 'n' # nmol and nC elif unit[0] in ['p', 'n', 'u']: unit = unit[0] # I'm only going to look at the prefix else: print('WARNING! unit=' + str(unit) + ' not recognized. calibration_curve() using raw SI.') unit = '' # ---------- shit, lots of plotting options... ---------# ax1, ax2a, ax2b, ax2c = None, None, None, None fig1, fig2 = None, None if ax == 'new': ax1 = 'new' ax2 = 'new' else: try: iter(ax) except TypeError: ax2c = ax else: try: ax1, ax2 = ax except (TypeError, IndexError): print('WARNING: calibration_curve couldn\'t use the give axes') if ax1 == 'new': ax1 = plot_experiment(data, masses=[mass], tspan=tspan_plot, emphasis=None, removebackground=False, unit='A') fig1 = ax1[0].get_figure() else: try: ax1a = ax1[0] except TypeError: ax1a = ax1 plot_signal(data, masses=[mass], tspan=tspan_plot, removebackground=False, unit='A', ax=ax1a) if ax2 == 'new': fig2, [ax2a, ax2c] = plt.subplots(ncols=2) ax2b = ax2a.twinx() fig2.set_figwidth(fig1.get_figheight() * 3) else: try: iter(ax2) except TypeError: ax2c = ax2 else: try: ax2a, ax2b, ax2c = ax2 except (TypeError, IndexError): print('WARNING: calibration_curve couldn\'t use the give ax2') # ----- cycle through and calculate integrals/averages -------- # Ys, ns, Vs, Is, Qs = [], [], [], [], [] for cycle in cycles: c = select_cycles(data, [cycle], cycle_str=cycle_str, verbose=verbose) if find_max: t_v, v = get_potential(c) v_max = max(v) mask = v_max - V_max_buffer * 1e-3 < v t_max = t_v[mask] t_start = t_max[0] + t_max_buffer t_end = t_max[-1] - t_max_buffer print('v_max = ' + str(v_max)) # debugging elif find_min: t_v, v = get_potential(c) v_min = min(v) mask = v < v_min + V_min_buffer * 1e-3 t_min = t_v[mask] t_start = t_min[0] + t_min_buffer t_end = t_min[-1] - t_min_buffer else: t_start = c['time/s'][0] t_end = c['time/s'][-1] print('[t_start, t_end] = ' + str([t_start, t_end]) + '\n\n') # debugging if mode == 'average': tspan = [t_end - t_int, t_end] elif mode == 'integral': c = select_cycles(data, [cycle - 1, cycle, cycle + 1], cycle_str=cycle_str, verbose=verbose) tspan = [t_start - t_pre, t_end + t_tail] t, I = get_current(c, tspan=tspan, verbose=verbose) t_v, v = get_potential(c, tspan=tspan, verbose=verbose) x, y = get_signal(c, mass=mass, tspan=tspan, verbose=verbose, unit='A') if use_bg_fun: # has to work on x. bg = background(x) elif type(background) is str and background in ['linear', 'endpoints']: if t_bg is None: t_bg = 5 tspan_before = [t_start - t_pre - t_bg, t_start - t_pre] tspan_after = [t_end + t_tail, t_end + t_tail + t_bg] x_before, y_before = get_signal(data, mass=mass, tspan=tspan_before) x_after, y_after = get_signal(data, mass=mass, tspan=tspan_after) x0, y0 = np.mean(x_before), np.mean(y_before) x1, y1 = np.mean(x_after), np.mean(y_after) bg = y0 + (x - x0) * (y1 - y0) / (x1 - x0) V = np.mean(v) if mode == 'average': I_av = np.mean(I) n = I_av / (n_el * Chem.Far) Y = np.mean(y - bg) Is += [I_av] elif mode == 'integral': Q = np.trapz(I, t) n = Q / (n_el * Chem.Far) Y = np.trapz(y - bg, x) Qs += [Q] if ax1 is not None: if color is None: color = m.get_color() try: iter(bg) except TypeError: y_bg = bg * np.ones(y.shape) else: y_bg = bg ax1[0].fill_between( x, y, y_bg, #where=y>y_bg, color=color, alpha=0.5) J = I * 1e3 / data['A_el'] J_bg = np.zeros(J.shape) ax1[2].fill_between(t, J, J_bg, color=J_color, alpha=0.5) ns += [n] Ys += [Y] Vs += [V] # ----- evaluate the calibration factor -------- # ns, Ys, Vs = np.array(ns), np.array(Ys), np.array(Vs) Is, Qs = np.array(Is), np.array(Qs) if remove_EC_bg: ns = ns - min(ns) if force_through_zero: F_cal = sum(Ys) / sum( ns) # I'd actually be surprised if any fitting beat this else: pfit = np.polyfit(ns, Ys, deg=1) F_cal = pfit[0] m.F_cal = F_cal # ----- plot the results -------- # if color is None: color = m.get_color() ax2 = [] if unit == 'p': ns_plot, Ys_plot = ns * 1e12, Ys * 1e12 elif unit == 'n': ns_plot, Ys_plot = ns * 1e9, Ys * 1e9 elif unit == 'u': ns_plot, Ys_plot = ns * 1e6, Ys * 1e6 else: ns_plot, Ys_plot = ns, Ys if ax2a is not None: # plot the internal H2 calibration V_str, J_str = sync_metadata(data, verbose=False) if n_el < 0: ax2a.invert_xaxis() ax2a.plot(Vs, ns_plot, '.-', color=J_color, markersize=10) ax2b.plot(Vs, Ys_plot, 's', color=color) ax2a.set_xlabel(V_str) if mode == 'average': ax2a.set_ylabel('<I>/(' + str(n_el) + '$\mathcal{F}$) / [' + unit + 'mol s$^{-1}$]') ax2b.set_ylabel('<' + mass + ' signal> / ' + unit + 'A') else: ax2a.set_ylabel('$\Delta$Q/(' + str(n_el) + '$\mathcal{F}$) / ' + unit + 'mol') ax2b.set_ylabel(mass + 'signal / ' + unit + 'C') colorax(ax2b, color) colorax(ax2a, J_color) #align_zero(ax2a, ax2b) ax2 += [ax2a, ax2b] if ax2c is not None: ax2c.plot(ns_plot, Ys_plot, '.', color=color, markersize=10) # plot the best-fit line if force_through_zero: ns_pred_plot = np.sort(np.append(0, ns_plot)) Y_pred_plot = F_cal * ns_pred_plot else: ns_pred_plot = np.sort(ns_plot) Y_pred_plot = F_cal * ns_pred_plot + pfit[1] #print('ns_pred_plot = ' + str(ns_pred_plot)) # debugging #print('Y_pred_plot = ' + str(Y_pred_plot)) # debugging ax2c.plot(ns_pred_plot, Y_pred_plot, '--', color=color) if mode == 'average': ax2c.set_xlabel('<I>/(' + str(n_el) + '$\mathcal{F}$) / [' + unit + 'mol s$^{-1}$]') ax2c.set_ylabel('<' + mass + ' signal> / ' + unit + 'A') else: ax2c.set_xlabel('$\Delta$Q/(' + str(n_el) + '$\mathcal{F}$) / ' + unit + 'mol') ax2c.set_ylabel(mass + ' signal / ' + unit + 'C') ax2 += [ax2c] # ------- parse 'out' and return -------- # if fig1 is None and ax1 is not None: fig1 = ax1[0].get_figure() if fig2 is None and ax2 is not None: fig2 = ax2[0].get_figure() possible_outs = { 'ax': [ax1, ax2], 'fig': [fig1, fig2], 'Molecule': m, 'Is': Is, 'Qs': Qs, 'F_cal': F_cal, 'Vs': Vs, 'ns': ns, 'Ys': Ys } if type(out) is str: outs = possible_outs[out] else: outs = [possible_outs[o] for o in out] if verbose: print('\nfunction \'calibration_curve\' finished!\n\n') return outs