def load_calibration_results(f, verbose=True): ''' Given the name of a pickle file saved by save_calibration_results(), opens the dictionary stored in the pickle file and does its best to convert the information stored in its values into molecule objects. ''' if verbose: print('\n\nfunction \'load_calibration_results\' at your service!\n') if type(f) is str: with open(f, 'rb') as f: # load the calibrations! calibration_results = pickle.load(f) else: calibration_results = pickle.load(f) mdict = {} # turn the calibration results back into Molecule objects for mol, result in calibration_results.items(): try: m = Molecule(mol, verbose=verbose) except FileNotFoundError: # this happens if any molecule names were changed try: m = Molecule(result['formula'], verbose=verbose) m.name = mol except (KeyError, FileNotFoundError): m = result else: for attr, value in result.items(): setattr(m, attr, value) mdict[mol] = m if verbose: print('\nfunction \'load_calibration_results\' finished!\n\n') return mdict
def getSymmetryElements(mol): from chem.molecules import Atom testmol = mol.copy() testmol.recenter() testmol.reorient() if isinstance(mol, list): #oops, xyz, better make a molecule object from Molecules import Molecule, getAtomListFromXYZ atomList = getAtomListFromXYZ(mol) testmol = Molecule(atomList) def testTransformation(trans, atoms): for atom in atoms: newAtom = atom.copy() newAtom.transform(trans) test = testmol.testAtomEquivalence(newAtom) if not test: #this is not a symmetry element return False return True symmElements = [] allAtoms = testmol.getAtoms() for trans in TRANSFORMATION_ORDER: matrix = transformations[trans] isSymmOp = testTransformation(matrix, allAtoms) if isSymmOp: symmElements.append(trans) return symmElements
def carrier_gas_cal( dataset=None, #if signal not given, reads average from dataset signal=None, #steady-state signal from in-flux of carrier gas mol='He', #calibration Molecule object or name of calibration molecule carrier=None, viscosity=None, mass='primary', #mass at which to calibrate composition=1, #mol fraction calibration molecule in carrier gas chip='SI-3iv1-1-C5', #chip object or name of chip tspan=None, ): ''' returns a calibration factor based the carrier gas concentration ''' calibration = {'type': 'carrier gas'} if type(chip) is str: chip = Chip(chip) if type(mol) is str: mol = Molecule(mol) if carrier is None: carrier = mol elif type(carrier) is str: carrier = Molecule(carrier) if mass == 'primary': mass = mol.primary if type(composition) in (float, int): fraction = composition elif type(composition) is dict: fraction = composition[mol.name] n_dot = chip.capillary_flow(gas=carrier) n_dot_i = fraction * n_dot F_cal = signal / n_dot_i if signal is None: if tspan is None: tspan = dataset['txpan'] x, y = get_signal(dataset, mass=mass, tspan=tspan) calibration['Q_QMS'] = np.trapz(y, x) signal = calibration['Q_QMS'] / (tspan[-1] - tspan[0]) calibration['n_mol'] = n_dot_i * (tspan[-1] - tspan[0]) calibration['mass'] = mass calibration['n_dot_i'] = n_dot_i calibration['signal'] = signal calibration['F_cal'] = F_cal return calibration
def delta_response(L=100e-6, q0=1e15/Chem.NA, mol='H2', D=None, kH=None, n_el=None, A=0.196e-4, p_m=1e5, Temp=298.15, verbose=True, tspan='auto', N_t=1000): ''' Returns the output when a stagnant_operator operates on a delta function. There's probably a much smarter way to do it, but for now I'll just do triangle pulse of width tau/250 ''' if D is None or kH is None: if type(mol) is str: mol = Molecule(mol) if D is None: D = mol.D if kH is None: kH = mol.kH if verbose: print('calculating a delta function response.') h = kH*Chem.R*Temp*q0/(p_m*A) #mass transfer coefficeint tau = L/h + L**2/(2*D) if tspan=='auto': tspan = [0, 4*tau] t = np.linspace(tspan[0], tspan[1], N_t) j = np.append(np.array([1]), np.zeros(N_t-1)) tj = [t,j] print(type(tj)) return stagnant_pulse(tj=tj, normalize=True, tspan=tspan, L=L, A=A, q0=q0, p_m=p_m, D=D, kH=kH, n_el=n_el, Temp=Temp, verbose=True, plot_type=None)
def chip_calibration(data, mol='O2', F_cal=None, primary=None, tspan=None, tspan_bg=None, gas='air', composition=None, chip='SI-3iv1'): ''' Returns obect of class EC_MS.Chip, given data for a given gas (typically air) for which one component (typically O2 at M32) has a trusted calibration. The chip object has a capillary length (l_cap) that is set so that the capillary flux matches the measured signal for the calibrated gas. ''' if type(mol) is str: m = Molecule(mol) else: m = mol mol = mol.name if F_cal is not None: m.F_cal = F_cal if primary is not None: m.primary = primary if gas == 'air' and composition is None: composition = air_composition[mol] x, y = m.get_flux(data, tspan=tspan, unit='mol/s') if tspan_bg is not None: x_bg, y_bg = m.get_flux(data, tspan=tspan_bg, unit='mol/s') y0 = np.mean(y_bg) else: y0 = 0 n_dot = np.mean(y) - y0 if type(chip) is str: chip = Chip(chip) n_dot_0 = chip.capillary_flow(gas=gas) / Chem.NA * composition l_eff = chip.l_cap * n_dot_0 / n_dot chip.l_cap = l_eff chip.parameters['l_cap'] = l_eff return chip
def capillary_flow(self, gas='He', w_cap=None, h_cap=None, l_cap=None, T=None, p=None): ''' adapted from Membrane_chip.py, equations from Daniel Trimarco's PhD Thesis Returns the flux in molecules/s of a carrier gas through the chip capillary. As the flow starts out viscous, at low analyte production rates, an analyte flux is simply the analyte's mol fraction in the chip times this flux. We assume that flow is governed by the bulk properties (viscosity) of the carrier gas and the molecular properties (diameter, mass) of the analyte. ''' if type(gas) is str: gas = Molecule(gas) ''' #This does not work! I do not know why. print(self.w_cap) for var in 'w_cap', 'h_cap', 'l_cap', 'T', 'p': if locals()[var] is None: locals()[var] = getattr(self, var) ''' #This is actually the most compact way I can figure out to do this. w = next(x for x in [w_cap, self.w_cap] if x is not None) h = next(x for x in [h_cap, self.h_cap] if x is not None) l = next(x for x in [l_cap, self.l_cap] if x is not None) T = next(x for x in [T, self.T] if x is not None) p = next(x for x in [p, self.p] if x is not None) s = gas.molecule_diameter #molecular diameter in m m = gas.molecule_mass #molecular mass in kg eta = gas.dynamic_viscosity #viscosity in Pa*s d = ((w * h) / np.pi)**0.5 * 2 # hydraulic diameter #d=4.4e-6 #used in Henriksen2009 a = d / 2 #hydraulic radius p_1 = p #pressure at start of capillary (chip pressure) lam = d #mean free path of transition from visc. to mol. flow p_t = Chem.kB * T / (2**0.5 * np.pi * s**2 * lam) #transition pressure p_2 = 0 #pressure at end of capillary (vacuum) p_m = (p_1 + p_t) / 2 #average pressure in the visc. + trans. flow region v_m = (8 * Chem.kB * T / (np.pi * m))**0.5 #mean molecular velocity nu = (m / (Chem.kB * T))**0.5 #a resiprocal velocity used for short-hand #dumb, but inherited straight from Henrikson2009 through all of #Daniel's work up to his thesis, where it was finally dropped, which #unfortunatly makes that term of the equation a bit silly-looking. N_dot = (1 / (Chem.kB * T) * 1 / l * ((a**4 * np.pi / (8 * eta) * p_m + a**3 * 2 * np.pi / 3 * v_m * (1 + 2 * a * nu * p_m / eta) / (1 + 2.48 * a * nu * p_m / eta)) * (p_1 - p_t) + a**3 * 2 * np.pi / 3 * v_m * (p_t - p_2))) return N_dot
def flow_operator( mode='steady', #in steady mode it's not really an operator. system='chip', # A_el=0.196e-4, A=0.196e-4, q0=1.5e15 / Chem.NA, Temp=None, #universal pars L=100e-6, w=5e-3, w2=5e-3, F=1e-9, #geometry pars #w and w2 changed from 0.5e-3 to 5e-3 on 17K28. c0=None, j0=None, j_el=-1, #inlet flow pars p_m=1e5, #chip pars phi=0.5, dp=20e-9, Lp=100e-6, #DEMS pars mol='H2', D=None, kH=None, n_el=None, M=None, #mol pars p_gas=0, normalize=False, unit='pmol/s', flux_direction='out', N=100, #solver pars verbose=True): ''' Follows the recipe in Scott's MSc, page 61, for calculating collection efficiency in a flow system by solving a differential equation. This can be used to compare different types of EC-MS ''' if verbose: print('\n\nfunction \'flow_operator\' at your service!\n') if type(mol) is str: mol = Molecule(mol) if Temp is not None: mol.set_temperature(Temp) else: Temp = 298.15 #standard temperature in K if D is None: D = mol.D #diffusion constant in electrolyte / [m^2/s] if kH is None: kH = mol.kH #dimensionless henry's law constant if M is None: M = Chem.Mass(mol.name) * 1e-3 # molar mass / [kg/mol] #print(c0) if n_el is None and c0 is None and not normalize: n_el = mol.n_el if c0 is None: if j0 is None: j0 = j_el * A_el / (n_el * Chem.Far) c0 = j0 / F #concentration is production rate over flow rate: [mol/s] / [m^3/s)] = [mol/m^3] ) if system == 'chip': h = kH * Chem.R * Temp * q0 / (p_m * A) #Scott's MSc, page 50 elif system == 'DEMS': h = kH * phi * dp / (3 * Lp) * np.sqrt( 8 / np.pi * Chem.R * Temp / M) #Scott's MSc, page 49 v0 = F / (L * w2) alpha = h * L / D beta = v0 * L**2 / (D * w) #There is a mistake in Scott's MSc page60! # There I got the non-dimensionalization wrong, and wrote beta = v0*w**2/(D*L) # in fact, beta = v0*L**2/(D*w) Xspan = np.linspace(0, 1, 1000) X, CC = solve_flow(alpha=alpha, beta=beta, Xspan=Xspan, N=N, verbose=verbose) x = X * w cc = CC * c0 j = cc[:, 0] * h # mol/m^3 * m/s = mol/(m^2*s) Z = np.linspace(0, 1, N) z = Z * L eta_m = 1 - np.trapz(CC[-1, :], Z) #Scott's MSc, page 61 eta_m_check = w2 * np.trapz(j, x) / ( c0 * F) # m*mol/(m^2*s)*m / ((mol/m^3)*m^3/s) = 1 qm = c0 * F * eta_m if verbose: print('portion not escaped = ' + str(eta_m)) print('portion collected = ' + str(eta_m_check) + '\n\n') if system == 'chip': eta_v = 1 elif system == 'DEMS': p_w = Chem.p_vap(mol='H2O', T=Temp, unit='Pa') M_w = Chem.Mass('H2O') * 1e-3 j_w = A * p_w / (Chem.R * Temp) * phi * dp / (3 * Lp) * np.sqrt( 8 / np.pi * Chem.R * Temp / M_w) eta_v = q0 / (qm + j_w) #fixed 17H10 eta = eta_m * eta_v if verbose: print('q0 = ' + str(q0) + ' mol/s, h = ' + str(h) + ' m/s, alpha = ' + str(alpha) + ', j0 = ' + str(j0) + ' mol/s, max(c)/c0 = ' + str(np.max(np.max(cc)) / c0) + ', kH = ' + str(kH) + ', eta = ' + str(eta) + ', mol = ' + str(mol.name) + ', system = ' + str(system) + '' + ', beta = ' + str(beta) + '' + ', v0 = ' + str(v0)) if verbose: print('\nfunction \'flow_operator\' at finished!\n\n') results = { 'x': x, 'z': z, 'j': j, 'cc': cc, 'eta_m': eta_m, 'eta_v': eta_v, 'eta': eta, 'dimensions': 'xz' } return results
def stagnant_operator(tj=None, tpulse=10, tspan=None, j_el=-1, L=100e-6, A=0.196e-4, q0=1.5e15 / Chem.NA, p_m=1e5, mol='H2', p_gas=0, normalize=False, D=None, kH=None, n_el=None, Temp=None, unit='pmol/s', flux_direction='out', verbose=True, ax=None, plot_type=None, startstate='zero', N=30, colormap='plasma', aspect='auto'): ''' Models a pulse of current towards a specified product in our EC-MS setup. Theory in chapter 2 of Scott's masters thesis. all arguments are in pure SI units. The electrode output can either be given as a steady-state square pulse of electrical current (tpulse, j_el, n_el), or as a measured current (tj[1]) as a function of time (tj[0]) #tj[1] should have units A/m^2. 1 mA/cm^2 is 10 A/m^2 #17B02: p_gas is the partial pressure of the analyte in the carrier gas. # this enables, e.g., CO depletion modelling. ''' if verbose: print('\n\nfunction \'stagnant_operator\' at your service!\n') if type(mol) is str: mol = Molecule(mol) if Temp is not None: mol.set_temperature(Temp) else: Temp = 298.15 #standard temperature in K if D is None: D = mol.D if kH is None: kH = mol.kH if n_el is None and not normalize: n_el = mol.n_el if tspan is None: if tj is None: tspan = [-0.1 * tpulse, 1.2 * tpulse] else: tspan = [tj[0][0], tj[0][-1]] h = kH * Chem.R * Temp * q0 / (p_m * A) #mass transfer coefficeint alpha = L * h / D #system parameter #non-dimensional scales: t0 = L**2 / D if tj is None: if normalize: j0 = 1 else: j0 = j_el / (n_el * Chem.Far) else: t = tj[0] if normalize: j0 = 1 j = tj[1] / np.max(np.abs(tj[1])) else: j = tj[1] / (n_el * Chem.Far) # A/m^2 --> mol/(m^2*s) j0 = max(np.abs(j)) c0 = j0 * L / D tau = L**2 / (2 * D) + L / h #from the approximate analytical solution, Scott's thesis appendix D Tpulse = tpulse / t0 Tspan = np.linspace(tspan[0], tspan[1], 1000) / t0 #why do I give so many time points? if tj is None: def J_fun(T): if T < 0: return 0 if T < Tpulse: return 1 return 0 else: T_in = t / t0 J_in = j / max(np.abs(j)) #print('max(J_in) = ' + str(max(J_in))) def J_fun(T): if T < T_in[0]: #assume no current outside of the input tj data return 0 if T < T_in[-1]: return np.interp(T, T_in, J_in) return 0 c_gas = p_gas / (Chem.R * Temp) cg = c_gas / kH #concentration analyte in equilibrium with carrier gas, 17B02 Cg = cg / c0 #non-dimensionalized concentration analyte at equilibrium with carrier gas, 17B02 #pars = ([alpha, J_fun, Cg],) #odeint needs the tuple. 17A12: Why ?! [T, CC] = solve_stagnant(alpha=alpha, J_fun=J_fun, Cg=Cg, Tspan=Tspan, startstate=startstate, flux=False, N=N) cc = CC * c0 t = T * t0 j = h * (cc[:, 0] - cg) #mass transport at the membrane #j1 = D * (cc[:,1] - cc[:,0]) #fick's first law at the membrane gives the same j :) if verbose: print('q0 = ' + str(q0) + ' mol/s, h = ' + str(h) + ' m/s, alpha = ' + str(alpha) + ', j0 = ' + str(j0) + ' mol/(m^2*s), max(j)/j0 = ' + str(max(j) / j0) + ', t0 = ' + str(t0) + ' s, c0 = ' + str(c0) + ' mol/m^3' + ', tau (analytical) = ' + str(tau) + ' s' + ', cg = ' + str(cg) + ' mM') # get ready to plot: N = np.shape(cc)[1] z = np.arange(N) / (N - 1) * L #this will only be used for heatmap, so it's okay if dx isn't quite right. if 'cm^2' not in unit: j = j * A if unit[0] == 'u': j = j * 1e6 elif unit[0] == 'n': j = j * 1e9 elif unit[0] == 'p': j = j * 1e12 if flux_direction == 'in': j = -j if normalize: s_int = np.trapz(j, t) if verbose: print('normalizing from area = ' + str(s_int)) j = j / s_int #plotting was moved on 17G30 some legacy code here: if plot_type is not None and ax is not None: print( 'We recommend you plot seperately, using the function \'plot_operation\'.' ) axes = plot_operation(cc=cc, t=t, z=z, j=j, ax=ax, plot_type=plot_type, colormap=colormap, aspect=aspect, verbose=verbose) if verbose: print('\nfunction \'stagnant_operator\' finished!\n\n') return t, j, axes results = {'t': t, 'z': z, 'j': j, 'cc': cc, 'dimensions': 'tz'} return results
else: #if you insist return str(date) else: return str(date) date_string = '{0:2d}{1:1s}{2:2d}'.format(year%100, chr(ord('A') + month - 1), day) date_string = date_string.replace(' ', '0') return date_string if __name__ == '__main__': from Molecules import Molecule #make an 'H2' molecule from the data file a = Molecule('H2') a.birthday = date_scott() #today is it's birthday! a.mood = 'Happy' #make it happy #write everything about the happy H2 molecule to a file f = open('data/test.txt', 'w') attributes_to_file(f, a) f.close() #make a CO2 molecule from the data file... b = Molecule('CO2') print(b.name) #... and confuse the shit out it! f = open('data/test.txt', 'r') file_to_attributes(f, b) #its attributes are reset with the H2 data f.close() print(b.name) #now it thinks it's H2. print(b.__str__) #but deep down it's not
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
def point_calibration( data, mol, mass='primary', cal_type='internal', tspan=None, n_el=None, tail=0, tspan_bg=None, chip=None, composition=None, carrier=None, ): ''' Returns a molecule object calibrated based in one of the following ways. internally: cal_type='internal', need n_el externally: cal_type='external', need chip, carrier, composition The signal is taken as an average over the tspan, or at a linear extrapolated point if len(tspan)==1. Same for current for internal cal. For external calibration, ''' m = Molecule(mol) if mass == 'primary': mass = m.primary if composition is None: if carrier == mol or carrier is None: composition = 1 elif carrier == 'air': composition = air_composition[mol] #get average signal if tspan is None: tspan = data['tspan'] if tspan_bg is not None: x_bg, y_bg = get_signal(data, mass, tspan=tspan_bg) y0 = np.mean(y_bg) else: y0 = 0 if type(tspan) in [int, float]: x, y = get_signal(data, mass, [tspan - 10, tspan + 10]) S = np.interp(tspan, x, y - y0) else: x, y = get_signal(data, mass, tspan=[tspan[0], tspan[1] + tail]) #S = np.trapz(y-y0, x) #/ (x[-1] - x[1]) S = np.mean(y) - y0 #more accurate for few, evenly spaced datapoints if cal_type == 'internal': if type(tspan) in [int, float]: t, i = get_current(data, tspan=[tspan - 10, tspan + 10], unit='A') I = np.interp(tspan, t, i) else: t, i = get_current(data, tspan=tspan, unit='A') I = np.mean(i) #np.trapz(i, t) #/ (t[-1] - t[1]) n = I / (n_el * Chem.Far) elif cal_type == 'external': if chip is None: chip = 'SI-3iv1' if type(chip) is str: chip = Chip(chip) if carrier == None: carrier = mol n = chip.capillary_flow(gas=carrier) / Chem.NA * composition if type(tspan) not in [int, float]: pass #n = n * (tspan[-1]- tspan[0]) else: print('not sure what you mean, dude, when you say cal_type = \'' + cal_type + '\'') F_cal = S / n m.F_cal = F_cal print('point_calibration() results: S = ' + str(S) + ' , n = ' + str(n) + ', F_cal = ' + str(F_cal)) return m
def ML_strip_cal(CV_and_MS, cycles=[1, 2], t_int=200, cycle_str='cycle number', mol='CO2', mass='primary', n_el=None, Vspan=[0.5, 1.0], redox=1, ax='two', title='default', verbose=True, plot_instantaneous=False): ''' Determines F_cal = Q_QMS / n_electrode by integrating a QMS signal over tspan, assuming the starting value is background; and integrating over vspan the difference between two CV cycles and converting that to a molar amount. Returns a partially populated calibration dictionary. The calibration factor is calibration['F_cal'] ''' if verbose: print('\n\ncalibration function \'ML_strip_cal\' at your service!\n') if ax == 'two': fig1 = plt.figure() ax1 = fig1.add_subplot(211) ax2 = fig1.add_subplot(212) elif ax is list: ax1 = ax[0] ax2 = ax[1] elif ax is None: ax1 = None ax2 = None else: ax1 = ax ax2 = None if type(mol) is str: mol = Molecule(mol, writenew=False) name = mol.name if n_el is None: n_el = mol.n_el if mass == 'primary': mass = mol.primary if np.size(t_int) == 1: #t_int = CV_and_MS['tspan_2'][0] + np.array([0, t_int]) #assumes I've cut stuff. Not necessarily true. #better solution below (17C22) pass if title == 'default': title = name + '_' + mass # print(type(CV_and_MS)) #was having a problem due to multiple outputs. cycles_data, ax1 = plot_CV_cycles(CV_and_MS, cycles, ax=ax1, title=title, cycle_str=cycle_str) ax1.xaxis.tick_top() ax1.xaxis.set_label_position('top') # print(type(cycles_data)) Q_diff, diff = CV_difference(cycles_data, Vspan=Vspan, redox=redox, ax=ax1) #if ax1 is not None: # ax1.set_title(title) n_mol = Q_diff / (Chem.Far * n_el) t = diff[0] J_diff = diff[2] A_el = CV_and_MS['A_el'] if np.size(t_int) == 1: #17C22 t_int = t[0] + np.array([0, t_int]) #now it starts at the same time as EC data in the vrange starts #Q_diff seemed to be having a problem, but turned out to just be because #I forgot to remove_delay(). # Now everything checks out, as can be seen here: ''' Q_diff1 = A_el * np.trapz(J_diff, t) * 1e-3 #factor 1e-3 converts mA to A print('Q_diff = ' + str(Q_diff) +'\nQ_diff1 = ' + str(Q_diff1) + '\nratio = ' + str(Q_diff1/Q_diff)) ''' x = CV_and_MS[mass + '-x'] y = CV_and_MS[mass + '-y'] I_keep = [I for (I, x_I) in enumerate(x) if t_int[0] < x_I < t_int[1]] x = x[I_keep] y = y[I_keep] background = min(y) #very simple background subtraction Q_QMS = np.trapz(y - background, x) F_cal = Q_QMS / n_mol y_el = J_diff * A_el / (Chem.Far * n_el) * F_cal * 1e-3 #factor 1e-3 converts mA to A if ax2 is not None: ax2.plot(x, y * 1e9, 'k-') #ax2.plot(x, [background*1e9]*len(x), 'k-') ax2.fill_between( x, background * 1e9, y * 1e9, #where=y>background, facecolor='g', interpolate=True) if plot_instantaneous: ax2.plot(t, y_el * 1e9, 'r--') #Without mass transport ax2.set_xlabel('time / s') ax2.set_ylabel('signal / nA') # ax2.set_yscale('log') print(('QMS measured {0:5.2e} C of charge at M44 for {1:5.2e} mol ' + name + '.\n' + 'Calibration factor for CO2 at M44 is {2:5.2e} C / mol.').format( Q_QMS, n_mol, F_cal)) calibration = {'type': 'ML_strip'} calibration['raw_data'] = CV_and_MS['title'] calibration['mass'] = mass calibration['n_mol'] = n_mol calibration['Q_el'] = Q_diff calibration['Q_QMS'] = Q_QMS calibration['F_cal'] = F_cal calibration['title'] = title if verbose: print('\ncalibration function \'ML_strip_cal\' finished!\n\n') if ax2 is None: ax = ax1 else: ax = [ax1, ax2] return calibration, ax
def steady_state_cal(CA_and_MS, t_int='half', mol='CO2', mass='primary', n_el=None, ax='new', title='default', verbose=True, background='min'): if verbose: print( '\n\ncalibration function \'steady_state_cal\' at your service!\n') if type(mol) is str: mol = Molecule(mol, writenew=False) name = mol.name if n_el is None: n_el = mol.n_el if mass == 'primary': mass = mol.primary if t_int == 'half': t_int = (CA_and_MS['tspan_2'][1] + np.array(CA_and_MS['tspan_2'])) / 2 elif t_int == 'all': t_int = np.array(CA_and_MS['tspan_2']) elif np.size(t_int) == 1: t_int = CA_and_MS['tspan_2'][1] + np.array([-t_int, 0]) #by default integrate for time t_int up to end of interval if title == 'default': title = name + '_' + mass x = CA_and_MS[mass + '-x'] y = CA_and_MS[mass + '-y'] if background == 'min': background = min(y) elif background is None: background = 0 I_keep = [I for (I, x_I) in enumerate(x) if t_int[0] < x_I < t_int[1]] x_r = x[I_keep] y_r = y[I_keep] Q_QMS = np.trapz(y_r - background, x_r) #integrated signal in C V_str, J_str = sync_metadata(CA_and_MS) t = CA_and_MS['time/s'] J = CA_and_MS[J_str] A_el = CA_and_MS['A_el'] I_keep = [I for (I, t_I) in enumerate(t) if t_int[0] < t_I < t_int[1]] t_r = t[I_keep] J_r = J[I_keep] Q_el = A_el * np.trapz(J_r, t_r) * 1e-3 # total electrode charge passed in C n_mol = Q_el / (Chem.Far * n_el) F_cal = Q_QMS / n_mol y_el = J * A_el / (Chem.Far * n_el) * F_cal * 1e-3 # expected QMS signal without mass transport etc print(('QMS measured {0:5.2e} C of charge at M44 for {1:5.2e} mol ' + name + '.\n' + 'Calibration factor for CO2 at M44 is {2:5.2e} C / mol.').format( Q_QMS, n_mol, F_cal)) if ax == 'new': fig1 = plt.figure() ax = fig1.add_subplot(111) if ax is not None: ax.plot(x, y, 'k-') ax.plot(t, y_el + background, 'r--') ax.set_title(title) calibration = {'type': 'steady_state'} calibration['raw_data'] = CA_and_MS['title'] calibration['mass'] = mass calibration['n_mol'] = n_mol calibration['Q_el'] = Q_el calibration['Q_QMS'] = Q_QMS calibration['F_cal'] = F_cal if verbose: print('\ncalibration function \'steady_state_cal\' finished!\n\n') return calibration