NucleoSpec / lib /pythoms /tome.py
viviandlin
Initial release of NucleoSpec
33ba306
Raw
History Blame Contribute Delete
47 kB
"""
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':<hex or name or RGB tuple>, '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