""" Tome v02 A compilation of all Lars' python scripts as callable functions IGNORE: functions: autoresolution (estimates the resolution of a spectrum) bindata (bins a list of values) binnspectra (bins n mass spectra together into a single mass spectrum) bincidspectra (bins mass spectra together based on their collision voltage) filepresent (checks for a file or directory in the current working directory) find_all (finds all locations of files of a given name in a provided directory) linmag (generates a list of values which is linear in magnification) linramp (generates a list of values which is linear from start to finish) locateinlist (locates a value or the closest value to it in a sorted list) lyround (rounds a number given a particular base number) mag (calculates and returns the magnification of a given y value relative to the start) normalize (normalizes a list to a given value) plotms (plots a mass spectrum) sigmafwhm (cacluates sigma and fwhm from a resolution and a mass) strtolist (converts a string to a list) version_input (uses the appropriate user input function depending on the python version) changelog: created mzML class and moved many functions to work within that class (removed several functions from Tome) added strtolist moved classes to separate files fullspeclist has been moved to _Spectrum class (there were issues with mutation of the original) calcindex has also been moved to _Spectrum class (it is used solely in that class) moved colours to _Colour class removed automz (now handled in the Molecule class) created bincidspectra to bin spectra with the same cid together removed loadwb, openpyxlcheck, pullparams (now included in XLSX class) generalized filepresent removed pwconvert (now included in mzML class) completely rewrote resolution rewrote resolution again to check multiple portions of the spectrum significant change to plotms moved alpha to XLSX class ---v02--- IGNORE """ import os import sys import scipy as sci import numpy as np from .spectrum import Spectrum from bisect import bisect_left, bisect_right from .colour import Colour from .molecule import IPMolecule import pylab as pl # ---------------------------------------------------------- # -------------------FUNCTION DEFINITIONS------------------- # ---------------------------------------------------------- def resolution(x, y, index=None, threshold=5): """ Finds the resolution and full width at half max of a spectrum :param x: list of mz values :param y: corresponding list of intensity values :param index: index of maximum intensity (optional; used if the resolution of a specific peak is desired) :param threshold: signal to noise threshold required to output a resolution :return: resolution """ y = sci.asarray(y) # convert to array for efficiency if index is None: # find index and value of maximum maxy = max(y) index = sci.where(y == maxy)[0][0] else: maxy = y[index] # if intensity to average is below this threshold (rough estimate of signal/noise) if maxy / (sum(y) / len(y)) < threshold: return None halfmax = maxy / 2 indleft = int(index) - 1 # generate index counters for left and right walking indright = int(index) + 1 while y[indleft] > halfmax: # while intensity is still above halfmax indleft -= 1 while y[indright] > halfmax: indright += 1 return x[index] / (x[indright] - x[indleft]) # return resolution (mz over full width at half max) def autoresolution(x, y, n=10, v=True): """ Attempts to determine the resolution of a provided spectrum by finding n pseudo-random samples, then finding a peak in each of those samples to determine the resolution. **Parameters** x: *list* List of x values (1D list) y: *list* List of y values (1D list, must be the same length as *x*) n: *int*, optional Number of sections to check in the supplied spectrum v: *Bool*, optional Verbose toggle **Returns** resolution: *float* The average resolution value determined by the function """ if len(x) == 0 or len(y) == 0: raise ValueError('the function has been handed an empty list') if v is True: sys.stdout.write('\rEstimating resolution of the spectrum') # find some peaks in the spectrum split = int(len(y) / n) start = 0 end = start + split splity = [] for i in range(n): splity.append(sci.asarray(y[start:end])) start += split end += split inds = [] for ind, section in enumerate(splity): maxy = max(section) if maxy == max(section[1:-1]): # if max is not at the edge of the spectrum inds.append(sci.where(section == maxy)[0][0] + split * ind) res = [] for ind in inds: # for each of those peaks res.append(resolution(x, y, ind)) res = [y for y in res if y is not None] # removes None values (below S/N) res = sum(res) / len(res) # calculate average if v is True: sys.stdout.write(': %.1f\n' % res) return res # return average def bindata(n, lst, v=1): """ Bins a list of values into bins of size *n*. **Parameters** n: *int* Number of values to bin together. e.g. ``n = 4`` would bin the first four values into a single value, then the next 4, etc. lst: *list* List of values to bin. v: *int* or *float*, optional Bin scalar. The calculated bin values will be divided by this value. e.g. if ``n = v`` the output values will be an average of each bin. **Returns** binned list: *list* A list of binned values. **Notes** - If the list is not divisible by `n`, the final bin will not be included in the output list. (The last values will be discarded.) """ out = [] delta = 0 ttemp = 0 for ind, val in enumerate(lst): delta += 1 ttemp += val # add current value to growing sum if delta == n: # critical number is reached out.append(ttemp / float(v)) # append sum to list delta = 0 # reset critical count and sum ttemp = 0 return out def binnspectra(lst, n, dec=3, start=50., end=2000.): """ Sums n spectra together. **Parameters** lst: *list* A list of paired lists of the form ``[ [[x1,x2,...,xn],[y1,y2,...,yn]] , [[],[]] ,...]`` where each index of the parent list is one paired spectrum of x and y values. The x values of one index do not have to be the same. The spectra will be combined based on the x value rounded to the nearest 10^-`dec`. n: *int* The number of adjacent spectra to bin together. e.g. ``n = 4`` would bin the first four spectra into a single spectrum, then the next 4, etc. dec: *int* The decimal place to track the x values to. e.g. ``dec = 3`` would track x values to the nearest 0.001 (10^-3) start: *float*, optional The minimum x value to track in the summed spectra. end: *float*, optional The maximum x value to track in the summed spectra. **Returns** binned spectrum list: *list* A list of paired lists (similar to *lst*) where each index is a binned spectrum. If there is only one item in the binned spectra, this returns a single paired list of the form ``[[x values],[y values]]``. """ out = [] delta = 0 spec = Spectrum( dec, start=start - 1, end=end + 1, ) for ind, (x, y) in enumerate(lst): # for each timepoint delta += 1 sys.stdout.write('\rBinning spectrum #%i/%i %.1f%%' % (ind + 1, len(lst), float(ind) / float(len(lst)) * 100.)) spec.add_spectrum(x, y) # add spectrum if delta == n: # critical number is reached out.append(spec.trim(zeros=True)) # append list spec.reset_y() # reset y list in object delta = 0 # reset critical sum sys.stdout.write(' DONE\n') if len(out) == 1: # if there is only one item return out[0] return out def bincidspectra(speclist, celist, dec=3, startmz=50., endmz=2000., threshold=0, fillzeros=False): """ Bins mass spectra together based on the collision voltage of associated with each spectrum. **Parameters** speclist: *list* A list of lists of the form ``[ [[x1,x2,...,xn],[y1,y2,...,yn]] , [[],[]] ,...]`` where each index of the parent list is one paired spectrum of x and y values. The x values of one index do not have to be the same. The spectra will be combined based on the x value rounded to the nearest 10^-`dec`. celist: *list* A list of collision energy values, where each index corresponds to the spectrum at that index of *speclist*. This list must be the same length as *speclist*. dec: *int* The decimal place to track the x values to. e.g. ``dec = 3`` would track x values to the nearest 0.001 (10^-3) startmz: *float*, optional The minimum mass to charge value to track in the summed spectra. end: *float*, optional The maximum mass to charge value to track in the summed spectra. threshold: *float*, optional The minimum y value intensity to track. fillzeros: *bool*, optional Whether to fill the resulting spectra with 0. for every value of x that does not have intensity. **Returns** specout: *list* A list of paired lists (similar to *speclst*) where each index is a binned spectrum. cv: *list* A sorted list of collision voltages with each index corresponding to that index in *specout*. """ binned = {} for ind, ce in enumerate(celist): sys.stdout.write('\rBinning spectrum by CID value #%i/%i %.1f%%' % ( ind + 1, len(celist), float(ind + 1) / float(len(celist)) * 100.)) if ce not in binned: # generate key and spectrum object if not present binned[ce] = Spectrum(dec, start=startmz, end=endmz) else: # otherwise add spectrum binned[ce].add_spectrum(speclist[ind][0], speclist[ind][1]) if threshold > 0 or fillzeros is True: # if manipulation is called for for vol in binned: # for each voltage sys.stdout.write('\rZero filling spectrum for %s eV' % str(vol)) if threshold > 0: binned[vol].threshold(threshold) # apply threshold if fillzeros is True: binned[vol].fill_with_zeros() # fill with zeros sys.stdout.write(' DONE\n') cv = [] # list for collision voltages specout = [] # list for spectra for vol, spec in sorted(binned.items()): sys.stdout.write('\rTrimming spectrum for %s eV' % str(vol)) cv.append(vol) # append voltage to list specout.append(spec.trim()) # append trimmed spectrum to list sys.stdout.write(' DONE\n') sys.stdout.flush() return specout, cv def find_all(fname, path): """ Finds all files matching a specified name within the directory specified. **Parameters** fname: *string* The name of the file to be located path: *string* The absolute directory path to search. **Returns** list of locations: *list* A list of all possible paths matching the filename in the specified directory. """ locations = [] for root, dirs, files in os.walk(path): if fname in files: locations.append(os.path.join(root, fname)) return locations def linmag(vali, magstart, magend, dur): """ Generates a ramp of values that is linear in magnification. **Parameters** vali: *float* The initial y value at the start of the ramp. magstart: *float* The magnification at the start of the ramp. magend: *float* The magnification at the end of the ramp. dur: *int* The desired number of steps to get from *magstart* to *magend*. **Returns** list of magnifications: *list* A list of magnifications corresponding to the ramp. """ out = [] for i in range(dur): out.append(float(vali) / ((magend - magstart) / dur * i + magstart)) return out def linramp(valstart, valend, dur): """ Generates a linear ramp of values. **Parameters** valstart: *float* The value at the start of the ramp. valend: *float* The value at the end of the ramp. dur: *int* The number of steps in the ramp. **Returns** List of ramped values: *list* """ out = [] for i in range(int(dur)): out.append(((float(valend - valstart)) / (float(dur))) * i + valstart) return out def locate_in_list(lst, value, bias='closest', within=0.1): """ Finds index in a sorted list of the value closest to a given value If two numbers are equally close, return the smallest number. roughly based on http://stackoverflow.com/questions/12141150/from-list-of-integers-get-number-closest-to-a-given-value :param lst: list of values to search :param value: value number to find :param bias: 'lesser' will return the position of the value just less than the provided value. 'greater' will return the position of the value just greater than the provided value. 'closest' will return the index of the nearest value to the one provided :param within: If the bias is closest, the position will only be returned if the position is this value away from the actual value :return: index of the position :rtype: int """ pos = bisect_left(lst, value) if pos == 0: # if at start of list return pos elif pos == len(lst): # if insertion is beyond index range return pos - 1 if lst[pos] == value: # if an exact match is found return pos if bias == 'greater': # return value greater than the value (bisect_left has an inherent bias to the right) return pos if bias == 'lesser': # return value lesser than the provided return pos - 1 if bias == 'closest': # check differences between index and index-1 and actual value, return closest adjval = abs(lst[pos - 1] - value) curval = abs(lst[pos] - value) if adjval > within and curval > within: # if the value is outside of the lookwithin bounds return None if adjval < curval: # if the lesser value is closer return pos - 1 if adjval == curval: # if values are equidistant return pos - 1 else: return pos def lyround(x, basen): """ Rounds the specified number using a specific base **Parameters** x: *float* The value to be rounded basen: *int* The number base to use for rounding **Returns** value: *float* The rounded value. **Notes** This function is based on http://stackoverflow.com/questions/2272149/round-to-5-or-other-number-in-python """ base = basen ** (int(len(str(int(x)))) - 1) return int(base * round(float(x) / base)) def mag(initial, current): """ Calculates the magnification of a specified value **Parameters** intial: *float* initial value (magnificiation of 1) current: *float* current value **Returns** magnification: *float* the magnification of the current value """ return float(initial) / float(current) def normalize(lst, maxval=1.): """ Normalizes a list of values with a specified value. **Parameters** lst: *list* List of values to be normalized maxval: *float*, optional The maximum value that the list will have after normalization. **Returns** normalized list: *list* A list of values normalized to the specified value. """ listmax = max(lst) for ind, val in enumerate(lst): lst[ind] = float(val) / float(listmax) * maxval return lst def localmax(x: list, y: list, xval: float, lookwithin: float = 1.): """ Finds the local maximum within +/- lookwithin of the xval :param x: x list :param y: y list :param xval: :param lookwithin: :return: maximum y value :rtype: float """ l = bisect_left(x, xval - lookwithin) r = bisect_right(x, xval + lookwithin) return max(y[l:r]) def trimspectrum(x: list, y: list, left: float, right: float, outside: bool = False): """ Trims a spectrum to the left and right bounds specified :param x: x value list :param y: y value list :param left: left trim value :param right: right trim value :param outside: Whether to include the next point outside of the trimmed spectrum. This provides continuity if the spectrum is to be used for image generation. :return: new spectrum :rtype: tuple of list """ # find indicies l = locate_in_list(x, left, 'greater') r = locate_in_list(x, right, 'lesser') if outside is True: l -= 1 r += 1 return x[l:r + 1], y[l:r + 1] # trim spectrum def estimated_exact_mass( x: list, y: list, em: float, simmin: float, simmax: float, lookwithin: float = 1, ): """ Estimates the exact mass of a peak in a spectrum within a provided simulated set of bounds :param x: x value list :param y: y value list :param em: estimated exact mass for the species :param simmin: minimum bound for the simulated isotope pattern :param simmax: maximum bounds for the simulated isotope pattern :param lookwithin: +/- bounds for the search :return: estimated exact mass :rtype: float """ # narrow range to that of the isotope pattern l = bisect_left(x, simmin - lookwithin) r = bisect_right(x, simmax + lookwithin) locmax = max(y[l:r]) # find local max in that range for ind, val in enumerate(y): if val == locmax: # if the y-value equals the local max if l <= ind <= r: # and if the index is in the range (avoids false locations) return x[ind] difleft = abs(em - simmin) difright = abs(em - simmax) return '>%.1f' % max(difleft, difright) # if no match is found, return maximum difference # TODO change simdict to be nonmutable def plot_mass_spectrum( realspec, simdict={}, mz='auto', # m/z bounds for the output spectrum outname='spectrum', # name for the output file output='save', # 'save' or 'show' the figure simtype='bar', # simulation overlay type ('bar' or 'gaussian') spectype='continuum', # spectrum type ('continuum' or 'centroid') maxy='max', # max or value norm=True, # True or False simnorm='spec', # top, spec, or value xlabel=True, # show x label ylabel=True, # show y label xvalues=True, # show x values yvalues=True, # show y values showx=True, # show x axis showy=True, # how y axis offsetx=True, # offset x axis (shows low intensity species better) fs=16, # font size lw=1.5, # line width for the plotted spectrum axwidth=1.5, # axis width simlabels=False, # show labels isotope for patterns bw='auto', # bar width for isotope patterns (auto does 2*fwhm) specfont='Arial', # the font for text in the plot size=[7.87, 4.87], # size in inches for the figure dpiout=300, # dpi for the output figure exten='png', # extension for the output figure resolution=None, # resolution to use for simulations (if not specified, automatically calculates) res_label=False, # output the resolution of the spectrum delta=False, # output the mass delta between the spectrum and the isotope patterns stats=False, # output the goodness of match between the spectrum and the predicted isotope patterns, speccolour='k', # colour for the spectrum to be plotted padding='auto', # padding for the output plot verbose=True, # verbose setting normwindow='fwhm', # the width of the window to look for a maximal value around the expected exact mass for a peak annotations=None, # annotations for the spectrum in dictionary form {'thing to print':[x,y],} normrel=100., # the maximum value for normalization ipmol_kwargs={}, # IPMolecule keyword arguments **kwargs ): """ Plots and saves a publication quality mass spectrum with optional overlaid isotope patterns :param list realspec: A paired list of x and y values of the form ``[[x values],[y values]]`` :param dict simdict: This can either be a molecular formula to predict the isotope pattern of (string), a list of formulae, or a dictionary of the form ``simdict = {'formula1':{'colour':, 'alpha':float}, ...}``. If this is dictionary is left empty, no isotope patterns will be overlaid on the output spectrum. :param list mz: The *m/z* bounds for the output spectrum. Default: 'auto', but can be supplied with a tuple or list of length 2 of the form ``[x start, x end]``. :param str outname: Name of the file to be saved. :param str output: Save ('save') or show ('show') the figure. :param str simtype: The type for the isotope pattern simulation overlay. Options: 'bar' or 'gaussian'. :param str spectype: The type of spectrum being handed to the function. Options: 'continuum' or 'centroid'. :param float maxy: The maximum y value for the spectrum. Options: 'max' or specify a value :param bool norm: Normalize the spectrum. Options: bool :param str, float simnorm: Normalize the isotope pattern simulations to what value. Options: 'top', 'spec', or specify a value. Top will normalize the patterns to ``maxy``, and will only function if maxy is not 'max'. Spec will normalize the patterns to the maximum spectrum y value within the x bounds of the simulated pattern. Specifying a value will normalize all isotope patterns to that value. :param bool xlabel: Whether to show the label for the *m/z* axis. :param bool ylabel: Whether to show the y-axis label. :param bool xvalues: Whether to show the values of the x-axis. :param bool yvalues: Whether to show the values of the y-axis. :param bool showx: Whether to show the x-axis line. :param bool showy: Whether to show the y-axis line. :param bool offsetx: Whether to offset the x-axis slightly. Enabling this shows makes it easier to see low intensity peaks. :param int fs: Font size to use for labels. :param float lw: Line width for the plotted spectrum. :param float axwidth: Line width for the axes and tick marks. Default 1.5 :param bool simlabels: Whether to show the names of the simulated isotope patterns. The names will be exactly as supplied in ``simdict``. :param float bw: The width of the bar in *m/z* for bar isotope patterns. Options: 'auto' or float. This only has an affect if *simtype* is 'bar'. Auto make the bars equal to 2 times the full width at half max of the peak they are simulating. :param str specfont: The font to use for text in the plot. The specified font must be accepted by matplotlib. :param list size: The size in inches for the output figure. This must be a list of length 2 of the form ``[width,height]``. :param int dpiout: The dots per inch for the output figure. :param str exten: The file extension for the output figure. Options: 'png', 'svg', or other supported by matplotlib. :param float resolution: Override the auto-resolution calculation with a specified instrument resolution :param bool res_label: Whether to output the resolution of the spectrum onto the figure. :param bool delta: Whether to calculate and output the mass delta between the exact mass predicted by the isotope pattern simulation and the location of the maximum intensity within the bounds specified by *normwindow*. :param bool stats: Whether to calculate and output the goodness of fit between the predicted isotope pattern and the supplied spectrum. This functionality is still a work in progress. :param speccolour: The colour for the real spectrum , # colour for the spectrum to be plotted :param list padding: This allows the user to specify the subplot padding of the output figure. Options: 'auto' or list of the form ``[left,right,bottom,top]`` scalars. :param bool verbose: Verbose option for the script. Options: bool. :param float normwindow: The *m/z* window width within with too look for a maximum intensity value. This will only have an effect if *delta* is ``True``. Options: 'fwhm' for full width at half max or float. :param dict annotations: Annotations for the spectrum in dictionary form: ``{'thing to print':[x,y],}``. :param normrel: The maximum value for normalization. This can be used to globally set the top value for normalizing simulated isotope patterns. This is used most often to show the lack of an isotope pattern in the shown area. :param ipmol_kwargs: Keyword arguments to use for IPMolecule calls. See IPMolecule for more details. :param kwargs: catch for unused kwargs """ def checksimdict(dct): """ checks the type of simdict, converting to dictionary if necessary also checks for alpha and colour keys and adds them if necessary (defaulting to key @ 0.5) """ if type(dct) is not dict: if type(dct) is str: dct = {dct: {}} elif type(dct) is list or type(dct) is tuple: tdct = {} for i in dct: tdct[i] = {} dct = tdct for species in dct: if 'colour' not in dct[species]: dct[species]['colour'] = 'k' if 'alpha' not in dct[species]: dct[species]['alpha'] = 0.5 return dct if resolution is None: if spectype != 'centroid': resolution = autoresolution(realspec[0], realspec[1]) # calculate resolution else: resolution = 5000 simdict = checksimdict(simdict) # checks the simulation dictionary for species in simdict: # generate Molecule object and set x and y lists simdict[species]['colour'] = Colour(simdict[species]['colour']) simdict[species]['mol'] = IPMolecule( species, resolution=resolution, **ipmol_kwargs, ) # simdict[species]['mol'] = Molecule(species, res=res, dropmethod='threshold') if simtype == 'bar': simdict[species]['x'], simdict[species]['y'] = simdict[species]['mol'].barip if simtype == 'gaussian': simdict[species]['x'], simdict[species]['y'] = simdict[species]['mol'].gausip if mz == 'auto': # automatically determine m/z range if verbose is True: sys.stdout.write('Automatically determining m/z window') mz = [10000000, 0] for species in simdict: simdict[species]['bounds'] = simdict[species]['mol'].bounds # calculate bounds if simdict[species]['bounds'][0] < mz[0]: mz[0] = simdict[species]['bounds'][0] - 1 if simdict[species]['bounds'][1] > mz[1]: mz[1] = simdict[species]['bounds'][1] + 1 if mz == [10000000, 0]: mz = [min(realspec[0]), max(realspec[0])] if verbose is True: sys.stdout.write(': %i - %i\n' % (int(mz[0]), int(mz[1]))) sys.stdout.flush() realspec[0], realspec[1] = trimspectrum( # trim real spectrum for efficiency realspec[0], realspec[1], mz[0] - 1, mz[1] + 1 ) if len(realspec[0]) == 0: # catch for empty spectrum post-trim (usually user error) raise ValueError(f'There are no spectral values in the specified m/z bounds ({mz[0]}-{mz[1]}). Common causes: ' f'no values in the loaded spectrum within the window of interest, an error in specifying the ' f'molecule(s) to simulate') if norm is True: # normalize spectrum realspec[1] = normalize(realspec[1], normrel) for species in simdict: # normalize simulations if simnorm == 'spec': # normalize to maximum around exact mass if normwindow == 'fwhm': # if default, look within the full width at half max window = simdict[species]['mol'].fwhm else: # otherwise look within the specified value window = normwindow simdict[species]['y'] = normalize( simdict[species]['y'], localmax( realspec[0], realspec[1], simdict[species]['mol'].estimated_exact_mass, window ) ) elif simnorm == 'top': # normalize to top of the y value if maxy == 'max': raise ValueError('Simulations con only be normalized to the top of the spectrum when the maxy setting ' 'is a specific value') simdict[species]['y'] = normalize(simdict[species]['y'], maxy) elif type(simnorm) is int or type(simnorm) is float: # normalize to specified value simdict[species]['y'] = normalize(simdict[species]['y'], simnorm) if delta is True: if normwindow == 'fwhm': # if default, look within the full width at half max window = simdict[species]['mol'].fwhm else: # otherwise look within the specified value window = normwindow est = estimated_exact_mass( # try to calculate exact mass realspec[0], realspec[1], simdict[species]['mol'].estimated_exact_mass, simmin=simdict[species]['mol'].estimated_exact_mass, simmax=simdict[species]['mol'].estimated_exact_mass, lookwithin=window, # min(simdict[species]['x']), # max(simdict[species]['x']) ) if type(est) is float: simdict[species]['delta'] = '%.3f (%.1f ppm)' % ( simdict[species]['mol'].estimated_exact_mass - est, simdict[species]['mol'].compare_exact_mass(est)) else: simdict[species]['delta'] = est pl.clf() # clear and close figure if open pl.close() fig = pl.figure(figsize=tuple(size)) ax = fig.add_subplot(111) ax.spines["right"].set_visible(False) # hide right and top spines ax.spines["top"].set_visible(False) if showx is False: ax.spines["bottom"].set_visible(False) # hide bottom axis if showy is False: ax.spines["left"].set_visible(False) # hide left axis for axis in ["top", "bottom", "left", "right"]: ax.spines[axis].set_linewidth(axwidth) if offsetx is True: # offset x axis ax.spines["bottom"].set_position(('axes', -0.01)) font = {'fontname': specfont, 'fontsize': fs} # font parameters for axis/text labels tickfont = pl.matplotlib.font_manager.FontProperties( # font parameters for axis ticks family=specfont, size=fs ) ax.set_xlim(mz) # set x bounds if maxy == 'max': # set y bounds ax.set_ylim(0., max(realspec[1])) top = max(realspec[1]) elif type(maxy) is int or type(maxy) is float: ax.set_ylim(0., maxy) top = maxy if simtype == 'bar': # generates zeros for bottom of bars (assumes m/z spacing is equal between patterns) for species in simdict: simdict[species]['zero'] = [] for i in simdict[species]['x']: simdict[species]['zero'].append(0.) for species in simdict: # for each species for subsp in simdict: # look at all the species if subsp != species: # if it is not itself ins = bisect_left(simdict[subsp]['x'], simdict[species]['x'][-1]) # look for insertion point if 0 < ins < len(simdict[subsp]['x']): # if species highest m/z is inside subsp list for i in range(ins): # add intensity of species to subsp zeros # used -ins+i-1 to fix an error, with any luck this won't break it next time simdict[subsp]['zero'][i] += simdict[species]['y'][-ins + i] # include resolution if specified (and spectrum is not centroid) if res_label is True and spectype != 'centroid': ax.text( mz[1], top * 0.95, f'resolution: {str(round(resolution))}', horizontalalignment='right', **font ) for species in simdict: # plot and label bars if simtype == 'bar': if bw == 'auto': bw = simdict[species]['mol'].fwhm * 2 else: bw = bw ax.bar( simdict[species]['x'], simdict[species]['y'], bw, alpha=simdict[species]['alpha'], color=simdict[species]['colour'].mpl, linewidth=0, align='center', bottom=simdict[species]['zero'] ) elif simtype == 'gaussian': ax.plot( simdict[species]['x'], simdict[species]['y'], alpha=simdict[species]['alpha'], color=simdict[species]['colour'].mpl, linewidth=lw ) ax.fill_between( simdict[species]['x'], 0, simdict[species]['y'], alpha=simdict[species]['alpha'], color=simdict[species]['colour'].mpl, linewidth=0 ) # if any labels are to be shown if simlabels is True or stats is True or delta is True: string = '' bpi = simdict[species]['y'].index(max(simdict[species]['y'])) # index of base peak if simlabels is True: # species name string += species if stats is True or delta is True: # add return if SER or delta is called for string += '\n' if stats is True: # standard error of regression string += f'SER: {simdict[species]["mol"].compare(realspec)} ' if delta is True: # mass delta string += f'mass delta: {simdict[species]["delta"]}' ax.text( simdict[species]['x'][bpi], top * 1.01, string, color=simdict[species]['colour'].mpl, horizontalalignment='center', **font ) if spectype == 'continuum': ax.plot( realspec[0], realspec[1], linewidth=lw, color=Colour(speccolour).mpl ) elif spectype == 'centroid': dist = [] for ind, val in enumerate(realspec[0]): # find distance between all adjacent m/z values if ind == 0: continue dist.append(realspec[0][ind] - realspec[0][ind - 1]) dist = sum(dist) / len(dist) # average distance ax.bar( realspec[0], realspec[1], dist * 0.75, linewidth=0, color=Colour(speccolour).mpl, align='center', alpha=0.8 ) if annotations is not None: for label in annotations: ax.text( annotations[label][0], annotations[label][1], label, horizontalalignment='center', **font ) # show or hide axis values/labels as specified if yvalues is False: # y tick marks and values ax.tick_params(axis='y', labelleft='off', length=0) else: # y value labels ax.tick_params( axis='y', length=axwidth * 3, width=axwidth, direction='out', right=False, ) for label in ax.get_yticklabels(): label.set_fontproperties(tickfont) if ylabel is True: # y unit if top == 100: # normalized ax.set_ylabel('relative intensity', **font) else: # set to counts ax.set_ylabel('intensity (counts)', **font) if xvalues is False: # x tick marks and values ax.tick_params(axis='x', labelbottom='off', length=0) else: # x value labels ax.tick_params( axis='x', length=axwidth * 3, width=axwidth, direction='out', top=False, ) for label in ax.get_xticklabels(): label.set_fontproperties(tickfont) if xlabel is True: # x unit ax.set_xlabel('m/z', style='italic', **font) pl.ticklabel_format(useOffset=False) # don't use the stupid shorthand thing if padding == 'auto': pl.tight_layout(pad=0.5) # adjust subplots if simlabels is True or stats is True or delta is True: pl.subplots_adjust(top=0.90) # lower top if details are called for elif type(padding) is list and len(padding) == 4: pl.subplots_adjust( left=padding[0], right=padding[1], bottom=padding[2], top=padding[3] ) if output == 'save': # save figure outname = '' # generate tag for filenaming for species in simdict: outname += ' ' + species outname = outname + outname + '.' + exten pl.savefig( outname, dpi=dpiout, format=exten, transparent=True ) if verbose is True: sys.stdout.write('Saved figure as:\n"%s"\nin the working directory' % outname) elif output == 'show': # show figure pl.show() def plotuv(wavelengths, intensities, **kwargs): """ Plots and saves a publication quality UV-Vis figure. **Parameters** wavelengths: *list* A list of wavelengths intensities: *list* A list of intensity values paired by index to *wavelengths* **Returns** return item: ``None`` This function has no pythonic return. **\*\*kwargs** axwidth: 1.5 Line width for the axes and tick marks. Options: float. colours: A list of colours to be used if the fuction is supplied with multiple traces. dpiout: 300 The dots per inch for the output figure. Options: integer. exten: 'png' The file extension for the output figure. Options: 'png', 'svg', or other supported by matplotlib. fs: 16 Font size to use for labels. Options: integer or float. legloc: 0 The matplotlib legend location key. See http://matplotlib.org/api/legend_api.html for location codes. lw: 1.5 Line width for the plotted spectrum. Options: float. outname: 'UV-Vis spectrum' Name for the output file. Options: string. output: 'save' Save ('save') or show ('show') the figure. padding: 'auto' This allows the user to specify the subplot padding of the output figure. Options: 'auto' or list of the form ``[left,right,bottom,top]`` scalars. size: [7.87,4.87] The size in inches for the output figure. This must be a list of length 2 of the form ``[width,height]``. specfont: 'Arial' The font to use for text in the plot. The specified font must be accepted by matplotlib. times: None A list of timepoints for each provided trace. These are used as labels in the legend. verbose: True Verbose option for the script. Options: bool. xrange: None The limits for the x axis. Options None or ``[x min,x max]`` yrange: None The limits for the y axis. Options None or ``[y min,y max]`` """ settings = { # default settings for the function 'outname': 'UV-Vis spectrum', # name for the output file 'fs': 16, # font size 'lw': 1.5, # line width for the plotted spectrum 'axwidth': 1.5, # axis width 'size': [7.87, 4.87], # size in inches for the figure 'dpiout': 300, # dpi for the output figure 'exten': 'png', # extension for the output figure 'specfont': 'Arial', # the font for text in the plot # colours to use for multiple traces in the same spectrum (feel free to specify your own) 'colours': ['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a', '#ffff99', '#8dd3c7', '#ffffb3', '#bebada', '#fb8072', '#80b1d3', '#fdb462', '#b3de69', '#fccde5', '#d9d9d9', '#bc80bd', '#ccebc5', ], 'xrange': None, # the limits for the x axis 'yrange': None, # the limits for the y axis 'times': None, # time points for each provided trace (for legend labels) 'output': 'save', # 'save' or 'show' the figure 'padding': None, # padding for the output plot 'verbose': True, # chatty 'legloc': 0, # legend location (see http://matplotlib.org/api/legend_api.html location codes) } if set(kwargs.keys()) - set(settings.keys()): # check for invalid keyword arguments string = '' for i in set(kwargs.keys()) - set(settings.keys()): string += ' %s' % i raise KeyError('Unsupported keyword argument(s): %s' % string) settings.update(kwargs) # update settings from keyword arguments pl.clf() # clear and close figure if open pl.close() fig = pl.figure(figsize=tuple(settings['size'])) ax = fig.add_subplot(111) ax.spines["right"].set_visible(False) # hide right and top spines ax.spines["top"].set_visible(False) font = {'fontname': settings['specfont'], 'fontsize': settings['fs']} # font parameters for axis/text labels tickfont = pl.matplotlib.font_manager.FontProperties(family=settings['specfont'], size=settings['fs']) # font parameters for axis ticks if type(intensities[0]) is float: # if the function has only been handed a single spectrum intensities = [intensities] # determine and set limits for axes if settings['xrange'] is None: # auto determine x limits settings['xrange'] = [min(wavelengths), max(wavelengths)] if settings['yrange'] is None: # auto determine y limits settings['yrange'] = [0, 0] for spec in intensities: if max(spec) > settings['yrange'][1]: settings['yrange'][1] = max(spec) ax.set_xlim(settings['xrange']) # set x bounds ax.set_ylim(settings['yrange']) # set y bounds # apply font and tick parameters to axes ax.tick_params(axis='x', length=settings['axwidth'] * 3, width=settings['axwidth'], direction='out', top='off') for label in ax.get_xticklabels(): label.set_fontproperties(tickfont) ax.tick_params(axis='y', length=settings['axwidth'] * 3, width=settings['axwidth'], direction='out', right='off') for label in ax.get_yticklabels(): label.set_fontproperties(tickfont) for axis in ["top", "bottom", "left", "right"]: ax.spines[axis].set_linewidth(settings['axwidth']) if settings['times'] is not None: if len(settings['times']) != len(intensities): raise IndexError('The numer of times provided do not match the number of traces provided.') for ind, spec in enumerate(intensities): # plot traces if settings['times'] is not None: string = 't = ' + str(round(settings['times'][ind], 1)) + 'm' ax.plot(wavelengths, spec, label=string, color=Colour(settings['colours'][ind]).mpl, linewidth=settings['lw']) else: ax.plot(wavelengths, spec, color=Colour(settings['colours'][ind]).mpl, linewidth=settings['lw']) if settings['times'] is not None: ax.legend(loc=0, frameon=False) ax.set_xlabel('wavelength (nm)', **font) ax.set_ylabel('absorbance (a.u.)', **font) if settings['padding'] is None: pl.tight_layout(pad=0.5) # adjust subplots elif type(settings['padding']) is list and len(settings['padding']) == 4: pl.subplots_adjust(left=settings['padding'][0], right=settings['padding'][1], bottom=settings['padding'][2], top=settings['padding'][3]) if settings['output'] == 'save': # save figure outname = settings['outname'] + '.' + settings['exten'] pl.savefig(outname, dpi=settings['dpiout'], format=settings['exten'], transparent=True) if settings['verbose'] is True: sys.stdout.write('Saved figure as:\n"%s"\nin the working directory' % outname) elif settings['output'] == 'show': # show figure pl.show() def sigmafwhm(res, x): """ Calculates the full width at half max and standard deviation for a spectrum peak. **Parameters** res: *float* The resolution of the peak in question x: *float* The x value of the peak in question **Returns** fwhm: *float* The full width at half max of the peak. sigma: *float* The standard deviation of the peak. """ fwhm = x / res sigma = fwhm / (2 * np.sqrt(2 * np.log(2))) # based on the equation FWHM = 2*sqrt(2ln2)*sigma return fwhm, sigma def strtolist(string): """ Converts a string to a list with more flexibility than ``string.split()`` by looking for both brackets of type ``(,),[,],{,}`` and commas. **Parameters** string: *string* The string to be split. **Returns** split list: *list* The split list **Examples** :: >>> strtolist('[(12.3,15,256.128)]') [12.3, 15, 256.128] """ out = [] temp = '' brackets = ['(', ')', '[', ']', '{', '}'] for char in list(string): if char not in brackets and char != ',': temp += char if char == ',': try: out.append(int(temp)) except ValueError: out.append(float(temp)) temp = '' if len(temp) != 0: # if there is a weird ending character try: out.append(int(temp)) except ValueError: out.append(float(temp)) return out